电商系统的订单+商品+营销的Mysql表设计是怎么样呢
一、前述
问题1: 电商系统创建订单的逻辑中,如果订单使用了优惠券的话,是会把优惠券直接标在订单表么,还是会单独创建一个表里记录订单和多个优惠券之间的关系
问题2: 如何设计一套mysql数据库的表,可以将订单信息、支付信息、优惠券信息以及商品信息之间关联起来
二、实战演练
结论先说:如果订单允许使用多个优惠券,必须用一张单独的关联表来记录 order ↔ coupon
的关系;同时在 orders
表保留优惠券总额(摘要字段)和一个主券引用(可选)以便快速查询。
下面写一套可直接落地的 MySQL 表设计(包含建表 SQL)、事务示例、常见场景说明与设计理由(包括快查字段、快照、退款/退券策略、分库分表注意点)。
设计要点(快速读)
-
orders
:订单主体(保留付款/价格摘要字段),主键为BIGINT
(生产建议用分布式 ID,示例用AUTO_INCREMENT
便于演示)。 -
order_items
:订单商品行,必须保存商品的快照(price/title),以保证历史稳定性。 -
coupon_templates
:优惠券模板(规则定义)。 -
user_coupons
:优惠券实例(发放给用户的那张券,包含有效期/状态/面额等)。 -
order_coupons
:关联表,记录某笔订单用了哪些user_coupon
、每张券抵扣了多少(并保存券的快照),支持多券。 -
payments
:支付流水表(一个订单可能有多笔支付/分次付款),用于对账与退款关联。 -
refunds
:退款单(关联到 payment / order),用于记录退款和是否恢复优惠券等业务逻辑。
建表 SQL(MySQL,示例可直接执行)
说明:示例使用
AUTO_INCREMENT
方便演示;生产环境建议用分布式 ID(snowflake/Leaf)替代AUTO_INCREMENT
并去掉AUTO_INCREMENT
属性。
所有金额均以分/最小货币单位(整数)存储,避免精度问题。
-- 1) 应用层生成 order_no(或从发号器拿 ID),并校验券合法性 BEGIN; -- 锁住要使用的券(按 user_coupon_id) SELECT * FROM user_coupons WHERE id = ? FOR UPDATE; -- 校验 status=ISSUED && expire_at>now && 满足 min_spend && template.stackable 或多券叠加策略 UPDATE user_coupons SET status='LOCKED', locked_until = NOW() + INTERVAL 30 MIN WHERE id = ?; -- 插入订单(或先在内存准备数据并插入) INSERT INTO orders (order_no, user_id, merchant_id, total_amount, shipping_fee, promotion_discount, coupon_discount, payable_amount, status) VALUES (..., 'CREATED'); -- 插入 order_items(包含商品快照) INSERT INTO order_items (...); -- 插入 order_coupons(记录每张券实际抵扣) INSERT INTO order_coupons (order_id, user_coupon_id, template_id, code, applied_amount, coupon_snapshot) VALUES (...); COMMIT; -- 2) 用户支付: -- 若支付成功,确认券为 USED(原来 LOCKED) UPDATE user_coupons SET status='USED', used_at=NOW(), used_order_id = <order_id> WHERE id = ? AND status='LOCKED'; -- 若用户取消或超时未支付,释放券 UPDATE user_coupons SET status='ISSUED', locked_until=NULL WHERE id=? AND status='LOCKED';
示例:查询一个订单及其优惠券/支付信息
SELECT o.order_no, o.total_amount, o.coupon_discount, o.payable_amount, p.payment_no, p.status AS payment_status FROM orders o LEFT JOIN payments p ON p.order_id = o.id AND p.status = 'SUCCESS' WHERE o.order_no = 'ORD202509120001'; -- 查询该订单使用了哪些券 SELECT oc.applied_amount, uc.code, ct.name, oc.coupon_snapshot FROM order_coupons oc JOIN user_coupons uc ON oc.user_coupon_id = uc.id JOIN coupon_templates ct ON oc.template_id = ct.id WHERE oc.order_id = (SELECT id FROM orders WHERE order_no='ORD202509120001');
小结(建议)
不要把多个 coupon 写成 orders 表字段或 CSV,要用 order_coupons 关联表。
orders 表保留 coupon_discount 总额作为快查字段,避免频繁 join。
user_coupons 必须保存实例快照(面额 / percent / expire),并且在下单时 SELECT ... FOR UPDATE 做锁定;确认支付后标记 USED,失败则释放。
退款流程要提前设计(是否恢复券、是否部分退回),并在 refunds 表记录是否需要恢复券。
生产环境主键使用分布式 ID(雪花 / 号段),便于分库分表与扩展;示例里为便捷起见用了 AUTO_INCREMENT,上线时替换即可。
三、要不要将正向订单和逆向订单拆成不同表
🎯 总体设计思路
-
是否放在同一个表?
-
放在一个表:
-
优点:统一查询、减少表数量。
-
缺点:字段差异大,逻辑分支复杂,影响扩展性。
-
-
分表:
-
通常业界实践是 正向订单 和 逆向订单 分开存放,因为生命周期、业务字段、处理逻辑不同。
-
比如:淘宝、京东、亚马逊这种大规模电商平台,都会区分“销售订单”和“售后订单(退款/退货/拒付)”。
-
-
-
逆向订单要不要分多个表?
-
一般不建议为每个逆向类型单独建表(退货单表、退款单表、拒付单表),否则表过多,扩展困难。
-
推荐:
-
正向订单表(销售订单)
-
逆向订单表(售后订单:退货、拒付、退款都放这里,通过
reverse_type
区分)
-
-
-
逆向订单与正向订单关系:
-
逆向订单是基于原始正向订单产生的。
-
必须有一个字段
origin_order_id
,指向对应的正向订单。
-
🎯 支付单是否要拆成正向/逆向两类?
你说得对:
-
在支付体系里,退款也需要有单独的“支付单号”(比如支付宝/微信会返回一个
refund_id
)。 -
如果退款单直接更新在原始支付单里,会丢失 多次退款 的链路信息(一个支付可能对应多个退款)。
因此,业界一般做法是:
-
支付单表只记录“正向支付”(即从用户到商户的资金流)。
-
退款单(逆向资金流)单独建表,而不是混在一个表里。
📑 数据库表设计(MySQL)
1. 正向订单表(sales_order)
CREATE TABLE reverse_order ( reverse_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '逆向订单ID', origin_order_id BIGINT NOT NULL COMMENT '原始正向订单ID', reverse_type VARCHAR(32) NOT NULL COMMENT '逆向类型: REFUND, RETURN, CHARGEBACK', reason VARCHAR(255) COMMENT '逆向原因', amount DECIMAL(18,2) NOT NULL COMMENT '逆向金额', status VARCHAR(32) NOT NULL COMMENT '逆向订单状态:CREATED, PROCESSING, SUCCESS, REJECTED', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (origin_order_id) REFERENCES sales_order(order_id) );
4. 逆向支付单表(reverse_payment)
CREATE TABLE reverse_payment ( reverse_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '退款单ID', payment_id BIGINT NOT NULL COMMENT '关联的正向支付单ID', reverse_order_id BIGINT NOT NULL COMMENT '关联的逆向订单ID', reverse_channel_order_no VARCHAR(64) COMMENT '支付通道返回的退款流水号', amount DECIMAL(18,2) NOT NULL COMMENT '退款金额', status VARCHAR(32) NOT NULL COMMENT '退款状态:INIT, PROCESSING, SUCCESS, FAILED', created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, FOREIGN KEY (payment_id) REFERENCES payment(payment_id), FOREIGN KEY (reverse_order_id) REFERENCES reverse_order(reverse_id) );
🔄 三种订单处理流程
1. 正常下单(正向订单)
-
用户下单 →
sales_order
生成记录(CREATED) -
支付成功 → 更新
payment.status=SUCCESS
,并更新sales_order.status=PAID
-
商户发货 →
sales_order.status=SHIPPED
-
用户确认收货 →
sales_order.status=COMPLETED
2. 退货单(逆向订单 - RETURN)
-
用户发起退货 →
reverse_order
新增一条(reverse_type=RETURN, status=CREATED) -
商户审核 →
reverse_order.status=PROCESSING
-
商户同意退货 → 用户退回商品 → 验货 →
reverse_order.status=SUCCESS
-
支付系统发起退款(更新
payment.status=REFUNDED
)
3. 拒付单(逆向订单 - CHARGEBACK)
-
银行或支付机构通知拒付 →
reverse_order
新增(reverse_type=CHARGEBACK) -
风控/客服人工审核 →
reverse_order.status=PROCESSING
-
若拒付成立 →
reverse_order.status=SUCCESS
,商户资金扣减 -
若拒付失败 →
reverse_order.status=REJECTED
4. 退款单(逆向订单 - REFUND)
-
用户未发货申请退款 →
reverse_order
新增(reverse_type=REFUND) -
商户审核通过 →
reverse_order.status=SUCCESS
-
支付网关执行退款,更新
payment.status=REFUNDED
🔄 支付单关系与流程
-
正向支付
-
用户下单 → 创建
payment
记录 -
调用支付通道(支付宝/微信) → 返回通道支付流水号(
pay_channel_order_no
) -
更新
payment.status=SUCCESS
-
-
退款
-
用户申请退款 → 创建
reverse_order
(逆向订单) -
基于某个
payment
新建refund
记录 -
调用支付通道退款接口 → 返回
refund_channel_order_no
-
更新
refund.status=SUCCESS
-
如果全额退款 → 同时更新
payment.status=REFUNDED
-
如果部分退款 →
payment
保持SUCCESS
,但挂有多条refund
-
-
多次退款
-
一个
payment
→ 可以对应多个refund
(部分退款场景常见)
-
-
拒付(chargeback)
-
通常不通过退款接口,而是由银行/支付机构发起资金回退
-
在你的
reverse_order
表里记一条CHARGEBACK
,然后在退款表refund
中也插一条对应的资金逆向流水
-
✅ 总结
-
正向订单与逆向订单分表存储,通过
origin_order_id
建立关系。 -
逆向订单表中通过
reverse_type
区分退款、退货、拒付,而不是拆成多个表。 -
处理流程:
-
正向:下单 → 支付 → 发货 → 收货
-
退货:退货单 → 审核 → 退回商品 → 退款
-
拒付:支付机构发起 → 审核 → 资金调整
-
退款:申请退款 → 审核 → 支付网关退款
-
-
支付单表:只记录正向资金流。
-
退款单表:单独建表,挂在支付单之下,用于记录逆向资金流。
-
退款单号:既有内部生成的
refund_id
,也有支付通道返回的refund_channel_order_no
。 -
优势:
-
保留完整支付+退款链路
-
支持部分退款、多次退款
-
对账时可以一一对应支付通道流水
-
📌 为什么推荐拆表?
-
业务语义清晰
-
正向订单:代表用户买东西(从用户到商户的资金流)。
-
逆向订单:代表退款/拒付/退货(从商户到用户的资金流)。
-
放在一个表里,需要
order_type
字段区分,查询和统计时要加很多条件,逻辑耦合度高。
-
-
状态机完全不同
-
正向订单常见状态:
INIT → PAID → SHIPPED → FINISHED
-
逆向订单常见状态:
INIT → APPROVED → REFUNDING → REFUNDED / FAILED
-
如果强行放在一张表,会出现大量无效字段,字段语义会越来越模糊。
-
-
扩展性好
-
逆向订单后续可能有 多次申请、部分退款、银行拒付 等场景,字段设计和正向订单差别很大。
-
单独一张表可以灵活加字段,不会污染正向订单表。
-
-
性能与分库分表
-
电商里正向订单数量远大于逆向订单。
-
如果放在一张表,数据量巨大且冷热数据混杂;拆表后,逆向订单表更小更轻,查询效率更高。
-
-
符合行业主流实践
-
支付宝 / 微信支付:明确区分
trade_order
和refund_order
(退款有单独的 refund_id)。 -
Amazon / eBay:也有
Order
表和Return / Refund
表,分开存储。
-
📌 那是不是一定要拆?
-
小系统(数据量小、逻辑简单):可以放一起,用
order_type
字段区分,开发成本低。 -
大中型系统(电商/支付中心):强烈推荐拆表,清晰、可扩展、利于分库分表。
📌 个人推荐
正向订单表:只存购买行为
逆向订单表:单独一张表存退款/退货/拒付这样:
正向和逆向的订单号规则也可以分开(比如
O20250913001
/R20250913001
)。支付单和退款单可以分别挂载在正向/逆向订单之下。
四、订单明细(order_item)和订单优惠券(order_coupons)是不是要拆分正向逆向
这是个非常关键的问题 ,涉及 正向单 & 逆向单 的设计选择。我们可以从电商系统里常见的 库存回滚、优惠券返还、对账一致性 来拆解。
1️⃣ 方案一:在 order_items
和 order_coupons
增加状态字段
-
做法:
在现有的正向订单明细表中,新增字段,比如:-
refund_status
(未退款/部分退款/已退款) -
refunded_quantity
(已退款数量) -
refunded_amount
(已退款金额)
-
-
优点:
-
表结构简单,不需要额外的逆向表。
-
查询订单整体情况时只查一张表,简单直观。
-
-
缺点:
-
难以支撑复杂的退款场景(部分退款、多次退款、跨期退款)。
-
如果要做对账或审计,正向/逆向混在一起,不容易拆清楚。
-
退款数据和下单数据耦合,不利于扩展。
-
2️⃣ 方案二:新增退款记录(逆向单表)
-
做法:
新增两张“逆向表”:-
refund_items
(对应order_items
,记录每次退款的商品明细) -
refund_coupons
(对应order_coupons
,记录每次优惠券返还的情况)
-
-
逻辑:
-
正向单表只保留原始下单数据,永远不变。
-
退款时,在
refund_items
和refund_coupons
里新增一条逆向记录,和对应的退款单 ID 关联。
-
-
优点:
-
数据清晰,正向与逆向天然隔离。
-
支持多次退款(部分退款、多商品退款),便于追溯。
-
更容易做财务对账。
-
-
缺点:
-
表数量增加,查询时要做多表 JOIN。
-
需要额外的退款单设计(refund_order)。
-
3️⃣ 业界常见做法
-
淘宝 / 京东 / 美团 等大型电商,基本都会采用 逆向表模式(方案二),因为:
-
电商退款场景非常复杂(部分退款、优惠券部分返还、活动优惠如何分摊)。
-
需要保留所有历史记录以供审计。
-
正向单的“不可变性”非常重要(避免篡改)。
-
✅ 推荐做法
个人推荐 方案二(逆向表),原因是:
-
正向表保持只读(下单时落一次),符合“订单一经生成不可修改”的原则。
-
退款、退货、逆向操作独立成逆向单表,和财务系统/库存系统/营销系统做对账更清晰。
-
对于 库存回滚、优惠券释放,只需要在退款表中新增一条记录,库存和券服务即可根据逆向表的数据来进行回滚。
📌 总结:
-
小型系统:可以在
order_items
/order_coupons
加状态字段(简单粗暴)。 -
中大型电商:必须拆逆向表,记录退款明细。
五、那么将上面的退款item信息和优惠券信息直接放在逆向订单表里加字段呢
其实就是 用 JSON 存退款明细,避免额外建 refund_items / refund_coupons 表。我们可以把两种做法对比一下。
🔹 方案 A:逆向订单表 + JSON 字段(refund_skus / refund_coupons)
-
表结构示例
CREATE TABLE refund_orders ( refund_id BIGINT PRIMARY KEY, order_id BIGINT NOT NULL, refund_amount DECIMAL(10,2) NOT NULL, refund_reason VARCHAR(255), refund_status TINYINT NOT NULL, refund_skus JSON, -- [{"sku_id":123,"qty":1,"amount":50.00}, ...] refund_coupons JSON, -- [{"coupon_id":456,"amount":10.00}, ...] created_at DATETIME, updated_at DATETIME );
-
优点
-
表数量少,结构简单,维护成本低。
-
小团队、业务场景不复杂时,开发效率高。
-
不需要复杂的多表 JOIN。
-
-
缺点
-
JSON 不好做 精确查询/统计(比如“查询某个 SKU 被退款的次数”)。
-
金融/财务对账场景下,JSON 数据不够规整,不符合审计要求。
-
复杂退款场景(多次退款、部分退款、优惠券跨退款拆分)时,JSON 会越来越难维护。
-
🔹 方案 B:拆分 refund_items / refund_coupons 子表
-
表结构示例
CREATE TABLE refund_orders ( refund_id BIGINT PRIMARY KEY, order_id BIGINT NOT NULL, refund_amount DECIMAL(10,2) NOT NULL, refund_reason VARCHAR(255), refund_status TINYINT NOT NULL, created_at DATETIME, updated_at DATETIME ); CREATE TABLE refund_items ( id BIGINT PRIMARY KEY AUTO_INCREMENT, refund_id BIGINT NOT NULL, sku_id BIGINT NOT NULL, refund_quantity INT NOT NULL, refund_amount DECIMAL(10,2) NOT NULL, created_at DATETIME, updated_at DATETIME ); CREATE TABLE refund_coupons ( id BIGINT PRIMARY KEY AUTO_INCREMENT, refund_id BIGINT NOT NULL, coupon_id BIGINT NOT NULL, refund_amount DECIMAL(10,2) NOT NULL, created_at DATETIME, updated_at DATETIME );
-
优点
-
数据规整,方便做统计/聚合/财务对账。
-
支持多次退款/部分退款,不会出现 JSON 难维护的问题。
-
审计友好(可以单表追踪优惠券/商品的退款情况)。
-
-
缺点
-
表数量多,维护复杂度更高。
-
查询时要 JOIN,开发略麻烦。
-
🔹 推荐选择
-
如果你做的是 小型系统 / 内部系统 / 不涉及复杂对账:
👉 方案 A(JSON 存在 refund_orders 表里)足够,简单快速。 -
如果你做的是 中大型电商 / 有财务审计 / 退款场景复杂:
👉 强烈推荐 方案 B(拆分 refund_items / refund_coupons),因为正向/逆向数据往往要被统计、追溯、审计,JSON 会成为数据治理的障碍。
✅ 个人推荐:
长远看还是方案 B(单独子表)更靠谱。虽然多了几张表,但数据结构规整,统计和审计更轻松。JSON 方案适合 MVP 或 POC 阶段,等业务复杂后还是会回到子表设计。
六、订单系统需要记录订单所使用的优惠券的核销情况么
虽然 营销系统 是优惠券的发放方、生命周期管理方(发放、领取、过期、可用范围),但在实际的电商支付链路中:
-
下单时:订单系统必须知道“优惠券是否已经占用、是否可用”,否则订单金额算不准。
-
支付成功时:订单系统必须明确这笔订单 确实使用了哪些优惠券,并将它们标记为已核销。
-
退款/退货时:订单系统要知道是否需要 释放优惠券(可退回)或保持已核销(不可退回)。
👉 如果只依赖营销系统,而订单系统不存储核销结果,会出现风险:
-
账不对:订单表里显示优惠 50 元,但营销系统显示券还没用掉。
-
退款难:订单系统没记录用过哪些券,退款时无法正确释放。
-
对账难:财务侧对比订单优惠金额与营销系统券核销金额,不一致。
所以,订单系统需要保存优惠券的核销结果。
1.实际落地的设计
表设计(简化版,对之前的order_coupon整改)
-- 订单优惠券关联表 CREATE TABLE order_coupons ( id BIGINT PRIMARY KEY AUTO_INCREMENT, order_id BIGINT NOT NULL, coupon_id BIGINT NOT NULL, discount_amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL, -- 0: 占用中, 1: 已核销, 2: 已退回 created_at DATETIME NOT NULL, updated_at DATETIME NOT NULL, UNIQUE(order_id, coupon_id) );
-
status 字段非常关键:
-
0: 占用中
→ 下单锁券,还未支付 -
1: 已核销
→ 支付成功,券使用完成 -
2: 已退回
→ 订单取消或退款,券退回用户
-
业务流程
-
下单
-
订单系统写
order_coupons(status=0)
-
营销系统同步标记该券“已占用”
-
-
支付成功
-
订单系统将
status=1
-
营销系统也更新为“已核销”
-
-
订单取消/退款
-
如果券可退回:订单系统更新
status=2
,通知营销系统退回 -
如果券不可退回:保持
status=1
-
3. 行业参考
-
淘宝/天猫:订单详情页会明确显示“已使用的优惠券/红包”,并在订单取消时自动退回券 → 说明订单系统一定存了核销信息。
-
京东:订单详情同样会记录优惠券抵扣金额,并在退单时处理释放。
-
亚马逊(含 gift card):订单侧会记录 gift card 抵扣的金额,退款时会退回到账户余额或 gift card。
👉 都是 订单系统和营销系统双记录,保证对账和退款链路可追溯。
✅ 结论:
订单系统必须 记录优惠券核销情况,不能只靠营销系统。
最合理的方式是:
-
营销系统 负责优惠券生命周期和发放管理;
-
订单系统 负责订单维度的优惠券核销落地(通过
order_coupons
表),确保账实一致。
转载请注明出处:https://www.cnblogs.com/fnlingnzb-learner/p/19089321