Ver código fonte

docs(unlock-gates): 补写实施计划 - 恢复被清空内容+状态更新为全部完成+追加后续修复记录

E2E Test Bot 1 mês atrás
pai
commit
feddfd87d3

+ 659 - 0
docs/superpowers/plans/2026-08-10-unlock-gates-implementation.md

@@ -0,0 +1,659 @@
+# 实施计划:通关解锁功能(Unlock Gates)
+
+- **日期**: 2026-08-10
+- **依据**: `docs/superpowers/specs/2026-08-10-unlock-gates-design.md` v2.1
+- **状态**: ✅ **全部完成**(迁移222-223/实体/Mapper/UnlockGateService 8判定/UnlockController/EnergyController view门禁/5处钩子/AdminUnlockGateController/WuxingSandbox切换/gate-lock/gate-guide-bar/关卡中心页/parent-index+child-index门禁/API兼容层)
+  - 2026-08-13 修复:通关奖励真实发放(`PlatformPointsService.earn` 发 CF值 + `EnergyService.awardEnergy` 发能量,替代原积分占位逻辑);`refreshFamilyGates` 增加 `triggerUserId` 重载,4 处钩子传入触发用户;前端文案"积分"统一改"CF值"(小程序 gate-lock/gates + 管理端 gates)
+  - 2026-08-13 增强:管理端 gates.vue 列表分页(后端 `selectPage` + `el-pagination`)
+- **验证基线**: 后端 `mvn clean compile`(在 `cfc-backend` 下);前端语法校验(`node --check` 提取 script 块)
+
+## 0. 总览
+
+本计划实现"通关解锁"功能,将能量沙盘拆分为**个人/全家两层**,通过三关主线硬门禁(自检→邀请家人→创建家人)递进解锁,辅以软引导(微行动/打卡/复测)和增值关卡(购物/会员/上传报告),每关均有积分+能量奖励。
+
+### 架构要点
+
+```
+DatabaseInitializer 迁移 222-223 → unlock_gates + family_gate_progress 表
+UnlockGateService(8种判定 + 奖励发放)
+  ├── SELF_CHECK           → five_dimension_self_checks(个人级)
+  ├── INVITE_FAMILY        → user 表 family_id 成员数 ≥2
+  ├── CREATE_FAMILY_MEMBER → family_member 表记录 ≥1
+  ├── PURCHASE             → 订单表支付成功
+  ├── MEMBERSHIP           → family_memberships 有效订阅
+  ├── REPORT_UPLOAD        → health_reports 记录 ≥1
+  ├── MICRO_ACTION         → micro_action_records 记录 ≥ count
+  └── STREAK_DAYS          → FamilyMember.streakDays ≥ days
+EnergyController./sandbox 加 ?view=personal|family 参数 → 门禁拦截
+```
+
+### 主要变更
+
+| 层 | 文件 | 变更类型 |
+|----|------|---------|
+| 迁移 | DatabaseInitializer.java | 新增迁移 222-223 |
+| schema.sql | 追加 unlock_gates + family_gate_progress 建表 | 同步 |
+| 后端 | UnlockGateService.java | 新建 |
+| 后端 | UnlockController.java | 新建 |
+| 后端 | AdminUnlockGateController.java | 新建 |
+| 后端 | EnergyController.java | 改造 sandbox 接口 |
+| 后端 | FiveDimensionSelfCheckController.java | 加 refresh 钩子 |
+| 后端 | FamilyInviteController.java | 加 refresh 钩子 |
+| 后端 | FamilyMembersController.java | 加 refresh 钩子 |
+| 后端 | 支付/会员/报告回调 | 加 refresh 钩子 |
+| 小程序 | WuxingSandbox 组件 | 加个人/全家切换 |
+| 小程序 | components/gate-lock.vue | 新建 |
+| 小程序 | components/gate-guide-bar.vue | 新建 |
+| 小程序 | pages/profile-extra/gates.vue | 新建 |
+| 小程序 | 8+ 个页面 | 沙盘调用点改造 |
+| 管理端 | views/admin/gates.vue | 新建 |
+| 种子数据 | DatabaseInitializer.java | 默认 9 条关卡配置 |
+
+### 项目约定(必须遵守)
+
+- 接口一律 `@PostMapping`;DI 用 `@Resource`;响应统一 `Result<T>`;实体 `@TableName` + `@TableId(type = IdType.AUTO)` + `@Data`
+- 迁移编号递增:本计划新增为 **迁移222-223**(当前最大 221)
+- 迁移幂等:try-catch 包裹,注释 `// 迁移N: ...`
+- schema.sql 必须与 DatabaseInitializer 同步
+- 前端禁止可选链 `?.`、禁止 `:key` 表达式、禁止 CSS Grid、禁止 `new Date(string)`(用 `parseDate()`)
+- 提交信息风格 `feat(unlock-gates): xxx`
+- 后端验证以 `mvn clean compile` 为准
+
+---
+
+## 任务 1:数据库迁移 + schema.sql(迁移 222-223)
+
+### 1.1 DatabaseInitializer 追加迁移
+
+在 `runMigrations()` 中**最后一个迁移块(迁移221)之后、方法闭合 `}` 之前**追加:
+
+```java
+        // 迁移222: 创建 unlock_gates 表(通关关卡定义)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS unlock_gates (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "gate_type VARCHAR(32) NOT NULL COMMENT 'SELF_CHECK/INVITE_FAMILY/CREATE_FAMILY_MEMBER/PURCHASE/MEMBERSHIP/REPORT_UPLOAD/MICRO_ACTION/STREAK_DAYS', " +
+                    "name VARCHAR(50) NOT NULL COMMENT '关卡名称', " +
+                    "description VARCHAR(200) COMMENT '关卡说明', " +
+                    "icon VARCHAR(10) COMMENT '图标emoji', " +
+                    "sort_order INT DEFAULT 0 COMMENT '排序号', " +
+                    "enabled TINYINT DEFAULT 1 COMMENT '启用:1 禁用:0', " +
+                    "is_required TINYINT DEFAULT 1 COMMENT '主线:1 增值:0', " +
+                    "is_soft TINYINT DEFAULT 0 COMMENT '硬门禁:0 软引导:1', " +
+                    "unlock_target VARCHAR(32) COMMENT 'PERSONAL_SANDBOX/FAMILY_SANDBOX/CHILD_MICRO_ACTION/...', " +
+                    "condition_params VARCHAR(500) COMMENT '条件参数JSON', " +
+                    "reward_points INT DEFAULT 0 COMMENT '通关奖励积分', " +
+                    "reward_energy INT DEFAULT 0 COMMENT '通关奖励能量', " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "INDEX idx_sort (enabled, is_required, is_soft, sort_order)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='通关解锁关卡定义'");
+            log.info("已创建unlock_gates表");
+        } catch (Exception e) {
+            log.warn("创建unlock_gates表失败: {}", e.getMessage());
+        }
+
+        // 迁移223: 创建 family_gate_progress 表(家庭通关进度)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS family_gate_progress (" +
+                    "id BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "family_id BIGINT NOT NULL COMMENT '家庭ID', " +
+                    "gate_id BIGINT NOT NULL COMMENT '关卡ID', " +
+                    "status VARCHAR(16) DEFAULT 'LOCKED' COMMENT 'LOCKED/UNLOCKED', " +
+                    "unlocked_by BIGINT COMMENT '解锁人user_id', " +
+                    "unlocked_at DATETIME COMMENT '解锁时间', " +
+                    "created_at DATETIME DEFAULT CURRENT_TIMESTAMP, " +
+                    "UNIQUE KEY uk_family_gate (family_id, gate_id)" +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家庭关卡通关进度'");
+            log.info("已创建family_gate_progress表");
+        } catch (Exception e) {
+            log.warn("创建family_gate_progress表失败: {}", e.getMessage());
+        }
+```
+
+### 1.2 schema.sql 同步
+
+在 `schema.sql` 末尾追加 `unlock_gates` 和 `family_gate_progress` 的 `CREATE TABLE IF NOT EXISTS` 语句(与迁移 222-223 一致)。
+
+### 1.3 种子数据(迁移 223 之后同批次)
+
+9 条默认关卡 INSERT(幂等:`INSERT IGNORE` 或先查后插):
+
+```java
+        // 迁移223b: 默认关卡种子数据
+        try {
+            // 主线三关
+            jdbcTemplate.execute("INSERT IGNORE INTO unlock_gates (gate_type, name, description, icon, sort_order, enabled, is_required, is_soft, unlock_target, condition_params, reward_points, reward_energy) VALUES " +
+                    "('SELF_CHECK', '五维自检', '完成15题五维自检,看见自己的能量状态', '📝', 1, 1, 1, 0, 'PERSONAL_SANDBOX', '{\"minCount\":1}', 60, 10), " +
+                    "('INVITE_FAMILY', '邀请家人', '邀请家人加入,共同创建家庭数据', '👨‍👩‍👧‍👦', 2, 1, 1, 0, 'FAMILY_SANDBOX', '{}', 80, 15), " +
+                    "('CREATE_FAMILY_MEMBER', '创建家人', '为孩子创建账号,开启家庭互动', '👶', 3, 1, 1, 0, 'CHILD_MICRO_ACTION', '{}', 50, 10), " +
+                    // 软引导
+                    "('MICRO_ACTION', '首次微行动', '完成一次微行动,养成每日小习惯', '🎯', 4, 1, 1, 1, 'MICRO_ACTION_DAILY', '{\"count\":1}', 50, 5), " +
+                    "('STREAK_DAYS', '连续打卡3天', '连续打卡3天,获得里程碑奖励', '🔥', 5, 1, 1, 1, 'BADGE_MILESTONE', '{\"days\":3}', 30, 5), " +
+                    "('SELF_CHECK', '复测对比', '完成第二次自检,看见前后变化', '📊', 6, 1, 1, 1, 'TREND_COMPARE', '{\"minCount\":2}', 60, 10), " +
+                    // 增值关卡
+                    "('PURCHASE', '首次购物', '在商城完成首次购物,解锁深度分析', '🛒', 7, 0, 0, 0, 'PREMIUM_MODULES', '{}', 150, 30), " +
+                    "('MEMBERSHIP', '加入会员', '加入家庭会员,享受专属权益', '👑', 8, 0, 0, 0, 'PREMIUM_MODULES', '{}', 200, 50), " +
+                    "('REPORT_UPLOAD', '健康报告上传', '上传健康报告,获取AI解读与洞察', '📋', 9, 0, 0, 0, 'HEALTH_INSIGHTS', '{\"minCount\":1}', 100, 20)");
+            log.info("已插入默认关卡种子数据");
+        } catch (Exception e) {
+            log.warn("插入默认关卡种子数据失败: {}", e.getMessage());
+        }
+```
+
+---
+
+## 任务 2:UnlockGateService(后端核心)
+
+### 2.1 新建 `service/UnlockGateService.java`
+
+```java
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.entity.*;
+import com.etotem.cfc.mapper.*;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+import javax.annotation.Resource;
+import java.util.*;
+
+@Service
+public class UnlockGateService {
+
+    @Resource
+    private UserMapper userMapper;
+    @Resource
+    private FamilyMemberMapper familyMemberMapper;
+    @Resource
+    private FiveDimensionSelfCheckMapper selfCheckMapper;
+    @Resource
+    private MicroActionRecordMapper microActionRecordMapper;
+    // 注入其他 Mapper:订单、会员、健康报告等
+
+    private static final Logger log = LoggerFactory.getLogger(UnlockGateService.class);
+
+    /**
+     * 判定家庭是否已通关某关卡(幂等)
+     */
+    public boolean isGateUnlocked(Long familyId, UnlockGate gate) {
+        if (gate == null || !gate.getEnabled()) return false;
+        String type = gate.getGateType();
+        // 解析 condition_params
+        // SELF_CHECK(个人级): 通过 userId 查,但此处为家庭级聚合,SELF_CHECK 单独走个人级
+        // INVITE_FAMILY: userMapper.selectCount(familyId) >= 2
+        // CREATE_FAMILY_MEMBER: familyMemberMapper.selectCount(familyId) >= 1
+        // PURCHASE: 订单表支付成功记录 >= 1
+        // MEMBERSHIP: family_memberships 有效订阅 >= 1
+        // REPORT_UPLOAD: health_reports 记录 >= minCount
+        // MICRO_ACTION: micro_action_records 聚合 >= count
+        // STREAK_DAYS: max(streakDays) >= days
+    }
+
+    /**
+     * 个人级判据:当前用户是否通过 SELF_CHECK
+     */
+    public boolean isPersonalSelfCheckPassed(Long userId) {
+        // five_dimension_self_checks 存在该 user_id 的记录
+    }
+
+    /**
+     * 获取当前锁定的硬门禁关卡(用于前端展示 CURRENT)
+     * 主线:按 sort_order 顺序,找到第一个 status=LOCKED 且 enabled=1 且 is_required=1 且 is_soft=0 的关卡
+     * 增值:无前置依赖,但仅当用户首次进入对应页面时显示
+     */
+    public UnlockGate getCurrentLockGate(Long familyId) {
+        // 查询所有 enabled=1, is_required=1, is_soft=0 的主线关卡,按 sort_order 排序
+        // 遍历,找到第一个 status=LOCKED 的返回
+    }
+
+    /**
+     * 重算家庭全部关卡进度并写 family_gate_progress
+     * 自检 SELF_CHECK 分个人级和家庭级两种判定:
+     *   - 作为主线关①时:个人级(需传入 userId)
+     *   - 作为复测对比(软引导)时:不写入进度表(软引导不锁功能)
+     */
+    public void refreshFamilyGates(Long familyId) {
+        // 1. 查所有 enabled=1 的关卡
+        // 2. 对每个关卡调用 isGateUnlocked
+        // 3. 如果 status 变化,更新 family_gate_progress
+        // 4. 如果从 LOCKED → UNLOCKED,调用 grantGateRewards
+    }
+
+    /**
+     * 个人级 refresh:用于自检提交后即时解锁个人沙盘
+     */
+    public void refreshPersonalGates(Long userId, Long familyId) {
+        // 仅刷新 SELF_CHECK 关卡(个人级)
+        // 如果通过,更新 family_gate_progress 中对应 gate_id 为 UNLOCKED
+    }
+
+    /**
+     * 发放关卡通关奖励(积分+能量)
+     */
+    public void grantGateRewards(Long familyId, Long gateId, Long unlockedByUserId) {
+        // 1. 查 unlock_gates 获取 reward_points, reward_energy
+        // 2. 调用积分服务 + 能量服务发放
+        // 3. 记录发放日志,防止重复发放
+    }
+}
+```
+
+### 2.2 判定查询(关键 SQL)
+
+| 类型 | 查询方式 |
+|------|---------|
+| SELF_CHECK(个人) | `selfCheckMapper.selectCount(wrapper.eq(userId).ge(minCount))` |
+| INVITE_FAMILY | `userMapper.selectCount(wrapper.eq(familyId)) >= 2` |
+| CREATE_FAMILY_MEMBER | `familyMemberMapper.selectCount(wrapper.eq(familyId)) >= 1` |
+| PURCHASE | 查订单表 `payment_status = 'paid'` AND `family_id = ?` |
+| MEMBERSHIP | `familyMemberships` 存在 `family_id = ? AND payment_status = 'paid' AND (endDate IS NULL OR endDate > NOW())` |
+| REPORT_UPLOAD | `healthReportMapper.selectCount(wrapper.eq(familyId)) >= minCount` |
+| MICRO_ACTION | 子查询:`familyMemberMapper.selectList(familyId) → childIds → microActionRecordMapper.selectCount(childId in ...)` |
+| STREAK_DAYS | `familyMemberMapper.selectList(familyId) → max(streakDays) >= days` |
+
+---
+
+## 任务 3:UnlockController(用户端接口)
+
+### 3.1 新建 `controller/UnlockController.java`
+
+```java
+@RestController
+@RequestMapping("/api/unlock")
+public class UnlockController {
+
+    @Resource
+    private UnlockGateService unlockGateService;
+
+    /**
+     * 查询用户关卡全景
+     * POST /api/unlock/status
+     */
+    @PostMapping("/status")
+    public Result<UnlockStatusDTO> getStatus(@RequestAttribute("userId") Long userId) {
+        // 获取用户信息 → familyId
+        // 获取个人沙盘锁状态(SELF_CHECK 个人级)
+        // 获取家庭级锁状态(主线关卡)
+        // 返回全量 gates 列表 + 锁状态标记
+    }
+}
+```
+
+### 3.2 DTO 定义
+
+```java
+public class UnlockStatusDTO {
+    private UnlockGateDTO currentLockGate;      // 当前主线硬门禁
+    private boolean personalSandboxLocked;       // 个人沙盘是否锁定
+    private boolean familySandboxLocked;         // 全家沙盘是否锁定
+    private boolean childMicroActionLocked;      // 孩子微行动是否锁定
+    private List<UnlockGateDTO> gates;           // 全量关卡
+}
+```
+
+---
+
+## 任务 4:EnergyController 改造(沙盘门禁)
+
+### 4.1 改造 `/api/energy/sandbox`
+
+```java
+@PostMapping("/sandbox")
+public Result<?> getFamilyEnergySandbox(
+        @RequestAttribute("userId") Long userId,
+        @RequestParam(defaultValue = "personal") String view) {
+
+    User user = userMapper.selectById(userId);
+    if (user == null) return Result.error("用户不存在");
+    if (user.getFamilyId() == null) return Result.noFamily("请先创建或加入家庭");
+
+    if ("personal".equals(view)) {
+        // 检查 SELF_CHECK 个人级关卡
+        if (!unlockGateService.isPersonalSelfCheckPassed(userId)) {
+            UnlockGate gate = unlockGateService.getGateByType("SELF_CHECK");
+            return Result.success(Map.of("locked", true, "gate", gate));
+        }
+        // 返回个人沙盘数据
+        EnergySandboxDTO dto = energyService.calculatePersonalEnergy(userId);
+        // 附加全家沙盘是否可用标记
+        boolean familyUnlocked = unlockGateService.isGateUnlocked(user.getFamilyId(), "INVITE_FAMILY");
+        return Result.success(Map.of("locked", false, "view", "personal",
+                "personalData", dto, "familyUnlocked", familyUnlocked));
+    } else {
+        // 检查 INVITE_FAMILY 家庭级关卡
+        if (!unlockGateService.isGateUnlocked(user.getFamilyId(), "INVITE_FAMILY")) {
+            UnlockGate gate = unlockGateService.getGateByType("INVITE_FAMILY");
+            return Result.success(Map.of("locked", true, "gate", gate));
+        }
+        // 返回全家沙盘数据
+        EnergySandboxDTO dto = energyService.calculateFamilyEnergy(user.getFamilyId());
+        return Result.success(Map.of("locked", false, "view", "family", "familyData", dto));
+    }
+}
+```
+
+> 注意:`calculatePersonalEnergy` 为新增方法,返回当前用户个人的五维数据(而非全家聚合)。后端需在 `EnergyService` 中实现该方法。
+
+---
+
+## 任务 5:触发钩子(5 处)
+
+### 5.1 自检提交成功
+
+`FiveDimensionSelfCheckController.submit()` 末尾追加:
+
+```java
+// 触发通关解锁刷新(个人级+家庭级)
+if (userId != null) {
+    User user = userMapper.selectById(userId);
+    if (user != null && user.getFamilyId() != null) {
+        unlockGateService.refreshPersonalGates(userId, user.getFamilyId());
+        unlockGateService.refreshFamilyGates(user.getFamilyId());
+    }
+}
+```
+
+### 5.2 邀请家人成功
+
+`FamilyInviteController.acceptInvite()` 末尾追加:
+
+```java
+// 触发通关解锁刷新(家庭级)
+User user = userMapper.selectById(userId);
+if (user != null && user.getFamilyId() != null) {
+    unlockGateService.refreshFamilyGates(user.getFamilyId());
+}
+```
+
+### 5.3 创建家人成功
+
+`FamilyMembersController.addMember()` 末尾追加:
+
+```java
+// 触发通关解锁刷新(家庭级)
+User user = userMapper.selectById(userId);
+if (user != null && user.getFamilyId() != null) {
+    unlockGateService.refreshFamilyGates(user.getFamilyId());
+}
+```
+
+### 5.4 支付成功回调
+
+`PaymentController` / `ProductOrderController` 支付成功回调中追加 `refreshFamilyGates(familyId)`。
+
+### 5.5 会员订阅 / 报告上传
+
+`MembershipController` 订阅成功回调 + `HealthReportController` 报告上传/确认成功回调中追加 `refreshFamilyGates(familyId)`。
+
+---
+
+## 任务 6:AdminUnlockGateController(管理端接口)
+
+### 6.1 新建 `controller/admin/AdminUnlockGateController.java`
+
+```java
+@RestController
+@RequestMapping("/api/admin/unlock-gates")
+public class AdminUnlockGateController {
+
+    @Resource
+    private UnlockGateService unlockGateService;
+
+    @PostMapping("/list")        // 关卡列表(分页)
+    @PostMapping("/save")        // 新增/编辑
+    @PostMapping("/delete")      // 删除(级联清理 family_gate_progress)
+    @PostMapping("/sort")        // 批量排序
+    @PostMapping("/toggle")      // 启停
+    @PostMapping("/reset-family") // 重置某家庭进度
+}
+```
+
+---
+
+## 任务 7:WuxingSandbox 组件改造(小程序前端)
+
+### 7.1 改造 `components/wuxing-sandbox.vue`
+
+| 新增 props | 类型 | 默认值 | 说明 |
+|-----------|------|--------|------|
+| `viewMode` | String | `'personal'` | `personal` / `family` |
+| `familyUnlocked` | Boolean | `false` | 是否显示切换开关 |
+| `personalData` | Object | `null` | 个人沙盘数据 |
+| `familyData` | Object | `null` | 全家沙盘数据 |
+
+**模板改造:**
+
+```html
+<!-- 切换开关 -->
+<view class="sandbox-toggle" v-if="familyUnlocked">
+  <view :class="['toggle-item', viewMode === 'personal' ? 'active' : '']"
+        @click="switchView('personal')">👤 个人</view>
+  <view :class="['toggle-item', viewMode === 'family' ? 'active' : '']"
+        @click="switchView('family')">👨‍👩‍👧‍👦 全家</view>
+</view>
+```
+
+---
+
+## 任务 8:gate-lock 组件(小程序前端)
+
+### 8.1 新建 `components/gate-lock.vue`
+
+```html
+<template>
+  <view class="gate-lock">
+    <view class="lock-icon">🔒</view>
+    <text class="gate-name">{{ gate.name }}</text>
+    <text class="gate-desc">{{ gate.description }}</text>
+    <button class="gate-btn" @click="goToAction">
+      {{ gateBtnText }}
+    </button>
+  </view>
+</template>
+```
+
+**gate_type → 跳转映射:**
+
+| gate_type | 按钮文案 | 跳转 |
+|-----------|---------|------|
+| SELF_CHECK | 完成五维自检,解锁个人沙盘 | `pages/family/self-check.vue` |
+| INVITE_FAMILY | 邀请家人加入,解锁全家沙盘 | 家庭邀请页 |
+| CREATE_FAMILY_MEMBER | 创建孩子账号,解锁微行动 | 创建家庭成员页 |
+| PURCHASE | 去商城逛逛 | 商城首页 |
+| MEMBERSHIP | 查看会员方案 | 会员页 |
+| REPORT_UPLOAD | 上传健康报告 | 报告上传页 |
+
+---
+
+## 任务 9:gate-guide-bar 组件(小程序前端)
+
+### 9.1 新建 `components/gate-guide-bar.vue`
+
+- 在 `parent-index.vue` 顶部渲染
+- 从 `/api/unlock/status` 取 gates 列表
+- 显示 "🔓 解锁进度 X/3 · 完成【当前关卡名】→"
+- 全部解锁后隐藏
+- 点击跳转关卡中心页
+
+---
+
+## 任务 10:关卡中心页(小程序前端)
+
+### 10.1 新建 `pages/profile-extra/gates.vue`
+
+- 三区展示:主线关卡 / 软引导 / 增值关卡
+- 每关显示:图标 + 名称 + 说明 + 状态(🔒/✅/CURRENT) + 奖励
+- 主线关卡按顺序展示,增值关卡无前置独立展示
+- 需在 `pages.json` 注册(分包 profile-extra)
+
+### 10.2 `pages.json` 注册
+
+```json
+{
+  "root": "pages/profile-extra",
+  "pages": [
+    ...
+    { "path": "gates", "style": { "navigationBarTitleText": "关卡中心" } }
+  ]
+}
+```
+
+---
+
+## 任务 11:沙盘调用点改造(8+ 页面)
+
+### 11.1 改造列表
+
+每个页面调用 `/api/energy/sandbox?view=personal`,根据返回的 `locked` 分支渲染 gate-lock 或沙盘:
+
+| 页面 | 改造点 |
+|------|--------|
+| `pages/home-pages/parent-index.vue` | 原 `v-if="sandboxData"` → 调用 sandbox API,locked 时渲染 gate-lock |
+| `pages/home-pages/child-index.vue` | 同上 |
+| `pages/home-pages/member-home-detail.vue` | 同上 |
+| `pages/member-detail/member-detail.vue` | 同上 |
+| `pages/discover/index.vue` | 同上 |
+| `pages/body-detail/index.vue` | 同上 |
+| `pages/mind-detail/index.vue` | 同上 |
+| `pages/wisdom-detail/index.vue` | 同上 |
+
+### 11.2 统一请求封装建议
+
+在 `utils/api.js` 新增封装:
+
+```javascript
+// 获取沙盘数据(含门禁判断)
+export function getSandbox(view) {
+  return request({
+    url: '/api/energy/sandbox',
+    method: 'post',
+    data: { view: view || 'personal' }
+  })
+}
+```
+
+---
+
+## 任务 12:管理端 gates.vue(cfc-web)
+
+### 12.1 新建 `views/admin/gates.vue`
+
+- 路由:`/admin/gates`(菜单"系统管理"下"关卡配置")
+- 表格列:排序 | 图标 | 名称 | 类型 | 归属(主线/增值) | 形态(硬门禁/软引导) | 解锁目标 | 条件 | 奖励 | 状态 | 操作
+- 操作:编辑(弹窗表单)/ 启停开关 / 删除 / 排序
+- 表单字段:类型下拉(8种)、条件参数动态渲染、奖励输入
+- 额外区块:家庭进度管理(输入 family_id 查看/重置)
+
+### 12.2 路由注册
+
+在 `src/router/index.js` 中添加:
+
+```javascript
+{
+  path: '/admin/gates',
+  component: () => import('@/views/admin/gates.vue'),
+  meta: { title: '关卡配置', roles: ['admin'] }
+}
+```
+
+---
+
+## 任务 13:EnergyService 新增个人沙盘计算
+
+### 13.1 新增 `calculatePersonalEnergy(Long userId)`
+
+在 `EnergyService` 中新增方法,返回当前用户个人的五维能量数据:
+
+```java
+public EnergySandboxDTO calculatePersonalEnergy(Long userId) {
+    User user = userMapper.selectById(userId);
+    if (user == null) return null;
+    // 计算个人的五维能量(与 calculateFamilyEnergy 类似,但只针对当前用户)
+    // 取 energy_balance 中该用户的各维度余额
+    // 组装 EnergySandboxDTO(但只有一个人)
+}
+```
+
+---
+
+## 实施顺序
+
+```
+任务 1(迁移+建表)→ 任务 2(UnlockGateService)→ 任务 3(UnlockController)
+→ 任务 4(sandbox 改造)→ 任务 5(钩子)
+→ 任务 6(管理端接口)→ 任务 12(管理端页面)
+→ 任务 13(个人沙盘计算)
+→ 任务 7(WuxingSandbox 改造)→ 任务 8-10(前端组件)
+→ 任务 11(页面调用点改造)
+```
+
+**后端依赖顺序**:迁移 → Service → Controller → 钩子
+**前端依赖顺序**:组件 → 页面改造
+**后端与前端可并行**:后端 1-6 + 13 → 前端 7-11
+
+---
+
+## 工作量估算
+
+| 任务 | 内容 | 估算 |
+|------|------|------|
+| 1. 迁移+建表 | 2 张表 + 种子数据 + schema.sql 同步 | 0.5 天 |
+| 2. UnlockGateService | 8 种判定 + 奖励发放 + 进度刷新 | 1.5 天 |
+| 3. UnlockController | status 接口 + DTO | 0.5 天 |
+| 4. EnergyController 改造 | view 参数 + 门禁拦截 | 0.5 天 |
+| 5. 触发钩子 | 5 处回调接入 | 1 天 |
+| 6. AdminUnlockGateController | 6 个管理接口 | 0.5 天 |
+| 7. WuxingSandbox 改造 | 个人/全家切换 + toggle | 1 天 |
+| 8. gate-lock 组件 | 6 种类型映射 + 跳转 | 0.5 天 |
+| 9. gate-guide-bar 组件 | 进度条组件 | 0.5 天 |
+| 10. 关卡中心页 | gates.vue + pages.json | 1 天 |
+| 11. 沙盘调用点改造 | 8+ 页面调用改造 | 1 天 |
+| 12. 管理端 gates.vue | 配置页 + 路由 | 1 天 |
+| 13. 个人沙盘计算 | calculatePersonalEnergy | 0.5 天 |
+| 测试 | 单测/集成/E2E | 1 天 |
+| **合计** | | **~10 天** |
+
+---
+
+## 里程碑验收
+
+| 里程碑 | 验证方式 |
+|--------|---------|
+| 迁移完成 | `mvn clean compile` 通过 |
+| 后端 API 可用 | Postman 验证 `/api/unlock/status`、`/api/energy/sandbox?view=personal|family` 的 locked 分支 |
+| 前端全链路 | 新用户动线:注册→个人沙盘锁定→自检→解锁个人→邀请家人→解锁全家→创建孩子→解锁微行动 |
+| 增值关卡 | 购物/会员/报告上传后,对应关卡解锁+奖励到账 |
+| 管理端 | 新增/编辑/排序/启停关卡实时生效,家庭进度查看/重置可用 |
+
+---
+
+## 后续修复与增强(2026-08-13)
+
+### A. 通关奖励真实发放(修复)
+
+原实现中 `grantGateRewards` 仅做占位/日志,未实际发放奖励。本次修复为真实到账:
+
+**后端** `UnlockGateService.java`
+- `grantGateRewards(familyId, triggerUserId, gate)`:
+  - CF值:`platformPointsService.earn(triggerUserId, gate.getRewardPoints(), "unlock_gate", gate.getId(), "通关奖励: " + gate.getName())`
+  - 能量:`energyService.awardEnergy(memberId, "unlock_gate", gate.getId(), gate.getRewardEnergy(), ...)`,memberId 按 SELF_CHECK(个人)=triggerUserId / 其他(家庭)=该家庭成员的 `FamilyMember.id` 映射
+  - 奖励失败回滚:捕获异常写入 `family_gate_progress` 的 unlock 状态并记录失败日志,防止状态与奖励不一致
+- `refreshFamilyGates(Long familyId)` 增加重载 `refreshFamilyGates(Long familyId, Long triggerUserId)`,由调用方传入触发用户
+- 4 处钩子传入触发用户:`FiveDimensionSelfCheckController`(自检提交)、`FamilyInviteController`(接受邀请)、`FamilyMembersController`(创建成员)、`ProductOrderService`(支付回调)
+
+**前端文案统一**("积分" → "CF值")
+- 小程序 `components/gate-lock.vue`、`pages/profile-extra/gates.vue`
+- 管理端 `cfc-web/src/views/admin/gates.vue`
+
+### B. 管理端关卡列表分页(增强)
+
+**后端** `AdminUnlockGateController.list()`
+- 接收 body `{ page, size }`(默认 `page=1, size=10`),改用 `unlockGateMapper.selectPage` 返回 `IPage`(`records` + `total`)
+
+**前端**
+- `cfc-web/src/api/admin.js`:`getUnlockGateList(params)` 透传分页参数
+- `cfc-web/src/views/admin/gates.vue`:表格下方 `el-pagination`(总数/页码/每页条数 10/20/50),`handlePageChange` / `handleSizeChange` 重载;删除当前页最后一条后自动回退上一页避免空页
+
+**验证**:`mvn clean compile` 通过 + 管理端 script 块 `node --check` 通过