Commit 89e68c1a by luoqi

W4: scenario SQL 排除升级牙位级 overlap(EMR.treat_plan 带 48.7% 牙位)

之前 ⑤a 排除是 patient + category 级("DW 限制" 注释);
EMR.treat_plan 进来后 actual 带牙位,可以升级到 tooth-level overlap。

逻辑(3 路 OR):
  ① 信号无牙位(K05 全口诊断)→ 仍 patient/category 级
  ② actual 无牙位(全口洁治/牙周治疗)→ 视为"全口覆盖"→ 仍排除
  ③ 双方都有牙位 → tooth array overlap(PG && 操作符)

实现细节:
- regexp_replace 把"15 B;24 B"非数字非分号字符替换为 ; → "15;24"
- string_to_array + array_remove '' 去空元素(关键:两 array 都有 '' 时 '' = '' 误返 true)
- PG && 任一元素相同即 overlap

收益例子:
- 罗国标 K04 14;15 + actual endodontic 36 → tooth overlap false → 正确召回 14;15
- 之前 patient/category 级会被误排除

向下兼容:48.7% actual 带牙位 → 这部分走 tooth-level 精筛;
其余 52% actual 无牙位(全口治疗)→ 走 ② 分支,跟现状一致(全口覆盖)。

不需要重导(SQL 改动,数据不动);下次 recompute-plans 立刻生效。

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
parent 194778d9
......@@ -252,15 +252,19 @@ export class TreatmentInitiationRecallScenario implements PlanScenarioPlugin {
// ║ │ 注:旧版还有上界(730天),W3 末已废 — 缺口不会自愈,scorer 自然衰减│ ║
// ║ └────────────────────────────────────────────────────────────────────┘ ║
// ║ ║
// ║ ┌─ ⑤ 排除闸:NOT EXISTS 同类 actual 治疗 ───────────────────────┐ ║
// ║ ┌─ ⑤a 排除闸:NOT EXISTS 同类 actual 治疗(牙位级 W4 末升级)─┐ ║
// ║ │ tx.type = 'treatment_record' AND tx.kind = 'actual' │ ║
// ║ │ tx.status IN ('active','fulfilled') (完成的 actual 是 fulfilled)│ ║
// ║ │ tx.content->>'category' = ANY(excludeCats) │ ║
// ║ │ excludeCats 来自 DxMap.K08.categories = ['implant','prostho']│ ║
// ║ │ tx.occurred_at >= sig.occurred_at ⭐ 关键:时间方向 │ ║
// ║ │ tx.occurred_at >= sig.occurred_at ⭐ 时间方向 │ ║
// ║ │ 只算"诊断之后才启动"的治疗,历史旧治疗(可能是另一颗牙)不算 │ ║
// ║ │ → 任一同类 actual 存在即排除该 patient │ ║
// ║ │ ⚠️ patient 级排除(不是牙位级)— 因结算表无牙位列,DW 限制 │ ║
// ║ │ │ ║
// ║ │ W4 末升级:**牙位级 overlap**(EMR.treat_plan 带 48.7% 牙位) │ ║
// ║ │ 信号有牙位 + actual 有牙位 → tooth array overlap 判排除 │ ║
// ║ │ 信号无牙位(K05 全口诊断)→ 不算 tooth,patient/category 级 │ ║
// ║ │ actual 无牙位(全口洁治)→ 视为"全口覆盖"→ 仍排除 category 全位 │ ║
// ║ │ ⇒ 罗国标 K04 14;15 之前误排(actual endo 在 36),现在正确召回 │ ║
// ║ └────────────────────────────────────────────────────────────────────┘ ║
// ║ ║
// ║ COALESCE(occurred_at, planned_for): ║
......@@ -304,6 +308,27 @@ export class TreatmentInitiationRecallScenario implements PlanScenarioPlugin {
AND tx.status IN ('active', 'fulfilled') -- actual 完成 status=fulfilled
AND tx.content->>'category' = ANY(${excludeCats}::text[])
AND tx.occurred_at >= COALESCE(sig.occurred_at, sig.planned_for) -- ⭐ 时间方向:诊断之后
-- W4 末升级:牙位级 overlap(详见上面 ⑤a 注释 box)
AND (
-- 信号无牙位(全口诊断如 K05)→ patient/category 级排除,跟现状一致
COALESCE(NULLIF(trim(sig.content->>'tooth_position'), ''), '') = ''
-- actual 无牙位(全口洁治/牙周治疗)→ 视为"全口覆盖"→ 排除该 category 所有信号
OR COALESCE(NULLIF(trim(tx.content->>'tooth_position'), ''), '') = ''
-- 双方都有牙位 → tooth array overlap
-- 牙位字符串形如 "15;24;26" / "15 B;24 B"(B/L/M 等牙面后缀)
-- 1. regexp_replace 把非数字非分号字符(空格/B/L/M 牙面)替换成 ; → "15;24;26;"
-- 2. string_to_array(...,';') → 含空字符串元素 {"15","24","26",""}
-- 3. array_remove(...,'') 去掉空元素 → {"15","24","26"}
-- 4. && PG array overlap 操作符,任一元素相同即 true
-- ⚠️ 必须 array_remove '' — 否则两个 array 都含空字符串时 '' = '' 误返 true
OR array_remove(
string_to_array(regexp_replace(sig.content->>'tooth_position', '[^0-9;]+', ';', 'g'), ';'),
''
) && array_remove(
string_to_array(regexp_replace(tx.content->>'tooth_position', '[^0-9;]+', ';', 'g'), ';'),
''
)
)
)
AND NOT EXISTS ( -- ⑤b 排除:患者已有未来预约(W3 末放宽)
-- 召回目的 = 让客服建预约。患者已经有未来预约 → 客服不需要再 push,医生到诊现场处理即可
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment