|
|
@@ -0,0 +1,238 @@
|
|
|
+# 设计:管理端家庭信息查看 + 家庭名称「XX家庭」
|
|
|
+
|
|
|
+日期:2026-10-30
|
|
|
+分支:`feature/family-overview`(worktree `/app/cfc-family-overview`)
|
|
|
+范围:cfc-backend(管理端接口 + 家庭命名规则)、cfc-web(管理端家庭列表页)
|
|
|
+
|
|
|
+---
|
|
|
+
|
|
|
+## 1. 意图与背景
|
|
|
+
|
|
|
+管理端「家庭列表」页(`cfc-web/src/views/Families.vue`)目前只能看到家庭名称、邀请码、成员、优惠券。
|
|
|
+运营/客服排查一个家庭时,需要跨多个模块(报告管理、健康方案管理、任务管理)分别用家庭名过滤,
|
|
|
+无法在一个地方看到该家庭实际沉淀了什么。用户要求:
|
|
|
+
|
|
|
+1. **家庭管理增加「家庭相关信息查看」功能**,包含报告、方案、任务三类。
|
|
|
+2. **家庭主账号填写真实姓名后,家庭名称自动改为「真实姓名家庭」**。
|
|
|
+
|
|
|
+## 2. 目标 / 非目标
|
|
|
+
|
|
|
+### 目标
|
|
|
+- G1:管理端家庭列表每行可一键打开该家庭的报告 / 方案 / 任务概览,含计数卡片 + 可下钻明细。
|
|
|
+- G2:家庭主账号(`families.creator_id` 对应用户)的 `users.real_name` 非空时,`families.name` 变为「真实姓名 + 家庭」。
|
|
|
+- G3:新命名规则对**存量数据**同样生效(一次性回填迁移)。
|
|
|
+- G4:零回归——不破坏现有家庭列表 / 成员 / 优惠券功能;不改变用户端既有接口行为(除家庭名展示值)。
|
|
|
+
|
|
|
+### 非目标
|
|
|
+- 不做家庭维度的报告详情页跳转(只读展示,不提供编辑/下载)。
|
|
|
+- 不做方案、任务的新增/修改(管理端只读)。
|
|
|
+- 不改报告/方案/任务本身的业务规则与表结构。
|
|
|
+- 不引入新表、不引入新依赖。
|
|
|
+
|
|
|
+## 3. 现状(已勘察核实)
|
|
|
+
|
|
|
+### 后端
|
|
|
+| 位置 | 现状 |
|
|
|
+|---|---|
|
|
|
+| `AdminController`(`/api/admin`) | 已有 `POST /families`(列表)、`POST /families/{id}`(详情)、`POST /families/{id}/members`、`POST /families/{id}/update`、`POST /families/{id}/delete`;均校验 `role == "admin"` |
|
|
|
+| `Family` 实体 | 字段:`id, name, inviteCode, streetId, province, city, district, street, teacherId, butlerId, creatorId, createdAt, updatedAt` |
|
|
|
+| `User` 实体 | 有 `realName`(真实姓名)、`familyId`、`role` |
|
|
|
+| `FamilyMemberService.createFamilyForUser` | 建家庭时命名:`nickname + "的家庭"`,无昵称则 `"我的家庭"` |
|
|
|
+| `UserService.updateUserInfo` | 用户改资料时写 `realName`,**不联动家庭名** |
|
|
|
+
|
|
|
+### 前端
|
|
|
+| 位置 | 现状 |
|
|
|
+|---|---|
|
|
|
+| `cfc-web/src/views/Families.vue` | 家庭列表页;已有「编辑」(仅家庭名称)/「查看成员」/「查看优惠券」/「发放优惠券」弹窗;操作列 140px 宽 |
|
|
|
+| `cfc-web/src/api/admin.js` | 已有 `getFamilyList / getFamilyDetail / updateFamily / deleteFamily / getFamilyMembers / setMemberAdminRole` |
|
|
|
+| `cfc-web/src/components/` | 已有 `StatusTag.vue`、`SectionTitle.vue`、`StatCard.vue`、`PageHeader.vue`(可复用) |
|
|
|
+| `cfc-web/src/views/admin/components/` | 已有 `ProductPicker.vue`、`ProductSkuEditor.vue`(子组件放置惯例) |
|
|
|
+
|
|
|
+### 数据规模(生产实测)
|
|
|
+| 数据 | 单家庭最大条数 | 说明 |
|
|
|
+|---|---|---|
|
|
|
+| `health_reports` | 8 | 类型:`gut_flora`(41) / `brain_status`(6) / `aptitude`(2) / `physical_exam`(1) |
|
|
|
+| `health_plans` | 4 | 状态:`published`(8) / `draft`(7) / `pending_review`(2);`plan_type` 均为 `health_plan` |
|
|
|
+| `tasks` | **346** | 状态:`pending`(3629) / `completed`(140) / `locked`(82) / `pending_review`(3);枚举含 assigned/in_progress/rejected/overdue/cancelled |
|
|
|
+
|
|
|
+> **关键约束**:任务量远大于报告与方案。若 overview 接口一次性返回全部任务明细,
|
|
|
+> 346 条 × N 字段会撑大响应体。设计上采用「计数 + 分页明细」。
|
|
|
+
|
|
|
+### 三张表的关联字段(已核实)
|
|
|
+- `health_reports`: `family_id`, `subject_id`, `report_type`, `report_number`, `report_date`, `person_name`, `status`(active/deleted/archived), `overall_score`, `gut_health_score`, `created_at`
|
|
|
+- `health_plans`: `family_id`, `member_ids`, `member_name`, `dimensions`, `goal`, `plan_content`, `plan_json`, `plan_type`, `status`, `teacher_id`, `total_days`, `created_at`
|
|
|
+- `tasks`: `family_id`, `family_member_id`, `child_id`, `title`, `description`, `status`, `category`, `points`, `deadline`, `executor_type`, `executor_id`, `task_type`, `source_type`, `created_at`, `completed_at`
|
|
|
+
|
|
|
+## 4. 决策(主理人已确认)
|
|
|
+
|
|
|
+| # | 决策点 | 结论 |
|
|
|
+|---|---|---|
|
|
|
+| D1 | 查看功能粒度 | **计数 + 可下钻明细**(计数卡片 → 展开明细列表) |
|
|
|
+| D2 | 「管理员」语义 | **家庭主账号**(`families.creator_id` 对应用户)填写真实姓名触发改名 |
|
|
|
+| D3 | 明细数据量 | 任务多(最大 346),overview 只回计数 + 报告/方案全量(≤8/≤4),任务明细走独立分页接口 |
|
|
|
+| D4 | 报告是否过滤 status | 不过滤,**全量返回并展示 status 列**(管理端需看到 archived/deleted 以便排查) |
|
|
|
+| D5 | 方案是否展示内容 | 展示 `goal` + `member_name` + `status` + `total_days`,**不返回 `plan_content`/`plan_json` 大字段**(避免响应体膨胀) |
|
|
|
+| D6 | 命名规则来源 | 真实姓名 `users.real_name` 非空 → `name = real_name + "家庭"`;否则保持现有 `nickname + "的家庭"` / `"我的家庭"` |
|
|
|
+| D7 | 改名触发时机 | ① 创建家庭时按新规则命名;② 用户更新资料(`realName` 变化)时联动;③ 存量一次性回填迁移 |
|
|
|
+| D8 | 改名冲突 | 用户手工改过的家庭名会被覆盖——**接受**(主理人要求「家庭名称改成真实姓名的XX家庭」) |
|
|
|
+
|
|
|
+## 5. 设计
|
|
|
+
|
|
|
+### 5.1 后端:家庭信息查看
|
|
|
+
|
|
|
+**新增 DTO**(`com.etotem.cfc.dto`):
|
|
|
+
|
|
|
+```
|
|
|
+FamilyOverviewDTO
|
|
|
+ family: Map<String,Object> // id, name, inviteCode, creatorId, createdAt, province/city/district/street
|
|
|
+ creatorName: String // 主账号昵称(展示用)
|
|
|
+ creatorRealName: String // 主账号真实姓名(展示用,可能为空)
|
|
|
+ counts: FamilyOverviewCounts
|
|
|
+ reports: int
|
|
|
+ plans: int
|
|
|
+ tasks: int
|
|
|
+ reportTypeCounts: Map<String,Integer> // gut_flora:N, brain_status:N ...
|
|
|
+ taskStatusCounts: Map<String,Integer> // pending:N, completed:N ...
|
|
|
+ reports: List<FamilyReportItemVO>
|
|
|
+ plans: List<FamilyPlanItemVO>
|
|
|
+```
|
|
|
+
|
|
|
+**新增 Service**:`FamilyAdminOverviewService`(`com.etotem.cfc.service`)
|
|
|
+- `FamilyOverviewDTO buildOverview(Long familyId)`:
|
|
|
+ - 校验 family 存在,否则抛 `RuntimeException("家庭不存在")`
|
|
|
+ - 计数用 MyBatis-Plus `selectCount`(3 次 + 2 次 groupBy)
|
|
|
+ - 报告明细:`LambdaQueryWrapper.eq(familyId).orderByDesc(report_date).orderByDesc(id)`,**全量返回**(最大 8 条)
|
|
|
+ - 方案明细:`eq(familyId).orderByDesc(created_at).orderByDesc(id)`,**全量返回**(最大 4 条)
|
|
|
+ - 任务明细:**不在此返回**,仅计数
|
|
|
+- `Page<Task> pageTasks(Long familyId, long page, long size)`:任务分页,按 `created_at desc, id desc`
|
|
|
+
|
|
|
+**新增 Controller**(新增独立 `AdminFamilyOverviewController`,路径 `/api/admin`):
|
|
|
+为避免把 AdminController 继续撑大,单独建控制器:
|
|
|
+
|
|
|
+| 方法 | 路径 | 入参 | 返回 |
|
|
|
+|---|---|---|---|
|
|
|
+| POST | `/api/admin/families/{id}/overview` | — | `Result<FamilyOverviewDTO>` |
|
|
|
+| POST | `/api/admin/families/{id}/tasks` | `{page, size}` | `Result<Page<TaskItemVO>>` |
|
|
|
+
|
|
|
+均校验 `role == "admin"`(与现有 `getFamilyMembers` 同款校验:`@RequestAttribute("role") String role`)。
|
|
|
+任务分页默认 `size=20`,上限 100。
|
|
|
+
|
|
|
+**VO 字段裁剪**(避免响应体膨胀):
|
|
|
+- `FamilyReportItemVO`: `id, reportType, reportNumber, reportDate, personName, subjectId, status, overallScore, gutHealthScore, createdAt`
|
|
|
+- `FamilyPlanItemVO`: `id, planType, memberName, goal, status, totalDays, teacherId, createdAt`
|
|
|
+- `TaskItemVO`: `id, title, status, category, points, deadline, childId, executorType, taskType, sourceType, createdAt, completedAt`
|
|
|
+
|
|
|
+### 5.2 后端:家庭命名「XX家庭」
|
|
|
+
|
|
|
+**新增工具方法**(放 `FamilyMemberService` 私有方法,或抽到 `FamilyNamingSupport`):
|
|
|
+
|
|
|
+```java
|
|
|
+static String buildFamilyName(User user) {
|
|
|
+ String realName = user.getRealName();
|
|
|
+ if (realName != null && !realName.trim().isEmpty()) {
|
|
|
+ return realName.trim() + "家庭";
|
|
|
+ }
|
|
|
+ String nickname = user.getNickname();
|
|
|
+ return (nickname != null && !nickname.isEmpty()) ? nickname + "的家庭" : "我的家庭";
|
|
|
+}
|
|
|
+```
|
|
|
+
|
|
|
+**触发点 1 — 创建家庭**:`FamilyMemberService.createFamilyForUser(user)` 第 913-915 行
|
|
|
+把 `family.setName(familyName)` 改为 `family.setName(buildFamilyName(user))`。
|
|
|
+
|
|
|
+**触发点 2 — 更新资料联动**:`UserService.updateUserInfo(userId, dto)`
|
|
|
+在 `userMapper.updateById(user)` 之后,若 `dto.getRealName()` 非空且与原值不同:
|
|
|
+- 查该用户作为 `creator_id` 的家庭(`families.creator_id = userId`)
|
|
|
+- 存在则 `family.name = realName.trim() + "家庭"` 并更新
|
|
|
+
|
|
|
+**触发点 3 — 存量回填**(一次性迁移,放 `DatabaseInitializer.runMigrations()` 新迁移号,幂等):
|
|
|
+
|
|
|
+```sql
|
|
|
+UPDATE families f
|
|
|
+JOIN users u ON u.id = f.creator_id
|
|
|
+SET f.name = CONCAT(TRIM(u.real_name), '家庭')
|
|
|
+WHERE u.real_name IS NOT NULL AND TRIM(u.real_name) <> ''
|
|
|
+ AND f.name <> CONCAT(TRIM(u.real_name), '家庭');
|
|
|
+```
|
|
|
+
|
|
|
+> **先只读 dry-run 查影响行数**,把结果报给主理人确认后再决定是否执行实际 UPDATE。
|
|
|
+> 迁移放在 `DatabaseInitializer`(幂等、可重复启动执行),与项目既有迁移约定一致。
|
|
|
+
|
|
|
+### 5.3 前端:管理端家庭列表页
|
|
|
+
|
|
|
+**新增 API**(`cfc-web/src/api/admin.js`):
|
|
|
+```js
|
|
|
+export function getFamilyOverview(id) { return request({ url: `/api/admin/families/${id}/overview`, method: 'post' }) }
|
|
|
+export function getFamilyTasks(id, params) { return request({ url: `/api/admin/families/${id}/tasks`, method: 'post', data: params }) }
|
|
|
+```
|
|
|
+
|
|
|
+**新增组件**:`cfc-web/src/views/admin/components/FamilyOverviewDialog.vue`
|
|
|
+- props: `visible`(sync)、`familyId`
|
|
|
+- 结构:
|
|
|
+ 1. `el-descriptions` 家庭基础信息(名称 / 邀请码 / 创建时间 / 主账号)
|
|
|
+ 2. **计数卡片区**:3 个 `StatCard`(报告 / 方案 / 任务),点击切换下方 tab;报告卡片下附类型分布小标签
|
|
|
+ 3. **明细区**:`el-tabs`
|
|
|
+ - 「报告」tab:`el-table` 全量(列:类型 / 报告编号 / 报告日期 / 姓名 / 状态 / 总分 / 肠道分),状态用 `StatusTag`
|
|
|
+ - 「方案」tab:`el-table` 全量(列:类型 / 成员 / 目标 / 状态 / 天数 / 创建时间)
|
|
|
+ - 「任务」tab:`el-table` + `el-pagination`(服务端分页,默认 20/页)
|
|
|
+- 报告类型中文映射复用 `ReportManagement.vue` 的 `gut_flora=菌群检测 / physical_exam=体检报告 / dan=DAN测评 / insurance=保单 / fortune=家庭周报`,补充 `brain_status=脑状态 / aptitude=先天潜能`
|
|
|
+- 方案状态映射复用 `HealthPlanManage.vue`:`draft=草稿 / pending_review=待审核 / published=已发布 / rejected=已驳回`
|
|
|
+- 任务状态映射复用 `Tasks.vue`:`pending=进行中 / pending_review=待审核 / completed=已完成`,补充 locked=前置锁定 / assigned=待接受 / in_progress=进行中 / rejected=已拒绝 / overdue=已过期 / cancelled=已取消
|
|
|
+
|
|
|
+**接入 Families.vue**:
|
|
|
+- 操作列新增按钮「查看信息」,`@click="handleViewOverview(row)"`
|
|
|
+- 操作列宽 140px → 200px(容纳新按钮)
|
|
|
+- `handleViewOverview(row)` → `this.overviewDialogVisible = true; this.overviewFamilyId = row.id`
|
|
|
+- 引入 `FamilyOverviewDialog` 并挂到模板末尾
|
|
|
+
|
|
|
+**注意**:现有编辑弹窗可手工改家庭名 → 与 G2 自动命名可能互相覆盖(见 D8,已接受)。
|
|
|
+
|
|
|
+## 6. 测试与验证
|
|
|
+
|
|
|
+### 单元测试(后端,JUnit + Mockito,随项目既有测试约定)
|
|
|
+| 测试 | 断言 |
|
|
|
+|---|---|
|
|
|
+| `FamilyAdminOverviewServiceTest` | 计数与明细条数正确;报告/方案全量返回;任务不在 overview 内 |
|
|
|
+| `FamilyNamingTest` | `buildFamilyName`:有 realName → 「张三家庭」;realName 空 → 回退 nickname 规则;realName 全空格 → 回退 |
|
|
|
+| `AdminFamilyOverviewControllerTest` | 非 admin 角色 → 拒绝;家庭不存在 → 错误提示 |
|
|
|
+
|
|
|
+### 前端验证(手工,vue-cli dev)
|
|
|
+- 家庭列表页点「查看信息」→ 弹窗显示计数卡片 → 切换三个 tab → 任务 tab 翻页正常
|
|
|
+- 真实姓名已填写的家庭(如 creator real_name 非空)→ 列表「家庭名称」列显示「XX家庭」
|
|
|
+
|
|
|
+### 部署验证(按 AGENTS.md 部署流程)
|
|
|
+1. `git status` 干净 → `git fetch && git pull --rebase origin cfclub` → 提交推送
|
|
|
+2. 备份 `/app/pro/cfc`(tar)**+ 备份 `families` 表受影响行**(mysqldump)
|
|
|
+3. 后端 `mvn clean package -Dmaven.test.skip=true` → 复制 jar → `systemctl restart cfc-backend`
|
|
|
+4. 前端 `npm run build` → `rsync -a --delete dist/ /app/pro/cfc/frontend/`
|
|
|
+5. 后端健康:`systemctl status cfc-backend` + `curl POST /api/cart/list` 预期 200
|
|
|
+6. 日志检查:新 PID 无 ERROR
|
|
|
+7. 存量回填:**dry-run 先报行数给主理人**,确认后再执行
|
|
|
+8. 管理端页面冒烟:`/admin/` 返回 200,家庭列表正常加载
|
|
|
+
|
|
|
+## 7. 风险
|
|
|
+
|
|
|
+| 风险 | 缓解 |
|
|
|
+|---|---|
|
|
|
+| 存量回填覆盖管理员手工命名的家庭 | 主理人已接受(D8);dry-run 先报数量 |
|
|
|
+| overview 响应体过大 | 任务明细走分页接口(D3);报告/方案已确认上限 8/4 |
|
|
|
+| 任务明细接口被高频调用 | 默认 20/页、上限 100;管理端内部使用 |
|
|
|
+| `FamilyMemberService` 单文件已很大(1400+ 行),继续加方法 | 命名方法抽到独立 `FamilyNamingSupport` 静态类,便于单测 |
|
|
|
+| 前端操作列变宽挤压 | 操作列 140px → 200px,列表本就横向可滚动 |
|
|
|
+| 用户更新资料触发家庭名变更,可能与管理员改名冲突 | 接受(D8),行为在文档中明确 |
|
|
|
+
|
|
|
+## 8. 交付物
|
|
|
+
|
|
|
+- 后端:`FamilyAdminOverviewService`、`AdminFamilyOverviewController`、`FamilyOverviewDTO` + 3 个 VO、`FamilyNamingSupport`、`UserService`/`FamilyMemberService` 联动改动、`DatabaseInitializer` 新迁移
|
|
|
+- 后端测试:3 个测试类
|
|
|
+- 前端:`api/admin.js` 2 个新方法、`FamilyOverviewDialog.vue`、`Families.vue` 接入
|
|
|
+- 文档:本设计文档(`docs/superpowers/specs/2026-10-30-family-overview-naming-design.md`)
|
|
|
+
|
|
|
+## 9. 留给实现者的问题(已全部解答)
|
|
|
+
|
|
|
+- 命名规则优先级 → D6(real_name 优先,回退 nickname)
|
|
|
+- 触发时机 → D7(三处:创建 / 更新资料联动 / 存量迁移)
|
|
|
+- 明细是否分页 → D3(任务分页,报告方案全量)
|
|
|
+- 报告是否过滤 status → D4(全量返回,展示 status 列)
|
|
|
+- 方案是否返回大字段 → D5(不返回 plan_content/plan_json)
|
|
|
+- 存量回填如何执行 → 第 6 节 dry-run → 确认 → 执行
|