
1. 为什么一个宠物救助站会需要管理系统前阵子帮一个民间流浪动物救助站点做了一套宠物之家管理系统从最初的需求梳理到最终上线部署前后折腾了大半个月。项目名写的是基于SpringBoot的宠物之家管理系统的设计与实现看起来是典型毕设选题但真正落地时你会发现这套系统从来不只是简单的CRUD而是一套围绕宠物档案、领养流转、健康记录、站内通知的完整业务闭环。一套设计合理的系统能帮救助站把纸质登记表、Excel台账和微信聊天记录里散落的信息收敛到一个平台上。这篇文章会从需求、选型、数据库、后端实现、前后端联调、部署踩坑六个角度把整个项目从头到尾捋一遍。适合正在做类似毕设的同学、想拿SpringBoot做实际小项目的开发者以及确实需要一个轻量管理系统的宠物机构参考。1.1 先盘清楚真正使用系统的人到底有哪几类我接手时第一件事不是写代码而是到救助站待了半天看他们平时怎么工作。真实的流程是这样的志愿者在外面捡到流浪猫狗带回站点后要登记基本信息、拍照片、做驱虫疫苗记录有市民想领养先要看看有哪些待领养的宠物选中后填一张申请表站长审核审核通过后签领养协议后续还要回访。光靠纸和表格信息一多根本对不上。所以这套系统的用户角色必须分成三类普通用户领养人注册登录、浏览宠物、申请领养、查看公告、提交留言。站点管理员站长/志愿者维护宠物档案、处理领养申请、登记健康记录、发布公告、管理用户。系统管理员拥有最高权限负责账号管理、角色分配、数据初始化。这个划分直接影响后面的权限设计和表结构。一开始我也想做一个大而全的系统后来把所有功能写在一张纸上按三个阶段排序初期只做宠物档案、领养申请、健康记录、公告中期加数据统计和回访记录后期再考虑丢失宠物寻主、捐赠信息。这样做的好处是每个阶段可交付不用憋一个大版本憋到夭折。1.2 功能边界划定什么必须做什么先不做基于上面的角色划分我划定了第一版的功能清单用户注册与登录区分普通用户和管理员。宠物档案管理录入、编辑、上下架、多图上传。领养申请流程提交申请、站长审核、记录结果。健康记录疫苗、驱虫、绝育、就诊日志。公告与留言站内通知和用户反馈。简单的数据看板宠物数量、待审核申请数、领养成功率。有同学会问为什么不直接做在线支付、宠物商城、社交论坛我的看法是这类功能在毕设答辩中确实亮眼但会大幅拉长开发周期而且救助站的真实需求痛点根本不在这。先把核心业务跑通、流程闭环比堆功能更值钱。2. 技术选型的关键决策SpringBoot不是唯一选择但一定是这个项目的最优解这个项目名字里有两个SpringBoot确实有点冗余但也能看出来选题方希望项目以SpringBoot为核心。实际开发中SpringBoot的价值不只是简化配置而是它把Spring生态里的组件以Starter的方式组织起来让开发者专注于业务代码。我整套后端用的是SpringBoot MyBatis-Plus MySQL Redis前端是Vue 2 Element UI前后端分离后打包部署。2.1 版本选择为什么我停在SpringBoot 2.7而不是直接上3.x很多同学一上来就装最新版Spring Boot 3.x结果遇到各种兼容性问题。我这个项目是在2024年初开始搭的当时思来想去还是选了2.7.x。原因很实际JDK版本Spring Boot 3要求JDK 17而很多学校的实验环境和服务器还在用JDK 8虽然可以装高版本但没必要给自己挖坑。生态兼容MyBatis-Plus对Spring Boot 3的支持当时有一些适配版本不像2.7这么顺手。用2.7 JDK 8 MyBatis-Plus 3.5.x组合网上遇到的问题和解决方案一抓一大把。稳定压倒一切毕设类项目、小团队真实业务稳定比追新重要。Spring Boot 2.7还在社区维护期内安全漏洞也有更新足够用。如果你非要上3.x建议先确认所有依赖都有对应版本尤其是数据库驱动、Redis客户端和MyBatis-Plus的Spring Boot 3专用包否则代码写到一半开始排版本依赖的坑非常消磨耐心。2.2 持久层MyBatis-Plus为什么比原生MyBatis更省心早期我写项目喜欢用原生MyBatis每个实体配一个XML文件增删改查都得手写。这个项目我直接用MyBatis-Plus核心看中的是三点通用Mapper继承BaseMapperT后单表CRUD不用写SQLselectById、selectPage、updateById直接可用。条件构造器QueryWrapper和LambdaQueryWrapper可以避免手写SQL字符串拼接的错误比如按宠物状态筛选、按发布时间排序都是链式调用。分页插件PaginationInnerInterceptor一行代码接入物理分页不用手动写LIMIT ?去计算偏移量。实体示例大致长这样Data TableName(pet) public class Pet { TableId(type IdType.AUTO) private Long id; private String name; private String species; private String breed; private Integer age; private String gender; private String status; // 0-待领养 1-已被领养 2-暂不可领养 private String coverImage; private String detailImages; private String description; private Date createdAt; private Date updatedAt; }TableName、TableId、TableField这些注解用熟了以后数据库表和实体类之间的映射关系一目了然后期改字段也方便。2.3 权限方案不引入Spring Security用JWT 拦截器就够了我之前在一些项目里用过Spring Security Security OAuth2功能强但配置复杂尤其对学生项目来说过滤器链、AuthenticationManager、UserDetailsService的概念很容易绕晕。这个项目的权限模型只有普通用户/管理员两级用JWT 自定义拦截器完全够用而且自己写的代码可控性更强出了问题一眼就能定位。做法是用户登录后后端生成JWT Token返回给前端。前端每次请求在Authorization头带上Token。后端写一个TokenInterceptor校验Token合法性并把用户ID和角色放进ThreadLocal或请求上下文。通过自定义注解RequireRole(ADMIN)标记需要管理员权限的接口拦截器里做角色判断。JWT工具类核心代码public class JwtUtils { private static final String SECRET pet-home-secret-key; private static final long EXPIRE 7L * 24 * 60 * 60 * 1000; public static String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim(role, role) .setIssuedAt(new Date()) .setExpiration(new Date(System.currentTimeMillis() EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Claims parseToken(String token) { return Jwts.parser().setSigningKey(SECRET).parseClaimsJws(token).getBody(); } }这套方案维护成本低答辩时也能讲清楚每行代码的用途。如果项目规模变大再换Spring Security也不迟SpringBoot的自动配置能力让这种替换相对平滑。3. 数据库设计宠物之家系统的业务根基数据库设计是整个系统最不能糊弄的部分。表没设计好后面写接口会反复改改一次牵扯一片极其痛苦。我花了两天时间画ER图、建表、调整字段磨刀不误砍柴工。3.1 七张核心表的职责划分这个项目最终一共设计了10张表核心7张如下表名职责关键字段user用户表username, password, role, phone, emailpet宠物档案表name, species, status, cover_image, detail_imagesadoption_application领养申请表user_id, pet_id, status, reason, contacthealth_record健康记录表pet_id, type, record_date, detailnotice公告表title, content, publisher, typemessage留言表user_id, content, reply_statusfavorite收藏表user_id, pet_id还有一些辅助表比如sys_config用来存站点基础配置login_log用来存登录日志这些按需建设就行。3.2 领养申请状态流转最容易设计错我最早把领养申请的status字段设计成单个整数0表示待审1表示通过2表示拒绝。后来仔细想发现不对实际业务里还有已签订协议回访中已完成已取消这些状态只靠一个字段硬编码会乱套。最终我用了字符串枚举PENDING待审核APPROVED审核通过REJECTED审核拒绝SIGNED已签协议VISITING回访中COMPLETED已完成CANCELED用户取消每个状态对应的操作也要限制比如只有APPROVED才能转为SIGNEDREJECTED和CANCELED是终态。这个逻辑我放在AdoptionApplicationService里用一组状态机判断方法和异常提示来约束避免前端绕过后端直接改状态。3.3 那些看不见的字段设计细节建表时很多同学只关注业务字段忽略了一些后期会救命的设计逻辑删除字段deleted默认0删除时改成1查询时TableLogic自动过滤。宠物档案误删时还能恢复比物理删除安全得多。创建时间created_at和更新时间updated_at统一用DATETIME业务上大量需要最近更新不加时间字段后期统计会很难受。状态字段默认值比如status默认0避免插入数据时漏传导致空值。唯一索引用户名、手机号一定要加唯一索引否则注册接口并发时会出重复账号。我就在测试时用多线程并发注册遇到过后来加索引才解决。数据库字符集统一用utf8mb4别用utf8。宠物姓名、公告内容里可能出现的emoji表情utf8编码根本存不进去存进去也是乱码这是非常常见的坑。4. 后端核心模块的实现思路与关键代码有了表和选型接下来就是具体实现。我按模块逐个开发这里挑几个最有代表性的模块讲登录鉴权、宠物档案、领养申请状态流转、定时任务。4.1 登录鉴权和角色控制登录接口本身没什么特殊特殊在于密码存储和Token校验。密码我用的BCrypt加密在Spring Boot里就是BCryptPasswordEncoder不需要自己写md5加盐那些老把式。用户注册时加密存储登录时用matches方法校验。数据库里就算泄露了密码拿到手也只是BCrypt串很难反推出明文。接口鉴权我写了两个注解Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface RequireLogin {} Target({ElementType.METHOD, ElementType.TYPE}) Retention(RetentionPolicy.RUNTIME) public interface RequireRole { String value(); // ADMIN 或 USER }拦截器里先判断注解再解析Token最后比对角色代码逻辑清晰。需要注意放行登录、注册、公告查询这类公开接口时要配置excludePathPatterns否则前端一进页面就被挡在门外连登录页都打不开。4.2 宠物档案模块的查询与图片处理宠物列表页是整个系统访问量最大的页面查询条件有物种、年龄、性别、状态、关键字。MyBatis-Plus的分页查询加LambdaQueryWrapper特别顺手public PageResultPetVO pagePets(PetQuery query) { LambdaQueryWrapperPet wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.hasText(query.getSpecies()), Pet::getSpecies, query.getSpecies()) .eq(query.getStatus() ! null, Pet::getStatus, query.getStatus()) .like(StringUtils.hasText(query.getKeyword()), Pet::getName, query.getKeyword()) .orderByDesc(Pet::getCreatedAt); PagePet page petMapper.selectPage(new Page(query.getPageNum(), query.getPageSize()), wrapper); return convertToPageResult(page); }图片处理是这个模块里比较麻烦的部分。宠物照片往往不止一张数据库里我用一个JSON字符串字段记录图片URL列表上传接口用MultipartFile接收文件按日期分目录保存到服务器本地磁盘再把访问路径返回给前端。生产环境应该用OSS一类对象存储但本地服务器用磁盘完全够。有一个细节容易被忽略图片上传一定要做大小和格式限制。最初我没限制有志愿者上传一张8MB原图页面加载卡到崩溃。后来限制单张不能超过5MB格式只允许jpg、png、webp后端处理前还要校验Content-Type不能只信文件名后缀。4.3 领养申请的状态机流转这块是整个项目里业务逻辑最复杂的地方我单独写了一个AdoptionApplicationService不把状态判断散落到Controller里。核心是一个handleStateChange方法public void changeState(Long applicationId, String targetState, Long operatorId) { AdoptionApplication app getById(applicationId); String current app.getStatus(); String role currentUserRole(); // 状态机校验 if (PENDING.equals(current)) { if (!List.of(APPROVED, REJECTED).contains(targetState)) { throw new BizException(当前状态只能进行审核通过或拒绝操作); } if (!ADMIN.equals(role)) { throw new BizException(只有管理员可以审核领养申请); } } else if (APPROVED.equals(current)) { if (!List.of(SIGNED, CANCELED).contains(targetState)) { throw new BizException(审核通过后只能签协议或取消); } } else if (SIGNED.equals(current)) { if (!VISITING.equals(targetState)) { throw new BizException(签协议后进入回访流程); } } else if (VISITING.equals(current)) { if (!COMPLETED.equals(targetState)) { throw new BizException(回访结束后记录完成); } } else { throw new BizException(当前状态不可变更); } app.setStatus(targetState); updateById(app); }这样写最直接的好处是前端不管怎么传状态后端都有一道自己的校验。即便有人通过Postman手动调接口也跳不过状态机。后面扩展新状态时只需要增加一个分支判断可读性也高。4.4 定时任务自动更新待办提醒救助站有大量重复性事务疫苗到期提醒、领养回访提醒、每日待办汇总。Spring Boot自带的Scheduled就能解决不用引入分布式任务框架。我在项目里写了一个RemindTaskComponent public class RemindTask { Scheduled(cron 0 0 8 * * ?) public void sendVaccineRemind() { ListHealthRecord records healthRecordService.list( new LambdaQueryWrapperHealthRecord() .eq(HealthRecord::getType, VACCINE) .eq(HealthRecord::getDeleted, 0) ); // 当次疫苗日期 1年 在当前日期前后7天内生成提醒 // 推送到管理员工作台待办列表 } }这里注意Scheduled默认是单线程串行执行的如果有多个任务建议在配置类里加一个自定义线程池的TaskScheduler否则一个任务卡住其他任务都会排队。还踩过一个坑Scheduled方法一定要在没有参数的情况下定义有参数会直接启动报错。另一个坑是cron表达式是6位还是7位Spring里默认是6位秒级在第一位和Linux的cron不一样写错了任务永远不执行。5. 前后端联调与部署中的现实问题后端接口全部写完开始对接前端页面时才是真正开始和现实妥协的地方。前后端分离看着简单联调起来全是细节。5.1 统一返回格式和全局异常处理没有统一返回格式的接口前端对接时只能每个响应类型硬解析一个接口一种写法崩溃是迟早的事。我定义了统一的ResultT结构{ code: 200, message: success, data: {} }所有Controller的返回类型都包一层Result配合RestControllerAdvice做全局异常处理。业务异常返回code 5001、参数校验失败返回code 4001前端拿到非200的code就弹message。这样统一的契约前后端联调效率能翻一倍。有个小技巧全局异常处理里不要直接e.getMessage()抛给前端很多JVM内部报错信息有堆栈路径既不友好又暴露信息。我都是维护一个异常码映射表手动抛出BizException时带上业务语义。5.2 Vue打包后放进SpringBoot的静态目录项目最终部署在一个轻量云服务器上并没有单独开Nginx而是把Vue打包后的dist目录直接复制到了SpringBoot的src/main/resources/static下面。这样做的好处是只有一个Java进程部署简单坏处是刷新页面时如果前端用了history路由会出现404。解决办法有两种改用Vue的hash路由URL带#刷新不会404但看起来不高级。保持history路由在SpringBoot里添加一个转发规则非/api/开头的路径全部转发到index.html。我用的第二种配置代码如下Controller public class PageForwardController { GetMapping(value {/, /index, /pet/**, /user/**, /admin/**}) public String forward() { return forward:/index.html; } }实际生产中还是建议Nginx分离部署前后端独立配置静态资源和后端负载压力都好控制。5.3 跨域问题本地联调时的必经之路本地开发时前端在8080端口后端在9090端口浏览器跨域是肯定绕不开的。我在SpringBoot里写了一个CorsConfigConfiguration 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)时addAllowedOrigin不能用*要用addAllowedOriginPattern(*)否则浏览器会直接判定跨域失败这个细节耗费过我一个小时。5.4 我实测遇到的几个比较隐蔽的坑第一个坑是SpringBoot默认的Tomcat请求体大小限制是1MB图片上传稍微大一点就报Maximum upload size exceeded。需要在application.yml里配置spring: servlet: multipart: max-file-size: 10MB max-request-size: 50MB第二个坑是MyBatis-Plus逻辑删除配置后如果表里deleted字段有NULL值TableLogic过滤条件会变成deleted0NULL的数据会被直接查不出来。懒省事最后还是要建表时就给非空默认值。第三个坑是时间字段的时区问题。服务器系统时区是UTC时查询出来的时间比北京时间少8小时。连接MySQL的URL上加serverTimezoneAsia/Shanghai并在application.yml里统一设置spring.jackson.time-zone: GMT8前后端时间显示才能一致。6. 如果重新做一次我会在几个地方换个思路系统上线跑了两周后救助站的志愿者们确实从手忙脚乱变成了按部就班这点让我很欣慰。但作为一个偏执的人我也复盘出几个当初应该做得更好的地方如果你也要做一个类似的SpringBoot管理系统可以直接避开。第一Redis缓存应该更早引入。宠物列表页和公告页是访问量最高的接口最开始每次请求都直查数据库测试阶段没感觉上线后同一时间十几个用户刷页面数据库连接数直接打满。后来我在热点接口加了Redis缓存缓存空值时间设成5分钟查询压力立刻降了下来。设计之初我就应该先把缓存方案加进去而不是出了性能问题再补。第二图片存储从一开始就该走OSS方案。本地磁盘存储虽然简单但备份、迁移、防盗链都麻烦。服务器一重置所有宠物图片全没了。后续我肯定会迁移到对象存储服务把上传接口改成生成预签名URL直传的方式这样应用服务器带宽压力更小图也能长期安全地保存。第三测试用例应该覆盖状态机变更。领养申请状态流转是最容易出bug的模块我前期全靠Postman手工测试后来同事帮忙测试才发现几个状态非法跳转的漏洞。如果最开始就用Spring Boot Test写一组状态机的单元测试一条测试数据覆盖一条路径代码质量会明显高一个层次。第四日志别只写在控制台。在线排查问题时控制台日志根本不够用。我后来加了Logback的滚动文件配置按天输出日志并保留最近30天配合异常码就能快速定位用户反馈的问题。项目发布到生产环境的第一件事就应该是配日志文件路径别省这一步。最后说点个人感受。这套宠物之家管理系统技术上没有用到什么高深难懂的东西SpringBoot的自动装配、Starter机制、MyBatis-Plus的CRUD、JWT的Token鉴权、Scheduled定时任务都是Java后端开发最常用也最该掌握的能力。但真正做完一个完整项目才会理解设计比编码重要功能边界划清楚、表结构设计稳、状态流转约束住开发效率自然就上来了。如果你正准备做类似的SpringBoot项目我建议不要纠结版本号是不是最新不要纠结功能是否高大上先从真实业务场景里找出一个能跑通的闭环把它做好做稳。等第一版上线了你会发现后面所有的扩展都有了一个扎实的地基。