做宠物管理系统这个项目,最早是给学生准备的一套Spring Boot + Vue前后端分离实战案例。市面上这套题目的源码一搜一大把,但真正能一次跑起来的很少,要么数据库表结构缺胳膊少腿,要么前端依赖装不上,要么文档就一句“下载后自行研究”。所以我整理这套项目时给自己定了三个硬指标:源码能直接启动、数据库脚本完整、配套文档能让人照着做到上线。这篇文章会把整个系统的设计思路、表结构、后端接口、前端页面以及部署排错过程完整过一遍。打算做课程设计、毕业设计,或者纯粹想用Spring Boot + Vue练手的开发者,都能从这里拿到一套能落地的参考方案。
1. 为什么选Spring Boot + Vue做宠物管理系统
1.1 从需求场景说起:宠物管理到底要管什么
第一次看到“宠物管理系统”这个题目,有人觉得不就是写个增删改查么。真要做细,远远不止。比如一个流浪动物救助站,每天要录入新收容的猫狗,记录它们的外貌特征、健康状况、疫苗注射情况,还要管理领养人的申请和回访信息。再比如一个宠物店,需要关注在售宠物的品种、价格、库存,以及卖出后和主人的绑定关系。所以背后至少包含宠物档案、品种、主人/用户、领养记录、疫苗记录这几张核心表,业务上还涉及状态流转:待领养、已领养、休息中等等。只有先把业务场景梳理清楚,表和接口才不会设计得四不像。
1.2 为什么是Spring Boot + Vue而不是SSM或单独JSP
现在做管理系统,最常见的技术组合无非三类:Spring Boot + Vue、SSM + JSP、Django + React。我为什么在这套项目里选了Spring Boot + Vue?先看一个对比:
| 方案 | 前端体验 | 学习成本 | 就业/毕设加分 | 部署复杂度 |
|---|---|---|---|---|
| Spring Boot + Vue | 前后端分离,界面流畅 | 中等,需要理解跨域和代理 | 高,主流岗位要求 | 中等 |
| SSM + JSP | 服务端渲染,页面较老 | 低,单体架构易上手 | 偏低,趋向过时 | 低 |
| Django + React | 前后端分离,开发效率高 | 中等,需要掌握Python栈 | 看岗位方向 | 中等 |
对多数课程设计和毕业设计场景来说,Spring Boot + Vue 最大的优势是“一次开发,很多地方都在用”。代码分成清晰的接口层、业务层和数据层,前端直接通过HTTP调接口,将来想接小程序或者App,后端几乎不用做大的改动。如果纯粹为了省事用JSP,确实能快速出活,但是页面效果和工程化程度都很难拿出手。所以这套项目我坚定选了前后端分离。前后端分离最大的门槛在环境配置和联调,这恰恰是学习者最容易卡住的地方,后面我会专门讲怎么避坑。
1.3 系统功能划分与主要角色
我从实际管理的角度把功能分成三类:
- 基础档案管理:宠物信息的新增、编辑、删除、详情查看,品种管理。
- 领养流程管理:领养人提交申请、管理员审核、记录领养状态。
- 系统管理:用户登录、账号管理、操作日志(简单项目可省略)。
登录角色至少分管理员和普通用户,管理员维护所有数据,普通用户只能查看宠物列表和提交领养申请。权限不需要做得很重,但用户身份要判断,否则后台接口裸奔会被答辩老师问住。前后端页面我按角色区分菜单,接口层面用拦截器校验登录状态就足够。这样的设计既控制复杂度,又回应了“权限管理”这个常见的答辩问题。
2. 数据库设计:宠物管理系统的表结构是灵魂
2.1 核心表结构总览
表结构规划是这套项目的起点,我按业务关系拆成五张核心表:
- sys_user:用户表,存放管理员和普通用户,字段id、username、password、nickname、role、create_time、update_time、delete_flag。
- pet_breed:宠物品种表,字段id、breed_name、category(猫/狗),做字典数据。
- pet:宠物信息表,字段id、pet_name、breed_id、gender、age、health_status、status、avatar、description、create_time、update_time、delete_flag。
- pet_vaccine:疫苗记录表,字段id、pet_id、vaccine_name、vaccine_time、remark。
- adoption_record:领养记录表,字段id、pet_id、user_id、apply_time、audit_status、audit_remark、adopt_time。
给最核心的pet表建表SQL如下:
CREATE TABLE `pet` ( `id` bigint NOT NULL AUTO_INCREMENT, `pet_name` varchar(50) NOT NULL COMMENT '宠物名', `breed_id` bigint DEFAULT NULL COMMENT '品种ID', `gender` tinyint DEFAULT '1' COMMENT '性别: 1公 2母', `age` int DEFAULT NULL COMMENT '年龄(月)', `health_status` varchar(100) DEFAULT NULL COMMENT '健康状况', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态: 1可领养 2已领养 3休息', `avatar` varchar(255) DEFAULT NULL COMMENT '图片地址', `description` varchar(500) DEFAULT NULL COMMENT '描述', `create_time` datetime DEFAULT NULL, `update_time` datetime DEFAULT NULL, `delete_flag` tinyint NOT NULL DEFAULT '0' COMMENT '逻辑删除', PRIMARY KEY (`id`), KEY `idx_breed` (`breed_id`), KEY `idx_status` (`status`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='宠物信息表';MySQL 8+ 和 5.7 在字符集上的处理有差异,项目里统一utf8mb4,避免中文乱码。外键我没有建物理外键,只用索引和代码逻辑来维护关系。原因很简单:物理外键在删除宠物时容易造成不必要的约束麻烦,而且答辩时如果能说清“为什么不用外键”,反而是加分项。
2.2 字段设计时容易被忽略的细节
状态字段用tinyint而不是varchar。如果直接用“可领养”“已领养”“休息”,显示是方便,但代码里到处是中文比较,改一个词要改所有代码。我用tinyint存状态值,前端去映射中文文案。后端定义常量或枚举类,比如PetStatusEnum,代码可读性和可维护性都好很多。
时间字段统一datetime,别用timestamp。timestamp有2038问题,而且会带时区干扰。配合MyBatis-Plus的自动填充,@TableField(fill = FieldFill.INSERT) 就能在插入时自动写入create_time,不用每次手动set。逻辑删除delete_flag必须加。宠物信息尤其是救助站数据,误删后想恢复就很麻烦。MyBatis-Plus里配置@TableLogic,查询时自动过滤删除数据,非常省心。
索引不要盲目建。当前系统最频繁的查询是宠物列表按品种、状态、名称模糊查询,所以在pet表上建了idx_breed和idx_status。模糊搜索用LIKE '%name%'无法走常规索引,数据量不大时没关系,但如果将来宠物上万条,建议改上Elasticsearch或者全文索引,暂时不需要过度设计。
2.3 初始化数据与SQL脚本管理
源码包里的sql目录我放两个文件:init.sql(建库建表)和data.sql(初始化数据)。data.sql里至少包含:
- 两个测试账号:admin/admin123,user/user123;
- 宠物品种基础数据:中华田园猫、英国短毛猫、金毛、拉布拉多等;
- 两三条宠物演示数据。
导入顺序一定是先init再data,很多新手直接双击data.sql报错,就是因为表不存在。另外,如果你的项目已经上线或者提交给老师,建议把脚本按版本维护,比如sql/v1.0_init.sql、sql/v1.1_add_adoption.sql。不要永远只有一份“最终版”,否则隔几个月想升级都不知道当初改了什么。这是个很实用的职业习惯。
3. Spring Boot后端搭建与核心接口实现
3.1 工程结构与依赖引入
先用Spring Initializr创建一个Spring Boot项目,Java版本就用8或11,Spring Boot版本建议2.7.x。为什么不用Spring Boot 3?3.x要求Java 17,很多课程设计机器上还跑着JDK 8,而且部分老版本MyBatis-Plus兼容性有坑。2.7.x已经足够稳定,等你熟练了再升级也不迟。
后端包结构如下:
com.example.petadmin ├── config ├── controller ├── entity ├── mapper ├── service │ └── impl ├── common │ ├── Result.java │ └── JwtUtil.java └── PetAdminApplication.javapom.xml核心依赖:
| 依赖 | 用途 |
|---|---|
| spring-boot-starter-web | Web支持 |
| mybatis-plus-boot-starter | ORM,减少SQL |
| mysql-connector-java | MySQL驱动 |
| jjwt-api/impl/jackson | JWT生成解析 |
| lombok | 减少实体类样板代码 |
application.yml数据源配置:
server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/pet_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleteFlagurl里必须带serverTimezone=Asia/Shanghai,否则连接MySQL 8会报时区错误。这些配置都是实际踩坑后确定的,少一个都可能让你多折腾一小时。
3.2 宠物模块的增删改查实现
Controller层我习惯写得很薄,只做参数接收和结果返回:
@RestController @RequestMapping("/api/pet") public class PetController { @Autowired private PetService petService; @GetMapping("/page") public Result page(@RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "10") Integer pageSize, String keyword, Long breedId, Integer status) { return Result.success(petService.pagePet(pageNum, pageSize, keyword, breedId, status)); } @PostMapping public Result add(@RequestBody @Validated Pet pet) { petService.addPet(pet); return Result.success(); } @PutMapping public Result update(@RequestBody @Validated Pet pet) { petService.updatePet(pet); return Result.success(); } @DeleteMapping("/{id}") public Result delete(@PathVariable Long id) { petService.deletePet(id); return Result.success(); } }分页参数一律从页码和size走,不要自己再用PageHelper,MyBatis-Plus自带分页插件。配置分页拦截器时别忘注入PaginationInnerInterceptor,否则分页失效,这是特别常见的新手坑。
Service层记得加事务注解@Transactional。提交领养申请时,要同时修改宠物状态和插入领养记录,只用Controller直接操作多个mapper一定会有隐患。在AdoptionService里完成事务控制,保证“申请失败不会改动宠物状态”。
3.3 登录鉴权与全局异常处理
这个项目没有引入Spring Security,而是用JWT + 拦截器实现登录态校验。原因很直接:课程设计重点是业务逻辑,Spring Security那套过长的过滤器链会分散精力。但完全不鉴权又说不过去,所以我写了JwtUtil工具类,登录成功后生成token,前端每次请求通过Authorization头带过来,拦截器统一校验。
核心逻辑:
- LoginController校验用户名密码,生成token返回;
- WebConfig注册拦截器,放行 /api/login,其他路径都校验;
- 拦截器解析失败时抛出BusinessException,全局异常处理器转成401响应。
全局异常处理类:
@RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(BusinessException.class) public Result handleBusiness(BusinessException e) { return Result.error(e.getCode(), e.getMessage()); } @ExceptionHandler(Exception.class) public Result handleException(Exception e) { return Result.error(500, "系统繁忙"); } }这样前端拿到任何异常都是统一的JSON结构,弹提示就行,不用到处try catch。
3.4 后端开发中常见问题
我整理了几个实际编码中遇到的问题。
第一,实体类字段和数据库字段映射。MyBatis-Plus默认开启驼峰转下划线,所以Java里的petName能映射到pet_name。如果你自己写XML,别忘记在application.yml里开启map-underscore-to-camel-case。
第二,Jackson序列化时间。默认情况下LocalDateTime会序列化成数组,很难看。需要在application.yml里配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8第三,跨域问题。开发阶段前端跑在5173,后端8080,直接请求会被浏览器拦截。我在后端写了CorsConfig放行指定origin,或者更推荐在前端Vite配置代理,后文会讲。
第四,上传文件大小限制。宠物头像如果走本地上传,Spring Boot默认单文件最大1MB,需要手动改spring.servlet.multipart.max-file-size,否则大图传不上去。
4. Vue前端开发:从环境配置到页面落地
4.1 环境准备与安装依赖
前端我选Vue 3 + Vite + Element Plus,因为Vite启动速度快,模板比Vue CLI干净。Node.js建议用18 LTS版本,不要用太老的12,否则依赖安装时会报错。装好Node后先换镜像:
npm config set registry https://registry.npmmirror.com然后创建项目:
npm create vite@latest pet-web -- --template vue cd pet-web npm install npm install element-plus axios vue-router@4 pinia第一次执行npm install如果报错,多半是Node版本和依赖不匹配,先执行node -v查看版本。另一个常见问题是网络问题导致安装一半失败,删掉node_modules和package-lock.json,重新install即可。
这里要提一下:很多人以为npm install成功就能直接启动,实际还需要在main.js里注册Element Plus:
import { createApp } from 'vue' import App from './App.vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import router from './router' import { createPinia } from 'pinia' const app = createApp(App) app.use(ElementPlus) app.use(router) app.use(createPinia()) app.mount('#app')4.2 路由、状态管理与axios封装
路由设计不复杂:
const routes = [ { path: '/login', component: Login }, { path: '/', component: Layout, redirect: '/pets', children: [ { path: 'pets', component: PetList }, { path: 'pets/edit/:id', component: PetEdit }, { path: 'adoptions', component: AdoptionAudit } ] } ]编辑页面我用路由参数id,而不是用Pinia去存当前编辑对象。原因是用户刷新页面时,Pinia里没数据会变成空白,而URL里的参数能保证刷新后依然拿到正确的ID。这是“vue路由参数”一个非常实际的使用场景。
axios封装是每个项目必须做的。我建了src/utils/request.js,实例设置baseURL为/api,请求拦截器里从localStorage取token塞到Authorization头,响应拦截器里对HTTP 401做跳转登录,对业务code非200统一用ElMessage提示。否则每个页面都要写一遍错误处理,代码会翻倍。
4.3 宠物管理页面实战
宠物列表页是核心页面,包含:搜索区(keyword、品种、状态)、新增按钮、el-table列表、el-pagination分页。表格列:宠物名、品种、性别、年龄、状态、操作。状态列用el-tag展现不同颜色,例如“可领养”绿色、“已领养”蓝色。
新增/编辑我用el-dialog内嵌el-form,表单里宠物名必填,品种用el-select,数据从后端品种接口拉。提交前先通过表单校验,再调用后端接口。这里的“前端校验 + 后端校验”双保险很重要,前端提升体验,后端保证安全。宠物名、品种、状态都是必填字段,失血过多需要明确提示用户。
如果你想快速看到页面效果,可以先写死一点假数据,把表格和分页调通再对接接口。但我不建议一直用假数据,因为联调阶段最常见的“数据格式对不上”问题,一定是真实接口才能暴露出来的。
4.4 联调时常见错误
本地联调时,Vite默认端口是5173,后端是8080,最简单的解决办法是在vite.config.js里配代理:
server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }这样前端请求/api/xxx就转发到了后端。我见过很多人在代码里写死http://localhost:8080,然后打开浏览器报跨域,其实就是没配代理或者代理生效没重启。改完配置文件一定要重启dev server。
另一个常见错误是请求方式对不上。后端用@RequestBody接收JSON,前端axios.post默认就是JSON,没问题。但我见过有同学手动把Content-Type改成application/x-www-form-urlencoded,后端立刻报HttpMessageNotReadableException。建议前端不要自定义Content-Type,用axios默认的application/json即可。
时间格式问题也很常见:后端返回的LocalDateTime在JSON里默认带T,页面显示“2024-05-01T10:30:00”,很难看。我在后端做了jackson全局配置,前端也可以在表格列里做格式化,两选一。推荐后端直接格式化好,前端少写方法。
5. 前后端联调、打包与部署实录
5.1 接口规范与联调流程
前后端分离项目,接口规范是最重要的“合同”。这套项目统一返回:
{ "code": 200, "message": "success", "data": { } }凡是code不是200,前端一律在axios响应拦截器里弹出message。注意分页接口的返回格式也要固定,我建议data里包含records和total两个字段,前端分页组件直接塞。
联调前先做一件事:打开后端控制台,把MyBatis-Plus的SQL日志打开。之前yml里配置的StdOutImpl就会打印每条SQL。如果前端页面数据不对,先看后端SQL执行结果,能省掉大量无意义的猜测。其次是让前端先用Postman或Apifox把“宠物分页查询”等关键接口测通,再写页面代码,避免两边同时出问题不知道怀疑谁。
5.2 前端打包与Spring Boot集成部署
如果你只需要交付一个jar包,最省事的做法是把前端dist目录复制到后端src/main/resources/static下,然后重新打包:
npm run build cp -r dist/* ../backend/src/main/resources/static/ cd ../backend mvn clean package -DskipTests java -jar target/pet-admin.jar启动后直接访问http://localhost:8080/ 就是前端页面,静态资源和/api接口同域,也不存在跨域了。
如果项目打算长期运行且访问量稍大,我更喜欢用Nginx分开部署。前端静态文件交给Nginx,接口反向代理到Spring Boot:
server { listen 80; server_name pet.example.com; root /opt/pet-web/dist; index 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; } location / { try_files $uri $uri/ /index.html; } }注意try_files那句不能省,否则刷新页面到/pets/edit/1会出现404,因为前端是history路由。
5.3 运行时问题排查与优化
项目跑起来后遇到最多的是两类问题:接口报错和页面慢。
接口报错先看后端日志,日志里能看到异常栈,基本能定位。如果是数据库连接失败,检查MySQL服务是否启动、账号密码是否正确。如果是404,先确认请求路径和自己的Controller映射是否一致。如果是空指针,大概率是查出来对象为null,比如宠物删除后领养记录还指向它,代码里需要先判断再取值。
页面慢通常不是后端慢,而是前端打包后没做路由懒加载。我在路由配置中用动态import,例如:
const PetList = () => import('../views/PetList.vue')这样首屏只加载通用框架,进入对应路由才加载对应JS,体验会明显提升。数据库层面给查询多的表加了索引,因为数据量不大,暂时没有更深的优化。另外JVM启动参数可以适当设置,比如java -jar -Xms256m -Xmx512m pet-admin.jar,避免个人服务器内存不够被杀掉。
6. 文档、源码组织与后续扩展
6.1 文档怎么写才有人看
这套项目的一大卖点是“附文档”,但很多文档就是使用说明粘贴了一遍,没价值。我按接手的人最需要的顺序来写:
- 项目介绍:一句话说明系统能做什么。
- 技术栈:写清楚后端Spring Boot版本、JDK版本、前端Node版本、MySQL版本。
- 环境要求:JDK 8+、Node 16+、MySQL 8。
- 快速启动:分后端和前端写,包含每个命令。
- 数据库导入:init.sql和data.sql的导入步骤,特别强调先建库再执行脚本。
- 接口说明:把主要接口列成表格,包含地址、请求方式、参数、返回示例。
- 常见问题:收录自己实战中遇到的问题。
文档不是给别人看的,更是给三个月后的自己看的。很多同学写完代码后根本不想写文档,等到答辩前一天才匆忙补,效果很差。我的习惯是每个模块完成就顺手写一段,最后汇总,这样文档永远是热乎的。
6.2 源码目录如何组织才能让接手的人不骂人
最终交付的源码目录结构建议是:
pet-management/ ├── backend/pet-admin/ # Spring Boot后端 ├── frontend/pet-web/ # Vue前端 ├── sql/ │ ├── init.sql │ └── data.sql └── docs/ ├── 快速启动.md ├── 部署文档.md └── 接口文档.md不要把所有代码平铺在一个文件夹里,前后端至少分开。后端不要传target、前端不要传node_modules,这些是常识,但在课程设计里总是有人打包传上去,导致别人解压后跑不起来以为代码有问题。
后端包名用com.example.petadmin清晰直观。controller统一处理请求,service写业务,mapper只做数据库操作。前端按照views、router、api、utils分层。组件命名用大驼峰,文件名和组件名保持一致。这些规则虽然简单,但对代码可读性提升非常大。
6.3 实际开发过程中我个人比较坚持的几个习惯
第一,接口路径统一以/api开头,方便后续做网关或反向代理。第二,涉及手机号等敏感信息,日志里不要打印明文。宠物管理系统虽然不涉及支付,但用户手机号也是敏感数据。第三,改完代码一定要跑一遍回归测试,至少把宠物增删改查和登录领养这五条主流程走一遍,不要只测自己改的那个接口。
另外,很多初学者会忽略“测试数据”的重要性。data.sql里的演示数据要有真实感,比如宠物名“小橘”“旺财”、品种“中华田园猫”“金毛”,健康状况写“已驱虫”“疫苗齐全”。这种细节在答辩演示时特别加分,因为老师一看就明白系统是能落地的,而不是拿几张空表凑数。
6.4 后续功能还能怎么扩展
这套系统的骨架搭好后,扩展方向很多。比如给宠物增加多图上传,可以接MinIO或阿里云OSS;领养审核流程增加短信通知,可以用阿里云短信服务;想做一个宠物视频展示模块,可以研究HLS切片后在Vue里用video.js播放m3u8流。这里要提醒的是,每加一个功能最好保持“后端接口 + 前端页面 + 数据库脚本 + 文档更新”同步,不要只写代码不补脚本和文档,否则项目会越来越难维护。
我个人的体会是,做管理系统最大的收获不是学会某个框架,而是建立起“用工程化思维拆解一个真实需求”的能力。源码能跑只是及格,能说清楚每个表为什么这么建、每个接口为什么这么设计、每个坑是怎么踩出来的,才是这套项目真正值钱的地方。希望这篇记录能帮你在自己的宠物管理系统项目里少走几步弯路。