design.md 17 KB

Context

现状

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 的单行成本拆解:

  1. &sql(SELECT ID INTO :feeExistsID FROM BS_HISFeeDetail WHERE Adm_Dr=:admRefID AND FeedetlSn=:feedetlSn) —— hisDataService.cls:735,数万次 SQL 往返。
  2. ##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 个索引。
  3. feeItem.%ToJSON() —— hisDataService.cls:802,51 字段全量序列化,写入 FeeDetailJson。
  4. 每次 %Save 都是独立隐式事务 → 每行一次 journal 提交。

实测基准:10 条就诊 = 681 条费用明细;折算 50–100 ms/行。生产单批次为数万行,耗时 20–50 分钟量级。

约束

  • 前端尽量不改(用户要求);本次仅允许 DataSync.tsx 小幅改造以支持进度轮询。
  • 02010067 同步模式返回结构保持原样(用户在方案确认中明确选择"暂不改,保持返回结构原样")。
  • 超时表现为 504 Gateway Timeout,且链路中存在中间转发器 172.16.1.6:8082 → 172.16.2.15:8082 → IRIS,仅调大本机 Nginx 超时无法根治。
  • 后端为 InterSystems IRIS ObjectScript,落库既有工具为 src.util.operatetable。

Goals / Non-Goals

Goals:

  • 消除 504:使同步受理请求在毫秒级返回,不受数据量影响。
  • 将费用明细落库单行成本下降一个数量级(约 50–100 ms → <5 ms)。
  • 提供真实进度反馈(已处理/总数、阶段、成功/失败),替代前端按天切批的伪进度。
  • 保持 (AdmDr, FeedetlSn) 增量 Upsert 语义与 SaveInpatientFeeData 既有返回字段兼容。
  • 修复 DeviceFeeWarning 重复调用缺陷。
  • 补全 Nginx 超时/缓冲配置,并定位中间转发器超时点。

Non-Goals:

  • 不改动 02010067 同步模式(asyncFlag='N')的返回结构。
  • 不新增费用明细的前端查询页面。
  • 不改造 02010068 的调度方式(其复用 M3 成果即可受益)。
  • 不引入消息队列 / 外部中间件(受限于 IRIS 技术栈与运维条件)。
  • 不做任务历史归档与清理策略。

Decisions

D1. 异步执行机制:IRIS JOB 后台进程(而非 %SYS.Task 或调大超时)

  • 备选 A — JOB 命令启动后台进程(选定):受理请求落任务记录后立即 JOB 起一个后台进程执行取数与落库,请求毫秒级返回;进度与状态写 BS_HISSyncTask,由 02010089 查询。即时性强、参数传递灵活、与既有 SyncSchedulerTask(%SYS.Task 周期调度)职责分离互不干扰。
  • 备选 B — 复用 %SYS.Task(否决):项目已有 SyncTaskService.InitScheduleTask(02010084)注册 IRIS 原生任务,但 %SYS.Task 面向周期性调度,不适合"用户点击即时触发单次"的场景;且注册/清理繁琐、参数传递受限。
  • 备选 C — 保持同步 + 无限调大超时(否决):504 来自中间转发器而非本机 Nginx,调大无效;且数万行耗时 20–50 分钟,用户不可能等待。

并发与可靠性配套:① 同一 (hospCode, stDate, endDate, admID) 已存在 pending/running 任务时直接复用返回该 taskId(幂等去重);② 全局并发 JOB 上限;③ running 态超过阈值无进度更新则由查询侧判定为僵死并置 failed(心跳兜底)。

D2. 落库方式:预编译 SQL 循环执行(而非逐行 operatetable.Insert/Update)

  • 备选 A — %SQL.Statement 预编译 INSERT/UPDATE + 循环 %Execute(选定):一次 %Prepare、多次 %Execute,彻底消除每次调用的类定义查找(%ExistsId)、FromDynamicObj 反射映射与对象实例化/关闭开销。索引维护仍由 IRIS 完成(不可避免,但相对反射开销为小数)。
  • 备选 B — 复用 operatetable.Insert 但批量传 data(否决):该方法的 while iter.%GetNext() 支持一次传多条,可将类定义查找降到 1 次,但对象 %Save 与反射映射成本不变,收益仅约 30–40%;且 Update 路径会额外执行 ToDynamicObj + %GlobalCharacterStream 修改日志,更重。
  • 备选 C — %Persistent 对象复用 + 手动属性赋值(否决):比反射快,但仍需对象实例化与 %Save,代码量大且收益低于 A。

D3. 去重判定:按就诊预加载内存映射(而非逐行 SELECT)

按就诊执行一次 SELECT FeedetlSn, ID FROM BS_HISFeeDetail WHERE Adm_Dr = ?,将 (FeedetlSn → ID) 载入内存映射(IRIS 本地数组或 %DynamicObject),行内 O(1) 判定插入/更新,把数万次 SQL 往返降到"就诊数"次。

  • 备选 — 临时表 / 游标 JOIN(否决):复杂度显著上升,收益与内存映射相当。

内存规模可控:单就诊约 68 行,单任务数万行 → 映射条目数万级,占用 MB 量级。

D4. 事务粒度:分段提交(默认 500 行/段)

  • 备选 A — 全量单事务(否决):journal 一次性提交最快,但数万行会导致 journal 缓冲膨胀、锁持有时间过长,且单点失败全量回滚。
  • 备选 B — 分段事务(选定):每 N 行(默认 500,可配置)TCOMMIT + TSTART。journal 提交次数从 N 降至 N/500;单段失败仅回滚该段;配合任务表记录断点可实现续跑。

D5. 返回契约:新增可选异步形态,同步形态逐字不变

用户在方案确认中要求"保持返回结构原样",故:

  • asyncFlag 缺省 'N'(缺省值依次取 params.asyncFlag → BS_HISSyncConfig.AsyncFlag → 'N'),走现有同步路径,返回结构与现状完全一致(result 仍为 HIS 原始数组,不做裁剪)。
  • asyncFlag='Y' 时返回新增形态 {errorCode:'0', result:{taskId, status:'pending'}}。

  • 备选 — 统一改造成异步(否决):会破坏 SyncTaskService.ExecuteScheduledSync(自动同步)等既有后端调用方。

D6. 前端切批逻辑:移除,改由后端上报真实进度

现状 DataSync.tsx:72-87 的 splitDateRange(..., 7) 按 7 天切批串行调用,每批独立走完整链路,进度是"第几批"的伪进度。异步化后一次提交整个日期范围,由后端单任务处理并上报 processedCount/totalCount,前端轮询展示真实百分比,同时删除前端切批代码。

D7. Nginx:补全配置 + 单独排查中间转发器

异步化后受理请求毫秒级返回、轮询请求为轻查询,超时不再是主矛盾;但仍需:

  • 补全 frontend/nginx.conf 8082 两个 location 缺失的 proxy_connect_timeout、proxy_send_timeout(默认 60s)、client_max_body_size(默认 1m,同步模式下大响应体易触发)、proxy_buffering 相关参数。
  • 关键:504 大概率来自中间转发器 172.16.1.6:8082,该跳的 proxy_read_timeout 需单独核对与调大;本机 Nginx 调再大也无效。

模块设计

M1 异步任务受理与状态机

职责边界:负责任务的创建、状态流转、并发去重与进度回写;不参与具体取数与落库逻辑。

状态机:

pending → running → success   (全部成功)
                  → partial  (部分失败)
                  → failed   (整体失败/异常)

关键约定:

  • TaskID 生成规则:hospCode _ "_" _ $zd(+$h,8) _ "_" _ $p($h,",",2) _ "_" _ $r(100000),保证可读且唯一。
  • 受理时若同一 (hospCode, stDate, endDate, admID) 已存在 pending/running 任务,直接返回既有 taskId,不重复起 JOB。
  • 后台进程每处理完一个就诊(或每 500 行费用)回写一次进度;避免每行回写造成额外开销。

M2 任务进度查询(02010089)

职责边界:只读查询任务状态与进度;不触发任何同步动作。

M3 费用明细批量落库引擎

职责边界:纯粹的"给定 HIS 数据 → 落库 → 返回统计",同步与异步链路共用;不发起 HIS 请求。

优化后单行流程:

按就诊预加载 (FeedetlSn→ID) 映射
  → 逐行:查映射判定 insert/update
        → 预编译 INSERT / UPDATE %Execute
        → 每 500 行 TCOMMIT + TSTART
        → 每就诊结束回写任务进度

M4 主链路缺陷修复

职责边界:修正 GetInpatientData 中分组/预警与病案首页同步的调用缺陷。

  • 删除 hisDataService.cls:446 的无条件 DeviceFeeWarning 调用,保留 if/elseif 分支结果。
  • GetMedicalRecord 的每人次 HTTP 调用纳入异步阶段并受 synFlag 控制(语义不变)。

M5 网关与链路加固

职责边界:仅涉及 Nginx / 转发器配置,不改业务代码。

M6 前端进度化改造

职责边界: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 刷新患者列表

Risks / Trade-offs

  • JOB 并发失控,压垮 IRIS → 同参数任务幂等去重 + 全局并发上限 + 单机构串行;任务表 HospStatusIndex 支撑快速判定。
  • JOB 进程异常退出,任务卡在 running → 后台进程定期回写进度(心跳);查询侧对 running 且超过阈值无更新的任务判定僵死并置 failed。
  • 分段事务导致部分成功 → 状态区分 partial,并在 BS_HISSyncLog / LogSyncError 留痕,便于重试。
  • 预编译 SQL 绕开 operatetable 的修改日志 → 费用明细本无修改日志需求,且 FeeDetailJson 已留存原始 JSON,可接受(需在评审中确认)。
  • HIS 单次返回体过大占用 IRIS 内存 → 异步模式下响应不回传前端,内存压力由后台进程承担;需监控 IRIS 内存与 $STORAGE。
  • 生产 IRIS 是否允许 JOB 后台进程及数量上限未知 → 实施前需运维确认;若受限则回退为"%SYS.Task + 任务表轮询"形态。
  • FeeFlag / AsyncFlag 配置列在生产表与仓库 .cls 存在偏差 → 沿用既有"核对生产表实际列"的做法,加列前先比对。
  • 前端删除 7 天切批后单次范围可能很大 → 后端一次吃下并上报进度;同时保留 admID 单患者同步入口。

Migration Plan

  1. 建表与编译:创建 User.BSHISSyncTask 并编译;为 BS_HISSyncConfig 加 AsyncFlag 列并回填 'Y'。
  2. 注册接口:执行 scripts/insert_his_sync_task_interface.sql 注册 02010089。
  3. 落库内核替换(M3):先落地批量化落库,此步为纯内部实现替换,可独立上线且不影响调用方;保留旧实现为 SaveInpatientFeeDataLegacy 以便回滚。
  4. 缺陷修复(M4):删除重复 DeviceFeeWarning 调用。
  5. 异步受理(M1/M2):asyncFlag 默认 'N',先在测试机构以 'Y' 验证;生产灰度放开。
  6. 前端(M6):asyncFlag='Y' + 轮询;灰度期间可由配置切回同步。
  7. 配置(M5):补全 Nginx 参数并排查 172.16.1.6:8082 转发器超时。

回滚策略:BS_HISSyncConfig.AsyncFlag 置 'N' 即整体回到现有同步链路;M3 可单独回退到 SaveInpatientFeeDataLegacy;M4/M5 为独立小改动,可单独 revert。


Open Questions

  1. 中间转发器 172.16.1.6:8082 的 proxy_read_timeout 当前值是多少、是否可控?
  2. 生产 IRIS 是否允许 JOB 后台进程?有无 JOB 数量/资源限制?
  3. 数万行费用明细对应的就诊数规模是多少(影响内存映射规模与分段事务段大小选取)?
  4. 是否需要任务取消能力(可预留 02010090)?
  5. 异步任务完成后是否需要主动通知(当前设计为前端轮询,无推送)?