尧图精选

Java SpringBoot实战:基于Web的网上预约挂号系统设计与实现

🕒 发布时间:2026/10/1 4:56:00 📁 来源:尧图网络
这个选题我太熟了。每年毕业季都有学生来问计算机毕设想做点实际的东西Java 和 SpringBoot 怎么写选什么题目好。今天说的这个基于 Web 的网上预约挂号系统就是其中出现频率相当高的一个。它是 JavaSpringBoot 的经典落地场景前后端配合完成医院网上预约挂号平台麻雀虽小五脏俱全既能展示你对 Java 后端、SpringBoot 框架、Web 开发的理解又不会复杂到写不完。适合拿来做毕设也适合刚学完 Java 想练手的人照着复刻一遍。我最初接触这个项目是帮一个学弟改答辩前的代码后来自己又在工作里用 SpringBoot 重构过类似的信息管理系统。这套系统的价值不在于挂号这两个字而在于它把用户权限、业务状态流转、数据一致性、并发控制这些后端工程师天天面对的问题浓缩在一个能看得见、能演示的项目里。下面我会从设计思路、核心实现、完整实操到避坑经验一条线讲清楚你想拿它当毕设也好想学 SpringBoot 实战也好这份梳理都值得收藏。1. 为什么这个选题在毕设里烂大街却依然靠谱很多人一听别人做过就担心重复实际上每年几百万毕业生单靠题目本身很难做到独一无二。评判一个好毕设的核心标准从来不是题目新不新鲜而是你有没有把里面的业务想透、把技术点做扎实。预约挂号系统就是典型的题目常见、深度可调类型你的发挥空间非常大。1.1 业务逻辑天然清晰评委一眼能看懂答辩现场最尴尬的事情就是你站在台上讲了十分钟评委老师还不知道你在做什么。预约挂号系统不存在这个问题。所有人都去医院挂过号都知道选科室—选医生—选时间—挂上号是什么意思。当你的系统演示完登录、选号、预约、取消、医生出诊管理这一套流程不用额外解释评委的代入感就已经建立了。这种业务场景熟悉度对毕设的现场表达有巨大帮助。你可以把大部分精力放在展示技术亮点上而不是反复解释业务背景。而且系统边界也异常清楚面向患者的是预约入口面向医生的是排班出诊面向管理员的是基础数据管理。三个角色三个界面功能划分一目了然这本身就是软件工程里最看重的模块化思维。1.2 SpringBootJava 这个组合的底气在哪Java 至今仍然是国内企业级应用使用率最高的语言SpringBoot 又是 Spring 生态里上手门槛最低的一档。选这个组合的优势非常实际。第一是社区资料极多。随便一个报错把关键字段复制到搜索框里基本就能找到解决方案。对于时间紧迫的毕设周期来说遇到问题能快速查到答案比什么都重要。第二是 SpringBoot 内置了 Tomcat不需要单独配置服务器一个 main 方法就能启动整个 Web 服务这对刚接触 Web 开发的同学非常友好。第三是它和前端交互的主流方式——REST API——结合得非常好你写几个 Controller 类就能提供一套完整的 JSON 接口给页面调用。真的去写你的第一个 SpringBoot 项目时你会发现它不像想象中那么多魔法。理解清楚自动配置、依赖注入、MVC 分层这三件事绝大多数代码你都能看懂、能改动、能在答辩时讲清楚这就足够了。2. 动手写代码前先把系统设计这件事想透很多同学拿着题目就开写结果写到一半发现表关系理不清、接口路径混乱、权限控制不知道往哪里放。预约挂号系统从需求到代码实现之间隔着设计这一步。把下面几件事想明白代码只是水到渠成的事情。2.1 三种角色权限边界要分清楚网上预约挂号平台至少需要三类用户患者、医生、管理员。患者是最核心的使用者能力集中在注册登录、浏览科室、查看医生排班、预约挂号、取消预约、查看自己的挂号记录。医生端相对简单主要是查看自己的排班、查看被预约的情况、可能会有停诊操作。管理员则要维护科室信息、医生账号、排班计划以及查看整个平台的统计信息。这里有个常见的误区就是三种角色各写一套接口最后代码里出现 roleController、patientController 一堆重复逻辑。更好的做法是统一用一个用户表加 role 字段区分身份接口层面通过拦截器或 Spring Security 做权限校验。患者和医生虽然功能不同但底层的用户认证逻辑是同一套这样设计能在答辩时加分因为体现了你理解抽象的价值。2.2 挂号流程里的关键状态仔细想预约挂号这个业务其本质是在某一个时间段内一个医生的号源被一个患者锁定。整个生命周期可以拆成这样几个状态待支付或已预约取决于是否结合在线支付已取消患者主动取消或超时未确认已完成已就诊已退号就诊前取消挂号状态设计是这类业务系统的灵魂。建议在数据库层面用一个 int 或 tinyint 字段表示状态值同时用常量类统一管理不要散落在代码各处。比如我用过这样的常量类public class OrderStatus { public static final int PENDING 0; // 已预约待就诊 public static final int CANCELED 1; // 已取消 public static final int COMPLETED 2; // 已完成 public static final int EXPIRED 3; // 已过期未就诊 }状态流通的规则也要提前约定已取消的号必须释放回号源池已完成或已过期的记录不能再被取消每个状态转换都涉及业务规则的副作用。把这些整理成表格写进你的设计文档里代码实现时就会很顺畅。2.3 数据库设计一张挂号表串起整个系统核心数据表不会太多我建议至少设计这六张用户表user患者和医生共用或分开共用要注意用 role 区分科室表department医生表doctor关联科室和用户排班表schedule记录医生某天某个时段出诊包含总号源和剩余号源挂号记录表appointment / order关联患者、排班、挂号时间、状态管理员表admin甚至可以复用用户表其中最关键的是排班表和挂号记录表。排班表里的 remain_count 字段是系统并发控制的核心每次成功挂号都要扣减每次取消要回补。这一正一反的操作如果不加锁就会出现典型的超卖问题——两个人同时抢最后一个号都扣到了剩余号最后卖出两个号。数据库表关系并不复杂我画不出 SQL 图但你可以用一条链路理解它科室 → 医生 → 排班 → 挂号记录。这条链路几乎覆盖了全部查询路径写 JOIN 语句时想清楚方向就足够。3. 核心功能实现挂号系统的关键细节这一节是整个项目的命脉。把排班、并发控制、状态流转这三块讲清楚你的项目就已经越过学生作品的门槛接近一个可以上线的真实系统了。3.1 排班就是库存号源管理才是核心预约挂号的重点看起来是挂号实际上落在号源管理上。排班表就是库存表一个医生一天可以有上午、下午两个时段每个时段放 30 个号那么 schedule 表里就有两条记录每条的 total_count 是 30remain_count 初始也是 30。患者挂号时系统要做的事是对号源进行一次原子性扣减。这不是普通的 update 语句最直接的做法是在 SQL 层面加入条件UPDATE schedule SET remain_count remain_count - 1 WHERE id #{scheduleId} AND remain_count 0;这条语句能保证同一时刻只有一个事务成功扣减号源。如果 update 返回的影响行数是 0说明已经没有号了直接提示号源已约满。这个方案比先 SELECT 再 UPDATE 安全得多是解决并发问题的第一道防线。3.2 并发挂号不超卖我实测过的三种方案我第一次做这个功能的时候天真地以为直接写个先查再扣的 Service 就完事了。直到我用 Jmeter 模拟 100 个并发请求同时抢 30 个号发现有 14 个人都显示挂号成功。事后分析是因为判断剩余号数和扣减之间不是原子操作两个线程同时读到剩余 1于是都往下执行了。解决这个问题实测有效的主流方案有三种方案实现方式优点缺点SQL 原子更新update 时加 remain_count 0 条件最简单、实施成本低对数据库连接消耗较多乐观锁表增加 version 字段更新时比对 version并发冲突少时性能好冲突多时需重试逻辑分布式锁用 Redis SETNX 锁定 scheduleId适合高并发分布式场景引入外部依赖 Redis增加维护成本对于毕设而言我推荐第一种 SQL 原子更新为主同时把事务和唯一索引兜底做上。再加上在挂号记录表里对schedule_id patient_id建一个唯一索引这样同一个患者对同一个排班最多只能有一条有效挂号记录双保险。答辩时你把这个设计讲出来已经很能说明你对并发和数据一致性的理解。3.3 状态流转与自动通知挂号成功后患者的挂号单应该处于已预约状态。这里有一个常被忽略的细节患者可能一直不就诊系统需要在就诊日期过后自动把记录置为已过期同时更新号源。这个逻辑不适合等用户手动触发要借助 SpringBoot 的定时任务Scheduled(cron 0 0 2 * * ?) // 每天凌晨2点执行 public void autoExpireAppointments() { // 找出所有就诊时间早于当前时间且状态仍为已预约的记录 // 批量更新状态为 EXPIRED // 如果当时没扣号源就不需要回补已扣的要核对逻辑 }在使用 Scheduled 之前需要在启动类上加上 EnableScheduling 注解。这个不起眼的功能在答辩时非常好讲因为它体现了你考虑了系统的自维护能力。至于通知机制毕设阶段不建议直接对接短信服务商成本高、审核也麻烦。一个比较容易落地的方案是用 Spring 的 JavaMailSender 发送邮件模板来通知患者。如果你不想引入邮件依赖也可以只在系统站内信表里写一条消息挂号成功后在页面右上角显示未读提醒。别小看这个细节很多评审老师就是凭这种接近真实产品的体验给你打高分的。4. 实操过程从空项目到一个能跑的挂号平台理论铺垫够多了现在进入正经的动手环节。我从初始化项目开始带你过一遍真正写代码时该怎么做。4.1 用 Spring Initializr 快速建好骨架第一步是在 start.spring.io 上生成项目也可以直接用 IDEA 内置的 Spring Initializr。我建议你选择的依赖最好就这几个后续不够再加Spring Web、Spring Data JPA、MySQL Driver、Lombok如果你想用 MyBatis 就换成 MyBatis Framework 和 MySQL Driver。对于毕设来说JPA 的自动建表能力可以省掉很多写 SQL 的精力但如果你的脑子里已经有很清晰的关系模型用 MyBatis 写 SQL 控制力更强也方便在文档里贴 SQL 语句。两者都是很好的选择重点是别把两种混着用。生成后打开 application.yml最基础配置如下server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/hospital?useSSLfalseserverTimezoneAsia/Shanghai username: root password: 123456 jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true这里使用 serverTimezone 是很有必要的如果缺失很多 MySQL 版本会报时间相关的连接错误。端口号也建议保持 8080因为后面前端页面联调时代理配置默认就指向它。4.2 实体类与数据访问层的写法以最核心的挂号记录表为例。使用 JPA 的话实体类可以这样定义Entity Table(name appointment) public class Appointment { Id GeneratedValue(strategy GenerationType.IDENTITY) private Long id; Column(name patient_id) private Long patientId; Column(name schedule_id) private Long scheduleId; Column(name appointment_code) private String appointmentCode; // 挂号单号 Column(name status) private Integer status; Column(name create_time) private LocalDateTime createTime; // getter/setter 省略 }使用 Lombok 的 Data 注解可以省掉 getter/setter 的冗余代码。为了方便后续操作Service 层实际上建议通过 Repository 接口写这样的方法public interface AppointmentRepository extends JpaRepositoryAppointment, Long { long countByPatientIdAndScheduleIdAndStatusNot(Long patientId, Long scheduleId, Integer status); ListAppointment findByPatientIdOrderByCreateTimeDesc(Long patientId); }这里的第一条方法就实现了同一个患者对同一个排班不能重复预约的查询规则配合唯一索引就是双保险。写代码时你会发现数据层的方法命名越接近自然语言后期维护越省心。4.3 Service 层写业务逻辑Service 层是整个系统最值得展示的地方因为业务规则全在这里。一个完整的挂号 Service 方法大概是这样的Transactional public Result createAppointment(Long patientId, Long scheduleId) { Schedule schedule scheduleRepository.findById(scheduleId) .orElseThrow(() - new RuntimeException(排班不存在)); // 校验排班日期是否已过期 if (schedule.getScheduleDate().isBefore(LocalDate.now())) { return Result.error(该排班已过期); } // 原子扣减号源 int updated scheduleRepository.deductRemainCount(scheduleId); if (updated 0) { return Result.error(号源已约满); } // 创建预约记录 Appointment appointment new Appointment(); appointment.setPatientId(patientId); appointment.setScheduleId(scheduleId); appointment.setStatus(OrderStatus.PENDING); appointment.setAppointmentCode(generateCode()); appointment.setCreateTime(LocalDateTime.now()); appointmentRepository.save(appointment); return Result.success(appointment); }这里我用了一个自定义的 deductRemainCount 方法对应的 Modifying 查询你们可以自己写 JPQL 实现。别忘了在方法上标注 Transactional否则事务可能不会在异常时自动回滚结果会出现扣了号但没有生成预约记录的问题。4.4 Controller 层设计 REST APIController 层遵循一个简单原则只做参数接收、调用 Service、返回统一结果。给患者用的接口大概长这样RestController RequestMapping(/api/appointment) public class AppointmentController { Autowired private AppointmentService appointmentService; // 创建挂号 PostMapping(/create) public Result create(RequestParam Long scheduleId) { Long userId currentUserId(); // 从登录态获取 return appointmentService.createAppointment(userId, scheduleId); } // 取消挂号 PostMapping(/cancel/{id}) public Result cancel(PathVariable Long id) { Long userId currentUserId(); return appointmentService.cancelAppointment(userId, id); } // 我的挂号记录 GetMapping(/my) public Result myAppointments() { Long userId currentUserId(); return appointmentService.listMyAppointments(userId); } }currentUserId 从 Session 或 Token 里面取毕设阶段用 Session 就够了。统一返回体 Result 可以设计为包含 code、message、data 三个字段所有接口都走相同的格式前端处理起来会非常舒服。4.5 前端联调Vue 还是原生页面如果你不是纯后端我建议用 Vue 3 Element Plus 写一个简单后台界面它能做出很专业的表格、表单、弹窗效果。如果从来没写过 Vue也不建议放弃前端只能写死 HTML。这里有一个折中方案用 Vue 的 CDN 方式引入不需要 Node 环境也能跑。!DOCTYPE html html langzh head meta charsetUTF-8 script srchttps://unpkg.com/vue3/dist/vue.global.js/script script srchttps://unpkg.com/element-plus/script /head body div idapp el-table :dataappointments el-column propappointmentCode label挂号单号/el-column el-column propcreateTime label预约时间/el-column el-column label操作 template #default{ row } el-button clickcancel(row)取消/el-button /template /el-column /el-table /div script const { createApp, ref, onMounted } Vue; createApp({ setup() { const appointments ref([]); const load async () { const res await fetch(/api/appointment/my); const data await res.json(); appointments.value data.data; }; onMounted(load); return { appointments }; } }).use(ElementPlus).mount(#app); /script /body /html把这段 HTML 放到 SpringBoot 的 static 目录下启动项目直接访问 8080 端口就能看到效果。用 fetch 调用后端接口整个过程不需要额外的前端服务器非常简单。这一招对于时间紧的毕设来说真是救命稻草。4.6 前后端联调中的跨域问题如果你偏偏用了一个独立的前端工程启动在 5173 端口而后端在 8080那你一定会遇到跨域问题。解决方式有两种一种是在后端写一个 CORS 配置类另一种是在前端 Vite 配置 proxy。我比较推荐后端配置因为代码量少而且体现你对 HTTP 协议的理解Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意 allowCredentials 设置为 true 时allowedOriginPatterns 不能简单的用 * 而应该使用具体域名或模式否则某些浏览器会拒绝带凭证的请求。这个细节也属于实践里才能学到的内容。5. 踩坑实录那些教程里没写清楚的坑这一节我把实际开发中和帮学生调试时遇到的问题整理出来。这些问题非常典型你有很大概率也会遇到。5.1 并发测试下挂号记录为什么会重复现象用 Jmeter 并发请求 20 次抢号数据库里出现多条相同 patientId 和 scheduleId 的挂号记录。排查思路第一步先看是否是同一次请求被重复提交如果是前端按钮需要做 loading 状态处理。第二步检查 Service 层是否有校验重复预约的代码——你如果用先查后插入的方式在两个并发请求同时启动时两个线程都查不到记录自然就都插入成功了。解决办法就是把查 插变成原子扣号 唯一索引的方案前面已经介绍过了。我还测试过一种组合方案在 schedule 行上加悲观锁SELECT ... FOR UPDATE也能解决但吞吐会更低毕设场景下没有必要。5.2 登录状态怎么在前后端维持如果你用 Session 来维持登录态万万注意前后端分离场景要开启 CORS 的 allowCredentials(true)并且保证前端 fetch 请求加上 credentials: include。我第一次写的时候第一步能登录成功刷新页面就丢登录状态查了半天发现是跨域配置里没放行 Cookie。后来我干脆在实现里引入了 JWT 的方案登录成功后生成一个 token 返回给前端前端请求接口时放到 Authorization 请求头里后端通过拦截器解析 token 并获取 userId。这个方案在做完 Session 版本后可以当成进阶优化答辩时提起来会让老师觉得你不仅会实现还在考虑扩展性和分布式环境下的会话问题。但我得说清楚bool 题能通过Session 足够想拔高JWT 是性价比很高的选择。5.3 预约日期的时区和格式坑有一个非常隐蔽的坑就是 Java 后端和前端在不同时区或不同时间格式下的展示不一致。排班日期你存的是 LocalDate创建时间存的是 LocalDateTime前端拿到的 JSON 可能是 ISO 标准字符串也可能因为你配置了时间序列化格式而变成时间戳。如果你在返回 JSON 时不做处理前端用 Element Plus 的表格显示创建时间时可能会显示出一个奇怪的长字符串而不是2025-05-20 10:30这样可读的内容。推荐在 application.yml 里配置spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai这样后端返回的 LocalDateTime 就会被格式化成可读时间。这一点老师不一定会问但做的时候不处理演示时会显得非常不专业。5.4 微信/支付宝支付要不要做很多同学问我这个系统要不要接支付。我的答案是千万别在毕设里做真实的在线支付。整个流程需要商户号、密钥、法人认证而且个人的申请流程很麻烦。你可以在页面里做一个去支付按钮的模拟流程点击后模拟支付回调更新订单状态为已支付。在文档里写明本系统演示环境下使用模拟支付生产环境可对接微信支付服务商接口。这样既展示了你的业务完整性又避开了现实中的资金风险。6. 一些加分项和答辩经验到了这个阶段系统基本能跑通了。想拿高分你还可以在细节上打磨。我挑几个实际效果最明显的做法给你参考。6.1 让系统看起来更完整的小功能挂号单号生成用时间加随机数生成格式如GH202505200001比自增 ID 更有真实感数据统计分析在管理员端展示每天、每周挂号量的图表可以用简单的柱状图库实现导出 Excel用 EasyExcel 或者 Apache POI 把挂号记录导出为表格文件这个功能在答辩现场演示效果很好页面加载优化给前端页面写一个简单的 loading 遮罩虽然不影响功能但整个系统的质感会提升一个档次6.2 答辩时怎么讲这个项目讲项目切忌从建表开始念代码。我建议你按业务背景 → 系统设计 → 核心难点 → 个人收获的顺序讲。业务背景一句话带过系统设计重点讲角色权限和数据表关系核心难点重点讲并发号源控制和状态流转个人收获讲你踩过哪些坑、怎么解决的。最好画一幅简单的系统架构图可以用画图工具或者手画打印出来贴在论文里或答辩PPT中帮助老师快速理解整体结构。老师大概率会问这么几个问题为什么用 SpringBoot 而不用 SSM此时可从自动配置、内置服务器、开发效率角度作答数据库为什么这么设计重点回答表之间的关联关系和状态字段带来的好处如果挂号人数瞬间上万系统会怎么样这时可以说你的方案如何应对以及后续可能要引入消息队列、Redis 缓存等优化手段这几个问题答好了答辩就稳了。6.3 后面还能怎么扩展这个项目后续扩展空间非常充裕。你要是想继续完善可以加入科室排班的时间段粒度细化、医生的接诊量统计、患者的就诊历史健康档案也可以把预约规则改成分时段限额的精细模式。技术层面则可以引入 Redis 做缓存和分布式锁用 RabbitMQ 处理挂号后的异步通知。每一次扩展都会让这个项目从一个毕设变成一个真正可落地的产品雏形你写在简历上的分量也会完全不同。做这个系统的整个过程我最大的体会是毕设不是比谁的项目名好听而是比你对真实业务的理解和对技术方案的取舍能力。预约挂号系统这个题业务不复杂但足够完整技术不前沿但每个点都能往深处挖。你把它做成什么样它就能反映出你是什么样的开发者。把这篇文章里提到的设计思路、并发控制、踩坑经验真正搞懂、动手实现一遍无论最后成绩如何你学到的这些东西会在以后的实际工作中持续给你回报。
上一篇/下一篇内容由系统自动关联 返回资讯列表 →