|
|
@@ -91,33 +91,48 @@ CREATE TABLE IF NOT EXISTS org_members (
|
|
|
|
|
|
**`create` 接口逻辑:**
|
|
|
1. 按 `adminPhone` 查找已有 `users` 记录(如有则复用);否则创建新 `users` 记录
|
|
|
-2. 为新用户设置 `roles = ["org_admin"]`、`role = "org_admin"`
|
|
|
-3. 创建 `organizations` 记录,`admin_user_id` 指向该 userId
|
|
|
-4. 返回 `organization` + `adminUser`
|
|
|
+2. 为新用户设置 `roles = ["org_admin"]`、`role = "org_admin"`(单一身份,不叠加其他后台角色)
|
|
|
+3. `code` 为空时服务端自动生成(`ORG` + 6 位随机大写字母数字,重试至唯一)
|
|
|
+4. 创建 `organizations` 记录,`admin_user_id` 指向该 userId
|
|
|
+5. 返回 `organization` + `adminUser`
|
|
|
|
|
|
### 4.2 组织成员管理(平台角色 + 组织管理员)
|
|
|
|
|
|
| 路径 | 说明 | 权限说明 |
|
|
|
|------|------|----------|
|
|
|
-| `POST /api/admin/org/member/list` | 成员列表(含简要家庭/任务/订单信息) | 平台看全量;org_admin 只看自己组织 |
|
|
|
-| `POST /api/admin/org/member/add` | 添加成员 | 同上 |
|
|
|
+| `POST /api/admin/org/member/list` | 成员列表(含简要家庭/任务/订单信息,默认只返回 `status=active`) | 平台看全量;org_admin 只看自己组织 |
|
|
|
+| `POST /api/admin/org/my-org` | 组织管理员获取自己所属的组织(首页跳转目标;停用返回错误) | 无参数,从 JWT 取 userId |
|
|
|
+| `POST /api/admin/org/user/search` | 按手机号/昵称搜索平台用户(添加成员时下拉选人用) | 同上 |
|
|
|
+| `POST /api/admin/org/member/add` | 添加成员(已存在记录则复活为 active 并更新 memberType) | 同上 |
|
|
|
| `POST /api/admin/org/member/remove` | 移除成员(软删,status=inactive) | 同上 |
|
|
|
-| `POST /api/admin/org/member/change-type` | 变更成员类型(planner ↔ user) | 同上 |
|
|
|
-
|
|
|
-**`member/list` 返回字段示例:**
|
|
|
+| `POST /api/admin/org/member/change-type` | 变更成员类型(planner ↔ user),同时会把 inactive 成员复活为 active | 同上 |
|
|
|
+
|
|
|
+**`member/list` 返回字段说明:**
|
|
|
+- `userId`:关联的用户ID
|
|
|
+- `nickname`:昵称
|
|
|
+- `phone`:手机号(脱敏后显示,如 138****1234)
|
|
|
+- `memberType`:`planner`(规划师)或 `user`(普通用户)
|
|
|
+- `joinedAt`:加入组织时间
|
|
|
+- `familyCount`:该成员关联的家庭数(仅计 `org_members` 直属家庭,不含 `teacherFamilyIds` 绑定)
|
|
|
+- `totalOrders`:该成员关联的有效订单总数(仅计 `product_orders` 状态为 COMPLETED/PAID,不含套餐/包年订单)
|
|
|
+- `totalConsumption`:该成员关联的消费总额(单位分,`product_orders` 完成订单的 amount 总和)
|
|
|
+
|
|
|
+**`member/add` 参数:**
|
|
|
+| 字段 | 类型 | 必填 | 说明 |
|
|
|
+|------|------|------|------|
|
|
|
+| `orgId` | Long | 是 | 目标组织ID |
|
|
|
+| `userId` | Long | 是 | 被添加用户ID |
|
|
|
+| `memberType` | String | 是 | `planner` / `user` |
|
|
|
+
|
|
|
+**`org/user/search` 返回字段:**
|
|
|
```json
|
|
|
{
|
|
|
- "id": 1,
|
|
|
- "userId": 1001,
|
|
|
+ "id": 1001,
|
|
|
"nickname": "张三",
|
|
|
"phone": "138****1234",
|
|
|
"avatar": "/static/avatar.jpg",
|
|
|
- "memberType": "planner",
|
|
|
- "status": "active",
|
|
|
- "joinedAt": "2026-08-01T10:00:00",
|
|
|
- "familyCount": 3,
|
|
|
- "totalOrders": 12,
|
|
|
- "totalConsumption": 58000
|
|
|
+ "memberType": "user",
|
|
|
+ "status": "active"
|
|
|
}
|
|
|
```
|
|
|
|
|
|
@@ -131,21 +146,92 @@ CREATE TABLE IF NOT EXISTS org_members (
|
|
|
| `POST /api/admin/org/stats/revenue` | 营收:订单金额/会员订阅/佣金,按时间段分组 |
|
|
|
| `POST /api/admin/org/stats/trend` | 日/周维度趋势图数据(活跃成员数/订单数/收入) |
|
|
|
|
|
|
-**实现方式:** 基于 `org_members` 表取出 `user_id` 集合,批量查现有表(`families`/`tasks`/`product_orders`/`commission_records` 等),不新建统计汇总表,避免数据同步问题。组织规模大时可加 Redis 缓存(TTL 5分钟)。
|
|
|
+**请求参数(所有 stats 接口):** `orgId`(body 传入)+ 可选 `startDate` / `endDate`(仅 date 部分 `yyyy-MM-dd`,不传默认近 30 天)。
|
|
|
+
|
|
|
+### 统计口径定义(实现必须严格遵循)
|
|
|
+
|
|
|
+**overview(总览卡片)**
|
|
|
+
|
|
|
+| 指标 | 口径 |
|
|
|
+|------|------|
|
|
|
+| 成员总数 `memberCount` | `org_members` 中 `org_id=? AND status='active'` 行数 |
|
|
|
+| 规划师数 `plannerCount` | 同上再加 `member_type='planner'` |
|
|
|
+| 普通用户数 `userCount` | 同上再加 `member_type='user'` |
|
|
|
+| 家庭数 `familyCount` | 上述成员 user_id 集合 → `families.creator_id IN (...)` 去重计数 |
|
|
|
+| 总订单数 `totalOrders` | 上述 user_id 集合 → `product_orders` 中 `user_id IN (...) AND status IN ('PAID','COMPLETED')` 计数 |
|
|
|
+| 总消费 `totalConsumption`(分) | 上述 order 集合的 `amount` 总和 |
|
|
|
+
|
|
|
+**families(家庭维度)**
|
|
|
+
|
|
|
+| 指标 | 口径 |
|
|
|
+|------|------|
|
|
|
+| 活跃家庭数 `activeFamilyCount` | `families.creator_id IN (成员user_id集合) AND updated_at >= 近30天` 计数 |
|
|
|
+| 成员任务完成率 `taskCompletionRate`(%) | 近30天 `tasks` 中 `status='completed'` 数 ÷ 同范围 `tasks` 总数;无分母时返回 0 |
|
|
|
+| 打卡率 `checkinRate`(%) | 近30天有任意打卡记录(`daily_checkin`/`health_checkins`/`emotion_checkin`)的成员数 ÷ 成员总数 |
|
|
|
+
|
|
|
+**active(活跃度,按天分组)**
|
|
|
+
|
|
|
+| 指标 | 口径 |
|
|
|
+|------|------|
|
|
|
+| 任务完成数 | 按 `tasks.finished_at` 日期分组计数(范围 startDate~endDate) |
|
|
|
+| 打卡数 | 按 `created_at` 日期分组,`daily_checkin` + `health_checkin` + `emotion_checkin` 求和 |
|
|
|
+| 报告上传数 | 按 `created_at` 日期分组,`dan_report_uploads` 计数 |
|
|
|
+
|
|
|
+**revenue(营收,按天/周分组)**
|
|
|
+
|
|
|
+| 指标 | 口径 |
|
|
|
+|------|------|
|
|
|
+| 订单金额 | `product_orders` 中成员相关订单按 `paid_at` 分组 amount 求和(状态 PAID/COMPLETED) |
|
|
|
+| 会员订阅金额 | `member_subscription_order` 按 `created_at` 分组 amount 求和(状态已支付) |
|
|
|
+| 佣金支出 | `commission_records` 中受益人属于成员的按 `created_at` 分组 amount 求和 |
|
|
|
+
|
|
|
+**trend(趋势图)**
|
|
|
+
|
|
|
+| 指标 | 口径 |
|
|
|
+|------|------|
|
|
|
+| 活跃成员数 | 当日有任一上述行为(任务/打卡/订单)的成员去重数 |
|
|
|
+| 订单数 | 当日 `product_orders`(PAID/COMPLETED)数 |
|
|
|
+| 收入(分) | 当日 order amount 求和 |
|
|
|
+
|
|
|
+### 性能约束(必须遵守)
|
|
|
+
|
|
|
+- **禁止 per-member 循环查询**:所有统计必须先用一次 `org_members` 查询拿到 `user_id` 集合,再用 `IN (...)` 批量查询,由数据库聚合(`GROUP BY` / `COUNT` / `SUM`)
|
|
|
+- 组织规模上限预估 500 成员;超出时先做性能压测再考虑缓存
|
|
|
+- 可选优化:热点统计(overview)加 Redis 缓存,key `org:stats:overview:{orgId}`,TTL 5 分钟;不做缓存也要保证接口 2s 内返回
|
|
|
|
|
|
---
|
|
|
|
|
|
## 五、权限规则
|
|
|
|
|
|
+### 5.1 角色定义
|
|
|
+
|
|
|
+`AdminRoles` 新增 `org_admin`(组织管理员)到 `RESIDENT_ROLES`,并补充 `roleLabel()` 映射 `org_admin → 组织管理员`。新增工具方法:
|
|
|
+
|
|
|
+```java
|
|
|
+/** 判断 roles 是否包含 org_admin */
|
|
|
+public static boolean isOrgAdmin(List<String> roles)
|
|
|
+
|
|
|
+/** 判断是否纯组织管理员视角(拥有 org_admin 且不含平台角色) */
|
|
|
+public static boolean isOrgAdminOnly(List<String> roles)
|
|
|
+```
|
|
|
+
|
|
|
+### 5.2 接口鉴权
|
|
|
+
|
|
|
```
|
|
|
-requirePlatformOrOrgAdmin(orgId, userId):
|
|
|
- 1. 先取当前用户所属的 orgId(organizations.admin_user_id = userId)
|
|
|
- 2. 若为平台角色 → 放行
|
|
|
- 3. 若 orgId 与请求中的 orgId 一致 → 放行
|
|
|
- 4. 否则 → 403 无权限
|
|
|
+requireOrgAccess(orgId, userId, roleObj, rolesObj):
|
|
|
+ 1. 若为平台角色(admin/operator/finance)→ 放行(可指定任意 orgId)
|
|
|
+ 2. 若 roles 含 org_admin:
|
|
|
+ a. 查 organizations 中 admin_user_id = userId 且 status='active' 的组织(若停用 → 返回"组织已停用")
|
|
|
+ b. 取该组织的 orgId,与请求中的 orgId 比较,一致 → 放行
|
|
|
+ 3. 否则 → 403 无权限
|
|
|
```
|
|
|
|
|
|
-**排除规则:** 组织管理员只能访问 `/api/admin/org/**` 下与自身组织相关的接口,不能访问用户管理、商品、佣金等其他管理端接口。
|
|
|
+### 5.3 排除规则
|
|
|
+
|
|
|
+- **组织管理员仅能访问 `/api/admin/org/**` 下与自身组织相关的接口**;不能访问用户管理、商品、佣金等其他管理端接口(现有 `requirePlatformRole` 会拦截,`org_admin` 不属于平台角色)
|
|
|
+- **成员搜索走组织专属接口** `/api/admin/org/user/search`,不开放 `/api/admin/user/search` 给 org_admin
|
|
|
+- **组织停用(status=disabled)后**:组织管理员登录管理端仍可进入(token 有效),但访问 `/api/admin/org/**` 时统一返回"组织已停用",防止数据外露;平台角色不受影响
|
|
|
+- **前端路由守卫**:`router.beforeEach` 中新增逻辑——token 角色含 `org_admin` 且不含平台角色时,访问 `/dashboard` 或 `/org` 列表页自动重定向到其组织的 `/org/:id/dashboard`;访问其他管理端路由时拦截提示无权限
|
|
|
|
|
|
---
|
|
|
|
|
|
@@ -169,7 +255,7 @@ requirePlatformOrOrgAdmin(orgId, userId):
|
|
|
|
|
|
### 组织管理员首页
|
|
|
|
|
|
-组织管理员登录管理端后,自动跳转至自己组织的 Dashboard(`/org/:id/dashboard`),不展示列表页。
|
|
|
+组织管理员登录管理端后,自动跳转至自己组织的 Dashboard(`/org/:id/dashboard`),不展示列表页。跳转链路:`Login.vue` 登录成功 → 按角色分流 → 检测 roles 含 `org_admin` 且不含平台角色 → 调 `POST /api/admin/org/my-org` 拿到自己的组织 → 跳 Dashboard。若组织已停用,则跳转登录页并提示"组织已停用"。
|
|
|
|
|
|
### 时间格式规范
|
|
|
|
|
|
@@ -215,22 +301,22 @@ try {
|
|
|
|
|
|
| 新增文件 | 说明 |
|
|
|
|----------|------|
|
|
|
-| `entity/Organization.java` | 组织实体 |
|
|
|
-| `entity/OrgMember.java` | 组织成员实体 |
|
|
|
-| `mapper/OrganizationMapper.java` | MyBatis Mapper |
|
|
|
-| `mapper/OrgMemberMapper.java` | MyBatis Mapper |
|
|
|
-| `service/OrganizationService.java` | 组织业务逻辑 |
|
|
|
-| `service/OrgMemberService.java` | 组织成员业务逻辑 |
|
|
|
-| `service/OrgStatsService.java` | 统计大屏业务逻辑 |
|
|
|
-| `controller/admin/AdminOrgController.java` | 组织 CRUD 接口 |
|
|
|
+| `entity/Organization.java` | 组织实体(`@TableName("organizations")`) |
|
|
|
+| `entity/OrgMember.java` | 组织成员实体(`@TableName("org_members")`) |
|
|
|
+| `mapper/OrganizationMapper.java` | 组织 Mapper |
|
|
|
+| `mapper/OrgMemberMapper.java` | 成员 Mapper |
|
|
|
+| `service/OrganizationService.java` | 组织 CRUD + `getByAdminId` + 管理员账号创建 |
|
|
|
+| `service/OrgMemberService.java` | 成员管理(add/remove/change-type/list 聚合家庭/订单摘要) |
|
|
|
+| `service/OrgStatsService.java` | 统计大屏(5 个口径的批量聚合查询) |
|
|
|
+| `controller/admin/AdminOrgController.java` | 组织 CRUD + my-org + user/search |
|
|
|
| `controller/admin/AdminOrgMemberController.java` | 成员管理接口 |
|
|
|
| `controller/admin/AdminOrgStatsController.java` | 统计大屏接口 |
|
|
|
|
|
|
| 修改文件 | 说明 |
|
|
|
|----------|------|
|
|
|
-| `config/DatabaseInitializer.java` | 新增建表迁移 |
|
|
|
-| `resources/schema.sql` | 新增两张表的 CREATE TABLE 语句 |
|
|
|
-| `common/AdminRoles.java` | 新增 `org_admin` 到 `RESIDENT_ROLES`,添加 `isOrgAdmin()` 方法 |
|
|
|
+| `config/DatabaseInitializer.java` | 新增建表迁移(见第七节) |
|
|
|
+| `resources/schema.sql` | 新增两张表的 `CREATE TABLE IF NOT EXISTS` 语句 |
|
|
|
+| `common/AdminRoles.java` | `org_admin` 加入 `RESIDENT_ROLES`;补 `roleLabel` 映射、`isOrgAdmin`/`isOrgAdminOnly` 方法 |
|
|
|
|
|
|
### 前端(cfc-web)
|
|
|
|
|
|
@@ -244,8 +330,22 @@ try {
|
|
|
|
|
|
| 修改文件 | 说明 |
|
|
|
|----------|------|
|
|
|
-| `src/router/index.js` | 新增 4 条路由 |
|
|
|
-| `src/views/Layout.vue` | 新增菜单项 |
|
|
|
+| `src/router/index.js` | 新增 4 条路由(含 org_admin 守卫分流) |
|
|
|
+| `src/views/Layout.vue` | 新增菜单项 + 组织管理员专属菜单显隐 |
|
|
|
+| `src/views/Login.vue` | 登录成功后按 `org_admin` 角色分流 |
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 八点五、实现顺序与依赖
|
|
|
+
|
|
|
+1. **后端基础层**:Entity → Mapper → DatabaseInitializer + schema.sql → `mvn clean compile` 验证
|
|
|
+2. **AdminRoles 改造**:加角色常量/方法(不破坏既有 `teacher` 等角色登录)
|
|
|
+3. **OrganizationService/OrgMemberService**:组织 CRUD + 成员管理(含 `/api/admin/org/user/search`、`my-org`)
|
|
|
+4. **OrgStatsService**:5 个统计接口(批量 `IN` 查询,禁止 N+1)
|
|
|
+5. **Controller 鉴权**:三个 Controller 统一调 `requireOrgAccess`(建议抽 `common/AdminOrgAccess.java` 静态工具类)
|
|
|
+6. **前端**:`src/api/org.js` → 4 页面 → 路由/菜单/Login 分流 → 本地 `npm run serve` 冒烟
|
|
|
+
|
|
|
+**校验方式**:每步后端改动后 `mvn clean compile`;前端改完后 `npm run serve` 启动页面冒烟(不要求完整跑通)。
|
|
|
|
|
|
---
|
|
|
|