# 实施计划:通关解锁功能(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`;实体 `@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 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 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 👤 个人 👨‍👩‍👧‍👦 全家 ``` --- ## 任务 8:gate-lock 组件(小程序前端) ### 8.1 新建 `components/gate-lock.vue` ```html ``` **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` 通过