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

chore: auto bump version and changelog [skip ci]

iwt 3 недель назад
Родитель
Сommit
17253568fc

+ 13 - 2
cfc-backend/src/main/java/com/etotem/cfc/common/AdminRoles.java

@@ -12,7 +12,7 @@ import java.util.Set;
  * 后台管理端角色枚举(统一角色口径,见《角色权限与菜单归类整合方案》B-1)
  *
  * 平台角色: admin / operator / finance
- * 入驻角色: butler / knowledge_admin / teacher / nutritionist / supplier / activity_manager
+ * 入驻角色: butler / knowledge_admin / teacher / nutritionist / supplier / activity_manager / org_admin
  * 遗留角色: activity_admin / article_admin / supplier_admin(历史数据兼容,逐步迁移)
  * C端角色: parent / child —— 不允许登录管理后台
  */
@@ -28,7 +28,7 @@ public final class AdminRoles {
     /** 入驻角色(只能看自己的/绑定的数据,首页为个人版看板) */
     public static final Set<String> RESIDENT_ROLES = Collections.unmodifiableSet(
             new HashSet<>(Arrays.asList(
-                    "butler", "knowledge_admin", "teacher", "nutritionist", "supplier", "activity_manager")));
+                    "butler", "knowledge_admin", "teacher", "nutritionist", "supplier", "activity_manager", "org_admin")));
 
     /** 遗留角色(旧数据兼容) */
     public static final Set<String> LEGACY_ROLES = Collections.unmodifiableSet(
@@ -122,6 +122,16 @@ public final class AdminRoles {
         return hasRole(roles, "activity_manager") && !hasAnyPlatformRole(roles);
     }
 
+    /** 判断 roles 是否包含 org_admin(组织管理员) */
+    public static boolean isOrgAdmin(List<String> roles) {
+        return hasRole(roles, "org_admin");
+    }
+
+    /** 判断是否纯组织管理员视角:拥有 org_admin 且不含任何平台角色(用于组织数据行级隔离) */
+    public static boolean isOrgAdminOnly(List<String> roles) {
+        return isOrgAdmin(roles) && !hasAnyPlatformRole(roles);
+    }
+
     /**
      * 从用户 roles 列表中提取全部后台角色(归一化、去重、保持原顺序);
      * 列表为空时回退主角色 role 字段。
@@ -160,6 +170,7 @@ public final class AdminRoles {
             case "nutritionist": return "营养师";
             case "supplier": return "供应商";
             case "activity_manager": return "活动管理员";
+            case "org_admin": return "组织管理员";
             default: return role;
         }
     }

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

@@ -3179,6 +3179,48 @@ insertSysConfigSeed("gift_membership_pay_price", "198000", "送会员商品达
         } catch (Exception e) {
             log.warn("迁移392: 添加 activity_feedback.updated_at / uk_activity_child 失败(可能已存在): {}", e.getMessage());
         }
+
+        // 迁移393: 创建 organizations 表(机构/企业组织主表)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `organizations` (" +
+                    "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "`name` VARCHAR(128) NOT NULL COMMENT '组织名称', " +
+                    "`type` VARCHAR(32) COMMENT 'enterprise/institution/other', " +
+                    "`code` VARCHAR(64) UNIQUE COMMENT '组织编号(唯一标识)', " +
+                    "`admin_user_id` BIGINT COMMENT '组织管理员 userId(关联 users.id)', " +
+                    "`contact_name` VARCHAR(64) COMMENT '联系人姓名', " +
+                    "`contact_phone` VARCHAR(32) COMMENT '联系人电话', " +
+                    "`description` TEXT COMMENT '组织描述', " +
+                    "`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT 'active/disabled', " +
+                    "`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
+                    "`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, " +
+                    "INDEX `idx_admin_user_id` (`admin_user_id`), " +
+                    "INDEX `idx_status` (`status`) " +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='机构/企业组织主表'");
+            log.info("迁移393: 已创建 organizations 表");
+        } catch (Exception e) {
+            log.warn("迁移393: 创建 organizations 表失败(可能已存在): {}", e.getMessage());
+        }
+
+        // 迁移393b: 创建 org_members 表(组织成员关联表)
+        try {
+            jdbcTemplate.execute("CREATE TABLE IF NOT EXISTS `org_members` (" +
+                    "`id` BIGINT AUTO_INCREMENT PRIMARY KEY, " +
+                    "`org_id` BIGINT NOT NULL COMMENT '组织ID', " +
+                    "`user_id` BIGINT NOT NULL COMMENT '用户ID(users.id)', " +
+                    "`member_type` VARCHAR(16) NOT NULL COMMENT 'planner/user', " +
+                    "`role_in_org` VARCHAR(32) COMMENT '组织内角色(teacher/student/member等)', " +
+                    "`status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT 'active/inactive', " +
+                    "`joined_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, " +
+                    "UNIQUE KEY `uk_org_user` (`org_id`, `user_id`), " +
+                    "INDEX `idx_org_id` (`org_id`), " +
+                    "INDEX `idx_user_id` (`user_id`), " +
+                    "INDEX `idx_status` (`status`) " +
+                    ") ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='组织成员关联表'");
+            log.info("迁移393b: 已创建 org_members 表");
+        } catch (Exception e) {
+            log.warn("迁移393b: 创建 org_members 表失败(可能已存在): {}", e.getMessage());
+        }
     }
 
     private void migrateHealthDimensionsTable() {

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

@@ -6465,3 +6465,35 @@ CREATE TABLE IF NOT EXISTS user_content_weights (
     INDEX idx_family_dimension (family_id, dimension_code),
     UNIQUE KEY uk_user_item (user_id, content_type, item_id)
 ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户内容偏好权重表';
+
+-- 迁移393: 机构/企业组织主表(组织管理功能)
+CREATE TABLE IF NOT EXISTS `organizations` (
+    `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
+    `name` VARCHAR(128) NOT NULL COMMENT '组织名称',
+    `type` VARCHAR(32) COMMENT 'enterprise/institution/other',
+    `code` VARCHAR(64) UNIQUE COMMENT '组织编号(唯一标识)',
+    `admin_user_id` BIGINT COMMENT '组织管理员 userId(关联 users.id)',
+    `contact_name` VARCHAR(64) COMMENT '联系人姓名',
+    `contact_phone` VARCHAR(32) COMMENT '联系人电话',
+    `description` TEXT COMMENT '组织描述',
+    `status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT 'active/disabled',
+    `created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    `updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
+    INDEX `idx_admin_user_id` (`admin_user_id`),
+    INDEX `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='机构/企业组织主表';
+
+-- 迁移393b: 组织成员关联表(组织管理功能)
+CREATE TABLE IF NOT EXISTS `org_members` (
+    `id` BIGINT AUTO_INCREMENT PRIMARY KEY,
+    `org_id` BIGINT NOT NULL COMMENT '组织ID',
+    `user_id` BIGINT NOT NULL COMMENT '用户ID(users.id)',
+    `member_type` VARCHAR(16) NOT NULL COMMENT 'planner/user',
+    `role_in_org` VARCHAR(32) COMMENT '组织内角色(teacher/student/member等)',
+    `status` VARCHAR(16) NOT NULL DEFAULT 'active' COMMENT 'active/inactive',
+    `joined_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+    UNIQUE KEY `uk_org_user` (`org_id`, `user_id`),
+    INDEX `idx_org_id` (`org_id`),
+    INDEX `idx_user_id` (`user_id`),
+    INDEX `idx_status` (`status`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='组织成员关联表';

+ 93 - 0
cfc-backend/src/test/java/com/etotem/cfc/common/AdminRolesTest.java

@@ -0,0 +1,93 @@
+package com.etotem.cfc.common;
+
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+/**
+ * AdminRoles 角色常量的单元测试。
+ * 覆盖 org_admin 组织管理员角色:角色集合归属、归一化、角色判定、中文标签。
+ */
+class AdminRolesTest {
+
+    @Test
+    void orgAdmin_inResidentRoles() {
+        assertTrue(AdminRoles.RESIDENT_ROLES.contains("org_admin"),
+                "org_admin 应归入入驻角色(只能看自己的/绑定的数据)");
+        assertTrue(AdminRoles.BACKEND_ROLES.contains("org_admin"),
+                "org_admin 应可登录管理后台");
+        assertFalse(AdminRoles.PLATFORM_ROLES.contains("org_admin"),
+                "org_admin 不是平台角色");
+        assertFalse(AdminRoles.CLIENT_ROLES.contains("org_admin"),
+                "org_admin 不是 C 端角色");
+    }
+
+    @Test
+    void isBackendRole_orgAdmin() {
+        assertTrue(AdminRoles.isBackendRole("org_admin"),
+                "org_admin 属于可登录后台的角色");
+        assertTrue(AdminRoles.isBackendRole(AdminRoles.normalize("org_admin")),
+                "org_admin 归一化后仍为自身");
+    }
+
+    @Test
+    void isPlatformRole_orgAdmin_false() {
+        assertFalse(AdminRoles.isPlatformRole("org_admin"),
+                "org_admin 不应被当作平台角色");
+    }
+
+    @Test
+    void hasRole_orgAdmin_true() {
+        List<String> roles = Arrays.asList("org_admin");
+        assertTrue(AdminRoles.hasRole(roles, "org_admin"));
+        assertTrue(AdminRoles.hasAnyBackendRole(roles));
+        assertFalse(AdminRoles.hasAnyPlatformRole(roles));
+    }
+
+    @Test
+    void nullOrEmptyRoles_safe() {
+        assertFalse(AdminRoles.isOrgAdmin(null));
+        assertFalse(AdminRoles.isOrgAdmin(Collections.emptyList()));
+        assertFalse(AdminRoles.isOrgAdminOnly(null));
+        assertFalse(AdminRoles.hasRole(null, "org_admin"));
+        assertFalse(AdminRoles.hasAnyBackendRole(null));
+        assertFalse(AdminRoles.hasAnyPlatformRole(null));
+    }
+
+    @Test
+    void roleLabel_orgAdmin() {
+        assertEquals("组织管理员", AdminRoles.roleLabel("org_admin"));
+    }
+
+    @Test
+    void isOrgAdminOnly_withPlatformRole_false() {
+        List<String> orgPlusPlatform = Arrays.asList("org_admin", "admin");
+        assertTrue(AdminRoles.isOrgAdmin(orgPlusPlatform),
+                "含平台角色时仍是 org_admin");
+        assertFalse(AdminRoles.isOrgAdminOnly(orgPlusPlatform),
+                "含平台角色的组织管理员不算纯组织管理员视角");
+        List<String> orgPlusResident = Arrays.asList("org_admin", "teacher");
+        assertTrue(AdminRoles.isOrgAdminOnly(orgPlusResident),
+                "同时是规划师的组织管理员仍视为纯组织管理员视角(不占平台权限)");
+    }
+
+    @Test
+    void normalize_orgAdmin_passthrough() {
+        assertEquals("org_admin", AdminRoles.normalize("org_admin"));
+        // 与遗留角色互通性:遗留角色归一化不应误伤 org_admin
+        assertNotEquals("org_admin", AdminRoles.normalize("activity_admin"));
+        assertNotEquals("org_admin", AdminRoles.normalize("supplier_admin"));
+    }
+
+    @Test
+    void backendRolesOf_orgAdmin() {
+        List<String> roles = AdminRoles.backendRolesOf(
+                Collections.singletonList("org_admin"), "org_admin");
+        assertEquals(Collections.singletonList("org_admin"), roles,
+                "backendRolesOf 应保留 org_admin");
+    }
+}

+ 1 - 1
cfc-web/.last_build_commit

@@ -1 +1 @@
-b378cfeec011e01de30f2b71c32a7615677a3d7b
+287870ca38462912947fe6010eb73a15ac2c135d

+ 2 - 2
cfc-web/package-lock.json

@@ -1,12 +1,12 @@
 {
   "name": "cfc-web",
-  "version": "1.0.1755",
+  "version": "1.0.1756",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "cfc-web",
-      "version": "1.0.1755",
+      "version": "1.0.1756",
       "dependencies": {
         "@wangeditor/editor": "^5.1.23",
         "@wangeditor/editor-for-vue": "^1.0.2",

+ 1 - 1
cfc-web/package.json

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

+ 6 - 0
cfc-web/public/CHANGELOG-v1.0.md

@@ -4,6 +4,12 @@
 
 ---
 
+## v1.0.1757 (2026-10-22)
+
+### 样式
+- C-14 OrderManage/ArticleAudit/OrderDetail/VirtualTeamList/ProxyReport 内联 style 归零
+
+
 ## v1.0.1756 (2026-10-22)
 
 ### 文档

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

@@ -1,6 +1,6 @@
 # 更新日志
 
-> 当前版本: v1.0.1756
+> 当前版本: v1.0.1757
 
 ## 历史版本
 
@@ -8,6 +8,12 @@
 
 ---
 
+## v1.0.1757 (2026-10-22)
+
+### 样式
+- C-14 OrderManage/ArticleAudit/OrderDetail/VirtualTeamList/ProxyReport 内联 style 归零
+
+
 ## v1.0.1756 (2026-10-22)
 
 ### 文档