面向 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 趋势展示 | 修改 |
文件:
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:
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
创建文件 cfc-langgraph/app/api/self_check.py:
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"),
}
在 cfc-langgraph/app/main.py 中,于 app.include_router(innate_portrait.router) 之后追加:
from app.api import self_check
app.include_router(self_check.router)
[ ] 步骤 4:语法校验
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
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 路由"
文件:
创建:cfc-langgraph/app/graphs/self_check_trend_graph.py
[ ] 步骤 1:创建 self_check_trend_graph.py
创建文件 cfc-langgraph/app/graphs/self_check_trend_graph.py:
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:语法校验
cd cfc-langgraph && python -m py_compile app/graphs/self_check_trend_graph.py && echo "OK"
[ ] 步骤 3:Commit
git add cfc-langgraph/app/graphs/self_check_trend_graph.py
git commit -m "feat(langgraph): 新增 self_check_trend_graph 趋势分析实现"
文件:
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 的字段替换为:
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;
}
创建文件 cfc-backend/src/main/java/com/etotem/cfc/service/SelfCheckAnalysisService.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:编译验证
cd cfc-backend && mvn clean compile -q
预期:BUILD SUCCESS
[ ] 步骤 4:Commit
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"
文件:
修改:cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java
[ ] 步骤 1:新增 generateSelfCheckAdvice 方法
在 AiGateway.java 中,于 generateInnateReading 方法之后追加:
/**
* 生成自检 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:编译验证
cd cfc-backend && mvn clean compile -q
预期:BUILD SUCCESS
[ ] 步骤 3:Commit
git add cfc-backend/src/main/java/com/etotem/cfc/service/AiGateway.java
git commit -m "feat(self-check): AiGateway 新增 generateSelfCheckAdvice/generateSelfCheckTrend"
文件:
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()
在 submitSelfCheck 方法中,将第 472-477 行(wuxingSourcingService.getAdvicesForLowScores + 落库 adviceJson)替换为:
// 生成 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 : "[]");
同时新增注入:
@Resource
private SelfCheckAnalysisService selfCheckAnalysisService;
并将 vo.setAdvices(...) 改为根据 adviceJson 解析(若无 AI 结果则空列表):
vo.setAdvices(parseAdvices(adviceJson));
在 Service 中新增:
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:编译验证
cd cfc-backend && mvn clean compile -q
预期:BUILD SUCCESS
[ ] 步骤 5:Commit
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"
文件:
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.javacfc-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 中追加:
@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("趋势分析失败");
}
}
在 HealthPlanService.java 中追加:
/** 基于自检低分维度自动生成 draft 计划 */
Long generateFromSelfCheck(Long userId, Long checkId);
在 HealthPlanServiceImpl.java 中追加(需新增 @Resource private com.etotem.cfc.mapper.FiveDimensionSelfCheckMapper selfCheckMapper;):
@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:编译验证
cd cfc-backend && mvn clean compile -q
预期:BUILD SUCCESS
[ ] 步骤 5:Commit
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"
文件:
修改:cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java
[ ] 步骤 1:sendMessage 支持 selfCheckId
在 AIChatController.java 的 sendMessage 方法中,于第 72 行(String surveyIdStr = params.get("surveyId");)之后追加:
String selfCheckIdStr = params.get("selfCheckId");
在第 122 行(inputs.put("portrait_prompt", chatPortrait);)之后追加:
// 注入自检上下文(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());
}
}
需要新增注入(如未已有):
@Resource
private com.etotem.cfc.mapper.FiveDimensionSelfCheckMapper selfCheckMapper;
@Resource
private com.fasterxml.jackson.databind.ObjectMapper objectMapper;
[ ] 步骤 2:编译验证
cd cfc-backend && mvn clean compile -q
预期:BUILD SUCCESS
[ ] 步骤 3:Commit
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)"
文件:
修改:cfc-frontend/utils/api.js
[ ] 步骤 1:追加 2 个 API 方法
在 cfc-frontend/utils/api.js 中,于 ignoreSelfCheck 方法之后追加:
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:语法校验
node --check cfc-frontend/utils/api.js && echo "api.js OK"
[ ] 步骤 3:Commit
git add cfc-frontend/utils/api.js
git commit -m "feat(self-check): 新增 generateSelfCheckPlan/getSelfCheckTrendAnalysis API"
文件:
修改:cfc-frontend/pages/family/self-check-result.vue
[ ] 步骤 1:替换寻源建议区块(P0-1)
将第 70-131 行的 <view class="scr-card" v-if="result.advices..."> 区块(五行相生寻源建议 + 全部健康提示)整体替换为:
<!-- 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>
将第 134-137 行的底部按钮区替换为:
<!-- 历史与操作 -->
<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>
在 data() 中追加 planGenerating: false;在 computed 中追加 hasLowScore;在 methods 中追加 generatePlan / askAI:
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 })
}
在文件头部 import 区追加:
import { generateSelfCheckPlan } from '@/utils/api'
在 <style> 块末尾追加(复用 .scr-btn 模式):
.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;
}
提取 script 块语法校验:
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
git add cfc-frontend/pages/family/self-check-result.vue
git commit -m "feat(self-check): 结果页改 AI 解读 + 生成计划按钮 + 问问AI(P0-1/P0-2/P1-2)"
文件:
修改:cfc-frontend/pages/family/self-check-entry.vue
[ ] 步骤 1:追加趋势区块
在 self-check-entry.vue 的状态提示(sce-status)之后、按钮区(sce-footer)之前,插入:
<!-- 趋势分析(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>
在 data() 追加 trendLoaded: false, trendLoading: false, aiInsight: '', trendSummary: '';在 onLoad 中追加 this.loadTrend();在 methods 追加:
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 追加
import { getSelfCheckTrendAnalysis } from '@/utils/api'
[ ] 步骤 4:样式追加
.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
git add cfc-frontend/pages/family/self-check-entry.vue
git commit -m "feat(self-check): 中间页展示自检趋势 AI 解读(P1-1)"
[ ] 步骤 1:后端编译
cd cfc-backend && mvn clean compile -q
预期:BUILD SUCCESS
[ ] 步骤 2:LangGraph 语法校验
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:前端语法校验
node --check cfc-frontend/utils/api.js && echo "api.js OK"
[ ] 步骤 4:提交最终 commit(如有遗漏)
git add -A
git diff --cached --stat
git commit -m "feat(self-check): 五维自检 AI 结合 P0+P1 全栈实现"
规格覆盖度:
占位符扫描: 无"待定"/"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 调用