docs: 服务机构端 C4 质控闸门实施计划
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
ff968766b8
commit
7c26bdbd7a
218
docs/superpowers/plans/2026-10-15-服务机构端-C4质控闸门.md
Normal file
218
docs/superpowers/plans/2026-10-15-服务机构端-C4质控闸门.md
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
# 服务机构端 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)。
|
||||||
Loading…
x
Reference in New Issue
Block a user