
前阵子为本地一个动物救助站做了套管理系统后端用的SpringBoot前端是Vue打包之后整个系统装进一个jar就能跑拿给救助站的志愿者直接内网部署用省事。正好最近总有同学问我要这套流浪动物救助系统的源码结构我把整个设计过程和踩坑记录整理成一篇博客希望给正在做SpringBoot课程设计、毕业设计或者想快速上手前后端分离项目的人一些可复用的经验。这套系统表面上看就是“动物信息”的增删改查但真正做下来后你会发现核心难点全在业务状态的流转上——一只流浪动物从被发现、救助、体检、录入待领养再到被申请领养中间涉及的权限、校验和协作关系远比普通管理系统复杂。正文里我会从需求模型、技术选型、数据库设计、核心接口实现一路讲到打包部署中的实际问题目录就是下面这六块按顺序看就行。1. 从发现流浪动物到领养回家系统的核心业务闭环1.1 救助站日常业务里最痛的三件事我最早接触救助站的需求时他们还在用Excel登记每一只流浪动物。一张表里塞了发现地点、救助人电话、动物照片文件名、疫苗日期和领养状态不同人维护的版本经常对不上。找人查一只狗从救来到被领养的全过程得翻好几个文件。最大的痛点有三个第一动物照片和档案分离照片存在电脑文件夹里编号稍有错位就找不到对应关系第二领养申请经常“撞车”同一个人可能给好几只动物提交申请管理员在微信上沟通经常漏掉或重复第三没有审核留痕谁在什么时候通过了哪次领养事后追溯全靠聊天记录。所以这套系统的第一个目标不是做得花哨而是把“发现动物—救助登记—体检更新—发布领养—申请审核—领养确认”这条链路上每个环节的碎片信息固化成数据记录让所有人看同一个后台就能知道每一只动物的完整状态。1.2 角色划分管理员、救助站工作人员与普通用户不同身份在系统里做的事完全不一样我最终规划了三个角色。普通用户是来领养动物的人能浏览在架的动物列表、查看动物详情、提交领养申请、查看自己申请的处理结果同时也能发布寻宠或丢失信息。救助站工作人员负责日常录入比如把一只新救回的猫登记入系统更新它的健康状态、疫苗记录上传照片也可以操作动物下架或恢复展示。管理员则掌握审核权力包括审核领养申请、管理用户、发布公告、查看统计等。这三种角色我在数据库里用一个role字段区分取值分别是USER、WORKER、ADMIN并没有做复杂的RBAC权限表。原因很简单这个系统的权限资源就三类接口查询接口所有登录用户可用录入和更新接口需要工作人员及以上审核和管理接口只允许管理员。用角色字段加拦截器做判断比引入Spring Security的全套RBAC更简单直接代码量少也好维护。提示如果以后要给不同救助站做多租户再考虑引入独立权限表也不迟现在这个阶段过度设计反而是负担。1.3 功能清单哪些必须有哪些是加分项我按优先级把功能分成两个梯队。必须有的一梯队包括动物信息管理新增、编辑、下架、列表分页、详情、领养申请提交、审核、进度查看、用户登录注册JWT鉴权、角色权限控制。二梯队是可选的加分项比如公告发布与轮播图、救助记录登记、捐赠记录管理、数据统计看板。做第一版时我的原则是先把一梯队做扎实把动物列表、详情、申请审核这条主流程跑通再加公告和统计。下面这张表是最终实现的功能清单你可以直接对照自己的需求筛选。模块功能点角色权限用户注册、登录、个人信息查看修改全部动物新增动物、编辑资料、上传照片、列表查询、详情展示、下架列表详情全部管理类WORKER及以上领养申请提交申请、取消申请、审核通过/拒绝用户提交管理员审核救助记录新增救助记录、关联动物、历史记录查询WORKER及以上公告发布公告、列表展示、置顶公告发布ADMIN查看全部统计动物总数、待领养数、本月新增领养数ADMIN系统业务闭环到这里已经清楚接下来就是技术选型怎么落地。2. SpringBoot为主体的技术栈我的选型逻辑和配置细节2.1 后端SpringBoot MyBatis-Plus MySQL为什么这个组合最稳SpringBoot本身就是一个自动配置的便捷框架对于这种中小型管理系统它最大的价值在于把Tomcat、数据源、JSON序列化这些东西全部装配好我只需要写业务代码不需要关心Servlet容器的启动和配置。这也是为什么大多数Java课程设计和实际小型项目中SpringBoot几乎成了默认后端框架。持久层我选了MyBatis-Plus而不是MyBatis原生原因很现实这个系统里80%的数据库操作是单表CRUD和带条件的分页查询MyBatis-Plus提供BaseMapper内置insert、selectById、updateById、deleteById写个接口继承它就有全套方法。条件筛选用QueryWrapper构造比在XML里写动态SQL快得多。只有领养审核这种稍微复杂一点的统计我会在Mapper里手动写SQL。数据库版本用MySQL 8.0字符集统一utf8mb4因为用户填写的领养理由里可能有表情符号utf8存不下。2.2 前端Vue3 Vite Element Plus如何和SpringBoot优雅配合前端我没有用传统的Thymeleaf模板而是选了前后端分离的Vue3。用Vite构建UI库用的是Element Plus组件的观感和操作习惯比较符合后台管理系统。前后端分离后最关心的就是部署问题。我的方案是开发时前端跑在localhost:5173后端跑在localhost:8080通过Vite代理转发解决跨域部署时把前端打包生成的dist目录内容复制到后端src/main/resources/static下让SpringBoot直接托管静态资源最终只打一个jar包给救助站使用。后面第5章会详细说打包细节。2.3 图片存储本地目录直传还是引入MinIO救助站上传的动物照片是一个必须处理的问题。最简单的方案是把图片存到服务器本地目录比如/usr/local/animal-rescue/upload/在SpringBoot里通过WebMvcConfigurer配置静态资源映射把/upload/**映射到本地物理路径。这个方案部署方便照片直接放在jar旁边备份一个目录就行。如果以后系统部署在云服务器或者会有多个后端实例共享图片就得引入对象存储。MinIO是目前比较轻量的选择用Docker一条命令就能起服务Java接入也简单需要引入minio依赖通过MinioClient的putObject传文件。我的建议是如果只是课程设计或小型救助站内部用本地存储完全够如果打算把项目写成生产级作品加分项就是把图片部分改成MinIO我给你留一个扩展接口FileStorageService里定义store(MultipartFile file)本地实现和MinIO实现分别放impl包用ConditionalOnProperty切换以后接阿里云OSS也方便。配置示例app: upload-dir: ./upload storage-type: local如果storage-type为minio再配置MinIO的地址、账号、密码和桶名即可。3. 数据库表设计里藏着业务逻辑核心表结构与状态流转3.1 五张核心表用户、动物、领养申请、救助记录、公告数据库是这套系统的地基我设计的时候反复画了好几遍关系图。最终核心表就五张user、animal、adoption_apply、rescue_record、notice。它们之间的关系是这样的user和animal不直接关联用户通过adoption_apply和动物关联rescue_record挂在animal下面一只动物一条初始救助记录notice独立不跟其他表关联方便以后扩展。下面是animal表的字段设计也是业务上最重要的一张表字段名类型说明idbigint主键自增namevarchar动物名字typevarchar类型猫/狗/其他breedvarchar品种agevarchar年龄描述如“3个月”“成年”sextinyint0未知 1公 2母health_statusvarchar健康状态描述vaccine_statustinyint0未疫苗 1已接种一针 2已接种全针photovarchar照片URLdescriptiontext动物个性、背景描述statustinyint0救助中 1待领养 2已申请 3已领养 4下架create_timedatetime创建时间update_timedatetime更新时间deletedtinyint逻辑删除0存在 1删除adoption_apply表的关键字段是user_id、animal_id、reason申请理由、phone、status0待审核 1通过 2拒绝、audit_remark审核意见、create_time、update_time。唯一索引建议加在(user_id,animal_id)上防止同一用户对同一动物重复提交申请。3.2 动物状态字段从“救助中”到“已被领养”的流转我最想强调的就是这个状态流转很多毕设系统把动物状态做成了简单的“在架/下架”这样领养的所有中间过程都无法体现。我在status字段里设计了五个状态形成了一个清晰的时间线0 救助中动物刚被救回来还在检查、治疗不对外展示1 待领养体检、驱虫、疫苗完成可以对外发布2 已申请有人提交了领养申请正在审核过程中此时应锁定动物避免其他用户重复申请3 已领养审核通过用户已把动物接走4 下架管理员主动下架可能是动物走失、死亡等特殊情况。状态流转的规则要写在Service层而不是分散在各个Controller里。比如动物只有处于1 待领养状态才允许被提交领养申请提交后立即改为2 已申请管理员审核通过后改为3 已领养拒绝就回到1 待领养。这种状态机让整个业务逻辑非常清晰也避免了一堆if判断散落在前端页面里。3.3 领养申请表的审核状态一个简单又可靠的状态机领养申请的状态我更愿意用0 待审核、1 通过、2 拒绝表示。但需要注意的是审核通过后要同时对animal的状态做变更这两步必须放在一个事务里。我遇到过不少人在Service方法里忘记加Transactional导致申请状态改了但动物状态没有变用户点进来发现动物还是待领养再次提交又会因为唯一索引报错。所以我的建议是凡是涉及两张表以上数据变更的方法直接标注Transactional(rollbackFor Exception.class)。代码层面多写一行却能避免数据不一致的大坑。注意审核拒绝的操作要填写审核意见audit_remark前端在用户查看申请进度时展示否则用户不知道自己为什么被拒只能打电话求助管理员反而增加工作量。4. 核心功能实现鉴权、领养申请和图片上传的完整套路4.1 登录鉴权JWT拦截器加注解半小时搞定这套系统的权限控制没有引入Spring Security我认为JWT加拦截器已经足够。用户登录成功后后端用HMAC256生成一个token里面带上用户ID和角色设置24小时过期。前端在axios请求拦截器里统一加上Authorization: Bearer token。后端做一个AuthInterceptor实现HandlerInterceptor的preHandle方法拦截所有/api/**请求从请求头解析token并校验把userId和role放进ThreadLocal或请求对象。需要角色限制的接口可以定义一个RequireRole注解标注在Controller方法上例如RequireRole({ADMIN, WORKER}) PostMapping(/animal) public Result saveAnimal(RequestBody Animal animal) { // ... }拦截器里先解析token再判断当前用户的角色是否在RequireRole允许的列表内不在就直接返回403。代码量不多但足够满足这个系统的权限需求而且这套思路和Spring Security的注解鉴权很接近以后真要去学Security也不会有冲突。4.2 领养申请接口状态校验和幂等性处理领养申请是全系统最核心的流程我把它单独拎出来讲实现细节。用户提交申请时接口需要做三件事。第一步校验动物是否存在且状态为1 待领养第二步检查该用户是否已经申请过这只动物如果有记录直接返回“您已经申请过该动物”第三步插入申请记录并更新动物状态为2 已申请。这里有个幂等性的问题用户可能手滑点了两下提交按钮如果前后端没做防重就会同时插入两条申请。除了数据库上加唯一索引接口层也要做一次校验。最稳妥的办法是在插入前用user_id animal_id查一次虽然并发极低的情况下可能有极小概率穿过查重但加唯一索引后数据库会兜底报错我们捕获异常后统一返回友好提示即可。领养申请审核的代码会涉及事务核心如下Transactional(rollbackFor Exception.class) public void auditApply(Long applyId, Integer status, String auditRemark) { AdoptionApply apply applyMapper.selectById(applyId); if (apply null) { throw new BizException(申请记录不存在); } if (!apply.getStatus().equals(0)) { throw new BizException(该申请已处理请勿重复操作); } apply.setStatus(status); apply.setAuditRemark(auditRemark); applyMapper.updateById(apply); Animal animal animalMapper.selectById(apply.getAnimalId()); if (status.equals(1)) { animal.setStatus(3); // 已领养 } else { animal.setStatus(1); // 回到待领养 } animalMapper.updateById(animal); }这段代码里先校验申请是否待处理然后更新申请和动物状态。千万别在Controller里写完两段独立调用除非你能保证事务否则极容易出现第一张表更新成功、第二张失败的情况。4.3 图片上传与静态资源映射别让前端打不开图片图片上传接口是一个比较常见的“技术点”我做的时候也踩过坑。上传路径我放在项目运行目录下的upload文件夹而不是项目内部src/main/resources/static/upload因为jar包里的静态资源在打包后就不可写了。Controller里用MultipartFile接收文件校验文件大小不超过5MB后缀名限定jpg、jpeg、png、gif然后用UUID重命名文件避免中文文件名和重名覆盖问题。关键一步是配置静态资源映射让前端能用URL访问到上传目录里的图片。新建一个WebMvcConfigurer配置类Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadDir /); }这样前端请求/upload/xxxx.jpg时SpringBoot会去磁盘的./upload目录找文件。需要注意路径末尾的斜杠不能丢否则会拼接出错误的文件路径。4.4 列表分页与条件筛选MyBatis-Plus的QueryWrapper实战动物列表页需要支持按类型、状态、关键字搜索还要分页使用MyBatis-Plus的Page和QueryWrapper非常顺手。PageAnimal page new Page(current, size); QueryWrapperAnimal wrapper new QueryWrapper(); wrapper.eq(StringUtils.hasText(type), type, type); wrapper.eq(status ! null, status, status); wrapper.like(StringUtils.hasText(keyword), name, keyword); wrapper.eq(deleted, 0); wrapper.orderByDesc(create_time); animalMapper.selectPage(page, wrapper);需要注意的是如果状态是精确匹配就用eq关键字模糊搜索用like条件为空的字段一定要放到eq或like的第一个参数这样MyBatis-Plus会在条件为空时自动忽略这行拼接省去手写一堆if else的麻烦。返回给前端的数据我用一个通用类Result包装包含code、message、data三个字段前端统一处理响应码比直接返回实体类干净。5. 前后端联调与打包部署我踩过的坑和确认过的方案5.1 CORS跨域怎么配配错会发生什么开发环境下前端跑在5173端口后端8080端口浏览器拦截请求直接在控制台报Access-Control-Allow-Origin错误。解决方案是后端写一个CorsFilter或实现WebMvcConfigurer的addCorsMappings方法。我这里建议写一个全局过滤器因为配置更集中。Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOrigin(http://localhost:5173); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }如果你用了Spring Security跨域配置还得放在Security的过滤器链前面否则跨域请求会被拦截。我这里没引Security所以最省心。还有一点allowCredentials(true)和addAllowedOrigin(*)不能同时使用否则浏览器还是会报错必须指定具体域名或前端地址。5.2 Vue打包后如何并入SpringBoot的classes目录前后端分离项目最舒服的部署方式是把前端打包后塞进SpringBoot的静态目录实现单jar部署。具体做法是修改Vue项目的vite.config.js设置build.outDir为后端项目的src/main/resources/static目录并且将base设为/这样打包后index.html直接引用同根路径下的assets文件夹SpringBoot默认就能访问。如果你像我一样不想每次打包都让Vite直接写进后端项目避免开发中误操作也可以这样在后端项目里建一个web目录Vue打包目标指向web/dist然后Maven的resources配置把web/dist作为额外资源目录打进去。核心就是在pom.xml里加resources resource directorysrc/main/resources/directory /resource resource directory../web/dist/directory targetPathstatic/targetPath /resource /resources这样执行mvn package后前端文件会自动进入BOOT-INF/classes/staticjar本身就是一个完整的工作站。注意如果你用Vue Router的createWebHistory模式刷新页面时会出现404因为SpringBoot默认找不到前端路由对应的后端接口。两个解决办法一是改用createWebHashHistoryURL里带#号刷新没问题二是在后端写一个/error转发到index.html的Controller把前端路由接管过来。我建议正式做的时候直接配置后者体验更好。5.3 SpringBoot版本选太高会带来哪些麻烦有段时间我直接用SpringBoot 3.2最新版做测试结果发现不少老项目里的配置方式不兼容了。比如说spring.factories自动配置被废弃替换成了META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.importsjavax.servlet被换成了jakarta.servlet一些旧版的第三方SDK根本不支持SpringBoot 3.x接入就报错。如果你手上是一套用老教程搭起来的项目代码用了大量的javax.annotation.PostConstruct或者Maven依赖里还躺着老版本的MyBatis-Starer贸然升到SpringBoot 3会非常痛苦。我的建议是这个项目老老实实用SpringBoot 2.7.x这是2.x最后的稳定版本无论是MyBatis-Plus兼容性、资料数量还是各种踩坑教程都很全。除非你明确知道自己的SDK都支持3.x否则别为了追新去尝试。等系统跑通了再单独升级验证也不迟。5.4 application.yml拆分dev和prod环境怎么切换救助站的实际部署环境可能不止一个本地测试地址和服务器数据库地址不一样图片上传路径也不一样。我把配置文件拆成三份application.yml只放公共配置如端口、应用名、JWT密钥application-dev.yml本地开发环境MySQL连接指向localhost日志级别DEBUG上传目录用./uploadapplication-prod.yml服务器环境MySQL连接指向真实IP上传目录用/data/animal-rescue/upload。启动时用spring.profiles.activeprod指定环境Maven打包时也可以配合profile做自动化替换。这样做的好处是提交代码到公司仓库不会把数据库密码带出去运维部署时只需要改一个环境变量就能切换环境。6. 源码结构复盘与后续扩展灵感6.1 一个清爽的SpringBoot项目目录长什么样项目代码结构是否清晰直接影响后期维护。我在源码里采用了经典的分层结构大致如下animal-rescue/ ├── src/main/java/com/example/rescue/ │ ├── RescueApplication.java │ ├── controller/ │ │ ├── AuthController.java │ │ ├── AnimalController.java │ │ ├── ApplyController.java │ │ └── NoticeController.java │ ├── service/ │ │ ├── AnimalService.java │ │ ├── AnimalServiceImpl.java │ │ ├── ApplyService.java │ │ └── ... │ ├── mapper/ │ │ ├── AnimalMapper.java │ │ ├── AdoptionApplyMapper.java │ │ └── ... │ ├── entity/ │ │ ├── User.java │ │ ├── Animal.java │ │ └── ... │ ├── config/ │ │ ├── WebMvcConfig.java │ │ ├── CorsConfig.java │ │ └── MinioConfig.java │ ├── common/ │ │ ├── Result.java │ │ ├── BizException.java │ │ └── GlobalExceptionHandler.java │ └── interceptor/ │ └── AuthInterceptor.java └── src/main/resources/ ├── application.yml ├── application-dev.yml └── application-prod.ymlcontroller层只负责接收参数和返回结果业务逻辑全部放在service层mapper层只写数据库交互的接口方法。GlobalExceptionHandler用RestControllerAdvice统一捕获业务异常和参数校验异常这样前端拿到的错误信息永远是{code:400, message:xxx}而不是默认的堆栈。6.2 从毕设到生产还能加哪些模块如果这套系统不只是为了交作业而真要给救助站长期使用我建议继续扩展这几个方向。一是消息推送。审核通过后用户希望能收到通知可以接入微信模板消息也可以在系统内增加站内信表把通知事件插到message表用户登录后拉取未读消息。二是数据统计看板。管理员首页想看到的不是空空的欢迎语而是“本月救助多少只、领养多少只、成功率多少”这些可以用ECharts画几张图。后端提供一个统计接口按月份分组rescue_record和adoption_apply表返回给前端渲染折线图和饼图。三是领养回访机制。目前审核完就结束了现实里救助站会在领养后一两周回访确认动物是否被善待。可以在领养表上增加follow_up_date字段到时间后管理员能看到待回访列表填回访记录。这个需求能很自然地扩展出“回访管理”模块让系统的业务闭环更完整。四是引入Redis缓存热点数据。动物详情页是访问最频繁的接口可以把查询结果缓存到Redis设置半小时过期当管理员修改动物信息后主动删除缓存。虽然目前数据量小不缓存也无所谓但这是一个能给面试官讲的加分项。我实际做完这个项目后最深的体会是系统能不能被业务方真正用起来关键不在功能多不多而在于状态是否清晰、流程是否完整。花时间把动物状态和领养审核状态设计好后面写代码、做前端其实都很快。如果你也在做类似的管理系统可以先参考这套思路去梳理自己的业务闭环再动手写表结构。遇到具体实现问题时多看SpringBoot官方文档和MyBatis-Plus的示例项目会比搜一堆零散博客更高效。