
简介面向计算机专业毕业设计的医院挂号小程序完整项目基于微信小程序与SSM、MySQL技术栈开发整合源码、数据库脚本、毕业论文与视频演示可满足从课题分析、系统设计到部署验收的全程需求。压缩包内含858个文件约46.83MB涵盖Vue/JS前端页面、Java后台逻辑、SQL数据库脚本、XML/Properties配置、PNG/SVG界面素材以及mp4视频演示和doc论文文档便于按需取用目录结构清晰包含安装配置脚本与SQL初始化文件降低环境搭建门槛。项目功能覆盖患者、医生、科室、专家信息、预约信息及取消预约申请等管理模块业务链条完整具备较高实用性。同时提供系统可行性分析、数据库设计等文档并附运行脚本与视频演示可帮助理解微信小程序与SSM框架的整合方式快速复现并独立完成论文撰写与答辩准备。已有180人学习适合计算机相关专业学生作为课题参考、课程设计或二次开发基础也便于快速搭建和验证医院挂号业务场景。1. 医院挂号小程序一个能让你少走三周弯路的SSM微信小程序毕业设计医院挂号这个小程序选题在毕业设计里属于典型的“怎么选都不会错”的类型功能明确、流程清晰但真要自己把微信小程序、SSM后端、MySQL三块从零打通一个月的时间大概率是起步价。这套资源把整个工程都配好了后端SpringSpringMVCMyBatis前端包括微信小程序和基于Vue的管理后台外加数据库初始化脚本、毕业论文和演示视频。拿到手不是让你直接交差而是先跑通、再改业务、最后能理直气壮说清楚每个接口在干嘛。适合两类人一类是毕设题目选了医院挂号、想省掉环境搭建和基础代码时间的学生另一类是刚学完Java Web想找一套能完整跑起来的前后端分离项目练手的新手。下面按后端、小程序端、数据库、坑位、进阶的顺序拆开讲。2. 后端SSM结构拆解包怎么分、登录权限怎么做这套后端是标准的SSM工程源码里带1-install.bat、2-run.bat、3-build.bat说明作者默认你在Windows本地一键跑这正好降低了复现门槛。我拆这类项目有个习惯先看包结构再追核心链路这套项目的链路是“小程序端请求→Controller→Service→Mapper→MySQL”只要链路通了后面改业务就是往中间塞逻辑。2.1 工程分包与数据库连接配置SSM工程一般按controller、service、mapper、entity四层拆这套医院挂号小程序后端也遵守这个约定。Controller用RestController直接返回JSONService放业务逻辑Mapper接口配合XML写SQL。因为是前后端分离后端不返回JSP页面所有接口返回统一Result对象里面至少包含code、msg、data三个字段。先从最关键的数据库配置说起。项目里的application.properties通常长这样spring.datasource.urljdbc:mysql://localhost:3306/hospital_appointment?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai spring.datasource.usernameroot spring.datasource.password123456 spring.datasource.driver-class-namecom.mysql.cj.jdbc.Driver mybatis.mapper-locationsclasspath:mapper/*.xml mybatis.type-aliases-packagecom.hospital.entity server.port8080这里每个参数都可能成为坑characterEncodingutf8必须写在URL上否则MySQL连接默认按系统编码解析中文数据存进去再查出来就是乱码serverTimezoneAsia/Shanghai是MySQL 8.x必加的不加会报Server returns invalid timezoneuseSSLfalse只是关掉本地SSL握手避免日志刷一堆warningmapper-locations指向Mapper XML路径如果XML实际放在resources/mapper下但配错启动后调用任何Mapper方法都会报Invalid bound statement (not found)。1-install.bat的逻辑其实就是执行mvn clean install会先把依赖拉下来再打包。跑之前确认本机是JDK 1.8和Maven 3.6左右版本太新容易把老SSM项目编译崩掉。有次我帮人排查他用的JDK 17一跑构建就报IllegalAccessError换回JDK 1.8立刻正常这种问题不是代码错是环境版本不兼容。Controller层是前端唯一会碰到的接口层。以医生管理为例RestController RequestMapping(/api/doctor) public class DoctorController { Autowired private DoctorService doctorService; GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer limit) { PageDoctor data doctorService.list(page, limit); return Result.success(data); } GetMapping(/detail) public Result detail(RequestParam Integer id) { return Result.success(doctorService.getById(id)); } }page和limit默认值必须写因为小程序端第一次加载页面时经常不显式传参没有默认值会直接报400。Result.success(data)是统一返回体代码里写死code200表示成功前端判断业务码而不是HTTP状态码。分页查询在Service层组Page对象常见的实现是PageHelper插件插件版本要和MyBatis版本匹配第5章会专门讲这个坑。Service层是业务逻辑真正所在。比如预约一个医生Service要做三步校验用户存在、校验医生排班时间、插入预约记录。建议方法上挂TransactionalTransactional(rollbackFor Exception.class) public void addAppointment(Appointment appointment) { userMapper.selectById(appointment.getUserId()); doctorMapper.checkSchedule(appointment.getDoctorId(), appointment.getAppointDate(), appointment.getAppointTime()); appointmentMapper.insert(appointment); }rollbackFor Exception.class必须显式写成异常类型。很多毕设只写Transactional不加这个参数一旦遇到非检查异常事务不会自动回滚数据就会半写半不写。这里源码里没看到号源扣减逻辑只写了预约记录是否要补上动态号源第6章会展开。2.2 登录鉴权与三种角色怎么区分系统里用户分三种角色患者、医生、管理员。一张user表通过role字段区分1患者、2医生、3管理员。登录接口直接查用户表PostMapping(/login) public Result login(RequestBody LoginDTO dto, HttpSession session) { User user userService.login(dto.getUsername(), dto.getPassword()); if (user null) { return Result.error(用户名或密码错误); } session.setAttribute(loginUser, user); return Result.success(user); }登录成功后用户对象放进Session后续请求靠Cookie里的JSESSIONID认人。这种机制在毕业设计里够用但要注意Session默认存活时间短用户退出小程序再重进session可能已经失效。如果想做得更稳可以换JWT方案但需要额外维护Token拦截器毕设阶段我更推荐继续用Session把精力放在业务完整度上。权限控制最简单的做法是拦截器Component public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(false); if (session null || session.getAttribute(loginUser) null) { response.setStatus(401); response.getWriter().write({\code\:401,\msg\:\未登录\}); return false; } User user (User) session.getAttribute(loginUser); if (user.getRole() 2 !checkDoctorPermission(request, user)) { response.setStatus(403); return false; } return true; } private boolean checkDoctorPermission(HttpServletRequest request, User user) { String uri request.getRequestURI(); return uri.contains(/doctor) || uri.contains(/appointment); } }上面这段代码演示了角色控制的思路医生登录后只能访问和医生、预约相关的接口。实际项目里不要靠URI字符串判断身份更稳的做法是在每个Service方法里拿当前登录用户的id和资源归属做一次校验。比如取消预约时先判断appointment.userId是不是当前用户不是就直接拒绝。资源里的IndexAsideStatic.vue、IndexHeader.vue、BreadCrumbs.vue这些文件名说明管理后台前端用的是Vue2加ElementUI。管理后台调用的是同一套SSM后端接口只是入口和管理员角色不同。SSM只负责暴露JSON接口Vue负责渲染页面两者完全分离。源码里那些.bak文件是作者改动前的备份直接忽略即可。3. 微信小程序端请求封装、页面状态和联调环境三件事很多拿到这套资源的人会卡在小程序端因为后端跑起来了但小程序界面白屏、请求不通。这章讲清楚小程序端怎么组织代码以及和后端联调的三个关键设置。3.1 小程序目录结构与request请求封装小程序端一般按pages放页面utils放工具函数app.js放全局逻辑。核心是网络请求封装。原始代码里可能是一个request.js我会在它外面再包一层Promiseconst baseUrl http://127.0.0.1:8080; function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: baseUrl path, method: method, data: data, header: { Content-Type: application/json, Cookie: wx.getStorageSync(sessionId) || }, success(res) { if (res.data.code 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail(err) { reject(err); } }); }); } module.exports { request, baseUrl };这段封装有几个细节需要注意。Cookie字段直接取了本地存储的sessionId前提是登录接口返回后你需要把后端返回的Set-Cookie手动写进Storage。res.data.code 200是后端约定的业务码不是HTTP状态码所以后端必须保证成功时返回code200否则Promise会一直走reject。method默认值写GET避免每个页面重复传。在实际调试时baseUrl不要写死localhost。微信开发者工具的模拟器访问本机localhost通常可以但真机预览就必须改成你电脑在局域网里的IP比如http://192.168.1.101:8080。这个坑几乎每个人都会踩而且改完IP还要记得改回否则脱离同一WiFi就请求失败。3.2 页面逻辑科室列表、专家详情、预约提交首页科室列表的逻辑很简单就是发起请求拉/api/department/list拿到数据后setData渲染。真正有技术含量的是预约提交页面const app getApp(); Page({ data: { doctorId: , date: , time: , userId: }, onLoad(options) { this.setData({ doctorId: options.doctorId, userId: wx.getStorageSync(userId) }); }, onDateChange(e) { this.setData({ date: e.detail.value }); }, submitAppointment() { const { doctorId, date, time, userId } this.data; if (!doctorId || !date || !time) { wx.showToast({ title: 请选择完整信息, icon: none }); return; } app.request(/api/appointment/add, POST, { doctorId, date, time, userId }).then(res { wx.showToast({ title: 预约成功, icon: success }); setTimeout(() wx.navigateBack(), 1500); }).catch(err { console.error(预约失败, err); }); } });这个页面里有三个常见错误点。第一userId不要从小程序端传而是从后端Session里取否则用户可以伪造userId预约别人。源码这么写可能是图省事但你要答辩就要能说明这个问题。第二日期选择框用的picker组件返回的是YYYY-MM-DD字符串后端如果定义的是DATETIME字段就需要在前端拼成完整时间。第三预约成功跳转前加setTimeout否则Toast还没展示就跳走了体验很差。app.json里配置页面路径和tabBar如果调试时发现tabBar不显示检查list里的页面路径是否和pages目录一致。另外微信开发者工具的“本地设置”里必须勾选“不校验合法域名”否则真机调试会拦截所有HTTP请求报url not in domain list。这个勾选是开发测试的基础操作但不少新手第一次接触时根本找不到入口。小程序端只是入口真正让预约状态流转起来的是数据库表设计。小程序端只负责提交能否约上还要看数据库里有没有冲突记录所以下一章把表结构讲透。4. 数据库设计五张核心表怎么建、预约状态怎么流转拿到资源后先别急着跑代码先把数据库脚本导入。导入后你会看到一堆表这些表结构直接决定功能边界。这里按我的理解把核心表拆开讲。4.1 核心表结构科室、医生、用户、预约、取消申请先给一套和源码基本对应的表结构CREATE TABLE department ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, intro VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE doctor ( id INT PRIMARY KEY AUTO_INCREMENT, department_id INT NOT NULL, name VARCHAR(50) NOT NULL, title VARCHAR(20), avatar VARCHAR(255), intro TEXT, schedule VARCHAR(255), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (department_id) REFERENCES department(id) ); CREATE TABLE user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(255) NOT NULL, role TINYINT DEFAULT 1, real_name VARCHAR(50), phone VARCHAR(20), id_card VARCHAR(18), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE appointment ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, doctor_id INT NOT NULL, appoint_date VARCHAR(10), appoint_time VARCHAR(20), status TINYINT DEFAULT 0 COMMENT 0待就诊 1已完成 2已取消 3待处理取消, remark TEXT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (user_id) REFERENCES user(id), FOREIGN KEY (doctor_id) REFERENCES doctor(id) ); CREATE TABLE cancel_apply ( id INT PRIMARY KEY AUTO_INCREMENT, appointment_id INT NOT NULL, user_id INT NOT NULL, reason VARCHAR(500), status TINYINT DEFAULT 0 COMMENT 0待审核 1同意 2拒绝, apply_time DATETIME DEFAULT CURRENT_TIMESTAMP, audit_time DATETIME, FOREIGN KEY (appointment_id) REFERENCES appointment(id), FOREIGN KEY (user_id) REFERENCES user(id) );字段取舍上要注意几个点。appoint_date和appoint_time我用的是VARCHAR而不是DATETIME因为医院排班时间段通常是“2024-05-20”和“上午/下午”这种字符串用DATETIME反而要处理时分秒。status用TINYINT比VARCHAR省空间程序里用数字判断更干净。doctor表里的schedule字段是排班的字符串表达比如“周一上午、周三下午”这种简化设计牺牲了号源管理但完全够毕设演示。用户表里的password字段这里是VARCHAR(255)如果你把加密从MD5换成BCrypt长度必须保持255否则哈希后的字符串存不进去。很多人在这一步翻车不是逻辑问题而是字段长度不够。4.2 预约与取消申请的状态流转预约状态是整个系统的核心。我建议在小程序端展示时就约定好0待就诊、1已完成、2已取消、3待处理取消。当患者发起取消预约申请先把appointment.status改成3再往cancel_apply插入申请记录管理员在后台看到申请同意就把appointment.status改成2拒绝就改回0。这个流转用代码表示Transactional(rollbackFor Exception.class) public void applyCancel(Integer appointmentId, Integer userId, String reason) { Appointment appointment appointmentMapper.selectById(appointmentId); if (!appointment.getUserId().equals(userId)) { throw new RuntimeException(无权操作该预约); } if (appointment.getStatus() ! 0) { throw new RuntimeException(当前状态不可取消); } appointmentMapper.updateStatus(appointmentId, 3); cancelApplyMapper.insert(appointmentId, userId, reason); }这段逻辑有两个点值得展开。第一状态更新和申请插入必须在同一个事务里否则状态改成3但申请记录没写进去后台会看到“幽灵”取消申请审核时找不到对应记录。第二必须校验userId归属A用户不能取消B的预约这是权限边界。数据库层面还有一个典型的隐患user表里同时存在患者、医生、管理员三种角色而doctor表又单独维护医生信息。两个表靠医生姓名和科室关联没有外键。实际项目里应该给doctor表加一个user_id字段让医生账号和医生资料联动但不少毕设为了省事就断开了。你拿到资源后可以自己补这样做的好处是管理后台编辑医生信息时能同时改到user表的真实姓名。另外cancel_apply表的appointment_id虽然是外键但源码里大概率没配级联删除。如果你删了一条预约记录取消申请会变成孤儿数据。做扩展时在cancel_apply的appointment_id外键上加ON DELETE CASCADE或者干脆在代码里先删申请再删预约。5. 避坑与常见问题跑这套资源的四个翻车点下面是我在帮人装这套环境时最常见的四个问题按现象、原因、解决三个步骤写基本覆盖从下载到跑通的主干线。5.1 现象后端启动后接口返回404或500后端Tomcat跑起来了但小程序端请求/api/login直接404或者请求医生列表报500。先看控制台异常如果看到Invalid bound statement (not found)大概率是MyBatis的Mapper XML没加载进去。原因mybatis.mapper-locationsclasspath:mapper/*.xml这个路径和实际目录不匹配。有些源码把XML放在src/main/resources/mapper下有些放在src/main/java/mapper下。如果目录不对Spring容器扫描不到XML接口存在但找不到具体SQL于是报绑定错误。解决打开target/classes目录看看mapper文件夹是否存在。不存在就手动把XML拷贝到src/main/resources/mapper下然后重新执行3-build.bat。如果路径没问题还报错再看XML里的namespace是否和Mapper接口的全限定名一致不一致同样报绑定错误。5.2 现象微信开发者工具里请求不到后端小程序页面能打开但所有请求都是红的控制台报url not in domain list或ERR_CONNECTION_REFUSED。原因第一微信开发者工具默认校验HTTPS合法域名本地HTTP接口不在白名单里第二即使关闭了校验如果后端监听的是IPv6或防火墙拦截连接也会被拒绝。最常见的是baseUrl写了localhost但后端只监听0.0.0.0或指定的局域网地址。解决在微信开发者工具右上角“详情-本地设置”里勾选“不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”。同时改request.js里的baseUrl开发者工具模拟器用http://127.0.0.1:8080真机预览改成电脑的局域网IP。改完彻底关掉小程序重新编译热更新有时不生效。5.3 现象导入数据库SQL时报错或者导入后中文变乱码用Navicat导入hospital.sql时提示语法错误或者成功导入后页面显示的中文全是问号。原因数据库脚本的编码和当前MySQL连接的字符集不一致。很多老项目的SQL文件是latin1或GBK编码而连接用的是utf8导入时MySQL按错误编码解析字段注释和中文数据全乱。另一个常见问题是MySQL 8和MySQL 5.7的排序规则差异旧SQL写了utf8_general_ci新库可能不认。解决先用VS Code打开SQL文件确认文件右下角是UTF-8。然后新建数据库字符集设为utf8mb4排序规则utf8mb4_general_ci再导入。导入前在Navicat连接属性里把编码也改成utf8。如果还有问题直接使用MySQL命令行source导入比图形工具更能暴露错误细节。5.4 现象同一时间段被预约了两次这是业务逻辑上的坑。患者在同一个医生、同一天、同一个时间段重复预约数据库里插入了两条记录页面出现撞号。原因预约接口先查库再插入两个请求同时到达时都检查到“无冲突”然后都执行了插入。数据库没有唯一约束兜底应用层也没加锁这就是典型的并发冲突。解决在appointment表的(doctor_id, appoint_date, appoint_time)上建联合唯一索引从数据库层面保证同一时段一个医生只能被约一次。然后在Service层的addAppointment方法上加Transactional配合synchronized做进程内锁。这个修复虽然简单但能让你在答辩时讲清楚并发冲突是怎么解决的。6. 往工程化再走一步统一返回、动态号源和定时清理到这里项目已经能在本地完整跑通但如果你想让它更经得起提问有三个地方值得改。第一个是统一异常处理把前面代码里的throw new RuntimeException接到一个RestControllerAdvice上任何业务异常都会转成{code:500,msg:具体原因}前端不会拿到一长串堆栈。第二个是动态号源当前的doctor.schedule是字符串你把它换成一张doctor_schedule子表存doctor_id、date、time_slot、total_count、booked_count预约时先扣减号源再插预约记录这一下就能堵住并发抢号的漏洞。第三个是定时任务用Spring的Scheduled每天凌晨把超过就诊日期且状态为0的预约改成1实现自动完成避免管理员手动逐条改状态。下面给一个Scheduled的简单示例Component public class AppointmentScheduleTask { Scheduled(cron 0 0 2 * * ?) public void autoFinish() { appointmentMapper.autoCompleteExpired(); } }cron表达式0 0 2 * * ?表示每天凌晨2点执行。数据库里配套的SQL是UPDATE appointment SET status 1 WHERE appoint_date CURDATE() AND status 0。这个逻辑虽然简单但演示了如何把业务状态自动化答辩时拿出来讲比“手动改数据库”有说服力得多。如果还有余力再给预约接口加上防重复提交小程序端在submitAppointment里加一个submitting标志点击后置灰按钮避免用户手抖连续触发两次请求。后端再配合数据库唯一索引兜底双保险基本能保证预约不重复。从那以后我每次拿到一个SSM项目第一件事不是看业务代码而是先跑数据库脚本和构建脚本再打开小程序端检查baseUrl最后才往里钻业务。这个顺序帮我避掉了至少一半的环境问题希望帮到你。本文还有配套的精品资源点击获取