tasks.md 11 KB

实施任务清单:患者费用明细统计分析(02010209)

变更目录:openspec/changes/profit-patient-fee-detail-analysis/ 接口号:02010209 方法:src.DRG.Profit.QueryPatientFeeDetailAnalysis 范围:后端接口 + CB_MapInterface 路由注册 + 前端菜单页面(profit-fee-detail) 参考实现:drg-profit-stage(02010208,同构)、drg-profit-insurance-area(02010207)


1. 类头与注释

  • 1.1 在 src/src/DRG/Profit.cls 类头接口清单(第 9~16 行区块)追加一行:/// 02010209 患者费用明细统计分析(基于 BS_HISFeeDetail)
  • 1.2 在类头「数据来源」注释区块追加 BS_HISFeeDetail (FD) 说明:费用明细流水(项目编码/名称、结算项目类别、单价、数量、金额、收费科室、开单医生、费用发生时间),关联键 FD.AdmID = PA.AdmID
  • 1.3 注明有效记录口径:FD.Deleted=0 且 FD.ValidFlag 为 NULL 或 <>'N'

2. 私有辅助方法 %BuildFeeWhere

  • 2.1 在 Profit.cls 末尾(业务方法之后)新增 ClassMethod %BuildFeeWhere(params As %Library.DynamicObject) As %String
  • 2.2 基础片段固定为 " WHERE FD.Deleted = 0 AND (FD.ValidFlag IS NULL OR FD.ValidFlag <> 'N') "
  • 2.3 追加条件(均为「非空才追加」):
    • params.fixmedinsCode → AND FD.FixmedinsCode = '...'
    • params.admID → AND FD.AdmID = '...'
    • params.patName → AND FD.PatName LIKE '%...%'
    • params.patMedicalNo → AND FD.PatMedicalNo = '...'
    • params.deptCode → AND FD.BilgDeptCodg = '...'
    • params.ecCode → AND FD.EcCode = '...'
    • params.itemName → AND (FD.MedinsListDesc LIKE '%...%' OR FD.MedinsListCodg LIKE '%...%')
    • params.drgCode → AND MR.DRG = '...'
  • 2.4 日期条件按 params.dateType 分流(fee=FD.FeeOcurTime、adm=PA.AdmDateTime、discharge=PA.DisDateTime,缺省 fee);endDate 追加 23:59:59
  • 2.5 所有字符串入参经 $REPLACE(x, "'", "") 处理防注入
  • 2.6 取值规范:set params = jsonObj.params.%Get(0) 后直接属性访问(如 set fixmedinsCode = params.fixmedinsCode),禁止 $G() / %Get("xxx") / 额外防御性写法
  • 2.7 本类为 Not ProcedureBlock,方法内用 new 隔离局部变量(主方法已 new (jsonObj))

3. 主方法 QueryPatientFeeDetailAnalysis

  • 3.1 新增 ClassMethod QueryPatientFeeDetailAnalysis(jsonObj As %Library.DynamicObject) As %Library.DynamicObject,方法头四行注释:Creator: 系统 / CreatDate: 2026-09-10 / Description: 患者费用明细统计分析(明细列表 + 结算项目类别/收费项目聚合) / Code: 02010209
  • 3.2 方法体开头:new (jsonObj) + set resultObj = {"errorCode":"0","errorMessage":"","result":{}} + try 包裹
  • 3.3 入参校验:jsonObj="" 或 jsonObj.params="" → ..SetErrorResult("-1","入参异常:缺少params参数或参数为空") 并 quit
  • 3.4 护栏校验:fixmedinsCode、admID、startDate/endDate 三者全空 → ..SetErrorResult("-1","查询条件不足:请至少提供机构代码、就诊号或日期区间之一") 并 quit
  • 3.5 set pg = ..%GetPage(jsonObj) 解析分页;set params = jsonObj.params.%Get(0);set w = ..%BuildFeeWhere(params)
  • 3.6 定义 mrCTE(WITH mr_agg AS (...))与公共 baseFrom:FROM BS_HISFeeDetail FD LEFT JOIN BS_HISPatAdm PA ON FD.AdmID = PA.AdmID LEFT JOIN mr_agg MR ON MR.HISAdmID = FD.AdmID AND MR.MedcasNo = FD.PatMedicalNo _ w
  • 3.7 [SQL-1 统计] SELECT COUNT(*) AS recordCount, SUM(COALESCE(FD.DetItemFeeSumamt,0)) AS totalAmount, SUM(COALESCE(FD.Cnt,0)) AS totalQty, SUM(CASE WHEN FD.DetItemFeeSumamt < 0 THEN FD.DetItemFeeSumamt ELSE 0 END) AS refundAmount, COUNT(DISTINCT FD.AdmID) AS admCount + baseFrom
  • 3.8 组装 statistics:totalAmount($FN(...,"",2))、totalQty、recordCount、admCount、patientCount=admCount、avgAmountPerPatient、avgRecordAmount、refundAmount(除零均判 0)
  • 3.9 %Prepare 失败 → ..%HandleError("查询患者费用明细统计", "02010209", jsonObj, sc)
  • 3.10 若 COUNT(DISTINCT FD.AdmID) 在当前 IRIS 版本 %Prepare 失败,改用 SELECT DISTINCT FD.AdmID 后循环计数兜底(禁止用派生表,见 design D5)
  • 3.11 实测修正:本环境 IRIS 不支持 WITH(CTE)(报 SQLCODE -51 需要 SQL 语句,找到 WITH),已移除 mr_agg CTE 与 MR 表 JOIN → DRG 过滤改 EXISTS 子查询、DRG 编码/名称改独立 GROUP BY HISAdmID 映射查询回填
  • 3.12 实测修正:退费合计去掉 SUM(CASE WHEN ...),改为独立聚合查询 ... AND FD.DetItemFeeSumamt < 0(CASE 语法风险规避)
  • 3.13 实测修正:聚合 SQL 的 ORDER BY SUM(...) 报 SQLCODE -73;改列序号 ORDER BY 3 DESC 仍报 -73,最终改为 SQL 不带 ORDER BY,在 ObjectScript 侧用下标数组(sortIdx(amount, idx) + $ORDER(...,-1))按 amount 降序排序
  • 3.14 已验证可用语法:COUNT(DISTINCT col)、COALESCE、EXISTS 子查询、LEFT JOIN、GROUP BY、ORDER BY <普通列>;已验证不可用:WITH(CTE)、ORDER BY 聚合表达式、ORDER BY 列序号

4. 聚合模式(mode='summary')

  • 4.1 解析 params.mode(缺省 detail)与 params.groupBy(缺省 category)
  • 4.2 groupBy='category' → FD.EcCode AS groupCode, FD.EcDesc AS groupName;groupBy='item' → FD.MedinsListCodg AS groupCode, FD.MedinsListDesc AS groupName
  • 4.3 [SQL-2 聚合] SUM(COALESCE(FD.DetItemFeeSumamt,0)) AS amount, SUM(COALESCE(FD.Cnt,0)) AS qty, COUNT(*) AS recordCount, COUNT(DISTINCT FD.AdmID) AS patientCount + GROUP BY <分组表达式> + ORDER BY SUM(...) DESC
  • 4.4 遍历结果:空 groupName 归一为「未分类」
  • 4.5 计算 avgPrice = qty>0 ? amount/qty : 0、ratio = totalAmount>0 ? $FN(amount/totalAmount*100,"",1) : 0
  • 4.6 params.topN > 0 时截断前 N 行
  • 4.7 set resultObj.result = {rows, total, statistics}(total = 聚合分组行数)

5. 明细模式(mode='detail')

  • 5.1 [SQL-2 明细] 查询 23 个明细字段(含 MR.DRG/MR.DRGDesc)+ baseFrom + ORDER BY FD.FeeOcurTime DESC, FD.ID DESC
  • 5.2 遍历结果组装 all 数组:金额/数量/单价用 +v 归一(空值置 0)
  • 5.3 set paged = ..%Paginate(all, pg),set result = {rows: paged.rows, total: paged.total, statistics: statistics}
  • 5.4 明细行放大风险已通过 mr_agg CTE 预聚合解决(不依赖 MR 单行 JOIN)

6. 收尾与错误处理

  • 6.1 catch(e) 分支:set resultObj = ..%HandleError("查询患者费用明细", "02010209", jsonObj, e.AsStatus())
  • 6.2 方法末尾 quit resultObj
  • 6.3 类文件写入后按项目规范复查:类的最后一个成员不要是方法(末尾追加 Parameter FeeDetailSourceTable),写入 20 秒后复查一次
  • 6.4 统计 { 与 } 数量确认括号配对(实测 320/320,复查后仍为 320/320)

7. 编译与路由注册

  • 7.1 编译:Do $SYSTEM.OBJ.Compile("src.DRG.Profit","ck"),确认无编译错误
  • 7.2 生成 db/register_interface_02010209.sql(幂等 INSERT ... WHERE NOT EXISTS + 校验 SELECT):Code='02010209'、Descripts='DRG盈亏分析-患者费用明细统计'、ClassName='src.DRG.Profit'、MethodName='QueryPatientFeeDetailAnalysis'、ServiceType='S'、SessionFlag='Y'、TokenFlag='N'
  • 7.3 在 IRIS(172.16.2.15, USER)手工执行注册脚本并 SELECT 校验记录数为 1(已由用户手动执行)
  • 7.4 重复执行脚本验证幂等(记录数仍为 1)

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

  • 8.1 mode='detail' + admID → 返回该就诊全部明细,按 feeOcurTime 降序
  • 8.2 mode='detail' + fixmedinsCode + 日期区间 + pagination.currentPage=2 → 分页切片正确、total 正确
  • 8.3 mode='summary' + groupBy='category' → 分组行含 amount/qty/recordCount/patientCount/avgPrice/ratio,按金额降序
  • 8.4 mode='summary' + groupBy='item' → 同上,按收费项目分组
  • 8.5 Σ(rows[].amount) === statistics.totalAmount(2 位精度内)
  • 8.6 空 EcCode 明细归入「未分类」且不丢金额
  • 8.7 存在负金额时 refundAmount 为负值之和、totalAmount 为净额
  • 8.8 Deleted=1 / ValidFlag='N' 记录不进入统计
  • 8.9 三个护栏条件全空 → errorCode='-1' 且提示「查询条件不足…」
  • 8.10 缺 params → errorCode='-1' 且提示「入参异常…」
  • 8.11 totalAmount=0 时 ratio 为 0 不除零;qty=0 时 avgPrice 为 0
  • 8.12 现有接口 02010201~02010208 回归调用结果不变

9. 文档与归档

  • 9.1 接口服务说明/05-盈亏分析服务.md:追加 02010209 接口完整说明(入参表、出参表、错误码、示例)
  • 9.2 接口服务说明/00-接口总览.md:接口清单追加 02010209
  • 9.3 系统设计/04-盈亏分析模块设计.md:追加第 8 章「患者费用明细统计分析」
  • 9.4 归档前用 /opsx:archive 执行归档,确认 openspec/specs/patient-fee-detail-analysis/spec.md 落位

10. 前端菜单与页面(profit-fee-detail)

  • 10.1 frontend/src/api/profit.ts:新增 FeeDetailMode / FeeDetailGroupBy / FeeDetailDateType、三个选项常量、FeeDetailSummaryItem、FeeDetailItem、FeeDetailStatistics、QueryPatientFeeDetailParams、PatientFeeDetailResult 与 queryPatientFeeDetailAnalysis()(invoke('02010209', [withHosp(params)], undefined, pagination))
  • 10.2 新建 frontend/src/pages/Profit/PatientFeeDetail.tsx:工具栏(机构 / 日期口径 / 中文 RangePicker / 就诊号 / 项目名称 / 汇总-明细切换 / 聚合维度)+ 5 张 KPI 卡片 + 表格 + 合计行 + CustomPagination + 导出Excel(CSV+BOM,showSaveFilePicker 回落 a.download)
  • 10.3 汇总/明细两种表格列定义,金额列红/绿着色,合计行对金额求和并标「(本页 N 条)」
  • 10.4 frontend/src/routes/menuRoutes.ts:keyToPath 加 'profit-fee-detail': '/profit/fee-detail',menuTitleMap 加 'profit-fee-detail': '患者费用明细统计'
  • 10.5 frontend/src/routes/menuRouteRegistry.tsx:import PatientFeeDetail 并在 MENU_RENDERERS 注册 'profit-fee-detail'
  • 10.6 生成 db/insert_fee_detail_menu.sql(CB_MenuDetail 幂等 INSERT + Admin 角色授权 + 回滚语句)
  • 10.7 npm run build 与 tsc --noEmit 均无错误
  • 10.8 执行菜单 SQL 并为相应角色分配菜单权限,登录后验证页面可访问、查询/导出正常