1. 医院预约挂号系统概述
医疗信息化建设已经进入深水区,传统窗口排队挂号的方式正在被线上预约系统全面取代。基于SpringBoot+Vue的医院预约挂号管理系统,通过前后端分离架构实现了患者在线预约、医生排班管理、号源分配等核心功能。这套系统不仅解决了"三长一短"(挂号排队时间长、候诊时间长、缴费时间长、就诊时间短)的医疗痛点,更为医院管理提供了数据支撑。
我在三甲医院信息化部门工作期间,主导过5个同类系统的落地实施。真实的医疗场景对系统有着特殊要求:必须支持瞬时高并发(特别是早8点放号时段),保证号源数据的强一致性,同时要兼容医保结算等复杂业务流程。这些实战经验让我深刻理解到,一个合格的挂号系统远不止是简单的CRUD操作。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术架构设计
2.1 整体技术栈选型
后端采用SpringBoot 2.7 + MyBatis Plus组合,前端使用Vue3 + Element Plus。这个组合经过我们团队在多个医疗项目的验证,具有以下优势:
- SpringBoot的自动配置特性大幅减少XML配置,内嵌Tomcat支持快速部署
- MyBatis Plus的代码生成器可快速产出科室、医生等基础模块的CRUD代码
- Vue3的Composition API使前端状态管理更清晰,特别是处理复杂的预约规则时
数据库选用MySQL 8.0,主要考虑其事务特性对号源抢占场景的支持。这里有个关键设计决策:我们没有采用微服务架构,因为单体应用在中小型医院场景下更易维护。但通过清晰的包结构划分(如com.hospital.booking.controller/api/service/domain等),为后期可能的拆分预留了空间。
2.2 核心业务流程建模
挂号业务的核心状态机如下图所示(伪代码表示):
java复制enum AppointmentStatus {
PENDING_PAYMENT, // 待支付(锁定号源15分钟)
COMPLETED, // 已完成支付
CANCELLED, // 患者取消
NO_SHOW, // 爽约
REFUNDED // 已退号
}
特别注意状态转换的约束条件:
- 只有PENDING_PAYMENT状态允许直接转为COMPLETED
- CANCELLED状态必须记录取消方(患者/系统/管理员)
- NO_SHOW状态由系统在就诊时间过后自动标记
3. 关键模块实现
3.1 号源池设计
号源管理是系统的核心难点,我们采用分时分区存储策略:
sql复制CREATE TABLE `schedule` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`doctor_id` BIGINT NOT NULL COMMENT '医生ID',
`dept_id` INT NOT NULL COMMENT '科室ID',
`date` DATE NOT NULL COMMENT '出诊日期',
`period` TINYINT NOT NULL COMMENT '时段(1上午/2下午/3晚上)',
`total_count` INT DEFAULT 30 COMMENT '总号源数',
`available_count` INT COMMENT '剩余号源',
`version` INT DEFAULT 0 COMMENT '乐观锁版本号',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_doctor_time` (`doctor_id`,`date`,`period`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
高并发下号源更新的解决方案:
java复制@Transactional
public boolean lockAppointment(Long scheduleId) {
// 使用乐观锁防止超卖
Schedule schedule = scheduleMapper.selectById(scheduleId);
if (schedule.getAvailableCount() <= 0) {
return false;
}
int updated = scheduleMapper.updateAvailableCount(
scheduleId,
schedule.getVersion(),
schedule.getAvailableCount() - 1);
return updated > 0;
}
3.2 预约规则引擎
不同科室有复杂的预约规则,我们采用策略模式实现:
java复制public interface BookingRule {
boolean validate(AppointmentRequest request);
}
@Component
@RuleType("pediatric")
public class PediatricRule implements BookingRule {
// 儿科规则:仅限18岁以下患者,且需填写监护人信息
@Override
public boolean validate(AppointmentRequest request) {
return request.getPatientAge() < 18
&& StringUtils.isNotBlank(request.getGuardianInfo());
}
}
规则配置通过数据库表动态加载,支持医院随时调整:
sql复制INSERT INTO `booking_rule` VALUES
(1,'内科','default',NULL),
(2,'儿科','pediatric','{"maxAge":18}'),
(3,'产科','gender', '{"requiredGender":"female"}');
4. 典型问题解决方案
4.1 高并发场景下的数据一致性问题
挂号系统在放号时段面临巨大并发压力,我们通过多级缓存缓解数据库压力:
-
使用Redis缓存未来7天的可预约号源信息
bash复制
redis-cli> HSET schedule:2023-08-01:1 available_count 30 -
采用Lua脚本保证原子性扣减
lua复制local key = KEYS[1] local count = tonumber(ARGV[1]) local current = tonumber(redis.call('HGET', key, 'available_count')) if current >= count then redis.call('HINCRBY', key, 'available_count', -count) return 1 end return 0 -
最终通过定时任务同步Redis与数据库数据
4.2 医疗业务特殊性处理
-
黑名单机制:对3次爽约的患者限制预约权限
java复制@Scheduled(cron = "0 0 18 * * ?") public void updateBlacklist() { List<Patient> noShowPatients = appointmentMapper .selectNoShowPatients(LocalDate.now()); noShowPatients.forEach(patient -> { if (patient.getNoShowCount() >= 3) { blacklistMapper.insert(patient.getId()); } }); } -
号源回收:对未支付的预约15分钟后自动释放
java复制@Scheduled(fixedRate = 60000) public void releaseTimeoutAppointments() { List<Appointment> timeoutList = appointmentMapper .selectTimeoutAppointments(15); timeoutList.forEach(app -> { redisTemplate.opsForHash().increment( "schedule:" + app.getScheduleId(), "available_count", 1); appointmentMapper.updateStatus( app.getId(), AppointmentStatus.CANCELLED); }); }
5. 前端关键实现
5.1 可视化排班表
使用Vue+Element UI实现交互式排班管理:
vue复制<template>
<el-calendar>
<template #dateCell="{date, data}">
<div @click="handleDateClick(date)">
<el-popover v-for="s in schedules[date]" :key="s.id">
<template #reference>
<el-tag :type="s.available_count ? '' : 'info'">
剩余: {{s.available_count}}
</el-tag>
</template>
<schedule-detail :schedule="s"/>
</el-popover>
</div>
</template>
</el-calendar>
</template>
5.2 微信支付集成
前端支付流程关键代码:
javascript复制async function startWxPay(orderId) {
const res = await axios.post('/api/pay/create', { orderId })
if (res.data.codeUrl) {
// 显示微信支付二维码
showQrcodeDialog(res.data.codeUrl)
// 轮询支付结果
const timer = setInterval(async () => {
const statusRes = await axios.get(`/api/pay/status/${orderId}`)
if (statusRes.data.paid) {
clearInterval(timer)
this.$router.push('/success')
}
}, 3000)
}
}
6. 部署与监控方案
6.1 生产环境部署
采用Docker Compose编排服务:
yaml复制version: '3'
services:
app:
image: hospital-booking:1.0
ports:
- "8080:8080"
depends_on:
- redis
- mysql
environment:
- SPRING_PROFILES_ACTIVE=prod
redis:
image: redis:6
ports:
- "6379:6379"
volumes:
- redis_data:/data
mysql:
image: mysql:8.0
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
6.2 监控指标配置
使用Spring Boot Actuator暴露关键指标:
properties复制# application-prod.properties
management.endpoints.web.exposure.include=health,metrics,prometheus
management.metrics.tags.application=${spring.application.name}
management.metrics.export.prometheus.enabled=true
Grafana监控看板应包含:
- 实时预约量(按科室统计)
- 支付成功率
- 接口响应时间P99
- 号源库存变化趋势
7. 项目演进方向
在实际运行中,我们发现系统还可以在以下方面进行优化:
- 智能推荐:基于历史数据为患者推荐就诊科室
java复制public List<Department> recommendDepartments(String symptoms) {
// 使用NLP分析症状描述
List<String> keywords = textAnalyzer.analyze(symptoms);
// 从ES查询相关科室
return esClient.search(builder -> builder
.query(q -> q
.match(m -> m
.field("common_illnesses")
.query(String.join(" ", keywords))
)
)
.fetchSource(new String[]{"dept_id","dept_name"}, null)
);
}
- 候诊队列实时推送:通过WebSocket通知患者当前叫号进度
javascript复制// 前端建立WebSocket连接
const socket = new WebSocket(`wss://${location.host}/queue/123`)
socket.onmessage = (event) => {
const data = JSON.parse(event.data)
this.currentNumber = data.current
this.waitCount = data.waitCount
if (data.current === this.myNumber) {
this.showAlert('请到诊室就诊')
}
}
- 检查报告集成:对接LIS/PACS系统获取检查结果
在实施过程中有个重要经验:一定要提前与医院医务科确认好《号源管理规范》,包括特殊号源(如专家号、扶贫号)的处理规则。我们在某医院就遇到过因规则不明确导致号源分配争议的情况。
