ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

宠物认养系统开发实战:SpringBoot+Vue3前后端分离与数据库设计

宠物认养系统开发实战:SpringBoot+Vue3前后端分离与数据库设计 先说我为什么做这个宠物猫认养系统。我见过太多流浪猫救助组织还在用Excel登记领养信息照片传在朋友圈申请表靠微信私聊管理员每天手动同步状态。信息一多就乱套猫咪被重复认养、审核流程无记录、用户找不到自己申请进度。所以当时我就想与其反复解释“我们系统正在开发”不如直接做一套能跑通的完整源码Java SpringBoot做后端Vue3做管理端和用户端页面MyBatis操作MySQL前后端分离一人维护也扛得住。这篇文章会把整个项目的需求拆解、数据库设计、后端接口、前端页面、部署踩坑全部过一遍代码片段是可以直接抄的。不管你是想二次开发、做毕业设计还是单纯想学一套真实项目的技术栈组合都能从这里拿到思路。这套项目表面上是个“宠物认养”业务实际上覆盖了Web开发里最常见的CRUD、状态流转、文件上传、权限校验、前后端联调。把它吃透之后你换到其他任何“信息发布申请审核”类系统比如二手交易、志愿者报名、公益活动登记都能很快迁移。1. 技术选型不是拍脑袋这套组合到底解决了什么问题1.1 前后端分离架构的核心收益我第一版系统用的是传统的Thymeleaf模板页面由后端渲染。做到认养申请列表时我发现一个问题管理员在后台审核猫咪状态用户在前端刷新列表两边都对同一张表操作后端返回的HTML一旦掺入大量逻辑前端想加一个动态筛选都要等后端改模板效率特别低。后来我拆成前后端分离Vue3负责页面渲染和交互SpringBoot只提供JSON接口两边通过HTTP通信职责一下就清楚了。前后端分离的好处不只是“代码分开”更重要的是可以独立开发、独立部署。前端用Node环境跑Vite开发服务器后端用SpringBoot内置Tomcat联调时配置代理上线时再把前端打包产物交给后端托管或者直接扔到Nginx。这套流程已经成了Web项目的默认姿势宠物认养系统这种需要频繁调整页面样式的项目用分离架构改起来尤其舒服。1.2 为什么是SpringBoot为什么是MyBatis很多人会问现在Java后端框架那么多为什么不选Spring Cloud或者JPA我的答案很简单项目规模说了算。宠物认养系统的核心是几个业务表和几个管理页面用Spring Cloud那一套分布式组件纯属给自己加戏。SpringBoot自带约定优于配置内置Tomcat一个jar包就能跑配合Maven管理依赖非常适合中小型业务系统。持久层我选了MyBatis而不是Spring Data JPA理由也很直接。认养业务里有不少复杂的查询条件比如按猫咪品种、年龄、是否绝育、是否疫苗接种状态组合筛选还要和认养申请记录联表统计。MyBatis的XML里可以精确定义SQL运行结果完全可控而JPA虽然启动快但一旦查询复杂起来方法名派生或者JPQL反而需要绕圈子。MyBatis还有一个容易被忽略的优势它对现有数据库的侵入性低。像宠物救助站这种场景数据库里可能早就存在一套老表字段命名不规范甚至不在当前项目里。用MyBatis你可以在XML里把查询结果明确映射到实体类不需要跟着表结构去改Java对象。1.3 版本选择SpringBoot 2.7还是3.xJava 8还是17这里必须多说一句因为“SpringBoot版本太高”这个问题我踩过。SpringBoot 3.x要求Java 17作为基线如果你的机器还在用Java 8或者公司团队习惯JDK 8那直接用3.x会让你在依赖、配置、部署上额外折腾。我做这套认养系统的时候稳定优先选了SpringBoot 2.7.18 Java 8 MyBatis 2.3.1 MySQL 5.7/8.0都兼容。组件版本说明JDK1.8企业存量环境最多兼容性最好SpringBoot2.7.18长期维护版本踩坑资料多MyBatis Starter2.3.1自动配置兼容SpringBoot 2.xVue3.4.x组合式API生态成熟Vite5.x开发服务器快构建简单MySQL5.7或8.05.7轻量8.0性能更好如果你非要尝鲜SpringBoot 3.x也不是不行但记得mybatis-spring-boot-starter要用3.0以上的版本javax改成jakarta很多老教程直接复制会报错。这个点我在后面“踩坑”部分会详细展开。2. 认养业务的需求边界与数据库建模2.1 核心角色和业务流程我建模前先把角色列清楚了。这个系统不复杂但角色不能含糊因为不同角色看到的页面和能做的操作完全不同。游客浏览猫咪列表、查看猫咪详情想申请认养必须先注册登录。注册用户登录后可以提交认养申请、查看自己的申请进度、取消待审核的申请。管理员维护猫咪信息上架、下架、编辑、审核认养申请、管理公告、管理用户。核心流程是一条链管理员录入猫咪信息并上架 - 用户浏览并申请认养 - 管理员审核 - 审核通过后猫咪状态改成“已被认养” - 其他用户不能再申请。这里有个关键点我一开始忽略了同一只猫不能同时被多个人申请成功。数据库设计时必须在状态上做约束后面接口层还要防止并发提交。2.2 MySQL表结构设计设计表的时候我遵循一个原则每张表只装一类真实世界的信息。下面这几张核心表你们都值得抄。用户表userCREATE TABLE user ( id bigint(20) NOT NULL AUTO_INCREMENT, username varchar(50) NOT NULL, password varchar(200) NOT NULL, phone varchar(20) DEFAULT NULL, avatar varchar(255) DEFAULT NULL, role tinyint(4) NOT NULL DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;角色字段我用0表示普通用户1表示管理员。密码字段必须存加密后的密文一定不能明文我用的BCrypt。猫咪信息表catCREATE TABLE cat ( id bigint(20) NOT NULL AUTO_INCREMENT, breed varchar(50) DEFAULT NULL, name varchar(50) DEFAULT NULL, age varchar(30) DEFAULT NULL, gender tinyint(4) DEFAULT NULL, sterilization tinyint(4) DEFAULT NULL, vaccine tinyint(4) DEFAULT NULL, description text, cover_image varchar(255) DEFAULT NULL, images varchar(1000) DEFAULT NULL, status tinyint(4) NOT NULL DEFAULT 0, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_status (status), KEY idx_breed (breed) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;status是关键字段0待认养、1审核中有人正在申请、2已被认养、3下架。images字段存多张图片时我用逗号拼接URL虽然不太规范但小项目中比额外建一张图片表实用得多。认养申请表adoption_applyCREATE TABLE adoption_apply ( id bigint(20) NOT NULL AUTO_INCREMENT, cat_id bigint(20) NOT NULL, user_id bigint(20) NOT NULL, reason text, address varchar(255) DEFAULT NULL, status tinyint(4) NOT NULL DEFAULT 0, apply_time datetime DEFAULT CURRENT_TIMESTAMP, audit_time datetime DEFAULT NULL, audit_remark varchar(255) DEFAULT NULL, PRIMARY KEY (id), KEY idx_cat_id (cat_id), KEY idx_user_id (user_id), UNIQUE KEY uk_cat_user (cat_id, user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;给cat_id user_id建唯一索引是为了防止同一用户重复提交对同一只猫的申请这个约束比代码里判断更可靠。申请表状态我用0待审核、1通过、2拒绝、3取消。所有状态变更都要记录时间方便管理员在列表里看到审核进度。2.3 为什么我坚持做状态机而不是直接改猫咪状态第一版偷懒用户申请认养时直接把cat.status改成1申请通过再改成2。看起来省事但逻辑一乱就出事。比如管理员拒绝了申请猫咪状态要从1改回0如果申请还在审核中又被另一个人申请两个操作互相覆盖最后查不到是谁先申请的。后来我改成“认养申请自己维护状态猫咪状态只作为展示字段”。用户提交申请时先检查猫的status是否为0然后创建一条待审核申请同时尝试用UPDATE语句把猫status从0改成1。如果UPDATE影响行数为0说明并发下已经被别人抢了直接提示该猫咪正在被申请中。这样比Java代码里先查再改安全得多因为数据库的行锁保证了原子性。2.4 索引和查询规划列表页最常见的查询是“按照品种筛选猫咪”品种字段的区分度不高但配合状态过滤时联合索引很有用。我建了idx_breed_status这样的组合索引筛选条件和排序都走索引。申请列表页按管理员账号查询时在user_id上建索引就够了不要每个字段都加索引写入性能会受影响。3. 后端从零实现Controller-Service-Mapper 怎么配合3.1 MyBatis Mapper接口与XML映射我习惯用XML写SQL因为后面要调整查询条件的话不用重新编译Java代码那么麻烦。Mapper接口长这样Mapper public interface CatMapper { ListCatVO selectCatList(Param(query) CatQuery query); int updateStatusById(Param(id) Long id, Param(status) Integer status); int lockCatForApply(Param(id) Long id); }XML里的核心查询select idselectCatList resultTypecom.example.CatVO select id, name, breed, age, gender, sterilization, vaccine, cover_image as coverImage, status, create_time as createTime from cat where if testquery.breed ! null and query.breed ! and breed like concat(%, #{query.breed}, %) /if if testquery.status ! null and status #{query.status} /if /where order by create_time desc /selectXML和接口之间有一个容易踩的坑Mapper接口的方法名一定不能重载一个方法只能对应一个SQL id。还有就是resultType用实体类时字段映射靠驼峰转换需要在application.yml配置map-underscore-to-camel-case: true否则数据库的create_time永远映射不到Java的createTime。3.2 认养申请的并发与事务控制完整的申请逻辑我在Service里是这样做的Transactional(rollbackFor Exception.class) public boolean applyAdoption(Long catId, Long userId, String reason, String address) { int locked catMapper.lockCatForApply(catId); if (locked 0) { return false; } AdoptionApply apply new AdoptionApply(); apply.setCatId(catId); apply.setUserId(userId); apply.setReason(reason); apply.setAddress(address); apply.setStatus(0); adoptionApplyMapper.insert(apply); return true; }lockCatForApply对应的SQL是update cat set status 1 where id #{id} and status 0这里虽然只写了一条UPDATE但配合Transactional这个操作和插入申请记录是同进同退的。UPDATE成功就是拿到了这只猫的“申请锁”后续的插入如果失败整个事务回滚猫的状态也会回滚到0不会出现“猫被锁住但申请记录没有”的脏数据。3.3 统一返回结构和全局异常前后端分离项目里接口返回格式不统一会让前端非常难受。我定义了一个基础Result类public class ResultT { private int code; private String message; private T data; }成功时code为0业务失败时返回自定义错误码比如1001表示“猫咪已被申请”1002表示“参数错误”。前端拿到非0就直接弹message不需要每页去try-catch。异常处理我是用RestControllerAdvice统一拦截Service里的业务异常抛出去全局处理器转换成Result返回。这样Controller就非常薄只负责参数接收和调用Service判断逻辑全部下沉。3.4 图片上传与静态资源映射猫咪图片不能只存一个URL上一张封面详情页还要轮播图。我在服务器上建了/data/upload目录配置SpringBoot的静态资源映射spring: servlet: multipart: max-file-size: 5MB max-request-size: 20MB web: resources: static-locations: file:/data/upload/上传接口接收MultipartFile重命名后保存。文件名我用UUID 原扩展名避免中文名和重复名带来的缓存问题。记得要校验文件类型不要只信前端后端必须检查contentType和扩展名否则别人上传一个jsp或者exe你就麻烦了。4. Vue3前端认养流程的人机交互实现4.1 项目初始化和目录规划前端我用Vite创建的Vue3项目目录按模块划分views放页面components放公共组件api放请求封装router放路由store放状态管理。宠物认养系统页面不算多但业务上分用户端和管理端最好把布局拆开。用户端有首页、猫咪列表、猫咪详情、认养申请、我的申请管理端有猫咪管理、申请审核、公告管理。路由我用Vue Router的懒加载写法const routes [ { path: /, component: () import(/views/home/index.vue) }, { path: /cat/:id, component: () import(/views/cat/detail.vue) }, { path: /apply, component: () import(/views/apply/create.vue), meta: { requiresAuth: true } } ];4.2 Composition API还是Options API很多刚开始学Vue3的人会纠结用哪种写法。我的建议是新项目直接上Composition API心智负担其实更小。Options API把数据、方法、生命周期分散在data、methods、watch里写长了上下翻找很累Composition API可以把某一个功能的所有逻辑放同一个区域阅读顺序和业务顺序一致。比如申请表单的提交我直接把表单校验、提交方法、倒计时这些放一个setup里代码紧凑。ref和reactive的差别是基础中的基础ref适合单个基本类型值reactive适合对象。你在项目里经常看到ref多是因为它在模板里自动解包用起来顺手。4.3 列表筛选、详情展示、申请表单的关键实现猫咪列表页是最典型的数据展示场景。我用axios调用后端接口拿到数组后循环渲染卡片。筛选区做成下拉框品种和状态变化时自动重新请求const params reactive({ breed: , status: 0, page: 1 }); const catList ref([]); async function load() { const res await api.getCatList({ ...params }); catList.value res.data; } watch(() params.breed, () { params.page 1; load(); });详情页除了展示基本信息还要把认养按钮作为一个显眼的CTA。如果猫的状态不是0就置灰且提示“已被申请”或“已找到新家”。这一点一定要从接口数据驱动不能只凭前端判断否则用户看到按钮可点提交后才发现失败体验很差。申请表单里有reason和address前端我用Element Plus做form校验。地址可以写得宽松一点因为救助站认养也需要上门回访地址不能太随意。4.4 Axios封装和登录态登录态我是用token实现的。用户登录后后端返回一个token前端存到localStorage。axios请求拦截器里统一加Authorization: Bearer xxx响应拦截器里遇到401就跳回登录页。这里有个前端的坑每次页面刷新后JS里的状态会丢失所以我做路由守卫时先检查localStorage里有没有token有token但还没拿用户信息就先请求一次/api/user/info再放行。不要在store初始化时就异步同步token容易造成页面白屏。5. 联调部署阶段踩过的坑5.1 跨域配置前后端分离项目第一个绕不开的问题就是跨域。Vite开发服务器默认端口是5173后端接口在8080浏览器会拦截跨域请求。最简单的办法是在SpringBoot里配置CORS全局策略Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }如果用了allowCredentials(true)allowedOrigins不能写成*要写具体域名或者用allowedOriginPatterns。这是我调了半小时才发现的细节。5.2 MySQL连接报错和SSL问题连MySQL时最常见的报错是Public Key Retrieval is not allowed和SSL连接错误。这跟MySQL 8.0默认的认证插件和SSL配置有关。我的连接串最后是这样写的jdbc:mysql://localhost:3306/pet_adopt?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse在本地开发没问题线上最好改成true并配置证书但这个系统内部使用我选择关闭SSL避免证书维护成本。还有MySQL 8.0要指定serverTimezone否则会报时区错误。如果是MySQL 5.7驱动用com.mysql.jdbc.DriverMySQL 8.0要换成com.mysql.cj.jdbc.Driver别照抄旧版本配置。5.3 Vue打包放进SpringBoot上线部署我选择了最简单的方案不单独部署Nginx直接把前端打包产物放到SpringBoot的static目录下。执行npm run build后把dist里的内容复制到src/main/resources/static重新打包SpringBoot的jar。这样只有一个Java进程服务器管理起来特别方便。坑点在于如果前端通过相对路径请求接口Vue Router使用history模式后刷新页面会404。我用了一个讨巧办法Vue Router改成hash模式URL里有#刷新不会重新请求后端路由。图片上传访问的静态资源路径也要做映射不能只处理接口。5.4 SpringBoot版本过高引发的依赖问题开头我说过SpringBoot 3.x要Java 17。如果你按网上最新教程装Vue3和最新SpringBoot很容易遇到javax.servlet不存在因为SpringBoot 3.x已经把javax换成jakarta。MyBatis的starter在2.x和3.x之间API也有变化。我的建议是新手和项目时间紧的人直接用SpringBoot 2.7.18这一套网上教程匹配度最高等真正理解依赖管理再升级到3.x不迟。6. 系统跑通之后我更想聊的是这几件事宠物猫认养系统的价值不在于功能多炫而在于它把一个真实业务的一条完整链路跑通了。管理员能录猫用户能申请审核能流转状态能更新数据全是活的。这就比很多只放几个示例页面的项目强太多。如果你要拿去二次开发我建议下一步做几个方向一是增加短信或邮件通知用户申请通过时能收到提醒二是增加认养回访记录救助站工作人员可以记录上门回访结果三是把图片上传改成对象存储不占用服务器磁盘四是给管理端增加一个数据看板展示每月认养成功数量。这些扩展点都不会破坏现有表结构只要按现有风格追加接口和页面就行。我个人在实际操作中最后悔的一件事是最开始没有早点写清晰的状态机。如果重新做一遍我会在第一周就先把申请的审核状态和猫咪状态的映射关系画清楚不要等到联调阶段才来补并发问题。另外所有SQL表设计里create_time和update_time这种字段一定要预留后面统计和追查数据时你才会感谢当时的自己。最后再分享一个小技巧如果你在配置MySQL时总是磕磕绊绊先别急着找代码问题用Navicat或命令行直接连一下数据库确认账号密码、IP端口、表名没问题再回到SpringBoot里排错。这套系统我本地从零跑通到部署上线前前后后大概花了两周时间其中有一天半都花在环境配置上。把这些经验写下来就是希望你能少走这些弯路。
返回列表