PAYATHON 2026

Paymill subscription in PHP 为何创建两次付款?

支付阿杰

结论

重复扣款是因为代码先调用了 Services_Paymill_Transactions::create()。

创建 transaction 时会立即扣款。随后创建 subscription,Paymill 又会按照订阅规则发起首次扣款,于是同一次操作产生了两笔付款。

获取 payment_id 不需要先创建 transaction。可以直接使用前端生成的 paymillToken 创建 payment object,再将 payment ID 传给 subscription。

正确流程

  1. 前端通过 Paymill Bridge 获取一次性 paymillToken。
  2. 使用该 token 调用 Payments API,创建 payment object。
  3. 从返回结果中获取 payment ID 和 client ID。
  4. 使用 payment、client 和 offer 创建 subscription。
  5. 除非需要单独进行一次即时扣款,否则不要额外创建 transaction。

创建 payment object 只是保存后续扣款所需的支付方式,并不会创建 transaction,也不会单独扣款。

代码示例

下面继续使用问题中的旧版 Services_Paymill_* PHP 接口。SDK 版本不同,返回字段的结构也可能不同,请以项目实际安装的版本为准。

<?php

$token = $_POST['paymillToken'] ?? null;

if (!$token) {
    throw new RuntimeException('Missing paymillToken');
}

require 'Services/Paymill/Payments.php';
require 'Services/Paymill/Subscriptions.php';

$paymentsObject = new Services_Paymill_Payments(
    PAYMILL_API_KEY,
    PAYMILL_API_HOST
);

// 这里只创建支付方式,不创建 transaction,因此不会单独立即扣款。
$payment = $paymentsObject->create([
    'token' => $token,
]);

$paymentId = $payment['id'];

// 不同版本的 SDK 可能返回 client ID 字符串,也可能返回 client 对象。
$clientId = is_array($payment['client'])
    ? $payment['client']['id']
    : $payment['client'];

$subscriptionsObject = new Services_Paymill_Subscriptions(
    PAYMILL_API_KEY,
    PAYMILL_API_HOST
);

$subscription = $subscriptionsObject->create([
    'client'  => $clientId,
    'offer'   => 'offer_9cdffb501f565bf827a8',
    'payment' => $paymentId,
]);

print_r($subscription);

如果当前 SDK 要求显式创建 client,可以先创建 client,再将其 ID 传给 payment:

<?php

require 'Services/Paymill/Clients.php';
require 'Services/Paymill/Payments.php';
require 'Services/Paymill/Subscriptions.php';

$clientsObject = new Services_Paymill_Clients(
    PAYMILL_API_KEY,
    PAYMILL_API_HOST
);

$client = $clientsObject->create([
    'email'       => $userEmail,
    'description' => 'User ID# ' . $userIdMain,
]);

$paymentsObject = new Services_Paymill_Payments(
    PAYMILL_API_KEY,
    PAYMILL_API_HOST
);

$payment = $paymentsObject->create([
    'token'  => $token,
    'client' => $client['id'],
]);

$subscriptionsObject = new Services_Paymill_Subscriptions(
    PAYMILL_API_KEY,
    PAYMILL_API_HOST
);

$subscription = $subscriptionsObject->create([
    'client'  => $client['id'],
    'offer'   => 'offer_9cdffb501f565bf827a8',
    'payment' => $payment['id'],
]);

为什么原代码会重复扣款

原代码执行了两次收费操作。

第一次是创建 transaction:

$transaction = $transactionsObject->create($params);

这段代码使用 token 创建并执行了一笔即时交易,金额为 49900,币种为 SEK。

随后又创建了 subscription:

$subscription = $subscriptionsObject->create($params);

根据 offer 配置和订阅开始时间,subscription 可能立即产生首笔订阅扣款。因此,同一次请求中既有手动创建的 transaction,也有订阅的首次扣款。

$transaction['payment']['id'] 是 transaction 返回结果中包含的 payment ID,但这不代表 payment 必须通过 transaction 获得。Payments API 可以直接使用 token 创建 payment object。

注意事项

  • paymillToken 通常只能使用一次,不要重复提交。
  • amount 使用最小货币单位。49900 对应的实际金额取决于该币种的指数规则。对于 SEK,通常可理解为 499.00 SEK,但仍应以当时 Paymill 的币种处理规则为准。
  • 如果 offer 已经定义了订阅金额和周期,就不需要再通过 transaction 收取同一笔首期费用。
  • 只有在业务确实需要立即收取开通费,并另行开始周期订阅时,才应同时创建 transaction 和 subscription。此时,两笔费用必须对应不同的收费项目。
  • 提交操作应增加幂等或本地状态检查,避免用户刷新页面、重复提交表单或网络重试时创建多个 subscription。
  • Paymill 及其旧版 PHP SDK 已属于历史接口。字段结构和具体行为应以项目实际使用的 SDK 版本及当时的 API 文档为准。

备注:内容仅供参考。