02010067(src.DRG.HISData.HisDataService.GetInpatientData)负责从 HIS(code=05900014)同步住院患者的就诊、DRG 参数、诊断、手术,并依据 BS_HISSyncConfig.FeeFlag 决定是否顺带落库费用明细(feeInfo)。开启 FeeFlag='Y' 后,单次调用返回 504 Gateway Timeout。
调用链与耗时分布(以 HisDataService.cls 行号定位):
| 环节 | 位置 | 现状复杂度 |
|---|---|---|
| HIS 取数 | GetInpatientData:380-389 |
1 次 HTTP,与 FeeFlag 无关 |
| 逐就诊分组/预警 | GetInpatientData:441-446 |
存在重复调用 BUG,DRG 时算两遍 |
| 逐就诊病案首页同步 | GetInpatientData:421-431 |
每人次 1 次 HIS+DRG 平台 HTTP |
| 就诊/诊断/手术落库 | SaveInpatientData:1053+ |
每人次常数级,可接受 |
| 费用明细落库 | SaveInpatientFeeData:658-894 |
每行 1 次 SELECT + 1 次 ORM 调用 + 1 次 JSON 序列化 + 1 次独立事务 |
SaveInpatientFeeData 的单行成本拆解:
&sql(SELECT ID INTO :feeExistsID FROM BS_HISFeeDetail WHERE Adm_Dr=:admRefID AND FeedetlSn=:feedetlSn) —— hisDataService.cls:735,数万次 SQL 往返。##class(src.util.operatetable).Insert/Update —— hisDataService.cls:832/836。该方法内部(src/src/util/operatetable.cls:24/113)每次都会执行 %Library.ClassDefinition.%ExistsId(className) 类定义查找,再 tbObj.FromDynamicObj(value) 对约 57 个属性做反射映射,最后 tbObj.%Save(0) 维护 FeedetlSnIndex(唯一)、AdmDrIndex、AdmIDIndex、FeeOcurTimeIndex、DeletedIndex 共 5 个索引。feeItem.%ToJSON() —— hisDataService.cls:802,51 字段全量序列化,写入 FeeDetailJson。%Save 都是独立隐式事务 → 每行一次 journal 提交。实测基准:10 条就诊 = 681 条费用明细;折算 50–100 ms/行。生产单批次为数万行,耗时 20–50 分钟量级。
DataSync.tsx 小幅改造以支持进度轮询。02010067 同步模式返回结构保持原样(用户在方案确认中明确选择"暂不改,保持返回结构原样")。172.16.1.6:8082 → 172.16.2.15:8082 → IRIS,仅调大本机 Nginx 超时无法根治。src.util.operatetable。Goals:
(AdmDr, FeedetlSn) 增量 Upsert 语义与 SaveInpatientFeeData 既有返回字段兼容。DeviceFeeWarning 重复调用缺陷。Non-Goals:
02010067 同步模式(asyncFlag='N')的返回结构。02010068 的调度方式(其复用 M3 成果即可受益)。JOB 后台进程(而非 %SYS.Task 或调大超时)JOB 命令启动后台进程(选定):受理请求落任务记录后立即 JOB 起一个后台进程执行取数与落库,请求毫秒级返回;进度与状态写 BS_HISSyncTask,由 02010089 查询。即时性强、参数传递灵活、与既有 SyncSchedulerTask(%SYS.Task 周期调度)职责分离互不干扰。%SYS.Task(否决):项目已有 SyncTaskService.InitScheduleTask(02010084)注册 IRIS 原生任务,但 %SYS.Task 面向周期性调度,不适合"用户点击即时触发单次"的场景;且注册/清理繁琐、参数传递受限。并发与可靠性配套:① 同一 (hospCode, stDate, endDate, admID) 已存在 pending/running 任务时直接复用返回该 taskId(幂等去重);② 全局并发 JOB 上限;③ running 态超过阈值无进度更新则由查询侧判定为僵死并置 failed(心跳兜底)。
operatetable.Insert/Update)%SQL.Statement 预编译 INSERT/UPDATE + 循环 %Execute(选定):一次 %Prepare、多次 %Execute,彻底消除每次调用的类定义查找(%ExistsId)、FromDynamicObj 反射映射与对象实例化/关闭开销。索引维护仍由 IRIS 完成(不可避免,但相对反射开销为小数)。operatetable.Insert 但批量传 data(否决):该方法的 while iter.%GetNext() 支持一次传多条,可将类定义查找降到 1 次,但对象 %Save 与反射映射成本不变,收益仅约 30–40%;且 Update 路径会额外执行 ToDynamicObj + %GlobalCharacterStream 修改日志,更重。%Persistent 对象复用 + 手动属性赋值(否决):比反射快,但仍需对象实例化与 %Save,代码量大且收益低于 A。按就诊执行一次 SELECT FeedetlSn, ID FROM BS_HISFeeDetail WHERE Adm_Dr = ?,将 (FeedetlSn → ID) 载入内存映射(IRIS 本地数组或 %DynamicObject),行内 O(1) 判定插入/更新,把数万次 SQL 往返降到"就诊数"次。
内存规模可控:单就诊约 68 行,单任务数万行 → 映射条目数万级,占用 MB 量级。
TCOMMIT + TSTART。journal 提交次数从 N 降至 N/500;单段失败仅回滚该段;配合任务表记录断点可实现续跑。用户在方案确认中要求"保持返回结构原样",故:
asyncFlag 缺省 'N'(缺省值依次取 params.asyncFlag → BS_HISSyncConfig.AsyncFlag → 'N'),走现有同步路径,返回结构与现状完全一致(result 仍为 HIS 原始数组,不做裁剪)。asyncFlag='Y' 时返回新增形态 {errorCode:'0', result:{taskId, status:'pending'}}。
备选 — 统一改造成异步(否决):会破坏 SyncTaskService.ExecuteScheduledSync(自动同步)等既有后端调用方。
现状 DataSync.tsx:72-87 的 splitDateRange(..., 7) 按 7 天切批串行调用,每批独立走完整链路,进度是"第几批"的伪进度。异步化后一次提交整个日期范围,由后端单任务处理并上报 processedCount/totalCount,前端轮询展示真实百分比,同时删除前端切批代码。
异步化后受理请求毫秒级返回、轮询请求为轻查询,超时不再是主矛盾;但仍需:
frontend/nginx.conf 8082 两个 location 缺失的 proxy_connect_timeout、proxy_send_timeout(默认 60s)、client_max_body_size(默认 1m,同步模式下大响应体易触发)、proxy_buffering 相关参数。172.16.1.6:8082,该跳的 proxy_read_timeout 需单独核对与调大;本机 Nginx 调再大也无效。职责边界:负责任务的创建、状态流转、并发去重与进度回写;不参与具体取数与落库逻辑。
状态机:
pending → running → success (全部成功)
→ partial (部分失败)
→ failed (整体失败/异常)
关键约定:
TaskID 生成规则:hospCode _ "_" _ $zd(+$h,8) _ "_" _ $p($h,",",2) _ "_" _ $r(100000),保证可读且唯一。(hospCode, stDate, endDate, admID) 已存在 pending/running 任务,直接返回既有 taskId,不重复起 JOB。职责边界:只读查询任务状态与进度;不触发任何同步动作。
职责边界:纯粹的"给定 HIS 数据 → 落库 → 返回统计",同步与异步链路共用;不发起 HIS 请求。
优化后单行流程:
按就诊预加载 (FeedetlSn→ID) 映射
→ 逐行:查映射判定 insert/update
→ 预编译 INSERT / UPDATE %Execute
→ 每 500 行 TCOMMIT + TSTART
→ 每就诊结束回写任务进度
职责边界:修正 GetInpatientData 中分组/预警与病案首页同步的调用缺陷。
hisDataService.cls:446 的无条件 DeviceFeeWarning 调用,保留 if/elseif 分支结果。GetMedicalRecord 的每人次 HTTP 调用纳入异步阶段并受 synFlag 控制(语义不变)。职责边界:仅涉及 Nginx / 转发器配置,不改业务代码。
职责边界:DataSync.tsx 受理-轮询流程与 api/hisData.ts 类型/方法扩展;不涉及后端逻辑。
BS_HISSyncTask(类 User.BSHISSyncTask)| 属性 | 类型 | 说明 |
|---|---|---|
TaskID |
%String(MAXLEN=100) |
任务唯一编号 |
HospCode |
%String(MAXLEN=50) |
医疗机构编码 |
HospName |
%String(MAXLEN=200) |
医疗机构名称 |
SyncType |
%String(MAXLEN=20) |
manual / auto / fee |
StDate / EndDate |
%String(MAXLEN=10) |
同步日期范围 |
AdmID |
%String(MAXLEN=50) |
指定就诊号(可空) |
Status |
%String(MAXLEN=20) |
pending / running / success / partial / failed |
Phase |
%String(MAXLEN=30) |
fetch / saveAdm / saveFee / done |
AdmTotal / AdmProcessed |
%Integer |
就诊总数 / 已处理 |
FeeTotal / FeeProcessed |
%Integer |
费用行总数 / 已处理 |
SuccessCount / FailedCount / SkippedCount |
%Integer |
成功 / 失败 / 跳过 |
StartTime / EndTime |
%String(MAXLEN=30) |
起止时间 |
ErrorMessage |
%String(MAXLEN=4000) |
错误摘要 |
CreateDate / CreateTime / UpdateDate / UpdateTime / Deleted |
常规公共字段 | 与既有 HIS 表一致 |
索引:TaskIDIndex On TaskID [Unique]、HospStatusIndex On (HospCode, Status)、CreateDateIndex On CreateDate。
类声明须遵循项目表类规范:Extends (%Persistent, %XML.Adaptor, src.util.DynamicObject.Adapter)、SqlRowIdName = ID、SqlTableName = BS_HISSyncTask、Parameter %JSONREFERENCE = "ID"、完整 Storage 块。
BS_HISSyncConfig新增 AsyncFlag As %String(MAXLEN=1) [ InitialExpression = "Y" ]('Y' 异步受理 / 'N' 同步执行),并在 GetSyncConfig(02010079) / SaveSyncConfig(02010080) 透传。
注:仓库内
BSHISSyncConfig.cls与生产表存在字段偏差,实施时需按既有做法核对生产表实际列后再加列。
02010067 GetInpatientData(改造)入参 params[0] 新增可选字段:
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
asyncFlag |
String | 否 | 'Y' 异步受理 / 'N' 同步执行。缺省取 BS_HISSyncConfig.AsyncFlag,再缺省 'N' |
返回值:
asyncFlag='N':与现状完全一致,result 为 HIS 原始数组。asyncFlag='Y':{errorCode:'0', errorMessage:'', result:{taskId:'...', status:'pending'}}。02010089 GetSyncTaskProgress(新增)CB_MapInterface → Code='02010089', ClassName='src.DRG.HISData.Interface', MethodName='GetSyncTaskProgress', ServiceType='S'。params[0]:{taskId}。result:| 字段 | 说明 |
|---|---|
taskId / status / phase |
任务号 / 状态 / 当前阶段 |
admTotal / admProcessed |
就诊总数 / 已处理 |
feeTotal / feeProcessed |
费用行总数 / 已处理 |
successCount / failedCount / skippedCount |
计数 |
percent |
0–100 进度百分比 |
startTime / endTime / errorMessage |
时间与错误摘要 |
1. 前端「立即同步」→ 02010067 {stDate, endDate, admID, asyncFlag:'Y'}
2. 后端:并发去重检查(同参数 pending/running → 直接复用 taskId)
3. 后端:INSERT BS_HISSyncTask(status=pending, admTotal=0, feeTotal=0)
4. 后端:JOB 启动 RunSyncTaskAsync(taskId) → 立即返回 {taskId, status:'pending'}
5. 后台进程:
a. status=running, phase=fetch
b. 调 HIS 05900014 取数 → 回填 admTotal / feeTotal
c. phase=saveAdm:SaveInpatientData(就诊/DRG参数/诊断/手术),每就诊回写 admProcessed
d. phase=saveFee(FeeFlag='Y'):SaveInpatientFeeData 批量落库,每 500 行回写 feeProcessed
e. 终态:success / partial / failed,写 endTime 与 errorMessage
6. 前端:每 2s 轮询 02010089 → Progress 展示 percent / feeProcessed
7. 前端:终态后调 02010075 刷新患者列表
HospStatusIndex 支撑快速判定。partial,并在 BS_HISSyncLog / LogSyncError 留痕,便于重试。operatetable 的修改日志 → 费用明细本无修改日志需求,且 FeeDetailJson 已留存原始 JSON,可接受(需在评审中确认)。$STORAGE。JOB 后台进程及数量上限未知 → 实施前需运维确认;若受限则回退为"%SYS.Task + 任务表轮询"形态。FeeFlag / AsyncFlag 配置列在生产表与仓库 .cls 存在偏差 → 沿用既有"核对生产表实际列"的做法,加列前先比对。admID 单患者同步入口。User.BSHISSyncTask 并编译;为 BS_HISSyncConfig 加 AsyncFlag 列并回填 'Y'。scripts/insert_his_sync_task_interface.sql 注册 02010089。SaveInpatientFeeDataLegacy 以便回滚。DeviceFeeWarning 调用。asyncFlag 默认 'N',先在测试机构以 'Y' 验证;生产灰度放开。asyncFlag='Y' + 轮询;灰度期间可由配置切回同步。172.16.1.6:8082 转发器超时。回滚策略:BS_HISSyncConfig.AsyncFlag 置 'N' 即整体回到现有同步链路;M3 可单独回退到 SaveInpatientFeeDataLegacy;M4/M5 为独立小改动,可单独 revert。
172.16.1.6:8082 的 proxy_read_timeout 当前值是多少、是否可控?JOB 后台进程?有无 JOB 数量/资源限制?02010090)?