资讯详情

资讯详情

Spring Boot+MyBatis打造洗衣店订单管理系统:从建模到部署全解析

1. 洗衣店订单管理的需求真相为什么不能只是记个账先讲个我自己的经历。有一次去小区楼下的洗衣店取羽绒服老板翻了一分钟本子才找到我的单子旁边还有三个顾客在排队等着查衣服洗到哪一步了。那一刻我就想这家店每天的订单量少说几十单每单又是少则一两件、多则七八件衣物靠Excel表格甚至纸质本子管理早晚要出乱子。后来帮朋友做毕设选题时他提出想做一个基于spring boot的洗衣店订单管理系统我一听就觉得这个选题特别实在——需求清晰、业务闭环完整、技术栈又主流做出来之后是真的能拿去给洗衣店用的系统而不是那种写完就躺代码仓库里的作业。这类系统的核心价值不是把纸质记录电子化这么简单。洗衣店的日常运营有五个老大难问题一是订单碎片化。顾客不是一次只洗一件而是把一周攒的外套、衬衫、窗帘一起送来每件衣物的洗涤方式和价格还都不一样。如果用一单一价的模型账根本算不清。二是状态追踪靠吼。衣服进入洗涤流程后顾客最常问的一句就是我的衣服好了没。没有系统的话店员只能靠记忆和翻本子高峰期一问三不知体验很差。三是取衣环节容易扯皮。取衣服时要核对件数、核对洗涤方式有没有做错、核实价格有没有多算。纸质单丢了、字迹看不清、价格记错了都是纠纷来源。四是营收统计滞后。老板想看看这个月干洗赚了多少、水洗赚了多少、哪个品类最赚钱翻本子翻到半夜也算不出来。五是会员管理靠感觉。谁充值了多少钱、余额还剩多少、是不是老客户全在老板脑子里换个人就看不懂了。所以从需求层面说基于spring boot的洗衣店订单管理系统实质上是围绕订单生命周期构建的一套小型业务中台覆盖开单、明细管理、状态流转、结算取衣、会员储值和经营统计。技术难度不高但对业务建模能力、数据表设计和接口设计的要求一点不低。本文适合正在做类似Java Web课程设计或毕业设计的同学也适合想给小型商户做信息化项目但又不想一上来就上微服务那套重武器的开发者。我会按需求分析→技术选型→表结构设计→核心接口实现→典型踩坑→部署实践这条线展开全程用我实际开发过程中采用的方案和踩过的坑说话。2. 技术选型复盘Spring Boot MyBatis这套组合好在哪2.1 为什么是Spring Boot而不是传统SSH或纯Servlet很多人做管理系统时有个误区觉得Spring Boot就是Spring MVC换了个壳。实际差别非常大。Spring Boot最核心的价值是自动配置和约定优于配置。洗衣店订单管理系统这种业务系统80%的代码都是CRUD 业务状态判断如果把这些时间浪费在配置XML、配数据源、配事务管理器上那项目的核心业务反而没精力打磨了。Spring Boot帮我们省掉的工作量具体到这个小项目上感受特别明显内嵌Tomcat一个java -jar命令就能起服务不需要在本地装Tomcat再手动部署war包。spring-boot-starter-web、spring-boot-starter-jdbc、spring-boot-starter-validation这些起步依赖引入一条依赖就等于配好了一整套和Spring MVC、JDBC、参数校验相关的默认环境。自动配置的数据源和MyBatis集成只需要在application.yml里写数据库连接信息其余交给框架处理。我用的是Spring Boot 2.7.x版本配合JDK 8。为什么不用最新的Spring Boot 3.x这里有一个很现实的考虑3.x基于JDK 17如果你电脑上装的是JDK 8那连编译都过不了而且部分第三方MyBatis增强插件对3.x的适配还不太成熟。课程设计和中小型项目稳定压倒一切2.7.x是经历过大量生产验证的版本。2.2 MyBatis和JPA之争我为什么选MyBatis选持久层框架时很多人会在Spring Data JPA和MyBatis之间纠结。我做这个项目选MyBatis理由非常直接洗衣店订单管理系统的查询场景有大量多表关联和统计类SQLMyBatis手写SQL的场景掌控力更强。举个例子前端要展示取衣页面时需要一次性查出来订单主信息、该订单下所有衣物明细、每件衣物的洗涤方式名称和价格、顾客姓名和会员余额。这个查询如果设计成一张大宽表用JPA的实体关系映射会很别扭用MyBatis写一个多表LEFT JOIN的查询把结果映射到自定义DTO思路清晰、SQL性能也肉眼可见地可控。再看经营统计这个功能要按日、按洗涤类型、按会员/散客分组聚合营业额这种动态SQL的拼装场景MyBatis的if标签和where标签简直就是为它而生的。用JPA去做这种动态条件查询要么退回去写Specification要么直接上原生SQL——绕了一圈还是回到SQL本身。另外一个很实际的原因是MyBatis的排查成本低。SQL写得再复杂报错了你直接拿日志里的SQL语句丢到Navicat里执行问题出在哪一眼就能看到。JPA生成的SQL一旦和你预期不一致定位问题反而多一道干扰。2.3 前端和后端的协作方式这个小项目我没上前后端分离用的是Thymeleaf模板引擎 Bootstrap。原因不复杂洗衣店订单管理系统的使用者是店员和老板并发量极小交互复杂度也就是表单提交、列表刷新、状态按钮切换。用Thymeleaf直接在服务端渲染页面一个Spring Boot MyBatis Thymeleaf MySQL的单体应用就全覆盖了学习的知识密度更高维护也更省事。如果你是冲着技术简历去的也可以在这个项目基础上把前端拆成Vue后端提供纯JSON接口。我的建议是先把这个单体版本跑通再考虑拆一上来就前后端分离事务控制、页面跳转、状态管理这些概念混在一起反而容易学成一锅粥。下表是我最终选型的结果后续所有实现都围绕这个组合展开。组件选型说明开发框架Spring Boot 2.7.x自动配置完善社区资料多持久层MyBatis手写SQL灵活适合统计报表数据库MySQL 8.0开源免费教学和实际使用都主流模板引擎Thymeleaf服务端渲染搭配Bootstrap布局前端样式Bootstrap 5栅格布局组件库快速搭建后台界面构建工具Maven依赖管理和打包最顺手开发工具VSCode Spring Boot插件轻量启动调试方便3. 核心模型设计订单表、状态机与洗衣单价的联动逻辑3.1 一单多衣订单主表和明细表为什么要拆开这是整个系统最关键的建模决策。洗衣店的业务形态很明确一个顾客送一次衣服产生一张订单一张订单里可能有外套、衬衫、裤子、被套等多件衣物每件衣物的洗涤方式不同价格也不同。如果把所有信息塞在一张表里会出现什么后果要么一行数据只能代表一件衣物导致一个订单在表里占多行没法精确表达这张单总价多少、谁取的、是否取走这种状态要么用冗余列存多件衣物字段数量不可控查询和统计都会变成灾难。所以必须拆成订单主表orders和订单明细表order_items。主表只管这一单是谁的、什么状态、应收多少钱、实收多少钱、取没取走明细表只管这一单里有哪几件衣物、分别是什么洗涤方式、单件多少钱、有无瑕疵备注。这里有一个业务关怀点必须体现在表结构里明细表中要有一个字段叫衣物现状备注。洗衣店收衣服时店员需要对衣物进行预检比如口袋有没有东西、衣服有没有破损、会不会掉色这些信息必须落到明细表的备注字段里。否则顾客取衣时发现我衣服上的纽扣之前就是松的双方说不清楚这就是纠纷隐患。我设计的订单主表核心字段如下id主键自增order_no订单编号格式如XD20250601001方便人工核对和口播customer_id关联会员表如果顾客不是会员则允许为空customer_name冗余存顾客姓名customer_phone冗余存手机号取衣时核对身份用total_quantity总件数由明细表聚合而来total_amount应收总金额status订单状态用TINYINT表示状态机编码remark订单整体备注create_time、update_time创建和更新时间注意customer_name和customer_phone是冗余字段。为什么不直接join会员表因为洗衣店会出现非会员顾客的订单这种情况下没有customer_id但姓名和电话必须留底否则没法取衣。3.2 订单状态机不要用字符串描述状态订单状态管理是这类系统最容易做烂的地方。有人图省事直接在status字段里存待洗洗好已取这种中文描述字符串后果就是前端显示依赖后端写死统计时中文值不可控一个已取走能写出一万种变体。正确做法是用整数编码 枚举类定义常量再在展示层做映射。我为订单设计了这样一个状态流0已下单待确认1清洗中2已晾干/已完工待取3已取衣4已取消这几个状态之间的流转路径不是任意的。0可以走到1或41只能走到22只能走到34是终止态。在Service层写状态变更逻辑时必须做状态合法性的校验不能出现已取衣又变成清洗中这种荒谬流转。这里要和洗衣店的实际流程对齐一点不要设计待支付和已支付两个状态。洗衣行业是先服务后付费顾客取衣时结算。如果洗完衣服顾客一直不来取订单就压在待取状态这是正常的业务滞留不代表异常。支付信息和订单状态分离支付字段单独用pay_status管理。3.3 洗衣价格表价格计算要留变数每种洗涤方式水洗、干洗、熨烫、精洗和衣物类型上衣、裤子、外套、窗帘、玩偶的组合价格都是需要维护的基础数据。我单独建了一张wash_price表字段是cloth_type衣物类型、wash_type洗涤方式、price价格。页面录入明细时选择衣物类型和洗涤方式价格自动带回允许店员根据实际污渍程度手动微调。这样设计的好处是老板调整价格时只要改数据库里的记录就行不用改代码。我还预留了一个discount_amount字段用于会员折扣、节庆优惠这种整单优惠场景。计算订单总价时逻辑是把所有明细单价累加减去discount_amount得到total_amount。应收金额在开单时就要算出来并沉淀到主表不能等取衣时再临场按明细重算否则一旦中间有人改了明细但忘了改主表金额就对不上了。4. 关键功能落地开单、状态流转、取衣结算的接口设计思路4.1 开单接口事务边界要划清楚开单是整个系统最高频的操作它做的事其实不少校验会员是否存在、生成订单编号、写入订单主表、逐件写入明细表、累计总件数和总金额、扣减会员余额如果是储值支付。这一串操作必须包在同一个事务里任何一个环节失败整单回滚。我用Transactional标注Service层方法默认的RuntimeException回滚策略就够了。开单的Controller接口我设计成接收一个DTO而不是直接把前端表格数据裸传给Service。DTO里包含两部分主单信息和明细列表。前端页面里店员先填写顾客信息然后动态添加一个明细表格行一行就是一件衣物选衣物类型、洗涤方式填备注表格下方自动汇总件数和总金额确认后提交。给Service层写核心代码时我用了简单的分层Override Transactional(rollbackFor Exception.class) public Long createOrder(OrderCreateDTO dto) { // 1. 生成订单编号格式 XD yyyyMMdd 当日序号 String orderNo orderNoGenerator.generate(); // 2. 组装订单主表实体status 初始为 0已下单 Order order new Order(); order.setOrderNo(orderNo); order.setCustomerId(dto.getCustomerId()); order.setCustomerName(dto.getCustomerName()); order.setCustomerPhone(dto.getCustomerPhone()); order.setStatus(OrderStatus.CREATED.getCode()); order.setRemark(dto.getRemark()); order.setCreateTime(LocalDateTime.now()); // 3. 计算总金额和总件数同时校验每件衣物价格合法 ListOrderItem items dto.getItems().stream().map(itemDTO - toItem(itemDTO)).collect(toList()); BigDecimal totalAmount items.stream() .map(OrderItem::getPrice) .reduce(BigDecimal.ZERO, BigDecimal::add); // 减去整单优惠 totalAmount totalAmount.subtract(dto.getDiscountAmount()); order.setTotalQuantity(items.size()); order.setTotalAmount(totalAmount); // 4. 先插入主表得到自增id再插入明细表 orderMapper.insert(order); for (OrderItem item : items) { item.setOrderId(order.getId()); orderItemMapper.insert(item); } return order.getId(); }这里有个细节很容易被忽略明细表的插入必须先拿到主表的自增ID。MyBatis的useGeneratedKeystrue keyPropertyid必须配好不然order.getId()返回的是null明细表外键就废了。4.2 状态流转接口一个方法搞定合法跳转状态流转接口在设计时我一开始写了好几个方法什么startWash()、finishWash()、pickUp()后来发现这纯粹给自己找事。状态流转本质上就是对status字段的受控更新用一个通用的方法内部做合法性校验反而更简洁。Override Transactional(rollbackFor Exception.class) public void changeOrderStatus(Long orderId, Integer targetStatus) { Order order orderMapper.selectById(orderId); if (order null) { throw new BusinessException(订单不存在); } // 状态机合法性校验 if (!OrderStatus.canTransfer(order.getStatus(), targetStatus)) { throw new BusinessException( String.format(订单状态无法从 %s 流转到 %s, OrderStatus.of(order.getStatus()).getDesc(), OrderStatus.of(targetStatus).getDesc())); } order.setStatus(targetStatus); order.setUpdateTime(LocalDateTime.now()); orderMapper.updateStatusById(order); }关键判断都收敛在OrderStatus枚举里枚举里用Map配置好合法的流转路径。以后想加状态、改流转规则只动这一个类不会影响其他代码。4.3 取衣结算并发场景下的金额校验取衣是最容易出事故的环节。后台页面打开一个订单时显示应收金额和会员余额。店员点击确认取衣后端要做这几步检查订单状态必须是已完工待取。重新核对明细表中的数量和金额防止有人中途改过。如果使用会员余额支付检查余额是否充足若充足则扣减余额并记录一条储值消费流水。更新订单状态为已取衣记录取衣时间。扣库存如果你做了库存管理的话简单场景下可以不考虑。第四步和第三步必须在一个事务里否则就会出现钱扣了但订单状态没更新的中间态。这种问题在测试阶段不容易暴露上量之后必现所以事务边界一定是接口设计时就要想清楚的。顺带提一个细节取衣时需要核验身份所以订单列表页要支持按手机号查询。我专门写了getOrderByPhoneAndStatus这样的查询方法店员输入手机尾号就能把待取订单筛出来效率远高于翻本子。5. 开发中绕不开的坑端口占用、MyBatis映射与事务静默失效5.1 端口号被占用Spring Boot项目启动失败的第一杀手很多人第一次启动Spring Boot项目就报Port 8080 was already in use包括我自己也遇到过。这个报错的原因很简单本机上已经有另一个进程占用了8080端口Spring Boot默认端口就是8080谁抢到算谁的。排查路径可以这样走打开命令行输入netstat -ano | findstr 8080找到占用8080端口的进程PID再打开任务管理器找到对应进程确认是无用进程就结束掉。另一个更省事的做法是修改Spring Boot的demo端口号在application.yml里加上一行server: port: 8085为什么要换成8085而不是8081、8082没有硬性规定关键是避开本机进程的常用端口范围。我实际用下来8085、9090这类端口被占用的概率远低于8080。如果你用VSCode开发Spring Boot项目可以在.vscode/launch.json里配置环境变量覆盖端口这样即使多个项目同时在跑也不会冲突{ type: java, name: Launch WashSystem, request: launch, mainClass: com.example.wash.WashApplication, env: { SERVER_PORT: 8085 } }这里有个经验端口配置不到万不得已不要硬编码在JAVA代码里。使用server.port配置项部署时可以通过启动参数--server.port8090覆盖这才是生产环境的玩法。5.2 MyBatis映射的经典陷阱下划线字段映射不上Spring Boot整合MyBatis时最经典的坑就是数据库字段是create_timeJava实体属性是createTime查询结果里createTime永远是null。原因在于MyBatis默认不做驼峰和下划线的自动映射。解决方案是在application.yml里开启驼峰映射mybatis: configuration: map-underscore-to-camel-case: true这一行加上create_time才能正确映射到createTime。但只开这个还不够。如果你写了复杂的多表JOIN查询返回结果是自定义DTO比如OrderDetailVO里面有个字段叫itemCount而SQL里给的是COUNT(*)。这个字段没有加别名MyBatis会把它映射成COUNT(*)对应的列名属性匹配肯定失败。标准做法是加别名SELECT o.id, o.order_no, COUNT(i.id) AS item_count FROM orders o LEFT JOIN order_items i ON o.id i.order_id GROUP BY o.id, o.order_no我踩过一次很深的坑是一个统计查询在Navicat里跑得好好的到代码里返回的itemCount全是null。排查了一晚上最后发现是忘了加AS item_count。这个教训很值钱别相信MyBatis自动映射别名一定要写规范。对于多对一或一对多的嵌套查询能不用association/collection就不要用。我之前试图用collection把一个订单下的明细列表一次性嵌套查出来结果触发了N1查询问题——先查主表再逐条查明细数据量一大页面就卡。后来改成先查主表再按order_id IN (...)批量查明细在Service层把数据组装成VO性能立刻上来了。5.3 事务静默失效同一个类里方法调用让Transactional形同虚设这个坑非常隐蔽。我写了一个OrderServiceImpl里面有方法createOrder它内部调用了同一个类里的deductBalance方法后者加了Transactional。测试时发现余额扣减和订单创建不在一个事务里其中一步失败另一步竟然提交成功了。原因在于Spring的Transactional是基于动态代理实现的。外部调用Service时代理对象拦截调用开启事务但如果是一个类里的方法调用另一个方法调用走的是this引用绕过了代理事务注解就被静默忽略。解决办法有三个按优先顺序排把需要事务保证的操作提到同一个方法里一个公开方法对应一个完整事务。把被调用的方法移到另一个Service类里通过注入的方式调用让代理接管。在同一个类里通过注入自身的代理对象来调用。我推荐第一种因为createOrder这个场景本身就是开单加扣款一个完整业务流程没必要拆成两个方法再组合。事务的粒度应该跟着业务边界走而不是跟着代码方法走。5.4 MySQL连接参数时区和SSL警告不能忽视Spring Boot连接MySQL时如果application.yml里连接串写法不完整控制台会刷出一条警告WARN: Establishing SSL connection without servers identity verification is not recommended同时报时区错误。这不是致命错误但不处理的话时间字段读写会差8个小时。正确的连接串写法spring: datasource: url: jdbc:mysql://localhost:3306/wash_shop?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.DriverserverTimezoneAsia/Shanghai保证数据库连接会话时区正确useSSLfalse关闭SSL警告allowPublicKeyRetrievaltrue解决MySQL 8.0在非SSL连接时可能出现的公钥检索报错。这三个参数属于不加也能跑、加了才稳定的典型组合。6. 从Demo到可上线打包部署与日常维护心得6.1 打包配置跳过测试和资源过滤开发调试时用mvn spring-boot:run就够了交付给别人演示时要打成可执行JAR。在项目根目录执行mvn clean package -DskipTests打包完成后在target目录下会生成一个wash-system-0.0.1.jar。注意Spring Boot的Maven插件必须配了spring-boot-maven-plugin否则打出来的JAR不是可执行的。我在pom.xml里还加了finalName配置让生成的包名固定为wash-system.jar部署脚本就不用改来改去了。给洗衣店老板部署时我不推荐装MySQL再单独搞一个Tomcat。直接用java -jar wash-system.jar --spring.profiles.activeprod启动配合application-prod.yml里配置生产数据库连接一台普通Windows电脑或者云服务器就能跑起来。JAR包自带内嵌Tomcat省掉传统应用服务器配置的繁琐环节。6.2 日志是排查线上问题的唯一线索系统上线后最怕的是线上环境复现不了开发环境的问题。我第一次部署到服务器时订单列表页偶发报错本地怎么测都好后来查了服务器上的日志文件才发现是服务器MySQL的sql_mode里开了ONLY_FULL_GROUP_BY导致一个统计SQL报错。本地数据库没开这个模式所以一直没暴露。所以日志配置一定要从开发期就做好。我在application.yml里配了滚动日志按天滚动、保留30天logging: file: name: logs/wash-system.log level: com.example.wash: info排查问题时先用grep关键字找到异常堆栈再顺着时间线往前追关联日志。没有日志线上问题就只能靠猜猜的效率极低。6.3 给店员和老板用的系统交互设计要反着来最后说一个很多开发者不重视的点。洗衣店订单管理系统是给店员用的不是给程序员用的所以交互设计逻辑和普通后台管理系统是反的。程序员喜欢表单越简洁越好查数据自己去数据库看店员需要的却是一个页面能完成所有操作不要让我记订单编号。我后来微调了几个交互细节效果立竿见影首页默认展示待取衣物列表按时间倒序而不是一个空空的统计仪表盘。手机号和订单号都支持模糊查询店员只需要记住顾客的姓氏加尾号就能找到单子。取衣按钮、状态切换按钮都放在列表行内减少页面跳转次数。结账时金额用大号字体显示店员和顾客都能一眼看到避免口头报数产生争议。这些改动没有增加一行复杂技术但对实际使用的口语化需求响应得很准。老板真正感动的功能也不是什么高深的统计报表而是顾客打电话问衣服好了没我在系统里一秒钟就能告诉他这个最简单的查询。根据我个人实操体会这种单体Spring Boot项目最难的地方从来不是技术选型而是能不能把业务状态流梳理清楚、把数据边界划清楚。只要订单主表、明细表、状态机这三根柱子立住了后续加会员、加报表、加打印小票都是顺水推舟的事。希望这篇记录能帮你少走几步弯路尤其是状态流转合法性校验和事务边界那两块值得多花点时间把它想透。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →