代码 微信支付分账:漏传 profit_sharing 就永远分不了,unfreeze_unsplit 传了钱还冻着

2026-10-28 09:01:26

微信支付分账:漏传 profit_sharing 就永远分不了,unfreeze_unsplit 传了钱还冻着

平台型业务都会遇到同一件事:用户付款后,钱要拆给供应商、分销、服务商。有人第一反应是自己建一张转账表,收款后再提现转出去。微信支付的分账(profitsharing)就是为这件事做的,但它的默认值很容易让人在资金上吃亏。

前提与不适用

适用 APIv3 直联商户、服务商模式下的普通分账,以及电商收付通的分账。商户号要先开通分账产品权限,否则直接 403 NO_AUTH。

接收方是个人的劳务报酬结算、跨境结算走别的产品。分账也不能给未实名账户打钱,接收方实名状态由微信侧校验,报 RECEIVER_REAL_NAME_NOT_VERIFIED。

一、下单没传 profit_sharing,事后补不了

分账的开关在下单那一刻就要打开。JSAPI、Native、APP、H5、小程序、付款码这些下单接口里都有一个 settle_info.profit_sharing(或旧字段 profit_sharing),默认 false。

没传的下场:支付成功后调 POST /v3/profitsharing/orders,返回「非分账订单不支持分账」。这个错误没有补救接口,只能退款重下。订单金额不大时很多人选择人工转账,量上来就是事故。

接入时把这一项当必填处理,即使当期没有分账需求。传 false 与不传一样,都不冻结资金。

二、能分的不是订单金额,是扣手续费后的净额

分账金额单位是分,整数,只支持按金额分,不支持按比例。上限有两层:

  • 订单层级:不能超过订单支付金额减去手续费后的净额。100.00 元的订单、费率 0.6%,可分的只有 99.40 元,按 100 元去分必然失败。
  • 比例层级:还有最大分账比例,默认 30%,服务商与电商可在商户平台调高。超了返回 403 RULE_LIMIT。

两者取小。本地算分配方案时先用「订单金额 - 手续费」做基数再乘比例,两条都过一遍,不要只写一条。

另外,一次最多分给 50 个接收方,同一订单最多发起 50 次分账请求。

三、30 天冻结期,过期就分不出去

传了 profit_sharing=true 的订单,资金进入不可用余额,冻结给分账用。普通商户冻结期 30 天,电商收付通默认 180 天。到期没发起分账指令,资金自动解冻给分账方,订单进入完结状态,之后再调分账接口返回「分账金额不足」。

这条对定时任务的设计有直接影响:分账不要挂在「用户确认收货 + 7 天」这类远端点,超过 30 天窗口就没救了。电商模式的 180 天宽松些,但同样要监控。

一个常见误解:冻结中的资金既不是可用余额,也不能拿去给别的订单退款。它只在本单退款时可用。

四、unfreeze_unsplit=true 传了,剩余资金仍然冻结

正常的收尾动作是「完结分账」:POST /v3/profitsharing/orders/unfreeze,把不需要分的剩余金额解冻给分账方。两种做法:

  1. 最后一次请求分账时带 unfreeze_unsplit: true,让平台在处理这次分账的同时解冻剩余;
  2. 分账完成后隔一分钟左右,单独调解冻接口。

第二种做法的参数有个坑:解冻接口的 out_order_no 是商户分账单号,而这个单号在同一订单内唯一。拿上一次请求分账用过的 out_order_no 去调解冻,会返回「订单已完结分账,不支持分账」或者语义类似的拒绝。解决方式是给解冻单独生成一个新的分账单号再调一次,别复用分账那笔的号。

开放社区里还有一类反馈:明明在请求分账时传了 unfreeze_unsplit: true,剩余金额还是冻着。遇到这种先按上面换号解冻处理,再看返回的 state 与 receivers——解冻也是异步动作,不是同步把钱划走。

还有一个约束:unfreeze_unsplit 为 true 时,本次的接收方不能是分账出资方自己,否则报「分账接收方不允许为分账出资方」。这时正确的做法就是直接调解冻接口。

解冻接口报 403 NOT_ENOUGH,描述是「分账金额为0」,意思通常是分账已经完成、没有剩余可解冻,不需要重试。

五、FINISHED 不代表分账成功

查询分账结果(GET /v3/profitsharing/orders/{out_order_no}?transaction_id=...)返回两层状态:

  • state:PROCESSING 处理中、FINISHED 处理完成。FINISHED 只表示动账动作执行完毕。
  • receivers[].result:PENDING 待分账、SUCCESS 分账成功、CLOSED 已关闭。SUCCESS 才是钱到了。

只判断 state == FINISHED 就置「分账完成」,等于把失败单也标成功。CLOSED 时要看 fail_reason:

  • ACCOUNT_ABNORMAL 接收账户异常
  • NO_RELATION 分账关系已解除
  • RECEIVER_HIGH_RISK 高风险接收方
  • RECEIVER_REAL_NAME_NOT_VERIFIED 接收方未实名
  • NO_AUTH 分账权限已解除
  • RECEIVER_RECEIPT_LIMIT 超出用户月收款限额
  • PAYER_ACCOUNT_ABNORMAL 分出方账户异常

因为某个接收方账户不可用导致的分账失败,那部分资金平台会直接转回分账方,不会悬空。接收方维度的失败要落到本地明细表里,才有依据重分或改走其他结算方式。

六、回退:个人的钱回不来,商户的钱要对方同意

订单退款的出资规则已有专文写过,这里只说分账回退这条支线。

POST /v3/profitsharing/return-orders 的前提很多,缺一条就失败:

  • 只能针对 MERCHANT_ID 类型的接收方,且原分账单 state=FINISHED、该接收方 result=SUCCESS。分给 PERSONAL_OPENID 的钱不支持回退,一分都拿不回来。
  • 接收方要在商户平台「交易中心-分账-分账接收设置」里开启同意分账回退。没开,接口报 403 NO_AUTH(回退方未开通分账回退功能)。
  • 从订单创建时间算起 180 天内。
  • return_mchid 必须填原分账接收方的商户号;return_account 与 mch_id 相同会被拒,因为不需要回退。
  • amount 不能超过原分账单分给该接收方的金额;同一分账单累计也不能超过,最多发起 50 次回退。
  • out_return_no 是新的回退单号;out_order_no 必须是请求分账时的那个商户分账单号,填回退单号会报「分账指令不存在」。

回退结果同步返回,三态:PROCESSING、SUCCESS、FAILED。

  • PROCESSING:不要换 out_return_no,一分钟后用原参数重试。回退单 5 天没成功会被置为失败。
  • FAILED 且 fail_reason=BALANCE_NOT_ENOUGH:回退商户号可用余额不足,充值后原单重试。
  • TIME_OUT_CLOSED:终态,换一个新的回退单重新发起。

回退的钱回到分账方的可用余额,不是直接退给用户。退款和分账回退没有强耦合,可以先回退再退款,也可以不发起回退,由分账方自己承担。这个顺序差异在财务账上必须记清楚,否则对账时会出现「已退款但钱没回来」。

七、本地怎么建表

分账不要复用订单状态。订单是 PAID、REFUNDED,分账是另一套:

CREATE TABLE profit_share (
  id            BIGINT PRIMARY KEY,
  order_no      VARCHAR(64) NOT NULL,
  transaction_id VARCHAR(64) NOT NULL,
  out_order_no  VARCHAR(64) NOT NULL,   -- 商户分账单号,一单可多笔
  kind          TINYINT NOT NULL,       -- 1 分账 2 解冻 3 回退
  state         VARCHAR(16) NOT NULL,   -- PROCESSING / FINISHED
  amount        INT NOT NULL,           -- 分
  attempt       INT NOT NULL DEFAULT 0,
  created_at    DATETIME NOT NULL,
  UNIQUE KEY uk_out_order_no (out_order_no)
);

CREATE TABLE profit_share_receiver (
  id           BIGINT PRIMARY KEY,
  out_order_no VARCHAR(64) NOT NULL,
  type         VARCHAR(16) NOT NULL,    -- MERCHANT_ID / PERSONAL_OPENID
  account      VARCHAR(64) NOT NULL,
  amount       INT NOT NULL,
  result       VARCHAR(16) NOT NULL,    -- PENDING / SUCCESS / CLOSED
  fail_reason  VARCHAR(64) NULL
);

幂等靠 out_order_no:同一分账单号多次请求等同一次,这是唯一可以放心重试的键。接口超时或 429 FREQUENCY_LIMITED 时,用原单号原参数重试,不要换号;换号等于新的一笔分账。

请求分账的 Go 片段:

type receiver struct {
    Type        string `json:"type"`
    Account     string `json:"account"`
    Amount      int64  `json:"amount"`
    Description string `json:"description"`
}

body := map[string]any{
    "appid":          appID,
    "transaction_id": txID,
    "out_order_no":   shareNo,      // 本地唯一,重试不换
    "receivers":      recvs,        // 单次不超过 50 个
    "unfreeze_unsplit": false,      // 还要继续分就走 false
}
// POST /v3/profitsharing/orders
// 应答只代表受理,落库 state=PROCESSING,再查 /v3/profitsharing/orders/{out_order_no}

限频口径:请求分账 300QPS,查询分账结果 500QPS,请求分账回退 100QPS,查询回退结果 500QPS。批量脚本并发不要按「一两千 QPS」估,超了就是 FREQUENCY_LIMITED。

排查清单

  1. 报「非分账订单不支持分账」→ 查下单时的 profit_sharing,没有就只能退款重下。
  2. 报「分账金额不足」→ 依次排:是否已全额退款、是否已过 30/180 天冻结期、是否分账总额超过净额(订单金额 - 手续费)、是否超过最大分账比例。
  3. 分账已 FINISHED 但业务说没收到 → 看 receivers[].result 与 fail_reason,别只看 state。
  4. 解冻报「订单已完结分账,不支持分账」→ 换一个新的 out_order_no 再调解冻。
  5. 解冻报 NOT_ENOUGH(分账金额为0)→ 大概率已解冻完,不用重试。
  6. 回退报「分账指令不存在」→ 检查 out_order_no 是否是分账单号、原分账是否 SUCCESS、是否超 180 天。
  7. 回退 PROCESSING → 原单号重试;FAILED + BALANCE_NOT_ENOUGH → 让回退商户号充值后原单重试。
  8. 分账给个人的钱想要回来 → 没有接口,只能线下协商,接入前就要在合同里写清楚。

口径说明

文中 30 天 / 180 天冻结期、50 个接收方、50 次分账、50 次回退、180 天回退时效、默认 30% 比例、各处 QPS、错误码与 fail_reason 枚举,均来自微信支付商户文档与官方 FAQ,未在真实商户号上逐项实测;以接入时对应版本文档为准。手续费按费率举例,实际以商户签约费率为准。

官方文档

推荐文章

程序员茄子在线接单