Java+微信小程序宠物医院预约源码:并发扣减与状态流转实战
发布时间:2026/9/25 6:01:09来源:尧图网络
简介这是一套面向计算机相关专业在校学生与教师的宠物医院预约微信小程序项目源码采用Java后端开发配套完整数据库脚本可作为课程设计、毕业设计、期末大作业或项目初期立项演示的参考方案。资源包共49个文件以35个Java源文件为核心业务实现辅以8个XML配置、1个YML与1个properties文件完成框架与参数设置另含1个SQL建库脚本、1个HTML页面及1个说明文档整体约95KB结构紧凑便于快速导入与二次开发。项目围绕宠物医院预约场景涵盖用户预约、信息管理等典型模块代码经验证可稳定运行适合入门进阶或在此基础上DIY其他功能。目前已有170人学习关注对于需要完整赛题方案、排错思路与目录结构参考的同学具备较高的借鉴与复用价值。1. 从一份宠物医院预约小程序源码说起Java 后端 微信小程序到底能跑出什么宠物医院这个场景看着小众实际业务链路一点都不简单宠物档案、医生排班、时段库存、预约锁定、到店核销、取消退号每一环都能把没做过的人绕进去。很多同学做课程设计时第一反应是找个现成模板改改结果打开一看只有几个静态页面数据库就一张 user 表连“同一个时段不能被两个人同时约上”这种最基本的问题都没处理。这份「基于 Java 开发的宠物医院预约微信小程序源码 数据库」之所以值得拿出来讲是因为它把 Java 后端、微信小程序前端、关系型数据库这三块串成了一条完整可运行的链路正好覆盖了课程设计里最容易被扣分的几个点并发预约、状态流转、数据一致性。如果你正在找 Java 课程设计案例源码或者想拿一个真实业务把 Java 基础、数据库增删改查、微信小程序项目实例这几样东西练一遍这个方向比外卖、图书管理那种烂大街的题目更能写出东西。下面我按“先跑起来、再改明白、最后避坑”的顺序把整套方案拆开讲清楚。2. 技术选型与整体架构为什么是 Spring Boot 小程序原生 MySQL2.1 后端为什么选 Spring Boot 而不是裸 Servlet课程设计里最常见的翻车是用纯 Servlet JSP 硬写写到后面接口一多路由、参数解析、事务全乱套。Spring Boot 的价值在于它把三件麻烦事一次性解决了内嵌 Tomcat 让你不用配 web.xmlstarter 依赖让你不用手动管 jar 冲突Transactional让你在预约扣库存时能一行注解搞定事务。对于宠物医院预约这种“读多写少、但写操作必须准”的场景事务边界清晰比性能重要得多。我一般会这样搭后端骨架pom.xml里核心依赖就这几个!-- Spring Boot Web提供 REST 接口和内嵌容器 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- MyBatis-Plus省掉大量单表 CRUD 的 XML -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- MySQL 驱动 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency逻辑说明spring-boot-starter-web负责把 Controller 暴露成 HTTP 接口小程序端用wx.request直接调MyBatis-Plus 在单表增删改查上能省掉 70% 的样板代码但涉及“查某医生某天剩余号源”这种多条件统计还是建议手写 SQL别硬套 Wrapper。参数上要注意 MySQL 驱动 8.x 必须配serverTimezone否则连接直接报时区错误这是新手第一个卡点。2.2 小程序端为什么用原生而不是 uniapp热词里“uniapp 开发微信小程序 vs android/ios/鸿蒙”讨论很多但课程设计这个场景我建议老老实实用微信小程序原生。原因很实际原生没有编译层报错信息直接对应到你的代码行调试成本最低而 uniapp 多一层编译遇到setData性能问题或者原生组件层级问题时新手根本分不清是框架的锅还是自己的锅。宠物医院预约的界面复杂度不高——首页、医生列表、预约表单、我的预约四个页面足够原生完全扛得住。小程序端目录结构建议这样组织pages/ index/ 首页展示科室和推荐医生 doctor/ 医生列表与详情 booking/ 预约表单选日期时段 myorder/ 我的预约含取消和核销码 utils/ request.js 统一封装 wx.request带 token 和错误处理 config.js 后端 baseUrl 集中管理request.js里最关键的是把 token 和错误码统一处理避免每个页面都写一遍wx.request的 success/fail 回调// utils/request.js const BASE_URL http://localhost:8080/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, // 登录后把 token 存进 storage每次请求带上 Authorization: wx.getStorageSync(token) || }, success: (res) { if (res.statusCode 200 res.data.code 0) { resolve(res.data.data); } else if (res.data.code 401) { // token 失效跳登录 wx.navigateTo({ url: /pages/login/login }); reject(res.data); } else { wx.showToast({ title: res.data.msg || 请求失败, icon: none }); reject(res.data); } }, fail: reject }); }); } module.exports { request };逻辑说明用 Promise 包一层页面里就能写await request({url:/doctor/list})比回调地狱清爽得多。参数上BASE_URL在开发阶段指向本机真机调试时要换成局域网 IP否则手机访问不到 localhost这是第二个高频卡点。2.3 数据库表设计预约场景的核心是“时段库存”数据库这块很多人栽在表设计上。宠物医院预约的本质是“医生 日期 时段”三个维度的库存扣减所以核心表至少要有user用户、pet宠物档案、doctor医生、schedule排班时段含剩余号源、appointment预约记录。其中schedule表是灵魂它把“某医生某天某时段有多少号”这件事物化成一行数据扣减时直接对这一行加锁即可。CREATE TABLE schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL COMMENT 医生ID, work_date DATE NOT NULL COMMENT 出诊日期, time_slot VARCHAR(20) NOT NULL COMMENT 时段如 09:00-10:00, total_num INT NOT NULL DEFAULT 0 COMMENT 总号源, left_num INT NOT NULL DEFAULT 0 COMMENT 剩余号源, version INT NOT NULL DEFAULT 0 COMMENT 乐观锁版本号, UNIQUE KEY uk_doctor_date_slot (doctor_id, work_date, time_slot) );逻辑说明uk_doctor_date_slot唯一索引保证同一医生同一天同一时段只有一行避免排班重复插入left_num是扣减目标version字段为乐观锁预留。参数上left_num绝不能允许为负扣减 SQL 里必须带left_num 0条件这是防超卖的最后一道防线。3. 把项目在本地跑起来从数据库导入到小程序联调3.1 数据库导入与后端启动的最小步骤拿到源码压缩包后第一步不是急着点运行而是先把数据库还原。常见做法是找到.sql文件用命令行或图形工具导入。我习惯用命令行因为报错信息最清楚# 1. 登录 MySQL mysql -u root -p # 2. 创建数据库字符集必须用 utf8mb4否则宠物昵称里的 emoji 会乱码 CREATE DATABASE pet_hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 3. 退出后用 source 导入 sql 文件 exit mysql -u root -p pet_hospital /path/to/pet_hospital.sql # 4. 验证表是否导入成功 mysql -u root -p -e USE pet_hospital; SHOW TABLES;逻辑说明utf8mb4而不是utf8是因为 MySQL 的utf8实际只支持 3 字节存不了 emoji 和部分生僻字宠物名字里带个表情就炸。导入后SHOW TABLES应该能看到 user、pet、doctor、schedule、appointment 等表。参数上如果 sql 文件里带了CREATE DATABASE语句第二步可以跳过但字符集仍要确认。后端启动前改application.yml里的数据库连接spring: datasource: url: jdbc:mysql://localhost:3306/pet_hospital?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver逻辑说明serverTimezoneAsia/Shanghai必须加否则 MySQL 8 会报The server time zone value is unrecognized。characterEncodingutf8配合库的 utf8mb4保证中文不乱码。改完直接mvn spring-boot:run或 IDE 里跑主类看到Started Application就说明后端起来了。3.2 小程序端配置与真机联调小程序端第一件事是改config.js里的 baseUrl。开发者工具里可以用localhost但真机预览必须换成电脑的局域网 IP比如http://192.168.1.100:8080/api。同时要在微信开发者工具的「详情 - 本地设置」里勾选「不校验合法域名」否则请求会被拦截。// utils/config.js module.exports { // 开发者工具用 localhost真机调试改成局域网 IP baseUrl: http://192.168.1.100:8080/api, // 预约提前天数限制 maxAdvanceDays: 7 };逻辑说明maxAdvanceDays控制用户最多能约几天后的号业务上一般设 7 天避免排班还没排就被约满。参数上这个值要和后端排班生成逻辑保持一致否则前端能选但后端没号体验直接崩。联调顺序建议先测登录接口拿到 token再测医生列表最后测预约提交。每步都用开发者工具的 Network 面板看请求和响应比盲猜快得多。3.3 预约核心接口的实现与事务控制预约提交是整个项目最不能出错的地方。用户点“确认预约”后端要做四件事校验时段是否还有号、扣减left_num、插入appointment记录、返回结果。这四步必须在一个事务里否则扣了号没插记录或者插了记录没扣号都是脏数据。Service public class AppointmentService { Autowired private ScheduleMapper scheduleMapper; Autowired private AppointmentMapper appointmentMapper; Transactional(rollbackFor Exception.class) public void book(Long userId, Long scheduleId, Long petId) { // 1. 带条件扣减left_num 0 保证不会扣成负数 int affected scheduleMapper.reduceStock(scheduleId); if (affected 0) { throw new BizException(该时段号源已约满); } // 2. 插入预约记录 Appointment appt new Appointment(); appt.setUserId(userId); appt.setScheduleId(scheduleId); appt.setPetId(petId); appt.setStatus(1); // 1已预约 appointmentMapper.insert(appt); } }对应的 Mapper SQLupdate idreduceStock UPDATE schedule SET left_num left_num - 1, version version 1 WHERE id #{scheduleId} AND left_num 0 /update逻辑说明reduceStock用一条原子 UPDATE 完成“判断 扣减”WHERE left_num 0是关键它让并发请求里只有一个能成功扣减其余返回 0 行受影响直接抛异常回滚。这比“先 SELECT 再 UPDATE”安全得多后者在并发下必然超卖。参数上Transactional的rollbackFor Exception.class不能省默认只回滚运行时异常业务异常如果不指定就不会回滚。4. 预约状态流转与并发扣减把“超卖”和“重复预约”摁死4.1 状态机设计预约记录不能只有“已预约”新手最容易犯的错是appointment表只有一个 status 字段值就 0 和 1。实际业务里预约要经历已预约 → 已到店核销 / 已取消 / 已过期。状态流转必须有明确规则否则用户取消后号源不返还或者过期记录一直占着号。状态值含义可流转到是否返还号源1已预约2 / 3 / 4否2已核销无否3已取消无是4已过期无是逻辑说明取消和过期都要返还号源即left_num 1。核销是到店后医生确认不返还。参数上“过期”一般由定时任务扫描把work_date已过且状态仍为 1 的记录改成 4 并返还号源。4.2 取消预约为什么要加“时间窗口”如果允许用户随时取消会出现“约了明天上午的号今天半夜取消”的情况医生排班已经定了号源返还也来不及被别人约。所以取消要加时间窗口常见做法是“就诊前 2 小时可取消”。public void cancel(Long appointmentId, Long userId) { Appointment appt appointmentMapper.selectById(appointmentId); if (appt null || !appt.getUserId().equals(userId)) { throw new BizException(预约记录不存在); } if (appt.getStatus() ! 1) { throw new BizException(当前状态不可取消); } // 校验是否在可取消时间窗口内 Schedule schedule scheduleMapper.selectById(appt.getScheduleId()); LocalDateTime deadline LocalDateTime.of(schedule.getWorkDate(), LocalTime.parse(00:00)) .minusHours(2); if (LocalDateTime.now().isAfter(deadline)) { throw new BizException(已超过可取消时间); } // 更新状态并返还号源 appt.setStatus(3); appointmentMapper.updateById(appt); scheduleMapper.addStock(appt.getScheduleId()); }逻辑说明deadline的计算要结合work_date和time_slot上面简化成当天 0 点前 2 小时实际应按具体时段算。参数上addStock的 SQL 是UPDATE schedule SET left_num left_num 1 WHERE id ?注意要加left_num total_num条件防止返还超过总号源。4.3 乐观锁 vs 悲观锁课程设计里怎么选并发扣减有两条路悲观锁SELECT ... FOR UPDATE和乐观锁版本号或条件 UPDATE。课程设计场景我推荐条件 UPDATE也就是第 3 章那种写法原因是它不需要显式加锁代码简单且 MySQL 的行锁本身就能保证原子性。悲观锁的问题是事务持有时间长如果后面还有别的操作容易锁等待甚至死锁。乐观锁的版本号写法适合“读多写少且冲突少”的场景但预约扣减冲突概率不低版本号重试反而增加复杂度。所以结论是扣减用条件 UPDATE查询用普通 SELECT别过度设计。5. 避坑与排查那些让课设从“能跑”变成“能过”的细节5.1 小程序请求报“不在以下 request 合法域名列表中”现象开发者工具里接口正常真机预览全部请求失败控制台提示域名不合法。原因微信小程序默认只允许请求已备案的 HTTPS 域名本地 IP 和 http 都不在白名单。解决开发阶段在开发者工具「详情 - 本地设置」勾选「不校验合法域名、web-view、TLS 版本以及 HTTPS 证书」。上线才需要配置真实域名课设阶段不用管。5.2 预约提交后号源没变或者变了但记录没插进去现象点预约提示成功但刷新后left_num没减或者减了但appointment表没数据。原因事务没生效或者Transactional加在了 private 方法上Spring AOP 代理不到。解决确认Transactional加在 public 方法上且调用方是通过 Spring 注入的 Bean 调用不是new出来的对象。另外检查数据库引擎是不是 InnoDBMyISAM 不支持事务。5.3 中文和 emoji 存进数据库变成问号现象宠物名字里的表情或生僻字存进去变成???。原因数据库、表、连接三处字符集不一致常见是库用了 utf8mb4 但连接串没指定。解决连接串加characterEncodingutf8库和表都用utf8mb4字段类型用VARCHAR而非CHAR。三处对齐后重启后端。5.4 定时任务把“已预约”误判成“已过期”现象用户明明约的是明天的号却被系统改成已过期并返还号源。原因过期判断用了work_date 今天但没考虑时区或者把time_slot忽略了。解决过期判断要精确到时段结束时间且用数据库时间或统一时区别用new Date()直接比。建议在 SQL 里用NOW()比较避免 Java 和 MySQL 时区不一致。5.5 小程序 setData 频繁导致页面卡顿现象医生列表滚动时卡顿或者预约表单输入延迟。原因每次输入都setData整个对象数据传输量大。解决只setData变化的字段比如this.setData({ form.phone: value })而不是重新赋值整个form。列表渲染用wx:key指定唯一字段减少 diff 开销。6. 进阶技巧把课设做出“可演示、可答辩”的完成度课设能不能拿高分很多时候不取决于功能多少而取决于演示时是否流畅、答辩时是否讲得清边界。这里给几个我踩过坑之后总结的实用技巧。第一准备一份“演示数据脚本”。答辩现场最怕数据库是空的点进去啥都没有。提前写好一段 SQL插入 3 个医生、每人 7 天排班、2 个测试用户和几条预约记录演示时直接 source 进去页面立刻有内容。-- 演示数据插入医生和排班 INSERT INTO doctor (name, title, dept) VALUES (张医生, 主治医师, 内科), (李医生, 副主任医师, 外科), (王医生, 医师, 皮肤科); -- 为每个医生生成未来 7 天、每天 4 个时段的排班 INSERT INTO schedule (doctor_id, work_date, time_slot, total_num, left_num) SELECT d.id, DATE_ADD(CURDATE(), INTERVAL n DAY), t.slot, 5, 5 FROM doctor d CROSS JOIN (SELECT 0 n UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6) days CROSS JOIN (SELECT 09:00-10:00 slot UNION SELECT 10:00-11:00 UNION SELECT 14:00-15:00 UNION SELECT 15:00-16:00) t;逻辑说明用CROSS JOIN一次性生成笛卡尔积3 医生 × 7 天 × 4 时段 84 条排班几秒搞定。参数上total_num设 5 是为了演示时容易约满方便展示“号源已满”的提示。第二给预约接口加一个“幂等”保护。用户手抖连点两次提交会生成两条预约记录。简单做法是在前端提交后立即禁用按钮后端则可以用userId scheduleId做唯一约束重复插入直接失败。ALTER TABLE appointment ADD UNIQUE KEY uk_user_schedule (user_id, schedule_id, status);逻辑说明这个唯一索引只对 status1 的活跃预约生效比较理想但 MySQL 唯一索引不支持条件所以实际做法是在插入前先查一次或者用 Redis 做短时锁。课设阶段前端禁用按钮 后端查询校验就够。第三答辩时主动讲清楚“没做什么”。比如“支付功能没接因为课设不涉及真实交易”“短信通知没做用小程序订阅消息替代”。主动划边界比被老师问倒强得多。我当年就是没提前想好这些被追问“为什么不做退款”时卡了半天血泪经验。最后说个习惯每次改完代码先跑一遍“预约 → 取消 → 再预约”的完整链路确认号源数字对得上。这个动作花不了一分钟但能挡住 80% 的低级 bug。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网