touyun-elderly-care/docs/superpowers/plans/2026-10-15-服务机构端-C4质控闸门.md
liuyun 7c26bdbd7a docs: 服务机构端 C4 质控闸门实施计划
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-10-15 01:20:34 +08:00

219 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 服务机构端 C4 质控闸门 实施计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在 C3 骨架上落地「质控闸门」:qc 表 + 补正表、全量质控(定时任务)、放行/退回(4001 自审自放行)、五类主动推送聚合、异常联动改计划、补正、admin 质控中心页。
**Architecture:** 纯新增。后端新增 qc 模块(依赖 record/abnormal/plan/order/elder),前端质控中心页。复用 C3 的 record(submitted)、abnormal、C2 的 plan.change、C1 的 elder。
**Tech Stack:** NestJS 12 + Prisma 6 + MySQL 8(utf8mb4_general_ci)、Vue 3 + Vite + Pinia + UnoCSS。
**Spec:** `openspec/changes/svc-org-c4-qc/`(proposal + specs + design + tasks);设计背景 `docs/superpowers/specs/2026-10-15-服务机构端-C4质控闸门-design.md`。
## Global Constraints
- 机构=租户:业务表带 `tenant_code`、UUID v7、`@map` snake_case;新表 `utf8mb4_general_ci`。
- 表命名:`qc`(无前缀,质控记录)、`ho_record_correction`(补正属留痕域 ho_)。
- 不动脚手架文件;业务代码落 `src/qc/`、`src/pages/ltc/qc/`。
- 放行前自检按序:四要素(1004) → 必拍(3001) → 幂等 → 自审自放行(4001)。
- 五类推送 = pending-list 跨表聚合(非 qc 落库);每类带消除条件。
- 补正 = 新增说明不改原记录;不可补正签到时间/定位/照片;≥3 次进质控重点。
- 权限码 qc.*;错误码域 46;规则号 1004/3001/4001 写 message。
- ⚠️ 迁移链损坏:`migrate dev` 不可用,用 `migrate diff --from-schema-datasource` + 手工迁移目录 + `migrate deploy`。
## Review Focus
1. **自审自放行**:`record.created_by === ctx.userId`(放行人=录入人)→ 4001 拒绝(不因角色并集放宽)。
2. **放行自检顺序**:四要素 → 必拍 → 幂等 → 自审,任一不过不写 pass_time。
3. **补正不改原值**:correct 只写 correction + 生成 recheck qc,不更新 record 的签到时间/定位/照片/项目。
4. **五类推送消除条件**:每类处理完后从 pending-list 消失(否则列表只长不消)。
5. **定时任务跨租户**:@Cron 无请求上下文,用 `prisma.raw`(无租户注入)跨租户扫描,否则只扫到单租户。
---
### Task 1: Prisma 模型 + 迁移
**Files:**
- Modify: `sources/ec-nest/prisma/schema.prisma`
- Modify: `sources/ec-nest/src/prisma/prisma.service.ts`
**Interfaces:**
- Produces: `Qc`/`RecordCorrection` 模型。
- [ ] **Step 1: 追加 2 个模型**
```prisma
/// 质控记录
model Qc {
id String @id @default(uuid(7)) @db.VarChar(36)
tenantCode String @map("tenant_code") @db.VarChar(64)
recordId String @map("record_id") @db.VarChar(36)
qcType String @default("normal") @map("qc_type") @db.VarChar(20)
qcResult String @default("pending") @map("qc_result") @db.VarChar(20)
qcNote String? @map("qc_note") @db.VarChar(200)
qcStaffId String? @map("qc_staff_id") @db.VarChar(36)
qcTime DateTime? @map("qc_time")
passTime DateTime? @map("pass_time")
firstQcId String? @map("first_qc_id") @db.VarChar(36)
createdAt DateTime @default(now()) @map("created_at")
createdBy String? @map("created_by") @db.VarChar(36)
updatedAt DateTime @updatedAt @map("updated_at")
deletedAt DateTime? @map("deleted_at")
@@index([tenantCode, recordId])
@@index([tenantCode, qcResult])
@@map("qc")
}
/// 补正说明(留痕域 ho_)
model RecordCorrection {
id String @id @default(uuid(7)) @db.VarChar(36)
tenantCode String @map("tenant_code") @db.VarChar(64)
recordId String @map("record_id") @db.VarChar(36)
qcId String? @map("qc_id") @db.VarChar(36)
correctionNote String @map("correction_note") @db.VarChar(200)
correctedBy String @map("corrected_by") @db.VarChar(36)
correctedAt DateTime @default(now()) @map("corrected_at")
@@index([recordId])
@@map("ho_record_correction")
}
```
- [ ] **Step 2: SOFT_DELETE 加 `Qc`**(prisma.service.ts 第 8 行集合加 `'Qc'`;`RecordCorrection` 子表不入)
- [ ] **Step 3: 迁移(utf8mb4_general_ci)**
Run: `cd sources/ec-nest && pnpm exec prisma migrate diff --from-schema-datasource prisma/schema.prisma --to-schema-datamodel prisma/schema.prisma --script > /tmp/c4.sql`;手工落盘迁移目录 `20261015xxxxxx_c4_qc/migration.sql`,`utf8mb4_unicode_ci` 全改 `utf8mb4_general_ci`;`pnpm exec prisma migrate deploy && pnpm exec prisma generate`。
- [ ] **Step 4: Commit**
```bash
git -C sources/ec-nest add prisma/schema.prisma prisma/migrations src/prisma/prisma.service.ts
git -C sources/ec-nest commit -m "feat(c4): qc + record_correction 数据模型"
```
---
### Task 2: 权限码 + 错误码 + APIS
**Files:** `permissions.ts`、`error-codes.ts`、`tenant-template.ts`(APIS)
- [ ] **Step 1:** permissions.ts 追加:
```ts
qc: {
pendingList: 'qc:pending-list', detail: 'qc:detail', pass: 'qc:pass', return: 'qc:return',
exceptionLink: 'qc:exception-link', correct: 'qc:correct', batchPass: 'qc:batch-pass',
},
```
- [ ] **Step 2:** error-codes.ts 补域 46:
```ts
// 服务机构端·质控(46)
QC_NOT_FOUND: 46001,
QC_ALREADY_HANDLED: 46002,
QC_SELF_REVIEW: 46003, // 自审自放行(4001)
QC_ELEMENT_MISSING: 46004, // 缺要素(1004)
QC_NOTE_REQUIRED: 46005,
QC_BATCH_UNSAFE: 46006,
QC_RECORD_NOT_SUBMITTED: 46007,
```
- [ ] **Step 3:** APIS 登记(parent 'qc'):
```ts
{ code: Permissions.qc.pendingList, name: '待质控列表', method: 'GET', path: '/api/v1/qc/pending/list', parent: 'qc' },
{ code: Permissions.qc.detail, name: '质控详情', method: 'GET', path: '/api/v1/qc/:id', parent: 'qc' },
{ code: Permissions.qc.pass, name: '放行', method: 'POST', path: '/api/v1/qc/:id/pass', parent: 'qc' },
{ code: Permissions.qc.return, name: '退回', method: 'POST', path: '/api/v1/qc/:id/return', parent: 'qc' },
{ code: Permissions.qc.exceptionLink, name: '异常联动改计划', method: 'POST', path: '/api/v1/qc/exception/link', parent: 'qc' },
{ code: Permissions.qc.correct, name: '补正', method: 'POST', path: '/api/v1/qc/correct', parent: 'qc' },
{ code: Permissions.qc.batchPass, name: '批量放行', method: 'POST', path: '/api/v1/qc/batch-pass', parent: 'qc' },
```
- [ ] **Step 4:** tsc + Commit
```bash
git -C sources/ec-nest add src/common/permissions.ts src/common/error-codes.ts src/common/tenant-template.ts
git -C sources/ec-nest commit -m "feat(c4): qc 权限码/错误码/APIS"
```
---
### Task 3: qc 模块
**Files:** 新建 `src/qc/{qc.module,qc.controller,qc.service,dto,qc.service.spec}.ts`;注册 module(本任务直接改 app.module.ts,本 change 单任务串行)。
**Interfaces:**
- Consumes: `PrismaService`(client + raw)、`getRequestContext()`、`Permissions.qc.*`、`ErrorCodes.QC_*`、record/abnormal/plan/order/elder 模型。
- Produces: `QcService`(generatePending/pendingList/detail/pass/return/exceptionLink/correct/batchPass + @Cron 定时任务)。
- [ ] **Step 1: 写失败测试**(qc.service.spec.ts):定时任务生成 pending qc;放行自检四步(1004/3001/幂等/4001);退回 qc_note 必填;补正不改原值 + 生成 recheck;批量放行排除异常记录;五类推送消除条件。
- [ ] **Step 2: 跑测试确认失败**
- [ ] **Step 3: 实现 qc.service**
关键逻辑:
- **定时任务**:`@Cron('0 * * * *')` `generatePendingQc()`——用 `prisma.raw` 跨租户扫描 `record_status='submitted'` 且无 qc 的 record → 为每条 create qc(tenantCode 取 record.tenantCode、qcType='normal'、qcResult='pending')。
- **pendingList(q)**:聚合五类(跨表查询):
1. 异常待处理:`recordAbnormal.count({ handleStatus:'open' })`(消除条件=已处理)。
2. 逾期未做:order `timeSlotEnd < now-24h` 且无 record 且 orderStatus 非 cancelled/not_met/done。
3. 住院仍有派单:elder `treatStatus='hosp_pause'` 且有 orderStatus ∈ {to_assign, assigned, serving} 的 order。
4. 计划未制定:elder `benefit_start ≤ now-5工作日` 且无 plan_status=active 的 plan。
5. 留痕待放行:`qc.count({ qcResult:'pending' })` + 待放行队列(pending qc 列表,含 record 摘要 + 已等待时长)。
- **pass(id)**:`requireQc` → `qcResult !== 'pending'` → QC_ALREADY_HANDLED(幂等);`record.findUnique` → ① 四要素(checkin_time/checkout_time/duration_min/assignee_id 任一缺 → QC_ELEMENT_MISSING 1004)→ ② 必拍已拍(遍历 record_item photoRequired && done && !isRefusedPhoto 无 photo → 3001,复用 C3 口径)→ ④ `record.createdBy === ctx.userId` → QC_SELF_REVIEW 4001;通过 → qc.update({ qcResult:'passed', qcStaffId, qcTime:now, passTime:now })。
- **return(id, dto)**:`dto.qcNote` 空 → QC_NOTE_REQUIRED;qc.update({ qcResult:'returned', qcNote, qcStaffId, qcTime:now })。
- **detail(id)**:qc + 关联 record(全文/项目/照片/异常/定位)+ 补正历史(correction 列表)。
- **exceptionLink(dto)**:`abnormal.findUnique` → 复用 `planService.change`(传 elderId + changeFromPlanId + items 含加项)生成新 plan → `abnormal.update({ linkPlanChange:true })` → 返回新 plan。
- **correct(dto)**:`record.findUnique`(record_status 须 submitted)→ `correction.create({ recordId, qcId, correctionNote, correctedBy: ctx.userId })` → 生成 recheck qc:`qc.findFirst({ recordId, qcResult:'returned', orderBy 最新 })` → `qc.create({ recordId, qcType:'recheck', qcResult:'pending', firstQcId: 首次qcId })`。(不改 record 任何证据字段。)
- **batchPass(dto)**:对每条 record 校验「有 abnormal 关联 / 有 exec_result∈{undo_not_done,undo_refused} / 有 extra_flag / duration<5min / 有 correction」→ 命中任一则 QC_BATCH_UNSAFE;否则逐条 pass。
- [ ] **Step 4: 实现 controller + dto + module**(`@Controller('qc')`;写操作 `@AuditLog`;`@Cron` 在 service;注册 module 到 app.module.ts)。**路由序**:静态路径(`pending/list`、`correct`、`batch-pass`、`exception/link`)必须声明在 `GET /:id` 之前(否则 `:id` 吞掉静态段)。
- [ ] **Step 5: 测试通过 + tsc + Commit**
```bash
git -C sources/ec-nest add src/qc src/app.module.ts
git -C sources/ec-nest commit -m "feat(c4): 质控模块(全量质控/放行退回/五类推送/异常联动/补正)"
```
---
### Task 4: 前端
**Files:** `src/api/qc.ts`;`src/pages/ltc/qc/index.vue`(替换占位)。
- [ ] **Step 1: api 封装**(`http` + 类型对齐后端 DTO;pendingList/detail/pass/return/exceptionLink/correct/batchPass)。
- [ ] **Step 2: 质控中心页**(`defineOptions({name:'qc'})`):三区:
- 待我处理(五类推送聚合:每类计数 + 点击跳对应队列)。
- 待放行队列(record 列表 → 逐条放行/退回,退回选原因下拉;批量放行按钮,后端拦不可批量项)。
- 异常处理台(abnormal 列表 → 处理/转派/关闭 + 处理意见 + 「联动改计划」入口)。
- [ ] **Step 3: type-check + Commit**
```bash
git -C sources/ec-admin add src/api/qc.ts src/pages/ltc/qc
git -C sources/ec-admin commit -m "feat(c4): 质控中心页"
```
---
### Task 5: 端到端验证
- [ ] **Step 1:** 起后端 + 登录,冒烟:submitted 留痕 → 定时任务(或手动触发)生成 qc → 放行(4001 自审拒/通过)→ 退回 → 补正 → recheck → 放行。
- [ ] **Step 2:** 五类推送:异常/逾期/住院/计划/待放行各出现与消除。
- [ ] **Step 3:** 异常联动改计划生成新 plan + link_plan_change;审计落库。停后端清理。
---
## Self-Review Notes
- Spec 覆盖:data-model 2 实体→T1;api-contract→T3;backend/qc→T3;frontend/ad/qc→T4。
- Review Focus 5 条均有测试(T3)。
- 类型一致:`Qc`/`RecordCorrection` 模型名、`Permissions.qc.*`、`ErrorCodes.QC_*` 在 T1/T2/T3 一致。
- 定时任务用 `prisma.raw` 跨租户(Review Focus #5)。