
简介一款基于JavaWeb的超市收银系统完整源码面向JavaWeb开发学习者与高校相关专业学生覆盖商品管理、订单管理、用户权限控制和销售统计等核心业务适合用于课程设计、毕业设计或小型超市项目二次开发参考。压缩包共147个文件主体由60个Java源文件、29个JSP页面、16个CSS样式和7个JS脚本构成并配有SQL数据库脚本与Maven配置整体仅1.61MB结构紧凑、便于快速部署运行。目前已有97人学习下载可借助完整源码快速理解JavaWeb项目的分层开发、会话管理、权限校验及前后端交互方式。项目内置多角色权限机制和库存预警、订单日志等细节既能巩固课堂知识也能为扩展支付场景或重构前端提供清晰起点。1. 超市收银系统JavaWeb 单体应用的核心价值一家小型超市每天要处理几百笔交易每笔交易涉及商品扫描、折扣计算、库存扣减、支付方式记录老板下班前还想知道今天卖了多少钱、哪个商品走量最快。这类需求用微服务做是杀鸡用牛刀Excel 又撑不住并发写入最适合的恰恰是 JavaWeb 单体应用Servlet 处理请求、JSP 渲染页面、JDBC 读写 MySQL前端用 Bootstrap 保证在老旧收银机上也能正常显示。这份基于 JavaWeb 的超市收银系统源码覆盖了商品管理、订单生成、多角色登录和销售统计四个核心模块对刚接触 JavaWeb 的开发者来说是完整的 MVC 案例对需要快速搭建内部管理系统的从业者来说则是可以直接改造成其他进销存场景的骨架。2. 技术选型与项目结构Maven Wrapper 和 Bootstrap 静态资源说明了什么看项目根目录mvnw.cmd 的存在说明这是一个用 Maven Wrapper 管理的项目。Maven Wrapper 的意义在于锁定了 Maven 版本你不需要在机器上预先安装 Maven只要系统里有 JDK执行mvnw.cmd clean package就会自动下载对应版本的 Maven 并完成构建。对团队协作来说这避免了“我本地能编译你本地不行”的版本分歧对单体 JavaWeb 项目来说这比用 IDEA 内置的 Maven 或多模块 Gradle 更直接。pom.xml 里依赖按 Web 项目标准配置javax.servlet-api 提供 Servlet 接口jsp-api 提供页面渲染支持mysql-connector-java 负责 MySQL 驱动单元测试相关的 junit 也会出现。前端方面bootstrap.css、bootstrap.rtl.css、bootstrap-utilities.css 等一系列文件被打包进了项目说明页面没有走 CDN 而是本地静态资源。这个选择对收银场景是合理的超市内网环境经常掐外网本地化 Bootstrap 保证页面样式在任何网络环境下都能完整加载。项目结构遵循经典的 JavaWeb 分层层名职责典型代码servlet 层接收 HTTP 请求、解析参数、调用 serviceProductServlet, OrderServletservice 层业务逻辑库存扣减、订单金额计算ProductService, OrderServicedao 层JDBC 操作 MySQL封装增删改查ProductDao, OrderDaoentity 层数据库表对应的 JavaBeanProduct, Order, Userweb 层JSP 页面 Bootstrap 组件product-list.jsp, login.jsp提示JavaWeb 项目里最常见的返工原因是层与层之间耦合太紧Servlet 里直接写 JDBC 代码在功能上能跑通但后续加库存预警、加支付方式时就只能翻整个方法重写。编译和打包走mvnw.cmd clean package最终会产出 WAR 包把这个 WAR 包丢进 Tomcat 的 webapps 目录即可启动。执行mvnw.cmd spring-boot:run这种命令在这里是不适用的因为项目没有引入 spring-boot-maven-plugin它的运行模型就是传统 Servlet 容器。如果 JavaWeb 项目里看到 mvnw.cmd潜意识里应该意识到使用者在设计时考虑了环境可迁移性如果你接手后要部署到生产机器只需要在这台机器安装 JDK 8 及以上版本其余依赖都由构建脚本完成。数据库连接配置通常在src/main/resources/db.properties或 JDBC 工具类中内容大致是jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf8 jdbc.usernameroot jdbc.password123456characterEncodingutf8指定了连接层的中文字符集。超市商品名、品类、供应商基本都是中文这里配错了会出现插入正常但查询乱码的现象。常见排查思路是先看页面编码JSPpageEncoding是否为 UTF-8再看请求编码Servlet 里是否设置了request.setCharacterEncoding(UTF-8)最后看数据库表字段的 collation 类型。带 bootstrap-rtl 文件还暗示了一个细节——如果将来要适配阿拉伯语等 RTL 阅读习惯的收银界面静态资源层已经有基础了不必重做样式。3. 核心功能实现商品管理、订单生成与多角色登录的代码路径3.1 用户登录与角色权限管理员和收银员的边界用户表设计通常包含id, username, password, role四个字段role字段用字符串区分管理员和收银员。登录流程不是简单的查询比对先看核心代码public User login(String username, String password) { String sql SELECT id, username, password, role FROM t_user WHERE username? AND password?; User user null; try (PreparedStatement ps DBUtil.getConnection().prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, MD5Util.encrypt(password)); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { user new User(); user.setId(rs.getInt(id)); user.setUsername(rs.getString(username)); user.setRole(rs.getString(role)); } } } catch (SQLException e) { e.printStackTrace(); } return user; }这个写法有两点值得注意。第一用PreparedStatement而不是Statement拼接 SQL能有效防止 SQL 注入第二密码经过MD5Util.encrypt(password)再入库比对。追求更好的安全性当然建议用 BCrypt 加盐但 JavaWeb 源码包里通常用 MD5因为不依赖额外 jar 包。接手这套代码后如果想升级常见做法是引入 jBCrypt 库把注册逻辑里的加密函数替换成BCrypt.hashpw(password, BCrypt.gensalt())同时登录逻辑改成BCrypt.checkpw(inputPassword, user.getPassword())数据库里旧 MD5 密文需要做一次性迁移。登录成功后用户信息要存进 Session否则每个请求都不知道当前操作者是谁。Servlet 里常见的做法是HttpSession session request.getSession(); session.setAttribute(loginUser, user);权限控制有两种粒度。粗粒度是在 Servlet 里判断角色收银员访问管理员的商品编辑接口时直接返回错误页细粒度是用 Filter 拦截 URL 前缀比如/admin/*开头的路径必须要求session.getAttribute(loginUser)存在且角色是admin。源码里大概率是两者结合Filter 做登录拦截Servlet 里面再做角色判断。收银员角色应该只有订单创建和查询权限管理员才有商品增删改和销售统计权限。这个边界如果没写好会出现收银员可以修改商品价格然后低价结账的内部漏洞。3.2 商品管理的增删改查库存字段的更新时机商品表的核心字段有id, name, barcode, price, stock, warning_stock, category_id。商品管理的 CRUD 常规写法是 ProductServlet 接收前端传来的action参数用 switch 分发到 add、update、delete、list 等处理方法。商品新增和修改有一个容易踩的坑前端输入的price是字符串JDBC 写入时需要setBigDecimal直接用字符串会导致 MySQL 隐式类型转换小数位精度在特定情况下丢位。某次用 9.99 元的商品反复测试写入后变成 9.99查询出来变成 9.99看似没影响但对按价格区间统计的报表来说SUM(price)的累加值可能出现微小偏差根源就是浮点数参与运算。规范做法是 Java 侧用BigDecimal数据库侧DECIMAL(10, 2)两边都不给浮点类型留余地。库存扣减的时机直接关系到订单生成逻辑。常见的错误在收银场景里用户选了三瓶可乐系统扣了三次库存但支付失败最后订单作废库存又加回来——这中间如果进程中途挂了数据就对不上。正确的做法是把“扣库存”和“生成订单”放进同一个事务public boolean createOrder(Order order, ListOrderItem items) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交 OrderDao orderDao new OrderDao(); orderDao.insertOrder(conn, order); for (OrderItem item : items) { int affected orderDao.deductStock(conn, item.getProductId(), item.getQuantity()); if (affected 0) { conn.rollback(); return false; // 库存不足回滚整个订单 } orderDao.insertOrderItem(conn, order.getId(), item); } conn.commit(); return true; } catch (Exception e) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } return false; } finally { DBUtil.close(conn); } }这里deductStock的 SQL 是UPDATE t_product SET stock stock - ? WHERE id ? AND stock ?注意AND stock ?这个条件非常重要。它让“库存是否足够”的判断和“扣减库存”的操作在一条 UPDATE 语句中原子完成避免了先 SELECT 再 UPDATE 两步操作之间的并发窗口。如果写成先查询库存再判断再更新两个收银员同时刷卡同一件商品最后一件库存可能被卖出两次。这就是为什么在收银这种高并发写入场景里条件更新比先查后改可靠得多。3.3 订单生成与多支付方式状态机的设计订单状态常见设计是pending、paid、cancelled支付方式字段pay_type用字符串区分现金、银行卡、移动支付。订单生成后如果没有支付状态是pending等到支付回调或收银员确认收款后更新为paid。订单日志表记录每次状态变更的操作人、时间和事件方便后续对账。支付方式的设计建议用状态机而不是任意改字段。比如一个订单从pending到paid允许从cancelled到paid不允许。代码里可以定义一个枚举public enum OrderStatus { PENDING, PAID, CANCELLED; }然后状态更新 SQL 里带上前置状态条件UPDATE t_order SET status PAID, pay_time NOW() WHERE id ? AND status PENDINGWHERE status PENDING防止重复支付导致的两次更新。收银系统里经常出现的问题是取消订单时把已支付订单也一起更新成取消导致收入凭空减少。状态机约束加在 SQL 层比加在 Java 判断更安全——因为 SET 和 WHERE 是同一个 UPDATE 的原子语义。订单查询功能一般包含按订单号、时间范围、收银员、支付方式等条件组合查询。这个页面的 SQL 用动态拼接要注意参数化防止拼接出来的 SQL 在特殊字符下崩溃SELECT * FROM t_order WHERE 1 1 if testorderNo ! null and orderNo ! AND order_no #{orderNo} /if if teststartTime ! null AND create_time #{startTime} /if ORDER BY create_time DESC前端 Bootstrap 表格配合分页后端用 LIMIT 加 count 查询完成分页。分页参数建议用page和pageSize页面显示的是“第 x 页 / 共 y 页”的控件。查询结果集大的时候count 和 list 两条 SQL 都要跑且条件保持一致否则页数和数据对不上。4. 订单统计与库存预警销售数据的 SQL 聚合方案4.1 按日/按月统计营业额GROUP BY 的日期函数应用统计功能是收银系统里最能体现 SQL 功底的部分。按日统计营业额的核心 SQL 是SELECT DATE(create_time) AS day, SUM(total_amount) AS total_amount, COUNT(*) AS order_count FROM t_order WHERE status PAID AND create_time DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY DATE(create_time) ORDER BY day DESCDATE_SUB(CURDATE(), INTERVAL 30 DAY)生成 30 天前的日期也就是滚动统计最近一个月的情况DATE(create_time)返回值是日期类型不包含时分秒正好作为分组键。订单表里如果同时存在已支付和已取消的状态统计时必须用status PAID过滤不然取消订单的金额会被算进营业额里。按月统计需要变换分组键SQL 如下SELECT DATE_FORMAT(create_time, %Y-%m) AS month, SUM(total_amount), COUNT(*) FROM t_order WHERE status PAID GROUP BY DATE_FORMAT(create_time, %Y-%m) ORDER BY month DESCDATE_FORMAT的%Y-%m格式会输出“2024-11”这种格式的字符串可以直接传给前端图表插件作为 X 轴标签。这里有个性能细节需要提醒对 create_time 字段使用函数会导致索引失效如果订单表的数据量超过十万行这条 GROUP BY 统计会明显变慢。常见的优化方案是在订单表上建立一个pay_time字段并且把统计条件改成pay_time ?配合BETWEEN的范围查询这样走索引会更快。支付方式占比统计是老板第二关心的问题。用 CASE WHEN 做单表分组SELECT CASE pay_type WHEN 1 THEN 现金 WHEN 2 THEN 银行卡 WHEN 3 THEN 移动支付 ELSE 其他 END AS pay_type_name, COUNT(*) AS cnt, SUM(total_amount) AS total_amount FROM t_order WHERE status PAID AND DATE(create_time) CURDATE() GROUP BY pay_type这段 SQL 返回每个支付方式的订单数和金额数前端 Bootstrap 表格或柱状图展示都方便。注意pay_type字段在前端表单里是数字提交时要做合法性校验否则非法值落入ELSE 其他分组里对账时容易困惑。统计页面用 JSP 渲染时把查询结果放进 request 域用c:forEach遍历展示Bootstrap 的table-striped类能让长表格隔行变色对比时更直观。4.2 库存预警低于安全库存时的自动检查库存预警不能用“每次查询时都全表扫一遍”的思路做。更可靠的做法是通过定期任务检测把低于安全库存的商品写入一个预警表前端收银页面加载时只查询这个预警表。这个设计把“高频的页面请求”和“低频的库存检测”分离避免用户每次刷新列表都跑一次WHERE stock warning_stock的全表扫描。检测逻辑本身是一句简单的 SQLSELECT id, name, stock, warning_stock FROM t_product WHERE stock warning_stockJavaWeb 场景的定时任务常见做法有两种。一种是 Servlet 的ServletContextListener结合ScheduledExecutorService项目启动时创建一个单线程的调度器每天定时执行检测另一种是配置一个 web.xml 的 listener在contextInitialized里加载。推荐前一种因为它不依赖外部调度框架。代码示意WebListener public class StockCheckListener implements ServletContextListener { private ScheduledExecutorService scheduler; Override public void contextInitialized(ServletContextEvent sce) { scheduler Executors.newSingleThreadScheduledExecutor(); scheduler.scheduleAtFixedRate(new StockCheckTask(), 0, 1, TimeUnit.HOURS); } Override public void contextDestroyed(ServletContextEvent sce) { scheduler.shutdownNow(); } }scheduleAtFixedRate的第二个参数表示首次执行的延迟第三个参数表示执行周期。这里写 1 小时执行一次超市场景下这个频率足够实时了。StockCheckTask的 run 方法里调用ProductDao.listLowStockProducts()把结果输出到日志或者写入预警表。在部署阶段排查这个功能时可以先手动在数据库里把某个商品的库存改成 1安全库存设为 10然后等调度器跑完一轮再去看预警表里有没有这条记录。4.3 商品动销率统计哪个商品最走量动销率 有销量商品数 / 总商品数是超市运营里判断品类健康度的重要指标。虽然摘要里没直接提但销售统计功能通常会包含畅销商品 Top N 的查询SQL 如下SELECT p.name, SUM(oi.quantity) AS total_sold FROM t_order_item oi JOIN t_product p ON oi.product_id p.id JOIN t_order o ON oi.order_id o.id WHERE o.status PAID AND o.create_time DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY p.id, p.name ORDER BY total_sold DESC LIMIT 10JOIN 关系t_order_item通过product_id找商品名通过order_id找订单再用订单表的status字段过滤已支付订单。LIMIT 10取销量前十。这里的分页和 Top N 是不同场景分页是被动浏览的完整结果集Top N 是主动输出的排序表两者对索引的需求不同。给t_order的create_time加普通索引给t_order_item的order_id加普通索引就能让这条 SQL 在十万级数据量下保持在几百毫秒内返回。统计页面输出到 JSP 后一般会配一个简单的柱状图。Bootstrap 本身不提供图表能力常见做法是引入一个开源图表库后端把 Top N 数据以 JSON 形式暴露前端 fetch 之后动态绘制柱状图。JSON 输出 Servlet 里用response.setContentType(application/json;charsetutf-8)再直接用JSONObject封装结果集返回。5. 收银系统的排错路径与上线前验证清单5.1 环境启动时的三类常见报错第一类Tomcat 启动时报ClassNotFoundException: com.mysql.jdbc.Driver。这是因为 mysql-connector-java 的 jar 包没有被打进 WEB-INF/lib 目录。Maven 项目里检查 pom.xml 的依赖 scopemysql依赖如果被加了scopeprovided/scope编译可以通过但运行时找不到。真正部署时不应该用 provided设置成默认的 compile 并重新mvnw.cmd package然后确认 WAR 包里的WEB-INF/lib是否出现了 mysql 开头的 jar 文件。第二类页面中文乱码。这个问题涉及三个环节。JSP 页面开头要有% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8 %Servlet 接收请求参数前要调用request.setCharacterEncoding(UTF-8)数据库连接 URL 必须带useUnicodetruecharacterEncodingutf8。三个环节里任何一环缺失都会导致中文变成问号。定位方法是先查数据库里的数据是否正常。数据库正常但页面乱码是响应编码问题数据库都乱码是连接编码或请求编码问题。第三类端口占用。Tomcat 默认 8080 端口被占用会导致启动失败报错信息里会出现Port 8080 required by Tomcat v9.0 Server is already in use。命令行执行netstat -ano | findstr 8080找到占用进程的 PID再通过任务管理器结束即可。如果生产机器上 8080 不能动修改server.xml里Connector port8080为 8081 或 9090前端页面的跳转链接里的端口也要同步调整。5.2 订单数据一致性的验证方法收银系统的核心是钱货一致上线前需要做一个简单的并发验证。开两个浏览器窗口同时以收银员身份登录将同一个商品库存修改为 1然后两个窗口同时提交订单。如果代码正确使用了条件更新 SQL则只有一个订单成功另一个订单被回滚数据库里库存为 0。如果两个订单都成功说明库存扣减逻辑有并发问题。测试后的排查重点是 Session 中存储的报错提示是否准确——失败的那一侧页面要显示“库存不足”而不是笼统的“下单失败”。另一个验证动作是订单金额对账。手动下一笔包含 3 件商品、其中 1 件享受折扣的订单记录页面显示总额然后去数据库分别执行SELECT total_amount, discount FROM t_order WHERE order_no 具体单号;SELECT SUM(quantity * price) FROM t_order_item WHERE order_id (SELECT id FROM t_order WHERE order_no 具体单号);两边的金额应该一致不一致时优先检查t_order_item里的单价是否保留了商品下单时的快照价格。商品价格是允许修改的如果订单明细直接关联商品表取价格历史订单会跟着变价所以订单明细表一般要冗余存一份price字段下单那一瞬间的成交价固定下来。这也是 JavaWeb 系统中“快照设计”的一个典型应用。5.3 系统上线前的功能检查清单功能模块检查项期望结果登录管理员和收银员登录同一 URL两类角色能看到不同的导航菜单商品管理添加中文商品名称并保存列表页和数据库均为中文不乱码商品管理设置库存为 1同时两个会话下单仅一个订单成功库存为 0订单管理现金支付一笔订单然后取消一笔已支付订单取消操作被拒营业额统计不变订单统计查看今日营业额金额等于全部已支付订单合计库存预警数据库把库存改为低于预警值等待定时任务执行预警表中出现对应商品记录权限边界收银员访问商品新增 URL返回无权限提示而非商品编辑页提示按这个清单测一轮之后把每项的结果记在测试文档里。将来项目交接、二次开发时的回归测试都以这份清单为基准比从头看代码高效得多。本文还有配套的精品资源点击获取