PAYATHON 2026

Payment system:如何用 PHP 在用户与管理员间分账

支付老李

结论

PHP 可以负责业务编排,但不能只凭系统中保存的信用卡或借记卡信息,直接把广告费分别“转入”用户卡和管理员卡。此类业务应接入支持平台收款和分账的支付服务商,例如 Stripe Connect、Adyen for Platforms,或所在地区具备相应资质的支付机构。

支付流程可以拆成三部分:

  1. 广告主授权平台扣款。
  2. 平台在支付服务商内部完成分账,或先计入内部账本,再定期结算。
  3. 支付服务商把用户收入提现到已验证的银行账户或受支持的借记卡。

PHP 负责调用支付 API、维护内部账本和处理 Webhook。银行卡数据、身份验证、资金托管和实际清算应由合规的支付服务商处理。

推荐的资金流程

如果一次有效广告点击对应 $10,可以按下面的方式分配:

广告主支付 $10
├── 用户收入:$5
└── 平台收入:$5

这里的 $5 + $5 通常只是业务上的毛额分配。支付手续费、退款和争议款由哪一方承担,需要另外约定。扣除手续费后,双方实际收到的金额未必都是 $5。

完整流程如下:

  1. 广告主注册并添加支付方式。
  2. 前端通过支付服务商提供的组件采集卡信息。
  3. 后端只保存 customer_id、payment_method_id 等令牌,不保存卡号或 CVV。
  4. 收款用户完成实名认证,并创建支付平台的 connected account 或 sub-merchant account。
  5. 用户点击广告后,服务端验证点击是否有效。
  6. 系统为该点击创建一条不可重复的计费事件。
  7. 从广告主授权的支付方式扣除 $10。
  8. 将 $5 记为用户收入,另外 $5 记为平台收入。
  9. 收到 Webhook 并确认支付成功后,再把账本状态改为可结算。
  10. 用户收入达到结算条件后,由支付服务商提现到其银行账户或受支持的卡。

不能根据浏览器发来的“点击成功”请求直接付款。点击事件可能被重复提交、伪造,也可能由机器人批量触发,因此必须先经过服务端去重和反作弊检查。

更适合广告计费的结算方式

技术上,每次点击都可以创建一笔 $10 的支付。但在实际系统中,更常见的方式是通过内部账本累计金额,再批量扣款和结算:

有效点击
  ↓
写入计费账本
  ↓
累计广告主消费和用户收入
  ↓
达到阈值或结算周期
  ↓
统一扣款、分账和提现

这种方式可以减少支付 API 调用、支付手续费、失败交易以及退款处理成本。

例如,每次确认有效点击后只记录:

advertiser_debit = 1000 cents
user_credit      = 500 cents
platform_credit  = 500 cents

等到每日结算,或广告主余额不足时,再统一执行资金操作。账本金额应使用最小货币单位的整数。例如,$10.00 应保存为 1000 cents,不能使用浮点数计算金额。

PHP 分账示例

下面使用 Stripe Connect 的 destination charge 模式说明基本流程。示例中的账户 ID、支付方式和异常处理仍需根据实际系统补充。

使用该流程需要满足以下条件:

  • 广告主已经以 Customer 形式保存了可用的支付方式;
  • 用户已经完成 Connected Account 的开通和验证;
  • 广告主已经同意相应的自动扣款条款;
  • 当前国家、币种和账户类型支持这种资金流。
<?php

require __DIR__ . '/vendor/autoload.php';

$stripe = new \Stripe\StripeClient($_ENV['STRIPE_SECRET_KEY']);

$advertiserCustomerId = 'cus_xxx';
$paymentMethodId = 'pm_xxx';
$userConnectedAccountId = 'acct_xxx';
$clickId = 'click_123456';

try {
    $paymentIntent = $stripe->paymentIntents->create(
        [
            'amount' => 1000,
            'currency' => 'usd',
            'customer' => $advertiserCustomerId,
            'payment_method' => $paymentMethodId,
            'confirm' => true,
            'off_session' => true,

            // 平台保留 $5
            'application_fee_amount' => 500,

            // 剩余金额转入用户的 Connected Account
            'transfer_data' => [
                'destination' => $userConnectedAccountId,
            ],

            'metadata' => [
                'click_id' => $clickId,
                'billing_type' => 'ad_click',
            ],
        ],
        [
            // 防止同一点击因重试而被重复扣款
            'idempotency_key' => 'ad-click-payment-' . $clickId,
        ]
    );

    echo $paymentIntent->id;
} catch (\Stripe\Exception\CardException $e) {
    // 卡被拒绝、需要额外验证等支付错误
    error_log($e->getMessage());
} catch (\Stripe\Exception\ApiErrorException $e) {
    // 网络错误、参数错误或支付服务异常
    error_log($e->getMessage());
}

这段代码表示:广告主支付 1000 cents,平台通过 application_fee_amount 保留 500 cents,其余资金进入用户的 Connected Account。

它不能保证双方最终净到账都恰好是 $5。处理费、跨境费、货币转换费、退款和拒付损失如何分配,取决于支付服务商的规则、账户配置及平台协议。

必须通过 Webhook 确认结果

不能只根据 PHP API 的调用返回值永久确认收入。支付可能异步处理,也可能失败、退款或发生争议。系统需要验证支付服务商的签名,并处理 Webhook:

<?php

require __DIR__ . '/vendor/autoload.php';

$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_STRIPE_SIGNATURE'] ?? '';
$webhookSecret = $_ENV['STRIPE_WEBHOOK_SECRET'];

try {
    $event = \Stripe\Webhook::constructEvent(
        $payload,
        $signature,
        $webhookSecret
    );
} catch (\UnexpectedValueException $e) {
    http_response_code(400);
    exit;
} catch (\Stripe\Exception\SignatureVerificationException $e) {
    http_response_code(400);
    exit;
}

switch ($event->type) {
    case 'payment_intent.succeeded':
        $paymentIntent = $event->data->object;

        // 根据 paymentIntent.id 或 metadata.click_id 查询本地记录。
        // 在数据库事务中将支付标记为成功。
        // 必须设置唯一约束,确保重复 Webhook 不会重复入账。
        break;

    case 'payment_intent.payment_failed':
        $paymentIntent = $event->data->object;

        // 标记扣款失败,不产生可提现收入。
        break;

    case 'charge.refunded':
    case 'charge.dispute.created':
        // 冲销收入或冻结相关结算金额。
        break;
}

http_response_code(200);

Webhook 处理程序必须具备幂等性。支付服务商可能针对同一事件发送多次通知,系统不能因此重复增加用户余额。

数据库至少需要记录什么

点击事件、支付记录和账本分录最好分开保存,不要只修改一个余额字段:

ad_clicks
- id
- advertiser_id
- user_id
- ad_id
- occurred_at
- validation_status
- billing_status

payments
- id
- click_id
- provider_payment_id
- amount
- currency
- status
- idempotency_key

ledger_entries
- id
- payment_id
- account_id
- entry_type
- amount
- currency
- status

同一个 click_id 只能生成一条计费记录,provider_payment_id 和 idempotency_key 也应设置唯一约束。仅靠前端禁用按钮或 PHP 内存变量,无法阻止并发请求造成重复扣款。

不应保存原始银行卡信息

对“系统已保存银行卡信息”这一点需要格外谨慎。

系统不应自行保存:

  • 完整卡号;
  • CVV/CVC;
  • 磁道数据;
  • 未加密的银行账户信息。

更安全的方案是使用支付服务商的托管页面或前端组件采集卡信息。系统只保存令牌和必要的展示信息,例如:

customer_id: cus_xxx
payment_method_id: pm_xxx
brand: visa
last4: 4242

CVV 通常禁止在授权后保存。即使数据经过加密,只要系统自行接触完整卡数据,就会显著扩大 PCI DSS 的合规范围。

信用卡通常用于付款,不是任意收款人的通用入账账户。用户提现应通过支付服务商支持的银行账户、借记卡或其他 payout destination 完成。

自动扣款的限制

保存支付方式并不意味着平台可以随时扣款。平台通常还需要:

  • 获得广告主对扣款金额或计费规则的明确授权;
  • 保存用户协议、授权时间和授权范围;
  • 正确设置后续的 off-session 使用场景;
  • 在适用地区处理 3-D Secure、SCA 等额外认证;
  • 如果扣款需要重新认证,应提供返回前端完成确认的流程;
  • 提前说明扣款周期、退款政策和账单描述。

因此,off_session => true 不能保证支付一定成功。支付机构或发卡行仍可能要求广告主重新验证。

支付与合规问题

PHP 本身没有特殊的支付限制。风险来自资金业务的设计,而不是使用哪种编程语言。上线前至少要确认以下事项:

  • 支付服务商是否允许广告点击奖励、广告分成或类似业务;
  • 平台是否属于 marketplace、支付促进方或其他受监管模式;
  • 用户是否必须完成 KYC/KYB、税务信息和制裁名单检查;
  • 平台是否可以代收、暂存或延迟结算第三方资金;
  • 用户获得点击收入后是否需要申报税务;
  • 未成年人、跨境支付和多币种结算是否受到限制;
  • 退款、拒付和欺诈损失由广告主、平台还是用户承担;
  • 隐私政策和用户协议是否涵盖支付数据及反欺诈处理。

具体要求会随国家、地区、支付服务商和业务模式变化,无法仅靠一份通用代码判断。选定服务商后,应由其合规团队以及当地的法律、税务专业人员进一步确认。

实施时最容易忽略的问题

  1. 点击欺诈

    系统需要过滤机器人、自点广告、重复点击、代理流量和异常设备,不能由客户端决定一次点击是否产生收入。

  2. 手续费分配

    如果广告主只支付 $10,处理费就必须由其中一方承担。如果要求用户和管理员净收各 $5,广告主实际支付的金额通常需要高于 $10,具体金额取决于费率。

  3. 退款和拒付

    已经分给用户的资金可能需要追回。用户余额应区分 pending、available 和 paid,并设置结算等待期和负余额处理规则。

  4. 金额精度

    金额应使用整数形式的最小货币单位,并配套明确的币种字段,不能使用 float:

    $totalAmount = 1000;
    $userShare = intdiv($totalAmount, 2);
    $platformShare = $totalAmount - $userShare;
  5. 密钥管理

    API Secret Key 和 Webhook Secret 应保存在环境变量或密钥管理系统中,不能提交到代码仓库,也不能发送到浏览器。

  6. 服务端可信性

    金额、收款账户和分成比例必须由服务端根据数据库配置生成,不能直接接受前端传入的金额或目标账户。

真实业务更适合采用“支付服务商的平台分账账户 + 内部双重记账账本 + Webhook 确认 + 延迟批量结算”的方案。PHP 可以负责整个流程的自动化,敏感卡数据、身份验证和资金清算则交给具备相应能力的支付机构。

备注:内容仅供参考。