我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

基于SpringBoot+Vue3的动物领养管理平台实战:从数据库设计到前后端联调

基于SpringBoot+Vue3的动物领养管理平台实战:从数据库设计到前后端联调 最近有个做校园公益的朋友来找我说流浪动物救助站缺一套能跑起来的领养管理系统手动登记信息实在撑不住了。我干脆把一套自己维护的动物领养平台源码重新整理了一遍基于 Java SpringBoot Vue3 MyBatis MySQL 的前后端分离方案把宠物档案、领养申请、审核流程、公告发布、用户中心这些模块全部打通。这篇文章就按这套源码的真实设计过程来写不空谈架构把我踩过的坑、前后端联调时容易翻车的地方、表结构怎么设计才不失控统统拆开讲。适合正在学 SpringBoot 和 Vue3 的后端同学、想做个人完整项目的应届生以及想快速搭一套 Web 管理系统直接改改用的开发者。1. 项目定位与整体设计思路1.1 需求拆分一个动物领养平台到底需要哪些功能先说业务。动物领养平台表面上就是“把流浪动物挂出来用户挑一只领走”但真正落地的时候流程远不止这么简单。救助站需要有人上传宠物信息领养人要能看到宠物照片和性格描述提交申请后还得经过审核审核通过才能线下接宠后期还要做回访记录。这中间任何一个环节断掉线上系统就变成摆设。我梳理需求时把角色分成三类普通用户、工作人员、系统管理员。普通用户能注册登录、浏览宠物列表、查看宠物详情、收藏宠物、提交领养申请、查看申请进度工作人员负责录入宠物资料、审核领养申请、更新领养状态、发布站内公告系统管理员负责用户管理、角色分配、数据统计和基础配置。从这几个角色出发再拆功能模块整个项目就有了边界。核心的业务闭环是宠物登记 - 公开展示 - 用户提交申请 - 工作人员审核 - 领养成功。这个闭环决定了数据库表之间的关联关系也决定了后端接口该往哪个方向设计。比如宠物状态得有“待领养、审核中、已领养、暂停领养”申请状态得有“待审核、已通过、已拒绝、已取消”每个状态都要有流转规则否则后台上随便改状态数据很快就乱了。1.2 技术选型背后的取舍这套项目用前后端分离不是跟风而是实际需求决定的。救助站的工作人员和用户在不同场景下访问系统前端静态资源可以独立部署到 Nginx后端只提供 JSON 接口两边迭代互不影响。而且 Vue3 SpringBoot 的组合在社区里资料非常全遇到问题基本都能搜到方案对于没有专职运维的小项目来说这比花里胡哨的技术栈实用得多。后端用 SpringBoot 是因为它够轻、够快内嵌 Tomcat一个 jar 包就能跑。MyBatis 相比 JPA让我对 SQL 有绝对控制权尤其是领养申请这种多表关联、条件动态变化的场景写到 XML 里用动态 SQL 拼条件比在代码里堆逻辑清晰得多。MySQL 则是稳定且部署成本低的选择5.7 和 8.0 都能跑我用的是 8.0 版本后面会专门讲时区和 SSL 的坑。前端选择 Vue3 Vite 的主要原因有两个一是组合式 API 让组件逻辑复用变得很舒服二是 Vite 的冷启动速度比 Webpack 时代快了几个量级开发体验好。UI 组件库选了 Element Plus表格、表单、对话框这些后台管理常用的组件都比较成熟能省不少开发时间。2. 数据库设计与核心表结构2.1 业务模型梳理先画清楚表之间的关系设计表结构之前我习惯先列出实体对象和它们之间的关系而不是打开 Navicat 直接建表。这套系统里最主要的实体有用户、宠物、领养申请、宠物图片/附件、公告、收藏。关系上一个用户可以收藏多只宠物一个宠物可以被多个用户收藏所以收藏要单独一张表一个用户可以对多只宠物发起领养申请但同一只宠物在“审核中”的状态下不应该再被其他人提交申请这个需要靠查询时做状态过滤。另外宠物基本信息里不要直接存一堆图片路径的字符串我建议拆出一张宠物图片表。虽然用逗号分隔也能实现但是后续要做封面图、图片排序、删除某一张图的时候字符串方案改起来非常痛苦。多表关联查询在 MyBatis 里用嵌套或分步查询都能处理性能在数据量几千条的阶段完全没问题。2.2 核心表结构实战用户表我一般叫 sys_user字段包括 id、username、password、nickname、phone、avatar、role、status、create_time。密码不存明文用 BCrypt 加密这是最基本的底线。角色字段用字符串区分 ADMIN、STAFF、USER项目简单时不引入复杂的权限框架靠拦截器校验角色就可以。宠物表 pet 是核心表我这样建CREATE TABLE pet ( id bigint NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 宠物名称, category varchar(20) NOT NULL COMMENT 猫/狗/其他, breed varchar(50) DEFAULT NULL COMMENT 品种, gender tinyint DEFAULT NULL COMMENT 1公 2母, age_month int DEFAULT NULL COMMENT 年龄(月), health_status varchar(200) DEFAULT NULL COMMENT 健康状况描述, personality varchar(200) DEFAULT NULL COMMENT 性格特点, story text COMMENT 救助故事, cover_image varchar(255) DEFAULT NULL COMMENT 封面图, status tinyint NOT NULL DEFAULT 0 COMMENT 0待领养 1审核中 2已领养 3暂停, create_time datetime DEFAULT NULL, update_time datetime DEFAULT NULL, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT宠物信息表;注意这里的 status 我用了 tinyint可以从代码里维护枚举而不是数据库里写死中文。很多同学喜欢直接把状态设计成 varchar 并填中文虽然看着直观但后续多语言、状态扩展都很麻烦。用 tinyint 配合代码注释反而更好维护。领养申请表 adoption_apply 要记录申请人和宠物之间的关联还包括申请理由、联系方式、审核状态、审核意见和审核时间CREATE TABLE adoption_apply ( id bigint NOT NULL AUTO_INCREMENT, pet_id bigint NOT NULL, user_id bigint NOT NULL, apply_reason varchar(500) NOT NULL COMMENT 申请理由, contact_phone varchar(20) DEFAULT NULL, address varchar(255) DEFAULT NULL, status tinyint NOT NULL DEFAULT 0 COMMENT 0待审核 1通过 2拒绝 3取消, review_remark varchar(255) DEFAULT NULL COMMENT 审核意见, review_time datetime DEFAULT NULL, create_time datetime DEFAULT NULL, PRIMARY KEY (id), KEY idx_pet_id (pet_id), KEY idx_user_id (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT领养申请表;索引这一步很多人会忽略但业务上经常要按照 pet_id 查“这个宠物有哪些申请”也要按 user_id 查“我的申请记录”所以这两个字段值得各建一个普通索引。数据量还没到需要联合索引优化的级别等真的慢了再调也来得及初期索引别乱堆。公告表、宠物图片表、收藏表的结构相对简单这里不贴全部 SQL提一下收藏表一定要用唯一约束user_id pet_id来避免重复收藏否则接口层每次都要多写一次判断。数据库约束能挡住的错误就不要让代码来扛。3. 后端核心实现细节3.1 SpringBoot 工程分层与 MyBatis 配置后端目录我按常见分包方式组织controller、service、mapper、entity、dto、config、common。Controller 只做参数接收和结果封装Service 写业务逻辑Mapper 层放数据库访问接口和 XML。很多新手喜欢把业务逻辑写在 Controller 里短期看着方便等接口一多改一个逻辑就要牵动好几个地方非常痛苦。pom.xml 里核心依赖是这些dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency这里特别提醒一下我早期用过 mybatis-spring-boot-starter 2.x 配合 SpringBoot 3.x直接报错因为新版 SpringBoot 的 Jakarta 命名空间和老版本 MyBatis 不兼容。这套源码统一用的是 SpringBoot 2.7.x MyBatis 2.3.x属于稳定匹配的组合。如果你非要用 SpringBoot 3.x就去选 mybatis-spring-boot-starter 3.0.3 以上版本。application.yml 里的几个关键配置spring: datasource: url: jdbc:mysql://localhost:3306/adoption?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456 servlet: multipart: max-file-size: 10MB mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImplmap-underscore-to-camel-case 必须打开否则数据库里的 create_time 映射不到实体类的 createTime 上查出来全是 null。log-impl 配成 StdOutImpl开发阶段能直接在控制台看到 SQL排查问题效率翻倍上线前记得去掉。3.2 关键接口与领养申请的事务处理后端接口按模块来拆比如宠物模块至少要有分页查询、详情查询、新增、修改、删除/上下架申请模块要有提交申请、审核通过、审核拒绝、查询我的申请。分页查询我直接用的 PageHelper虽然轻量但要注意 PageHelper 的依赖版本和 SpringBoot 兼容性而且只在紧接着的一条查询生效很多人一不注意就在中间插了赋值代码导致分页失效。看一下宠物分页查询的 Service 代码逻辑没什么花哨的东西关键是把查询条件封装到 PetQuery 对象里然后用 PageHelper.startPage 让下一句 Mapper 查询自动分页public PageResultPetVO pagePet(PetQuery query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); ListPetVO list petMapper.selectPetPage(query); PageInfoPetVO pageInfo new PageInfo(list); return PageResult.of(pageInfo.getList(), pageInfo.getTotal()); }领养申请提交是这个项目里最值得细讲的地方因为它必须保证“同一宠物不会同时产生多条待审核申请”。如果只靠代码里先查询再插入两个用户同时提交时会有并发问题所以我在 SQL 层做了控制。提交前先执行一条带状态的更新与校验几百个用户同时点提交也不会出现重复申请。核心思路是在领养申请表中先查询是否存在该宠物且状态为待审核或已通过的记录如果有就拒绝同时为了避免并发穿透可以给 pet 表的 status 加乐观锁判断。对于这种体量的项目其实用数据库的唯一索引加状态字段组合也能解决具体方案可以根据并发量选择但一定要有兜底不能裸奔。审核接口应该放在 Service 层加事务。比如工作人员点击“通过”后要同时更新申请单状态、把宠物状态改成已领养、记录审核意见这三个动作必须是一个原子操作任何一步失败都要回滚Transactional(rollbackFor Exception.class) public void approveApply(Long applyId, String remark) { AdoptionApply apply applyMapper.selectById(applyId); if (apply null || apply.getStatus() ! 0) { throw new BusinessException(申请不存在或已被处理); } applyMapper.updateStatus(applyId, 1, remark); petMapper.updateStatus(apply.getPetId(), 2); }3.3 图片上传、角色拦截与常见封装救助站上传宠物照片用的是本地存储方案不走对象存储。我在服务器上开一个 upload 目录通过自定义的映射路径暴露给前端访问。上传接口用 MultipartFile 接收文件生成 UUID 文件名防止重名再按日期分目录存储避免单个目录文件太多。部署时只要把上传目录做成软链或者单独挂载就行数据库里只存相对路径。角色拦截这块我没引入 Shiro 和 Spring Security而是写了个 HandlerInterceptor。登录接口发放 JWT token前端每次请求把 token 放到 Authorization 头里后端拦截器解析后把用户信息放入 ThreadLocal。需要校验工作人员角色的接口在方法上打自定义注解拦截器里判断用户角色是否是 ADMIN 或 STAFF。这种轻量做法足够应付教学项目和小型内部系统比整套安全框架的配置成本低很多等真正需要复杂权限模型时再引入框架也不迟。统一返回结构我也提一下我定义了一个 R 类包含 code、message、data 三个字段所有接口都返回这个结构。前端 axios 响应拦截器统一判断 code成功才取 data失败就弹 message。这样做的好处是前端不用每个接口单独处理异常前后端口头约定好这一个结构联调效率会提升很多。4. 前端 Vue3 实战要点4.1 Vite 工程搭建与请求封装前端我用 Vite 创建 Vue3 项目命令是 npm create vitelatest选择 Vue 模板后进入目录装上 vue-router、pinia、axios 和 element-plus。这个组合对前后端分离项目来说很经典Vue Router 管页面跳转Pinia 存登录状态和用户信息axios 管 HTTP 请求Element Plus 提供现成组件。axios 封装这一块必须做好不然每个页面都写重复的 loading 和错误弹窗代码会很臃肿。我单独建了一个 http.jsimport axios from axios import { ElMessage } from element-plus const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( response { const res response.data if (res.code ! 200) { ElMessage.error(res.message || 请求失败) return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(error.response?.data?.message || 网络异常) return Promise.reject(error) } ) export default request这里 baseURL 写成 /api是为了配合 Vite 的代理。开发环境下前端跑在 5173 端口后端跑在 8080 端口如果不做代理会跨域。我在 vite.config.js 里配了 proxy把 /api 开头的请求转发给后端server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这套方案在开发和部署阶段都很实用。部署时前端构建产物放到 NginxNginx 再配置 /api 反向代理到后端服务前端代码不需要任何改动。4.2 核心页面与组件拆分宠物大厅是用户端最重要的页面我用卡片列表展示宠物通过 Element Plus 的 el-card 和 el-image 组合分页用 el-pagination。长列表必须做分页后端已经返回 total前端只需要在页码变化时重新请求接口。搜索条件我放在顶部条件变化后重置页码为 1这个细节很多人会漏导致搜索后停在超出范围的页面上。领养申请页用表单实现提交前做前端校验比如申请理由不能为空、手机号格式要正确。前端校验只是第一道防线后端 Service 层还要校验两边的校验规则必须一致不然会出现前端提示正常、后端回 500 的情况。后台管理页面里宠物管理表格用 el-table操作列放了“编辑”“删除”“暂停领养”按钮。新增和编辑共用同一个弹窗表单组件通过 props 传入当前行数据判断是新增还是编辑。组件拆分原则是一个页面一个组件文件复用性高的部分比如图片上传单独抽成组件不要把所有代码堆在单个 Vue 文件里那会让文件超过一千行改起来想砸键盘。4.3 状态管理与路由守卫Pinia 里我只存了用户信息和登录状态没有把宠物列表这类接口数据放进 store。因为接口数据有自己的时效性混在一起容易出现多个页面数据不同步的问题。用户登录成功后调用 getUserInfo 接口把用户信息写入 Pinia store刷新页面后通过路由守卫重新拉取用户信息。路由守卫主要做两个判断未登录的用户不能访问需要登录的页面角色是 ADMIN 或 STAFF 的用户才能访问后台管理路由。Vue Router 的 beforeEach 方法里读取 Pinia token再根据 meta.requiresAuth 和 meta.roles 判断。有一点要注意刷新页面时 Pinia 里的数据会清空所以不能只在登录时存一次用户信息要在守卫里判断没有用户信息时调接口恢复否则用户一刷新就被踢回登录页体验非常差。5. 常见问题与排查记录5.1 跨域问题的三种解法前后端分离项目里跨域是绕不开的问题。开发环境最推荐用 Vite proxy 做代理因为浏览器看到的请求还是同源的不需要后端参与。生产环境用 Nginx 反向代理。如果后端接口有时要被外部直接访问那就在 SpringBoot 里加 CORS 配置。我比较反对在开发环境就全局放开后端 CORS这样会掩盖掉很多问题等部署到生产环境又冒出来前后端都难受。实际排查跨域时先看浏览器 Network 面板注意区分预检请求 OPTIONS 和真实请求。如果 OPTIONS 请求直接报错通常是后端没处理请求方法或没有配置允许的请求头。如果真实请求返回了 200 但数据拿不到一般是响应头里少了 Access-Control-Allow-Origin。5.2 MyBatis 映射中的隐形陷阱MyBatis 最容易踩的坑就是数据库字段到实体属性的映射。虽然全局开了 map-underscore-to-camel-case但 XML 里如果手动写了 resultMap就要检查 column 属性是否和数据库列名一致。另外用 resultType 查询时返回的字段最好用别名改成和实体属性一致的驼峰格式比如 select count(*) 可以取别名 totalCount否则映射不上接口数据莫名其妙少字段。XML 里还有一个常见问题是复杂条件拼错。我建议把所有动态查询条件统一写在 标签里用 判断参数不要手工拼 where 11。虽然 where 11 能用但看着不专业而且 MyBatis 的 标签会自动处理掉多余的前缀 AND 或 OR写起来更安全。5.3 MySQL 8.0 连接与编码问题MySQL 8.0 和旧版驱动有一个明显的区别url 里如果不加 useSSLfalse控制台会一直刷 SSL 连接警告如果不指定 allowPublicKeyRetrievaltrue用普通账号连接时可能报 Public Key Retrieval is not allowed。这两个参数建议直接写在 jdbc url 里省得每次启动都心惊胆战。再就是字符集建库建表一定要用 utf8mb4不是 utf8。因为 utf8 在 MySQL 里最多只能存 3 字节的字符遇到表情符号就会报错。宠物故事、申请理由这些文本字段里用户可能输入任何内容用 utf8mb4 才能彻底避免乱码和写入失败的尴尬。连接串里的 characterEncodingutf8 也要保留配合服务端字符集中文乱码问题基本能解决。5.4 部署打包与版本冲突排查后端打包很简单mvn clean package -DskipTests 生成 jar然后在服务器上用 nohup java -jar 运行。这里推荐加 -Dfile.encodingUTF-8 参数不然 Linux 环境日志中文容易乱码。前端打包执行 npm run build产物在 dist 目录里部署到 Nginx 的 html 目录下再把 /api 请求代理到后端端口一个完整的线上环境就起来了。版本冲突是 Java 项目里最容易让人心态爆炸的问题。SpringBoot 2.7 需要 JDK8 或 JDK11 都能跑但 Maven 插件版本不匹配就会报错。我一般用 spring-boot-starter-parent 来统一管理依赖版本自己额外引入的依赖尽量不用越级的大版本。改依赖版本时不要一次换太多改一个、跑一次、看一次报错定位问题会容易得多。6. 最后分享几个实战心得整套系统从表结构设计到前后端联调跑通我实际用时大概两周左右。最大的体会是不要一上来就追求完美架构把业务闭环跑通远比炫技重要。先实现宠物展示和申请审核的主流程再慢慢补后台管理、权限、图片上传这些周边功能你会发现自己对需求的理解会随着开发深入越来越清晰。另外一个建议是趁早把接口文档固定下来。哪怕只是用表格列出每个接口的路径、请求参数、返回结构也能在前后端分工开发时省掉大量沟通成本。我就是前期接口文档写得太潦草后来前端反复问返回字段里到底是 status 还是 state非常浪费时间。如果你准备照着这套源码改造成自己的项目建议先把表结构调整为自己业务的真实字段比如在 pet 表增加疫苗记录、绝育状态然后在后台管理页面补上对应表单这个项目就能从毕业设计级别变成真正能落地使用的小系统。
返回列表