2026-10-22-org-admin-management-implementation.md 32 KB

机构/企业组织管理 实现计划

面向 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 末尾):

// 迁移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 追加模板(文件末尾):

-- 迁移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 "组织管理员";
  • [ ] 新增静态方法:

    /** 判断 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 验证编译通过

测试用例参考:

@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>,新增方法:

    // 根据 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)),存在则复用;否则创建新用户:

      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')
2) memberCount = size
3) plannerCount = count where member_type='planner'
4) familyCount = families.creator_id IN (memberIds) 去重计数
5) totalOrders = product_orders buyer_id IN (memberIds) AND status IN ('PAID','COMPLETED') count
6) totalConsumption = 同上 total_amount sum
families(orgId, startDate, endDate) 1) memberIds 同概览
2) activeFamilyCount = families.creator_id IN (memberIds) AND updated_at >= startDate count
3) taskCompletionRate = tasks status='completed' AND completed_at BETWEEN startDate AND endDate / 同范围总任务数
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 复用:

    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):

@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")) 统一提取
  • [ ] 完成后执行 路由冲突检查:

    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 路由格式):

{
  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 行):

// --- 组织管理 ---
{ 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 角色分流(在登录成功回调中):

// 现有逻辑后追加:
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 冒烟。遇到编译错误优先修复,不跳过步骤。