SpringBoot医院挂号系统如何实现?并发扣号、Redis缓存与避坑指南 简介基于SpringBoot的医院挂号系统Java毕业设计项目源码面向计算机相关专业毕业生与Java后端初学者。项目围绕医院日常挂号场景涵盖科室管理、医生排班、患者信息、挂号收费等核心模块Controller、Service、Mapper分层清晰适合作为毕业设计选题参考或课程设计实践。压缩包共含108个文件以77个Java源文件为主配合10个XML配置、14个JPG与4个PNG图片素材还有YML环境配置、SQL数据库脚本及一个HTML静态页面整体体积仅12.43MB便于下载和本地部署。已有1555人学习使用项目包含完整可运行的SpringBoot工程代码、数据库初始化脚本及配套页面素材可直接导入IDE启动调试通过阅读核心控制器与业务实现类可以掌握SpringBoot下Web层与业务层的协作写法对理解医院信息系统的业务流程和相关模块开发有实际帮助。1. 基于SpringBoot的医院挂号系统到底是什么这不是一道增删改查题每年毕业季都会有一大批Java方向的选题落在“XX管理系统”上而基于SpringBoot的医院挂号系统是其中少有的“看起来常规、做起来有讲究”的题目。表面上是科室、医生、排班、挂号单这几张表的CRUD但真正把它做完的人会发现挂号业务天然带着“并发扣号”“状态流转”“超时释放”这些后端面试最喜欢追问的点。这篇文章我会从业务拆分、表设计、核心接口、缓存与定时任务、常见坑位一路讲到答辩前的可验证清单让你拿到这个项目标题时能直接照着搭出一套能跑、能讲、不怕问的实现。这套方案适合三类人需要完成Java毕业设计并希望答辩有东西可讲的学生学完了SSM但缺一个完整业务闭环的初级开发者以及想把“会写接口”升级成“会做业务系统”的后端新人。下面从业务地图开始。2. 把医院挂号拆成一张业务地图角色、两条核心链路与六张表2.1 多角色权限为什么毕业设计用拦截器加JWT就够了医院挂号系统的第一个问题不是怎么写接口而是系统里有几类人、各自能干什么。最常见的最小划分是三个角色管理员维护科室、医生、排班和号源数量医生查看自己的排班与已挂号患者患者在前端完成注册、查科室、选医生、挂号与退号。权限这块的落地路径我不建议一上来就上Spring Security加OAuth2那套完整方案。毕业设计的时间成本和答辩追问点不成正比。更常见的做法是SpringBoot内置拦截器HandlerInterceptor配合一个简单的Token机制——登录成功后在服务端生成Token把用户ID和角色存进去后续请求在拦截器里校验Token并取出角色字段做接口级权限判断。这样既能讲清楚“认证与授权”的基本原理又不会因为Spring Security的过滤器链配置问题卡住进度。这里有两个参数在实现时要定好一是Token的有效期建议设成30分钟或2小时并在Redis里存一份服务端会话用于主动失效二是角色判断放在拦截器里做不要散落在每个Controller方法里对着user.getRole()做if判断。不然等接口多了权限逻辑会变成一团乱麻。2.2 两条核心业务链路预约锁号与退号回补挂号和退号是整个系统的心脏也是答辩时可以展开讲十分钟的业务闭环。先看挂号链路患者进入科室列表选择一个医生的某个日期时段此时前端展示“剩余号源数”点挂号后后端先锁定一个号再生成挂号单。注意这里锁号不是把页面上那个数字减一就算完而是要让这个减一操作在数据库层面是原子性的。再看退号链路患者取消挂号系统把挂号单状态改成“已取消”同时把对应排班的剩余号源回补1。这里最容易被忽略的是回补操作要和状态更新在同一个事务里否则会出现挂号单已经取消、号源却没有恢复的脏数据。这两条链路会引出一个所有挂号系统都绕不开的状态机问题。我通常把挂号单的状态定义为待支付、已支付、已取号、已就诊、已取消、已退号。其中退号和取消的区别在于取消是患者主动取消未就诊的挂号单退号通常发生在医生停诊时由系统批量处理两者最终都释放号源但触发方不同。这个状态枚举在后面的第6章会给出可运行的代码。2.3 核心数据表设计从用户表到挂号单的最小闭环表设计直接决定后面写代码是顺滑还是反复返工。我推荐用六张表撑起这个系统用户表、医生表、科室表、排班表、挂号单表、号源流水表。用户表放患者的账号密码手机号医生表通过user_id关联登录账号同时存科室id和职称科室表只存科室名称和简介排班表存医生id、科室id、日期、时段和总号源数挂号单表存排班id、患者用户id、就诊人信息、状态号源流水表用于记录每一次锁号和回补操作这样对账时有据可查。用一个表格把这六张表的最小字段列出来字段名以实际开发时常用的风格为准表名关键字段说明sys_userid, username, password, role, phonerole区分管理员/医生/患者departmentid, name, description科室doctorid, user_id, department_id, title医生绑定登录用户与科室scheduleid, doctor_id, department_id, date, time_slot, total_num, available_num排班与余号registrationid, schedule_id, patient_user_id, patient_name, status, create_time挂号单status存状态码reg_flowid, registration_id, type, remark, create_timetype标记锁号/回补/停诊释放这里面最容易被忽视的是两个约束排班表的(doctor_id, date, time_slot)建议加唯一索引防止同一个医生同一天同一时段被创建两条排班挂号单表的(schedule_id, patient_user_id, date)也要加唯一索引防止患者对同一个排班重复挂号。这两个索引会在第5章和第6章里反复被提到现在先埋下伏笔。外键关系一般用逻辑外键即可不一定要在MySQL里声明物理外键。真正要做的是一张“医生排班在挂号后不能随便删”的业务约束以及管理员删除科室前要检查是否有关联医生和未完成的挂号记录。这一点建议提前写在接口层校验靠数据库物理外键在删除时会报错返回给前端的信息也不友好。表设计到这里系统的主干已经通了。下一步就是真的创建一个SpringBoot项目把这些表映射成代码并把“创建挂号单”和“退号”这两个核心接口写成能跑的最小版本。3. 在IDEA里从零跑通最小挂号模块依赖、配置与事务接口3.1 项目初始化JDK 1.8 SpringBoot 2.7.18 MyBatis-Plus 的选型理由很多人在项目初始化这一步就开始纠结版本网上教程的SpringBoot版本从2.3到3.2五花八门照着抄很容易踩坑。我建议不要追新直接选SpringBoot 2.7.18配合JDK 1.8。原因有三条2.7.x是3.x之前最后一个稳定大版本网上能找到的资料和排错经验最全很多高校的毕业设计验收环境还在用JDK 1.8SpringBoot 3.x强制要求JDK 17答辩现场换环境容易出问题MyBatis-Plus和PageHelper这些常用组件对2.7.x的兼容性最好。创建方式就直接在IDEA里选Spring Initializr把Type选成MavenJava版本选8依赖勾上Spring Web、MyBatis、MySQL Driver、Validation。下面是最小可运行的pom.xml关键依赖注意不要一股脑把整个文件贴过来只看这些核心坐标parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.5/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependencies这段配置里最关键的是SpringBoot父依赖锁定2.7.18所有starter的版本都由它统一管理不用自己写版本号。MyBatis-Plus要单独指定版本因为父依赖不会管理它。建议学SpringBoot的时候留意一下它的自动装配原理spring.factories文件里注册了各种AutoConfiguration你在pom里引入一个starter它就会自动配置对应的Bean这也是为什么引入依赖后几乎不用写配置类。答辩被问到“SpringBoot为什么能简化开发”时从自动装配切入是回报率最高的答法。接下来是application.yml我习惯把数据源、MyBatis-Plus、日志这三块先配好server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/hospital?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0这里有两个参数会直接影响后面的开发体验map-underscore-to-camel-case开启后数据库里的create_time能自动映射到Java字段createTime少写很多TableFieldlogic-delete配置让MyBatis-Plus的删除操作变成逻辑删除业务数据不会真的从库里消失对排班和挂号单这类敏感数据来说等于加了后悔药。日志级别打印SQL可以在开发时看MyBatis实际执行的语句排查问题非常有用上线前记得关掉。3.2 一张挂号单的诞生创建接口与退号接口的完整代码现在写整个系统最核心的两个接口创建挂号单和退号。Controller层保持干净只做参数接收和统一返回Api(tags 挂号接口) RestController RequestMapping(/api/registration) public class RegistrationController { Resource private RegistrationService registrationService; ApiOperation(创建挂号单) PostMapping(/create) public ResultString create(RequestBody Valid RegisterRequest request) { String regNo registrationService.createRegistration(request); return Result.success(挂号成功单号 regNo); } ApiOperation(退号) PostMapping(/cancel) public ResultString cancel(RequestParam Long registrationId) { registrationService.cancelRegistration(registrationId); return Result.success(退号成功); } }Valid注解会触发RegisterRequest里的字段校验减少Service层的if判断。真正干活的是Service实现类这两个方法我会这样写Service public class RegistrationServiceImpl implements RegistrationService { Resource private ScheduleMapper scheduleMapper; Resource private RegistrationMapper registrationMapper; Override Transactional(rollbackFor Exception.class) public String createRegistration(RegisterRequest request) { Long scheduleId request.getScheduleId(); Long patientId request.getPatientId(); Schedule schedule scheduleMapper.selectById(scheduleId); if (schedule null) { throw new BizException(排班不存在); } if (schedule.getAvailableNum() 0) { throw new BizException(该时段号源已约满); } int updated scheduleMapper.deductAvailableNum(scheduleId); if (updated 0) { throw new BizException(号源被抢完请重新选择时段); } Registration registration new Registration(); registration.setScheduleId(scheduleId); registration.setPatientUserId(patientId); registration.setPatientName(request.getPatientName()); registration.setStatus(RegistrationStatus.CREATED.getValue()); registration.setRegNo(generateRegNo()); registrationMapper.insert(registration); return registration.getRegNo(); } Override Transactional(rollbackFor Exception.class) public void cancelRegistration(Long registrationId) { Registration registration registrationMapper.selectById(registrationId); if (registration null) { throw new BizException(挂号单不存在); } int currentStatus registration.getStatus(); if (currentStatus ! RegistrationStatus.CREATED.getValue() currentStatus ! RegistrationStatus.PAID.getValue()) { throw new BizException(当前状态不允许退号); } registration.setStatus(RegistrationStatus.CANCELED.getValue()); registrationMapper.updateById(registration); int restored scheduleMapper.restoreAvailableNum(registration.getScheduleId()); if (restored 0) { throw new BizException(号源回补失败请联系管理员); } } }两个方法都加了Transactional这是整个实现里最不能省的一步。创建挂号单的事务边界是扣减号源和插入挂号单必须同生共死任何一个失败都要回滚。退号的事务边界是更新挂号单状态和回补号源必须同生共死否则就会出现状态是已取消、余号没加回来的脏数据。事务注解上有两个参数值得说rollbackFor Exception.class表示所有异常都触发回滚不加的话Spring默认只在RuntimeException时回滚而自定义的BizException如果继承的是Exception类就会静默提交这是很多事务失效的根因。另一个是Mapper里的条件更新方法ScheduleMapper中需要写这样的SQLUPDATE schedule SET available_num available_num - 1 WHERE id #{scheduleId} AND available_num 0这个SQL是并发控制的关键。先select再update会有两个线程同时读到available_num1然后各自扣减最终结果变成-1。而把判断条件写进UPDATE的WHERE子句后数据库行锁会保证同一时刻只有一个扣减能成功返回的行数updated1另一个线程updated0就会走进号源不足的分支。这就是把“先检查后操作”改成“条件更新”的典型用法。退号方法里我刻意保留了状态判断而不是无条件直接改状态。对于挂号单而言已就诊的不能退已取消的不能再退这种状态约束放到业务代码里比依靠前端按钮的显隐更可靠。3.3 统一返回体与Swagger让接口可以直接演示给老师看所有接口的返回格式建议统一成一个Result类不然前端处理返回值时要针对每个接口写判断老师看代码时也会觉得混乱。这个类不复杂但它是整个项目里被引用最多的类之一Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 200; result.message success; result.data data; return result; } public static T ResultT fail(String message) { ResultT result new Result(); result.code 500; result.message message; return result; } }这里有个容易被忽略的细节Result里的泛型T在校验框架抛出MethodArgumentNotValidException时返回给前端的结构会变所以建议额外写一个全局异常处理器用RestControllerAdvice把校验异常和业务异常统一转成Result格式。这样Swagger里展示的所有接口返回结构都是一致的。Swagger配置建议直接用springfox的2.9.2版本加一个简单的配置类Configuration EnableSwagger2 public class SwaggerConfig { Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2) .select() .apis(RequestHandlerSelectors.basePackage(com.hospital.controller)) .paths(PathSelectors.any()) .build() .apiInfo(new ApiInfoBuilder() .title(医院挂号系统接口文档) .version(1.0) .build()); } }basePackage这里写你的Controller实际所在包路径如果写错或者启动类和Controller不在同一个根包下会出现Swagger页面打开却扫不到接口的情况。如果想省这个麻烦直接用.apis(RequestHandlerSelectors.withClassAnnotation(Api.class))按注解扫描这在第5章避坑里会进一步说明。4. 把挂号量加上去Redis缓存、定时任务与接口限流的三个关键配置4.1 用Redis缓存医生排班key设计、过期时间与缓存穿透排班余号是挂号系统里读多写少的典型场景患者进入医生详情页就要查一次剩余号源但余号真正变化只发生在挂号成功的瞬间。把这个数据直接放数据库也能跑但一旦并发上来频繁的COUNT查询会拖慢整个接口。更常见的做法是把“余号数量”热数据放进Redis数据库只存排班基础信息和总量。缓存key的设计通常用冒号分层比如schedule:available:10011001是排班IDvalue存剩余号数过期时间控制在30到60分钟。为什么不是永久不过期因为退号、停诊释放这些操作都会修改余号设置过期时间可以在极端情况下自动让缓存和数据库重新对齐避免脏数据长期存活。读取逻辑要注意一个小坑我习惯把“缓存里没有这个key”和“缓存里没查到值”区分开。正常流程是先查缓存没有就查数据库查到了回填缓存并设置过期时间。但如果是数据库里也不存在这个排班这个空结果不会回填缓存下一次请求又会打到数据库这就是缓存穿透。解决的方法很简单在查询排班基础信息后如果确实为空就向缓存写入一个空值字符串过期时间设置成60秒成本极低但能挡住一大半的无效穿透。public Integer getAvailableNum(Long scheduleId) { String key schedule:available: scheduleId; String cached redisTemplate.opsForValue().get(key); if (cached ! null) { return Integer.parseInt(cached); } Schedule schedule scheduleMapper.selectById(scheduleId); Integer num schedule null ? 0 : schedule.getAvailableNum(); redisTemplate.opsForValue().set(key, String.valueOf(num), 60, TimeUnit.SECONDS); return num; }这段代码里的TimeUnit.SECONDS指定了过期时间单位60秒是缓存穿透防御的短TTL方案。真正影响这个缓存正确性的不是读而是写也就是4.2节里定时任务做停诊释放余号的时候更新完数据库一定要同步删除或更新缓存而且顺序上建议“先更新数据库再删缓存”这个WaitAndSleep的做法能避免并发读把旧值回填进缓存。这也是很多项目后来演进成Canal订阅binlog同步缓存的原因毕设阶段手动调用delete即可。4.2 定时任务处理停诊cron表达式与多实例环境下的重复执行医院挂号系统里天然需要一个“后台兜底”机制来应对两种场景医生临时停诊需要把该医生未来若干天的排班状态改成停诊同时把已挂号的挂号单批量退号患者创建挂号单但一直没支付超过一定时间后系统要自动释放这个号源。SpringBoot的Scheduled配合cron表达式实现这两种后台任务最简单。先看一个释放超时未支付号源的任务Component public class ScheduleTask { Resource private RegistrationMapper registrationMapper; Resource private ScheduleMapper scheduleMapper; Scheduled(cron 0 */5 * * * ?) public void releaseExpiredRegistrations() { ListRegistration expiredList registrationMapper.selectExpiredCreatedOrders(30); for (Registration reg : expiredList) { registrationMapper.updateStatusById(reg.getId(), RegistrationStatus.CANCELED.getValue()); scheduleMapper.restoreAvailableNum(reg.getScheduleId()); } } }cron表达式在这一行要拆开理解秒、分、时、日、月、星期0 */5 * * * ?表示每分钟的第0秒开始每5分钟执行一次。30这个参数是“超过30分钟未支付就释放”的业务阈值可以在application.yml里配置也可以在Mapper的SQL里固定写我更推荐配置化这样产品提“改成15分钟”时不用改代码。定时任务有一个隐藏坑如果你把项目部署成多个实例Scheduled会在每个实例上同时执行同一个任务被多个进程跑了一遍又一遍。这在单机调试时看不出来部署后就会出现重复退号、重复回补。最朴素的解法是用一个任务锁表执行任务前先尝试往表里插入一条带任务名的记录插入成功的人才能继续执行失败的人直接return。表里加唯一索引就能保证同一时刻只有一个实例拿到锁任务结束后删除这条记录。这个坑在第5章会展开细讲。4.3 给挂号接口加一个简单的限流拦截器挂号系统在放号瞬间的并发是所有接口里最高的尤其是热门科室的号几十个人同时抢一个号源是常态。毕业设计不需要上Sentinel或Guava RateLimiter那么重的方案但完全不做限流又显得对高并发场景没有意识。我常用的做法是写一个轻量拦截器基于ConcurrentHashMap做固定窗口计数。Component public class RateLimitInterceptor implements HandlerInterceptor { private static final MapString, AtomicInteger COUNTER new ConcurrentHashMap(); private static final int LIMIT 100; private static final int WINDOW_SECONDS 60; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String key request.getRequestURI(); AtomicInteger counter COUNTER.computeIfAbsent(key, k - new AtomicInteger(0)); int count counter.incrementAndGet(); if (count LIMIT) { response.setStatus(429); response.getWriter().write(请求过于频繁请稍后再试); return false; } return true; } }这段代码有两个参数LIMIT是每个接口每分钟最多100次请求WINDOW_SECONDS是窗口宽度注意这里用ConcurrentHashMap只能实现固定窗口不是滑动窗口临界点会有一瞬间的突发流量但毕业设计完全够用。它最大的问题是窗口重置逻辑上面的代码把重置交给了外层定时任务清空Map或者按分钟生成不同的key来天然过期否则计数器永远不会归零。真正上线时会换成Redis的INCR加EXPIRE但原理是一样的请求进来先计数计数超过阈值直接拒绝。写到这里系统已经能跑、能扛一定并发了。接下来要给读者最实用的一个章节——把这些天开发过程中一定会踩到的坑集中讲一遍每条都是血泪经验。5. 医院挂号系统SpringBoot实现中的常见坑与排查5.1 同一时段同一医生被重复挂号并发下先查后扣的经典翻车现象两个患者同时提交挂号请求系统里生成了两张挂号单都指向同一个医生的同一个时段但排班表里的余号只减少了1。原因代码在一开始写的是selectById查看余号余号大于0就继续操作然后再执行update扣减。但这两个操作不是原子的线程A和线程B同时查到了余号1然后各自认为自己抢到了号插入挂号单时因为缺少唯一索引两条记录都成功落库。解决两个动作一起做。第一在schedule表上针对(doctor_id, date, time_slot)建唯一索引从数据源头上保证不能重复排班。第二扣减余号必须使用带条件的UPDATE即UPDATE schedule SET available_num available_num - 1 WHERE id ? AND available_num 0当返回行数为0时抛出“号源已满”让第二个请求失败。第三在registration表上建(schedule_id, patient_user_id)唯一索引即使业务层漏了数据库也会兜底拒绝重复挂号。这三层一起上才能做到真正的防重复。5.2 退号回补号源失败余号变成负数现象患者退号之后排班表里的available_num没有恢复甚至在下一次有人挂号时变成了-1。原因退号方法里先更新了挂号单状态再执行号源回补但这两个操作不在同一个事务里。更新状态成功后抛出异常事务已经提交回补操作没有执行数据就变成“挂号单已取消但余号没变”。更隐蔽的情况是回补SQL本身没加条件把已经满的号源又加了一次。解决退号接口务必加Transactional把状态更新和available_num available_num 1放在同一个事务里。回补时也要先判断当前余号是否已经等于总号源数如果相等说明状态异常不要继续叠加。出现负数时可以用一条UPDATE把负数拉回0但这不是根治真正要做的是在每次回补前做可恢复性检查。5.3 定时任务部署多实例后重复执行现象本地启动项目一切正常一旦用多个实例部署群里出现大量重复退号通知同一个患者的挂号单被取消了好几次。原因Scheduled是进程内的调度器每个实例都会加载同一个任务并按照cron触发没有全局协调机制。多实例下等于一个任务被执行了N份这些任务操作同一批数据库记录就会产生重复更新甚至因为并发update把数据改乱。解决用一个任务锁表表结构只需要task_name和update_time两个字段task_name加唯一索引。任务执行的第一步是尝试INSERT一条记录插入成功说明获得锁执行真正的业务逻辑最后DELETE掉这条记录插入失败说明有其他实例正在执行直接return。这样既不需要引入XXL-Job这种重组件又能保证最好的情况下只有一个实例在工作。5.4 Swagger打开一片空白或扫不到接口现象启动项目后访问Swagger的UI地址页面能打开但看不到任何接口列表或者显示“No operations defined in spec”。原因最常见的是两个配置问题叠加出现。一是Docket里配置了basePackage(com.hospital.controller)但项目实际的Controller包路径不是这个或者Controller没有加任何注解导致扫描不到二是SpringBoot 2.6及以上版本默认的路径匹配策略是PathPatternMatcher而springfox 2.9.2只支持AntPathMatcher两者不兼容接口全被过滤了。解决Swagger配置里优先用.withClassAnnotation(Api.class)代替basePackage这样只要Controller上标注了Api就能被识别。同时在application.yml里加一行spring.mvc.pathmatch.matching-strategy: ant_path_matcher把匹配策略降级到兼容模式。这两处改完99%的Swagger空白问题都能解决。5.5 删除科室后挂号记录变成孤儿数据现象管理员在后台删掉一个科室几分钟后前端患者详情页报错查挂号记录里的科室信息为空。原因删除科室时只执行了DELETE department WHERE id ?但没有检查哪些排班、挂号单引用了这个科室。如果这些子表又没有物理外键约束删除操作会静默成功留下大量引用不存在科室的孤儿数据。解决不建议做物理删除用逻辑删除字段deleted1把科室标记为不可用。如果一定要物理删除删除前先查三条该科室下有没有医生、有没有未开始的排班、有没有状态为待支付/已支付/已取号的挂号单。任何一条存在就返回“该科室存在关联业务数据不可删除”。对挂号记录这类历史数据查询时也不要JOIN科室表而是把科室名称冗余到挂号单或医生信息里查询性能更好也不会因为科室删除而查不到历史数据。这些都是我在同样的项目里真实踩过的坑每一条的修复成本都不高但每一条都足以在答辩演示时让现场卡住。从这章开始系统已经具备“能稳定跑”的条件最后一步是验证它到底能扛多少压力以及为答辩准备一个有深度的技术点。6. 从能跑通到敢答辩压测、状态机与上线前的检查清单6.1 用JMeter做一次挂号接口压测至少知道系统能扛多少并发答辩时最怕被问“你这个系统能支持多少人同时挂号”。与其支支吾吾不如提前跑一次压测拿到真实数字。用JMeter建一个线程组关键参数如下参数建议值说明Number of Threads100模拟100个并发用户Ramp-Up Period1010秒内全部启动避免瞬时冲击过大Loop Count20每个线程循环20次共2000个请求HTTP请求配置成POST /api/registration/createBody里写死一个排班ID。跑完后看聚合报告里的“吞吐量”和“95%响应时间”正常情况下SpringBoot加MySQL能扛到几百TPS。如果吞量很低或报错率高优先看数据库连接池参数把HikariCP的maximum-pool-size从默认10调到20再测一次你会看到明显的数字变化。这几个数据写进论文或答辩PPT里比任何功能截图都有说服力。6.2 把挂号状态机写成一张表答辩时最有价值的业务亮点我见过太多毕设把状态字段写成字符串在代码里一层一层if判断最后绕成一团。更清爽的做法是先用一个枚举把状态和状态之间的流转规则显式表达出来public enum RegistrationStatus { CREATED(0, 待支付), PAID(1, 已支付), TAKEN(2, 已取号), VISITED(3, 已就诊), CANCELED(4, 已取消), REFUNDED(5, 已退号); private final int code; private final String desc; }状态流转规则可以写成一张表待支付可以流转到已支付或已取消已支付可以流转到已取号或已取消已取号只能流转到已就诊停诊触发时待支付和已支付直接流转到已退号。这张表画在答辩PPT里能让评委立刻看出你对业务状态的理解而不是只会写增删改查。代码层面在Service的cancel方法里用枚举判断当前状态是否允许退号比重复出现magic number要严谨得多。6.3 上线前检查清单从配置分离到Swagger关闭我在部署任何SpringBoot项目前都会过一遍这个清单避免在答辩现场因为一个小配置翻车。开发环境与生产环境用application-dev.yml和application-prod.yml分开prod里数据库地址、Redis地址、日志级别全部换成真实配置把MyBatis的SQL日志级别改成WARN避免答辩时控制台刷出一大堆SQLSwagger在prod环境关闭用Profile(!prod)标注配置类即可。如果部署环境支持Docker一个基础的Dockerfile也就十几行核心是固定JDK 8的基础镜像把jar包拷贝进去后用java -jar启动注意在容器里指定时区为Asia/Shanghai否则定时任务的时间会慢8小时。最后说一个我自己的习惯每次改完代码我不只看接口通不通还会亲自模拟一遍完整流程从注册登录、科室列表、医生排班、挂号成功到退号后看余号是否恢复再重登系统确认状态已经变化。这条路走通一遍心里才踏实。希望帮到你。本文还有配套的精品资源点击获取