SpringBoot+Vue洗衣店管理系统开发实战:状态机与权限控制

发布时间:2026/10/12 8:49:51
SpringBoot+Vue洗衣店管理系统开发实战:状态机与权限控制
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 vuelatest快速初始化然后装上 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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;表设计的几个注意点订单编号不要用数据库自增 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 ResultT { 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 标签追踪。这些不用在答辩里讲但在简历的项目展望里写上有这层思考面试官会觉得你是在真正做系统的而不是在交作业。毕竟洗衣店管理系统的核心是管理管理做明白了换到任何一个小型行业的进销存项目思路都是通用的。