docs/superpowers/specs/2026-08-10-unlock-gates-design.md v2.1PlatformPointsService.earn 发 CF值 + EnergyService.awardEnergy 发能量,替代原积分占位逻辑);refreshFamilyGates 增加 triggerUserId 重载,4 处钩子传入触发用户;前端文案"积分"统一改"CF值"(小程序 gate-lock/gates + 管理端 gates)selectPage + el-pagination)mvn clean compile(在 cfc-backend 下);前端语法校验(node --check 提取 script 块)本计划实现"通关解锁"功能,将能量沙盘拆分为个人/全家两层,通过三关主线硬门禁(自检→邀请家人→创建家人)递进解锁,辅以软引导(微行动/打卡/复测)和增值关卡(购物/会员/上传报告),每关均有积分+能量奖励。
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// 迁移N: ...?.、禁止 :key 表达式、禁止 CSS Grid、禁止 new Date(string)(用 parseDate())feat(unlock-gates): xxxmvn clean compile 为准在 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());
}
在 schema.sql 末尾追加 unlock_gates 和 family_gate_progress 的 CREATE TABLE IF NOT EXISTS 语句(与迁移 222-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());
}
service/UnlockGateService.javapackage 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. 记录发放日志,防止重复发放
}
}
| 类型 | 查询方式 |
|---|---|
| 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 |
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 列表 + 锁状态标记
}
}
public class UnlockStatusDTO {
private UnlockGateDTO currentLockGate; // 当前主线硬门禁
private boolean personalSandboxLocked; // 个人沙盘是否锁定
private boolean familySandboxLocked; // 全家沙盘是否锁定
private boolean childMicroActionLocked; // 孩子微行动是否锁定
private List<UnlockGateDTO> gates; // 全量关卡
}
/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中实现该方法。
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());
}
}
FamilyInviteController.acceptInvite() 末尾追加:
// 触发通关解锁刷新(家庭级)
User user = userMapper.selectById(userId);
if (user != null && user.getFamilyId() != null) {
unlockGateService.refreshFamilyGates(user.getFamilyId());
}
FamilyMembersController.addMember() 末尾追加:
// 触发通关解锁刷新(家庭级)
User user = userMapper.selectById(userId);
if (user != null && user.getFamilyId() != null) {
unlockGateService.refreshFamilyGates(user.getFamilyId());
}
PaymentController / ProductOrderController 支付成功回调中追加 refreshFamilyGates(familyId)。
MembershipController 订阅成功回调 + HealthReportController 报告上传/确认成功回调中追加 refreshFamilyGates(familyId)。
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") // 重置某家庭进度
}
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>
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 | 上传健康报告 | 报告上传页 |
components/gate-guide-bar.vueparent-index.vue 顶部渲染/api/unlock/status 取 gates 列表pages/profile-extra/gates.vuepages.json 注册(分包 profile-extra)pages.json 注册{
"root": "pages/profile-extra",
"pages": [
...
{ "path": "gates", "style": { "navigationBarTitleText": "关卡中心" } }
]
}
每个页面调用 /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 |
同上 |
在 utils/api.js 新增封装:
// 获取沙盘数据(含门禁判断)
export function getSandbox(view) {
return request({
url: '/api/energy/sandbox',
method: 'post',
data: { view: view || 'personal' }
})
}
views/admin/gates.vue/admin/gates(菜单"系统管理"下"关卡配置")在 src/router/index.js 中添加:
{
path: '/admin/gates',
component: () => import('@/views/admin/gates.vue'),
meta: { title: '关卡配置', roles: ['admin'] }
}
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 |
| 前端全链路 | 新用户动线:注册→个人沙盘锁定→自检→解锁个人→邀请家人→解锁全家→创建孩子→解锁微行动 |
| 增值关卡 | 购物/会员/报告上传后,对应关卡解锁+奖励到账 |
| 管理端 | 新增/编辑/排序/启停关卡实时生效,家庭进度查看/重置可用 |
原实现中 grantGateRewards 仅做占位/日志,未实际发放奖励。本次修复为真实到账:
后端 UnlockGateService.java
grantGateRewards(familyId, triggerUserId, gate):
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),由调用方传入触发用户FiveDimensionSelfCheckController(自检提交)、FamilyInviteController(接受邀请)、FamilyMembersController(创建成员)、ProductOrderService(支付回调)前端文案统一("积分" → "CF值")
components/gate-lock.vue、pages/profile-extra/gates.vuecfc-web/src/views/admin/gates.vue后端 AdminUnlockGateController.list()
{ 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 通过