|
@@ -0,0 +1,645 @@
|
|
|
|
|
+# 机构/企业组织管理 实现计划
|
|
|
|
|
+
|
|
|
|
|
+> **面向 AI 代理的工作者:** 必需子技能:使用 `subagent-driven-development`(推荐)或 `executing-plans` 逐任务实现此计划。步骤使用复选框(`- [ ]`)语法来跟踪进度。
|
|
|
|
|
+
|
|
|
|
|
+**目标:** 实现平台创建组织/企业、绑定组织管理员、组织管理员在管理端统一管理组织内规划师/用户成员、并拥有专属统计大屏的功能。
|
|
|
|
|
+
|
|
|
|
|
+**架构:** 后端新建 `organizations` + `org_members` 两表,复用现有 `users` 体系(org_admin 角色挂载到 `users.roles`),组织管理员登录复用现有 `AdminAuthController`。权限:平台角色(admin/operator/finance)看全量,org_admin 仅看自身组织。统计基于 org_members 的 user_id 集合批量 IN 查现有表(families/tasks/product_orders 等),不建汇总表。前端 cfc-web 新增 4 页面 + API 封装 + 路由/菜单/Login 分流。
|
|
|
|
|
+
|
|
|
|
|
+**技术栈:** Spring Boot 2.7.18 + MyBatis-Plus + Java 8 + MySQL 8.0(后端);Vue 2 + Element UI(前端 cfc-web)。验证方式:`mvn clean compile` 编译通过即视为验证完成。
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 文件结构
|
|
|
|
|
+
|
|
|
|
|
+### 后端新增文件
|
|
|
|
|
+| 文件 | 职责 |
|
|
|
|
|
+|------|------|
|
|
|
|
|
+| `entity/Organization.java` | 组织实体(`@TableName("organizations")`) |
|
|
|
|
|
+| `entity/OrgMember.java` | 组织成员实体(`@TableName("org_members")`) |
|
|
|
|
|
+| `mapper/OrganizationMapper.java` | 组织 MyBatis Mapper |
|
|
|
|
|
+| `mapper/OrgMemberMapper.java` | 组织成员 MyBatis Mapper |
|
|
|
|
|
+| `mapper/OrgMemberMapper.xml` | 成员联表聚合查询 SQL |
|
|
|
|
|
+| `service/OrganizationService.java` | 组织 CRUD + `getByAdminId` + 管理员账号创建 + code 生成 |
|
|
|
|
|
+| `service/OrgMemberService.java` | 成员管理(add/remove/change-type/list 聚合家庭/订单摘要) |
|
|
|
|
|
+| `service/OrgStatsService.java` | 统计大屏(5 个口径的批量聚合查询) |
|
|
|
|
|
+| `service/UserSearchService.java` | 平台用户搜索(供 org_admin 选择成员用) |
|
|
|
|
|
+| `controller/admin/AdminOrgController.java` | 组织 CRUD + my-org + user/search |
|
|
|
|
|
+| `controller/admin/AdminOrgMemberController.java` | 成员管理接口 |
|
|
|
|
|
+| `controller/admin/AdminOrgStatsController.java` | 统计大屏接口 |
|
|
|
|
|
+| `common/AdminOrgAccess.java` | org_admin 鉴权静态工具类(供三个 Controller 复用) |
|
|
|
|
|
+
|
|
|
|
|
+### 后端修改文件
|
|
|
|
|
+| 文件 | 说明 |
|
|
|
|
|
+|------|------|
|
|
|
|
|
+| `config/DatabaseInitializer.java` | 迁移 393:创建 organizations + org_members 两表(`runMigrations()` 末尾追加) |
|
|
|
|
|
+| `resources/schema.sql` | 追加两张表的 `CREATE TABLE IF NOT EXISTS` 语句(文件末尾) |
|
|
|
|
|
+| `common/AdminRoles.java` | `org_admin` 加入 `RESIDENT_ROLES`;`roleLabel` 补映射;新增 `isOrgAdmin` / `isOrgAdminOnly` |
|
|
|
|
|
+| `docs/superpowers/api/API_REFERENCE.md` | 同步记录新增的 13 个 `/api/admin/org/**` 接口 |
|
|
|
|
|
+
|
|
|
|
|
+### 前端新增文件
|
|
|
|
|
+| 文件 | 说明 |
|
|
|
|
|
+|------|------|
|
|
|
|
|
+| `src/views/admin/org/List.vue` | 组织列表页(平台角色可见) |
|
|
|
|
|
+| `src/views/admin/org/Form.vue` | 组织表单页(新建/编辑) |
|
|
|
|
|
+| `src/views/admin/org/Detail.vue` | 组织详情页(成员管理:搜/增/删/改类型) |
|
|
|
|
|
+| `src/views/admin/org/Dashboard.vue` | 统计大页面(5 卡片 + 3 图表) |
|
|
|
|
|
+| `src/api/org.js` | API 封装 |
|
|
|
|
|
+
|
|
|
|
|
+### 前端修改文件
|
|
|
|
|
+| 文件 | 说明 |
|
|
|
|
|
+|------|------|
|
|
|
|
|
+| `src/router/index.js` | 新增 4 条路由(含 org_admin 守卫分流逻辑) |
|
|
|
|
|
+| `src/views/Layout.vue` | 侧边栏菜单新增「组织管理」分组 + org_admin 专属菜单显隐 |
|
|
|
|
|
+| `src/views/Login.vue` | 登录成功后按 `org_admin` 角色分流 → 自动跳 `/org/:id/dashboard` |
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 任务分解
|
|
|
|
|
+
|
|
|
|
|
+> **规范提醒:**
|
|
|
|
|
+> - 后端接口统一 `@PostMapping`,禁止 `@GetMapping/@PutMapping/@DeleteMapping`
|
|
|
|
|
+> - MyBatis-Plus:`@TableName` + `@TableId(type = IdType.AUTO)`
|
|
|
|
|
+> - 响应统一 `Result<T>` (code/message/data)
|
|
|
|
|
+> - DI 使用 `@Resource`,字段名匹配默认 Bean Name
|
|
|
|
|
+> - 权限检查在 Controller 内手动校验 `@RequestAttribute("role")` / `@RequestAttribute("roles")`
|
|
|
|
|
+> - 每步完成后执行 `mvn clean compile`,编译通过即视为验证通过
|
|
|
|
|
+> - **新增接口前**必须查阅 `docs/superpowers/api/API_REFERENCE.md` 防重复;完成后同步记录
|
|
|
|
|
+> - Bean 名冲突检查:新 Controller/Service 类名不能与其他包同名类重名
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 任务 1:DB 迁移(迁移 393 创建两张表 + schema.sql 同步)
|
|
|
|
|
+
|
|
|
|
|
+**文件:**
|
|
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/config/DatabaseInitializer.java`(`runMigrations()` 末尾追加)
|
|
|
|
|
+- 修改:`cfc-backend/src/main/resources/schema.sql`(文件末尾追加)
|
|
|
|
|
+
|
|
|
|
|
+**步骤:**
|
|
|
|
|
+- [ ] 在 `runMigrations()` 方法末尾(迁移 392 之后)添加迁移 393 代码块
|
|
|
|
|
+- [ ] 迁移使用 `try-catch` 包裹,幂等可重复执行
|
|
|
|
|
+- [ ] 在 `schema.sql` 文件末尾追加两张表的 `CREATE TABLE IF NOT EXISTS` 语句,字段定义与迁移保持一致
|
|
|
|
|
+- [ ] 执行 `mvn clean compile` 验证编译通过
|
|
|
|
|
+
|
|
|
|
|
+**迁移 393 代码模板(直接追加到 runMigrations 末尾):**
|
|
|
|
|
+```java
|
|
|
|
|
+// 迁移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());
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**schema.sql 追加模板(文件末尾):**
|
|
|
|
|
+```sql
|
|
|
|
|
+-- 迁移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='组织成员关联表';
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 任务 2:AdminRoles 扩展 org_admin 角色
|
|
|
|
|
+
|
|
|
|
|
+**文件:**
|
|
|
|
|
+- 修改:`cfc-backend/src/main/java/com/etotem/cfc/common/AdminRoles.java`
|
|
|
|
|
+- 新增/修改:`cfc-backend/src/test/java/com/etotem/cfc/common/AdminRolesTest.java`(如不存在则新建;如存在 TestRoles.java 则扩展)
|
|
|
|
|
+
|
|
|
|
|
+**步骤:**
|
|
|
|
|
+- [ ] 在 `RESIDENT_ROLES` 集合中添加 `"org_admin"`
|
|
|
|
|
+- [ ] 在 `roleLabel()` switch 中添加 `case "org_admin": return "组织管理员";`
|
|
|
|
|
+- [ ] 新增静态方法:
|
|
|
|
|
+ ```java
|
|
|
|
|
+ /** 判断 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);
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
+- [ ] 更新类头 javadoc:在“入驻角色”行后加 `org_admin` 说明
|
|
|
|
|
+- [ ] 编写/扩展单元测试(`AdminRolesTest.java`)覆盖:`isOrgAdmin`、`isOrgAdminOnly`、`roleLabel`、归一化、后台角色集合包含关系
|
|
|
|
|
+- [ ] 执行 `mvn clean compile` 验证编译通过
|
|
|
|
|
+
|
|
|
|
|
+**测试用例参考:**
|
|
|
|
|
+```java
|
|
|
|
|
+@Test
|
|
|
|
|
+void orgAdminRole() {
|
|
|
|
|
+ assertTrue(AdminRoles.isOrgAdmin(Arrays.asList("org_admin")));
|
|
|
|
|
+ assertFalse(AdminRoles.isOrgAdmin(Arrays.asList("teacher")));
|
|
|
|
|
+ assertTrue(AdminRoles.isOrgAdminOnly(Arrays.asList("org_admin")));
|
|
|
|
|
+ assertFalse(AdminRoles.isOrgAdminOnly(Arrays.asList("org_admin", "admin"))); // 含平台角色
|
|
|
|
|
+ assertEquals("组织管理员", AdminRoles.roleLabel("org_admin"));
|
|
|
|
|
+ assertTrue(AdminRoles.RESIDENT_ROLES.contains("org_admin"));
|
|
|
|
|
+ assertTrue(AdminRoles.BACKEND_ROLES.contains("org_admin"));
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 任务 3:Entity + Mapper(Organization / OrgMember)
|
|
|
|
|
+
|
|
|
|
|
+**文件:**
|
|
|
|
|
+- 新增:`cfc-backend/src/main/java/com/etotem/cfc/entity/Organization.java`
|
|
|
|
|
+- 新增:`cfc-backend/src/main/java/com/etotem/cfc/entity/OrgMember.java`
|
|
|
|
|
+- 新增:`cfc-backend/src/main/java/com/etotem/cfc/mapper/OrganizationMapper.java`
|
|
|
|
|
+- 新增:`cfc-backend/src/main/java/com/etotem/cfc/mapper/OrgMemberMapper.java`
|
|
|
|
|
+- 新增:`cfc-backend/src/main/resources/mapper/OrgMemberMapper.xml`
|
|
|
|
|
+
|
|
|
|
|
+**步骤:**
|
|
|
|
|
+- [ ] 编写 `Organization.java` 实体类(参考 `SupplySystem.java` 模板):
|
|
|
|
|
+ - `@TableName("organizations")`、`@TableId(type = IdType.AUTO)`、Lombok `@Data`
|
|
|
|
|
+ - 字段对应表:id/name/type/code/adminUserId/contactName/contactPhone/description/status/createdAt/updatedAt
|
|
|
|
|
+- [ ] 编写 `OrgMember.java` 实体类:
|
|
|
|
|
+ - `@TableName("org_members")`、`@TableId(type = IdType.AUTO)`
|
|
|
|
|
+ - 字段:id/orgId/userId/memberType/roleInOrg/status/joinedAt
|
|
|
|
|
+- [ ] 编写 `OrganizationMapper.java`:继承 `BaseMapper<Organization>`,无额外方法
|
|
|
|
|
+- [ ] 编写 `OrgMemberMapper.java`:继承 `BaseMapper<OrgMember>`,新增方法:
|
|
|
|
|
+ ```java
|
|
|
|
|
+ // 根据 orgId 查活跃成员(带用户基础信息)
|
|
|
|
|
+ List<OrgMemberVO> selectActiveMembersWithUser(@Param("orgId") Long orgId);
|
|
|
|
|
+ // 根据 userId 集合批量查成员类型(用于统计过滤)
|
|
|
|
|
+ List<OrgMember> selectByOrgIdAndUserIds(@Param("orgId") Long orgId, @Param("userIds") List<Long> userIds);
|
|
|
|
|
+ ```
|
|
|
|
|
+- [ ] 编写 `OrgMemberMapper.xml`:实现上述两个方法(LEFT JOIN users 表查 nickname/phone/avatar)
|
|
|
|
|
+- [ ] 执行 `mvn clean compile` 验证编译通过
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 任务 4:OrganizationService(组织 CRUD + 管理员账号创建 + code 生成)
|
|
|
|
|
+
|
|
|
|
|
+**文件:**
|
|
|
|
|
+- 新增:`cfc-backend/src/main/java/com/etotem/cfc/service/OrganizationService.java`
|
|
|
|
|
+
|
|
|
|
|
+**依赖:** `UserMapper`(复用现有)、`OrganizationMapper`、`OrgMemberMapper`、`SupplySystemService` 无依赖
|
|
|
|
|
+
|
|
|
|
|
+**步骤:**
|
|
|
|
|
+- [ ] 实现 `list(keyword, type, status, page, size)` 分页查询
|
|
|
|
|
+- [ ] 实现 `create(params)` 创建组织:
|
|
|
|
|
+ - 参数:`name`, `type`, `code`, `adminPhone`, `adminRealName`(必填:name, adminPhone)
|
|
|
|
|
+ - `code` 为空时自动生成:`ORG` + 6 位随机大写字母数字(循环重试至唯一)
|
|
|
|
|
+ - 按 `adminPhone` 查 `UserMapper.selectOne(eq(phone))`,存在则复用;否则创建新用户:
|
|
|
|
|
+ ```java
|
|
|
|
|
+ User user = new User();
|
|
|
|
|
+ user.setPhone(adminPhone);
|
|
|
|
|
+ user.setRealName(adminRealName);
|
|
|
|
|
+ user.setNickname(adminRealName != null ? adminRealName : "组织管理员" + adminPhone.substring(7));
|
|
|
|
|
+ user.setRole("org_admin"); // 主角色
|
|
|
|
|
+ user.setRoles(Collections.singletonList("org_admin")); // roles 列表(单一身份)
|
|
|
|
|
+ user.setPassword("");
|
|
|
|
|
+ user.setOpenid("admin_" + System.currentTimeMillis());
|
|
|
|
|
+ user.setFamilyId(0L);
|
|
|
|
|
+ user.setCreatedAt(new Date());
|
|
|
|
|
+ user.setUpdatedAt(new Date());
|
|
|
|
|
+ userMapper.insert(user);
|
|
|
|
|
+ ```
|
|
|
|
|
+ - 创建 `Organization` 记录,`adminUserId = user.getId()`
|
|
|
|
|
+ - 同时在 `org_members` 插入一条记录:`orgId`=新组织、`userId`=user.id、`memberType`="planner"(管理员默认作为规划师类型)
|
|
|
|
|
+ - 返回包含 `organization` + `adminUser` 的 Map/VO
|
|
|
|
|
+- [ ] 实现 `update(id, name, type, contactName, contactPhone, description)` 更新
|
|
|
|
|
+- [ ] 实现 `disable(id, status)` 启用/停用(status: active/disabled)
|
|
|
|
|
+- [ ] 实现 `detail(id)` 查详情(含管理员用户信息:LEFT JOIN users)
|
|
|
|
|
+- [ ] 实现 `getByAdminId(userId)` 返回该用户管理的组织(供 my-org 接口用)
|
|
|
|
|
+- [ ] 实现 `getMyOrg(userId)` 别名方法
|
|
|
|
|
+- [ ] 执行 `mvn clean compile` 验证编译通过
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 任务 5:OrgMemberService(成员管理 + 聚合摘要)
|
|
|
|
|
+
|
|
|
|
|
+**文件:**
|
|
|
|
|
+- 新增:`cfc-backend/src/main/java/com/etotem/cfc/service/OrgMemberService.java`
|
|
|
|
|
+
|
|
|
|
|
+**步骤:**
|
|
|
|
|
+- [ ] 实现 `memberList(orgId)` 返回活跃成员列表(含摘要字段):
|
|
|
|
|
+ - 调用 `OrgMemberMapper.selectActiveMembersWithUser(orgId)` 得到基础列表
|
|
|
|
|
+ - 收集所有 `userId` → 批量查 `families`(`creator_id IN (...)`)→ `Map<userId, familyCount>`
|
|
|
|
|
+ - 批量查 `product_orders`(`buyer_id IN (...) AND status IN ('PAID','COMPLETED')`)→ `Map<userId, orderStats>`
|
|
|
|
|
+ - 组装返回 VO:`userId`/`nickname`/`phone`(脱敏 `DataMaskUtil.maskPhone`)/`avatar`/`memberType`/`joinedAt`/`familyCount`/`totalOrders`/`totalConsumption`
|
|
|
|
|
+- [ ] 实现 `addMember(orgId, userId, memberType)`:
|
|
|
|
|
+ - 先查 `org_members` 是否已存在记录(uk_org_user)
|
|
|
|
|
+ - 存在且 status=inactive → 更新 status=active + memberType + joinedAt=now
|
|
|
|
|
+ - 存在且 status=active → 仅更新 memberType
|
|
|
|
|
+ - 不存在 → INSERT 新记录
|
|
|
|
|
+ - 返回成员记录
|
|
|
|
|
+- [ ] 实现 `removeMember(orgId, userId)`:UPDATE status='inactive' WHERE org_id=... AND user_id=...
|
|
|
|
|
+- [ ] 实现 `changeType(orgId, userId, memberType)`:
|
|
|
|
|
+ - 找记录(含 inactive),更新 memberType + status=active + joinedAt=now(如之前 inactive)
|
|
|
|
|
+- [ ] 实现 `userSearch(keyword, memberType)`:委托 `UserSearchService.search(keyword, memberType)`
|
|
|
|
|
+- [ ] 执行 `mvn clean compile` 验证编译通过
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 任务 6:UserSearchService(平台用户搜索)
|
|
|
|
|
+
|
|
|
|
|
+**文件:**
|
|
|
|
|
+- 新增:`cfc-backend/src/main/java/com/etotem/cfc/service/UserSearchService.java`
|
|
|
|
|
+
|
|
|
|
|
+**步骤:**
|
|
|
|
|
+- [ ] 实现 `search(keyword, memberType)`:
|
|
|
|
|
+ - `LambdaQueryWrapper<User>`:`like` nickname 或 phone,可选过滤 `memberType`(需通过 `family_members` 关联判断规划师/用户,简化:只按 User 表字段查,不加 memberType 过滤)
|
|
|
|
|
+ - 限制 20 条,返回脱敏后的 `id/nickname/phone/avatar/status`
|
|
|
|
|
+- [ ] 执行 `mvn clean compile` 验证编译通过
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 任务 7:OrgStatsService(统计大屏 5 个口径)
|
|
|
|
|
+
|
|
|
|
|
+**文件:**
|
|
|
|
|
+- 新增:`cfc-backend/src/main/java/com/etotem/cfc/service/OrgStatsService.java`
|
|
|
|
|
+
|
|
|
|
|
+**统计口径(严格按规格文档第 4.3 节):**
|
|
|
|
|
+
|
|
|
|
|
+| 接口 | 关键 SQL 逻辑 |
|
|
|
|
|
+|------|--------------|
|
|
|
|
|
+| `overview(orgId)` | 1) `org_members` 查 `userId` 集合 `memberIds`(`status='active'`)<br>2) `memberCount` = size<br>3) `plannerCount` = count where `member_type='planner'`<br>4) `familyCount` = `families.creator_id IN (memberIds)` 去重计数<br>5) `totalOrders` = `product_orders` `buyer_id IN (memberIds) AND status IN ('PAID','COMPLETED')` count<br>6) `totalConsumption` = 同上 `total_amount` sum |
|
|
|
|
|
+| `families(orgId, startDate, endDate)` | 1) `memberIds` 同概览<br>2) `activeFamilyCount` = `families.creator_id IN (memberIds) AND updated_at >= startDate` count<br>3) `taskCompletionRate` = `tasks` `status='completed' AND completed_at BETWEEN startDate AND endDate` / 同范围总任务数<br>4) `checkinRate` = 近30天有 `daily_checkin`/`health_checkins`/`emotion_checkin` 记录的成员数 / 总成员数 |
|
|
|
|
|
+| `active(orgId, startDate, endDate)` | 按天分组:`tasks.completed_at`(任务完成)、`daily_checkin/health_checkins/emotion_checkin.created_at`(打卡)、`dan_report_uploads.created_at`(报告上传)→ 各自 `GROUP BY DATE(created_at)` COUNT |
|
|
|
|
|
+| `revenue(orgId, startDate, endDate, groupBy)` | `product_orders.buyer_id IN (memberIds) AND status IN ('PAID','COMPLETED')` → `GROUP BY DATE(paid_at)/WEEK(paid_at)` `total_amount` SUM + `member_subscription_order` 同理 + `commission_records` 受益人 IN (memberIds) `amount` SUM |
|
|
|
|
|
+| `trend(orgId, startDate, endDate)` | 合并上述三指标:活跃成员数(当日有任一行为去重)、订单数、收入(分) |
|
|
|
|
|
+
|
|
|
|
|
+**实现约束:**
|
|
|
|
|
+- 所有统计**严禁** per-member 循环查询 → 必须先拿 `memberIds` 再 `IN (...)` 批量聚合
|
|
|
|
|
+- 可选:overview 加 Redis 缓存 `org:stats:overview:{orgId}` TTL 5min(先不加,性能够用再加)
|
|
|
|
|
+
|
|
|
|
|
+**步骤:**
|
|
|
|
|
+- [ ] 实现私有方法 `List<Long> getActiveMemberIds(orgId)` 复用
|
|
|
|
|
+- [ ] 实现 5 个公共方法按上述口径
|
|
|
|
|
+- [ ] 执行 `mvn clean compile` 验证编译通过
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 任务 8:AdminOrgAccess 鉴权工具类
|
|
|
|
|
+
|
|
|
|
|
+**文件:**
|
|
|
|
|
+- 新增:`cfc-backend/src/main/java/com/etotem/cfc/common/AdminOrgAccess.java`
|
|
|
|
|
+
|
|
|
|
|
+**步骤:**
|
|
|
|
|
+- [ ] 编写静态工具类,供三个 Controller 复用:
|
|
|
|
|
+ ```java
|
|
|
|
|
+ public final class AdminOrgAccess {
|
|
|
|
|
+ private AdminOrgAccess() {}
|
|
|
|
|
+
|
|
|
|
|
+ /** 校验当前用户是否有权访问指定 orgId
|
|
|
|
|
+ * 返回 null = 通过;否则返回错误 Result(直接 return) */
|
|
|
|
|
+ public static Result<?> requireOrgAccess(Long requestOrgId, Long userId,
|
|
|
|
|
+ Object roleObj, Object rolesObj,
|
|
|
|
|
+ OrganizationService orgService) {
|
|
|
|
|
+ // 1. 平台角色直接放行
|
|
|
|
|
+ if (AdminRoles.hasAnyPlatformRole(extractRoles(roleObj, rolesObj))) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 2. org_admin:查自己管理的组织
|
|
|
|
|
+ if (AdminRoles.isOrgAdmin(extractRoles(roleObj, rolesObj))) {
|
|
|
|
|
+ Organization org = orgService.getByAdminId(userId);
|
|
|
|
|
+ if (org == null) {
|
|
|
|
|
+ return Result.error(403, "您未关联任何组织");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!"active".equals(org.getStatus())) {
|
|
|
|
|
+ return Result.error(403, "组织已停用");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!org.getId().equals(requestOrgId)) {
|
|
|
|
|
+ return Result.error(403, "无权访问该组织");
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return Result.error(403, "无权限访问该接口");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static List<String> extractRoles(Object roleObj, Object rolesObj) {
|
|
|
|
|
+ // 同 AdminController.requirePlatformRole 的提取逻辑
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ ```
|
|
|
|
|
+- [ ] 执行 `mvn clean compile` 验证编译通过
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 任务 9:三个 Controller(AdminOrgController / AdminOrgMemberController / AdminOrgStatsController)
|
|
|
|
|
+
|
|
|
|
|
+**文件:**
|
|
|
|
|
+- 新增:`cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminOrgController.java`
|
|
|
|
|
+- 新增:`cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminOrgMemberController.java`
|
|
|
|
|
+- 新增:`cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminOrgStatsController.java`
|
|
|
|
|
+
|
|
|
|
|
+**通用模板(每个 Controller):**
|
|
|
|
|
+```java
|
|
|
|
|
+@RestController
|
|
|
|
|
+@RequestMapping("/api/admin/org") // 或 /member /stats
|
|
|
|
|
+public class AdminOrgController {
|
|
|
|
|
+ @Resource private OrganizationService organizationService;
|
|
|
|
|
+ @Resource private OrgMemberService orgMemberService;
|
|
|
|
|
+ @Resource private UserSearchService userSearchService;
|
|
|
|
|
+ // 或 OrgStatsService
|
|
|
|
|
+
|
|
|
|
|
+ // 每个方法签名:@PostMapping("/xxx") public Result<?> xxx(@RequestBody Map<String,Object> params,
|
|
|
|
|
+ // @RequestAttribute(value="role",required=false) Object roleObj,
|
|
|
|
|
+ // @RequestAttribute(value="roles",required=false) Object rolesObj)
|
|
|
|
|
+ // 方法体首行:Result<?> denied = AdminOrgAccess.requireOrgAccess(orgId, userId, roleObj, rolesObj, organizationService);
|
|
|
|
|
+ // if (denied != null) return denied;
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**AdminOrgController 接口(12 个):**
|
|
|
|
|
+| 方法 | 路径 | 鉴权 | 备注 |
|
|
|
|
|
+|------|------|------|------|
|
|
|
|
|
+| list | `/list` | 平台角色全量;org_admin 只看自己组织(忽略传入 orgId,自动用自己的) | |
|
|
|
|
|
+| create | `/create` | 仅平台角色 | 创建组织+管理员账号 |
|
|
|
|
|
+| update | `/update` | 平台角色全量;org_admin 仅自己组织 | |
|
|
|
|
|
+| disable | `/disable` | 仅平台角色 | 启用/停用组织 |
|
|
|
|
|
+| detail | `/detail` | 平台角色全量;org_admin 仅自己组织 | |
|
|
|
|
|
+| myOrg | `/my-org` | org_admin 专用(无需传 orgId,从 JWT 取 userId) | 登录首页跳转用 |
|
|
|
|
|
+| userSearch | `/user/search` | 平台角色全量;org_admin 仅自己组织 | 成员添加下拉搜人 |
|
|
|
|
|
+
|
|
|
|
|
+**AdminOrgMemberController 接口(5 个):**
|
|
|
|
|
+| 方法 | 路径 | 鉴权 |
|
|
|
|
|
+|------|------|------|
|
|
|
|
|
+| memberList | `/member/list` | 平台/本组织 |
|
|
|
|
|
+| addMember | `/member/add` | 同上 |
|
|
|
|
|
+| removeMember | `/member/remove` | 同上 |
|
|
|
|
|
+| changeType | `/member/change-type` | 同上 |
|
|
|
|
|
+
|
|
|
|
|
+**AdminOrgStatsController 接口(5 个):**
|
|
|
|
|
+| 方法 | 路径 | 鉴权 |
|
|
|
|
|
+|------|------|------|
|
|
|
|
|
+| overview | `/stats/overview` | 同上 |
|
|
|
|
|
+| families | `/stats/families` | 同上 |
|
|
|
|
|
+| active | `/stats/active` | 同上 |
|
|
|
|
|
+| revenue | `/stats/revenue` | 同上 |
|
|
|
|
|
+| trend | `/stats/trend` | 同上 |
|
|
|
|
|
+
|
|
|
|
|
+**步骤:**
|
|
|
|
|
+- [ ] 编写三个 Controller 类(参考 `SupplySystemController` 结构 + `AdminController` 鉴权模式)
|
|
|
|
|
+- [ ] 每个方法首行调用 `AdminOrgAccess.requireOrgAccess(...)`(成员/统计接口从 params 取 `orgId`;my-org 从 JWT 取 `userId` 自动查 orgId)
|
|
|
|
|
+- [ ] 请求参数用 `ParamUtils.getLong(params.get("xxx"))` 统一提取
|
|
|
|
|
+- [ ] 完成后执行 **路由冲突检查**:
|
|
|
|
|
+ ```bash
|
|
|
|
|
+ grep -rn '@PostMapping' cfc-backend/src/main/java/com/etotem/cfc/controller/admin/ | \
|
|
|
|
|
+ grep -oP '@PostMapping\("\K[^"]*' | sort -u
|
|
|
|
|
+ ```
|
|
|
|
|
+- [ ] 执行 `mvn clean compile` 验证编译通过
|
|
|
|
|
+- [ ] 同步更新 `docs/superpowers/api/API_REFERENCE.md` 记录 13 个新接口
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 任务 10:前端 API 封装
|
|
|
|
|
+
|
|
|
|
|
+**文件:**
|
|
|
|
|
+- 新增:`cfc-web/src/api/org.js`
|
|
|
|
|
+
|
|
|
|
|
+**步骤:**
|
|
|
|
|
+- [ ] 封装 13 个接口函数,使用现有 `request` 工具(`@/utils/request`)
|
|
|
|
|
+- [ ] 导出:`listOrg`, `createOrg`, `updateOrg`, `disableOrg`, `detailOrg`, `myOrg`, `userSearch`, `memberList`, `addMember`, `removeMember`, `changeMemberType`, `statsOverview`, `statsFamilies`, `statsActive`, `statsRevenue`, `statsTrend`
|
|
|
|
|
+- [ ] 执行 `cd cfc-web && npm run lint`(如配置)或直接进入下一步
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 任务 11:前端 List.vue(组织列表页)
|
|
|
|
|
+
|
|
|
|
|
+**文件:**
|
|
|
|
|
+- 新增:`cfc-web/src/views/admin/org/List.vue`
|
|
|
|
|
+
|
|
|
|
|
+**参考:** `cfc-web/src/views/admin/supply-system/List.vue`
|
|
|
|
|
+
|
|
|
|
|
+**关键点:**
|
|
|
|
|
+- 表格列:名称/类型/编号/管理员/状态/创建时间/操作
|
|
|
|
|
+- 搜索:关键词/类型/状态
|
|
|
|
|
+- 分页:page/size
|
|
|
|
|
+- 操作:编辑/详情/停用
|
|
|
|
|
+- 权限:`v-if="$hasPerm('system:org')"` 或 perm 判断
|
|
|
|
|
+
|
|
|
|
|
+**步骤:**
|
|
|
|
|
+- [ ] 复制 SupplySystem List.vue 改字段/接口
|
|
|
|
|
+- [ ] 执行 `cd cfc-web && npm run serve` 启动冒烟验证页面渲染无报错
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 任务 12:前端 Form.vue(组织表单页)
|
|
|
|
|
+
|
|
|
|
|
+**文件:**
|
|
|
|
|
+- 新增:`cfc-web/src/views/admin/org/Form.vue`
|
|
|
|
|
+
|
|
|
|
|
+**参考:** `cfc-web/src/views/admin/supply-system/Form.vue`
|
|
|
|
|
+
|
|
|
|
|
+**字段:** 名称*(必填)/类型下拉/编号(可选,留空自动生成)/管理员手机号*(必填)/管理员姓名/联系人/联系电话/描述
|
|
|
|
|
+
|
|
|
|
|
+**步骤:**
|
|
|
|
|
+- [ ] 复制 SupplySystem Form.vue 改字段/验证/提交接口
|
|
|
|
|
+- [ ] 新建模式:提交 `createOrg`;编辑模式:提交 `updateOrg`
|
|
|
|
|
+- [ ] 冒烟验证
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 任务 13:前端 Detail.vue(组织详情页 = 成员管理)
|
|
|
|
|
+
|
|
|
|
|
+**文件:**
|
|
|
|
|
+- 新增:`cfc-web/src/views/admin/org/Detail.vue`
|
|
|
|
|
+
|
|
|
|
|
+**功能:**
|
|
|
|
|
+- 顶部显示组织基本信息
|
|
|
|
|
+- 成员表格:搜索用户(调 `/user/search`)→ 选择用户 + 类型 → 添加成员
|
|
|
|
|
+- 成员列表:成员类型切换(planner↔user)、移除
|
|
|
|
|
+- 权限:平台角色可切 orgId;org_admin 页面只显示自己组织且隐藏 orgId 选择器
|
|
|
|
|
+
|
|
|
|
|
+**步骤:**
|
|
|
|
|
+- [ ] 基于 List.vue + Form.vue 模式组合实现
|
|
|
|
|
+- [ ] 成员类型下拉:`planner`/`user`
|
|
|
|
|
+- [ ] 手机号脱敏显示(前端复用 `DataMaskUtil` 逻辑或后端已脱敏)
|
|
|
|
|
+- [ ] 冒烟验证
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 任务 14:前端 Dashboard.vue(统计大页面)
|
|
|
|
|
+
|
|
|
|
|
+**文件:**
|
|
|
|
|
+- 新增:`cfc-web/src/views/admin/org/Dashboard.vue`
|
|
|
|
|
+
|
|
|
|
|
+**布局:**
|
|
|
|
|
+- 顶部 5 卡片:成员总数/规划师数/家庭数/总订单/总消费
|
|
|
|
|
+- 第二行:活跃家庭数/任务完成率/打卡率
|
|
|
|
|
+- 第三行:近 30 天活跃度趋势图(ECharts 柱状/折线)、营收趋势图、日趋势图
|
|
|
|
|
+- 参考:现有 Dashboard 页面的 ECharts 用法(`cfc-web/src/views/Dashboard.vue` 或 admin 下其他统计页)
|
|
|
|
|
+
|
|
|
|
|
+**步骤:**
|
|
|
|
|
+- [ ] 引入 ECharts(项目已有依赖),用 `resize` 指令响应式
|
|
|
|
|
+- [ ] `mounted` 调 5 个 stats 接口渲染
|
|
|
|
|
+- [ ] 日期筛选器(默认近 30 天,startDate/endDate 传给后端)
|
|
|
|
|
+- [ ] 冒烟验证
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 任务 15:路由 + 菜单 + Login 分流
|
|
|
|
|
+
|
|
|
|
|
+**文件:**
|
|
|
|
|
+- 修改:`cfc-web/src/router/index.js`
|
|
|
|
|
+- 修改:`cfc-web/src/views/Layout.vue`
|
|
|
|
|
+- 修改:`cfc-web/src/views/Login.vue`
|
|
|
|
|
+
|
|
|
|
|
+**路由新增(参考 supply-system 路由格式):**
|
|
|
|
|
+```javascript
|
|
|
|
|
+{
|
|
|
|
|
+ path: '/org',
|
|
|
|
|
+ component: Layout,
|
|
|
|
|
+ redirect: '/org/list',
|
|
|
|
|
+ meta: { title: '组织管理', icon: 'el-icon-office-building', perm: 'system:org' },
|
|
|
|
|
+ children: [
|
|
|
|
|
+ { path: 'list', component: () => import('@/views/admin/org/List.vue'), meta: { title: '组织列表', perm: 'system:org' } },
|
|
|
|
|
+ { path: 'create', component: () => import('@/views/admin/org/Form.vue'), meta: { title: '新建组织', perm: 'system:org' } },
|
|
|
|
|
+ { path: 'edit/:id', component: () => import('@/views/admin/org/Form.vue'), meta: { title: '编辑组织', perm: 'system:org', hidden: true } },
|
|
|
|
|
+ { path: ':id', component: () => import('@/views/admin/org/Detail.vue'), meta: { title: '组织详情', perm: 'system:org', hidden: true } },
|
|
|
|
|
+ { path: ':id/dashboard', component: () => import('@/views/admin/org/Dashboard.vue'), meta: { title: '统计大屏', perm: 'system:org', hidden: true } },
|
|
|
|
|
+ ]
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Layout.vue 菜单新增(参考供应商体系分组位置 ~323 行):**
|
|
|
|
|
+```javascript
|
|
|
|
|
+// --- 组织管理 ---
|
|
|
|
|
+{ title: '组织管理', icon: 'el-icon-office-building', perm: 'system:org',
|
|
|
|
|
+ children: [
|
|
|
|
|
+ { path: '/org/list', label: '组织列表', icon: 'el-icon-s-management', perm: 'system:org' },
|
|
|
|
|
+ // org_admin 专属:不显示列表,只显示大屏
|
|
|
|
|
+ ]
|
|
|
|
|
+},
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**Login.vue 角色分流(在登录成功回调中):**
|
|
|
|
|
+```javascript
|
|
|
|
|
+// 现有逻辑后追加:
|
|
|
|
|
+if (roles.includes('org_admin') && !roles.some(r => ['admin','operator','finance'].includes(r))) {
|
|
|
|
|
+ // 纯 org_admin 视角
|
|
|
|
|
+ const res = await myOrg() // 调 /api/admin/org/my-org
|
|
|
|
|
+ if (res.code === 200 && res.data) {
|
|
|
|
|
+ router.push(`/org/${res.data.id}/dashboard`)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ router.push('/login') // 组织停用或未关联
|
|
|
|
|
+ Message.error('组织已停用或未关联组织')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+```
|
|
|
|
|
+
|
|
|
|
|
+**步骤:**
|
|
|
|
|
+- [ ] 修改 router/index.js 新增上述路由
|
|
|
|
|
+- [ ] 修改 Layout.vue 侧边栏菜单数组插入「组织管理」分组
|
|
|
|
|
+- [ ] 修改 Login.vue 登录成功分流逻辑(参考现有 teacher 分流)
|
|
|
|
|
+- [ ] 执行 `cd cfc-web && npm run serve` 完整冒烟:登录 org_admin 账号 → 自动跳 Dashboard;平台 admin → 看到组织管理菜单 → 进列表/新建/详情/大屏均可访问
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+### 任务 16:最终验收检查
|
|
|
|
|
+
|
|
|
|
|
+**执行清单:**
|
|
|
|
|
+- [ ] 后端:`cd cfc-backend && mvn clean compile` 编译通过(0 error)
|
|
|
|
|
+- [ ] 后端:检查无重复路由(`grep -rn '@PostMapping' .../controller/admin/ | grep -oP '@PostMapping\("\K[^"]*' | sort -u | wc -l` 与预期一致)
|
|
|
|
|
+- [ ] 后端:`AdminRolesTest` 单元测试通过(`mvn test -Dtest=AdminRolesTest`)
|
|
|
|
|
+- [ ] 前端:`cd cfc-web && npm run serve` 启动无控制台报错
|
|
|
|
|
+- [ ] 前端:页面冒烟通过(列表/新建/编辑/详情/大屏、org_admin 登录跳转、权限菜单显隐)
|
|
|
|
|
+- [ ] 文档:`docs/superpowers/api/API_REFERENCE.md` 已同步 13 个新接口
|
|
|
|
|
+- [ ] 代码规范:无 `console.log`/`debugger`、无 `@GetMapping`、无硬编码、Bean 名无冲突
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 实现顺序建议
|
|
|
|
|
+
|
|
|
|
|
+1. **Task 1-3(DB + AdminRoles + Entity/Mapper)** — 基础设施,互不依赖可并行
|
|
|
|
|
+2. **Task 4-7(Service 层)** — 依赖 Task 3,按 Service 依赖顺序:OrganizationService → UserSearchService → OrgMemberService → OrgStatsService
|
|
|
|
|
+3. **Task 8-9(鉴权 + Controller)** — 依赖 Service,Controller 间无依赖可并行
|
|
|
|
|
+4. **Task 10-14(前端页面)** — 依赖后端 API 定稿,可并行开发页面骨架
|
|
|
|
|
+5. **Task 15(路由/菜单/Login)** — 依赖页面完成
|
|
|
|
|
+6. **Task 16(验收)**
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 风险点与缓解
|
|
|
|
|
+
|
|
|
|
|
+| 风险 | 缓解 |
|
|
|
|
|
+|------|------|
|
|
|
|
|
+| 统计查询性能(大组织 500+ 成员) | 严格批量 `IN` 查询;如超时再加 Redis 缓存(overview 优先) |
|
|
|
|
|
+| org_admin 角色与既有 teacher/butler 等角色冲突 | AdminRoles 归一化逻辑已验证;`isOrgAdminOnly` 明确排除平台角色 |
|
|
|
|
|
+| 版本构建文件冲突 | 本计划不改动 cfc-web 版本文件;前端仅改 src/ 下业务代码 |
|
|
|
|
|
+| Bean 名冲突 | 类名 `AdminOrgController/Service` 与现有 `Admin*Controller/Service` 均不重名 |
|
|
|
|
|
+| 接口路径重复 | 任务 9 完成后执行路由冲突检查命令 |
|
|
|
|
|
+| 前端 org_admin 登录跳转死循环 | Login.vue 分流后直接 `router.push` 不再触发守卫;guard 中排除 `/org/:id/dashboard` |
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+## 验收标准(对应规格文档)
|
|
|
|
|
+
|
|
|
|
|
+| 规格项 | 验收方式 |
|
|
|
|
|
+|--------|----------|
|
|
|
|
|
+| 平台创建组织+绑定管理员 | 登录 admin → 组织列表 → 新建 → 填手机号 → 提交成功 → 该手机号可登录管理端并自动跳大屏 |
|
|
|
|
|
+| org_admin 仅看自己组织 | 登录 org_admin → 组织列表仅显示自己组织 / 成员列表仅显示本组织成员 / 统计仅本组织数据 |
|
|
|
|
|
+| 成员管理 增/删/改类型 | Detail 页:搜索用户 → 添加为 planner/user → 列表出现 → 切换类型 → 移除 → 列表消失 |
|
|
|
|
|
+| 统计大屏 5 口径 | Dashboard 页:5 卡片 + 3 图表均有数据,数字与数据库口径一致(人工抽查 1-2 项) |
|
|
|
|
|
+| 组织停用后 org_admin 访问拦截 | 平台 admin 停用组织 → org_admin 登录 → 跳登录页提示"组织已停用" |
|
|
|
|
|
+| 编译验证 | `mvn clean compile` 0 error;前端 `npm run serve` 无报错 |
|
|
|
|
|
+
|
|
|
|
|
+---
|
|
|
|
|
+
|
|
|
|
|
+**计划完成。** 按任务顺序逐步实现,每步 `mvn clean compile` 验证,前端页面 `npm run serve` 冒烟。遇到编译错误优先修复,不跳过步骤。
|