
简介这份资源面向计算机相关专业的毕业生与课程设计开发者提供一套基于Spring Boot与Vue的宠物领养系统完整实现方案可用于毕业设计、论文写作或全栈项目练手。系统功能覆盖宠物列表与详情、领养公告、个人中心等前台模块普通用户可管理我的申请、陪伴记录与宠物知识管理员则负责种类与品种管理、宠物信息维护、领养审核、宠物回访及公告发布业务闭环较为完整。资源包共4个文件包含2个zip源码压缩包、1个sql数据库脚本和1个docx部署文档整体约664KB源码、建库脚本与部署说明齐备便于快速还原运行环境。文档中还配有系统架构图、用例图、顺序图与E-R图等专业绘图可直接支撑论文中的需求分析与设计章节。目前已有42人学习适合需要完整赛题方案、数据库结构与部署思路的读者参考借鉴。1. 宠物领养系统从零到上线一套 SpringBootVue 全栈方案到底能跑多快宠物领养这件事线下靠微信群和朋友圈转发信息散、状态乱、领养人和送养人之间来回问一只猫从发布到被领走平均要折腾三四轮沟通。我去年帮本地一家流浪动物救助站做了一套基于 SpringBootVue 的宠物领养系统从建库到部署上线一共用了六天核心诉求就三个宠物信息能结构化录入、领养申请能走审批流、后台能看数据。这套东西适合谁适合想拿一个完整全栈项目练手的后端初学者也适合小型救助站或宠物店想自己搭一套内部管理工具的技术负责人。技术栈选型上SpringBoot 做后端接口和业务逻辑Vue 做前后端分离的前台展示和管理后台MySQL 存数据部署用 Nginx 加 Jar 包的方式。整套方案不依赖任何云服务一台 2 核 4G 的轻量服务器就能跑起来。下面我把从环境搭建到部署上线的完整路径拆开讲包括数据库表怎么设计、接口怎么分层、Vue 路由怎么配、打包后怎么塞进 SpringBoot 里一起部署以及我踩过的那些坑。2. 环境搭建与项目骨架SpringBoot 和 Vue 各自怎么初始化2.1 后端骨架用 Spring Initializr 生成可运行的最小工程我一般不会手动去建 Maven 目录结构直接用 Spring Initializr 生成骨架最省事。访问 start.spring.io选 Maven 项目、Java 17、SpringBoot 3.2.x注意别选太新的版本后面说为什么依赖勾选 Spring Web、MyBatis Framework、MySQL Driver、Lombok。生成后解压用 IDEA 打开目录结构长这样pet-adoption/ ├── src/main/java/com/example/petadoption/ │ ├── PetAdoptionApplication.java │ ├── controller/ │ ├── service/ │ ├── mapper/ │ ├── entity/ │ └── config/ ├── src/main/resources/ │ ├── application.yml │ └── mapper/ └── pom.xmlpom.xml 里关键依赖确认一下dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies逻辑说明spring-boot-starter-web 提供内嵌 Tomcat 和 MVC 能力mybatis-spring-boot-starter 负责 ORM 映射mysql-connector-j 是 JDBC 驱动。参数上MyBatis 的版本要跟 SpringBoot 3.x 对齐用 3.0.3 以上否则会出现 SqlSessionFactory 找不到的启动报错。application.yml 的最小配置server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/pet_adoption?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.petadoption.entity这里有个参数容易被忽略serverTimezone 必须显式指定否则 MySQL 8.x 会报时区错误。数据库名 pet_adoption 要提前建好字符集用 utf8mb4不然宠物名字里的特殊字符会乱码。2.2 前端骨架Vue 3 项目初始化与路由配置前端我用 Vue 3 加 Vite 的组合比 Webpack 快很多。初始化命令npm create vitelatest pet-adoption-web -- --template vue cd pet-adoption-web npm install npm install vue-router4 axios element-plus安装完依赖后在 src 下建 router/index.jsimport { createRouter, createWebHistory } from vue-router const routes [ { path: /, component: () import(../views/Home.vue) }, { path: /pets, component: () import(../views/PetList.vue) }, { path: /pet/:id, component: () import(../views/PetDetail.vue) }, { path: /apply/:petId, component: () import(../views/ApplyForm.vue) }, { path: /admin, component: () import(../views/Admin.vue), children: [ { path: pets, component: () import(../views/admin/PetManage.vue) }, { path: applications, component: () import(../views/admin/ApplyManage.vue) } ] } ] const router createRouter({ history: createWebHistory(), routes }) export default router逻辑说明createWebHistory 用的是 HTML5 History 模式URL 干净但需要 Nginx 配 try_files 回退否则刷新页面会 404。路由懒加载用 () import() 写法打包时会自动分包。参数上vue-router 4 是 Vue 3 专用版本别装成 3.x。axios 用来调后端接口element-plus 提供表格和表单组件管理后台基本靠它撑起来。2.3 前后端联调跨域配置和接口代理开发阶段前端跑在 5173 端口后端跑在 8080直接调接口会跨域。两种解法后端加 CORS 配置或者前端 Vite 配代理。我一般两个都配开发用代理生产用 Nginx 转发。后端 CORS 配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowCredentials(true) .maxAge(3600); } }前端 vite.config.js 代理export default defineConfig({ plugins: [vue()], server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })逻辑说明allowedOriginPatterns 用 * 而不是 allowedOrigins是因为 allowCredentials 为 true 时后者不允许通配符。changeOrigin 设为 true 让代理请求的 Host 头跟目标一致。开发时前端请求 /api/pets 会被代理到 localhost:8080/api/pets不用改代码。3. 数据库设计与核心表结构宠物、领养申请、用户三张主表怎么定3.1 表结构设计从业务字段反推 DDL宠物领养系统的核心业务就三条线宠物信息管理、领养申请审批、用户角色区分。我设计了五张表主表三张辅助两张。CREATE TABLE user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL, password VARCHAR(100) NOT NULL, role TINYINT DEFAULT 0 COMMENT 0普通用户 1管理员, phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE pet ( id BIGINT NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, species VARCHAR(20) COMMENT 猫/狗/其他, breed VARCHAR(50), age INT, gender TINYINT COMMENT 0公 1母, health_status VARCHAR(100), description TEXT, image_url VARCHAR(255), status TINYINT DEFAULT 0 COMMENT 0待领养 1已申请 2已领养, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE adoption_apply ( id BIGINT NOT NULL AUTO_INCREMENT, pet_id BIGINT NOT NULL, user_id BIGINT NOT NULL, reason TEXT, living_condition VARCHAR(200), status TINYINT DEFAULT 0 COMMENT 0待审核 1通过 2拒绝, apply_time DATETIME DEFAULT CURRENT_TIMESTAMP, audit_time DATETIME, PRIMARY KEY (id), KEY idx_pet_id (pet_id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;逻辑说明user 表的 role 字段用 TINYINT 区分权限比字符串省空间且索引快。pet 表的 status 字段是核心状态机0 到 2 的流转控制领养流程。adoption_apply 表用 pet_id 和 user_id 做外键关联加索引是因为后台按宠物查申请列表是高频操作。字符集统一 utf8mb4宠物描述里可能有 emoji。3.2 MyBatis 映射XML 写 SQL 还是注解简单查询用注解复杂动态查询用 XML。宠物列表带多条件筛选我写在 XML 里select idselectByCondition resultTypePet SELECT * FROM pet where if testspecies ! null and species ! AND species #{species} /if if teststatus ! null AND status #{status} /if if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR breed LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY create_time DESC /select逻辑说明where 标签自动处理第一个 ANDif 标签按条件拼接。参数用 #{} 是预编译占位符防 SQL 注入别用 ${}。keyword 模糊查询用 CONCAT 拼接通配符MySQL 和 PostgreSQL 都兼容。对应的 Mapper 接口Mapper public interface PetMapper { ListPet selectByCondition(Param(species) String species, Param(status) Integer status, Param(keyword) String keyword); int insert(Pet pet); int updateStatus(Param(id) Long id, Param(status) Integer status); }Param 注解让 XML 里能直接用参数名引用不加的话只能按 param1、param2 引用容易搞混。3.3 状态流转与事务控制领养申请审批通过时要同时更新申请状态和宠物状态必须在一个事务里Service public class AdoptionService { Autowired private AdoptionApplyMapper applyMapper; Autowired private PetMapper petMapper; Transactional(rollbackFor Exception.class) public void approveApply(Long applyId, Long petId) { applyMapper.updateStatus(applyId, 1); petMapper.updateStatus(petId, 2); } }逻辑说明Transactional 的 rollbackFor 设为 Exception.class默认只回滚 RuntimeException受检异常不会触发回滚。两步更新要么都成功要么都失败否则会出现申请通过了但宠物还是待领养状态的脏数据。参数上隔离级别用默认的 REPEATABLE READ 就够这种低频写场景不用调。4. 前后端功能实现领养申请流程和后台管理的接口与页面4.1 领养申请接口从提交到审批的完整链路领养申请是这套系统的核心业务流。用户在前端填表提交后端接收后落库管理员在后台审批。Controller 层RestController RequestMapping(/api/apply) public class ApplyController { Autowired private AdoptionService adoptionService; PostMapping(/submit) public Result submit(RequestBody ApplyDTO dto) { // 校验宠物是否已被领养 Pet pet adoptionService.getPetById(dto.getPetId()); if (pet.getStatus() 2) { return Result.fail(该宠物已被领养); } adoptionService.submitApply(dto); return Result.success(); } PostMapping(/audit) public Result audit(RequestBody AuditDTO dto) { adoptionService.approveApply(dto.getApplyId(), dto.getPetId()); return Result.success(); } }逻辑说明submit 接口先查宠物状态已领养的直接拒绝避免重复申请。Result 是统一返回体包含 code、msg、data 三个字段。参数上ApplyDTO 里带 petId、userId、reason、livingCondition前端表单字段跟 DTO 一一对应。Service 层的提交逻辑public void submitApply(ApplyDTO dto) { AdoptionApply apply new AdoptionApply(); BeanUtils.copyProperties(dto, apply); apply.setStatus(0); apply.setApplyTime(new Date()); applyMapper.insert(apply); // 更新宠物状态为已申请 petMapper.updateStatus(dto.getPetId(), 1); }BeanUtils.copyProperties 做 DTO 到 Entity 的字段拷贝省去手动 set。注意属性名要一致否则拷不过去。4.2 前端页面宠物列表和申请表单的 Vue 实现宠物列表页用 element-plus 的卡片布局template div classpet-list el-row :gutter20 el-col :span6 v-forpet in pets :keypet.id el-card :body-style{ padding: 0px } img :srcpet.imageUrl classpet-image / div classpet-info h3{{ pet.name }}/h3 p{{ pet.species }} · {{ pet.breed }} · {{ pet.age }}个月/p el-tag :typepet.status 0 ? success : info {{ pet.status 0 ? 待领养 : 已领养 }} /el-tag el-button click$router.push(/pet/${pet.id})查看详情/el-button /div /el-card /el-col /el-row /div /template script setup import { ref, onMounted } from vue import axios from axios const pets ref([]) onMounted(async () { const res await axios.get(/api/pet/list, { params: { status: 0 } }) pets.value res.data.data }) /script逻辑说明script setup 是 Vue 3 的组合式 API 写法ref 定义响应式数据onMounted 在组件挂载后调接口。axios 的 params 会自动拼成查询字符串。el-tag 的 type 根据状态动态切换颜色。申请表单页template el-form :modelform label-width100px el-form-item label申请理由 el-input v-modelform.reason typetextarea :rows4 / /el-form-item el-form-item label居住条件 el-input v-modelform.livingCondition / /el-form-item el-form-item el-button typeprimary clicksubmit提交申请/el-button /el-form-item /el-form /template script setup import { reactive } from vue import { useRoute, useRouter } from vue-router import axios from axios import { ElMessage } from element-plus const route useRoute() const router useRouter() const form reactive({ petId: route.params.petId, reason: , livingCondition: }) const submit async () { if (!form.reason) { ElMessage.warning(请填写申请理由) return } await axios.post(/api/apply/submit, { ...form, userId: 1 }) ElMessage.success(申请已提交) router.push(/pets) } /script逻辑说明reactive 定义表单对象route.params.petId 从路由参数取宠物 ID。提交前做非空校验成功后跳转列表页。userId 这里写死为 1实际项目要从登录态取。4.3 后台管理审批列表和状态操作后台审批页用 el-table 展示申请列表带通过和拒绝按钮template el-table :dataapplications stylewidth: 100% el-table-column propid label申请ID width80 / el-table-column proppetName label宠物名称 / el-table-column propusername label申请人 / el-table-column propreason label申请理由 show-overflow-tooltip / el-table-column label状态 width100 template #default{ row } el-tag :typestatusType(row.status){{ statusText(row.status) }}/el-tag /template /el-table-column el-table-column label操作 width180 template #default{ row } el-button v-ifrow.status 0 sizesmall typesuccess clickaudit(row, 1)通过/el-button el-button v-ifrow.status 0 sizesmall typedanger clickaudit(row, 2)拒绝/el-button /template /el-table-column /el-table /template逻辑说明el-table-column 的 template #default 插槽拿到当前行数据 row根据 status 渲染不同按钮。show-overflow-tooltip 让长文本溢出时显示省略号加悬浮提示。audit 方法调后端接口后刷新列表。后端审批接口要联表查宠物名和用户名SQL 这样写SELECT a.*, p.name AS petName, u.username FROM adoption_apply a LEFT JOIN pet p ON a.pet_id p.id LEFT JOIN user u ON a.user_id u.id ORDER BY a.apply_time DESCLEFT JOIN 保证即使宠物被删了申请记录也能查出来实际项目里宠物一般做逻辑删除不会物理删。5. 部署上线Vue 打包塞进 SpringBoot 还是 Nginx 分开部署5.1 两种部署方案对比与选择部署这块有两条路一是 Vue 打包后把 dist 目录塞进 SpringBoot 的 static 目录打成一个 Jar 包二是前端 dist 放 Nginx后端 Jar 单独跑Nginx 反代 API 请求。我两种都试过说下区别。对比项合并部署Jar 包分离部署Nginx部署复杂度低一个 Jar 搞定中要配 Nginx前端更新要重新打包 Jar替换 dist 目录即可性能静态资源走 TomcatNginx 处理静态资源更快跨域无需 Nginx 反代适用场景小型项目、演示正式环境、前后端独立迭代我一般正式环境用分离部署演示或内部工具用合并部署。下面两种都讲。5.2 合并部署Vue 打包产物放进 SpringBoot static 目录Vue 项目打包npm run build生成的 dist 目录里有 index.html 和 assets 文件夹。把这两个东西复制到 SpringBoot 的 src/main/resources/static/ 下。然后改一下 vue-router 的模式如果用 createWebHistory 需要后端配一个 fallbackController public class WebConfig { RequestMapping(value /{path:[^\\.]*}) public String forward() { return forward:/index.html; } }逻辑说明这个映射把所有不带点的路径都转发到 index.html让 Vue Router 接管路由。带点的路径如 .js、.css不匹配正常走静态资源。参数上正则 [^\.]* 表示不含点的任意字符。然后打包 SpringBootmvn clean package -DskipTests java -jar target/pet-adoption-0.0.1-SNAPSHOT.jar访问 localhost:8080 就能看到前端页面API 请求也走同一个端口没有跨域问题。5.3 分离部署Nginx 配置与后端 Jar 守护前端 dist 上传到服务器 /var/www/pet-adoption/ 目录Nginx 配置server { listen 80; server_name your_domain.com; location / { root /var/www/pet-adoption; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }逻辑说明try_files 依次尝试文件、目录都找不到就回退到 index.html解决 History 模式刷新 404。proxy_pass 把 /api/ 开头的请求转发到后端 8080 端口。proxy_set_header 传递真实 Host 和客户端 IP后端记日志时能拿到真实来源。后端 Jar 用 systemd 守护[Unit] DescriptionPet Adoption System Afternetwork.target [Service] Userwww ExecStart/usr/bin/java -jar /opt/pet-adoption/app.jar Restartalways RestartSec10 [Install] WantedBymulti-user.target逻辑说明Restartalways 让进程崩溃后自动拉起RestartSec10 表示 10 秒后重启。Userwww 用非 root 用户跑安全一些。配置放到 /etc/systemd/system/pet-adoption.service然后 systemctl enable 和 start。5.4 数据库初始化与备份上线前把建表 SQL 在服务器 MySQL 里执行一遍。备份用 mysqldumpmysqldump -u root -p pet_adoption /backup/pet_adoption_$(date %Y%m%d).sql逻辑说明$(date %Y%m%d) 生成日期后缀每天备份一个文件。可以加到 crontab 里每天凌晨跑一次。恢复用 mysql -u root -p pet_adoption backup.sql。提示生产环境数据库密码别写死在 application.yml 里用环境变量或者外部配置文件覆盖。6. 避坑与排查这套系统上线前后最容易翻车的五个地方6.1 坑一SpringBoot 版本太高导致 MyBatis 不兼容现象启动报错 Invalid value type for attribute factoryBeanObjectType或者 SqlSessionFactory 创建失败。原因SpringBoot 3.2 以上对 MyBatis 的 FactoryBean 做了改动旧版 mybatis-spring-boot-starter 不兼容。解决要么把 SpringBoot 降到 3.1.x要么把 mybatis-spring-boot-starter 升到 3.0.3 以上。我一般选后者新项目直接用配套的新版本。6.2 坑二Vue 打包后刷新页面 404现象开发环境正常部署后点导航没问题但按 F5 刷新就白屏或 404。原因createWebHistory 模式下/pets 这种路径在服务器上找不到对应的物理文件。解决Nginx 加 try_files $uri $uri/ /index.html或者合并部署时加那个 forward 映射。用 createWebHashHistory 也能绕开但 URL 带 # 不好看。6.3 坑三MySQL 时区导致时间差 8 小时现象数据库里存的时间和实际时间差 8 小时或者启动报 The server time zone value is unrecognized。原因JDBC 连接没指定时区MySQL 用了系统默认时区。解决连接 URL 加 serverTimezoneAsia/Shanghai或者 MySQL 配置文件里设 default-time-zone08:00。两个都配最稳。6.4 坑四图片上传路径在打包后失效现象开发时图片能上传能显示打成 Jar 包后上传报错或图片访问 404。原因代码里用了相对路径或者 src/main/resources 下的路径打包后这些路径不存在。解决上传目录配成绝对路径比如 /opt/pet-adoption/upload/然后加一个静态资源映射Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file:/opt/pet-adoption/upload/); } }逻辑说明addResourceLocations 用 file: 前缀指向文件系统绝对路径不要用 classpath。这样上传的图片存在 Jar 包外面重新部署不会丢。6.5 坑五并发申请同一只宠物导致状态覆盖现象两个人同时申请同一只宠物两条申请都提交成功宠物状态被改成已申请但管理员审批时不知道该批哪个。原因submit 接口先查状态再更新查和更新之间有时间窗口并发时两个请求都查到待领养状态。解决在 updateStatus 的 SQL 里加状态条件UPDATE pet SET status 1 WHERE id #{id} AND status 0然后判断 affected rows如果返回 0 说明状态已被改过回滚事务并提示用户。这是乐观锁的思路不用加锁就能解决大部分并发场景。7. 进阶技巧用 SpringBoot 自定义自动配置把通用能力抽出来这套系统做完之后我又接了第二个类似的项目发现用户认证、统一返回体、异常处理这些代码每个项目都要抄一遍。后来我把它们抽成了一个 starter新项目引一个依赖就搞定。这个技巧对做过两三个 SpringBoot 项目的人特别有用。先建一个独立的 Maven 模块pom 里引 spring-boot-autoconfiguredependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-autoconfigure/artifactId /dependency然后写自动配置类AutoConfiguration ConditionalOnWebApplication public class CommonAutoConfiguration { Bean ConditionalOnMissingBean public GlobalExceptionHandler globalExceptionHandler() { return new GlobalExceptionHandler(); } Bean ConditionalOnMissingBean public ResultAdvice resultAdvice() { return new ResultAdvice(); } }逻辑说明AutoConfiguration 是 SpringBoot 2.7 之后的新注解替代原来的 Configuration 加 spring.factories。ConditionalOnMissingBean 保证用户自己定义了同类型 Bean 时以用户的为准不覆盖。ConditionalOnWebApplication 限定只在 Web 环境生效。然后在 src/main/resources/META-INF/spring/ 下建 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件内容写一行类的全限定名com.example.common.CommonAutoConfiguration逻辑说明这个文件是 SpringBoot 3.x 的自动配置注册方式2.7 之前用的是 spring.factories。文件名和路径不能错否则配置不生效。验证方法在业务项目里引这个 starter 的依赖启动后看日志有没有加载 CommonAutoConfiguration或者故意抛个异常看 GlobalExceptionHandler 有没有生效。如果没生效检查 imports 文件路径和类名是否一致这是最常见的翻车点。参数上如果你想让某些配置可开关加 ConditionalOnPropertyBean ConditionalOnProperty(name common.exception.enabled, havingValue true, matchIfMissing true) public GlobalExceptionHandler globalExceptionHandler() { return new GlobalExceptionHandler(); }matchIfMissing true 表示配置项不写时默认开启写了 false 才关闭。这样业务项目可以在 application.yml 里灵活控制。我现在的习惯是每做完一个项目就把里面跟业务无关的代码往 starter 里挪一点攒到第三个项目的时候基本就是引依赖加改配置的事了。这套宠物领养系统里的统一返回体、全局异常处理、分页工具类后来都进了我的 common-starter。你要是也在做类似的全栈项目不妨从第一个项目就开始抽别等到复制粘贴到吐了才想起来。希望帮到你。本文还有配套的精品资源点击获取