简介:完整的洗衣店管理系统毕业设计论文,面向Java Web开发学习者、高校计算机相关专业学生以及需要快速上手B/S结构项目的开发者。文档基于B/S架构,采用JSP、Java与MySQL数据库进行设计,详细阐述了用户消费记录查询、衣服清洗/修补/赔偿管理、会员卡管理及营业额统计等核心功能模块,并对系统安全、可扩展性与可维护性进行了分析。资源包仅含1个docx文档,文件大小3.11MB,内容涵盖绪论、系统开发环境、功能设计、数据库设计、操作界面等完整章节,并附有中英文摘要与目录结构。文档中不仅给出了系统总体架构和数据库表结构,还对各模块的JSP页面与后台Java逻辑交互流程进行了说明,可以帮助读者快速建立开发思路,直接适配课程设计或毕业设计需求。目前已有154人学习下载,适合需要参考完整论文撰写规范与项目设计流程的读者。
1. 基于java洗衣店管理系统设计与实现,先解决“衣服到哪了”
一个普通洗衣店,一天收一百来单,每单三五件衣服。真正让店员头疼的不是洗不干净,而是顾客问“我那件羽绒服好了没”时,前台只能说“我去看看”。干洗、水洗、熨烫、质检、返洗、暂存,每换一个环节就经手一个人,漏记一次,后面全是翻找和扯皮。基于java洗衣店管理系统设计与实现要解决的,就是把“一件衣服现在在哪个状态”变成可查询的记录,再用权限、事务和报表把每个环节锁住。
这套系统适合两类人:一是做毕业设计或课程设计的Java后端学习者,能在订单、鉴权、统计模块里完整走一遍;二是想给本地小店做轻量化改造的开发者。洗衣单不是普通买卖单,它有多件衣物明细、动态状态、按件计费和会员储值,数据模型必须围绕状态流转来设计。
2. 用Spring Boot先落数据模型:订单、衣物、计费规则怎么建表
2.1 下单要同时写多张表,实体边界先分清
洗衣店业务中,最核心的实体不是“衣服”,而是“洗衣订单”。一个订单包含多件衣物,每件衣物有独立的洗护方式、价格和状态,因此需要拆成订单主表、订单明细表和状态流转记录表。常见的做法是:客户表算一方,订单表算一方,明细表记录多方衣物。状态流转单独建表,是因为洗衣工质检不过、顾客要求返洗这类事件,后续要用来算返洗率和绩效。
以下是我常用的建表结构,数据库用MySQL 8,字符集统一utf8mb4。
CREATE TABLE laundry_order ( order_id BIGINT AUTO_INCREMENT PRIMARY KEY COMMENT '订单ID', order_no VARCHAR(24) NOT NULL COMMENT '订单号,前台展示用', customer_id BIGINT NOT NULL COMMENT '客户ID', store_id BIGINT NOT NULL COMMENT '门店ID', order_status VARCHAR(20) NOT NULL DEFAULT 'CREATED' COMMENT '订单状态', total_amount DECIMAL(10,2) NOT NULL COMMENT '原价总额', discount_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '折扣金额', payable_amount DECIMAL(10,2) NOT NULL COMMENT '应付金额', created_at DATETIME NOT NULL COMMENT '创建时间', updated_at DATETIME NOT NULL COMMENT '最后更新时间', UNIQUE KEY uk_order_no (order_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='洗衣订单主表'; CREATE TABLE order_item ( item_id BIGINT AUTO_INCREMENT PRIMARY KEY, order_id BIGINT NOT NULL COMMENT '所属订单ID', item_name VARCHAR(64) NOT NULL COMMENT '衣物名称', wash_method VARCHAR(20) NOT NULL COMMENT '洗护方式', item_count INT NOT NULL DEFAULT 1 COMMENT '件数', price_snapshot DECIMAL(10,2) NOT NULL COMMENT '下单时单价', item_status VARCHAR(20) NOT NULL DEFAULT 'CREATED' COMMENT '衣物状态', KEY idx_order_item (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='衣物明细表';订单主表把整单金额、订单状态锁在一起;明细表负责单件衣物的洗护方式和状态。注意price_snapshot记录的是下单那一刻的单价,不是每次查询时去读价目表。洗衣店的价目表会随着换季、活动调整,如果明细里不保留下单价,改价之后历史账单会全部乱掉。创建时间字段用 DATETIME,不要用 TIMESTAMP 存储,避免 2038 年问题和夏令时换算干扰,Java 侧用 LocalDateTime 接收。
2.2 金额用DECIMAL,状态用字符串还是枚举
金额和折扣类字段必须用 DECIMAL(10,2),Java 实体里对应 BigDecimal,不要用 double 或 float。洗衣店的会员折扣、返洗赔付、储值抵扣经常出现 0.05 这种精度要求,浮点运算在小数位多时会出现金额对不平的情况,排查成本远高于建表时多写几个字符。字段类型可以按下面这张表定,减少后期返工。
| 字段用途 | 推荐类型 | 原因 |
|---|---|---|
| 金额、折扣 | DECIMAL(10,2) | 精度可控,避免浮点误差 |
| 订单号 | VARCHAR(24) | 需要自定义规则,比如日期加流水 |
| 状态 | VARCHAR(20) | 可读性好,排查数据方便 |
| 创建时间 | DATETIME | 避开 TIMESTAMP 的 2038 年问题 |
订单状态字段我习惯在数据库里存字符串,在 Java 代码里用枚举定义常量。不要用数字代表状态,否则三个月后你会面对一个“3 到底表示已取衣还是已结算”的问题。枚举上可以挂上中文标签:
public enum OrderStatus { CREATED("待取衣"), WASHING("洗护中"), QUALITY_CHECK("质检中"), READY("可取衣"), PICKED_UP("已取衣"), REWASH("返洗"), VOID("已作废"); private final String label; OrderStatus(String label) { this.label = label; } }状态流转时只允许从当前状态迁移到指定下一状态。这个校验如果散落在前端页面,迟早会被绕过。后面第 4 章会给出一个集中的状态机服务来管这件事。
2.3 持久层选MyBatis-Plus还是Spring Data JPA
单体项目里我一般选 MyBatis-Plus,原因有三个。第一,多表关联查询可以直接写 SQL,洗衣店的营运报表经常要 join 订单、明细、支付三张表,MyBatis-Plus 的 xml 或注解 SQL 肉眼可读。第二,分页插件对“下单记录要分页列表”这种需求开箱即用,不需要自己拼 LIMIT。第三,updateById默认只更新非空字段,做部分状态更新比较友好,不容易把其他字段覆盖掉。
如果指导老师要求用 Spring Data JPA,也可以。但要注意关联查询时的 N+1 问题:取衣列表经常要查订单明细,不要在循环里逐条查数据库。用@EntityGraph或者写一个显式 join 查询,一次取出明细列表,回到内存后按 orderId 分组,再用视图对象组装,数据库只查两次。
2.4 初始化脚本用Flyway管理
数据表不是一次建完就结束,后面加字段、加索引是常态。用 Flyway 把这些变更纳入版本管理,比每个人都手动执行 SQL 脚本靠谱。项目启动时配置spring.flyway.enabled=true,在src/main/resources/db/migration下放 V1、V2 等脚本,第一版就放上面的建表语句。升级的时候,V2 里只写 ALTER 语句,比如给 order_item 加一个tag_no标签号字段,用于店内扫码找衣。新环境从零初始化、老环境增量升级都走同一条路径,不会出现“我这能跑你那就报错”的情况。
提示:启动报 “Flyway validate failed” 时,先检查脚本 checksum 是否被手动改过。如果数据已经变更,用 repair 命令修复记录,不要直接删 flyway_schema_history 表。
新环境跑不起来的头号原因不是代码,而是本机 Java 环境变量配置、MySQL 版本和连接串里的时区参数。JDK 17 下连接 MySQL 8,url 里加上serverTimezone=Asia/Shanghai,能省掉大量时间字段错乱的问题。
3. 洗衣店管理系统的登录鉴权:三种角色怎么控制权限
3.1 前台、洗衣工、店长的操作边界
先按业务把角色理清。前台负责下单、录入衣物、收款、取衣核销;洗衣工负责接单、上报送洗、上报完成;店长负责改价、返洗、退款、关账。如果还有“加盟店老板”,不要因此把权限模型复杂化,在角色表里加一行即可。
| 角色 | 能做的操作 | 不能做的操作 |
|---|---|---|
| 前台 | 下单、改衣、结算、取衣 | 退款、批量改价、关账 |
| 洗衣工 | 接单、上报洗护完成 | 结算、退款、作废订单 |
| 店长 | 全部操作 | 无 |
权限维度不多,我倾向在代码里用 Spring Security 注解硬编码,而不是引入动态权限表。需要重点保护的接口是:退款只允许ROLE_MANAGER;返洗需要店长二次确认;已作废订单不能被再次修改。硬编码的好处是代码即文档,后端维护人员一眼能看到某个接口谁能调。
3.2 Spring Security + JWT的登录流程
前端和后端分开部署时,用 JWT 比 Session 更适合,省掉跨域 Session 同步。登录接口接收手机号和密码,校验通过后生成 token,把 userId、storeId、role 放进去。后续请求经过过滤器时从 token 里取用户信息,设置到 SecurityContext。
@Component public class JwtAuthFilter extends OncePerRequestFilter { private final JwtTokenUtil jwtTokenUtil; private final UserService userService; public JwtAuthFilter(JwtTokenUtil jwtTokenUtil, UserService userService) { this.jwtTokenUtil = jwtTokenUtil; this.userService = userService; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String header = request.getHeader("Authorization"); if (header != null && header.startsWith("Bearer ")) { String token = header.substring(7); String userId = jwtTokenUtil.getUserId(token); if (userId != null) { User user = userService.getById(userId); UsernamePasswordAuthenticationToken auth = new UsernamePasswordAuthenticationToken(user, null, user.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(auth); } } chain.doFilter(request, response); } }这段过滤器的核心逻辑是:从请求头拿到 Authorization,判断是否以Bearer开头,解析 token 取 userId,再查库得到用户和角色。UsernamePasswordAuthenticationToken的第一个参数放当前用户对象,第三个参数放角色集合,中间参数是凭证,JWT 模式下不需要再放密码。
有个容易踩的坑:header.substring(7)的前提是字符串长度一定大于 7。如果前端传了个空 token 或格式不标准的字符串,这里会直接抛异常。建议先判断header.length() > 7再截取,或者把解析逻辑包在 try-catch 里,解析失败时直接放行,让后续 Spring Security 的匿名用户逻辑接手。
3.3 登录接口和权限注解
登录接口做两件事:校验密码和签发 token。密码在数据库里存 BCrypt 哈希,不要存明文。登录成功后返回 token 和用户角色,前端把 token 存到 localStorage,每次请求带上即可。这里不推荐做“七天免登录”之类的长有效期 token,洗衣店前台共用浏览器,token 有效期控制在 12 小时以内比较稳妥。
@RestController @RequestMapping("/api/auth") public class AuthController { private final AuthService authService; public AuthController(AuthService authService) { this.authService = authService; } @PostMapping("/login") public Result<LoginVO> login(@RequestBody LoginDTO dto) { LoginVO vo = authService.login(dto.getPhone(), dto.getPassword()); return Result.ok(vo); } }Service 层校验手机号是否存在、密码是否匹配,并且限制错误次数。洗衣店前台可能连续输错几次,一锁就 5 分钟会打扰营业。更好的做法是同一个手机号一分钟内最多 5 次失败,连续失败 10 次锁 15 分钟。密码校验失败统一返回“手机号或密码错误”,不要把“用户不存在”和“密码错误”区分开,否则容易被拿来撞库。
接口保护用注解,比如退款接口:
@PostMapping("/refund") @PreAuthorize("hasRole('MANAGER')") public Result<Void> refund(@RequestBody RefundDTO dto) { return refundService.refund(dto); }Spring Security 的角色前缀默认是ROLE_,所以 User 实体里给 authority 时要写成ROLE_MANAGER,hasRole('MANAGER')才能匹配。如果数据库里存的是MANAGER,注解要改成hasAuthority('MANAGER'),或者统一加前缀。这个细节经常导致接口 403,排查时先看角色字符串。
3.4 敏感操作二次校验
退款、改价这类操作,要强制操作人再次输入登录密码。后端在 RefundDTO 里带一个confirmPassword字段,校验时从当前登录用户取出密码哈希,再做 BCrypt 匹配。前端弹窗只是交互形式,后端必须独立校验,否则接口被脚本直接调用就绕过了。洗衣店场景里,店长不在时前台代操作的情况很多,这个二次校验能挡住大部分误操作。
4. 下单、洗护、取衣、结算:核心业务链路的Java实现
4.1 下单事务:明细与订单同时写入
java 面试题里经常问的事务传播与回滚边界,在这个下单接口就是一道必答题。下单涉及订单主表、明细表、可能还有会员储值账户流水,整单必须在一个事务里完成,任何一步失败都回滚。用@Transactional时注意,默认只回滚 RuntimeException,建议显式写rollbackFor = Exception.class。
后端不能信任前端传过来的金额。正确顺序是:读出当前价目表,重新计算每件衣物金额,再累加整单金额。洗衣店的计价规则里常有“三件以上每件八折、会员再打九折”这种组合,把计价逻辑抽成独立的PriceCalculator,以后改规则只动一个类。
@Transactional(rollbackFor = Exception.class) public Long createOrder(CreateOrderDTO dto, User operator) { Member member = memberService.getById(dto.getMemberId()); List<OrderItem> items = buildOrderItems(dto.getItems(), member); BigDecimal total = items.stream() .map(OrderItem::getPriceSnapshot) .reduce(BigDecimal.ZERO, BigDecimal::add); LaundryOrder order = new LaundryOrder(); order.setOrderNo(generateOrderNo()); order.setCustomerId(member.getId()); order.setStoreId(operator.getStoreId()); order.setTotalAmount(total); order.setPayableAmount(total); order.setOrderStatus(OrderStatus.CREATED); orderMapper.insert(order); items.forEach(item -> item.setOrderId(order.getOrderId())); itemService.saveBatch(items); return order.getOrderId(); }rollbackFor = Exception.class的目的:如果下单成功后生成取衣码的步骤抛了受检异常,事务也能回滚。generateOrderNo()建议用日期 + 门店号 + 4 位流水,比如2025010210010001,方便前台按订单号口头报单,也方便按日期查流水。
4.2 洗护状态机:怎么防住“直接改成已取衣”
状态机是 java 后端完整成长路线里绕不开的点,落到洗衣店就是一张迁移白名单。订单状态包括创建、洗护中、质检中、可取衣、已取衣、返洗、作废,衣物明细状态可能比订单更细,需要区分“正在洗”“已洗完”“已质检”。订单和明细的状态同步更新,但明细状态是订单状态的前置条件。
状态变更记录单独建表:
CREATE TABLE state_history ( history_id BIGINT AUTO_INCREMENT PRIMARY KEY, entity_type VARCHAR(20) NOT NULL COMMENT 'ORDER 或 ITEM', entity_id BIGINT NOT NULL COMMENT '订单ID或明细ID', from_status VARCHAR(20) COMMENT '迁移前状态', to_status VARCHAR(20) NOT NULL COMMENT '迁移后状态', operator_id BIGINT NOT NULL COMMENT '操作人ID', remark VARCHAR(255) COMMENT '备注,返洗原因等', created_at DATETIME NOT NULL COMMENT '操作时间' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='业务状态流转记录';状态迁移规则如下:
| 当前状态 | 下一状态(白名单) | 允许角色 | 前置条件 |
|---|---|---|---|
| CREATED | WASHING / VOID | 前台、洗衣工 | 衣物已清点 |
| WASHING | QUALITY_CHECK / REWASH | 洗衣工、店长 | 质检不合格时返洗 |
| QUALITY_CHECK | READY / REWASH | 店长、质检员 | 返洗需记录原因 |
| READY | PICKED_UP | 前台 | 必须校验取衣码 |
| PICKED_UP | REWASH | 店长 | 顾客取衣后发现问题 |
驱动状态迁移的服务里,第一步查当前状态,第二步查允许的下一状态,第三步校验操作人角色,第四步写入 state_history:
public void transition(LaundryOrder order, OrderStatus target, User operator, String remark) { OrderStatus current = order.getOrderStatus(); List<OrderStatus> allowed = TRANSITIONS.get(current); if (allowed == null || !allowed.contains(target)) { throw new BusinessException("SSE-4001", "不允许的状态迁移"); } if (target == OrderStatus.PICKED_UP) { checkPickupCode(order); } order.setOrderStatus(target); orderMapper.updateById(order); StateHistory history = new StateHistory(); history.setEntityType("ORDER"); history.setEntityId(order.getOrderId()); history.setFromStatus(current.name()); history.setToStatus(target.name()); history.setOperatorId(operator.getId()); history.setRemark(remark); stateHistoryMapper.insert(history); }transition方法的四个参数中,order 是当前订单实体,target 是目标状态,operator 是当前登录用户,remark 记录返洗原因。这里有个关键约束:前端页面只能调用服务层暴露的transition接口,不能自己拼 UPDATE 语句。如果发现某个订单的状态不是通过白名单迁移过来的,说明代码路径绕过状态机,要优先排查。
4.3 取衣码和取衣校验
取衣是洗衣店最频繁的操作。很多前台图快,只报手机号就让顾客拿走,但手机号容易被他人猜到或报错。更好的方式是在订单确认时生成 4 位取衣码,取衣时顾客出示,前台输入后校验。取衣码存冗余在 order 表里,随机数生成后落库,并设置当日有效。
顾客在收银报手机号和取衣码,前台输入后校验命中,才允许点击“取衣”按钮。如果订单金额超过 500 元,可以要求双重校验:取衣码加下单时登记的顾客手机尾号。取衣码连续输入错误 5 次后锁定,需店长解锁,防止恶意试码。这个机制简单,但能显著降低“衣服被错取”的纠纷。
4.4 结算时的折扣与会员储值
结算和下单可以分离。下单时只记录应收金额,取衣时再结算。结算接口要做四件事:重新核对金额、优先扣会员储值、剩余走现金或收款码、写支付流水。
public BigDecimal calcDiscount(LaundryOrder order, Member member) { BigDecimal total = order.getTotalAmount(); BigDecimal ratio = BigDecimal.ONE; if (member != null && member.getLevel() >= 2) { ratio = ratio.multiply(new BigDecimal("0.9")); } if (order.getItemCount() >= 3) { ratio = ratio.multiply(new BigDecimal("0.8")); } BigDecimal payable = total.multiply(ratio).setScale(2, RoundingMode.HALF_UP); order.setDiscountAmount(total.subtract(payable)); order.setPayableAmount(payable); return payable; }折扣比例用字符串构造 BigDecimal,不要用0.9这种 double 常量,否则会出现0.899999这类精度问题。计算后必须setScale(2, RoundingMode.HALF_UP)指定舍入模式,不然后续相加会出现“一分钱”差异。会员等级和满减活动属于促销规则,建议抽成List<DiscountRule>按顺序应用。洗衣店常见的活动规则是“不同时叠加,取最优”,不要把多个折扣乘在一起,要先在业务侧明确规则,再写代码。
储值扣款时要先查余额,再扣款,再写流水,三步都在一个事务里,并且对账户行加悲观锁,防止两个前台同时操作同一张会员卡导致余额扣成负数。这个场景在并发量不高的洗衣店里不容易触发,但一旦发生,顾客会直接投诉。
5. 报表核对和状态机迁移记录,是系统上线后最值钱的部分
5.1 营业日报SQL别用sum(amount)糊弄账
洗衣店的营业报表要能回答两个问题:今天实收多少钱,今天该收多少钱。前者来自订单结算时间,后者只看下单时间和明细金额。如果两个口径对不上,说明有单子结算时间缺失,或者订单状态没更新。用一条 SQL 把当天的取衣结算金额按支付方式统计:
SELECT DATE_FORMAT(settle_time, '%Y-%m-%d') AS stat_date, payment_method, COUNT(*) AS order_count, SUM(payable_amount) AS settle_amount, SUM(discount_amount) AS discount_total FROM laundry_order WHERE settle_time IS NOT NULL AND settle_time >= '2025-01-01 00:00:00' AND settle_time < '2025-01-02 00:00:00' GROUP BY DATE_FORMAT(settle_time, '%Y-%m-%d'), payment_method ORDER BY payment_method;payable_amount 是实收金额,discount_total 是打折总金额。时间范围用>=和<圈住一整天,避免用 BETWEEN 造成零点边界误差。每天对账时,把这条 SQL 的实收金额和微信、支付宝后台的账单比一下,差几分钱大概率是舍入。
5.2 用定时任务生成日终汇总
每天人工跑 SQL 容易漏。用 Spring Boot 自带的定时任务,在每天凌晨生成前一天的营业汇总:
@Component public class DailyReportJob { private final ReportService reportService; @Scheduled(cron = "0 5 0 * * ?") public void generateYesterdayReport() { reportService.generateDailyReport(LocalDate.now().minusDays(1)); } }cron 表达式0 5 0 * * ?表示每天 00:05 执行,跑的是前一天的数据。生成报表时要检查 state_history 里是否有“已取衣”但 laundry_order 状态不是 PICKED_UP 的不一致记录。发现这种数据,要在报表里单独列一个“异常单数”,先找操作人记录,再决定是补操作记录还是改状态,不要强行修数据掩盖问题。
5.3 验证状态机迁移是否被绕过
上线前写一段数据校验脚本,把订单当前状态和 state_history 的末端状态做一致性比对:
SELECT o.order_no, o.order_status, h.to_status FROM laundry_order o LEFT JOIN state_history h ON h.entity_type = 'ORDER' AND h.entity_id = o.order_id WHERE h.created_at = (SELECT MAX(h2.created_at) FROM state_history h2 WHERE h2.entity_type = 'ORDER' AND h2.entity_id = o.order_id) AND o.order_status <> h.to_status;这条 SQL 找出每个订单最后一条历史记录与当前状态不一致的单子。如果查询结果有数据,说明有代码路径绕过状态机直接改了状态,或者历史记录没写全。洗衣店系统长期维护的难点就在这里:光有状态字段不够,还要有状态的历史证据,所有返洗、退款、改价都要能在 state_history 里对出来。
本文还有配套的精品资源,点击获取