医院网上挂号就诊系统设计与实现——SpringBoot与Vue实战
发布时间:2026/9/17 4:14:33 锦皓数字建站

1. 项目缘起与整体设计思路1.1 为什么选“网上挂号就诊系统”做毕设我这几年带过不少毕业生的项目也帮人救急改过不少半成品“医院网上挂号就诊系统”这个题目在计算机毕设里出现频率非常高。原因很简单它属于典型的管理信息系统业务逻辑清晰却不简单既有C端用户操作又有B端后台管理还牵扯到时间窗口、号源状态这类容易出并发问题的细节。对一个计算机专业学生来说这种项目既能展示基本功又能在答辩时有足够的“技术谈资”。达明医院网上挂号就诊系统这个题目本质上要解决的是传统窗口挂号的几个痛点排队时间长、号源不透明、医生排班信息靠现场公告、就医记录不好追溯。系统要做的事情就是把这些线下行为搬到线上让患者能查科室、看排班、预定号源、管理自己的就诊记录让医院管理员能维护科室医生、配置排班、统计挂号数据。如果你也在选毕设题目我建议你重点关注这个题目的一个优势它的需求边界非常明确不存在那种“做完才发现题目本身就有歧义”的坑。挂号系统的核心流程就是“用户选科室 - 看排班 - 锁定号源 - 生成挂号记录”无论你怎么拓展功能这条主链路不会变。只要主链路稳定系统就算立住了。1.2 技术选型背后的考量先说硬结论后端用 Spring Boot前端用 Vue数据库用 MySQLORM 选 MyBatis-Plus。这套组合在毕设圈子里是绝对的“标准答案”它不一定是最前沿的但对一个需要短期交付、还要能答辩的项目来说是风险最低的方案。为什么不用 SSMSSMSpring Spring MVC MyBatis虽然教学上经常提但配置繁琐光是 Spring 和 MyBatis 的 XML 配置文件就能劝退一批人。Spring Boot 把自动化配置做到位了你只要写业务逻辑就行这个对毕设开发效率的提升不是一星半点。前端选 Vue 而不是 JSP 的原因更直接现在本科阶段普遍会教前端三大框架Vue 在易用性和中文社区资源上都占优。更重要的是前后端分离的结构在论文里可以单独画架构图、单独写接口设计比 JSP 那种前后端揉在一起的写法容易表达得多。数据库这块没有悬念MySQL 依然是毕设首选。需要注意的一点是如果你本机装的是 MySQL 8.x驱动和连接串的配置跟 5.x 有些差异建议直接用 MySQL 8别为了“兼容旧教材”去装老版本。另外提一句这个项目里涉及号源状态变化和排班时间判断如果你的代码水平还能支撑可以在后面自己加一点缓存处理比如用 Redis 做号源计数这会让答辩的含金量上一个档次。不过如果时间紧张不做也不影响系统跑通。1.3 功能模块拆解与影响范围分析一个完整的网上挂号系统我实践中通常把它拆成三个端用户端、医生端、管理端。用户端面向患者核心功能包括注册登录、科室与医生信息浏览、排班查询、在线挂号、就诊记录查看、个人信息维护。其中在线挂号是主功能它的操作流程是选日期 - 看该日期下医生的排班时段 - 判断该时段是否还有余号 - 提交挂号 - 生成挂号单。这个过程看起来简单但对数据一致性的要求很高后面我会着重讲。医生端面向临床医生核心功能是查看自己的排班表、查看某时段挂了自己号的患者列表、以及简单的就诊状态标记比如把“已挂号”改成“已完成”。有些系统还会给医生加一个“停诊停号”操作这个可以考虑做进去因为它在数据库层面就是批量更新一次排班状态逻辑不复杂但功能很实用。管理端面向医院运营人员核心功能包括科室管理、医生信息管理、排班管理为指定的医生在指定日期生成出诊时段、挂号数据统计、用户管理。排班管理这个功能是整个系统里最容易写乱的部分因为它涉及“排班生成”和“号源初始化”两个动作的联动。说白了医生排了一班系统就要自动生成这个时段内的号源池多少人挂满就是多少号。这三端的角色权限我的建议是用 Spring Boot 里的拦截器加自定义注解实现比引入 Spring Security 省事得多。后面我会贴出关键代码片段。2. 数据库设计与核心表结构2.1 表结构设计的总盘子数据库设计是这种管理系统的根表结构理不顺后面写业务代码就是反复返工。达明医院这个项目我实际使用的表有 8 张若不算用户角色关联表就是 7 张分别是sys_user系统用户表存统一登录账号、密码、角色标识hospital_department科室表hospital_doctor医生信息表通过 department_id 关联科室outdoctor_schedule排班表记录某医生某天某时段的出诊信息registration_record挂号记录表doctor_time_interval时段表或者把时段信息并进排班表看个人偏好sys_role / sys_user_role角色表和用户-角色关联表若你的用户表里有 role_type 字段这套可以简化掉这 7 张表怎么建我建议你在动手写代码前先在纸上画出实体关系科室 1 对 多 医生医生 1 对 多 排班排班 1 对 多 挂号记录。核心关系就是这三条其它表都是围绕它们做支撑。2.2 核心表设计的关键字段与状态定义先给出最重要的三张表的字段清单这是我改造过很多次后觉得最顺手的版本。sys_user 用户表字段名类型说明idbigint主键自增usernamevarchar(50)登录账号唯一passwordvarchar(255)密码BCrypt 加密role_typetinyint0-患者1-医生2-管理员real_namevarchar(50)真实姓名phonevarchar(20)手机号create_timedatetime创建时间角色这一栏我没有单独建角色表直接用 role_type 字段做了区分。毕设场景下角色数量固定这样最省事。你要是想显得更“工程化”用 Spring Security 的 RBAC 模型也无可厚非但代码量会明显增加。outdoctor_schedule 排班表字段名类型说明idbigint主键doctor_idbigint关联医生schedule_datedate出诊日期start_timetime开始时间end_timetime结束时间total_slotsint该时段总号源数remain_slotsint剩余号源数statustinyint0-未开始1-进行中2-已停诊排班表这里有两个关键设计一是 total_slots 和 remain_slots 分开存而不是用一张“号源明细表”来记录每一个号。后者的设计更灵活但是每次查询都要 count而且生成号源明细时数据量大对毕设来说属于过度设计。前者用两个整数就能解决“还有几个号”的问题性能也好。二是增加了 status 字段这样管理员停诊时只需要 update 这条排班记录不需要动其它表逻辑非常干净。registration_record 挂号记录表字段名类型说明idbigint主键user_idbigint挂号用户schedule_idbigint关联排班doctor_idbigint冗余医生ID方便查询visit_datedate就诊日期visit_timevarchar(50)就诊时段描述order_novarchar(32)挂号单号statustinyint0-已预约1-已完成2-已取消create_timedatetime创建时间挂号记录表里我特意冗余了 doctor_id 和 visit_date不要觉得这是多余。在用户端的“我的挂号记录”列表里往往要一次性查出来显示医生姓名、就诊日期、科室名称冗余字段能帮你省掉好几次关联查询。系统是给人用的表结构是给人写的合理冗余在毕设场景里不算坏味道。2.3 初始化数据的坑很多同学的库表建好之后就急着去写后端接口结果界面上一片空白调试起来特别痛苦。我的习惯是先把 SQL 初始化脚本写好每个模块关联的数据都准备至少两条并且保证它们能串起来。比如先建两个科室每个科室下挂两三个医生给其中两个医生生成三天的排班排班的余号要有的多、有的少、有的甚至为 0。这样你在做用户端测试时各种状态都能一眼看到不用临时造数据。还有一个小细节数据库的字符集一定要在建库时指定 utf8mb4否则前端传过来的中文会导致插入报错或乱码。建库语句我直接写给你CREATE DATABASE IF NOT EXISTS dahospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;3. 核心功能实现挂号流程与排班管理3.1 登录认证从简而非从繁毕设项目的登录认证我的建议是不要引入 JWT 和 Spring Security 全家桶除非你特别想在答辩时讲“安全设计”。更多的系统直接用 Session 拦截器就够了代码量少逻辑也好懂。具体做法是用户登录成功后把用户 ID 和角色标识放进 Session然后定义一个 LoginInterceptor在配置类里配置拦截规则。需要放行的路径是登录接口、注册接口和静态资源其余接口都要求登录并且在拦截器里校验 role_type 是否符合接口要求的角色。拦截器里校验角色的逻辑大概是这样public class RoleInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object roleType session.getAttribute(roleType); String uri request.getRequestURI(); if (uri.startsWith(/admin) !2.equals(String.valueOf(roleType))) { response.setStatus(403); return false; } if (uri.startsWith(/doctor) !1.equals(String.valueOf(roleType))) { response.setStatus(403); return false; } return true; } }这段代码是我在自己项目里简化过的版本逻辑非常直白根据路径前缀判断需要什么角色当前会话角色对不上就拒绝。前端拿到 403 就提示用户无权限跳到对应入口。密码加密方面毕设里很多人用 MD5但答辩老师问起来会显得没底气。用 BCrypt 加密也就多十行代码Spring Security 的工具类可以直接调用建议用 BCrypt 来存储密码这一点写进论文里也是加分的。3.2 排班管理不要让管理员手动造号源排班功能的实现思路是管理员选择医生、选择日期、选择时间段然后设置该时段可预约的号源总数系统自动生成一条 schedule 记录同时把 remain_slots 初始化成 total_slots。这个功能实现不难你只需要在插入排班记录时设置 total_slots 和 remain_slots 为同一个值即可public boolean addSchedule(Schedule schedule) { schedule.setRemainSlots(schedule.getTotalSlots()); schedule.setStatus(0); return scheduleMapper.insert(schedule) 0; }真正麻烦的是页面上要处理的选择逻辑。我的实践是把时间段做成固定档位比如上午分成 8:00-9:00、9:00-10:00、10:00-11:30 三个时段下午分成 14:00-15:00、15:00-16:00、16:00-17:30 三个时段管理员只能从下拉框选不能手输。这样做不是为了偷懒而是为了防止不合法的时间段进入数据库后续做去重和展示都省心。另一个容易忽略的点是排班冲突校验。如果一个医生在同一天同一时段已经被排过了管理员再次提交时应该提示冲突。校验逻辑是查一下有没有 doctor_id 相同、schedule_date 相同、start_time 相同的记录存在就拒绝插入。这个 SQL 用 MyBatis-Plus 的 QueryWrapper 两行就写完但漏掉的话系统运行一段时间后数据就会很乱。3.3 挂号秒杀场景的简化处理在线挂号这个功能乍一看很普通但其中藏着整个系统最大的技术难点——多人在同一时刻抢同一时段最后一个号源时怎么保证不会超卖。先说你最可能踩的坑很多人会把校验余号和扣减余号分成两步执行先查询 remain_slots 大于 0再执行 update remain_slots remain_slots - 1。在单用户时没问题但一旦多个请求并发进入查询时都发现余号大于 0然后都执行扣减最后剩下一个负数库存这就是超卖。解决的方案有两种。第一种是“乐观锁”。给排班表加一个 version 字段更新号源时携带之前查到的 version执行 update 语句时要求 version 匹配才更新UPDATE outdoctor_schedule SET remain_slots remain_slots - 1, version version 1 WHERE id #{scheduleId} AND remain_slots 0 AND version #{version}执行之后判断受影响的行数为 0 说明更新失败说明号源已被抢走或者余号不足直接返回给用户“号源紧张请重新选择”即可。第二种是“原子更新”。不依赖 version 字段直接靠 WHERE remain_slots 0 来保证不超卖。上面那条 SQL 去掉 version 条件就行。这条 SQL 在 MySQL 里是原子操作两个并发请求同时进来数据库会排队处理只有一个能成功扣减到0另一个会因为条件不满足而更新 0 行。我个人建议用第二种代码更简洁。下面是挂号的核心代码片段Transactional public RegistrationResult register(RegisterRequest req) { Schedule schedule scheduleMapper.selectById(req.getScheduleId()); if (schedule null) { return RegistrationResult.fail(排班信息不存在); } if (schedule.getStatus() 2) { return RegistrationResult.fail(该时段已停诊); } if (schedule.getRemainSlots() 0) { return RegistrationResult.fail(该时段号源已满); } // 原子更新扣减余号 int rows scheduleMapper.decreaseRemainSlots(req.getScheduleId()); if (rows 0) { return RegistrationResult.fail(号源已被抢完请选择其他时段); } // 插入挂号记录 RegistrationRecord record new RegistrationRecord(); record.setUserId(req.getUserId()); record.setScheduleId(schedule.getId()); record.setDoctorId(schedule.getDoctorId()); record.setVisitDate(schedule.getScheduleDate()); record.setOrderNo(generateOrderNo()); record.setStatus(0); registrationMapper.insert(record); return RegistrationResult.success(record); }这个接口用了 Transactional 事务注解保证扣减号源和插入挂号记录要么都成功、要么都回滚不会出现“号扣了但记录没生成”的脏数据。这里提醒一下事务只对同一个数据源内的操作有效如果你的系统后续接了消息队列或者第三方支付那就不能这样简单地包事务了需要注意。3.4 用户端界面与交互的几个关键点前端我用的是 Vue 2 Element UI视图层大致分两块浏览预约区和个人中心区。浏览预约区要有一个部门树或者科室导航用户点进科室后能看到医生卡片每张卡片上要展示医生照片、职称、擅长领域。点“排班”按钮后弹出一个周视图的排班面板日期是横轴时段是纵轴每个格子显示“余号 n”余号为 0 的格子置灰不可点。这个交互设计是我参考了好几个真实挂号平台后得出的用户在选号时的认知负担最小。个人中心区域要展示“我的挂号记录”列表要带上状态标签和操作按钮。已经挂号且未就诊的要提供“取消挂号”功能取消时同样要恢复余号数也就是执行一次 remain_slots 1 的更新。这里特别注意取消接口也要加事务逻辑是更新挂号记录状态为 2、更新排班余号加 1这两个动作必须捆绑。如果你只改了记录状态忘了恢复号源号过几天管理员就会发现号池对不上了。4. 源码结构与本地部署指南4.1 拿到源码后先不要急着启动如果你是从网上下载的这套“达明医院网上挂号就诊系统”源码第一件事不是双击启动而是看项目整体目录结构。一个规范的毕设源码应该是前后端分离的通常包含一个 backend或 server目录和一个 frontend或 web目录。后端目录里重点看三个东西pom.xml 里依赖的版本、application.yml / application.properties 里的数据库配置、以及 sql 目录下的初始化脚本。前端目录里重点看 package.json 里的依赖和 vue.config.js 里的代理配置。我的习惯是先把初始化 SQL 导入本地数据库再改配置文件里的数据库用户名密码然后先启动后端用 Postman 测一个不需要登录的公开接口比如科室列表查询通了之后再启动前端开发服务器。前端启动后如果页面打不开多半是代理端口配错了回头检查 vue.config.js 里的 proxy target 是不是指向了后端启动端口。这里我把后端配置文件的核心部分贴一下注意看数据源和端口server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/dahospital?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: 你自己的密码 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0注意 serverTimezoneAsia/Shanghai 这个参数不配的话 MySQL 8 和本地时区不一致时间字段经常会出现“差8小时”的诡异问题我当年就被这个坑过。4.2 前端启动与联调要点前端的启动命令是npm install后npm run serve。npm install 如果卡住可以先试npm config set registry https://registry.npmmirror.com把源切到镜像。这个和项目本身没关系但能节省你大量等待时间。联调阶段要特别注意跨域问题。单独启动的 Vue 开发服务器默认端口是 8080和后端冲突的话就改成 8081然后在 vue.config.js 里配置代理把/api开头的请求都转发到后端 8080 端口module.exports { devServer: { port: 8081, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } }后端接口统一加/api前缀这是我强烈建议的做法。一是避免和后端静态资源路径混淆二是前端代理只需要配一个前缀规则三是后面万一前后端要分服务器部署用 nginx 转发也简单。4.3 直接打包部署还是开发模式跑一般毕设现场演示用开发模式跑就够了一个是 npm run serve一个是 mvn spring-boot:run两个进程挂在前台能演示还能现场改代码。但如果答辩现场的机器性能一般或者网络环境不一定装了 Node可以考虑把前端构建产物交给后端托管。具体做法是在前端项目里执行npm run build生成 dist 目录把 dist 下的文件复制到后端项目的 src/main/resources/static 目录下重新打包后端为 jar之后直接java -jar启动。这样整个系统就是一个进程前端静态页面由 Spring Boot 的内置 Tomcat 直接托管演示时只需要起一个 jar稳定性高很多。我实测过这种方式唯一的坑是前端路由用 history 模式时刷新页面会出现 404 问题因为后端没有配置对这些前端路由的 fallback。解决办法是让后端加一个 Controller 或 用 WebMvcConfigurer 把非 /api 请求转发到 index.html。如果你前端用的是 hash 模式路由路由里有 #就没有这个烦恼。毕设演示系统我建议直接用 hash 模式省心。5. 论文写作与毕业答辩要点5.1 论文结构怎么串很多同学代码写完了卡在论文上。其实毕设论文的结构基本是定式摘要、绪论、需求分析、系统设计、系统实现、系统测试、总结。关键在于你怎么把论文写得像“自己做的”。需求分析章节要画出用例图、功能模块图配合文字描述用户和管理员的典型操作场景。系统设计章节要把数据库表结构设计放进去每一张表都要有建表语句和说明。系统实现章节是重头戏每个核心功能配一个核心代码片段加运行截图代码不需要多但要挑有代表性的。比如挂号功能就放那段原子更新的代码排班管理就放新增排班的逻辑。这里我想特别提醒一点论文里的截图必须有连贯性要能看出完整的操作流程。比如演示挂号就要先把排班列表截一张再点进详情页截一张再提交挂号后截一张成功页。不要东拼西凑答辩老师最反感前后对不上的截图。5.2 技术难度怎么呈现答辩时最容易被追问的就是“你这个项目的难点在哪里”。如果只是说“我用了 Spring Boot Vue”老师是不会满意的。你要围绕具体的难点讲出故事哪怕它其实并不复杂。比如你可以这样讲系统在设计中验证了号源一致性核心难点是并发场景下避免超卖。然后解释为什么不采用先查询再更新的方式而是使用数据库层的条件更新也就是 update ... where remain_slots 0通过受影响行数判断抢号结果。这个点讲清楚老师就会觉得你是真的在做系统而不是在抄代码。再比如你如果在排班冲突校验上做了 unique 索引或联合查询校验也可以拿出来讲数据完整性设计。哪怕只是一个小点只要是你在实践中遇到问题、思考过、解决了的都比背几个概念更有说服力。5.3 演示预案现场演示最容易出幺蛾子我这里给你一个我常用的“最小演示路径”提前把数据库服务、后端 jar、前端页面全部启动好演示时先注册一个全新用户走一遍完整挂号流程然后在管理端把该排班的余号改成 0再回用户端验证“号已满不能挂号”的提示最后在用户端取消挂号回管理端看余号恢复为 1这条路径覆盖了核心功能、边界状态和事务一致性两分钟就能演示完而且每一步都有画面感。我还见过不少同学专门开着数据库可视化工具在演示取消挂号时把 SQL 查询窗口同时给老师看余号从 0 变 1老师说“这个好”。6. 常见问题与排查技巧实录6.1 启动阶段的高频报错这里把我在帮人排查这套系统时遇到频率最高的几个问题整理出来每一个都对应一个真实场景。报错现象原因解决方式启动时报 Access denied for user数据库用户名密码和配置不一致检查 application.yml确认密码中文乱码数据库字符集不是 utf8mb4重建库或修改库、表字符集时间字段差 8 小时连接串缺少 serverTimezone 参数在 jdbc url 里加上 serverTimezoneAsia/Shanghai端口被占用8080 被其它程序占用改 server.port同时改前端代理 target页面打不开控制台报 Proxy error前端代理配置指向错误端口检查 vue.config.js 的 target前端 npm install 慢或失败默认镜像源问题切换 npmmirror 镜像源第三个问题我多说一句如果 MySQL 服务器是默认 SET time_zone 08:00而 Java 程序连接时没指定时区MyBatis 查出来的 java.util.Date 会和本地相差 8 小时。这个问题的诡异之处在于有时候启动后看不出问题只有插入记录时才发现创建时间不对。最省事的方法就是在 JDBC 连接串里把 serverTimezone 写死。6.2 功能逻辑上的隐蔽 Bug这类问题启动时看不出来要操作到特定流程才会暴露是最容易在答辩时翻车的。第一是取消挂号没有恢复余号。症状是用户取消成功但管理端发现号源越卖越少。排查思路是先看 cancel 接口的事务有没有生效再看更新排班表的方法有没有真正执行。实践中我发现有一类写法是在 Service 层把“更新挂号记录”和“更新排班余号”两个方法都写对了但类上没有加 Transactional 注解导致第一个操作提交后第二个操作抛异常也没回滚数据库状态就错了。第二是排班重复。同一个医生同一天同一时段被排了两次用户端就会出现两个一模一样的时段。排查思路是对 outdoctor_schedule 表做一次 group by doctor_id, schedule_date, start_time having count(*) 1 的查询。如果有数据就要在新增排班的地方加校验或者在数据库层加一个联合唯一索引双保险。第三是用户重复挂号。毕设系统一般不要求像大医院那样限制“同一患者在同一医生同一日期只能挂一个号”但如果你要加就在挂号逻辑里先查询是否存在 user_id 和 schedule_id 相同的记录且状态不是已取消。注意这个查询要在扣减号源之前做。6.3 几个值得保留的调试习惯最后分享几个我做了这么多管理系统以后总结出的调试习惯。第一后端接口有问题时先用浏览器直接访问接口地址或者用 Postman、Apifox 之类的工具测接口确认接口返回正常后再去看前端页面。很多同学一遇到页面报错就改前端代码其实问题可能出在后端空指针或者 SQL 报错后端控制台的异常堆栈才是第一现场。第二MyBatis-Plus 默认打印的 SQL 日志不是特别详尽你可以在配置文件里开启 mybatis-plus 的日志输出mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl开启之后控制台会打印每一条执行的 SQL、参数和返回行数。排查“为什么查不到数据”或者“为什么更新影响了 0 行”这类问题看这个日志是最快的。第三数据库的备份。毕设演示前把初始化好的数据库做一次 mysqldump 导出如果演示时数据被改乱了直接重新导入恢复。这一步成本极低但经常被人忽略在现场演示时才后悔。就比如那次帮一个学弟调试他的用户明明注册成功了但登录一直提示密码错误。查了半天才发现他在注册接口里用的是 MD5 加密而登录接口用了 BCrypt 去校验加密方式不一致当然永远对不上。这种问题不打开 SQL 日志看参数靠肉眼很难定位。这套达明医院网上挂号就诊系统从选题到开发再到答辩准备我基本把能踩的坑都替你们踩了一遍。如果你拿到的是完整的源码项目按照我上面说的思路去梳理目录、调整配置、理解核心流程顺着排班管理和挂号这条主线去扩展到最后你会发现自己不只是“跑通了一个毕设”而是真正啃下了一个完整的管理系统开发流程。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。