☰
Spring Boot小型超市管理系统全栈实战:从数据库设计到事务部署
2026/10/6 4:52:27 网站建设 项目流程

简介:基于Spring Boot的小型超市管理系统完整毕业设计项目,集前端界面、后端逻辑、数据库脚本与设计文档于一体,面向计算机专业毕业生及需要快速搭建零售管理原型的开发者。系统采用B/S架构,涵盖商品分类管理、采购流程优化、销售数据统计、经营报表生成等核心模块,可有效缓解小型商超在数字化断层、供应链信息孤岛和动态决策支持不足等方面的运营痛点。压缩包共402个文件,总大小13.18MB,其中158个Java文件用于实现后端服务,58个Vue文件用于构建管理界面,配以30个JavaScript文件、25个XML配置、15个CSS样式及1个SQL数据库脚本,另有Word格式的LW设计文档,从需求分析到部署运行均有完整呈现。当前已有147人学习下载,目录结构清晰,项目可直接导入集成开发环境进行二次开发,既适合作为毕业设计参考,也适合系统学习Spring Boot与MySQL的整合应用,具备商品、采购、销售、报表等多个可用模块,可作为小型超市信息化改造的基础。

1. Spring Boot小型超市管理系统:毕设里最值得跑通的一条完整链路

拿Spring Boot写小型超市管理系统,几乎是Java毕设里性价比最高的选择:它把Controller路由、Service事务、Mapper持久化全都覆盖到了,又不会像秒杀系统那样把分布式、消息队列全拖进来,让你三个月都在Debug中间件。这套资源自带完整前后端代码、MySQL建库脚本和论文(LW),意味着你不只是拿到一段能跑的代码,而是拿到一条可以照着改、照着答辩的完整链路。适合时间紧、需要快速产出可演示系统的本科毕设选手,也适合刚学完Spring Boot、想找一个完整项目对照着理解分层结构的新手。下面按数据库设计→后端实现→页面联调→部署避坑的顺序拆,参数和坑都会摊开讲。

2. 系统架构与数据库设计:ER关系、建表SQL与初始化数据

2.1 为什么Spring Boot + MySQL + MyBatis是这套系统的地基

小型超市管理系统这类业务,本质就是围绕"商品、库存、订单、供应商"做增删改查。Spring Boot负责把HTTP请求路由到Service层,Service层把业务逻辑(比如下单时扣库存)组织好,MyBatis负责把Java对象和MySQL表互相映射。这个组合的好处是每一层都有明确边界,拿到代码后能快速找到"哪个类对应哪个功能"。

我拿到别人项目的第一习惯是看pom.xml确认依赖版本,再看application.yml确认数据库配置,最后才看业务代码。这套资源里Spring Boot用的2.x版本,MySQL用的是5.7或8.0都可以。Spring Boot 2.x对JDK 8支持最好,也最贴合高校实验室环境,不会出现JDK 17下Lombok插件失效这种玄学问题。

<dependencies> <!-- Web启动器,内嵌Tomcat,提供HTTP服务 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- Thymeleaf模板引擎,服务端渲染页面 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-thymeleaf</artifactId> </dependency> <!-- MyBatis与Spring Boot整合 --> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.2.2</version> </dependency> <!-- MySQL驱动 --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> </dependencies>

这里有个细节要注意:mybatis-spring-boot-starter的版本必须和Spring Boot主版本匹配。Spring Boot 2.5.x对应MyBatis starter 2.2.x,Spring Boot 3.x就得换3.x的starter。很多同学拿到代码第一步就是把Spring Boot升到最新,结果MyBatis依赖没跟上,启动直接报错。这类版本匹配的坑我放到第5章统一说。

2.2 四大核心表结构设计:用户、商品、供应商、订单

小型超市的业务不复杂,但表与表之间的关系必须理清楚。这套系统的核心表四张:用户表、商品表、供应商表、订单表,外加一个订单明细表。

表名用途关键字段
sys_user登录用户(管理员/收银员)id, username, password, role
product商品信息id, name, price, stock, category, supplier_id
supplier供应商id, name, contact, phone
sale_order销售订单主表id, order_no, user_id, total_amount, status
sale_order_item订单明细id, order_id, product_id, quantity, price

商品表通过supplier_id关联供应商,订单明细表通过order_id关联订单主表,订单表通过user_id关联用户表。

CREATE TABLE `product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '商品ID', `name` varchar(100) NOT NULL COMMENT '商品名称', `price` decimal(10,2) NOT NULL COMMENT '销售单价', `stock` int(11) NOT NULL DEFAULT 0 COMMENT '库存数量', `category` varchar(50) DEFAULT NULL COMMENT '商品分类', `supplier_id` bigint(20) DEFAULT NULL COMMENT '供应商ID', `status` tinyint(4) NOT NULL DEFAULT 1 COMMENT '状态:1上架,0下架', PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

建表有几个关键决策要说明。价格字段必须用decimal(10,2),不能用float或double——二进制浮点数存金额会产生精度误差,0.1+0.2不等于0.3的问题在收银场景是致命的。库存字段用int就够了,小型超市单品库存量级一般不会超过几十万。所有表统一用InnoDB引擎加utf8mb4字符集,InnoDB支持事务和外键约束,utf8mb4支持中文和特殊符号,避免后面乱码折腾。

CREATE TABLE `sale_order` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '订单ID', `order_no` varchar(32) NOT NULL COMMENT '订单编号', `user_id` bigint(20) NOT NULL COMMENT '操作员ID', `total_amount` decimal(10,2) NOT NULL COMMENT '订单总金额', `status` tinyint(4) NOT NULL DEFAULT 0 COMMENT '状态:0待结算,1已结算,2已取消', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '下单时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='销售订单表';

订单编号order_no要加唯一索引,我见过有人用时间戳当订单号,结果同一毫秒内下两单导致主键冲突。常见做法是时间戳加随机数,或者直接用System.currentTimeMillis()拼上自增ID,保证不重复就行。

2.3 application.yml配置与数据库连接参数说明

数据库连不上、中文乱码、时区报错,90%的问题都出在这份配置文件上。我一般把配置写成下面这样:

spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 thymeleaf: cache: false mybatis: mapper-locations: classpath:mapper/*.xml configuration: map-underscore-to-camel-case: true

逐个参数拆开说。driver-class-name用com.mysql.cj.jdbc.Driver,这是MySQL 8.x的驱动类;如果本机装的是MySQL 5.7,得换成com.mysql.jdbc.Driver,新驱动类在5.7下会报ClassNotFoundException。url里的serverTimezone=Asia/Shanghai不能省,MySQL 8.x默认时区是UTC,不指定的话连本地库会报"server time zone valueis unrecognized"。useSSL=false是本地开发标配,毕设没有敏感数据,不需要走SSL加密。map-underscore-to-camel-case=true打开后,数据库的create_time字段能自动映射到Java对象的createTime属性,省掉一堆手写ResultMap的重复劳动。

提示:注解方式写SQL的项目可以不用mapper-locations配置,但XML统一放一个目录,后面排查SQL更方便。

3. 后端核心功能实现:商品CRUD、库存事务与订单流程

3.1 工程结构与登录拦截器的实现

拿到这套代码后,先看包结构,它基本等于一张导航图:

src/main/java/com/example/supermarket/ ├── controller/ # 控制器,接收HTTP请求 │ ├── LoginController.java │ ├── ProductController.java │ └── OrderController.java ├── service/ # 业务接口与实现 │ ├── ProductService.java │ ├── impl/ │ │ ├── ProductServiceImpl.java │ │ └── OrderServiceImpl.java ├── mapper/ # MyBatis Mapper接口 │ ├── ProductMapper.java │ └── OrderMapper.java ├── entity/ # 实体类 │ ├── Product.java │ ├── SaleOrder.java │ └── User.java └── config/ └── LoginInterceptor.java

Controller层只做参数接收和视图跳转,Service层放业务逻辑,Mapper层只负责SQL交互。这样一个最简单的登录拦截器加在config包里:

@Component public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user = request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect("/login"); return false; } return true; } }

preHandle方法返回false时请求被拦截并跳转到登录页,返回true则放行。登录成功后把用户对象塞进Session,标记为loginUser。注意一个很常见的坑:拦截器默认拦所有路径,如果你不主动排除静态资源,CSS和JS也会被拦掉,页面样式全丢。注册拦截器时记得加excludePathPatterns("/css/", "/js/", "/images/**", "/login")。

3.2 商品管理模块:Service接口+Mapper XML的CRUD写法

商品管理是最标准的增删改查场景,这套代码用的是Service接口+实现类的设计模式,好处是便于替换实现、方便测试。

@Service public class ProductServiceImpl implements ProductService { @Autowired private ProductMapper productMapper; @Override public List<Product> listProducts(String keyword) { if (keyword == null || keyword.isEmpty()) { return productMapper.findAll(); } return productMapper.searchByKeyword(keyword); } @Override public boolean addProduct(Product product) { return productMapper.insert(product) > 0; } @Override public boolean updateProduct(Product product) { return productMapper.updateById(product) > 0; } }

searchByKeyword的SQL写在Mapper XML里,用预编译参数拼接模糊查询:

<select id="searchByKeyword" resultType="com.example.supermarket.entity.Product"> SELECT * FROM product WHERE name LIKE CONCAT('%', #{keyword}, '%') AND status = 1 </select> <update id="updateById" parameterType="com.example.supermarket.entity.Product"> UPDATE product SET name = #{name}, price = #{price}, stock = #{stock}, category = #{category} WHERE id = #{id} </update>

这里有个必须说清楚的点:LIKE查询一定要用CONCAT('%', #{keyword}, '%'),不能直接写'%${keyword}%'。前者走预编译,MyBatis会把#{keyword}替换成?占位符,用户输入的内容永远只是参数值,不会变成SQL指令;后者是字符串直接拼接,用户在搜索框输入' OR '1'='1就能把整张表查出来。这个细节答辩老师问到的概率极高,回答"预编译防SQL注入"就过关了。

3.3 下单扣库存:事务与行级锁的配合

下单是这个系统里最值得仔细读的模块,因为涉及多张表的数据变更。核心流程是:查询商品→校验库存是否足够→扣减库存→生成订单主记录→生成订单明细→计算总价。整个流程必须包在事务里,否则会出现库存扣了但订单没生成、或者订单生成了库存没扣的数据不一致问题。

@Transactional(rollbackFor = Exception.class) @Override public SaleOrder createOrder(List<CartItem> items, Long userId) { SaleOrder order = new SaleOrder(); order.setOrderNo(String.valueOf(System.currentTimeMillis())); order.setUserId(userId); order.setStatus(0); orderMapper.insert(order); BigDecimal total = BigDecimal.ZERO; for (CartItem item : items) { Product product = productMapper.selectByIdForUpdate(item.getProductId()); if (product == null || product.getStock() < item.getQuantity()) { throw new RuntimeException("商品库存不足:" + item.getProductId()); } productMapper.reduceStock(item.getProductId(), item.getQuantity()); // 写订单明细,省略... total = total.add(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } order.setTotalAmount(total); orderMapper.updateById(order); return order; }

三个关键参数必须解释。@Transactional(rollbackFor = Exception.class)里的rollbackFor不能省,Spring默认只在抛出RuntimeException时回滚,如果SQL执行抛的是受检异常,事务是不会回滚的,数据就悄悄写进去了。selectByIdForUpdate的SQL语句在Mapper里对应的是SELECT ... FOR UPDATE,这是行级锁,两个收银员同时抢最后一瓶可乐时,第二个请求会在这一行上等着,等第一个事务提交后才读到扣减后的库存,天然避免超卖。金额计算全部用BigDecimal,total.add(...)和multiply(...)链式操作,杜绝double精度丢失。

<select id="selectByIdForUpdate" resultType="com.example.supermarket.entity.Product"> SELECT * FROM product WHERE id = #{id} FOR UPDATE </select> <update id="reduceStock"> UPDATE product SET stock = stock - #{quantity} WHERE id = #{id} </update>

扣库存的SQL用的是stock = stock - #{quantity}而不是先查出来再set进去,前者是原子操作,在数据库层面完成减法,减少一次查询往返和并发覆盖的窗口期。

4. 前端页面与联调:Thymeleaf模板、购物车会话与结算流程

4.1 Thymeleaf模板页面与静态资源组织方式

前端不做前后端分离,用Spring Boot自带的Thymeleaf模板引擎做服务端渲染。页面文件放在src/main/resources/templates/目录下,静态资源(CSS、JS、图片)放在src/main/resources/static/目录下。商品列表页的核心模板代码:

<table class="table table-bordered"> <thead> <tr> <th>商品ID</th> <th>商品名称</th> <th>分类</th> <th>单价</th> <th>库存</th> <th>操作</th> </tr> </thead> <tbody> <tr th:each="p : ${productList}"> <td th:text="${p.id}">1</td> <td th:text="${p.name}">可乐</td> <td th:text="${p.category}">饮料</td> <td th:text="${p.price}">3.50</td> <td th:text="${p.stock}">100</td> <td> <a th:href="@{/product/edit/{id}(id=${p.id})}">编辑</a> <button th:data-id="${p.id}" class="btn-add-cart">加入购物车</button> </td> </tr> </tbody> </table>

th:each是Thymeleaf的循环语法,${productList}是Controller通过Model传递过来的List数据。th:text是文本输出,th:href是URL拼接,@{/product/edit/{id}(id=${p.id})}会生成类似/product/edit/3的路径。写模板时注意th:前缀的属性是服务端渲染的,普通html属性是静态占位用的,两者要区分开。

4.2 购物车会话管理:Session Map结构实现加购与结算

购物车属于会话级数据,用Session存储最直接,不需要额外的表。核心逻辑是:从Session里取购物车Map,如果还没有就新建一个Map。Map的key是商品ID,value是商品ID和数量的组合对象。

@Controller @RequestMapping("/cart") public class CartController { @GetMapping("/add") public String addToCart(@RequestParam Long productId, @RequestParam(defaultValue = "1") Integer quantity, HttpSession session) { Map<Long, CartItem> cart = (Map<Long, CartItem>) session.getAttribute("cart"); if (cart == null) { cart = new HashMap<>(); session.setAttribute("cart", cart); } CartItem item = cart.get(productId); if (item == null) { item = new CartItem(productId, quantity); cart.put(productId, item); } else { item.setQuantity(item.getQuantity() + quantity); } return "redirect:/product/list"; } }

CartItem包含productId和quantity两个字段。重复加购同一商品时走else分支,数量累加。整个购物车数据只存在当前浏览器会话内,关浏览器就清空,符合"临时购物车"的定位。如果老师要求购物车数据重启不丢,那要改成存订单草稿表,但毕设一般不用做到那一步。Session方案的好处是代码量少、演示时不用反复登录,坏处是重启服务Session就没了,需要重新加购——演示前记得先登录加购好再开始录屏。

4.3 后端参数校验与前端提示的衔接

商品表单提交时,价格可能是负数、库存可能填了字母,后端必须做参数校验。Spring Boot自带javax.validation的Hibernate Validator实现,在实体类字段上加注解就行:

public class Product { private Long id; @NotBlank(message = "商品名称不能为空") private String name; @NotNull(message = "单价不能为空") @DecimalMin(value = "0.01", message = "单价必须大于0") private BigDecimal price; @Min(value = 0, message = "库存不能为负数") private Integer stock; }

Controller层处理校验结果:

@PostMapping("/save") public String saveProduct(@Valid @ModelAttribute Product product, BindingResult result, Model model) { if (result.hasErrors()) { model.addAttribute("errorMsg", result.getFieldError().getDefaultMessage()); return "product/edit"; } productService.updateProduct(product); return "redirect:/product/list"; }

BindingResult必须紧跟在@Valid参数后面,这是Spring MVC的硬性规定,中间插别的参数会直接报校验失效。校验失败后把错误信息放进Model,用th:if="(errorMsg != null)"在页面上渲染出来。注意Spring Boot 2.3之后,spring-boot-starter-web默认不再打包validation组件,需要在pom.xml里额外引入spring-boot-starter-validation依赖,不然@Valid注解完全不生效——这个坑我见不少人踩过。

5. 避坑指南:版本兼容、数据精度与部署运行的常见报错

5.1 版本兼容类:Spring Boot升级、MyBatis starter、JDK版本

现象:项目原本能跑,把Spring Boot从2.5.5升到3.2.1后直接启动失败,控制台报错误BeanDefinitionStoreException或NoClassDefFoundError,指向MyBatis相关的类。

原因:mybatis-spring-boot-starter 2.x是基于Spring Boot 2.x构建的,Spring Boot 3.x全面迁移到Jakarta命名空间,javax.servlet和javax.validation全会失效,旧版starter加载不到对应类。

解决:同步升级MyBatis starter。Spring Boot 3对应mybatis-spring-boot-starter 3.0.3及以上,同时把import里的javax.servlet改成jakarta.servlet,javax.validation改成jakarta.validation。如果不想折腾,就别动Spring Boot版本,老老实实停在2.x。

现象:IDEA里编译通过,但启动时提示Local class incompatible、Invalid byte tag in constant pool之类异常。

原因:JDK版本高于项目编译目标。比如用JDK 17跑target/classes里编译成Java 8的class,或者Lombok插件与JDK版本不匹配。

解决:项目统一JDK 8,IDEA里Project Structure和Settings的JavaCompiler都改成8。用IntelliJ IDEA社区版跑Spring Boot完全没问题,不用特意装旗舰版。

5.2 数据精度与编码:金额、中文、日期时区

现象:订单总价计算结果是39.999999999,页面显示少了一分钱。

原因:Service层金额用了double求和,double在二进制里无法精确表示0.1,累加多次误差就暴露了。

解决:全部换成BigDecimal,数据库字段用decimal(10,2),前端展示用th:text="${#numbers.formatDecimal(price, 1, 2)}"限制两位小数。这是资金类系统的硬性要求,跟SQL注入一样属于答辩加分点。

现象:从页面表格导出数据或打印小票时中文变成"???"。

原因:MySQL建库建表时用了latin1字符集,或者JDBC连接url没带characterEncoding=utf8参数。

解决:建库时指定CHARSET=utf8mb4,url里加useUnicode=true&characterEncoding=utf8。两个都要做,只改一个也白搭。数据已经乱了的表,用ALTER TABLE product CONVERT TO CHARACTER SET utf8mb4抢救一下。

5.3 部署运行与端口、Tomcat冲突

现象:启动时报Web server failed to start,说明端口被占用。

原因:之前跑过一个没关掉的Spring Boot实例,8080被占着。

解决:命令行执行netstat -ano | findstr 8080查到PID,taskkill /PID xxx /F杀掉。或者直接在application.yml里改server.port,改成8081、8082都行,不影响功能。

现象:部署到服务器后页面能打开,但过几分钟就卡死,日志有大量连接超时。

原因:最常见的两类:一是MySQL的wait_timeout默认8小时,连接池里的旧连接被数据库断开后没有自动重连;二是云服务器安全组没放通3306端口。

解决:在application.yml里给HikariCP配置连接保活参数:

spring: datasource: hikari: minimum-idle: 5 maximum-pool-size: 20 connection-test-query: SELECT 1 idle-timeout: 30000 max-lifetime: 60000

maximum-pool-size控制最多20个连接,minimum-idle保持最少5个空闲连接,connection-test-query每30秒探活一次,确保被MySQL踢掉的旧连接能及时重建。这类问题不遇到时完全无感,遇到一次能折腾一晚上,属于毕设答辩演示翻车的重灾区。

6. 交付前最后一步:接口自测与关键路径验证

6.1 用Postman把核心接口从头到尾跑一遍

答辩前一天,我会把核心接口按业务顺序过一遍,确认数据流是通的。用Postman建一个Collection,按顺序测:登录→查询商品→加入购物车→模拟下单→查订单。

接口方法路径预期结果
管理员登录POST/login返回redirect,Session写入用户
商品列表GET/product/list返回商品表格页
添加商品POST/product/add列表出现新商品
加入购物车GET/cart/add?productId=1&quantity=2购物车数量累加
提交订单POST/order/create库存扣减,订单生成

6.2 用curl模拟并发下单验证超卖防护

for i in {1..3} do curl -s -b cookies.txt -X POST http://localhost:8080/order/create \ -H "Content-Type: application/json" \ -d '{"items":[{"productId":1,"quantity":2}]}' & done wait

跑完后再查商品表的库存和订单表,正常结果应该是三个请求只成功一个或按库存余量决定成功数量,库存永远不会变负数。如果出现负库存,说明@Transactional或FOR UPDATE没生效,回到第3.3节检查。我在交付前每次都强制走一遍这个流程,从那以后答辩现场再没出过数据流断裂的丑。希望帮到你。

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

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

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

立即咨询