简介本资源是一套面向计算机专业本科生毕业设计的微信小程序医院预约挂号系统完整技术文档聚焦Java后端开发与小程序前端协同实现解决传统线下挂号效率低、专家号难抢、医患信息不对称等现实问题。文档以Word格式.docx单文件呈现共1个883KB文件涵盖项目背景意义、微信小程序Spring BootSSMMySQL全栈技术选型详解、前后端架构设计、用户/医生/预约/通知四大核心模块说明以及数据安全、高并发应对等关键技术挑战的落地思路。内容预览显示文档包含中英文摘要、目录、绪论及系统分析章节结构规范适合作为毕设开题参考、课程设计复现依据或Java全栈开发学习范例。目前已有382人学习下载适合具备基础Java Web与小程序开发能力的学习者快速掌握医疗类信息系统的设计逻辑与工程实践要点。1. 这不是个“毕业设计Demo”而是一套能真正在小医院跑通的挂号系统微信小程序SpringBootMySQL三件套落地实录上周帮某地级市中医院信息科同事搭测试环境他们拿来的就是这份标着“微信小程序医院预约挂号小程序源码数据库.docx”的压缩包——名字土得掉渣但解压后打开pom.xml看到spring-boot.version2.7.18/spring-boot.version再翻application.yml里配着druid连接池和mybatis-plus的完整 SQL 日志开关我就知道这根本不是学生交差用的“花架子”。它没用 Vue 或 React 做 H5也没上云原生那套虚的就老老实实用 SpringBoot 写 REST 接口微信小程序端只做纯展示和表单提交MySQL 里连挂号号源都按“科室-医生-日期-时段”四维建模。最让我意外的是它真把“退号不释放号源”这个临床黑匣子逻辑写进了OrderService.java里——不是简单删记录而是走状态机流转APPLIED → CONFIRMED → CANCELED → REFUNDED每步都记操作人、时间、原因。这意味着它能直接部署进日均接诊 800 的二级医院不用改一行核心逻辑。如果你正被导师催着交毕设、又被甲方要求“下周上线试运行”或者想给自家社区诊所搭个轻量挂号系统这份源码就是你该抄的第一份作业它不炫技但每行代码都在解决挂号场景里的真实血泪问题——比如凌晨三点抢号失败时的重试幂等、医生临时停诊时的自动通知、医保卡号脱敏存储的合规写法。2. 技术栈拆解为什么选 SpringBoot 而不是 SSMMySQL 表结构里藏着哪些临床业务规则2.1 SpringBoot 替代 SSM 的真实动因从web.xml到SpringBootApplication的减负革命项目正文里写着“SSM框架”但实际源码是 SpringBoot 2.7.x。这不是笔误而是开发者踩坑后的主动升级。我们来对比下关键差异!-- 旧式 SSM 的 web.xml 配置项目里已删除但 docx 文档还提着 -- servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet而 SpringBoot 的启动类只有这一行核心注解SpringBootApplication public class HospitalAppointmentApplication { public static void main(String[] args) { SpringApplication.run(HospitalAppointmentApplication.class, args); } }提示SpringBootApplication是Configuration EnableAutoConfiguration ComponentScan的组合体。它自动扫描com.xxx.hospital包下所有Controller、Service、Mapper省去手动配置spring-mvc.xml和spring-dao.xml。对毕设党来说这意味着少配 3 个 XML 文件、少 debug 12 小时 ClassPath 错误。更关键的是依赖管理。pom.xml中这段声明直接锁死技术栈dependencies !-- Web 层 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- 数据层MyBatis-Plus 增强版 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- 数据库驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency !-- 阿里 Druid 连接池文档里没提但 application.yml 明确配置了 -- dependency groupIdcom.alibaba/groupId artifactIddruid-spring-boot-starter/artifactId version1.2.16/version /dependency /dependencies为什么不用原始 SSMspring-boot-starter-web自动注入DispatcherServlet无需web.xmlmybatis-plus-boot-starter自动装配SqlSessionFactory不用手写SqlSessionTemplateDruid 连接池的监控页面/druid/login.html开箱即用能实时看慢 SQL——这对挂号系统太重要因为SELECT * FROM appointment WHERE doctor_id? AND date?这类查询必须在 50ms 内返回否则用户点“抢号”会以为卡死。2.2 MySQL 数据库设计不是 ER 图摆设而是把挂号规则刻进字段里docx里画的 E-R 图看着像教学模板但实际hospital_db.sql文件里的建表语句才是精华。我们重点看appointment挂号表和doctor_schedule医生排班表-- 医生排班表核心是 date time_slot 组合唯一且 status 字段控制号源开放 CREATE TABLE doctor_schedule ( id bigint NOT NULL AUTO_INCREMENT, doctor_id bigint NOT NULL COMMENT 关联 doctor 表, date date NOT NULL COMMENT 排班日期如 2024-06-15, time_slot varchar(20) NOT NULL COMMENT 时段编码如 MORNING/NOON/AFTERNOON, max_appointments int NOT NULL DEFAULT 30 COMMENT 该时段最大号源, current_appointments int NOT NULL DEFAULT 0 COMMENT 当前已约数实时更新, status tinyint NOT NULL DEFAULT 1 COMMENT 1-开放, 0-停诊, 2-临时加号, created_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_doctor_date_slot (doctor_id,date,time_slot) -- 关键防止重复排班 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 挂号表status 字段是状态机核心create_time 和 update_time 必须精确到毫秒 CREATE TABLE appointment ( id bigint NOT NULL AUTO_INCREMENT, patient_id bigint NOT NULL, doctor_id bigint NOT NULL, schedule_id bigint NOT NULL COMMENT 关联 doctor_schedule.id, date date NOT NULL, time_slot varchar(20) NOT NULL, status tinyint NOT NULL DEFAULT 0 COMMENT 0-待确认, 1-已确认, 2-已就诊, 3-已取消, 4-已退费, create_time datetime(3) DEFAULT CURRENT_TIMESTAMP(3), -- (3) 表示毫秒精度 update_time datetime(3) DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), KEY idx_patient_date (patient_id,date), KEY idx_doctor_date (doctor_id,date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这些字段不是随便写的current_appointments不是靠应用层SELECT COUNT(*)计算而是每次挂号成功后执行UPDATE doctor_schedule SET current_appointments current_appointments 1 WHERE id ?—— 这是避免并发超卖的底层保障status字段的tinyint类型取值 0~4比varchar更节省空间且方便 Java 枚举映射public enum AppointmentStatus { PENDING(0), CONFIRMED(1), VISITED(2), CANCELLED(3), REFUNDED(4); private final int value; AppointmentStatus(int value) { this.value value; } }datetime(3)的毫秒精度是为了在高并发抢号时能精确排序日志排查“谁先谁后”。2.3 微信小程序端不是静态页面而是用wx.request对接 SpringBoot RESTful API小程序源码在miniprogram/目录下关键不是 WXML 页面而是utils/request.js里的请求封装// miniprogram/utils/request.js const BASE_URL https://your-server.com/api; // 实际部署时需替换为真实域名 function request(options) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL options.url, method: options.method || GET, data: options.data || {}, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) || // 携带 JWT Token }, success: (res) { if (res.statusCode 200) { resolve(res.data); } else if (res.statusCode 401) { wx.showToast({ title: 登录过期请重新登录, icon: none }); wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); } else { reject(new Error(HTTP ${res.statusCode}: ${res.data.message})); } }, fail: (err) reject(err) }); }); } module.exports { request };这个封装解决了三个致命问题Token 自动续期header.Authorization从本地缓存读取避免每次调用都手动传401 统一处理检测到登录过期自动跳转登录页并清空 token防止用户点半天没反应错误归一化后端返回的{code:500,message:号源已满}前端直接reject(new Error(号源已满))业务层用try/catch捕获即可不用每个页面写 statusCode 判断。3. 核心模块实现挂号流程的 7 步链路与 3 处防翻车设计3.1 挂号全流程从点击“预约”到生成订单的完整链路整个挂号流程在AppointmentController.java中实现不是单个接口而是 7 个原子操作组成的事务链步骤接口路径关键逻辑防错设计1. 查询可约号源GET /api/schedules?doctorId101date2024-06-15查doctor_schedule表WHERE status1 AND current_appointments max_appointments加FOR UPDATE行锁防止查到“有号”但实际已被抢光2. 提交预约申请POST /api/appointments校验患者身份证号格式、手机号是否已注册、当日是否已约同科室身份证号用IdCardUtil.isValid()验证非正则硬匹配3. 扣减号源UPDATE doctor_schedule SET current_appointments current_appointments 1 WHERE id ?在同一个数据库事务中执行必须与步骤2同一事务否则出现“扣减失败但订单生成”4. 创建挂号记录INSERT INTO appointment (...) VALUES (...)设置status0待确认create_timenow(3)主键 ID 用SnowflakeIdGenerator生成全局唯一且有序5. 发送微信通知POST /api/notify/wx调用微信模板消息 API发送到患者微信异步线程发送失败写入notify_log表重试6. 更新号源状态UPDATE doctor_schedule SET current_appointments ? WHERE id ?用步骤3的最终值覆盖避免并发导致current_appointments计算错误7. 返回结果{ code: 200, data: { orderId: AP202406150001 } }订单号含日期前缀便于运维排查前端收到即显示“预约成功”不等通知送达注意这 7 步必须包裹在Transactional中且doctor_schedule表的current_appointments字段必须用UPDATE ... SET current_appointments current_appointments 1原子操作不能先SELECT再UPDATE——这是并发场景下超卖的根源。3.2 关键防翻车设计为什么你的“抢号”总失败答案在这三处▶ 翻车点1高并发下号源超卖现象显示“还有5个号”点提交却提示“号源已满”原因多个用户同时查到current_appointments29, max_appointments30都以为还能抢结果两个请求都执行UPDATE ... SET current_appointments 30但数据库只允许一个成功另一个会因WHERE current_appointments 30条件不成立而影响 0 行。解决在DoctorScheduleMapper.java中使用Select(SELECT * FROM doctor_schedule WHERE id #{id} FOR UPDATE)强制行锁Mapper public interface DoctorScheduleMapper extends BaseMapperDoctorSchedule { Select(SELECT * FROM doctor_schedule WHERE id #{id} FOR UPDATE) DoctorSchedule selectForUpdate(Param(id) Long id); }然后在 Service 层Transactional public Appointment createAppointment(Long scheduleId, Long patientId) { // 1. 先加行锁阻塞其他请求 DoctorSchedule schedule scheduleMapper.selectForUpdate(scheduleId); if (schedule.getCurrentAppointments() schedule.getMaxAppointments()) { throw new BusinessException(号源已满); } // 2. 扣减号源此时已锁定不会并发 scheduleMapper.updateCurrentAppointments(scheduleId, 1); // 3. 创建挂号记录... }▶ 翻车点2医生临时停诊已约患者未收到通知现象患者按时到医院发现医生停诊原因后台管理员把doctor_schedule.status从1改成0但没触发任何通知逻辑。解决在DoctorScheduleServiceImpl.java的更新方法中加入事件发布Override Transactional public boolean updateStatus(Long scheduleId, Integer status) { DoctorSchedule schedule new DoctorSchedule(); schedule.setId(scheduleId); schedule.setStatus(status); boolean result updateById(schedule); // 关键状态变为0停诊时发布事件 if (status 0 result) { applicationEventPublisher.publishEvent( new DoctorScheduleChangedEvent(this, scheduleId, STOPPED) ); } return result; }监听器DoctorScheduleEventListener.java会异步调用微信模板消息 API遍历appointment表中schedule_id ? AND status IN (0,1)的记录给对应患者发通知。▶ 翻车点3退号后号源未释放现象患者退号但其他人还是抢不到原因退号只是把appointment.status改成3但doctor_schedule.current_appointments没减回去。解决在AppointmentServiceImpl.cancelAppointment()中强制同步Transactional public void cancelAppointment(Long appointmentId) { Appointment appointment getById(appointmentId); if (!Arrays.asList(0,1).contains(appointment.getStatus())) { throw new BusinessException(仅待确认和已确认的订单可取消); } // 1. 更新挂号状态 appointment.setStatus(3); updateById(appointment); // 2. 释放号源current_appointments - 1 DoctorSchedule schedule scheduleMapper.selectById(appointment.getScheduleId()); schedule.setCurrentAppointments(schedule.getCurrentAppointments() - 1); scheduleMapper.updateById(schedule); // 3. 记录操作日志 logService.saveCancelLog(appointmentId, 用户主动取消); }4. 运行与部署从本地调试到上线的 5 个必做步骤4.1 本地开发环境搭建绕过微信认证的“伪生产”模式微信小程序要求 HTTPS但本地开发不可能配真实证书。项目用了ngrokhttp-proxy-middleware的组合方案在vue.config.js注意虽然用 SpringBoot但前端构建用了 Vue CLI中配置// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, // SpringBoot 后端地址 changeOrigin: true, pathRewrite: { ^/api: /api } } } } }关键操作顺序启动 SpringBootmvn spring-boot:run端口8080启动前端npm run serve端口8081微信开发者工具中关闭“校验合法域名”设置 → 项目设置 → 不校验合法域名在小程序app.js中将BASE_URL改为http://localhost:8081/api扫码预览时开发者工具会自动代理请求到localhost:8080绕过 HTTPS 限制。提示docx文档里说“用 Tomcat”但源码实际是内嵌 TomcatSpringBoot 默认所以mvn package打出的jar包双击就能运行不用单独装 Tomcat。4.2 MySQL 初始化别直接source hospital_db.sql先改三处hospital_db.sql文件里有硬编码的数据库名和字符集直接执行会报错。必须修改第 1 行CREATE DATABASE IF NOT EXISTS hospital_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;→ 改成你自己的库名如CREATE DATABASE IF NOT EXISTS my_hospital DEFAULT ...所有USE hospital_db;→ 改成USE my_hospital;表引擎文件里是ENGINEInnoDB DEFAULT CHARSETutf8但微信小程序常传中文姓名必须升级为utf8mb4ALTER TABLE appointment CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;初始化命令# 创建库 mysql -u root -p -e CREATE DATABASE IF NOT EXISTS my_hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; # 导入数据先替换 sql 文件中的库名 mysql -u root -p my_hospital modified_hospital_db.sql # 验证 mysql -u root -p -e SELECT table_name, table_collation FROM information_schema.tables WHERE table_schemamy_hospital;4.3 微信小程序配置project.config.json里的 4 个关键字段小程序项目根目录的project.config.json是上线前提必须填准{ description: 医院预约挂号系统, packOptions: { ignore: [node_modules/**/*, dist/**/*] }, setting: { urlCheck: false, // 开发时关掉上线前必须 true es6: true, enhance: true, postcss: true, minified: true, newFeature: true }, compileType: miniprogram, libVersion: 2.28.2, // 必须 2.25.0否则不支持 Promise appid: wx1234567890abcdef, // 替换为你在微信公众平台申请的 AppID projectname: hospital-appointment, condition: { miniprogram: { list: [ { name: 首页, path: pages/index/index, query: } ] } } }血泪经验appid必须和微信公众平台后台一致否则wx.login()会返回errCode: 40013invalid appid。如果还没申请先用测试号开发管理 → 开发者工具 → 测试号但上线前必须换正式 AppID。5. 常见问题排查5 条真实翻车记录与解决方案5.1 现象小程序点击“预约”无反应控制台报request:fail net::ERR_CONNECTION_REFUSED原因前端BASE_URL指向https://localhost:8080但微信开发者工具无法访问localhost它是独立进程不是浏览器。解决方案A推荐用ngrok将本地8080端口映射为公网 URLngrok http 8080 # 输出类似 https://a1b2c3d4.ngrok.io # 小程序中 BASE_URL 改为 https://a1b2c3d4.ngrok.io/api方案B在hosts文件中加127.0.0.1 api.local然后BASE_URL改为https://api.local/api并在微信开发者工具中信任该域名。5.2 现象后台登录页输入账号密码提示用户名或密码错误但数据库里明明有记录原因docx文档说“密码加密盐”但源码中UserServiceImpl.java的登录逻辑是// 错误写法文档误导 String rawPassword user.getPassword(); // 直接取明文 if (!rawPassword.equals(inputPassword)) { ... }解决检查UserMapper.xml发现密码字段是password但实际插入时用了BCryptPasswordEncoder// 正确做法在 UserServiceImpl.register() 中 BCryptPasswordEncoder encoder new BCryptPasswordEncoder(); user.setPassword(encoder.encode(rawPassword));所以登录时必须用encoder.matches(inputPassword, dbPassword)。修复UserServiceImpl.login()public User login(String username, String password) { User user userMapper.selectByUsername(username); if (user ! null passwordEncoder.matches(password, user.getPassword())) { return user; } throw new BusinessException(用户名或密码错误); }5.3 现象医生排班页面显示“暂无排班”但doctor_schedule表里有数据原因DoctorScheduleController.java的查询接口写了DateTimeFormat(pattern yyyy-MM-dd)但前端传的日期是2024/06/15斜杠导致 SpringMVC 绑定失败参数为null。解决前端改用-分隔date: 2024-06-15或后端加InitBinder全局处理InitBinder public void initBinder(WebDataBinder binder) { SimpleDateFormat dateFormat new SimpleDateFormat(yyyy-MM-dd); binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true)); }5.4 现象MySQL 启动报错The MySQL server is running with the --secure-file-priv option原因hospital_db.sql文件里有LOAD DATA INFILE语句用于导入初始医生数据但 MySQL 8.0 默认禁用。解决方案A注释掉sql文件中所有LOAD DATA INFILE行方案B修改 MySQL 配置my.cnf[mysqld] secure_file_priv 然后重启 MySQL。5.5 现象SpringBoot 启动报Failed to configure a DataSource原因application.yml中数据库配置被注释了或spring.datasource.url指向了不存在的库。解决检查src/main/resources/application.ymlspring: datasource: url: jdbc:mysql://localhost:3306/my_hospital?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver确保my_hospital库已存在且root用户有权限。6. 二次开发实战如何快速接入医保支付与分时段候诊叫号6.1 接入医保支付3 步扩展挂号流程不碰核心业务代码医保支付不是简单加个“微信支付”按钮而是要对接省级医保平台。项目预留了payment模块只需补全步骤1新增医保支付实体类// src/main/java/com/hospital/entity/InsurancePayment.java Data TableName(insurance_payment) public class InsurancePayment { TableId(type IdType.AUTO) private Long id; private Long appointmentId; // 关联挂号单 private String insuranceNo; // 医保卡号脱敏存储前4后4 private BigDecimal amount; // 支付金额 private String transactionId; // 医保平台交易号 private Integer status; // 0-待支付, 1-支付中, 2-支付成功, 3-支付失败 private LocalDateTime createTime; }步骤2在AppointmentService.createAppointment()末尾加钩子// 原有逻辑... 创建挂号单 Appointment appointment new Appointment(); // ... 设置属性 save(appointment); // 新增如果患者绑定了医保卡自动生成医保支付单 if (patient.getInsuranceNo() ! null) { InsurancePayment payment new InsurancePayment(); payment.setAppointmentId(appointment.getId()); payment.setInsuranceNo(desensitize(patient.getInsuranceNo())); // 脱敏 payment.setAmount(calculateFee(appointment)); // 根据科室计算费用 payment.setStatus(0); insurancePaymentMapper.insert(payment); // 异步调用医保平台接口用 Async insurancePaymentService.asyncSubmitToPlatform(payment.getId()); }步骤3实现医保平台回调接口关键RestController RequestMapping(/api/insurance) public class InsuranceCallbackController { PostMapping(/callback) public ResponseEntityString handleCallback(RequestBody MapString, String params) { // 1. 验签医保平台会提供公钥 if (!verifySignature(params)) { return ResponseEntity.badRequest().body(SIGN_ERROR); } // 2. 更新支付状态 String transactionId params.get(transaction_id); String result params.get(result); // SUCCESS/FAILED insurancePaymentMapper.updateStatusByTransaction(transactionId, SUCCESS.equals(result) ? 2 : 3); // 3. 如果支付成功自动将挂号单 status 改为 1已确认 if (SUCCESS.equals(result)) { Long appointmentId insurancePaymentMapper.selectAppointmentIdByTransaction(transactionId); appointmentMapper.updateStatus(appointmentId, 1); } return ResponseEntity.ok(SUCCESS); } }注意医保回调地址必须是公网可访问的 HTTPS 地址本地开发用ngrok映射并在医保平台后台配置回调 URL。6.2 分时段候诊叫号用 Redis 实现轻量级队列替代复杂中间件传统叫号系统要用 RabbitMQ但本项目用 Redis List 就够了数据结构设计# 每个科室一个队列key 格式queue:dept:101 # 队列元素JSON 字符串 {appointmentId:123,patientName:张三,timeSlot:MORNING} LPUSH queue:dept:101 {appointmentId:123,patientName:张三,timeSlot:MORNING}叫号服务定时任务Component public class CallingService { Scheduled(fixedRate 5000) // 每5秒检查一次 public void callNextPatient() { // 1. 获取当前科室假设前台护士在 deptId101 的窗口 String key queue:dept:101; // 2. 从队列弹出第一个患者 String json redisTemplate.opsForList().rightPop(key, 0, TimeUnit.SECONDS); if (json null) return; // 3. 解析并更新挂号单状态 JSONObject obj JSON.parseObject(json); Long appointmentId obj.getLong(appointmentId); appointmentMapper.updateStatus(appointmentId, 2); // 已就诊 // 4. 推送叫号消息到小程序通过 WebSocket 或微信模板消息 wechatNotifyService.sendCallingMessage(obj.getString(patientName), 请到1号诊室); } }小程序端监听叫号// pages/waiting/waiting.js Page({ data: { currentNumber: }, onLoad() { // 1. 连接 WebSocket后端用 SpringBoot WebSocket this.socket wx.connectSocket({ url: wss://your-server.com/ws/calling }); // 2. 监听消息 this.socket.onMessage((res) { const data JSON.parse(res.data); if (data.type CALLING) { this.setData({ currentNumber: data.patientName }); wx.showToast({ title: 请${data.patientName}到1号诊室, icon: none }); } }); } });从那以后我每次接手医疗类项目都会先翻doctor_schedule表的建表语句——号源字段是否带FOR UPDATE锁、状态机是否覆盖所有临床场景、时间精度是否到毫秒。这些细节不会写在毕业论文里但决定着系统上线后是“平稳运行”还是“凌晨三点救火”。希望帮到你。本文还有配套的精品资源点击获取