spec: PRD 差距收尾设计(8 个工作包,12 项 + 1 bug)

This commit is contained in:
2026-07-07 11:08:38 +08:00
parent 2877171f09
commit baa01a910a

View 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-07PRD 的 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. 人工匹配 APIPOST /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=06b 无凭据环境下确认账单不报错且站内通知正常推送代码路径有单测覆盖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` + 相关单测。