2026-08-10-unlock-gates-implementation.md 27 KB

实施计划:通关解锁功能(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)之后、方法闭合 } 之前追加:

        // 迁移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_gatesfamily_gate_progressCREATE TABLE IF NOT EXISTS 语句(与迁移 222-223 一致)。

1.3 种子数据(迁移 223 之后同批次)

9 条默认关卡 INSERT(幂等:INSERT IGNORE 或先查后插):

        // 迁移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

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

@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 定义

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

@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() 末尾追加:

// 触发通关解锁刷新(个人级+家庭级)
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() 末尾追加:

// 触发通关解锁刷新(家庭级)
User user = userMapper.selectById(userId);
if (user != null && user.getFamilyId() != null) {
    unlockGateService.refreshFamilyGates(user.getFamilyId());
}

5.3 创建家人成功

FamilyMembersController.addMember() 末尾追加:

// 触发通关解锁刷新(家庭级)
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

@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 全家沙盘数据

模板改造:

<!-- 切换开关 -->
<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

<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 注册

{
  "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 新增封装:

// 获取沙盘数据(含门禁判断)
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 中添加:

{
  path: '/admin/gates',
  component: () => import('@/views/admin/gates.vue'),
  meta: { title: '关卡配置', roles: ['admin'] }
}

任务 13:EnergyService 新增个人沙盘计算

13.1 新增 calculatePersonalEnergy(Long userId)

EnergyService 中新增方法,返回当前用户个人的五维能量数据:

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
前端全链路 新用户动线:注册→个人沙盘锁定→自检→解锁个人→邀请家人→解锁全家→创建孩子→解锁微行动
增值关卡 购物/会员/报告上传后,对应关卡解锁+奖励到账
管理端 新增/编辑/排序/启停关卡实时生效,家庭进度查看/重置可用

后续修复与增强(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.vuepages/profile-extra/gates.vue
  • 管理端 cfc-web/src/views/admin/gates.vue

B. 管理端关卡列表分页(增强)

后端 AdminUnlockGateController.list()

  • 接收 body { page, size }(默认 page=1, size=10),改用 unlockGateMapper.selectPage 返回 IPagerecords + total

前端

  • cfc-web/src/api/admin.jsgetUnlockGateList(params) 透传分页参数
  • cfc-web/src/views/admin/gates.vue:表格下方 el-pagination(总数/页码/每页条数 10/20/50),handlePageChange / handleSizeChange 重载;删除当前页最后一条后自动回退上一页避免空页

验证mvn clean compile 通过 + 管理端 script 块 node --check 通过