电商系统的订单+商品+营销的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) 用户/商户/商品(简化,用于 FK/演示)
CREATE TABLE users (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(64) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX(idx_username(username))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE merchants (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(128) NOT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE products (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  sku VARCHAR(64) NOT NULL,
  title VARCHAR(255) NOT NULL,
  price INT NOT NULL, -- 单位:分(当前售卖价的基价)
  currency CHAR(3) DEFAULT 'CNY',
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX(idx_sku(sku))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


-- 2) 优惠券模板(定义规则)
CREATE TABLE coupon_templates (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(128) NOT NULL,
  type ENUM('FIXED','PERCENT','FREE_SHIPPING') NOT NULL COMMENT 'FIXED: 固定金额; PERCENT: 百分比',
  amount INT DEFAULT NULL COMMENT '当 type=FIXED 时,面额(分)',
  percent SMALLINT DEFAULT NULL COMMENT '当 type=PERCENT 时,百分比,0-10000(表示0.00%-100.00%),可用精度小数处理',
  min_spend INT DEFAULT 0 COMMENT '满减门槛(分)',
  max_discount INT DEFAULT NULL COMMENT '百分比折扣上限(分)',
  stackable TINYINT(1) DEFAULT 0 COMMENT '是否可与其他优惠券叠加',
  valid_from DATETIME,
  valid_to DATETIME,
  total_issuance INT DEFAULT NULL,
  metadata JSON DEFAULT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


-- 3) 发放到用户的优惠券实例(每张券都是一条记录)
CREATE TABLE user_coupons (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  user_id BIGINT UNSIGNED NOT NULL,
  template_id BIGINT UNSIGNED NOT NULL,
  code VARCHAR(64) DEFAULT NULL, -- 若有券码
  status ENUM('ISSUED','LOCKED','USED','EXPIRED','REVOKED') DEFAULT 'ISSUED',
  face_amount INT DEFAULT NULL,      -- 迁移模板到实例时的“面额快照”,单位分(若FIXED)
  percent SMALLINT DEFAULT NULL,     -- percent 快照
  min_spend INT DEFAULT NULL,        -- 快照
  expire_at DATETIME DEFAULT NULL,
  issued_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
  locked_until DATETIME DEFAULT NULL,
  used_at DATETIME DEFAULT NULL,
  used_order_id BIGINT UNSIGNED DEFAULT NULL,
  source VARCHAR(64) DEFAULT NULL,   -- 发放来源/活动ID
  metadata JSON DEFAULT NULL,
  INDEX(idx_user_status(user_id, status)),
  FOREIGN KEY (user_id) REFERENCES users(id),
  FOREIGN KEY (template_id) REFERENCES coupon_templates(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


-- 4) 订单主表
CREATE TABLE orders (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  order_no VARCHAR(64) NOT NULL UNIQUE COMMENT '对外订单号 (建议业务方生成,或交易系统发号器)',
  user_id BIGINT UNSIGNED NOT NULL,
  merchant_id BIGINT UNSIGNED NOT NULL,
  currency CHAR(3) DEFAULT 'CNY',
  total_amount INT NOT NULL COMMENT '商品原始总价(分)',
  shipping_fee INT DEFAULT 0 COMMENT '运费(分)',
  promotion_discount INT DEFAULT 0 COMMENT '促销/活动优惠总额(分)',
  coupon_discount INT DEFAULT 0 COMMENT '优惠券抵扣总额(分) —— 快查字段',
  payable_amount INT NOT NULL COMMENT '实际应付金额 = total_amount + shipping_fee - promotion_discount - coupon_discount',
  status ENUM('CREATED','PAID','CANCELLED','SHIPPED','COMPLETED','REFUNDED','CLOSED') DEFAULT 'CREATED',
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  INDEX(idx_user_created(user_id, created_at)),
  FOREIGN KEY (user_id) REFERENCES users(id),
  FOREIGN KEY (merchant_id) REFERENCES merchants(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


-- 5) 订单商品明细(每行存快照)
CREATE TABLE order_items (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  order_id BIGINT UNSIGNED NOT NULL,
  product_id BIGINT UNSIGNED DEFAULT NULL,
  sku VARCHAR(64) DEFAULT NULL,
  title VARCHAR(255) NOT NULL COMMENT '商品标题快照',
  qty INT NOT NULL DEFAULT 1,
  unit_price INT NOT NULL COMMENT '下单时单价(分)',
  orig_total INT NOT NULL COMMENT 'unit_price * qty(分)',
  item_discount INT DEFAULT 0 COMMENT '本行促销/活动优惠(分)',
  item_coupon_discount INT DEFAULT 0 COMMENT '本行优惠券抵扣(分),用于拆分多张券场景',
  final_amount INT NOT NULL COMMENT '最终该行实付金额(分)',
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX(idx_order_id(order_id)),
  FOREIGN KEY (order_id) REFERENCES orders(id),
  FOREIGN KEY (product_id) REFERENCES products(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


-- 6) 订单与优惠券的关联表(记录每张券实际抵扣金额及快照)
CREATE TABLE order_coupons (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  order_id BIGINT UNSIGNED NOT NULL,
  user_coupon_id BIGINT UNSIGNED NOT NULL COMMENT '引用 user_coupons.id(券实例)',
  template_id BIGINT UNSIGNED NOT NULL COMMENT '引用 coupon_templates.id(便于查询)',
  code VARCHAR(64) DEFAULT NULL,
  applied_amount INT NOT NULL COMMENT '该张券在本订单中实际抵扣(分)',
  coupon_snapshot JSON DEFAULT NULL COMMENT '冗余快照(如面额/percent/min_spend/stackable),用于审计/回溯',
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  INDEX(idx_order_id(order_id)),
  INDEX(idx_user_coupon(user_coupon_id)),
  FOREIGN KEY (order_id) REFERENCES orders(id),
  FOREIGN KEY (user_coupon_id) REFERENCES user_coupons(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


-- 7) 支付流水表(支持多笔支付/分次支付)
CREATE TABLE payments (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  payment_no VARCHAR(64) NOT NULL UNIQUE COMMENT '对外支付单号/商户支付流水号',
  order_id BIGINT UNSIGNED NOT NULL,
  amount INT NOT NULL COMMENT '本次支付金额(分)',
  channel VARCHAR(64) NOT NULL COMMENT '渠道,如 ALIPAY, WECHAT, STRIPE 等',
  channel_txn_id VARCHAR(128) DEFAULT NULL COMMENT '通道返回流水号',
  status ENUM('PENDING','SUCCESS','FAILED','PARTIALLY_REFUNDED','REFUNDED') DEFAULT 'PENDING',
  fee INT DEFAULT 0 COMMENT '渠道手续费(分)',
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  paid_at DATETIME DEFAULT NULL,
  refunded_amount INT DEFAULT 0,
  INDEX(idx_order_id(order_id)),
  FOREIGN KEY (order_id) REFERENCES orders(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;


-- 8) 退款表(记录退款单、是否需要退回优惠券等)
CREATE TABLE refunds (
  id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
  refund_no VARCHAR(64) NOT NULL UNIQUE,
  order_id BIGINT UNSIGNED NOT NULL,
  payment_id BIGINT UNSIGNED DEFAULT NULL,
  amount INT NOT NULL COMMENT '退款金额(分)',
  status ENUM('REQUESTED','PROCESSING','SUCCESS','FAILED') DEFAULT 'REQUESTED',
  reason VARCHAR(255) DEFAULT NULL,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
  processed_at DATETIME DEFAULT NULL,
  channel_refund_id VARCHAR(128) DEFAULT NULL,
  need_restore_coupon TINYINT(1) DEFAULT 0 COMMENT '退款成功后是否需返还优惠券(业务决定)',
  INDEX(idx_order_id(order_id)),
  FOREIGN KEY (order_id) REFERENCES orders(id),
  FOREIGN KEY (payment_id) REFERENCES payments(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

设计逻辑与细节说明(逐项解释)

是否把优惠券直接写在 orders 表?

  • 不推荐将多张优惠券的关系直接放在 orders 表(比如把 coupon_id 写成 CSV),理由:

    • 订单可能使用多张券(多对多);

    • 需要记录每张券的实际抵扣金额与快照(券规则可能变),以及用于审计和退券处理;

    • 查询/统计/对账更复杂。

  • 推荐方案order_coupons 单独表存一条券实例与订单的关联,并在 orders 表保留 coupon_discount(总额)和可选 primary_coupon_id(若你想快速指向主券),用于快速检索和支付计算。

为什么需要快照字段(order_items.unit_price / coupon_snapshot)?

  • 商品价格、活动规则、优惠券模板可能随时间变更。为保证订单历史一旦生成不可变更,必须把当时的关键字段快照在 order_itemsuser_coupons(实例化时)和 order_coupons.coupon_snapshot 里。否则后续审计/回溯无法还原当时账务。

优惠券的生命周期(状态机)

  • ISSUED:发到用户,未被锁定/使用。

  • LOCKED:下单时临时锁住(避免并发二次使用),通常和订单创建在同一事务/或 SELECT FOR UPDATE 配合设置 locked_until。下单成功且支付成功后设置 USED,超时/取消则回滚或设置为 ISSUED

  • USED:已在订单中生效并且确认(通常在支付成功或订单提交成功时被标记)。

  • EXPIRED / REVOKED:失效或被后台回收。

事务处理要点:在创建订单并应用券时,应 SELECT ... FOR UPDATE 锁住对应 user_coupons 行,status 检查通过后更新为 LOCKED、插入 ordersorder_itemsorder_coupons,最后在支付成功时更新 user_coupons.status = USED(或在下单提交成功时直接置为 USED,根据业务决定)。

报表/统计与查询优化

  • 经常需要查询用户在某段时间使用的券、按商户汇总的券抵扣等,所以 user_couponsorder_coupons 应该有 user_id, order_id, template_id 的索引。

  • orders.coupon_discountorders.promotion_discount 作为聚合/对账快查字段,避免每次统计都 join 多张表。

退款与优惠券处理

  • 退款时要明确业务规则:退款是否退回优惠券?

    • 常见做法:如果整单退款且规则允许,则将 user_coupons.status 恢复为 ISSUED(或 REVOKED,看活动策略),同时在 refunds.need_restore_coupon=1 记录;若只部分退款,可能不退券或分段退回。

    • 所以 refunds 表要记录 need_restore_coupon 以及退款成功后由后台或异步任务去恢复券状态并记录历史。

并发/幂等

  • 所有对 user_coupons 的消费都要做到幂等并发保护,用 SELECT ... FOR UPDATE 或行级锁实现。

  • order_nopayment_no 都需要唯一索引以保证幂等(避免重复下单/重复支付)。

分库分表考虑

  • 在分库分表场景:

    • orders 通常以 user_idorder_no 的某种哈希为分片键;

    • user_coupons 也建议按 user_id 分片(便于局部事务操作);

    • 跨库事务要尽量避免,订单创建时最好能在同一个库完成 ordersorder_itemsorder_couponsuser_coupons(LOCK) 的操作——因此建议把 user_couponsorders 存在同一分片路由上(例如按 user_id 分片)。

    • 如果 user_coupons 在别的库,应用层需做好分布式事务补偿(异步补偿 / SAGA)。


典型下单与优惠券使用(事务化伪代码)

-- 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,上线时替换即可。

 

 三、要不要将正向订单和逆向订单拆成不同表

🎯 总体设计思路

  1. 是否放在同一个表?

    • 放在一个表:

      • 优点:统一查询、减少表数量。

      • 缺点:字段差异大,逻辑分支复杂,影响扩展性。

    • 分表:

      • 通常业界实践是 正向订单逆向订单 分开存放,因为生命周期、业务字段、处理逻辑不同。

      • 比如:淘宝、京东、亚马逊这种大规模电商平台,都会区分“销售订单”和“售后订单(退款/退货/拒付)”。

  2. 逆向订单要不要分多个表?

    • 一般不建议为每个逆向类型单独建表(退货单表、退款单表、拒付单表),否则表过多,扩展困难。

    • 推荐:

      • 正向订单表(销售订单)

      • 逆向订单表(售后订单:退货、拒付、退款都放这里,通过 reverse_type 区分)

  3. 逆向订单与正向订单关系:

    • 逆向订单是基于原始正向订单产生的。

    • 必须有一个字段 origin_order_id,指向对应的正向订单。


🎯 支付单是否要拆成正向/逆向两类?

你说得对:

  • 在支付体系里,退款也需要有单独的“支付单号”(比如支付宝/微信会返回一个 refund_id)。

  • 如果退款单直接更新在原始支付单里,会丢失 多次退款 的链路信息(一个支付可能对应多个退款)。

因此,业界一般做法是:

  • 支付单表只记录“正向支付”(即从用户到商户的资金流)。

  • 退款单(逆向资金流)单独建表,而不是混在一个表里。


📑 数据库表设计(MySQL)

1. 正向订单表(sales_order)

CREATE TABLE sales_order (
    order_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '订单ID',
    user_id BIGINT NOT NULL COMMENT '用户ID',
    merchant_id BIGINT NOT NULL COMMENT '商户ID',
    total_amount DECIMAL(18,2) NOT NULL COMMENT '订单总金额',
    status VARCHAR(32) NOT NULL COMMENT '订单状态:CREATED, PAID, SHIPPED, COMPLETED, CANCELED',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);

2. 正向支付单表(payment)

CREATE TABLE payment (
    payment_id BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '支付单ID',
    order_id BIGINT NOT NULL COMMENT '正向订单ID',
    pay_channel_order_no VARCHAR(64) NOT NULL COMMENT '支付通道返回的支付流水号',
    amount DECIMAL(18,2) NOT NULL COMMENT '支付金额',
    method VARCHAR(32) NOT NULL COMMENT '支付方式: ALIPAY, WECHAT, VISA...',
    status VARCHAR(32) NOT NULL COMMENT '支付状态:INIT, SUCCESS, FAILED',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
    UNIQUE KEY uq_channel_order (pay_channel_order_no),
    FOREIGN KEY (order_id) REFERENCES sales_order(order_id)
);

3. 逆向订单表(reverse_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


🔄 支付单关系与流程

  1. 正向支付

    • 用户下单 → 创建 payment 记录

    • 调用支付通道(支付宝/微信) → 返回通道支付流水号(pay_channel_order_no

    • 更新 payment.status=SUCCESS

  2. 退款

    • 用户申请退款 → 创建 reverse_order(逆向订单)

    • 基于某个 payment 新建 refund 记录

    • 调用支付通道退款接口 → 返回 refund_channel_order_no

    • 更新 refund.status=SUCCESS

    • 如果全额退款 → 同时更新 payment.status=REFUNDED

    • 如果部分退款 → payment 保持 SUCCESS,但挂有多条 refund

  3. 多次退款

    • 一个 payment → 可以对应多个 refund(部分退款场景常见)

  4. 拒付(chargeback)

    • 通常不通过退款接口,而是由银行/支付机构发起资金回退

    • 在你的 reverse_order 表里记一条 CHARGEBACK,然后在退款表 refund 中也插一条对应的资金逆向流水


✅ 总结

  • 正向订单与逆向订单分表存储,通过 origin_order_id 建立关系。

  • 逆向订单表中通过 reverse_type 区分退款、退货、拒付,而不是拆成多个表。

  • 处理流程:

    • 正向:下单 → 支付 → 发货 → 收货

    • 退货:退货单 → 审核 → 退回商品 → 退款

    • 拒付:支付机构发起 → 审核 → 资金调整

    • 退款:申请退款 → 审核 → 支付网关退款

  • 支付单表:只记录正向资金流。

  • 退款单表:单独建表,挂在支付单之下,用于记录逆向资金流。

  • 退款单号:既有内部生成的 refund_id,也有支付通道返回的 refund_channel_order_no

  • 优势

    • 保留完整支付+退款链路

    • 支持部分退款、多次退款

    • 对账时可以一一对应支付通道流水


📌 为什么推荐拆表?

  1. 业务语义清晰

    • 正向订单:代表用户买东西(从用户到商户的资金流)。

    • 逆向订单:代表退款/拒付/退货(从商户到用户的资金流)。

    • 放在一个表里,需要 order_type 字段区分,查询和统计时要加很多条件,逻辑耦合度高。

  2. 状态机完全不同

    • 正向订单常见状态:INIT → PAID → SHIPPED → FINISHED

    • 逆向订单常见状态:INIT → APPROVED → REFUNDING → REFUNDED / FAILED

    • 如果强行放在一张表,会出现大量无效字段,字段语义会越来越模糊。

  3. 扩展性好

    • 逆向订单后续可能有 多次申请、部分退款、银行拒付 等场景,字段设计和正向订单差别很大。

    • 单独一张表可以灵活加字段,不会污染正向订单表。

  4. 性能与分库分表

    • 电商里正向订单数量远大于逆向订单。

    • 如果放在一张表,数据量巨大且冷热数据混杂;拆表后,逆向订单表更小更轻,查询效率更高。

  5. 符合行业主流实践

    • 支付宝 / 微信支付:明确区分 trade_orderrefund_order(退款有单独的 refund_id)。

    • Amazon / eBay:也有 Order 表和 Return / Refund 表,分开存储。


📌 那是不是一定要拆?

  • 小系统(数据量小、逻辑简单):可以放一起,用 order_type 字段区分,开发成本低。

  • 大中型系统(电商/支付中心):强烈推荐拆表,清晰、可扩展、利于分库分表。


📌 个人推荐

正向订单表:只存购买行为
逆向订单表:单独一张表存退款/退货/拒付

这样:

  • 正向和逆向的订单号规则也可以分开(比如 O20250913001 / R20250913001)。

  • 支付单和退款单可以分别挂载在正向/逆向订单之下。

 四、订单明细(order_item)和订单优惠券(order_coupons)是不是要拆分正向逆向

这是个非常关键的问题 ,涉及 正向单 & 逆向单 的设计选择。我们可以从电商系统里常见的 库存回滚、优惠券返还、对账一致性 来拆解。


1️⃣ 方案一:在 order_itemsorder_coupons 增加状态字段

  • 做法
    在现有的正向订单明细表中,新增字段,比如:

    • refund_status(未退款/部分退款/已退款)

    • refunded_quantity(已退款数量)

    • refunded_amount(已退款金额)

  • 优点

    • 表结构简单,不需要额外的逆向表。

    • 查询订单整体情况时只查一张表,简单直观。

  • 缺点

    • 难以支撑复杂的退款场景(部分退款、多次退款、跨期退款)。

    • 如果要做对账或审计,正向/逆向混在一起,不容易拆清楚。

    • 退款数据和下单数据耦合,不利于扩展。


2️⃣ 方案二:新增退款记录(逆向单表)

  • 做法
    新增两张“逆向表”:

    • refund_items(对应 order_items,记录每次退款的商品明细)

    • refund_coupons(对应 order_coupons,记录每次优惠券返还的情况)

  • 逻辑

    • 正向单表只保留原始下单数据,永远不变。

    • 退款时,在 refund_itemsrefund_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

posted @ 2025-09-13 16:23  Boblim  阅读(41)  评论(0)    收藏  举报