“宠物领养网站”这个题目,在Java Web方向的课设和毕业设计里,确实是出现频率最高的选题之一。它不像商城系统那样体量大,也不像博客系统那样太单薄,功能边界清晰、业务流程完整、技术栈覆盖面又广,非常适合拿来检验SSM框架的掌握程度。但现实情况是,很多人拿到这个题目,从网上找了一堆零散代码,最后勉强跑通,却完全讲不清楚“为什么这么写”。
这篇文章我不打算把每个页面截图贴一遍,而是想把基于SSM+Java的宠物领养网站从技术选型、数据库设计、核心代码落地到部署避坑完整过一遍。文章适合正在做课设或毕设的Java初学者,也适合想系统梳理SSM三层架构的开发者。我会把当初踩过的坑、后来带人时看到的高频错误,全部揉进去。
1. 先别急着写代码:SSM这套组合拳到底是怎么分工的
1.1 为什么一个老掉牙的技术栈,现在还在被大量用作教学项目
SSM是Spring、SpringMVC、MyBatis三个框架的合称。在Spring Boot还没有流行起来的那几年,这套组合基本就是Java Web开发的事实标准。现在看它确实“老”,但它的教学价值恰恰体现在“老”:三个框架的职责边界被拆得清清楚楚,几乎没有自动化的“魔法”可言。
用宠物领养网站来练手时,你会真实感受到一个请求从浏览器出发,经过Controller、Service、Mapper层层传递到最后落库的完整流程。这个体验是Spring Boot给不了的,因为Spring Boot把太多东西做成了“约定大于配置”,初学者往往只知道加依赖就能跑,却不清楚内部到底发生了什么。
我见过不少用Spring Boot做毕设很顺,但面试被问到“SpringMVC的核心流程”“MyBatis的动态SQL”就卡壳的人。反过来,凡是把SSM项目从头到尾手写一遍的人,后面学Spring Boot基本是降维打击。所以如果你正处于学习阶段,别嫌弃题目“老”,把它当成一门解剖课来做。
1.2 一次领养请求从点击到落库,在SSM里经历了什么
拿用户点击“申请领养”这个动作来说。浏览器会发起一个POST请求到Tomcat,Tomcat根据web.xml里配置的DispatcherServlet把请求拦截下来。DispatcherServlet并不直接处理业务,它先通过HandlerMapping找到对应的Controller方法。
Controller收到参数后,把参数整理成表单对象,调用Service接口。注意,Controller里面不写SQL、不写JDBC,它只做三件事:接收参数、调用Service、决定跳转哪个页面。
Service层接手后开始处理业务规则。比如判断这只宠物是否存在、状态是否可领养、当前用户有没有重复提交过申请。业务规则判断完,才轮到Mapper接口出场。Mapper是MyBatis的抽象入口,它负责把Java方法调用翻译成SQL语句,再通过JDBC打到MySQL数据库。
数据从数据库返回时是逆着这条路走的:Mapper返回Java对象,Service拿到对象后组织结果,Controller把结果放进ModelAndView,视图解析器根据返回的逻辑视图名找到对应的JSP页面,渲染成HTML,最终回到浏览器。
这套流程看似繁琐,但每一层都有明确职责。宠物领养项目里最典型的业务规则是“一只宠物只能被成功领养一次”,这个规则必须放在Service层判断,而不能写在Controller或JSP页面里。理解了这个分工,后面写代码就不会到处乱塞逻辑。
1.3 版本选型清单,照着配能省掉一半烦恼
SSM项目最让人头疼的就是版本兼容。我整理了一份自己用着很稳的组合,你可以直接照抄。
| 组件 | 推荐版本 | 说明 |
|---|---|---|
| JDK | 1.8 | 稳定、生态成熟,禁止用JDK 17以上跑旧项目 |
| Spring / SpringMVC | 5.1.9.RELEASE | Spring的webmvc和tx等模块必须同版本 |
| MyBatis | 3.5.3 | 3.5.x都比较稳 |
| mybatis-spring | 2.0.4 | 必须和MyBatis版本匹配,不能随手乱搭 |
| MySQL | 5.7 | 项目教学首选,8.x需要额外处理时区 |
| MySQL驱动 | 5.1.49 | 配MySQL 5.7几乎无坑 |
| Tomcat | 8.5 | 支持Servlet 3.1,配合Java EE的javax命名空间 |
| Maven | 3.6.x | 太老版本可能拉不到新依赖 |
如果你的数据库已经是MySQL 8.x,驱动要换com.mysql.cj.jdbc.Driver,URL里还必须加serverTimezone=Asia/Shanghai,否则时间字段会乱掉。这个细节我在第5章还会重点说。
2. 业务分析与数据库设计:宠物领养网站的核心其实是“状态管理”
2.1 前台后台功能清单,画清楚才动手
宠物领养网站从使用角色上分两类:普通用户和管理员。
前台面向普通用户,功能包括:注册登录、宠物浏览与分类筛选、宠物详情查看、提交领养申请、个人中心(查看我的申请、发布我发现的宠物、管理个人资料)、查看公告、给宠物留言咨询。
后台面向管理员,功能包括:登录验证、宠物审核(待审宠物上架、已上架宠物下架或删除)、领养申请审核(同意或拒绝)、用户管理(禁用/启用)、公告发布管理、留言回复。
这些功能列表看着不复杂,但每一个都对应数据库里的字段和状态。我见过很多人先建表再想功能,结果表设计完发现很多场景覆盖不了,比如“宠物下架后原来的领养申请怎么办”“用户重复申请怎么办”,全都要回头加字段。正确的顺序是先梳理业务规则,再设计表结构。
2.2 五张核心表与字段设计
整个项目我建议从这五张表起步,后续有余力再扩展收藏、疫苗记录之类的表。
user表:用户表,存账号、密码、联系方式、角色。
CREATE TABLE `user` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `username` VARCHAR(50) NOT NULL UNIQUE, `password` VARCHAR(64) NOT NULL COMMENT 'MD5加密后存储', `nickname` VARCHAR(50), `phone` VARCHAR(20), `email` VARCHAR(100), `address` VARCHAR(200), `role` TINYINT DEFAULT 0 COMMENT '0普通用户 1管理员', `avatar` VARCHAR(255), `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP );pet表:宠物表,这是整个系统的核心。
CREATE TABLE `pet` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(50) NOT NULL, `category` VARCHAR(20) COMMENT '猫/狗/其他', `breed` VARCHAR(50) COMMENT '品种', `age` VARCHAR(20) COMMENT '年龄段,如1岁以内', `sex` TINYINT DEFAULT 0 COMMENT '0公 1母', `health_status` VARCHAR(200) COMMENT '疫苗、驱虫等健康描述', `description` TEXT, `image` VARCHAR(255), `status` TINYINT DEFAULT 0 COMMENT '0待审核 1待领养 2已领养 3下架', `publisher_id` BIGINT, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP );application表:领养申请表,记录谁在什么时候想领养哪只宠物。
CREATE TABLE `application` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `pet_id` BIGINT NOT NULL, `user_id` BIGINT NOT NULL, `applicant_name` VARCHAR(50), `applicant_phone` VARCHAR(20), `applicant_address` VARCHAR(200), `reason` VARCHAR(500) COMMENT '申请理由', `status` TINYINT DEFAULT 0 COMMENT '0待审核 1已通过 2已拒绝 9已失效', `review_remark` VARCHAR(200), `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP, `review_time` DATETIME );notice表和message表:公告、宠物留言咨询。字段就是常规的title/content、content/reply,这里不展开。
关于application表,有个很多人不理解的设计:为什么申请表里要冗余申请人的姓名、电话、地址,而不是直接关联user表去查?因为领养申请是有业务时效性的,用户可能会修改自己的手机号,但这条申请记录必须保留提交那一刻的联系方式。这叫“业务快照”。如果你在答辩时能把这个设计原因讲出来,评委通常会认可你确实理解业务。
2.3 pet.status与application.status的联动逻辑
宠物领养系统最核心的,其实是状态管理。
宠物状态我设计了四个:0待审核、1待领养、2已领养、3下架。发布宠物的人提交之后默认是0,管理员审核通过变成1,有人领养成功变成2,管理员主动下架变成3。
申请状态对应四层:0待审核、1已通过、2已拒绝、9已失效。
为什么需要9这个“已失效”状态?因为一只宠物被某个人成功领养之后,这只宠物下还挂着另外好几个“待审核”的申请。如果不把它们改成失效,申请用户登录后看到的状态就是“审核中”,但宠物其实已经被人领走了,这会带来很差的体验,也容易产生纠纷。管理员审核通过时,要把同一宠物下其余待审申请统一批量置为9。
这两个状态的联动逻辑,是整个业务里最关键的地方。设计表的时候先把这个想明白,后续代码写起来会顺很多。
3. 三层架构落地的关键:从Controller到Mapper的职责划分与代码边界
3.1 包结构与职责边界
SSM项目的包结构看起来很简单,但实际很多人会写乱。我的习惯是:
com.xxx.pet ├── controller ├── service │ ├── interface │ └── impl ├── mapper ├── entity ├── vo └── commonentity放数据库表对应的实体类,vo放页面提交或展示用的对象。Controller层只做参数接收和页面跳转,Service接口定义业务动作,Service实现类写具体规则,Mapper只做最基础的增删改查和简单查询。
记住一个原则:在Controller里看到直接调用Mapper的地方,基本都是不合格的写法。哪怕是一个简单的查询,也应该走Service再转一次。原因是Service是事务边界,如果Controller绕过Service,事务注解就无法生效。这个过程前期会感觉多写了很多代码,但对理解分层有巨大帮助。
3.2 提交领养申请的核心代码:Service层是业务主战场
我以“用户提交领养申请”为例,把三层代码怎么配合讲清楚。
Controller:
@Controller @RequestMapping("/application") public class ApplicationController { @Autowired private ApplicationService applicationService; @PostMapping("/submit") public String submit(ApplicationForm form, HttpSession session, Model model) { User loginUser = (User) session.getAttribute("loginUser"); if (loginUser == null) { return "redirect:/login"; } try { applicationService.submit(form.getPetId(), loginUser.getId(), form); model.addAttribute("msg", "申请已提交,请等待管理员审核"); } catch (BizException e) { model.addAttribute("error", e.getMessage()); } return "apply/result"; } }Service接口:
public interface ApplicationService { void submit(Long petId, Long userId, ApplicationForm form) throws BizException; void review(Long applyId, Integer result, String remark); }Service实现里就是业务判断的核心:
@Transactional public void submit(Long petId, Long userId, ApplicationForm form) throws BizException { Pet pet = petMapper.selectById(petId); if (pet == null || pet.getStatus() != 1) { throw new BizException("该宠物当前不可领养"); } int count = applicationMapper.countByPetIdAndStatus(petId, 0); if (count > 0) { throw new BizException("你已经提交过申请,请等待审核结果"); } Application app = new Application(); app.setPetId(petId); app.setUserId(userId); // 表单里允许用户补充地址和理由,姓名电话默认取登录用户资料 app.setApplicantName(loginUser.getNickname()); app.setApplicantPhone(loginUser.getPhone()); app.setApplicantAddress(form.getAddress()); app.setReason(form.getReason()); app.setStatus(0); applicationMapper.insert(app); }这里有个容易想拧的地方:提交申请时,要不要同时把宠物状态改成类似“申请中”?
我的建议是不改。宠物处于“待领养”状态时,允许多个用户同时提交申请,这些申请进同一个审核池,管理员从中挑选最合适的领养人。如果提交申请就锁定宠物,管理员反而失去了选择空间。宠物状态的变更只发生在管理员最终审核通过的那一刻,我把它叫“申请池模式”。这个设计和现实中的领养流程是一致的,别人问起来也说得通。
3.3 MyBatis的动态SQL写法和一个容易踩的坑
宠物列表页通常要支持多条件筛选:按分类、按名字模糊查询、按状态过滤。这种场景最适合MyBatis的动态SQL。
<select id="listByCondition" resultType="com.xxx.pet.entity.Pet"> select * from pet <where> <if test="category != null and category != ''"> and category = #{category} </if> <if test="status != null"> and status = #{status} </if> <if test="keyword != null and keyword != ''"> and (name like concat('%', #{keyword}, '%') or description like concat('%', #{keyword}, '%')) </if> </where> order by create_time desc </select><where>标签会自动处理第一个and,比手工写where 1=1干净得多。再强调一下concat的写法,不要用'%' #{keyword} '%'这种字符串拼接,容易因为空格和注入问题翻车。
动态SQL的坑主要出在resultType上。如果数据库字段是create_time,实体类字段是createTime,MyBatis默认不会自动映射。最简单的解决方式是在mybatis-config.xml里开启驼峰映射:
<settings> <setting name="mapUnderscoreToCamelCase" value="true"/> </settings>开完这个,create_time自动映射到createTime字段,省掉一整个resultMap的重复编写。很多人项目跑起来发现“查出数据但字段全是null”,八成就是漏了这个配置。
3.4 事务边界:为什么注解要写在Service实现类上
宠物领养最典型的并发场景是:管理员同时收到两只宠物的申请,审核通过时正好都点在“同意”按钮上。如果没有事务控制,可能出现申请状态已通过、宠物状态没改成已领养的脏数据。
@Transactional必须写在ServiceImpl类的public方法上,原因有两个。
第一,Spring的事务是通过AOP代理实现的。代理只会拦截通过Spring容器拿到的bean实例,而对同一个类内部的方法调用(this.xxx())无法拦截。写在Controller上的事务注解,如果在同一个Controller里调用另一个方法,很可能不会生效。
第二,事务粒度要匹配业务粒度。一个事务应该覆盖一次完整的业务操作,而不是一次HTTP请求。所以提交申请、审核申请这种动作,才需要加事务;获取列表这种查询,通常不需要强制加。
审核方法的事务是这样的:
@Transactional public void review(Long applyId, Integer result, String remark) throws BizException { Application app = applicationMapper.selectById(applyId); if (app == null || app.getStatus() != 0) { throw new BizException("该申请已被处理过"); } if (result == 1) { // 关键:条件更新,防止并发重复领养同一只宠物 int rows = petMapper.updateStatusIfStatusEquals(app.getPetId(), 2, 1); if (rows == 0) { throw new BizException("该宠物已被其他用户领养"); } applicationMapper.updateStatusById(applyId, 1, remark); // 把这只宠物其他待审申请统一置为失效 applicationMapper.updateStatusByPetId(app.getPetId(), 0, 9); } else { applicationMapper.updateStatusById(applyId, 2, remark); } }同一个事务里先更新宠物状态,再更新申请状态,再批量失效其他申请。任何一个步骤抛异常,全部回滚。这种写法出现脏数据的可能性才真正降到了最低。
4. 领养审核这条主链路:从用户提交到管理员确认的完整实现
4.1 前端表单完整性与后端校验的兜底
申请领养的表单通常包含:姓名、联系电话、居住地址、申请理由。前端要做基本校验,后端必须再做一次兜底,否则别人直接绕过页面构造HTTP请求,什么脏数据都能塞进来。
前端校验这里不展开,用原生JS或jQuery的校验插件都行。后端校验的重点是:不能信任前端传过来的任何值。我自己都是写一个简单的工具方法或者直接手写判断:
if (StringUtils.isEmpty(form.getAddress())) { throw new BizException("居住地址不能为空"); } if (StringUtils.isEmpty(form.getReason())) { throw new BizException("请填写申请理由"); } if (!Pattern.matches("^1[3-9]\\d{9}$", form.getPhone())) { throw new BizException("手机号格式不正确"); }如果项目后面引入了Hibernate Validator,可以用@NotBlank这类注解做声明式校验,但SSM阶段我不建议过早依赖它。手写判断虽然“土”,但它能让你清楚知道每一条规则是怎么校验的,答辩时也更容易讲明白。
4.2 用户重复申请的边界
我在3.2节里已经写了一个判断:同一用户对同一宠物只能有一条待审核状态的申请。这个判断在提交时做一次,但这里要提醒一个问题——理论上存在两个相邻请求同时通过判断的极端情况。
比如同一用户开两个浏览器标签,同时点击提交,两个请求都查到count = 0,都通过了校验,最终就会插入两条待审核申请。要彻底杜绝,最稳妥的做法是在数据库层面加唯一约束,但SSM项目阶段很少做这么细致的并发控制。实际项目中这个缺陷影响不大,最多是管理员看到两条重复申请,点掉一条即可。但如果你在文章或答辩里能主动说出这个隐患和解决方案,观感会很好。
4.3 管理员审核通过后的批量失效
前一章的review方法已经展示了审核的核心SQL。这里的重点在于petMapper.updateStatusIfStatusEquals这个条件更新。它的SQL本质是:
UPDATE pet SET status = 2 WHERE id = #{petId} AND status = 1为什么要写成带AND status = 1的条件更新?
假设管理员A和管理员B同时在处理两位申请人的申请,这两份申请恰好都是同一只宠物下的。A先执行更新,影响行数是1;B再执行时,因为宠物的status已经变成2,条件不再成立,影响行数是0。B就可以明确知道“宠物已经被领养”,然后提示管理员。这就是用数据库的行锁配合条件更新,解决并发抢资源的问题,也是整个项目里最漂亮的一个设计点。
管理员拒绝某个申请时,只需要把这条申请的状态改成2(拒绝)即可,不需要动宠物的状态。这样宠物还能继续留在领养池里等下一个申请。
4.4 个人中心的数据展示
用户登录后的个人中心包含“我的申请”和“我发布的宠物”两个核心页面。
“我的申请”列表如果直接查application表,看到的是petId,根本不知道宠物叫什么名字。比较自然的做法是用JOIN一次性查出来,避免在Java代码里循环再查一次宠物表,也就是所谓的N+1查询问题。
SELECT a.*, p.name AS pet_name, p.image AS pet_image FROM application a LEFT JOIN pet p ON a.pet_id = p.id WHERE a.user_id = #{userId} ORDER BY a.create_time DESC在SQL里JOIN一次就能拿到申请信息和宠物摘要,一条语句搞定,比Service里循环查询高效得多。如果项目规模变大,列表数据量大,查询慢,再考虑分页和索引优化。
5. 部署上线前的避坑清单:乱码、路径、图片上传、JSON序列化
这章全部是我实际遇到过的问题,也是检查代码时最容易“看哪都对、跑哪都错”的地方。
5.1 中文乱码的三个来源和一次解决
中文乱码在SSM项目的排查难度不高,但来源确实多。我见过的情况有三种。
第一种是数据库本身乱码。建库时没用utf8mb4,或者表字段默认不是utf8。数据库和表都要显式指定DEFAULT CHARSET=utf8mb4,这是治本。
第二种是请求参数乱码。特别是POST请求,SpringMVC需要配置CharacterEncodingFilter,放在web.xml的最前面,让编码过滤器优先于其他过滤器执行。
<filter> <filter-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>第三种是JSP页面乱码。每个JSP头部都要写pageEncoding="UTF-8"和contentType="text/html;charset=UTF-8"。用Maven创建项目时模板自带的JSP可能不是UTF-8,全改一遍就好。
一次解决的标准流程是:先检查数据库字符集,再检查连接串是否带characterEncoding=UTF-8,最后确认Filter和JSP页面。按这个顺序查,最多五分钟定位。
5.2 绝对路径与${pageContext.request.contextPath}
SSM项目里的JSP页面,经常出现CSS样式加载不出来、图片404、点击链接404这种问题,根源基本都是用了相对路径。
如果你的项目部署在Tomcat的webapps/下面,访问地址可能是http://localhost:8080/pet/,其中/pet就是上下文路径。页面上如果写css/style.css,浏览器解析时把它当成了http://localhost:8080/css/style.css,自然就找不到。
正确做法是每个页面开头统一声明:
<c:set var="ctx" value="${pageContext.request.contextPath}"/>引用资源时全部用${ctx}/css/style.css,表单action写成${ctx}/application/submit。这个细节虽然基础,但能解决大半的“部署后页面错乱”问题。
5.3 图片上传:物理路径与虚拟路径映射
宠物发布时的图片上传是必做功能。我建议把所有上传文件放到一个独立目录,而不是项目的webapp目录下。放webapp下有个缺点:每次重新部署war包,上传的图片就会丢,因为Tomcat会重新解压并覆盖目录。
以Windows为例,把图片存到D:/pet-upload/,然后在SpringMVC配置里增加资源映射:
<mvc:resources mapping="/upload/**" location="file:D:/pet-upload/"/>这样数据库里存的图片地址是/upload/2025/01/xxx.jpg,浏览器访问时,SpringMVC会自动去磁盘对应目录读取。部署到Linux时,把这个绝对路径换成Linux路径即可,代码结构不用改。
上传文件本身还需要配置multipart解析器。SSM里常用的是CommonsMultipartResolver,记得引入commons-fileupload和commons-io两个jar包。很多项目在这里漏掉依赖,启动就报错。
5.4 MyBatis懒加载和JSON序列化的相爱相杀
如果你在宠物列表里用了MyBatis的懒加载,比如查询宠物时关联查询留言,页面或接口在关闭SqlSession之后去访问关联对象,就会报LazyInitializationException。
这个异常在SSM项目里有几种缓解方式。最省事的方案是不使用懒加载,把关联查询改为JOIN或者立即加载。项目规模不大时,这完全够用并且不会出问题。如果你确实要用懒加载,可以在web.xml配置Spring的OpenSessionInViewFilter,让SqlSession在整个请求周期内保持打开,但这会带来长事务、连接占用等问题,我不推荐新手这么做。
一句话总结:SSM阶段老老实实关闭MyBatis的懒加载,所有关联数据在事务方法内取好,再返回给页面。
5.5 依赖冲突与JDBC驱动版本
Maven的多模块和依赖传递,有时会引入两个不同版本的Spring模块,最常见的报错是方法找不到或类找不到。
整改方法只有一个:检查pom.xml里所有Spring相关依赖版本是否完全一致,用mvn dependency:tree查看传递依赖,把冲突的版本用<exclusion>排除掉。
JDBC驱动则是另一个高频坑。用MySQL 5.7配了8.x驱动,或者用8.x数据库配了5.x驱动,都会出问题。最简单的办法是确定数据库版本后,去Maven仓库拉一个完全匹配的驱动版本,URL同步调整。不要图省事随便复制网上的配置。
我把这些坑整理成一张速查表,方便以后直接对照:
| 问题现象 | 常见根因 | 解决思路 |
|---|---|---|
| 中文全是问号 | 数据库/连接串/JSP编码不统一 | 统一utf8mb4,加Filter,页面加pageEncoding |
| CSS图片404 | JSP用了相对路径 | 所有路径用${ctx}拼 |
| 重新部署图片丢失 | 上传在项目内目录 | 独立目录+mvc:resources映射 |
| JSON序列化报懒加载 | 关联对象在session外访问 | 关闭懒加载或事务内取值 |
| 启动时ClassNotFound | Spring版本不一致 | dependency:tree排查并固定版本 |
| 时间字段错乱 | MySQL 8.x驱动没配时区 | URL加serverTimezone=Asia/Shanghai |
6. 做完之后还能怎么改:迈向Spring Boot的一步之遥
6.1 配置代码化:从XML到注解到自动装配
SSM项目里最繁琐的就是三个配置文件:spring-mvc.xml、spring-mybatis.xml、spring-service.xml。改造成Spring Boot后,这些全部用注解和配置类替代。
比如MyBatis的配置,Spring Boot只需要两步:在启动类或配置类上加上@MapperScan("com.xxx.pet.mapper"),在application.yml里配置数据源。连事务管理都可以靠@EnableTransactionManagement一行开启。对已经写过SSM项目的人来说,这个过程是水到渠成的,因为你知道每个配置项原本是干嘛的,换成Spring Boot只是换了个载体。
6.2 Mapper可以保留,Controller响应改成JSON
改造时最不用动的就是MyBatis的Mapper层,因为MyBatis-Spring-Boot-Starter兼容原生MyBatis写法。JSP则建议直接替换掉,前端可以换成Thymeleaf模板,或者直接把后端改造成纯JSON接口,前端用Vue这类框架来渲染页面。
把Controller从返回ModelAndView改成返回Result对象,重点是把错误处理统一。原来页面跳转里的model.addAttribute("error", e.getMessage()),在纯接口方案里变成统一的JSON错误结构。这个改造会让整个项目前后端彻底分离,也更好扩展。
6.3 一个更有意思的扩展方向
如果做完基础功能还有余力,我推荐几个方向。
给宠物列表增加Redis缓存,把点击量高的宠物详情页放进缓存,降低数据库压力。这个方向能涉及缓存一致性、过期策略这些知识点。把本地图片上传换成对象存储,摆脱磁盘路径的依赖,这个方向能让你接触云服务相关概念。再给宠物增加疫苗记录、回访记录这种运营功能,也是很好的业务扩展,能让项目在答辩时更有“落地感”。
不管选哪个方向,都建立在SSM阶段已经把基础链路跑通的前提下。基础不打牢,扩展就是空中楼阁。
最后说一点个人体会。这个项目做下来,我最大的收获不是学会了SpringMVC注解怎么用、MyBatis SQL怎么写,而是理解了业务状态的一致性到底有多重要。pet.status和application.status这两个字段,背后是整套领养流程的逻辑。你把状态流转想清楚了,事务边界自然就清楚了;事务边界清楚了,代码该写在哪一层也自然就清楚了。
如果重新让我做一遍这个项目,我会先画一张宠物状态和申请状态的流转图,把自己的操作顺序理清楚再动手写Mapper。这个习惯我现在做任何带状态机的业务时都还在用,也确实帮我少走了很多弯路。