☰
基于SpringBoot的服装销售平台设计与实现:SKU表结构、下单扣库存与源码避坑指南
2026/10/27 6:36:45 网站建设 项目流程

简介:这是一套基于SpringBoot的服装销售平台完整项目源码,面向计算机专业学生、Java初学者及需要课程设计或毕业设计参考的开发者,帮助解决从零搭建电商类Web系统的实际需求。项目采用Java、SpringBoot、Vue、Ajax、Maven、MySQL与MyBatisPlus等技术栈,前后端分离,涵盖用户信息管理、图片素材管理、视频素材管理等核心模块,并附有绪论、相关技术介绍、系统分析、系统设计、数据库设计与系统实现等完整文档章节,便于理解整体架构与开发思路。资源包共862个文件,包含146个Java源文件、52个Vue组件、153个JavaScript脚本、46个HTML页面、44个CSS样式及大量svg、gif、jpg等图片素材,另有xml配置、sql相关与bat启动脚本,压缩包约17.98MB,目录结构清晰,便于按模块查阅与二次开发。目前已有111人学习下载,适合需要快速掌握SpringBoot电商项目开发流程、对照源码查漏补缺的读者参考使用。

1. 服装销售平台从零到一:一套 SpringBoot 源码到底能跑出什么

电商系统里,服装类目是最难啃的骨头之一。同一个 SPU 下要挂 S/M/L/XL 四个尺码、五六种颜色,每个组合还得独立管库存和价格,这跟卖一本书、一个手机完全不是一个量级。很多 Java 工程师想拿一套服装销售平台源码来练手或做二次开发,结果拉下来发现跑不起来、表结构看不懂、下单逻辑绕成一团。这篇笔记就围绕「基于 SpringBoot 的服装销售平台设计与实现」这条线,把技术选型、表结构设计、核心链路代码、部署踩坑一次讲透。适合正在找毕业设计选题的在校生、想补一个完整项目经验的初中级 Java 工程师,以及需要快速搭一套服装电商原型的小团队。读完你能自己判断一套源码值不值得改,也能从零把最小可运行版本搭出来。

2. 技术选型与工程骨架:为什么是 SpringBoot + MyBatis-Plus 而不是别的

2.1 服装电商场景对技术栈的真实要求

服装销售平台跟通用商城最大的区别在 SKU 维度爆炸。一件 T 恤,4 个尺码 × 5 个颜色 = 20 个 SKU,每个 SKU 有独立库存、独立条码、可能还有独立售价。这意味着数据访问层要频繁做多条件组合查询,对 ORM 框架的动态 SQL 能力要求很高。

选 SpringBoot 的理由很直接:自动配置省掉大量 XML,起步依赖把 Web、事务、连接池一次性拉齐,内嵌 Tomcat 让部署变成java -jar一条命令。选 MyBatis-Plus 而不是 JPA,核心原因是服装平台的查询条件太灵活——后台要按季节、品类、尺码段、价格区间、上架状态任意组合筛选,MyBatis-Plus 的QueryWrapper和自定义 XML 能精确控制 SQL,不会像 JPA 那样在复杂关联查询时生成一堆看不懂的嵌套语句。

数据库用 MySQL 8.x,缓存用 Redis 存购物车和热点商品,前端可以是 Vue 打包后放进 SpringBoot 的static目录,也可以前后端分离部署。如果你的项目里还涉及实时销售大屏,SpringBoot 整合 Flink 做流式统计也是一条常见路线,但那是进阶需求,最小版本不需要。

2.2 工程目录结构与依赖配置

一个能跑起来的服装销售平台,后端目录大致长这样:

src/main/java/com/example/clothing/ ├── ClothingApplication.java // 启动类 ├── config/ // 配置类 │ ├── MybatisPlusConfig.java // 分页插件、乐观锁插件 │ ├── RedisConfig.java // 序列化配置 │ └── WebMvcConfig.java // 跨域、拦截器注册 ├── controller/ // 接口层 ├── service/ // 业务层 │ └── impl/ ├── mapper/ // 数据访问层 ├── entity/ // 数据库实体 ├── dto/ // 传输对象 ├── vo/ // 视图对象 └── common/ // 统一返回、异常处理

pom.xml里必须锁死版本,SpringBoot 版本太高会导致 MyBatis-Plus 的某些插件不兼容,这是高频翻车点。我一般用 SpringBoot 2.7.x 配 MyBatis-Plus 3.5.x,稳定且资料多。

<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> </parent> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.3.1</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies>

application.yml里几个关键配置不能漏:

spring: datasource: url: jdbc:mysql://localhost:3306/clothing_mall?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 mybatis-plus: mapper-locations: classpath:mapper/*.xml global-config: db-config: logic-delete-field: deleted # 逻辑删除字段 logic-delete-value: 1 logic-not-delete-value: 0

逻辑删除字段deleted是电商系统的后悔药——商品下架、订单取消都不做物理删除,方便对账和追溯。分页插件必须在MybatisPlusConfig里注册,否则Page对象查出来永远是全量数据。

@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分页插件,指定数据库类型 interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); // 乐观锁插件,用于库存扣减 interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor()); return interceptor; } }

分页插件不注册,后台商品列表一次查出几万条,接口直接超时。乐观锁插件不注册,@Version注解不生效,并发扣库存必出超卖。这两个是骨架里最容易被忽略但后果最严重的配置。

3. 服装 SKU 表结构设计与核心链路代码

3.1 商品-SPU-SKU 三表怎么拆

服装平台的数据模型核心就三张表:product(商品/SPU)、product_spec(规格项)、product_sku(具体库存单元)。很多人图省事把尺码颜色塞进一个 JSON 字段,后期做库存扣减和销量统计时哭都来不及。

表名关键字段说明
productid, name, category_id, season, main_image, statusSPU 主表,存公共信息
product_specid, product_id, spec_name, spec_values规格定义,如「颜色:红,蓝;尺码:S,M,L」
product_skuid, product_id, spec_json, price, stock, versionSKU 表,spec_json 存组合值

product_sku的spec_json存{"颜色":"红","尺码":"M"}这样的结构,配合version字段做乐观锁。查询时用 MySQL 的 JSON 函数或直接在应用层解析,服装平台 SKU 数量通常几百到几千,应用层解析完全够用。

3.2 下单扣库存的完整代码链路

下单是服装平台最容易出问题的环节。用户选中某个 SKU,数量 1,点击提交,后端要做的事:校验库存 → 锁定库存 → 生成订单 → 扣减库存。高并发下必须用乐观锁或 Redis 预扣。

@Service public class OrderServiceImpl implements OrderService { @Autowired private ProductSkuMapper skuMapper; @Autowired private OrderMapper orderMapper; @Override @Transactional(rollbackFor = Exception.class) public OrderVO createOrder(Long skuId, Integer quantity, Long userId) { // 1. 查询 SKU,带 version 字段 ProductSku sku = skuMapper.selectById(skuId); if (sku == null || sku.getStock() < quantity) { throw new BizException("库存不足"); } // 2. 乐观锁扣减:UPDATE ... SET stock = stock - ? , version = version + 1 WHERE id = ? AND version = ? int affected = skuMapper.deductStock(skuId, quantity, sku.getVersion()); if (affected == 0) { // 版本冲突,说明有其他线程同时扣减,让用户重试 throw new BizException("当前下单人数过多,请重试"); } // 3. 生成订单 Order order = new Order(); order.setUserId(userId); order.setSkuId(skuId); order.setQuantity(quantity); order.setTotalPrice(sku.getPrice().multiply(new BigDecimal(quantity))); order.setStatus(OrderStatus.PENDING_PAY.getCode()); orderMapper.insert(order); return convertToVO(order); } }

对应的 Mapper XML:

<update id="deductStock"> UPDATE product_sku SET stock = stock - #{quantity}, version = version + 1 WHERE id = #{skuId} AND version = #{version} AND stock >= #{quantity} </update>

WHERE条件里同时带version和stock >= quantity是双保险。version防并发覆盖,stock >= quantity防超卖。@Transactional保证订单插入和库存扣减在同一事务里,任何一步失败全部回滚。注意rollbackFor = Exception.class不能省,否则受检异常不回滚。

3.3 购物车用 Redis 还是 MySQL

购物车数据读写频率极高,放 MySQL 会给数据库带来不必要的压力。常见做法是用 Redis Hash 存储:key 为cart:{userId},field 为skuId,value 为数量。

@Service public class CartService { @Autowired private StringRedisTemplate redisTemplate; private static final String CART_KEY = "cart:%d"; public void addToCart(Long userId, Long skuId, Integer quantity) { String key = String.format(CART_KEY, userId); // Hash 结构:field=skuId, value=quantity redisTemplate.opsForHash().increment(key, skuId.toString(), quantity); // 设置过期时间,避免冷用户购物车永久占用内存 redisTemplate.expire(key, 7, TimeUnit.DAYS); } public Map<Object, Object> getCart(Long userId) { String key = String.format(CART_KEY, userId); return redisTemplate.opsForHash().entries(key); } }

increment保证同一 SKU 多次加购数量累加,expire设置 7 天过期。下单时从 Redis 读取购物车,清空对应 field,再走上面的扣库存流程。Redis 挂了购物车会丢,对服装平台来说可接受,但订单数据必须落 MySQL。

4. 避坑与排查:服装销售平台源码跑不起来的 5 个高频原因

4.1 启动报错 Unknown database 或表不存在

现象:java.sql.SQLSyntaxErrorException: Table 'clothing_mall.product' doesn't exist。

原因:源码包里通常只给了.sql文件,但没说明要先建库再导入,或者 SQL 文件里的库名和application.yml里配的不一致。

解决:先CREATE DATABASE clothing_mall DEFAULT CHARACTER SET utf8mb4;,再USE clothing_mall;然后source xxx.sql;。导入后执行SHOW TABLES;确认表数量。如果 SQL 文件里写死了USE other_db,手动改成你的库名。

4.2 SpringBoot 版本太高导致 MyBatis-Plus 报错

现象:启动时java.lang.NoClassDefFoundError: org/mybatis/logging/LoggerFactory或Invalid bound statement (not found)。

原因:SpringBoot 3.x 默认用 Jakarta EE 命名空间,而老版本 MyBatis-Plus 还在用javax。或者 MyBatis-Plus 版本和 SpringBoot 版本不匹配。

解决:要么把 SpringBoot 降到 2.7.x,要么把 MyBatis-Plus 升到 3.5.3+ 并使用mybatis-plus-spring-boot3-starter。我一般直接锁 2.7.18 + 3.5.3.1 这套组合,省心。

4.3 前端 Vue 打包后放进 static 目录刷新 404

现象:首页能打开,点进商品详情再刷新,浏览器报 404。

原因:Vue 的 history 路由模式需要服务端把所有未匹配路径转发到index.html,SpringBoot 默认不处理。

解决:加一个配置类,把非 API 路径转发到首页。

@Configuration public class WebMvcConfig implements WebMvcConfigurer { @Override public void addViewControllers(ViewControllerRegistry registry) { // 所有非 /api 开头的路径都转发到 index.html registry.addViewController("/{path:[^\\.]*}") .setViewName("forward:/index.html"); } }

注意/{path:[^\\.]*}这个正则排除了带点的静态资源请求,否则 JS/CSS 文件也会被转发。

4.4 库存扣减出现负数

现象:压测或多人同时下单后,stock字段变成负数。

原因:扣减 SQL 只写了SET stock = stock - #{quantity},没有WHERE stock >= #{quantity}条件,或者没用乐观锁。

解决:扣减语句必须同时带version和stock >= quantity条件,检查affected rows是否为 0。为 0 就抛异常让用户重试,绝不能默默继续。

4.5 图片上传后访问 404

现象:后台上传商品图片成功,但前端<img src>加载不出来。

原因:文件存到了服务器本地磁盘,但没配置静态资源映射,SpringBoot 不知道去哪个目录找。

解决:在application.yml里配file.upload-path,然后加映射。

@Configuration public class ResourceConfig implements WebMvcConfigurer { @Value("${file.upload-path}") private String uploadPath; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { // 把 /images/** 映射到本地上传目录 registry.addResourceHandler("/images/**") .addResourceLocations("file:" + uploadPath); } }

file:前缀不能少,否则会被当成 classpath 路径。上传目录末尾要带/,这是血泪经验。

5. 从能跑到好用:接口压测与二次开发切入点

5.1 用 JMeter 验证下单接口的并发安全性

源码跑起来只是第一步,你得知道它在并发下什么表现。用 JMeter 对/api/order/create发 200 个并发请求,观察库存是否精确扣减、有没有超卖、响应时间是否可接受。

线程组配置:线程数 200,Ramp-Up 1 秒,循环 1 次。HTTP 请求填localhost:8080/api/order/create,Body 里带skuId和quantity。跑完后查数据库:

SELECT stock, version FROM product_sku WHERE id = 1; -- 预期:stock = 初始库存 - 成功订单数,version = 初始版本 + 成功订单数 SELECT COUNT(*) FROM orders WHERE sku_id = 1; -- 订单数应等于库存扣减量

如果stock出现负数,说明乐观锁没生效,回去检查MybatisPlusConfig里插件是否注册、实体类version字段有没有加@Version注解。如果订单数远小于库存扣减量,说明事务边界有问题,检查@Transactional是否加在 public 方法上、是否被同类内部调用绕过代理。

5.2 二次开发的三个高价值切入点

拿到一套服装销售平台源码,别急着改页面。先看三个地方:

第一,SKU 生成逻辑。很多源码的商品发布页是让你手动一个个填 SKU,效率极低。你可以改成笛卡尔积自动生成:选好颜色和尺码后,后端自动组合出所有 SKU 记录,运营只需批量填价格和库存。这个改动对实际使用体验提升最大。

第二,订单状态机。检查订单状态流转是否用了状态机模式。如果只是if-else堆砌,加一个「换货」状态就会牵一发动全身。用枚举 + 状态流转表重构,后续加售后流程会轻松很多。

第三,缓存策略。商品详情页是读多写少的典型场景。如果源码里每次请求都查数据库,加一层 Redis 缓存,设置 5 分钟过期,配合更新时主动删除缓存。注意缓存穿透问题,对不存在的商品 ID 也缓存空值,过期时间设短一点,比如 60 秒。

public ProductVO getProductDetail(Long productId) { String cacheKey = "product:detail:" + productId; // 先查缓存 String cached = redisTemplate.opsForValue().get(cacheKey); if (cached != null) { return JSON.parseObject(cached, ProductVO.class); } // 查数据库 ProductVO vo = loadFromDb(productId); if (vo == null) { // 缓存空值防穿透,60 秒过期 redisTemplate.opsForValue().set(cacheKey, "", 60, TimeUnit.SECONDS); return null; } redisTemplate.opsForValue().set(cacheKey, JSON.toJSONString(vo), 5, TimeUnit.MINUTES); return vo; }

缓存空值时存空字符串而不是null,因为 Redis 存不了null。读取时判断cached != null且非空字符串才反序列化。

5.3 一个容易被忽略的细节:逻辑删除与唯一索引冲突

服装平台里商品编码、SKU 条码通常要加唯一索引。但如果你用了逻辑删除,删除一条记录后再插入相同编码,唯一索引会报冲突——因为旧记录还在表里,只是deleted = 1。

解决办法有两个:一是唯一索引带上deleted字段,变成联合唯一索引UNIQUE KEY (code, deleted),但这样只能删一次;二是删除时把编码改掉,比如追加时间戳后缀。我一般用第二种,在delete方法里把code更新为code + '_del_' + System.currentTimeMillis(),再设deleted = 1。这样既保留了历史数据,又不影响新记录插入。

这套方案我前后搭过三次,每次都会在库存扣减和缓存一致性上翻车,后来养成了习惯:任何涉及库存的 SQL 必须带stock >= quantity,任何缓存更新必须先操作数据库再删缓存。希望帮到你。

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

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

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

立即咨询