仿得物电商后端实战:我的一点记录
最近把「得物实战」做完了。不是那种大而全的项目,但商品、订单、支付整条链路都自己搭了一遍,算是第一次把电商后端从浏览到付钱真正跑通。这篇就记一下我做了什么。
技术栈是 Spring Boot + MyBatis + MySQL,Redis 用来生成订单号,支付接支付宝。用户侧流程很直观:刷商品列表 → 点进详情选尺码 → 登录 → 提交订单 → 跳转支付宝 → 付完等回调改状态。看起来就几步,做起来模块不少。
商品模块:SPU 和 SKU
库表也是两张:product 存 SPU,product_detail 存 SKU,用 product_id 关联。列表接口查 SPU 分页,详情接口按 productId 把下面所有 SKU 拉出来。用户最终下单,关联的是 SKU,不是列表上那个 SPU。
商品列表还做了分页。前端传页码和每页条数,后端先 count 总数,再 LIMIT 查当前页。页码越界要处理,不然翻到不存在的页会出问题。
登录:Session 校验
下单前得知道是谁在买。项目用的是 Session。
前端调 /api/user/checklogin,后端从 Session 里看有没有登录信息。没登录就引导去登录,登录了才能下单。
有个细节印象很深:下单时 userId 必须从 Session 取,不能信前端传的。前端啥都能改,Session 是服务端管的。我也理解为啥——不然改个 userId 就能给别人下单了。
订单模块:从提交到入库
用户选好尺码点「提交订单」,后端 POST /api/order/add,进来一个 Order,主要就 productId 有用,别的很多是后端填的。我当时的实现逻辑大概是:
校验 productId → 查 SKU 拿价格和商品信息 → 生成主键 → 状态设成待支付 → 用 Redisson 生成唯一 orderNumber → 转 DO 入库 → 返回订单。
订单号用 Redisson 而不是自己拼,是要求也是合理做法——分布式下要保证唯一,重复订单号后面支付、对账都麻烦。订单状态就几个:待支付、支付成功、支付失败、已关闭。一开始就是待支付,等支付回调再改。
订单查询:别在循环里一个个查
后面做了查最近支付成功订单的接口。列表要展示用户昵称、商品信息,如果只查 order 表,信息不够。我的做法是:
- 先分页查订单
- 收集所有
userId和productId - 批量调
UserService、ProductDetailService - 内存里组装再返回
一开始想在循环里一条条查用户和商品,后来知道这叫 N+1,订单一多就慢。批量查是小优化,但是真实项目里会遇到的那种。
支付模块
下单只是生成待支付订单,真正掏钱是支付模块。这块我花时间最多,也踩坑最多。
发起支付
用户点支付,调 /api/alipay/pay,传 orderNumber、金额这些。后端先记一条支付流水,状态 PENDING,再校验订单存在,然后调支付宝 SDK 组装请求,pageExecute 返回一段 HTML 表单,前端渲染后跳转支付宝收银台。
流水和订单分开存,因为一笔订单可能支付失败、用户重试,每次尝试都要有记录,方便后面查和对账。
支付宝两条回调
这块我一开始搞混了。付完钱之后有两条路:
return_url:用户浏览器跳回你的网站,看个成功页notify_url:支付宝服务器直接 POST 给你,告诉你付成了没有
我一开始以为跳回成功页就算完事了。后来才明白——用户可能付完直接关页面,return_url 压根没触发。真正可靠的,是 notify_url 那条异步回调。而且支付宝会反复发,直到你回一个 success 才停。
所以更新订单状态,必须以 notify_url 为准。return_url 只做体验,不能当支付成功的依据。
回调里干啥
回调进来,PayController 接到请求,解析参数,交给 PayService 处理。先验签,确认是支付宝发的,不是别人伪造的。再看 trade_status,成了就:
- 订单改支付成功
- 流水改
SUCCESS - 商品付款人数 +1
失败就记失败状态。同一条回调可能收好几次,更新前先看订单是不是已经成功,别重复加付款人数。这就是幂等,支付场景里基本必做。
并发和库存
高并发下的库存问题,这个我印象也深。很多人同时买同一 SKU,如果简单 stock = stock - 1,可能超卖——卖出去的比库存还多,电商里是大事故。
项目里用 Redis 做并发控制,保证库存不为负、付款人数不超过库存。具体实现课程有讲,核心就是:高并发下核心数据不能靠「先查再改」这种 naive 写法。