业务可用性测试 Issue「超百天自动加急覆盖回访计划」:
[被测者]「超过百天他就会给你加急自动升了一个紧急,而且他设置过的回访的话他实际是不记得」
## 先纠两个事实
1. 阈值是 **90 天**不是 100 天(urgency_level:>90 紧急 / 30-90 高 / <30 中),
而且它不只是标签 —— 急迫性 ×0.4 是综合分权重最大的一块。
2. 「覆盖了回访计划」不准确:PAC **从没看过** task_date,不是覆盖是没接。
(回访表本身早就进召回了 —— gapWhere 里的外院已治疗闸读 rv.result;
只是一直没用 task_date 这一列。)
## 口径:排到哪天挡到哪天
AND NOT EXISTS (SELECT 1 FROM patient_return_visits rv
WHERE rv.patient_id = p.id AND rv.task_date > now()::date)
跟 ⑤b(未来预约)同一个道理,注释原话就够用:「召回目的 = 让客服建预约。
患者已经有未来预约 → 客服不需要再 push」。回访是同构的,只是接触形式换成电话/微信。
三个设计选择,都是被生产数据推翻假设之后定的:
· **不设窗口上限。** 原本想拍 N=60 天,查完发现理由不成立:在池患者的未来排程
99.65% 在一年内(2026 年 25,105 / 2027 年 5,271 / 2028 年以后共 50 条,最远 2033-11-12)。
固定窗口反而会在第 N+1 天把"诊所排了 N+30 天回访"的人放回池,制造重复触达。
那 50 条脏数据不值得引一个新常量 —— 真要防该在摄入侧校 task_date 范围。
· **不为"排了不做"加兜底。** 全库到期任务执行率 74.3%(118.1 万 / 159.1 万),
确实有 1/4 烂尾。但闸的条件是 task_date > today,任务日一过闸自动失效、患者当天回池 ——
SQL 每轮重算就是自愈机制,不需要解除逻辑。
· **放 scenario 不放 gapWhere。** 这是本次唯一的架构判断:
gapWhere(召回 + 画像共享)= 【临床事实】层 —— 已治疗/患者拒绝/外院已做 → 缺口真没了
scenario WHERE(仅召回) = 【运营时机】层 —— 缺口还在,只是现在不该打电话(⑤b/⑤f)
未来回访显然是后者:排了个电话不代表那颗缺牙补上了。放进 gapWhere 会让画像因为
"排了个回访"就说这人没有潜在治疗 —— 那才是口径分叉。
## 生产影响(上线前实测)
在池 plan 236,508
⑤g 将挡下 30,426 (12.9%)
其中 urgent 17,263 ← 正是一线抱怨的"明明排了回访还催我"
未来排程到期分布 ≤30天 8,724 / 31-90天 8,502 / 91-180天 9,407 / >180天 3,793
被挡住的 plan 会被 supersede(0 命中 → closeStaleActivePlan),到期是新版本 —— 已确认
可接受:数据都有记录,跟踪得到;分配逻辑将来单独做。contactAttempts 归零暂不处理。
## 顺带修一处过期注释
plan-engine.service.ts:70 还写着「关闭 active(非 assigned)plan / assigned 不动」,
但代码从批量路径修复那次起就已含 assigned(:145 条件是 active OR assigned,
:261 注释也写明是有意改的)。注释改成与代码一致。
## 本地验证
- 477 单测通过(新增 8 例:闸形状 / 只认未到期 / 患者级 / 无窗口上限 /
⑤b⑤f 未被替换 / gapWhere 不含未来 task_date / gapWhere 仍保留外院闸 / 画像 SQL 不含本闸)
测试用递归摊平 Prisma.Sql 的方式断言 —— 嵌套片段在 tagged template 里只是绑定参数,
不摊开断言会假通过(第一版就踩了这个,gapWhere 内容根本没被检查到)。
- service / web tsc 干净
- 真库跑引擎:
吕学文 1921711(有 2026-10-09 未来回访)→ plansClosed=1,plan 转 superseded 出池
李石明 1873810(无未来回访) → 不受影响,仍 active
把 2026-10-09 改成昨天再跑 → plansCreated=1,v2 active 回池(自愈方向验证),随后还原
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>