2026-08-31-self-check-ai-integration.md 45 KB

五维家庭自检 AI 结合(P0 + P1)实现计划

面向 AI 代理的工作者: 必需子技能:使用 superpowers:subagent-driven-development(推荐)或 superpowers:executing-plans 逐任务实现此计划。步骤使用复选框(- [ ])语法来跟踪进度。

目标: 将五维自检结果从静态五行寻源表(WuxingSourcingService)替换为 LangGraph AI 生成建议,并叠加健康计划生成、趋势分析、Chat 上下文注入三个能力。

架构: LangGraph 新增 2 个 graph(self_check_analysis_graphself_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

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

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) 之后追加:

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 路由"
    

任务 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

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 趋势分析实现"
    

任务 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 的字段替换为:

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

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"
    

任务 4:Java — AiGateway 新增方法

文件:

  • 修改: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"
    

任务 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_METAlevelOf()levelName()dimensionName()

  • 步骤 2:FiveDimensionSelfCheckService 改造 submitSelfCheck

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));
  • 步骤 3:新增 parseAdvices + trend 方法

在 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"
    

任务 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 中追加:

    @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 中追加:

    /** 基于自检低分维度自动生成 draft 计划 */
    Long generateFromSelfCheck(Long userId, Long checkId);
  • 步骤 3:HealthPlanServiceImpl 实现 generateFromSelfCheck

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"
    

任务 7:Java — AIChatController P1-2 context 注入

文件:

  • 修改:cfc-backend/src/main/java/com/etotem/cfc/controller/ai/AIChatController.java

  • [ ] 步骤 1:sendMessage 支持 selfCheckId

AIChatController.javasendMessage 方法中,于第 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)"
    

任务 8:前端 — utils/api.js 新增方法

文件:

  • 修改: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"
    

任务 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..."> 区块(五行相生寻源建议 + 全部健康提示)整体替换为:

      <!-- 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 行的底部按钮区替换为:

      <!-- 历史与操作 -->
      <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

      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 区追加:

import { generateSelfCheckPlan } from '@/utils/api'
  • 步骤 5:样式追加

<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;
}
  • 步骤 6:语法校验

提取 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)"
    

任务 10:前端 — self-check-entry.vue 趋势展示(P1-1)

文件:

  • 修改: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>
  • 步骤 2:script 追加

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)"
    

任务 11:最终验证

  • [ ] 步骤 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 全栈实现"
    

自检清单

规格覆盖度:

  • P0-1 纯 AI 替换 → 任务 1(LangGraph graph)+ 任务 3/4/5(Java Service/AiGateway/submit)
  • P0-2 用户点击生成计划 → 任务 6(Controller + HealthPlanService)+ 任务 9(前端按钮)
  • P1-1 历史趋势 + AI 解读 → 任务 2(trend graph)+ 任务 6(trend-analysis)+ 任务 10(前端展示)
  • 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 调用