1. 项目概述与核心需求拆解
1.1 这个毕业设计到底在做什么
洗衣店管理系统,乍一听是个不起眼的管理系统,但真做起来你会发现它麻雀虽小五脏俱全。绝大部分同学做毕业设计时喜欢选图书管理、超市管理这类烂大街的题目,而洗衣店这个场景天然自带一套完整业务链:顾客送衣、收衣登记、洗衣分类、进度跟踪、取衣结算、会员储值、逾期提醒、营收统计。这些环节串起来之后,恰好能把 SpringBoot 后端、Vue 前端、MySQL 数据库、权限控制、状态机设计这些核心技术栈全部覆盖到,而且业务逻辑比图书馆借书复杂得多,答辩时有东西可讲,不会三分钟被问穿。
从标题里可以看到三个关键词:SpringBoot、Vue、一体化服务管理。拆开来说,这个项目的核心定位是:给中小型洗衣门店做一套前后端分离的管理系统,解决传统手工台账记单容易漏、洗衣进度全靠电话问、会员储值账目混乱这些实际痛点。系统至少包含两个端,一个是给店里员工用的 PC 管理端,另一个是可选的小程序或 H5 用户端,用户端用来在线下单、查进度、查余额。对于毕业设计来说,如果你只有一个人,我建议你优先把管理端做扎实,用户端做一个极简版就够了,后面我会详细说怎么分配工作量。
1.2 为什么是 SpringBoot + Vue 这套组合
选型这件事要在开题时就讲清楚逻辑,因为答辩老师第一个问题基本就是"你为什么选这个架构"。SpringBoot 的核心价值在于简化配置、内嵌容器、生态成熟,配合 MyBatis Plus 操作数据库能省掉大量重复的 CRUD 代码。Vue 负责前端交互,配合 Element UI 组件库,能在很短时间内搭建出像样的管理界面。前后端分离是当前企业级项目最主流的分工方式,用这套技术栈做毕业设计,既能体现你的工程化能力,也贴近实际招聘需求。
还有一个隐性好处是调试便利。SpringBoot 自带 Tomcat,跑起来就是java -jar一个命令,Vue 开发环境有热更新,前端改一行保存就生效,这种开发体验对赶工期的毕业生来说太重要了。我见过不少用 JSP + Servlet 做类似系统的,光改一次样式重启一次服务就够折磨人的。
提示:选型时不要只写"SpringBoot 很流行"这种废话,要写清楚具体解决什么问题。比如用 Redis 做验证码缓存和 Token 管理是为了解决无状态认证问题,用 MyBatis Plus 的乐观锁解决洗衣订单并发修改问题,每一条都要能和实际问题对上号。
在正式动工之前,还有一件事必须做,那就是把需求边界划清楚。很多同学一上来就画表结构,结果做到一半发现业务逻辑根本绕不通,只能推倒重来。我建议先用一天时间把整个洗衣流程从头到尾走一遍,列出所有角色、所有状态、所有动作,再开始建表。
2. 系统整体设计与模块划分
2.1 角色权限设计的思路
洗衣店的日常运营通常有四类角色:店长/管理员、收银员、洗衣工、顾客。设计权限模型时,我采用的是 RBAC(基于角色的访问控制)模型,这也是实际项目中最常用、最稳妥的方案。管理员拥有全部权限,包括员工账号管理、服务项目定价、数据报表查看;收银员负责收衣登记、取衣结算、会员办卡充值;洗衣工只能看到自己的待洗任务列表,进行状态流转;顾客通过用户端查看自己的订单和余额。
具体到 SpringBoot 实现层,我整合了 Spring Security + JWT。登录成功之后后端返回一个 Token,前端存在 localStorage 里,每次请求在拦截器中带上Authorization头。后端用一个拦截器校验 Token 有效性,再从 Token 中解析出用户角色,配合方法级注解@PreAuthorize做细粒度控制。这里有一个答辩时可以重点展开的细节:为什么用 JWT 而不是 Session?因为前后端分离架构下后端不维护 Session 状态,JWT 天然无状态,适合横向扩展。
2.2 核心业务模块与数据表设计
我设计的核心业务模块有六个:员工与权限管理、会员管理、洗衣服务项目管理、收衣与订单管理、洗衣进度跟踪、统计报表。整个数据库一共 13 张表,这里挑最关键的几张展开说。
订单主表是整个系统的核心。字段设计时有一个地方特别容易踩坑:金额字段一定要用 decimal 而不是 float/double。洗衣店涉及储值、折扣、零头计算,浮点数会出现 0.1 + 0.2 不等于 0.3 的问题,这在财务场景里是不能容忍的。所有涉及钱的地方全部用decimal(10,2),在 Java 实体类中用BigDecimal对应。除了金额,还有一个值得注意的设计点是订单状态字段,我没有直接用 int 存 0/1/2,而是用 varchar 存业务可读的状态枚举值,比如PENDING、WASHING、DRYING、FINISHED、PICKED_UP、OVERDUE,这样代码里一眼就能看出业务含义,排查问题的时候不用翻字典表。
衣物明细表也很关键。一张订单可能包含多件衣物,每件衣物有品类(上衣、裤子、外套、床单、窗帘)、洗涤方式(水洗、干洗、熨烫)、有无特殊污渍。我把这些拆成订单明细表,主表只存订单级信息,这样查统计报表时非常灵活,比如"这个月洗了多少件羽绒服"这类需求,一条 SQL 就出来了。
会员表里我加了一个冗余字段total_spent累计消费金额,每次订单完成结算时累加更新。为什么不实时去订单表sum?因为统计报表页要频繁展示会员等级和累计消费,实时聚合在数据量大时会拖慢查询速度,这种冗余设计在实际开发中很常见,答辩时主动提出来会加分。
2.3 模块之间的数据流关系
理解了表结构之后,模块间的数据流动就很清晰了。顾客到店送衣,收银员在收衣页面创建订单,选择会员(非会员可以用手机号快速建档),逐件录入衣物信息,系统根据洗衣服务项目价格表自动算出预估金额。订单进入PENDING状态。洗衣工在"待洗任务"列表看到新订单,开始洗涤后状态改为WASHING,洗完烘干改为DRYING,质检挂到取衣架上改为FINISHED。顾客来取衣时收银员发起结算,核销会员储值或现金支付,状态改为PICKED_UP,订单完成。如果衣物取衣时间超过 N 天,系统自动打OVERDUE标记,在待取列表里置顶提醒。
这条数据流贯穿了整个系统,是所有页面和接口设计的主线。前期把它想明白,后面写代码基本就是体力活。
3. 核心流程与技术实现细节
3.1 洗衣订单状态机:核心中的核心
订单状态是整个系统里最容易写乱的地方。很多新手会把状态流转写在各种零散的 if-else 里,改一个地方牵连三个接口。我的做法是引入状态机模式,用一张配置表把"当前状态 + 触发动作 = 下一状态"定死,然后在代码里用一个统一的服务类来做状态流转校验。
核心逻辑是这样的:
当前状态:PENDING(待洗涤) 触发动作:start_wash(开始洗涤) 下一状态:WASHING 需要权限:洗衣工 额外操作:记录开始时间 当前状态:WASHING(洗涤中) 触发动作:finish_wash(完成洗涤) 下一状态:DRYING 需要权限:洗衣工 当前状态:DRYING(烘干/晾干中) 触发动作:finish_dry(完成烘干) 下一状态:FINISHED 需要权限:洗衣工 额外操作:计算预计可取时间 当前状态:FINISHED(已洗好待取) 触发动作:pick_up(顾客取衣) 下一状态:PICKED_UP 需要权限:收银员 额外操作:生成结算记录,更新会员累计消费在后端里我用一个 Map 来维护转移表,然后在状态变更的 Service 方法里统一做三次校验:动作是否合法、角色是否匹配、当前状态是否与数据库一致。第三点在并发场景下很重要,我用 MyBatis Plus 的乐观锁注解给订单表的version字段加锁,防止两个员工同时操作同一单。
代码层面提供一个类似于下面的状态转移核心方法:
public boolean transition(OrderStatusAction action, Order order, User operator) { StateTransition transition = transitionTable.get(order.getStatus()).get(action); if (transition == null) { throw new BizException("当前状态不允许执行该操作"); } if (!transition.getAllowedRoles().contains(operator.getRole())) { throw new BizException("当前角色没有该操作的权限"); } // 乐观锁校验 if (order.getVersion() != transition.getExpectedVersion()) { throw new BizException("订单已被其他人操作,请刷新后再试"); } // 执行状态变更以及额外业务动作 order.setStatus(transition.getTargetStatus()); order.setVersion(order.getVersion() + 1); executeExtraAction(transition, order, operator); return orderMapper.updateById(order) > 0; }这套设计的好处是,后续如果要加"返洗"操作,只需要往配置表里加一行WASHING -> PENDING,不会改动任何业务代码。答辩的时候把这个讲清楚,面试官和老师都会觉得你有工程思维,而不是在背 CRUD。
3.2 计费规则与金额精度处理
洗衣店计费有几个容易忽略的点:会员折扣、满减优惠、加急费、衣物单件价格。我的方案是在服务项目表里存基础价格,订单明细生成时把单价快照到明细表里。为什么不用实时关联查询?因为洗衣价格后续可能会调整,如果显示订单详情时去查最新价格,历史订单显示的价格就会变,这在财务审计上是不允许的。快照设计是行业惯例,也值得写进毕业论文里。
折扣计算我放在一个独立的PriceCalculator组件里,输入订单明细和会员等级,输出应付金额。会员等级分为普通、银卡、金卡,折扣率分别是 1.0、0.95、0.88。计算时全部用BigDecimal,先算明细合计再算折扣,最后四舍五入到分。除了金额,取衣逾期费也在这里统一算:超过免费寄存天数后每天每单收取 2 元。
有一个非常隐蔽的坑:前端展示金额时,JavaScript 的浮点数会被 IEEE 754 搞出 0.30000000000000004 这种精度问题。我的处理方式是后端所有金额字段统一以"分"为单位返回整数,前端展示时再除以 100。这样既避免了前端精度问题,传输也干净。
3.3 前端页面设计与 Vue 实现要点
管理端的页面结构我分成了四个核心区域:工作台仪表盘、订单管理、会员管理、系统设置。工作台放今日收衣量、待取衣订单数、今日营业额三个统计卡片,下面配一个 ECharts 折线图展示最近七天营收趋势。订单管理页是整个系统最复杂的页面,左侧是状态筛选标签,右侧是订单表格,点击一行弹出抽屉详情,里面可以看衣物明细和完整状态流转时间线。
Vue 实现时有一个经验值得分享:不要把所有的下拉框选项写死在组件里。洗衣项目类型、衣物品类、状态枚举这些字典数据,我封装了一个useDict组合式函数,启动时从后端一次性拉取,缓存在 Pinia 中。这样后续改价格改项目,前端不用重新发布,刷新一下就生效。这种"数据字典"意识在企业项目中非常重视,你在简历上写一句"实现了基于字典驱动的动态下拉配置",含金量比写一堆 CSS 特效高得多。
订单状态流转在前端有一个比较麻烦的交互:洗衣工点击"开始洗涤"后,下拉框只显示当前状态下可执行的动作。这个逻辑前端也要维护一份状态机表,把后端 Java 里的 transitionMap 用 JavaScript 重写一份,虽然有点重复,但为了保证交互体验和校验安全性,前后端各校验一次是必要的。
3.4 用户端(小程序/H5)的极简实现方案
如果时间充裕,用户端建议做成一个 H5 移动端页面直接用 Vue 写,省去小程序审核和打包的流程。用户端核心只有三个功能:手机号验证码登录、首页下单预约收衣、订单进度查询。
预约收衣的场景是这样:用户在家下单,选择服务项目、填写衣物数量、期望上门取件时间,后台生成一个RESERVED状态的预约单。洗衣工上门取衣后把它转为正式订单。如果是到店送衣,则直接走管理端收衣流程。
用户端登录我用的方案是验证码登录,手机号 + 图形验证码到后端换取短信验证码,再换取 JWT。这里说明一下,模拟环境下短信服务我用了一个开源组件模拟发送,控制台打印验证码即可。答辩时说清楚"生产环境下可以替换为真实短信服务商"就行,学院派老师不会较真,但你要表现出你知道真实场景怎么做。
4. 实操过程:从零搭建与关键配置
4.1 一步步搭起开发环境
先列一套我在实际项目里验证过的环境组合,照着配就行。
后端: JDK 1.8+(建议直接用 11,企业里现在用的多) Maven 3.6+ MySQL 8.0 Redis 5.0+(可选,简单项目可以用本地缓存替代) SpringBoot 2.7.x(不要追新用 3.x,有些老依赖不兼容) 前端: Node.js 16+ Vue 3 Vite 4 Element Plus ECharts 5 Pinia后端项目直接用 Spring Initializr 生成,勾选 Web、MySQL Driver、MyBatis、Validation、Security 这五个依赖就行。别用 MyBatis Generator 自动生成一堆没用的 XML,直接在实体类上加 MyBatis Plus 注解手写,代码更可控。
前端用npm create vue@latest快速初始化,然后装上 Element Plus 和路由。有一个小技巧:建议在开发环境配置一个.env.development文件,里面写上VITE_API_BASE_URL=/api,然后在 Vite 配置里设置代理到http://localhost:8080,这样开发时不会有跨域烦恼,上线时只需要把前端静态文件丢到 Nginx 里反代到后端接口就行,代码一行不用改。
4.2 数据表初始化脚本(节选)
建表是整个项目的地基,我当时的初始化脚本节选一段关键的表来说明。业务字段不要过度设计,够用就行,但基础字段一个都不能少。
CREATE TABLE `laundry_order` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL COMMENT '订单编号,如LO20250415001', `member_id` bigint DEFAULT NULL COMMENT '会员ID,散客为空', `customer_name` varchar(32) NOT NULL COMMENT '顾客姓名', `customer_phone` varchar(11) NOT NULL COMMENT '联系电话', `status` varchar(20) NOT NULL DEFAULT 'PENDING' COMMENT '订单状态', `total_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '应收总金额', `discount_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '优惠金额', `final_amount` decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT '实付金额', `pay_method` varchar(20) DEFAULT NULL COMMENT '支付方式: CASH/WEIXIN/BALANCE', `expect_pickup_time` datetime DEFAULT NULL COMMENT '预计可取时间', `remark` varchar(200) DEFAULT NULL COMMENT '备注,如特殊污渍说明', `version` int NOT NULL DEFAULT 0 COMMENT '乐观锁版本号', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_status` (`status`), KEY `idx_member_id` (`member_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;表设计的几个注意点:
- 订单编号不要用数据库自增 id,要用"业务前缀 + 日期 + 流水号"这种可读性强的格式,方便顾客报单号查进度。
status字段用业务枚举字符串,我在前面说过原因了。create_time和update_time设置为数据库自动维护,Java 实体里不用手动赋值。
4.3 后端接口分层与一个核心接口实现
接口设计我遵循 RESTful 风格,但不过度纠结,一共就这么几个核心资源:/api/orders、/api/members、/api/services、/api/auth、/api/stats。每个资源的标准动作就是 GET 列表、GET 详情、POST 新增、PUT 修改、DELETE 删除、以及业务特有动作如/api/orders/{id}/transition。
代码分层是标准的 Controller -> Service -> Mapper 三层,这里额外注意一个点:Controller 里不写任何业务逻辑。有的同学图省事直接在 Controller 里查库,后面要加个权限判断时就会改得很痛苦。我把所有业务动作都收拢到 Service 层,Controller 只做参数接收、调用 Service、返回统一 Result 包装。统一返回结构如下:
public class Result<T> { private Integer code; // 0 成功,非0失败 private String message; private T data; }所有前端请求都走这个包装,全局异常处理器会把 BizException 的 message 直接传给前端弹出提示。成功和失败的返回结构一致,前端 Axios 响应拦截器里统一判断 code 是否为 0,这样能省掉每个页面大量的 try-catch 代码。
拿"收衣登记"这个接口举例。一个收衣操作涉及三张表:创建订单主表、批量插入订单明细、扣减会员余额(如果选用余额支付)。这三个操作必须在一个数据库事务里完成,否则会出现"订单创建成功但明细丢失"这种脏数据。实现就是在 Service 方法上标注@Transactional(rollbackFor = Exception.class),然后在方法里依次执行三个 Mapper 操作。这里要特意说明一点:Spring 事务默认只在RuntimeException时回滚,rollbackFor指定为Exception.class可以保证捕获到所有异常类型时都回滚。
4.4 前端核心页面:订单管理页的组件拆分
这个页面是工作量最大的部分。我把它拆成了四个子组件:OrderFilter负责状态筛选和搜索条件、OrderTable展示订单列表、OrderDetailDrawer展示订单详情和状态流转、TransitionButtons按当前状态渲染可执行动作按钮。父子组件之间通过 Pinia 里的订单状态 store 通信,页面本身只负责组装。
这里有一个非常实用的经验:状态流转按钮要绑定二次确认。比如"开始洗涤"是单向不可逆(至少在系统设计里需要确认)的操作,直接在按钮上套一层 ElMessageBox.confirm,防止手滑点错。洗衣工文化程度参差不齐,界面交互必须极简——按钮文字直接写"开始洗涤""完成烘干"这种大白话,不要用技术黑话。
统计图表页用 ECharts 的时候,有一个坑要注意:ECharts 在 Vue 3 里直接使用是拿不到响应式数据的。需要在onMounted里初始化实例,然后通过watch监听数据变化后手动setOption。直接写在模板里是渲染不出来的,这一点卡了我半天。另外,图表容器必须有明确的宽度和高度,不能是 0 或者自适应弹性高度,否则 ECharts 初始化时拿到的是 0,图表会白屏。
4.5 关键代码实践:完整演示一个后端 Service
选一个核心且完整的方法来展示,我挑了"取衣结算"这个动作。它是整个系统业务最集中的地方,涉及余额扣减、状态流转、累计消费更新。
@Service public class OrderService { @Transactional(rollbackFor = Exception.class) public OrderVO settlePickup(Long orderId, String payMethod) { Order order = orderMapper.selectById(orderId); // 状态校验:只有 FINISHED 才能取衣 if (!OrderStatus.FINISHED.equals(order.getStatus())) { throw new BizException("当前订单状态不允许取衣结算"); } // 金额计算(快照) BigDecimal finalAmount = order.getFinalAmount(); // 如果选择余额支付,扣减会员储值 if ("BALANCE".equals(payMethod)) { Member member = memberMapper.selectById(order.getMemberId()); if (member.getBalance().compareTo(finalAmount) < 0) { throw new BizException("会员余额不足,请选择其他支付方式"); } member.setBalance(member.getBalance().subtract(finalAmount)); member.setTotalSpent(member.getTotalSpent().add(finalAmount)); // 更新会员等级(可以根据累计消费自动升级) updateMemberLevel(member); memberMapper.updateById(member); } // 订单状态流转 order.setStatus(OrderStatus.PICKED_UP); order.setPayMethod(payMethod); order.setPickupTime(LocalDateTime.now()); orderMapper.updateById(order); // 异步记录营收统计 statsService.recordRevenue(order, finalAmount); return convertToVO(order); } }这段代码虽然不长,但浓缩了三个核心设计思路:第一,状态校验先行,保证业务流程的不可逆跳转;第二,余额扣减和状态更新放在同一事务里,避免扣钱和更新不一致;第三,统计记录用异步处理,不影响主流程响应时间。代码注释也要养成写中文的好习惯,答辩评委看的是代码之外的设计思考,不是代码本身。
5. 常见问题与排查技巧实录
5.1 跨域问题:明明后端接口能访问,前端却报 401
前后端分离项目第一个拦路虎百分之八十是跨域。我本地开发时用 Vite 代理解决了,但用 Nginx 部署到服务器后,又踩了一次坑。现象是前端能打开页面,但所有请求都报 CORS 错误,或者 OPTIONS 预检请求 404。
排查思路:先用浏览器开发者工具的 Network 面板确认请求是不是真的发出去了。如果是 CORS 错误,检查后端是否配置了允许跨域的过滤器。我用的方案是在后端写一个全局 WebMvcConfigurer,统一设置跨域配置。至于你在网上搜到的或者 SpringBoot 内置的 CORS 配置,核心就是允许来源、允许方法、允许头这三个参数不要配错。
注意:生产环境强烈不建议用后端跨域配置,正确做法是用 Nginx 做反向代理,前后端同源访问。跨域配置仅限开发阶段用。
5.2 订单并发修改:两个员工同时操作同一单
店员和店长可能同时点开一个订单,一个在点"开始洗涤",另一个在点"完成洗涤"。如果不做并发控制,后提交的更新会把前面的状态覆盖掉。我用 MyBatis Plus 乐观锁解决了这个问题。具体实现是分三步走:实体类状态字段上加上@Version注解,MyBatis Plus 配置文件里启用乐观锁拦截器,然后所有 update 操作会自动带上WHERE version = ?条件。
还有一个小技巧,如果用户看到"订单已被其他人操作"的提示,合理处理方式是自动刷新订单详情并重新渲染按钮,而不是让他手动刷新页面。
5.3 状态是散的:报表统计数据对不上
有段时间我发现统计报表的"已取衣订单数"和订单列表对不上。后来排查发现,问题出在数据初始化脚本不规范。开发时我手动改过几条测试订单的状态,直接 UPDATE SQL 跳过了正常流转,导致统计口径缺失。没有任何程序 bug,纯粹是脏数据。
这个问题的教训是:开发阶段也尽量通过页面操作完成状态流转,不要用 SQL 命令去改状态字段。如果一定要改,也通过系统提供的数据修复接口来做,同时记录一条操作日志。统计报表的所有数据来源必须有清晰的业务口径,脏数据的排查成本远比想象中高。
5.4 前端表单校验:金额输入框输负数
洗衣单金额允许手工调整,有次店员手滑输入了一个负数,结果订单总额变成负的,后续所有账单都乱套了。前端表单校验只能防君子不能防小人,后端必须再做一次参数校验。我的做法是在后端 DTO 字段上用 Validation 注解标注@DecimalMin(value = "0.00"),同时额外在 Service 里做一次业务规则校验,比如"实付金额不能大于应收金额加逾期费"。记住一个原则:前端校验是用户体验,后端校验才是安全底线。
5.5 前后端联调时常见的时间格式问题
SpringBoot 返回的时间默认是 ISO 8601 格式,但 Element Plus 的日期组件需要的是yyyy-MM-dd HH:mm:ss。解决方式是在实体类的时间字段上用@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")注解统一格式化,包括LocalDateTime也一样。如果你用的是 Java 8 时间类型,记得给 Jackson 注册JavaTimeModule,否则直接序列化会报错。
6. 实操心得与个人经验总结
这个项目做下来,我自己最有价值的体会是:一个项目能不能立住,靠的不是炫技,而是业务逻辑闭环。洗衣店管理系统没有高并发、没有大数据、没有机器学习,但它有了完整的状态机设计、权限控制、事务管理、异常处理、接口规范,该有的工程实践一个不少。毕业设计的评分标准和企业里的项目标准其实是两回事,老师看的是你有没有完整地做出来、逻辑能不能讲通、代码是不是自己写的,而不是你用了多新的技术。
最后分享三个细节补充:
第一,论文里的用例图、E-R 图、时序图,做的时候一定要自己画一遍,答辩老师会随机挑一个图问你细节,你说不上来就会很尴尬。尤其是状态流转图,直接照着你代码里的状态机表来画,能讲得很透。
第二,答辩演示时提前准备好测试数据。业务系统最怕演示的时候"手边没有数据",临时创建会很浪费时间。建议提前插入一单全流程的模拟订单,从收衣到取衣的每一步状态都有记录,演示时直接一个动作一个动作地走,每一步都有页面反馈,效果比什么都强。
第三,以后有精力可以继续扩展的方向,比如接入真实支付接口、生成每日营收 Excel 报表、衣物 RFID 标签追踪。这些不用在答辩里讲,但在简历的"项目展望"里写上有这层思考,面试官会觉得你是在真正做系统的,而不是在交作业。毕竟洗衣店管理系统的核心是"管理",管理做明白了,换到任何一个小型行业的进销存项目,思路都是通用的。