ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Java医院预约挂号系统微信小程序源码实战:防超卖与并发扣减

Java医院预约挂号系统微信小程序源码实战:防超卖与并发扣减 简介这是一套基于Java与微信小程序的医院预约挂号系统源码面向计算机专业学生、课程设计或毕业设计开发者以及需要快速搭建医疗预约类项目的技术人员。系统采用SpringBoot MyBatisPlus MySQL Redis架构前台使用uni-app与Vue开发微信小程序后台分为管理员端与医生端涵盖用户管理、排班管理、预约记录、科室与疾病管理、医生管理、医院信息及公告管理等模块前台支持注册登录、预约挂号、核酸检测预约与记录查询、查看坐诊信息、管理就诊人及导航到院等功能。资源包共185个文件以113个vue组件、41个js脚本、10个scss样式为主另含json配置、md说明与字体图标等压缩包约517KB结构清晰便于二次开发。目前已有2377人学习下载适合作为完整项目参考帮助读者理解前后端分离与小程序端联调思路快速掌握医疗预约系统的业务实现与代码组织方式。1. 从一份 Java 医院预约挂号系统源码说起微信小程序端到底解决了什么医院挂号这件事线下窗口排队的体验已经被吐槽了十几年。真正让「Java医院预约挂号系统 微信小程序」这个组合在课程设计和中小型项目里反复出现的不是技术有多新而是它把三个刚需咬合得很紧患者要的是不用装 App、扫码即用医院要的是号源可控、不被黄牛刷爆开发者要的是一个能跑通「登录鉴权 → 号源查询 → 锁号下单 → 支付回调 → 取消退号」完整闭环的练手场景。微信小程序天然带 openid 体系省掉了自建账号密码那一套Java 后端又擅长处理并发扣减和事务两边一拼就是一个能讲清楚业务、又能压出技术深度的题目。这份标题指向的是一套可运行的工程压缩包通常包含 Java 后端Spring Boot 居多、小程序前端、以及一份 SQL 建表脚本。它适合三类人正在找课程设计或毕设题材的学生、想练手完整业务闭环的初中级 Java 开发者、以及需要快速搭一个预约类 MVP 的小团队。下面我不复述任何一份具体源码而是按这个方向最常见的落地路径把选型、建表、接口、并发、联调、踩坑一层层拆开让你拿到任何一份同类工程都能看懂、改得动、跑得起来。2. 技术选型与数据库设计号源表怎么建才扛得住并发2.1 后端为什么默认 Spring Boot MyBatis-Plus 这套组合拿到一份 Java 挂号系统先看它的依赖。绝大多数同类工程用的是 Spring Boot 2.x/3.x 打底持久层要么 MyBatis-Plus要么 JPA。选 MyBatis-Plus 的理由很实际挂号系统里「按科室、按日期、按医生、按上下午」这种多条件动态查询特别多XML 或 Wrapper 写起来比 JPA 的 Specification 直观改字段不用动实体映射。缓存层一般挂 Redis用来存 session、验证码、以及号源余量的热点计数。定时任务用 Spring Task 或 Quartz负责每天凌晨生成未来 N 天的排班号源。小程序端常见两种写法原生小程序或者 uniapp 开发微信小程序。原生胜在包体小、调用微信原生能力比如wx.login、wx.requestPayment没有中间层uniapp 胜在一套代码能兼顾多端。如果你只做微信小程序我一般建议原生少一层编译出问题好定位。这里不涉及任何跨端打包的支付差异讨论挂号场景的支付链路相对单一。2.2 号源表的核心字段与索引设计挂号系统最容易翻车的地方不在接口在建表。号源如果设计成「一个医生一天一条记录余量字段自减」高并发下必然超卖。正确做法是把号源拆成「排班schedule」和「号源明细slot」两层或者用「库存 流水」的思路。下面是一份我常用的最小表结构字段名可按你手上的工程调整。-- 科室表 CREATE TABLE department ( id BIGINT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(64) NOT NULL COMMENT 科室名, parent_id BIGINT DEFAULT 0 COMMENT 上级科室0为顶级, status TINYINT DEFAULT 1 COMMENT 1启用 0停用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 医生排班表一个医生 一个出诊时段 一条 CREATE TABLE doctor_schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL, dept_id BIGINT NOT NULL, work_date DATE NOT NULL COMMENT 出诊日期, period TINYINT NOT NULL COMMENT 1上午 2下午, total_count INT NOT NULL COMMENT 总号源, left_count INT NOT NULL COMMENT 剩余号源, fee DECIMAL(10,2) NOT NULL COMMENT 挂号费, version INT DEFAULT 0 COMMENT 乐观锁版本, UNIQUE KEY uk_doc_date_period (doctor_id, work_date, period), KEY idx_dept_date (dept_id, work_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; -- 挂号订单表 CREATE TABLE register_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 业务订单号, user_id BIGINT NOT NULL, schedule_id BIGINT NOT NULL, patient_name VARCHAR(32) NOT NULL, id_card VARCHAR(20) NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待支付 1已支付 2已取消 3已就诊, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id), KEY idx_schedule (schedule_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明doctor_schedule上的唯一索引uk_doc_date_period是防重复排班的最后一道闸业务层再校验一次双保险。left_count是热点字段扣减必须走原子操作。register_order的order_no用唯一索引兜底防止前端重复点击生成两笔订单。参数上total_count和left_count用 INT 足够别用 VARCHAR 存数字排序和比较都会出问题。2.3 号源扣减的三种写法与选择第一种直接UPDATE doctor_schedule SET left_count left_count - 1 WHERE id ? AND left_count 0靠数据库行锁保证原子性返回影响行数为 0 就是没抢到。这是最稳、最省事的写法中小流量完全够用。第二种乐观锁version字段适合读多写少、冲突不激烈的场景但挂号高峰期冲突率高重试次数会飙升。第三种Redis 预扣减 异步落库适合秒杀级流量但引入了一致性维护成本课程设计级别没必要。我一般先用第一种把闭环跑通压测发现数据库扛不住再上 Redis。别一上来就堆中间件出了问题你连是哪一层挂的都分不清。3. 微信小程序端登录鉴权与请求封装openid 怎么拿、token 怎么存3.1 小程序登录换 openid 的完整链路微信小程序的登录不是账号密码而是wx.login拿到临时code传给后端后端用code appid secret调微信接口换openid和session_key。这条链路里secret绝对不能放在小程序端必须留在 Java 后端。下面是小程序端的调用代码。// utils/auth.js function login() { return new Promise((resolve, reject) { wx.login({ success(res) { if (!res.code) { reject(new Error(获取code失败)); return; } // 把 code 发给自己的 Java 后端由后端去换 openid wx.request({ url: https://your-domain.com/api/auth/login, method: POST, data: { code: res.code }, success(r) { if (r.data.code 0) { // 后端返回自定义 token存本地 wx.setStorageSync(token, r.data.data.token); resolve(r.data.data); } else { reject(new Error(r.data.msg)); } }, fail: reject }); }, fail: reject }); }); } module.exports { login };逻辑说明wx.login的code只能用一次五分钟内有效所以拿到就立刻发后端不要缓存。后端换到openid后查用户表没有就自动注册一条然后签发自己的 tokenJWT 或 Redis session 都行。参数上appid和secret写在 Java 的配置文件里用环境变量注入别硬编码进仓库。3.2 请求封装把 token 和错误码统一收口小程序里如果每个页面都手写wx.requesttoken 过期、网络错误、业务错误码会散落各处后期改一处要翻十个文件。封装一层是必须的。// utils/request.js const BASE_URL https://your-domain.com; function request(options) { const token wx.getStorageSync(token); 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: token ? Bearer token : }, success(res) { const { statusCode, data } res; if (statusCode 401) { // token 失效清掉并重新登录 wx.removeStorageSync(token); reject(new Error(登录已过期)); return; } if (data.code 0) { resolve(data.data); } else { wx.showToast({ title: data.msg || 请求失败, icon: none }); reject(new Error(data.msg)); } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request };逻辑说明Authorization头统一带 token后端拦截器解析。statusCode 401单独处理因为这是鉴权失败和业务错误要区分开。业务错误码约定code 0为成功其余为失败msg直接给用户看。参数上BASE_URL抽成常量方便切测试和生产环境。这套封装是「微信小程序 请求封装」这个高频搜索词的标准答案抄过去改改就能用。3.3 token 有效期与缓存时间的设置token 存本地用wx.setStorageSync默认是永久存储但服务端应该给它设过期时间。常见做法是 JWT 设 7 天或者 Redis session 设 2 小时滑动过期。小程序端不需要自己判断过期交给后端返回 401 触发重登即可。如果你要缓存科室列表这类不常变的数据可以用wx.setStorageSync加一个时间戳字段读取时判断是否超过设定时长这就是「微信小程序设置缓存时间」的落地方式。4. 挂号核心接口实现锁号、下单、支付回调怎么串起来4.1 号源查询接口分页与余量展示查询接口要支持按科室、日期筛选返回每个排班的剩余号源。注意别把left_count为 0 的排班直接过滤掉前端要展示「已约满」状态过滤了用户会以为没排班。GetMapping(/schedules) public ResultListScheduleVO listSchedules(RequestParam Long deptId, RequestParam DateTimeFormat(pattern yyyy-MM-dd) Date date) { // 按科室和日期查排班带上医生信息 ListScheduleVO list scheduleMapper.selectByDeptAndDate(deptId, date); // left_count 为 0 的保留前端置灰 return Result.ok(list); }逻辑说明ScheduleVO里除了号源字段还要带医生姓名、职称、挂号费减少前端二次请求。参数上日期用DateTimeFormat接收yyyy-MM-dd格式别用时间戳前端传起来麻烦。这个接口是读多写少可以加 Redis 缓存key 用schedule:deptId:date过期时间设 60 秒避免排班变更后长时间不刷新。4.2 锁号下单事务边界与防重下单是整个系统最关键的接口必须在一个事务里完成「扣号源 建订单」。扣号源用原子 UPDATE影响行数为 0 直接抛异常回滚。Transactional(rollbackFor Exception.class) public OrderVO createOrder(Long userId, Long scheduleId, String patientName, String idCard) { // 1. 原子扣减号源left_count 0 才扣 int affected scheduleMapper.decreaseLeftCount(scheduleId); if (affected 0) { throw new BizException(号源已约满); } // 2. 查排班信息校验是否已过期 DoctorSchedule schedule scheduleMapper.selectById(scheduleId); if (schedule null || schedule.getWorkDate().before(new Date())) { throw new BizException(该排班已失效); } // 3. 同一用户同一排班不能重复挂号 Long cnt orderMapper.countByUserAndSchedule(userId, scheduleId); if (cnt 0) { throw new BizException(您已挂过该号); } // 4. 建订单 RegisterOrder order new RegisterOrder(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setScheduleId(scheduleId); order.setPatientName(patientName); order.setIdCard(idCard); order.setStatus(0); orderMapper.insert(order); return toVO(order); }逻辑说明decreaseLeftCount对应的 SQL 是UPDATE doctor_schedule SET left_count left_count - 1 WHERE id #{id} AND left_count 0这是防超卖的核心。第 3 步的重复校验放在扣减之后是因为扣减是原子操作先扣再查能减少并发下的重复判断窗口。参数上Transactional的rollbackFor要写Exception.class否则受检异常不回滚这是血泪经验。generateOrderNo建议用「日期 用户ID后四位 随机数」别用纯自增暴露业务量。4.3 支付回调幂等处理不能省微信支付回调会重复推送同一个订单可能收到多次通知。回调接口必须做幂等先查订单状态已支付就直接返回成功不再处理。PostMapping(/pay/notify) public String payNotify(RequestBody String body) { // 1. 验签省略具体验签代码按微信官方文档实现 // 2. 解析出 out_trade_no String orderNo parseOrderNo(body); RegisterOrder order orderMapper.selectByOrderNo(orderNo); if (order null) { return FAIL; } // 3. 幂等已支付直接返回成功 if (order.getStatus() 1) { return SUCCESS; } // 4. 更新订单状态 orderMapper.updateStatus(orderNo, 1); return SUCCESS; }逻辑说明回调接口返回SUCCESS微信才停止重推返回其他会一直重试。第 3 步的幂等判断是必须的否则用户会被重复扣款或订单状态错乱。参数上out_trade_no就是我们自己的order_no对得上才能更新。验签部分按微信官方文档实现别自己造轮子。5. 联调与部署避坑那些让你排查到凌晨的坑5.1 小程序请求域名必须备案且配 HTTPS现象本地wx.request调http://localhost:8080能通真机预览报「不在以下 request 合法域名列表中」。原因微信小程序正式环境只允许 HTTPS且域名要在小程序后台配置白名单。解决开发阶段在开发者工具里勾选「不校验合法域名」真机调试和上线必须用备案域名 HTTPS 证书。别想着绕过这是平台硬规则。5.2 号源超卖并发压测才暴露现象单机测试一切正常用 JMeter 并发 100 请求同一排班left_count变成负数。原因扣减写成了「先查再减」两条 SQL 之间有并发窗口。解决改成UPDATE ... WHERE left_count 0的原子写法或者加乐观锁重试。压测是发现这个问题的唯一可靠手段别等上线后被用户投诉才发现。5.3 订单重复前端防抖不等于后端防重现象用户手快点了两次「确认挂号」生成两笔订单扣了两次号源。原因只在前端做了按钮禁用网络延迟下第二次请求还是发出去了。解决后端用「用户ID 排班ID」做唯一约束或查询校验数据库层加唯一索引兜底。前端防抖是体验优化后端防重才是数据安全。5.4 时间字段时区错乱现象后端存的work_date是2024-06-01小程序显示成2024-05-31。原因数据库时区、JVM 时区、小程序端new Date()解析格式不一致。解决统一用yyyy-MM-dd字符串传输日期后端DateTimeFormat接收数据库连接串加serverTimezoneAsia/Shanghai。日期字段别用Date直接 JSON 序列化会带时区偏移。5.5 微信开发者工具缓存导致改了代码不生效现象改了小程序代码编译后页面还是旧的。原因开发者工具的缓存没清或者wx.setStorageSync存的旧数据干扰。解决工具栏「清缓存」→「全部清除」重新编译。如果还不行检查是不是app.json里页面路径写错加载了另一个页面。这个坑不涉及技术深度但能浪费你半小时。6. 把挂号系统做成可演示的作品压测脚本与一个提效技巧跑通闭环只是及格线要让这套 Java 医院预约挂号系统在答辩或面试里拿得出手你得能证明它扛得住并发。我一般会写一个简单的压测脚本用 JMeter 或 Python 的locust都行重点压「锁号下单」这个接口。下面是一个最小可用的 Python 压测片段模拟 200 个用户抢同一个排班。# stress_test.py 依赖: pip install requests import threading import requests URL https://your-domain.com/api/order/create TOKEN 替换成真实token SCHEDULE_ID 1 success 0 fail 0 lock threading.Lock() def grab(): global success, fail headers {Authorization: Bearer TOKEN} data {scheduleId: SCHEDULE_ID, patientName: 测试, idCard: 110101199001011234} r requests.post(URL, jsondata, headersheaders) with lock: if r.json().get(code) 0: success 1 else: fail 1 threads [threading.Thread(targetgrab) for _ in range(200)] for t in threads: t.start() for t in threads: t.join() print(f成功 {success}失败 {fail})逻辑说明200 个线程同时打同一个排班如果left_count初始是 50最终成功数应该正好是 50多一个都说明超卖。参数上TOKEN要换成真实登录后的 tokenSCHEDULE_ID换成压测目标排班。跑完去数据库查left_count和订单数两者对得上才算过关。这个脚本不复杂但能让你在演示时直接甩出「200 并发零超卖」的结论比空口说「我做了并发控制」有说服力得多。一个提效技巧把号源扣减和订单创建做成「先扣库存、再落订单、失败回补」的三段式配合日志打点。每次压测后看日志里「扣减成功但订单失败」的条数如果大于 0说明事务边界或异常处理有问题。我吃过这个亏早期版本回补逻辑写在 catch 里但没加事务回补失败导致号源凭空消失排查了一整晚。后来养成习惯凡是涉及库存增减日志必须记录「操作前值、操作后值、订单号」出问题直接对账。这套东西值不值得做如果你只是想交个课程设计跑通闭环就够了如果你想拿它当面试项目讲把并发扣减、幂等回调、压测数据这三块吃透面试官问「怎么防超卖」你能从 SQL 原子性讲到事务隔离级别基本就稳了。我自己的习惯是每做一个预约类系统先把压测脚本写好再写业务代码因为你知道哪里会被打写的时候就会下意识避开那些坑。希望帮到你。本文还有配套的精品资源点击获取
返回列表