ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot+Vue宠物管理系统全栈实战:从架构设计到部署上线

SpringBoot+Vue宠物管理系统全栈实战:从架构设计到部署上线 我参与过不少基于SpringBootVue的管理系统项目这次要拆解的是一个宠物管理系统。说实话宠物管理系统算是SpringBootVue全栈入门里非常经典的项目选题很多人做毕设或者练手都会选它因为它功能边界清晰、领域模型直观又覆盖了前后端分离开发的大部分核心知识点。我打算结合自己实操踩过的坑把这个项目从需求分析、架构设计到具体代码实现、部署上线整个链路完整过一遍希望能给准备做同类项目的同学一些实在的参考。如果你正在筹备毕设或者想通过一个完整项目打通前后端技能这套宠物管理系统是个不错的选择。它能解决宠物信息登记、领养审核、寄养预约、医疗档案管理等实际问题用到的核心技术栈就是项目标题里的SpringBoot和Vue上手路径比较明确做出来的东西也看得见摸得着。1. 项目整体设计与技术选型1.1 为什么选定SpringBootVue这套组合先说后端。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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-security/artifactId /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt/artifactId version0.9.1/version /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependencyapplication.yml里的几个配置项我要提醒几个容易踩坑的地方spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/pet_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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: GMT8 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数据库连接串里的serverTimezoneAsia/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 IPagePetVO getPetPage(PetQueryDTO query) { PagePet page new Page(query.getPageNum(), query.getPageSize()); LambdaQueryWrapperPet 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 ResultString 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 vitelatest pet-management-frontend -- --template vue cd pet-management-frontend npm install npm install element-plus element-plus/icons-vue pinia axios vue-routerVue的开发环境配置是很多新手的第一个卡点热搜里也有“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/loginVite会把请求转发到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宠物照片 width120 template #default{ row } el-image :srcgetFullUrl(row.avatar) fitcover stylewidth: 60px; height: 60px; border-radius: 8px; /el-image /template /el-table-columngetFullUrl这个函数需要注意因为后端返回的是相对路径前端需要拼接完整地址才能访问。如果你用了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指定charsetUTF-8后端配置server.tomcat.uri-encodingUTF-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.html4.4 我从这个项目里收获的实操心得做完这个宠物管理系统我在整个过程中体会最深的点是一个项目真正难的不是写代码本身而是厘清业务逻辑和做好边界处理。比如领养审核的状态流转用户在申请期间改主意了怎么办宠物在寄养期间突然生病了费用怎么算管理员把用户误删了关联的申请记录怎么处理这些问题如果你不在动手写代码前想清楚后面改起来非常痛苦。我的建议是尤其是毕设项目先把自己的角色当成一个产品经理把系统要解决的业务问题写清楚把每个实体类的关系图画清楚再去写代码。SpringBoot和Vue都是非常成熟的技术写代码反而是一气呵成的事情。最后分享一个实用的小技巧你在本地开发调试时后端控制台打印的SQL一定要开启。MyBatis-Plus配置上log-impl: org.apache.ibatis.logging.stdout.StdOutImpl这样每执行一条SQL都会在控制台输出完整的SQL语句和参数值。开发阶段排查分页查不出数据、条件筛选不生效这类问题看SQL一眼就能定位到是条件拼接问题还是参数传递问题效率能提升一个档次。
返回列表