
简介面向小型超市的收银系统源码毕业设计项目旨在解决基础收银操作与门店管理效率问题适合计算机相关专业学生进行实际业务系统开发练习。系统覆盖添加商品、商品销售、商品查询和计算器等核心模块并延伸至库存管理、会员管理、销售报表、支付接口、退货退款与权限管理等辅助功能可作为理解数据库设计、前端界面与后端逻辑整合的完整示例。资源以RAR压缩包形式提供整体约3.3MB内部包含系统源代码、数据库配置及使用说明等资料便于开发者直接导入工程并对照学习。目前已有3116人浏览学习适合需要完成课程设计或毕业设计的开发者参考借鉴。该项目的模块划分与代码结构对初学者理解商业系统开发流程有直接帮助。1. 超市收银系统源码一条从扫码到扣库存的完整业务链如果你只做过增删改查第一次打开超市收银系统源码最容易犯的错是盯着页面看半天然后问这不就是个订单表吗实际不是。一套能落地的收银系统核心不在界面而在扫码枪扫一下商品 → 价格自动带出 → 结算收款 → 库存实时扣减 → 会员积分同时变动这条链能不能无断点地走通。这套源码正好是按这个业务闭环组织的适合课程设计、期末项目也适合小店做轻量信息化的底子。我先说结论这套东西值不值得下关键看三个点——数据库脚本全不全、库存扣减有没有事务、会员积分是否跟着订单走。后面几章我按这个顺序拆。2. 读懂这套源码的骨架模块、技术栈与数据库怎么串起来的2.1 收银系统的模块划分不只是一张订单表打开源码先不要急着启动先看包结构。收银系统的模块列表基本是固定的商品管理、分类管理、购物车/收银台、订单管理、会员管理、库存进销存、报表统计、系统用户。这套源码在功能上就是按这个列表来的后端按 controller / service / mapper 分包前端按页面目录分包。商品和订单是两个最大入口收银台操作的是商品但真正记录到库里的核心是订单主表和订单明细表其余的库存扣减、会员积分、报表统计全部是从订单这条线衍生出来的。这里有一个很多人容易忽略的设计点收银台的购物车列表只是前端的内存对象它不落库。只有点击结算之后后端才会一次性接收商品明细列表创建订单并统一处理扣库存、积分等后续动作。不要指望数据库里有一张临时购物车表那是电商系统的做法不是这套源码的做法。模块之间是单向依赖收银台依赖商品查询接口订单创建后调用库存服务与会员服务报表模块只读订单数据。这样的好处是新增一种支付方式或者会员等级规则时不需要动到核心订单逻辑。2.2 技术栈与环境前后端分离是这套源码的默认形态这套源码是比较典型的前后端分离结构后端用 Spring Boot 加 MyBatis-Plus 操作 MySQL前端用 Vue 加 Element UI通过 Axios 调后端接口。环境准备上JDK 1.8、Maven 3.6、Node.js 14 以上、MySQL 5.7 或 8.0这四样是启动前必须装好的。别在没装 Maven 的环境里直接双击 jar也别用太新的 Node 版本去跑旧前端项目后面避坑章会讲到这类问题的表现。为什么选这套组合而不是单体 JSP因为课程设计和实际小店改造场景里前后端分离的源码更容易被人看懂、更好扩展前端有一个独立的收银台页面后端接口可以用任意 HTTP 工具测试排查问题的时候能明确区分是接口错还是页面错。配置方面后端端口默认 8080前端开发服务器端口默认 9528。前端通过代理把 /api 前缀的请求转发到后端 8080。这两个端口在本地跑的时候是连通的但如果有人单独把前端 index.html 用浏览器直接打开file://就会因为跨域请求失败报网络错误。后面避坑章专门讲。2.3 数据库核心表订单流水与库存变动是两条最关键的线数据库脚本是这套源码里最值钱的部分。建表脚本里大概有 10 张表真正核心的是下面这几张。表名关键字段作用productid, barcode, name, price, cost_price, stock, unit商品档案与实时库存ordersorder_no, total_amount, discount_amount, pay_amount, pay_type, member_id, create_time, status订单主表记录一次结算结果order_itemorder_id, product_id, quantity, price, subtotal订单明细一行一种商品membercard_no, name, phone, balance, points, level会员档案、余额与积分stock_logproduct_id, change_type, change_qty, order_no, remark库存变动流水sys_userusername, password, real_name, role操作员账号订单主表与订单明细表是一对多关系。扣库存时不直接改 stock_log而是先改 product.stock再写一条 stock_log 做审计。member 表的 points 字段是冗余累计值明细记录在 member_point_log 里这样查询会员积分余额时不用聚合性能更好代价是积分变动必须和会员表更新放在同一事务里。建库的时候注意两点一是字符集要指定 utf8mb4否则商品名里的生僻字或繁体字会变问号二是 orders 这种表名在 MySQL 里不是保留字但为了避免某些环境的问题脚本里一般会写成orders带反引号。后面出现 SQL 语法错误时先看看是不是表名和关键字撞了。数据库脚本里通常还会预置一个管理员账号admin / admin123和若干测试商品、会员数据。第一次启动前先打开 SQL 文件搜索 INSERT INTO sys_user确认密码加密方式。如果源码里用的是 MD5 加密那么这个初始密码可以直接登录如果用了 BCrypt那初始密码字符串是密文前端登录后正常验证。这个点决定了你第一次能不能进去系统值得花一分钟看清楚。3. 半小时跑起来数据库初始化、后端启动与前端联调3.1 第一步建库导数据脚本里的初始数据先确认一遍拿到源码包先找 sql 目录下的 supermarket.sql。用命令行导入比用图形工具更不容易出乱码我一般这么操作mysql -uroot -p --default-character-setutf8mb4 -e CREATE DATABASE supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; mysql -uroot -p supermarket --default-character-setutf8mb4 supermarket.sql先建库再导入脚本。这样做的逻辑是脚本里的建表语句大部分没有 CREATE DATABASE 语句需要手动把库建好指定 default-character-set 是为了让导入过程全程使用 utf8mb4 编码避免表结构建好后又去手工改字符集。如果你用的是 MySQL 8.0密码策略默认比较严建库时用同样命令即可如果导入时报 Unknown collation那说明脚本是按 MySQL 5.7 的排序规则写的把 COLLATE 参数改成 utf8mb4_general_ci 就行。导完后验证一下mysql -uroot -p supermarket -e SHOW TABLES; SELECT COUNT(*) AS cnt FROM product;正常应该能看到 10 张左右的表product 表里有几十条测试商品数据。如果 product 表是空的收银台页面会扫不出来任何商品这时候别急着往前端页面找问题先回来确认数据是否导入成功。另外导入脚本之后建议顺手把 admin 账号的密码确认一下。部分源码会把初始密码用 BCrypt 加密存进 sys_user 表你没法直接从数据库里看出明文如果前端登录一直提示密码错误优先查后端源码里有没有一段初始化密码的逻辑或者直接跑一遍注册接口重设密码。3.2 第二步后端配置与启动后端工程是标准的 Maven 结构启动前要改的是 src/main/resources/application.yml 里的数据库连接server: port: 8080 spring: datasource: url: jdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: root password: 你的数据库密码 driver-class-name: com.mysql.cj.jdbc.Driverurl 里几个参数是经常出问题的逐个说characterEncodingutf8 保证读写中文不乱码serverTimezoneAsia/Shanghai 是 MySQL 8.0 必带的否则驱动报时区错误useSSLfalse 是本地调试时避免 SSL 握手失败的干扰。driver-class-name 用 com.mysql.cj.jdbc.Driver 表示驱动是 MySQL 8 系列的如果你本地还是 MySQL 5.7换 8.x 驱动也不会受影响反过来不行。改好后启动cd server # 后端工程根目录 mvn spring-boot:run如果你的环境里没有 Maven也可以先把项目打成 jar 再运行mvn clean package -DskipTests java -jar target/supermarket-server.jar启动日志里看到 Started ... in x.xxx seconds 并且没有报数据库连接异常就说明后端起来了。常见的启动失败是驱动版本不对、端口被占用。端口被占用时把 server.port 改成 8081同时前端代理也要跟着改。如果后端能启动但登录接口报错先看控制台有没有 Duplicate Key 或 Unknown column 之类的 SQL 异常。这类错误多数是数据库脚本和实体类字段没对齐比如实体里有 createBy 但表里没有。解决方式不是改代码而是先核对数据库表结构少数情况下需要手动补一列。3.3 第三步前端启动与登录前端工程在 ui 或 web 目录下启动命令是固定的两步cd ui npm install npm run devnpm install 如果卡住先确认 Node 版本不要用 npm 换源以外的偏门方式硬等。启动后浏览器访问 http://localhost:9528能看到登录页说明前端编译成功。登录页输入 admin / admin123具体以数据库脚本里的初始账号为准进去先看左侧菜单收银台、商品管理、会员管理、订单管理、报表统计这几项都在说明前后端接口已经打通。如果登录按钮转圈然后报错打开浏览器开发者工具看 Network 面板请求应该是发到 9528 端口的 /api/login。前端工程里一般有个 vue.config.js里面配置了 devServer.proxy把 /api 转发到 8080。右键点开那个失败的请求如果状态是 404 或 500是后端没起来或路由不对如果是 net::ERR_CONNECTION_REFUSED那就是代理没生效检查 vue.config.js 里的 target 端口是否和后端端口一致。3.4 验证第一个业务闭环扫条码下单登录后进入收银台页面页面上有一个商品条码输入框。手工输入测试商品条码比如 6901234567890查到后自动加入购物车再输入一个不同条码购物车变成两行。点击结算弹出收银窗口选择现金或微信点击确认。手工输入条码的本质是前端把输入框内容作为条码参数请求后端商品查询接口接口返回商品 JSON 后由前端塞进购物车数组。这个流程可以完全脱离页面用 curl 直接验证商品查询接口是否正常。可以先用一个简单的 GET 请求确认商品查询接口能按条码返回数据再回页面操作。这一步走完后去订单管理页面刚才那笔订单应该出现在列表里。去商品管理页面看刚才那两个商品的库存数量已经减少了对应数字。这就证明从扫码到扣库存的链路通了。如果扣库存了但订单列表没有记录或者订单有记录但库存没变问题出在订单创建接口的事务边界上。第 4 章我会拆解这段代码这里先记住一个排查经验订单和库存必须在同一事务前端看到的结果只是表象真正决定一致性的是后端事务是否覆盖了两个动作。这一步走完后最简单又有效的验证方法是打开数据库执行一条统计SELECT o.order_no, COUNT(i.id) AS item_count, o.pay_amount, o.create_time FROM orders o LEFT JOIN order_item i ON o.id i.order_id ORDER BY o.id DESC LIMIT 3;能查出刚才的订单且明细条数为 2业务闭环就算彻底确认了。4. 收银与库存核心代码拆解事务、并发扣减与会员积分4.1 收银核心订单创建与明细落库收银台的结算按钮最终调用的是订单创建接口。这个接口的代码通常写在 OrderController 里核心逻辑在 OrderService 的 createOrder 方法。我拆过的这类源码里方法逻辑一般是四步先根据购物车明细查出商品最新价格然后计算总价接着插入订单主表再明细批量插入。真实的代码骨架大致是这样Transactional(rollbackFor Exception.class) public OrderVO createOrder(OrderCreateRequest req) { // 1. 汇总购物车商品ID、数量 ListCartItem items req.getItems(); // 2. 查商品拿到最新价格与库存 ListProduct products productMapper.selectBatchIds( items.stream().map(CartItem::getProductId).collect(Collectors.toList())); // 3. 计算原价合计、会员折扣、实付金额 BigDecimal total BigDecimal.ZERO; for (CartItem item : items) { Product p products.stream() .filter(x - x.getId().equals(item.getProductId())).findFirst().orElse(null); total total.add(p.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 4. 组装订单主表并插入 Order order new Order(); order.setOrderNo(generateOrderNo()); order.setTotalAmount(total); order.setPayAmount(applyMemberDiscount(total, req.getMemberId())); order.setPayType(req.getPayType()); order.setStatus(1); // 1已完成 orderMapper.insert(order); // 5. 插入明细、扣库存、累计积分 insertOrderItems(order.getId(), items, products); deductStock(order.getId(), items); addMemberPoints(req.getMemberId(), order.getPayAmount()); return toVO(order); }这个方法上标注了 Transactional这是整条链安全的关键。它的含义是方法内所有数据库操作要么全部成功提交要么全部回滚。insertOrderItems 插入了一半出错前面插入的订单主表也会撤销不会出现订单在、明细缺的脏数据。使用时有两点要注意rollbackFor 指定了 Exception.class否则 Spring 默认只对运行时异常回滚自定义的受检异常会导致事务失效第二事务要放在 createOrder 这一层不能放在 Controller 层否则方法内部自调用会让事务注解失效。4.2 库存扣减为什么必须用条件更新而不是先查再改库存扣减是最容易写出并发 bug 的地方。常见错误写法是先查 stock判断大于 0再执行 update 减去数量。这个写法在单机单人操作时没问题可一旦两个收银员同时结算同一个商品两个请求都查到库存为 1都判断可以通过然后各自 update库存最终可能变成 -1。正确做法是把判断库存和扣减合并成一条带约束的 updatefor (CartItem item : items) { int updated productMapper.deductStock(item.getProductId(), item.getQuantity()); if (updated 0) { throw new BusinessException(商品库存不足); } }对应的 SQL 是UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}这条 SQL 靠 WHERE 条件里的 stock quantity 保证扣减永远不会把库存扣成负数。如果库存不够这条 update 影响行数为 0代码里通过 updated 0 抛出异常触发整个事务回滚。因为 update 会在行上加锁并发请求串行执行第二个请求到达时库存已经变小条件不满足自然被拦下。这是最简单有效的并发扣库存方案比先用 SELECT 再 UPDATE 安全得多也比重试机制直观。配套的库存日志不能漏。每次扣减都要写一条 stock_log记录商品、变动数量、来源订单号这不仅是审计需要更是后面对账时判断这一天的销售到底消耗了多少库存的依据。4.3 会员模块积分、折扣与储值消费会员的常规动作有三个积分累计、等级折扣、储值余额扣款。这套源码里会员表用 level 字段控制折扣级别用 points 存储当前积分用 balance 存储储值余额。积分累计的逻辑在收银代码里看到的是 payAmount 乘以积分倍率。常见约定是实付 1 元累计 1 积分100 积分可抵 1 元。下单后调用会员服务累加积分Transactional public void addPoints(Long memberId, BigDecimal payAmount) { BigDecimal rate new BigDecimal(1.0); // 1元1积分 int points payAmount.multiply(rate).intValue(); memberMapper.increasePoints(memberId, points); memberPointLogMapper.insert( new MemberPointLog(memberId, points, 消费积分, order.getOrderNo())); }会员折扣的实现方式通常是这样的从请求里拿到 memberId 后查出会员等级再根据等级换算折扣比例。比如普通会员 100%、银卡 95%、金卡 90%计算折扣金额时保留两位小数用 setScale(2, RoundingMode.HALF_UP)。这里有个新手常踩的坑订单表里 discount_amount 存的是优惠金额原价减现价而前端展示时容易把它当成折扣率导致报表上优惠合计翻几倍。写接口时字段命名和注释一定要跟着源码来。储值消费稍微复杂一些。储值账户的扣款必须和订单创建在同一事务里否则会出现订单支付成功但会员余额没扣或者余额扣了订单没生成的情况。处理方式是在事务里同时更新 orders 和 member.balance并且用带条件的更新语句保证余额不为负UPDATE member SET balance balance - #{amount} WHERE id #{memberId} AND balance #{amount}如果这条 update 影响行数为 0说明余额不足直接抛异常让事务回滚。这个写法和库存扣减是同一个套路属于这类系统里最值得模仿的并发控制手段。5. 使用与二次开发避坑五个最常见的翻车点这一章的排序基本就是我这些年反复踩坑的频率排序。前四条是启动阶段的任何一条没注意到系统连登录页都进不去最后一条是上线后才会暴露的等门店跑了一周数据才回头对账往往会花掉一整个下午。建议对照自己的环境逐条核一遍比出了问题再翻日志省时间。5.1 数据库连接失败时区、SSL 与驱动版本的三连坑现象后端启动时控制台报 Communications link failure或者 java.sql.SQLException: The server time zone value ... is unrecognized。原因多半是两种——MySQL 8.0 默认把时区设为 UTC而 JDBC 驱动要求显式给出时区另一类是驱动版本太老MySQL 8.0 默认使用 caching_sha2_password 加密老版本 mysql-connector-java 5.x 无法识别。解决连接串上把 serverTimezoneAsia/Shanghai 和 useSSLfalse 都加上同时确认 pom.xml 里 MySQL 驱动坐标是 8.x。如果 pom 里拿的还是 5.1.47 这种老坐标换成 8.x 之后清理 target 再重新编译避免旧类被缓存。改完后重启后端这个错基本就消失。如果仍然报错就检查 MySQL 用户是否有远程权限本地使用 root 的情况下确认不是密码写错。5.2 中文乱码商品名和会员名变成问号现象数据库里中文正常但页面上下单后商品名或会员名显示为乱码或者反过来数据库里就已经是乱码。原因字符集链路没打通。数据从 MySQL 到后端再到浏览器任何一环用的编码不是 utf8mb4中文就会在某一层被截断。最常见的两处是建库没指定字符集以及 JDBC 连接串里少了 characterEncodingutf8。解决把连接串改完整并确认建库语句用了 utf8mb4。已经乱码的数据可以执行 ALTER TABLE ... CONVERT TO CHARACTER SET utf8mb4 恢复但前提是数据在写入时没有被截断。最有效的办法是删掉重导一次我一般会先把乱码的表清理掉再重新导入脚本这样最省时间。为了以后不被坑我拿到任何一套源码的第一件事就是确认连接串里三个参数齐全。5.3 并发下单把库存扣成负数现象两个人几乎同时结算同一商品库存从 1 变成 -1但系统没有报库存不足。原因库存扣减用了先查库存再写 update的写法两个事务同时读到旧库存都认为还够然后各自执行 update覆盖掉了对方的扣减结果。这个问题在单机测试时很难复现在门店两台收银机同时收银时才会暴露。解决把扣减语句改成条件更新也就是第 4 章写过的 UPDATE ... SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}并用受影响行数判断是否抛异常。改完后用两个终端并发刷同一个商品的接口库存不会低于 0。如果不想改 SQL也可以在 service 层加 synchronized 锁但那只对本进程有效分布式部署时仍然会破不建议。如果收银台页面不做前端数量限制用户还可以直接调接口传 quantity 为 -1条件更新一样能拦但最好在 DTO 校验里做范围限制。5.4 前端跨域不通登录页都进不去现象前端 npm run dev 正常浏览器打开登录页点登录Network 面板里请求显示红色报 CORS 或 net::ERR_CONNECTION_REFUSED。原因前端开发服务器在 9528后端接口在 8080浏览器默认拦截跨域请求。这套源码如果没配后端 CORS前端又没有代理请求就会失败。CORS 报错信息里带 origin 的是后端拦截ERR_CONNECTION_REFUSED 是代理没转发两个方向很好分。解决优先走前端代理。在 vue.config.js 的 devServer.proxy 里把 /api 转发到 http://localhost:8080并设置 changeOrigin: true。改完保存重启 npm run dev。如果你用的是 Nginx 部署生产环境代理规则同理location /api/ 转发到后端 8080。不要为了省事在后端加一个允许所有来源的 CORS 配置那只适合调试上线前记得收紧。5.5 报表统计口径对不上对账时差几块几分现象日销售报表统计的金额和当天实际收款金额差了几元甚至几十元反复核对也对不上。原因常见有两类。一类是报表按 create_time 分组而订单创建时间精确到秒退货订单也在同一张表里导致销售金额里混入了退货记录或者跨天订单被归到了第二天。另一类是用支付金额而不是实付金额做统计会员折扣、抹零金额没有单独扣掉。解决看订单表里是否有 status 字段报表统计时先过滤 status 已完成退货单单独统计金额上优先使用 pay_amount实付金额而不是 total_amount原价合计。如果统计时间维度要更准确建议订单表增加一个 finish_time 字段支付完成时才写入报表按 finish_time 分组。这个字段如果源码没有自己加也不要动 create_time 的逻辑避免影响订单列表排序。如果表里没有退货标识也可以用订单号前缀区分比如前缀 TH 的就是退货单。6. 给系统做个体检压测脚本、对账 SQL 和条码搜索的实用扩展6.1 用并发脚本验证库存扣减是否真的安全改完扣减逻辑后先用一段简单脚本验证扣不减为负for i in 1 2 3 4 5 6 7 8; do curl -s -X POST http://localhost:8080/api/order/create \ -H Content-Type: application/json \ -d {items:[{productId:1,quantity:1}],memberId:1,payType:CASH} done wait跑完后查该商品库存应该是原库存减 8如果小于 0说明条件更新没生效或者事务虽然回滚了但异常被吞掉了。6.2 用一条 SQL 对账库存变动与订单金额互相印证对账思路很简单用订单明细里的销售数量对库存日志的出库数量。注意设计里 stock_log.change_qty 出库记负数所以 -change_qty 才是正数出库量。SELECT p.barcode, p.name, SUM(oi.quantity) AS sold_qty, (SELECT IFNULL(SUM(-change_qty),0) FROM stock_log WHERE product_id p.id AND change_type 1) AS stock_out_qty FROM product p LEFT JOIN order_item oi ON oi.product_id p.id LEFT JOIN orders o ON o.id oi.order_id AND o.status 1 GROUP BY p.id, p.barcode, p.name HAVING sold_qty stock_out_qty;两列不一致的行就是需要人工复核的异常数据。金额对账则是把当日实付金额和收款台账核对口径统一按 status 过滤后再分组。6.3 一个实战扩展让扫描枪像键盘一样触发收银台搜索最后说一个我认为最实用的改造点扫码枪本质上是一个键盘设备扫条码等于在焦点所在的输入框快速输入数字并回车。所以不需要任何硬件 SDK只要在收银台页面的条码输入框上监听回车事件扫完自动触发查询加入购物车。改造点很小但门店实际使用中收银效率的提升非常明显。我印象很深的一次交付对方说系统慢排查下来不是接口慢而是收银员每次都要手工搜商品名再点加入购物车。加上扫码自动触发后整单时间从四十多秒降到十几秒。从那以后我每次拿到收银类源码都会先把扫码触发这条链路完整走一遍再交付。这套源码我按同样的流程验过值得下载后把业务闭环跑通跑通一遍你对订单、库存、会员怎么协同的理解会比看十篇教程都深。希望帮到你。本文还有配套的精品资源点击获取