# 文章阅读后答题奖励 + 认知雷达记录 — 设计规格 > 日期:2026-09-19 > 状态:待审查 > 范围:cfc-backend + cfc-langgraph + cfc-frontend(小程序文章详情页) ## 1. 需求概述 用户读完文章(达到阅读时长)后,系统根据文章内容 + 用户画像 AI 生成若干道选择题。用户作答后,根据答对数量发放 **CF 值** 和 **五维能量**,并将题目与作答情况写入用户的 **认知雷达**(`profile_snapshot` 扩展字段),作为后续精准推送的基础。 ## 2. 现状盘点 | 环节 | 现状 | 差距 | |------|------|------| | 阅读计时/达标 | 前端计时 + `/api/articles/report-reading-time` + `/api/articles/complete-read`(+5能量) | 无 | | AI 出题 | `/api/articles/quiz/generate` 调 `aiService.sendWorkflow("article-quiz")`(Dify),失败回退 3 道硬编码题 | 未走 AiGateway + LangGraph | | 提交答题 | `/api/articles/quiz/submit` 只算分、发能量(答对×5),**未持久化**、未发 CF 值、未写画像 | 缺记录表、缺 CF、缺画像 | | 记录表 | `ArticleQuizRecord` 实体存在(`@TableName("article_quiz_records")`),但 schema.sql **无建表语句** | 需建表 | | 认知雷达 | `profile_snapshot` 表存在(含 `dimension_scores`/`problem_domains` JSON),无答题维度 | 需扩展 | | 奖励服务 | `PointsService.awardCfPoints(memberId, amount, reason)`、`EnergyService.awardEnergy(memberId, sourceType, sourceId, totalAmount, desc, daysToExpire)` 均可用 | 需接线 | ## 3. 架构与数据流 ``` 用户读完文章(前端计时达标) │ ├─ ① POST /api/articles/complete-read → 发阅读能量(已存在) │ ├─ ② POST /api/articles/quiz/generate → AiGateway.generateArticleQuiz() │ └─ LangGraph /api/v1/article/quiz/generate(article_quiz_graph) │ 失败 → 降级到现有硬编码 3 题 fallback │ ├─ ③ 用户作答(前端 quiz 弹窗) │ └─ ④ POST /api/articles/quiz/submit ├─ 落库 article_quiz_records(题目+作答+得分) ├─ 发 CF 值(PointsService.awardCfPoints) ├─ 发五维能量(EnergyService.awardEnergy,按文章关联维度分配) └─ 更新 profile_snapshot.quiz_summary(认知雷达) ``` ## 4. 数据模型 ### 4.1 新增表 `article_quiz_records` 实体 `ArticleQuizRecord` 已存在,建表时采用规范字段(表尚不存在,无需历史兼容,直接建最终结构): | 字段 | 类型 | 说明 | |------|------|------| | id | BIGINT PK AI | 主键 | | article_id | BIGINT | 文章 ID | | user_id | BIGINT | 登录用户 ID | | member_id | BIGINT | 家庭成员 ID(作答人,统一口径,见 `docs/architecture/userid-vs-memberid-usage-guide.md`) | | questions | TEXT | 题目快照 JSON(含正确答案与维度) | | answers | TEXT | 用户作答 JSON(含每题所选选项),作答前为 NULL | | score | INT | 答对题数 | | total_questions | INT | 题目总数 | | cf_earned | INT | 发放的 CF 值 | | energy_earned | INT | 发放的能量值 | | created_at | DATETIME | 创建时间 | | updated_at | DATETIME | 更新时间 | > 实体现有 `childId`/`pointsEarned` 字段不再使用(表为全新创建,不引入 childId 口径);`questions`/`answers` 字段直接承载 JSON。实体需同步调整:去掉 `childId`/`pointsEarned`,新增 `memberId`/`totalQuestions`/`cfEarned`/`energyEarned`。 ### 4.2 扩展 `profile_snapshot` 新增一列: | 字段 | 类型 | 说明 | |------|------|------| | quiz_summary | JSON | 答题认知雷达汇总:`{"quiz_count":N,"correct_rate":0.8,"by_dimension":{"wisdom":{"quiz_count":n,"correct_rate":r},...},"last_quiz_at":"..."}` | `by_dimension` 的 key 使用五维代码(body/mind/wisdom/action/wealth),来自文章的 `related_dimensions` / `dimension_weights`。 ## 5. AI 出题(LangGraph) ### 5.1 新 graph:`cfc-langgraph/app/graphs/article_quiz_graph.py` - 输入:`article_title`、`article_content`(截断至合理长度)、`profile_summary`(用户画像摘要,可选) - 输出:3 道单选题,结构化 JSON: ```json [ { "question": "问题文本", "options": ["A. xxx", "B. xxx", "C. xxx", "D. xxx"], "answer": "A", "dimension": "wisdom", "explanation": "解析文本(可选)" } ] ``` - `dimension` 取值与文章 `related_dimensions` 对齐,缺省为 `wisdom`。 - 系统提示词走 `prompt_service.get_prompt("article_quiz")`,无则用内置 DEFAULT_PROMPTS 兜底(需在 `prompt_service.py` 的 `DEFAULT_PROMPTS` 增加 `article_quiz` 条目)。 ### 5.2 新路由:`cfc-langgraph/app/api/article_quiz.py` - `POST /api/v1/article/quiz/generate`,Pydantic 模型放 `app/models/article_quiz.py`。 - 响应:`{"questions": [...], "fallback_used": false}`。 - 在 `app/main.py` 注册 `include_router(article_quiz.router)`。 ### 5.3 AiGateway 新方法 `AiGateway.generateArticleQuiz(articleTitle, articleContent, profileSummary)`: - 调 `baseUrl + "/api/v1/article/quiz/generate"` - 遵循现有熔断/超时/Fallback 模式,失败返回 `null`,由调用方降级。 ## 6. 后端接口改造 ### 6.1 `/api/articles/quiz/generate`(ArticleCommentController) - 改为:先调 `aiGateway.generateArticleQuiz(...)`,成功则取得题目列表。 - 失败则降级到现有硬编码 3 题 fallback(保持不变)。 - 组装 `profile_summary`:从 `profile_snapshot` 读取当前成员画像摘要传入 AI。 - **建记录并返回 recordId**:将题目快照(含正确答案)落库到 `article_quiz_records`(`answers` 为 NULL),返回 `{recordId, questions}`。判分以库内答案为唯一依据,前端无法篡改。 ### 6.2 `/api/articles/quiz/submit`(ArticleCommentController) 改造为完整落库 + 双奖励 + 画像更新: 1. 接收 `recordId`、`memberId`、`answers`(含每题所选选项)。 2. 按 `recordId` 读取库内题目快照,后端自行判分:计算 `score`(答对题数)、`totalQuestions`。 3. 回填 `article_quiz_records`(作答 JSON + 得分 + 奖励额度),拒绝重复提交(`answers` 已非 NULL)。 4. 发放奖励: - **CF 值**:`pointsService.awardCfPoints(memberId, cfEarned, "文章答题奖励")` - **五维能量**:`energyService.awardEnergy(memberId, "article_quiz", articleId, energyEarned, "文章答题奖励", null)`,按文章维度分配。 5. 更新 `profile_snapshot.quiz_summary`(认知雷达汇总,见 4.2)。 6. 返回 `{correctCount, totalQuestions, cfEarned, energyEarned}`。 > 判分与现有 `ArticleService.getAiQuestions`/`submitAnswers` 模式一致(出题建记录 → 提交按 recordId 读库判分),保持代码风格统一。 ### 6.3 奖励规则(可配置) | 场景 | CF 值 | 能量 | |------|-------|------| | 阅读完成 | 0 | +5(已存在) | | 答题:每答对 1 题 | +1 | +5 | | 答题:全对额外奖励 | +3 | 0 | > 规则常量先硬编码在 Service 层,后续如需运营可配置,再迁移到 `sys_config`(YAGNI,本期不引入配置项)。 ## 7. 前端改造(cfc-frontend) 文件:`pages/article-center/article-detail.vue` - `startQuiz`:调用 `generateQuiz` 不变,响应结构仍为题目数组(后端已兼容)。 - `submitQuiz`:改为传「每题所选选项」而非 `correctCount`;结果展示改为同时显示 CF 值 + 能量。 - 结果弹窗 `result-dialog`:增加「CF 值 +X」展示。 > 前端遵循:禁止可选链 `?.`、禁止 CSS Grid、`:key` 用方法调用、日期用 `parseDate()`。小程序打包由 HBuilderX 完成,Agent 只做语法校验。 ## 8. 迁移清单 `DatabaseInitializer.runMigrations()` 新增(最新迁移编号 298,递增): - **迁移 299**:创建 `article_quiz_records` 表。 - **迁移 300**:`profile_snapshot` 增加 `quiz_summary` JSON 列(`ensureColumn`)。 同步更新 `schema.sql`(建表语句 + profile_snapshot 列)。 ## 9. 错误处理与边界 - **AI 出题失败**:降级到硬编码 fallback,不影响答题主流程。 - **重复提交**:`article_quiz_records` 已存在作答记录(`answers_json` 非空)时拒绝重复提交(与现有 `submitAnswers` 逻辑一致)。 - **成员不存在**:`awardCfPoints` 内部抛「孩子不存在」,提交接口需捕获并降级(能量/CF 失败不阻断答题结果返回)。 - **文章无维度**:`dimension` 缺省 `wisdom`,`quiz_summary.by_dimension` 使用 `wisdom` 兜底。 ## 10. 测试 - 后端:`mvn clean compile` 验证编译通过(项目当前唯一验证方式)。 - LangGraph:本地 `uvicorn` 起服务后 curl `/api/v1/article/quiz/generate` 验证返回 3 题结构化 JSON。 - 前端:`node --check` 校验修改的 script 块语法(不打包,HBuilderX 负责打包)。 ## 11. 成功标准 - [ ] 阅读达标后弹出 AI 出题的答题弹窗(AI 失败时 fallback 3 题)。 - [ ] 提交答题后:落库 `article_quiz_records`、发放 CF 值 + 五维能量、更新 `profile_snapshot.quiz_summary`。 - [ ] 前端结果弹窗展示「答对 X/Y 题」「+CF 值」「+能量」。 - [ ] `mvn clean compile` 通过。