☰
SpringBoot+Vue宠物管理系统全栈实战:从架构设计到部署上线
2026/9/25 19:39:28 网站建设 项目流程

我参与过不少基于SpringBoot+Vue的管理系统项目,这次要拆解的是一个宠物管理系统。说实话,宠物管理系统算是SpringBoot+Vue全栈入门里非常经典的项目选题,很多人做毕设或者练手都会选它,因为它功能边界清晰、领域模型直观,又覆盖了前后端分离开发的大部分核心知识点。我打算结合自己实操踩过的坑,把这个项目从需求分析、架构设计到具体代码实现、部署上线整个链路完整过一遍,希望能给准备做同类项目的同学一些实在的参考。

如果你正在筹备毕设,或者想通过一个完整项目打通前后端技能,这套宠物管理系统是个不错的选择。它能解决宠物信息登记、领养审核、寄养预约、医疗档案管理等实际问题,用到的核心技术栈就是项目标题里的SpringBoot和Vue,上手路径比较明确,做出来的东西也看得见摸得着。

1. 项目整体设计与技术选型

1.1 为什么选定SpringBoot+Vue这套组合

先说后端。SpringBoot在Java生态里的地位不用多讲,它最核心的价值是帮我们把繁琐的配置简化掉。以前用SSM框架搭一个项目,要写一堆XML配置,还得自己处理各种Bean的装配,SpringBoot直接用自动配置和起步依赖把这些事全包了,一个带内嵌Tomcat的Jar包就能跑起来,部署非常轻量。对于宠物管理系统这种以CRUD为主的业务系统,SpringBoot的快速开发特性特别契合。

前端选Vue,主要看重的是它的组件化开发模式和响应式数据绑定。宠物管理系统的页面交互不算特别复杂,但涉及表单校验、列表筛选、弹窗确认、状态更新这类高频操作,用Vue的双向绑定能省掉大量操作DOM的代码。再加上Vue生态里的Element Plus组件库,表格、表单、分页、对话框这些都能直接拿来用,开发效率是真的快。

选择这套组合也有一些现实考量。首先是学习资料丰富,不管遇到什么问题几乎都能搜到解决方案;其次是岗位需求量大,掌握这套技术栈对找工作有实际帮助;再有就是社区活跃,Vue有官方维护的DevTools调试工具,SpringBoot的版本迭代也会兼容老项目的迁移,前面热搜词里就有不少关于版本配置、依赖安装的讨论,说明大家都在用、都在维护,你踩坑也更容易找到同伴。

1.2 系统功能模块拆解

宠物管理系统从业务上可以分成两大端的视角。管理员端负责全局管理,普通用户端负责日常使用。

  • 宠物信息管理:登记宠物基本信息(名称、品种、年龄、性别、毛色、健康状况)、上传宠物图片、维护疫苗记录和驱虫记录。
  • 领养管理:发布待领养宠物、用户提交领养申请、管理员审核领养资格、记录领养回访情况。
  • 寄养管理:配置寄养笼位、预约寄养时间、生成寄养订单、记录寄养期间的喂养和健康状态。
  • 医疗档案:记录每次就诊的病症描述、诊断结果、用药明细和治疗费用。
  • 用户中心:用户注册登录、个人资料维护、我的申请记录、我的寄养订单。
  • 公告资讯:发布平台公告、宠物养护知识文章。

我实际做的时候把角色分成了三种:超级管理员、普通员工、注册用户。权限控制前后端都要做,后端用Spring Security + JWT校验接口权限,前端根据角色动态渲染菜单和按钮,比如普通用户看不到管理后台的入口,员工只能操作自己职责范围内的事务。

1.3 数据库设计思路

数据库设计是这类系统最关键的一环。我建议一开始就按照业务实体来拆表,不要图省事把所有字段堆到一张大表里。核心表大致包括:

  • pet:宠物表,存储基本信息、图片URL、状态(待领养/已领养/寄养中)。
  • user:用户表,存储账号、密码(BCrypt加密)、角色、联系方式。
  • adoption_application:领养申请表,关联宠物和用户,记录申请状态(待审核/通过/拒绝)。
  • foster_order:寄养订单表,记录入店时间、离店时间、费用、状态。
  • medical_record:医疗记录表,关联宠物ID,记录诊断和用药。
  • category:宠物分类表,比如猫、狗、兔子、鸟类,方便前端筛选。

外键关联我没在数据库层面强加太多限制,主要靠应用层逻辑维护一致性。这么做是为了后面扩展方便,比如以后要接消息通知或者引入工作流引擎,数据模型不会被外键绑死。但是索引一定要建好,pet表的状态字段、adoption_application表的宠物ID和用户ID都要加索引,等数据量上来查询速度差距还是很明显的。

2. 后端核心细节与SpringBoot实现要点

2.1 项目结构划分

后端项目结构建议按功能模块分包,不要按技术层次分包。按技术层次分包(controller/service/mapper各放一个包)在项目初期看着整齐,但业务一旦复杂起来,改一个功能要在好几个包之间来回跳,非常难受。

com.pet.management ├── common // 通用类:统一返回结果、异常处理、常量 ├── config // 配置类:跨域、安全、文件上传 ├── controller // 控制层:接收请求、返回结果 ├── service // 业务层:核心业务逻辑 │ └── impl // 业务实现类 ├── mapper // 数据访问层(MyBatis-Plus的Mapper接口) ├── entity // 实体类 ├── dto // 数据传输对象:请求参数和响应结果 └── utils // 工具类:JWT工具、文件处理工具

这个结构里我特别要强调一下dto包的作用。很多新手习惯直接把entity实体类暴露给前端当返回结果,这样做隐患很大,实体类字段通常跟数据库表结构一一对应,里面可能会有你不想返回的字段(比如用户密码、盐值),而且一旦表结构变动,前端接的接口也跟着变,耦合太紧。用DTO做一层转换,实体可以自己演化,接口可以保持稳定。

2.2 SpringBoot关键配置与启动类

用IDEA新建SpringBoot项目时,建议直接去Spring Initializr选择依赖,免去了手动加坐标的麻烦。核心依赖如下:

<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency>

application.yml里的几个配置项,我要提醒几个容易踩坑的地方:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/pet_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: yourpassword servlet: multipart: max-file-size: 10MB max-request-size: 20MB jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0

数据库连接串里的serverTimezone=Asia/Shanghai必须加上,否则高版本MySQL驱动会报时区错误。spring.jackson.date-format用来统一后端返回给前端的日期格式,不配置的话前端拿到的时间戳格式比较难解析。logic-delete-field是MyBatis-Plus的全局逻辑删除配置,这一点非常实用。宠物管理系统的数据删除很多其实是逻辑删除,比如下架一条宠物信息,你不想真的把数据库记录删掉,加一个deleted字段标记就行,配置好之后MP会在所有查询里自动追加WHERE deleted = 0。

2.3 权限认证与拦截器设计

宠物管理系统涉及用户登录、领养审核这些有权限区分的操作,所以认证授权必须做。我采用的是Spring Security + JWT的方案,Security负责过滤链路和密码加密,JWT负责无状态认证。

登录流程是这样的:用户提交账号密码到/api/auth/login,后端校验通过后用JWT工具类生成一个包含用户ID和角色的token返回给前端。前端把token存到localStorage里,每次请求在拦截器里加上Authorization: Bearer <token>头。后端有一个JwtAuthenticationFilter继承OncePerRequestFilter,在每次请求进来时解析token、把用户信息放到SecurityContext里,这样Controller里通过@AuthenticationPrincipal就能拿到当前用户。

具体代码逻辑简洁如下:

public class JwtAuthenticationFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws IOException, ServletException { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); try { Claims claims = JwtUtil.parseToken(token); request.setAttribute("userId", claims.get("userId")); request.setAttribute("role", claims.get("role")); } catch (Exception e) { // token无效,直接放行,后面接口会做权限校验 } } chain.doFilter(request, response); } }

SecurityConfig里要放行登录接口、注册接口、宠物查看接口,其余接口全部认证后访问。用requestMatchers方法配置放行路径时要注意写法,"/api/auth/**"这种通配符表示匹配该路径下所有子路径,不要漏了/**导致404。

2.4 宠物管理的CRUD与分页查询实现

宠物列表是系统里最核心的接口。前端需要支持按品种筛选、按状态筛选、搜索关键词、分页加载,后端对应的是一个带条件查询的分页接口。我用MyBatis-Plus的LambdaQueryWrapper来构建查询条件,用分页插件实现物理分页。

先讲分页插件配置,热搜词里也有提到“mybatis的分页插件的用法 springboot”,我在这里具体说下:

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }

配置好之后,分页查询就非常简单了:

public IPage<PetVO> getPetPage(PetQueryDTO query) { Page<Pet> page = new Page<>(query.getPageNum(), query.getPageSize()); LambdaQueryWrapper<Pet> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.hasText(query.getStatus()), Pet::getStatus, query.getStatus()) .eq(query.getCategoryId() != null, Pet::getCategoryId, query.getCategoryId()) .like(StringUtils.hasText(query.getKeyword()), Pet::getName, query.getKeyword()) .orderByDesc(Pet::getCreateTime); return petMapper.selectPage(page, wrapper); }

这里要注意,eq方法第一个参数是boolean条件,条件成立时才拼接SQL。这样前端不传某个筛选条件时,SQL就不会带上对应的WHERE子句,很优雅。还有一个细节是Page对象的泛型类型,如果返回的是PetVO而不是Pet,需要额外做一次实体到VO的转换,直接用BeanUtils.copyProperties复制字段即可。

2.5 文件上传与图片处理

宠物上传照片是刚需。我在做的时候,图片文件向导到服务器的/upload/pet/目录,数据库里只存相对路径,比如/upload/pet/2024/06/01/xxx.jpg。这样有几个好处,数据库字段长度可控、前后端通过统一前缀拼接完整URL、以后如果要迁移到OSS云存储只需改上传逻辑和URL前缀,不影响已有数据。

@PostMapping("/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty()) { return Result.error("上传文件不能为空"); } String originalFilename = file.getOriginalFilename(); String suffix = originalFilename.substring(originalFilename.lastIndexOf(".")); if (!allowedSuffixSet.contains(suffix)) { return Result.error("不支持的图片格式"); } String ymd = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy/MM/dd")); String dirPath = uploadDir + "/" + ymd; File dir = new File(dirPath); if (!dir.exists()) { dir.mkdirs(); } String newFileName = UUID.randomUUID().toString().replace("-", "") + suffix; file.transferTo(new File(dir, newFileName)); return Result.success("/" + ymd + "/" + newFileName); }

规范上面代码,有几点经验值得分享。一是文件后缀必须白名单校验,我遇到过有人直接传.html或者.jsp后缀的文件名,如果服务器配置有缺陷会被解析执行,这是很严重的安全隐患。二是文件名绝对不能使用用户上传的原始名称,否则会有路径穿越和重名覆盖问题,用UUID重新生成最省心。三是限制文件大小在前端和后端都要做,前端限制是为了用户体验,后端限制才是真正的安全底线,我上面的max-file-size配置就是干这个用的。

3. 前端Vue实现与页面落地

3.1 项目初始化与依赖安装

前端我用的Vue 3 + Vite + Element Plus + Pinia + Vue Router。Vite创建项目的命令:

npm create vite@latest pet-management-frontend -- --template vue cd pet-management-frontend npm install npm install element-plus @element-plus/icons-vue pinia axios vue-router

Vue的开发环境配置是很多新手的第一个卡点,热搜里也有“vue安装及环境配置”和“vue安装依赖”的词条。我建议用nvm来管理Node.js版本,因为Vite对Node版本有要求(18+),你电脑上如果还装了别的老项目,直接装最新的Node可能会破坏其他项目环境。Vue 3不要再使用Vue 2时代的main.js里new Vue()写法,而是用createApp函数创建实例,挂载时要用.use()方法安装router和pinia插件。

vite.config.js里建议配置路径别名,以后引入组件和工具函数都会方便很多:

import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' import path from 'path' export default defineConfig({ plugins: [vue()], resolve: { alias: { '@': path.resolve(__dirname, './src') } }, server: { port: 3000, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })

代理配置非常关键,它解决了前后端联调时的跨域问题。开发阶段前端跑在3000端口,后端跑在8080端口,如果不配置代理,浏览器的同源策略会拦截所有请求。配置完成后前端代码里请求/api/login,Vite会把请求转发到http://localhost:8080/api/login,绕过跨域限制。

3.2 路由设计与菜单权限控制

路由设计上我区分了公开路由和需要登录的路由。公开路由包括登录页、注册页、宠物展示页,需要登录的路由包括个人中心、领养申请、寄养订单管理。管理员专属路由还包括用户管理、宠物审核、统计分析。

Vue Router的写法:

const routes = [ { path: '/login', component: Login, meta: { public: true } }, { path: '/pets', component: PetList, meta: { public: true } }, { path: '/admin', component: AdminLayout, meta: { requiresAuth: true, role: 'ADMIN' }, children: [ { path: 'users', component: UserManage }, { path: 'audits', component: AuditManage } ] } ]

路由守卫里做登录校验和角色校验:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const role = localStorage.getItem('role') if (to.meta.requiresAuth && !token) { next('/login') } else if (to.meta.role && to.meta.role !== role) { next('/403') } else { next() } })

这里有一个需要注意的细节:菜单展示不要写死在侧边栏组件里,最好根据当前用户的角色动态生成菜单数组。比如管理员能看到“用户管理”,普通用户看不到。我在项目里是把菜单配置和路由配置分开维护的,菜单数组里每一项带roles字段,前端根据用户角色过滤后再渲染,这样就不需要为不同角色写多套侧边栏组件。

3.3 Axios封装与请求拦截器

宠物管理系统前后端交互非常频繁,Axios一定要封装,不要每个页面都直接axios.get裸调。封装的核心目的有三个:统一baseURL、统一token注入、统一错误处理。

import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' 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 => { if (error.response?.status === 401) { localStorage.clear() router.push('/login') ElMessage.error('登录已过期,请重新登录') } else { ElMessage.error(error.message || '网络异常') } return Promise.reject(error) } )

这个封装里我做了统一返回值的处理。后端接口统一返回Result对象,结构是{ code: 200, data: {...}, message: "success" },拦截器里判断code是否为200,非200则弹出错误消息并reject。这样业务代码里就不用每个页面都写try-catch去处理业务错误了,页面只管成功的数据流。

用户信息在页面里的读取,我更推荐用Pinia来管理。登录成功后把用户信息存到Pinia store里,并且做了持久化处理(存到localStorage),刷新页面后自动恢复。不要在多个页面都单独从localStorage里取值,那样一旦key写错或者字段名调整,所有用到的地方都要改,维护成本高。

3.4 宠物列表页与表格组件的落地

宠物列表页是展示层最典型、也最能体现组件化价值的页面。我用Element Plus的el-table来展示宠物信息,用el-form的inline模式做筛选条件,用el-pagination做分页。

表格里有一个自定义列是宠物图片,这里我用了作用域插槽:

<el-table-column label="宠物照片" width="120"> <template #default="{ row }"> <el-image :src="getFullUrl(row.avatar)" fit="cover" style="width: 60px; height: 60px; border-radius: 8px;" ></el-image> </template> </el-table-column>

getFullUrl这个函数需要注意,因为后端返回的是相对路径,前端需要拼接完整地址才能访问。如果你用了Vite代理,那直接拼接/api前缀即可。但如果文件是通过其他方式存储的(比如对象存储服务),这里的处理逻辑就不能写死,建议将文件访问的基础路径作为一个前端全局配置来统一管理。

还有一个容易忽略的细节是el-table的行点击事件和状态标记。比如一条领养申请,状态是待审核的,可以给整行加一个高亮背景色,已处理的就不高亮,这样管理员一眼就能看到哪些申请需要处理。这个效果用el-table的:row-class-name回调函数实现,根据行数据里的状态字段返回不同的CSS类名。

3.5 领养流程的表单与状态流转

领养申请流程比较典型,我详细说一下。用户看到待领养的宠物列表后,点击“申请领养”会弹出一个表单,包含申请人的住房情况、养宠经验、经济状况等字段。这些字段是为了管理员审核时有依据。

前端提交领养申请后,生成一条adoption_application记录,初始状态为PENDING。管理员在后台看到这条记录后有两个操作按钮:通过或拒绝。点击通过时,后端的处理逻辑里做了一个事务控制,同时更新申请状态和宠物状态,把宠物从“待领养”改成“已领养”。这两个操作必须在一个事务里,不然可能出现申请通过了但宠物状态没更新的脏数据。

@Transactional public void approveAdoption(Long applicationId) { AdoptionApplication application = applicationMapper.selectById(applicationId); if (application == null || !"PENDING".equals(application.getStatus())) { throw new BusinessException("申请不存在或已审核"); } application.setStatus("APPROVED"); application.setAuditTime(LocalDateTime.now()); applicationMapper.updateById(application); Pet pet = petMapper.selectById(application.getPetId()); pet.setStatus("ADOPTED"); petMapper.updateById(pet); }

事务控制在实际项目中必须重视,哪怕像宠物管理系统这种看起来不复杂的业务,也会有这种跨表的联动更新。不加上@Transactional,一旦中间业务逻辑抛异常,数据库就会留下半成品数据,而且这种问题排查难度极大。

4. 联调部署与常见问题排查实录

4.1 前后端联调的关键细节

开发阶段的前后端联调,除了代理配置之外,还有几个细节值得提醒。后端返回的时间格式,如果前端用的Element Plus日期组件是date-picker类型,两者格式不一致会导致解析错误或者默认值填充为空。统一的做法我记得是在配置里设置全局的date-format为yyyy-MM-dd HH:mm:ss,前端的日期组件也统一用value-format指定同样的格式,两边对齐之后就不会出问题。

还有一个是关于接口字段命名的问题。Java的后端习惯是驼峰命名(比如createTime),如果前端请求参数想用下划线风格(比如create_time),后端又没有配置map-underscore-to-camel-case,那么对不上就查不出数据。MyBatis-Plus默认配置是把下划线自动映射到驼峰,所以我建议前后端统一用驼峰命名,前端传参、后端接收、数据库字段都用驼峰,虽然数据库里推荐下划线规范,但Model层映射已经处理好了,实际编程时不必纠结。

4.2 打包构建与本地部署

前端打包运行npm run build,产物会输出到dist目录,里面是静态文件(html、js、css、图片)。后端的打包用Maven的package命令,生成可执行Jar包:

mvn clean package -DskipTests java -jar target/pet-management-0.0.1-SNAPSHOT.jar

这里有一个常见的问题,也是很多新手会纠结的地方:前端打包出来的dist文件怎么处理。有两种方案:如果你有独立的Nginx或者云服务器静态资源托管,那就把dist直接部署到Nginx的html目录下,然后配置反向代理,把/api开头的请求转发到后端的8080端口;如果没有独立的前端服务器,可以直接把dist目录放到后端项目的resources/static下,打包时一起打进Jar里,实现单机部署,访问8080端口直接能看到页面。

我个人的建议是,开发调试阶段用前后端分离部署,Nginx代理很灵活;如果只是做毕设演示或者小规模使用,把前端打进Jar包更省事,不用维护两个服务。下面给一个Nginx配置做参考:

server { listen 80; server_name pet.example.com; location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

这里try_files $uri $uri/ /index.html是Vue前端必须配置的,因为Vue Router的history模式在浏览器访问某个具体路径(比如/admin/users)时,后端找不到对应的静态文件会返回404,必须把所有路由请求都回退到index.html让前端路由接管。

4.3 典型报错与解决方案速查表

我把项目开发过程中遇到的高频问题和解决方案整理成一张表,这批经验是从真实操作中积累的:

报错现象根本原因解决方案
启动时提示Failed to configure a DataSource缺少数据源配置检查application.yml的数据库连接配置是否正确,确认MySQL服务已启动
跨域请求被阻止前后端端口不同开发环境用Vite proxy代理,生产环境用Nginx反向代理
接口返回401token缺失或过期请求拦截器检查Authorization头是否携带;登录页逻辑检查token是否在有效期内
中文乱码编码不一致前端HTML指定charset=UTF-8,后端配置server.tomcat.uri-encoding=UTF-8
上传文件大小超出限制前端或后端限制过低同时在Vite和SpringBoot的multipart配置中调大max-file-size
日期显示为yyyy-MM-ddTHH:mm:ss前后端日期格式不统一后端配置spring.jackson.date-format,前端组件指定value-format
刷新页面404Vite/Router history模式未配置回退Nginx配置try_files $uri $uri/ /index.html

4.4 我从这个项目里收获的实操心得

做完这个宠物管理系统,我在整个过程中体会最深的点是,一个项目真正难的不是写代码本身,而是厘清业务逻辑和做好边界处理。比如领养审核的状态流转,用户在申请期间改主意了怎么办?宠物在寄养期间突然生病了费用怎么算?管理员把用户误删了,关联的申请记录怎么处理?这些问题如果你不在动手写代码前想清楚,后面改起来非常痛苦。

我的建议是,尤其是毕设项目,先把自己的角色当成一个产品经理,把系统要解决的业务问题写清楚,把每个实体类的关系图画清楚,再去写代码。SpringBoot和Vue都是非常成熟的技术,写代码反而是一气呵成的事情。

最后分享一个实用的小技巧:你在本地开发调试时,后端控制台打印的SQL一定要开启。MyBatis-Plus配置上log-impl: org.apache.ibatis.logging.stdout.StdOutImpl,这样每执行一条SQL都会在控制台输出完整的SQL语句和参数值。开发阶段排查分页查不出数据、条件筛选不生效这类问题,看SQL一眼就能定位到是条件拼接问题还是参数传递问题,效率能提升一个档次。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询