☰
Java SpringBoot实战:基于Web的网上预约挂号系统设计与实现
2026/10/1 4:55:59 网站建设 项目流程

这个选题我太熟了。每年毕业季都有学生来问:计算机毕设想做点实际的东西,Java 和 SpringBoot 怎么写,选什么题目好。今天说的这个"基于 Web 的网上预约挂号系统",就是其中出现频率相当高的一个。它是 Java+SpringBoot 的经典落地场景,前后端配合完成医院网上预约挂号平台,麻雀虽小五脏俱全,既能展示你对 Java 后端、SpringBoot 框架、Web 开发的理解,又不会复杂到写不完。适合拿来做毕设,也适合刚学完 Java 想练手的人照着复刻一遍。

我最初接触这个项目是帮一个学弟改答辩前的代码,后来自己又在工作里用 SpringBoot 重构过类似的信息管理系统。这套系统的价值不在于"挂号"这两个字,而在于它把用户权限、业务状态流转、数据一致性、并发控制这些后端工程师天天面对的问题,浓缩在一个能看得见、能演示的项目里。下面我会从设计思路、核心实现、完整实操到避坑经验一条线讲清楚,你想拿它当毕设也好,想学 SpringBoot 实战也好,这份梳理都值得收藏。

1. 为什么这个选题在毕设里"烂大街"却依然靠谱

很多人一听别人做过就担心重复,实际上每年几百万毕业生,单靠题目本身很难做到独一无二。评判一个好毕设的核心标准从来不是题目新不新鲜,而是你有没有把里面的业务想透、把技术点做扎实。预约挂号系统就是典型的"题目常见、深度可调"类型,你的发挥空间非常大。

1.1 业务逻辑天然清晰,评委一眼能看懂

答辩现场最尴尬的事情,就是你站在台上讲了十分钟,评委老师还不知道你在做什么。预约挂号系统不存在这个问题。所有人都去医院挂过号,都知道"选科室—选医生—选时间—挂上号"是什么意思。当你的系统演示完登录、选号、预约、取消、医生出诊管理这一套流程,不用额外解释,评委的代入感就已经建立了。

这种业务场景熟悉度,对毕设的现场表达有巨大帮助。你可以把大部分精力放在展示技术亮点上,而不是反复解释业务背景。而且系统边界也异常清楚:面向患者的是预约入口,面向医生的是排班出诊,面向管理员的是基础数据管理。三个角色三个界面,功能划分一目了然,这本身就是软件工程里最看重的模块化思维。

1.2 SpringBoot+Java 这个组合的底气在哪

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 是 30,remain_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?useSSL=false&serverTimezone=Asia/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 JpaRepository<Appointment, Long> { long countByPatientIdAndScheduleIdAndStatusNot(Long patientId, Long scheduleId, Integer status); List<Appointment> 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 API

Controller 层遵循一个简单原则:只做参数接收、调用 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 lang="zh"> <head> <meta charset="UTF-8"> <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> <script src="https://unpkg.com/element-plus"></script> </head> <body> <div id="app"> <el-table :data="appointments"> <el-column prop="appointmentCode" label="挂号单号"></el-column> <el-column prop="createTime" label="预约时间"></el-column> <el-column label="操作"> <template #default="{ row }"> <el-button @click="cancel(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 处理挂号后的异步通知。每一次扩展都会让这个项目从一个毕设变成一个真正可落地的产品雏形,你写在简历上的分量也会完全不同。

做这个系统的整个过程,我最大的体会是:毕设不是比谁的项目名好听,而是比你对真实业务的理解和对技术方案的取舍能力。预约挂号系统这个题,业务不复杂但足够完整,技术不前沿但每个点都能往深处挖。你把它做成什么样,它就能反映出你是什么样的开发者。把这篇文章里提到的设计思路、并发控制、踩坑经验真正搞懂、动手实现一遍,无论最后成绩如何,你学到的这些东西会在以后的实际工作中持续给你回报。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询