Commit 2f97e81f by luoqi

fix(recall): 召回口径两条 — §E 减数位收紧 + 牙未拔除独立场景

§E 正畸减数位排除收紧:仅前磨牙(^[1-4][45])+ 纯前磨牙记录 + 对称成对
  (14;24 或 34;44),避免把普通缺牙误判为正畸减数(张学军 24 误排修正)。

牙未拔除独立场景:EXTRACTION_RECOMMENDED 跨病种拔除动作不再硬归 K01
  (毕强松动牙被误标"阻生牙未拔除")。新增 extraction_recommended 子场景
  (中性"牙未拔除"),GAP_FLAGS deferToToothDx 让位同牙编码诊断,去重避免双召。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
parent dc6bfb88
......@@ -28,6 +28,7 @@ export interface GapCfgFlags {
excludeThirdMolar?: boolean; // §E 智齿位 18/28/38/48 不召种植
excludeOrthoExtractionSites?: boolean; // §E 正畸减数位(外科拔除 + 正畸语境)折进 resolved
excludeCongenitalName?: boolean; // §E name 含"先天" → 正畸统筹,不自动召修复
deferToToothDx?: boolean; // 「建议拔除」场景专用:有同牙编码病种诊断 → 让位给该病种场景(避免双召)
}
/// §E gap 修正 flag 的单一真理源(按 primaryCode 查)。召回 SUB_SCENARIOS 与画像标签都引此。
......@@ -39,6 +40,10 @@ export const GAP_FLAGS_BY_PRIMARY: Record<string, GapCfgFlags> = {
excludeOrthoExtractionSites: true,
excludeCongenitalName: true,
},
// 「建议拔除」独立场景:有同牙编码病种诊断 → 让位给病种场景,不重复召(残根=K03、阻生=K01…)
EXTRACTION_RECOMMENDED: {
deferToToothDx: true,
},
};
const RESTORATION_INELIGIBLE_NAMES = [...RESTORATION_INELIGIBLE_DX_NAMES];
......@@ -53,9 +58,12 @@ export const GAP_PRIMARY_GROUPS: Record<string, { dxCodes: string[]; recCodes: s
K02: { dxCodes: ['K02'], recCodes: ['FILLING_RECOMMENDED'] },
K03: { dxCodes: ['K03'], recCodes: ['HARD_TISSUE_REPAIR_RECOMMENDED', 'CROWN_RECOMMENDED'] },
K06: { dxCodes: ['K06'], recCodes: ['GUM_TREATMENT_RECOMMENDED'] },
K01: { dxCodes: ['K01'], recCodes: ['EXTRACTION_RECOMMENDED'] },
K01: { dxCodes: ['K01'], recCodes: [] }, // 阻生牙只认真诊断(阻生/埋伏);「建议拔除」拆到下方独立场景
K09: { dxCodes: ['K09'], recCodes: ['JAW_CYST_REMOVAL_RECOMMENDED'] },
K00: { dxCodes: ['K00'], recCodes: ['ERUPTION_INTERVENTION_RECOMMENDED'] },
// 「建议拔除」是跨病种的治疗动作(阻生/残根/松动牙/龋坏…都可能拔),不绑任何 K 病种 → 独立场景。
// 显示「牙未拔除(医生建议)」,不冒充某病;有同牙编码诊断时让位给病种场景(deferToToothDx)。
EXTRACTION_RECOMMENDED: { dxCodes: [], recCodes: ['EXTRACTION_RECOMMENDED'] },
};
/// 牙位字符串 → 牙位数组(只剥"牙位base+空格+牙面字母"后缀,保 FDI 数字 & Palmer 乳牙字母)。
......@@ -147,21 +155,48 @@ export function buildGapCore(input: GapCoreInput): GapCorePieces {
: Prisma.sql`AND COALESCE(rfx.occurred_at, rfx.planned_for) >= COALESCE(sig.occurred_at, sig.planned_for)`;
// §E (d) 正畸减数位(仅 missing_tooth):该牙有外科拔除 + 患者有正畸语境 → 折进 resolved 减掉。
// §E (d) 正畸减数位(仅 missing_tooth):正畸为排齐做的【对称前磨牙减数】才剔 → 折进 resolved。
// 2026-06 收紧(张学军案例):旧口径"任意 actual 外科拔牙 + 患者曾有任意正畸语境"过宽,
// 把"种植/全口修复前的拔牙"误当减数位剔掉(24 缺牙不召;拔牙 2021 早于正畸诊断 2023、且拔了
// 磨牙/相邻牙,根本不是减数)。新口径要求该拔牙记录呈【正畸减数模式】:
// ① 该牙是前磨牙(4/5 号);② 该记录"纯前磨牙"(混磨牙/切牙/尖牙 = 非减数,如全口修复拔牙);
// ③ 记录含对称第一前磨牙对(14&24 或 34&44 —— 减数的临床特征是成对拔前磨牙)。
// 去掉时间窗(case-specific);用减数模式纯度判别,更通用、贴临床。
const exTeeth = toothArrSql(Prisma.sql`ex.content->>'tooth_position'`);
const orthoExtractBranch = cfgFlags.excludeOrthoExtractionSites
? Prisma.sql`
UNION
SELECT eet AS t
FROM patient_facts ex
CROSS JOIN unnest(${toothArrSql(Prisma.sql`ex.content->>'tooth_position'`)}) AS eet
CROSS JOIN unnest(${exTeeth}) AS eet
WHERE ex.patient_id = p.id
AND ex.type = 'treatment_record' AND ex.kind = 'actual' AND ex.status IN ('active','fulfilled')
AND ex.content->>'category' = 'surgical'
AND eet ~ '^[1-4][45]$' -- ① 该牙是前磨牙(4/5 号)
AND NOT EXISTS ( -- ② 该拔牙记录"纯前磨牙"(无磨牙/切牙/尖牙混入)
SELECT 1 FROM unnest(${exTeeth}) AS xt WHERE xt !~ '^[1-4][45]$'
)
AND (${exTeeth} @> ARRAY['14','24']::text[] -- ③ 含对称第一前磨牙对(上颌)
OR ${exTeeth} @> ARRAY['34','44']::text[]) -- 或(下颌)
AND EXISTS (SELECT 1 FROM patient_facts oc WHERE oc.patient_id = p.id
AND ((oc.type='diagnosis_record' AND oc.status='active' AND oc.content->>'code'='K07')
OR (oc.type='treatment_record' AND oc.content->>'category'='orthodontic')))`
: Prisma.empty;
// (e) 「建议拔除」让位:同牙有【编码病种诊断】(K00-K04/K06/K08/K09 牙位级)→ 折进 resolved,
// 该牙交给对应病种场景召回,建议拔除场景不重复召(残根→K03、阻生→K01…)。仅 deferToToothDx 启用。
// 毕强 12;31;… 是松动牙(无 code)→ 不被折进 → 由建议拔除场景正常召回。
const deferDxBranch = cfgFlags.deferToToothDx
? Prisma.sql`
UNION
SELECT ddt AS t
FROM patient_facts dxx
CROSS JOIN unnest(${toothArrSql(Prisma.sql`dxx.content->>'tooth_position'`)}) AS ddt
WHERE dxx.patient_id = p.id
AND dxx.type = 'diagnosis_record' AND dxx.status = 'active'
AND dxx.content->>'code' = ANY(ARRAY['K00','K01','K02','K03','K04','K06','K08','K09']::text[])`
: Prisma.empty;
const congenitalFrag = cfgFlags.excludeCongenitalName
? Prisma.sql`AND COALESCE(sig.content->>'name_zh','') NOT LIKE '%先天%'`
: Prisma.empty;
......@@ -264,6 +299,7 @@ export function buildGapCore(input: GapCoreInput): GapCorePieces {
AND COALESCE(rdx.occurred_at, rdx.planned_for) >= COALESCE(sig.occurred_at, sig.planned_for)
AND sig.type = 'diagnosis_record'
${orthoExtractBranch}
${deferDxBranch}
) u)`;
const lateralJoin = Prisma.sql`
......
......@@ -184,6 +184,15 @@ export class TreatmentInitiationRecallScenario implements PlanScenarioPlugin {
label: '牙发育/萌出异常未处置',
goal: '邀约评估处置(乳牙滞留/多生牙拔除 · 先天缺失修复 · 釉质发育不全美容修复 · 萌出障碍助萌)',
},
// 「建议拔除」独立场景 —— 跨病种治疗动作(阻生/残根/松动牙/龋坏…都可能拔),不绑病种。
// 旧:EXTRACTION_RECOMMENDED 硬归 K01 → 任何建议拔除都误标"阻生牙未拔除"(毕强松动牙案例)。
// 显示中性的「牙未拔除(医生建议)」;有同牙编码诊断时让位给病种场景(GAP_FLAGS deferToToothDx)。
extraction_recommended: {
base: 30,
primaryCode: 'EXTRACTION_RECOMMENDED',
label: '牙未拔除',
goal: '邀约完成医生建议的拔除(残根 / 阻生 / 无保留价值牙等),避免感染或影响邻牙及后续修复',
},
} as const;
constructor(private readonly prisma: PrismaService) {}
......
......@@ -52,7 +52,7 @@ export const PACDiagnosisCodes = {
CROWN_RECOMMENDED: { nameZh: '建议戴冠' }, // → K08 / K04 后续
FILLING_RECOMMENDED: { nameZh: '建议充填' }, // → K02 caries
SRP_RECOMMENDED: { nameZh: '建议牙周基础治疗' }, // → K05 perio
EXTRACTION_RECOMMENDED: { nameZh: '建议拔除' }, // → K01 impacted / K03 残根
EXTRACTION_RECOMMENDED: { nameZh: '建议拔除' }, // 跨病种拔除动作 → 独立 extraction_recommended 场景(不绑 K 码)
ANNUAL_REVIEW_RECOMMENDED: { nameZh: '建议年度复查' }, // → post-treatment(K05/K08 复查)
ORTHO_CONSULT_RECOMMENDED: { nameZh: '建议正畸咨询' }, // → K07 ortho
RCT_RECOMMENDED: { nameZh: '建议根管治疗' }, // → K04 endo
......@@ -753,6 +753,7 @@ export const PACScenarioSubLabels: Record<string, string> = {
'treatment_initiation_recall.impacted_tooth': '阻生牙未拔除', // K01
'treatment_initiation_recall.jaw_cyst': '颌骨囊肿未处理', // K09
'treatment_initiation_recall.development_eruption': '牙发育 / 萌出异常未处置', // K00
'treatment_initiation_recall.extraction_recommended': '牙未拔除', // 建议拔除(跨病种,不绑 K 码)
};
export function subLabelZh(
......
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