
系统确实能做而且做出来后你会发现技术本身没什么门槛真正花时间的是业务流程的设计——比如一只宠物从发布到被领养中间要经过几个状态谁来申请、谁审批、审批不通过怎么办这些想清楚了代码反而是水到渠成的事。这篇文章我就按自己实际开发的经验来拆一拆这个系统从选题价值、技术组合、数据库设计到核心代码怎么组织、SpringBoot整合SSM的坑怎么避、最后部署上线和面试答辩怎么讲一条线讲透。不管你是正在为毕设发愁的学生还是想拿个完整项目练手的Java学习者照着这个思路走都能少走很多弯路。1. 先聊点实际的这系统到底解决什么问题1.1 线下宠物领养的真实痛点宠物领养这件事在线下其实是一堆信息孤岛。救助站、个人送养人、想领养的人各方信息散落在微信群、朋友圈、甚至一张张纸质登记表里。送养人不知道谁是真的想养领养人不知道哪里有猫可领救助站每天接到大量询问电话来回沟通成本极高。更麻烦的是审核环节。你没法知道申请领养的人是否靠谱领养之后宠物过得怎么样有没有被转卖、弃养。很多救助组织其实有回访制度但全靠人工电话和微信联系时间一长就断了。我见过一个做救助的朋友手机通讯录里存了几百个领养人的名字靠备注“花花-已领养-3月回访”来管理看着都累。所以一个能集中展示宠物信息、把领养申请和审核流程搬到线上、并且记录每只宠物去向的管理系统几乎是刚需。1.2 这个系统的业务本质是什么如果你把需求翻译成技术语言会发现这个项目的核心其实是一套标准的管理信息系统MIS——普通用户注册、登录、浏览宠物列表、查看宠物详情、提交领养申请、查看申请进度。管理员登录后台、发布宠物信息、审核领养申请、管理用户、发布公告。系统级功能宠物图片上传、领养申请状态流转、分类搜索、分页展示。用生活化一点的话说这就像一个小型“中介平台”宠物是房源领养人是租客管理员是中介。房源要上架租客要提交资料中介要审核把关成交之后房源状态从“待出租”变成“已租出”。想清楚这个模型整个系统的模块划分就非常清晰了。1.3 为什么这个选题在毕业设计里长盛不衰每年到了毕设选题季宠物领养管理系统都是热门。原因很简单业务链条完整但复杂度适中。它既有基本的增删改查用户、宠物、公告又有带状态流转的业务逻辑领养申请审核还有文件上传宠物图片、权限控制管理员和普通用户、搜索分页这些高频考点做完之后能讲的技术点非常多。而且选题本身带公益属性答辩时导师问“你这个项目有什么意义”答案现成的不用担心无话可说。最关键的是这套系统的工作量对于一个人来说刚好——不靠团队协作也能独立完成代码量不大但五脏俱全特别适合用来检验自己对SpringBoot和SSM的掌握程度。2. SpringBoot和SSM那点事为什么这么组合不别扭2.1 先搞清楚概念上的误解很多新手看到“JavaSpringBootSSM”这种标题就懵了SpringBoot和SSM不是两套东西吗怎么放在一起说真的这个误解太常见了。听我一句话理解SSM Spring SpringMVC MyBatis是三个框架的组合负责解决“对象管理、Web请求处理、数据库操作”三类问题。SpringBoot Spring家族推出的快速开发脚手架本质上是把Spring、SpringMVC这些底层框架包了一层通过自动配置和起步依赖让你不用写一堆XML配置就能跑起来。所以“SpringBoot整合SSM”的意思是用SpringBoot作为项目骨架底层仍然使用SpringMVC处理请求、MyBatis操作数据库。SpringBoot没有替代SSM只是把Spring和SpringMVC的配置过程大幅简化了。你可以理解为SSM是一套精装修的施工方案而SpringBoot是“拎包入住”的成品房但水电管线那套底层工艺还是同一个师傅做的。2.2 为什么毕设项目和实际小项目都推荐这套组合我自己的评价是这套组合是当前Java Web项目里“性价比”最高的搭配尤其适合一个人开发的中小型系统。求职面试有话题Java岗位面试必问SSM和SpringBoot做完这个项目等于把高频考点全部实战了一遍。参考资料最全从上世纪风格的XML配置教程到最新的SpringBoot 2.x案例搜“SpringBootMyBatis”能搜出一整片海洋踩坑时永远能找到前人经验。开发效率足够SpringBoot的自动配置把繁琐的Bean声明全干了你只需要专注业务代码。MyBatis的SQL可控性强复杂的关联查询、状态更新自己写SQL反而更直接。当然要承认如果是大型互联网项目SpringBootMyBatis不是唯一选择Spring Cloud全家桶、MyBatis-Plus、JPA都有各自生态位。但在“一个人从零到上线跑通一个管理系统”这个场景下SSM组合就是最顺手的那把刀。2.3 版本搭配建议血泪教训版本问题我一个一个踩过这里直接给你一套稳妥的搭配方案照着配基本不会翻车组件推荐版本原因JDK1.8 或 11生态兼容性最好大量教程基于JDK8编写SpringBoot2.7.x稳定且资料多支持JDK8/11MyBatis Startermybatis-spring-boot-starter 2.2.2与SpringBoot 2.x配合成熟MySQL5.7 或 8.0两个版本都行注意驱动的区别数据库驱动MySQL 8对应mysql-connector-j 8.x5.7可选5.1.49驱动版本不对会连不上库Lombok1.18.x简化实体类代码IDEA需装插件这里特别要提醒一句不要一上来就上SpringBoot 3.x。SpringBoot 3要求JDK 17起步而且把javax包名换成了jakarta很多网上教程的代码直接跑不起来对于想快速完成毕设或课设的同学来说纯属给自己挖坑。如果没有特殊要求SpringBoot 2.7全家桶就是最稳妥的选择。2.4 前后端方案怎么选模板渲染还是前后端分离这是一个很容易纠结的问题。我的建议非常明确如果任务是“独立完成一个管理系统”首选Thymeleaf模板渲染不推荐强行上Vue前后端分离。原因有两个。第一模板渲染模式下后端同学不用处理跨域问题CORS、预请求、Token鉴权这些额外的复杂度全部绕开了第二页面直接由Controller渲染返回调试的思路更线性出问题容易排查。前后端分离的优势是在多人协作、大型团队时才能体现出来单人开发纯属增加工作量。当然如果你对Vue已经熟练掌握做前后端分离也完全可以只要记得在后端处理好跨域配置和登录状态传递。这篇主要以Thymeleaf方案为主线来讲因为对大多数人来说这是最快跑通的一条路。3. 模块和数据库动工之前先想清楚的几件事3.1 角色权限这套系统的使用者只有两类人权限模型没必要搞RBAC那种复杂设计这套系统里两类角色就够用管理员role0除了不能用普通用户的领养功能其他都能干——发布宠物、审核申请、管理用户和公告。普通用户role1浏览宠物、查看详情、提交领养申请、查看自己的申请进度、修改个人信息。权限控制的做法很简单登录时把用户对象放进Session拦截器里取Session判断是否登录到管理员接口时再校验一次role字段。后面第5章我会给出具体实现代码。3.2 核心业务流程一只宠物从发布到被领养要经过哪些状态画清楚业务流转比写代码重要得多。这套系统的核心链路是管理员发布宠物状态待领养 ↓ 用户在列表看到宠物点击提交领养申请申请状态待审核 ↓ 管理员审核申请通过 / 拒绝拒绝需填写备注 ↓ 通过 → 宠物状态更新为“已领养” 拒绝 → 用户看到拒绝原因宠物继续待领养这里有一个非常关键的细节宠物状态和申请状态是两个独立的状态字段不能混在一起。宠物状态只关心“是否还在等待领养”申请状态关心的是“这条申请走到哪一步了”。很多新手在这里会犯迷糊把两个状态搞成一个字段结果一只宠物被多人申请后状态直接乱套。3.3 数据库表设计四张核心表加两张辅助表我建议的表结构是这样的user 用户表字段类型说明idint主键自增usernamevarchar(50)登录名唯一passwordvarchar(100)BCrypt加密后的密码nicknamevarchar(50)昵称phonevarchar(20)联系电话avatarvarchar(255)头像路径roletinyint0管理员 1普通用户create_timedatetime注册时间pet 宠物表字段类型说明idint主键自增namevarchar(50)宠物名字categoryvarchar(20)分类猫/狗/其他breedvarchar(50)品种ageint年龄月gendervarchar(10)性别health_statusvarchar(50)健康状况descriptiontext描述imagevarchar(255)图片路径statustinyint0待领养 1已领养 2下架create_userint发布人IDcreate_timedatetime发布时间adoption_apply 领养申请表字段类型说明idint主键自增pet_idint申请领养的宠物IDuser_idint申请人IDreasonvarchar(500)领养理由statustinyint0待审核 1通过 2拒绝audit_remarkvarchar(255)审核备注拒绝原因apply_timedatetime申请时间audit_timedatetime审核时间notice 公告表id、title、content、create_time管理员发布站内公告用。comment 留言表id、pet_id、user_id、content、create_time用户可以留言咨询宠物情况。几个设计上的小心得状态字段用tinyint数字不要用字符串。一方面查询效率高另一方面扩展方便以后状态增加到3、4数字编码比字符串更灵活。表与表之间用逻辑关联不用物理外键。毕设项目经常需要演示删除功能物理外键会在删除关联数据时各种报错逻辑外键靠代码控制完全够用而且后期改起来轻松。所有时间字段都用datetime千万不要图省事存字符串。后面按时间排序、统计时你会感谢当初的决定。密码一定不能明文存用BCrypt加盐哈希MD5那种方案现在基本等于裸奔。4. 核心实现思路从登录到领养审核这条路怎么走通4.1 登录鉴权用Session加拦截器简单还能讲清楚登录方案我推荐老牌的Session方案别一上来就搞JWT。为什么因为这套系统是模板渲染没有前后端分离的需求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; } }再写一个管理员拦截器继承登录拦截器的逻辑额外校验rolepublic class AdminInterceptor extends LoginInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { boolean loggedIn super.preHandle(request, response, handler); if (!loggedIn) { return false; } User user (User) request.getSession().getAttribute(loginUser); if (user.getRole() ! 0) { response.sendRedirect(/index); return false; } return true; } }在WebConfig里注册拦截器这一步非常容易踩坑的是放行路径。登录接口、注册接口、静态资源CSS、JS、图片都必须放行否则页面会一片空白Configuration public class WebConfig implements WebMvcConfigurer { Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new LoginInterceptor()) .addPathPatterns(/**) .excludePathPatterns(/login, /register, /index, /static/**, /images/**, /pet/list, /pet/detail/**); registry.addInterceptor(new AdminInterceptor()) .addPathPatterns(/admin/**); } }4.2 宠物发布与图片上传路径存储的坑一次说清宠物发布是管理员的核心操作表单里除了文本信息还有一张图片。SpringBoot处理文件上传非常简单用MultipartFile接收即可但有一个点非常容易踩坑——数据库里存什么路径页面上怎么回显。我的做法图片保存到项目的src/main/resources/static/images/uploads/目录本地调试时直接用绝对路径保存数据库只存相对路径如/images/uploads/xxx.jpg。页面直接用img th:src${pet.image}就能访问因为SpringBoot默认把static目录映射到根路径/images/uploads/xxx.jpg自动对应到静态目录里的文件。文件上传的配置写在application.yml里默认限制只有1MB上传大图会直接报错spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB如果你部署到云服务器图片保存到绝对路径是更好的方案比如/data/pet-images/然后通过一个虚拟路径映射到静态资源。本地毕设的话用static目录最省事打包成jar时图片也会一并打进去。4.3 领养申请的状态流转并发场景下的核心代码这是整个系统里最值得讲的一段代码也是面试时最能体现水平的点。先看提交申请的Service逻辑Service public class AdoptionApplyService { Autowired private PetMapper petMapper; Autowired private AdoptionApplyMapper applyMapper; Transactional public void submitApply(Integer petId, Integer userId, String reason) { Pet pet petMapper.findById(petId); if (pet null || pet.getStatus() ! 0) { throw new RuntimeException(该宠物暂不可领养); } AdoptionApply apply new AdoptionApply(); apply.setPetId(petId); apply.setUserId(userId); apply.setReason(reason); apply.setStatus(0); // 待审核 apply.setApplyTime(new Date()); applyMapper.insert(apply); } }这里有三层思考校验合法性宠物必须存在且状态是待领养不能对已领养的宠物提交申请。事务保护加Transactional确保校验和插入操作要么全部成功、要么全部回滚。为什么不在这里直接改宠物状态因为申请需要管理员审核提交申请不等于领养成功宠物状态必须在审核通过时才变更。再看审核通过的方法这里有个并发问题很多人会忽视Transactional public void approveApply(Integer applyId) { AdoptionApply apply applyMapper.findById(applyId); if (apply null || apply.getStatus() ! 0) { throw new RuntimeException(申请状态异常); } // 关键这条update语句自带条件判断防止并发下两只猫被同一只宠物申请成功 // 场景管理员同时点了两个不同申请者的“通过”按钮 int rows petMapper.updateStatusIfAvailable(apply.getPetId(), 1); if (rows 0) { throw new RuntimeException(该宠物已被其他申请领养); } apply.setStatus(1); apply.setAuditTime(new Date()); applyMapper.updateStatus(apply); }对应的Mapper SQLupdate idupdateStatusIfAvailable UPDATE pet SET status #{status} WHERE id #{petId} AND status 0 /update这里的核心逻辑是通过UPDATE ... WHERE status 0影响行数来判断是否更新成功。如果两条审核请求同时到达数据库行锁会保证只有一个请求更新成功另一个影响行数为0直接抛异常。这个方案比先查再改靠谱得多也是“乐观锁”思想的一种落地实践。4.4 搜索、分页和分类展示宠物列表页支持按分类筛选、按关键字搜索、分页展示。分类筛选直接用MyBatis的动态SQL判断条件搜索用LIKE CONCAT(%, #{keyword}, %)拼接关键字注意不要直接拼字符串进SQL防SQL注入的习惯要养成。分页我用的是MyBatis官方分页插件PageHelper配置很简单PageHelper.startPage(pageNum, pageSize); ListPet petList petMapper.selectPetList(category, keyword); PageInfoPet pageInfo new PageInfo(petList);PageHelper的底层原理是拦截器自动拦截要执行的SQL在语句后面拼接LIMIT核心的点在于PageHelper.startPage()之后的第一条SQL会被分页所以必须紧跟在查询语句前调用中间不要插入其他数据库操作。这个坑我见很多人踩过——startPage之后先执行了别的Mapper查询分页结果全乱了。5. 我把源码调通的完整过程这些坑你大概率也会踩这一章把我实际调试过程中遇到的高频问题按排查链路整理出来。每个问题我都按“现象 → 排查思路 → 根因与解决”的顺序讲而不是直接告诉你答案这样下次遇到问题时你能自己定位。5.1 启动报错找不到MyBatis的Mapper现象项目一启动就报Invalid bound statement (not found): com.xx.mapper.UserMapper.selectByUsername。排查链路我先打开启动类看有没有加MapperScan确认加上了然后去application.yml看mybatis.mapper-locations配置最后发现问题是Mapper XML文件路径写错我放在resources/mapper目录配置里写的却是classpath:mybatis/*.xml。根因与解决SpringBoot整合MyBatis时有两个地方必须对应——启动类加了MapperScan(com.example.petadoption.mapper)同时配置文件指定XML位置mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.petadoption.entity configuration: map-underscore-to-camel-case: true这里顺手说一下map-underscore-to-camel-case这个配置它能把数据库的create_time自动映射到实体类的createTime省掉大量resultMap手写工作。推荐打开除非你的字段命名风格不一致。5.2 页面样式全丢了拦截器把静态资源拦了现象登录页能打开但是没有任何CSS样式页面惨不忍睹。排查链路按F12看Network发现CSS请求全被重定向到/login了。进一步检查所有静态资源请求都返回302那我基本确定是登录拦截器把静态资源也拦截了。根因与解决拦截器配置的放行路径没有包含静态资源。SpringBoot 2.x中静态资源默认路径是/static/、/public/、/resources/、/META-INF/resources/拦截器不会自动放行必须在excludePathPatterns里手动排除.excludePathPatterns(/static/**, /css/**, /js/**, /images/**, /error)这个坑在模板渲染方案里非常高频几乎每次写新拦截器都会碰到一次。记住一点拦截器放行路径是所有Web项目的“天生敏感区”写完先检查静态资源有没有被排除。5.3 图片上传报错超出大小限制现象上传宠物图片时控制台报FileSizeLimitExceededException或前端提示Maximum upload size exceeded。排查链路我先看报错信息明确是MaxUploadSizeExceededException说明是配置问题而不是代码逻辑问题。去查SpringBoot默认的multipart配置发现默认单文件上限是1MB。管理员随便拍一张宠物照片就能超过这个数。根因与解决在application.yml里调大上传限制配置我上面给出过这里再强调一遍max-file-size和max-request-size的区别——前者是单个文件上限后者是单次请求所有文件的总上限。如果只配了前者一次上传多张图还是会踩后者的限制。5.4 数据库查询时间差了8小时现象页面上显示的发布时间比数据库里的实际时间早8个小时。排查链路这个坑几乎是每个用MySQL的Java项目都会碰到。我第一反应是时区问题打开Navicat看数据库里的时间本身是正确的然后看JDBC连接串发现没有配置时区参数。JDBC默认使用服务器系统时区而MySQL驱动的默认时区往往被识别为UTC导致时间转换时少了8小时。根因与解决JDBC连接URL上加上时区参数url: jdbc:mysql://localhost:3306/pet_adoption?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse注意useSSLfalse也建议加上本地开发环境用SSL握手反而会多出一些奇怪警告。5.5 端口被占用8080起不来了现象启动时报Port 8080 was already in use。排查链路这个非常常见尤其是曾经跑过其他项目、或者IDEA调度器异常没有释放端口的情况。Windows下用netstat -ano | findstr 8080查占用端口的进程PID然后去任务管理器确认假如是我之前跑过的僵尸Java进程直接结束掉。根因与解决如果端口确实被其他项目占用最简单的方案是换一个端口server: port: 8081如果频繁遇到“上次启动的进程没退出”可以在IDEA的Run/Debug Configurations里配置热部署时的进程释放策略或者直接改用Spring Boot DevTools。5.6 修改了页面但刷新还是不生效现象改了Thymeleaf模板刷新页面的效果没有变化。排查链路一开始我以为是浏览器缓存强制刷新也没用。然后意识到Thymeleaf有模板缓存默认情况会缓存解析后的模板开发时非常碍事。根因与解决开发环境关闭模板缓存spring: thymeleaf: cache: false但注意如果打包部署到生产环境这个配置要么删掉要么设成true否则每次请求都会重新解析模板性能损耗明显。5.7 Lombok的坑IDEA环境下的连锁问题现象代码里用了Data注解简化实体类但编译报错找不到getter/setter方法。排查链路我先检查pom.xml是否引入Lombok依赖确认引了然后确认IDEA的Settings里Plugins搜索Lombok插件是否安装并启用最后发现是项目导入时没有开启注解处理Annotation Processing。根因与解决IDEA使用Lombok需要三步——依赖引入、插件安装、开启Enable annotation processing。这三者缺一不可。很多人只做了前两步就在IDEA里看到满屏的红色编译错误其实根本不是代码问题。顺带一提Lombok用起来虽爽但面试时如果被问到建议你诚实说明它只是编译期生成getter/setter的语法糖并不影响运行时逻辑。5.8 SQL中文乱码问题现象表单提交的中文保存到数据库后是问号或乱码。排查链路中文乱码的排查链一般有三个位置——页面编码、连接传输编码、数据库存储编码。我先确认HTML页面和模板是UTF-8然后看JDBC连接串最后检查数据库表字符集发现表是latin1存储的。根因与解决在创建数据库时统一指定utf8mb4这是兼容性最好的字符集能存emoji表情连接串里也显式声明编码CREATE DATABASE pet_adoption DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;另外SpringBoot的HttpMessageConverter默认UTF-8一般不需要额外配置。如果用的是表单提交而不是JSON确认页面里的accept-charsetUTF-8即可。6. 部署、测试和上线的最后一公里6.1 本地跑起来的完整步骤如果你拿到源码第一件事不是读代码而是把它跑起来。我给一个标准流程准备数据库用Navicat或命令行执行项目里的SQL脚本创建库表和初始数据。注意SQL文件里有管理员账号和一个演示普通用户初始密码建议用123456登录后能改。改数据库配置打开application.yml把username/password改成你自己的url里的数据库名要和建库时一致。启动项目找到Application启动类右键Run。看到Tomcat started on port(s): 8080就说明成功了。浏览器访问打开localhost:8080先走一遍用户流程——注册、登录、浏览宠物、提交领养申请再用管理员账号登录去后台审核。这里有个经验首次调试不要直接看代码先在页面上把每个功能点一遍把系统的整体流程走通。只有你知道“用户应该看到什么、管理员应该看到什么”才能在代码和页面之间建立对应关系否则看代码就像瞎子摸象。6.2 演示和答辩前必须要准备的几个点账号和演示数据确保管理员和用户账号都能正常登录宠物列表里至少有十几条不同分类的记录数据要真实感强一点——别全是“一只猫”品种、年龄、健康状况、描述都要填完整导师一眼就能看出你是不是认真做的。准备一条完整的业务链路演示时不要求把所有功能点一次讲完但这条链路——管理员发布宠物 → 用户提交申请 → 管理员审核通过 → 用户查看进度 ——必须畅通无阻这是系统的灵魂。准备好“异常演示”比如去申请一只已经被认领的宠物看系统如何处理报错提示。会演示异常处理往往比正常流程更能加印象分。6.3 打包部署到云服务器的关键问题本地跑通之后如果想放到云服务器上步骤也不复杂。先打包mvn clean package -DskipTests然后看target目录下生成的jar包上传到服务器运行java -jar pet-adoption-system.jar但这里有几个服务器特有的坑我逐个提醒安全组端口云服务器控制台的防火墙没放行8080端口外网永远访问不到本地却一切正常这个排查方向一定要有。MySQL远程连接服务器上的数据库要允许远程连接并且给MySQL用户赋权否则应用连不上库。部署时最稳妥的方案是让jar包和MySQL都在同一台服务器上直接用localhost连接少一层网络问题。内存问题如果你用的是2G内存的轻量服务器JVM默认堆内存可能过大导致启动失败。加上参数限制java -Xms256m -Xmx512m -jar pet-adoption-system.jar后台运行直接java -jar关闭终端就停了用nohup后台运行nohup java -jar pet-adoption-system.jar app.log 21 6.4 调试技巧会看日志比会写代码更救命整个调试过程里最有用的习惯是看懂异常堆栈的第一行。很多人一看到报错就慌复制整段到搜索引擎其实90%的问题答案都在异常堆栈的前三行里——哪个类、哪个方法、哪一行代码出了问题信息量非常充足。遇到问题时我的标准流程是先看堆栈定位到具体行号再看上下文逻辑然后看对应配置最后才考虑去搜资料。如果堆栈看不懂就手动在关键位置加日志输出或者断点走查把流程里各个变量的值打出来。千万别一上来就东改西改改半天往往把正常的代码也改坏了。Spring Boot DevTools这个依赖我也建议加上它提供了热部署能力改完代码按一下F9自动重启比每次手动重启快太多。但它只适合开发环境用生产环境要移除。7. 答辩和面试中这个项目可以怎么讲出亮点7.1 技术层面不要只讲增删改查说实话一个管理系统的代码量并不大如果你只回答“我实现了用户管理、宠物管理、申请管理”那答辩和面试的观感就非常普通。你得把“为什么这么设计”讲出来这才是拉开差距的地方。我可以给你一组实实在在的问答方向Q为什么用SpringBoot整合SSM而不是单独用SSM或者直接用SpringBootMyBatis-PlusASpringBoot负责自动配置和依赖管理SpringMVC负责请求路由MyBatis负责SQL控制。三者的分工清晰SpringBoot降低了配置成本MyBatis保留了SQL的灵活性对于业务逻辑包含状态流转的系统这种方式让我能精确控制每一步SQL行为比如用UPDATE pet SET status 1 WHERE id ? AND status 0来实现并发场景下的防重复领养。Q领养申请审核时怎么处理并发问题A如果两个申请同时被审核都通过“先查再改”的逻辑会出问题。我的做法是在更新宠物状态时使用条件更新语句通过影响行数判断是否被人抢先更新这也是乐观锁思想的落地。核心代码就是我上面展示的updateStatusIfAvailable方法。Q拦截器和过滤器的区别A过滤器是Servlet规范中的概念在请求进入Servlet容器后、进入Servlet前执行作用域更广拦截器是SpringMVC的组件基于HandlerMapping和HandlerAdapter可以拿到Handler方法信息还可以通过HandlerInterceptor的preHandle/postHandle/afterCompletion在请求处理前后分别执行逻辑。这套系统里我用拦截器做登录和管理员权限校验放行规则写在addInterceptors方法里。Q事务失效的常见场景有哪些ATransactional默认只在运行时异常时回滚如果方法被内部调用this调用而不是通过代理调用事务会失效如果是非public方法、异常被catch吞掉、或者方法名里用了try-catch抛了检查异常而没指定rollbackFor也会失效。做这个项目时我在Service层对“提交申请”“审核通过”两个关键业务方法加了事务并特意验证了异常情况下数据是否回滚。这些问答你提前准备一遍比临时翻书有效得多。7.2 业务层面一个系统的价值在流程设计项目答辩时最容易回答不出来的问题是“你这个系统相比线下手工操作核心价值在哪里”。这个问题的答案不是“数据存数据库了”而是——信息的集中可见所有待领养宠物统一展示搜索、分类、详情一键到位让合适的宠物能被合适的人看到。流程线上化留痕谁在什么时间申请、管理员什么时间审核、审核意见是什么全流程记录在库不用担心纸质表丢失或者口头承诺没依据。防重复领养机制通过状态控制和条件更新从技术上保证了同一只宠物不会被两个申请者同时认领成功。可追溯、可回访每一只宠物的领养人、联系电话、申请理由都有记录方便后续做回访和追踪。这样一讲你的系统就不只是一堆CRUD页面而是一个真正解决业务问题的工具导师和面试官想质疑都找不到角度。7.3 后续扩展思路这个项目还能长出什么如果还有余力有几个扩展方向都能快速增加项目的亮点给系统加一个微信小程序端用户从小程序里浏览宠物、提交申请后台复用现有接口面试时可以说“我为系统设计了第二个客户端基于微信生态降低了用户使用门槛”。增加消息通知机制审核通过或拒绝后给站内信或邮件通知用户体现流程闭环意识。用定时任务做领养回访提醒SpringBoot自带Scheduled调度器领养成功30天后自动生成回访任务提醒管理员联系领养人。宠物档案扩展给宠物增加疫苗接种记录、驱虫记录形成更完整的健康档案。当然这些扩展不是为了做而做而是要能在文档和答辩里讲清楚“它解决了什么新问题”。加一个功能却讲不出业务价值反而减分。8. 一些实际项目里才有的心得写到这儿我想把做这个项目最值得说的几条经验单独拎出来也是我个人在实际开发过程中体会最深的部分。第一条心得是先画流程图再写代码哪怕只是画在草稿纸上。我当时第一部分代码就直接从用户模块开始写写到领养申请状态流转时发现自己反复改表结构因为状态怎么走根本没想清楚。后来停下来把“提交申请→审核→领养成功/拒绝”的每个分支画清楚代码反而一晚上就理顺了。第二条心得是状态字段的设计决定系统的复杂度上限。用数字编码状态、单独字段管理宠物状态与申请状态这个决策在后来的扩展中带来的收益远超想象。一旦你陷入“用字符串描述状态、一个字段管到底”的做法系统越往后改越痛苦。第三条心得是把代码写“给别人看”比写“给自己跑通”更重要。毕业设计和真实项目最大的区别在于代码是要被讲解、被评审的。包名规范、Controller、Service、Mapper分层清晰、关键方法写注释这些习惯会让你在答辩前名单上少费一百倍口水。我见过太多功能跑通但代码混乱的现场作者讲着讲着被导师一句“你这个Controller为什么写了三百行”问得哑口无言。按时分层、克制散装代码这本身就是专业性的体现。如果你正在做这个项目我真心建议你把第四章里的并发防重逻辑吃透——这是整个系统里技术含金量最高的一段也是面试官最可能追问的一个点。把它讲明白了这个项目在你履历里的价值立刻不一样。以上都是我实际做项目时的经验希望对你有帮助。