先说一个很多人没意识到的坑:网上关于"聚合支付数据库设计"的文章,八成从第一张表开始就设计错了。它们几乎清一色地把"订单"和"支付流水"当成同一张表,一张 orders 表里塞满了订单信息、支付信息、退款信息,字段越加越多,最后这张表变成一个谁都不敢动的庞然大物。等你真拿它上线,第一笔"用户支付失败后换通道重试"的订单,就能让你的表结构当场翻车。

这篇文章不给你抄一段现成的建表 SQL 就完事。我要从"为什么这么设计"讲起,把聚合支付数据库最核心的四张表——商户表、通道表、订单表、支付流水表,外加一张最容易被人忽略的对账表,一张一张拆给你看。这些设计逻辑,是我见过太多支付系统踩坑之后总结出来的,网上那些教程里基本不会跟你讲这些"为什么"。

在往下走之前,先对齐一个前提。这篇文章讲的是聚合支付系统的库表设计——也就是中间那一层:上面接商户,下面接微信、支付宝、易支付这些通道,你自己是那个"把钱从 A 通道转给 B 商户"的平台。如果你做的是直连某一家支付、没有商户和通道的概念,那这套结构对你来说是杀鸡用牛刀;但只要你沾了"聚合"两个字,下面这套逻辑就是地基,地基歪了上面全白搭。

先想清楚一件事:订单和支付流水,必须是两张表

这是全文最重要的一句话,我把它放在最前面,因为你后面所有表的设计,都建立在这个认知上。

为什么订单和支付流水不能合并?因为它们的生命周期根本不同步。你想象下面这个真实到不能再真实的场景:

一个用户在你的商户那里下单,金额 100 元。他先选了"微信支付",扫码后因为余额不足取消了,又回来改选"支付宝",这次支付成功了。请问,这一个"订单",对应了几次"支付行为"?——两次。第一次微信支付失败,第二次支付宝支付成功。

如果你把订单和支付合并成一张表,这个场景你怎么表达?你只能在 orders 表里记一个"支付渠道=支付宝"、"支付状态=成功",可中间那次失败的微信支付尝试去哪了?丢了。更糟的是,如果用户第一次微信支付其实"银行已扣款但回调没到",这就成了悬账,你没地方记录这笔"状态不明"的支付,对账的时候根本对不上。

所以正确的做法是:订单表(order)管"业务",支付流水表(payment)管"钱"。一个订单可以挂多条支付流水,一条流水对应一个订单、一次支付尝试。这样"微信失败后换支付宝成功"这件事,就是 order 表里一条记录,挂两条 payment 记录——一条失败的微信、一条成功的支付宝。账清清楚楚,谁也不会丢。

记住这个图,下面四张表都是围着它转的:

merchant(商户) 1 ── N order(订单) 1 ── N payment(支付流水) N ── 1 channel(通道)
即:一个商户有多笔订单,一笔订单有多条支付流水,一条流水走一个通道

第一张表:商户表(merchant),别把它当用户表用

商户表是整个聚合支付系统的"根",但很多人把它设计错了——他们把商户表和网站的用户表(users)合并,一个用户想收钱就直接给他一个商户号。这个做法在起步阶段没问题,等你的商户多了、业务复杂了,就会出大问题。

为什么?因为"用户"和"商户"是两个完全不同的概念。用户是你平台上的一个账号,他可能只是来买东西的;商户是跟你签了协议、具备收款资格、要走结算流程的主体。一个用户能不能收款、能收什么类型的款、费率多少、结算周期多长,这些是"商户"维度的属性,不该塞进用户表里污染它。

商户表的关键字段,我按重要性给你列出来,并说清楚为什么要有它:

  • merchant_no(商户号):平台内部唯一标识,对外暴露用,不要直接拿自增 id 当商户号,因为自增 id 会被猜、会暴露你的商户量级。商户号要无规律、够长。
  • merchant_name(商户名称):展示用,对账、结算单上要出现。
  • api_key / api_secret(接口密钥):商户调用你接口时的鉴权凭据。这里要说一个很多人忽略的点:key 和 secret 要分开存,而且 secret 不能明文存,至少要对称加密或哈希。密钥泄露是支付系统最致命的打击,这个字段的安全等级必须拉满。密钥怎么安全管理和轮换,这篇易支付密钥轮换实操:不停机更换商户API密钥的完整步骤讲得很细,值得单独看。
  • fee_rate(费率):这个商户的手续费比例。注意费率是按商户维度存的,不是按通道存——因为不同商户你给的费率优惠不一样。
  • settle_cycle(结算周期):T+0、T+1 还是自定义,商户的资金什么时候能提。
  • status(状态):正常、冻结、注销。冻结是风控的常用手段,必须有。
  • merchant_type(商户类型/类目):这个商户卖什么。这条看似不起眼,却直接关系到后面通道能不能给他用——因为不同通道对商户类目有准入限制。

商户表里最容易漏的一个字段是 merchant_type。很多系统上线时只有"个人商户"一种,等来了一个"卡密寄售"的商户,你才发现你的通道根本不接这类业务,而你的表结构里根本没有地方记录"这个商户属于什么类目",最后只能临时加字段、改逻辑。所以类目字段一开始就要留好。

第二张表:通道表(channel),一个商户为什么要有多个通道

通道表是聚合支付系统区别于直连系统的关键所在。所谓"聚合",聚合的就是通道。

通道表记录的是:你的平台接入了哪些上游支付能力。微信、支付宝这些是通道,易支付、其他聚合平台也可以是通道(通道下面还能接通道,这就是"多级聚合",先不展开)。每个通道都有自己的一套参数——appid、商户号、密钥、证书路径、回调地址等等。

通道表的核心字段:

  • channel_no(通道编号):平台内部标识,比如 WXPAY_001、ALIPAY_002。
  • channel_type(通道类型):微信、支付宝、银联、易支付……这个字段决定了后面用哪套协议去对接。
  • channel_config(通道配置):这里有个设计决策很关键——通道的密钥、证书这类敏感配置,建议单独存一张 channel_config 表或存加密的 JSON 字段,不要直接摊在通道表主表里。因为通道配置会频繁变动(密钥轮换、证书更新),而且它高度敏感,单独隔离便于管理和脱敏。
  • fee_rate(通道成本费率):这个通道本身收你多少成本。注意它和商户表的 fee_rate 不是一回事——商户表存的是你收商户多少,通道表存的是通道收你多少,两者的差就是你的利润。
  • status / weight(状态 / 权重):通道是否可用,以及多个同类型通道之间怎么分流。

这里要讲透一个关键设计:商户和通道之间,不是直接绑定的,中间要有一张"商户通道"关联表。因为一个商户可能同时开了微信和支付宝,一个微信通道下面又挂了成百上千个商户。如果你在商户表里直接加一个 channel_id 字段,那一个商户就永远只能用一个通道,这显然不合理。

所以正确的结构是:商户表、通道表各自独立,中间加一张 merchant_channel 关联表(商户 id + 通道 id + 该商户在该通道上的独立配置)。这样"商户 A 用微信通道 + 支付宝通道,商户 B 只用微信通道"就能灵活表达,而且每个商户在某个通道上还能有自己的费率、自己的限额。这个关联表,是很多入门教程直接跳过的,但它恰恰是聚合支付"聚合"二字的落点。

第三张表:订单表(order),只管业务,不管钱

回到前面说的核心原则,订单表只负责"业务订单"这一层。它回答的问题是:谁、在什么时候、下了一笔什么单、金额多少、现在处于什么业务状态。

订单表的核心字段:

  • order_no(平台订单号):平台内部唯一,主键或唯一索引。
  • merchant_no(商户号):这笔单属于哪个商户。
  • out_trade_no(商户订单号):这是商户自己生成的订单号,必须加唯一索引,而且唯一性要限定在"商户维度"内。也就是说,同一个商户下的 out_trade_no 不能重复,但不同商户之间可以重复(因为商户 A 和商户 B 各自生成自己的单号)。这个唯一索引是防重复下单、防重复入账的第一道闸门,漏了它,你的系统迟早被重复回调打穿。
  • subject / body(商品标题/描述):展示给用户看的。
  • amount(订单金额):这里必须是整数,单位是"分"。不要用 float,不要用 decimal 存元。浮点数在二进制里是近似值,0.1 + 0.2 不等于 0.3 的经典坑,在钱上面会要命。用 BIGINT 存分,所有计算、比较、累加都精确无误差。这是支付系统的铁律,谁用 float 存钱谁就是在埋雷。
  • status(订单状态):待支付、支付成功、已关闭、部分退款、全额退款。注意订单状态和支付流水状态是两个维度,别混。
  • notify_url / return_url(回调地址):支付完成后的异步通知和同步跳转地址。这里要强调:回调地址属于订单维度,因为不同商户、甚至不同订单的回调地址可能不同。

订单表最容易踩的坑,是把它当成"万能表",往里塞支付结果、第三方流水号、退款记录。一旦你这么干,订单表就会膨胀成一张"上帝表",改一次字段要牵连一堆接口。记住:订单表只回答"这笔业务订单怎么样了",至于钱走没走成、走了哪个通道、第三方流水号是多少,那是支付流水表的事。

第四张表:支付流水表(payment),才是真正碰钱的表

支付流水表是整个系统里最忙、也最重要的一张表。它记录的是每一次真实的支付行为,一条记录对应一次"发起支付→收到结果"的完整过程。

支付流水表的核心字段:

  • payment_no(平台流水号):平台内部唯一,每一次支付尝试生成一个。
  • order_no(关联订单号):这条流水属于哪笔订单。一个订单可以有多条流水,这就是"一对多"的落点。
  • channel_no(走的哪个通道):这条流水是通过哪个通道完成的。
  • amount(支付金额):同订单金额,整数分。
  • transaction_id(第三方流水号):这是通道(微信/支付宝/易支付)返回的官方流水号,必须加唯一索引。它是你对账、退款、查单的唯一凭据,也是防止重复入账的第二道闸门——同一条第三方流水号,绝不能在你的表里入账两次。
  • status(支付状态):待支付、支付成功、支付失败、已退款、部分退款。注意和订单状态是两回事:订单可以"部分退款",但某一条流水可能是"已退款",另一条流水还是"支付成功"。
  • paid_at(实际支付时间):用户真正付钱的那一刻,不是回调到达的那一刻。这个时间对账、退款、统计都很关键。
  • notify_status(回调状态):是否已经通知商户、通知了几次、是否成功。回调的可靠性要单独管,因为商户可能没收到、可能收到了但处理失败,你需要重试。

支付流水表有两个最要命的设计细节,我必须单独强调:

第一,transaction_id 的唯一索引,是整个系统防重复入账的命门。通道的回调是会重复推的——网络重试、你这边处理慢、通道自己重发,同一个 transaction_id 可能给你推好几次。如果你不靠唯一约束挡着,一笔钱就会被记两次。所以拿到回调后,先按 transaction_id 查有没有已处理的记录,有就直接返回成功,这叫幂等。幂等做不到,你的账永远对不上,用户被扣两次钱,反馈和资损一起来。

第二,支付流水的状态变更,要能追溯,不要原地覆盖。一笔流水从"待支付"变成"支付成功",再变成"部分退款",这个过程如果只改一个 status 字段,你就丢了历史。严谨的做法是加一张流水状态变更表,或者至少在关键节点记录操作日志。这样出了问题,你能还原这笔钱从头到尾经历了什么。

关于回调怎么处理、sign 怎么验、幂等和重试怎么做,这套接口层的细节,这篇易支付接口平台API对接完整指南:参数表+签名算法+可跑代码里讲得很完整,表结构设计好了,接口层照着那套来,基本就闭环了。

对账表(reconciliation):存的是差异,不是明细

对账是支付系统里最容易被"轻看"、但一出事就最要命的一块。很多系统压根没有对账,或者对账就是把两边的流水拉出来肉眼比一比。这在小单量下能凑合,单量一大,每天几千几万笔,肉眼对账就是天方夜谭。

对账的本质,一句话:把自己系统的账,和通道那边的账,逐笔核对,找出差异。所谓"账实相符","账"是你平台记的,"实"是通道实际发生的,两边必须一样,不一样的地方就是差异,就是资损或错账。

所以对账表的正确设计,不是把每天的交易明细再存一遍(那是冗余),而是只存"对不上的差异"。对账跑完,绝大多数笔数是能自动对平的,真正需要人工介入的是那少数差异。对账表存的就是这些差异,字段大致是:

  • recon_batch_no(对账批次号):哪一天、哪一轮的对账。
  • channel_no(对账的通道):对账是按通道逐一对的。
  • payment_no / transaction_id(平台流水号 / 第三方流水号):差异涉及的流水。
  • diff_type(差异类型):这是对账表的核心。常见三类:"平台有、通道无"(我记了这笔,通道那边没这笔,可能是回调没到或通道漏单);"通道有、平台无"(通道扣了钱,我这边没收到通知,这是最危险的资损);"两边都有但金额不一致"(手续费、优惠、汇率导致的差异)。
  • diff_amount(差异金额):差了多少。
  • handle_status(处理状态):未处理、已处理、已忽略。

这三类差异里,"通道有、平台无"是最要命的,因为它意味着钱被通道扣走了,但你的系统里压根没有这笔记录,用户可能已经付了钱却收不到货,最后反馈、退款、资损一条龙。所以对账一定要做,而且要每天定时跑、跑完出差异报告、差异要有人工跟进闭环。对账这件事不是"锦上添花",它是支付系统的"安全带"。

几个能救命的设计细节,一次说清

前面四张表加一张对账表讲完了,最后把几个散落的、但极其关键的细节集中串一遍,这些都是真实系统里踩出来的坑:

金额一律用整数分,BIGINT 存。这条我再强调一遍,因为它是所有支付 bug 里最不该犯、却最常犯的。元转分、分转元只在展示层做,存储层永远只碰整数。

唯一索引是幂等的物理保障。out_trade_no 在商户维度唯一、transaction_id 全局唯一、payment_no 全局唯一。这三把锁架上,重复下单、重复入账、重复回调全被挡在数据库层,比你在代码里写一堆 if 判断可靠一百倍。

状态变更要留痕。订单和流水的状态,关键节点要能追溯。别只留一个最终状态,出了问题你还原不了过程,排查就是两眼一抹黑。

敏感字段要隔离和脱敏。商户密钥、通道密钥、证书,这些要么单独表存、要么加密存,日志里绝不能明文打印。密钥泄露一次,前面所有设计都白费。

把这四张表加一张对账表的关系理顺、把金额、幂等、状态留痕、敏感字段这几条铁律守住,你的聚合支付数据库就有了一个能扛住真实流量的地基。剩下的接口对接、签名验签、回调处理,都是在这张地基上盖房子,地基不歪,房子才盖得起来。