基于SpringBoot+Vue的流浪动物救助领养平台设计与实现
1. 一次救助站志愿经历让我动了做领养平台的心思去年的一个周末朋友拉我去本地一家流浪动物救助站帮忙。说是帮忙其实就是打扫猫舍狗舍、给动物换水喂粮。忙了一下午我坐在院子里歇脚看到救助站负责人王姐抱着一摞纸质登记表在核对信息表格上密密麻麻写着“品种中华田园猫年龄约1岁健康状况已驱虫待疫苗”“领养人意向已看中等回访”之类的字迹。旁边一位刚填完表的领养人问我“你们这个网上能看动物照片吗我翻表翻得眼睛都花了。”那一刻我才意识到救助站的动物信息全靠纸质档案和口口相传领养人要找到合适的宠物只能一趟趟跑现场救助站要审核领养人资质也全靠人工电话回访。而实际上这个场景完全可以做成一个基于SpringBoot的流浪动物救助与领养匹配平台把救助站、动物、领养人、回访记录全部搬到线上。回去之后我就决定毕业论文的题目就做这个SpringBootVue宠物在线认养全流程管理系统。这篇文章我会把整个项目的设计思路、技术选型、核心功能实现、数据表设计以及开发过程中踩过的坑完整梳理一遍。如果你是计算机相关专业正准备做毕业设计或者想用SpringBootVue做一套带完整业务闭环的管理系统这篇内容应该能帮你省不少事。1.1 纸质登记表管理下的真实痛点先说说我在救助站观察到的几个具体问题。这些痛点直接决定了系统该设计哪些功能模块而不是凭空想一个“万能的管理系统”。第一动物的信息极度分散。每只动物对应一张纸质登记表表上有基本信息、健康记录、疫苗接种情况、领养状态。但纸质表的查询效率很低王姐想找“所有已接种疫苗、适合领养的两岁以下母猫”得把几十张表挨个翻一遍。第二领养流程没有闭环。从领养人表达意向到提交申请到救助站审核再到领养后的定期回访这些环节全部靠微信聊天和电话沟通。经常出现的情况是某只猫被两个人同时看中王姐只能靠记忆判断谁先谁后或者领养人接走宠物之后回访记录没人跟进后续发生了什么完全不知道。第三缺少量化的匹配依据。很多领养人来救助站时只会说“我想养只猫”但家里是否有封窗条件、是否有其他宠物、是否有养宠经验这些关键信息没有结构化记录。结合这些痛点平台的核心功能就清晰了动物档案管理、领养申请全流程流转、申请人与动物的匹配评估、回访记录追踪。1.2 为什么这个选题适合做毕业设计从毕业设计角度讲这个题目有几点好处业务场景真实功能模块边界清晰技术栈主流且不冷门。它不只是一堆CRUD的堆砌而是带完整的业务状态流转和权限控制正好能体现SpringBoot后端Vue前端的全栈能力。而且这种“救流浪动物”的题材在答辩时天然有加成分——评委老师对带有社会价值和社会责任感的选题记忆点和好感度都会更高。你可以在答辩时很自然地讲出“系统已模拟救助站真实业务流程可减少人工登记遗漏提高领养匹配效率。”不过我得提醒一句选题有社会意义是不够的技术实现必须撑得起来。如果只是几个增删改查页面答辩时肯定会被问住。所以下面的内容我重点讲系统设计和技术实现里那些能体现深度的部分。2. 技术选型不是跟风SpringBootVue这套组合的真实考量这套系统的技术栈如下后端SpringBoot 2.7 MyBatis-Plus MySQL 8.0前端Vue 3 Element Plus Axios文件存储用MinIO鉴权用JWT。下面说清楚我为什么这么选以及每个选型背后考虑的替换方案。2.1 后端SpringBoot到底强在哪SpringBoot的核心价值是“约定大于配置”。它内置了Tomcat省去了繁琐的XML配置项目启动就是一个Main方法。对于毕设项目来说SpringBoot还有一个隐藏优势生态成熟遇到问题网上资料一抓一大把。我选的是SpringBoot 2.7.x不是最新的3.x原因是3.x要求JDK 17而很多学校机房或评委老师的演示环境还是JDK 8选2.7避开了环境兼容的坑。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version relativePath/ /parentORM框架我用的是MyBatis-Plus而不是纯MyBatis或JPA。原因是MyBatis-Plus提供了BaseMapper单表CRUD基本不用写SQL开发速度提升明显但复杂的多表联查和统计报表它又保留了手写XML SQL的能力两头都能顾上。相比JPA那种“实体关系映射”的抽象MyBatis-Plus的学习成本也更低更适合在论文里讲清楚“数据访问层如何实现”。2.2 前端VueElement UI的搭配逻辑前端选Vue 3 Element Plus。Vue的响应式数据绑定和组件化开发天然适合做这种信息展示表单操作密集型的系统。页面拆成“动物卡片列表”“领养申请表单”“后台管理表格”等独立组件数据通过Props和事件在组件间流动逻辑清晰。如果你的Vue基础比较薄我的建议是不要一上来就啃Vue源码直接拿Element Plus的组件搭页面。表格用el-table表单校验用el-form的rules弹窗用el-dialog分页用el-pagination。这些组件拼起来就是一个有模有样的后台管理界面。template el-form :modelapplyForm :rulesrules refapplyFormRef label-width100px el-form-item label领养人姓名 propapplicantName el-input v-modelapplyForm.applicantName placeholder请输入姓名 / /el-form-item el-form-item label居住情况 prophousingType el-radio-group v-modelapplyForm.housingType el-radio label自有住房自有住房/el-radio el-radio label租房租房/el-radio /el-radio-group /el-form-item el-form-item el-button typeprimary clicksubmitApply提交认养申请/el-button /el-form-item /el-form /template2.3 存储选型MySQLMinIO的方案确定业务数据用MySQL 8.0理由不展开说了关系型数据、事务支持、毕业设计标配。容易忽略的是图片等文件存储。动物的照片、领养人的身份证照片等如果直接存进数据库的BLOB字段数据库体积会迅速膨胀性能也会被拖累。我采用的方案是MinIO。它是开源的对象存储服务兼容S3协议本地用Docker一条命令就能跑起来。图片上传后返回一个URL数据库里只存路径展示时直接用URL访问。整个接入过程也不复杂# application.yml 中MinIO相关配置 minio: endpoint: http://localhost:9000 access-key: minioadmin secret-key: minioadmin bucket-name: pet-images// 上传文件的Service方法 public String uploadImage(MultipartFile file) { String fileName UUID.randomUUID().toString() file.getOriginalFilename().substring(file.getOriginalFilename().lastIndexOf(.)); try { minioClient.putObject( PutObjectArgs.builder() .bucket(bucketName) .object(fileName) .stream(file.getInputStream(), file.getSize(), -1) .contentType(file.getContentType()) .build()); return endpoint / bucketName / fileName; } catch (Exception e) { throw new RuntimeException(图片上传失败, e); } }这里有个细节MinIO默认生成的访问链接带了时间戳签名直接放img标签里会过期失效。我当初就在这里栽了跟头一会儿第5章专门说这个坑。3. 领养平台的命根子动物档案与领养流程的数据设计系统说到底是对数据的操作。数据表设计得好不好直接决定了后续业务逻辑是顺滑还是拧巴。这一章我完整梳理数据表结构和核心状态流转。3.1 动物基础信息表结构拆解动物信息表animal是系统的核心主数据我设计了以下字段字段名类型说明idbigint主键animal_namevarchar(50)动物昵称animal_typetinyint类型1-猫2-狗3-其他breedvarchar(50)品种gendertinyint性别0-未知1-公2-母age_monthint月龄health_statusvarchar(255)健康状态描述vaccine_statustinyint疫苗状态0-未接种1-已接种sterilization_statustinyint绝育状态0-未绝育1-已绝育personality_tagsvarchar(255)性格标签逗号分隔photo_urlvarchar(255)照片地址statustinyint领养状态0-待领养1-已申请2-已领养3-已下架shelter_idbigint所属救助站IDcreate_timedatetime创建时间update_timedatetime更新时间这里status字段是整个系统状态流转的核心。从“待领养”到“已申请”再到“已领养”每一步都对应不同的业务流程。性格标签这块我强烈建议你做因为它是后面“匹配推荐”的数据基础。比如一只猫的性格标签是“亲人,安静,适合新手”另一只是“活泼,需要大活动空间”领养人按照自己的养宠经验、居住环境去筛选匹配效率会高很多。3.2 领养申请单的状态机设计领养申请不是“提交了就完事”它是一条完整的状态链。我建了adoption_application表核心状态字段status取值如下待审核0领养人提交申请等待救助站工作人员审核初审通过1工作人员审核通过等待安排线下见面待回访2领养人已接走动物进入定期回访阶段已完成3回访通过领养关系正式确立已拒绝4审核不通过或回访不通过// 状态流转的核心Service逻辑 public void reviewApplication(Long applicationId, Integer result, String reviewRemark) { AdoptionApplication app applicationMapper.selectById(applicationId); // 校验当前状态是否允许执行审核操作 if (app.getStatus() ! 0) { throw new BusinessException(该申请已处理请勿重复操作); } if (result 1) { app.setStatus(1); // 初审通过 // 同步将动物状态改为“已申请” animalMapper.updateStatus(app.getAnimalId(), 1); } else { app.setStatus(4); // 已拒绝 } app.setReviewRemark(reviewRemark); app.setReviewTime(LocalDateTime.now()); applicationMapper.updateById(app); }为什么必须设计状态机而不是随便用一个字段因为状态之间的转换是有约束的——你不能让一个还处于“待审核”的申请直接跳成“已完成”更不能让一只已经被领养动物再次开放申请。状态机保证了数据在业务规则内流转这也是答辩时体现系统设计能力的重要一点。3.3 领养匹配推荐从规则到简单标签打分平台里有个“为你推荐”的功能。实现逻辑并不复杂核心思路是给领养人和动物分别打标签做标签匹配度评分。动物侧已经有了personality_tags和health_status等字段。领养人侧我在user表增加了experience_type养宠经验新手/有经验、has_other_pets是否有其他宠物、housing_type居住类型自有住房/租房、has_child是否有小孩等字段。匹配打分逻辑写成工具类public int calculateMatchScore(User applicant, Animal animal) { int score 0; // 新手优先推荐性格温顺的动物 if (新手.equals(applicant.getExperienceType()) animal.getPersonalityTags().contains(亲人) animal.getPersonalityTags().contains(安静)) { score 20; } // 家有其他宠物推荐已绝育且性格温和的 if (applicant.getHasOtherPets() 1 animal.getSterilizationStatus() 1) { score 15; } // 租房优先推荐不太活跃的小型动物 if (租房.equals(applicant.getHousingType()) animal.getAnimalType() 1) { score 10; } return score; }然后按分数倒序取TopN返回给前端。这个方案不涉及任何复杂的推荐算法模型但足以说明“领养人与动物之间的匹配关系”是如何量化的。答辩时被问到推荐策略你也能结合实际业务把逻辑讲圆。4. 用户端与管理端的核心功能落地整个系统分用户端、救助站管理端两端。用户端用的Vue页面有首页动物列表搜索筛选、动物详情页、认养申请页、个人中心我的申请记录。管理端页面有动物管理、申请审核、回访记录、数据统计看板。4.1 用户端筛选、详情、申请表单的关键交互首页的动物展示列表要有筛选栏这是用户体验的良心。筛选条件包括动物类型猫/狗、疫苗状态、绝育状态、性格标签。前端拿到条件后封装成查询参数传给后端后端用MyBatis-Plus的LambdaQueryWrapper动态拼接查询条件public PageResultAnimalVO pageAnimals(AnimalQuery query) { LambdaQueryWrapperAnimal wrapper new LambdaQueryWrapper(); // 类型筛选 if (query.getAnimalType() ! null) { wrapper.eq(Animal::getAnimalType, query.getAnimalType()); } // 疫苗状态筛选条件为“已接种”时只查status1的记录 if (query.getVaccineStatus() ! null) { wrapper.eq(Animal::getVaccineStatus, query.getVaccineStatus()); } // 状态上只展示“待领养”的动物 wrapper.eq(Animal::getStatus, 0); // 按创建时间倒序 wrapper.orderByDesc(Animal::getCreateTime); PageAnimal page animalMapper.selectPage( new Page(query.getPageNum(), query.getPageSize()), wrapper); // 转VO、补充照片等信息 return convertToPageResult(page); }详情页展示动物的完整信息和性格标签下方是一个醒目的“申请认养”按钮。点击后弹出申请表单表单里要包含领养人姓名、联系电话、居住情况、是否有养宠经验、是否有其他宠物、家庭人数等。这些字段不仅是流程需要更是为了让救助站审核时有足够依据。4.2 管理端审核、回访、数据看板管理端最核心的页面是申请审核列表。工作人员能查看每位领养人的详细资料根据表单信息和地区情况做判断。审核通过后系统会自动把对应动物的状态改成“已申请”并给领养人发一条站内通知。回访模块是体现“全流程”的关键部分。领养人接走动物后救助站需要在1个月、3个月、6个月三个时间节点进行回访。我建了visit_record表字段说明id主键application_id关联领养申请IDvisit_time本次回访时间visit_round回访轮次1-首次回访2-二次回访3-终次回访animal_condition动物当前状态描述visit_result回访结果0-正常1-异常需介入remark备注数据看板用ECharts做图表展示统计每月新增动物数、领养成功率、待审核申请数、各类型动物占比。这些统计SQL基本都是GROUP BYCOUNTMyBatis-Plus写selectMaps方法就能拿结果集没有太多技术门槛但对完整度很加分。4.3 消息通知的几种实现路径申请状态变化后用户怎么知道我用了两种方式站内消息表WebSocket实时推送。站内消息表实现很简单notification表里存user_id、content、type、is_read、create_time。审核操作发生时在同一个事务里插入一条消息记录用户登录后拉取未读消息数量。Transactional public void reviewApplication(Long applicationId, Integer result, String reviewRemark) { // 审核逻辑... // 发送站内通知 Notification notification new Notification(); notification.setUserId(app.getApplicantId()); notification.setContent(您的领养申请 (result 1 ? 已通过初审 : 未通过审核)); notification.setType(1); notificationMapper.insert(notification); }WebSocket是加分项实现也不复杂。引入spring-boot-starter-websocket定义WebSocketServer端点用户连接后把session放到Map里审核状态变更时向指定用户的session推送消息。前端用new WebSocket()监听。这个功能答辩现场演示效果很好后台点击“通过审核”用户端不需要刷新页面消息和状态自动更新。5. 真实开发中踩过的坑和排查过程下面这部分是整个项目里花时间最多、也最有分享价值的内容。我没按教科书顺序讲就按我踩坑的时间顺序来。5.1 图片上传MinIO部署时的诡异403第一次把MinIO跑起来上传图片接口调用成功返回的URL也能在浏览器打开但放到Vue的img标签里就显示403。排查了很久才发现MinIO对私有bucket的默认访问策略是要带签名参数的我直接拼接了一个裸路径返回给前端自然会被拒绝。解决路径有两个方案一bucket权限改为public连接地址直接用裸路径。适合演示环境不推荐生产环境。方案二保留私有bucket后端生成带时效的预签名URL返回给前端。// 生成带签名、有效期为7天的URL public String getPresignedUrl(String objectName) { try { GetPresignedObjectUrlArgs args GetPresignedObjectUrlArgs.builder() .method(Method.GET) .bucket(bucketName) .object(objectName) .expiry(7 * 24 * 60 * 60) // 7天单位秒 .build(); return minioClient.getPresignedObjectUrl(args); } catch (Exception e) { throw new RuntimeException(生成预览链接失败, e); } }选了方案二之后图片展示层不再直接存URL而是拿到objectName后动态生成签名URL。这个处理逻辑在论文里写“基于时效签名保证对象访问安全性”答辩时老师会觉得你想得比较全。5.2 跨域配置本地联调时Axios请求被拦前端跑在localhost:5173Vite默认端口后端跑在localhost:8080前后端端口不一致必然触发跨域。Vue页面发的请求被浏览器拦截控制台报CORS error。解决办法是后端加全局跨域配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }注意allowCredentials(true)和allowedOriginPatterns(*)必须一起用如果用allowedOrigins(*)会被浏览器拒绝这是很多跨域配置失效的常见原因。另外如果配置了JWT拦截器一定要让OPTIONS预检请求放行否则前端依然报跨域错误// 拦截器里放行预检请求 if (HttpMethod.OPTIONS.equals(request.getMethod())) { return true; }这个问题属于“配置五分钟排查两小时”的典型提前写在笔记里后面能省很多事。5.3 并发领养申请同一只猫被两个人同时认养系统上线试运行时出了个尴尬问题救助站同事用两个账号同时点开同一只猫详情页一起提交了认养申请结果数据库里出现了两条status待审核的申请记录。虽然审核时工作人员会人工判断但数据层面已经产生了脏数据。原因很简单我审核代码里校验动物状态时是“先查询再更新”这两个操作之间存在时间窗口——并发场景下两个事务都读到了status0然后各自执行更新互相覆盖。解决思路是加乐观锁或唯一约束。更实际的做法是对动物ID加条件更新// 用更新受影响行数判断是否被抢先 int rows animalMapper.updateStatusIfAvailable(animalId, 0, 1); if (rows 0) { throw new BusinessException(该动物已被其他用户抢先申请); }对应的SQL是UPDATE animal SET status 1 WHERE id ? AND status 0。这样在更新那一刻数据库层面会串行化处理只有一个事务能成功另一个影响行数为0直接提示“已被抢先”。这个坑让我深刻体会到光在业务层做判断不保险数据库的原子操作才是最后防线。如果你答辩时能把这个并发问题以及解决思路讲清楚评委一定会认为你是真做了项目而不是照着教程敲了一遍。6. 上线前的准备与毕业设计答辩要点6.1 演示环境怎么搭最稳妥毕业设计答辩的演示环境我的建议是本地运行而不是依赖线上服务器。线上环境出问题没法现场改但本地环境你可以提前把端口、数据库、MinIO全部配好做完整的演示彩排。推荐用docker-compose一次性拉起MySQL和MinIOversion: 3 services: mysql: image: mysql:8.0 container_name: pet-mysql ports: - 3306:3306 environment: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: pet_adoption volumes: - ./mysql-data:/var/lib/mysql minio: image: minio/minio container_name: pet-minio ports: - 9000:9000 - 9001:9001 command: server /data --console-address :9001 environment: MINIO_ROOT_USER: minioadmin MINIO_ROOT_PASSWORD: minioadmin volumes: - ./minio-data:/data后端用mvn spring-boot:run启动前端用npm run dev启动。事先跑一遍完整演示流程注册用户 → 浏览动物 → 筛选 → 提交申请 → 后台审核 → 通过 → 回访记录录入 → 数据看板。另外提醒一个容易被忽略的点演示数据一定要提前备好。我给每个状态都准备了对应的演示账号和动物数据比如“待审核申请”“回访中的案例”“已完成领养”各两条。这样演示不是只停留在“新增一条记录”而是能展示系统在真实业务周期里积累下来的数据形态让人对系统完整度更有信心。6.2 数据填充与演示脚本的设计光有账号不够演示时说什么也要提前想好。我给自己列了一个演示脚本打开首页用筛选条件“猫”“已接种疫苗”过滤展示列表变化点进一只猫详情强调性格标签和健康档案以用户身份提交认养申请强调表单校验切换到管理端展示待审核申请列表、审核操作用户端实时收到WebSocket推送演示回访记录的新增展示动物状态随之变为“已领养”打开数据看板展示统计图表整个流程控制在8-10分钟。答辩时宁可少讲代码细节也不能跳过业务闭环的展示。6.3 这几个月开发下来的个人体会项目从零到能跑通全流程大概花了三周半的时间其中真正写业务代码的时间只有两周多剩下时间几乎都耗在环境搭建、踩坑排查和演示打磨上。有几个心得想分享给准备做类似项目的同学第一一定先把数据表设计清楚再动代码。我最初就是在写完动物表和申请表之后直接开写接口结果做到后面发现需要加回访表、通知表来回改表结构改得怀疑人生。如果你现在刚启动这个项目请先花一个晚上把表结构、字段、状态枚举全部定下来用SQL文件建好表再开始写Java代码。第二前后端联调和环境配置的问题往往比业务逻辑更耗时。跨域、MinIO签名、图片跨域访问、WebSocket握手失败这类问题的排查经验非常宝贵。建议把问题记录成笔记答辩时的“项目难点”部分直接取材于此。第三别迷信过于复杂的架构。看到很多同学一上来就上Redis、RabbitMQ、分布式微服务对毕设来说完全没有必要。一个单体SpringBoot项目把业务逻辑、状态流转、权限控制做好已经足够体现设计能力。在答辩时把“为什么选用单体架构”解释清楚反而不容易翻车。最后一个小技巧如果你也打算把项目部署到云服务器上给朋友试用记得把MinIO的bucket访问策略、后端生产环境配置文件里的数据库密码改成强密码并开启HTTPS。网络安全意识在答辩加分和在真实场景里同样重要。
上一篇/下一篇内容由系统自动关联
返回资讯列表 →