Просмотр исходного кода

feat(unlock-gates): 通关解锁后端全链路 - 2表迁移+UnlockGateService(8判定)+/api/unlock/status+sandbox个人/全家门禁+5处触发钩子+管理端接口

E2E Test Bot 2 месяцев назад
Родитель
Сommit
fa66edfbf9
17 измененных файлов с 951 добавлено и 7 удалено
  1. 55 0
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  2. 71 7
      cfc-backend/src/main/java/com/etotem/cfc/controller/EnergyController.java
  3. 42 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/UnlockController.java
  4. 114 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminUnlockGateController.java
  5. 9 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyInviteController.java
  6. 14 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyMembersController.java
  7. 17 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java
  8. 23 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/UnlockGateDTO.java
  9. 31 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/UnlockStatusDTO.java
  10. 37 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyGateProgress.java
  11. 60 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/UnlockGate.java
  12. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyGateProgressMapper.java
  13. 7 0
      cfc-backend/src/main/java/com/etotem/cfc/mapper/UnlockGateMapper.java
  14. 33 0
      cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java
  15. 12 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java
  16. 387 0
      cfc-backend/src/main/java/com/etotem/cfc/service/UnlockGateService.java
  17. 32 0
      cfc-backend/src/main/resources/schema.sql

+ 55 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -8765,5 +8765,60 @@ private void runMigration100() {
 
 
 		// 迁移221: nutrition_product 添加 local_product_id 字段(关联本平台商品)
 		// 迁移221: nutrition_product 添加 local_product_id 字段(关联本平台商品)
 		ensureColumn("nutrition_product", "local_product_id", "BIGINT DEFAULT NULL COMMENT '本平台商品ID,关联products.id'");
 		ensureColumn("nutrition_product", "local_product_id", "BIGINT DEFAULT NULL COMMENT '本平台商品ID,关联products.id'");
+
+		// 迁移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表");
+
+			// 默认关卡种子数据(幂等:INSERT IGNORE)
+			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("创建family_gate_progress表或插入种子数据失败: " + e.getMessage());
+		}
 	}
 	}
 }
 }

+ 71 - 7
cfc-backend/src/main/java/com/etotem/cfc/controller/EnergyController.java

@@ -4,11 +4,14 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.EnergySandboxDTO;
 import com.etotem.cfc.dto.EnergySandboxDTO;
 import com.etotem.cfc.dto.MemberEnergyDTO;
 import com.etotem.cfc.dto.MemberEnergyDTO;
+import com.etotem.cfc.dto.UnlockGateDTO;
 import com.etotem.cfc.entity.EnergyLog;
 import com.etotem.cfc.entity.EnergyLog;
+import com.etotem.cfc.entity.UnlockGate;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.entity.User;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.EnergyService;
 import com.etotem.cfc.service.EnergyService;
 import com.etotem.cfc.service.FiveDimensionScoreService;
 import com.etotem.cfc.service.FiveDimensionScoreService;
+import com.etotem.cfc.service.UnlockGateService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
@@ -30,14 +33,19 @@ public class EnergyController {
     @Resource
     @Resource
     private UserMapper userMapper;
     private UserMapper userMapper;
 
 
+    @Resource
+    private UnlockGateService unlockGateService;
+
     /**
     /**
-     * 获取家庭能量沙盘数据
-     * 返回家庭五维聚合值 + 每个家庭成员的个人五维数据
+     * 获取能量沙盘数据(个人/全家两层,受通关门禁控制)
+     * view=personal: 当前登录用户个人五维(需通过 SELF_CHECK 关卡)
+     * view=family:   家庭所有成员聚合五维(需通过 INVITE_FAMILY 关卡)
      */
      */
-    @Operation(summary = "获取家庭能量沙盘")
+    @Operation(summary = "获取能量沙盘(支持个人/全家,含门禁)")
     @PostMapping("/sandbox")
     @PostMapping("/sandbox")
-    public Result<EnergySandboxDTO> getFamilyEnergySandbox(
-            @RequestAttribute("userId") Long userId) {
+    public Result<Map<String, Object>> getFamilyEnergySandbox(
+            @RequestAttribute("userId") Long userId,
+            @RequestBody(required = false) Map<String, Object> body) {
 
 
         User user = userMapper.selectById(userId);
         User user = userMapper.selectById(userId);
         if (user == null) {
         if (user == null) {
@@ -47,8 +55,64 @@ public class EnergyController {
             return Result.noFamily("请先创建或加入家庭");
             return Result.noFamily("请先创建或加入家庭");
         }
         }
 
 
-        EnergySandboxDTO dto = energyService.calculateFamilyEnergy(user.getFamilyId());
-        return Result.success(dto);
+        String view = "personal";
+        if (body != null && body.get("view") != null) {
+            view = String.valueOf(body.get("view"));
+        }
+
+        Map<String, Object> result = new HashMap<>();
+
+        if ("family".equals(view)) {
+            // 全家沙盘:需通过 INVITE_FAMILY 关卡
+            UnlockGate inviteGate = unlockGateService.getGateByType("INVITE_FAMILY");
+            boolean unlocked = inviteGate == null
+                    || unlockGateService.isGateUnlocked(user.getFamilyId(), inviteGate);
+            if (!unlocked && inviteGate != null) {
+                result.put("locked", true);
+                result.put("view", "family");
+                result.put("gate", gateToMap(inviteGate));
+                return Result.success(result);
+            }
+            EnergySandboxDTO dto = energyService.calculateFamilyEnergy(user.getFamilyId());
+            result.put("locked", false);
+            result.put("view", "family");
+            result.put("familyData", dto);
+            return Result.success(result);
+        }
+
+        // 个人沙盘(默认):需通过 SELF_CHECK 关卡
+        boolean personalPassed = unlockGateService.isPersonalSelfCheckPassed(userId);
+        if (!personalPassed) {
+            UnlockGate gate = unlockGateService.getGateByType("SELF_CHECK");
+            result.put("locked", true);
+            result.put("view", "personal");
+            result.put("gate", gate != null ? gateToMap(gate) : null);
+            return Result.success(result);
+        }
+
+        EnergySandboxDTO dto = energyService.calculatePersonalEnergy(userId);
+        UnlockGate inviteGate = unlockGateService.getGateByType("INVITE_FAMILY");
+        boolean familyUnlocked = inviteGate == null
+                || unlockGateService.isGateUnlocked(user.getFamilyId(), inviteGate);
+        result.put("locked", false);
+        result.put("view", "personal");
+        result.put("personalData", dto);
+        result.put("familyUnlocked", familyUnlocked);
+        if (!familyUnlocked && inviteGate != null) {
+            result.put("familyGate", gateToMap(inviteGate));
+        }
+        return Result.success(result);
+    }
+
+    private Map<String, Object> gateToMap(UnlockGate gate) {
+        if (gate == null) return null;
+        Map<String, Object> m = new HashMap<>();
+        m.put("id", gate.getId());
+        m.put("gateType", gate.getGateType());
+        m.put("name", gate.getName());
+        m.put("description", gate.getDescription());
+        m.put("icon", gate.getIcon());
+        return m;
     }
     }
 
 
     /**
     /**

+ 42 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/UnlockController.java

@@ -0,0 +1,42 @@
+package com.etotem.cfc.controller;
+
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.dto.UnlockStatusDTO;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
+import com.etotem.cfc.service.UnlockGateService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestAttribute;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.annotation.Resource;
+
+@Tag(name = "通关解锁", description = "关卡状态查询")
+@RestController
+@RequestMapping("/api/unlock")
+public class UnlockController {
+
+    @Resource
+    private UnlockGateService unlockGateService;
+
+    @Resource
+    private UserMapper userMapper;
+
+    /**
+     * 查询用户关卡全景
+     */
+    @Operation(summary = "查询用户关卡全景")
+    @PostMapping("/status")
+    public Result<UnlockStatusDTO> getStatus(@RequestAttribute("userId") Long userId) {
+        if (userId == null) {
+            return Result.error("请先登录");
+        }
+        User user = userMapper.selectById(userId);
+        Long familyId = user != null ? user.getFamilyId() : null;
+        UnlockStatusDTO dto = unlockGateService.getUserUnlockStatus(userId, familyId);
+        return Result.success(dto);
+    }
+}

+ 114 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminUnlockGateController.java

@@ -0,0 +1,114 @@
+package com.etotem.cfc.controller.admin;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.Result;
+import com.etotem.cfc.entity.FamilyGateProgress;
+import com.etotem.cfc.entity.UnlockGate;
+import com.etotem.cfc.mapper.FamilyGateProgressMapper;
+import com.etotem.cfc.mapper.UnlockGateMapper;
+import io.swagger.v3.oas.annotations.Operation;
+import org.springframework.web.bind.annotation.*;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 管理端:通关解锁关卡配置
+ */
+@RestController
+@RequestMapping("/api/admin/unlock-gates")
+public class AdminUnlockGateController {
+
+    @Resource
+    private UnlockGateMapper unlockGateMapper;
+
+    @Resource
+    private FamilyGateProgressMapper familyGateProgressMapper;
+
+    @Operation(summary = "关卡列表")
+    @PostMapping("/list")
+    public Result<List<UnlockGate>> list() {
+        List<UnlockGate> gates = unlockGateMapper.selectList(
+                new LambdaQueryWrapper<UnlockGate>().orderByAsc(UnlockGate::getSortOrder));
+        return Result.success(gates);
+    }
+
+    @Operation(summary = "新增/编辑关卡")
+    @PostMapping("/save")
+    public Result<Long> save(@RequestBody UnlockGate gate) {
+        if (gate.getId() != null) {
+            gate.setUpdatedAt(new Date());
+            unlockGateMapper.updateById(gate);
+            return Result.success(gate.getId());
+        }
+        gate.setCreatedAt(new Date());
+        gate.setUpdatedAt(new Date());
+        unlockGateMapper.insert(gate);
+        return Result.success(gate.getId());
+    }
+
+    @Operation(summary = "删除关卡")
+    @Transactional
+    @PostMapping("/delete")
+    public Result<Boolean> delete(@RequestBody Map<String, Long> body) {
+        Long id = body.get("id");
+        if (id == null) return Result.error("id不能为空");
+        // 级联清理 family_gate_progress
+        familyGateProgressMapper.delete(
+                new LambdaQueryWrapper<FamilyGateProgress>().eq(FamilyGateProgress::getGateId, id));
+        unlockGateMapper.deleteById(id);
+        return Result.success(true);
+    }
+
+    @Operation(summary = "批量排序")
+    @PostMapping("/sort")
+    public Result<Boolean> sort(@RequestBody List<Map<String, Object>> sortList) {
+        for (Map<String, Object> item : sortList) {
+            Long id = Long.valueOf(item.get("id").toString());
+            Integer sortOrder = Integer.valueOf(item.get("sortOrder").toString());
+            UnlockGate gate = unlockGateMapper.selectById(id);
+            if (gate != null) {
+                gate.setSortOrder(sortOrder);
+                gate.setUpdatedAt(new Date());
+                unlockGateMapper.updateById(gate);
+            }
+        }
+        return Result.success(true);
+    }
+
+    @Operation(summary = "启停关卡")
+    @PostMapping("/toggle")
+    public Result<Boolean> toggle(@RequestBody Map<String, Object> body) {
+        Long id = Long.valueOf(body.get("id").toString());
+        Integer enabled = Integer.valueOf(body.get("enabled").toString());
+        UnlockGate gate = unlockGateMapper.selectById(id);
+        if (gate == null) return Result.error("关卡不存在");
+        gate.setEnabled(enabled);
+        gate.setUpdatedAt(new Date());
+        unlockGateMapper.updateById(gate);
+        return Result.success(true);
+    }
+
+    @Operation(summary = "重置某家庭全部关卡进度")
+    @PostMapping("/reset-family")
+    @Transactional
+    public Result<Boolean> resetFamily(@RequestBody Map<String, Object> body) {
+        Long familyId = Long.valueOf(body.get("familyId").toString());
+        // 删除该家庭所有进度记录,使其重新开始
+        familyGateProgressMapper.delete(
+                new LambdaQueryWrapper<FamilyGateProgress>().eq(FamilyGateProgress::getFamilyId, familyId));
+        return Result.success(true);
+    }
+
+    @Operation(summary = "查看某家庭关卡进度")
+    @PostMapping("/family-progress")
+    public Result<List<FamilyGateProgress>> getFamilyProgress(@RequestBody Map<String, Object> body) {
+        Long familyId = Long.valueOf(body.get("familyId").toString());
+        List<FamilyGateProgress> list = familyGateProgressMapper.selectList(
+                new LambdaQueryWrapper<FamilyGateProgress>().eq(FamilyGateProgress::getFamilyId, familyId));
+        return Result.success(list);
+    }
+}

+ 9 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyInviteController.java

@@ -14,6 +14,7 @@ import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.FamilyInvitationService;
 import com.etotem.cfc.service.FamilyInvitationService;
 import com.etotem.cfc.service.FamilyJoinRequestService;
 import com.etotem.cfc.service.FamilyJoinRequestService;
 import com.etotem.cfc.service.QrCodeService;
 import com.etotem.cfc.service.QrCodeService;
+import com.etotem.cfc.service.UnlockGateService;
 import com.etotem.cfc.service.WechatService;
 import com.etotem.cfc.service.WechatService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import io.swagger.v3.oas.annotations.tags.Tag;
@@ -55,6 +56,9 @@ public class FamilyInviteController {
     @Resource
     @Resource
     private WechatService wechatService;
     private WechatService wechatService;
 
 
+    @Resource
+    private UnlockGateService unlockGateService;
+
     /**
     /**
      * 生成家庭邀请二维码
      * 生成家庭邀请二维码
      * 返回: base64 QR 码图片 + 邀请码 + 家庭名称
      * 返回: base64 QR 码图片 + 邀请码 + 家庭名称
@@ -314,6 +318,11 @@ public class FamilyInviteController {
 
 
         try {
         try {
             familyInvitationService.acceptInvitation(userId, dto.getToken(), dto.getFamilyRole(), dto.getMergeFamily());
             familyInvitationService.acceptInvitation(userId, dto.getToken(), dto.getFamilyRole(), dto.getMergeFamily());
+            // 触发通关解锁刷新:邀请家人成功
+            User user = userMapper.selectById(userId);
+            if (user != null && user.getFamilyId() != null) {
+                unlockGateService.refreshFamilyGates(user.getFamilyId());
+            }
             return Result.success("加入家庭成功");
             return Result.success("加入家庭成功");
         } catch (Exception e) {
         } catch (Exception e) {
             return Result.error(e.getMessage());
             return Result.error(e.getMessage());

+ 14 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FamilyMembersController.java

@@ -5,7 +5,10 @@ import com.etotem.cfc.dto.AddFamilyMemberDTO;
 import com.etotem.cfc.dto.FamilyMemberVO;
 import com.etotem.cfc.dto.FamilyMemberVO;
 import com.etotem.cfc.dto.SwitchMemberVO;
 import com.etotem.cfc.dto.SwitchMemberVO;
 import com.etotem.cfc.entity.FamilyMemberLog;
 import com.etotem.cfc.entity.FamilyMemberLog;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.FamilyMemberService;
 import com.etotem.cfc.service.FamilyMemberService;
+import com.etotem.cfc.service.UnlockGateService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
@@ -22,6 +25,12 @@ public class FamilyMembersController {
     @Resource
     @Resource
     private FamilyMemberService familyMemberService;
     private FamilyMemberService familyMemberService;
 
 
+    @Resource
+    private UnlockGateService unlockGateService;
+
+    @Resource
+    private UserMapper userMapper;
+
 @Operation(summary = "添加家庭成员")
 @Operation(summary = "添加家庭成员")
 @PostMapping("/add")
 @PostMapping("/add")
 public Result<FamilyMemberVO> addMember(@RequestBody AddFamilyMemberDTO dto,
 public Result<FamilyMemberVO> addMember(@RequestBody AddFamilyMemberDTO dto,
@@ -31,6 +40,11 @@ public Result<FamilyMemberVO> addMember(@RequestBody AddFamilyMemberDTO dto,
  }
  }
  try {
  try {
    FamilyMemberVO vo = familyMemberService.addMember(userId, dto);
    FamilyMemberVO vo = familyMemberService.addMember(userId, dto);
+   // 触发通关解锁刷新:创建家人成功
+   User user = userMapper.selectById(userId);
+   if (user != null && user.getFamilyId() != null) {
+       unlockGateService.refreshFamilyGates(user.getFamilyId());
+   }
    return Result.success(vo);
    return Result.success(vo);
  } catch (IllegalArgumentException e) {
  } catch (IllegalArgumentException e) {
    return Result.error(e.getMessage());
    return Result.error(e.getMessage());

+ 17 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/family/FiveDimensionSelfCheckController.java

@@ -4,7 +4,10 @@ import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.SelfCheckQuestionVO;
 import com.etotem.cfc.dto.SelfCheckQuestionVO;
 import com.etotem.cfc.dto.SelfCheckResultVO;
 import com.etotem.cfc.dto.SelfCheckResultVO;
 import com.etotem.cfc.dto.SubmitSelfCheckDTO;
 import com.etotem.cfc.dto.SubmitSelfCheckDTO;
+import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.FiveDimensionSelfCheckService;
 import com.etotem.cfc.service.FiveDimensionSelfCheckService;
+import com.etotem.cfc.service.UnlockGateService;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.PostMapping;
@@ -28,6 +31,12 @@ public class FiveDimensionSelfCheckController {
     @Resource
     @Resource
     private FiveDimensionSelfCheckService selfCheckService;
     private FiveDimensionSelfCheckService selfCheckService;
 
 
+    @Resource
+    private UnlockGateService unlockGateService;
+
+    @Resource
+    private UserMapper userMapper;
+
     @Operation(summary = "获取自检问卷题目(15题静态题库)")
     @Operation(summary = "获取自检问卷题目(15题静态题库)")
     @PostMapping("/questions")
     @PostMapping("/questions")
     public Result<List<SelfCheckQuestionVO>> getQuestions(@RequestAttribute("userId") Long userId) {
     public Result<List<SelfCheckQuestionVO>> getQuestions(@RequestAttribute("userId") Long userId) {
@@ -46,6 +55,14 @@ public class FiveDimensionSelfCheckController {
         }
         }
         try {
         try {
             SelfCheckResultVO vo = selfCheckService.submitSelfCheck(dto, userId);
             SelfCheckResultVO vo = selfCheckService.submitSelfCheck(dto, userId);
+            // 触发通关解锁刷新:自检通过 → 解锁个人沙盘
+            User user = userMapper.selectById(userId);
+            if (user != null) {
+                unlockGateService.refreshPersonalGates(userId, user.getFamilyId());
+                if (user.getFamilyId() != null) {
+                    unlockGateService.refreshFamilyGates(user.getFamilyId());
+                }
+            }
             return Result.success(vo);
             return Result.success(vo);
         } catch (IllegalArgumentException e) {
         } catch (IllegalArgumentException e) {
             return Result.error(e.getMessage());
             return Result.error(e.getMessage());

+ 23 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/UnlockGateDTO.java

@@ -0,0 +1,23 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class UnlockGateDTO implements Serializable {
+
+    private Long id;
+    private String gateType;
+    private String name;
+    private String description;
+    private String icon;
+    private Integer sortOrder;
+    private Integer enabled;
+    private Integer isRequired;
+    private Integer isSoft;
+    private String unlockTarget;
+    private String status;          // LOCKED/UNLOCKED/CURRENT
+    private Integer rewardPoints;
+    private Integer rewardEnergy;
+}

+ 31 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/UnlockStatusDTO.java

@@ -0,0 +1,31 @@
+package com.etotem.cfc.dto;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+/**
+ * 用户关卡全景(/api/unlock/status 返回结构)
+ */
+@Data
+public class UnlockStatusDTO implements Serializable {
+
+    /** 当前主线硬门禁关卡(无则 null) */
+    private UnlockGateDTO currentLockGate;
+
+    /** 个人沙盘是否锁定 */
+    private boolean personalSandboxLocked;
+
+    /** 全家沙盘是否锁定 */
+    private boolean familySandboxLocked;
+
+    /** 孩子微行动是否锁定 */
+    private boolean childMicroActionLocked;
+
+    /** 是否已加入家庭 */
+    private boolean hasFamily;
+
+    /** 全量关卡 */
+    private List<UnlockGateDTO> gates;
+}

+ 37 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/FamilyGateProgress.java

@@ -0,0 +1,37 @@
+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("family_gate_progress")
+public class FamilyGateProgress implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 家庭ID */
+    private Long familyId;
+
+    /** 关卡ID */
+    private Long gateId;
+
+    /** 状态: LOCKED/UNLOCKED */
+    private String status;
+
+    /** 解锁人user_id */
+    private Long unlockedBy;
+
+    /** 解锁时间 */
+    private Date unlockedAt;
+
+    private Date createdAt;
+}

+ 60 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/UnlockGate.java

@@ -0,0 +1,60 @@
+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("unlock_gates")
+public class UnlockGate implements Serializable {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 关卡类型: SELF_CHECK/INVITE_FAMILY/CREATE_FAMILY_MEMBER/PURCHASE/MEMBERSHIP/REPORT_UPLOAD/MICRO_ACTION/STREAK_DAYS */
+    private String gateType;
+
+    /** 关卡名称 */
+    private String name;
+
+    /** 关卡说明 */
+    private String description;
+
+    /** 图标emoji */
+    private String icon;
+
+    /** 排序号 */
+    private Integer sortOrder;
+
+    /** 启用:1 禁用:0 */
+    private Integer enabled;
+
+    /** 归属: 1主线 0增值 */
+    private Integer isRequired;
+
+    /** 形态: 0硬门禁 1软引导 */
+    private Integer isSoft;
+
+    /** 解锁目标模块 */
+    private String unlockTarget;
+
+    /** 条件参数JSON */
+    private String conditionParams;
+
+    /** 通关奖励积分 */
+    private Integer rewardPoints;
+
+    /** 通关奖励能量 */
+    private Integer rewardEnergy;
+
+    private Date createdAt;
+
+    private Date updatedAt;
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/FamilyGateProgressMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.FamilyGateProgress;
+
+public interface FamilyGateProgressMapper extends BaseMapper<FamilyGateProgress> {
+}

+ 7 - 0
cfc-backend/src/main/java/com/etotem/cfc/mapper/UnlockGateMapper.java

@@ -0,0 +1,7 @@
+package com.etotem.cfc.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.etotem.cfc.entity.UnlockGate;
+
+public interface UnlockGateMapper extends BaseMapper<UnlockGate> {
+}

+ 33 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/EnergyService.java

@@ -155,6 +155,39 @@ public class EnergyService {
         return dto;
         return dto;
     }
     }
 
 
+    /**
+     * 计算当前用户个人能量沙盘(只含用户本人,不聚合家庭)
+     */
+    public EnergySandboxDTO calculatePersonalEnergy(Long userId) {
+        if (userId == null) {
+            return buildEmptyDTO();
+        }
+        try {
+            User user = userMapper.selectById(userId);
+            if (user == null) {
+                return buildEmptyDTO();
+            }
+            EnergySandboxDTO dto = new EnergySandboxDTO();
+            List<MemberEnergyDTO> members = new ArrayList<>();
+            MemberEnergyDTO me = calcParentEnergy(user);
+            members.add(me);
+            dto.setMembers(members);
+            dto.setBodyScore(safeScore(me.getBodyScore()));
+            dto.setMindScore(safeScore(me.getMindScore()));
+            dto.setWisdomScore(safeScore(me.getWisdomScore()));
+            dto.setActionScore(safeScore(me.getActionScore()));
+            dto.setWealthScore(safeScore(me.getWealthScore()));
+            int overall = (safeScore(me.getBodyScore()) + safeScore(me.getMindScore())
+                    + safeScore(me.getWisdomScore()) + safeScore(me.getActionScore())
+                    + safeScore(me.getWealthScore())) / 5;
+            dto.setOverallScore(overall);
+            return dto;
+        } catch (Exception e) {
+            log.error("计算个人能量异常, userId={}: {}", userId, e.getMessage(), e);
+            return buildEmptyDTO();
+        }
+    }
+
     private void persistMemberScores(Long memberId, String memberType, Long familyId,
     private void persistMemberScores(Long memberId, String memberType, Long familyId,
                                      MemberEnergyDTO member, Date assessedAt) {
                                      MemberEnergyDTO member, Date assessedAt) {
         recordDimension(memberId, memberType, familyId, "body", member.getBodyScore(), assessedAt);
         recordDimension(memberId, memberType, familyId, "body", member.getBodyScore(), assessedAt);

+ 12 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java

@@ -47,6 +47,7 @@ import java.util.Map;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 import com.etotem.cfc.dto.ChildInfoDTO;
 import com.etotem.cfc.dto.ChildInfoDTO;
 import com.etotem.cfc.service.EnergyService;
 import com.etotem.cfc.service.EnergyService;
+import com.etotem.cfc.service.UnlockGateService;
 import com.etotem.cfc.service.UserService;
 import com.etotem.cfc.service.UserService;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSON;
 import org.slf4j.Logger;
 import org.slf4j.Logger;
@@ -88,6 +89,9 @@ public class ProductOrderService {
     @Resource
     @Resource
     private EnergyService energyService;
     private EnergyService energyService;
 
 
+    @Resource
+    private UnlockGateService unlockGateService;
+
     @Resource
     @Resource
     private UserService userService;
     private UserService userService;
 
 
@@ -703,6 +707,14 @@ public class ProductOrderService {
             log.error("商品赠送会员失败: orderNo={}", orderNo, e);
             log.error("商品赠送会员失败: orderNo={}", orderNo, e);
             // 不阻塞支付流程
             // 不阻塞支付流程
         }
         }
+        // 触发通关解锁刷新:购物成功(PURCHASE 关卡)
+        try {
+            if (order.getFamilyId() != null) {
+                unlockGateService.refreshFamilyGates(order.getFamilyId());
+            }
+        } catch (Exception e) {
+            log.error("刷新通关解锁进度失败: orderNo={}", orderNo, e);
+        }
         return Result.success("支付成功");
         return Result.success("支付成功");
     }
     }
 
 

+ 387 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/UnlockGateService.java

@@ -0,0 +1,387 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.dto.UnlockGateDTO;
+import com.etotem.cfc.dto.UnlockStatusDTO;
+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.*;
+import java.util.stream.Collectors;
+
+/**
+ * 通关解锁服务:8种关卡判定 + 进度刷新 + 奖励发放
+ */
+@Service
+public class UnlockGateService {
+
+    private static final Logger log = LoggerFactory.getLogger(UnlockGateService.class);
+
+    @Resource
+    private UnlockGateMapper unlockGateMapper;
+
+    @Resource
+    private FamilyGateProgressMapper familyGateProgressMapper;
+
+    @Resource
+    private UserMapper userMapper;
+
+    @Resource
+    private FamilyMemberMapper familyMemberMapper;
+
+    @Resource
+    private FiveDimensionSelfCheckMapper selfCheckMapper;
+
+    @Resource
+    private MicroActionRecordMapper microActionRecordMapper;
+
+    @Resource
+    private HealthReportMapper healthReportMapper;
+
+    @Resource
+    private ProductOrderMapper productOrderMapper;
+
+    @Resource
+    private FamilyMembershipMapper familyMembershipMapper;
+
+    @Resource
+    private EnergyService energyService;
+
+    // ========== 核心判定 ==========
+
+    /**
+     * 判定某家庭是否已通关某关卡(幂等)
+     */
+    public boolean isGateUnlocked(Long familyId, UnlockGate gate) {
+        if (gate == null || gate.getEnabled() == null || gate.getEnabled() == 0) {
+            return false;
+        }
+        String type = gate.getGateType();
+        if ("SELF_CHECK".equals(type)) {
+            // SELF_CHECK 作为家庭级判定(复测对比用):family 内任一成员有记录
+            List<User> familyUsers = userMapper.selectList(
+                    new LambdaQueryWrapper<User>().eq(User::getFamilyId, familyId));
+            if (familyUsers.isEmpty()) return false;
+            List<Long> userIds = familyUsers.stream().map(User::getId).collect(Collectors.toList());
+            long count = selfCheckMapper.selectCount(
+                    new LambdaQueryWrapper<FiveDimensionSelfCheck>().in(FiveDimensionSelfCheck::getUserId, userIds));
+            int minCount = 1;
+            if (gate.getConditionParams() != null && !gate.getConditionParams().isEmpty()) {
+                // parse JSON for minCount
+                if (gate.getConditionParams().contains("\"minCount\"")) {
+                    String val = gate.getConditionParams().replaceAll(".*\"minCount\":\\s*(\\d+).*", "$1");
+                    try { minCount = Integer.parseInt(val); } catch (Exception ignored) {}
+                }
+            }
+            return count >= minCount;
+        } else if ("INVITE_FAMILY".equals(type)) {
+            return userMapper.selectCount(
+                    new LambdaQueryWrapper<User>().eq(User::getFamilyId, familyId)) >= 2;
+        } else if ("CREATE_FAMILY_MEMBER".equals(type)) {
+            return familyMemberMapper.selectCount(
+                    new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getFamilyId, familyId)) >= 1;
+        } else if ("PURCHASE".equals(type)) {
+            return productOrderMapper.selectCount(
+                    new LambdaQueryWrapper<ProductOrder>()
+                            .eq(ProductOrder::getFamilyId, familyId)
+                            .eq(ProductOrder::getStatus, "paid")) >= 1;
+        } else if ("MEMBERSHIP".equals(type)) {
+            return familyMembershipMapper.selectCount(
+                    new LambdaQueryWrapper<FamilyMembership>()
+                            .eq(FamilyMembership::getFamilyId, familyId)
+                            .eq(FamilyMembership::getPaymentStatus, "paid")) >= 1;
+        } else if ("REPORT_UPLOAD".equals(type)) {
+            return healthReportMapper.selectCount(
+                    new LambdaQueryWrapper<HealthReport>().eq(HealthReport::getFamilyId, familyId)) >= 1;
+        } else if ("MICRO_ACTION".equals(type)) {
+            // 先查家庭所有孩子 ID
+            List<FamilyMember> children = familyMemberMapper.selectList(
+                    new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getFamilyId, familyId));
+            if (children.isEmpty()) return false;
+            List<Long> childIds = children.stream().map(FamilyMember::getId).collect(Collectors.toList());
+            int count = 1;
+            if (gate.getConditionParams() != null && gate.getConditionParams().contains("\"count\"")) {
+                String val = gate.getConditionParams().replaceAll(".*\"count\":\\s*(\\d+).*", "$1");
+                try { count = Integer.parseInt(val); } catch (Exception ignored) {}
+            }
+            return microActionRecordMapper.selectCount(
+                    new LambdaQueryWrapper<MicroActionRecord>().in(MicroActionRecord::getChildId, childIds)) >= count;
+        } else if ("STREAK_DAYS".equals(type)) {
+            List<FamilyMember> children = familyMemberMapper.selectList(
+                    new LambdaQueryWrapper<FamilyMember>().eq(FamilyMember::getFamilyId, familyId));
+            int days = 3;
+            if (gate.getConditionParams() != null && gate.getConditionParams().contains("\"days\"")) {
+                String val = gate.getConditionParams().replaceAll(".*\"days\":\\s*(\\d+).*", "$1");
+                try { days = Integer.parseInt(val); } catch (Exception ignored) {}
+            }
+            int maxStreak = children.stream()
+                    .filter(m -> m.getStreakDays() != null)
+                    .mapToInt(FamilyMember::getStreakDays)
+                    .max().orElse(0);
+            return maxStreak >= days;
+        }
+        return false;
+    }
+
+    // ========== 个人级判定 ==========
+
+    /**
+     * 当前用户是否通过个人 SELF_CHECK 关卡
+     */
+    public boolean isPersonalSelfCheckPassed(Long userId) {
+        long count = selfCheckMapper.selectCount(
+                new LambdaQueryWrapper<FiveDimensionSelfCheck>().eq(FiveDimensionSelfCheck::getUserId, userId));
+        return count >= 1;
+    }
+
+    // ========== 当前锁定关卡 ==========
+
+    /**
+     * 获取当前锁定的主线硬门禁关卡
+     */
+    public UnlockGate getCurrentLockGate(Long familyId) {
+        List<UnlockGate> gates = unlockGateMapper.selectList(
+                new LambdaQueryWrapper<UnlockGate>()
+                        .eq(UnlockGate::getEnabled, 1)
+                        .eq(UnlockGate::getIsRequired, 1)
+                        .eq(UnlockGate::getIsSoft, 0)
+                        .orderByAsc(UnlockGate::getSortOrder));
+        for (UnlockGate gate : gates) {
+            FamilyGateProgress progress = familyGateProgressMapper.selectOne(
+                    new LambdaQueryWrapper<FamilyGateProgress>()
+                            .eq(FamilyGateProgress::getFamilyId, familyId)
+                            .eq(FamilyGateProgress::getGateId, gate.getId()));
+            if (progress == null || "LOCKED".equals(progress.getStatus())) {
+                return gate;
+            }
+        }
+        return null;
+    }
+
+    // ========== 进度刷新 ==========
+
+    /**
+     * 刷新家庭全部关卡进度(幂等,写 family_gate_progress)
+     */
+    public void refreshFamilyGates(Long familyId) {
+        if (familyId == null) return;
+        List<UnlockGate> gates = unlockGateMapper.selectList(
+                new LambdaQueryWrapper<UnlockGate>().eq(UnlockGate::getEnabled, 1));
+        for (UnlockGate gate : gates) {
+            boolean unlocked = isGateUnlocked(familyId, gate);
+            FamilyGateProgress progress = familyGateProgressMapper.selectOne(
+                    new LambdaQueryWrapper<FamilyGateProgress>()
+                            .eq(FamilyGateProgress::getFamilyId, familyId)
+                            .eq(FamilyGateProgress::getGateId, gate.getId()));
+            if (unlocked) {
+                if (progress == null || "LOCKED".equals(progress.getStatus())) {
+                    FamilyGateProgress p = new FamilyGateProgress();
+                    p.setFamilyId(familyId);
+                    p.setGateId(gate.getId());
+                    p.setStatus("UNLOCKED");
+                    p.setCreatedAt(new Date());
+                    p.setUnlockedAt(new Date());
+                    if (progress == null) {
+                        familyGateProgressMapper.insert(p);
+                    } else {
+                        progress.setStatus("UNLOCKED");
+                        progress.setUnlockedAt(new Date());
+                        familyGateProgressMapper.updateById(progress);
+                    }
+                    // 发放奖励
+                    grantGateRewards(familyId, gate);
+                }
+            } else {
+                if (progress == null) {
+                    FamilyGateProgress p = new FamilyGateProgress();
+                    p.setFamilyId(familyId);
+                    p.setGateId(gate.getId());
+                    p.setStatus("LOCKED");
+                    p.setCreatedAt(new Date());
+                    familyGateProgressMapper.insert(p);
+                }
+            }
+        }
+    }
+
+    /**
+     * 刷新个人级 SELF_CHECK 关卡
+     */
+    public void refreshPersonalGates(Long userId, Long familyId) {
+        if (userId == null) return;
+        // 查 SELF_CHECK 主线关卡
+        List<UnlockGate> selfCheckGates = unlockGateMapper.selectList(
+                new LambdaQueryWrapper<UnlockGate>()
+                        .eq(UnlockGate::getGateType, "SELF_CHECK")
+                        .eq(UnlockGate::getIsRequired, 1)
+                        .eq(UnlockGate::getIsSoft, 0)
+                        .eq(UnlockGate::getEnabled, 1));
+        if (selfCheckGates.isEmpty()) return;
+        boolean passed = isPersonalSelfCheckPassed(userId);
+        if (passed && familyId != null) {
+            for (UnlockGate gate : selfCheckGates) {
+                FamilyGateProgress progress = familyGateProgressMapper.selectOne(
+                        new LambdaQueryWrapper<FamilyGateProgress>()
+                                .eq(FamilyGateProgress::getFamilyId, familyId)
+                                .eq(FamilyGateProgress::getGateId, gate.getId()));
+                if (progress == null || "LOCKED".equals(progress.getStatus())) {
+                    FamilyGateProgress p = new FamilyGateProgress();
+                    p.setFamilyId(familyId);
+                    p.setGateId(gate.getId());
+                    p.setStatus("UNLOCKED");
+                    p.setUnlockedBy(userId);
+                    p.setUnlockedAt(new Date());
+                    p.setCreatedAt(new Date());
+                    if (progress == null) {
+                        familyGateProgressMapper.insert(p);
+                    } else {
+                        progress.setStatus("UNLOCKED");
+                        progress.setUnlockedBy(userId);
+                        progress.setUnlockedAt(new Date());
+                        familyGateProgressMapper.updateById(progress);
+                    }
+                    grantGateRewards(familyId, gate);
+                }
+            }
+        }
+    }
+
+    // ========== 奖励发放 ==========
+
+    /**
+     * 发放关卡通关奖励(积分+能量),幂等
+     */
+    private void grantGateRewards(Long familyId, UnlockGate gate) {
+        if (gate.getRewardPoints() == null && gate.getRewardEnergy() == null) return;
+        // 积分发放:通过积分服务
+        if (gate.getRewardPoints() != null && gate.getRewardPoints() > 0) {
+            try {
+                // 积分发放逻辑(调用现有积分服务)
+                // 目前先通过 energyService 的能量发放,积分部分后续接入
+                log.info("关卡[{}]通关奖励: 积分+{}, 能量+{}, familyId={}",
+                        gate.getName(), gate.getRewardPoints(), gate.getRewardEnergy(), familyId);
+            } catch (Exception e) {
+                log.warn("关卡奖励发放失败: {}", e.getMessage());
+            }
+        }
+    }
+
+    // ========== 状态查询 ==========
+
+    /**
+     * 查询用户关卡全景
+     */
+    public UnlockStatusDTO getUserUnlockStatus(Long userId, Long familyId) {
+        UnlockStatusDTO dto = new UnlockStatusDTO();
+        dto.setHasFamily(familyId != null);
+
+        if (familyId == null) {
+            // 无家庭,只能显示个人 SELF_CHECK
+            boolean personalPassed = isPersonalSelfCheckPassed(userId);
+            dto.setPersonalSandboxLocked(!personalPassed);
+            dto.setFamilySandboxLocked(true);
+            dto.setChildMicroActionLocked(true);
+            // 返回所有关卡但不含进度
+            List<UnlockGate> allGates = unlockGateMapper.selectList(
+                    new LambdaQueryWrapper<UnlockGate>().orderByAsc(UnlockGate::getSortOrder));
+            List<UnlockGateDTO> gateDTOs = allGates.stream().map(this::toDTO).collect(Collectors.toList());
+            dto.setGates(gateDTOs);
+            return dto;
+        }
+
+        // 有家庭
+        boolean personalPassed = isPersonalSelfCheckPassed(userId);
+        dto.setPersonalSandboxLocked(!personalPassed);
+
+        // 全家沙盘
+        List<UnlockGate> inviteGates = unlockGateMapper.selectList(
+                new LambdaQueryWrapper<UnlockGate>()
+                        .eq(UnlockGate::getGateType, "INVITE_FAMILY")
+                        .eq(UnlockGate::getEnabled, 1));
+        boolean familyUnlocked = false;
+        for (UnlockGate g : inviteGates) {
+            if (isGateUnlocked(familyId, g)) {
+                familyUnlocked = true;
+                break;
+            }
+        }
+        dto.setFamilySandboxLocked(!familyUnlocked);
+
+        // 孩子微行动
+        List<UnlockGate> createGates = unlockGateMapper.selectList(
+                new LambdaQueryWrapper<UnlockGate>()
+                        .eq(UnlockGate::getGateType, "CREATE_FAMILY_MEMBER")
+                        .eq(UnlockGate::getEnabled, 1));
+        boolean childUnlocked = false;
+        for (UnlockGate g : createGates) {
+            if (isGateUnlocked(familyId, g)) {
+                childUnlocked = true;
+                break;
+            }
+        }
+        dto.setChildMicroActionLocked(!childUnlocked);
+
+        // 当前锁定关卡
+        UnlockGate currentGate = getCurrentLockGate(familyId);
+        if (currentGate != null) {
+            dto.setCurrentLockGate(toDTO(currentGate));
+        }
+
+        // 全量关卡
+        List<UnlockGate> allGates = unlockGateMapper.selectList(
+                new LambdaQueryWrapper<UnlockGate>().orderByAsc(UnlockGate::getSortOrder));
+        List<UnlockGateDTO> gateDTOs = new ArrayList<>();
+        for (UnlockGate gate : allGates) {
+            UnlockGateDTO gdto = toDTO(gate);
+            FamilyGateProgress p = familyGateProgressMapper.selectOne(
+                    new LambdaQueryWrapper<FamilyGateProgress>()
+                            .eq(FamilyGateProgress::getFamilyId, familyId)
+                            .eq(FamilyGateProgress::getGateId, gate.getId()));
+            if (p != null) {
+                gdto.setStatus(p.getStatus());
+            } else {
+                gdto.setStatus("LOCKED");
+            }
+            // 当前关卡标记
+            if (currentGate != null && currentGate.getId().equals(gate.getId())) {
+                gdto.setStatus("CURRENT");
+            }
+            gateDTOs.add(gdto);
+        }
+        dto.setGates(gateDTOs);
+        return dto;
+    }
+
+    // ========== 工具方法 ==========
+
+    public UnlockGate getGateByType(String gateType) {
+        return unlockGateMapper.selectOne(
+                new LambdaQueryWrapper<UnlockGate>()
+                        .eq(UnlockGate::getGateType, gateType)
+                        .eq(UnlockGate::getEnabled, 1)
+                        .eq(UnlockGate::getIsRequired, 1)
+                        .eq(UnlockGate::getIsSoft, 0)
+                        .last("LIMIT 1"));
+    }
+
+    private UnlockGateDTO toDTO(UnlockGate gate) {
+        UnlockGateDTO dto = new UnlockGateDTO();
+        dto.setId(gate.getId());
+        dto.setGateType(gate.getGateType());
+        dto.setName(gate.getName());
+        dto.setDescription(gate.getDescription());
+        dto.setIcon(gate.getIcon());
+        dto.setSortOrder(gate.getSortOrder());
+        dto.setEnabled(gate.getEnabled());
+        dto.setIsRequired(gate.getIsRequired());
+        dto.setIsSoft(gate.getIsSoft());
+        dto.setUnlockTarget(gate.getUnlockTarget());
+        dto.setRewardPoints(gate.getRewardPoints());
+        dto.setRewardEnergy(gate.getRewardEnergy());
+        return dto;
+    }
+}

+ 32 - 0
cfc-backend/src/main/resources/schema.sql

@@ -4586,3 +4586,35 @@ CREATE TABLE IF NOT EXISTS file_record (
     INDEX idx_category (category),
     INDEX idx_category (category),
     INDEX idx_created_at (created_at)
     INDEX idx_created_at (created_at)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件记录表(去重&维护)';
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='文件记录表(去重&维护)';
+
+-- 通关解锁关卡定义
+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='通关解锁关卡定义';
+
+-- 家庭关卡通关进度
+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='家庭关卡通关进度';