如果你正在为计算机毕业设计选题发愁,又对电商类项目比较感兴趣,那基于 SpringBoot 做的清河羊绒销售系统是个很值得参考的方向。它不是那种烂大街的图书管理或者简单商品展示,而是把羊绒产业带的真实交易场景压缩进了一个可运行、可演示、可答辩的系统里,涵盖用户端商城、商家端库存管理、平台端运营分析,核心链路从商品上架一路打通到下单支付和库存扣减。这篇文章我会用做毕设的实际思路,把需求分析、技术选型、数据库设计、核心代码逻辑以及我踩过的坑都拆开讲,适合准备做 SpringBoot 电商方向毕业设计、或者想快速上手一套完整企业级分层项目的同学。
1. 做这个项目之前,先搞清楚清河羊绒的业务逻辑
1.1 羊绒产业带的真实交易场景是什么
很多人一听“羊绒销售系统”,第一反应就是“这不就是个商城吗,商品加购物车加订单,网上模板一抓一大把”。但真的做下去会发现,如果只是复制一个通用电商 Demo,答辩时老师问两个业务问题就会露馅。清河羊绒产业带的特点是:上游有大量中小型羊绒加工企业和档口,下游有电商卖家、微商和散客,交易模式既包含 B2C 的零售,也包含小批量的批发拿货。
羊绒商品本身有几个明显特征。第一,季节性极强,每年 8 月到次年 1 月是销售旺季,过了季节库存就是压仓货。第二,SKU 维度复杂,同一款羊绒衫可能有多种颜色、多个尺码、不同纱线支数,价格还随克重浮动。第三,库存深度不一致,爆款可能随时断码,常规款则长期有货。第四,线上销售经常遇到“拍下不付款”的情况,订单在“待付款”状态占用了库存,到一定时间必须释放。
所以这套系统的定位不能是纯展示型商城,而应该是:面向普通消费者的线上交易端 + 面向商家的商品与库存管理端 + 面向平台运营的管理后台,三端合一,尤其要把“库存一体化”做扎实。库存一体化指的是,用户每次下单都要实时校验并扣减可售库存,取消订单或超时未支付要回补库存,商家端后台补货入库后前台可售库存同步变化,全链路不能出现超卖或者负数库存。
1.2 系统边界梳理:哪些功能必须做,哪些可以做成加分项
围绕上述场景,我把系统功能边界定了三层。
第一层是用户端,包含注册登录、商品分类浏览、关键字搜索、商品详情、购物车、结算下单、在线支付模拟、订单列表、取消订单、收货地址管理。第二层是商家端,包含商品管理和库存管理,比如添加商品时生成多规格 SKU、录入采购批次、上架下架、库存查询、库存预警、订单发货。第三层是平台管理端,包含用户管理、商家审核、分类管理、订单总览、销售统计报表。
我建议毕设至少把第一层和第二层完整做出来,第三层可以做简版,比如只保留数据统计图表和订单状态管理。比单纯堆功能更重要的是把业务闭环讲清楚:商品从 SKU 生成到库存入库,从前台下单到库存扣减,从支付成功到订单流转,每一步都要有明确的表结构和代码逻辑支撑。
如果你想让项目更有亮点,可以额外加分的内容包括:Redis 缓存热点商品和验证码、Spring 定时任务处理超时未支付订单、库存不足时的采购预警邮件/站内信。这些不加也不影响核心功能,但加了以后无论写论文还是答辩,都能明显提升技术层次。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 技术选型的取舍:SpringBoot 配上这些组合最适合毕设场景
2.1 为什么用 SpringBoot 而不是 SpringCloud 或 SSH
做毕业设计最重要的一条原则是:选型要兼顾工作量、稳定性和可解释性。SpringBoot 是目前最适合这个场景的落地方案,因为它把配置简化到了极致,内嵌 Tomcat,可以独立运行,不需要额外部署 Web 容器,开发调试都很顺畅。
有人喜欢在毕设里硬上 SpringCloud 微服务,把订单、商品、用户拆成三个独立服务,再用 Nacos 做注册中心。在真实项目里这是合理的,但作为本科毕设,微服务会带来分布式事务、服务间调用、部署复杂度等一系列问题,最后很可能陷入技术债务里,连完整演示都跑不通。反而是单体架构 + 模块化包结构,把 controller、service、mapper、entity、config 分清楚,既能体现分层设计思想,又不会把自己逼到死角。
我建议使用 SpringBoot 2.7.x 而不是 3.x,原因有两个:一是国内大量教程、博客和开源项目都基于 2.x,遇到问题能搜到的解决方案多;二是 3.x 要求 JDK17 及以上,很多同学本机环境还停留在 JDK8,切换成本大。当然,如果你已经习惯 JDK17,用 3.x 也没有问题,但要注意 MyBatis-Plus 等中间件需要选择适配版本。
2.2 选型清单:MyBatis-Plus、Redis、MySQL 和前端框架怎么搭配
持久层框架我首选 MyBatis-Plus。它有条件构造器,单表 CRUD 几乎不用手写 SQL,分页插件也很好用,做后台管理列表页能省很多时间。配合代码生成器,可以根据数据库表自动生成 entity、mapper、service 和 controller 骨架,一周内能把基础增删改查全部搭完。
数据库用 MySQL 8.0,理由是大三课程基本都学过,而且 8.0 对窗口函数的支持让统计报表更好写。缓存用 Redis,承担验证码存储、token 黑名单、热点商品缓存和库存预扣四类职责。需要说明的是,如果没有独立 Redis 环境,可以用 Windows 版 Redis 或者 Docker 起一个,但不要跳过,因为库存扣减部分用 Redis 是个很好的答辩亮点。
前端方面,如果你 Java 基础比较稳、时间紧,可以用 Thymeleaf 服务端渲染,整个项目部署成一个 Jar 包就能运行。如果你想体现前后端分离,那就用 Vue2 + ElementUI + Axios,开发接口时通过跨域配置让前端独立访问后端。考虑到毕设工期,我更推荐 Vue 分离方案,因为后台管理页面用 ElementUI 的表格、表单、对话框组件非常快,效果也好看,论文里还能截图展示独立的前端工程结构。
最终我的技术栈是:SpringBoot + MyBatis-Plus + MySQL + Redis + Vue2 + ElementUI + Maven + Git。这套组合的好处是每个组件都说得清楚“为什么用它”,不会出现答辩时被追问组件原理却答不出来的尴尬。
3. 数据库设计:羊绒行业的商品和库存,核心在于 SKU 和批次
3.1 商品表设计:SPU 与 SKU 分离是第一步
羊绒商品如果用一张表直接写商品名、颜色、尺码、价格、库存,那系统做到一半就废了。正确做法是 SPU 与 SKU 分离:SPU(Standard Product Unit)代表一款羊绒衫,SKU(Stock Keeping Unit)代表这款羊绒衫下面的具体规格项,比如“白色 M 码 100 支羊绒衫”就是一个 SKU。
product 表作为 SPU 表,保存商品主图、标题、详情富文本、品牌、产地纱线信息、上下架状态。product_sku 表保存具体规格项,比如颜色、尺码、价格、市场价、库存唯一标识、SKU 状态。两张表通过 product_id 关联,这样前台商品详情页展示规格选择时,就变成根据选中颜色和尺码查询对应 SKU 的库存和价格。
这里有个容易忽略的点:羊绒商品通常有色号名称,比如“米驼”“燕麦灰”“藏蓝”,这不是简单的一个 color 字段,因为在商家后台需要为每个色号配置图片,甚至同一颜色在不同批次下深浅不同。所以我会把颜色单独建一个字典表或者枚举字段,避免用户搜索时出现同色不同名的问题。规格维度的设计不用过度复杂,但颜色、尺码这两个字段务必独立成列,方便筛选和统计。
3.2 库存表设计:可售库存、锁定库存和采购批次三个概念别混
库存设计是这套系统的命门。我建议建两张核心表:stock_sku(SKU 库存表)和 stock_batch(入库批次表)。
stock_sku 表记录当前 SKU 的可售库存 stock、锁定库存 locked_stock、预警阈值 warn_stock。当用户下单成功时,可售库存减少,锁定库存增加;当订单取消或者超时关闭时,锁定库存减少,可售库存回补;当支付成功并完成发货后,锁定库存最终扣减。这样设计是为了解决“拍下未付款占库存”的典型问题。
stock_batch 表对应商家每次进货录入的批次记录,包含采购数量、入库时间、入库单价、供应商等。羊绒类商品批次很重要,不同批次的纱线质量、颜色可能与上次存在微差,真实的档口生意会按批次管理。在毕设里做批次表还有一个好处:库存流水记录可以成为项目里一个很自然的改进点,比如实现 stock_log 表记录每次变化的来源类型(下单、取消、支付、入库),答辩时直接解释“全链路库存可追溯”。
下面是简化的核心建表 SQL,你可以直接参考改造。
sql复制CREATE TABLE `product_sku` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`product_id` BIGINT NOT NULL COMMENT '商品ID',
`sku_code` VARCHAR(64) NOT NULL COMMENT 'SKU编码',
`color_name` VARCHAR(50) DEFAULT NULL COMMENT '颜色',
`size_name` VARCHAR(20) DEFAULT NULL COMMENT '尺码',
`price` DECIMAL(10,2) NOT NULL COMMENT '销售价',
`market_price` DECIMAL(10,2) DEFAULT NULL COMMENT '市场价',
`status` TINYINT DEFAULT 1 COMMENT '1上架 0下架',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_sku_code` (`sku_code`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
CREATE TABLE `stock_sku` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`sku_id` BIGINT NOT NULL,
`stock` INT NOT NULL DEFAULT 0 COMMENT '可售库存',
`locked_stock` INT NOT NULL DEFAULT 0 COMMENT '锁定库存',
`warn_stock` INT NOT NULL DEFAULT 5 COMMENT '预警阈值',
`update_time` DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_sku_id` (`sku_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
3.3 订单相关表:状态机设计宁可细一点也别偷懒
订单表是电商系统里数据关系最密集的部分。我的建议是 orders 主表存总金额、订单状态、用户ID、收货地址快照、支付方式、下单时间、支付时间、发货时间;order_item 子表存商品快照,包含商品名、SKU 编码、购买单价、数量、小计金额。这里一定要做“快照”,因为商品价格和标题后期可能修改,订单里的信息必须保持下单那一刻的原始状态。
订单状态建议用一个字段存数字枚举:0 待付款、1 已付款待发货、2 已发货、3 已确认收货、4 已完成、5 已取消、6 已退款。这个状态流转要形成闭环:下单生成待付款订单,支付成功变成已付款,商家发货变已发货,用户确认收货变已完成。取消动作既发生在待付款阶段,也发生在已付款后申请退款阶段,不同阶段对应的库存处理逻辑完全不同。
地址信息建议单独建表 user_address,下单时把选中的地址快照进 orders 表,而不是直接引用地址表,否则用户修改地址后历史订单的收货信息就乱了。这种细节未必会在需求文档里写明,但做出来以后,懂行的人一眼就能看出你对电商业务是有概念的。
4. 核心功能落地的关键代码路径:从登录到下单支付
4.1 用户登录鉴权:JWT + 拦截器比 Session 更适合前后端分离
在前后端分离结构下,用户登录状态建议使用 JWT 维护。用户在登录接口提交用户名和密码,后端校验通过后生成一个包含用户 ID、用户名、角色、过期时间的 token 串返回给前端。前端把 token 存在 localStorage 中,之后每次请求都在请求头携带 Authorization: Bearer token,后端通过拦截器解析 token 并放行对应接口。
密码存储绝对不要用明文,至少要用 BCrypt 加密。Spring Security 虽然有完整的安全体系,但如果只做登录鉴权,引入它会增加很多配置成本,我个人的做法是手写拦截器 + JWT,代码量可控,也方便在答辩时逐行讲解。
java复制@Component
public class JwtInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String token = request.getHeader("Authorization");
if (token == null || !token.startsWith("Bearer ")) {
throw new BusinessException("未登录或登录已过期");
}
String realToken = token.substring(7);
Claims claims = JwtUtil.parseToken(realToken);
if (claims == null) {
throw new BusinessException("token无效");
}
request.setAttribute("userId", claims.get("userId"));
return true;
}
}
这段代码的核心思路是:拦截器只负责“验身份”,真正的权限校验(比如商家端接口只有商家角色能访问)可以在 Controller 方法里通过自定义注解配合角色判断实现。不要把所有逻辑堆到拦截器里,否则代码会很臃肿。
4.2 商品搜索与详情:列表查询不要写成一坨大 SQL
前台商品列表页通常需要支持三个维度的筛选:分类、关键词、排序(默认综合 / 销量 / 价格)。用 MyBatis-Plus 的 LambdaQueryWrapper 可以优雅地拼装查询条件,分类属于精确查询,关键词属于模糊查询,排序条件动态传入。
商品详情页需要同时查出 SPU 信息和该商品下所有启用的 SKU。这里有两个查询技巧:一是详情页的富文本内容不要和列表查询放在同一张宽表里查,否则列表性能会受影响;二是 SKU 列表要关联 stock_sku 查询实时库存,库存为 0 的 SKU 在规格选择里置灰,不要让用户选了之后才提示“无货”。
我在实际开发中踩过一个坑:商品列表页图片字段在数据库里存的是相对路径 /upload/1.jpg,前端直接拼接 <img src="/upload/1.jpg"> 在开发环境正常,部署到服务器后因为项目路径带 context-path,图片全部 404。解决办法是定义一个全局配置类,把虚拟路径映射到本地磁盘目录:
yaml复制spring:
web:
resources:
static-locations: file:D:/upload/,classpath:/static/
4.3 下单与库存扣减:一个事务里面别只写三行代码
下单是整个系统最核心的接口,业务上要完成五件事:校验购物车选中商品、计算总金额、创建订单主表和子表、扣减商品库存、清空购物车中已下单的商品。这几步必须放在同一个事务里,任何一个环节异常都要整体回滚。
库存扣减的逻辑我推荐用“数据库乐观扣减”的方式,核心是 SQL 里的库存条件判断,而不是先查出来再减。看下面这个例子:
sql复制UPDATE stock_sku
SET stock = stock - #{count},
locked_stock = locked_stock + #{count}
WHERE sku_id = #{skuId}
AND stock >= #{count}
这条 SQL 利用数据库的行锁机制保证同一时刻只有一个会话能成功修改这一行。如果影响行数是 0,说明库存不足,此时直接抛出异常回滚整个订单。这种写法不需要 Redis 也能保证不超卖,对于毕设量级的并发完全够用,也更容易在论文里写清楚。
订单创建时还需要生成一个唯一的订单编号。我用的规则是:日期 + 用户ID后四位 + 随机数,比如 202506081030120000102581。不要直接使用数据库自增 ID 作为订单号,因为订单号需要具备追溯性,也方便外部分散沟通。
4.4 支付模块的折中做法:模拟支付回调比接入真实支付更适合毕设
真实接入微信支付或支付宝支付,需要企业资质、商户号、证书、回调配置,学生个人账号很难申请下来。我建议做一个“模拟支付”模块:用户在前端点击“去支付”,弹出一个模拟收银台,点击确认支付后,后端把订单状态从待付款改为已付款,同时增加一条支付流水记录。
但为了让项目更有说服力,我会把模拟支付做得像真实支付一样:定义统一的支付接口,同一个方法里预留商户回调解析的逻辑,在开发环境中通过一个 mock 服务直接触发回调。这样在论文中可以描述为“为了保证演示环境可运行,实际接入时为商家支付网关的模拟实现”,答辩老师通常都会认可。
支付成功后的一个隐藏逻辑是锁定库存的最终扣减:
sql复制UPDATE stock_sku
SET locked_stock = locked_stock - #{count}
WHERE sku_id = #{skuId}
这一步容易被忽略。如果只扣可售库存不处理锁定库存,库存数字会越滚越乱。正确顺序是:下单时可售转锁定,支付成功时锁定扣减,取消时锁定回补可售。
5. 那些答辩一定会问的难点:超卖、库存一致性和数据统计
5.1 超卖问题的完整回答思路
如果老师问你“你这个系统在高并发下会不会超卖”,你不能只说“用了事务”。需要按三层来回答。
第一层是数据库兜底,也就是上面那条带 stock >= #{count} 条件的 UPDATE 语句,这一层能保证无论多少并发请求,最终实际扣减的总数不可能超过库存总量。第二层是应用层优化,在高并发抢购场景下可以通过 Redis 预扣库存来削峰,先把请求打到 Redis 上,库存扣减成功后再异步落库,这个方案对缓存和数据库的一致性要求更高。第三层是业务层兜底,比如限制单个用户对同一 SKU 的购买数量,防止黄牛式抢购。
毕设阶段建议把第一层完整实现并加入压测对比,比如用 Jmeter 开 100 个线程同时下单一个库存为 10 的商品,最终成功订单数必须小于等于 10,且库存不为负数。把这个测试结果截图放进论文,比写十页理论都管用。
5.2 库存一致性场景怎么讲清楚
我把库存拆成可售库存和锁定库存后,系统里其实有两种一致性要保证。第一种是订单状态与库存状态的一致:取消订单、超时关闭、支付成功,都要在同一个事务里修改 order 表和 stock_sku 表。第二种是缓存与数据库的一致:如果用 Redis 缓存了 SKU 库存,那每次扣减后要主动更新缓存,不能只等过期时间。
对于超时未支付订单,我用了 Spring 的定时任务 @Scheduled,每隔 30 秒扫描创建时间超过 15 分钟且仍处于待付款状态的订单,把订单改为已关闭,并回补锁定库存。这个功能实现起来很简单,但效果极好,它让系统在一定程度上有“自愈”能力,答辩时讲到业务闭环也更有底气。
java复制@Scheduled(fixedDelay = 30_000)
public void closeExpiredOrders() {
List<Order> expiredOrders = orderMapper.selectExpiredUnpaidOrders(15);
for (Order order : expiredOrders) {
orderService.cancelByTimeout(order.getId());
}
}
5.3 管理端统计报表:用 ECharts 展示三个核心维度
平台管理端如果只有 CRUD 会显得单薄,我加了一个销售统计模块,用 ECharts 柱状图展示近 7 天销售额趋势,用饼图展示不同分类商品的销售占比,用表格展示商家维度销售排行。这部分的 SQL 主要是 group by 和 sum,配合日期函数按天分组就可以。
sql复制SELECT DATE_FORMAT(pay_time, '%Y-%m-%d') AS day,
SUM(total_amount) AS total_amount
FROM orders
WHERE pay_status = 1
AND pay_time >= DATE_SUB(NOW(), INTERVAL 7 DAY)
GROUP BY DATE_FORMAT(pay_time, '%Y-%m-%d')
ORDER BY day ASC
这类统计查询是 MySQL 的强项,不需要引入复杂的 BI 组件。做完之后界面很直观,论文里的“系统测试”章节也能有真实的页面截图支撑。
6. 实话实说:我在这类项目里踩过的坑和复盘总结
6.1 三个最容易让人浪费一整天的问题
第一个坑是金额字段用了 double 类型。我在开发时一度为了省事用 double 存储价格,结果下单计算出现 0.1 + 0.2 = 0.30000000000000004 这种问题,数据库金额精度错乱,对账怎么都对不上。后来全部改成 BigDecimal,入库字段对应 DECIMAL(10,2)。这是电商项目的基本常识,但只有踩过一遍才会真正记住。
第二个坑是 MyBatis-Plus 的字段自动填充。创建时间和更新时间如果每次插入都要手动 set,非常繁琐,还容易漏。正确做法是在实体类字段上配置 @TableField(fill = FieldFill.INSERT),再写一个 MetaObjectHandler 组件,让插入时自动填充创建时间和更新时间,更新时自动刷新更新时间。
第三个坑是前端跨域。前后端分离模式下,Vue 跑在 8080 端口,后端跑在 8081 端口,直接请求会被浏览器拦截。我刚开始只在后端配了 @CrossOrigin,后来发现拦截器拦截的请求在进入 Controller 之前依然会被跨域校验挡住。解决方案是要实现 WebMvcConfigurer 统一配置跨域规则。
java复制@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOriginPatterns("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowCredentials(true)
.maxAge(3600);
}
}
注意:allowedOriginPatterns 和 allowCredentials(true) 要搭配使用,直接用 allowedOrigins("*") 加 allowCredentials(true) 在某些版本的 Spring 里会报错。
6.2 事务失效的隐蔽原因:同类调用与 try-catch 吞异常
下单接口里我一开始直接把事务注解加在 controller 方法上,后来在 service 内部写了另一个方法去调用 this.createOrder(),结果异常发生后数据没有被回滚,查了很才发现是同类内部方法调用绕过了 Spring 代理,导致 @Transactional 失效。解决办法是把事务方法拆到独立 service 中,或者在同类调用时注入自身代理对象。
还有一次,我在带事务的方法里 try catch 了所有异常并返回一个错误提示给前端,结果数据也跟着提交了,因为事务只对未捕获的运行时异常回滚。修正方式是让异常继续向上抛,在 controller 统一异常处理中做响应包装。这里建议项目里写一个全局的 @RestControllerAdvice,把业务异常、参数校验异常、未知异常分别处理,前端拿到的错误信息也会规范很多。
6.3 项目收尾阶段的交付建议
做完项目不意味着做完毕设,交付环节有大量看不见的功夫。环境方面,我建议把 JDK、MySQL、Redis 的版本写进项目 README,再写一份启动手册,包括导入数据库脚本、修改配置文件的步骤、如何启动后端和前端,这样答辩老师拿过去也能自己跑起来。
数据库中一定要准备演示数据,不要用空的 insert into 列表去答辩。每个分类放三五个商品,SKU 库存刻意设置一个存在库存的爆款和一个库存为 0 的款式,这样既方便演示正常下单,也方便演示“无货状态下规格置灰”和“库存不足下单失败”的业务逻辑。
最后给你的个人建议是:控制好项目的边界,把核心链路做深,比多堆十个没用的功能模块更能体现工程能力。我见过太多同学把大量时间耗在花哨的页面动画和伪需求上,却在库存扣减、事务一致性这些真正重要的问题上糊弄过去。SpringBoot 清河羊绒销售系统的价值不在于“羊绒”这两个字,而在于它强迫你把电商交易链路上的每一个关键决策都理清楚——从 SKU 建模到库存锁定,从订单状态到支付回调,这些基本功打磨透了,以后无论做什么项目都能直接迁移过去。
