毕业设计实战:基于SpringBoot的线上医院挂号系统,并发与业务逻辑避坑指南!
当初做医院挂号系统时,光“医生排班”和“号源库存”的并发处理就卡了5天——多个用户同时抢同一个医生的号时没加锁,导致“一号多挂”,导师一句“医疗系统业务逻辑必须严谨”让我重构了整个预约模块😫。今天把挂号系统的核心业务流程、并发控制、时间冲突检测说透,让你轻松搞定医疗类毕设!
一、搞清“医院挂号”核心痛点!别做花哨功能
刚开始我想做“智能分诊推荐”、“病历管理”、“在线问诊”,结果导师说“挂号系统的核心是号源管理、预约冲突检测、支付流程,不是扩展功能”。后来调研发现,患者最需要的是快速挂上号、清晰的时间安排、可靠的预约确认。
1. 核心用户&功能聚焦(精简版)
系统三类用户,权限要严格:
-
患者端(核心体验):
- 医生查询:按科室/医生姓名筛选、查看医生详情(擅长、排班、挂号费)
- 在线挂号:选择日期、时间段、提交预约、在线支付
- 订单管理:查看预约记录、取消预约、查看就诊状态
- 医患交流:给医生留言(简化版,别做在线问诊!)
-
医生端(管理核心):
- 排班管理:设置可预约时间、临时停诊、调整号源数量
- 患者管理:查看预约患者列表、标记就诊状态
- 留言回复:回复患者咨询
-
管理员端(系统维护):
- 医生管理:审核医生资质、设置科室和职称
- 订单管理:查看所有预约、处理异常订单
- 系统配置:管理科室分类、时间段设置
2. 需求分析避坑指南(血泪教训)
- 别做“在线问诊”:医疗资质问题复杂,涉及法律责任,毕设不要碰!
- 号源设计要合理:我当初每个医生每天固定100个号,结果上午就抢光。后来改成分时段放号(上午50,下午50)
- 时间冲突检测必须做:患者不能同时段挂两个号,医生不能同一时间被重复预约
3. 可行性分析(医疗系统特殊要求)
- 技术可行:SpringBoot+MySQL,事务保证数据一致性
- 法律可行:不做诊断、不开处方,只做预约挂号,不涉及医疗核心业务
- 安全要求:患者隐私保护(姓名、身份证号加密存储)
二、技术选型:事务和并发是重点!
挂号系统最怕数据不一致。推荐SpringBoot 2.7 + MySQL 8.0(事务隔离级别RR)+ Redis分布式锁 + Vue2。
1. 技术栈选择理由
| 技术 | 为什么选 | 避坑提醒 |
|---|---|---|
| MySQL 8.0 | 支持行级锁,事务隔离级别可配置 | 一定用InnoDB引擎,MyISAM不支持事务 |
| Redis分布式锁 | 解决并发抢号问题 | 别用synchronized,集群部署会失效 |
| Quartz定时任务 | 定时释放过期未支付号源 | 别用Timer,功能太弱 |
| Vue2 + ElementUI | 时间选择组件丰富,适合排班管理 | 日期选择用el-date-picker |
2. 环境搭建(重点:Redis配置)
# application.yml
spring:
redis:
host: localhost
port: 6379
password:
lettuce:
pool:
max-active: 8
max-wait: -1ms
max-idle: 8
min-idle: 0
三、数据库设计:时间和冲突检测是核心!
挂号系统最复杂的是时间管理。我当初设计的表结构没考虑“医生临时停诊”,结果患者约了号医生却不在。
1. 核心表设计(7张表足够)
必做核心表:
- 医生表(doctor):id、姓名、科室、职称、头像、挂号费、简介、是否停诊
- 医生排班表(schedule):id、医生id、日期、时间段(上午/下午)、总号源数、剩余号源数、是否停诊
- 患者表(patient):id、姓名、手机号、身份证号(加密)、头像
- 挂号订单表(appointment):id、订单号、患者id、医生id、排班id、预约状态(0待支付/1已预约/2已取消/3已完成)、支付状态、创建时间
选做扩展表:
- 科室表(department):id、科室名称、简介
- 患者留言表(message):id、患者id、医生id、内容、回复、状态
- 号源释放记录表(release_log):id、排班id、释放时间、释放原因(超时未支付/主动取消)
2. 排班表设计技巧(关键!)
CREATE TABLE doctor_schedule (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
doctor_id BIGINT NOT NULL COMMENT '医生ID',
schedule_date DATE NOT NULL COMMENT '排班日期',
time_slot TINYINT NOT NULL COMMENT '时间段:1上午,2下午',
total_count INT DEFAULT 20 COMMENT '总号源数',
remaining_count INT DEFAULT 20 COMMENT '剩余号源数',
is_cancelled TINYINT DEFAULT 0 COMMENT '是否停诊:0正常,1停诊',
UNIQUE KEY uk_doctor_date_slot (doctor_id, schedule_date, time_slot)
) COMMENT='医生排班表';
重要约束:
- 唯一索引防止重复排班
- 剩余号源不能为负数
- 停诊后所有预约自动取消
3. 挂号订单状态设计
public enum AppointmentStatus {
PENDING_PAYMENT(0, "待支付"), // 下单未支付
RESERVED(1, "已预约"), // 支付成功
CANCELLED(2, "已取消"), // 用户取消
COMPLETED(3, "已完成"), // 已就诊
EXPIRED(4, "已过期"); // 超时未支付
// 状态流转:0→1(支付),0→2(取消),0→4(超时),1→2(取消),1→3(就诊)
}
四、核心业务实现:并发抢号是难点!
挂号系统的核心难点是“高并发下的数据一致性”。
1. 挂号流程(带并发控制)
@Service
public class AppointmentService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Transactional(rollbackFor = Exception.class)
public Result makeAppointment(Long scheduleId, Long patientId) {
// 1. 获取分布式锁(防止并发)
String lockKey = "appointment_lock:" + scheduleId;
String lockValue = UUID.randomUUID().toString();
boolean locked = false;
try {
locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, lockValue, 30, TimeUnit.SECONDS);
if (!locked) {
return Result.error("系统繁忙,请稍后再试");
}
// 2. 检查号源(使用悲观锁或乐观锁)
DoctorSchedule schedule = scheduleMapper.selectForUpdate(scheduleId);
if (schedule == null || schedule.getIsCancelled() == 1) {
return Result.error("该号源已停诊");
}
if (schedule.getRemainingCount() <= 0) {
return Result.error("号源已抢完");
}
// 3. 检查时间冲突(同一患者同一时间段只能有一个预约)
boolean hasConflict = checkTimeConflict(patientId, schedule.getScheduleDate(),
schedule.getTimeSlot());
if (hasConflict) {
return Result.error("该时间段已有其他预约");
}
// 4. 扣减号源
int rows = scheduleMapper.decreaseRemainingCount(scheduleId, schedule.getVersion());
if (rows == 0) {
return Result.error("号源不足,请重新选择");
}
// 5. 创建订单
Appointment appointment = new Appointment();
appointment.setOrderNo(generateOrderNo());
appointment.setScheduleId(scheduleId);
appointment.setPatientId(patientId);
appointment.setStatus(AppointmentStatus.PENDING_PAYMENT.getCode());
appointmentMapper.insert(appointment);
// 6. 设置支付超时(15分钟)
redisTemplate.opsForValue().set(
"appointment_pay_timeout:" + appointment.getId(),
"1", 15, TimeUnit.MINUTES
);
return Result.success("预约成功,请在15分钟内支付", appointment);
} finally {
// 释放锁
if (locked) {
if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
}
}
2. 支付超时处理(定时任务)
@Component
public class AppointmentTimeoutTask {
@Scheduled(cron = "0 */1 * * * ?") // 每分钟执行一次
public void handleTimeoutAppointments() {
// 1. 查询超时未支付的订单(创建时间超过15分钟)
List<Appointment> timeoutList = appointmentMapper.selectTimeoutList();
for (Appointment appointment : timeoutList) {
try {
// 2. 释放号源
scheduleMapper.increaseRemainingCount(appointment.getScheduleId());
// 3. 更新订单状态
appointment.setStatus(AppointmentStatus.EXPIRED.getCode());
appointmentMapper.updateById(appointment);
// 4. 可选:发送通知(站内信/短信)
notifyPatient(appointment.getPatientId(), "您的预约已超时取消");
} catch (Exception e) {
log.error("处理超时订单失败:{}", appointment.getId(), e);
}
}
}
}
3. 患者端页面设计要点
- 医生列表页:
- 显示医生头像、姓名、职称、科室、挂号费
- 标签显示:有号/无号/停诊
- 快速筛选:科室、职称、有号医生
- 排班选择页:
- 日历式选择(最近7天)
- 时间段选择(上午/下午)
- 实时显示剩余号源(小于5个标红)
- 订单确认页:
- 显示医生信息和预约时间
- 倒计时15分钟支付
- 支付方式(模拟支付即可)
五、权限控制:医疗数据要严谨
医疗系统对权限要求严格,不同角色数据隔离。
1. 数据权限设计
@RestController
@RequestMapping("/api/appointment")
public class AppointmentController {
// 患者只能看自己的预约
@GetMapping("/my")
@PreAuthorize("hasRole('PATIENT')")
public List<Appointment> getMyAppointments() {
Long patientId = getCurrentPatientId();
return appointmentService.findByPatientId(patientId);
}
// 医生只能看自己的排班预约
@GetMapping("/doctor/my")
@PreAuthorize("hasRole('DOCTOR')")
public List<Appointment> getDoctorAppointments() {
Long doctorId = getCurrentDoctorId();
return appointmentService.findByDoctorId(doctorId);
}
}
2. 敏感数据加密
@Component
public class DataEncryptor {
private static final String KEY = "your-secret-key-16bytes";
// 身份证号加密存储
public String encryptIdCard(String idCard) {
try {
Cipher cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
SecretKeySpec keySpec = new SecretKeySpec(KEY.getBytes(), "AES");
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
byte[] encrypted = cipher.doFinal(idCard.getBytes());
return Base64.getEncoder().encodeToString(encrypted);
} catch (Exception e) {
throw new RuntimeException("加密失败", e);
}
}
// 查询时解密
public String decryptIdCard(String encrypted) {
// 解密逻辑
}
}
六、测试重点:并发和异常流程
挂号系统必须重点测试并发场景和异常流程。
1. 并发测试用例
| 测试场景 | 并发数 | 预期结果 | 测试工具 |
|---|---|---|---|
| 同一号源多人抢 | 100人同时抢10个号 | 只有10人成功,90人失败 | JMeter |
| 支付超时释放 | 创建订单不支付 | 15分钟后自动取消,号源释放 | 手动测试 |
| 医生临时停诊 | 停诊后患者预约 | 所有预约自动取消,退款 | 手动测试 |
2. 业务异常测试
@Test
public void testConcurrentAppointment() {
// 模拟100个线程同时抢号
ExecutorService executor = Executors.newFixedThreadPool(100);
CountDownLatch latch = new CountDownLatch(100);
AtomicInteger successCount = new AtomicInteger(0);
for (int i = 0; i < 100; i++) {
executor.submit(() -> {
try {
Result result = appointmentService.makeAppointment(scheduleId, patientId);
if (result.isSuccess()) {
successCount.incrementAndGet();
}
} finally {
latch.countDown();
}
});
}
latch.await();
// 验证:成功数 <= 号源总数
assertTrue(successCount.get() <= totalCount);
}
3. 性能优化点
- 号源查询缓存:医生排班信息缓存5分钟
- 分库分表准备:订单表按月份分表
- 读写分离:查询走从库,写操作走主库
七、答辩准备:突出“医疗业务严谨性”
医疗系统答辩要突出业务逻辑的严谨性和数据的安全性。
- 演示主线:患者注册→查询医生→选择时间→提交预约→支付成功→查看订单
- 技术亮点:
- Redis分布式锁解决并发抢号
- 定时任务处理超时订单
- 敏感数据加密存储
- 业务严谨性:
- 时间冲突检测
- 号源一致性保证
- 医生停诊自动处理
- 安全考虑:
- 患者隐私保护
- 数据备份机制
- 操作日志记录
八、论文写作要点
- 第三章系统分析:
- 业务流程图(挂号完整流程)
- 用例图(患者、医生、管理员)
- 第四章系统设计:
- 数据库E-R图(突出排班-订单关系)
- 状态机图(订单状态流转)
- 并发控制方案设计
- 第五章系统实现:
- 关键业务代码(带注释)
- 界面截图(带数据脱敏)
- 第六章系统测试:
- 并发测试结果
- 异常流程测试用例
最后:特别提醒(医疗系统敏感!)
- 不要涉及诊断:只做挂号预约,不做任何医疗建议
- 测试数据虚构:患者信息用生成器生成,不要用真实数据
- 免责声明:在系统明显位置标注“本系统仅用于毕业设计演示”
- 支付模拟:用模拟支付,不要接真实支付接口
需要完整的挂号系统源码、JMeter并发测试脚本、医疗数据脱敏方案的同学,评论区留言“医院挂号”。遇到并发控制、时间冲突检测等问题也可以提问。
记住:医疗系统的核心是可靠和安全,不是功能的多少。
点赞收藏,做严谨的医疗系统毕设!祝大家顺利通过答辩!🏥