如何在 Codeigniter 中集成 Barclaycard 定期付款?
结论
可以实现,但 CodeIgniter 没有专门用于 Barclaycard 定期付款的组件。通常的做法是通过商户已开通的 Barclaycard 支付接口完成首次付款和持卡人授权,保存 Barclaycard 返回的支付令牌或协议标识,再由后端定时任务发起续费。
不要在数据库中保存卡号或 CVV/CVC,也不要让 CodeIgniter 直接接收完整的卡片信息。Barclaycard 有 Barclaycard Payments、Smartpay、Smartpay Fuse 等不同产品,其 URL、字段名、签名方式和 recurring 标记并不通用。实际开发时,必须以商户后台对应产品的官方文档为准。
推荐流程
一次完整的定期付款通常包括以下步骤:
- 用户选择订阅方案。
- 页面展示付款周期、金额和取消方式等信息,并取得用户的明确授权。
- 使用 Barclaycard 托管支付页、Hosted Fields 或官方客户端组件收集卡片信息。
- 在首次付款时创建可供后续扣款使用的令牌或支付协议。
- 服务端验证 Barclaycard 返回的结果,并将令牌、客户编号和订阅状态保存到数据库。
- 付款到期后,由队列或 cron 任务从服务端发起 merchant-initiated recurring payment。
- 通过 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 提供的测试卡和测试场景完成端到端验证。
备注:内容仅供参考。
