简介:这份资源是一套基于 JavaWeb 的超市收银系统完整源码,面向计算机专业学生、JavaWeb 初学者及需要课程设计或毕业设计参考的开发者,帮助解决收银结算、库存管理与销售统计等实际业务场景的实现问题。压缩包共 147 个文件,约 1.61MB,以 60 个 Java 源文件与 29 个 JSP 页面为主体,配合 22 个 MyBatis 映射文件、16 个 CSS 与 7 个 JavaScript 前端资源,另有 SQL 建表脚本、Maven 配置及说明文档,结构完整、层次清晰。系统覆盖商品增删改查与库存预警、订单生成查询与多支付方式、多角色用户登录与权限管理,以及销售数据统计分析等模块,前端引入 Bootstrap 组件库,便于二次开发与界面调整。目前已有 97 人学习下载,适合作为 JavaWeb 综合实践的学习范本,读者可据此理解 MVC 分层设计、数据库映射与前后端交互的完整实现思路。
1. 超市收银系统这套 JavaWeb 源码,拿到手到底能跑出什么
很多人拿到「基于 JavaWeb 的超市收银系统」这类源码包,第一反应是双击解压、找 main 方法、直接 run,然后发现根本跑不起来——没有数据库、没有依赖、Tomcat 版本对不上,折腾两小时连登录页都见不到。这不是你技术不行,是 JavaWeb 项目和 Spring Boot 项目的启动逻辑完全不是一回事。JavaWeb 项目本质是「Servlet 容器 + WAR 包」的模型,它没有内嵌服务器,你得自己配 Tomcat、自己建库、自己导 SQL,少一步就是 404 或 500。
这套源码能解决的核心问题很具体:它把超市收银场景里最高频的几个动作——商品扫码录入、会员折扣计算、库存扣减、小票结算、日结报表——用 JSP + Servlet + JDBC 串成了一条完整链路。适合谁?适合正在做课程设计的学生、想从「只会写 CRUD 接口」过渡到「理解请求怎么从浏览器走到数据库再走回来」的初中级开发者,以及需要一套能改能扩的收银业务底座的独立开发者。它不适合想直接上生产的人,因为收银系统涉及金额计算和并发扣库存,源码级项目在这两块通常只做了演示级处理,后面我会专门讲怎么补。
2. 从 WAR 包到能登录:环境搭建与数据库初始化
2.1 先搞清楚这套 JavaWeb 项目的目录结构
拿到源码包解压后,你看到的目录大概率长这样:src下按包名分层,通常是com.xxx.entity、com.xxx.dao、com.xxx.servlet、com.xxx.util;WebContent或web目录下放WEB-INF、jsp页面、css/js静态资源和lib依赖 jar。这里有个新手最容易翻车的点:WEB-INF目录下的 JSP 是不能被浏览器直接访问的,必须通过 Servlet 转发,如果你发现某个页面 404,先确认它是不是被放在了WEB-INF里面。
lib目录里的 jar 包决定了你能不能编译通过。常见的有mysql-connector-java、jstl、servlet-api、commons-dbutils或druid。注意servlet-api这个 jar 在 Tomcat 运行时是 provided 状态,如果你把它打进 WAR 的WEB-INF/lib,启动时可能报类冲突。我一般会先看lib里有没有servlet-api.jar,有就把它从构建路径里移除,只保留编译期引用。
2.2 IDEA 里配置 Tomcat 并跑通第一个 JSP
用 IDEA 跑 JavaWeb 项目的完整流程如下。先File -> Project Structure -> Modules,确认src被标记为 Sources Root,WebContent被标记为 Web Resource Directory,并且web.xml的路径指向正确。然后在Artifacts里新建一个Web Application: Exploded,输出目录默认即可。
接着配置 Tomcat。Run -> Edit Configurations -> + -> Tomcat Server -> Local,在Server标签页指定你本地的 Tomcat 安装目录,在Deployment标签页点+把刚才的 Artifact 加进去,Application context 建议设成/cashier,这样访问路径就是http://localhost:8080/cashier/。
# 确认 Tomcat 版本与项目 servlet-api 匹配 # 老项目常见 Tomcat 8.5 + Servlet 3.1,别硬上 Tomcat 10 cd /path/to/tomcat/conf grep -r "Connector port" server.xml # 默认 8080,被占用就改成 8081启动后如果看到 Tomcat 日志里Deployment of web application archive成功,且浏览器能打开登录页,说明容器层通了。如果报ClassNotFoundException: com.mysql.jdbc.Driver,说明驱动 jar 没进WEB-INF/lib或者版本太老,换成mysql-connector-java-8.0.x并把驱动类名改成com.mysql.cj.jdbc.Driver。
2.3 建库、导 SQL、改连接参数这三步一个都不能少
数据库是这套系统能不能登录的关键。源码包里通常会有一个.sql文件,里面包含建库语句、建表语句和初始数据。常见表有user(收银员账号)、product(商品信息)、stock(库存)、sale_order(销售单)、sale_item(销售明细)、member(会员)。
-- 先建库,字符集用 utf8mb4,否则中文商品名会乱码 CREATE DATABASE cashier_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE cashier_db; -- 典型的商品表结构,注意 price 用 decimal 不要用 float CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, barcode VARCHAR(50) UNIQUE NOT NULL, name VARCHAR(100) NOT NULL, price DECIMAL(10,2) NOT NULL, stock INT DEFAULT 0, category_id INT ); -- 初始收银员账号,密码字段看源码是明文还是 MD5 INSERT INTO user (username, password, role) VALUES ('admin', '123456', 'cashier');导入完成后,找到源码里的数据库连接配置。JavaWeb 项目一般有两种写法:一种是在src下有个db.properties或jdbc.properties,另一种是直接在DBUtil.java里硬编码。找到后把 URL、用户名、密码改成你本地的。
# db.properties 典型内容 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/cashier_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=你的密码注意:
serverTimezone这个参数在 MySQL 8 驱动里必须加,否则启动时报The server time zone value is unrecognized。这是 JavaWeb 老项目迁移到新驱动时最高频的坑。
改完配置重启 Tomcat,用初始账号登录。如果登录后跳转正常但页面样式全丢,检查css/js路径是不是用了相对路径而你的 context path 不是/,把页面里的${pageContext.request.contextPath}补上即可。
3. 收银核心链路:扫码、算价、扣库存、出小票
3.1 扫码录入到购物车的数据流是怎么走的
收银台的核心交互是:收银员在输入框里扫条码或手输条码,前端发一个请求到 Servlet,Servlet 调 DAO 查商品,查到后把商品加入当前会话的购物车,前端刷新列表。这条链路看着简单,但涉及三个关键设计点。
第一,购物车存哪里。常见做法是存在HttpSession里,用一个List<CartItem>或Map<String, CartItem>。用 Map 的好处是同一条码重复扫码时可以直接累加数量,不用遍历。第二,条码查询要加索引,product表的barcode字段必须建唯一索引,否则商品多了以后每次扫码都全表扫描。第三,扫码请求要用 POST 而不是 GET,因为条码可能包含特殊字符,GET 的 URL 编码容易出问题。
// ScanServlet 核心逻辑,省略异常处理 protected void doPost(HttpServletRequest req, HttpServletResponse resp) { String barcode = req.getParameter("barcode"); ProductDao dao = new ProductDao(); Product p = dao.findByBarcode(barcode); if (p == null) { resp.getWriter().write("{\"code\":404,\"msg\":\"商品不存在\"}"); return; } HttpSession session = req.getSession(); Map<String, CartItem> cart = (Map<String, CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new LinkedHashMap<>(); session.setAttribute("cart", cart); } CartItem item = cart.get(barcode); if (item == null) { item = new CartItem(p, 1); cart.put(barcode, item); } else { item.setQty(item.getQty() + 1); } resp.getWriter().write("{\"code\":200,\"name\":\"" + p.getName() + "\"}"); }这段代码里LinkedHashMap保证购物车按扫码顺序展示,CartItem里存商品快照和数量。参数说明:barcode是前端传的条码,qty是数量。失败时看什么?如果返回 404 但你确认数据库有这条记录,检查findByBarcode的 SQL 是不是用了like而不是=,或者条码字段有没有前后空格。
3.2 金额计算为什么必须用 BigDecimal
这是收银系统里最不能省的一步。很多源码为了省事用double算金额,结果 0.1 + 0.2 得到 0.30000000000000004,小票上就会出现「合计 19.999999」这种鬼东西。金额计算必须用BigDecimal,而且要用String构造器,不能用double构造器。
// 正确的金额计算方式 BigDecimal total = BigDecimal.ZERO; for (CartItem item : cart.values()) { BigDecimal price = item.getProduct().getPrice(); // 已经是 BigDecimal BigDecimal qty = new BigDecimal(item.getQty()); total = total.add(price.multiply(qty)); } // 会员折扣,比如 9 折 BigDecimal discount = new BigDecimal("0.9"); BigDecimal payable = total.multiply(discount).setScale(2, RoundingMode.HALF_UP);参数说明:setScale(2, RoundingMode.HALF_UP)表示保留两位小数并四舍五入,这是收银场景的标准做法。RoundingMode不要用HALF_DOWN,否则 0.125 会变成 0.12,顾客会跟你吵架。如果源码里用的是double,建议你直接重构这块,别心存侥幸,金额算错是收银系统的致命伤。
3.3 结算时扣库存的并发问题与两种解法
结算动作要在一个事务里完成三件事:插入销售单、插入销售明细、扣减库存。前两步是纯插入,问题不大,第三步扣库存是并发重灾区。如果两个收银台同时卖最后一件商品,不做控制就会出现超卖。
// 方案一:悲观锁,在查询库存时就加行锁 String sql = "SELECT stock FROM product WHERE id = ? FOR UPDATE"; // 方案二:乐观锁,用版本号或库存条件更新 String updateSql = "UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?"; int rows = update(updateSql, qty, productId, qty); if (rows == 0) { throw new RuntimeException("库存不足,扣减失败"); }我一般会推荐方案二,因为收银场景并发量不算极端,乐观锁足够用,而且不用长时间持有数据库行锁。参数说明:stock >= ?这个条件保证了不会扣成负数,rows == 0就是扣减失败的信号,此时要回滚整个事务并提示收银员。失败时看什么?如果频繁出现扣减失败但库存明明够,检查是不是有未提交的事务在锁行,或者连接池配置的隔离级别有问题。
3.4 小票生成:从 JSP 渲染到打印样式适配
小票本质是一段格式化的 HTML,用 JSP 渲染后调浏览器打印。关键不在技术,在样式。小票打印机通常是 58mm 或 80mm 热敏纸,宽度固定,所以 CSS 要用@media print控制,字体用等宽字体,边距要压到最小。
@media print { @page { size: 58mm auto; margin: 0; } body { width: 58mm; font-family: "Courier New", monospace; font-size: 12px; } .no-print { display: none; } }参数说明:size: 58mm auto告诉浏览器纸张宽度,auto表示高度自适应。.no-print用来隐藏打印按钮本身。如果打印出来内容被截断,检查是不是body有默认 margin,或者商品名太长没有换行。常见做法是给商品名加word-break: break-all。
4. 避坑与排查:这套源码最容易翻车的五个地方
4.1 中文乱码:从请求到响应到数据库的三层排查
现象:商品名显示成??????或手机。原因通常有三层:请求体编码、响应编码、数据库连接编码。解决顺序是先看数据库,SHOW VARIABLES LIKE 'character%'确认character_set_server是utf8mb4;再看连接 URL 有没有characterEncoding=utf8;最后在 Servlet 里加req.setCharacterEncoding("UTF-8")和resp.setContentType("text/html;charset=UTF-8")。三层都对了,乱码必消。
4.2 登录后 Session 丢失:不是代码问题,是 Cookie 路径问题
现象:登录成功跳转到主页,但主页又跳回登录页。原因多半是 Session 的 Cookie 路径和你的 context path 不一致。比如你的应用部署在/cashier,但 Cookie 的 path 是/,某些容器下会出问题。解决:在web.xml里配<session-config>,或者检查 Tomcat 的context.xml有没有改过sessionCookiePath。另一个可能是浏览器禁了 Cookie,换无痕窗口试一下就能排除。
4.3 商品条码重复扫码后数量不累加
现象:同一商品扫两次,购物车里出现两条记录而不是数量变成 2。原因:购物车用了List且每次扫码都add新对象,没有做去重判断。解决:把购物车改成Map<String, CartItem>,key 用条码,扫码时先get再决定是新建还是累加。如果源码用的是 List,找到add的地方改成先遍历查找。
4.4 结算时报事务未回滚导致库存扣了但订单没生成
现象:库存少了,但销售单表里查不到记录。原因:扣库存和插订单不在同一个事务里,或者事务方法没有加@Transactional(JavaWeb 项目通常用 JDBC 手动控制事务)。解决:在 Service 层用Connection.setAutoCommit(false)开启事务,所有操作成功后commit,任何一步异常就rollback。检查 DAO 里有没有自己关了连接,关了连接事务就断了。
4.5 Tomcat 启动报端口占用或 JDK 版本不匹配
现象:启动时Address already in use或Unsupported major.minor version。原因:8080 被别的进程占了,或者项目编译用的 JDK 版本高于 Tomcat 运行的 JDK。解决:netstat -ano | findstr 8080找到占用进程杀掉或改端口;JDK 版本用Project Structure -> Project统一设成 1.8 或 11,和 Tomcat 的JAVA_HOME保持一致。老 JavaWeb 项目用 JDK 8 最稳,别硬上 17。
5. 把这套源码改造成能用的收银底座:三个进阶动作
第一个动作是加一层 Service。源码里大概率是 Servlet 直接调 DAO,业务逻辑散在 Servlet 里。你可以在中间加一个SaleService,把「算价、扣库存、生成订单」封装成一个方法,Servlet 只负责收参数和返回结果。这样以后要加「满减」「第二件半价」这类促销规则,只改 Service 就行,不用动 Servlet。
第二个动作是给库存扣减加一个对账机制。每天日结时跑一条 SQL,对比product.stock和「初始库存 - 销售明细汇总」,不一致的就报警。收银系统最怕账实不符,这个对账脚本能帮你提前发现问题。
-- 库存对账:找出账面库存和实际销售不符的商品 SELECT p.id, p.name, p.stock AS book_stock, p.init_stock - IFNULL(SUM(si.qty), 0) AS should_stock FROM product p LEFT JOIN sale_item si ON p.id = si.product_id GROUP BY p.id HAVING p.stock <> should_stock;第三个动作是把 JSP 里的 Java 代码片段逐步替换成 JSTL 和 EL 表达式。源码里如果到处是<% ... %>,维护起来很痛苦。替换成<c:forEach>和${item.name}之后,页面会干净很多,也更容易交给前端改样式。
我自己的习惯是:拿到任何一套 JavaWeb 源码,先不急着改功能,先把它在本地跑通、把数据库对一遍、把登录和结算两条链路走通,然后再动第一行代码。这套收银系统源码的价值不在于它现在有多完善,而在于它给了你一个真实的、能摸到的业务骨架,你在这个骨架上补的每一刀,都比从零写一个 CRUD 学到的多。希望帮到你。
本文还有配套的精品资源,点击获取