
简介一套面向小型超市的收银系统源码以添加商品、商品销售、商品查询和计算器小工具为核心覆盖了收银结算、库存跟踪、会员积分、促销折扣等日常运营场景适合作为计算机专业毕业设计选题也适合开发者快速上手商业管理类项目。压缩包为rar格式约3.3MB据资源说明内含源代码、数据库配置及用户手册等配套内容可支撑从环境搭建到功能调试的完整学习。系统按业务模块划分清晰在商品管理部分关注条形码快速录入与库存预警在销售部分突出多种支付方式和退货退款流程同时具备报表分析与员工权限设计便于理解小型超市的前台收银与后台管理一体化思路。通过研读这套源码能学习数据库表结构设计、前端交互界面开发、后端业务逻辑处理以及系统集成方法并借鉴其目录结构与工程组织方式。目前已有3114人学习资源热度较高适合课程实践或作为商业项目开发入门参考。1. 超市收银系统源码一套能跑起来的收银台比想象中少一半工作量在技术社区里搜“超市收银系统源码”结果一大把但能扛住早高峰收银台的没几个。很多代码只是把商品表包了一层增删改查扫码枪一接就乱码库存扣重小票缺行日结对不上账。其实一套最小可用的超市收银系统核心只有四件事扫码加购、结算收钱、扣库存、出报表。下面按从业者常走的路线从选型到建表从交易链路到部署排错把源码跑通需要知道的细节一次讲透。这篇笔记适合准备做课程设计、小店换系统的同行也适合第一次接收银项目外包的开发者。2. 收银系统源码怎么选型Java Spring Boot 还是 Python Django先看你的场景2.1 单体应用为主流前后端分离不是必选项很多收银源码一上来就是 Vue Spring Boot 前后端分离看着现代但对小店反而是累赘。收银台屏幕就一块触屏交互密度远低于后台管理系统页面不需要那么多异步刷新部署在本地 Windows 收银机上一套内嵌页面比两个服务更省心。常见做法是用单体架构后端渲染收银员页面数据接口和页面同在一个进程里出一张工单就能解决全部问题。选型可以从三条线判断店员培训成本、维护复杂度和未来扩展空间。如果后面要接微信支付宝扫码支付、电子秤、会员系统Spring Boot 生态的支付 SDK 和串口通信资料最全如果只是想快速改现有源码、用 Python 写得更顺Django 自带 admin 后台能少写很多页面。但我的建议是 Java理由不是性能而是市面上能搜到的收银系统源码、商业开源项目大多集中在这个栈你踩过的坑别人早就踩过查答案更容易。如果你手上的源码是 PHP 写的也不是不能跑。PHP 部署简单二手收银机装上 Apache 就能用但后续接摄像头扫码识别、电子秤串口、离线缓存都比较费劲短生命周期模型让常驻服务和队列变得别扭做小票打印时经常要挂第三方扩展。所以我的排序是 Java C# Python PHP优先级完全按“收银硬件驱动的成熟度”来排。2.2 用 Spring Boot 搭出最小可运行工程以 Spring Boot 3.x MySQL 8.0 Thymeleaf 为例建一个叫 pos-server 的 Maven 工程。先不管界面只要把一个商品查询接口跑通骨架就立住了。用命令行生成工程骨架最省事mvn archetype:generate -DgroupIdcom.supermarket \ -DartifactIdpos-server \ -DarchetypeArtifactIdmaven-archetype-quickstart \ -DinteractiveModefalse这条命令把 Maven 标准骨架拉下来生成的工程不算现代所以大多数人还是去 Spring Initializr 页面生成。重点不是骨架命令而是 pom.xml 里必须带上 spring-boot-starter-web、spring-boot-starter-thymeleaf、mybatis-plus 和 mysql-connector-j 这四个依赖。mybatis-plus 能省掉大量单表 CRUD收银这类业务的核心逻辑在交易不应该把时间耗在商品表的增删改查上。骨架出来后写一个最小 Controller 验证工程能跑RestController RequestMapping(/api/goods) public class GoodsController { private final GoodsMapper goodsMapper; public GoodsController(GoodsMapper goodsMapper) { this.goodsMapper goodsMapper; } GetMapping(/barcode/{barcode}) public Goods findGoods(PathVariable String barcode) { return goodsMapper.findByBarcode(barcode); } }这段代码逻辑很直白扫码枪扫到的条码作为路径参数传进来Mapper 去商品表查对应记录。注意这里故意没有做空值判断条码不存在时返回 null页面拿不到对象会直接白屏。这是许多接手的收银源码翻车的第一个点第 5 章会专门讲怎么兜底。参数 barcode 在 URL 里出现时条码基本是纯数字路径参数问题不大但还是建议改成 GET 参数?barcodexxx更稳妥。第一次跑起来后控制台看到Tomcat started on port 8080就说明工程没问题。用浏览器访问/api/goods/barcode/6901028105653能返回一条商品 JSON 就是通了。此时数据库里那条商品的 barcode 要对得上否则返回 null 也正常问题多半出在初始化数据没导全。2.3 源码目录结构收银台、后台、报表各自归位拿到源码先别急着跑先看包结构。一个值得在上面改业务的收银系统源码目录至少是这种形态pos-server ├── controller # 收银台接口、后台接口、报表接口 ├── service # 交易、库存、会员、报表业务 ├── mapper # MyBatis 数据访问 ├── entity # 商品、订单、订单明细、流水实体 ├── config # 拦截器、打印配置、支付配置 └── resources ├── mapper # XML 里放复杂报表 SQL ├── templates # 收银台页面 └── application.yml如果拿到手的源码把商品增删改查全写在 Controller 里或者 SQL 直接埋在 Service 里那改库存逻辑时会非常难受。收银系统的核心是交易链路链路中间任何一步改动都会牵动商品、订单、库存、流水四张表分层不清的源码改一处炸一处。判断源码质量的第一个动作就看 trade 相关代码有没有独立成 service而不是散落在 Controller 和 Mapper 里。resources/mapper 目录也不要只放 ORM 生成的 CRUD XML。日结报表、毛利统计这类 SQL 要放到这里用 MyBatis 的 XML 写不然在 Java 里拼字符串 SQL 很难维护。你拿到源码时可以打开这个目录看如果里面只有 BaseResultMap 和 selectByPrimaryKey那这个源码的业务基本没沉淀下来后面所有报表都得自己补。2.4 收银机运行环境Windows 是默认主场Linux 是报表服务器收银机普遍是 Windows因为小票打印机和扫码枪的驱动基本都是 Windows 优先。源码如果只提供 Linux 部署脚本接上打印机就是一场灾难。最省心的方案是MySQL 装在一台旧电脑当服务器Windows 收银机通过局域网连接Java 服务就部署在那台 Windows 收银机上。这么做的好处是断外网也能收银因为数据库和收银服务都在一张局域网里。很多人一开始就把系统部署在云服务器上结果收银机一断外网整个门店停摆这是选型时就该避开的坑。数据访问层的配置建议把 localhost 换成 MySQL 的实际 IP方便以后扩第二台收银机。数据库备份也要在选型时考虑。Windows 服务器上直接用 mysqldump 加计划任务每天晚上 22 点导出一次保留最近 7 份。别把鸡蛋放一个篮子里收银系统的账本出问题比收银机硬盘坏了严重得多。3. 数据库设计先行商品、库存、订单与流水的核心表和索引3.1 商品表与库存表条码是唯一索引不是主键商品表是收银系统的地基。大部分源码会用自增 id 做主键条码字段加唯一索引这是正确的做法。先看建表语句CREATE TABLE t_goods ( id BIGINT PRIMARY KEY AUTO_INCREMENT, barcode VARCHAR(32) NOT NULL, name VARCHAR(128) NOT NULL, spec VARCHAR(64), -- 规格如 500ml/瓶 unit VARCHAR(8), -- 单位个、瓶、包 sale_price DECIMAL(10,2) NOT NULL, status TINYINT DEFAULT 1, -- 1上架 0下架 category_id BIGINT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_barcode (barcode) );这里有个容易混淆的点为什么不用条码当主键因为条码规则不在自己手里供应商一种商品可能发好几个条码散装商品有时候是称重后临时生成的电子秤条码这种条码在数据库里是重复创建的。自增主键虽然没业务含义但做订单明细外键关联时查询更快。sale_price 必须用 DECIMAL(10,2)不要用 FLOAT小数运算会累积误差。商品表里不要放库存字段库存是变化量每次销售都要 UPDATE放在商品表里会让商品查询和库存扣减互相锁行早高峰容易卡。category_id 可以做普通索引也可以不做因为超市商品分类就那几个全表扫描也比走索引快。真正要关注的是 status 字段查询时默认带上status 1下架商品不能出现在扫码结果里。很多源码忘了这个条件导致旧条码还能扫出商品店员还以为系统出了 bug。3.2 订单主表与明细表为什么必须拆两张表订单保存时不拆主表和明细表是新手源码最常见的病。只存一张表意味着退货、挂单、报表统计全都绕不开重复数据。正确做法是两张表主表存一次交易的公共信息明细表存这次交易里每个商品的一行记录。先看订单主表CREATE TABLE t_order ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) NOT NULL, total_amount DECIMAL(10,2) NOT NULL, discount_amount DECIMAL(10,2) DEFAULT 0.00, payable_amount DECIMAL(10,2) NOT NULL, payment_type TINYINT NOT NULL, -- 1现金 2微信 3支付宝 4会员卡 cashier_id BIGINT NOT NULL, store_id BIGINT, status TINYINT DEFAULT 1, -- 1正常 2已退货 3挂单作废 create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_order_no (order_no) );然后订单明细表CREATE TABLE t_order_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, goods_id BIGINT NOT NULL, barcode VARCHAR(32), goods_name VARCHAR(128), price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, subtotal DECIMAL(10,2) NOT NULL, KEY idx_order_id (order_id) );明细表里冗余 barcode 和 goods_name不是为了省一次关联查询而是为了历史订单可追溯。商品价格今天可以改但三个月前的订单里那瓶水就值当时的价格。如果只存 goods_id报表系统重新关联商品表一旦商品被删或改价历史订单就错乱了。重点看 goods_name 和 price 这种冗余字段这是商用系统必备的设计很多课设源码里没有。注意 total_amount、discount_amount、payable_amount 这三个字段是有关系的total_amount 是原价合计discount_amount 是整单折扣payable_amount 是最终实收。不要让 payable_amount 直接等于前端传进来的数字后端要用 total 减 discount 重新算一遍。这也是对账不平的最大来源之一。3.3 库存表与流水表扣减和核对都靠它们库存不放在商品表单独开一张库存表CREATE TABLE t_stock ( goods_id BIGINT PRIMARY KEY, quantity INT NOT NULL DEFAULT 0, warn_quantity INT DEFAULT 10, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );库存变化必须记流水。收银系统不只是卖货还有入库、退货、盘点、报损。没有流水表的源码一旦库存对不上只能人肉翻订单。流水表的常见设计CREATE TABLE t_stock_flow ( id BIGINT PRIMARY KEY AUTO_INCREMENT, goods_id BIGINT NOT NULL, change_quantity INT NOT NULL, -- 正数入库负数出库 before_quantity INT NOT NULL, after_quantity INT NOT NULL, biz_type VARCHAR(16) NOT NULL, -- SALE/RETURN/PURCHASE/CHECK order_no VARCHAR(32), operator_id BIGINT, create_time DATETIME DEFAULT CURRENT_TIMESTAMP );流水表必须记录 before_quantity 和 after_quantity这两个字段不是冗余而是对账时能还原每一个操作的历史。比如一次退货的 change_quantity 是 1如果没有 before 和 after你只能知道加回来了但不知道当时库存基数。流水表不要加唯一索引因为同一秒可能有多笔交易查询时按 create_time 排序就行。在应用层约定 biz_type 的取值SALE 的 change_quantity 是负数RETURN 是正数PURCHASE 是正数CHECK 是盘点差异的绝对值。把 biz_type 用枚举写在代码里禁止用魔法数字 1、2、3否则三个月后你自己都看不懂。读流水时如果发现 after - before ! change_quantity那这笔流水一定写错了调账时第一时间查这种坏数据。3.4 索引与事务边界别把收银事务拖成慢查询索引设计围绕高频查询。收银台最高频动作是扫码走 t_goods 的 uk_barcode订单查询走 uk_order_no报表日结按天扫 t_order走 create_time 索引。规划一张索引清单所属表索引名字段用途t_goodsuk_barcodebarcode扫码秒查t_orderuk_order_noorder_no订单查询、退款关联t_orderidx_create_timecreate_time日结、时段报表t_order_itemidx_order_idorder_id按订单查明细不要对 t_stock_flow 的 order_no 建索引流水表一天几万条索引维护成本高日结查询用 create_time 范围即可。这里容易犯的错是每个字段都建索引把 t_order_item 的 goods_id、barcode、name 都建一遍写入时因为索引维护太多变慢。收银系统的写入峰值是排队结账那几分钟不要让索引数量拖垮 INSERT。另一个重点是事务边界。一次收银会插入订单主表、插入明细、扣库存、写库存流水这一整套必须在一个事务里。把“扣库存”单独抽出去等订单插入成功后再另起事务扣那中间任何一步失败库存和订单就永久不一致。事务里也不要执行外部调用比如打印小票、推送通知这些应该放到事务提交后做否则打印机卡住数据库事务一直持有锁后面所有收银都卡住。4. 收银核心流程的实现从扫码到小票打印的完整链路4.1 扫码添加商品人为按回车还是自动识别条码收银台最常被误解的是扫码枪。它本质是一个键盘扫出来的内容最后会在输入框里模拟一遍按键。很多源码的条码输入框只监听 keypress遇到中文输入法就出乱码。更稳的做法是监听 keydown用事件里的 key 构造缓冲检测到 Enter 时才触发查询。参考实现const barcodeInput document.getElementById(barcode); let barcodeBuffer ; barcodeInput.addEventListener(keydown, function (e) { if (e.key Enter) { const barcode barcodeBuffer.trim(); if (barcode) { addGoodsByBarcode(barcode); } barcodeBuffer ; e.preventDefault(); } else { barcodeBuffer e.key; } }); async function addGoodsByBarcode(barcode) { const goods await fetch(/api/goods/barcode/${barcode}).then(r r.json()); if (!goods || !goods.id) { alert(商品不存在 barcode); return; } cart.add(goods); renderCart(); }为什么用 keydown 而不是 keypress高速扫码枪在 keypress 阶段可能丢事件keydown 对中英文输入法的兼容更好。barcodeBuffer 的作用是扫码枪不是一次把条码复制进输入框而是逐字模拟按键如果监听元素的 change 事件触发查询扫码枪还没扫完change 根本不会触发。这是社区源码里最常见的翻车点。4.2 购物车与结算折扣、抹零、挂单怎么算购物车放前端还是后端单机收银台放前端最快但多收银机统一结算要放后端。常见做法是前端维护购物车结算时把整单提交后端后端负责计算金额和创建订单。折扣和抹零必须放在后端算不要信任前端传过来的最终金额否则能改请求的人就能改付款金额。结算接口的简化代码PostMapping(/api/order/checkout) public Result checkout(RequestBody CheckoutRequest req) { ListItem items req.getItems(); BigDecimal total items.stream() .map(item - item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))) .reduce(BigDecimal.ZERO, BigDecimal::add); BigDecimal discount req.getDiscountAmount(); BigDecimal payable total.subtract(discount).setScale(2, RoundingMode.HALF_UP); payable payable.setScale(1, RoundingMode.DOWN); Order order OrderService.createOrder(req, total, payable); return Result.success(order); }上面代码里setScale(1, RoundingMode.DOWN)就是“分以下抹零”把 12.89 抹成 12.80如果商家要四舍五入改成 HALF_UP。金额计算全部用 BigDecimal不能用 double否则 0.10.2 会打印出 0.30000000000000004。折扣是按整单减还是按单品打折两类超市都有。整单打折简单但后续退货要按比例分摊折扣非常麻烦给单品加 discount 字段每行的 subtotal 已经是折后价退货时按明细退就行。建议源码实现尽量走单品折扣日后会轻松很多。4.3 支付与打印对接支付通道前先让本地记账闭环很多源码宣称已对接微信支付但没有商户号根本验不了。真实开店场景里很多小店用的是 POS 机或扫码盒子收钱收银系统只需要记录付款方式把“已收款”作为打印小票的前提。也就是说支付动作在线下完成系统记账在线上二者通过支付参考号关联。这个设计虽然不叫支付对接但已经满足店面财务需要比硬接一套支付 SDK 更可靠。小票打印又是一个坑。小票打印机大多走 ESC/POS 指令通过串口或网口发送。在 Java 里常见做法是用 jpos 或用系统打印命令。最小可用方案是把小票排版成纯文本通过系统打印命令输出printf 商品名 数量 金额\n%s $line | lp -d receipt_printerWindows 收银机上更常见的做法是页面调 window.print()让浏览器弹打印窗口。浏览器打印的好处是不用装驱动坏处是用户能改打印设置。商用场景建议直接用 ESC/POS 指令但打印模块必须独立成服务别和结算逻辑耦合否则打印机型号一换整个收银流程都要动。4.4 库存扣减先记账再扣库存且扣减必须带条件库存扣减是全部源码里最容易写错的一处。顺序要固定在“创建订单 → 扣库存 → 写库存流水”这一个事务内。参考实现Transactional public Order createOrderWithStock(Order order, ListItem items) { orderMapper.insert(order); for (Item item : items) { int affected stockMapper.deduct(item.getGoodsId(), item.getQuantity()); if (affected 0) { throw new RuntimeException(库存不足 item.getGoodsName()); } } return order; }配合的 SQL 是关键不能用裸 UPDATEUPDATE t_stock SET quantity quantity - #{quantity} WHERE goods_id #{goodsId} AND quantity #{quantity};这条 UPDATE 的作用是让数据库在扣减的同一时刻判断库存是否足够affect rows 为 0 说明库存不足或商品不存在。不要先 SELECT 再判断再 UPDATE那种写法在并发收银时必然超卖。等 UPDATE 成功后流水表里的 after_quantity 可以直接用当前库存加回 quantity 推算出不必再查一次。写流水也要放在同一个事务里事务提交失败时流水回滚库存和订单一起回到原状。4.5 挂单与退货收银台上的两个高频操作挂单和退货最容易被课设源码漏掉。挂单就是当前顾客还没凑齐商品先接待下一位之后恢复购物车重新收银。最简单的实现是建一张 hang_order 表把购物车 JSON 序列化存进去恢复时反序列化。注意挂单表要记录挂单时间、收银员和购物车数据恢复时只能由同一账号操作避免交接班时互相覆盖。退货不能直接删原订单。正确做法是在原订单上标记退货状态同时生成一张退货单记录原订单号再把商品库存加回来。为什么不能删订单一旦删了日结报表、库存流水、财务审计全部断链。如果源码里只有“删除订单然后恢复库存”直接判死刑这家系统换掉是早晚的事。5. 部署与避坑收银系统跑不起来的 5 个常见坑5.1 本地部署最小步骤先跑通课设源码再谈换生产系统在换掉现有收银机之前先在本地把源码跑起来。需要 JDK 17、MySQL 8.0不需要 Redis。最小部署命令git clone 你找到的源码仓库 cd pos-server mvn clean package -DskipTests java -jar target/pos-server.jar --spring.profiles.activelocal大多数课设源码自带 init.sql先执行它建库建表再启动服务。启动前把本地的数据源配上server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/pos?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: root thymeleaf: cache: false mybatis-plus: configuration: map-underscore-to-camel-case: true配置里最容易踩的坑是数据库驱动版本和 MySQL 8 认证方式。MySQL 8 默认用 caching_sha2_password老项目的 mysql-connector-java 5.x 连不上会报 unable to load authentication plugin。遇到这种问题把依赖换成 mysql-connector-j 8.x。启动后访问 http://localhost:8080/index 登录收银台如果白屏优先看后端日志里的模板路径和数据库 SQL 报错多半是表名前缀或驼峰映射没配好。5.2 坑一扫码枪乱码同一件商品扫出两个名字现象切到中文输入法后扫码商品名变成一串乱码或者同一件商品不同收银员扫出来的结果不一样。原因扫码枪输出的是 ASCII 字符中文输入法把键盘事件组合成汉字或吞掉部分按键。很多收银系统源码只监听 keypress输入法状态下 keypress 拿到的是组合字符不是原始扫码值。解决前端监听 keydown用 e.code 判断物理按键不要用 e.key 的字符值。再给条码输入框加 autocompleteoff 和 styleime-mode: disabled旧内核有效更可靠的是在扫码枪驱动里把输出模式调成英文。如果扫码枪支持配置后缀把后缀改成一个不常用的按键比如 Tab也能减少误触发。最直接的办法是让收银员固定使用英文输入法但这个靠制度不如靠代码。5.3 坑二并发收银超卖库存变负数现象早高峰两台收银机同时扫同一件饮料结完账后库存变 -2。原因代码先 SELECT 再 UPDATE两个事务读到同一个库存值都判断库存够然后各自扣减。解决换成 4.4 里那条带库存条件的 UPDATE 语句。注意这里事务隔离级别默认 REPEATABLE_READ 不影响行锁在 UPDATE 执行时才加条件里写 quantity #{quantity} 会让第二个并发事务阻塞等第一个提交后再判断库存不足时 affected rows 为 0直接抛异常回滚订单。压测时可以把库存设成 1再并发下两个单验证最终库存不是负数。5.4 坑三小票打印缺行或走纸异常现象小票中间多空一行或者一单走了两次纸偶尔第一行被切掉。原因模板里用了br导致打印驱动多走一行打印指令没发初始化命令打印机保留上一次排版状态Windows 打印队列里上一单没清空。解决每次打印前先发送 ESC 十六进制 1B 40初始化打印机再发文本最后发切纸命令。如果走纸量不对检查打印驱动里纸张类型是 58mm 还是 80mm。浏览器打印方案则要用 window.onload 或等渲染完成再触发 window.print()否则页面还没铺满用户看到的就是缺行。5.5 坑四交接班对账不平订单金额和流水对不上现象早班交班时系统里订单金额总和比收银员手里现金少 3.5 元而且定位不到是哪一单。原因结算时用 double 计算金额或者做整单折扣时没分摊到明细导致明细加总不等于订单汇总。对账脚本把 HAVING 条件写错比较漏掉。解决金额统一切成 DECIMAL BigDecimal打折先算到每个商品再合计确保 t_order_item.subtotal 之和等于 t_order.total_amount。用下面这条 SQL 找出差异单SELECT order_id, SUM(subtotal) AS item_total, total_amount FROM t_order_item GROUP BY order_id HAVING item_total ! total_amount;这条 SQL 在日结前跑一次能直接列出对不上的订单。如果你接手的源码连 HAVING 都还没有说明作者根本没想过对账这回事。5.6 坑五定时任务重复跑日结报表翻倍现象第二天早上看日结报表营业额是前一天的两倍或者库存被重复扣了一次。原因定时任务没有幂等控制到点后两个实例各跑一次日结或者因为数据库锁等待被调度器重试。解决日结任务加批次号每次跑之前先查任务表有没有相同日期批次记录有就跳过没有先插入再生成报表。更简单粗暴也有效的办法是在任务表日期字段上建唯一索引重复插入会报错捕获这个 DuplicateKeyException 就说明已有任务跑完。注意日结生成报表、更新库存预警、清缓存这三步必须放在一起全部成功后才标记完成否则第二天库存同步就会漏。6. 进阶优化从能收到能用再到敢换掉原来的系统6.1 用并发脚本验证收银接口提前发现超卖源码跑起来之后先别急着接真机。写一个简单脚本模拟 20 个并发请求同时下单看库存是否为负。参考脚本import concurrent.futures import requests def checkout(i): payload {items: [{goodsId: 1, quantity: 1}], discountAmount: 0} r requests.post(http://localhost:8080/api/order/checkout, jsonpayload) return r.json() with concurrent.futures.ThreadPoolExecutor(max_workers20) as pool: results list(pool.map(checkout, range(20))) failed [r for r in results if r.get(code) ! 0] print(fail count:, len(failed))跑完去查 t_stock 表如果 quantity 小于 0说明扣库存逻辑没带条件如果 fail count 远大于预期可能存在重复订单或风控拦截。压测前把商品库存恢复到一个已知值否则结果没法判断。这个脚本已经是半个接口测试建议保留下来每次改动交易链路都跑一遍。6.2 离线收银与断网兜底超市收银系统最怕断网。源码里如果没有离线缓存断网时收银台一片空白。常见做法是本地 SQLite 缓存商品表和订单表网络恢复后把订单同步到服务器。最小实现是收银台页面联机时每 5 分钟把商品全量表拉到内存或本地断网后扫码走本地库订单先写本地恢复后批量上传。这个功能复杂度不低但它是老板敢换系统的关键。如果你的源码没有离线能力至少把 MySQL 和收银服务部署在同一局域网里能扛住外网抖动。6.3 操作日志与权限后端把关别只藏在前端菜单权限模型两张大表就能搞定t_user、t_role。收银员只拥有收银台菜单权限店长拥有报表和商品建档权限。注意权限校验必须放在后端拦截器里不能只在前端菜单上隐藏按钮因为收银机是共用的有人直接改 URL 就能访问后台接口。我见过最离谱的源码后台接口没有任何校验扫码枪一改 URL 直接进数据库。这样的系统上线等于把账本摊在柜台上。所有改价、退货、挂单恢复、库存调整操作都要写操作日志。日志表至少包含 operator_id、action、before_value、after_value、create_time。改价这件事尤其重要没有日志的改价等于给收银员留了一扇门。6.4 源码转生产系统前必须检查的 5 个边界检查项必须达到的行为常见源码通病断电恢复收银机重启后未打印小票能补打订单只放内存断电丢单退货退货要关联原订单号不修改原单直接删除原单再恢复库存改价店长可改价但留下改价日志任意收银员都能改价会员价会员结算按等级折扣明细可追溯只在总金额上打折明细对不上盘点盘点冻结库存按实盘数回写直接改库存表无审批这些边界不要求第一版全做但源码里至少要预留字段和模块位置。我自己接手过好几套收银系统源码最容易翻车的不是功能不够而是把课设代码直接当生产系统用。如果你准备从源码起步按上面几章的路径先本地跑通再用并发脚本和这张边界清单过一遍再决定要不要替换现有收银机。希望帮到你。本文还有配套的精品资源点击获取