1. 洗衣店系统的真正难点衣物的全生命周期追踪每年毕业设计季总有同学拿着“基于SpringBoot的洗衣店管理系统”这个题目来找我。很多人的第一反应是这不就是个进销存吗用户、商品、订单、结算四张表一凑跑起来就完事。但我见过太多在答辩现场卡壳的版本——订单能新增、能删除可一问“衣服送到洗衣房之后系统怎么跟踪洗到哪一步”就没人答得上来。这个题真正要检验的不是你会不会写 CRUD而是你能不能把一件衣服从收衣台到取衣台的整个生命周期用一套可追踪、可结算、可统计的数据模型表达出来。这里我不想再用“项目介绍功能截图核心代码”那种流水账来写而是把这套 SpringBoot 洗衣店管理系统——你也可以叫它干洗服务智能运营平台或者衣物洗护中心数字化管理系统——从业务分析、技术选型、表结构、状态机、会员营销、报表统计到答辩准备的关键链路拆开讲清楚。适合这几类人看准备拿这个题目做毕业设计的学生正在帮门店做数字化方案的技术人员以及想自己拉一套小系统做副业开发的工程师。1.1 洗衣店和超市的订单本质上是两种逻辑超市卖的是标准商品库存是“进多少、卖多少、剩多少”的线性关系。洗衣店卖的是服务服务对象是一件具体的、非标准化的衣服。同样是“下订单”超市订单只需要知道商品编号和数量洗衣店订单必须知道更多这件衣服是棉的还是羊毛的能不能水洗袖口有没有污渍纽扣有没有掉客人希望什么时候取。这些信息一旦缺失后面所有环节都会出问题。洗衣工不敢开工前台找不到衣服客人取衣时质疑“我送来的衣服不是这件”店长月底对不上账。所以洗衣店管理系统的第一设计原则不是“把订单录进去”而是“把每一件衣物从进门到出门的每一个关键节点都留下来”。另一个容易忽视的点是结算时机。超市通常是先付钱再拿货而很多洗衣店是收衣时不付款取衣时再结算。这意味着订单表和支付表必须分开订单状态和支付状态也要分开。不要想着用一个字段把“洗护状态支付状态”都表达清楚后面统计和做会员积分的时候会把自己绕死。1.2 谁在用这个系统角色与使用场景做系统之前先盘一遍使用者这是很朴素但很有效的设计方法。洗衣店管理系统里至少有四类角色前台收银员负责收衣、打单、取衣、收款是系统的最高频使用者。洗衣工/车间主管负责更新洗护进度比如“已入机”“洗护中”“质检完成”。店长/老板看营业数据管理会员、套餐、库存偶尔处理退款和异常订单。会员顾客虽然门店端不一定开放自助入口但会员信息、余额、积分都是围绕顾客建立的。毕业设计阶段可以把系统做成纯后台管理端但对这些角色的梳理不能省。你后面分页面、分菜单、分接口权限全靠这张角色表兜底。如果一上来就只做“管理员”和“用户”两个角色答辩时老师大概率会追问洗衣工和收银员权限一样吗店长看的数据和前台看的数据能一样吗这个问题答不上来会很尴尬。2. 技术选型不能只看热度SpringBoot 3 MyBatis-Plus Vue 的取舍技术选型是毕业设计里最容易被老师问“为什么”的部分。千万不要只说“因为 SpringBoot 很火”这个答案约等于没说。你要能讲清楚这个技术在这个场景里解决了什么具体问题。2.1 配置方案与依赖版本怎么定我建议的基线方案是这样模块推荐选型关键理由后端框架SpringBoot 3.2.x自动装配、内置 Tomcat、Starter 生态成熟减少 XML 配置JDK 版本JDK 17SpringBoot 3 基于 JDK 17毕业设计用新版本反而更省事持久层MyBatis-Plus 3.5.x单表 CRUD 不用手写 SQL分页插件开箱即用数据库MySQL 8.0主流稳定字段类型和索引支持都够用权限认证Sa-Token 或 JWT 拦截器轻量能讲清楚原理比硬套 Spring Security 更容易落地前端Vue 3 Element Plus后台表单、表格组件齐全能快速搭出合理界面缓存Redis可选用于验证码、Token 或高频热点数据不作为必需项这里有个很现实的问题如果你电脑上只有 JDK 1.8不要硬上 SpringBoot 3.x。SpringBoot 3 要求 JDK 17依赖注入和字节码库不兼容折腾半天全是环境报错。这种情况下老老实实选择 SpringBoot 2.7.18 配合 JDK 8反而能顺利跑完整个项目。所以先确定 Java 环境再选框架版本这个顺序不要反。2.2 为什么不用 SSM也不用微服务很多教材还在用 SSMSpring SpringMVC MyBatis但 SpringBoot 对 SSM 的简化是实打实的不用写一堆 XML Bean 配置不用手动配数据源和事务管理器依赖引入后自动装配。对以业务系统为主要成果的项目来说SpringBoot 能省下大量“配置调试”时间把精力放到真正的业务逻辑上。至于微服务我只说一句洗衣店管理系统是典型的单体业务系统强行拆成订单服务、会员服务、库存服务除了增加网络通信和分布式事务的复杂度没有任何业务收益。答辩时如果被问“为什么不做微服务”可以从业务体量、团队规模、运维成本三个角度回答重点强调“微服务解决的是扩展性问题而不是业务复杂度问题”。3. 数据库设计从洗衣订单到衣物明细的关键表结构数据库设计是洗衣店管理系统真正拉开差距的地方。很多版本只做了order一张表把多件衣服用逗号拼在一个字段里这属于典型的“能跑但没法用”。3.1 核心业务表拆解我通常会拆出这样几张核心表sys_user员工账号包含角色字段或关联角色表。member会员基础信息包含姓名、手机号、余额、积分。clothes_category衣物分类比如外套、衬衫、羽绒服、皮鞋。service_item服务价格项比如“羽绒服干洗 38 元”“衬衫水洗 12 元”。laundry_order洗衣订单主表记录客户、应收金额、实收金额、整体状态。laundry_order_item订单明细一件衣服对应一条记录。laundry_process_log洗护流转日志记录状态变更的时间点和操作人。payment_record支付流水记录现金、微信、支付宝、余额支付等支付方式。member_points_log会员积分变动流水。inventory_stock、inventory_record耗材库存和出入库记录。laundry_order表的大致结构如下CREATE TABLE laundry_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL COMMENT 订单号, member_id BIGINT DEFAULT NULL COMMENT 会员ID非会员为空, customer_name VARCHAR(50) NOT NULL COMMENT 客户姓名, customer_phone VARCHAR(20) NOT NULL COMMENT 客户电话, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 订单总额, discount_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 优惠金额, paid_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 实付金额, order_status TINYINT NOT NULL DEFAULT 0 COMMENT 0收衣 1洗护 2待取 3完成 4取消, pay_status TINYINT NOT NULL DEFAULT 0 COMMENT 0未支付 1已支付, receive_time DATETIME NOT NULL COMMENT 收衣时间, expect_finish_time DATETIME DEFAULT NULL COMMENT 预计完成时间, take_time DATETIME DEFAULT NULL COMMENT 取衣时间, remark VARCHAR(500) DEFAULT NULL COMMENT 备注, create_by BIGINT DEFAULT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_member_id (member_id), KEY idx_customer_phone (customer_phone), KEY idx_order_status (order_status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;订单明细表要单独强调一下CREATE TABLE laundry_order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, service_item_id BIGINT NOT NULL, item_name VARCHAR(100) NOT NULL COMMENT 衣物/服务名称快照, category_id BIGINT NOT NULL, wash_method VARCHAR(50) DEFAULT NULL COMMENT 洗涤方式, unit_price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL DEFAULT 1, amount DECIMAL(10,2) NOT NULL, stain_desc VARCHAR(500) DEFAULT NULL COMMENT 污渍/瑕疵描述, defect_desc VARCHAR(500) DEFAULT NULL COMMENT 破损/异常描述, KEY idx_order_id (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;3.2 容易漏掉的两个关键字段第一stain_desc和defect_desc绝对不能省。收衣时前台要在明细上记录“袖口有油渍”“下摆开线”之类的异常。这既是洗衣工的作业依据也是发生纠纷时的凭证。很多同学嫌麻烦不设计这个字段结果演示到“衣物详情”页面时无话可说只能干巴巴地放一个数量。第二service_item_id旁边要有item_name和unit_price两个快照字段。洗衣店的价目表会调价如果订单明细永远关联最新价格历史订单金额就会跟着变。把价格和服务名称冗余到订单明细里才叫真正的“订单快照”。此外会员姓名和手机号也应该冗余到订单主表。这样即使会员资料被修改历史订单上的联系信息也保持不变。4. 订单状态机设计把“洗到哪一步”管起来洗衣店系统的灵魂其实是订单状态流转。收衣之后一件衣服不是直接跳到取衣而是要经过洗护、烘干、质检等多个环节。如果没有状态机前台和洗衣工都只能靠问效率极低还容易丢件。4.1 用枚举把状态迁移规则固化下来我先定义一组状态0已收衣1洗护中2待取衣3已完成4已取消最怕的写法是每个接口里都来一段if (status 0) { ... }状态一多逻辑散得满项目都是。更稳妥的做法是用枚举管理状态与合法迁移public enum OrderStatus { RECEIVED(0, 已收衣), WASHING(1, 洗护中), READY(2, 待取衣), COMPLETED(3, 已完成), CANCELLED(4, 已取消); private final int code; private final String description; private static final MapInteger, SetInteger TRANSITIONS Map.of( RECEIVED.getCode(), Set.of(WASHING.getCode(), CANCELLED.getCode()), WASHING.getCode(), Set.of(READY.getCode()), READY.getCode(), Set.of(COMPLETED.getCode()), COMPLETED.getCode(), Set.of(), CANCELLED.getCode(), Set.of() ); public boolean canTransferTo(OrderStatus target) { return TRANSITIONS .getOrDefault(this.code, Set.of()) .contains(target.getCode()); } }在业务代码里更新状态前先调用canTransferTo不合法就直接抛异常。这样做的好处是状态规则能被集中审查新增状态时只改一个枚举不会出现“这里忘了拦截”“那里又放行了”的漏洞。4.2 下单、洗护、取衣三条链路的数据一致性收衣下单这条链路的正确顺序是生成订单号、保存订单主表、批量保存订单明细、写操作日志最后在事务里提交。订单号不要直接用数据库自增 ID建议用yyyyMMddHHmmss 4位随机数的格式再加唯一索引。原因很简单客户会通过手机尾号或者取衣码来找订单可读性强的订单号能减少很多沟通成本。洗护环节更新状态时要考虑两个问题一是权限洗衣工只能把“已收衣”改为“洗护中”不能越权修改金额和会员信息二是留痕每次状态变更都写入laundry_process_log记录旧状态、新状态、操作人和操作时间。答辩时可以展示这个日志表说明系统满足可追溯性这是很好的加分点。取衣结算环节更要注意事务。取衣时如果同时发生“更新订单状态、记录支付流水、扣减会员余额、发放积分”任何一个步骤失败都必须全部回滚。我的习惯是取衣操作放在一个Transactional方法里先校验订单状态和金额再处理支付最后发积分。这里不要图省事在 Controller 里写业务逻辑否则事务边界很容易失控。5. 会员余额、积分与充值套餐计算口径必须先定死会员模块是洗衣店管理系统从“工具”变成“平台”的分水岭。很多同学把会员做成一张只有姓名和手机号的表但真正让店长认可的价值是会员能不能储值能不能积分能不能享受套餐价。5.1 余额、积分、折扣怎么算才不会算错先明确一个计算顺序应付款 订单总额 - 优惠金额 - 积分抵扣金额然后才轮得到“余额支付”。这个顺序不能乱否则前端传一个金额过来后端无从校验。积分的设计要分“获取”和“使用”两条流水。获取通常是消费金额按比例换算比如“每消费 1 元积 1 分”或者“每消费 10 元积 1 分”。使用则是在结算时用积分抵扣现金比如“100 积分抵 1 元”。每次变动都要写入member_points_log并在会员表里维护一个total_points总数字段。不要只靠 SUM 流水来算当前积分否则会员列表加载会很慢但流水表必须留着用来对账。会员余额扣减要用“条件更新”不能先查再改。伪代码如下UPDATE member SET balance balance - #{amount} WHERE id #{memberId} AND balance #{amount};如果更新影响行数为 0说明余额不足或者会员不存在直接抛业务异常。这样能避免两个请求同时读到同一个余额、一起扣款成功的情况。这个思路在答辩时非常加分因为很多同学还在用“先 select 再 update”的写法。5.2 充值套餐和次卡怎么建模充值赠送是洗衣店最常用的营销手段。比如“充 1000 送 200”不能简单在页面上写死因为套餐会变。我会建一张recharge_package表包含pay_amount、gift_amount、valid_days等字段会员充值时记录一笔余额流水并在流水里带上套餐 ID。次卡适合“洗衣卡”“熨烫卡”这类场景核心是“次数”而不是“金额”。次卡可以直接设计成member_card表包含卡类型、总次数、剩余次数、有效期。消费时扣次数并写流水。注意不要把余额卡和次卡混在一张表里它们的扣减逻辑完全不同。营销模块的要点是“先定规则再写代码”。哪一步该发积分哪一步该扣余额退款时积分要不要退回这些口径必须在设计文档里写清楚。答辩老师最喜欢追问的就是这些边界场景。6. 统计报表和耗材库存让数据能被店长真正用起来一个只增删改查的系统演示到最后一页往往很空。加上统计报表和简单的库存管理项目完整度和业务价值立刻上了一个台阶。6.1 营业额统计订单表不是唯一的数据来源店长看营业额关心的是“实际收了多少钱”不是“订单上写了多少钱”。所以报表查询应该以payment_record为主表而不是laundry_order。因为存在未付款订单、退款订单、部分支付的情况用订单总金额直接聚合会虚高。一个常用的日营收统计 SQL 大致是这样的SELECT DATE(pay_time) AS day, SUM(paid_amount) AS revenue, COUNT(DISTINCT order_id) AS paid_order_count FROM payment_record WHERE pay_status 1 AND pay_time #{startDate} AND pay_time DATE_ADD(#{endDate}, INTERVAL 1 DAY) GROUP BY DATE(pay_time) ORDER BY day;另外再做一个“衣物分类洗涤统计”用来分析店里哪些服务最受欢迎SELECT c.category_name, oi.item_name, COUNT(*) AS item_count FROM laundry_order_item oi JOIN laundry_order o ON oi.order_id o.id LEFT JOIN clothes_category c ON oi.category_id c.id WHERE o.create_time BETWEEN #{startDate} AND #{endDate} GROUP BY c.category_name, oi.item_name ORDER BY item_count DESC;这两个查询写完首页的报表模块基本就立住了。要注意给payment_record.pay_time、laundry_order.create_time加上索引否则演示数据一多查询会肉眼可见地变慢。6.2 耗材库存扣减时机要符合真实作业习惯洗衣店需要管理洗衣液、消毒液、包装袋、衣架等耗材。但和超市不同洗衣店耗材不是每一单都能精确扣减因为洗一筐衣服用了多少毫升很难计量。我建议把库存拆成“采购入库”和“盘点调整”两个动作。日常订单不需要一单一扣每天下班后由店长按实际消耗做一次“耗材出库”登记或者每周做一次盘点用“期初 入库 - 实盘 消耗”倒挤数据。这样更符合门店的作业习惯也避免了“系统库存量看似精确实际没人维护”的尴尬。如果只是为了毕业设计展示可以做一张通用的inventory_record流水表字段包含change_type、quantity_before、quantity_after、biz_type、biz_no。这样采购、领用、盘点调整都能统一记录前端也能展示完整的库存变动历史。7. 毕业设计答辩中最容易翻车的几个追问很多项目做得不错但答辩时因为“只做了功能没想清原理”而丢分。下面这些问题如果被问到建议用我给的思路回答。追问角度参考回答思路为什么用 SpringBoot因为 SpringBoot 提供自动配置和起步依赖内置容器能快速构建独立运行的业务系统相比传统 SSH/SSM减少了大量 XML 配置更适合中小型管理系统的快速交付。订单状态为什么不用数字判断要用枚举枚举能把状态码和状态描述绑定还能把合法迁移规则集中管理避免状态逻辑散落在各个 Service 里。数据库为什么要拆订单主表和明细表因为一笔订单包含多件衣物明细表记录每件衣物的服务项目和价格快照主表记录整单状态和总体金额符合“一对多”的业务模型。会员扣款时怎么防止并发超扣使用条件更新UPDATE ... SET balance balance - amount WHERE balance amount通过数据库行锁保证同时只有一个请求能成功扣减。前端传金额后端直接入库行不行不行。金额应该由后端根据价目表和折扣规则重新计算前端传参只能作为订单标识或业务备注否则会出现篡改金额的安全漏洞。如果将来开连锁店这个系统怎么扩展先保持单体架构把模块边界做清晰需要时再按订单、会员、库存等业务域拆分服务把高频接口用 Redis 缓存再用消息队列削峰。回答这些问题时尽量不要背概念。拿自己项目里的一行代码、一个字段、一张表举例子说服力会强很多。8. 做完这套系统后我特别想提醒你的几件事最后分享几个我做类似项目时踩过坑之后养成的习惯不一定写在教科书里但很管用。第一订单号别用数据库自增 ID。自增 ID 虽然简单但会暴露订单量而且不便于口头报号。我习惯用yyyyMMddHHmmss 4位随机数生成之后立刻查重因为极端情况下的随机数冲突是可能发生的。加唯一索引是为了兜底而不是为了触发异常。第二凡是涉及钱和积分的地方后端一定要有一份“计算口径”的注释。我见过很多项目昨天说“积分按实付金额算”今天改成“积分按订单总额算”代码改来改去最后对不上账。把口径写成注释或文档是对自己也是对后来维护者负责。第三做毕业设计不要等到最后才补项目文档。数据库设计说明、核心接口说明、状态流转说明最好在写完对应模块当天就记下来。这样做有两个好处一是答辩前不用熬夜“编造”文档二是写文档的过程中会很自然地发现代码里的逻辑漏洞这种提前发现问题的机会非常宝贵。洗衣店管理系统看起来平平无奇但真正把它做明白需要兼顾业务流程、数据一致性、权限边界和经营分析。把这些点想清楚你的项目就不是一个普通的课设而是一个能站得住脚的“干洗服务智能运营平台”。
