-
feat(plan): 召回分配 P0 地基 —— 权限/退回原因/引擎记账/写路径边界 · 3cbb3899
分配功能开工前的前置修复。这一刀**不含任何新功能**,但把四个 「不修就会静默出错」的洞补上 —— 分配一上线它们会同时被放大。 ## P0.1 新增 PLAN_DISPATCH 权限(主管判据) PLAN_ASSIGN 连 staff 都有(自助认领语义),STATS_VIEW 是零端点零组件的 死权限 —— 现有权限没一条能区分主管,新立一条。只授 leader + admin。
⚠ ️ 顺带修了 R7,且**原方案不够**:规划只说前端 auth-store 补 merge permissions,但 GET /auth/session 是把 JWT 里那份快照原样回传的, merge 了也还是旧清单。真正的口径是 —— **ROLE_PERMISSIONS 是真理源, JWT 只是缓存**,三处统一改成按 role 现算: · PermissionsGuard (后端判定) · GET /auth/session (前端拿新权限) · McpAuthService (工具条件注册,少一个工具模型只会说"我没这能力") 不改的话,发版当天所有已登录的 leader 在 token 到期前都是 staff 待遇, 不报错不告警。已用伪造的「发版前 token」实测:JWT 里没有 plan:dispatch, session 仍返回 true。 ## P0.2 ReleaseReason(8 值)+ PlanEventReason 退回原因结构化,每个值绑一根**主管可调的杠杆**(lever)—— 否则原因分布只是一张好看的饼图。⛔ 类型里**不给 suppressDays 字段**:照抄放弃原因那套抑制窗会把被退回的 患者静默压 30~90 天,池子里凭空少一批人。用类型系统拦住,再加运行时断言。⛔ 不复用 RECALL_FEEDBACK_OPTIONS 的 bad_timing:同名不同义是统计事故的 标准配方(那个指"召回时机不对",这里会被读成"时效太紧")。 PlanEventReason 给 plan_event_logs.reason 列做登记 —— 该列即将同时承载 系统原因与 8 个退回原因,不登记就是第二个「随手写字符串」的地方。 ## P0.5 引擎丢归属补记账 —— 实测是 4 处,规划里漏了最大的那处 规划列的是单刷路径 closeStaleActivePlan,但**每日全量跑的批量收尾** (runAllForHost 的 updateMany)才是量级最大的:它同样会关掉 assigned 的单, 同样零记账。补完四处: · unchanged 分支的诊所重归属 → reason=clinic_moved · 升版本 clinicMoved 不继承归属 → reason=clinic_moved(planId 记**旧版本**) · closeStaleActivePlan(单刷) → reason=signals_cleared · runAllForHost 批量收尾(全量) → reason=signals_cleared⚠ ️ 判定必须在事务**之前**定好:supersede 那句 update 之后 latest.status 已经是 superseded,进了事务再读条件当场失效(内存 mock 暴露了这个别名陷阱, 真 Prisma 返回脱离副本看不出来)。⚠ ️ 批量路径顺带修了一个既有隐患:原来是一条 `id: { in: staleIds }`, PG bind 变量上限 32767,池子上三万条就直接报错。改成 1000 一片、每片自成事务。⚠ ️ 无人认领的关闭**一条事件都不写**,否则每日全量会造事件洪峰; 真有洪峰时打一行 warn 说明"这不是 bug"。 ## P0.6 recycle 收退回原因 —— T7 此前根本落不了地 controller 写的是 `@Body() _dto`,下划线,收了就扔。客服填了等于没填。 链路三处打通(schema → controller → service),原因落 plan_event_logs.reason、 说明落 details。other 不带说明**服务端**拒绝(不能只信前端)。⛔ 全程不动 snoozedUntil,并在 update 处写死注释 + 单测钉住。 ## P0.7 assign/recycle 补诊所硬边界(F4) findFirst 只校验 host/tenant/sourceUnit,没有 targetClinicId —— A 诊所 leader 拿到 B 诊所的 planId 就能跨诊所写入。他**看不到**那些单 (读路径的 buildListWhere 明确挡着),但写入不受挡:隔离在写路径上比读路径 弱一档。批量分配会把它从「知道 planId 才能利用」放大成「有 UI、一次几百条」。 ## P0.3 / P0.4 / F3 · 删 5 个未挂载的死文件(1,524 行),顺手改 3 处指向它们的过期注释 · MCP toolsCache 从单例改成按**能力指纹**分桶 —— 这是工具条件注册(P3.5)的 安全前置:不分桶会串号,进程重启后第一个进来的若是客服,全公司主管都拿 客服清单,反之更糟,且两种错法都不报错。企微那条合成身份路径显式走 basic 桶。 · schema.prisma 的 recycle_at 注释指向不存在的 tenant.rules_config,删掉 ## 验证 · 846 单测通过(新增 23 条断言:权限红线 / 抑制窗红线 / 四处记账 / 边界) · 本地真实数据(5,825 患者 / 2,724 plan)端到端实测: 跨诊所 assign → 10004 not found;同诊所 → ok other 缺说明 → 10001 拒;非法枚举 → 10002 拒 退回落库 event=release reason=over_capacity held=12s note 在 details plan 回到 active,snoozed_until 未被动 · 文档两处实测更正:迁移是 37 个不是 38;data/jvs-dw/users.json 存在且已入库 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>luoqi committed
| Name |
Last commit
|
Last update |
|---|---|---|
| .. | ||
| app | Loading commit data... | |
| components | Loading commit data... | |
| ds | Loading commit data... | |
| hooks | Loading commit data... | |
| lib | Loading commit data... | |
| stores | Loading commit data... | |
| instrumentation-client.ts | Loading commit data... | |
| instrumentation.ts | Loading commit data... | |
| sentry.edge.config.ts | Loading commit data... | |
| sentry.server.config.ts | Loading commit data... |