简介这是一份基于Java的洗衣店管理系统毕业设计论文采用B/S结构以JSP为核心技术、Java为开发语言、MySQL为后台数据库完整呈现了系统从需求分析到功能实现的开发过程。文档面向计算机相关专业毕业生、需要完成管理信息系统课程设计的学生以及想了解传统行业信息化改造的开发者。系统涵盖普通用户消费记录、衣服清洗、修补、赔偿等查询功能也提供管理员会员卡管理、营业额统计等模块并兼顾安全性、可扩展性与可维护性设计。资源包仅含1个docx文档大小约3.11MB共32页正文包含摘要、目录、绪论、系统开发环境、详细设计与实现、总结等部分可直接作为毕业论文写作的框架参考和代码设计蓝本。已有154人学习对于需要快速搭建选题思路、明确系统模块划分与数据库设计的读者来说是一份结构完整、落地性较强的参考资料。1. 洗衣店管理系统为什么到今天还值得拆一遍如果你的印象还停留在“JSP 是老古董新项目没人用”那这套洗衣店管理系统刚好是个反例。直到现在二线城市大量洗衣连锁店、干洗门店的进销存后台仍然是 JSP Servlet MySQL 这套 B/S 架构在跑新接手的人往往不是不会 Spring Boot而是看不懂老系统里会员卡、清洗记录、消费流水这几张表是怎么拧在一起的。这个毕业设计最大的价值不在技术新而在数据模型完整会员卡管理、衣服清洗、修补、赔偿、消费记录、营业额统计六条业务线闭环权限只分管理员和会员两种角色非常适合用来理解「一张会员卡如何驱动整店财务流水」的建模思路。本文按数据库设计、会话权限、消费闭环、部署排错的顺序拆开讲新手能照着建表写代码老手可以直接把 ER 模型和状态机设计拿去用。2. 数据库设计会员卡与消费流水如何避免数据冗余2.1 从业务反推表结构洗衣店的核心业务是「收衣 → 洗衣 →可能修补/赔偿→ 取衣 → 记账」。注意赔偿和修补不是独立事件它们是清洗订单的衍生动作所以在表设计上要把清洗订单作为主链路赔偿、修补、消费记录都围绕它展开。常见做法是设计五张核心表会员卡表member_card、清洗订单表cleaning_order、修补订单表repair_order、赔偿记录表compensation_record、消费记录表consume_record外加管理员表admin和会员表member。会员和会员卡可以合并也可以分开这里采用合并方案因为洗衣店会员通常就是一张卡对应一个人。建表 SQL 如下CREATE TABLE member_card ( id INT NOT NULL AUTO_INCREMENT, card_no VARCHAR(32) NOT NULL COMMENT 会员卡号, member_name VARCHAR(64) NOT NULL COMMENT 会员姓名, phone VARCHAR(20) DEFAULT NULL, balance DECIMAL(10,2) DEFAULT 0.00 COMMENT 卡内余额, level TINYINT DEFAULT 1 COMMENT 1普通 2银卡 3金卡, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_card_no (card_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE cleaning_order ( id INT NOT NULL AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, card_id INT NOT NULL COMMENT 关联会员卡id, clothes_type VARCHAR(32) COMMENT 衣物类型羽绒服/西装/窗帘, clothes_desc VARCHAR(255) COMMENT 衣物特征描述防止拿错, status TINYINT DEFAULT 0 COMMENT 0待清洗 1清洗中 2已完工 3已取件, amount DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT 清洗费用, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, finished_at DATETIME DEFAULT NULL COMMENT 完工时间, PRIMARY KEY (id), KEY idx_card_id (card_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这里的关键决定是把card_id作为外键贯穿所有业务表而不是把会员姓名到处复制。如果每张表都存member_name一旦会员改名或者换卡就要更新所有历史订单这就是典型的数据冗余问题。balance字段冗余在会员卡表里是因为消费记录表是流水账每次查询余额都去 SUM 流水会很慢洗衣店场景下读多写少冗余是划算的。2.2 赔偿与修补表的外键策略赔偿记录和修补订单不需要独立于清洗订单存在。比如一件羽绒服洗坏了管理员在清洗订单里标记「赔偿」同时生成一条赔偿记录赔偿金额进入消费流水。SQL 设计如下CREATE TABLE compensation_record ( id INT NOT NULL AUTO_INCREMENT, cleaning_order_id INT NOT NULL COMMENT 关联清洗订单, card_id INT NOT NULL, reason VARCHAR(255) COMMENT 赔偿原因洗坏/染色/丢失, amount DECIMAL(10,2) NOT NULL DEFAULT 0.00, status TINYINT DEFAULT 0 COMMENT 0待确认 1已赔付 2已拒绝, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_cleaning_order (cleaning_order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE consume_record ( id INT NOT NULL AUTO_INCREMENT, card_id INT NOT NULL, order_no VARCHAR(32) NOT NULL COMMENT 业务单号关联清洗或修补, biz_type TINYINT NOT NULL COMMENT 1清洗 2修补 3赔偿扣款 4充值, amount DECIMAL(10,2) NOT NULL, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_card_created (card_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段作用注意事项biz_type区分流水类型不要用字符串TINYINT 省空间且索引效率高order_no业务单号冗余用于从流水反查清洗订单方便对账idx_card_created联合索引支撑「按会员查历史消费」的高频查询字段设计上的一个坑是consume_record.amount要允许负值。赔偿扣款和退款都可能是负数如果建表时加UNSIGNED约束后面做营业额统计时会出现数据溢出。营业额统计不能直接 SUM 赔偿表的金额而是从消费流水里按biz_type过滤后聚合这样才能保证「清洗费 修补费 − 赔偿支出」的口径一致。3. 登录与权限Session 会话如何区分管理员和会员3.1 双角色登录的 Filter 拦截方案洗衣店管理系统只有两种角色没必要引入 Spring Security 或 Shiro 这种重量级框架。JSP Servlet 体系下最实用的做法是Filter Session在web.xml里配置一个全局过滤器对所有*.jsp和/api/*请求做登录态校验。关键代码public class AuthFilter implements Filter { Override public void doFilter(ServletRequest req, ServletResponse resp, FilterChain chain) throws IOException, ServletException { HttpServletRequest request (HttpServletRequest) req; HttpServletResponse response (HttpServletResponse) resp; HttpSession session request.getSession(false); Object role session null ? null : session.getAttribute(role); String uri request.getRequestURI(); // 放行登录页和静态资源 if (uri.endsWith(login.jsp) || uri.contains(/static/) || uri.endsWith(login.do)) { chain.doFilter(request, response); return; } if (role null) { response.sendRedirect(request.getContextPath() /login.jsp); return; } // 管理员页面会员角色禁止访问 if (uri.contains(/admin/) !admin.equals(role)) { response.sendError(HttpServletResponse.SC_FORBIDDEN); return; } chain.doFilter(request, response); } }这段过滤器的判定逻辑有三个要点第一getSession(false)不会主动创建 Session避免未登录用户也被分配会话 ID减少内存浪费第二角色信息存放在 Session 而不是 Cookie 里因为洗衣店场景没有「记住我」这种需求Session 失效后重新登录即可第三拦截规则用 URL 路径前缀区分权限/admin/开头的 JSP 只有admin角色能访问会员访问会收到 403。3.2 登录逻辑与密码安全登录接口的典型实现是查询 admin 表或 member 表比对通过后写入 Session。这里有个容易疏漏的点管理员和会员是两张表必须先从请求参数里判断登录类型否则会出现管理员账号被会员接口查到的逻辑漏洞。protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException { String username request.getParameter(username); String password request.getParameter(password); String type request.getParameter(type); // admin / member if (admin.equals(type)) { Admin admin adminDao.findByUsernameAndPassword(username, password); if (admin ! null) { request.getSession().setAttribute(role, admin); request.getSession().setAttribute(adminId, admin.getId()); response.sendRedirect(admin/index.jsp); return; } } else { MemberCard card memberCardDao.findByCardNoAndPhone(username, password); if (card ! null) { request.getSession().setAttribute(role, member); request.getSession().setAttribute(cardId, card.getId()); response.sendRedirect(member/myOrders.jsp); return; } } request.setAttribute(error, 账号或密码错误); request.getRequestDispatcher(login.jsp).forward(request, response); }注意这个实现里会员登录用的是「卡号 手机号」而不是「卡号 密码」这是从洗衣店实际场景出发的会员大多是中老年人记不住密码但能记住手机号。密码字段在前端用 MD5 加盐处理再传后端虽然比不上 BCrypt但对毕业设计级别的系统已经够用。Session 的失效时间要单独设置。Tomcat 默认 30 分钟空闲销毁但对洗衣店前台来说收银员可能一上午都不关浏览器建议在web.xml里把session-timeout设为 120 分钟。4. 业务闭环清洗状态机与营业额统计实现4.1 清洗订单的状态流转洗衣店业务流程不是「提交订单 → 完成」这么简单。一件衣服从收衣到取衣至少要经过四个状态待清洗、清洗中、已完工、已取件。赔偿和修补是挂在中间状态上的分支动作。状态机设计如下表状态值状态名可流转到触发操作0待清洗1清洗中、2已完工收衣登记后默认 0开始洗时置 11清洗中2已完工洗完后质检若损坏则生成赔偿记录2已完工3已取件前台通知取衣3已取件终态取衣时生成消费记录实现时建议加一个status_history表记录每次状态变更的时间否则后续统计「平均清洗耗时」时没有数据支撑。修改状态的更新语句要注意带上WHERE status ?防止并发情况下重复流转UPDATE cleaning_order SET status 2, finished_at NOW() WHERE id ? AND status 1;更新后要检查受影响行数如果为 0说明订单状态已被其他人改过需要提示「订单状态已变化请刷新页面」。这类乐观锁写法在 JSP 系统里比加行锁要实用得多因为洗衣店前台通常只有一两台电脑操作冲突概率低不需要引入复杂的锁机制。4.2 营业额统计的 SQL 写法营业额统计是管理员模块最核心的功能需求是「按日、周、月查看营业额」。由于消费流水表consume_record已经记录了所有金额变动统计逻辑就变得非常直接-- 按日统计营业额含所有业务类型 SELECT DATE_FORMAT(created_at, %Y-%m-%d) AS biz_date, SUM(CASE WHEN biz_type IN (1,2) THEN amount ELSE 0 END) AS income, SUM(CASE WHEN biz_type 3 THEN ABS(amount) ELSE 0 END) AS compensation, SUM(amount) AS net_amount FROM consume_record WHERE created_at 2025-01-01 00:00:00 AND created_at 2025-02-01 00:00:00 GROUP BY biz_date ORDER BY biz_date DESC;这个统计 SQL 有三个容易踩坑的地方一是时间范围必须用和而不是BETWEEN因为BETWEEN包含右边界会把 1 月 31 日 00:00:00 的数据也算进去导致日期串天二是聚合必须在数据库完成不要在 Java 里遍历 List 累加记录量大了之后内存会扛不住三是对created_at建索引否则按月统计时全表扫描数据量到十万条之后响应会超过三秒。DATE_FORMAT的格式字符串里%Y是四位年份%m是两位月份%d是两位日期。如果按周统计可以用YEARWEEK(created_at, 1)来分组第二个参数 1 表示周一作为一周的开始符合国内习惯。4.3 会员端「我的记录」的查询优化会员登录后要查看「我的消费记录」「我的衣服清洗」「我的衣服修补」「我的赔偿」这四个查询全部以card_id为过滤条件。最直接的写法是四个 DAO 方法分别查四张表然后展示在四个 Tab 页里。但更高效的做法是一次性查出组合数据public ListMapString, Object getMemberRecords(Integer cardId, int page, int size) { String sql SELECT cleaning AS biz_type, id AS biz_id, order_no, amount, created_at, status FROM cleaning_order WHERE card_id ? UNION ALL SELECT repair, id, order_no, amount, created_at, status FROM repair_order WHERE card_id ? ORDER BY created_at DESC LIMIT ?, ?; // 使用 JDBC PreparedStatement 执行注意分页参数 }UNION ALL 的查询方式在 JSP 课程设计里不常见但对于会员端这种「只看最近 20 条」的场景非常合适。分页参数LIMIT ?, ?里的偏移量要在 Java 端计算为(page - 1) * size不能直接拿页码往 SQL 里拼。5. 部署实战Tomcat 8 MySQL 5.7 的版本组合与常见坑5.1 JDK 与容器的版本对应关系这套系统是基于 JSP 技术的传统项目不推荐一上来就上 Tomcat 10。Tomcat 10 把javax.servlet换成了jakarta.servlet老项目的import javax.servlet.*会直接编译报错。最稳妥的组合是 JDK 1.8 Tomcat 8.5 MySQL 5.7这也是当年这种毕业设计最常见的运行环境。部署步骤# 1. 编译打包生成 laundry.war mvn clean package -DskipTests # 2. 部署到 Tomcat webapps 目录 cp target/laundry.war /opt/tomcat/webapps/ # 3. 初始化数据库首次部署执行 mysql -uroot -p sql/init.sql # 4. 启动 Tomcat /opt/tomcat/bin/startup.sh如果项目里没有 Maven 的pom.xml只有原生 JSP 和 Servlet也可以直接把整个项目目录拷到webapps/下面Tomcat 启动时会自动编译 JSP。这种情况下要注意WEB-INF/lib里必须有mysql-connector-java的 JAR 包否则运行时报ClassNotFoundException: com.mysql.jdbc.Driver。5.2 JDBC 连接参数的三个关键配置数据库连接是这类系统最容易出问题的地方。JDBC URL 的写法直接决定了系统能不能连上 MySQL 5.7Class.forName(com.mysql.jdbc.Driver); String url jdbc:mysql://localhost:3306/laundry ?useUnicodetruecharacterEncodingutf8 useSSLfalseserverTimezoneAsia/Shanghai allowPublicKeyRetrievaltrue; Connection conn DriverManager.getConnection(url, root, password);characterEncodingutf8必须显式指定否则 Linux 服务器上 MySQL 默认的latin1字符集会让中文乱码。serverTimezoneAsia/Shanghai解决的是 JDBC 驱动和 MySQL 服务器时区不一致导致的日期偏移 8 小时问题。useSSLfalse是因为本地开发环境通常没配 SSL 证书不关掉会有警告但不报错。注意驱动类名在 MySQL 5.7 及以前是com.mysql.jdbc.Driver从 6.0 开始变成com.mysql.cj.jdbc.Driver。如果用 MySQL 8.0 的驱动连接 5.7 的数据库旧类名会报错需要改成新类名。数据源这块还有一个容易忽略的细节连接池要选对。不建议用 DBCP它在高并发下连接回收有 bug用 C3P0 或者 HikariCP 都可以。洗衣店场景单店并发低直接DriverManager.getConnection每次打开连接也行但数据库连接的开关开销大推荐至少在工具类里做一个简单的连接池复用。5.3 验证部署是否成功的检查清单部署完成后从浏览器访问http://localhost:8080/laundry/login.jsp如果页面能正常渲染登录后用管理员账号进入后台逐一检查以下功能点# 1. 检查 Tomcat 启动日志确认没有异常 tail -f /opt/tomcat/logs/catalina.out # 2. 检查数据库表是否创建成功 mysql -uroot -p -e USE laundry; SHOW TABLES; # 3. 检查 JSP 是否编译成功 ls /opt/tomcat/work/Catalina/localhost/laundry/org/apache/jsp/一个很常见的坑是catalina.out里报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized这是因为serverTimezone参数没配。这个报错信息看起来是乱码实际上就是时区识别失败把参数改成Asia/Shanghai就能解决。此外如果发现新添加的会员卡无法登录先查member_card表里的phone字段是否是唯一的。会员表如果允许重复手机号登录时findByCardNoAndPhone会返回多条记录程序取第一条会造成串号问题。正确的做法是在建表时给phone字段加唯一索引同时在 DAO 查询时用LIMIT 1兜底。5.4 一个实用技巧把营业额统计做成实时报表最后分享一个可以直接抄的优化方案。管理员后台的需求通常是「打开营业额统计页面能看到今日、本周、本月的营业额对比」。不要每次点击都在 Java 端拼三个 SQL 查询而是写成一条汇总语句SELECT SUM(CASE WHEN DATE(created_at) CURDATE() THEN amount ELSE 0 END) AS today_amount, SUM(CASE WHEN YEARWEEK(created_at, 1) YEARWEEK(CURDATE(), 1) THEN amount ELSE 0 END) AS week_amount, SUM(CASE WHEN DATE_FORMAT(created_at, %Y-%m) DATE_FORMAT(CURDATE(), %Y-%m) THEN amount ELSE 0 END) AS month_amount FROM consume_record;这条 SQL 在数据量小的时候性能没问题但如果这张表已经积累了几年的数据全表扫描会越来越慢。更好的做法是给consume_record建一个物化视图或者定时统计表每天凌晨用事件调度器把前一天的汇总结果写入daily_report表。在 JSP 系统里实现定时任务可以直接用 MySQL 的EVENT调度器CREATE EVENT daily_report_job ON SCHEDULE EVERY 1 DAY STARTS 2025-01-01 02:00:00 DO INSERT INTO daily_report (biz_date, total_amount) SELECT DATE(created_at), SUM(amount) FROM consume_record WHERE created_at DATE_SUB(CURDATE(), INTERVAL 1 DAY) AND created_at CURDATE();这样营业额统计页面只查询「已经算好」的daily_report表几百条汇总数据无论底层订单量多大都能毫秒级返回。注意ON SCHEDULE需要在 MySQL 里开启event_scheduler参数否则事件不会生效。用一条查询把月度账单和会员消费排行也做成联动的报表接口整个后台的数据看板就完整了。本文还有配套的精品资源点击获取