☰
基于Spring Boot的流浪动物管理系统:核心业务闭环与状态管理
2026/10/14 23:36:32 网站建设 项目流程

如果你接到一个基于 Spring Boot 的 Web 端流浪动物信息管理系统、或者叫流浪宠物救助与领养平台,先别急着把它当成普通 CRUD 来写。流浪猫狗信息追踪这一条,决定了它不能只是几十个页面拼出来的 Demo。我上次帮人把一套类似系统从零搭到能验收,最深的感觉是:这个题的难点不在 Spring Boot,也不在 MyBatis,而在业务闭环。救助站要录动物,领养人要找动物,管理员要审核申请,志愿者要更新状态,中间还有那些看不见的规则,比如同一个人能不能同时申请两只、一只动物被两个人同时看中怎么办。这些规则没想清楚之前,页面做得再花哨也没用。

这篇内容适合几种人:正在选毕设题目、打算复刻一个宠物救助平台的在校生;想用 Spring Boot 做一套“能说服答辩老师”的完整系统的同学;还有接手过类似需求、想快速理清业务模型的开发者。我会把模块拆分、数据库设计、核心接口实现、高频问题和部署上线都说一遍,按真实项目会遇到的顺序来讲,不是教你怎么写一个纸面系统,而是教你怎么把它做成一个能跑、能演示、能落地的东西。

1. 从题目到系统:这个流浪动物平台到底在解决什么问题

1.1 一个容易被低估的选题

“流浪动物信息管理”听起来是一个纯后台管理题,很多人第一反应是:建一张动物表,做几个增删改查页面,再放几张照片,这不就完了吗?真这么写,大概率答辩一轮就被问住。老师随口问几个问题就够呛:一只动物从被人发现,到被救助站接收,再到开放领养、被申请、被审核、被接走,它的状态怎么流转?如果一个人提交了领养申请,这只动物还能不能继续被别人看到?如果审核被拒,状态改回哪里去?如果领养人带回去之后想放弃,又该走什么流程?

所以这个选题真正考的,不是会不会写select,而是能不能把一个真实业务里的“不确定状态”管理起来。

我把这套系统理解为三个核心问题:

  • 流浪动物的信息从哪来:救助站、志愿者、路人上报。
  • 信息怎么流转:待救助、救助中、可领养、审核中、已领养、已离世/暂停领养。
  • 领养过程怎么留痕:谁申请、审核结果、领养协议、回访记录、状态变更日志。

只要这三个问题接住了,后面的功能都是衍生的。系统里会出现的“动物列表”“搜索筛选”“领养申请”“我的申请记录”“消息通知”“数据统计”,本质上都是在支撑这三条主线。

1.2 核心业务流程拆解

我把这套系统的业务闭环画成一条线,这样后面写数据库的时候思路会非常清晰。

第一步,发现与登记。志愿者或者救助站工作人员发现流浪动物后,先在系统里登记基本信息:种类是猫还是狗、性别、大概年龄、毛色、发现地点、是否受伤、是否需要紧急救治、现场照片。这一步对应“动物信息表”的新增。

第二步,救助与康复。动物被接收后进入救助流程。可能需要驱虫、打疫苗、绝育手术、住院观察。后台要把每一次救助行为记录到“救助记录表”里,方便追踪这个动物从街头到能领养经历了什么。这一点很重要,也是“信息追踪”的原始依据。

第三步,开放领养。动物经过评估,确认健康、性格稳定后,管理员把它设置为“可领养”,同时填写领养要求,比如家里必须有纱窗、不能散养、接受回访。这些信息会直接展示给前台浏览的领养人。

第四步,申请与审核。领养人看中某只动物后,提交一份申请,内容包括居住情况、养宠经验、目前家庭成员、未来照顾计划等。管理员进入审核页面,看到申请材料后选择通过或拒绝。通过后,系统应该自动把动物改成“审核中”或“已领养”,并生成一份电子领养协议。

第五步,回访与追踪。很多真实救助平台不会在动物送出去之后就撒手不管,系统里应该有一个“回访记录表”,记录接走一个月、三个月、半年后的检查结果。如果发现问题,或者领养人确实无法继续饲养,动物会被退回,状态重新变成“可领养”。

这个闭环是整个系统最大的亮点。如果你只做前四步,它是个普通领养网站;把第五步做完,它才配叫“信息追踪及领养服务系统”。

1.3 角色权限怎么划分

这个系统的用户角色建议分三种,别贪多:

  • 平台管理员:管理所有用户,审核领养申请,发布系统公告,查看统计报表。
  • 救助站/志愿者:录入动物信息,上传救助记录,更新动物状态。
  • 普通领养人:注册、浏览、收藏、提交申请、查看我的申请进度、填写回访反馈。

为了扩展性,我建议用 RBAC 来做,也就是用户、角色、菜单权限三张表。有的同学问“是不是可以直接在用户表里加一个role字段”,当然可以,前期代码会省很多时间。但一旦后面要加“某个救助站只能看自己录入的动物”这种数据权限,只靠一个字段会变得很别扭。用 RBAC 至少能把功能权限和数据权限分开考虑。前台页面根据角色动态渲染菜单,后台接口再根据角色做二次校验,两层都要管,不能只靠前端隐藏按钮来防越权。

1.4 这类系统的影响范围

说完业务,再说影响范围。这种系统做出来之后,真正能影响的绝不只是“写代码的人”自己。

对于救助站来说,它能把原来散落在微信群、Excel、纸质登记本里的信息集中起来。一只动物从街头到领养家庭的全过程都有记录,谁在什么时候做了什么操作,回溯得清清楚楚。对于领养人来说,它降低了“想领养但不知道去哪找”的沟通成本,也不用靠朋友圈刷屏去碰运气。对于学校或者社区层面的宣传活动来说,通过后台统计数据,比如本季度成功领养了多少只、各类流浪动物占比,能很直观地展示救助成果,也方便申请资源。

所以这个选题虽然名字朴素,做出来的东西覆盖的角色、使用的场景、连接的数据链路,都比想象中广。下面聊技术实现的时候,我会按“能支撑上面这些业务”的标准来写。

2. 技术选型梳理:Spring Boot为主干,前端和数据库怎么搭配

2.1 后端为什么选 Spring Boot + MyBatis-Plus

这个题目指定了 Spring Boot,实际开发也确实是合理选择。Spring Boot 的自动配置能把项目初始化的成本降到很低,内置的事务管理、参数校验、接口测试工具都能直接用。配合 Maven 管理依赖,环境搭建基本是一场mvn spring-boot:run的事。

持久层我建议用 MyBatis-Plus,而不是原生 MyBatis。原因有三个:第一,单表 CRUD 不用手写重复 SQL,BaseMapper直接继承;第二,分页插件写起来非常简单,配置一个 interceptor 后Page对象就出来了;第三,自带乐观锁@Version,后面处理“同一只动物被多人同时申请”的问题时会省很多事。

版本上我习惯用 Spring Boot 2.7.x + JDK 8 或 11,而不是一上来就追 Spring Boot 3。主要考虑兼容性。实验室和老服务器上 MySQL 驱动、JDK 版本不一定跟得上,2.7.x 生态最成熟,出问题也容易搜到解决方案。

2.2 前端怎么选:分离还是合并

标题写了“基于 Web”,但没说前端一定要分开。这里有两套走法。

如果希望部署简单、代码量少,用 Thymeleaf 模板直接在后端渲染页面。Spring Boot 对 Thymeleaf 支持很好,页面可以直接通过ThymeleafViewResolver渲染,不需要额外启动一个 Node 工程。优点是打包成一个 jar 就能跑,适合没有前端基础的人。

如果希望界面更好看、演示效果加分,用前后端分离。前端用 Vue 3 + Element Plus,后端只提供 JSON 接口。页面和接口分离后,线上部署时用 Nginx 托管前端静态文件,再把/api请求反向代理到 Spring Boot 服务。这个方案对开发和部署都有一定要求,但也是目前实际项目的主流结构。

我的建议是:如果时间充裕、自己也愿意折腾,走前后端分离;如果目标是快速把功能做完整、把重点放在业务逻辑上,Thymeleaf 完全够用。下面讲的接口设计两种方案都通用。

2.3 数据库设计:一张状态流转表怎么把数据串起来

数据库是整个系统能不能撑住业务的关键。我建表时会把“动物信息表”“领养申请表”“救助记录表”“回访记录表”作为四大核心表,另外再加用户、角色、菜单、通知、日志等辅助表。

动物信息表大致应该包含这些字段:

  • id:主键
  • name:动物名字,可能是救助站临时起的
  • species:猫还是狗
  • breed:品种,非必填
  • gender:公母
  • age_month:估算月龄
  • health_status:健康状态,比如健康、待驱虫、待手术
  • vaccine_status:是否已疫苗
  • sterilization_status:是否已绝育
  • location:当前所在救助点位置
  • cover_img:封面图 URL
  • images:多图地址,可以用 JSON 数组字符串存
  • state:核心状态字段,用整数存
  • description:性格描述、救助故事
  • version:乐观锁版本号

状态字段我建议用一个数字字典管理,不直接在数据库里存汉字:

state含义说明
0待救助刚被上报,还没被救助站接收
1救助中/暂不可领养正在治疗或观察
2可领养可以对外展示并接收申请
3领养审核中有人提交了申请,暂缓展示
4已领养全流程结束
5已离世/暂停领养特殊情况

为什么把审核中单独拆出来?因为领养人在前端浏览时,大概率希望“可领养”的动物池是干净的。如果一只动物已经被人申请了,还挂在列表里,很容易出现别人也提交申请、最后被拒的尴尬。放在审核中,其实是给业务留了一个缓冲周期。

领养申请表要有独立的申请编号,同时记录animal_id、user_id、申请理由、居住情况、工作联系电话,以及审核状态。申请被拒绝后,这条记录不能删,要永久保留,一是为了方便追溯,二是答辩时可以说“我做了完整的历史留痕”。

2.4 图片和附件存储的取舍

动物系统免不了传图片。最常见的问题是有人把图片转成 Base64 塞进数据库字段,这是最不推荐的做法。图片是二进制大对象,塞进数据库会让表变得非常大,查询变慢,备份也慢。正常方案是图片上传到本地磁盘或者对象存储,数据库里只保存访问 URL。

本地磁盘上传简单,注意配置访问映射。比如上传到服务器的/data/upload/目录,然后在配置里把/upload/**映射到这个目录。对象存储更适合公网项目,但没有本地方案直观。我建议毕设阶段优先用本地磁盘,答辩演示时不容易出错。

3. 核心功能实现:从登录到领养审核的完整链路

3.1 用户登录、权限校验与基础安全

登录接口是系统第一个要写的核心接口。密码一定不能用明文,用 BCrypt 加密后存库。Spring Security 的BCryptPasswordEncoder可以直接拿来用,不需要真的引入整个安全框架。

如果不想把 Spring Security 配置搞得太复杂,可以用 token 机制。用户登录成功后签发一个 JWT 或一个简单 token,前端后续请求放在请求头里,后端通过拦截器解析当前用户和角色。这样既能防止每次请求都查一遍数据库,也方便角色校验。

@PostMapping("/api/login") public Result login(@RequestBody LoginDTO dto) { User user = userService.getOne( new LambdaQueryWrapper<User>() .eq(User::getUsername, dto.getUsername()) ); if (user == null || !BCryptPasswordEncoder.matches(dto.getPassword(), user.getPassword())) { return Result.error("用户名或密码错误"); } if (user.getStatus() != 1) { return Result.error("账号已被禁用"); } String token = JwtUtil.createToken(user.getId(), user.getRole()); return Result.success(token); }

这里有个细节:菜单权限不能只靠前端路由判断。接口层面必须有一个拦截器或者注解,验证当前用户是否拥有对应角色。不然前端用户手动拼一个/admin/audit请求,后台数据就暴露了。最简单的做法是定义一个@RequireRole("admin")注解,拦截器里判断当前用户角色,匹配不上直接返回 403。

3.2 救助记录与动物状态更新

救助站工作人员录入动物时,系统要做的不是一条 insert,而是“动物基本信息 + 救助记录”的组合提交。我实际开发时是把两个接口分开的:

  • POST /api/animal:创建动物基础信息
  • POST /api/rescue-record:给动物追加救助记录

为什么要分开?因为动物可能接受过多次救助。比如第一次住院做手术,疫苗结束后又做了一次复查,这些记录要能不断追加,组成一条完整的时间线。动物详情页里展示“救助时间轴”,会比单纯一个“备注”字段专业很多。

状态变更统一走一个接口:

@PutMapping("/api/animal/{id}/state") @RequireRole({"station_admin", "admin"}) public Result changeState(@PathVariable Long id, @RequestBody StateChangeDTO dto) { AnimalInfo update = new AnimalInfo(); update.setId(id); update.setState(dto.getState()); animalInfoService.updateById(update); return Result.success(); }

如果怕有人随意乱改状态,可以再加一层校验:旧状态到新状态的转换必须在允许列表里。比如“救助中”不能直接变成“已领养”,必须经过可领养和审核中。这个列表写在一个枚举或者配置类里,代码清晰,答辩时也容易讲。

3.3 领养申请与审核的完整链路

领养申请的核心逻辑,我用一个方法来说明,这也是最容易出并发 bug 的地方。

@Transactional(rollbackFor = Exception.class) public void submitAdoption(Long animalId, Long userId, AdoptionDTO dto) { AnimalInfo animal = animalInfoService.getById(animalId); if (animal == null || animal.getState() != 2) { throw new ServiceException("该动物当前不可领养"); } AdoptionApplication application = new AdoptionApplication(); application.setApplicationNo(generateNo()); application.setAnimalId(animalId); application.setUserId(userId); application.setReason(dto.getReason()); application.setAuditStatus(0); adoptionApplicationService.save(application); AnimalInfo update = new AnimalInfo(); update.setId(animalId); update.setState(3); update.setVersion(animal.getVersion()); int rows = animalInfoService.updateById(update); if (rows == 0) { throw new ServiceException("提交失败,可能已被其他人先申请了"); } }

这段代码里,animalInfoService.updateById(update)配合@Version乐观锁,会自动在 SQL 里带上WHERE id = ? AND version = ?。如果两次提交同时来了,第二次更新影响行数是 0,事务回滚,用户会看到“已被其他人先申请”的提示。如果没有这个机制,两个人同时提交都能成功,动物状态就被覆盖了。

审核通过和拒绝又是两个分支:

  • 通过:生成领养协议记录,状态改成 4,给领养人发系统通知,更新动物下架。
  • 拒绝:状态改回 2,让这只动物重新回到可领养池,同时把拒绝原因记录到申请记录里。

这个“审核拒绝后状态回退”的操作,很多新手会忘。只要忘了,那只动物就永远留在审核中,对正常业务是灾难。

3.4 消息通知与站内信

领养过程中,用户需要知道自己的申请有没有被通过。站内信实现很简单:建一张通知表,审核通过后插入一条userId, title, content,用户在右上角红点里读。

如果想让系统更完整,可以加入邮件通知或者短信通知,但毕设阶段不建议接付费服务。站内信已经能演示完整流程。通知表里要加一个type字段,区分是系统通知、审核结果还是回访提醒。这个字段后续做筛选会很方便。

3.5 数据看板与统计

别看这是个业务系统,数据统计往往是最容易得分的模块。不需要复杂的 BI,只要在后台主页放三个图表:

  • 流浪动物总数按状态分布:待救助、救助中、可领养、已领养
  • 近六个月新增救助数量趋势
  • 领养成功率:申请总数 / 审核通过总数

用SELECT state, COUNT(*) FROM animal_info GROUP BY state就能拿到分桶数据。趋势图再按月份GROUP BY DATE_FORMAT(create_time, '%Y-%m')统计。这些接口都很简单,但是页面展示出来之后,整个系统的“管理感”立刻就有了。

4. 实操中的高频问题与排查实录

4.1 同一只动物被两个人同时申请,怎么防

前面已经提到乐观锁,这里再补充一个场景:如果不加@Version,多人同时提交时会出现“两个申请都成功了,但动物状态只变成其中一个人的审核中”的情况。从用户视角看,第二个人的申请记录也查得到,管理员会看到两条申请对应同一只动物的矛盾数据。

我的排查和修复过程是:先在领养申请接口里加上事务,让“插入申请 + 更新动物状态”成为原子操作;然后再给动物信息表加version字段;最后在 MyBatis-Plus 配置里注册乐观锁插件。

@Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; }

这样改完后,并发测试里第二次提交会直接失败并回滚。处理结果虽然简单,但能看到“并发安全”这个点的人,已经比只会写增删改查的强很多了。

4.2 图片上传成功,但前端访问 404

这个坑我踩过不止一次。本地磁盘保存图片后,Spring Boot 默认并没有把/data/upload目录映射成可访问的 URL。上传接口返回一个/upload/20250310/xxx.jpg,前端直接打开却被 404。

解决方法是手动配置静态资源映射:

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:/data/upload/"); } }

同时还要检查上传配置:

spring: servlet: multipart: max-file-size: 20MB max-request-size: 50MB

不配置max-file-size,默认 1MB 会直接导致大图上传失败。前端也会报“不支持的请求”。遇到这种问题,先看上传接口的异常,再确认映射目录,最后看 Nginx 有没有拦截/upload这个前缀。

4.3 动物列表 N+1 查询,怎么优化

如果前端列表页要同时显示每只动物的“最近救助记录”,天然容易写出循环查询。比如先查 10 只动物,再在循环里每只查一次救助记录,数据库就会被查 11 次。数据少的时候没感觉,数据量上来后接口响应时间会从几十毫秒涨到一秒多。

优化思路很简单:先批量查出列表里的动物 ID,再一次性查所有animal_id IN (...)的救助记录,最后在 Java 里按动物 ID 分组,拼接到返回 VO 上。相比于嵌套循环,这种方式是稳定的O(n)级。

4.4 数据库时间差 8 小时

用 MySQL 时最常见的坑是时间字段插入后差 8 个小时。原因是 JDBC 连接串里没有指定服务器时区,系统默认取了 UTC。

解决办法是在 JDBC URL 后面加上参数:

jdbc:mysql://localhost:3306/animal_system?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

如果用的是 Spring Boot 配置,可以在spring.jackson.time-zone里也补一个 GMT+8,保证后端返回 JSON 时时间正确。

4.5 部署后接口被 Nginx 拦截

前后端分离部署时,如果前端把接口请求写成/api/animal/list,而 Nginx 只托管了前端静态文件,没有把/api转发到后端进程,浏览器会一直报 404。最有效的解决方法是把所有后端接口统一放在/api前缀下,然后在 Nginx 里加一段反转代理:

location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }

这里要注意proxy_pass末尾是否带/,带不带会影响转发路径拼接。调试时先看浏览器网络请求的完整 URL,再看后端日志是否收到请求,问题很容易定位。

5. 部署上线时容易翻车的地方和个人建议

5.1 打包与启动

后端打包前先确认测试环境配置和生产环境配置分开。我习惯在application.yml里用spring.profiles.active切换,本地用dev,服务器用prod。打包命令:

mvn clean package -DskipTests

不打测试能省很多时间,尤其是项目里没有写测试代码的时候。打包完成后会得到一个 jar 包,直接:

java -jar target/animal-system.jar --spring.profiles.active=prod

如果服务器内存不大,可以加-Xms256m -Xmx512m限制堆内存,避免占用资源过多。

前端如果是 Vue 工程,记得先构建:

npm run build

然后把dist目录里的文件放到 Nginx 的静态目录下。部署的时候一定要确认请求的 /api 前缀和后端的 context-path 对得上,这是 404 的重灾区。

5.2 容易被忽略的细节

有一个细节我每次都会检查:数据库连接串里必须加useSSL=false,不然 MySQL 8 可能因为 SSL 握手失败连不上。还有一个细节是字符集,数据库、表和连接串全部统一成utf8mb4,否则用户输入生僻字或者 emoji 会出现乱码。

业务层面还有一个容易被忽视的点:删除功能能不做就不做。动物信息一旦被登记,尽量别物理删除,可以用状态字段把记录隐藏掉。对于一个救助平台来说,历史数据本身就是价值。答辩时如果老师问“这个动物信息可以删除吗”,回答“我们没有物理删除,只是下架,所有历史申请和回访记录都保留”会显得更有工程意识。

5.3 关于这套系统,我最后的体会

自己做下来最大的感受是:这个题目的上限,完全取决于你把“状态管理”做到什么程度。如果你只把动物表当成普通数据,它就是一个管理后台;如果你把状态机、申请审核、回访记录串成一个闭环,它就是一套能真正给救助组织用的业务系统。

我一个经验是:项目做到最后,真正花时间的往往不是写接口,而是把状态流转的细节理清楚。比如申请被拒绝后要不要通知用户、审核中能否取消申请、领养人提交的申请能不能改第二份、回访时状态要记到什么字段里。这些看起来都是小事,但每一个都在回答同一个问题:系统里出现的每一只猫、每一条申请,它的生命周期到底是被谁、在哪个节点、以什么方式推进的。

如果你正在做这个题目,我建议你先把动物生命周期画在白板上,再开始建表。代码后面可以重构,表结构一旦建错,改造的代价会翻倍。把业务先想透,这个毕设项目的价值就会远超“一个 Spring Boot 作业”本身。

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

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

立即咨询