版本: v1.0 日期: 2026-07-10 前置审计: docs/superpowers/plans/2026-07-10-determinism-gap-audit.md(基于 5 路并行 explore 代码审计) 总体工作量估算: 38-52 人天(P0: 5-7d / P1: 15-20d / P2: 12-16d / P3: 6-9d)
本计划以"确定性"为主线,把审计报告中的 18 个修复任务按优先级分层,强调安全先于叙事、闭环先于优化、闭环先于优化、闭环先于优化。
| 阶段 | 主题 | 任务数 | 估算人天 | 价值锚点 |
|---|---|---|---|---|
| P0 | 隐私 + 营销根基 | 3 | 5-7 | 堵最大隐私漏洞;打开"成长可视化"叙事 |
| P1 | 信任 + 数据闭环 | 6 | 15-20 | 邀请/视角/订单/Dify 闭环;地址全国可用 |
| P2 | 数据质量 + 可维护性 | 5 | 12-16 | 迁移可追溯;schema 自洽;报告机读;推荐可解释 |
| P3 | 长期治理 | 4 | 6-9 | 死字段清理;PDF 多供应商;sfms 只读;前端去 mock |
关键里程碑:
familyId 越权测试零案例;五维分数可图表化回放共用约定:所有新建表通过
DatabaseInitializer.runMigrations()新增CREATE TABLE IF NOT EXISTS;schema.sql 同步追加;新增 controller 用@PostMapping;DI 用@Resource字段名匹配 Bean Name。
优先级: P0 🔴
估算: 1.5 人天
前置依赖: 无
影响文件: JwtInterceptor.java、WebConfig.java、7 个控制器
7 个控制器从 JSON body/参数中拿 familyId 后直接使用,不校验该家庭是否属于当前 JWT 用户:
DanAssessmentController 第 65/79/89/208 行InteractionLogController 第 62 行FoodRecommendController 第 50 行WishController 第 229 行TeacherMessageController 第 43 行GrowthRecordController 第 90 行TraditionalMirrorController 第 88 行[ ] 新增 FamilyAccessInterceptor(config/FamilyAccessInterceptor.java)
/api/**(除 JwtInterceptor 已放行的 8 个公开路径)familyId 字段(用 ContentCachingRequestWrapper 缓存 body 以便可重复读)@RequestAttribute("userId") 查 User.familyId 与 User.teacherFamilyIdsfamilyId 必须等于 user.familyId 或 在 teacherFamilyIds 列表中response.setStatus(403);返回 JSON {"code":403,"message":"无权访问该家庭数据"}[ ] WebConfig.addInterceptors 注册 FamilyAccessInterceptor,order 在 JwtInterceptor 之后
[ ] User 实体新增 helper getEffectiveFamilyIds():返回包含 familyId + 解析后的 teacherFamilyIds 的 Set<Long>
[ ] 识别例外路径:admin 调用路径(如 /api/admin/*)跳过此拦截器;teacher 绑定家庭的 guideBindFamily 由 G10 任务单独加审批,暂放行
[ ] 测试:新增 FamilyAccessInterceptorTest
[ ] 不要修改原有 7 个控制器内部逻辑——拦截器统一处理
mvn clean compile 通过mvn test -Dtest=FamilyAccessInterceptorTest 通过{familyId: B的家庭ID} 调 /api/dan-assessment/materials 返回 403/api/** 路径(除 admin 子树)自动校验 familyId 归属优先级: P0 🔴
估算: 2.5 人天
前置依赖: 无
影响文件: 新增 entity/mapper/service、EnergyService.java、DatabaseInitializer.java、schema.sql
EnergyService.calculateFamilyEnergy() 每次请求重算身/心/智/行/富 0-100 分;无任何表保存"X 孩子在 Y 日期的分数",导致:
[ ] 新建 five_dimension_scores 表
CREATE TABLE IF NOT EXISTS five_dimension_scores (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
member_id BIGINT NOT NULL COMMENT '家庭成员ID(family_members.id)',
family_id BIGINT NOT NULL,
dimension_code VARCHAR(32) NOT NULL COMMENT 'body/mind/wisdom/action/wealth',
score INT NOT NULL COMMENT '0-100 分',
assessed_at DATETIME NOT NULL COMMENT '评估时间',
source_type VARCHAR(32) NOT NULL COMMENT 'assessment/task_complete/manual/periodic',
source_id BIGINT COMMENT '源对象ID(如dan_assessment_results.id)',
detail_json TEXT COMMENT '细分项快照(cognitive_scores/emi_scores/...)',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_member_dim_time (member_id, dimension_code, assessed_at),
INDEX idx_family_dim_time (family_id, dimension_code, assessed_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='五维能量历史快照';
DatabaseInitializer.runMigrations() 添加schema.sql[ ] 新建 FiveDimensionScore entity(标准 MyBatis-Plus)
@TableName("five_dimension_scores"), @TableId(type=IdType.AUTO)[ ] 新建 FiveDimensionScoreMapper
[ ] 新建 FiveDimensionScoreService
recordSnapshot(memberId, familyId, dimension, score, sourceType, sourceId, detailJson):插入getHistoryByMember(memberId, dimension, limit=20):返回时间序列getLatestByFamilyMap(familyId):返回 Map
compareFirstVsLatest(memberId):返回 delta(先实现接口,G18 用)[ ] EnergyService.calculateFamilyEnergy() 末尾插入持久化钩子
fiveDimensionScoreService.recordSnapshot(...)memberId+dimension+assessed_at(精度=天)+source_type+source_id 已存在,跳过插入EnergyService 的计算逻辑——只在尾部加 record[ ] EnergyController.getOverview 优先读 fiveDimensionScores 最新记录;fallback 到现有重算逻辑(保证向下兼容)
[ ] 新增端点:
POST /api/energy/score-history:入参 memberId, dimension, limit,返回时间序列POST /api/energy/family-score-history:入参 familyId, dimension, fromDate, toDatemvn clean compile 通过/api/energy/overview 两次,DB 中应出现两条快照(去重生效时只有 1 条)/api/energy/score-history?memberId=X&dimension=wisdom&limit=5 返回时间序列EnergyService.calcChildWisdom() 系数(如临时改为 0.5),重启,历史分数不变,新分数按新系数EnergyService 算法改动不再影响历史数据优先级: P0 🔴
估算: 1.5 人天
前置依赖: G1(推荐先完成,否则对比来源是重算数据)
影响文件: GrowthRecordController/AssessmentService,或新建 AssessmentComparisonController
AGENTS.md 宣传文案"自驱力 4.07→5.30"在系统中无法查到——无 compareAssessments 端点,没有方法取首个和最近一次测评计算 delta。cognitive-report.vue 只显示单次报告。
[ ] 新建 AssessmentComparisonService
compareByChild(childId):取该孩子 dan_assessment_results 中最早 + 最近两条,计算维度 deltacompareByChildAndDimension(childId, dimensionCode):聚焦单维度getProgressNarrative(childId):返回结构化 delta + 文本叙事("智维度提升 +12 分,主要由 logicScore 拉动 +18,专注力下降 -5,建议...")[ ] 数据源策略:
five_dimension_scores(G1 新增)取历史值dan_assessment_results 直接 diff[ ] 新建端点:
POST /api/assessment/compare/by-child 入参 childId
返回:
{
"firstAssessment": { "id":42, "date":"2025-08-10", "scores":{...} },
"latestAssessment": { "id":88, "date":"2026-07-01", "scores":{...} },
"dimensionDeltas": [
{"dimension":"wisdom","delta":12.3,"direction":"up","drivers":["logicScore +18","memoryScore +6"]},
...
],
"overallDelta": 7.4,
"narrative": "智维度提升 12.3 分,主要由 logicScore..."
}
POST /api/assessment/compare/by-family 入参 familyId,返回家庭所有孩子对比数组
[ ] 前端 pages/wisdom-detail/ 新增 assessment-comparison.vue:渲染对比卡片(先文案样式即可,不强求图表)
AssessmentComparisonServiceTest:mock 两次测评,断言 delta 计算/api/assessment/compare/by-child?childId=X 返回结构化 delta优先级: P1
估算: 2.5 人天
前置依赖: 无
影响文件: FamilyUserController、UserService、FamilyMembersController、前端 store/index.js、pages/profile/*
/api/family/member/switch(返回 SwitchMemberVO,不动 DB)——这是正确模型FamilyUserController.switchMode → 改为 404 或重定向到 /member/switchFamilyUserController.switchToChild → 同上UserService.switchToChild、switchMode 加 @Deprecated,保留代码以兼容老 JWT 但记 log warningUserService.switchBackToParent() 加入密码校验:
password 参数,调用 passwordEncoder.matches(password, user.password)Result.error("密码错误")POST /api/family/user/switch-back-verify:前端切回家长视角前调用,强制输入密码store/index.js 改造:
isSwitchedChild、isSwitchedMember 双 bool,合并为 currentView: 'self'|'member'switchBackFromMember 调用 /switch-back-verify,失败显示密码弹窗role 单点真源,仍写但加签名(HMAC with server-issued nonce)防止篡改(可选,先做密码校验)/switch-back-verify 必须密码通过才能切回mvn test -Dtest=UserServiceTest#switchBackWithWrongPassword 失败/switch-mode、/switch-to-child 返回 410 Gone 或 308优先级: P1
估算: 1.5 人天
前置依赖: 无
影响文件: FamilyController、FamilyInviteController、FamilyInvitationService、UserAddress 等
FamilyController /invite-code 的简易码,全部走 FamilyInviteController /generate 生成 token-based invitationFamilyInvitation 增强:
maxUseCount INT DEFAULT 1usedCount INT DEFAULT 0inviterUserId BIGINT NOT NULLexpiresAt(7 天)保持validateInvitation() 强化:
usedCount < maxUseCountstatus='active' 且未过期acceptInvitation() 在事务中 UPDATE ... SET usedCount = usedCount + 1,并带 WHERE usedCount < maxUseCount 防并发POST /api/family/invite/revoke:入参 invitationId,仅 inviter 或 family admin 可调用,将 status 设为 revokedFamilyController.guideBindFamily 加 familyId 视角下AMILY.getId() ≠ 邀请绑定的(endpoint 自然受 G7 拦截器保护,可保留)Bucket4j 或简单 Redis 计数)accept 返回 410优先级: P1
估算: 2 人天
前置依赖: G9(共用邀请码机制)
影响文件: FamilyController.guideBindFamily、TeacherFamilyBindingRequest(新 entity)、FamilyController
[ ] 新建 teacher_family_binding_requests 表
CREATE TABLE IF NOT EXISTS teacher_family_binding_requests (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
teacher_id BIGINT NOT NULL,
family_id BIGINT NOT NULL,
invitation_id BIGINT NOT NULL,
status VARCHAR(20) NOT NULL DEFAULT 'pending' COMMENT 'pending/approved/rejected/cancelled',
requested_at DATETIME DEFAULT CURRENT_TIMESTAMP,
decided_by BIGINT COMMENT '审批家长 user_id',
decided_at DATETIME,
reject_reason VARCHAR(200),
UNIQUE KEY uk_teacher_family (teacher_id, family_id) COMMENT '防重复申请',
INDEX idx_family_status (family_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
[ ] FamilyController.guideBindFamily() 改造:从直接 ADD teacherFamilyIds → 插入 binding_request,status=pending
[ ] 新增端点:
POST /api/family/teacher-bindings/pending:家长查看自家待审批列表POST /api/family/teacher-bindings/approve:入参 requestId,调家长权限校验,status=approved,写入 User.teacherFamilyIdsPOST /api/family/teacher-bindings/reject:入参 requestId, reason[ ] 消息通知:审批状态变化通过现有 MessageService 推送小程序模板消息给规划师
[ ] MessageController 增加类型:teacher_binding_approved / teacher_binding_rejected
teacherFamilyIds 看到 familyId优先级: P1
估算: 2 人天
前置依赖: 无
影响文件: AssessmentOrder、AssessmentOrderService、GuideRecordController、AssessmentService
assessment_orders 加字段 result_id BIGINT + results_ready_at DATETIME(通过迁移)AssessmentOrder.status 枚举扩展:pending|paid|results_ready|completed|cancelled|refundedGuideRecordController.record() 写结果后联动更新订单:
appointmentId 反查 order,写入 orders.result_id = danAssessmentResult.id、status='results_ready'、results_ready_at=now()AssessmentOrderController 新增端点 POST /api/assessment/order/{id}/ack-results:家长确认收到结果,订单 status → completedresults_ready 触发小程序模板消息
MessageService.sendTemplateMessage 已有路径assessment_results_readyAssessmentOrderController.detail 返回 resultId、resultsReadyAt、resultSnapshot(精简版 DanAssessmentResult)POST /order/{id}/ack-results 后变 completed优先级: P1
估算: 3-4 人天
前置依赖: 无
影响文件: DatabaseInitializer.java(seed)、RegionController.java、Street.java、新建 china_regions.csv/SQL
modood/Administrative-divisions-of-China GitHub 仓库(CSV/JSON 格式,已包含省/市/区/街道 4 级)db/seed/china_regions.sql:
INSERT INTO streets (...) VALUES ..., (...); 批量语句DatabaseInitializer 添加迁移:检测 streets 表行数 < 1000 时执行种子导入
DELETE FROM streets WHERE province_code != '44' 清掉旧的广东测试数据(或保留并 IDUPSERT)INSERT IGNORE 或 ON DUPLICATE KEY UPDATE 保证幂等RegionController 改造:
ALL_REGIONS 静态硬编码列表StreetService/api/region/streets 真正返回街道数据UserAddress 表加 street_id BIGINT 外键(保留旧 street 字符串字段以兼容历史数据),新建地址时填 street_iduser_address 和 danshop_addresses 写入逻辑:地址选择器只允许从 streets 表选,强制 street_id 非空AddressPicker 组件:四联动从 /api/region/provinces → cities → districts → streets 拉 DB 数据StreetService.matchByStreetWithFallback() 仍然有效,但现在数据齐了,回退少触发SELECT COUNT(*) FROM streets ≈ 3000+RegionController 全部端点单测 PASSAddressPicker 组件在家长地址页能选北京→朝阳→XX街道RegionController 中无任何硬编码优先级: P1
估算: 2.5 人天
前置依赖: 无
影响文件: 新建 chat_conversations、chat_messages 表 + entity + service;AIService.sendMessage 加镜像写入;端点 fallback 读本地
[ ] 新建 chat_conversations 表
CREATE TABLE IF NOT EXISTS chat_conversations (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
dify_conversation_id VARCHAR(64) NOT NULL,
assistant_type VARCHAR(32) NOT NULL COMMENT 'family/nutrition/tongue',
title VARCHAR(200),
last_message_at DATETIME,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uk_dify_conv (dify_conversation_id),
INDEX idx_user (user_id)
) ENGINE=InnoDB;
[ ] 新建 chat_messages 表
CREATE TABLE IF NOT EXISTS chat_messages (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
conversation_id BIGINT NOT NULL,
dify_message_id VARCHAR(64),
role VARCHAR(20) NOT NULL COMMENT 'user/assistant',
content TEXT NOT NULL,
inputs_json TEXT,
metadata TEXT COMMENT '推荐标签解析结果等',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_conv (conversation_id)
) ENGINE=InnoDB;
[ ] 新建 ChatConversation / ChatMessage entity、Mapper、Service
[ ] AIService.sendMessage 改造:
@Async 或直接同步,量大时再优化)conversationId 自动 upsert chat_conversations(找不到插一条)chat_messages[ ] AIChatController.getConversations / getMessages 加 fallback:
chat_messages[ ] 新增端点 POST /api/ai/chat/search-history:用户全文搜索自己历史对话(搜 content LIKE)
sendMessage 后 chat_messages 表 2 条新记录getMessages 仍能返回历史(来自本地)search-history 能从消息中搜到关键词优先级: P2
估算: 3 人天
前置依赖: 无
影响文件: DatabaseInitializer.java、新建 SchemaVersion entity
[ ] 新建 schema_versions 表
CREATE TABLE IF NOT EXISTS schema_versions (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
version_code VARCHAR(64) NOT NULL UNIQUE COMMENT '如 migration_2026_07_10_001',
description VARCHAR(500),
checksum VARCHAR(64) COMMENT 'SHA256 of SQL content',
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP,
applied_by VARCHAR(100)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
[ ] 重构 DatabaseInitializer.runMigrations():
schema_versions)migrationApplied(versionCode) 查表,已跑则跳过schema_versions 表[ ] 不立即重构所有现存迁移(206 个分散的 try-catch),先冻结现状并按"重要 batch"切割为 5-10 个 version_code(如 base_schema_v1, growth_record_rebuild_v1, address_seed_v1)
[ ] 新加迁移强制使用新机制:未来所有 DDL 必须用 runMigration("version_X", "描述", () -> { ... }) helper
[ ] /api/admin/schema-versions 端点:admin 查看已跑迁移(只读)
优先级: P2
估算: 2 人天
前置依赖: G23(共享版本机制后对账更准)
影响文件: schema.sql、新建脚本 tools/validate-schema-sync.ps1
[ ] 跑脚本枚举 DatabaseInitializer + entity @TableName:
# 列出所有 DatabaseInitializer 中 CREATE TABLE 的表名
# 列出所有 @TableName 注解的 entity 表名
# 比对 schema.sql 中 CREATE TABLE 表名
[ ] 补齐 schema.sql 缺失的 ~40 张表:
[ ] 补齐已有表缺失的列:扫描 entity 字段 vs schema.sql 列名,diff 出缺失字段补到 schema.sql
[ ] 新增 CI 脚本 tools/validate-schema-sync.ps1:每次提交时跑对账,diff 非空时 fail build
优先级: P2
估算: 3 人天
前置依赖: 无
影响文件: DanAssessmentResult、GuideRecordController、AssessmentService、web 端规划师录入表单
DanAssessmentResult 新增字段 structured_analysis JSON(保留 analysis_report TEXT 兼容历史)[ ] 结构化 schema:
{
"summary": "一句话总评",
"dimensions": [
{"code":"wisdom","score":82,"level":"good","indicator":"logicScore","comment":"逻辑推理突出"},
...5个维度...
],
"highlight_strengths": ["专注力","记忆容量"],
"concern_areas": ["情绪管理"],
"suggestions": [
{"dimension":"mind","action":"每日 15 分钟正念练习","target":"3 周后 EMI 自我觉察 ≥75"},
...
]
}
[ ] GuideRecordController.create() 接收 structured_analysis JSON,验证字段必填
[ ] 新增端点 POST /api/dan-assessment/results/{id}/structured:返回结构化数据(家长端用)
[ ] ReportParseService 改造:从 AI prompt 解析时产出 structured_analysis 一起入库
[ ] 家长端 cognitive-report.vue 渲染结构化维度块、建议列表
[ ] web 端规划师录入表单:从单 big textarea 改为分维度表单(强制总结 + 5 维度评分 + 至少 2 个 strength + 1 个 concern)
优先级: P2
估算: 0.5 人天
前置依赖: 无
影响文件: WechatService、application.yml
WechatService 加 @PostConstruct 启动检查
@Value("${spring.profiles.active:dev}") 与 @Value("${wechat.test-mode:false}")throw new IllegalStateException("wechat.test-mode=true in production profile!")tools/check-test-mode.ps1 扫描 application-prod*.yml 有 test-mode: true 时 fail优先级: P2
估算: 3-4 人天
前置依赖: 无(G14 同期,可并行)
影响文件: MealRecommendService、新建 RecommendationRuleService、recommendation_log 表
[ ] 新建 recommendation_log 表
CREATE TABLE IF NOT EXISTS recommendation_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT NOT NULL,
family_member_id BIGINT,
recommendation_type VARCHAR(32) NOT NULL COMMENT 'meal/supplement/exercise',
recommended_item VARCHAR(200),
explanation_json TEXT COMMENT '规则命中的解释',
dify_response_snippet VARCHAR(500),
accepted BOOLEAN,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_user_time (user_id, created_at)
) ENGINE=InnoDB;
[ ] 新建 RecommendationRuleService:在 Dify 之前执行确定性规则层
nutritionTags contains 'iron')+ reason="你的健康报告显示铁含量低,建议补充含铁食物"user_food_preference.allergens 非空 → 排除 candidate foods 中含过敏原的{recommendations:[...], reason: "规则命中文本"}[ ] MealRecommendService.buildDifyInputs() 改造:
[ ] DietaryRestrictions 不再硬编码:从 user_food_preference 表读 allergens、restrictions
[ ] MealRecommendController.recommend 落库:每次返回前插 recommendation_log
[ ] 新增端点 POST /api/meal/recommend/{logId}/accept:用户标记采纳/拒绝,回填 accepted 字段
[ ] 新增端点 POST /api/meal/recommendations/history:用户查历史推荐
优先级: P3
估算: 2 人天
前置依赖: G3(测评结构化后死字段已部分被替代)
影响文件: DanAssessmentResult、ReportParseService
resultDesc + processDesc:规划师原来填的,G3 改为结构化后已经无意义 → 删除(迁移 DROP COLUMN,实体删字段)gameRecords JSON:有价值(原始测试数据),但无消费者 → 接入:新增端点 POST /api/dan-assessment/results/{id}/game-records 返回解析后数据;EnergyService.calcChildWisdom 可读 gameRecords 替代部分 INT 字段BigFiveNormService 做百分位换算;cognitive-report.vue 增"人格倾向"模块[ ] 删字段迁移:
try {
jdbcTemplate.execute("ALTER TABLE dan_assessment_results DROP COLUMN result_desc");
jdbcTemplate.execute("ALTER TABLE dan_assessment_results DROP COLUMN process_desc");
} catch (Exception e) {}
[ ] 同步删 entity 字段、DanAssessmentResult.java
优先级: P3
估算: 3-4 人天
前置依赖: 无
影响文件: PdfParseService 重构为 ReportParserRegistry、ReportParser接口、各供应商 parser impl
[ ] 定义接口 ReportParser
interface ReportParser {
String supplierCode();
boolean canHandle(String pdfTextOrFilename);
ParsedReport parse(byte[] pdfBytes) throws ReportParseException;
}
[ ] 抽象 ReportParserRegistry:注入所有 ReportParser bean,按 canHandle 自动路由
[ ] 现有募极逻辑移到 MujiGutReportParser implements ReportParser(保留 868 行现有代码)
[ ] 新增通用 fallback SimpleScoreReportParser:现有 ReportParseService 抽出
[ ] 解析结果加 ParseConfidence:枚举 HIGH/MEDIUM/LOW;分数全找到=HIGH,<50% 字段找到=LOW
[ ] HealthReportDraft 加 confidence_level 字段,preview 端点返回
[ ] OCR 后备:若 PDF 文本提取为空(扫描版),调用腾讯云 OCR 或阿里 OCR 把图像转文本后走相同 parser
[ ] HealthReportController.parse-preview 入参加 supplierHint:可显式指定供应商
优先级: P3
估算: 0.5 人天
前置依赖: 无
影响文件: SfmsDataSourceConfig
setReadOnly(true)connection-test-query: SET SESSION TRANSACTION READ ONLY(MySQL 8 支持会话级 read only)sfmsJdbcTemplate.execute() 包装层:新增 SfmsReadOnlyJdbcTemplate extends JdbcTemplate,覆写所有 write 方法抛 UnsupportedOperationException优先级: P3
估算: 1.5 人天
前置依赖: G1(G1 完成后真正有数据可用)
影响文件: growth-curve.vue、health-trend.vue、stats/index.vue、dimension-detail.vue
growth-curve.vue 第 135 行 mock 移除
health-trend.vue 第 109-132 行 mock 移除,改为 EmptyStateEmptyState 组件 components/EmptyState.vue:图标 + 提示 + CTAprocess.env.NODE_ENV === 'development' && uni.getStorageSync('useMock') 才显示 mock,且页面顶部加 "演示数据" 红色 bannerP0:
G7 ──┐
G1 ──┬──── G18 (建议 G1 先)
│
P1: │
G8 ───┤ (G8 不依赖 G1)
G9 ───┬─ G10
G13 ──┤ (G13 与 G1 并行)
G14 ──┤ (G14 与 G1 并行)
G2 ───┘ (G2 与其他独立)
│
P2: │
G23 ──┬─ G25 (G23 先,对账机制)
G26 ──┤ (G26 独立)
G3 ───┤ (G3 与 G23 并行)
G15 ──┤ (G15 与 G23 并行)
│
P3: │
G27 ──┐ (G27 独立)
G22 ──┤ (G22 等 G1)
G21 ──┤ (G21 独立)
G4 ───┘ (G4 等 G3)
mvn clean compile 通过 + 单测 PASSFamilyAccessInterceptorTest 全 PASS(G7)/api/energy/score-history 返回时间序列(G1)/api/assessment/compare/by-child 返回 delta(G18)try-catch 包裹,失败不阻塞应用启动SELECT 校验种子数据完整性,确保关键省/市/区无空缺analysis_report TEXT 字段,新加 structured_analysis JSON 并行存在 3 个月,再淘汰旧字段| 版本 | 日期 | 修订内容 |
|---|---|---|
| v1.0 | 2026-07-10 | 首次发布,18 任务 4 优先级 |