news 2026/9/25 6:01:06

Java+微信小程序宠物医院预约源码:并发扣减与状态流转实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Java+微信小程序宠物医院预约源码:并发扣减与状态流转实战

简介:这是一套面向计算机相关专业在校学生与教师的宠物医院预约微信小程序项目源码,采用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 + 小程序原生 + MySQL

2.1 后端为什么选 Spring Boot 而不是裸 Servlet

课程设计里最常见的翻车,是用纯 Servlet + JSP 硬写,写到后面接口一多,路由、参数解析、事务全乱套。Spring Boot 的价值在于它把三件麻烦事一次性解决了:内嵌 Tomcat 让你不用配 web.xml,starter 依赖让你不用手动管 jar 冲突,@Transactional让你在预约扣库存时能一行注解搞定事务。对于宠物医院预约这种“读多写少、但写操作必须准”的场景,事务边界清晰比性能重要得多。

我一般会这样搭后端骨架,pom.xml里核心依赖就这几个:

<!-- Spring Boot Web:提供 REST 接口和内嵌容器 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- MyBatis-Plus:省掉大量单表 CRUD 的 XML --> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <!-- MySQL 驱动 --> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</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?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver

逻辑说明:serverTimezone=Asia/Shanghai必须加,否则 MySQL 8 会报The server time zone value is unrecognized。characterEncoding=utf8配合库的 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 SQL:

<update id="reduceStock"> 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出来的对象。另外检查数据库引擎是不是 InnoDB,MyISAM 不支持事务。

5.3 中文和 emoji 存进数据库变成问号

现象:宠物名字里的表情或生僻字存进去变成???。

原因:数据库、表、连接三处字符集不一致,常见是库用了 utf8mb4 但连接串没指定。

解决:连接串加characterEncoding=utf8,库和表都用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);

逻辑说明:这个唯一索引只对 status=1 的活跃预约生效比较理想,但 MySQL 唯一索引不支持条件,所以实际做法是在插入前先查一次,或者用 Redis 做短时锁。课设阶段前端禁用按钮 + 后端查询校验就够。

第三,答辩时主动讲清楚“没做什么”。比如“支付功能没接,因为课设不涉及真实交易”“短信通知没做,用小程序订阅消息替代”。主动划边界比被老师问倒强得多。我当年就是没提前想好这些,被追问“为什么不做退款”时卡了半天,血泪经验。

最后说个习惯:每次改完代码,先跑一遍“预约 → 取消 → 再预约”的完整链路,确认号源数字对得上。这个动作花不了一分钟,但能挡住 80% 的低级 bug。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 6:00:27

HP Z24nf显示器OSD菜单锁定解决方案

1. 问题现象与初步排查那天早上到办公室&#xff0c;发现HP Z24nf显示器右下角一直显示"已锁定屏幕菜单"的提示&#xff0c;所有物理按键按下去都没反应。作为一台专业设计显示器&#xff0c;这个状态直接导致我无法调整亮度、对比度等关键参数&#xff0c;严重影响工…

作者头像 李华
网站建设 2026/9/25 5:59:59

自建CRM实战:从零部署一套永久在线的私有化客户管理系统

在几个免费CRM之间来回切换折腾了大半年之后&#xff0c;我下定决心把客户管理彻底收回来&#xff0c;自己搭了一套DeskcommCRM——一套长期运行在自己服务器上、完全由自己掌控数据和功能的CRM系统。说实话&#xff0c;这个决定最初被团队里的同事质疑过&#xff1a;明明有现成…

作者头像 李华
网站建设 2026/9/25 5:59:14

RTX5线程生命周期终结指南:osThreadExit正确收尾与资源回收

做嵌入式开发&#xff0c;线程管理属于“看起来简单&#xff0c;做起来全是细节”的活。RTX5 的线程模型继承了 CMSIS-RTOS v2 的标准接口&#xff0c;创建线程有 osThreadNew&#xff0c;调度等待有 osDelay、osMutexAcquire&#xff0c;可一旦要考虑线程什么时候退出、退出后…

作者头像 李华
网站建设 2026/9/25 5:59:09

天猫复购预测高分代码复现:特征工程与LightGBM调参避坑指南

简介&#xff1a;这是一份基于阿里天池大赛学习赛的天猫复购预测完整案例&#xff0c;面向需要完成期末大作业、课程设计或入门数据挖掘的 Python 学习者。项目涵盖数据下载与预处理、特征工程、模型训练与测试全流程&#xff0c;代码注释详尽&#xff0c;新手也能读懂并快速复…

作者头像 李华
网站建设 2026/9/25 5:59:09

Android+XAMPP+MySQL家校互动平台:环境搭建与联调实战

简介&#xff1a;这是一份面向Android开发学习者与毕业设计/课程设计人员的家校互动平台项目资料&#xff0c;基于Android客户端、XAMPP服务端与MySQL数据库实现&#xff0c;采用CS架构完成家校通知、成绩查询、互动留言等核心功能&#xff0c;适用于相关项目设计及Android与服…

作者头像 李华
网站建设 2026/9/25 5:58:09

treg:规则驱动的终端目录树工具,告别tree命令的忽略尴尬

1. 项目概述&#xff1a;treg 到底是什么&#xff0c;解决什么问题如果你和我一样&#xff0c;日常在 Linux 终端下干活&#xff0c;大概率用过tree命令来查看目录结构。tree在展示层级目录上是把好手&#xff0c;但它有个很尴尬的地方&#xff1a;没有任何内置规则引擎&#x…

作者头像 李华