# 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/)