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

feat: 文章答题出题/提交/双奖励/认知雷达接口改造

Sisyphus пре 1 месец
родитељ
комит
589e38d6ed

+ 224 - 27
cfc-backend/src/main/java/com/etotem/cfc/controller/content/ArticleCommentController.java

@@ -3,16 +3,28 @@ package com.etotem.cfc.controller.content;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.ArticleComment;
 import com.etotem.cfc.entity.Article;
+import com.etotem.cfc.entity.ArticleQuizRecord;
+import com.etotem.cfc.entity.ProfileSnapshot;
 import com.etotem.cfc.mapper.ArticleMapper;
+import com.etotem.cfc.mapper.ArticleQuizRecordMapper;
+import com.etotem.cfc.mapper.ProfileSnapshotMapper;
 import com.etotem.cfc.service.ArticleCommentService;
-import com.etotem.cfc.service.AIService;
+import com.etotem.cfc.service.AiGateway;
 import com.etotem.cfc.service.EnergyService;
+import com.etotem.cfc.service.PointsService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
+import java.text.SimpleDateFormat;
 import java.util.*;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.util.ParamUtils;
 
 @Tag(name = "文章评论", description = "文章评论与AI答题")
@@ -20,14 +32,22 @@ import com.etotem.cfc.util.ParamUtils;
 @RequestMapping("/api/articles")
 public class ArticleCommentController {
 
+    private static final Logger log = LoggerFactory.getLogger(ArticleCommentController.class);
+
     @Resource
     private ArticleCommentService articleCommentService;
     @Resource
     private ArticleMapper articleMapper;
     @Resource
-    private AIService aiService;
-    @Resource
     private EnergyService energyService;
+    @Resource
+    private AiGateway aiGateway;
+    @Resource
+    private PointsService pointsService;
+    @Resource
+    private ArticleQuizRecordMapper articleQuizRecordMapper;
+    @Resource
+    private ProfileSnapshotMapper profileSnapshotMapper;
 
     @PostMapping("/comments/list")
     public Result<List<Map<String, Object>>> listComments(@RequestBody Map<String, Object> params) {
@@ -83,39 +103,216 @@ public class ArticleCommentController {
     }
 
     @PostMapping("/quiz/generate")
-    public Result<List<Map<String, Object>>> generateQuiz(@RequestBody Map<String, Object> params) {
+    public Result<Map<String, Object>> generateQuiz(@RequestBody Map<String, Object> params,
+                                                    @RequestAttribute("userId") Long userId,
+                                                    @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
         Long articleId = ParamUtils.getLong(params.get("articleId"));
+        Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
         Article article = articleMapper.selectById(articleId);
         if (article == null) return Result.error("文章不存在");
-        String c = article.getContent();
-        if (c != null && c.length() > 2000) c = c.substring(0, 2000);
+
+        String content = article.getContent();
+        if (content != null && content.length() > 2000) content = content.substring(0, 2000);
+
+        // 组装用户画像摘要(从 profile_snapshot 读五维评分)
+        String profileSummary = "";
+        if (memberId != null) {
+            ProfileSnapshot snapshot = profileSnapshotMapper.selectOne(
+                    new LambdaQueryWrapper<ProfileSnapshot>().eq(ProfileSnapshot::getMemberId, memberId));
+            if (snapshot != null && snapshot.getDimensionScores() != null) {
+                profileSummary = snapshot.getDimensionScores();
+            }
+        }
+
+        // 优先走 AiGateway + LangGraph,失败降级硬编码
+        List<Map<String, Object>> questions = null;
         try {
-            Map<String, Object> inputs = new HashMap<>();
-            inputs.put("article_title", article.getTitle());
-            inputs.put("article_content", c);
-            Map<String, Object> dr = aiService.sendWorkflow("article-quiz", inputs);
-            List<Map<String, Object>> qs = (List<Map<String, Object>>) dr.getOrDefault("questions", new ArrayList<>());
-            if (!qs.isEmpty()) return Result.success(qs);
-        } catch (Exception e) {}
-        List<Map<String, Object>> fb = new ArrayList<>();
-        fb.add(q("文章的主要内容是什么?", Arrays.asList("A. 提升认知","B. 健康生活","C. 亲子关系","D. 财富管理"), "A"));
-        fb.add(q("文章提到了哪些维度?", Arrays.asList("A. 身","B. 心","C. 智","D. 以上都有"), "D"));
-        fb.add(q("你对这篇文章的收获是什么?", Arrays.asList("A. 学到了新知识","B. 获得了启发","C. 需要再想想","D. 很有共鸣"), "A"));
-        return Result.success(fb);
+            questions = aiGateway.generateArticleQuiz(article.getTitle(), content, profileSummary);
+        } catch (Exception e) {
+            log.warn("AI 出题异常: {}", e.getMessage());
+        }
+
+        if (questions == null || questions.isEmpty()) {
+            questions = new ArrayList<>();
+            questions.add(q("文章的主要内容是什么?", Arrays.asList("A. 提升认知", "B. 健康生活", "C. 亲子关系", "D. 财富管理"), "A", "wisdom"));
+            questions.add(q("文章提到了哪些维度?", Arrays.asList("A. 身", "B. 心", "C. 智", "D. 以上都有"), "D", "wisdom"));
+            questions.add(q("你对这篇文章的收获是什么?", Arrays.asList("A. 学到了新知识", "B. 获得了启发", "C. 需要再想想", "D. 很有共鸣"), "A", "wisdom"));
+        }
+
+        // 落库题目快照,返回 recordId(判分以库内答案为准,防篡改)
+        ArticleQuizRecord record = new ArticleQuizRecord();
+        record.setArticleId(articleId);
+        record.setUserId(userId);
+        record.setMemberId(memberId);
+        record.setQuestions(JSON.toJSONString(questions));
+        record.setAnswers(null);
+        record.setScore(0);
+        record.setTotalQuestions(questions.size());
+        record.setCfEarned(0);
+        record.setEnergyEarned(0);
+        record.setCreatedAt(new Date());
+        record.setUpdatedAt(new Date());
+        articleQuizRecordMapper.insert(record);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("recordId", record.getId());
+        result.put("questions", questions);
+        return Result.success(result);
     }
-    private Map<String, Object> q(String q, List<String> opts, String a) {
-        Map<String, Object> m = new HashMap<>(); m.put("question", q); m.put("options", opts); m.put("answer", a); return m;
+
+    private Map<String, Object> q(String q, List<String> opts, String a, String dimension) {
+        Map<String, Object> m = new HashMap<>();
+        m.put("question", q);
+        m.put("options", opts);
+        m.put("answer", a);
+        m.put("dimension", dimension);
+        return m;
     }
 
     @PostMapping("/quiz/submit")
-    public Result<Map<String, Object>> submitQuiz(@RequestBody Map<String, Object> params, @RequestAttribute("userId") Long userId,
+    public Result<Map<String, Object>> submitQuiz(@RequestBody Map<String, Object> params,
+                                                  @RequestAttribute("userId") Long userId,
                                                   @RequestAttribute(value = "currentMemberId", required = false) Long currentMemberId) {
-        int correct = params.get("correctCount") != null ? Integer.parseInt(params.get("correctCount").toString()) : 0;
-        int earned = correct * 5;
+        Long recordId = ParamUtils.getLong(params.get("recordId"));
         Long memberId = ParamUtils.getLong(params.get("memberId"), currentMemberId);
-        if (memberId != null && earned > 0) try { energyService.awardEnergy(memberId, "article_quiz", 0L, earned, "答题奖励", null); } catch (Exception e) {}
-        Map<String, Object> r = new HashMap<>();
-        r.put("correctCount", correct); r.put("totalQuestions", 3); r.put("earnedEnergy", earned);
-        return Result.success(r);
+
+        ArticleQuizRecord record = articleQuizRecordMapper.selectById(recordId);
+        if (record == null) return Result.error("答题记录不存在");
+        if (record.getAnswers() != null && !record.getAnswers().isEmpty()) {
+            return Result.error("已作答,不可重复提交");
+        }
+
+        int score = 0;
+        int totalQuestions = 0;
+        List<String> answerList = new ArrayList<>();
+        Map<String, int[]> dimStats = new HashMap<>(); // dimension -> [correct, total]
+        try {
+            JSONArray qArr = JSON.parseArray(record.getQuestions());
+            JSONArray aArr = parseAnswers(params.get("answers"));
+            totalQuestions = qArr != null ? qArr.size() : 0;
+            if (aArr != null) {
+                for (int i = 0; i < aArr.size(); i++) {
+                    answerList.add(aArr.getString(i));
+                }
+            }
+            for (int i = 0; i < totalQuestions; i++) {
+                JSONObject q = qArr.getJSONObject(i);
+                String correct = q.getString("answer");
+                String dim = q.getString("dimension");
+                if (dim == null || dim.isEmpty()) dim = "wisdom";
+                String selected = i < answerList.size() ? answerList.get(i) : "";
+                boolean isCorrect = correct != null && selected != null && correct.equalsIgnoreCase(selected.trim());
+                int[] stat = dimStats.computeIfAbsent(dim, k -> new int[2]);
+                stat[1]++;
+                if (isCorrect) { stat[0]++; score++; }
+            }
+        } catch (Exception e) {
+            log.warn("解析答题结果失败: {}", e.getMessage());
+        }
+
+        int cfEarned = score + (totalQuestions > 0 && score == totalQuestions ? 3 : 0);
+        int energyEarned = score * 5;
+
+        record.setAnswers(JSON.toJSONString(answerList));
+        record.setScore(score);
+        record.setTotalQuestions(totalQuestions);
+        record.setCfEarned(cfEarned);
+        record.setEnergyEarned(energyEarned);
+        record.setUpdatedAt(new Date());
+        articleQuizRecordMapper.updateById(record);
+
+        if (memberId != null) {
+            if (cfEarned > 0) {
+                try { pointsService.awardCfPoints(memberId, cfEarned, "文章答题奖励"); }
+                catch (Exception e) { log.warn("CF值发放失败: {}", e.getMessage()); }
+            }
+            if (energyEarned > 0) {
+                try { energyService.awardEnergy(memberId, "article_quiz", record.getArticleId(), energyEarned, "答题奖励", null); }
+                catch (Exception e) { log.warn("能量发放失败: {}", e.getMessage()); }
+            }
+        }
+
+        updateQuizSummary(memberId, score, totalQuestions, dimStats);
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("correctCount", score);
+        result.put("totalQuestions", totalQuestions);
+        result.put("cfEarned", cfEarned);
+        result.put("energyEarned", energyEarned);
+        return Result.success(result);
+    }
+
+    /**
+     * 兼容前端两种传参:answers 为 JSON 数组字符串("[\"A\",\"B\"]")或直接为数组(["A","B"])。
+     * 后端 @RequestBody 反序列化时数组会变成 List,直接 String.valueOf(list) 得到 "[A, B]" 无法被 fastjson 解析,
+     * 故必须区分处理。
+     */
+    private JSONArray parseAnswers(Object answers) {
+        if (answers == null) return null;
+        if (answers instanceof String) {
+            String s = ((String) answers).trim();
+            return s.isEmpty() ? null : JSON.parseArray(s);
+        }
+        if (answers instanceof List) {
+            return JSON.parseArray(JSON.toJSONString(answers));
+        }
+        if (answers instanceof JSONArray) {
+            return (JSONArray) answers;
+        }
+        return null;
+    }
+
+    private void updateQuizSummary(Long memberId, int score, int totalQuestions, Map<String, int[]> dimStats) {
+        if (memberId == null) return;
+        try {
+            ProfileSnapshot snapshot = profileSnapshotMapper.selectOne(
+                    new LambdaQueryWrapper<ProfileSnapshot>().eq(ProfileSnapshot::getMemberId, memberId));
+
+            JSONObject summary = new JSONObject();
+            if (snapshot != null && snapshot.getQuizSummary() != null && !snapshot.getQuizSummary().isEmpty()) {
+                summary = JSON.parseObject(snapshot.getQuizSummary());
+            }
+
+            int prevCorrect = summary.getIntValue("correct_count");
+            int prevTotal = summary.getIntValue("total_count");
+            int newCorrect = prevCorrect + score;
+            int newTotal = prevTotal + totalQuestions;
+            summary.put("quiz_count", summary.getIntValue("quiz_count") + 1);
+            summary.put("correct_count", newCorrect);
+            summary.put("total_count", newTotal);
+            summary.put("correct_rate", newTotal > 0 ? (double) newCorrect / newTotal : 0.0);
+            summary.put("last_quiz_at", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()));
+
+            JSONObject byDim = summary.getJSONObject("by_dimension");
+            if (byDim == null) byDim = new JSONObject();
+            for (Map.Entry<String, int[]> entry : dimStats.entrySet()) {
+                String dim = entry.getKey();
+                int correct = entry.getValue()[0];
+                int total = entry.getValue()[1];
+                JSONObject dimStat = byDim.getJSONObject(dim);
+                if (dimStat == null) dimStat = new JSONObject();
+                int dimCorrect = dimStat.getIntValue("correct_count") + correct;
+                int dimTotal = dimStat.getIntValue("total_count") + total;
+                dimStat.put("correct_count", dimCorrect);
+                dimStat.put("total_count", dimTotal);
+                dimStat.put("correct_rate", dimTotal > 0 ? (double) dimCorrect / dimTotal : 0.0);
+                byDim.put(dim, dimStat);
+            }
+            summary.put("by_dimension", byDim);
+
+            if (snapshot == null) {
+                snapshot = new ProfileSnapshot();
+                snapshot.setMemberId(memberId);
+                snapshot.setQuizSummary(summary.toJSONString());
+                snapshot.setUpdatedAt(new Date());
+                profileSnapshotMapper.insert(snapshot);
+            } else {
+                snapshot.setQuizSummary(summary.toJSONString());
+                snapshot.setUpdatedAt(new Date());
+                profileSnapshotMapper.updateById(snapshot);
+            }
+        } catch (Exception e) {
+            log.warn("更新认知雷达失败: {}", e.getMessage());
+        }
     }
 }