变更目录:
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)
src/src/DRG/Profit.cls 类头接口清单(第 9~16 行区块)追加一行:/// 02010209 患者费用明细统计分析(基于 BS_HISFeeDetail)BS_HISFeeDetail (FD) 说明:费用明细流水(项目编码/名称、结算项目类别、单价、数量、金额、收费科室、开单医生、费用发生时间),关联键 FD.AdmID = PA.AdmIDFD.Deleted=0 且 FD.ValidFlag 为 NULL 或 <>'N'%BuildFeeWhereProfit.cls 末尾(业务方法之后)新增 ClassMethod %BuildFeeWhere(params As %Library.DynamicObject) As %String" WHERE FD.Deleted = 0 AND (FD.ValidFlag IS NULL OR FD.ValidFlag <> 'N') "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 = '...'params.dateType 分流(fee=FD.FeeOcurTime、adm=PA.AdmDateTime、discharge=PA.DisDateTime,缺省 fee);endDate 追加 23:59:59$REPLACE(x, "'", "") 处理防注入set params = jsonObj.params.%Get(0) 后直接属性访问(如 set fixmedinsCode = params.fixmedinsCode),禁止 $G() / %Get("xxx") / 额外防御性写法Not ProcedureBlock,方法内用 new 隔离局部变量(主方法已 new (jsonObj))QueryPatientFeeDetailAnalysisClassMethod QueryPatientFeeDetailAnalysis(jsonObj As %Library.DynamicObject) As %Library.DynamicObject,方法头四行注释:Creator: 系统 / CreatDate: 2026-09-10 / Description: 患者费用明细统计分析(明细列表 + 结算项目类别/收费项目聚合) / Code: 02010209new (jsonObj) + set resultObj = {"errorCode":"0","errorMessage":"","result":{}} + try 包裹jsonObj="" 或 jsonObj.params="" → ..SetErrorResult("-1","入参异常:缺少params参数或参数为空") 并 quitfixmedinsCode、admID、startDate/endDate 三者全空 → ..SetErrorResult("-1","查询条件不足:请至少提供机构代码、就诊号或日期区间之一") 并 quitset pg = ..%GetPage(jsonObj) 解析分页;set params = jsonObj.params.%Get(0);set w = ..%BuildFeeWhere(params)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 _ wSELECT 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 + baseFromstatistics:totalAmount($FN(...,"",2))、totalQty、recordCount、admCount、patientCount=admCount、avgAmountPerPatient、avgRecordAmount、refundAmount(除零均判 0)%Prepare 失败 → ..%HandleError("查询患者费用明细统计", "02010209", jsonObj, sc)COUNT(DISTINCT FD.AdmID) 在当前 IRIS 版本 %Prepare 失败,改用 SELECT DISTINCT FD.AdmID 后循环计数兜底(禁止用派生表,见 design D5)WITH(CTE)(报 SQLCODE -51 需要 SQL 语句,找到 WITH),已移除 mr_agg CTE 与 MR 表 JOIN → DRG 过滤改 EXISTS 子查询、DRG 编码/名称改独立 GROUP BY HISAdmID 映射查询回填SUM(CASE WHEN ...),改为独立聚合查询 ... AND FD.DetItemFeeSumamt < 0(CASE 语法风险规避)ORDER BY SUM(...) 报 SQLCODE -73;改列序号 ORDER BY 3 DESC 仍报 -73,最终改为 SQL 不带 ORDER BY,在 ObjectScript 侧用下标数组(sortIdx(amount, idx) + $ORDER(...,-1))按 amount 降序排序COUNT(DISTINCT col)、COALESCE、EXISTS 子查询、LEFT JOIN、GROUP BY、ORDER BY <普通列>;已验证不可用:WITH(CTE)、ORDER BY 聚合表达式、ORDER BY 列序号mode='summary')params.mode(缺省 detail)与 params.groupBy(缺省 category)groupBy='category' → FD.EcCode AS groupCode, FD.EcDesc AS groupName;groupBy='item' → FD.MedinsListCodg AS groupCode, FD.MedinsListDesc AS groupNameSUM(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(...) DESCgroupName 归一为「未分类」avgPrice = qty>0 ? amount/qty : 0、ratio = totalAmount>0 ? $FN(amount/totalAmount*100,"",1) : 0params.topN > 0 时截断前 N 行set resultObj.result = {rows, total, statistics}(total = 聚合分组行数)mode='detail')MR.DRG/MR.DRGDesc)+ baseFrom + ORDER BY FD.FeeOcurTime DESC, FD.ID DESCall 数组:金额/数量/单价用 +v 归一(空值置 0)set paged = ..%Paginate(all, pg),set result = {rows: paged.rows, total: paged.total, statistics: statistics}mr_agg CTE 预聚合解决(不依赖 MR 单行 JOIN)catch(e) 分支:set resultObj = ..%HandleError("查询患者费用明细", "02010209", jsonObj, e.AsStatus())quit resultObjParameter FeeDetailSourceTable),写入 20 秒后复查一次{ 与 } 数量确认括号配对(实测 320/320,复查后仍为 320/320)Do $SYSTEM.OBJ.Compile("src.DRG.Profit","ck"),确认无编译错误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'SELECT 校验记录数为 1(已由用户手动执行)mode='detail' + admID → 返回该就诊全部明细,按 feeOcurTime 降序mode='detail' + fixmedinsCode + 日期区间 + pagination.currentPage=2 → 分页切片正确、total 正确mode='summary' + groupBy='category' → 分组行含 amount/qty/recordCount/patientCount/avgPrice/ratio,按金额降序mode='summary' + groupBy='item' → 同上,按收费项目分组Σ(rows[].amount) === statistics.totalAmount(2 位精度内)EcCode 明细归入「未分类」且不丢金额refundAmount 为负值之和、totalAmount 为净额Deleted=1 / ValidFlag='N' 记录不进入统计errorCode='-1' 且提示「查询条件不足…」params → errorCode='-1' 且提示「入参异常…」totalAmount=0 时 ratio 为 0 不除零;qty=0 时 avgPrice 为 0接口服务说明/05-盈亏分析服务.md:追加 02010209 接口完整说明(入参表、出参表、错误码、示例)接口服务说明/00-接口总览.md:接口清单追加 02010209系统设计/04-盈亏分析模块设计.md:追加第 8 章「患者费用明细统计分析」/opsx:archive 执行归档,确认 openspec/specs/patient-fee-detail-analysis/spec.md 落位frontend/src/api/profit.ts:新增 FeeDetailMode / FeeDetailGroupBy / FeeDetailDateType、三个选项常量、FeeDetailSummaryItem、FeeDetailItem、FeeDetailStatistics、QueryPatientFeeDetailParams、PatientFeeDetailResult 与 queryPatientFeeDetailAnalysis()(invoke('02010209', [withHosp(params)], undefined, pagination))frontend/src/pages/Profit/PatientFeeDetail.tsx:工具栏(机构 / 日期口径 / 中文 RangePicker / 就诊号 / 项目名称 / 汇总-明细切换 / 聚合维度)+ 5 张 KPI 卡片 + 表格 + 合计行 + CustomPagination + 导出Excel(CSV+BOM,showSaveFilePicker 回落 a.download)frontend/src/routes/menuRoutes.ts:keyToPath 加 'profit-fee-detail': '/profit/fee-detail',menuTitleMap 加 'profit-fee-detail': '患者费用明细统计'frontend/src/routes/menuRouteRegistry.tsx:import PatientFeeDetail 并在 MENU_RENDERERS 注册 'profit-fee-detail'db/insert_fee_detail_menu.sql(CB_MenuDetail 幂等 INSERT + Admin 角色授权 + 回滚语句)npm run build 与 tsc --noEmit 均无错误