简介:医院门诊预约挂号管理系统文献综述,是面向计算机相关专业毕业生撰写毕业论文或课程设计的参考文献资料。内容围绕基于SpringBoot与Vue的预约挂号系统展开,系统梳理了传统门诊“三长一短”问题、B/S与C/S架构及MIS结构特点、医生/管理员/患者三方功能划分,以及Java跨平台优势与运行速度不足、Vue在交互体验与适老化设计上的价值;并结合中国人口老龄化趋势、政府预约诊疗政策背景,分析了预约挂号系统可能存在的统一性不足、制度不严谨等问题及后续智能化升级方向。文档包含前言、国内外相关研究和结论,体系完整,可直接作为文献综述章节的写作框架,也能为系统需求分析和架构设计提供参考。压缩包内共1个doc文档,大小38KB,内容精炼便于阅读、摘录与仿写;该资料已有789人学习,适合正在准备医疗信息化方向毕业设计、开题报告或课程论文的高校学生使用。
1. 从“挂号难”到“预约挂号管理系统”:文献综述要先厘清什么
医院门诊预约挂号管理系统在工程师眼里,并不是一个简单的“挂号App”,而是一个带强约束的分布式资源分配系统。号源每天按排班生成,需要在微信、App、窗口、自助机等多个渠道之间保持同步;放号瞬间要对抗高并发抢号,日常运营还要处理退号、停诊、爽约和候补。做文献综述时最忌讳把论文题目堆成清单,关键是回答三个问题:号源这种核心资源到底怎么建模;多个人同时抢最后一个号时如何保证不超卖;患者行为数据能不能反向优化排班策略。读下来会发现,多数方案最终都能落到一套“排班驱动号源、事务控制扣减、消息队列异步削峰”的架构上。下面按这条路径展开,从数据模型讲到并发控制,再讲工程落地和效果验证,适合后端、全栈以及医疗信息化从业者按图索骥。
2. 医院门诊预约挂号管理系统的基础模型:号源、时段与状态机
2.1 号源模型与排班数据怎么设计
“号源”不是凭空产生的数字,而是由医生排班驱动生成的。常见做法是:每个医生在某个出诊时间段内,按预估就诊时长切成固定数量的号源,一个号源对应一位患者的预约凭证,而不是一个库存余量。这样设计的好处是,每个号源都能独立进入暂挂、已约、取消、完成等状态,后续做候补队列、检验检查预约时也能复用同一套逻辑。
一个较清晰的表结构是这样:
CREATE TABLE doctor_schedule ( id BIGINT PRIMARY KEY, doctor_id INT NOT NULL, department_id INT NOT NULL, clinic_date DATE NOT NULL, session_type TINYINT NOT NULL, -- 0=上午 1=下午 2=晚班 start_time TIME NOT NULL, end_time TIME NOT NULL, status TINYINT DEFAULT 1 ); CREATE TABLE appointment_slots ( id BIGINT PRIMARY KEY, schedule_id BIGINT NOT NULL, slot_time TIME NOT NULL, status TINYINT NOT NULL, -- 0=可预约 1=占用中 2=已预约 3=停诊释放 version INT DEFAULT 0 );表里两个字段的分工需要分清:session_type用来筛选医生出诊的上下午场次,slot_time用来精确到几点几分的预约时段,二者不能混用,否则早晚班的时间校验会越写越乱。appointment_slots.status不是简单的“已约/未约”两态,因为用户选号后到确认前有一个中间态,文献里常称为“暂挂 HELD”。version 字段是为乐观锁预留的,后面并发扣减会用到。
2.2 预约状态机是系统的“法律”
预约订单的状态流转比普通电商复杂,不能“付款后直接完单”,还要经过签到、过号、退号、爽约等动作。文献综述里常见的状态定义至少包含这 6 个:
| 状态 | 触发动作 | 约束条件 |
|---|---|---|
| AVAILABLE | 排班生成号源 | 无 |
| HELD | 用户选号、锁定中 | 需在限时内完成确认 |
| BOOKED | 用户确认/支付 | 只能由 HELD 转移 |
| CANCELLED | 用户退号/停诊 | BOOKED 状态下才可退 |
| COMPLETED | 取号或签到就诊 | 仅 BOOKED 可完成 |
| NO_SHOW | 过时未到 | BOOKED + 时间段结束 |
用枚举固定这些状态,业务代码会安全很多:
from enum import Enum class AppointmentStatus(Enum): AVAILABLE = 0 HELD = 1 BOOKED = 2 CANCELLED = 3 COMPLETED = 4 NO_SHOW = 5把状态定义收敛到一个模块里,而不是散落在 service 层,是我读文献后比较认同的一点。预约状态的每次变化都要对应一条流水记录,若没有统一状态机,后续做运营报表时就只能靠操作日志反推,排错成本极高。流水表appointment_log至少要有:appointment_id, from_status, to_status, operator, created_at这几个字段。
2.3 文献里对“爽约”的定义与建模方式
“无预约不到”是预约挂号文献里出现率最高的词,通常定义为:患者已确认预约,却未在规定时间内取消且没到院就诊。工程上必须先把统计口径定死,否则会影响资源补偿策略。一个可用的周粒度 SQL 是:
SELECT date_trunc('week', scheduled_time) AS week_start, count(*) AS total, sum(CASE WHEN status = 'NO_SHOW' THEN 1 ELSE 0 END) AS no_show_cnt FROM appointments GROUP BY week_start ORDER BY week_start;这里直接对状态字段聚合,但建议不要只查订单表,要用scheduled_time作为时间基准,否则待就诊订单会被误算进分母。文献中还会引入“提前预约天数”“历史爽约次数”“预约渠道”等维度,这些放到第 5 章的特征工程里展开。对于刚起步的团队,先按周汇总爽约率,再拆到科室维度,比直接上预测模型更稳妥。
3. 防超卖:医院门诊预约挂号管理系统的并发控制
3.1 为什么不能用“先查再插”做预约
任何“先查余量,再插入订单”的写法,在放号高峰都会超卖。放号瞬间通常在早上 8 点整,几千个用户同时点同一个专家号,数据库读到的余量大概率是同一个值,随后一起执行插入,号源必然超卖。工程对策有两种流派:数据库锁与 Redis 预扣减。想向团队证明问题,可以先用下面这段 SQL 复现:
-- 错误示范:并发放号场景下会超卖 SELECT available_count FROM doctor_schedule WHERE id = 100; -- 应用层判断 available_count > 0 INSERT INTO appointment_slots_occupied(appointment_id, slot_id) VALUES (12345, 100);这段代码缺少事务与条件更新。即使外层包上事务,默认可重复读隔离级别也解决不了SELECT到INSERT之间的写入窗口。正确做法是让数据库原子地完成“判断并扣减”。
3.2 用数据库行锁与乐观锁守住号源
最简单直接的方案,是在扣减号源时使用条件更新,也叫乐观锁:
BEGIN; UPDATE appointment_slots SET status = 1, version = version + 1 WHERE id = #{slotId} AND status = 0 AND version = #{expectedVersion}; -- 如果 update 影响行数为 0,说明已被占用 INSERT INTO appointment_order(appointment_id, slot_id, user_id, status) VALUES (#{apptId}, #{slotId}, #{userId}, 'BOOKED'); COMMIT;UPDATE ... WHERE status = 0 AND version = ?同时承担了比较和交换两个动作。执行后必须检查影响行数,为 0 就抛错,不能继续写订单。多行号源同时更新时,建议先按主键排序,避免多个事务互相等锁产生死锁。另一种方式是SELECT ... FOR UPDATE锁住doctor_schedule行,逻辑更直观,但锁粒度如果在医生维度而不是号源维度,同一医生下的所有号都会被串行化,放号吞吐量差很多。号源一行对应一个锁的场景下,乐观锁更适合。
3.3 Redis原子扣减与分布式锁的取舍
系统要同时支撑微信公众号、App、窗口等多渠道时,数据库行锁会成为瓶颈。很多团队的方案是在 Redis 里预存余量,扣减逻辑用一个 Lua 脚本:
local stock = redis.call('GET', KEYS[1]) if not stock or tonumber(stock) <= 0 then return 0 end redis.call('DECRBY', KEYS[1], 1) return 1KEYS[1]通常设置为slot:{scheduleId}:remain,放号前先初始化总量。DECRBY返回值小于 0 时说明没号了。这里最要命的是“Redis 扣减成功,后续创建订单失败”的补偿场景。文献里通常的做法是把 Redis 当作预占,订单异步落库,落库失败后回补库存。纯粹使用分布式锁加数据库更新也可以,但锁误释放、锁过期带来的风险比 Lua 脚本更复杂,我一般会把 Redis 扣减 + 数据库幂等键作为默认方案。
| 方案 | 吞吐 | 一致性 | 适用场景 |
|---|---|---|---|
| SELECT FOR UPDATE | 低 | 强 | 号源量小,窗口挂号 |
| 乐观锁 UPDATE | 中 | 强 | 单一消费者 |
| Redis Lua | 高 | 最终一致 | 放号高峰,多渠道 |
提示:库存回补必须走消息队列异步执行,不能在订单失败回执里同步回补,否则并发下会重复加库存。
4. 从文献到工程:预约挂号管理系统的模块划分与削峰
4.1 单体还是微服务:文献怎么说的
文献综述很少推荐预约系统从微服务起步。原因很直接:预约流程的核心是一个强一致的短事务,拆成微服务会让事务变成最终一致,复杂度陡增。常见做法是模块化单体:预约模块、号源模块、支付模块、消息通知模块作为同仓库内的子包,通过内部接口互相调用。这样部署简单,放号高峰时还能把承载流量的“预约创建”部分单独水平扩容。
一个可落地的模块清单大致包括:排班管理生成并发布号源;预约中心承接挂号下单、退号、候补;号源库存保存余量并原子扣减;消息通知发送公众号模板消息、短信、App推送;支付对账处理挂号费和停诊退款;数据分析负责爽约率和运营看板。
模块间通信建议使用事件而不是同步调用。预约创建成功后发布AppointmentCreatedEvent,通知模块订阅后发短信,这样短信延迟不会阻塞挂号请求。用户从点击提交到拿到预约成功,耗时一般控制在 500ms 以内;超过这个阈值,优先排查同步调用链上的外部依赖。
4.2 用消息队列处理预约创建的异步流程
放号瞬时流量可能达到几千 TPS,如果在 HTTP 请求里同步完成“锁号源-写订单-发通知”三步,数据库和短信服务都会被拖垮。常见做法是把锁号源和写订单留在同步链路,通知、日志、积分变动放到消息队列:
def create_appointment(user_id, slot_id): # 1. 原子扣减 Redis 号源 result = slot_stock.deduct(slot_id, 1) if result is False: raise NoStockException() # 2. 数据库写入订单,带幂等键 try: order = appointment_repo.create( user_id, slot_id, idempotent_key=uuid4() ) except DuplicateKeyException: return get_duplicated_order() # 3. 发布异步消息 event_bus.publish("appointment.created", order.id) return orderidempotent_key是关键参数,一般由user_id + slot_id + request_time哈希生成,并在数据库建唯一索引,避免用户重复点击产生两笔订单。event_bus初期可以先用 Redis 的 List 做轻量队列,生产环境再换 RabbitMQ 或 Kafka。消费者需要先置订单状态为“通知中”,发送成功或失败都留日志,方便定位“短信没发”和“短信重复发”的问题。文献里常说的“削峰填谷”,落地时其实就是把非关键路径挪到异步,而不是把整个挂号请求改成异步,否则用户会明显感到响应变慢。
4.3 预约挂号管理系统的高可用清单
| 环节 | 常见策略 | 参数与注意点 |
|---|---|---|
| 数据库 | 主从复制/读写分离 | 放号写入走主库,查询走从库 |
| Redis | 哨兵或Cluster模式 | maxmemory-policy 不要用 allkeys-lru |
| 消息队列 | 持久化+手动ack | 消费失败要重试,不能直接丢弃 |
| 前端 | 放号倒计时+按钮置灰 | 请求先走网关,限制单用户频率 |
放号时前端通常会做“倒计时结束后按钮才可点击”,后端用 API 网关限制同一个用户每秒创建预约的请求数。Redis 的maxmemory-policy建议使用noeviction或volatile-lru,不能设成allkeys-lru,否则放号前预设的号源库存键可能被淘汰,放号直接失败。运维巡检要监控slot:*:remain前缀的键数量,一旦淘汰率升高立刻报警。
5. 读文献时的三个改进方向:号源分配、爽约预测与灰度验证
5.1 号源分配策略:公平性 vs 利用率
文献对号源分配的讨论,本质上是一个多目标优化问题:既要防止热门专家号被少数人抢走,又要保证号源最终能用掉。工程上可落地的策略包括分批放号,例如专家号拆成 8:00、10:00、14:00 三个时间点释放;对历史爽约率高的科室限制放号额度;给连续预约用户开放优先通道。评估公平性时可以用 Gini 系数:
import numpy as np def gini(arr): arr = np.sort(np.asarray(arr, dtype=float)) n = len(arr) cumsum = np.cumsum(arr) return (2 * np.sum((np.arange(1, n + 1) - (n + 1) / 2) * arr)) / (n * np.sum(arr)) # 按每位患者当周获得的预约号数计算 patient_counts = [2, 1, 1, 5, 2, 3, 4, 1, 9] print(round(gini(patient_counts), 3))Gini 系数越大,说明少数患者占据了大量号源。这个指标可以放在运营看板上,而不是只盯爽约率。注意如果patient_counts全为 0,函数会除零,计算前要加一层过滤。文献里的“患者公平性”另一个常用规则是:同一患者在一周内对相同科室最多预约 2 次,避免连续占用黄金时段。
5.2 爽约预测模型怎么设计特征
预测患者是否会爽约,是预约挂号文献里最大的分支。这种模型的核心价值是:对预测概率高的患者提前释放号源进入候补队列,从而提升整体就诊率。基本特征可以分为三类:
| 特征维度 | 具体字段 | 处理方式 |
|---|---|---|
| 患者历史 | 科室累计爽约次数、取消次数 | 用最近 12 个月窗口 |
| 行为特征 | 预约提前时长、时段、渠道 | 从订单时间字段提取 |
| 状态特征 | 是否复诊、是否在医生放号首日 | 来自患者档案 |
最大的坑是“提前天数”直接用自然日期计算。正确做法是用“放号时间 - 预约创建时间”得到小时差,而不是日历天数,否则同一个排班下不同患者的数据分布会失真。模型离线评估用 ROC-AUC,一般能达到 0.65~0.75;低于 0.6 说明“预约行为稳定性”特征没提取出来,比如患者过去是否频繁修改预约。工程实现可以用 LightGBM:
import lightgbm as lgb from sklearn.model_selection import train_test_split X = feature_df.drop(columns=['no_show']) y = feature_df['no_show'] X_train, X_val, y_train, y_val = train_test_split(X, y, test_size=0.2, random_state=42) model = lgb.LGBMClassifier( n_estimators=500, learning_rate=0.05, num_leaves=31, max_depth=6 ) model.fit(X_train, y_train)num_leaves=31、max_depth=6是一组常见初值。数据量只有几万时,建议降低n_estimators并加上早停,否则过拟合很快。预测结果不能直接用来取消号,要加一道规则:预测概率高于阈值,且距离就诊时间大于 6 小时,才把号源释放给候补队列,避免患者已在来医院路上却被取消。
5.3 从文献到落地:随机对照的灰度方案
文献里的实证研究通常做随机对照试验,到系统落地时也要保留干预组和对照组。最简单的灰度方式是按用户 ID 尾号分桶,实验组使用新号源分配策略,对照组维持原逻辑。灰度期间要记录experiment_group到业务日志里,方便事后分析。观察指标不是单次放号成功率,而是未来一周的爽约率和就诊率。如果实验组的 Gini 系数下降,同时爽约率没有显著上升,策略才值得全量放开。
6. 一个验证预约扣减不超卖的压测与断言技巧
最后分享一个我常用的验证手段:针对并发扣减写一个固定压测脚本,断言“已预约数 + 剩余可预约数 = 号源总量”。
import aiohttp import asyncio async def book(session, slot_id): async with session.post(f"http://localhost:8080/api/booking/{slot_id}") as resp: return resp.status async def main(): total = 100 concurrency = 50 async with aiohttp.ClientSession() as session: tasks = [book(session, 100) for _ in range(total)] results = await asyncio.gather(*tasks) ok = sum(1 for r in results if r == 200) print(f"success={ok}, failed={total - ok}") asyncio.run(main())执行前先确认三件事:接口健康、Redis 键slot:100:remain已初始化、数据库里该号源没有历史脏数据。压测后执行SELECT count(*) FROM appointment_order WHERE slot_id=100,同时查GET slot:100:remain,两者相加必须等于 100。大于 100 是超卖,小于 100 说明有订单落库失败但补偿没生效。并发数建议从 50、200、1000 三档递增,每档跑完都检查一次appointment_log,确认是否存在从 HELD 直接变为 BOOKED 的异常路径。这一步能一次验证乐观锁参数、Redis 回补和消息队列补偿是否可靠。
本文还有配套的精品资源,点击获取