电商平台搭建这件事,说难也难,说简单也简单。难的是把商品、订单、支付、库存、物流这几条链路串成一个闭环,任何一个环节的边界没想清楚,后面就是无穷无尽的补丁;简单的是,如果你把"最小可用闭环"拆对了,一个能跑通真实交易的平台其实并不需要堆多少技术栈。我前后参与过几个从零起步的电商项目,有给朋友的小店做的单品类商城,也有支撑多商户的中型平台,踩过的坑基本都集中在"想当然"三个字上——想当然地以为订单状态很简单、想当然地以为库存扣减不会出问题、想当然地以为支付回调一定按顺序来。这篇内容就把我从零搭一个电商平台的完整思路摊开讲,从需求拆解、技术选型、核心链路设计,到库存并发、订单状态机、支付对账这些真正会让人半夜爬起来处理的问题,全部按实战顺序过一遍。适合正在做第一个电商项目的新手,也适合做过一两个项目但总觉得系统"哪里不对劲"的同行对照着看。
1. 先把"电商平台"这四个字拆成能落地的模块
很多人一上来就问用什么框架、用什么数据库,这其实是把顺序搞反了。技术选型是结果,不是起点。真正该先想清楚的是:这个平台到底要承载什么样的交易形态。单品类自营和多商户平台,在数据模型上的差异大到会影响你后面所有的表结构设计。
1.1 交易形态决定了你的数据模型骨架
我习惯把电商平台按两个维度切:供给方数量和履约复杂度。供给方是单一商家还是多个商家,决定了你的商品表要不要挂merchant_id、订单要不要按商户拆单、结算要不要做分账。履约复杂度则决定了你要不要处理多仓发货、拆包裹、部分退款这些场景。
举个具体的例子。如果是一个自营单品类商城,商品表可以很简单:product(id, title, price, stock, status),订单表直接关联商品快照就行。但如果是多商户平台,商品表就得挂商户,订单在支付成功后要按商户维度拆成多个子订单,每个子订单独立发货、独立结算。这个差异不是加个字段能解决的,它会影响你订单表的主键设计、状态流转逻辑、退款流程。
我的建议是:第一个项目尽量做自营单商户。不是因为多商户难,而是因为多商户会把你的注意力从"交易闭环"分散到"商户结算"上,而交易闭环才是电商系统的命根子。等单商户跑通了,再把商户维度加进去,改造成本远低于一开始就上多商户然后处处受制。
1.2 最小可用闭环到底包含哪几个环节
一个能真实收钱、真实发货的电商平台,最小闭环是这五段:商品展示 → 加购下单 → 支付 → 发货履约 → 售后。每一段背后都对应一组必须想清楚的状态和边界。
商品展示段的核心是"快照"概念。用户下单那一刻的商品价格、标题、图片,必须固化到订单里,不能在下单后还去关联查商品表。原因很直接:商品可能改价、下架、甚至删除,但订单里的信息必须永远可追溯。我见过太多新手在这里偷懒,订单详情页直接 join 商品表,结果商品一改价,历史订单金额全变了,对账时根本说不清。
支付段的核心是"幂等"和"异步"。支付平台的回调可能重复、可能乱序、可能延迟,你的系统必须能扛住这些。发货履约段的核心是"状态机",订单从待发货到已发货到已完成,每一步的触发条件和可逆性都要定义清楚。售后段的核心是"逆向流程",退款不是简单地把钱退回去,它涉及库存回补、优惠券返还、积分回滚等一系列联动。
1.3 模块划分与依赖关系
把闭环拆成模块后,依赖关系是这样的:商品模块是基础,订单模块依赖商品快照,支付模块依赖订单,履约模块依赖支付结果,售后模块反向依赖订单和支付。这个依赖链意味着你的开发顺序也应该是这个顺序,先做商品和订单,再做支付,最后做履约和售后。
这里有个容易被忽略的点:库存模块应该独立于商品模块。很多新手把库存当成商品表的一个字段,update product set stock = stock - 1,单机低并发时没问题,一旦有并发下单就会超卖。库存扣减本质是一个并发控制问题,它需要独立的库存流水表来保证可追溯和可对账。这个后面会专门展开讲。
2. 技术选型:别被"高并发"三个字带偏
选型这件事,我的核心观点是:按你未来半年可能的真实量级选,而不是按你想象中的量级选。一个日订单几十到几百的平台,用单体架构加一个关系型数据库完全够用,硬上微服务、消息队列、分库分表,只会让你在还没跑通业务的时候先被运维复杂度拖死。
2.1 单体优先,但要把边界留出来
我推荐的技术组合是:后端用一门你熟悉的语言(Java、Python、Node.js 都行),数据库用 MySQL 或 PostgreSQL,缓存用 Redis,前端用主流框架。这套组合的特点是生态成熟、资料多、出问题好查。
关键在于,即使是单体应用,也要在代码层面把模块边界划清楚。比如订单服务、库存服务、支付服务在代码里是独立的包或目录,模块之间只通过明确定义的接口调用,不直接跨模块操作对方的表。这样做的好处是,将来真要拆服务时,你只需要把接口调用换成 RPC 调用,业务逻辑不用重写。
提示:模块边界的第一条铁律是"不跨模块直接读写对方的表"。订单模块要查商品信息,走商品模块的接口;库存模块要扣减,走库存模块的接口。这条守住了,后面怎么演进都不会太痛。
2.2 数据库表设计里最容易被低估的三张表
大部分教程会教你设计商品表、订单表、用户表,但真正决定系统健壮性的是这三张容易被忽略的表:库存流水表、订单状态变更日志表、支付流水表。
库存流水表记录每一次库存变动的来龙去脉:变动类型(下单扣减、取消回补、退款回补、手动调整)、变动数量、变动前后的值、关联订单号。有了它,任何一次库存对不上都能顺着流水查清楚。订单状态变更日志表记录订单每一次状态跃迁的时间、操作人、触发原因,排查"订单为什么卡在待发货"这类问题时,这张表就是你的黑匣子。支付流水表记录每一笔支付请求和回调的原始报文,对账时全靠它。
这三张表的共同特点是:只增不改。它们是审计日志,不是业务状态,任何时候都不应该去 update 它们。这个设计习惯能帮你省掉大量排查时间。
2.3 缓存和队列什么时候才真正需要
Redis 在电商系统里最常见的用途是缓存商品详情和会话。但我要提醒一句:缓存商品详情的前提是你接受短暂的数据不一致。商品价格改了,缓存里可能还是旧价,用户看到旧价下单,你按新价结算,这就是纠纷。所以价格这类敏感字段,要么不缓存,要么在下单时强制回源校验。
消息队列(如 RabbitMQ、Kafka)在电商里的典型用途是异步处理支付回调、异步发通知、削峰。但我的经验是:日订单没过千之前,队列不是必需品。支付回调用同步处理加幂等控制完全能扛住,等真的出现处理瓶颈了再引入队列,那时候你对业务的理解也更到位,知道该把什么放进队列。
3. 商品与订单:快照设计是整个系统的地基
商品和订单这两块,表面看是 CRUD,实际上藏着电商系统最重要的设计决策——快照。快照设计得好,后面所有对账、售后、纠纷处理都有据可依;设计得差,你会陷入"这个订单当时到底买的是啥、多少钱"的无尽扯皮。
3.1 商品模型:SPU 与 SKU 的取舍
稍微正规一点的电商都会区分 SPU(标准产品单位)和 SKU(库存量单位)。SPU 是"iPhone 15"这个概念,SKU 是"iPhone 15 128G 黑色"这个具体可售单元。价格和库存在 SKU 上,描述和图片在 SPU 上。
但我要说的是:如果你的商品没有多规格,就别硬上 SPU/SKU 两层结构。单规格商品直接一张商品表加一张规格表就够了,强行套两层模型只会让代码多一层无意义的转换。我见过一个卖单一品类的小商城,硬套 SPU/SKU,结果每个商品只有一个 SKU,代码里到处是spu.skus[0]这种写法,纯属自找麻烦。
判断标准很简单:同一个商品是否有多个影响价格或库存的可选属性。有,就上 SPU/SKU;没有,就单表。这个决策做对了,商品模块的复杂度能降一半。
3.2 订单快照:下单那一刻必须固化什么
订单快照要固化的信息,我列一个清单,这些都是实战中真正会用到、缺了会出事的:
| 快照字段 | 为什么必须固化 | 缺失后的后果 |
|---|---|---|
| 商品标题 | 商品可能改名 | 历史订单显示错误名称 |
| 商品价格 | 商品可能调价 | 订单金额与用户实际支付不符 |
| 商品图片 | 图片可能更换或删除 | 订单详情无法展示 |
| 规格属性 | 规格可能调整 | 无法确认用户买的具体规格 |
| 优惠明细 | 优惠规则可能变化 | 退款时无法正确计算应退金额 |
| 收货地址 | 用户可能修改或删除地址 | 发货时找不到地址 |
这张表里的每一行,背后都是一次真实的踩坑。尤其是优惠明细和收货地址,新手最容易漏。优惠明细不固化,退款时你根本算不清该退多少;收货地址不固化,用户下单后把地址删了,你的发货单就成了无头案。
快照的存储方式,我推荐用 JSON 字段存一份完整的商品和优惠快照,同时把关键字段(如单价、数量、小计)冗余成独立列,方便查询和统计。JSON 存全量保证可追溯,独立列保证查询性能,两者不冲突。
3.3 订单状态机:把"卡单"问题消灭在设计阶段
订单状态机是电商系统里最容易被做成一团乱麻的地方。我见过最夸张的一个系统,订单状态有十几个,状态之间的流转靠一堆 if-else 判断,最后没人说得清一个订单从下单到完成到底会经过哪些状态。
正确的做法是:先画出状态流转图,再写代码。一个标准的订单状态机大致是这样:
待支付 → 已支付 → 待发货 → 已发货 → 已完成 ↓ ↓ ↓ 已取消 退款中 退款中 ↓ ↓ 已退款 已退款每个状态跃迁都要明确三件事:触发条件(什么事件导致跃迁)、前置校验(跃迁前必须满足什么)、副作用(跃迁后要做什么,比如发通知、回补库存)。把这三件事定义清楚,代码就是照着填,不会乱。
注意:状态机里最危险的是"逆向流转",比如已发货的订单要退款,涉及库存回补、物流拦截、资金退回多个联动。这类流转一定要单独设计,不能和正向流转混在一起用同一套逻辑。
4. 库存并发:超卖问题的完整排查与解决链路
库存这块,我单独拎出来讲,因为它是电商系统里最容易出线上事故的地方,也是面试和实战中都绕不开的核心问题。超卖一旦发生,轻则赔钱发货,重则用户投诉、平台信誉受损。
4.1 超卖是怎么发生的:一次真实的排查过程
先说一个我亲历的场景。一个促销活动上线,库存 100 件,活动开始后几分钟,卖出去了 130 件。排查过程是这样的:
第一步,看库存流水表,发现同一时刻有多条"下单扣减"记录,扣减前的库存值都是同一个数。这说明多个请求同时读到了相同的库存值,然后各自扣减,典型的读-改-写竞态。
第二步,看代码,果然是select stock from product where id = ?然后if (stock >= num) update product set stock = stock - num。这个写法在并发下必然超卖,因为 select 和 update 之间没有任何锁保护。
第三步,确认并发来源,是活动开始瞬间的大量请求打到了同一商品。问题定位清楚了,解决方案也就明确了。
4.2 三种库存扣减方案的取舍
针对超卖,常见的有三种方案,各有适用场景:
方案一:数据库行锁(悲观锁)。用select ... for update锁住商品行,再判断库存、扣减。优点是实现简单、绝对不超卖;缺点是并发性能差,同一商品的请求会串行化。适合库存少、并发不高的场景。
方案二:条件更新(乐观锁的一种)。用update product set stock = stock - num where id = ? and stock >= num,靠数据库的行锁和 where 条件保证原子性。判断是否扣减成功看影响行数。优点是性能好、实现简单;缺点是无法区分"库存不足"和"商品不存在",需要额外查询。这是我最推荐的方案,绝大多数场景够用。
方案三:Redis 预扣减。把库存放到 Redis,用 Lua 脚本保证扣减的原子性,扣减成功后再异步落库。优点是性能极高,适合秒杀级并发;缺点是要处理 Redis 和数据库的一致性,复杂度高。除非你真的有秒杀场景,否则别上这个方案。
我的建议是:默认用方案二,遇到性能瓶颈再考虑方案三。方案二的 SQL 长这样:
UPDATE inventory SET stock = stock - #{num}, version = version + 1 WHERE sku_id = #{skuId} AND stock >= #{num};执行后检查影响行数,为 1 说明扣减成功,为 0 说明库存不足。这个方案的精髓在于把"判断"和"扣减"合并成一条原子 SQL,从根上消除了竞态窗口。
4.3 库存回补与对账:别让退款把库存搞乱
扣减解决了,回补同样要小心。订单取消、退款成功都要回补库存,但回补必须幂等——同一个订单不能回补两次。做法是在库存流水表里记录回补记录,回补前先查这个订单是否已经回补过。
更稳妥的做法是给库存流水表加唯一索引,比如(order_id, change_type)唯一,这样即使代码逻辑有漏洞,数据库层面也能挡住重复回补。这个技巧我在多个项目里用过,非常有效。
对账则是定期跑一个任务,把库存流水表的变动汇总,和商品表的当前库存比对,对不上就报警。这个任务能帮你及早发现那些逻辑漏洞导致的库存漂移,而不是等到盘点时才发现账实不符。
5. 支付集成:回调、幂等与对账三件套
支付是电商系统里唯一直接和钱打交道的模块,它的容错要求比其他模块高一个数量级。支付这块做不好,不是功能问题,是资金安全问题。
5.1 支付回调为什么必须做幂等
支付平台的回调有几个特点:可能重复、可能乱序、可能延迟。同一个订单,你可能收到两次"支付成功"的回调;也可能先收到"支付成功"再收到"支付失败"(虽然少见但存在);回调延迟几分钟甚至几小时也是常事。
如果你的回调处理不幂等,重复回调会导致订单被重复标记、库存被重复扣减、通知被重复发送。幂等的实现方式很简单:以支付平台的交易号作为唯一键,处理前先查这笔交易是否已处理过,处理过就直接返回成功。
-- 支付流水表加唯一索引 ALTER TABLE payment_record ADD UNIQUE KEY uk_trade_no (trade_no);有了这个唯一索引,即使并发回调同时到达,数据库也会挡住重复插入,你的业务逻辑只需要处理插入成功的那一次。
5.2 主动查询:不能只依赖回调
只依赖回调是有风险的,回调可能丢失。所以必须有一个主动查询的补偿机制:对于处于"支付中"状态超过一定时间的订单,主动去支付平台查询交易状态,根据查询结果更新订单。
这个补偿任务建议用定时任务跑,间隔不要太短(避免给支付平台造成压力),也不要太长(避免用户等太久)。我的经验是 1 到 5 分钟查一次,超过一定时间(比如 30 分钟)还没结果的,标记为异常订单人工介入。
5.3 对账:每天必须做的一件事
对账是资金安全的最后一道防线。做法是每天定时拉取支付平台的交易流水,和本地的支付流水表逐笔比对,找出三类差异:平台有本地无(漏记)、本地有平台无(错记)、金额不一致(错账)。
对账结果要落库并生成报表,差异项必须人工核查。这个流程看起来繁琐,但它是唯一能保证"账对得上"的手段。我见过一个系统上线半年没做过对账,某天发现少了几万块,查了三天才定位到是一笔回调处理异常导致的,如果每天对账,当天就能发现。
6. 履约与售后:正向流程之外的逆向设计
正向流程(下单到收货)大部分人都能想清楚,真正考验设计功力的是逆向流程——取消、退款、退货、换货。逆向流程的特点是联动多、状态多、边界多,设计不好就是一堆补丁。
6.1 退款不是"把钱退回去"这么简单
一笔退款涉及的动作远不止退钱:库存要不要回补(看商品是否已发货)、优惠券要不要返还(看优惠券是否已使用且未过期)、积分要不要回滚、运费要不要退(看责任方)。这些联动如果在下单时没有把快照做全,退款时就会抓瞎。
我的做法是把退款拆成"退款单"这个独立实体,退款单关联原订单,记录退款类型(仅退款/退货退款)、退款金额、退款原因、处理状态。退款单的金额计算基于订单快照里的优惠明细,按比例分摊,而不是简单退商品单价。这个细节很多人忽略,导致部分退款时金额算错。
6.2 发货与物流信息的对接思路
发货这块,核心是把订单和物流单号关联起来。物流信息的获取有两种方式:主动查询物流平台的接口,或者接收物流平台的推送。前者实现简单但有延迟,后者实时但需要处理推送的可靠性。
我的建议是两者结合:以推送为主,定时查询为辅做补偿。物流信息展示给用户时,要注意脱敏,不要把完整的收件人信息暴露在物流轨迹里。
6.3 售后状态与订单状态的解耦
一个常见的错误是把售后状态塞进订单状态里,导致订单状态机变得无比复杂。正确的做法是售后单独立于订单,订单有自己的状态,售后单有自己的状态,两者通过关联字段联系。订单是否可退款、可退货,由订单状态和售后单状态共同决定,而不是把售后状态混进订单状态。
这个解耦做对了,你的订单状态机就能保持简洁,售后流程再复杂也不会污染主流程。
7. 上线前必须过的几道自检
系统搭完不等于能用,上线前有几项自检我每次都会做,这里列出来供参考。
并发自检:用压测工具模拟同一商品的高并发下单,验证不超卖。这一步能暴露库存方案的问题。
幂等自检:手动重复触发支付回调、重复提交订单,验证系统不会重复处理。这一步能暴露幂等设计的漏洞。
边界自检:测试库存刚好等于购买量、优惠刚好用完、退款金额刚好等于订单金额这些边界情况。边界往往是最容易出 bug 的地方。
对账自检:跑一遍完整的下单-支付-退款流程,然后执行对账任务,确认账能对上。这一步是对整个链路的终极验证。
异常自检:模拟支付回调延迟、物流接口超时、数据库连接断开等异常,验证系统的降级和补偿机制是否生效。
这几道自检过完,系统才算真正具备上线条件。我在实际项目里的体会是,前面设计阶段多花的时间,都会在上线后的稳定性上还回来;而设计阶段偷的懒,都会在线上以事故的形式加倍偿还。电商系统尤其如此,因为它直接和钱、和用户体验挂钩,容错空间比一般系统小得多。