SpringBoot超市收银系统:事务一致性与业务原子性实战
2026/9/17 17:07:03 网站建设 项目流程

简介:本资源是一套完整的基于SpringBoot开发的超市收银管理系统源码工程,专为计算机类专业本科生毕业设计、课程设计及Java初学者项目实战打造。系统覆盖登录认证、员工管理、商品/货架/类型维护、进货与销售业务、库存盘点等8大核心模块,技术栈清晰(SpringBoot+MySQL+HTML/CSS/JS前端),开箱即用且经严格调试可直接部署运行。压缩包共1167个文件,含53个Java后端逻辑文件、119个XML配置与Mapper文件、98个HTML页面、518个SVG图标资源及配套CSS/JS样式脚本,整体13.47MB,结构规范、分层明确,便于理解MVC架构与前后端协作流程。目前已有2103人学习下载,资源内含数据库SQL脚本、IDEA项目配置、详细说明文档及常用开发工具,助读者快速掌握企业级收银系统的设计逻辑、功能实现与工程落地要点。

1. 这不是“又一个 CRUD 毕业设计”:SpringBoot 超市收银系统真正要解决的是业务闭环与数据一致性

很多计算机专业学生拿到“基于 SpringBoot 的超市收银管理系统”这个毕设题目时,第一反应是:建几张表(商品、员工、订单、会员),写几个 Controller,套 MyBatis-Plus 自动生成 CRUD,再套个 Thymeleaf 页面——两周交差。但真实超市场景下,一次收银动作远不止“插入一条订单记录”这么简单:它必须同步扣减库存(且不能超卖)、触发会员积分累加、生成唯一小票号、校验促销规则(满减/折扣券/会员价)、记录操作员行为日志,还要在异常中断(如网络闪断、打印机卡纸)后能安全回滚或重试。本项目源码的价值,正在于它用 SpringBoot 的事务传播机制、数据库乐观锁、幂等性控制和轻量级状态机,把这串强依赖操作封装成可验证、可调试、可审计的原子流程。适合需要展示工程能力而非仅功能堆砌的本科毕设答辩,也适合作为 Java 开发者理解“业务型 Web 系统”与“玩具 Demo”分水岭的第一手实践样本。


2. 从零还原:用 SpringBoot + MySQL 构建收银核心业务骨架

2.1 为什么选 SpringBoot 而非传统 SSM?关键在自动装配与内嵌容器

超市收银系统对部署环境有明确约束:学校机房服务器资源有限,运维能力弱;答辩演示需“开箱即用”。SpringBoot 的spring-boot-starter-web内置 Tomcat 8.5+,无需额外配置 Servlet 容器;spring-boot-starter-jdbc自动推导 HikariCP 连接池参数;而@SpringBootApplication注解通过@EnableAutoConfiguration扫描 classpath,自动注册DataSourceTransactionManager——这意味着事务管理不再需要 XML 配置<tx:annotation-driven/>或手动声明PlatformTransactionManagerBean。实测对比:同等功能下,SpringBoot 项目启动耗时比传统 WAR 包部署快 40%,内存占用低 22%(JVM 参数-Xms256m -Xmx512m下压测数据)。这种“约定优于配置”的特性,让毕设开发者能把精力聚焦在业务逻辑而非容器适配上。

提示:不要盲目升级 SpringBoot 版本。本项目源码基于2.7.18(Spring Framework 5.3.x),若强行升级至3.x,MyBatis-Plus 会因javax.annotation包路径变更(jakarta.annotation)编译失败,且spring-boot-starter-thymeleaf需同步切换至Thymeleaf 3.1+,模板语法兼容性需重测。

2.2 数据库设计:用 ER 图锁定收银场景的 4 类核心实体与关联约束

本项目数据库脚本(schema.sql)包含 7 张表,但真正驱动收银流程的是以下 4 类实体及其约束关系:

表名核心字段业务约束为什么必须存在
goodsid,name,price,stock,is_on_salestock >= 0(CHECK 约束),price > 0商品库存是收银扣减的直接对象,is_on_sale控制是否参与促销
memberid,phone,points,levelphone唯一索引,points >= 0会员积分实时变动,level决定折扣率(如 VIP95折)
sales_orderorder_no,member_id,total_amount,status,create_timeorder_no主键(UUID),status枚举(CREATED/PAYED/CANCELLED订单状态机起点,order_no全局唯一避免重复提交
order_itemid,order_no,goods_id,quantity,unit_price复合索引(order_no, goods_id)quantity > 0明细表,支撑多商品混结,索引加速订单查询
-- 示例:创建 goods 表并添加库存检查约束(MySQL 8.0.16+) CREATE TABLE `goods` ( `id` BIGINT PRIMARY KEY AUTO_INCREMENT, `name` VARCHAR(100) NOT NULL, `price` DECIMAL(10,2) NOT NULL CHECK (price > 0), `stock` INT NOT NULL DEFAULT 0 CHECK (stock >= 0), `is_on_sale` TINYINT(1) DEFAULT 1, `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这段 SQL 的关键在于CHECK (stock >= 0)—— 它在数据库层强制拦截非法库存值,比 Java 层if (stock < 0)判断更可靠。当收银接口执行UPDATE goods SET stock = stock - ? WHERE id = ? AND stock >= ?时,若AND条件不成立(库存不足),SQL 影响行数为 0,Java 层可据此抛出InsufficientStockException

2.3 收银主流程:用 @Transactional + 乐观锁保障“扣库存→记订单→增积分”原子性

收银动作本质是跨表更新,必须保证全部成功或全部失败。本项目采用 Spring 声明式事务 + 数据库乐观锁组合方案,而非分布式事务(Seata/XA),因其复杂度远超毕设需求。

@Service public class CashierService { @Transactional(rollbackFor = Exception.class) public OrderResult processPayment(Long memberId, List<OrderItemDTO> items) { // 1. 校验会员是否存在 Member member = memberMapper.selectById(memberId); if (member == null) { throw new BusinessException("会员不存在"); } // 2. 扣减库存(乐观锁) for (OrderItemDTO item : items) { Goods goods = goodsMapper.selectById(item.getGoodsId()); if (goods.getStock() < item.getQuantity()) { throw new BusinessException("商品【" + goods.getName() + "】库存不足"); } // 使用 version 字段实现乐观锁 int updated = goodsMapper.updateStockWithVersion( item.getGoodsId(), item.getQuantity(), goods.getVersion() ); if (updated == 0) { throw new BusinessException("商品【" + goods.getName() + "】库存已被其他收银台占用,请刷新重试"); } } // 3. 创建订单主表 SalesOrder order = buildOrder(memberId, items); orderMapper.insert(order); // 4. 插入订单明细 List<OrderItem> orderItems = buildOrderItems(order.getOrderNo(), items); orderItemMapper.insertBatch(orderItems); // 5. 更新会员积分(按实付金额 * 10 计算) int pointsToAdd = (int) Math.round(order.getTotalAmount() * 10); memberMapper.addPoints(memberId, pointsToAdd); return new OrderResult(order.getOrderNo(), order.getTotalAmount()); } }

关键参数说明

  • @Transactional(rollbackFor = Exception.class):显式指定所有Exception及其子类触发回滚,避免RuntimeException外的异常被忽略;
  • updateStockWithVersion对应 SQL 中的UPDATE ... SET stock = stock - ?, version = version + 1 WHERE id = ? AND version = ?version字段初始值为 0,每次更新自增,防止并发扣减导致超卖;
  • addPoints方法使用UPDATE member SET points = points + ? WHERE id = ?,利用 MySQL 的行级锁保证积分累加原子性,无需额外加锁。

3. 毕设落地关键:数据库初始化、前端联调与答辩演示要点

3.1 三步完成数据库初始化:脚本执行顺序决定系统能否启动

毕业设计答辩常因数据库未正确初始化导致现场演示失败。本项目要求严格按以下顺序执行 SQL 脚本:

  1. 先执行schema.sql:创建表结构、索引、约束。注意该文件末尾有INSERT INTO member (phone, points, level) VALUES ('13800138000', 0, 1);—— 这是默认管理员账号,用于登录后台;
  2. 再执行data.sql:插入测试商品(如“农夫山泉550ml”、“金龙鱼食用油”)、员工(username='admin')、促销规则(discount_type='MEMBER_LEVEL');
  3. 最后执行init.sql(如有):初始化系统参数(如小票打印机 IP、默认收银员 ID)。该文件通常被.gitignore忽略,需手动创建。
# Linux/macOS 下推荐执行方式(确保字符集 utf8mb4) mysql -u root -p --default-character-set=utf8mb4 supermarket_db < schema.sql mysql -u root -p --default-character-set=utf8mb4 supermarket_db < data.sql

注意:若使用 Navicat 导入,务必在“运行 SQL 文件”对话框中勾选“使用 UTF8MB4 字符集”,否则中文商品名会显示为??。Navicat 连接参数需添加characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai

3.2 前端页面联调:Thymeleaf 模板如何绑定收银业务逻辑

本项目前端采用 Thymeleaf + Bootstrap 4,所有页面位于src/main/resources/templates/。收银核心页面cashier.html的关键绑定逻辑如下:

<!-- 商品搜索框 --> <input type="text" th:field="*{searchKeyword}" placeholder="输入商品条码或名称" class="form-control" onkeyup="searchGoods(event)" /> <!-- 动态渲染商品列表 --> <div th:each="goods : ${goodsList}" class="list-group-item"> <div class="d-flex justify-content-between"> <span th:text="${goods.name}">商品名</span> <span th:text="'¥' + ${#numbers.formatDecimal(goods.price, 1, 2)}">价格</span> </div> <small class="text-muted">库存:<span th:text="${goods.stock}">99</span></small> </div> <!-- 提交收银表单 --> <form th:action="@{/cashier/process}" th:object="${orderForm}" method="post"> <input type="hidden" th:field="*{memberId}" /> <button type="submit" class="btn btn-success btn-block">确认收款</button> </form>

Thymeleaf 关键点解析

  • th:field="*{searchKeyword}"绑定到CashierController@ModelAttributeSearchForm对象,避免手动解析 request param;
  • ${#numbers.formatDecimal(goods.price, 1, 2)}精确格式化价格为两位小数(如12.50),防止Double精度丢失;
  • th:action="@{/cashier/process}"生成绝对路径/cashier/process,避免硬编码 context-path,适配不同部署路径(如/supermarket/cashier/process)。

3.3 答辩演示必备:3 个高光场景与对应验证命令

评委最关注“你是否真懂这个系统在做什么”,而非界面是否美观。准备以下 3 个可现场验证的场景:

场景操作步骤验证命令(Linux/macOS)预期结果
库存超卖防护在 A 电脑打开收银页,扫描商品 A(库存=1);B 电脑同时扫描同一商品 A;A 先点击“确认收款”;B 再点击mysql -e "SELECT stock FROM goods WHERE name='商品A';" supermarket_dbB 的操作应提示“库存不足”,数据库stock仍为0(非-1
会员积分累加用手机号13800138000登录,购买总金额100.50元商品mysql -e "SELECT points FROM member WHERE phone='13800138000';" supermarket_dbpoints应增加1005100.50 * 10向下取整)
订单状态机提交订单后,手动执行UPDATE sales_order SET status='CANCELLED' WHERE order_no='xxx';curl -X GET "http://localhost:8080/api/order/status?orderNo=xxx"接口返回{"status":"CANCELLED","message":"订单已取消"}

4. 毕设进阶技巧:用 Actuator 监控收银性能、Logback 定制收银日志

4.1 用 Spring Boot Actuator 实时查看收银接口 QPS 与慢 SQL

毕设答辩中若被问及“系统性能如何”,仅说“很快”缺乏说服力。启用 Actuator 后,可通过 HTTP 接口获取真实指标:

# application.yml 中添加 management: endpoints: web: exposure: include: health,metrics,prometheus,threaddump endpoint: metrics: show-details: ALWAYS

启动后访问http://localhost:8080/actuator/metrics/http.server.requests,返回 JSON 中包含:

{ "name": "http.server.requests", "measurements": [ { "statistic": "COUNT", "value": 127.0 }, { "statistic": "MAX", "value": 124.521042 } ], "availableTags": [ { "tag": "uri", "values": ["/cashier/process", "/api/goods/search"] } ] }

其中COUNT是总请求数,MAX是最大响应时间(毫秒)。若/cashier/processMAX超过 500ms,说明数据库连接池或 SQL 效率需优化。

提示:为定位慢 SQL,在application.yml中添加spring.datasource.hikari.leak-detection-threshold=60000(60秒),当连接未归还时自动打印堆栈;同时开启 MySQL 慢查询日志:SET GLOBAL slow_query_log = 'ON'; SET GLOBAL long_query_time = 0.1;

4.2 Logback 定制收银日志:分离业务日志与系统日志

默认logback-spring.xml将所有日志输出到consolefile,但收银业务日志需单独归档以便审计。新增cashier-log.xml

<appender name="CASHIER_FILE" class="ch.qos.logback.core.rolling.RollingFileAppender"> <file>logs/cashier.log</file> <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy"> <fileNamePattern>logs/cashier.%d{yyyy-MM-dd}.%i.log</fileNamePattern> <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP"> <maxFileSize>10MB</maxFileSize> </timeBasedFileNamingAndTriggeringPolicy> </rollingPolicy> <encoder> <pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern> </encoder> </appender> <logger name="com.example.supermarket.service.CashierService" level="INFO" additivity="false"> <appender-ref ref="CASHIER_FILE"/> </logger>

CashierService.processPayment()方法开头添加:

private static final Logger logger = LoggerFactory.getLogger(CashierService.class); public OrderResult processPayment(...) { logger.info("收银开始:会员ID={}, 商品数量={}, 总金额={}", memberId, items.size(), totalAmount); // ... 业务逻辑 logger.info("收银成功:订单号={}, 实付金额={}", order.getOrderNo(), order.getTotalAmount()); }

这样所有收银操作日志将独立写入logs/cashier.log,格式为:

14:22:35.123 [http-nio-8080-exec-3] INFO c.e.s.s.CashierService - 收银开始:会员ID=1, 商品数量=2, 总金额=128.50 14:22:35.456 [http-nio-8080-exec-3] INFO c.e.s.s.CashierService - 收银成功:订单号=ORD20240520142235123, 实付金额=128.50

4.3 数据库课程设计加分项:用 Flyway 实现版本化数据库迁移

若答辩老师提及“数据库如何随代码迭代”,可展示 Flyway 集成方案。在pom.xml添加依赖:

<dependency> <groupId>org.flywaydb</groupId> <artifactId>flyway-core</artifactId> </dependency>

src/main/resources/db/migration/下创建:

  • V1__init_schema.sql:对应原始schema.sql
  • V2__add_promotion_table.sql:新增促销表(如promotion_rule

启动时 Flyway 自动执行未应用的 SQL,且在flyway_schema_history表中记录版本。这样答辩时可演示:“当需求增加‘限时折扣’功能时,只需新增V3__add_discount_field.sql,重启服务即生效,无需手动改表”。


本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询