简介本资源是一份面向计算机专业本科生的毕业设计文档聚焦基于Java技术栈的咖啡厅管理系统开发实践适用于课程设计、毕设参考及中小型Web项目学习者。文档完整覆盖系统需求分析、JSP前端实现、MySQL数据库设计含E-R图与逻辑结构、管理员/导购员/前台三大模块详细实现及功能测试报告内容扎实具备较强工程落地参考价值。资源为单文件Word文档.docx共1个文件大小1.78MB格式规范目录清晰含摘要、技术选型、业务流程图、数据库设计表及系统测试结果等核心章节。目前已有177人学习下载读者可直接获取完整的系统设计思路、模块划分逻辑、关键代码实现说明及测试验证方法特别适合Java Web初学者理解MVC架构在实际业务场景中的应用。1. 为什么一个“咖啡厅管理系统”值得用 Java 重做一遍不是写个 CRUD 就叫系统而是让点单、库存、结账、排班全在线上闭环跑通你见过多少个“基于 Java 的咖啡厅管理系统”毕业设计十有八九是登录页 商品列表 订单表 管理员后台数据库建了五张表JSP 页面堆满% %部署到 Tomcat 一刷新就 500 错误——这不是系统这是「Java Web 技术栈的填空练习」。真正能落地的咖啡厅管理系统核心不在“有没有”而在“能不能扛住早高峰 30 分钟连续下单 87 单、库存实时扣减不超卖、店员换班时权限秒级切换、老板手机查昨天拿铁销量 Top3”。它本质是一个轻量级 ERP前端要适配 iPad 点餐屏和微信扫码点单后端得在 MySQL 里稳住事务边界业务逻辑必须绕开 JSP 的历史包袱用 Servlet JDBC 做干净分层再补上库存预警、销售日报、员工提成计算这些真实需求。本文不讲“如何新建一个 Dynamic Web Project”而是带你从零搭起一个可编译、可调试、可部署、可扩展的 Java 咖啡厅系统骨架——用最朴素的javax.servletmysql-connector-java 8.0.33commons-dbutils组合避开 Spring Boot 的自动配置黑匣子把每一行 SQL、每一次连接、每一个事务控制权亲手握在自己手里。适合刚学完 JDBC 想实战的新手也适合想带学生做真项目、拒绝模板化毕设的指导老师。2. 用纯 Java Web 搭建最小可行系统从建库建表到首页渲染三步跑通核心链路2.1 MySQL 建库建表按业务实体拆分而不是按“用户/商品/订单”粗暴三张表咖啡厅的真实数据关系比电商简单但比教科书复杂商品coffee_item不只是名称价格还要区分“现磨/挂耳/冷萃”关联原料如“美式”消耗 1 份浓缩 120ml 热水库存inventory必须按原料维度咖啡豆、牛奶、糖浆单独建表且每条记录带warehouse_id支持多门店订单order_header和订单明细order_detail之间是强事务关系但明细表要存item_idspec_id比如“大杯/少冰/燕麦奶”不能只存item_name字符串员工staff表必须含role_codebarista / cashier / manager和shift_pattern早班/晚班/轮值为排班模块留接口。提示别用 Navicat 图形界面一键生成建表语句。手动写.sql文件用utf8mb4_unicode_ci排序规则所有DECIMAL字段明确精度如price DECIMAL(10,2)created_at用TIMESTAMP DEFAULT CURRENT_TIMESTAMP避免时区陷阱。-- coffee_db.sql CREATE DATABASE IF NOT EXISTS coffee_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE coffee_db; CREATE TABLE coffee_item ( id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50) NOT NULL, category ENUM(espresso,milk,tea,food) NOT NULL, price DECIMAL(10,2) NOT NULL, is_active TINYINT(1) DEFAULT 1, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE inventory ( id INT PRIMARY KEY AUTO_INCREMENT, item_name VARCHAR(50) NOT NULL COMMENT 原料名如「哥伦比亚豆」「全脂牛奶」, unit VARCHAR(20) NOT NULL COMMENT 计量单位如「kg」「L」「瓶」, current_stock DECIMAL(10,3) DEFAULT 0.000, min_stock DECIMAL(10,3) DEFAULT 0.000 COMMENT 低于此值触发预警, warehouse_id INT DEFAULT 1, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ); CREATE TABLE order_header ( id VARCHAR(20) PRIMARY KEY COMMENT 格式ORD-20240521-0001, staff_id INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, status ENUM(pending,confirmed,served,cancelled) DEFAULT pending, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE order_detail ( id INT PRIMARY KEY AUTO_INCREMENT, order_id VARCHAR(20) NOT NULL, item_id INT NOT NULL, quantity INT NOT NULL DEFAULT 1, spec_desc VARCHAR(100) COMMENT 如「大杯/少冰/燕麦奶」, price_per_unit DECIMAL(10,2) NOT NULL, FOREIGN KEY (order_id) REFERENCES order_header(id) ON DELETE CASCADE, FOREIGN KEY (item_id) REFERENCES coffee_item(id) );执行命令mysql -u root -p coffee_db.sql。建完立刻用SELECT COUNT(*) FROM coffee_item;验证表存在别等写完 Java 代码才发现Unknown table coffee_db.coffee_item。2.2 Java 工程结构拒绝“src 下直接扔 .java”用标准分层隔离关注点Maven 项目结构不是摆设。pom.xml中只引入三个必要依赖servlet-apiprovided、mysql-connector-javaruntime、commons-dbutilscompile。不加 Spring、不加 Hibernate、不加任何 ORM 框架——因为你要看清 JDBC 的 Connection 是怎么被复用的PreparedStatement 是怎么防 SQL 注入的事务是怎么在connection.commit()和connection.rollback()之间卡住的。src/main/ ├── java/ │ └── com/coffee/system/ │ ├── model/ // POJO字段名与数据库列名严格一致id → idnot itemId │ ├── dao/ // 接口定义CoffeeItemDao、InventoryDao、OrderDao │ ├── dao/impl/ // 实现类CoffeeItemDaoImpl用 DbUtils QueryRunner │ ├── service/ // 业务逻辑OrderService 处理「创建订单→扣库存→更新状态」原子操作 │ └── servlet/ // 控制器LoginServlet、OrderListServlet、AddOrderServlet ├── webapp/ │ ├── WEB-INF/ │ │ ├── web.xml // 配置 Servlet 映射不用注解兼容老 Tomcat │ │ └── views/ // JSP 存放目录禁止放在 webapp 根下 │ ├── static/ // CSS/JS/IMG路径为 /static/css/app.css │ └── index.jsp // 仅作入口跳转不写业务逻辑关键约束model类必须实现Serializable所有属性privatepublic getter/setterdao接口方法命名直白ListCoffeeItem findAllActive()不叫queryAll()service层方法必须声明throws ServiceException把 SQLException 转为业务异常servlet中只做三件事取参数 → 调 service → 转发到 JSP绝不出现 new Connection()。2.3 首页渲染用 JSP 做展示层但用 Servlet 控制流程彻底告别 scriptlet很多毕设的index.jsp里塞满% while(rs.next()){ %这是灾难。正确做法IndexServlet查询活跃商品列表存入request.setAttribute(items, items)再request.getRequestDispatcher(/WEB-INF/views/index.jsp).forward(request, response)。JSP 只用 JSTL!-- /WEB-INF/views/index.jsp -- % taglib prefixc urihttp://java.sun.com/jsp/jstl/core % html headtitle咖啡厅点单台/title/head body h1今日推荐/h1 div classmenu-grid c:forEach items${items} varitem div classitem-card h3${item.name}/h3 p classprice¥${item.price}/p button onclickaddToCart(${item.id}, ${item.name}, ${item.price})加入订单/button /div /c:forEach /div script src/static/js/order.js/script /body /html注意c:forEach的items必须是ListCoffeeItem不是ResultSet${item.name}能取到值说明CoffeeItem的 getter 是getName()不是getItemName()/static/js/order.js里用原生 fetch 提交订单不拼接 URL 参数用 JSON body避免 XSS。3. 库存扣减与订单创建用 JDBC 手动事务保证“下单成功库存已扣”拒绝超卖玄学3.1 为什么UPDATE inventory SET current_stock current_stock - ? WHERE item_name ?是错的表面看这句 SQL 能扣库存但它埋了三个雷并发安全缺失两个线程同时读到current_stock 10都执行-1结果变成9实际应为8库存校验滞后先扣再查是否为负扣完才发现不够只能 rollback用户体验差原料关联断裂一杯拿铁消耗「浓缩液 1 份 牛奶 180ml」但coffee_item表里没存这个关系扣库存时不知道该扣哪几条inventory记录。正确解法在 Service 层用 Connection 控制事务边界用 SELECT ... FOR UPDATE 锁定原料行再校验再更新。以OrderService.createOrder()为例// OrderServiceImpl.java public String createOrder(ListOrderDetailDTO details, int staffId) throws ServiceException { Connection conn null; try { conn dataSource.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 // 步骤1预检查所有原料库存是否充足锁住相关 inventory 行 for (OrderDetailDTO detail : details) { CoffeeItem item coffeeItemDao.findById(detail.getItemId()); ListIngredientUsage usages ingredientDao.findByItemId(item.getId()); // 查「美式」需哪些原料 for (IngredientUsage usage : usages) { Inventory inv inventoryDao.findByNameForUpdate(usage.getIngredientName(), conn); if (inv.getCurrentStock().compareTo(usage.getAmount().multiply(BigDecimal.valueOf(detail.getQuantity()))) 0) { throw new ServiceException(原料「 usage.getIngredientName() 」库存不足); } } } // 步骤2生成订单头 String orderId generateOrderId(); OrderHeader header new OrderHeader(orderId, staffId, calculateTotal(details), pending); orderDao.insertHeader(header, conn); // 步骤3插入明细并扣减库存同一 connection for (OrderDetailDTO detail : details) { orderDao.insertDetail(new OrderDetail(orderId, detail.getItemId(), detail.getQuantity(), detail.getSpecDesc(), detail.getPricePerUnit()), conn); // 扣减每种原料 ListIngredientUsage usages ingredientDao.findByItemId(detail.getItemId()); for (IngredientUsage usage : usages) { inventoryDao.decreaseStock(usage.getIngredientName(), usage.getAmount().multiply(BigDecimal.valueOf(detail.getQuantity())), conn); } } conn.commit(); return orderId; } catch (SQLException e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ignored) {} } throw new ServiceException(创建订单失败 e.getMessage(), e); } finally { if (conn ! null) { try { conn.close(); } catch (SQLException ignored) {} } } }关键点说明inventoryDao.findByNameForUpdate(...)对应 SQLSELECT * FROM inventory WHERE item_name ? FOR UPDATE这是 InnoDB 行锁阻塞其他事务修改同一行decreaseStock()方法里用UPDATE inventory SET current_stock current_stock - ? WHERE item_name ?此时current_stock是锁住后的最新值不会超扣calculateTotal()必须用BigDecimal运算避免double的 0.10.2≠0.3generateOrderId()用ORD-YYYYMMDD-XXXXX格式XXXXX用AtomicInteger保证当日唯一不用数据库自增 ID易被猜出日销量。3.2 JSP 表单提交到 Servlet用 POST JSON而不是 GET 拼参常见错误form actionAddOrderServlet?itemId1qty2—— 这会让浏览器地址栏暴露参数且长度受限。正确方式!-- /WEB-INF/views/order_form.jsp -- form idorderForm input typehidden idstaffId value${sessionScope.staffId} div idcartItems/div button typebutton onclicksubmitOrder()提交订单/button /form script function submitOrder() { const items []; // 从页面收集购物车数据 document.querySelectorAll(.cart-item).forEach(el { items.push({ itemId: el.dataset.itemId, quantity: parseInt(el.querySelector(.qty).value), specDesc: el.querySelector(.spec).value || , pricePerUnit: parseFloat(el.dataset.price) }); }); fetch(/AddOrderServlet, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ items: items, staffId: document.getElementById(staffId).value }) }) .then(r r.json()) .then(data { if (data.success) { alert(订单创建成功单号 data.orderId); location.href /OrderListServlet; } else { alert(错误 data.message); } }); } /script对应的AddOrderServlet.doPost()用request.getReader()读取 JSON 字符串用Jackson或org.json解析为MapString, Object校验staffId是否在 session 中防未登录提交调用orderService.createOrder(...)返回response.getWriter().write({\success\:true,\orderId\:\ORD-20240521-0001\});。注意web.xml中servlet-mapping的url-pattern必须是/AddOrderServlet不是/addorder大小写敏感Tomcat 9 默认支持application/json无需额外配置。4. 避坑指南那些让系统上线即翻车的 5 个硬核细节4.1 现象JSP 页面中文显示为乱码MySQL 里存的是正常中文原因Tomcat 的URIEncoding未设为 UTF-8导致 GET 请求参数解码失败同时 JSP 文件本身未声明 pageEncodingMySQL 连接 URL 缺少useUnicodetruecharacterEncodingutf8mb4。解决修改conf/server.xml在Connector标签加URIEncodingUTF-8所有 JSP 文件首行加% page pageEncodingUTF-8 contentTypetext/html;charsetUTF-8 %jdbc:mysql://localhost:3306/coffee_db?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/Shanghai验证在 MySQL 命令行执行SHOW VARIABLES LIKE character_set%;确保character_set_client、character_set_connection、character_set_database全为utf8mb4。4.2 现象库存扣减后管理后台看到的数值比预期少 0.001如 10.000 变成 9.999原因Java 用float或double计算库存二进制浮点数无法精确表示十进制小数0.1 0.2 ≠ 0.3。解决所有库存字段在 Java 中用BigDecimal数据库用DECIMAL(10,3)运算必须用add()、subtract()、multiply()方法禁用 - * /运算符构造BigDecimal时用字符串new BigDecimal(10.000)不用 doublenew BigDecimal(10.0)会带精度误差。4.3 现象Tomcat 启动时报ClassNotFoundException: javax.servlet.http.HttpServlet原因Maven 依赖 scope 设为compile但servlet-api是容器提供的打包时不应打进 WAR或 IDE如 Eclipse未将Tomcat/lib/servlet-api.jar加入 Build Path。解决pom.xml中scopeprovided/scopeEclipse 右键项目 → Properties → Targeted Runtime → 选中已配置的 TomcatIntelliJ 在 Project Structure → Modules → Dependencies → Scope 切为Provided。4.4 现象订单创建后库存扣减了但订单状态仍是pending没变confirmed原因orderDao.insertHeader()和orderDao.insertDetail()用了不同 Connection事务不生效或conn.commit()被异常吞掉没执行。解决所有 DAO 方法必须接受Connection参数如insertHeader(OrderHeader h, Connection conn)由 Service 统一传入commit()前加日志log.info(即将提交订单事务ID{}, orderId)用 try-with-resources 包裹Connection确保 close 不影响 commit。4.5 现象管理员登录后点击“销售报表”页面空白浏览器控制台报Uncaught ReferenceError: $ is not defined原因JSP 引入了 jQuery但script src/static/js/jquery.min.js/script路径 404或静态资源未配置欢迎文件/static/被 Tomcat 拦截。解决确认webapp/static/js/jquery.min.js文件存在在web.xml中添加 welcome-file-listwelcome-file-list welcome-fileindex.jsp/welcome-file /welcome-file-list并确保static目录在webapp下不是src/main/resources/static后者是 Spring Boot 的位置。5. 让系统真正可用加一个库存预警邮件功能用 JavaMail API 发送告警不依赖第三方服务毕业设计常止步于“能增删改查”但真实咖啡厅需要主动预警。比如当「哥伦比亚豆」库存低于 2.000kg 时自动给店长邮箱发提醒。不用 Spring Mail用原生 JavaMail API三步搞定5.1 添加 JavaMail 依赖与 SMTP 配置pom.xml加dependency groupIdcom.sun.mail/groupId artifactIdjavax.mail/artifactId version1.6.2/version /dependency在src/main/resources/mail.properties写mail.smtp.hostsmtp.163.com mail.smtp.port465 mail.smtp.authtrue mail.smtp.ssl.enabletrue mail.usernameyour_email163.com mail.passwordyour_authorization_code # 注意不是邮箱密码是 163 开启 SMTP 后生成的授权码5.2 编写库存预警检查器独立线程每 5 分钟扫描一次// InventoryAlertChecker.java public class InventoryAlertChecker implements Runnable { private final InventoryDao inventoryDao; private final Properties mailProps; public InventoryAlertChecker(InventoryDao inventoryDao) { this.inventoryDao inventoryDao; this.mailProps loadMailProperties(); } Override public void run() { while (!Thread.currentThread().isInterrupted()) { try { ListInventory lowStockItems inventoryDao.findBelowMinStock(); for (Inventory item : lowStockItems) { sendAlertEmail(item); } Thread.sleep(5 * 60 * 1000); // 5分钟 } catch (InterruptedException e) { Thread.currentThread().interrupt(); break; } catch (Exception e) { log.error(库存预警检查失败, e); } } } private void sendAlertEmail(Inventory item) { try { Session session Session.getInstance(mailProps, new Authenticator() { Override protected PasswordAuthentication getPasswordAuthentication() { return new PasswordAuthentication( mailProps.getProperty(mail.username), mailProps.getProperty(mail.password) ); } }); Message message new MimeMessage(session); message.setFrom(new InternetAddress(mailProps.getProperty(mail.username))); message.setRecipients(Message.RecipientType.TO, InternetAddress.parse(managercafe.com)); // 店长邮箱 message.setSubject(【咖啡厅库存预警】 item.getItemName() 低于安全库存); message.setText( 当前库存 item.getCurrentStock() item.getUnit() \n 安全库存 item.getMinStock() item.getUnit() \n 请及时补货\n\n —— 咖啡厅管理系统 自动发送 ); Transport.send(message); log.info(已发送预警邮件{}, item.getItemName()); } catch (MessagingException e) { log.error(发送邮件失败, e); } } private Properties loadMailProperties() { Properties props new Properties(); try (InputStream is getClass().getClassLoader() .getResourceAsStream(mail.properties)) { props.load(is); } catch (IOException e) { throw new RuntimeException(e); } return props; } }5.3 在系统启动时启动检查器ServletContextListener// StartupListener.java public class StartupListener implements ServletContextListener { private Thread alertThread; Override public void contextInitialized(ServletContextEvent sce) { InventoryDao inventoryDao new InventoryDaoImpl(); InventoryAlertChecker checker new InventoryAlertChecker(inventoryDao); alertThread new Thread(checker, InventoryAlertChecker); alertThread.setDaemon(true); // 设为守护线程避免阻塞 Tomcat 关闭 alertThread.start(); sce.getServletContext().log(库存预警检查器已启动); } Override public void contextDestroyed(ServletContextEvent sce) { if (alertThread ! null alertThread.isAlive()) { alertThread.interrupt(); } sce.getServletContext().log(库存预警检查器已停止); } }并在web.xml中注册listener listener-classcom.coffee.system.StartupListener/listener-class /listener提示163 邮箱 SMTP 端口 465 必须用 SSL授权码在邮箱设置 → POP3/SMTP/IMAP → 开启 SMTP 服务后生成测试时先用InternetAddress.parse(testqq.com)发给自己确认能收到再切正式邮箱邮件内容用纯文本避免 HTML 渲染问题。6. 交付前必做的 3 项验证用真实数据跑通全流程而不是截图“运行成功”写完代码不等于系统可用。我带过 12 届毕设90% 的学生卡在“以为做完其实没做完”。以下三项验证缺一不可每项都要录屏或截图存档6.1 高并发下单压力测试用 Apache Bench 模拟早高峰别信“本地跑通就行”。用ab命令压测下单接口ab -n 100 -c 10 -p order_data.json -T application/json http://localhost:8080/AddOrderServlet其中order_data.json是{ items: [{itemId:1,quantity:1,specDesc:,pricePerUnit:28.00}], staffId: 101 }观察指标成功率必须 100%出现 500 错误说明事务没兜住库存一致性压测前查SELECT current_stock FROM inventory WHERE item_name哥伦比亚豆压测后再次查询变化值应等于成功订单数 × 每单消耗量响应时间P95 800ms否则 iPad 点单会卡顿。6.2 权限边界验证用不同角色账号实测操作禁区建三个测试账号barista工号 101只能创建订单、查看今日订单不能修改商品价格cashier工号 102能结账、打印小票不能进入库存管理页manager工号 100能调价、改库存、查销售报表。验证方式用barista登录访问/EditPriceServlet?id1应返回 403 或重定向到首页用cashier提交订单检查order_header.staff_id是否为 102用manager修改商品价格立刻在首页刷新看到新价格且历史订单价格不变价格快照逻辑。6.3 数据持久化验证重启 Tomcat MySQL确认数据不丢失这是最常被忽略的一步。操作流程用系统创建 5 笔订单确认库存扣减正确systemctl restart mysqlLinux或重启 MySQL 服务./catalina.sh stop ./catalina.sh startLinux或重启 Tomcat重新登录检查订单列表是否完整显示order_header表数据在库存数值是否与重启前一致inventory.current_stock未重置员工信息、商品信息全部可查。如果重启后数据消失一定是数据库 URL 写成了jdbc:h2:mem:testdbH2 内存库或者 MySQL 配置了skip-innodb导致表被创建为 MyISAM不支持事务。最后说句实在话这个系统没有炫酷的 Vue 前端没有 Spring Cloud 微服务甚至没用 Maven 多模块。但它把 Java Web 最底层的链条——从 HTTP 请求解析、JDBC 连接池、SQL 事务控制、到邮件发送——全都亲手拧紧了一遍。我当年第一次用SELECT ... FOR UPDATE解决超卖时盯着日志里Lock wait timeout exceeded报错调了 7 小时现在看那 7 小时比写一百行 Spring Boot 配置都值。技术选型不是越新越好而是越可控越好系统价值不在于用了多少框架而在于老板打开后台一眼就能看出“今天燕麦奶卖了多少升”。希望帮到你。本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?