Просмотр исходного кода

docs: 设计文档补充实现核验结论与边界决策

Xiaogang Liao 1 неделя назад
Родитель
Сommit
f64fc6ea74
1 измененных файлов с 61 добавлено и 3 удалено
  1. 61 3
      docs/superpowers/specs/2026-10-30-family-overview-naming-design.md

+ 61 - 3
docs/superpowers/specs/2026-10-30-family-overview-naming-design.md

@@ -63,6 +63,22 @@
 - `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`
 
+### 3.1 实现核验结论(源码 + 生产库实测,非假设)
+
+| 项 | 核验结论 |
+|---|---|
+| 管理端鉴权 | `JwtInterceptor` 第 153 行 `request.setAttribute("role", role)`;`"admin".equals(role)` 校验约定**成立**。新控制器沿用 `@RequestAttribute("role") String role` |
+| MyBatis-Plus 版本 | `3.5.3.1`(`selectCount` 返回 `Long`)。**分组计数不能用 `selectCount`**(会把 GROUP BY 包进 COUNT 子查询),须用 `selectMaps + groupBy`——项目已有先例(`FileRecordService` 第 301-306 行 `selectMaps` + `QueryWrapper.groupBy`) |
+| `UserService` 依赖 | 已注入 `FamilyMapper`(第 48 行),**无循环依赖**;`updateUserInfo` **无 `@Transactional`**(家庭名联动失败不回滚用户资料——可接受,家庭名为非关键展示值) |
+| Task 逻辑删除 | `Task` 实体**无 `@TableLogic`**、表无 `deleted` 列 → 计数/分页直接 `WHERE family_id=?`,无需处理软删 |
+| `health_plans.member_name` | 单值显示名(`varchar(50)`),直接展示;`member_ids` 为逗号分隔列表,**不需要** |
+| `families.creator_id` | 有 **5 行为 NULL**(历史/系统创建)→ overview 的 `creatorName/creatorRealName` 需 **null-guard**;命名迁移 `INNER JOIN` 自然跳过 NULL(安全) |
+| 前端 StatCard | `StatCard.vue` **已支持** `clickable` prop + `@click`(点击切换 tab 直接用) |
+| 前端 StatusTag | props 含 `value` + `options:[{value,label,type}]`(**自带枚举映射**,传 `options` 即可,无需外部 if-else) |
+| Element UI 版本 | `2.15.13` → `el-descriptions`、`el-tabs`、`el-pagination` 均可用 |
+| 代码坐标 | 已核对:`createFamilyForUser` 命名逻辑在 913-915 行;`updateUserInfo` 在 1159 行起;`getFamilyMembers` 在 826 行起 |
+| 现有接口冲突 | `AdminController` **无** 现成 reports/plans 查询端点;新路径 `/families/{id}/overview`、`/families/{id}/tasks` 与现有 `/families/{id}`、`/families/{id}/members` 段不同,**无路由歧义**(Spring 允许多控制器共享 `/api/admin` 前缀,路径唯一即可) |
+
 ## 4. 决策(主理人已确认)
 
 | # | 决策点 | 结论 |
@@ -75,6 +91,8 @@
 | D6 | 命名规则来源 | 真实姓名 `users.real_name` 非空 → `name = real_name + "家庭"`;否则保持现有 `nickname + "的家庭"` / `"我的家庭"` |
 | D7 | 改名触发时机 | ① 创建家庭时按新规则命名;② 用户更新资料(`realName` 变化)时联动;③ 存量一次性回填迁移 |
 | D8 | 改名冲突 | 用户手工改过的家庭名会被覆盖——**接受**(主理人要求「家庭名称改成真实姓名的XX家庭」) |
+| D9 | realName 清空 | 用户把真实姓名**清空**时不自动回退家庭名(需求只规定「填写后→改名」,清空不触发;家庭名维持「XX家庭」)——**实现者自行决定,无需上报** |
+| D10 | 更新资料事务 | `updateUserInfo` 不加 `@Transactional`;家庭名联动失败仅记日志、不阻断用户资料保存(家庭名为非关键展示值)——**实现者自行决定** |
 
 ## 5. 设计
 
@@ -100,7 +118,7 @@ FamilyOverviewDTO
 **新增 Service**:`FamilyAdminOverviewService`(`com.etotem.cfc.service`)
 - `FamilyOverviewDTO buildOverview(Long familyId)`:
   - 校验 family 存在,否则抛 `RuntimeException("家庭不存在")`
-  - 计数用 MyBatis-Plus `selectCount`(3 次 + 2 次 groupBy)
+  - 计数:`selectCount` 返回 `Long`,字段类型用 `long`;分组计数用 **`selectMaps` + `groupBy`**(`QueryWrapper.select("report_type as k","count(*) as v").eq("family_id",fid).groupBy("report_type")`,结果映射到 `Map<String,Integer>`)
   - 报告明细:`LambdaQueryWrapper.eq(familyId).orderByDesc(report_date).orderByDesc(id)`,**全量返回**(最大 8 条)
   - 方案明细:`eq(familyId).orderByDesc(created_at).orderByDesc(id)`,**全量返回**(最大 4 条)
   - 任务明细:**不在此返回**,仅计数
@@ -122,6 +140,11 @@ FamilyOverviewDTO
 - `FamilyPlanItemVO`: `id, planType, memberName, goal, status, totalDays, teacherId, createdAt`
 - `TaskItemVO`: `id, title, status, category, points, deadline, childId, executorType, taskType, sourceType, createdAt, completedAt`
 
+**边界处理**:
+- `creatorId` 为 NULL → `creatorName/creatorRealName` 返回空串(null-guard)
+- `personName` / `memberName` / `overallScore` 可空 → 前端展示 `-`(`StatusTag` 自带 `fallback="-"`)
+- 报告 `status`(active/deleted/archived)→ 前端用 `StatusTag` 展示:active=成功绿、archived=警告、deleted=信息灰
+
 ### 5.2 后端:家庭命名「XX家庭」
 
 **新增工具方法**(放 `FamilyMemberService` 私有方法,或抽到 `FamilyNamingSupport`):
@@ -144,6 +167,7 @@ static String buildFamilyName(User user) {
 在 `userMapper.updateById(user)` 之后,若 `dto.getRealName()` 非空且与原值不同:
 - 查该用户作为 `creator_id` 的家庭(`families.creator_id = userId`)
 - 存在则 `family.name = realName.trim() + "家庭"` 并更新
+- **不加 `@Transactional`**;家庭名联动失败仅 `log.warn`,不阻断用户资料保存(D10)
 
 **触发点 3 — 存量回填**(一次性迁移,放 `DatabaseInitializer.runMigrations()` 新迁移号,幂等):
 
@@ -176,8 +200,11 @@ export function getFamilyTasks(id, params) { return request({ url: `/api/admin/f
      - 「方案」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=已取消
+- 方案状态映射复用 `HealthPlanManage.vue`:`draft=草稿 / pending_review=待审核 / published=已发布 / rejected=已驳回`;`plan_type` 映射 `health_plan=健康方案 / family_attribution_plan=家庭归因方案`
+- 任务状态映射复用 `Tasks.vue`:`pending=进行中 / pending_review=待审核 / completed=已完成`,补充 `locked=前置锁定 / assigned=待接受 / in_progress=进行中 / rejected=已拒绝 / overdue=已过期 / cancelled=已取消`(用 `StatusTag` 的 `options` 数组,`value`=任务 status)
+- **任务 tab 列**:标题 / 状态(StatusTag)/ 分类 / 积分 / 截止时间 / 来源类型 / 创建时间。`taskType`(onetime=一次性/recurring=循环)、`sourceType`(self/assigned/plan/health_plan/...)用 `StatusTag` `options` 映射,无映射值则原样显示(`fallback`)
+- **方案 tab 列**:类型(plan_type 中文)/ 成员 / 目标(goal,超长省略)/ 状态(StatusTag)/ 天数 / 创建时间
+- **报告 tab 列**:类型(reportType 中文)/ 报告编号 / 报告日期 / 姓名 / 状态(StatusTag)/ 总分 / 肠道分
 
 **接入 Families.vue**:
 - 操作列新增按钮「查看信息」,`@click="handleViewOverview(row)"`
@@ -220,6 +247,17 @@ export function getFamilyTasks(id, params) { return request({ url: `/api/admin/f
 | `FamilyMemberService` 单文件已很大(1400+ 行),继续加方法 | 命名方法抽到独立 `FamilyNamingSupport` 静态类,便于单测 |
 | 前端操作列变宽挤压 | 操作列 140px → 200px,列表本就横向可滚动 |
 | 用户更新资料触发家庭名变更,可能与管理员改名冲突 | 接受(D8),行为在文档中明确 |
+| 分组计数用 `selectCount` 会静默出错 | 强制用 `selectMaps + groupBy`(已在 3.1 核验表标注,代码评审检查项) |
+| `creator_id` 为 NULL 的 5 个家庭拿不到主账号信息 | null-guard 返回空串;命名迁移 INNER JOIN 跳过(不误改) |
+| `updateUserInfo` 无事务,家庭名联动可能部分失败 | 家庭名为非关键展示值,失败仅告警日志(D10) |
+
+## 7.1 明确排除的边界情况
+
+- 真实姓名为空/全空格 → 家庭名回退现有 nickname 规则(D6)
+- 用户清空真实姓名 → 家庭名**不自动回退**,维持「XX家庭」(D9)
+- `creator_id` 为 NULL → 跳过改名迁移,overview 主账号信息显示空
+- 报告 `person_name` 为空 → 前端显示 `-`
+- 任务 `deadline` 为 NULL(进度型任务常见)→ 前端显示 `-`
 
 ## 8. 交付物
 
@@ -236,3 +274,23 @@ export function getFamilyTasks(id, params) { return request({ url: `/api/admin/f
 - 报告是否过滤 status → D4(全量返回,展示 status 列)
 - 方案是否返回大字段 → D5(不返回 plan_content/plan_json)
 - 存量回填如何执行 → 第 6 节 dry-run → 确认 → 执行
+- 分组计数用什么 API → `selectMaps + groupBy`(3.1 核验表)
+- 计数类型 → `long`(MP 3.5.3.1 `selectCount` 返回 `Long`)
+- 更新资料是否加事务 → 不加(D10)
+- realName 清空是否回退 → 不回退(D9)
+- creatorId 为 NULL 怎么办 → null-guard + 迁移跳过(3.1 / 7.1)
+- 任务/方案 tab 显示哪些列、枚举如何中文化 → 5.3 末四条
+
+## 10. Builder 复审记录
+
+设计文档初稿经 builder 子代理复审,提出 8 项待核实:
+1. 管理端鉴权属性/角色值 → **已核实成立**(`JwtInterceptor:153`)
+2. MP 分组计数与 Long/int → **已修正设计**(`selectMaps + groupBy`,字段 `long`)
+3. `updateUserInfo` 联动可行性 → **已核实**(DTO 有 `realName`、已注入 `FamilyMapper`、无事务→D10)
+4. `creatorId` NULL / realName 清空 → **已补设计**(null-guard + D9)
+5. 前端组件 props 契约 → **已核实**(StatCard 支持 click、StatusTag 支持 options、Element UI 2.15.13)
+6. 任务/方案枚举标签缺口 → **已补 5.3 列定义与映射**
+7. 软删除与列名 → **已核实**(Task 无 `@TableLogic`、无 deleted 列;member_name 单值)
+8. 代码坐标漂移 → **已核对准确**(实现时按语义定位,不硬编码行号)
+
+结论:8 项均已闭环,无需再向上追问。