spec: PRD 差距收尾设计(8 个工作包,12 项 + 1 bug)
This commit is contained in:
149
docs/superpowers/specs/2026-07-07-prd-gap-closure-design.md
Normal file
149
docs/superpowers/specs/2026-07-07-prd-gap-closure-design.md
Normal file
@@ -0,0 +1,149 @@
|
||||
# PRD 差距收尾 — 设计文档
|
||||
|
||||
> 日期:2026-07-07
|
||||
> 范围:PRD v1.0 审计后剩余的全部 ⚠️ 部分实现 / ❌ 未实现项 + 1 个确认 bug,共 12 项,拆为 8 个工作包(WP)。
|
||||
> 执行方式:实现工作全部派发给 `deepseek/deepseek-v4-pro` 子代理,每个 WP 独立验收,最后统一构建验证。
|
||||
|
||||
## 背景
|
||||
|
||||
基于 6 路并行代码审计(2026-07-07),PRD 的 P0/P1 主体功能已完成。剩余缺口:
|
||||
|
||||
| 类别 | 项 |
|
||||
|---|---|
|
||||
| Bug | 今日出勤率恒为 0 |
|
||||
| ⚠️ 部分实现 | 钉钉兜底匹配、档案脱敏、多班型封面、PDF 专业课对比、面板角色差异化、organization 残留、增量同步、账单外部推送 |
|
||||
| ❌ 未实现 | 入住 enrollment 预填、入住时间线、排课单双周 |
|
||||
|
||||
## WP1 — Bug:今日出勤率恒为 0
|
||||
|
||||
**问题**:`apps/server/src/dashboard/dashboard.service.ts` `getStats()` 在 ~L92 计算了 `todayAttendanceRate`,但 L153-173 的 return 对象漏掉该字段;前端 `apps/admin/src/pages/Dashboard/index.tsx:386` 读 `stats?.todayAttendanceRate` 恒为 undefined,指标卡显示 0。
|
||||
|
||||
**修复**:return 对象补 `todayAttendanceRate`。无其他改动。
|
||||
|
||||
**验收**:GET /dashboard/stats 响应含 `todayAttendanceRate` 字段(有当日考勤数据时为非零字符串)。
|
||||
|
||||
## WP2 — 钉钉考勤兜底匹配
|
||||
|
||||
**现状**:`apps/server/src/attendance/attendance.service.ts` `autoMatchDingRecords()` 仅按姓名匹配;PRD 23.3 要求手机号、身份证号兜底。
|
||||
|
||||
**设计**:
|
||||
|
||||
1. 匹配链(顺序执行,命中即停):
|
||||
- `students.resource_user_id = raw.ding_user_id`
|
||||
- 经 users 同步表由 dingUserId 取 phone → `students.phone` 精确匹配
|
||||
- 同上取到的身份信息 → `students.id_card` 精确匹配(若钉钉侧有)
|
||||
- 姓名精确匹配(现有逻辑保留,作为最后一级)
|
||||
2. 任一级命中**多个**候选学生 → 不自动绑定,保持 `match_status=待匹配`(人工处理页兜底)。
|
||||
3. `ding_attendance_raw` 实体新增可空 `phone` 列;打卡事件落库时若能从已同步 User 拿到手机号则冗余写入,便于人工匹配页展示与排查。SQLite 开发库用 `synchronize` 自动加列;不需要手写迁移(生产 MySQL 上线时统一走 schema 同步流程)。
|
||||
4. 人工匹配 API(POST /ding-attendance-raw/:id/match)不变。
|
||||
|
||||
**验收**:构造 4 条 raw 记录(分别只能由 resourceUserId / phone / idCard / name 命中),autoMatch 后均绑定正确 student;构造重名两学生 → 该记录保持待匹配。
|
||||
|
||||
## WP3 — 学生档案三项
|
||||
|
||||
### 3a 敏感信息脱敏(PRD 1.3)
|
||||
|
||||
**现状**:`apps/admin/src/pages/Students/index.tsx` 已实现脱敏+二次确认+后端日志;`apps/admin/src/components/StudentProfileContent/index.tsx` 直接明文展示 phone/idNumber。
|
||||
|
||||
**设计**:复用 Students 页的既有模式(同一确认弹窗文案、同一后端敏感查看日志端点),StudentProfileContent 中 phone/idNumber 默认脱敏(`138****1234` / `110***********1234`),点击"查看"→ Modal.confirm → 调日志端点 → 显示明文。**不新造第二套脱敏组件**:若 Students 页逻辑是内联的,抽为共享工具/组件后两处复用。
|
||||
|
||||
### 3b 多班型封面(PRD 2.1)
|
||||
|
||||
**现状**:档案封面 enrollment 卡片只展示前 2 个。
|
||||
|
||||
**设计**:改为全部展示,卡片区 flex-wrap 布局;3+ 班型时自动换行。
|
||||
|
||||
### 3c PDF 报表专业课对比(PRD 2.2)
|
||||
|
||||
**现状**:`apps/server/src/archive/archive-report.service.ts` `buildExamDetail` 仅渲染文化课考试。
|
||||
|
||||
**设计**:镜像文化课渲染逻辑,增加专业课成绩独立表格 + 趋势区块;按 exam 记录的课程类别(文化课/专业课)分组,专业课组为空时该区块不渲染(不出空表)。
|
||||
|
||||
**验收**:3a 档案页 phone/idNumber 默认脱敏、确认后显示且 operation_logs 有记录;3b 造 3 个 enrollment 的学生封面全部可见;3c 有专业课成绩的学生报表 HTML 含专业课对比表,无专业课成绩的学生报表不含空区块。
|
||||
|
||||
## WP4 — 数据面板角色差异化(PRD 23.6)
|
||||
|
||||
**现状**:Dashboard 仅有 CampusScope 部门过滤,无角色差异化。
|
||||
|
||||
**设计**:
|
||||
|
||||
1. 后端 `getStats()`(及 class-attendance-ranking 等端点)注入当前用户:
|
||||
- 用户为班主任类角色(存在 ClassTeacher 关联且非 super_admin/staff 系管理角色)→ 考勤指标、班级出勤排行仅统计其所带班级(ClassTeacher → classId 过滤)。
|
||||
- 用户无 `bill`/`deposit` 相关权限 → return 对象**不含** `monthlyIncome`、`pendingDeposits`、`incomeTrend`、`billStats` 字段。
|
||||
2. 前端指标卡/图表按字段有无条件渲染:字段 undefined 即不渲染该卡片/图。
|
||||
3. super_admin 与 staff 系角色行为完全不变。
|
||||
4. 判定依据用现有 RBAC 权限节点(请求上下文里已有用户权限),不新增权限节点。
|
||||
|
||||
**验收**:以班主任账号请求 /dashboard/stats → 无收入/押金字段、考勤数只含本班;超管请求 → 字段齐全,与改动前一致。
|
||||
|
||||
## WP5 — organization → tenant_id 收尾(PRD 1.1)
|
||||
|
||||
**现状**:`students.tenant_id` 外键与筛选已生效,旧 `organization` 文本列共存。
|
||||
|
||||
**设计**(干净切换):
|
||||
|
||||
1. 回填逻辑放在 StudentsService(或专用 backfill service)的 `onModuleInit` 中幂等执行(项目约定:`synchronize: true` + 服务内幂等 seed,参照 `expense-types.service.ts` 的 `seedDefaults`):对 `organization` 非空且 `tenant_id` 为空的学生,按名称 find-or-create Tenant(新建的给默认颜色),回填 `tenantId`。注意:`organization` 列从 entity 删除后 TypeORM synchronize 不会自动删物理列(SQLite/MySQL 均保留孤列),回填需在删除 entity 字段前用 raw query 读取该列,保证幂等重跑安全。
|
||||
2. 迁移完成后删除 `organization`:entity 字段、DTO、导入导出模板列、前端表单/表格列全部清除。
|
||||
3. Excel 导入模板中原"机构"列改为按 Tenant 名称解析(find-or-create 同上),保持导入体验不回退。
|
||||
|
||||
**验收**:迁移后无 `organization` 引用(grep 为零,实体除外的历史注释可留);导入含机构名的 Excel 仍能正确挂 tenant;学生列表机构筛选正常。
|
||||
|
||||
## WP6 — 第三方集成两项
|
||||
|
||||
### 6a 增量同步(INT.3)
|
||||
|
||||
**约束**:钉钉/企微组织架构 API 不支持"按变更时间查询",真增量不可行。
|
||||
|
||||
**设计**:全量拉取 + 本地 diff 写入:
|
||||
- `syncAll()` 拉取后与库内记录逐条比对,仅对有实际字段变化的记录执行 update,新记录 insert,无变化 skip。
|
||||
- 同步日志(sync_log)记录 `created/updated/skipped` 三个数量;`lastSyncAt` 保留用于展示。
|
||||
- 移除 `_lastSyncAt` 假形参(要么真用于日志,要么删除),不留误导性签名。
|
||||
|
||||
### 6b 账单外部推送(10.2)
|
||||
|
||||
**设计**:
|
||||
- 账单**确认**(confirmed)时触发(draft 不推,避免打扰):对账单学生查 `resource_user_id`,钉钉侧发工作通知、企微侧发应用消息(走 integration 模块现有 token 机制,新增发消息方法)。
|
||||
- 凭据未配置或学生无 resourceUserId → 静默跳过,仅站内 SSE 通知(现状保留),不抛错不阻塞账单流程。
|
||||
- 推送结果写操作日志(module=bills, action=notify)。
|
||||
|
||||
**验收**:6a 连续两次同步,第二次日志 skipped≈全部、updated=0;6b 无凭据环境下确认账单不报错且站内通知正常,推送代码路径有单测覆盖(mock integration service 验证调用参数)。
|
||||
|
||||
## WP7 — 入住管理两项
|
||||
|
||||
### 7a enrollment 预填(8.1)
|
||||
|
||||
入住弹窗选中学生后,调现有档案/enrollment 查询接口拉取该生当前在读 enrollment,在表单内只读展示班级/班型/班主任提示信息。不改数据模型、不落库。
|
||||
|
||||
### 7b 入住历史时间线(8.3)
|
||||
|
||||
入住列表行加"历史"按钮 → Drawer 内 AntD `Timeline` 按时间倒序展示该学生全部 occupancy 记录(入住/换房/退住节点,含房间号、日期、退住原因)。数据用现有 occupancies 按 studentId 查询的 API(如无该筛选参数则补上)。
|
||||
|
||||
**验收**:7a 选择有 enrollment 的学生显示班级提示、无 enrollment 学生不报错;7b 多次入住的学生时间线节点完整有序。
|
||||
|
||||
## WP8 — 排课单双周
|
||||
|
||||
**设计**:
|
||||
|
||||
1. `class_schedule` 新增 `week_parity` 列:`'all' | 'odd' | 'even'`,默认 `'all'`;奇偶按 **ISO 8601 周数**判定。
|
||||
2. 冲突检测(`apps/server/src/schedules/schedules.service.ts`):同教室同 weekDay 时间重叠时,`odd` vs `even` 不冲突;`all` 与任何值冲突。RENTAL 类型视为 `all`。
|
||||
3. 前端排课表单加"周次"选择(默认每周);周视图格子对 odd/even 排课显示"单/双"角标;月视图按具体日期的 ISO 周奇偶过滤显示。
|
||||
4. 排课→考勤自动生成(`attendance.service.ts` generateFromSchedules):按目标日期 ISO 周奇偶跳过不匹配的排课。
|
||||
5. 班级课表(/classes/:id/schedule)响应带 weekParity 字段,前端同样标注。
|
||||
|
||||
**验收**:同教室同时段创建 odd+even 两条排课成功、再建 all 报冲突;单周排课在双周日期不生成考勤记录;周视图角标正确。
|
||||
|
||||
## 横切约束
|
||||
|
||||
- 所有新增写操作接入 OperationLogsService(沿现有各 controller 模式)。
|
||||
- 不引入新依赖;ISO 周数计算用 dayjs 现有插件或自实现纯函数。
|
||||
- 各 WP 不跑项目级 lint/test/build——由主控最后统一验证。
|
||||
- 前端遵循现有页面结构与 AntD 6 组件用法;后端遵循 entity/dto/service/controller 模块结构。
|
||||
|
||||
## 工作包依赖
|
||||
|
||||
全部 WP 相互独立、可并行。WP8 内部排课字段与考勤生成改动同包完成,无跨包依赖。
|
||||
|
||||
## 测试策略
|
||||
|
||||
- WP2、WP6b、WP8 冲突检测:Jest 单测(新增/修改的 service 方法)。
|
||||
- 其余 WP:以 API 响应/页面行为验收,主控统一 `turbo build` + 相关单测。
|
||||
Reference in New Issue
Block a user