先说结论
门诊系统里的患者、处方、库存和结算不是四组独立 CRUD。只要模拟结算成功但库存未扣,或者一个处方被重复结算,页面看起来正常也不代表业务成立。
当前项目的模拟结算与发药链路主要依赖数据库状态和事务边界:
- 业务 ID 把患者、就诊单、处方和库存记录关联起来。
- 就诊单状态条件更新让并发确认只有一个请求能继续结算。
- 库存扣减、发药状态和结算记录在同一 Spring 事务中处理。
- 数量不合法或发药失败时中止流程并回滚状态和库存。
关系字段和 JSONB 怎么分工
患者 ID、就诊单 ID、药品编码、状态、金额等高频关联和强约束字段,适合保留在普通关系列中。它们需要清楚的类型、索引和外键语义。
过敏史、症状补充、既往史等结构可能变化、字段相对稀疏的病历内容,可以放入 PostgreSQL JSONB。这样能减少频繁改表,同时保留结构化查询能力。
JSONB 不是“所有字段都塞进去”的理由。稳定字段放进 JSONB 后,会让约束、关联和统计变得更困难。
GIN 索引适合加速 JSONB 包含、键和值检索,但它有写入和存储成本,也不是所有查询都会自动变快。是否加索引应该由真实查询条件决定,而不是看到 JSONB 就机械添加。
状态条件更新决定结算是否能继续
模拟结算以就诊单的待处理状态作为条件更新。并发请求只有一个能将该状态推进;其他请求因条件不再成立而不能重复完成结算。这是结算防重的数据库边界。
库存扣减和发药状态变更在同一个事务中完成。若校验或发药失败,抛出异常后回滚本次状态与库存变更,避免只完成一部分。
不要把其他写入场景使用的 UPSERT 泛化为当前结算的保证;本项目的并发确认依赖就诊状态条件更新,库存与发药一致性依赖同一事务。
Redis 在当前项目中的职责
Redis 用于候诊队列和若依基础缓存,不是结算发药流程的业务锁。把 Redis 当作结算并发保证会错误描述当前实现,也会掩盖数据库真正执行的状态判断。
即使其他系统在入口使用 Redis 锁,它也不能替代数据库约束和事务:
- 锁可能超时。
- 服务可能在持锁期间宕机。
- 解锁实现不当可能误删别人的锁。
- 其他写入口可能绕开 Redis。
因此结算流程仍需由数据库条件更新和事务兜住最终状态;当前项目没有以 Redis 锁保证结算防重。
事务回滚解决什么
假设一张处方包含多种药,其中一种无法发药。如果每次库存扣减各自提交,系统会留下“部分发药”的中间状态。
把结算状态、库存扣减和发药记录放进清晰的事务边界后,任一药品处理失败都应终止流程,使先前的扣减与状态修改一起回滚。仓库 README 记录的 PostgreSQL 集成测试覆盖并发确认、重复确认和失败回滚;该测试记录不等于生产医疗流程验收。
事务也有边界。跨外部服务、消息队列或第三方支付后,单库事务不再足够,需要幂等、补偿、事件状态或更完整的分布式一致性方案。当前项目并没有把自己包装成这一规模的生产医疗系统。
状态为什么不能只写“完成”
患者、就诊和库存真正串起来,需要状态能表达业务阶段。当前实现使用了简化状态;如果面向更严格的生产流程,更合理的表达应至少区分:
待缴费 -> 已缴费待发药 -> 已发药 -> 已归档
状态拆清楚后,系统才能判断哪些操作允许执行、失败应该回到哪里、重复请求是忽略还是报错。
项目边界
该项目基于 RuoYi 生态进行业务化二次开发,重点展示门诊流程、PostgreSQL 建模和一致性措施,不把通用权限框架包装为个人原创,也不宣称已经达到成熟医疗 SaaS 的生产合规水平。