ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot+Vue洗衣店订单管理系统:设计、实现与避坑指南

SpringBoot+Vue洗衣店订单管理系统:设计、实现与避坑指南 1. 这套系统到底要做什么先把业务理顺再谈技术“洗衣店订单管理系统”这个课题放在毕设和课设里属于典型的“中规中矩但足够完整”的项目。它不是电商系统那样动不动就牵扯秒杀、分布式锁、支付回调也不像纯后台管理系统那样只有增删改查。洗衣店订单的独特之处在于订单生命周期有明确的状态流——用户下单、门店接单、洗涤中、洗护完成、取件结算每一步都牵扯用户和店员两种角色的操作天然适合用来展示你对业务流程建模、权限控制和前后端交互的理解。选 SpringBoot Vue 这个组合说到底就是“市场主流 上手成本低”。SpringBoot 简化了 Spring 的配置内置 Tomcat一个 Jar 包就能跑Vue 配合 Element UI 做管理后台组件齐全列表、表单、弹窗、标签页都有现成的封装不需要从零写 HTML 和 CSS。MySQL 负责存储数据量级对这个场景完全够用。你可以把结构想象成Vue 是门店前台负责接待和展示SpringBoot 是后厨负责处理订单、计算价格、更新状态MySQL 是仓库负责把所有东西记在账上。三者通过 RESTful 接口通信前端拿到 JSON 数据渲染后端拿到参数写库。这套架构也是目前中小型企业内部系统最常用的形态你做完这一个项目实际上就把“前后端分离开发”的整套协作流程走了一遍。很多同学纠结为什么不用 SSM 或者直接用 JSP。我的建议是除非学校明确要求否则优先选 SpringBoot。SpringBoot 默认整合了 Jackson、内部校验、事务管理写起来比传统 SSM 少了大半的 XML 配置而且现在招聘市场聊的 SpringBoot 面试题也远多于 SSM你拿这个项目去面试简历上写“基于 SpringBoot Vue 的订单管理系统”比写“JSP 洗衣店网站”好听太多含金量也实在一些。再明确一下适合人群准备做毕设或课设的计算机专业学生想找一个能完整跑起来、能答辩、能往简历上写的项目自学 Java 和前端、想通过一个完整项目串联起零散知识的人以及在培训机构学了基础想找个东西练手的新手。这套系统不搞花哨的分布式不引入消息队列核心就是一套干净、能讲清楚、能实操落地的订单闭环。1.1 洗衣店缺的不是系统是把账算明白洗衣店的日常业务看起来简单——顾客送来衣服店员登记、定价、洗护、通知取件。但真正营业过的人都知道门店最怕的是“账对不上”衣服收多了不知道哪些还没洗完洗完了记不清该通知谁月底一算账发现会员余额和记录对不上。所以订单管理系统要解决的根本问题不是“用什么框架”而是把“衣服从进店到出店”这条链路完整数字化。拆开看系统至少要覆盖这几件事顾客能注册、登录、查看自己的订单历史能在线提交洗衣需求。店员或管理员能录入新订单选择服务项目水洗、干洗、熨烫、去渍等自动计算金额。订单状态必须可流转从“待接单”到“洗涤中”再到“已完成待取件”每步都要有时间记录。取件时有取件码或者订单号核验防止拿错衣服。门店需要统计营业额、订单数量、服务项目占比支撑简单经营决策。这些需求拆完之后技术选型才能对号入座。SpringBoot 负责暴露接口和业务逻辑Vue 负责做成操作界面MySQL 存放用户表、订单表、订单明细表、服务项目表等。数据库怎么建表、接口怎么设计后面我会详细拆。1.2 为什么 SpringBoot Vue 是毕设/课设最稳妥的搭配先说结论这个组合不一定是技术上限最高的但对“以毕业设计为目标”的人来说是用最少的时间换最高完成度的方案。SpringBoot 的好处是“开箱即用”。你不需要像传统 SSM 那样花一个下午把 Spring、SpringMVC、MyBatis 的 XML 配置文件理顺SpringBoot 的自动配置会帮你把大部分基础设施准备好。你只需要关注业务代码本身Controller 接收请求、Service 处理业务、Mapper 访问数据库。而且 SpringBoot 生态里 MyBatis-Plus、Druid、JWT、Lombok 这些工具都是现成的引入依赖就能用省去大量重复劳动。Vue 这边我用的是 Vue 2 Element UI Axios这是目前中文社区资料最丰富、踩坑答案最齐全的组合。Vue 2 的 API 简洁options 写法的 this 指认逻辑比 Vue 3 的 Composition API 更容易让新手理解Element UI 的表单、表格、消息提示、分页组件直接把页面组装成本降到最低。你不用设计复杂的 CSS 交互默认样式已经足够整洁稍微调一下配色就能拿去答辩。MySQL 作为数据存储部署简单、SQL 查询直观、Navicat 可视化操作对不熟悉命令行的同学非常友好。配合 MyBatis-Plus 的代码生成器连实体类和 Mapper 都能自动生成为基础代码你只需要专注写业务逻辑。这也是为什么这套组合在毕设圈子里长盛不衰——它把“平台建设”的租子降到最低让你把精力花在业务实现上而不是和环境配置纠缠。1.3 把功能清单列出来哪些能加分哪些是坑项目功能不是越多越好关键是“逻辑能闭环流程能跑通”。我建议的功能划分如下按模块组织系统登录与权限基于 JWT 或者简单 Token 实现区分管理员和普通用户页面按角色显示管理员可见后台管理菜单用户只能操作自己相关的功能。用户管理注册、登录、个人信息维护、会员余额查看管理员端可以查看用户列表、启停用账号。订单管理用户提交订单选择服务项目、填写衣物描述、预约时间管理员接单、修改订单状态、查看订单详情支持订单列表多条件筛选按状态、按时间段、按用户。服务项目管理管理员维护衣物服务项目名称、单价、预计耗时增删改查。会员卡与余额用户充值和消费时余额实时变化写订单时自动扣减或待取件时支付记录流水。数据统计管理员首页展示今日订单数、今日营业额、总会员数按日/按月统计订单量趋势用简单柱状图或折线图展示。功能上要避开两个坑第一别一上来就做“微信支付接入”毕设答辩时你解释不清支付回调、签名验签的逻辑反而给自己挖坑第二别去做“实时洗护进度追踪”这种需要物联网设备配合的功能纯属给自己找罪受。把订单状态流转和金额计算做扎实比堆砌花哨功能更能打动评委。2. 模块功能与业务流程先画好状态流转图再动手写代码我见过不少同学一上来就建表、写 Controller结果写到一半发现订单状态怎么都对不上哭着删表重建。核心问题就是没把业务流程在纸上先理顺。洗衣店订单系统最核心的不是增删改查而是订单状态怎么流转每一步谁有权限操作。2.1 订单状态流转系统里最关键的业务逻辑洗衣店的订单生命周期我把它拆成六个状态用一个整数或者字符串字段标识避免散落的魔法值状态值含义说明0待接单用户提交订单后等待门店确认1洗涤中门店接单后开始洗护记录开始时间2待取件洗护完成通知用户取件显示取件码3已完成用户确认取件订单结束生成支付记录4已取消用户未接单前取消或管理员取消5售后中取件后用户申请售后可选超时未取可流转为什么用整数而不是“待接单”这种字符串一是数据库存储和比较效率更高二是可以方便地配合前端 switch-case 或对象映射在页面上显示对应标签。更重要的是状态流转必须做“合法性校验”不能从“待接单”直接跳到“已完成”也不能从“洗涤中”跳回“待接单”。这类校验放在 Service 层做而不是靠前端按钮隐藏。具体流转规则建议这样设计用户提交订单后状态为 0管理员点击“接单”后变为 1管理员点击“完成洗护”后变为 2用户到店点击“取件”后变为 3。取件后如果用户反馈衣物问题可以进入 5。用户提交订单后、管理员未接单前用户可以取消此时为 4。每一步操作都要在数据库里更新一个“操作时间”字段便于后续统计和追溯。2.2 表结构设计五张核心表就够用数据库设计我遵循“够用、好扩展、不过度设计”的原则。项目核心五张表加上一张洗衣项目表一张余额流水表共八张左右用户表userid、用户名、密码BCrypt 加密存储、真实姓名、手机号、角色0用户/1管理员、余额、注册时间、状态。订单主表ordersid、订单编号用时间戳随机数生成、用户id、门店员工id、服务类型普通/加急、订单金额、实付金额、状态、衣物数量、备注、取件码、下单时间、接单时间、完成时间、取件时间。订单明细表order_itemid、订单id、衣物名称、服务项目id、数量、单价、小计金额用于记录多件衣物和多个服务项方便统计营业额。服务项目表service_itemid、名称、单价、预计耗时、适用范围水洗/干洗/熨烫、状态。余额流水表balance_logid、用户id、金额变动正负、变动类型充值/消费/退款、订单id可空、备注、创建时间。订单主表里冗余了“取件码”字段。这个取件码可以是 4 位随机数字下单时生成取件时用户报码给店员核验。如果你觉得存字段不够“高级”也可以取件时实时生成但存字段更简单可靠适合毕设。这里要特别注意“金额精度”问题Java 端千万不要用 BigDecimal 以外的类型算钱MySQL 字段不要用 float/double必须用 decimal(10,2)。这个坑我后面单独说。2.3 权限与角色不要做成“一竿子捅到底”最常见的毕设毛病是登录之后所有页面都能看、所有按钮都能点。答辩老师一眼看穿你没做权限控制。这个项目至少要区分“用户”和“管理员”两个角色实现方式有两种简单方式前端用路由守卫Vue Router 的 beforeEach根据 store 里的角色字段判断能不能进对应页面菜单根据角色渲染。稳妥方式后端在每个需要鉴权的接口上用拦截器或自定义注解校验登录状态和角色前端再配合隐藏按钮。我建议两个都做。后端拦截是安全底线前端守卫是用户体验。用 JWT 实现时后端生成 token携带角色信息前端每次请求在 axios 拦截器里带上 token后端解析后放进 ThreadLocal 或者直接放请求参数里Service 里判断角色。不要把权限做成 RBAC 那种角色-菜单-按钮的完整体系对洗衣店项目来说过度建设了。两个角色一个登录拦截一个角色判断就足够展示你的工程意识。3. 实操过程与核心环节实现从创建工程到跑通订单闭环下面进入真正的实操环节。我会按“脚手架搭建 → 后端核心代码 → 前端页面组装 → 联调跑通”的顺序来写都是可以直接照抄的经验。3.1 搭建工程后端和前端的基础准备工作环境版本我踩过不少坑先把最稳妥的版本组合列出来组件建议版本说明JDK1.8 或 11兼容性最好很多云服务器 JDK 8 最省事SpringBoot2.7.x不要用 3.x很多老教程和依赖不兼容且 3.x 要求 JDK17MySQL5.7 或 8.08.0 注意时区配置和 SSL 问题Node.js14.x 或 16.xVue 2 配 14/16 最稳用 18 可能 sass 报错Vue CLI4.x 或 5.x脚手架版本要配 Node建议用官方模板创建 SpringBoot 工程推荐直接用 IDEA 的 Spring Initializr勾选 Web、MyBatis、MySQL Driver、Lombok 这四个依赖即可。MyBatis-Plus 的依赖建议下载后手动加入因为 Initializr 默认不带 MP 的选项。关键 pom 依赖如下dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.2/version /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependencyapplication.yml 里最核心的是数据源配置这里直接给你一份可用的注意 8.0 的驱动类名和时区server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/laundry?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: your_password jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8 mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpluseSSLfalse 和 serverTimezoneAsia/Shanghai 是常年出现问题的两行配置MySQL 8.0 默认开 SSL本地开发经常报错时区不设置则返回的时间与本地差 8 小时。这两个坑我后面还会在“常见问题”里展开。前端工程用 Vue CLI 创建vue create laundry-web # 选择 Vue 2 预设 cd laundry-web npm install axios element-ui vue-router vuex3.2 后端核心订单模块的关键代码怎么写后端代码结构我习惯按 com.example.laundry 分包controller、service、mapper、entity、common统一返回和异常、config。写订单模块时有四个地方是真正体现功力的统一返回、异常处理、订单创建与状态流转、金额计算。先写统一返回结构前端不要直接拿 JSON 拼参数统一格式对后期联调无比重要Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT error(String message) { ResultT r new Result(); r.code 500; r.message message; return r; } }业务上最核心的是“创建订单”这个方法它决定了整个后续流程的质量。我在 Service 里大致这么写Transactional(rollbackFor Exception.class) public Order createOrder(OrderCreateDTO dto, Long userId) { // 1. 校验服务项目是否存在、是否停用 // 2. 计算金额遍历细则数量 * 单价累加 // 3. 生成订单编号和取件码 // 4. 保存主表和明细表 // 5. 返回订单详情 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setUserId(userId); order.setStatus(OrderStatusEnum.PENDING.getCode()); order.setPickupCode(generatePickupCode()); // ... 保存明细 ... // 扣除余额或标记待支付 }关键点是创建订单涉及主表和明细表两次插入必须加 Transactional防止出现“主表插入成功、明细失败”导致的无头订单。金额计算我单独抽了一个方法不用在 Controller 里算保持业务闭合。状态流转我写了一个专门的方法不对外暴露直接修改状态的入口所有操作走同一套校验逻辑public boolean changeOrderStatus(Long orderId, Integer targetStatus, Long operatorId) { Order order orderMapper.selectById(orderId); if (order null) throw new BusinessException(订单不存在); // 校验状态流转是否合法 if (!canTransit(order.getStatus(), targetStatus)) { throw new BusinessException(非法状态流转); } order.setStatus(targetStatus); // 根据目标状态更新时间字段 return orderMapper.updateById(order) 0; }canTransit 方法内部放一个 map 或者 switch 判断比如 PENDING 只能转 PROCESSING 或 CANCELLEDPROCESSING 只能转 FINISHED 或 CANCELLEDFINISHED 只能转 COMPLETED 等。这样前端再怎么乱点后端都锁得住。金额计算注意 BigDecimal 的使用。定义价目表时单价用 BigDecimal数量用 Integer乘法用 multiply。不要用 double 去乘洗完十件衣服可能就差出几分钱累计多了查账对不上很头疼BigDecimal amount BigDecimal.ZERO; for (ItemDTO item : orderItems) { BigDecimal unitPrice serviceItemMapper.selectById(item.getServiceId()).getPrice(); BigDecimal subtotal unitPrice.multiply(BigDecimal.valueOf(item.getQuantity())); amount amount.add(subtotal); }会员余额支付的话还要校验余额不足分支。这里我建议单独写一个“余额消费”方法同步更新余额和插入流水表同样用事务包住Transactional public void deductBalance(Long userId, BigDecimal amount, Long orderId) { User user userMapper.selectById(userId); if (user.getBalance().compareTo(amount) 0) { throw new BusinessException(余额不足); } user.setBalance(user.getBalance().subtract(amount)); userMapper.updateById(user); balanceLogMapper.insert(new BalanceLog(userId, amount.negate(), 消费, orderId)); }3.3 前端核心用 Vue 把页面组装出来前端我用 Vue 2 Element UI按照“登录页 → 框架布局 → 订单管理页 → 统计页”的顺序来写。登录页核心逻辑就是调用登录接口拿到 token 后存到 localStorage同时把用户信息含角色放到 Vuex 里。路由守卫做页面访问控制router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (!token to.path ! /login) { next(/login); } else if (token to.path /login) { next(/); } else { next(); } });订单列表页是整个系统里最复杂的页面也是最能看出设计能力的部分。我用 el-table 展示订单el-tag 显示状态el-select 做筛选el-pagination 做分页。需要注意的地方有三个第一后端返回的订单状态是数字页面上要翻译成文字和颜色不要直接显示 0/1/2。加一个 filter 方法function statusTag(status) { const map { 0: { text: 待接单, type: warning }, 1: { text: 洗涤中, type: primary }, 2: { text: 待取件, type: success }, 3: { text: 已完成, type: info }, 4: { text: 已取消, type: danger } }; return map[status] || { text: 未知, type: info }; }第二分页查询的参数要一次传全用对象结构比较清晰async fetchOrders(currentPage) { const params { current: currentPage, size: this.pageSize, status: this.filterStatus, keyword: this.keyword }; const res await getOrders(params); this.orders res.data.records; this.total res.data.total; }第三状态操作按钮有权限控制管理员看到“接单”“完成洗护”用户看到“取消”“确认取件”。用 v-if 判断当前用户角色再渲染别一股脑全显示出来。前端联调最烦的是跨域。开发环境下我在 Vue 里配置代理避开 CORS 的麻烦// vue.config.js module.exports { devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } };后端请求路径统一加 /api 前缀前端所有请求走相对路径比如 axios 配置 baseURL 为 /api这样生产部署时还可以用 Nginx 做反向代理逻辑统一。3.4 部署演示如何把项目跑起来给老师看答辩或者展示时最怕环境影响发挥我强烈建议你本地装好 JDK、MySQL、Node 之后按照这个顺序启动启动 MySQL建库建表导入初始化 SQL插入测试数据至少两个角色账号若干条订单。后端启动IDEA 里直接运行 LaundryApplication看到 Tomcat started on port 8080 就算成功。前端启动在 laundry-web 目录下 npm run serve浏览器访问 http://localhost:8081。我现在给的是 8081 端口因为 Vue CLI 默认如果 8080 占用会换 8081实际端口以控制台输出为准。如果不想前端每次编译慢可以用 npm run build 产出静态文件放到 Nginx 或者直接放到 SpringBoot 的 static 目录下但那会稍微复杂一点演示阶段用 dev server 就够了。4. 常见问题与排查技巧实录这些坑我都替你踩过做项目最有价值的部分不是顺利跑通了而是遇到问题能定位、能解决。下面这些问题是我亲手踩过的每一条都是真实场景。4.1 建库建表阶段的三类大坑第一坑是 MySQL 字符集。建库时如果没指定 utf8mb4默认可能是 latin1导致存储中文变成问号。建库语句直接用CREATE DATABASE laundry DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;第二坑是 Mysql 8.0 的 SSL 连接报错。报错信息里会看到 useSSL 相关的提示开发环境直接关闭即可连接字符串上带 useSSLfalse。注意 MySQL 5.7 的驱动是 com.mysql.jdbc.Driver8.0 之后改成 com.mysql.cj.jdbc.Driver别抄错。第三坑是 Navicat 连接不上报 2002 错误或者 1130 错误。1130 说明 MySQL 拒绝远程连接本地开发可以用 rootlocalhost 或者授予远程访问权限-- 创建用户并授权远程访问慎用生产环境别这么干 CREATE USER laundry% IDENTIFIED BY password; GRANT ALL PRIVILEGES ON laundry.* TO laundry%; FLUSH PRIVILEGES;4.2 前后端联调阶段的高频问题第一个高频问题是跨域。如果你不想配代理也可以在后端加一个 CORS 配置类继承 WebMvcConfigurer 重写 addCorsMappings。但我建议用代理方案因为 CORS 配置有时候对预检请求处理不当反而多出问题。第二个是 Postman 请求正常前端一调就 401。典型原因是 axios 的 header 没有带 token或者后端拦截器要求 Authorization 头而前端存的是 token 字段。统一处理办法是在 axios 拦截器里全局设置axios.interceptors.request.use(config { const token localStorage.getItem(token); if (token) { config.headers[Authorization] token; } return config; });第三个是分页数据不显示。检查后端返回结构是不是 Result 包装的如果前端拿 res.data 直接当数组用结果拿到的是 { code, message, data } 就不能直接遍历。我这里统一用 Result 包装前端取数据时就写成 res.data.data.records很多人就是这层包装弄混了。4.3 时间、金额与状态相关的隐藏雷区时间问题出现最多的就是差 8 小时。根本原因是 MySQL 连接 url 没配 serverTimezone以及 Jackson 默认按 UTC 序列化。解决方案就是我在配置里写的那两行URL 带 serverTimezoneAsia/ShanghaiJackson 设置 time-zoneGMT8。金额问题的雷区是“小数点后位对不上”。数据库字段用 decimal(10,2)Java 用 BigDecimal前端展示时用 toFixed(2)。不要用 float 或 double 存储金额不要用“分转元”的手工换算统一用元为单位、保留两位小数逻辑最简单。状态问题的隐藏雷区是“并发操作同一笔订单”。比如用户点了取件管理员同时点了完成洗护可能导致状态覆盖。我在 Service 层加了一个乐观锁机制订单表加 version 字段更新时 SQL 里带 where version ?更新成功 version1。这样两个请求同时进来只有一个会成功另一个抛异常或者提示“订单状态已更新”。这块在答辩时提出来很加分说明你考虑过并发下的数据一致性。5. 写在最后我做完这个项目的几点心得项目从零到跑通前后大概花了两周。第一周在搭环境和建表第二周在写接口和调页面。我真实的体会是这类管理系统项目难的不是代码而是“先想清楚业务再动手”。如果你把订单状态流转的每个分支都列出来把金额计算的规则定清楚把角色的权限边界画明白代码写起来就是按部就班的翻译过程。还有点小建议建表之后顺手把初始数据写好多造一些真实的测试数据——比如不同状态、不同金额、不同日期的订单。答辩演示时直接从数据库里翻数据比现场临时造数据从容得多。如果系统里能再展示一个简单的月收入曲线图讲故事的完整度立刻就上去了。最后再分享一个小技巧答辩前把系统里的测试账号密码写在项目 README 里并把后端启动方式、数据库初始化脚本也放进去。这看起来是个小细节但评委老师会真的去点开你的项目仓库一个能“按文档两分钟跑起来”的项目和一个只丢一堆源码让人自己琢磨的项目印象分完全不一样。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进