医院门诊药房系统:挂号→收费→发药→退药,一个完整的状态机
文章目录
- 医院门诊药房系统:挂号→收费→发药→退药,一个完整的状态机
-
- 一、场景
- 二、四个角色,四套操作
- 三、门诊链路:一条最长状态链
- 四、药库链路:进销存管理
- 五、住院链路:更长,但不复杂
- 六、日结:每天和钱对一次账
- 七、总结
不是 CRUD。是每一步都锁着上一步。
一、场景
一个人去医院门诊的完整路径:
挂号 → 看医生 → 医生开处方 → 收费处缴费 → 药房取药 → 走人。
如果在任何一个环节出了问题——收错费、发错药、要退药——系统要能退回去,而且要退得干净。不是因为退药就把发药记录删了,是不能删,只能冲正。
下面拆解整条链路上的状态机设计。
二、四个角色,四套操作
系统分四个功能域:
| 挂号处 | 挂号、退号、日结 |
| 收费处 | 收费、作废、退费、取消作废、日结 |
| 药房 | 发药、取消发药、退药 |
| 药库 | 入库审批、出库审批、盘点、调价、药房申领 |
每个角色的操作不是孤立的——收费锁着发药,发药锁着退药,退药锁着退费。前一个状态决定后一个能不能做。
三、门诊链路:一条最长状态链
挂号 ──→ 退号(只有未收费的挂号可退)
│
▼
收费 ──→ 作废(只有未发药的收费可作废)
│ │
│ └──→ 取消作废(作废撤销)
│
▼
发药 ──→ 取消发药
│
▼
退药 ──→ 退费(只有退药生成的处方才能退费)
每一步的约束:
| 挂号 | 无 | 创建就诊记录 |
| 退号 | 未收费 | 标记退号,不删记录 |
| 收费 | 已挂号 + 有处方 | 生成结算单,更新费用汇总 |
| 作废 | 未发药 | 费用撤销,不删记录 |
| 取消作废 | 已作废 | 恢复到已收费状态 |
| 发药 | 已收费 + 未发药 | 更新处方发药字段 + 减库存 |
| 取消发药 | 已发药 | 处方回到未发药,库存加回 |
| 退药 | 已发药 + 有药品可退 | 生成新处方(退药处方),明细插入负数,库存加回 |
| 退费 | 有退药处方 | 生成退费结算单 |
伪代码实现:
// 挂号
function 挂号(患者ID, 科室ID):
record = INSERT INTO 就诊记录(患者ID,科室ID,状态='已挂号')
return record.就诊ID
// 收费
function 收费(就诊ID, 处方列表):
rec = SELECT * FROM 就诊记录 WHERE ID=就诊ID
if rec.状态 != '已挂号': return error("未挂号或已收费")
sum = 0
for item in 处方列表:
INSERT INTO 费用明细(就诊ID, 项目, 金额, 状态='未发药')
sum += item.金额
INSERT INTO 结算单(就诊ID, 金额=sum, 状态='已收费')
UPDATE 就诊记录 SET 状态='已收费' WHERE ID=就诊ID
return "收费成功"
// 作废(只有未发药的能作废)
function 作废(就诊ID):
rec = SELECT * FROM 就诊记录 WHERE ID=就诊ID
if rec.状态 != '已收费': return error("状态不正确")
if EXISTS(SELECT 1 FROM 费用明细 WHERE 就诊ID=就诊ID AND 状态='已发药'):
return error("已有发药记录,不能作废,请走退药流程")
UPDATE 结算单 SET 状态='已作废' WHERE 就诊ID=就诊ID
UPDATE 就诊记录 SET 状态='已作废' WHERE ID=就诊ID
return "作废成功"
// 发药
function 发药(就诊ID, 药品列表):
for drug in 药品列表:
item = SELECT * FROM 费用明细 WHERE 就诊ID=就诊ID AND 项目=drug.项目
if item.状态 != '未发药': continue // 已发过的跳过
stock = SELECT 库存 FROM 药房库存 WHERE 药品ID=drug.药品ID
if stock < drug.数量: return error("库存不足")
UPDATE 药房库存 SET 库存=库存-drug.数量 WHERE 药品ID=drug.药品ID
UPDATE 费用明细 SET 状态='已发药', 发药时间=NOW() WHERE ID=item.ID
return "发药成功"
// 退药(生成冲正记录,不删原记录)
function 退药(就诊ID, 退药列表):
for drug in 退药列表:
item = SELECT * FROM 费用明细 WHERE 就诊ID=就诊ID AND 项目=drug.项目
if item.状态 != '已发药': return error("未发药,不能退")
if item.数量 + item.退药数量 >= drug.数量: return error("无可退数量")
// 生成退药处方(数量为负)
INSERT INTO 处方汇总(就诊ID, 退药标志='已退', 关联原处方号=item.处方号)
INSERT INTO 费用明细(处方号=新处方号, 项目=drug.项目, 数量=-drug.数量, 状态='已退药')
UPDATE 药房库存 SET 库存=库存+drug.数量 WHERE 药品ID=drug.药品ID
UPDATE 费用明细 SET 退药数量=退药数量+drug.数量 WHERE ID=item.ID
return "退药成功"
// 退费(只有退药处方才能退费)
function 退费(处方ID):
rec = SELECT * FROM 处方汇总 WHERE ID=处方ID
if rec.退药标志 != '已退': return error("非退药处方,不能退费")
if EXISTS(SELECT 1 FROM 结算单 WHERE 处方ID=处方ID AND 类型='退费'):
return error("已退费,不能重复")
sum = SELECT SUM(金额) FROM 费用明细 WHERE 处方ID=处方ID // 金额为负
INSERT INTO 结算单(就诊ID=rec.就诊ID, 金额=sum, 类型='退费', 状态='已退费')
return "退费成功"
关键的锁:
- 收费→发药之间:只有未发药的处方可以作废。已发药的不能作废,只能走退药。
- 发药→退药:退药不是"把发药记录删掉",是生成一条新处方,明细是负数。原处方保留,库存加回。审计可追溯。
- 退药→退费:只有退药生成的处方才能退费。不是随便一笔收费都能退。
四、药库链路:进销存管理
药库管的是整批的药,药房管的是发给患者的那几盒。
药库:入库申请 → 入库审批 → 更新库存 + 保存历史价格
药库:出库申请 → 出库审批 → 更新库存
药库:盘点录入 → 盘点确认 → 更新库存
药库:调价申请 → 调价审批 → 保存历史价格
药房申领:药房发起 → 药库确认 → 更新药房库存
入库/出库都走申请→审批两条线。审批不通过,库存不动。 调价不是直接改价格——是保存新价格,旧价格进历史表。历史价格永不删除。
五、住院链路:更长,但不复杂
住院比门诊多了一层"病区护士":
入院登记 → 缴费/押金 → 分配床位
→ 诊断 → 护理记录 → 体温单
→ 医嘱录入 → 医嘱复核 → 医嘱提交
→ 住院发药 → 医嘱执行 → 计费 → 退费
→ 出院登记 → 出院结算 → 撤销结算 → 撤销出院登记
→ 催款(押金不够时触发)
→ 停止医嘱(长期医嘱的终止)
角色分三层:
| 住院收费处 | 入院登记、缴费、出院结算 |
| 病区护士 | 分配床位、护理记录、体温单、医嘱执行、催款 |
| 医生 | 诊断、医嘱录入 |
| 药房 | 住院发药 |
医嘱复合→提交→发药→执行,这条线是住院的核心。医嘱不提交,药房看不到;药房不发药,护士不能执行。
六、日结:每天和钱对一次账
挂号处和收费处每天要做日结:
挂号日结 → 撤销日结 → 确认 → 查询
收费日结 → 撤销日结 → 确认 → 查询
日结把当天的挂号记录和收费记录快照一份,生成日结报表。报表可以撤销重做,但确认后不可再撤销。
日结不是业务操作,是财务操作。业务系统可以崩,日结数据不能丢。
七、总结
这套系统写了十几年,状态机设计定下来之后就没大改过。为什么?因为医院的业务流程是写在制度里的——挂号→收费→发药→退药,不是系统决定的,是医院管理规范决定的。系统只是忠实地把这些规则变成了代码。
几个不变的原则:
HIS 系统的复杂度不在于算法。在于规则多、角色多、不允许出错。每次看到"医院系统崩溃"的新闻,我都能感受到屏幕背后的 DBA 在经历什么。


