做毕设或者课程设计的时候,很多人一上来就纠结“到底用SSM还是SpringBoot”。说实话,这个基于Java+SpringBoot实现的宠物领养管理系统,正好把两套东西揉在了一个项目里:外层是SpringBoot的自动配置和快速启动,内层还是SpringMVC+MyBatis那套熟悉的Controller-Service-Mapper分层。也就是说,你既能用SpringBoot的方式省掉一堆XML配置,又能保留SSM的三层架构思路,对毕设答辩和后面找工作面试都很友好。
这类项目的核心价值在于它覆盖了管理系统最常见的完整链路:用户登录注册、宠物信息发布、领养申请提交、管理员审核、公告管理、留言反馈,几乎是“标准后台管理系统”的模板级功能。不管你是拿它交Java课程设计,还是作为毕业设计的选题,甚至是想给本地宠物救助站做一个信息化管理工具,这套流程都能直接复用。适合的人群也很明确:Java基础刚学完、想通过一个真实项目串起SpringBoot+MyBatis+MySQL的在校生,以及需要快速落地一套管理后台的初级开发者。
下面我就把整个系统的设计思路、数据库规划、核心代码实现和调试过程完整拆开讲一遍。
1. 项目定位与技术选型:SpringBoot如何整合SSM
1.1 为什么选SpringBoot而不是纯SSM
先聊一个很多人想不明白的问题:既然题目里写了SSM,为什么还要用SpringBoot?这两者其实不冲突。
传统SSM项目需要自己配web.xml、Spring配置文件、SpringMVC配置文件、MyBatis配置文件,四份XML堆在一起,光是把环境跑起来就得折腾半天。我见过不少同学,项目代码本身没问题,结果卡在配置阶段,一个ClassNotFoundException查了一晚上。SpringBoot的核心思路是“约定大于配置”,它把Tomcat内嵌进来,把数据源、事务、MyBatis的SqlSessionFactory全部交给自动配置处理,你要做的只是写一个application.yml外加几个注解。用SpringBoot重写SSM,本质上还是那套Controller-Service-Mapper三层,MyBatis的@Mapper照样用,SQL照样写在XML里,但配置量直接砍掉一大半。
另外一个很实际的考虑是:现在大部分毕设答辩老师已经默认SpringBoot是基础操作,纯SSM反而显得项目偏旧。可如果你直接上SpringCloud那套微服务,对毕设来说又太重了,尤其是一个人开发,维护成本不划算。SpringBoot+SSM的组合正好卡在一个比较舒服的位置:既有主流技术栈的样子,又不会因为框架过重导致自己陷入分布式调试的泥潭。
1.2 系统角色与核心流程设计
这个系统我建议拆成三个角色,不用做得太复杂,但每个角色的权限边界要清晰。
- 访客(未登录):可以浏览宠物列表、查看宠物详情、看公告。
- 领养人(普通用户):注册登录后可以提交领养申请、查看自己的申请进度、修改个人资料、给宠物留言。
- 管理员:负责宠物信息审核与上下架、领养申请审批、公告发布、用户管理。
核心业务线只有一条:管理员发布宠物信息,用户浏览后提交领养申请,管理员审核申请是否通过,通过后用户线下完成交接,最后管理员把宠物状态改成“已领养”。整个系统所有的功能表、接口设计基本都围绕这条主线展开,这样你在写论文和画系统架构图的时候,逻辑也会非常顺畅,不会出现功能模块互相打架的情况。
2. 数据库设计与核心功能模块拆解
2.1 五张核心表:从用户到领养申请
这个项目的数据库设计属于“麻雀虽小,五脏俱全”,我按实际开发时最常用的方案给出表结构。考虑到毕设一般不需要搞太复杂的权限表,五张表完全够用。
第一张是用户表tb_user,字段包括id、username、password、phone、real_name、role、create_time。role字段用0表示普通用户,1表示管理员。密码存储建议用MD5加盐或者至少MD5加密,虽然Spring Security更专业,但毕设里为了控制复杂度,用拦截器校验登录状态加上MD5存储已经足够说明安全意识了。
第二张是宠物表tb_pet,字段包括id、name、type(猫/狗/其他)、breed、age、gender、health、image、status、publisher_id、create_time。这里的status是整个系统的关键:0表示待审核,1表示可领养,2表示审核被驳回,3表示已被申请(待线下交接),4表示已完成领养。
第三张是领养申请表tb_adopt_apply,字段包括id、pet_id、user_id、reason(领养理由)、status、apply_time、audit_time、audit_remark。这张表把用户和宠物多对多的关系解开,同时记录了审核意见,方便追溯。
第四张是公告表tb_notice,字段很简单:id、title、content、create_time。
第五张是留言表tb_message,字段包括id、pet_id、user_id、content、reply、create_time。
外键关系上,tb_pet.publisher_id指向tb_user.id,tb_adopt_apply.pet_id指向tb_pet.id,tb_adopt_apply.user_id指向tb_user.id。这种设计既保证了数据完整性,又不会让表关系过于复杂导致自己写SQL时绕晕。
2.2 领养状态机:PENDING到COMPLETED的流转
很多同学做这类系统最容易犯的错是把状态直接弄成一个布尔值,比如is_adopted,领养了就是true,没领养就是false。但真实业务里状态不是只有“未领养/已领养”两个值,中间还有一个“正在审核/待交接”的过渡阶段。
我实际开发时把宠物状态设计成数字状态机,流程是这样走的:
0-待审核 -> 1-可领养 -> 3-已被申请 -> 4-已完成领养 | -> 2-审核驳回(可重新编辑后再次提交)这里有个小细节:当用户提交领养申请时,宠物状态应该从1变为3,而不是直接变为4。这样其他用户再看到这只宠物时会显示“已被申请”,不会出现多人同时申请同一只宠物的情况。管理员在用户完成线下交接后再把状态更新为4,整个过程留有余地,也符合实际救助站的流程。
领养申请这张表的状态同样设计为数字:0待审核、1已通过、2已拒绝。通过后用户可以在前台看到“申请已通过,请联系管理员进行线下交接”之类的提示,后台管理员可以填写审核备注。
3. 实操过程:从零搭建到领养流程跑通
3.1 项目初始化和分层怎么写才不像堆代码
开始写代码之前,先把项目结构理清楚。我推荐用Maven构建,JDK用1.8,SpringBoot版本选2.5.x就好,不要追新。SpringBoot 3.x要求JDK17,很多学校机器的JDK版本不一定支持,反而给自己挖坑。
com.example.petadopt ├── controller // 控制层,接收请求 ├── service // 业务层,处理核心逻辑 ├── mapper // 数据访问层,MyBatis的Mapper接口 ├── entity // 实体类,对应数据库表 ├── config // 配置类,比如拦截器、文件上传配置 └── common // 公共类,统一返回结果、状态枚举application.yml里的关键配置长这样:
spring: datasource: url: jdbc:mysql://localhost:3306/pet_adopt?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.jdbc.Driver servlet: multipart: max-file-size: 2MB max-request-size: 2MB thymeleaf: cache: false mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.petadopt.entity注意mapper-locations一定要配,否则MyBatis扫描不到XML文件里的SQL,启动的时候不报错,一调用Mapper方法就抛Invalid bound statement (not found)。这个错误我相信做过SSM整合的人都遇到过,后面调试部分我会再展开。
3.2 登录拦截与图片上传两个高频功能的落地
登录校验我不建议用特别复杂的Shiro或Spring Security,毕设项目用SpringBoot拦截器就能把逻辑说得清清楚楚。
先写一个AuthInterceptor,在preHandle方法里判断Session里有没有用户信息:
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user = (User) request.getSession().getAttribute("loginUser"); if (user == null) { // 未登录,重定向到登录页 response.sendRedirect("/login"); return false; } // 判断是否是管理员接口 if (request.getRequestURI().startsWith("/admin") && user.getRole() != 1) { response.sendRedirect("/403"); return false; } return true; } }然后在配置类里注册拦截器,并设置放行路径:
@Configuration public class WebConfig implements WebMvcConfigurer { @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new AuthInterceptor()) .addPathPatterns("/**") .excludePathPatterns("/login", "/register", "/logout", "/css/**", "/js/**", "/images/**", "/pet/list", "/pet/detail/**"); } }图片上传这个功能看似简单,但有几个坑很经典。Controller里接收MultipartFile时,要注意文件名的处理,绝对不能直接用用户上传的原始文件名去保存,否则很容易出现重名覆盖和路径穿越问题。我通常的做法是:
String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String fileName = UUID.randomUUID().toString().replace("-", "") + ext; file.transferTo(new File(uploadDir + fileName));上传后的图片访问也是个容易忽略的点。如果你把图片存到了项目的/upload目录下,而SpringBoot默认的静态资源路径又不包含它,前端图片就会全部404。解决办法是在配置类里加一个虚拟路径映射:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceLocations("file:" + System.getProperty("user.dir") + "/upload/"); }这样/upload/xxx.jpg就能正常访问了。实际开发中很多人把图片直接存到数据库的BLOB字段里,我不建议这么做。图片存磁盘、数据库只存路径,既减小数据库体积,又方便静态资源加速。
3.3 领养申请的核心代码:一个事务里做完校验和落库
领养申请是核心业务,一定要考虑完整的事务性。我实现的时候写了这样一个方法:
@Transactional public Result applyAdopt(Integer petId, Integer userId, String reason) { // 1. 校验宠物是否存在且可领养 Pet pet = petMapper.findById(petId); if (pet == null || pet.getStatus() != 1) { return Result.error("该宠物不存在或已被申请"); } // 2. 校验该用户是否已经申请过这只宠物 int count = adoptApplyMapper.countByPetIdAndUserId(petId, userId); if (count > 0) { return Result.error("您已经申请过该宠物,请勿重复提交"); } // 3. 插入申请记录,状态为0待审核 AdoptApply apply = new AdoptApply(); apply.setPetId(petId); apply.setUserId(userId); apply.setReason(reason); apply.setStatus(0); adoptApplyMapper.insert(apply); // 4. 把宠物状态改成3-已被申请 petMapper.updateStatus(petId, 3); return Result.success("申请提交成功"); }加上@Transactional注解后,如果第4步更新宠物状态失败,第3步插入的申请记录也会回滚,不会出现“申请记录查得到但宠物状态没变”这种脏数据。
这里还有一个细节要提醒:不要把状态判断写在Controller里。有的同学喜欢在Controller里先查宠物、再判断状态,然后把业务逻辑散落在各个方法中。正确的做法是把这些逻辑收敛到Service层,Controller只负责接收参数和返回结果。这样答辩时被问到“业务和展示层如何解耦”,你能直接举出自己的代码例子。
4. 调试实录:毕设项目最常见的五个坑
4.1 启动类报错与Mapper找不到
我把这个项目从零搭起来的时候,踩过的坑主要集中在以下几个方面,整理成表格方便你们对照排查:
| 报错信息 | 原因 | 解决方案 |
|---|---|---|
Failed to configure a DataSource | application.yml里数据源配置缺失或数据库没启动 | 检查URL、用户名密码是否匹配,确认MySQL服务已启动 |
Invalid bound statement (not found) | MyBatis扫描不到Mapper XML | 检查mapper-locations路径,确认XML的namespace和接口全限定名一致 |
Ambiguous mapping. Cannot map controller | 两个Controller方法写了相同的请求路径 | 全局搜索@RequestMapping,找到重复路径并修改 |
The temporary upload location is not valid | Tomcat临时目录被清理,文件上传失败 | 在配置类里手动指定MultipartFile的临时目录,或者重启应用 |
| 数据库中文乱码 | 连接串没加characterEncoding=utf8 | URL里加上useUnicode=true&characterEncoding=utf8 |
这里面Invalid bound statement是出现频率最高的,绝大部分原因是XML文件的路径放错了。很多初学SpringBoot的人把mapper/*.xml放在src/main/java目录下,Maven默认不会把XML打包到classes里。正确的做法是放在src/main/resources/mapper/目录下,这样构建时才会被复制到classpath中。
4.2 联调时前端传参和后端接收的细节
前后端联调的时候,我遇到的最典型问题是日期格式和时间戳问题。如果你用Thymeleaf模板引擎,在页面里提交birthday这种日期字段,Controller用@DateTimeFormat注解接收:
@DateTimeFormat(pattern = "yyyy-MM-dd") private LocalDate birthday;否则会报Failed to convert from type java.lang.String to type java.time.LocalDate。用Vue或者Axios这类前后端分离方式时,后端返回的LocalDateTime默认序列化成数组格式,比如[2024, 5, 20, 15, 30, 0],前端拿到的是个数组而不是字符串,这里就需要在application.yml里配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8还有一个很隐蔽的问题:当宠物状态为3-已被申请时,如果某个用户申请失败或者被拒绝,管理员把状态改回1-可领养,但是之前的申请记录还在。这个时候前台宠物列表要显示的按钮文本就得分情况处理。如果用Thymeleaf,在模板里写判断:
<th:block th:if="${pet.status == 1}"> <a href="/apply/{id}" th:href="'/apply/' + ${pet.id}">申请领养</a> </th:block> <th:block th:if="${pet.status == 3}"> <span>已被申请</span> </th:block>这种前端状态展示逻辑虽然简单,但特别考验细心程度。答辩老师喜欢问“你如何避免用户重复申请同一个宠物”,你不仅要答数据库层的唯一索引或业务判断,还要提到前端状态展示层的配合。
4.3 事务不生效与Session失效问题排查
事务不生效也是我实际调试中遇到过的隐蔽问题。一个常见场景是:在Service里调用了本类的另一个方法,比如applyAdopt方法内部调用了sendNotify方法,而sendNotify方法上加了@Transactional,但它是被this调用的,Spring的代理不会生效,事务自然不起作用。解决办法是避免同类内部调用,或者把需要事务的方法拆到另一个Service类里。
Session失效这个问题通常在部署后出现。开发时用SpringBoot内嵌Tomcat,Session没什么问题;但部署到外部Tomcat时,如果重启了Tomcat,Session会丢失,用户需要重新登录。对于毕设答辩场景,只要在答辩前把需要用到的演示账号重新登录一遍就行。不过代码层面也可以做兜底:拦截器里对Ajax请求做特殊处理,如果Session失效,返回JSON提示而不是重定向页面,避免前端卡在奇怪的状态。
5. 这套系统还能怎么扩展
项目做完交上去,不代表就没事了。我自己的体会是,把基础版跑通之后,有几个扩展方向性价比特别高,如果要参加优秀毕设评选或者想放简历上展示,可以挑一两个做深。
第一个方向是消息提醒。现在用户提交领养申请后,管理员必须主动登录后台才能看到新申请。如果加一个Spring Boot整合WebSocket的实时通知,管理员后台有新申请时自动弹出一条提醒,系统交互体验立刻上一个档次。而且WebSocket在课程里通常不太会细讲,自己主动掌握这一块,面试也能多聊几句。
第二个方向是数据可视化。管理员后台加一个统计面板,展示每周新增宠物数量、领养成功率、宠物类型分布图,用ECharts画几张图表放到前端页面上。这一块能直接引用到论文的“系统测试与数据分析”章节,让整个项目的完整度看起来更高。
第三个方向是引入简单的推荐逻辑。比如根据用户浏览过的宠物类型和品种偏好,在首页推荐相似的宠物。用SimpleTag的协同过滤或者最简单的“同类型随机推荐”,然后在前端展示“猜你喜欢”栏目。这个扩展技术上不难,但会让答辩评委眼前一亮,因为系统不再是纯粹的增删改查,而是带了一点算法思维。
最后一个很实际的小建议:代码写完之后,花一个小时把项目的README写好,内容包括数据库初始化脚本的使用方式、默认管理员账号密码、各模块的接口说明。这不光是为了交差,等到你过一个月再看自己的项目,会发现有这个文档真的能省下大量回忆时间。我自己就是因为当年偷懒没写README,导致毕业设计答辩前调试环境的时候浪费了大半天去回忆当年是怎么配置的。
做这类管理系统,核心不在于用了多新奇的技术,而在于把一条业务主线的每个环节都做扎实:状态流转、权限控制、事务处理、文件管理、异常兜底。把宠物领养这条链路完整走通,你对SpringBoot+SSM这套技术栈的理解,绝对比照着教程敲一遍要深刻得多。