ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue前后端分离的流浪动物领养平台开发实践

SpringBoot+Vue前后端分离的流浪动物领养平台开发实践 打开系统后台最先看到的是领养申请的审批列表每一条记录都备注着申请人的居住条件、工作状态和意向动物编号前台页面则是一张张流浪猫狗的信息卡片照片、性格描述、驱虫疫苗记录清清楚楚。这就是我基于 SpringBoot 和 Vue 做的动物领养平台一套完整的流浪动物救助与领养匹配系统。这套系统既是一个贴近真实场景的计算机毕业设计题也可以直接改造成社区宠物帮助小组的运营工具。前后端分离、流程管理、权限控制、文件上传这些开发中绕不开的问题在项目里都会真刀真枪遇上一轮。想完整走一遍开发链路、又想做出有话题度的作品选这个题目很合适。1. 项目整体设计与思路拆解1.1 这个平台到底要解决什么问题流浪动物救助最大的痛点不是没人想帮忙而是信息散、流程乱、责任不清。志愿者在群里发照片热心人想领养却不知道猫狗打没打疫苗救助人担心遇到不靠谱的领养者机构又缺少一个统一的审核手段。做这个平台不是单纯为了“做一个管理系统交差”而是把线下几个人靠微信聊天解决的问题搬到线上变成一条可控的流程。所以我的第一版需求拆得很直接公开动物信息让用户看到动物状态用户提交领养申请管理员审核申请救助人登记新动物领养成功后还能留一个回访记录入口。这五个核心动作对应五张核心表再多加一张公告表做信息发布。这套设计对毕业设计来说足够完整又不会因为步子迈得太大导致做不完。1.2 系统角色与功能梳理我把使用者分成三类角色每个角色看到的内容和能做的操作完全不同。普通游客和登录用户只能浏览动物列表、查看详情、收藏登录后的用户才能提交领养申请。管理员负责审核申请、管理动物上下架、发布公告、管理用户状态。救助人角色偏向信息录入可以提交救助的流浪动物但无法直接操作其他内容。功能上分成了六个模块动物信息管理、领养申请审核、用户管理、公告管理、救助登记、数据统计。毕业设计答辩时最容易被问的就是“你的系统有哪些角色、每个角色能做什么”提前把角色功能矩阵整理清楚回答起来就非常顺。这里我用表格列一下模块普通用户救助人管理员动物浏览/检索支持支持支持动物信息录入不支持支持支持领养申请提交支持支持支持领养申请审核不支持不支持支持公告管理阅读阅读发布/删除用户管理无无停用/权限设置在代码层面我不会把权限写死在前端页面而是前后端都做校验。前端根据登录用户角色控制菜单显示后端在每个写操作的接口上做角色判断避免出现“直接调接口就能绕过审核”的低级漏洞。1.3 为什么选 SpringBoot Vue 这套组合这个问题在毕设答辩里几乎必被问到。选 SpringBoot 是因为它把 Spring 那一套复杂的配置简化到了极致内置 Tomcat一个 jar 包就能把后端跑起来非常适合快速交付。SpringBoot 还提供了 spring-boot-starter-data-jpa 或 mybatis-spring-boot-starter 这类起步依赖数据库操作不需要自己写一堆 xml 配置文件开发效率明显高一个档次。Vue 负责前端界面核心卖点是组件化和响应式数据绑定。我只需要维护一份数据状态页面会自动跟着更新不用像 jQuery 时代那样手动操作 DOM。前后端通过 RESTful API 通信数据格式统一用 JSON。这样的技术栈也是业界真实项目里使用较多的方案写在简历上有说服力。如果当年用 JSP 做单体应用页面和后端代码揉在一起改一个样式都可能要重启服务。前后端分离之后前端跑在 8080 端口后端跑在 8081 端口各自能独立开发调试。我本地前端用 Vue 自带的开发服务器后端用 SpringBoot 内嵌的 Tomcat两边接口一对接开发体验确实舒服。2. 核心细节解析与实操要点2.1 数据库表结构怎么设计表结构是这类系统最容易出问题的地方。我的建议是别一开始就追求特别复杂的表关系先把主体流程串起来。我第一版设计了六张核心表用户表 account、动物表 animal、领养申请表 adoption_apply、救助登记表 rescue_record、公告表 notice、动物图片表 animal_image。用户表不能只存用户名密码还要存真实姓名、联系电话、微信号、居住城市、住房类型、家庭成员情况。这些字段在领养审核时是判断依据直接决定这个人适不适合养宠物。我当时把住房类型设计成枚举值自有住房、租房、集体宿舍。很多做这个题目的同学容易漏掉这些信息到了审核环节才发现没数据可看只能干瞪眼。动物表要区分状态我用 status 字段标记0 待救助、1 待领养、2 已领养、3 已下架。动物的性格、疫苗情况、驱虫情况、绝育情况单独建列方便前端做筛选。图片不直接塞到动物表里而是单独放一张图片表一个动物可以关联多张图片这也符合真实场景救助人往往会上传好几张不同角度的照片。领养申请表除了基础的外键关联还需要记录申请理由、生活习惯描述、审核状态、审核意见、审核时间。这里有几个关键点申请人和动物是多对一关系一个动物同时只能有一条“待审核”或“审核通过”的申请否则会造成重复领养。我在数据库层面用一个联合唯一索引约束了 animal_id 和 status保证同一只动物不会同时通过多个申请。2.2 登录认证与角色权限后端登录方案我选的是 JWT没有用传统的 Session。理由很简单前后端分离后后端接口要同时面对 PC 页面和手机端小程序如果靠 Session 维护登录态就必须处理跨域携带 Cookie 的复杂性而且每次重启服务 Session 就丢了。JWT 把用户信息和过期时间算成一个加密串返回给前端前端每次请求带着这个串后端验签通过就放行天然适合分布式部署。SpringBoot 里我引入了 spring-boot-starter-security 做安全性控制先把所有接口的保护规则用代码配置好。登录接口、公开的动物列表接口、图片访问接口放行其余请求都要求登录。然后通过自定义注解加 AOP 拦截在 service 层判断当前登录用户角色是不是管理员避免前端隐藏按钮就能绕过权限。密码存储必须加密。明文存数据库是最低级的错误答辩老师一看就会觉得项目没有安全意识。我用的是 BCryptPasswordEncoder同样的密码每次加密结果不同数据库就算泄露也没法直接逆推。如果项目里用了 JPA可以直接在实体类的 password 字段上做转换注解存库时自动加密查询时自动忽略。2.3 领养流程的状态流转领养不是一锤子买卖一整套流程至少要经过“提交申请、管理员审核、线下沟通、领养完成、后续回访”这几个环节。为了实现这个流程我把 adoption_apply 表的 status 设计成这些状态WAIT_CHECK待审核申请提交后自动进入该状态管理员未处理。PASSED审核通过状态变为已通过等待线下接触。REJECTED审核不通过建议管理员填写原因比如“申请人暂不具备饲养条件”。FINISHED领养完成动物状态同步改为已领养。CANCELED用户取消申请防止误提交。这里最容易忽略的是状态同步问题。申请状态变成 FINISHED 的同时animal 表里的 status 必须跟着变成已领养。如果只改了申请状态动物依然显示待领养前端就会出现多人重复申请的情况。我是在 service 层的同一次数据库事务里同时更新两张表要么都成功要么都回滚保证数据一致性。还有一点值得做进去的是操作日志。管理员每次审核通过或驳回我都往一张 audit_log 表里插入一条记录字段包括操作人、审核单号、动作、备注。表面上看起来只是多了一张表实际上答辩时这是非常加分的点体现出你考虑了数据的可追溯性也方便后续排查问题。2.4 动物匹配的核心思路平台名称带了“匹配”两个字那不能只做一个简单的列表查询。我的做法是提取动物的标签体系品种、毛色、体型、年龄层、性格、是否适合家有小孩、是否适合与猫共处等。用户在提交领养申请时选择自己的条件偏好后端就把动物表按条件组合打分分数高的排前面。打分逻辑不复杂用户的条件和动物的标签逐项比对每一项命中加 1 分最后按分数倒序返回。这个逻辑放在 service 层处理页面只需要传参数。虽然不如推荐算法那么高级但胜在可解释性强而且代码量不大。如果想让项目更有亮点可以在前台表单里增加“城市”和“住房类型”两个匹配项让系统的筛选结果更贴近真实领养场景。匹配功能我只做了排序展示并没有阻止用户看不符合条件的信息。因为领养条件太死板反而会把潜在好心人挡在门外审核员可以在后台人工判断特殊情况。技术方案懂得展示也要给业务留有余地这一点我在代码注释里写得很清楚。3. 实操过程与核心环节实现3.1 后端工程搭建与目录结构创建后端工程时我直接在 Spring Initializr 选了 SpringBoot 2.7.x 版本。这个版本兼容性好网上资料多比直接上 3.x 遇到问题的概率低。依赖选了 Spring Web、Spring Validation、MyBatis、MySQL Driver、Lombok。目录结构按常见分层来controller、service、mapper、entity、common。entity 层对应数据库表用 Lombok 的 Data 注解省掉 getter settermapper 层负责数据库交互service 层写业务逻辑controller 层只做参数接收和结果封装。这样分层的意义在于业务逻辑和数据库操作解耦后续替换数据库或者调整接口参数不会互相影响。一个简单的动物查询接口代码长这样RestController RequestMapping(/api/animal) public class AnimalController { Resource private AnimalService animalService; GetMapping(/list) public Result list(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size) { PageInfoAnimalVO data animalService.queryAnimalPage(page, size); return Result.success(data); } GetMapping(/detail/{id}) public Result detail(PathVariable Long id) { return Result.success(animalService.getAnimalDetail(id)); } }这种结构看懂之后会发现在项目里到处复用。新增一个模块就是复制这个套路建实体、写 Mapper、写 Service、写 Controller。我刚做完第一个动物模块后面公告、用户、申请模块都是半小时一个。3.2 前端工程搭建与页面结构前端我用 Vue CLI 创建项目选择了 Vue 2 版本。原因很实在Element UI 对 Vue 2 的支持最成熟组件齐全出问题的概率小。如果你们老师没强制要求 Vue 3稳妥起见可以用 Vue 2如果希望项目看起来更新潮Vue 3 配 Element Plus 也完全可以基本开发套路一致。前端目录我按页面来拆views 下放页面组件router 目录配置路由api 目录统一封装请求components 目录放可复用的卡片、表单项。路由有两类静态路由给游客动态路由在登录成功后根据角色注入。比如管理员登录后才注册后台管理相关的路由权限和菜单通过动态路由统一控制这是最常见的 Vue 后台权限方案。下面是前端调用后端列表接口的一个简单封装import request from /utils/request export function getAnimalList(params) { return request({ url: /api/animal/list, method: get, params }) }request 模块里用 axios 创建实例统一配置 baseURL 和请求头。我在这里做了一层拦截器请求发出前自动带上 localStorage 里存的 token响应回来时如果发现状态码是 401就跳转登录页。这样全项目不需要在每一个请求页面重复写 token 逻辑干净得多。3.3 一个领养申请接口的完整实现领养申请是系统里逻辑最重的接口。用户提交申请时前端把动物 ID、申请理由、居住信息一起传过来。后端第一步检查动物是否存在并且状态必须是待领养第二步检查当前用户 7 天内是否已经申请过这只动物防止重复提交第三步插入申请记录状态设为待审核第四步同步更新动物的申请次数方便管理员看到热度。关键代码我重点看 service 层Transactional(rollbackFor Exception.class) public Long submitApply(AdoptionApplyDTO dto, Long userId) { Animal animal animalMapper.selectById(dto.getAnimalId()); if (animal null || !1.equals(animal.getStatus())) { throw new BizException(该动物不在待领养状态); } int count adoptionApplyMapper.countRecentApply(dto.getAnimalId(), userId); if (count 0) { throw new BizException(7天内已申请过该动物请勿重复提交); } AdoptionApply apply new AdoptionApply(); BeanUtils.copyProperties(dto, apply); apply.setUserId(userId); apply.setStatus(WAIT_CHECK); adoptionApplyMapper.insert(apply); animalMapper.increaseApplyCount(dto.getAnimalId()); return apply.getId(); }Transactional 注解一定要加上因为这里涉及多张表的写操作。我之前没有加时出现过一次问题申请记录插入成功但申请次数更新失败数据对不上。加了这个注解之后任何一步抛异常都会整体回滚保证了数据的一致性。3.4 照片上传与静态文件处理动物照片上传是整个项目里细节最密集的环节。我采用的方案是本地磁盘存储上传时把 MultipartFile 写到服务器指定目录再把文件的访问路径存到数据库。上传路径我没有写死而是从 application.yml 里读取方便部署时修改upload: path: /data/pet-platform/image/上传接口接收文件之后先做格式校验只允许 jpg、png、webp大小限制 5MB。文件名我用 UUID 重命名避免用户上传的文件名带中文或特殊字符造成访问异常。如果把照片目录放在项目的 resources 下打包成 jar 之后写文件会报错因为 jar 包内部的路径是不允许直接写入的。正确做法是放到服务器外部目录然后用 SpringBoot 自定义静态资源映射暴露访问地址Configuration public class WebMvcConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/images/**) .addResourceLocations(file: uploadPath); } }如果生产环境需要扩展可以把本地文件存储换成 MinIO 或者阿里云 OSS。MinIO 加到 SpringBoot 的方式也很简单引入依赖、配置 endpoint、写一个 client 工具类就行。从本地文件改成对象存储核心业务代码几乎不用动只需要改工具类里的存储实现这也是在设计时把上传逻辑独立成模块的好处。4. 常见问题与排查技巧实录4.1 前后端联调跨域问题本地开发时前端端口 8080后端端口 8081浏览器会拦截跨域请求。最常见的报错是 Access-Control-Allow-Origin。解决办法有两个后端配置全局跨域过滤器或者前端开发环境的 devServer 开启代理。我建议开发时用前端代理生产环境用 Nginx 反向代理因为生产环境把前端静态资源和后端接口放在同一个域名下才是最干净的方案。如果后端要放开跨域用下面这个配置最简单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) 和 allowedOrigin() 是冲突的。之前我直接用“”放行域名结果携带 token 的请求还是被拦截因为浏览器不允许在携带凭证时使用通配符域名。后来改用 allowedOriginPattern(*) 才解决。这一点很多人踩坑记一下能省不少时间。4.2 版本和依赖冲突问题SpringBoot 的版本升级会带来一批连锁问题。3.x 之后包名从 javax 换成了 jakarta旧代码里的 import javax.servlet 全都要改。而且 MyBatis、Shiro、JWT 这类老牌框架的 starter 不一定适配 3.x需要额外找对应的新版本。所以我的建议是能用 2.7.x 就先用 2.7.x先把项目逻辑打通毕设展示和答辩环节不会因为版本问题卡住。Maven 依赖冲突也很常见。同一套逻辑我加了一个 security 依赖后原来能正常访问的接口全部 401。排查方法是先看启动日志里的提示再检查依赖树执行 mvn dependency:tree 命令看看是不是有重复依赖、版本不一致的情况。网上还有不少直接把“SpringBoot版本太高”相关的问题提出来核心原因大多集中在 jakarta 迁移、starter 版本过旧这两类。我还遇到过前端 Vue 安装依赖时脚手架版本过高导致的编译报错。这种情况建议锁定版本号把 package.json 里 vue、vue-router、element-ui 的版本写死不要用 ^ 前缀避免自动化升级引入意外改动。4.3 查询列表时出现懒加载和序列化异常使用 JPA 的同学大概率会遇到 lazy loading 问题查询动物列表时返回了关联的图片列表但因为懒加载配置序列化时抛出一个“could not initialize proxy - no Session”异常。解决方式有三种把关联字段的 fetch 类型改成 FetchType.EAGER在 service 层事务结束前把数据封装成 VO或者使用 JsonIgnore 忽略某个关联字段单独提供一个详情接口去查。我一开始用全局忽略策略结果发现动物列表没有图片信息前端展示全是空白。后来改成了在 service 层手动组装 VO查询时只查需要用的字段图片数量、封面图、申请状态分别查一次再合并返回。牺牲了一点查询性能但思路很直观排查和答辩都很好解释。如果用了 MyBatis需要留意的是 N1 问题。查询动物列表后循环查图片表10 条动物记录会额外产生 10 条 SQL。我后来改成了一条带 group by 的查询把多张图片的数据一次性查出来再按 animalId 分组装配。这个优化点写进系统设计说明文档里也是个不错的加分项。4.4 打包部署与 jar 包排查项目完成后要打 jar 包部署。后端执行 mvn clean package前端执行 npm run build然后把前端生成的 dist 目录交给 Nginx 托管接口路径通过 Nginx 转发到后端的 8081 端口。这里最容易犯的错是忘了把前端接口的 baseURL 从 localhost 改成服务器实际地址结果是本地页面能打开一上线全刷不到数据。还有一个很实用的排查技巧就是直接从 jar 包反查代码配置。如果后端同事离职了或者本地代码找不到了只有服务器上留了一个可运行的 jar 包你可以把 jar 解压出来里面的 classes 目录就放着编译后的 .class 文件用反编译工具打开就能看到原来的 controller 代码和 application.yml 配置。这个技巧我实测过很多次尤其适合快速定位端口配置、数据库连接对象名这些关键信息但反编译出来的代码是恢复不了注释和代码结构的只能参考不能当源代码来改。如果是前端部署后页面白屏优先检查浏览器控制台的报错和 Nginx 的 error.log。大部分问题集中在静态资源路径不对、接口跨域、路由的 history 模式没有做重定向这三类。Vue 的 hash 模式部署时最省心不需要配置路由回退用 history 模式就得在 Nginx 配一句 try_files 把请求都转发到 index.html。最后收个尾做这个项目的过程里我最深的体会是流程设计比代码本身更重要。流浪动物领养这个话题天然有温度但落到实处还是得靠状态机、权限控制、数据关联这些冷冰冰的技术细节来保证运营不混乱。你先想清楚“一只动物从被发现到被领养要经过哪些环节、每个环节谁负责”再去写代码思路会顺非常多。如果后续想继续扩展可以接入小程序端或者把领养回访做成定时任务提醒让平台真正能投入到救助站使用。这个项目我前后花了三周左右白天看 SpringBoot 自动装配原理和 Vue 动态路由的文档晚上埋头写业务代码最后看到一只只流浪动物的状态从“待领养”变成“已领养”确实有一种很踏实的成就感。
返回列表