
简介这是一套基于Spring Boot、Vue和MySQL的洗衣店订单管理系统完整项目面向计算机专业毕业设计或课程设计人群覆盖管理员、顾客、店家三类角色的典型业务场景包括店铺信息、衣服类型、洗衣信息、订单管理、进度跟踪与交流区等模块。包内共831个文件以Java后端源码、Vue前端页面、JavaScript与CSS样式、HTML静态文件为主附带SQL数据库脚本、YML配置文件、Maven包装器及一键启动的BAT脚本压缩包约19.19MB结构清晰便于导入IDE运行调试。截至目前已有247人学习下载。借助该资源可快速理解Spring Boot与Vue前后端分离开发思路参考订单状态流转、权限角色划分等实现细节还能直接复用数据库设计、接口封装与页面布局无论是功能模块划分还是界面设计都很有参考价值尤其适合需要完成类似管理系统类毕业设计的学生。1. 洗衣店订单管理系统要先把状态流转想清楚再谈Spring Boot代码洗衣店订单管理系统这类毕业论文项目最常见的翻车不是页面数量不够而是把系统做成一张订单表的增删改查。洗衣店的真实业务是一串状态顾客送衣后订单处于待取衣安排洗涤后进洗涤中洗完到待领取顾客取走并结清才算完成。订单只是这条状态链的载体Spring Boot 在这里的价值不是“框架新”而是用最少的配置把状态机、事务、分页和统计串成一条能当场演示的闭环。下面的内容按数据建模、Spring Boot核心实现、查询与统计参数、版本排错和答辩复盘四条线展开适合正在做 Java 毕设、并且想在演示和提问环节把“这个状态是怎么防串改的”答明白的人。2. 洗衣服务的数据建模订单主表与明细表怎么拆才撑得起论文2.1 订单状态机待取衣、洗涤中、待领取、已完成四个主状态从哪来洗衣店订单的状态不能随便拍脑袋定。先看业务动作顾客把衣服送到前台店员登记衣物品类、数量和约定取衣时间衣物进入洗衣车间开始洗涤洗完后前台通知顾客取衣顾客到店确认衣物无损、支付费用后取走。在这条链上至少需要四个业务状态和两个边界状态状态码状态名允许流转到的目标触发方0待取衣洗涤中、已取消店员/顾客1洗涤中待领取、待取衣店员2待领取已完成、待取衣店员/顾客3已完成无终态系统4已取消无终态店员/顾客把“已完成”和“已取消”设计成终态是为了让统计口径简单营业日报只汇总状态为“已完成”的订单不会把取消单混进收入。状态机里我看到不少毕设会漏掉“洗涤中回退待取衣”这条边现实场景里确实存在衣服被洗坏或顾客临时要求先不洗的情况没有回退边业务演示时就会卡死在实现上。2.2 表结构拆分为什么订单明细必须从订单主表拆出去洗衣订单天然是一对多关系同一单里可能同时包含水洗一件外套、干洗一件衬衫、熨烫一条裤子。如果把这些服务项以逗号字符串存在订单表的一个字段里查询“这个月洗了多少件衬衫”时只能靠 LIKE 匹配索引失效且统计不准。常规设计是订单主表只保存顾客、总额、状态、取衣时间这类聚合信息明细表一行一条服务项。我一般会保留五张核心表客户表、服务项目表、订单主表、订单明细表、状态变更日志表。支付记录可以并入订单主表里的支付时间字段论文数据量下不需要单独拆表。这种规模能支撑论文章节里 ER 图的完整性也不会因为表太多让答辩时被追问“你这张表存在的理由是什么”。字段设计上有两个经验点。第一金额用DECIMAL(10,2)不要用FLOAT订单总额的累加误差在论文实验里虽然看不出问题但统计营业日报时误差会被放大。第二订单主表里冗余一个total_count总件数明细表条数由程序保证一致性后列表页查询件数就不用每次SUM明细。2.3 用建表 DDL 把模型落成 MySQL 表直接放进 schema.sqlCREATE TABLE IF NOT EXISTS customer ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 客户编号, name VARCHAR(32) NOT NULL COMMENT 姓名, phone VARCHAR(20) NOT NULL COMMENT 手机号, created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 建档时间, UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT客户表; CREATE TABLE IF NOT EXISTS orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT 订单号, customer_id BIGINT NOT NULL COMMENT 客户ID, status TINYINT NOT NULL DEFAULT 0 COMMENT 状态:0待取衣,1洗涤中,2待领取,3已完成,4已取消, total_count INT NOT NULL DEFAULT 0 COMMENT 衣物总件数, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 应收总额, received_time DATETIME DEFAULT NULL COMMENT 送衣时间, finish_time DATETIME DEFAULT NULL COMMENT 完成时间, operator_id BIGINT COMMENT 最后操作员ID, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_customer_phone (customer_id), KEY idx_status_created (status, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表;这段 DDL 里的KEY idx_status_created (status, created_at)是个容易被忽略的联合索引。订单列表页最常用的筛选条件是状态和时间范围这个联合索引能让第 4 章的分页和统计查询直接走索引而不是每次全表扫。updated_at用ON UPDATE CURRENT_TIMESTAMP在状态变更更新记录时自动刷新对演示“最后修改时间”很有用。CREATE TABLE IF NOT EXISTS order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL COMMENT 订单ID, service_name VARCHAR(64) NOT NULL COMMENT 服务项目名称, unit_price DECIMAL(10,2) NOT NULL COMMENT 单价, quantity INT NOT NULL DEFAULT 1 COMMENT 件数, amount DECIMAL(10,2) NOT NULL COMMENT 小计, KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细表;明细表不建外键约束是我在毕设项目里刻意做的取舍。论文系统用真实外键会引来两个问题删客户时级联删除订单容易误删演示数据且 MySQL InnoDB 在外键检查上多一层开销。数据一致性由 Service 层在同一个事务里保证这一点在答辩时能主动说出来反而是加分项。3. Spring Boot 订单系统的核心实现状态机校验与事务边界要一起设计3.1 用 Java 枚举实现订单状态流转校验代替散落的 if-else状态流转逻辑如果直接写在 Controller 里每种状态都来一个if (0.equals(status)) { ... }测试时很难覆盖全答辩时也讲不清楚。我更习惯把状态机建模成 Java 枚举将“当前状态能不能到目标状态”收敛成枚举内部的一个方法。public enum OrderStatus { PENDING_PICKUP(0, 待取衣), WASHING(1, 洗涤中), READY(2, 待领取), COMPLETED(3, 已完成), CANCELLED(4, 已取消); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public static OrderStatus of(Integer code) { if (code null) { throw new BizException(订单状态不能为空); } for (OrderStatus status : values()) { if (status.code code) { return status; } } throw new BizException(未知订单状态: code); } public boolean canTransitTo(OrderStatus target) { switch (this) { case PENDING_PICKUP: return target WASHING || target CANCELLED; case WASHING: return target READY || target PENDING_PICKUP; case READY: return target COMPLETED || target PENDING_PICKUP; default: return false; } } public int getCode() { return code; } public String getDesc() { return desc; } }这段代码的关键是canTransitTo它把第 2 章状态表里的流转规则写成了可测试的代码。of方法负责把数据库里的TINYINT转成枚举写错状态码时能立刻抛出业务异常而不是返回 null 继续运行。新增一个状态只需要改枚举和switchController 与 Service 都不用动。3.2 Service 层定义 changeStatus加锁、校验、落日志缺一不可订单状态变更必须先查询出订单当前状态再校验目标状态是否合法最后更新。这里的难点是并发顾客在柜台办理取衣的同时店员可能在系统里误操作一遍“转洗涤中”两条请求并发改同一条订单传统查改模式会丢更新。常见做法是更新前加行锁把 SELECT 语句升级成锁定读。Service public class OrderService { private final OrderMapper orderMapper; public OrderService(OrderMapper orderMapper) { this.orderMapper orderMapper; } Transactional(rollbackFor Exception.class) public void changeStatus(Long orderId, OrderStatus target, Long operatorId) { Order order orderMapper.selectByIdForUpdate(orderId); if (order null) { throw new BizException(订单不存在); } OrderStatus current OrderStatus.of(order.getStatus()); if (!current.canTransitTo(target)) { throw new BizException( String.format(不允许从[%s]流转到[%s], current.getDesc(), target.getDesc())); } order.setStatus(target.getCode()); order.setOperatorId(operatorId); orderMapper.updateStatus(order); StatusLog log new StatusLog(); log.setOrderId(orderId); log.setFromStatus(current.getCode()); log.setToStatus(target.getCode()); log.setOperatorId(operatorId); orderMapper.insertStatusLog(log); } }对应的 Mapper 里要写这样一个查询Select(SELECT * FROM orders WHERE id #{id} FOR UPDATE) Order selectByIdForUpdate(Long id);FOR UPDATE是 InnoDB 的行级排他锁第一个事务更新订单期间第二个事务的同一查询会阻塞等前一个提交后才读到新状态。Transactional(rollbackFor Exception.class)保证状态更新和日志插入要么同时成功要么同时回滚。如果抛出的业务异常类型是 RuntimeExceptionSpring 默认也会回滚但rollbackFor写出来会让读代码的人一眼确认事务行为。3.3 Spring Boot MyBatis 启动时自动建表schema.sql 和初始化模式怎么配论文答辩时不用手动导 SQL 建库是一个小巧思项目启动自动把表建好演示环境换成另一台电脑也能一分钟跑起来。Spring Boot 里配动态数据源初始化即可spring: datasource: url: jdbc:mysql://localhost:3306/laundry?createDatabaseIfNotExisttrueuseUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver sql: init: mode: always schema-locations: classpath:schema.sql encoding: utf-8提示Spring Boot 2.5 之后初始化配置统一在spring.sql.init下之前版本是spring.datasource.initialization-mode抄旧博客代码时注意版本差异。配置里的createDatabaseIfNotExisttrue解决库不存在问题schema-locations指向 classpath 下的 schema.sql。初始化脚本里的建表语句必须全部写成CREATE TABLE IF NOT EXISTS因为mode: always在每次启动都会执行一遍没有 IF 判断第二次启动就会直接报表已存在并中断应用。MySQL 驱动如果是 8.xdriver-class-name要用com.mysql.cj.jdbc.Driver旧驱动类com.mysql.jdbc.Driver在新驱动包中已经移除。4. 订单查询、分页与营业统计把论文系统的检索能力收口4.1 分页参数怎么设计手写 LIMIT 与 PageHelper 选哪种订单列表是订单管理系统的门面页面分页查询既要稳定又要能在答辩时讲清原理。两种常见实现方式一是 MyBatis 手写LIMIT #{offset}, #{pageSize}二是引入 PageHelper 插件。我建议毕业论文项目优先手写分页理由有三个不引入额外依赖打包体积更小SQL 执行流程完全可控回答“分页原理是什么”时可以直接讲 SQL 层面的 offset 和 limit避免 PageHelper 在复杂 SQL 下自动拼接 limit 到错误语句位置的坑。SELECT COUNT(*) FROM orders WHERE status #{status}; SELECT id, customer_id, status, total_count, total_amount, received_time, finish_time, operator_id FROM orders WHERE status #{status} ORDER BY created_at DESC LIMIT #{offset}, #{pageSize};对应的查询参数是一个简单 DTOpublic class OrderQuery { private Integer status; private Integer pageNum 1; private Integer pageSize 10; public int getOffset() { return Math.max((pageNum null ? 1 : pageNum) - 1, 0) * getSafePageSize(); } public int getSafePageSize() { return Math.min(pageSize null ? 10 : pageSize, 100); } }getOffset转换有一个细节前端传的是从 1 开始的页码SQL 需要的是从 0 开始的偏移量所以先减一再乘每页条数。getSafePageSize把单页查询条数限制在 100防止用户传pageSize10000一次把整张表拉出来这在学校演示现场很常见参数校验要写在查询参数构造阶段而不是 SQL 层。4.2 营业日报的聚合 SQL日期范围左闭右开金额只统计完成态洗衣店老板最关心的是一天收了多少单、赚了多少钱。营业日报接口只做一件按自然日聚合。聚合 SQL 用日期函数对created_at做分组需要注意日期范围的边界写法统计某天数据时不要用BETWEEN start AND end否则end那天的 00:00:00 到第二天的数据会被漏掉或重复。SELECT DATE_FORMAT(created_at, %Y-%m-%d) AS biz_date, COUNT(*) AS order_count, IFNULL(SUM(total_amount), 0) AS total_amount, IFNULL(SUM(total_count), 0) AS total_count FROM orders WHERE status 3 AND created_at #{startDate} AND created_at DATE_ADD(#{endDate}, INTERVAL 1 DAY) GROUP BY DATE_FORMAT(created_at, %Y-%m-%d) ORDER BY biz_date DESC;这段 SQL 里面有三个容易讲错的位置。第一status 3表示已完成订单因为待领取订单还没结清提前算进营收会让日报和月报对不上。第二IFNULL(SUM(...), 0)处理该日期没有订单时SUM返回 null 的情况避免 Java 侧拆箱空指针。第三DATE_ADD(#{endDate}, INTERVAL 1 DAY)把结束日期转成第二天的零点配合和正好覆盖一整天。4.3 查询接口的必调参数联合索引、拆分深层分页和时区查询层最容易出现“写着写着就慢下来”的地方是深分页。订单数据到几千条以后LIMIT 10000, 20会先读前 10020 行再丢弃前 10000 行一次比一次慢。论文级别的系统通常在演示数据里看不出问题但答辩评委一句“数据量翻十倍还快吗”就要求你能说出优化方案。一个常用的优化是“延迟关联”先只在索引上查出目标主键再用主键回表取完整行SELECT o.* FROM orders o INNER JOIN ( SELECT id FROM orders WHERE status #{status} ORDER BY created_at DESC LIMIT #{offset}, #{pageSize} ) t ON o.id t.id ORDER BY o.created_at DESC;这样内层查询只需要扫联合索引(status, created_at)两级外层再按主键精准取 20 行避免一开始就把所有字段捞出临时表。另一个常见问题是 JDBC 连接串里漏掉serverTimezoneAsia/Shanghai导致日期字段在 JavaLocalDateTime和 MySQLDATETIME之间转换出现八小时偏差营业日报按天分组时数据跑错日期这个坑在演示前夜排查最为痛苦配置要提前检查。5. Spring Boot 版本太高引发的兼容排错与毕业论文答辩追问口径5.1 javax 变成 jakartaSpring Boot 3.x 下项目起不来的处理思路你是不是下载 Spring Boot 时自动选到了 3.x 版本项目启动后报Uncaught exception java.lang.NoClassDefFoundError: javax/servlet/ServletException这个报错几乎每个把 Spring Boot 版本拉高的毕设都会遇到一次。Spring Boot 3.0 从 Java EE 迁到了 Jakarta EE 9包名从javax.*全部改成jakarta.*依赖里引用的旧版拦截器、过滤器、Tomcat 组件都还在找javax.servlet类结果自然是类找不到。处理思路是先确认当前项目的 Java 版本。Spring Boot 3.x 要求 Java 17 起如果你本机装的 JDK 还是 8直接保住 2.7.x 更省事。如果一定要用 3.x需要把所有源码和第三方依赖里的import javax.servlet替换成import jakarta.servlet并且让所有 Web 相关依赖升级到 Jakarta 兼容版本。用 Maven 查看冲突坐标是第一步mvn dependency:tree -Dincludesjavax.servlet:javax.servlet-api这个命令会把所有依赖 javax.servlet-api 的组件路径列出来看到哪个三方 starter 还在传递引入旧 servlet就排除掉或换它的 Jakarta 版本。在pom.xml里排除的方式如下dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.7/version exclusions exclusion groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId /exclusion /exclusions /dependency5.2 答辩追问状态流转失败怎么答才能体现事务设计能力评委经常顺着演示问一句“如果店员把订单从待领取误操作到洗涤中系统是怎么阻止的”你的回答要先点出枚举校验再说事务回滚changeStatus 在提交 SQL 前调用canTransitTo不合法立即抛 BizException事务回滚后数据库状态原封不动。这个回答已经能覆盖大部分评分点。追问再深一层“如果审计日志也想记录失败尝试呢”这里有个事务传播陷阱记录失败日志如果也写在当前事务里抛异常后日志会被回滚掉等于白记。常见的做法是给失败日志方法单独开一个新事务用REQUIRES_NEW传播级别让日志提交不受主事务回滚影响。对这个传播机制的理解比项目里有多少个接口更能拉开答辩差距。5.3 答辩前要画好的三张图及每张图的讲解口径洗衣店订单管理系统论文通常需要三张图支撑核心章节。ER 图按第 2 章的表结构画画图时把客户与订单画成 1:N订单与明细画成 1:N状态日志与订单画成 N:1并在图上标注订单主表里的状态字段取值范围。流程图画出状态机的六个状态和八条合法流转边直接复用canTransitTo的逻辑。架构图画三层Controller 层不写业务逻辑只接收参数Service 层承载 changeStatus 和事务边界Mapper 层只做 SQL 映射。讲解时把第 3 章的枚举代码和一张堆栈调用截图放进对比页说明为什么 Controller 里禁止出现“把 status 改成 2”这种裸赋值。本文还有配套的精品资源点击获取