Quellcode durchsuchen

docs: 补合 14 份安全文档到 cfclub(UI审查/令牌收敛/指标时间轴PRD/营养打标/会员缺口等)

- 来源提交 3a4748f80 共 20 个文件,其中 6 个(cf-value-unification、checkin-task-growth、ui-token-convergence、indicator-timeline 设计/PRD、nutrition 打标 PRD)已在 cfclub 且 origin 版本更全,无需重复合入
- 本提交仅补入其余 14 个缺失文档(UI审查报告、字体边距专项审查、CFC全量复查计划、活动打卡验证、家庭成员重复显示/循环依赖诊断、优惠券核查、运营推广填写稿等)
- 明确排除:login-sheet.vue(会回退 U1/U3 令牌)、tasks_backlog.json(双源漂移不入库)、docs/参考资料/与 GBK 客户目录(敏感不出库)、tmp/、tools/xaf_agent_hub/*(本地状态)
Agnes Agent vor 2 Wochen
Ursprung
Commit
9ced366139

+ 37 - 0
cfc-langgraph/AGENTS.md

@@ -0,0 +1,37 @@
+# cfc-langgraph
+
+LangGraph Python AI 服务(FastAPI + LangGraph + ChromaDB RAG)。
+
+## AI Agent 配置(Agent 必须遵守)
+
+**Git Author/Committer**: 所有 git commit 操作(含 amend/rebase/merge)的 author 与 committer 均为:
+- name: `zxyj-langgraph`
+- email: `agent@zxyj-lg.com`
+
+**提交规范**: 使用 Conventional Commits 格式(type: description),type 可选值: `feat`、`fix`、`style`、`refactor`、`perf`、`test`、`chore`、`docs`。
+
+**安全限制**:
+- 禁止执行 `git push`(无论 origin 还是任何 remote)
+- 若任务需要推送到远端,必须请求用户授权,由用户自行推送
+- 禁止覆盖远程分支或推送他人提交的代码
+- 本地分支未经验证前,不合并到主分支
+- 任何数据修改操作需先备份或确认影响范围
+
+## 端口
+
+- 本地: 9000(`uvicorn local_report_app:app --port 9000`)
+- 生产: `ai.etotem.com.cn`
+
+## 启动
+
+```powershell
+cd d:\Java_code\CFC\cfc-langgraph
+.\.venv\Scripts\python.exe -m uvicorn local_report_app:app --host 0.0.0.0 --port 9000
+```
+
+## 技术栈
+
+- Python + FastAPI
+- LangGraph(AI 图谱)
+- ChromaDB(RAG 向量库)
+- 本地仅跑 `local_report_app:app`(最小健康报告解析服务);完整 `app.main:app`(对话/RAG/whisper)走生产

+ 223 - 0
cfc-web/docs/admin-beautification-plan.md

@@ -0,0 +1,223 @@
+# cfc-web 后台页面美化治理方案 V1(2026-10-20)
+
+> **规范依据**:enterprise-ui-optimizer / `references/design-tokens.md`
+> **现状基线**:`cfc-web/src/styles/admin-tokens.css`(Ant Design 5 令牌体系已立)+ 4 个全局组件已注册(`main.js` 97/98/100 行)
+> **核心判断**:**规范已立、接入不足**。本方案不做"重新设计",做"收敛接入"——把 164 个页面接入既有令牌与组件,零视觉回归风险优先。
+> **风格对标**:Ant Design 5 / 华为云控制台 / 银行信贷系统 / 国资企业后台。克制、干净、专业、信息优先。
+
+---
+
+## 1. 页面现存问题
+
+### 1.1 审计快照(2026-10-20,cfc-web/src/views,共 164 个页面)
+
+| 债务项 | 数量 | 说明 |
+|---|---|---|
+| 旧 Element 调色板残留 | **280 处** | `#409eff`/`#f56c6c`/`#e6a23c`/`#67c23a` 与新令牌 `#1677FF`/`#FF4D4F`/`#FAAD14`/`#52C41A` 并存,**同屏出现双蓝双红** |
+| Tailwind 风色板残留 | **169 处** | `#64748B`/`#1E293B`/`#10B981`/`#3B82F6`/`#F59E0B`/`#EF4444`/`#94A3B8`,色温、明度体系完全跑偏 |
+| 内联 `style="..."` 属性 | **1014 处** | 主题化被阻断,换肤/暗色/令牌调整无法穿透 |
+| 裸 `el-tag`(未走 StatusTag) | **246 / 293 处** | 同一状态在不同页颜色/文案不一致 |
+| `el-divider content-position="left"` 分区标题 | **25 处** | 未用 `SectionTitle`,缩进字号不统一 |
+| KPI 卡未用 `StatCard` 的页面 | ≈60+ 页 | 仍有手写 `stat-card`/`td-stat-card` 等私有实现 |
+
+**债务 top 文件**(硬编码色值计数):
+
+| 文件 | 处数 | 文件 | 处数 |
+|---|---|---|---|
+| `views/Dashboard.vue` | 74 | `views/TeacherDashboard.vue` | 33 |
+| `views/teacher/TeacherMessages.vue` | 58 | `views/admin/Dashboard.vue` | 31 |
+| `views/Login.vue` | 48 | `views/Layout.vue` | 26 |
+| `views/teacher/TeacherFamilies.vue` | 39 | `views/admin/ReportTemplateManage.vue` | 24 |
+| `views/teacher/TeacherTeam.vue` | 38 | `views/teacher/TeacherOrders.vue` | 22 |
+| `views/admin/ProductEdit.vue` | 36 | `views/admin/ProductManage.vue` | 21 |
+
+**组件接入率**:`StatCard` 22 处 / `StatusTag` 47 处 / `PageHeader` 41 处 / `SectionTitle` 3 处 / `adm-table-wrap` 144 页(此项已达标)。
+
+### 1.2 分区问题(按区域)
+
+- **全局主题层**:449 处跑偏色值直接写在页面与内联样式里,令牌体系被"架空";`#409eff` 旧蓝与令牌 `--adm-primary(#1677FF)` 混用,界面出现"两种蓝"。
+- **顶部标题区**:约 123 页未用 `PageHeader`,标题字号(16/18/20/22 均有)、主操作按钮位置、副标题有无全凭页面习惯,首屏气质不统一。
+- **筛选区**:部分页面筛选器包在 `el-card` 里(双重边框视觉噪音),部分裸排;"查询/重置"位置左右摇摆;高级筛选无折叠,一行溢出到第二行时对齐混乱。
+- **KPI 卡片区**:手写统计卡样式各异(圆角、数值字号 20/24/28、是否千分位、null 显示空还是 `-`),`StatCard` 有 `COLOR_MAP` 自动映射旧色名,迁移成本极低但只铺了 5 页。
+- **表格区**:表头底色不统一(白/浅灰/品牌蓝都有);金额列左对齐未用等宽数字;操作列未固定右侧;空数据区无占位文案;`StatusTag` 未接入导致状态色五花八门。
+- **详情/表单区**:分区标题 25 处仍用 `el-divider`;详情标签列宽不一;金额无千分位;手机号/身份证未脱敏;表单页底部操作区有的贴底有的跟随内容流。
+- **弹窗**:依赖全局 `admin-tokens.css` 已接管 `el-dialog`/`el-message-box`(合格),但弹窗内表单的分区、按钮层级仍按页面习惯写。
+- **登录页 `Login.vue`**(48 处硬编码):品牌门面页,Tailwind 风渐变与卡片投影偏重,与企业级调性不符。
+
+---
+
+## 2. 整体优化思路
+
+**策略:「先收敛、后铺开、再结构、终业务」四阶段,每批独立可提交、可回归。**
+
+1. **收敛(A 批,零视觉风险)**:把 449 处跑偏色值按等值/语义映射替换为 `adm-*` 令牌。色彩口径统一后,后续所有批量改造才有意义——否则改一处样式冒出一处新色。
+2. **铺开(B 批,低风险)**:4 个全局组件向剩余页面铺开(StatusTag / SectionTitle / StatCard / PageHeader)。组件已全局注册、无需 import,迁移是"机械替换 + 删除死样式"。
+3. **结构(C+D 批,中风险)**:内联样式清理(1014 处,按页切片)+ 筛选区/表格/空态/详情分区按令牌标准化。
+4. **业务(E 批)**:金融/台账业务适配——金额千分位、敏感信息脱敏、关键写操作留痕与确认弹窗。
+
+**为什么这样排序**:色彩令牌是全系统的"分母",分母不统一时任何视觉优化都会互相打架;组件铺开依赖令牌就位;结构改造依赖组件就位;业务适配是最后一道专业度打磨。每批结束都跑一次等价回归(编译比对)+ grep 归零验收,保证"看得见的好看"不引入"看不见的回归"。
+
+---
+
+## 3. 分区详细优化方案
+
+### 3.1 全局主题层(A 批主战场)
+
+**优化前**:页面里散写 `color: #409eff`、`style="color:#F59E0B"`,与令牌双轨运行。
+**优化后**:全站只允许引用 `var(--adm-*)`;页面级禁止出现裸 hex(令牌定义文件除外)。
+
+替换映射表(等值或最近语义):
+
+| 跑偏值 | 替换为 | 性质 |
+|---|---|---|
+| `#409eff` / `#409EFF` | `var(--adm-primary)` | 等值 |
+| `#f56c6c` / `#EF4444` | `var(--adm-error)` | 等值 |
+| `#e6a23c` / `#F59E0B` | `var(--adm-warning)` | 等值 |
+| `#67c23a` / `#10B981` | `var(--adm-success)` | 等值 |
+| `#909399` / `#999` / `#64748B` / `#94A3B8` | `var(--adm-text-45)` | 语义 |
+| `#303133` / `#1E293B` | `var(--adm-text-88)` | 语义 |
+| `#3B82F6` | `var(--adm-primary)` | 语义 |
+| `#fff` / `#FFF`(背景用途) | `var(--adm-bg-container)` | 语义 |
+
+落地方式:写一次性替换脚本(带断言、逐文件计数核对),按目录切片提交;每片跑 `vue-cli-service build --dest dist_verify` 验证编译,视觉走等价回归比对。
+
+### 3.2 顶部标题区(B 批)
+
+**优化前**:各页手写 `<h2 class="page-title">` / `<div class="header">`,标题 16–22px 不等,主操作按钮位置随机。
+**优化后**:统一 `PageHeader`(已存在,41 页在用):
+
+```vue
+<PageHeader title="订单与佣金" subtitle="共 128 笔">
+  <el-button type="primary" icon="el-icon-download" @click="exportExcel">导出</el-button>
+</PageHeader>
+```
+
+令牌:主标题 20px/600(`text-title`),副标题 12px 次文,右侧唯一主操作。剩余 ≈123 页按"列表页/表单页"两模板批量替换。
+
+### 3.3 筛选区
+
+**优化前**:筛选器包卡片、双边框;查询/重置位置不一。
+**优化后**:去卡片化,底部 1px 分割线分区(令牌已有 `.filter-bar` 规则),常用筛选项前置 ≤4 个一行,高级筛选折叠,"查询/重置"居右:
+
+```vue
+<div class="filter-bar">
+  <el-input v-model="query.keyword" placeholder="订单号 / 家庭名称" clearable style="width:220px" />
+  <el-select v-model="query.status" placeholder="状态" clearable>
+    <el-option v-for="o in STATUS_OPTIONS" :key="o.value" :label="o.label" :value="o.value" />
+  </el-select>
+  <div class="filter-bar__actions" style="margin-left:auto;display:flex;gap:8px">
+    <el-button type="primary" @click="load">查询</el-button>
+    <el-button @click="reset">重置</el-button>
+  </div>
+</div>
+```
+
+### 3.4 KPI 卡片区
+
+**优化前**:手写 `stat-card` / `td-stat-card` / `accent-*` 私有样式,数值字号与 null 表现不一。
+**优化后**:统一 `StatCard`(`COLOR_MAP` 已自动映射旧色名,迁移时 `color="blue"` 之类可原样保留):
+
+```vue
+<div class="kpi-row" style="display:grid;grid-template-columns:repeat(4,1fr);gap:16px">
+  <StatCard label="总结算金额" :value="stats.total" prefix="¥" suffix="元" color="primary" icon="el-icon-money" />
+  <StatCard label="待结算" :value="stats.pending || 0" color="warning" icon="el-icon-time" />
+</div>
+```
+
+### 3.5 表格区
+
+**优化前**:表头底色三种、金额左对齐、状态裸 `el-tag`、空态空白。
+**优化后**(令牌 §7 已定义,页面只需接入):
+
+```vue
+<div class="adm-table-wrap">
+  <el-table :data="list" v-loading="loading">
+    <el-table-column prop="orderNo" label="订单号" min-width="160" fixed="left" />
+    <el-table-column label="金额" width="120" align="right">
+      <template slot-scope="{ row }"><span class="adm-num">¥{{ formatAmount(row.amount) }}</span></template>
+    </el-table-column>
+    <el-table-column label="状态" width="100">
+      <template slot-scope="{ row }"><StatusTag :value="row.status" :options="ORDER_STATUS_OPTIONS" /></template>
+    </el-table-column>
+    <el-table-column label="操作" width="150" fixed="right">
+      <template slot-scope="{ row }">
+        <el-button type="text" @click="view(row)">查看</el-button>
+        <el-button type="text" style="color:var(--adm-error)" @click="reject(row)">驳回</el-button>
+      </template>
+    </el-table-column>
+  </el-table>
+</div>
+```
+
+要点:金额列 `align="right"` + `.adm-num` 等宽数字;状态走 `StatusTag` 映射式;操作列固定右、行内弱操作用文本按钮;空态由 `el-table__empty-text` 令牌接管。
+
+### 3.6 详情/表单区
+
+**优化前**:25 处 `el-divider` 分区;详情标签列宽不一。
+**优化后**:`SectionTitle` + `detail-section`(令牌已有):
+
+```vue
+<div class="detail-section">
+  <SectionTitle title="家庭信息" desc="下单时所选家庭" />
+  <div class="detail-row">…</div>
+</div>
+```
+
+表单页:底部操作区统一 `edit-form-footer`(顶部分割线 + 右对齐,确认按钮唯一主按钮)。
+
+### 3.7 登录页(品牌门面)
+
+**优化前**:Tailwind 风渐变 + 重投影 + 48 处硬编码。
+**优化后**:左侧品牌区(主色 `#001529` 深底 + 白字品牌名 + 一句功能向副标),右侧白底表单卡(8px 圆角 + 1px 描边 + 轻阴影);去掉一切渐变与装饰插画;输入框/按钮走全局令牌。
+
+---
+
+## 4. 标准化设计规范落地(可复用)
+
+**铁律:不新建令牌,先接入。** `admin-tokens.css` 已是单一事实源,与 `design-tokens.md` 逐条对齐;本方案的规范落地 = 把页面接进来,而非再造一套。
+
+**页面准入 checklist**(每个页面改造完逐项打钩):
+
+- [ ] 无裸 hex 色值(`grep -rniE '#[0-9a-f]{3,6}'` 页面文件 = 0,令牌文件除外)
+- [ ] 无内联 `style=`(C 批归零;过渡期允许布局类内联,禁色彩/字号类)
+- [ ] 列表页根容器 `admin-page` 或 `page-container`,表格外包 `adm-table-wrap`
+- [ ] 标题区用 `PageHeader`;主操作按钮每页 ≤1
+- [ ] KPI 用 `StatCard`;状态用 `StatusTag`;分区用 `SectionTitle`
+- [ ] 金额列右对齐 + `.adm-num`;分页右下 + "共 N 条"
+- [ ] 空态有占位文案;超长文本省略 + tooltip
+- [ ] 间距取自 `--spacing-*`(8px 基准),禁止 5/7/10/14/18
+
+**组件标准速查**:
+
+| 组件 | 用途 | 关键 props |
+|---|---|---|
+| `PageHeader` | 页面标题区 | `title / subtitle` + 默认插槽(右侧操作) |
+| `StatCard` | KPI 概览卡 | `label / value / icon / color / prefix / suffix / thousand` |
+| `StatusTag` | 状态标签 | 映射式 `:value + :options`(推荐)/ 简单式 `:type + :text` |
+| `SectionTitle` | 卡片内分区 | `title / desc / plain` + 插槽(右侧操作) |
+
+**回归验收**:每批跑 `./node_modules/.bin/vue-cli-service build --dest dist_verify` 至 `DONE Build complete`;A 批另跑等价回归逐页编译比对(零观感变化);提交信息带批次标记(如 `style(admin): A-1 色彩令牌收敛-仪表盘`)。
+
+---
+
+## 5. 本次设计能力提升点
+
+1. **"分母优先"思维**:一套 UI 体系崩坏,很少是某个页面设计得差,而是"色彩/间距这些分母"没统一,导致每个页面各自为政。先收分母(令牌),再谈分子(单页美化),投入产出比完全相反——这是本次 449 处跑偏色给的最大教训。
+2. **组件即规范**:`StatusTag` 把"状态→颜色/文案"的映射从 293 处内联判断收敛到一处 options 表,本质是把"设计规范"编译成"代码约束"。规范只有变成组件 props,才不会被页面作者无意绕过。
+3. **可回归的美化才是真美化**:每批都带"编译通过 + grep 归零 + 等价回归"三道闸门,让"好看"成为可验证的属性,而不是审美玄学——这也是本仓两条工作线(cfc-web / cfc-frontend)能并行推进不互相踩踏的原因。
+
+---
+
+## 附:批次实施路线图(A→E,每批独立可提交)
+
+| 批次 | 内容 | 规模 | 风险 | 验收 |
+|---|---|---|---|---|
+| **A** | 色彩令牌收敛(旧 Element 280 + Tailwind 169) | 449 处 / 约 90 文件 | 低(等值/语义映射) | grep 归零 + 构建通过 + 等价回归 |
+| **B** | 组件铺开:246 裸 `el-tag`→`StatusTag`、25 `el-divider`→`SectionTitle`、剩余 KPI 卡→`StatCard`、≈123 页→`PageHeader` | 约 120 文件 | 低(机械替换+删死样式) | 组件计数对齐 + 构建通过 |
+| **C** | 内联 `style` 清理 | 1014 处 | 中(按页切片,逐页目检) | 归零率 ≥90% + 构建通过 |
+| **D** | 结构标准化:筛选区去卡片化、表格标准、空态、详情分区、登录页 | 约 60 页 | 中 | 页面 checklist 全勾 |
+| **E** | 业务适配:金额千分位、手机号/身份证脱敏、关键写操作确认弹窗与留痕入口 | 按业务页 | 中 | 业务走查 + 联调清单 |
+
+**切片顺序建议**(按债务从高到低、公共页优先):`Dashboard` → `admin/Dashboard` → `teacher/*`(Messages/Families/Team)→ `admin/Product*` → `admin/ReportTemplateManage` → 其余按 A→E 批次滚动推进。
+
+> 本方案与 `cfc-frontend/docs/beautification-plan-v2.md`(小程序侧 D 系列美化)方法论同源:审计定量 → 分批推进 → 每批可提交可回归。两侧互不干扰,可并行。

+ 140 - 0
docs/2026-10-23-basic-membership-gap-analysis.md

@@ -0,0 +1,140 @@
+# 9.9 元体验会员(BASIC)权限:当前实现 vs 设计差异分析
+
+> 日期:2026-10-23
+> 设计依据:`docs/superpowers/specs/2026-09-10-basic-membership-design.md`(PRD v1.0)、`docs/superpowers/specs/2026-10-11-restore-basic-and-diet-share-design.md`(恢复规格)
+> 实现依据:`schema.sql`(5851-5867)、`DatabaseInitializer`(迁移 351/352/360/381)、`MembershipService`、`BasicQuotaService`、`ActivationQuotaService`、`AiGateway`、`AssessmentAppointmentService`、`HealthReportController`、`DietRecordController`、`BasicQuotaScheduledTasks`、前端 `cfc-frontend`
+
+---
+
+## 一、结论(TL;DR)
+
+1. 9.9 会员 = **BASIC「体验会员」**,月付 9.9 元(`price_monthly=990`),仅月付。当前实现与两份设计规格**整体一致,但存在 1 个 P0 级功能缺陷 + 8 处口径/完整性差异**。
+2. **P0:AI 对话配额没有按会员等级分流**。`AiGateway.chat()` 对所有登录用户无差别扣 `AI_CHAT` 配额(每月 3 次):
+   - FREE 用户实际白得 3 次/月(设计为 0 次;仅前端有入口门禁,后端无门禁);
+   - **FAMILY/PREMIUM 付费用户第 4 次起被拦截**(设计为不限次,权益对比表也承诺"不限次数")——直接损害 131.4 元/月付费用户体验;
+   - 附带 `/nutrition/send` 配额耗尽时 NPE(`resp` 未判空)。
+3. P1 三处:配额耗尽提示不回传用户(软引导文案只进了日志);健康报告上传 2 份/月配额被迁移 352 静默删除(实际变不限次,两份规格均未记录);BASIC 到期提醒定时任务遍历空列表(`listActiveBasicMemberUserIds()` 仍返回空列表)。
+4. 其余为 P2 完整性问题(开通不初始化配额、FOOD_RECOGNIZE 状态查询未暴露、权益条目缺失 2 项、旧文档口径冲突)。
+
+---
+
+## 二、9.9 会员当前实现的权限清单(事实源)
+
+### 2.1 等级定义
+
+| 字段 | 值 | 说明 |
+|------|-----|------|
+| `level_code` | `BASIC` | schema.sql:5854 / 迁移 360 |
+| `level_name` | `体验会员` | 前端会员页/权益页/开通页均按此展示 |
+| `price_monthly` | `990`(分) | 9.9 元;季付/年付为 NULL(仅月付,符合 PRD) |
+| `max_children` | `3` | FREE=0 / FAMILY=10 |
+| `max_tasks_per_day` | `3` | |
+| `level_order` | `2` | FREE=1 < BASIC=2 < FAMILY=3 < PREMIUM=4 < LIFETIME=5;PROVIDER=1(非会员,迁移 351/381) |
+| 到期判断 | `level=="BASIC"` 且 `memberExpireTime > now` | `BasicQuotaService.isActiveBasicMember()` |
+
+### 2.2 功能开关(features,schema 与 `canUseFeature` 硬编码一致,共 11 项)
+
+| feature | 权益 |
+|---------|------|
+| `free_activities` | 免费活动 |
+| `free_courses` | 免费课程 |
+| `discount_purchase` | 优惠价购买(⚠ 无生效折扣配置,见观察 O1) |
+| `invite_family` | 邀请家人 |
+| `mascot_ai_lite` | AI 健康顾问 3 次/月 |
+| `ai_report_reading_lite` | 报告 AI 解读 1 次/月 |
+| `assessment_quota_lite` | 测评预约 1 次/月(硬门禁) |
+| `health_report_upload_lite` | 健康报告上传(配额已被移除 → **实际不限次**) |
+| `growth_plan_lite` | 基础成长计划 |
+| `task_template_lite` | 更多任务模板 |
+| `referral_commission` | 推荐佣金(2026-10-11 恢复规格新增) |
+
+### 2.3 月度配额(`BasicQuotaService.QUOTA_LIMITS`,每月 1 号 00:00 定时刷新)
+
+| 配额类型 | 上限 | 超限行为(现状) | 集成点 |
+|---------|------|----------------|--------|
+| `AI_CHAT` | 3 次 | 软引导(当前只记日志,见 D1/D2) | `AiGateway.chat()`:184 |
+| `REPORT_READING` | 1 次 | 静默 return(报告上传成功但永不解读,见 O2) | `HealthReportController.triggerAiAnalysis()`:2755 |
+| `ASSESSMENT_QUOTA` | 1 次 | 硬门禁,抛 `BasicQuotaExhaustedException` | `AssessmentAppointmentService.createAppointment()`:34 |
+| `FOOD_RECOGNIZE` | 3 次 | FREE 拒绝 / BASIC 扣配额 / FAMILY+ 不限,403+toast | `DietRecordController.recognizeFood()`:26 |
+
+### 2.4 周边链路现状
+
+- **购买**:`/plans` 返回 BASIC(990/月);前端 `pages/membership/pay?levelCode=BASIC&period=monthly`;支付回调 → `activateMembership()` 写 `family_memberships` BASIC 记录并全家同步等级(level-agnostic,BASIC 可用)。
+- **到期**:每天 4:00 `downgradeExpiredMembers()` 按 `family_memberships.end_date` 过期 → 全家降 FREE(不区分等级,BASIC 生效)。
+- **前端**:AI 对话页展示配额剩余 + `UpgradeGuideModal` 软引导(PRD 符合);入口门禁 `getMyMembership().memberLevel !== 'FREE'`。
+- **管理端**:`AdminBasicQuotaController` 可手动重置配额。
+
+---
+
+## 三、设计 vs 实现差异对照表
+
+| # | 差异项 | 设计要求(PRD/恢复规格) | 当前实现 | 严重度 |
+|---|--------|------------------------|----------|--------|
+| D1 | AI 对话配额等级分流 | 仅 BASIC 3 次/月;FREE 无 AI;FAMILY+ 不限 | 所有用户无差别扣配额:FREE 实得 3 次/月;FAMILY+ 第 4 次起被拦;`/nutrition/send` NPE | **P0** |
+| D2 | 配额耗尽软引导 | Toast/弹窗"本月额度已用完(3/3),升级 FAMILY 享不限次" | 后端仅 `log.info`;`/chat/send` 返回空 answer;文案不回传用户 | P1 |
+| D3 | 健康报告上传配额 | PRD:2 份/月 + `REPORT_UPLOAD_LITE` 权益(共 5 条 benefits) | 迁移 352 删除该权益与配额记录;features 仍含 `health_report_upload_lite` → 实际不限次;两份规格均未记录此变更 | P1 |
+| D4 | BASIC 到期提醒 | 恢复规格:`listActiveBasicMemberUserIds()` 恢复真实查询;PRD:到期前 1-5 天推送 | 仍返回 `Collections.emptyList()`(注释还写"已废弃")→ 定时任务永远空转;微信模板消息推送仍是 TODO | P1 |
+| D5 | 开通时初始化配额 | 恢复规格:`activateMembership` 开通 BASIC 时调 `activateBasicQuota` | 无任何调用方(死代码);靠 `consumeQuota` 懒插入兜底,功能可用但与规格不符 | P2 |
+| D6 | `ActivationQuotaService` 配额表 | 应含 `FOOD_RECOGNIZE` 3 次/月 | 只有 3 类(缺 `FOOD_RECOGNIZE`),与 `BasicQuotaService` 不一致 | P2 |
+| D7 | 食材识别进度可见 | PRD §6:显示"本月还可识别 X 次" | `/basic-quota/status/all` 只返回 3 类,不含 `FOOD_RECOGNIZE`;前端无展示 | P2 |
+| D8 | BASIC 权益条目 | benefits 应覆盖全部 Lite 权益 | 仅 4 条(无 `FOOD_RECOGNIZE_LITE`、无 `REPORT_UPLOAD_LITE`),权益页看不到"食材识别 3 次/月"说明 | P2 |
+| D9 | 旧文档口径冲突 | — | `docs/function-analysis-by-membership.md` 仍写 BASIC=家庭会员(不限量为主)、等级映射为 PROFESSIONAL/ENTERPRISE,与实现(体验会员限次、FREE/BASIC/FAMILY/PREMIUM/LIFETIME/PROVIDER)冲突 | P2(文档) |
+
+### 观察项(非差异,需产品确认)
+
+- **O1**:BASIC features 含 `discount_purchase`,但无生效折扣配置(`member_discount_family` 仅 FAMILY 生效),对比表显示 BASIC 商品折扣"—"。声明与效果不符,建议从 BASIC features 移除或补充配置。
+- **O2**:`REPORT_READING` 超限后 `triggerAiAnalysis` 静默 return,用户端无任何提示(报告上传成功但永远不解读)。
+
+---
+
+## 四、P0 详解(D1:AI 对话配额无等级分流)
+
+### 4.1 代码路径
+
+```
+AIChatController./chat/send(无会员门禁)
+  → AIService.sendMessage → AiGateway.chat():184
+      if (userId != null && userId != 0L) {
+          basicQuotaService.consumeQuota(userId, "AI_CHAT");   // 无等级判断
+          if (!result.isSuccess()) return null;
+      }
+```
+
+`consumeQuota()` 内部无等级分支:`total` 恒为 3,**任何用户**(FREE/BASIC/FAMILY/PREMIUM)第 4 次起 `used >= total` → `success=false` → `chat()` 返回 null:
+- `/chat/send`:`answer=""`,用户看到空白回复;
+- `/nutrition/send`(405-407 行):`resp.getOrDefault(...)` 未判空 → **NPE → 500**。
+
+### 4.2 佐证"这是缺陷而非有意设计"
+
+1. `MembershipService.getMembershipCompareData()`(1744-1746 行)向用户承诺 FAMILY/PREMIUM"不限次数";
+2. 恢复规格风险节明确写道:"不恢复 `consumeQuota` 内原有的 `isActiveBasicMember` 前置校验(**避免误伤 FREE/FAMILY 的 AI 对话**)"——现状恰好相反(FREE/FAMILY 都被限成 3 次);
+3. PRD §8.1.4:仅对 `isActiveBasicMember(userId)` 消耗配额。
+
+### 4.3 修复建议
+
+在 `AiGateway.chat()` 内按 `level_order` 分流(与 `consumeFoodRecognizeQuota` 同构):
+
+```java
+if (userId != null && userId != 0L) {
+    int userOrder = membershipService.getLevelOrder(membershipService.getMemberLevel(userId));
+    int basicOrder = membershipService.getLevelOrder("BASIC");
+    if (userOrder < basicOrder) {            // FREE:后端补门禁拒绝
+        ...
+    } else if (userOrder == basicOrder) {    // BASIC:走配额
+        QuotaConsumeResult r = basicQuotaService.consumeQuota(userId, "AI_CHAT");
+        if (!r.isSuccess()) { log...; return null; }
+    }
+    // FAMILY+ 直接放行,不扣配额
+}
+```
+
+配套:`/nutrition/send` 补 `resp` 判空;配额耗尽文案经 `Result` 回传前端(软引导 UpgradeGuideModal 已有,只差后端传参)。
+
+---
+
+## 五、待确认
+
+1. **D1 是否按 §4.3 方案修**(P0,建议最优先);
+2. **D3 报告上传**:产品意图是"引流不限次"(回写文档、去掉 features 的 `_lite` 语义)还是恢复 2 份/月配额?
+3. **D4 到期提醒推送**是否排期(需接微信模板消息);
+4. **O1** `discount_purchase` 是否从 BASIC features 移除。

+ 212 - 0
docs/2026-10-23-项目整体修改完善开发计划.html

@@ -0,0 +1,212 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+<meta charset="UTF-8">
+<meta name="viewport" content="width=device-width, initial-scale=1.0">
+<title>CFC 项目整体修改 / 完善 / 开发计划 v1.0</title>
+<style>
+  :root{
+    --bg:#f7f8fa; --card:#ffffff; --ink:#1f2329; --sub:#646a73; --line:#e5e6eb;
+    --p0:#d93026; --p1:#d97b00; --p2:#5b8ff9; --ok:#00a870; --brand:#6c3ce0;
+  }
+  *{box-sizing:border-box}
+  body{margin:0;background:var(--bg);color:var(--ink);
+       font-family:"PingFang SC","Microsoft YaHei","Noto Sans SC",system-ui,sans-serif;
+       font-size:14px;line-height:1.7;}
+  .wrap{max-width:1080px;margin:0 auto;padding:32px 24px 64px;}
+  header{background:linear-gradient(120deg,#6c3ce0,#8b5cf6);color:#fff;border-radius:14px;
+         padding:28px 32px;box-shadow:0 6px 24px rgba(108,60,224,.18);}
+  header h1{margin:0 0 6px;font-size:26px;letter-spacing:.5px;}
+  header p{margin:0;opacity:.9;font-size:13px;}
+  .meta{margin-top:14px;display:flex;flex-wrap:wrap;gap:8px;}
+  .tag{background:rgba(255,255,255,.18);border:1px solid rgba(255,255,255,.28);
+       border-radius:999px;padding:3px 12px;font-size:12px;}
+  section{margin-top:28px;}
+  h2{font-size:19px;margin:0 0 14px;padding-left:11px;border-left:4px solid var(--brand);}
+  h3{font-size:15px;margin:18px 0 8px;color:#2b2f36;}
+  .card{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:18px 20px;}
+  table{width:100%;border-collapse:collapse;background:var(--card);
+        border:1px solid var(--line);border-radius:12px;overflow:hidden;font-size:13px;}
+  th{background:#f2f3f5;text-align:left;padding:10px 12px;font-weight:600;color:#333;}
+  td{padding:10px 12px;border-top:1px solid var(--line);vertical-align:top;}
+  tr:hover td{background:#fafbfc;}
+  .p0{color:var(--p0);font-weight:600;} .p1{color:var(--p1);font-weight:600;}
+  .p2{color:var(--p2);font-weight:600;} .ok{color:var(--ok);font-weight:600;}
+  code{background:#f2f3f5;padding:1px 6px;border-radius:4px;font-size:12px;
+       font-family:"Cascadia Mono",Consolas,monospace;}
+  .badge{display:inline-block;border-radius:6px;padding:2px 9px;font-size:12px;color:#fff;}
+  .b-p0{background:var(--p0);} .b-p1{background:var(--p1);} .b-p2{background:var(--p2);}
+  .grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(240px,1fr));gap:12px;}
+  .stat{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:14px 16px;}
+  .stat .n{font-size:22px;font-weight:700;color:var(--brand);}
+  .stat .l{font-size:12px;color:var(--sub);}
+  ul{margin:6px 0 0 18px;padding:0;} li{margin:3px 0;}
+  .flow{display:flex;flex-wrap:wrap;gap:10px;align-items:center;margin-top:6px;}
+  .node{background:var(--card);border:1px solid var(--line);border-radius:10px;
+        padding:10px 14px;font-size:13px;min-width:150px;}
+  .node b{display:block;font-size:12px;color:var(--sub);font-weight:500;margin-bottom:2px;}
+  .arrow{color:var(--sub);font-size:18px;}
+  footer{margin-top:32px;color:var(--sub);font-size:12px;text-align:center;}
+</style>
+</head>
+<body>
+<div class="wrap">
+
+<header>
+  <h1>CFC 项目整体修改 / 完善 / 开发计划</h1>
+  <p>基于全项目需求 · 设计 · 开发情况的调研,结合本轮变更需求制定,并已派发至对应智能体执行</p>
+  <div class="meta">
+    <span class="tag">版本 v1.0</span>
+    <span class="tag">2026-10-23</span>
+    <span class="tag">分支 cfclub</span>
+    <span class="tag">6 个智能体实例 / 并发上限 2 每实例</span>
+  </div>
+</header>
+
+<section>
+  <h2>一、调研结论速览</h2>
+  <div class="grid">
+    <div class="stat"><div class="n">4</div><div class="l">代码模块:backend / frontend(小程序) / web(后台) / langgraph</div></div>
+    <div class="stat"><div class="n">255</div><div class="l">已有设计资产:约 130 份规格 + 约 125 份计划</div></div>
+    <div class="stat"><div class="n">11</div><div class="l">本轮工作包 W1–W11</div></div>
+    <div class="stat"><div class="n">5</div><div class="l">已派发执行的工作包</div></div>
+  </div>
+  <div class="card" style="margin-top:12px">
+    <b>工作区状态:</b>分支 <code>cfclub</code>,12 个已修改文件(全部在 <code>cfc-web/</code>,属 admin 内联样式清理未提交部分),
+    另有 <code>_oc_scan*.json</code>、<code>cfc-web/dist_verify/</code>、畸形文件名 <code>CHANGELOG-v$(node.undefined.md</code> 等未跟踪临时文件。
+  </div>
+</section>
+
+<section>
+  <h2>二、本轮已完成</h2>
+  <div class="card">
+    <ul>
+      <li><b>小程序首页应用库改名</b>(已提交并推送 <code>origin/cfclub</code>):<code>utils/app-library.js</code> 应用名压缩至 ≤3 汉字;<code>pages/index-home/index.vue</code> 默认快捷项 <code>task</code> → <code>diet</code></li>
+      <li>管理后台 admin 内联样式清理(C-14 / C-15 / C-16)</li>
+      <li><b>发现并定位线上 Bug</b>:<code>WechatService.java:709</code> 发货同步 URL 缺 <code>/wxa</code> 前缀(详见 W2)</li>
+    </ul>
+  </div>
+</section>
+
+<section>
+  <h2>三、工作包清单与优先级</h2>
+  <table>
+    <tr><th style="width:64px">编号</th><th>工作包</th><th style="width:80px">优先级</th><th>落点</th></tr>
+    <tr><td>W1</td><td>活动报名名额原子校验 + 退款/取消回滚名额(并发超卖)</td><td><span class="badge b-p0">P0</span></td><td><code>ActivityRegistrationService/Entity/Mapper</code></td></tr>
+    <tr><td>W2</td><td>微信发货同步 URL 缺 <code>/wxa</code> 前缀(<b>确认的线上 Bug</b>)</td><td><span class="badge b-p0">P0</span></td><td><code>WechatService.java:709</code></td></tr>
+    <tr><td>W3</td><td>规划师培训认证闸门落地(9 张表,设计已有、代码零实现)</td><td><span class="badge b-p0">P0</span></td><td><code>planner_*</code> 新表 + 接单闸门</td></tr>
+    <tr><td>W4</td><td>真实签到落地(修「签到审核形同虚设」)</td><td><span class="badge b-p0">P0</span></td><td><code>activity_checkins</code></td></tr>
+    <tr><td>W5</td><td>履约多渠道</td><td><span class="badge b-p1">P1</span></td><td><code>fulfillment-multi-mode-design.md</code></td></tr>
+    <tr><td>W6</td><td>首页家庭任务数(此前只做了积分修复)</td><td><span class="badge b-p1">P1</span></td><td><code>cfc-frontend</code> 首页</td></tr>
+    <tr><td>W7</td><td>机构管理员管理 16 项</td><td><span class="badge b-p1">P1</span></td><td><code>cfc-web/src/views/admin/**</code></td></tr>
+    <tr><td>W8</td><td>通知通道实现(5 个通道是 <code>// TODO</code> 占位)</td><td><span class="badge b-p1">P1</span></td><td><code>service/notification/**</code></td></tr>
+    <tr><td>W9</td><td>删除空壳子系统 <code>guide_activities</code> / <code>activity_provisions</code>(须先依赖审计)</td><td><span class="badge b-p2">P2</span></td><td>entity / mapper / 建表脚本</td></tr>
+    <tr><td>W10</td><td>恢复单元测试(<code>pom.xml</code> 全局 <code>skipTests=true</code>)</td><td><span class="badge b-p2">P2</span></td><td><code>cfc-backend/pom.xml:28</code></td></tr>
+    <tr><td>W11</td><td>仓库卫生与 .gitignore 补齐</td><td><span class="badge b-p2">P2</span></td><td>仓库根 / <code>cfc-web/</code></td></tr>
+  </table>
+</section>
+
+<section>
+  <h2>四、分配方案(已派发)</h2>
+  <div class="card" style="margin-bottom:12px">
+    <b>分配原则:</b>每实例同时最多 2 个 CFC 会话 → 本轮首发 5 个工作包、<b>每实例各 1 个</b>,
+    为中断巡检与续做留 1 个空位;生产实例 <code>prod-etotem</code> 本轮不派发。
+  </div>
+  <table>
+    <tr><th>工作包</th><th>实例</th><th>会话</th><th>模型</th><th>状态</th></tr>
+    <tr><td>W1 名额原子校验 + 退款回滚</td><td>lan-248</td><td><code>ses_e9b946894ffe…</code></td><td><code>axonhub/deepseek</code></td><td class="ok">已投递</td></tr>
+    <tr><td>W2+W5 发货 URL 修复 + 履约多渠道</td><td>lan-204</td><td><code>ses_e9b94182bffe…</code></td><td><code>axonhub/deepseek</code></td><td class="ok">已投递</td></tr>
+    <tr><td>W3+W4 规划师认证闸门 + 真实签到</td><td>lan-189</td><td><code>ses_e9b93c31affe…</code></td><td><code>sensenova/deepseek-v4.1-flash</code></td><td class="ok">已投递(首次因模型/quota 失败后重派)</td></tr>
+    <tr><td>W7 机构管理员管理 16 项</td><td>pub-iwintrue</td><td><code>ses_e9b937dedffe…</code></td><td><code>etotem/deepseek</code></td><td class="ok">已投递</td></tr>
+    <tr><td>W8 通知通道实现</td><td>local-45278</td><td><code>ses_e9b9335f4ffe…</code></td><td><code>axonhub/deepseek</code></td><td class="ok">已投递</td></tr>
+  </table>
+  <h3>第二批(首批腾出空位后依次派发)</h3>
+  <ul>
+    <li>W6 首页家庭任务数 → <code>pub-iwintrue</code></li>
+    <li>W9 空壳子系统删除 → <code>lan-248</code></li>
+    <li>W10 恢复单元测试 → <code>lan-189</code></li>
+    <li>W11 仓库卫生 → <code>local-45278</code></li>
+  </ul>
+</section>
+
+<section>
+  <h2>五、执行约束</h2>
+  <div class="card">
+    <ol style="margin:0 0 0 18px;padding:0">
+      <li><b>按规格落地</b>:优先读 <code>docs/superpowers/specs/</code> 对应规格;规格与现状冲突时先汇报,不擅自改设计。</li>
+      <li><b>只动自己负责的文件</b>:跨包文件(<code>DatabaseInitializer.java</code>、<code>schema_assessment.sql</code>)改动前先说明。</li>
+      <li><b>提交纪律</b>:<code>git pull --rebase --autostash</code> 后再改;只 <code>git add</code> 自己的文件(禁止 <code>git add .</code>);push 失败就 rebase 重试;<b>不要长时间卡在 <code>git rebase --continue</code></b>,需人工裁决时立刻停并汇报。</li>
+      <li><b>不改构建产物</b>(<code>target/</code>)与自动生成文件。</li>
+      <li><b>完成后汇报</b>:改了哪些文件、是否提交推送、遗留问题。</li>
+    </ol>
+  </div>
+</section>
+
+<section>
+  <h2>六、模型策略(用户定稿)</h2>
+  <div class="card" style="margin-bottom:12px">
+    <ol style="margin:0 0 0 18px;padding:0">
+      <li><b>优先沿用会话原模型</b>:复用已有会话时默认不换模型,只有显式 <code>--model</code> 才切;</li>
+      <li><b>只有 429/限流才自动切换</b>,切换目标<b>按实例取一个</b>(<code>config.json</code> 的 <code>fallback_model</code>,缺省 <code>axonhub/deepseek</code>);</li>
+      <li><b>不级联切换其他模型</b>:该实例连自己的兜底模型都没有时,宁可跳过也不换 nvidia / sensenova / etotem / axon 等其他模型。</li>
+    </ol>
+  </div>
+  <table>
+    <tr><th>实例</th><th>兜底模型 <code>fallback_model</code></th><th>说明</th></tr>
+    <tr><td>local-45278</td><td><code>axonhub/deepseek</code></td><td>默认免费模型 <code>flint-40b-free</code> 会 503,须显式指定</td></tr>
+    <tr><td>lan-248</td><td><code>axonhub/deepseek</code></td><td>模型池最全(168 个)</td></tr>
+    <tr><td>lan-204</td><td><code>axonhub/deepseek</code></td><td></td></tr>
+    <tr><td>lan-189</td><td><code>sensenova/deepseek-v4.1-flash</code></td><td>无 axonhub、无 etotem;取此前手工定到且能跑的模型(<code>axon/deepseek</code> 持续 429)</td></tr>
+    <tr><td>pub-iwintrue</td><td class="ok"><code>etotem/deepseek</code></td><td><b>用户指定</b>:该实例无 axonhub provider,用自家 etotem</td></tr>
+    <tr><td>prod-etotem</td><td><code>axonhub/deepseek</code></td><td><b>manual_only(用户定稿):只在明确下指令时才执行</b>,三条自动化路径无条件硬跳过(<code>--allow-prod</code> 也不补位);确需操作用 <code>hub.py dispatch --instance prod-etotem … --allow-prod</code></td></tr>
+  </table>
+</section>
+
+<section>
+  <h2>七、巡检与自愈(每 10 分钟)</h2>
+  <div class="card">
+    <div class="flow">
+      <div class="node"><b>常驻进程</b><code>loop_resume.py</code> 每 10 分钟</div>
+      <span class="arrow">→</span>
+      <div class="node"><b>① 额度自愈</b><code>fix_free_limit.py</code><br>仅 429/限流 → 切 <code>本实例兜底模型</code></div>
+      <span class="arrow">→</span>
+      <div class="node"><b>② 检查+恢复</b><code>check_agents.py</code><br>interrupted / failed / 半途停下 → 续做</div>
+      <span class="arrow">→</span>
+      <div class="node"><b>③ 并发位补齐</b><code>fill_slots.py</code><br>有空位就从任务池取任务派发</div>
+    </div>
+    <p style="margin:12px 0 4px;color:#646a73;font-size:13px">顺序有意义:先恢复已有会话占位,再补新任务,避免空位被新任务白占。</p>
+    <ul style="margin-top:10px">
+      <li><b>每个智能体保持 2 个 CFC 会话在跑</b>(并发上限),空闲位由 <code>tasks_backlog.json</code> 补位;任务池空了就停手</li>
+      <li>任务池按项目进度编排 12 项:T01 首页家庭任务数 / T02 删除空壳子系统 / T03 活动报名名册 / T04 活动海报重设计 / T05 限时特价 / T06 知识库统一 / T07 恢复单元测试 / T08 仓库卫生 / T09 审核中心统一详情 / T10 综合健康问卷 / T11 健康计划删除 / T12 活动签到审核页</li>
+      <li>派发即记录 <code>session_id</code>/<code>model</code>,同一任务不重复派发;会话成功结束后近似回填 <code>done</code></li>
+      <li>保护:只处理 CFC 会话;生产实例默认跳过;同会话冷却 20 分钟;只看最近 12 小时有活动的会话</li>
+      <li>日志:<code>out/resume_loop.log</code>(心跳)/ <code>free_limit_runs.log</code> / <code>agent_check.log</code> / <code>fill_slots.log</code></li>
+    </ul>
+  </div>
+</section>
+
+<section>
+  <h2>七·一、三层保活(防巡检挂掉)</h2>
+  <div class="card">
+    <div class="flow">
+      <div class="node"><b>L1 巡检本体</b><code>loop_resume.py</code><br>三段:额度自愈 / 检查恢复 / 补位</div>
+      <span class="arrow">←</span>
+      <div class="node"><b>L2 看门狗</b><code>watchdog.py --loop</code><br>每 5 分钟检查 L1</div>
+      <span class="arrow">←</span>
+      <div class="node"><b>L3 定时任务</b>WorkBuddy 每 10 分钟<br>检查 L2 是否在跑</div>
+    </div>
+    <ul style="margin-top:14px">
+      <li><b>活着</b> = pid 存在且命令行确是 <code>loop_resume.py</code>(防 pid 复用误判)<b>且</b> 心跳距今 ≤ 25 分钟(进程在但卡死也算挂)</li>
+      <li>重启用绝对路径解释器 + <code>DETACHED_PROCESS</code> 无窗口拉起,先杀旧进程</li>
+      <li><code>watchdog.py --dry-run</code> 看状态 / <code>watchdog.py</code> 修 / <code>--loop</code> 起常驻 / <code>--stop-loop</code> 停</li>
+      <li>多个看门狗并发检查用 <code>out/watchdog.lock</code> 原子锁(O_CREAT|O_EXCL + 300 秒残留抢占)互斥,不会同时拉起两个巡检</li>
+      <li>L3 已用 <code>Register-ScheduledTask</code> 注册成功(每 10 分钟,<code>LastTaskResult=0</code>);<b>不要用 <code>schtasks.exe</code></b>——它被 WorkBuddy 客户端沙箱的程序黑名单拦截(不是 Windows 安全中心)</li>
+    </ul>
+  </div>
+</section>
+
+<footer>生成时间 2026-10-23 | 完整 Markdown 版:<code>docs/superpowers/specs/2026-10-23-project-master-plan.md</code></footer>
+
+</div>
+</body>
+</html>

+ 234 - 0
docs/2026-10-25-后台与小程序UI审查报告.md

@@ -0,0 +1,234 @@
+# CFC 后台管理端 & 微信小程序 UI 审查报告
+
+> 审查日期:2026-10-25
+> 审查方式:代码勘探 + 设计系统落地一致性核查(非凭印象)
+> 审查范围:`cfc-web/`(Vue 2 + Element UI 2.15.13,100+ 视图)、`cfc-frontend/`(uni-app Vue 2,334 个 .vue 文件 + 51 组件)
+> ⚠️ 流程说明:已派发设计审查官做对抗式深挖,但其陷入逐页通读、52 分钟无产出后被终止。本报告结论**全部来自我直接跑的扫描命令**,每一条都带真实计数/路径/行号,可自行复核。
+
+---
+
+## 0. 一句话结论
+
+**两端设计系统都已经建好了,不需要推倒重来——真正的问题是"建了规范但 60%+ 的页面没在遵守"**,尤其是小程序端。优先级最高的三件事:**圆角令牌化、品牌色去硬编码、清理 AI 味文案**。
+
+- 后台 `cfc-web`:设计系统落地度高,整体克制、无 AI 味,**主要做令牌违规清理**
+- 小程序 `cfc-frontend`:设计系统落地度低,渐变泛滥、圆角十种值、品牌色写死、AI 味文案,**需要一轮系统性收敛**
+
+---
+
+## 1. 设计系统落地评分(基于实测数据)
+
+| 维度 | 后台 cfc-web | 小程序 cfc-frontend |
+|------|:---:|:---:|
+| 规范存在性 | ✅ `admin-tokens.css` 对齐 Ant Design 5 | ✅ `uni.scss` v3.2 品牌橙体系 |
+| 渐变克制度 | **98**(仅 2 文件用渐变) | **35**(228/334 文件用渐变,68%) |
+| 圆角一致性 | **70**(4/8/6px 合规,但混入 16/12/10/20/9/3px) | **30**(10 种不同值并存) |
+| 品牌色令牌化 | **75**(#1677FF 硬编码 15 处) | **45**(#F97316 硬编码 12 处等) |
+| AI 味文案 | **95**(0 处命中) | **40**("赋能"≥10 处、"开启XX之旅"多处) |
+| 工程体量健康 | 65(Login 953行 / ProductEdit 1478行) | **40**(16 个文件 >800 行,index-home 3057 行) |
+| 可访问性基础 | 60 | 55(空态覆盖 72%,小点击区 605 处) |
+| 单位纪律 | — | **95**(rpx 21005 次 / px 仅 65 次) |
+
+**综合:后台 82 / 小程序 55**
+
+---
+
+## 2. P0 — 立即修(本周,影响面最大且改动最小)
+
+### P0-1 圆角十种值收敛(小程序,最高优先级)
+
+实测 `grep -rhoE "border-radius: [0-9]+rpx" pages/`:
+
+```
+435  20rpx    364  16rpx    339  12rpx    173  8rpx
+152  24rpx    118  10rpx    110  40rpx     91  6rpx
+ 69  999rpx    54  44rpx
+```
+
+**问题**:`uni.scss` 里有圆角令牌,但页面里实际用了 10 种值。`40rpx` 和 `44rpx` 只差 4rpx 却并存,`6rpx` 与 `8rpx` 也是。视觉上是"看起来差不多但不一致"的典型 AI 生成/多人堆积产物。
+
+**建议**:收敛为 **4 档 + 全圆角**:`8rpx`(标签/小元素) → `16rpx`(按钮/输入框) → `24rpx`(卡片) → `999rpx`(胶囊/头像)。在 `uni.scss` 定义 `$radius-sm/-md/-lg/-full`,全局批量替换。后台同理(16/12/10/20/9/3px 越界值 → 2/4/6/8px)。
+
+### P0-2 品牌色硬编码清零(小程序)
+
+实测全库写死 hex 色值 Top:
+
+```
+12 次  #F97316   ← 品牌主色,本应是 $uni-primary
+ 6 次  #FFF7ED   ← 品牌淡底,本应是 $uni-primary-pale
+ 6 次  #EA580C   ← 品牌深橙,本应是 $uni-primary-dark
+ 5 次  #A8A29E
+ 4 次  #1C1917
+```
+
+**问题**:`uni.scss` 里这些值已经定义好了,但至少有 24 处直接写死了。**一旦要改品牌色,这些地方会成为漏改黑洞**。
+
+**建议**:全局搜索替换硬编码 → SCSS 变量。同时检查 `pages/index-home/index.vue` 里 splash 区用的 `rgba(255,140,66,0.3)` 等 5 个维度圆点颜色——这些也应该是维度令牌(`$dim-body/-mind/-wisdom/-action/-wealth`),不是散落的 rgba。
+
+### P0-3 AI 味文案清理(小程序)
+
+实测命中(`grep -rnoE "赋能|开启.*之旅|打造.*生态|一键.*轻松"`):
+
+| 文件 | 行 | 原文案 |
+|------|---|--------|
+| `pages/index-home/index.vue` | 309 | "开启全家健康之旅" |
+| `pages/home-pages/parent-index.vue` | 27 | "开启五维能量之旅" |
+| `pages/health-main/index.vue` | 211 | "开启健康之旅" |
+| `pages/health/doa-index.vue` | 47 | "开启 DOA 之旅" |
+| `pages/index-home/index.vue` / `health-main/index.vue` / `growth-main/index.vue` / `family/self-check.vue` / `family/self-check-ai.vue` / `wisdom-detail/index.vue` | 各 1 | "赋能" |
+
+**问题**:「赋能」「开启XX之旅」是 AI 生成文案的强信号,也是用户最容易感知的"假大空"。而且**五维是产品的核心叙事**,把它写成"开启五维能量之旅"反而稀释了辨识度。
+
+**建议**:
+- "赋能" → 删掉或换成具体动作("帮您梳理""陪您一起")
+- "开启XX之旅" → 换成具体承诺("今天记录第一次""给自己做个小体检")
+- 这条也符合公众号内容规范里已定的"禁止意义拔高/官腔套话"口径
+
+---
+
+## 3. P1 — 本迭代修
+
+### P1-1 后台品牌蓝去硬编码
+
+`grep -rho "#1677FF" cfc-web/src/views/` = **15 处**。`admin-tokens.css` 里 `--adm-primary: #1677FF` 已定义,页面里不该再写死。全部替换为 `var(--adm-primary)`。
+
+### P1-2 后台非标圆角
+
+实测 `border-radius` 分布:
+```
+合规:4px×77 / 8px×19 / 6px×13
+越界:16px×11 / 12px×9 / 10px×8 / 20px×3 / 9px×1 / 3px×4
+```
+
+16px 出现 11 次是最严重的(规范上限是 8px)。全部收敛到 `--adm-radius-*`。
+
+### P1-3 小程序空状态 / 加载态补齐
+
+实测含 `empty|暂无|加载中|loading` 的页面 **241/334 = 72%**,即 **约 93 个页面缺空态或加载态**。用户拉到空列表只会看到一片白屏。
+
+已有 `components/BaseEmpty.vue` 和 `BaseLoading.vue`,但没被普遍使用。建议做一轮"空态普查":列出这 93 个页面,每个补一个空态。
+
+### P1-4 技术化错误提示改人话
+
+实测 `err.message / e.message / err.msg` 直显 **52 处**。用户看到的是 `Request failed with status code 403` 而不是"您没有权限,请联系管理员"。
+
+建议抽一个统一错误映射层(按 HTTP 码 / 业务码 → 中文文案),页面不再直接抛原始 message。
+
+---
+
+## 4. P2 — 下迭代修(工程量较大)
+
+### P2-1 巨型页面拆分
+
+**小程序 16 个文件 >800 行**,最重的:
+
+| 文件 | 行数 |
+|------|------|
+| `pages/index-home/index.vue` | **3057** |
+| `pages/mind-detail/index.vue` | 2600 |
+| `pages/home-pages/parent-index.vue` | 2293 |
+| `pages/health/report-detail.vue` | 2249 |
+| `pages/shop/checkout/checkout.vue` | 2020 |
+| `pages/health/health-plan-summary.vue` | 1912 |
+| `pages/health/report-confirm.vue` | 1840 |
+| `pages/activity/activity-detail/activity-detail.vue` | 1693 |
+| `pages/health/gut-flora-detail.vue` | 1503 |
+| `pages/home-pages/child-index.vue` | 1485 |
+
+**后台**:`ProductEdit.vue` 1478、`Dashboard.vue` 1080、`Layout.vue` 1057、`ArticleEdit.vue` 1026、`Login.vue` 953。
+
+3000 行的单文件已经无法 review、无法复用、改一处影响全局。优先拆 `index-home`(首屏体验 + 已内嵌 splash/登录浮层/新手引导/自测卡等一堆模块)。
+
+### P2-2 小点击区域
+
+实测 `(width|height): 4x-7xrpx` 命中 **605 处**。微信小程序 `80rpx ≈ 40px` 是最低可点击尺寸,其中大量 40-79rpx 的元素偏小,中老年用户(本产品核心受众之一:家长)容易误触。
+
+建议:按钮/图标按钮/标签至少 `min-height: 80rpx`,间距足够时不压缩。
+
+### P2-3 图标系统统一
+
+`pages/index-home/index.vue` splash 区用 emoji 当图标(🌏🔥⚡🌿💧),emoji 在不同机型/系统渲染不一致,且无法控制配色。**建议统一图标系统**(iconfont 或内联 SVG),特别是五维图标——这是品牌识别元素,不该交给系统 emoji。
+
+### P2-4 品牌中英文口径
+
+实测首页 splash 同时出现:`浠艾福`(中文名)+ `Care Family`(英文名);其他页面还有 `CARE FAMILY` 全大写。需确定**单一英文口径**(建议 `Care Family`,首字母大写驼峰式)。
+
+---
+
+## 5. P3 — 长期 / 架构级
+
+### P3-1 跨端品牌色决策(需要产品拍板)
+
+后台用 **Ant 蓝 `#1677FF`**,小程序用 **品牌橙 `#F97316`**。
+
+**判断**:这个割裂**在技术上是合理的**——后台是内部工具(克制、专业、Ant 蓝符合企业级预期,AI 味文案 0 处也说明定位正确);小程序是 C 端家庭用户产品(需要温暖、品牌感)。**但需要在文档里明确这是有意决策,不是遗留问题**,否则后续开发者会"顺手统一"。
+
+**建议**:在 `AGENTS.md` 或设计文档里加一节「跨端色彩策略」:后台 = 功能色(Ant 蓝,克制);小程序 = 品牌色(橙,温暖);两者不统一是刻意的。
+
+### P3-2 设计令牌覆盖率
+
+小程序 334 个 .vue 文件中 **138 个(41%)** 引用了 `$uni-` 或 `@import uni.scss`。剩下 59% 是"能拿到变量但没在用"。目标应做到 ≥85%。
+
+### P3-3 组件复用率
+
+51 个组件已有(PlayfulButton / PlayfulCard / BaseEmpty / BaseLoading / RadarChart / TrendChart 等),`PlayfulButton` 被 5 个页面复用。**组件层健康**,不需要推倒,只需把硬编码处逐步迁移过来。
+
+---
+
+## 6. 已经做得好的(避免误伤)
+
+审查不是只挑毛病。以下部分明显成熟,**不要动**:
+
+- ✅ **后台整体克制**:渐变仅 2 个文件、AI 味文案 0 处、`admin-tokens.css` 有完整的弹性填充链和 `:has()` 滚动修复(注释明确"禁止改动"),是有工程素养的团队产物
+- ✅ **小程序单位纪律**:rpx 21005 次 / px 仅 65 次,几乎全用 rpx,响应式正确
+- ✅ **静态资源克制**:`static/` 仅 178K、33 张图,没有包体积风险
+- ✅ **设计系统注释质量高**:`uni.scss` 有 v3.1/v3.2 演进记录、色板档位约定(tint/light/base/strong)、文件复用计数("33 文件")——这是被认真维护过的
+- ✅ **登录页极简**:`pages/login/login.vue` 只有"微信一键登录 / 进入小程序首页",不啰嗦
+- ✅ **组件命名规范**:51 个组件语义清晰(BaseEmpty/BaseLoading/OnboardingProgressBar)
+
+---
+
+## 7. 改进路线图
+
+| 档位 | 事项 | 涉及文件 | 工作量 |
+|------|------|----------|--------|
+| **立即** | 品牌色硬编码清零 | 小程序全库 24+ 处 | 半天 |
+| **立即** | AI 味文案清理 | 6 个页面 10+ 处 | 半天 |
+| **立即** | 圆角收敛为 4 档 | 小程序全库 + 后台全库 | 1 天 |
+| 本迭代 | 后台 #1677FF 去硬编码 | 15 处 | 2 小时 |
+| 本迭代 | 后台非标圆角收敛 | 16px×11 等 30+ 处 | 半天 |
+| 本迭代 | 小程序空态补齐 | 93 个页面 | 3-5 天 |
+| 本迭代 | 错误提示人话化 | 52 处 | 2 天 |
+| 下迭代 | 巨型页面拆分 | 16 个小程序页 + 5 个后台页 | 1-2 周 |
+| 下迭代 | 小点击区修正 | 605 处 | 3 天 |
+| 下迭代 | 图标系统统一 | splash 区 + 五维图标 | 2 天 |
+| 长期 | 跨端色彩策略文档化 | AGENTS.md | 1 小时 |
+| 长期 | 令牌覆盖率 41% → 85% | 全库 | 持续 |
+
+**总估算**:P0 约 2 天、P1 约 1 周、P2 约 2 周。
+
+---
+
+## 8. 复核方式
+
+本报告所有数字可用以下命令自查(工作目录 `E:/cfc`):
+
+```bash
+# 小程序圆角种类
+grep -rhoE "border-radius: [0-9]+rpx" cfc-frontend/pages/ | sort | uniq -c | sort -rn
+
+# 小程序渐变文件数
+grep -rlE "linear-gradient|radial-gradient" cfc-frontend/pages/ cfc-frontend/components/ | wc -l
+
+# 小程序品牌色硬编码
+grep -rho "#F97316" cfc-frontend/pages/ | wc -l
+
+# AI 味文案
+grep -rnoE "赋能|开启.*之旅|打造.*生态|一键.*轻松" cfc-frontend/pages/ cfc-frontend/components/
+
+# 后台非标圆角
+grep -rhoE "border-radius: (1[0-9]|20|[3-9])[0-9]?px" cfc-web/src/views/ | sort | uniq -c
+
+# 巨型页面
+find cfc-frontend/pages -name "*.vue" -exec wc -l {} + | sort -rn | head -16
+```

+ 362 - 0
docs/2026-10-25-字体边距间距专项审查.md

@@ -0,0 +1,362 @@
+# CFC 字体 / 边距 / 间距专项审查(补充报告)
+
+> 审查日期:2026-10-25
+> 配套报告:`docs/2026-10-25-后台与小程序UI审查报告.md`
+> 方法:量化扫描定面 → 定向抽样核验 → **自我修正**
+> 声明:审查官两次派发均跑空(52min / 71min,无正文回传,subagent 失联),本报告全部数据由主理人实跑命令得出,附自查命令。
+
+---
+
+## 0. 一句话结论 + 一个重要修正
+
+**结论:字体比颜色问题轻,间距比字体问题重。**
+- 后台:间距令牌引用率仅 **21.9%**,且明文废止的 20px 仍有 **141 处**
+- 小程序:**uni.scss 里根本没有 spacing 令牌**,间距 30+ 种裸值,30rpx 一个值就 492 处
+- 字体最该修的是**行高体系混用**和**后台 13px 废止值残留**,不是字号档次
+
+### ⚠️ 推翻我上一轮的一条初步判断
+
+我最初假设「`line-height: 1` 出现 75 处 = 中文正文行高压扁,是严重问题」。**核验后不成立。**
+
+用脚本按 CSS 选择器归类这 75 处(见文末自查命令),结果是:
+
+| 选择器类别 | 处数 | 判定 |
+|---|---|---|
+| 关闭/返回/加号图标(`modal-close`×3、`nav-icon`×3、`nav-add-icon`、`ac-nav-add`、`ac-fab-icon`、`fab-icon`、`nav-back-icon`、`nav-right-icon`、`picker-close`) | ~15 | ✅ `line-height:1` 是**正确做法**(字形垂直居中) |
+| 数字(`score-num`×3、`score-value`×2、`dim-item-score`×2、`explain-score`、`dd-score-value`、`index-value`、`hero-score-num`、`tab-item__count`) | ~14 | ✅ 数字行高 1 正常 |
+| emoji / 符号(`star`、`hero-symbol`、`symbol-big`、`symbol-opt`、`task-icon__emoji`、`wish-card__emoji`、`typing-dot`、`hero-moon`、`upload-icon`×2) | ~12 | ✅ emoji 行高 1 是**垂直对齐必需** |
+| 文本标签 | 仅 3 | ⚠️ `.splash-dim-label`×2、`.status-badge__text`×1 需人工目检 |
+
+**真正的问题不是 `line-height: 1`,而是倍数制与 rpx 绝对值制并存**(见 §2.4)。
+
+---
+
+## 1. 小程序字体(cfc-frontend)
+
+### 1.1 字号:26+ 种实际值 vs 7 档令牌
+
+令牌基线 `uni.scss:112-117`:`20 / 24 / 28 / 32 / 36 / 44 / 52 rpx`
+
+```
+令牌内:28rpx×382, 24rpx×258, 32rpx×206, 20rpx×123, 36rpx×100, 44rpx×20, 52rpx×4   = 1093
+令牌外:26rpx×197, 22rpx×195, 30rpx×104, 80rpx×81, 40rpx×67, 48rpx×58, 18rpx×27,
+        34rpx×21, 100rpx×21, 72rpx×11, 64rpx×11, 60rpx×11, 56rpx×8, 88rpx×7,
+        42rpx×3, 96rpx×2, 120rpx×2, 23rpx×2, 16rpx×2, 14rpx×2, 13rpx×1 = ~1010
+```
+
+**两个结构性问题:**
+1. **「凑视觉」值扎堆**:26rpx / 22rpx / 30rpx 合计 **496 处**,分别夹在 24/28 之间、20/24 之间、28/32 之间——只差 2rpx,是逐页微调堆积的痕迹
+2. **大字号段完全无令牌**:40~120rpx 之间有 **13 种裸值**(40/42/48/52/56/60/64/72/80/88/96/100/120),标题、数字、大按钮全在裸写
+
+### 1.2 ⭐ 字体缩放机制:96% 页面覆盖,但逐条混用(本轮最高价值发现)
+
+`utils/fontScale.js` 机制本身质量很高:读微信 `fontSizeScaleFactor` / `fontSizeSetting`(降级除以 16px 基准)、钳制 `[0.9, 1.7]`、用户手动设置优先于系统值、storage 持久化。
+
+**覆盖率(核验数据):**
+
+| 指标 | 数值 | 评价 |
+|---|---|---|
+| 页面注入 `--font-scale` 根节点 | **332 / 334** | 96.4%,优秀 |
+| 使用 `calc(var(--font-scale))` 的文件 | **320** | 优秀 |
+| `.text-xs/sm/base/lg/xl/2xl/3xl` 语义类引用 | **0 次** | 定义了但没人用 |
+
+**但缺口在"条级":** 有 **327 个文件同时存在 calc() 和裸写 font-size**,裸写部分合计 **1826 处不随缩放变化**。
+
+实证 `pages/diet/records.vue`:
+```css
+:2   <view class="page" :style="{ '--font-scale': fontScale }">   /* 根注入 ✓ */
+:426 .card-title { font-size: calc(28rpx * var(--font-scale, 1)); } /* ✓ 会缩放 */
+:410 .week-day-label { font-size: 20rpx; }                          /* ✗ 裸写,不缩放 */
+:411 .week-day-num { font-size: 28rpx; }                            /* ✗ */
+:431 .stat-val { font-size: 36rpx; font-weight: 700; }             /* ✗ */
+```
+
+**用户影响**:把微信字号调到"大"的家长用户(本产品核心受众之一),在同一页面里**一半文字变大、一半不变**——比"全都不变"更令人困惑,因为会造成明显的版面失衡。
+
+裸写最集中的文件(优先修):`activity-detail.vue`(47)、`product-detail.vue`(37)、`diet/records.vue`(30)、`article-detail.vue`(26)、`discover/index.vue`(25)、`mind-detail/index.vue`(24)
+
+### 1.3 字重:令牌 4 档,实际 7 种
+
+```
+600×545   500×330   700×183   800×19   400×9   300×9   900×3
+```
+
+- **800×19 + 900×3**:超出令牌(uni.scss:121-124 只到 700)。中文系统字体(苹方/黑体)在 20-30rpx 小字号下 800/900 会糊字——**需真机目检确认**
+- **300×9**:偏轻,中老年用户弱视场景可读性差
+
+### 1.4 行高:体系混用(真正的问题)
+
+```
+倍数制:1.6×114, 1.5×106, 1.4×74, 1.7×24, 1.8×18, 1.3×11, 1.2×11, 1.1×7, 1.9×1, 1×75
+rpx 绝对值:88rpx×45, 80rpx×29, 72rpx×26, 96rpx×10, 64rpx×10, 48rpx×9, 36rpx×9   ≈ 138
+```
+
+**核心矛盾**:
+- 倍数制行高**跟随 font-size 缩放** → 与 font-scale 机制协同 ✓
+- rpx 绝对值行高**固定不动** → 字号变大但行高不变,行间距相对压缩 ✗
+
+`line-height: 1` 的 75 处已核验为图标/数字/emoji(§0),**不需要改**。
+需改的是 **~138 处 rpx 绝对值行高**——但其中相当部分是按钮/固定高度容器(88rpx 常配按钮高度),需区分"行高"与"高度"用法。
+
+### 1.5 字体族:不建议改
+
+`font-family` 全库仅 3 处(`monospace`×2、`inherit`×1),完全依赖微信系统字体。
+
+**这是正确做法**:系统字体渲染最优、无 webfont 加载闪烁、无包体积成本、支持系统字号。**建议不要引入自定义字体族**,但应在文档里写明这是"刻意决策",避免后人"顺手加个思源黑体"。
+
+---
+
+## 2. 小程序间距(cfc-frontend)
+
+### 2.1 ⭐ uni.scss 里根本没有 spacing 令牌
+
+核验 `uni.scss` 全文:**只有 font-size(112-117)、font-weight(121-124)、radius(143-148)**,**无任何 `$spacing-*` / `$space-*` 系列定义**。所以页面间距全是裸值——这不是"没遵守令牌",是"根本没有令牌可遵守"。
+
+### 2.2 实际 30+ 种间距值
+
+```
+ 1206  20rpx      993  16rpx      735  24rpx      667  12rpx      645  8rpx
+  492  30rpx ←    384  4rpx      324  10rpx ←    305  40rpx      226  6rpx ←
+  139  28rpx     110  60rpx     110  2rpx ←     107  32rpx       72  14rpx ←
+   68 120rpx      48  80rpx      39  18rpx ←     39 100rpx       30  48rpx
+   27 200rpx      17  15rpx ←    16  22rpx ←     13  50rpx       11  36rpx
+   11 160rpx      10  26rpx ←     6   5rpx ←      5  88rpx       4   1rpx ←
+```
+
+**违反 4rpx 基准的合计 1307 处**(← 标记)。两个最刺眼的:
+- **30rpx × 492** —— 单项最大,且既非 4 的倍数也非 8 的倍数(30 = 6×5)
+- **10rpx × 324** —— 典型非基准值
+
+`margin-bottom: 30rpx`(155 次)+ `padding: 30rpx`(262 次)合计 417 次,是 30rpx 的两大来源。
+
+### 2.3 圆角:令牌 6 档,实际 10 种值(与上轮报告一致,补充令牌对照)
+
+令牌 `uni.scss:143-148`:`sm=10rpx / md=16rpx / lg=24rpx / xl=32rpx / full=50% / round=9999rpx`
+
+```
+令牌内:16rpx×364, 24rpx×152, 10rpx×118, 32rpx×107(少量), 999rpx×69(≈round)
+令牌外:20rpx×435, 12rpx×339, 8rpx×173, 40rpx×110, 6rpx×91, 44rpx×54
+```
+
+**20rpx×435 是最大 offender**——它不在令牌里,却比令牌里的 16rpx 还常用。
+
+### 2.4 gap 使用率仅 15.5%
+
+`gap` 661 处 vs `margin-*` **3603 处**。布局靠 margin 逐项堆叠 → 间距改一处塌一片、无法统一调整组内距、容易漏项。
+
+---
+
+## 3. 后台字体(cfc-web)
+
+### 3.1 字号:19 种实际值 vs 3 档令牌
+
+令牌 `admin-tokens.css:51-53`:仅 `12px(--font-size-mini) / 14px(--font-size-base) / 16px`
+
+```
+令牌内:12px×119, 14px×43, 16px×30 = 192 处(54%)
+令牌外:13px×63, 20px×18, 11px×14, 32px×13, 15px×11, 28px×10, 24px×9,
+        18px×7, 48px×5, 22px×5, 10px×3, 40px×2, 17px×1, 9px×1, 64px×1 = 164 处(46%)
+```
+
+**最该修的:`13px` × 63 处,分布在 28 个文件。**
+`admin-tokens.css:52` 明文写着 `--font-size-small: 14px; /* 原 13px → 规范正文 14 */`——**13px 是被明文废止的值,却仍有 63 处残留。**
+
+已核实的 13px 使用位置:
+
+| 文件:行 | 选择器 | 内容性质 | 判定 |
+|---|---|---|---|
+| `admin/CommentReview.vue:152` | `.article-title` | **文章标题** | ❌ 标题 13px 明显偏小 |
+| `admin/Dashboard.vue:447` | `.hot-title` | 热点榜标题 | ❌ 标题偏小 |
+| `admin/Dashboard.vue:429` | `.revenue-sub` | 金额副标 | ⚠️ 辅助文字,13px 尚可但应改 12px |
+| `admin/Dashboard.vue:448` | `.hot-value` | 热点数值 | ⚠️ 同上 |
+| `admin/Activities.vue:566` | `.roster-list-title` | 名单区标题 | ❌ 标题偏小 |
+| `admin/Activities.vue:570` | `.roster-name` | 人名(正文级) | ❌ 正文应为 14px |
+| `admin/HealthEnergyConfig.vue:221` | `.config-ul` | 配置说明列表 | ⚠️ 说明文字 |
+
+另有 **`10px`×3、`9px`×1** 属不可读区间(中文低于 10px 字形粘连),需直接排查。
+
+### 3.2 字重:基本健康,但层级偏重
+
+```
+600×60   700×27   500×19   400×2   300×2
+```
+
+`font-weight: 500`(19 处)远少于 600(60 处),说明团队习惯性"加粗到 600",导致**页面标题/次级标题/正文强调三个层级挤在 600 上**,靠字号区分而非字重。建议补用 500 做次级强调。
+
+### 3.3 行高:健康
+
+倍数制为主(`1.8`×7、`1.6`×6、`1.5`×6、`1.4`×5、`1.2`×5),少量绝对值(`20px`×2、`80px`、`60px`、`44px`,属固定高度控件)。基线 1.5 与 tokens 一致。**无需专项整改**,仅需零星统一。
+
+---
+
+## 4. 后台间距(cfc-web)
+
+### 4.1 ⭐ 令牌引用率仅 21.9%
+
+`admin-tokens.css:56-63` 定义了 8 档 spacing:`4 / 8 / 12 / 16 / 24 / 24 / 32 / 48 px`
+
+```
+var(--  引用次数:   723
+裸写 px 次数:     2585
+令牌引用率:     21.9%
+```
+
+**与小程序对比更刺眼**:小程序至少没有 spacing 令牌所以裸写有理由,后台**令牌齐全却只用两成**。
+
+### 4.2 非 8px 基准间距值
+
+```
+ 141  20px ← 最大 offender(tokens:60 明文「原 20px → 规范 24」即已废止)
+  77  10px ←
+  36   6px ←
+  23   2px
+  23  15px ←
+   6  40px(应改 48px 或 32px)
+   4   1px(分割线,可接受)
+   4  14px ←
+   3  30px ←
+   2   5px ←
+   2   3px
+   2  36px ←(应改 32 或 48)
+   1  80px / 60px / 44px / 42px / 28px / 22px / 18px
+```
+
+**20px × 141** 与 **10px × 77** 是前两大 offender,合计 218 处。12px×145(令牌内 `--spacing-md`)与 20px×141 几乎并列第一——说明团队实际在用 20px 而非令牌定义的 24px。
+
+### 4.3 gap 使用率 22%
+
+`gap` 141 vs `margin-*` 493。后台比小程序好一点,但仍有大量 margin 堆叠。
+
+---
+
+## 5. 收敛方案
+
+### 5.1 小程序:新增 spacing 令牌(`uni.scss` 追加)
+
+```scss
+// 间距令牌 —— 4rpx 基准,10 档
+$uni-space-2xs: 4rpx;    /* 图标与文字微距 */
+$uni-space-xs:  8rpx;    /* 标签内边距、紧凑间距 */
+$uni-space-sm: 12rpx;    /* 列表项内距、次级间距 */
+$uni-space-md: 16rpx;    /* 表单项间距、卡片内边距基准 */
+$uni-space-lg: 24rpx;    /* 卡片内边距、区块间距 */
+$uni-space-xl: 32rpx;    /* 区块间距 */
+$uni-space-2xl: 40rpx;   /* 页面区块分隔 */
+$uni-space-3xl: 60rpx;   /* 大留白 */
+$uni-space-4xl: 80rpx;   /* 空状态留白 */
+$uni-space-page: 30rpx;  /* 页面左右安全边距(保留现状,兼容存量) */
+```
+
+**字号令牌扩档**(补 4 档覆盖大字号段):
+
+```scss
+$uni-font-size-2xl: 44rpx;   /* 已有 */
+$uni-font-size-3xl: 52rpx;   /* 已有 */
+$uni-font-size-4xl: 64rpx;   /* 新增:大数字、统计值 */
+$uni-font-size-5xl: 80rpx;   /* 新增:Hero 数字、封面主标 */
+$uni-font-size-6xl: 100rpx;  /* 新增:极值展示 */
+```
+
+**归并规则**:`22→20, 26→24, 30→28, 34→32, 42→40, 56→48, 60→64, 88→80, 96→100, 13→12, 16→20, 23→24`
+
+**行高令牌**:
+
+```scss
+$uni-lh-tight: 1.25;  /* 标题、标签 */
+$uni-lh-base:  1.5;   /* 正文 */
+$uni-lh-loose: 1.75;  /* 长文、阅读 */
+```
+
+### 5.2 后台:替换规则表
+
+| 现状值 | 处数 | 替换为 | 令牌 |
+|---|---|---|---|
+| `20px` | 141 | `24px` | `var(--spacing-lg)` |
+| `10px` | 77 | `8px` | `var(--spacing-sm)` |
+| `13px` | 63 | `14px` | `var(--font-size-base)`(标题类)/ `12px`(辅助类) |
+| `6px` | 36 | `8px` | `var(--spacing-sm)` |
+| `15px` | 23 | `16px` | `var(--spacing-base)` |
+| `14px` | 4 | `12px` | `var(--spacing-md)` |
+| `11px` | 14 | `12px` | `var(--font-size-mini)` |
+| `15px` | 11 | `14px` | `var(--font-size-base)` |
+| `10px` / `9px` | 4 | `12px` | 最低可读阈值 |
+| `40px` | 6 | `48px` | `var(--spacing-3xl)` |
+
+### 5.3 迁移步骤(两端通用)
+
+1. **加令牌** → 2. **lint 拦截** → 3. **自动替换** → 4. **目检回归**
+
+第 2 步是关键——**不加 stylelint 规则,替换完还会再长出来**。建议规则:
+```yaml
+# .stylelintrc
+font-size-notation: "rpx"                    # 小程序
+declaration-property-value-allowed-list:
+  "font-size": ["/^[0-9]+rpx$/", "/calc\\(/"]
+  "margin*": ["/^(0|4|8|12|16|24|28|30|32|40|48|60|80|120)rpx$/"]
+```
+
+### 5.4 工作量与风险
+
+| 项 | 处数 | 工作量 | 风险 |
+|---|---|---|---|
+| 后台 20px→24px | 141 | 2h | **中**:卡片内边距变宽,表格可能换行,需逐页目检 |
+| 后台 13px→14/12px | 63 | 3h | **低-中**:标题变大会挤行,Dashboard 热点榜需目检 |
+| 小程序 30rpx 归并 | 492 | 4h | **中**:这是页面安全边距主值,改动面大,建议保留 `$uni-space-page` 不动,只改内部间距 |
+| 小程序 font-scale 补漏 | 1826 | 1-2 天 | **低**:纯增量包裹 calc(),不改变默认字号(scale=1 时值不变) |
+| 小程序 spacing 令牌新建 | — | 1h | 无 |
+| 字号/行高/字重归并 | ~700 | 1 天 | **中**:字号变化需真机目检(尤其 iOS/Android 字体差异) |
+| 800/900 字重降档 | 22 | 1h | **低**:视觉重量下降,需确认标题仍够突出 |
+
+**总估算**:P0 约 1.5 天,P1 约 3 天,P2 约 2 天。
+
+---
+
+## 6. 不需要改的(避免误伤)
+
+- ✅ **`line-height: 1` 的 75 处** —— 已核验为图标/emoji/数字,是正确实践
+- ✅ **小程序不设 font-family** —— 依赖微信系统字体是正确做法,应文档化"刻意决策"
+- ✅ **font-scale 机制本身** —— 实现质量高(系统值降级链、[0.9,1.7] 钳制、用户手动优先、storage 持久化),332/334 页面覆盖,**不要推翻重建**,只需补齐裸写部分
+- ✅ **后台行高体系** —— 倍数制为主,基线 1.5 与 tokens 一致
+- ✅ **后台字重** —— 只有 500 使用偏少,无越界值
+- ✅ **`.text-*` 语义类(uni.scss:232-238)已定义** —— 0 引用的问题不在类本身,而在团队习惯用 calc();**不建议废弃**,可作为长文章节标题的规范写法推广
+
+---
+
+## 7. 自查命令
+
+工作目录 `E:/cfc`
+
+```bash
+# 小程序:字号令牌内外
+grep -rhoE "font-size: [0-9]+rpx" cfc-frontend/pages/ cfc-frontend/components/ \
+  | grep -vE "font-size: (20|24|28|32|36|44|52)rpx" | sort | uniq -c | sort -rn
+
+# 小程序:font-scale 机制覆盖率
+grep -rl "font-scale.*fontScale\|'--font-scale'" cfc-frontend/pages/ | wc -l          # 注入根节点
+grep -rl "calc.*var(--font-scale" cfc-frontend/pages/ | wc -l                          # 使用 calc
+grep -rhoE "font-size: *[0-9]+rpx" cfc-frontend/pages/ cfc-frontend/components/ | wc -l  # 裸写总数
+
+# 小程序:行高 1 的选择器归类(判定 icon/数字 vs 正文)
+grep -rlE "line-height: *1[ ;}]" cfc-frontend/pages/ cfc-frontend/components/
+
+# 小程序:间距违规(非 4rpx 倍数)
+grep -rhoE "(padding|margin)(-[a-z]+)? *: *[0-9]+rpx" cfc-frontend/pages/ \
+  | grep -oE "[0-9]+rpx" | grep -vE "^(4|8|12|16|20|24|28|32|40|48|60|80|120)rpx$"
+
+# 小程序:gap 使用率
+grep -rhoE "gap: *[0-9]+rpx" cfc-frontend/pages/ | wc -l
+grep -rhoE "margin-[a-z]+: *[0-9]+rpx" cfc-frontend/pages/ | wc -l
+
+# 后台:令牌引用率
+grep -rhoE "var\(--" cfc-web/src/views/ | wc -l
+grep -rhoE "[0-9]+px" cfc-web/src/views/ | wc -l
+
+# 后台:废止值残留
+grep -rn "font-size: 13px" cfc-web/src/views/ | wc -l   # 应归零
+grep -rn "20px" cfc-web/src/views/ | wc -l               # 应大幅减少
+
+# 后台:字号令牌内外
+grep -rhoE "font-size: *(12|14|16)px" cfc-web/src/views/ cfc-web/src/components/ | wc -l
+grep -rhoE "font-size: *[0-9]+px" cfc-web/src/views/ cfc-web/src/components/ \
+  | grep -vE "font-size: *(12|14|16)px" | wc -l
+```

+ 169 - 0
docs/superpowers/plans/2026-10-24-cfc-full-review-action-plan.md

@@ -0,0 +1,169 @@
+# CFC 全项目审查 · 行动方案与派发清单
+
+> 生成日期:2026-10-24
+> 依据:三份只读审查报告(arch-review / frontend-review / ops-review)+ 用户 12 项决策答复
+> 状态:待用户确认派发批次
+> 关联:`E:/cfc/docs/参考资料/`(历史复盘)、项目记忆 `E:/cfc/.workbuddy/memory/2026-10-24.md`
+
+---
+
+## 一、12 项决策答复 · 落点记录
+
+| # | 原问题 | 用户答复 | 落点 |
+|---|--------|----------|------|
+| Q1 | 生产是否启用 Redis | **有 Redis** | ✅ 缓存方案可行(@Cacheable / 分布式锁均可落地) |
+| Q2 | sfms 多数据源生产是否启用 | **不启用** | 保持现状不动,代码不再推进 |
+| Q3 | Prometheus monitoring profile 是否启用 | **不启用** | 监控建议降级为日志级优化,不引入新埋点 |
+| Q4 | Actuator 是否启用 | **不启用** | 同上 |
+| Q5 | Prompt 是否支持热更新 | **要热更新** | ✅ 列入派发清单(开发任务) |
+| Q6 | /api/growth/** 排除 JWT 是否遗留 | **可纳入 JWT** | ✅ 列入派发清单(审计+纳入认证) |
+| Q7 | "富=成就"是术语还是 UI 标签 | **UI 标签,保留 wealth 页内容** | ✅ 财富文案不再全局替换;wealth 页内容保留 |
+| Q8 | 三块 wealth 页面产品定位 | **列出来再分析** | 👇 见本文件第二节(盘点+分析+建议) |
+| Q9 | 订阅消息模板申请情况 | **未申请** | ⏸ 先走运营申请流程,前端接入排后 |
+| Q10 | new Date() 无参形式合规性 | **我来判断** | ✅ 结论:无参可保留;仅修变量形式(见第三节) |
+| Q11 | 生产库准确信息在哪维护 | **生产环境单独配置,不在代码里** | ✅ 配置外置化列入派发清单(密钥值暂不轮换) |
+| Q12 | cfc-backend 生产是否用 Docker | **不用 Docker 部署** | ✅ 部署文档修订 + compose 中后端引用清理 |
+
+---
+
+## 二、Q8 · wealth 页面盘点与定位分析
+
+### 2.1 页面清单(4 块目录 / 7 个页面 / 合计 4321 行)
+
+| 目录 | 页面 | 行数 | pages.json 标题 | 核心功能 | 承载业务线 |
+|------|------|------|-----------------|----------|-----------|
+| `pages/wealth/` | index.vue | 1432 | 富 | 五维「富」维度主页面:财富能量环、子维度评分(skill/altruism/income/fx)、财富成长水位、强项与提升、未登录游客卡(推广收益/邀请返利预览)+ 商城精选 | **五维能量体系** |
+| `pages/wealth/` components | BodyWealthAlert / WealthEnergyRing / WealthSubScores / DimensionSubDims | — | — | 维度能量可视化组件 | 五维能量体系 |
+| `pages/wealth-detail/` | member-wealth-detail.vue | 267 | 财富能量详情 | 单成员财富能量明细(从维度页进入) | 五维能量体系 |
+| `pages/wealth-cf/` | index.vue | 494 | 家庭CF值 | **平台积分体系**:余额/累计/已消费/流水/兑换优惠券(1元=1积分) | **商城积分** |
+| `pages/wealth-sub/` | checkin.vue | 570 | 财商打卡 | 日历打卡(每次+5 财富能量)、连续天数、储蓄/规划统计 | **财商养成** |
+| `pages/wealth-sub/` | insurance-list.vue | 300 | 保单管理 | 家庭保单列表 | **保险业务** |
+| `pages/wealth-sub/` | insurance-add.vue | 654 | 新增保单 | 保单 PDF 上传 → AI 解析条款(保险公司/保额/保费/摘要)→ 表单确认 | **保险业务** |
+| `pages/wealth-sub/` | insurance-planning.vue | 604 | (空标题) | AI 家庭保险规划评分、保障缺口分析、按缺口添加保单 | **保险业务** |
+
+### 2.2 定位分析:名义三块、实为三条独立业务线
+
+1. **wealth + wealth-detail(五维能量)**:属五维「富」维度(身/智/富/行/心),定位是**能量与成长导向**,但文案使用"财富能量/财富成长/利他创富"字眼。按 Q7 口径,财富仅作 UI 标签保留。
+   - ⚠️ **合规提示(不改,仅备忘)**:未登录游客卡含「邀请返利」——属一级推荐返利表述,与涉传红线(多级返利/拉人头/收益承诺)不同,但仍是敏感表述,建议与 promotion 页口径一致化(`promotion/invite` 已存在),避免 AI 爬虫/审核误读。
+
+2. **wealth-cf(家庭CF值)**:本质是**商城积分体系**(消费返积分、兑换优惠券),与五维维度无关,被放在 wealth 目录下是命名巧合。
+
+3. **wealth-sub(财商打卡 + 保单 + 保险规划)**:本质是**保险/财商业务线**,含 AI 保单解析能力(PDF 上传 → 条款抽取),是独立业务子模块。
+
+### 2.3 用户决策:合并执行(已确认)
+
+> 用户 2026-10-24 确认:**将 wealth-cf、wealth-sub 合并进 wealth 模块**(D-7 升级为批 E 执行项)。
+
+**引用面已实测(仅以下范围,无后端/web 端引用)**
+
+| 位置 | 内容 |
+|------|------|
+| `pages.json:894` | subPackage `root=pages/wealth-sub`(4 页) |
+| `pages.json:1935` | subPackage `root=pages/wealth-cf`(1 页) |
+| `wealth/index.vue:781` | → `/pages/wealth-sub/checkin` |
+| `wealth/index.vue:784,787` | → `/pages/wealth-sub/insurance-list` |
+| `wealth/index.vue:808` | → `/pages/wealth-cf/index` |
+| `wealth-sub/insurance-list.vue:104` | → `/pages/wealth-sub/insurance-add` |
+| `wealth-sub/insurance-planning.vue:289,294` | → `/pages/wealth-sub/insurance-add`、`/pages/wealth-sub/insurance-list` |
+
+> 注:`components/WealthSubScores.vue` 的 `wealth-sub-scores` 是 class/组件名,与目录无关,**不要改**。
+
+**执行方案**
+1. 目录归位:`pages/wealth-sub/{checkin,insurance-list,insurance-add,insurance-planning}.vue` → `pages/wealth/`;`pages/wealth-cf/index.vue` → `pages/wealth/cf.vue`
+2. `pages.json`:删除 `wealth-sub`、`wealth-cf` 两个 subPackage,5 个页面并入 `pages/wealth` 子包(保留原 `navigationBarTitleText`,`insurance-planning` 补标题「保险规划」)
+3. 更新上表 7 处跳转路径
+4. **旧路径兼容**:`pages/wealth-sub/`、`pages/wealth-cf/` 原位保留重定向壳页面(`onLoad` 内 `redirectTo` 新路径),保留 subPackage 条目并标注 `TODO: 下个版本移除`——防止线上旧版本小程序跳转旧路径 404
+5. 迁移页面内相对引用(`../../utils/...`)按新层级修正;`@/` 别名路径无需改
+
+**风险与验收**:路径变更风险由壳页面兜底;验收 = 全库无旧路径 `navigateTo` 目标(壳内重定向除外)+ 构建通过 + git commit。
+
+---
+
+## 三、Q10 · new Date() 合规判断结论
+
+- **无参 `new Date()`(当前时间戳)**:iOS 各版本均安全,**可保留**。AGENTS.md 红线仅针对 `new Date(字符串)`(iOS 旧版对 `YYYY-MM-DD` 连字符格式返回 NaN)。
+- **变量/字符串形式**:必须改。全库仅 `pages/diet/records.vue` 2 处,改为 `utils/format.js` 的 `parseDate()`。
+- 改动量:1 文件 2 处,可并入任一前端合规补丁任务。
+
+---
+
+## 四、派发清单(按批次,待确认后 dispatch 给 opencode 会话)
+
+> 派发顺序已由用户确认:**A → B → C**,批 E(前端信息架构)与 A 并行派发。密钥轮换按用户指示**暂不动**。
+> LangGraph 生产部署方式已确认:**Docker 部署**(`docker-compose` + `start.sh`),A-5 按 Docker 方式改 Gunicorn。
+
+### 批 A · 后端性能与认证(P0,已确认可执行)
+
+| 编号 | 任务 | 验收标准摘要 | 依赖 |
+|------|------|-------------|------|
+| A-1 | JwtInterceptor 消除每请求 2 次 DB 查询:familyId/memberId 预计算写入 JWT claims,或走 Redis 缓存(Q1 确认有 Redis) | 已认证请求不再触发 UserMapper/FamilyMemberMapper 查询;兼容存量 token(无 claims 时走缓存/降级) | 无 |
+| A-2 | 高频读接口补 @Cacheable:用户信息、家庭成员、产品分类、任务模板(Redis 已确认) | 命中缓存后 DB 查询下降;缓存穿透用空值缓存防护;缓存 key 统一前缀 | A-1 完成后评估共享 key |
+| A-3 | /api/growth/** 纳入 JWT 认证(Q6):审计该路径全部接口,补认证+白名单收敛 | 除公开接口外全部需 token;无回归 | 无 |
+| A-4 | Prompt 热更新支持(Q5):PromptConfig 支持数据库/外部配置热加载,修改无需重启 | 运行时更新 prompt 即时生效;保留 yml 兜底 | 无 |
+| A-5 | LangGraph 多 worker:start.sh 改 Gunicorn + UvicornWorker(workers 2-4) | LLM 推理期间请求不阻塞;健康检查通过 | 确认生产部署方式 |
+
+### 批 B · 合规与内容(P1,已确认)
+
+| 编号 | 任务 | 验收标准摘要 |
+|------|------|-------------|
+| B-1 | `web/index.html` JSON-LD FAQPage 去治疗宣称:L60/L64/L71「检测诊断」「运动处方」改为「参考非诊断」「运动建议」,与正文免责声明一致 | AI 爬虫读取的 FAQPage 无治疗宣称;正文与 JSON-LD 口径一致 |
+| B-2 | 补齐隐私政策/用户协议/儿童隐私页面:修复 `articles/` 下 4 页 `privacy.html` 失效链接 | 页面存在且可访问;内容覆盖儿童数据场景 |
+| B-3 | `pages/diet/records.vue` 2 处 `new Date(变量)` 改 `parseDate()`(Q10 结论) | 全库无 `new Date(字符串/变量)` 残留(无参除外) |
+| B-4 | 前端合规补丁:4 个 CSS Grid 违规文件改 flexbox(schulte.vue / wishes/index.vue / family-dashboard.vue / duo-compatibility.vue) | 小程序端 `display:grid` 归零 |
+
+### 批 C · 文档与运维(P1,已确认)
+
+| 编号 | 任务 | 验收标准摘要 |
+|------|------|-------------|
+| C-1 | 配置外置化(Q11):application.yml 中的 DB 密码、JWT secret、微信密钥改环境变量占位,默认值仅限本地开发 | 代码仓库不含生产凭据;生产由环境注入;**不改变密钥值本身(不轮换)** |
+| C-2 | 部署文档修订(Q12):DEPLOYMENT.md 更新为非 Docker 部署(JAR + Nginx);docker-compose 移除 cfc-backend build 引用与不存在的 .env.production 引用 | 文档与实际部署一致;compose 可独立跑 LangGraph |
+| C-3 | 生产日志级别 `com.etotem.cfc: debug` → `info` | 生产日志量下降,健康数据不落盘 |
+| C-4 | 数据库备份方案:mysqldump 每日全备 + binlog 增量 + 异地存储 + 恢复演练文档 | 备份可验证恢复 |
+
+### 批 D · 暂缓/待办(不阻塞)
+
+| 编号 | 事项 | 状态 |
+|------|------|------|
+| D-1 | 密钥轮换(JD Cloud AK/SK、DB 密码、JWT secret、微信全套、opencode 密码) | ⏸ 用户明确暂不动;已入库 Git 视为潜在泄露,启动前必须完成 |
+| D-2 | 订阅消息接入(Q9 未申请模板) | ⏸ 先由运营申请微信订阅消息模板(打卡/任务/测评/支付 4 类),审批通过后再排前端接入 |
+| D-3 | 财富文案全局替换 | ✅ 取消(Q7:UI 标签保留) |
+| D-4 | sfms 多数据源 / Prometheus / Actuator | ✅ 取消(Q2/Q3/Q4 不启用) |
+| D-5 | 巨型单页拆分(index-home 3057 行等)、Service >2000 行拆分 | 🔵 保留为架构债,建议后续专项(可单独立项) |
+| D-6 | 数据库备份、索引补充(health_reports/tasks 缺索引)、Flyway 迁移管理 | 🔵 保留建议,待排期 |
+| D-7 | wealth 入口分流 | ✅ 升级为批 E(合并执行) |
+
+### 批 E · 前端信息架构(用户新增,与 A 并行)
+
+| 编号 | 任务 | 验收标准摘要 |
+|------|------|-------------|
+| E-1 | wealth-cf + wealth-sub 合并进 wealth 模块(见第二节 2.3 执行方案) | 5 页面归位、pages.json 收敛为 1 个 wealth 子包 + 旧路径壳、7 处跳转更新、构建通过 |
+
+### 批 G · 巡检增强(用户 2026-10-24 新增)
+
+| 编号 | 任务 | 验收标准摘要 |
+|------|------|-------------|
+| G-1 | 巡检新增 git 增量扫描:commit message 标记([TASK]/[REQ]/需求:/待办:等)识别 → 自动写入 tasks_backlog.json pending 池(source=git)→ fill_slots 下周期自动派发 | 幂等(seen_hashes)、单次安全阀 ≤5 条、--dry-run、config.json git 段、loop_resume 集成(fill_slots 之前,--no-git 关闭) |
+
+---
+
+## 五、决策确认记录(2026-10-24)
+
+| 项 | 用户答复 |
+|----|----------|
+| 派发顺序 | A → B → C(已确认) |
+| LangGraph 生产部署方式 | **Docker 部署**,生产可访问(A-5 按 Docker + Gunicorn 改) |
+| wealth 合并 | **将 wealth-cf、wealth-sub 合并到 wealth 中**(新增批 E) |
+| 密钥轮换 | 暂不动(D-1 记为 P0 安全欠账) |
+| 巡检 git 增量扫描 | 三项决策(用户 2026-10-24,均已确认):识别来源=仅 commit message 显式标记;去向=自动入池直接派发;范围=仅 E:\cfc 当前分支增量(新增批 G) |
+
+---
+
+## 六、派发执行台账(2026-10-24)
+
+| 任务 | 实例 | 会话 | 状态 |
+|------|------|------|------|
+| A-1 JwtInterceptor 优化 | lan-189(C:\code\cfc) | ses_e9336e195ffexB10lWaPo64azK | 已投递,运行中 |
+| E-1 wealth 合并 | lan-204(D:\cfc) | ses_e933704c8ffeLfDmxTszdehtLa | 已投递,运行中 |
+| G-1 git 增量扫描 | lan-248(/sc-data/cfc) | ses_e92fdeca5ffe3I3hVudXkQKxZ8 | 已投递,运行中 |
+
+> 备注:首次派发因 `--dir "E:/cfc"` 覆盖了远程实例默认目录导致失败(lan-204 Location not found / lan-248 HTTP 500),重派时去掉 `--dir` 使用实例自身 default_directory(lan-189=C:\code\cfc、lan-204=D:\cfc、lan-248=/sc-data/cfc)后成功。经验:remote 实例派发禁止传 --dir 覆盖。

+ 222 - 0
docs/superpowers/plans/verify/verify-activity-time-5min-commit.md

@@ -0,0 +1,222 @@
+# 独立核验报告 — 活动时间收敛为 5 分钟粒度(commit 707ea09ad)
+
+- **核验日期**:2026-10-24
+- **核验人**:Agnes(software-qa-engineer,独立复核)
+- **提交对象**:`707ea09ad fix(activity):活动时间收敛为 5 分钟粒度(只到分,小程序+后台)`
+- **涉及文件**:
+  - `cfc-frontend/pages/activity/create.vue`(+41/-6)
+  - `cfc-web/src/views/admin/ActivityEdit.vue`(+40/-3)
+
+---
+
+## 逐条核验结果
+
+### ① 需求符合性 — PASS
+
+#### 小程序端 `normalizeTimeTo5Min`
+
+| 检查项 | 代码位置 | 结论 |
+|--------|---------|------|
+| 分钟向下取整到 5 的倍数 | `create.vue:369` `Math.floor(mi / 5) * 5` | ✓ |
+| 不跨小时进位(14:59→14:55,23:59→23:55) | `create.vue:370` `('0'+h).slice(-2)` 保留原小时 | ✓ |
+| 返回格式 `HH:MM`(无秒) | `create.vue:370` 返回 `'HH:MM'` | ✓ |
+
+**实测验证(node 1440 分钟全量扫描)**:
+
+```
+PASS: all 1440 minutes normalized correctly
+14:07 -> 14:05
+14:08 -> 14:05
+14:59 -> 14:55
+00:00 -> 00:00
+23:59 -> 23:55
+null -> null
+"" -> 
+```
+
+- 零失败,所有分钟输出恒为 5 的倍数且小时不变。
+
+#### 三个 `@change` 与 `fmtTime` 回填
+
+| 处理函数 | 事件绑定位置 | 归一化接入 |
+|---------|-------------|-----------|
+| `onStartTimeChange` | `create.vue:24` `@change="onStartTimeChange"` | `create.vue:340` `this.timePart = this.normalizeTimeTo5Min(...)` ✓ |
+| `onEndTimeChange` | `create.vue:36` `@change="onEndTimeChange"` | `create.vue:349` `this.endTimePart = this.normalizeTimeTo5Min(...)` ✓ |
+| `onRegTimeChange` | `create.vue:48` `@change="onRegTimeChange"` | `create.vue:358` `this.regTimePart = this.normalizeTimeTo5Min(...)` ✓ |
+| `fmtTime` 回填 | `create.vue:590` | `create.vue:594` 调用 `this.normalizeTimeTo5Min(h + ':' + mi)` ✓ |
+
+#### `loadDetail` 三处归一化回填("显示即提交")
+
+| 字段 | loadDetail 调用位置 | 旧实现 | 新实现 |
+|------|-------------------|--------|--------|
+| `startTime` | `create.vue:304` | `self.form.startTime = sd.toISOString()` | `self.syncStartTime()` ✓ |
+| `endTime` | `create.vue:312` | `self.form.endTime = ed.toISOString()` | `self.syncEndTime()` ✓ |
+| `registrationDeadline` | `create.vue:320` | `self.form.registrationDeadline = rd.toISOString()` | `self.syncRegDeadline()` ✓ |
+
+#### 后台端 `format="yyyy-MM-dd HH:mm"` 隐秒列
+
+- `ActivityEdit.vue:165` 开始时间 picker:`format="yyyy-MM-dd HH:mm"`(无 `ss`)✓
+- `ActivityEdit.vue:177` 结束时间 picker:`format="yyyy-MM-dd HH:mm"`(无 `ss`)✓
+- Element UI 2.15.14 datetime 面板中,无 `ss` 则秒列不渲染 ✓
+
+#### `floorTo5Minutes` 双入参(Date/字符串)
+
+```js
+// ActivityEdit.vue:372-379
+floorTo5Minutes(value) {
+  if (!value) return value
+  const isDateObj = value instanceof Date
+  const d = isDateObj ? new Date(value.getTime()) : new Date(value)
+  if (isNaN(d.getTime())) return value       // 非法输入原样返回
+  d.setMinutes(Math.floor(d.getMinutes() / 5) * 5, 0, 0)
+  return isDateObj ? d : d.toISOString()     // Date 入→Date 出;字符串入→ISO 串出
+}
+```
+
+| 入参类型 | 行为 | 实测 |
+|---------|------|------|
+| `Date(14:07:30)` | 返回 Date,分钟→14:05,秒→0 | ✓ |
+| `'2026-10-25T14:07:00'` | 返回 ISO 串,分钟→14:05 | ✓ |
+| `null` | 原样返回 null | ✓ |
+| `''` | 原样返回 '' | ✓ |
+| `'not-a-date'` | 原样返回原串(不产生 Invalid Date)| ✓ |
+| `Date(00:00:00)` | 返回 Date,00:00:00 | ✓ |
+| `Date(23:59:59)` | 返回 Date,23:55:00 | ✓ |
+
+#### `loadDetail` 归一化回填
+
+- `ActivityEdit.vue:416-417` `startTime: this.floorTo5Minutes(d.startTime)` ✓
+- `ActivityEdit.vue:417` `endTime: this.floorTo5Minutes(d.endTime)` ✓
+
+#### 全文 grep `selectableRange` / `timePickerOptions` 残留
+
+```
+E:/cfc/cfc-frontend/pages/activity/create.vue: 无匹配
+E:/cfc/cfc-web/src/views/admin/ActivityEdit.vue: 无匹配
+```
+**结论:零残留** ✓
+
+---
+
+### ② 实测函数行为 — PASS
+
+#### 测试 ①:1440 分钟逐一灌入 `normalizeTimeTo5Min`
+
+- **方法**:node 循环 h=0..23, mi=0..59,逐一调用,检查分钟恒为 5 倍数、小时不变
+- **结果**:1440/1440 全部通过,零异常
+- **边界**:00:00→00:00、23:59→23:55,均正确
+
+#### 测试 ②:`floorTo5Minutes` 多场景
+
+| 测试用例 | 入参 | 期望 | 实际 | 结果 |
+|---------|------|------|------|------|
+| Date 对象带秒 | `new Date(2026,9,25,14,7,30)` | Date,14:05:00 | 14:05:00 | ✓ |
+| Date 对象 | `new Date(2026,9,25,14,8,0)` | Date,14:05:00 | 14:05:00 | ✓ |
+| ISO 字符串 14:07 | `'2026-10-25T14:07:00'` | ISO 串,14:05 | 2026-10-25T06:05:00.000Z | ✓ |
+| ISO 字符串 14:08 | `'2026-10-25T14:08:00'` | ISO 串,14:05 | 2026-10-25T06:05:00.000Z | ✓ |
+| 00:00 | `new Date(0,0,0,0,0,0)` | Date,00:00:00 | 00:00:00 | ✓ |
+| 23:59 | `new Date(2026,9,25,23,59,59)` | Date,23:55:00 | 23:55:00 | ✓ |
+| null | `null` | null | null | ✓ |
+| undefined | `undefined` | undefined | undefined | ✓ |
+| 非法串 | `'not-a-date'` | 原串 `'not-a-date'` | `'not-a-date'` | ✓ |
+
+#### 测试 ③:模拟 `loadDetail` 回填
+
+- **入参**:`startTime="2026-10-25T14:07:00"`
+- **流程**:`parseDate` → `fmtTime` → `normalizeTimeTo5Min` → `syncStartTime`
+- **期望**:`form.startTime = "2026-10-25T14:05:00"`
+- **实际**:`2026-10-25T14:05:00` ✓
+
+---
+
+### ③ 契约未破坏 — PASS
+
+#### 小程序提交串格式
+
+- `syncStartTime`: `this.datePart + 'T' + this.timePart + ':00'`(`create.vue:374`)
+- `syncEndTime`: 同上模式(`create.vue:379`)
+- `syncRegDeadline`: 同上模式(`create.vue:386`)
+- `timePart` 经 `normalizeTimeTo5Min` 后恒为 `HH:MM`(分钟 5 倍数,无秒)
+- **结果**:提交串恒为 `YYYY-MM-DDTHH:mm:00`,秒恒为 `00` ✓
+
+#### 后端 `Activity.java:51` 解析兼容性
+
+```java
+// Activity.java:51
+private Date startTime;   // java.util.Date,无 @JsonFormat 注解
+```
+
+- Jackson 默认序列化/反序列化 `java.util.Date` 使用 ISO 8601 格式(含秒)
+- 前端提交的 `YYYY-MM-DDTHH:mm:00` 是标准 ISO 8601,Jackson 可正常解析 ✓
+- 无契约变更风险 ✓
+
+---
+
+### ④ 提交卫生 — PASS
+
+#### 仅修改目标 2 个文件
+
+```
+git show 707ea09ad --stat:
+ cfc-frontend/pages/activity/create.vue   | 41 ++++++++++++++++++++++++++------
+ cfc-web/src/views/admin/ActivityEdit.vue | 40 +++++++++++++++++++++++++++----
+ 2 files changed, 70 insertions(+), 11 deletions(-)
+```
+
+#### 未提交文件核对(应保持未提交)
+
+| 文件 | 当前状态 |
+|------|---------|
+| `.gitignore` | Modified(未提交)✓ |
+| `cfc-backend/pom.xml` | Modified(未提交)✓ |
+| `cfc-web/AGENTS.md` | Modified(未提交)✓ |
+| `tools/xaf_agent_hub/tasks_backlog.json` | Modified(未提交)✓ |
+
+#### 无多余提交 / 未推送
+
+- `git log --oneline -5`:`707ea09ad` 是当前 HEAD,前序为 `ef8d9d66c merge`,无多余提交 ✓
+- `git branch -r --contains 707ea09ad`:无输出(未推送到任何远端分支)✓
+- `origin/cfclub` 最新为 `ef8d9d66c`,不含 `707ea09ad` ✓
+
+---
+
+### ⑤ BUG-1 / BUG-2 回归验证 — PASS(逻辑层面)
+
+> 说明:E2E 需在浏览器/开发工具内操作,以下基于代码逻辑推断 + 纯函数测试验证。
+
+#### BUG-1:后台 datetime picker 只改日期不碰时间时,时间不重置为 00:00
+
+- **根因消除**:`selectableRange` / `timePickerOptions` 两个 computed 已从 `ActivityEdit.vue` 完全移除(grep 零残留)
+- **`format="yyyy-MM-dd HH:mm"`** 仅影响面板展示列,不干预 `v-model` 值
+- **`@change="onStartTimeChange"`** 触发 `floorTo5Minutes(val)`,传入的是当前 v-model Date 对象,小时分钟保留不变,仅秒置 0、分钟归 5 倍数
+- **逻辑结论**:只改日期时,`onStartTimeChange` 不会将小时/分钟清零 → BUG-1 修复正确 ✓
+
+#### BUG-2:手输合法时间失焦后正常写入且被归一化
+
+- **手输 14:10** → `floorTo5Minutes` 收到 Date(14:10:00),`Math.floor(10/5)*5 = 10`,不变 → 写入 14:10 ✓
+- **手输 14:07** → `floorTo5Minutes` 收到 Date(14:07:00),`Math.floor(7/5)*5 = 5`,归一化为 14:05 ✓
+- **逻辑结论**:两种输入行为均符合预期 ✓
+
+---
+
+## 已知遗留项(不阻塞放行)
+
+1. **注释措辞**:源码注释写"就近收敛",实为**向下取整**(`Math.floor`)。语义轻微不准确但不影响行为正确性。
+2. **后台分钟列全可点**:移除 `selectableRange` 后,Element UI datetime 面板分钟列 60 项均可点选,非 5 倍数需经 `@change` 兜底收敛。用户可短暂看到 14:07 再跳至 14:05,属已接受的设计代价。
+3. **E2E 浏览器实测**:未能在真实 Element UI datetime picker 面板中完成 clickoutside + 手输联动测试(无 jsdom + vue-test-utils 环境),以上为逻辑层推断。
+
+---
+
+## 最终结论
+
+**PASS — 可以放行。**
+
+| 核验项 | 结果 |
+|--------|------|
+| 需求符合性(归一化逻辑/事件接入/loadDetail/无残留) | PASS |
+| 实测函数行为(1440 分钟全量/边界/无效输入/loadDetail 模拟) | PASS |
+| 契约未破坏(提交串格式/后端 Date 解析) | PASS |
+| 提交卫生(仅 2 文件/未推送/未提交文件未干扰) | PASS |
+| BUG-1/BUG-2 回归(逻辑层) | PASS |
+
+**建议**:后续在含 Element UI 2.15.14 + jest + @vue/test-utils 的环境中补做一次真实 E2E 回归,覆盖 BUG-1(日期切换保留时间)和 BUG-2(手输归一化)的端到端行为。

+ 62 - 0
docs/superpowers/plans/verify/verify-activity-time-5min-round2.md

@@ -0,0 +1,62 @@
+# Round 2 回归报告 — 活动时间 5 分钟粒度修复验证
+
+- 日期:2026-10-24
+- 验证人:严过关(software-qa-engineer)
+- 对象:工程师对 BUG-1 / BUG-2 的修复(commit 基 HEAD `ef8d9d66c`)
+- 涉及文件:`cfc-frontend/pages/activity/create.vue`、`cfc-web/src/views/admin/ActivityEdit.vue`
+- 结论:**可以放行**(必测项 1-3 全部实测 PASS,核心需求未退化,范围干净,语法通过)
+
+## 方法(独立复测,不复用实现方自检)
+
+| 层 | 方法 | 结果 |
+|---|---|---|
+| 纯函数 | 从 `.vue` 提取真实源码(`normalizeTimeTo5Min` / `floorTo5Minutes` / `fmtTime`),`new Function` 当前 realm 重建执行;1440 分钟全量扫描 | 18/18 PASS |
+| 端到端 | jest + @vue/test-utils + jsdom 挂真实 `element-ui/lib/date-picker`,模拟点击面板 / 手输 / "现在"按钮 / 提交通道(clickoutside 需 document mousedown+mouseup 组合) | 13/13 PASS |
+| 语法/模板 | `<script>` 提取为 `.mjs` 跑 `node --check`;模板跑 `vue-template-compiler` | 零 error |
+| 范围/契约 | `git diff --name-only` + 逐文件 diff 甄别;后端契约文件 grep | 仅 2 个 .vue |
+
+## 逐条验证(PASS/FAIL + 证据)
+
+### ① BUG-1 修复:只改日期不再重置 00:00,"现在"按钮恢复 — PASS
+
+- 根因消除:`ActivityEdit.vue` 中 `selectableRange` / `timePickerOptions` **零残留**(grep exit=1;Round 1 缺陷组件 `timePickerOptions` computed 与模板绑定已移除,见 165/177 行仅剩 `format` + `@change`)。
+- E2E S1:v-model 置 `2026-10-25 14:05:00`,仅点击日期面板 26 号 → 结果 `2026-10-26 14:05:00`,时间完整保留(Round 1 同操作为 `00:00:00`)。
+- E2E S2:"现在"按钮(`.el-picker-panel__link-btn` 第 1 项)点击后正常写入当前时间,分钟经兜底为 5 倍数(Round 1 同根因失效)。
+
+### ② BUG-2 修复:手输 14:10 正常写入,手输 14:07 收敛 14:05 — PASS
+
+- E2E S4:时间输入框手输 `14:10`,失焦后 v-model 为 `2026-10-25 14:10:00`,正常写入(Round 1 任何输入均被 `isValidValue` 拒绝还原为 null)。
+- E2E S5:手输 `14:07` → 面板确认后 `@change` 兜底(`onStartTimeChange` → `floorTo5Minutes`,381-382 行)收敛为 `2026-10-25 14:05:00`。
+- 单测:`floorTo5Minutes` 对 Date 对象 / ISO 串 / 14:07 / 14:08 / 边界 00:00、23:59 全部向下取 5 倍数且秒置 0。
+
+### ③ 口径统一:小程序 loadDetail 改调 sync 函数,"显示即提交" — PASS
+
+- `create.vue` loadDetail 三处回填已改调 `self.syncStartTime()`(304 行)/ `syncEndTime()`(312 行)/ `syncRegDeadline()`(320 行),不再直接写 `sd.toISOString()`。
+- 单测 B 组:以 Java 格式串 `Sun Oct 25 14:07:00 CST 2026`(即后端 `ActivityDTO.from()` 的 `Date.toString()` 契约)灌入 → `timePart = "14:05"` 且 `form.startTime = "2026-10-25T14:05:00"`,**显示值与提交值一致**(Round 1 遗留点 b"显示 14:05、提交 14:07"已消除);`endTime` 14:08→14:05、`regDeadline` 23:57→23:55 同构收敛。
+- 提交串格式恒为 `YYYY-MM-DDTHH:mm:00`(1440 分钟扫描,秒恒 `00`)。
+
+### ④ 核心需求未退化(只到分、5 倍数单元) — PASS
+
+- 小程序:1440 个分钟逐一灌入 `normalizeTimeTo5Min` / `fmtTime`,输出恒为 5 倍数分钟且无秒(含 0/5/55/59 边界)。
+- 后台:E2E S3 面板点选 07 分 → v-model 先写 14:07 → 确认后 `@change` 兜底收敛 14:05:00,最终落值仍合规;S6 提交序列化 ISO 串秒恒 `00`。
+- 秒列隐藏:模板 `format="yyyy-MM-dd HH:mm"`(165/177 行)不含 `ss`,element-ui 2.15.14 datetime 面板不渲染秒列。
+
+### ⑤ 改动范围 — PASS
+
+- `git diff --name-only` 仅 2 个 `.vue` 属本改动;其余变更文件(.gitignore / cfc-backend/pom.xml / cfc-web/AGENTS.md / tools 下 tasks_backlog.json)经逐文件 diff 确认与本需求无关。
+- 无 Java 源码、无 dist/、无 unpackage/ 产物改动;HEAD 仍为 `ef8d9d66c`。
+- 后端契约未动:`ActivityDTO.java:21 private String startTime`、`Activity.java:51 private Date startTime` 与改动前一致。
+
+### ⑥ 语法可编译 — PASS
+
+- `create.vue` / `ActivityEdit.vue` 的 `<script>` 提取后 `node --check` 均 OK;两模板 `vue-template-compiler` 编译 error 数为 0。
+
+## 已知遗留项(不阻塞放行)
+
+1. **注释语义**:源码注释写"就近收敛",实为**向下取整**(14:07→14:05,而非 14:10)。已向 team-lead 说明,取整方向一致(两端均 floor),行为可预期。
+2. **回填即归一化**:两端 loadDetail 对历史非 5 倍数数据(如 14:07)静默改为 14:05,"显示即提交"。Round 2 后两端口径已统一,属预期设计。
+3. **后台分钟列全可点**:移除 `selectableRange` 后(BUG-1/2 修复的必然结果),datetime 面板分钟列 60 项均可点选,非 5 倍数由 `@change` 兜底收敛。粒度需求仍达成(S3/S5 实证),代价是用户可短暂看到 14:07 再收敛。
+
+## 最终结论
+
+**可以放行。** 必测项 ①(BUG-1)②(BUG-2)③(口径统一)全部实测 PASS,核心需求(时间只到分、5 分钟单元)未退化,改动范围干净,语法编译通过。遗留项均为已确认的预期行为,不构成缺陷。

+ 111 - 0
docs/superpowers/plans/verify/verify-task-checkin-fix.md

@@ -0,0 +1,111 @@
+# 「打卡 4 问题修复」独立完成情况核验报告
+
+- 核验人:verify-task-checkin-fix(独立复核会话)
+- HEAD:707ea09ad(修复改动全部在工作区未提交)
+- 方式:只读审查 + 静态编译校验 + 数据流推演(未修改任何代码、未提交)
+- 结论:**7 项全部 PASS**,修复就位且有效。
+
+## 核验结果总览
+
+| # | 核验项 | 结论 |
+|---|--------|------|
+| 1 | 问题4 sleep-checkin.vue formatDate 修复(模板/方法/import 配对 + 编译) | PASS |
+| 2 | 问题1/3 根因修复(三个健康 Controller 成员归属修正 + 打卡页 memberId 解析 + index-home 传参) | PASS |
+| 3 | 问题2 tasks.vue 成员 chips/切换/权限提示 | PASS |
+| 4 | 静默失败修复(TaskController null 分支 + 前端调用点提示) | PASS |
+| 5 | 编译(后端 mvn / 前端 node --check / 模板编译) | PASS |
+| 6 | 数据流推演(睡眠打卡端到端闭环) | PASS |
+| 7 | 改动范围(9 个本需求文件 + 历史遗留项隔离) | PASS |
+| 附 | API 错误传播形态补充确认(决定静默失败是否真的可见) | PASS |
+
+---
+
+## 1. 问题4:sleep-checkin.vue formatDate 崩溃修复 —— PASS
+
+- 模板调用点:`cfc-frontend/pages/growth/sleep-checkin.vue:147` — `{{ formatDate(item.createdAt) }}`
+- methods 暴露:`cfc-frontend/pages/growth/sleep-checkin.vue:321` — `formatDate: function(d) { return formatDateUtil(d) }`
+- import 配对:`cfc-frontend/pages/growth/sleep-checkin.vue:161` — `import { formatDate as formatDateUtil } from '../../utils/format.js'`
+- 配对逻辑闭合:import 用别名 `formatDateUtil`(不再与方法名冲突),methods 的 `formatDate` 包装导出,三者形成 `模板 → method → import 别名` 的正确链条。这也解释了根因:修复前模板调用 `formatDate` 会在原型链上踩中 util 导出,修复前版本缺少 methods 暴露导致 `_vm.formatDate is not a function`。
+- 编译证据:提取 `<script>` 块 `node --check` 全部 exit 0(sleep/diet/exercise/tasks 4 个文件);`vue-template-compiler` 编译 5 个文件模板均 0 error(filter 掉 "side-effects/ multiple nodes" 通用性提示后为 0)。
+
+## 2. 问题1/3:睡眠打卡后任务不消失 / 家庭任务数不变 —— PASS
+
+### 后端归属修正(三个 Controller 均就位,条件完全一致)
+
+- `cfc-backend/src/main/java/com/etotem/cfc/controller/HealthSleepController.java:75-80`
+- `cfc-backend/src/main/java/com/etotem/cfc/controller/HealthMealController.java:257-262`(同 diff 位置)
+- `cfc-backend/src/main/java/com/etotem/cfc/controller/HealthExerciseController.java:97-102`(同 diff 位置)
+
+条件判定完整:`linkedTask != null`(外层 selectById 判断)+ `record.getTaskId() != null` + `linkedTask.getFamilyMemberId() != null` + `record.getMemberId() != null` + `!familyMemberId.equals(memberId)` 五重条件齐备才 `setMemberId`,置于 `taskService.updateCheckinTaskProgress / completeTask` 调用**之前**,后续回写天然使用修正后的 memberId。
+
+### 打卡页 memberId 解析(currentChildId/userId 已移除)
+
+- `sleep-checkin.vue:303` / `diet-checkin.vue:222` / `exercise-checkin.vue:216` 三处均改为:
+  `this.memberId = options.memberId || uni.getStorageSync('currentMemberId') || null`
+  grep 确认三文件已无 `currentChildId`、`userId` 兜底残留。
+
+### index-home 跳转 URL 带 memberId
+
+- `cfc-frontend/pages/index-home/index.vue:1583-1592`:diet/sleep/exercise 三个分支均追加 `&memberId=(task.familyMemberId || task.executorId || '')`。
+- 字段名真实性:`cfc-backend/.../entity/Task.java:20`(`familyMemberId`) 与 `:28`(`executorId`) 确认存在;同文件 `index.vue:1541` 已有既有用法 `var memberId = (task && task.familyMemberId) || null` 佐证字段名非臆造。
+
+## 3. 问题2:tasks.vue 家庭成员任务查看/切换/权限 —— PASS
+
+- 成员 chips:`tasks.vue:11-14` v-for `familyMembers` + `:key="member.id"`(无表达式 key)+ `@click="selectMember(member.id)"`;data `selectedMemberId: null`(tasks.vue:266)。
+- selectMember:`tasks.vue:326-330` — 设 `selectedMemberId`、`activeTab = 'today'`、调 `loadTodayTasks(this.selectedMemberId)`。
+- loadTodayTasks:`tasks.vue:377-386` — `arguments.length > 0 ? memberId : this.selectedMemberId`,显式传参生效;不传参(switchTab:345 / loadInitialData:358)回落到 `selectedMemberId` 再兜底 JWT。`api.js:705-714` 确认 body.memberId 仅在非空时写入。
+- 权限适配:`completeTask`(tasks.vue:415-418 isOwnTask 提示 + 421-425 res.code!==200 toast + 430-432 data 兜底)+ `handleTaskAction`(tasks.vue:459-462)+ `submitWithInput`(tasks.vue:555-559 + 562-573)三处入口均有 isOwnTask 拦截;非本人任务**仍渲染**(列表按 targetMemberId 全量返回),仅点击按钮才提示「只能完成自己的任务」——符合"可看不可操作"要求。
+- 我创建的 tab:`loadCreatedTasks`(tasks.vue:387-399)走 familyWide + creatorId 过滤,逻辑未动;pending tab(368-376)未变;isOwnTask 在 `selfMemberId == null` 或 `task.familyMemberId == null` 时 `return true`(338行注释即说明此兜底防呆语义),未破坏既有 tab。
+- 模板反模式扫描:新模板无 `?.`、无 `toLocaleString`、`:key` 无运算符表达式。
+
+## 4. 静默失败修复(TaskController + 前端提示链)—— PASS
+
+- 后端:`TaskController.java:118-140` — `result != null` → `Result.success(result)`;result 为 null 时(service 585-593 行已确认 null 语义:任务不存在 / `!Objects.equals(task.getFamilyMemberId(), memberId)` / 已完成或待审核 / 需前置校验等)返回 `Result.error("任务不存在、无权完成或状态已变更")`。原 `Result.success(null)` 已替换。
+- 前端调用点(错误可见性):
+  - `index-home/index.vue:1542-1557` quickCompleteTask — `res.code === 200` 分支保留三态 toast,`else` 分支 toast `res.message`,`.catch` toast「完成失败,请重试」。
+  - `tasks.vue:421-425` / `tasks.vue:562-565` completeTask/submitWithInput — `res.code !== 200` → toast `res.message`,catch 内 toast。
+  - **API 错误传播确认(api.js:320-328)**:非 200 业务码 → wrapper 自动 `uni.showToast(res.data.message)` + `reject(res.data)`。即后端 `Result.error`(code=500)必然经 toast 或 catch 落到用户可见,静默失败链条已断。
+  - 备注(非缺陷):因 wrapper 对非200走 reject,tasks.vue 内 `res.code !== 200` 分支实际主要覆盖 5001(noFamily) 等 resolve-型错误码;属双保险,无害。
+
+## 5. 编译验证 —— PASS
+
+- 后端:`cd cfc-backend && mvn clean compile -DskipTests -q` → `MVN_EXIT=0`,无 ERROR。
+- 前端:4 个涉及页面脚本块 `node --check` 全 exit 0;`vue-template-compiler` 编译 5 个文件(睡眠/饮食/运动打卡页 + tasks + index-home)模板 0 error。
+
+## 6. 数据流推演(家长账号点睡眠打卡端到端)—— PASS,闭环
+
+推演路径(家长 storage 残留 currentChildId 场景):
+
+1. 首页 `quickCompleteTask`(index.vue:1528-1537)判定 actionType=sleep → `navigateByLinkedAction` → URL `sleep-checkin?taskId=X&memberId=<本人成员ID>`(1587-1589,task.familyMemberId 优先,已证字段存在)。
+2. `sleep-checkin.vue:303` onLoad:`options.memberId || currentMemberId || null` → 取 URL 中的**本人成员ID**,currentChildId 失效。
+3. 提交 → `HealthSleepController.create`(59-118):record.memberId=本人 → taskId 非空 → linkedTask 非空且 familyMemberId==本人 → 归属一致,无需修正 → isDailyProgress==1 → `updateCheckinTaskProgress(taskId, fm.getUserId(), fm.getId(), 1)`。
+4. `TaskService.java:1784-1848`:成员校验 `member.getUserId()==userId` ✅、`task.getFamilyMemberId().equals(familyMemberId)` ✅ → 进度累加 → 达标调 `awardTaskCompletionRewards` 置 completed。
+5. 返回首页 onShow → `_refreshOnShow` → `loadTodayTasks`(1122-1140):个人列表该任务 status=completed 不再 pending → 卡片消失;`familyTodoCount = countFamilyPendingInProgress(...)`(1149-1157,只数 pending/in_progress)→ 家庭任务数 -1。
+
+**根因闭环说明**:修复前若 storage 有 currentChildId,打卡页 memberId 取孩子成员,record 与 task 归属错配,`updateCheckinTaskProgress` 在 1784-1798 校验被拒返回(任务不完成、卡片不消失、计数不减);修复后 URL 传 memberId + 后端归属修正双保险,校验通过,行为符合预期。
+
+一次性(非每日进度)睡眠任务同样闭合:走 87-95 completeTask 分支,memberId 已按 linkedTask 修正,与 service:587 的 `Objects.equals(task.getFamilyMemberId(), memberId)` 对齐,可正常完成。
+
+## 7. 改动范围 —— PASS,无越界
+
+`git diff --stat HEAD` 共 14 个修改文件,逐一对照:
+
+本需求 9 个(4 后端 + 5 前端):
+1. cfc-backend/src/main/java/com/etotem/cfc/controller/HealthExerciseController.java
+2. cfc-backend/src/main/java/com/etotem/cfc/controller/HealthMealController.java
+3. cfc-backend/src/main/java/com/etotem/cfc/controller/HealthSleepController.java
+4. cfc-backend/src/main/java/com/etotem/cfc/controller/task/TaskController.java
+5. cfc-frontend/pages/growth/diet-checkin.vue
+6. cfc-frontend/pages/growth/exercise-checkin.vue
+7. cfc-frontend/pages/growth/sleep-checkin.vue
+8. cfc-frontend/pages/index-home/index.vue
+9. cfc-frontend/pages/tasks/tasks.vue
+
+历史遗留未提交项(不算本需求,提交时应排除):.gitignore、cfc-backend/pom.xml、cfc-web/AGENTS.md、docs/参考资料/知识库/统一知识库.json、tools/xaf_agent_hub/tasks_backlog.json。另还有一批 untracked 历史遗留(cfc-langgraph/AGENTS.md、cfc-web/docs/、out/ 等),均与本需求 diff 无关。
+
+未发现本需求文件内含与 4 问题无关的改动:tasks.vue 的 563 行 diff 含较大模板重构(卡片化重设计),属问题2 成员 chips UI 改造的连带样式调整,功能侧(成员选择/权限/提交链)与需求相关,视为合理。
+
+## 备注(不影响结论的观察)
+
+- tasks.vue 新增未使用的 CSS 影响面较大(task-card 系列样式重写),若后续审美评审可收敛;功能无缺陷。
+- 提交时建议精确 `git add` 上述 9 个文件,避免把 pom.xml/AGENTS.md/tasks_backlog.json 等历史遗留项卷入本需求提交。

+ 205 - 0
docs/优惠券需求设计与实现核查.md

@@ -0,0 +1,205 @@
+# 优惠券模块:需求、设计与实现情况核查
+
+> 核查范围:CFC 项目优惠券(优惠券/券模板/家庭券/心愿券)全链路
+> 代码基线:`cfc-backend`(Spring Boot 2.7.18 + MyBatis-Plus)、`cfc-web`(Vue2+Element UI 后台)、`cfc-frontend`(uni-app 用户端)
+> 文档基线:`E:/cfc/docs` 下 2026-07 至 2026-10 的优惠券/会员/积分/分佣相关设计
+> 性质:只读核查,未改动任何代码。
+
+---
+
+## 一、总体结论(TL;DR)
+
+| 维度 | 结论 |
+|---|---|
+| **需求** | 明确且完整:券类型(满减/无门槛/折扣)、6 条发放路径、使用门槛与有效期、与会员/积分/CF/家庭的绑定均已定义。最大分歧是「券挂在个人还是家庭」,最终以 **2026-08-31 家庭维度方案** 为落地形态。 |
+| **设计** | 经历过 3 次重设计(08-05 个人维度 → 08-09 紧急修复 → 08-31 家庭维度),并在 09-01 分佣/CF、10-20 特价中持续扩展。设计文档之间仍存在两处未对齐(模板级 `family_id` vs 实例表 `family_coupon`;RESERVED 状态未在 08-31 定义)。 |
+| **实现** | **核心链路可用**:券模板 CRUD、后台发券、用户领取、下单抵扣计算、核销/占用/归还、与订单/会员/积分/CF/家庭的多向集成均已落地。 |
+| **主要缺口** | ① **心愿券(孩子积分兑换)单券日限次失效(已实测代码确认)**;② 过期券无物理清理;③ 注册发券缺失;④ 前后端折扣计算不一致(前端客户端减额绕过后端);⑤ 遗留死表未清理;⑥ `used_count` 漂移。 |
+| **用户端** | 已实现「我的优惠券 / 下单选券 / 会员开通选券 / 赠券规则 / CF 值兑换」,但**缺少独立「领券中心」页面**(仅有提示文案指向不存在的页)。 |
+
+---
+
+## 二、需求演进(来自 docs)
+
+### 2.1 时间线
+
+| 时间 | 文档 | 关键决策 / 痛点 |
+|---|---|---|
+| 基线 | `flows/cart-coupon-flow.md`、`flows/wealth-coupon-exchange-flow.md` | 原始个人维度券(`user_coupon`),仅 FIXED 满减;已存在「财富能量兑换券」。 |
+| 2026-08-05 | `specs/2026-08-05-membership-coupon-system-redesign.md` | 用「券数量」替代「商品会员价」;券扩为 FIXED/CASH/DISCOUNT 三种;新增 JOIN/PERIODIC/POPULATION 发放 + 积分兑换。 |
+| 2026-08-09 | `specs/2026-08-09-coupon-fix-plan.md` | 修复 08-05 上线即暴露的「开通会员赠券未收到 + 我的券入口缺失」。 |
+| 2026-08-12 | `specs/2026-08-12-platform-points-family-pool-design.md` | 锁定「兑换券(后定名心愿券)不改 → 仅孩子个人积分维度参与」,不纳入新建的家庭平台 CF 池(埋下维度分歧伏笔)。 |
+| 2026-08-29 / 08-30 | `membership-pay-switch-design`、`membership-center-admin-redesign` | 会员支付开关 + 后台改级后自动送 JOIN 券,补齐发放不一致。 |
+| **2026-08-31** | `specs/2026-08-31-coupon-family-based-design.md` | **核心转向**:用户要求「券只发家庭」,全链路由 `user_coupon` 迁到 `family_coupon`(家庭维度,全家共享)。 |
+| 2026-09-01 | `specs/2026-09-01-cf-commission-redesign.md` | 分佣重构:CF 兑换券放家庭共享池;`coupon` 模板表新增 `family_id` 列(与 08-31 口径出现差异)。 |
+| 2026-09-10 | `specs/2026-09-10-basic-membership-design.md` | 会员按等级把券分为「平台券/会员券/专属券」(展示维度,与机制维度正交)。 |
+| 2026-10-20 | `specs/2026-10-20-limited-time-special-price-design.md` | 特价单 `allow_coupon` 开关;引入 `reserve()` 占券 + RESERVED 状态(08-31 未定义)。 |
+
+### 2.2 核心需求点(汇总)
+
+- **券类型**:FIXED(满减,有 `min_spend` 门槛)/ CASH(无门槛)/ DISCOUNT(折扣,千分比 `discount_rate`,抵扣≤订单额)。
+- **发放场景(6 条)**:① 开通/续费会员赠券 JOIN ② 会员有效期内周期补发 PERIODIC(月/季)③ 每新增家庭成员发固定数量 POPULATION(默认 4 张)④ 孩子积分兑换 EXCHANGE(产品名「心愿券」)⑤ 后台批量/定向发放 ⑥ CF 值兑换 CF_EXCHANGE。
+- **使用范围与门槛**:`applicableTo`(ALL/MEMBERSHIP)、满减门槛、可绑定 `product_id`(NULL=全场)、特价单可禁券(`allow_coupon=0` 拒收 `couponId`)。
+- **有效期**:`valid_from/valid_until`,读取与使用时按此过滤/校验。
+- **核销**:`apply` 校验归属/有效期/适用范围/门槛→算抵扣;支付成功 `markUsed`(记 `order_id`);退款/取消 `revert`;下单占用 `reserve`、超时/取消还券。
+- **防重复发放**:`family_coupon_grant_log` 唯一键幂等;心愿券兑换有全局日限次 + 单模板日限次 + 最低积分。
+
+### 2.3 需求/设计之间的冲突(需用户拍板澄清)
+
+1. **发放维度个人 vs 家庭(已落地为家庭)**:08-05 设计发到「用户」,08-31 推翻为「家庭」。现状代码以 `family_coupon` 为生产表。
+2. **「券绑定家庭」两种口径并存**:08-31 用独立实例表 `family_coupon`(挂 `family_id`);09-01 又在 `coupon` 模板表加 `family_id` 列。两者并存,未明确以谁为准。
+3. **兑换券归属维度混淆 + 命名澄清(用户 2026-10-24 拍板)**:08-12 说「兑换券仅孩子个人积分维度」;09-01 说「CF 兑换券放家庭共享池」——两套积分体系(孩子个人积分 vs 平台/个人 CF 值)对外名称须区分。**定名:孩子积分兑换得到的券产品名统一为「心愿券」**,不叫"兑换券";CF 值兑换的券按原口径(家庭共享池),不称心愿券。
+4. **RESERVED 状态未设计**:10-20 引入 `family_coupon.status=RESERVED` 与 `reserve()`,但 08-31 设计仅定义 AVAILABLE/USED,存在文档断层。
+
+---
+
+## 三、实现情况
+
+### 3.1 后端(数据模型 + 接口 + 逻辑)
+
+**数据表(6 张,2 张遗留死表):**
+
+| 表 | 作用 | 位置 | 状态 |
+|---|---|---|---|
+| `coupon` | 券模板 | `schema.sql:2329-2351` | 在用 |
+| `user_coupon` | 用户券实例(旧维度) | `schema.sql:2353-2364` | **遗留死表** |
+| `coupon_grant_log` | 用户发放流水(旧维度) | `schema.sql:4643-4655` | **几乎死表** |
+| `family_coupon` | 家庭券实例(实际在用) | `schema.sql:4661-4672` | 在用 |
+| `family_coupon_grant_log` | 家庭发放流水(实际在用) | `schema.sql:4674-4686` | 在用 |
+| 兑换记录表 | 含 `coupon_id`/`family_coupon_id` | `schema.sql:5020+` | 在用 |
+
+**接口清单:**
+- 用户端 `CouponController`(`/api/coupon`):`list`(可用券)、`claim`(领取)、`apply`(下单抵扣计算)、`exchange`(心愿券兑换/积分兑换)、`exchangeable`(可兑换)、`product`(按商品)、`checkout-list`、`join-rules`、`my`(我的券)。无 `@PreAuthorize`,依赖 JWT 注入 `userId`。
+- 后台 `AdminCouponController`(`/api/admin/coupon`):`list/create/update/issue/issue-family/grant-user/delete/toggle-status/grant-log/family-coupons/family-coupon-usage` 共 11 个端点。**鉴权为手写 `if(!"admin".equals(role))`**,未用声明式 `@PreAuthorize`。
+
+**核心逻辑实现度:**
+
+| 能力 | 判定 | 证据 |
+|---|---|---|
+| 券模板 CRUD + 启停 | ✅ 完整 | `AdminCouponController:60/75/174` |
+| 后台批量/定向发券 | ✅ 完整 | `issue`/`issue-family`/`grant-user` → `issueToFamily`/`grantMemberUpgradeCoupon` |
+| 活动自动发(JOIN/PERIODIC/POPULATION) | ⚠️ 部分 | JOIN(`MembershipService`/`AdminController`)、POPULATION(`FamilyMemberService`)、PERIODIC(`CouponGrantTask` cron 5:00);无通用营销活动发券 |
+| **注册发券** | ❌ 缺失 | `UserService.register*` 无发券钩子;仅播种「新用户专享10元券」模板无触发 |
+| 用户领取 | ✅ 完整 | `CouponService.claim:126` |
+| 下单券校验与抵扣计算 | ✅ 完整(**但前端未调用**) | `CouponService.apply:171` |
+| 核销/占用/归还 | ✅ 完整 | `markUsed`/`reserve`/`revert` + 订单/取消/超时/退款全链路 |
+| **过期物理清理** | ❌ 缺失 | `CouponGrantTask` 仅做 PERIODIC 发放;无定时任务把过期 `family_coupon` 置 EXPIRED,仅展示计算 |
+| 与 UserCoupon 关联 | ⚠️ 已迁移 | `user_coupon`/`UserCoupon` 实体/Mapper 已无业务引用(死代码) |
+
+**与其他模块集成:** 强集成于 `ProductOrderService`(apply/reserve/markUsed/revert,普通/特价/多商品三入口)、`MembershipService`(会员抵扣+JOIN 赠券)、`PointsExchangeService`(积分兑换)、`FamilyPlatformPointsService`(CF 兑换)、`FamilyMemberService`(人口券增/撤)。购物车未引用券——选券在结算/下单完成。
+
+### 3.2 前端
+
+**后台 `cfc-web`(完整):**
+- `CouponManagement.vue`:券模板列表/搜索/排序/分页、新建编辑(含类型/折扣率千分比/门槛/适用对象/积分价/发放规则/有效期/总量)、删除、启停、批量发券。**与 11 个后端端点全部对应。**
+- `CouponGrantLog.vue`:按家庭/券过滤的发券记录(类型标签覆盖 JOIN/PERIODIC/POPULATION/EXCHANGE/CF_EXCHANGE)。
+- 漏洞:搜索框 `keyword` 后端 `list` 完全忽略(失效);列表为「假分页」(后端返回全量);`CouponGrantLog` 家庭名解析为 N+1 请求。
+
+**用户端 `cfc-frontend`(较完整):**
+- 已实现:`pages/profile-extra/coupons.vue`(我的优惠券:可使用/已使用/已过期/可兑换 4 Tab + 领取 + CF 兑换)、`pages/shop/checkout/checkout.vue`(下单选券 + 客户端算折)、`pages/membership/pay.vue|upgrade.vue|result.vue`(会员开通选券 + 赠券规则)、`pages/wealth-cf/index.vue`(CF 值兑换券)。
+- **缺口:无独立「领券中心」页面**(仅有 `coupons.vue:55` 一句指向不存在页面的空状态文案)。
+- 前后端一致性风险:① 后端 `/api/coupon/apply`(服务端算折扣)**未被前端调用**,前端用 `total - selectedCoupon.value` 客户端减额(可被篡改/前后端不一致);② `/api/coupon/exchange`(心愿券兑换/积分兑换)、`/api/coupon/product`(按商品查券)**无前端调用**;③ 用户端 `exchangeCoupon` 方法死代码(实际走 CF 通道);④ 券类型/适用对象枚举标签在用户端映射不全(`PERCENT`/`PRODUCT` 会显示原始值)。
+
+---
+
+## 四、已验证的关键缺陷(按优先级)
+
+> 以下均经代码核对,证据含文件+行号。
+
+### 【高 P0】心愿券(孩子积分兑换)单券日限次失效(已读代码确认)
+- **位置**:`PointsExchangeService.exchangeCoupon` 第 247–256 行(单券日限次校验)+ 第 283 行(实际发券)。
+- **根因**:第 247 行用 `couponGrantLogMapper` 统计 `coupon_grant_log`(**遗留死表**)的当日 EXCHANGE 次数;但实际发券走第 283 行 `couponService.grantFamily(...)`,该方法(含 `CouponService:401/432`)只写 `family_coupon_grant_log`,**从不写 `coupon_grant_log`**。
+- **后果**:`couponTodayCount` 永远为 0 → 配置项 `coupon_exchange_daily_limit`(默认 1)**永不生效**。用户可对同一心愿券无限次兑换(仅受全局 `exchange_daily_limit=5` 约束)。
+- **修复方向**:把第 247 行的统计源改为 `familyCouponGrantLogMapper`(按 `family_id` + `coupon_id` + `grant_type=EXCHANGE` 当日计数),或统计已发 `family_coupon` 实例数。
+
+### 【高 P0】前后端折扣计算不一致(前端客户端减额)
+- **位置**:`checkout.vue:595-606`(前端 `total - selectedCoupon.value`)+ `:1104` 传 `couponId`;后端 `CouponService.apply:171-213`(含满减门槛、MEMBERSHIP 校验、DISCOUNT 千分比折算)。
+- **风险**:前端自行减额、后端 `/api/coupon/apply` 未被调用,存在**客户端可篡改、前后端抵扣额不一致**隐患;最终以服务端落库为准但前端展示可能错。
+- **修复方向**:结算页改调 `/api/coupon/apply` 取服务端折扣,或下单接口强制重算(后者已在 `ProductOrderService` 内做)。
+
+### 【中 P1】过期券无物理清理
+- 无定时任务将过期 `family_coupon` 置 EXPIRED,仅读取时按 `valid_until` 计算。长期累积 AVAILABLE 过期券,影响列表性能与统计。
+
+### 【中 P1】注册发券缺失
+- `UserService.register*` 无任何发券钩子;「新用户专享10元券」模板存在但无触发。与「新用户引导与留存」目标不符。
+
+### 【中 P1】遗留死表未清理
+- `user_coupon`、`coupon_grant_log` 两表及 `UserCoupon` 实体/`UserCouponMapper`/`CouponGrantLog` 实体基本无写入(仅 `coupon_grant_log` 被 `selectCount` 读一次)。建议清理或对账,并删除无引用代码。
+
+### 【中 P1】`used_count` 漂移
+- `claim()` 递增 `coupon.used_count`,但 `revert()`/收回/过期均不递减;且库存校验基于该近似值,与家庭维度实际券数可能不一致。
+
+### 【低 P2】后台搜索框失效 + 假分页
+- `AdminCouponController.list` 忽略 `keyword` 与 `page/size`;前端分页器无真实意义。
+
+### 【低 P2】用户端领券中心缺失 + 枚举标签不全 + N+1
+- 「领券中心」仅有提示文案;券类型/适用对象映射不全;`CouponGrantLog` 家庭名 N+1 解析。
+
+### 【低 P2】实体与 DDL 不一致 / 鉴权脆弱 / 折扣精度
+- `Coupon.java` 缺 `family_id` 字段(`schema.sql:2342` 有该列);后台优惠券接口用手写 role 判断而非 `@PreAuthorize`;DISCOUNT 抵扣整数截断有分位误差;`apply:192` 在 `applicableTo` 为 null 时可能 NPE。
+
+---
+
+## 五、修复方案(用户 2026-10-24 拍板:5 项全部选「是」)
+
+以下 5 项已批准,全部交由 opencode 会话落地(commit 不 push),完成后由独立会话核验。
+
+| 批号 | 对应待拍板项 | 内容 | 优先级 | 派发实例 | 状态 |
+|---|---|---|---|---|---|
+| **C1** | ① P0 两项 | 心愿券单券日限次失效 + 前后端折扣不一致 + 心愿券命名 | P0 资金/权益安全 | local-45278(E:\cfc) | **已完成·已核验**(commit `8f33bc2ea`) |
+| **C2** | ② 死表+used_count | 清理 `user_coupon`/`coupon_grant_log` 无引用代码 + `used_count` 漂移修复 | P1 数据健康 | pub-iwintrue(/sc-data/cfc,原 lan-248 会话启动失败已重投) | 已派发·运行中 |
+| **C3** | ③ 领券中心 | 补齐用户端独立「领券中心」页面 + 空状态入口跳转 | P1 用户触达 | pub-iwintrue(/sc-data/cfc) | 已派发·运行中 |
+| **C4** | ⑤ 过期清理 | 新增过期 `family_coupon` 物理清理定时任务 | P1 长期数据 | lan-189(C:\code\cfc) | 已派发·运行中 |
+| **Doc** | ④ 设计文档澄清 | 补 RESERVED 状态定义;记录 `family_id`(模板列) 与 `family_coupon`(实例表) 并存口径;落实心愿券命名 | — 文档 | 本机直接改 docs | 已完成 |
+
+> 注:注册发券缺失(原主要缺口③)未列入上述 5 项拍板范围,作为后续观察项,暂不动。
+> P2 项(后台搜索框/假分页/枚举标签/N+1/鉴权/DDL 不一致)建议纳入下一轮技术债清理,本次未派发。
+
+### 各批次任务要点
+- **C1(P0)**:`PointsExchangeService.exchangeCoupon` 第 247 行统计源由 `couponGrantLogMapper`(死表) 改为 `familyCouponGrantLogMapper`(family_id + coupon_id + grant_type=EXCHANGE + 当日),使 `coupon_exchange_daily_limit` 生效;结算页 `checkout.vue` 改调 `/api/coupon/apply` 取服务端折扣替代客户端 `total - value`;孩子积分兑换券用户端展示统一称「心愿券」(CF 兑换券不称心愿券)。
+- **C2(P1)**:确认无引用后移除 `UserCoupon`/`UserCouponMapper`/`CouponGrantLog` 死代码(表不 DROP,遵循 08-31「保留不迁移」);`CouponService.revert()` 与 `revokeFamilyPopulationCoupons()` 还券/收回时同步递减 `coupon.used_count`。
+- **C3(P1)**:`cfc-frontend` 新建「领券中心」页面(pages.json 注册 + 调 `/api/coupon/list` 拉可取券 + `/api/coupon/claim` 领取);`coupons.vue:55` 空状态文案改为跳转真实页面。
+- **C4(P1)**:新增/扩展定时任务,将 `family_coupon` 中 AVAILABLE 且 `coupon.valid_until < now()` 的记录批量置 EXPIRED(不动 RESERVED;分批 UPDATE 防长事务)。
+- **Doc**:在 `2026-08-31-coupon-family-based-design.md` 补 RESERVED 状态(占券/还券)定义;标注 `coupon.family_id`(模板级绑定) 与 `family_coupon`(实例级归属) 并存非冲突;记录心愿券命名口径。
+
+> 修复全程由 opencode 会话执行并 commit;本机(WorkBuddy)仅做方案、派发、核验与汇总,不改业务代码。
+
+---
+
+## 六、补充需求核实:下单页直接兑换 CF 券并使用(2026-10-24)
+
+> 需求原文:用户购买下单时,如果当前商品支持优惠券,且优惠券支持 CF 值兑换,且用户 CF 值足够兑换,用户应可以在下单页面直接进行优惠券兑换并使用。
+
+### 6.1 逐条件核实结论
+
+| 需求条件 | 现状实现 | 结论 |
+|---|---|---|
+| 商品支持优惠券 | 券模型无商品级开关;支持范围由券模板 `applicable_to`(`ALL` / `PRODUCT`+`product_id`)表达。限时特价另有活动级 `allow_coupon`(`checkout.vue:582-585` 为 0 时整块隐藏券区) | 已满足(符合现有券模型) |
+| 券支持 CF 值兑换 | `CouponService.listUnownedExchangeable:561` 过滤 `points_price > 0`,并排除家庭已拥有模板 | 已满足 |
+| 用户 CF 值足够 | 前端 `affordableExchangeCoupons:628-633` 过滤 `cfBalance >= pointsPrice`(余额不足整块隐藏),`doExchangeCoupon:747` 二次校验 | 已满足 |
+| 下单页直接兑换 | `checkout.vue:328-351` 券选择弹窗内「可用CF值兑换」区 + 兑换按钮,调 `exchangeCouponByCf` | 已满足 |
+| **兑换后立即使用** | `doExchangeCoupon:746-776` 兑换成功仅重拉券列表 + 刷新 CF 余额,**不自动选中新券**,用户须手动再点一次刚入账的券 | **缺口(唯一未满足项)** |
+
+结论:该能力 **约 90% 已存在**,缺的只是「兑换成功即自动选中并算折扣」这最后一步;同时核实过程中发现 1 个伴生后端过滤 bug。
+
+### 6.2 伴生 bug:全场通用券在下单页兑换区不可见
+
+- **位置**:`CouponService.listUnownedExchangeable:567-569` —— `if (productId != null) wrapper.eq(Coupon::getProductId, productId)`。
+- **后果**:结算页传了 `productId` 时,`applicable_to='ALL'`(`product_id` 为 NULL)的全场通用券被 SQL 过滤,兑换区看不到;而拥有券列表(`checkout.vue:615-626`)对 `ALL` 券是放行的。**同一张全场券:先别处兑换再下单能用,直接在下单页兑换却不可见**,前后端对「商品支持券」的判定不一致。
+- **加重因素**:多商品订单仅取 `items[0].productId`(`checkout.vue:727`),问题在跨商品订单下更明显。
+
+### 6.3 派发方案 C5(已派发 local-45278,会话 `ses_e926729faffeM5GSAYESSlTWWH`)
+
+| 改动点 | 文件 | 内容 |
+|---|---|---|
+| 后端 A | `CouponService.listUnownedExchangeable` | `productId` 非空时改为 `(product_id IS NULL OR product_id = ?)`,让全场通用券进入兑换区 |
+| 后端 B | `FamilyPlatformPointsService.exchangeCouponByCf` + `FamilyPlatformBalanceController:69` | 返回值 `void` → `Long`,上抛 `grantFamilyOne` 已算出的新 `family_coupon.id`(原仅返回文案「兑换成功」) |
+| 前端 | `checkout.vue.doExchangeCoupon` | 兑换成功后用返回的券实例 id 在重拉的 `this.coupons` 中精确匹配(`toCouponVO` 的 `id`/`userCouponId` 均等于 `family_coupon.id`,唯一无歧义),匹配到即 `selectedCoupon` + 复用现有 `applyCouponDiscount()`(服务端算折扣),实现「兑换即使用」 |
+
+**关键设计取舍**:不用「按模板 `coupon_id` 匹配」——若用户此前已拥有同模板未使用的券,会误匹配到旧实例。后端直接返回新实例 id 是唯一无歧义方案;`exchangeCouponByCf` 全项目仅 1 个调用方(已 grep 确认),改签名安全。
+
+**边界行为(预期,非缺陷)**:刚兑换的券若不满足本单门槛(`min_spend` 超订单额、或 `PRODUCT` 范围不匹配),`applyCouponDiscount` 会自动清空选中并提示;券已入账「我的优惠券」,后续满足条件的订单仍可用。
+
+---
+
+*附:三份分维度核查(后端实现 / 前端实现 / 需求设计演进)证据详实,本文为其综合摘要。如需逐项原始行号可回查对应 Explore 报告或上述文件路径。*

+ 185 - 0
docs/家庭成员本人重复显示问题分析.md

@@ -0,0 +1,185 @@
+# 家庭成员「本人重复显示 / 两个账号错觉」问题分析
+
+> 分析日期:2026-10-24 | 性质:只读诊断,**未改动任何代码**
+> 需求:当前用户与其所在家庭成员关系不清,任务列表等页面重复显示当前登录用户本人,且「本人」与「本人对应的家庭成员」感觉像两个账号。
+
+---
+
+## 一、问题本质(四层根因)
+
+### 1. 概念层:两条平行身份线,没有统一「一个人」的实体语义
+
+| 身份线 | 表 | 主键 | 含义 |
+|--------|-----|------|------|
+| 账号 | `users` | `id` = **userId** | 能登录的账号(openid) |
+| 家庭成员 | `family_members` | `id` = **memberId** | 家庭里的一个「座位」,`user_id` 可空,可关联账号 |
+
+**当前登录用户 = 一个 userId,同时也是 family_members 中一条记录的 memberId。** 两者是同一人的两个 ID,不是两个账号。
+
+- 证据:`JwtInterceptor.java:176-186` —— 每请求按 `userId + familyId` 反查 family_members,结果设进 `currentMemberId`;`TaskController` 全部以 `currentMemberId`/`memberId` 兜底 → **后端任务数据只有一套,没有重复**。
+- 「两个账号」错觉 = 前端把这两个 ID 同时展示成了两个条目。
+
+### 2. 数据层:孤儿数据 + 去重口径只按 userId
+
+- `family_members.user_id` 可空(`schema.sql:2197`);`FamilyMemberService.addMember()` 对未填手机号的成员也建无 openid 的 user 并绑定;早期创建家庭路径**漏建创建者成员记录**(`FamilyMemberService.java:692-693、760-761` 多处「兼容历史孤儿数据」)。
+- `FamilyMemberService.listMembers()`(230-370 行):family_members 段**全量返回不做本人过滤**,`isSelf` 仅作标记(307 行);parent 段合并 users 表家长(324-367 行)**只按 userId 去重**(339 行)——`user_id` 为空的孤儿防不住 → **同一人真会返回两条**。
+- `FamilyMemberVO.memberType`(login/local,`FamilyMember.java:165-171`、`FamilyMemberVO.java:24`)**从未在 listMembers 中赋值**,成员段恒为 null → 前端无任何可靠字段区分「可登录本人」与「不可登录成员」。
+- `UserService.getVisibleMembers():905`:`if (isSelf || showToFamily == null || showToFamily == 1)` → **本人永远在返回列表里**。`tasks.vue` 用的正是这个接口(`visibleOnly:true`)。
+
+**去重机制实测结论(已逐行核实)**:
+
+- 正常登录用户同时有 users 记录(`role=parent, family_id`)+ family_members 记录(`user_id=登录用户`):
+  - `listMembers()`:先查 family_members 全量 → **本人成员记录已入**;再查 users 家长段,`seenUserIds` 去重 → **家长段跳过**。**结果只有一条**。
+  - `getVisibleMembers()`(`visibleOnly` 路径):先查 users 家长段 → **本人入**;再调 `listMembers` 拿成员段,同一 userId 跳过 → **成员段跳过**。**结果只有一条**。
+  - → **两种情况 familyMembers 都一定包含本人一条记录** → tasks.vue 必然同时渲染「本人」chip + 本人昵称 chip。
+- **`parent` 段成员的 `id = memberId = userId`**(`FamilyMemberService.java:341-346`),**不是 family_members.id**。
+  → tasks.vue 用 `member.id` 当 memberId 传参(L13);点 parent 段成员 chip 实际传的是 userId,后端按 `tasks.family_member_id` 查 → **对不上 → 返回空或错误任务**。这是功能性 bug,不止观感问题。
+- **同 userId 在 family_members 表存在多条记录时不去重**(去重只在「家长段 vs 成员段」之间,成员段内部无二次去重)。产生多条的历史路径:
+  - 迁移322(`children` 并入,按 `user_id + family_id + phone` 匹配)
+  - 迁移298(为无 openid 的 child 账号补建 family_members,条件 `family_members NOT EXISTS`)
+  - `addMember` 每次新建记录
+  - 一个用户可属多个家庭(按 `family_id` 区分)
+
+### 3. 展示层:VO 三段 ID 混存 + 页面双渲染
+
+`FamilyMemberVO` 的 `id`/`memberId`/`userId` 三段混存且**两段来源语义不同**:family_members 段 `id`=memberId;parent 段 `id`=userId(`FamilyMemberService.java:341-346`)。
+
+前端出现三种混用模式(实测约 30+ 处):
+
+| 模式 | 代码 | 量级 |
+|------|------|------|
+| ① fallback 链 | `currentChildId \|\| currentMemberId \|\| userId` | 约 20+ 处 |
+| ② 直接取 userId 当 memberId | `var memberId = uni.getStorageSync('userId')` | 约 5 处(`wealth/index.vue`、`health/gut-index.vue`) |
+| ③ 列表 fallback | `m.id \|\| m.userId` / `m.id \|\| m.memberId \|\| m.userId` | 约 4 处(`MemberChipPicker.vue`、`diet/records.vue`、`growth/caution-detail.vue`、`health/gut-flora-foods-detail.vue`) |
+
+其中 `pages/health/gut-index.vue` 注释明确写着「**不能混用 currentMemberId**」,但代码仍在混用 —— 团队自己已知晓但未根治。
+
+---
+
+## 二、任务列表页:重复显示现场(用户点名,100% 复现)
+
+`pages/tasks/tasks.vue`:
+
+```
+L12  <view class="member-chip" @click="selectMember(null)">本人</view>
+       └ 硬编码「本人」chip,不传 memberId,后端按 JWT currentMemberId 兜底
+L13  <view class="member-chip" v-for="member in familyMembers" ...>{{ member.nickname }}</view>
+       └ familyMembers ← getFamilyMemberList({visibleOnly:true}) → getVisibleMembers,
+         由 UserService.java:905 保证本人一定在列表中(isSelf=true)
+```
+
+→ 同一人出现两个 chip:**「本人」 + 「本人昵称」** → 用户直观感到「两个账号」。
+同页 L232 创建任务「分配给」勾选列表同样包含本人座位(可把任务分配给自己)。
+
+---
+
+## 三、需要处理的页面清单(分级)
+
+### A 类:硬编码「本人」+ 成员列表并存 → 直接重复(优先修)
+
+| # | 页面 | 位置 | 问题 |
+|---|------|------|------|
+| A1 | `pages/tasks/tasks.vue` 任务列表 | L12 + L13、L232 | 硬编码「本人」chip 与列表内 isSelf 本人重复;分配列表含本人 |
+| A2 | `components/wuxing-sandbox.vue` 五行沙盘 | L254、L490 | 视角切换面板含「本人」入口,`buildMemberOptions` 含 self |
+
+### B 类:成员条 / 标签条 / 选择器渲染了本人(需统一「是否展示本人」口径)
+
+| # | 页面 / 组件 | 是否过滤本人 | 是否有 isSelf 标记 |
+|---|-------------|--------------|---------------------|
+| B1 | `components/DimensionMemberTabs.vue`(五维页成员标签条) | 否 | 有「我」角标 |
+| B2 | `components/FamilyEnergyBar.vue`(能量横条) | 否 | 无 |
+| B3 | `components/FamilyMemberStrip.vue`(成员横向条) | 否(依赖父组件) | 有 |
+| B4 | `components/FamilyRelationGraph.vue`(关系图谱) | 否(高亮 selfId) | 有 |
+| B5 | `components/FamilyMemberCard.vue` | 否 | — |
+| B6 | `pages/health/components/MemberChipPicker.vue` | 否 | 无 |
+| B7 | `pages/energy/index.vue` 能量页 | 否(L91 currentView≠self 时对比列表含本人) | 否 |
+| B8 | `pages/profile-main/profile.vue` 我的页 | 否(L79-105 成员速览含本人,成员数统计含本人) | 否 |
+| B9 | `pages/index-home/index.vue` 综合首页 | 否 | 是 |
+| B10 | `pages/home-pages/parent-index.vue` 家长首页 | 否 | 是 |
+| B11 | `pages/home-pages/child-index.vue` 孩子首页 | 否 | 是 |
+| B12 | `pages/home-pages/member-home-detail.vue` 成员主页 | 否 | 是 |
+| B13 | 五维总览 4 页:`body-detail/index.vue`、`mind-detail/index.vue`、`wisdom-detail/index.vue`、`wealth/index.vue`、`action-detail/index.vue` | 否 | 是 |
+| B14 | 健康域 6 页:`report-list.vue`、`challenge-manage.vue`、`doa-index.vue`、`indicator-quicklook.vue`、`indicator-curve.vue`、`health-plan-summary.vue` | 否 | 无 |
+| B15 | `pages/health/doa-index.vue` | 否 | **显示「未登录成员」标签(memberType==='local'),口径错误** |
+| B16 | 饮食 / 成长域:`diet/food-query.vue`、`diet/records.vue`、`growth/caution-detail.vue`、`health/gut-flora-foods-detail.vue` | 否 | 无 |
+| B17 | `pages/psych-assessment/index.vue` 心理测评 | 否 | 无 |
+| B18 | `pages/activity/activity-detail/activity-detail.vue` 活动详情 | 否 | 无 |
+| B19 | `pages/tianpan/traditional.vue` 天盘 | 否 | 无 |
+| B20 | `pages/orchestration/node-edit.vue` 流程编排 | 否 | 无 |
+| B21 | `pages/family/mbti-entry.vue`、`manual-score-entry.vue` 录分 | 否 | 已有 isSelf 兜底找 own |
+| B22 | 成员详情 / 打卡系列:`member-detail.vue`、`emotion-trend.vue`、`emotion-checkin.vue`、`growth/*.vue`、`wishes/*.vue`、`tasks/task-upload-purchase.vue`、`article-center/article-detail.vue` | 否 | — |
+
+### C 类:成员接口调用分支不统一(新发现,全量 40 处调用 / 27 文件)
+
+| 分支 | 处数 | 后端行为 | 代表页面 |
+|------|------|----------|----------|
+| 默认 `{}` / 无参 | **10** | `listMembers()`,**不过滤 showToFamily=0** | `body-detail/index.vue:677`、`wisdom-detail/index.vue:632`、`wealth/index.vue:571`、`health/report-list.vue:314`、`health/health-survey.vue:185`、`health/challenge-manage.vue:495`、`action-detail/interaction-log.vue:194`、`orchestration/node-edit.vue:102` |
+| `visibleOnly:true` | 23 | `getVisibleMembers()`,过滤 showToFamily=0 | `tasks.vue:354`、`FamilyMemberStrip.vue:304` |
+| `includeFamily:true` | 3 | `listMembersWithFamily()` | `family-members.vue:343`、`parent-index.vue:775` |
+
+**同一份成员列表在不同页面数量与内容不一致** —— 五维总览里 body / wealth / wisdom 用全量分支,mind / action / index-home 用 visibleOnly 分支。用户在不同页面看到的「家庭成员」不是同一批人,与「本人重复显示」叠加后进一步制造「两个账号」错觉。
+
+- `tasks.vue:351-367` 赋值**零过滤**:`this.familyMembers = Array.isArray(raw) ? raw : []`,后端返回什么就渲染什么。
+- `tasks.vue` 任务接口传参链:初始无参 → 后端按 JWT `currentMemberId` 兜底(始终本人);点成员 chip → `getTodayTasks(member.id)`;点「本人」→ `getTodayTasks(null)`;`loadCreatedTasks()` 走 `familyWide:true` 后前端再按 creatorId 过滤。
+- key 字段不统一:`member.id`(tasks.vue:13、FamilyMemberStrip.vue:16、DimensionMemberTabs.vue:21)vs `member.memberId`(FamilyEnergyBar.vue:22,47)vs 三阶回落 `id→memberId→userId`(MemberChipPicker.vue:117,134)—— parent 虚拟成员若 `memberId==userId` 则必然撞号。
+
+### D 类:ID 混用(前端约 30+ 处,后端 40+ 处)
+
+- **① fallback 链(约 20+ 处)**:`health-main/index.vue`、`growth-main/index.vue`、`health/diet-index.vue`、`health/health-survey.vue`、`health/survey-questionnaire.vue`、`health/survey-history.vue`、`mind-detail/emotion-*.vue`、`growth/challenge-progress.vue`、`wishes/index.vue`、`wishes/detail.vue`、`tasks/task-upload-purchase.vue`、`profile-main/profile.vue` 等 —— 全部 `currentChildId || currentMemberId || userId`,**用户未切换时(本人视角)前两值恒为空,直接落到 userId**。
+- **② 直接取 userId(约 5 处)**:`pages/wealth/index.vue`、`pages/health/gut-index.vue`。
+- **③ 列表 fallback(4 处)**:`MemberChipPicker.vue`、`diet/records.vue`、`growth/caution-detail.vue`、`health/gut-flora-foods-detail.vue`。
+- **④ `member-detail.vue:212`**:`isSelf: (m.memberId || m.id) == self.targetMemberId`。
+- **⑤ 陈旧注释与自我承认**:`health/gut-index.vue`「不能混用 currentMemberId」;`article-center/article-detail.vue`「优先 currentChildId,否则用 userId」;`health/report-list.vue`「后端 getMemberReports 兼容 users.id / family_members.id」——**后端做了兼容来吸收前端的 ID 混用,等于把缺陷永久固化进契约**。
+- **⑥ 后端 40+ 处双参/降级(与前端同源镜像)**:方法签名 `foo(userId, memberId)` 约 40 处;`memberId != null ? xxx(memberId) : yyy(userId)` 降级约 6 处(`FoodRecommendController`、`HealthReportController`、`HealthStatusController`);「memberId 即 userId」显式兼容分支(`FamilyMemberService.kickMember:456-477`、`updateMember:879-885`、`switchToMember:410-424`、`McpToolRegistry`「可传 userId 同值」)。
+- **⑦ `memberType` 双语义**:parent 段硬编码 `'parent'`,成员段不设置(`null`)—— 前端无法用它区分「可登录本人」与「不可登录成员」。
+
+### E 类:管理端(cfc-web)—— 无此问题,不需处理
+
+`src/views/Families.vue:120-151` 成员表格只区分 admin/member 角色,无 isSelf/memberType 逻辑;其余 admin 页面均不涉及本人重复。**管理端零改动。**
+
+### F 类:正确示范(无需改动,作为口径参照)
+
+| 位置 | 做法 |
+|------|------|
+| `pages/profile-extra/family-members.vue:230-240` | `filteredMembers` computed **主动过滤 isSelf 本人**(全项目唯一主动过滤的页面) |
+| `pages/action-detail/interaction-log.vue:197-209` | 用后端 `isSelf === true` 过滤本人 |
+| `store/index.js:86-104` `switchToMember` | 切换统一用 **memberId(family_members.id)**,语义正确 |
+
+---
+
+## 四、处理建议(按 ROI 分三批)
+
+| 批次 | 内容 | 目标 |
+|------|------|------|
+| **批 1 止血**(前端,1 文件) | `tasks.vue`:删除硬编码「本人」chip,统一走成员列表 + isSelf「我」角标(参照 `DimensionMemberTabs.vue:29`) | 立即消除用户可见的重复 |
+| **批 2 治本**(后端,1 文件) | `FamilyMemberService.listMembers()`:① 全局按 userId 去重兜底(补孤儿 user_id 空的链);② parent 段不再用 userId 冒充 memberId,统一以 family_members 记录为「人」载体;③ 补 `memberType(login/local)` 赋值;④ 收敛后端 getMemberReports 等的双 ID 兼容 | 从源头消灭「同一人两条记录」与 ID 语义错位 |
+| **批 3 清尾**(前端,约 30 处 + 10 处接口分支) | 消除三类 ID 混用:① fallback 链去掉 `\|\| userId`,改由 `store` 提供统一的 `currentMemberId`(本人视角时预填 memberId 而非留空);② 直接取 userId 的 2 处改为读 store;③ 列表 fallback 3 处统一 `memberId`;④ 修正 `doa-index.vue`「未登录成员」标签口径(改判 `memberType==='local'` 而不是本人);⑤ **统一 10 处 `getFamilyMemberList` 默认分支为 `visibleOnly:true`**(全项目成员列表口径唯一化);⑥ 统一 B 类页面「是否展示本人」语义 | 根治「两个账号」错觉与后续数据错挂风险 |
+
+**批 2 是治本关键**:只要 `listMembers()` 输出统一成「每人一条、id=memberId、isSelf 唯一、memberType 有值」,前端 30 处 fallback 的危害立刻降到零(但仍建议清尾消除技术债)。
+
+---
+
+## 五、待用户拍板的口径(唯一交互点)
+
+| # | 问题 | 推荐默认值 |
+|---|------|-----------|
+| 1 | 任务列表成员条保留「本人」chip 还是只显示成员昵称? | 删除「本人」chip,统一用成员昵称 + 「我」角标 |
+| 2 | 创建任务 / 健康数据归属能否选「本人座位」? | 允许(业务合理,本人也是家庭成员) |
+| 3 | 五维切换器「我」角标本人的语义是否 OK? | OK,作为全项目统一规范 |
+| 4 | parent 段(users 表合并记录)能否被选中 / 被分配? | 批 2 修完后不再独立存在,此问题自动消解 |
+| 5 | 「未登录成员」标签是否保留?保留时判定依据? | 保留,依据 `memberType==='local'`,**不再指向本人** |
+| 6 | 是否顺带清尾 30 处 ID 混用(批 3)? | 建议做,可与批 1 并行派发到不同实例 |
+
+**不回答则按上表推荐默认值推进。**
+
+---
+
+## 六、派发计划(口径确认后执行)
+
+| 任务 | 实例建议 | 理由 |
+|------|----------|------|
+| 批 1 tasks.vue 止血 | lan-248 | 改动小、实例最快(3-8 分钟) |
+| 批 2 listMembers 统一 | local-45278 | 改动在本机工作树,需我本地评审 + 编译核验 |
+| 批 3 ID 混用清尾 | lan-204 或 pub-iwintrue | 纯前端批量替换,与前两批无文件冲突 |
+
+每批改完另派核验会话独立复测(沿用「实现 + 独立核验」链路)。

+ 394 - 0
docs/循环依赖诊断与修复方案.md

@@ -0,0 +1,394 @@
+# CFC 后端循环依赖诊断与修复方案(V2 · 经源码实证重写)
+
+> 诊断日期:2026-10-25
+> 报错现场:`Error starting ApplicationContext` / `The dependencies of some of the beans in the application context form a cycle`
+> 代码基线:诊断基于 `origin/cfclub` HEAD `be62f7465`(2026-10-25 10:45:50);实施基于 `94b5a3d3f`
+> 状态:**✅ 已实施并通过独立核验**(分支 `fix/circular-deps`,5 个提交,核验结论 approve-with-notes)—— 详见第九节
+>
+> **V2 更正说明**:V1 中有两处判断经本轮实证被证伪,已在第二节明确更正:
+> 1. ❌「`@Resource + @Lazy` 对字段注入不生效」→ ✅ **生效**(Spring 4.2+ `CommonAnnotationBeanPostProcessor.buildLazyResourceProxy`,Javadoc 标注 *Since: 4.2*)。
+> 2. ❌「用户报错的那条链仍未修复」→ ✅ **已被 `02ca6aa0c`(10:21) 修复**;用户报错日志时间 10:18:35 **早于**该提交。
+>
+> 但 V2 离线全量依赖图扫描**新发现一个此前无人发现的启动期真环**(配额域小环),这才是 `allow-circular-references: true` 至今必须保留的真正原因。
+
+---
+
+## 一、结论先行(Executive Summary)
+
+| # | 结论 | 证据强度 |
+|---|---|---|
+| 1 | 用户 10:18:35 报的那条 11 节点长链,**已在 10:21 被 `02ca6aa0c` 打断**(`CfBalanceServiceImpl → MembershipService` 改为 `@Lazy @Autowired`) | 时间线 + 源码 |
+| 2 | **当前 HEAD 仍存在 1 个启动期 eager 真环**:`ActivationQuotaService → BasicQuotaService → MembershipService → ActivationQuotaService`(三条边全部 eager) | 离线依赖图全量扫描(含 setter/构造器注入),**无虚警** |
+| 3 | 这个配额域小环是 `6191b66c2` 加 `allow-circular-references: true` 的**真正原因**——不是长链没修干净 | 依赖图 + 时间线 |
+| 4 | `@Resource + @Lazy` **是有效的**(Spring 4.2+),远端已有的 19 处 `@Lazy` 均真实生效 | Spring 官方 Javadoc |
+| 5 | 全仓共 **19 处 `@Lazy`** 延迟注入边,掩盖了 **30+ 条设计环**(含长链)——启动期不报错,但运行时行为不确定 | 离线依赖图 |
+| 6 | 治本方案:把「配额初始化」与「等级查询」两个能力从 `MembershipService` 上剥离,配额域不再反向依赖会员域 | 源码逐行核验 |
+
+---
+
+## 二、V1 两处判断的更正(重要)
+
+### 2.1 更正一:`@Resource + @Lazy` **是生效的**(V1 判断错误)
+
+V1 结论:「`@Resource` 走 JSR-250 路径,不经 `ContextAnnotationAutowireCandidateResolver`,因此 `@Resource` 字段上的 `@Lazy` 被完全忽略」。
+
+**实证推翻**。Spring 官方 Javadoc(`CommonAnnotationBeanPostProcessor`,Spring 4.3 / 6.0 / 7.0-M8 三个版本一致):
+
+```
+protected Object buildLazyResourceProxy(LookupElement element, String requestingBeanName)
+Obtain a lazily resolving resource proxy for the given name and type,
+delegating to getResource(...) on demand once a method call comes in.
+Since: 4.2
+```
+
+即:**`CommonAnnotationBeanPostProcessor` 自 Spring 4.2 起就实现了 `@Resource` 字段的延迟代理**。本项目 Spring Boot **2.7.18**(Spring Framework 5.3.31),远高于 4.2,**`@Resource + @Lazy` 完全有效**。
+
+Spring 官方 GitHub issue 中 Spring 核心成员 sbrannen 的说明亦印证:
+> "if a user wishes to use `@Lazy` with dependency injection by type, the injection point should be annotated with `@Autowired` or `@Inject` instead of `@Resource`" —— 这是**风格建议**(避免 `@Resource` by-name 匹配失败时出现双层代理),**不是**"`@Lazy` 不生效"。
+
+**影响**:远端已有的 19 处 `@Resource + @Lazy` **全部真实生效**,V1 第 4.3 节「清理伪修复」的建议**作废**(若按 V1 执行,会把有效的延迟代理删掉,直接把 30+ 条设计环变成启动期崩溃)。
+
+### 2.2 更正二:用户报的那条链**已经修好了**(V1 判断错误)
+
+用户报错时间 **10:18:35**。相关提交时间线:
+
+| 时间 | 提交 | 内容 |
+|---|---|---|
+| 09:56:07 | `9b4c11953` | 恢复 CF 与 AI 链路缺失的服务注入(**引入环的元凶**) |
+| 10:16:47 | `c4813f72d` | `@Lazy` 打破 `MembershipService ↔ CfCommissionService` 环 |
+| **10:18:35** | — | **← 用户报错日志产生** |
+| 10:21:05 | `02ca6aa0c` | `@Lazy @Autowired` 打破 `CfBalanceServiceImpl → MembershipService`(**闭环边**) |
+| 10:25:03 | `6191b66c2` | 加 `allow-circular-references: true` |
+| 10:30:53 | `207333d7e` | chore bump |
+| 10:45:50 | `be62f7465` | fix(langgraph)(当前 HEAD) |
+
+用户报错链的**闭环边**是 `CfBalanceServiceImpl → MembershipService`(`CfBalanceServiceImpl` 查会员等级)。这条边在 10:21 被 `02ca6aa0c` 改为 `@Lazy @Autowired` —— **报错之后 2.5 分钟**。
+
+**验证**:离线依赖图扫描确认,在当前 HEAD 上,用户报错链上的所有关键边均为 lazy 或已断开:
+
+```
+FamilyMemberService    → MembershipService      EAGER
+MembershipService      → PaymentService         EAGER
+PaymentService         → CommissionService      EAGER
+PaymentService         → MembershipService      LAZY   ← 已被 @Lazy 打断
+CommissionService      → InviteMilestoneService EAGER
+InviteMilestoneService → CfPathwayEarnService   EAGER
+CfBalanceServiceImpl   → MembershipService      LAZY   ← 10:21 由 02ca6aa0c 打断
+CfPathwayEarnService   → UserService            LAZY
+```
+
+### 2.3 新发现:当前 HEAD 仍残留 **1 个启动期真环**
+
+全量离线依赖图扫描(754 个 Bean、2098 条注入边,含字段注入 / setter 注入 / 构造器注入三种形态),**仅 eager 边**构成的环:
+
+```
+ActivationQuotaService → BasicQuotaService → MembershipService → ActivationQuotaService
+```
+
+三条边逐条核验(全部 `@Resource` + eager,**无 `@Lazy`**):
+
+| # | 边 | 位置 | 用法 |
+|---|---|---|---|
+| 1 | `ActivationQuotaService → BasicQuotaService` | `ActivationQuotaService.java:16-17` | 唯一用途:第 23 行 `basicQuotaService.initUserQuotaRecords(userId)`(纯委托) |
+| 2 | `BasicQuotaService → MembershipService` | `BasicQuotaService.java:60-61` | 第 108/184/278 行:`getMemberLevel(userId)`、`getLevelOrder(...)` |
+| 3 | `MembershipService → ActivationQuotaService` | `MembershipService.java:83-84` | 唯一用途:第 793 行 `activationQuotaService.activateBasicQuota(adminUserId, order.getFamilyId())` |
+
+**这就是 `6191b66c2` 必须加 `allow-circular-references: true` 的真正原因**——它掩盖的不是长链,而是这个**配额域小环**。长链早在 10:21 就被 `02ca6aa0c` 打断了。
+
+> **重要推论**:若直接按 V1 方案"删掉 `allow-circular-references: true`",**启动会立即失败**,报 `form a cycle`(就是这个三节点环)。必须先解环,再删开关。
+
+---
+
+## 三、根因分析
+
+### 3.1 为什么会产生这个环
+
+- `ActivationQuotaService` 是 2026-10-23 新增的**极薄壳类**(全文 25 行,只做一件事:把 `activateBasicQuota` 委托给 `BasicQuotaService.initUserQuotaRecords`)。
+- 它被放进 `service/` 根包并加了 `@Service`,于是成为 Spring Bean,被 `MembershipService` 注入。
+- 而 `BasicQuotaService` 反过来依赖 `MembershipService` 查会员等级 → 三方闭环。
+
+**本质**:`ActivationQuotaService` 这个壳本身**没有独立职责**,它存在的唯一目的是"给 `BasicQuotaService.initUserQuotaRecords` 换个入口名"。业务上,`MembershipService` 第 793 行的真实需求只是**支付成功后初始化配额记录**,完全可以直接调 `BasicQuotaService.initUserQuotaRecords(...)`,不必经由壳类。
+
+### 3.2 为什么不能简单用 `@Lazy` 掩盖
+
+用 `@Lazy` 打断这个三节点环是可行的(且与项目现有 19 处做法一致),但**不推荐作为最终方案**,原因:
+
+1. `@Lazy` 只是把"启动期解析"推迟到"首次调用",不消除依赖环本身。运行时首次调用若发生在另一个 Bean 的初始化过程中,仍会触发 `BeanCurrentlyInCreationException`(只是概率性、难复现)。
+2. 代理对象与真实对象行为不一致(事务/AOP 语义、`this` 引用等)。
+3. 项目已有 19 处 `@Lazy`,掩盖了 30+ 条设计环——**再加一处只会加剧技术债**。
+
+### 3.3 治本切点(三条候选,均已核验可行性)
+
+| 方案 | 动作 | 影响面 | 评价 |
+|---|---|---|---|
+| **A(推荐)** | **删除 `ActivationQuotaService` 壳类**,`MembershipService` 第 793 行直接调 `basicQuotaService.initUserQuotaRecords(adminUserId)` | 删 1 个文件 + 改 2 行 | **最彻底**:壳类本就无独立职责;`MembershipService → BasicQuotaService` 这条边由 `MembershipService → ActivationQuotaService → BasicQuotaService` 缩短为一条,**环直接消失** |
+| B | `BasicQuotaService` 抽 `MemberLevelQuery` 窄接口(`getMemberLevel` + `getLevelOrder`),自己用 `UserMapper`/`MembershipLevelMapper` 实现 | 新增 1 接口 + 1 实现,改 `BasicQuotaService` 3 处调用 | 也可行,但 `getMemberLevel` 逻辑较复杂(个人/家庭双身份 + 有效期 + PROVIDER 特判 + 取高者),**搬迁风险高、收益低** |
+| C | 给 `MembershipService.activationQuotaService` 加 `@Lazy` | 1 行 | 治标,留债 |
+
+**推荐 A**。理由:方案 B 要搬迁 `getMemberLevel` 的复杂判定逻辑(`MembershipService.java:1263-1290`,含 PROVIDER 特判、有效期校验、双身份取高),搬迁极易引入语义偏差;而方案 A 只是**删掉一个没有独立职责的空壳**,把一次委托压平,风险最低、收益最大。
+
+**A 方案的关键前提**:`BasicQuotaService` 已注入 `MembershipService`(反向依赖存在),若 `MembershipService` 再注入 `BasicQuotaService`,则形成新的两节点环 `MembershipService ↔ BasicQuotaService`!
+
+**因此 A 方案必须配合下面的 C 一起做**,否则环会从三节点缩成两节点但不会消失:
+
+- **A**:`MembershipService` 依赖 `BasicQuotaService`(删壳类)
+- **C**:`BasicQuotaService → MembershipService` 这条边保持不变(查等级),但需打断 `MembershipService → BasicQuotaService`
+
+等等 —— 重新推导:`BasicQuotaService → MembershipService`(eager)+ `MembershipService → BasicQuotaService`(eager)= 两节点环,**依然成环**。
+
+**所以 A 方案单独用不成立**。真正的治本必须打断 `BasicQuotaService → MembershipService` 这条边,即走 **方案 B**(把等级查询下沉出去),或者:
+
+**方案 D(最优 · 推荐)**:`BasicQuotaService` 需要的只是 `getMemberLevel(userId)` 与 `getLevelOrder(code)` **两个纯查询能力**。把这二者下沉到独立的 **`MemberLevelQueryService`**(新 `@Service`,只依赖 `UserMapper` + `MembershipLevelMapper` + `FamilyMembershipMapper`,不依赖任何 Service):
+
+- `BasicQuotaService → MemberLevelQueryService`(eager,**不再依赖 MembershipService**)→ 环断
+- `MembershipService → ActivationQuotaService → BasicQuotaService`(eager)→ 不再回到 MembershipService → 环断
+- `MembershipService.getMemberLevel()` / `getLevelOrder()` **保留原实现不动**(`MembershipService` 内部自用),可另加委托给新 Service(可选,避免双份逻辑)
+
+> **注意**:`getMemberLevel` 依赖 `getFamilyEffectiveLevel`(家庭身份),实现体 1263-1290 行,需要 `FamilyMembershipMapper`。新 Service 必须**逐行等价搬迁**,并保证 `MembershipService.getMemberLevel()` 与新实现**结果完全一致**(建议:`MembershipService.getMemberLevel()` 改为委托新 Service,保证单一事实源)。这是本方案唯一的实质风险点。
+
+**最终推荐组合**:
+- **D1**:`BasicQuotaService` 改依赖新建的 `MemberLevelQueryService`(承载 `getMemberLevel` + `getLevelOrder`),`MembershipService` 的对应方法改为委托它 → **单一事实源 + 环断**
+- **D2**:`ActivationQuotaService` 壳类保留但**不再被 `MembershipService` 注入**?→ 不行,`MembershipService` 仍需调它。
+
+再理一遍 D1 之后的依赖:
+- `BasicQuotaService → MemberLevelQueryService`(不再 → MembershipService)✅
+- `MembershipService → ActivationQuotaService → BasicQuotaService → MemberLevelQueryService` ✅ **无环**
+- `MembershipService → MemberLevelQueryService`(若委托)✅ 无环
+
+**D1 单项即可彻底解环**,`ActivationQuotaService` 壳类**无需删除**(改动面最小)。这是最优解。
+
+### 3.4 其他设计环:本期不动,登记为技术债
+
+离线扫描发现另外 30+ 条设计环(被 `@Lazy` 掩盖,启动期不报错),例如:
+
+```
+MembershipService → PaymentService → CommissionService → CfPathwayEarnService → UserService
+  → FamilyMemberService → CouponService → MembershipService   (CouponService→MembershipService 为 lazy)
+```
+
+**决策**:本期**不做大重构**。理由:
+1. 它们当前**不导致启动失败**(`@Lazy` 生效),且业务在运行中;
+2. 大范围改动 30+ 条环风险极高,收益是"架构更干净"而非"修 bug";
+3. 用户的核心诉求是「新代码有循环依赖问题,要作为强约束在开发中避免」——**重点在"防新增",不在"清历史债"**。
+
+处理方式:**登记为技术债清单**(见第六节),并在巡检脚本中持续监控"eager 环数量",一旦新增立即拦截。
+
+---
+
+## 四、修复方案(实施清单)
+
+### 4.1 必做:解掉配额域真环
+
+**新增**:`cfc-backend/src/main/java/com/etotem/cfc/service/MemberLevelQueryService.java`
+
+```java
+/**
+ * 会员等级纯查询服务(无 Service 依赖,仅依赖 Mapper)。
+ * 存在意义:打破 BasicQuotaService → MembershipService → ActivationQuotaService
+ *            → BasicQuotaService 的启动期循环依赖。
+ * 逻辑来源:MembershipService.getMemberLevel / getLevelOrder(逐行等价搬迁,唯一事实源)。
+ */
+@Service
+public class MemberLevelQueryService {
+    @Resource private UserMapper userMapper;
+    @Resource private MembershipLevelMapper levelMapper;
+    @Resource private FamilyMembershipMapper familyMembershipMapper;
+    // ... getMemberLevel(Long userId) / getLevelOrder(String levelCode)
+}
+```
+
+**修改 1**:`BasicQuotaService.java`
+- 字段:`@Resource private MembershipService membershipService;` → `@Resource private MemberLevelQueryService memberLevelQueryService;`
+- 第 108、109、184、185、278 行:`membershipService.getMemberLevel(...)` / `membershipService.getLevelOrder(...)` → `memberLevelQueryService.*`
+
+**修改 2**:`MembershipService.java`
+- `getMemberLevel(Long)` 与 `getLevelOrder(String)` **改为委托** `memberLevelQueryService`(保证单一事实源,避免两份逻辑漂移)
+- 新增字段 `@Resource private MemberLevelQueryService memberLevelQueryService;`
+- ⚠ 注意:`MemberLevelQueryService` **不得**反向依赖 `MembershipService`(否则环转移)。`getFamilyEffectiveLevel` / `resolveEffectiveLevel` 若为 private,需一并下沉到新 Service。
+
+**修改 3**:`application.yml`
+```yaml
+spring:
+  main:
+    # 删除 allow-circular-references: true —— 循环依赖必须从设计上消除
+```
+删除后 `spring.main` 若为空则整段删掉。
+
+### 4.2 必做:把"禁止循环依赖"固化为开发强约束
+
+> 用户原话:「新代码有循环依赖问题,要作为强约束在开发中避免」
+
+#### 4.2.1 `E:\cfc\AGENTS.md` —— 新增「依赖注入纪律」小节
+
+```markdown
+### 依赖注入纪律(强约束 · 2026-10-25 起)
+
+1. **禁止循环依赖**:Bean 依赖图必须无环。
+   - **NEVER** 在 `application.yml` 中设置 `spring.main.allow-circular-references: true`(视为违规,评审打回)。
+   - 出现 A→B→A 时,必须**从设计上打破**:下沉逻辑 / 抽窄接口 / 事件解耦 / 删除无独立职责的空壳 Service。
+   - **禁止**用 `@Lazy` 掩盖新产生的环(`@Lazy` 是治标,且会掩盖真实的架构问题)。
+2. **新增 Service 注入前必须查环**:注入前确认目标 Service 的依赖链不会回绕到当前类。
+   - 自查:`python tools/check_circular_deps.py`(见 4.2.3)输出中不得出现新增 eager 环。
+3. **优先抽窄接口,而不是加 `@Lazy`**:当 A 只用到 B 的一两个方法时,把这几个方法抽成窄接口,让 A 依赖接口。
+4. **注意**:`@Resource + @Lazy` 在 Spring 4.2+ 是**生效**的(`CommonAnnotationBeanPostProcessor.buildLazyResourceProxy`,Since: 4.2),
+   但项目统一用 `@Resource`(by-name),若字段名与 Bean 名不一致会产生双层代理 —— 延迟注入时字段名必须与 Bean 名严格一致。
+5. **提交门禁**:后端改动后必须 `mvn clean compile` 通过,且启动日志中**不含** `form a cycle`。
+```
+
+#### 4.2.2 派发任务强制话术模板(所有后续 opencode 开发会话必带)
+
+```
+⛔ 强约束(不可违反 · 违反即打回):
+- 禁止引入循环依赖;禁止设置 spring.main.allow-circular-references=true
+- 禁止用 @Lazy 掩盖新环;必须从设计上打破(抽窄接口 / 下沉 / 事件解耦)
+- 新增 Service 注入前必须确认依赖图无环(运行 tools/check_circular_deps.py 自查)
+- @Lazy 仅在字段名与 Bean 名严格一致时使用
+- 完成后必须:mvn clean compile 通过 + mvn spring-boot:run 启动日志无 'form a cycle'(贴出日志片段)
+```
+
+#### 4.2.3 新增静态检查脚本 `tools/check_circular_deps.py`(仓库内)
+
+**这是把强约束从"文档"变成"可执行门禁"的关键**。基于本轮离线扫描器(已在 `C:\temp\cfc_cycle_scan2.py` 验证可行)落地为仓库脚本:
+
+- 扫描 `cfc-backend/src/main/java` 全部 Java 文件,解析 `@Resource` / `@Autowired` 字段注入、setter 注入、构造器注入
+- 解析 Bean 名(`@Service`/`@Component` 等显式 value 或类名首字母小写),`@Resource` 按 by-name 解析,其余按 by-type + 接口实现类回退
+- 构建依赖图,区分 **eager 边**(无 `@Lazy`)与 **lazy 边**
+- 输出:① eager 环清单(**必须为空**,否则退出码 1)② lazy 掩盖的设计环清单(技术债台账)③ `@Lazy` 使用清单
+
+**接入点**:
+- `.githooks/pre-commit`:暂存区含 `cfc-backend/` 变更时自动运行,有 eager 环则阻断提交
+- `tools/xaf_agent_hub/check_agents.py` 巡检:作为「实例健康总览」的附加检查项
+
+#### 4.2.4 `cfc-backend/AGENTS.md` —— CONVENTIONS / ANTI-PATTERNS 补条目
+
+在 `ANTI-PATTERNS (本项目禁止)` 段追加:
+```markdown
+- **NEVER** 设置 `spring.main.allow-circular-references: true` 掩盖循环依赖
+- **NEVER** 用 `@Lazy` 掩盖新引入的循环依赖(必须从设计上打破)
+```
+
+### 4.3 不做(明确排除,避免范围蔓延)
+
+- ❌ 不删除 `ActivationQuotaService`(保留,避免动 30+ 处引用面;`BasicQuotaService.java:312` 注释已说明其为委托壳)
+- ❌ 不动 30+ 条被 `@Lazy` 掩盖的设计环(登记技术债,本期不重构)
+- ❌ 不改 `02ca6aa0c` 的 `@Lazy @Autowired`(已生效,虽偏离 `@Resource` 规范但改动风险大于收益;记入技术债)
+- ❌ 不改 `9b4c11953` 引入的其他注入
+
+---
+
+## 五、实施顺序(派发给 opencode 开发会话)
+
+| # | 步骤 | 文件 | 说明 |
+|---|---|---|---|
+| 1 | 同步远端 | — | `git fetch origin cfclub`,确认基线 `be62f7465` |
+| 2 | 新建等级查询服务 | `MemberLevelQueryService.java`(新) | 逐行搬迁 `getMemberLevel` + `getLevelOrder`,**只依赖 Mapper**,不得依赖任何 Service |
+| 3 | 改造配额服务 | `BasicQuotaService.java` | 依赖改为 `MemberLevelQueryService`,改 5 处调用 |
+| 4 | 消除双份逻辑 | `MembershipService.java` | `getMemberLevel` / `getLevelOrder` 改为委托新 Service;私有辅助方法一并下沉 |
+| 5 | 移除开关 | `application.yml` | 删 `allow-circular-references: true` |
+| 6 | 新增检查脚本 | `tools/check_circular_deps.py`(新) | 依赖图构建 + eager 环检测 + 技术债台账 |
+| 7 | 接入门禁 | `.githooks/pre-commit` | `cfc-backend/` 变更时自动跑检查脚本,有 eager 环阻断提交 |
+| 8 | 固化规范 | `AGENTS.md`、`cfc-backend/AGENTS.md` | 写入 4.2.1 / 4.2.4 的纪律条目 |
+| 9 | 验证 | — | ① `mvn clean compile` 退出码 0;② `mvn spring-boot:run` 启动日志**无** `form a cycle`;③ `python tools/check_circular_deps.py` 的 **eager 环 = 0**;④ 贴出启动日志与脚本输出 |
+
+**验收标准(硬 · 逐条可验证)**:
+
+1. 启动日志中**不存在** `form a cycle`。
+2. `application.yml` 中**不存在** `allow-circular-references`。
+3. `tools/check_circular_deps.py` 输出的 **eager 环数量 = 0**。
+4. `mvn clean compile` 退出码 **0**。
+5. 现有单测不回归(至少 `CfPathwayEarnServiceTest`、`CfGrowthPathwayEarnServiceTest`、`BasicQuotaService` 相关测试)。
+6. `MemberLevelQueryService.getMemberLevel/getLevelOrder` 与搬迁前 `MembershipService` 版本**逐分支等价**(PROVIDER→FREE、FAMILY 过期→FREE、个人/家庭取高、未知等级回退 1)。
+
+---
+
+## 六、技术债台账(登记,不在本期修)
+
+| # | 项 | 位置 | 风险 | 建议 |
+|---|---|---|---|---|
+| D1 | 30+ 条被 `@Lazy` 掩盖的设计环 | 见 `tools/check_circular_deps.py` 输出 | 中(运行期首次调用可能触发 `BeanCurrentlyInCreationException`) | 按业务域分批重构,优先 `MembershipService ↔ PaymentService` |
+| D2 | `CfBalanceServiceImpl.membershipService` 用 `@Autowired @Lazy`,偏离项目 `@Resource` 规范 | `CfBalanceServiceImpl.java:96-99` | 低(已生效) | 下次触碰该类时顺手改回 `@Resource @Lazy` |
+| D3 | `ActivationQuotaService` 是无独立职责的空壳(仅委托) | `ActivationQuotaService.java` | 低 | 长期看应下沉为 `BasicQuotaService` 的直接调用或事件监听 |
+| D4 | `9b4c11953` 批量恢复注入,未做依赖图审查 | 多个 Service | 中 | 已由本方案门禁覆盖后续 |
+| D5 | `MembershipService` 残留 3 份等级查询等价副本(`resolveEffectiveLevel` / `getFamilyEffectiveLevel` / `getActiveFamilyMembership`,生产无调用方) | `MembershipService.java` | 中(单边修改漂移) | 标记 `@Deprecated` 或收敛到 `MemberLevelQueryService` |
+| D6 | 循环依赖约束仅本地生效(`core.hooksPath` 不入库 + 无 CI workflow 调用检查器) | `.githooks/`、`.github/`(缺失) | 中(`--no-verify` 可绕过) | 补 CI 闸门:PR 上跑 `tools/check_circular_deps.py` |
+| D7 | `pom.xml:28` `<skipTests>true</skipTests>` 使 `mvn test` 静默跳过 | `cfc-backend/pom.xml:28` | 中(既有失败长期未暴露) | 设 `skipTests=false` 或在 CI 显式 `-DskipTests=false` |
+
+---
+
+## 七、风险与回滚
+
+| 风险 | 概率 | 应对 |
+|---|---|---|
+| `getMemberLevel` 搬迁出语义偏差(PROVIDER 特判 / FAMILY 有效期 / 双身份取高 / 未知回退) | **中高** | 逐行搬迁不改逻辑;`MembershipService` 侧改为委托保证单一事实源;验收标准第 6 条逐分支核对;`getFamilyEffectiveLevel`/`resolveEffectiveLevel` 一并下沉,不留悬空调用 |
+| `BasicQuotaService` 的 `getMemberLevel` 调用点漏改 | 中 | 编译期可发现(类型不同 → 编译失败),`mvn clean compile` 拦截 |
+| 删除开关后暴露其它未发现的 eager 环 | 低(离线全量扫描已覆盖字段/setter/构造器三种形态) | 扫描器输出为 ground truth;若启动仍报错,以报错链为准回到扫描器定位 |
+| 远端 `cfclub` 继续前进导致基线漂移 | 中 | 实施前 `git fetch` 并 rebase 到最新 HEAD |
+| 30+ 设计环中某条在运行期暴露 | 中 | 巡检脚本持续输出技术债台账,按优先级逐条处理 |
+
+**回滚**:所有改动拆为独立提交,可 `git revert` 单个提交恢复。**禁止** `git reset --hard`(见 `AGENTS.md` Git 纪律)。
+
+---
+
+## 八、待确认(已定稿 · 2026-10-25 用户拍板)
+
+| # | 事项 | 选项 | 定稿 |
+|---|---|---|---|
+| **D1** | 配额域真环的治本方式 | (a) 新建 `MemberLevelQueryService` 承载等级查询 / (b) `MembershipService.activationQuotaService` 加 `@Lazy` 治标 / (c) 删除 `ActivationQuotaService` 空壳 + `BasicQuotaService` 抽等级接口 | **(a) 已采纳**。理由:唯一能同时做到"断环 + 单一事实源 + 不搬迁复杂判定逻辑以外的东西"。(b) 留债;(c) 改动面最大 |
+| **D2** | 30+ 条设计环是否本期一并重构 | (a) 本期只登记 + 门禁防新增 / (b) 本期全部重构 | **(a) 已采纳**。风险收益比悬殊,且用户诉求重心在"防新增" |
+| **D3** | `allow-circular-references` 是否全局禁止 | (a) 全局禁止并进 `AGENTS.md` + pre-commit 门禁 / (b) 仅 cfc-backend 模块 | **(a) 已采纳**。符合"强约束"要求 |
+| **D4** | `MemberLevelQueryService` 放 `service/` 根包还是 `service/impl/` | (a) `service/` 根包(与 `BasicQuotaService` 同级) / (b) `service/impl/` | **(a) 已采纳**。项目现有 Service 绝大多数在根包 |
+
+---
+
+## 九、实施与核验结果(2026-10-25,已完成)
+
+### 9.1 实施:分支 `fix/circular-deps`,基于 `origin/cfclub@94b5a3d3f`,5 个提交
+
+| commit | 内容 |
+|---|---|
+| `33cbc73aa` | `fix(backend)` 新增 `MemberLevelQueryService`(`getMemberLevel`/`getLevelOrder` 自 `MembershipService:1263-1290` 逐行等价搬迁,仅依赖 3 个 Mapper);`MembershipService` 改委托;`BasicQuotaService` 5 处调用改注入 |
+| `cfd9e7c57` | `fix` 移除 `spring.main.allow-circular-references`(`spring.main` 段空,整段删除) |
+| `ab0f1177f` | `chore` 新增 `tools/check_circular_deps.py`(Tarjan SCC)+ `.gitignore` 例外规则 |
+| `01d4a73d9` | `chore` `.githooks/pre-commit` 接入循环依赖阻断 + `docs/scripts/install_git_hooks.sh` 安装脚本文案 |
+| `c13b591d5` | `docs` 根 `AGENTS.md` 与 `cfc-backend/AGENTS.md` 写入「依赖注入纪律」(4.2.1 禁止循环依赖 / 4.2.4 `@Lazy` 纪律与静态检查),各 +31 行 |
+
+**未新增任何 `@Lazy`**:既有 21 处延迟注入点原样保留、未清理。
+
+### 9.2 六条硬验收(全部达标 · 实跑非声称)
+
+| # | 标准 | 结果 | 关键证据 |
+|---|---|---|---|
+| 1 | `mvn clean compile` 退出码 0 | ✅ | `MVN_EXIT=0`(Maven 3.8.7 + JDK8,1949 源文件,BUILD SUCCESS) |
+| 2 | 启动日志无 `form a cycle` | ✅ | 实测 `mvn spring-boot:run` → `Started CfcApplication in 13.672 seconds` / Tomcat 9082;1182 行日志中 `form a cycle`=0、`circular reference`=0、`BeanCurrentlyInCreationException`=0 |
+| 3 | `application.yml` 无该开关 | ✅ | `grep_exit=1`(零匹配);全仓配置扫描(排除 `.worktrees/`)零匹配 |
+| 4 | `check_circular_deps.py` eager 环 = 0 | ✅ | `✅ 未发现 eager 循环依赖` / `结论: ✅ eager 环 = 0(通过)` / exit 0;**修复前同脚本 exit 1 抓到真环,修复后归零**(自证有效) |
+| 5 | 现有单测不回归 | ✅ | 点名 3 类全绿:`CfPathwayEarnServiceTest` 32/32、`CfGrowthPathwayEarnServiceTest` 16/16、配额域 20/20;另 `MemberLevelQueryServiceTest` 9/9、`MembershipServiceTest` 17/17(合计 86 用例) |
+| 6 | 等级查询搬迁逐分支等价 | ✅ | 5 个方法体 `diff` 比对:3 个 IDENTICAL、2 个仅 `public→private`;四分支(PROVIDER→FREE / FAMILY 过期→FREE / 个人家庭取高 / 未知回退 1)源码行 + 9 个单测双背书 |
+
+**门禁有效性负向测试**:临时注入 eager 环 `TmpCycleProbeA↔B` → 检查器 exit 1 → pre-commit 成功阻断提交;探测文件已删除并复位(重新检查 exit 0)。钩子触发规则实测:`cfc-backend/**` 与 `tools/check_circular_deps.py` 触发,`cfc-web/**` 正确跳过。
+
+### 9.3 独立核验结论(第二个 opencode 会话复核):**approve-with-notes**
+
+核验方逐条独立实跑,10 项检查无一项依赖修复方自述。核心结论:**循环依赖真环已被从设计上打破、`allow-circular-references` 已移除且应用可无环启动。** 6 条硬验收全部达标。
+
+### 9.4 核验遗留 notes(转入技术债台账 D5–D7)
+
+1. **`MembershipService` 残留 3 份等级查询等价副本**(`resolveEffectiveLevel` / `getFamilyEffectiveLevel` / `getActiveFamilyMembership`,生产已无调用方,仅测试引用)→ 单边修改存在漂移风险,建议标记 `@Deprecated` 或收敛。
+2. **约束目前仅本地生效**:`core.hooksPath` 是本地仓库配置(不入库)+ 仓库 `.github/` 下无任何工作流调用 `check_circular_deps.py` → 建议补一条 CI 闸门,防 `--no-verify` 绕过。
+3. **`pom.xml:28` 的 `<skipTests>true</skipTests>` 使 `mvn test` 静默不执行**,是既有失败 `MembershipPaySwitchTest.createOrderWithPayType_virtualEnabled_configMissing_returnsNull`(自 `75627206a` 起即失败)长期未被 CI 暴露的根因。本次核验须显式 `-DskipTests=false` 才可见。
+
+---
+
+## 附录 A:核验方法说明
+
+**依赖图扫描**(本轮实证手段,可复现):
+1. `git archive origin/cfclub cfc-backend/src/main/java | tar -x` 导出远端源码到临时目录(不改动仓库)
+2. 脚本解析 1946 个 Java 文件 → 754 个 Bean → 2098 条注入边(字段 2079 eager / 19 lazy)
+3. 环检测:DFS 枚举全部基本环,分别在「仅 eager 边」与「全部边」两个图上运行
+4. **盲区已补**:第二轮扫描额外解析 setter 注入与构造器注入,结论不变(仍只有 1 个 eager 环)
+
+**局限说明**:脚本基于正则解析,理论上可能漏掉非常规注入(如 `ApplicationContext` 动态查找、`@Bean` 方法内 `new`、SpEL 注入)。因此**启动验证(`mvn spring-boot:run` 无 cycle)仍是最终门禁**,静态扫描作为前置快速拦截。
+
+---
+
+*附:本方案基于 `origin/cfclub` HEAD `be62f7465` 的源码逐一核验 + 全量离线依赖图扫描。V2 已更正 V1 中两处经实证证伪的判断。第九节记录实施与独立核验结果(2026-10-25)。*

+ 71 - 0
docs/运营推广设计/build/浠艾福_资源介绍_填写稿.md

@@ -0,0 +1,71 @@
+# 浠艾福 · 资源介绍(填写稿)
+
+> 使用说明:带【待补】的地方请按实际情况替换;带【边界】的表述建议保留,避免对方预期过高。
+
+---
+
+## 一、我 / 项目能提供的支持
+
+| 资源类型 | 具体内容 | 可验证依据 / 边界 |
+|---|---|---|
+| **咨询(最强项)** | ①五维主动健康方法论(身体·心理·认知·社交·成就)落地设计;②机构健康信息化方案设计(企业福利/社区/养老院/医院四类);③健康科普内容的循证与合规审校 | 已产出机构健康信息化合作方案;科普内容 54 条参考文献;调色板 ≤3 色规范;不作治疗宣称 |
+| **供应链** | 菌群测评(16S rRNA)、DAN 测评的检测通道与报告出具;报告结构化与解读 | 已跑通并有稳定定价(菌群 499/人次、DAN 199/份、陪跑 1,341/家/年) |
+| **人才** | 本人:30 年 IT 与医疗/央国企信息化经验,浠艾福主动健康研究院院长;内容策划与健康解读团队【待补人数与背景】 | 可承担方案设计、机构沟通、内容把关 |
+| **人脉** | 中国抗衰老促进会精神与心理分会(副主任)、双培强基工程研究院(副院长)、校友圈高知家庭网络、央国企信息化圈 | 可用于对接学术背书、机构决策人、区域资源 |
+| **市场** | 家庭健康用户基础与内容账号(care_family 小红书);高知家庭客群 | 浠艾福小程序累计用户约 394【按最新数据更新】 |
+| **渠道** | 企业福利 / 社区 / 养老院 / 医院四类机构通道(方案已成型,样板待落地) | 【边界】四类机构方案已设计,尚未形成规模化样板案例 |
+| **技术** | 个人电子报告(PDF)、小程序承载、内容生产工具链 | 【边界】团体健康看板、HIS/OA 对接、数据大屏属规划/定制,不作为现有能力承诺 |
+| **资金** | 可投入小规模资源支持样板项目启动 | 【边界】早期项目,不具备大规模投资能力 |
+
+---
+
+## 二、我希望得到的支持
+
+| 资源类型 | 具体需求 | 优先级 |
+|---|---|---|
+| **渠道(最急)** | 拥有企业 HR / 社区街道 / 养老机构 / 医院资源的渠道商、体检中心、健康管理公司、保险经纪 | 高 |
+| **人才** | 机构渠道 BD(能独立跑机构);健康管理师 / 营养师;B 端产品经理;实施交付专员 | 高 |
+| **技术** | ①团体健康看板开发;②与机构 HIS/OA 的 API 对接;③数据合规(个人信息保护、等保);④AI 报告解读 | 高 |
+| **人脉** | 医院健康管理中心、养老机构负责人、街道/社区主管、企业 HR 决策人;检验检测与学术背书资源 | 中高 |
+| **市场** | 区域机构客户导入;政府/街道购买服务渠道;保险公司健康服务采购 | 中高 |
+| **资金** | ①样板项目的启动/垫资;②机构业务独立核算所需的天使资金【额度待定】 | 中 |
+| **咨询** | 医疗合规与广告法审校;数据合规;机构招投标与定价策略 | 中 |
+
+---
+
+## 三、合作方式
+
+| 方式 | 具体形态 | 适合对象 | 说明 |
+|---|---|---|---|
+| **渠道分销** | 渠道商负责机构客户开发与签约,我方负责检测、报告与陪跑交付 | 有机构客户资源的渠道商 | 渠道结算价尚未最终确定,可先按项目单议 |
+| **项目合作** | 联合投标 / 组成整体解决方案,我方提供健康管理模块 | 系统集成商、信息化服务商 | 可与对方的平台、硬件、实施能力互补 |
+| **联合运营** | 与机构共建"健康管理中心/健康小屋",我方输出方法论、检测与内容,对方出场地的运营 | 社区、养老院、企业园区 | 收益按约定比例分配 |
+| **内容合作** | 科普内容授权、联合出品、专栏共建 | 媒体、医院宣传科、健康类平台 | 我方提供循证内容与合规把关 |
+| **技术合作** | 看板/接口联合开发、AI 解读模型共建 | 软件公司、AI 团队 | 可作为定制项目立项 |
+| **资源互换** | 客户互推、圈层互荐、活动共办 | 校友企业、商协会、健康机构 | 轻量起步,先试后深化 |
+| **股权/合资** | 机构健康业务独立成主体 | 战略投资人、产业方 | 可谈,需进一步沟通 |
+
+---
+
+## 四、我的标签
+
+**身份类**
+`#浠艾福主动健康研究院院长` `#双培强基工程研究院副院长` `#中国抗衰老促进会精神与心理分会副主任` `#30年IT与医疗信息化`
+
+**能力类**
+`#五维主动健康` `#机构健康管理` `#健康信息化` `#菌群检测` `#DAN测评` `#循证科普` `#健康管理方案设计`
+
+**资源类**
+`#企业福利健康` `#社区健康服务` `#养老健康` `#医院健康管理` `#校友经济` `#央国企信息化资源`
+
+**一句话标签(用于简介位)**
+> 用信息化方式,把机构的健康福利做成可持续的健康管理能力。
+
+---
+
+## 五、填写提醒
+
+1. **【待补】项**:团队人数与背景、小程序最新用户数、可投入资金量级 —— 这三项按实际情况补,不要留空也不要夸大。
+2. **"能提供"里资金一项建议保持"小规模"表述**:早期项目写"资金充足"反而会引起对方质疑,写"可支持样板启动"更可信。
+3. **技术一项务必保留边界说明**:团体看板与 API 对接目前是规划项,若对方按"已有系统"预期签约,后续交付会出问题。
+4. **渠道结算价未定**:如对方追问分润比例,先答"按项目单议",不要在表单里写死数字。