|
|
@@ -0,0 +1,1303 @@
|
|
|
+# 五维家庭自检 AI 结合(P0 + P1)实现计划
|
|
|
+
|
|
|
+> **面向 AI 代理的工作者:** 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
|
|
|
+
|
|
|
+**目标:** 将五维自检结果从静态五行寻源表(`WuxingSourcingService`)替换为 LangGraph AI 生成建议,并叠加健康计划生成、趋势分析、Chat 上下文注入三个能力。
|
|
|
+
|
|
|
+**架构:** LangGraph 新增 2 个 graph(`self_check_analysis_graph`、`self_check_trend_graph`)+ 1 个 api 模块;Java 新增 `SelfCheckAnalysisService` + 2 个 AiGateway 方法 + 3 个 Controller 接口;前端新增 API 方法 + 改造结果页/中间页/Chat。
|
|
|
+
|
|
|
+**技术栈:** Java 8 / Spring Boot 2.7.18 / MyBatis-Plus / Python 3.11 + FastAPI + LangGraph + LangChain / uni-app Vue 2
|
|
|
+
|
|
|
+**规格文档:** `docs/superpowers/specs/2026-08-31-self-check-ai-integration-design.md`
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 文件结构
|
|
|
+
|
|
|
+| 文件 | 职责 | 变更 |
|
|
|
+|------|------|------|
|
|
|
+| `cfc-langgraph/app/graphs/self_check_analysis_graph.py` | P0-1:自检建议 AI 生成 graph | 新建 |
|
|
|
+| `cfc-langgraph/app/graphs/self_check_trend_graph.py` | P1-1:趋势分析 AI 生成 graph | 新建 |
|
|
|
+| `cfc-langgraph/app/api/self_check.py` | 注册 P0-1 + P1-1 路由 | 新建 |
|
|
|
+| `cfc-langgraph/app/main.py` | 注册 self_check router | 修改 |
|
|
|
+| `cfc-backend/src/main/java/com/etotem/cfc/dto/WuxingSourcingAdviceVO.java` | VO 改造(去 upstream/restrainer/action,加 AI 字段) | 修改 |
|
|
|
+| `cfc-backend/src/main/java/com/etotem/cfc/service/SelfCheckAnalysisService.java` | P0-1:调 AiGateway 生成建议 | 新建 |
|
|
|
+| `cfc-backend/src/main/java/com/etotem/cfc/service/WuxingSourcingService.java` | 删除 SOURCING_TABLE/getAdvice/getAdvicesForLowScores | 修改 |
|
|
|
+| `cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionSelfCheckService.java` | submitSelfCheck 调新 Service;新增 trend 方法 | 修改 |
|
|
|
+| `cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java` | 新增 generateSelfCheckAdvice/generateSelfCheckTrend | 修改 |
|
|
|
+| `cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java` | 新增 generatePlan、trendAnalysis 接口;submit 改造 | 修改 |
|
|
|
+| `cfc-backend/src/main/java/com/etotem/cfc/service/HealthPlanService.java` | 新增 generateFromSelfCheck 接口 | 修改 |
|
|
|
+| `cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthPlanServiceImpl.java` | 实现 generateFromSelfCheck | 修改 |
|
|
|
+| `cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java` | sendMessage 支持 selfCheckId 参数 | 修改 |
|
|
|
+| `cfc-frontend/utils/api.js` | 新增 3 个 API 方法 | 修改 |
|
|
|
+| `cfc-frontend/pages/family/self-check-result.vue` | P0-1/P0-2/P1-2 展示改造 | 修改 |
|
|
|
+| `cfc-frontend/pages/family/self-check-entry.vue` | P1-1 趋势展示 | 修改 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 1:LangGraph — self_check_analysis_graph(P0-1 核心)
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 创建:`cfc-langgraph/app/graphs/self_check_analysis_graph.py`
|
|
|
+- 创建:`cfc-langgraph/app/api/self_check.py`
|
|
|
+
|
|
|
+- [ ] **步骤 1:创建 self_check_analysis_graph.py**
|
|
|
+
|
|
|
+创建文件 `cfc-langgraph/app/graphs/self_check_analysis_graph.py`:
|
|
|
+
|
|
|
+```python
|
|
|
+import json
|
|
|
+import logging
|
|
|
+from typing import TypedDict, Optional
|
|
|
+from langgraph.graph import StateGraph, START, END
|
|
|
+from langchain_core.messages import SystemMessage, HumanMessage
|
|
|
+from app.llm.client import get_llm
|
|
|
+from app.monitoring import monitor_agent
|
|
|
+
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
+
|
|
|
+SYSTEM_PROMPT = """你是一位家庭健康顾问,基于五维自检结果(身·智·富·行·心,每维0-9分,满分45)给出个性化建议。
|
|
|
+
|
|
|
+要求:
|
|
|
+1. 对每个低分维度(≤6分)给出1-2句解读和2-3个具体可执行的微行动
|
|
|
+2. 如有历史数据,简要对比趋势(改善/下滑)
|
|
|
+3. 语气温暖口语化,每条解读不超过80字
|
|
|
+4. 最后给出1句家庭整体洞察(30字以内)
|
|
|
+
|
|
|
+返回 JSON(严格格式,不要额外文字):
|
|
|
+{
|
|
|
+ "advice": [
|
|
|
+ {
|
|
|
+ "dimension": "mind",
|
|
|
+ "dimensionName": "心",
|
|
|
+ "interpretation": "你的情绪能量偏低,可能最近压力较大,建议...",
|
|
|
+ "microActions": ["今晚睡前做10分钟深呼吸", "和伴侣约定每周一次夜谈"],
|
|
|
+ "fallbackUsed": false
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ "familyInsight": "建议从行动维度入手,关系顺畅了内心才能安定"
|
|
|
+}
|
|
|
+"""
|
|
|
+
|
|
|
+
|
|
|
+class GraphState(TypedDict):
|
|
|
+ scores: dict
|
|
|
+ question_ids: list
|
|
|
+ user_id: int
|
|
|
+ recent_history: list
|
|
|
+ advice: Optional[dict]
|
|
|
+ error: Optional[str]
|
|
|
+
|
|
|
+
|
|
|
+class SelfCheckAnalysisAgent:
|
|
|
+ def __init__(self):
|
|
|
+ self.llm = get_llm()
|
|
|
+
|
|
|
+ @monitor_agent("self_check_analysis")
|
|
|
+ async def run(self, scores: dict, question_ids: list, user_id: int, recent_history: list) -> dict:
|
|
|
+ try:
|
|
|
+ score_summary = "\n".join(
|
|
|
+ f"{v.get('dimensionName', k)}({k}): {v.get('score', 0)}分"
|
|
|
+ for k, v in scores.items()
|
|
|
+ )
|
|
|
+ history_summary = ""
|
|
|
+ if recent_history:
|
|
|
+ history_summary = "历史趋势:\n" + "\n".join(
|
|
|
+ f"- {h.get('createdAt', '')}: 总分{h.get('totalScore', 0)}分"
|
|
|
+ for h in recent_history[:3]
|
|
|
+ )
|
|
|
+ messages = [
|
|
|
+ SystemMessage(content=SYSTEM_PROMPT),
|
|
|
+ HumanMessage(content=f"用户ID: {user_id}\n当前自检得分:\n{score_summary}\n{history_summary}"),
|
|
|
+ ]
|
|
|
+ response = await self.llm.ainvoke(messages)
|
|
|
+ text = response.content.strip()
|
|
|
+ if "```json" in text:
|
|
|
+ text = text.split("```json")[1].split("```")[0].strip()
|
|
|
+ elif "```" in text:
|
|
|
+ text = text.split("```")[1].split("```")[0].strip()
|
|
|
+ data = json.loads(text)
|
|
|
+ advice_list = data.get("advice", [])
|
|
|
+ for item in advice_list:
|
|
|
+ item.setdefault("fallbackUsed", False)
|
|
|
+ if advice_list:
|
|
|
+ advice_list[0]["familyInsight"] = data.get("familyInsight", "")
|
|
|
+ return {"advice": {"advice_json": json.dumps(advice_list, ensure_ascii=False), "fallback_used": False}}
|
|
|
+ except Exception as e:
|
|
|
+ logger.warning("自检建议生成失败: %s", e)
|
|
|
+ return {"advice": {"advice_json": None, "fallback_used": True}}
|
|
|
+
|
|
|
+
|
|
|
+def build_graph():
|
|
|
+ agent = SelfCheckAnalysisAgent()
|
|
|
+
|
|
|
+ def parse_input(state: GraphState) -> GraphState:
|
|
|
+ return state
|
|
|
+
|
|
|
+ async def call_llm(state: GraphState) -> dict:
|
|
|
+ return await agent.run(state["scores"], state["question_ids"], state["user_id"], state["recent_history"])
|
|
|
+
|
|
|
+ def validate(state: GraphState) -> GraphState:
|
|
|
+ adv = state.get("advice")
|
|
|
+ if adv is None or adv.get("advice_json") is None:
|
|
|
+ return {**state, "error": "AI 建议生成失败"}
|
|
|
+ return state
|
|
|
+
|
|
|
+ graph = StateGraph(GraphState)
|
|
|
+ graph.add_node("parse", parse_input)
|
|
|
+ graph.add_node("llm", call_llm)
|
|
|
+ graph.add_node("validate", validate)
|
|
|
+ graph.add_edge(START, "parse")
|
|
|
+ graph.add_edge("parse", "llm")
|
|
|
+ graph.add_edge("llm", "validate")
|
|
|
+ graph.add_edge("validate", END)
|
|
|
+ return graph.compile()
|
|
|
+
|
|
|
+
|
|
|
+_graph = None
|
|
|
+
|
|
|
+
|
|
|
+def get_graph():
|
|
|
+ global _graph
|
|
|
+ if _graph is None:
|
|
|
+ _graph = build_graph()
|
|
|
+ return _graph
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:创建 self_check.py API 模块**
|
|
|
+
|
|
|
+创建文件 `cfc-langgraph/app/api/self_check.py`:
|
|
|
+
|
|
|
+```python
|
|
|
+import logging
|
|
|
+from fastapi import APIRouter
|
|
|
+from pydantic import BaseModel
|
|
|
+from typing import Any, Dict, Optional
|
|
|
+from app.graphs.self_check_analysis_graph import get_graph
|
|
|
+from app.graphs.self_check_trend_graph import get_trend_graph
|
|
|
+
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
+router = APIRouter(prefix="/api/v1", tags=["self-check"])
|
|
|
+
|
|
|
+
|
|
|
+class SelfCheckAnalysisRequest(BaseModel):
|
|
|
+ scores: Dict[str, Any]
|
|
|
+ question_ids: list
|
|
|
+ user_id: int
|
|
|
+ recent_history: Optional[list] = None
|
|
|
+
|
|
|
+
|
|
|
+class SelfCheckTrendRequest(BaseModel):
|
|
|
+ history: list
|
|
|
+ user_id: int
|
|
|
+
|
|
|
+
|
|
|
+@router.post("/self-check/analysis")
|
|
|
+async def self_check_analysis(req: SelfCheckAnalysisRequest):
|
|
|
+ graph = get_graph()
|
|
|
+ state = {
|
|
|
+ "scores": req.scores,
|
|
|
+ "question_ids": req.question_ids,
|
|
|
+ "user_id": req.user_id,
|
|
|
+ "recent_history": req.recent_history or [],
|
|
|
+ "advice": None,
|
|
|
+ "error": None,
|
|
|
+ }
|
|
|
+ result = await graph.ainvoke(state)
|
|
|
+ advice = result.get("advice") or {}
|
|
|
+ return {
|
|
|
+ "advice_json": advice.get("advice_json"),
|
|
|
+ "fallback_used": advice.get("fallback_used", True),
|
|
|
+ "error": result.get("error"),
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+@router.post("/self-check/trend")
|
|
|
+async def self_check_trend(req: SelfCheckTrendRequest):
|
|
|
+ graph = get_trend_graph()
|
|
|
+ state = {"history": req.history, "user_id": req.user_id, "insight": None, "error": None}
|
|
|
+ result = await graph.ainvoke(state)
|
|
|
+ insight = result.get("insight") or {}
|
|
|
+ return {
|
|
|
+ "aiInsight": insight.get("aiInsight", ""),
|
|
|
+ "trendSummary": insight.get("trendSummary", ""),
|
|
|
+ "error": result.get("error"),
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:注册路由**
|
|
|
+
|
|
|
+在 `cfc-langgraph/app/main.py` 中,于 `app.include_router(innate_portrait.router)` 之后追加:
|
|
|
+
|
|
|
+```python
|
|
|
+from app.api import self_check
|
|
|
+app.include_router(self_check.router)
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:语法校验**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-langgraph && python -m py_compile app/graphs/self_check_analysis_graph.py app/graphs/self_check_trend_graph.py app/api/self_check.py && echo "OK"
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-langgraph/app/graphs/self_check_analysis_graph.py \
|
|
|
+ cfc-langgraph/app/graphs/self_check_trend_graph.py \
|
|
|
+ cfc-langgraph/app/api/self_check.py \
|
|
|
+ cfc-langgraph/app/main.py
|
|
|
+git commit -m "feat(langgraph): 新增自检建议+trend graph + self_check 路由"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 2:LangGraph — self_check_trend_graph(P1-1 核心)
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 创建:`cfc-langgraph/app/graphs/self_check_trend_graph.py`
|
|
|
+
|
|
|
+- [ ] **步骤 1:创建 self_check_trend_graph.py**
|
|
|
+
|
|
|
+创建文件 `cfc-langgraph/app/graphs/self_check_trend_graph.py`:
|
|
|
+
|
|
|
+```python
|
|
|
+import json
|
|
|
+import logging
|
|
|
+from typing import TypedDict, Optional
|
|
|
+from langgraph.graph import StateGraph, START, END
|
|
|
+from langchain_core.messages import SystemMessage, HumanMessage
|
|
|
+from app.llm.client import get_llm
|
|
|
+from app.monitoring import monitor_agent
|
|
|
+
|
|
|
+logger = logging.getLogger(__name__)
|
|
|
+
|
|
|
+SYSTEM_PROMPT = """分析用户近3次五维自检趋势,输出:
|
|
|
+- aiInsight: 趋势解读(2-3句,指出最大变化维度和可能原因,口语化)
|
|
|
+- trendSummary: 各维度 delta 简写(如"身-2 智+1 富0 行-1 心+2")
|
|
|
+返回 JSON:{"aiInsight": "...", "trendSummary": "..."}
|
|
|
+"""
|
|
|
+
|
|
|
+
|
|
|
+class TrendState(TypedDict):
|
|
|
+ history: list
|
|
|
+ user_id: int
|
|
|
+ insight: Optional[dict]
|
|
|
+ error: Optional[str]
|
|
|
+
|
|
|
+
|
|
|
+class SelfCheckTrendAgent:
|
|
|
+ def __init__(self):
|
|
|
+ self.llm = get_llm()
|
|
|
+
|
|
|
+ @monitor_agent("self_check_trend")
|
|
|
+ async def run(self, history: list, user_id: int) -> dict:
|
|
|
+ try:
|
|
|
+ if len(history) < 2:
|
|
|
+ return {"insight": {"aiInsight": "自检次数不足,建议完成至少2次自检后查看趋势", "trendSummary": ""}, "error": None}
|
|
|
+ history_text = "\n".join(
|
|
|
+ f"{h.get('createdAt', '')}: 总分{h.get('totalScore', 0)}," +
|
|
|
+ " ".join(f"{d.get('name','')}{d.get('score',0)}分" for d in h.get('dimensions', []))
|
|
|
+ for h in history[-3:]
|
|
|
+ )
|
|
|
+ messages = [
|
|
|
+ SystemMessage(content=SYSTEM_PROMPT),
|
|
|
+ HumanMessage(content=f"用户{user_id}的自检历史:\n{history_text}"),
|
|
|
+ ]
|
|
|
+ response = await self.llm.ainvoke(messages)
|
|
|
+ text = response.content.strip()
|
|
|
+ if "```json" in text:
|
|
|
+ text = text.split("```json")[1].split("```")[0].strip()
|
|
|
+ elif "```" in text:
|
|
|
+ text = text.split("```")[1].split("```")[0].strip()
|
|
|
+ data = json.loads(text)
|
|
|
+ return {"insight": data, "error": None}
|
|
|
+ except Exception as e:
|
|
|
+ logger.warning("趋势分析失败: %s", e)
|
|
|
+ return {"insight": {"aiInsight": "", "trendSummary": ""}, "error": str(e)}
|
|
|
+
|
|
|
+
|
|
|
+def build_trend_graph():
|
|
|
+ agent = SelfCheckTrendAgent()
|
|
|
+
|
|
|
+ async def call_llm(state: TrendState) -> dict:
|
|
|
+ return await agent.run(state["history"], state["user_id"])
|
|
|
+
|
|
|
+ graph = StateGraph(TrendState)
|
|
|
+ graph.add_node("llm", call_llm)
|
|
|
+ graph.add_edge(START, "llm")
|
|
|
+ graph.add_edge("llm", END)
|
|
|
+ return graph.compile()
|
|
|
+
|
|
|
+
|
|
|
+_trend_graph = None
|
|
|
+
|
|
|
+
|
|
|
+def get_trend_graph():
|
|
|
+ global _trend_graph
|
|
|
+ if _trend_graph is None:
|
|
|
+ _trend_graph = build_trend_graph()
|
|
|
+ return _trend_graph
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:语法校验**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-langgraph && python -m py_compile app/graphs/self_check_trend_graph.py && echo "OK"
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-langgraph/app/graphs/self_check_trend_graph.py
|
|
|
+git commit -m "feat(langgraph): 新增 self_check_trend_graph 趋势分析实现"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 3:Java — WuxingSourcingAdviceVO 改造 + SelfCheckAnalysisService
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/dto/WuxingSourcingAdviceVO.java`
|
|
|
+- 新建:`cfc-backend/src/main/java/com/etotem/cfc/service/SelfCheckAnalysisService.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:改造 WuxingSourcingAdviceVO**
|
|
|
+
|
|
|
+将 `cfc-backend/src/main/java/com/etotem/cfc/dto/WuxingSourcingAdviceVO.java` 的字段替换为:
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.dto;
|
|
|
+
|
|
|
+import com.fasterxml.jackson.annotation.JsonProperty;
|
|
|
+import lombok.Data;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+@Data
|
|
|
+public class WuxingSourcingAdviceVO {
|
|
|
+
|
|
|
+ private String dimension;
|
|
|
+ private String dimensionName;
|
|
|
+ private String element;
|
|
|
+ private String color;
|
|
|
+ private Integer score;
|
|
|
+ private String level;
|
|
|
+ private String levelName;
|
|
|
+
|
|
|
+ /** AI 解读(替换原 upstreamReason) */
|
|
|
+ private String interpretation;
|
|
|
+
|
|
|
+ /** AI 微行动列表 */
|
|
|
+ private List<String> microActions;
|
|
|
+
|
|
|
+ /** AI 补充洞察 */
|
|
|
+ private String aiInsight;
|
|
|
+
|
|
|
+ /** 家庭整体洞察(仅第一条携带) */
|
|
|
+ private String familyInsight;
|
|
|
+
|
|
|
+ /** 是否使用降级静态建议 */
|
|
|
+ @JsonProperty("fallbackUsed")
|
|
|
+ private Boolean fallbackUsed;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:创建 SelfCheckAnalysisService**
|
|
|
+
|
|
|
+创建文件 `cfc-backend/src/main/java/com/etotem/cfc/service/SelfCheckAnalysisService.java`:
|
|
|
+
|
|
|
+```java
|
|
|
+package com.etotem.cfc.service;
|
|
|
+
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.etotem.cfc.entity.FiveDimensionSelfCheck;
|
|
|
+import com.etotem.cfc.mapper.FiveDimensionSelfCheckMapper;
|
|
|
+import com.fasterxml.jackson.core.type.TypeReference;
|
|
|
+import com.fasterxml.jackson.databind.ObjectMapper;
|
|
|
+import org.slf4j.Logger;
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import javax.annotation.Resource;
|
|
|
+import java.util.*;
|
|
|
+
|
|
|
+@Service
|
|
|
+public class SelfCheckAnalysisService {
|
|
|
+
|
|
|
+ private static final Logger log = LoggerFactory.getLogger(SelfCheckAnalysisService.class);
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private AiGateway aiGateway;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private FiveDimensionSelfCheckMapper selfCheckMapper;
|
|
|
+
|
|
|
+ private final ObjectMapper objectMapper = new ObjectMapper();
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成自检建议(AI 优先,失败返回 adviceJson=null + fallbackUsed=true)
|
|
|
+ */
|
|
|
+ public Map<String, Object> generateAdvice(Long userId, Map<String, Integer> scoreMap, List<Integer> questionIds) {
|
|
|
+ try {
|
|
|
+ List<Map<String, Object>> recentHistory = getRecentHistory(userId, 3);
|
|
|
+ Map<String, Object> scoresWithMeta = new LinkedHashMap<>();
|
|
|
+ for (Map.Entry<String, Integer> entry : scoreMap.entrySet()) {
|
|
|
+ Map<String, Object> m = new LinkedHashMap<>();
|
|
|
+ m.put("dimension", entry.getKey());
|
|
|
+ m.put("dimensionName", getDimensionName(entry.getKey()));
|
|
|
+ m.put("score", entry.getValue());
|
|
|
+ scoresWithMeta.put(entry.getKey(), m);
|
|
|
+ }
|
|
|
+ Map<String, Object> inputs = new LinkedHashMap<>();
|
|
|
+ inputs.put("scores", scoresWithMeta);
|
|
|
+ inputs.put("questionIds", questionIds);
|
|
|
+ inputs.put("userId", userId);
|
|
|
+ inputs.put("recentHistory", recentHistory);
|
|
|
+ Map<String, Object> result = aiGateway.generateSelfCheckAdvice(inputs);
|
|
|
+ if (result == null) {
|
|
|
+ log.info("AI 自检建议生成失败,fallback");
|
|
|
+ return Map.of("adviceJson", null, "fallbackUsed", true);
|
|
|
+ }
|
|
|
+ String adviceJson = (String) result.get("advice_json");
|
|
|
+ Boolean fallbackUsed = (Boolean) result.getOrDefault("fallback_used", false);
|
|
|
+ return Map.of("adviceJson", adviceJson, "fallbackUsed", fallbackUsed);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("自检建议生成异常: {}", e.getMessage());
|
|
|
+ return Map.of("adviceJson", null, "fallbackUsed", true);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 生成趋势分析(AI 优先,失败返回空 insight)
|
|
|
+ */
|
|
|
+ public Map<String, Object> generateTrend(Long userId) {
|
|
|
+ try {
|
|
|
+ List<Map<String, Object>> history = getRecentHistory(userId, 3);
|
|
|
+ Map<String, Object> inputs = Map.of("history", history, "userId", userId);
|
|
|
+ Map<String, Object> result = aiGateway.generateSelfCheckTrend(inputs);
|
|
|
+ if (result == null) {
|
|
|
+ return Map.of("aiInsight", "", "trendSummary", "");
|
|
|
+ }
|
|
|
+ return Map.of(
|
|
|
+ "aiInsight", result.getOrDefault("aiInsight", ""),
|
|
|
+ "trendSummary", result.getOrDefault("trendSummary", "")
|
|
|
+ );
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("趋势分析异常: {}", e.getMessage());
|
|
|
+ return Map.of("aiInsight", "", "trendSummary", "");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 获取最近 N 次自检历史(供 AI 和前端使用) */
|
|
|
+ public List<Map<String, Object>> getRecentHistory(Long userId, int limit) {
|
|
|
+ try {
|
|
|
+ List<FiveDimensionSelfCheck> records = selfCheckMapper.selectList(
|
|
|
+ new LambdaQueryWrapper<FiveDimensionSelfCheck>()
|
|
|
+ .eq(FiveDimensionSelfCheck::getUserId, userId)
|
|
|
+ .orderByDesc(FiveDimensionSelfCheck::getCreatedAt)
|
|
|
+ .last("LIMIT " + limit)
|
|
|
+ );
|
|
|
+ List<Map<String, Object>> result = new ArrayList<>();
|
|
|
+ for (FiveDimensionSelfCheck r : records) {
|
|
|
+ Map<String, Object> m = new LinkedHashMap<>();
|
|
|
+ m.put("createdAt", r.getCreatedAt() != null ? r.getCreatedAt().toString() : "");
|
|
|
+ m.put("totalScore", r.getTotalScore());
|
|
|
+ if (r.getScoresJson() != null) {
|
|
|
+ try {
|
|
|
+ Map<String, Integer> scores = objectMapper.readValue(r.getScoresJson(),
|
|
|
+ new TypeReference<Map<String, Integer>>() {});
|
|
|
+ List<Map<String, Object>> dims = new ArrayList<>();
|
|
|
+ for (Map.Entry<String, Integer> e : scores.entrySet()) {
|
|
|
+ Map<String, Object> d = new LinkedHashMap<>();
|
|
|
+ d.put("dimension", e.getKey());
|
|
|
+ d.put("name", getDimensionName(e.getKey()));
|
|
|
+ d.put("score", e.getValue());
|
|
|
+ dims.add(d);
|
|
|
+ }
|
|
|
+ m.put("dimensions", dims);
|
|
|
+ } catch (Exception ignored) {}
|
|
|
+ }
|
|
|
+ result.add(m);
|
|
|
+ }
|
|
|
+ return result;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("获取自检历史失败: {}", e.getMessage());
|
|
|
+ return Collections.emptyList();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private String getDimensionName(String dim) {
|
|
|
+ switch (dim) {
|
|
|
+ case "body": return "身";
|
|
|
+ case "wisdom": return "智";
|
|
|
+ case "wealth": return "富";
|
|
|
+ case "action": return "行";
|
|
|
+ case "mind": return "心";
|
|
|
+ default: return dim;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -q
|
|
|
+```
|
|
|
+
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 4:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/dto/WuxingSourcingAdviceVO.java \
|
|
|
+ cfc-backend/src/main/java/com/etotem/cfc/service/SelfCheckAnalysisService.java
|
|
|
+git commit -m "feat(self-check): WuxingSourcingAdviceVO 改造为 AI 建议 VO + SelfCheckAnalysisService"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 4:Java — AiGateway 新增方法
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:新增 generateSelfCheckAdvice 方法**
|
|
|
+
|
|
|
+在 `AiGateway.java` 中,于 `generateInnateReading` 方法之后追加:
|
|
|
+
|
|
|
+```java
|
|
|
+/**
|
|
|
+ * 生成自检 AI 建议(调用 LangGraph self_check_analysis_graph)
|
|
|
+ * @return 含 "advice_json" 和 "fallback_used" 的 Map;失败返回 null
|
|
|
+ */
|
|
|
+public Map<String, Object> generateSelfCheckAdvice(Map<String, Object> inputs) {
|
|
|
+ if (!enabled || isCircuitOpen()) return null;
|
|
|
+ try {
|
|
|
+ ObjectNode body = objectMapper.valueToTree(inputs);
|
|
|
+ HttpEntity<String> entity = new HttpEntity<>(body.toString(), createJsonHeaders());
|
|
|
+ String url = baseUrl + "/api/v1/self-check/analysis";
|
|
|
+ ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
|
|
|
+ if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
|
|
+ JsonNode root = objectMapper.readTree(response.getBody());
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("advice_json", root.has("advice_json") ? root.get("advice_json").asText() : null);
|
|
|
+ result.put("fallback_used", root.has("fallback_used") ? root.get("fallback_used").asBoolean() : true);
|
|
|
+ consecutiveFailures.set(0);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("AiGateway generateSelfCheckAdvice 调用失败: {}", e.getMessage());
|
|
|
+ recordFailure();
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 生成自检趋势分析(调用 LangGraph self_check_trend_graph)
|
|
|
+ * @return 含 "aiInsight" 和 "trendSummary" 的 Map;失败返回 null
|
|
|
+ */
|
|
|
+public Map<String, Object> generateSelfCheckTrend(Map<String, Object> inputs) {
|
|
|
+ if (!enabled || isCircuitOpen()) return null;
|
|
|
+ try {
|
|
|
+ ObjectNode body = objectMapper.valueToTree(inputs);
|
|
|
+ HttpEntity<String> entity = new HttpEntity<>(body.toString(), createJsonHeaders());
|
|
|
+ String url = baseUrl + "/api/v1/self-check/trend";
|
|
|
+ ResponseEntity<String> response = restTemplate.postForEntity(url, entity, String.class);
|
|
|
+ if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
|
|
|
+ JsonNode root = objectMapper.readTree(response.getBody());
|
|
|
+ Map<String, Object> result = new LinkedHashMap<>();
|
|
|
+ result.put("aiInsight", root.has("aiInsight") ? root.get("aiInsight").asText() : "");
|
|
|
+ result.put("trendSummary", root.has("trendSummary") ? root.get("trendSummary").asText() : "");
|
|
|
+ consecutiveFailures.set(0);
|
|
|
+ return result;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("AiGateway generateSelfCheckTrend 调用失败: {}", e.getMessage());
|
|
|
+ recordFailure();
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -q
|
|
|
+```
|
|
|
+
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java
|
|
|
+git commit -m "feat(self-check): AiGateway 新增 generateSelfCheckAdvice/generateSelfCheckTrend"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 5:Java — WuxingSourcingService 清理 + FiveDimensionSelfCheckService 改造
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/WuxingSourcingService.java`
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionSelfCheckService.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:清理 WuxingSourcingService**
|
|
|
+
|
|
|
+删除 `WuxingSourcingService.java` 中的:
|
|
|
+- `SOURCING_TABLE` 静态字段(第 41-68 行)
|
|
|
+- `getAdvice()` 方法(第 78-109 行)
|
|
|
+- `getAdvicesForLowScores()` 方法(第 117-132 行)
|
|
|
+
|
|
|
+保留:`DIMENSION_META`、`levelOf()`、`levelName()`、`dimensionName()`
|
|
|
+
|
|
|
+- [ ] **步骤 2:FiveDimensionSelfCheckService 改造 submitSelfCheck**
|
|
|
+
|
|
|
+在 `submitSelfCheck` 方法中,将第 472-477 行(`wuxingSourcingService.getAdvicesForLowScores` + 落库 adviceJson)替换为:
|
|
|
+
|
|
|
+```java
|
|
|
+ // 生成 AI 建议(AI 优先,失败降级为空建议)
|
|
|
+ String adviceJson = null;
|
|
|
+ try {
|
|
|
+ Map<String, Object> adviceResult = selfCheckAnalysisService.generateAdvice(userId, scoreMap, submittedQuestionIds);
|
|
|
+ adviceJson = (String) adviceResult.get("adviceJson");
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("AI 建议生成异常,使用空建议: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ record.setAdviceJson(adviceJson != null ? adviceJson : "[]");
|
|
|
+```
|
|
|
+
|
|
|
+同时新增注入:
|
|
|
+
|
|
|
+```java
|
|
|
+ @Resource
|
|
|
+ private SelfCheckAnalysisService selfCheckAnalysisService;
|
|
|
+```
|
|
|
+
|
|
|
+并将 `vo.setAdvices(...)` 改为根据 adviceJson 解析(若无 AI 结果则空列表):
|
|
|
+
|
|
|
+```java
|
|
|
+ vo.setAdvices(parseAdvices(adviceJson));
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:新增 parseAdvices + trend 方法**
|
|
|
+
|
|
|
+在 Service 中新增:
|
|
|
+
|
|
|
+```java
|
|
|
+ private List<WuxingSourcingAdviceVO> parseAdvices(String adviceJson) {
|
|
|
+ if (adviceJson == null || adviceJson.isEmpty() || "[]".equals(adviceJson)) {
|
|
|
+ return new ArrayList<>();
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return objectMapper.readValue(adviceJson,
|
|
|
+ objectMapper.getTypeFactory().constructCollectionType(List.class, WuxingSourcingAdviceVO.class));
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("解析 AI 建议 JSON 失败: {}", e.getMessage());
|
|
|
+ return new ArrayList<>();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 获取自检趋势分析结果(含历史列表) */
|
|
|
+ public Map<String, Object> getTrendAnalysis(Long userId) {
|
|
|
+ Map<String, Object> trend = selfCheckAnalysisService.generateTrend(userId);
|
|
|
+ Map<String, Object> resp = new HashMap<>();
|
|
|
+ resp.put("history", selfCheckAnalysisService.getRecentHistory(userId, 3));
|
|
|
+ resp.put("aiInsight", trend.getOrDefault("aiInsight", ""));
|
|
|
+ resp.put("trendSummary", trend.getOrDefault("trendSummary", ""));
|
|
|
+ return resp;
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -q
|
|
|
+```
|
|
|
+
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 5:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/service/WuxingSourcingService.java \
|
|
|
+ cfc-backend/src/main/java/com/etotem/cfc/service/FiveDimensionSelfCheckService.java
|
|
|
+git commit -m "feat(self-check): 清理 WuxingSourcingService 静态表 + submitSelfCheck 接 AI"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 6:Java — Controller 新增接口 + HealthPlanService
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java`
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/HealthPlanService.java`
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthPlanServiceImpl.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:Controller 新增 generatePlan 接口**
|
|
|
+
|
|
|
+在 `FiveDimensionSelfCheckController.java` 中追加:
|
|
|
+
|
|
|
+```java
|
|
|
+ @Resource
|
|
|
+ private com.etotem.cfc.service.HealthPlanService healthPlanService;
|
|
|
+
|
|
|
+ @Operation(summary = "用户点击生成健康计划(基于自检低分维度)")
|
|
|
+ @PostMapping("/generate-plan")
|
|
|
+ public Result<Map<String, Object>> generatePlan(
|
|
|
+ @RequestBody(required = false) Map<String, Object> body,
|
|
|
+ @RequestAttribute("userId") Long userId) {
|
|
|
+ if (userId == null) return Result.error("请先登录");
|
|
|
+ Long checkId = body != null ? com.etotem.cfc.util.ParamUtils.getLong(body.get("checkId")) : null;
|
|
|
+ try {
|
|
|
+ Long planId = healthPlanService.generateFromSelfCheck(userId, checkId);
|
|
|
+ if (planId == null) return Result.error("计划生成失败,请稍后重试");
|
|
|
+ Map<String, Object> resp = new HashMap<>();
|
|
|
+ resp.put("planId", planId);
|
|
|
+ resp.put("status", "draft");
|
|
|
+ return Result.success(resp);
|
|
|
+ } catch (Exception e) {
|
|
|
+ return Result.error("计划生成失败");
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Operation(summary = "获取自检历史趋势分析")
|
|
|
+ @PostMapping("/trend-analysis")
|
|
|
+ public Result<Map<String, Object>> getTrendAnalysis(@RequestAttribute("userId") Long userId) {
|
|
|
+ if (userId == null) return Result.error("请先登录");
|
|
|
+ try {
|
|
|
+ return Result.success(selfCheckService.getTrendAnalysis(userId));
|
|
|
+ } catch (Exception e) {
|
|
|
+ return Result.error("趋势分析失败");
|
|
|
+ }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:HealthPlanService 接口新增 generateFromSelfCheck**
|
|
|
+
|
|
|
+在 `HealthPlanService.java` 中追加:
|
|
|
+
|
|
|
+```java
|
|
|
+ /** 基于自检低分维度自动生成 draft 计划 */
|
|
|
+ Long generateFromSelfCheck(Long userId, Long checkId);
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:HealthPlanServiceImpl 实现 generateFromSelfCheck**
|
|
|
+
|
|
|
+在 `HealthPlanServiceImpl.java` 中追加(需新增 `@Resource private com.etotem.cfc.mapper.FiveDimensionSelfCheckMapper selfCheckMapper;`):
|
|
|
+
|
|
|
+```java
|
|
|
+ @Override
|
|
|
+ public Long generateFromSelfCheck(Long userId, Long checkId) {
|
|
|
+ try {
|
|
|
+ com.etotem.cfc.entity.FiveDimensionSelfCheck check;
|
|
|
+ if (checkId != null) {
|
|
|
+ check = selfCheckMapper.selectById(checkId);
|
|
|
+ } else {
|
|
|
+ check = selfCheckMapper.selectOne(
|
|
|
+ new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<com.etotem.cfc.entity.FiveDimensionSelfCheck>()
|
|
|
+ .eq(com.etotem.cfc.entity.FiveDimensionSelfCheck::getUserId, userId)
|
|
|
+ .orderByDesc(com.etotem.cfc.entity.FiveDimensionSelfCheck::getCreatedAt)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ }
|
|
|
+ if (check == null) return null;
|
|
|
+ Map<String, Integer> scores = objectMapper.readValue(check.getScoresJson(),
|
|
|
+ new com.fasterxml.jackson.core.type.TypeReference<Map<String, Integer>>() {});
|
|
|
+ List<String> lowDims = new ArrayList<>();
|
|
|
+ for (Map.Entry<String, Integer> e : scores.entrySet()) {
|
|
|
+ if (e.getValue() != null && e.getValue() <= 6) lowDims.add(e.getKey());
|
|
|
+ }
|
|
|
+ if (lowDims.isEmpty()) return null;
|
|
|
+ com.etotem.cfc.entity.User user = userMapper.selectById(userId);
|
|
|
+ if (user == null || user.getFamilyId() == null) return null;
|
|
|
+ Map<String, Object> inputs = new LinkedHashMap<>();
|
|
|
+ inputs.put("familyId", user.getFamilyId());
|
|
|
+ inputs.put("dimensions", String.join(",", lowDims));
|
|
|
+ inputs.put("goal", "基于五维自检结果,针对低分维度制定改善计划");
|
|
|
+ String planContent = aiGateway.generateHealthPlan(inputs);
|
|
|
+ if (planContent == null) return null;
|
|
|
+ com.etotem.cfc.entity.HealthPlan plan = new com.etotem.cfc.entity.HealthPlan();
|
|
|
+ plan.setFamilyId(user.getFamilyId());
|
|
|
+ plan.setDimensions(String.join(",", lowDims));
|
|
|
+ plan.setGoal("五维自检自动生成");
|
|
|
+ plan.setPlanContent(planContent);
|
|
|
+ plan.setStatus("draft");
|
|
|
+ plan.setCreatedAt(new Date());
|
|
|
+ healthPlanMapper.insert(plan);
|
|
|
+ return plan.getId();
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("generateFromSelfCheck 失败: {}", e.getMessage());
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -q
|
|
|
+```
|
|
|
+
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 5:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java \
|
|
|
+ cfc-backend/src/main/java/com/etotem/cfc/service/HealthPlanService.java \
|
|
|
+ cfc-backend/src/main/java/com/etotem/cfc/service/impl/HealthPlanServiceImpl.java
|
|
|
+git commit -m "feat(self-check): 新增 /generate-plan + /trend-analysis 接口 + HealthPlanService.generateFromSelfCheck"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 7:Java — AIChatController P1-2 context 注入
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java`
|
|
|
+
|
|
|
+- [ ] **步骤 1:sendMessage 支持 selfCheckId**
|
|
|
+
|
|
|
+在 `AIChatController.java` 的 `sendMessage` 方法中,于第 72 行(`String surveyIdStr = params.get("surveyId");`)之后追加:
|
|
|
+
|
|
|
+```java
|
|
|
+ String selfCheckIdStr = params.get("selfCheckId");
|
|
|
+```
|
|
|
+
|
|
|
+在第 122 行(`inputs.put("portrait_prompt", chatPortrait);`)之后追加:
|
|
|
+
|
|
|
+```java
|
|
|
+ // 注入自检上下文(P1-2)
|
|
|
+ if (selfCheckIdStr != null && !selfCheckIdStr.trim().isEmpty()) {
|
|
|
+ try {
|
|
|
+ Long selfCheckId = Long.valueOf(selfCheckIdStr);
|
|
|
+ com.etotem.cfc.entity.FiveDimensionSelfCheck selfCheck = selfCheckMapper.selectById(selfCheckId);
|
|
|
+ if (selfCheck != null) {
|
|
|
+ inputs.put("self_check_result", objectMapper.writeValueAsString(selfCheck));
|
|
|
+ }
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.warn("注入自检上下文失败: {}", e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+需要新增注入(如未已有):
|
|
|
+
|
|
|
+```java
|
|
|
+ @Resource
|
|
|
+ private com.etotem.cfc.mapper.FiveDimensionSelfCheckMapper selfCheckMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private com.fasterxml.jackson.databind.ObjectMapper objectMapper;
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:编译验证**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -q
|
|
|
+```
|
|
|
+
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java
|
|
|
+git commit -m "feat(self-check): AIChat sendMessage 支持 selfCheckId 注入上下文(P1-2)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 8:前端 — utils/api.js 新增方法
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-frontend/utils/api.js`
|
|
|
+
|
|
|
+- [ ] **步骤 1:追加 2 个 API 方法**
|
|
|
+
|
|
|
+在 `cfc-frontend/utils/api.js` 中,于 `ignoreSelfCheck` 方法之后追加:
|
|
|
+
|
|
|
+```js
|
|
|
+export const generateSelfCheckPlan = (data) => {
|
|
|
+ return request('/api/family/self-check/generate-plan', 'POST', data || {})
|
|
|
+}
|
|
|
+
|
|
|
+export const getSelfCheckTrendAnalysis = () => {
|
|
|
+ return request('/api/family/self-check/trend-analysis', 'POST', {})
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:语法校验**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --check cfc-frontend/utils/api.js && echo "api.js OK"
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/utils/api.js
|
|
|
+git commit -m "feat(self-check): 新增 generateSelfCheckPlan/getSelfCheckTrendAnalysis API"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 9:前端 — self-check-result.vue 改造(P0-1/P0-2/P1-2)
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-frontend/pages/family/self-check-result.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:替换寻源建议区块(P0-1)**
|
|
|
+
|
|
|
+将第 70-131 行的 `<view class="scr-card" v-if="result.advices...">` 区块(五行相生寻源建议 + 全部健康提示)整体替换为:
|
|
|
+
|
|
|
+```html
|
|
|
+ <!-- AI 健康解读(P0-1) -->
|
|
|
+ <view class="scr-card" v-if="result.advices && result.advices.length > 0">
|
|
|
+ <view class="scr-card-title">
|
|
|
+ <text class="scr-card-title-text">AI 健康解读</text>
|
|
|
+ <text class="scr-card-sub">基于你的五维自检结果</text>
|
|
|
+ </view>
|
|
|
+ <view v-for="adv in result.advices" :key="adv.dimension" class="scr-advice-item">
|
|
|
+ <view class="scr-advice-dim-row">
|
|
|
+ <view class="scr-advice-dim-badge" :style="{ background: adv.color }">
|
|
|
+ <text class="scr-advice-dim-name">{{ adv.dimensionName }}</text>
|
|
|
+ <text class="scr-advice-dim-elem">{{ adv.score }}分</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <text class="scr-advice-interpretation" v-if="adv.interpretation">{{ adv.interpretation }}</text>
|
|
|
+ <view class="scr-advice-actions" v-if="adv.microActions && adv.microActions.length > 0">
|
|
|
+ <text class="scr-advice-action-label">本周行动:</text>
|
|
|
+ <text v-for="(action, i) in adv.microActions" :key="'ma'+i" class="scr-advice-action">{{ i+1 }}. {{ action }}</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <view class="scr-family-insight" v-if="result.advices[0] && result.advices[0].familyInsight">
|
|
|
+ <text class="scr-family-insight-label">💡 家庭整体洞察</text>
|
|
|
+ <text class="scr-family-insight-text">{{ result.advices[0].familyInsight }}</text>
|
|
|
+ </view>
|
|
|
+ <view class="scr-fallback" v-if="result.advices[0] && result.advices[0].fallbackUsed">
|
|
|
+ <text>AI 分析暂时不可用,请稍后再试</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+ <!-- 全部健康提示 -->
|
|
|
+ <view class="scr-card" v-else-if="result.totalScore >= 35">
|
|
|
+ <view class="scr-all-healthy">
|
|
|
+ <text class="scr-all-healthy-icon">🌿</text>
|
|
|
+ <text class="scr-all-healthy-text">五维状态均健康,请继续保持这份平衡!</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:底部按钮区改造(P0-2 + P1-2)**
|
|
|
+
|
|
|
+将第 134-137 行的底部按钮区替换为:
|
|
|
+
|
|
|
+```html
|
|
|
+ <!-- 历史与操作 -->
|
|
|
+ <view class="scr-footer">
|
|
|
+ <button class="scr-btn scr-btn-primary" @click="goHistory">历史记录</button>
|
|
|
+ <button class="scr-btn scr-btn-outline" @click="retake">重新自检</button>
|
|
|
+ <button class="scr-btn scr-btn-secondary" v-if="hasLowScore" :loading="planGenerating" @click="generatePlan">
|
|
|
+ 生成健康计划
|
|
|
+ </button>
|
|
|
+ <button class="scr-btn scr-btn-ghost" @click="askAI">问问 AI</button>
|
|
|
+ </view>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:script 改造**
|
|
|
+
|
|
|
+在 `data()` 中追加 `planGenerating: false`;在 `computed` 中追加 `hasLowScore`;在 `methods` 中追加 `generatePlan` / `askAI`:
|
|
|
+
|
|
|
+```javascript
|
|
|
+ planGenerating: false, // data() 内
|
|
|
+ // computed 内
|
|
|
+ hasLowScore: function() {
|
|
|
+ return this.result && this.result.advices && this.result.advices.some(function(a) {
|
|
|
+ return a.score != null && a.score <= 6
|
|
|
+ })
|
|
|
+ },
|
|
|
+ // methods 内
|
|
|
+ generatePlan: function() {
|
|
|
+ var self = this
|
|
|
+ if (this.planGenerating) return
|
|
|
+ this.planGenerating = true
|
|
|
+ generateSelfCheckPlan({ checkId: this.result && this.result.id }).then(function(res) {
|
|
|
+ self.planGenerating = false
|
|
|
+ if (res.code === 200 && res.data && res.data.planId) {
|
|
|
+ uni.showToast({ title: '计划已生成,可在健康计划页查看', icon: 'success' })
|
|
|
+ setTimeout(function() {
|
|
|
+ uni.navigateTo({ url: '/pages/health-main/index?planId=' + res.data.planId })
|
|
|
+ }, 1000)
|
|
|
+ } else {
|
|
|
+ uni.showToast({ title: (res && res.message) || '计划生成失败', icon: 'none' })
|
|
|
+ }
|
|
|
+ }).catch(function() {
|
|
|
+ self.planGenerating = false
|
|
|
+ uni.showToast({ title: '计划生成失败', icon: 'none' })
|
|
|
+ })
|
|
|
+ },
|
|
|
+ askAI: function() {
|
|
|
+ var selfCheckId = this.result && this.result.id
|
|
|
+ if (!selfCheckId) {
|
|
|
+ uni.showToast({ title: '暂无自检记录', icon: 'none' })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ uni.navigateTo({ url: '/pages/ai/chat?selfCheckId=' + selfCheckId })
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:import 追加**
|
|
|
+
|
|
|
+在文件头部 import 区追加:
|
|
|
+
|
|
|
+```javascript
|
|
|
+import { generateSelfCheckPlan } from '@/utils/api'
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:样式追加**
|
|
|
+
|
|
|
+在 `<style>` 块末尾追加(复用 `.scr-btn` 模式):
|
|
|
+
|
|
|
+```css
|
|
|
+.scr-btn-secondary {
|
|
|
+ background: linear-gradient(135deg, #10B981, #34D399);
|
|
|
+ color: #fff;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.scr-btn-ghost {
|
|
|
+ background: #fff;
|
|
|
+ color: #666;
|
|
|
+ border: 2rpx solid #D1D5DB;
|
|
|
+}
|
|
|
+.scr-advice-interpretation {
|
|
|
+ display: block;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #555;
|
|
|
+ line-height: 1.6;
|
|
|
+ margin: 16rpx 0;
|
|
|
+}
|
|
|
+.scr-advice-actions {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8rpx;
|
|
|
+ margin-top: 12rpx;
|
|
|
+}
|
|
|
+.scr-advice-action-label {
|
|
|
+ font-size: 24rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #F97316;
|
|
|
+}
|
|
|
+.scr-advice-action {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #555;
|
|
|
+ line-height: 1.5;
|
|
|
+}
|
|
|
+.scr-family-insight {
|
|
|
+ background: #F5FAFE;
|
|
|
+ border-radius: 16rpx;
|
|
|
+ padding: 20rpx 24rpx;
|
|
|
+ margin-top: 24rpx;
|
|
|
+}
|
|
|
+.scr-family-insight-label {
|
|
|
+ display: block;
|
|
|
+ font-size: 24rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #F97316;
|
|
|
+ margin-bottom: 8rpx;
|
|
|
+}
|
|
|
+.scr-family-insight-text {
|
|
|
+ display: block;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #555;
|
|
|
+ line-height: 1.6;
|
|
|
+}
|
|
|
+.scr-fallback {
|
|
|
+ background: #FFF7ED;
|
|
|
+ border-radius: 12rpx;
|
|
|
+ padding: 16rpx 20rpx;
|
|
|
+ margin-top: 16rpx;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+.scr-fallback text {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 6:语法校验**
|
|
|
+
|
|
|
+提取 script 块语法校验:
|
|
|
+
|
|
|
+```bash
|
|
|
+node -e "
|
|
|
+var fs = require('fs');
|
|
|
+var content = fs.readFileSync('cfc-frontend/pages/family/self-check-result.vue', 'utf8');
|
|
|
+var m = content.match(/<script>([\s\S]*?)<\/script>/);
|
|
|
+if (m) { new Function(m[1]); console.log('script OK'); } else { console.log('no script'); }
|
|
|
+"
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 7:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/pages/family/self-check-result.vue
|
|
|
+git commit -m "feat(self-check): 结果页改 AI 解读 + 生成计划按钮 + 问问AI(P0-1/P0-2/P1-2)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 10:前端 — self-check-entry.vue 趋势展示(P1-1)
|
|
|
+
|
|
|
+**文件:**
|
|
|
+- 修改:`cfc-frontend/pages/family/self-check-entry.vue`
|
|
|
+
|
|
|
+- [ ] **步骤 1:追加趋势区块**
|
|
|
+
|
|
|
+在 `self-check-entry.vue` 的状态提示(`sce-status`)之后、按钮区(`sce-footer`)之前,插入:
|
|
|
+
|
|
|
+```html
|
|
|
+ <!-- 趋势分析(P1-1) -->
|
|
|
+ <view class="sce-trend" v-if="trendLoaded">
|
|
|
+ <view class="sce-trend-header">
|
|
|
+ <text class="sce-trend-title">📈 自检趋势</text>
|
|
|
+ <text class="sce-trend-summary" v-if="trendSummary">{{ trendSummary }}</text>
|
|
|
+ </view>
|
|
|
+ <text class="sce-trend-insight" v-if="aiInsight">{{ aiInsight }}</text>
|
|
|
+ <view class="sce-trend-loading" v-if="trendLoading">
|
|
|
+ <text>AI 趋势分析中...</text>
|
|
|
+ </view>
|
|
|
+ </view>
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 2:script 追加**
|
|
|
+
|
|
|
+在 `data()` 追加 `trendLoaded: false, trendLoading: false, aiInsight: '', trendSummary: ''`;在 `onLoad` 中追加 `this.loadTrend()`;在 `methods` 追加:
|
|
|
+
|
|
|
+```javascript
|
|
|
+ loadTrend: function() {
|
|
|
+ var self = this
|
|
|
+ this.trendLoading = true
|
|
|
+ getSelfCheckTrendAnalysis().then(function(res) {
|
|
|
+ self.trendLoading = false
|
|
|
+ self.trendLoaded = true
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
+ self.aiInsight = res.data.aiInsight || ''
|
|
|
+ self.trendSummary = res.data.trendSummary || ''
|
|
|
+ }
|
|
|
+ }).catch(function() {
|
|
|
+ self.trendLoading = false
|
|
|
+ self.trendLoaded = true
|
|
|
+ })
|
|
|
+ }
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:import 追加**
|
|
|
+
|
|
|
+```javascript
|
|
|
+import { getSelfCheckTrendAnalysis } from '@/utils/api'
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:样式追加**
|
|
|
+
|
|
|
+```css
|
|
|
+.sce-trend {
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 24rpx;
|
|
|
+ padding: 30rpx 26rpx;
|
|
|
+ margin-bottom: 24rpx;
|
|
|
+ box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.05);
|
|
|
+}
|
|
|
+.sce-trend-header {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: space-between;
|
|
|
+ margin-bottom: 12rpx;
|
|
|
+}
|
|
|
+.sce-trend-title {
|
|
|
+ font-size: 30rpx;
|
|
|
+ font-weight: 600;
|
|
|
+ color: #333;
|
|
|
+}
|
|
|
+.sce-trend-summary {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #F97316;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+.sce-trend-insight {
|
|
|
+ display: block;
|
|
|
+ font-size: 26rpx;
|
|
|
+ color: #555;
|
|
|
+ line-height: 1.6;
|
|
|
+}
|
|
|
+.sce-trend-loading {
|
|
|
+ padding: 10rpx 0;
|
|
|
+}
|
|
|
+.sce-trend-loading text {
|
|
|
+ font-size: 24rpx;
|
|
|
+ color: #999;
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 5:语法校验**
|
|
|
+
|
|
|
+提取 script 块校验(同任务 9 步骤 6 方法)。
|
|
|
+
|
|
|
+- [ ] **步骤 6:Commit**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add cfc-frontend/pages/family/self-check-entry.vue
|
|
|
+git commit -m "feat(self-check): 中间页展示自检趋势 AI 解读(P1-1)"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 任务 11:最终验证
|
|
|
+
|
|
|
+- [ ] **步骤 1:后端编译**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-backend && mvn clean compile -q
|
|
|
+```
|
|
|
+
|
|
|
+预期:BUILD SUCCESS
|
|
|
+
|
|
|
+- [ ] **步骤 2:LangGraph 语法校验**
|
|
|
+
|
|
|
+```bash
|
|
|
+cd cfc-langgraph && python -m py_compile app/graphs/self_check_analysis_graph.py app/graphs/self_check_trend_graph.py app/api/self_check.py app/main.py && echo "OK"
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 3:前端语法校验**
|
|
|
+
|
|
|
+```bash
|
|
|
+node --check cfc-frontend/utils/api.js && echo "api.js OK"
|
|
|
+```
|
|
|
+
|
|
|
+- [ ] **步骤 4:提交最终 commit(如有遗漏)**
|
|
|
+
|
|
|
+```bash
|
|
|
+git add -A
|
|
|
+git diff --cached --stat
|
|
|
+git commit -m "feat(self-check): 五维自检 AI 结合 P0+P1 全栈实现"
|
|
|
+```
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 自检清单
|
|
|
+
|
|
|
+**规格覆盖度:**
|
|
|
+- [x] P0-1 纯 AI 替换 → 任务 1(LangGraph graph)+ 任务 3/4/5(Java Service/AiGateway/submit)
|
|
|
+- [x] P0-2 用户点击生成计划 → 任务 6(Controller + HealthPlanService)+ 任务 9(前端按钮)
|
|
|
+- [x] P1-1 历史趋势 + AI 解读 → 任务 2(trend graph)+ 任务 6(trend-analysis)+ 任务 10(前端展示)
|
|
|
+- [x] P1-2 自检注入 Chat → 任务 7(AIChatController)+ 任务 9(askAI 按钮)
|
|
|
+
|
|
|
+**占位符扫描:** 无"待定"/"TODO"/"后续实现"
|
|
|
+
|
|
|
+**类型一致性:**
|
|
|
+- `WuxingSourcingAdviceVO` 新字段(interpretation/microActions/aiInsight/familyInsight/fallbackUsed)— 任务 3 定义,任务 5 解析、任务 9 渲染
|
|
|
+- `SelfCheckAnalysisService.generateAdvice/generateTrend/getRecentHistory` — 任务 3 定义,任务 5/6 调用
|
|
|
+- `AiGateway.generateSelfCheckAdvice/generateSelfCheckTrend` — 任务 4 定义,任务 3 调用
|
|
|
+- `HealthPlanService.generateFromSelfCheck` — 任务 6 接口+实现,Controller 调用
|
|
|
+- `generateSelfCheckPlan/getSelfCheckTrendAnalysis` — 任务 8 前端定义,任务 9/10 调用
|