Selaa lähdekoodia

docs: add plans, specs, analysis docs for energy system, article system, wuxing sandbox, subPackages

New docs:
- Plans: module development plan, wuxing layout update, wuxing sandbox refactor, article publishing, energy system
- Specs: wuxing sandbox refactor design, body dimension energy, remaining dimensions energy
- Analysis: domain comparison (亲子 vs 个人健康/关系维护/康养/认知提升)
- product-plan/ directory
- Updated: pre-login discovery plan/spec, 需求分析文档, 小程序页面重设计划

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
User 3 kuukautta sitten
vanhempi
sitoutus
e4f146f65b

+ 276 - 0
docs/product-plan/阶段一-活动文章商品规划.md

@@ -0,0 +1,276 @@
+# 浠艾福阶段一:活动·文章·商品 产品规划
+
+**状态:** 产品规划(待开发评审)
+**关联:** 浠艾福家庭成长支持平台 Phase 1
+
+---
+
+## 一、定位与目标
+
+### 平台定位重申
+
+浠艾福是 **家庭幸福与成长支持平台**,不限于家庭教育。每个用户拥有独立的:健康档案、心理档案、成长档案、行为跟踪、财富档案。阶段一是整个平台的内容与消费入口。
+
+### 阶段一目标
+
+搭建 **内容→商品→交易** 的基础闭环,让用户能:
+1. 浏览**活动**信息并报名参与
+2. 阅读**文章**内容获取知识
+3. 浏览并购买**商品/服务**
+4. 为后续档案体系积累用户基础和内容资产
+
+---
+
+## 二、模块详解
+
+### 2.1 文章模块 · Article(🆕 新建)
+
+#### 定位
+平台内容引擎,承载健康科普、成长知识、家庭关系等内容。作为平台的"获客与粘性"入口。
+
+#### 数据结构
+
+```java
+Article {
+    id              Long        // PK
+    title           String      // 标题
+    summary         String      // 摘要
+    content         Text        // 正文(富文本 / Markdown)
+    coverImage      String      // 封面图
+    category        String      // 分类:health(健康)/psychology(心理)/growth(成长)/wealth(财富)/family(家庭)
+    tags            String      // 标签,逗号分隔
+    authorId        Long        // 作者(关联用户或运营)
+    authorName      String      // 作者名称
+    status          String      // draft / published / archived
+    publishedAt     DateTime    // 发布时间
+    viewCount       Integer     // 阅读数
+    likeCount       Integer     // 点赞数
+    collectCount    Integer     // 收藏数
+    createdAt       DateTime
+    updatedAt       DateTime
+}
+```
+
+**说明:**
+- `category` 与平台的五行域(action/mind/body/wisdom/wealth)和档案体系对齐
+- 内容由平台运营/服务商发布,用户只读
+- 后续可扩展:文章关联商品、文章关联活动
+
+#### API 设计
+
+| 端点 | 方法 | 说明 | 权限 |
+|------|------|------|------|
+| `/api/article/list` | POST | 文章列表(分页+分类筛选) | ALL |
+| `/api/article/detail` | POST | 文章详情 | ALL |
+| `/api/article/create` | POST | 发布文章 | admin/teacher |
+| `/api/article/update` | POST | 编辑文章 | admin/teacher(仅自己) |
+| `/api/article/delete` | POST | 删除文章 | admin |
+| `/api/article/my` | POST | 我的文章列表 | admin/teacher |
+
+#### 前端页面
+
+| 页面 | 说明 | 复用 |
+|------|------|------|
+| 发现页文章列表 | 在 `discover/index.vue` 增加文章 Tab | 复用现有发现页 |
+| 文章详情页 | 新建 `pages/article/detail.vue` | 🆕 |
+| 文章管理(Web) | 在 cfc-web 增加文章管理 | 🆕 |
+
+#### 用户流程
+
+```
+用户 → 发现页 → 文章列表 → 点击 → 文章详情页(阅读/点赞/收藏)
+运营/服务商 → Web管理端 → 文章管理 → 新建/编辑/发布
+```
+
+---
+
+### 2.2 活动模块 · Activity(🆕 新建 + 复用商品系统)
+
+#### 定位
+活动是平台服务的重要组成部分,作为商品的一种特殊类型(`product_type=activity`),但活动需要额外的排期、报名、签到等业务逻辑。
+
+#### 策略
+**复用 Product 实体**作为商品基础,**新建 Activity 实体**承载活动特有信息。
+
+```java
+// 已存在的 Product 实体(复用)
+Product {
+    id, name, description, coverImage, images,
+    price, memberPrice, stock, salesCount,
+    productType: "activity",           // ← 活动类型
+    vendorId, vendorName,              // ← 服务商信息
+    status, domain, memberEligible,
+    ...
+}
+
+// 新建:活动详情表
+Activity {
+    id              Long        // PK = Product.id(一对一)
+    productId       Long        // 关联 Product
+    location        String      // 活动地点
+    address         String      // 详细地址
+    startTime       DateTime    // 开始时间
+    endTime         DateTime    // 结束时间
+    deadline        DateTime    // 报名截止
+    maxParticipants Integer     // 人数上限
+    currentCount    Integer     // 当前报名人数
+    agenda          Text        // 活动议程(富文本)
+    hostName        String      // 主办方
+    hostPhone       String      // 主办方电话
+    status          String      // draft / open / full / closed / finished / cancelled
+    createdAt       DateTime
+    updatedAt       DateTime
+}
+
+// 新建:报名记录
+ActivityRegistration {
+    id              Long
+    activityId      Long
+    userId          Long
+    childId         Long?       // 为孩子报名时的孩子ID
+    orderNo         String      // 关联商品订单(如付费活动)
+    contactName     String      // 联系人
+    contactPhone    String      // 联系电话
+    participantCount Integer    // 参加人数
+    status          String      // registered / checked_in / cancelled
+    checkedInAt     DateTime?   // 签到时间
+    createdAt       DateTime
+}
+```
+
+**为什么新建 Activity 表而不是把所有字段塞进 Product:**
+- Product 是商品通用模型,不适合承载活动特有的时间/地点/报名逻辑
+- 活动需要独立的查询(按时间/地点/状态筛选)
+- 遵循单一职责,Product 管交易,Activity 管交付
+
+#### API 设计
+
+| 端点 | 方法 | 说明 | 权限 |
+|------|------|------|------|
+| `/api/activity/list` | POST | 活动列表(按时间/地点/状态筛选) | ALL |
+| `/api/activity/detail` | POST | 活动详情 | ALL |
+| `/api/activity/create` | POST | 新建活动(含 Product) | teacher/vendor |
+| `/api/activity/update` | POST | 编辑活动 | teacher/vendor(仅自己) |
+| `/api/activity/register` | POST | 报名活动 | parent |
+| `/api/activity/cancel` | POST | 取消报名 | parent |
+| `/api/activity/check-in` | POST | 签到 | teacher/vendor(扫码/手动) |
+| `/api/activity/my-registrations` | POST | 我的报名列表 | parent |
+| `/api/activity/my-activities` | POST | 我发布的活动 | teacher/vendor |
+
+#### 前端页面
+
+| 页面 | 说明 |
+|------|------|
+| 活动列表页 | 新建 `pages/activity/list.vue`(按时间线展示) |
+| 活动详情页 | 新建 `pages/activity/detail.vue`(含报名入口) |
+| 报名表单页 | 新建 `pages/activity/register.vue` |
+| 我的活动(家长) | 新建 `pages/parent/activities.vue` |
+| 活动管理(服务商) | 复用 Web 管理端 |
+
+#### 用户流程
+
+```
+服务商端:
+  服务商 → 创建活动(基本信息+排期+定价)→ 审核/发布
+
+客户端:
+  客户浏览活动列表 → 查看详情 → 报名(免费/付费)→ 签到
+  
+  活动结束后 → 服务商可录入参与记录 → 关联到参与者的行为跟踪
+```
+
+---
+
+### 2.3 商品模块 · Product(✅ 增强已有)
+
+#### 现状
+商品系统已完整实现,包括:
+
+| 组件 | 状态 | 说明 |
+|------|------|:----:|
+| `Product` 实体 | ✅ | 5种 productType: activity/course/physical/digital/service |
+| `ProductOrder` 实体 | ✅ | 含订单流水号、状态、支付 |
+| `ProductController` | ✅ | CRUD + 上下架 + 我的商品 |
+| `ProductOrderController` | ✅ | 下单/支付/取消/确认收货 |
+| `AdminController`(商品审核) | ✅ | 商品审核上下架 |
+| `VendorController` | ✅ | 服务商入驻+商品管理 |
+| 发现页 `discover/index.vue` | ✅ | 商品推荐流 |
+| 商城页 `shop/index.vue` | ✅ | 商品列表+筛选 |
+| 商品详情 `product-detail.vue` | ✅ | 商品详情页 |
+| 订单列表/详情 | ✅ | shop/order-list, shop/order-detail |
+| DatabaseInitializer 种子数据 | ✅ | 5 个种子商品覆盖 5 种类型 |
+
+#### 阶段一增强项
+
+| 增强项 | 说明 | 优先级 |
+|--------|------|:------:|
+| 商品详情页富文本描述 | 当前 Product 只有 description/intro,改为富文本编辑器 | P1 |
+| 多图上传 | 当前 coverImage + images(json),需要前端图片上传组件 | P1 |
+| 服务商商品管理页面 | Web端已有,小程序端增加服务商的商品管理入口 | P2 |
+| 商品评价功能 | 购买后评价,积累口碑 | P2 |
+| 套餐打包 | 多个商品组合为套餐(可复用现有 GuidePackage 逻辑) | P2 |
+
+---
+
+## 三、与现有系统的关系
+
+```
+┌──────────────────────────────────────────────────┐
+│                  浠艾福平台                        │
+├──────────┬──────────┬──────────┬─────────────────┤
+│  任务系统  │  测评系统  │  成长档案  │ Phase1 新增     │
+│  (已有)   │  (已有)   │  (已有)   │                 │
+│          │          │          │  文章 ──→ 内容引擎│
+│  积分心愿  │  规划师   │  家庭管理  │  活动 ──→ 服务交付│
+│  (已有)   │  (已有)   │  (已有)   │  商品 ──→ 交易闭环│
+└──────────┴──────────┴──────────┴─────────────────┘
+                   │
+                   ▼
+        ┌──────────────────┐
+        │  未来全人档案体系   │
+        │ 健康/心理/行为/财富│
+        └──────────────────┘
+```
+
+### 商品与活动的关键关系
+
+```
+Product(product_type=activity) ──1:1──→ Activity(活动详情)
+         │                                      │
+         │  ProductOrder                        │ ActivityRegistration
+         │  (交易:下单/支付)                     │ (交付:报名/签到)
+         ▼                                      ▼
+     交易已完成                            服务已完成
+```
+
+---
+
+## 四、分阶段实施建议
+
+### Sprint A — 商品增强(1-2周)
+- 商品详情富文本支持
+- 多图片上传
+- 服务商小程序端商品管理
+
+### Sprint B — 文章模块(1-2周)
+- Article 实体 + API(CRUD)
+- 文章列表/详情页
+- Web 端内容管理
+
+### Sprint C — 活动模块(2-3周)
+- Activity 实体 + ActivityRegistration
+- 活动 API(CRUD + 报名 + 签到)
+- 活动列表/详情/报名页
+- 服务商活动管理
+
+---
+
+## 五、关键决策记录
+
+| 序号 | 决策 | 理由 |
+|:----:|------|------|
+| D1 | 活动不独立建商品表,复用 Product(product_type=activity) | 避免重复的支付/订单逻辑,活动本质是可购买的服务 |
+| D2 | 但活动需要独立的 Activity 表存排期/地点/报名 | Product 不能承载活动特有逻辑,一对一关联 |
+| D3 | 文章从零新建,不塞入 Product | 文章不是商品,没有价格/交易属性 |
+| D4 | 文章分类对齐五行域(health/psychology/growth/wealth/family) | 与平台档案体系一致,后续可关联档案模块 |
+| D5 | 服务商可发布文章和活动(不限于admin) | 内容供给需要服务商参与,平台做审核把关 |

+ 15 - 15
docs/superpowers/plans/2026-06-04-pre-login-discovery.md

@@ -10,7 +10,7 @@
 
 ---
 
-### Task 1: 品牌更名 溪艾福→艾福
+### Task 1: 品牌更名 溪艾福→艾福
 
 **Files:**
 - Modify: `cfc-frontend/pages/index/parent-index.vue` (4处)
@@ -20,35 +20,35 @@
 - Modify: `cfc-frontend/utils/api.js` (1处)
 - Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java` (4处)
 
-- [ ] **Step 1: 替换所有前端「溪艾福」→「艾福」**
+- [ ] **Step 1: 替换所有前端「溪艾福」→「艾福」**
 
 Replace in parent-index.vue:
-- Line 68: 五行能量沙盘(溪艾福)→ 五行能量沙盘(艾福)
-- Line 125: 会员Banner(溪福俱乐部)→ 会员Banner(福俱乐部)
-- Line 1213: 溪福俱乐部 → 福俱乐部
-- Line 1249: 五行能量沙盘(溪艾福)→ 五行能量沙盘(艾福)
+- Line 68: 五行能量沙盘(溪艾福)→ 五行能量沙盘(艾福)
+- Line 125: 会员Banner(溪福俱乐部)→ 会员Banner(浠艾福俱乐部)
+- Line 1213: 溪福俱乐部 → 浠艾福俱乐部
+- Line 1249: 五行能量沙盘(溪艾福)→ 五行能量沙盘(艾福)
 
 Replace in child-index.vue:
-- Line 54: 五行能量沙盘(溪艾福)→ 五行能量沙盘(艾福)
-- Line 881: 五行能量沙盘(溪艾福)→ 五行能量沙盘(艾福)
+- Line 54: 五行能量沙盘(溪艾福)→ 五行能量沙盘(艾福)
+- Line 881: 五行能量沙盘(溪艾福)→ 五行能量沙盘(艾福)
 
 Replace in pages.json:
-- Line 329: 溪福俱乐部 → 福俱乐部
+- Line 329: 溪福俱乐部 → 浠艾福俱乐部
 
 Replace in profile.vue:
-- Line 52: 溪福俱乐部 → 福俱乐部
+- Line 52: 溪福俱乐部 → 浠艾福俱乐部
 
 Replace in api.js:
-- Line 722: 溪福俱乐部 → 福俱乐部
+- Line 722: 溪福俱乐部 → 浠艾福俱乐部
 
 Replace in DatabaseInitializer.java (comments only):
-- "溪福俱乐部" → "福俱乐部" (4 occurrences)
+- "溪福俱乐部" → "浠艾福俱乐部" (4 occurrences)
 
 - [ ] **Step 2: Commit**
 
 ```bash
 git add $(rtk list -u --modified -- cfc-frontend/ cfc-backend/)
-git commit -m "chore: 品牌名溪艾福/溪福俱乐部统一改为汐艾福/汐福俱乐部"
+git commit -m "chore: 品牌名溪艾福/溪福俱乐部统一改为浠艾福/浠艾福俱乐部"
 ```
 
 ---
@@ -209,7 +209,7 @@ export default {
 
 - [ ] **Step 2: 替换 parent-index.vue 内联沙盘为组件**
 
-Remove the inline wuxing-sandbox HTML+CSS block from parent-index.vue (the entire block from `<!-- 五行能量沙盘(艾福) -->` to its closing `</view>`), replace with:
+Remove the inline wuxing-sandbox HTML+CSS block from parent-index.vue (the entire block from `<!-- 五行能量沙盘(艾福) -->` to its closing `</view>`), replace with:
 
 ```vue
       <wuxing-sandbox mode="parent" badgeText="综合成长力 0%" @point-click="goToDomain" />
@@ -405,7 +405,7 @@ git commit -m "feat: App.vue 添加登录态TabBar切换逻辑(未登录隐藏
     <!-- Banner -->
     <view class="banner">
       <view class="banner-content">
-        <text class="banner-brand">🌟 艾福</text>
+        <text class="banner-brand">🌟 艾福</text>
         <text class="banner-title">Care Family Club</text>
         <text class="banner-subtitle">以五行五维为内核的家庭主动健康管理平台</text>
         <view class="banner-tags">

+ 569 - 0
docs/superpowers/plans/2026-06-06-module-development-plan.md

@@ -0,0 +1,569 @@
+# 全模块开发实施计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 根据已验证的需求文档,按优先级完成所有未实现/部分实现的功能模块开发
+
+**Architecture:** 前端 uni-app Vue 2(微信小程序),后端 Spring Boot + JPA + MySQL。后端 API 优先开发,前端页面随后跟进。新功能继承现有代码风格(无可选链、rpx 单位、JWT 认证)。
+
+**Tech Stack:** 前端 uni-app + Vue 2,后端 Spring Boot + JPA + MySQL,图表 uCharts,富文本编辑器 uParse
+
+---
+
+## 一、现状总览
+
+### 已完成(本轮会话)
+- ✅ 品牌改名:汐艾福→浠艾福,汐福俱乐部→浠艾福俱乐部(8文件)
+- ✅ TabBar重构:首页/发现/商城/我的 → 首页/身体/心智/行动/我的(5 tabs)
+- ✅ 新建占位页:pages/body/index.vue,pages/mind/index.vue
+- ✅ 未登录可浏览:入口页 = discover/index(非 login)
+- ✅ 自定义底部导航:未登录时2-tab(首页+行动)
+- ✅ 后端编译修复:StreakService(mvn clean compile)
+
+### 需求文档状态
+| 文档 | 模块 | 未实现/部分实现项 |
+|------|------|-------------------|
+| task-requirements.md | 任务管理 | TASK-016(批量操作)❌ |
+| focus-requirements.md | 专注训练 | FOCUS-005/006/007 ❌,FOCUS-004/008 ⚠️ |
+| guide-requirements.md | 成长规划师套餐 | 全部后端+前端 ❌(不含已实现的需求分析文档中的规划师基础功能) |
+| badge-requirements.md | 勋章系统 | BADGE-001~006 全部 ❌ |
+| report-requirements.md | 报表统计 | REPORT-001~004 全部 ❌ |
+| dan-requirements.md | DAN测评 | DAN-001~019 大部分 ❌ |
+| requirements-analysis.md | 规划师基础功能 | 全部 ✅(已在之前实现) |
+
+---
+
+## 二、优先级分组
+
+### P0 - 阻塞项(必须先完成)
+1. Tab图标资源文件缺失
+2. 身体/心智Tab占位页补全内容
+
+### P1 - 核心功能
+3. 专注训练模块剩余功能(记录分析+成果展示+排行)
+4. 勋章系统全功能(定义→解锁→展示→排行)
+5. 报表统计模块(个人+家庭统计+趋势图表)
+6. 成长规划师套餐模块(后端API+前端管理)
+
+### P2 - 高级功能
+7. DAN测评模块(大部分P0/P1需求)
+8. 任务批量操作(TASK-016)
+9. 训练提醒与激励(FOCUS-007)
+
+### P3 - 扩展功能
+10. DAN测评集成支付(DAN-016)
+11. 数据导出PDF/Excel
+12. 同龄对比数据
+
+---
+
+## 三、实施任务分解
+
+---
+
+### Task 1: Tab图标资源文件(P0)
+
+**阻断了** 已部署的5-tab自定义导航栏显示(目前引用不存在的图标文件)
+
+**Files:**
+- Create: `cfc-frontend/static/tab-body.png`
+- Create: `cfc-frontend/static/tab-body-active.png`
+- Create: `cfc-frontend/static/tab-mind.png`
+- Create: `cfc-frontend/static/tab-mind-active.png`
+- Create: `cfc-frontend/static/tab-action.png`
+- Create: `cfc-frontend/static/tab-action-active.png`
+- Create: `cfc-frontend/static/tab-home.png`
+- Create: `cfc-frontend/static/tab-home-active.png`
+- Create: `cfc-frontend/static/tab-profile.png`
+- Create: `cfc-frontend/static/tab-profile-active.png`
+
+- [ ] **Step 1: 确认已有图标**
+
+检查 `cfc-frontend/static/` 下已有 `logo.png`。从设计稿或在线资源生成24x24px PNG图标:
+- tab-home, tab-home-active:首页/房子图标
+- tab-body, tab-body-active:身体/健康图标
+- tab-mind, tab-mind-active:心智/大脑图标
+- tab-action, tab-action-active:行动/靶心图标
+- tab-profile, tab-profile-active:我的/人物图标
+
+生成规则:未选中 = 灰色线描,选中 = 橙色填充(主色 #F97316)
+
+- [ ] **Step 2: 生成或创建占位图标**
+
+如果无设计资源,创建纯色简约占位图标(圆形+文字缩写)确保自定义tab-bar不报错。
+
+- [ ] **Step 3: 验证**
+
+检查 `custom-tab-bar/index.vue` 中所有 `icon` 和 `selectedIcon` 路径对应的文件均存在。
+
+---
+
+### Task 2: 身体Tab内容页(P0)
+
+**Files:**
+- Modify: `cfc-frontend/pages/body/index.vue`
+
+身体/健康Tab页。展示健康相关内容入口(步数、运动、饮食记录等)。
+
+- [ ] **Step 1: 设计身体Tab页面布局**
+
+参考 `pages/index/index.vue` 风格,从上到下:
+- 顶部轮播/推荐区(健康文章推荐)
+- 功能入口网格(4-6个):我的运动、饮食记录、睡眠监测、健康知识等
+- 底部占位(后续功能)
+
+- [ ] **Step 2: 实现页面 UI**
+
+```vue
+<template>
+  <view class="body-page">
+    <!-- 顶部Banner -->
+    <swiper class="banner" indicator-dots autoplay interval="3000">
+      <swiper-item v-for="(item, i) in banners" :key="i">
+        <image class="banner-img" :src="item.image" mode="scaleToFill" />
+      </swiper-item>
+    </swiper>
+
+    <!-- 功能入口网格 -->
+    <view class="grid">
+      <view class="grid-item" v-for="(item, i) in features" :key="i" @click="navigateTo(item.path)">
+        <image class="grid-icon" :src="item.icon" mode="aspectFit" />
+        <text class="grid-label">{{ item.label }}</text>
+      </view>
+    </view>
+
+    <!-- 健康资讯列表 -->
+    <view class="section">
+      <text class="section-title">健康知识</text>
+      <view class="article-card" v-for="(a, i) in articles" :key="i">
+        <text class="article-title">{{ a.title }}</text>
+        <text class="article-desc">{{ a.desc }}</text>
+      </view>
+    </view>
+  </view>
+</template>
+```
+
+- [ ] **Step 3: 添加脚本+样式**
+
+使用 rpx 单位,主色调 #F97316,遵循现有首页样式风格。底部预留 safe-area。
+
+- [ ] **Step 4: 验证**
+
+`lsp_diagnostics` 清洁,页面在真机/模拟器可正常渲染,Tab切换流畅。
+
+---
+
+### Task 3: 心智Tab内容页(P0)
+
+**Files:**
+- Modify: `cfc-frontend/pages/mind/index.vue`
+
+心智/心理健康Tab页。
+
+- [ ] **Step 1: 设计心智Tab页面布局**
+
+- 顶部功能区:心理测评入口、情绪日记
+- 文章列表:心理教育/成长文章,分类筛选
+- 底部:心理热线/求助入口
+
+- [ ] **Step 2: 实现页面 UI**
+
+风格参考body页面,保持视觉一致性。
+
+- [ ] **Step 3: 验证**
+
+`lsp_diagnostics` 清洁。
+
+---
+
+### Task 4: 专注训练 - 训练记录与数据分析(P1)
+
+**需求文档:** focus-requirements.md
+**FOCUS-004/005/006** - 训练记录存储、数据分析、成果展示
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/GameService.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/GameRecord.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/repository/GameRecordRepository.java`
+- Create: `cfc-frontend/pages/games/records.vue`
+- Create: `cfc-frontend/pages/games/statistics.vue`
+
+- [ ] **Step 1: 创建 GameRecord 实体**
+
+```java
+@Entity
+@Table(name = "game_record")
+public class GameRecord {
+    @Id
+    @GeneratedValue(strategy = GenerationType.IDENTITY)
+    private Long id;
+    private Long childId;
+    private String gameCode;
+    private Integer score;
+    private Integer duration;       // 秒
+    private String level;           // easy/medium/hard
+    private Integer rank;
+    private Integer pointsEarned;
+    private Date completedAt;
+    private Date createdAt;
+    private Date updatedAt;
+}
+```
+
+- [ ] **Step 2: 创建 GameRecordRepository**
+
+```java
+public interface GameRecordRepository extends JpaRepository<GameRecord, Long> {
+    List<GameRecord> findByChildIdOrderByCompletedAtDesc(Long childId);
+    List<GameRecord> findByChildIdAndGameCodeOrderByCompletedAtDesc(Long childId, String gameCode);
+    Optional<GameRecord> findTopByChildIdAndGameCodeOrderByScoreDesc(Long childId, String gameCode);
+    Long countByChildId(Long childId);
+    Long countByChildIdAndCompletedAtBetween(Long childId, Date start, Date end);
+
+    @Query("SELECT AVG(g.score) FROM GameRecord g WHERE g.childId = :childId AND g.gameCode = :gameCode")
+    Double avgScoreByChildIdAndGameCode(@Param("childId") Long childId, @Param("gameCode") String gameCode);
+}
+```
+
+- [ ] **Step 3: 实现训练统计 API**
+
+`GameController` 新增:
+- `POST /api/games/record` - 记录成绩(孩子)
+- `GET /api/games/records/{childId}` - 记录列表(分页)
+- `GET /api/games/best/{childId}/{gameCode}` - 最佳成绩
+- `GET /api/games/statistics/{childId}` - 训练统计(总次数、平均分、趋势)
+
+- [ ] **Step 4: 实现训练记录前端页面 `pages/games/records.vue`**
+
+列表展示历史训练记录,支持按游戏筛选。
+
+- [ ] **Step 5: 实现训练统计前端页面 `pages/games/statistics.vue`**
+
+数据卡片 + uCharts趋势图表,展示总分、平均分、最佳成绩。
+
+- [ ] **Step 6: 后端 API 榜单**
+
+`GET /api/games/ranking/{gameCode}` - 游戏排行榜
+`GET /api/games/ranking/family/{familyId}` - 家庭排行榜
+
+- [ ] **Step 7: 验证**
+
+后端编译通过,API 返回正确数据,前端图表渲染正常。
+
+---
+
+### Task 5: 勋章系统(P1)
+
+**需求文档:** badge-requirements.md
+**BADGE-001~006** - 勋章定义、解锁、授予、展示、有效期、排行
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/Badge.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/ChildBadge.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/repository/BadgeRepository.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/repository/ChildBadgeRepository.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/BadgeService.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/BadgeController.java`
+- Create: `cfc-frontend/pages/badges/badges.vue`
+- Create: `cfc-frontend/pages/badges/detail.vue`
+- Create: `cfc-frontend/pages/badges/library.vue`
+
+- [ ] **Step 1: 创建 Badge 实体**
+
+对应 `badge` 表,字段:id, badgeId(唯一标识), name, description, icon, category(学习/运动/家务/阅读/打卡/综合), level(铜牌/银牌/金牌/钻石), rarity(普通/稀有/史诗/传说), triggerType(task_count/streak_days/points_total/reward_count/custom), threshold, needApproval, expireDays, sortOrder, isActive, createdAt, updatedAt。
+
+- [ ] **Step 2: 创建 ChildBadge 实体**
+
+对应 `child_badge` 表,字段:id, childId, badgeId, earnedAt, expireAt, status(1有效/0过期/-1撤销), isFavorite, createdAt, updatedAt。联合唯一索引 (child_id, badge_id)。
+
+- [ ] **Step 3: 创建 BadgeService 实现**
+
+方法:
+- 创建/更新/删除勋章定义
+- 检查并授予勋章(按 triggerType 检查条件)
+- 定时任务:勋章过期检查
+- 获取孩子的勋章列表
+- 收藏/取消收藏
+
+- [ ] **Step 4: 实现勋章管理 API**
+
+`BadgeController`:
+- `POST /api/badges` - 创建勋章(管理员)
+- `GET /api/badges` - 列表(公开)
+- `GET /api/badges/{id}` - 详情(公开)
+- `PUT /api/badges/{id}` - 编辑(管理员)
+- `DELETE /api/badges/{id}` - 删除(管理员)
+- `POST /api/badges/grant` - 手动授予(家长/管理员)
+- `POST /api/badges/check` - 检查并授予(系统)
+- `GET /api/badges/child/{childId}` - 孩子勋章列表
+- `PUT /api/badges/{id}/favorite` - 收藏切换
+- `GET /api/badges/statistics/child/{childId}` - 统计
+- `GET /api/badges/statistics/family/{familyId}` - 家庭统计
+- `GET /api/badges/ranking/family/{familyId}` - 家庭排行
+
+- [ ] **Step 5: 实现勋章墙前端页面 `pages/badges/badges.vue`**
+
+展示已获得勋章,按分类/时间排列。未获得显示锁定。
+
+- [ ] **Step 6: 实现勋章详情页 `pages/badges/detail.vue`**
+
+展示勋章名称、描述、获得时间、稀有度、等级。
+
+- [ ] **Step 7: 实现勋章库页面 `pages/badges/library.vue`**
+
+浏览所有勋章定义,区分已获得/未获得。
+
+- [ ] **Step 8: 验证**
+
+后端测试 API 正确,前端勋章墙渲染正常,收藏功能可用。
+
+---
+
+### Task 6: 报表统计模块(P1)
+
+**需求文档:** report-requirements.md
+**REPORT-001~004** - 个人任务统计、家庭统计、积分明细、成长趋势
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/ReportController.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/ReportService.java`
+- Create: `cfc-frontend/pages/report/child.vue`
+- Create: `cfc-frontend/pages/report/family.vue`
+- Create: `cfc-frontend/pages/report/trend.vue`
+
+- [ ] **Step 1: 实现 ReportService 统计逻辑**
+
+方法:
+- `getChildTaskStats(childId, startDate, endDate, category)` - 个人任务统计:任务总数、完成数、超时数、驳回数、完成率、平均耗时
+- `getChildPointsStats(childId, startDate, endDate)` - 个人积分统计:余额、获得、扣除、净变化、各来源占比
+- `getFamilyTaskStats(familyId, startDate, endDate)` - 家庭任务统计
+- `getFamilyRanking(familyId)` - 家庭排行
+- `getChildTrend(childId, granularity)` - 成长趋势数据
+
+- [ ] **Step 2: 实现报表 API**
+
+`ReportController`:
+- `GET /api/reports/child/{childId}/tasks` - 个人任务统计
+- `GET /api/reports/child/{childId}/points` - 个人积分统计
+- `GET /api/reports/child/{childId}/trend` - 个人成长趋势
+- `GET /api/reports/family/{familyId}/tasks` - 家庭任务统计
+- `GET /api/reports/family/{familyId}/ranking` - 家庭排行
+
+- [ ] **Step 3: 实现个人统计前端页面 `pages/report/child.vue`**
+
+数字卡片 + 条件筛选 + 列表展示。
+
+- [ ] **Step 4: 实现家庭统计页面 `pages/report/family.vue`**
+
+家庭概览卡片 + 孩子对比表格。
+
+- [ ] **Step 5: 实现成长趋势页面 `pages/report/trend.vue`**
+
+uCharts 折线图 + 柱状图,支持日/周/月粒度切换。
+
+- [ ] **Step 6: 验证**
+
+后端数据正确性验证,前端图表渲染正常。
+
+---
+
+### Task 7: 成长规划师套餐模块(P1)
+
+**需求文档:** guide-requirements.md
+后端服务类型管理、服务内容管理(富文本)、套餐模板、规划师套餐。
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/ServiceType.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/ServiceContent.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/GuidePackageTemplate.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/PackageTemplateService.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/GuidePackage.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/GuidePackageService.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/GuideService.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/GuideController.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/AdminServiceController.java`
+- Modify: `cfc-frontend/pages/guide/packages/index.vue`
+- Modify: `cfc-frontend/pages/guide/packages/create.vue`
+
+- [ ] **Step 1: 创建服务类型/服务内容实体**
+
+`ServiceType`:id, name, icon, description, sortOrder, status(ACTIVE/DISABLED)
+`ServiceContent`:id, typeId, title, content(Html), duration, ageRange, price, status(DRAFT/ACTIVE)
+
+- [ ] **Step 2: 创建套餐模板/规划师套餐实体**
+
+`GuidePackageTemplate`:id, name, validityMonths, referencePrice, minPrice, description, status
+`PackageTemplateService`:id, templateId, contentId, quantity, unit
+`GuidePackage`:id, teacherId, templateId, name, price, validityMonths, description, status(DRAFT/PUBLISHED/PAUSED), salesCount
+`GuidePackageService`:id, packageId, contentId, quantity, unit
+
+- [ ] **Step 3: 实现服务管理 API**
+
+`AdminServiceController`:
+- `GET/POST/PUT/DELETE /api/admin/service-types`
+- `GET/POST/PUT/DELETE /api/admin/service-contents`
+
+- [ ] **Step 4: 实现套餐管理 API**
+
+`GuideController`:
+- `GET /api/guide/package-templates` - 模板列表
+- `GET /api/guide/package-templates/{id}` - 模板详情(含服务项)
+- `GET /api/guide/packages` - 我的套餐
+- `POST /api/guide/packages` - 创建套餐
+- `PUT /api/guide/packages/{id}` - 更新套餐
+- `PUT /api/guide/packages/{id}/status` - 更新状态
+- `DELETE /api/guide/packages/{id}` - 删除套餐
+
+- [ ] **Step 5: 更新套餐前端页面**
+
+`pages/guide/packages/index.vue` - 展示套餐列表,支持状态筛选
+`pages/guide/packages/create.vue` - 选择模板、自定义名称价格、发布
+
+- [ ] **Step 6: 验证**
+
+后端编译通过,套餐CRUD流程完整,前端列表/创建/详情页面正常。
+
+---
+
+### Task 8: DAN测评模块(P1/P2)
+
+**需求文档:** dan-requirements.md
+**DAN-001~015** - 测评包管理、指标库、结果录入、查询、趋势、建议
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/DanPackage.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/DanIndicator.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/DanIndicatorTemplate.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/repository/DanPackageRepository.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/repository/DanIndicatorRepository.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/DanService.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/DanController.java`
+- Create: `cfc-frontend/pages/dan/result.vue`
+- Create: `cfc-frontend/pages/dan/history.vue`
+
+- [ ] **Step 1: 创建测评包+指标库实体**
+
+`DanPackage`:id, packageCode(A1/A2/B1-B6), packageName, ageRangeMin, ageRangeMax, category, indicators(JSON), isActive
+`DanIndicator`:id, indicatorCode, indicatorName, dimension(COG/EMI/SOC/PHY), description, levelThresholds(JSON), sortOrder, isActive
+`DanIndicatorTemplate`:id, indicatorCode, templateId, weight, isActive
+
+- [ ] **Step 2: 实现测评包/指标库 API**
+
+- `GET/POST/PUT/DELETE /api/dan/packages`
+- `GET/POST/PUT/DELETE /api/dan/indicators`
+- `GET/POST/PUT/DELETE /api/dan/indicator-templates`
+
+- [ ] **Step 3: 实现测评结果 API**
+
+利用已有的 `DanAssessmentResult` 实体:
+- `POST /api/dan/assessments` - 录入结果
+- `GET /api/dan/assessments?childId=` - 结果列表
+- `GET /api/dan/assessments/{id}` - 结果详情
+- `GET /api/dan/assessments/{id}/trend` - 历史趋势
+
+- [ ] **Step 4: 实现指标等级计算+成长建议生成**
+
+`DanService.calculateLevel(score, thresholds)` - 等级计算
+`DanService.generateSuggestions(indicatorScores)` - 按指标生成建议文本
+
+- [ ] **Step 5: 实现任务自动推荐**
+
+`DanService.recommendTasks(childId, assessmentId)` - 根据低分指标查询关联任务模板
+
+- [ ] **Step 6: 实现测评结果前端页面**
+
+`pages/dan/result.vue` - 展示测评结果、指标得分雷达图、等级、成长建议
+`pages/dan/history.vue` - 历史测评列表、趋势对比
+
+- [ ] **Step 7: 验证**
+
+完整的测评流程:录入→等级计算→展示→建议→推荐任务
+
+---
+
+### Task 9: 任务批量操作 TASK-016(P2)
+
+**需求文档:** task-requirements.md §TASK-016
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/TaskController.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java`
+- Modify: `cfc-frontend/pages/tasks/tasks.vue`
+
+- [ ] **Step 1: 后端批量操作 API**
+
+`TaskController` 新增:
+- `POST /api/tasks/batch-create` - 批量创建任务
+- `PUT /api/tasks/batch-status` - 批量更新状态(完成/暂停/删除)
+- `PUT /api/tasks/batch-assign` - 批量分配孩子
+
+- [ ] **Step 2: 前端批量操作 UI**
+
+任务列表页增加多选模式 → 底部操作栏(批量完成/批量删除/批量分配)
+
+- [ ] **Step 3: 验证**
+
+批量选中的任务状态正确更新,不可恢复的删除操作有确认弹窗。
+
+---
+
+### Task 10: 训练提醒与激励 FOCUS-007(P2)
+
+**需求文档:** focus-requirements.md §2.7
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/scheduler/TrainingReminderScheduler.java`
+- Modify: `cfc-frontend/pages/games/index.vue`
+
+- [ ] **Step 1: 每日训练提醒定时任务**
+
+`TrainingReminderScheduler` - 每日定时检查昨日未训练的孩子,发送订阅消息。
+
+- [ ] **Step 2: 连续训练激励逻辑**
+
+查询 `game_record` 表统计连续天数,达到里程碑(7天/14天/30天)发放额外积分。
+
+- [ ] **Step 3: 前端展示激励信息**
+
+游戏列表页顶部展示连续训练天数、下次激励目标。
+
+---
+
+## 四、预计工作量
+
+| 任务 | 预估工时 | 依赖 |
+|------|----------|------|
+| Task 1: Tab图标 | 0.5h | 无 |
+| Task 2: 身体Tab | 2h | 无 |
+| Task 3: 心智Tab | 2h | 无 |
+| Task 4: 训练记录分析 | 6h | 无 |
+| Task 5: 勋章系统 | 10h | 无 |
+| Task 6: 报表统计 | 8h | 任务/积分数据 |
+| Task 7: 规划师套餐 | 12h | 无 |
+| Task 8: DAN测评 | 16h | 任务模板 |
+| Task 9: 批量操作 | 3h | 任务模块 |
+| Task 10: 训练提醒 | 3h | 训练记录 |
+
+**总计预估:约 62.5h**
+
+---
+
+## 五、风险与注意事项
+
+1. **Tab图标** - 如果无设计资源,先用纯色占位图标,收集后可替换
+2. **富文本编辑器** - 服务内容管理(guide-requirements.md 2.1.2)依赖 uParse 或 uni-app 富文本组件,小程序环境对 HTML 支持有限
+3. **勋章触发引擎** - BADGE-003 的定时任务与事件触发双模式,需要考虑事务一致性和性能
+4. **图表组件** - 小程序端 uCharts 兼容性需要验证,备选方案:f2 或 echarts 微信版
+5. **DAN测评** - 涉及较多实体关联,建议先完成数据模型定义再开发API
+6. **Shop(行动Tab)** - 已有 pages/shop/index.vue,当前未登录用户可浏览(products API 公开),无需额外修改
+
+---
+
+## 六、验证标准
+
+每个Task完成后需满足:
+- [ ] `lsp_diagnostics` 清洁(无 error)
+- [ ] 后端 `mvn clean compile` 成功
+- [ ] 核心业务流程端到端可用
+- [ ] 已有功能未受影响(回归)

+ 335 - 0
docs/superpowers/plans/2026-06-08-wuxing-pentagram-layout-update.md

@@ -0,0 +1,335 @@
+# 五角星布局 & 五维映射修正 — 实现计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 将前端五行能量沙盘的五角星布局与后端五维映射修正为新的五维五行体系(智·金→富·水→行·木→心·火→身·土),并更新 README 品牌理念表。
+
+**Architecture:** 仅涉及前端 `wuxing-sandbox.vue` 模板/CSS/Canvas 映射 + 后端现有 EnergyService 中的维度变量命名对齐 + README 品牌理念表更新。不新增表或 API。
+
+**Tech Stack:** uni-app Vue 2 + Canvas 2D / Spring Boot / Markdown
+
+**设计依据:** `docs/superpowers/specs/2026-06-08-five-dimension-wuxing-philosophy.md`
+
+---
+
+## 变更前后映射对照
+
+### 五角星顶点布局
+
+| 位置 | CSS class | 当前(old) | 新(new) | 五行 | 颜色 |
+|------|-----------|-----------|---------|------|------|
+| 最左侧 (top-left, 26% 13%) | `star-point--ul` | 身(body) | **行(action)** | 木 | 绿 #4CAF50 |
+| 顶部 (top, 3% 50%) | `star-point--top` | 心(mind) | 心(mind) ——不变—— | 火 | 橙 #FF6B35 |
+| 最右侧 (top-right, 26% 87%) | `star-point--ur` | 财/富(wealth) | **身(body)** | 土 | 棕 #8D6E63 |
+| 右下 (bottom-right, 73% 75%) | `star-point--lr` | 行(action) | **智(wisdom)** | 金 | 黄 #FFD700 |
+| 左下 (bottom-left, 73% 25%) | `star-point--ll` | 智(wisdom) | **富(wealth)** | 水 | 蓝 #42A5F5 |
+
+### Canvas 星臂索引映射
+
+| Canvas i | 对应星臂位置 | 当前(old) ts | 新(new) ts | 当前(old) color | 新(new) color |
+|:--------:|:----------:|:------------:|:----------:|:--------------:|:------------:|
+| 0 | top-left | animBody | **animAction** | 木·绿(76,175,80) | 木·绿 ——不变—— |
+| 1 | top | animMind | animMind ——不变—— | 火·橙(255,107,53) | 火·橙 ——不变—— |
+| 2 | top-right | animWealth | **animBody** | 土·棕(141,110,99) | 土·棕 ——不变—— |
+| 3 | bottom-right | animAction | **animWisdom** | 金·黄(255,215,0) | 金·黄 ——不变—— |
+| 4 | bottom-left | animWisdom | **animWealth** | 水·蓝(66,165,245) | 水·蓝 ——不变—— |
+
+### 维度详情条 & CSS dot 样式
+
+| 顺序 | 当前(old) | 新(new) |
+|:----:|:---------:|:-------:|
+| 第1项 | 🌿身 (dim-item--wood) | **🌿行 (dim-item--wood)** |
+| 第2项 | 🔥心 (dim-item--fire) | 🔥心 (dim-item--fire)——不变—— |
+| 第3项 | 🌏智 (dim-item--earth) | **🌏身 (dim-item--earth)** |
+| 第4项 | ⚡行 (dim-item--metal) | **⚡智 (dim-item--metal)** |
+| 第5项 | 💧富 (dim-item--water) | **💧富 (dim-item--water)** |
+
+---
+
+## 文件改动清单
+
+| 文件 | 改动类型 | 说明 |
+|:----|:--------:|------|
+| `cfc-frontend/components/wuxing-sandbox.vue` | 修改 | 模板顶点映射、Canvas 颜色/ts 映射、CSS 样式一致性 |
+| `cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java` | 修改 | 维度注释/变量名对齐(逻辑不变) |
+| `cfc-backend/src/main/java/com/etotem/cfc/dto/EnergySandboxDTO.java` | 不改 | DTO 字段按名称访问,顺序无关 |
+| `cfc-backend/src/main/java/com/etotem/cfc/dto/MemberEnergyDTO.java` | 不改 | 同上 |
+| `README.md` | 修改 | 品牌理念表格五行 icon 对齐新映射 |
+
+---
+
+### Task 1: 修正前端 wuxing-sandbox.vue 模板 —— 顶点标签 & 样式
+
+**Files:**
+- Modify: `cfc-frontend/components/wuxing-sandbox.vue:17-52` (template)
+- Modify: `cfc-frontend/components/wuxing-sandbox.vue:752-756` (CSS)
+
+- [ ] **Step 1: 更新 HTML 模板中的五个顶点标签和点击回调**
+
+将以下代码(旧映射):
+```html
+<view class="star-point star-point--ul" @click="onPointClick('body')">
+  <view class="point-dot point-dot--wood"><text class="point-icon">🌿</text></view>
+  <text class="point-label">身</text>
+</view>
+<view class="star-point star-point--top" @click="onPointClick('mind')">
+  <view class="point-dot point-dot--fire"><text class="point-icon">🔥</text></view>
+  <text class="point-label">心</text>
+</view>
+<view class="star-point star-point--ur" @click="onPointClick('wealth')">
+  <view class="point-dot point-dot--earth"><text class="point-icon">🌏</text></view>
+  <text class="point-label">财</text>
+</view>
+<view class="star-point star-point--lr" @click="onPointClick('action')">
+  <view class="point-dot point-dot--metal"><text class="point-icon">⚡</text></view>
+  <text class="point-label">行</text>
+</view>
+<view class="star-point star-point--ll" @click="onPointClick('wisdom')">
+  <view class="point-dot point-dot--water"><text class="point-icon">💧</text></view>
+  <text class="point-label">智</text>
+</view>
+```
+
+改为(新映射):
+```html
+<view class="star-point star-point--ul" @click="onPointClick('action')">
+  <view class="point-dot point-dot--wood"><text class="point-icon">🌿</text></view>
+  <text class="point-label">行</text>
+</view>
+<view class="star-point star-point--top" @click="onPointClick('mind')">
+  <view class="point-dot point-dot--fire"><text class="point-icon">🔥</text></view>
+  <text class="point-label">心</text>
+</view>
+<view class="star-point star-point--ur" @click="onPointClick('body')">
+  <view class="point-dot point-dot--earth"><text class="point-icon">🌏</text></view>
+  <text class="point-label">身</text>
+</view>
+<view class="star-point star-point--lr" @click="onPointClick('wisdom')">
+  <view class="point-dot point-dot--metal"><text class="point-icon">⚔️</text></view>
+  <text class="point-label">智</text>
+</view>
+<view class="star-point star-point--ll" @click="onPointClick('wealth')">
+  <view class="point-dot point-dot--water"><text class="point-icon">💧</text></view>
+  <text class="point-label">富</text>
+</view>
+```
+
+- [ ] **Step 2: 更新维度详情条(dimension-bar)**
+
+将以下代码:
+```html
+<view class="dim-item dim-item--wood">🌿身 {{ activeScores[0] }}%</view>
+<view class="dim-item dim-item--fire">🔥心 {{ activeScores[1] }}%</view>
+<view class="dim-item dim-item--earth">🌏智 {{ activeScores[2] }}%</view>
+<view class="dim-item dim-item--metal">⚡行 {{ activeScores[3] }}%</view>
+<view class="dim-item dim-item--water">💧富 {{ activeScores[4] }}%</view>
+```
+
+改为:
+```html
+<view class="dim-item dim-item--wood">🌿行 {{ activeScores[0] }}%</view>
+<view class="dim-item dim-item--fire">🔥心 {{ activeScores[1] }}%</view>
+<view class="dim-item dim-item--earth">🌏身 {{ activeScores[2] }}%</view>
+<view class="dim-item dim-item--metal">⚡智 {{ activeScores[3] }}%</view>
+<view class="dim-item dim-item--water">💧富 {{ activeScores[4] }}%</view>
+```
+
+> 注意:`activeScores` 数组索引保持 `[body, mind, wisdom, action, wealth]` 不变。Canvas 渲染通过 `ts` 数组取值时,映射会通过在 `renderStar` 中调整 `ts` 的索引顺序来实现(见 Task 2)。
+
+---
+
+### Task 2: 修正 Canvas 渲染 —— 颜色映射 & ts 数组索引
+
+**Files:**
+- Modify: `cfc-frontend/components/wuxing-sandbox.vue:57, 88-94, 322-337, 448-458, 513`
+
+**关键逻辑:** Canvas 的 `renderStar()` 用 `for i = 0..4` 遍历,每个 i 对应一个星臂几何位置。当前 `ts` 数组 `[animBody, animMind, animWealth, animAction, animWisdom]` 的索引顺序必须与新的顶点映射一致。
+
+- [ ] **Step 1: 修正 Canvas 五色数组的索引映射**
+
+只需将 colors 数组的注释对齐(颜色值不变),同时调整 `ts` 数组的取值索引以匹配新布局:
+
+```javascript
+// 在 renderStar() 方法中:
+
+// 当前(旧):
+var ts = [
+  this.animBody,     // i=0 → top-left (旧:身)
+  this.animMind,     // i=1 → top (心——不变)
+  this.animWealth,   // i=2 → top-right (旧:财)
+  this.animAction,   // i=3 → bottom-right (旧:行)
+  this.animWisdom    // i=4 → bottom-left (旧:智)
+]
+
+// 改为(新):
+var ts = [
+  this.animAction,   // i=0 → top-left (新:行·木)
+  this.animMind,     // i=1 → top (新:心·火——不变)
+  this.animBody,     // i=2 → top-right (新:身·土)
+  this.animWisdom,   // i=3 → bottom-right (新:智·金)
+  this.animWealth    // i=4 → bottom-left (新:富·水)
+]
+```
+
+- [ ] **Step 2: 修正 colors 数组注释对齐**
+
+```javascript
+var colors = [
+  { r: 76, g: 175, b: 80 },   // 木·行
+  { r: 255, g: 107, b: 53 },  // 火·心
+  { r: 141, g: 110, b: 99 },  // 土·身
+  { r: 255, g: 215, b: 0 },   // 金·智
+  { r: 66, g: 165, b: 245 }   // 水·富
+]
+```
+
+- [ ] **Step 3: 修正 `animateFill` 中的 `startTs` 和 `dimKeys`**
+
+```javascript
+// animateFill 方法中:
+var startTs = [
+  this.animBody,
+  this.animMind,
+  this.animWealth,
+  this.animAction,
+  this.animWisdom
+]
+// 改为:
+var startTs = [
+  this.animAction,  // 行
+  this.animMind,    // 心
+  this.animBody,    // 身
+  this.animWisdom,  // 智
+  this.animWealth   // 富
+]
+
+// dimKeys 数组:
+var dimKeys = ['animBody', 'animMind', 'animWealth', 'animAction', 'animWisdom']
+// 改为:
+var dimKeys = ['animAction', 'animMind', 'animBody', 'animWisdom', 'animWealth']
+```
+
+- [ ] **Step 4: 修正 `onCanvasTap` 中的 domains 数组**
+
+```javascript
+// 当前:
+var domains = ['body', 'mind', 'wealth', 'action', 'wisdom']
+// 改为(按新顶点顺序,i=0→top-left=行, i=1→top=心, etc.):
+var domains = ['action', 'mind', 'body', 'wisdom', 'wealth']
+```
+
+- [ ] **Step 5: 修正 data 中的动画变量默认值顺序(仅对齐注释,值不变)**
+
+```javascript
+// data() 中:
+animBody: 0,      // 身
+animMind: 0,      // 心
+animWisdom: 0,    // 智
+animAction: 0,    // 行
+animWealth: 0,    // 富
+// 保持字段名不变(它们通过 dimKeys 动态访问),不需要改变量声明
+```
+
+- [ ] **Step 6: 验证映射一致性**
+
+检查 `renderStar` 循环中的多边形几何索引 `(i+4)%5`, `(i+3)%5` 等是否正确对应新布局。这些是纯数学计算,不依赖维度名称,因此**不需要修改**。
+
+---
+
+### Task 3: 对齐后端 EnergyService 维度注释
+
+**Files:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java`
+
+- [ ] **Step 1: 修正 Javadoc 注释中的维度描述**
+
+在第 17 行的类级注释中:
+```java
+/**
+ * 家庭能量沙盘 — 核心计算服务
+ * 计算每个家庭成员(家长+孩子)的身/心/智/行/富五维能量值 (0-100%),
+ * 并聚合出家庭整体能量视图。
+ */
+```
+
+改为:
+```java
+/**
+ * 家庭能量沙盘 — 核心计算服务
+ * 计算每个家庭成员(家长+孩子)的智/富/行/心/身五维能量值 (0-100%),
+ * 并聚合出家庭整体能量视图。
+ */
+```
+
+- [ ] **Step 2: 修正各 calc 方法的注释(五行对齐)**
+
+保持方法名和逻辑不变,仅修正注释中的五行元素对应:
+
+| 方法 | 旧注释 | 新注释 |
+|------|--------|--------|
+| `calcParentBody` / `calcChildBody` | 身 — 运动健康 | **身·土** — 运动健康 |
+| `calcParentMind` / `calcChildMind` | 心 — 学习/阅读/亲子 | **心·火** — 学习/阅读/亲子 |
+| `calcParentWisdom` / `calcChildWisdom` | 智 — 学习提升/游戏测评 | **智·金** — 学习提升/游戏测评 |
+| `calcParentAction` / `calcChildAction` | 行 — 习惯/自律/打卡 | **行·木** — 习惯/自律/打卡 |
+| `calcParentWealth` / `calcChildWealth` | 富 — 积分积累 | **富·水** — 积分积累 |
+
+例如:
+```java
+/** 家长 身·土 — 运动健康类任务完成率 */
+private int calcParentBody(User parent) {
+```
+
+将每个注释行中的 `「身」` 改为 `「身·土」`,`「心」` 改为 `「心·火」` 等,方法名和逻辑不变。
+
+---
+
+### Task 4: 更新 README 品牌理念表格
+
+**Files:**
+- Modify: `README.md`
+
+- [ ] **Step 1: 更新品牌理念五维表格**
+
+当前表格:
+```markdown
+| 维度 | 代码 | 含义 |
+|:----:|:----:|------|
+| 🌏 **身** | body | 身体健康、活力充沛 |
+| 🔥 **心** | mind | 情绪管理、内心丰盈 |
+| ⚔️ **智** | wisdom | 认知提升、终身学习 |
+| 🌿 **行** | action | 行动力强、习惯养成 |
+| 💧 **富** | wealth | 财富素养、价值创造 |
+```
+
+改为(补充五行元素列,排序改为相生链:智→富→行→心→身):
+```markdown
+| 维度 | 五行 | 代码 | 含义 |
+|:----:|:----:|:----:|------|
+| ⚔️ **智** | 金 | wisdom | 认知提升、终身学习 |
+| 💧 **富** | 水 | wealth | 财富素养、价值创造 |
+| 🌿 **行** | 木 | action | 行动力强、习惯养成 |
+| 🔥 **心** | 火 | mind | 情绪管理、内心丰盈 |
+| 🌏 **身** | 土 | body | 身体健康、活力充沛 |
+```
+
+> 说明:该顺序遵循相生链(智生富→富生行→行生心→心生身),与 `2026-06-08-five-dimension-wuxing-philosophy.md` 保持一致。项目简介首段中的"身、心、智、行、富"描述也同步更新。
+
+- [ ] **Step 2: 同步更新项目简介首段**
+
+```markdown
+浠艾福(Care Family)是一个以家庭为单位的健康成长平台,围绕**智、富、行、心、身**五个维度...
+```
+
+将原有的 `"身、心、智、行、富"` 改为 `"智、富、行、心、身"`。
+
+---
+
+## 验证清单
+
+- [ ] wuxing-sandbox.vue 重新编译后,五角星最左侧显示"行·🌿木",顶部显示"心·🔥火",右上显示"身·🌏土",右下显示"智·⚔️金",左下显示"富·💧水"
+- [ ] 点击每个顶点触发正确的 `onPointClick` 回调(domain 参数正确)
+- [ ] Canvas 填充颜色与顶点元素一致(木绿→行,火橙→心,土棕→身,金黄→智,水蓝→富)
+- [ ] Canvas 填充动画 (ts 数组) 对应正确的星臂几何位置
+- [ ] LSP 诊断无错误
+- [ ] 后端编译无错误
+- [ ] README 品牌理念表格五行 icon 正确对应

+ 1177 - 0
docs/superpowers/plans/2026-06-08-wuxing-sandbox-refactor.md

@@ -0,0 +1,1177 @@
+# 能量沙盘组件全面重构 实施计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 将 812 行的 wuxing-sandbox.vue 拆分为 1 个薄编排组件 + 3 个纯 JS 工具模块,重新设计 props/events/slots 接口,清理死代码,适配 3 个消费页面。
+
+**Architecture:** 单组件 + 提取工具模块。主组件保留 template/CSS/Vue 逻辑,Canvas 渲染/动画/点击检测提取为纯 JS 模块。不用子组件(避免 uni-app canvas 跨组件问题),不用 mixin(避免命名冲突)。
+
+**Tech Stack:** uni-app Vue 2 Options API, Canvas 2D API, 微信小程序
+
+**Spec:** `docs/superpowers/specs/2026-06-08-wuxing-sandbox-refactor-design.md`
+
+---
+
+## File Structure
+
+```
+cfc-frontend/
+├── components/
+│   ├── wuxing-sandbox.vue                    # MODIFY — 薄编排组件(812→~300行)
+│   └── wuxing-sandbox-helpers/               # CREATE — 工具模块目录
+│       ├── render.js                         # CREATE — Canvas 渲染函数
+│       ├── animation.js                      # CREATE — 动画控制器类
+│       └── hitdetect.js                      # CREATE — 点击检测工具
+├── pages/index/parent-index.vue              # MODIFY — 适配新 props 接口
+├── pages/index/index.vue                     # MODIFY — 无实质改动(确认兼容)
+└── pages/discover/index.vue                  # MODIFY — 无实质改动(确认兼容)
+```
+
+---
+
+### Task 1: 创建 hitdetect.js — 点击检测工具模块
+
+**Files:**
+- Create: `cfc-frontend/components/wuxing-sandbox-helpers/hitdetect.js`
+
+- [ ] **Step 1: 创建目录和文件**
+
+```js
+/**
+ * wuxing-sandbox-helpers/hitdetect.js
+ * 五角星点击检测工具(纯函数,无状态)
+ */
+
+var DIM_KEYS = ['body', 'mind', 'wisdom', 'action', 'wealth']
+
+/**
+ * 射线法(Ray Casting)判断点是否在任意多边形内
+ * @param {number} x - 点的 x 坐标
+ * @param {number} y - 点的 y 坐标
+ * @param {Array<{x:number,y:number}>} polygon - 多边形顶点数组
+ * @returns {boolean}
+ */
+export function pointInPolygon(x, y, polygon) {
+  var inside = false
+  var n = polygon.length
+  for (var i = 0, j = n - 1; i < n; j = i++) {
+    var xi = polygon[i].x
+    var yi = polygon[i].y
+    var xj = polygon[j].x
+    var yj = polygon[j].y
+    if ((yi > y) !== (yj > y) && x < (xj - xi) * (y - yi) / (yj - yi) + xi) {
+      inside = !inside
+    }
+  }
+  return inside
+}
+
+/**
+ * 计算指定星臂的多边形顶点
+ * @param {number} armIndex - 星臂索引 0~4 (body/mind/wisdom/action/wealth)
+ * @param {number} cx - 中心 x
+ * @param {number} cy - 中心 y
+ * @param {{outer:Array,inner:Array}} vertices - calcStarVertices 返回值
+ * @returns {Array<{x:number,y:number}>}
+ */
+export function calcArmPolygon(armIndex, cx, cy, vertices) {
+  var outer = vertices.outer
+  var inner = vertices.inner
+  var outerIdx = (armIndex + 4) % 5
+  var iBefore = (armIndex + 3) % 5
+  var iAfter = (armIndex + 4) % 5
+  return [
+    { x: cx, y: cy },
+    inner[iBefore],
+    outer[outerIdx],
+    inner[iAfter]
+  ]
+}
+
+/**
+ * 检测点击位置命中哪个星臂
+ * @param {number} relX - Canvas 相对 x 坐标
+ * @param {number} relY - Canvas 相对 y 坐标
+ * @param {number} cx - 中心 x
+ * @param {number} cy - 中心 y
+ * @param {number} outerR - 外半径
+ * @param {Function} calcVertices - calcStarVertices 函数引用
+ * @returns {number} 命中星臂索引 0~4,-1 表示未命中
+ */
+export function detectHitArm(relX, relY, cx, cy, outerR, calcVertices) {
+  var vertices = calcVertices(cx, cy, outerR)
+  for (var i = 0; i < 5; i++) {
+    var polygon = calcArmPolygon(i, cx, cy, vertices)
+    if (pointInPolygon(relX, relY, polygon)) {
+      return i
+    }
+  }
+  return -1
+}
+
+/**
+ * 星臂索引 → 维度 key 映射
+ * @param {number} armIndex
+ * @returns {string} 'body'|'mind'|'wisdom'|'action'|'wealth'
+ */
+export function armIndexToDomain(armIndex) {
+  return DIM_KEYS[armIndex] || ''
+}
+```
+
+- [ ] **Step 2: 验证文件创建**
+
+Run: `ls -la cfc-frontend/components/wuxing-sandbox-helpers/hitdetect.js`
+
+Expected: 文件存在,约 75 行
+
+---
+
+### Task 2: 创建 render.js — Canvas 渲染工具模块
+
+**Files:**
+- Create: `cfc-frontend/components/wuxing-sandbox-helpers/render.js`
+
+- [ ] **Step 1: 创建 render.js**
+
+```js
+/**
+ * wuxing-sandbox-helpers/render.js
+ * 五角星 Canvas 渲染(纯函数,无状态)
+ */
+
+/**
+ * 计算五角星顶点坐标
+ * 外顶点:5 个,72° 等分,起始角度 -90°(正上方)
+ * 内顶点:5 个,偏移 36°,半径 = 外半径 × 0.382
+ * @param {number} cx - 中心 x
+ * @param {number} cy - 中心 y
+ * @param {number} outerR - 外半径
+ * @returns {{outer:Array<{x:number,y:number}>,inner:Array<{x:number,y:number}>}}
+ */
+export function calcStarVertices(cx, cy, outerR) {
+  var innerR = outerR * 0.382
+  var outer = []
+  var inner = []
+  for (var i = 0; i < 5; i++) {
+    var angle = -Math.PI / 2 + i * 2 * Math.PI / 5
+    outer.push({
+      x: cx + outerR * Math.cos(angle),
+      y: cy + outerR * Math.sin(angle)
+    })
+    var innerAngle = angle + Math.PI / 5
+    inner.push({
+      x: cx + innerR * Math.cos(innerAngle),
+      y: cy + innerR * Math.sin(innerAngle)
+    })
+  }
+  return { outer: outer, inner: inner }
+}
+
+/** 五色配置(与 CSS 一致) */
+var COLORS = [
+  { r: 76, g: 175, b: 80 },   // 木·身
+  { r: 255, g: 107, b: 53 },  // 火·心
+  { r: 141, g: 110, b: 99 },  // 土·智
+  { r: 255, g: 215, b: 0 },   // 金·行
+  { r: 66, g: 165, b: 245 }   // 水·富
+]
+
+/**
+ * 绘制完整五角星图(含填充、描边、太极)
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {number} w - canvas 宽度
+ * @param {number} h - canvas 高度
+ * @param {Array<number>} ts - 5 维归一化值 (0~1),顺序 [body,mind,wisdom,action,wealth]
+ * @param {Object} [options]
+ * @param {boolean} [options.showTaiji=true] - 是否绘制太极
+ */
+export function renderStar(ctx, w, h, ts, options) {
+  if (!ctx || !w || !h) return
+
+  var opts = options || {}
+  var showTaiji = opts.showTaiji !== false
+
+  // 清空
+  ctx.clearRect(0, 0, w, h)
+
+  // 星型几何
+  var cx = w / 2
+  var cy = h / 2
+  var outerR = Math.min(w, h) * 0.42
+  var vertices = calcStarVertices(cx, cy, outerR)
+  var outer = vertices.outer
+  var inner = vertices.inner
+
+  // 绘制每个星臂
+  for (var i = 0; i < 5; i++) {
+    var outerIdx = (i + 4) % 5
+    var iBefore = (i + 3) % 5
+    var iAfter = (i + 4) % 5
+    var t = ts[i]
+    var c = COLORS[i]
+
+    // 星臂 polygon: center, inner[iBefore], outer[outerIdx], inner[iAfter]
+    var polygon = [
+      { x: cx, y: cy },
+      inner[iBefore],
+      outer[outerIdx],
+      inner[iAfter]
+    ]
+
+    // --- 背景填充(低透明度) ---
+    ctx.beginPath()
+    ctx.moveTo(polygon[0].x, polygon[0].y)
+    for (var p = 1; p < polygon.length; p++) {
+      ctx.lineTo(polygon[p].x, polygon[p].y)
+    }
+    ctx.closePath()
+    ctx.fillStyle = 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', 0.08)'
+    ctx.fill()
+
+    // --- 渐变填充(t 控制色彩饱和度,覆盖整个星臂) ---
+    if (t > 0.01) {
+      ctx.save()
+      ctx.beginPath()
+      ctx.moveTo(polygon[0].x, polygon[0].y)
+      for (var fp = 1; fp < polygon.length; fp++) {
+        ctx.lineTo(polygon[fp].x, polygon[fp].y)
+      }
+      ctx.closePath()
+      ctx.clip()
+
+      var grad = ctx.createRadialGradient(cx, cy, 0, cx, cy, Math.max(outerR, 1))
+      grad.addColorStop(0, 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', ' + (0.85 * t) + ')')
+      grad.addColorStop(0.5, 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', ' + (0.35 * t) + ')')
+      grad.addColorStop(1, 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', ' + (0.02 * t) + ')')
+      ctx.fillStyle = grad
+      ctx.fillRect(0, 0, w, h)
+
+      ctx.restore()
+    }
+
+    // --- 星臂描边 ---
+    ctx.beginPath()
+    ctx.moveTo(polygon[0].x, polygon[0].y)
+    for (var sp = 1; sp < polygon.length; sp++) {
+      ctx.lineTo(polygon[sp].x, polygon[sp].y)
+    }
+    ctx.closePath()
+    ctx.strokeStyle = 'rgba(' + c.r + ', ' + c.g + ', ' + c.b + ', 0.4)'
+    ctx.lineWidth = 1.5
+    ctx.stroke()
+  }
+
+  // --- 五角星外轮廓(10 个顶点依次连接) ---
+  ctx.beginPath()
+  ctx.moveTo(outer[0].x, outer[0].y)
+  ctx.lineTo(inner[0].x, inner[0].y)
+  ctx.lineTo(outer[1].x, outer[1].y)
+  ctx.lineTo(inner[1].x, inner[1].y)
+  ctx.lineTo(outer[2].x, outer[2].y)
+  ctx.lineTo(inner[2].x, inner[2].y)
+  ctx.lineTo(outer[3].x, outer[3].y)
+  ctx.lineTo(inner[3].x, inner[3].y)
+  ctx.lineTo(outer[4].x, outer[4].y)
+  ctx.lineTo(inner[4].x, inner[4].y)
+  ctx.closePath()
+  ctx.strokeStyle = 'rgba(143, 197, 232, 0.50)'
+  ctx.lineWidth = 2
+  ctx.stroke()
+
+  // --- 内部五边形(5 个内顶点) ---
+  ctx.beginPath()
+  ctx.moveTo(inner[0].x, inner[0].y)
+  ctx.lineTo(inner[1].x, inner[1].y)
+  ctx.lineTo(inner[2].x, inner[2].y)
+  ctx.lineTo(inner[3].x, inner[3].y)
+  ctx.lineTo(inner[4].x, inner[4].y)
+  ctx.closePath()
+  ctx.strokeStyle = 'rgba(143, 197, 232, 0.35)'
+  ctx.lineWidth = 1.5
+  ctx.stroke()
+
+  // --- 中心太极阴阳鱼(在 Canvas 最上层绘制) ---
+  if (showTaiji) {
+    var taijiR = outerR * 0.22
+    drawTaiji(ctx, cx, cy, taijiR)
+  }
+}
+
+/**
+ * 绘制太极阴阳鱼
+ * @param {CanvasRenderingContext2D} ctx
+ * @param {number} cx - 中心 x
+ * @param {number} cy - 中心 y
+ * @param {number} r - 半径
+ */
+export function drawTaiji(ctx, cx, cy, r) {
+  var yangColor = 'rgba(91, 155, 213, 0.35)'
+  var yinColor = 'rgba(26, 42, 74, 0.75)'
+  var strokeColor = 'rgba(143, 197, 232, 0.2)'
+  var yangEyeColor = 'rgba(26, 42, 74, 0.85)'
+  var yinEyeColor = 'rgba(143, 197, 232, 0.75)'
+
+  ctx.save()
+
+  // 1. 外圆背景及边框
+  ctx.beginPath()
+  ctx.arc(cx, cy, r, 0, Math.PI * 2)
+  ctx.closePath()
+  ctx.fillStyle = yangColor
+  ctx.fill()
+  ctx.strokeStyle = strokeColor
+  ctx.lineWidth = 1
+  ctx.stroke()
+
+  // 2. 左半边:阴(深色)
+  ctx.beginPath()
+  ctx.moveTo(cx, cy)
+  ctx.arc(cx, cy, r, Math.PI / 2, -Math.PI / 2)
+  ctx.closePath()
+  ctx.fillStyle = yinColor
+  ctx.fill()
+
+  // 3. 上半小圆左半弧:阳色
+  ctx.beginPath()
+  ctx.moveTo(cx, cy - r)
+  ctx.arc(cx, cy - r / 2, r / 2, Math.PI / 2, -Math.PI / 2)
+  ctx.closePath()
+  ctx.fillStyle = yangColor
+  ctx.fill()
+
+  // 4. 下半小圆右半弧:阴色
+  ctx.beginPath()
+  ctx.moveTo(cx, cy + r)
+  ctx.arc(cx, cy + r / 2, r / 2, -Math.PI / 2, Math.PI / 2)
+  ctx.closePath()
+  ctx.fillStyle = yinColor
+  ctx.fill()
+
+  // 5. 阳眼(深色点,上方)
+  ctx.beginPath()
+  ctx.arc(cx, cy - r / 2, r * 0.12, 0, Math.PI * 2)
+  ctx.closePath()
+  ctx.fillStyle = yangEyeColor
+  ctx.fill()
+
+  // 6. 阴眼(浅色点,下方)
+  ctx.beginPath()
+  ctx.arc(cx, cy + r / 2, r * 0.12, 0, Math.PI * 2)
+  ctx.closePath()
+  ctx.fillStyle = yinEyeColor
+  ctx.fill()
+
+  ctx.restore()
+
+  // 7. 文字标签
+  ctx.save()
+  var fontSize = Math.round(r * 0.3)
+  ctx.font = 'bold ' + fontSize + 'px sans-serif'
+  ctx.textAlign = 'center'
+  ctx.textBaseline = 'middle'
+
+  ctx.fillStyle = 'rgba(143, 197, 232, 0.6)'
+  ctx.fillText('家', cx - r * 0.3, cy + r * 0.15)
+
+  ctx.fillStyle = 'rgba(91, 155, 213, 0.8)'
+  ctx.fillText('个', cx + r * 0.3, cy - r * 0.15)
+
+  ctx.restore()
+}
+```
+
+- [ ] **Step 2: 验证文件创建**
+
+Run: `ls -la cfc-frontend/components/wuxing-sandbox-helpers/render.js`
+
+Expected: 文件存在,约 200 行
+
+---
+
+### Task 3: 创建 animation.js — 动画控制器模块
+
+**Files:**
+- Create: `cfc-frontend/components/wuxing-sandbox-helpers/animation.js`
+
+- [ ] **Step 1: 创建 animation.js**
+
+```js
+/**
+ * wuxing-sandbox-helpers/animation.js
+ * 五角星填充动画控制器
+ */
+
+/**
+ * FillAnimator - 管理 ease-out cubic 填充动画
+ * @param {Function} getCurrentTs - 返回当前 5 维插值数组 [0~1]
+ * @param {Function} onFrame - 每帧回调,参数为插值后的 ts 数组
+ * @param {Function} [onComplete] - 动画完成回调
+ */
+export function FillAnimator(getCurrentTs, onFrame, onComplete) {
+  this._getCurrentTs = getCurrentTs
+  this._onFrame = onFrame
+  this._onComplete = onComplete || function() {}
+  this._timer = null
+}
+
+/**
+ * 启动动画
+ * @param {Array<number>} targetTs - 目标 5 维值 (0~1)
+ * @param {number} [duration=600] - 动画时长 ms
+ */
+FillAnimator.prototype.start = function(targetTs, duration) {
+  this.cancel()
+
+  var dur = duration || 600
+  var startTime = Date.now()
+  var startTs = this._getCurrentTs()
+  var self = this
+
+  function step() {
+    var elapsed = Date.now() - startTime
+    var progress = Math.min(elapsed / dur, 1)
+    // ease-out cubic
+    var eased = 1 - Math.pow(1 - progress, 3)
+
+    var interpolated = []
+    for (var i = 0; i < 5; i++) {
+      interpolated.push(startTs[i] + (targetTs[i] - startTs[i]) * eased)
+    }
+
+    self._onFrame(interpolated)
+
+    if (progress < 1) {
+      self._timer = setTimeout(step, 16)
+    } else {
+      self._timer = null
+      self._onComplete()
+    }
+  }
+
+  step()
+}
+
+/**
+ * 取消动画,清理 timer
+ */
+FillAnimator.prototype.cancel = function() {
+  if (this._timer) {
+    clearTimeout(this._timer)
+    this._timer = null
+  }
+}
+```
+
+- [ ] **Step 2: 验证文件创建**
+
+Run: `ls -la cfc-frontend/components/wuxing-sandbox-helpers/animation.js`
+
+Expected: 文件存在,约 65 行
+
+---
+
+### Task 4: 重写 wuxing-sandbox.vue — 主组件重构
+
+**Files:**
+- Modify: `cfc-frontend/components/wuxing-sandbox.vue` (全文重写)
+
+这是最核心的任务。将 812 行组件重写为 ~300 行薄编排组件,导入 3 个 helper 模块,重新设计 props/events/slots。
+
+- [ ] **Step 1: 重写 wuxing-sandbox.vue**
+
+完整替换为以下内容:
+
+```vue
+<template>
+<view class="wuxing-sandbox">
+  <!-- header slot -->
+  <slot name="header">
+    <view class="sandbox-header">
+      <text class="sandbox-title">🌟 家庭能量沙盘</text>
+      <text class="sandbox-badge">{{ displayBadgeText }}</text>
+    </view>
+  </slot>
+
+  <view class="sandbox-star" :style="{height: starHeight}">
+    <view class="star-glow-bg"></view>
+
+    <!-- Canvas 2D 五行星图 -->
+    <canvas id="starCanvas" type="2d" style="width:100%;height:100%;position:absolute;top:0;left:0;z-index:1"></canvas>
+
+    <!-- Canvas 点击感应层 -->
+    <view class="canvas-tap-area" @click="onCanvasTap"></view>
+
+    <!-- 顶点:图标 + 标签 身/心/智/行/财 -->
+    <view class="star-point star-point--ul" @click="onPointClick('body')">
+      <view class="point-dot point-dot--wood"><text class="point-icon">🌿</text></view>
+      <text class="point-label">身</text>
+    </view>
+    <view class="star-point star-point--top" @click="onPointClick('mind')">
+      <view class="point-dot point-dot--fire"><text class="point-icon">🔥</text></view>
+      <text class="point-label">心</text>
+    </view>
+    <view class="star-point star-point--ur" @click="onPointClick('wealth')">
+      <view class="point-dot point-dot--earth"><text class="point-icon">🌏</text></view>
+      <text class="point-label">财</text>
+    </view>
+    <view class="star-point star-point--lr" @click="onPointClick('action')">
+      <view class="point-dot point-dot--metal"><text class="point-icon">⚡</text></view>
+      <text class="point-label">行</text>
+    </view>
+    <view class="star-point star-point--ll" @click="onPointClick('wisdom')">
+      <view class="point-dot point-dot--water"><text class="point-icon">💧</text></view>
+      <text class="point-label">智</text>
+    </view>
+
+    <!-- 中心点击区域(太极由 Canvas 绘制) -->
+    <view class="star-center" @click="onCenterClick">
+      <text class="energy-hint">{{ viewMode === 'family' ? '家庭能量' : (currentMember ? currentMember.name : '个人') }}</text>
+    </view>
+  </view>
+
+  <!-- dimension-bar slot -->
+  <slot name="dimension-bar" v-if="showDimensionBar && mode !== 'preview'">
+    <view class="dimension-bar">
+      <view class="dim-item dim-item--wood">🌿身 {{ activeScores[0] }}%</view>
+      <view class="dim-item dim-item--fire">🔥心 {{ activeScores[1] }}%</view>
+      <view class="dim-item dim-item--earth">🌏智 {{ activeScores[2] }}%</view>
+      <view class="dim-item dim-item--metal">⚡行 {{ activeScores[3] }}%</view>
+      <view class="dim-item dim-item--water">💧富 {{ activeScores[4] }}%</view>
+    </view>
+  </slot>
+</view>
+</template>
+
+<script>
+import { renderStar, calcStarVertices } from './wuxing-sandbox-helpers/render.js'
+import { FillAnimator } from './wuxing-sandbox-helpers/animation.js'
+import { detectHitArm, armIndexToDomain } from './wuxing-sandbox-helpers/hitdetect.js'
+
+var DIM_KEYS = ['body', 'mind', 'wisdom', 'action', 'wealth']
+
+export default {
+  props: {
+    /** 模式:'preview' 随机数据 / 'live' 真实数据 */
+    mode: {
+      type: String,
+      default: 'preview',
+      validator: function(v) { return v === 'preview' || v === 'live' }
+    },
+    /** 五维分值对象 {body,mind,wisdom,action,wealth},0-100 */
+    scores: {
+      type: Object,
+      default: function() { return { body: 0, mind: 0, wisdom: 0, action: 0, wealth: 0 } }
+    },
+    /** 综合分值 0-100 */
+    overallScore: {
+      type: Number,
+      default: 0
+    },
+    /** 家庭成员数组(MemberEnergyDTO 格式) */
+    members: {
+      type: Array,
+      default: function() { return [] }
+    },
+    /** 顶部徽章文字,空则自动计算 */
+    badgeText: {
+      type: String,
+      default: ''
+    },
+    /** 是否显示底部维度条 */
+    showDimensionBar: {
+      type: Boolean,
+      default: true
+    },
+    /** 星图区域高度 */
+    starHeight: {
+      type: String,
+      default: '480rpx'
+    }
+  },
+  data: function() {
+    return {
+      viewMode: 'family',
+      selectedMemberIndex: 0,
+      previewScores: [0, 0, 0, 0, 0],
+      canvasReady: false,
+      canvasWidth: 0,
+      canvasHeight: 0,
+      ctx: null,
+      dpr: 1,
+      _canvasLeft: 0,
+      _canvasTop: 0,
+      animBody: 0,
+      animMind: 0,
+      animWisdom: 0,
+      animAction: 0,
+      animWealth: 0,
+      _animator: null
+    }
+  },
+  computed: {
+    isPreview: function() {
+      return this.mode === 'preview'
+    },
+    currentMember: function() {
+      if (this.isPreview) return null
+      var m = this.members
+      if (!m || m.length === 0) return null
+      if (this.selectedMemberIndex >= m.length) return m[0]
+      return m[this.selectedMemberIndex]
+    },
+    /** 当前活跃的五维值数组 [body, mind, wisdom, action, wealth] */
+    activeScores: function() {
+      if (this.isPreview) return this.previewScores
+      if (this.viewMode === 'family') {
+        var s = this.scores
+        return [
+          s.body || 0,
+          s.mind || 0,
+          s.wisdom || 0,
+          s.action || 0,
+          s.wealth || 0
+        ]
+      }
+      var m = this.currentMember
+      if (!m) return [0, 0, 0, 0, 0]
+      return [
+        m.bodyScore || 0,
+        m.mindScore || 0,
+        m.wisdomScore || 0,
+        m.actionScore || 0,
+        m.wealthScore || 0
+      ]
+    },
+    activeOverall: function() {
+      if (this.isPreview) {
+        var s = this.previewScores
+        return Math.round((s[0] + s[1] + s[2] + s[3] + s[4]) / 5)
+      }
+      if (this.viewMode === 'family') return this.overallScore || 0
+      var m = this.currentMember
+      return m ? (m.overallScore || 0) : 0
+    },
+    activeTs: function() {
+      return [
+        this.activeScores[0] / 100,
+        this.activeScores[1] / 100,
+        this.activeScores[2] / 100,
+        this.activeScores[3] / 100,
+        this.activeScores[4] / 100
+      ]
+    },
+    displayBadgeText: function() {
+      if (this.badgeText) return this.badgeText
+      var label = this.viewMode === 'family' ? '家庭能量' : (this.currentMember ? this.currentMember.name : '个人能量')
+      return label + ' ' + (this.activeOverall || 0) + '%'
+    },
+    _allDimTs: function() {
+      return this.activeTs
+    },
+    hasMultipleMembers: function() {
+      return this.members && this.members.length > 1
+    }
+  },
+  watch: {
+    _allDimTs: function() {
+      if (this.canvasReady) {
+        this._startAnimation()
+      }
+    }
+  },
+  mounted: function() {
+    var self = this
+
+    // 预览模式:生成随机五维值
+    if (this.isPreview) {
+      var scores = []
+      for (var i = 0; i < 5; i++) {
+        scores.push(Math.floor(Math.random() * 51) + 35)
+      }
+      this.previewScores = scores
+    }
+
+    // 等待 DOM 就绪后初始化 Canvas
+    setTimeout(function() {
+      self.initCanvas()
+    }, 100)
+  },
+  beforeDestroy: function() {
+    if (this._animator) {
+      this._animator.cancel()
+      this._animator = null
+    }
+  },
+  methods: {
+    /* ===========================
+       视图切换
+    =========================== */
+
+    onCenterClick: function() {
+      // 预览模式不切换
+      if (this.isPreview) {
+        this.$emit('center-click', { viewMode: this.viewMode, selectedMemberIndex: this.selectedMemberIndex })
+        return
+      }
+      var members = this.members
+      if (!members || members.length === 0) {
+        this.viewMode = 'family'
+        this.$emit('center-click', { viewMode: this.viewMode, selectedMemberIndex: this.selectedMemberIndex })
+        return
+      }
+      if (this.viewMode === 'family') {
+        this.viewMode = 'member'
+      } else {
+        if (members.length === 1) {
+          this.viewMode = 'family'
+          this.selectedMemberIndex = 0
+        } else {
+          var nextIdx = this.selectedMemberIndex + 1
+          if (nextIdx >= members.length) {
+            this.viewMode = 'family'
+            this.selectedMemberIndex = 0
+          } else {
+            this.selectedMemberIndex = nextIdx
+          }
+        }
+      }
+      this.$emit('center-click', { viewMode: this.viewMode, selectedMemberIndex: this.selectedMemberIndex })
+      this.$emit('mode-change', { viewMode: this.viewMode, selectedMemberIndex: this.selectedMemberIndex })
+    },
+
+    onPointClick: function(domain) {
+      this.$emit('point-click', {
+        domain: domain,
+        viewMode: this.viewMode,
+        member: this.currentMember
+      })
+    },
+
+    /* ===========================
+       Canvas 初始化 & 渲染
+    =========================== */
+
+    initCanvas: function() {
+      var self = this
+      uni.createSelectorQuery().in(this).select('#starCanvas')
+        .fields({ node: true, size: true, rect: true })
+        .exec(function(res) {
+          if (!res || !res[0] || !res[0].node) return
+
+          var canvas = res[0].node
+          var width = res[0].width
+          var height = res[0].height
+          if (!width || !height) return
+
+          var pixelRatio = uni.getSystemInfoSync().pixelRatio || 2
+
+          canvas.width = width * pixelRatio
+          canvas.height = height * pixelRatio
+
+          var ctx = canvas.getContext('2d')
+          ctx.scale(pixelRatio, pixelRatio)
+
+          self.ctx = ctx
+          self.canvasWidth = width
+          self.canvasHeight = height
+          self.dpr = pixelRatio
+          self._canvasLeft = res[0].left || 0
+          self._canvasTop = res[0].top || 0
+          self.canvasReady = true
+
+          self._startAnimation()
+        })
+    },
+
+    _doRender: function() {
+      if (!this.ctx) return
+      var ts = [this.animBody, this.animMind, this.animWisdom, this.animAction, this.animWealth]
+      renderStar(this.ctx, this.canvasWidth, this.canvasHeight, ts)
+    },
+
+    /* ===========================
+       动画控制
+    =========================== */
+
+    _startAnimation: function() {
+      var self = this
+      if (!this._animator) {
+        this._animator = new FillAnimator(
+          function() {
+            return [self.animBody, self.animMind, self.animWisdom, self.animAction, self.animWealth]
+          },
+          function(interpolated) {
+            self.animBody = interpolated[0]
+            self.animMind = interpolated[1]
+            self.animWisdom = interpolated[2]
+            self.animAction = interpolated[3]
+            self.animWealth = interpolated[4]
+            self._doRender()
+          }
+        )
+      }
+      this._animator.start(this.activeTs, 600)
+    },
+
+    /* ===========================
+       点击检测
+    =========================== */
+
+    onCanvasTap: function(e) {
+      if (!this.canvasReady) return
+
+      var pageX = e.detail.x
+      var pageY = e.detail.y
+      if (typeof pageX !== 'number') {
+        var touch = e.touches ? e.touches[0] : null
+        if (touch) {
+          pageX = touch.x || touch.clientX || 0
+          pageY = touch.y || touch.clientY || 0
+        } else {
+          var changedTouch = e.changedTouches ? e.changedTouches[0] : null
+          if (changedTouch) {
+            pageX = changedTouch.x || changedTouch.clientX || 0
+            pageY = changedTouch.y || changedTouch.clientY || 0
+          } else {
+            return
+          }
+        }
+      }
+
+      var relX = pageX - (this._canvasLeft || 0)
+      var relY = pageY - (this._canvasTop || 0)
+      if (relX < 0 || relY < 0 || relX > this.canvasWidth || relY > this.canvasHeight) return
+
+      var cx = this.canvasWidth / 2
+      var cy = this.canvasHeight / 2
+      var outerR = Math.min(this.canvasWidth, this.canvasHeight) * 0.42
+      var armIdx = detectHitArm(relX, relY, cx, cy, outerR, calcStarVertices)
+      if (armIdx >= 0) {
+        this.onPointClick(armIndexToDomain(armIdx))
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+/* ============================================================
+   容器
+   ============================================================ */
+.wuxing-sandbox {
+  margin: 20rpx 30rpx;
+  padding: 20rpx 24rpx 24rpx;
+  background: linear-gradient(135deg, #1A2A4A, #1E3A5F, #16213E);
+  border-radius: 24rpx;
+  position: relative;
+  overflow: hidden;
+}
+.wuxing-sandbox::before {
+  content: '';
+  position: absolute;
+  top: -50%;
+  left: -50%;
+  width: 200%;
+  height: 200%;
+  background: radial-gradient(ellipse at 30% 20%, rgba(91, 155, 213, 0.04), transparent 60%),
+    radial-gradient(ellipse at 70% 80%, rgba(66, 165, 245, 0.03), transparent 60%);
+  pointer-events: none;
+}
+
+/* ---- 标题 ---- */
+.sandbox-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 12rpx;
+  position: relative;
+  z-index: 5;
+}
+.sandbox-title { font-size: 26rpx; font-weight: bold; color: #8FC5E8; }
+.sandbox-badge {
+  font-size: 20rpx;
+  color: rgba(143,197,232,0.7);
+  background: rgba(143,197,232,0.1);
+  padding: 4rpx 14rpx;
+  border-radius: 20rpx;
+}
+
+/* ---- 五角星区域 ---- */
+.sandbox-star {
+  position: relative;
+  width: 100%;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+}
+
+/* 背景光晕 */
+.star-glow-bg {
+  position: absolute; width: 440rpx; height: 440rpx;
+  top: 50%; left: 50%; transform: translate(-50%, -50%);
+  background: radial-gradient(circle, rgba(91,155,213,0.10), transparent 70%);
+  border-radius: 50%; pointer-events: none;
+}
+
+/* Canvas 点击感应层 */
+.canvas-tap-area {
+  position: absolute;
+  top: 0; left: 0; right: 0; bottom: 0;
+  z-index: 2;
+}
+
+/* ============================================================
+   顶点:图标 + 中文标签
+   ============================================================ */
+.star-point {
+  position: absolute; transform: translate(-50%, -50%); z-index: 3; cursor: pointer;
+  display: flex; flex-direction: column; align-items: center;
+}
+.star-point--top { top: 3%; left: 50%; }
+.star-point--ur { top: 26%; left: 87%; }
+.star-point--lr { top: 73%; left: 75%; }
+.star-point--ll { top: 73%; left: 25%; }
+.star-point--ul { top: 26%; left: 13%; }
+
+.point-dot {
+  width: 52rpx; height: 52rpx; border-radius: 50%;
+  display: flex; align-items: center; justify-content: center;
+  box-shadow: 0 0 24rpx rgba(255,255,255,0.25);
+}
+.point-dot--fire { background: linear-gradient(135deg, #FF6B35, #E65100); }
+.point-dot--water { background: linear-gradient(135deg, #42A5F5, #1565C0); }
+.point-dot--earth { background: linear-gradient(135deg, #8D6E63, #5D4037); }
+.point-dot--metal { background: linear-gradient(135deg, #FFD700, #FF8F00); }
+.point-dot--wood { background: linear-gradient(135deg, #4CAF50, #2E7D32); }
+
+.point-icon { font-size: 26rpx; line-height: 1; }
+
+.point-label {
+  font-size: 22rpx; font-weight: bold; color: rgba(255,255,255,0.9);
+  margin-top: 6rpx; text-shadow: 0 1rpx 4rpx rgba(0,0,0,0.5);
+  letter-spacing: 2rpx;
+}
+
+/* ============================================================
+   中心点击区域
+   ============================================================ */
+.star-center {
+  position: absolute; top: 50%; left: 50%;
+  transform: translate(-50%, -50%); z-index: 4;
+  width: 130rpx; height: 130rpx; border-radius: 50%;
+  cursor: pointer;
+}
+
+.energy-hint {
+  position: absolute; bottom: -30rpx; left: 50%;
+  transform: translateX(-50%);
+  font-size: 18rpx; color: rgba(143,197,232,0.4);
+  white-space: nowrap;
+}
+
+/* ============================================================
+   维度详情条
+   ============================================================ */
+.dimension-bar {
+  display: flex; align-items: center; justify-content: center;
+  margin-top: 16rpx; gap: 8rpx;
+  position: relative; z-index: 5; flex-wrap: wrap;
+}
+.dim-item {
+  font-size: 20rpx; padding: 4rpx 12rpx; border-radius: 10rpx; white-space: nowrap;
+}
+.dim-item--wood { color: #4CAF50; background: rgba(76,175,80,0.12); }
+.dim-item--fire { color: #FF6B35; background: rgba(255,107,53,0.12); }
+.dim-item--earth { color: #8D6E63; background: rgba(141,110,99,0.12); }
+.dim-item--metal { color: #FFD700; background: rgba(255,215,0,0.12); }
+.dim-item--water { color: #42A5F5; background: rgba(66,165,245,0.12); }
+</style>
+```
+
+- [ ] **Step 2: 检查 LSP 诊断**
+
+Run: `n/a`(前端 .vue 文件暂无 LSP,改为手动检查导入路径正确性)
+
+手动验证:
+- `import { renderStar, calcStarVertices } from './wuxing-sandbox-helpers/render.js'` → 文件需在 Task 2 创建
+- `import { FillAnimator } from './wuxing-sandbox-helpers/animation.js'` → 文件需在 Task 3 创建
+- `import { detectHitArm, armIndexToDomain } from './wuxing-sandbox-helpers/hitdetect.js'` → 文件需在 Task 1 创建
+- `sandbox-star` 高度改为 `:style="{height: starHeight}"` 绑定,默认 `480rpx` 不变
+- 删除了 `sandbox` prop,新增 `scores`/`overallScore`/`members`/`showDimensionBar`/`starHeight`
+- 新增 `@center-click` 和 `@mode-change` 事件
+- 新增 `header` 和 `dimension-bar` slots
+- 删除死代码:`_calcFillPolygon`、`pointInCircle`、`dimVal`、`curVal`、`curMax`
+- `_fillTimer` 替换为 `_animator`(FillAnimator 实例)
+- `toggleEnergyMode` 改名为 `onCenterClick`,增加了事件发射
+
+---
+
+### Task 5: 适配 parent-index.vue — 消费页面改造
+
+**Files:**
+- Modify: `cfc-frontend/pages/index/parent-index.vue`
+
+- [ ] **Step 1: 修改模板中的组件调用**
+
+将第 82-84 行:
+```html
+<wuxing-sandbox mode="parent"
+  :sandbox="safeSandbox"
+  @point-click="goToDomain" />
+```
+
+改为:
+```html
+<wuxing-sandbox mode="live"
+  :scores="energyScores"
+  :overall-score="energyOverall"
+  :members="energyMembers"
+  @point-click="goToDomain" />
+```
+
+- [ ] **Step 2: 替换 safeSandbox computed**
+
+将第 317-319 行:
+```js
+safeSandbox() {
+  return this.energyData || { bodyScore: 0, mindScore: 0, wisdomScore: 0, actionScore: 0, wealthScore: 0, overallScore: 0, members: [] }
+},
+```
+
+替换为 3 个 computed:
+```js
+energyScores() {
+  var d = this.energyData
+  if (!d) return { body: 0, mind: 0, wisdom: 0, action: 0, wealth: 0 }
+  return {
+    body: d.bodyScore || 0,
+    mind: d.mindScore || 0,
+    wisdom: d.wisdomScore || 0,
+    action: d.actionScore || 0,
+    wealth: d.wealthScore || 0
+  }
+},
+energyOverall() {
+  return (this.energyData && this.energyData.overallScore) || 0
+},
+energyMembers() {
+  return (this.energyData && this.energyData.members) || []
+},
+```
+
+- [ ] **Step 3: 验证没有残留 safeSandbox 引用**
+
+Run: `grep -n "safeSandbox" cfc-frontend/pages/index/parent-index.vue`
+
+Expected: 无输出(所有引用已清除)
+
+---
+
+### Task 6: 确认 index.vue 和 discover/index.vue 兼容
+
+**Files:**
+- Verify: `cfc-frontend/pages/index/index.vue`
+- Verify: `cfc-frontend/pages/discover/index.vue`
+
+- [ ] **Step 1: 检查 index.vue 调用方式**
+
+当前调用:
+```html
+<wuxing-sandbox mode="preview" badgeText="登录查看完整报告" @point-click="handleLogin" />
+```
+
+验证点:
+- `mode="preview"` — 新接口支持,validator 允许 `'preview'` ✅
+- `badgeText="登录查看完整报告"` — prop 保留 ✅
+- `@point-click="handleLogin"` — event 保留 ✅
+- 无 `sandbox` prop — 新接口不要求传入 ✅(组件内部 `scores` 默认全 0,`isPreview` 为 true 所以显示随机数据)
+
+**无需修改。**
+
+- [ ] **Step 2: 检查 discover/index.vue 调用方式**
+
+当前调用与 index.vue 完全相同:
+```html
+<wuxing-sandbox mode="preview" badgeText="登录查看完整报告" @point-click="handleLogin" />
+```
+
+**无需修改。**
+
+---
+
+### Task 7: 编译验证与最终提交
+
+**Files:**
+- All modified files
+
+- [ ] **Step 1: 检查 helper 模块文件完整性**
+
+Run:
+```bash
+ls -la cfc-frontend/components/wuxing-sandbox-helpers/
+```
+
+Expected: 3 个 .js 文件存在(render.js, animation.js, hitdetect.js)
+
+- [ ] **Step 2: 检查主组件导入路径正确**
+
+Run:
+```bash
+grep -n "import.*from.*wuxing-sandbox-helpers" cfc-frontend/components/wuxing-sandbox.vue
+```
+
+Expected: 3 行 import 语句,路径均指向 `./wuxing-sandbox-helpers/`
+
+- [ ] **Step 3: 检查消费页面无残留旧接口**
+
+Run:
+```bash
+grep -rn "sandbox=" cfc-frontend/pages/ | grep -v "wuxing-sandbox.vue" | grep -v "wuxing-sandbox-helpers"
+```
+
+Expected: 无输出(消费页面不再使用 `:sandbox=` 绑定)
+
+- [ ] **Step 4: 检查死代码已清除**
+
+Run:
+```bash
+grep -n "_calcFillPolygon\|pointInCircle\|dimVal\|curVal\|curMax\|safeSandbox" cfc-frontend/components/wuxing-sandbox.vue
+```
+
+Expected: 无输出
+
+- [ ] **Step 5: 提交**
+
+```bash
+git add cfc-frontend/components/wuxing-sandbox.vue \
+        cfc-frontend/components/wuxing-sandbox-helpers/ \
+        cfc-frontend/pages/index/parent-index.vue \
+        docs/superpowers/specs/2026-06-08-wuxing-sandbox-refactor-design.md \
+        docs/superpowers/plans/2026-06-08-wuxing-sandbox-refactor.md
+git commit -m "refactor: 全面重构能量沙盘组件 - 拆分为3个helper模块 + 扁平化props接口 + 清理死代码"
+```
+
+---
+
+## Self-Review
+
+**1. Spec 覆盖度检查:**
+- ✅ 拆分为 1 组件 + 3 helper 模块 → Task 1-4
+- ✅ Props 扁平化(scores/overallScore/members)→ Task 4
+- ✅ Events(center-click/mode-change)→ Task 4
+- ✅ Slots(header/dimension-bar)→ Task 4
+- ✅ 死代码清理(5 个方法)→ Task 4
+- ✅ 消费页面适配 → Task 5-6
+- ✅ 导入路径不变 → Task 6 验证
+
+**2. 占位符扫描:** 无 TBD/TODO/类似后补
+
+**3. 类型一致性检查:**
+- `renderStar(ctx, w, h, ts, options)` — Task 2 定义,Task 4 调用 `renderStar(this.ctx, this.canvasWidth, this.canvasHeight, ts)` ✅
+- `calcStarVertices(cx, cy, outerR)` — Task 2 导出,Task 4 在 `onCanvasTap` 中传给 `detectHitArm` ✅
+- `detectHitArm(relX, relY, cx, cy, outerR, calcVertices)` — Task 1 定义,Task 4 调用 ✅
+- `armIndexToDomain(armIdx)` — Task 1 定义,Task 4 调用 ✅
+- `FillAnimator(getCurrentTs, onFrame, onComplete)` — Task 3 定义,Task 4 实例化 ✅
+- `FillAnimator.prototype.start(targetTs, duration)` — Task 3 定义,Task 4 调用 ✅
+- `FillAnimator.prototype.cancel()` — Task 3 定义,Task 4 的 `beforeDestroy` 调用 ✅

+ 2278 - 0
docs/superpowers/plans/2026-06-09-article-publishing-system.md

@@ -0,0 +1,2278 @@
+# 文章内容发布系统 — 实施计划
+
+> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
+
+**Goal:** 建立三端文章发布系统——后端动态CRUD + Web管理端编辑发布 + 小程序端浏览阅读
+
+**Architecture:** 后端新增 Article/ArticleCategory 实体+Service+Controller(含公开端和管理端),DatabaseInitializer 自动建表;Web管理端新增3个Vue页面+API封装+路由/菜单;小程序端新增文章详情页,3个页面从硬编码改为API数据源
+
+**Tech Stack:** Spring Boot 2.7.18 + MyBatis-Plus / Vue 2 + Element UI 2.15.13 + vue-quill-editor / uni-app Vue 2 小程序
+
+---
+
+## 文件结构总览
+
+### 后端新增(8个文件)
+| 文件 | 路径 | 职责 |
+|:-----|:-----|:-----|
+| ArticleCategory.java | entity/ | 文章分类实体 |
+| Article.java | entity/ | 文章实体 |
+| ArticleCategoryMapper.java | mapper/ | 分类Mapper |
+| ArticleMapper.java | mapper/ | 文章Mapper |
+| ArticleCategoryService.java | service/ | 分类CRUD |
+| ArticleService.java | service/ | 文章CRUD+查询逻辑 |
+| ArticleController.java | controller/content/ | 公开端点(list/detail/featured/categories/record-read) |
+| AdminArticleController.java | controller/admin/ | 管理端点(CRUD/publish/toggle-featured) |
+
+### 后端修改(2个文件)
+| 文件 | 路径 | 改动 |
+|:-----|:-----|:-----|
+| DatabaseInitializer.java | config/ | 新增 article_categories + articles 建表SQL |
+| WebConfig.java | config/ | JWT排除列表新增4个公开端点 |
+
+### Web管理端新增(4个文件)
+| 文件 | 路径 | 职责 |
+|:-----|:-----|:-----|
+| article.js | src/api/ | 文章/分类API封装 |
+| ArticleManage.vue | src/views/admin/ | 文章列表管理 |
+| ArticleEdit.vue | src/views/admin/ | 文章创建/编辑(富文本) |
+| ArticleCategory.vue | src/views/admin/ | 分类管理 |
+
+### Web管理端修改(2个文件)
+| 文件 | 路径 | 改动 |
+|:-----|:-----|:-----|
+| router/index.js | src/router/ | 新增3个文章管理路由+adminRoutes守卫 |
+| Layout.vue | src/views/ | 系统管理菜单新增"文章管理"项 |
+
+### 小程序端新增(1个文件+1个目录)
+| 文件 | 路径 | 职责 |
+|:-----|:-----|:-----|
+| article-detail.vue | pages/mind/article-detail/ | 文章详情页(rich-text渲染) |
+
+### 小程序端修改(4个文件)
+| 文件 | 路径 | 改动 |
+|:-----|:-----|:-----|
+| api.js | utils/ | 新增6个文章API函数 |
+| articles.vue | pages/mind/ | 删除mock→调用API |
+| index.vue | pages/mind/ | 推荐阅读从API获取 |
+| discover/index.vue | pages/discover/ | 精选文章从API获取 |
+| pages.json | 根目录 | 注册article-detail页面 |
+
+---
+
+## Task 1: 后端数据层 — Entity + Mapper + 建表SQL
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/ArticleCategory.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/entity/Article.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/ArticleCategoryMapper.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/mapper/ArticleMapper.java`
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
+
+- [ ] **Step 1: 创建 ArticleCategory 实体**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+@TableName("article_categories")
+public class ArticleCategory implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String name;
+
+    private String icon;
+
+    private String color;
+
+    private Integer sortOrder;
+
+    private Integer status;
+}
+```
+
+- [ ] **Step 2: 创建 Article 实体**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("articles")
+public class Article implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long categoryId;
+
+    private String title;
+
+    private String summary;
+
+    private String coverImage;
+
+    private String content;
+
+    private String tags;
+
+    private String author;
+
+    private Integer readTime;
+
+    private String relatedDimensions;
+
+    private String status;
+
+    private Integer isFeatured;
+
+    private Date publishedAt;
+
+    private Integer viewCount;
+
+    private Long createdBy;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}
+```
+
+- [ ] **Step 3: 创建 Mapper 接口**
+
+```java
+// ArticleCategoryMapper.java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ArticleCategory;
+
+public interface ArticleCategoryMapper extends BaseMapper<ArticleCategory> {
+}
+```
+
+```java
+// ArticleMapper.java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.Article;
+
+public interface ArticleMapper extends BaseMapper<Article> {
+}
+```
+
+- [ ] **Step 4: 在 DatabaseInitializer 中新增建表SQL**
+
+在 `initializeTables()` 的 `createTableSQLs` 列表末尾追加:
+
+```java
+// 文章分类表
+"CREATE TABLE IF NOT EXISTS article_categories (" +
+"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+"name VARCHAR(50) NOT NULL, " +
+"icon VARCHAR(20), " +
+"color VARCHAR(20), " +
+"sort_order INT DEFAULT 0, " +
+"status TINYINT DEFAULT 1, " +
+"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP" +
+") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+
+// 文章表
+"CREATE TABLE IF NOT EXISTS articles (" +
+"id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+"category_id BIGINT, " +
+"title VARCHAR(200) NOT NULL, " +
+"summary VARCHAR(500), " +
+"cover_image VARCHAR(500), " +
+"content LONGTEXT, " +
+"tags VARCHAR(200), " +
+"author VARCHAR(100), " +
+"read_time INT DEFAULT 0, " +
+"related_dimensions VARCHAR(100), " +
+"status VARCHAR(20) DEFAULT 'draft', " +
+"is_featured TINYINT DEFAULT 0, " +
+"published_at DATETIME, " +
+"view_count INT DEFAULT 0, " +
+"created_by BIGINT, " +
+"created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+"updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+"INDEX idx_category_id (category_id), " +
+"INDEX idx_status (status), " +
+"INDEX idx_is_featured (is_featured)" +
+") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4",
+```
+
+在 `initializeDefaultData()` 方法末尾追加默认分类种子数据:
+
+```java
+// 初始化默认文章分类(仅首次)
+try {
+    Long categoryCount = jdbcTemplate.queryForObject(
+        "SELECT COUNT(*) FROM article_categories", Long.class);
+    if (categoryCount != null && categoryCount == 0) {
+        jdbcTemplate.update("INSERT INTO article_categories (name, icon, color, sort_order, status) VALUES ('育儿', '👶', '#5B9BD5', 1, 1)");
+        jdbcTemplate.update("INSERT INTO article_categories (name, icon, color, sort_order, status) VALUES ('心理', '🧠', '#8B5CF6', 2, 1)");
+        jdbcTemplate.update("INSERT INTO article_categories (name, icon, color, sort_order, status) VALUES ('健康', '💪', '#10B981', 3, 1)");
+        jdbcTemplate.update("INSERT INTO article_categories (name, icon, color, sort_order, status) VALUES ('活动', '🏃', '#F97316', 4, 1)");
+        jdbcTemplate.update("INSERT INTO article_categories (name, icon, color, sort_order, status) VALUES ('学习', '📚', '#EC4899', 5, 1)");
+        log.info("默认文章分类初始化完成");
+    }
+} catch (Exception e) {
+    log.warn("初始化文章分类失败: {}", e.getMessage());
+}
+```
+
+- [ ] **Step 5: 修改 WebConfig.java JWT排除列表**
+
+在 `excludePathPatterns` 中追加4个公开端点:
+
+```java
+"/api/articles/list",
+"/api/articles/detail",
+"/api/articles/featured",
+"/api/articles/categories",
+"/api/articles/record-read"
+```
+
+- [ ] **Step 6: 编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+
+Expected: BUILD SUCCESS
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/entity/ArticleCategory.java \
+  cfc-backend/src/main/java/com/etotem/cfc/entity/Article.java \
+  cfc-backend/src/main/java/com/etotem/cfc/mapper/ArticleCategoryMapper.java \
+  cfc-backend/src/main/java/com/etotem/cfc/mapper/ArticleMapper.java \
+  cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java \
+  cfc-backend/src/main/java/com/etotem/cfc/config/WebConfig.java
+git commit -m "feat: add article/category entities, mappers, DB schema and JWT exclusions"
+```
+
+---
+
+## Task 2: 后端 Service 层
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/ArticleCategoryService.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java`
+
+- [ ] **Step 1: 创建 ArticleCategoryService**
+
+```java
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.ArticleCategory;
+import com.etotem.cfc.mapper.ArticleCategoryMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+
+@Service
+public class ArticleCategoryService {
+
+    @Resource
+    private ArticleCategoryMapper articleCategoryMapper;
+
+    public List<ArticleCategory> getActiveCategories() {
+        LambdaQueryWrapper<ArticleCategory> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ArticleCategory::getStatus, 1)
+               .orderByAsc(ArticleCategory::getSortOrder);
+        return articleCategoryMapper.selectList(wrapper);
+    }
+
+    public List<ArticleCategory> listAll() {
+        LambdaQueryWrapper<ArticleCategory> wrapper = new LambdaQueryWrapper<>();
+        wrapper.orderByAsc(ArticleCategory::getSortOrder);
+        return articleCategoryMapper.selectList(wrapper);
+    }
+
+    public ArticleCategory getById(Long id) {
+        return articleCategoryMapper.selectById(id);
+    }
+
+    public void create(ArticleCategory category) {
+        if (category.getStatus() == null) {
+            category.setStatus(1);
+        }
+        if (category.getSortOrder() == null) {
+            category.setSortOrder(0);
+        }
+        articleCategoryMapper.insert(category);
+    }
+
+    public boolean update(ArticleCategory category) {
+        category.setUpdatedAt(new Date());
+        return articleCategoryMapper.updateById(category) > 0;
+    }
+
+    public boolean delete(Long id) {
+        return articleCategoryMapper.deleteById(id) > 0;
+    }
+}
+```
+
+- [ ] **Step 2: 创建 ArticleService**
+
+```java
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.entity.Article;
+import com.etotem.cfc.entity.ArticleCategory;
+import com.etotem.cfc.mapper.ArticleMapper;
+import com.etotem.cfc.mapper.ArticleCategoryMapper;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Service
+public class ArticleService {
+
+    @Resource
+    private ArticleMapper articleMapper;
+
+    @Resource
+    private ArticleCategoryMapper articleCategoryMapper;
+
+    /**
+     * 公开端 - 文章分页列表(只返回published)
+     */
+    public Map<String, Object> listPublished(Long categoryId, String keyword, int page, int size) {
+        Page<Article> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Article::getStatus, "published");
+        if (categoryId != null) {
+            wrapper.eq(Article::getCategoryId, categoryId);
+        }
+        if (keyword != null && !keyword.isEmpty()) {
+            wrapper.and(w -> w.like(Article::getTitle, keyword).or().like(Article::getSummary, keyword));
+        }
+        wrapper.orderByDesc(Article::getPublishedAt);
+        Page<Article> result = articleMapper.selectPage(pageParam, wrapper);
+
+        // 填充分类名
+        Map<Long, String> categoryMap = getCategoryNameMap();
+        for (Article a : result.getRecords()) {
+            a.setTags(a.getTags()); // 触发Lombok getter确保字段可用
+            String catName = categoryMap.get(a.getCategoryId());
+            // 将categoryName放入额外字段(通过Map返回)
+        }
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("records", fillCategoryName(result.getRecords(), categoryMap));
+        data.put("total", result.getTotal());
+        data.put("page", page);
+        data.put("size", size);
+        return data;
+    }
+
+    /**
+     * 公开端 - 文章详情(含内容)
+     */
+    public Article getDetail(Long id) {
+        Article article = articleMapper.selectById(id);
+        if (article != null && "published".equals(article.getStatus())) {
+            // 增加浏览次数
+            article.setViewCount(article.getViewCount() + 1);
+            articleMapper.updateById(article);
+        }
+        return article;
+    }
+
+    /**
+     * 公开端 - 精选文章
+     */
+    public List<Article> getFeatured(int size) {
+        LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Article::getStatus, "published")
+               .eq(Article::getIsFeatured, 1)
+               .orderByDesc(Article::getPublishedAt)
+               .last("LIMIT " + size);
+        List<Article> articles = articleMapper.selectList(wrapper);
+        return fillCategoryName(articles, getCategoryNameMap());
+    }
+
+    /**
+     * 管理端 - 文章分页列表(含draft)
+     */
+    public Map<String, Object> adminList(String status, Long categoryId, String keyword, int page, int size) {
+        Page<Article> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<Article> wrapper = new LambdaQueryWrapper<>();
+        if (status != null && !status.isEmpty()) {
+            wrapper.eq(Article::getStatus, status);
+        }
+        if (categoryId != null) {
+            wrapper.eq(Article::getCategoryId, categoryId);
+        }
+        if (keyword != null && !keyword.isEmpty()) {
+            wrapper.and(w -> w.like(Article::getTitle, keyword).or().like(Article::getSummary, keyword));
+        }
+        wrapper.orderByDesc(Article::getCreatedAt);
+        Page<Article> result = articleMapper.selectPage(pageParam, wrapper);
+
+        Map<String, Object> data = new HashMap<>();
+        data.put("records", fillCategoryName(result.getRecords(), getCategoryNameMap()));
+        data.put("total", result.getTotal());
+        data.put("page", page);
+        data.put("size", size);
+        return data;
+    }
+
+    /**
+     * 管理端 - 创建文章
+     */
+    public Long create(Article article, Long userId) {
+        article.setCreatedBy(userId);
+        article.setViewCount(0);
+        article.setCreatedAt(new Date());
+        article.setUpdatedAt(new Date());
+        if ("published".equals(article.getStatus()) && article.getPublishedAt() == null) {
+            article.setPublishedAt(new Date());
+        }
+        articleMapper.insert(article);
+        return article.getId();
+    }
+
+    /**
+     * 管理端 - 更新文章
+     */
+    public boolean update(Article article) {
+        article.setUpdatedAt(new Date());
+        return articleMapper.updateById(article) > 0;
+    }
+
+    /**
+     * 管理端 - 删除文章
+     */
+    public boolean delete(Long id) {
+        return articleMapper.deleteById(id) > 0;
+    }
+
+    /**
+     * 管理端 - 发布/下架
+     */
+    public boolean publish(Long id, String status) {
+        Article article = articleMapper.selectById(id);
+        if (article == null) return false;
+        article.setStatus(status);
+        article.setUpdatedAt(new Date());
+        if ("published".equals(status) && article.getPublishedAt() == null) {
+            article.setPublishedAt(new Date());
+        }
+        return articleMapper.updateById(article) > 0;
+    }
+
+    /**
+     * 管理端 - 切换精选
+     */
+    public boolean toggleFeatured(Long id, Integer isFeatured) {
+        Article article = articleMapper.selectById(id);
+        if (article == null) return false;
+        article.setIsFeatured(isFeatured);
+        article.setUpdatedAt(new Date());
+        return articleMapper.updateById(article) > 0;
+    }
+
+    // ========== 私有方法 ==========
+
+    private Map<Long, String> getCategoryNameMap() {
+        List<ArticleCategory> categories = articleCategoryMapper.selectList(null);
+        return categories.stream().collect(Collectors.toMap(ArticleCategory::getId, ArticleCategory::getName));
+    }
+
+    private List<Article> fillCategoryName(List<Article> articles, Map<Long, String> categoryMap) {
+        for (Article a : articles) {
+            // Article实体没有categoryName字段,在Controller层用Map包装
+        }
+        return articles;
+    }
+}
+```
+
+注意:`fillCategoryName` 方法目前只返回Article本身,因为Article实体无categoryName字段。在Controller层通过包装Map补充categoryName。
+
+- [ ] **Step 3: 编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+
+Expected: BUILD SUCCESS
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/service/ArticleCategoryService.java \
+  cfc-backend/src/main/java/com/etotem/cfc/service/ArticleService.java
+git commit -m "feat: add article/category service layer"
+```
+
+---
+
+## Task 3: 后端 Controller 层
+
+**Files:**
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/content/ArticleController.java`
+- Create: `cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java`
+
+- [ ] **Step 1: 创建公开端 ArticleController**
+
+```java
+package com.etotem.cfc.controller.content;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Article;
+import com.etotem.cfc.entity.ArticleCategory;
+import com.etotem.cfc.entity.ArticleReadingRecord;
+import com.etotem.cfc.mapper.ArticleReadingRecordMapper;
+import com.etotem.cfc.service.ArticleCategoryService;
+import com.etotem.cfc.service.ArticleService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+@RestController
+@RequestMapping("/api/articles")
+public class ArticleController {
+
+    @Resource
+    private ArticleService articleService;
+
+    @Resource
+    private ArticleCategoryService articleCategoryService;
+
+    @Resource
+    private ArticleReadingRecordMapper articleReadingRecordMapper;
+
+    /**
+     * 文章分页列表(只返回published)
+     */
+    @PostMapping("/list")
+    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> body) {
+        Long categoryId = body.get("categoryId") != null ? ((Number) body.get("categoryId")).longValue() : null;
+        String keyword = (String) body.get("keyword");
+        int page = body.get("page") != null ? ((Number) body.get("page")).intValue() : 1;
+        int size = body.get("size") != null ? ((Number) body.get("size")).intValue() : 10;
+
+        Map<String, Object> data = articleService.listPublished(categoryId, keyword, page, size);
+        return Result.success(data);
+    }
+
+    /**
+     * 文章详情(含内容)
+     */
+    @PostMapping("/detail")
+    public Result<Map<String, Object>> detail(@RequestBody Map<String, Object> body) {
+        Long id = Long.valueOf(body.get("id").toString());
+        Article article = articleService.getDetail(id);
+        if (article == null) {
+            return Result.error("文章不存在");
+        }
+        // 包装分类名
+        Map<String, Object> data = new HashMap<>();
+        data.put("id", article.getId());
+        data.put("categoryId", article.getCategoryId());
+        data.put("title", article.getTitle());
+        data.put("summary", article.getSummary());
+        data.put("coverImage", article.getCoverImage());
+        data.put("content", article.getContent());
+        data.put("tags", article.getTags());
+        data.put("author", article.getAuthor());
+        data.put("readTime", article.getReadTime());
+        data.put("relatedDimensions", article.getRelatedDimensions());
+        data.put("status", article.getStatus());
+        data.put("isFeatured", article.getIsFeatured());
+        data.put("publishedAt", article.getPublishedAt());
+        data.put("viewCount", article.getViewCount());
+        data.put("createdAt", article.getCreatedAt());
+        // 补充分类名
+        if (article.getCategoryId() != null) {
+            ArticleCategory cat = articleCategoryService.getById(article.getCategoryId());
+            data.put("categoryName", cat != null ? cat.getName() : "");
+            data.put("categoryIcon", cat != null ? cat.getIcon() : "");
+            data.put("categoryColor", cat != null ? cat.getColor() : "");
+        }
+        return Result.success(data);
+    }
+
+    /**
+     * 精选文章列表
+     */
+    @PostMapping("/featured")
+    public Result<List<Article>> featured(@RequestBody Map<String, Object> body) {
+        int size = body.get("size") != null ? ((Number) body.get("size")).intValue() : 5;
+        List<Article> articles = articleService.getFeatured(size);
+        return Result.success(articles);
+    }
+
+    /**
+     * 分类列表(只返回启用的)
+     */
+    @PostMapping("/categories")
+    public Result<List<ArticleCategory>> categories() {
+        return Result.success(articleCategoryService.getActiveCategories());
+    }
+
+    /**
+     * 记录阅读行为
+     */
+    @PostMapping("/record-read")
+    public Result<String> recordRead(@RequestBody Map<String, Object> body) {
+        Long articleId = Long.valueOf(body.get("articleId").toString());
+        Integer durationSeconds = body.get("durationSeconds") != null ? ((Number) body.get("durationSeconds")).intValue() : 0;
+        Long childId = body.get("childId") != null ? ((Number) body.get("childId")).longValue() : null;
+        Long userId = body.get("userId") != null ? ((Number) body.get("userId")).longValue() : null;
+
+        ArticleReadingRecord record = new ArticleReadingRecord();
+        record.setUserId(userId);
+        record.setChildId(childId);
+        record.setContent("article:" + articleId);
+        record.setDurationSeconds(durationSeconds);
+        record.setReadAt(new Date());
+        record.setCreatedAt(new Date());
+        articleReadingRecordMapper.insert(record);
+
+        return Result.success("记录成功");
+    }
+}
+```
+
+注意:需要创建 `ArticleReadingRecordMapper.java`,如果尚不存在:
+
+```java
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.ArticleReadingRecord;
+
+public interface ArticleReadingRecordMapper extends BaseMapper<ArticleReadingRecord> {
+}
+```
+
+- [ ] **Step 2: 创建管理端 AdminArticleController**
+
+```java
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.Article;
+import com.etotem.cfc.entity.ArticleCategory;
+import com.etotem.cfc.service.ArticleCategoryService;
+import com.etotem.cfc.service.ArticleService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import javax.annotation.Resource;
+import java.io.File;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/api/admin/articles")
+public class AdminArticleController {
+
+    @Resource
+    private ArticleService articleService;
+
+    @Resource
+    private ArticleCategoryService articleCategoryService;
+
+    /**
+     * 后台文章列表
+     */
+    @PostMapping("/list")
+    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> body) {
+        String status = (String) body.get("status");
+        Long categoryId = body.get("categoryId") != null ? ((Number) body.get("categoryId")).longValue() : null;
+        String keyword = (String) body.get("keyword");
+        int page = body.get("page") != null ? ((Number) body.get("page")).intValue() : 1;
+        int size = body.get("size") != null ? ((Number) body.get("size")).intValue() : 10;
+
+        Map<String, Object> data = articleService.adminList(status, categoryId, keyword, page, size);
+        return Result.success(data);
+    }
+
+    /**
+     * 创建文章
+     */
+    @PostMapping("/create")
+    public Result<Long> create(@RequestBody Article article) {
+        Long id = articleService.create(article, null);
+        return Result.success(id);
+    }
+
+    /**
+     * 更新文章
+     */
+    @PostMapping("/update")
+    public Result<String> update(@RequestBody Article article) {
+        boolean success = articleService.update(article);
+        return success ? Result.success("更新成功") : Result.error("更新失败");
+    }
+
+    /**
+     * 删除文章
+     */
+    @PostMapping("/delete")
+    public Result<String> delete(@RequestBody Map<String, Object> body) {
+        Long id = Long.valueOf(body.get("id").toString());
+        boolean success = articleService.delete(id);
+        return success ? Result.success("删除成功") : Result.error("删除失败");
+    }
+
+    /**
+     * 发布/下架
+     */
+    @PostMapping("/publish")
+    public Result<String> publish(@RequestBody Map<String, Object> body) {
+        Long id = Long.valueOf(body.get("id").toString());
+        String status = (String) body.get("status");
+        boolean success = articleService.publish(id, status);
+        return success ? Result.success("操作成功") : Result.error("操作失败");
+    }
+
+    /**
+     * 切换精选
+     */
+    @PostMapping("/toggle-featured")
+    public Result<String> toggleFeatured(@RequestBody Map<String, Object> body) {
+        Long id = Long.valueOf(body.get("id").toString());
+        Integer isFeatured = ((Number) body.get("isFeatured")).intValue();
+        boolean success = articleService.toggleFeatured(id, isFeatured);
+        return success ? Result.success("操作成功") : Result.error("操作失败");
+    }
+
+    // ========== 分类管理接口 ==========
+
+    /**
+     * 分类列表(含停用)
+     */
+    @PostMapping("/categories/list")
+    public Result<List<ArticleCategory>> categoryList() {
+        return Result.success(articleCategoryService.listAll());
+    }
+
+    /**
+     * 创建分类
+     */
+    @PostMapping("/categories/create")
+    public Result<String> categoryCreate(@RequestBody ArticleCategory category) {
+        articleCategoryService.create(category);
+        return Result.success("创建成功");
+    }
+
+    /**
+     * 更新分类
+     */
+    @PostMapping("/categories/update")
+    public Result<String> categoryUpdate(@RequestBody ArticleCategory category) {
+        boolean success = articleCategoryService.update(category);
+        return success ? Result.success("更新成功") : Result.error("更新失败");
+    }
+
+    /**
+     * 删除分类
+     */
+    @PostMapping("/categories/delete")
+    public Result<String> categoryDelete(@RequestBody Map<String, Object> body) {
+        Long id = Long.valueOf(body.get("id").toString());
+        boolean success = articleCategoryService.delete(id);
+        return success ? Result.success("删除成功") : Result.error("删除失败");
+    }
+}
+```
+
+- [ ] **Step 3: 检查 ArticleReadingRecordMapper 是否已存在**
+
+搜索 `ArticleReadingRecordMapper` — 如果不存在,需创建(代码见Step 1中的mapper)。同时在 `DatabaseInitializer` 中确认 `article_reading_records` 表是否已存在建表SQL。如果不存在,需追加建表SQL。
+
+- [ ] **Step 4: 创建文件上传Controller**
+
+```java
+package com.etotem.cfc.controller.admin;
+
+import com.etotem.cfc.common.Result;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.UUID;
+
+@RestController
+@RequestMapping("/api/admin/upload")
+public class UploadController {
+
+    /**
+     * 上传封面图
+     */
+    @PostMapping("/image")
+    public Result<String> uploadImage(MultipartFile file) {
+        if (file == null || file.isEmpty()) {
+            return Result.error("文件不能为空");
+        }
+        try {
+            String originalFilename = file.getOriginalFilename();
+            String ext = "";
+            if (originalFilename != null && originalFilename.contains(".")) {
+                ext = originalFilename.substring(originalFilename.lastIndexOf("."));
+            }
+            String fileName = UUID.randomUUID().toString() + ext;
+            String uploadDir = "uploads/articles/";
+            File dir = new File(uploadDir);
+            if (!dir.exists()) {
+                dir.mkdirs();
+            }
+            File dest = new File(dir, fileName);
+            file.transferTo(dest);
+            String url = "/uploads/articles/" + fileName;
+            return Result.success(url);
+        } catch (IOException e) {
+            return Result.error("上传失败: " + e.getMessage());
+        }
+    }
+}
+```
+
+注意:需要配置Spring Boot静态资源映射,让 `/uploads/**` 可访问。在 `WebConfig.java` 中追加:
+
+```java
+@Override
+public void addResourceHandlers(ResourceHandlerRegistry registry) {
+    registry.addResourceHandler("/uploads/**")
+            .addResourceLocations("file:uploads/");
+}
+```
+
+需在 WebConfig.java 的 import 中追加 `org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry`。
+
+- [ ] **Step 5: 编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+
+Expected: BUILD SUCCESS
+
+- [ ] **Step 6: 启动验证**
+
+Run: `cd cfc-backend && mvn spring-boot:run`
+
+Expected: 控制台出现 `Started` 字样,数据库表自动创建
+
+- [ ] **Step 7: Commit**
+
+```bash
+git add cfc-backend/src/main/java/com/etotem/cfc/controller/content/ArticleController.java \
+  cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminArticleController.java \
+  cfc-backend/src/main/java/com/etotem/cfc/controller/admin/UploadController.java \
+  cfc-backend/src/main/java/com/etotem/cfc/mapper/ArticleReadingRecordMapper.java \
+  cfc-backend/src/main/java/com/etotem/cfc/config/WebConfig.java
+git commit -m "feat: add article/category controllers (public + admin) and image upload"
+```
+
+---
+
+## Task 4: Web管理端 — API封装 + 分类管理页
+
+**Files:**
+- Create: `cfc-web/src/api/article.js`
+- Create: `cfc-web/src/views/admin/ArticleCategory.vue`
+
+- [ ] **Step 1: 创建 article.js API封装**
+
+```javascript
+import request from '@/utils/request'
+
+// ========== 文章管理 ==========
+
+export function getArticleList(params) {
+  return request({ url: '/api/admin/articles/list', method: 'post', data: params })
+}
+
+export function createArticle(data) {
+  return request({ url: '/api/admin/articles/create', method: 'post', data })
+}
+
+export function updateArticle(data) {
+  return request({ url: '/api/admin/articles/update', method: 'post', data })
+}
+
+export function deleteArticle(id) {
+  return request({ url: '/api/admin/articles/delete', method: 'post', data: { id } })
+}
+
+export function publishArticle(id, status) {
+  return request({ url: '/api/admin/articles/publish', method: 'post', data: { id, status } })
+}
+
+export function toggleFeatured(id, isFeatured) {
+  return request({ url: '/api/admin/articles/toggle-featured', method: 'post', data: { id, isFeatured } })
+}
+
+// ========== 分类管理 ==========
+
+export function getCategoryList() {
+  return request({ url: '/api/admin/articles/categories/list', method: 'post' })
+}
+
+export function createCategory(data) {
+  return request({ url: '/api/admin/articles/categories/create', method: 'post', data })
+}
+
+export function updateCategory(data) {
+  return request({ url: '/api/admin/articles/categories/update', method: 'post', data })
+}
+
+export function deleteCategory(id) {
+  return request({ url: '/api/admin/articles/categories/delete', method: 'post', data: { id } })
+}
+
+// ========== 图片上传 ==========
+
+export function uploadImage(file) {
+  const formData = new FormData()
+  formData.append('file', file)
+  return request({
+    url: '/api/admin/upload/image',
+    method: 'post',
+    data: formData,
+    headers: { 'Content-Type': 'multipart/form-data' }
+  })
+}
+```
+
+- [ ] **Step 2: 创建 ArticleCategory.vue 分类管理页**
+
+```vue
+<template>
+  <div class="article-category">
+    <el-card>
+      <div slot="header">
+        <span>文章分类管理</span>
+        <el-button style="float: right" type="primary" size="small" @click="handleCreate">+ 添加分类</el-button>
+      </div>
+      <el-table :data="categories" stripe>
+        <el-table-column prop="id" label="ID" width="80"></el-table-column>
+        <el-table-column prop="name" label="分类名称"></el-table-column>
+        <el-table-column prop="icon" label="图标" width="80">
+          <template slot-scope="scope">{{ scope.row.icon }}</template>
+        </el-table-column>
+        <el-table-column prop="color" label="颜色" width="100">
+          <template slot-scope="scope">
+            <span :style="{ color: scope.row.color, fontWeight: 'bold' }">{{ scope.row.color }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="sortOrder" label="排序" width="80"></el-table-column>
+        <el-table-column prop="status" label="状态" width="100">
+          <template slot-scope="scope">
+            <el-tag :type="scope.row.status === 1 ? 'success' : 'danger'">
+              {{ scope.row.status === 1 ? '启用' : '停用' }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="150">
+          <template slot-scope="scope">
+            <el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
+            <el-button size="mini" :type="scope.row.status === 1 ? 'warning' : 'success'" @click="toggleStatus(scope.row)">
+              {{ scope.row.status === 1 ? '停用' : '启用' }}
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-card>
+
+    <el-dialog :visible.sync="dialogVisible" :title="isEdit ? '编辑分类' : '添加分类'" width="500px">
+      <el-form :model="form" label-width="80px">
+        <el-form-item label="分类名称">
+          <el-input v-model="form.name"></el-input>
+        </el-form-item>
+        <el-form-item label="图标">
+          <el-input v-model="form.icon" placeholder="如: 📖"></el-input>
+        </el-form-item>
+        <el-form-item label="颜色">
+          <el-input v-model="form.color" placeholder="如: #5B9BD5"></el-input>
+        </el-form-item>
+        <el-form-item label="排序">
+          <el-input-number v-model="form.sortOrder" :min="0"></el-input-number>
+        </el-form-item>
+      </el-form>
+      <div slot="footer">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" @click="handleSubmit">保存</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { getCategoryList, createCategory, updateCategory, deleteCategory } from '@/api/article'
+
+export default {
+  name: 'ArticleCategory',
+  data() {
+    return {
+      categories: [],
+      dialogVisible: false,
+      isEdit: false,
+      form: {
+        id: null,
+        name: '',
+        icon: '',
+        color: '',
+        sortOrder: 0,
+        status: 1
+      }
+    }
+  },
+  created() {
+    this.loadData()
+  },
+  methods: {
+    async loadData() {
+      try {
+        const res = await getCategoryList()
+        if (res.data) {
+          this.categories = res.data
+        }
+      } catch (e) {
+        console.error(e)
+      }
+    },
+    handleCreate() {
+      this.isEdit = false
+      this.form = { id: null, name: '', icon: '', color: '', sortOrder: 0, status: 1 }
+      this.dialogVisible = true
+    },
+    handleEdit(row) {
+      this.isEdit = true
+      this.form = { ...row }
+      this.dialogVisible = true
+    },
+    async toggleStatus(row) {
+      row.status = row.status === 1 ? 0 : 1
+      try {
+        await updateCategory(row)
+        this.$message.success('状态已更新')
+      } catch (e) {
+        row.status = row.status === 1 ? 0 : 1
+        this.$message.error('更新失败')
+      }
+    },
+    async handleSubmit() {
+      try {
+        if (this.isEdit) {
+          await updateCategory(this.form)
+        } else {
+          await createCategory(this.form)
+        }
+        this.$message.success('保存成功')
+        this.dialogVisible = false
+        this.loadData()
+      } catch (e) {
+        this.$message.error('保存失败')
+      }
+    }
+  }
+}
+</script>
+
+<style scoped>
+.article-category {
+  padding: 20px;
+}
+</style>
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add cfc-web/src/api/article.js cfc-web/src/views/admin/ArticleCategory.vue
+git commit -m "feat(web): add article API client and category management page"
+```
+
+---
+
+## Task 5: Web管理端 — 文章列表管理页
+
+**Files:**
+- Create: `cfc-web/src/views/admin/ArticleManage.vue`
+
+- [ ] **Step 1: 创建 ArticleManage.vue**
+
+```vue
+<template>
+  <div class="article-manage">
+    <el-card>
+      <div slot="header">
+        <span>文章管理</span>
+        <el-button style="float: right" type="primary" size="small" @click="handleCreate">+ 新建文章</el-button>
+      </div>
+
+      <!-- 筛选栏 -->
+      <div class="filter-bar">
+        <el-select v-model="filterStatus" placeholder="状态" clearable size="small" style="width: 120px" @change="loadData">
+          <el-option label="草稿" value="draft"></el-option>
+          <el-option label="已发布" value="published"></el-option>
+          <el-option label="已下架" value="archived"></el-option>
+        </el-select>
+        <el-select v-model="filterCategory" placeholder="分类" clearable size="small" style="width: 140px; margin-left: 10px" @change="loadData">
+          <el-option v-for="cat in categoryOptions" :key="cat.id" :label="cat.name" :value="cat.id"></el-option>
+        </el-select>
+        <el-input v-model="filterKeyword" placeholder="搜索标题" clearable size="small" style="width: 200px; margin-left: 10px" @clear="loadData"></el-input>
+        <el-button size="small" type="primary" style="margin-left: 10px" @click="loadData">搜索</el-button>
+      </div>
+
+      <el-table :data="articles" stripe v-loading="loading">
+        <el-table-column prop="id" label="ID" width="80"></el-table-column>
+        <el-table-column prop="title" label="标题" show-overflow-tooltip></el-table-column>
+        <el-table-column prop="categoryName" label="分类" width="100"></el-table-column>
+        <el-table-column prop="author" label="作者" width="100"></el-table-column>
+        <el-table-column prop="status" label="状态" width="100">
+          <template slot-scope="scope">
+            <el-tag :type="statusTagType(scope.row.status)">{{ statusLabel(scope.row.status) }}</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column prop="isFeatured" label="精选" width="80">
+          <template slot-scope="scope">
+            <el-switch :value="scope.row.isFeatured === 1" @change="handleToggleFeatured(scope.row)"></el-switch>
+          </template>
+        </el-table-column>
+        <el-table-column prop="publishedAt" label="发布时间" width="160">
+          <template slot-scope="scope">{{ scope.row.publishedAt || '-' }}</template>
+        </el-table-column>
+        <el-table-column prop="viewCount" label="浏览" width="80"></el-table-column>
+        <el-table-column label="操作" width="220">
+          <template slot-scope="scope">
+            <el-button size="mini" @click="handleEdit(scope.row)">编辑</el-button>
+            <el-button size="mini" :type="scope.row.status === 'published' ? 'warning' : 'success'" @click="handlePublish(scope.row)">
+              {{ scope.row.status === 'published' ? '下架' : '发布' }}
+            </el-button>
+            <el-button size="mini" type="danger" @click="handleDelete(scope.row)">删除</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <el-pagination
+        v-if="total > 0"
+        :current-page="page"
+        :page-size="size"
+        :total="total"
+        layout="total, prev, pager, next"
+        @current-change="handlePageChange"
+        style="margin-top: 20px; text-align: right"
+      ></el-pagination>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { getArticleList, deleteArticle, publishArticle, toggleFeatured } from '@/api/article'
+import { getCategoryList } from '@/api/article'
+
+export default {
+  name: 'ArticleManage',
+  data() {
+    return {
+      articles: [],
+      categoryOptions: [],
+      loading: false,
+      filterStatus: '',
+      filterCategory: null,
+      filterKeyword: '',
+      page: 1,
+      size: 10,
+      total: 0
+    }
+  },
+  created() {
+    this.loadCategories()
+    this.loadData()
+  },
+  methods: {
+    async loadCategories() {
+      try {
+        const res = await getCategoryList()
+        if (res.data) {
+          this.categoryOptions = res.data
+        }
+      } catch (e) { console.error(e) }
+    },
+    async loadData() {
+      this.loading = true
+      try {
+        const params = { page: this.page, size: this.size }
+        if (this.filterStatus) params.status = this.filterStatus
+        if (this.filterCategory) params.categoryId = this.filterCategory
+        if (this.filterKeyword) params.keyword = this.filterKeyword
+        const res = await getArticleList(params)
+        if (res.data) {
+          this.articles = res.data.records || []
+          this.total = res.data.total || 0
+        }
+      } catch (e) { console.error(e) }
+      this.loading = false
+    },
+    handleCreate() {
+      this.$router.push('/article-edit')
+    },
+    handleEdit(row) {
+      this.$router.push('/article-edit?id=' + row.id)
+    },
+    async handlePublish(row) {
+      const newStatus = row.status === 'published' ? 'archived' : 'published'
+      try {
+        await publishArticle(row.id, newStatus)
+        this.$message.success('操作成功')
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败') }
+    },
+    async handleToggleFeatured(row) {
+      const newFeatured = row.isFeatured === 1 ? 0 : 1
+      try {
+        await toggleFeatured(row.id, newFeatured)
+        this.$message.success('操作成功')
+        this.loadData()
+      } catch (e) { this.$message.error('操作失败') }
+    },
+    async handleDelete(row) {
+      try {
+        await this.$confirm('确定删除该文章?', '提示', { type: 'warning' })
+        await deleteArticle(row.id)
+        this.$message.success('删除成功')
+        this.loadData()
+      } catch (e) { /* 取消 */ }
+    },
+    handlePageChange(page) {
+      this.page = page
+      this.loadData()
+    },
+    statusTagType(status) {
+      if (status === 'published') return 'success'
+      if (status === 'draft') return 'info'
+      if (status === 'archived') return 'warning'
+      return 'info'
+    },
+    statusLabel(status) {
+      if (status === 'published') return '已发布'
+      if (status === 'draft') return '草稿'
+      if (status === 'archived') return '已下架'
+      return status
+    }
+  }
+}
+</script>
+
+<style scoped>
+.article-manage {
+  padding: 20px;
+}
+.filter-bar {
+  display: flex;
+  align-items: center;
+  margin-bottom: 20px;
+}
+</style>
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add cfc-web/src/views/admin/ArticleManage.vue
+git commit -m "feat(web): add article management list page"
+```
+
+---
+
+## Task 6: Web管理端 — 文章编辑页(含富文本)
+
+**Files:**
+- Create: `cfc-web/src/views/admin/ArticleEdit.vue`
+
+前置:需安装 `vue-quill-editor`
+
+```bash
+cd cfc-web && npm install vue-quill-editor --save
+```
+
+- [ ] **Step 1: 安装 vue-quill-editor**
+
+Run: `cd cfc-web && npm install vue-quill-editor --save`
+
+- [ ] **Step 2: 创建 ArticleEdit.vue**
+
+```vue
+<template>
+  <div class="article-edit">
+    <el-card>
+      <div slot="header">
+        <span>{{ isEdit ? '编辑文章' : '新建文章' }}</span>
+        <el-button style="float: right; margin-left: 10px" @click="goBack">返回</el-button>
+      </div>
+
+      <el-form :model="form" label-width="100px" v-loading="loading">
+        <el-form-item label="标题">
+          <el-input v-model="form.title" placeholder="文章标题"></el-input>
+        </el-form-item>
+        <el-form-item label="分类">
+          <el-select v-model="form.categoryId" placeholder="选择分类">
+            <el-option v-for="cat in categories" :key="cat.id" :label="cat.name" :value="cat.id"></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="摘要">
+          <el-input v-model="form.summary" type="textarea" :rows="3" placeholder="文章摘要"></el-input>
+        </el-form-item>
+        <el-form-item label="封面图">
+          <el-upload
+            class="cover-uploader"
+            :action="uploadUrl"
+            :show-file-list="false"
+            :on-success="handleUploadSuccess"
+            :headers="uploadHeaders"
+            accept="image/*"
+          >
+            <img v-if="form.coverImage" :src="coverFullUrl" class="cover-preview" />
+            <i v-else class="el-icon-plus cover-uploader-icon"></i>
+          </el-upload>
+        </el-form-item>
+        <el-form-item label="正文内容">
+          <div class="quill-editor-wrap">
+            <quill-editor v-model="form.content" :options="editorOption"></quill-editor>
+          </div>
+        </el-form-item>
+        <el-form-item label="标签">
+          <el-input v-model="form.tags" placeholder='JSON数组格式,如: ["育儿","亲子"]'></el-input>
+        </el-form-item>
+        <el-form-item label="作者">
+          <el-input v-model="form.author" placeholder="作者名"></el-input>
+        </el-form-item>
+        <el-form-item label="阅读时间">
+          <el-input-number v-model="form.readTime" :min="0" placeholder="预计阅读分钟数"></el-input-number>
+        </el-form-item>
+        <el-form-item label="关联五维">
+          <el-checkbox-group v-model="selectedDimensions">
+            <el-checkbox label="body">身</el-checkbox>
+            <el-checkbox label="mind">心</el-checkbox>
+            <el-checkbox label="wisdom">智</el-checkbox>
+            <el-checkbox label="action">行</el-checkbox>
+            <el-checkbox label="wealth">富</el-checkbox>
+          </el-checkbox-group>
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio label="draft">草稿</el-radio>
+            <el-radio label="published">发布</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="精选">
+          <el-switch v-model="isFeaturedBool" active-text="精选" inactive-text="普通"></el-switch>
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" @click="handleSave">保存</el-button>
+          <el-button @click="goBack">取消</el-button>
+        </el-form-item>
+      </el-form>
+    </el-card>
+  </div>
+</template>
+
+<script>
+import { quillEditor } from 'vue-quill-editor'
+import 'quill/dist/quill.core.css'
+import 'quill/dist/quill.snow.css'
+import { getArticleList, createArticle, updateArticle, getCategoryList } from '@/api/article'
+
+export default {
+  name: 'ArticleEdit',
+  components: { quillEditor },
+  data() {
+    return {
+      isEdit: false,
+      loading: false,
+      categories: [],
+      selectedDimensions: [],
+      isFeaturedBool: false,
+      form: {
+        id: null,
+        categoryId: null,
+        title: '',
+        summary: '',
+        coverImage: '',
+        content: '',
+        tags: '',
+        author: '',
+        readTime: 3,
+        relatedDimensions: '',
+        status: 'draft',
+        isFeatured: 0
+      },
+      editorOption: {
+        placeholder: '请输入文章内容...',
+        modules: {
+          toolbar: [
+            ['bold', 'italic', 'underline', 'strike'],
+            ['blockquote', 'code-block'],
+            [{ header: 1 }, { header: 2 }],
+            [{ list: 'ordered' }, { list: 'bullet' }],
+            [{ script: 'sub' }, { script: 'super' }],
+            ['link', 'image'],
+            ['clean']
+          ]
+        }
+      },
+      uploadUrl: (process.env.VUE_APP_BASE_API || 'http://localhost:8080') + '/api/admin/upload/image'
+    }
+  },
+  computed: {
+    uploadHeaders() {
+      const token = localStorage.getItem('token')
+      return token ? { Authorization: 'Bearer ' + token } : {}
+    },
+    coverFullUrl() {
+      if (!this.form.coverImage) return ''
+      if (this.form.coverImage.startsWith('http')) return this.form.coverImage
+      return (process.env.VUE_APP_BASE_API || 'http://localhost:8080') + this.form.coverImage
+    }
+  },
+  created() {
+    this.loadCategories()
+    const id = this.$route.query.id
+    if (id) {
+      this.isEdit = true
+      this.form.id = parseInt(id)
+      this.loadArticle(id)
+    }
+  },
+  methods: {
+    async loadCategories() {
+      try {
+        const res = await getCategoryList()
+        if (res.data) {
+          this.categories = res.data
+        }
+      } catch (e) { console.error(e) }
+    },
+    async loadArticle(id) {
+      this.loading = true
+      try {
+        const res = await getArticleList({ page: 1, size: 1000 })
+        if (res.data && res.data.records) {
+          const article = res.data.records.find(a => a.id === parseInt(id))
+          if (article) {
+            this.form = {
+              id: article.id,
+              categoryId: article.categoryId,
+              title: article.title,
+              summary: article.summary || '',
+              coverImage: article.coverImage || '',
+              content: article.content || '',
+              tags: article.tags || '',
+              author: article.author || '',
+              readTime: article.readTime || 3,
+              relatedDimensions: article.relatedDimensions || '',
+              status: article.status || 'draft',
+              isFeatured: article.isFeatured || 0
+            }
+            this.isFeaturedBool = this.form.isFeatured === 1
+            // 解析relatedDimensions
+            try {
+              this.selectedDimensions = JSON.parse(this.form.relatedDimensions || '[]')
+            } catch (e) {
+              this.selectedDimensions = []
+            }
+          }
+        }
+      } catch (e) { console.error(e) }
+      this.loading = false
+    },
+    handleUploadSuccess(res) {
+      if (res.code === 200 && res.data) {
+        this.form.coverImage = res.data
+        this.$message.success('上传成功')
+      } else {
+        this.$message.error('上传失败')
+      }
+    },
+    async handleSave() {
+      this.form.isFeatured = this.isFeaturedBool ? 1 : 0
+      this.form.relatedDimensions = JSON.stringify(this.selectedDimensions)
+      try {
+        if (this.isEdit) {
+          await updateArticle(this.form)
+          this.$message.success('更新成功')
+        } else {
+          await createArticle(this.form)
+          this.$message.success('创建成功')
+        }
+        this.$router.push('/article-manage')
+      } catch (e) {
+        this.$message.error('保存失败')
+      }
+    },
+    goBack() {
+      this.$router.push('/article-manage')
+    }
+  }
+}
+</script>
+
+<style scoped>
+.article-edit {
+  padding: 20px;
+}
+.cover-uploader {
+  display: inline-block;
+}
+.cover-uploader >>> .el-upload {
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  cursor: pointer;
+  width: 200px;
+  height: 120px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  overflow: hidden;
+}
+.cover-uploader >>> .el-upload:hover {
+  border-color: #409EFF;
+}
+.cover-uploader-icon {
+  font-size: 28px;
+  color: #8c939d;
+}
+.cover-preview {
+  width: 200px;
+  height: 120px;
+  object-fit: cover;
+}
+.quill-editor-wrap {
+  width: 100%;
+}
+.quill-editor-wrap >>> .ql-editor {
+  min-height: 300px;
+}
+</style>
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add cfc-web/src/views/admin/ArticleEdit.vue cfc-web/package.json cfc-web/package-lock.json
+git commit -m "feat(web): add article edit page with quill rich text editor"
+```
+
+---
+
+## Task 7: Web管理端 — 路由注册 + 侧边栏菜单
+
+**Files:**
+- Modify: `cfc-web/src/router/index.js`
+- Modify: `cfc-web/src/views/Layout.vue`
+
+- [ ] **Step 1: 在 router/index.js 中添加3个路由**
+
+在 `children` 数组中,`energy-sandbox` 路由后面追加:
+
+```javascript
+// 文章内容管理
+{
+  path: 'article-manage',
+  name: 'ArticleManage',
+  component: () => import('@/views/admin/ArticleManage.vue'),
+  meta: { title: '文章管理' }
+},
+{
+  path: 'article-edit',
+  name: 'ArticleEdit',
+  component: () => import('@/views/admin/ArticleEdit.vue'),
+  meta: { title: '文章编辑' }
+},
+{
+  path: 'article-category',
+  name: 'ArticleCategory',
+  component: () => import('@/views/admin/ArticleCategory.vue'),
+  meta: { title: '文章分类' }
+},
+```
+
+在 `adminRoutes` 数组中追加3个路由名:
+
+```javascript
+'ArticleManage', 'ArticleEdit', 'ArticleCategory'
+```
+
+- [ ] **Step 2: 在 Layout.vue 侧边栏添加文章管理菜单**
+
+在系统管理 `el-submenu` 中,`energy-sandbox` 菜单项后面追加:
+
+```html
+<el-menu-item index="/article-manage">
+  <i class="el-icon-document"></i>
+  <span>文章管理</span>
+</el-menu-item>
+<el-menu-item index="/article-category">
+  <i class="el-icon-menu"></i>
+  <span>文章分类</span>
+</el-menu-item>
+```
+
+- [ ] **Step 3: 在 main.js 中注册 vue-quill-editor**
+
+在 `cfc-web/src/main.js` 中追加:
+
+```javascript
+import VueQuillEditor from 'vue-quill-editor'
+Vue.use(VueQuillEditor)
+```
+
+- [ ] **Step 4: Commit**
+
+```bash
+git add cfc-web/src/router/index.js cfc-web/src/views/Layout.vue cfc-web/src/main.js
+git commit -m "feat(web): add article routes, sidebar menu and quill editor registration"
+```
+
+---
+
+## Task 8: 小程序端 — API函数封装
+
+**Files:**
+- Modify: `cfc-frontend/utils/api.js`
+
+- [ ] **Step 1: 在 api.js 末尾追加6个文章API函数**
+
+```javascript
+// ========== 文章内容 ==========
+
+export const articleList = (data) => request('/api/articles/list', 'POST', data)
+
+export const articleDetail = (data) => request('/api/articles/detail', 'POST', data)
+
+export const articleFeatured = (data) => request('/api/articles/featured', 'POST', data)
+
+export const articleCategories = () => request('/api/articles/categories', 'POST', {})
+
+export const articleRecordRead = (data) => request('/api/articles/record-read', 'POST', data)
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add cfc-frontend/utils/api.js
+git commit -m "feat(frontend): add article API functions"
+```
+
+---
+
+## Task 9: 小程序端 — 新增文章详情页
+
+**Files:**
+- Create: `cfc-frontend/pages/mind/article-detail/article-detail.vue`
+- Modify: `cfc-frontend/pages.json`
+
+- [ ] **Step 1: 创建 article-detail.vue**
+
+```vue
+<template>
+  <view class="article-detail">
+    <!-- 封面图 -->
+    <image v-if="article.coverImage" class="detail-cover" :src="coverFullUrl" mode="aspectFill" />
+
+    <!-- 标题区 -->
+    <view class="detail-header">
+      <text class="detail-title">{{ article.title }}</text>
+      <view class="detail-meta">
+        <text class="detail-author">{{ article.author || '浠艾福' }}</text>
+        <text class="detail-date">{{ article.publishedAt || '' }}</text>
+      </view>
+      <view class="detail-tags">
+        <view v-if="article.categoryName" class="tag-item" :style="{ background: article.categoryColor || '#5B9BD5' }">
+          <text class="tag-text">{{ article.categoryName }}</text>
+        </view>
+        <view v-for="dim in dimensionList" :key="dim" class="tag-item tag-dim">
+          <text class="tag-text">{{ dimLabel(dim) }}</text>
+        </view>
+      </view>
+    </view>
+
+    <!-- 正文 -->
+    <view class="detail-body">
+      <rich-text :nodes="article.content || ''" />
+    </view>
+
+    <!-- 底部 -->
+    <view class="detail-footer">
+      <text class="footer-read-time">📖 {{ article.readTime || 0 }}分钟阅读</text>
+      <text class="footer-view-count">{{ article.viewCount || 0 }}次浏览</text>
+    </view>
+  </view>
+</template>
+
+<script>
+import { articleDetail, articleRecordRead } from '../../../utils/api.js'
+
+export default {
+  data() {
+    return {
+      articleId: null,
+      article: {},
+      enterTime: 0,
+      dimensionList: []
+    }
+  },
+  computed: {
+    coverFullUrl() {
+      if (!this.article.coverImage) return ''
+      if (this.article.coverImage.indexOf('http') === 0) return this.article.coverImage
+      return 'http://localhost:8080' + this.article.coverImage
+    }
+  },
+  onLoad(options) {
+    this.articleId = options.id
+    this.enterTime = Date.now()
+    this.loadDetail()
+  },
+  onUnload() {
+    this.recordRead()
+  },
+  methods: {
+    async loadDetail() {
+      try {
+        var res = await articleDetail({ id: this.articleId })
+        if (res.code === 200 && res.data) {
+          this.article = res.data
+          // 解析关联五维
+          try {
+            this.dimensionList = JSON.parse(res.data.relatedDimensions || '[]')
+          } catch (e) {
+            this.dimensionList = []
+          }
+        }
+      } catch (e) {
+        uni.showToast({ title: '加载失败', icon: 'none' })
+      }
+    },
+    recordRead() {
+      var duration = Math.round((Date.now() - this.enterTime) / 1000)
+      if (duration < 3) return
+      var userId = uni.getStorageSync('userId')
+      var childId = uni.getStorageSync('childId')
+      var data = { articleId: this.articleId, durationSeconds: duration }
+      if (userId) data.userId = userId
+      if (childId) data.childId = childId
+      articleRecordRead(data).catch(function() {})
+    },
+    dimLabel(dim) {
+      var map = { body: '身', mind: '心', wisdom: '智', action: '行', wealth: '富' }
+      return map[dim] || dim
+    }
+  }
+}
+</script>
+
+<style scoped>
+.article-detail {
+  min-height: 100vh;
+  background: #fff;
+}
+.detail-cover {
+  width: 100%;
+  height: 400rpx;
+}
+.detail-header {
+  padding: 30rpx;
+  border-bottom: 1rpx solid #f0f0f0;
+}
+.detail-title {
+  display: block;
+  font-size: 36rpx;
+  font-weight: bold;
+  color: #333;
+  line-height: 1.5;
+  margin-bottom: 16rpx;
+}
+.detail-meta {
+  display: flex;
+  flex-direction: row;
+  align-items: center;
+  margin-bottom: 16rpx;
+}
+.detail-author {
+  font-size: 24rpx;
+  color: #666;
+  margin-right: 20rpx;
+}
+.detail-date {
+  font-size: 24rpx;
+  color: #999;
+}
+.detail-tags {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+  gap: 12rpx;
+}
+.tag-item {
+  display: inline-block;
+  padding: 4rpx 16rpx;
+  border-radius: 16rpx;
+}
+.tag-text {
+  font-size: 20rpx;
+  color: #fff;
+  font-weight: 500;
+}
+.tag-dim {
+  background: rgba(91,155,213,0.2);
+}
+.tag-dim .tag-text {
+  color: #5B9BD5;
+}
+.detail-body {
+  padding: 30rpx;
+  font-size: 28rpx;
+  color: #333;
+  line-height: 1.8;
+}
+.detail-footer {
+  display: flex;
+  flex-direction: row;
+  justify-content: space-between;
+  padding: 20rpx 30rpx;
+  border-top: 1rpx solid #f0f0f0;
+  background: #fafafa;
+}
+.footer-read-time, .footer-view-count {
+  font-size: 22rpx;
+  color: #999;
+}
+</style>
+```
+
+- [ ] **Step 2: 在 pages.json 中注册新页面**
+
+在 `pages/mind/articles` 配置后面追加:
+
+```json
+{
+  "path": "pages/mind/article-detail/article-detail",
+  "style": {
+    "navigationBarTitleText": "文章详情"
+  }
+}
+```
+
+- [ ] **Step 3: Commit**
+
+```bash
+git add cfc-frontend/pages/mind/article-detail/article-detail.vue cfc-frontend/pages.json
+git commit -m "feat(frontend): add article detail page with rich-text and reading record"
+```
+
+---
+
+## Task 10: 小程序端 — 文章列表页改为API数据源
+
+**Files:**
+- Modify: `cfc-frontend/pages/mind/articles.vue`
+
+- [ ] **Step 1: 替换 articles.vue 的 script 部分**
+
+将整个 `<script>` 替换为:
+
+```vue
+<script>
+import { articleList, articleCategories } from '../../utils/api.js'
+import BottomNav from '../../components/bottom-nav.vue'
+
+export default {
+  components: { BottomNav },
+  data() {
+    return {
+      categoryList: [{ label: '全部', value: '' }],
+      currentCategory: '',
+      articles: [],
+      page: 1,
+      size: 10,
+      loading: false,
+      loadingMore: false,
+      noMore: false,
+      isLoggedIn: false
+    }
+  },
+  onLoad() {
+    this.isLoggedIn = !!uni.getStorageSync('token')
+    this.loadCategories()
+    this.loadArticles()
+  },
+  onShow() {
+    this.isLoggedIn = !!uni.getStorageSync('token')
+  },
+  methods: {
+    async loadCategories() {
+      try {
+        var res = await articleCategories()
+        if (res.code === 200 && res.data) {
+          var cats = [{ label: '全部', value: '' }]
+          res.data.forEach(function(cat) {
+            cats.push({ label: cat.name, value: cat.id })
+          })
+          this.categoryList = cats
+        }
+      } catch (e) {
+        // 使用默认分类
+      }
+    },
+    onCategoryChange(value) {
+      this.currentCategory = value
+      this.page = 1
+      this.articles = []
+      this.noMore = false
+      this.loadArticles()
+    },
+    async loadArticles() {
+      if (this.loading) return
+      this.loading = true
+      var data = { page: this.page, size: this.size }
+      if (this.currentCategory) {
+        data.categoryId = this.currentCategory
+      }
+      try {
+        var res = await articleList(data)
+        if (res.code === 200 && res.data) {
+          var records = res.data.records || []
+          if (this.page === 1) {
+            this.articles = records
+          } else {
+            this.articles = this.articles.concat(records)
+          }
+          this.noMore = this.articles.length >= res.data.total
+        }
+      } catch (e) {
+        uni.showToast({ title: '加载失败', icon: 'none' })
+      }
+      this.loading = false
+      this.loadingMore = false
+    },
+    onLoadMore() {
+      if (this.noMore || this.loadingMore) return
+      this.page++
+      this.loadingMore = true
+      this.loadArticles()
+    },
+    goDetail(id) {
+      if (this.isLoggedIn) {
+        uni.navigateTo({ url: '/pages/mind/article-detail/article-detail?id=' + id })
+      } else {
+        uni.navigateTo({ url: '/pages/login/login?redirect=' + encodeURIComponent('/pages/mind/articles') })
+      }
+    }
+  }
+}
+</script>
+```
+
+模板中 `article.publishDate` 改为 `article.publishedAt`,`article.readCount` 改为 `article.viewCount`,`item.categoryName` 保持不变(后端list接口会返回)。
+
+需要修改模板中的3处字段名:
+1. `item.publishDate` → `item.publishedAt`
+2. `item.readCount` → `item.viewCount`
+3. 点击跳转路径:`/pages/mind/article-detail/article-detail?id=` + id
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add cfc-frontend/pages/mind/articles.vue
+git commit -m "feat(frontend): replace mock data with API in articles list page"
+```
+
+---
+
+## Task 11: 小程序端 — 心智成长首页改为API数据源
+
+**Files:**
+- Modify: `cfc-frontend/pages/mind/index.vue`
+
+- [ ] **Step 1: 修改 mind/index.vue 的推荐阅读区块**
+
+将 `data()` 中的 `articles` 硬编码数组改为空数组:
+
+```javascript
+articles: [],
+```
+
+在 `onShow` 中调用API加载推荐文章:
+
+```javascript
+import { articleFeatured } from '../../utils/api.js'
+
+// 在 onShow 中追加:
+this.loadFeaturedArticles()
+```
+
+在 `methods` 中新增方法:
+
+```javascript
+async loadFeaturedArticles() {
+  try {
+    var res = await articleFeatured({ size: 5 })
+    if (res.code === 200 && res.data) {
+      this.articles = res.data.map(function(a) {
+        return {
+          id: a.id,
+          category: a.categoryName || '心理健康',
+          categoryColor: a.categoryColor || 'linear-gradient(135deg, #5B9BD5, #8FC5E8)',
+          title: a.title,
+          summary: a.summary,
+          views: a.viewCount || 0,
+          likes: 0,
+          date: a.publishedAt || ''
+        }
+      })
+    }
+  } catch (e) {
+    // 保留空数组
+  }
+},
+```
+
+在推荐阅读区块的 article-card 上添加点击事件:
+
+```html
+<view class="article-card" v-for="article in articles" :key="article.id" @click="goArticleDetail(article.id)">
+```
+
+在 `methods` 中追加:
+
+```javascript
+goArticleDetail(id) {
+  uni.navigateTo({ url: '/pages/mind/article-detail/article-detail?id=' + id })
+},
+```
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add cfc-frontend/pages/mind/index.vue
+git commit -m "feat(frontend): replace hardcoded articles with API in mind/index"
+```
+
+---
+
+## Task 12: 小程序端 — 发现页精选文章改为API数据源
+
+**Files:**
+- Modify: `cfc-frontend/pages/discover/index.vue`
+
+- [ ] **Step 1: 修改 discover/index.vue**
+
+在 `import` 中追加:
+
+```javascript
+import { articleFeatured } from '@/utils/api.js'
+```
+
+在 `data()` 中追加:
+
+```javascript
+featuredArticles: [],
+```
+
+在 `onShow` 中追加调用:
+
+```javascript
+this.loadFeaturedArticles()
+```
+
+在 `methods` 中新增:
+
+```javascript
+async loadFeaturedArticles() {
+  try {
+    var res = await articleFeatured({ size: 3 })
+    if (res.code === 200 && res.data) {
+      this.featuredArticles = res.data
+    }
+  } catch (e) {
+    // 保留空数组
+  }
+},
+goArticleDetail(id) {
+  uni.navigateTo({ url: '/pages/mind/article-detail/article-detail?id=' + id })
+},
+```
+
+替换模板中3个硬编码 article-card 为动态渲染:
+
+```html
+<view class="article-list">
+  <view v-if="featuredArticles.length === 0" class="article-card" @click="handleLogin">
+    <view class="article-cover" style="background: linear-gradient(135deg, #D6EAF8, #8FC5E8);">
+      <text class="article-cover-icon">📖</text>
+    </view>
+    <view class="article-info">
+      <text class="article-title">登录查看更多精彩文章</text>
+      <text class="article-desc">海量育儿、心理、健康内容等您探索</text>
+    </view>
+  </view>
+  <view v-for="item in featuredArticles" :key="item.id" class="article-card" @click="goArticleDetail(item.id)">
+    <view class="article-cover" :style="{ background: 'linear-gradient(135deg, #D6EAF8, #8FC5E8)' }">
+      <text class="article-cover-icon">📖</text>
+    </view>
+    <view class="article-info">
+      <text class="article-title">{{ item.title }}</text>
+      <text class="article-desc">{{ item.summary }}</text>
+      <text class="article-meta">{{ item.publishedAt || '' }} · {{ item.readTime || 0 }}分钟阅读</text>
+    </view>
+  </view>
+</view>
+```
+
+注意:如果 `item.coverImage` 存在,也可以渲染为 `<image>` 而非渐变色背景。但需处理封面图URL拼接。此处先用渐变色兜底,后续迭代可优化。
+
+- [ ] **Step 2: Commit**
+
+```bash
+git add cfc-frontend/pages/discover/index.vue
+git commit -m "feat(frontend): replace hardcoded articles with API in discover page"
+```
+
+---
+
+## Task 13: 全端编译验证
+
+**Files:** 无新增/修改,仅验证
+
+- [ ] **Step 1: 后端编译验证**
+
+Run: `cd cfc-backend && mvn clean compile`
+
+Expected: BUILD SUCCESS
+
+- [ ] **Step 2: 后端启动验证**
+
+Run: `cd cfc-backend && mvn spring-boot:run`
+
+Expected: 控制台显示 `Started` 字样,数据库表自动创建,无端口冲突
+
+验证要点:
+- 控制台日志出现 `默认文章分类初始化完成`
+- 请求 `POST /api/articles/categories` 返回5个默认分类
+- 请求 `POST /api/articles/featured` 返回空数组(尚无文章)
+
+- [ ] **Step 3: Web管理端编译验证**
+
+Run: `cd cfc-web && npm run serve`
+
+Expected: 编译成功,localhost:8082 可访问
+
+验证要点:
+- 侧边栏"系统管理"下出现"文章管理"和"文章分类"
+- 点击可正常进入列表页
+- 分类管理页可新增/编辑分类
+
+- [ ] **Step 4: 小程序前端语法验证**
+
+Run: `node --check` 对所有修改的Vue文件script块和JS模块进行语法检查
+
+- [ ] **Step 5: 最终Commit**
+
+```bash
+git add -A
+git commit -m "feat: complete article publishing system (backend + web admin + mini-program)"
+```
+
+---
+
+## 自检清单
+
+### 1. 设计文档覆盖度
+
+| 设计文档要求 | 对应Task | 状态 |
+|:------------|:---------|:-----|
+| article_categories 表 | Task 1 (DatabaseInitializer) | ✅ |
+| articles 表 | Task 1 (DatabaseInitializer) | ✅ |
+| article_reading_records 关联 | Task 3 (ArticleController.recordRead) | ✅ |
+| /api/articles/list 公开端点 | Task 3 | ✅ |
+| /api/articles/detail 公开端点 | Task 3 | ✅ |
+| /api/articles/featured 公开端点 | Task 3 | ✅ |
+| /api/articles/categories 公开端点 | Task 3 | ✅ |
+| /api/articles/record-read 端点 | Task 3 | ✅ |
+| /api/admin/articles/* 管理端6个端点 | Task 3 | ✅ |
+| /api/admin/categories/* 管理端4个端点 | Task 3 | ✅ |
+| /api/admin/upload/image 端点 | Task 3 | ✅ |
+| Web ArticleManage.vue 列表页 | Task 5 | ✅ |
+| Web ArticleEdit.vue 编辑页+富文本 | Task 6 | ✅ |
+| Web ArticleCategory.vue 分类页 | Task 4 | ✅ |
+| 小程序 article-detail.vue | Task 9 | ✅ |
+| 小程序 articles.vue 改API | Task 10 | ✅ |
+| 小程序 mind/index.vue 改API | Task 11 | ✅ |
+| 小程序 discover/index.vue 改API | Task 12 | ✅ |
+| JWT排除公开端点 | Task 1 (WebConfig) | ✅ |
+
+### 2. Placeholder扫描
+
+- 无TBD/TODO/类似占位符
+- 所有代码步骤包含完整实现
+- 所有文件路径精确
+
+### 3. 类型一致性
+
+- Article实体字段名与Controller/Service/前端API调用保持一致(camelCase)
+- categoryId 类型为 Long(后端)/ Number(前端)
+- isFeatured 类型为 Integer(后端,0/1)/ Boolean转换(前端)
+- relatedDimensions 存储为JSON字符串,前端解析为Array
+- tags 存储为JSON字符串
+
+### 4. 待定事项处理
+
+| 待定 | 处理 |
+|:-----|:-----|
+| 封面图存储方式 | 使用服务器本地 `/uploads/articles/`(Task 3 UploadController) |
+| 富文本编辑器选型 | 选择 `vue-quill-editor`(Task 6) |
+| 阅读记录与能量系统联动 | 本次只记录不做联动(Task 9 recordRead仅写入article_reading_records) |
+| 文章评论/点赞 | 本次不做 |
+| 文章SEO/分享卡片 | 本次不做 |

+ 891 - 0
docs/superpowers/plans/2026-06-09-energy-system-implementation-plan.md

@@ -0,0 +1,891 @@
+# 五维能量系统实施计划(更新版)
+
+**日期:** 2026-06-09
+**状态:** 待实施
+**分支:** cfclub
+**工作量预估:** 3~4周
+
+---
+
+## 现状分析
+
+### 现有代码(旧版·评分系统)
+| 文件 | 说明 |
+|------|------|
+| `EnergyController.java` | `/api/energy/sandbox` — 返回家庭能量沙盘(百分比评分) |
+| `EnergyService.java` | 基于任务完成率 + 游戏得分 + 测评结果 + 阅读时长 计算 0-100% 评分 |
+| `EnergyDimensionConfig.java` | 单表 `energy_dimension_config`(dimension/data_source/category/weight)⚠️ 将被废弃 |
+| `EnergyDimensionConfigMapper.java` | 旧表 Mapper ⚠️ 将被废弃 |
+| `EnergySandboxDTO.java` | 家庭聚合 + 成员列表 DTO |
+| `MemberEnergyDTO.java` | 成员五维评分DTO |
+| `api.js` | `getFamilyEnergySandbox()` → `/api/energy/sandbox` |
+
+### 现有限制
+- 旧版 EnergyService 是**计算型**(查询历史数据算百分比),不是**账本型**(发放/消耗)
+- 旧表 `energy_dimension_config` 是单表维度权重配置,与新 4 表设计不兼容
+- DatabaseInitializer 中未包含任何能量表 DDL
+- 前端无能量详情页
+
+### 共存策略
+旧版评分系统 = **健康指数(healthIndex)**的前身,新账本系统 = **能量值(energy)**。
+两个概念在 API 响应中并存:`energy`(账本余额)+ `healthIndex`(百分比,暂时返回 0)。
+
+**实施原则:**
+1. 不修改旧版 `EnergyService` 的评分计算方法
+2. 在旧版 `EnergyService` 中新增账本方法(award/deduct/overview/logs)
+3. 旧版 Controller 保留 `/sandbox`,新增 `/overview` `/logs` `/configure`
+4. 旧表 `energy_dimension_config` 暂不删除,新 4 表独立创建
+
+---
+
+## Task 1: 数据库迁移 — 4张新表 + 种子数据
+
+**文件:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`
+
+在 `initSchema()` 末尾(`log.info("数据库迁移完成")` 之前)添加。
+
+> 以下 SQL 可直接复制。注意:旧表 `energy_dimension_config` 不在此处创建(已在数据库中存在)。
+
+- [ ] **1.1 DDL: 创建 4 张能量表**
+
+```java
+// ==================== 五维能量系统(账本模式) ====================
+
+// 维度定义表
+jdbcTemplate.execute(
+    "CREATE TABLE IF NOT EXISTS energy_dimension (" +
+    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+    "code VARCHAR(16) NOT NULL UNIQUE COMMENT 'body/mind/wisdom/action/wealth', " +
+    "name VARCHAR(20) NOT NULL COMMENT '身/心/智/行/富', " +
+    "icon VARCHAR(16) COMMENT '图标emoji', " +
+    "element VARCHAR(10) COMMENT '五行: 土/火/金/木/水', " +
+    "sort_order INT DEFAULT 0 COMMENT '显示顺序', " +
+    "status TINYINT DEFAULT 1 COMMENT '1启用/0禁用', " +
+    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP" +
+    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='能量维度定义表'"
+);
+
+// 服务-维度比例配置表
+jdbcTemplate.execute(
+    "CREATE TABLE IF NOT EXISTS energy_source_config (" +
+    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+    "source_type VARCHAR(32) NOT NULL COMMENT '来源类型: task/activity/course/product/consultation', " +
+    "source_id BIGINT NOT NULL COMMENT '来源业务表ID', " +
+    "source_name VARCHAR(200) COMMENT '冗余名称', " +
+    "dimension_id BIGINT NOT NULL COMMENT '维度ID', " +
+    "ratio DECIMAL(5,4) NOT NULL COMMENT '占比如0.6000=60%', " +
+    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+    "INDEX idx_source (source_type, source_id)" +
+    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='服务-维度比例配置表'"
+);
+
+// 能量流水表
+jdbcTemplate.execute(
+    "CREATE TABLE IF NOT EXISTS energy_log (" +
+    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+    "child_id BIGINT NOT NULL COMMENT '孩子ID', " +
+    "dimension_id BIGINT NOT NULL COMMENT '维度ID', " +
+    "amount INT NOT NULL COMMENT '变动量(正=获得,负=消耗)', " +
+    "balance_after INT NOT NULL COMMENT '变动后余额', " +
+    "source_type VARCHAR(32) COMMENT '来源类型: task/product/medical_report/daily_record', " +
+    "source_id BIGINT COMMENT '来源ID', " +
+    "ref_id BIGINT COMMENT '关联旧流水ID(覆盖时标记)', " +
+    "expires_at DATETIME COMMENT '过期时间(空=永不过期)', " +
+    "description VARCHAR(500) COMMENT '描述', " +
+    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+    "INDEX idx_child_dim (child_id, dimension_id), " +
+    "INDEX idx_created (created_at), " +
+    "INDEX idx_source (source_type, source_id)" +
+    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='能量流水表'"
+);
+
+// 维度余额表
+jdbcTemplate.execute(
+    "CREATE TABLE IF NOT EXISTS energy_balance (" +
+    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+    "child_id BIGINT NOT NULL COMMENT '孩子ID', " +
+    "dimension_id BIGINT NOT NULL COMMENT '维度ID', " +
+    "balance INT DEFAULT 0 COMMENT '当前能量值(可为负)', " +
+    "total_earned INT DEFAULT 0 COMMENT '累计获得', " +
+    "total_spent INT DEFAULT 0 COMMENT '累计消耗', " +
+    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+    "UNIQUE KEY uk_child_dim (child_id, dimension_id)" +
+    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='维度余额表'"
+);
+```
+
+- [ ] **1.2 种子数据: 维度定义**
+
+```java
+// 维度种子数据(按相生链: 智→富→行→心→身)
+Integer dimCount = jdbcTemplate.queryForObject(
+    "SELECT COUNT(*) FROM energy_dimension", Integer.class);
+if (dimCount == null || dimCount == 0) {
+    jdbcTemplate.execute("INSERT INTO energy_dimension (code, name, icon, element, sort_order, status) VALUES " +
+        "('mind', '心', '🔥', '火', 4, 1), " +
+        "('body', '身', '🌏', '土', 5, 1), " +
+        "('wisdom', '智', '⚔️', '金', 1, 1), " +
+        "('action', '行', '🌿', '木', 3, 1), " +
+        "('wealth', '富', '💧', '水', 2, 1)");
+}
+```
+
+- [ ] **1.3 种子数据: 已有商品映射**
+
+```java
+// 为已有商品插入默认比例配置(与Product.domain一致)
+Integer productConfigCount = jdbcTemplate.queryForObject(
+    "SELECT COUNT(*) FROM energy_source_config WHERE source_type = 'product'", Integer.class);
+if (productConfigCount == null || productConfigCount == 0) {
+    String[][] domainMapping = {
+        {"action", "action"}, {"mind", "mind"}, {"body", "body"},
+        {"wisdom", "wisdom"}, {"wealth", "wealth"}
+    };
+    for (String[] mapping : domainMapping) {
+        jdbcTemplate.execute(
+            "INSERT INTO energy_source_config (source_type, source_id, source_name, dimension_id, ratio) " +
+            "SELECT 'product', p.id, p.name, d.id, 1.0000 " +
+            "FROM products p " +
+            "JOIN energy_dimension d ON d.code = '" + mapping[1] + "' " +
+            "WHERE p.domain = '" + mapping[0] + "' " +
+            "AND NOT EXISTS (" +
+            "  SELECT 1 FROM energy_source_config esc " +
+            "  WHERE esc.source_type = 'product' AND esc.source_id = p.id" +
+            ")"
+        );
+    }
+}
+```
+
+- [ ] **1.4 验证:** `cd cfc-backend && mvn clean compile -q`
+
+---
+
+## Task 2: 4个新 Entity
+
+**新建:**
+- `entity/EnergyDimension.java`
+- `entity/EnergySourceConfig.java`
+- `entity/EnergyLog.java`
+- `entity/EnergyBalance.java`
+
+遵循现有模式:`@Data @TableName @TableId(type = IdType.AUTO) implements Serializable`
+
+> **注意:** 新增 `EnergyLog.expiresAt` 和 `EnergyLog.refId` 字段(与总设计 spec 相比新增,用于支持过期和覆盖机制)。
+
+- [ ] **2.1 创建 EnergyDimension.java**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("energy_dimension")
+public class EnergyDimension implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String code;        // body/mind/wisdom/action/wealth
+    private String name;        // 身/心/智/行/富
+    private String icon;        // emoji
+    private String element;     // 土/火/金/木/水
+    private Integer sortOrder;
+    private Integer status;     // 1启用/0禁用
+    private Date createdAt;
+}
+```
+
+- [ ] **2.2 创建 EnergySourceConfig.java**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.util.Date;
+
+@Data
+@TableName("energy_source_config")
+public class EnergySourceConfig implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private String sourceType;      // task/activity/course/product/consultation
+    private Long sourceId;
+    private String sourceName;
+    private Long dimensionId;
+    private BigDecimal ratio;       // 0.6000 = 60%
+    private Date createdAt;
+}
+```
+
+- [ ] **2.3 创建 EnergyLog.java**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("energy_log")
+public class EnergyLog implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long childId;
+    private Long dimensionId;
+    private Integer amount;
+    private Integer balanceAfter;
+    private String sourceType;      // task/product/medical_report/daily_record
+    private Long sourceId;
+    private Long refId;             // 关联旧流水(覆盖时标记)
+    private Date expiresAt;         // 过期时间(空=永不过期)
+    private String description;
+    private Date createdAt;
+}
+```
+
+- [ ] **2.4 创建 EnergyBalance.java**
+
+```java
+package com.etotem.cfc.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+@Data
+@TableName("energy_balance")
+public class EnergyBalance implements Serializable {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+    private Long childId;
+    private Long dimensionId;
+    private Integer balance;
+    private Integer totalEarned;
+    private Integer totalSpent;
+    private Date updatedAt;
+}
+```
+
+- [ ] **2.5 验证:** `cd cfc-backend && mvn clean compile -q`
+
+---
+
+## Task 3: 4个 Mapper
+
+**新建:**
+- `mapper/EnergyDimensionMapper.java`
+- `mapper/EnergySourceConfigMapper.java`
+- `mapper/EnergyLogMapper.java`
+- `mapper/EnergyBalanceMapper.java`
+
+遵循现有模式:`@Mapper interface XxxMapper extends BaseMapper<Xxx>`,无自定义方法。
+
+- [ ] **3.1 创建 4 个 Mapper**(略,标准模板)
+- [ ] **3.2 验证:** `cd cfc-backend && mvn clean compile -q`
+
+---
+
+## Task 4: EnergyService 扩展 — 新增账本方法
+
+**文件:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java`
+
+在现有`EnergyService`类中**新增**以下账本方法,保留旧方法不动。
+
+- [ ] **4.1 新增注入 + 维度缓存**
+
+```java
+// ==== 新增: 账本注入 ====
+@Resource
+private EnergyDimensionMapper energyDimensionMapper;
+@Resource
+private EnergySourceConfigMapper energySourceConfigMapper;
+@Resource
+private EnergyLogMapper energyLogMapper;
+@Resource
+private EnergyBalanceMapper energyBalanceMapper;
+
+// 维度缓存(懒加载,维度数据极少变更)
+private List<EnergyDimension> dimCache;
+private Map<String, EnergyDimension> dimCodeMap;
+```
+
+- [ ] **4.2 新增: awardEnergy() — 发放能量,自动查比例配置分配各维度**
+
+```java
+/**
+ * 发放能量 — 按 energy_source_config 比例分配至各维度
+ * @param childId     孩子ID
+ * @param sourceType  来源类型 (task/product/medical_report/etc)
+ * @param sourceId    来源ID
+ * @param totalAmount 总能量值
+ * @param description 描述
+ * @param daysToExpire 过期天数(null=不过期)
+ * @return Map<dimensionCode, amount>
+ */
+@Transactional
+public Map<String, Integer> awardEnergy(Long childId, String sourceType, Long sourceId,
+                                         Integer totalAmount, String description,
+                                         Integer daysToExpire) {
+    if (totalAmount == null || totalAmount <= 0) return new HashMap<>();
+
+    // 1. 查比例配置 → Fallback链
+    List<EnergySourceConfig> configs = energySourceConfigMapper.selectList(
+        new LambdaQueryWrapper<EnergySourceConfig>()
+            .eq(EnergySourceConfig::getSourceType, sourceType)
+            .eq(EnergySourceConfig::getSourceId, sourceId)
+    );
+    Map<Long, BigDecimal> dimRatios = resolveDimensionRatios(configs, sourceType, sourceId);
+
+    if (dimRatios.isEmpty()) return new HashMap<>();
+
+    // 2. 日上限检查
+    if (isDailyLimitExceeded(childId, dimRatios.keySet(), totalAmount)) {
+        log.warn("维度日上限已达,跳过发放");
+        return new HashMap<>();
+    }
+
+    // 3. 按比例分配(整数,余数加到最大比例维度)
+    Map<Long, Integer> allocations = calculateAllocations(totalAmount, dimRatios);
+
+    // 4. 更新余额 + 写流水
+    Map<String, Integer> result = new LinkedHashMap<>();
+    Date now = new Date();
+    Date expiresAt = daysToExpire != null
+        ? new Date(now.getTime() + (long) daysToExpire * 86400000L) : null;
+
+    for (Map.Entry<Long, Integer> entry : allocations.entrySet()) {
+        if (entry.getValue() <= 0) continue;
+
+        EnergyBalance balance = getOrCreateBalance(childId, entry.getKey());
+        balance.setBalance(balance.getBalance() + entry.getValue());
+        balance.setTotalEarned(balance.getTotalEarned() + entry.getValue());
+        balance.setUpdatedAt(now);
+        energyBalanceMapper.updateById(balance);
+
+        EnergyLog log = new EnergyLog();
+        log.setChildId(childId);
+        log.setDimensionId(entry.getKey());
+        log.setAmount(entry.getValue());
+        log.setBalanceAfter(balance.getBalance());
+        log.setSourceType(sourceType);
+        log.setSourceId(sourceId);
+        log.setExpiresAt(expiresAt);
+        log.setDescription(description);
+        log.setCreatedAt(now);
+        energyLogMapper.insert(log);
+
+        EnergyDimension dim = getDimById(entry.getKey());
+        if (dim != null) result.put(dim.getCode(), entry.getValue());
+    }
+    return result;
+}
+```
+
+- [ ] **4.3 新增: deductEnergy() — 扣除能量(余额不足返回 -1)**
+
+```java
+/**
+ * 扣除能量
+ * @return 扣除后余额,-1 = 余额不足
+ */
+@Transactional
+public int deductEnergy(Long childId, Long dimensionId, Integer amount, String reason) {
+    if (amount == null || amount <= 0)
+        throw new IllegalArgumentException("扣除数量必须为正数");
+
+    EnergyBalance balance = getOrCreateBalance(childId, dimensionId);
+    if (balance.getBalance() < amount) return -1;
+
+    balance.setBalance(balance.getBalance() - amount);
+    balance.setTotalSpent(balance.getTotalSpent() + amount);
+    balance.setUpdatedAt(new Date());
+    energyBalanceMapper.updateById(balance);
+
+    EnergyLog log = new EnergyLog();
+    log.setChildId(childId);
+    log.setDimensionId(dimensionId);
+    log.setAmount(-amount);
+    log.setBalanceAfter(balance.getBalance());
+    log.setDescription(reason);
+    log.setCreatedAt(new Date());
+    energyLogMapper.insert(log);
+
+    return balance.getBalance();
+}
+```
+
+- [ ] **4.4 新增: getOverview() — 查询五维概览**
+
+```java
+/**
+ * 查询五维能量概览
+ */
+public Map<String, Object> getOverview(Long childId) {
+    List<EnergyDimension> allDims = getAllDimensions();
+    List<Map<String, Object>> dimList = new ArrayList<>();
+    int totalEnergy = 0;
+
+    for (EnergyDimension dim : allDims) {
+        if (dim.getStatus() != 1) continue;
+        EnergyBalance balance = energyBalanceMapper.selectOne(
+            new LambdaQueryWrapper<EnergyBalance>()
+                .eq(EnergyBalance::getChildId, childId)
+                .eq(EnergyBalance::getDimensionId, dim.getId())
+        );
+        int energy = balance != null ? balance.getBalance() : 0;
+        totalEnergy += energy;
+
+        Map<String, Object> item = new LinkedHashMap<>();
+        item.put("code", dim.getCode());
+        item.put("name", dim.getName());
+        item.put("icon", dim.getIcon());
+        item.put("element", dim.getElement());
+        item.put("energy", energy);
+        item.put("healthIndex", 0); // 算法待定
+        dimList.add(item);
+    }
+
+    Map<String, Object> result = new LinkedHashMap<>();
+    result.put("dimensions", dimList);
+    result.put("totalEnergy", totalEnergy);
+    result.put("totalHealthIndex", 0);
+    return result;
+}
+```
+
+- [ ] **4.5 新增: getLogs() — 流水查询(按维度筛选,分页)**
+
+```java
+public Page<EnergyLog> getLogs(Long childId, String dimensionCode, Integer page, Integer size) {
+    Page<EnergyLog> pageParam = new Page<>(page, size);
+    LambdaQueryWrapper<EnergyLog> wrapper = new LambdaQueryWrapper<EnergyLog>()
+        .eq(EnergyLog::getChildId, childId);
+
+    if (dimensionCode != null && !dimensionCode.isEmpty()) {
+        EnergyDimension dim = getDimByCode(dimensionCode);
+        if (dim != null) wrapper.eq(EnergyLog::getDimensionId, dim.getId());
+    }
+    wrapper.orderByDesc(EnergyLog::getCreatedAt);
+    return energyLogMapper.selectPage(pageParam, wrapper);
+}
+```
+
+- [ ] **4.6 新增: configureSourceRatios() — 配置来源比例**
+
+```java
+@Transactional
+public void configureSourceRatios(String sourceType, Long sourceId, String sourceName,
+                                   List<Map<String, Object>> ratios) {
+    energySourceConfigMapper.delete(
+        new LambdaQueryWrapper<EnergySourceConfig>()
+            .eq(EnergySourceConfig::getSourceType, sourceType)
+            .eq(EnergySourceConfig::getSourceId, sourceId)
+    );
+    for (Map<String, Object> r : ratios) {
+        String dimCode = (String) r.get("dimensionCode");
+        BigDecimal ratioVal = new BigDecimal(r.get("ratio").toString());
+        EnergyDimension dim = getDimByCode(dimCode);
+        if (dim == null) continue;
+
+        EnergySourceConfig config = new EnergySourceConfig();
+        config.setSourceType(sourceType);
+        config.setSourceId(sourceId);
+        config.setSourceName(sourceName);
+        config.setDimensionId(dim.getId());
+        config.setRatio(ratioVal);
+        config.setCreatedAt(new Date());
+        energySourceConfigMapper.insert(config);
+    }
+}
+```
+
+- [ ] **4.7 新增: 内部辅助方法**
+
+```java
+// ===== 辅助方法 =====
+
+private Map<Long, BigDecimal> resolveDimensionRatios(
+    List<EnergySourceConfig> configs, String sourceType, Long sourceId) {
+    
+    if (!configs.isEmpty()) {
+        Map<Long, BigDecimal> result = new LinkedHashMap<>();
+        for (EnergySourceConfig c : configs) result.put(c.getDimensionId(), c.getRatio());
+        return result;
+    }
+
+    // Fallback 1: Product.domain
+    if ("product".equals(sourceType)) {
+        // 查 Product.domain → 单维度 100%
+        try { ... } catch (Exception ignored) {}
+    }
+
+    // Fallback 2: 默认 → 行(action) 100%
+    EnergyDimension action = getDimByCode("action");
+    if (action != null) return Collections.singletonMap(action.getId(), BigDecimal.ONE);
+    return Collections.emptyMap();
+}
+
+private Map<Long, Integer> calculateAllocations(Integer total, Map<Long, BigDecimal> ratios) { ... }
+private EnergyBalance getOrCreateBalance(Long childId, Long dimId) { ... }
+private boolean isDailyLimitExceeded(Long childId, Set<Long> dimIds, Integer amount) { ... }
+private List<EnergyDimension> getAllDimensions() { ... }
+private EnergyDimension getDimByCode(String code) { ... }
+private EnergyDimension getDimById(Long id) { ... }
+```
+
+> **`isDailyLimitExceeded` 逻辑:**
+> `energy_log` 查 childId + dimensionId + 今日 created_at 的 amount 总和 + 本次发放量 > 该维度日上限 → true
+> 同时校验五维总和日上限
+
+**默认日上限(从 remaining-dimensions 设计):**
+
+| 维度 | 日上限 |
+|------|-------|
+| 身 | 50 |
+| 心 | 40 |
+| 智 | 80 |
+| 行 | 60 |
+| 富 | 30 |
+| ⚠️ 全局 | 200 |
+
+- [ ] **4.8 验证:** `cd cfc-backend && mvn clean compile -q`
+
+---
+
+## Task 5: EnergyController 新增端点
+
+**文件:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/controller/EnergyController.java`
+
+保留现有 `/api/energy/sandbox` 端点,新增 3 个端点。
+
+> 注意:现有 controller 在 `controller/` 下(不在 `/energy` 子包),保持该目录。
+
+- [ ] **5.1 新增 3 个端点**
+
+```java
+@PostMapping("/overview")
+public Result<Map<String, Object>> getOverview(@RequestBody Map<String, Object> params) { ... }
+
+@PostMapping("/logs")
+public Result<Page<EnergyLog>> getLogs(@RequestBody Map<String, Object> params) { ... }
+
+@PostMapping("/configure")
+public Result<String> configure(@RequestBody Map<String, Object> params,
+                                @RequestAttribute("userId") Long userId) { ... }
+```
+
+> 路径映射确认(避免冲突):当前 `@RequestMapping("/api/energy")` 下有 `/sandbox`、`/overview`、`/logs`、`/configure`,无重复。
+
+- [ ] **5.2 验证:** `cd cfc-backend && mvn clean compile -q`
+
+---
+
+## Task 6: TaskService 集成 — 完成任务时发放能量
+
+**文件:**
+- Modify: `cfc-backend/src/main/java/com/etotem/cfc/service/TaskService.java`
+
+在 `completeTask()` 中,积分发放之后、`Map<String, Object> result` 之前,添加能量发放调用。
+
+- [ ] **6.1 注入 EnergyService**
+
+```java
+@Resource
+private EnergyService energyService;
+```
+
+- [ ] **6.2 在 completeTask() 末尾添加**
+
+```java
+// 五维能量发放(与积分并行,独立系统,失败不影响积分)
+try {
+    int energyAmount = Math.abs(pointsEarned);
+    if (energyAmount > 0) {
+        // 查 task.category 的过期天数配置,目前任务不过期
+        energyService.awardEnergy(childId, "task", taskId, energyAmount,
+            "完成任务: " + task.getTitle(), null);
+    }
+} catch (Exception e) {
+    log.warn("能量发放失败(不影响积分): childId={}, taskId={}, error={}",
+        childId, taskId, e.getMessage());
+}
+```
+
+- [ ] **6.3 验证:** `cd cfc-backend && mvn clean compile -q`
+
+---
+
+## Task 7: 前端 API 封装
+
+**文件:**
+- Modify: `cfc-frontend/utils/api.js`
+
+在末尾新增 3 个 API 调用。
+
+- [ ] **7.1 在 api.js 末尾添加**
+
+```js
+// ==== 五维能量系统 ====
+export const getEnergyOverview = (childId) => {
+  return request('/api/energy/overview', 'POST', { childId })
+}
+
+export const getEnergyLogs = (childId, dimensionCode, page = 1, size = 10) => {
+  return request('/api/energy/logs', 'POST', { childId, dimensionCode, page, size })
+}
+```
+
+> 已有 `getFamilyEnergySandbox`(旧版 `/api/energy/sandbox`),保留不动。
+
+- [ ] **7.2 Commit**
+
+---
+
+## Task 8: 前端 wuxing-sandbox 组件重写
+
+**文件:**
+- Modify: `cfc-frontend/components/wuxing-sandbox.vue`
+
+组件改造为接收真实数据 props,实现三层视觉结构(外层维度角、中层健康指数扇区、内层中心圆)。
+
+- [ ] **8.1 组件 props 更新**
+
+当前组件已有 props: `mode`, `badgeText`。
+新增 props: `dimensions`, `totalEnergy`, `totalHealthIndex`。
+
+```js
+props: {
+  mode: { type: String, default: 'preview' },
+  badgeText: { type: String, default: '综合成长力 0%' },
+  dimensions: { type: Array, default: () => [] },
+  totalEnergy: { type: Number, default: 0 },
+  totalHealthIndex: { type: Number, default: 0 }
+},
+computed: {
+  sortedDimensions: function () {
+    // 按顺序: 心/行/富/智/身 → mind/action/wealth/wisdom/body
+    var order = ['mind', 'action', 'wealth', 'wisdom', 'body'];
+    var dimMap = {}
+    for (var i = 0; i < this.dimensions.length; i++) {
+      dimMap[this.dimensions[i].code] = this.dimensions[i]
+    }
+    var result = []
+    for (var j = 0; j < order.length; j++) {
+      result.push(dimMap[order[j]] || { code: order[j], name: '', icon: '', element: '', energy: 0, healthIndex: 0 })
+    }
+    return result
+  }
+}
+```
+
+> ⚠️ **Vue 2 限制(不使用 Composition API):** 禁止可选链 `?.`,用 `&&` 短路或用 `|| {}` 兜底
+
+- [ ] **8.2 template 改造**
+
+```html
+<!-- 心·火 (顶) → sortedDimensions[0] -->
+<view class="star-point star-point-top" @click="onPointClick(0)">
+  <text class="point-icon">{{ sortedDimensions[0].icon }}</text>
+  <text class="point-label">{{ sortedDimensions[0].name }}·{{ sortedDimensions[0].element }}</text>
+  <text class="point-energy">{{ sortedDimensions[0].energy }}</text>
+</view>
+<!-- 行·木 (左), 富·水 (右), 智·金 (左下), 身·土 (右下) → index 1-4 -->
+```
+
+- [ ] **8.3 验证:** `cd cfc-frontend && npm run dev:mp-weixin`(检查编译)
+
+---
+
+## Task 9: 首页集成 — parent-index + child-index
+
+**文件:**
+- Modify: `cfc-frontend/pages/index/parent-index.vue`
+- Modify: `cfc-frontend/pages/index/child-index.vue`
+
+在 `onShow` 中调用 `getEnergyOverview`,将数据传入 wuxing-sandbox。
+
+- [ ] **9.1 parent-index.vue 修改**
+
+```js
+import { getEnergyOverview } from '../../utils/api.js'
+
+// data 新增:
+energyDimensions: [],
+totalEnergy: 0,
+totalHealthIndex: 0,
+
+// methods 新增:
+loadEnergyOverview: function () {
+  var that = this
+  var childId = that.currentChildId
+  if (!childId) return
+  getEnergyOverview(childId).then(function (res) {
+    if (res.data) {
+      that.energyDimensions = res.data.dimensions || []
+      that.totalEnergy = res.data.totalEnergy || 0
+      that.totalHealthIndex = res.data.totalHealthIndex || 0
+    }
+  }).catch(function () { /* 静默失败 */ })
+},
+
+// onShow 末尾调用:
+this.loadEnergyOverview()
+```
+
+template 中的 wuxing-sandbox:
+```html
+<wuxing-sandbox mode="parent"
+  :badgeText="'综合成长力 ' + totalHealthIndex + '%'"
+  :dimensions="energyDimensions"
+  :totalEnergy="totalEnergy"
+  :totalHealthIndex="totalHealthIndex"
+  @point-click="goToDomain" />
+```
+
+- [ ] **9.2 child-index.vue 修改**
+
+(同上模式,使用 `that.childId` 代替 `currentChildId`)
+
+---
+
+## Task 10: 前端能量详情页 + pages.json
+
+**文件:**
+- Create: `cfc-frontend/pages/energy/detail.vue`
+- Modify: `cfc-frontend/pages.json`
+
+- [ ] **10.1 创建 detail.vue**(维度详情页:头部显示维度信息 + 流水列表 + 加载更多)
+- [ ] **10.2 pages.json 注册**
+
+```json
+{
+  "path": "pages/energy/detail",
+  "style": {
+    "navigationBarTitleText": "维度能量详情"
+  }
+}
+```
+
+- [ ] **10.3 从 wuxing-sandbox 点击跳转**
+
+wuxing-sandbox 的 `@point-click` 事件在父页面处理:
+```js
+goToDomain: function (code) {
+  var childId = this.currentChildId || this.childId
+  var dim = this.sortedDimensions ? this.sortedDimensions.find(function (d) { return d.code === code }) : null
+  if (!childId || !dim) return
+  uni.navigateTo({
+    url: '/pages/energy/detail?childId=' + childId + '&code=' + code
+      + '&dimData=' + encodeURIComponent(JSON.stringify(dim))
+  })
+}
+```
+
+---
+
+## Phase 2(后续实施)
+
+以下任务在核心框架上线后,按业务优先级分步实施。
+
+### Task 11: 身体维度接口集成
+
+- 新建 `BodyHealthMetricController`(或扩展现有)中接入 `awardEnergy`
+- 实现体检报告能量计算逻辑(基础50 + 覆盖率 + 质量奖励 + 专项奖励)
+- 实现日常记录能量计算(连续性系数 × 丰富性系数,日上限50)
+- 覆盖逻辑:新报告 → 查找旧流水 → 扣旧发新
+
+### Task 12: 任务 category 默认比例映射
+
+在 `TaskService.completeTask()` 中,若 `energy_source_config` 无配置,按 `task.category` 找默认比例:
+
+| category | body | mind | wisdom | action | wealth |
+|----------|------|------|--------|--------|--------|
+| 运动类 | 60% | — | — | 40% | — |
+| 学习类 | — | — | 100% | — | — |
+| 生活习惯 | 40% | 30% | — | 30% | — |
+| 家务类 | — | 10% | — | 90% | — |
+| 阅读类 | — | 20% | 80% | — | — |
+| 财商类 | — | — | 10% | — | 90% |
+| 默认 | 30% | 20% | 20% | 25% | 5% |
+
+> `CATEGORY_DIMENSION_MAP` 定义在 `EnergyService` 中,`TaskService` 调用时自动 fallback
+
+### Task 13: 能量过期定时任务
+
+- 每日凌晨执行 `ScheduledEnergyExpiryTask`
+- SQL: `SELECT * FROM energy_log WHERE expires_at IS NOT NULL AND expires_at < NOW() AND ref_id IS NULL`
+- 对每条过期流水:扣减余额 + 写入回收流水(标记 `ref_id`)
+
+### Task 14: 旧代码清理(可选)
+
+- 确认 `energy_dimension_config` 表不再被任何功能引用
+- 删除 `EnergyDimensionConfig.java` / `EnergyDimensionConfigMapper.java` / `EnergySandboxDTO.java`
+- `MemberEnergyDTO.java` 保留(仍是健康指数的基础)
+
+---
+
+## 工作量汇总
+
+| Task | 内容 | 文件数 | 预估工时 | 依赖 |
+|------|------|--------|---------|------|
+| 1 | 数据库迁移 | 1 | 2h | — |
+| 2 | 4个Entity | 4 | 1h | T1 |
+| 3 | 4个Mapper | 4 | 0.5h | T2 |
+| 4 | EnergyService账本方法 | 1 | 3h | T3 |
+| 5 | EnergyController端点 | 1 | 1h | T4 |
+| 6 | TaskService集成 | 1 | 1h | T4 |
+| 7 | 前端API封装 | 1 | 0.5h | T5 |
+| 8 | wuxing-sandbox重写 | 1 | 2h | T7 |
+| 9 | 首页集成 | 2 | 1h | T7-T8 |
+| 10 | 能量详情页 | 2 | 2h | T7 |
+| **Phase 1** | **基础框架上线** | **18** | **≈2周** | — |
+| 11 | 身体维度集成 | 2+ | 2d | T4 |
+| 12 | category映射 | 1 | 1d | T6 |
+| 13 | 过期定时任务 | 1 | 1d | T4 |
+| 14 | 旧代码清理 | 3 | 0.5d | T4-T10 |
+| **Phase 2** | **全功能上线** | — | **≈1周** | — |
+
+---
+
+## 验证清单
+
+- [ ] `mvn clean compile` 通过
+- [ ] 应用启动无报错
+- [ ] `POST /api/energy/overview` 返回五维能量值
+- [ ] `POST /api/energy/logs` 返回流水(分页)
+- [ ] 完成任务后 `energy_log` 产生记录
+- [ ] `GET /api/energy/sandbox` 旧接口不破坏(向后兼容)
+- [ ] wuxing-sandbox 显示真实数据(非0占位)
+- [ ] 维度能量详情页可跳转
+- [ ] 日上限生效(测试连续发同名能量)
+
+## 相关文档
+
+- `docs/superpowers/specs/2026-06-05-five-dimension-energy-design.md` — 五维能量系统总设计(4表方案)
+- `docs/superpowers/specs/2026-06-09-body-dimension-energy-design.md` — 身维度计算规则
+- `docs/superpowers/specs/2026-06-09-remaining-dimensions-energy-design.md` — 心/智/行/富四维 + category映射
+- `docs/superpowers/plans/2026-06-09-energy-system-implementation-plan.md` — 本文件

+ 8 - 8
docs/superpowers/specs/2026-06-04-pre-login-discovery-design.md

@@ -2,13 +2,13 @@
 
 > **状态:** Draft
 > **日期:** 2026-06-04
-> **品牌名变更:** 溪艾福 → 艾福(本设计沿用新品牌名)
+> **品牌名变更:** 溪艾福 → 艾福(本设计沿用新品牌名)
 
 ---
 
 ## 1. 设计目标
 
-让未登录用户在登录前即可浏览平台内容,对艾福的定位和功能形成初步了解,降低注册门槛,提升转化率。
+让未登录用户在登录前即可浏览平台内容,对艾福的定位和功能形成初步了解,降低注册门槛,提升转化率。
 
 ### 核心原则
 
@@ -25,9 +25,9 @@
 
 | 原词 | 新词 | 范围 |
 |------|------|------|
-| 溪艾福 | 艾福 | 所有前端页面标题、文案、Banner |
-| 溪福俱乐部 | 福俱乐部 | 会员相关文案 |
-| 溪艾福 (Care Family Club) | 艾福 (Care Family Club) | 品牌全称 |
+| 溪艾福 | 艾福 | 所有前端页面标题、文案、Banner |
+| 溪福俱乐部 | 浠艾福俱乐部 | 会员相关文案 |
+| 溪艾福 (Care Family Club) | 艾福 (Care Family Club) | 品牌全称 |
 | XIAIFU | XIAIFU | 无变化(拼音相同) |
 
 ---
@@ -90,7 +90,7 @@
 ├─────────────────────────────────────────────┤
 │  ★ Banner 区                                │
 │  ┌───────────────────────────────────────┐  │
-│  │  🌟 艾福 Care Family Club           │  │
+│  │  🌟 艾福 Care Family Club           │  │
 │  │  以五行五维为内核的                     │  │
 │  │  家庭主动健康管理平台                   │  │
 │  │                                       │  │
@@ -253,7 +253,7 @@
 | `cfc-frontend/components/wuxing-sandbox.vue` | 新增 | 五行沙盘组件提取 |
 | `cfc-frontend/components/bottom-nav.vue` | 新增 | 自定义底部导航组件 |
 | `cfc-frontend/utils/api.js` | 新增 | 公开内容接口 |
-| 全站前端文件 | 文本替换 | 溪艾福→艾福 |
+| 全站前端文件 | 文本替换 | 溪艾福→艾福 |
 
 ### 不改动的范围
 - 后端 API 不需要新增(除非需要动态内容接口)
@@ -272,5 +272,5 @@
 - [ ] 商城页未登录时展示商品列表,点击详情/购买跳转登录
 - [ ] 登录后恢复原生 TabBar(首页/发现/商城/我的)
 - [ ] 退出登录后回到未登录态
-- [ ] 全站「溪艾福」文本已替换为「艾福」
+- [ ] 全站「溪艾福」文本已替换为「艾福」
 - [ ] 五行沙盘可复用(preview/parent/child 三种模式)

+ 190 - 0
docs/superpowers/specs/2026-06-08-wuxing-sandbox-refactor-design.md

@@ -0,0 +1,190 @@
+# 能量沙盘组件全面重构设计
+
+**日期**: 2026-06-08
+**状态**: 待实施
+**范围**: cfc-frontend/components/wuxing-sandbox.vue 及 3 个消费页面
+
+## 1. 背景与目标
+
+当前 `wuxing-sandbox.vue` 是一个 812 行的单文件组件,混合了 5 种职责(Canvas 渲染、填充动画、点击检测、视图模式切换、UI 编排),并包含 5 处死代码。`sandbox` 对象 prop 耦合后端 DTO 结构,导致消费页面需要 `safeSandbox` hack 规避 WXML 编译问题。
+
+**目标**:
+1. 将 812 行单文件拆分为 1 个薄编排组件 + 3 个纯 JS 工具模块
+2. 重新设计 props/events/slots 接口,扁平化、显式化
+3. 清理死代码(约 50 行)
+4. 保持 3 个消费页面的导入路径不变,仅适配接口
+
+## 2. 架构方案:单组件 + 提取工具模块
+
+### 2.1 文件结构
+
+```
+components/
+├── wuxing-sandbox.vue              # 主组件(orchestrator,~250行)
+└── wuxing-sandbox-helpers/
+    ├── render.js                   # Canvas 渲染函数(~180行)
+    ├── animation.js                # 动画控制器类(~80行)
+    └── hitdetect.js                # 点击检测工具(~50行)
+```
+
+### 2.2 为什么不用子组件拆分?
+
+uni-app 微信小程序中,`<canvas type="2d">` 与 HTML 元素(star-point 标签)在同一组件层级才能正确叠放。拆成子组件会引入:
+- canvas `createSelectorQuery().in(this)` 作用域问题
+- 跨组件 canvas 坐标对齐复杂性
+- HTML 覆盖层 z-index 在子组件间不可靠
+
+提取为纯 JS 工具模块是 uni-app Vue 2 下最安全的方式,无 mixin 命名冲突风险,函数可独立测试。
+
+## 3. 接口设计
+
+### 3.1 Props(扁平化替代 sandbox 对象)
+
+| Prop | Type | Default | 说明 |
+|------|------|---------|------|
+| `mode` | String | `'preview'` | `'preview'` 随机数据 / `'live'` 真实数据 |
+| `scores` | Object | `{body:0,mind:0,wisdom:0,action:0,wealth:0}` | 五维分值(0-100) |
+| `overallScore` | Number | `0` | 综合分值 |
+| `members` | Array | `[]` | 家庭成员数组(MemberEnergyDTO 格式) |
+| `badgeText` | String | `''` | 顶部徽章文字,空则自动计算 |
+| `showDimensionBar` | Boolean | `true` | 是否显示底部维度条 |
+| `starHeight` | String | `'480rpx'` | 星图区域高度 |
+
+**关键变更**:删除 `sandbox` 对象 prop,用 `scores` + `overallScore` + `members` 替代。组件内部处理 null/undefined,消费页面无需 `safeSandbox` hack。
+
+### 3.2 Events
+
+| Event | Payload | 说明 |
+|-------|---------|------|
+| `@point-click` | `{domain, viewMode, member}` | 点击星臂维度(保留原有) |
+| `@center-click` | `{viewMode, selectedMemberIndex}` | 点击中心太极区域 |
+| `@mode-change` | `{viewMode, selectedMemberIndex}` | 家庭↔个人模式切换后触发 |
+
+### 3.3 Slots
+
+| Slot | 说明 |
+|------|------|
+| `header` | 替换默认标题+徽章区域 |
+| `dimension-bar` | 替换默认维度详情条 |
+
+### 3.4 内部状态(非 prop)
+
+- `viewMode`: `'family'` | `'member'` — 当前视图
+- `selectedMemberIndex`: Number — 当前选中成员下标
+- `previewScores`: Array — 预览模式随机值
+- `canvasReady`, `canvasWidth`, `canvasHeight`, `ctx`, `dpr` — Canvas 状态
+- `animBody/Mind/Wisdom/Action/Wealth` — 动画插值
+
+## 4. 工具模块详细设计
+
+### 4.1 render.js — Canvas 渲染(纯函数,无状态)
+
+```js
+// 导出
+export function calcStarVertices(cx, cy, outerR) → { outer: [{x,y}], inner: [{x,y}] }
+export function renderStar(ctx, w, h, ts, options) → void
+export function drawTaiji(ctx, cx, cy, r) → void
+
+// ts = [body, mind, wisdom, action, wealth] 归一化值 (0~1)
+// options = { colors, outerR, showTaiji, taijiLabel }
+```
+
+`renderStar` 内部调用 `calcStarVertices`、`drawTaiji`,一次调用完成整个星图渲染。
+
+### 4.2 animation.js — 动画控制器
+
+```js
+export class FillAnimator {
+  constructor(getCurrentTs, onFrame, onComplete)
+  start(targetTs, duration = 600) → void   // ease-out cubic
+  cancel() → void                            // 清理 timer
+}
+```
+
+- `getCurrentTs()` 返回当前 5 维插值数组
+- `onFrame(interpolatedTs)` 每帧回调
+- `onComplete()` 动画完成回调
+- 使用 `setTimeout(fn, 16)` 模拟 requestAnimationFrame(uni-app 兼容)
+
+### 4.3 hitdetect.js — 点击检测
+
+```js
+export function pointInPolygon(x, y, polygon) → boolean
+export function detectHitArm(relX, relY, cx, cy, outerR) → number  // 0~4 或 -1
+```
+
+`detectHitArm` 封装了星臂多边形计算 + 遍历检测,返回命中星臂索引。
+
+## 5. 主组件 wuxing-sandbox.vue 结构(~250 行)
+
+```
+<template> (~55行)
+  - header slot
+  - star area: canvas + tap area + 5 point labels + center click
+  - dimension-bar slot (含默认内容)
+
+<script> (~120行)
+  - import from helpers
+  - props 定义
+  - data() — 内部状态
+  - computed — isPreview, currentMember, normalizedScores, activeTs, displayBadgeText 等
+  - watch — activeTs 变化触发动画
+  - mounted — 预览数据生成 + canvas 初始化
+  - beforeDestroy — animator.cancel()
+  - methods — toggleEnergyMode, onPointClick, onCanvasTap, initCanvas
+
+<style scoped> (~80行)
+  - 保留现有样式,微调
+```
+
+## 6. 死代码清理
+
+| 方法 | 行数 | 原因 |
+|------|------|------|
+| `_calcFillPolygon` | ~12行 | 填充算法改为全臂渐变后不再调用 |
+| `pointInCircle` | ~5行 | 未被任何代码引用 |
+| `dimVal` | ~8行 | 未被模板或方法调用 |
+| `curVal` | ~3行 | dimVal 的包装,同样未使用 |
+| `curMax` | ~3行 | 固定返回 100,未使用 |
+
+## 7. 消费页面适配
+
+### 7.1 parent-index.vue
+
+**Before**:
+```html
+<wuxing-sandbox mode="parent" :sandbox="safeSandbox" @point-click="goToDomain" />
+```
+
+**After**:
+```html
+<wuxing-sandbox mode="live"
+  :scores="energyScores"
+  :overall-score="energyOverall"
+  :members="energyMembers"
+  @point-click="goToDomain" />
+```
+
+删除 `safeSandbox` computed,新增 3 个简单 computed:
+```js
+energyScores() {
+  var d = this.energyData
+  return d ? { body: d.bodyScore||0, mind: d.mindScore||0, wisdom: d.wisdomScore||0, action: d.actionScore||0, wealth: d.wealthScore||0 } : { body:0,mind:0,wisdom:0,action:0,wealth:0 }
+},
+energyOverall() { return (this.energyData && this.energyData.overallScore) || 0 },
+energyMembers() { return (this.energyData && this.energyData.members) || [] }
+```
+
+### 7.2 index.vue / discover/index.vue
+
+仅 mode 值不变(`'preview'`),去掉 `sandbox` 相关,无需其他改动:
+```html
+<wuxing-sandbox mode="preview" badgeText="登录查看完整报告" @point-click="handleLogin" />
+```
+
+## 8. 不变项
+
+- 导入路径:3 个页面 `import WuxingSandbox from '../../components/wuxing-sandbox.vue'` 不变
+- 视觉效果:五角星渲染、太极图、填充动画、星臂颜色完全不变
+- Canvas type="2d" 绘制逻辑:仅从组件 methods 迁移到 render.js,算法不变
+- uni-app 兼容性:禁止可选链 `?.`、禁止 CSS Grid,继续遵守

+ 354 - 0
docs/superpowers/specs/2026-06-09-body-dimension-energy-design.md

@@ -0,0 +1,354 @@
+# "身"维度能量系统设计
+
+**日期:** 2026-06-09
+**状态:** 待确认
+**分支:** cfclub
+**依赖:** 五维能量系统设计 (`2026-06-05-five-dimension-energy-design.md`)
+
+---
+
+## 1. 设计约束与决策
+
+| # | 决策 | 含义 |
+|---|------|------|
+| 1 | 纯展示 | 能量值不可兑换、不可消耗,仅可视化 + 健康反馈 |
+| 2 | 完全独立 | 能量 ≠ 积分,PointsService 不受影响,energy_balance 独立记账 |
+| 3 | 不做IoT | 数据来源 = 用户主动上传/记录 + 任务系统触发 |
+| 4 | 专业版收费 | 体检/菌群报告 → 付费用户(会员/套餐购买后可解锁) |
+| 5 | 有过期+覆盖 | 部分能量值有效期到期;同类新数据可覆盖旧数据(能量值同步更新) |
+| 6 | 异常提醒 | 异常数据不惩罚,标记 + 鼓励复查 + 系统推送提醒 |
+
+---
+
+## 2. 数据来源总览
+
+```
+身维度能量来源
+├── 评估报告类(一次性高分)
+│   ├── 体检报告(专业版·付费)
+│   └── 菌群检测报告(专业版·付费)
+├── 日常记录类(持续低频)
+│   ├── 体重
+│   ├── 血压/心率
+│   ├── 睡眠
+│   └── 饮水量
+├── 任务完成类(系统化拿能)
+│   ├── 运动类任务
+│   └── 生活习惯任务
+└── 产品消费类(事后激励)
+    └── 商城健康类商品购买
+```
+
+---
+
+## 3. 各来源能量计算规则
+
+### 3.1 体检报告(专业版·付费)
+
+**触发条件:** 用户已购买会员/套餐,具备体检报告上传权限。
+
+**解析字段:**
+- 血压 systolic/diastolic
+- 血糖 fasting_blood_sugar
+- 血脂 total_cholesterol / triglyceride
+- 肝功能 alt / ast
+- 肾功能 creatinine
+- 血常规 wbc / rbc / hgb
+- BMI(由身高体重自动计算)
+
+**能量计算公式:**
+
+```
+体检报告能量 = 基础奖励 + 覆盖率奖励 + 质量奖励
+
+基础奖励:    50 能量(上传即得,鼓励参与)
+覆盖率奖励:  min(上传指标数 × 5, 50) 能量  → 上限 50
+质量奖励(按健康分档):
+  ≥85分(优秀)  → +100
+  70~84(良好)  → +50
+  55~69(一般)  → +20
+  <55(需关注)  → +10(鼓励复查,不惩罚)
+
+指标专项奖励(可叠加,每项 +10):
+  血压正常 / 血糖正常 / 血脂正常 / BMI正常
+```
+
+**举例:** 用户上传完整体检报告(10项指标),血压/血糖/血脂/BMI均正常,健康分78:
+```
+50(基础)+ 50(覆盖率)+ 50(质量档)+ 40(4项正常)= 190 能量
+```
+
+### 3.2 菌群检测报告(专业版·付费)
+
+**触发条件:** 同上(会员/套餐权限)。
+
+**关键字段:**
+- 多样性指数(Shannon index)
+- 益生菌属丰度(Bifidobacterium / Lactobacillus)
+- 有害菌占比
+- 菌群年龄(microbiome age) vs 实际年龄
+
+**能量计算公式:**
+
+```
+菌群报告能量 = 基础奖励 60 + 多样性奖励 + 益生菌奖励
+
+多样性指数 ≥ 4.0 → +20
+益生菌占比 ≥ 30% → +15
+菌群年龄 ≤ 实际年龄 → +15(年轻化)
+```
+
+### 3.3 日常状态记录(自由版)
+
+**频率上限与基础能量:**
+
+| 记录项 | 频率上限 | 基础能量 |
+|--------|---------|---------|
+| 体重 | 1次/天 | +3 |
+| 血压 | 2次/天 | +2 |
+| 心率 | 2次/天 | +2 |
+| 睡眠 | 1次/天 | +4 |
+| 饮水量 | 3次/天 | +1 |
+| 情绪状态 | 1次/天 | +1(心身联动) |
+
+**计算公式:**
+
+```
+单次记录能量 = 基础值 × 连续性系数 × 丰富性系数
+
+基础值:5 能量/次(常量)
+连续性系数:min(1 + log(N+1) / 2, 1.5)
+  第1天:×1.0 = 5
+  第3天:×1.2 = 6
+  第7天:×1.5 = 7(上限)
+丰富性系数:今日已记录类型数 / 总可用类型数
+  仅体重(1/4)= ×0.8
+  体重+血压+睡眠(3/4)= ×1.0
+
+日上限:50 能量(防止刷分)
+```
+
+### 3.4 健康任务完成(自由+专业)
+
+**维度比例映射表:**
+
+| 任务 category | 说明 | body | action | mind |
+|---------------|------|------|--------|------|
+| 运动类 | 跑步/跳绳/球类 | 60% | 40% | — |
+| 生活习惯 | 早睡/喝水/冥想 | 40% | 30% | 30% |
+| 学习类 | 健康知识学习 | — | — | 100%(智) |
+| 默认(未配置) | fallback | 100% | — | — |
+
+**计算方式:**
+```
+任务完成能量 = Task.points × 维度比例(从 energy_source_config 读取)
+
+示例:
+Task: 晨跑30分钟,points=5
+energy_source_config: task#123 → body:60%, action:40%
+结果:身维度 +3,行维度 +2
+```
+
+### 3.5 健康产品消费
+
+**计算方式:**
+```
+能量 = 订单金额 × Product.profitRate × product→body 映射比例
+
+示例:
+购买益生菌 200元,profitRate=30%,body 映射=100%
+能量 = 200 × 0.30 × 1.0 = 60 能量
+```
+
+---
+
+## 4. 过期与覆盖机制
+
+### 4.1 过期规则
+
+| 能量来源 | 有效期 | 说明 |
+|---------|--------|------|
+| 体检报告奖励 | 365天 | 每年需重新上传 |
+| 菌群检测奖励 | 180天 | 菌群变化较快 |
+| 日常记录能量 | 无过期 | 日/周上限限制总量 |
+| 任务完成能量 | 跟随任务 | 任务删除/过期时能量不回收(已发放不受影响) |
+| 产品消费能量 | 无过期 | 已发放不回收 |
+
+**过期处理:** 允许能量值为负值(表示"透支"或"欠健康"),系统不自动补正。
+
+### 4.2 覆盖规则(Overwrite)
+
+**触发条件:** 同一 report_type + 同一 user 上传新报告。
+
+**处理流程:**
+
+```
+1. 新报告解析 → 计算新能量值
+2. 查找旧报告对应的能量发放记录
+3. 扣减旧能量(-原值)+ 发放新能量(+新值)
+4. 净变化 = 新值 - 旧值
+5. 旧 energy_log 标记 is_overwrite=1,记录 overwritten_by
+```
+
+| 数据类型 | 覆盖行为 |
+|---------|---------|
+| 体检报告 | 新报告覆盖旧报告(能量值重算) |
+| 日常记录 | 单日多次记录 → 取最后一次有效(或累加,视策略) |
+| 任务完成 | 不覆盖(每次完成任务独立发放) |
+| 产品消费 | 不覆盖(每笔订单独立) |
+
+---
+
+## 5. 异常数据标记与提醒
+
+### 5.1 异常阈值
+
+| 指标 | 正常范围 | 异常标记 | 系统响应 |
+|------|---------|---------|---------|
+| 血压 | 90~140 / 60~90 | 偏高/低 | 提示"建议复查,持续偏高请咨询医生" |
+| 血糖(空腹) | 3.9~6.1 mmol/L | 偏高/低 | 同上 |
+| BMI | 18.5~23.9(中国标准) | 偏瘦/超重/肥胖 | 提示"你的BMI显示偏瘦/超重,建议关注" |
+| 睡眠时长 | 6~10小时 | 过短/过长 | 提示"睡眠不足/过长影响健康" |
+| 心率 | 60~100次/分 | 偏快/偏慢 | 提示"静息心率异常,建议复查" |
+
+### 5.2 前端展示规则
+
+- **正常数据:** 绿色圆点 + 正常数值
+- **边界数据**(正常范围边缘): 黄色圆点 + 轻微提醒
+- **异常数据:** 红色圆点 + 明确提醒 + "一键复查"按钮
+- **连续异常3天:** 推送给用户 + 可选的家属通知
+
+### 5.3 异常不惩罚原则
+
+异常数据 **不扣能量、不扣分、不影响等级**,仅:
+1. 数据标记为异常状态
+2. 展示时用颜色区分
+3. 给出改善建议
+4. 鼓励复查而非制造焦虑
+
+---
+
+## 6. 与现有系统集成
+
+### 6.1 任务系统集成
+
+在 `TaskService.completeTask()` 中,积分发放之后并行调用能量发放:
+
+```java
+// 现有:发放积分(已完成)
+pointsLogMapper.insert(pointsLog);
+
+// 新增:发放能量(并行,互不影响)
+try {
+    int energyAmount = Math.abs(pointsEarned);
+    if (energyAmount > 0) {
+        energyService.awardEnergy(
+            childId, "task", taskId, energyAmount,
+            "完成任务: " + task.getTitle()
+        );
+    }
+} catch (Exception e) {
+    log.warn("能量发放失败(不影响积分): {}", e.getMessage());
+}
+```
+
+### 6.2 体检报告上传触发能量
+
+在 `BodyHealthMetricController`(或新建 `BodyReportController`)的上传接口中:
+
+```java
+// 1. 保存报告数据
+// 2. 计算报告能量值(专业版规则)
+int energy = calculateReportEnergy(metrics);
+// 3. 发放能量
+energyService.awardEnergy(userId, "medical_report", reportId, energy, "体检报告上传");
+// 4. 若为新报告覆盖旧报告,先扣减旧能量
+// 5. 记录过期时间 → EnergyLog.expiresAt
+// 6. 异常指标 → 推送提醒
+```
+
+### 6.3 日常记录触发能量
+
+在 `BodyHealthMetricController.createRecord()` 中:
+
+```java
+// 1. 保存记录
+// 2. 检查今日是否已有同类记录(防刷)
+// 3. 计算能量 = 5 × 连续性系数 × 丰富性系数
+// 4. 能量上限检查(今日身维度是否已满50)
+// 5. awardEnergy("daily_record", recordId, calculatedEnergy, ...)
+```
+
+---
+
+## 7. 前端展示设计
+
+### 7.1 能量趋势图
+
+- 时间范围:周 / 月 / 年
+- 维度:身(Body)
+- X轴:日期,Y轴:能量值(可负)
+- 数据线:实线 = 能量值;虚线 = 目标线
+- 区域填充:正常(绿)/ 警告(黄)/ 异常(红)
+
+### 7.2 健康时间线
+
+```
+时间线节点:
+├─ 📊 体检报告 → 显示能量值 + 健康评分 + 异常标记
+├─ 📝 日常记录 → 小圆点 + 能量微增量
+└─ ✅ 任务完成 → 任务图标 + 能量增量
+点击节点可展开详情。
+```
+
+### 7.3 异常提醒卡片
+
+```
+⚠️ 健康提醒
+你的血压记录偏高(145/95),建议:
+1. 保持冷静,休息10分钟后复测
+2. 连续偏高请咨询医生
+3. 减少盐分摄入,规律作息
+
+[一键复查]  [查看历史趋势]
+```
+
+---
+
+## 8. 数据流总览
+
+```
+用户操作 → 数据采集 → 规则计算 → 能量发放 → 存储 → 展示
+  ├─ 上传体检报告 → BodyMedicalReport → 专业版规则 → awardEnergy
+  ├─ 完成任务     → Task.complete    → energy_source_config → awardEnergy
+  └─ 打卡记录     → body_health_metrics → 日常规则 → awardEnergy
+
+过期定时任务(每日):
+  扫描 energy_log.expires_at → 扣减过期能量 → 记录回收流水
+
+覆盖检测(新报告上传时):
+  新报告 → 旧报告标记overwritten → 扣减旧能量 + 发放新能量
+```
+
+---
+
+## 9. 待确认事项
+
+| # | 问题 | 当前假设 | 待确认 |
+|---|------|---------|--------|
+| 1 | 能量值为负的含义 | 允许负值,表示"透支" | 是否需要"归零"机制? |
+| 2 | 专业版门槛 | 付费用户 | 具体是会员?套餐?独立付费? |
+| 3 | 过期定时任务频率 | 每日一次 | 具体执行时间? |
+| 4 | 体检报告解析 | 手动标注/OCR | 后续是否接入OCR自动识别? |
+| 5 | 能量趋势图粒度 | 日/周/月 | 小程序端图表组件选型? |
+| 6 | 异常通知渠道 | 站内提醒 | 是否推送给家属? |
+
+---
+
+## 10. 相关文档
+
+- `docs/superpowers/specs/2026-06-05-five-dimension-energy-system.md` — 五维能量系统总设计
+- `docs/superpowers/plans/2026-06-05-five-dimension-energy-system.md` — 五维能量系统实施计划
+- `docs/superpowers/specs/2026-06-02-care-family-club-design.md` — 溪福俱乐部四域架构设计
+- `cfc-backend/entity/BodyHealthMetric.java` — 健康指标实体(已有)
+- `cfc-backend/entity/BodyNutritionLog.java` — 水营养记录实体(已有)

+ 404 - 0
docs/superpowers/specs/2026-06-09-remaining-dimensions-energy-design.md

@@ -0,0 +1,404 @@
+# 心/智/行/富 四维能量系统设计
+
+**日期:** 2026-06-09
+**状态:** 待确认
+**依赖:** `2026-06-09-body-dimension-energy-design.md`(身维度)
+
+---
+
+> 本文档仅描述 心(Mind)、智(Wisdom)、行(Action)、富(Wealth) 四个维度的**特有规则**。
+> 过期/覆盖/异常标记/前端展示等通用机制,统一复用身维度的设计。
+
+---
+
+## 1. 心 (Mind) — 心理情绪
+
+### 1.1 数据来源
+
+```
+心维度能量来源
+├── 评估报告类(一次性高分)
+│   ├── 心理测评(SCL-90 / SDS / SAS 等)
+│   └── 家庭氛围评估
+├── 日常记录类(持续低频)
+│   ├── 情绪日志(每日心情+标签)
+│   ├── 冥想/放松练习
+│   └── 睡眠记录(心身联动:sleep → mind 20%)
+└── 任务完成类(系统化拿能)
+    ├── 心理健康任务
+    └── 家庭关系任务
+```
+
+### 1.2 能量计算规则
+
+#### ① 心理测评(专业版·付费)
+
+**触发条件:** 已购买会员/套餐。
+
+**能量公式:**
+```
+心理测评能量 = 基础奖励 50 + 完成度奖励
+
+完成度 = 答题完成率 × 100
+  = 全部答完 → +50
+  = 完成80% → +40
+  = 完成50% → +25
+  = 完成<50% → 0(鼓励完整作答)
+
+结果解读奖励(一次性):
+  评估报告生成 → +30
+```
+
+#### ② 家庭氛围评估
+
+```
+能量 = 基础奖励 30 + 参与奖励 20 = 50 能量
+(所有家庭成员参与时额外 +10/人,上限 50)
+```
+
+#### ③ 日常情绪记录
+
+| 记录项 | 频率上限 | 基础能量 | 说明 |
+|--------|---------|---------|------|
+| 心情打卡 | 1次/天 | +2 | 选择心情标签(开心/平静/焦虑/低落等) |
+| 情绪笔记 | 3次/天 | +3 | 文字记录,>50字额外 +2 |
+| 冥想练习 | 1次/天 | +5~10 | 按完成时长:5min=+5, 10min=+8, 20min+=+10 |
+| 呼吸练习 | 3次/天 | +2 | 引导式呼吸练习完成 |
+
+**连续性系数:** 同身维度(连续记录有加成,上限 1.5x)。
+
+#### ④ 任务完成
+
+| 任务 task category | 映射维度 | body | mind | action | wisdom |
+|---------------------|---------|------|------|--------|--------|
+| 心理健康(mind task) | 心事 | — | 80% | — | 20% |
+| 家庭关系任务 | 心事 | — | 50% | 30% | 20% |
+| 冥想/正念 | 心事 | — | 70% | 10% | 20% |
+| 阅读/学习 | 智事 | — | — | — | 100% |
+| 运动 | 身事 | 60% | — | 40% | — |
+
+---
+
+## 2. 智 (Wisdom) — 学习认知
+
+### 2.1 数据来源
+
+```
+智维度能量来源
+├── 评估报告类(一次性高分)
+│   ├── DAN认知测评(已有)
+│   └── 专项认知测评(新增)
+├── 日常记录类(持续低频)
+│   ├── 阅读记录
+│   ├── 学习时长记录
+│   └── 课程/练习完成
+└── 任务完成类(系统化拿能)
+    ├── 学习任务(最大来源)
+    ├── 小游戏(数独/舒尔特/猜数字)
+    └── 专注训练
+```
+
+### 2.2 能量计算规则
+
+#### ① DAN认知测评(已有)
+
+```
+能量 = 基础奖励 30 + 等级奖励
+
+DAN等级 A1~B2  → +30
+        B3~B6  → +50
+        C级以上 → +80
+```
+
+#### ② 学习任务(最大来源)
+
+| 任务类型 | 说明 | body | mind | action | wisdom |
+|----------|------|------|------|--------|--------|
+| 作业完成 | 学校/培训作业 | — | — | — | 100% |
+| 阅读 | 课外阅读 | — | 20% | — | 80% |
+| 专注训练 | 番茄钟/简单模式 | — | 30% | — | 70% |
+| 小游戏 | 数独/舒尔特/猜数字 | — | 10% | — | 90% |
+| 错题复习 | 复习错题 | — | — | — | 100% |
+| 课程学习 | 在线课/录播课 | — | — | — | 100% |
+| 知识点测验 | 小测验正确率 | — | — | — | 100%(按正确率×points) |
+
+**知识点测验特殊规则:**
+```
+实际能量 = Task.points × (正确率 / 100) × wisdom比例
+例如:测验10题对8题,Task.points=10,正确率80%
+能量 = 10 × 0.8 × 1.0 = 8 能量
+```
+
+#### ③ 阅读记录
+
+```
+单次阅读记录:
+  基础能量 = 3
+  时长奖励 = min(阅读分钟数 / 10, 5) → 上限 +5
+  摘录/笔记奖励 = +3(有笔记内容时)
+  日上限 = 20 能量
+```
+
+---
+
+## 3. 行 (Action) — 关系践行
+
+### 3.1 数据来源
+
+```
+行维度能量来源
+├── 活动类(线下/线上活动)
+│   ├── 活动报名+签到
+│   ├── 活动照片/心得分享
+│   └── 活动创建(组织者)
+├── 家庭实践任务
+│   ├── 关系践行任务完成
+│   └── 长者关怀记录
+├── 社区互动
+│   ├── 社区发帖/评论
+│   └── 互助回答
+└── 任务完成类
+    └── action_practice_tasks(关系践行任务表)
+```
+
+### 3.2 能量计算规则
+
+#### ① 活动参与
+
+| 行为 | 能量 | 说明 |
+|------|------|------|
+| 报名活动 | +5 | 报名即给(防鸽) |
+| 签到到场 | +10~20 | 按活动类型:线上=+10,线下=+20 |
+| 活动后分享 | +5~15 | 有照片/心得:+10,仅签到:+5 |
+| 组织活动 | +30 | 活动创建者,活动结束后发放 |
+
+**活动签到特殊规则:**
+```
+如果活动有 fee(收费):
+  能量 = min(fee × 0.5, 50) → 按付费比例激励
+如果活动免费:
+  能量 = 10(基础签到) + 5(分享奖励)
+```
+
+#### ② 家庭实践任务
+
+```
+action_practice_tasks 完成:
+  能量 = Task.points × 以下比例:
+
+  relationship 类型 → body 20% + mind 30% + action 50%
+  parenting 类型  → mind 40% + wisdom 30% + action 30%
+  elder-care 类型  → body 10% + mind 20% + action 70%
+  community 类型  → action 60% + mind 20% + wisdom 20%
+```
+
+#### ③ 长者关怀记录
+
+```
+完成 care_item:
+  能量 = 5(基础)+ 关怀类型加成
+  生活照料 → +5
+  情感陪伴 → +8(>30分钟通话/见面)
+  健康管理 → +10(记录用药/血压等)
+  紧急事件   → +20(紧急情况处理)
+  日上限 = 30 能量
+```
+
+#### ④ 社区互动
+
+| 行为 | 能量 | 上限 |
+|------|------|------|
+| 发帖 | +5 | 3次/天 |
+| 评论 | +2 | 10次/天 |
+| 获赞 | +1/赞 | 20/天 |
+| 被精选 | +20 | 无上限(鼓励优质内容) |
+
+---
+
+## 4. 富 (Wealth) — 财富管理
+
+### 4.1 数据来源
+
+```
+富维度能量来源
+├── 任务完成类
+│   ├── 财商学习任务
+│   ├── 储蓄/记账任务
+│   └── 理财知识任务
+├── 产品消费类
+│   └── 理财/财商类商品购买
+└── 评估类
+    └── 财商测评(未来扩展)
+```
+
+### 4.2 能量计算规则
+
+#### ① 财商任务
+
+| 任务类型 | 说明 | 能量计算 |
+|----------|------|---------|
+| 记账 | 记录当日收支 | +3/天 |
+| 储蓄计划 | 完成当日储蓄目标 | +5~15(按目标完成率) |
+| 财商学习 | 完成课程/阅读 | +5~10 |
+| 理财目标 | 设置/更新理财目标 | +5 |
+
+**储蓄任务特殊规则:**
+```
+能量 = 基础 5 + 完成度奖励
+  = 100%完成 → +10
+  = 80%~99%  → +7
+  = 50%~79%  → +4
+  < 50%       → +1(鼓励记录,不惩罚)
+```
+
+#### ② 财商测评(未来)
+
+```
+能量 = 基础奖励 30 + 等级奖励
+
+入门级 → +20
+进阶级 → +40
+精通级 → +60
+```
+
+#### ③ 财商类商品购买
+
+```
+能量 = 订单金额 × Product.profitRate × wealth 映射比例
+
+wealth 映射比例默认 = 100%(财商商品纯归入富维度)
+```
+
+---
+
+## 5. 维度比例总表(energy_source_config 默认值)
+
+以下为各 source_type 的默认维度比例(当未手动配置时使用):
+
+| source_type | body | mind | wisdom | action | wealth |
+|-------------|------|------|--------|--------|--------|
+| task(默认) | 30% | 20% | 20% | 25% | 5% |
+| task:运动类 | 60% | — | — | 40% | — |
+| task:学习类 | — | — | 100% | — | — |
+| task:生活习惯 | 40% | 30% | — | 30% | — |
+| task:财商类 | — | — | 10% | — | 90% |
+| product(默认) | 50% | 15% | 15% | 15% | 5% |
+| product:health | 80% | 10% | — | 10% | — |
+| product:course | — | 10% | 80% | 10% | — |
+| activity(默认) | — | 15% | 10% | 70% | 5% |
+| consultation | 20% | 40% | 20% | 15% | 5% |
+
+> **说明:** 上述为推荐默认值,实际可通过 `energy_source_config` 表逐条配置。
+> `body` 默认值偏高是因为 CFC 核心用户是家长+孩子,身体维度应优先关注。
+
+---
+
+## 6. 日/周上限汇总
+
+| 维度 | 日获取上限 | 周获取上限 | 说明 |
+|------|-----------|-----------|------|
+| 身 | 50 | 300 | 防止打卡刷分 |
+| 心 | 40 | 200 | 情绪记录低频 |
+| 智 | 80 | 500 | 学习任务量大 |
+| 行 | 60 | 300 | 社区互动有上限 |
+| 富 | 30 | 150 | 财商任务低频 |
+
+**全局日上限:** 身+心+智+行+富 总和不超过 200 能量/天。
+
+---
+
+## 7. 过期规则(统一)
+
+| 维度 | 涉及来源 | 有效期 | 备注 |
+|------|---------|--------|------|
+| 身 | 体检/菌群报告 | 365/180天 | 已定义 |
+| 心 | 心理测评 | 365天 | 心理状态变化周期 |
+| 智 | DAN测评/专项测评 | 180天 | 认知能力可提升,定期重测 |
+| 行 | 活动记录 | 无过期 | 行为记录永久有效 |
+| 富 | 财商测评 | 365天 | 同心理测评 |
+
+---
+
+## 8. 专业版 vs 自由版对比
+
+| 能力 | 自由版(所有用户) | 专业版(付费用户) |
+|------|------------------|------------------|
+| 日常记录打卡 | ✅ | ✅ |
+| 健康任务完成 | ✅ | ✅ |
+| 商城购买获能 | ✅ | ✅ |
+| 心理测评 | ❌ | ✅ |
+| 体检报告上传分析 | ❌(仅手动记录) | ✅(报告解析+专项能量) |
+| 菌群检测 | ❌ | ✅ |
+| 家庭氛围评估 | ❌(仅单用户) | ✅(全家参与) |
+| AI健康解读 | ❌ | ✅ |
+
+---
+
+## 9. 数据结构补充
+
+### 9.1 `energy_source_config` 默认数据
+
+```sql
+-- 任务类默认配置
+INSERT INTO energy_source_config (source_type, source_id, source_name, dimension_id, ratio)
+VALUES
+  ('task_default', 0, '默认任务', 1, 0.3000),  -- body
+  ('task_default', 0, '默认任务', 2, 0.2000),  -- mind
+  ('task_default', 0, '默认任务', 3, 0.2000),  -- wisdom
+  ('task_default', 0, '默认任务', 4, 0.2500),  -- action
+  ('task_default', 0, '默认任务', 5, 0.0500); -- wealth
+
+-- 具体任务映射(示例)
+INSERT INTO energy_source_config (source_type, source_id, source_name, dimension_id, ratio)
+VALUES
+  ('task', 101, '晨跑30分钟', 1, 0.6000),   -- body
+  ('task', 101, '晨跑30分钟', 4, 0.4000),   -- action
+  ('task', 102, '课外阅读', 3, 1.0000),     -- wisdom
+  ('task', 103, '冥想练习', 2, 0.7000),     -- mind
+  ('task', 103, '冥想练习', 4, 0.1000),     -- action
+  ('task', 103, '冥想练习', 3, 0.2000);     -- wisdom
+```
+
+### 9.2 维度调整映射(category → 默认比例)
+
+在 `TaskService.completeTask()` 中,根据 `task.category` 查找默认比例:
+
+```java
+// category → 维度比例映射(fallback 当 energy_source_config 无配置时)
+private static final Map<String, BigDecimal[]> CATEGORY_DIMENSION_MAP = new HashMap<>();
+static {
+    // 运动类: body 60%, action 40%
+    CATEGORY_DIMENSION_MAP.put("运动类", new BigDecimal[]{0.6, 0, 0, 0.4, 0});
+    // 学习类: wisdom 100%
+    CATEGORY_DIMENSION_MAP.put("学习类", new BigDecimal[]{0, 0, 1.0, 0, 0});
+    // 生活习惯: body 40%, mind 30%, action 30%
+    CATEGORY_DIMENSION_MAP.put("生活习惯", new BigDecimal[]{0.4, 0.3, 0, 0.3, 0});
+    // 阅读类: wisdom 80%, mind 20%
+    CATEGORY_DIMENSION_MAP.put("阅读类", new BigDecimal[]{0, 0.2, 0.8, 0, 0});
+    // 财商类: wisdom 10%, wealth 90%
+    CATEGORY_DIMENSION_MAP.put("财商类", new BigDecimal[]{0, 0, 0.1, 0, 0.9});
+    // 默认: body 30%, mind 20%, wisdom 20%, action 25%, wealth 5%
+    CATEGORY_DIMENSION_MAP.put("default", new BigDecimal[]{0.3, 0.2, 0.2, 0.25, 0.05});
+}
+```
+
+---
+
+## 10. 实施优先级建议
+
+| 阶段 | 内容 | 预估工作量 |
+|------|------|-----------|
+| Phase 1 | 五维能量框架 + 身/智维度(任务系统集成) | 1周 |
+| Phase 2 | 心/行维度(任务+日常记录) | 1周 |
+| Phase 3 | 富维度(财商任务) | 3天 |
+| Phase 4 | 专业版评估(心理测评+体检上传) | 2周 |
+| Phase 5 | 过期/覆盖定时任务 + 前端可视化 | 1周 |
+
+---
+
+## 相关文档
+
+- `docs/superpowers/specs/2026-06-09-body-dimension-energy-design.md` — 身维度详细设计
+- `docs/superpowers/specs/2026-06-05-five-dimension-energy-design.md` — 五维能量系统总设计
+- `docs/superpowers/plans/2026-06-02-care-family-club-design.md` — 溪福俱乐部四域架构
+- `docs/superpowers/plans/2026-06-05-five-dimension-energy-system.md` — 五维实施计划

+ 2 - 2
docs/系统设计/小程序页面重设计划.md

@@ -7,7 +7,7 @@
 | 问题 | 详情 |
 |------|------|
 | **设计Token不统一** | `uni.scss` 中存在两套变量:旧 `$uni-primary: #FF6B6B` 和新 `--color-primary: #0EA5E9`,混用导致视觉不一致 |
-| **TabBar与业务脱节** | 当前 TabBar(首页/发现/商城/我的)是福俱乐部 Phase0 产物,弱化了核心任务/心愿功能 |
+| **TabBar与业务脱节** | 当前 TabBar(首页/发现/商城/我的)是浠艾福俱乐部 Phase0 产物,弱化了核心任务/心愿功能 |
 | **页面膨胀** | 76个 .vue 文件在 35个目录中,无统一组件体系,各页面独立开发,风格不统一 |
 | **设计Token冲突** | 旧版 `$uni-primary: #FF6B6B`(珊瑚红) vs 新版 `--color-primary: #0EA5E9`(天蓝) vs 新设计暖橙 `#F97316` — 三套并存 |
 | **角色分流混乱** | 首页 `index/index` -> `child-index` / `parent-index` 跳转流不直观 |
@@ -57,7 +57,7 @@
 | 家长/孩子 | 首页 | 任务 | 心愿单 | — | 我的 |
 | 规划师 | 首页 | 家庭 | 团队 | 成长档案 | 我的 |
 
-> ⚠️ **注意**:当前 `pages.json` 中配置的 TabBar 是「首页/发现/商城/我的」(福俱乐部 Phase0),与 `custom-tab-bar` 组件不一致。重设计时需统一。
+> ⚠️ **注意**:当前 `pages.json` 中配置的 TabBar 是「首页/发现/商城/我的」(浠艾福俱乐部 Phase0),与 `custom-tab-bar` 组件不一致。重设计时需统一。
 
 ---
 

+ 134 - 8
docs/需求分析/需求分析文档.md

@@ -1,4 +1,4 @@
-# 心知益家小程序需求分析文档
+# 浠艾福(XAF)需求分析文档
 
 ## 版本信息
 
@@ -6,7 +6,8 @@
 |------|------|------|----------|
 | V1.0 | 2026-03-30 | Sisyphus | 初始版本 |
 | V1.3 | 2026-05-04 | Sisyphus | 新增成长规划师注册与升级系统 |
-| **V2.0** | **2026-05-07** | **Sisyphus** | **基于开发进度重新梳理,分离已完成/未完成需求,更新为P0-MVP/P1-增强/P2-未来三层结构** |
+| V2.0 | 2026-05-07 | Sisyphus | 基于开发进度重新梳理,分离已完成/未完成需求,更新为P0-MVP/P1-增强/P2-未来三层结构 |
+| **V3.0** | **2026-06-08** | **Sisyphus** | **升级为"浠艾福"全人生管理平台,新增跨领域扩展分析与抽象化方案** |
 
 ---
 
@@ -16,12 +17,30 @@
 
 本产品源于一位程序员父亲为解决6岁孩子拖延症而开发的小程序"宝贝积分屋"。在解决亲子教育中"任务管理+即时反馈"这一核心痛点的基础上,引入"双培强基工程"的DAN测评体系,为每个孩子提供基于科学测评的个性化任务模板。
 
+**V3.0 扩展**:项目升级为"浠艾福"(XAF),从"亲子教育管理"扩展为覆盖**个人健康、老人康养、学习成长、认知提升、关系维护**等多个生活领域的全人生管理平台。核心机制(任务+积分+测评+角色协作)保持通用,通过领域抽象化配置适应不同场景。
+
 ### 1.2 产品定位
 
-**产品名称**:心知益家  
+**产品名称**:浠艾福(XAF)
 **产品类型**:微信小程序 + Web管理端  
-**核心定位**:基于DAN测评的家庭教育任务管理工具,通过积分奖励机制激励孩子自主完成任务,培养时间管理和延迟满足能力。  
-**目标用户**:6-12岁儿童家长(主要决策者)、6-12岁儿童(实际使用者)、成长规划师(专业服务提供者)
+**核心定位**:全人生成长管理平台,通过"驱动/监督/指导/协作"四种行为模式支撑用户在健康、教育、康养、关系、认知等领域的自我提升与互助成长。  
+**目标用户**:覆盖全年龄段——个人(自我提升)、家长(亲子教育)、子女(老人康养)、学习者(认知成长)、伙伴(关系维护)
+
+### 1.3 扩展领域愿景
+
+| 领域 | 驱动模式 | 核心角色 | 与现有代码匹配度 | 扩展成本 |
+|------|---------|---------|:---------------:|:--------:|
+| 亲子教育(当前) | 委托驱动(A→B) | parent→child | ✅ 地基 | — |
+| 老人康养 | 委托驱动(子女→父母) | guardian→elderly | ⚠️ 中等 | 中 |
+| 个人健康 | 自我驱动(A→A) | self→self | ⚠️ 中低 | 中 |
+| 认知提升/学习成长 | 专业指导(导师→学习者) | mentor→learner | ✅ **高** | 低 |
+| 关系维护 | 伙伴协作(A↔B) | partner↔partner | ❌ 低 | **建议独立模块** |
+
+**四种基础行为模式**:
+- **自我驱动**(Self-motivated):自己创建目标、自己执行、无审核(个人健康、自律训练)
+- **守望监督**(Guardian-led):守护者为受益人创建任务、审核成果(亲子教育、老人照护)
+- **专业指导**(Mentor-guided):专业导师制定计划、学习者执行并接受评估(学习成长、认知培训)
+- **伙伴协作**(Peer-collaborative):平级互相监督、共同推进(健身伙伴、关系维护)
 
 ---
 
@@ -325,6 +344,8 @@
 
 ## 5. 角色与权限矩阵
 
+### 5.1 当前角色体系(V2.0)
+
 | 角色 | 可登录端 | 核心权限 |
 |------|----------|----------|
 | **管理员** | Web管理端 | 全部系统管理、配置、审核 |
@@ -337,6 +358,18 @@
 - 规划师数据范围:以家庭为单位查看,进入家庭后才可见成员和任务详情
 - 规划师等级:初级/中级/高级,升级服务根据当前等级动态可见
 
+### 5.2 扩展角色体系(V3.0+ 未来)
+
+| 领域 | 守护者 | 成长者 | 指导者 | 伙伴 |
+|------|--------|--------|--------|------|
+| 亲子教育 | `parent` | `child` | `teacher` | — |
+| 老人康养 | `guardian`(子女) | `elderly` | `specialist`(康养规划师) | — |
+| 个人健康 | `self` | `self` | `coach`(健康教练) | `buddy`(健身伙伴) |
+| 认知提升 | `learner` | `self` | `mentor`(导师) | `study_group`(学伴) |
+| 关系维护 | — | — | — | `partner`(伴侣/亲友) |
+
+> **注意**:扩展角色体系需通过 `DomainConfig` 配置层实现,不对现有代码结构造成破坏。
+
 ---
 
 ## 6. 优先级总结
@@ -378,12 +411,105 @@
 | **测评订单** | 家长申请→规划师确认→支付→录入结果 | 完整B2B2C链路 |
 | **成长规划师** | 经认证的专业服务人员 | 可切换家长身份 |
 | **双培强基工程** | 青少年教育项目 | 通过科技、数据、AI、心理科学揭示成长规律 |
+| **DomainConfig** | 领域配置层,定义每个领域的角色映射/积分类型/审核规则 | V3.0 核心抽象 |
+| **自我驱动** | 创建者=执行者,跳过审核环节,自动完成 | 个人健康/自律场景 |
+| **平级监督** | 无层级关系的互相监督(伙伴模式) | 健身伙伴/学伴 |
+| **掌握度** | 学习领域的进度指标(0-100%) | 替代二元的完成/未完成 |
+| **关系事件** | 关系维护领域的核心实体,记录互动/情感 | 独立于Task模型 |
+
+---
+
+## 8. 跨领域扩展分析(V3.0 新增)
+
+### 8.1 四领域对比总览
+
+| 维度 | 老人康养 | 个人健康 | 认知提升 | 关系维护 |
+|------|---------|---------|---------|---------|
+| 驱动模式 | 委托驱动(子女→老人) | **自我驱动(自设目标)** | 专业指导(导师→学员) | 伙伴协作 |
+| 审核机制 | 子女审核 + 异常通知 | **默认无审核** | 自动+导师批改 | 无需审核 |
+| 积分归属 | User 实体 | User 实体 | User 实体 | 不适用(建议无积分) |
+| IoT 设备 | ✅ 药盒/手环/血压计 | ✅ 手环/体脂秤 | ❌ 无 | ❌ 无 |
+| 异常检测 | ✅ 行为异常预警 | ✅ 健康指标预警 | ❌ 无 | ❌ 无 |
+| 关系模式 | N→1(多子女→一老) | 平级(伙伴互督) | 层级(导师→学员) | **自由关系网络** |
+| 与现有代码匹配度 | ⚠️ 中等 | ⚠️ 中低 | ✅ **高(~90%)** | ❌ **低(~20%)** |
+
+### 8.2 抽象化优先级(按影响面排序)
+
+| 优先级 | 抽象项 | 影响领域 | 当前代码问题 | 改动量 |
+|--------|--------|---------|-------------|--------|
+| **P0** | `PointsLog.childId` → `ownerId+ownerType` | 康养+健康+学习 | 积分绑定 Child 表,User 不可持有 | ~4h |
+| **P0** | `User.role` 验证扩展 | 所有领域 | `isValidRole()` 只认 parent/child/teacher | ~10min |
+| **P1** | `TaskService.reviewTask()` 角色硬编码解除 | 康养+学习 | `getPendingReviewTasks()` 检查 `"parent"` | ~2h |
+| **P1** | 创建者=执行者的无审核路径 | 健康 | 当前所有 API 假设创建者是他人 | ~1天 |
+| **P2** | 平级关系模型(Buddy 互督) | 健康+关系 | Family 只支持层级关系 | ~3天 |
+| **P2** | 课程层级结构(Course→Module→Lesson) | 认知提升 | 只有 Task→TaskTemplate 两层 | ~3天 |
+| **P3** | IoT 设备数据通道 | 康养+健康 | 无设备数据接入机制 | ~2周 |
+| **P3** | 关系事件实体(独立于 Task) | 关系维护 | 任务模型完全不适用 | ~2天 |
+
+### 8.3 可复用模块清单(不需改动)
+
+以下模块在所有领域间通用:
+- **Task 实体核心字段**:title/points/status/deadline/repeatType/completeTypes — 与领域无关
+- **任务执行流水线**:`completeTask()` 的 guard → 状态变更 → 分发的逻辑通用
+- **连续打卡**:`StreakResetTask` → `StreakService` — 所有领域共用
+- **重复任务生成**:`RepeatTaskGenerator` 的模板复制逻辑通用
+- **44个 Service 接口层**:已提供抽象扩展基础
+- **审核类型字段**:`reviewType`(creator/parent/teacher/ai)— 字符串字段天然支持扩展
+
+### 8.4 需新增模块清单
+
+| 模块 | 所属领域 | 说明 | 预估工作量 |
+|------|---------|------|-----------|
+| `DomainConfig` 接口 + 配置体系 | 全局 | 角色映射/积分类型/审核规则 配置化 | ~3天 |
+| `MedicationReminderService` | 老人康养 | 定时用药提醒 + 时效敏感通知 | ~2天 |
+| `AnomalyDetectionService` | 康养+健康 | 行为/健康基线偏离预警 | ~1周 |
+| `Course`/`CourseModule`/`Lesson` 实体 | 认知提升 | 课程层级结构 | ~3天 |
+| `MasteryRecord` 实体 | 认知提升 | 掌握度追踪 | ~2天 |
+| `Buddy` 关系模型 | 个人健康 | 平级互相监督 | ~3天 |
+| `RelationshipEvent` 实体 | 关系维护 | 关系事件记录(独立模块) | ~2天 |
+| IoT 设备数据通道 | 康养+健康 | 手环/药盒/血压计数据接入 | ~2周 |
+| 多渠道通知通道 | 康养 | 短信+电话+小程序多渠道 | ~3天 |
+
+### 8.5 架构演化路径
+
+```
+V2.0(亲子教育):
+  TaskController → TaskService → Task / PointsLog / Child
+
+V3.0(多领域):
+  AbstractTaskController → DomainConfig(亲子教育) → parent/child/积分/Child持有
+                          → DomainConfig(老人康养) → guardian/elderly/关爱分/User持有
+                          → DomainConfig(个人健康) → self/coach/健康分/User持有
+                          → DomainConfig(认知提升) → mentor/learner/学分/User持有
+  RelationshipEventController → [独立于Task的模块]
+```
+
+**DomainConfig 接口定义(建议)**:
+```java
+public interface DomainConfig {
+    String getDomainType();
+    Map<String, String> getRoleMapping();
+    PointsConfig getPointsConfig();
+    ReviewConfig getReviewConfig();
+    NotificationConfig getNotificationConfig();
+}
+```
+
+### 8.6 参考文档
+
+| 文档 | 内容 |
+|------|------|
+| `docs/需求分析/领域对比分析-亲子vs康养.md` | 亲子教育 vs 老人康养 — 9维度 + 抽象化矩阵 |
+| `docs/需求分析/领域对比分析-亲子vs个人健康.md` | 亲子教育 vs 个人健康 — 自我驱动模式分析 |
+| `docs/需求分析/领域对比分析-亲子vs认知提升.md` | 亲子教育 vs 认知提升 — 课程层级与掌握度 |
+| `docs/需求分析/领域对比分析-亲子vs关系维护.md` | 亲子教育 vs 关系维护 — 独立模块建议 |
+| 《用户故事集V2》 | 42个用户故事(25原始 + 17扩展) |
 
 ---
 
-## 8. 验收标准
+## 9. 验收标准
 
-### 8.1 已完成的P0验收
+### 9.1 已完成的P0验收
 
 | 功能 | 验收条件 | 状态 |
 |------|----------|------|
@@ -396,4 +522,4 @@
 | 数据迁移 | curl POST /api/migration/run 执行全量迁移 | ✅ |
 | Web侧边栏 | 管理员和规划师看到不同的菜单 | ✅ |
 
-*本文档版本 V2.0,已完成功能与待完成功能分离。后续随开发进展持续更新。*
+*本文档版本 V3.0,新增跨领域扩展分析,已完成功能与待完成功能分离。后续随开发进展持续更新。*

+ 187 - 0
docs/需求分析/领域对比分析-亲子vs个人健康.md

@@ -0,0 +1,187 @@
+# 领域对比分析:亲子教育 vs 个人健康
+
+**版本**: V1.0
+**日期**: 2026-06-08
+**状态**: 初稿
+
+---
+
+## 概述
+
+本报告对比"亲子教育"(浠艾福现有核心领域)与"个人健康"(扩展目标领域)的任务生命周期,重点揭示**自我驱动模式**与现有**委托驱动模式**的根本性差异。
+
+分析基于:
+- 现有代码实现(Task/Service/Controller/Points系统)
+- 个人健康用户故事(US-H-01~03)
+- 跨领域整合故事(US-X-02 健康积分兑换)
+
+---
+
+## 核心差异:委托驱动 vs 自我驱动
+
+| 模式 | 亲子教育 | 个人健康 | 
+|------|---------|---------|
+| 创建者→执行者 | A(家长/老师)→ B(孩子) | **A→A**(自己给自己设目标) |
+| 监督者 | 家长/老师 | **自己 / 教练 / 伙伴 / AI** |
+| 任务性质 | 教育性、习惯养成 | 自律性、健康管理 |
+| 失败后果 | 扣分 | 扣分 + 健康风险 |
+| 激励机制 | 外在(心愿兑换) | **内在(健康数据改善)** |
+
+---
+
+## 二、九维度对比分析
+
+### 维度1:任务创建
+
+| 对比项 | 亲子教育(现有实现) | 个人健康(目标) | 评估 |
+|--------|-------------------|----------------|------|
+| 创建者 | 家长/规划师(他人) | **自己(自我驱动)** | ❌ 现有所有 API 都假设创建者是"他人" |
+| 是否默认创建即执行 | 否(assign → executor) | **是**(自己设目标自己完成) | ❌ 当前 `pending` 状态等待执行者接单 |
+| 是否可委托他人监督 | 否(家长天然是监督者) | **可选"教练"或"伙伴"作为监督者** | ❌ 新增"社会监督"模式 |
+| 第三方代创建 | 是(家长为孩子创建) | **是(教练为客户创建训练计划)** | ⚠️ 模式可复用,角色名不同 |
+| 首次性 | 「创建任务」一次性 | 「制定目标」+「每日动作」双层结构 | ❌ 目标-动作-进度 需要新设计 |
+
+**代码位置**: `TaskService.createTask()`, `CreateTaskDTO.java`
+
+**核心差异**: `CreateTaskDTO` 的 `childIds` 字段假设执行者是孩子。个人健康的执行者可以是自己(`userId`),也可以是教练指派给自己(`userId=executorId=self`)。当前无"自我创建"场景支持。
+
+---
+
+### 维度2:任务执行
+
+| 对比项 | 亲子教育 | 个人健康 | 评估 |
+|--------|---------|---------|------|
+| 执行者 | 孩子/家长 | **自己** | ⚠️ 角色参数化 |
+| 执行入口 | 小程序→我的任务 | **小程序/手环/设备** | ❌ 多入口 |
+| 自动记录 | 手动打卡 | **手环步数、心率自动记录** | ❌ IoT 设备数据通道 |
+| 连续执行 | 每日打卡 | **每日锻炼/每日用药** | ✅ 打卡逻辑通用 |
+| 完成标准 | 主观确认(拍照/打卡) | **客观数据达标(步数/心率区间)** | ❌ 数据驱动的完成判定 |
+
+---
+
+### 维度3:任务审核
+
+| 对比项 | 亲子教育 | 个人健康 | 评估 |
+|--------|---------|---------|------|
+| 审核必要性 | 部分任务需家长/老师审核 | **自己执行的无需审核**(自动完成) | ❌ 核心差异:自我驱动任务无审核环节 |
+| 教练/伙伴审核 | 无 | 可选:请教练/伙伴监督 + 远程确认 | ❌ 新增「社会监督」审核模式 |
+| AI审核 | reviewType=ai 已有 | **AI 审核运动姿势、饮食记录** | ⚠️ AI审核逻辑可扩展 |
+| 审核动作 | 通过→保持/驳回→重置 | 同 + 教练可反馈建议 | ⚠️ 审核动作参数化 |
+
+**核心差异**:个人健康的默认模式是**无审核→自动完成**,现有代码则默认"需要或被审核"。self-execution 路径(创建者=执行者)应默认跳过审核环节。
+
+---
+
+### 维度4:积分奖励体系
+
+| 对比项 | 亲子教育 | 个人健康 | 评估 |
+|--------|---------|---------|------|
+| 积分类型 | 教育积分(`totalPoints`) | **健康分/自律分**(`healthPoints`) | ❌ 新积分类型 |
+| 积分归属 | `Child` 实体 | **`User` 实体**(自己持有) | ❌ 当前绑定 `Child` |
+| 积分来源 | 任务完成 + 系统奖励 | 任务完成 + ❌**健康数据达标奖** | ❌ IoT 数据触发积分 |
+| 积分用途 | 心愿兑换 | ❌**健康层级解锁**(如解锁高级训练) | ❌ 新机制 |
+| 积分过期 | 有 | 可能无需过期(健康养成无截止) | ⚠️ 策略不同 |
+
+---
+
+### 维度5:角色关系模型
+
+| 对比项 | 亲子教育 | 个人健康 | 评估 |
+|--------|---------|---------|------|
+| 核心角色 | parent→child | **self→self / self→coach→self** | ❌ 全新关系 |
+| 教练 | teacher(教育类) | **coach(健康教练)** | ⚠️ "指导者"模式类似但领域不同 |
+| 伙伴 | 无 | **buddy(互相监督的健身伙伴)** | ❌ "平等监督"全新模式 |
+| AI | reviewType=ai | **AI 健康助手 / 训练检测** | ⚠️ AI 参与更深 |
+
+**核心差异**: 现有代码的角色模型是**层级式**(家长高于孩子、老师高于学生)。个人健康引入了**平级关系**(伙伴互相监督)——这是现有 `Family` 结构完全不支持的。
+
+---
+
+### 维度6:评估测评体系
+
+| 对比项 | 亲子教育 | 个人健康 | 评估 |
+|--------|---------|---------|------|
+| 评估类型 | DAN认知测评 | **体适能评估 / 健康风险评估** | ❌ 评估维度不同 |
+| 评估频率 | 3-6个月 | 1周-1个月(体测周期更短) | ⚠️ 周期可配置 |
+| 推荐机制 | 测评→推荐任务 | **体测→推荐训练计划(周期性强)** | ⚠️ 推荐引擎同,策略不同 |
+| 数据参考 | 单次测评 | **历史趋势 + 当前体能** | ❌ 需要持续性数据流 |
+
+---
+
+### 维度7:通知机制
+
+| 对比项 | 亲子教育 | 个人健康 | 评估 |
+|--------|---------|---------|------|
+| 通知方向 | 家长→孩子 | **自己→自己**(闹钟/提醒) | ❌ 自提醒模式 |
+| 社交通知 | 无 | **伙伴监督提醒**(你没打卡,我收到了通知) | ❌ 平级通知 |
+| 教练通知 | 规划师→家长 | **教练→学员**(训练建议/调整计划) | ⚠️ 类似 teacher→parent |
+| 设备通知 | 无 | **手环震动/手机通知** | ❌ 硬件联动 |
+
+---
+
+### 维度8:报表分析
+
+| 对比项 | 亲子教育 | 个人健康 | 评估 |
+|--------|---------|---------|------|
+| 报表类型 | 任务完成率、积分趋势 | **运动时长趋势、心率分布、体脂变化** | ❌ 指标不同 |
+| 展示对象 | 家长 | **自己 / 教练** | ⚠️ 受众不同 |
+| 健康预警 | 无 | **心率异常/运动过度/久坐提醒** | ❌ 实时健康预警 |
+| 数据可视化 | 简单统计 | **图表趋势、达标率可视化** | ⚠️ 前端可扩展 |
+
+---
+
+### 维度9:定时任务
+
+| 对比项 | 亲子教育 | 个人健康 | 评估 |
+|--------|---------|---------|------|
+| 每日重复 | ✅ `RepeatTaskGenerator` | ✅ 晨跑/午间拉伸/晚间冥想 | ✅ 复用 |
+| 连续打卡 | ✅ `StreakResetTask` | ✅ 连续运动天数 | ✅ 复用 |
+| **新增** | — | **设备数据同步定时任务** | ❌ 全新 |
+| **新增** | — | **健康趋势预警定时分析** | ❌ 全新 |
+
+---
+
+## 三、个人健康领域独有的全新机制
+
+| # | 机制 | 说明 | 工作量 |
+|---|------|------|--------|
+| 1 | **自我驱动模式** | 创建者=执行者时,自动跳过审核、默认自动完成 | ~1天 |
+| 2 | **平级监督(伙伴模式)** | 两人互相创建监督任务,无层级关系的监督 | ~3天 |
+| 3 | **目标-动作双层结构** | 目标健身目标(如"3个月减5kg")+ 每日动作(如"跑步30分钟") | ~5天 |
+| 4 | **IoT 健康数据通道** | 手环/体脂秤/心率带数据同步(与老人康养复用) | ~2周 |
+| 5 | **数据驱动的完成判定** | 步数达标→自动完成任务,无需手动打卡 | ~2天 |
+| 6 | **健康层级解锁** | 积分达到阈值→解锁更高阶训练计划 | ~3天 |
+
+---
+
+## 四、抽象化补充(基于现有分析)
+
+在亲子教育 vs 老人康养对比基础上,个人健康领域**新增**的抽象需求:
+
+| 抽象需求 | 说明 | 涉及的代码模块 |
+|---------|------|--------------|
+| `executorId` 扩展 | 当前 `childIds` 强绑定 child 表,需支持 executorId=userId | `CreateTaskDTO`, `Task` |
+| 无审核路径 | 当创建者=执行者时,整个审核环节跳过 | `TaskService.reviewTask()` 流程 |
+| 平级关系模型 | 伙伴互相监督→需要"非层级化"的关系绑定 | `Family.java`, 新 `Buddy` 实体 |
+| 目标-动作聚合 | 每日任务是"大目标"的子集 | `Task` 新增 `parentTaskId` |
+| 数据指标标准化 | 步数、心率、卡路里作为任务完成判定的客观输入 | 新 `Metric` 实体 + `MetricService` |
+
+---
+
+## 五、三个领域对比总结
+
+| 维度 | 亲子教育 | 老人康养 | 个人健康 |
+|------|---------|---------|---------|
+| 驱动模式 | 委托驱动(A→B) | 委托驱动(子女→老人) | **自我驱动(A→A)** |
+| 审核模式 | 家长/老师审核 | 子女审核 + 自动驳回通知 | **默认无审核 / 社会监督** |
+| 积分归属 | Child 实体 | User 实体 | User 实体 |
+| IoT 设备 | 无 | 药盒/手环/血压计 | 手环/体脂秤/心率带 |
+| 异常检测 | 无 | 行为异常预警 | **健康指标异常预警** |
+| 关系模式 | 层级(家长→孩子) | N→1(多子女→一老) | **平级(伙伴互督)** |
+
+---
+
+## 相关文档
+
+- `docs/需求分析/领域对比分析-亲子vs康养.md` — 老人康养对比
+- 用户故事集 V2 中的 US-H-01~03(个人健康)和 US-X-02(健康积分)

+ 128 - 0
docs/需求分析/领域对比分析-亲子vs关系维护.md

@@ -0,0 +1,128 @@
+# 领域对比分析:亲子教育 vs 关系维护
+
+**版本**: V1.0
+**日期**: 2026-06-08
+**状态**: 初稿
+
+---
+
+## 概述
+
+本报告对比"亲子教育"与"关系维护"领域。关系维护是所有扩展领域中与现有**任务模型**差异最大的领域——核心不再是"任务完成"而是"关系质量"的维护与追踪。
+
+---
+
+## 核心结论:需新模式支撑
+
+| 维度 | 亲子教育 | 关系维护 | 复用度 |
+|------|---------|---------|--------|
+| 核心概念 | 任务(完成/未完成) | **关系事件**(互动/交流/陪伴) | **20%** |
+| 成功标准 | 任务完成(二元) | **关系质量提升**(连续/多维) | **10%** |
+| 追踪方式 | 打卡/审核 | **日记/记录/情感反馈** | **15%** |
+| 激励机制 | 积分 → 心愿 | **关系成长图谱**(非功利性) | **30%** |
+
+**根本上不同**:亲子教育的任务模型是"用户完成任务 → 获得奖励"的**交易式**逻辑,而关系维护是"记录互动 → 滋养关系"的**记录式**逻辑。用"任务"来框架关系维护,反而可能扭曲用户行为。
+
+---
+
+## 二、关键差异分析
+
+### 差异1:驱动模式完全不同
+
+```
+亲子教育:  指派 → 执行 → 完成 → 奖励    (交易驱动)
+关系维护:  发生 → 记录 → 回顾 → 成长   (记录驱动)
+```
+
+关系维护的核心不是"我做了没有",而是"我感受到了什么"——无法用二元完成状态衡量。
+
+**举例**: 用户故事 US-R-01 "和父亲通了30分钟电话"
+- 任务模型:✅ 完成了"和父亲通电话"这个任务 → +10积分
+- 关系维护:记录"通了电话,聊了关于孙女的近况,父亲心情很好" → 关系图谱更新
+
+"积分奖励"反而会让关系维护变得功利化(为了积分才打电话)。
+
+### 差异2:没有"失败"概念
+
+| 亲子教育 | 关系维护 |
+|---------|---------|
+| 任务可失败(超时=扣分) | ❌ 没有"陪伴失败"——只有"今天没有记录" |
+| 错过打卡=影响积分 | ❌ 关系维护没有惩罚机制 |
+| 连续打卡=奖励 | ⚠️ 可以鼓励"连续记录",但不应惩罚"漏记" |
+
+**核心**: 关系维护应只有**正向激励**,没有负向反馈。
+
+### 差异3: "关系事件" 而非 "任务"
+
+关系维护需要一种新实体 `RelationshipEvent`:
+- 类型:电话/见面/礼物/共餐/旅行/散步...
+- 参与人:多对多(可以和多人同时)
+- 情感标注:开心/感动/平淡/想念...
+- 内容:自由文本日记
+- 关联对象:**可以同时关联多人**(家庭聚餐涉及所有成员)
+
+这与 `Task` 的"单人执行+单人审核"模式完全不同。
+
+---
+
+## 三、九维度对比分析
+
+| 维度 | 亲子教育 | 关系维护 | 差异度 |
+|------|---------|---------|--------|
+| 1. 创建 | 家长/老师创建给子 | **用户记录自己**发生的互动 | ❌ |
+| 2. 执行 | 孩子完成并打卡 | **无"执行"概念**,互动已自然发生 | ❌ 不适用 |
+| 3. 审核 | 家长/老师审核 | **无需审核**(个人记录) | ❌ 不适用 |
+| 4. 积分 | 有(交易式) | ❌**建议无积分**或弱关联(非交易) | ❌ |
+| 5. 角色 | parent/child/teacher | **任意关系对**(伴侣/朋友/亲子/祖孙) | ❌ |
+| 6. 测评 | DAN认知测评 | **关系质量自评**(情感温度计) | ⚠️ 不同但模式类似 |
+| 7. 通知 | 任务提醒 | 关系里程碑提醒("认识10年"纪念) | ⚠️ 弱关联 |
+| 8. 报表 | 完成率 | **关系图谱 / 互动频率热力图 / 情感趋势** | ⚠️ 指标不同 |
+| 9. 定时 | 重复/打卡/过期 | **生日/纪念日提醒** | ✅ 日历提醒可复用 |
+
+---
+
+## 四、可以复用的现有功能
+
+虽然关系维护与任务模型差异大,但以下底层基础设施可以复用:
+
+| 现有功能 | 复用方式 |
+|---------|---------|
+| `User` 实体 + 多角色 | 关系维护的对象都是 User,关系对是两个 User 的连接 |
+| `Family` 结构 | "家庭关系"是关系维护的一个子集(但需扩展为通用"关系网络") |
+| 打卡日期记录 | 关系事件的"日期"字段可复用打卡的数据格式 |
+| 通知推送机制 | 纪念日/生日提醒可复用 `TaskReminderService` |
+| 统计报表框架 | `TaskStatsService` 的统计框架可复用 | 
+
+---
+
+## 五、需要新增的模块
+
+| # | 新增模块 | 说明 | 工作量 |
+|---|---------|------|--------|
+| 1 | `RelationshipEvent` 实体 | 关系事件(类型/参与人/情感/内容) | ~2天 |
+| 2 | `RelationshipGraph` 关系图谱 | 用户之间的关系网络(非 Family 约束) | ~3天 |
+| 3 | 关系质量自评量表 | 情感温度计/亲密度量表 | ~2天 |
+| 4 | 里程碑自动识别 | 连续互动 N 天 / 认识 N 年 / 互动频率变化 | ~2天 |
+| 5 | 关系时间线可视化 | 前端时间轴组件(与双向日记集成) | ~3天 |
+
+> **建议**: 关系维护作为一个**独立功能模块**实现,不强行套用任务模型的框架。与核心系统的交互点限于:用户身份认证、通知推送、统计框架。
+
+---
+
+## 六、四个领域对比总览
+
+| 领域 | 与现有代码匹配度 | 核心差异 | 新增模块数 | 建议优先级 |
+|------|---------------|---------|-----------|-----------|
+| 老人康养 | ⚠️ 中等 | 角色扩展 + 积分归属 + IoT | 5个 | **高**(商业需求) |
+| 个人健康 | ⚠️ 中低 | 自我驱动模式 + 无审核 + IoT | 6个 | **高**(大众市场) |
+| 认知提升 | ✅ **高** | 课程层级结构 + 掌握度 | 5个 | **中**(依赖教育内容) |
+| 关系维护 | ❌ **低** | 非任务模型 + 关系图谱 | 5个 | **低**(离核心最远) |
+
+---
+
+## 相关文档
+
+- `docs/需求分析/领域对比分析-亲子vs康养.md`
+- `docs/需求分析/领域对比分析-亲子vs个人健康.md`
+- `docs/需求分析/领域对比分析-亲子vs认知提升.md`
+- 用户故事集 V2 中的 US-R-01~03(关系维护)

+ 218 - 0
docs/需求分析/领域对比分析-亲子vs康养.md

@@ -0,0 +1,218 @@
+# 领域对比分析:亲子教育 vs 老人康养
+
+**版本**: V1.0
+**日期**: 2026-06-08
+**状态**: 初稿
+
+---
+
+## 概述
+
+本报告对比"亲子教育"(浠艾福现有核心领域)与"老人康养"(扩展目标领域)两个领域的任务生命周期,识别现有代码中哪些可以直接复用、哪些需要抽象化、哪些需新增实现。
+
+分析基于:
+- 现有代码实现(Task实体/Service/Controller/Points系统)
+- 老人康养用户故事(US-E-01~03)
+- 跨领域整合故事(US-X-04 代际任务)
+
+---
+
+## 一、九维度对比分析
+
+### 维度1:任务创建
+
+| 对比项 | 亲子教育(现有实现) | 老人康养(目标) | 评估 |
+|--------|-------------------|----------------|------|
+| 创建者 | 家长(`parent`)或成长规划师(`teacher`) | 子女(`guardian`),多子女可共同创建 | ⚠️ 模式相同、角色名不同 |
+| 任务对象 | 孩子(`Child` 实体),通过 `childId` 关联 | 老人(`User` 实体,`role=elderly`),无独立"老人表" | ❌ 对象实体体系不同 |
+| 多指派 | `CreateTaskDTO.childIds` | 多子女共同照护,子任务按人分配 | ⚠️ 多指派逻辑可复用,分配语义不同 |
+| 默认积分 | 2分(硬编码 `DEFAULT_POINTS`) | 关爱分,按领域可配置 | ❌ 积分含义领域相关 |
+| 适龄任务池 | `TaskTemplateService` | 老人任务池 + 安全提醒类任务 | ✅ 模板机制通用 |
+
+**代码位置**: `TaskService.createTask()` (line 50), `TaskService.createSingleTask()` (line 96), `CreateTaskDTO.java`
+
+**核心差异**: `children` 表 vs `users` 表 — 老人没有独立实体。
+
+---
+
+### 维度2:任务执行
+
+| 对比项 | 亲子教育 | 老人康养 | 评估 |
+|--------|---------|---------|------|
+| 执行者 | 孩子(`executorType=child`) | 老人(`executorType=elderly`) | ⚠️ 模式相同,角色参数化 |
+| 完成方式 | 打卡/拍照/文字/语音/视频 | 打卡/拍照/语音(适老化) | ⚠️ 前端适老化,后端 `completeTypes` 通用 |
+| 自动完成 / 需审核 | 默认自动完成,特殊任务待审核 | 用药任务需子女审核,锻炼/社交自动完成 | ⚠️ 审核规则参数化 |
+| 智能设备 | 无 | 智能药盒/手环/血压计数据自动同步 | ❌ 全新机制 |
+
+**代码位置**: `TaskService.completeTask()` (line 263-310), `completeParentTask()` (line 479-500)
+
+---
+
+### 维度3:任务审核
+
+| 对比项 | 亲子教育 | 老人康养 | 评估 |
+|--------|---------|---------|------|
+| 审核人 | `reviewType`: creator/parent/teacher/ai | 子女(guardian)+ 康养规划师(specialist) | ⚠️ `reviewType` 字段已支持扩展 |
+| 审核逻辑 | 通过→保持completed;驳回→重置pending | 通过→保持completed;驳回→自动通知子女 | ❌ 审核动作可扩展(触发通知) |
+| 批量审核 | ✅ `batchReviewTasks()` | 子女一次审核老人多项任务 | ✅ 可直接复用 |
+| 单任务审核 | ✅ `reviewTask()` | 同接口 | ✅ 兼容 |
+| 硬编码检查 | `TaskService.java:432` 检查 `!"parent".equals(role)` | 应改为根据 `reviewType` 匹配 | ❌ 硬编码需抽象 |
+
+**代码位置**: `TaskService.reviewTask()` (line 377-405), `TaskService.getPendingReviewTasks()` (line 429-444)
+
+---
+
+### 维度4:积分奖励体系
+
+| 对比项 | 亲子教育 | 老人康养 | 评估 |
+|--------|---------|---------|------|
+| 积分类型 | `totalPoints`(通用)+ `systemPoints`(系统) | 关爱分(`carePoints`) | ❌ 新币种,但 `PointsLog.category` 已预留 |
+| 积分归属 | `Child` 实体,`PointsLog.childId NOT NULL` | `User` 实体 或家庭共享账户 | ❌ 当前积分绑定 `Child` 表 |
+| 积分用途 | 兑换心愿/奖励 | 兑换子女陪伴/家庭聚会/实物 | ⚠️ 兑换逻辑可复用,商品目录不同 |
+| 有效期 | ✅ 有有效期机制 | 可能需要不同策略 | ⚠️ 策略参数化 |
+
+**硬编码约束**:
+- `PointsLog.java:18` — `private Long childId;` NOT NULL,无法记录老人积分
+- `PointsService.awardSystemPoints()` — 参数 `Long childId`
+- `PointsService.getBalance()` — 参数 `Long childId`
+
+---
+
+### 维度5:角色关系模型
+
+| 对比项 | 亲子教育 | 老人康养 | 评估 |
+|--------|---------|---------|------|
+| 核心关系 | 家长→孩子(1对N) | 子女→父母(N对1,多人照护一人) | ⚠️ 方向一致,人数配比反转 |
+| 角色值 | parent/child/teacher | guardian/elderly/specialist | ❌ 新角色值需注册 |
+| 关系绑定 | `User.familyId` → 同家庭 | 多子女绑定到同一老人 | ❌ Family 是家长中心聚合 |
+| 多角色 | `User.roles` JSON字段 | 子女可能同时也是家长 | ⚠️ 多角色机制已部分支持 |
+
+**硬编码约束**:
+- `UserService.isValidRole()` (line 589) — 只允许 `parent/child/teacher`
+- `Family.java` — 以 `creatorId` 为中心,不支持 N→1 关系
+
+---
+
+### 维度6:评估测评体系
+
+| 对比项 | 亲子教育 | 老人康养 | 评估 |
+|--------|---------|---------|------|
+| 测评类型 | DAN认知/注意力/逻辑 | ADL日常能力 + 认知状态 + 精神状态 | ❌ 评估维度不同 |
+| 测评人 | 成长规划师 | 康养规划师/社区医生 | ⚠️ 模式相同 |
+| 推荐机制 | DAN等级→推荐任务模板 | 评估→推荐照护计划(多人分配) | ⚠️ 推荐引擎逻辑同,策略不同 |
+| 复测周期 | 3-6个月 | 1-3个月(慢性病) | ⚠️ 周期参数化 |
+
+**评估→推荐→执行 的流水线模式通用,但评估量表和推荐策略是领域特定的。**
+
+---
+
+### 维度7:通知机制
+
+| 对比项 | 亲子教育 | 老人康养 | 评估 |
+|--------|---------|---------|------|
+| 通知方向 | 家长→孩子 | 子女→父母 | ✅ 方向相同 |
+| 关键触发 | 到期/超时/审核 | 用药提醒(定时定点)+ 异常行为 | ❌ 用药提醒 + 异常检测 |
+| 通知渠道 | 小程序内 | 小程序 + 短信/电话(紧急) | ❌ 多渠道扩展 |
+| 异常检测 | 无 | 未打卡→异常→紧急通知子女 | ❌ 全新机制 |
+
+---
+
+### 维度8:报表分析
+
+| 对比项 | 亲子教育 | 老人康养 | 评估 |
+|--------|---------|---------|------|
+| 报表类型 | 完成率/积分趋势/分类统计 | 健康指标趋势/活动规律/认知评分 | ❌ 指标不同 |
+| 接收者 | 家长 | 子女 + 康养规划师 | ⚠️ 多方分发逻辑同 |
+| 异常预警 | 无 | 活动量骤降→健康预警 | ❌ 全新机制 |
+
+---
+
+### 维度9:定时任务
+
+| 对比项 | 亲子教育 | 老人康养 | 评估 |
+|--------|---------|---------|------|
+| 每日重复 | `RepeatTaskGenerator` 05:00 | 用药提醒(定时定点) | ✅ 生成逻辑通用 |
+| 连续打卡 | `StreakResetTask` 00:05 | 连续服药/活动打卡 | ✅ 计数逻辑通用 |
+| 积分过期 | `PointsExpireTask` 01:00 | 关爱分策略可能不同 | ⚠️ 策略参数化 |
+| 异常检测 | 无 | 检查老人活动情况 | ❌ 全新定时任务类型 |
+
+---
+
+## 二、抽象化矩阵
+
+| 层级 | 需抽象化 | 已通用(无需改动) | 需新增 |
+|------|---------|------------------|-------|
+| **实体层** | `PointsLog.childId` → `ownerId+ownerType` | `Task` 核心字段 | `ElderlyAssessment`, `DeviceRecord` |
+| | `User.role` 角色验证扩展 | `Family` 框架 | |
+| | 新增 `domain` 字段到核心实体 | | |
+| **Service层** | `getPendingReviewTasks()` 角色硬编码 | `createSingleTask()`, `completeTask()` | `MedicationReminderService` |
+| | `PointsService` 全部 child-centric 方法 | `StreakService`, `StreakResetTask` | `AnomalyDetectionService` |
+| | `RepeatTaskGenerator` 按 domain 筛选 | | IoT 设备数据接入 |
+| **Controller层** | role 检查点参数化 | `TaskController` 基础 CRUD | 领域路由 / 领域适配器 |
+| **配置层** | 领域配置(Domain Config) | 定时任务框架 | 异常检测规则配置 |
+| | 积分领域映射 | JWT 认证 | 多渠道通知配置 |
+
+---
+
+## 三、代码改动清单
+
+### 🟢 改动最小(纯配置/参数化)
+
+| # | 改动项 | 文件 | 当前写法 | 改为 | 工作量 |
+|---|--------|------|---------|------|--------|
+| 1 | 角色验证 | `UserService.java:589` | 硬编码 parent/child/teacher | 配置驱动 | ~10min |
+| 2 | 积分类别 | `PointsLog.java:26-27` | category="task"/"system" | +"care"、"health" | ~5min |
+| 3 | 审核类型 | `Task.java:49-50` | reviewType=creator/parent/teacher/ai | +"guardian"、"specialist" | ~5min |
+
+### 🟡 中等改动(逻辑抽象)
+
+| # | 改动项 | 文件 | 问题 | 方案 | 工作量 |
+|---|--------|------|------|------|--------|
+| 4 | 审核角色检查 | `TaskService.java:432` | `!"parent".equals(role)` | 改为 `reviewType` 匹配 | ~2h |
+| 5 | 积分归属 | `PointsService.java` 全部 | `Long childId` 参数 | `ownerId+ownerType` | ~4h |
+| 6 | 任务角色检查 | `GuideFamilyTaskController.java:53` | `"teacher"` 硬编码 | 配置的 domain 角色检查 | ~1h |
+
+### 🔴 最大改动(架构新增)
+
+| # | 改动项 | 说明 | 工作量 |
+|---|--------|------|--------|
+| 7 | Domain 配置层 | 领域配置:角色映射/积分类型/审核规则 | ~3天 |
+| 8 | 家族关系模型扩展 | Family 不支持 N→1,需新增 CareGroup 或扩展 | ~5天 |
+| 9 | IoT 设备数据通道 | 手环/血压计/智能药盒 | ~2周 |
+| 10 | 异常检测引擎 | 行为基线偏离预警 | ~1周 |
+| 11 | 多渠道通知 | 小程序 + 短信/电话/语音 | ~3天 |
+
+---
+
+## 四、架构演化路径
+
+```
+当前(亲子教育 only):
+  TaskController → TaskService → Task/PointsLog/Child
+
+目标(多领域):
+  AbstractTaskController
+       → DomainConfig(亲子教育): parent/child/积分/Child持有
+       → DomainConfig(老人康养): guardian/elderly/关爱分/User持有
+       → DomainConfig(个人健康): self/coach/健康分/User持有
+```
+
+**DomainConfig 接口定义**:
+```java
+public interface DomainConfig {
+    String getDomainType();                    // education, eldercare, health...
+    Map<String, String> getRoleMapping();      // 角色名映射
+    PointsConfig getPointsConfig();            // 积分类型与归属
+    ReviewConfig getReviewConfig();            // 审核规则
+    NotificationConfig getNotificationConfig(); // 通知策略
+}
+```
+
+---
+
+## 相关文档
+
+- `docs/需求分析/需求分析文档.md` — 全模块需求总览
+- `docs/task-requirements.md` — 任务管理需求规格
+- `docs/guide-requirements.md` — 成长规划师需求规格
+- 《用户故事集V2》中的 US-E-01~03(老人康养)和 US-X-04(代际任务)

+ 111 - 0
docs/需求分析/领域对比分析-亲子vs认知提升.md

@@ -0,0 +1,111 @@
+# 领域对比分析:亲子教育 vs 认知提升/学习成长
+
+**版本**: V1.0
+**日期**: 2026-06-08
+**状态**: 初稿
+
+---
+
+## 概述
+
+本报告对比"亲子教育"与"认知提升/学习成长"领域。认知提升是所有扩展领域中与现有代码**最匹配**的——DAN 测评的设计初衷就是认知评估,测评→推荐学习路径的流水线与现有逻辑几乎一致。
+
+---
+
+## 核心结论:现有代码覆盖度最高
+
+| 维度 | 亲子教育 | 认知提升/学习成长 | 复用度 |
+|------|---------|-----------------|--------|
+| 测评体系 | DAN 认知测评 | 能力评估(语言/逻辑/创造力) | **90%** |
+| 任务模型 | 孩子完成学习任务 | 学习者完成课程/练习 | **80%** |
+| 推荐引擎 | 测评→推荐任务模板 | 能力评估→推荐学习路径 | **85%** |
+| 积分体系 | 教育积分 | 学习积分/学分 | **70%** |
+| 角色分工 | 老师→孩子 | 导师→学员/自学 | **75%** |
+
+**主要差异不在"是否适配",而在"粒度升级"**:从"零散任务"升级为"课程体系→模块→课时→练习"的结构。
+
+---
+
+## 二、关键差异点
+
+### 1. 任务结构的层级升级
+
+```
+亲子教育(当前):    任务 → 模板(Task + TaskTemplate)
+认知提升(目标):    课程 → 模块 → 课时 → 练习 → 测验
+                    (Course → Module → Lesson → Exercise → Quiz)
+```
+
+现有 `Task` 实体的 "重复任务" 模式(`repeatType=daily`/`weekly`)只能表达"每天做这个",不能表达"先学第1课,再做第2课"的序列化学习路径。
+
+**需要新增**:
+- `Course`(课程)— 顶层学习单元
+- `CourseModule`(模块)— 课程的子章节
+- `Lesson`(课时)— 最小的学习单元,对应一个 Task(可选)
+
+### 2. 进度追踪
+
+| 亲子教育 | 认知提升 |
+|---------|---------|
+| 完成/未完成(二元) | **掌握度 0-100%**(完成课时→70%,通过测验→100%) |
+| 无前后置依赖 | 课程有**前置依赖**(学完A才能学B) |
+| 积分累计 | 学分累计 + **能力图谱点亮** |
+
+**需要新增**: `Mastery` 概念(掌握度)和前置依赖(`prerequisites`)机制。
+
+### 3. 推荐机制升级
+
+现有 DAN 测评结果 → 推荐模板的流程基本可用,但认知提升需要更精细的:
+- **测评→能力差距分析** → 推荐课程
+- **学习进度→自适应调整** → 推荐下一课时
+- **测验结果→薄弱环节强化** → 知识点回朔
+
+这不是重新设计,而是在现有推荐引擎上的**增强**(规则引擎 → 自适应算法)。
+
+### 4. 导师角色
+
+现有 `teacher` 角色(成长规划师)可以与认知提升的 `mentor`(导师)高度复用:
+- 规划师分配学习任务 ✅ 导师布置课程作业
+- 规划师审核完成情况 ✅ 导师批改测验
+- 规划师生成报告 ✅ 导师出具学习报告
+
+**角色名不同但模式相同**,属于 DomainConfig 角色映射范畴。
+
+---
+
+## 三、九维度对比分析
+
+| 维度 | 亲子教育 | 认知提升/学习 | 评估 |
+|------|---------|-------------|------|
+| 1. 任务创建 | 家长/老师 创建给 孩子 | 导师创建课程/自学选课 | ⚠️ 多"选课"模式 |
+| 2. 任务执行 | 每日打卡 | 课时学习 + 练习 + 测验 | ⚠️ 序列化执行 |
+| 3. 任务审核 | 家长/老师审核 | 自动完成 + 导师批改 | ⚠️ 混合模式 |
+| 4. 积分体系 | child-centric, task/system | User-centric, 学分 + 学习币 | ⚠️ 归属 + 币种不同 |
+| 5. 角色关系 | parent/child/teacher | self/mentor/learner | ⚠️ 角色映射即可 |
+| 6. 评估测评 | DAN 认知测评 | 能力评估 + 知识测验 | ✅ 高度复用 |
+| 7. 通知机制 | 任务提醒 | 课程更新/测验成绩通知 | ✅ 基本复用 |
+| 8. 报表分析 | 完成率/积分 | 学习时长/掌握度/知识图谱 | ⚠️ 指标更丰富 |
+| 9. 定时任务 | 重复/打卡/过期 | 定时课程推送/测验提醒 | ✅ 复用 |
+
+**整体评估**: 认知提升/学习成长是扩展成本最低的领域。
+
+---
+
+## 四、需要新增的功能
+
+| # | 功能 | 说明 | 预估工作量 |
+|---|------|------|-----------|
+| 1 | `Course` + `Module` + `Lesson` 实体 | 课程层级结构 | ~3天 |
+| 2 | 前置依赖机制 | `prerequisites` 字段 + 解锁逻辑 | ~1天 |
+| 3 | 掌握度模型 | `MasteryRecord` 实体 + 进度计算 | ~2天 |
+| 4 | 测评结果→课程推荐增强 | 在现有推荐引擎上增强 | ~2天 |
+| 5 | 知识图谱可视化 | 前端新增能力图谱页面 | ~3天 |
+
+**注意**: 以上大部分工作量属于**新增**,而非修改现有代码。现有 Task/Assessment/Teacher 体系改动极小。
+
+---
+
+## 相关文档
+
+- `docs/dan-requirements.md` — DAN 测评需求(现有,高度相关)
+- 用户故事集 V2 中的 US-L-01~02(学习成长)和 US-C-01~02(认知提升)