当前位置: 首页 > news >正文

电商系统的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,   -- 发放来源/活动IDmetadata 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,上线时替换即可。

 

http://www.hskmm.com/?act=detail&tid=3414

相关文章:

  • Docker应用 - CloudSaver
  • SQL查找是否存在,别再count了! - DAYTOY
  • Cursor小程序实战系列二:如何从原型界面到小程序界面
  • Cursor小程序实战系列三: 前后端对接保姆级拆解
  • 课前问题思考2
  • Cursor小程序实战四:如何让AI写好后端代码
  • Web 3
  • Cursor小程序实战系列一:0到1开发一个小程序,需求整理、小程序注册备案
  • 深入解析:MySQL 数据类型与运算符详解
  • 【前端Vue】如何优雅地在vue中引入ace-editor编辑器 - 指南
  • USACO08 OPEN Roads Around the Farm S (递归)
  • JavaScript生成随机数的方法
  • LiveOS 的制作简介
  • .gitignore 文件
  • 目标检测 | 基于Weiler–Atherton算法的IoU求解
  • 对比Java学习Go——函数、集合和OOP
  • MySQL集群高可用架构 - 指南
  • 【WRF-VPRM 预处理器】HEG 安装(服务器)-MRT专业的工具替代
  • 如何在Spring MVC中处理请求参数
  • redis实现缓存2-解决缓存穿透,缓存击穿
  • 单克隆抗体人源化:从鼠源缺陷到全人源突破,3 大阶段破解临床应用难题
  • 在Kubernetes中DaemonSet无法在master节点调度的问题
  • 9 12-
  • 桌面客户端的主要类型和技术方案
  • AGX Orin平台RTC驱动导致reboot系统卡住障碍调试
  • C 语言实现动态数组、链表、栈与队列
  • git reset
  • ICPC 2025 网络赛第一场 M
  • Brute It -TryHackMe
  • 题解:P12336 第三心脏