design.md 22 KB

Context

现状

  • 后端服务层:DRG 模块下现有 src.DRG.Profit(盈亏分析 020102xx)、src.DRG.Warning(020101xx)、src.DRG.QC(020103xx)、src.DRG.System(020104xx)、src.DRG.SpecialAppeal(020105xx)、src.DRG.Workbench / src.DRG.Interface(020100xx)。没有独立的报表服务类,frontend/src/pages/Report/PatientFeeDetail.tsx 实际调用的是盈亏分析的 02010209。
  • 数据现状(全部为已有表,本次不新增表):
    • BS_DRGMedInsuMedicalRecord(MR):医保结算明细,含 27 项费用汇总列 + MedfeeSumamt、DRG/DRGDesc、HISAdmID、MedcasNo、MdtrtId、PsnNo、AdmDate/DscgDate、ImageURL、FixmedinsCode、ValiFlag(日期口径仅入院/出院,不使用结算日期);
    • BS_HISPatAdm(PA):住院就诊,AdmID、PatName、PatMedicalNo、InsuType(险种类型存于本表)、InLocCode/InLocDesc、AdmDocCode/AdmDocDesc、AdmDateTime/DisDateTime;
    • BS_HISFeeDetail(FD):HIS 费用明细流水,MedinsListCodg/MedinsListDesc、EcCode/EcDesc、Pric、Cnt、DetItemFeeSumamt、FeeOcurTime、Deleted、ValidFlag;
    • BS_HISDiseInfo:诊断,AdmID(就诊关联,已确认)、DiagCode/DiagName/MainFlag/DiagSn;另有 Adm_Dr(指向 BS_HISPatAdm.ID 的对象引用,本报表不使用);无 Deleted 字段;
    • BS_HISOprnInfo:手术,AdmID(就诊关联,已确认)、OprnCode/OprnName/MainFlag/OprnSn;同样含 Adm_Dr(不使用);无 Deleted 字段;
    • BS_DRGGroupRecord(GR):分组记录,DRG/DRGDesc/BenchmarkCost/Weight/GroupStage/ActiveFlag;
    • CB_DRGDictTable:险种字典表(已有)。父记录 Parent_Dr IS NULL AND Code='INSU_TYPE' AND Status='Y',子记录 Parent_Dr = 父.ID、Code=险种编码、Name=中文名。
  • 环境约束(02010209 实测结论,必须遵守):
    1. 本环境 IRIS 不支持 WITH(CTE)(报 SQLCODE -51),不支持 ORDER BY 聚合表达式与列序号(报 SQLCODE -73);可用:COUNT(DISTINCT col)、COALESCE、EXISTS 子查询、LEFT JOIN、GROUP BY、ORDER BY <普通列>;
    2. 排序聚合结果必须在 ObjectScript 侧用下标数组(sortIdx(amount, idx) + $ORDER(...,-1))完成;
    3. 业务类为 Not ProcedureBlock,方法内需 new 隔离局部变量;
    4. 入参取值禁止 $G() / %Get("xxx") 防御写法,统一 set params = jsonObj.params.%Get(0) 后直接属性访问;
    5. .cls 文件的最后一个成员不要是方法(IDE 自动重写会吞掉结束大括号),末尾追加 Parameter;
    6. CB_MapInterface 路由注册:ClassName 直接写业务类(如 src.DRG.Profit),ServiceType='S'、SessionFlag='Y'、TokenFlag='N'。

干系人

医保办 / 运营管理(看差异、定控费策略)、信息科(部署与菜单授权)、开发(前后端实现)。

Goals / Non-Goals

Goals:

  1. 建立 src.DRG.Report 报表服务类(与 Profit 同级、号段 020106xx),作为后续所有报表的承载骨架;
  2. 提供「已完成首页分组就诊」的检索能力(普通区 + 高级区筛选);
  3. 提供 2~5 例就诊的并列费用比对:结算清单汇总列 / HIS 明细项目 / 结算类别聚合三个维度;
  4. 同时比对诊断信息、手术信息、DRG 支付标准信息;
  5. 后端输出差异标记供前端高亮,并生成中文文字比对分析报告;
  6. 兼容不同 DRG 病组之间的比对;
  7. 前端页面纳入「报表分析」菜单,符合项目既有 UI 规范(中文日期、合计行、导出Excel)。

Non-Goals:

  1. 不新增/修改任何数据库表;
  2. 不修改 src.DRG.Profit 与 020102xx 现有接口;
  3. 不引入第三方 Excel 导出库(沿用 CSV + BOM);
  4. 不做比对方案的持久化、分享与定时任务;
  5. 不涉及 DIP 付费口径;
  6. 不做费用合理性的医学判定(只做结构化差异呈现与提示性文字)。

Decisions

D1 新建 src.DRG.Report,号段 020106xx

选择:新建 src/src/DRG/Report.cls(Class src.DRG.Report Extends (%RegisteredObject, %XML.Adaptor) [ Not ProcedureBlock ]),接口号段 020106xx。

理由:020100/020101/020102/020103/020104/020105 已分别归属分组、预警、盈亏、质控、系统、特例单议,020106 未被占用;报表与盈亏分析是不同关注点(盈亏=聚合统计,报表=横向比对/明细呈现),混在一个类会让 Profit.cls(已 110KB+)继续膨胀。

备选:继续挂 020102xx → 拒绝,号段语义混乱且加剧单文件膨胀。

D2 拆分为「检索」与「比对」两个接口

选择:02010601 QueryComparableVisits(分页检索)+ 02010602 CompareDRGFee(按 admIds 一次性返回全部比对结果,不分页)。

理由:检索需要分页(结果集可能上千),比对最多 5 例、数据量可控且必须一次性拿全才能算并集与差异;拆开后前端可先浏览筛选再加入比对篮,交互与负载都更合理。

备选:单接口既检索又比对 → 拒绝,入参耦合、无法分页。

D3 「已完成首页分组」的判定口径

选择:MR 存在记录且 MR.ImageURL <> ''(首页已入库)且 EXISTS (SELECT 1 FROM BS_DRGGroupRecord g WHERE g.AdmID = MR.HISAdmID AND g.ActiveFlag='1' AND g.DRG<>'')(已有效分组)且 (MR.ValiFlag IS NULL OR MR.ValiFlag='1')。

理由:与 src.DRG.Workbench 中「病案分组」筛选口径(首页存在 + 事后分组记录存在)保持一致,且 ImageURL<>'' 是项目内既有的「首页已上传」判定方式。

D4 主诊断/主手术过滤用 EXISTS 子查询

选择:mainDiagCode / mainDiagName / mainOprnCode / mainOprnName 通过 EXISTS (SELECT 1 FROM BS_HISDiseInfo d WHERE ... AND d.MainFlag=1) 与 EXISTS (... BS_HISOprnInfo o ... AND o.MainFlag='Y') 实现。

理由:本环境 IRIS 不支持 CTE 与派生表 JOIN;EXISTS 不影响主表行数、不会造成明细放大。

备选:LEFT JOIN 诊断/手术表后过滤 → 拒绝,会在主诊断多条时放大行数(LEFT JOIN + WHERE 还会退化成内连接)。

D5 三个维度一次算完,并集与排序在 ObjectScript 侧完成

选择:CompareDRGFee 在后端依次执行 4 段查询(MR 汇总列 / FD 按项目聚合 / FD 按类别聚合 / 诊断手术支付标准),在 ObjectScript 侧用下标数组构建行并集、计算差异矩阵,并对 feeItem / feeCategory 按金额降序排序(不用 SQL ORDER BY 聚合)。

理由:SQL 侧无法表达「跨 N 例的全外连接并集」,UNION + 多例组合会产生 N 倍 SQL;并集在内存构建最直观。排序受 SQLCODE -73 限制必须放应用层。

D6 差异基准支持 first / avg,后端算、前端只着色

选择:入参 diffBase(first 缺省 / avg);后端为每个单元格计算 diffFlag / diffAmount / diffRate,前端不做任何比较计算,仅按 diffFlag 着色。

理由:差异口径变更只需改后端一次;前端不重复实现业务规则。数值相等判定采用 2 位小数、容差 0.01,避免浮点误差造成「假差异」。

D7 文字分析报告由后端生成

选择:后端 %BuildReportText 用 ObjectScript 拼装,输出 analysis.reportText: string[];前端逐段渲染 + 「复制文本」按钮。

理由:用户明确选定后端生成,保证报告口径与差异矩阵同源;文案规则变更集中在后端一处。

备选:前端拼装 → 拒绝,会造成前后端口径漂移。

D8 前端多选并列(N≤5),Tab 分区

选择:待比对列表 rowSelection(type: 'checkbox'),最多勾选 5 条;已选以「比对篮」Tag 形式展示、可移除;比对结果区用 Tabs 分「结算清单费用汇总 / HIS费用明细项目 / 结算类别聚合」三 Tab,上方固定诊断/手术/支付标准信息卡,下方分析报告卡。

理由:用户选定无固定基准的并列对比;Tab 分区避免三类粒度混在一张表造成阅读灾难。

D9 导出沿用项目既有前端 CSV 方案

选择:「导出Excel」将当前 Tab 的比对矩阵(行 + 各例列 + 差异列)转成带 BOM 的 CSV,MIME application/vnd.ms-excel;charset=utf-8,.xls 后缀;优先 window.showSaveFilePicker,非安全上下文(内网 http)回落 a.download。

理由:与 Workbench.tsx、PatientFeeDetail.tsx 完全一致,零第三方依赖;内网 http 下必然走回落,属预期行为。

D10 机构筛选沿用既有 hook 与 withHosp

选择:新建 frontend/src/api/report.ts,导出 withHospReport(沿用 profit.ts 的 withHosp 语义);页面用 useHospitalOptions() 取机构选项与默认值(非管理员锁定当前登录机构)。

理由:与 Profit 各页面保持一致。

D11 险种类型中文映射放在后端

选择:新增私有辅助 %GetInsuTypeName(code),用 CB_DRGDictTable 自关联一次性查出险种中文名,后端在 02010601 列表与 02010602 比对结果中直接输出中文 insuType(同时保留编码 insuTypeCode);查不到时回落为原编码值。险种过滤条件作用于 PA.InsuType(险种存在 BS_HISPatAdm,与 Profit.cls 的 %BuildWhere 一致)。

已验证可用的现成写法(Workbench.cls 第 216 行 / HisDataService.cls 第 1606 行):

SELECT b.Name INTO :insuTypeName
FROM CB_DRGDictTable a INNER JOIN CB_DRGDictTable b ON a.ID = b.Parent_Dr
WHERE a.Parent_Dr IS NULL AND a.Code='INSU_TYPE' AND a.Status='Y' AND b.Code=:insuType

理由:用户明确要求映射放后端;字典表已存在且项目内多处已用同一 SQL,口径统一。

备选:前端常量映射 → 拒绝,字典变更需发版前端。

补充:因险种过滤需 PA.InsuType,02010601 主 SQL 必须 INNER JOIN BS_HISPatAdm PA ON PA.AdmID = MR.HISAdmID AND PA.Deleted=0(不能只 LEFT JOIN 后过滤,会退化且语义不符)。

D12 feeItem 截断阈值 Top 500

选择:feeItem 并集行数超过 500 行时,按各例金额最大值降序截断到 500 行,并在响应 analysis 与 reportText 中追加提示「费用明细项目较多,仅展示金额 Top 500 项」。

理由:用户确认阈值;避免 5 例 × 数千条明细导致的接口超时与前端渲染卡顿。截断基于聚合后的并集行数,不基于单例明细条数。

数据库设计

本次无 DDL 变更,仅读取以下已有表字段:

表(别名) 用途 关键字段
BS_DRGMedInsuMedicalRecord (MR) 检索主表、结算清单费用汇总列、支付信息 HISAdmID、MedcasNo、PsnName、Gend、Age、AdmDate、DscgDate、IptDays、DRG、DRGDesc、MedfeeSumamt、MdtrtId、PsnNo、FixmedinsCode、ImageURL、ValiFlag + 27 项费用列(无结算日期字段,日期口径仅 AdmDate/DscgDate)
BS_HISPatAdm (PA) 科室/医生/住院日期/险种类型 AdmID、PatName、PatMedicalNo、InsuType、InLocCode、InLocDesc、AdmDocCode、AdmDocDesc、AdmDateTime、DisDateTime、Deleted
BS_HISFeeDetail (FD) 费用明细(Tab2/Tab3) AdmID、PatMedicalNo、MedinsListCodg、MedinsListDesc、EcCode、EcDesc、Pric、Cnt、DetItemFeeSumamt、FeeOcurTime、Deleted、ValidFlag
BS_HISDiseInfo 诊断比对 AdmID(就诊号,直接关联 MR.HISAdmID)、DiagCode、DiagName、MainFlag、DiagSn(无 Deleted 字段)
BS_HISOprnInfo 手术比对 AdmID(就诊号,直接关联 MR.HISAdmID)、OprnCode、OprnName、MainFlag、OprnSn(无 Deleted 字段)
BS_DRGGroupRecord (GR) 分组与支付标准 AdmID、MedcasNo、DRG、DRGDesc、BenchmarkCost、Weight、GroupStage、ActiveFlag
CB_DRGDictTable 险种编码 → 中文名 父:ID、Code='INSU_TYPE'、Parent_Dr IS NULL、Status='Y';子:Parent_Dr、Code(险种编码)、Name(中文名)

关联键:

  • MR.HISAdmID = PA.AdmID
  • FD.AdmID = MR.HISAdmID
  • GR.AdmID = MR.HISAdmID
  • BS_HISDiseInfo.AdmID = MR.HISAdmID(已确认,Adm_Dr 为指向 BS_HISPatAdm.ID 的对象引用,本报表不依赖、不做反查)
  • BS_HISOprnInfo.AdmID = MR.HISAdmID(已确认,同上)

有效记录口径:

  • MR:(ValiFlag IS NULL OR ValiFlag='1');
  • PA:Deleted=0;
  • FD:Deleted=0 AND (ValidFlag IS NULL OR ValidFlag<>'N');
  • GR:ActiveFlag='1';
  • 诊断/手术:两表均无 Deleted 字段,不做作废过滤;主诊断用 MainFlag=1、主手术用 MainFlag='Y' 判主,其余全部纳入并集。

已确认结论(用户 2026-09-17 确认,无需再查库):

  1. BS_HISDiseInfo / BS_HISOprnInfo 的就诊关联字段为 AdmID,直接与 MR.HISAdmID 关联;
  2. Adm_Dr 是指向 BS_HISPatAdm 表 ID 的对象引用,本报表不使用(禁止 Adm_Dr -> BS_HISPatAdm.ID 反查写法);
  3. 取消结算日期类型,dateType 仅支持 adm(MR.AdmDate)/ discharge(MR.DscgDate),缺省 adm;
  4. 险种类型有字典表 CB_DRGDictTable,中文映射 放后端;
  5. feeItem 截断阈值定为 Top 500 行;
  6. BS_HISDiseInfo / BS_HISOprnInfo 均无 Deleted 字段,不做作废过滤。

接口设计

02010601 QueryComparableVisits

  • ClassName='src.DRG.Report'、MethodName='QueryComparableVisits'、ServiceType='S'、SessionFlag='Y'、TokenFlag='N'。

请求:

{
  "code": "02010601",
  "params": [{
    "fixmedinsCode": "H51010001",
    "dateType": "discharge",
    "startDate": "2026-01-01",
    "endDate": "2026-03-31",
    "drgCode": "CB55",
    "patName": "", "medcasNo": "", "admID": "",
    "mdtrtId": "", "psnNo": "", "insuType": "",
    "mainDiagCode": "", "mainDiagName": "",
    "mainOprnCode": "", "mainOprnName": ""
  }],
  "pagination": [{ "pageSize": 20, "currentPage": 1 }]
}

响应:

{
  "errorCode": "0",
  "errorMessage": "",
  "result": {
    "rows": [{
      "admID": "A001", "medcasNo": "M001", "patName": "张三", "gend": "男", "age": 62,
      "deptCode": "D01", "deptName": "眼科", "doctorCode": "DOC1", "doctorName": "李医生",
      "admDate": "2026-01-05", "dscgDate": "2026-01-15", "iptDays": 10,
      "drgCode": "CB55", "drgName": "晶状体手术",
      "medfeeSumamt": 25000, "payStandard": 20000,
      "insuType": "职工基本医疗保险", "insuTypeCode": "310",
      "psnNo": "5101***", "mdtrtId": "MD001",
      "fixmedinsCode": "H51010001"
    }],
    "total": 128
  }
}

dateType 取值:仅 adm(入院日期,MR.AdmDate)/ discharge(出院日期,MR.DscgDate),缺省 adm;已取消结算日期类型,传入其它值一律按 adm 处理。

insuType(高级区入参)= 险种编码,过滤 PA.InsuType;insuType(响应)= 险种中文名(后端查 CB_DRGDictTable 映射,见 D11),并同时返回编码 insuTypeCode;映射不到时中文名回落为编码原值。

02010602 CompareDRGFee

  • ClassName='src.DRG.Report'、MethodName='CompareDRGFee'、ServiceType='S'、SessionFlag='Y'、TokenFlag='N';不使用分页。

请求:

{
  "code": "02010602",
  "params": [{ "admIds": ["A001", "A002", "A003"], "diffBase": "first" }]
}

响应(结构摘要):

{
  "errorCode": "0",
  "errorMessage": "",
  "result": {
    "visits": [
      { "admID": "A001", "medcasNo": "M001", "patName": "张三", "deptName": "眼科",
        "doctorName": "李医生", "admDate": "2026-01-05", "dscgDate": "2026-01-15",
        "iptDays": 10, "insuType": "职工医保" }
    ],
    "drgPayment": [
      { "admID": "A001", "drgCode": "CB55", "drgName": "晶状体手术",
        "payStandard": 20000, "weight": 1.82, "totalFee": 25000,
        "profitAmount": -5000, "profitRate": -20 }
    ],
    "settleSummary": [
      { "field": "WmFee", "fieldName": "西药费",
        "cells": [
          { "admID": "A001", "value": 3200.5, "diffFlag": "same",  "diffAmount": 0,    "diffRate": 0 },
          { "admID": "A002", "value": 5100.0, "diffFlag": "diff",  "diffAmount": 1899.5, "diffRate": 59.35 }
        ],
        "maxAmount": 5100, "minAmount": 3200.5, "rangeAmount": 1899.5 }
    ],
    "feeItem": [
      { "itemCode": "1001", "itemName": "人工晶体", "spec": "", "unit": "片",
        "cells": [
          { "admID": "A001", "value": 8000, "qty": 1, "diffFlag": "same", "diffAmount": 0, "diffRate": 0 },
          { "admID": "A002", "value": 0,    "qty": 0, "diffFlag": "missing", "diffAmount": -8000, "diffRate": -100 }
        ],
        "maxAmount": 8000, "minAmount": 0, "rangeAmount": 8000 }
    ],
    "feeCategory": [
      { "categoryCode": "01", "categoryName": "西药费",
        "cells": [ { "admID": "A001", "value": 3200.5, "diffFlag": "same", "diffAmount": 0, "diffRate": 0 } ],
        "maxAmount": 3200.5, "minAmount": 3200.5, "rangeAmount": 0 }
    ],
    "diagnoses": [
      { "diagCode": "H25.900", "diagName": "老年性白内障",
        "marks": [ { "admID": "A001", "exists": true, "isMain": true } ] }
    ],
    "operations": [
      { "oprnCode": "13.4100", "oprnName": "白内障超声乳化抽吸术",
        "marks": [ { "admID": "A001", "exists": true, "isMain": true } ] }
    ],
    "analysis": {
      "reportText": [
        "【结论摘要】本次共比对 3 例就诊……",
        "【费用总览】……",
        "【结构差异 Top5】……",
        "【诊断与手术差异】……",
        "【支付标准与盈亏】……",
        "【风险提示】……"
      ]
    }
  }
}

字段约定:

  • diffFlag 取值:same / diff / missing / onlyIn;
  • diffBase='first' 时首例各单元格 diffAmount=0 / diffFlag='same';diffBase='avg' 时以各例均值为基准;
  • 所有金额统一 $FN(x,"",2) 保留 2 位;diffRate 保留 2 位;
  • 除零一律判 0。

业务流程

  1. 页面加载 → useHospitalOptions() 拉机构列表并默认选中当前登录机构;
  2. 检索:用户填写普通区(机构/日期类型/起止日期/DRG 编码),可展开「高级搜索」补 10 个高级条件 → 点「查询」→ 调 02010601 → 渲染待比对列表(CustomPagination 分页,金额列合计行);
  3. 加入比对:勾选 2~5 条 →「加入比对」→ 比对篮展示已选 Tag(可移除)→ 自动调 02010602;
  4. 比对渲染:
    • 顶部:各例基本信息卡(visits)+ DRG 支付标准卡(drgPayment);
    • 中部:Tabs 三维度比对表,差异单元格按 diffFlag 着色(diff 橙色/红色、onlyIn 蓝色、missing 灰色);
    • 底部:诊断比对表、手术比对表;
    • 最下:分析报告卡(reportText 逐段渲染 + 复制按钮);
  5. 切换差异基准:Radio.Group(与首条对比 / 与平均值对比)→ 重新调 02010602(前端不重算);
  6. 导出:按当前 Tab 导出比对矩阵 CSV(含各例列与差异列)。

Risks / Trade-offs

  • [数据量] 5 例 × 数千条明细 → 接口变慢 / 超时。→ Tab2 先按 MedinsListCodg 聚合再比对;单次最多 5 例;feeItem 行数超阈值(如 500 行)时按金额 Top N 截断并在报告与页面上提示。
  • [SQL 兼容] ORDER BY 聚合、WITH(CTE) 在本环境不可用。→ 排序下沉到 ObjectScript;主诊断/主手术用 EXISTS;不使用派生表 JOIN。
  • [关联字段不确定] BS_HISDiseInfo / BS_HISOprnInfo 就诊关联字段存在三种写法。→ 实施第一步先在生产库确认实际列名,再写代码(任务清单已列为准入门槛)。
  • [跨病组不可比] 不同 DRG 费用结构天然不同,报告可能误导。→ DRG 不一致时强制插入「跨病组比对提示」段。
  • [前端渲染] 5 列 × 数百行差异矩阵可能卡顿。→ 表格 size="small" + 横向滚动 + Tab 懒渲染(仅渲染当前 Tab)。
  • [映射缺失] 险种类型 InsuType、结算类别 EcCode 等字典值需中文展示。→ 优先取表中已有描述列(EcDesc);InsuType 中文映射若在后端无字典表,则后端原值透传,前端用既有常量映射,无映射时显示原值。
  • [类文件被 IDE 重写] .cls 末尾为方法时结束大括号可能被吞。→ 类末尾追加 Parameter 成员;写入 20 秒后复查并统计 {/} 数量。

Migration Plan

  1. 在 src/src/DRG/Report.cls 新建类并编译:Do $SYSTEM.OBJ.Compile("src.DRG.Report","ck");
  2. 执行 db/register_interface_02010601.sql、db/register_interface_02010602.sql,SELECT 校验各返回 1 行;
  3. 前端 npm run build + tsc --noEmit 通过后部署 dist 到生产(E:\DRG\dist,nginx 8082);
  4. 执行 db/insert_fee_comparison_menu.sql,为相应角色分配菜单权限,登录后验证页面可访问;
  5. 回归:确认 02010201~02010210 与现有报表页行为不变。

回滚:DELETE FROM CB_MapInterface WHERE Code IN ('02010601','02010602'); + 删除菜单与角色授权记录 + 回退前端 dist 包;Report.cls 可保留(未被调用即无影响)。

Open Questions

(无。全部已确认。)

已关闭:

  • 诊断/手术关联字段是 AdmID 还是 Adm_Dr → 确认为 AdmID
  • 结算日期类型 → 确认取消,仅入院/出院
  • 险种是否有字典表、映射放哪端 → 有 CB_DRGDictTable,映射放后端
  • feeItem 截断阈值 → 定为 Top 500
  • 诊断/手术是否有 Deleted 字段 → 均无,不做作废过滤