PAYATHON 2026

如何实现商户自动支付及类似分账系统的功能?

支付老李

结论

可以实现,但要先判断具体属于哪类业务:

  • 如果15个用户都是交易参与方,资金需要按订单比例分配,应使用支付宝开放平台的分账类产品。
  • 如果商户按照内部规则计算金额,再向15个用户付款,这更接近商家转账、批量付款或佣金结算,不能把普通转账当作分账处理。

系统可以自动计算每人的应付金额,再将款项转入用户的支付宝账户,但前提是满足产品签约、账户实名、资金来源和合规要求。能否直接转入”支付宝余额”、支持哪些收款账户,以及单笔和单日限额,都要以商户实际签约的产品及支付宝开放平台当期规则为准。

推荐的系统结构

支付成功后,不要立即循环调用15次转账接口。更合适的方式是分开处理金额计算、账务记录和实际付款:

  1. 接收订单支付成功通知。
  2. 验证通知签名,确认订单金额和状态。
  3. 按照分配规则计算每个用户的金额。
  4. 生成不可重复的结算单及明细。
  5. 通过异步任务逐笔提交分账或转账请求。
  6. 查询处理结果,或接收结果通知。
  7. 更新账务状态,对失败记录进行重试或转人工处理。
  8. 定期核对系统账单和支付宝账单。

这样可以减少接口超时、重复付款,以及部分付款成功后无法恢复等问题。

金额如何计算

金额应以”分”为最小单位,或者使用支持精确小数的 Decimal 类型。资金计算不能直接使用 float 或 double。

假设可分配金额为:

10000 分

15名用户按权重分配:

用户应得金额 = floor(可分配金额 × 用户权重 / 总权重)

向下取整后可能会有余数。余数需要按照固定规则补齐。例如,先按用户编号排序,再依次给每人补1分,直到分配总额完全一致。

最终必须满足:

所有用户分配金额之和 + 手续费 + 商户留存金额 = 原始可结算金额

下面是使用 Java BigDecimal 计算金额的示例:

import java.math.BigDecimal;
import java.math.RoundingMode;
import java.util.ArrayList;
import java.util.List;

public class AllocationService {

    public record Receiver(String userId, BigDecimal weight) {}

    public record Allocation(String userId, long amountCent) {}

    public static List<Allocation> allocate(
            long totalCent,
            List<Receiver> receivers
    ) {
        if (totalCent < 0) {
            throw new IllegalArgumentException("totalCent must not be negative");
        }
        if (receivers == null || receivers.isEmpty()) {
            throw new IllegalArgumentException("receivers must not be empty");
        }

        BigDecimal totalWeight = receivers.stream()
                .map(Receiver::weight)
                .reduce(BigDecimal.ZERO, BigDecimal::add);

        if (totalWeight.compareTo(BigDecimal.ZERO) <= 0) {
            throw new IllegalArgumentException("total weight must be positive");
        }

        List<Allocation> result = new ArrayList<>();
        long allocatedCent = 0;

        for (Receiver receiver : receivers) {
            if (receiver.weight().compareTo(BigDecimal.ZERO) < 0) {
                throw new IllegalArgumentException("weight must not be negative");
            }

            long amountCent = BigDecimal.valueOf(totalCent)
                    .multiply(receiver.weight())
                    .divide(totalWeight, 0, RoundingMode.DOWN)
                    .longValueExact();

            result.add(new Allocation(receiver.userId(), amountCent));
            allocatedCent += amountCent;
        }

        long remainder = totalCent - allocatedCent;

        for (int i = 0; i < remainder; i++) {
            Allocation current = result.get(i % result.size());
            result.set(
                    i % result.size(),
                    new Allocation(current.userId(), current.amountCent() + 1)
            );
        }

        return result;
    }
}

实际使用时还要明确用户排序规则,否则同一批数据在不同机器上运行时,余数可能分配给不同的用户。

结算数据表设计

结算数据至少需要包含结算主单和结算明细。

CREATE TABLE settlement_order (
    id BIGINT PRIMARY KEY,
    settlement_no VARCHAR(64) NOT NULL UNIQUE,
    source_order_no VARCHAR(64) NOT NULL,
    total_amount_cent BIGINT NOT NULL,
    status VARCHAR(32) NOT NULL,
    created_at TIMESTAMP NOT NULL,
    updated_at TIMESTAMP NOT NULL
);

CREATE TABLE settlement_detail (
    id BIGINT PRIMARY KEY,
    settlement_no VARCHAR(64) NOT NULL,
    receiver_id VARCHAR(128) NOT NULL,
    amount_cent BIGINT NOT NULL,
    request_no VARCHAR(64) NOT NULL UNIQUE,
    status VARCHAR(32) NOT NULL,
    provider_order_no VARCHAR(128),
    failure_code VARCHAR(64),
    failure_message VARCHAR(512),
    created_at TIMESTAMP NOT NULL,
    updated_at TIMESTAMP NOT NULL,
    UNIQUE (settlement_no, receiver_id)
);

request_no 是每笔付款的幂等号。即使任务重复执行,也要使用相同的业务单号提交,不能在每次重试时生成新号码。

自动付款流程示例

下面用内部接口名称演示流程。这些名称不代表支付宝开放平台的实际接口名称或请求参数。正式接入时,需要按照商户签约的产品替换 PaymentProvider 的实现。

public interface PaymentProvider {

    TransferResult transfer(TransferRequest request);

    TransferResult query(String requestNo);
}

public record TransferRequest(
        String requestNo,
        String receiverIdentity,
        long amountCent,
        String remark
) {}

public record TransferResult(
        String providerOrderNo,
        TransferStatus status,
        String failureCode,
        String failureMessage
) {}

public enum TransferStatus {
    PROCESSING,
    SUCCESS,
    FAILED
}

执行单条结算明细前,应先锁定记录并检查当前状态:

@Transactional
public void executeTransfer(long detailId) {
    SettlementDetail detail = repository.lockById(detailId);

    if (detail.getStatus() == TransferStatus.SUCCESS) {
        return;
    }

    TransferRequest request = new TransferRequest(
            detail.getRequestNo(),
            detail.getReceiverIdentity(),
            detail.getAmountCent(),
            "业务结算"
    );

    TransferResult result = paymentProvider.transfer(request);

    switch (result.status()) {
        case SUCCESS -> repository.markSuccess(
                detailId,
                result.providerOrderNo()
        );

        case PROCESSING -> repository.markProcessing(
                detailId,
                result.providerOrderNo()
        );

        case FAILED -> repository.markFailed(
                detailId,
                result.failureCode(),
                result.failureMessage()
        );
    }
}

网络超时不能直接判定为付款失败。支付宝可能已经受理请求,只是响应没有成功返回。遇到这种情况,应使用原业务单号查询处理结果,确认失败后再决定是否重试。

接入前需要准备什么

商户通常需要完成这些准备工作:

  1. 注册并认证支付宝开放平台应用。
  2. 申请符合业务场景的分账或转账产品。
  3. 配置应用公钥、支付宝公钥或平台证书。
  4. 妥善保存应用私钥,不要把私钥提交到代码仓库。
  5. 配置服务端异步通知地址。
  6. 收集产品要求的收款方身份信息。
  7. 明确分配协议、退款规则、手续费承担方式和税务处理方式。
  8. 在正式环境中使用小金额测试付款、失败处理、退款和对账流程。

收款时需要支付宝用户 ID、登录账号还是其他标识,取决于商户签约的产品。不要只凭手机号或页面昵称发起付款,也不要自行猜测接口参数。

分账和普通转账的差异

分账通常与原始交易绑定,支付宝可以识别资金来源、订单关系和交易参与方。这类产品适合平台在交易完成后,将资金分给服务商、门店或其他参与者。

普通转账则是商户使用自己的可用资金向用户付款,常见场景包括佣金、奖励、报销和结算。它不一定会在支付宝平台层面与某笔支付订单建立关联。

两者对资金冻结、可分金额、退款处理、手续费、收款方准入和合规材料的要求可能完全不同。如果产品选错,申请可能无法通过;即使能够上线,后续也可能无法正确处理退款和对账。

必须处理的异常情况

自动付款不能只记录”成功”和”失败”,至少要区分以下状态:

  • CREATED:已经生成结算明细;
  • PROCESSING:支付宝已经受理,结果尚未确定;
  • SUCCESS:最终付款成功;
  • FAILED:已经明确失败,可根据原因继续处理;
  • CLOSED:结算已关闭;
  • MANUAL_REVIEW:需要人工核对。

网络中断、服务繁忙等可恢复错误,可以设置有限次数的重试。账户不存在、身份信息不一致、权限不足等业务错误则不能无限重试。

如果15笔付款中有12笔成功、3笔失败,已经完成的外部转账无法回滚。系统应保留12笔成功记录,只处理失败的3笔。这也是每条结算明细都需要单独记账的原因。

安全与合规注意事项

  • 所有异步通知都要验签,同时校验商户号、应用 ID、订单号、金额和当前状态。
  • 不能根据前端显示的”支付成功”页面发起付款,应以后端查询结果或可信的异步通知为准。
  • 回调接口必须支持幂等处理,同一通知重复到达时不能重复入账。
  • 私钥应保存在密钥管理服务或受控配置中,并限制读取权限。
  • 收款账户、姓名和手机号等信息属于敏感数据,需要在日志中脱敏。
  • 不要擅自使用用户充值资金、待退款资金或受限制资金进行转账。
  • 佣金、劳务费和奖励等付款可能涉及合同、发票、个税或代扣代缴,应交由财务和法务确认。
  • 应建立每日对账机制,核对业务账、结算账、支付宝账和银行资金账。

这类功能在技术上可以实现。实施时不能简单地”循环转账15次”,而要先选择适合业务的支付宝产品,并处理好精确计算、幂等付款、异步确认、异常补偿和资金对账。

备注:内容仅供参考。