475 lines
22 KiB
PHP
475 lines
22 KiB
PHP
<?php
|
||
if ( ! defined( 'ABSPATH' ) ) {
|
||
exit;
|
||
}
|
||
|
||
/**
|
||
* Moneris API 占位类
|
||
* 说明:此处为示例/占位实现,便于搭建整体流程。
|
||
* 上线前请替换为实际的 Moneris Vault/Tokenization 与支付接口对接代码。
|
||
*/
|
||
class Yoone_Moneris_API implements Yoone_Moneris_API_Interface {
|
||
|
||
// 基本配置
|
||
protected $store_id;
|
||
protected $api_token;
|
||
protected $sandbox; // 'yes' or 'no'
|
||
protected $timeout = 30; // 请求超时(秒)
|
||
protected $country_code = 'CA'; // CA 或 US
|
||
protected $crypt_type = '7'; // 默认 7
|
||
protected $status_check = false;
|
||
protected $protocol = 'https';
|
||
protected $port = 443;
|
||
protected $host_override = ''; // 自定义主机(高级)
|
||
protected $path_override = ''; // 自定义路径(高级)
|
||
protected $http_method = 'POST';
|
||
|
||
/**
|
||
* 构造函数:初始化直连 Moneris 的基础与高级配置。
|
||
*
|
||
* @param string $store_id Moneris Store ID
|
||
* @param string $api_token Moneris API Token
|
||
* @param 'yes'|'no' $sandbox 是否为沙箱模式('yes' 表示沙箱)
|
||
* @param array<mixed> $config 直连配置:country_code, crypt_type, status_check, protocol, port, host, path, http_method, timeout
|
||
*/
|
||
public function __construct( $store_id, $api_token, $sandbox = 'yes', $config = array() ) {
|
||
$this->store_id = (string) $store_id;
|
||
$this->api_token = (string) $api_token;
|
||
$this->sandbox = (string) $sandbox;
|
||
|
||
// 合并配置
|
||
if ( is_array( $config ) ) {
|
||
$this->country_code = isset( $config['country_code'] ) ? (string) $config['country_code'] : $this->country_code;
|
||
$this->crypt_type = isset( $config['crypt_type'] ) ? (string) $config['crypt_type'] : $this->crypt_type;
|
||
$this->status_check = isset( $config['status_check'] ) ? (bool) $config['status_check'] : $this->status_check;
|
||
$this->protocol = isset( $config['protocol'] ) ? (string) $config['protocol'] : $this->protocol;
|
||
$this->port = isset( $config['port'] ) ? absint( $config['port'] ) : $this->port;
|
||
$this->host_override= isset( $config['host'] ) ? (string) $config['host'] : $this->host_override;
|
||
$this->path_override= isset( $config['path'] ) ? (string) $config['path'] : $this->path_override;
|
||
$this->http_method = isset( $config['http_method'] ) ? strtoupper( (string) $config['http_method'] ) : $this->http_method;
|
||
$this->timeout = isset( $config['timeout'] ) ? absint( $config['timeout'] ) : $this->timeout;
|
||
}
|
||
error_log( 'Moneris API 配置' . print_r( array(
|
||
'store_id' => $this->store_id,
|
||
'sandbox' => $this->sandbox,
|
||
'country_code' => $this->country_code,
|
||
'crypt_type' => $this->crypt_type,
|
||
'status_check' => $this->status_check,
|
||
'protocol' => $this->protocol,
|
||
'port' => $this->port,
|
||
'host_override'=> $this->host_override,
|
||
'path_override'=> $this->path_override,
|
||
'http_method' => $this->http_method,
|
||
'timeout' => $this->timeout,
|
||
) ,true));
|
||
}
|
||
|
||
/**
|
||
* 令牌化卡片(res_add_cc)。
|
||
*
|
||
* @param array{number:string,exp_month:string|int,exp_year:string|int,cvc?:string} $card 原始卡信息
|
||
* @param int|null $customer_id 预留(未使用)
|
||
* @return array{success:bool,token?:string,last4?:string,brand?:string,exp_month?:string,exp_year?:string,error?:string}
|
||
*/
|
||
public function tokenize_card( $card, $customer_id = null ) {
|
||
// 仅直连 Moneris Vault: res_add_cc
|
||
if ( empty( $this->store_id ) || empty( $this->api_token ) ) {
|
||
return array( 'success' => false, 'error' => 'Missing Moneris credentials' );
|
||
}
|
||
$payload = array(
|
||
'pan' => isset( $card['number'] ) ? preg_replace( '/\D+/', '', (string) $card['number'] ) : '',
|
||
// expdate 采用 YYMM
|
||
'expdate' => $this->format_expdate( $card ),
|
||
'cryptType'=> $this->crypt_type,
|
||
);
|
||
if ( ! empty( $card['cvc'] ) ) {
|
||
$payload['cvdInfo'] = array(
|
||
'cvdIndicator' => 1,
|
||
'cvdValue' => preg_replace( '/\D+/', '', (string) $card['cvc'] ),
|
||
);
|
||
}
|
||
$num = isset($card['number']) ? preg_replace('/\D+/', '', (string) $card['number']) : '';
|
||
$masked = $num ? str_repeat('*', max(strlen($num) - 4, 0)) . substr($num, -4) : '';
|
||
error_log('【Yoone Moneris API】tokenize_card 请求:last4=' . ($num ? substr($num, -4) : '') . ' len=' . strlen($num) . ' expdate=' . $payload['expdate'] . ' cryptType=' . $payload['cryptType']);
|
||
// 使用集中加载的交易类型常量接口
|
||
$res = $this->send_moneris_xml( Yoone_Moneris_Txn_Types::RES_ADD_CC, $payload );
|
||
error_log('$res11111111'. print_r($res, true));
|
||
if ( $res['ok'] ) {
|
||
return array(
|
||
'success' => true,
|
||
'token' => (string) ( $res['receipt']['dataKey'] ?? '' ),
|
||
'last4' => (string) ( $res['receipt']['last4'] ?? $res['receipt']['maskedPan'] ?? '' ),
|
||
'brand' => (string) ( $res['receipt']['cardType'] ?? '' ),
|
||
'exp_month' => isset( $card['exp_month'] ) ? (string) $card['exp_month'] : '',
|
||
'exp_year' => isset( $card['exp_year'] ) ? (string) $card['exp_year'] : '',
|
||
);
|
||
}
|
||
return array( 'success' => false, 'error' => (string) $res['error'] );
|
||
}
|
||
|
||
/**
|
||
* 使用令牌扣款(res_purchase_cc 或 res_preauth_cc)。
|
||
*
|
||
* @param string $token Moneris Vault 返回的 dataKey
|
||
* @param float $amount 扣款金额
|
||
* @param string $currency 货币(当前占位未使用)
|
||
* @param int $order_id 订单号(作为 orderId 传入)
|
||
* @param bool $capture 是否直接购买(true)或仅预授权(false)
|
||
* @return array{success:bool,transaction_id?:string,error?:string}
|
||
*/
|
||
public function charge_token( $token, $amount, $currency, $order_id, $capture = true ) {
|
||
// 仅直连 Moneris Vault token 扣款:res_purchase_cc 或 res_preauth_cc
|
||
if ( empty( $this->store_id ) || empty( $this->api_token ) ) {
|
||
return array( 'success' => false, 'error' => 'Missing Moneris credentials' );
|
||
}
|
||
// 为避免重复 orderId 导致主机拒绝,生成唯一订单 ID
|
||
if ( ! function_exists( 'yoone_moneris_unique_order_id' ) ) {
|
||
require_once __DIR__ . '/utils/orderid.php';
|
||
}
|
||
$unique_order_id = yoone_moneris_unique_order_id( $order_id );
|
||
// 使用集中加载的交易类型常量接口
|
||
$type = $capture ? Yoone_Moneris_Txn_Types::RES_PURCHASE_CC : Yoone_Moneris_Txn_Types::RES_PREAUTH_CC;
|
||
$payload = array(
|
||
'dataKey' => (string) $token,
|
||
'orderId' => (string) $unique_order_id,
|
||
'amount' => $this->format_amount( $amount ),
|
||
'cryptType' => $this->crypt_type,
|
||
);
|
||
error_log('【Yoone Moneris API】charge_token 请求:type=' . $type . ' token_len=' . strlen($token) . ' amount=' . $payload['amount'] . ' currency=' . $currency . ' orderId=' . $unique_order_id . ' (源Woo订单ID=' . $order_id . ')');
|
||
$res = $this->send_moneris_xml( $type, $payload );
|
||
if ( $res['ok'] ) {
|
||
return array(
|
||
'success' => true,
|
||
'transaction_id' => (string) ( $res['receipt']['txnNumber'] ?? '' ),
|
||
'order_id_used' => $unique_order_id,
|
||
'retry' => false,
|
||
);
|
||
}
|
||
// 如果出现系统错误,尝试一次自动重试(更换新的唯一 orderId)
|
||
$err_msg = isset($res['error']) ? (string)$res['error'] : '';
|
||
$is_system_error = stripos($err_msg, 'SYSTEM ERROR') !== false;
|
||
if ( $is_system_error ) {
|
||
$retry_order_id = yoone_moneris_unique_order_id( $order_id );
|
||
$payload['orderId'] = (string) $retry_order_id;
|
||
error_log('【Yoone Moneris API】检测到 SYSTEM ERROR,准备重试一次。新的 orderId=' . $retry_order_id);
|
||
// 短暂延时,避免立即重复错误
|
||
usleep(300 * 1000);
|
||
$res_retry = $this->send_moneris_xml( $type, $payload );
|
||
if ( $res_retry['ok'] ) {
|
||
return array(
|
||
'success' => true,
|
||
'transaction_id' => (string) ( $res_retry['receipt']['txnNumber'] ?? '' ),
|
||
'order_id_used' => $retry_order_id,
|
||
'retry' => true,
|
||
);
|
||
}
|
||
// 记录重试失败
|
||
error_log('【Yoone Moneris API】重试仍失败:' . (string) ($res_retry['error'] ?? 'Unknown'));
|
||
return array( 'success' => false, 'error' => (string) ($res_retry['error'] ?? $err_msg) );
|
||
}
|
||
return array( 'success' => false, 'error' => (string) $err_msg );
|
||
}
|
||
|
||
/**
|
||
* 退款(refund)。
|
||
*
|
||
* @param string $transaction_id 原交易 txnNumber
|
||
* @param float $amount 退款金额
|
||
* @return array{success:bool,error?:string}
|
||
*/
|
||
public function refund( $transaction_id, $amount ) {
|
||
// 仅直连 Moneris 退款
|
||
if ( empty( $this->store_id ) || empty( $this->api_token ) ) {
|
||
return array( 'success' => false, 'error' => 'Missing Moneris credentials' );
|
||
}
|
||
$payload = array(
|
||
'txnNumber' => (string) $transaction_id,
|
||
'amount' => $this->format_amount( $amount ),
|
||
'cryptType' => $this->crypt_type,
|
||
);
|
||
// 使用集中加载的交易类型常量接口
|
||
$res = $this->send_moneris_xml( Yoone_Moneris_Txn_Types::REFUND, $payload );
|
||
if ( $res['ok'] ) {
|
||
return array( 'success' => true );
|
||
}
|
||
return array( 'success' => false, 'error' => (string) $res['error'] );
|
||
}
|
||
|
||
/**
|
||
* 低层请求封装:POST JSON 到 moneris
|
||
*/
|
||
// 移除 moneryze 代理模式,仅保留直连实现
|
||
|
||
/**
|
||
* 直连 Moneris:构建 XML 并发送。
|
||
*
|
||
* 参考 moneryze 的实现按国家/环境选择主机与路径,POST text/xml。
|
||
* @param string $type 交易类型(如 res_add_cc、res_purchase_cc、refund)
|
||
* @param array<string,mixed> $payload 交易负载(键支持驼峰/蛇形,内部转蛇形)
|
||
* @return array{ok:bool,receipt?:array,error?:string}
|
||
*/
|
||
protected function send_moneris_xml( $type, $payload ) {
|
||
$endpoint = $this->build_endpoint( $type );
|
||
if ( ! $endpoint ) {
|
||
return array( 'ok' => false, 'error' => 'Endpoint not configured' );
|
||
}
|
||
error_log('【Yoone Moneris API】发送 XML:type=' . $type . ' endpoint=' . $endpoint);
|
||
|
||
// 构建请求体
|
||
$body = array(
|
||
'storeId' => $this->store_id,
|
||
'apiToken' => $this->api_token,
|
||
// 明确发送字符串 true/false,避免 boolean false 被序列化为空字符串
|
||
'statusCheck' => $this->status_check ? 'true' : 'false',
|
||
// 添加处理国家代码(CA/US),与端点匹配
|
||
'processingCountryCode' => strtoupper($this->country_code),
|
||
// 在沙箱环境显式声明 test=true(与官方示例一致)
|
||
'test' => $this->is_test_mode() ? 'true' : 'false',
|
||
);
|
||
|
||
// 风险查询特殊结构(暂不支持;如需支持可扩展)
|
||
$body[ $type ] = $payload;
|
||
|
||
$xml = $this->array_to_moneris_xml( array( 'request' => $body ) );
|
||
|
||
// 日志:打印部分已脱敏的 XML 以便排查格式问题(掩码 pan、data_key、api_token)
|
||
$xml_log = $xml;
|
||
$xml_log = preg_replace('/<pan>[^<]*<\/pan>/', '<pan>****</pan>', $xml_log);
|
||
$xml_log = preg_replace('/<data_key>[^<]*<\/data_key>/', '<data_key>****</data_key>', $xml_log);
|
||
$xml_log = preg_replace('/<api_token>[^<]*<\/api_token>/', '<api_token>****</api_token>', $xml_log);
|
||
error_log('【Yoone Moneris API】请求 XML(脱敏)前 400 字符:' . substr($xml_log, 0, 400));
|
||
|
||
$args = array(
|
||
'method' => $this->http_method ?: 'POST',
|
||
'timeout' => $this->timeout,
|
||
'headers' => array(
|
||
'User-Agent' => 'PHP NA - yoone-moneris/1.0.0',
|
||
'Content-Type' => 'text/xml',
|
||
),
|
||
'body' => $xml,
|
||
'data_format' => 'body',
|
||
);
|
||
|
||
// 仅支持 POST
|
||
$resp = wp_remote_post( $endpoint, $args );
|
||
if ( is_wp_error( $resp ) ) {
|
||
error_log('【Yoone Moneris API】HTTP 错误:' . $resp->get_error_message());
|
||
return array( 'ok' => false, 'error' => $resp->get_error_message() );
|
||
}
|
||
$code = wp_remote_retrieve_response_code( $resp );
|
||
$raw = wp_remote_retrieve_body( $resp );
|
||
if ( $code < 200 || $code >= 300 ) {
|
||
error_log('【Yoone Moneris API】HTTP 非 2xx:code=' . $code . ' body=' . substr($raw, 0, 200));
|
||
return array( 'ok' => false, 'error' => 'HTTP ' . $code . ' ' . $raw );
|
||
}
|
||
|
||
// 解析 XML
|
||
$parsed = $this->parse_moneris_xml( $raw );
|
||
// 打印部分响应原文,便于调试
|
||
error_log('【Yoone Moneris API】响应原文前 300 字符:' . substr($raw, 0, 300));
|
||
if ( ! $parsed['parsed'] ) {
|
||
error_log('【Yoone Moneris API】XML 解析失败:' . ($parsed['error'] ?? 'Unknown'));
|
||
return array( 'ok' => false, 'error' => 'Parse error: ' . ( $parsed['error'] ?? 'Unknown' ) );
|
||
}
|
||
$receipt = $parsed['receipt'];
|
||
$approved = $this->is_approved( $receipt );
|
||
if ( ! $approved ) {
|
||
$message = isset( $receipt['message'] ) ? $receipt['message'] : ( $receipt['Message'] ?? 'Declined' );
|
||
error_log('【Yoone Moneris API】交易未批准:ResponseCode=' . ($receipt['responseCode'] ?? $receipt['response_code'] ?? '') . ' message=' . $message);
|
||
return array( 'ok' => false, 'error' => $message );
|
||
}
|
||
error_log('【Yoone Moneris API】交易批准:ResponseCode=' . ($receipt['responseCode'] ?? $receipt['response_code'] ?? '') . ' txnNumber=' . ($receipt['txnNumber'] ?? $receipt['txn_number'] ?? ''));
|
||
error_log('receipt'. print_r($receipt,true));
|
||
return array( 'ok' => true, 'receipt' => $receipt );
|
||
}
|
||
|
||
/**
|
||
* 构建 Moneris endpoint(主机与路径)。
|
||
*
|
||
* 优先使用高级覆盖,其次依据国家/环境选择默认主机与路径;部分类型可走 MPI。
|
||
* @param string $type 交易类型
|
||
* @return string 完整 URL(含协议/主机/端口/路径)
|
||
*/
|
||
protected function build_endpoint( $type ) {
|
||
// 自定义覆盖优先
|
||
if ( $this->host_override && $this->path_override ) {
|
||
$host = $this->host_override;
|
||
$path = $this->path_override;
|
||
return $this->protocol . '://' . $host . ':' . $this->port . $path;
|
||
}
|
||
|
||
// 使用常量默认值映射
|
||
$defaults = yoone_moneris_endpoint_defaults( $this->country_code, $this->is_test_mode() );
|
||
$host = $defaults['host'];
|
||
$path = $defaults['path'];
|
||
$protocol = $defaults['protocol'];
|
||
$port = $defaults['port'];
|
||
|
||
// 3DS MPI 交易(如果未来需要)
|
||
if ( in_array( $type, array( 'acs', 'txn' ), true ) ) {
|
||
$path = YOONE_MONERIS_MPI_PATH;
|
||
}
|
||
|
||
// 自定义覆盖优先
|
||
if ( $this->host_override ) {
|
||
$host = $this->host_override;
|
||
}
|
||
if ( $this->path_override ) {
|
||
$path = $this->path_override;
|
||
}
|
||
|
||
// 使用对象上的协议/端口(允许覆盖默认)
|
||
$protocol = $this->protocol ?: $protocol;
|
||
$port = $this->port ?: $port;
|
||
|
||
return $protocol . '://' . $host . ':' . $port . $path;
|
||
}
|
||
|
||
/**
|
||
* 判断是否处于沙箱模式。
|
||
*
|
||
* @return bool true 表示沙箱环境
|
||
*/
|
||
protected function is_test_mode() {
|
||
return strtolower( (string) $this->sandbox ) === 'yes';
|
||
}
|
||
|
||
/**
|
||
* 将数组转换为 Moneris XML(统一为小写蛇形命名)。
|
||
*
|
||
* @param array<string,mixed> $data 顶层通常为 ['request' => [...]]
|
||
* @return string XML 字符串
|
||
*/
|
||
protected function array_to_moneris_xml( $data ) {
|
||
// 简单地用 DOMDocument 生成,保持节点顺序与结构
|
||
$doc = new DOMDocument( '1.0', 'UTF-8' );
|
||
$doc->formatOutput = true;
|
||
|
||
$build = function( $parent, $key, $value ) use ( &$build, $doc ) {
|
||
$key = $this->camel_to_snake( $key );
|
||
$node = $doc->createElement( $key );
|
||
if ( is_array( $value ) ) {
|
||
foreach ( $value as $k => $v ) {
|
||
$build( $node, $k, $v );
|
||
}
|
||
} else {
|
||
$node->appendChild( $doc->createTextNode( (string) $value ) );
|
||
}
|
||
$parent->appendChild( $node );
|
||
};
|
||
|
||
$root_key = key( $data );
|
||
$root_val = current( $data );
|
||
$root = $doc->createElement( $this->camel_to_snake( $root_key ) );
|
||
foreach ( $root_val as $k => $v ) {
|
||
$build( $root, $k, $v );
|
||
}
|
||
$doc->appendChild( $root );
|
||
return $doc->saveXML();
|
||
}
|
||
|
||
/**
|
||
* 将驼峰命名转换为蛇形命名。
|
||
*
|
||
* @param string $name 字段名(如 orderId、dataKey、cvdInfo)
|
||
* @return string 转换后的蛇形命名(小写)
|
||
*/
|
||
protected function camel_to_snake( $name ) {
|
||
$out = preg_replace( '/([a-z])([A-Z])/', '$1_$2', (string) $name );
|
||
return strtolower( $out );
|
||
}
|
||
|
||
/**
|
||
* 解析 Moneris XML 响应为数组。
|
||
*
|
||
* 期望结构:response → receipt;统一为小写蛇形键,同时保留常见原始大小写键以兼容。
|
||
* @param string $xml 响应 XML 字符串
|
||
* @return array{parsed:bool,receipt?:array,error?:string}
|
||
*/
|
||
protected function parse_moneris_xml( $xml ) {
|
||
libxml_use_internal_errors( true );
|
||
$obj = simplexml_load_string( $xml );
|
||
if ( false === $obj ) {
|
||
$err = 'Invalid XML';
|
||
$errors = libxml_get_errors();
|
||
if ( ! empty( $errors ) ) {
|
||
$err = trim( $errors[0]->message );
|
||
}
|
||
libxml_clear_errors();
|
||
return array( 'parsed' => false, 'error' => $err );
|
||
}
|
||
$json = json_decode( json_encode( $obj ), true );
|
||
// 期望结构:response -> receipt
|
||
$receipt = isset( $json['receipt'] ) ? $json['receipt'] : ( isset( $json['response']['receipt'] ) ? $json['response']['receipt'] : array() );
|
||
// 统一键名:小写蛇形
|
||
$norm = array();
|
||
foreach ( (array) $receipt as $k => $v ) {
|
||
$norm[ $this->camel_to_snake( $k ) ] = is_array( $v ) ? ( isset( $v['_text'] ) ? $v['_text'] : ( isset( $v[0] ) ? $v[0] : current( $v ) ) ) : $v;
|
||
}
|
||
// 同时保留常见字段的原始大小写以兼容(例如 txnNumber/TxnNumber)
|
||
$norm['txnNumber'] = isset( $receipt['TxnNumber'] ) ? $receipt['TxnNumber'] : ( $norm['txn_number'] ?? '' );
|
||
$norm['dataKey'] = isset( $receipt['DataKey'] ) ? $receipt['DataKey'] : ( $norm['data_key'] ?? '' );
|
||
$norm['responseCode']= isset( $receipt['ResponseCode'] ) ? $receipt['ResponseCode'] : ( $norm['response_code'] ?? '' );
|
||
$norm['message'] = isset( $receipt['Message'] ) ? $receipt['Message'] : ( $norm['message'] ?? '' );
|
||
$norm['cardType'] = isset( $receipt['CardType'] ) ? $receipt['CardType'] : ( $norm['card_type'] ?? '' );
|
||
$norm['maskedPan'] = isset( $receipt['MaskedPan'] ) ? $receipt['MaskedPan'] : ( $norm['masked_pan'] ?? '' );
|
||
return array( 'parsed' => true, 'receipt' => $norm );
|
||
}
|
||
|
||
/**
|
||
* 判断交易是否批准(ResponseCode < 50)。
|
||
*
|
||
* @param array<string,mixed> $receipt 解析后的收据数组
|
||
* @return bool 批准返回 true
|
||
*/
|
||
protected function is_approved( $receipt ) {
|
||
$code = isset( $receipt['response_code'] ) ? $receipt['response_code'] : ( $receipt['ResponseCode'] ?? ( $receipt['responseCode'] ?? '' ) );
|
||
$code = is_numeric( $code ) ? intval( $code ) : 999;
|
||
return $code >= 0 && $code < 50; // Moneris: <50 视为批准
|
||
}
|
||
|
||
/**
|
||
* 格式化有效期为 YYMM。
|
||
*
|
||
* @param array{exp_year?:string|int,exp_month?:string|int} $card 卡片有效期
|
||
* @return string 两位年+两位月的 YYMM 格式
|
||
*/
|
||
protected function format_expdate( $card ) {
|
||
$yy = '';
|
||
$mm = '';
|
||
if ( ! empty( $card['exp_year'] ) ) {
|
||
$yy = substr( preg_replace( '/\D+/', '', (string) $card['exp_year'] ), -2 );
|
||
}
|
||
if ( ! empty( $card['exp_month'] ) ) {
|
||
$mm = substr( '0' . preg_replace( '/\D+/', '', (string) $card['exp_month'] ), -2 );
|
||
}
|
||
return $yy . $mm; // YYMM
|
||
}
|
||
|
||
/**
|
||
* 格式化金额为两位小数的字符串。
|
||
*
|
||
* @param float $amount 金额
|
||
* @return string 形如 "10.00" 的字符串
|
||
*/
|
||
protected function format_amount( $amount ) {
|
||
return number_format( (float) $amount, 2, '.', '' );
|
||
}
|
||
/**
|
||
* 卡片识别
|
||
*/
|
||
/**
|
||
* 简单识别卡组织(卡品牌)。
|
||
*
|
||
* @param string $number 卡号
|
||
* @return string 可能值:visa/mastercard/amex/discover/card
|
||
*/
|
||
protected function detect_brand( $number ) {
|
||
$n = preg_replace( '/\D+/', '', $number );
|
||
if ( preg_match( '/^4[0-9]{12}(?:[0-9]{3})?$/', $n ) ) return 'visa';
|
||
if ( preg_match( '/^5[1-5][0-9]{14}$/', $n ) ) return 'mastercard';
|
||
if ( preg_match( '/^3[47][0-9]{13}$/', $n ) ) return 'amex';
|
||
if ( preg_match( '/^6(?:011|5[0-9]{2})[0-9]{12}$/', $n ) ) return 'discover';
|
||
return 'card';
|
||
}
|
||
} |