订单架构和库表

463 阅读7分钟

我写过好几套订单系统,踩过不少坑,现在把这些订单系统总结,做成个通用的设计。按照最全的库表算,有订单表、付款单表、退款申请表、退款单表、订单结算表,每个表和其他表之间有关联,各位可以根据自己的业务场景灵活组合。

订单

create table order
(
    order_no              varchar(32)                            not null comment '订单号'
        primary key,
    status                int          default 10                not null comment '订单状态:10-待付款,20-进行中,30-已完成,40-已取消',
    pre_status            int          default 10                not null comment '上一个状态',
    user_id               bigint                                 null comment '用户ID',
    user_name             varchar(32)                            null comment '用户名称',
    title                 varchar(512)                           not null comment '订单标题',
    amount                decimal(10, 2)                         not null comment '订单金额',
    create_at             datetime     default CURRENT_TIMESTAMP null comment '创建时间',
    update_at             datetime     default CURRENT_TIMESTAMP null comment '修改时间',
    remark                varchar(128) default ''                null comment '备注'
)
    comment '订单表' collate = utf8mb4_unicode_ci
                     row_format = COMPACT; 

订单是主表,后续所有表都是基于订单表。这是订单表的骨架,请根据自己的业务扩展字段。

订单表有自己的状态机,以我现在写的业务为例,有以下状态。

状态:10-待付款,20-待接单,30-代练中,40-待验收,50-售后中,60-已完成,70-已售后,71-申诉中,72-申述完成,80-待付款款取消,90-已付款取消。

状态机的扭转要做校验,每次变动状态,需保证流向正确。比如代付款只能到已取消或者待接单。

image.png

image.png

如上图所示,修改订单状态的流程大致如此。

  1. 先用悲观锁查询行。
  2. 校验修改字段。
  3. 校验状态流转。
  4. 正式更新。

有任何错误,我不仅打了错误日志,也在飞书、钉钉同步报警。

订单表的状态和业务相关,和支付状态分离。

订单表大致这些内容,核心是状态机要设计好。

付款单

CREATE TABLE `order_pay`
(
    `pay_order_no`    varchar(32)    NOT NULL COMMENT '付款订单号',
    `order_no`        varchar(32)    NOT NULL COMMENT '关联订单号',
    `third_order_no`  varchar(128)   NULL DEFAULT NULL COMMENT '三方支付订单号',
    `status`          int            NOT NULL DEFAULT 1 COMMENT '付款状态:1待付款2已付款3退款中4部分退款5全部退款6取消',
    `user_id`         bigint         NULL DEFAULT NULL COMMENT '用户ID',
    `title`           varchar(512)   NOT NULL COMMENT '付款标题',
    `pay_amount`      decimal(10, 2) NOT NULL COMMENT '付款金额',
    `refund_amount`   decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '退款金额',
    `remain_amount`   decimal(10, 2) NOT NULL DEFAULT 0.00 COMMENT '剩余金额',
    `pay_at`          datetime       NULL DEFAULT NULL COMMENT '支付时间',
    `create_at`       datetime       NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_at`       datetime       NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `remark`          varchar(255)   NULL DEFAULT '' COMMENT '备注',
    PRIMARY KEY (`pay_order_no`),
    INDEX `idx_order_no` (`order_no`),
    INDEX `idx_user_id` (`user_id`),
    INDEX `idx_status` (`status`),
    INDEX `idx_create_at` (`create_at`)
) ENGINE = InnoDB
  CHARACTER SET = utf8mb4
  COLLATE = utf8mb4_unicode_ci COMMENT = '订单付款表(简化版)'
  ROW_FORMAT = DYNAMIC; 

付款单是为了订单支付的记录,我的习惯在创建订单的同时,也会创建付款单。

而且我会把创建订单和创建付款单都放到订单模块,用事物包裹一起保证原子性,无论订单还是付款单创建过程中有任何异常都回滚,同时报警。

付款单有自己的状态机,我一般是用如下几个,甭管什么系统,付款单的状态是一样的,状态机扭转也是一样的。

1待付款2已付款3退款中4部分退款5全部退款6取消

如果做的简单点,付款单的状态可以不包含退款,那就要在每次申请退款的时候查询所有退款单来校验剩余退款金额、是否有退款中的付款单。

付款单在生成后,创建个延时任务做订单自动取消,取消的时候连同订单状态一起改掉。

付款单生成后,使用单号和其他参数调支付中心获取支付链接,支付成功后支付中心异步回调过来,再同时修改订单状态和付款单状态。

依然记得每次操作数据库的时候,有任何失败不仅要打 error 日志,还要报警。

有些场景一个订单对应多个付款单,客户会对订单加价,每次加价生成一个新的付款单,Type 为加价类型,加价完成同时修改订单金额和状态。这里要注意状态扭转,我曾在这里出过 bug,把完结的订单又改成进行中了。

退款申请和退款单

CREATE TABLE `order_refund`
(
    `refund_order_no` varchar(32)    NOT NULL COMMENT '退款订单号',
    `pay_order_no`    varchar(32)    NOT NULL COMMENT '关联付款订单号',
    `order_no`        varchar(32)    NOT NULL COMMENT '关联订单号',
    `third_order_no`  varchar(128)   NULL DEFAULT NULL COMMENT '三方退款订单号',
    `status`          int            NOT NULL DEFAULT 1 COMMENT '退款状态:1待退款2退款成功3退款失败',
    `type`            int            NOT NULL DEFAULT 1 COMMENT '退款类型:1主动退款2申诉退款3系统退款',
    `refund_amount`   decimal(10, 2) NOT NULL COMMENT '退款金额',
    `refund_reason`   varchar(255)   NULL DEFAULT '' COMMENT '退款原因',
    `refund_at`       datetime       NULL DEFAULT NULL COMMENT '退款完成时间',
    `create_at`       datetime       NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
    `update_at`       datetime       NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
    `remark`          varchar(255)   NULL DEFAULT '' COMMENT '备注',
    PRIMARY KEY (`refund_order_no`),
    INDEX `idx_pay_order_no` (`pay_order_no`),
    INDEX `idx_order_no` (`order_no`),
    INDEX `idx_status` (`status`),
    INDEX `idx_create_at` (`create_at`)
) ENGINE = InnoDB
  CHARACTER SET = utf8mb4
  COLLATE = utf8mb4_unicode_ci COMMENT = '订单退款表(精简版)'
  ROW_FORMAT = DYNAMIC; 

退款的状态机很简单,只有1待退款2退款成功3退款失败,在退款完成时也会同步修改付款表的退款金额和状态。

退款一般是调用支付中心的原路退回,这里有两种处理方案。

  1. 业务在原路退回回调中写。(比如接到原路退回成功回到才把退款单状态改为退款成功)
  2. 业务先把状态都写好(先把状态改为退款成功再调接口),最后调原路退回,只做三方退款单号填入,如果原路退回失败,可以重试。

我之前用第一种写法,但业务复杂度非常高,因为支付宝和微信的原路退回一个同步一个异步,我不仅要在同步调用写一堆逻辑,还要做伪同步和异步回调。

所有我个人推荐第二种方式,先把业务上该改的全改了,这样后续就只剩下一个三方单号的填充,为了防止原路退回调用失败,可以做同一个单号的重复调用,只要商户单号是相同的,在支付宝、微信里面也不担心会重复退款。

有的业务退款需要客服审核一道,所以审核表根据场景加吧。

结算

订单完结后,会给两边用户打钱,还有普通手续费扣除。这里推荐用 kafka 队列,ack = all,消费时手动偏移并使用死信队列,保证每笔订单必会结算且只结算一次。

结算就会涉及钱包、资金,资金流水里面订单号+类型做唯一键,从数据库层面保证相同订单的相同类型(比如收入)只有一条流水。

ok 以上就是订单的通用设计,核心只有三张表,订单、付款单、退款单,用户不同的操作会这些表,同时操作多张表记得用事物包裹,每张表都有自己的状态机,保证状态扭转顺序正确,终态不允许扭转。任何操作的错误不仅要打 error 日志,也要飞书、钉钉报警。

涉及钱的地方一定要多多谨慎,适当抛弃性能追求稳定。