每年毕业季前后,总能在各种技术群里看到有人问同一个问题:“毕设选题到底选什么好?”我看了很多题目之后发现,像“基于Spring Boot的衣物保养管理系统”这种题目,出镜率相当高。原因是它看起来不像电商、外卖那么复杂,但里面的业务逻辑却比普通的增删改查完整得多,特别适合用来展示一个学生的数据库设计能力、业务建模能力和 Java 后端基本功。
我前后帮几个同学完整梳理过这套系统,从需求分析到数据库设计,再到核心代码落地,每一步都踩过一些坑。这篇文章就按实际开发顺序,把这个项目从选题到部署的关键环节拆开讲清楚,重点解释每个设计决策背后的原因,也给准备做相似管理系统的同学一份可以直接参考的实操方案。
1. 项目概述与需求拆解
1.1 选题背景:它到底解决什么问题
衣物保养这件事,听起来很生活化,但真管理起来是有痛点。普通人家里有几十件衣服,哪些需要干洗,哪些必须手洗,哪些换季之后要特别收纳,时间一长根本记不住。如果放到干洗店、小型服装护理店或者学校洗衣房这类场景里,问题会被放大:客户送来的衣物不知道上次保养时间是什么时候,不同材质的衣服保养周期也不一样,全靠店员脑袋记,很容易漏掉重要提醒。
这个项目本质上做的就是把“衣物信息、保养周期、保养任务、执行记录、到期提醒”这条链路搬到线上。Spring Boot 作为后端框架,正好能承担起用户管理、衣物档案维护、任务生成和提醒查询这些核心接口。对于毕设而言,这个选题的价值在于它既有基本的 CRUD,又有业务状态变化、定时任务、报表统计这些加分项,不会显得太单薄,也不至于复杂到做不完。
1.2 需求梳理:两个角色和一条业务主链
做系统之前,第一步不是写代码,而是先把角色理清楚。这个管理系统我一般建议分成两个核心角色:
- 普通用户:可以注册登录,录入自己的衣物,查看保养计划,确认保养任务,查看历史保养记录。
- 管理员:除了拥有普通用户功能外,还可以管理用户账号、查看所有衣物统计信息、维护衣物分类和保养项目模板。
如果用户量不大,这两种角色共用同一套用户表,用一个role字段区分就行,不要建两张独立的用户表,那样反而增加维护成本。系统跑起来之后,核心业务主线是:用户登记衣物 -> 系统根据衣物类别和上次保养时间自动生成保养计划 -> 用户执行保养并填写记录 -> 系统在下次保养时间到来之前发出提醒。这条主链一旦打通,项目演示时无论问哪个模块,你都能把逻辑串起来讲清楚。
1.3 功能边界与亮点设计
很多同学做毕设喜欢堆功能,觉得页面越多越好。但衣物保养管理系统最忌讳的就是把简单事情复杂化。我建议把功能控制在这个范围:
- 用户注册登录、个人资料修改。
- 衣物信息的新增、编辑、删除、分页查询。
- 衣物图片上传与展示。
- 保养任务自动生成、手动确认完成。
- 保养到期提醒(登录后待办提醒即可)。
- 按衣物类别、保养状态、保养完成率做统计图表。
如果还想加点亮点,可以加入“保养日历”,把每个月的保养任务散点展示在日历上,这个功能本质还是查表,只是展示形式更好看,答辩时也比较容易把思路讲明白。
2. 技术选型与架构设计
2.1 为什么坚定选择 Spring Boot
题目里点名要 Spring Boot,这本身就说明它适合做这类系统。对比之前的 SSM 组合,Spring Boot 最大的变化是省掉了大量 XML 配置,只需要在pom.xml里引入依赖,再配合自动配置就能启动 Web 项目。对于几个月的毕设周期来说,把时间省下来去打磨业务细节,比花时间在配置上更有价值。
另外 Spring Boot 内嵌了 Tomcat,本地开发不需要单独装服务器,打包之后一个java -jar命令就能跑起来。这对后面部署演示来说非常友好,即使答辩现场的电脑环境比较干净,只要装了 JDK 就能运行系统。生态方面,Spring Boot 接入 MyBatis、Redis、Spring Security 这些常用组件都相当顺手,真遇到问题,网上的资料也最多。
2.2 前端方案:从模板渲染到前后端分离
前端怎么选,主要看你的时间预算。我有两种推荐方案:
- 第一种是后端使用 Thymeleaf 模板引擎,加上 Bootstrap 或者 Layui 做样式。这种方式适合开发周期很紧、对前端不熟的同学,所有页面都由后端渲染,不需要单独启动前端服务,代码量相对少。
- 第二种是前后端分离,前端用 Vue 3 + Element Plus,后端只提供 JSON 接口。这种方式结构清晰,答辩时也可以强调“前后端分离架构”,但需要额外处理跨域、Token 存储、接口联调这些问题。
我带的同学里,有一个选了第一种,两个月就完成了;另一个选了第二种,多花了三周时间,但最终效果确实更现代。如果你已经有一定 Vue 基础,建议选第二种;如果时间真的不够,用模板渲染也完全能拿得出手,重点还是在后端业务逻辑。
2.3 分层架构与项目目录规划
Spring Boot 项目启动很快,但如果刚开始就把所有代码堆到一个类里,后面越写越难受。我会在项目初始化的时候就把包结构固定下来:
com.example.clothes ├── common // 统一返回结果、异常处理、常量 ├── config // 配置类,如跨域、拦截器、静态资源映射 ├── controller // 接口层 ├── service // 业务逻辑层 ├── mapper // 数据访问层 ├── entity // 数据库实体 ├── dto // 入参出参对象 └── task // 定时任务这个分层的好处是每一层职责单一,比如Controller只负责接收参数和返回结果,Service只处理业务逻辑,Mapper只跟数据库打交道。后面做事务控制、权限校验的时候,改动范围也很明确,不容易出现改一个功能把另一个功能带崩的情况。
3. 数据库设计与业务建模
3.1 核心表设计逻辑
数据库设计是整个项目的地基,表设计得好,后面的代码能省一半力气。这套系统我建议设计六张核心表,分别是用户表、衣物表、保养任务表、保养记录表、提醒表、操作日志表。
用户表sys_user主要字段有id、username、password、nickname、phone、role、status、create_time。密码字段存储加密后的结果,不要存明文,这样演示安全功能的时候也能讲两句。
衣物表clothing是系统的核心,字段设计一定要想清楚。我常用的字段如下:
id:主键。user_id:所属用户。name:衣物名称。category:衣物类别,比如外套、裤装、裙装。fabric:材质,如羊毛、棉麻、化纤。colour:颜色。image_url:图片地址。purchase_time:购买时间。care_cycle_type:保养周期类型,是按天还是按月。care_cycle_value:周期数值,比如 30 天。last_care_time:上次保养时间。next_care_time:下次保养时间。status:当前状态,在保养中、正常、待处理。
这里的关键是last_care_time和next_care_time,几乎所有跟保养相关的功能都要查这两个字段。比如用户登录之后,系统要计算哪些衣物已经超过next_care_time,就需要用当前时间和它做对比。
保养任务表care_task用于记录每一条待办任务,字段包括id、clothing_id、user_id、task_type、plan_time、actual_time、status、operator、remark。任务状态我习惯用0表示待处理,1表示已完成,2表示已取消。这样后面做状态统计时直接用group by status就行了。
保养记录表care_record用于保存用户实际操作过什么,字段包括clothing_id、care_type、care_time、cost、content、next_care_time。任务表和记录表的区别要讲清楚:任务是“计划要做的事”,记录是“已经做过的事”。很多同学会把这两张表合在一起,结果逻辑越写越混乱,我强烈建议分开。
提醒表remind_record用于记录系统产生的提醒消息,字段有user_id、clothing_id、task_id、remind_time、is_read。操作日志表operation_log则记录谁在什么时候做了什么操作,主要给管理员查看,顺便也能作为答辩时体现系统完整性的一个点。
3.2 关键字段与约束细节
字段设计有几个细节,直接影响后面编码难度:
- 状态字段统一用
tinyint,不要用字符串。字符串'1'和'0'看着直观,但查询效率和存储空间都不如数值,还容易写错。 - 金额字段用
decimal(10,2),如果保养需要记录费用,别用double,浮点计算会有精度问题。 - 时间字段统一用
datetime,不要混用date和timestamp,否则处理时间区间查询时很容易出问题。 - 图片地址不要直接存二进制
blob,存一个可访问的 URL 字符串就行,文件放到服务器或对象存储上。
另外索引设计也很重要。clothing表的user_id和next_care_time要加索引,因为首页查询“我的衣物待保养列表”时最常用的就是这两个条件。care_task表的user_id和status同样建议加联合索引。虽然学生项目数据量不大,索引影响不明显,但设计文档里写上索引,答辩老师会觉得你考虑得比较全面。
3.3 数据权限与状态机设计
那么问题来了,数据权限怎么控制?用户登录之后,只能看到自己的衣物和任务,管理员可以看全部。这个逻辑不能只靠 SQL 里固定写死,比较好的做法是在 Service 层先从登录信息里取出userId,再作为查询条件传给 Mapper。也就是说,后端的每一个查询接口都要有当前用户身份的参与,不能提供不受限制的全量查询接口。
状态流转也要提前规划。衣物的状态不是随便改的,通常的流转路径是:登记 -> 待保养 -> 保养中 -> 已保养。如果代码里到处允许把“已保养”直接改成“登记”,整个系统就会显得不严谨。我一般会在 Service 层写一个状态校验,只有合法的状态迁移才允许执行。例如执行保养时,只有待保养状态的任务才能置为完成,如果是已经完成的任务再提交,就直接抛出业务异常。
4. 核心功能模块实现
4.1 衣物档案管理:从表单到上传
衣物管理模块是整个项目的基础,包含新增、编辑、删除、列表查询四个接口。新增和编辑可以合并成一个保存接口,通过传入的id判断是新增还是更新。
衣物图片上传是大多数同学卡住的地方。前端选完图片后,后端接口接收MultipartFile,然后把文件保存到本地目录,再把访问路径返回。这里我建议分两步:
- 配置静态资源映射,把
/upload/**映射到本机的/data/upload/目录。 - 文件保存时生成唯一文件名,比如用
UUID.randomUUID()加上原始扩展名,避免中文文件名乱码和重名覆盖。
一个简单的上传接口示例:
@PostMapping("/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { if (file.isEmpty() || file.getSize() > 5 * 1024 * 1024) { throw new BusinessException("文件为空或超过5MB"); } String originalFilename = file.getOriginalFilename(); String ext = originalFilename.substring(originalFilename.lastIndexOf(".")); String filename = UUID.randomUUID().toString().replace("-", "") + ext; File dest = new File(uploadDir + filename); file.transferTo(dest); return Result.success("/upload/" + filename); }注意文件大小和类型都要校验,类型可以用扩展名白名单,比如只允许jpg、png、jpeg、webp。如果不做校验,上传一个.exe文件也会成功,从系统安全角度说不过去。
4.2 保养任务自动生成:定时体系怎么搭
任务自动生成是这个系统最需要讲清楚的功能。它本质上是一段定时任务,每天扫描一次所有衣物,查找当前时间已经超过next_care_time且没有生成过任务的衣服,然后生成一条care_task。
Spring Boot 里开启定时任务很简单,启动类上加上@EnableScheduling,然后在任务类的方法上标注@Scheduled。为了不每分钟跑一次太浪费,我通常配置成每天零点执行一次,或者每隔一小时执行一次。
核心思路如下:
@Component public class CareTaskGenerator { @Scheduled(cron = "0 0 2 * * ?") public void generateTasks() { List<Clothing> overdueList = clothingMapper.selectOverdue(DateTime.now()); for (Clothing clothing : overdueList) { int count = careTaskMapper.countPendingTask(clothing.getId()); if (count > 0) { continue; } CareTask task = new CareTask(); task.setClothingId(clothing.getId()); task.setUserId(clothing.getUserId()); task.setStatus(0); task.setPlanTime(LocalDateTime.now()); careTaskMapper.insert(task); } } }这里的countPendingTask非常重要,它用来防止重复生成任务。如果某件衣服已经有一条“待处理”状态的任务,就不应该再生成第二条。我就见过一个同学没写这一步,跑了一晚上定时任务,第二天数据库里多了几百条重复数据,演示的时候尴尬得不行。
4.3 提醒通知模块实现
提醒功能不需要做得太复杂,站内信形式就足够。用户每次登录后,在首页查询未读提醒,展示“您的羊毛大衣已超过保养时间3天,请及时处理”这样的消息。点进去之后可以把is_read置为 1,也可以直接点击跳转到对应衣物详情。
查询示例可以这样写:
public List<RemindVO> listUnRead(Long userId) { List<RemindRecord> records = remindMapper.selectUnRead(userId); return records.stream().map(record -> { RemindVO vo = new RemindVO(); Clothing clothing = clothingMapper.selectById(record.getClothingId()); vo.setClothingName(clothing.getName()); vo.setDays(ChronoUnit.DAYS.between(record.getRemindTime(), LocalDateTime.now())); return vo; }).collect(Collectors.toList()); }除了站内信,如果想展示更多技术点,还可以在提醒模块里引入邮件通知。用 Spring Boot 的spring-boot-starter-mail依赖,配置一个发件邮箱,在生成任务的同时发送一封保养提醒邮件。注意邮件发送过程可能比较慢,不要在用户请求线程里同步发送,可以丢到异步线程池处理,或者直接用@Async注解让方法异步执行。
4.4 统计报表与数据看板
管理员首页一般要展示几个核心指标:衣物总数、待保养任务数、本月完成保养次数、各衣物类别占比。这些数据用 SQL 聚合查询很快就能得到。
例如统计每个类别的衣物数量:
SELECT category, COUNT(*) AS total FROM clothing GROUP BY category;统计任务完成率:
SELECT COUNT(*) AS total_task, SUM(CASE WHEN status = 1 THEN 1 ELSE 0 END) AS finished_task FROM care_task;前端展示可以用 ECharts 的饼图和柱状图,这个库接入简单、效果也不错。我建议后端把统计结果封装成一个StatisticVO,一次性返回多个指标,避免前端频繁调接口。比如:
public StatisticVO getStatistics() { StatisticVO vo = new StatisticVO(); vo.setClothingTotal(clothingMapper.selectCount(null)); vo.setPendingTask(careTaskMapper.selectPendingCount()); vo.setFinishedTask(careTaskMapper.selectFinishedCount()); vo.setCategoryStats(clothingMapper.selectCategoryStats()); return vo; }有同学会问,统计报表是不是要实时计算?其实不需要。这个项目的实时计算代价很低,因为数据量小,聚合查询基本都是毫秒级,没必要引入缓存和中间件。但如果以后衣物表数据量上来了,可以把统计结果定时计算好存到一张统计表,再用定时任务每天刷新。
5. 实操过程与部署落地
5.1 从零初始化一个Spring Boot项目
项目初始化我推荐用 IDEA 自带的 Spring Initializr,也可以用官网的在线生成工具。选择项目构建工具 Maven 或 Gradle,我个人更建议 Maven,因为毕业设计用的人最多,遇到问题容易找到解决方案。
依赖选择上,核心的几个必须勾上:
Spring Web。MyBatis Framework或Spring Data JPA。MySQL Driver。Lombok,减少实体类 getter/setter 代码。Validation,做参数校验。
如果是前后端分离,还需要额外引入Spring Security或自己写 JWT 拦截器。这里提醒一下,Spring Security 学习曲线略陡,很多同学配了半天发现登录一直被拦截。如果时间不够,可以自己写一个简单的 JWT 过滤器,核心逻辑就是校验请求头里的 Token 是否存在且合法,最多几十行代码,完全够用。
application.yml里最关键的是数据源配置。本地开发可以直接:
spring: datasource: url: jdbc:mysql://localhost:3306/clothes_care?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 123456 servlet: multipart: max-file-size: 5MB max-request-size: 10MB mybatis-plus: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true如果使用 MyBatis-Plus,map-underscore-to-camel-case这个配置很重要,它能让数据库字段next_care_time自动映射到实体的nextCareTime,省掉大量手写resultMap的麻烦。
5.2 核心的Service层代码怎么写更稳
Service 层是业务逻辑最集中的地方,最容易出现的问题就是代码越长越乱,最后所有方法堆在一起。我的习惯是每个核心业务定义一个 Service 接口和一个实现类,接口只暴露业务方法,实现类里再把公共逻辑抽成私有方法。
比如说衣物保存,不能只写一句 insert。完整流程应该是:
- 参数校验。
- 判断是新增还是更新。
- 如果新增,设置创建时间和初始状态。
- 根据
care_cycle_type和care_cycle_value计算出next_care_time。 - 执行插入或更新。
这里的计算逻辑,我习惯单独抽一个方法:
private LocalDateTime calculateNextCareTime(LocalDateTime lastCareTime, Integer type, Integer value) { if (type == 1) { return lastCareTime.plusDays(value); } else if (type == 2) { return lastCareTime.plusMonths(value); } return lastCareTime.plusDays(30); }这么做的原因是,后期如果保养周期规则变了,只需要改这一个方法,其他地方不用动。同时,Service 层所有对外方法都要抛统一的业务异常,错误信息用中文直接描述,例如“该衣物不存在”“当前状态不允许执行该操作”。不要直接把底层异常抛给前端,否则页面会看到一堆看不懂的英文堆栈。
5.3 打包部署:从本地到服务器
本地跑通之后,打包部署也是一个环节。在项目根目录执行:
mvn clean package -DskipTests打包完成后,target目录下会生成一个clothes-care-0.0.1-SNAPSHOT.jar文件。开发环境可以这样启动:
java -jar target/clothes-care-0.0.1-SNAPSHOT.jar如果放到服务器上,建议把上传目录也放到非用户目录,比如/data/clothes/upload,并用application-prod.yml隔离线上配置。再写一个启动脚本,包含启动、停止、重启功能,会显得专业很多。演示的时候,最稳的方式是提前在答辩电脑上装好 JDK 和 MySQL,把项目打包成 jar 包启动,所有数据提前录好。不过要记住,服务器上的 MySQL 字符集一定要设置为utf8mb4,否则用户注册中文昵称时会出现乱码。
6. 常见问题与避坑实录
6.1 周期计算总是差一天
这个我见过太多次了。很多同学计算下次保养时间喜欢用时间戳相减,或者用Date进行计算,结果把时区问题带进来,用户在前天提交的保养记录,系统算出来的下次保养时间差了整整一天。
解决办法是全部使用LocalDate或LocalDateTime,不要用java.util.Date做加减法。并且要保证数据库连接的serverTimezone=Asia/Shanghai。另外,算天数差的时候注意“当天”的定义,比如今天是保养日的当天,到底算不算已过期,要在代码里定好。我建议用“超过”而不是“大于等于”,也就是当前日期晚于next_care_time时才触发提醒,这样用户体验比较好。
6.2 定时任务出现重复数据
前面提到过重复数据的问题,这里再展开讲讲。定时任务本身不是只跑一次,它会按周期重复执行,如果上一次还没执行完,下一次又开始跑了,就可能重复。虽然 Spring Boot 默认定时任务是单线程串行执行,但假如你把@Scheduled放在多个方法上,或者把方法执行时间拉得很长,还是有机会出现并发问题。
对策有三个层次:
- 第一层:在生成任务前查一下是否已经存在待处理任务。
- 第二层:给
care_task表加上clothing_id + status的唯一索引,从数据库层面防止重复。 - 第三层:如果后面部署了多实例,就需要引入 Redis 分布式锁。学生项目做到前两层就够了。
6.3 图片上传后访问404
上传成功后,图片路径如果访问不到,大概率是静态资源映射没配置。Spring Boot 默认只把classpath:/static/作为静态资源目录,你保存到本机/data/upload/的图片不会自动被访问到。
需要在配置类里做映射:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadDir); }注意uploadDir末尾要带/,不然拼接路径时会出问题。我在本地调试时也踩过一次,最后发现是 Windows 路径分隔符和 Linux 不一致导致的,后来统一用Paths.get(uploadDir).toAbsolutePath()来生成路径,兼容性就好很多。
6.4 前后端联调时的数据格式冲突
前后端分离模式下,最容易出现两个问题:跨域和日期格式。
跨域解决方案很简单,写一个配置类实现WebMvcConfigurer,放行所有来源即可用于开发环境:
registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") .allowCredentials(true);日期格式问题也很典型。后端把LocalDateTime序列化之后,前端收到的可能是"2025-05-12T10:30:00",带个T,页面显示不好看。两种处理办法:一种是前端解析的时候替换掉T,另一种是后端在application.yml里统一配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai推荐第二种,一劳永逸。
写在最后的一些体会
这个项目从题目看似乎不复杂,但真把它做完整,涉及到的东西并不少:角色权限、衣物建模、保养周期计算、定时任务、状态流转、统计报表、文件上传、前后端联调、打包部署,每一块都能单独拿出来给面试官讲十分钟。我个人的体会是,做这类管理系统,前期花在数据库设计上的时间绝对值得,表结构理清了,后面的 Service 层代码会写得非常顺,反而是急着写代码的同学,后面改表结构改到崩溃。
最后再分享一个小技巧:演示的时候不要只演示正常流程,可以提前准备一件“已经超过保养时间”的衣物数据,登录后首页立刻出现提醒,然后你现场点一下“确认保养”,再去看统计页面,数字也会同步变化。这套连招走下来,整个业务闭环特别完整,比单纯展示列表和表单有说服力得多。希望这篇文章能帮到正在设计和实现这个系统的你,也祝答辩顺利。