持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第30天,点击查看活动详情
前言
在医院业务中,业务流程是非常复杂的,不同部门之间,不同角色之间也经常存在需要沟通和协作的事项
比如医生开完医嘱之后,护士需要核对才能继续后续的流程
比如某个患者检查结果有危机值了要通知到医生及时处理
这些流程从管理上来说要各种手段可以进行通知,比如电话,比如微信等等
对于医院信息化系统来说,也需要有这种消息通讯机制,能满足院内系统,科室之间,角色、工作组、用户等多维度的消息传输,做到消息的及时通知,接受者在收到消息的处理过程,处理记录也要能够进行查询和可追溯
场景
危急值
按照业务分类,有如下业务消息类型
EMR相关消息:
1、系统通知(系统升级、新功能提示)
2、科室排班消息。
3、院内通知
CPOE相关消息
1、医嘱提交时,需要通知护士对此次提交的医嘱进行核对。
2、检查、检验结果回报时,需要提醒医生。
3、检查、检验结果异常时,需要提醒医生。
4、检查、检验预约消息提醒
5、护士核对医嘱异常时,需要通知护士。
6、特殊类的医嘱下达时(病情、膳食、护理等医嘱),需要通知、提醒护士。
7、会诊医嘱下达时,需要通知相关的会诊医嘱。
PIS相关消息
1、药师写完用药建议通知医生
2、医生接受或拒绝用药建议通知药师
3、医生写完药学咨询通知药师
4、药师回复药学咨询通知医生
CP相关消息
1、路径中每个阶段延期后需自动提醒医生进行操作
SDE病历相关消息:
- 三级阅改提交或退回时,需要通知审核人进行审核。
- 病历书写消息提醒,患者入院后,按照国家病历书写规范提醒医生需要书写的病历。
- 质控人员登记问题后,需要产生通知医生进行处理的消息。
- 终末质控不合格质控人员进行退回处理后,需通知医生进行病历修改的消息。
- 医生质控人员登记的问题进行回复后,需要通知质控人员问题处理情况。
\
消息的分发对象:
1、分发到指定的单个人员
2、分发到指定科室所有人员
3、分发到指定的病区所有人员
协议选择
现有技术客户端及时获得服务器更新,有如下方式
AJAX
AJAX基于Javascript的 XmlHttpRequest 对象。它是Asynchronous Javascript和XML的缩写形式
XmlHttpRequest 对象允许执行Javascript而无需重新加载完整的网页
- 它们需要发送HTTP标头,这使数据量更大
- 通信是半双工的
- Web服务器消耗更多资源
轮询(Polling)
轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。
这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源
也不适合获取实时信息的,客户端和服务器之间会一直进行连接,每隔一段时间就询问一次。客户端会轮询,有没有新消息。这种方式连接数会很多,一个接受,一个发送。而且每次发送请求都会有 HTTP 的 Header,会很耗流量,也会消耗 CPU 的利用率。
长轮询(Long Polling)
客户端和服务器保持连接处于活动状态,直到获取某些数据或发生超时。如果由于某些原因导致连接丢失,则客户端可以重新开始并执行顺序请求
长轮询是对轮询的改进版,客户端发送 HTTP 给服务器之后,检查有没有新消息,如果没有新消息,就一直等待。直到有消息或者超时了,才会返回给客户端。
消息返回后,客户端再次建立连接,如此反复。这种做法在某种程度上减小了网络带宽和 CPU 利用率等问题。
这种方式也有一定的弊端,实时性不高。如果是高实时的系统,肯定不会采用这种办法。因为一个 GET 请求来回需要 2个 RTT,很可能在这段时间内,数据变化很大,客户端拿到的数据已经延后很多了。
另外,网络带宽低利用率的问题也没有从根源上解决。每个 Request 都会带相同的 Header
WebSocket
相对HTTP协议来说,是持久化协议
HTTP协议是无状态的,如果需要获取消息,需要先主动向服务器发起请求
Websocket创建连接后可以双向通信,服务器可以主动推送消息给客户端
本着实时性,开销小的原则,选择Websocket作为消息组件实现的通讯协议
整体流程
参与角色
前端
心跳流程
nginx等反向代理服务器会在一段时间后切断不活跃的连接,心跳消息用来维持客户端和服务端的长连接
心跳由客户主动向服务器发起
心跳流程本质上是一个循环,前端需要在空闲的时候或者定时发送心跳消息给服务端保持活跃的连接
重连
服务器原因或者客户端网络原因,客户端连接可能会主动断开或者被动断开,客户端需要监听断开连接的事件,并在断开后发起重连
重连流程跟客户端登录流程一致
收件箱流程
前端收件箱流程独立于实时消息处理流程,不需要参与实时消息的交互,在处理消息时调用相应的 HTTP 接口 完成站内信消息的处理
收件箱消息在登录进工作站后调用接口查询,用来展示自己已经收到的消息,对于未读的消息,需要在顶部状态栏有明确的标记提示用户
实时消息处理流程
前端需要针对不同类型的实时消息做出不同的处理,处理流程如下
对于 danger 类型的消息,用户在登录后会把此用户未处理的消息继续推送,直到此消息的状态被标记为已处理为止
后端
消息发布
消息接收
通讯协议设计
地址格式
用户登录成功后会获取到token和一些上下文信息, 服务端需要根据url中参数解析token参数,确认用户的身份
未来如果需要其他的参数也需要通过url参数进行传递
基础推送消息格式
{
'sender':'user',
'receiver':'user|group|system',
'msgId':'11111',
'cmd':"命令",
'msgType':'notice|normal|urgent|danger',
'title':'消息标题',
'content':'消息内容',
'businessId':'业务关联编号',
'businessType':'业务类型'
'time':'2021-02-01 10:00:00',
'attachment':{},
}
| 名称 | 类型 | 说明 |
|---|---|---|
| code | string | 正常 为 0请求没有此字段,只有响应有****服务器主动推送的消息没有此字段其他未应用错误具体错误见错误码表 |
| sender | string | 消息来源,消息发送者 可选项为 系统 科室 用户 |
| receiver | string | 消息接收者,可以是user或者group 可选项为系统 科室 用户 工作组 角色 |
| receiverType | string | 接收者类型 |
| id | strting | 消息ID唯一标识一条消息由服务端发起时,由服务端决定,用来唯一确定一条消息,一般是数据库的主键 |
| cmd | string | 命令 ,参考命令表 |
| msgType | string | 消息类型,参考消息类型表 |
| businessId | string | 关联业务编号 |
| businessType | string | 业务消息类型,业务接收方需要根据此类型确定 |
| title | string | 消息标题 |
| handleMode | string | 客户端消息处理行为 不区分大小写NONE 不弹窗ONLYCLOSE 弹窗显示关闭按钮CLOSEANDHANDLE 弹窗显示关闭和处理按钮HANDLE 弹窗只显示处理按钮 |
| content | string | 消息内容 |
| attachment | json object | 扩展对象客户端可以根据实际情况展示此字段的内容 |
| time | number | 消息发送时间 |
正常响应
{
'id':"111",
'code':'0',
'cmd':"请求命令字,比如auth",
'msg':'错误描述'
}
错误响应
错误响应分为两种
- 网络问题导致的问题,一般可能是网络超时,由客户端框架自行处理
- 应用错误,由正常的错误消息体现,如下
{
'code':'错误码',
'cmd':"请求命令",
'msg':'错误描述'
}
连接在断开时客户端会收到 CloseEvent事件,事件中会包含连接断开的原因 断开原因参考 CloseEvent - Web API 接口参考
命令
| 名称 | 说明 | 描述 |
|---|---|---|
| auth | 鉴权消息 | 客户端握手成功后需要发送此消息请求鉴权 |
| swprefer | 切换上下文时发送此消息 | 此消息只有鉴权成功后才能发送, |
| ping | 心跳请求 | 客户端发出,服务端响应 |
| pong | 心跳响应 | 服务端响应ping |
| push | 推送命令 | 服务端发送给客户端的消息 |
| ack | 消息确认 | 客户端发出,服务端收到消息后将对应的消息标记为已读 |
消息类型
除了客户端主动发起的心跳消息和服务端发出的notice,error,其他类型的消息都需要客户端在收到消息后针对相应的消息进行确认,即发送ack类型的消息
| 名称 | 说明 | |
|---|---|---|
| 名称 | 说明 | |
| notice | 通知消息 | 由服务端发出,消息不需要用户确认客户端会出现此消息,不要求发送确认 |
| normal | 正常消息 | 由服务端发出 |
| urgent | 紧急消息 | 由服务端发出 |
| danger | 危险消息 | 优先级最高由服务端发出 |
业务消息类型
| 业务类型编码 | 说明 | 描述 |
|---|---|---|
| OPS-UPGRADE | 升级消息 | 由devops平台发出,所有的工作站都能收到. |
| BCS-BROADCAST | 全院通知 | 由bcs平台发出, 所有在线用户都能收到, |
错误码
| 取值 | 说明 | 客户端应该采取的动作 |
|---|---|---|
| 0 | 正常消息 | 正常处理消息 |
| 403 | 鉴权失败 | 引导用户重新登录 |
| 500 | 网关错误 | 引导用户重试 |
消息说明
心跳请求
{
'cmd':"ping",
}
心跳响应
{
'code':"0",
'cmd':"pong",
}
请求鉴权消息
{
'cmd':'auth',
'X-Auth-Token':'xxx',
'systemUri':'xxx',
'X-Preference':'上下文信息'
}
上下文格式
{"X-Preference":{"systemUri":"bcs","orgId":257154,"orgCode":"200820","orgName":"住院收费室","areaId":102,"areaCode":"H0002","areaName":"主院区","instId":1,"instCode":"443","instName":"威海市立医院"}}
请求鉴权响应
{
'code':"0",
'cmd':'auth',
'msg':""
}
切换上下文消息
工作站在登录工作站并切换当前选择的上下文时需要发此消息
{
'cmd':'swprefer',
'X-Preference':'切换后的上下文'
}
切换上下文消息响应
{
'code':"0",
'cmd':'swprefer',
'X-Preference':'切换后的上下文'
}
推送(push)
{
'sender':'user',
'receiver':'user|group|system',
'id':'msgid',
'cmd':"push",
'businessId':'业务关联编号',
'businessType':'业务类型',
'handleMode':'',
'msgType':'参考消息类型表',
'title’:'title',
'content':'',
'sendTime':毫秒数,
}
确认(ack)
{
'id':'msgid',
'cmd':"ack",
}
推送升级消息
content 字段使用富文本格式,由devops平台给出,title 字段是纯文本格式
{
'sender':'devops',
'receiver':'system',
'id':'msgid',
'cmd':"push",
'businessId':**null**,
'businessType':'OPS-UPGRADE',
'handleMode':'ONLYCLOSE',
'msgType':'NORMAL',
'title’:'系统将于2022-01-01 14:00 进行例行维护,届时可能会出现短暂的连接超时或者断开,请做好相关的数据保存,以免数据丢失',
'content':'富文本测试<h2>住院医生站</h2><p>新增了医嘱开立</p>',
'sendTime':毫秒数,
}
消息路由设计
在实际的消息流程中,接收者是由客户端指定指定的,比如发给科室的消息必须在代码中写好要发送的接收者,流程也是确定的,如果涉及到未来需要修改流程,则需要修改代码重新发布版本,重新部署,不灵活
新的设计需要做到可以动态增减某种消息类型的目标接收者,不同的医院同类型的消息可以有不同的接收人.修改这些规则时需要做到不需要修改代码,而动态生效
此设计基于规则编码实现,规则编码是一个逻辑上的概念,用来代表某一类接收者,比如科室,具体的人员等. 可以理解为获取某类接收者的函数
编码采用如下命名格式:
由业务 + 下划线 + 消息编码组成, 如形式: HIS_SEND_ORG 消息编码是可以清楚的表达这个规则要做的事情
具体的业务系统需要根据编码实现具体的规则接口,在调用接口发送消息时,需要设置对应规则的输入参数,这个参数会在下一步计算目标接收者时传递给对应的规则,框架会在发送时会自动调用这些规则实现类设置发送的目标接收者,最终发送消息
规则接口定义
调用流程
- 业务实现对应的规则
- 配置对应的业务消息各个接收角色使用的规则编码
- 业务指定消息类型并设置输入参数,调用消息发送方法
- 框架根据消息类型查询规则,以对应输入参数调用规则实现,获取对应角色的接收者并设置到发送目标中
- 发送消息
当然业务类型也可以不定义路由,这种消息就需要业务自行设置消息接收者