最近帮人调试了一套宠物领养一站式服务系统,用的是Java + SpringBoot + SSM这套经典组合。整套系统包含了前端页面、后端接口、数据库脚本、论文文档、调试工具,代码完整度很高,从用户注册、宠物发布、领养申请到后台审核、记录管理,整个业务闭环是通的,不是那种只有几个页面糊弄人的demo。如果你是正在做毕设、或者想找一个练手项目巩固Java后端知识,这套系统值得认真过一遍。
先说一个很多人容易绕晕的点:SpringBoot和SSM到底是什么关系?SSM是Spring + SpringMVC + MyBatis三个框架的组合,而SpringBoot本身就是Spring家族的产品,它通过自动配置把SpringMVC、MyBatis这些组件整合进一个工程里,省掉了大量XML配置。所以"SpringBoot+SSM"正确的理解方式是:用SpringBoot作为项目骨架,SpringMVC处理Web请求,MyBatis操作数据库,Spring管理业务对象。这套组合在中小型管理系统中非常成熟,面试也常问,学它不吃亏。
整篇文章我会从方案选型开始,逐步拆解这个宠物领养系统的设计思路、数据库建模、核心功能实现,再到环境搭建和排坑实录,最后分享一些我实际调试中积累的经验。代码片段我尽量给全,你照着抄也能跑起来。
1. 项目整体设计与技术选型:为什么这套组合值得做
1.1 用SpringBoot整合SSM,而不是传统SSM
如果你搜过老教程,会发现传统SSM项目有一堆繁琐的配置文件:web.xml、spring-mvc.xml、spring-mybatis.xml、mybatis-config.xml,每换一个环境都要手动改配置,版本稍微不一致就容易出幺蛾子。SpringBoot最大的价值就是把这一层包袱甩掉了。
这个宠物领养系统用的是SpringBoot作为基础框架,SpringMVC负责控制层(Controller),MyBatis负责持久层(Mapper),Spring负责Service层的依赖注入和事务管理。相比传统写法,省去了大量XML,同时保留了SSM的分层思想:Controller只处理请求参数和响应,Service写业务逻辑,Mapper只做数据库交互。这种分层方式对后期维护和扩展都非常友好。
选型的时候我特意确认了JDK版本配套关系。如果你打算用SpringBoot 3.x,那必须配JDK 17及以上;如果沿用SpringBoot 2.7.x,JDK 8就能跑得很稳。这套系统用的是JDK 8 + SpringBoot 2.x组合,原因很简单:兼容性最好,网上资料最多,部署到服务器也不需要额外装高版本JDK。很多同学一上来用了SpringBoot 3.x,发现javax.servlet变成了jakarta.servlet,各种第三方库不兼容,排查起来非常痛苦——做这类项目真的不用追新,稳定压倒一切。
1.2 功能模块划分
整个系统从角色上分三类:管理员、宠物主人(发布者)、领养人(普通用户)。从功能上分大概六个大块:
- 用户模块:注册、登录、个人信息修改、密码重置
- 宠物模块:宠物发布、宠物列表浏览、宠物详情、按品种/年龄筛选
- 领养模块:提交领养申请、查看申请进度、审核申请
- 审核模块:管理员审核宠物信息和领养资格
- 记录模块:领养成功后生成领养记录,支持后续回访
- 公告模块:系统公告发布和展示
为什么这样划分?因为宠物领养业务的核心是"信息发布-意向申请-审核确认-建立记录"这条链路,任何一个环节缺失都会让系统显得不完整。比如很多新手只做了发布和浏览,忽略了审核环节,结果就是"领养"成了一个假功能。我这套系统把审核和回访都补齐了,整体业务才算闭环。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 数据库设计:一套领养系统背后的核心表结构
2.1 核心表设计与字段说明
数据库我用的是MySQL 5.7,字符集统一utf8mb4,避免中文乱码。表不算多,六张核心表:
t_user:用户表,存登录账号、角色、联系方式t_pet:宠物表,存宠物信息、健康状态、当前状态t_apply:领养申请表,记录谁在什么时候申请了哪只宠物t_adopt_record:领养记录表,审核通过后就生成一条记录t_notice:公告表,管理员发布的通知t_message:站内留言表,用户之间关于宠物的咨询
这里重点说一下t_pet表的设计,它是整个系统的中枢。除了基本的宠物名、品种、年龄、性别之外,一定要加一个status字段,用来标记宠物当前处于什么状态:
| 状态值 | 含义 | 说明 |
|---|---|---|
| 0 | 待领养 | 正常展示,用户可申请 |
| 1 | 申请审核中 | 已经有人提交申请,系统自动锁定 |
| 2 | 已领养 | 审核通过且完成线下交接,不再展示 |
| 3 | 已下架 | 宠物主人主动下架 |
| 99 | 草稿 | 未正式发布 |
为什么要把状态维护得这么细?因为领养和买商品不一样,一只宠物不能同时被多个人领养,必须通过状态字段保证"一宠一单"。我见过很多半成品系统不加状态控制,结果同一只猫被三个人同时申请,线下就乱套了。
t_apply表的核心字段包括:pet_id、user_id、apply_reason(申请理由)、contact_phone(联系电话)、home_address(居住地址)、has_yard(是否有院子/阳台)、status(审核状态:0待审、1通过、2拒绝)。为什么要有has_yard这种细节字段?因为宠物领养和买东西不一样,机构或个人送养人通常都希望了解领养人的居住环境是否适合养宠物,这个字段能直接帮管理员做初筛。
2.2 重要SQL参考
建表之后,有几个常见的查询一定要写好。比如分页查询宠物列表:
sql复制SELECT p.id, p.name, p.type, p.breed, p.age, p.avatar,
p.status, u.nickname AS publisher_name
FROM t_pet p
LEFT JOIN t_user u ON p.user_id = u.id
WHERE p.status = 0
ORDER BY p.create_time DESC
LIMIT #{offset}, #{pageSize}
查询某只宠物的领养申请列表:
sql复制SELECT a.id, a.apply_reason, a.contact_phone, a.home_address,
a.status, a.create_time, u.nickname AS applicant_name
FROM t_apply a
LEFT JOIN t_user u ON a.user_id = u.id
WHERE a.pet_id = #{petId}
ORDER BY a.create_time ASC
这些SQL看似简单,但写DAO层的时候一定要确认字段名和实体类属性是对应的。MyBatis默认开启了驼峰转换,数据库用下划线命名create_time,实体类用createTime,在application.yml里配置map-underscore-to-camel-case: true就能自动映射,省去大量resultMap。
2.3 事务设计:领养链路不能出现"半成功"
领养申请不是一条insert就完事的,它会牵动多张表联动。举个例子:用户A申请领养宠物P,系统要做三件事:
- 插入一条
t_apply申请记录,状态为待审核 - 修改
t_pet表中宠物P的status为1(申请审核中) - 如果该宠物之前有其他待审核的申请,要把它们批量置为失效(通常自动拒绝)
这三步必须放在同一个事务里,否则就会出现"申请记录插入成功但宠物状态没改"的脏数据。我在AdoptService里加上@Transactional(rollbackFor = Exception.class),并且注意一点:rollbackFor一定要指定,否则遇到受检异常时Spring默认不会回滚。
事务分析这里补充一个很重要的经验:Service层方法内部调用同类中的另一个方法,事务是不生效的,因为Spring事务基于AOP代理,同类调用不会经过代理。如果你发现@Transactional没起作用,优先检查是不是"自己调自己"。
3. 核心功能实现:从登录到领养申请的完整链路
3.1 登录鉴权:拦截器 + Session就够了
很多毕设都喜欢上SpringSecurity + JWT,但实际上对于这种单体管理类系统,用拦截器加Session完全可以解决问题,而且代码量少、易理解。我在项目里实现了一个简单的LoginInterceptor:
java复制public class LoginInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
HttpSession session = request.getSession();
Object user = session.getAttribute("loginUser");
if (user == null) {
// 判断是否为Ajax请求
String requestedWith = request.getHeader("X-Requested-With");
if ("XMLHttpRequest".equals(requestedWith)) {
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":401,\"msg\":\"未登录\"}");
} else {
response.sendRedirect(request.getContextPath() + "/login.html");
}
return false;
}
return true;
}
}
这里做了Ajax请求的区分处理,避免前端拿到一个HTML页面导致解析错误。注册到WebMvc配置器时,要注意放行登录页、静态资源(css/js/images)以及注册接口。很多人排查"静态资源被拦截"的问题,就是因为在注册拦截器时没有排除静态资源路径。
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/login", "/register", "/api/login", "/api/register",
"/css/**", "/js/**", "/images/**", "/fonts/**", "/error");
}
}
密码存储这块别用明文,至少用MD5加盐,或者直接用SpringSecurity自带的BCryptPasswordEncoder做哈希。我知道有的同学觉得密码加密麻烦,但这是系统安全的基本底线,答辩时老师也很喜欢问这个点。
我实际做的时候用了一个取巧的方案:用户注册时生成随机盐值,密码存的是MD5(密码 + 盐值),登录时用同样的规则重新计算并比对。虽然MD5不算强安全,但比明文好太多,而且代码实现简单。
3.2 宠物发布与图片上传:避不开的静态资源映射
宠物发布功能一定要支持图片上传。这个系统的做法是:前端通过FormData提交宠物信息和图片文件,后端用MultipartFile接收,将文件保存到本地磁盘指定目录,然后把文件路径存到数据库。
这里有一个非常关键的问题:SpringBoot默认只映射classpath:/static/下的静态资源,你把图片存在了服务器磁盘的/upload目录,前端是访问不到的。解决办法是做静态资源映射:
java复制@Configuration
public class ResourceConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
// 将 /upload/** 映射到本地磁盘路径
String uploadPath = System.getProperty("user.dir") + File.separator + "upload" + File.separator;
registry.addResourceHandler("/upload/**")
.addResourceLocations("file:" + uploadPath);
}
}
这个配置我踩过两次坑。第一次是路径末尾的反斜杠忘了加,导致映射失效,静态资源404。第二次是Windows环境下路径分隔符用的/,在Linux上出问题,后来统一改用File.separator当前系统的分隔符解决。
再补充一个上传配置,application.yml里一定要把文件大小上限调大,否则默认1MB基本没法用:
yaml复制spring:
servlet:
multipart:
max-file-size: 20MB
max-request-size: 50MB
3.3 领养申请与状态流转:核心业务怎么写
领养申请是系统里最有含金量的部分,因为它涉及状态流转。我在AdoptService中写了这样一个方法:
java复制@Transactional(rollbackFor = Exception.class)
public synchronized boolean applyAdopt(ApplyDTO dto) {
// 1. 校验宠物是否存在且状态为待领养
Pet pet = petMapper.selectById(dto.getPetId());
if (pet == null || pet.getStatus() != 0) {
throw new BizException("该宠物当前不可申请领养");
}
// 2. 校验是否重复申请
int count = applyMapper.countByPetIdAndUserIdAndStatus(dto.getPetId(),
dto.getUserId(), 0);
if (count > 0) {
throw new BizException("您已提交过申请,请勿重复操作");
}
// 3. 插入申请记录,状态为待审核
AdoptApply apply = new AdoptApply();
BeanUtils.copyProperties(dto, apply);
apply.setStatus(0);
applyMapper.insert(apply);
// 4. 更新宠物状态为申请审核中
Pet update = new Pet();
update.setId(pet.getId());
update.setStatus(1);
petMapper.updateById(update);
return true;
}
加synchronized是为了避免并发情况下同一只宠物被两个人同时申请。实际部署若有多台服务器,synchronized就不够了,需要引入分布式锁,但对于单体项目已经足够。这里我还做了"重复申请校验",防止同一用户对同一只宠物反复提交申请。
审核通过的方法逻辑类似,但要多做一步:把同一宠物其他待审核的申请置为拒绝状态,同时生成一条t_adopt_record领养记录。这样设计的好处是:数据库里的数据始终是"有效数据",不会残留一堆状态互相矛盾的申请。
java复制@Transactional(rollbackFor = Exception.class)
public boolean approveApply(Integer applyId, Integer adminId) {
AdoptApply apply = applyMapper.selectById(applyId);
if (apply == null || apply.getStatus() != 0) {
throw new BizException("该申请不存在或已处理");
}
// 1. 通过当前申请
apply.setStatus(1);
apply.setAuditId(adminId);
apply.setAuditTime(new Date());
applyMapper.updateById(apply);
// 2. 拒绝同宠物的其他待审申请
applyMapper.rejectOtherPending(apply.getPetId(), apply.getId());
// 3. 宠物状态改为已领养
Pet pet = new Pet();
pet.setId(apply.getPetId());
pet.setStatus(2);
petMapper.updateById(pet);
// 4. 生成领养记录
AdoptRecord record = new AdoptRecord();
record.setPetId(apply.getPetId());
record.setUserId(apply.getUserId());
record.setApplyId(apply.getId());
record.setAdoptTime(new Date());
adoptRecordMapper.insert(record);
return true;
}
这套逻辑我调试了很长时间,最大的体会就是一定要画清楚状态机再动手写代码。先把每个状态之间有哪些流转路径列出来,把例外情况比如"审核中能否撤销申请"、"管理员能否取消已通过申请"想清楚,写代码才不会前后矛盾。
3.4 列表查询与分页:PageHelper的正确姿势
宠物列表查询是访问量最大的接口,肯定要分页。项目里用的是MyBatis的PageHelper插件,用法很简单:
java复制@Service
public class PetServiceImpl implements PetService {
@Override
public PageInfo<PetVO> queryPetList(PetQueryDTO query) {
PageHelper.startPage(query.getPageNum(), query.getPageSize());
List<PetVO> petList = petMapper.selectPetList(query);
return new PageInfo<>(petList);
}
}
这里有一个坑必须提醒:PageHelper.startPage()只对紧接着的下一条SQL查询生效。如果你在调用Mapper前做了其他数据库操作,或者Mapper套了别的查询,分页就会失效甚至错乱。正确做法是确保startPage()和petMapper.selectPetList()之间没有任何多余操作。
排序字段我建议用create_time desc,新发布的宠物排在前面。筛选条件可以用动态SQL,MyBatis的<if>标签很好用,但记得每个<if>里面的字段名要和实体类一致,不然条件永远不生效——这个问题我排查过整整一下午,最后发现是query.getPetType()写成了query.getType(),字段名和前端传参对不上,后端一直拿不到条件值。
4. 环境搭建与部署排坑:从JDK版本到Docker打包
4.1 版本配套:JDK8还是JDK17?
我建议把这套系统跑在JDK 8 + SpringBoot 2.7.x上,原因前面说过,兼容性最好。网上教程、博客、答辩老师熟悉的都是这套组合。
如果你不小心用了SpringBoot 3.x,一定要知道两个重大变化:
第一,javax.*包全部改成了jakarta.*,包括javax.servlet.http.HttpServletRequest变成了jakarta.servlet.http.HttpServletRequest。如果你项目里还引用旧包,启动会直接报ClassNotFoundException。
第二,Spring Security 6的配置方式和Spring Security 5完全不同,之前常见的WebSecurityConfigurerAdapter已经被移除了,改成基于SecurityFilterChain的Bean声明式配置。
版本选择上我的建议是不要纠结,直接明确用SpringBoot 2.7.18,这是2.x系列的最终版本,稳定性和兼容性都比较理想。
4.2 编译报错:源发行版17需要目标发行版17
这个报错很多新手都遇到过,症状是明明代码没写错,一编译就报java: 警告: 源发行版 17 需要目标发行版 17。本质上是三处JDK版本不一致:
- IDEA的Project Structure里配置的Project SDK
- IDEA的Java Compiler里设置的Target bytecode version
- Maven的pom.xml里
maven.compiler.source和maven.compiler.target
其中任意一处是17、另外一处是8,就会报错。排查方法按顺序来:
- File -> Project Structure -> Project,确认Project SDK是1.8
- File -> Project Structure -> Settings,确认Language Level是8
- Settings -> Build Tools -> Maven -> Importing,确认JDK for importer是1.8
- 最后看pom.xml的
java.version改成1.8
这里我还有一个土办法:直接在pom.xml里写死编码参数:
xml复制<properties>
<java.version>1.8</java.version>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
三个地方全写清楚,IDEA和Maven就都不会再乱猜了。
4.3 运行时报错:内存不足和Lombok不生效
运行阶段最常见的错误是java: outofmemoryerror: insufficient memory。这个报错有几种情况:
第一种是Maven编译时内存不足,在IDEA的Build Tools -> Maven -> Runner里把VM Options设置成-Xms512m -Xmx1024m。
第二种是运行SpringBoot应用时JVM内存不够,在运行配置的VM options里加:
code复制-Xms512m -Xmx1024m -XX:MetaspaceSize=128m -XX:MaxMetaspaceSize=256m
第三种,也是比较隐蔽的,是Docker容器内存限制太小。如果你的应用跑在Docker里,默认容器可能只有256MB内存,SpringBoot启动都要几百MB,自然就OOM了。用docker run -m 1g给足内存即可。
另一个高频报错是java: you aren't using a compiler supported by lombok, so lombok will not work。这个是因为本机JDK版本太高,而项目里的Lombok版本太老。比如JDK 17配Lombok 1.18.20就会出问题。解决方案:
- 把Lombok依赖升级到最新版本(1.18.30+),支持到JDK 21
- 或者把JDK版本降回8
另外还要检查IDEA是否安装了Lombok插件,并且Settings -> Build -> Compiler -> Annotation Processors里勾选了"Enable annotation processing"。这两个少一个,Lombok的@Data、@Slf4j都不会生效。
4.4 打包部署:JDK8项目如何构建Docker镜像
项目开发完总要部署跑起来,我现在习惯用Docker来部署。这里分享一下SpringBoot项目打包Docker镜像的完整过程。
首先确认pom.xml里引入了spring-boot-maven-plugin,然后执行打包:
bash复制mvn clean package -DskipTests
打包完成后,在target目录下会生成一个pet-adopt-system.jar。接下来写Dockerfile:
dockerfile复制# 基础镜像选用JDK8
FROM openjdk:8-jdk-alpine
# 维护者信息
MAINTAINER your_name
# 设置工作目录
WORKDIR /app
# 将jar包复制到镜像中
COPY target/pet-adopt-system.jar /app/app.jar
# 暴露端口
EXPOSE 8080
# 启动命令
ENTRYPOINT ["java", "-jar", "/app/app.jar"]
构建命令:
bash复制docker build -t pet-adopt:latest .
运行命令:
bash复制docker run -d --name pet-adopt -p 8080:8080 -v /data/upload:/app/upload -e "TZ=Asia/Shanghai" pet-adopt:latest
特别注意-v /data/upload:/app/upload这个参数,它把宿主机目录挂载到容器的/upload目录上。因为图片上传到容器内部的磁盘,容器一删就没了,挂载出来才能持久化。这个坑是我把项目迁到Docker时踩到的,原来在本地跑得好好的,一上Docker图片全丢,折腾半天才想起来容器文件系统是临时的。
如果你是用Windows的Docker Desktop,注意JDK8基础镜像可能要加-Djdk.tls.client.protocols=TLSv1.2之类的参数才能正常访问某些HTTPS服务,这个问题比较偏,遇到了再排查即可。
5. 常见问题与排查技巧实录:一张表对照排查
我把调试过程中遇到的高频问题整理成了一个速查表,希望对你有帮助:
| 现象 | 根本原因 | 解决办法 |
|---|---|---|
| 编译报"源发行版17需要目标发行版17" | 项目JDK版本配置不一致 | 统一Project SDK、Language Level、Maven compiler为JDK8 |
| 运行报OOM内存不足 | JVM或容器内存太小 | 调大IDEA的VM Options,Docker加-m 1g |
| Lombok的@Data不生效 | JDK与Lombok版本不兼容 | 升级Lombok到1.18.30+,或退回JDK8 |
| 静态资源404 | 未做资源映射或拦截器拦截了静态路径 | 配置WebMvcConfigurer的文件映射,拦截器排除/upload/** |
| 上传文件超过大小限制 | SpringBoot默认限制1MB | 配置spring.servlet.multipart.max-file-size和max-request-size |
| 事务不生效 | Service内部方法自调用 | 把事务方法拆分到不同Bean中调用 |
| PageHelper分页失效 | startPage()和SQL之间插入了其他查询 |
确保startPage()后紧跟目标Mapper查询 |
| 中文乱码 | 数据库或连接URL缺少字符集参数 | 建库用utf8mb4,JDBC URL加characterEncoding=utf8 |
| 校验失败的请求没有响应 | 未区分Ajax请求和普通请求 | 拦截器中通过X-Requested-With头判断并分别处理 |
| 启动时端口被占用 | 8080端口已被其他进程使用 | 使用lsof -i:8080或netstat -ano查占用进程,或修改server.port |
5.1 一个典型的循环依赖问题排查过程
SpringBoot项目在Bean比较多的时候容易遇到循环依赖报错。我调试这套系统时也碰到一次:UserService里面注入了ApplyService,而ApplyService又注入了UserService,启动时直接报错The dependencies of some of the beans in the application context form a cycle。
解决方案有三个:
第一,重新审视业务设计。很多时候循环依赖是设计问题,比如两个Service功能边界没划清。UserService其实只需要ApplyService里的一个方法,完全可以把那个方法挪到独立的一个ApplyQueryService中,从根上切断循环。
第二,使用@Lazy注解,让其中一个Bean延迟加载:
java复制@Service
public class UserService {
@Lazy
@Autowired
private ApplyService applyService;
}
第三,SpringBoot 2.6及以上版本默认禁止循环依赖,可以通过配置临时放开:
yaml复制spring:
main:
allow-circular-references: true
但我不推荐第三种,治标不治本,答辩时被问到也答不好。最好的办法还是重构代码结构,把公共逻辑抽到独立Service或者直接用工具类解决。
5.2 单元测试:@SpringBootTest的正确打开方式
最后聊一下测试。很多学生项目完全没写测试,但如果你想把项目做到"答辩加分"的水平,一定要在核心Service层写上单元测试。我实际写过的领养申请测试如下:
java复制@SpringBootTest
@Transactional
class AdoptServiceTest {
@Autowired
private AdoptService adoptService;
@Autowired
private PetMapper petMapper;
@Test
void testApplyAdoptSuccess() {
// 准备数据
Pet pet = new Pet();
pet.setName("测试猫咪");
pet.setStatus(0);
petMapper.insert(pet);
// 调用申请
ApplyDTO dto = new ApplyDTO();
dto.setPetId(pet.getId());
dto.setUserId(1);
dto.setApplyReason("喜欢动物");
boolean result = adoptService.applyAdopt(dto);
// 断言
Assertions.assertTrue(result);
Assertions.assertEquals(1, petMapper.selectById(pet.getId()).getStatus());
}
}
加@Transactional注解后测试执行完会自动回滚,不会污染数据库,非常适合反复跑。我在调试状态流转逻辑时就靠这个测试方法节省了大量时间——每次改完代码跑一遍,通过就是通过,不通过马上定位,比手工点页面高效得多。
6. 项目扩展方向:这套系统还能长出什么新功能
把这套基础系统跑通之后,如果你还有余力,可以尝试加一些进阶功能,对提升项目含金量很有帮助。
第一个方向是搜索增强。目前宠物搜索是按品种模糊查询,如果你想让"搜索"看起来更智能,可以接入HanLP这类中文分词工具,把宠物描述文本分词后建立索引,用户搜"布偶猫"能匹配到描述里写"蓝眼睛长毛猫"的宠物。这个思路我实践过,效果很惊艳,答辩时也是很好的谈资。
第二个方向是消息通知。目前用户提交申请后,管理员需要主动登录后台去看。可以增加站内信或邮件通知,在用户提交申请的同时,给管理员发一条通知消息。实现起来不复杂,在事务方法里插入一条t_message记录即可。如果用得上,还可以接入企业微信机器人做通知推送,但注意不要选不稳定的渠道。
第三个方向是定时任务。比如对超过7天未处理的申请自动提醒,或者每天定时统计领养数据生成报表。实现定时任务用Spring自带的@Scheduled就可以,简单直接。如果后续任务量大了,可以换成PowerJob或XXL-Job这类分布式调度框架,不过对单体项目来说有点过度设计。
7. 最后的几点实操心得
写完这个项目,我自己也有几点体会。
第一,不要一上来就写代码。先用思维导图把角色、功能、状态流转梳理清楚,尤其是领养流程的状态机,画明白了再动手,会少走很多弯路。我第一版代码就是因为流程没想清楚,后来返工了将近一半。
第二,遇到报错先看堆栈最底部的Caused by,不要被表面的Exception字样带偏。很多人一看到NullPointerException就懵了,其实根因可能只是某个参数没传进来,往上看两行就能发现。
第三,项目里一定要保留日志。我在每个Service的核心方法都加了log.info日志,记录入参、关键状态变化、异常信息。调试时打开日志看一遍执行链路,比打断点单步跟踪快得多。
第四,如果这套系统是拿来交作业或答辩的,请一定把数据库脚本、源码、文档放在一起,并且确保在全新环境下能一键跑起来。我见过太多人代码写得不错,结果换台电脑就启动不起来,印象分大打折扣。
这套宠物领养系统本身技术难度不算高,但胜在业务完整、流程清晰,非常适合作为Java后端学习者研究。希望这篇文章能帮你少走一些弯路,做出一套真正能跑、能讲、能展示的完整项目。
