
简介这是一套面向高校毕业设计的医院挂号微信小程序完整工程基于SSMMySQLVue技术栈实现适合计算机相关专业学生直接参考或二次开发。压缩包共包含858个文件大小约46.83MB涵盖Java后端源码、Vue后台管理页面、微信小程序wxml/wxss界面、SQL数据库脚本及jpg/png图标素材等其中mp4演示视频和doc毕业论文可直接辅助答辩。资源已实现患者管理、医生管理、科室管理、专家信息、预约挂号及取消预约申请等核心功能后台采用SSM框架小程序端使用微信开发者工具前后端联调流程完整。毕业论文内容涵盖系统分析、可行性分析与数据库设计便于快速理解项目整体方案。当前已有180人浏览学习适合需要快速搭建医院挂号系统毕业设计、梳理项目文档或学习微信小程序SSM实战的同学。1. 医院挂号小程序毕业设计源码在手跑不起来的大有人在医院挂号小程序是毕业设计里常年不缺人的题目选它的理由很实在业务贴近真实、表关系清楚、微信小程序端是现成技术栈。但真正把「微信小程序 SSM 后端 MySQL 数据库」三头接起来的人往往比选题目的人少一截。经常有人源码到手、论文写了一半最后卡在 MySQL 时区报错、小程序请求 404、POST 参数全 null 上一卡两三天。下面按「建表 → 后端接口 → 小程序调用 → 联调验证 → 填坑」的顺序把医院挂号毕设从数据库到小程序出号的完整做法讲清楚适合需要源码、数据库、论文和演示视频一体交付的同学。目标不是看懂是跑通。2. SSM 后端与 MySQL 表结构挂号业务怎么拆成五张表2.1 为什么毕业设计里 SSM 比 Spring Boot 更适合讲框架原理拿到题目先别急着敲代码先把「SSM 为什么是这个题的默认组合」想明白。SSM 是 Spring、SpringMVC、MyBatis 三个框架的缩写Spring 管对象的创建、依赖注入和事务SpringMVC 管 HTTP 请求的路由和参数绑定MyBatis 管 SQL 和结果集映射。三者各管一段分层非常清楚Controller 收参数、Service 写业务、Mapper 操作数据库。这套分层在毕业设计里价值很大论文的「系统设计」章节能按层展开写答辩时导师顺着三层往下问每一层都能说出具体类名和方法名。相比之下 Spring Boot 是约定优于配置很多环节被自动装配盖住了真被问到「DispatcherServlet 怎么初始化的」反而容易露怯。我一般建议选了 SSM 题的同学答辩前把「一次请求从小程序到数据库再回显」的完整路径背下来这条链路讲顺了比背十个概念都管用。2.2 五张核心表的建表 SQL 与字段设计思路医院挂号的核心流程是「患者选科室 → 选医生 → 选排班 → 提交挂号 → 查看挂号单」。按这个流程拆表最简方案是五张表患者表user、科室表department、医生表doctor、排班表schedule、挂号记录表registration。其它像管理员表、公告表可以后加先把主链路跑通。下面是我常用的建表脚本存成 hospital.sql 直接导入 MySQL 就能用CREATE DATABASE IF NOT EXISTS hospital DEFAULT CHARACTER SET utf8mb4; USE hospital; CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL UNIQUE COMMENT 微信登录标识, name VARCHAR(32) DEFAULT COMMENT 患者姓名, phone VARCHAR(16) DEFAULT COMMENT 手机号, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT患者表; CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, dept_name VARCHAR(50) NOT NULL COMMENT 科室名称, intro VARCHAR(255) DEFAULT COMMENT 科室简介, sort_order INT DEFAULT 0 COMMENT 排序越小越靠前 ) ENGINEInnoDB COMMENT科室表; CREATE TABLE doctor ( id INT PRIMARY KEY AUTO_INCREMENT, dept_id INT NOT NULL COMMENT 所属科室, doctor_name VARCHAR(32) NOT NULL COMMENT 医生姓名, title VARCHAR(32) DEFAULT COMMENT 职称如主任医师, intro VARCHAR(255) DEFAULT COMMENT 医生简介 ) ENGINEInnoDB COMMENT医生表; CREATE TABLE schedule ( id INT PRIMARY KEY AUTO_INCREMENT, doctor_id INT NOT NULL COMMENT 医生, work_date VARCHAR(10) NOT NULL COMMENT 出诊日期如2025-06-01, period TINYINT NOT NULL COMMENT 时段1上午 2下午, total INT NOT NULL COMMENT 总号源数, remain INT NOT NULL COMMENT 剩余号源数, version INT DEFAULT 0 COMMENT 乐观锁版本号 ) ENGINEInnoDB COMMENT医生排班表; CREATE TABLE registration ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 挂号单号, user_id INT NOT NULL COMMENT 患者, schedule_id INT NOT NULL COMMENT 关联排班, doctor_id INT NOT NULL COMMENT 冗余医生, dept_id INT NOT NULL COMMENT 冗余科室, doctor_name VARCHAR(32) DEFAULT COMMENT 冗余医生姓名, dept_name VARCHAR(50) DEFAULT COMMENT 冗余科室名称, reg_date VARCHAR(10) NOT NULL COMMENT 就诊日期, period TINYINT NOT NULL COMMENT 1上午 2下午, status TINYINT DEFAULT 0 COMMENT 0待就诊 1已完成 2已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT挂号记录表;这里有几个字段是故意「冗余」的registration 表里存了 doctor_name、dept_name、reg_date、period。原因是挂号单生成之后排班表的数据可能调整比如医生停诊、日期变动挂号单作为凭证要保留用户下单那一刻的快照。联表查询当然也能查出这些字段但写 SQL 会多两个 JOIN而且排班一旦改了历史挂号单显示的内容也会跟着变这在业务上是不能接受的。毕业设计里能主动解释「为什么冗余」是加分项。version 字段是给「号源扣减」准备的。用户提交挂号时后端执行的是「先查剩余号再更新剩余号」两步操作如果不做并发控制两个用户同时挂最后一个号都看到 remain1就会都扣成功超挂。加了 version 之后更新的 SQL 会带上WHERE version 旧值更新成功才把 version 加一失败就提示「号源已被抢完」。这个点放进论文「系统安全与并发控制」一节比写一堆理论管用。2.3 MyBatis 的 Mapper 接口与 XML挂号记录联表查询表建好后写访问层。MyBatis 我喜欢用接口 XML 的方式SQL 可读性好答辩时也方便指着 XML 讲。先定义接口public interface RegistrationMapper { ListRegistrationVO selectMyRegistrations(Param(userId) Integer userId); int deductRemain(Param(scheduleId) Integer scheduleId, Param(expectedVersion) Integer expectedVersion); }deductRemain 就是前面说的乐观锁扣减。对应的 XML 片段select idselectMyRegistrations resultTypecom.example.domain.RegistrationVO SELECT r.order_no, r.reg_date, r.period, r.status, r.doctor_name, r.dept_name FROM registration r WHERE r.user_id #{userId} ORDER BY r.create_time DESC /select update iddeductRemain UPDATE schedule SET remain remain - 1, version version 1 WHERE id #{scheduleId} AND remain 0 AND version #{expectedVersion} /update代码块里的#{userId}是预编译占位符MyBatis 会把它转成 PreparedStatement 的?能防 SQL 注入和字符串拼接的${}有本质区别。联表查询这里直接用冗余字段完全不需要 JOIN如果不用冗余字段就得把 schedule、doctor、department 三张表 JOIN 进来SQL 长一倍查询效率也差一些。RegistrationVO 是一个普通 POJO字段名和查询列的驼峰映射记得在 mybatis-config.xml 里开启mapUnderscoreToCamelCasetrue否则 r.reg_date 映射不到 regDate 上——这个配置漏了前端拿到的 JSON 里字段全是 null是后面排查的重点之一。3. 微信小程序端结构与登录态从 wx.login 到 openid 的闭环3.1 小程序目录结构页面、工具函数、全局配置的分工拿到了小程序源码先别急着点编译把目录结构认一遍。典型的小程序端分四块pages 放页面utils 放工具函数app.js 是全局逻辑app.json 是全局配置。tabBar 页面首页、科室、我的放在 pages 第一层非 tab 页面医生列表、确认挂号、挂号记录单独建目录每个页面由 .js、.json、.wxml、.wxss 四个文件组成。app.js 里我习惯把后端地址集中到一个变量避免每个页面各写一个 URLApp({ globalData: { // 开发环境用局域网IP上线换成正式域名 baseUrl: http://192.168.1.100:8080/hospital-api } })这里的 baseUrl 是整个联调的关键。注意两点一是地址里的/hospital-api是后端工程的 contextPath后端部署时叫什么这里就得写什么拼错了就是 404二是开发阶段不能用 localhost模拟器里 localhost 指电脑本身问题不大但手机真机预览时 localhost 指的是手机必然连不上所以要填电脑在局域网里的 IP。3.2 wx.request 封装与后端接口对齐路径和参数名不一致是常态小程序官方提供的 wx.request 是回调式写法每个页面都写一遍回调会很散。我一般先封装一个 Promise 版本的 request统一处理 token、错误码和提示const request (options) { return new Promise((resolve, reject) { wx.request({ url: getApp().globalData.baseUrl options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, token: wx.getStorageSync(token) || }, success: (res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data) } else if (res.statusCode 401) { wx.navigateTo({ url: /pages/login/login }) reject(res) } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }) reject(res) } }, fail: (err) reject(err) }) }) }封装里最重要的约定是「接口返回结构」后端统一返回{ code: 0, msg: ok, data: ... }code 为 0 表示成功非 0 是业务错误HTTP 401 表示登录态过期。这个约定前后端必须对齐后端如果返回的字段叫message而不是msg前端这里就要跟着改否则提示语永远是空。header 里的 token 从本地缓存读取后端拦截器拿到它做登录校验这样每个页面不用重复写鉴权逻辑。Content-Type 这里还有一个容易翻车的细节如果后端接口用RequestBody接 JSON那 header 必须是application/jsondata 直接传对象如果后端用RequestParam接表单参数header 要设成application/x-www-form-urlencoded。封装里写死application/json意味着后端所有 POST 接口都得统一用RequestBody接收——前后端约定不一致是「请求发出去了后端却收到 null」的最常见原因。3.3 登录态怎么存本地缓存与后端 session 的关系微信小程序的登录和网页登录不一样没有账号密码核心是「用 wx.login 换 code再用 code 换 openid」。完整流程是小程序调 wx.login 拿到临时 code把 code 发给后端后端拿 code 加上自己的 appid 和 secret 去调微信官方的 jscode2session 接口换回 openid 和 session_key后端拿 openid 去 user 表查用户查不到就自动注册一条然后后端生成自己的登录态比如 UUID存到后端缓存或数据库再把 token 返回给小程序。小程序拿到 token 后存进本地缓存之后每个请求都带上它。这里有个常见的错误设计小程序端每次启动都调 wx.login后端每次都重新生成 token导致旧的 token 立刻失效用户操作到一半就被踢下线。正确的做法是先读本地缓存的 token调一个接口验证有效性失效了才重新走 wx.login。const login () { return new Promise((resolve) { wx.login({ success: async (res) { const { code } res const data await request({ url: /auth/login, method: POST, data: { code } }) wx.setStorageSync(token, data.token) wx.setStorageSync(userInfo, data.userInfo) resolve(data) } }) }) }这段代码里的code是一次性的用完之后不能再复用所以后端拿到 code 换 openid 失败时要让前端重新调 wx.login 取新 code而不是重试同一个 code。后端 token 建议和 openid 绑定存到一张表或缓存里并设置过期时间毕业设计里用一张user_token表最简单字段就是 token、user_id、expire_time查登录态时一条 SQL 就能校验。4. 本地跑通前后端联调三件套就位后的最小可用链路4.1 后端跑起来Tomcat 部署 SSM 项目的最小步骤后端是 SSM最稳妥的部署方式是打 war 包丢给 Tomcat。环境建议 JDK 8 Tomcat 8.5/9 MySQL 5.7/8版本组合别乱JDK 版本太高会碰到 Tomcat 兼容问题徒增排查成本。拿到工程后先改数据库配置SSM 项目里通常是一个 jdbc.properties 或 db.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hospital?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password123456MySQL 8 和 MySQL 5.7 的驱动类名不一样5.7 用com.mysql.jdbc.Driver8.x 必须换成com.mysql.cj.jdbc.Driver。url 里的serverTimezoneAsia/Shanghai也不能省不写的话 MySQL 8 会在启动时报The server time zone value的错这是毕业设计里出现频率最高的启动报错没有之一。password 记得改成自己本机的密码很多源码包的配置是作者本机的直接跑必然连不上。改完配置后在项目根目录执行打包命令mvn clean package -DskipTests打包完成后把 target 目录下生成的 war 包复制到 Tomcat 的 webapps 目录启动 Tomcatwar 包会被自动解压部署。然后打开浏览器访问curl http://localhost:8080/hospital-api/health/check注意访问路径里的/hospital-api是 war 包的名字如果 war 叫 ROOT.war路径就没有这一段。这里给后端加一个健康检查接口返回固定 JSON是验证后端是否真正起来的快捷手段。浏览器能返回数据后端就基本没问题剩下的事都交给小程序端。4.2 小程序开发者工具里的两个开关不校验合法域名与局域网地址后端起来了小程序还在报「不在以下合法域名列表中」或「网络错误」这是两个开关没设置。第一个开关在开发者工具的「详情 → 本地设置」里勾选「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」。开发阶段后端是 HTTP 协议的局域网地址不满足微信对正式环境 HTTPS 备案域名的要求不勾这个开关请求会被直接拦掉。第二个开关是 baseUrl 的写法。前面说过不能用 localhost要填电脑的局域网 IP。怎么查这个 IPWindows 上用ipconfigmacOS/Linux 上用ifconfig找到 192.168.x.x 或 10.x.x.x 开头的地址填进 app.js。注意电脑和小程序模拟器共用同一网络手机真机预览时手机和电脑也要在同一个 WiFi 下。提示这两个设置只用于开发联调。交付或上线前必须换 HTTPS 正式域名并把「不校验合法域名」关掉否则正式环境直接白屏。4.3 从「科室列表」到「提交挂号」一条完整链路的验证顺序前后端都准备好了别直接点「提交挂号」那个链路最长出错不好定位。我习惯按下面的顺序逐级验证先调一个最简单的 GET 接口科室列表确认通路通再验登录接口拿到 token再带 token 查医生列表最后才提交挂号。科室列表页的核心代码就一段const loadDepartments () { request({ url: /department/list, method: GET }).then((list) { this.setData({ deptList: list }) }) }这段代码能跑通说明后端接口、IP、端口、contextPath、JSON 返回结构全部没问题。之后每一步如果失败按顺序排查看小程序 Network 面板里请求有没有发出、状态码是多少看后端控制台有没有对应的 SQL 日志再看数据库里数据有没有变化。三个位置的信息对不上就能定位到具体是哪一层出了问题。提交挂号成功后去数据库 registration 表查一条记录顺便把 schedule 表的 remain 数看一遍确认号源确实扣了。5. 踩坑排查毕业设计里最容易翻车的 5 个位置5.1 现象MySQL 连接失败报错关键词是 time zone 或 ClassNotFoundException「Tomcat 启动时报错数据库连不上」排在坑位第一名。最常见的是两种一种是 MySQL 8 驱动类加载不到报ClassNotFoundException: com.mysql.jdbc.Driver原因是 pom 里引入的驱动版本是 5.x或者 jdbc.properties 里驱动类名没换另一种是The server time zone value报错原因是驱动和数据库之间的时区协商失败。解决pom 里 mysql-connector-java 的版本换成 8.0.x驱动类名统一用com.mysql.cj.jdbc.Driverjdbc.url 里拼上serverTimezoneAsia/Shanghai。改完 clean 再 package别只改文件不重新打包后者是很多人反复试错没效果的原因。另外确认本机 MySQL 的端口是不是 3306装过多个实例的机器端口经常被改掉url 里的端口和实际不匹配也会连不上。5.2 现象小程序请求一直 404后端接口明明存在接口在 Controller 里写着浏览器直接访问也能出数据小程序里就是 404。原因几乎都是路径拼接问题baseUrl 里的 contextPath 写错或者 RequestMapping 的路径和前端 url 不一致。举例后端工程 contextPath 是hospital-apiController 的类上写RequestMapping(/department)方法上是RequestMapping(/list)完整路径是/hospital-api/department/list前端 url 必须从department/list写起baseUrl 里已经有hospital-api了就不能再重复写。解决先打开后端 Controller 确认类和方法上的路径再打开 app.js 确认 baseUrl最后打开页面的 request 调用确认 url。三个位置拼起来和浏览器访问的地址一致404 立刻消失。还要注意类上和方法上的路径之间有没有斜杠/department拼list和拼/list结果不同这类斜杠问题经常肉眼看不出来直接复制地址栏里的完整路径去对比最快。5.3 现象POST 提交挂号的请求发出去了后端收到的参数全是 null前端 console 里能看到请求成功发出后端日志也打印了请求进来但 Controller 方法里的参数对象全是 null或者个别字段是 null。这类问题九成出在 Content-Type 和参数接收方式不匹配。解决先看后端接口签名。如果方法参数写了RequestBody RegistrationDTO dto前端 header 必须是application/jsondata 传对象如果写的是RequestParam前端必须用表单格式提交。我的习惯是后端全部统一用RequestBody接收 JSON前端封装里固定application/json用不变量消灭这一类问题。个别字段是 null 的再去核对前端传的字段名和后端 DTO 的属性名是不是大小写不一致。DTO 里的字段建议都用包装类型 Integer、Long别用 int否则前端传 null 时后端会解析成 0看起来像「有值」其实是错的。5.4 现象登录态一刷新就丢页面来回跳登录小程序里操作几步就跳回登录页或者某个页面拿到的是「未登录」错误。最常见的原因是每次进页面都调 wx.login而后端每调一次就生成新的 token 并把旧 token 作废前一个请求还没用完token 就被后一个请求顶掉了。另一个原因是后端把登录态存在自己的 session 里小程序端其实是拿不到 session 的每次请求也不一定会带上相同的 Cookie。解决小程序端加一个「token 存在就直接用不再调 wx.login」的判断后端用 token 表加过期时间校验逻辑从 session 改成查 token。wx.login 只在首次启动或 token 校验失败时调用一次。还要注意后端的登录拦截器要排除登录接口本身以及健康检查、科室列表这类不需要登录就能访问的接口否则会出现「登录接口被登录校验拦住」的死循环症状也是反复跳登录页。5.5 现象模拟器正常真机预览白屏或请求失败模拟器里数据齐全一扫码真机就成了白屏或者接口报错这个现象最玄学原因其实就两类域名校验没关或者 baseUrl 的 IP 不对。真机上微信小程序默认强制校验合法域名开发版虽然可以绕过但很多人不知道要打开调试开关。IP 的问题就是前面说的 localhost模拟器上 localhost 指电脑真机上指手机自己自然连不上。解决真机预览时在小程序右上角菜单里打开「调试」baseUrl 改成电脑的局域网 IP并确保手机和电脑在同一个网络如果学校或公司网络有隔离策略手机和电脑互相 ping 不通那就换一个手机热点电脑连手机热点后重新查 IP 填进去。这个「换热点」的办法是联调里最实用的后悔药。真机上调试信息不显示在小程序开发者工具里建议在后端打印日志或者在小程序里引入 vConsole 看前端报错能省下大量「到底谁没响应」的猜测时间。6. 让毕设答辩更稳视频演示脚本与论文里的非功能设计6.1 视频演示脚本按「角色-操作-预期结果」录演示视频最容易翻车的地方是录的时候手忙脚乱页面来回跳最后自己都不知道点到了哪里。我的做法是先写脚本再录每一步只做一个动作录完不剪辑也能看。下面是一个可以直接套用的四步脚本步骤操作预期结果1打开小程序进入首页展示科室列表请求无报错2点击某个科室进入医生列表医生信息显示完整3选择某医生的排班点击挂号弹出确认框号源数减一4进入「我的挂号」显示刚提交的挂号单状态为待就诊录制时先把小程序开发者工具的 Network 面板和控制台日志关掉画面干净。每个步骤之间停两秒给讲解留空隙。论文里对应的功能截图和视频保持同一顺序答辩时照着讲演示和文档就对上了。6.2 论文不能只写 CRUD三个必写的非功能点挂号类系统论文里只写增删改查是不够的评审老师大概率会追问「多人同时挂号怎么办」「登录过期怎么办」。这三个非功能点建议写进论文一是号源扣减用乐观锁把 2.2 节里的 version 字段和 2.3 节的 deductRemain SQL 解释清楚二是登录态用 token 统一管理过期返回 401前端统一跳登录页把 3.2 节的封装逻辑写进系统设计三是异常提示统一由前端根据 code 弹 toast把「后端返回什么错误码前端显示什么话术」列成一章。这三个点每一样都只多花几页但答辩深度完全不一样。我自己的习惯是论文写完先自己照着 6.1 的脚本走一遍全流程录完视频再核对一遍数据确认没有把开发环境里的调试弹窗录进去。能做到这一步答辩演示基本不会翻车。这篇内容里覆盖的坑都是这些年看别人踩过、自己也踩过的照着这个顺序走能少熬几个夜。希望帮到你。本文还有配套的精品资源点击获取