前段时间我接到一个开发任务要做一套基于JavaSpringBootSSM的宠物领养一站式服务系统。刚看到这个标题时我下意识觉得这不就是标准的“毕设三件套”吗——SpringBoot做后端、SpringMVC管请求、MyBatis写数据库再把前端模板一套CRUD一拼就完事。但真正动手才发现宠物领养这个业务场景比想象中要复杂它不只是“宠物信息发布用户点击申请”这么简单中间还涉及宠物状态管理、领养审核流程、申请状态流转、图片上传、公告维护等一系列联动逻辑。把这些东西理清楚再结合源码、LW论文、调试文档和讲解视频的交付要求整个项目才算是真正做完整了。这篇文章我想完整复盘一遍这个系统的设计与实现过程从需求拆解、技术选型、数据库设计到核心代码逻辑、调试踩坑、交付物整理。如果你正在准备同类毕设或者想用SpringBootSSM从头做一套能答辩、能演示、能拿源码去交差的项目这篇内容可以帮你省下大量试错时间。1. 为什么宠物领养值得做成一个系统从业务痛点倒推需求清单1.1 线下领养流程的信息断层很多人觉得宠物领养系统就是个“宠物信息展示网站”实际上线下领养场景里的痛点远不止展示这一步。我接触过一些做动物救助的团队他们最头疼的事情有这几类宠物信息散落在微信群、朋友圈没有统一的展示渠道领养人不知道有哪些宠物正在等待领养。领养申请靠聊天记录确认救助人经常搞混申请人和宠物之间的对应关系也很难追踪“谁申请了哪只猫/狗”。领养审核过程没有记录审核通过后宠物是否被接走、是否完成回访全靠线下记忆。领养人想反馈养宠情况找不到一个固定入口留言和咨询散落在各个平台。这些问题的本质是信息不对称和流程缺失。系统要做的就是把这些线下的离散行为收敛成一条可追踪的业务链路宠物信息录入后台 → 用户浏览检索 → 提交领养申请 → 管理员审核 → 审核结果通知 → 宠物状态变更 → 用户查看进度。1.2 角色权限只用两种角色也能把流程讲清楚在需求收敛阶段最忌讳一上来就设计“用户、管理员、送养人、志愿者”四种角色。角色越多权限控制越复杂开发量成倍增加但核心业务并没有变得更完善。对于这套系统我把角色收敛为两类角色主要权限说明普通用户注册登录、浏览宠物、提交领养申请、查看申请进度、留言咨询、修改个人信息未登录用户只能浏览不能申请和留言管理员登录后台、宠物信息增删改、审核领养申请、发布公告、管理留言、数据统计管理员不参与前端领养申请只做管理动作这样的角色划分足够支撑答辩时讲清楚“权限控制”这个必考点同时不会让代码结构失控。系统实现时通过Session保存登录用户信息自定义拦截器判断用户是否登录、是否管理员就能在Controller层统一做权限拦截。1.3 功能列表收敛成模块我最终把系统拆成以下功能模块每一个模块都对应明确的页面和接口用户模块注册、登录、退出、个人信息修改宠物模块宠物列表展示、分类检索、宠物详情、宠物图片展示领养模块提交领养申请、我的申请列表、申请状态跟踪审核模块管理员查看申请列表、通过/拒绝申请、宠物状态联动更新公告模块公告发布、公告列表展示留言模块用户留言咨询、管理员查看和回复后台管理管理员登录、宠物管理、申请管理、公告管理这里我建议开发时用表格列一份需求清单每做完一个模块就打个勾。因为后面写LW论文的时候这份清单就是“系统功能需求分析”那一章的底稿直接改改就能用。2. SpringBootSSM这套组合到底怎么分工技术选型复盘2.1 先搞清楚SpringBoot和SSM根本不是二选一这恐怕是新手最容易搞混的一点。SSM是Spring SpringMVC MyBatis的缩写而SpringBoot是一个整合性的开发框架它把Spring、SpringMVC这些组件的配置都自动完成让开发者可以通过starter依赖快速搭建项目。所以这个项目的技术栈准确描述是“基于SpringBoot框架整合SSM三件套”不是“SpringBoot替换了SSM”。这套组合在项目里的实际分工是这样的组件在系统中的位置承担的任务Spring整个应用的容器管理Service、Mapper等Bean的创建和依赖注入SpringMVCWeb层接收浏览器请求调用Service后返回页面或JSON数据MyBatis持久层通过Mapper接口和XML文件操作MySQL数据库SpringBoot基础设施内置Tomcat、自动配置数据源、简化依赖管理用生活化一点的说法SpringBoot是“装修好的精装房”你不用亲手配水电写一堆XML配置但屋里用的仍然是Spring和SpringMVC这套水电系统MyBatis则是你定制家具的师傅怎么查询数据库完全由你说了算。2.2 版本组合怎么选稳定优先别追新我给这套系统选择的版本组合是JDK 1.8Spring Boot 2.7.xMyBatis Starter 2.2.xMySQL 5.78.0也可以注意驱动配置差异Maven 3.6Thymeleaf模板引擎Layui或Bootstrap做后台页面样式为什么不选Spring Boot 3.x因为Spring Boot 3要求JDK 17起步而且很多老教程、老依赖的兼容性要重新验证。对于要快速交付、要稳定运行的毕设项目来说Spring Boot 2.7 JDK 8是经过大量项目验证的稳妥组合遇到问题搜索解决方案也最容易。2.3 工程结构的组织方式我习惯的包结构是这样的src/main/java/com/example/petadopt ├── controller // 控制层接收请求 ├── service // 业务层处理业务逻辑 │ └── impl ├── mapper // MyBatis的Mapper接口 ├── entity // 实体类对应数据库表 ├── config // 配置类拦截器、文件上传等 ├── interceptor // 登录/权限拦截器 ├── common // 通用返回结果、常量类 └── PetAdoptApplication.java // SpringBoot启动类这个分层结构清晰特别适合答辩时讲解“三层架构”。Controller只负责接参数和返回Service负责业务规则Mapper只做数据访问。有人喜欢在Controller里直接写业务代码图省事但后面改需求的时候会哭——尤其是审核流程这种状态联动逻辑放在Service层才能做到多处复用。2.4 环境准备里最容易卡壳的细节Maven仓库下载依赖慢建议把中央仓库镜像换成国内镜像这一步能省下大量等待时间。JDK、Maven、MySQL安装完成后先别急着写代码优先跑一个最简单的SpringBoot启动Demo验证环境通顺再开始搭业务代码。MySQL连接串里注意设置characterEncodingutf8和serverTimezoneAsia/Shanghai否则后面查询中文乱码、时间字段报错会让你怀疑人生。3. 数据库设计一张宠物表怎么撑起整条领养业务链3.1 五张核心表的结构设计数据库是整个系统最值得花时间的地方因为业务逻辑最终都落在数据上。我最终设计了一张核心业务表加几张辅助表t_user用户表t_pet宠物信息表t_adopt_apply领养申请表t_notice公告表t_message留言表其中宠物表是整个系统的核心字段设计如下字段名类型说明idint主键自增pet_namevarchar(50)宠物名称pet_typevarchar(20)宠物类型猫/狗/其他pet_breedvarchar(50)品种pet_agevarchar(20)年龄描述如“2个月”“3岁”pet_gendervarchar(10)性别pet_healthvarchar(255)健康状况描述如“已驱虫”“已打疫苗”pet_imagevarchar(255)宠物图片路径pet_desctext宠物详细介绍statusint宠物状态0空闲可领养1已申请待审核2已领养完成3下架create_timedatetime录入时间这里有个容易忽略的细节pet_age我设计成varchar而不是int因为救助站登记的宠物年龄经常是“约3个月”“同龄”用数字存反而不好表达。这种字段设计的取舍在写LW的系统设计章节时非常加分。领养申请表的重点是状态字段和关联字段字段名类型说明idint主键pet_idint申请的宠物IDpet_namevarchar(50)冗余存储宠物名称user_idint申请用户IDuser_namevarchar(50)冗余存储用户名reasonvarchar(500)领养理由contactvarchar(50)联系电话addressvarchar(255)居住地址statusint0待审核1审核通过2已拒绝3已完成apply_timedatetime申请时间audit_timedatetime审核时间冗余这两个字段的目的是减少联表查询。展示申请列表时直接读pet_name和user_name不用每次都用INNER JOIN去关联宠物表和用户表。对于这种数据量不大、查询频繁的系统冗余读取绰绰有余还让SQL简单了不少。3.2 领养申请的状态流转逻辑状态流转是这个系统业务最核心的部分也是答辩时最可能被追问的地方。我的设计是用户提交领养申请 → 申请状态为0待审核同时宠物状态变为1已申请待审核管理员通过申请 → 申请状态变为1审核通过宠物状态变为2已领养完成管理员拒绝申请 → 申请状态变为2已拒绝宠物状态恢复为0空闲可领养用户取消申请 → 申请状态变为某个取消值宠物状态恢复为0为什么要设计宠物状态和申请状态两个字段联动因为如果不加宠物状态可能会出现“同一只宠物被多个用户同时申请”的冲突。我在Service层做了校验只有宠物状态为0时才能提交申请。这个设计在讲项目亮点时可以直接说——“通过宠物状态字段避免并发领养冲突”比单纯说“我用了外键关联”要高级得多。3.3 外键到底建不建我的做法是不建数据库物理外键只在实体类里保留逻辑关联关系。因为MyBatis体系里外键约束往往带来不必要的麻烦比如删除宠物时提示受约束无法删除。业务层的Service代码已经保证了数据一致性物理外键的缺失对系统功能没有任何影响。但你需要在LW里说明这一点避免答辩老师觉得你是“忘了建外键”。4. 领养主流程从登录到完成关键代码与实现思路4.1 登录注册与拦截器权限控制用户登录的逻辑不复杂核心代码如下PostMapping(/login) public String login(String username, String password, HttpSession session, Model model) { User user userService.login(username, password); if (user ! null) { session.setAttribute(loginUser, user); if (admin.equals(user.getRole())) { return redirect:/admin/index; } return redirect:/index; } model.addAttribute(msg, 用户名或密码错误); return login; }但光有登录逻辑不够真正的权限控制靠拦截器。我写了一个登录拦截器检查Session里是否有用户public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); User user (User) session.getAttribute(loginUser); if (user null) { response.sendRedirect(/login); return false; } return true; } }拦截器注册时特别要注意放行路径否则会出现登录页面的CSS和JS被拦截页面样式全丢了。我在WebMvcConfigurer里配置了静态资源和公开路径的放行registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /register, /index, /pet/detail/**, /css/**, /js/**, /images/**, /layui/**);4.2 宠物列表与动态条件检索宠物列表是用户访问最多的页面必须支持按类型筛选和关键词搜索。这里用MyBatis的动态SQL是最方便的select idselectPetList parameterTypemap resultTypePet SELECT * FROM t_pet where if testtype ! null and type ! AND pet_type #{type} /if if testkeyword ! null and keyword ! AND (pet_name LIKE CONCAT(%, #{keyword}, %) OR pet_breed LIKE CONCAT(%, #{keyword}, %)) /if AND status IN (0, 1) /where ORDER BY create_time DESC /selectwhere标签会自动去掉多余的前缀ANDIF判断则避免了空条件带入SQL导致查不出数据的问题。宠物状态IN0,1的含义是可领养的和已申请待审核的都展示出来但已经领养完成的宠物就不出现在列表中。这里有一个UI上的处理细节宠物状态为1时详情页要显示“领养申请审核中”的标识按钮置灰不可点避免用户重复提交申请。4.3 提交领养申请的后端校验逻辑前端按钮置灰只是用户体验优化真正防重靠Service代码。提交申请的Service方法需要三步public boolean addApply(AdoptApply apply) { // 1. 检查宠物状态只有0才能申请 Pet pet petMapper.selectById(apply.getPetId()); if (pet null || pet.getStatus() ! 0) { return false; } // 2. 检查用户是否已经申请过这只宠物 int count adoptApplyMapper.checkExist(apply.getUserId(), apply.getPetId()); if (count 0) { return false; } // 3. 插入申请记录同时更新宠物状态为1 adoptApplyMapper.insert(apply); petMapper.updateStatus(apply.getPetId(), 1); return true; }校验放在Service层而不是Controller层是因为Service层是可复用的业务逻辑将来如果做移动端接口这一层直接暴露出去就能用。还有一点这里不能用pet.getStatus() 0来判断要始终想着数据库里的值可能是null用Integer.valueOf(0).equals(pet.getStatus())更稳妥。就是这类小细节写代码时多留个心眼调试时能省很多时间。4.4 管理员审核两条链路的数据一致性管理员审核申请时系统做的事是更新申请记录的状态、审核时间和审核意见同时更新宠物的状态。因为涉及两个表的写操作我一开始把两个更新直接写在了Controller里后来改成在Service方法里用Transactional注解包起来Transactional public void auditApply(Integer applyId, Integer auditStatus) { AdoptApply apply adoptApplyMapper.selectById(applyId); if (apply null) { throw new RuntimeException(申请记录不存在); } // 更新申请状态 adoptApplyMapper.updateAudit(applyId, auditStatus); // 根据审核结果更新宠物状态 if (auditStatus 1) { // 审核通过宠物变为已领养 petMapper.updateStatus(apply.getPetId(), 2); } else if (auditStatus 2) { // 审核拒绝宠物恢复可领养 petMapper.updateStatus(apply.getPetId(), 0); } }Transactional保证了两条更新要么都成功要么都回滚避免出现“申请通过了但宠物还是待审核状态”这种数据不一致的尴尬情况。这是答辩时讲“事务控制”的现成例子也直接在LW的测试章节里可以作为“数据一致性测试”的用例。5. 最容易翻车的几个地方调试实录与避坑经验5.1 MyBatis条件判断的经典大坑我调试时遇到过最莫名其妙的问题按宠物类型“猫”查询时返回的数据是空的。单看SQL片段觉得没问题后面才发现是MyBatis的IF判断触碰到了“值为0”这个特殊场景——如果type传入0if testtype ! null and type ! 这个判断中MyBatis会把0解析成false。但在我的系统里宠物类型的值都是字符串“猫”、“狗”不存在数字0的问题。真正踩坑的是宠物状态的查询我用status0查可领养的宠物动态SQL写好之后怎么都查不出数据。排查了好几轮最后发现是实体类里的status字段初始化成了null把它当成参数传入XMLif判断里既匹配不到非空条件SQL又把null拼进去导致WHERE status null永远查不到数据。这个坑的教训是对所有条件字段做统一处理在Controller里先把参数整理好null值统一设为空字符串避免把null传给MyBatis。5.2 图片上传的路径与访问之谜宠物图片上传是另一个经典翻车点。一开始我把图片保存路径写成D://upload/上传功能一切正常但前端页面死活加载不出图片。查了很久才发现SpringBoot默认是不把本地磁盘路径映射到URL的。解决办法是在配置类里加一个虚拟路径映射Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { String uploadPath System.getProperty(user.dir) /upload/; registry.addResourceHandler(/upload/**) .addResourceLocations(file: uploadPath); } }这样访问/upload/xxx.jpg就会被映射到项目运行目录下的upload文件夹。上传时也记得对文件名做重命名不然用户上传一张1.jpg另一台机器的一张图片也叫1.jpg就会互相覆盖。我习惯用时间戳随机数的方式重新生成文件名String fileName System.currentTimeMillis() _ UUID.randomUUID().toString().substring(0, 8) . originalFilename.substring(originalFilename.lastIndexOf(.));5.3 日期格式化与JSON返回的兼容问题涉及申请记录列表展示时页面上显示的时间经常出现“2023-10-05T12:00:00”这种带T的格式非常难看。这是因为Java 8的LocalDateTime默认序列化格式和前端期望不一致。我的处理方式是在配置类里统一设置全局JSON序列化格式Bean public Jackson2ObjectMapperBuilderCustomizer jsonCustomizer() { return builder - { builder.serializers(new LocalDateTimeSerializer( DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); }; }如果是返回视图Thymeleaf模板则直接在页面里用#dates.format()或者${#temporals.format()}处理这个需要根据实际模板引擎来选择。只要你后端返回的是JSON给前端渲染上面这个全局配置基本能一次性解决。5.4 拦截器放行顺序先静态资源后业务路径拦截器配置的匹配顺序也很容易出问题。Spring的拦截器匹配用的是Ant路径规则/**匹配所有路径。如果你先写了addPathPatterns(/**)再写excludePathPatterns(/css/**)静态资源放行是生效的但如果你把拦截器注册顺序搞错或者路径写成了/css/*只放行一级目录/css/style.css其实需要/css/**才能匹配前端页面会直接崩掉。这里我给一个排查技巧如果页面样式加载不出来先打开浏览器开发者工具F12看CSS资源的请求状态。如果是302跳转到登录页基本可以确定是拦截器把静态资源拦了此时检查excludePathPatterns里的路径是否用对了**通配符。6. 交付物整理源码、LW、调试文档和讲解怎么准备才不返工6.1 LW论文的章节组织思路这套系统交付时要求带源码、LW、调试文档和讲解LW是最容易让开发者头疼的部分因为它不是写代码而是把项目过程写成一篇规范论文。我的建议是直接按照这个结构来组织摘要写清楚系统解决的问题、采用的技术、实现的功能。300字左右重点提“SpringBootSSM”“宠物领养一站式服务”“审核流程”这几个关键词。绪论背景和意义线下领养痛点、国内外现状简单带过、论文结构。需求分析功能需求列表、角色分析、用例图用Visio或ProcessOn画。系统设计架构图、技术选型说明、数据库设计表结构、E-R图。系统实现每个模块的核心代码截图注意代码不要贴太多每段配一两句注释式说明。系统测试功能测试用例表、测试结果、典型测试场景描述。写LW最大的技巧是所有图表从项目里现成导出来功能截图用真实页面截图E-R图用数据库设计工具生成。不要让论文里的图和实际代码对不上答辩最怕这种低级扣分项。6.2 调试文档应该列出什么调试文档是给“拿到源码的人”用的也是给未来的自己用的。我整理调试文档时固定包含以下几项项目内容环境要求JDK 1.8、Maven 3.6、MySQL 5.7/8.0数据库初始化提供pet_adopt.sql脚本包含建表和测试数据配置文件修改application.yml中数据库用户名密码、上传路径等启动步骤导入项目、执行SQL、启动Application、访问地址、管理员账号常见问题端口占用、数据库连接失败、依赖下载失败的处理办法调试文档不要写大而全的话就写“怎么跑起来”。能一条命令跑通就不要写三句话能配截图就配截图。6.3 演示讲解的节奏安排我做技术演示一般控制在8到10分钟节奏是先讲线下痛点30秒再讲系统技术架构1分钟然后演示用户端核心流程3分钟接着演示管理端审核链路3分钟最后讲项目的两个亮点2分钟。演示环节有一个特别注意的细节提前准备好测试账号和演示数据不要现场注册。现场注册如果遇到网络问题、短信验证码问题会直接影响答辩效果。我通常用一个固定账号密码比如admin/123456提前录入三只不同类型的宠物其中一只状态设置为“待审核”这样演示时直接能看到申请列表里有数据可审。另外演示时优先展示“提交申请→管理员审核→宠物状态变化”这条完整链路因为它最能体现系统的业务闭环也比单纯点来点去看列表要更有说服力。最后再分享一点做同类项目的实在体会我做完这套宠物领养一站式服务系统最大的感受是这类项目代码量不大但它特别考验你对业务状态和数据关系的设计能力。宠物状态和申请状态怎么联动、哪些校验放在Service层、哪些字段需要冗余这些想清楚了写代码其实很快想不清楚后面改起来就是拆东墙补西墙。动手之前先花半天时间把数据库表、状态流转、核心流程图在纸上画清楚。这一步做扎实后面无论是写代码、写LW还是准备讲解都会顺很多。准备答辩时把“状态联动校验”“事务保证数据一致”这两个点反复演练一下基本不会被问倒。祝你的项目也顺利跑通、一次过审。