PAYATHON 2026

如何在 Codeigniter 中集成 Barclaycard 定期付款?

支付老李

结论

可以实现,但 CodeIgniter 没有专门用于 Barclaycard 定期付款的组件。通常的做法是通过商户已开通的 Barclaycard 支付接口完成首次付款和持卡人授权,保存 Barclaycard 返回的支付令牌或协议标识,再由后端定时任务发起续费。

不要在数据库中保存卡号或 CVV/CVC,也不要让 CodeIgniter 直接接收完整的卡片信息。Barclaycard 有 Barclaycard Payments、Smartpay、Smartpay Fuse 等不同产品,其 URL、字段名、签名方式和 recurring 标记并不通用。实际开发时,必须以商户后台对应产品的官方文档为准。

推荐流程

一次完整的定期付款通常包括以下步骤:

  1. 用户选择订阅方案。
  2. 页面展示付款周期、金额和取消方式等信息,并取得用户的明确授权。
  3. 使用 Barclaycard 托管支付页、Hosted Fields 或官方客户端组件收集卡片信息。
  4. 在首次付款时创建可供后续扣款使用的令牌或支付协议。
  5. 服务端验证 Barclaycard 返回的结果,并将令牌、客户编号和订阅状态保存到数据库。
  6. 付款到期后,由队列或 cron 任务从服务端发起 merchant-initiated recurring payment。
  7. 通过 webhook 或服务端查询确认最终结果,然后更新账单和订阅状态。

首次交易通常需要持卡人参与,并可能触发 3-D Secure。后续续费属于商户发起的交易,但具体标记取决于所使用的 Barclaycard 产品。

数据表设计

订阅信息和每次扣款记录至少应分开保存,不要只在用户表中增加一个“是否已付款”字段。

CREATE TABLE subscriptions (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    user_id BIGINT UNSIGNED NOT NULL,
    provider_customer_id VARCHAR(191) NULL,
    payment_token VARCHAR(512) NOT NULL,
    plan_code VARCHAR(64) NOT NULL,
    amount_minor INT UNSIGNED NOT NULL,
    currency CHAR(3) NOT NULL,
    status VARCHAR(32) NOT NULL,
    next_charge_at DATETIME NULL,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL
);

CREATE TABLE subscription_payments (
    id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    subscription_id BIGINT UNSIGNED NOT NULL,
    merchant_reference VARCHAR(128) NOT NULL UNIQUE,
    provider_payment_id VARCHAR(191) NULL,
    amount_minor INT UNSIGNED NOT NULL,
    currency CHAR(3) NOT NULL,
    status VARCHAR(32) NOT NULL,
    response_payload TEXT NULL,
    created_at DATETIME NOT NULL,
    updated_at DATETIME NOT NULL
);

amount_minor 使用最小货币单位。例如,GBP 10.00 通常写为 1000,但仍需确认当前接口对金额格式的具体要求。

支付令牌属于敏感数据,应限制数据库访问权限,并根据实际安全要求进行应用层加密。日志中也不能记录令牌、Authorization 请求头或完整的 webhook 内容。

CodeIgniter 服务类示例

下面以 CodeIgniter 4 为例说明代码结构。接口路径和请求字段都是占位符,因为不同 Barclaycard 产品采用的协议不同,不能将它们当作官方参数直接使用。

<?php

namespace App\Services;

use CodeIgniter\HTTP\CURLRequest;
use RuntimeException;

final class BarclaycardService
{
    private CURLRequest $http;
    private string $baseUrl;
    private string $apiKey;

    public function __construct()
    {
        $this->http = service('curlrequest');
        $this->baseUrl = rtrim((string) env('barclaycard.baseUrl'), '/');
        $this->apiKey = (string) env('barclaycard.apiKey');
    }

    public function chargeRecurring(array $subscription, string $reference): array
    {
        /*
         * 以下字段仅表达续费请求所需的信息。
         * 请按当前 Barclaycard 产品的官方文档替换 endpoint、认证方式和字段名。
         */
        $payload = [
            'merchantReference' => $reference,
            'amount' => [
                'value'    => $subscription['amount_minor'],
                'currency' => $subscription['currency'],
            ],
            'paymentToken' => $subscription['payment_token'],
            'recurring' => [
                'type' => 'subsequent',
            ],
        ];

        $response = $this->http->request('POST', $this->baseUrl . '/payments', [
            'headers' => [
                'Authorization' => 'Bearer ' . $this->apiKey,
                'Accept'        => 'application/json',
                'Content-Type'  => 'application/json',
                'Idempotency-Key' => $reference,
            ],
            'json'        => $payload,
            'http_errors' => false,
            'timeout'     => 30,
        ]);

        $statusCode = $response->getStatusCode();
        $body = json_decode($response->getBody(), true);

        if (!is_array($body)) {
            throw new RuntimeException(
                'Barclaycard returned an invalid JSON response.'
            );
        }

        if ($statusCode < 200 || $statusCode >= 300) {
            throw new RuntimeException(
                'Barclaycard request failed with HTTP status ' . $statusCode
            );
        }

        return $body;
    }
}

环境变量可根据实际使用的产品配置:

barclaycard.baseUrl="https://YOUR-BARCLAYCARD-ENDPOINT"
barclaycard.apiKey="YOUR_SECRET"
barclaycard.webhookSecret="YOUR_WEBHOOK_SECRET"

不要把密钥提交到 Git,也不要将其写进控制器或配置仓库。如果接口使用 HMAC、客户端证书或 HTTP Basic Authentication,应按照官方要求替换示例中的 Bearer Token。

创建首次付款

首次付款更适合通过 Barclaycard 托管页面完成。CodeIgniter 后端负责创建支付会话,然后将用户重定向到托管页面:

public function subscribe()
{
    $user = auth()->user();

    $reference = 'initial_' . bin2hex(random_bytes(12));

    // 这里调用与你所用 Barclaycard 产品对应的“创建支付会话”接口。
    $session = $this->barclaycard->createCheckoutSession([
        'merchant_reference' => $reference,
        'amount_minor'       => 1000,
        'currency'           => 'GBP',
        'return_url'         => site_url('billing/return'),
        'recurring_consent'  => true,
    ]);

    return redirect()->to($session['redirect_url']);
}

createCheckoutSession()、redirect_url 和 recurring_consent 只用于说明程序结构,不表示所有 Barclaycard 接口都会使用这些名称。实现时需要确认:

  • 如何声明当前付款是定期付款的首次交易;
  • 如何申请或取得可供后续扣款使用的 token;
  • 是否需要保存 scheme transaction ID;
  • 后续交易需要传递哪些 initial transaction reference;
  • 如何处理 3-D Secure 和 Strong Customer Authentication。

浏览器返回地址只能用于改善用户体验,不能作为付款成功的唯一依据。服务端还需要验证 webhook,或者主动查询支付状态。

定时执行续费

可以创建一个 CodeIgniter CLI 命令,再通过 cron 定时调用:

<?php

namespace App\Commands;

use App\Models\SubscriptionModel;
use App\Services\BarclaycardService;
use CodeIgniter\CLI\BaseCommand;
use CodeIgniter\CLI\CLI;

final class ChargeSubscriptions extends BaseCommand
{
    protected $group = 'Billing';
    protected $name = 'billing:charge-subscriptions';
    protected $description = 'Charges subscriptions that are due.';

    public function run(array $params)
    {
        $model = new SubscriptionModel();
        $gateway = new BarclaycardService();

        $subscriptions = $model
            ->where('status', 'active')
            ->where('next_charge_at <=', date('Y-m-d H:i:s'))
            ->findAll();

        foreach ($subscriptions as $subscription) {
            $reference = 'renewal_' . $subscription['id']
                . '_' . date('Ymd', strtotime($subscription['next_charge_at']));

            try {
                $result = $gateway->chargeRecurring(
                    $subscription,
                    $reference
                );

                /*
                 * 记录本次请求,但不要仅凭 HTTP 2xx 就认定资金已经结算。
                 * 最终状态应根据接口语义、webhook 或状态查询确定。
                 */
                CLI::write(
                    'Submitted subscription ' . $subscription['id'],
                    'green'
                );
            } catch (\Throwable $e) {
                log_message(
                    'error',
                    'Recurring charge failed for subscription {id}: {message}',
                    [
                        'id'      => $subscription['id'],
                        'message' => $e->getMessage(),
                    ]
                );
            }
        }
    }
}

cron 示例:

*/10 * * * * cd /path/to/project && php spark billing:charge-subscriptions

生产环境更适合使用队列,并为同一订阅增加数据库锁。每次续费都必须使用稳定且唯一的商户参考号或 Idempotency-Key,避免任务重跑、网络超时或并发执行造成重复扣款。

Webhook 验证

支付结果应通过经过签名验证的 webhook 更新。下面的代码只说明处理原则,签名算法必须采用对应 Barclaycard 产品规定的方式:

public function webhook()
{
    $rawBody = $this->request->getBody();
    $signature = $this->request->getHeaderLine('X-Signature');

    if (!$this->barclaycard->verifyWebhook($rawBody, $signature)) {
        return $this->response
            ->setStatusCode(401)
            ->setJSON(['error' => 'Invalid signature']);
    }

    $event = json_decode($rawBody, true);

    if (!is_array($event) || empty($event['id'])) {
        return $this->response
            ->setStatusCode(400)
            ->setJSON(['error' => 'Invalid event']);
    }

    // 先按 event ID 去重,然后在事务中更新付款和订阅状态。
    $this->processPaymentEvent($event);

    return $this->response->setJSON(['received' => true]);
}

签名验证必须使用原始请求体,并通过 hash_equals() 或官方 SDK 提供的方法进行常量时间比较。在验证签名前,不要解析、格式化或重新编码 JSON。

常见问题

能否只用 CodeIgniter Session 保存卡信息?

不能。Session 不适合保存卡号和 CVV/CVC。卡片信息应直接提交给 Barclaycard 的托管页面或官方前端组件,应用只保存服务端返回的令牌。

能否只创建一条 cron 任务,每月直接扣款?

cron 可以定时触发扣款,但系统还需要处理授权、幂等、失败重试、卡片失效、退款、取消订阅和 webhook。重复扣款失败时,应设置有限次数的重试,并通知用户更新付款方式,不能无限重试。

是否必须使用 Barclaycard 自带的订阅功能?

不一定。有些产品由支付平台管理付款周期和扣款,有些只提供 tokenization,需要商户系统自行维护扣款计划。应先确认当前商户账号支持哪种模式,再决定由 Barclaycard 还是 CodeIgniter 管理周期。

上线前检查

  • 确认商户账号已开通 recurring payment 或 Continuous Payment Authority。
  • 使用与当前 Barclaycard 产品完全匹配的官方文档和测试环境。
  • 在首次付款页面明确展示金额、币种、周期和取消规则。
  • 不存储 CVV/CVC,并尽量避免应用接触完整卡号。
  • 正确标记首次交易和后续 merchant-initiated transaction。
  • 对续费请求实施幂等控制和并发锁。
  • 验证 webhook 签名,并按事件 ID 去重。
  • 区分 authorized、captured、settled、failed 等状态。
  • 覆盖取消、退款、卡片过期、拒付和订阅到期等场景。
  • 切换到生产环境前,使用 Barclaycard 提供的测试卡和测试场景完成端到端验证。

备注:内容仅供参考。