
简介这是一套面向Java初、中级开发者的洗衣店一体化管理系统源码覆盖前台衣物收取、会员卡充值与消费、财务报表统计等核心业务可帮助学习者理解Spring Boot Shiro MyBatis在真实项目中的整合方式也适合毕业设计或中小型管理系统二次开发参考。资源包共261个文件约1.57MB以179个Java源码文件为主配合23个JS、22个XML配置、6个CSS样式及Vue页面文件包含前后端完整工程结构目录划分清晰便于按模块阅读与调试。已有1218人浏览学习是兼顾业务逻辑与框架实践的小型项目样例。通过阅读源码可以掌握Druid连接池配置、Quartz定时任务、Shiro权限控制以及SLF4J日志管理等多种开发细节同时借助Swagger相关页面与样式文件快速理解接口结构为实际项目开发积累可复用的设计思路。1. 洗衣店为什么要上一套Java管理系统Java洗衣店智能管理系统源码管住了哪些钱一家正常营业的洗衣店每天要经手收衣、登记、计价、收钱、取衣这一整串动作。老板用本子记或者Excel记白天忙的时候还能凑合月底对账时就很容易出问题谁家会员卡里还剩多少钱记不清哪几单衣服逾期没人取也找不到。这套Java洗衣店智能管理系统源码解决的问题就是把“收衣-洗涤-取衣-结算”这条链路从纸面搬到Web上用订单表、会员表、收银记录表把每一笔账钉死。适合正在学Spring Boot做课设的人也适合想把门店从手写账本切换成系统管理的小店老板照着改。下面按我拿到这种源码包后的真实整理顺序来拆。2. 打开压缩包前先核实选型为什么这套系统大概率是Spring Boot MyBatis MySQL2.1 三种常见架构横向对比Swing、SSH老项目与Spring BootMyBatis各自适合谁市面上这类命名带“源码.zip”的Java管理项目正常情况下逃不出三种技术骨架。第一种是Swing/JavaFX单机版界面在本地窗口里跑数据存本地文件或单机MySQL好处是破解成本低但门店一旦有两台收银机要同步数据就很痛苦。第二种是JSPServletSSH老项目十年前课设常用现在新装JDK17以后跑Tomcat老版本非常难受基本不推荐新接手。第三种就是Spring Boot MyBatis MySQL的Web项目这也是今天要重点讲的。它前后端通常不做重分离Spring Boot直接渲染Thymeleaf页面或者放一个Vue打包后的静态目录接口走RESTful JSON数据库用InnoDB。这类系统对部署环境的要求低一台Windows小主机或云服务器都能跑而且Java技术栈招人、找人维护都容易。洗衣店管理系统的核心模块无外乎会员卡、订单、洗衣项目、收银记录用MyBatis做CRUD非常顺手比JPA那种自动帮你建表的黑匣子风格更可控。还有一点容易被忽略这类项目源码包里的SQL脚本往往是判断成色的关键。老项目可能就给你一张表而一套合格的管理系统至少得有8到10张表。拿到zip先别急着启动把pom.xml和SQL脚本翻了再决定要不要花时间跑起来。我一般会先看一眼目录结构确认没有把target和.idea这些东西打包进来。2.2 先看pom.xml再动代码四个版本号决定你能不能直接跑起来打开压缩包后先把pom.xml拖进文本编辑器不要一上来就用IDE导。重点看Spring Boot版本、Java版本、MyBatis Starter版本以及有没有引入MySQL驱动。以下是一个典型的配置片段。!-- pom.xml 中需要最先确认的四个配置 -- parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent properties java.version1.8/java.version /properties dependencies dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency /dependenciesSpring Boot 2.7.x对应的是Java 8到Java 17都兼容但如果源码里用了较新的语法你本地JDK版本太低一样编不过。MyBatis Starter的版本要跟Spring Boot版本匹配2.3.x用于Boot 2.7没问题如果你非要把项目升到Spring Boot 3.xMyBatis Starter也要换成3.0以上版本同时javax包名会改成jakarta这个改动涉及所有Controller和实体类不是改一个版本号就能了事。看这里先把java.version设成和本机一致是最省事的做法。2.3 看初始化脚本判断项目完整度表数量与演示数据真正能跑起来的源码包里一定带SQL初始化脚本常见文件名是laundry.sql、db_laundry.sql或者init.sql放在sql/、db/或docs/目录下。没有SQL脚本的项目大概率是你需要自己建库建表接手成本会高很多。我拿到脚本后的第一步是查表的数量。最少应该有这几张管理员表、会员表、会员充值记录表、洗衣项目表、订单主表、订单明细表、收银流水表。有些系统再加一张系统配置表存门店名称、小票打印抬头、营业时间之类。如果只有三张表就声称是管理系统基本只是课设水平可以理解为“能交差”但离“能用”还有距离。同时再确认脚本里有没有insert演示数据。没有初始数据的脚本你启动后登录页面都进不去因为管理员账号是空的。常见的默认账号是admin/admin123但如果脚本里没写就得先跑一段插入语句补上。-- 手动补充管理员账号密码请先用MD5或BCrypt生成后的密文 INSERT INTO sys_user (username, password, real_name, role_id, status) VALUES (admin, 密文, 管理员, 1, 1);这里补充说明一点如果源码里用的加密方式是BCrypt就别直接insert明文登录永远匹配不上。先写个临时测试类生成密文再插入不要问我是怎么知道的。2.4 环境先跑通JDK、Maven、MySQL三个东西的配置顺序很多源码跑不起来不是代码本身的问题是环境没对齐。我的建议顺序永远是先装MySQL再配JDK最后配Maven。“java环境配置”和“mysql80 zip 配置教程”这两个热词放在一起检索时能看到大量失败案例都集中在版本和初始化动作上。MySQL如果用的是zip免安装版要手动做四件事解压目录不要带中文和空格在根目录写my.ini并指定basedir和datadir用管理员命令行执行mysqld --initialize-insecure再启动mysqld服务。这一步最容易出现的坑是“服务无法启动”和“data目录权限不对”。关于密码initialize-insecure生成的root账号默认空密码启动后应该立刻用ALTER USER设置密码。JDK方面建议直接装JDK 8或者JDK 17不要装最新的JDK 21去跑老项目编译器和框架版本不兼容会报一堆“cannot find symbol”。Maven则要注意settings.xml里的本地仓库路径和镜像源这一节在第五章避坑里再展开这里先记住环境问题占了这类源码包无法运行的一半以上原因。3. 订单表怎么建、状态怎么转把“收衣-洗涤-取衣”变成数据库里看得见的东西3.1 订单主表字段设计为什么不能用“一行记录一件衣服”的思路新手设计洗衣订单时最容易犯的错是拿到一件衣服就插入一行订单记录取衣的时候一件一件点确认。这样做的结果就是订单特别碎用户来取一次衣服可能要核销五六条记录非常不现实。正确的做法是订单主表加订单明细表一单多件。订单主表管的是这个订单的总额、件数、状态、取衣码明细表管的是每件衣服对应哪个洗衣项目、单价多少、有没有特殊要求。CREATE TABLE laundry_order ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 订单ID, order_no VARCHAR(32) NOT NULL COMMENT 业务单号例如LX20250101001, member_id BIGINT NOT NULL COMMENT 会员ID非会员用影子会员, total_quantity INT NOT NULL DEFAULT 0 COMMENT 总件数, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 应收总价, discount_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 优惠金额, paid_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 实付金额, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待付款 1洗涤中 2待取衣 3已完成 4已取消, pickup_code VARCHAR(16) COMMENT 取衣码可用于前台核销, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_member (member_id), KEY idx_status (status), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT洗衣订单主表;明细表则单独存order_id和item_id以及件数和金额。这里有一个参数容易被忽略所有金额字段统一用DECIMAL(10,2)不要用float或double否则算总价时会出现0.1加0.2不等于0.3这类经典问题这也是Java面试题里经常问到的BigDecimal来源。3.2 用状态枚举管住流程状态更新的代码写在Service而不是Controller订单状态是整个系统里最需要统一口径的东西。收衣以后至少要经过待付款、洗涤中、待取衣、已完成这几个状态逾期未取的订单有的系统会加一个“逾期”状态有的则靠查询条件过滤完成时间超过7天的记录。把这个状态定义成一个枚举而不是在代码里散落数字魔数是最低成本的约束方式。public enum OrderStatus { PENDING_PAY(0, 待付款), WASHING(1, 洗涤中), READY_PICKUP(2, 待取衣), FINISHED(3, 已完成), CANCELED(4, 已取消); private final int code; private final String desc; OrderStatus(int code, String desc) { this.code code; this.desc desc; } public int getCode() { return code; } public String getDesc() { return desc; } public static OrderStatus fromCode(int code) { for (OrderStatus status : values()) { if (status.code code) { return status; } } throw new IllegalArgumentException(未知订单状态: code); } }状态流转的逻辑必须放在Service层并且要加校验。比如一个状态为“已完成”的订单不应该允许再改回“洗涤中”“已取消”的订单也不应该允许支付。我见过不少系统只在Controller里简单set一个status字段结果就是状态随便跳门店库存和财务数据对不上。正确写法是定义一个changeStatus(orderId, fromStatus, toStatus)方法里面先查旧状态再判断是否允许切换必要时加数据库行锁。3.3 会员卡、余额、积分三张表的取舍充值赠送的活动逻辑放哪层会员体系是这个系统里财务关系最复杂的部分。洗衣店常见的玩法是充值赠送比如充300送30、充500送80。如果只做一个member表挂一个balance字段那送的部分怎么记账、怎么核销完全是一笔糊涂账。我一般建议拆两张表member表和member_recharge_record表。member表存当前余额record表只存充值流水。CREATE TABLE member ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, member_no VARCHAR(20) NOT NULL COMMENT 会员卡号, name VARCHAR(30) NOT NULL, phone VARCHAR(11) NOT NULL, balance DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 当前可用余额, points INT NOT NULL DEFAULT 0 COMMENT 积分, level TINYINT NOT NULL DEFAULT 1 COMMENT 1普通 2银卡 3金卡, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT会员表; CREATE TABLE member_recharge_record ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, member_id BIGINT NOT NULL, recharge_amount DECIMAL(10,2) NOT NULL COMMENT 用户实际支付金额, gift_amount DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 赠送金额, current_balance DECIMAL(10,2) NOT NULL COMMENT 充值后余额快照, operator_id BIGINT NOT NULL COMMENT 操作员工, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_member (member_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT会员充值记录表;这里有一个值得注意的取舍余额到底要不要实时更新到member表要。因为收银台每个操作都要读余额如果不冗余这个字段每次查询都要SUM流水表数据量大了以后很慢。但同时必须保留流水表因为一旦发生纠纷只能靠流水来还原账目。充100送20这种活动不要把优惠金额直接融进balance里而是保留gift_amount字段这样对账时才能说清楚哪些是本金哪些是赠送。扣款逻辑同理扣费时不仅更新balance还要在消费流水表里写一条记录并且要在同一个事务里完成。核心SQL是这条UPDATE member SET balance balance - #{amount} WHERE id #{memberId} AND balance #{amount};这条更新语句本身就携带了余额充足校验影响行数为0说明余额不够这是处理并发扣款最简单有效的手段比先查询再判断再更新的方式安全得多。4. Controller、Service、Mapper三层代码怎么配合把订单创建和会员扣款写给你看4.1 创建订单接口参数校验、算价、落库三段代码的顺序不能反一个创建订单的接口新手写法和合格写法之间的差距不在代码数量而在顺序。正确顺序是先校验参数再计算价格最后落库。如果把算价逻辑放在Controller里或者把校验交给前端后果就是可以绕过页面直接调接口价格随便传。RestController RequestMapping(/api/order) public class OrderController { PostMapping(/create) public Result? create(RequestBody Valid OrderCreateDTO dto) { OrderCreateDTO 校验通过后交给 service 处理 return Result.success(orderService.createOrder(dto)); } }Service public class OrderServiceImpl implements OrderService { Transactional(rollbackFor Exception.class) Override public OrderVO createOrder(OrderCreateDTO dto) { // 1. 算价遍历明细累加金额再应用会员折扣 BigDecimal totalAmount calculateTotal(dto.getItems()); BigDecimal discount memberService.getDiscount(dto.getMemberId()); BigDecimal payAmount totalAmount.multiply(BigDecimal.ONE.subtract(discount)); // 2. 落库主表 明细表 LaundryOrder order new LaundryOrder(); order.setOrderNo(generateOrderNo()); order.setMemberId(dto.getMemberId()); order.setTotalAmount(totalAmount); order.setPaidAmount(payAmount); order.setStatus(OrderStatus.PENDING_PAY.getCode()); order.setPickupCode(generatePickupCode()); orderMapper.insert(order); // 3. 批量插入明细 orderItemMapper.batchInsert(order.getId(), dto.getItems()); return OrderVO.from(order); } private String generateOrderNo() { // 常见做法日期 三位随机数例如 20250101123 return LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE) String.format(%03d, ThreadLocalRandom.current().nextInt(1000)); } }这里有两个参数需要留心。第一是Valid注解需要DTO里配合NotNull、DecimalMin这些约束才有效否则它就是摆设。第二是Transactional(rollbackFor Exception.class)这个写法rollbackFor必须写明因为Spring默认只对RuntimeException回滚如果你抛出的是自定义检查异常它会直接提交事务这是很多血泪经验换来的教训。4.2 会员充值与扣款Transactional自调用失效与“扣两次”的真实场景很多人写Service时喜欢在一个类里互相调用方法比如充值时先调checkMemberExists再调updateBalance这种写法在事务上有一个经典陷阱同类内部方法通过this调用不会走Spring代理Transactional不生效。Service public class MemberService { // 反例同类的另一个方法调用 doRecharge事务不生效 public void recharge(Long memberId, BigDecimal amount) { // 这里的调用直接走 this事务注解被忽略 this.doRecharge(memberId, amount); } Transactional(rollbackFor Exception.class) public void doRecharge(Long memberId, BigDecimal amount) { // 更新余额、插入流水 memberMapper.increaseBalance(memberId, amount); rechargeRecordMapper.insert(new RechargeRecord(memberId, amount)); } }recharge方法内部调用doRechargedoRecharge上的Transactional不会生效。如果insert流水失败balance的更新已经提交出去了就是充值成功但没流水的脏账。解决方式有三种把doRecharge放到另一个Service类里或者在recharge方法上直接加Transactional或者获取代理对象来调用。实际项目中我倾向第三种用AopContext.currentProxy()因为触发票务保证方法保持原子。再说“扣两次”的问题。用户双击充值按钮前端发来两个请求后端如果没有做幂等就会产生两条充值记录。常见的幂等做法是前端生成一个requestId请求编号后端在流水表里对requestId建唯一索引插入时如果冲突就捕获异常返回“请勿重复提交”。这也解释了为什么流水表要留一个request_id字段。4.3 MyBatis动态SQL写列表与统计排序字段别直接拼进SQL订单列表是这个系统里访问最频繁的接口通常要支持按状态筛选、按手机号或单号搜索、按时间排序。MyBatis的动态SQL很适合这种场景但有一个安全边界必须守住排序字段不能直接用${}拼接前端传的值。select idpageOrderList resultTypemap SELECT o.id, o.order_no, o.status, o.total_amount, o.paid_amount, o.create_time, m.name AS member_name FROM laundry_order o LEFT JOIN member m ON o.member_id m.id where if teststatus ! null AND o.status #{status} /if if testkeyword ! null and keyword ! AND (o.order_no LIKE CONCAT(%, #{keyword}, %) OR m.phone LIKE CONCAT(%, #{keyword}, %)) /if /where ORDER BY choose when testsort amounto.total_amount DESC/when when testsort timeo.create_time DESC/when otherwiseo.id DESC/otherwise /choose LIMIT #{offset}, #{pageSize} /select三个关键点。第一order by后面用了choose分支sort参数只被用来和常量字符串比较等于把排序规则锁死在白名单里前端传什么orderBy都没用。第二LIKE搜索用CONCAT(%, #{keyword}, %)不要自己在Java代码里拼好%再传这样做既避免注入风险又让MySQL能用上索引优化。第三LIMIT参数用#{offset}而不是${}防止分页参数的SQL注入。统计接口同理比如要查今天收了多少件衣服直接复用这条SQL的where条件把select字段改成SUM(total_quantity)即可。条件逻辑散落在Java代码里是最难维护的常见做法是把where块抽成一个公共SQL片段用sql标签复用。5. 部署和联调避坑从拿到zip到接口能通的四段踩坑记录5.1 现象项目导入IDE后全是红色波浪线Maven依赖一直下载不下来原因这类zip包里一般不包含maven仓库依赖要从中央仓库拉取国内网络环境下经常卡在某个jar上下载失败另外还有可能是本地JDK版本和pom里java.version不匹配IDE报的其实是编译级别错误。解决先改Maven的settings.xml加一个阿里云镜像源本地仓库路径也一并改到非系统盘。改完在IDE里执行cleanreimport如果还下载不下来再检查是否开了公司代理。比较省时间的方法是先命令行执行mvn -v确认Maven能识别JDK版本再处理IDE导包。下面是镜像配置。mirror idaliyunmaven/id mirrorOfcentral/mirrorOf urlhttps://maven.aliyun.com/repository/public/url /mirror5.2 现象启动时报Access denied for user rootlocalhost或报Public Key Retrieval is not allowed原因MySQL 8的默认认证插件是caching_sha2_password老项目里的驱动版本和连接URL不认识这个插件还有一种是初始化MySQL时root密码没设对后端配置文件里的密码本来就是错的。解决按照mysql80 zip 配置教程里的思路重新梳理一遍。先确认MySQL服务在运行然后用命令行以root登录执行ALTER USER把密码改掉同时把认证插件改为mysql_native_password。然后在application.yml的连接串里加allowPublicKeyRetrievaltrue。下面是一段可用的连接配置。spring: datasource: url: jdbc:mysql://localhost:3306/laundry?useUnicodetruecharacterEncodingutf8useSSLfalseallowPublicKeyRetrievaltrueserverTimezoneAsia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver注意serverTimezone必须和你的服务器时区对上否则看起来能启动一到查询时间字段就报时区异常。5.3 现象前端页面能打开但所有请求都在PendingF12控制台报跨域错误原因如果前端是独立端口访问比如前端跑在8080后端跑在9090浏览器默认拦截跨域请求。很多源码里后端忘了配跨域或者只在某个Controller上加了CrossOrigin其他接口全挂。解决全局配一个CorsFilter一次性解决。把允许的路径、域名、方法都配好开发阶段可以放开所有来源上线前再收窄。Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedMethod(*); config.addAllowedHeader(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/api/**, config); return new CorsFilter(source); } }这里有个细节allowCredentials(true)时addAllowedOrigin不能再用*要改用allowedOriginPattern否则新版Spring Boot启动时就会报错。5.4 现象点击一次“确认收款”订单被创建了两条会员余额被扣了两次原因收银页面按钮没有防重复提交逻辑用户双击或者网络慢时点了一次又点了第二次后端也没有做幂等处理接口被重复调用就重复落库。解决前端按钮点击后置灰请求返回前不允许重复点击后端针对创建订单、充值、扣款这类写接口要求调用方传一个requestId并在流水表上给requestId加唯一索引。捕获到唯一索引冲突异常时不报错而是返回上一次的成功结果。这个方案比单纯用分布式锁简单得多对单机部署的小门店系统完全够用。try { rechargeRecordMapper.insert(record); } catch (DuplicateKeyException e) { // 重复请求直接返回已成功 return Result.success(请勿重复提交); }判断是否真是重复请求还需要在插入前根据requestId查一下记录存在不存在这里用唯一索引兜底是为了防止并发穿透。6. 让这套源码更像一个能落地的产品打印取衣小票的两种做法源码自带的页面通常只支持在网页上查看订单但真实门店里收衣时要给顾客一张凭据。最省事的做法是直接用浏览器的打印能力把取衣小票做成一个打印友好页面顾客凭小票上的取衣码来取衣。第二种做法是用小票打印机通过后端把内容发送到USB或网口打印机适合订单量大的门店。!-- 取衣小票模板A5纸或58mm热敏纸 -- div classticket p{{shopName}}/p p订单号{{orderNo}}/p p件数{{totalQuantity}}/p p取衣码strong{{pickupCode}}/strong/p p下单时间{{createTime}}/p /div后端在打印接口里把订单数据和取衣码塞进模板返回一个单独的HTML页面前端调用window.print()即可。这里我踩过的坑是热敏纸宽度不一普通A5模板在58mm打印机上会截断所以模板里要针对打印机的纸张宽度做CSS媒体查询。我的习惯是先把取衣码设计成6位数字或数字字母混合串生成时查重保证同一家店的取衣码唯一。取衣时前台输入取衣码后端先校验状态是否为“待取衣”校验通过才更新为“已完成”这一步能很好的防止拿错和别人冒领。这一类管理源码包能不能从“课设水平”变成“门店愿意用的工具”最关键的不是功能多不多而是异常路径考虑得够不够。我现在拿到任何源码包第一件事都是先看状态字典怎么定义、数据库脚本有没有唯一约束、事务边界标在哪里这三样过关了剩下的只是堆功能的问题。希望帮到你。本文还有配套的精品资源点击获取