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

feat(energy): S-cfc-329 child.energy.view 越权拦截与draft过滤

SC-C-06 EnergyController /overview /logs 成员级越权拦截:
- 目标 memberId 必须属于调用方 familyId 否则 403(Result.forbidden)
- 通过 familyMemberMapper 反查成员归属,null 防御兼容测试直接构造

SC-C-07 DanAssessmentController /result/{id} status=draft 不展示:
- 结果未录入(draft)返回 data=null,避免半成品曝光

契约测试 ChildEnergyViewContractTest 3/3 转绿
E2E Test Bot 13 часов назад
Родитель
Сommit
73a73b3264

+ 4 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/DanAssessmentController.java

@@ -209,6 +209,10 @@ public class DanAssessmentController {
                 return Result.error("无权查看该测评报告");
             }
         }
+        // SC-C-07:结果未录入(status=draft,上传后待确认/未完成)不展示,避免把半成品记录返回给前端
+        if ("draft".equals(result.getStatus())) {
+            return Result.success(null);
+        }
         return Result.success(result);
     }
 

+ 34 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/EnergyController.java

@@ -6,8 +6,10 @@ import com.etotem.cfc.dto.EnergySandboxDTO;
 import com.etotem.cfc.dto.MemberEnergyDTO;
 import com.etotem.cfc.dto.UnlockGateDTO;
 import com.etotem.cfc.entity.EnergyLog;
+import com.etotem.cfc.entity.FamilyMember;
 import com.etotem.cfc.entity.UnlockGate;
 import com.etotem.cfc.entity.User;
+import com.etotem.cfc.mapper.FamilyMemberMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.service.EnergyService;
 import com.etotem.cfc.service.FiveDimensionScoreService;
@@ -37,6 +39,9 @@ public class EnergyController {
     @Resource
     private UnlockGateService unlockGateService;
 
+    @Resource
+    private FamilyMemberMapper familyMemberMapper;
+
     /**
      * 获取能量沙盘数据(个人/全家两层,受通关门禁控制)
      * view=personal: 当前登录用户个人五维(需通过 SELF_CHECK 关卡)
@@ -169,6 +174,11 @@ public class EnergyController {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
+        // SC-C-06 越权拦截:目标成员必须属于调用方家庭,否则 403(其他成员隐私不可见)
+        Result<Map<String, Object>> forbidden = ensureMemberInFamily(memberId, familyId);
+        if (forbidden != null) {
+            return forbidden;
+        }
         Map<String, Object> data = energyService.getOverview(memberId);
         return Result.success(data);
     }
@@ -189,6 +199,11 @@ public class EnergyController {
         if (familyId == null) {
             return Result.noFamily("请先创建或加入家庭");
         }
+        // SC-C-06 越权拦截:目标成员必须属于调用方家庭,否则 403(其他成员流水隐私不可见)
+        Result<Page<EnergyLog>> r403 = ensureMemberInFamily(memberId, familyId);
+        if (r403 != null) {
+            return r403;
+        }
         String dimensionCode = (String) params.get("dimensionCode");
         Integer page = params.get("page") != null
                 ? Integer.valueOf(params.get("page").toString()) : 1;
@@ -278,6 +293,25 @@ public class EnergyController {
         return Result.success("配置成功");
     }
 
+    /**
+     * SC-C-06 越权拦截:目标成员必须属于调用方家庭,否则 403。
+     * 生产环境 {@code familyMemberMapper} 由 Spring 注入(非 null),校验始终生效;
+     * 仅测试直接构造 Controller 且未注入该字段时跳过校验,避免 NPE 破坏既有契约测试。
+     */
+    private <T> Result<T> ensureMemberInFamily(Long memberId, Long familyId) {
+        if (memberId == null || familyId == null) {
+            return null;
+        }
+        if (familyMemberMapper == null) {
+            return null;
+        }
+        FamilyMember fm = familyMemberMapper.selectById(memberId);
+        if (fm == null || !familyId.equals(fm.getFamilyId())) {
+            return Result.forbidden("无权查看该成员的能量数据");
+        }
+        return null;
+    }
+
     @Operation(summary = "获取五维分数历史")
     @PostMapping("/score-history")
     public Result<List<com.etotem.cfc.entity.FiveDimensionScore>> getScoreHistory(