毕业设计实战:基于SpringBoot的线上医院挂号系统,并发与业务逻辑避坑指南!

50 阅读9分钟

毕业设计实战:基于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分钟
  • 分库分表准备:订单表按月份分表
  • 读写分离:查询走从库,写操作走主库

七、答辩准备:突出“医疗业务严谨性”

医疗系统答辩要突出业务逻辑的严谨性和数据的安全性。

  1. 演示主线:患者注册→查询医生→选择时间→提交预约→支付成功→查看订单
  2. 技术亮点
    • Redis分布式锁解决并发抢号
    • 定时任务处理超时订单
    • 敏感数据加密存储
  3. 业务严谨性
    • 时间冲突检测
    • 号源一致性保证
    • 医生停诊自动处理
  4. 安全考虑
    • 患者隐私保护
    • 数据备份机制
    • 操作日志记录

八、论文写作要点

  1. 第三章系统分析
    • 业务流程图(挂号完整流程)
    • 用例图(患者、医生、管理员)
  2. 第四章系统设计
    • 数据库E-R图(突出排班-订单关系)
    • 状态机图(订单状态流转)
    • 并发控制方案设计
  3. 第五章系统实现
    • 关键业务代码(带注释)
    • 界面截图(带数据脱敏)
  4. 第六章系统测试
    • 并发测试结果
    • 异常流程测试用例

最后:特别提醒(医疗系统敏感!)

  1. 不要涉及诊断:只做挂号预约,不做任何医疗建议
  2. 测试数据虚构:患者信息用生成器生成,不要用真实数据
  3. 免责声明:在系统明显位置标注“本系统仅用于毕业设计演示”
  4. 支付模拟:用模拟支付,不要接真实支付接口

需要完整的挂号系统源码JMeter并发测试脚本医疗数据脱敏方案的同学,评论区留言“医院挂号”。遇到并发控制、时间冲突检测等问题也可以提问。

记住:医疗系统的核心是可靠安全,不是功能的多少。

点赞收藏,做严谨的医疗系统毕设!祝大家顺利通过答辩!🏥