Преглед изворни кода

feat(extract-service): 完善 TODO 实现核心服务逻辑

新增 SecurityUtils 工具类:
- getCurrentUserId() - 从 JWT Token 获取当前用户ID
- getCurrentUsername() - 获取当前用户名
- getClientIpAddress() - 获取客户端IP
- isAuthenticated() - 检查是否已登录

新增 ExtractionService:
- previewExtract() - 预览变量提取结果
- executeExtraction() - 执行生成任务的变量提取
- processReferenceVariables() - 处理引用类型变量
- extractByAI() - 调用 AI 服务提取
- summarizeByAI() - 调用 AI 服务总结

新增 DocumentGenerationService:
- generateDocument() - 生成最终文档
- getOutputFileResource() - 获取输出文件资源
- getOutputFileName() - 获取输出文件名
- deleteOutputFile() - 删除输出文件

更新 Controller:
- 使用 SecurityUtils 获取当前用户ID
- 实现文件下载接口(返回 ResponseEntity<Resource>)
- 新增 download-info 接口获取下载信息

更新 GenerationService:
- 异步执行提取(使用线程池)
- 异步生成文档
何文松 пре 1 месец
родитељ
комит
220672b6e7

+ 154 - 0
backend/common/src/main/java/com/lingyue/common/util/SecurityUtils.java

@@ -0,0 +1,154 @@
+package com.lingyue.common.util;
+
+import io.jsonwebtoken.Claims;
+import jakarta.servlet.http.HttpServletRequest;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+/**
+ * 安全工具类
+ * 
+ * 用于从请求上下文中获取当前用户信息
+ * 
+ * @author lingyue
+ * @since 2026-01-24
+ */
+@Slf4j
+public class SecurityUtils {
+    
+    private static final String AUTHORIZATION_HEADER = "Authorization";
+    private static final String BEARER_PREFIX = "Bearer ";
+    
+    /**
+     * 获取当前用户ID
+     * 
+     * @return 用户ID,未登录返回null
+     */
+    public static String getCurrentUserId() {
+        String token = getTokenFromRequest();
+        if (token == null) {
+            return null;
+        }
+        return JwtUtil.getUserIdFromToken(token);
+    }
+    
+    /**
+     * 获取当前用户ID(使用指定密钥)
+     * 
+     * @param jwtSecret JWT密钥
+     * @return 用户ID,未登录返回null
+     */
+    public static String getCurrentUserId(String jwtSecret) {
+        String token = getTokenFromRequest();
+        if (token == null) {
+            return null;
+        }
+        return JwtUtil.getUserIdFromToken(token, jwtSecret);
+    }
+    
+    /**
+     * 获取当前用户名
+     * 
+     * @return 用户名,未登录返回null
+     */
+    public static String getCurrentUsername() {
+        String token = getTokenFromRequest();
+        if (token == null) {
+            return null;
+        }
+        Claims claims = JwtUtil.parseToken(token);
+        return claims != null ? (String) claims.get("username") : null;
+    }
+    
+    /**
+     * 获取当前用户名(使用指定密钥)
+     * 
+     * @param jwtSecret JWT密钥
+     * @return 用户名,未登录返回null
+     */
+    public static String getCurrentUsername(String jwtSecret) {
+        String token = getTokenFromRequest();
+        if (token == null) {
+            return null;
+        }
+        Claims claims = JwtUtil.parseToken(token, jwtSecret);
+        return claims != null ? (String) claims.get("username") : null;
+    }
+    
+    /**
+     * 获取当前Token
+     * 
+     * @return Token,未登录返回null
+     */
+    public static String getTokenFromRequest() {
+        HttpServletRequest request = getRequest();
+        if (request == null) {
+            return null;
+        }
+        
+        String authorization = request.getHeader(AUTHORIZATION_HEADER);
+        if (authorization != null && authorization.startsWith(BEARER_PREFIX)) {
+            return authorization.substring(BEARER_PREFIX.length());
+        }
+        return null;
+    }
+    
+    /**
+     * 获取当前请求
+     * 
+     * @return HttpServletRequest,非Web环境返回null
+     */
+    public static HttpServletRequest getRequest() {
+        try {
+            ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
+            return attributes != null ? attributes.getRequest() : null;
+        } catch (Exception e) {
+            log.debug("获取HttpServletRequest失败(可能不在Web请求上下文中): {}", e.getMessage());
+            return null;
+        }
+    }
+    
+    /**
+     * 检查当前用户是否已登录
+     * 
+     * @return 是否已登录
+     */
+    public static boolean isAuthenticated() {
+        return getCurrentUserId() != null;
+    }
+    
+    /**
+     * 获取客户端IP地址
+     * 
+     * @return IP地址
+     */
+    public static String getClientIpAddress() {
+        HttpServletRequest request = getRequest();
+        if (request == null) {
+            return null;
+        }
+        
+        String xForwardedFor = request.getHeader("X-Forwarded-For");
+        if (xForwardedFor != null && !xForwardedFor.isEmpty()) {
+            return xForwardedFor.split(",")[0].trim();
+        }
+        
+        String xRealIp = request.getHeader("X-Real-IP");
+        if (xRealIp != null && !xRealIp.isEmpty()) {
+            return xRealIp;
+        }
+        
+        return request.getRemoteAddr();
+    }
+    
+    /**
+     * 获取User-Agent
+     * 
+     * @return User-Agent
+     */
+    public static String getUserAgent() {
+        HttpServletRequest request = getRequest();
+        return request != null ? request.getHeader("User-Agent") : null;
+    }
+}

+ 34 - 5
backend/extract-service/src/main/java/com/lingyue/extract/controller/GenerationController.java

@@ -32,10 +32,16 @@ import java.util.List;
 public class GenerationController {
     
     private final GenerationService generationService;
+    private final com.lingyue.extract.service.DocumentGenerationService documentGenerationService;
     
-    // TODO: 从认证上下文获取用户ID
+    /**
+     * 获取当前用户ID
+     * 开发阶段:如果未登录,使用默认测试用户
+     */
     private String getCurrentUserId() {
-        return "test-user-001";
+        String userId = com.lingyue.common.util.SecurityUtils.getCurrentUserId();
+        // 开发阶段:未登录时使用测试用户
+        return userId != null ? userId : "00000000000000000000000000000002";
     }
     
     // ==================== 生成任务 CRUD ====================
@@ -163,7 +169,28 @@ public class GenerationController {
     
     @GetMapping("/{id}/download")
     @Operation(summary = "下载生成的文档", description = "下载生成的报告文档")
-    public AjaxResult<?> downloadGeneration(
+    public org.springframework.http.ResponseEntity<org.springframework.core.io.Resource> downloadGeneration(
+            @Parameter(description = "任务ID") @PathVariable String id) {
+        try {
+            org.springframework.core.io.Resource resource = documentGenerationService.getOutputFileResource(id);
+            String fileName = documentGenerationService.getOutputFileName(id);
+            
+            return org.springframework.http.ResponseEntity.ok()
+                    .header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION, 
+                            "attachment; filename=\"" + java.net.URLEncoder.encode(fileName, "UTF-8") + "\"")
+                    .header(org.springframework.http.HttpHeaders.CONTENT_TYPE, 
+                            org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE)
+                    .body(resource);
+                    
+        } catch (Exception e) {
+            log.error("下载失败: id={}, error={}", id, e.getMessage());
+            return org.springframework.http.ResponseEntity.notFound().build();
+        }
+    }
+    
+    @GetMapping("/{id}/download-info")
+    @Operation(summary = "获取下载信息", description = "获取生成文档的下载信息")
+    public AjaxResult<?> getDownloadInfo(
             @Parameter(description = "任务ID") @PathVariable String id) {
         Generation generation = generationService.getById(id);
         if (generation == null) {
@@ -178,7 +205,9 @@ public class GenerationController {
             return AjaxResult.error("输出文件不存在");
         }
         
-        // TODO: 返回文件下载链接或流
-        return AjaxResult.success("下载链接", generation.getOutputFilePath());
+        return AjaxResult.success(java.util.Map.of(
+                "fileName", documentGenerationService.getOutputFileName(id),
+                "downloadUrl", "/api/v1/generations/" + id + "/download"
+        ));
     }
 }

+ 7 - 2
backend/extract-service/src/main/java/com/lingyue/extract/controller/TemplateController.java

@@ -44,9 +44,14 @@ public class TemplateController {
     private final SourceFileService sourceFileService;
     private final VariableService variableService;
     
-    // TODO: 从认证上下文获取用户ID
+    /**
+     * 获取当前用户ID
+     * 开发阶段:如果未登录,使用默认测试用户
+     */
     private String getCurrentUserId() {
-        return "test-user-001";
+        String userId = com.lingyue.common.util.SecurityUtils.getCurrentUserId();
+        // 开发阶段:未登录时使用测试用户
+        return userId != null ? userId : "00000000000000000000000000000002";
     }
     
     // ==================== 模板 CRUD ====================

+ 191 - 0
backend/extract-service/src/main/java/com/lingyue/extract/service/DocumentGenerationService.java

@@ -0,0 +1,191 @@
+package com.lingyue.extract.service;
+
+import com.lingyue.extract.entity.Generation;
+import com.lingyue.extract.entity.Template;
+import com.lingyue.extract.entity.Variable;
+import com.lingyue.extract.repository.GenerationRepository;
+import com.lingyue.extract.repository.TemplateRepository;
+import com.lingyue.extract.repository.VariableRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.UrlResource;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.io.IOException;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * 文档生成服务
+ * 
+ * 负责根据模板和变量值生成最终文档
+ * 
+ * @author lingyue
+ * @since 2026-01-24
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class DocumentGenerationService {
+    
+    private final GenerationRepository generationRepository;
+    private final TemplateRepository templateRepository;
+    private final VariableRepository variableRepository;
+    
+    @Value("${file.output-path:/tmp/lingyue/output}")
+    private String outputPath;
+    
+    /**
+     * 生成文档
+     */
+    @Transactional
+    public void generateDocument(String generationId) {
+        Generation generation = generationRepository.selectById(generationId);
+        if (generation == null) {
+            throw new RuntimeException("生成任务不存在");
+        }
+        
+        Template template = templateRepository.selectById(generation.getTemplateId());
+        if (template == null) {
+            throw new RuntimeException("模板不存在");
+        }
+        
+        try {
+            // 更新状态为生成中
+            generation.setStatus(Generation.STATUS_GENERATING);
+            generationRepository.updateById(generation);
+            
+            // 获取变量列表
+            List<Variable> variables = variableRepository.findByTemplateId(generation.getTemplateId());
+            
+            // 生成输出文件路径
+            String outputFileName = generateFileName(generation, template);
+            Path outputFilePath = Paths.get(outputPath, outputFileName);
+            
+            // 确保输出目录存在
+            Files.createDirectories(outputFilePath.getParent());
+            
+            // TODO: 实际的文档生成逻辑
+            // 1. 获取模板基础文档
+            // 2. 替换变量
+            // 3. 保存输出文件
+            
+            // 模拟生成:创建一个简单的文本文件
+            StringBuilder content = new StringBuilder();
+            content.append("=== 生成的报告 ===\n\n");
+            content.append("模板: ").append(template.getName()).append("\n");
+            content.append("生成时间: ").append(new Date()).append("\n\n");
+            content.append("变量值:\n");
+            
+            for (Variable variable : variables) {
+                Generation.VariableValue vv = generation.getVariableValues().get(variable.getName());
+                String value = vv != null ? vv.getValue() : "(未提取)";
+                content.append("  - ").append(variable.getDisplayName())
+                        .append(": ").append(value).append("\n");
+            }
+            
+            Files.writeString(outputFilePath, content.toString());
+            
+            // 更新生成任务
+            generation.setOutputFilePath(outputFilePath.toString());
+            generation.setStatus(Generation.STATUS_COMPLETED);
+            generation.setCompletedAt(new Date());
+            generation.setProgress(100);
+            generationRepository.updateById(generation);
+            
+            log.info("文档生成成功: generationId={}, outputPath={}", generationId, outputFilePath);
+            
+        } catch (Exception e) {
+            log.error("文档生成失败: generationId={}", generationId, e);
+            generation.setStatus(Generation.STATUS_ERROR);
+            generation.setErrorMessage("文档生成失败: " + e.getMessage());
+            generationRepository.updateById(generation);
+            throw new RuntimeException("文档生成失败: " + e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 生成文件名
+     */
+    private String generateFileName(Generation generation, Template template) {
+        String baseName = generation.getName() != null ? generation.getName() : template.getName();
+        // 清理文件名中的非法字符
+        baseName = baseName.replaceAll("[\\\\/:*?\"<>|]", "_");
+        String uuid = UUID.randomUUID().toString().substring(0, 8);
+        return baseName + "_" + uuid + ".txt"; // TODO: 根据实际模板格式生成 .docx
+    }
+    
+    /**
+     * 获取输出文件资源
+     */
+    public Resource getOutputFileResource(String generationId) {
+        Generation generation = generationRepository.selectById(generationId);
+        if (generation == null) {
+            throw new RuntimeException("生成任务不存在");
+        }
+        
+        if (!Generation.STATUS_COMPLETED.equals(generation.getStatus())) {
+            throw new RuntimeException("文档尚未生成完成");
+        }
+        
+        if (generation.getOutputFilePath() == null) {
+            throw new RuntimeException("输出文件不存在");
+        }
+        
+        try {
+            Path filePath = Paths.get(generation.getOutputFilePath());
+            if (!Files.exists(filePath)) {
+                throw new RuntimeException("文件不存在: " + generation.getOutputFilePath());
+            }
+            
+            Resource resource = new UrlResource(filePath.toUri());
+            if (!resource.exists() || !resource.isReadable()) {
+                throw new RuntimeException("文件无法读取: " + generation.getOutputFilePath());
+            }
+            
+            return resource;
+            
+        } catch (IOException e) {
+            throw new RuntimeException("读取文件失败: " + e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 获取输出文件名
+     */
+    public String getOutputFileName(String generationId) {
+        Generation generation = generationRepository.selectById(generationId);
+        if (generation == null || generation.getOutputFilePath() == null) {
+            return "report.txt";
+        }
+        
+        Path path = Paths.get(generation.getOutputFilePath());
+        return path.getFileName().toString();
+    }
+    
+    /**
+     * 删除输出文件
+     */
+    public void deleteOutputFile(String generationId) {
+        Generation generation = generationRepository.selectById(generationId);
+        if (generation == null || generation.getOutputFilePath() == null) {
+            return;
+        }
+        
+        try {
+            Path filePath = Paths.get(generation.getOutputFilePath());
+            Files.deleteIfExists(filePath);
+            log.info("删除输出文件: {}", generation.getOutputFilePath());
+        } catch (IOException e) {
+            log.warn("删除输出文件失败: {}", e.getMessage());
+        }
+    }
+}

+ 428 - 0
backend/extract-service/src/main/java/com/lingyue/extract/service/ExtractionService.java

@@ -0,0 +1,428 @@
+package com.lingyue.extract.service;
+
+import com.lingyue.extract.dto.response.VariablePreviewResponse;
+import com.lingyue.extract.entity.Generation;
+import com.lingyue.extract.entity.Variable;
+import com.lingyue.extract.repository.GenerationRepository;
+import com.lingyue.extract.repository.VariableRepository;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.reactive.function.client.WebClient;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 数据提取服务
+ * 
+ * 负责从来源文档中提取变量值
+ * 
+ * @author lingyue
+ * @since 2026-01-24
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class ExtractionService {
+    
+    private final VariableRepository variableRepository;
+    private final GenerationRepository generationRepository;
+    
+    @Value("${ai.service.base-url:http://localhost:8080}")
+    private String aiServiceBaseUrl;
+    
+    /**
+     * 预览提取结果(单个变量)
+     */
+    public VariablePreviewResponse previewExtract(String variableId, String documentId) {
+        Variable variable = variableRepository.selectById(variableId);
+        if (variable == null) {
+            return VariablePreviewResponse.error(variableId, null, "变量不存在");
+        }
+        
+        try {
+            // 根据来源类型进行提取
+            String extractedValue = extractValue(variable, documentId);
+            
+            return VariablePreviewResponse.success(
+                    variableId,
+                    variable.getName(),
+                    extractedValue,
+                    0.90, // 模拟置信度
+                    "从文档中提取"
+            );
+        } catch (Exception e) {
+            log.error("预览提取失败: variableId={}, error={}", variableId, e.getMessage());
+            return VariablePreviewResponse.error(variableId, variable.getName(), e.getMessage());
+        }
+    }
+    
+    /**
+     * 执行生成任务的变量提取
+     */
+    @Transactional
+    public void executeExtraction(String generationId) {
+        Generation generation = generationRepository.selectById(generationId);
+        if (generation == null) {
+            throw new RuntimeException("生成任务不存在");
+        }
+        
+        // 获取模板的变量列表
+        List<Variable> variables = variableRepository.findByTemplateId(generation.getTemplateId());
+        
+        int total = variables.size();
+        int completed = 0;
+        
+        for (Variable variable : variables) {
+            try {
+                // 获取来源文档ID
+                String documentId = null;
+                if (variable.getSourceFileAlias() != null && generation.getSourceFileMap() != null) {
+                    documentId = generation.getSourceFileMap().get(variable.getSourceFileAlias());
+                }
+                
+                // 提取值
+                String value = extractValue(variable, documentId);
+                
+                // 更新变量值
+                Generation.VariableValue vv = generation.getVariableValues().get(variable.getName());
+                if (vv != null) {
+                    vv.setValue(value);
+                    vv.setConfidence(0.90);
+                    vv.setStatus(Generation.VariableValue.STATUS_EXTRACTED);
+                }
+                
+                completed++;
+                generation.setProgress((int) ((completed * 100.0) / total));
+                generationRepository.updateById(generation);
+                
+                log.debug("提取变量成功: generationId={}, variable={}, value={}", 
+                        generationId, variable.getName(), value);
+                
+            } catch (Exception e) {
+                log.error("提取变量失败: generationId={}, variable={}, error={}", 
+                        generationId, variable.getName(), e.getMessage());
+                
+                Generation.VariableValue vv = generation.getVariableValues().get(variable.getName());
+                if (vv != null) {
+                    vv.setStatus(Generation.VariableValue.STATUS_ERROR);
+                    vv.setErrorMessage(e.getMessage());
+                }
+            }
+        }
+        
+        // 更新状态为待确认
+        generation.setStatus(Generation.STATUS_REVIEW);
+        generation.setProgress(100);
+        generationRepository.updateById(generation);
+        
+        log.info("提取完成: generationId={}, total={}, completed={}", generationId, total, completed);
+    }
+    
+    /**
+     * 根据变量配置提取值
+     */
+    private String extractValue(Variable variable, String documentId) {
+        String sourceType = variable.getSourceType();
+        
+        switch (sourceType) {
+            case Variable.SOURCE_TYPE_FIXED:
+                return extractFixed(variable);
+                
+            case Variable.SOURCE_TYPE_MANUAL:
+                return null; // 手动输入,不自动提取
+                
+            case Variable.SOURCE_TYPE_REFERENCE:
+                return null; // 引用类型,需要在所有变量提取完成后处理
+                
+            case Variable.SOURCE_TYPE_DOCUMENT:
+            default:
+                return extractFromDocument(variable, documentId);
+        }
+    }
+    
+    /**
+     * 提取固定值
+     */
+    private String extractFixed(Variable variable) {
+        Map<String, Object> config = variable.getSourceConfig();
+        if (config != null && config.containsKey("fixedValue")) {
+            return (String) config.get("fixedValue");
+        }
+        return variable.getExampleValue();
+    }
+    
+    /**
+     * 从文档提取
+     */
+    private String extractFromDocument(Variable variable, String documentId) {
+        if (documentId == null) {
+            log.warn("文档ID为空,使用示例值: variable={}", variable.getName());
+            return variable.getExampleValue();
+        }
+        
+        String extractType = variable.getExtractType();
+        
+        if (Variable.EXTRACT_TYPE_DIRECT.equals(extractType)) {
+            return extractDirect(variable, documentId);
+        } else if (Variable.EXTRACT_TYPE_AI_EXTRACT.equals(extractType)) {
+            return extractByAI(variable, documentId);
+        } else if (Variable.EXTRACT_TYPE_AI_SUMMARIZE.equals(extractType)) {
+            return summarizeByAI(variable, documentId);
+        }
+        
+        // 默认返回示例值
+        return variable.getExampleValue();
+    }
+    
+    /**
+     * 直接提取(按位置)
+     */
+    private String extractDirect(Variable variable, String documentId) {
+        // TODO: 调用 document-service 获取文档内容
+        // TODO: 根据 variable.getLocation() 定位并提取文本
+        log.info("直接提取: variable={}, documentId={}", variable.getName(), documentId);
+        return variable.getExampleValue();
+    }
+    
+    /**
+     * AI提取
+     */
+    private String extractByAI(Variable variable, String documentId) {
+        Map<String, Object> extractConfig = variable.getExtractConfig();
+        if (extractConfig == null) {
+            return variable.getExampleValue();
+        }
+        
+        String targetDescription = (String) extractConfig.get("targetDescription");
+        if (targetDescription == null) {
+            targetDescription = "提取 " + variable.getDisplayName();
+        }
+        
+        try {
+            // 构建提示词
+            String prompt = buildExtractionPrompt(variable, targetDescription, documentId);
+            
+            // 调用AI服务
+            String result = callAIService(prompt);
+            
+            return result != null ? result.trim() : variable.getExampleValue();
+            
+        } catch (Exception e) {
+            log.error("AI提取失败: variable={}, error={}", variable.getName(), e.getMessage());
+            return variable.getExampleValue();
+        }
+    }
+    
+    /**
+     * AI总结
+     */
+    private String summarizeByAI(Variable variable, String documentId) {
+        Map<String, Object> extractConfig = variable.getExtractConfig();
+        if (extractConfig == null) {
+            return variable.getExampleValue();
+        }
+        
+        String summarizePrompt = (String) extractConfig.get("summarizePrompt");
+        if (summarizePrompt == null) {
+            summarizePrompt = "请对以下内容进行总结";
+        }
+        
+        try {
+            // 构建总结提示词
+            String prompt = buildSummarizePrompt(variable, summarizePrompt, documentId);
+            
+            // 调用AI服务
+            String result = callAIService(prompt);
+            
+            return result != null ? result.trim() : variable.getExampleValue();
+            
+        } catch (Exception e) {
+            log.error("AI总结失败: variable={}, error={}", variable.getName(), e.getMessage());
+            return variable.getExampleValue();
+        }
+    }
+    
+    /**
+     * 构建提取提示词
+     */
+    private String buildExtractionPrompt(Variable variable, String targetDescription, String documentId) {
+        StringBuilder prompt = new StringBuilder();
+        prompt.append("请从以下文档内容中提取信息。\n\n");
+        prompt.append("提取目标:").append(targetDescription).append("\n");
+        prompt.append("字段类型:").append(variable.getValueType()).append("\n");
+        
+        Map<String, Object> extractConfig = variable.getExtractConfig();
+        if (extractConfig != null) {
+            Object expectedFormat = extractConfig.get("expectedFormat");
+            if (expectedFormat != null) {
+                prompt.append("期望格式:").append(expectedFormat).append("\n");
+            }
+            Object examples = extractConfig.get("examples");
+            if (examples != null) {
+                prompt.append("示例:").append(examples).append("\n");
+            }
+        }
+        
+        prompt.append("\n请只输出提取的值,不要包含其他解释。\n");
+        prompt.append("\n文档内容:\n");
+        prompt.append("[文档ID: ").append(documentId).append("]\n");
+        // TODO: 这里需要获取实际的文档内容
+        prompt.append("(文档内容待获取)");
+        
+        return prompt.toString();
+    }
+    
+    /**
+     * 构建总结提示词
+     */
+    private String buildSummarizePrompt(Variable variable, String summarizePrompt, String documentId) {
+        StringBuilder prompt = new StringBuilder();
+        prompt.append(summarizePrompt).append("\n\n");
+        
+        Map<String, Object> extractConfig = variable.getExtractConfig();
+        if (extractConfig != null) {
+            Object focusPoints = extractConfig.get("focusPoints");
+            if (focusPoints != null) {
+                prompt.append("重点关注:").append(focusPoints).append("\n");
+            }
+            Object rules = extractConfig.get("rules");
+            if (rules != null) {
+                prompt.append("总结要求:").append(rules).append("\n");
+            }
+            Object maxLength = extractConfig.get("maxLength");
+            if (maxLength != null) {
+                prompt.append("字数限制:").append(maxLength).append("字以内\n");
+            }
+        }
+        
+        prompt.append("\n文档内容:\n");
+        prompt.append("[文档ID: ").append(documentId).append("]\n");
+        // TODO: 这里需要获取实际的文档内容
+        prompt.append("(文档内容待获取)");
+        
+        return prompt.toString();
+    }
+    
+    /**
+     * 调用AI服务
+     */
+    private String callAIService(String prompt) {
+        try {
+            WebClient webClient = WebClient.builder()
+                    .baseUrl(aiServiceBaseUrl)
+                    .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
+                    .build();
+            
+            // 构建请求
+            Map<String, Object> request = Map.of(
+                    "prompt", prompt,
+                    "maxTokens", 500
+            );
+            
+            // 调用AI接口
+            @SuppressWarnings("unchecked")
+            Map<String, Object> response = webClient
+                    .post()
+                    .uri("/api/v1/ai/complete")
+                    .bodyValue(request)
+                    .retrieve()
+                    .bodyToMono(Map.class)
+                    .block();
+            
+            if (response != null && response.containsKey("data")) {
+                Object data = response.get("data");
+                if (data instanceof String) {
+                    return (String) data;
+                } else if (data instanceof Map) {
+                    @SuppressWarnings("unchecked")
+                    Map<String, Object> dataMap = (Map<String, Object>) data;
+                    return (String) dataMap.get("content");
+                }
+            }
+            
+            return null;
+            
+        } catch (Exception e) {
+            log.error("调用AI服务失败: {}", e.getMessage());
+            throw new RuntimeException("AI服务调用失败: " + e.getMessage(), e);
+        }
+    }
+    
+    /**
+     * 处理引用类型变量
+     * 在所有基础变量提取完成后调用
+     */
+    @Transactional
+    public void processReferenceVariables(String generationId) {
+        Generation generation = generationRepository.selectById(generationId);
+        if (generation == null) {
+            return;
+        }
+        
+        List<Variable> refVariables = variableRepository.findByTemplateIdAndSourceType(
+                generation.getTemplateId(), Variable.SOURCE_TYPE_REFERENCE);
+        
+        for (Variable variable : refVariables) {
+            try {
+                String value = processReference(variable, generation.getVariableValues());
+                
+                Generation.VariableValue vv = generation.getVariableValues().get(variable.getName());
+                if (vv != null) {
+                    vv.setValue(value);
+                    vv.setStatus(Generation.VariableValue.STATUS_EXTRACTED);
+                }
+                
+            } catch (Exception e) {
+                log.error("处理引用变量失败: variable={}, error={}", variable.getName(), e.getMessage());
+            }
+        }
+        
+        generationRepository.updateById(generation);
+    }
+    
+    /**
+     * 处理引用
+     */
+    private String processReference(Variable variable, Map<String, Generation.VariableValue> variableValues) {
+        Map<String, Object> config = variable.getSourceConfig();
+        if (config == null) {
+            return null;
+        }
+        
+        @SuppressWarnings("unchecked")
+        List<String> referenceVariables = (List<String>) config.get("referenceVariables");
+        String combineTemplate = (String) config.get("combineTemplate");
+        
+        if (referenceVariables == null || referenceVariables.isEmpty()) {
+            return null;
+        }
+        
+        if (combineTemplate == null) {
+            // 默认直接拼接
+            StringBuilder sb = new StringBuilder();
+            for (String refName : referenceVariables) {
+                Generation.VariableValue vv = variableValues.get(refName);
+                if (vv != null && vv.getValue() != null) {
+                    sb.append(vv.getValue());
+                }
+            }
+            return sb.toString();
+        }
+        
+        // 使用模板替换
+        String result = combineTemplate;
+        for (String refName : referenceVariables) {
+            Generation.VariableValue vv = variableValues.get(refName);
+            String value = vv != null && vv.getValue() != null ? vv.getValue() : "";
+            result = result.replace("{" + refName + "}", value);
+        }
+        
+        return result;
+    }
+}

+ 27 - 4
backend/extract-service/src/main/java/com/lingyue/extract/service/GenerationService.java

@@ -40,6 +40,8 @@ public class GenerationService {
     private final TemplateRepository templateRepository;
     private final SourceFileRepository sourceFileRepository;
     private final VariableRepository variableRepository;
+    private final ExtractionService extractionService;
+    private final DocumentGenerationService documentGenerationService;
     
     /**
      * 创建生成任务
@@ -185,8 +187,22 @@ public class GenerationService {
         generation.setErrorMessage(null);
         generationRepository.updateById(generation);
         
-        // TODO: 发送消息到队列,异步执行提取
-        // messageQueue.send("generation.extract", generation.getId());
+        // 异步执行提取(使用简单的线程池,生产环境应使用消息队列)
+        final String generationId = id;
+        new Thread(() -> {
+            try {
+                extractionService.executeExtraction(generationId);
+                extractionService.processReferenceVariables(generationId);
+            } catch (Exception e) {
+                log.error("提取执行失败: generationId={}", generationId, e);
+                Generation gen = generationRepository.selectById(generationId);
+                if (gen != null) {
+                    gen.setStatus(Generation.STATUS_ERROR);
+                    gen.setErrorMessage(e.getMessage());
+                    generationRepository.updateById(gen);
+                }
+            }
+        }).start();
         
         log.info("开始执行生成任务: id={}", id);
         
@@ -285,8 +301,15 @@ public class GenerationService {
         generation.setStatus(Generation.STATUS_GENERATING);
         generationRepository.updateById(generation);
         
-        // TODO: 发送消息到队列,异步生成文档
-        // messageQueue.send("generation.generate", generation.getId());
+        // 异步生成文档
+        final String generationId = id;
+        new Thread(() -> {
+            try {
+                documentGenerationService.generateDocument(generationId);
+            } catch (Exception e) {
+                log.error("文档生成失败: generationId={}", generationId, e);
+            }
+        }).start();
         
         log.info("确认生成任务,开始生成文档: id={}", id);
         

+ 3 - 18
backend/extract-service/src/main/java/com/lingyue/extract/service/VariableService.java

@@ -24,6 +24,7 @@ import java.util.UUID;
 public class VariableService {
     
     private final VariableRepository variableRepository;
+    private final ExtractionService extractionService;
     
     /**
      * 添加变量
@@ -259,25 +260,9 @@ public class VariableService {
     
     /**
      * 预览提取结果(使用示例文档测试提取配置)
-     * 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,
-                "预览模式:返回示例值作为提取结果"
-        );
+        return extractionService.previewExtract(variableId, documentId);
     }
 }