Browse Source

feat(planner): 规划师(teacher)按 roles 列表判定身份,修复主角色为 parent 的认证规划师看不到服务中心入口

后端:
- 新增 RolesUtil:hasTeacherRole(role, roles) 按完整角色列表判定规划师身份;
  effectiveRoles(user) 保证签发 token 时首位恒为主角色(role claim 不变)、roles claim 携带完整角色集
- 小程序 token 签发(UserService×10、InviteCardService×4)改为携带完整 roles
- 40 处原 !"teacher".equals(role) 守卫改为 !RolesUtil.hasTeacherRole(role, roles),
  涉及 guide/planner/health-plan/assessment/growth/article 相关控制器及 GuideFamilyAccessGuard
- GuidePlanReviewController/GuideFamilyQueryController 调用方透传 roles
- 既有契约测试补 roles=null(语义等价旧断言);新增 RolesUtilTest 与
  GuideFamilyAccessGuardTest roles 放行用例;123 个受影响单测通过

前端(需人工重新打包小程序):
- api.js: 修正 getUserRoles/switchRole URL(/api/family/user/...),
  新增 getTeacherInfo/getTeacherPendingTasks(指向 dashboard-stats)/getTeacherUnreadCount
- profile-main: loadTeacherIdentity 按 roles 探测规划师身份,服务中心入口/待审核提示
  改用 isTeacherUser,写 teacherStatus/userRoles 至 storage
- service-center: initRole 按 roles 判定 + 状态兜底拉取,解析 dashboard-stats 待办/服务家庭数,
  新增「培训认证」入口(打通 certification-center 唯一入口)
Xiaogang Liao 5 days ago
parent
commit
49e2edae05
31 changed files with 477 additions and 216 deletions
  1. 50 0
      cfc-backend/src/main/java/com/etotem/cfc/common/RolesUtil.java
  2. 13 8
      cfc-backend/src/main/java/com/etotem/cfc/controller/HealthPlanController.java
  3. 10 6
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminHealthPlanController.java
  4. 4 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentAppointmentController.java
  5. 4 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/content/ArticleCommentController.java
  6. 4 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/growth/GrowthPlanController.java
  7. 10 6
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/AssessmentController.java
  8. 4 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideFamilyListController.java
  9. 10 6
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideFamilyQueryController.java
  10. 31 20
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideFamilyTaskController.java
  11. 15 11
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuidePlanReviewController.java
  12. 4 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideRecordController.java
  13. 10 6
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/PlanReviewController.java
  14. 10 6
      cfc-backend/src/main/java/com/etotem/cfc/controller/guide/TeacherMessageController.java
  15. 22 14
      cfc-backend/src/main/java/com/etotem/cfc/controller/planner/PlannerCertificationController.java
  16. 4 2
      cfc-backend/src/main/java/com/etotem/cfc/service/GuideFamilyAccessGuard.java
  17. 5 4
      cfc-backend/src/main/java/com/etotem/cfc/service/InviteCardService.java
  18. 11 10
      cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
  19. 2 2
      cfc-backend/src/test/java/com/etotem/cfc/controller/HealthPlanDeleteControllerTest.java
  20. 20 20
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideFamilyTaskControllerTest.java
  21. 9 9
      cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideRolePermissionTest.java
  22. 1 1
      cfc-backend/src/test/java/com/etotem/cfc/unit/AssessmentQuotaAppointmentContractTest.java
  23. 7 7
      cfc-backend/src/test/java/com/etotem/cfc/unit/GuideBindingAccessContractTest.java
  24. 12 5
      cfc-backend/src/test/java/com/etotem/cfc/unit/GuideFamilyAccessGuardTest.java
  25. 12 12
      cfc-backend/src/test/java/com/etotem/cfc/unit/GuidePlanReviewControllerAuthTest.java
  26. 7 7
      cfc-backend/src/test/java/com/etotem/cfc/unit/PlanLifecycleParentContractTest.java
  27. 26 26
      cfc-backend/src/test/java/com/etotem/cfc/unit/PlanReviewTeacherContractTest.java
  28. 68 0
      cfc-backend/src/test/java/com/etotem/cfc/unit/RolesUtilTest.java
  29. 36 8
      cfc-frontend/pages/profile-main/profile.vue
  30. 41 8
      cfc-frontend/pages/service-center/index.vue
  31. 15 2
      cfc-frontend/utils/api.js

+ 50 - 0
cfc-backend/src/main/java/com/etotem/cfc/common/RolesUtil.java

@@ -0,0 +1,50 @@
+package com.etotem.cfc.common;
+
+import com.etotem.cfc.entity.User;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 角色判定统一工具。
+ * 以「完整角色列表」而非单一主角色判定身份,支持主角色为 parent、但 roles 同时含
+ * teacher(规划师)的用户访问规划师专属能力,同时保持主角色(role claim)语义不变。
+ */
+public final class RolesUtil {
+
+    private RolesUtil() {
+    }
+
+    /**
+     * 是否具备规划师(teacher)身份:主角色为 teacher,或角色列表包含 teacher。
+     */
+    public static boolean hasTeacherRole(String role, List<String> roles) {
+        if ("teacher".equals(role)) {
+            return true;
+        }
+        return roles != null && roles.contains("teacher");
+    }
+
+    /**
+     * 构造签发 token 的有效角色列表:首位恒为主角色(保持 role claim 不变),
+     * 随后去重追加其余角色,使 JWT 的 roles claim 携带完整角色集。
+     */
+    public static List<String> effectiveRoles(User user) {
+        List<String> result = new ArrayList<>();
+        if (user == null) {
+            return result;
+        }
+        String primary = user.getRole();
+        if (primary != null && !primary.isEmpty()) {
+            result.add(primary);
+        }
+        if (user.getRoles() != null) {
+            for (String r : user.getRoles()) {
+                if (r != null && !r.isEmpty() && !result.contains(r)) {
+                    result.add(r);
+                }
+            }
+        }
+        return result;
+    }
+}

+ 13 - 8
cfc-backend/src/main/java/com/etotem/cfc/controller/HealthPlanController.java

@@ -14,6 +14,7 @@ import javax.annotation.Resource;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import com.etotem.cfc.util.ParamUtils;
 import com.etotem.cfc.util.ParamUtils;
+import com.etotem.cfc.common.RolesUtil;
 
 
 @Tag(name = "健康方案", description = "健康方案生成与管理")
 @Tag(name = "健康方案", description = "健康方案生成与管理")
 @RestController
 @RestController
@@ -242,8 +243,9 @@ public class HealthPlanController {
             @RequestBody Map<String, Object> params,
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute("role") String role,
             @RequestAttribute("role") String role,
-            @RequestAttribute(value = "familyId", required = false) Long familyId) {
-        if (!"teacher".equals(role) && !"admin".equals(role)) {
+            @RequestAttribute(value = "familyId", required = false) Long familyId,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles) && !"admin".equals(role)) {
             return Result.error("无权限");
             return Result.error("无权限");
         }
         }
         if (familyId == null) return Result.error("familyId不能为空");
         if (familyId == null) return Result.error("familyId不能为空");
@@ -257,8 +259,9 @@ public class HealthPlanController {
     public Result<HealthPlan> updatePlan(
     public Result<HealthPlan> updatePlan(
             @RequestBody Map<String, Object> params,
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute("userId") Long userId,
-            @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role) && !"admin".equals(role)) {
+            @RequestAttribute("role") String role,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles) && !"admin".equals(role)) {
             return Result.error("无权限");
             return Result.error("无权限");
         }
         }
         Long planId = ParamUtils.getLong(params.get("planId"));
         Long planId = ParamUtils.getLong(params.get("planId"));
@@ -274,8 +277,9 @@ public class HealthPlanController {
     public Result<PlanApproveResultDTO> approvePlan(
     public Result<PlanApproveResultDTO> approvePlan(
             @RequestBody Map<String, Object> params,
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute("userId") Long userId,
-            @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role) && !"admin".equals(role)) {
+            @RequestAttribute("role") String role,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles) && !"admin".equals(role)) {
             return Result.error("无权限");
             return Result.error("无权限");
         }
         }
         Long planId = ParamUtils.getLong(params.get("planId"));
         Long planId = ParamUtils.getLong(params.get("planId"));
@@ -290,8 +294,9 @@ public class HealthPlanController {
     public Result<HealthPlan> rejectPlan(
     public Result<HealthPlan> rejectPlan(
             @RequestBody Map<String, Object> params,
             @RequestBody Map<String, Object> params,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute("userId") Long userId,
-            @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role) && !"admin".equals(role)) {
+            @RequestAttribute("role") String role,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles) && !"admin".equals(role)) {
             return Result.error("无权限");
             return Result.error("无权限");
         }
         }
         Long planId = ParamUtils.getLong(params.get("planId"));
         Long planId = ParamUtils.getLong(params.get("planId"));

+ 10 - 6
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminHealthPlanController.java

@@ -13,6 +13,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
+import com.etotem.cfc.common.RolesUtil;
 
 
 @Tag(name = "管理端-健康方案", description = "跨家庭健康方案列表与详情")
 @Tag(name = "管理端-健康方案", description = "跨家庭健康方案列表与详情")
 @RestController
 @RestController
@@ -25,8 +26,9 @@ public class AdminHealthPlanController {
     @Operation(summary = "健康方案分页列表(按成员展示)")
     @Operation(summary = "健康方案分页列表(按成员展示)")
     @PostMapping("/list")
     @PostMapping("/list")
     public Result<Page<HealthPlan>> list(@RequestBody Map<String, Object> params,
     public Result<Page<HealthPlan>> list(@RequestBody Map<String, Object> params,
-                                         @RequestAttribute("role") String role) {
-        if (!"admin".equals(role) && !"teacher".equals(role) && !"butler".equals(role)
+                                         @RequestAttribute("role") String role,
+                                         @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!"admin".equals(role) && !RolesUtil.hasTeacherRole(role, roles) && !"butler".equals(role)
                 && !"nutritionist".equals(role) && !"operator".equals(role)) {
                 && !"nutritionist".equals(role) && !"operator".equals(role)) {
             return Result.error("无权限");
             return Result.error("无权限");
         }
         }
@@ -42,8 +44,9 @@ public class AdminHealthPlanController {
     @Operation(summary = "健康方案详情(含分解任务)")
     @Operation(summary = "健康方案详情(含分解任务)")
     @PostMapping("/detail")
     @PostMapping("/detail")
     public Result<HealthPlan> detail(@RequestBody Map<String, Object> params,
     public Result<HealthPlan> detail(@RequestBody Map<String, Object> params,
-                                     @RequestAttribute("role") String role) {
-        if (!"admin".equals(role) && !"teacher".equals(role) && !"butler".equals(role)
+                                     @RequestAttribute("role") String role,
+                                     @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!"admin".equals(role) && !RolesUtil.hasTeacherRole(role, roles) && !"butler".equals(role)
                 && !"nutritionist".equals(role) && !"operator".equals(role)) {
                 && !"nutritionist".equals(role) && !"operator".equals(role)) {
             return Result.error("无权限");
             return Result.error("无权限");
         }
         }
@@ -63,8 +66,9 @@ public class AdminHealthPlanController {
     @Operation(summary = "删除健康方案(级联删除方案产生的全部任务,含每日实例)")
     @Operation(summary = "删除健康方案(级联删除方案产生的全部任务,含每日实例)")
     @PostMapping("/delete")
     @PostMapping("/delete")
     public Result<Map<String, Object>> deletePlan(@RequestBody Map<String, Object> params,
     public Result<Map<String, Object>> deletePlan(@RequestBody Map<String, Object> params,
-                                                  @RequestAttribute("role") String role) {
-        if (!"admin".equals(role) && !"teacher".equals(role) && !"butler".equals(role)
+                                                  @RequestAttribute("role") String role,
+                                                  @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!"admin".equals(role) && !RolesUtil.hasTeacherRole(role, roles) && !"butler".equals(role)
                 && !"nutritionist".equals(role) && !"operator".equals(role)) {
                 && !"nutritionist".equals(role) && !"operator".equals(role)) {
             return Result.error("无权限");
             return Result.error("无权限");
         }
         }

+ 4 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/assessment/AssessmentAppointmentController.java

@@ -19,6 +19,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 import com.etotem.cfc.util.ParamUtils;
 import com.etotem.cfc.util.ParamUtils;
+import com.etotem.cfc.common.RolesUtil;
 
 
 @Tag(name = "评估预约", description = "评估预约管理")
 @Tag(name = "评估预约", description = "评估预约管理")
 @RestController
 @RestController
@@ -127,8 +128,9 @@ public class AssessmentAppointmentController {
     @PostMapping("/guide-list")
     @PostMapping("/guide-list")
     public Result<List<Map<String, Object>>> getGuideAppointments(
     public Result<List<Map<String, Object>>> getGuideAppointments(
             @RequestAttribute("userId") Long userId,
             @RequestAttribute("userId") Long userId,
-            @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role)) {
+            @RequestAttribute("role") String role,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles)) {
             return Result.error("只有成长规划师可以查看此列表");
             return Result.error("只有成长规划师可以查看此列表");
         }
         }
         List<AssessmentAppointment> appointments = appointmentService.getByGuideId(userId);
         List<AssessmentAppointment> appointments = appointmentService.getByGuideId(userId);

+ 4 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/content/ArticleCommentController.java

@@ -27,6 +27,7 @@ import com.alibaba.fastjson.JSONArray;
 import com.alibaba.fastjson.JSONObject;
 import com.alibaba.fastjson.JSONObject;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.util.ParamUtils;
 import com.etotem.cfc.util.ParamUtils;
+import com.etotem.cfc.common.RolesUtil;
 
 
 @Tag(name = "文章评论", description = "文章评论与AI答题")
 @Tag(name = "文章评论", description = "文章评论与AI答题")
 @RestController
 @RestController
@@ -69,8 +70,9 @@ public class ArticleCommentController {
     @PostMapping("/comments/audit")
     @PostMapping("/comments/audit")
     public Result<String> auditComment(@RequestBody Map<String, Object> params,
     public Result<String> auditComment(@RequestBody Map<String, Object> params,
                                         @RequestAttribute("userId") Long userId,
                                         @RequestAttribute("userId") Long userId,
-                                        @RequestAttribute("role") String role) {
-        if (!"admin".equals(role) && !"teacher".equals(role)) {
+                                        @RequestAttribute("role") String role,
+                                        @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!"admin".equals(role) && !RolesUtil.hasTeacherRole(role, roles)) {
             return Result.error("无权限,仅管理员或成长规划师可审核评论");
             return Result.error("无权限,仅管理员或成长规划师可审核评论");
         }
         }
         Long commentId = ParamUtils.getLong(params.get("commentId"));
         Long commentId = ParamUtils.getLong(params.get("commentId"));

+ 4 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/growth/GrowthPlanController.java

@@ -15,6 +15,7 @@ import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.mapper.UserMapper;
 import com.etotem.cfc.mapper.GrowthPlanMapper;
 import com.etotem.cfc.mapper.GrowthPlanMapper;
+import com.etotem.cfc.common.RolesUtil;
 
 
 @Tag(name = "成长方案", description = "成长方案创建、更新、复盘等接口")
 @Tag(name = "成长方案", description = "成长方案创建、更新、复盘等接口")
 @RestController
 @RestController
@@ -107,8 +108,9 @@ public class GrowthPlanController {
     @PostMapping("/list")
     @PostMapping("/list")
     public Result<IPage<Map<String, Object>>> listPlans(@RequestBody Map<String, Object> params,
     public Result<IPage<Map<String, Object>>> listPlans(@RequestBody Map<String, Object> params,
                                                           @RequestAttribute("userId") Long userId,
                                                           @RequestAttribute("userId") Long userId,
-                                                          @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role) && !"admin".equals(role)) {
+                                                          @RequestAttribute("role") String role,
+                                                          @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles) && !"admin".equals(role)) {
             return Result.error("无权限");
             return Result.error("无权限");
         }
         }
         int page = params.get("page") != null ? Integer.parseInt(params.get("page").toString()) : 1;
         int page = params.get("page") != null ? Integer.parseInt(params.get("page").toString()) : 1;

+ 10 - 6
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/AssessmentController.java

@@ -9,6 +9,7 @@ import io.swagger.v3.oas.annotations.tags.Tag;
 import org.springframework.web.bind.annotation.*;
 import org.springframework.web.bind.annotation.*;
 
 
 import java.util.List;
 import java.util.List;
+import com.etotem.cfc.common.RolesUtil;
 
 
 /**
 /**
  * 测评报告控制器
  * 测评报告控制器
@@ -28,9 +29,10 @@ public class AssessmentController {
     @PostMapping("/result/{id}")
     @PostMapping("/result/{id}")
     public Result<DanAssessmentResult> getAssessmentResult(@RequestAttribute("userId") Long userId,
     public Result<DanAssessmentResult> getAssessmentResult(@RequestAttribute("userId") Long userId,
                                                           @RequestAttribute("role") String role,
                                                           @RequestAttribute("role") String role,
-                                                          @PathVariable Long id) {
+                                                          @PathVariable Long id,
+                                                          @RequestAttribute(value = "roles", required = false) List<String> roles) {
         // 只有指导师可以查看测评报告
         // 只有指导师可以查看测评报告
-        if (!"teacher".equals(role)) {
+        if (!RolesUtil.hasTeacherRole(role, roles)) {
             return Result.error("只有指导师可以查看测评报告");
             return Result.error("只有指导师可以查看测评报告");
         }
         }
 
 
@@ -49,9 +51,10 @@ public class AssessmentController {
     @Operation(summary = "获取指导师自己的测评报告列表")
     @Operation(summary = "获取指导师自己的测评报告列表")
     @PostMapping("/my-results")
     @PostMapping("/my-results")
     public Result<List<DanAssessmentResult>> getMyAssessmentResults(@RequestAttribute("userId") Long userId,
     public Result<List<DanAssessmentResult>> getMyAssessmentResults(@RequestAttribute("userId") Long userId,
-                                                                     @RequestAttribute("role") String role) {
+                                                                     @RequestAttribute("role") String role,
+                                                                     @RequestAttribute(value = "roles", required = false) List<String> roles) {
         // 只有指导师可以查看测评报告
         // 只有指导师可以查看测评报告
-        if (!"teacher".equals(role)) {
+        if (!RolesUtil.hasTeacherRole(role, roles)) {
             return Result.error("只有指导师可以查看测评报告");
             return Result.error("只有指导师可以查看测评报告");
         }
         }
 
 
@@ -65,9 +68,10 @@ public class AssessmentController {
     @Operation(summary = "获取下级指导师的测评报告列表")
     @Operation(summary = "获取下级指导师的测评报告列表")
     @PostMapping("/child-results")
     @PostMapping("/child-results")
     public Result<List<DanAssessmentResult>> getChildAssessmentResults(@RequestAttribute("userId") Long userId,
     public Result<List<DanAssessmentResult>> getChildAssessmentResults(@RequestAttribute("userId") Long userId,
-                                                                       @RequestAttribute("role") String role) {
+                                                                       @RequestAttribute("role") String role,
+                                                                       @RequestAttribute(value = "roles", required = false) List<String> roles) {
         // 只有指导师可以查看测评报告
         // 只有指导师可以查看测评报告
-        if (!"teacher".equals(role)) {
+        if (!RolesUtil.hasTeacherRole(role, roles)) {
             return Result.error("只有指导师可以查看测评报告");
             return Result.error("只有指导师可以查看测评报告");
         }
         }
 
 

+ 4 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideFamilyListController.java

@@ -21,6 +21,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
+import com.etotem.cfc.common.RolesUtil;
 
 
 @RestController
 @RestController
 @RequestMapping("/api/guide/family")
 @RequestMapping("/api/guide/family")
@@ -37,8 +38,9 @@ public class GuideFamilyListController {
 
 
     @PostMapping("/list")
     @PostMapping("/list")
     public Result<List<Map<String, Object>>> list(@RequestAttribute("userId") Long guideId,
     public Result<List<Map<String, Object>>> list(@RequestAttribute("userId") Long guideId,
-                                                  @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role)) {
+                                                  @RequestAttribute("role") String role,
+                                                  @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles)) {
             return Result.error("Access denied");
             return Result.error("Access denied");
         }
         }
 
 

+ 10 - 6
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideFamilyQueryController.java

@@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
+import com.etotem.cfc.common.RolesUtil;
 
 
 @Tag(name = "指导师-服务家庭查询", description = "规划师查询自己服务的家庭与客户家庭成员")
 @Tag(name = "指导师-服务家庭查询", description = "规划师查询自己服务的家庭与客户家庭成员")
 @RestController
 @RestController
@@ -27,9 +28,10 @@ public class GuideFamilyQueryController {
     @PostMapping("/my-families")
     @PostMapping("/my-families")
     public Result<List<Map<String, Object>>> myFamilies(
     public Result<List<Map<String, Object>>> myFamilies(
             @RequestAttribute("role") String role,
             @RequestAttribute("role") String role,
-            @RequestAttribute("userId") Long userId) {
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
         // 本端点语义为「我自己的家庭」,无 familyId 维度,故不用 checkFamilyAccess(其要求 familyId 非空)
         // 本端点语义为「我自己的家庭」,无 familyId 维度,故不用 checkFamilyAccess(其要求 familyId 非空)
-        if (!"teacher".equals(role) && !"admin".equals(role)) {
+        if (!RolesUtil.hasTeacherRole(role, roles) && !"admin".equals(role)) {
             return Result.error(403, "无权访问该接口");
             return Result.error(403, "无权访问该接口");
         }
         }
         return Result.success(guideFamilyQueryService.listMyBoundFamilies(userId));
         return Result.success(guideFamilyQueryService.listMyBoundFamilies(userId));
@@ -43,8 +45,9 @@ public class GuideFamilyQueryController {
     @PostMapping
     @PostMapping
     public Result<List<Map<String, Object>>> rootFamilies(
     public Result<List<Map<String, Object>>> rootFamilies(
             @RequestAttribute("role") String role,
             @RequestAttribute("role") String role,
-            @RequestAttribute("userId") Long userId) {
-        return this.myFamilies(role, userId);
+            @RequestAttribute("userId") Long userId,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        return this.myFamilies(role, userId, roles);
     }
     }
 
 
     @Operation(summary = "客户家庭成员列表")
     @Operation(summary = "客户家庭成员列表")
@@ -52,8 +55,9 @@ public class GuideFamilyQueryController {
     public Result<List<FamilyMember>> familyMembers(
     public Result<List<FamilyMember>> familyMembers(
             @RequestAttribute("role") String role,
             @RequestAttribute("role") String role,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute("userId") Long userId,
-            @PathVariable("familyId") Long familyId) {
-        Result<Void> denied = guideFamilyAccessGuard.checkFamilyAccess(role, userId, familyId);
+            @PathVariable("familyId") Long familyId,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        Result<Void> denied = guideFamilyAccessGuard.checkFamilyAccess(role, roles, userId, familyId);
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
 
 
         return Result.success(guideFamilyQueryService.listFamilyMembers(familyId));
         return Result.success(guideFamilyQueryService.listFamilyMembers(familyId));

+ 31 - 20
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideFamilyTaskController.java

@@ -28,6 +28,7 @@ import java.time.ZoneId;
 import java.util.*;
 import java.util.*;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 import com.etotem.cfc.util.ParamUtils;
 import com.etotem.cfc.util.ParamUtils;
+import com.etotem.cfc.common.RolesUtil;
 
 
 @Tag(name = "指导师-家庭任务管理", description = "指导师查看家庭任务、审核任务等接口")
 @Tag(name = "指导师-家庭任务管理", description = "指导师查看家庭任务、审核任务等接口")
 @RestController
 @RestController
@@ -65,8 +66,9 @@ public class GuideFamilyTaskController {
     @PostMapping("/unbind")
     @PostMapping("/unbind")
     public Result<Map<String, Object>> unbindFamily(@RequestAttribute("userId") Long guideId,
     public Result<Map<String, Object>> unbindFamily(@RequestAttribute("userId") Long guideId,
                                                    @RequestAttribute("role") String role,
                                                    @RequestAttribute("role") String role,
-                                                   @RequestBody Map<String, Object> params) {
-        if (!"teacher".equals(role)) {
+                                                   @RequestBody Map<String, Object> params,
+                                                   @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles)) {
             return Result.error("Access denied");
             return Result.error("Access denied");
         }
         }
         Long id = ParamUtils.getLong(params.get("id"));
         Long id = ParamUtils.getLong(params.get("id"));
@@ -112,8 +114,9 @@ public class GuideFamilyTaskController {
         @ApiResponse(responseCode = "401", description = "未登录")
         @ApiResponse(responseCode = "401", description = "未登录")
     })
     })
     @PostMapping("/bound-families")
     @PostMapping("/bound-families")
-    public Result<List<Map<String, Object>>> getBoundFamilies(@RequestAttribute("userId") Long guideId, @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+    public Result<List<Map<String, Object>>> getBoundFamilies(@RequestAttribute("userId") Long guideId, @RequestAttribute("role") String role,
+    @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles)) { return Result.error("Access denied"); }
 
 
         // 获取当前指导师信息
         // 获取当前指导师信息
         User guide = userMapper.selectById(guideId);
         User guide = userMapper.selectById(guideId);
@@ -186,8 +189,9 @@ public class GuideFamilyTaskController {
     public Result<Map<String, Object>> getFamilyTaskOverview(
     public Result<Map<String, Object>> getFamilyTaskOverview(
             @RequestAttribute("userId") Long guideId,
             @RequestAttribute("userId") Long guideId,
             @RequestAttribute("role") String role,
             @RequestAttribute("role") String role,
-            @PathVariable Long familyId) {
-        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+            @PathVariable Long familyId,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles)) { return Result.error("Access denied"); }
         Result<Void> denied = requireBound(guideId, familyId);
         Result<Void> denied = requireBound(guideId, familyId);
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
 
 
@@ -258,8 +262,9 @@ public class GuideFamilyTaskController {
             @PathVariable Long familyId,
             @PathVariable Long familyId,
             @PathVariable Long memberId,
             @PathVariable Long memberId,
             @RequestAttribute("role") String role,
             @RequestAttribute("role") String role,
-            @RequestBody(required = false) Map<String, Object> params) {
-        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+            @RequestBody(required = false) Map<String, Object> params,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles)) { return Result.error("Access denied"); }
         Result<Void> denied = requireBound(guideId, familyId);
         Result<Void> denied = requireBound(guideId, familyId);
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
 
 
@@ -286,9 +291,10 @@ public class GuideFamilyTaskController {
             @RequestAttribute("userId") Long guideId,
             @RequestAttribute("userId") Long guideId,
             @PathVariable Long familyId,
             @PathVariable Long familyId,
             @PathVariable Long memberId,
             @PathVariable Long memberId,
-            @RequestAttribute("role") String role) {
+            @RequestAttribute("role") String role,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
 
 
-        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+        if (!RolesUtil.hasTeacherRole(role, roles)) { return Result.error("Access denied"); }
         Result<Void> denied = requireBound(guideId, familyId);
         Result<Void> denied = requireBound(guideId, familyId);
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
         List<Task> tasks = taskService.getPendingReviewTasksForFamilyMember(memberId);
         List<Task> tasks = taskService.getPendingReviewTasksForFamilyMember(memberId);
@@ -311,9 +317,10 @@ public class GuideFamilyTaskController {
             @PathVariable Long familyId,
             @PathVariable Long familyId,
             @PathVariable Long taskId,
             @PathVariable Long taskId,
             @RequestAttribute("role") String role,
             @RequestAttribute("role") String role,
-            @RequestBody TaskReviewDTO dto) {
+            @RequestBody TaskReviewDTO dto,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
 
 
-        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+        if (!RolesUtil.hasTeacherRole(role, roles)) { return Result.error("Access denied"); }
         Result<Void> denied = requireBound(guideId, familyId);
         Result<Void> denied = requireBound(guideId, familyId);
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
         Result<?> memberCheck = checkMemberOnly(taskId);
         Result<?> memberCheck = checkMemberOnly(taskId);
@@ -338,7 +345,8 @@ public class GuideFamilyTaskController {
             @RequestAttribute("userId") Long guideId,
             @RequestAttribute("userId") Long guideId,
             @PathVariable Long familyId,
             @PathVariable Long familyId,
             @RequestAttribute("role") String role,
             @RequestAttribute("role") String role,
-            @RequestBody Map<String, Object> params) {
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
 
 
         @SuppressWarnings("unchecked")
         @SuppressWarnings("unchecked")
         List<Object> rawTaskIds = (List<Object>) params.get("taskIds");
         List<Object> rawTaskIds = (List<Object>) params.get("taskIds");
@@ -351,7 +359,7 @@ public class GuideFamilyTaskController {
         Boolean approved = params.get("approved") != null ? Boolean.valueOf(params.get("approved").toString()) : Boolean.FALSE;
         Boolean approved = params.get("approved") != null ? Boolean.valueOf(params.get("approved").toString()) : Boolean.FALSE;
 
 
 
 
-        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+        if (!RolesUtil.hasTeacherRole(role, roles)) { return Result.error("Access denied"); }
         Result<Void> denied = requireBound(guideId, familyId);
         Result<Void> denied = requireBound(guideId, familyId);
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
         int successCount = 0;
         int successCount = 0;
@@ -399,9 +407,10 @@ public class GuideFamilyTaskController {
             @PathVariable Long familyId,
             @PathVariable Long familyId,
             @PathVariable Long taskId,
             @PathVariable Long taskId,
             @RequestAttribute("role") String role,
             @RequestAttribute("role") String role,
-            @RequestBody Map<String, Object> taskData) {
+            @RequestBody Map<String, Object> taskData,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
         
         
-        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+        if (!RolesUtil.hasTeacherRole(role, roles)) { return Result.error("Access denied"); }
         Result<Void> denied = requireBound(guideId, familyId);
         Result<Void> denied = requireBound(guideId, familyId);
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
         
         
@@ -452,9 +461,10 @@ public class GuideFamilyTaskController {
             @RequestAttribute("userId") Long guideId,
             @RequestAttribute("userId") Long guideId,
             @PathVariable Long familyId,
             @PathVariable Long familyId,
             @PathVariable Long taskId,
             @PathVariable Long taskId,
-            @RequestAttribute("role") String role) {
+            @RequestAttribute("role") String role,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
         
         
-        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+        if (!RolesUtil.hasTeacherRole(role, roles)) { return Result.error("Access denied"); }
         Result<Void> denied = requireBound(guideId, familyId);
         Result<Void> denied = requireBound(guideId, familyId);
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
         
         
@@ -481,8 +491,9 @@ public class GuideFamilyTaskController {
      */
      */
     @Operation(summary = "获取规划师首页统计")
     @Operation(summary = "获取规划师首页统计")
     @PostMapping("/dashboard-stats")
     @PostMapping("/dashboard-stats")
-    public Result<Map<String, Object>> getDashboardStats(@RequestAttribute("userId") Long guideId, @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role)) { return Result.error("Access denied"); }
+    public Result<Map<String, Object>> getDashboardStats(@RequestAttribute("userId") Long guideId, @RequestAttribute("role") String role,
+    @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles)) { return Result.error("Access denied"); }
 
 
         User guide = userMapper.selectById(guideId);
         User guide = userMapper.selectById(guideId);
         long totalFamilies = 0;
         long totalFamilies = 0;

+ 15 - 11
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuidePlanReviewController.java

@@ -31,8 +31,9 @@ public class GuidePlanReviewController {
             @RequestBody(required = false) Map<String, Object> params,
             @RequestBody(required = false) Map<String, Object> params,
             @RequestAttribute("role") String role,
             @RequestAttribute("role") String role,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute("userId") Long userId,
-            @PathVariable("familyId") Long familyId) {
-        Result<Void> denied = guideFamilyAccessGuard.checkFamilyAccess(role, userId, familyId);
+            @PathVariable("familyId") Long familyId,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        Result<Void> denied = guideFamilyAccessGuard.checkFamilyAccess(role, roles, userId, familyId);
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
 
 
         Object rawTeacherId = params == null ? null : params.get("teacherId");
         Object rawTeacherId = params == null ? null : params.get("teacherId");
@@ -47,8 +48,9 @@ public class GuidePlanReviewController {
             @RequestAttribute("role") String role,
             @RequestAttribute("role") String role,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute("userId") Long userId,
             @PathVariable("familyId") Long familyId,
             @PathVariable("familyId") Long familyId,
-            @PathVariable("planId") Long planId) {
-        Result<Void> denied = resolvePlan(role, userId, familyId, planId);
+            @PathVariable("planId") Long planId,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        Result<Void> denied = resolvePlan(role, roles, userId, familyId, planId);
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
 
 
         String planContent = params == null ? null : (String) params.get("planContent");
         String planContent = params == null ? null : (String) params.get("planContent");
@@ -63,8 +65,9 @@ public class GuidePlanReviewController {
             @RequestAttribute("role") String role,
             @RequestAttribute("role") String role,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute("userId") Long userId,
             @PathVariable("familyId") Long familyId,
             @PathVariable("familyId") Long familyId,
-            @PathVariable("planId") Long planId) {
-        Result<Void> denied = resolvePlan(role, userId, familyId, planId);
+            @PathVariable("planId") Long planId,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        Result<Void> denied = resolvePlan(role, roles, userId, familyId, planId);
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
 
 
         String comment = params == null ? null : (String) params.get("comment");
         String comment = params == null ? null : (String) params.get("comment");
@@ -78,8 +81,9 @@ public class GuidePlanReviewController {
             @RequestAttribute("role") String role,
             @RequestAttribute("role") String role,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute("userId") Long userId,
             @PathVariable("familyId") Long familyId,
             @PathVariable("familyId") Long familyId,
-            @PathVariable("planId") Long planId) {
-        Result<Void> denied = resolvePlan(role, userId, familyId, planId);
+            @PathVariable("planId") Long planId,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        Result<Void> denied = resolvePlan(role, roles, userId, familyId, planId);
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
         if (denied != null) return Result.error(denied.getCode(), denied.getMessage());
 
 
         String comment = params == null ? null : (String) params.get("comment");
         String comment = params == null ? null : (String) params.get("comment");
@@ -91,8 +95,8 @@ public class GuidePlanReviewController {
      *
      *
      * @return null 表示放行;非 null 为 403 响应
      * @return null 表示放行;非 null 为 403 响应
      */
      */
-    private Result<Void> resolvePlan(String role, Long userId, Long familyId, Long planId) {
-        Result<Void> denied = guideFamilyAccessGuard.checkFamilyAccess(role, userId, familyId);
+    private Result<Void> resolvePlan(String role, List<String> roles, Long userId, Long familyId, Long planId) {
+        Result<Void> denied = guideFamilyAccessGuard.checkFamilyAccess(role, roles, userId, familyId);
         if (denied != null) return denied;
         if (denied != null) return denied;
 
 
         HealthPlan plan = healthPlanService.getPlanForFamily(planId, familyId);
         HealthPlan plan = healthPlanService.getPlanForFamily(planId, familyId);
@@ -101,7 +105,7 @@ public class GuidePlanReviewController {
         }
         }
         // 兜底重复校验:getPlanForFamily 已按 familyId 过滤,故此处 plan.getFamilyId() 必然等于
         // 兜底重复校验:getPlanForFamily 已按 familyId 过滤,故此处 plan.getFamilyId() 必然等于
         // 上方已校验的 familyId,本次调用实际不会拒绝任何请求。保留仅为纵深防御,勿依赖它拦截路径不一致。
         // 上方已校验的 familyId,本次调用实际不会拒绝任何请求。保留仅为纵深防御,勿依赖它拦截路径不一致。
-        Result<Void> recheck = guideFamilyAccessGuard.checkFamilyAccess(role, userId, plan.getFamilyId());
+        Result<Void> recheck = guideFamilyAccessGuard.checkFamilyAccess(role, roles, userId, plan.getFamilyId());
         if (recheck != null) return recheck;
         if (recheck != null) return recheck;
 
 
         return null;
         return null;

+ 4 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/GuideRecordController.java

@@ -14,6 +14,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import javax.annotation.Resource;
 import java.util.Date;
 import java.util.Date;
 import java.util.List;
 import java.util.List;
+import com.etotem.cfc.common.RolesUtil;
 
 
 @Tag(name = "成长规划师记录", description = "成长规划师录入测评结果(含成员快照)")
 @Tag(name = "成长规划师记录", description = "成长规划师录入测评结果(含成员快照)")
 @RestController
 @RestController
@@ -36,8 +37,9 @@ public class GuideRecordController {
     public Result<DanAssessmentResult> createRecord(
     public Result<DanAssessmentResult> createRecord(
             @RequestAttribute("userId") Long userId,
             @RequestAttribute("userId") Long userId,
             @RequestAttribute("role") String role,
             @RequestAttribute("role") String role,
-            @RequestBody DanAssessmentResult result) {
-        if (!"teacher".equals(role)) {
+            @RequestBody DanAssessmentResult result,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles)) {
             return Result.error("只有成长规划师可以录入测评结果");
             return Result.error("只有成长规划师可以录入测评结果");
         }
         }
         result.setTeacherId(userId);
         result.setTeacherId(userId);

+ 10 - 6
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/PlanReviewController.java

@@ -19,6 +19,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import com.etotem.cfc.util.SortUtil;
 import com.etotem.cfc.util.SortUtil;
 import com.etotem.cfc.util.ParamUtils;
 import com.etotem.cfc.util.ParamUtils;
+import com.etotem.cfc.common.RolesUtil;
 
 
 @Tag(name = "指导师-方案审核", description = "指导师查看/审核/激活测评方案")
 @Tag(name = "指导师-方案审核", description = "指导师查看/审核/激活测评方案")
 @RestController
 @RestController
@@ -44,8 +45,9 @@ public class PlanReviewController {
     public Result<List<TaskPlanInstance>> listFamilyPlans(@RequestBody Map<String, Object> body,
     public Result<List<TaskPlanInstance>> listFamilyPlans(@RequestBody Map<String, Object> body,
                                                            @RequestAttribute("userId") Long userId,
                                                            @RequestAttribute("userId") Long userId,
                                                            @RequestAttribute("role") String role,
                                                            @RequestAttribute("role") String role,
-                                                           @RequestAttribute(value = "familyId", required = false) Long jwtFamilyId) {
-        if (!"teacher".equals(role) && !"admin".equals(role)) {
+                                                           @RequestAttribute(value = "familyId", required = false) Long jwtFamilyId,
+                                                           @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles) && !"admin".equals(role)) {
             return Result.error("无权限");
             return Result.error("无权限");
         }
         }
         String status = (String) body.get("status");
         String status = (String) body.get("status");
@@ -75,8 +77,9 @@ public class PlanReviewController {
     @PostMapping("/review")
     @PostMapping("/review")
     public Result<String> reviewPlan(@RequestBody Map<String, Object> body,
     public Result<String> reviewPlan(@RequestBody Map<String, Object> body,
                                      @RequestAttribute("userId") Long userId,
                                      @RequestAttribute("userId") Long userId,
-                                     @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role) && !"admin".equals(role)) {
+                                     @RequestAttribute("role") String role,
+                                     @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles) && !"admin".equals(role)) {
             return Result.error("无权限");
             return Result.error("无权限");
         }
         }
         Long planId = ParamUtils.getLong(body.get("planId"));
         Long planId = ParamUtils.getLong(body.get("planId"));
@@ -100,8 +103,9 @@ public class PlanReviewController {
     @PostMapping("/activate")
     @PostMapping("/activate")
     public Result<String> activatePlan(@RequestBody Map<String, Object> body,
     public Result<String> activatePlan(@RequestBody Map<String, Object> body,
                                        @RequestAttribute("userId") Long userId,
                                        @RequestAttribute("userId") Long userId,
-                                       @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role) && !"admin".equals(role)) {
+                                       @RequestAttribute("role") String role,
+                                       @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles) && !"admin".equals(role)) {
             return Result.error("无权限");
             return Result.error("无权限");
         }
         }
         // 认证闸门:规划师(基础类别)认证通过才可激活方案(下发任务)
         // 认证闸门:规划师(基础类别)认证通过才可激活方案(下发任务)

+ 10 - 6
cfc-backend/src/main/java/com/etotem/cfc/controller/guide/TeacherMessageController.java

@@ -15,6 +15,7 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
 import com.etotem.cfc.util.ParamUtils;
 import com.etotem.cfc.util.ParamUtils;
+import com.etotem.cfc.common.RolesUtil;
 
 
 @Tag(name = "成长规划师消息", description = "成长规划师接受家庭咨询的接口")
 @Tag(name = "成长规划师消息", description = "成长规划师接受家庭咨询的接口")
 @RestController
 @RestController
@@ -62,8 +63,9 @@ public class TeacherMessageController {
     public Result<Boolean> replyMessage(@RequestAttribute("userId") Long teacherId,
     public Result<Boolean> replyMessage(@RequestAttribute("userId") Long teacherId,
                                          @RequestAttribute("role") String role,
                                          @RequestAttribute("role") String role,
                                          @PathVariable Long messageId,
                                          @PathVariable Long messageId,
-                                         @RequestBody Map<String, String> body) {
-        if (!"teacher".equals(role)) {
+                                         @RequestBody Map<String, String> body,
+                                         @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles)) {
             return Result.error("只有成长规划师可以回复");
             return Result.error("只有成长规划师可以回复");
         }
         }
 
 
@@ -83,10 +85,11 @@ public class TeacherMessageController {
     @PostMapping("/list")
     @PostMapping("/list")
     public Result<Map<String, Object>> getMessages(@RequestAttribute("userId") Long teacherId,
     public Result<Map<String, Object>> getMessages(@RequestAttribute("userId") Long teacherId,
             @RequestAttribute("role") String role,
             @RequestAttribute("role") String role,
-            @RequestBody Map<String, Object> params) {
+            @RequestBody Map<String, Object> params,
+            @RequestAttribute(value = "roles", required = false) List<String> roles) {
         Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
         Integer page = params.get("page") != null ? Integer.valueOf(params.get("page").toString()) : 1;
         Integer size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;
         Integer size = params.get("size") != null ? Integer.valueOf(params.get("size").toString()) : 10;
-        if (!"teacher".equals(role)) {
+        if (!RolesUtil.hasTeacherRole(role, roles)) {
             return Result.error("只有成长规划师可以查看");
             return Result.error("只有成长规划师可以查看");
         }
         }
 
 
@@ -117,8 +120,9 @@ public class TeacherMessageController {
     @Operation(summary = "成长规划师获取未读消息数")
     @Operation(summary = "成长规划师获取未读消息数")
     @PostMapping("/unread-count")
     @PostMapping("/unread-count")
     public Result<Long> getUnreadCount(@RequestAttribute("userId") Long teacherId,
     public Result<Long> getUnreadCount(@RequestAttribute("userId") Long teacherId,
-                                        @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role)) {
+                                        @RequestAttribute("role") String role,
+                                        @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles)) {
             return Result.error("只有成长规划师可以查看");
             return Result.error("只有成长规划师可以查看");
         }
         }
         long count = teacherMessageService.getUnreadCount(teacherId);
         long count = teacherMessageService.getUnreadCount(teacherId);

+ 22 - 14
cfc-backend/src/main/java/com/etotem/cfc/controller/planner/PlannerCertificationController.java

@@ -10,6 +10,7 @@ import org.springframework.web.bind.annotation.*;
 import javax.annotation.Resource;
 import javax.annotation.Resource;
 import java.util.List;
 import java.util.List;
 import java.util.Map;
 import java.util.Map;
+import com.etotem.cfc.common.RolesUtil;
 
 
 /**
 /**
  * 规划师端:培训认证中心
  * 规划师端:培训认证中心
@@ -35,8 +36,9 @@ public class PlannerCertificationController {
     @Operation(summary = "我的全部类别认证状态")
     @Operation(summary = "我的全部类别认证状态")
     @PostMapping("/certification/my")
     @PostMapping("/certification/my")
     public Result<List<Map<String, Object>>> myCertifications(@RequestAttribute("userId") Long userId,
     public Result<List<Map<String, Object>>> myCertifications(@RequestAttribute("userId") Long userId,
-                                                              @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role) && !"admin".equals(role)) {
+                                                              @RequestAttribute("role") String role,
+                                                              @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles) && !"admin".equals(role)) {
             return Result.error(403, "无权限");
             return Result.error(403, "无权限");
         }
         }
         return Result.success(certificationService.listMyCertifications(userId));
         return Result.success(certificationService.listMyCertifications(userId));
@@ -46,8 +48,9 @@ public class PlannerCertificationController {
     @PostMapping("/courses")
     @PostMapping("/courses")
     public Result<List<PlannerCourse>> courses(@RequestBody Map<String, Object> body,
     public Result<List<PlannerCourse>> courses(@RequestBody Map<String, Object> body,
                                                @RequestAttribute("userId") Long userId,
                                                @RequestAttribute("userId") Long userId,
-                                               @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role) && !"admin".equals(role)) {
+                                               @RequestAttribute("role") String role,
+                                               @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles) && !"admin".equals(role)) {
             return Result.error(403, "无权限");
             return Result.error(403, "无权限");
         }
         }
         Integer categoryId = body.get("categoryId") != null ? Integer.parseInt(body.get("categoryId").toString()) : null;
         Integer categoryId = body.get("categoryId") != null ? Integer.parseInt(body.get("categoryId").toString()) : null;
@@ -60,8 +63,9 @@ public class PlannerCertificationController {
     @Operation(summary = "可报名/签到的培训活动列表")
     @Operation(summary = "可报名/签到的培训活动列表")
     @PostMapping("/training-activities")
     @PostMapping("/training-activities")
     public Result<List<Map<String, Object>>> trainingActivities(@RequestAttribute("userId") Long userId,
     public Result<List<Map<String, Object>>> trainingActivities(@RequestAttribute("userId") Long userId,
-                                                                @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role) && !"admin".equals(role)) {
+                                                                @RequestAttribute("role") String role,
+                                                                @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles) && !"admin".equals(role)) {
             return Result.error(403, "无权限");
             return Result.error(403, "无权限");
         }
         }
         return Result.success(offlineCourseService.listTrainingActivities(userId));
         return Result.success(offlineCourseService.listTrainingActivities(userId));
@@ -71,8 +75,9 @@ public class PlannerCertificationController {
     @PostMapping("/training-activities/check-in")
     @PostMapping("/training-activities/check-in")
     public Result<Boolean> checkIn(@RequestBody Map<String, Object> body,
     public Result<Boolean> checkIn(@RequestBody Map<String, Object> body,
                                    @RequestAttribute("userId") Long userId,
                                    @RequestAttribute("userId") Long userId,
-                                   @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role) && !"admin".equals(role)) {
+                                   @RequestAttribute("role") String role,
+                                   @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles) && !"admin".equals(role)) {
             return Result.error(403, "无权限");
             return Result.error(403, "无权限");
         }
         }
         Long activityId = com.etotem.cfc.util.ParamUtils.getLong(body.get("activityId"));
         Long activityId = com.etotem.cfc.util.ParamUtils.getLong(body.get("activityId"));
@@ -87,8 +92,9 @@ public class PlannerCertificationController {
     @PostMapping("/exam/start")
     @PostMapping("/exam/start")
     public Result<Map<String, Object>> startExam(@RequestBody Map<String, Object> body,
     public Result<Map<String, Object>> startExam(@RequestBody Map<String, Object> body,
                                                  @RequestAttribute("userId") Long userId,
                                                  @RequestAttribute("userId") Long userId,
-                                                 @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role) && !"admin".equals(role)) {
+                                                 @RequestAttribute("role") String role,
+                                                 @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles) && !"admin".equals(role)) {
             return Result.error(403, "无权限");
             return Result.error(403, "无权限");
         }
         }
         Integer categoryId = body.get("categoryId") != null ? Integer.parseInt(body.get("categoryId").toString()) : null;
         Integer categoryId = body.get("categoryId") != null ? Integer.parseInt(body.get("categoryId").toString()) : null;
@@ -113,8 +119,9 @@ public class PlannerCertificationController {
     @PostMapping("/exam/submit")
     @PostMapping("/exam/submit")
     public Result<Map<String, Object>> submitExam(@RequestBody Map<String, Object> body,
     public Result<Map<String, Object>> submitExam(@RequestBody Map<String, Object> body,
                                                   @RequestAttribute("userId") Long userId,
                                                   @RequestAttribute("userId") Long userId,
-                                                  @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role) && !"admin".equals(role)) {
+                                                  @RequestAttribute("role") String role,
+                                                  @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles) && !"admin".equals(role)) {
             return Result.error(403, "无权限");
             return Result.error(403, "无权限");
         }
         }
         Long attemptId = com.etotem.cfc.util.ParamUtils.getLong(body.get("attemptId"));
         Long attemptId = com.etotem.cfc.util.ParamUtils.getLong(body.get("attemptId"));
@@ -133,8 +140,9 @@ public class PlannerCertificationController {
     @PostMapping("/exam/result")
     @PostMapping("/exam/result")
     public Result<Object> examResult(@RequestBody Map<String, Object> body,
     public Result<Object> examResult(@RequestBody Map<String, Object> body,
                                      @RequestAttribute("userId") Long userId,
                                      @RequestAttribute("userId") Long userId,
-                                     @RequestAttribute("role") String role) {
-        if (!"teacher".equals(role) && !"admin".equals(role)) {
+                                     @RequestAttribute("role") String role,
+                                     @RequestAttribute(value = "roles", required = false) List<String> roles) {
+        if (!RolesUtil.hasTeacherRole(role, roles) && !"admin".equals(role)) {
             return Result.error(403, "无权限");
             return Result.error(403, "无权限");
         }
         }
         Integer categoryId = body.get("categoryId") != null ? Integer.parseInt(body.get("categoryId").toString()) : null;
         Integer categoryId = body.get("categoryId") != null ? Integer.parseInt(body.get("categoryId").toString()) : null;

+ 4 - 2
cfc-backend/src/main/java/com/etotem/cfc/service/GuideFamilyAccessGuard.java

@@ -4,6 +4,8 @@ import com.etotem.cfc.common.Result;
 import org.springframework.stereotype.Component;
 import org.springframework.stereotype.Component;
 
 
 import javax.annotation.Resource;
 import javax.annotation.Resource;
+import com.etotem.cfc.common.RolesUtil;
+import java.util.List;
 
 
 /**
 /**
  * 规划师访问客户家庭的统一授权判定。
  * 规划师访问客户家庭的统一授权判定。
@@ -39,8 +41,8 @@ public class GuideFamilyAccessGuard {
      *
      *
      * @return null 表示放行;非 null 为 403 响应
      * @return null 表示放行;非 null 为 403 响应
      */
      */
-    public Result<Void> checkFamilyAccess(String role, Long userId, Long familyId) {
-        if (!"teacher".equals(role) && !"admin".equals(role)) {
+    public Result<Void> checkFamilyAccess(String role, List<String> roles, Long userId, Long familyId) {
+        if (!RolesUtil.hasTeacherRole(role, roles) && !"admin".equals(role)) {
             return Result.error(403, DENIED);
             return Result.error(403, DENIED);
         }
         }
         if ("admin".equals(role)) {
         if ("admin".equals(role)) {

+ 5 - 4
cfc-backend/src/main/java/com/etotem/cfc/service/InviteCardService.java

@@ -10,6 +10,7 @@ import org.springframework.stereotype.Service;
 import javax.annotation.Resource;
 import javax.annotation.Resource;
 import java.time.LocalDateTime;
 import java.time.LocalDateTime;
 import java.util.*;
 import java.util.*;
+import com.etotem.cfc.common.RolesUtil;
 
 
 @Service
 @Service
 public class InviteCardService {
 public class InviteCardService {
@@ -161,7 +162,7 @@ private String generateCode(String type) {
         inviteCardMapper.updateById(card);
         inviteCardMapper.updateById(card);
 
 
         Map<String, Object> result = new HashMap<>();
         Map<String, Object> result = new HashMap<>();
-        result.put("token", jwtConfig.generateToken(user.getId(), user.getRole()));
+        result.put("token", jwtConfig.generateToken(user.getId(), RolesUtil.effectiveRoles(user)));
         result.put("userId", user.getId());
         result.put("userId", user.getId());
         result.put("role", "parent"); // 不可登录成员 role 统一 parent(新模型)
         result.put("role", "parent"); // 不可登录成员 role 统一 parent(新模型)
         result.put("familyId", user.getFamilyId());
         result.put("familyId", user.getFamilyId());
@@ -198,7 +199,7 @@ private String generateCode(String type) {
         inviteCardMapper.updateById(card);
         inviteCardMapper.updateById(card);
 
 
         Map<String, Object> result = new HashMap<>();
         Map<String, Object> result = new HashMap<>();
-        result.put("token", jwtConfig.generateToken(user.getId(), user.getRole()));
+        result.put("token", jwtConfig.generateToken(user.getId(), RolesUtil.effectiveRoles(user)));
         result.put("userId", user.getId());
         result.put("userId", user.getId());
         result.put("role", user.getRole());
         result.put("role", user.getRole());
         result.put("familyId", user.getFamilyId());
         result.put("familyId", user.getFamilyId());
@@ -248,7 +249,7 @@ private String generateCode(String type) {
         inviteCardMapper.updateById(card);
         inviteCardMapper.updateById(card);
 
 
         result.put("needsTeacherRegister", false);
         result.put("needsTeacherRegister", false);
-        result.put("token", jwtConfig.generateToken(user.getId(), user.getRole()));
+        result.put("token", jwtConfig.generateToken(user.getId(), RolesUtil.effectiveRoles(user)));
         result.put("userId", user.getId());
         result.put("userId", user.getId());
         result.put("role", "teacher");
         result.put("role", "teacher");
         result.put("teacherFamilyIds", updatedIds);
         result.put("teacherFamilyIds", updatedIds);
@@ -339,7 +340,7 @@ private String generateCode(String type) {
         inviteCardMapper.updateById(card);
         inviteCardMapper.updateById(card);
 
 
         Map<String, Object> result = new HashMap<>();
         Map<String, Object> result = new HashMap<>();
-        result.put("token", jwtConfig.generateToken(parent.getId(), parent.getRole()));
+        result.put("token", jwtConfig.generateToken(parent.getId(), RolesUtil.effectiveRoles(parent)));
         result.put("userId", parent.getId());
         result.put("userId", parent.getId());
         result.put("role", parent.getRole());
         result.put("role", parent.getRole());
         result.put("familyId", parent.getFamilyId());
         result.put("familyId", parent.getFamilyId());

+ 11 - 10
cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java

@@ -36,6 +36,7 @@ import com.etotem.cfc.entity.Consignee;
 import com.etotem.cfc.mapper.ConsigneeMapper;
 import com.etotem.cfc.mapper.ConsigneeMapper;
 import com.etotem.cfc.util.SortUtil;
 import com.etotem.cfc.util.SortUtil;
 import com.etotem.cfc.util.UserUtil;
 import com.etotem.cfc.util.UserUtil;
+import com.etotem.cfc.common.RolesUtil;
 
 
 
 
 @Slf4j
 @Slf4j
@@ -165,7 +166,7 @@ private FamilyInvitationService familyInvitationService;
         }
         }
         
         
         // 生成 token
         // 生成 token
-        String token = jwtConfig.generateToken(user.getId(), user.getRole());
+        String token = jwtConfig.generateToken(user.getId(), RolesUtil.effectiveRoles(user));
 
 
         LoginResultDTO result = new LoginResultDTO();
         LoginResultDTO result = new LoginResultDTO();
         result.setToken(token);
         result.setToken(token);
@@ -244,7 +245,7 @@ private FamilyInvitationService familyInvitationService;
   ensureUserHasFamily(user);
   ensureUserHasFamily(user);
 
 
   // 生成token
   // 生成token
-  String token = jwtConfig.generateToken(user.getId(), user.getRole());
+  String token = jwtConfig.generateToken(user.getId(), RolesUtil.effectiveRoles(user));
 
 
   LoginResultDTO result = new LoginResultDTO();
   LoginResultDTO result = new LoginResultDTO();
   result.setToken(token);
   result.setToken(token);
@@ -303,7 +304,7 @@ private FamilyInvitationService familyInvitationService;
         ensureUserHasFamily(user);
         ensureUserHasFamily(user);
 
 
         // 4. 生成token
         // 4. 生成token
-        String token = jwtConfig.generateToken(user.getId(), user.getRole());
+        String token = jwtConfig.generateToken(user.getId(), RolesUtil.effectiveRoles(user));
 
 
         LoginResultDTO result = new LoginResultDTO();
         LoginResultDTO result = new LoginResultDTO();
         result.setToken(token);
         result.setToken(token);
@@ -344,7 +345,7 @@ private FamilyInvitationService familyInvitationService;
         }
         }
 
 
         ensureUserHasFamily(user);
         ensureUserHasFamily(user);
-        String token = jwtConfig.generateToken(user.getId(), user.getRole());
+        String token = jwtConfig.generateToken(user.getId(), RolesUtil.effectiveRoles(user));
 
 
         LoginResultDTO result = new LoginResultDTO();
         LoginResultDTO result = new LoginResultDTO();
         result.setToken(token);
         result.setToken(token);
@@ -599,7 +600,7 @@ private FamilyInvitationService familyInvitationService;
         }
         }
 
 
         // 生成token
         // 生成token
-        String token = jwtConfig.generateToken(user.getId(), user.getRole());
+        String token = jwtConfig.generateToken(user.getId(), RolesUtil.effectiveRoles(user));
 
 
         LoginResultDTO result = new LoginResultDTO();
         LoginResultDTO result = new LoginResultDTO();
         result.setToken(token);
         result.setToken(token);
@@ -689,7 +690,7 @@ private FamilyInvitationService familyInvitationService;
         UserUtil.fillDefaultIsChild(user);
         UserUtil.fillDefaultIsChild(user);
         user.setUpdatedAt(new Date());
         user.setUpdatedAt(new Date());
         userMapper.updateById(user);
         userMapper.updateById(user);
-        String token = jwtConfig.generateToken(user.getId(), user.getRole());
+        String token = jwtConfig.generateToken(user.getId(), RolesUtil.effectiveRoles(user));
         LoginResultDTO result = new LoginResultDTO();
         LoginResultDTO result = new LoginResultDTO();
         result.setToken(token);
         result.setToken(token);
         result.setUserId(user.getId());
         result.setUserId(user.getId());
@@ -726,7 +727,7 @@ private FamilyInvitationService familyInvitationService;
         }
         }
         ensureUserHasFamily(user);
         ensureUserHasFamily(user);
 
 
-        String token = jwtConfig.generateToken(user.getId(), user.getRole());
+        String token = jwtConfig.generateToken(user.getId(), RolesUtil.effectiveRoles(user));
         LoginResultDTO result = new LoginResultDTO();
         LoginResultDTO result = new LoginResultDTO();
         result.setToken(token);
         result.setToken(token);
         result.setUserId(user.getId());
         result.setUserId(user.getId());
@@ -1051,7 +1052,7 @@ private FamilyInvitationService familyInvitationService;
         ensureUserHasFamily(user);
         ensureUserHasFamily(user);
 
 
         // 生成token
         // 生成token
-        String token = jwtConfig.generateToken(user.getId(), user.getRole());
+        String token = jwtConfig.generateToken(user.getId(), RolesUtil.effectiveRoles(user));
 
 
         LoginResultDTO result = new LoginResultDTO();
         LoginResultDTO result = new LoginResultDTO();
         result.setToken(token);
         result.setToken(token);
@@ -1660,7 +1661,7 @@ FamilyMember child = familyMemberMapper.selectById(memberId);
             distributionService.registerByInviteCode(inviteCode, user.getId());
             distributionService.registerByInviteCode(inviteCode, user.getId());
             ensureUserHasFamily(user);
             ensureUserHasFamily(user);
 
 
-            String token = jwtConfig.generateToken(user.getId(), user.getRole());
+            String token = jwtConfig.generateToken(user.getId(), RolesUtil.effectiveRoles(user));
 
 
             LoginResultDTO result = new LoginResultDTO();
             LoginResultDTO result = new LoginResultDTO();
             result.setToken(token);
             result.setToken(token);
@@ -1728,7 +1729,7 @@ FamilyMember child = familyMemberMapper.selectById(memberId);
         ensureUserHasFamily(user);
         ensureUserHasFamily(user);
 
 
         // 生成token
         // 生成token
-        String token = jwtConfig.generateToken(user.getId(), user.getRole());
+        String token = jwtConfig.generateToken(user.getId(), RolesUtil.effectiveRoles(user));
 
 
         LoginResultDTO result = new LoginResultDTO();
         LoginResultDTO result = new LoginResultDTO();
         result.setToken(token);
         result.setToken(token);

+ 2 - 2
cfc-backend/src/test/java/com/etotem/cfc/controller/HealthPlanDeleteControllerTest.java

@@ -82,13 +82,13 @@ class HealthPlanDeleteControllerTest {
     @Test
     @Test
     void 管理端_方案不存在返回方案不存在() {
     void 管理端_方案不存在返回方案不存在() {
         when(planService.deletePlanCascade(99L, null)).thenThrow(new IllegalStateException("方案不存在"));
         when(planService.deletePlanCascade(99L, null)).thenThrow(new IllegalStateException("方案不存在"));
-        Result<?> r = adminController.deletePlan(params(99L), "admin");
+        Result<?> r = adminController.deletePlan(params(99L), "admin", null);
         assertEquals("方案不存在", r.getMessage());
         assertEquals("方案不存在", r.getMessage());
     }
     }
 
 
     @Test
     @Test
     void 管理端_planId为空返回错误() {
     void 管理端_planId为空返回错误() {
-        Result<?> r = adminController.deletePlan(params(null), "admin");
+        Result<?> r = adminController.deletePlan(params(null), "admin", null);
         assertEquals("planId不能为空", r.getMessage());
         assertEquals("planId不能为空", r.getMessage());
         verify(planService, never()).deletePlanCascade(any(), any());
         verify(planService, never()).deletePlanCascade(any(), any());
     }
     }

+ 20 - 20
cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideFamilyTaskControllerTest.java

@@ -80,14 +80,14 @@ public class GuideFamilyTaskControllerTest {
     @Test
     @Test
     public void getBoundFamilies_TeacherRole_Success() {
     public void getBoundFamilies_TeacherRole_Success() {
         when(userMapper.selectList(any())).thenReturn(Collections.emptyList());
         when(userMapper.selectList(any())).thenReturn(Collections.emptyList());
-        Result<?> result = controller.getBoundFamilies(1L, "teacher");
+        Result<?> result = controller.getBoundFamilies(1L, "teacher", null);
         assertNotNull(result);
         assertNotNull(result);
     }
     }
 
 
     @Test
     @Test
     public void getBoundFamilies_NonTeacherRole_AccessDenied() {
     public void getBoundFamilies_NonTeacherRole_AccessDenied() {
         for (String role : NON_TEACHER_ROLES) {
         for (String role : NON_TEACHER_ROLES) {
-            Result<?> result = controller.getBoundFamilies(2L, role);
+            Result<?> result = controller.getBoundFamilies(2L, role, null);
             assertRoleDenied(result);
             assertRoleDenied(result);
         }
         }
     }
     }
@@ -98,7 +98,7 @@ public class GuideFamilyTaskControllerTest {
         params.put("page", 1);
         params.put("page", 1);
         params.put("size", 10);
         params.put("size", 10);
         for (String role : NON_TEACHER_ROLES) {
         for (String role : NON_TEACHER_ROLES) {
-            Result<?> result = controller.getFamilyMemberTasks(1L, 100L, 20L, role, params);
+            Result<?> result = controller.getFamilyMemberTasks(1L, 100L, 20L, role, params, null);
             assertRoleDenied(result);
             assertRoleDenied(result);
         }
         }
     }
     }
@@ -106,7 +106,7 @@ public class GuideFamilyTaskControllerTest {
     @Test
     @Test
     public void getPendingReviewTasks_NonTeacherRole_AccessDenied() {
     public void getPendingReviewTasks_NonTeacherRole_AccessDenied() {
         for (String role : NON_TEACHER_ROLES) {
         for (String role : NON_TEACHER_ROLES) {
-            Result<?> result = controller.getPendingReviewTasks(1L, 100L, 20L, role);
+            Result<?> result = controller.getPendingReviewTasks(1L, 100L, 20L, role, null);
             assertRoleDenied(result);
             assertRoleDenied(result);
         }
         }
     }
     }
@@ -116,7 +116,7 @@ public class GuideFamilyTaskControllerTest {
         TaskReviewDTO dto = new TaskReviewDTO();
         TaskReviewDTO dto = new TaskReviewDTO();
         dto.setApproved(true);
         dto.setApproved(true);
         for (String role : NON_TEACHER_ROLES) {
         for (String role : NON_TEACHER_ROLES) {
-            Result<?> result = controller.reviewTask(1L, 100L, 1L, role, dto);
+            Result<?> result = controller.reviewTask(1L, 100L, 1L, role, dto, null);
             assertRoleDenied(result);
             assertRoleDenied(result);
         }
         }
     }
     }
@@ -127,7 +127,7 @@ public class GuideFamilyTaskControllerTest {
         params.put("taskIds", Arrays.asList(1L, 2L));
         params.put("taskIds", Arrays.asList(1L, 2L));
         params.put("approved", true);
         params.put("approved", true);
         for (String role : NON_TEACHER_ROLES) {
         for (String role : NON_TEACHER_ROLES) {
-            Result<?> result = controller.batchReviewTasks(1L, 100L, role, params);
+            Result<?> result = controller.batchReviewTasks(1L, 100L, role, params, null);
             assertRoleDenied(result);
             assertRoleDenied(result);
         }
         }
     }
     }
@@ -137,7 +137,7 @@ public class GuideFamilyTaskControllerTest {
         Map<String, Object> taskData = new HashMap<>();
         Map<String, Object> taskData = new HashMap<>();
         taskData.put("title", "新标题");
         taskData.put("title", "新标题");
         for (String role : NON_TEACHER_ROLES) {
         for (String role : NON_TEACHER_ROLES) {
-            Result<?> result = controller.updateTask(1L, 100L, 1L, role, taskData);
+            Result<?> result = controller.updateTask(1L, 100L, 1L, role, taskData, null);
             assertRoleDenied(result);
             assertRoleDenied(result);
         }
         }
     }
     }
@@ -145,7 +145,7 @@ public class GuideFamilyTaskControllerTest {
     @Test
     @Test
     public void deleteTask_NonTeacherRole_AccessDenied() {
     public void deleteTask_NonTeacherRole_AccessDenied() {
         for (String role : NON_TEACHER_ROLES) {
         for (String role : NON_TEACHER_ROLES) {
-            Result<?> result = controller.deleteTask(1L, 100L, 1L, role);
+            Result<?> result = controller.deleteTask(1L, 100L, 1L, role, null);
             assertRoleDenied(result);
             assertRoleDenied(result);
         }
         }
     }
     }
@@ -167,7 +167,7 @@ public class GuideFamilyTaskControllerTest {
         when(taskService.getPendingReviewTasksForChild(20L)).thenReturn(new ArrayList<>());
         when(taskService.getPendingReviewTasksForChild(20L)).thenReturn(new ArrayList<>());
 
 
         @SuppressWarnings("unchecked")
         @SuppressWarnings("unchecked")
-        Result<List<Map<String, Object>>> result = (Result<List<Map<String, Object>>>) (Result<?>) controller.getBoundFamilies(guideId, "teacher");
+        Result<List<Map<String, Object>>> result = (Result<List<Map<String, Object>>>) (Result<?>) controller.getBoundFamilies(guideId, "teacher", null);
         assertNotNull(result);
         assertNotNull(result);
         assertEquals(200, result.getCode());
         assertEquals(200, result.getCode());
     }
     }
@@ -183,7 +183,7 @@ public class GuideFamilyTaskControllerTest {
         when(taskService.getPendingReviewTasksForFamilyMember(20L)).thenReturn(new ArrayList<>());
         when(taskService.getPendingReviewTasksForFamilyMember(20L)).thenReturn(new ArrayList<>());
         when(taskService.getAllTasksByFamilyMember(20L)).thenReturn(new ArrayList<>());
         when(taskService.getAllTasksByFamilyMember(20L)).thenReturn(new ArrayList<>());
 
 
-        Result<Map<String, Object>> result = controller.getFamilyTaskOverview(guideId, "teacher", familyId);
+        Result<Map<String, Object>> result = controller.getFamilyTaskOverview(guideId, "teacher", familyId, null);
         Map<String, Object> data = result.getData();
         Map<String, Object> data = result.getData();
         assertNotNull(data);
         assertNotNull(data);
         assertEquals(familyId, data.get("familyId"));
         assertEquals(familyId, data.get("familyId"));
@@ -194,7 +194,7 @@ public class GuideFamilyTaskControllerTest {
 
 
     @Test
     @Test
     public void getFamilyTaskOverview_NonTeacher_AccessDenied() {
     public void getFamilyTaskOverview_NonTeacher_AccessDenied() {
-        Result<Map<String, Object>> result = controller.getFamilyTaskOverview(1L, "parent", 100L);
+        Result<Map<String, Object>> result = controller.getFamilyTaskOverview(1L, "parent", 100L, null);
         assertEquals("Access denied", result.getMessage());
         assertEquals("Access denied", result.getMessage());
     }
     }
 
 
@@ -217,7 +217,7 @@ public class GuideFamilyTaskControllerTest {
         params.put("page", 1);
         params.put("page", 1);
         params.put("size", 10);
         params.put("size", 10);
 
 
-        Result<Page<Task>> result = controller.getFamilyMemberTasks(1L, familyId, memberId, "teacher", params);
+        Result<Page<Task>> result = controller.getFamilyMemberTasks(1L, familyId, memberId, "teacher", params, null);
         Page<Task> page = result.getData();
         Page<Task> page = result.getData();
         assertEquals(2, page.getTotal());
         assertEquals(2, page.getTotal());
         assertEquals(2, page.getRecords().size());
         assertEquals(2, page.getRecords().size());
@@ -232,7 +232,7 @@ public class GuideFamilyTaskControllerTest {
         );
         );
         when(taskService.getPendingReviewTasksForFamilyMember(memberId)).thenReturn(pendingTasks);
         when(taskService.getPendingReviewTasksForFamilyMember(memberId)).thenReturn(pendingTasks);
 
 
-        Result<List<Task>> result = controller.getPendingReviewTasks(1L, familyId, memberId, "teacher");
+        Result<List<Task>> result = controller.getPendingReviewTasks(1L, familyId, memberId, "teacher", null);
         assertEquals(1, result.getData().size());
         assertEquals(1, result.getData().size());
     }
     }
 
 
@@ -249,7 +249,7 @@ public class GuideFamilyTaskControllerTest {
         when(taskMapper.selectById(taskId)).thenReturn(mockTask(taskId, 20L, familyId, "pending_review"));
         when(taskMapper.selectById(taskId)).thenReturn(mockTask(taskId, 20L, familyId, "pending_review"));
         when(taskService.reviewTask(eq(taskId), any(TaskReviewDTO.class))).thenReturn(true);
         when(taskService.reviewTask(eq(taskId), any(TaskReviewDTO.class))).thenReturn(true);
 
 
-        Result<Boolean> result = controller.reviewTask(1L, familyId, taskId, "teacher", dto);
+        Result<Boolean> result = controller.reviewTask(1L, familyId, taskId, "teacher", dto, null);
         assertTrue(result.getData());
         assertTrue(result.getData());
     }
     }
 
 
@@ -265,7 +265,7 @@ public class GuideFamilyTaskControllerTest {
         when(taskMapper.selectById(anyLong())).thenReturn(mockTask(1L, 20L, familyId, "pending_review"));
         when(taskMapper.selectById(anyLong())).thenReturn(mockTask(1L, 20L, familyId, "pending_review"));
         when(taskService.reviewTask(anyLong(), any(TaskReviewDTO.class))).thenReturn(true);
         when(taskService.reviewTask(anyLong(), any(TaskReviewDTO.class))).thenReturn(true);
 
 
-        Result<Map<String, Object>> result = controller.batchReviewTasks(1L, familyId, "teacher", params);
+        Result<Map<String, Object>> result = controller.batchReviewTasks(1L, familyId, "teacher", params, null);
         Map<String, Object> data = result.getData();
         Map<String, Object> data = result.getData();
         assertEquals(3, data.get("successCount"));
         assertEquals(3, data.get("successCount"));
         assertEquals(0, data.get("failCount"));
         assertEquals(0, data.get("failCount"));
@@ -284,7 +284,7 @@ public class GuideFamilyTaskControllerTest {
         taskData.put("description", "新描述");
         taskData.put("description", "新描述");
         taskData.put("points", 20);
         taskData.put("points", 20);
 
 
-        Result<Boolean> result = controller.updateTask(1L, familyId, taskId, "teacher", taskData);
+        Result<Boolean> result = controller.updateTask(1L, familyId, taskId, "teacher", taskData, null);
         assertTrue(result.getData());
         assertTrue(result.getData());
     }
     }
 
 
@@ -294,7 +294,7 @@ public class GuideFamilyTaskControllerTest {
 
 
         Map<String, Object> taskData = new HashMap<>();
         Map<String, Object> taskData = new HashMap<>();
         taskData.put("title", "新标题");
         taskData.put("title", "新标题");
-        Result<Boolean> result = controller.updateTask(1L, 100L, 999L, "teacher", taskData);
+        Result<Boolean> result = controller.updateTask(1L, 100L, 999L, "teacher", taskData, null);
         assertEquals("任务不存在", result.getMessage());
         assertEquals("任务不存在", result.getMessage());
     }
     }
 
 
@@ -306,7 +306,7 @@ public class GuideFamilyTaskControllerTest {
 
 
         Map<String, Object> taskData = new HashMap<>();
         Map<String, Object> taskData = new HashMap<>();
         taskData.put("title", "新标题");
         taskData.put("title", "新标题");
-        Result<Boolean> result = controller.updateTask(1L, 100L, taskId, "teacher", taskData);
+        Result<Boolean> result = controller.updateTask(1L, 100L, taskId, "teacher", taskData, null);
         assertEquals("任务不属于该家庭", result.getMessage());
         assertEquals("任务不属于该家庭", result.getMessage());
     }
     }
 
 
@@ -318,14 +318,14 @@ public class GuideFamilyTaskControllerTest {
         when(taskMapper.selectById(taskId)).thenReturn(task);
         when(taskMapper.selectById(taskId)).thenReturn(task);
         when(taskMapper.updateById(any(Task.class))).thenReturn(1);
         when(taskMapper.updateById(any(Task.class))).thenReturn(1);
 
 
-        Result<Boolean> result = controller.deleteTask(1L, familyId, taskId, "teacher");
+        Result<Boolean> result = controller.deleteTask(1L, familyId, taskId, "teacher", null);
         assertTrue(result.getData());
         assertTrue(result.getData());
     }
     }
 
 
     @Test
     @Test
     public void deleteTask_NotFound_ReturnsError() {
     public void deleteTask_NotFound_ReturnsError() {
         when(taskMapper.selectById(999L)).thenReturn(null);
         when(taskMapper.selectById(999L)).thenReturn(null);
-        Result<Boolean> result = controller.deleteTask(1L, 100L, 999L, "teacher");
+        Result<Boolean> result = controller.deleteTask(1L, 100L, 999L, "teacher", null);
         assertEquals("任务不存在", result.getMessage());
         assertEquals("任务不存在", result.getMessage());
     }
     }
 }
 }

+ 9 - 9
cfc-backend/src/test/java/com/etotem/cfc/integration/controller/GuideRolePermissionTest.java

@@ -161,19 +161,19 @@ public class GuideRolePermissionTest {
         when(userMapper.selectById(1L)).thenReturn(guide);
         when(userMapper.selectById(1L)).thenReturn(guide);
         when(userMapper.selectList(any())).thenReturn(Collections.emptyList());
         when(userMapper.selectList(any())).thenReturn(Collections.emptyList());
 
 
-        Result<?> result = guideFamilyTaskController.getBoundFamilies(1L, "teacher");
+        Result<?> result = guideFamilyTaskController.getBoundFamilies(1L, "teacher", null);
         assertEquals(200, result.getCode());
         assertEquals(200, result.getCode());
     }
     }
 
 
     @Test
     @Test
     public void boundFamilies_ParentDenied() {
     public void boundFamilies_ParentDenied() {
-        Result<?> result = guideFamilyTaskController.getBoundFamilies(2L, "parent");
+        Result<?> result = guideFamilyTaskController.getBoundFamilies(2L, "parent", null);
         assertEquals("Access denied", result.getMessage());
         assertEquals("Access denied", result.getMessage());
     }
     }
 
 
     @Test
     @Test
     public void boundFamilies_ChildDenied() {
     public void boundFamilies_ChildDenied() {
-        Result<?> result = guideFamilyTaskController.getBoundFamilies(3L, "child");
+        Result<?> result = guideFamilyTaskController.getBoundFamilies(3L, "child", null);
         assertEquals("Access denied", result.getMessage());
         assertEquals("Access denied", result.getMessage());
     }
     }
 
 
@@ -186,14 +186,14 @@ public class GuideRolePermissionTest {
         when(taskService.getPendingReviewTasksForChild(20L)).thenReturn(new ArrayList<>());
         when(taskService.getPendingReviewTasksForChild(20L)).thenReturn(new ArrayList<>());
         when(taskService.getAllTasksByChild(20L)).thenReturn(new ArrayList<>());
         when(taskService.getAllTasksByChild(20L)).thenReturn(new ArrayList<>());
 
 
-        Result<Map<String, Object>> result = guideFamilyTaskController.getFamilyTaskOverview(1L, "teacher", 100L);
+        Result<Map<String, Object>> result = guideFamilyTaskController.getFamilyTaskOverview(1L, "teacher", 100L, null);
         assertEquals(200, result.getCode());
         assertEquals(200, result.getCode());
         assertEquals(100L, result.getData().get("familyId"));
         assertEquals(100L, result.getData().get("familyId"));
     }
     }
 
 
     @Test
     @Test
     public void familyTaskOverview_AdminDenied() {
     public void familyTaskOverview_AdminDenied() {
-        Result<Map<String, Object>> result = guideFamilyTaskController.getFamilyTaskOverview(1L, "admin", 100L);
+        Result<Map<String, Object>> result = guideFamilyTaskController.getFamilyTaskOverview(1L, "admin", 100L, null);
         assertEquals("Access denied", result.getMessage());
         assertEquals("Access denied", result.getMessage());
     }
     }
 
 
@@ -211,7 +211,7 @@ public class GuideRolePermissionTest {
         when(taskMapper.selectById(1L)).thenReturn(reviewTask);
         when(taskMapper.selectById(1L)).thenReturn(reviewTask);
         when(taskService.reviewTask(eq(1L), any(TaskReviewDTO.class))).thenReturn(true);
         when(taskService.reviewTask(eq(1L), any(TaskReviewDTO.class))).thenReturn(true);
 
 
-        Result<Boolean> result = guideFamilyTaskController.reviewTask(1L, 100L, 1L, "teacher", dto);
+        Result<Boolean> result = guideFamilyTaskController.reviewTask(1L, 100L, 1L, "teacher", dto, null);
         assertTrue(result.getData());
         assertTrue(result.getData());
     }
     }
 
 
@@ -220,7 +220,7 @@ public class GuideRolePermissionTest {
         TaskReviewDTO dto = new TaskReviewDTO();
         TaskReviewDTO dto = new TaskReviewDTO();
         dto.setApproved(true);
         dto.setApproved(true);
 
 
-        Result<Boolean> result = guideFamilyTaskController.reviewTask(1L, 100L, 1L, "parent", dto);
+        Result<Boolean> result = guideFamilyTaskController.reviewTask(1L, 100L, 1L, "parent", dto, null);
         assertEquals("Access denied", result.getMessage());
         assertEquals("Access denied", result.getMessage());
     }
     }
 
 
@@ -238,7 +238,7 @@ public class GuideRolePermissionTest {
         when(taskMapper.selectById(anyLong())).thenReturn(batchTask);
         when(taskMapper.selectById(anyLong())).thenReturn(batchTask);
         when(taskService.reviewTask(anyLong(), any(TaskReviewDTO.class))).thenReturn(true);
         when(taskService.reviewTask(anyLong(), any(TaskReviewDTO.class))).thenReturn(true);
 
 
-        Result<Map<String, Object>> result = guideFamilyTaskController.batchReviewTasks(1L, 100L, "teacher", params);
+        Result<Map<String, Object>> result = guideFamilyTaskController.batchReviewTasks(1L, 100L, "teacher", params, null);
         assertEquals(200, result.getCode());
         assertEquals(200, result.getCode());
         assertEquals(2, result.getData().get("successCount"));
         assertEquals(2, result.getData().get("successCount"));
     }
     }
@@ -249,7 +249,7 @@ public class GuideRolePermissionTest {
         params.put("taskIds", Arrays.asList(1L));
         params.put("taskIds", Arrays.asList(1L));
         params.put("approved", true);
         params.put("approved", true);
 
 
-        Result<Map<String, Object>> result = guideFamilyTaskController.batchReviewTasks(1L, 100L, "child", params);
+        Result<Map<String, Object>> result = guideFamilyTaskController.batchReviewTasks(1L, 100L, "child", params, null);
         assertEquals("Access denied", result.getMessage());
         assertEquals("Access denied", result.getMessage());
     }
     }
 
 

+ 1 - 1
cfc-backend/src/test/java/com/etotem/cfc/unit/AssessmentQuotaAppointmentContractTest.java

@@ -448,7 +448,7 @@ class AssessmentQuotaAppointmentContractTest extends AssessmentPlanContractTestB
         inject(c, "danAssessmentExecutionService", mock(DanAssessmentExecutionService.class));
         inject(c, "danAssessmentExecutionService", mock(DanAssessmentExecutionService.class));
         inject(c, "userMapper", mock(UserMapper.class));
         inject(c, "userMapper", mock(UserMapper.class));
 
 
-        Result<?> r = c.getGuideAppointments(1L, "parent");
+        Result<?> r = c.getGuideAppointments(1L, "parent", null);
         assertEquals(500, r.getCode());
         assertEquals(500, r.getCode());
         assertTrue(r.getMessage().contains("只有成长规划师"));
         assertTrue(r.getMessage().contains("只有成长规划师"));
     }
     }

+ 7 - 7
cfc-backend/src/test/java/com/etotem/cfc/unit/GuideBindingAccessContractTest.java

@@ -58,7 +58,7 @@ class GuideBindingAccessContractTest extends AssessmentPlanContractTestBase {
         inject(guard, "guideFamilyService", guideFamilySvc);
         inject(guard, "guideFamilyService", guideFamilySvc);
 
 
         when(guideFamilySvc.isBound(7L, 1L)).thenReturn(true);
         when(guideFamilySvc.isBound(7L, 1L)).thenReturn(true);
-        assertNull(guard.checkFamilyAccess("teacher", 7L, 1L));
+        assertNull(guard.checkFamilyAccess("teacher", null, 7L, 1L));
     }
     }
 
 
     @Test
     @Test
@@ -69,7 +69,7 @@ class GuideBindingAccessContractTest extends AssessmentPlanContractTestBase {
         inject(guard, "guideFamilyService", guideFamilySvc);
         inject(guard, "guideFamilyService", guideFamilySvc);
 
 
         when(guideFamilySvc.isBound(7L, 1L)).thenReturn(false);
         when(guideFamilySvc.isBound(7L, 1L)).thenReturn(false);
-        Result<Void> r = guard.checkFamilyAccess("teacher", 7L, 1L);
+        Result<Void> r = guard.checkFamilyAccess("teacher", null, 7L, 1L);
         assertEquals(403, r.getCode());
         assertEquals(403, r.getCode());
         assertTrue(r.getMessage().contains("无权访问该家庭数据"));
         assertTrue(r.getMessage().contains("无权访问该家庭数据"));
     }
     }
@@ -81,7 +81,7 @@ class GuideBindingAccessContractTest extends AssessmentPlanContractTestBase {
         GuideFamilyAccessGuard guard = new GuideFamilyAccessGuard();
         GuideFamilyAccessGuard guard = new GuideFamilyAccessGuard();
         inject(guard, "guideFamilyService", guideFamilySvc);
         inject(guard, "guideFamilyService", guideFamilySvc);
 
 
-        Result<Void> r = guard.checkFamilyAccess("parent", 7L, 1L);
+        Result<Void> r = guard.checkFamilyAccess("parent", null, 7L, 1L);
         assertEquals(403, r.getCode());
         assertEquals(403, r.getCode());
     }
     }
 
 
@@ -92,7 +92,7 @@ class GuideBindingAccessContractTest extends AssessmentPlanContractTestBase {
         GuideFamilyAccessGuard guard = new GuideFamilyAccessGuard();
         GuideFamilyAccessGuard guard = new GuideFamilyAccessGuard();
         inject(guard, "guideFamilyService", guideFamilySvc);
         inject(guard, "guideFamilyService", guideFamilySvc);
 
 
-        assertNull(guard.checkFamilyAccess("admin", 1L, 2L));
+        assertNull(guard.checkFamilyAccess("admin", null, 1L, 2L));
     }
     }
 
 
     @Test
     @Test
@@ -131,7 +131,7 @@ class GuideBindingAccessContractTest extends AssessmentPlanContractTestBase {
         when(familyMemberMapper.selectList(org.mockito.ArgumentMatchers.any()))
         when(familyMemberMapper.selectList(org.mockito.ArgumentMatchers.any()))
                 .thenReturn(Arrays.asList(new FamilyMember(), new FamilyMember()));
                 .thenReturn(Arrays.asList(new FamilyMember(), new FamilyMember()));
 
 
-        Result<List<Map<String, Object>>> r = c.list(7L, "teacher");
+        Result<List<Map<String, Object>>> r = c.list(7L, "teacher", null);
         assertEquals(200, r.getCode());
         assertEquals(200, r.getCode());
         assertEquals(1, r.getData().size());
         assertEquals(1, r.getData().size());
         assertEquals("张家庭", r.getData().get(0).get("familyName"));
         assertEquals("张家庭", r.getData().get(0).get("familyName"));
@@ -146,7 +146,7 @@ class GuideBindingAccessContractTest extends AssessmentPlanContractTestBase {
         inject(c, "familyMapper", mock(FamilyMapper.class));
         inject(c, "familyMapper", mock(FamilyMapper.class));
         inject(c, "familyMemberMapper", mock(FamilyMemberMapper.class));
         inject(c, "familyMemberMapper", mock(FamilyMemberMapper.class));
 
 
-        Result<List<Map<String, Object>>> r = c.list(7L, "parent");
+        Result<List<Map<String, Object>>> r = c.list(7L, "parent", null);
         assertEquals(500, r.getCode());
         assertEquals(500, r.getCode());
         assertTrue(r.getMessage().contains("Access denied"));
         assertTrue(r.getMessage().contains("Access denied"));
     }
     }
@@ -165,7 +165,7 @@ class GuideBindingAccessContractTest extends AssessmentPlanContractTestBase {
         guide.setTeacherFamilyIds(null);
         guide.setTeacherFamilyIds(null);
         when(userMapper.selectById(7L)).thenReturn(guide);
         when(userMapper.selectById(7L)).thenReturn(guide);
 
 
-        Result<List<Map<String, Object>>> r = c.list(7L, "teacher");
+        Result<List<Map<String, Object>>> r = c.list(7L, "teacher", null);
         assertEquals(200, r.getCode());
         assertEquals(200, r.getCode());
         assertTrue(r.getData().isEmpty());
         assertTrue(r.getData().isEmpty());
     }
     }

+ 12 - 5
cfc-backend/src/test/java/com/etotem/cfc/unit/GuideFamilyAccessGuardTest.java

@@ -7,6 +7,7 @@ import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
 import org.junit.jupiter.api.Test;
 
 
 import java.lang.reflect.Field;
 import java.lang.reflect.Field;
+import java.util.Arrays;
 
 
 import static org.junit.jupiter.api.Assertions.*;
 import static org.junit.jupiter.api.Assertions.*;
 import static org.mockito.Mockito.*;
 import static org.mockito.Mockito.*;
@@ -58,30 +59,36 @@ class GuideFamilyAccessGuardTest {
 
 
     @Test
     @Test
     void checkFamilyAccess_管理员跳过归属校验() {
     void checkFamilyAccess_管理员跳过归属校验() {
-        assertNull(guard.checkFamilyAccess("admin", 1L, 10L));
+        assertNull(guard.checkFamilyAccess("admin", null, 1L, 10L));
         verify(svc, never()).isBound(any(), any());
         verify(svc, never()).isBound(any(), any());
     }
     }
 
 
     @Test
     @Test
     void checkFamilyAccess_规划师已绑定放行() {
     void checkFamilyAccess_规划师已绑定放行() {
         when(svc.isBound(1L, 10L)).thenReturn(true);
         when(svc.isBound(1L, 10L)).thenReturn(true);
-        assertNull(guard.checkFamilyAccess("teacher", 1L, 10L));
+        assertNull(guard.checkFamilyAccess("teacher", null, 1L, 10L));
     }
     }
 
 
     @Test
     @Test
     void checkFamilyAccess_规划师未绑定返回403() {
     void checkFamilyAccess_规划师未绑定返回403() {
         when(svc.isBound(1L, 10L)).thenReturn(false);
         when(svc.isBound(1L, 10L)).thenReturn(false);
-        assertEquals(403, guard.checkFamilyAccess("teacher", 1L, 10L).getCode());
+        assertEquals(403, guard.checkFamilyAccess("teacher", null, 1L, 10L).getCode());
     }
     }
 
 
     @Test
     @Test
     void checkFamilyAccess_家长被拒() {
     void checkFamilyAccess_家长被拒() {
-        assertEquals(403, guard.checkFamilyAccess("parent", 1L, 10L).getCode());
+        assertEquals(403, guard.checkFamilyAccess("parent", null, 1L, 10L).getCode());
         verify(svc, never()).isBound(any(), any());
         verify(svc, never()).isBound(any(), any());
     }
     }
 
 
+    @Test
+    void checkFamilyAccess_主角色parent但roles含规划师_放行() {
+        when(svc.isBound(1L, 10L)).thenReturn(true);
+        assertNull(guard.checkFamilyAccess("parent", Arrays.asList("parent", "teacher"), 1L, 10L));
+    }
+
     @Test
     @Test
     void checkFamilyAccess_孩子被拒() {
     void checkFamilyAccess_孩子被拒() {
-        assertEquals(403, guard.checkFamilyAccess("child", 1L, 10L).getCode());
+        assertEquals(403, guard.checkFamilyAccess("child", null, 1L, 10L).getCode());
     }
     }
 }
 }

+ 12 - 12
cfc-backend/src/test/java/com/etotem/cfc/unit/GuidePlanReviewControllerAuthTest.java

@@ -48,10 +48,10 @@ class GuidePlanReviewControllerAuthTest {
 
 
     @Test
     @Test
     void 未绑定家庭应返回403且不查方案() {
     void 未绑定家庭应返回403且不查方案() {
-        when(guard.checkFamilyAccess("teacher", 1L, 10L))
+        when(guard.checkFamilyAccess("teacher", null, 1L, 10L))
                 .thenReturn(Result.error(403, "无权访问该家庭数据"));
                 .thenReturn(Result.error(403, "无权访问该家庭数据"));
 
 
-        Result<?> r = controller.listPendingPlans(Collections.emptyMap(), "teacher", 1L, 10L);
+        Result<?> r = controller.listPendingPlans(Collections.emptyMap(), "teacher", 1L, 10L, null);
 
 
         assertEquals(403, r.getCode());
         assertEquals(403, r.getCode());
         verify(planService, never()).listPendingReviewPlans(any(), any());
         verify(planService, never()).listPendingReviewPlans(any(), any());
@@ -59,10 +59,10 @@ class GuidePlanReviewControllerAuthTest {
 
 
     @Test
     @Test
     void planId不属于路径familyId应返回403() {
     void planId不属于路径familyId应返回403() {
-        when(guard.checkFamilyAccess("teacher", 1L, 10L)).thenReturn(null);
+        when(guard.checkFamilyAccess("teacher", null, 1L, 10L)).thenReturn(null);
         when(planService.getPlanForFamily(99L, 10L)).thenReturn(null);
         when(planService.getPlanForFamily(99L, 10L)).thenReturn(null);
 
 
-        Result<?> r = controller.approvePlan(Collections.emptyMap(), "teacher", 1L, 10L, 99L);
+        Result<?> r = controller.approvePlan(Collections.emptyMap(), "teacher", 1L, 10L, 99L, null);
 
 
         assertEquals(403, r.getCode());
         assertEquals(403, r.getCode());
         verify(planService, never()).approveAndPublish(any(), any(), any());
         verify(planService, never()).approveAndPublish(any(), any(), any());
@@ -70,20 +70,20 @@ class GuidePlanReviewControllerAuthTest {
 
 
     @Test
     @Test
     void 已绑定且planId匹配应放行() {
     void 已绑定且planId匹配应放行() {
-        when(guard.checkFamilyAccess("teacher", 1L, 10L)).thenReturn(null);
+        when(guard.checkFamilyAccess("teacher", null, 1L, 10L)).thenReturn(null);
         when(planService.getPlanForFamily(99L, 10L)).thenReturn(plan(99L, 10L));
         when(planService.getPlanForFamily(99L, 10L)).thenReturn(plan(99L, 10L));
         when(planService.listPendingReviewPlans(10L, 1L)).thenReturn(Collections.emptyList());
         when(planService.listPendingReviewPlans(10L, 1L)).thenReturn(Collections.emptyList());
 
 
-        Result<?> r = controller.listPendingPlans(Collections.emptyMap(), "teacher", 1L, 10L);
+        Result<?> r = controller.listPendingPlans(Collections.emptyMap(), "teacher", 1L, 10L, null);
         assertEquals(200, r.getCode());
         assertEquals(200, r.getCode());
     }
     }
 
 
     @Test
     @Test
     void 驳回端点同样做IDOR反查() {
     void 驳回端点同样做IDOR反查() {
-        when(guard.checkFamilyAccess("teacher", 1L, 10L)).thenReturn(null);
+        when(guard.checkFamilyAccess("teacher", null, 1L, 10L)).thenReturn(null);
         when(planService.getPlanForFamily(99L, 10L)).thenReturn(null);
         when(planService.getPlanForFamily(99L, 10L)).thenReturn(null);
 
 
-        Result<?> r = controller.rejectPlan(Collections.emptyMap(), "teacher", 1L, 10L, 99L);
+        Result<?> r = controller.rejectPlan(Collections.emptyMap(), "teacher", 1L, 10L, 99L, null);
 
 
         assertEquals(403, r.getCode());
         assertEquals(403, r.getCode());
         verify(planService, never()).rejectPlan(any(), any(), any());
         verify(planService, never()).rejectPlan(any(), any(), any());
@@ -91,10 +91,10 @@ class GuidePlanReviewControllerAuthTest {
 
 
     @Test
     @Test
     void 编辑端点同样做IDOR反查() {
     void 编辑端点同样做IDOR反查() {
-        when(guard.checkFamilyAccess("teacher", 1L, 10L)).thenReturn(null);
+        when(guard.checkFamilyAccess("teacher", null, 1L, 10L)).thenReturn(null);
         when(planService.getPlanForFamily(99L, 10L)).thenReturn(null);
         when(planService.getPlanForFamily(99L, 10L)).thenReturn(null);
 
 
-        Result<?> r = controller.updatePlan(Collections.emptyMap(), "teacher", 1L, 10L, 99L);
+        Result<?> r = controller.updatePlan(Collections.emptyMap(), "teacher", 1L, 10L, 99L, null);
 
 
         assertEquals(403, r.getCode());
         assertEquals(403, r.getCode());
         verify(planService, never()).updatePlanContent(any(), any(), any());
         verify(planService, never()).updatePlanContent(any(), any(), any());
@@ -102,12 +102,12 @@ class GuidePlanReviewControllerAuthTest {
 
 
     @Test
     @Test
     void 管理员可按teacherId过滤() {
     void 管理员可按teacherId过滤() {
-        when(guard.checkFamilyAccess("admin", 1L, 10L)).thenReturn(null);
+        when(guard.checkFamilyAccess("admin", null, 1L, 10L)).thenReturn(null);
         Map<String, Object> params = new HashMap<>();
         Map<String, Object> params = new HashMap<>();
         params.put("teacherId", 77L);
         params.put("teacherId", 77L);
         when(planService.listPendingReviewPlans(10L, 77L)).thenReturn(Collections.emptyList());
         when(planService.listPendingReviewPlans(10L, 77L)).thenReturn(Collections.emptyList());
 
 
-        Result<?> r = controller.listPendingPlans(params, "admin", 1L, 10L);
+        Result<?> r = controller.listPendingPlans(params, "admin", 1L, 10L, null);
 
 
         assertEquals(200, r.getCode());
         assertEquals(200, r.getCode());
         verify(planService).listPendingReviewPlans(10L, 77L);
         verify(planService).listPendingReviewPlans(10L, 77L);

+ 7 - 7
cfc-backend/src/test/java/com/etotem/cfc/unit/PlanLifecycleParentContractTest.java

@@ -418,7 +418,7 @@ class PlanLifecycleParentContractTest extends AssessmentPlanContractTestBase {
     @Test
     @Test
     @DisplayName("SC-T-04 权限:非 teacher/admin 查待审核列表 → 无权限")
     @DisplayName("SC-T-04 权限:非 teacher/admin 查待审核列表 → 无权限")
     void pendingReviewList_权限_非teacher() throws Exception {
     void pendingReviewList_权限_非teacher() throws Exception {
-        Result<List<HealthPlan>> r = controller.listPendingReview(new HashMap<>(), 1L, "parent", 7L);
+        Result<List<HealthPlan>> r = controller.listPendingReview(new HashMap<>(), 1L, "parent", 7L, null);
         assertEquals(500, r.getCode());
         assertEquals(500, r.getCode());
         assertTrue(r.getMessage().contains("无权限"));
         assertTrue(r.getMessage().contains("无权限"));
     }
     }
@@ -428,7 +428,7 @@ class PlanLifecycleParentContractTest extends AssessmentPlanContractTestBase {
     void pendingReviewList_正常_teacher() throws Exception {
     void pendingReviewList_正常_teacher() throws Exception {
         when(planSvc.listPendingReviewPlans(7L, 1L))
         when(planSvc.listPendingReviewPlans(7L, 1L))
                 .thenReturn(Collections.singletonList(plan(88L, 7L, "pending_review")));
                 .thenReturn(Collections.singletonList(plan(88L, 7L, "pending_review")));
-        Result<List<HealthPlan>> r = controller.listPendingReview(new HashMap<>(), 1L, "teacher", 7L);
+        Result<List<HealthPlan>> r = controller.listPendingReview(new HashMap<>(), 1L, "teacher", 7L, null);
         assertEquals(200, r.getCode());
         assertEquals(200, r.getCode());
         assertEquals(1, r.getData().size());
         assertEquals(1, r.getData().size());
     }
     }
@@ -438,7 +438,7 @@ class PlanLifecycleParentContractTest extends AssessmentPlanContractTestBase {
     void pendingReviewUpdate_权限_非teacher() throws Exception {
     void pendingReviewUpdate_权限_非teacher() throws Exception {
         Map<String, Object> params = new HashMap<>();
         Map<String, Object> params = new HashMap<>();
         params.put("planId", 88L);
         params.put("planId", 88L);
-        Result<HealthPlan> r = controller.updatePlan(params, 1L, "parent");
+        Result<HealthPlan> r = controller.updatePlan(params, 1L, "parent", null);
         assertEquals(500, r.getCode());
         assertEquals(500, r.getCode());
         assertTrue(r.getMessage().contains("无权限"));
         assertTrue(r.getMessage().contains("无权限"));
     }
     }
@@ -451,7 +451,7 @@ class PlanLifecycleParentContractTest extends AssessmentPlanContractTestBase {
         Map<String, Object> params = new HashMap<>();
         Map<String, Object> params = new HashMap<>();
         params.put("planId", 88L);
         params.put("planId", 88L);
         params.put("planContent", "新内容");
         params.put("planContent", "新内容");
-        Result<HealthPlan> r = controller.updatePlan(params, 1L, "teacher");
+        Result<HealthPlan> r = controller.updatePlan(params, 1L, "teacher", null);
         assertEquals(200, r.getCode());
         assertEquals(200, r.getCode());
         assertNotNull(r.getData());
         assertNotNull(r.getData());
     }
     }
@@ -461,7 +461,7 @@ class PlanLifecycleParentContractTest extends AssessmentPlanContractTestBase {
     void pendingReviewApprove_权限_非teacher() throws Exception {
     void pendingReviewApprove_权限_非teacher() throws Exception {
         Map<String, Object> params = new HashMap<>();
         Map<String, Object> params = new HashMap<>();
         params.put("planId", 88L);
         params.put("planId", 88L);
-        Result<PlanApproveResultDTO> r = controller.approvePlan(params, 1L, "parent");
+        Result<PlanApproveResultDTO> r = controller.approvePlan(params, 1L, "parent", null);
         assertEquals(500, r.getCode());
         assertEquals(500, r.getCode());
         assertTrue(r.getMessage().contains("无权限"));
         assertTrue(r.getMessage().contains("无权限"));
     }
     }
@@ -478,7 +478,7 @@ class PlanLifecycleParentContractTest extends AssessmentPlanContractTestBase {
         Map<String, Object> params = new HashMap<>();
         Map<String, Object> params = new HashMap<>();
         params.put("planId", 88L);
         params.put("planId", 88L);
         params.put("comment", "ok");
         params.put("comment", "ok");
-        Result<PlanApproveResultDTO> r = controller.approvePlan(params, 1L, "teacher");
+        Result<PlanApproveResultDTO> r = controller.approvePlan(params, 1L, "teacher", null);
         assertEquals(200, r.getCode());
         assertEquals(200, r.getCode());
         assertEquals("published", r.getData().getStatus());
         assertEquals("published", r.getData().getStatus());
         assertTrue(r.getData().getTaskGenerated());
         assertTrue(r.getData().getTaskGenerated());
@@ -492,7 +492,7 @@ class PlanLifecycleParentContractTest extends AssessmentPlanContractTestBase {
         Map<String, Object> params = new HashMap<>();
         Map<String, Object> params = new HashMap<>();
         params.put("planId", 88L);
         params.put("planId", 88L);
         params.put("comment", "补数据");
         params.put("comment", "补数据");
-        Result<HealthPlan> r = controller.rejectPlan(params, 1L, "teacher");
+        Result<HealthPlan> r = controller.rejectPlan(params, 1L, "teacher", null);
         assertEquals(200, r.getCode());
         assertEquals(200, r.getCode());
         assertEquals("rejected", r.getData().getStatus());
         assertEquals("rejected", r.getData().getStatus());
     }
     }

+ 26 - 26
cfc-backend/src/test/java/com/etotem/cfc/unit/PlanReviewTeacherContractTest.java

@@ -65,12 +65,12 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
     @Test
     @Test
     @DisplayName("SC-T-04 正常:绑定家庭待审核列表 → 200(teacherId 取登录用户)")
     @DisplayName("SC-T-04 正常:绑定家庭待审核列表 → 200(teacherId 取登录用户)")
     void guidePlan_pending_正常() throws Exception {
     void guidePlan_pending_正常() throws Exception {
-        when(accessGuard.checkFamilyAccess("teacher", 7L, 1L)).thenReturn(null);
+        when(accessGuard.checkFamilyAccess("teacher", null, 7L, 1L)).thenReturn(null);
         when(healthPlanSvc.listPendingReviewPlans(1L, 7L))
         when(healthPlanSvc.listPendingReviewPlans(1L, 7L))
                 .thenReturn(Collections.singletonList(healthPlan(88L, 1L, "pending_review")));
                 .thenReturn(Collections.singletonList(healthPlan(88L, 1L, "pending_review")));
 
 
         Result<List<HealthPlan>> r = guidePlanController.listPendingPlans(
         Result<List<HealthPlan>> r = guidePlanController.listPendingPlans(
-                new HashMap<>(), "teacher", 7L, 1L);
+                new HashMap<>(), "teacher", 7L, 1L, null);
         assertEquals(200, r.getCode());
         assertEquals(200, r.getCode());
         assertEquals(1, r.getData().size());
         assertEquals(1, r.getData().size());
         assertEquals("pending_review", r.getData().get(0).getStatus());
         assertEquals("pending_review", r.getData().get(0).getStatus());
@@ -79,11 +79,11 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
     @Test
     @Test
     @DisplayName("SC-T-04 权限:非 teacher/admin 查待审核列表 → 403 无权访问")
     @DisplayName("SC-T-04 权限:非 teacher/admin 查待审核列表 → 403 无权访问")
     void guidePlan_pending_权限_非teacher() throws Exception {
     void guidePlan_pending_权限_非teacher() throws Exception {
-        when(accessGuard.checkFamilyAccess("parent", 7L, 1L))
+        when(accessGuard.checkFamilyAccess("parent", null, 7L, 1L))
                 .thenReturn(Result.error(403, "无权访问该家庭数据"));
                 .thenReturn(Result.error(403, "无权访问该家庭数据"));
 
 
         Result<List<HealthPlan>> r = guidePlanController.listPendingPlans(
         Result<List<HealthPlan>> r = guidePlanController.listPendingPlans(
-                new HashMap<>(), "parent", 7L, 1L);
+                new HashMap<>(), "parent", 7L, 1L, null);
         assertEquals(403, r.getCode());
         assertEquals(403, r.getCode());
         assertTrue(r.getMessage().contains("无权访问"));
         assertTrue(r.getMessage().contains("无权访问"));
     }
     }
@@ -91,14 +91,14 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
     @Test
     @Test
     @DisplayName("SC-T-14 正常:规划师编辑已绑定客户方案 → 200")
     @DisplayName("SC-T-14 正常:规划师编辑已绑定客户方案 → 200")
     void guidePlan_update_正常() throws Exception {
     void guidePlan_update_正常() throws Exception {
-        when(accessGuard.checkFamilyAccess("teacher", 7L, 1L)).thenReturn(null);
+        when(accessGuard.checkFamilyAccess("teacher", null, 7L, 1L)).thenReturn(null);
         HealthPlan p = healthPlan(88L, 1L, "pending_review");
         HealthPlan p = healthPlan(88L, 1L, "pending_review");
         when(healthPlanSvc.getPlanForFamily(88L, 1L)).thenReturn(p);
         when(healthPlanSvc.getPlanForFamily(88L, 1L)).thenReturn(p);
         when(healthPlanSvc.updatePlanContent(88L, "新内容", null)).thenReturn(p);
         when(healthPlanSvc.updatePlanContent(88L, "新内容", null)).thenReturn(p);
 
 
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
         body.put("planContent", "新内容");
         body.put("planContent", "新内容");
-        Result<HealthPlan> r = guidePlanController.updatePlan(body, "teacher", 7L, 1L, 88L);
+        Result<HealthPlan> r = guidePlanController.updatePlan(body, "teacher", 7L, 1L, 88L, null);
         assertEquals(200, r.getCode());
         assertEquals(200, r.getCode());
         assertNotNull(r.getData());
         assertNotNull(r.getData());
     }
     }
@@ -106,12 +106,12 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
     @Test
     @Test
     @DisplayName("SC-T-14 权限:planId 不属于路径 familyId → 403 无权访问(IDOR 防护)")
     @DisplayName("SC-T-14 权限:planId 不属于路径 familyId → 403 无权访问(IDOR 防护)")
     void guidePlan_update_权限_planId跨家庭() throws Exception {
     void guidePlan_update_权限_planId跨家庭() throws Exception {
-        when(accessGuard.checkFamilyAccess("teacher", 7L, 1L)).thenReturn(null);
+        when(accessGuard.checkFamilyAccess("teacher", null, 7L, 1L)).thenReturn(null);
         when(healthPlanSvc.getPlanForFamily(88L, 1L)).thenReturn(null);
         when(healthPlanSvc.getPlanForFamily(88L, 1L)).thenReturn(null);
 
 
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
         body.put("planContent", "新内容");
         body.put("planContent", "新内容");
-        Result<HealthPlan> r = guidePlanController.updatePlan(body, "teacher", 7L, 1L, 88L);
+        Result<HealthPlan> r = guidePlanController.updatePlan(body, "teacher", 7L, 1L, 88L, null);
         assertEquals(403, r.getCode());
         assertEquals(403, r.getCode());
         assertTrue(r.getMessage().contains("无权访问该方案"));
         assertTrue(r.getMessage().contains("无权访问该方案"));
         verify(healthPlanSvc, never()).updatePlanContent(anyLong(), any(), any());
         verify(healthPlanSvc, never()).updatePlanContent(anyLong(), any(), any());
@@ -120,7 +120,7 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
     @Test
     @Test
     @DisplayName("SC-X-09/SC-T-04 正常:审核通过并发布 → 200 DTO")
     @DisplayName("SC-X-09/SC-T-04 正常:审核通过并发布 → 200 DTO")
     void guidePlan_approve_正常() throws Exception {
     void guidePlan_approve_正常() throws Exception {
-        when(accessGuard.checkFamilyAccess("teacher", 7L, 1L)).thenReturn(null);
+        when(accessGuard.checkFamilyAccess("teacher", null, 7L, 1L)).thenReturn(null);
         HealthPlan p = healthPlan(88L, 1L, "pending_review");
         HealthPlan p = healthPlan(88L, 1L, "pending_review");
         when(healthPlanSvc.getPlanForFamily(88L, 1L)).thenReturn(p);
         when(healthPlanSvc.getPlanForFamily(88L, 1L)).thenReturn(p);
         PlanApproveResultDTO dto = new PlanApproveResultDTO();
         PlanApproveResultDTO dto = new PlanApproveResultDTO();
@@ -131,7 +131,7 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
 
 
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
         body.put("comment", "ok");
         body.put("comment", "ok");
-        Result<PlanApproveResultDTO> r = guidePlanController.approvePlan(body, "teacher", 7L, 1L, 88L);
+        Result<PlanApproveResultDTO> r = guidePlanController.approvePlan(body, "teacher", 7L, 1L, 88L, null);
         assertEquals(200, r.getCode());
         assertEquals(200, r.getCode());
         assertEquals("published", r.getData().getStatus());
         assertEquals("published", r.getData().getStatus());
         assertTrue(r.getData().getTaskGenerated());
         assertTrue(r.getData().getTaskGenerated());
@@ -140,18 +140,18 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
     @Test
     @Test
     @DisplayName("SC-T-04 权限:非 teacher 审核通过 → 403")
     @DisplayName("SC-T-04 权限:非 teacher 审核通过 → 403")
     void guidePlan_approve_权限_非teacher() throws Exception {
     void guidePlan_approve_权限_非teacher() throws Exception {
-        when(accessGuard.checkFamilyAccess("parent", 7L, 1L))
+        when(accessGuard.checkFamilyAccess("parent", null, 7L, 1L))
                 .thenReturn(Result.error(403, "无权访问该家庭数据"));
                 .thenReturn(Result.error(403, "无权访问该家庭数据"));
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
         body.put("comment", "ok");
         body.put("comment", "ok");
-        Result<PlanApproveResultDTO> r = guidePlanController.approvePlan(body, "parent", 7L, 1L, 88L);
+        Result<PlanApproveResultDTO> r = guidePlanController.approvePlan(body, "parent", 7L, 1L, 88L, null);
         assertEquals(403, r.getCode());
         assertEquals(403, r.getCode());
     }
     }
 
 
     @Test
     @Test
     @DisplayName("SC-T-04 正常:驳回方案 → 200 rejected")
     @DisplayName("SC-T-04 正常:驳回方案 → 200 rejected")
     void guidePlan_reject_正常() throws Exception {
     void guidePlan_reject_正常() throws Exception {
-        when(accessGuard.checkFamilyAccess("teacher", 7L, 1L)).thenReturn(null);
+        when(accessGuard.checkFamilyAccess("teacher", null, 7L, 1L)).thenReturn(null);
         HealthPlan p = healthPlan(88L, 1L, "pending_review");
         HealthPlan p = healthPlan(88L, 1L, "pending_review");
         when(healthPlanSvc.getPlanForFamily(88L, 1L)).thenReturn(p);
         when(healthPlanSvc.getPlanForFamily(88L, 1L)).thenReturn(p);
         HealthPlan rejected = healthPlan(88L, 1L, "rejected");
         HealthPlan rejected = healthPlan(88L, 1L, "rejected");
@@ -159,7 +159,7 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
 
 
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
         body.put("comment", "补数据");
         body.put("comment", "补数据");
-        Result<HealthPlan> r = guidePlanController.rejectPlan(body, "teacher", 7L, 1L, 88L);
+        Result<HealthPlan> r = guidePlanController.rejectPlan(body, "teacher", 7L, 1L, 88L, null);
         assertEquals(200, r.getCode());
         assertEquals(200, r.getCode());
         assertEquals("rejected", r.getData().getStatus());
         assertEquals("rejected", r.getData().getStatus());
     }
     }
@@ -167,12 +167,12 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
     @Test
     @Test
     @DisplayName("SC-T-04 权限:admin 跳过归属校验放行 → 200")
     @DisplayName("SC-T-04 权限:admin 跳过归属校验放行 → 200")
     void guidePlan_pending_权限_admin放行() throws Exception {
     void guidePlan_pending_权限_admin放行() throws Exception {
-        when(accessGuard.checkFamilyAccess("admin", 1L, 2L)).thenReturn(null);
+        when(accessGuard.checkFamilyAccess("admin", null, 1L, 2L)).thenReturn(null);
         when(healthPlanSvc.listPendingReviewPlans(2L, null))
         when(healthPlanSvc.listPendingReviewPlans(2L, null))
                 .thenReturn(Collections.singletonList(healthPlan(88L, 2L, "pending_review")));
                 .thenReturn(Collections.singletonList(healthPlan(88L, 2L, "pending_review")));
 
 
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
-        Result<List<HealthPlan>> r = guidePlanController.listPendingPlans(body, "admin", 1L, 2L);
+        Result<List<HealthPlan>> r = guidePlanController.listPendingPlans(body, "admin", 1L, 2L, null);
         assertEquals(200, r.getCode());
         assertEquals(200, r.getCode());
         assertEquals(1, r.getData().size());
         assertEquals(1, r.getData().size());
     }
     }
@@ -196,7 +196,7 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
         body.put("planId", 88L);
         body.put("planId", 88L);
         body.put("approved", true);
         body.put("approved", true);
         body.put("comment", "ok");
         body.put("comment", "ok");
-        Result<String> r = c.reviewPlan(body, 7L, "teacher");
+        Result<String> r = c.reviewPlan(body, 7L, "teacher", null);
         assertEquals(200, r.getCode());
         assertEquals(200, r.getCode());
         assertEquals("方案已批准", r.getData());
         assertEquals("方案已批准", r.getData());
         assertEquals("reviewed", plan.getStatus());
         assertEquals("reviewed", plan.getStatus());
@@ -219,7 +219,7 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
         body.put("planId", 88L);
         body.put("planId", 88L);
         body.put("approved", false);
         body.put("approved", false);
-        Result<String> r = c.reviewPlan(body, 7L, "teacher");
+        Result<String> r = c.reviewPlan(body, 7L, "teacher", null);
         assertEquals(200, r.getCode());
         assertEquals(200, r.getCode());
         assertEquals("方案已驳回", r.getData());
         assertEquals("方案已驳回", r.getData());
         assertEquals("rejected", plan.getStatus());
         assertEquals("rejected", plan.getStatus());
@@ -239,7 +239,7 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
         body.put("planId", 88L);
         body.put("planId", 88L);
         body.put("approved", true);
         body.put("approved", true);
-        Result<String> r = c.reviewPlan(body, 7L, "teacher");
+        Result<String> r = c.reviewPlan(body, 7L, "teacher", null);
         assertEquals(500, r.getCode());
         assertEquals(500, r.getCode());
         assertTrue(r.getMessage().contains("方案状态异常"));
         assertTrue(r.getMessage().contains("方案状态异常"));
     }
     }
@@ -258,7 +258,7 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
         body.put("planId", 88L);
         body.put("planId", 88L);
         body.put("approved", true);
         body.put("approved", true);
-        Result<String> r = c.reviewPlan(body, 7L, "teacher");
+        Result<String> r = c.reviewPlan(body, 7L, "teacher", null);
         assertEquals(500, r.getCode());
         assertEquals(500, r.getCode());
         assertTrue(r.getMessage().contains("方案不存在"));
         assertTrue(r.getMessage().contains("方案不存在"));
     }
     }
@@ -274,7 +274,7 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
 
 
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
         body.put("planId", 88L);
         body.put("planId", 88L);
-        Result<String> r = c.reviewPlan(body, 1L, "parent");
+        Result<String> r = c.reviewPlan(body, 1L, "parent", null);
         assertEquals(500, r.getCode());
         assertEquals(500, r.getCode());
         assertTrue(r.getMessage().contains("无权限"));
         assertTrue(r.getMessage().contains("无权限"));
     }
     }
@@ -294,7 +294,7 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
 
 
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
         body.put("planId", 88L);
         body.put("planId", 88L);
-        Result<String> r = c.activatePlan(body, 7L, "teacher");
+        Result<String> r = c.activatePlan(body, 7L, "teacher", null);
         assertEquals(200, r.getCode());
         assertEquals(200, r.getCode());
         assertEquals("方案已激活", r.getData());
         assertEquals("方案已激活", r.getData());
         verify(actSvc).activate(88L, 7L);
         verify(actSvc).activate(88L, 7L);
@@ -315,7 +315,7 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
 
 
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
         body.put("planId", 88L);
         body.put("planId", 88L);
-        Result<String> r = c.activatePlan(body, 7L, "teacher");
+        Result<String> r = c.activatePlan(body, 7L, "teacher", null);
         assertEquals(403, r.getCode());
         assertEquals(403, r.getCode());
         assertTrue(r.getMessage().contains("认证"));
         assertTrue(r.getMessage().contains("认证"));
     }
     }
@@ -331,7 +331,7 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
 
 
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
         body.put("planId", 88L);
         body.put("planId", 88L);
-        Result<String> r = c.activatePlan(body, 1L, "parent");
+        Result<String> r = c.activatePlan(body, 1L, "parent", null);
         assertEquals(500, r.getCode());
         assertEquals(500, r.getCode());
         assertTrue(r.getMessage().contains("无权限"));
         assertTrue(r.getMessage().contains("无权限"));
     }
     }
@@ -353,7 +353,7 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
 
 
         Map<String, Object> body = new HashMap<>();
         Map<String, Object> body = new HashMap<>();
         body.put("planId", 88L);
         body.put("planId", 88L);
-        Result<String> r = c.activatePlan(body, 7L, "teacher");
+        Result<String> r = c.activatePlan(body, 7L, "teacher", null);
         assertEquals(500, r.getCode());
         assertEquals(500, r.getCode());
         assertTrue(r.getMessage().contains("方案状态异常"));
         assertTrue(r.getMessage().contains("方案状态异常"));
     }
     }
@@ -369,7 +369,7 @@ class PlanReviewTeacherContractTest extends AssessmentPlanContractTestBase {
         inject(c, "planActivationService", mock(PlanActivationService.class));
         inject(c, "planActivationService", mock(PlanActivationService.class));
         inject(c, "plannerCertificationGuard", mock(PlannerCertificationGuard.class));
         inject(c, "plannerCertificationGuard", mock(PlannerCertificationGuard.class));
 
 
-        Result<String> r = c.activatePlan(new HashMap<>(), 7L, "teacher");
+        Result<String> r = c.activatePlan(new HashMap<>(), 7L, "teacher", null);
         assertEquals(500, r.getCode());
         assertEquals(500, r.getCode());
         assertTrue(r.getMessage().contains("planId required"));
         assertTrue(r.getMessage().contains("planId required"));
     }
     }

+ 68 - 0
cfc-backend/src/test/java/com/etotem/cfc/unit/RolesUtilTest.java

@@ -0,0 +1,68 @@
+package com.etotem.cfc.unit;
+
+import com.etotem.cfc.common.RolesUtil;
+import com.etotem.cfc.entity.User;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * RolesUtil:按完整角色列表判定规划师(teacher)身份,保持主角色语义不变。
+ */
+class RolesUtilTest {
+
+    @Test
+    void hasTeacherRole_主角色为teacher_true() {
+        assertTrue(RolesUtil.hasTeacherRole("teacher", null));
+        assertTrue(RolesUtil.hasTeacherRole("teacher", Collections.singletonList("teacher")));
+    }
+
+    @Test
+    void hasTeacherRole_角色列表含teacher_true() {
+        // 主角色为 parent,但 roles 含 teacher(规划师)——本改造的核心场景
+        assertTrue(RolesUtil.hasTeacherRole("parent", Arrays.asList("parent", "teacher")));
+        assertTrue(RolesUtil.hasTeacherRole("parent", Arrays.asList("parent", "activity_manager", "teacher")));
+    }
+
+    @Test
+    void hasTeacherRole_非规划师_false() {
+        assertFalse(RolesUtil.hasTeacherRole("parent", Collections.singletonList("parent")));
+        assertFalse(RolesUtil.hasTeacherRole("parent", null));
+        assertFalse(RolesUtil.hasTeacherRole("child", Arrays.asList("parent", "child")));
+        assertFalse(RolesUtil.hasTeacherRole(null, Collections.emptyList()));
+    }
+
+    @Test
+    void effectiveRoles_首位恒为主角色() {
+        User u = new User();
+        u.setRole("parent");
+        u.setRoles(Arrays.asList("parent", "activity_manager", "teacher"));
+        assertEquals(Arrays.asList("parent", "activity_manager", "teacher"), RolesUtil.effectiveRoles(u));
+    }
+
+    @Test
+    void effectiveRoles_roles为null_回退主角色() {
+        User u = new User();
+        u.setRole("teacher");
+        u.setRoles(null);
+        assertEquals(Collections.singletonList("teacher"), RolesUtil.effectiveRoles(u));
+    }
+
+    @Test
+    void effectiveRoles_去重() {
+        User u = new User();
+        u.setRole("parent");
+        u.setRoles(Arrays.asList("parent", "teacher", "teacher"));
+        assertEquals(Arrays.asList("parent", "teacher"), RolesUtil.effectiveRoles(u));
+    }
+
+    @Test
+    void effectiveRoles_user为null_空列表() {
+        assertEquals(Collections.emptyList(), RolesUtil.effectiveRoles(null));
+    }
+}

+ 36 - 8
cfc-frontend/pages/profile-main/profile.vue

@@ -309,7 +309,7 @@
       </view>
       </view>
 
 
       <!-- 规划师服务中心入口(仅规划师可见) -->
       <!-- 规划师服务中心入口(仅规划师可见) -->
-      <view class="service-center-section" v-if="role === 'teacher' && teacherStatus === 'approved'">
+      <view class="service-center-section" v-if="isTeacherUser && teacherStatus === 'approved'">
         <view class="service-center-card" @click="goToServiceCenter">
         <view class="service-center-card" @click="goToServiceCenter">
           <view class="service-center-icon">👨‍🏫</view>
           <view class="service-center-icon">👨‍🏫</view>
           <view class="service-center-info">
           <view class="service-center-info">
@@ -321,7 +321,7 @@
       </view>
       </view>
 
 
       <!-- 规划师待审核提示 -->
       <!-- 规划师待审核提示 -->
-      <view class="service-center-section" v-if="role === 'teacher' && teacherStatus !== 'approved'">
+      <view class="service-center-section" v-if="isTeacherUser && teacherStatus !== 'approved'">
         <view class="service-center-card disabled-card">
         <view class="service-center-card disabled-card">
           <view class="service-center-icon">👨‍🏫</view>
           <view class="service-center-icon">👨‍🏫</view>
           <view class="service-center-info">
           <view class="service-center-info">
@@ -375,7 +375,7 @@ import {
   verifyPassword, getPointsBalance, getUserInfo,
   verifyPassword, getPointsBalance, getUserInfo,
   cartCount, getOrderCounts, getCfSummary,
   cartCount, getOrderCounts, getCfSummary,
   getProfileStats, getMyReferrer, getVendorTeamList, getVendorHierarchyInfo, getMyCoupons,
   getProfileStats, getMyReferrer, getVendorTeamList, getVendorHierarchyInfo, getMyCoupons,
-  getTeacherInfo, getTeacherPendingTasks, getTeacherUnreadCount,
+  getTeacherInfo, getTeacherPendingTasks, getTeacherUnreadCount, getUserRoles,
   getReferralCode, extractReferralCode, getShareQrCode, getEnergySandbox,
   getReferralCode, extractReferralCode, getShareQrCode, getEnergySandbox,
   vendorStatus
   vendorStatus
 } from '../../utils/api.js'
 } from '../../utils/api.js'
@@ -392,6 +392,8 @@ export default {
       nickname: '',
       nickname: '',
       role: 'parent',
       role: 'parent',
       isSwitchedChild: false,
       isSwitchedChild: false,
+      userRoles: [],
+      isTeacherUser: false,
       userInfo: {},
       userInfo: {},
       memberLevel: null,
       memberLevel: null,
       membershipDays: 0,
       membershipDays: 0,
@@ -483,11 +485,11 @@ export default {
     // 服务商身份判断(成就区「我的团队」入口按此分发)
     // 服务商身份判断(成就区「我的团队」入口按此分发)
     this.checkVendorStatus()
     this.checkVendorStatus()
 
 
-    // 规划师加载额外数据
-    if (role === 'teacher') {
-      this.loadTeacherStatus()
-      this.loadTeacherData()
-    } else {
+    // 规划师身份探测(主角色 teacher 或 roles 含 teacher 均生效),并加载其待办/消息数据
+    this.loadTeacherIdentity()
+
+    // 主角色为 parent 时加载本人/家庭数据;教师主角色保持原逻辑(不加载家庭区块)
+    if (role !== 'teacher') {
       this.loadAllData()
       this.loadAllData()
       this.loadMyReferrer()
       this.loadMyReferrer()
     }
     }
@@ -511,6 +513,30 @@ export default {
         }
         }
       }).catch(function() {})
       }).catch(function() {})
     },
     },
+    loadTeacherIdentity() {
+      var self = this
+      var rolesP = (typeof getUserRoles === 'function' ? getUserRoles() : Promise.resolve({ data: [] }))
+        .catch(function() { return { data: [] } })
+      rolesP.then(function(res) {
+        var rolesData = (res && res.data) || []
+        self.userRoles = Array.isArray(rolesData) ? rolesData : []
+        uni.setStorageSync('userRoles', self.userRoles)
+        self.isTeacherUser = self.role === 'teacher' || self.userRoles.indexOf('teacher') > -1
+        // 仅对规划师拉取认证状态与待办/消息,避免普通家长额外请求
+        if (self.isTeacherUser) {
+          var teacherP = (typeof getTeacherInfo === 'function' ? getTeacherInfo() : Promise.resolve({}))
+            .catch(function() { return {} })
+          teacherP.then(function(tres) {
+            var t = (tres && tres.data) || {}
+            self.teacherStatus = t.verificationStatus || t.status || 'pending'
+            uni.setStorageSync('teacherStatus', self.teacherStatus)
+            if (self.teacherStatus === 'approved') {
+              self.loadTeacherData()
+            }
+          })
+        }
+      })
+    },
     loadTeacherData() {
     loadTeacherData() {
       var self = this
       var self = this
       // 并行加载待处理任务和未读消息
       // 并行加载待处理任务和未读消息
@@ -519,6 +545,8 @@ export default {
           self.pendingTasks = res.data.length
           self.pendingTasks = res.data.length
         } else if (res && res.data && res.data.pendingCount != null) {
         } else if (res && res.data && res.data.pendingCount != null) {
           self.pendingTasks = res.data.pendingCount
           self.pendingTasks = res.data.pendingCount
+        } else if (res && res.data && res.data.pendingReviewTasks != null) {
+          self.pendingTasks = res.data.pendingReviewTasks
         }
         }
       }).catch(function() {})
       }).catch(function() {})
       getTeacherUnreadCount().then(function(res) {
       getTeacherUnreadCount().then(function(res) {

+ 41 - 8
cfc-frontend/pages/service-center/index.vue

@@ -50,6 +50,10 @@
           <view class="menu-icon purple">📊</view>
           <view class="menu-icon purple">📊</view>
           <text class="menu-label">录入测评结果</text>
           <text class="menu-label">录入测评结果</text>
         </view>
         </view>
+        <view class="menu-item" @click="goToCertification">
+          <view class="menu-icon orange">🎓</view>
+          <text class="menu-label">培训认证</text>
+        </view>
       </view>
       </view>
     </view>
     </view>
 
 
@@ -64,7 +68,7 @@
 </template>
 </template>
 
 
 <script>
 <script>
-import { getTeacherPendingTasks, getTeacherUnreadCount } from '@/utils/api.js'
+import { getUserRoles, getTeacherInfo, getTeacherPendingTasks, getTeacherUnreadCount } from '@/utils/api.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 import fontScaleMixin from '@/mixins/font-scale.js'
 import shareMixin from '../../components/share-mixin.js'
 import shareMixin from '../../components/share-mixin.js'
 
 
@@ -83,17 +87,37 @@ export default {
     }
     }
   },
   },
   onLoad() {
   onLoad() {
-    this.initRole()
-    this.loadTeacherData()
+    var self = this
+    this.initRole().then(function() {
+      self.loadTeacherData()
+    })
   },
   },
   methods: {
   methods: {
-    initRole() {
+    async initRole() {
       var role = uni.getStorageSync('role') || uni.getStorageSync('currentRole') || 'parent'
       var role = uni.getStorageSync('role') || uni.getStorageSync('currentRole') || 'parent'
-      this.isTeacher = role === 'teacher'
+      var roles = uni.getStorageSync('userRoles')
+      if (!Array.isArray(roles)) {
+        try {
+          var res = await getUserRoles()
+          roles = (res && res.data) || []
+          uni.setStorageSync('userRoles', roles)
+        } catch (e) {
+          roles = []
+        }
+      }
+      var isTeacher = role === 'teacher' || (Array.isArray(roles) && roles.indexOf('teacher') > -1)
+      this.isTeacher = isTeacher
       var status = uni.getStorageSync('teacherStatus') || ''
       var status = uni.getStorageSync('teacherStatus') || ''
-      this.isTeacherPending = role === 'teacher' && status === 'pending'
-      this.isTeacherRejected = role === 'teacher' && status === 'rejected'
-      this.isTeacherApproved = role === 'teacher' && status === 'approved'
+      if (!status) {
+        try {
+          var t = await getTeacherInfo()
+          status = (t && t.data && (t.data.verificationStatus || t.data.status)) || ''
+          uni.setStorageSync('teacherStatus', status)
+        } catch (e) {}
+      }
+      this.isTeacherPending = isTeacher && status === 'pending'
+      this.isTeacherRejected = isTeacher && status === 'rejected'
+      this.isTeacherApproved = isTeacher && status === 'approved'
     },
     },
     async loadTeacherData() {
     async loadTeacherData() {
       if (!this.isTeacherApproved) return
       if (!this.isTeacherApproved) return
@@ -103,6 +127,11 @@ export default {
           this.pendingTaskCount = res.data.length
           this.pendingTaskCount = res.data.length
         } else if (res && res.data && res.data.pendingCount != null) {
         } else if (res && res.data && res.data.pendingCount != null) {
           this.pendingTaskCount = res.data.pendingCount
           this.pendingTaskCount = res.data.pendingCount
+        } else if (res && res.data && res.data.pendingReviewTasks != null) {
+          this.pendingTaskCount = res.data.pendingReviewTasks
+        }
+        if (res && res.data && res.data.totalFamilies != null) {
+          this.serviceFamilyCount = res.data.totalFamilies
         }
         }
       } catch (e) {
       } catch (e) {
         this.pendingTaskCount = uni.getStorageSync('pendingTaskCount') || 0
         this.pendingTaskCount = uni.getStorageSync('pendingTaskCount') || 0
@@ -130,6 +159,9 @@ export default {
     },
     },
     goToAssessmentResult() {
     goToAssessmentResult() {
       uni.navigateTo({ url: '/pages/assessment/execution' })
       uni.navigateTo({ url: '/pages/assessment/execution' })
+    },
+    goToCertification() {
+      uni.navigateTo({ url: '/pages/teacher/certification-center' })
     }
     }
   }
   }
 }
 }
@@ -254,6 +286,7 @@ $c-cyan:           #06B6D4;   // cyan-500
 .menu-icon.green { background: rgba($dim-action, 0.15); color: $dim-action; }
 .menu-icon.green { background: rgba($dim-action, 0.15); color: $dim-action; }
 .menu-icon.cyan { background: rgba($c-cyan, 0.15); color: $c-cyan; }
 .menu-icon.cyan { background: rgba($c-cyan, 0.15); color: $c-cyan; }
 .menu-icon.purple { background: rgba($facade-violet, 0.15); color: $facade-violet; }
 .menu-icon.purple { background: rgba($facade-violet, 0.15); color: $facade-violet; }
+.menu-icon.orange { background: rgba(255, 153, 0, 0.15); color: #FF9900; }
 
 
 .menu-label {
 .menu-label {
   font-size: calc(24rpx * var(--font-scale, 1));
   font-size: calc(24rpx * var(--font-scale, 1));

+ 15 - 2
cfc-frontend/utils/api.js

@@ -586,11 +586,24 @@ export const switchBackVerify = (password) => {
 }
 }
 
 
 export const getUserRoles = () => {
 export const getUserRoles = () => {
-	return request('/api/user/roles', 'POST')
+	return request('/api/family/user/roles', 'POST')
 }
 }
 
 
 export const switchRole = (role) => {
 export const switchRole = (role) => {
-	return request('/api/user/switch-role', 'POST', { role })
+	return request('/api/family/user/switch-role', 'POST', { role })
+}
+
+/** 规划师(teacher)能力:主角色非 teacher、但 roles 含 teacher 亦可访问 */
+export const getTeacherInfo = () => {
+	return request('/api/teacher/info', 'POST', {})
+}
+
+export const getTeacherPendingTasks = () => {
+	return request('/api/guide/families/dashboard-stats', 'POST', {})
+}
+
+export const getTeacherUnreadCount = () => {
+	return request('/api/guide/messages/unread-count', 'POST', {})
 }
 }
 
 
 // 会员模块
 // 会员模块