多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于Spring Boot+SSM的宠物领养救助系统设计复盘

基于Spring Boot+SSM的宠物领养救助系统设计复盘 去年我接了个宠物救助站的数字化需求对方负责人开门见山就跟我说我们站里每一只猫狗从进站到被领养中间的救助记录、疫苗情况、领养人审核、回访跟进全都写在纸质档案本上翻起来费劲还经常丢失。于是就有了这套基于 Spring Boot SSM 架构的宠物领养救助系统内部项目编号 027uj。今天这篇复盘我想把这套系统从需求拆解到数据库设计、从核心代码到上线部署的完整思路捋一遍重点讲讲那些上规模教程不会写的细节——比如领养状态机怎么设计、救助记录怎么跟宠物档案关联、文件上传路径为什么总在本地好好的一部署就挂以及做这类管理系统时最容易忽略的业务闭环。先给正在做毕业设计、或者打算接类似外包项目的朋友交个底这个系统表面看是个常见的后台管理项目实际上它比普通的图书管理、订单管理要复杂一个维度——因为你面对的不是静态数据而是一条带着强业务规则的生命周期。狗狗进站、驱虫、绝育、公示领养、申请审核、签订协议、回访确认每个环节都有状态变化和操作者权限设计不好就会变成一张到处脱节的“数据孤岛”大拼盘。这篇会尽量把整个过程讲透让没有做过完整项目的朋友也能照着搭出一套能跑、能交付、能演示的系统。1. 宠物领养救助系统的业务逻辑先弄明白谁在用、用在哪儿很多人在拿到这类项目时习惯先把 Spring Boot 工程搭起来再去想表结构这是典型的顺序错误。我一般先画角色图和业务流程图把谁在什么时间对什么东西做什么操作搞清楚再回头设计代码结构。1.1 三类核心角色和他们的真实痛点这套系统里最主要的用户不是管理员而是救助站的工作人员和潜在的领养人。救助站工作人员分两种一种是负责接收流浪动物并登记信息的人员另一种负责审核领养申请和安排回访。我在设计权限时按了三套体系超级管理员站长、普通员工救助员、访客/领养申请人。每个角色对应的核心动作差异很大。救助员需要快速录入一只新进站动物的照片、品种、年龄、健康状态、入站来源并且在下一次给它打疫苗或治病时补充记录。领养申请人需要能看到所有待领养宠物的列表、详情、救助故事然后提交自己的申请资料。而审核人员需要在一个待办池里看到所有申请的关联信息——既能看到申请人填写的家庭情况也要看到宠物当前的健康和疫苗状态避免“申请表填得漂亮但宠物本身不适合进入家庭”的情况发生。1.2 业务闭环从流浪动物进站到领养回访的完整链路把业务流画出来之后系统的边界就清楚了。完整链路是这样的发现流浪动物 - 救助登记入站- 健康检查与治疗 / 疫苗接种 / 绝育 - 待领养状态上架 - 领养人浏览并提交申请 - 工作人员审核 - 线下核验 / 签订领养协议 - 完成领养 - 定期回访。这里有一个特别容易被做成“摆设”的环节就是回访。很多管理系统做到“领养完成”就结束了但救助站实际业务里回访才是衡量救助成果的关键。比如一只受伤的猫被领养后三个月内需要确认它在新家庭适应得好不好、是否按时绝育回访结果需要记录在案。于是系统里我加了一个“回访记录表”关联宠物和领养单让整个救助链路形成闭环而不仅仅是给每只宠物建一个档案、再建一个领养申请那么简单。1.3 非功能性需求为什么这个系统必须轻量但完整宠物救助站通常没有专职运维服务器配置也不高所以这套系统我刻意控制了技术栈重量。不用微服务不用 Redis 集群连消息队列都没上——因为这些对业务本身没有实质提升反而增加部署和排查成本。核心诉求是能跑、能多用户并发录入、数据不出错、图片不过期。基于这个前提单机 MySQL 本地文件存储的架构完全够用Spring Boot 内置 Tomcat 直接打包 jar 运行部署成本几乎为零。2. 技术选型的取舍Spring Boot 与 SSM 是怎么共存的项目标题里的“springboot_ssm”让不少人疑惑Spring Boot 和 SSM 到底是二选一还是叠加关系这里先把这个讲清楚——因为这决定了整个项目的代码结构。2.1 从 SSM 到 Spring Boot不是替代是封装与演进SSM 指 Spring Spring MVC MyBatis在 Spring Boot 出现之前这是 Java Web 项目最常见的组合。它的典型配置方式非常繁琐Spring 的 XML 或配置类要单独维护Spring MVC 的视图解析器、拦截器、乱码过滤器都要手动声明MyBatis 的 SqlSessionFactory、MapperScanner 也要一步步装配。Spring Boot 将这些繁琐的配置封装成自动化配置只要引入spring-boot-starter-web和mybatis-spring-boot-starter就能在一个工程里同时享受 SSM 的编程模型和 Spring Boot 的自动配置。所以“springboot_ssm869”这个命名并不矛盾。它代表的是基础框架依然是 Spring 的 IoC 和 AOPWeb 层依然走 Spring MVC 的 DispatcherServlet 路由持久层依然用 MyBatis 写 SQL只是这一切都在 Spring Boot 的自动配置和管理下运行。项目里我保留了经典的 Controller-Service-Mapper 三层结构这对大多数毕设和中小型后台系统来说是最清晰、最容易维护的组织方式。2.2 为什么不直接上更重的框架这个问题我每次都会被问。有人觉得前后端分离 Vue Spring Cloud 才是“现代项目”但宠物救助系统的核心痛点不在高并发、不在分布式事务而在于业务规则的正确性和数据的完整性。引入过重框架意味着学习成本、部署成本和调试成本成倍上升交给救助站这种没有专职技术人员的环境出了问题他们根本处理不了。直接使用 Spring Boot Thymeleaf 做服务端渲染前端页面由后端模板直接控制代码量少部署也简单对这类业务系统是比前后端分离更合理的选择。这种取舍思路做实际项目的人要能说清楚而不是觉得“越新越好”。2.3 版本选型Spring Boot 2.x 还是 3.x这个项目我用的是 Spring Boot 2.7.x原因很实际它稳定MyBatis 和各种第三方 starter 的兼容性最好遇到问题网上的解决方案也多。Spring Boot 3.x 基于 JDK 17做新项目问题不大但很多老版本的 starter 还没适配容易踩坑。如果你的毕业设计或小项目只是这套业务2.7.x 是稳妥之选。JDK 用 8 或 11 都行老救助站的服务器上装的如果是低版本 JDK你用了 JDK 17 编译出来的 jar 直接跑不起来这就是非常现实的兼容性问题。3. 数据库设计把领养、救助、用户三块数据串成业务闭环数据库设计是这个项目最关键的一步。我做了三个版本迭代才最终定稿——第一次拆了三十多张表过度设计第二次强行合并丢了业务细节第三次才找到合适的粒度。这里分享最终的核心表结构。3.1 用户、角色、宠物、领养申请四张核心表的设计思路用户表t_user我存了基础信息包括用户名、密码MD5 加盐、手机号、家庭住址、身份类型管理员、员工、申请人。角色控制采用简单的role字段加拦截器判断没有引入基于 RBAC 的五表权限模型——对这个规模的项目够用了。如果你想要更细粒度的权限控制比如“只能审核不能修改”可以再拆 user_role、role_menu 等表但一般情况下没必要。宠物表t_pet是全系统的核心字段设计得非常细致pet_name、species猫/狗/其他、breed品种、age_month月龄、gender、health_status健康状态用字典值待检查/治疗中/健康、vaccine_status疫苗状态未接种/已接种一针/已完成、sterilization是否已绝育、entry_reason入站来源比如街道救助、弃养、走失寻主、photo_url图片路径、status管理状态在站/待领养/已领养/已离世。为什么单独拆这么多字段而不是用一个 JSON 字段糊弄过去因为后续列表筛选手“按品种筛选”“只看已绝育的猫”都直接映射到数据库查询拆开存才能走 SQL 索引否则全部靠逻辑层过滤一百条数据就有明显卡顿。领养申请表t_adoption要保存申请时的快照数据包括pet_id、user_id、申请人家庭情况家里有没有其他宠物、是否有阳台封窗、是否同意回访等——这是审核人真正关心的、application_status状态流转、create_time、audit_time、audit_remark。注意我在申请表上冗余了pet_name和pet_species这样在审核列表页直接展示宠物信息不用每次都联表查宠物表查询效率高代码也简洁。逻辑冗余在这里是合理的不算违背三范式。回访记录表t_follow_up是业务闭环的保证保存adoption_id、pet_id、user_id、visit_time、content回访内容、result回访结果适应良好/有问题待跟进/失联。这张表的存在让“领养成功后系统就变死数据”的问题从根源上解决掉。3.2 救助记录与健康档案让每只宠物的经历连续可追溯救助记录我单独建了t_rescue_record表字段包括pet_id、rescue_time、location救助地点、rescue_type发现流浪/群众上报/其他机构移交、description、handler_id救助员工 id。健康记录t_health_record表存疫苗记录、体检记录、治疗记录类型用record_type区分额外字段vet_name记录诊疗机构或兽医名避免出现“打了疫苗但谁打的、哪一批疫苗都查不到”的尴尬。这两张表与宠物表是一对多关系列表或详情页展示时按时间倒序排。3.3 状态字段的字典管理避免魔法数字散落代码pet.status、adoption.application_status这些字段的取值如果直接写在 Java 代码里后面改逻辑会非常痛苦。我从一开始就做了字典常量类DictConstants把状态值定义为常量。举例来说领养状态PENDING0, APPROVED1, REJECTED2, COMPLETED3, RETURNED4。所有状态流转的判断都复用常量前端展示通过枚举或字典接口转成中文。这个习惯看起来多写几行代码但后期加“退回申请”“延期回访”这类状态时你就能体会到什么叫舒服。4. 领养审核与救助跟踪状态机在业务代码里怎么落地表结构定完接下来是最核心的业务代码。宠物管理和用户管理都是常规 CURD真正考验设计的是领养审核流程的状态机和救助记录与宠物档案的联动更新。4.1 领养申请的状态流转与权限校验我在AdoptionServiceImpl里实现了一个严格状态机。核心逻辑只有处于“待审核”状态的申请才能被审核审核通过后宠物状态立刻变为“已领养”如果审核拒绝宠物回到“待领养”状态如果领养人后来退养申请状态变为“已退回”宠物重新上架。public void auditAdoption(Integer adoptionId, Integer auditResult, String auditRemark, Integer auditorId) { AdoptionRecord record adoptionMapper.selectById(adoptionId); if (record null || !ApplicationStatus.PENDING.equals(record.getStatus())) { throw new BusinessException(该申请不在待审核状态不能重复审核); } Pet pet petMapper.selectById(record.getPetId()); if (AuditResult.APPROVED.equals(auditResult)) { record.setStatus(ApplicationStatus.APPROVED); pet.setStatus(PetStatus.ADOPTED.getCode()); petMapper.updateById(pet); } else { record.setStatus(ApplicationStatus.REJECTED); } record.setAuditRemark(auditRemark); record.setAuditTime(new Date()); record.setAuditorId(auditorId); adoptionMapper.updateById(record); }这段看着简单但实际开发中踩了很多坑。最典型的是状态校验和事务边界的问题。如果审核操作不放在事务里宠物状态更新成功但申请状态更新失败时数据库里会出现“宠物已领养但申请还在待审核”的数据不一致。解决方法是把整段操作放到一个Transactional方法里同时用乐观锁或条件更新防止并发重复审核。我在代码里用的方案是 update 时带上status PENDING的条件如果更新影响行数为 0 则说明已被他人处理直接抛出异常提示简洁且可靠。4.2 救助记录与宠物状态的联动新增治疗记录时更新健康概况宠物健康状态不是一个只在健康记录表里“自己活着的字段”它需要实时反映在宠物主表上。比如救助员给一只猫添加了一条“猫瘟治疗记录”那么pet.health_status应该从“治疗中”跳转成“治疗中”治疗结束后由操作人主动置为“健康”。这个流程我做了两处逻辑一是新增健康记录时调用petService.recalcHealthStatus(petId)方法根据是否存在未结束的治疗记录重新计算二是在前端详情页上显示最近三条健康记录方便审核人一眼看清这只宠物的近期医疗情况而不是点进二级页面再翻日志。这类“主表状态由明细表聚合”的设计思路在订单、工单、审批等场景也通用关键是把聚合逻辑放在同一个服务方法里避免散落多处导致逻辑不一致。4.3 我为什么坚持所有操作都记录操作日志这个系统从开始设计时我就加入了t_operation_log表记录谁在什么时间对哪个宠物或申请做了操作包括操作内容快照。救助站的工作人员流动性比较大经常出现“上周的猫咪是谁负责登记的”这种查证需求。操作日志让追责和复盘都变得简单。实现上我用了一个自定义注解LogOperation(审核领养申请) AOP 切面的方式在 Controller 层方法上加注解切面里统一记录操作人、IP、参数摘要。这样业务代码不用到处塞日志代码AOP 一行都不用改就能在后续增加新操作类型时自动生效。5. 开发与上线阶段的典型坑从本地能跑到服务器就挂的完整排查写这套系统时我踩了不少坑。挑四个最有代表性的写出来每一个都是排查很久才发现原因希望能让后来人少走弯路。5.1 图片上传路径硬编码导致的部署事故宠物照片上传是我开发的第一个想到“本地测试全过一部署就挂”的功能。本地开发时E:/upload/pet/这种绝对路径跑得欢打包部署到 Linux 服务器后路径不存在上传直接抛异常。后来我改成application.yml里的相对路径配置用upload.dir./uploads再通过接口读取时在WebMvcConfigurer里注册资源映射把/upload/**映射到本地目录。这样代码里不再出现任何绝对路径。另外文件保存时文件名不能直接用宠物名字因为可能有中文或特殊字符我用UUID . 扩展名重新命名图片访问路径也不会乱码。5.2 MyBatis 的#{}和${}一个粗心导致的注入风险与慢查询我在写动态筛选宠物列表时一开始图省事在排序字段和排序方向上直接用了${}拼接比如ORDER BY ${sortField} ${sortOrder}。这在内部系统里问题不大但一旦接口暴露到外网排序字段就可能被拼成恶意 SQL。后来我把排序字段白名单化前端传入的字段名经过sortFieldMap映射只允许create_time、adopt_count、age_month三个白名单字段方向只允许asc或desc其余一律走默认排序。另外提醒一句模糊搜索要用CONCAT(%, #{keyword}, %)而不是字符串拼接避免单引号注入同时配合 MySQL 的字符集设置中文搜索才不会乱码。5.3 时区与日期格式化问题Json 返回的时间总是差 8 小时系统上线后用户反馈领养申请时间在页面上显示比实际时间少了 8 小时。这个坑特别隐蔽。原因是我在生产 MySQL 连接串里没加serverTimezone参数数据库连接用了默认时区和本地的 Asia/Shanghai 不一致。同时在 Spring Boot 返回 JSON 时LocalDateTime默认序列化格式也不能直观展示。我最终的解决方式是在application.yml里统一配置日期格式化并在 JDBC URL 显式指定serverTimezoneAsia/ShanghaiuseUnicodetruecharacterEncodingutf8。至此时间显示稳定下来。5.4 JAR 包部署时的静态资源与外部配置Spring Boot 打包后是单个 jar默认情况下 templates 和 static 目录都在 jar 内部但宠物图片上传后产生的外部文件必须放在 jar 之外否则一旦重新部署就全部丢失。我把上传目录设置在 jar 包所在目录的uploads子目录下并通过启动参数--upload.dir/data/pet-adoption/uploads覆盖默认配置。这样升级版本时图片数据不受影响。部署脚本里还加了停止旧服务、备份旧 jar、启动新 jar 的固定流程避免手动 kill 进程导致数据异常。5.5 三级联动下拉省市区数据到底存编码还是存名字宠物救助站的救助地点需要记录区级信息比如哪个街道发现的流浪狗。前端页面上我做了三级联动下拉但数据库中只存了最终地点的编码。后来发现工作人员录入时会选错市级而且不同员工对同一街道的表述不一致。最终方案是在表里存编码同时提供字典表维护行政区划编码与名称的映射。这样做的好处是统计报表按区聚合时稳定——用编码分组不会因为不同人写“朝阳区”和“北京市朝阳区”导致统计分叉。6. 从系统设计到交付给同类宠物救助/公益管理系统的实现建议到这里整个项目的核心维度已经讲完了。最后综合我自己多轮迭代后的经验给做这类系统、尤其是毕业设计或公益项目外包的朋友几条实在建议。第一如果你的定位是“毕设作品”不要只盯着“跑通”。把设计思想写清楚尤其是这里讲到的业务闭环救助 - 领养 - 回访和状态机设计比堆砌 CRUD 和花哨的框架更能体现项目价值。答辩时能说清楚“为什么这样设计表”“状态流转出了异常怎么办”比单纯说“我用了 Spring Boot MyBatis”强太多了。第二功能开发顺序要有优先级。我建议先从宠物管理模块入手因为它的增删改查最直观适合打底然后做救助登记与健康记录理解一对多的明细数据设计接着做领养申请与审核掌握状态机的核心逻辑最后做回访登记和数据统计。按这个顺序每一步都建立在上一步的基础上不会有“功能全做完但逻辑串不上”的崩塌感。第三给想接“公益管理系统”外包的朋友提醒需求调研比编码重要。很多公益组织的实际流程和你想的不一样——比如有的救助站要求“领养人必须参加一次线下志愿者活动”这个规则如果不在审核表单里设计后期只能在备注里硬塞。我在做这套系统时与救助站负责人反复确认了整整三周的业务细节才动手建表。这个投入非常值得它避免了后来大量的返工。第四关于 Spring Boot 的学习路径如果这个项目是你练手或毕设的第一选择我的建议是不要一上来就啃源码、不要过度追逐最新版本。先用 2.7.x 做出一个完整闭环把 Controller、Service、Mapper 三层结构写熟练理解 Spring Boot 自动配置的“约定优于配置”思想然后回头去看 Starter 的内部实现你的理解深度会和直接追新版本完全不同。做项目的目的是建立完整的工程思维而不是证明你会议多新的关键词。装一个最新版 Spring Boot 能跑通 Hello World和能设计出这套状态机并成功上线是完全不同的两回事。在系统的交付文档里我最后加了一页“使用者需知”包含每天备份数据库的定时脚本、照片上传后定期同步到移动硬盘的提醒以及“三个月内回访率要超过 80%”的系统内统计报表查看方法。看到救助站工作人员真的通过这套系统查到了某只猫是哪个志愿者在哪条街救回来的那一刻才觉得这套系统真正起效了。整个过程做下来最有成就感的并不是代码本身而是系统真的帮助一个公益团队把以前一团乱麻的纸质记录变成了一套清清楚楚可追溯的业务闭环。
返回列表