589 lines
19 KiB
Markdown
589 lines
19 KiB
Markdown
# WooCommerce Gateway Moneris 令牌化与自动续费实现参考
|
||
|
||
## 背景意义
|
||
|
||
### 为什么需要令牌化
|
||
- **安全性**:避免存储敏感的信用卡信息
|
||
- **合规性**:符合 PCI DSS 标准要求
|
||
- **用户体验**:支持一键支付和自动续费
|
||
- **业务价值**:提高订阅业务的续费成功率
|
||
|
||
### 为什么需要自动续费
|
||
- **订阅业务**:WooCommerce Subscriptions 的核心功能
|
||
- **减少流失**:避免手动续费导致的客户流失
|
||
- **现金流**:提供可预测的收入流
|
||
- **运营效率**:减少人工干预和客服成本
|
||
|
||
## 核心概念定义
|
||
|
||
### 令牌化(Tokenization)
|
||
将敏感的支付信息(如信用卡号)替换为安全的令牌(Token),用于后续的支付操作。
|
||
|
||
### 自动续费(Auto Renewal)
|
||
基于已保存的支付令牌,在订阅到期时自动处理续费支付。
|
||
|
||
## 实现路径对比
|
||
|
||
| 实现方式 | 适用场景 | 安全级别 | 实现复杂度 | 用户体验 |
|
||
|---------|---------|---------|-----------|---------|
|
||
| Checkout 令牌化 | 标准电商支付 | 高 | 低 | 优秀 |
|
||
| 直接 API 令牌化 | 自定义支付流程 | 高 | 中 | 良好 |
|
||
| 临时转永久令牌 | 特殊业务需求 | 高 | 高 | 一般 |
|
||
|
||
## 使用流程
|
||
|
||
### 1. Checkout 令牌化流程
|
||
|
||
#### 步骤说明
|
||
1. 用户在结账页面输入支付信息
|
||
2. 前端调用 Moneris Checkout API
|
||
3. 获取临时令牌
|
||
4. 后端将临时令牌转换为永久令牌
|
||
5. 保存令牌到用户账户
|
||
|
||
#### 核心代码示例
|
||
|
||
```php
|
||
/**
|
||
* 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 令牌化流程
|
||
|
||
```php
|
||
/**
|
||
* 支付令牌处理器
|
||
* 负责令牌的创建、验证和管理
|
||
*/
|
||
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 集成
|
||
|
||
```php
|
||
/**
|
||
* 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. 令牌对象结构
|
||
|
||
```php
|
||
/**
|
||
* 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);
|
||
}
|
||
}
|
||
```
|
||
|
||
## 端到端流程示例
|
||
|
||
### 订阅创建到自动续费完整流程
|
||
|
||
```php
|
||
/**
|
||
* 完整的订阅支付流程示例
|
||
*/
|
||
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. 订阅集成配置
|
||
|
||
```php
|
||
/**
|
||
* 订阅功能集成配置
|
||
*/
|
||
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. 错误处理与重试机制
|
||
|
||
```php
|
||
/**
|
||
* 续费失败处理
|
||
*/
|
||
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 合规标准
|
||
- **数据保护**:不得存储敏感的卡片信息
|
||
- **用户同意**:令牌化需要用户明确同意
|
||
|
||
### 最佳实践
|
||
- **错误处理**:实现完善的错误处理和重试机制
|
||
- **日志记录**:记录关键操作用于调试和审计
|
||
- **测试环境**:充分测试令牌化和续费流程
|
||
- **监控告警**:监控续费成功率和失败原因
|
||
|
||
## 参考文档
|
||
|
||
- [Moneris API 官方文档](https://developer.moneris.com/)
|
||
- [WooCommerce Subscriptions 开发文档](https://docs.woocommerce.com/document/subscriptions/)
|
||
- [WooCommerce Payment Gateway API](https://docs.woocommerce.com/document/payment-gateway-api/)
|
||
- [PCI DSS 合规指南](https://www.pcisecuritystandards.org/) |