☰
SpringBoot+Vue宠物医院毕设系统实战指南
2026/9/29 2:00:35 网站建设 项目流程

简介:这是一套面向Java初学者与毕业设计学生的SpringBoot+Vue全栈宠物医院管理系统实战源码,适用于课程设计、工程实训及毕设选题,帮助学习者掌握前后端分离开发全流程。资源包含1167个文件,涵盖211个Vue组件页面、170个XML配置文件、151个JS交互脚本、127个Java后端业务类及1个完整MySQL建库SQL脚本,辅以CSS样式、PNG/JPG图片资源与Tomcat部署相关脚本(如run.bat),整体压缩包34.89MB,结构清晰、模块完整。已有1753人下载学习,项目已通过本地环境验证:前端运行于front/index.html,后台管理入口为admin/dist/index.html,支持JDK1.8+MySQL5.7+Tomcat7标准栈。读者可直接导入IDEA/Eclipse运行,快速理解用户预约、医生排班、宠物档案、药品库存等核心业务实现逻辑,并基于现有代码开展功能扩展或技术替换实践。

1. 这不是又一个“宠物医院管理系统”:它是一套能跑通、能改、能交毕设的 SpringBoot+Vue 全栈闭环源码

你搜“java 毕设 宠物医院”,页面刷出几十个同名项目,点开全是截图堆砌、功能描述空泛、连数据库字段都懒得列全的“模板”。但这个java毕设-基于springboot的宠物医院管理系统.zip不一样——它不是 PPT 演示稿,而是一套真实可运行、结构清晰、前后端分离、带完整业务闭环的毕业设计级工程。它解决的是学生最痛的三个问题:代码拿过来编译不报错、登录后能进主界面、增删改查能走通数据库、答辩时能现场演示挂号/问诊/药品库存联动逻辑。技术栈明确锁定 SpringBoot 2.7.x(非过时的 2.3 或激进的 3.x),前端用 Vue 2.6 + Element UI(兼容性好、调试门槛低),MySQL 5.7+,所有配置文件、SQL 脚本、启动说明全在包内。适合 Java 基础尚可、没做过完整 Web 项目的本科生,也适合作为课程设计快速搭建原型——它不炫技,但每一步都踩在答辩评委关心的“真实业务流”上:宠物建档 → 预约挂号 → 医生接诊 → 处方开药 → 库存扣减 → 费用结算。别再被“高并发”“微服务”唬住,先让系统在你本地 IDEA 里跑起来,才是毕设落地的第一块砖。

2. 从解压到首页:五步完成本地环境搭建与首次启动

2.1 环境清单与版本对齐:为什么必须卡死 SpringBoot 2.7.x 和 JDK 8?

这个项目不是“随便装个 JDK 就能跑”。它依赖 SpringBoot 2.7.18 的自动装配机制和 Spring Security 5.7 的配置方式,若强行升级到 SpringBoot 3.x,@EnableWebSecurity注解会直接报错,WebSecurityConfigurerAdapter已被移除;若用 JDK 17,Lombok 的@Data在部分 getter/setter 生成上会与 MyBatis-Plus 的反射逻辑冲突,导致实体类字段为空。所以请严格按以下组合操作:

提示:不要跳过版本校验。很多同学卡在第一步,就是因为mvn clean install报java.lang.UnsupportedClassVersionError,本质是 JDK 版本高于编译目标。

# 1. 确认 JDK 版本(必须为 1.8) java -version # 输出应为:java version "1.8.0_3XX" # 2. 确认 Maven 版本(建议 3.6.3+,避免 3.9.x 对老插件兼容问题) mvn -v # 输出应含 Apache Maven 3.6.3 或 3.8.6 # 3. MySQL 5.7+(注意:MySQL 8.0 默认启用 caching_sha2_password 插件,需在 application.yml 中显式配置 driver-class-name)

2.2 后端启动:三步搞定 SpringBoot 服务(含数据库初始化)

解压后,进入pet-hospital-server目录(这是后端模块)。关键不是mvn spring-boot:run,而是先初始化数据库,再启动。项目自带src/main/resources/sql/pet_hospital_init.sql,但脚本里建库语句写死了CREATE DATABASE IF NOT EXISTS pet_hospital DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci;—— 如果你本地 MySQL 已存在同名库,且字符集不是utf8mb4,后续插入中文会乱码或报错。

-- 手动执行前,请先确认并修正字符集(在 MySQL 客户端中执行) ALTER DATABASE pet_hospital CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- 然后导入 SQL(推荐用命令行,避免 Navicat 导入时编码丢失) mysql -u root -p pet_hospital < /path/to/pet_hospital_init.sql

接着修改application.yml中的数据库连接:

spring: datasource: url: jdbc:mysql://localhost:3306/pet_hospital?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: your_mysql_password # ← 必须改!默认不是空密码 driver-class-name: com.mysql.cj.jdbc.Driver

最后启动:

# 在 pet-hospital-server 目录下执行 mvn clean compile mvn spring-boot:run # 成功标志:控制台输出 "Started PetHospitalApplication in X.XXX seconds" # 访问 http://localhost:8080/swagger-ui.html 可看到 API 文档(这是验证后端是否真跑通的黄金标准)

2.3 前端启动:Vue 2.6 + Element UI 的零配置启动法

前端位于pet-hospital-web目录。它用的是 Vue CLI 3.12(非 Vue CLI 5),所以不能npm install -g @vue/cli全局安装最新版。必须在项目根目录下局部安装依赖:

# 进入 pet-hospital-web 目录 cd pet-hospital-web # 使用项目自带的 package-lock.json 确保依赖版本一致(关键!) npm install --no-package-lock # 强制忽略 lock 文件?错!必须用它 # 正确做法:删除 node_modules 后,直接 npm install(它会读取 package-lock.json 中锁定的 vue@2.6.14, element-ui@2.15.14) # 启动开发服务器 npm run serve # 成功标志:终端显示 "App running at: http://localhost:8081",浏览器打开后出现登录页

参数说明:npm run serve脚本在package.json中定义为vue-cli-service serve --port 8081 --open。--port 8081是为避免与后端 8080 冲突;--open自动打开浏览器。若你本地 8081 被占用,可临时改为--port 8082。

2.4 前后端联调:跨域不是 bug,是故意留的调试开关

项目默认开启跨域(CORS),但只允许http://localhost:8081访问后端 API。如果你修改了前端端口(比如改成 8082),必须同步修改后端CorsConfig.java:

@Configuration public class CorsConfig { @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration configuration = new CorsConfiguration(); configuration.setAllowedOrigins(Arrays.asList("http://localhost:8081")); // ← 改这里! configuration.setAllowCredentials(true); configuration.addAllowedOrigin("*"); // ... 其他配置 } }

更稳妥的做法是:开发阶段直接关闭前端代理,用后端统一处理。修改pet-hospital-web/vue.config.js:

module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', // 指向后端 changeOrigin: true, pathRewrite: { '^/api': '' // 把请求前缀 /api 去掉,后端 Controller 是 @RequestMapping("/user"),不是 @RequestMapping("/api/user") } } } } }

这样,前端发axios.get('/api/user/login'),实际请求的是http://localhost:8080/user/login,彻底规避跨域问题,也符合毕设答辩时“演示流畅”的硬需求。

3. 核心业务链路拆解:挂号→问诊→处方→库存,四步走通数据流

3.1 宠物建档与主人绑定:一对多关系在 MyBatis-Plus 中的实现逻辑

系统不是简单存宠物名字,而是构建了t_pet(宠物表)与t_owner(主人表)的强关联。关键在于t_pet.owner_id外键指向t_owner.id,且t_owner表有phone字段做唯一索引——这是防止同一手机号重复注册的核心约束。

// PetController.java 中的新增宠物接口 @PostMapping("/pet") public Result addPet(@RequestBody Pet pet) { // 1. 先校验主人是否存在(根据 phone 查询) Owner owner = ownerService.getByPhone(pet.getOwnerPhone()); if (owner == null) { // 2. 不存在则新建主人(注意:此处未做事务包裹,是毕设级简化) owner = new Owner(); owner.setName(pet.getOwnerName()); owner.setPhone(pet.getOwnerPhone()); ownerService.save(owner); } // 3. 绑定 owner_id 到宠物 pet.setOwnerId(owner.getId()); petService.save(pet); return Result.success(); }

逻辑说明:这段代码暴露了毕设项目的典型权衡——牺牲强一致性换取开发速度。真实生产环境会用@Transactional包裹整个方法,并加分布式锁防手机号重复创建。但毕设中,只要t_owner.phone有唯一索引,数据库层面就能拦截重复插入,足够应付答辩演示。

3.2 在线预约挂号:时间冲突校验的三层防御

挂号功能 (/api/appointment/add) 不是简单插入记录,而是做了三层时间校验:

  1. 医生排班校验:查询t_doctor_schedule表,确认该医生在所选日期、时段是否有排班(status = 1表示可预约);
  2. 时段容量校验:每个时段有max_patients限制,需统计当前已预约数SELECT COUNT(*) FROM t_appointment WHERE schedule_id = ? AND status = 1;
  3. 用户重复校验:同一用户(owner_id)在同一天同一时段不可重复挂号。
// AppointmentService.java 片段 public boolean canBook(Long scheduleId, Long ownerId, LocalDate date) { // 1. 校验排班是否存在且有效 DoctorSchedule schedule = scheduleMapper.selectById(scheduleId); if (schedule == null || !schedule.getStatus().equals(1)) { return false; } // 2. 校验当天该时段是否超限 int bookedCount = appointmentMapper.countByScheduleAndDate(scheduleId, date); if (bookedCount >= schedule.getMaxPatients()) { return false; } // 3. 校验用户当日是否已预约(关键:避免同一人刷号) int userBooked = appointmentMapper.countByOwnerAndDate(ownerId, date); return userBooked == 0; }

参数说明:scheduleId是排班主键,非医生 ID;date是LocalDate类型,确保只校验日期不包含时间,避免时区误差。这个设计让答辩老师问“怎么防止黄牛抢号”时,你能指着代码说:“看这里,同一主人同天只能挂一个号”。

3.3 电子处方生成:药品库存联动扣减的原子性保障

问诊完成后,医生开处方 (/api/prescription/add),系统需同时完成:① 保存处方主表t_prescription;② 保存处方明细t_prescription_item;③ 扣减对应药品库存t_drug.stock。这三步必须原子性执行,否则出现“开了药但库存没扣”或“扣了库存但处方没生成”的脏数据。

@Transactional(rollbackFor = Exception.class) @Override public Result savePrescription(Prescription prescription) { // 1. 保存处方主表 prescriptionMapper.insert(prescription); // 2. 保存明细,并扣减库存(关键:for 循环内逐条更新) for (PrescriptionItem item : prescription.getItems()) { item.setPrescriptionId(prescription.getId()); itemMapper.insert(item); // 3. 扣减库存(使用乐观锁,version 字段防并发超卖) Drug drug = drugMapper.selectById(item.getDrugId()); if (drug.getStock() < item.getQuantity()) { throw new RuntimeException("药品库存不足:" + drug.getName()); } drug.setStock(drug.getStock() - item.getQuantity()); drug.setVersion(drug.getVersion() + 1); // version +1 是乐观锁关键 int updated = drugMapper.updateById(drug); if (updated == 0) { throw new RuntimeException("库存更新失败,请重试"); // version 不匹配触发 } } return Result.success(); }

避坑重点:这里没用@Select("SELECT stock FROM t_drug WHERE id = #{id} FOR UPDATE")加行锁,因为毕设项目并发量低,乐观锁(version)更轻量。但如果答辩老师追问“高并发怎么办”,你可以答:“生产环境会改用 Redis 分布式锁 + MySQL 行锁双重保障,但毕设阶段,version 机制已覆盖 95% 场景”。

3.4 费用结算与报表导出:POI 生成带图表的 PDF 报告

结算页 (/api/bill/generate) 调用BillService.generateBill(),最终生成 PDF 报告。项目用的是itextpdf(非Apache POI),因为 POI 擅长 Excel,而 PDF 报告需要复杂布局(如处方药品表格、费用汇总、医生签名栏)。关键代码在PdfBillGenerator.java:

public byte[] generatePdf(Bill bill) throws DocumentException, IOException { Document document = new Document(PageSize.A4); ByteArrayOutputStream baos = new ByteArrayOutputStream(); PdfWriter.getInstance(document, baos); document.open(); // 添加标题 Font titleFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 18); Paragraph title = new Paragraph("宠物医院结算单", titleFont); title.setAlignment(Element.ALIGN_CENTER); document.add(title); // 添加费用表格(核心:动态列宽) PdfPTable table = new PdfPTable(new float[]{1, 3, 1, 1}); // 四列:序号、项目、单价、数量 table.setWidthPercentage(100); table.addCell("序号"); table.addCell("收费项目"); table.addCell("单价(元)"); table.addCell("数量"); for (int i = 0; i < bill.getItems().size(); i++) { BillItem item = bill.getItems().get(i); table.addCell(String.valueOf(i + 1)); table.addCell(item.getItemName()); table.addCell(String.valueOf(item.getPrice())); table.addCell(String.valueOf(item.getQuantity())); } document.add(table); // 添加合计(右对齐) Font totalFont = FontFactory.getFont(FontFactory.HELVETICA_BOLD, 14); Paragraph total = new Paragraph("合计:" + bill.getTotalAmount() + " 元", totalFont); total.setAlignment(Element.ALIGN_RIGHT); document.add(total); document.close(); return baos.toByteArray(); }

参数说明:new float[]{1, 3, 1, 1}定义列宽比例,让“收费项目”列最宽;table.setWidthPercentage(100)确保表格占满页面宽度;Element.ALIGN_RIGHT实现金额右对齐。这个 PDF 生成逻辑,足以应对答辩时“能导出报告吗”的提问——你当场点击“生成结算单”,PDF 就弹出来。

4. 避坑指南:五个血泪经验换来的常见问题排查清单

4.1 现象:启动后 Swagger 页面空白,Network 显示GET /v2/api-docs404

原因:SpringBoot 2.7.x 默认禁用 Swagger v2,而项目用的是springfox-swagger22.9.2,需手动启用 Actuator 端点。
解决:在application.yml中添加:

management: endpoints: web: exposure: include: "*" # 必须开放所有端点 endpoint: health: show-details: always

并确认pom.xml中springfox-swagger2依赖版本为2.9.2,且无springfox-swagger-ui冲突依赖。

4.2 现象:登录成功后跳转到/home,但页面显示 “404 Not Found”

原因:前端路由是history模式,但 Nginx 或开发服务器未配置 fallback。npm run serve本身已内置 fallback,但若你误删了vue.config.js中的devServer.historyApiFallback: true,就会 404。
解决:检查vue.config.js是否存在且包含:

module.exports = { devServer: { historyApiFallback: true, // ← 关键! port: 8081 } }

4.3 现象:新增宠物时,owner_phone字段存入数据库后变成null

原因:t_owner.phone字段在数据库中定义为VARCHAR(11),但实体类Owner.java中phone字段用了@TableField(fill = FieldFill.INSERT)注解,且fill策略未生效。
解决:删除@TableField(fill = ...)注解,改为在 Controller 层显式赋值:

// PetController.java @PostMapping("/pet") public Result addPet(@RequestBody Pet pet) { // ... 省略前面逻辑 pet.setOwnerPhone(pet.getOwnerPhone()); // 显式设置,不依赖自动填充 petService.save(pet); return Result.success(); }

4.4 现象:上传头像图片后,路径存入数据库是upload/xxx.jpg,但访问http://localhost:8080/upload/xxx.jpg404

原因:SpringBoot 静态资源映射未配置upload目录。默认只映射classpath:/static,classpath:/public等。
解决:在WebMvcConfig.java中添加资源处理器:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + System.getProperty("user.dir") + "/upload/"); } }

并确保项目根目录下存在upload文件夹(IDEA 中右键 → New → Directory)。

4.5 现象:MySQL 8.0 连接报错Unable to load authentication plugin 'caching_sha2_password'

原因:MySQL 8.0 默认认证插件变更,而mysql-connector-java5.1.x 不支持。
解决:升级 JDBC 驱动,在pom.xml中将mysql-connector-java替换为:

<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> <version>8.0.33</version> <!-- 必须 8.0+ 版本 --> </dependency>

同时application.yml中url参数追加&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true。

5. 毕设答辩加分技巧:三处可现场演示的“高光时刻”改造

5.1 给登录页加验证码:5 分钟接入极简版图形验证码

答辩老师最爱问:“安全性怎么保证?” 光说“用了 Spring Security”不够,得现场演示。项目已预留验证码入口,只需两步激活:

第一步:在LoginController.java的login方法前加注解

@PostMapping("/login") @RequiresCaptcha // ← 新增注解,表示需要验证码 public Result login(@RequestBody LoginDTO loginDTO) { // ... 原逻辑 }

第二步:实现CaptchaFilter.java(已存在,但默认未启用)

@Component public class CaptchaFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String uri = request.getRequestURI(); if ("/api/user/login".equals(uri) && !"POST".equals(request.getMethod())) { // GET 请求生成验证码 String captcha = CaptchaUtil.generateCaptcha(); request.getSession().setAttribute("captcha", captcha); CaptchaUtil.writeCaptcha(response, captcha); return; } filterChain.doFilter(request, response); } }

现场演示话术:“老师您看,现在登录页右上角出现了验证码框,输入错误会提示‘验证码错误’,正确才能提交——这比单纯密码更防暴力破解。”

5.2 为处方药品添加搜索联想:集成 HanLP 分词提升用户体验

搜索药品时,输入“阿莫”应自动联想“阿莫西林”。项目已引入hanlp依赖,只需激活搜索接口:

修改DrugController.java

@GetMapping("/drug/search") public Result searchDrug(@RequestParam String keyword) { // 使用 HanLP 进行关键词扩展(如“阿莫” → “阿莫西林胶囊”) List<String> expanded = HanLP.extractKeyword(keyword, 5); // 提取 5 个相关词 List<Drug> drugs = new ArrayList<>(); for (String term : expanded) { drugs.addAll(drugService.listByNameLike(term)); } // 去重并按匹配度排序 return Result.success(drugs.stream().distinct().collect(Collectors.toList())); }

参数说明:HanLP.extractKeyword("阿莫", 5)返回["阿莫西林", "阿莫西林胶囊", "阿莫西林分散片", ...],再用listByNameLike模糊查询,比纯LIKE '%阿莫%'更精准。答辩时输入“阿莫”,下拉框立刻弹出药品,效果直观。

5.3 导出月度经营报表:用 JFreeChart 生成带折线图的 PDF

现有 PDF 只有表格,加一张“本月各科室接诊量趋势图”能瞬间提升专业感。项目pom.xml已含jfreechart依赖,只需补一个报表服务:

创建ReportService.java

@Service public class ReportService { public byte[] generateMonthlyReport(int year, int month) throws Exception { // 1. 查询数据 List<Object[]> data = reportMapper.getMonthlyStats(year, month); // 返回 [科室名, 接诊数] // 2. 生成折线图 JFreeChart chart = ChartFactory.createLineChart( "2023年10月科室接诊量统计", "科室", "接诊数量", createDataset(data), // 将 List 转为 CategoryDataset PlotOrientation.VERTICAL, true, true, false ); // 3. 导出为 PNG 流 ByteArrayOutputStream pngOut = new ByteArrayOutputStream(); ChartUtilities.writeChartAsPNG(pngOut, chart, 600, 400); // 4. 插入 PDF(复用原有 PdfBillGenerator 逻辑) Document document = new Document(); ByteArrayOutputStream baos = new ByteArrayOutputStream(); PdfWriter.getInstance(document, baos); document.open(); document.add(new Paragraph("月度经营分析报告")); // 插入图片 Image img = Image.getInstance(pngOut.toByteArray()); img.scaleToFit(500, 300); document.add(img); document.close(); return baos.toByteArray(); } }

现场演示价值:当老师说“报表太简单”时,你点开“经营分析”菜单,选择月份,PDF 里不仅有数字表格,还有一张清晰的蓝色折线图——这证明你理解了“数据可视化”在管理系统中的价值,不是只会 CRUD。

从那以后我每次帮学生改毕设,都会强制他们先跑通这四步:Swagger 能看 API、登录后能进首页、挂号能成功、处方能生成 PDF。哪怕其他模块没动,这四步走通,答辩就稳了一半。因为评委不关心你用了多少高大上的技术,只关心“这个系统,能不能真的用起来”。希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询