Просмотр исходного кода

feat: 新增报告快速分析端点 /api/ai/report/analyze(免费版Redis每日1次+付费版多轮)

- 抽出 buildChatInputs 私有方法供 /chat/send 与 /report/analyze 复用
- report/analyze:会员级别检查 + 免费版 Redis INCR 每日1次(fail-open)+ 免费版移除 conversationId 锁死多轮
- 调 aiService.sendMessage(含记忆层+镜像),出参 stripTaskMarkers
Xiaogang Liao 1 неделя назад
Родитель
Сommit
f48d15f86f

+ 110 - 34
cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java

@@ -15,9 +15,12 @@ import com.etotem.cfc.service.UserService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
+import java.time.Duration;
+import java.time.LocalDate;
 import java.util.*;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -68,6 +71,9 @@ public class AIChatController {
     @Resource
     private com.fasterxml.jackson.databind.ObjectMapper objectMapper;
 
+    @Resource
+    private RedisTemplate<String, Object> redisTemplate;
+
     @Operation(summary = "发送聊天消息(支持传入reportId以解读报告)")
     @PostMapping("/chat/send")
     public Result<Map<String, Object>> sendMessage(
@@ -75,14 +81,64 @@ public class AIChatController {
             @RequestBody Map<String, String> params) {
         String query = params.get("query");
         String conversationId = params.get("conversationId");
-        String reportIdStr = params.get("reportId");
-        String surveyIdStr = params.get("surveyId");
-        String selfCheckIdStr = params.get("selfCheckId");
 
         if (query == null || query.trim().isEmpty()) {
             return Result.error("消息不能为空");
         }
 
+        // 复用上下文组装(mascot + 家庭/报告 + 画像 + 自检 + 记忆层)
+        Map<String, Object> inputs = buildChatInputs(userId, params);
+
+        Map<String, Object> difyResp = aiService.sendMessage(
+                query, String.valueOf(userId),
+                conversationId, inputs);
+
+        String answer = (String) difyResp.getOrDefault("answer", "");
+        Map<String, Object> result = new LinkedHashMap<>();
+        result.put("answer", answer);
+        result.put("conversationId", difyResp.getOrDefault("conversationId", ""));
+
+        // Parse [TASK] markers and create task records
+        List<Map<String, Object>> tasks = new ArrayList<>();
+        List<TaskParseResult> parsedTasks = taskParseService.parseTasks(answer);
+        String cleanAnswer = taskParseService.stripTaskMarkers(answer);
+        
+        for (TaskParseResult taskResult : parsedTasks) {
+            com.etotem.cfc.entity.GrowthTask task = growthTaskService.createDynamicTask(
+                userId,
+                taskResult.getTitle(),
+                taskResult.getDescription(),
+                taskResult.getDimension(),
+                taskResult.getRewardPoints(),
+                (String) difyResp.getOrDefault("conversationId", "")
+            );
+            Map<String, Object> taskInfo = new LinkedHashMap<>();
+            taskInfo.put("id", task.getId());
+            taskInfo.put("title", task.getTitle());
+            taskInfo.put("description", task.getDescription());
+            taskInfo.put("dimension", task.getDimension());
+            taskInfo.put("rewardPoints", task.getRewardPoints());
+            tasks.add(taskInfo);
+        }
+        
+        result.put("answer", cleanAnswer);
+        result.put("tasks", tasks);
+
+        try { growthTaskService.updateProgress(userId, "DAILY_AI", 1); } catch (Exception e) { log.warn("成长任务AI对话进度更新失败: userId={}, error={}", userId, e.getMessage()); }
+
+        return Result.success(result);
+    }
+
+    /**
+     * 组装 AI 对话上下文(mascot + 家庭/报告 + 画像 + 自检 + 记忆层)
+     * 供 /chat/send 和 /report/analyze 共用
+     */
+    private Map<String, Object> buildChatInputs(Long userId, Map<String, String> params) {
+        String conversationId = params.get("conversationId");
+        String reportIdStr = params.get("reportId");
+        String surveyIdStr = params.get("surveyId");
+        String selfCheckIdStr = params.get("selfCheckId");
+
         // 获取用户mascot设置
         com.etotem.cfc.entity.User user = userService.getUserInfo(userId);
         String mascotCode = user != null ? user.getMascot() : null;
@@ -147,44 +203,64 @@ public class AIChatController {
 
         // AI记忆层注入(会话摘要+关键事实)
         inputs = aiService.enrichInputsWithMemory(userId, conversationId, inputs);
+        return inputs;
+    }
+
+    @Operation(summary = "报告快速分析(免费版每日1次,付费版多轮)")
+    @PostMapping("/report/analyze")
+    public Result<Map<String, Object>> reportAnalyze(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody Map<String, String> params) {
+        String query = params.get("query");
+        String reportIdStr = params.get("reportId");
+        if (query == null || query.trim().isEmpty()) {
+            return Result.error("消息不能为空");
+        }
+        if (reportIdStr == null || reportIdStr.trim().isEmpty()) {
+            return Result.error("报告ID不能为空");
+        }
+        Long reportId = Long.valueOf(reportIdStr);
+
+        // 1. 会员级别检查
+        com.etotem.cfc.entity.User user = userService.getUserInfo(userId);
+        String memberLevel = user != null ? user.getMemberLevel() : "FREE";
+        boolean isFree = memberLevel == null || "FREE".equals(memberLevel);
+
+        // 2. 免费版 Redis 计数(每日 1 次;Redis 异常时 fail-open 放行)
+        if (isFree) {
+            try {
+                String key = "report:analyze:free:" + userId + ":" + LocalDate.now();
+                Long count = redisTemplate.opsForValue().increment(key);
+                if (count != null && count == 1L) {
+                    redisTemplate.expire(key, Duration.ofDays(1));
+                }
+                if (count != null && count > 1L) {
+                    return Result.error("免费版每日仅限 1 次快速分析,升级会员解锁更多");
+                }
+            } catch (Exception e) {
+                log.warn("Redis 计数失败,放行: {}", e.getMessage());
+            }
+        }
+
+        // 3. 组装上下文(免费版不带 conversationId → LangGraph 每次新建会话,锁死多轮)
+        if (isFree) {
+            params.remove("conversationId");
+        }
+        Map<String, Object> inputs = buildChatInputs(userId, params);
 
+        // 4. 调 LangGraph(经 aiService.sendMessage 含记忆层+镜像)
         Map<String, Object> difyResp = aiService.sendMessage(
                 query, String.valueOf(userId),
-                conversationId, inputs);
+                params.get("conversationId"), inputs);
 
+        if (difyResp == null || difyResp.isEmpty()) {
+            return Result.error("AI 服务暂不可用,请稍后重试");
+        }
         String answer = (String) difyResp.getOrDefault("answer", "");
-        Map<String, Object> result = new LinkedHashMap<>();
-        result.put("answer", answer);
-        result.put("conversationId", difyResp.getOrDefault("conversationId", ""));
-
-        // Parse [TASK] markers and create task records
-        List<Map<String, Object>> tasks = new ArrayList<>();
-        List<TaskParseResult> parsedTasks = taskParseService.parseTasks(answer);
         String cleanAnswer = taskParseService.stripTaskMarkers(answer);
-        
-        for (TaskParseResult taskResult : parsedTasks) {
-            com.etotem.cfc.entity.GrowthTask task = growthTaskService.createDynamicTask(
-                userId,
-                taskResult.getTitle(),
-                taskResult.getDescription(),
-                taskResult.getDimension(),
-                taskResult.getRewardPoints(),
-                (String) difyResp.getOrDefault("conversationId", "")
-            );
-            Map<String, Object> taskInfo = new LinkedHashMap<>();
-            taskInfo.put("id", task.getId());
-            taskInfo.put("title", task.getTitle());
-            taskInfo.put("description", task.getDescription());
-            taskInfo.put("dimension", task.getDimension());
-            taskInfo.put("rewardPoints", task.getRewardPoints());
-            tasks.add(taskInfo);
-        }
-        
+        Map<String, Object> result = new LinkedHashMap<>();
         result.put("answer", cleanAnswer);
-        result.put("tasks", tasks);
-
-        try { growthTaskService.updateProgress(userId, "DAILY_AI", 1); } catch (Exception e) { log.warn("成长任务AI对话进度更新失败: userId={}, error={}", userId, e.getMessage()); }
-
+        result.put("conversationId", difyResp.getOrDefault("conversationId", ""));
         return Result.success(result);
     }