Explorar o código

账户权限分离

jiapu hai 1 mes
pai
achega
c7434d9d2b
Modificáronse 41 ficheiros con 1500 adicións e 343 borrados
  1. 1 1
      cfc-backend/config/application.yml
  2. 144 0
      cfc-backend/sql/cleanup_test_dirty_data.sql
  3. 166 0
      cfc-backend/src/main/java/com/etotem/cfc/common/AdminRoles.java
  4. 339 87
      cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java
  5. 1 0
      cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java
  6. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/config/WebConfig.java
  7. 6 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminActivityController.java
  8. 108 36
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminAuthController.java
  9. 5 5
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java
  10. 36 3
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java
  11. 6 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminInventoryController.java
  12. 8 1
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductController.java
  13. 6 2
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductOrderController.java
  14. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/SupplyManageController.java
  15. 30 10
      cfc-backend/src/main/java/com/etotem/cfc/controller/admin/SupplySettlementController.java
  16. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/dto/AdminLoginDTO.java
  17. 2 0
      cfc-backend/src/main/java/com/etotem/cfc/entity/Family.java
  18. 3 1
      cfc-backend/src/main/java/com/etotem/cfc/entity/User.java
  19. 11 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ActivityAdminService.java
  20. 5 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java
  21. 11 0
      cfc-backend/src/main/java/com/etotem/cfc/service/InventoryService.java
  22. 11 0
      cfc-backend/src/main/java/com/etotem/cfc/service/ProductOrderService.java
  23. 77 0
      cfc-backend/src/main/java/com/etotem/cfc/service/SupplySettlementService.java
  24. 17 9
      cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java
  25. 23 0
      cfc-backend/src/main/java/com/etotem/cfc/service/impl/ServiceRoleApplicationServiceImpl.java
  26. 6 6
      cfc-backend/src/main/resources/application.yml
  27. 4 1
      cfc-backend/src/main/resources/schema.sql
  28. 1 1
      cfc-web/.last_build_commit
  29. 1 1
      cfc-web/package.json
  30. 8 0
      cfc-web/public/CHANGELOG-v$(node.undefined.md
  31. 7 1
      cfc-web/public/CHANGELOG.md
  32. 9 0
      cfc-web/src/api/auth.js
  33. 61 33
      cfc-web/src/router/index.js
  34. 160 29
      cfc-web/src/utils/permissions.js
  35. 114 92
      cfc-web/src/views/Layout.vue
  36. 77 5
      cfc-web/src/views/Login.vue
  37. 20 8
      cfc-web/src/views/Users.vue
  38. 1 1
      cfc-web/src/views/admin/HealthEnergyConfig.vue
  39. 6 3
      cfc-web/src/views/admin/MenuManage.vue
  40. 1 1
      cfc-web/src/views/admin/ProductEdit.vue
  41. 2 2
      tests/e2e/admin-menu-survey.spec.js

+ 1 - 1
cfc-backend/config/application.yml

@@ -18,7 +18,7 @@ spring:
       max-request-size: 50MB
   datasource:
     driver-class-name: com.mysql.cj.jdbc.Driver
-    url: jdbc:mysql://127.0.0.1:3306/zxyj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true
+    url: jdbc:mysql://192.168.16.251:3306/zxyj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true
     username: zxyj
     password: zxyj@123
     hikari:

+ 144 - 0
cfc-backend/sql/cleanup_test_dirty_data.sql

@@ -0,0 +1,144 @@
+-- ============================================================
+-- 测试脏数据清理脚本(《角色权限与菜单归类整合方案》B-7)
+-- 目标库: 192.168.16.251:3306/zxyj
+-- 使用方式: 逐段执行。每段先 SELECT 预览,确认无误后再执行 DELETE/UPDATE。
+-- 已排查修正: points_log 列名(task_id/amount)、自引用子查询 ERROR 1093、
+--             LIKE 通配符、users.role NOT NULL、role 为 MySQL 8.0 保留字需加反引号
+-- ============================================================
+
+-- 重要: 本文件含中文匹配模式,必须确保连接字符集为 utf8mb4,
+-- 否则 LIKE 匹配失效或报编码错误(命令行执行时: mysql --default-character-set=utf8mb4)
+SET NAMES utf8mb4;
+
+-- ------------------------------------------------------------
+-- 第 1 步:清理 TEST-Energy-Grant 测试任务
+-- 注意执行顺序: 先 1.2/1.4(依赖 tasks 关联),再 1.3 删除任务
+-- ------------------------------------------------------------
+
+-- 1.1 预览将被删除的任务
+SELECT id, family_id, creator_id, title, status, created_at
+FROM tasks
+WHERE title LIKE 'TEST-Energy-Grant%';
+
+-- 1.2 预览关联的积分流水(通过 task_id 关联,须先于 1.3 执行)
+SELECT pl.id, pl.child_id, pl.task_id, pl.amount, pl.description, pl.created_at
+FROM points_log pl
+INNER JOIN tasks t ON pl.task_id = t.id
+WHERE t.title LIKE 'TEST-Energy-Grant%';
+
+-- 1.3 (可选)确认后删除关联积分流水(必须在 1.4 之前执行)
+DELETE pl FROM points_log pl
+INNER JOIN tasks t ON pl.task_id = t.id
+WHERE t.title LIKE 'TEST-Energy-Grant%';
+
+-- 1.4 确认后删除任务
+DELETE FROM tasks WHERE title LIKE 'TEST-Energy-Grant%';
+
+-- ------------------------------------------------------------
+-- 第 2 步:清理乱码昵称/家庭名(UTF-8 按 latin1 误读产生的乱码,如"娴嬭瘯骞哥?瀹跺涵")
+-- 说明: MySQL LIKE 中 ? 不是通配符(通配符是 _ 和 %),乱码按实际片段匹配;
+--       执行预览后如还有其他乱码片段,请自行补充 LIKE 条件
+-- ------------------------------------------------------------
+
+-- 2.1 预览乱码家庭
+SELECT id, name, invite_code, created_at
+FROM families
+WHERE name LIKE '%娴嬭瘯%' OR name LIKE '%瀹跺涵%';
+
+-- 2.2 预览乱码用户昵称
+SELECT id, nickname, phone, `role`, family_id, created_at
+FROM users
+WHERE nickname LIKE '%娴嬭瘯%' OR nickname LIKE '%瀹跺涵%';
+
+-- 2.3 确认后:清理乱码家庭
+--     修正: MySQL 不允许 UPDATE users 时子查询 users 同表(ERROR 1093),
+--     且 families 在 FROM 时不能作为 UPDATE 目标,改用多表 UPDATE JOIN
+-- 2.3a 解除用户与乱码家庭的关联
+UPDATE users u
+INNER JOIN families f ON u.family_id = f.id
+SET u.family_id = NULL
+WHERE f.name LIKE '%娴嬭瘯%' OR f.name LIKE '%瀹跺涵%';
+
+-- 2.3b 删除乱码家庭
+DELETE FROM families WHERE name LIKE '%娴嬭瘯%' OR name LIKE '%瀹跺涵%';
+
+-- 2.4 确认后:清理乱码昵称用户(软删或硬删二选一)
+-- 软删(推荐,users 表 deleted 列已由迁移 98 创建):
+UPDATE users SET deleted = 1 WHERE nickname LIKE '%娴嬭瘯%' OR nickname LIKE '%瀹跺涵%';
+-- 硬删(与软删二选一,默认注释):
+-- DELETE FROM users WHERE nickname LIKE '%娴嬭瘯%' OR nickname LIKE '%瀹跺涵%';
+
+-- ------------------------------------------------------------
+-- 第 3 步:历史数据角色口径修复(B-2 配套:审核已通过但 roles 未回写的存量用户)
+-- 说明: users.role 为 VARCHAR(20) NOT NULL(迁移 99),不会为 NULL;
+--       role 是 MySQL 8.0 保留关键字,必须用反引号包裹
+-- ------------------------------------------------------------
+
+-- 3.1 预览:营养师已审核通过但 roles 中无 nutritionist
+SELECT id, nickname, phone, `role`, roles, nutritionist_status
+FROM users
+WHERE nutritionist_status = 'approved'
+  AND (roles IS NULL OR roles = '' OR roles NOT LIKE '%nutritionist%');
+
+-- 3.2 预览:规划师已审核通过但 roles 中无 teacher
+SELECT id, nickname, phone, `role`, roles, teacher_status
+FROM users
+WHERE teacher_status = 'approved'
+  AND (roles IS NULL OR roles = '' OR roles NOT LIKE '%teacher%');
+
+-- 3.3 预览:管家已认证但 roles 中无 butler
+SELECT id, nickname, phone, `role`, roles, butler_status
+FROM users
+WHERE butler_status = 'approved'
+  AND (roles IS NULL OR roles = '' OR roles NOT LIKE '%butler%');
+
+-- 3.4 预览:供应商已审核但 roles 中无 supplier
+SELECT id, nickname, phone, `role`, roles, vendor_status
+FROM users
+WHERE vendor_status = 'approved'
+  AND (roles IS NULL OR roles = '' OR roles NOT LIKE '%supplier%');
+
+-- 3.5 确认后回写 roles(JSON 数组格式;roles 为 NULL/空则新建,已有 JSON 数组则追加)
+-- 营养师
+UPDATE users
+SET roles = CASE
+        WHEN roles IS NULL OR roles = '' THEN CONCAT('["', `role`, '","nutritionist"]')
+        WHEN roles LIKE '[%]' THEN REPLACE(roles, ']', ',"nutritionist"]')
+        ELSE CONCAT(roles, ',nutritionist')
+    END
+WHERE nutritionist_status = 'approved'
+  AND (roles IS NULL OR roles = '' OR roles NOT LIKE '%nutritionist%');
+
+-- 规划师
+UPDATE users
+SET roles = CASE
+        WHEN roles IS NULL OR roles = '' THEN CONCAT('["', `role`, '","teacher"]')
+        WHEN roles LIKE '[%]' THEN REPLACE(roles, ']', ',"teacher"]')
+        ELSE CONCAT(roles, ',teacher')
+    END
+WHERE teacher_status = 'approved'
+  AND (roles IS NULL OR roles = '' OR roles NOT LIKE '%teacher%');
+
+-- 管家
+UPDATE users
+SET roles = CASE
+        WHEN roles IS NULL OR roles = '' THEN CONCAT('["', `role`, '","butler"]')
+        WHEN roles LIKE '[%]' THEN REPLACE(roles, ']', ',"butler"]')
+        ELSE CONCAT(roles, ',butler')
+    END
+WHERE butler_status = 'approved'
+  AND (roles IS NULL OR roles = '' OR roles NOT LIKE '%butler%');
+
+-- 供应商(含遗留 vendor 审核通过记录)
+UPDATE users
+SET roles = CASE
+        WHEN roles IS NULL OR roles = '' THEN CONCAT('["', `role`, '","supplier"]')
+        WHEN roles LIKE '[%]' THEN REPLACE(roles, ']', ',"supplier"]')
+        ELSE CONCAT(roles, ',supplier')
+    END
+WHERE vendor_status = 'approved'
+  AND (roles IS NULL OR roles = '' OR roles NOT LIKE '%supplier%');
+
+-- ============================================================
+-- 执行完成后,受影响的已登录用户需重新登录以获取新 token 中的 roles
+-- ============================================================

+ 166 - 0
cfc-backend/src/main/java/com/etotem/cfc/common/AdminRoles.java

@@ -0,0 +1,166 @@
+package com.etotem.cfc.common;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * 后台管理端角色枚举(统一角色口径,见《角色权限与菜单归类整合方案》B-1)
+ *
+ * 平台角色: admin / operator / finance
+ * 入驻角色: butler / knowledge_admin / teacher / nutritionist / supplier / activity_manager
+ * 遗留角色: activity_admin / article_admin / supplier_admin(历史数据兼容,逐步迁移)
+ * C端角色: parent / child —— 不允许登录管理后台
+ */
+public final class AdminRoles {
+
+    private AdminRoles() {
+    }
+
+    /** 平台角色(可看全平台数据) */
+    public static final Set<String> PLATFORM_ROLES = Collections.unmodifiableSet(
+            new HashSet<>(Arrays.asList("admin", "operator", "finance")));
+
+    /** 入驻角色(只能看自己的/绑定的数据,首页为个人版看板) */
+    public static final Set<String> RESIDENT_ROLES = Collections.unmodifiableSet(
+            new HashSet<>(Arrays.asList(
+                    "butler", "knowledge_admin", "teacher", "nutritionist", "supplier", "activity_manager")));
+
+    /** 遗留角色(旧数据兼容) */
+    public static final Set<String> LEGACY_ROLES = Collections.unmodifiableSet(
+            new HashSet<>(Arrays.asList("activity_admin", "article_admin", "supplier_admin")));
+
+    /** C端角色(禁止登录后台) */
+    public static final Set<String> CLIENT_ROLES = Collections.unmodifiableSet(
+            new HashSet<>(Arrays.asList("parent", "child")));
+
+    /** 所有可登录管理后台的角色 */
+    public static final Set<String> BACKEND_ROLES;
+    static {
+        Set<String> all = new HashSet<>();
+        all.addAll(PLATFORM_ROLES);
+        all.addAll(RESIDENT_ROLES);
+        all.addAll(LEGACY_ROLES);
+        BACKEND_ROLES = Collections.unmodifiableSet(all);
+    }
+
+    /** 遗留角色 → 新角色映射 */
+    public static String normalize(String role) {
+        if (role == null) {
+            return null;
+        }
+        switch (role) {
+            case "activity_admin": return "activity_manager";
+            case "article_admin": return "knowledge_admin";
+            case "supplier_admin": return "supplier";
+            default: return role;
+        }
+    }
+
+    public static boolean isBackendRole(String role) {
+        return role != null && BACKEND_ROLES.contains(role);
+    }
+
+    public static boolean isPlatformRole(String role) {
+        return role != null && PLATFORM_ROLES.contains(role);
+    }
+
+    /** roles 列表中是否包含任一平台角色 */
+    public static boolean hasAnyPlatformRole(List<String> roles) {
+        if (roles == null) {
+            return false;
+        }
+        for (String r : roles) {
+            if (isPlatformRole(normalize(r))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** roles 列表中是否包含任一可登录后台的角色(含遗留角色归一化) */
+    public static boolean hasAnyBackendRole(List<String> roles) {
+        if (roles == null) {
+            return false;
+        }
+        for (String r : roles) {
+            if (isBackendRole(normalize(r))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /** roles 列表(归一化后)是否包含指定角色 */
+    public static boolean hasRole(List<String> roles, String target) {
+        if (roles == null) {
+            return false;
+        }
+        for (String r : roles) {
+            if (target.equals(normalize(r))) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 是否为"纯供应商"视角:拥有 supplier 角色且不含任何平台角色(用于商品/订单/库存行级隔离)
+     */
+    public static boolean isSupplierOnly(List<String> roles) {
+        return hasRole(roles, "supplier") && !hasAnyPlatformRole(roles);
+    }
+
+    /**
+     * 是否为"纯活动管理员"视角:拥有 activity_manager 角色且不含任何平台角色(用于活动行级隔离)
+     */
+    public static boolean isActivityManagerOnly(List<String> roles) {
+        return hasRole(roles, "activity_manager") && !hasAnyPlatformRole(roles);
+    }
+
+    /**
+     * 从用户 roles 列表中提取全部后台角色(归一化、去重、保持原顺序);
+     * 列表为空时回退主角色 role 字段。
+     */
+    public static List<String> backendRolesOf(List<String> roles, String mainRole) {
+        Set<String> result = new LinkedHashSet<>();
+        if (roles != null) {
+            for (String r : roles) {
+                String normalized = normalize(r);
+                if (isBackendRole(normalized)) {
+                    result.add(normalized);
+                }
+            }
+        }
+        if (result.isEmpty()) {
+            String normalized = normalize(mainRole);
+            if (isBackendRole(normalized)) {
+                result.add(normalized);
+            }
+        }
+        return new ArrayList<>(result);
+    }
+
+    /** 角色中文标签(登录页身份选择下拉框展示用) */
+    public static String roleLabel(String role) {
+        if (role == null) {
+            return "";
+        }
+        switch (normalize(role)) {
+            case "admin": return "超级管理员";
+            case "operator": return "运营";
+            case "finance": return "财务";
+            case "butler": return "管家";
+            case "knowledge_admin": return "知识管理员";
+            case "teacher": return "成长规划师";
+            case "nutritionist": return "营养师";
+            case "supplier": return "供应商";
+            case "activity_manager": return "活动管理员";
+            default: return role;
+        }
+    }
+}

+ 339 - 87
cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java

@@ -9181,6 +9181,255 @@ private void runMigration100() {
 		ensureColumn("product_skus", "linked_product_id", "BIGINT COMMENT '关联商品ID,null=Legacy SKU'");
 		ensureColumn("product_skus", "label", "VARCHAR(100) COMMENT '自定义显示标签,null=使用linked_product.name'");
 
+		// 迁移248: families 添加 butler_id(B-5 管家行级隔离)
+		ensureColumn("families", "butler_id", "BIGINT COMMENT '绑定的管家userId'");
+		try {
+			jdbcTemplate.execute("CREATE INDEX idx_families_butler_id ON families(butler_id)");
+			log.info("已创建families.butler_id索引");
+		} catch (Exception e) {
+			String msg = e.getMessage();
+			if (msg == null || !(msg.contains("Duplicate") || msg.contains("exists"))) {
+				log.warn("创建families.butler_id索引失败: {}", msg);
+			}
+		}
+
+		// 迁移249: sys_menu 菜单归类调整 + 权限点拆分(《角色权限与菜单归类整合方案》第二部分/B-3,幂等)
+		migrateSysMenuReorg();
+
+		// 迁移250: 清理测试脏数据(B-7:TEST-Energy-Grant 测试任务、乱码昵称)
+		try {
+			int deletedTasks = jdbcTemplate.update("DELETE FROM tasks WHERE title LIKE 'TEST-Energy-Grant%'");
+			int fixedNicknames = jdbcTemplate.update("UPDATE users SET nickname = '测试家庭' WHERE nickname LIKE '娴嬭瘯%' AND nickname <> '测试家庭'");
+			if (deletedTasks > 0 || fixedNicknames > 0) {
+				log.info("迁移250: 已清理测试任务 {} 条,修复乱码昵称 {} 个", deletedTasks, fixedNicknames);
+			}
+		} catch (Exception e) {
+			log.warn("迁移250失败: {}", e.getMessage());
+		}
+
+		// 迁移251: 健康饮食组挂载 报告管理/报告审核/应季食材(《角色权限与菜单归类整合方案》第二部分,幂等)
+		migrateHealthMenuMount();
+
+	}
+
+	/**
+	 * 迁移249: 存量库 sys_menu 菜单归类调整 + 权限点拆分(幂等,按 path/title 定位)
+	 * 1. 审核中心改为分组,归集:服务角色申请审核/活动审核/待退款管理/收益提现审核
+	 * 2. 能量规则/能量行为/周期性服务 → 系统配置·健康配置
+	 * 3. LangGraph 管理 → 报告解析系统
+	 * 4. 测评管理/测评订单/待分配规划师 → 测评咨询(删除系统配置下原"测评管理"子组)
+	 * 5. 菜单组权限统一 模块:* 格式;system:config 拆分为 system:base/config:member/config:dimension/config:health/config:report-parser
+	 * 6. 术语统一:七维能量配置 → 五维能量配置
+	 */
+	private void migrateSysMenuReorg() {
+		try {
+			// A. 审核中心:顶级叶子 → 分组,并插入"审核中心总览"子项
+			jdbcTemplate.update("UPDATE sys_menu SET title='审核中心', label=NULL, path=NULL, perm='audit', sort=3 WHERE path='/review-center' AND parent_id=0 AND title IS NULL");
+			Long reviewGroup = queryMenuId("SELECT id FROM sys_menu WHERE parent_id=0 AND title='审核中心' AND (path IS NULL OR path='')");
+			if (reviewGroup != null) {
+				Integer childCount = jdbcTemplate.queryForObject(
+						"SELECT COUNT(*) FROM sys_menu WHERE parent_id=" + reviewGroup + " AND path='/review-center'", Integer.class);
+				if (childCount == null || childCount == 0) {
+					jdbcTemplate.update(
+						"INSERT INTO sys_menu (parent_id, title, label, path, icon, perm, sort, visible) VALUES (?, NULL, '审核中心总览', '/review-center', 'el-icon-s-check', 'audit', 0, 1)",
+						reviewGroup);
+				}
+				// 迁入审核中心
+				moveMenu("/service-role-applications", reviewGroup, "audit:service-role", 1);
+				moveMenu("/activity-review", reviewGroup, "audit:activity", 2);
+				moveMenu("/pending-refund", reviewGroup, "audit:refund", 3);
+				moveMenu("/family-earnings-withdraw", reviewGroup, "audit:withdraw", 4);
+			}
+
+			// B. 能量规则/能量行为/周期性服务 → 系统配置·健康配置
+			Long healthConfig = queryMenuId("SELECT id FROM sys_menu WHERE title='健康配置' AND (path IS NULL OR path='')");
+			if (healthConfig != null) {
+				jdbcTemplate.update("UPDATE sys_menu SET perm='config:health' WHERE id=" + healthConfig);
+				moveMenu("/energy-rule", healthConfig, "config:health", 3);
+				moveMenu("/energy-behavior-config", healthConfig, "config:health", 4);
+				moveMenu("/periodic-service-config", healthConfig, "config:health", 5);
+			}
+
+			// C. LangGraph 管理 → 报告解析系统
+			Long reportParser = queryMenuId("SELECT id FROM sys_menu WHERE parent_id=0 AND title='报告解析系统'");
+			if (reportParser != null) {
+				jdbcTemplate.update("UPDATE sys_menu SET perm='config:report-parser' WHERE id=" + reportParser);
+				moveMenu("/langgraph-admin", reportParser, "config:report-parser", 4);
+				updateMenuPermByPath("/report-types", "config:report-parser");
+				updateMenuPermByPath("/report-parser-import", "config:report-parser");
+				updateMenuPermByPath("/report-unknown-clusters", "config:report-parser");
+				updateMenuPermByPath("/report-auto-learn", "config:report-parser");
+			}
+
+			// D. 测评管理/测评订单/待分配规划师 → 测评咨询
+			Long assessment = queryMenuId("SELECT id FROM sys_menu WHERE parent_id=0 AND title='测评咨询'");
+			Long oldAssessmentManage = queryMenuId("SELECT id FROM sys_menu WHERE title='测评管理' AND (path IS NULL OR path='')");
+			if (assessment != null) {
+				moveMenu("/assessment-admin", assessment, "assessment:manage", 0);
+				moveMenu("/assessment-orders", assessment, "assessment:orders", 1);
+				moveMenu("/assessment-assign", assessment, "assessment:assign", 2);
+			}
+			if (oldAssessmentManage != null && assessment != null && !oldAssessmentManage.equals(assessment)) {
+				// 子项已迁走,删除系统配置下空的"测评管理"子组及其授权关联
+				jdbcTemplate.update("DELETE FROM sys_role_menu WHERE menu_id=" + oldAssessmentManage);
+				jdbcTemplate.update("DELETE FROM sys_menu WHERE id=" + oldAssessmentManage);
+			}
+
+			// E. 菜单组权限统一 模块:* 格式(废弃 operation/content/commerce/health 单段写法)
+			jdbcTemplate.update("UPDATE sys_menu SET perm='family:*' WHERE parent_id=0 AND title='家庭运营'");
+			jdbcTemplate.update("UPDATE sys_menu SET perm='articles:*' WHERE parent_id=0 AND title='知识中心'");
+			jdbcTemplate.update("UPDATE sys_menu SET perm='commerce:*' WHERE parent_id=0 AND title='商城营销'");
+			jdbcTemplate.update("UPDATE sys_menu SET perm='health:*' WHERE parent_id=0 AND title='健康饮食'");
+
+			// F. system:config 拆分
+			updateMenuPermByPath("/sys-config", "system:base");
+			updateMenuPermByPath("/gates", "system:base");
+			updateMenuPermByPath("/membership-center", "config:member");
+			updateMenuPermByPath("/badge-manage", "config:member");
+			updateMenuPermByPath("/virtual-goods-config", "config:member");
+			updateMenuPermByPath("/dimension-config", "config:dimension");
+			updateMenuPermByPath("/zodiac-configs", "config:dimension");
+			updateMenuPermByPath("/bazi-configs", "config:dimension");
+			updateMenuPermByPath("/blood-type-configs", "config:dimension");
+			updateMenuPermByPath("/health-norm-config", "config:health");
+			updateMenuPermByPath("/health-data-source", "config:health");
+			// 术语统一:七维能量配置 → 五维能量配置
+			jdbcTemplate.update("UPDATE sys_menu SET label='五维能量配置', perm='config:health' WHERE path='/health-energy-config'");
+			updateMenuPermByPath("/virtual-teams", "system:base");
+			updateMenuPermByPath("/knowledge-base", "knowledge:base");
+			updateMenuPermByPath("/health-knowledge", "knowledge:base");
+			updateMenuPermByPath("/supplier-products", "supply:manage");
+			updateMenuPermByPath("/nutrition-products", "health:products");
+			updateMenuPermByPath("/energy-sandbox", "health:energy");
+			// 子组权限
+			jdbcTemplate.update("UPDATE sys_menu SET perm='system:base' WHERE title='基础管理' AND (path IS NULL OR path='')");
+			jdbcTemplate.update("UPDATE sys_menu SET perm='config:dimension' WHERE title='配置中心' AND (path IS NULL OR path='')");
+			jdbcTemplate.update("UPDATE sys_menu SET perm='config:dimension' WHERE title='人生维度' AND (path IS NULL OR path='')");
+			jdbcTemplate.update("UPDATE sys_menu SET perm='system:base' WHERE title='虚拟团队' AND (path IS NULL OR path='')");
+
+			// G. 商城营销叶子权限统一(B-4 映射可识别)
+			updateMenuPermByPath("/product-manage", "commerce:products,commerce:self");
+			updateMenuPermByPath("/inventory", "inventory:self");
+			updateMenuPermByPath("/order-manage", "commerce:orders,order:self");
+			updateMenuPermByPath("/ecom-supplier", "commerce:supplier");
+			updateMenuPermByPath("/product-profit-rate", "finance:profit-rate");
+			updateMenuPermByPath("/coupon", "commerce:coupon");
+			updateMenuPermByPath("/coupon-grant-log", "commerce:coupon");
+			updateMenuPermByPath("/promotion", "commerce:promotion");
+			updateMenuPermByPath("/family-earnings", "finance:family-earnings");
+			updateMenuPermByPath("/activities", "activity:list,activity:self");
+			updateMenuPermByPath("/activity-registration-review", "activity:registration");
+			updateMenuPermByPath("/families", "family:list,family:read,family:bound");
+			updateMenuPermByPath("/children", "family:children,family:read,family:bound");
+			updateMenuPermByPath("/points-log", "family:points,family:bound");
+			updateMenuPermByPath("/growth-task", "task:growth");
+
+			log.info("迁移249: sys_menu 菜单归类调整与权限拆分完成");
+		} catch (Exception e) {
+			log.warn("迁移249失败: {}", e.getMessage());
+		}
+	}
+
+	/** 按 path 更新菜单权限(找不到时忽略) */
+	private void updateMenuPermByPath(String path, String perm) {
+		try {
+			jdbcTemplate.update("UPDATE sys_menu SET perm=? WHERE path=?", perm, path);
+		} catch (Exception ignored) {
+		}
+	}
+
+	/**
+	 * 迁移251: 健康饮食组挂载 报告管理/报告审核/应季食材(存量库 sys_menu 幂等补齐)
+	 * 对应页面 ReportManagement.vue / HealthReportAudit.vue / SeasonalFoods.vue 已开发完成但此前未挂载;
+	 * 同时将健康饮食组排序对齐《浠艾福后台-角色权限与菜单归类整合方案》最终菜单树。
+	 */
+	private void migrateHealthMenuMount() {
+		try {
+			Long healthGroup = queryMenuId("SELECT id FROM sys_menu WHERE parent_id=0 AND title='健康饮食'");
+			if (healthGroup == null) {
+				log.warn("迁移251: 未找到健康饮食分组,跳过");
+				return;
+			}
+			// 1. 存量菜单排序对齐(健康报告0..五维能量11),先更新防止插入冲突
+			jdbcTemplate.update("UPDATE sys_menu SET sort=0 WHERE parent_id=" + healthGroup + " AND path='/health-reports'");
+			jdbcTemplate.update("UPDATE sys_menu SET sort=3 WHERE parent_id=" + healthGroup + " AND path='/health-indicators'");
+			jdbcTemplate.update("UPDATE sys_menu SET sort=4 WHERE parent_id=" + healthGroup + " AND path='/health-checkins'");
+			jdbcTemplate.update("UPDATE sys_menu SET sort=5 WHERE parent_id=" + healthGroup + " AND path='/emotion-alert'");
+			jdbcTemplate.update("UPDATE sys_menu SET sort=6 WHERE parent_id=" + healthGroup + " AND path='/nutrition-mappings'");
+			jdbcTemplate.update("UPDATE sys_menu SET sort=7 WHERE parent_id=" + healthGroup + " AND path='/foods'");
+			jdbcTemplate.update("UPDATE sys_menu SET sort=8 WHERE parent_id=" + healthGroup + " AND path='/recipes'");
+			jdbcTemplate.update("UPDATE sys_menu SET sort=10 WHERE parent_id=" + healthGroup + " AND path='/nutrition-products'");
+			jdbcTemplate.update("UPDATE sys_menu SET sort=11 WHERE parent_id=" + healthGroup + " AND path='/energy-sandbox'");
+			// 2. 按 path 幂等插入 3 个新页面
+			Long reportManagementId = insertHealthMenu(healthGroup, "报告管理", "/report-management", "el-icon-document", "health:reports", 1);
+			Long reportAuditId = insertHealthMenu(healthGroup, "报告审核", "/report-audit", "el-icon-document-checked", "report:audit", 2);
+			Long seasonalFoodsId = insertHealthMenu(healthGroup, "应季食材", "/seasonal-foods", "el-icon-apple", "diet:foods", 9);
+			// 3. 条件授权:仅当已配置过角色授权(sys_role_menu 非空)时补授权,
+			//    避免破坏"所有角色均未授权 → 返回全量可见树"的兼容逻辑(SysMenuService.userMenus)
+			Integer grantCount = jdbcTemplate.queryForObject("SELECT COUNT(*) FROM sys_role_menu", Integer.class);
+			if (grantCount != null && grantCount > 0) {
+				String[] roles = {"admin", "butler", "nutritionist"};
+				Long[] menuIds = {reportManagementId, reportAuditId, seasonalFoodsId};
+				for (String role : roles) {
+					for (Long menuId : menuIds) {
+						grantMenuToRole(role, menuId);
+					}
+				}
+				log.info("迁移251: 已为 admin/butler/nutritionist 补授权新健康菜单");
+			}
+			log.info("迁移251: 健康饮食组挂载 报告管理/报告审核/应季食材完成");
+		} catch (Exception e) {
+			log.warn("迁移251失败: {}", e.getMessage());
+		}
+	}
+
+	/** 按 path 幂等插入健康饮食叶子菜单,返回菜单 id(已存在返回原 id) */
+	private Long insertHealthMenu(Long parentId, String label, String path, String icon, String perm, int sort) {
+		try {
+			Long existed = queryMenuId("SELECT id FROM sys_menu WHERE parent_id=" + parentId + " AND path='" + path + "'");
+			if (existed != null) {
+				return existed;
+			}
+			jdbcTemplate.update(
+				"INSERT INTO sys_menu (parent_id, title, label, path, icon, perm, sort, visible) VALUES (?, NULL, ?, ?, ?, ?, ?, 1)",
+				parentId, label, path, icon, perm, sort
+			);
+			Long id = jdbcTemplate.queryForObject("SELECT LAST_INSERT_ID()", Long.class);
+			log.info("迁移251: 已挂载健康饮食菜单「{}」", label);
+			return id;
+		} catch (Exception e) {
+			log.warn("迁移251: 挂载菜单「{}」失败: {}", label, e.getMessage());
+			return null;
+		}
+	}
+
+	/** 角色-菜单授权(幂等,唯一键 uk_role_menu 冲突忽略) */
+	private void grantMenuToRole(String role, Long menuId) {
+		if (role == null || menuId == null) {
+			return;
+		}
+		try {
+			jdbcTemplate.update("INSERT IGNORE INTO sys_role_menu (role, menu_id) VALUES (?, ?)", role, menuId);
+		} catch (Exception ignored) {
+		}
+	}
+
+	/** 将菜单(按 path)移动到指定父组并更新权限/排序(幂等) */
+	private void moveMenu(String path, Long parentId, String perm, int sort) {
+		try {
+			jdbcTemplate.update("UPDATE sys_menu SET parent_id=?, perm=?, sort=? WHERE path=?", parentId, perm, sort, path);
+		} catch (Exception ignored) {
+		}
+	}
+
+	/** 查询单个菜单 id,不存在返回 null */
+	private Long queryMenuId(String sql) {
+		try {
+			List<Long> ids = jdbcTemplate.queryForList(sql, Long.class);
+			return ids.isEmpty() ? null : ids.get(0);
+		} catch (Exception e) {
+			return null;
+		}
 	}
 
 	/**
@@ -9193,115 +9442,118 @@ private void runMigration100() {
 		String[][] menus = {
 			// 顶级叶子
 			{"", "home", "", "首页", "/dashboard", "el-icon-s-home", "dashboard", "0"},
-			// 家庭运营
-			{"", "operation", "家庭运营", "", "", "el-icon-s-custom", "operation", "1"},
-			{"operation", "families", "", "家庭列表", "/families", "el-icon-s-custom", "family:list", "0"},
-			{"operation", "children", "", "孩子管理", "/children", "el-icon-user-solid", "family:children", "1"},
+			// 家庭运营(family:*)
+			{"", "operation", "家庭运营", "", "", "el-icon-s-custom", "family:*", "1"},
+			{"operation", "families", "", "家庭列表", "/families", "el-icon-s-custom", "family:list,family:read,family:bound", "0"},
+			{"operation", "children", "", "孩子管理", "/children", "el-icon-user-solid", "family:children,family:read,family:bound", "1"},
 			{"operation", "points", "", "积分管理", "/points", "el-icon-s-finance", "family:points", "2"},
-			{"operation", "pointsLog", "", "积分记录", "/points-log", "el-icon-document", "family:points", "3"},
+			{"operation", "pointsLog", "", "积分记录", "/points-log", "el-icon-document", "family:points,family:bound", "3"},
 			{"operation", "wishes", "", "心愿管理", "/wishes", "el-icon-star-off", "family:wishes", "4"},
 			{"operation", "rewards", "", "奖励管理", "/rewards", "el-icon-s-goods", "reward:list", "5"},
-			// 任务中心
+			// 任务中心(task:*)
 			{"", "taskCenter", "任务中心", "", "", "el-icon-s-order", "task:*", "2"},
 			{"taskCenter", "tasks", "", "任务列表", "/tasks", "el-icon-s-order", "task:list", "0"},
 			{"taskCenter", "taskTemplates", "", "任务模板", "/task-templates", "el-icon-document", "task:templates", "1"},
-			{"taskCenter", "growthTask", "", "成长任务管理", "/growth-task", "el-icon-s-management", "task:list", "2"},
-			// 审核中心
-			{"", "reviewCenter", "", "审核中心", "/review-center", "el-icon-s-check", "audit", "3"},
-			{"", "serviceRoleApp", "", "服务角色申请审核", "/service-role-applications", "el-icon-s-check", "audit", "4"},
-			// 商城营销
-			{"", "commerce", "商城营销", "", "", "el-icon-s-goods", "commerce", "5"},
-			{"commerce", "productManage", "", "商品管理", "/product-manage", "el-icon-s-goods", "commerce:products", "0"},
-			{"commerce", "inventory", "", "库存管理", "/inventory", "el-icon-document", "commerce:products", "1"},
-			{"commerce", "ecomSupplier", "", "供应商管理", "/ecom-supplier", "el-icon-s-shop", "commerce:products", "2"},
-			{"commerce", "orderManage", "", "订单管理", "/order-manage", "el-icon-s-order", "commerce:orders", "3"},
-			{"commerce", "pendingRefund", "", "待退款管理", "/pending-refund", "el-icon-warning", "commerce:orders", "4"},
-			{"commerce", "productProfitRate", "", "产品利润率", "/product-profit-rate", "el-icon-data-line", "commerce:profit", "5"},
-			{"commerce", "coupon", "", "优惠券管理", "/coupon", "el-icon-ticket", "marketing:coupon", "6"},
-			{"commerce", "couponGrantLog", "", "发券记录", "/coupon-grant-log", "el-icon-document", "marketing:coupon", "7"},
-			{"commerce", "promotion", "", "推广管理", "/promotion", "el-icon-s-marketing", "marketing:promotion", "8"},
-			{"commerce", "familyEarnings", "", "家庭收益", "/family-earnings", "el-icon-s-money", "marketing:promotion", "9"},
-			{"commerce", "earningWithdraw", "", "收益提现审核", "/family-earnings-withdraw", "el-icon-document-checked", "audit:withdraw", "10"},
-			// 知识中心
-			{"", "knowledge", "知识中心", "", "", "el-icon-document", "content", "6"},
+			{"taskCenter", "growthTask", "", "成长任务管理", "/growth-task", "el-icon-s-management", "task:growth", "2"},
+			// 审核中心(audit,归集各审核页:服务角色申请/活动审核/退款/提现)
+			{"", "reviewCenterGroup", "审核中心", "", "", "el-icon-s-check", "audit", "3"},
+			{"reviewCenterGroup", "reviewCenter", "", "审核中心总览", "/review-center", "el-icon-s-check", "audit", "0"},
+			{"reviewCenterGroup", "serviceRoleApp", "", "服务角色申请审核", "/service-role-applications", "el-icon-s-check", "audit:service-role", "1"},
+			{"reviewCenterGroup", "activityReview", "", "活动审核", "/activity-review", "el-icon-document-checked", "audit:activity", "2"},
+			{"reviewCenterGroup", "pendingRefund", "", "待退款管理", "/pending-refund", "el-icon-warning", "audit:refund", "3"},
+			{"reviewCenterGroup", "earningWithdraw", "", "收益提现审核", "/family-earnings-withdraw", "el-icon-document-checked", "audit:withdraw", "4"},
+			// 商城营销(commerce:*)
+			{"", "commerce", "商城营销", "", "", "el-icon-s-goods", "commerce:*", "4"},
+			{"commerce", "productManage", "", "商品管理", "/product-manage", "el-icon-s-goods", "commerce:products,commerce:self", "0"},
+			{"commerce", "inventory", "", "库存管理", "/inventory", "el-icon-document", "inventory:self", "1"},
+			{"commerce", "orderManage", "", "订单管理", "/order-manage", "el-icon-s-order", "commerce:orders,order:self", "2"},
+			{"commerce", "ecomSupplier", "", "供应商管理", "/ecom-supplier", "el-icon-s-shop", "commerce:supplier", "3"},
+			{"commerce", "productProfitRate", "", "产品利润率", "/product-profit-rate", "el-icon-data-line", "finance:profit-rate", "4"},
+			{"commerce", "coupon", "", "优惠券管理", "/coupon", "el-icon-ticket", "commerce:coupon", "5"},
+			{"commerce", "couponGrantLog", "", "发券记录", "/coupon-grant-log", "el-icon-document", "commerce:coupon", "6"},
+			{"commerce", "promotion", "", "推广管理", "/promotion", "el-icon-s-marketing", "commerce:promotion", "7"},
+			{"commerce", "familyEarnings", "", "家庭收益", "/family-earnings", "el-icon-s-money", "finance:family-earnings", "8"},
+			// 知识中心(articles:*)
+			{"", "knowledge", "知识中心", "", "", "el-icon-document", "articles:*", "5"},
 			{"knowledge", "articleCategories", "", "知识分类", "/article-categories", "el-icon-folder", "articles:categories", "0"},
 			{"knowledge", "articleManage", "", "知识管理", "/article-manage", "el-icon-document", "articles:manage", "1"},
 			{"knowledge", "commentReview", "", "评论审核", "/comment-review", "el-icon-chat-dot-round", "articles:manage", "2"},
 			{"knowledge", "knowledgeTags", "", "知识标签", "/knowledge-tags", "el-icon-price-tag", "articles:categories", "3"},
-			{"knowledge", "knowledgeBase", "", "知识库", "/knowledge-base", "el-icon-reading", "system:config", "4"},
-			{"knowledge", "healthKnowledge", "", "健康知识库", "/health-knowledge", "el-icon-first-aid-kit", "system:config", "5"},
-			// 活动管理
-			{"", "activity", "活动管理", "", "", "el-icon-date", "activity:*", "7"},
-			{"activity", "activities", "", "活动列表", "/activities", "el-icon-date", "activity:list", "0"},
-			{"activity", "activityReview", "", "活动审核", "/activity-review", "el-icon-document-checked", "activity:review", "1"},
-			{"activity", "activityRegReview", "", "活动报名审核", "/activity-registration-review", "el-icon-document-checked", "activity:review", "2"},
-			// 健康饮食
-			{"", "health", "健康饮食", "", "", "el-icon-first-aid-kit", "health", "8"},
+			{"knowledge", "knowledgeBase", "", "知识库", "/knowledge-base", "el-icon-reading", "knowledge:base", "4"},
+			{"knowledge", "healthKnowledge", "", "健康知识库", "/health-knowledge", "el-icon-first-aid-kit", "knowledge:base", "5"},
+			// 活动管理(activity:*,活动审核已移入审核中心)
+			{"", "activity", "活动管理", "", "", "el-icon-date", "activity:*", "6"},
+			{"activity", "activities", "", "活动列表", "/activities", "el-icon-date", "activity:list,activity:self", "0"},
+			{"activity", "activityRegReview", "", "活动报名审核", "/activity-registration-review", "el-icon-document-checked", "activity:registration", "1"},
+			// 健康饮食(health:*,能量规则/行为/周期性服务已移入系统配置·健康配置,LangGraph已移入报告解析系统;报告管理/报告审核/应季食材按方案挂载)
+			{"", "health", "健康饮食", "", "", "el-icon-first-aid-kit", "health:*", "7"},
 			{"health", "healthReports", "", "健康报告", "/health-reports", "el-icon-document", "health:reports", "0"},
-			{"health", "healthIndicators", "", "健康指标", "/health-indicators", "el-icon-data-line", "health:indicators", "1"},
-			{"health", "healthCheckins", "", "健康打卡", "/health-checkins", "el-icon-s-order", "health:checkins", "2"},
-			{"health", "emotionAlert", "", "情绪告警", "/emotion-alert", "el-icon-warning", "health:checkins", "3"},
-			{"health", "nutritionMappings", "", "营养素映射", "/nutrition-mappings", "el-icon-connection", "health:mappings", "4"},
-			{"health", "energySandbox", "", "五维能量", "/energy-sandbox", "el-icon-data-line", "energy", "5"},
-			{"health", "energyRule", "", "能量规则管理", "/energy-rule", "el-icon-setting", "energy", "6"},
-			{"health", "energyBehaviorConfig", "", "能量行为配置", "/energy-behavior-config", "el-icon-edit-outline", "energy", "7"},
-			{"health", "periodicServiceConfig", "", "周期性服务配置", "/periodic-service-config", "el-icon-refresh", "energy", "8"},
-			{"health", "foods", "", "食材管理", "/foods", "el-icon-apple", "diet:foods", "9"},
-			{"health", "recipes", "", "食谱管理", "/recipes", "el-icon-dish", "diet:recipes", "10"},
-			{"health", "langgraphAdmin", "", "LangGraph 管理", "/langgraph-admin", "el-icon-cpu", "system:config", "11"},
-			{"health", "nutritionProducts", "", "营养产品", "/nutrition-products", "el-icon-first-aid-kit", "health:*", "12"},
-			// 报告解析系统
-			{"", "reportParser", "报告解析系统", "", "", "el-icon-cpu", "system:config", "9"},
-			{"reportParser", "reportTypes", "", "报告类型管理", "/report-types", "el-icon-collection", "system:config", "0"},
-			{"reportParser", "reportParserImport", "", "解析器导入", "/report-parser-import", "el-icon-upload2", "system:config", "1"},
-			{"reportParser", "reportUnknownClusters", "", "未知报告审核", "/report-unknown-clusters", "el-icon-question", "system:config", "2"},
-			{"reportParser", "reportAutoLearn", "", "报告自学习", "/report-auto-learn", "el-icon-magic-stick", "system:config", "3"},
-			// 家庭服务(规划师
-			{"", "familyService", "家庭服务", "", "", "el-icon-s-custom", "service:*", "10"},
+			{"health", "reportManagement", "", "报告管理", "/report-management", "el-icon-document", "health:reports", "1"},
+			{"health", "reportAudit", "", "报告审核", "/report-audit", "el-icon-document-checked", "report:audit", "2"},
+			{"health", "healthIndicators", "", "健康指标", "/health-indicators", "el-icon-data-line", "health:indicators", "3"},
+			{"health", "healthCheckins", "", "健康打卡", "/health-checkins", "el-icon-s-order", "health:checkins", "4"},
+			{"health", "emotionAlert", "", "情绪告警", "/emotion-alert", "el-icon-warning", "health:checkins", "5"},
+			{"health", "nutritionMappings", "", "营养素映射", "/nutrition-mappings", "el-icon-connection", "health:mappings", "6"},
+			{"health", "foods", "", "食材管理", "/foods", "el-icon-apple", "diet:foods", "7"},
+			{"health", "recipes", "", "食谱管理", "/recipes", "el-icon-dish", "diet:recipes", "8"},
+			{"health", "seasonalFoods", "", "应季食材", "/seasonal-foods", "el-icon-apple", "diet:foods", "9"},
+			{"health", "nutritionProducts", "", "营养产品", "/nutrition-products", "el-icon-first-aid-kit", "health:products", "10"},
+			{"health", "energySandbox", "", "五维能量", "/energy-sandbox", "el-icon-data-line", "health:energy", "11"},
+			// 报告解析系统(config:report-parser,LangGraph 管理归入)
+			{"", "reportParser", "报告解析系统", "", "", "el-icon-cpu", "config:report-parser", "8"},
+			{"reportParser", "reportTypes", "", "报告类型管理", "/report-types", "el-icon-collection", "config:report-parser", "0"},
+			{"reportParser", "reportParserImport", "", "解析器导入", "/report-parser-import", "el-icon-upload2", "config:report-parser", "1"},
+			{"reportParser", "reportUnknownClusters", "", "未知报告审核", "/report-unknown-clusters", "el-icon-question", "config:report-parser", "2"},
+			{"reportParser", "reportAutoLearn", "", "报告自学习", "/report-auto-learn", "el-icon-magic-stick", "config:report-parser", "3"},
+			{"reportParser", "langgraphAdmin", "", "LangGraph 管理", "/langgraph-admin", "el-icon-cpu", "config:report-parser", "4"},
+			// 家庭服务(service:*
+			{"", "familyService", "家庭服务", "", "", "el-icon-s-custom", "service:*", "9"},
 			{"familyService", "myFamilies", "", "我的家庭", "/my-families", "el-icon-s-custom", "service:family", "0"},
 			{"familyService", "teacherTeam", "", "我的团队", "/teacher-team", "el-icon-s-custom", "service:team", "1"},
-			// 业务管理(规划师
-			{"", "biz", "业务管理", "", "", "el-icon-s-marketing", "biz:*", "11"},
+			// 业务管理(biz:*
+			{"", "biz", "业务管理", "", "", "el-icon-s-marketing", "biz:*", "10"},
 			{"biz", "teacherPackages", "", "任务模板管理", "/teacher-packages", "el-icon-s-goods", "biz:packages", "0"},
 			{"biz", "teacherOrders", "", "订单佣金", "/teacher-orders", "el-icon-s-order", "biz:orders", "1"},
-			// 测评咨询(规划师)
-			{"", "assessment", "测评咨询", "", "", "el-icon-edit", "assessment:*", "12"},
-			{"assessment", "teacherAssessment", "", "DAN测评", "/teacher-assessment", "el-icon-edit", "assessment:dan", "0"},
-			{"assessment", "teacherConsult", "", "家长咨询", "/teacher-consult", "el-icon-chat-dot-round", "assessment:consult", "1"},
-			{"assessment", "growthRecords", "", "成长记录", "/growth-records", "el-icon-document", "growth:records", "2"},
-			{"assessment", "growthPlans", "", "成长计划", "/growth-plans", "el-icon-document", "growth:plans", "3"},
-			// 系统配置(三级)
-			{"", "system", "系统配置", "", "", "el-icon-s-tools", "system:*", "13"},
-			{"system", "baseManage", "基础管理", "", "", "el-icon-s-tools", "system:config", "0"},
-			{"baseManage", "sysConfig", "", "系统配置", "/sys-config", "el-icon-s-tools", "system:config", "0"},
+			// 测评咨询(测评管理/测评订单/待分配规划师已从系统配置移入)
+			{"", "assessment", "测评咨询", "", "", "el-icon-edit", "assessment:*", "11"},
+			{"assessment", "assessmentAdmin", "", "测评管理", "/assessment-admin", "el-icon-edit", "assessment:manage", "0"},
+			{"assessment", "assessmentOrders", "", "测评订单", "/assessment-orders", "el-icon-s-order", "assessment:orders", "1"},
+			{"assessment", "assessmentAssign", "", "待分配规划师", "/assessment-assign", "el-icon-user", "assessment:assign", "2"},
+			{"assessment", "teacherAssessment", "", "DAN测评", "/teacher-assessment", "el-icon-edit", "assessment:dan", "3"},
+			{"assessment", "teacherConsult", "", "家长咨询", "/teacher-consult", "el-icon-chat-dot-round", "assessment:consult", "4"},
+			{"assessment", "growthRecords", "", "成长记录", "/growth-records", "el-icon-document", "growth:records", "5"},
+			{"assessment", "growthPlans", "", "成长计划", "/growth-plans", "el-icon-document", "growth:plans", "6"},
+			// 系统配置(system:*,权限拆分 system:base / config:member / config:dimension / config:health)
+			{"", "system", "系统配置", "", "", "el-icon-s-tools", "system:*", "12"},
+			{"system", "baseManage", "基础管理", "", "", "el-icon-s-tools", "system:base", "0"},
+			{"baseManage", "sysConfig", "", "系统配置", "/sys-config", "el-icon-s-tools", "system:base", "0"},
 			{"baseManage", "users", "", "用户管理", "/users", "el-icon-user", "system:users", "1"},
 			{"baseManage", "operationLogs", "", "操作日志", "/operation-logs", "el-icon-s-order", "system:logs", "2"},
-			{"baseManage", "membershipCenter", "", "会员中心", "/membership-center", "el-icon-s-custom", "system:config", "3"},
-			{"baseManage", "badgeManage", "", "勋章管理", "/badge-manage", "el-icon-medal", "system:config", "4"},
-			{"baseManage", "virtualGoodsConfig", "", "虚拟支付道具", "/virtual-goods-config", "el-icon-goods", "system:config", "5"},
-			{"baseManage", "gates", "", "关卡配置", "/gates", "el-icon-key", "system:config", "6"},
+			{"baseManage", "membershipCenter", "", "会员中心", "/membership-center", "el-icon-s-custom", "config:member", "3"},
+			{"baseManage", "badgeManage", "", "勋章管理", "/badge-manage", "el-icon-medal", "config:member", "4"},
+			{"baseManage", "virtualGoodsConfig", "", "虚拟支付道具", "/virtual-goods-config", "el-icon-goods", "config:member", "5"},
+			{"baseManage", "gates", "", "关卡配置", "/gates", "el-icon-key", "system:base", "6"},
 			{"baseManage", "menuManage", "", "菜单管理", "/menu-manage", "el-icon-menu", "system:menu", "7"},
-			{"system", "configCenter", "配置中心", "", "", "el-icon-setting", "system:config", "1"},
-			{"configCenter", "dimensionConfig", "", "维度配置", "/dimension-config", "el-icon-data-line", "system:config", "0"},
+			{"system", "configCenter", "配置中心", "", "", "el-icon-setting", "config:dimension", "1"},
+			{"configCenter", "dimensionConfig", "", "维度配置", "/dimension-config", "el-icon-data-line", "config:dimension", "0"},
 			{"system", "supplyHierarchy", "供应商体系", "", "", "el-icon-s-management", "system:supply", "2"},
 			{"supplyHierarchy", "supplySystem", "", "供应商体系", "/supply-system", "el-icon-s-management", "system:supply", "0"},
 			{"system", "supplierManage", "供应商管理", "", "", "el-icon-s-shop", "supply:manage", "3"},
 			{"supplierManage", "supplyManage", "", "供应商管理", "/supply-manage", "el-icon-s-shop", "supply:manage", "0"},
-			{"supplierManage", "supplierProducts", "", "商品管理", "/supplier-products", "el-icon-s-goods", "commerce:product", "1"},
-			{"system", "healthConfig", "健康配置", "", "", "el-icon-first-aid-kit", "system:config", "4"},
-			{"healthConfig", "healthNormConfig", "", "健康常模", "/health-norm-config", "el-icon-data-line", "system:config", "0"},
-			{"healthConfig", "healthDataSource", "", "健康数据源", "/health-data-source", "el-icon-data-line", "system:config", "1"},
-			{"healthConfig", "healthEnergyConfig", "", "七维能量配置", "/health-energy-config", "el-icon-data-line", "system:config", "2"},
-			{"system", "lifeDimension", "人生维度", "", "", "el-icon-s-custom", "system:config", "5"},
-			{"lifeDimension", "zodiacConfigs", "", "星座配置", "/zodiac-configs", "el-icon-s-management", "system:config", "0"},
-			{"lifeDimension", "baziConfigs", "", "八字配置", "/bazi-configs", "el-icon-s-management", "system:config", "1"},
-			{"lifeDimension", "bloodTypeConfigs", "", "血型配置", "/blood-type-configs", "el-icon-s-management", "system:config", "2"},
-			{"system", "assessmentManage", "测评管理", "", "", "el-icon-edit", "assessment:dan", "6"},
-			{"assessmentManage", "assessmentAdmin", "", "测评管理", "/assessment-admin", "el-icon-edit", "assessment:dan", "0"},
-			{"assessmentManage", "assessmentOrders", "", "测评订单", "/assessment-orders", "el-icon-s-order", "assessment:dan", "1"},
-			{"assessmentManage", "assessmentAssign", "", "待分配规划师", "/assessment-assign", "el-icon-user", "assessment:dan", "2"},
-			{"system", "virtualTeam", "虚拟团队", "", "", "el-icon-s-custom", "system:config", "7"},
-			{"virtualTeam", "virtualTeams", "", "虚拟团队", "/virtual-teams", "el-icon-s-custom", "system:config", "0"},
+			{"supplierManage", "supplierProducts", "", "商品管理", "/supplier-products", "el-icon-s-goods", "supply:manage", "1"},
+			{"system", "healthConfig", "健康配置", "", "", "el-icon-first-aid-kit", "config:health", "4"},
+			{"healthConfig", "healthNormConfig", "", "健康常模", "/health-norm-config", "el-icon-data-line", "config:health", "0"},
+			{"healthConfig", "healthDataSource", "", "健康数据源", "/health-data-source", "el-icon-data-line", "config:health", "1"},
+			{"healthConfig", "healthEnergyConfig", "", "五维能量配置", "/health-energy-config", "el-icon-data-line", "config:health", "2"},
+			{"healthConfig", "energyRule", "", "能量规则管理", "/energy-rule", "el-icon-setting", "config:health", "3"},
+			{"healthConfig", "energyBehaviorConfig", "", "能量行为配置", "/energy-behavior-config", "el-icon-edit-outline", "config:health", "4"},
+			{"healthConfig", "periodicServiceConfig", "", "周期性服务配置", "/periodic-service-config", "el-icon-refresh", "config:health", "5"},
+			{"system", "lifeDimension", "人生维度", "", "", "el-icon-s-custom", "config:dimension", "5"},
+			{"lifeDimension", "zodiacConfigs", "", "星座配置", "/zodiac-configs", "el-icon-s-management", "config:dimension", "0"},
+			{"lifeDimension", "baziConfigs", "", "八字配置", "/bazi-configs", "el-icon-s-management", "config:dimension", "1"},
+			{"lifeDimension", "bloodTypeConfigs", "", "血型配置", "/blood-type-configs", "el-icon-s-management", "config:dimension", "2"},
+			{"system", "virtualTeam", "虚拟团队", "", "", "el-icon-s-custom", "system:base", "6"},
+			{"virtualTeam", "virtualTeams", "", "虚拟团队", "/virtual-teams", "el-icon-s-custom", "system:base", "0"},
 		};
 
 		for (String[] m : menus) {

+ 1 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/JwtInterceptor.java

@@ -49,6 +49,7 @@ public class JwtInterceptor implements HandlerInterceptor {
         "/api/admin-auth/send-code",
         "/api/admin-auth/login",
         "/api/admin-auth/login-by-password",
+        "/api/admin-auth/account-roles",
         "/api/articles/list",
         "/api/articles/detail",
         "/api/articles/categories",

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/config/WebConfig.java

@@ -105,6 +105,7 @@ public class WebConfig implements WebMvcConfigurer {
                 "/api/admin-auth/send-code",
                 "/api/admin-auth/login",
                 "/api/admin-auth/login-by-password",
+                "/api/admin-auth/account-roles",
                 "/api/product/list",
                 "/api/product/detail",
                 "/api/product/type-list",
@@ -146,6 +147,7 @@ public class WebConfig implements WebMvcConfigurer {
                 "/api/admin-auth/send-code",
                 "/api/admin-auth/login",
                 "/api/admin-auth/login-by-password",
+                "/api/admin-auth/account-roles",
                 "/api/product/list",
                 "/api/product/detail",
                 "/api/product/type-list",

+ 6 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminActivityController.java

@@ -24,10 +24,14 @@ public class AdminActivityController {
     /**
      * 分页查询活动列表(包含草稿)
      * 支持按 status / dimensionCode 过滤
+     * B-5:纯活动管理员登录时仅能看到自己创建/服务的活动
      */
     @PostMapping("/list")
-    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params) {
-        return activityAdminService.list(params);
+    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params,
+                                            @RequestAttribute(value = "userId", required = false) Long userId,
+                                            @RequestAttribute(value = "roles", required = false) java.util.List<String> roleList) {
+        Long vendorUserId = com.etotem.cfc.common.AdminRoles.isActivityManagerOnly(roleList) ? userId : null;
+        return activityAdminService.list(params, vendorUserId);
     }
 
     /**

+ 108 - 36
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminAuthController.java

@@ -1,6 +1,7 @@
 package com.etotem.cfc.controller.admin;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.etotem.cfc.common.AdminRoles;
 import com.etotem.cfc.common.Result;
 import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
 import com.etotem.cfc.config.JwtConfig;
@@ -17,6 +18,7 @@ import org.springframework.beans.factory.annotation.Value;
 import org.springframework.web.bind.annotation.*;
 
 import javax.servlet.http.HttpServletRequest;
+import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashMap;
@@ -70,16 +72,14 @@ public class AdminAuthController {
             return Result.error("该手机号未注册");
         }
 
-        // 检查是否有管理员、规划师、营养师、活动管理员、文章管理员或供应商管理员角色
+        // 检查是否拥有任一后台角色(平台/入驻/遗留兼容,parent/child 不允许登录后台)
         String role = user.getRole();
-        if (!"admin".equals(role) && !"teacher".equals(role)
-                && !"nutritionist".equals(role) && !"activity_admin".equals(role) && !"article_admin".equals(role)
-                && !"supplier_admin".equals(role)
-                && !userService.hasRole(user.getId(), "teacher")
-                && !userService.hasRole(user.getId(), "nutritionist")
-                && !userService.hasRole(user.getId(), "activity_admin")
-                && !userService.hasRole(user.getId(), "article_admin")
-                && !userService.hasRole(user.getId(), "supplier_admin")) {
+        if (AdminRoles.CLIENT_ROLES.contains(role)
+                && !AdminRoles.hasAnyBackendRole(user.getRoles())) {
+            return Result.error("该账号没有管理权限");
+        }
+        if (!AdminRoles.hasAnyBackendRole(user.getRoles())
+                && !AdminRoles.isBackendRole(role)) {
             return Result.error("该账号没有管理权限");
         }
 
@@ -122,21 +122,36 @@ public class AdminAuthController {
 
         loginAttemptService.clearSuccess(lockKey);
 
-        // 使用用户的角色,如果是admin或teacher
-        String role = user.getRole() != null ? user.getRole() : "admin";
+        // 确定角色:优先取后台角色(含遗留角色归一化),parent/child 不允许登录后台
+        List<String> backendRoles = AdminRoles.backendRolesOf(user.getRoles(), user.getRole());
+        if (backendRoles.isEmpty()) {
+            log.warn("用户 {} 尝试验证码登录后台管理,无后台角色", user.getId());
+            return Result.error("该账号没有管理权限");
+        }
+
+        // 身份选择:多角色账号可指定以哪个角色登录
+        String selected = backendRoles.get(0);
+        if (dto.getLoginRole() != null && !dto.getLoginRole().isEmpty()) {
+            String normalized = AdminRoles.normalize(dto.getLoginRole());
+            if (!backendRoles.contains(normalized)) {
+                return Result.error("该账号没有【" + AdminRoles.roleLabel(normalized) + "】身份");
+            }
+            selected = normalized;
+        }
+
         List<String> roles = userService.getUserRoles(user.getId());
         if (roles.isEmpty()) {
-            roles = Collections.singletonList(role);
+            roles = Collections.singletonList(selected);
         }
 
-        String token = jwtConfig.generateToken(user.getId(), roles);
+        String token = jwtConfig.generateToken(user.getId(), buildTokenRoles(selected, roles));
 
         Map<String, Object> result = new HashMap<>();
         result.put("token", token);
         result.put("adminId", user.getId());
         result.put("username", user.getNickname() != null ? user.getNickname() : user.getPhone());
         result.put("realName", user.getRealName() != null ? user.getRealName() : user.getNickname());
-        result.put("role", role);
+        result.put("role", selected);
         result.put("roles", roles);
 
         return Result.success(result);
@@ -280,45 +295,82 @@ public class AdminAuthController {
 
         loginAttemptService.clearSuccess(lockKey);
 
-        // 确定角色
-        String role;
-        String userRole = user.getRole();
-        if ("admin".equals(userRole)) {
-            role = "admin";
-        } else if ("teacher".equals(userRole) || userService.hasRole(user.getId(), "teacher")) {
-            role = "teacher";
-        } else if ("nutritionist".equals(userRole) || userService.hasRole(user.getId(), "nutritionist")) {
-            role = "nutritionist";
-        } else if ("activity_admin".equals(userRole) || userService.hasRole(user.getId(), "activity_admin")) {
-            role = "activity_admin";
-        } else if ("article_admin".equals(userRole) || userService.hasRole(user.getId(), "article_admin")) {
-            role = "article_admin";
-        } else if ("supplier_admin".equals(userRole) || userService.hasRole(user.getId(), "supplier_admin")) {
-            role = "supplier_admin";
-        } else {
-            // 如果用户没有匹配的角色,给个默认角色,但记录警告
-            role = userRole != null ? userRole : "admin";
-            log.warn("用户 {} 登录后台管理,角色为: {}", user.getId(), role);
+        // 确定角色:取第一个后台角色(含遗留角色归一化),parent/child 不允许登录后台
+        List<String> backendRoles = AdminRoles.backendRolesOf(user.getRoles(), user.getRole());
+        if (backendRoles.isEmpty()) {
+            log.warn("用户 {} 尝试密码登录后台管理,无后台角色,角色为: {}", user.getId(), user.getRole());
+            return Result.error("该账号没有管理权限");
+        }
+
+        // 身份选择:多角色账号可指定以哪个角色登录(登录页身份下拉框传入)
+        String selected = backendRoles.get(0);
+        if (dto.getLoginRole() != null && !dto.getLoginRole().isEmpty()) {
+            String normalized = AdminRoles.normalize(dto.getLoginRole());
+            if (!backendRoles.contains(normalized)) {
+                return Result.error("该账号没有【" + AdminRoles.roleLabel(normalized) + "】身份");
+            }
+            selected = normalized;
         }
 
         List<String> roles = userService.getUserRoles(user.getId());
         if (roles.isEmpty()) {
-            roles = Collections.singletonList(role);
+            roles = Collections.singletonList(selected);
         }
 
-        String token = jwtConfig.generateToken(user.getId(), roles);
+        String token = jwtConfig.generateToken(user.getId(), buildTokenRoles(selected, roles));
 
         Map<String, Object> result = new HashMap<>();
         result.put("token", token);
         result.put("adminId", user.getId());
         result.put("username", user.getNickname() != null ? user.getNickname() : user.getPhone());
         result.put("realName", user.getRealName() != null ? user.getRealName() : user.getNickname());
-        result.put("role", role);
+        result.put("role", selected);
         result.put("roles", roles);
 
         return Result.success(result);
     }
 
+    /**
+     * 查询账号可用的后台身份列表(登录页身份选择下拉框用,公开接口)
+     * 返回: [{role: 'admin', label: '超级管理员'}, ...]
+     */
+    @PostMapping("/account-roles")
+    public Result<List<Map<String, String>>> accountRoles(@RequestBody Map<String, String> params) {
+        String phone = params == null ? null : params.get("phone");
+        if (phone == null || phone.isEmpty()) {
+            return Result.error("手机号不能为空");
+        }
+        User user = userMapper.selectOne(new LambdaQueryWrapper<User>()
+                .eq(User::getPhone, phone));
+        List<Map<String, String>> list = new ArrayList<>();
+        if (user != null) {
+            for (String r : AdminRoles.backendRolesOf(user.getRoles(), user.getRole())) {
+                Map<String, String> item = new HashMap<>();
+                item.put("role", r);
+                item.put("label", AdminRoles.roleLabel(r));
+                list.add(item);
+            }
+        }
+        return Result.success(list);
+    }
+
+    /**
+     * 构造 token 中的 roles 列表:选中身份置顶(token 的 role claim 即当前登录身份),其余角色随后去重追加
+     */
+    private List<String> buildTokenRoles(String selected, List<String> allRoles) {
+        List<String> tokenRoles = new ArrayList<>();
+        tokenRoles.add(selected);
+        if (allRoles != null) {
+            for (String r : allRoles) {
+                String normalized = AdminRoles.normalize(r);
+                if (!tokenRoles.contains(normalized)) {
+                    tokenRoles.add(normalized);
+                }
+            }
+        }
+        return tokenRoles;
+    }
+
     private String getClientIP(HttpServletRequest request) {
         String ip = request.getHeader("X-Forwarded-For");
         if (ip == null || ip.isEmpty() || "unknown".equalsIgnoreCase(ip)) {
@@ -330,6 +382,26 @@ public class AdminAuthController {
         return ip;
     }
 
+    /**
+     * 解析用户的后台角色:
+     * 1. 优先从 roles 数组取第一个后台角色(遗留角色归一化为新枚举);
+     * 2. roles 为空时回退主角色 role 字段;
+     * 3. parent/child / 无后台角色 → 返回 null(禁止登录后台)。
+     */
+    private String resolveBackendRole(User user) {
+        List<String> roles = user.getRoles();
+        if (roles != null) {
+            for (String r : roles) {
+                String normalized = AdminRoles.normalize(r);
+                if (AdminRoles.isBackendRole(normalized)) {
+                    return normalized;
+                }
+            }
+        }
+        String normalized = AdminRoles.normalize(user.getRole());
+        return AdminRoles.isBackendRole(normalized) ? normalized : null;
+    }
+
     /**
      * 验证登录密码:兼容旧 MD5 哈希与新 BCrypt 哈希
      * MD5 格式:xzyj_salt_{password} 或 xzyj_admin_salt_{password}

+ 5 - 5
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminButlerController.java

@@ -37,7 +37,7 @@ public class AdminButlerController {
     @PostMapping("/list")
     public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params,
                                              @RequestAttribute("role") String role) {
-        if (!"admin".equals(role)) {
+        if (!"admin".equals(role) && !"finance".equals(role)) {
             return Result.error("权限不足");
         }
 
@@ -71,7 +71,7 @@ public class AdminButlerController {
     @PostMapping("/review")
     public Result<String> review(@RequestBody Map<String, Object> params,
                                   @RequestAttribute("role") String role) {
-        if (!"admin".equals(role)) {
+        if (!"admin".equals(role) && !"finance".equals(role)) {
             return Result.error("权限不足");
         }
 
@@ -96,7 +96,7 @@ public class AdminButlerController {
     @PostMapping("/assign-member")
     public Result<String> assignMember(@RequestBody Map<String, Object> params,
                                         @RequestAttribute("role") String role) {
-        if (!"admin".equals(role)) {
+        if (!"admin".equals(role) && !"finance".equals(role)) {
             return Result.error("权限不足");
         }
 
@@ -113,7 +113,7 @@ public class AdminButlerController {
     @PostMapping("/service-records")
     public Result<Map<String, Object>> serviceRecords(@RequestBody Map<String, Object> params,
                                                        @RequestAttribute("role") String role) {
-        if (!"admin".equals(role)) {
+        if (!"admin".equals(role) && !"finance".equals(role)) {
             return Result.error("权限不足");
         }
 
@@ -143,7 +143,7 @@ public class AdminButlerController {
     @PostMapping("/settlements")
     public Result<Map<String, Object>> settlements(@RequestBody Map<String, Object> params,
                                                     @RequestAttribute("role") String role) {
-        if (!"admin".equals(role)) {
+        if (!"admin".equals(role) && !"finance".equals(role)) {
             return Result.error("权限不足");
         }
 

+ 36 - 3
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminController.java

@@ -2,6 +2,7 @@ package com.etotem.cfc.controller.admin;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.AdminRoles;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.*;
 import com.etotem.cfc.mapper.*;
@@ -485,16 +486,44 @@ public class AdminController {
     // 家庭管理
     @PostMapping("/families")
     public Result<Page<Family>> getFamilies(@RequestBody Map<String, Object> params,
-                                            @RequestAttribute("role") String role) {
-        if (!"admin".equals(role)) {
+                                            @RequestAttribute("userId") Long userId,
+                                            @RequestAttribute("role") String role,
+                                            @RequestAttribute("roles") List<String> roleList) {
+        // B-5 行级隔离:平台角色看全量;管家按 butler_id;规划师/营养师按绑定关系;其他无权限
+        boolean platform = AdminRoles.hasAnyPlatformRole(roleList) || "admin".equals(role)
+                || AdminRoles.hasRole(roleList, "operator") || AdminRoles.hasRole(roleList, "finance");
+        boolean butler = AdminRoles.hasRole(roleList, "butler");
+        boolean bound = AdminRoles.hasRole(roleList, "teacher") || AdminRoles.hasRole(roleList, "nutritionist")
+                || "teacher".equals(role);
+        if (!platform && !butler && !bound) {
             return Result.error("无权限");
         }
         int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
         int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 10;
         String name = (String) params.get("name");
-        
+
         Page<Family> pageParam = new Page<>(page, size);
         LambdaQueryWrapper<Family> wrapper = new LambdaQueryWrapper<>();
+        if (!platform) {
+            if (butler) {
+                // 管家:仅自己服务的家庭
+                wrapper.eq(Family::getButlerId, userId);
+            } else {
+                // 规划师/营养师:仅绑定家庭(复用 teacherFamilyIds 服务绑定关系)
+                User self = userMapper.selectById(userId);
+                String familyIds = self != null ? self.getTeacherFamilyIds() : null;
+                java.util.List<Long> ids = new java.util.ArrayList<>();
+                if (familyIds != null && !familyIds.isEmpty()) {
+                    for (String id : familyIds.split(",")) {
+                        try { ids.add(Long.parseLong(id.trim())); } catch (Exception ignored) {}
+                    }
+                }
+                if (ids.isEmpty()) {
+                    return Result.success(new Page<>(page, size));
+                }
+                wrapper.in(Family::getId, ids);
+            }
+        }
         if (name != null && !name.isEmpty()) {
             wrapper.like(Family::getName, name);
         }
@@ -727,6 +756,8 @@ public class AdminController {
         user.setRole("teacher");
         user.setUpdatedAt(new Date());
         userMapper.updateById(user);
+        // B-2 审核通过回写 users.roles(旧端点兼容)
+        userService.addRole(user.getId(), "teacher");
 
         // 同步创建/更新admins表记录,确保规划师可登录Web管理端并显示规划师菜单
         Admin admin = adminMapper.selectOne(new LambdaQueryWrapper<Admin>()
@@ -830,6 +861,8 @@ public class AdminController {
         user.setNutritionistStatus("approved");
         user.setUpdatedAt(new Date());
         userMapper.updateById(user);
+        // B-2 审核通过回写 users.roles(旧端点兼容)
+        userService.addRole(user.getId(), "nutritionist");
 
         return Result.success(true);
     }

+ 6 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminInventoryController.java

@@ -82,7 +82,9 @@ public class AdminInventoryController {
 
     @Operation(summary = "库存流水列表")
     @PostMapping("/transactions/list")
-    public Result<Map<String, Object>> listTransactions(@RequestBody Map<String, Object> params) {
+    public Result<Map<String, Object>> listTransactions(@RequestBody Map<String, Object> params,
+                                                        @RequestAttribute(value = "userId", required = false) Long userId,
+                                                        @RequestAttribute(value = "roles", required = false) java.util.List<String> roleList) {
         int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
         int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
         Long productId = ParamUtils.getLong(params.get("productId"));
@@ -90,7 +92,9 @@ public class AdminInventoryController {
         String type = (String) params.get("type");
         String startDate = (String) params.get("startDate");
         String endDate = (String) params.get("endDate");
-        return inventoryService.listTransactions(page, size, productId, skuId, type, startDate, endDate);
+        // B-5 行级隔离:纯供应商登录时仅能看到自己商品的库存流水
+        Long supplierUserId = com.etotem.cfc.common.AdminRoles.isSupplierOnly(roleList) ? userId : null;
+        return inventoryService.listTransactions(page, size, productId, skuId, type, startDate, endDate, supplierUserId);
     }
 
     // ========== 盘点 ==========

+ 8 - 1
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductController.java

@@ -2,6 +2,7 @@ package com.etotem.cfc.controller.admin;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.etotem.cfc.common.AdminRoles;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.dto.ProductCreateRequest;
 import com.etotem.cfc.dto.ProductDTO;
@@ -32,7 +33,9 @@ public class AdminProductController {
 
     @Operation(summary = "商品列表")
     @PostMapping("/list")
-    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params) {
+    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params,
+                                            @RequestAttribute(value = "userId", required = false) Long userId,
+                                            @RequestAttribute(value = "roles", required = false) java.util.List<String> roleList) {
         String status = (String) params.get("status");
         String productType = (String) params.get("productType");
         String keyword = (String) params.get("keyword");
@@ -45,6 +48,10 @@ public class AdminProductController {
 
         Page<Product> pageParam = new Page<>(page, size);
         LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<Product>();
+        // B-5 行级隔离:纯供应商登录时仅能看到自己的商品(vendor_id = userId)
+        if (AdminRoles.isSupplierOnly(roleList)) {
+            wrapper.eq(Product::getVendorId, userId);
+        }
         @SuppressWarnings("unchecked")
         List<Map<String, String>> sortSpecs = params.get("sort") != null
                 ? (List<Map<String, String>>) params.get("sort")

+ 6 - 2
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminProductOrderController.java

@@ -26,7 +26,9 @@ public class AdminProductOrderController {
 
     @Operation(summary = "订单列表")
     @PostMapping("/list")
-    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params) {
+    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params,
+                                            @RequestAttribute(value = "userId", required = false) Long userId,
+                                            @RequestAttribute(value = "roles", required = false) java.util.List<String> roleList) {
         String status = (String) params.get("status");
         String keyword = (String) params.get("keyword");
         String startDate = (String) params.get("startDate");
@@ -37,7 +39,9 @@ public class AdminProductOrderController {
         List<Map<String, String>> sortSpecs = params.get("sort") != null
                 ? (List<Map<String, String>>) params.get("sort")
                 : null;
-        return orderService.adminOrderPage(page, size, status, keyword, startDate, endDate, sortSpecs);
+        // B-5 行级隔离:纯供应商登录时仅能看到自己的订单
+        Long supplierUserId = com.etotem.cfc.common.AdminRoles.isSupplierOnly(roleList) ? userId : null;
+        return orderService.adminOrderPage(page, size, status, keyword, startDate, endDate, sortSpecs, supplierUserId);
     }
 
     @Operation(summary = "订单详情")

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/SupplyManageController.java

@@ -114,6 +114,8 @@ public class SupplyManageController {
         user.setNickname(nickname != null ? nickname : "供应商" + phone.substring(7));
         user.setRealName(realName);
         user.setRole("supplier_admin");
+        // B-1/B-2:roles 数组写入供应商角色(遗留角色 supplier_admin 由 AdminRoles.normalize 归一化为 supplier)
+        user.setRoles(java.util.Arrays.asList("supplier_admin", "supplier"));
         user.setVendorStatus("approved"); // 默认通过审核
         user.setVendorType("product_supplier"); // 默认商品供应商
         user.setCreatedAt(new Date());

+ 30 - 10
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/SupplySettlementController.java

@@ -1,11 +1,13 @@
 package com.etotem.cfc.controller.admin;
 
+import com.etotem.cfc.common.AdminRoles;
 import com.etotem.cfc.common.Result;
 import com.etotem.cfc.entity.SupplySettlement;
 import com.etotem.cfc.entity.SupplySettlementDetail;
 import com.etotem.cfc.service.SupplySettlementService;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestAttribute;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
 
@@ -23,49 +25,67 @@ public class SupplySettlementController {
 
     /**
      * 结算记录列表(分页+过滤)
+     * B-5 行级隔离:纯供应商仅能看到自己体系的结算单
      */
     @PostMapping("/list")
-    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params) {
-        return supplySettlementService.list(params);
+    public Result<Map<String, Object>> list(@RequestBody Map<String, Object> params,
+                                            @RequestAttribute(value = "userId", required = false) Long userId,
+                                            @RequestAttribute(value = "roles", required = false) List<String> roleList) {
+        Long supplierUserId = AdminRoles.isSupplierOnly(roleList) ? userId : null;
+        return supplySettlementService.list(params, supplierUserId);
     }
 
     /**
      * 结算详情
      */
     @PostMapping("/detail")
-    public Result<SupplySettlement> detail(@RequestBody Map<String, Object> params) {
+    public Result<SupplySettlement> detail(@RequestBody Map<String, Object> params,
+                                           @RequestAttribute(value = "userId", required = false) Long userId,
+                                           @RequestAttribute(value = "roles", required = false) List<String> roleList) {
         Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) {
             return Result.error("id不能为空");
         }
-        return supplySettlementService.detail(id);
+        Long supplierUserId = AdminRoles.isSupplierOnly(roleList) ? userId : null;
+        return supplySettlementService.detail(id, supplierUserId);
     }
 
     /**
      * 结算明细列表
      */
     @PostMapping("/detail-items")
-    public Result<List<SupplySettlementDetail>> detailItems(@RequestBody Map<String, Object> params) {
+    public Result<List<SupplySettlementDetail>> detailItems(@RequestBody Map<String, Object> params,
+                                                            @RequestAttribute(value = "userId", required = false) Long userId,
+                                                            @RequestAttribute(value = "roles", required = false) List<String> roleList) {
         Long settlementId = ParamUtils.getLong(params.get("settlementId"));
         if (settlementId == null) {
             return Result.error("settlementId不能为空");
         }
-        return supplySettlementService.detailItems(settlementId);
+        Long supplierUserId = AdminRoles.isSupplierOnly(roleList) ? userId : null;
+        return supplySettlementService.detailItems(settlementId, supplierUserId);
     }
 
     /**
-     * 手动生成结算单
+     * 手动生成结算单(平台操作,供应商只读)
      */
     @PostMapping("/create")
-    public Result<SupplySettlement> create(@RequestBody Map<String, Object> params) {
+    public Result<SupplySettlement> create(@RequestBody Map<String, Object> params,
+                                           @RequestAttribute(value = "roles", required = false) List<String> roleList) {
+        if (AdminRoles.isSupplierOnly(roleList)) {
+            return Result.error("供应商无权生成结算单");
+        }
         return supplySettlementService.createSettlement(params);
     }
 
     /**
-     * 确认结算
+     * 确认结算(平台操作,供应商只读)
      */
     @PostMapping("/confirm")
-    public Result<Void> confirm(@RequestBody Map<String, Object> params) {
+    public Result<Void> confirm(@RequestBody Map<String, Object> params,
+                                @RequestAttribute(value = "roles", required = false) List<String> roleList) {
+        if (AdminRoles.isSupplierOnly(roleList)) {
+            return Result.error("供应商无权确认结算");
+        }
         Long id = ParamUtils.getLong(params.get("id"));
         if (id == null) {
             return Result.error("id不能为空");

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/dto/AdminLoginDTO.java

@@ -7,4 +7,6 @@ public class AdminLoginDTO {
     private String phone;
     private String code;
     private String password;
+    /** 登录身份选择(可选):多角色账号指定以哪个角色身份登录,如 admin/teacher/supplier */
+    private String loginRole;
 }

+ 2 - 0
cfc-backend/src/main/java/com/etotem/cfc/entity/Family.java

@@ -28,6 +28,8 @@ public class Family implements Serializable {
 
     private Long teacherId;       // 绑定的成长规划师ID
 
+    private Long butlerId;        // 绑定的管家ID(B-5:管家按 butler_id 行级过滤)
+
     private Long creatorId;       // 家庭创建者(管理员)
 
     private Date createdAt;

+ 3 - 1
cfc-backend/src/main/java/com/etotem/cfc/entity/User.java

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.TableField;
 import com.baomidou.mybatisplus.annotation.TableId;
 import com.baomidou.mybatisplus.annotation.TableLogic;
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
 import com.fasterxml.jackson.annotation.JsonIgnore;
 import lombok.Data;
 
@@ -13,7 +14,7 @@ import java.util.Date;
 import java.util.List;
 
 @Data
-@TableName("users")
+@TableName(value = "users", autoResultMap = true)
 public class User implements Serializable {
 
     @TableId(type = IdType.AUTO)
@@ -29,6 +30,7 @@ public class User implements Serializable {
     private String role;
 
     // 用户拥有的所有角色列表 (JSON格式: ["parent","teacher"] 或逗号分隔)
+    @TableField(typeHandler = JacksonTypeHandler.class)
     private List<String> roles;
 
     private String nickname;

+ 11 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ActivityAdminService.java

@@ -31,6 +31,13 @@ public class ActivityAdminService {
      * 支持按 status / dimensionCode / auditStatus 过滤
      */
     public Result<Map<String, Object>> list(Map<String, Object> params) {
+        return list(params, null);
+    }
+
+    /**
+     * 活动分页列表(B-5:vendorUserId 非空时按活动管理员行级隔离——自己创建/自己作为服务商的活动)
+     */
+    public Result<Map<String, Object>> list(Map<String, Object> params, Long vendorUserId) {
         int page = params.get("page") != null ? ((Number) params.get("page")).intValue() : 1;
         int size = params.get("size") != null ? ((Number) params.get("size")).intValue() : 20;
         String status = (String) params.get("status");
@@ -38,6 +45,10 @@ public class ActivityAdminService {
         String auditStatus = (String) params.get("auditStatus");
 
         LambdaQueryWrapper<Activity> wrapper = new LambdaQueryWrapper<>();
+        if (vendorUserId != null) {
+            wrapper.and(w -> w.eq(Activity::getVendorId, vendorUserId)
+                    .or().eq(Activity::getCreatorId, vendorUserId));
+        }
         if (status != null && !status.isEmpty()) {
             wrapper.eq(Activity::getStatus, status);
         }

+ 5 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/ButlerService.java

@@ -30,6 +30,9 @@ public class ButlerService {
     @Resource
     private ButlerProfileMapper butlerProfileMapper;
 
+    @Resource
+    private UserService userService;
+
     /**
      * 申请成为管家
      * @param userId 申请人用户 ID
@@ -79,6 +82,8 @@ public class ButlerService {
         user.setButlerApprovedAt(new Date());
         user.setUpdatedAt(new Date());
         userMapper.updateById(user);
+        // B-2 审核通过回写 users.roles(旧端点兼容;butler 单向兼容权限由前端权限并集处理)
+        userService.addRole(userId, "butler");
         return Result.success("审核通过");
     }
 

+ 11 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/InventoryService.java

@@ -170,9 +170,20 @@ public class InventoryService {
 
     public Result<Map<String, Object>> listTransactions(int page, int size, Long productId, Long skuId,
                                                          String type, String startDate, String endDate) {
+        return listTransactions(page, size, productId, skuId, type, startDate, endDate, null);
+    }
+
+    /**
+     * 库存流水分页(B-5:supplierUserId 非空时仅能看到自己商品的流水)
+     */
+    public Result<Map<String, Object>> listTransactions(int page, int size, Long productId, Long skuId,
+                                                         String type, String startDate, String endDate, Long supplierUserId) {
         Page<InventoryTransaction> pageParam = new Page<>(page, size);
         LambdaQueryWrapper<InventoryTransaction> wrapper = new LambdaQueryWrapper<InventoryTransaction>()
                 .orderByDesc(InventoryTransaction::getCreatedAt);
+        if (supplierUserId != null) {
+            wrapper.inSql(InventoryTransaction::getProductId, "SELECT id FROM products WHERE vendor_id = " + supplierUserId);
+        }
         if (productId != null) wrapper.eq(InventoryTransaction::getProductId, productId);
         if (skuId != null) wrapper.eq(InventoryTransaction::getSkuId, skuId);
         if (type != null && !type.isEmpty()) wrapper.eq(InventoryTransaction::getType, type);

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

@@ -1055,9 +1055,20 @@ public class ProductOrderService {
     }
 
     public Result<Map<String, Object>> adminOrderPage(int page, int size, String status, String keyword, String startDate, String endDate, List<Map<String, String>> sortSpecs) {
+        return adminOrderPage(page, size, status, keyword, startDate, endDate, sortSpecs, null);
+    }
+
+    /**
+     * 管理端订单分页(B-5:supplierUserId 非空时按供应商行级隔离——自己名下 supplier_id 或自己商品产生的订单)
+     */
+    public Result<Map<String, Object>> adminOrderPage(int page, int size, String status, String keyword, String startDate, String endDate, List<Map<String, String>> sortSpecs, Long supplierUserId) {
         Page<ProductOrder> pageParam = new Page<>(page, size);
         LambdaQueryWrapper<ProductOrder> wrapper = new LambdaQueryWrapper<ProductOrder>()
             .orderByDesc(ProductOrder::getCreatedAt);
+        if (supplierUserId != null) {
+            wrapper.and(w -> w.eq(ProductOrder::getSupplierId, supplierUserId)
+                    .or().inSql(ProductOrder::getProductId, "SELECT id FROM products WHERE vendor_id = " + supplierUserId));
+        }
         if (status != null && !status.isEmpty()) {
             wrapper.eq(ProductOrder::getStatus, status);
         }

+ 77 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/SupplySettlementService.java

@@ -47,11 +47,35 @@ public class SupplySettlementService extends ServiceImpl<SupplySettlementMapper,
      * 分页查询结算记录列表
      */
     public Result<Map<String, Object>> list(Map<String, Object> params) {
+        return list(params, null);
+    }
+
+    /**
+     * 分页查询结算记录列表
+     * B-5 行级隔离:supplierUserId 非空时仅返回该供应商管理的体系下的结算单
+     */
+    public Result<Map<String, Object>> list(Map<String, Object> params, Long supplierUserId) {
         Integer pageNum = params.get("page") != null ? Integer.parseInt(params.get("page").toString()) : 1;
         Integer pageSize = params.get("size") != null ? Integer.parseInt(params.get("size").toString()) : 20;
         Long systemId = params.get("systemId") != null ? ParamUtils.getLong(params.get("systemId")) : null;
         String status = (String) params.get("status");
 
+        // 供应商视角:先解析自己管理的体系 ID 集合
+        List<Long> ownSystemIds = null;
+        if (supplierUserId != null) {
+            List<SupplySystem> ownSystems = supplySystemMapper.selectList(
+                    new LambdaQueryWrapper<SupplySystem>().eq(SupplySystem::getAdminId, supplierUserId));
+            ownSystemIds = ownSystems.stream().map(SupplySystem::getId).collect(Collectors.toList());
+            if (ownSystemIds.isEmpty()) {
+                Map<String, Object> empty = new HashMap<>();
+                empty.put("records", new java.util.ArrayList<>());
+                empty.put("total", 0L);
+                empty.put("page", pageNum);
+                empty.put("size", pageSize);
+                return Result.success(empty);
+            }
+        }
+
         Page<SupplySettlement> pageParam = new Page<>(pageNum, pageSize);
         LambdaQueryWrapper<SupplySettlement> wrapper = new LambdaQueryWrapper<SupplySettlement>()
                 .orderByDesc(SupplySettlement::getCreatedAt);
@@ -59,6 +83,9 @@ public class SupplySettlementService extends ServiceImpl<SupplySettlementMapper,
         if (systemId != null) {
             wrapper.eq(SupplySettlement::getSystemId, systemId);
         }
+        if (ownSystemIds != null) {
+            wrapper.in(SupplySettlement::getSystemId, ownSystemIds);
+        }
         if (status != null && !status.isEmpty()) {
             wrapper.eq(SupplySettlement::getStatus, status);
         }
@@ -77,10 +104,21 @@ public class SupplySettlementService extends ServiceImpl<SupplySettlementMapper,
      * 按 ID 查询结算详情
      */
     public Result<SupplySettlement> detail(Long id) {
+        return detail(id, null);
+    }
+
+    /**
+     * 按 ID 查询结算详情
+     * B-5 行级隔离:supplierUserId 非空时校验结算单归属该供应商管理的体系
+     */
+    public Result<SupplySettlement> detail(Long id, Long supplierUserId) {
         SupplySettlement settlement = this.getById(id);
         if (settlement == null) {
             return Result.error("结算记录不存在");
         }
+        if (supplierUserId != null && !isOwnSettlement(settlement, supplierUserId)) {
+            return Result.error("无权查看该结算记录");
+        }
         return Result.success(settlement);
     }
 
@@ -88,6 +126,23 @@ public class SupplySettlementService extends ServiceImpl<SupplySettlementMapper,
      * 查询结算明细列表
      */
     public Result<List<SupplySettlementDetail>> detailItems(Long settlementId) {
+        return detailItems(settlementId, null);
+    }
+
+    /**
+     * 查询结算明细列表
+     * B-5 行级隔离:supplierUserId 非空时校验结算单归属
+     */
+    public Result<List<SupplySettlementDetail>> detailItems(Long settlementId, Long supplierUserId) {
+        if (supplierUserId != null) {
+            SupplySettlement settlement = this.getById(settlementId);
+            if (settlement == null) {
+                return Result.error("结算记录不存在");
+            }
+            if (!isOwnSettlement(settlement, supplierUserId)) {
+                return Result.error("无权查看该结算记录");
+            }
+        }
         LambdaQueryWrapper<SupplySettlementDetail> wrapper = new LambdaQueryWrapper<>();
         wrapper.eq(SupplySettlementDetail::getSettlementId, settlementId);
         SortUtil.applySort(wrapper);
@@ -95,6 +150,28 @@ public class SupplySettlementService extends ServiceImpl<SupplySettlementMapper,
         return Result.success(items);
     }
 
+    /**
+     * 校验结算单是否属于该供应商管理的体系
+     */
+    private boolean isOwnSettlement(SupplySettlement settlement, Long supplierUserId) {
+        if (settlement.getSystemId() == null) {
+            return false;
+        }
+        SupplySystem system = supplySystemMapper.selectById(settlement.getSystemId());
+        return system != null && supplierUserId.equals(system.getAdminId());
+    }
+
+    /**
+     * 校验是否为供应商管理体系(用于生成/确认结算单的操作权限校验)
+     */
+    public boolean isOwnSystem(Long systemId, Long supplierUserId) {
+        if (systemId == null || supplierUserId == null) {
+            return false;
+        }
+        SupplySystem system = supplySystemMapper.selectById(systemId);
+        return system != null && supplierUserId.equals(system.getAdminId());
+    }
+
     /**
      * 手动生成结算单
      *

+ 17 - 9
cfc-backend/src/main/java/com/etotem/cfc/service/UserService.java

@@ -2,6 +2,7 @@ package com.etotem.cfc.service;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.etotem.cfc.config.JwtConfig;
+import com.etotem.cfc.common.AdminRoles;
 import com.etotem.cfc.dto.*;
 import com.etotem.cfc.dto.RegisterWithIdCardDTO;
 import com.etotem.cfc.dto.RegisterWithInviteCodeDTO;
@@ -651,16 +652,22 @@ private FamilyInvitationService familyInvitationService;
         if (user == null || !isValidRole(newRole)) {
             return false;
         }
-        // 如果用户还没有roles字段,设置默认值
-        if (user.getRoles() == null || user.getRoles().isEmpty()) {
-            user.setRoles(java.util.Collections.singletonList(user.getRole()));
-        }
-        // 检查是否已拥有该角色
-        if (hasRole(user, newRole)) {
+        // 归一化角色列表(可变 List,避免 singletonList 不可变导致 add 抛异常)
+        List<String> owned = new ArrayList<>();
+        if (user.getRoles() != null) {
+            owned.addAll(user.getRoles());
+        } else if (user.getRole() != null) {
+            owned.add(user.getRole());
+        }
+        // 检查是否已拥有该角色(含遗留角色归一化兼容)
+        if (owned.contains(newRole) || owned.contains(com.etotem.cfc.common.AdminRoles.normalize(newRole))
+                || com.etotem.cfc.common.AdminRoles.normalize(newRole) != null
+                && owned.stream().anyMatch(r -> newRole.equals(com.etotem.cfc.common.AdminRoles.normalize(r)))) {
             return true; // 已有该角色
         }
         // 添加新角色
-        user.getRoles().add(newRole);
+        owned.add(newRole);
+        user.setRoles(owned);
         user.setUpdatedAt(new Date());
         userMapper.updateById(user);
         return true;
@@ -680,10 +687,11 @@ private FamilyInvitationService familyInvitationService;
 
     /**
      * 检查角色是否有效
+     * 平台/入驻 9 种新角色 + 遗留 3 种角色兼容 + C 端 parent/child(见 AdminRoles)
      */
     private boolean isValidRole(String role) {
-        return "parent".equals(role) || "child".equals(role) || "teacher".equals(role)
-                || "supplier_admin".equals(role);
+        return "parent".equals(role) || "child".equals(role)
+                || AdminRoles.BACKEND_ROLES.contains(role);
     }
 
 /**

+ 23 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/impl/ServiceRoleApplicationServiceImpl.java

@@ -84,7 +84,11 @@ public class ServiceRoleApplicationServiceImpl
         List<String> roles = JSON.parseArray(app.getRoles(), String.class);
         User user = userMapper.selectById(app.getUserId());
 
+        // 初始化已有角色集合(保持一人多角色)
+        List<String> ownedRoles = user.getRoles() != null ? new java.util.ArrayList<>(user.getRoles()) : new java.util.ArrayList<>();
+
         for (String role : roles) {
+            // 申请角色 → 后台角色枚举映射,审核通过即回写 users.roles(B-2)
             switch (role) {
                 case "planner":
                     Guide guide = new Guide();
@@ -95,9 +99,15 @@ public class ServiceRoleApplicationServiceImpl
                     guide.setDistrict(app.getDistrict());
                     guide.setStreet(app.getStreet());
                     guideMapper.insert(guide);
+                    if (!ownedRoles.contains("teacher")) {
+                        ownedRoles.add("teacher");
+                    }
                     break;
                 case "nutritionist":
                     user.setNutritionistStatus("approved");
+                    if (!ownedRoles.contains("nutritionist")) {
+                        ownedRoles.add("nutritionist");
+                    }
                     break;
                 case "butler":
                     user.setButlerStatus("approved");
@@ -105,18 +115,31 @@ public class ServiceRoleApplicationServiceImpl
                     bp.setUserId(app.getUserId());
                     bp.setStatus("active");
                     butlerProfileMapper.insert(bp);
+                    if (!ownedRoles.contains("butler")) {
+                        ownedRoles.add("butler");
+                    }
                     break;
                 case "vendor":
                     user.setVendorStatus("approved");
+                    if (!ownedRoles.contains("supplier")) {
+                        ownedRoles.add("supplier");
+                    }
                     break;
                 case "article_admin":
                     user.setArticleAdminStatus("approved");
+                    if (!ownedRoles.contains("knowledge_admin")) {
+                        ownedRoles.add("knowledge_admin");
+                    }
                     break;
                 case "activity_provider":
                     user.setActivityProviderStatus("approved");
+                    if (!ownedRoles.contains("activity_manager")) {
+                        ownedRoles.add("activity_manager");
+                    }
                     break;
             }
         }
+        user.setRoles(ownedRoles);
         userMapper.updateById(user);
 
         return Result.success("审核通过");

+ 6 - 6
cfc-backend/src/main/resources/application.yml

@@ -16,9 +16,9 @@ spring:
     name: cfc-backend
   datasource:
     driver-class-name: com.mysql.cj.jdbc.Driver
-    url: jdbc:mysql://mysql-internet-cn-north-1-23feae22680e4dfa.rds.jdcloud.com:3306/zxyj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true
-    username: cfc
-    password: ${DB_PASSWORD:cfc@1314}
+    url: jdbc:mysql://192.168.16.251:3306/zxyj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true
+    username: zxyj
+    password: zxyj@123
     hikari:
       maximum-pool-size: 50
       minimum-idle: 10
@@ -31,9 +31,9 @@ sfms:
   datasource:
     enabled: false  # 本地开发禁用 SFMS 数据源,需要迁移时改为 true
     driver-class-name: com.mysql.cj.jdbc.Driver
-    url: jdbc:mysql://mysql-internet-cn-north-1-23feae22680e4dfa.rds.jdcloud.com:3306/zxyj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true
-    username: cfc
-    password: ${DB_PASSWORD:cfc@1314}
+    url: jdbc:mysql://192.168.16.251:3306/zxyj?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true&createDatabaseIfNotExist=true
+    username: zxyj
+    password: zxyj@123
 
 mybatis-plus:
   mapper-locations: classpath*:/mapper/**/*.xml

+ 4 - 1
cfc-backend/src/main/resources/schema.sql

@@ -6,9 +6,11 @@ CREATE TABLE IF NOT EXISTS families (
     id BIGINT AUTO_INCREMENT PRIMARY KEY,
     name VARCHAR(100),
     invite_code VARCHAR(8) UNIQUE,
+    butler_id BIGINT COMMENT '绑定的管家userId',
     created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
     updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-    INDEX idx_invite_code (invite_code)
+    INDEX idx_invite_code (invite_code),
+    INDEX idx_butler_id (butler_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
 
 -- 用户表
@@ -18,6 +20,7 @@ CREATE TABLE IF NOT EXISTS users (
     unionid VARCHAR(64),
     family_id BIGINT,
     role VARCHAR(20) NOT NULL,
+    roles VARCHAR(100) COMMENT '角色列表JSON/逗号分隔(多角色)',
     nickname VARCHAR(50),
     avatar VARCHAR(255),
     password VARCHAR(128),

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-764ca9df9a568ac9ccde0a6863c313869f3b9c24
+fa23ae473358ce7c2f5c58e38fa26e338e9972c7

+ 1 - 1
cfc-web/package.json

@@ -1,6 +1,6 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1102",
+  "version": "1.0.1103",
   "private": true,
   "scripts": {
     "dev": "vue-cli-service serve",

+ 8 - 0
cfc-web/public/CHANGELOG-v$(node.undefined.md

@@ -3,6 +3,14 @@
 [« 返回最新版本](CHANGELOG.md)
 
 ---
+## v$(node (2026-08-18)
+
+### 其他
+- 修复报错,以及升级JDK
+- 修复报错,以及升级JDK
+- 修复报错,以及升级JDK
+
+
 ## v$(node (2026-08-03)
 
 ### 新功能

+ 7 - 1
cfc-web/public/CHANGELOG.md

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1102
+> 当前版本: v$(node
 
 ## 历史版本
 
@@ -8,6 +8,12 @@
 
 ---
 
+# v1.0 版本历史
+
+[« 返回最新版本](CHANGELOG.md)
+
+---
+
 ## v1.0.1102 (2026-08-17)
 
 ### Bug 修复

+ 9 - 0
cfc-web/src/api/auth.js

@@ -18,6 +18,15 @@ export function adminLogin(data) {
   })
 }
 
+// 查询账号可用的后台身份列表(登录页身份选择下拉框用)
+export function getAccountRoles(phone) {
+  return request({
+    url: '/api/admin-auth/account-roles',
+    method: 'post',
+    data: { phone }
+  })
+}
+
 // 获取管理员信息
 export function getAdminInfo() {
   return request({

+ 61 - 33
cfc-web/src/router/index.js

@@ -2,7 +2,7 @@ import Vue from 'vue'
 import VueRouter from 'vue-router'
 import Layout from '@/views/Layout.vue'
 import axios from 'axios'
-import { getEffectivePermissions, hasPermission } from '@/utils/permissions'
+import { getEffectivePermissions, hasPermission, getRoleHomePage } from '@/utils/permissions'
 
 Vue.use(VueRouter)
 
@@ -70,7 +70,7 @@ const routes = [
         path: 'membership-center',
         name: 'MembershipCenter',
         component: () => import('@/views/admin/MembershipCenter.vue'),
-        meta: { title: '会员中心', perm: 'system:config' }
+        meta: { title: '会员中心', perm: 'config:member' }
       },
       // 任务管理
       {
@@ -232,7 +232,7 @@ const routes = [
         path: 'product-manage',
         name: 'ProductManage',
         component: () => import('@/views/admin/ProductManage.vue'),
-        meta: { title: '商品管理', perm: 'commerce:products' }
+        meta: { title: '商品管理', perm: 'commerce:products,commerce:self' }
       },
       {
         path: 'inventory',
@@ -298,7 +298,7 @@ const routes = [
       path: 'sys-config',
       name: 'SysConfig',
       component: () => import('@/views/admin/SysConfig.vue'),
-      meta: { title: '系统配置', perm: 'system:config' }
+      meta: { title: '系统配置', perm: 'system:base' }
     },
     {
       path: 'menu-manage',
@@ -377,13 +377,13 @@ const routes = [
         path: 'virtual-goods-config',
         name: 'VirtualGoodsConfig',
         component: () => import('@/views/admin/VirtualGoodsConfig.vue'),
-        meta: { title: '虚拟支付道具', perm: 'system:config' }
+        meta: { title: '虚拟支付道具', perm: 'config:member' }
       },
       {
         path: 'gates',
         name: 'Gates',
         component: () => import('@/views/admin/gates.vue'),
-        meta: { title: '关卡配置', perm: 'system:config' }
+        meta: { title: '关卡配置', perm: 'system:base' }
       },
       // ========== 佣金推荐系统 ==========
       {
@@ -473,19 +473,19 @@ const routes = [
         path: 'zodiac-configs',
         name: 'ZodiacConfigs',
         component: () => import('@/views/admin/ZodiacConfigs.vue'),
-        meta: { title: '星座配置', perm: 'system:config' }
+        meta: { title: '星座配置', perm: 'config:dimension' }
       },
       {
         path: 'bazi-configs',
         name: 'BaziConfigs',
         component: () => import('@/views/admin/BaziConfigs.vue'),
-        meta: { title: '八字配置', perm: 'system:config' }
+        meta: { title: '八字配置', perm: 'config:dimension' }
       },
       {
         path: 'blood-type-configs',
         name: 'BloodTypeConfigs',
         component: () => import('@/views/admin/BloodTypeConfigs.vue'),
-        meta: { title: '血型配置', perm: 'system:config' }
+        meta: { title: '血型配置', perm: 'config:dimension' }
       },
 
       // ========== 饮食管理 (admin) ==========
@@ -557,11 +557,29 @@ const routes = [
         component: () => import('@/views/nutritionist/NutritionMappings.vue'),
         meta: { title: '营养素映射管理', perm: 'health:mappings' }
       },
+      {
+        path: 'report-management',
+        name: 'ReportManagement',
+        component: () => import('@/views/admin/ReportManagement.vue'),
+        meta: { title: '报告管理', perm: 'health:reports' }
+      },
+      {
+        path: 'report-audit',
+        name: 'HealthReportAudit',
+        component: () => import('@/views/health/HealthReportAudit.vue'),
+        meta: { title: '报告审核', perm: 'report:audit' }
+      },
+      {
+        path: 'seasonal-foods',
+        name: 'SeasonalFoods',
+        component: () => import('@/views/admin/SeasonalFoods.vue'),
+        meta: { title: '应季食材', perm: 'diet:foods' }
+      },
       {
         path: 'activities',
         name: 'Activities',
         component: () => import('@/views/admin/Activities.vue'),
-        meta: { title: '活动列表', perm: 'activity:list' }
+        meta: { title: '活动列表', perm: 'activity:list,activity:self' }
       },
         {
           path: 'activity-edit',
@@ -640,63 +658,63 @@ const routes = [
         path: 'dimension-config',
         name: 'DimensionConfig',
         component: () => import('@/views/admin/dimension'),
-        meta: { title: '维度配置', perm: 'system:config' }
+        meta: { title: '维度配置', perm: 'config:dimension' }
       },
       {
         path: 'knowledge-base',
         name: 'KnowledgeBase',
         component: () => import('@/views/admin/knowledge'),
-        meta: { title: '知识库管理', perm: 'system:config' }
+        meta: { title: '知识库管理', perm: 'knowledge:base' }
       },
       {
         path: 'health-knowledge',
         name: 'HealthKnowledge',
         component: () => import('@/views/admin/health-knowledge'),
-        meta: { title: '健康知识库管理', perm: 'system:config' }
+        meta: { title: '健康知识库管理', perm: 'knowledge:base' }
       },
       // ========== 健康维度配置 ==========
       {
         path: 'health-norm-config',
         name: 'HealthNormConfig',
         component: () => import('@/views/admin/dimension-config'),
-        meta: { title: '健康常模配置', perm: 'system:config' }
+        meta: { title: '健康常模配置', perm: 'config:health' }
       },
       {
         path: 'health-data-source',
         name: 'HealthDataSource',
         component: () => import('@/views/admin/data-source-config'),
-        meta: { title: '健康数据源配置', perm: 'system:config' }
+        meta: { title: '健康数据源配置', perm: 'config:health' }
       },
       {
         path: 'health-energy-config',
         name: 'HealthEnergyConfig',
         component: () => import('@/views/admin/HealthEnergyConfig'),
-        meta: { title: '七维能量配置', perm: 'system:config' }
+        meta: { title: '五维能量配置', perm: 'config:health' }
       },
       {
         path: 'energy-config',
         name: 'EnergyConfig',
         component: () => import('@/views/admin/energy-config/WuxingConfig'),
-        meta: { title: '五行能量配置', perm: 'system:config' }
+        meta: { title: '五行能量配置', perm: 'config:health' }
       },
       {
         path: 'indicators',
         name: 'IndicatorManage',
         component: () => import('@/views/admin/indicators'),
-        meta: { title: '指标管理', perm: 'system:config' }
+        meta: { title: '指标管理', perm: 'config:health' }
       },
       // ========== 虚拟服务商团队管理 ==========
       {
         path: 'virtual-teams',
         name: 'VirtualTeamList',
         component: () => import('@/views/admin/VirtualTeamList'),
-        meta: { title: '虚拟团队管理', perm: 'system:config' }
+        meta: { title: '虚拟团队管理', perm: 'system:base' }
       },
       {
         path: 'virtual-team-detail/:systemId',
         name: 'VirtualTeamDetail',
         component: () => import('@/views/admin/VirtualTeamDetail'),
-        meta: { title: '团队成员管理', perm: 'system:config' },
+        meta: { title: '团队成员管理', perm: 'system:base' },
         props: true
       },
       // ========== 电商供应商管理 ==========
@@ -710,39 +728,39 @@ const routes = [
         path: 'badge-manage',
         name: 'BadgeManage',
         component: () => import('@/views/admin/BadgeManage'),
-        meta: { title: '勋章管理', perm: 'system:config' }
+        meta: { title: '勋章管理', perm: 'config:member' }
       },
       // ========== 报告指纹解析系统 ==========
       {
         path: 'report-types',
         name: 'ReportTypeManagement',
         component: () => import('@/views/admin/ReportTypeManagement.vue'),
-        meta: { title: '报告类型管理', perm: 'system:config' }
+        meta: { title: '报告类型管理', perm: 'config:report-parser' }
       },
       {
         path: 'report-parser-import',
         name: 'ReportParserImport',
         component: () => import('@/views/admin/ReportParserImport.vue'),
-        meta: { title: '解析器导入管理', perm: 'system:config' }
+        meta: { title: '解析器导入管理', perm: 'config:report-parser' }
       },
       {
         path: 'report-unknown-clusters',
         name: 'ReportUnknownCluster',
         component: () => import('@/views/admin/ReportUnknownCluster.vue'),
-        meta: { title: '未知报告审核', perm: 'system:config' }
+        meta: { title: '未知报告审核', perm: 'config:report-parser' }
       },
       {
         path: 'report-auto-learn',
         name: 'ReportAutoLearn',
         component: () => import('@/views/admin/ReportAutoLearn.vue'),
-        meta: { title: '报告自学习', perm: 'system:config' }
+        meta: { title: '报告自学习', perm: 'config:report-parser' }
       },
       // ========== LangGraph AI 服务管理 ==========
       {
         path: 'langgraph-admin',
         name: 'LangGraphAdmin',
         component: () => import('@/views/admin/LangGraphAdmin.vue'),
-        meta: { title: 'LangGraph 管理', perm: 'system:config' }
+        meta: { title: 'LangGraph 管理', perm: 'config:report-parser' }
       },
       // ========== 营养产品管理 ==========
       {
@@ -755,7 +773,7 @@ const routes = [
         path: 'file-manage',
         name: 'FileManage',
         component: () => import('@/views/admin/FileManage'),
-        meta: { title: '文件管理', perm: 'system:config' }
+        meta: { title: '文件管理', perm: 'system:base' }
       }
     ]
   }
@@ -790,9 +808,14 @@ router.beforeEach(async (to, from, next) => {
 
   // 1. 登录页允许访问
   if (to.path === '/login') {
-    // 如果已有token和role,直接跳转到对应首页
-    if (token && role === 'teacher') {
-      return next('/teacher-dashboard')
+    // 如果已有token和role,直接跳转到对应角色首页
+    if (token && role) {
+      const rolesStr = localStorage.getItem('roles')
+      const roleList = rolesStr ? JSON.parse(rolesStr) : [role]
+      const home = getRoleHomePage(roleList)
+      if (home !== '/') {
+        return next(home)
+      }
     }
     return next()
   }
@@ -819,9 +842,14 @@ router.beforeEach(async (to, from, next) => {
     return next('/login')
   }
 
-  // 3.5 规划师访问 /dashboard → 跳转规划师仪表板
-  if (role === 'teacher' && to.path === '/dashboard') {
-    return next('/teacher-dashboard')
+  // 3.5 入驻角色访问 /dashboard → 跳转各自工作台(B-6 入驻角色默认首页)
+  if (to.path === '/dashboard') {
+    const rolesStr = localStorage.getItem('roles')
+    const roleList = rolesStr ? JSON.parse(rolesStr) : [role]
+    const home = getRoleHomePage(roleList)
+    if (home !== '/') {
+      return next(home)
+    }
   }
 
   // 4. 权限检查:如果路由有 perm 要求,检查用户权限

+ 160 - 29
cfc-web/src/utils/permissions.js

@@ -1,25 +1,63 @@
 /**
- * 角色→权限映射表
+ * 角色→权限映射表(《角色权限与菜单归类整合方案》B-4)
  * 每个角色映射一组权限标识,支持通配符 '*' 和前缀通配 'prefix:*'
+ *
+ * 平台角色: admin / operator / finance
+ * 入驻角色: butler / knowledge_admin / teacher / nutritionist / supplier / activity_manager
+ * 遗留角色: article_manager / activity_admin / supplier_admin(历史数据兼容)
  */
 const ROLE_PERMISSIONS = {
+  // 超管:全部权限
   admin: ['*'],
-  teacher: ['dashboard', 'service:*', 'growth:*', 'biz:*', 'assessment:*', 'messages'],
-  nutritionist: ['dashboard', 'service:family', 'health:*', 'diet:*', 'assessment:dan', 'energy'],
-  article_manager: ['dashboard', 'articles:*'],
-  activity_manager: ['dashboard', 'activity:*'],
-  supplier_admin: ['dashboard', 'commerce:*', 'supply:*', 'system:config'],
+  // 运营:审核全量、活动全量、任务全量、家庭/商城/知识/测评只读
+  operator: ['dashboard', 'audit:*', 'activity:*', 'task:*', 'assessment:read', 'family:read', 'commerce:read', 'articles:read', 'knowledge:read'],
+  // 财务:财务/退款/提现、订单只读、CF值、商城营销、业务管理
+  finance: ['dashboard', 'finance:*', 'audit:withdraw', 'audit:refund', 'commerce:read', 'commerce:orders:read', 'biz:*', 'cf-value:*'],
+  // 管家:家庭客服 + 规划师 + 营养师权限并集(getEffectivePermissions 中自动并入 teacher/nutritionist)
+  butler: ['dashboard', 'family:*', 'task:*', 'reward:*', 'service:*', 'growth:*', 'biz:*',
+           'assessment:dan', 'health:*', 'diet:*', 'report:audit'],
+  // 知识管理员
+  knowledge_admin: ['dashboard', 'articles:*', 'knowledge:*'],
+  // 成长规划师
+  teacher: ['dashboard', 'service:*', 'growth:*', 'biz:*', 'assessment:dan', 'family:bound', 'messages'],
+  // 营养师
+  nutritionist: ['dashboard', 'health:*', 'diet:*', 'report:audit', 'service:family', 'biz:orders', 'messages'],
+  // 供应商(仅自己的商品/库存/订单/结算)+ 业务管理
+  supplier: ['dashboard', 'commerce:self', 'inventory:self', 'order:self', 'settlement:self', 'supply:member', 'biz:*'],
+  // 活动管理员(仅自己的活动)+ 业务管理
+  activity_manager: ['dashboard', 'activity:self', 'activity:registration', 'biz:*'],
+  // ---- 遗留角色兼容(与后端 AdminRoles.normalize 对应)----
+  article_manager: ['dashboard', 'articles:*', 'knowledge:*'],
+  activity_admin: ['dashboard', 'activity:self', 'activity:registration'],
+  supplier_admin: ['dashboard', 'commerce:self', 'inventory:self', 'order:self', 'settlement:self', 'supply:member'],
+}
+
+/**
+ * 遗留角色 → 新角色归一化(与后端 AdminRoles.normalize 保持一致)
+ */
+function normalizeRole(role) {
+  if (!role) return role
+  if (role === 'activity_admin') return 'activity_manager'
+  if (role === 'article_admin' || role === 'article_manager') return 'knowledge_admin'
+  if (role === 'supplier_admin') return 'supplier'
+  return role
 }
 
 /**
  * 获取用户有效权限列表
+ * butler 单向兼容:分配 butler 时自动附带 teacher 与 nutritionist 的权限(权限求并集,角色记录仍为 butler)
  * @param {string[]} roles - 用户拥有的角色列表
  * @returns {string[]} - 去重后的权限标识列表
  */
 export function getEffectivePermissions(roles) {
   if (!roles || roles.length === 0) return []
   const permSet = new Set()
-  for (const role of roles) {
+  const normalizedRoles = []
+  for (let i = 0; i < roles.length; i++) {
+    const normalized = normalizeRole(roles[i])
+    normalizedRoles.push(normalized)
+  }
+  for (const role of normalizedRoles) {
     const perms = ROLE_PERMISSIONS[role]
     if (perms) {
       for (const p of perms) {
@@ -27,45 +65,131 @@ export function getEffectivePermissions(roles) {
       }
     }
   }
+  // B-2 单向兼容:butler 附带 teacher + nutritionist 权限并集
+  if (normalizedRoles.indexOf('butler') >= 0) {
+    const union = [].concat(ROLE_PERMISSIONS.teacher || [], ROLE_PERMISSIONS.nutritionist || [])
+    for (const p of union) {
+      permSet.add(p)
+    }
+  }
   return Array.from(permSet)
 }
 
 /**
  * 判断是否拥有指定权限
  * @param {string[]} userPerms - 用户的有效权限列表
- * @param {string} required - 需要的权限标识(支持 'prefix:*' 通配匹配)
+ * @param {string} required - 需要的权限标识(支持 'prefix:*' 通配、逗号分隔多值任一匹配)
  * @returns {boolean}
  *
  * 匹配规则:
- * - 如果 userPerms 包含 '*' → 返回 true
- * - 如果 required 是 'prefix:sub' 且 userPerms 包含 'prefix:*' → 返回 true
- * - 如果 required 是 'prefix'(无冒号)且 userPerms 包含 'prefix:*' → 返回 true(组级权限)
- * - 如果 userPerms 包含 required → 返回 true
- * - 否则返回 false
+ * - '*' → 全部放行
+ * - required 含逗号 → 任一命中即通过(如 'family:read,family:bound')
+ * - 精确匹配
+ * - required 为单段(组级权限如 'audit')→ 拥有 'audit:*' 或任何 'audit:xxx' 即通过
+ * - required 为 'p:*'(模块通配)→ 拥有 'p:*' 或任何 'p:xxx' 即通过(模块只读/自管可见)
+ * - required 为 'p:sub' → 拥有 'p:*'、'p:read'(模块只读)或任何以 'p:sub:' 开头的更具体权限即通过
  */
 export function hasPermission(userPerms, required) {
   if (!userPerms || !required) return false
-  if (userPerms.includes('*')) return true
+  if (userPerms.indexOf('*') >= 0) return true
+
+  // 逗号分隔多值:任一命中即通过
+  if (required.indexOf(',') >= 0) {
+    const parts = required.split(',')
+    for (let i = 0; i < parts.length; i++) {
+      if (matchSingle(userPerms, parts[i].trim())) return true
+    }
+    return false
+  }
+  return matchSingle(userPerms, required)
+}
 
+function matchSingle(userPerms, required) {
+  if (!required) return false
   // 精确匹配
-  if (userPerms.includes(required)) return true
+  if (userPerms.indexOf(required) >= 0) return true
 
-  // 如果 required 不含冒号(组级权限如 'health', 'audit'),
-  // 检查 userPerms 中是否有 'required:*' 前缀通配
-  var colonIndex = required.indexOf(':')
+  const colonIndex = required.indexOf(':')
   if (colonIndex < 0) {
-    var groupWildcard = required + ':*'
-    if (userPerms.includes(groupWildcard)) return true
-    // 也检查是否有任何以 'required:' 开头的权限
-    for (var i = 0; i < userPerms.length; i++) {
+    // 组级权限 'audit':拥有 'audit:*' 或任何 'audit:xxx'
+    if (userPerms.indexOf(required + ':*') >= 0) return true
+    for (let i = 0; i < userPerms.length; i++) {
       if (userPerms[i].indexOf(required + ':') === 0) return true
     }
-  } else {
-    // 前缀通配匹配: required = 'articles:manage', 检查 'articles:*' 是否在 userPerms 中
-    var prefix = required.substring(0, colonIndex) + ':*'
-    if (userPerms.includes(prefix)) return true
+    return false
+  }
+
+  const prefix = required.substring(0, colonIndex)
+  if (colonIndex === required.length - 2 && required.charAt(required.length - 1) === '*') {
+    // 模块通配 'p:*':拥有 'p:*' 或任何 'p:xxx'
+    if (userPerms.indexOf(prefix + ':*') >= 0) return true
+    for (let i = 0; i < userPerms.length; i++) {
+      if (userPerms[i].indexOf(prefix + ':') === 0) return true
+    }
+    return false
+  }
+
+  // 具体权限 'p:sub'
+  // 1) 拥有模块通配 'p:*'
+  if (userPerms.indexOf(prefix + ':*') >= 0) return true
+  for (let i = 0; i < userPerms.length; i++) {
+    const perm = userPerms[i]
+    // 2) 模块只读 'p:read' 可见模块内页面(数据安全由后端行级隔离保证)
+    if (perm === prefix + ':read') return true
+    // 3) 拥有更具体权限 'p:sub:xxx' 蕴含粗粒度 'p:sub'(如 finance:cf-value:read → cf-value:manage 场景由通配覆盖)
+    if (perm.indexOf(required + ':') === 0) return true
+  }
+  return false
+}
+
+/**
+ * 入驻角色(登录后默认首页为个人版看板)
+ */
+export const RESIDENT_ROLES = ['butler', 'knowledge_admin', 'teacher', 'nutritionist', 'supplier', 'activity_manager']
+
+/**
+ * 入驻角色默认首页(登录后/访问 /dashboard 时跳转到个人工作台,B-6)
+ * 平台角色(admin/operator/finance)返回 '/' 即平台首页
+ */
+const ROLE_HOME_PAGES = {
+  butler: '/my-families',
+  teacher: '/teacher-dashboard',
+  nutritionist: '/health-reports',
+  supplier: '/product-manage',
+  activity_manager: '/activities',
+  knowledge_admin: '/article-manage',
+}
+
+export function getRoleHomePage(roles) {
+  if (!roles || roles.length === 0) return '/'
+  let home = null
+  for (let i = 0; i < roles.length; i++) {
+    const normalized = normalizeRole(roles[i])
+    // 平台角色优先:拥有平台角色则进平台首页
+    if (normalized === 'admin' || normalized === 'operator' || normalized === 'finance') {
+      return '/'
+    }
+    if (!home && ROLE_HOME_PAGES[normalized]) {
+      home = ROLE_HOME_PAGES[normalized]
+    }
   }
+  return home || '/'
+}
 
+/**
+ * 判断角色列表是否为入驻角色(非平台角色 admin/operator/finance)
+ */
+export function isResident(roles) {
+  if (!roles || roles.length === 0) return false
+  for (let i = 0; i < roles.length; i++) {
+    const normalized = normalizeRole(roles[i])
+    if (normalized === 'admin' || normalized === 'operator' || normalized === 'finance') {
+      return false
+    }
+    if (RESIDENT_ROLES.indexOf(normalized) >= 0) {
+      return true
+    }
+  }
   return false
 }
 
@@ -74,12 +198,19 @@ export function hasPermission(userPerms, required) {
  */
 export function getRoleLabel(role) {
   const labels = {
-    admin: '管理员',
+    admin: '超级管理员',
+    operator: '运营',
+    finance: '财务',
+    butler: '管家',
+    knowledge_admin: '知识管理员',
     teacher: '成长规划师',
     nutritionist: '营养师',
-    article_manager: '文章管理员',
+    supplier: '供应商',
     activity_manager: '活动管理员',
-    supplier_admin: '供应商管理员',
+    // 遗留角色
+    article_manager: '知识管理员',
+    activity_admin: '活动管理员',
+    supplier_admin: '供应商',
   }
   return labels[role] || role
 }

+ 114 - 92
cfc-web/src/views/Layout.vue

@@ -168,92 +168,91 @@ export default {
         // ===== 1. 首页 (ALL) =====
         { path: '/dashboard', label: '首页', icon: 'el-icon-s-home', perm: 'dashboard' },
 
-        // ===== 2. 家庭运营 (admin) =====
-        { title: '家庭运营', icon: 'el-icon-s-custom', perm: 'operation',
+        // ===== 2. 家庭运营 (family:*) =====
+        { title: '家庭运营', icon: 'el-icon-s-custom', perm: 'family:*',
           children: [
-            { path: '/families', label: '家庭列表', icon: 'el-icon-s-custom', perm: 'family:list' },
-            { path: '/children', label: '孩子管理', icon: 'el-icon-user-solid', perm: 'family:children' },
+            { path: '/families', label: '家庭列表', icon: 'el-icon-s-custom', perm: 'family:list,family:read,family:bound' },
+            { path: '/children', label: '孩子管理', icon: 'el-icon-user-solid', perm: 'family:children,family:read,family:bound' },
             { path: '/points', label: '积分管理', icon: 'el-icon-s-finance', perm: 'family:points' },
-            { path: '/points-log', label: '积分记录', icon: 'el-icon-document', perm: 'family:points' },
+            { path: '/points-log', label: '积分记录', icon: 'el-icon-document', perm: 'family:points,family:bound' },
             { path: '/wishes', label: '心愿管理', icon: 'el-icon-star-off', perm: 'family:wishes' },
             { path: '/rewards', label: '奖励管理', icon: 'el-icon-s-goods', perm: 'reward:list' },
           ]},
 
-        // ===== 3. 任务中心 (admin) =====
-        { title: '任务中心', icon: 'el-icon-s-order', perm: 'task:*',
+        // ===== 3. 任务中心 (task:*) =====
+        { title: '任务中心', icon: 'el-icon-s-order', perm: 'task:*,task:read',
           children: [
             { path: '/tasks', label: '任务列表', icon: 'el-icon-s-order', perm: 'task:list' },
             { path: '/task-templates', label: '任务模板', icon: 'el-icon-document', perm: 'task:templates' },
-            { path: '/growth-task', label: '成长任务管理', icon: 'el-icon-s-management', perm: 'task:list' },
+            { path: '/growth-task', label: '成长任务管理', icon: 'el-icon-s-management', perm: 'task:growth' },
           ]},
 
-// ===== 4. 审核中心 (admin) =====
-      { path: '/review-center', label: '审核中心', icon: 'el-icon-s-check', perm: 'audit' },
-      { path: '/service-role-applications', label: '服务角色申请审核', icon: 'el-icon-s-check', perm: 'audit' },
-
-
-        // ===== 5. 商城营销 (admin) =====
-        { title: '商城营销', icon: 'el-icon-s-goods', perm: 'commerce',
+        // ===== 4. 审核中心(归集:服务角色申请/活动审核/退款/提现) =====
+        { title: '审核中心', icon: 'el-icon-s-check', perm: 'audit',
           children: [
-            { path: '/product-manage', label: '商品管理', icon: 'el-icon-s-goods', perm: 'commerce:products' },
-            { path: '/inventory', label: '库存管理', icon: 'el-icon-document', perm: 'commerce:products' },
-            { path: '/ecom-supplier', label: '供应商管理', icon: 'el-icon-s-shop', perm: 'commerce:products' },
-            { path: '/order-manage', label: '订单管理', icon: 'el-icon-s-order', perm: 'commerce:orders' },
-            { path: '/pending-refund', label: '待退款管理', icon: 'el-icon-warning', perm: 'commerce:orders' },
-            { path: '/product-profit-rate', label: '产品利润率', icon: 'el-icon-data-line', perm: 'commerce:profit' },
-            { path: '/coupon', label: '优惠券管理', icon: 'el-icon-ticket', perm: 'marketing:coupon' },
-            { path: '/coupon-grant-log', label: '发券记录', icon: 'el-icon-document', perm: 'marketing:coupon' },
-            { path: '/promotion', label: '推广管理', icon: 'el-icon-s-marketing', perm: 'marketing:promotion' },
-            { path: '/family-earnings', label: '家庭收益', icon: 'el-icon-s-money', perm: 'marketing:promotion' },
+            { path: '/review-center', label: '审核中心总览', icon: 'el-icon-s-check', perm: 'audit' },
+            { path: '/service-role-applications', label: '服务角色申请审核', icon: 'el-icon-s-check', perm: 'audit:service-role' },
+            { path: '/activity-review', label: '活动审核', icon: 'el-icon-document-checked', perm: 'audit:activity' },
+            { path: '/pending-refund', label: '待退款管理', icon: 'el-icon-warning', perm: 'audit:refund' },
             { path: '/family-earnings-withdraw', label: '收益提现审核', icon: 'el-icon-document-checked', perm: 'audit:withdraw' },
+          ]},
 
+        // ===== 5. 商城营销 (commerce:*) =====
+        { title: '商城营销', icon: 'el-icon-s-goods', perm: 'commerce:*',
+          children: [
+            { path: '/product-manage', label: '商品管理', icon: 'el-icon-s-goods', perm: 'commerce:products,commerce:self' },
+            { path: '/inventory', label: '库存管理', icon: 'el-icon-document', perm: 'inventory:self' },
+            { path: '/order-manage', label: '订单管理', icon: 'el-icon-s-order', perm: 'commerce:orders,order:self' },
+            { path: '/ecom-supplier', label: '供应商管理', icon: 'el-icon-s-shop', perm: 'commerce:supplier' },
+            { path: '/product-profit-rate', label: '产品利润率', icon: 'el-icon-data-line', perm: 'finance:profit-rate' },
+            { path: '/coupon', label: '优惠券管理', icon: 'el-icon-ticket', perm: 'commerce:coupon' },
+            { path: '/coupon-grant-log', label: '发券记录', icon: 'el-icon-document', perm: 'commerce:coupon' },
+            { path: '/promotion', label: '推广管理', icon: 'el-icon-s-marketing', perm: 'commerce:promotion' },
+            { path: '/family-earnings', label: '家庭收益', icon: 'el-icon-s-money', perm: 'finance:family-earnings' },
           ]},
 
-        // ===== 6. 知识中心 (admin + article_manager + activity_manager) =====
-        { title: '知识中心', icon: 'el-icon-document', perm: 'content',
+        // ===== 6. 知识中心 (articles:*) =====
+        { title: '知识中心', icon: 'el-icon-document', perm: 'articles:*,knowledge:read',
           children: [
             { path: '/article-categories', label: '知识分类', icon: 'el-icon-folder', perm: 'articles:categories' },
             { path: '/article-manage', label: '知识管理', icon: 'el-icon-document', perm: 'articles:manage' },
             { path: '/comment-review', label: '评论审核', icon: 'el-icon-chat-dot-round', perm: 'articles:manage' },
             { path: '/knowledge-tags', label: '知识标签', icon: 'el-icon-price-tag', perm: 'articles:categories' },
-            { path: '/knowledge-base', label: '知识库', icon: 'el-icon-reading', perm: 'system:config' },
-            { path: '/health-knowledge', label: '健康知识库', icon: 'el-icon-first-aid-kit', perm: 'system:config' },
-        
+            { path: '/knowledge-base', label: '知识库', icon: 'el-icon-reading', perm: 'knowledge:base' },
+            { path: '/health-knowledge', label: '健康知识库', icon: 'el-icon-first-aid-kit', perm: 'knowledge:base' },
           ]},
 
-      // ===== 6.5 活动管理(独立模块) =====
-      { title: '活动管理', icon: 'el-icon-date', perm: 'activity:*', children: [
-        { path: '/activities', label: '活动列表', icon: 'el-icon-date', perm: 'activity:list' },
-        { path: '/activity-review', label: '活动审核', icon: 'el-icon-document-checked', perm: 'activity:review' },
-        { path: '/activity-registration-review', label: '活动报名审核', icon: 'el-icon-document-checked', perm: 'activity:review' },
-      ]},
+        // ===== 6.5 活动管理(activity:*,活动审核已移入审核中心) =====
+        { title: '活动管理', icon: 'el-icon-date', perm: 'activity:*', children: [
+          { path: '/activities', label: '活动列表', icon: 'el-icon-date', perm: 'activity:list,activity:self' },
+          { path: '/activity-registration-review', label: '活动报名审核', icon: 'el-icon-document-checked', perm: 'activity:registration' },
+        ]},
 
-        // ===== 7. 健康饮食 (admin + nutritionist) =====
-        { title: '健康饮食', icon: 'el-icon-first-aid-kit', perm: 'health',
+        // ===== 7. 健康饮食 (health:*,报告管理/报告审核/应季食材挂载,顺序对齐方案最终菜单树) =====
+        { title: '健康饮食', icon: 'el-icon-first-aid-kit', perm: 'health:*',
           children: [
             { path: '/health-reports', label: '健康报告', icon: 'el-icon-document', perm: 'health:reports' },
+            { path: '/report-management', label: '报告管理', icon: 'el-icon-document', perm: 'health:reports' },
+            { path: '/report-audit', label: '报告审核', icon: 'el-icon-document-checked', perm: 'report:audit' },
             { path: '/health-indicators', label: '健康指标', icon: 'el-icon-data-line', perm: 'health:indicators' },
             { path: '/health-checkins', label: '健康打卡', icon: 'el-icon-s-order', perm: 'health:checkins' },
             { path: '/emotion-alert', label: '情绪告警', icon: 'el-icon-warning', perm: 'health:checkins' },
             { path: '/nutrition-mappings', label: '营养素映射', icon: 'el-icon-connection', perm: 'health:mappings' },
-            { path: '/energy-sandbox', label: '五维能量', icon: 'el-icon-data-line', perm: 'energy' },
-            { path: '/energy-rule', label: '能量规则管理', icon: 'el-icon-setting', perm: 'energy' },
-            { path: '/energy-behavior-config', label: '能量行为配置', icon: 'el-icon-edit-outline', perm: 'energy' },
-            { path: '/periodic-service-config', label: '周期性服务配置', icon: 'el-icon-refresh', perm: 'energy' },
             { path: '/foods', label: '食材管理', icon: 'el-icon-apple', perm: 'diet:foods' },
             { path: '/recipes', label: '食谱管理', icon: 'el-icon-dish', perm: 'diet:recipes' },
-            
-            { path: '/langgraph-admin', label: 'LangGraph 管理', icon: 'el-icon-cpu', perm: 'system:config' },
-            { path: '/nutrition-products', label: '营养产品', icon: 'el-icon-first-aid-kit', perm: 'health:*' },
+            { path: '/seasonal-foods', label: '应季食材', icon: 'el-icon-apple', perm: 'diet:foods' },
+            { path: '/nutrition-products', label: '营养产品', icon: 'el-icon-first-aid-kit', perm: 'health:products' },
+            { path: '/energy-sandbox', label: '五维能量', icon: 'el-icon-data-line', perm: 'health:energy' },
           ]},
 
-        // ===== 报告解析系统 (admin) =====
-        { title: '报告解析系统', icon: 'el-icon-cpu', perm: 'system:config',
+        // ===== 8. 报告解析系统 (config:report-parser,LangGraph 管理归入) =====
+        { title: '报告解析系统', icon: 'el-icon-cpu', perm: 'config:report-parser',
           children: [
-            { path: '/report-types', label: '报告类型管理', icon: 'el-icon-collection', perm: 'system:config' },
-            { path: '/report-parser-import', label: '解析器导入', icon: 'el-icon-upload2', perm: 'system:config' },
-            { path: '/report-unknown-clusters', label: '未知报告审核', icon: 'el-icon-question', perm: 'system:config' },
-            { path: '/report-auto-learn', label: '报告自学习', icon: 'el-icon-magic-stick', perm: 'system:config' },
+            { path: '/report-types', label: '报告类型管理', icon: 'el-icon-collection', perm: 'config:report-parser' },
+            { path: '/report-parser-import', label: '解析器导入', icon: 'el-icon-upload2', perm: 'config:report-parser' },
+            { path: '/report-unknown-clusters', label: '未知报告审核', icon: 'el-icon-question', perm: 'config:report-parser' },
+            { path: '/report-auto-learn', label: '报告自学习', icon: 'el-icon-magic-stick', perm: 'config:report-parser' },
+            { path: '/langgraph-admin', label: 'LangGraph 管理', icon: 'el-icon-cpu', perm: 'config:report-parser' },
           ]},
 
         { title: '家庭服务', icon: 'el-icon-s-custom', perm: 'service:*',
@@ -266,70 +265,70 @@ export default {
             { path: '/teacher-packages', label: '任务模板管理', icon: 'el-icon-s-goods', perm: 'biz:packages' },
             { path: '/teacher-orders', label: '订单佣金', icon: 'el-icon-s-order', perm: 'biz:orders' },
           ]},
-        { title: '测评咨询', icon: 'el-icon-edit', perm: 'assessment:*',
+        // ===== 测评咨询(测评管理/订单/待分配规划师已从系统配置移入) =====
+        { title: '测评咨询', icon: 'el-icon-edit', perm: 'assessment:*,assessment:read',
           children: [
+            { path: '/assessment-admin', label: '测评管理', icon: 'el-icon-edit', perm: 'assessment:manage' },
+            { path: '/assessment-orders', label: '测评订单', icon: 'el-icon-s-order', perm: 'assessment:orders' },
+            { path: '/assessment-assign', label: '待分配规划师', icon: 'el-icon-user', perm: 'assessment:assign' },
             { path: '/teacher-assessment', label: 'DAN测评', icon: 'el-icon-edit', perm: 'assessment:dan' },
             { path: '/teacher-consult', label: '家长咨询', icon: 'el-icon-chat-dot-round', perm: 'assessment:consult' },
             { path: '/growth-records', label: '成长记录', icon: 'el-icon-document', perm: 'growth:records' },
             { path: '/growth-plans', label: '成长计划', icon: 'el-icon-document', perm: 'growth:plans' },
           ]},
 
-        // ===== 9. 系统配置 (admin) =====
+        // ===== 9. 系统配置 (system:*,权限拆分 system:base/config:member/config:dimension/config:health) =====
         { title: '系统配置', icon: 'el-icon-s-tools', perm: 'system:*',
           children: [
             // --- 基础管理 ---
-            { title: '基础管理', icon: 'el-icon-s-tools', perm: 'system:config',
+            { title: '基础管理', icon: 'el-icon-s-tools', perm: 'system:base',
               children: [
-                { path: '/sys-config', label: '系统配置', icon: 'el-icon-s-tools', perm: 'system:config' },
+                { path: '/sys-config', label: '系统配置', icon: 'el-icon-s-tools', perm: 'system:base' },
                 { path: '/users', label: '用户管理', icon: 'el-icon-user', perm: 'system:users' },
                 { path: '/operation-logs', label: '操作日志', icon: 'el-icon-s-order', perm: 'system:logs' },
-                { path: '/membership-center', label: '会员中心', icon: 'el-icon-s-custom', perm: 'system:config' },
-                { path: '/badge-manage', label: '勋章管理', icon: 'el-icon-medal', perm: 'system:config' },
-                { path: '/virtual-goods-config', label: '虚拟支付道具', icon: 'el-icon-goods', perm: 'system:config' },
-                { path: '/gates', label: '关卡配置', icon: 'el-icon-key', perm: 'system:config' },
-                { path: '/file-manage', label: '文件管理', icon: 'el-icon-files', perm: 'system:config' },
+                { path: '/membership-center', label: '会员中心', icon: 'el-icon-s-custom', perm: 'config:member' },
+                { path: '/badge-manage', label: '勋章管理', icon: 'el-icon-medal', perm: 'config:member' },
+                { path: '/virtual-goods-config', label: '虚拟支付道具', icon: 'el-icon-goods', perm: 'config:member' },
+                { path: '/gates', label: '关卡配置', icon: 'el-icon-key', perm: 'system:base' },
+                { path: '/file-manage', label: '文件管理', icon: 'el-icon-files', perm: 'system:base' },
               ]},
             // --- 配置中心 ---
-            { title: '配置中心', icon: 'el-icon-setting', perm: 'system:config',
+            { title: '配置中心', icon: 'el-icon-setting', perm: 'config:dimension',
               children: [
-                { path: '/dimension-config', label: '维度配置', icon: 'el-icon-data-line', perm: 'system:config' },
+                { path: '/dimension-config', label: '维度配置', icon: 'el-icon-data-line', perm: 'config:dimension' },
               ]},
             // --- 供应商体系 ---
             { title: '供应商体系', icon: 'el-icon-s-management', perm: 'system:supply',
               children: [
                 { path: '/supply-system', label: '供应商体系', icon: 'el-icon-s-management', perm: 'system:supply' },
               ]},
-            // --- 供应商管理 (supplier_admin) ---
-  { title: '供应商管理', icon: 'el-icon-s-shop', perm: 'supply:manage',
-    children: [
-      { path: '/supply-manage', label: '供应商管理', icon: 'el-icon-s-shop', perm: 'supply:manage' },
-      { path: '/supplier-products', label: '商品管理', icon: 'el-icon-s-goods', perm: 'commerce:product' }
-    ]},
-            // --- 健康配置 ---
-            { title: '健康配置', icon: 'el-icon-first-aid-kit', perm: 'system:config',
+            // --- 供应商管理 ---
+            { title: '供应商管理', icon: 'el-icon-s-shop', perm: 'supply:manage',
               children: [
-                { path: '/health-norm-config', label: '健康常模', icon: 'el-icon-data-line', perm: 'system:config' },
-                { path: '/health-data-source', label: '健康数据源', icon: 'el-icon-data-line', perm: 'system:config' },
-                { path: '/health-energy-config', label: '七维能量配置', icon: 'el-icon-data-line', perm: 'system:config' },
+                { path: '/supply-manage', label: '供应商管理', icon: 'el-icon-s-shop', perm: 'supply:manage' },
+                { path: '/supplier-products', label: '商品管理', icon: 'el-icon-s-goods', perm: 'supply:manage' },
               ]},
-            // --- 人生维度 ---
-            { title: '人生维度', icon: 'el-icon-s-custom', perm: 'system:config',
+            // --- 健康配置(能量规则/行为/周期性服务已从健康饮食移入;术语统一:五维能量配置) ---
+            { title: '健康配置', icon: 'el-icon-first-aid-kit', perm: 'config:health',
               children: [
-                { path: '/zodiac-configs', label: '星座配置', icon: 'el-icon-s-management', perm: 'system:config' },
-                { path: '/bazi-configs', label: '八字配置', icon: 'el-icon-s-management', perm: 'system:config' },
-                { path: '/blood-type-configs', label: '血型配置', icon: 'el-icon-s-management', perm: 'system:config' },
+                { path: '/health-norm-config', label: '健康常模', icon: 'el-icon-data-line', perm: 'config:health' },
+                { path: '/health-data-source', label: '健康数据源', icon: 'el-icon-data-line', perm: 'config:health' },
+                { path: '/health-energy-config', label: '五维能量配置', icon: 'el-icon-data-line', perm: 'config:health' },
+                { path: '/energy-rule', label: '能量规则管理', icon: 'el-icon-setting', perm: 'config:health' },
+                { path: '/energy-behavior-config', label: '能量行为配置', icon: 'el-icon-edit-outline', perm: 'config:health' },
+                { path: '/periodic-service-config', label: '周期性服务配置', icon: 'el-icon-refresh', perm: 'config:health' },
               ]},
-            // --- 测评管理 ---
-            { title: '测评管理', icon: 'el-icon-edit', perm: 'assessment:dan',
+            // --- 人生维度 ---
+            { title: '人生维度', icon: 'el-icon-s-custom', perm: 'config:dimension',
               children: [
-                { path: '/assessment-admin', label: '测评管理', icon: 'el-icon-edit', perm: 'assessment:dan' },
-                { path: '/assessment-orders', label: '测评订单', icon: 'el-icon-s-order', perm: 'assessment:dan' },
-                { path: '/assessment-assign', label: '待分配规划师', icon: 'el-icon-user', perm: 'assessment:dan' },
+                { path: '/zodiac-configs', label: '星座配置', icon: 'el-icon-s-management', perm: 'config:dimension' },
+                { path: '/bazi-configs', label: '八字配置', icon: 'el-icon-s-management', perm: 'config:dimension' },
+                { path: '/blood-type-configs', label: '血型配置', icon: 'el-icon-s-management', perm: 'config:dimension' },
               ]},
             // --- 虚拟团队 ---
-            { title: '虚拟团队', icon: 'el-icon-s-custom', perm: 'system:config',
+            { title: '虚拟团队', icon: 'el-icon-s-custom', perm: 'system:base',
               children: [
-                { path: '/virtual-teams', label: '虚拟团队', icon: 'el-icon-s-custom', perm: 'system:config' },
+                { path: '/virtual-teams', label: '虚拟团队', icon: 'el-icon-s-custom', perm: 'system:base' },
               ]},
           ]},
       ],
@@ -385,11 +384,12 @@ export default {
       return this.$route.path
     },
     // 动态菜单优先,未加载/加载失败时回退硬编码 menuItems
+    // B-7:按 title/path 去重,修复侧边栏菜单重复渲染 bug
     effectiveMenuItems() {
-      if (this.dynamicMenuItems && this.dynamicMenuItems.length > 0) {
-        return this.dynamicMenuItems
-      }
-      return this.menuItems
+      const source = (this.dynamicMenuItems && this.dynamicMenuItems.length > 0)
+        ? this.dynamicMenuItems
+        : this.menuItems
+      return this.dedupeMenuItems(source)
     },
     currentRole() {
       return localStorage.getItem('role') || 'admin'
@@ -398,9 +398,8 @@ export default {
       return localStorage.getItem('vendorType') || ''
     },
     effectivePerms() {
-      const rolesStr = localStorage.getItem('roles')
-      const roles = rolesStr ? JSON.parse(rolesStr) : [this.currentRole]
-      return getEffectivePermissions(roles)
+      // 菜单按当前登录身份隔离(登录时选择的身份),多角色账号不再默认并集展示
+      return getEffectivePermissions([this.currentRole])
     },
     displayRoles() {
       const rolesStr = localStorage.getItem('roles')
@@ -441,6 +440,29 @@ export default {
     window.removeEventListener('resize', this.handleResize)
   },
   methods: {
+    /**
+     * 菜单去重:同层级内按 (title || path) + path 组合键去重,递归处理 children
+     * 防止后端动态菜单与硬编码/全量菜单数据重叠时侧边栏重复渲染
+     */
+    dedupeMenuItems(items) {
+      if (!items || items.length === 0) return items
+      const seen = {}
+      const result = []
+      for (let i = 0; i < items.length; i++) {
+        const it = items[i]
+        const key = (it.title || '') + '|' + (it.path || '') + '|' + (it.label || '')
+        if (seen[key]) continue
+        seen[key] = true
+        if (it.children && it.children.length > 0) {
+          const copy = Object.assign({}, it)
+          copy.children = this.dedupeMenuItems(it.children)
+          result.push(copy)
+        } else {
+          result.push(it)
+        }
+      }
+      return result
+    },
     hasPerm(required) {
       return hasPermission(this.effectivePerms, required)
     },

+ 77 - 5
cfc-web/src/views/Login.vue

@@ -148,6 +148,7 @@
               placeholder="用户名 / 手机号"
               size="large"
               :disabled="loading"
+              @input="onPhoneInput"
             >
               <i slot="prefix" class="el-icon-user input-icon"></i>
             </el-input>
@@ -166,6 +167,24 @@
             </el-input>
           </el-form-item>
 
+          <el-form-item prop="loginRole">
+            <el-select
+              v-model="loginForm.loginRole"
+              placeholder="登录身份(输入手机号后自动带出)"
+              size="large"
+              class="identity-select"
+              :disabled="loading || identityOptions.length === 0"
+              :loading="identityLoading"
+            >
+              <el-option
+                v-for="item in identityOptions"
+                :key="item.role"
+                :label="item.label"
+                :value="item.role"
+              ></el-option>
+            </el-select>
+          </el-form-item>
+
           <el-form-item prop="captcha" class="captcha-item">
             <SliderCaptcha
               ref="sliderCaptcha"
@@ -196,7 +215,8 @@
 </template>
 
 <script>
-import { adminLogin } from '@/api/auth'
+import { adminLogin, getAccountRoles } from '@/api/auth'
+import { getRoleHomePage } from '@/utils/permissions'
 import SliderCaptcha from '@/components/SliderCaptcha'
 
 export default {
@@ -206,7 +226,8 @@ export default {
     return {
       loginForm: {
         username: '',
-        password: ''
+        password: '',
+        loginRole: ''
       },
       rules: {
         username: [
@@ -219,7 +240,11 @@ export default {
         ]
       },
       loading: false,
-      captchaPassed: false
+      captchaPassed: false,
+      identityOptions: [],
+      identityLoading: false,
+      lastFetchedPhone: '',
+      phoneInputTimer: null
     }
   },
   mounted() {
@@ -239,6 +264,43 @@ export default {
     onCaptchaSuccess() {
       this.captchaPassed = true
     },
+    onPhoneInput() {
+      // 防抖:输入停止 500ms 后自动查询身份
+      if (this.phoneInputTimer) {
+        clearTimeout(this.phoneInputTimer)
+      }
+      var self = this
+      this.phoneInputTimer = setTimeout(function() {
+        self.fetchIdentities()
+      }, 500)
+    },
+    async fetchIdentities() {
+      const phone = (this.loginForm.username || '').trim()
+      if (!phone || phone === this.lastFetchedPhone) {
+        return
+      }
+      this.lastFetchedPhone = phone
+      this.identityLoading = true
+      try {
+        const res = await getAccountRoles(phone)
+        const list = (res && res.data) ? res.data : []
+        this.identityOptions = list
+        if (list.length > 0) {
+          // 默认选中第一个身份;当前选中值不在列表中时重置
+          const exists = list.some(function(it) { return it.role === this.loginForm.loginRole }.bind(this))
+          if (!exists) {
+            this.loginForm.loginRole = list[0].role
+          }
+        } else {
+          this.loginForm.loginRole = ''
+        }
+      } catch (e) {
+        this.identityOptions = []
+        this.loginForm.loginRole = ''
+      } finally {
+        this.identityLoading = false
+      }
+    },
     onCaptchaFail() {
       this.captchaPassed = false
     },
@@ -258,9 +320,13 @@ export default {
         if (!valid) return
         this.loading = true
         try {
+          if (this.identityOptions.length === 0) {
+            await this.fetchIdentities()
+          }
           const res = await adminLogin({
             phone: this.loginForm.username,
-            password: this.loginForm.password
+            password: this.loginForm.password,
+            loginRole: this.loginForm.loginRole || undefined
           })
           localStorage.setItem('token', res.data.token)
           localStorage.setItem('adminId', res.data.adminId)
@@ -275,7 +341,8 @@ export default {
             console.log('登录成功')
           }
         await this.$nextTick()
-        const redirectRole = role === 'teacher' ? '/teacher-dashboard' : '/'
+        // B-6 按所选登录身份跳转:入驻角色进个人工作台,平台角色进首页
+        const redirectRole = getRoleHomePage([role])
         this.$router.replace({ path: redirectRole })
         } catch (e) {
           const msg = e && e.message ? e.message : '登录失败,请检查账号密码'
@@ -310,6 +377,11 @@ export default {
     'PingFang SC', 'Microsoft YaHei', 'Noto Sans SC', sans-serif;
 }
 
+/* 身份选择下拉框擑满表单宽度 */
+.identity-select {
+  width: 100%;
+}
+
 /* ==================== 左侧品牌区 — 暖棕渐变 ==================== */
 .brand-panel {
   flex: 0 0 55%;

+ 20 - 8
cfc-web/src/views/Users.vue

@@ -20,10 +20,14 @@
           <el-select v-model="searchForm.role" placeholder="全部角色" clearable @change="handleSearch">
             <el-option label="家长" value="parent"></el-option>
             <el-option label="孩子" value="child"></el-option>
+            <el-option label="超级管理员" value="admin"></el-option>
+            <el-option label="平台运营" value="operator"></el-option>
+            <el-option label="平台财务" value="finance"></el-option>
+            <el-option label="管家" value="butler"></el-option>
+            <el-option label="知识管理员" value="knowledge_admin"></el-option>
             <el-option label="成长规划师" value="teacher"></el-option>
-            <el-option label="管理员" value="admin"></el-option>
             <el-option label="营养师" value="nutritionist"></el-option>
-            <el-option label="文章管理员" value="article_manager"></el-option>
+            <el-option label="供应商" value="supplier"></el-option>
             <el-option label="活动管理员" value="activity_manager"></el-option>
           </el-select>
         </el-form-item>
@@ -152,7 +156,7 @@
             >{{ r.label }}</el-checkbox>
           </el-checkbox-group>
           <div style="font-size:12px;color:#999;margin-top:4px">
-            platform roles: admin / teacher / nutritionist / article_manager / activity_manager
+            平台角色: admin / operator / finance;入驻角色: butler / knowledge_admin / teacher / nutritionist / supplier / activity_manager
           </div>
         </el-form-item>
       </el-form>
@@ -245,10 +249,14 @@ export default {
         ]
       },
       allRoles: [
-        { value: 'admin', label: '管理员', disabled: false },
+        { value: 'admin', label: '超级管理员', disabled: false },
+        { value: 'operator', label: '平台运营', disabled: false },
+        { value: 'finance', label: '平台财务', disabled: false },
+        { value: 'butler', label: '管家', disabled: false },
+        { value: 'knowledge_admin', label: '知识管理员', disabled: false },
         { value: 'teacher', label: '成长规划师', disabled: false },
         { value: 'nutritionist', label: '营养师', disabled: false },
-        { value: 'article_manager', label: '文章管理员', disabled: false },
+        { value: 'supplier', label: '供应商', disabled: false },
         { value: 'activity_manager', label: '活动管理员', disabled: false },
       ]
     }
@@ -386,9 +394,14 @@ export default {
       const types = {
         parent: 'success',
         child: 'warning',
+        admin: 'danger',
+        operator: 'primary',
+        finance: 'warning',
+        butler: 'success',
+        knowledge_admin: 'primary',
         teacher: 'primary',
         nutritionist: 'success',
-        article_manager: '',
+        supplier: 'warning',
         activity_manager: 'warning'
       }
       return types[role] || 'info'
@@ -398,8 +411,7 @@ export default {
     },
     formatDate(dateStr) {
       if (!dateStr) return '-'
-      const date = new Date(dateStr)
-      return date.toLocaleString('zh-CN')
+      return String(dateStr).replace('T', ' ').substring(0, 19)
     }
   }
 }

+ 1 - 1
cfc-web/src/views/admin/HealthEnergyConfig.vue

@@ -2,7 +2,7 @@
   <div class="health-energy-config admin-page">
     <el-card>
       <div slot="header">
-        <span>七维健康能量配置</span>
+        <span>五维能量配置</span>
         <el-button style="float: right" type="primary" size="small" @click="handleSave" :loading="submitting">保存配置</el-button>
       </div>
 

+ 6 - 3
cfc-web/src/views/admin/MenuManage.vue

@@ -225,12 +225,15 @@ import {
 } from '@/api/menu'
 
 const ROLES = [
-  { label: '管理员', value: 'admin' },
+  { label: '超级管理员', value: 'admin' },
+  { label: '平台运营', value: 'operator' },
+  { label: '平台财务', value: 'finance' },
+  { label: '管家', value: 'butler' },
+  { label: '知识管理员', value: 'knowledge_admin' },
   { label: '成长规划师', value: 'teacher' },
   { label: '营养师', value: 'nutritionist' },
-  { label: '文章管理员', value: 'article_manager' },
+  { label: '供应商', value: 'supplier' },
   { label: '活动管理员', value: 'activity_manager' },
-  { label: '供应商管理员', value: 'supplier_admin' },
 ]
 
 export default {

+ 1 - 1
cfc-web/src/views/admin/ProductEdit.vue

@@ -473,7 +473,7 @@ export default {
     isSupplierAdmin() {
       const rolesStr = localStorage.getItem('roles')
       const roles = rolesStr ? JSON.parse(rolesStr) : []
-      return roles.includes('supplier_admin')
+      return roles.includes('supplier') || roles.includes('supplier_admin')
     },
   uploadActionUrl() {
     const baseURL = process.env.VUE_APP_BASE_API || ''

+ 2 - 2
tests/e2e/admin-menu-survey.spec.js

@@ -100,7 +100,7 @@ var ADMIN_MENUS = [
   // 健康/报告
   { path: '/health-reports', title: '健康报告' },
   { path: '/report-management', title: '报告管理' },
-  { path: '/health-report-audit', title: '报告草稿审核' },
+  { path: '/report-audit', title: '报告草稿审核' },
   { path: '/health-indicators', title: '健康指标' },
   { path: '/health-checkins', title: '健康打卡' },
   { path: '/emotion-alert', title: '情绪告警管理' },
@@ -121,7 +121,7 @@ var ADMIN_MENUS = [
   { path: '/health-knowledge', title: '健康知识库管理' },
   { path: '/health-norm-config', title: '健康常模配置' },
   { path: '/health-data-source', title: '健康数据源配置' },
-  { path: '/health-energy-config', title: '维能量配置' },
+  { path: '/health-energy-config', title: '维能量配置' },
   { path: '/energy-config', title: '五行能量配置' },
   { path: '/indicators', title: '指标管理' },
   // 虚拟团队