1. 项目背景与核心需求
医院挂号就诊系统是医疗信息化建设的基础环节,也是改善患者就医体验的关键入口。传统线下挂号模式普遍存在排队时间长、号源分配不透明、就诊流程繁琐等问题。基于SpringBoot+Vue的技术架构,我们设计实现了这套支持线上挂号、分诊叫号、电子病历管理的全流程解决方案。
这套系统的核心价值体现在三个维度:
- 对患者:提供7×24小时在线挂号服务,支持按科室/医生/时间段的多维度查询,就诊当天通过手机推送实时叫号提醒
- 对医院:实现号源智能分配(如根据医生职称自动设置不同挂号限额),自动生成门诊量统计报表
- 对管理员:可视化监控各科室就诊负荷,动态调整号源分配策略,防范黄牛倒号行为
2. 技术架构设计
2.1 前后端分离架构
采用经典的SpringBoot+Vue前后端分离方案:
code复制前端:Vue 2.6 + ElementUI 2.15 + Axios 0.21
后端:SpringBoot 2.5.6 + MyBatis 3.5.7
数据库:MySQL 8.0.26
选择这套技术栈主要基于以下考量:
- Vue的响应式特性非常适合频繁更新状态的挂号排队场景
- ElementUI提供现成的表单验证、分页组件,加速开发进度
- SpringBoot内嵌Tomcat简化部署,Starter机制方便集成MyBatis等组件
2.2 数据库设计要点
核心表结构设计遵循医疗行业规范:
sql复制CREATE TABLE `doctor` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(20) NOT NULL COMMENT '医生姓名',
`title` enum('主任医师','副主任医师','主治医师') NOT NULL,
`dept_id` int NOT NULL COMMENT '所属科室',
`max_patients` int DEFAULT 30 COMMENT '单日接诊上限',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `registration` (
`id` bigint NOT NULL COMMENT '挂号单号',
`patient_id` int NOT NULL,
`doctor_id` int NOT NULL,
`status` enum('待支付','已预约','就诊中','已完成','已取消') NOT NULL,
`create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
`appointment_date` date NOT NULL COMMENT '预约日期',
`time_slot` enum('08:00-09:00','09:00-10:00') NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_doctor_date` (`doctor_id`,`appointment_date`)
) COMMENT='挂号记录表';
特别注意:
- 挂号单号使用雪花算法生成(避免自增ID暴露业务量)
- 建立医生+日期的联合索引提升查询效率
- 使用ENUM类型约束固定取值范围
3. 核心功能实现
3.1 号源动态分配算法
医生每日可接诊量根据职称动态计算:
java复制// DoctorService.java
public int calculateDailyQuota(Doctor doctor) {
int base = 30; // 基础接诊量
switch (doctor.getTitle()) {
case "主任医师":
return (int)(base * 1.5);
case "副主任医师":
return (int)(base * 1.2);
default:
return base;
}
}
号源释放策略:
- 预约成功后15分钟内未支付自动释放
- 就诊日前一天20:00统一释放未支付的号源
- 医生临时停诊时批量释放已挂号源
3.2 分诊叫号逻辑
采用双队列机制保证公平性:
java复制// TriageService.java
public synchronized Patient callNext(Integer deptId) {
// 优先队列:预约患者按挂号顺序
Patient appointmentPatient = appointmentQueue.poll();
if (appointmentPatient != null) {
return appointmentPatient;
}
// 普通队列:现场挂号患者
return walkInQueue.poll();
}
通过WebSocket实时推送叫号信息到前端:
javascript复制// frontend/src/utils/socket.js
const socket = new WebSocket(`wss://${location.host}/ws/triage`);
socket.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'CALL') {
ElNotification({
title: '请到诊室就诊',
message: `${msg.room}号诊室正在叫号:${msg.number}`,
duration: 0
});
}
};
4. 关键问题解决方案
4.1 高并发挂号场景处理
采用Redis+Lua脚本实现原子化号源扣减:
lua复制-- resources/scripts/register.lua
local key = KEYS[1] -- "dept:1:doctor:5:20230501"
local remaining = tonumber(redis.call("GET", key))
if remaining and remaining > 0 then
redis.call("DECR", key)
return 1 -- 成功
end
return 0 -- 失败
Java调用示例:
java复制// RegistrationService.java
Long result = redisTemplate.execute(
registerScript,
Collections.singletonList(resourceKey),
Collections.emptyList()
);
if (result == 1) {
// 执行数据库插入操作
}
4.2 就诊流程状态管理
使用状态模式封装业务规则:
java复制public interface RegistrationState {
void cancel(Registration registration);
void complete(Registration registration);
}
@Component
@Scope("prototype")
public class PendingPaymentState implements RegistrationState {
@Override
public void cancel(Registration reg) {
reg.setStatus(RegistrationStatus.CANCELLED);
// 释放号源
redisTemplate.opsForValue().increment(reg.getResourceKey());
}
}
状态转换示意图:
code复制[待支付] --15min超时--> [已取消]
[待支付] --支付成功--> [已预约]
[已预约] --就诊日签到--> [就诊中]
[就诊中] --医生确认--> [已完成]
5. 安全与合规设计
5.1 敏感数据保护措施
- 患者身份证号加密存储:
java复制// Patient.java
@Column(name = "id_card")
@Convert(converter = CryptoConverter.class)
private String idCard;
- 日志脱敏处理:
java复制@Bean
public PatternLayout patternLayout() {
PatternLayout layout = new PatternLayout();
layout.setReplacement("%msg{replace(身份证号,\\d{4}(\\d{10})\\d{4},$1****$2)}");
return layout;
}
5.2 防黄牛机制
- 同一IP/设备5分钟内最多发起3次挂号请求
- 就诊人身份证号+就诊日期唯一约束
- 热门科室挂号需短信验证码二次确认
6. 部署与监控方案
6.1 多环境配置
通过Spring Profile实现环境隔离:
yaml复制# application-prod.yml
spring:
datasource:
url: jdbc:mysql://mysql-cluster:3306/hospital?useSSL=false
hikari:
maximum-pool-size: 20
management:
endpoints:
web:
exposure:
include: health,metrics,prometheus
6.2 监控指标采集
- 关键业务指标:
- 挂号成功率
- 平均叫号等待时间
- 号源利用率
- 使用Micrometer对接Prometheus:
java复制@Bean
MeterRegistryCustomizer<PrometheusMeterRegistry> configureMetrics() {
return registry -> registry.config().commonTags("application", "hospital-reg");
}
7. 开发实践建议
-
API设计规范:
- 挂号接口采用POST /api/registrations (避免URL包含敏感信息)
- 分页查询统一使用PageHelper插件
java复制@GetMapping("/doctors") public PageInfo<Doctor> listDoctors( @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize) { PageHelper.startPage(pageNum, pageSize); return new PageInfo<>(doctorService.listAll()); } -
前后端联调技巧:
- 使用Mock.js模拟接口返回
javascript复制Mock.mock('/api/doctors', { 'data|10': [{ 'id|+1': 1, 'name': '@cname', 'title|1': ['主任医师','副主任医师','主治医师'] }] })- 配置axios响应拦截器统一处理错误
javascript复制axios.interceptors.response.use(response => { return response.data }, error => { if (error.response.status === 401) { router.push('/login') } return Promise.reject(error) }) -
性能优化经验:
- 科室列表等低频变化数据使用Guava Cache:
java复制@Cacheable(value = "departments", unless = "#result == null") public List<Department> listAll() { return departmentMapper.selectAll(); }- Vue组件按需加载:
javascript复制const DoctorList = () => import('./views/DoctorList.vue')
这套系统在实际部署中需要特别注意与HIS系统的数据对接规范,建议采用中间库方式交换数据,避免直连生产库。对于三甲医院等大规模场景,应考虑引入RocketMQ解耦挂号与支付流程,防止支付高峰期阻塞正常挂号请求。
