商品服务搭完之后,下一个大模块就是购物车和订单。购物车看着简单,实际要考虑的东西不少——特别是和库存、价格的联动。订单就更复杂了,从下单到支付是一条很长的链路。这篇聊聊我的实现思路。
购物车存储方案
购物车需要频繁读写,对一致性要求没那么高(购物车里的数据丢了用户重新加就行),但对性能要求很高。最终选了 Redis Hash 来存储。
结构设计:
Key: cart:{user_id}
Field: {sku_id}
Value: JSON { "sku_id": 123, "quantity": 2, "checked": true, "added_at": "..." }
为什么用 Hash 而不是 String?
- 可以单独操作某个 SKU(加购、改数量),不需要读出整个购物车
HGETALL一次取出所有商品,效率高HDEL可以删除单个商品
用 JSON 作为 Value 是因为每个购物车项需要存多个字段(数量、选中状态、加入时间等),如果每个字段都单独用一个 Hash Field 会导致 Field 数量爆炸。
购物车上限
购物车不能无限添加商品,设了 200 个 SKU 的上限。加购前先 HLEN 检查一下数量。
登录态问题
未登录用户也需要购物车。我的方案是给未登录用户分配一个临时 token(存 Cookie),购物车 Key 用 cart:tmp:{token}。用户登录后,把临时购物车合并到用户购物车中,合并逻辑是:相同 SKU 数量相加,不同 SKU 直接追加。合并完成后删除临时购物车。
购物车操作
加购
POST /cart/add
{
"sku_id": 12345,
"quantity": 1
}
加购流程:
- 校验 SKU 是否存在、是否上架
- 校验库存是否充足(这里只做轻量校验,不锁库存)
- 查询该 SKU 在购物车中是否已存在
- 已存在:数量累加;不存在:新增一条
- 返回购物车最新数据
修改数量
PUT /cart/update
{
"sku_id": 12345,
"quantity": 3
}
前端传的是最终数量而不是增减数量,这样可以避免并发问题。后端直接覆盖 quantity 字段。如果 quantity 为 0,等同于删除。
勾选/取消勾选
购物车里每个商品有一个 checked 状态,用来标记结算时包含哪些商品。还有"全选"和"全不选"操作,全选就是遍历所有 Field 把 checked 设为 true。
这个操作频率很高(用户反复勾选),所以我没有做数据库持久化,全部在 Redis 里操作。
删除
支持批量删除:
DELETE /cart/nemove
{
"sku_ids": [12345, 67890]
}
用 HDEL 一次删多个 Field。
购物车展示数据
前端展示购物车时,需要的数据不仅仅是购物车本身存的那些字段。还需要:商品标题、图片、价格、库存状态。这些数据不应该存在购物车里(商品信息随时可能变化),而是展示时实时查询。
流程是:从 Redis 取出购物车数据(SKU ID 列表),然后批量查商品服务获取最新商品信息,合并后返回前端。如果某个 SKU 已经下架或删除了,在前端标记为"该商品已失效"。
下单流程
下单是从购物车到创建订单的过程,也是系统里最长的一条链路。
整体流程
购物车 → 确认订单页 → 提交订单 → 扣库存 → 创建订单 → 调支付
Step 1:确认订单页
用户在购物车里勾选商品,点击"去结算",进入确认订单页。这一步需要:
- 获取用户默认收货地址
- 计算运费(根据地址和商品重量)
- 重新查询商品价格(防止购物车价格过期)
- 展示优惠信息(后期做优惠券时再加)
确认订单页的数据通过一个聚合接口返回,后端同时查多个服务拼装数据。
Step 2:提交订单
用户点击"提交订单"后,后端开始创建订单。这是最核心的步骤:
- 防重校验:用订单 Token 做幂等,防止用户重复点击。进入确认订单页时后端生成一个 Token,提交时带上,后端校验后立即删除。
- 再次校验价格和库存:确认订单页到提交订单之间可能有时间差,价格和库存可能已经变了。
- 扣减库存:调用库存服务扣减。扣减失败直接返回失败。这里用的是"下单减库存"策略,不是"支付减库存"。
- 创建订单记录:写入 order 主表和 order_item 明细表。
- 清除购物车:删除已下单的购物车项。
- 发送延迟消息:创建一条30分钟的延迟消息,超时未支付自动关闭订单并回退库存。
Step 3:调支付
订单创建成功后,返回订单号给前端,前端跳转到支付页面。用户选择支付方式后,调用支付系统创建支付单。支付这块在之前的文章里写过了。
订单状态机
订单有一套完整的状态流转:
待支付 → 已支付/待发货 → 已发货/待收货 → 已完成
↓ ↓ ↓
已取消 退款中 退款中
↓ ↓
已退款 已退款
状态转换规则:
- 待支付 → 已取消:用户主动取消,或超时自动取消
- 待支付 → 已支付:收到支付回调
- 已支付 → 已发货:后台操作发货
- 已发货 → 已完成:用户确认收货,或超时自动确认(15天)
- 已支付/已发货 → 退款中:用户申请退款
每次状态变更我都记了一条 order_status_log,方便追踪问题。
库存扣减策略
库存扣减有三种策略,各有利弊:
- 下单减库存(我选的):下单时就扣库存。好处是不会超卖;坏处是可能有人恶意下单占库存不付款。通过超时关单+回退库存来缓解。
- 支付减库存:支付成功才扣库存。好处是不会被恶意占库存;坏处是可能超卖(多人同时下单都成功但库存不够)。
- 预扣库存:下单时预扣,支付后确认扣减,超时释放。最复杂但最合理。后面可能会改成这个方案。
当前用的"下单减库存",扣减操作用了 Redis + Lua 保证原子性,然后异步同步到数据库。
关键接口总结
POST /cart/add 加购
PUT /cart/update 修改数量
DELETE /cart/nemove 删除商品
GET /cart/list 获取购物车列表
PUT /cart/check-all 全选/全不选
GET /order/confirm 确认订单页数据
POST /order/submit 提交订单
GET /order/detail/:id 订单详情
POST /order/cancel/:id 取消订单
GET /order/list 我的订单列表
购物车和订单做完之后,商城的核心链路就通了:浏览商品 → 加购物车 → 下单 → 支付。后面要做的就是不断完善细节——优惠券、物流跟踪、评价等。