变更目录:
openspec/changes/report-duplicate-charge-statistics/后端:追加方法到既有src/src/DRG/Report.cls(号段 020106xx) 前端:新增frontend/src/pages/Report/DuplicateCharge.tsx(菜单 keyreport-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
BS_DRGMedInsuMedicalRecord 的已分组基准字段:DRG、ValiFlag、AdmDate、DscgDate、HISAdmID 均存在且可用BS_HISFeeDetail 字段:AdmID、FeedetlSn、MedinsListCodg/MedinsListDesc、EcCode/EcDesc、Spec、Pric、Cnt、DetItemFeeSumamt、FeeOcurTime、BilgDeptName、OrdersDrName、Deleted、ValidFlagFeeOcurTime 为 %TimeStamp,$EXTRACT(feeOcurTime,1,10) 可稳定得到 YYYY-MM-DD(ObjectScript 侧截取,与 Profit.cls 第 2238 行一致)FD.FeeOcurTime >= 'YYYY-MM-DD 00:00:00' AND FD.FeeOcurTime <= 'YYYY-MM-DD 23:59:59'admID + 日期 + 项目编码src/src/DRG/Report.cls 类头与私有辅助02010603 重复收费汇总统计 / 02010604 重复收费明细查询 / 02010605 单次就诊费用信息查询ClassMethod %BuildDupWhere(params As %Library.DynamicObject) As %String
" WHERE FD.Deleted = 0 AND (FD.ValidFlag IS NULL OR FD.ValidFlag <> 'N')"" 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.DscgDatefee → 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 '%...%'AND FD.DetItemFeeSumamt >= 0 AND FD.MedinsListCodg <> ''(用独立开关参数控制,全量金额统计时不附加)$REPLACE(x, "'", "") 防注入WITH(CTE) / 派生表 JOIN / 窗口函数 / ORDER BY 聚合表达式与列序号ClassMethod %CalcDupAmount(amount As %Numeric, times As %Integer, mode As %String) As %Numeric
mode="total" → quit amountmode="excess" → quit $FN(amount * (times - 1) / times, "", 2);times<=1 → quit 0ClassMethod %CalcDupQty(qty As %Numeric, times As %Integer, mode As %String) As %Numeric(同 2.4 语义)ClassMethod %Ratio(molecule As %Numeric, denominator As %Numeric) As %Numeric:denominator=0 → 0,否则 $FN(molecule / denominator * 100, "", 2)set params = jsonObj.params.%Get(0) 后直接属性访问(set dateType = params.dateType,= 后带空格),禁止 $G() / %Get("xxx") / $D / $ISOBJECT 预判Parameter(如 Parameter DupSourceTable As %String = "BS_HISFeeDetail";),规避 IDE 自动重写吞大括号QueryDuplicateChargeSummary(汇总统计)Creator: 系统 / CreatDate: 2026-09-17 / Description: 重复收费汇总统计(按收费项目类型/项目,含双比重与全局KPI) / Code: 02010603new (jsonObj) + set resultObj = {"errorCode":"0","errorMessage":"","result":{}} + tryjsonObj="" 或 jsonObj.params="" → SetErrorResult("-1","入参异常:缺少params参数或参数为空") 并 quitfixmedinsCode、startDate/endDate、admID、patName 全空 → SetErrorResult("-1","查询条件不足:请至少提供机构代码、日期区间或就诊标识之一") 并 quitparams.dupTimes($CASE(params.dupTimes,"":2,:+params.dupTimes);<2 按 2 兜底)、params.dupAmountMode($CASE(...,"total":"total",:"excess"))、params.groupBy($CASE(...,"item":"item",:"category");dept 等未知值统一回落 category,本次不实现科室维度)、params.dateTypeSELECT 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, <日期部分>
FD.FeeOcurTime 的最小值/最大值或回 ObjectScript 侧截取后归并;优先方案:SQL 只 GROUP BY FD.AdmID, FD.MedinsListCodg,把 FeeOcurTime 一起取出后在 ObjectScript 侧用 $E(ts,1,10) 归并成组(与 Profit.cls 完全一致的写法)times >= dupTimes → 命中组;为每组计算 dupAmount / dupQty(用 2.4 / 2.5)SUM(DetItemFeeSumamt) 得 statistics.totalAmount(含负数退费)GROUP BY FD.EcCode → catTotal(EcCode)(分母,含负数退费;空 EcCode 归为「未分类」)dateType 走 MR 或 FD)的 COUNT(DISTINCT FD.AdmID) → statistics.visitCount;命中组涉及就诊去重计数 → dupVisitCountgroupBy 再归一层(category 用 CatCode/CatName,item 用 MedinsListCodg/ItemName),累加 totalAmount(取 SQL-3 的类型合计)、dupAmount、dupQty、dupRecordCount、dupItemCount、dupVisitCountdupRatioOfCategory(用 2.6,分母为类型合计)与 dupRatioOfTotal(分母为 statistics.totalAmount);应用层按 dupAmount 降序排序(sortIdx(amount, idx) + $ORDER(...,-1))statistics(visitCount / dupVisitCount / dupVisitRatio / dupItemCount / dupGroupCount / dupRecordCount / totalAmount / dupAmount / dupRatioOfTotal)catch(e) → ..%HandleError("重复收费汇总统计", "02010603", jsonObj, e.AsStatus());末尾 quit resultObjQueryDuplicateChargeDetail(明细查询,按组分页)Description: 重复收费明细查询(按重复组分页,同组同色号),Code: 02010604)new (jsonObj) + resultObj + try;入参校验与护栏同 3.3 / 3.4set pg = ..%GetPage(jsonObj)(缺省 pageSize 10 / currentPage 1)dupAmount 降序排序,为每个组分配稳定 colorIndex = 组序号 # 8(序号从 0 开始,跨页恒定)MAX_DUP_GROUP=2000 时截断到 2000 组,resultObj.result.message = "命中重复组过多,仅展示重复金额最高的 2000 组,请缩小查询范围"total = 命中组数;取第 [ (pg.currentPage-1)*pg.pageSize , ... ) 个组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 条未展示」)dupGroupKey(admID|YYYY-MM-DD|itemCode)、colorIndex、times、amount、dupAmount、isDup(times>=dupTimes AND DetItemFeeSumamt>=0 AND MedinsListCodg<>'')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 放大行数)$FN(x,"",2)),缺失字段置空串或 0(不出 null)statistics(与 3.13 同构,供明细 Tab 复用 KPI 卡)catch(e) → ..%HandleError("重复收费明细查询", "02010604", jsonObj, e.AsStatus());末尾 quit resultObjQueryVisitFeeDetail(单次就诊费用信息)Description: 单次就诊费用信息(明细 + 双维度汇总 + 重复分组统计),Code: 02010605)new (jsonObj) + resultObj + tryparams.admID 为空 → SetErrorResult("-1","缺少就诊号admID") 并 quitadmID / patName / medcasNo / deptName / doctorName / admDate / dscgDate / drgCode / drgName / insuType(中文用 ..%GetInsuTypeName)/ medfeeSumamt;查不到时 visit = {},不报错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(含负数退费行)summaryByCategory(EcCode/EcDesc)、summaryByItem(MedinsListCodg/MedinsListDesc),含 amount / qty / recordCount / avgPrice / ratio;空 EcCode 归「未分类」;应用层降序排序$E(feeOcurTime,1,10) + MedinsListCodg,仅 DetItemFeeSumamt>=0 且编码非空),过滤 times>=dupTimes,输出 dupGroups(date / itemCode / itemName / catName / times / qty / amount / dupAmount),按日期倒序、组内按 dupAmount 降序dupGroupKey / colorIndex(组稳定序号 # 8)/ times / amount / dupAmount / isDup;负数行 isDup=false 且不下发 colorIndexstatistics:totalAmount(全部有效明细金额,含负数)、dupAmount、dupRatio、recordCount、refundAmount(负数行金额合计)HisDataService 相关),不得写库catch(e) → ..%HandleError("查询单次就诊费用信息", "02010605", jsonObj, e.AsStatus());末尾 quit resultObj{ 与 } 数量确认配对;延时 20 秒以上复查一次(WriteAllText + UTF8Encoding($false),LF 换行、无 BOM)Do $SYSTEM.OBJ.Compile("src.DRG.Report","ck"),确认无编译错误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 与回滚 DELETEdb/register_interface_02010604.sql:同上,MethodName='QueryDuplicateChargeDetail',描述「DRG报表-重复收费明细查询」db/register_interface_02010605.sql:同上,MethodName='QueryVisitFeeDetail',描述「DRG报表-单次就诊费用信息查询」SELECT 各校验返回 1 行;重复执行验证幂等(记录数仍各为 1)statistics.visitCount 与 MR 已分组口径一致(抽查若干 admID 确认 MR.DRG 非空)dateType 三种取值(adm / discharge / fee)结果不同且正确;传入无效值按 adm 处理dupAmountMode 两种口径:excess 下 dupAmount = amount × (times-1)/times;total 下 dupAmount = amount;KPI 随之变化dupTimes=3 时仅 times>=3 的组计入;dupTimes=1 按 2 处理times,但计入 totalAmount 与类型合计分母(dupRatioOfCategory 分母含负数)EcCode/EcDesc 金额归入「未分类」,不丢失dupRatioOfCategory 与 dupRatioOfTotal 手工核算一致(两位小数,分母为 0 时置 0)errorCode='-1' 且 errorMessage 含「查询条件不足」dupGroupKey 的 colorIndex 不变total=2000 且返回 message 提示;单组 > 30 条时追加提示行admID → errorCode='-1';传不存在 admID → errorCode='0' 且 rows=[]isDup=true 且同组 colorIndex 一致;负数行 isDup=falseBS_HISFeeDetail 记录数不变)frontend/src/api/report.tsDuplicateSummaryItem、DuplicateStatistics、DuplicateDetailItem、VisitFeeDetailResult、VisitInfo、VisitFeeSummaryItem、VisitDupGroup、QueryDuplicateParamsDUP_DATE_TYPE_OPTIONS(入院日期 adm / 出院日期 discharge / 费用日期 fee)、DUP_AMOUNT_MODE_OPTIONS(超出部分 excess / 全部金额 total)、DUP_GROUP_BY_OPTIONS(收费项目类型 category / 收费项目 item)queryDuplicateChargeSummary(params, pagination) → invoke('02010603', [withHosp(params)], undefined, pagination)queryDuplicateChargeDetail(params, pagination) → invoke('02010604', [withHosp(params)], undefined, pagination)queryVisitFeeDetail(params) → invoke('02010605', [withHosp(params)])(不分页)frontend/src/pages/Report/DuplicateCharge.tsx(一个菜单两 Tab)Tabs(summary 汇总统计 / detail 明细查询)+ 单次就诊费用 ModaluseHospitalOptions,非管理员锁定)、日期口径 Select(入院日期 / 出院日期 / 费用日期)、RangePicker(locale={zhCN}、format="YYYY年MM月DD日"、dayjs.locale('zh-cn'))、重复阈值 InputNumber(默认 2,min 2 max 99)、重复金额口径 Select(默认「超出部分」)、查询 / 重置按钮Badge 显示已填条件数searched 状态:菜单打开不自动查询,点击「查询」后才随条件/机构变化自动重查;重置回到未查询态Table.Summary 合计行:金额列求和红色加粗,蓝色「合计」+ 灰色「(本页 N 条)」Segmented 切 groupBy(收费项目类型 / 收费项目),切换回到第 1 页重查onRow 按 colorIndex 取色池(DUP_BG_COLORS[colorIndex % 8],色池与 FeeInfoPanel.tsx 完全一致),重复行加「重复」Tag 与左侧色条(DUP_BAR_COLORS[colorIndex % 8]);提示行(isTip=1)灰色斜体、不参与合计CustomPagination 分页(pagination={false} + CustomPagination)pageSize=200),CSV + \uFEFF BOM、MIME application/vnd.ms-excel;charset=utf-8、.xls 后缀;优先 showSaveFilePicker(AbortError 静默返回),回落 a.download;文件名 重复收费统计_<Tab名>_<日期区间>_<时间戳>.xls,sanitize \ / : * ? " < > |,核心长度 ≤ 80;无数据(total===0)时按钮 disabledModal(宽 ≥1100)调用 02010605,展示患者基本信息 Descriptions(3 列、labelStyle={{ width: 100 }})+ 该就诊费用汇总(Segmented 切类型/项目)+ 重复分组统计 + 全部明细表(重复行同色高亮)fmtMoney;比率 toFixed(2) + %)Tooltip 说明「超出部分 = 组内总额 ×(次数−1)/次数(按均价保留一次)」;「占类型比重」「占总费用比重」列表头说明「分母为全部有效明细金额(含退费冲账)」frontend/src/routes/menuRoutes.ts:keyToPath 加 'report-duplicate-charge': '/report/duplicate-charge';menuTitleMap 加 'report-duplicate-charge': '重复收费统计'frontend/src/routes/menuRouteRegistry.tsx:import DuplicateCharge 并在 MENU_RENDERERS 注册 'report-duplicate-charge'frontend/src/context/MenuContext.tsx:「报表分析」分组 children 追加 { key: 'report-duplicate-charge', label: '重复收费统计' }npm run build 与 tsc --noEmit 均无错误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(注释)与回滚 DELETESELECT 校验落库且父级为 report;为相应角色分配菜单权限dist 到生产(nginx 8082),验证 /report/duplicate-charge 深链接可打开groupBy → Tab2 验证同色标注、按组分页、翻页色号不变 → 点「查看费用」验证下钻 → 两个 Tab 分别导出 Excel接口服务说明/09-报表服务.md:追加 02010603 / 02010604 / 02010605 完整说明(入参表、出参表、错误码、示例 JSON)接口服务说明/00-接口总览.md:接口清单「2.8 报表服务 (src.DRG.Report)」追加三条系统设计/09-报表模块设计.md:追加第 2 章「重复收费统计」(表结构与字段、接口设计、业务流程、SQL 兼容性约束、口径说明)/opsx:archive 执行归档,确认 openspec/specs/duplicate-charge-summary/spec.md、openspec/specs/duplicate-charge-detail/spec.md、openspec/specs/visit-fee-detail-query/spec.md 落位