综合 支付状态与订单状态分离:三张表、三层幂等与最终一致性落地

2026-09-19 21:32:46

支付状态与订单状态分离:三张表、三层幂等与最终一致性落地

参考来源:SpringCloud 微服务实战:支付全链路生产级落地(接口对接 + 异步通知 + 订单状态闭环),https://jishuzhan.net/article/2036259148061540353

服务拆分与回调服务独立部署

按单一职责拆分为网关服务(SpringCloud Gateway,负责转发、鉴权、限流、日志)、订单服务(订单创建与状态管理,是订单状态的唯一权威数据源)、支付服务(三方支付对接、预支付、支付记录、结果查询)、回调通知服务(独立部署,接收回调、验签、消息分发,与核心业务隔离),以及 Nacos、Sentinel、RocketMQ、Redis 等公共基础服务。

回调服务单独拆出来的原因只有一个:回调接口是三方支付直接访问的入口,它的可用性不取决于业务逻辑有多复杂,而取决于它有多快、多稳。独立部署后,核心业务的流量波动、发布、故障都不会影响回调可达性,也便于单独做安全防护与扩容。

设计原则

  • 安全第一:签名验签、全程 HTTPS、敏感信息加密。
  • 幂等性优先:所有支付相关接口,尤其是回调接口,必须幂等。
  • 最终一致性:用可靠消息加兜底补偿保证订单状态与支付状态一致,不追求强一致。
  • 可追溯性:下单、预支付、回调、状态更新全链路留日志。
  • 快速响应与降级:核心接口尤其是回调接口 100ms 内返回,非核心逻辑异步解耦,支持熔断降级。

三张核心表

t_order_info:订单信息表

订单状态的唯一权威数据源,由订单服务写入。

字段:idorder_no(全局唯一)、user_idproduct_idorder_amountpay_amountorder_statuspay_type(1 微信 2 支付宝)、transaction_id(三方流水号)、pay_timeexpire_timecreate_timeupdate_timeremark

order_status 取值:0 待支付、1 支付中、2 支付成功、3 支付失败、4 已取消、5 已完成。

索引:PRIMARY(id)UNIQUE uk_order_no(order_no)KEY idx_user_idKEY idx_create_time

CREATE TABLE `t_order_info` (
  `id`             bigint unsigned NOT NULL AUTO_INCREMENT,
  `order_no`       varchar(64)  NOT NULL COMMENT '订单号,全局唯一',
  `user_id`        bigint       NOT NULL,
  `product_id`     bigint       NOT NULL,
  `order_amount`   decimal(10,2) NOT NULL COMMENT '订单金额(元)',
  `pay_amount`     decimal(10,2) DEFAULT NULL COMMENT '实付金额(元)',
  `order_status`   tinyint      NOT NULL DEFAULT 0 COMMENT '0待支付 1支付中 2支付成功 3支付失败 4已取消 5已完成',
  `pay_type`       tinyint      DEFAULT NULL COMMENT '1微信 2支付宝',
  `transaction_id` varchar(64)  DEFAULT NULL COMMENT '三方支付流水号',
  `pay_time`       datetime     DEFAULT NULL,
  `expire_time`    datetime     DEFAULT NULL,
  `create_time`    datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time`    datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `remark`         varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_no` (`order_no`),
  KEY `idx_user_id` (`user_id`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

t_pay_record:支付记录表

每笔支付请求一条记录,与订单是多对一:一个订单可能因超时关闭、重新发起等原因产生多条支付流水。

字段:idpay_no(唯一)、order_nouser_idpay_amountpay_typepay_statustransaction_idprepay_idpay_timecreate_timeupdate_time

pay_status 取值:0 待支付、1 支付中、2 成功、3 失败、4 已关闭。

索引:PRIMARY(id)UNIQUE uk_pay_no(pay_no)KEY idx_order_noKEY idx_user_id

CREATE TABLE `t_pay_record` (
  `id`             bigint unsigned NOT NULL AUTO_INCREMENT,
  `pay_no`         varchar(64)  NOT NULL COMMENT '支付流水号,全局唯一',
  `order_no`       varchar(64)  NOT NULL,
  `user_id`        bigint       NOT NULL,
  `pay_amount`     decimal(10,2) NOT NULL,
  `pay_type`       tinyint      NOT NULL COMMENT '1微信 2支付宝',
  `pay_status`     tinyint      NOT NULL DEFAULT 0 COMMENT '0待支付 1支付中 2成功 3失败 4已关闭',
  `transaction_id` varchar(64)  DEFAULT NULL COMMENT '三方支付流水号',
  `prepay_id`      varchar(128) DEFAULT NULL,
  `pay_time`       datetime     DEFAULT NULL,
  `create_time`    datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time`    datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_pay_no` (`pay_no`),
  KEY `idx_order_no` (`order_no`),
  KEY `idx_user_id` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

t_pay_callback_log:支付回调日志表

所有回调全量落库,既是排查依据,也是幂等体系的第一层保障。

字段:idorder_notransaction_idpay_typerequest_body(text)request_header(text)sign_verify_result(0 失败 1 成功)、handle_result(0 待处理 1 成功 2 失败)、response_bodycallback_countcreate_timeupdate_time

索引:PRIMARY(id)UNIQUE uk_order_transaction(order_no, transaction_id)KEY idx_create_time

CREATE TABLE `t_pay_callback_log` (
  `id`                 bigint unsigned NOT NULL AUTO_INCREMENT,
  `order_no`           varchar(64)  NOT NULL,
  `transaction_id`     varchar(64)  NOT NULL,
  `pay_type`           tinyint      NOT NULL COMMENT '1微信 2支付宝',
  `request_body`       text         COMMENT '原始回调报文',
  `request_header`     text         COMMENT '原始请求头',
  `sign_verify_result` tinyint      NOT NULL DEFAULT 0 COMMENT '0失败 1成功',
  `handle_result`      tinyint      NOT NULL DEFAULT 0 COMMENT '0待处理 1成功 2失败',
  `response_body`      varchar(512) DEFAULT NULL,
  `callback_count`     int          NOT NULL DEFAULT 1 COMMENT '该笔回调次数',
  `create_time`        datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  `update_time`        datetime     NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_order_transaction` (`order_no`, `transaction_id`),
  KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

两个设计取舍

订单状态与支付状态为什么分开。 order_status 由订单服务维护,描述交易本身的生命周期(待支付到已完成);pay_status 由支付服务维护,描述某一次支付请求的结果。一个订单可以对应多次支付尝试,把两者压进一张表,就会出现「订单已取消但支付成功」「多次支付流水互相覆盖」这类无法表达的状态。分表之后,每个状态只有一个写入方,回调和查单只更新支付记录,订单状态由订单服务根据支付结果推进。

回调日志为什么全量落库。 回调是外部系统触发的、不可重放的输入,丢掉一次请求就等于丢掉对账依据。日志表同时承担三个职责:原始报文可回查、验签与处理结果可统计、order_no + transaction_id 联合唯一索引在数据库层直接拒绝重复回调插入。

所有订单号、支付流水号都建唯一索引,唯一性由数据库保证,不依赖应用层判断。三张表都带 create_timeupdate_time,便于对账和排查。

支付对接中与数据层相关的约束

以微信支付 V3 为例,支付宝逻辑一致,差异在签名方式和参数。签名为 SHA256 with RSA,商户私钥签名、平台公钥验签,请求头携带 Authorization(包含签名信息、商户号、证书序列号),必须走 HTTPS。

与数据一致性直接相关的几条:

  • 统一下单的 out_trade_no 就是幂等号,用支付流水号(pay_no)生成,保证每次请求唯一,重复请求不会创建新的预支付单。
  • 金额单位是分,必须是整数。前端传入的 total_amount 是元字符串、保留两位小数;落库与三方交互按整数分处理,避免浮点误差。
  • 服务器出口 IP 必须加入商户平台 IP 白名单,否则请求直接被拦截。
  • 回调地址必须是公网 HTTPS,不带参数、不挂登录鉴权拦截,否则三方回调根本进不来;本地调试用 frp 或花生壳做内网穿透。
  • 生产密钥放配置中心并加密(如 Nacos 配置加密),禁止硬编码进代码仓库;环境隔离,HTTP 客户端配置连接池与超时。

回调与幂等

微信支付在未收到成功响应时,按 15s、15s、30s、1m、2m、5m、10m、30m、1h、2h、3h、3h、3h、6h、6h 的间隔重试,24 小时内共 15 次。重复回调是常态,不是异常。

回调设计原则:快速响应、验签优先、全链路日志、多层幂等、异常隔离。

三层幂等,每层拦截不同的东西

  1. DB 唯一索引t_pay_callback_logorder_no + transaction_id 联合唯一。重复回调在插入日志这一步就被数据库拒绝,拦截的是完全相同的重复报文。
  2. Redis 分布式锁:以订单号加锁,锁超时设 10 分钟。拦截的是并发到达的同一订单回调,避免多个线程同时进入业务处理。
  3. 订单状态机前置校验:只有待支付、支付中的订单才能被更新为支付成功;已经是支付成功的订单直接返回成功、不做任何处理。这一层是最后兜底,拦截索引与锁都没覆盖到的时序问题。

回调接口的处理顺序

取原始报文 → 取签名相关请求头(Wechatpay-SerialSignatureTimestampNonce)→ 校验证书序列号 → 验签 → 解密报文(AES-GCM,使用 APIv3 密钥)→ 幂等校验,重复直接返回成功 → 落回调日志 → 发 MQ 消息异步处理业务 → 返回 200 与 {"code":"SUCCESS"}

验签失败直接返回 400;只有业务处理失败才返回 FAIL,让三方按重试策略再推。回调接口内部不做任何复杂业务,先落库、再异步。

失败重试与归档

处理失败的回调记录留在日志表,定时任务每 5 分钟扫描失败记录重发 MQ,最多重试 10 次,超限告警;MQ 配置死信队列,由人工介入;回调日志按天归档。

订单状态闭环

状态闭环由三部分组成:状态机前置校验、可靠消息(RocketMQ)、定时兜底查单。回调到达后不直接改订单,而是落库并投递消息,由订单服务消费消息推进订单状态;定时任务扫描长时间处于「支付中」的订单,主动向三方查单补齐结果。

这套组合保证的是最终一致,目标是杜绝「支付成功但订单还是待支付」这种状态。强一致在这个链路上做不到,也不需要。

金额口径统一一遍:前端 total_amount 是元字符串、保留两位小数,三方接口金额单位是分且必须为正整数,库内按整数分处理以避免浮点误差。

复制全文 生成海报 支付系统 MySQL 幂等 状态机 RocketMQ

推荐文章

程序员茄子在线接单