
简介面向医院预约挂号场景的Java微信小程序完整源码包后台采用SpringBootMyBatisPlusMySQLRedis前台基于uni-appVue覆盖用户、管理员、医生三类角色。前台支持注册登录、预约记录、核酸检测预约、医院公告、坐诊信息、就诊人管理、导航到院后台包含用户、排班、预约、科室、疾病、职位、医生、医院信息、公告等管理模块医生端可设置排班并处理预约订单。资源共185个文件以113个vue页面组件、41个js逻辑文件、10个scss样式文件为主另有少量配置文件、说明文档及图片压缩包仅517KB便于快速部署与学习。目前已有2378人学习对想掌握微信小程序Java全栈开发、理解医院预约业务建模的开发者是一份结构清晰、可运行的实操参考。1. 拿到这个 Java 医院预约挂号系统 zip先别急着解压导库很多人第一次拿到这个压缩包习惯是解压、丢进 IDEA、等 Maven 下完依赖就跑。但如果你对照过真实医院的上线流程会发现一个反直觉的结论这个项目最值钱的地方不在增删改查而在号源并发控制。预约挂号系统跟普通电商订单不同一个医生的号源是有限的上午放 30 个号就真的只能卖出 30 个多卖一个就得出医疗事故。这个 zip 里装着的是 Java 后端 微信小程序前端的完整代码能让你跑通从「患者打开小程序选科室」到「医生后台看到预约列表」的整条链路适合中小医院、私立诊所做预约信息化也适合拿来当作 Spring Boot 和小程序联调的项目练手。但要让它真能在门诊高峰扛住几十人同时抢号你得把后端那套状态机和幂等逻辑彻底吃透后面几章我会把这些地方一个个拆开讲。2. 从科室排班到号源锁定Java 后端必须处理的四类核心业务2.1 预约挂号的状态机号源不是库存是带约束的库存医院预约挂号的业务模型跟一般商品库存有本质区别。商品库存卖完了可以补货号源不行一个医生半天门诊就那么多时间还得预留复诊和急诊名额。所以整个系统的核心不是「加减库存」而是一个严格的状态机号源的状态从「可预约」到「锁定」再到「已支付」或「取消释放」。常见做法是后端用一张号源表schedule_slot记录每个医生在某个时间段的剩余号数另有一张预约订单表appointment_order记录患者与号源的绑定。你在 Java 代码里要做的是把这几个状态转换写清楚患者提交预约时订单先进入「锁定」状态同时扣减号源余量限定时间内未支付定时任务把订单改为「已取消」同时把号源余量加回来。这里最容易翻车的是扣减余量那步如果直接用UPDATE schedule_slot SET remain remain - 1 WHERE id ? AND remain 0在高并发下会有一堆线程同时读到 remain1然后全都执行扣减最后变负数。我的习惯是把这个 SQL 当成一个原子操作来调并且用受影响行数判断是否扣成功而不是先 SELECT 再 UPDATE。2.2 后端接口设计用 RESTful 把挂号流程暴露给小程序整个系统的接口设计要围绕小程序端的操作路径来。患者打开小程序先看到科室列表点进科室看医生再选日期和号源紧接着创建预约订单最后支付。对应到 Spring Boot 后端常见做法是暴露这几个 RESTful 接口接口路径方法作用关键参数/api/dept/listGET科室列表parentId, pageNum, pageSize/api/doctor/listGET某科室下的医生列表deptId/api/schedule/availableGET某医生可预约的号源doctorId, date/api/appointment/createPOST创建预约订单doctorId, slotId, patientId/api/appointment/payPOST模拟支付或对接微信支付orderId, payType/api/appointment/cancelPOST取消预约orderId接口设计上要注意三点。第一创建预约的接口必须是幂等的小程序端有可能因为网络超时重复提交同一个请求你需要在后端用「患者 ID 号源 ID 当天日期」做唯一约束或者让前端生成一个 requestId 带上。第二查询可用号源时不要直接返回remain字段让前端自己去算而是返回每个时段的总号数和已约数这样前端可以自己展示「约满」和「剩余 X 号」两种状态。第三支付回调接口要独立于业务接口预约状态变更必须放在事务里并且要支持回调幂等。2.3 数据库表结构五张表怎么互相约束我在看类似项目源码时第一件事是打开 SQL 脚本看表设计。预约挂号系统的表设计一般围绕五个实体展开科室dept、医生doctor、排班schedule、号源schedule_slot、预约订单appointment_order。如果你拿到的 zip 里用的是 MyBatis Plus通常会有一张sys_user表来区分患者、医生和管理员三种角色这里就不单独展开权限了。关键表结构可以这样落CREATE TABLE schedule_slot ( id BIGINT PRIMARY KEY AUTO_INCREMENT, schedule_id BIGINT NOT NULL COMMENT 排班ID, start_time DATETIME NOT NULL, end_time DATETIME NOT NULL, total_count INT DEFAULT 0, remain_count INT DEFAULT 0, version INT DEFAULT 0, UNIQUE KEY uk_schedule_time (schedule_id, start_time) ) COMMENT 号源表; CREATE TABLE appointment_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, patient_id BIGINT NOT NULL, doctor_id BIGINT NOT NULL, slot_id BIGINT NOT NULL, status TINYINT NOT NULL COMMENT 0锁定 1已支付 2已取消 3已完成, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_patient_slot (patient_id, slot_id, status) ) COMMENT 预约订单表;这里加了version字段就是为乐观锁做准备的。你可以在更新号源时使用UPDATE schedule_slot SET remain_count remain_count - 1, version version 1 WHERE id ? AND remain_count 0 AND version ?如果更新影响行数为 0说明号源已经被抢空了这时直接返回「号源不足」。我在实际项目里更推荐把这种更新写在 Service 层用Transactional包裹避免在 DAO 层做过多判断。另外订单表里的uk_patient_slot唯一索引是为了防止同一个患者对同一个号源重复下单如果违反唯一约束要让前端提示「你已预约过该时段」而不是笼统报错。3. 小程序端从科室列表到支付预约的完整交互实现3.1 页面结构四层科室列表、医生列表、排班详情、预约确认微信小程序端的页面结构可以直接对应后端接口。拿到的 zip 里一般会有pages目录核心四个页面跑不掉首页科室列表、医生列表页、排班详情页、预约确认页。这些页面的跳转关系就像一条漏斗患者每次点击都在缩小选择范围越到后面信息越具体但操作成本也越高。所以小程序端的设计重点不是动画和视觉而是把「拥挤程度」和「剩余号源」这种关键信息在列表页就展示出来减少无效点击。实际开发中我会把首页做成科室卡片列表每个卡片上直接标注「今日可约」还是「明日」让患者不用点进去就知道有没有号。医生列表页要展示医生的职称、擅长领域和最近的排班日期按日期分成 tab点击某个日期再进入该日期下的号源明细。排班详情页就是一天的时间轴每个时段显示剩余号数约满的置灰。最后的预约确认页要展示患者姓名、科室、医生、时间、费用并且放一个「提交预约」按钮同时触发一个 15 分钟的倒计时提醒患者及时支付。3.2 封装 wx.request统一带上 token处理 401 和业务错误小程序端向后端发请求不能像 axios 那样直接拦截器但可以用一个公共的 request 函数来封装。这个函数要做三件事把baseUrl拼上具体路径、在 header 里塞token、统一处理 HTTP 状态码和业务状态码。// utils/request.js const BASE_URL https://your-domain.com/api; // 上线时改成正式域名 function request(path, method, data) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method: method, data: data, header: { Content-Type: application/json, Authorization: wx.getStorageSync(token) || }, timeout: 10000, success(res) { if (res.statusCode 401) { wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); reject(new Error(未登录或登录过期)); } else if (res.data res.data.code 0) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(new Error(res.data.msg)); } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request };这里timeout设成 10000 毫秒是很有讲究的。医院场景下患者经常在移动网络下操作网络延迟可能到 2 到 3 秒但后端创建订单要做事务、扣号源再返回结果整个链条在 5 秒内通常能完成。如果你把 timeout 设太短比如 5000容易出现「前端超时报错后端订单已创建」的脏状态设太长又会让患者以为卡死了。10 秒是一个相对稳妥的值配合后端幂等设计超时后允许患者重新提交不会产生重复订单。业务状态码code 0是常见约定如果拿到的源码里用的不是这个你统一改成项目里的约定就行。3.3 号源余量展示与倒计时锁号的前端逻辑排班详情页的号源展示是前端最需要细心的地方。后端返回的每个时段信息里要有totalCount和remainCount前端计算剩余比例来决定是否置灰。更细的做法是当remainCount 0时按钮显示「约满」当remainCount 5时显示「仅剩 X 号」并给按钮加一个红色徽标。这些交互不涉及复杂算法但能直接感知到系统「活」了。倒计时锁号是预约流程里的一个关键体验。当患者提交预约成功后后端把号源锁定并给订单设置一个过期时间通常 15 分钟。前端拿到订单创建时间后在预约确认页显示一个倒计时。我会用setInterval每秒更新剩余秒数同时监听页面的onUnload清掉定时器避免内存泄漏。// pages/confirm/confirm.js Page({ data: { expireText: , orderId: , locked: true }, startCountdown(expireTime) { const endTime new Date(expireTime).getTime(); this.timer setInterval(() { const remain endTime - Date.now(); if (remain 0) { clearInterval(this.timer); this.setData({ expired: true, expireText: 订单已超时请重新预约 }); return; } const min Math.floor(remain / 60000); const sec Math.floor((remain % 60000) / 1000); this.setData({ expireText: ${min}:${sec 10 ? 0 sec : sec} }); }, 1000); }, onUnload() { if (this.timer) clearInterval(this.timer); } });这里的注意点是倒计时结束时前端只能提示「订单已超时」不能直接调用取消接口。因为后端定时任务会统一处理超时释放号源前端主动调取消反而可能与定时任务产生竞争导致号源被重复释放。你只需要在倒计时结束后把按钮置灰让患者走重新预约的流程就行。4. 把 zip 变成能联调的系统导入、配置、启动三步走4.1 后端IDEA 导入 Spring Boot 项目并初始化数据库拿到 zip 包后首先确认目录结构。典型的 Maven 工程会有pom.xml、src/main/java、src/main/resources。用 IDEA 的「Open」直接选择解压后的目录等待 Maven 下载依赖。如果依赖下载很慢可以在settings.xml里配置阿里云镜像这是国内开发者绕不开的一步。启动类一般在com.xxx.HospitalApplication直接右键运行即可。但运行之前必须做一件事导入数据库脚本。在resources/db目录下通常能找到schema.sql或init.sql。如果你用的 MySQL先建一个空库然后执行脚本。常见坑是字符集脚本里如果没写DEFAULT CHARSETutf8mb4导入后会出现中文乱码以后查科室名字全是问号。我一般会在建库时就指定CREATE DATABASE hospital_app DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;导入后打开application.yml把spring.datasource.url改成你本机的数据库地址和端口用户名密码也一并改掉。如果你的 MySQL 是 8.x记得在pom.xml里确认mysql-connector-java的版本是 8.0 以上否则驱动类都加载不对。4.2 小程序端开发者工具导入与本地设置关闭域名校验小程序端的导入比较简单。打开微信开发者工具选择「导入项目」项目目录选中 zip 解压出来的miniprogram或wechat-app文件夹AppID 可以先用测试号。然后重点来了开发者工具默认要求所有请求域名必须是已备案的 HTTPS 域名并且要在小程序后台配置白名单。本地联调阶段你肯定不想先去配服务器域名所以在工具里勾选「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」这个选项在右上角的「详情」-「本地设置」里。不过要清楚这只是开发阶段的救命稻草真机预览时如果不勾选请求照样会被拦。所以联调通过后你要把后端接口部署到一台有备案域名的服务器上并在微信公众平台把域名加到「request 合法域名」。这个步骤很多人会忘导致「开发工具里一切正常手机一测全挂」。4.3 联调关键参数接口地址、超时与请求头对齐联调时最容易暴露的问题是前后端约定不一致。前端在request.js里拼的BASE_URL必须和后端application.yml里的server.port以及context-path完全对得上。我自己踩过一个大坑后端配置了server.servlet.context-path: /api但前端BASE_URL写的是https://xxx.com导致请求全部 404。这个问题排查起来很费劲因为浏览器或开发者工具里看的 URL 是对的实际上少了一层路径。另一个对齐点是请求头。后端如果用 Spring Security 或 JWT 拦截器会从Authorization头里取 token。前端封装的 request 里已经带了但你得确认后端的拦截器是读Authorization还是X-Token。类似项目里常见两种写法一种用Authorization: Bearer token另一种直接token: value。建议打开后端代码搜索getHeader看看它读的是什么名称再回来改前端。最后确认时区MySQL 的serverTimezone如果设成UTC前端传过来的是北京时间日期排班会偏移 8 小时调度医生的时候看到的时间全错位。我习惯在 JDBC URL 里加serverTimezoneAsia/Shanghai。5. 避坑指南预约挂号系统最常见的 5 个翻车现场5.1 号源超卖两个患者同时拿到同一个号这是一个经典问题。现象两个患者几乎同时提交预约后端都返回「预约成功」但数据库里同一个号源被两个订单绑定最后必须人工介入处理。原因代码在 Service 层先查询remainCount 0再执行扣减两个请求都通过了判断更新时也没有带条件。解决把扣减 SQL 改为原子更新并且以受影响行数作为成功标志而不是靠查询结果判断。Transactional public boolean lockSlot(Long slotId) { int rows slotMapper.reduceRemainCount(slotId); return rows 0; }对应的 XML 里写update idreduceRemainCount UPDATE schedule_slot SET remain_count remain_count - 1 WHERE id #{slotId} AND remain_count gt; 0 /update如果rows 0直接抛业务异常前端提示「号源已被抢完」。同时配合订单表唯一索引双保险。注意remain_count 0在 XML 里要转义成gt;不然会解析报错。5.2 微信登录 session 失效code2session 请求报 40029现象小程序端调用wx.login后拿 code 请求后端后端去微信接口换 openid 时返回40029或40163。原因绝大多数是appid和secret配置错了或者小程序在开发者工具里用的测试号而后端配置的是正式 AppID。另外wx.login的 code 只能用一次如果前端代码重复调用同一个 code第二次就会失效。解决检查application.yml里的wx.appid和wx.secret是否与开发者工具里的一致如果联调用测试号需要在工具里点击「清除缓存」后重新编译让代码重新生成。更深层的问题是后端换完 openid 后要把自定义登录态 token 返回给前端不能直接把 openid 给前端暴露防止接口被恶意刷。5.3 小程序真机请求 https 失败报「不在以下 request 合法域名列表中」现象开发者工具里勾了不校验域名一切正常手机真机预览时所有请求都失败console 里提示域名不合法。原因微信小程序有严格的域名白名单机制而且要求必须是 HTTPS不能是 IP 也不能带端口。解决把后端部署到带备案域名的服务器上配好 HTTPS 证书然后在微信公众平台「开发管理」-「服务器域名」里添加 request 合法域名。注意这里还有一层如果后端用了二级域名比如api.hospital.com那白名单里只能加这个二级域名不能加hospital.com就指望通配生效。5.4 排班日期跨月导致号源查询不到现象月底最后一天患者选择下月 1 号的排班页面显示「暂无号源」但数据库里明明有数据。原因前端传给后端的日期格式是2025-08-01后端解析时用了SimpleDateFormat(yyyy-MM-dd)本身应该没问题。问题往往出在时区或字符串比较上。比如 MySQL 的schedule_date字段是datetime后端用BETWEEN 2025-08-01 00:00:00 AND 2025-08-01 23:59:59.999查询但如果数据库连接时区是 UTC传入的字符串被当成 UTC 时间本地一转换就偏移到前一天。解决确保 JDBC URL 有serverTimezoneAsia/Shanghai并且后端在接收前端日期参数时统一用DateTimeFormat(pattern yyyy-MM-dd)避免依赖数据库隐式转换。5.5 支付回调重复通知导致订单重复进入已完成状态现象对接微信支付后回调接口被微信多次调用后端没有做幂等结果同一订单被重复更新流水表里出现两条支付成功记录。原因微信支付回调会有重试机制直到你的接口返回成功标识。如果你的回调处理逻辑没有判断「该订单是否已是支付成功」就直接改状态、加流水就会重复落账。解决在回调入口加一个基于orderNo的分布式锁或数据库唯一索引并且回调处理里先查订单状态只有status 锁定的才允许改为已支付。另外回调接口返回给微信的报文必须是微信规定的格式不能返回自定义的 JSON否则微信会一直重试。6. 让系统更抗打分布式锁、消息队列与定时任务的取舍当你把踩坑都填平之后系统在同机联调环境已经能稳定跑但这只是及格线。门诊高峰期并发量一旦上来你会遇到新的瓶颈扣减号源的UPDATE语句在高并发下虽然不会超卖但平均响应时间可能飙升到几百毫秒大量未支付订单需要定时清理如果定时任务跑得过于频繁会给数据库带来额外压力。这三个问题分别有相对成熟的解法但要根据团队规模来决定做到哪一步。第一分布式锁。如果你只有单实例部署用 MySQL 的乐观锁就够了前面已经讲过。但如果后端拆成多个实例或者准备做负载均衡version乐观锁在某些极端情况下还是不够——比如两个实例同时对同一个号源发起更新乐观锁会让其中一个失败然后重试但重试会占用线程池。这种场景可以用 Redis 的SET NX EX实现一个简单的分布式锁在扣减号源前先锁号源 ID操作完再释放。这是成本最低的升级路径不建议一上来就用 Redisson因为引入额外组件会增大运维负担。第二消息队列。预约下单和支付成功通知这两个动作天然适合用 MQ 做解耦。比如创建订单时先写订单表再发一条order_locked消息由消费端去扣减号源。这样做的好处是数据库写压力被削峰坏处是引入了消息可靠性问题消费失败怎么办对于中小系统我更倾向于直接在一个事务里完成下单和扣号源不做异步因为 MySQL 单库单表在几千 QPS 下完全撑得住。等到日订单量过万再上 MQ 也来得及。第三定时任务关单。把超时未支付的订单批量关闭并释放号源这是必须要有的。常见做法是Scheduled(cron 0 */5 * * * ?)每 5 分钟扫描一次但要注意全表扫描的代价。优化方式是加一个索引覆盖status和create_time每次只查status 0 AND create_time NOW() - INTERVAL 15 MINUTE的前 100 条处理完再查下一批。我见过最惨痛的教训是定时任务没加锁两个实例同时跑同一批订单被重复取消结果号源被释放了两次患者支付成功却没号。所以无论用不用分布式任务调度平台任务执行前必须拿一个全局锁。最后说一个我自己的习惯拿到别人的项目源码我不会先去看业务代码而是先画一遍状态图和数据流转图。预约挂号这个场景特别容易让人沉浸在增删改查的舒适区里忘了医疗系统对一致性的要求。等到把状态图跟代码一一对应上再动手改并发和异常处理心里才有底。这套方式不仅对这个小程序项目管用换成任何一个涉及资源抢购的系统都能复用。希望帮到你。本文还有配套的精品资源点击获取