
简介这份资源是面向计算机专业学生与Java初学者的一套医院预约挂号系统毕业设计完整资料包含论文与可运行源码适合需要完成课程设计、毕业设计或想通过真实项目巩固Java Web开发技能的人群。压缩包共约2000个文件整体86.78MB以gif图片、xml配置、htm与aspx页面、css样式、js脚本及cs代码文件为主另含doc、docx论文文档、sql数据库文件mdf、ldf与少量dll组件覆盖前端页面、后端逻辑与数据库脚本等完整工程结构。目前已有208人学习下载。读者可从中获得一套可直接参考的挂号系统实现方案包括预约挂号、科室医生管理、用户信息维护等核心模块的代码与设计文档并借助论文理清需求分析、系统设计与实现思路便于二次开发或作为答辩材料使用。1. 从一份课程设计说起医院预约挂号系统到底在解决什么问题很多计算机专业的学生在毕业设计阶段都会碰到这个题目——基于Java的医院预约挂号系统。听起来像是老生常谈但真正动手做的时候你会发现它远比想象中复杂。它要解决的核心问题其实很朴素让患者不用凌晨去窗口排队让号源分配更公平让医生排班和患者需求之间尽量匹配。技术层面它涉及用户认证、排班管理、并发抢号、订单状态流转、数据一致性这几个硬骨头。适合谁看正在做课程设计或毕业设计的同学想拿它练手Spring Boot全栈的开发者以及需要快速搭一套预约类系统原型的小团队。这篇文章不讲空话直接按可落地的路径拆开讲。2. 技术选型与数据库设计为什么用Spring Boot而不是Servlet2.1 后端框架的取舍逻辑做这个系统第一道选择题就是后端框架。十年前大家用ServletJSP现在主流做法是Spring Boot。原因不复杂预约挂号的核心业务涉及大量的REST接口、事务控制和权限校验Spring Boot的自动配置和starter依赖能省掉大量XML配置。我一般会选Spring Boot 2.7.x或3.x搭配MyBatis-Plus前者生态最稳后者对单表CRUD几乎零SQL。如果你对JPA更熟用Spring Data JPA也行但在排班查询这种多条件动态拼接的场景下MyBatis-Plus的Wrapper会更顺手。前端方面如果只是课程设计Thymeleaf或VueElement UI都够用。想省事就用Thymeleaf服务端渲染想练前后端分离就上Vue3。数据库选MySQL 8.0缓存用Redis做号源库存扣减这是最经典的组合。2.2 核心表结构设计与字段说明表设计是这个系统的地基设计不好后面全是坑。最少需要这几张表用户表、医生表、科室表、排班表、预约订单表。下面给出关键表的建表语句。-- 用户表患者和医生共用用role区分 CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(100) NOT NULL COMMENT BCrypt加密后的密码, real_name VARCHAR(50) DEFAULT NULL COMMENT 真实姓名, phone VARCHAR(20) DEFAULT NULL COMMENT 手机号, role TINYINT NOT NULL DEFAULT 0 COMMENT 0患者 1医生 2管理员, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 排班表核心表决定号源 CREATE TABLE schedule ( id BIGINT NOT NULL AUTO_INCREMENT, doctor_id BIGINT NOT NULL COMMENT 医生ID, dept_id BIGINT NOT NULL COMMENT 科室ID, work_date DATE NOT NULL COMMENT 出诊日期, time_slot TINYINT NOT NULL COMMENT 0上午 1下午, total_quota INT NOT NULL COMMENT 总号源数, remain_quota INT NOT NULL COMMENT 剩余号源, fee DECIMAL(10,2) NOT NULL COMMENT 挂号费, PRIMARY KEY (id), UNIQUE KEY uk_doctor_date_slot (doctor_id,work_date,time_slot) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT医生排班表; -- 预约订单表 CREATE TABLE appointment ( id BIGINT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号, user_id BIGINT NOT NULL, schedule_id BIGINT NOT NULL, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待就诊 1已就诊 2已取消, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id), KEY idx_schedule (schedule_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT预约订单表;逻辑说明schedule表的uk_doctor_date_slot唯一索引保证同一个医生同一天同一时段只有一条排班记录避免重复排班。remain_quota字段是扣减号源的依据。appointment表的order_no唯一索引防止重复提交产生重复订单。参数说明time_slot用TINYINT而不是字符串省空间且查询快。fee用DECIMAL而不是FLOAT金额计算不能有精度丢失。status字段用TINYINT枚举比VARCHAR更省索引空间。2.3 号源扣减的两种实现路径号源扣减是并发最高的地方。常见做法有两种一是直接UPDATE schedule SET remain_quota remain_quota - 1 WHERE id ? AND remain_quota 0靠数据库行锁保证原子性二是用Redis的DECR命令预扣减再异步落库。前者实现简单适合课程设计后者性能高但要处理Redis和MySQL的数据一致性问题。我一般建议先用第一种把功能跑通压测发现瓶颈再引入Redis。3. 核心功能实现从登录到抢号的完整链路3.1 登录认证与权限拦截登录用Spring Security或自己写拦截器都行。课程设计里我倾向自己写一个简单的JWT拦截器代码量少且容易讲清楚。核心逻辑是登录成功后签发Token后续请求在Header里带Token拦截器校验并解析出用户ID和角色。// JWT工具类核心方法 public class JwtUtil { private static final String SECRET your-secret-key-must-be-long-enough; private static final long EXPIRE 24 * 60 * 60 * 1000L; // 24小时 public static String createToken(Long userId, Integer role) { return Jwts.builder() .claim(userId, userId) .claim(role, role) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); } }逻辑说明createToken把用户ID和角色写进payload设置过期时间后用HS256签名。parseToken校验签名并返回Claims如果Token被篡改或过期会抛异常拦截器捕获后返回401。参数说明SECRET长度要够太短容易被暴力破解。EXPIRE根据场景调整课程设计24小时够用生产环境建议2小时并配合刷新Token机制。3.2 预约挂号的完整流程与代码预约流程分四步查排班、校验资格、扣号源、生成订单。这四步必须在一个事务里否则会出现号扣了但订单没生成的情况。Service public class AppointmentService { Autowired private ScheduleMapper scheduleMapper; Autowired private AppointmentMapper appointmentMapper; Transactional(rollbackFor Exception.class) public String book(Long userId, Long scheduleId) { // 1. 扣减号源利用行锁和remain_quota 0条件保证不超卖 int affected scheduleMapper.decreaseQuota(scheduleId); if (affected 0) { throw new BizException(号源已抢完); } // 2. 校验用户是否已挂过该排班的号防止重复挂号 Long count appointmentMapper.countByUserAndSchedule(userId, scheduleId); if (count 0) { throw new BizException(您已预约过该号源); } // 3. 生成订单 Appointment appt new Appointment(); appt.setOrderNo(generateOrderNo()); appt.setUserId(userId); appt.setScheduleId(scheduleId); appt.setStatus(0); appointmentMapper.insert(appt); return appt.getOrderNo(); } }对应的Mapper方法update iddecreaseQuota UPDATE schedule SET remain_quota remain_quota - 1 WHERE id #{scheduleId} AND remain_quota 0 /update逻辑说明decreaseQuota的WHERE条件里带remain_quota 0这是防超卖的关键。数据库在执行UPDATE时会加行锁多个请求同时到达时只有一个能成功扣减其余返回0行受影响。Transactional保证扣号和插订单要么都成功要么都回滚。参数说明rollbackFor Exception.class确保所有异常都触发回滚默认只回滚RuntimeException。generateOrderNo建议用时间戳用户ID后四位随机数保证唯一且可读。3.3 排班查询与分页展示患者查号时通常按科室和日期筛选。用MyBatis-Plus的Wrapper可以动态拼接条件。public PageScheduleVO querySchedules(Integer deptId, String date, Integer page, Integer size) { LambdaQueryWrapperSchedule wrapper new LambdaQueryWrapper(); wrapper.eq(deptId ! null, Schedule::getDeptId, deptId) .eq(StringUtils.hasText(date), Schedule::getWorkDate, date) .gt(Schedule::getRemainQuota, 0) // 只显示有号的 .orderByAsc(Schedule::getWorkDate); PageSchedule p scheduleMapper.selectPage(new Page(page, size), wrapper); // 转换为VO补充医生姓名、科室名称等 return p.convert(this::toVO); }逻辑说明eq方法的第一个参数是条件开关条件为false时不拼接该条件这样一套代码支持多种查询组合。gt(remainQuota, 0)过滤掉已约满的排班避免患者点进去才发现没号。参数说明page从1开始size建议不超过20太大影响响应速度。orderByAsc按日期升序让最近的排班排在前面。4. 避坑与排查那些年我们踩过的雷4.1 号源超卖现象、原因与解决现象压测时发现剩余号源变成负数或者同一个号被两个人约到。原因扣减号源的SQL没有加remain_quota 0条件或者用了先查后扣的两步操作中间有时间窗口。解决必须用UPDATE ... WHERE remain_quota 0的单条原子操作不要先SELECT再UPDATE。如果用了Redis预扣减要确保Redis扣减和MySQL扣减的最终一致性常见做法是Redis扣减成功后发消息到MQ异步落库失败则回补Redis。4.2 重复挂号同一个患者约了同一个号现象用户快速点击提交按钮生成两条订单。原因前端没做防抖后端没做唯一性校验。解决前端按钮点击后置灰后端在appointment表加uk_user_schedule唯一索引或者在插入前先查一次。更稳妥的做法是两者都做前端防误触后端兜底。4.3 事务失效扣了号但订单没生成现象号源减少了但订单表里查不到记录。原因Transactional注解失效常见情况是方法被同类内部调用、方法不是public、异常被catch后没重新抛出。解决确保事务方法通过代理调用异常要么不catch要么catch后throw new RuntimeException(e)。另外注意rollbackFor要设成Exception.class。4.4 时间字段的时区问题现象排班日期显示比实际少一天或多一天。原因数据库连接URL没设时区或者Java的Date和LocalDate转换时出错。解决JDBC URL加上serverTimezoneAsia/Shanghai实体类用LocalDate和LocalDateTime替代java.util.DateMyBatis-Plus对Java 8时间类型支持很好。4.5 密码明文存储现象数据库里能看到用户密码明文。原因注册时直接存了原始密码。解决用BCrypt加密Spring Security提供了BCryptPasswordEncoder每次加密结果不同但校验时能匹配。不要用MD5MD5已经被彩虹表攻破。5. 进阶技巧让系统更接近真实可用5.1 用Redis缓存排班数据排班数据读多写少非常适合缓存。做法是查排班时先查Redis没有则查MySQL并写入Redis设置5分钟过期。扣号源时同步更新Redis里的剩余号源避免缓存和数据库不一致。注意缓存穿透问题对不存在的排班ID也缓存一个空值过期时间设短一点。5.2 定时任务处理过期订单患者预约后如果一直不就诊号源就浪费了。可以加一个定时任务每天凌晨把前一天状态为“待就诊”且未就诊的订单自动取消并把号源回补到schedule表。用Spring的Scheduled注解就能实现。Scheduled(cron 0 0 2 * * ?) // 每天凌晨2点执行 public void cancelExpiredOrders() { ListAppointment expired appointmentMapper.selectExpired(); for (Appointment appt : expired) { appt.setStatus(2); // 已取消 appointmentMapper.updateById(appt); scheduleMapper.increaseQuota(appt.getScheduleId()); // 回补号源 } }逻辑说明cron表达式0 0 2 * * ?表示每天2点执行。先查出过期订单逐个取消并回补号源。回补号源的SQL是UPDATE schedule SET remain_quota remain_quota 1 WHERE id ?。参数说明cron的六个字段分别是秒、分、时、日、月、周?表示不指定。回补号源时要注意不要超过total_quota可以在SQL里加AND remain_quota total_quota条件。5.3 接口限流防止恶意抢号有人用脚本高频请求抢号需要在网关或拦截器里做限流。简单做法是用Redis的INCR命令每个用户每分钟最多请求10次预约接口。public boolean allowRequest(Long userId) { String key limit:book: userId; Long count redisTemplate.opsForValue().increment(key); if (count 1) { redisTemplate.expire(key, 1, TimeUnit.MINUTES); } return count 10; }逻辑说明每次请求对key自增第一次设置1分钟过期。超过10次返回false拦截器直接拒绝。这个方案简单有效适合课程设计。生产环境可以用Guava RateLimiter或Sentinel做更细粒度的控制。参数说明expire必须在increment返回1时设置否则每次都会刷新过期时间导致限流失效。阈值10根据实际并发调整太小误伤正常用户太大起不到防护作用。5.4 用状态机管理订单流转订单状态有“待就诊、已就诊、已取消”三种状态之间的流转是有规则的待就诊可以变成已就诊或已取消已就诊和已取消不能再变。用枚举加状态判断比一堆if-else清晰得多。public enum AppointmentStatus { PENDING(0, 待就诊), VISITED(1, 已就诊), CANCELLED(2, 已取消); private final int code; private final String desc; AppointmentStatus(int code, String desc) { this.code code; this.desc desc; } public static boolean canTransfer(int from, int to) { if (from PENDING.code) { return to VISITED.code || to CANCELLED.code; } return false; } }逻辑说明canTransfer方法定义了合法的状态流转路径只有待就诊状态才能流转到已就诊或已取消。在更新订单状态前先调用这个方法校验不合法就抛异常。参数说明code和数据库里的status字段对应。枚举的desc用于前端展示避免前端硬编码中文。做这个系统最大的教训是不要一开始就追求大而全。先把登录、排班、预约、取消这四个核心流程跑通再考虑缓存、限流、定时任务这些锦上添花的东西。我见过太多人卡在表设计阶段反复修改结果 deadline 到了连一个能跑的 Demo 都拿不出来。先把最小闭环做出来哪怕代码丑一点跑起来之后再重构。希望帮到你。本文还有配套的精品资源点击获取