Ver código fonte

feat(extract-service): 补充模板系统欠缺的逻辑

新增 GenerationService 和 GenerationController:
- 生成任务 CRUD: create, get, list, delete
- 执行相关: execute, getProgress, confirm
- 变量值管理: updateVariableValue
- 下载: download

补充接口:
- 变量预览提取: POST /templates/variables/{id}/preview
- 来源文件重排序: POST /templates/{id}/source-files/reorder
- 变量重排序: POST /templates/{id}/variables/reorder

新增 DTO:
- CreateGenerationRequest
- UpdateVariableValueRequest
- ReorderRequest
- GenerationResponse
- GenerationProgressResponse
- VariablePreviewResponse

完善 Entity:
- Generation.VariableValue 增加 variableName, displayName 字段
- Generation 增加 STATUS_GENERATING 状态常量
何文松 1 mês atrás
pai
commit
5e75dc3e7b

+ 184 - 0
backend/extract-service/src/main/java/com/lingyue/extract/controller/GenerationController.java

@@ -0,0 +1,184 @@
+package com.lingyue.extract.controller;
+
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.lingyue.common.domain.AjaxResult;
+import com.lingyue.extract.dto.request.CreateGenerationRequest;
+import com.lingyue.extract.dto.request.GenerationProgressResponse;
+import com.lingyue.extract.dto.request.UpdateVariableValueRequest;
+import com.lingyue.extract.dto.response.GenerationResponse;
+import com.lingyue.extract.entity.Generation;
+import com.lingyue.extract.service.GenerationService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 生成任务控制器
+ * 
+ * @author lingyue
+ * @since 2026-01-24
+ */
+@Slf4j
+@RestController
+@RequestMapping("/api/v1/generations")
+@RequiredArgsConstructor
+@Tag(name = "生成任务管理", description = "报告生成任务 CRUD 和执行接口")
+public class GenerationController {
+    
+    private final GenerationService generationService;
+    
+    // TODO: 从认证上下文获取用户ID
+    private String getCurrentUserId() {
+        return "test-user-001";
+    }
+    
+    // ==================== 生成任务 CRUD ====================
+    
+    @PostMapping
+    @Operation(summary = "创建生成任务", description = "创建新的报告生成任务")
+    public AjaxResult<?> createGeneration(@Valid @RequestBody CreateGenerationRequest request) {
+        try {
+            String userId = getCurrentUserId();
+            Generation generation = generationService.create(userId, request);
+            return AjaxResult.success("创建成功", GenerationResponse.fromEntity(generation));
+        } catch (Exception e) {
+            log.error("创建生成任务失败", e);
+            return AjaxResult.error("创建失败: " + e.getMessage());
+        }
+    }
+    
+    @GetMapping("/{id}")
+    @Operation(summary = "获取生成任务详情", description = "获取生成任务详细信息,包含变量值")
+    public AjaxResult<?> getGeneration(
+            @Parameter(description = "任务ID") @PathVariable String id) {
+        GenerationResponse response = generationService.getDetail(id);
+        if (response == null) {
+            return AjaxResult.error("生成任务不存在");
+        }
+        return AjaxResult.success(response);
+    }
+    
+    @GetMapping
+    @Operation(summary = "获取生成任务列表", description = "分页获取当前用户的生成任务列表")
+    public AjaxResult<?> listGenerations(
+            @Parameter(description = "页码") @RequestParam(defaultValue = "1") int page,
+            @Parameter(description = "每页数量") @RequestParam(defaultValue = "20") int size) {
+        String userId = getCurrentUserId();
+        Page<GenerationResponse> result = generationService.listByUserId(userId, page, size);
+        return AjaxResult.success(result);
+    }
+    
+    @GetMapping("/recent")
+    @Operation(summary = "获取最近的生成任务", description = "获取用户最近的生成任务")
+    public AjaxResult<?> getRecentGenerations(
+            @Parameter(description = "数量") @RequestParam(defaultValue = "10") int limit) {
+        String userId = getCurrentUserId();
+        List<GenerationResponse> result = generationService.getRecentByUserId(userId, limit);
+        return AjaxResult.success(result);
+    }
+    
+    @GetMapping("/template/{templateId}")
+    @Operation(summary = "按模板获取生成任务", description = "获取指定模板的所有生成任务")
+    public AjaxResult<?> listByTemplate(
+            @Parameter(description = "模板ID") @PathVariable String templateId) {
+        List<GenerationResponse> result = generationService.listByTemplateId(templateId);
+        return AjaxResult.success(result);
+    }
+    
+    @DeleteMapping("/{id}")
+    @Operation(summary = "删除生成任务", description = "删除生成任务")
+    public AjaxResult<?> deleteGeneration(
+            @Parameter(description = "任务ID") @PathVariable String id) {
+        try {
+            boolean success = generationService.delete(id);
+            if (!success) {
+                return AjaxResult.error("生成任务不存在");
+            }
+            return AjaxResult.success("删除成功");
+        } catch (Exception e) {
+            log.error("删除生成任务失败", e);
+            return AjaxResult.error("删除失败: " + e.getMessage());
+        }
+    }
+    
+    // ==================== 执行相关 ====================
+    
+    @PostMapping("/{id}/execute")
+    @Operation(summary = "执行提取", description = "开始执行变量提取")
+    public AjaxResult<?> executeGeneration(
+            @Parameter(description = "任务ID") @PathVariable String id) {
+        try {
+            Generation generation = generationService.execute(id);
+            return AjaxResult.success("开始执行", GenerationResponse.fromEntity(generation));
+        } catch (Exception e) {
+            log.error("执行生成任务失败", e);
+            return AjaxResult.error("执行失败: " + e.getMessage());
+        }
+    }
+    
+    @GetMapping("/{id}/progress")
+    @Operation(summary = "获取执行进度", description = "获取生成任务的执行进度")
+    public AjaxResult<?> getProgress(
+            @Parameter(description = "任务ID") @PathVariable String id) {
+        GenerationProgressResponse progress = generationService.getProgress(id);
+        if (progress == null) {
+            return AjaxResult.error("生成任务不存在");
+        }
+        return AjaxResult.success(progress);
+    }
+    
+    @PutMapping("/{id}/variables/{variableName}")
+    @Operation(summary = "更新变量值", description = "手动修改生成任务中的变量值")
+    public AjaxResult<?> updateVariableValue(
+            @Parameter(description = "任务ID") @PathVariable String id,
+            @Parameter(description = "变量名") @PathVariable String variableName,
+            @Valid @RequestBody UpdateVariableValueRequest request) {
+        try {
+            Generation generation = generationService.updateVariableValue(id, variableName, request.getValue());
+            return AjaxResult.success("更新成功", GenerationResponse.fromEntity(generation));
+        } catch (Exception e) {
+            log.error("更新变量值失败", e);
+            return AjaxResult.error("更新失败: " + e.getMessage());
+        }
+    }
+    
+    @PostMapping("/{id}/confirm")
+    @Operation(summary = "确认并生成文档", description = "确认变量值并生成最终文档")
+    public AjaxResult<?> confirmGeneration(
+            @Parameter(description = "任务ID") @PathVariable String id) {
+        try {
+            Generation generation = generationService.confirm(id);
+            return AjaxResult.success("确认成功,开始生成文档", GenerationResponse.fromEntity(generation));
+        } catch (Exception e) {
+            log.error("确认生成任务失败", e);
+            return AjaxResult.error("确认失败: " + e.getMessage());
+        }
+    }
+    
+    @GetMapping("/{id}/download")
+    @Operation(summary = "下载生成的文档", description = "下载生成的报告文档")
+    public AjaxResult<?> downloadGeneration(
+            @Parameter(description = "任务ID") @PathVariable String id) {
+        Generation generation = generationService.getById(id);
+        if (generation == null) {
+            return AjaxResult.error("生成任务不存在");
+        }
+        
+        if (!Generation.STATUS_COMPLETED.equals(generation.getStatus())) {
+            return AjaxResult.error("文档尚未生成完成");
+        }
+        
+        if (generation.getOutputFilePath() == null) {
+            return AjaxResult.error("输出文件不存在");
+        }
+        
+        // TODO: 返回文件下载链接或流
+        return AjaxResult.success("下载链接", generation.getOutputFilePath());
+    }
+}

+ 44 - 0
backend/extract-service/src/main/java/com/lingyue/extract/controller/TemplateController.java

@@ -330,4 +330,48 @@ public class TemplateController {
             return AjaxResult.error("删除失败: " + e.getMessage());
         }
     }
+    
+    @PostMapping("/variables/{id}/preview")
+    @Operation(summary = "预览变量提取结果", description = "使用指定文档测试变量提取配置")
+    public AjaxResult<?> previewVariable(
+            @Parameter(description = "变量ID") @PathVariable String id,
+            @Parameter(description = "测试文档ID") @RequestParam String documentId) {
+        try {
+            var result = variableService.preview(id, documentId);
+            return AjaxResult.success(result);
+        } catch (Exception e) {
+            log.error("预览变量提取失败", e);
+            return AjaxResult.error("预览失败: " + e.getMessage());
+        }
+    }
+    
+    // ==================== 重排序接口 ====================
+    
+    @PostMapping("/{templateId}/source-files/reorder")
+    @Operation(summary = "重排序来源文件", description = "调整来源文件的显示顺序")
+    public AjaxResult<?> reorderSourceFiles(
+            @Parameter(description = "模板ID") @PathVariable String templateId,
+            @Valid @RequestBody com.lingyue.extract.dto.request.ReorderRequest request) {
+        try {
+            sourceFileService.reorder(templateId, request.getOrderedIds());
+            return AjaxResult.success("重排序成功");
+        } catch (Exception e) {
+            log.error("重排序来源文件失败", e);
+            return AjaxResult.error("重排序失败: " + e.getMessage());
+        }
+    }
+    
+    @PostMapping("/{templateId}/variables/reorder")
+    @Operation(summary = "重排序变量", description = "调整变量的显示顺序")
+    public AjaxResult<?> reorderVariables(
+            @Parameter(description = "模板ID") @PathVariable String templateId,
+            @Valid @RequestBody com.lingyue.extract.dto.request.ReorderRequest request) {
+        try {
+            variableService.reorder(templateId, request.getOrderedIds());
+            return AjaxResult.success("重排序成功");
+        } catch (Exception e) {
+            log.error("重排序变量失败", e);
+            return AjaxResult.error("重排序失败: " + e.getMessage());
+        }
+    }
 }

+ 30 - 0
backend/extract-service/src/main/java/com/lingyue/extract/dto/request/CreateGenerationRequest.java

@@ -0,0 +1,30 @@
+package com.lingyue.extract.dto.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.Map;
+
+/**
+ * 创建生成任务请求
+ * 
+ * @author lingyue
+ * @since 2026-01-24
+ */
+@Data
+@Schema(description = "创建生成任务请求")
+public class CreateGenerationRequest {
+    
+    @NotBlank(message = "模板ID不能为空")
+    @Schema(description = "模板ID", required = true)
+    private String templateId;
+    
+    @Schema(description = "任务名称")
+    private String name;
+    
+    @NotNull(message = "来源文件映射不能为空")
+    @Schema(description = "来源文件映射,key为来源文件别名,value为文档ID", required = true)
+    private Map<String, String> sourceFileMap;
+}

+ 22 - 0
backend/extract-service/src/main/java/com/lingyue/extract/dto/request/ReorderRequest.java

@@ -0,0 +1,22 @@
+package com.lingyue.extract.dto.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 重排序请求
+ * 
+ * @author lingyue
+ * @since 2026-01-24
+ */
+@Data
+@Schema(description = "重排序请求")
+public class ReorderRequest {
+    
+    @NotNull(message = "ID列表不能为空")
+    @Schema(description = "按新顺序排列的ID列表", required = true)
+    private List<String> orderedIds;
+}

+ 23 - 0
backend/extract-service/src/main/java/com/lingyue/extract/dto/request/UpdateVariableValueRequest.java

@@ -0,0 +1,23 @@
+package com.lingyue.extract.dto.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotBlank;
+import lombok.Data;
+
+/**
+ * 更新变量值请求
+ * 
+ * @author lingyue
+ * @since 2026-01-24
+ */
+@Data
+@Schema(description = "更新变量值请求")
+public class UpdateVariableValueRequest {
+    
+    @NotBlank(message = "变量值不能为空")
+    @Schema(description = "变量值", required = true)
+    private String value;
+    
+    @Schema(description = "是否手动修改")
+    private Boolean manual = true;
+}

+ 48 - 0
backend/extract-service/src/main/java/com/lingyue/extract/dto/response/GenerationProgressResponse.java

@@ -0,0 +1,48 @@
+package com.lingyue.extract.dto.request;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 生成任务进度响应
+ * 
+ * @author lingyue
+ * @since 2026-01-24
+ */
+@Data
+@Schema(description = "生成任务进度响应")
+public class GenerationProgressResponse {
+    
+    @Schema(description = "任务ID")
+    private String id;
+    
+    @Schema(description = "状态")
+    private String status;
+    
+    @Schema(description = "进度 0-100")
+    private Integer progress;
+    
+    @Schema(description = "变量总数")
+    private Integer total;
+    
+    @Schema(description = "已完成数")
+    private Integer completed;
+    
+    @Schema(description = "当前处理的变量名")
+    private String currentVariable;
+    
+    @Schema(description = "消息")
+    private String message;
+    
+    public static GenerationProgressResponse of(String id, String status, int progress, 
+                                                  int total, int completed, String currentVariable) {
+        GenerationProgressResponse response = new GenerationProgressResponse();
+        response.setId(id);
+        response.setStatus(status);
+        response.setProgress(progress);
+        response.setTotal(total);
+        response.setCompleted(completed);
+        response.setCurrentVariable(currentVariable);
+        return response;
+    }
+}

+ 100 - 0
backend/extract-service/src/main/java/com/lingyue/extract/dto/response/GenerationResponse.java

@@ -0,0 +1,100 @@
+package com.lingyue.extract.dto.response;
+
+import com.lingyue.extract.entity.Generation;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.Date;
+import java.util.Map;
+
+/**
+ * 生成任务响应
+ * 
+ * @author lingyue
+ * @since 2026-01-24
+ */
+@Data
+@Schema(description = "生成任务响应")
+public class GenerationResponse {
+    
+    @Schema(description = "任务ID")
+    private String id;
+    
+    @Schema(description = "模板ID")
+    private String templateId;
+    
+    @Schema(description = "模板名称")
+    private String templateName;
+    
+    @Schema(description = "用户ID")
+    private String userId;
+    
+    @Schema(description = "任务名称")
+    private String name;
+    
+    @Schema(description = "来源文件映射")
+    private Map<String, String> sourceFileMap;
+    
+    @Schema(description = "变量值")
+    private Map<String, Generation.VariableValue> variableValues;
+    
+    @Schema(description = "输出文档ID")
+    private String outputDocumentId;
+    
+    @Schema(description = "输出文件路径")
+    private String outputFilePath;
+    
+    @Schema(description = "状态: pending/extracting/review/generating/completed/error")
+    private String status;
+    
+    @Schema(description = "错误信息")
+    private String errorMessage;
+    
+    @Schema(description = "进度 0-100")
+    private Integer progress;
+    
+    @Schema(description = "创建时间")
+    private Date createTime;
+    
+    @Schema(description = "完成时间")
+    private Date completedAt;
+    
+    // ==================== 统计信息 ====================
+    
+    @Schema(description = "变量总数")
+    private Integer totalVariables;
+    
+    @Schema(description = "已提取变量数")
+    private Integer extractedVariables;
+    
+    /**
+     * 从实体转换
+     */
+    public static GenerationResponse fromEntity(Generation generation) {
+        GenerationResponse response = new GenerationResponse();
+        response.setId(generation.getId());
+        response.setTemplateId(generation.getTemplateId());
+        response.setUserId(generation.getUserId());
+        response.setName(generation.getName());
+        response.setSourceFileMap(generation.getSourceFileMap());
+        response.setVariableValues(generation.getVariableValues());
+        response.setOutputDocumentId(generation.getOutputDocumentId());
+        response.setOutputFilePath(generation.getOutputFilePath());
+        response.setStatus(generation.getStatus());
+        response.setErrorMessage(generation.getErrorMessage());
+        response.setProgress(generation.getProgress());
+        response.setCreateTime(generation.getCreateTime());
+        response.setCompletedAt(generation.getCompletedAt());
+        
+        // 统计变量
+        if (generation.getVariableValues() != null) {
+            response.setTotalVariables(generation.getVariableValues().size());
+            long extracted = generation.getVariableValues().values().stream()
+                    .filter(v -> v.getValue() != null && !v.getValue().isEmpty())
+                    .count();
+            response.setExtractedVariables((int) extracted);
+        }
+        
+        return response;
+    }
+}

+ 60 - 0
backend/extract-service/src/main/java/com/lingyue/extract/dto/response/VariablePreviewResponse.java

@@ -0,0 +1,60 @@
+package com.lingyue.extract.dto.response;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+/**
+ * 变量预览提取响应
+ * 
+ * @author lingyue
+ * @since 2026-01-24
+ */
+@Data
+@Schema(description = "变量预览提取响应")
+public class VariablePreviewResponse {
+    
+    @Schema(description = "变量ID")
+    private String variableId;
+    
+    @Schema(description = "变量名")
+    private String variableName;
+    
+    @Schema(description = "提取的值")
+    private String extractedValue;
+    
+    @Schema(description = "置信度 0-1")
+    private Double confidence;
+    
+    @Schema(description = "来源预览文本")
+    private String sourcePreview;
+    
+    @Schema(description = "来源位置描述")
+    private String sourceLocation;
+    
+    @Schema(description = "是否成功")
+    private Boolean success;
+    
+    @Schema(description = "错误信息")
+    private String errorMessage;
+    
+    public static VariablePreviewResponse success(String variableId, String variableName, 
+                                                    String value, Double confidence, String sourcePreview) {
+        VariablePreviewResponse response = new VariablePreviewResponse();
+        response.setVariableId(variableId);
+        response.setVariableName(variableName);
+        response.setExtractedValue(value);
+        response.setConfidence(confidence);
+        response.setSourcePreview(sourcePreview);
+        response.setSuccess(true);
+        return response;
+    }
+    
+    public static VariablePreviewResponse error(String variableId, String variableName, String errorMessage) {
+        VariablePreviewResponse response = new VariablePreviewResponse();
+        response.setVariableId(variableId);
+        response.setVariableName(variableName);
+        response.setSuccess(false);
+        response.setErrorMessage(errorMessage);
+        return response;
+    }
+}

+ 9 - 2
backend/extract-service/src/main/java/com/lingyue/extract/entity/Generation.java

@@ -94,6 +94,7 @@ public class Generation {
     public static final String STATUS_PENDING = "pending";
     public static final String STATUS_EXTRACTING = "extracting";
     public static final String STATUS_REVIEW = "review";
+    public static final String STATUS_GENERATING = "generating";
     public static final String STATUS_COMPLETED = "completed";
     public static final String STATUS_ERROR = "error";
     
@@ -103,6 +104,12 @@ public class Generation {
     @Data
     public static class VariableValue {
         
+        @Schema(description = "变量名")
+        private String variableName;
+        
+        @Schema(description = "显示名称")
+        private String displayName;
+        
         @Schema(description = "提取的值")
         private String value;
         
@@ -112,7 +119,7 @@ public class Generation {
         @Schema(description = "来源内容预览")
         private String sourcePreview;
         
-        @Schema(description = "状态: pending-待提取, extracted-已提取, modified-已修改, error-错误")
+        @Schema(description = "状态: pending-待提取, extracted-已提取, manual-手动修改, error-错误")
         private String status;
         
         @Schema(description = "错误信息")
@@ -121,7 +128,7 @@ public class Generation {
         // 状态常量
         public static final String STATUS_PENDING = "pending";
         public static final String STATUS_EXTRACTED = "extracted";
-        public static final String STATUS_MODIFIED = "modified";
+        public static final String STATUS_MANUAL = "manual";
         public static final String STATUS_ERROR = "error";
     }
 }

+ 334 - 0
backend/extract-service/src/main/java/com/lingyue/extract/service/GenerationService.java

@@ -0,0 +1,334 @@
+package com.lingyue.extract.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.lingyue.extract.dto.request.CreateGenerationRequest;
+import com.lingyue.extract.dto.request.GenerationProgressResponse;
+import com.lingyue.extract.dto.response.GenerationResponse;
+import com.lingyue.extract.entity.Generation;
+import com.lingyue.extract.entity.SourceFile;
+import com.lingyue.extract.entity.Template;
+import com.lingyue.extract.entity.Variable;
+import com.lingyue.extract.repository.GenerationRepository;
+import com.lingyue.extract.repository.SourceFileRepository;
+import com.lingyue.extract.repository.TemplateRepository;
+import com.lingyue.extract.repository.VariableRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+import java.util.stream.Collectors;
+
+/**
+ * 生成任务服务
+ * 
+ * @author lingyue
+ * @since 2026-01-24
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class GenerationService {
+    
+    private final GenerationRepository generationRepository;
+    private final TemplateRepository templateRepository;
+    private final SourceFileRepository sourceFileRepository;
+    private final VariableRepository variableRepository;
+    
+    /**
+     * 创建生成任务
+     */
+    @Transactional
+    public Generation create(String userId, CreateGenerationRequest request) {
+        // 验证模板
+        Template template = templateRepository.selectById(request.getTemplateId());
+        if (template == null) {
+            throw new RuntimeException("模板不存在");
+        }
+        if (!Template.STATUS_PUBLISHED.equals(template.getStatus())) {
+            throw new RuntimeException("模板未发布,无法使用");
+        }
+        
+        // 验证来源文件
+        List<SourceFile> requiredFiles = sourceFileRepository.findRequiredByTemplateId(request.getTemplateId());
+        for (SourceFile sf : requiredFiles) {
+            if (!request.getSourceFileMap().containsKey(sf.getAlias())) {
+                throw new RuntimeException("缺少必须的来源文件: " + sf.getAlias());
+            }
+        }
+        
+        // 获取变量列表,初始化变量值
+        List<Variable> variables = variableRepository.findByTemplateId(request.getTemplateId());
+        Map<String, Generation.VariableValue> variableValues = new HashMap<>();
+        for (Variable v : variables) {
+            Generation.VariableValue vv = new Generation.VariableValue();
+            vv.setVariableName(v.getName());
+            vv.setDisplayName(v.getDisplayName());
+            vv.setStatus(Generation.VariableValue.STATUS_PENDING);
+            variableValues.put(v.getName(), vv);
+        }
+        
+        // 创建生成任务
+        Generation generation = new Generation();
+        generation.setId(UUID.randomUUID().toString().replace("-", ""));
+        generation.setTemplateId(request.getTemplateId());
+        generation.setUserId(userId);
+        generation.setName(request.getName() != null ? request.getName() : template.getName() + " - " + new Date());
+        generation.setSourceFileMap(request.getSourceFileMap());
+        generation.setVariableValues(variableValues);
+        generation.setStatus(Generation.STATUS_PENDING);
+        generation.setProgress(0);
+        generation.setCreateTime(new Date());
+        
+        generationRepository.insert(generation);
+        
+        // 增加模板使用次数
+        templateRepository.incrementUseCount(request.getTemplateId());
+        
+        log.info("创建生成任务成功: id={}, templateId={}, userId={}", 
+                generation.getId(), request.getTemplateId(), userId);
+        
+        return generation;
+    }
+    
+    /**
+     * 根据ID获取
+     */
+    public Generation getById(String id) {
+        return generationRepository.selectById(id);
+    }
+    
+    /**
+     * 获取任务详情(包含模板名称)
+     */
+    public GenerationResponse getDetail(String id) {
+        Generation generation = generationRepository.selectById(id);
+        if (generation == null) {
+            return null;
+        }
+        
+        GenerationResponse response = GenerationResponse.fromEntity(generation);
+        
+        // 填充模板名称
+        Template template = templateRepository.selectById(generation.getTemplateId());
+        if (template != null) {
+            response.setTemplateName(template.getName());
+        }
+        
+        return response;
+    }
+    
+    /**
+     * 分页查询用户的生成任务
+     */
+    public Page<GenerationResponse> listByUserId(String userId, int page, int size) {
+        Page<Generation> pageRequest = new Page<>(page, size);
+        
+        LambdaQueryWrapper<Generation> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Generation::getUserId, userId)
+               .orderByDesc(Generation::getCreateTime);
+        
+        Page<Generation> generationPage = generationRepository.selectPage(pageRequest, wrapper);
+        
+        // 转换为响应对象
+        Page<GenerationResponse> responsePage = new Page<>(page, size, generationPage.getTotal());
+        List<GenerationResponse> records = generationPage.getRecords().stream()
+                .map(generation -> {
+                    GenerationResponse response = GenerationResponse.fromEntity(generation);
+                    // 填充模板名称
+                    Template template = templateRepository.selectById(generation.getTemplateId());
+                    if (template != null) {
+                        response.setTemplateName(template.getName());
+                    }
+                    return response;
+                })
+                .collect(Collectors.toList());
+        responsePage.setRecords(records);
+        
+        return responsePage;
+    }
+    
+    /**
+     * 按模板查询生成任务
+     */
+    public List<GenerationResponse> listByTemplateId(String templateId) {
+        List<Generation> generations = generationRepository.findByTemplateId(templateId);
+        return generations.stream()
+                .map(GenerationResponse::fromEntity)
+                .collect(Collectors.toList());
+    }
+    
+    /**
+     * 执行提取(异步处理,这里只更新状态)
+     */
+    @Transactional
+    public Generation execute(String id) {
+        Generation generation = generationRepository.selectById(id);
+        if (generation == null) {
+            throw new RuntimeException("生成任务不存在");
+        }
+        
+        if (!Generation.STATUS_PENDING.equals(generation.getStatus()) && 
+            !Generation.STATUS_ERROR.equals(generation.getStatus())) {
+            throw new RuntimeException("当前状态不允许执行: " + generation.getStatus());
+        }
+        
+        // 更新状态为提取中
+        generation.setStatus(Generation.STATUS_EXTRACTING);
+        generation.setProgress(0);
+        generation.setErrorMessage(null);
+        generationRepository.updateById(generation);
+        
+        // TODO: 发送消息到队列,异步执行提取
+        // messageQueue.send("generation.extract", generation.getId());
+        
+        log.info("开始执行生成任务: id={}", id);
+        
+        return generation;
+    }
+    
+    /**
+     * 获取执行进度
+     */
+    public GenerationProgressResponse getProgress(String id) {
+        Generation generation = generationRepository.selectById(id);
+        if (generation == null) {
+            return null;
+        }
+        
+        int total = 0;
+        int completed = 0;
+        String currentVariable = null;
+        
+        if (generation.getVariableValues() != null) {
+            total = generation.getVariableValues().size();
+            for (Generation.VariableValue vv : generation.getVariableValues().values()) {
+                if (Generation.VariableValue.STATUS_EXTRACTED.equals(vv.getStatus()) || 
+                    Generation.VariableValue.STATUS_MANUAL.equals(vv.getStatus())) {
+                    completed++;
+                } else if ("extracting".equals(vv.getStatus())) {
+                    currentVariable = vv.getDisplayName();
+                }
+            }
+        }
+        
+        return GenerationProgressResponse.of(
+                id, 
+                generation.getStatus(), 
+                generation.getProgress(), 
+                total, 
+                completed, 
+                currentVariable
+        );
+    }
+    
+    /**
+     * 更新变量值
+     */
+    @Transactional
+    public Generation updateVariableValue(String id, String variableName, String value) {
+        Generation generation = generationRepository.selectById(id);
+        if (generation == null) {
+            throw new RuntimeException("生成任务不存在");
+        }
+        
+        if (generation.getVariableValues() == null) {
+            throw new RuntimeException("变量列表为空");
+        }
+        
+        Generation.VariableValue vv = generation.getVariableValues().get(variableName);
+        if (vv == null) {
+            throw new RuntimeException("变量不存在: " + variableName);
+        }
+        
+        vv.setValue(value);
+        vv.setStatus(Generation.VariableValue.STATUS_MANUAL);
+        
+        generationRepository.updateById(generation);
+        log.info("更新变量值成功: generationId={}, variable={}", id, variableName);
+        
+        return generation;
+    }
+    
+    /**
+     * 确认并生成文档
+     */
+    @Transactional
+    public Generation confirm(String id) {
+        Generation generation = generationRepository.selectById(id);
+        if (generation == null) {
+            throw new RuntimeException("生成任务不存在");
+        }
+        
+        if (!Generation.STATUS_REVIEW.equals(generation.getStatus())) {
+            throw new RuntimeException("当前状态不允许确认: " + generation.getStatus());
+        }
+        
+        // 检查是否所有必须的变量都有值
+        if (generation.getVariableValues() != null) {
+            for (Map.Entry<String, Generation.VariableValue> entry : generation.getVariableValues().entrySet()) {
+                Generation.VariableValue vv = entry.getValue();
+                if (vv.getValue() == null || vv.getValue().isEmpty()) {
+                    // TODO: 检查变量是否必须
+                    log.warn("变量 {} 没有值", entry.getKey());
+                }
+            }
+        }
+        
+        // 更新状态为生成中
+        generation.setStatus(Generation.STATUS_GENERATING);
+        generationRepository.updateById(generation);
+        
+        // TODO: 发送消息到队列,异步生成文档
+        // messageQueue.send("generation.generate", generation.getId());
+        
+        log.info("确认生成任务,开始生成文档: id={}", id);
+        
+        return generation;
+    }
+    
+    /**
+     * 删除生成任务
+     */
+    @Transactional
+    public boolean delete(String id) {
+        Generation generation = generationRepository.selectById(id);
+        if (generation == null) {
+            return false;
+        }
+        
+        // 不允许删除正在执行的任务
+        if (Generation.STATUS_EXTRACTING.equals(generation.getStatus()) ||
+            Generation.STATUS_GENERATING.equals(generation.getStatus())) {
+            throw new RuntimeException("任务正在执行中,无法删除");
+        }
+        
+        generationRepository.deleteById(id);
+        log.info("删除生成任务成功: id={}", id);
+        
+        return true;
+    }
+    
+    /**
+     * 获取用户最近的生成任务
+     */
+    public List<GenerationResponse> getRecentByUserId(String userId, int limit) {
+        List<Generation> generations = generationRepository.findRecentByUserId(userId, limit);
+        return generations.stream()
+                .map(generation -> {
+                    GenerationResponse response = GenerationResponse.fromEntity(generation);
+                    Template template = templateRepository.selectById(generation.getTemplateId());
+                    if (template != null) {
+                        response.setTemplateName(template.getName());
+                    }
+                    return response;
+                })
+                .collect(Collectors.toList());
+    }
+}

+ 31 - 0
backend/extract-service/src/main/java/com/lingyue/extract/service/SourceFileService.java

@@ -200,4 +200,35 @@ public class SourceFileService {
     public int count(String templateId) {
         return sourceFileRepository.countByTemplateId(templateId);
     }
+    
+    /**
+     * 重排序来源文件定义
+     */
+    @Transactional
+    public void reorder(String templateId, List<String> orderedIds) {
+        List<SourceFile> sourceFiles = sourceFileRepository.findByTemplateId(templateId);
+        
+        // 验证所有ID都存在
+        for (String id : orderedIds) {
+            boolean found = sourceFiles.stream().anyMatch(sf -> sf.getId().equals(id));
+            if (!found) {
+                throw new RuntimeException("来源文件不存在: " + id);
+            }
+        }
+        
+        // 更新顺序
+        for (int i = 0; i < orderedIds.size(); i++) {
+            String id = orderedIds.get(i);
+            SourceFile sf = sourceFiles.stream()
+                    .filter(s -> s.getId().equals(id))
+                    .findFirst()
+                    .orElse(null);
+            if (sf != null && sf.getDisplayOrder() != i) {
+                sf.setDisplayOrder(i);
+                sourceFileRepository.updateById(sf);
+            }
+        }
+        
+        log.info("重排序来源文件定义成功: templateId={}, count={}", templateId, orderedIds.size());
+    }
 }

+ 56 - 0
backend/extract-service/src/main/java/com/lingyue/extract/service/VariableService.java

@@ -224,4 +224,60 @@ public class VariableService {
     public List<Variable> findReferencingVariables(String templateId, String variableName) {
         return variableRepository.findVariablesReferencingVariable(templateId, variableName);
     }
+    
+    /**
+     * 重排序变量
+     */
+    @Transactional
+    public void reorder(String templateId, List<String> orderedIds) {
+        List<Variable> variables = variableRepository.findByTemplateId(templateId);
+        
+        // 验证所有ID都存在
+        for (String id : orderedIds) {
+            boolean found = variables.stream().anyMatch(v -> v.getId().equals(id));
+            if (!found) {
+                throw new RuntimeException("变量不存在: " + id);
+            }
+        }
+        
+        // 更新顺序
+        for (int i = 0; i < orderedIds.size(); i++) {
+            String id = orderedIds.get(i);
+            Variable v = variables.stream()
+                    .filter(var -> var.getId().equals(id))
+                    .findFirst()
+                    .orElse(null);
+            if (v != null && v.getDisplayOrder() != i) {
+                v.setDisplayOrder(i);
+                v.setUpdateTime(new Date());
+                variableRepository.updateById(v);
+            }
+        }
+        
+        log.info("重排序变量成功: templateId={}, count={}", templateId, orderedIds.size());
+    }
+    
+    /**
+     * 预览提取结果(使用示例文档测试提取配置)
+     * TODO: 实际实现需要调用 ai-service
+     */
+    public com.lingyue.extract.dto.response.VariablePreviewResponse preview(String variableId, String documentId) {
+        Variable variable = variableRepository.selectById(variableId);
+        if (variable == null) {
+            return com.lingyue.extract.dto.response.VariablePreviewResponse.error(
+                    variableId, null, "变量不存在");
+        }
+        
+        // TODO: 调用 ai-service 进行实际提取
+        // 这里返回模拟数据
+        log.info("预览变量提取: variableId={}, documentId={}", variableId, documentId);
+        
+        return com.lingyue.extract.dto.response.VariablePreviewResponse.success(
+                variableId,
+                variable.getName(),
+                variable.getExampleValue(), // 模拟:返回示例值
+                0.95,
+                "预览模式:返回示例值作为提取结果"
+        );
+    }
 }