面向 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 类名不能与其他包同名类重名
文件:
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='组织成员关联表';
文件:
cfc-backend/src/main/java/com/etotem/cfc/common/AdminRoles.javacfc-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"));
}
文件:
cfc-backend/src/main/java/com/etotem/cfc/entity/Organization.javacfc-backend/src/main/java/com/etotem/cfc/entity/OrgMember.javacfc-backend/src/main/java/com/etotem/cfc/mapper/OrganizationMapper.javacfc-backend/src/main/java/com/etotem/cfc/mapper/OrgMemberMapper.javacfc-backend/src/main/resources/mapper/OrgMemberMapper.xml步骤:
Organization.java 实体类(参考 SupplySystem.java 模板):
@TableName("organizations")、@TableId(type = IdType.AUTO)、Lombok @DataOrgMember.java 实体类:
@TableName("org_members")、@TableId(type = IdType.AUTO)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 验证编译通过
文件:
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 验证编译通过
文件:
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>userId/nickname/phone(脱敏 DataMaskUtil.maskPhone)/avatar/memberType/joinedAt/familyCount/totalOrders/totalConsumptionaddMember(orgId, userId, memberType):
org_members 是否已存在记录(uk_org_user)removeMember(orgId, userId):UPDATE status='inactive' WHERE org_id=... AND user_id=...changeType(orgId, userId, memberType):
userSearch(keyword, memberType):委托 UserSearchService.search(keyword, memberType)mvn clean compile 验证编译通过文件:
cfc-backend/src/main/java/com/etotem/cfc/service/UserSearchService.java步骤:
search(keyword, memberType):
LambdaQueryWrapper<User>:like nickname 或 phone,可选过滤 memberType(需通过 family_members 关联判断规划师/用户,简化:只按 User 表字段查,不加 memberType 过滤)id/nickname/phone/avatar/statusmvn clean compile 验证编译通过文件:
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 = size3) 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') count6) totalConsumption = 同上 total_amount sum |
families(orgId, startDate, endDate) |
1) memberIds 同概览2) activeFamilyCount = families.creator_id IN (memberIds) AND updated_at >= startDate count3) 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) |
合并上述三指标:活跃成员数(当日有任一行为去重)、订单数、收入(分) |
实现约束:
memberIds 再 IN (...) 批量聚合org:stats:overview:{orgId} TTL 5min(先不加,性能够用再加)步骤:
List<Long> getActiveMemberIds(orgId) 复用mvn clean compile 验证编译通过文件:
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 验证编译通过
文件:
cfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminOrgController.javacfc-backend/src/main/java/com/etotem/cfc/controller/admin/AdminOrgMemberController.javacfc-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 | 同上 |
步骤:
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 个新接口
文件:
cfc-web/src/api/org.js步骤:
request 工具(@/utils/request)listOrg, createOrg, updateOrg, disableOrg, detailOrg, myOrg, userSearch, memberList, addMember, removeMember, changeMemberType, statsOverview, statsFamilies, statsActive, statsRevenue, statsTrendcd cfc-web && npm run lint(如配置)或直接进入下一步文件:
cfc-web/src/views/admin/org/List.vue参考: cfc-web/src/views/admin/supply-system/List.vue
关键点:
v-if="$hasPerm('system:org')" 或 perm 判断步骤:
cd cfc-web && npm run serve 启动冒烟验证页面渲染无报错文件:
cfc-web/src/views/admin/org/Form.vue参考: cfc-web/src/views/admin/supply-system/Form.vue
字段: 名称(必填)/类型下拉/编号(可选,留空自动生成)/管理员手机号(必填)/管理员姓名/联系人/联系电话/描述
步骤:
createOrg;编辑模式:提交 updateOrg文件:
cfc-web/src/views/admin/org/Detail.vue功能:
/user/search)→ 选择用户 + 类型 → 添加成员步骤:
planner/userDataMaskUtil 逻辑或后端已脱敏)文件:
cfc-web/src/views/admin/org/Dashboard.vue布局:
cfc-web/src/views/Dashboard.vue 或 admin 下其他统计页)步骤:
resize 指令响应式mounted 调 5 个 stats 接口渲染文件:
cfc-web/src/router/index.jscfc-web/src/views/Layout.vuecfc-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('组织已停用或未关联组织')
}
}
步骤:
cd cfc-web && npm run serve 完整冒烟:登录 org_admin 账号 → 自动跳 Dashboard;平台 admin → 看到组织管理菜单 → 进列表/新建/详情/大屏均可访问执行清单:
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 启动无控制台报错docs/superpowers/api/API_REFERENCE.md 已同步 13 个新接口console.log/debugger、无 @GetMapping、无硬编码、Bean 名无冲突| 风险 | 缓解 |
|---|---|
| 统计查询性能(大组织 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 冒烟。遇到编译错误优先修复,不跳过步骤。