资讯动态

医院预约挂号系统开发实战:从架构设计到并发控制与部署

发布时间:2026/10/15 17:44:16 来源:尧图企业网站定制
1. 项目整体设计与思路拆解1.1 为什么选这个课题方向医院预约挂号平台这几年在毕业设计里出现频率相当高原因很简单业务场景完整、需求边界清晰、技术栈覆盖广而且贴合真实行业痛点。我当年选这个题目的时候导师给的意见就是“不要选太泛的,要选一个你能讲清楚业务闭环的”。预约挂号恰好满足这个条件——从患者注册、科室浏览、医生查询、号源选择、预约下单到后台管理端的审核、排班、统计是一条非常完整的业务链路。整个系统最核心的价值在于它不是一个纯展示型的CRUD Demo而是一个有状态流转、有时效性约束、有并发考量的小型业务系统。比如挂号的号源数量限制、预约时间冲突的处理、用户重复挂号的校验这些都是在面试或答辩时能拿出来讲清楚“为什么这么设计”的加分点。1.2 系统整体功能架构这个平台按角色拆分成三个端患者端、医生端、管理员端。患者端面向普通用户提供注册登录、科室筛选、医生检索、号源查看与预约、个人预约记录管理等功能。医生端相对简单主要查看自己的排班和预约患者列表。管理员端负责科室管理、医生信息维护、排班发布、预约统计和系统基础配置。三个角色共用一套登录认证体系但通过角色字段控制访问权限。前端页面按角色区分目录后端接口通过拦截器校验token和角色标识这样避免了一个普通患者直接调用管理员接口修改数据的越权风险。1.3 技术选型背后的考量技术栈用的是Spring Boot MyBatis Plus Vue Element UI MySQL这套组合在毕设项目里非常主流但我想强调一点选这套不是因为它“烂大街”而是因为它最稳。Spring Boot解决了传统SSH配置地狱的问题内置Tomcat一个main方法就能跑起来这对毕设阶段的调试效率帮助很大。MyBatis Plus在单表CRUD上几乎可以不用写SQL分页查询、条件构造器都是内置的省下大量重复编码时间。前端Vue加上Element UI组件丰富表格、表单、日期选择器、弹窗提示全是现成的能快速搭出一个观感合格的管理界面。数据库选了MySQL 5.7理由很务实部署简单、资料多、出问题容易搜索到解决方案。如果换成Oracle或者PostgreSQL答辩时反而要额外解释为什么做这个选择。注意技术选型不需要追求“新”关键是能讲清楚每个组件在项目里承担了什么职责以及为什么是它而不是别的。评审老师更看重你对技术的理解深度而不是用的框架版本有多新。2. 核心模块与数据库设计2.1 核心模块拆分整个平台拆成六大模块用户认证模块、科室与医生管理模块、排班管理模块、预约挂号模块、订单支付模块可选扩展、后台统计模块。这里我想单独讲一下排班管理和预约挂号因为它们是最容易踩坑的地方。排班模块解决的是“医生什么时候出诊、放多少号”的问题。我采用的是按时间段生成号源的方式管理员为某个医生在某一天设置多个时间段比如上午8:00-8:30、8:30-9:00每个时间段对应一个号源记录初始余量设为1或2。这样患者在前端看到的就是一个个可选的时间段而不是一个大而化之的“上午门诊”。预约挂号模块是整个系统的业务核心它的状态机很简单待就诊、已完成、已取消。但真正实现起来麻烦的是并发控制——多个患者同时抢最后一个号源时不能让系统出现超卖。我当时用的方案是数据库乐观锁在号源表上加一个version字段更新时校验版本号一旦冲突就提示用户“该时间段已被预约”。2.2 数据库表结构详解数据库一共设计了十张表这里挑几张最关键的说。用户表是最基础的字段包括主键id、用户名、密码BCrypt加密存储、真实姓名、手机号、角色类型、创建时间。需要注意的细节是用户名必须加唯一索引手机号在注册时做格式校验。科室表字段简单就是科室名称、科室简介、状态。但医生表要单独设计因为医生隶属于科室而且需要关联排班。医生表字段包括所属科室id、姓名、职称、擅长领域、简介、头像地址。排班表是核心表之一我当时的设计字段是主键id、医生id、排班日期、开始时间、结束时间、号源总数、剩余号数、版本号、状态。这里有三个点值得注意日期和时间分开存方便按天和按时段两个维度查询剩余号数不直接等于号源总数减去订单数而是每次预约成功手动减一这样配合乐观锁更可控状态字段区分“未开始、进行中、已结束、已停诊”已经停诊的排班前端要过滤掉预约订单表同样关键字段包括主键id、订单编号用时间戳加随机数生成、患者id、排班id、医生id、科室id、预约日期、时间段、状态、创建时间、取消时间。订单编号要保证唯一我用的方案是“yyyyMMddHHmmss 四位随机数”在并发量不高的情况下足够用。2.3 为什么表结构要这样设计表结构的设计直接决定了后面业务逻辑的复杂程度。我见过不少同学把排班信息和订单信息混在一张表里表面上看减少了关联查询实际上更新和统计的时候非常痛苦。科室和医生拆两张表是因为科室是相对稳定的数据医生会变动而且一个科室下有多个医生、一个医生只属于一个科室这是典型的一对多关系。医生和排班也是同样的道理一张排班表只记录“某医生某天的某个时间段”数据粒度小后续做日历视图或者时间段筛选都方便。订单表冗余了医生id和科室id而不只是排班id是为了列表展示时少几次关联查询牺牲一点规范化换查询性能和代码简洁度在毕设体量下完全值得。提示数据库设计不要一味追求三范式适当冗余可以让查询代码简单很多。评审老师问起来你只要回答“这是基于查询效率的考虑”即可。3. 实操过程与核心环节实现3.1 开发环境与项目初始化本地开发环境我建议统一版本避免不必要的兼容问题。经过实测比较稳的组合是JDK 1.8、Maven 3.6.3、Node 14.17.0、MySQL 5.7。Spring Boot版本选2.3.x系列不要直接上3.x因为3.x要求JDK 17部分教程和插件还不兼容换环境排查问题很浪费时间。项目初始化我用的是Spring Initializr选择Web、MyBatis、MySQL Driver这几个基础依赖后续再手动引入MyBatis Plus和JWT相关依赖。前端项目用Vue CLI创建选择Router和VuexElement UI通过npm安装。3.2 后端核心代码实现后端代码最关键的三块登录认证拦截器、号源并发控制、预约下单事务。登录认证我用JWT生成token用户登录成功后返回token前端存储在localStorage里每次请求在请求头带Authorization字段。后端用一个拦截器统一校验token有效性通过用户角色字段做接口级权限控制。预约下单的核心逻辑要同时保证数据一致性和业务正确性我贴一下当时的核心代码片段已简化Transactional(rollbackFor Exception.class) public Result bookOrder(BookRequest request) { // 1. 校验排班是否存在且状态有效 Schedule schedule scheduleMapper.selectById(request.getScheduleId()); if (schedule null || schedule.getStatus() 2) { return Result.error(该排班不存在或已停诊); } // 2. 乐观锁扣减号源 int result scheduleMapper.deductCount( request.getScheduleId(), schedule.getVersion() ); if (result 0) { return Result.error(该时段号源已被抢完请选择其他时间); } // 3. 校验当前用户是否重复预约 Integer count orderMapper.selectCount( new LambdaQueryWrapperOrder() .eq(Order::getPatientId, request.getPatientId()) .eq(Order::getScheduleId, request.getScheduleId()) .eq(Order::getStatus, 0) ); if (count 0) { throw new RuntimeException(您已预约该时段请勿重复操作); } // 4. 生成订单并保存 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setPatientId(request.getPatientId()); order.setScheduleId(request.getScheduleId()); order.setStatus(0); orderMapper.insert(order); return Result.success(预约成功); }这里有一个非常重要的细节乐观锁扣减号源和查询重复预约的先后顺序。我当时是先扣号源再查重复这样能尽量避免用户在扣减号源之后因为校验失败导致事务回滚。但由于加了Transactional事务内任何异常都会回滚所以不会出现号源被扣了但订单没生成的情况。generateOrderNo()方法要注意线程安全问题不能简单的用随机数直接拼接。我当时用Redis生成自增序列号但考虑到部分毕设环境没装Redis最终改成了“当前时间戳 线程池内的AtomicInteger自增”这样单机部署不会重复。3.3 前端关键页面实现前端最复杂的页面是预约挂号页数据加载逻辑是三级联动选科室、选医生、选时间段。科室列表通过后端接口/department/list获取医生列表根据所选科室id请求/doctor/listByDept时间段列表则根据医生id和选择的日期请求/schedule/listByDoctorAndDate。三级联动实现的核心是watch监听selectedDeptId和selectedDoctorId的变化一旦变化就重置下一级选项并重新请求数据。预约弹窗里要回显医生姓名、科室名称、预约日期、时间段这些信息确认按钮点击后调用创建订单接口。成功后提示用户并跳转到“我的预约”页面那里展示订单状态、取消入口和就诊提醒信息。3.4 排班管理的实现细节排班管理界面有三个功能点批量生成排班、修改号源数量、停诊处理。批量生成是我当时花时间最多的功能因为需要处理跨天和周期间的逻辑。我实现的方案是管理员选择医生、起始日期、天数以及每天的号源时间模板可以设置多个时间段后端循环日期组装排班数据批量插入。时间模板的格式是JSON数组字符串前端用动态表单添加多个时间段提交后端时解析JSON。停诊处理要额外考虑医生某一时间段停诊后该时间段已预约的订单怎么办我提供的处理策略是给管理员选择——自动取消所有已预约订单并释放号源或者标记为“待改期”由患者自行取消。默认实现选择第一种逻辑简单且对患者友好订单状态改为“已取消”用户会收到状态变更提醒。3.5 部署与上线验证项目完成后我做了本地打包验证后端使用Maven的package命令打成jar包通过java -jar启动前端使用npm run build生成dist静态文件用Nginx部署。部署时的关键点是设置反向代理将/api前缀的请求转发到后端8080端口。否则前端的生产环境请求会直接请求到Nginx的80端口导致404。Nginx配置片段大致如下server { listen 80; server_name localhost; location / { root /usr/share/nginx/html; index index.html index.htm; } location /api/ { proxy_pass http://localhost:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }部署时还遇到过一个静态资源404问题排查下来是Vue Router使用了history模式刷新页面时Nginx不知道如何处理前端路由。解决方案是在location块里添加try_files $uri $uri/ /index.html;配置。4. 常见问题与排查技巧实录4.1 号源超卖问题这个问题是我在压测时发现的。当时用Postman同时发了5个预约请求排班号源只有1个结果返回了2个成功。排查后发现是乐观锁SQL写错了update语句里where version #{version}的版本号取的是查询之后改过的值导致每次更新都能成功。正确的做法是查询排班记录时取出version值然后在更新语句中带上这个version作为条件更新成功后再将新的version返回并更新到缓存或实体对象中。MyBatis Plus的updateById配合Version注解也能实现这个逻辑但需要确保version字段在实体类上有正确的类型和注解。提示并发问题测试不能只看普通情况要用并发工具压测。我后来用JMeter模拟了100个并发预约请求验证了乐观锁在200的并发量下依然有效。4.2 跨域请求问题前后端分离项目必然要处理跨域。开发环境下我用的方案是Vue CLI的proxy代理在vue.config.js里配置开发服务器将/api请求转发到后端地址。但生产环境部署时Nginx没有代理配置导致接口全部请求失败。后来我统一采用后端加CORS配置的方式写了一个配置类允许前端地址访问并支持预检请求。这种方式比前端代理更稳因为前后端部署在不同服务器也能正常工作。Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true); } }4.3 JWT过期与刷新问题患者在使用过程中如果token过期再操作会跳回登录页体验很差。我实现了token刷新机制前端在响应拦截器中检测到401状态码时自动带着refreshToken调用刷新接口获取新的token后重放原请求。这样可以保证用户操作基本无感。不过这个方案也有一个隐患频繁刷新会增加后端压力。所以我把refreshToken的过期时间设为了7天比accessToken的2小时长不少平时刷新的频率并不高。4.4 图片上传失败问题医生头像上传功能在本地测试正常部署到服务器后报错。排查发现是前端上传组件默认上传到后端接口但Nginx没有配置上传文件大小限制导致大图片上传失败。解决方案是在Nginx的http块中添加client_max_body_size 10m;配置。这个坑看起来小但一旦遇到会浪费很多排查时间。建议在项目初期就把Nginx的上传大小限制、静态资源缓存策略一起配置好不要等部署阶段再处理。4.5 数据库时区问题数据库连接串如果没设置serverTimezone参数在部分机器上会报时区错误。原因是JDBC驱动版本不同默认读取系统时区可能不一致。我在连接串里显式添加serverTimezoneAsia/Shanghai统一了数据库和应用的时区问题解决。另外排班日期如果存的是字符串排序和范围查询都会有问题。建议所有的日期字段都使用datetime或date类型Java侧用LocalDate和LocalDateTime对应避免字符串比较的坑。5. 答辩准备与项目扩展方向5.1 答辩时的核心亮点设计答辩时不要只讲功能重点讲清楚三个方向的问题业务难点、技术选型理由、系统安全性设计。业务难点可以重点讲并发抢号的处理这个最能体现你的思考深度。技术选型理由可以讲为什么用Spring Boot不用SSH、为什么用MyBatis Plus不用原生JPA。安全性设计可以讲密码加密、JWT认证、角色权限、参数校验这些都是评审老师喜欢听的内容。我当年答辩时做了一个测试数据展示页把用户量、科室数、医生数、预约成功率这些指标可视化展示老师对这个设计印象很深因为说明你考虑了真实运行场景。5.2 可扩展的方向毕设做完不是终点后续扩展空间很大。最值得做的是引入消息队列进行削峰填谷预约请求量大时可以通过定时任务或者异步队列处理。另一个方向是增加在线支付功能接入虚拟支付渠道把挂号费支付闭环补齐。还有一个思路是增加数据统计模块用ECharts展示科室预约热度、医生排班饱和度、用户就诊趋势等从数据驱动运营的角度提升系统价值。5.3 一组实践建议给正在做这个课题的同学一个建议不要等代码全部写完再写文档边做边记录。我当时的做法是每完成一个模块就截图并记录核心逻辑最后整理成文档非常快。另外要善用版本管理每次功能完成后提交一次出bug时能快速回退。“一条龙定制”这个概念在学校里很常见但我的真实体会是不管是不是从零开始写你都要把整个系统的逻辑吃透尤其是数据库设计和核心业务代码。因为答辩时老师一定会深挖细节如果你连自己的项目都讲不清楚代码是不是自己写的老师们一眼就能看出来。如果做的时候遇到问题欢迎一起交流代码、文档、讲解视频我都可以分享但更希望你能在理解的基础上拿去做二次开发。做一个能运行的项目很容易做一个能在答辩时讲得清楚的优秀毕设才是我们真正要花时间的地方。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑