医院门诊药房系统:挂号→收费→发药→退药,一个完整的状态机
文章目录
- 医院门诊药房系统:挂号→收费→发药→退药,一个完整的状态机
- 一、场景
- 二、四个角色,四套操作
- 三、门诊链路:一条最长状态链
- 四、药库链路:进销存管理
- 五、住院链路:更长,但不复杂
- 六、日结:每天和钱对一次账
- 七、总结
不是 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 在经历什么。