19 KiB
19 KiB
WooCommerce Gateway Moneris 令牌化与自动续费实现参考
背景意义
为什么需要令牌化
- 安全性:避免存储敏感的信用卡信息
- 合规性:符合 PCI DSS 标准要求
- 用户体验:支持一键支付和自动续费
- 业务价值:提高订阅业务的续费成功率
为什么需要自动续费
- 订阅业务:WooCommerce Subscriptions 的核心功能
- 减少流失:避免手动续费导致的客户流失
- 现金流:提供可预测的收入流
- 运营效率:减少人工干预和客服成本
核心概念定义
令牌化(Tokenization)
将敏感的支付信息(如信用卡号)替换为安全的令牌(Token),用于后续的支付操作。
自动续费(Auto Renewal)
基于已保存的支付令牌,在订阅到期时自动处理续费支付。
实现路径对比
| 实现方式 | 适用场景 | 安全级别 | 实现复杂度 | 用户体验 |
|---|---|---|---|---|
| Checkout 令牌化 | 标准电商支付 | 高 | 低 | 优秀 |
| 直接 API 令牌化 | 自定义支付流程 | 高 | 中 | 良好 |
| 临时转永久令牌 | 特殊业务需求 | 高 | 高 | 一般 |
使用流程
1. Checkout 令牌化流程
步骤说明
- 用户在结账页面输入支付信息
- 前端调用 Moneris Checkout API
- 获取临时令牌
- 后端将临时令牌转换为永久令牌
- 保存令牌到用户账户
核心代码示例
/**
* Checkout 令牌化处理类
* 负责处理 Moneris Checkout 的令牌化流程
*/
class WC_Gateway_Moneris_Checkout_Credit_Card extends WC_Gateway_Moneris_Credit_Card {
/**
* 处理支付并创建令牌
* @param int $order_id 订单ID
* @return array 支付结果
*/
public function process_payment($order_id) {
$order = wc_get_order($order_id);
// 获取 Checkout 响应中的临时令牌
$temp_token = $this->get_checkout_temp_token();
if ($temp_token && $this->should_tokenize_payment_method()) {
// 将临时令牌转换为永久令牌
$permanent_token = $this->create_permanent_token($temp_token, $order);
if ($permanent_token) {
// 保存令牌到用户账户
$this->get_payment_tokens_handler()->add_token($order->get_user_id(), $permanent_token);
}
}
return $this->process_standard_payment($order);
}
/**
* 创建永久令牌
* @param string $temp_token 临时令牌
* @param WC_Order $order 订单对象
* @return WC_Gateway_Moneris_Payment_Token|null
*/
protected function create_permanent_token($temp_token, $order) {
try {
// 调用 Moneris API 创建永久令牌
$request = $this->get_api()->get_add_token_request($order);
$request->set_temp_token($temp_token);
$response = $this->get_api()->add_token($request);
if ($response && $response->transaction_approved()) {
// 创建令牌对象
return new WC_Gateway_Moneris_Payment_Token(
$response->get_payment_token_id(),
array(
'type' => 'credit_card',
'last4' => $response->get_masked_pan(),
'exp_month' => $response->get_exp_month(),
'exp_year' => $response->get_exp_year(),
'card_type' => $response->get_card_type(),
)
);
}
} catch (Exception $e) {
$this->add_debug_message('令牌创建失败: ' . $e->getMessage());
}
return null;
}
}
2. 直接 API 令牌化流程
/**
* 支付令牌处理器
* 负责令牌的创建、验证和管理
*/
class WC_Gateway_Moneris_Payment_Tokens_Handler {
/**
* 创建支付令牌
* @param WC_Order $order 订单对象
* @param array $payment_data 支付数据
* @return WC_Gateway_Moneris_Payment_Token|null
*/
public function create_token($order, $payment_data) {
// 检查是否应该进行令牌化
if (!$this->should_tokenize($order, $payment_data)) {
return null;
}
try {
// 构建令牌化请求
$request = $this->build_tokenize_request($order, $payment_data);
// 调用 Moneris API
$response = $this->get_api()->tokenize_payment_method($request);
if ($response && $response->transaction_approved()) {
// 创建并保存令牌
$token = $this->build_token_from_response($response);
$this->add_token($order->get_user_id(), $token);
return $token;
}
} catch (Exception $e) {
$this->log_error('令牌创建失败', $e);
}
return null;
}
/**
* 判断是否应该进行令牌化
* @param WC_Order $order 订单对象
* @param array $payment_data 支付数据
* @return bool
*/
protected function should_tokenize($order, $payment_data) {
// 用户选择保存支付方式
if (!empty($payment_data['save_payment_method'])) {
return true;
}
// 订阅订单自动令牌化
if (function_exists('wcs_order_contains_subscription') &&
wcs_order_contains_subscription($order)) {
return true;
}
// 预授权订单
if ($this->is_pre_order($order)) {
return true;
}
return false;
}
/**
* 从 API 响应构建令牌对象
* @param WC_Moneris_API_Response $response API 响应
* @return WC_Gateway_Moneris_Payment_Token
*/
protected function build_token_from_response($response) {
return new WC_Gateway_Moneris_Payment_Token(
$response->get_payment_token_id(),
array(
'type' => 'credit_card',
'last4' => $response->get_masked_pan(),
'exp_month' => $response->get_exp_month(),
'exp_year' => $response->get_exp_year(),
'card_type' => $response->get_card_type(),
'gateway_id' => $this->get_gateway()->get_id(),
'user_id' => get_current_user_id(),
)
);
}
}
3. WooCommerce Subscriptions 集成
/**
* Moneris 订阅集成类
* 处理与 WooCommerce Subscriptions 的集成
*/
class WC_Moneris_Payment_Gateway_Integration_Subscriptions {
/**
* 处理订阅续费
* @param float $amount 续费金额
* @param WC_Order $renewal_order 续费订单
* @return void
*/
public function process_subscription_payment($amount, $renewal_order) {
try {
// 获取父订单的支付令牌
$subscription = wcs_get_subscription($renewal_order->get_meta('_subscription_renewal'));
$parent_order = $subscription->get_parent();
$token = $this->get_order_token($parent_order);
if (!$token) {
throw new Exception('未找到有效的支付令牌');
}
// 使用令牌处理续费支付
$result = $this->process_token_payment($renewal_order, $token, $amount);
if ($result['success']) {
// 续费成功
$renewal_order->payment_complete($result['transaction_id']);
$renewal_order->add_order_note(
sprintf('Moneris 自动续费成功 - 交易ID: %s', $result['transaction_id'])
);
} else {
// 续费失败
$renewal_order->update_status('failed', $result['message']);
}
} catch (Exception $e) {
$renewal_order->update_status('failed', '自动续费处理失败: ' . $e->getMessage());
}
}
/**
* 使用令牌处理支付
* @param WC_Order $order 订单对象
* @param WC_Gateway_Moneris_Payment_Token $token 支付令牌
* @param float $amount 支付金额
* @return array 支付结果
*/
protected function process_token_payment($order, $token, $amount) {
// 构建支付请求
$request = $this->build_payment_request($order, $token, $amount);
// 设置 Credential on File (CoF) 参数
$request->set_cof_info(array(
'payment_indicator' => 'R', // R = Recurring (订阅续费)
'payment_information' => '2', // 2 = Subsequent payment
'issuer_id' => $token->get_issuer_id(), // 从初始交易获取
));
// 调用支付 API
$response = $this->get_api()->credit_card_purchase($request);
return array(
'success' => $response && $response->transaction_approved(),
'transaction_id' => $response ? $response->get_transaction_id() : null,
'message' => $response ? $response->get_message() : '支付处理失败',
);
}
/**
* 获取订单关联的支付令牌
* @param WC_Order $order 订单对象
* @return WC_Gateway_Moneris_Payment_Token|null
*/
protected function get_order_token($order) {
$token_id = $order->get_meta('_payment_token_id');
if ($token_id) {
$tokens = WC_Payment_Tokens::get_customer_tokens($order->get_user_id());
foreach ($tokens as $token) {
if ($token->get_token() === $token_id &&
$token instanceof WC_Gateway_Moneris_Payment_Token) {
return $token;
}
}
}
return null;
}
}
4. 令牌对象结构
/**
* Moneris 支付令牌类
* 继承自 WooCommerce 的支付令牌基类
*/
class WC_Gateway_Moneris_Payment_Token extends WC_Payment_Token_CC {
/** @var string 令牌类型 */
protected $type = 'moneris_cc';
/**
* 获取令牌的额外数据
* @return array
*/
protected function get_hook_prefix() {
return 'woocommerce_payment_token_moneris_cc_get_';
}
/**
* 验证令牌数据
* @return bool
*/
public function validate() {
if (false === parent::validate()) {
return false;
}
// 验证 Moneris 特定字段
if (!$this->get_token()) {
return false;
}
return true;
}
/**
* 获取 Moneris 令牌ID
* @return string
*/
public function get_moneris_token_id() {
return $this->get_meta('moneris_token_id');
}
/**
* 设置 Moneris 令牌ID
* @param string $token_id
*/
public function set_moneris_token_id($token_id) {
$this->add_meta_data('moneris_token_id', $token_id, true);
}
/**
* 获取发卡行ID(用于 CoF)
* @return string
*/
public function get_issuer_id() {
return $this->get_meta('issuer_id');
}
/**
* 设置发卡行ID
* @param string $issuer_id
*/
public function set_issuer_id($issuer_id) {
$this->add_meta_data('issuer_id', $issuer_id, true);
}
}
端到端流程示例
订阅创建到自动续费完整流程
/**
* 完整的订阅支付流程示例
*/
class Moneris_Subscription_Flow_Example {
/**
* 步骤1: 用户创建订阅订单
*/
public function create_subscription_order() {
// 1. 用户在前端选择订阅产品并结账
// 2. 系统检测到订阅产品,自动启用令牌化
// 3. 调用 process_payment 处理初始支付
$order_id = 12345;
$order = wc_get_order($order_id);
// 检查是否为订阅订单
if (wcs_order_contains_subscription($order)) {
// 强制启用令牌化
add_filter('wc_moneris_tokenize_payment_method', '__return_true');
}
return $this->gateway->process_payment($order_id);
}
/**
* 步骤2: 处理初始支付并保存令牌
*/
public function process_initial_payment($order) {
// 1. 处理支付
$payment_result = $this->process_credit_card_payment($order);
if ($payment_result['success']) {
// 2. 创建并保存令牌
$token = $this->create_payment_token($order, $payment_result['response']);
// 3. 将令牌关联到订阅
$subscriptions = wcs_get_subscriptions_for_order($order);
foreach ($subscriptions as $subscription) {
$subscription->update_meta_data('_payment_token_id', $token->get_id());
$subscription->save();
}
}
return $payment_result;
}
/**
* 步骤3: 自动续费处理(由 WooCommerce Subscriptions 触发)
*/
public function process_renewal_payment($amount, $renewal_order) {
// 1. 获取原始订阅和令牌
$subscription = wcs_get_subscription($renewal_order->get_meta('_subscription_renewal'));
$token_id = $subscription->get_meta('_payment_token_id');
$token = WC_Payment_Tokens::get($token_id);
if (!$token) {
$renewal_order->update_status('failed', '未找到有效的支付令牌');
return;
}
// 2. 使用令牌处理续费
try {
$request = $this->build_renewal_request($renewal_order, $token, $amount);
$response = $this->get_api()->credit_card_purchase($request);
if ($response && $response->transaction_approved()) {
// 续费成功
$renewal_order->payment_complete($response->get_transaction_id());
$renewal_order->add_order_note(
sprintf('自动续费成功 - 令牌: %s, 交易ID: %s',
$token->get_token(),
$response->get_transaction_id()
)
);
} else {
// 续费失败
$error_msg = $response ? $response->get_message() : '支付处理失败';
$renewal_order->update_status('failed', $error_msg);
}
} catch (Exception $e) {
$renewal_order->update_status('failed', '续费异常: ' . $e->getMessage());
}
}
/**
* 构建续费支付请求
*/
protected function build_renewal_request($order, $token, $amount) {
$request = new WC_Moneris_API_Credit_Card_Purchase_Request();
// 基本支付信息
$request->set_order_id($order->get_order_number());
$request->set_amount($amount);
$request->set_payment_token($token->get_token());
// CoF (Credential on File) 信息 - 用于订阅续费
$request->set_cof_info(array(
'payment_indicator' => 'R', // R = Recurring
'payment_information' => '2', // 2 = Subsequent payment
'issuer_id' => $token->get_issuer_id(),
));
// 客户信息
$request->set_customer_info(array(
'email' => $order->get_billing_email(),
'phone' => $order->get_billing_phone(),
));
return $request;
}
}
关键配置与注意事项
1. 网关配置要求
| 配置项 | 说明 | 必需性 |
|---|---|---|
| Store ID | Moneris 商户标识 | 必需 |
| API Token | API 访问令牌 | 必需 |
| 令牌化支持 | 启用令牌化功能 | 订阅必需 |
| CoF 支持 | Credential on File 支持 | 续费推荐 |
| CSC 验证 | 安全码验证设置 | 可选 |
2. 订阅集成配置
/**
* 订阅功能集成配置
*/
public function configure_subscription_support() {
// 声明支持的功能
$this->supports = array(
'products',
'subscriptions',
'subscription_cancellation',
'subscription_suspension',
'subscription_reactivation',
'subscription_amount_changes',
'subscription_date_changes',
'multiple_subscriptions',
'tokenization',
);
// 注册订阅相关的钩子
add_action('woocommerce_scheduled_subscription_payment_' . $this->id,
array($this, 'process_subscription_payment'), 10, 2);
add_action('wcs_resubscribe_order_created',
array($this, 'delete_resubscribe_meta'), 10);
add_filter('wcs_renewal_order_meta_query',
array($this, 'remove_renewal_order_meta'), 10);
}
3. 错误处理与重试机制
/**
* 续费失败处理
*/
public function handle_renewal_failure($renewal_order, $error_message) {
// 记录失败原因
$renewal_order->add_order_note('自动续费失败: ' . $error_message);
// 根据错误类型决定处理策略
if ($this->is_temporary_error($error_message)) {
// 临时错误:标记为待重试
$renewal_order->update_meta_data('_renewal_retry_count',
$renewal_order->get_meta('_renewal_retry_count') + 1);
// 安排重试(如果未超过最大重试次数)
if ($renewal_order->get_meta('_renewal_retry_count') < 3) {
wp_schedule_single_event(
time() + (24 * 60 * 60), // 24小时后重试
'moneris_retry_renewal_payment',
array($renewal_order->get_id())
);
}
} else {
// 永久错误:通知客户更新支付方式
$this->send_payment_method_update_notice($renewal_order);
}
}
限制与注意事项
技术限制
- CSC 要求:如果启用 CSC 验证,可能影响令牌化支付
- 令牌有效期:需要处理令牌过期和更新
- 网络依赖:依赖 Moneris API 的可用性
合规要求
- PCI DSS:必须遵循 PCI 合规标准
- 数据保护:不得存储敏感的卡片信息
- 用户同意:令牌化需要用户明确同意
最佳实践
- 错误处理:实现完善的错误处理和重试机制
- 日志记录:记录关键操作用于调试和审计
- 测试环境:充分测试令牌化和续费流程
- 监控告警:监控续费成功率和失败原因