2. 门诊预约业务建模
门诊预约系统的核心难点不在界面,而在于“号源”这一稀缺资源如何被正确地分配、占用、释放与追溯。要把它做对,第一步不是写代码,而是把业务中的关键对象及其关系梳理成一张可被数据库和程序共同理解的实体关系模型。本节围绕号源、科室、医生、时段、患者、预约规则与业务约束七个对象展开,重点讲清它们之间的基数关系、约束条件和落地时的字段设计。
一、先分清“资源”与“事件”两类实体
在预约业务中,实体可以分成两类。第一类是资源型实体:科室、医生、排班时段、号源。它们描述“能提供什么服务、在什么时间提供、提供多少”。第二类是事件型实体:患者、预约记录。它们描述“谁在什么时候占用了哪个号源”。区分这两类非常重要,因为资源型实体的数量是有限的、需要被保护的,而事件型实体的数量是增长的、需要被审计的。
号源(Slot)是连接两类实体的枢纽。一个号源本质上是一个“可被占用的时间-医生-序号”三元组,例如“2025-06-10 上午 心内科 张医生 第 7 号”。它必须同时绑定排班时段和医生,并携带状态字段。号源不是静态数据,它有明确的生命周期:可预约 → 已锁定 → 已预约 → 已就诊 / 已取消 / 爽约。状态流转必须由预约规则驱动,而不是由前端随意修改。
二、实体关系与基数
科室与医生是典型的一对多关系:一个科室有多名医生,一名医生通常归属一个主执业科室(多点执业场景下可扩展为多对多,但初版建模建议先按一对多收敛)。医生与排班时段是一对多:一名医生可以在不同日期、不同午别拥有多个排班。排班时段与号源是一对多:一个时段按容量拆分成若干号源。患者与预约记录是一对多:一名患者可以有多条预约,但同一时段同一科室通常只允许一条有效预约。预约记录与号源是一对一:一条有效预约占用且仅占用一个号源。
下面这张实体关系图展示了七个对象之间的连接方向与基数,其中号源处于中心位置,规则与约束以“作用点”的形式标注在关系线上。
三、关键业务约束的落地方式
实体关系只是骨架,真正决定系统是否可用的是约束。以下四条约束在门诊预约中几乎必然出现,需要在数据库层和业务层同时表达。
约束一:号源唯一占用。一个号源在同一时刻只能被一条有效预约占用。实现方式是在号源表上设置状态字段,并在预约表上对 slot_id 建立唯一索引(仅对状态为“已预约”的记录生效,可用部分索引或应用层加锁)。并发抢号时,推荐使用“先锁定号源再创建预约”的两阶段方式,锁定超时(例如 15 分钟)自动释放。
约束二:患者防重复预约。同一患者在同一科室、同一午别内不允许存在两条有效预约。这条约束不能只靠前端提示,必须在写入时校验。示例判断逻辑如下:
-- 校验:同一患者、同一科室、同一日期、同一午别是否已有有效预约
SELECT COUNT(*) FROM appointment a
JOIN slot s ON a.slot_id = s.slot_id
WHERE a.patient_id = :patientId
AND s.dept_id = :deptId
AND s.slot_date = :slotDate
AND s.period = :period -- AM / PM
AND a.status IN ('BOOKED','CHECKED_IN');
-- 返回 > 0 则拒绝本次预约
约束三:取消时限。多数医院规定就诊前若干小时可自助取消,超时后需到窗口处理。这要求预约记录保存 cancel_deadline,该值由排班时段的开始时间减去规则配置的提前量计算得到。取消后号源状态回退为“可预约”,并记录释放时间,供后续患者重新占用。
约束四:实名与限号。患者实体必须携带证件类型与证件号,且证件号在院内唯一。限号则体现在排班时段的容量字段上:时段容量决定可生成号源的数量,号源数量一旦生成,不应在预约过程中被随意增减,否则会破坏已占用号源的序号连续性。
四、字段设计示例
把上述关系落到表结构时,建议至少保留以下核心字段。号源表是重点,它需要同时承载“属于谁、属于哪个时段、当前状态、被谁锁定”四类信息。
| 实体 | 核心字段 | 说明 |
|---|---|---|
| 科室 | dept_id, dept_name, dept_type | dept_type 区分普通/专家/专病 |
| 医生 | doctor_id, dept_id, title, reg_fee | reg_fee 用于挂号费计算 |
| 排班时段 | schedule_id, doctor_id, slot_date, period, capacity | period 取 AM/PM,capacity 为号源上限 |
| 号源 | slot_id, schedule_id, doctor_id, seq_no, status, lock_expire_at | status 驱动生命周期,lock_expire_at 控制锁定超时 |
| 患者 | patient_id, id_type, id_no, phone | id_no 唯一,用于实名校验 |
| 预约记录 | appt_id, patient_id, slot_id, status, cancel_deadline | slot_id 唯一索引防重复占用 |
五、从模型到可运行逻辑的检查点
完成实体梳理后,可以用三个问题检验模型是否可用。第一,给定一个号源 ID,能否在一条查询中还原出它的科室、医生、日期、午别和当前状态?第二,给定一名患者和一次预约请求,能否在不访问前端的情况下独立判断是否违反防重复、实名和取消时限约束?第三,当一次预约被取消时,号源状态、预约状态和释放时间是否能在同一事务内完成更新?如果这三个问题都能用清晰的表结构和约束回答,说明实体关系梳理已经达到可进入接口设计与并发控制阶段的程度。
需要强调的是,号源的状态流转必须由服务端统一驱动,任何绕过状态机的直接写库都会破坏“唯一占用”这一根本约束。把规则显式地建模为约束、把约束落到字段和索引上,是门诊预约系统稳定运行的基础。