基于Spring Boot的社区诊所在线挂号与排队系统设计与实现 1. 这个毕设题目为什么值得做从题面看技术覆盖1.1 题面拆解社区诊所、在线挂号、排队系统分别要求什么带过毕业设计这几年我接得最多的题目类型就是基于Spring Boot的XX管理系统。这个社区诊所在线挂号与排队系统看起来平平无奇但它其实比普通的增删改查系统要高半个档次——普通管理系统只要把数据录进去、列出来就行而挂号排队系统天然带着业务约束和状态流转这两样东西恰恰是答辩老师最爱问、也是区分高分和及格分的分水岭。先拆题面。题干里三个关键词都是有用的社区诊所说明门诊规模小科室数量不会太多用户量也不是三甲医院的并发量级。这意味着我们不需要做微服务、不需要搞消息队列、不需要分布式锁把单机Spring Boot应用做好做完整就够了。很多同学看到“挂号”就往大医院的方向想把系统设计得过度复杂反而容易做不完。在线挂号这是核心业务。患者要选科室、选医生、选日期、选时段提交后占用一个号源。看起来是简单的insert一条记录但真正要处理的是同一时段只剩一个号时多个患者同时抢的问题。这个点可以展开写成论文章节也可以作为答辩时的亮点。排队系统这跟前端的号源不同。患者到店签到后进入候诊队列医生看完一个叫下一个。它要解决的不是并发抢号而是队列顺序和状态推进。过号怎么办、弃诊怎么办、医生临时停诊怎么办这些业务规则如果能在论文里写清楚比代码里贴一堆无意义的方法要加分得多。这个题目适合的人群也很明确Java基础还行但没做过完整Web项目的大三、大四学生或者想用一套中等复杂度的系统作为入门项目的新手。它的难度正好处于能独立完成和需要思考之间的平衡点不会像秒杀系统那样让人无从下手也不会像图书管理那样一眼看穿毫无技术含量。1.2 技术栈选型围绕Spring Boot搭一套能跑完答辩的组合技术选型是我一直跟学生强调不要追求新奇追求稳妥的环节。社区诊所系统的最佳组合如下后端Spring Boot 2.7.x MyBatis-Plus MySQL 8.0前端服务端渲染用Thymeleaf Bootstrap或者前后端分离用Vue 2/Vue 3 Element UI认证简单的Session或JWT都可以可选Redis缓存号源余量但纯数据库方案也能说清楚并发控制这套组合的核心逻辑是Spring Boot撑起项目骨架MyBatis-Plus减少SQL手写量MySQL存业务数据前端不必花哨能完成操作闭环就行。这里单独说一下版本问题。现在网上一搜Spring Boot教程全是3.x的但很多毕设项目代码还是基于2.x。这是一个非常现实的选择题Spring Boot 3.x要求JDK 17如果你本地装的是JDK 8那你只能选2.7.x如果你的机器已经是JDK 17或者21选3.x反而更省事因为新版本依赖管理更现代。关键是保证项目里的所有依赖版本和JDK版本能对上不要出现Spring Boot 2.x配JDK 17这种别扭组合。我后面会专门用一个章节讲运行调试时的版本坑这里大家先记住一个原则以项目里的pom.xml为准而不是以教程里的版本为准。还有一个容易被忽略的点这个系统不需要Redis也能解释清楚并发但如果你在论文里写了使用Redis缓存号源那演示的时候就必须真用Redis否则老师一问Redis里存了什么键就露馅了。我建议是会就上不会就不写不要为了凑技术名词给自己挖坑。2. 数据库设计预约与排队系统的核心是那几个状态2.1 核心表的职责划分五张主表撑起整套业务我见过太多学生在数据库设计这个环节糊弄事建一张巨大的挂号表把所有字段塞进去后面写代码到处取字段改起来想死。实际上社区诊所系统可以拆成五张主表每张表的职责非常清晰表名核心职责关键字段sys_user患者/管理员/医生的统一登录账号id, username, password, real_name, roledoctor_info医生档案id, department_id, name, title, introductionschedule_info医生排班与号源id, doctor_id, work_date, time_slot, total_no, remain_noregistration_info患者预约挂号记录id, user_id, schedule_id, doctor_id, status, create_timequeue_record当日候诊排队记录id, registration_id, queue_no, status, start_time这五张表的关系是用户登录后查doctor_info看有哪些医生再根据schedule_info选定排班插入一条registration_info到院签到后生成一条queue_record。很多同学还会纠结要不要单独建department科室表。我的建议是建因为社区诊所虽然科室不多但有了科室表医生信息可以多一个归属维度论文里画E-R图也更完整。如果嫌麻烦直接在医生表里放一个department_name字符串字段也能过得去——这个属于业务简化跟表结构不规范是两回事。2.2 号源和排队号的状态流转先把状态枚举写清楚再写代码做这种带流程的系统我特别强调先设计状态再写功能。很多人一上来就画页面、写接口结果写到取消预约时要把号源还回去这种逻辑时发现代码已经绕成了一团。其实只要把状态流转图在脑子里过一遍代码只是翻译。预约单registration_info.status的建议取值0 已取消患者取消预约此时要把schedule_info.remain_no加回去1 已预约预约成功但还没到院此时只是占了一个号2 已签到患者到院点击签到后进入候诊队列3 已完成医生叫号并完成诊疗4 爽约预约了没来也没取消这个记录保留下来可以用于统计排队记录queue_record.status的建议取值0 未报到预约已生成但未签到1 候诊中签到完成进入队列等待叫号2 呼叫中医生点了下一个大屏/页面提示当前号码3 就诊中患者进入诊室4 已完成诊疗结束5 过号呼叫了好几次没人应答医生选择跳过这个号大家要注意预约状态和排队状态是两个维度。一个患者可以说预约单状态已签到排队状态候诊中这是正常组合。答辩的时候如果老师问状态为什么不全放在一张表里你可以回答预约单管的是号源生命周期排队列管的是当天到店后的流程两者生命周期不同拆开设计更清晰也不容易互相干扰。这个状态设计还有一个实际的好处写Excel表数据导入和导出时可以用数字存数据库展示的时候转换成中文既节省存储又方便统计。很多同学直接在数据库里存候诊中三个字后面如果要写SQL统计今天有多少人过号就会发现设计得不行的表自己先哭了。3. 核心业务代码如何落笔锁号、签到、叫号这三件大事3.1 预约下单的并发控制同一秒两个人抢最后一个号怎么办这一节是全文最值得细看的也是你答辩时最有利的武器。预约下单的下单操作在系统中的逻辑是前端提交user_id和schedule_id后端查schedule_info判断remain_no 0如果大于0执行扣减号源remain_no remain_no - 1插入一条registration_info记录返回成功看起来简单但问题出在第2步和第3步之间。如果两个请求同时通过第2步的判断都认为还剩1个号然后同时执行扣减一个变0一个变-1数据库里就会出现负数号源。这在低并发下很难触发但答辩老师最喜欢问的就是你怎么解决并发问题的。如果你回答我用的是数据库update语句自带的锁那就及格了。推荐的写法是把这个扣减操作做成一条受影响的语句利用MySQL的行锁机制UPDATE schedule_info SET remain_no remain_no - 1 WHERE id #{scheduleId} AND remain_no 0然后判断这条UPDATE的受影响行数受影响行数 1说明锁号成功继续插入预约记录受影响行数 0说明号源已满或已被并发抢走直接返回号源不足在Java里用MyBatis-Plus的update方法加上QueryWrapper条件就能实现不需要手动写SELECT ... FOR UPDATE。这里更要注意的是整个操作必须加Transactional防止出现号源扣了但预约记录没插入成功的脏数据。还有一个经典的防重复预约需求同一个患者在同一时段不能重复预约。这个用一条数据库唯一约束就解决了在registration_info表上建联合唯一索引(user_id, schedule_id)插入重复记录时数据库会直接报错比在代码里IF EXISTS查询更可靠。你把这条索引写在SQL初始化文件里论文里补一句DB层约束保证数据最终一致性答辩的气场一下就起来了。3.2 签到入队与叫号推进状态机的代码实现思路预约完成之后患者当天到院要签到。签到的核心是生成排队号码。诊所场景的排队号规则通常是当日按科室生成序号比如内科01、内科02。简化做法是当天同一科室的queue_record条数加一但在并发下可能生成重复号所以更保险的做法是读取该科室当天最大序号再1并且用数据库唯一约束兜底。签到的伪代码逻辑如下Transactional public QueueRecord signIn(Long registrationId) { // 1. 校验预约单状态是已预约 RegistrationInfo reg registrationService.getById(registrationId); if (reg null || reg.getStatus() ! 1) { throw new BizException(预约单状态异常无法签到); } // 2. 生成当天科室排队号 Integer maxNo queueRecordMapper.selectMaxNoByDepartment( reg.getDoctor().getDepartmentId(), LocalDate.now()); int newNo maxNo null ? 1 : maxNo 1; // 3. 插入排队记录状态为候诊中 QueueRecord qr new QueueRecord(); qr.setRegistrationId(registrationId); qr.setQueueNo(formatQueueNo(departmentCode, newNo)); qr.setStatus(1); queueRecordMapper.insert(qr); // 4. 修改预约单状态为已签到 reg.setStatus(2); registrationService.updateById(reg); return qr; }叫号流程就更有意思了。医生的页面上显示当前候诊队列点下一个按钮时后端做的事情是把队列中状态为候诊中的记录按queue_no升序取第一条把它的状态改为呼叫中同时把上一条正在就诊的记录改为已完成如果存在。这个上一条状态推进的逻辑一定不要在前端做否则刷新页面后状态就错乱了必须由后端用一个事务完成前端只负责调接口和刷新展示。有同学会问叫号页面怎么实时刷新最简单的方式是前端写一个setInterval每5秒或10秒调一次获取当前队列的接口。社区诊所的规模完全够用你不用为了炫技硬上WebSocket但如果论文里写了WebSocket就一定要把握手、心跳、断线重连这些说出来并且演示。我个人做这个项目时用的是短轮询因为实现简单、不会因为断连出问题答辩时也可以直接说考虑到轻量级场景选择轮询方案。这不是偷懒是合理的工程取舍。3.3 接口设计与前端配合别把Controller写成万能类代码结构上我建议按Controller - Service - Mapper三层做其中Controller只负责参数接收和结果返回所有业务逻辑都放Service。很多同学的项目里Service层方法只有一行return mapper.selectList(wrapper)那和把逻辑直接写在Controller里没区别老师抽查代码时会很明显看到。社区诊所系统的接口可以按业务域拆成几个ControllerController核心接口说明AuthController登录、登出、验证码三个角色共同入口DoctorController医生列表、医生详情患者端浏览医生ScheduleController排班列表、余号查询患者端选择时段RegistrationController预约下单、取消、我的预约患者端核心操作QueueController签到、获取队列、叫号患者签到医生叫号AdminController排班管理、医生管理、统计管理员后台功能这个划分的好处是论文的系统模块设计章节可以直接照这个列表来写每一块都有明确的职责边界。前端页面里患者端就是一个首页-医生列表-选择时段-预约成功的流程后台管理就是一个排班管理-医生管理-数据概览的流程不用设计太多花哨页面。4. 调试运行时最容易踩的坑从环境到配置全链路排雷4.1 Java、MySQL、Redis版本匹配先看pom再定环境说句实话我帮学生远程调试的时候一半以上的问题出在环境版本对不上而不是代码本身。最典型的现象是项目在压缩包里带着说明JDK 1.8你本地装的是JDK 17启动的时候项目正在跑跑到某个地方突然报一堆奇怪的类加载错误。先自查一个最简单的命令java -version如果项目基于Spring Boot 2.xjavac版本必须是8或11如果项目基于Spring Boot 3.x就必须是17或21。推荐用IDEA里自带的Project Structure - SDK把Project SDK选对而不是依赖系统默认的Java。MySQL版本相对宽容5.7和8.0都能跑但要注意8.0以上驱动变化。如果你用的是最新的mysql-connector-j依赖数据库连接串里要显式写上?useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue否则可能会遇到时区错误或公钥检索失败的报错。这些都是看着莫名其妙、实际上都是环境配置的经典问题。4.2 启动端口、跨域和登录拦截运行起来只是个开始项目导入IDEA后第一件事不是点运行而是先改配置。打开application.yml确认三件事server.port是否和你本机其他项目冲突很多同学同时开两个后端项目端口被其中一个占用另一个启动直接报Port already in usespring.datasource.url里的数据库名称、username、password是不是你本机的这个基本是必改项mybatis的mapper位置是否正确mybatis-plus.mapper-locations如果写死了某个包路径少一个*符号就会出现找不到mapper的错误如果你的前端是独立的Vue项目还会有跨域问题。前端跑在localhost:8081后端跑在localhost:8080前端请求后端时如果没配CORS浏览器控制台会报CORS policy错误。配置方式是在后端加一个全局配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOrigins(http://localhost:8081) .allowCredentials(true) .allowedMethods(GET, POST, PUT, DELETE); } }登录拦截是另一个高频问题。很多系统的拦截器会把静态资源一起拦截了导致登录页的CSS和JS加载不出来页面整个变形。拦截器里一定要排除登录接口和静态资源路径常见写法是registry.addInterceptor(authInterceptor) .addPathPatterns(/**) .excludePathPatterns(/login, /api/login, /css/**, /js/**, /images/**);我见过不少同学在调试时被这个问题折磨一下午页面一直报401或者跳转到登录页死循环其实只是拦截器没放行静态资源。4.3 演示数据与演示账号让它开箱就能跑起来一个毕业设计项目给答辩老师演示的时候最怕什么最怕现场建流程、现场录数据。哪怕你操作很快评委看着你一步一步录入也会觉得这系统没做完。所以项目里一定要带初始化SQL脚本把以下数据一次性准备好1个管理员账号admin/123456角色ADMIN5个医生分属内科、外科、儿科、口腔科、全科3个患者账号用于演示不同角色登录未来一周的排班数据覆盖剩余号源充足的日期和即将约满的时段初始化数据的方式是执行项目里的sql/init_data.sql而不是每次手动插入。这一步做完后你打开项目就能直接登录、直接查排班、直接预约演示流程压缩到一分钟之内。而且初始化数据里的医生名称也建议直接用能记住的名字比如张医生李医生答辩时不至于对着PPT念错。还有一点系统里如果涉及密码加密初始化脚本里必须写的是加密后的密码不能写明文。很多学生在初始化脚本里写明文密码123456结果登录验证时后端用MD5/SHA加密去比对永远比对不上。这也是明明账号密码看起来对但就是登不进去的经典原因之一。我建议在初始化脚本里直接查一下项目加密工具类输出的密文或者看文档里写的默认密文是什么而不是自己猜。5. 文档写作与答辩准备把源码真正消化成你的成绩5.1 论文章节骨架需求、设计、实现、测试一个都不能少很多同学以为毕设做完系统就万事大吉其实论文占的分值很大。社区诊所系统的论文骨架可以直接按下面列的顺序来组织每个章节都有清晰的产出物绪论选题背景、国内外研究现状、论文组织结构需求分析系统角色分析、功能性需求用Use Case图、非功能性需求性能、安全性、易用性系统设计系统架构图、技术选型理由、数据库E-R图、表结构说明、核心模块设计系统实现每个核心功能的页面截图 关键代码 实现说明系统测试测试环境、测试用例表、测试结果分析总结与展望做了哪些工作、存在哪些不足、未来扩展方向每章具体怎么扩字数我的建议是不要堆截屏——一张截图下面只有一行字的段落老师看着就想快进。要写为什么这样设计和这个模块解决了什么业务痛点。比如排班模块你可以写排班表的设计将医生每周可预约时段与号源数量解耦使得管理员调整门诊时间时不需要修改预约记录只需修改排班表即可。5.2 答辩必问问题与回答思路提前把问题答案背下来答辩环节有些老师就是刨根问底不问清楚关键实现不放手。下面这几个问题请你务必做好准备Q1同一时段多个患者同时预约你是怎么保证不超卖号源的答扣减号源时使用UPDATE schedule_info SET remain_no remain_no - 1 WHERE id ? AND remain_no 0利用数据库行锁保证原子性并且对同一个用户在同一个排班的预约记录建立联合唯一索引防止重复预约。Q2如果患者预约了没来怎么办答系统提供取消预约功能取消时号源自动回补给remain_no若已超过预约时段且未取消预约单状态保留为爽约管理员可以在后台查看爽约记录。这块业务目前在系统里已经完整实现。Q3排队叫号时过号如何处理答如果医生呼叫后患者未在指定时间内报到医生可以直接点击过号按钮把该记录状态改为过号后续患者再签到时系统会将过号患者放置到队尾或由医生手动调整。当前实现是放置队尾但也保留了扩展空间。Q4你这个系统为什么不用Redis答社区诊所的用户并发量级不高MySQL利用乐观锁和唯一约束已经能保证数据一致性整体架构更简单、部署成本更低。如果扩展为多院区并发场景可以将余号字段迁移到Redis并使用Lua脚本保证原子扣减。把这些问题答案吃透答辩时基本能做到从容应对。比背PPT强一百倍。5.3 定制扩展的思路想让项目升级可以这样加毕业设计如果只求过线按上面做已经够了。但如果你想要更稳更亮眼的成绩可以在现有系统上加一些不算难但有明显效果的扩展点消息通知模块预约成功、签到提醒、排队叫号提醒。可以先用短信接口的Mock实现不必真实接入第三方服务数据统计看板按天统计各科室挂号量、医生接诊量、爽约率排名用ECharts画折线图和柱状图Excel导入导出管理员批量导入排班表、导出就诊明细。这个功能代码量不大但论起来实际应用场景非常有效过号自动重排通过定时任务把过号患者自动放回队列增强排队规则的完整性我甚至见过一个学生加了大屏展示功能把诊室门口的一个屏幕页面做成了当前队列和呼叫号码的展示答辩时现场效果非常好。但前提是你要真的能把这个功能稳定演示出来宁可少加功能也不要在答辩当天现场翻车。6. 写在最后一些带过很多届学生后得出的实在经验这套社区诊所在线挂号与排队系统是我觉得最经典的Spring Boot入门实战项目之一技术上不难但业务完整做完之后你对Spring Boot、MyBatis-Plus、MySQL、前端联调这些核心技术会有一次系统的串联理解。很多人会去网上找各种源码仓库存一套下来然后陷入能启动但不会讲、不会改的尴尬境地。我的建议是拿到任何一套源码第一件事不是跑起来而是先打开数据库和核心Service把预约单状态流转和排队状态流转这两条主流程的代码完整读一遍。只要这两条线读懂了整个项目至少懂了一半。调试运行时如果卡住了也不要慌着到处发帖子问。先排查版本、再排查配置、再排查依赖这个顺序能解决九成问题。如果自己还是搞不定别忘了你身边还有远程调试这条路可以走找人讲一下比自己在网上瞎折腾省下好几个晚上的时间。最后送大家一个我在实战中反复验证过的习惯每次改代码之前先想清楚这条代码在数据库里会产生哪些行的变化再动手写。很多复杂的逻辑你把数据变化想清楚了Java代码自然就顺了。希望这篇经验对正在做这个题目的你有帮助也祝答辩顺利。