
简介这是一套基于Java的超市收银系统设计源码由狂神说团队开发面向JavaWeb初学者与课程设计、毕业设计人群帮助读者理解收银业务从商品管理到结算的完整实现。压缩包共123个文件约561KB其中Java源文件33个承担核心业务逻辑21个jsp与23个js、5个css共同构成前端页面与交互样式另有9个xml配置、1个sql初始化脚本及若干图片资源整体结构清晰、便于按模块阅读。项目采用MVC设计模式将业务逻辑、数据访问与用户界面分离配合Maven的pom.xml管理依赖具备良好的扩展性与维护性。目前已有551人学习下载适合作为JavaWeb练习项目参考读者可从中获取完整的收银系统实现思路、数据库脚本与前后端协作方式用于二次开发或课程实践。1. 从一张小票说起狂神说超市收银系统源码到底能跑出什么很多人第一次搜「基于Java的狂神说超市收银系统设计源码」脑子里想的其实不是「我要读一套源码」而是「我要交一个能演示、能答辩、能改的课程设计」。这个标题背后是一套典型的 Java 桌面/Web 收银系统商品管理、会员管理、收银结算、库存扣减、销售流水、小票打印通常用 Swing 或 JavaFX 做界面MySQL 存数据JDBC 或 MyBatis 做持久层。它解决的是「从零写一个能跑通的业务闭环」这件事适合 Java 基础刚过、想拿一个完整项目练手的学生也适合想快速搭一套小型零售收银原型的工程师。真正值钱的地方不是界面多漂亮而是收银这条主链路怎么保证「扫码→算钱→扣库存→出小票」四步不翻车。2. 收银主链路拆解从商品扫码到库存扣减的完整闭环2.1 为什么先定分层再写代码拿到一套收银系统源码最忌讳上来就改界面。收银系统的复杂度不在 UI而在业务状态的流转一笔订单生成后库存要扣、会员积分要加、销售流水要落库、小票要能重打。如果这些逻辑全塞在按钮的点击事件里改一个需求就会牵动全身。常见做法是分四层实体层Product、Order、OrderItem、Member、DAO 层负责 SQL、Service 层负责事务和业务规则、UI 层只负责展示和收集输入。狂神说这类教学项目一般会把这四层放在entity、dao、service、view四个包里你接手时先按包名把职责认清楚再动手。分层之后收银主链路的调用顺序就固定了UI 收集商品条码 → Service 查商品并校验库存 → 累加订单明细 → 结算时开启事务 → 写订单主表、写订单明细、扣库存、加积分 → 提交事务 → 返回小票数据。这条链路里唯一必须用事务包住的是「写订单 扣库存 加积分」这三步少一步都会出现「钱收了库存没扣」的对账事故。2.2 商品扫码与订单明细累加的实现扫码的本质是「用条码换商品对象」。真实超市的条码枪相当于一个键盘扫一下就把条码字符串加回车输入到焦点框里。所以代码里不需要什么特殊驱动监听输入框的回车事件即可。下面这段是订单明细累加的核心逻辑用 Map 做去重累加避免同一商品扫两次生成两条明细。// OrderService.java 片段扫码加入订单 public void scanBarcode(String barcode, Order order) { // 1. 按条码查商品查不到直接抛业务异常 Product product productDao.findByBarcode(barcode); if (product null) { throw new BizException(条码不存在: barcode); } // 2. 库存校验收银时不允许超卖 if (product.getStock() 0) { throw new BizException(库存不足: product.getName()); } // 3. 同一商品累加数量而不是新增明细行 OrderItem exist order.findItem(product.getId()); if (exist ! null) { exist.setQuantity(exist.getQuantity() 1); } else { OrderItem item new OrderItem(); item.setProductId(product.getId()); item.setProductName(product.getName()); item.setPrice(product.getPrice()); item.setQuantity(1); order.getItems().add(item); } // 4. 重算订单总额金额用 BigDecimal别用 double order.recalcTotal(); }逻辑说明第一步查商品是整条链路的入口查不到必须中断不能静默跳过否则收银员会以为扫上了。第二步库存校验放在加购阶段而不是结算阶段是为了让收银员当场发现缺货而不是结账时才报错。第三步用「先查再累加」而不是直接 add是因为同一商品重复扫码在收银场景里极其常见。第四步重算总额注意金额字段一律用BigDecimaldouble在 0.10.2 这种场景会给你算出 0.30000000000000004小票金额对不上就是从这里来的。参数说明barcode是条码枪输入的字符串注意前后可能有空格入库前trim()order是当前挂单对象通常放在内存里切换挂单时序列化到本地或数据库product.getStock()是实时库存如果系统支持多终端同时收银这里的库存校验只能算「软校验」真正的扣减要靠数据库行锁兜底。2.3 结算事务与库存扣减的 SQL 写法结算这一步是整个系统最容易出对账问题的地方。常见做法是把「写订单、写明细、扣库存、加积分」放进一个 Service 方法用Connection的手动提交控制事务。下面给出核心 SQL 和事务骨架。// OrderService.java 片段结算 public void checkout(Order order, Member member) { Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交开启事务 // 1. 写订单主表 orderDao.insertOrder(conn, order); // 2. 写订单明细 for (OrderItem item : order.getItems()) { orderItemDao.insert(conn, order.getId(), item); // 3. 扣库存带 stock ? 条件防止扣成负数 int rows productDao.reduceStock(conn, item.getProductId(), item.getQuantity()); if (rows 0) { throw new BizException(库存扣减失败可能已被其他终端售出); } } // 4. 会员积分累加 if (member ! null) { memberDao.addPoints(conn, member.getId(), order.getTotal().intValue()); } conn.commit(); // 全部成功才提交 } catch (Exception e) { if (conn ! null) conn.rollback(); // 任何一步失败整体回滚 throw new BizException(结算失败: e.getMessage()); } finally { DBUtil.close(conn); } }逻辑说明setAutoCommit(false)是手动事务的开关忘记写这句后面所有操作都是各自独立提交回滚等于没写。扣库存的 SQL 必须带条件正确写法是UPDATE product SET stock stock - ? WHERE id ? AND stock ?靠数据库的行级判断保证不会扣成负数而不是先查再扣——先查再扣在并发下必然超卖。rows 0说明库存不够或商品被删直接抛异常触发回滚。参数说明item.getQuantity()是购买数量扣减时按数量扣而不是按 1 扣order.getTotal().intValue()是积分换算真实项目里积分比例通常是可配置的别写死DBUtil是连接工具类教学项目里常见的是自己封装的 JDBC 工具生产项目建议换成连接池HikariCP 或 Druid否则并发一上来连接就爆。3. 数据库表设计与 MyBatis 映射收银系统绕不开的六张表3.1 六张核心表的最小字段集收银系统的表不用多但每张表的字段要想清楚。下面这套是能支撑完整收银闭环的最小集合字段名按常见教学项目习惯给出你接手源码时对照着看即可。表名作用关键字段注意点product商品id, barcode, name, price, stock, category_idbarcode 建唯一索引category分类id, name用于商品筛选member会员id, phone, name, points, balancephone 建唯一索引orders订单主表id, order_no, member_id, total, pay_type, create_timeorder_no 唯一防重order_item订单明细id, order_id, product_id, price, quantity存下单时价格快照stock_log库存流水id, product_id, change, type, create_time对账靠它别省order_item里的price必须存「下单那一刻的价格」不能只存 product_id 去关联查。原因很直接商品调价后历史订单的小票金额必须还是当时的价格否则财务对账永远对不平。stock_log这张表很多教学项目会省掉但一旦出现「库存对不上」的投诉没有流水就只能靠猜这是血泪经验。3.2 MyBatis 映射文件里最容易写错的三个点如果源码用的是 MyBatismapper.xml里有三个地方新手最容易翻车。第一是resultMap的字段映射数据库用下划线命名product_idJava 用驼峰productId不开mapUnderscoreToCamelCase就必须手写映射否则查出来全是 null。第二是#{}和${}的区别收银系统里所有用户输入条码、手机号都必须用#{}预编译用${}拼接就是 SQL 注入的口子。第三是批量插入明细时foreach的写法分隔符写错会直接报语法错误。!-- OrderItemMapper.xml 片段批量插入订单明细 -- insert idbatchInsert INSERT INTO order_item (order_id, product_id, price, quantity) VALUES foreach collectionitems itemit separator, (#{it.orderId}, #{it.productId}, #{it.price}, #{it.quantity}) /foreach /insert逻辑说明foreach的separator,负责在每组值之间加逗号collection指向传入的集合名。批量插入比循环单条插入快一个数量级一笔订单十几条明细时差别不明显但日结批量补数据时差距就出来了。参数说明items是 List元素里必须包含 orderId如果 orderId 是插入主表后才生成的自增 ID就要么先插主表拿 ID 再批量插明细要么用useGeneratedKeys回填。3.3 订单号生成别用时间戳硬拼订单号是收银系统的身份证重复订单号会导致小票重打、对账混乱。常见做法是「日期 终端号 自增序列」比如20240520-01-0001。不要用System.currentTimeMillis()单独做订单号同一毫秒内两笔订单会撞号也不要用 UUID太长且无序索引性能差。稳妥做法是数据库建一张序列表或者用 Redis 的INCR单机教学项目用synchronized加内存计数器也能顶住。4. 避坑与排查收银系统上线前必须过的五道坎4.1 金额用 double 导致小票对不上现象订单明细相加和订单总额差几分钱或者 3 件 9.9 元的商品算出 29.699999999999996。原因double是二进制浮点无法精确表示 0.1 这类十进制小数。解决所有金额字段用BigDecimal数据库用DECIMAL(10,2)Java 里构造BigDecimal用字符串构造器new BigDecimal(9.9)不要用new BigDecimal(9.9)后者会把 double 的误差原样带进来。4.2 库存扣成负数现象两个收银终端同时卖最后一件商品两边都结算成功库存变成 -1。原因先SELECT查库存再UPDATE扣减两步之间没有锁并发下都读到 1都以为够。解决扣减 SQL 写成UPDATE product SET stock stock - ? WHERE id ? AND stock ?用数据库的行锁和条件判断兜底判断affectedRows是否为 0。4.3 事务没生效回滚等于没写现象结算时扣库存失败抛了异常但订单主表已经写进去了出现「有订单没扣库存」。原因Connection没关自动提交或者用了 Spring 但Transactional方法被同类内部调用代理没生效。解决JDBC 手动事务确认setAutoCommit(false)在第一步Spring 项目确认事务方法是从外部类调进来的或者用AopContext.currentProxy()拿代理再调。4.4 条码枪输入带隐藏字符现象手动输入条码能查到商品条码枪扫就是查不到。原因条码枪在条码前后可能附加回车、换行或不可见字符直接拿去查数据库匹配不上。解决入库和查询前统一trim()必要时用正则去掉非数字字符调试时把条码System.out.println出来看长度多一个字符就是它。4.5 小票重打导致重复扣库存现象收银员点了打印打印机没反应再点一次结果库存扣了两次。原因打印动作和结算动作耦合在一起点一次打印就触发一次结算。解决结算和打印分离结算只负责生成订单并落库打印只负责读订单渲染小票重打时按订单号查已有订单绝不重新走结算逻辑。订单号唯一索引是最后一道防线。5. 从能跑到好用收银系统的三个进阶改造点5.1 用挂单和取单支撑真实收银节奏真实收银台经常遇到顾客东西没拿全就去补货的情况这时候需要「挂单」把当前订单暂存先服务下一位回头再取出来继续扫。实现上就是把内存里的Order对象序列化到本地文件或数据库的hold_order表取单时反序列化回来。关键点是挂单不扣库存、不生成订单号只有结算才走完整事务。这个功能教学项目里往往没有但它是区分「能演示」和「能上岗」的分水岭。5.2 加一层对账查询让库存差异可追溯库存对不上是收银系统最常见的投诉。与其事后靠猜不如提前加一个对账视图按商品汇总stock_log的增减和product.stock当前值比对不一致就标红。stock_log的type字段区分「销售扣减」「进货增加」「盘点调整」「退货回补」每次库存变动都写一条。这样一旦差异出现直接查流水就能定位是哪一笔操作漏写了日志而不是全表翻数据。5.3 收银小票的打印格式与字符集小票打印机热敏 58mm 或 80mm通常走 ESC/POS 指令集Java 里通过javax.print或直接写串口/USB 输出。最容易翻车的是字符集中文小票必须用 GBK 或打印机支持的编码用 UTF-8 打出来就是乱码。另外小票宽度有限商品名太长要截断或换行金额右对齐。调试阶段先用「打印到文件」的方式把指令流存下来确认格式对了再接真机能省掉大量纸张。// 小票行格式化商品名左对齐金额右对齐 public String formatLine(String name, BigDecimal price) { int width 32; // 58mm 小票约 32 个半角字符 String priceStr price.setScale(2, RoundingMode.HALF_UP).toString(); // 中文按 2 个半角宽度算简单处理超长截断 int nameWidth width - priceStr.length(); if (name.length() nameWidth) { name name.substring(0, nameWidth - 1) …; } return String.format(%- nameWidth s%s, name, priceStr); }逻辑说明setScale(2, RoundingMode.HALF_UP)保证金额两位小数且四舍五入避免出现 9.9000000001 这种脏数据。String.format的%-Ns是左对齐补空格把商品名撑到指定宽度金额自然就右对齐了。参数说明width按打印机型号调整58mm 一般 32 字符80mm 一般 48 字符中文宽度处理这里做了简化严格做法是按字符的显示宽度逐个累加遇到中文算 2。我自己接手这类源码的习惯是先不改任何业务代码把「扫码→结算→查订单→重打小票」这条链路手动跑通三遍确认每一步的数据库变化都符合预期再动界面和扩展功能。很多翻车不是因为代码难而是因为没先摸清主链路就急着加需求。希望帮到你。本文还有配套的精品资源点击获取