tasks.md 21 KB

实施任务清单:重复收费统计报表(02010603 / 02010604 / 02010605)

变更目录:openspec/changes/report-duplicate-charge-statistics/ 后端:追加方法到既有 src/src/DRG/Report.cls(号段 020106xx) 前端:新增 frontend/src/pages/Report/DuplicateCharge.tsx(菜单 key report-duplicate-charge,一个菜单两个 Tab) 参考实现:src.DRG.Profit 的 02010209(%BuildFeeWhere / 自然日 $EXTRACT(feeOcurTime,1,10))、src.DRG.Report 的 02010601/02010602、components/FeeInfoPanel.tsx(重复色池与视觉语言) 参考文档:design.md(D1~D15)、specs/duplicate-charge-summary、specs/duplicate-charge-detail、specs/visit-fee-detail-query


1. 前置:生产库字段与口径确认(阻塞后续所有后端任务)

  • 1.1 确认 BS_DRGMedInsuMedicalRecord 的已分组基准字段:DRG、ValiFlag、AdmDate、DscgDate、HISAdmID 均存在且可用
  • 1.2 确认 BS_HISFeeDetail 字段:AdmID、FeedetlSn、MedinsListCodg/MedinsListDesc、EcCode/EcDesc、Spec、Pric、Cnt、DetItemFeeSumamt、FeeOcurTime、BilgDeptName、OrdersDrName、Deleted、ValidFlag
  • 1.3 确认 FeeOcurTime 为 %TimeStamp,$EXTRACT(feeOcurTime,1,10) 可稳定得到 YYYY-MM-DD(ObjectScript 侧截取,与 Profit.cls 第 2238 行一致)
  • 1.4 确认日期范围比较写法:FD.FeeOcurTime >= 'YYYY-MM-DD 00:00:00' AND FD.FeeOcurTime <= 'YYYY-MM-DD 23:59:59'
  • 1.5 抽样验证「同一天同一项目 ≥2 次」在生产数据中确有命中(用于自测基线),记录样本 admID + 日期 + 项目编码

2. 后端:src/src/DRG/Report.cls 类头与私有辅助

  • 2.1 类头注释追加三条接口说明:02010603 重复收费汇总统计 / 02010604 重复收费明细查询 / 02010605 单次就诊费用信息查询
  • 2.2 类头「数据来源」注释追加:MR/PA/FD 三表在重复收费场景下的关联键与有效口径;追加「重复收费判定口径」说明段(分组键、阈值、排除退费、金额双口径、双比重分母)
  • 2.3 新增 ClassMethod %BuildDupWhere(params As %Library.DynamicObject) As %String
    • 固定片段:" WHERE FD.Deleted = 0 AND (FD.ValidFlag IS NULL OR FD.ValidFlag <> 'N')"
    • MR 基准(任何日期口径都必须附加):" AND EXISTS (SELECT 1 FROM BS_DRGMedInsuMedicalRecord MR WHERE MR.HISAdmID = FD.AdmID AND (MR.ValiFlag IS NULL OR MR.ValiFlag <> '0') AND MR.DRG IS NOT NULL AND MR.DRG <> '')"
    • MR.DRG 非空必须 IS NOT NULL 与 <> '' 成对书写(三值逻辑)
    • 机构:params.fixmedinsCode → AND FD.FixmedinsCode = '...'
    • 日期:按 params.dateType 分流
    • adm(缺省)→ AND EXISTS (SELECT 1 FROM BS_DRGMedInsuMedicalRecord m WHERE m.HISAdmID = FD.AdmID AND m.AdmDate >= '...' AND m.AdmDate <= '...')
    • discharge → 同上换 m.DscgDate
    • fee → AND FD.FeeOcurTime >= 'YYYY-MM-DD 00:00:00' AND FD.FeeOcurTime <= 'YYYY-MM-DD 23:59:59'
    • 高级条件(非空才追加):admID→FD.AdmID = '...';patName→FD.PatName LIKE '%...%';itemCodeOrName→(FD.MedinsListCodg = '...' OR FD.MedinsListDesc LIKE '%...%');ecCode→FD.EcCode = '...';deptName→FD.BilgDeptName LIKE '%...%'
    • 重复判定过滤(候选组聚合 SQL 专用):AND FD.DetItemFeeSumamt >= 0 AND FD.MedinsListCodg <> ''(用独立开关参数控制,全量金额统计时不附加)
    • 所有字符串入参经 $REPLACE(x, "'", "") 防注入
    • 禁止 WITH(CTE) / 派生表 JOIN / 窗口函数 / ORDER BY 聚合表达式与列序号
  • 2.4 新增 ClassMethod %CalcDupAmount(amount As %Numeric, times As %Integer, mode As %String) As %Numeric
    • mode="total" → quit amount
    • mode="excess" → quit $FN(amount * (times - 1) / times, "", 2);times<=1 → quit 0
  • 2.5 新增 ClassMethod %CalcDupQty(qty As %Numeric, times As %Integer, mode As %String) As %Numeric(同 2.4 语义)
  • 2.6 新增 ClassMethod %Ratio(molecule As %Numeric, denominator As %Numeric) As %Numeric:denominator=0 → 0,否则 $FN(molecule / denominator * 100, "", 2)
  • 2.7 取值规范:set params = jsonObj.params.%Get(0) 后直接属性访问(set dateType = params.dateType,= 后带空格),禁止 $G() / %Get("xxx") / $D / $ISOBJECT 预判
  • 2.8 类末尾不放方法:最后一个成员为 Parameter(如 Parameter DupSourceTable As %String = "BS_HISFeeDetail";),规避 IDE 自动重写吞大括号

3. 后端:接口 02010603 QueryDuplicateChargeSummary(汇总统计)

  • 3.1 方法头四行注释:Creator: 系统 / CreatDate: 2026-09-17 / Description: 重复收费汇总统计(按收费项目类型/项目,含双比重与全局KPI) / Code: 02010603
  • 3.2 方法体:new (jsonObj) + set resultObj = {"errorCode":"0","errorMessage":"","result":{}} + try
  • 3.3 入参校验:jsonObj="" 或 jsonObj.params="" → SetErrorResult("-1","入参异常:缺少params参数或参数为空") 并 quit
  • 3.4 护栏校验:fixmedinsCode、startDate/endDate、admID、patName 全空 → SetErrorResult("-1","查询条件不足:请至少提供机构代码、日期区间或就诊标识之一") 并 quit
  • 3.5 解析 params.dupTimes($CASE(params.dupTimes,"":2,:+params.dupTimes);<2 按 2 兜底)、params.dupAmountMode($CASE(...,"total":"total",:"excess"))、params.groupBy($CASE(...,"item":"item",:"category");dept 等未知值统一回落 category,本次不实现科室维度)、params.dateType
  • 3.6 [SQL-1 候选组聚合] SELECT FD.AdmID, FD.MedinsListCodg, <FeeOcurTime日期>, COUNT(*) AS Times, SUM(Cnt) AS Qty, SUM(DetItemFeeSumamt) AS Amount, MAX(FD.MedinsListDesc) AS ItemName, MAX(FD.EcCode) AS CatCode, MAX(FD.EcDesc) AS CatName FROM BS_HISFeeDetail FD + %BuildDupWhere(含重复判定过滤)GROUP BY FD.AdmID, FD.MedinsListCodg, <日期部分>
    • 日期部分在 SQL 侧若不可靠,改为只选 FD.FeeOcurTime 的最小值/最大值或回 ObjectScript 侧截取后归并;优先方案:SQL 只 GROUP BY FD.AdmID, FD.MedinsListCodg,把 FeeOcurTime 一起取出后在 ObjectScript 侧用 $E(ts,1,10) 归并成组(与 Profit.cls 完全一致的写法)
  • 3.7 应用层过滤 times >= dupTimes → 命中组;为每组计算 dupAmount / dupQty(用 2.4 / 2.5)
  • 3.8 [SQL-2 全量金额] 同 WHERE 但不附加「排除退费 / 编码非空」过滤 → SUM(DetItemFeeSumamt) 得 statistics.totalAmount(含负数退费)
  • 3.9 [SQL-3 类型合计] 同 WHERE(不附加重复判定过滤)GROUP BY FD.EcCode → catTotal(EcCode)(分母,含负数退费;空 EcCode 归为「未分类」)
  • 3.10 [SQL-4 就诊总数] 满足 MR 基准 + 机构 + 日期(按 dateType 走 MR 或 FD)的 COUNT(DISTINCT FD.AdmID) → statistics.visitCount;命中组涉及就诊去重计数 → dupVisitCount
  • 3.11 按 groupBy 再归一层(category 用 CatCode/CatName,item 用 MedinsListCodg/ItemName),累加 totalAmount(取 SQL-3 的类型合计)、dupAmount、dupQty、dupRecordCount、dupItemCount、dupVisitCount
  • 3.12 计算 dupRatioOfCategory(用 2.6,分母为类型合计)与 dupRatioOfTotal(分母为 statistics.totalAmount);应用层按 dupAmount 降序排序(sortIdx(amount, idx) + $ORDER(...,-1))
  • 3.13 组装 statistics(visitCount / dupVisitCount / dupVisitRatio / dupItemCount / dupGroupCount / dupRecordCount / totalAmount / dupAmount / dupRatioOfTotal)
  • 3.14 catch(e) → ..%HandleError("重复收费汇总统计", "02010603", jsonObj, e.AsStatus());末尾 quit resultObj

4. 后端:接口 02010604 QueryDuplicateChargeDetail(明细查询,按组分页)

  • 4.1 方法头四行注释(Description: 重复收费明细查询(按重复组分页,同组同色号),Code: 02010604)
  • 4.2 方法体 new (jsonObj) + resultObj + try;入参校验与护栏同 3.3 / 3.4
  • 4.3 set pg = ..%GetPage(jsonObj)(缺省 pageSize 10 / currentPage 1)
  • 4.4 复用 3.6 的候选组聚合与 3.7 的过滤,得到全部命中组
  • 4.5 应用层按 dupAmount 降序排序,为每个组分配稳定 colorIndex = 组序号 # 8(序号从 0 开始,跨页恒定)
  • 4.6 超大结果集保护:命中组数 > MAX_DUP_GROUP=2000 时截断到 2000 组,resultObj.result.message = "命中重复组过多,仅展示重复金额最高的 2000 组,请缩小查询范围"
  • 4.7 按组分页:total = 命中组数;取第 [ (pg.currentPage-1)*pg.pageSize , ... ) 个组
  • 4.8 [SQL-5 明细回捞] SELECT ... FROM BS_HISFeeDetail FD WHERE FD.AdmID IN (<当页组涉及的 AdmID 列表>) AND FD.Deleted = 0 AND (FD.ValidFlag IS NULL OR FD.ValidFlag <> 'N') ORDER BY FD.AdmID, FD.FeeOcurTime, FD.ID;回捞后在应用层按 dupGroupKey 归并到组
    • 每组最多回 MAX_ROW_PER_GROUP=30 条;超出时在该组末尾追加提示行(isTip=1,不参与合计,文案「…该组还有 N 条未展示」)
  • 4.9 为每行填 dupGroupKey(admID|YYYY-MM-DD|itemCode)、colorIndex、times、amount、dupAmount、isDup(times>=dupTimes AND DetItemFeeSumamt>=0 AND MedinsListCodg<>'')
  • 4.10 每行填业务字段:feedetlSn、admID、patName、medcasNo、deptName、doctorName、feeDate、medinsListCodg、medinsListDesc、ecCode、ecDesc(空→「未分类」)、spec、pric、cnt、detItemFeeSumamt、feeOcurTime、bilgDeptName、ordersDrName、drgCode、drgName
    • patName / medcasNo / 科室 / 医生 / DRG 用独立查询按 AdmID 批量回填(避免 JOIN 放大行数)
  • 4.11 金额字段两位小数($FN(x,"",2)),缺失字段置空串或 0(不出 null)
  • 4.12 同时输出 statistics(与 3.13 同构,供明细 Tab 复用 KPI 卡)
  • 4.13 catch(e) → ..%HandleError("重复收费明细查询", "02010604", jsonObj, e.AsStatus());末尾 quit resultObj

5. 后端:接口 02010605 QueryVisitFeeDetail(单次就诊费用信息)

  • 5.1 方法头四行注释(Description: 单次就诊费用信息(明细 + 双维度汇总 + 重复分组统计),Code: 02010605)
  • 5.2 方法体 new (jsonObj) + resultObj + try
  • 5.3 入参校验:params.admID 为空 → SetErrorResult("-1","缺少就诊号admID") 并 quit
  • 5.4 [SQL-A 就诊基本信息] MR + PA 取 admID / patName / medcasNo / deptName / doctorName / admDate / dscgDate / drgCode / drgName / insuType(中文用 ..%GetInsuTypeName)/ medfeeSumamt;查不到时 visit = {},不报错
  • 5.5 [SQL-B 全部明细] SELECT ... FROM BS_HISFeeDetail FD WHERE FD.AdmID = '<admID>' AND FD.Deleted = 0 AND (FD.ValidFlag IS NULL OR FD.ValidFlag <> 'N') ORDER BY FD.FeeOcurTime, FD.ID(含负数退费行)
  • 5.6 应用层:双维度汇总 summaryByCategory(EcCode/EcDesc)、summaryByItem(MedinsListCodg/MedinsListDesc),含 amount / qty / recordCount / avgPrice / ratio;空 EcCode 归「未分类」;应用层降序排序
  • 5.7 应用层:重复分组(日期用 $E(feeOcurTime,1,10) + MedinsListCodg,仅 DetItemFeeSumamt>=0 且编码非空),过滤 times>=dupTimes,输出 dupGroups(date / itemCode / itemName / catName / times / qty / amount / dupAmount),按日期倒序、组内按 dupAmount 降序
  • 5.8 为每条明细填 dupGroupKey / colorIndex(组稳定序号 # 8)/ times / amount / dupAmount / isDup;负数行 isDup=false 且不下发 colorIndex
  • 5.9 组装 statistics:totalAmount(全部有效明细金额,含负数)、dupAmount、dupRatio、recordCount、refundAmount(负数行金额合计)
  • 5.10 只读不写:本接口不得调用任何 HIS 同步方法(HisDataService 相关),不得写库
  • 5.11 catch(e) → ..%HandleError("查询单次就诊费用信息", "02010605", jsonObj, e.AsStatus());末尾 quit resultObj
  • 5.12 写入后统计 { 与 } 数量确认配对;延时 20 秒以上复查一次(WriteAllText + UTF8Encoding($false),LF 换行、无 BOM)

6. 后端:编译与路由注册

  • 6.1 Do $SYSTEM.OBJ.Compile("src.DRG.Report","ck"),确认无编译错误
  • 6.2 新建 db/register_interface_02010603.sql:INSERT INTO CB_MapInterface (Code, Descripts, ClassName, MethodName, ServiceType, SessionFlag, TokenFlag) SELECT '02010603','DRG报表-重复收费汇总统计','src.DRG.Report','QueryDuplicateChargeSummary','S','Y','N' WHERE NOT EXISTS (...);附校验 SELECT 与回滚 DELETE
  • 6.3 新建 db/register_interface_02010604.sql:同上,MethodName='QueryDuplicateChargeDetail',描述「DRG报表-重复收费明细查询」
  • 6.4 新建 db/register_interface_02010605.sql:同上,MethodName='QueryVisitFeeDetail',描述「DRG报表-单次就诊费用信息查询」
  • 6.5 依次执行三个 SQL,SELECT 各校验返回 1 行;重复执行验证幂等(记录数仍各为 1)

7. 后端自测(生产或本地 IRIS)

  • 7.1 02010603 仅传机构 + 日期区间 → 返回 statistics.visitCount 与 MR 已分组口径一致(抽查若干 admID 确认 MR.DRG 非空)
  • 7.2 dateType 三种取值(adm / discharge / fee)结果不同且正确;传入无效值按 adm 处理
  • 7.3 dupAmountMode 两种口径:excess 下 dupAmount = amount × (times-1)/times;total 下 dupAmount = amount;KPI 随之变化
  • 7.4 dupTimes=3 时仅 times>=3 的组计入;dupTimes=1 按 2 处理
  • 7.5 退费负数记录不计入 times,但计入 totalAmount 与类型合计分母(dupRatioOfCategory 分母含负数)
  • 7.6 空 EcCode/EcDesc 金额归入「未分类」,不丢失
  • 7.7 dupRatioOfCategory 与 dupRatioOfTotal 手工核算一致(两位小数,分母为 0 时置 0)
  • 7.8 条件全空 → errorCode='-1' 且 errorMessage 含「查询条件不足」
  • 7.9 02010604 同组明细不被跨页切断;翻页后同 dupGroupKey 的 colorIndex 不变
  • 7.10 02010604 命中组 > 2000 时 total=2000 且返回 message 提示;单组 > 30 条时追加提示行
  • 7.11 02010605 传空 admID → errorCode='-1';传不存在 admID → errorCode='0' 且 rows=[]
  • 7.12 02010605 重复行 isDup=true 且同组 colorIndex 一致;负数行 isDup=false
  • 7.13 02010605 不触发 HIS 同步(调用前后 BS_HISFeeDetail 记录数不变)
  • 7.14 回归:02010601 / 02010602 调用结果不变;02010209 结果不变

8. 前端 API 层 frontend/src/api/report.ts

  • 8.1 头注释追加 02010603 / 02010604 / 02010605 三行说明
  • 8.2 类型定义:DuplicateSummaryItem、DuplicateStatistics、DuplicateDetailItem、VisitFeeDetailResult、VisitInfo、VisitFeeSummaryItem、VisitDupGroup、QueryDuplicateParams
  • 8.3 常量:DUP_DATE_TYPE_OPTIONS(入院日期 adm / 出院日期 discharge / 费用日期 fee)、DUP_AMOUNT_MODE_OPTIONS(超出部分 excess / 全部金额 total)、DUP_GROUP_BY_OPTIONS(收费项目类型 category / 收费项目 item)
  • 8.4 queryDuplicateChargeSummary(params, pagination) → invoke('02010603', [withHosp(params)], undefined, pagination)
  • 8.5 queryDuplicateChargeDetail(params, pagination) → invoke('02010604', [withHosp(params)], undefined, pagination)
  • 8.6 queryVisitFeeDetail(params) → invoke('02010605', [withHosp(params)])(不分页)

9. 前端页面 frontend/src/pages/Report/DuplicateCharge.tsx(一个菜单两 Tab)

  • 9.1 页面骨架:查询条件 Card + Tabs(summary 汇总统计 / detail 明细查询)+ 单次就诊费用 Modal
  • 9.2 查询条件(两 Tab 共享):医疗机构(useHospitalOptions,非管理员锁定)、日期口径 Select(入院日期 / 出院日期 / 费用日期)、RangePicker(locale={zhCN}、format="YYYY年MM月DD日"、dayjs.locale('zh-cn'))、重复阈值 InputNumber(默认 2,min 2 max 99)、重复金额口径 Select(默认「超出部分」)、查询 / 重置按钮
  • 9.3 高级搜索折叠区(默认收起):就诊号 / 患者姓名、项目名称或编码、收费类型、科室;标题带 Badge 显示已填条件数
  • 9.4 searched 状态:菜单打开不自动查询,点击「查询」后才随条件/机构变化自动重查;重置回到未查询态
  • 9.5 Tab1 汇总统计:KPI 卡行(病历总数 / 存在重复收费病历数 + 占比 / 重复项目数 / 重复金额合计 + 占总费用比重,金额两位小数);汇总表列:编码、名称、类型合计金额、重复金额、重复数量、涉及就诊数、占类型比重、占总费用比重
  • 9.6 汇总表 Table.Summary 合计行:金额列求和红色加粗,蓝色「合计」+ 灰色「(本页 N 条)」
  • 9.7 汇总表顶部 Segmented 切 groupBy(收费项目类型 / 收费项目),切换回到第 1 页重查
  • 9.8 Tab2 明细查询:重复明细表列(就诊号、患者、病案号、科室、医生、费用日期、项目编码、项目名称、结算类别、规格、单价、数量、金额、当日次数、重复标记)
  • 9.9 明细表同组同色标注:onRow 按 colorIndex 取色池(DUP_BG_COLORS[colorIndex % 8],色池与 FeeInfoPanel.tsx 完全一致),重复行加「重复」Tag 与左侧色条(DUP_BAR_COLORS[colorIndex % 8]);提示行(isTip=1)灰色斜体、不参与合计
  • 9.10 两个 Tab 均用 CustomPagination 分页(pagination={false} + CustomPagination)
  • 9.11 两个 Tab 均提供「导出Excel」:跨分页拉取全部(最多 100 页,pageSize=200),CSV + \uFEFF BOM、MIME application/vnd.ms-excel;charset=utf-8、.xls 后缀;优先 showSaveFilePicker(AbortError 静默返回),回落 a.download;文件名 重复收费统计_<Tab名>_<日期区间>_<时间戳>.xls,sanitize \ / : * ? " < > |,核心长度 ≤ 80;无数据(total===0)时按钮 disabled
  • 9.12 单次就诊费用弹窗:汇总行 / 明细行「查看费用」入口 → Modal(宽 ≥1100)调用 02010605,展示患者基本信息 Descriptions(3 列、labelStyle={{ width: 100 }})+ 该就诊费用汇总(Segmented 切类型/项目)+ 重复分组统计 + 全部明细表(重复行同色高亮)
  • 9.13 金额统一两位小数(元用 fmtMoney;比率 toFixed(2) + %)
  • 9.13.1 口径说明(D16-3 / D16-4):「重复金额」列表头与 KPI 卡加 Tooltip 说明「超出部分 = 组内总额 ×(次数−1)/次数(按均价保留一次)」;「占类型比重」「占总费用比重」列表头说明「分母为全部有效明细金额(含退费冲账)」
  • 9.14 空态:未查询显示「请选择查询条件后点击「查询」」;已查询无数据显示「未查询到重复收费记录」

10. 前端路由与菜单注册

  • 10.1 frontend/src/routes/menuRoutes.ts:keyToPath 加 'report-duplicate-charge': '/report/duplicate-charge';menuTitleMap 加 'report-duplicate-charge': '重复收费统计'
  • 10.2 frontend/src/routes/menuRouteRegistry.tsx:import DuplicateCharge 并在 MENU_RENDERERS 注册 'report-duplicate-charge'
  • 10.3 frontend/src/context/MenuContext.tsx:「报表分析」分组 children 追加 { key: 'report-duplicate-charge', label: '重复收费统计' }
  • 10.4 npm run build 与 tsc --noEmit 均无错误

11. 菜单初始化与部署验收

  • 11.1 新建 db/insert_duplicate_charge_menu.sql:INSERT INTO SQLUser.BS_DRGSystemMenu (MenuCode, MenuName, ParentID, MenuPath, MenuIcon, Component, SeqNo, MenuLevel, IsLeaf, IsActive, IsVisible, MenuType, Permission, ...) SELECT 'report-duplicate-charge','重复收费统计','report','/report/duplicate-charge','WarningOutlined','pages/Report/DuplicateCharge',30,2,'Y','Y','Y','02','report:duplicate-charge',... WHERE NOT EXISTS (...);附校验 SELECT、角色授权 INSERT(注释)与回滚 DELETE
  • 11.2 执行菜单 SQL,SELECT 校验落库且父级为 report;为相应角色分配菜单权限
  • 11.3 部署前端 dist 到生产(nginx 8082),验证 /report/duplicate-charge 深链接可打开
  • 11.4 端到端验收:设置条件 → Tab1 看 KPI 与双比重 → 切 groupBy → Tab2 验证同色标注、按组分页、翻页色号不变 → 点「查看费用」验证下钻 → 两个 Tab 分别导出 Excel
  • 11.5 性能验收:默认区间(近三个月)汇总接口响应在可接受范围内(记录实际耗时);如超时则收窄默认区间或增加索引

12. 文档与归档

  • 12.1 接口服务说明/09-报表服务.md:追加 02010603 / 02010604 / 02010605 完整说明(入参表、出参表、错误码、示例 JSON)
  • 12.2 接口服务说明/00-接口总览.md:接口清单「2.8 报表服务 (src.DRG.Report)」追加三条
  • 12.3 系统设计/09-报表模块设计.md:追加第 2 章「重复收费统计」(表结构与字段、接口设计、业务流程、SQL 兼容性约束、口径说明)
  • 12.4 归档前用 /opsx:archive 执行归档,确认 openspec/specs/duplicate-charge-summary/spec.md、openspec/specs/duplicate-charge-detail/spec.md、openspec/specs/visit-fee-detail-query/spec.md 落位