资讯动态

SpringBoot+Vue宠物领养系统:前后端分离项目实战与部署详解

发布时间:2026/10/17 16:37:30 来源:尧图企业网站定制
做宠物领养系统这个项目我一直觉得是前后端入门者性价比最高的练手选择之一。它不像电商系统那样业务链路冗长也不像管理系统那样全是枯燥的增删改查宠物领养天然带了一点情感属性做起来有成就感而且核心流程覆盖了权限控制、状态流转、文件上传、模糊搜索这些企业级项目常见考点。这篇文章就围绕这个SpringBoot Vue MyBatis MySQL的前后端分离版本完整梳理一遍设计思路、核心实现和部署细节源码层面的关键代码我会贴出来讲目标是你看完能自己从零搭一套跑起来。1. 项目整体设计与技术选型思路1.1 为什么选前后端分离架构传统单体项目把页面和后端逻辑放一起早期开发确实快但一旦涉及多端复用、多人协作问题就出来了。宠物领养系统虽然看起来小但实际使用场景里往往同时存在普通用户端、管理员后台甚至未来还可能加一个移动端或小程序端。如果一开始就做成前后端不分的单体结构后面想拆就难受了。前后端分离的核心好处是把前端展示逻辑和后端业务逻辑彻底解耦。前端通过HTTP接口拿到JSON数据自己负责渲染和交互后端只关心业务规则、数据存取和接口安全。这样前端团队和后端团队可以并行开发部署上也能各自独立——前端静态文件扔到Nginx或者对象存储后端打成Jar包跑在服务器上互不干扰。有人会觉得一个小项目搞前后端分离是不是过度设计我的看法是如果你是为了快速交作业那随意但如果你是想真正理解企业开发模式或者这个项目要拿去面试、作为简历作品那前后端分离反而更能体现工程化思维。接口文档、跨域处理、状态码规范、Token鉴权这些在分离架构下都是绕不开的环节做了你才能真正理解为什么企业里要这么拆。1.2 技术栈选型SpringBoot Vue MyBatis MySQL的组合逻辑这套组合是目前国内中小型项目最常见的标配之一核心原因就是学习曲线平滑、资料丰富、招聘需求量大。SpringBoot负责后端基础框架。它解决了传统Spring配置繁琐的问题通过自动配置和起步依赖几行代码就能跑起一个Web服务。内嵌Tomcat也让部署变得特别简单打一个Jar包直接java -jar就能跑不需要单独装容器。MyBatis负责数据库操作。有人会问为什么不用MyBatis-Plus或者直接用JPA原版MyBatis的SQL完全由自己掌控适合需要写复杂查询、多表关联的场景。拿宠物领养系统举例宠物列表需要按品种、性别、状态多条件筛选还要分页用MyBatis手写SQL反而一目了然。当然如果你觉得写XML麻烦用MyBatis-Plus也能事半功倍后面我会提一句扩展方向。Vue负责前端渲染。Vue的上手门槛比React低不少中文文档也友好特别适合一个人包揽前后端的开发者。宠物领养系统的页面复杂度不高无非就是列表页、详情页、表单页、后台管理页Vue的组件化开发方式可以很干净地把这些拆开。MySQL负责数据持久化。作为最流行的开源关系型数据库它的性能、稳定性、生态都经过了大量项目验证。对宠物领养系统这种数据量级MySQL完全够用而且数据库设计和SQL优化又是面试必问点多练练没坏处。这套组合选型背后的逻辑很简单主流、文档多、社区问答多踩坑了基本一搜就有答案。对个人开发者来说能快速搞定一个完整项目比追求冷门技术栈重要得多。2. 数据库设计与核心模块拆解2.1 表结构设计从业务需求反推数据库设计是这一步的核心表结构设计得合理后面的代码就是水到渠成的事。我做这系统时是先梳理业务再画E-R图最后落地建表SQL。宠物领养系统的核心参与者有这么几类用户普通用户、管理员、宠物、领养申请、公告。围绕这五个核心实体表设计如下用户表user用户ID、用户名、密码加密存储、手机号、角色普通用户/管理员、头像、注册时间宠物表pet宠物ID、名称、品种、性别、年龄、描述、图片URL、领养状态可领养/已申请/已领养、发布人ID、发布时间领养申请表adoption_apply申请ID、宠物ID、申请人ID、申请备注、状态待审核/通过/拒绝、申请时间、审核时间公告表notice公告ID、标题、内容、发布时间宠物表和领养申请表是最核心的两张表。宠物表通过status字段记录宠物当前状态初始发布是“可领养”用户提交领养申请后变为“申请中”管理员审核通过后变为“已领养”。领养申请表通过pet_id和user_id建立关联这样就能查出来谁申请了哪只宠物、当前进度是什么。这里有一个容易踩的坑用户申请领养时后端不仅要写领养申请表还要同步更新宠物表的status字段。如果业务里这两个操作不在一个事务里处理就会出现宠物显示“可领养”但用户申请时发现宠物已被别人申请了的情况。我用的建表SQL关键部分长这样CREATE TABLE pet ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL COMMENT 宠物名称, breed VARCHAR(50) COMMENT 品种, gender TINYINT COMMENT 性别 0-母 1-公, age DECIMAL(3,1) COMMENT 年龄岁, description TEXT COMMENT 描述, image_url VARCHAR(255) COMMENT 图片地址, status TINYINT DEFAULT 0 COMMENT 状态 0-可领养 1-申请中 2-已领养, user_id INT COMMENT 发布人ID, create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE adoption_apply ( id INT PRIMARY KEY AUTO_INCREMENT, pet_id INT COMMENT 宠物ID, user_id INT COMMENT 申请人ID, reason VARCHAR(500) COMMENT 申请理由, status TINYINT DEFAULT 0 COMMENT 状态 0-待审核 1-通过 2-拒绝, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );2.2 核心模块功能清单整个系统按功能划分可以分为用户端和管理端两大部分。用户端是访客和登录用户都能看到的宠物列表分页展示所有宠物支持按品种、性别、状态筛选宠物详情展示宠物详细信息登录用户可以提交领养申请我的申请查看自己提交过的领养申请记录及审核状态个人中心修改个人信息、头像管理端是管理员登录后使用的宠物管理对用户发布的宠物进行审核、上下架、编辑领养审核查看所有领养申请通过或拒绝用户管理查看注册用户列表禁用违规用户公告管理发布平台公告展示在首页这两个端的功能差异决定了前端要做两套路由、两套页面后端则通过登录用户的角色来控制接口访问权限。3. 后端核心实现SpringBoot MyBatis3.1 项目分层与目录组织我做后端项目通常会严格分层Controller、Service、Mapper各司其职。这样做的好处是代码逻辑清晰后续要加功能也好定位。宠物领养系统的后端目录结构如下src/main/java/com/example/adopt/ ├── controller/ # 接口层接收请求、返回响应 │ ├── PetController.java │ ├── UserController.java │ ├── AdoptionController.java │ └── NoticeController.java ├── service/ # 业务逻辑层 │ ├── PetService.java │ ├── UserService.java │ └── AdoptionService.java ├── mapper/ # MyBatis数据访问层 │ ├── PetMapper.java │ ├── UserMapper.java │ └── AdoptionMapper.java ├── entity/ # 实体类对应数据库表 │ ├── Pet.java │ ├── User.java │ └── AdoptionApply.java ├── common/ # 通用类返回结果封装、异常处理等 │ ├── Result.java │ └── GlobalExceptionHandler.java └── config/ # 配置类跨域、拦截器等 └── WebConfig.java实际开发时大家还会把DTO数据传输对象和VO视图对象单独抽出来避免直接暴露实体类。比如响应用户信息时不应该返回密码字段这时就可以定义一个UserVO只包含用户ID、用户名、头像这些安全字段。这个小细节在面试时提一嘴能加分不少。3.2 核心接口实现与业务逻辑宠物模块的几个核心接口可以直接作为模板参考。宠物列表接口支持分页和筛选Controller层接收参数Service层处理业务Mapper层执行SQLRestController RequestMapping(/api/pet) public class PetController { Autowired private PetService petService; GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, RequestParam(required false) String breed, RequestParam(required false) Integer gender, RequestParam(required false) Integer status) { return Result.success(petService.getPetList(page, size, breed, gender, status)); } GetMapping(/detail/{id}) public Result detail(PathVariable Integer id) { return Result.success(petService.getPetDetail(id)); } }Mapper里的动态SQL是MyBatis最常用的能力多条件筛选就是靠if标签拼接实现的select idselectPetList resultTypecom.example.adopt.entity.Pet SELECT * FROM pet WHERE 11 if testbreed ! null and breed ! AND breed LIKE CONCAT(%, #{breed}, %) /if if testgender ! null AND gender #{gender} /if if teststatus ! null AND status #{status} /if ORDER BY create_time DESC LIMIT #{offset}, #{size} /select这里有一个性能相关的小知识点分页查询用LIMIT #{offset}, #{size}时如果数据量大了越往后翻页越慢。比如一页10条翻到第10000页MySQL还是要扫描前10万行。对小项目不是问题但你要是想展示自己有优化意识可以提一下用LIMIT #{size} OFFSET #{offset}或者基于游标的分页方案来优化。这个点写进面试话术里会很加分。领养申请是整个系统的核心业务流程。用户提交申请时后端要做几件事检查宠物当前状态是否仍为可领养、写入领养申请记录、更新宠物状态。这三个操作必须在同一事务里完成否则会出现并发问题。我直接用Spring的Transactional注解来保证事务Service public class AdoptionService { Autowired private AdoptionMapper adoptionMapper; Autowired private PetMapper petMapper; Transactional public void applyAdopt(Integer petId, Integer userId, String reason) { Pet pet petMapper.selectById(petId); if (pet null || pet.getStatus() ! 0) { throw new RuntimeException(该宠物暂不可领养); } // 1. 写入申请记录 AdoptionApply apply new AdoptionApply(); apply.setPetId(petId); apply.setUserId(userId); apply.setReason(reason); apply.setStatus(0); adoptionMapper.insert(apply); // 2. 更新宠物状态为申请中 petMapper.updateStatus(petId, 1); } }这里可以顺便延伸讲一下为什么用Transactional事务是保证数据一致性的关键机制。一旦中间抛异常比如更新宠物状态失败前面的申请记录插入操作会回滚不会留一条无主数据。这个机制在真实业务里太重要了项目里用了面试里必须能讲透。管理员审核领养申请的逻辑正好相反审核通过宠物状态变为已领养申请状态变为通过审核拒绝宠物状态恢复为可领养申请状态变为拒绝。同样需要事务控制。3.3 登录鉴权与权限控制宠物领养系统虽然是个“练手级”项目但权限控制不能省。用户能提交领养申请管理员能审核申请这两类角色的权限边界必须清晰。这里我用了JWTJSON Web Token做登录态管理。用户登录成功后后端签发一个Token返回给前端前端在后续请求的Header里带上Authorization: Bearer token后端通过拦截器解析Token拿到用户ID和角色信息。整个鉴权链路是这样的用户请求/api/user/login传用户名密码后端校验通过生成Token返回Token里包含用户ID和角色前端把Token存到本地localStorage或sessionStorage后续请求前端通过axios拦截器自动在Header里加Token后端拦截器校验Token放过或拦截请求在需要管理员权限的接口上校验角色是否为管理员Token生成可以用Java的io.jsonwebtoken库配置上密钥和过期时间就行。用户密码存储则建议用BCryptPasswordEncoder加密不要明文保存密码很多新手在这一块容易忽略一旦数据库泄露就是事故。为了让接口更安全我还加了统一的响应结果封装类Result约定code200表示成功其他表示失败前端只需要判断code即可。这样比裸返回JSON对象要规范得多前后端联调时也省心。4. 前端核心实现Vue 组件化开发4.1 Vue项目结构与页面路由设计前端我用的是Vue Vue Router Axios Element UI的组合。Vue Router管页面跳转Axios管接口请求Element UI快速搭建后台管理页面。前端项目的目录结构如下src/ ├── api/ # 接口请求封装 │ ├── pet.js │ ├── user.js │ └── adoption.js ├── router/ # 路由配置 │ └── index.js ├── store/ # Vuex状态管理 │ └── index.js ├── views/ # 页面组件 │ ├── home/ # 首页 │ ├── pet/ # 宠物列表、宠物详情 │ ├── user/ # 登录、注册、个人中心 │ ├── adoption/ # 我的申请 │ └── admin/ # 后台管理页面 └── App.vue路由设计上我用了动态路由守卫来判断登录状态和角色。未登录用户访问需要登录的页面比如提交领养申请、个人中心会被重定向到登录页。管理员访问用户端页面没问题但普通用户访问管理端页面会被拦截并提示无权限。路由守卫代码router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next({ path: /login, query: { redirect: to.fullPath } }); } else if (to.meta.requiresAdmin) { const role localStorage.getItem(role); if (role ! admin) { next({ path: /, message: 无权限访问 }); } else { next(); } } else { next(); } });4.2 axios请求封装与跨域处理前端和后端大概率是分开部署的接口域名不同跨域问题必须处理。解决跨域有两种主流方式一是在后端加CORS配置二是前端通过Nginx反向代理。我在开发环境用后端CORS配置方便直接调试在生产环境用Nginx代理把/api路径的请求转发到后端服务这样前端页面请求的是同源地址根本不存在跨域问题。这个方案也是企业项目里最常用的做法。axios封装上我做了统一拦截。请求发出前带上Token响应回来后统一处理业务码axios.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers.Authorization Bearer ${token}; } return config; }); axios.interceptors.response.use( response { const res response.data; if (res.code 200) { return res; } // 401表示Token过期清掉本地信息跳转登录页 if (res.code 401) { localStorage.clear(); window.location.href /login; } return Promise.reject(new Error(res.message || 请求失败)); }, error Promise.reject(error) );这套封装的优点是所有接口请求的通用逻辑都收敛到一处不用每个页面都重复写鉴权、错误处理、Token刷新的逻辑。小型项目这样足够如果项目再大一点还可以把接口按模块拆到不同的api文件里每个模块一个方法页面里直接调方法就好。4.3 用户端与管理端的功能实现要点用户端的核心页面是宠物列表页和宠物详情页。宠物列表页用卡片布局展示宠物每张卡片显示宠物图片、名称、品种、性别、状态。用户可以通过下拉框筛选品种和性别输入框做名称模糊搜索。我用了Element UI的el-card和el-pagination组件配合后端的分页接口实现前端翻页操作。宠物详情页除了展示基本信息核心交互是“提交领养申请”按钮。点击后弹出对话框用户需要填写申请理由提交后调用后端申请接口。这里要注意的一个细节是如果宠物状态已经是“已领养”或“申请中”按钮应该置灰禁用或者点击后给出友好提示避免用户提交无效申请。管理端页面我做了三个菜单宠物管理、领养审核、公告管理。宠物管理页是一个表格管理员可以看到所有用户发布的宠物可以对宠物进行下架、编辑操作。下架操作本质是修改宠物状态但下架后的宠物在用户端列表中不该显示出来这里可以做一个软删除的设计——用status-1表示已下架列表查询时默认排除这个状态。领养审核页是管理员最常用的页面。表格展示所有领养申请记录包括宠物信息、申请人信息、申请理由、状态。管理员点击“通过”或“拒绝”按钮后调用审核接口同时会改变宠物状态。这个页面要注意数据实时性管理员审核完最好刷新一下列表我用的是审核成功后重新加载列表数据的方式。5. 部署教程与踩坑记录5.1 本地开发环境搭建部署前先得能把项目在本地跑起来。本地开发环境需要准备的东西有JDK 8以上、Maven 3.x、Node.js 14、MySQL 5.7。后端项目启动步骤用IDEA打开后端项目等待Maven下载依赖修改application.yml中的数据库连接配置把数据库名、用户名、密码改成自己的在MySQL里创建数据库并导入项目里的schema.sql初始化脚本运行AdoptApplication.java启动类看到Spring Boot的启动日志就说明启动成功接口地址默认是http://localhost:8080前端项目启动步骤打开前端项目在终端运行npm install安装依赖修改src/api里的请求地址开发环境把地址指到http://localhost:8080运行npm run devVue CLI会启动开发服务器默认地址是http://localhost:8081浏览器访问前端地址就能看到页面前后端都启动后前端请求后端接口就能正常返回数据了。这里有一个常见问题后端接口地址和前端页面地址不同源前端访问后端接口时浏览器会被拦截。解决办法是按上面说的在后端配置CORS。配置方法很简单在SpringBoot里加一个配置类Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }allowedOriginPatterns(*)表示允许所有来源访问。生产环境不要这样配要指定具体的域名否则接口就暴露给所有网站了。5.2 生产环境部署Jar包 Nginx生产环境部署整个流程分三步后端打Jar包、前端打包静态文件、配置Nginx。后端打包在项目根目录运行mvn clean package打包成功后会在target目录生成一个Jar文件把Jar包上传到服务器用java -jar adopt.jar启动如果想让后端服务常驻后台运行推荐用nohup命令或者systemd服务。我用的是nohupnohup java -jar adopt.jar adopt.log 21 日志会输出到adopt.log排查问题就看这个文件。前端打包在前端项目目录运行npm run build构建完成后会在dist目录生成静态文件把dist目录的内容上传到服务器Nginx的静态文件目录比如/usr/share/nginx/htmlNginx配置要做两件事一是托管前端静态页面二是把/api路径的请求反向代理到后端服务server { listen 80; server_name your-domain.com; location / { root /usr/share/nginx/html; index index.html; 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 $uri $uri/ /index.html;这行特别重要。Vue是单页应用路由跳转都是前端路由控制的如果用户直接访问/pet/1这种路径Nginx会去找服务器上有这个文件吗不会。加上try_files所有不存在的路径都会回退到index.html由前端路由接管。部署完成之后访问http://your-domain.com就能打开系统了/api开头的请求会被Nginx转发到后端服务整个前后端就串联起来了。5.3 常见问题与排查技巧实录我在开发这个项目的过程中踩了不少坑有些问题排查了大半天才找到原因。这里整理一个速查表希望能帮后来者省点时间。问题现象可能原因解决方法前端请求接口浏览器报CORS错误后端未配置CORS或配置不正确检查后端CORS配置确认allowedOriginPatterns允许的域名是否包含前端地址数据库连接失败报Access denied数据库账号密码错误或权限不足检查application.yml中的数据库配置在MySQL命令行手动验证账号密码图片上传后无法访问静态资源映射未配置配置WebMvcConfigurer添加静态资源映射把本地文件目录映射到URL路径前端登录成功刷新页面就退出Token存储失效或被清除检查localStorage存储和vuex状态管理刷新后重新从localStorage恢复用户信息用户提交领养申请提示宠物不可领养宠物状态判断逻辑有误检查宠物状态字段的类型和含义确认状态码0/1/2分别代表什么打包部署后页面空白控制台报404Nginx路由配置错误检查是否配置了try_files,Vue路由是history模式时必须配置后端Jar包启动失败端口被占8080端口被其他进程占用用netstat -tlnp图片上传这个坑我要重点说一下。宠物领养系统肯定要上传宠物照片而后端默认的安全配置会拦截对静态资源的访问。我第一次做的时候图片传到服务器本地目录了但前端通过URL访问图片时直接404折腾了好一会儿才明白是静态资源映射的问题。解决办法是在SpringBoot里配置静态资源映射Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath /); } }这样/upload/xxx.jpg的请求就会映射到服务器本地对应目录下的文件。生产环境建议把图片存到对象存储服务或独立的文件服务器但作为学习项目本地存储完全够用。另一个容易被忽略的问题是数据库时区配置。MySQL默认时区跟国内差8小时导致宠物发布时间、申请时间显示不对。解决方案是在数据库连接URL上加参数serverTimezoneAsia/Shanghai或者直接在MySQL初始化SQL里设置时区。6. 写在最后一些真实的项目体会这个项目从设计到部署前前后后我大概花了两周左右的业余时间。做完之后最大的感受就是很多知识点单独学的时候觉得懂了真正串起来才发现到处都是细节。比如事务注解上课时听老师讲觉得不就一个注解的事吗真的业务里用的时候才发现要思考哪些操作需要包在同一个事务里事务的隔离级别怎么选这些才是实战里真正有价值的东西。如果你打算拿这个项目作为面试作品建议重点准备几个点一个是领养申请这个核心流程的事务和并发处理一个是JWT鉴权和权限控制的完整链路还有一个是Nginx部署和跨域解决策略。这三个点如果能讲清楚面试官基本能判断你是真的做过不是只看了几篇教程。这个项目后续还可以扩展的方向很多加入Swagger接口文档、引入Redis缓存热门宠物列表、做图片上传后的压缩处理、增加邮件或短信通知、把MyBatis升级为MyBatis-Plus简化开发。每一个方向都能让你的项目在原有基础上再上一个台阶。最后说一句实在的做项目不是背代码而是要通过项目把知识点真正串起来。宠物领养系统是个很好的载体工作之余收养一只流浪猫狗这种真实场景的温暖也会让写代码这件事多了一点温度。希望这篇内容对你有用有问题欢迎一起交流。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑