简介这是一套面向计算机相关专业在校学生与教师的宠物医院预约微信小程序项目源码采用Java技术栈开发适合作为课程设计、毕业设计、期末大作业或项目初期立项演示使用。项目围绕宠物医院在线预约场景展开涵盖用户预约、信息管理等核心业务逻辑代码结构完整可直接运行验证。压缩包共49个文件以35个Java源文件为主体配合8个XML配置、1个YML配置、1个HTML页面、1个properties配置、1个SQL数据库脚本及1个Markdown说明文档整体约95KB体积轻量便于快速部署与二次开发。目前已有170人学习关注。对于需要完整赛题方案与可运行代码的读者可借此梳理后端分层结构、数据库表设计与前后端交互流程并在此基础上DIY其他功能模块具备较好的入门进阶与拓展价值。1. 宠物医院预约小程序从课设到能跑通全流程的实战拆解宠物医院这个场景看着简单真动手做预约系统才会发现坑不少。挂号要分科室、医生排班有早晚班、宠物档案得跟主人绑定、预约时段还得防重复提交。我带过几届课设十个做宠物医院预约的八个卡在「预约时间冲突检测」和「微信小程序登录态跟后端 session 对不上」这两件事上。这套基于 Java 后端加微信小程序的方案核心价值就在于把「用户端预约 → 后端校验 → 数据库落库 → 状态回传」这条链路完整跑通而不是只做个花架子界面。它适合正在找 Java 课程设计案例源码的在校生也适合想快速搭一套预约类小程序原型的开发者。下面我按实际落地的顺序把技术选型、数据库设计、接口实现和排错经验一层层拆开讲你照着做能少走至少两周弯路。2. 技术选型与项目骨架为什么是 Spring Boot 原生小程序2.1 后端为什么选 Spring Boot 而不是 Servlet 手写课设项目最容易犯的错是用纯 Servlet JSP 硬写写到后面接口一多web.xml 配置能把人逼疯。我一般会直接上 Spring Boot原因很实在内嵌 Tomcat 不用单独装服务器起步依赖把 JSON 序列化、数据库连接池全带上了一个application.yml就能管完所有配置。对于宠物医院预约这种需要快速出接口的场景Spring Boot 的RestController配合 MyBatis 或 MyBatis-Plus一个预约接口从 Controller 到 Mapper 不超过四十行代码。具体依赖上核心就四个spring-boot-starter-web负责 REST 接口mybatis-plus-boot-starter省掉大量单表 CRUD 的 XMLmysql-connector-java连数据库lombok减少实体类的 getter/setter 噪音。版本上不用追最新Spring Boot 2.7.x 配 JDK 8 或 11 是最稳的组合课设环境里老师机器大概率也是这个配置别用 JDK 17 给自己找不痛快。!-- pom.xml 核心依赖版本按自己环境微调 -- dependencies !-- Web 层提供 REST 接口和内嵌 Tomcat -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- ORM 层MyBatis-Plus 简化单表操作 -- dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency !-- 数据库驱动MySQL 8.x 用这个 -- dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency !-- Lombok编译期生成 getter/setter -- dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这段依赖里mybatis-plus-boot-starter的版本要跟 Spring Boot 版本对齐3.5.x 系列兼容 2.7.x。mysql-connector-j是 MySQL 8 之后的官方驱动名如果你用的是 5.7 数据库换成mysql-connector-java并降版本到 5.1.x。Lombok 记得在 IDE 里装插件否则编译报「找不到符号」。2.2 小程序端为什么不用 uni-app 而用原生热搜里「uniapp 开发微信小程序 vs android/ios/鸿蒙」讨论很多但课设场景我建议直接用微信原生小程序开发。理由有三第一原生小程序的wx.request、wx.login、wx.getUserProfile跟微信登录体系无缝对接uni-app 多一层编译登录态出问题时排查链路更长第二课设答辩时老师看的是你对小程序生命周期的理解原生Page的onLoad、onShow你能讲清楚uni-app 的跨端抽象反而说不明白第三原生项目体积小微信开发者工具直接打开就能跑不用配 HBuilderX 环境。小程序端的目录结构按功能分pages/index放首页和科室导航pages/doctor放医生列表和排班展示pages/appointment放预约表单和提交逻辑pages/mine放我的预约和宠物档案。每个页面四个文件.wxml结构、.wxss样式、.js逻辑、.json配置。别把所有逻辑堆在首页后期改一个预约状态要翻五个文件。2.3 前后端联调的第一个坎域名和端口微信开发者工具默认只允许请求https域名本地开发要在「详情 → 本地设置」里勾上「不校验合法域名」。后端跑在localhost:8080小程序里请求地址写http://localhost:8080/api/...。这里有个血泪经验如果你用真机预览localhost指向的是手机自己必须换成电脑的局域网 IP比如http://192.168.1.100:8080而且手机和电脑要在同一个 WiFi 下。我见过太多人卡在这里以为是代码问题其实是网络没通。3. 数据库设计五张表撑起整个预约流程3.1 核心表结构与字段说明宠物医院预约系统的数据库不用搞太复杂五张表就能覆盖全部业务用户表、宠物表、医生表、排班表、预约表。热搜里「数据库增删改查」「数据库课程设计」是高频词但课设的难点不在 CRUD 本身而在表之间的关联设计和状态流转。先看用户表t_user字段包括id、openid微信唯一标识、nickname、avatar、phone、create_time。openid必须加唯一索引因为微信登录时同一个用户可能多次授权没有唯一约束会插入重复记录。宠物表t_pet关联用户字段有id、user_id、pet_name、pet_type猫/狗/其他、breed、age、weight。user_id建普通索引因为「我的宠物列表」查询频繁。医生表t_doctor存医生基本信息id、name、title主治医师/副主任医师、department内科/外科/皮肤科、avatar、intro。排班表t_schedule是预约的核心id、doctor_id、work_date、time_slot上午/下午、max_num最大预约数、current_num当前已约数。doctor_id和work_date建联合索引查询某医生某天的排班时走索引。预约表t_appointment记录每一笔预约id、user_id、pet_id、schedule_id、status0待确认/1已确认/2已完成/3已取消、symptom症状描述、create_time。schedule_id外键关联排班表status字段控制整个流程的状态机。-- 建库建表字符集用 utf8mb4 支持 emoji CREATE DATABASE pet_hospital DEFAULT CHARACTER SET utf8mb4; -- 用户表openid 唯一索引防止重复插入 CREATE TABLE t_user ( id BIGINT PRIMARY KEY AUTO_INCREMENT, openid VARCHAR(64) NOT NULL COMMENT 微信openid, nickname VARCHAR(50) DEFAULT , avatar VARCHAR(255) DEFAULT , phone VARCHAR(20) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_openid (openid) ) COMMENT 用户表; -- 排班表联合索引加速按医生日期查询 CREATE TABLE t_schedule ( id BIGINT PRIMARY KEY AUTO_INCREMENT, doctor_id BIGINT NOT NULL, work_date DATE NOT NULL, time_slot TINYINT NOT NULL COMMENT 0上午 1下午, max_num INT DEFAULT 10, current_num INT DEFAULT 0, KEY idx_doctor_date (doctor_id, work_date) ) COMMENT 医生排班表; -- 预约表status 控制状态流转 CREATE TABLE t_appointment ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL, pet_id BIGINT NOT NULL, schedule_id BIGINT NOT NULL, status TINYINT DEFAULT 0 COMMENT 0待确认 1已确认 2已完成 3已取消, symptom VARCHAR(500) DEFAULT , create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user (user_id), KEY idx_schedule (schedule_id) ) COMMENT 预约记录表;建表时注意utf8mb4字符集宠物名字里可能有 emoji用utf8会报错。t_schedule的current_num字段是并发控制的关键后面讲预约接口时会重点说。3.2 状态流转与业务约束预约状态不是随便改的得按流程走用户提交预约 →status0待确认医院后台确认 →status1已确认就诊完成 →status2已完成用户取消或超时未就诊 →status3已取消。每次状态变更都要校验当前状态是否允许流转比如status2已完成的记录不能再改成status3。业务约束上同一个用户同一天同一时段不能重复预约同一医生这个在接口层用查询判断不要只靠数据库唯一索引因为「同一时段」是业务概念不是单一字段。另外current_num不能超过max_num这个在更新排班表时用条件更新实现后面细讲。3.3 数据库连接池配置Spring Boot 默认用 HikariCP配置在application.yml里。课设环境并发不高但连接池参数配错会导致「连接超时」的玄学问题。spring: datasource: url: jdbc:mysql://localhost:3306/pet_hospital?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10 # 最大连接数课设10够用 minimum-idle: 2 # 最小空闲连接 connection-timeout: 30000 # 获取连接超时30秒 idle-timeout: 600000 # 空闲连接10分钟回收serverTimezoneAsia/Shanghai必须加否则 MySQL 8 的时区问题会让create_time差 8 小时。maximum-pool-size别设太大课设机器内存有限10 个连接足够。如果启动报「Public Key Retrieval is not allowed」在 url 后面加allowPublicKeyRetrievaltrue。4. 预约接口实现从微信登录到防超约的完整链路4.1 微信登录换 openid 的接口写法小程序端调用wx.login拿到临时code传给后端后端拿code加appid、secret去微信服务器换openid和session_key。这一步是预约系统的入口openid拿不到后面全白搭。// WxLoginController.java RestController RequestMapping(/api/wx) public class WxLoginController { Value(${wx.appid}) private String appid; Value(${wx.secret}) private String secret; Autowired private UserService userService; PostMapping(/login) public Result login(RequestBody MapString, String params) { String code params.get(code); // 拼接微信登录凭证校验接口 String url https://api.weixin.qq.com/sns/jscode2session ?appid appid secret secret js_code code grant_typeauthorization_code; // 用 RestTemplate 发 GET 请求 RestTemplate restTemplate new RestTemplate(); String resp restTemplate.getForObject(url, String.class); JSONObject json JSON.parseObject(resp); String openid json.getString(openid); if (openid null) { return Result.fail(微信登录失败 json.getString(errmsg)); } // 查库没有就注册 User user userService.getByOpenid(openid); if (user null) { user new User(); user.setOpenid(openid); userService.save(user); } // 生成 token 返回给小程序后续请求带 token String token JwtUtil.generate(user.getId()); return Result.ok(Map.of(token, token, userId, user.getId())); } }这段代码的关键点appid和secret写在application.yml里不要硬编码在 Java 文件里否则提交代码时泄露。RestTemplate是 Spring 自带的 HTTP 客户端课设够用不用引 OkHttp。返回的token用 JWT 生成里面塞userId后续接口从请求头Authorization里解析。小程序端拿到token后存到wx.setStorageSync(token, token)每次wx.request的 header 里带上。4.2 预约提交接口的防超约逻辑这是整个项目最容易翻车的地方。用户点「提交预约」后端要做三件事检查该用户是否重复预约、检查排班余量、扣减余量并插入预约记录。三件事必须在一个事务里否则并发下会出现「约满了还能约」的情况。// AppointmentService.java Service public class AppointmentService { Autowired private ScheduleMapper scheduleMapper; Autowired private AppointmentMapper appointmentMapper; Transactional(rollbackFor Exception.class) public Result submit(Long userId, Long petId, Long scheduleId, String symptom) { // 1. 查排班是否存在 Schedule schedule scheduleMapper.selectById(scheduleId); if (schedule null) { return Result.fail(排班不存在); } // 2. 检查是否重复预约同一用户同一排班只能约一次 Long count appointmentMapper.selectCount( new LambdaQueryWrapperAppointment() .eq(Appointment::getUserId, userId) .eq(Appointment::getScheduleId, scheduleId) .ne(Appointment::getStatus, 3) // 已取消的不算 ); if (count 0) { return Result.fail(您已预约该时段请勿重复提交); } // 3. 条件更新扣减余量current_num max_num 才更新 int updated scheduleMapper.update(null, new LambdaUpdateWrapperSchedule() .setSql(current_num current_num 1) .eq(Schedule::getId, scheduleId) .lt(Schedule::getCurrentNum, schedule.getMaxNum()) ); if (updated 0) { return Result.fail(该时段已约满); } // 4. 插入预约记录 Appointment appt new Appointment(); appt.setUserId(userId); appt.setPetId(petId); appt.setScheduleId(scheduleId); appt.setSymptom(symptom); appt.setStatus(0); appointmentMapper.insert(appt); return Result.ok(预约成功); } }核心在第三步的条件更新setSql(current_num current_num 1)配合.lt(Schedule::getCurrentNum, schedule.getMaxNum())生成 SQL 是UPDATE t_schedule SET current_num current_num 1 WHERE id ? AND current_num max_num。这条 SQL 在数据库层面是原子的并发时只有一个线程能更新成功updated返回 0 就说明约满了。这比「先查再更新」的写法靠谱得多后者在并发下必然超约。Transactional注解保证三步操作要么全成功要么全回滚。注意rollbackFor Exception.class不能省默认只回滚RuntimeException如果抛的是受检异常不会回滚。4.3 小程序端预约表单的提交与回显小程序端pages/appointment页面用户选宠物、选症状、点提交。提交前要做表单校验别把空数据发给后端。// pages/appointment/appointment.js Page({ data: { petList: [], selectedPetId: null, symptom: , scheduleId: null }, onLoad(options) { // 从医生详情页跳转过来带 scheduleId this.setData({ scheduleId: options.scheduleId }); this.loadPets(); }, loadPets() { wx.request({ url: http://localhost:8080/api/pet/list, header: { Authorization: wx.getStorageSync(token) }, success: (res) { if (res.data.code 200) { this.setData({ petList: res.data.data }); } } }); }, submit() { const { selectedPetId, symptom, scheduleId } this.data; if (!selectedPetId) { wx.showToast({ title: 请选择宠物, icon: none }); return; } if (!symptom.trim()) { wx.showToast({ title: 请填写症状描述, icon: none }); return; } wx.request({ url: http://localhost:8080/api/appointment/submit, method: POST, header: { Authorization: wx.getStorageSync(token), Content-Type: application/json }, data: { petId: selectedPetId, scheduleId, symptom }, success: (res) { if (res.data.code 200) { wx.showToast({ title: 预约成功 }); setTimeout(() wx.navigateBack(), 1500); } else { wx.showToast({ title: res.data.msg, icon: none }); } } }); } });onLoad里的options.scheduleId是从上一个页面wx.navigateTo的 url 参数带过来的。wx.request的 header 里带Authorization后端拦截器解析 token 拿userId。提交成功后wx.navigateBack返回上一页onShow里重新拉取预约列表刷新状态。5. 避坑与排查那些让我熬夜的报错5.1 小程序请求报「不在以下 request 合法域名列表中」现象开发者工具里请求后端接口控制台红字提示域名不合法接口根本没发出去。原因微信小程序默认只允许https且已备案的域名本地http://localhost不在白名单。解决开发者工具右上角「详情 → 本地设置 → 勾选不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」。真机预览时把请求地址换成电脑局域网 IP并确保手机和电脑同一 WiFi。如果还不行检查电脑防火墙是否拦了 8080 端口。5.2 预约提交后 current_num 没变但预约记录插入了现象数据库里t_appointment有记录但t_schedule的current_num还是 0导致余量显示不对。原因Transactional没生效或者条件更新的updated判断逻辑写反了。常见的是把updated 0当成成功或者Transactional加在了 private 方法上Spring AOP 不代理 private 方法。解决确认Transactional加在 public 方法上且调用方是通过 Spring 代理调用的不能是同类内部this.调用。在条件更新后打印updated值确认是 1 还是 0。如果updated一直是 0检查max_num和current_num的初始值current_num初始必须是 0max_num要大于 0。5.3 微信登录换 openid 返回 40029现象调用jscode2session接口返回{errcode:40029,errmsg:invalid code}。原因code只能用一次且有效期五分钟。常见的是小程序端wx.login拿到的code被重复使用或者后端测试时手动拼了一个过期的code。解决每次登录都重新调wx.login拿新code不要缓存。后端调试时在wx.login的success回调里console.log(code)把打印出来的code立刻拿去请求别隔几分钟再试。另外检查appid和secret是否跟小程序后台一致复制时别带空格。5.4 真机预览时图片加载不出来现象开发者工具里医生头像正常显示真机上全是空白。原因图片地址用了http://localhost或者本地文件路径真机访问不到。解决图片要么用网络图床的https地址要么把图片放在小程序项目的static目录下用相对路径引用。如果图片存在后端服务器把图片访问地址配成局域网 IP比如http://192.168.1.100:8080/upload/doctor1.jpg并确保后端静态资源映射配置正确。5.5 数据库中文乱码现象宠物名字、症状描述存进去变成???或者乱码。原因数据库字符集不是utf8mb4或者 JDBC url 没指定characterEncoding。解决建库时DEFAULT CHARACTER SET utf8mb4JDBC url 加useUnicodetruecharacterEncodingutf8mb4。已经建好的表用ALTER TABLE t_pet CONVERT TO CHARACTER SET utf8mb4转换。另外检查 MySQL 的my.ini里character-set-server是不是utf8mb4。6. 让课设拿高分的三个进阶技巧第一个技巧是给预约接口加限流。课设答辩时老师常问「如果很多人同时抢一个号怎么办」你可以在submit方法上加一个基于 Redis 的分布式锁或者简单点用synchronized关键字锁住scheduleId。更优雅的做法是用 Redis 的setnx命令key 是schedule:lock:{scheduleId}过期时间设 5 秒拿到锁才执行扣减逻辑。这样即使多个实例部署也不会超约。// 基于 Redis 的简单锁课设够用 public Result submitWithLock(Long userId, Long scheduleId, ...) { String lockKey schedule:lock: scheduleId; Boolean locked redisTemplate.opsForValue() .setIfAbsent(lockKey, 1, 5, TimeUnit.SECONDS); if (Boolean.FALSE.equals(locked)) { return Result.fail(当前预约人数较多请稍后重试); } try { return submit(userId, petId, scheduleId, symptom); } finally { redisTemplate.delete(lockKey); } }第二个技巧是给预约列表加分页和状态筛选。小程序端「我的预约」页面用户可能约了很多次一次性返回全部数据会卡。后端用 MyBatis-Plus 的Page对象传pageNum和pageSize前端用onReachBottom触底加载下一页。状态筛选用status参数用户点「待确认」只查status0的记录。第三个技巧是加一个定时任务清理过期预约。每天凌晨跑一次把work_date是昨天且status0的预约改成status3已取消同时把对应排班的current_num减回去。用 Spring 的Scheduled(cron 0 0 1 * * ?)注解就能实现不用引 Quartz。// 每天凌晨1点清理过期未确认预约 Scheduled(cron 0 0 1 * * ?) public void cleanExpiredAppointments() { // 查出昨天及以前、状态为待确认的预约 ListAppointment expired appointmentMapper.selectList( new LambdaQueryWrapperAppointment() .eq(Appointment::getStatus, 0) .lt(Appointment::getCreateTime, LocalDate.now()) ); for (Appointment appt : expired) { appt.setStatus(3); appointmentMapper.updateById(appt); // 排班余量减一 scheduleMapper.update(null, new LambdaUpdateWrapperSchedule() .setSql(current_num current_num - 1) .eq(Schedule::getId, appt.getScheduleId()) .gt(Schedule::getCurrentNum, 0) ); } }这三个技巧不用全上挑一个讲透就能让答辩老师眼前一亮。我自己的习惯是先把基础预约流程跑通再回头加限流和定时任务别一上来就搞复杂架构容易把自己绕进去。数据库设计阶段多花半小时想清楚状态流转后面写接口能省半天调试时间。希望帮到你。本文还有配套的精品资源点击获取