现在市面上关于“SpringBoot + 电商”的教学项目,十个里起码有八个是图书管理、学生选课、二手交易这类老面孔。蛋糕烘焙销售服务系统这个题目,看起来只是换了一层皮,但真做起来,你会发现它跟普通电商系统的差别不是一点点。蛋糕这个品类有几个跟卖数码产品完全不一样的特点:非标品占大头(尺寸、口味、奶油类型、加料自己说了算)、保质期以小时计、配送成功率完全依赖时间窗,还有那个动不动就让你抓狂的“当天现做”逻辑。
这篇文章我打算把做这样一个系统时最容易踩的坑、最值得展开的设计点,从需求拆解到数据库模型,再落到关键接口的实现细节,完整写一遍。如果你正好在准备类似项目,不管是毕业设计、个人作品集,还是想给亲戚的小店搞个真正的线上化工具,顺着这套思路走,能帮你少加至少三十天的班。
1. 项目整体设计:先想清楚你到底是卖货还是做服务
很多人拿到“基于SpringBoot的蛋糕烘焙销售服务系统”这个标题,第一反应是照抄电商模板:商品表、订单表、购物车表,抄完就开写。但实际操作下来,你会发现这套逻辑在蛋糕行业局部失灵。
1.1 核心需求解析
先把这个系统面对的真实场景摆出来。一家蛋糕门店,日常经营里主要有四类角色需要被系统服务:顾客,负责浏览蛋糕、下单并支付;店员/客服,负责接单、确认订单、处理售后;烘焙师/后厨,需要看到制作任务并更新制作进度;店长/老板,要看销量、库存损耗、回头客等经营数据。
对应的系统边界就很清楚了:用户端要有商品展示、规格选择、购物车、下单支付、订单追踪;管理端要有商品管理、库存管理、订单处理、会员与积分、基础数据统计。注意,这里我没有提角色权限,但实际项目里必须要有,SpringBoot生态里用Spring Security或者Sa-Token都能解决,后面细说。
1.2 为什么不用微服务那一套
这是我最想吐槽的地方。现在网上很多同题项目一上来就是注册中心、网关、分布式事务,看起来高大上,实际对这个场景属于过度设计。蛋糕烘焙销售服务,业务量级撑死一天几百单,数据库单表百万级都用不完,单体应用加缓存已经完全够了。
技术选型上我的建议是个体面且务实的组合:
| 层级 | 选型 | 用途说明 |
|---|---|---|
| 框架 | SpringBoot 2.7+ | 快速搭建Web服务,天然集成众多组件 |
| ORM | MyBatis-Plus | 单表CRUD不用写SQL,复杂查询再手写XML |
| 数据库 | MySQL 8.0 | 稳定可靠,支持JSON字段和事务 |
| 缓存 | Redis | 首页热点数据、购物车临时保存、分布式锁 |
| 鉴权 | Sa-Token 或 JWT | 用户状态管理和权限控制 |
| 前端 | Vue3 + Element Plus | 管理端界面;用户端可h5或小程序思路迁移 |
这个组合的好处是每一层都有明确的替代方案,踩坑时搜索引擎能搜到足够多的现成答案。单体应用还有个大优势:本地启动个MySQL和Redis就能跑,导师演示或者自己录系统展示视频都方便,不需要连线上服务器折腾一晚上。
1.3 功能模块如何划分
我习惯先画一条用户操作的主链路:浏览 → 加购 → 提交订单 → 支付 → 商家确认 → 制作 → 配送/自提 → 完成 → 评价。这九个环节能拆出以下核心模块:
- 商品模块:分类列表、详情页、规格组合(如1磅/2磅、动物奶油/植物奶油、黄桃/芒果夹心)。
- 购物车模块:临时保存用户选中的商品与规格,支持修改数量、清空。这里建议直接用Redis存,选型理由后面讲。
- 订单模块:订单创建、取消、超时关闭、支付回调处理、订单状态机流转。
- 库存模块:每日库存量受原料和制作产能限制,需要按日期扣减。
- 用户模块:注册登录、收货地址、个人资料、会员等级与积分。
- 营销模块:优惠券、满减活动、限时折扣(这块锦上添花,但要做就做完整)。
- 后台管理:商品上下架、库存维护、订单处理、数据看板。
需求拆到这一步,项目边界就清晰了。接下来比功能更重要的,是数据库设计。说句难听的,功能没做完还能演示核心链路,表设计错了,那可是全链路都受影响。
2. 数据库设计与核心业务模型
蛋糕系统跟普通电商最大的区别在哪?两个字:规格。普通电商的商品规格一般是颜色、内存大小、套装版本,属性简单,规格参数也固定。但一个生日蛋糕的规格是组合式的:磅数决定大小,奶油类型、夹心选择、水果配料都是用户可以点的,这些选项排列组合,直接决定价格和制作难度。
2.1 商品与规格的建模思路
如果照抄普通电商的SPU/SKU表结构,你会发现蛋糕的SKU是膨胀的。一个蛋糕有3种磅数、2种奶油、4种夹心,组合就是24个SKU,这还不算加料区的可选项。传统做法是提前把SKU表铺满,每个组合一个库存,维护起来极其痛苦。
我的做法是商品表(product)只存基础信息,如名称、默认展示图、起售价、简介等;规格维度用一张规格选项表(product_spec_option)去描述,记录规格名(磅数/奶油/夹心)以及对应的选项和加价金额;购物车和订单行项里,则保存用户选中的规格组合快照。
CREATE TABLE product_spec_option ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, spec_name VARCHAR(50) NOT NULL, spec_value VARCHAR(100) NOT NULL, price_delta DECIMAL(10,2) DEFAULT 0, sort_order INT DEFAULT 0, is_required TINYINT DEFAULT 1 );这样做的好处是,蛋糕上架时不需要手工铺几十个SKU,只需要配置规格维度;下单时按选中的选项实时计算价格。缺点则是库存不能精确到SKU,只能靠每日总库存控制,这一点对蛋糕场景完全够用。
2.2 库存与日期
蛋糕库存跟普通商品还有个本质差异:普通商品库存是“总量递减”,蛋糕库存是“按日期切分”。消费者今天买明天配送的蛋糕,消耗的是明天的产能,不是今天的。所以库存表上一定要带一个“商品日期”字段。
我设计了一张蛋糕每日库存表(product_daily_stock):
CREATE TABLE product_daily_stock ( id BIGINT PRIMARY KEY AUTO_INCREMENT, product_id BIGINT NOT NULL, stock_date DATE NOT NULL, total_stock INT NOT NULL, booked_stock INT NOT NULL DEFAULT 0, create_time DATETIME, update_time DATETIME, UNIQUE KEY uk_product_date (product_id, stock_date) );下单时活动的字段是booked_stock,制作完成后库存消耗才真正落到total_stock上。搞懂“预约库存”和“实际库存”的区别,是蛋糕系统库存设计的关键一步。
2.3 订单状态机
订单系统最值得画心思的,是状态流转设计。蛋糕订单跟普通订单状态还不太一样,多出“制作中”和“待自提/待配送”两个环节。
我的订单状态表设计如下:
- 待支付 → 已取消(用户主动,或超时自动)
- 待支付 → 已支付
- 已支付 → 制作中(商家接单/确认制作)
- 制作中 → 待配送/待自提(制作完成)
- 待配送/待自提 → 已完成(用户确认收货或系统自动完成)
- 已支付/制作中 → 退款中 → 已退款(售后场景)
状态流转要用状态机驱动,而不是随处status = 某值。我习惯在代码里建一个状态流转处理器,把合法的动作枚举和前置状态约束放在一个地方。不然开发到后半程,订单到处被改状态,排查问题能查到怀疑人生。
2.4 用户与地址设计细节注意
地址表不做成复制的简单表,而是要支持“默认地址”逻辑。有些项目直接在user表存一个默认地址ID,看起来简单,但用户删除旧地址后默认关联就断了。我建议地址表独立,加一个is_default字段,每次设置默认地址时,使用事务把该用户所有地址is_default置0,再更新指定地址为1。
会员表方面,可以直接在用户表冗余一个等级字段,或者单独建会员等级表。这个项目的核心还是销售流程,不需要把会员系统做得很重。但积分功能可以做:下单、评价送积分,积分抵扣金额,这个功能点的复现率很高,加分的性价比也高。
3. 核心功能实现:从下单到出库的完整链路
功能开发顺序上,我个人的建议是:先打通订单主链路(支付 → 状态回调 → 库存扣减 → 订单详情),再回头完善购物车、商品展示等周边的功能。因为主线一通,整个项目的骨架就稳了,后续加模块都是在骨架里填肉。
3.1 用户端购物车为什么选Redis
购物车这个功能,很多教材都是用MySQL建cart表,持久化保存。但对一个蛋糕门店系统,购物车的生命周期很短,一般用户今天选好明天就下单,而且大部分是访客浏览完了直接下单,根本不会去点“加入购物车”。用MySQL表存购物车,等于为一个低价值数据占用宝贵的数据库资源。
我的方案是:登录用户购物车以Redis Hash存储,key为cart:userId,field为商品ID:规格选项MD5,value为选中数量。同时保存规格快照信息,避免用户改动规格导致购物车数据对不上。
@Resource private StringRedisTemplate stringRedisTemplate; public void addCartItem(Long userId, CartItemDTO dto) { String key = "cart:" + userId; String field = dto.getProductId() + ":" + dto.getSpecKey(); // 序列化规格快照为JSON String itemJson = JSONUtil.toJsonStr(dto); stringRedisTemplate.opsForHash().put(key, field, itemJson); }好处很明显:读写快,不产生数据库碎片。缺点也很明显——Redis会重启丢数据,导致购物车清空。但对蛋糕店场景,这个缺点几乎不存在,购物车清空用户重新加一遍就行,不会有人为了这个去投诉你。这个取舍你要心里有数。
3.2 下单流程设计:事务边界要窄
下单核心接口我强烈建议用事务注解但只包裹必要操作:生成订单主表和明细 → 创建支付单 → 标记库存预约。查询、库存预检查、价格校验这些前置操作全部放在事务外。
@Transactional(rollbackFor = Exception.class) public OrderVO createOrder(CreateOrderDTO dto) { // 1. 生成订单号(后面单独说) // 2. 计算订单总价、保存订单主表 // 3. 解析规格选项快照,保存订单明细 // 4. 创建支付流水 // 5. 扣减预约库存(乐观锁控制) return OrderVO.from(order); }这么设计的原因:事务越宽,锁持有时间越长,并发高的场景下数据库连接会被拖死。蛋糕订单的并发不会太夸张,但事务规范这件事应该养成好习惯。
3.3 订单号的生成方式
订单号这事,很多新手直接使用数据库自增ID,或者时间戳+随机数。自增ID的问题在于容易被别人通过订单号推测出你的日订单量,而且多表共享主键序列时会产生冲突。时间戳在并发高的时刻会重复,需要附带随机数。
我的习惯是用Redis生成每日序列号:
public String generateOrderNo() { Date now = new Date(); String dateStr = DateUtil.format(now, "yyyyMMdd"); Long incr = stringRedisTemplate.opsForValue().increment("orderNo:" + dateStr); // 订单号 = 日期 + 6位序列号 + 4位随机数 String no = dateStr + String.format("%06d", incr) + RandomUtil.randomNumbers(4); return no; }这套方案一天百万单以内都不会重复,而且订单号里自带日期信息,排查问题时一眼能看出是什么时候的订单,体验比纯自增好太多。
3.4 库存扣减的并发安全
预约库存扣减,最简单的正确姿势是乐观锁:UPDATE product_daily_stock SET booked_stock = booked_stock + 1 WHERE product_id = ? AND stock_date = ? AND booked_stock < total_stock。受影响行数如果为0,说明当天产能卖光了,返回“当日已售罄”即可。
不要先去查一下库存够不够,再执行update。这种先查后更的方式在并发下必然存在库存超卖问题。也不要引入复杂的分步锁,这个项目用不上,在SQL层面一次性解决。
下单后半小时未支付,订单要自动取消并释放库存。我习惯用延迟消息或Spring的@Scheduled写一个轮询任务,每分钟扫一遍超时未支付订单,执行状态置为已取消,同时回滚预约库存。轮询有分钟级延迟,对蛋糕场景完全可接受。除非你想卷实时性,那再去用消息队列,但演示效果提升有限。
3.5 支付回调处理
支付这一块,最常见的选择是接一个第三方支付平台,但不同支付方式的沙箱接入流程长短不一,对刚接触支付开发的人来说,光是回调验签就能折腾两天。
如果只是做流程演示,可以考虑在系统中预留支付策略接口,先实现一个模拟支付逻辑:前端付款页点击“模拟支付”,后端直接调订单状态补齐逻辑,流转为已支付并生成支付流水。这样整个订单链路是完整的,而且后续想接真实支付平台,完全不需要改订单主逻辑,只替换支付实现类就行。
注意:模拟支付不等于没有支付逻辑,回调接口、幂等处理、状态校验都要按真实支付场景开发,这样写进简历和项目文档才站得住脚。支付回调必须做幂等:同一个支付回调可能到达多次,必须以订单号加支付流水号为唯一约束做判重,否则重复更新订单状态会引发一系列脏数据。
3.6 物流配送与自提时间窗
最后一个核心环节是配送。蛋糕配送对时间敏感,我建议做两个功能:时间窗选择和配送状态记录。用户提交订单时选择期望自提/配送时间段,商家制作完成时录入实际完成时间,配送员可更新配送进度。
实现上可以建一张配送信息表,关联订单ID,保存配送类型(自提/同城配送)、期望时间、实际完成时间、配送员联系方式。这块功能做出来以后,整个项目的完整度立刻上一个档次,尤其是“商家开启打烊”和“用户确认收货”这两个时间点能体现业务时序。
4. 用户端与管理端的关键页面与接口设计
4.1 用户端商品列表与详情
用户端的性能优化点是首页和商品列表。蛋糕店首页有两类数据值得缓存:一是分类菜单,二是热销商品列表。Redis存储的key分别设置为cache:categoryList和cache:hotProducts,设置合理的过期时间,比如分类菜单1小时、热销商品5分钟,用缓存减轻MySQL压力。
商品详情页呢,我建议直接全动态渲染,不要过度缓存。理由很简单:蛋糕的规格选项多、价格变动频繁,商家后台经常边接单边调整价格,你辛辛苦苦做了缓存,结果每次商家改价格你都得处理缓存同步,反而容易出bug。
4.2 管理端订单面板
管理后台是门店员工一天打开最多次的页面。订单列表要有筛选条件组合:按日期、状态、配送类型、是否加急等;订单详情要展示规格选项快照、备注信息、收货人信息、订单状态变更时间线。
为了操作效率,我强烈建议管理端做一个“今日订单工作台”大面板,把待确认、制作中、待配送、已完成四个状态的订单数量用统计卡片展示,并能让员工一键变更状态。这个功能不是炫技,而是门店老板真正会高频使用的功能。
4.3 数据看板设计
销售服务系统的管理者视角需要成交量与销售额走势图、热销商品排行、用户复购率简表、当日待处理库存预警。统计数据不建议前端实时查表刷,应该在后端做聚合查询接口,或者更优雅一点,设计一张统计流水表,定期汇总。
SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS day, COUNT(DISTINCT order_no) AS order_count, SUM(pay_amount) AS sales_amount FROM orders WHERE pay_status = 1 AND create_time >= ? GROUP BY DATE_FORMAT(create_time, '%Y-%m-%d') ORDER BY day DESC;这一层做出来,项目的完整度和“行业理解”明显不一样。很多时候答辩或面试官问“你做了这么多功能,哪个最体现业务流程”,数据看板就是很好的例子。
5. 安全管理与权限设计注意事项
5.1 用户认证方案
用户端和后台管理端的认证建议分开设计。用户端用简单的Token鉴权即可,比如微信小程序登录或用户名密码登录后签发Token。管理端则用Spring Security的账号密码登录,结合角色的URL级权限控制。
我这次不是特别推荐在用户端上Spring Security,因为用户端的很多接口并不需要强鉴权,商品列表、详情、分类都是公共数据,强制登录反而增加调接口的复杂度。
5.2 防重复提交与接口幂等
下单和支付类接口,防重复提交必须做。前端按钮置灰只能挡住正常用户的二次点击,但挡不住脚本重放。后端可以通过拦截器对指定Path做幂等控制,生成一个一次性Token,提交后被删除,第二次携带同一Token直接拒绝。
// 创建一个幂等Token工具 public String generateIdempotentToken() { return UUID.randomUUID().toString().replace("-", ""); }5.3 数据权限与防越权
列表接口的防越权核心是:查询数据必须带上当前用户ID作为条件,不能直接查全表再筛。例如用户查看自己的订单,接口的SQL就应该是WHERE user_id = #{userId},而不是把订单表所有数据查出来再按名称过滤。我见过好几个项目在订单列表接口上不带用户条件,这种漏洞一旦被发现,非常尴尬。
6. 部署运行与项目打包实践
6.1 本地开发环境准备
运行这个项目最少需要安装:JDK 8或11、MySQL 5.7+、Redis 3.2+、Maven 3.6+。前端如果使用Vue3,还需要Node 16+。部署时这些中间件的版本最好跟开发环境保持一致,日期格式化、JSON序列化这些点都很容易因版本差异出问题。
6.2 打包与线上部署
SpringBoot项目打包非常简单:mvn clean package,生成可执行Jar。部署时需要注意配置文件分离,我习惯把数据库密码、Redis密码等敏感信息放到jar包外部的application-prod.yml中,通过启动参数--spring.profiles.active=prod切换。
java -jar springboot-cake-system.jar --spring.profiles.active=prod同时建议配置SpringBoot的优雅停机,避免用户在系统升级时下单到一半被打断。
6.3 数据库初始化与演示数据
演示项目不死的地方是初始化数据。项目启动时可以通过Spring SQL INIT机制自动建表和插入基础数据,开发环境每次启动容易反复重置。更好的方案是用Flyway管理数据库版本,既能初始化又能持续升级。
演示数据一定要真实。分类要包含“慕斯蛋糕”、“奶油蛋糕”、“翻糖蛋糕”这类,商品名要包含“草莓慕斯”、“榴莲千层”、“黑森林”这种,规格选项要给完整价格。不要只放三条叫“蛋糕1”、“蛋糕2”的测试数据,答辩时摄像头一照,细节不好看。
7. 常见问题与排查技巧实录
大量实操之后,我把这个项目最常见的坑整理成了速查表,你遇到类似问题时可以先从这里排查:
| 问题现象 | 原因分析 | 排查方法与建议 |
|---|---|---|
| 前端请求接口报跨域 | 未配置CORS或配置了但启动端口不一致 | 检查前后端端口,在WebMvcConfigurer中统一配置allowCors |
| 日期返回格式变成时间戳 | 未配置Jackson日期格式 | 在application.yml配置spring.jackson.date-format,统一为yyyy-MM-dd HH:mm:ss |
| 金额计算出现小数误差 | 用double或float存金额 | 数据库用DECIMAL,Java代码用BigDecimal,计算用String构造 |
| 并发下单库存超卖 | 先查询后更新或没有乐观锁 | 改成UPDATE ... WHERE booked_stock < total_stock 的条件更新 |
| 微信支付回调重复更新 | 未做回调幂等 | 支付流水表加唯一约束,先查流水状态再更新 |
| Redis缓存与数据库不一致 | 缓存更新策略没设计好 | 简单场景用Cache Aside Pattern,先更新库再删缓存 |
| 订单状态错乱 | 多处代码直接改状态字段 | 建订单状态机类,收敛所有状态变更入口 |
| 上传商品图片后无法访问 | 本地存储路径没有做静态资源映射 | 配置addResourceHandlers映射到实际上传目录 |
| 登录状态失效后管理端还能访问 | 未配置拦截器或权限注解 | 增加全局拦截器,对admin/**路径做Token校验 |
| 顾客下单后商家看不到订单 | 新旧订单按时间排序逻辑错误 | 订单列表默认按创建时间倒序,加索引(create_time) |
7.1 一个典型的排查现场
放一个真实场景:有次我遇到订单状态一会是“已支付”,过一阵刷新又变回“待支付”。查了重启日志,发现是支付回调接口和订单取消的定时任务在并发执行。订单先被支付回调更新为已支付,紧接着超时扫描任务查出的是更新前缓存的旧状态数据,又把状态更新回已取消。
这个问题的根因:超时取消任务没有在更新前再次确认订单当前状态,或者更新时没有带上“当前状态=待支付”这个条件。正确的写法:
int result = orderMapper.updateStatusAndTimeById( OrderStatus.CANCELED, OrderStatus.PENDING_PAYMENT, expireTime, orderId); if (result == 0) { // 说明状态已经被别的流程改过,当前任务放弃操作 }用条件更新来保证状态流转不被并发流程撕裂。这类问题排查多了你会发现,大部分数据事故起源于“没有带条件更新”或者“没有唯一约束”,这两板斧在数据库操作上真的是护身符。
7.2 编辑器工具推荐
代码层面,我推荐在开发时就加上一些基础规范工具。比如用Lombok简化实体类代码,用MapStruct做DTO和VO的转换,用Hutool工具集处理常见校验和序列化。这套组合能让你把精力集中在业务逻辑,而不是写一堆乏味的getter/setter和类型转换代码。
前端如果用Vue3,建议直接上Element Plus当作后台管理界面的UI骨架,表格、表单、弹窗、上传组件都有成熟的封装,实操效率高,而且观感不差。用户端如果想要小程序,思路也可以平移,把接口复用一下就行。
8. 项目管理与文档沉淀经验
最后聊一个很多人做项目时忽略的点:项目管理。这个系统要想做得值钱,代码只是胜负手的一半。我强烈建议你在开发过程中建立三份文档:
第一份是需求文档。凡是你自己“拍脑袋”觉得要做的功能,都写进去,标注优先级是必须做还是可选做。第二份是接口文档。用Swagger/knife4j自动生成即可,但每个接口要写清楚入参出参含义和业务规则。第三份是发布记录。每次发布版本,记录新增了什么功能、修了什么BUG、改了哪些数据库结构。
为什么强调这个?因为蛋糕烘焙销售服务系统这种东西,很容易越做越想加功能。今天加个砍价,明天加个会员日,后天又想做店铺自提核销码。没有文档约束和优先级管理,最后代码里塞满了写了一半的功能,核心主链路反而被拖得脆弱不堪。
我在实际开发中的习惯是:每完成一个小迭代,就提交一次代码,commit信息写得清楚,比如“新增规格加价计算逻辑”、“修复超时订单重复取消BUG”、“优化订单列表查询索引”。这种习惯救过我很多次——把代码回滚或者定位问题的时候,你只需要瞄一眼commit信息,就能锁定改动范围,不用在大一坨代码里漫无目的地搜索。
这个项目的边界还可以继续扩展,比如引入管理端的数据可视化大屏、接入短信提醒用户订单状态变化、开发用户端小程序版本、增加烘焙DIY预定模式。实际动手的时候,建议从订单主链路入手,先跑通一个用户从选蛋糕到确认收货的完整闭环,再逐步补全周边功能。抓住“蛋糕非标品、强时效、按日期切分库存”这几个行业特征,你的系统自然就跟那些千篇一律的电商项目拉开差距了。