我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

Spring Boot小型超市管理系统全栈实战:从数据库设计到事务部署

Spring Boot小型超市管理系统全栈实战:从数据库设计到事务部署 简介基于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 groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency !-- Thymeleaf模板引擎服务端渲染页面 -- dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-thymeleaf/artifactId /dependency !-- MyBatis与Spring Boot整合 -- dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.2.2/version /dependency !-- MySQL驱动 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId scoperuntime/scope /dependency /dependencies这里有个细节要注意mybatis-spring-boot-starter的版本必须和Spring Boot主版本匹配。Spring Boot 2.5.x对应MyBatis starter 2.2.xSpring Boot 3.x就得换3.x的starter。很多同学拿到代码第一步就是把Spring Boot升到最新结果MyBatis依赖没跟上启动直接报错。这类版本匹配的坑我放到第5章统一说。2.2 四大核心表结构设计用户、商品、供应商、订单小型超市的业务不复杂但表与表之间的关系必须理清楚。这套系统的核心表四张用户表、商品表、供应商表、订单表外加一个订单明细表。表名用途关键字段sys_user登录用户管理员/收银员id, username, password, roleproduct商品信息id, name, price, stock, category, supplier_idsupplier供应商id, name, contact, phonesale_order销售订单主表id, order_no, user_id, total_amount, statussale_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) ) ENGINEInnoDB AUTO_INCREMENT1 DEFAULT CHARSETutf8mb4 COMMENT商品表;建表有几个关键决策要说明。价格字段必须用decimal(10,2)不能用float或double——二进制浮点数存金额会产生精度误差0.10.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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 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?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/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里的serverTimezoneAsia/Shanghai不能省MySQL 8.x默认时区是UTC不指定的话连本地库会报server time zone valueis unrecognized。useSSLfalse是本地开发标配毕设没有敏感数据不需要走SSL加密。map-underscore-to-camel-casetrue打开后数据库的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.javaController层只做参数接收和视图跳转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 ListProduct 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 idsearchByKeyword resultTypecom.example.supermarket.entity.Product SELECT * FROM product WHERE name LIKE CONCAT(%, #{keyword}, %) AND status 1 /select update idupdateById parameterTypecom.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 11就能把整张表查出来。这个细节答辩老师问到的概率极高回答预编译防SQL注入就过关了。3.3 下单扣库存事务与行级锁的配合下单是这个系统里最值得仔细读的模块因为涉及多张表的数据变更。核心流程是查询商品→校验库存是否足够→扣减库存→生成订单主记录→生成订单明细→计算总价。整个流程必须包在事务里否则会出现库存扣了但订单没生成、或者订单生成了库存没扣的数据不一致问题。Transactional(rollbackFor Exception.class) Override public SaleOrder createOrder(ListCartItem 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这是行级锁两个收银员同时抢最后一瓶可乐时第二个请求会在这一行上等着等第一个事务提交后才读到扣减后的库存天然避免超卖。金额计算全部用BigDecimaltotal.add(...)和multiply(...)链式操作杜绝double精度丢失。select idselectByIdForUpdate resultTypecom.example.supermarket.entity.Product SELECT * FROM product WHERE id #{id} FOR UPDATE /select update idreduceStock 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 classtable table-bordered thead tr th商品ID/th th商品名称/th th分类/th th单价/th th库存/th th操作/th /tr /thead tbody tr th:eachp : ${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} classbtn-add-cart加入购物车/button /td /tr /tbody /tableth: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是商品IDvalue是商品ID和数量的组合对象。Controller RequestMapping(/cart) public class CartController { GetMapping(/add) public String addToCart(RequestParam Long productId, RequestParam(defaultValue 1) Integer quantity, HttpSession session) { MapLong, CartItem cart (MapLong, 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.servletjavax.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 8IDEA里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没带characterEncodingutf8参数。解决建库时指定CHARSETutf8mb4url里加useUnicodetruecharacterEncodingutf8。两个都要做只改一个也白搭。数据已经乱了的表用ALTER TABLE product CONVERT TO CHARACTER SET utf8mb4抢救一下。5.3 部署运行与端口、Tomcat冲突现象启动时报Web server failed to start说明端口被占用。原因之前跑过一个没关掉的Spring Boot实例8080被占着。解决命令行执行netstat -ano | findstr 8080查到PIDtaskkill /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: 60000maximum-pool-size控制最多20个连接minimum-idle保持最少5个空闲连接connection-test-query每30秒探活一次确保被MySQL踢掉的旧连接能及时重建。这类问题不遇到时完全无感遇到一次能折腾一晚上属于毕设答辩演示翻车的重灾区。6. 交付前最后一步接口自测与关键路径验证6.1 用Postman把核心接口从头到尾跑一遍答辩前一天我会把核心接口按业务顺序过一遍确认数据流是通的。用Postman建一个Collection按顺序测登录→查询商品→加入购物车→模拟下单→查订单。接口方法路径预期结果管理员登录POST/login返回redirectSession写入用户商品列表GET/product/list返回商品表格页添加商品POST/product/add列表出现新商品加入购物车GET/cart/add?productId1quantity2购物车数量累加提交订单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节检查。我在交付前每次都强制走一遍这个流程从那以后答辩现场再没出过数据流断裂的丑。希望帮到你。本文还有配套的精品资源点击获取
返回列表