简介:面向医疗信息化开发者与 HIS 系统实施人员的 Caché 数据库开发文档合集,聚焦 InterSystems Caché 在医疗业务场景中的安装配置、数据建模、接口开发与运维安全等主题,适合希望从入门过渡到实战的工程师查漏补缺。压缩包共 145 个文件,以 143 份 PDF 技术文档为主体,辅以 1 个 HTML 入口页和 1 张 JPG 示意图,整体约 77.18MB,目录按文档主题归集,便于按模块检索。已有 1036 人学习下载。内容覆盖基础教程、数据模型设计指南、MUMPS 编程语法与最佳实践、API 参考、高可用与灾难恢复策略、权限与数据安全管理,以及报表分析工具说明和示例项目,读者可据此理解 Caché 多维数据模型在实时医疗数据读写、集群切换和系统集成中的落地方式,并对照文档搭建开发环境、梳理接口调用与排错思路。
1. Caché 撑起的 HIS:一张门诊挂号单背后的数据库选型
早上七点四十,门诊挂号窗口刚开,一台自助机卡了两三秒,后面的队伍立刻往前挤。这类现场问题,最后往往落到同一个地方——HIS 的数据库。很多运行了十年以上的医院信息系统,底层用的不是常见的关系库,而是 Caché 这种后关系型数据库:数据以多维稀疏数组(Global)存储,同时又能用 SQL 和对象两种方式访问,读写路径短、单机吞吐高,这也是它当年能在门诊高峰撑住的原因。这份资料包就是围绕 Caché + HIS 的开发文档,覆盖部署、数据字典、ObjectScript 编码规范、接口对接和运维巡检,适合刚接手 HIS 二次开发的程序员、需要排查线上问题的运维,以及想搞懂 Global 结构再去改业务逻辑的人。它不教你数据库原理,它给你一套能对着改的现场资料。
2. 资料包全景:文档目录结构与它覆盖的 HIS 业务模块
拿到一份资料,先别急着翻代码。我一般先看目录,目录结构基本能反映这套系统的分层思路:哪些是部署层的东西,哪些是数据层的东西,哪些是给二次开发看的接口约定。这份 Caché HIS 文档资料的目录大致长这样:
HIS-Cache-Docs/ ├─ 01-部署与安装/ 单机部署、双机热备与 ECP、升级回滚 checklist ├─ 02-数据字典/ 核心 Global 清单、字段与索引对照、字典码表 ├─ 03-开发规范/ ObjectScript 编码规范、包结构、事务与锁约定 ├─ 04-接口文档/ 医保结算、LIS/PACS 对接、REST 与 WebService 入口 ├─ 05-运维手册/ 日常巡检、备份与 Journal 恢复、告警处理 └─ 06-示例代码/ HIS.Biz 业务类、HIS.Web 页面类、demo-data 导出这个划分不是随便切的。01和05解决「跑起来」和「别跑挂」,02和03解决「看懂代码」和「按规矩改代码」,04解决「和外部系统说话」,06是给你练手的可导入示例。真正上手时,阅读顺序建议反过来:先看02的数据字典认表,再看03的规范认风格,然后06导入跑通,最后回头补01和05。
2.1 业务模块与数据归属:HIS 到底分了哪几块
HIS 不是一个单体功能,它是十几个业务域拼起来的。文档里按模块拆得很清楚,每个模块对应一组 Global 和一组类:
| 业务模块 | 核心数据对象 | 典型操作 |
|---|---|---|
| 门诊挂号 | 患者、就诊、排班 | 建就诊号、分诊、退号 |
| 门急诊收费 | 收费单、明细、发票 | 划价、结算、退费、日结 |
| 药房药库 | 库存、出入库、处方 | 发药、盘点、调拨 |
| 住院管理 | 住院登记、床位、预交金 | 入出院、转科、欠费控制 |
| 医嘱处理 | 长期/临时医嘱、执行记录 | 开立、审核、执行、停止 |
| 医保结算 | 结算流水、上传报文 | 预结算、正式结算、对账 |
| 接口层 | 消息队列、日志 | LIS/PACS/EMR 数据交换 |
理解这张表的价值在于:改任何一个功能之前,你得先知道它动的是哪几个 Global。门诊挂号看似简单,实际上同时写就诊主表、排班余量、挂号流水三处数据,任何一处漏写,第二天对账就会不平。文档里每个模块都附了「涉及 Global 清单」,这是最该先背下来的部分,比背代码有用。
2.2 技术栈与依赖关系:ObjectScript、CSP 和 SQL 三条腿
Caché 的特点是同一份数据有三种访问姿势,HIS 这套代码三种都用上了,这也是新手最容易绕晕的地方。
第一种是 ObjectScript 直接操作 Global。业务里对性能敏感的部分,比如挂号取号、发药扣库存,很多是直接读写 Global 的,路径最短。第二种是对象访问,%Persistent类提供%New()、%Save()、%OpenId()这套 API,索引和事务由框架维护,适合业务逻辑复杂的场景。第三种是 SQL,报表、查询、对外接口基本都走 SQL,因为写起来直观,也方便 BI 工具直接连。
三条腿并存带来的直接后果是:数据一致性靠人守。用对象层保存,索引会自动更新;直接Set ^HIS.ADM(id)=...,索引不动。这个坑后面第 5 章会专门讲。
技术栈上还有两块要留意。CSP 是页面层,类继承%CSP.Page,OnPage()里输出 HTML,很多老 HIS 的门诊工作站界面就是这么写的,不是前后端分离。集成层如果用了 Caché 自带的消息组件,接口报文走的是命名空间内的消息类,排查问题时不能只看应用日志,还得去消息日志里翻。
提示:资料包里的示例代码默认命名空间叫
HISAPP,如果你本地建成了别的名字,导入类之前先改包映射,否则类能编译但找不到数据。
3. 数据模型拆解:Global 多维数组怎么撑起 HIS 的表结构
这一章是整份资料里含金量最高的部分。你打开管理门户看到的是「表」,但在 Caché 里,表只是 Global 的一层皮。搞不清这层映射关系,调试时就会陷入「SQL 查得到、终端看不到」或者反过来的黑匣子状态。
3.1 Global 存储结构:一个就诊记录在磁盘上长什么样
Caché 的 Global 是带下标的持久变量,下标可以是字符串也可以是数字,天然支持多层嵌套,稀疏存储不占空间。HIS 里最典型的设计是以「就诊」为主线:
; 就诊主索引:就诊号 -> RowId,用于按号快速定位 Set ^HIS.ADM(0,"IDX",AdmNo) = RowId ; 就诊主记录:RowId -> 定长字段列表 ; $LB 打包:患者ID、就诊日期、科室、医生、状态 Set ^HIS.ADM(RowId) = $LB(PatientID, +$H, DeptID, DocID, "R") ; 就诊下的医嘱明细:RowId + 序号 -> 医嘱内容 Set ^HIS.ADM(RowId,"ORD",1) = $LB("XY01", 2, 12.50) Set ^HIS.ADM(RowId,"ORD",2) = $LB("XY07", 1, 36.00)$LB是 list build,把多个值打包成一个字符串存进单个节点,读取时用$LI(^HIS.ADM(RowId), 3)取第 3 个元素。这是 Caché 里非常常见的手法,好处是一条记录一次磁盘 IO 就能取全,坏处是字段定长、加字段要改所有读写点。
参数说明:第一层下标0是保留的索引区,业务数据从1开始;"IDX"是纯字符串下标,和数字下标混用没问题;"ORD"是子节点,表示一对多关系。这种「主表节点 + 子表节点」的树形结构,就是 Caché 里表达关系模型的标准做法,SQL 引擎在背后也是这么翻译的。
3.2 从 Global 到 SQL 表:映射规则和索引怎么建
SQL 层看到的HIS_Biz.Admission表,其实是上面那些 Global 的一个投影。Caché 通过类定义的 Storage 段来描述映射,日常开发不用手写 Storage,定义属性和索引,编译时自动生成。关键是索引:
Class HIS.Biz.Admission Extends %Persistent { Property AdmNo As %String(MAXLEN = 20) [ Required ]; Property PatientID As %String(MAXLEN = 30) [ Required ]; Property AdmDate As %Date; Property DeptID As %String(MAXLEN = 10); Property Status As %String(MAXLEN = 2) [ InitialExpression = "R" ]; Index AdmNoIdx On AdmNo [ Unique ]; Index PatDateIdx On (PatientID, AdmDate); Index DeptDateIdx On (DeptID, AdmDate) [ Type = bitmap ]; }Index定义会在 Global 里额外生成一组索引节点,比如^HIS.Biz.AdmissionI("PatDateIdx", PatientID, AdmDate, RowId)。所以一条记录写下去,实际磁盘上动了主数据和三份索引,共四个位置。Unique保证就诊号不重;bitmap位图索引适合DeptID这种低基数字段,按科室统计当天挂号量时走位图比走普通索引快一个量级。
参数要点:MAXLEN在 Caché 里按字符算,中文姓名给 30 够用;InitialExpression只在对象新建时生效,直接写 Global 不会触发;[ Required ]是对象层校验,SQL 层插入时如果字段非空约束没建,仍然可能写进空值。
3.3 一条挂号落库的完整链路:从终端到事务提交
光看结构没感觉,看一段真实业务代码。挂号的核心动作是:校验患者、生成就诊号、写主记录、写流水,全程一个事务。
ClassMethod Register(pPatientID As %String, pDeptID As %String, pDocID As %String, ByRef pAdmNo As %String) As %Status { Set sts = $$$OK, pAdmNo = "" TSTART Try { ; 1. 患者必须存在,否则直接抛异常回滚 If '##class(HIS.Biz.Patient).%ExistsId(pPatientID) { Set sts = $$$ERROR($$$GeneralError, "患者不存在:"_pPatientID) Throw ##class(%Exception.StatusException).CreateFromStatus(sts) } ; 2. 生成就诊号:日期 + 当日 6 位流水,$INCREMENT 是原子自增 Set pAdmNo = $ZDATE(+$H, 8)_$TRANSLATE($J($INCREMENT(^HIS.ADM.CNT(+$H)), 6), " ", "0") ; 3. 落主记录 Set adm = ##class(HIS.Biz.Admission).%New() Set adm.AdmNo = pAdmNo Set adm.PatientID = pPatientID Set adm.DeptID = pDeptID Set adm.DocID = pDocID Set adm.AdmDate = +$H Set adm.AdmTime = $PIECE($H, ",", 2) Set adm.Status = "R" Set sts = adm.%Save() If $$$ISERR(sts) Throw ##class(%Exception.StatusException).CreateFromStatus(sts) TCOMMIT } Catch ex { TROLLBACK Set sts = ex.AsStatus() } Quit sts }逐段说。TSTART到TCOMMIT之间是一个事务,Catch里必须TROLLBACK,否则事务挂起会污染后续请求,这是 ObjectScript 里最常见的翻车点。$INCREMENT对 Global 做原子自增,并发挂号不会拿到重复流水号,比「先查再加一再写」安全得多。+$H是 Caché 的 horolog 日期(从某基准日起的天数),$PIECE($H, ",", 2)取当天秒数,两者拼接就是完整时间戳。%Save()内部同时维护主记录和全部索引。
调用和验证:
Set sts = ##class(HIS.Biz.Admission).Register("P000123", "D01", "DOC07", .admNo) Write "status=", $$$ISOK(sts), " admNo=", admNo, ! Write $LI(^HIS.ADM(1), 3), ! ; 取第 1 条记录的科室如果status=0,用Do $SYSTEM.Status.DisplayError(sts)打出具体错误。第三个参数.admNo是引用传递,前面必须加点号,漏了点号拿不到返回值,这个细节坑过不少人。
4. 环境落地:在本机把 Caché + HIS 示例库跑起来
文档看得再熟,不跑一遍永远不知道自己卡在哪。这一章按我这边的习惯,把从零到能调业务代码的步骤走一遍。整套东西对机器要求不高,普通开发本 8G 内存够用,安装包和示例代码都在资料包的01和06目录里。
4.1 安装与命名空间划分:把数据和代码分开放
安装过程没什么可说的,一路下一步,两个地方要留意:一是安装类型选「开发」,会带上编译器和终端工具;二是字符集选 Unicode,选了别的字符集,后面中文姓名和地址会出现截断,改起来只能重装。
装完先做数据库和命名空间规划,别把业务表扔在USER里。我一般建两个数据库:HISDAT存数据(CACHE.DAT),HISCODE存例程和类定义,然后建一个命名空间HISAPP,把两者映射进去。这样做的好处是备份数据不影响代码,导出代码不影响数据。
# Windows 下启动实例并进入终端 ccontrol start CACHE csession CACHE -U HISAPP进终端后用Write $NAMESPACE确认当前命名空间,用Write $SYSTEM.Version.GetEncoding()确认字符集。这两条我每次搭新环境都敲一遍,血泪经验:有一次连错环境调了半天,最后发现是在%SYS里改数据。
4.2 导入示例代码并跑通第一个查询
资料包里的demo-data.xml是类导出文件,用$SYSTEM.OBJ.Load导入:
; 导入整个目录的类定义,/compile 表示导入后编译 Set sts = $SYSTEM.OBJ.LoadDir("D:\his\src", "ck", .err, 1) Write "import status=", $$$ISOK(sts), !第一个参数是目录,第二个"ck"表示编译并保留源码,.err收集错误,最后一个1表示递归子目录。导入报错先看.err数组,多半是包名对不上或者依赖类缺失。
然后跑一个只读查询验证数据通不通:
Set sql = "SELECT AdmNo, PatientID, DeptID FROM HIS_Biz.Admission WHERE AdmDate = ? AND Status = ?" Set stmt = ##class(%SQL.Statement).%New() Set stmt.%SelectMode = 2 ; 2 = 只读游标,别用默认值 Set sts = stmt.%Prepare(sql) If $$$ISERR(sts) { Write $SYSTEM.Status.DisplayError(sts), ! Quit } Set rs = stmt.%Execute(+$H, "R") While rs.%Next() { Write rs.AdmNo, " | ", rs.PatientID, " | ", rs.DeptID, ! } Write "rowcount=", rs.%ROWCOUNT, !%SelectMode = 2表示逻辑模式,日期按 horolog 数字返回,避免格式转换踩坑;%Execute的参数按问号顺序传;%Next()返回 0 时循环结束,%ROWCOUNT是总行数。这套 API 和 JDBC 很像,从 Java 转过来的话半天就能适应。
页面层再验证一次。建个 CSP 类,能打开就说明 Web 网关通了:
Class HIS.Web.RegList Extends %CSP.Page { ClassMethod OnPage() As %Status { Write "<table border=1>" Set rs = ##class(%SQL.Statement).%ExecDirect(, "SELECT TOP 20 * FROM HIS_Biz.Admission ORDER BY AdmDate DESC") While rs.%Next() { Write "<tr><td>", rs.AdmNo, "</td><td>", rs.DeptID, "</td></tr>" } Write "</table>" Quit $$$OK }访问地址是http://localhost:57772/csp/his/HIS.Web.RegList.cls,端口以安装时配置为准。打不开先查 IIS/Apache 网关配置,再查类有没有编译通过,最后看%SYS里的 Web 应用路径是不是指到了HISAPP命名空间。
注意:示例库里的数据是脱敏造出来的,
AdmDate用的是当天 horolog 值,如果查询时传的是字符串日期,结果一定为空,别急着怀疑 SQL 写错了。
5. 避坑与排查:Caché 跑 HIS 时最容易翻车的五个地方
这一章按「现象 → 原因 → 解决」写,都是我在现场真碰过的。Caché 的问题有个共同特点:症状出现在上层,根因往往在数据层或者部署层,不熟的话很容易在应用代码里空转。
5.1 数据一致性类:SQL 查不到终端里明明有的数据
现象:终端里Write ^HIS.ADM(100)有完整记录,SELECT却查不出来,报表当天少统计了一批患者。
原因:有人为了图快,直接Set ^HIS.ADM(100)=...写 Global,绕过了%Save()。主数据是有了,但^HIS.Biz.AdmissionI("PatDateIdx", ...)这些索引节点没写,SQL 走索引查自然查不到。批量导数据时这个坑几乎必踩。
解决:立刻重建索引,Set sts = ##class(HIS.Biz.Admission).%BuildIndices(),大表加$SYSTEM.SQL.SetOption控制批大小,避免锁表。根治办法是约定:业务代码一律走对象层或 SQL,确实需要裸写 Global 的场景(比如日志、计数),写完必须同步索引,并在03-开发规范里登记。
5.2 排序类:$ORDER取「最新一条」取错了
现象:按流水号倒序取当日最大号,结果取到了 9 号,实际上已经发到 100 号。
原因:Global 下标是字符串排序,"100" < "9"。$ORDER(^HIS.ADM.CNT(""), -1)倒序遍历拿到的是字典序最大的那个,不是数值最大的。
解决:要么用$INCREMENT自己在计数节点上维护,直接取值不做遍历;要么把下标补齐成定长,$J(seq, 10)生成"0000000100";要么这种统计直接交给 SQL 层ORDER BY。我在03-开发规范里加了一条硬规矩:所有参与排序的数字下标,一律补零定长,谁破例谁请客。
5.3 并发类:高峰期偶发<LOCK>错误
现象:门诊高峰,挂号接口偶发 500,日志里是<LOCK>报错,重试一次又能过。
原因:两个可能。一是事务里夹了外部调用,医保预结算接口慢的时候,事务持锁几秒,后面排队的请求直接超时;二是多个方法的加锁顺序不一致,A 先锁就诊再锁收费,B 反过来,形成死锁。
解决:外部调用挪到TSTART之前做完,事务里只保留纯数据库操作;加锁统一顺序,按「患者 → 就诊 → 费用」的层级加,禁止反向;给锁设置等待上限LOCK +^HIS.ADM(id):5,超时就返回友好提示而不是挂死。改完之后,高峰期<LOCK>从每天十几次降到零。
5.4 环境类:脚本跑到了生产库上
现象:测试环境执行了一个批量修复脚本,生产环境的数据跟着变了。
原因:Global 映射配错,或者脚本执行前没切命名空间,默认落在了USER,而USER又被映射到了生产库的 Global。
解决:脚本第一行强制ZN "HISAPP"并断言If $NAMESPACE'="HISAPP" Quit;运维账号按环境拆分,生产库给脚本账号只读权限,需要写入的操作必须走审批工单。这条不是技术问题,是流程问题,但出一次事的代价比写十条规范都大。
5.5 字符集类:中文姓名变问号或被截断
现象:新增患者保存成功,查询出来姓名是问号,或者报<MAXLEN>超长。
原因:安装时字符集选了非 Unicode;或者客户端连接串没指定 UTF-8,驱动按本地编码转换,中文一个字符被当成多字节截断。
解决:先用Write $SYSTEM.Version.GetEncoding()确认服务端编码,再用Write $LENGTH("张三")验证是按字符还是按字节计数(正常应该返回 2)。客户端 JDBC 连接串补上字符集参数。如果服务端本身装错了,只能重装实例再导数据,所以安装那一步别图省事。
提示:这五类问题在资料包的
05-运维手册/常见告警处理.md里都有对应的巡检命令,出问题先跑一遍巡检脚本,比人肉翻日志快得多。
6. 二次开发:从读文档到改代码的几条实操路径
资料看得差不多,接下去就是真改代码了。二次开发最容易犯的错是「一上来就动核心表结构」,HIS 的表结构牵着收费、医保、报表一大片,加字段之前先想清楚这个字段会不会进结算报文。我的做法是先做只读改造,再碰写入逻辑。
第一条路径是加查询,不动数据。这类需求最多,比如「按科室和日期统计挂号量」。做法是加一个只读类方法,走 SQL,必要的时候补位图索引:
Class HIS.Biz.Admission Extends %Persistent { Index DeptDateIdx On (DeptID, AdmDate) [ Type = bitmap ]; ClassMethod StatByDept(pDate As %Integer) As %Status { Set sql = "SELECT DeptID, COUNT(*) AS Cnt FROM HIS_Biz.Admission "_ "WHERE AdmDate = ? AND Status <> 'C' GROUP BY DeptID" Set rs = ##class(%SQL.Statement).%ExecDirect(, sql, pDate) While rs.%Next() { Write rs.DeptID, ":", rs.Cnt, ! } Quit $$$OK } }位图索引在低基数字段上效果好,但要注意:频繁增删的表上大量位图索引会带来锁竞争,门诊挂号表写入量大,索引数量控制在三个以内,多了反而拖慢%Save()。
第二条路径是改业务流程。比如挂号要加「是否复诊」标记。步骤是:类定义加属性 → 编译 → 老数据兼容(属性给默认值InitialExpression)→ 改Register()方法签名 → 同步改调用方 → 跑回归。千万别直接改 Global 结构,加了字段不补历史数据,$LI取到空值会在下游炸出一堆<NULL VALUE>。
第三条路径是接口对接。资料包04-接口文档里给了报文格式和重试约定,改造时重点看两点:接口失败要不要回滚本地事务(一般不要,落本地消息表后异步重试),以及幂等键用什么(HIS 里通常用就诊号 + 业务类型)。
验证方法我固定走三步:先在终端里用%SQL.Statement跑一遍 SQL 看结果集;再写一个只读 CSP 页面肉眼核对;最后用示例库造 1000 条并发数据跑压测,看<LOCK>和响应时间。三步都过才算改完。
踩过的坑里最贵的一次,是给一张百万级的费用明细表加了个普通索引,白天业务高峰直接锁表十几分钟被投诉。从那以后我每次加索引,都强制先在测试库用同等数据量跑一遍%BuildIndices(),记录耗时,超过阈值就改到夜间窗口做,并且提前把回滚脚本准备好。这套习惯后来救过我好几次。希望帮到你。
本文还有配套的精品资源,点击获取