mirror of
https://gitee.com/liuxioabin/fengketrade.git
synced 2026-04-21 05:47:32 +08:00
订单
This commit is contained in:
parent
b65e4d520c
commit
dd8c8b0ab0
@ -15,17 +15,17 @@ use think\Env;
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
// API基础地址 (生产环境)
|
// API基础地址 (生产环境)
|
||||||
'api_base_url' => 'https://life.ccb.com/tran/merchant/channel/api.jhtml',
|
'api_base_url' => 'https://yunbusiness.ccb.com/tp_service/txCtrl/server',
|
||||||
|
|
||||||
// 收银台地址 (生产环境)
|
// 收银台地址 (生产环境)
|
||||||
'cashier_url' => 'https://yunbusiness.ccb.com/clp_service/txCtrl',
|
'cashier_url' => 'https://yunbusiness.ccb.com/clp_service/txCtrl',
|
||||||
|
|
||||||
// 交易代码映射
|
// 交易代码映射
|
||||||
'tx_codes' => [
|
'tx_codes' => [
|
||||||
'order_push' => 'svc_occMebOrderPush', // 订单推送
|
'order_push' => 'A3341TP01', // 订单推送
|
||||||
'order_update' => 'svc_occMebOrderStatusUpdate', // 订单状态更新
|
'order_update' => 'A3341TP02', // 订单状态更新
|
||||||
'order_query' => 'svc_occPlatOrderQry', // 订单查询
|
'order_query' => 'A3341TP03', // 订单查询
|
||||||
'order_refund' => 'svc_occRefund', // 订单退款
|
'order_refund' => 'A3341TP04', // 订单退款
|
||||||
],
|
],
|
||||||
|
|
||||||
// 服务方信息(生产环境)
|
// 服务方信息(生产环境)
|
||||||
@ -37,9 +37,13 @@ return [
|
|||||||
'branch_id' => Env::get('ccb.branch_id', '340650000'),
|
'branch_id' => Env::get('ccb.branch_id', '340650000'),
|
||||||
|
|
||||||
// 密钥配置 (从.env读取,BASE64格式,不含PEM头尾)
|
// 密钥配置 (从.env读取,BASE64格式,不含PEM头尾)
|
||||||
|
// ⚠️ 注意:密钥会在代码中自动添加PEM包装,.env中只需要存储BASE64内容
|
||||||
'private_key' => Env::get('ccb.private_key', 'MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALrJmPmtQfP6mURtMxLEXqJHLldN3zYukoaRxG0lw2IdcC86H9C9brFz4YlJ+98z2mdELJaQWu8VWI4actSuPKgHTBr9MSpaii0QQpdINpwXJD9AglIrT7MxhMLYx3qAYDhjKUlC5hnWVYOg4sG32k/3dCebRHY8RDlrXUfHB2+VAgMBAAECgYArgn5R2pv8WymMmOtGudtZbb9LsuYF1v9mvVnGGv/SQQ060w1KMHYye83TjxpOueNsHqNMR0AHZS+Fmn+ZLyUNj9S77oQvUx5HQvY2/TDnsKbETzEMDybIWB+XdLsUkOrB3peVLTbk25i6oSNPOT2Fvd8TWbDqzBL9Ci27uJH72QJBAP/DfDLYoYx9OIRCykkxrDdQVFEkzhXj0wIkLa0Wnf8kP/JfBqvr0AGUPF8nEfh7fLVXYQlh5ab2FL5KvUifSL8CQQC69crW0fryyDHePp6OIVRUbw0T93h52vbGXnoQ6wdvKxZeL3MsfdNUvsJYeSxmtyY+LLgz1p3qOoEn6UpLvCirAkEA4N7qUvY+y3vJdhgXLNV8mkGJcLKQc5SUkJxogHeTQKGJi7ra7ctuXgUMM4jxduxz0CjcS1iEhxBzWn/x/mj1lwJBALgtv39VKLTXx1i7s5Ms/liXdfi/iC3zKbxOAk58WryHY+exMvMXmYMY0Xg7FySxNLl3cJeQy8ydifL5fbmSSTUCQQCj/YUbcTP8BQ6N0AgFdBwmXJyiNkB9zaDI5cEtpSCgq72m8lfn883GJ1MT7nKVXeX69/q5yDQUYiYPBXH4lCEC'),
|
'private_key' => Env::get('ccb.private_key', 'MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALrJmPmtQfP6mURtMxLEXqJHLldN3zYukoaRxG0lw2IdcC86H9C9brFz4YlJ+98z2mdELJaQWu8VWI4actSuPKgHTBr9MSpaii0QQpdINpwXJD9AglIrT7MxhMLYx3qAYDhjKUlC5hnWVYOg4sG32k/3dCebRHY8RDlrXUfHB2+VAgMBAAECgYArgn5R2pv8WymMmOtGudtZbb9LsuYF1v9mvVnGGv/SQQ060w1KMHYye83TjxpOueNsHqNMR0AHZS+Fmn+ZLyUNj9S77oQvUx5HQvY2/TDnsKbETzEMDybIWB+XdLsUkOrB3peVLTbk25i6oSNPOT2Fvd8TWbDqzBL9Ci27uJH72QJBAP/DfDLYoYx9OIRCykkxrDdQVFEkzhXj0wIkLa0Wnf8kP/JfBqvr0AGUPF8nEfh7fLVXYQlh5ab2FL5KvUifSL8CQQC69crW0fryyDHePp6OIVRUbw0T93h52vbGXnoQ6wdvKxZeL3MsfdNUvsJYeSxmtyY+LLgz1p3qOoEn6UpLvCirAkEA4N7qUvY+y3vJdhgXLNV8mkGJcLKQc5SUkJxogHeTQKGJi7ra7ctuXgUMM4jxduxz0CjcS1iEhxBzWn/x/mj1lwJBALgtv39VKLTXx1i7s5Ms/liXdfi/iC3zKbxOAk58WryHY+exMvMXmYMY0Xg7FySxNLl3cJeQy8ydifL5fbmSSTUCQQCj/YUbcTP8BQ6N0AgFdBwmXJyiNkB9zaDI5cEtpSCgq72m8lfn883GJ1MT7nKVXeX69/q5yDQUYiYPBXH4lCEC'),
|
||||||
'public_key' => Env::get('ccb.public_key', 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6yZj5rUHz+plEbTMSxF6iRy5XTd82LpKGkcRtJcNiHXAvOh/QvW6xc+GJSfvfM9pnRCyWkFrvFViOGnLUrjyoB0wa/TEqWootEEKXSDacFyQ/QIJSK0+zMYTC2Md6gGA4YylJQuYZ1lWDoOLBt9pP93Qnm0R2PEQ5a11HxwdvlQIDAQAB'),
|
'public_key' => Env::get('ccb.public_key', 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6yZj5rUHz+plEbTMSxF6iRy5XTd82LpKGkcRtJcNiHXAvOh/QvW6xc+GJSfvfM9pnRCyWkFrvFViOGnLUrjyoB0wa/TEqWootEEKXSDacFyQ/QIJSK0+zMYTC2Md6gGA4YylJQuYZ1lWDoOLBt9pP93Qnm0R2PEQ5a11HxwdvlQIDAQAB'),
|
||||||
|
|
||||||
|
// 建行平台公钥(如果建行提供则配置,否则使用merchant_public_key)
|
||||||
|
'platform_public_key' => Env::get('ccb.platform_public_key', ''),
|
||||||
|
|
||||||
// HTTP请求配置
|
// HTTP请求配置
|
||||||
'http' => [
|
'http' => [
|
||||||
'timeout' => 30, // 超时时间(秒)
|
'timeout' => 30, // 超时时间(秒)
|
||||||
@ -59,7 +63,7 @@ return [
|
|||||||
'log' => [
|
'log' => [
|
||||||
'enabled' => true,
|
'enabled' => true,
|
||||||
'level' => Env::get('ccb.log_level', 'info'), // debug, info, warning, error
|
'level' => Env::get('ccb.log_level', 'info'), // debug, info, warning, error
|
||||||
'path' => runtime_path() . 'log/ccblife/',
|
'path' => __DIR__ . '/../../../runtime/log/ccblife/',
|
||||||
],
|
],
|
||||||
|
|
||||||
// 安全配置
|
// 安全配置
|
||||||
|
|||||||
@ -424,7 +424,6 @@ class Ccbtest extends Common
|
|||||||
|
|
||||||
$checks = [
|
$checks = [
|
||||||
'基础配置' => [
|
'基础配置' => [
|
||||||
'API地址' => $config['api_base_url'] ?? '未配置',
|
|
||||||
'收银台地址' => $config['cashier_url'] ?? '未配置',
|
'收银台地址' => $config['cashier_url'] ?? '未配置',
|
||||||
'服务方编号' => $config['service_id'] ?? '未配置',
|
'服务方编号' => $config['service_id'] ?? '未配置',
|
||||||
],
|
],
|
||||||
|
|||||||
@ -8,11 +8,6 @@ namespace addons\shopro\library\ccblife;
|
|||||||
*/
|
*/
|
||||||
class CcbHttpClient
|
class CcbHttpClient
|
||||||
{
|
{
|
||||||
/**
|
|
||||||
* 建行API生产环境地址
|
|
||||||
*/
|
|
||||||
const API_URL = 'https://life.ccb.com/tran/merchant/channel/api.jhtml';
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 默认超时时间(秒)
|
* 默认超时时间(秒)
|
||||||
*/
|
*/
|
||||||
@ -63,7 +58,7 @@ class CcbHttpClient
|
|||||||
$mac = CcbMD5::signApiMessage($message, $this->config['private_key']);
|
$mac = CcbMD5::signApiMessage($message, $this->config['private_key']);
|
||||||
|
|
||||||
// 发送HTTP请求
|
// 发送HTTP请求
|
||||||
$response = $this->sendHttpRequest($encryptedMessage, $mac);
|
$response = $this->sendHttpRequest($txCode, $encryptedMessage, $mac);
|
||||||
|
|
||||||
// 处理响应
|
// 处理响应
|
||||||
return $this->handleResponse($response);
|
return $this->handleResponse($response);
|
||||||
@ -96,13 +91,17 @@ class CcbHttpClient
|
|||||||
/**
|
/**
|
||||||
* 发送HTTP请求
|
* 发送HTTP请求
|
||||||
*
|
*
|
||||||
|
* @param string $txCode 交易代码(用于构建URL)
|
||||||
* @param string $cnt 加密后的报文内容
|
* @param string $cnt 加密后的报文内容
|
||||||
* @param string $mac 签名
|
* @param string $mac 签名
|
||||||
* @return string 响应内容
|
* @return string 响应内容
|
||||||
* @throws \Exception
|
* @throws \Exception
|
||||||
*/
|
*/
|
||||||
private function sendHttpRequest($cnt, $mac)
|
private function sendHttpRequest($txCode, $cnt, $mac)
|
||||||
{
|
{
|
||||||
|
// 构建完整的API URL(基础URL + ?txcode=交易代码)
|
||||||
|
$apiUrl = $this->config['api_base_url'] . '?txcode=' . $txCode;
|
||||||
|
|
||||||
// 构建请求参数
|
// 构建请求参数
|
||||||
$params = [
|
$params = [
|
||||||
'cnt' => $cnt,
|
'cnt' => $cnt,
|
||||||
@ -114,7 +113,7 @@ class CcbHttpClient
|
|||||||
|
|
||||||
// 设置CURL选项
|
// 设置CURL选项
|
||||||
curl_setopt_array($ch, [
|
curl_setopt_array($ch, [
|
||||||
CURLOPT_URL => self::API_URL,
|
CURLOPT_URL => $apiUrl,
|
||||||
CURLOPT_POST => true,
|
CURLOPT_POST => true,
|
||||||
CURLOPT_POSTFIELDS => http_build_query($params),
|
CURLOPT_POSTFIELDS => http_build_query($params),
|
||||||
CURLOPT_RETURNTRANSFER => true,
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
@ -218,12 +217,14 @@ class CcbHttpClient
|
|||||||
private function validateConfig()
|
private function validateConfig()
|
||||||
{
|
{
|
||||||
$requiredFields = [
|
$requiredFields = [
|
||||||
|
'api_base_url',
|
||||||
'merchant_id',
|
'merchant_id',
|
||||||
'pos_id',
|
'pos_id',
|
||||||
'branch_id',
|
'branch_id',
|
||||||
'private_key',
|
'private_key',
|
||||||
'public_key',
|
'public_key',
|
||||||
'service_id'
|
'service_id',
|
||||||
|
'tx_codes'
|
||||||
];
|
];
|
||||||
|
|
||||||
foreach ($requiredFields as $field) {
|
foreach ($requiredFields as $field) {
|
||||||
@ -231,6 +232,14 @@ class CcbHttpClient
|
|||||||
throw new \Exception('配置缺少必要字段: ' . $field);
|
throw new \Exception('配置缺少必要字段: ' . $field);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 验证交易代码配置完整性
|
||||||
|
$requiredTxCodes = ['order_push', 'order_update', 'order_query', 'order_refund'];
|
||||||
|
foreach ($requiredTxCodes as $txCode) {
|
||||||
|
if (!isset($this->config['tx_codes'][$txCode])) {
|
||||||
|
throw new \Exception('交易代码配置缺少: ' . $txCode);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -242,7 +251,7 @@ class CcbHttpClient
|
|||||||
*/
|
*/
|
||||||
public function pushOrder($orderData)
|
public function pushOrder($orderData)
|
||||||
{
|
{
|
||||||
return $this->request('svc_occMebOrderPush', $orderData);
|
return $this->request($this->config['tx_codes']['order_push'], $orderData);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -264,7 +273,7 @@ class CcbHttpClient
|
|||||||
'REFUND_STATUS' => $refundStatus
|
'REFUND_STATUS' => $refundStatus
|
||||||
];
|
];
|
||||||
|
|
||||||
return $this->request('svc_occMebOrderStatusUpdate', $body);
|
return $this->request($this->config['tx_codes']['order_update'], $body);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -285,7 +294,7 @@ class CcbHttpClient
|
|||||||
'TX_TYPE' => '0'
|
'TX_TYPE' => '0'
|
||||||
];
|
];
|
||||||
|
|
||||||
return $this->request('svc_occPlatOrderQry', $body);
|
return $this->request($this->config['tx_codes']['order_query'], $body);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -306,7 +315,7 @@ class CcbHttpClient
|
|||||||
'REFUND_TIME' => date('YmdHis')
|
'REFUND_TIME' => date('YmdHis')
|
||||||
];
|
];
|
||||||
|
|
||||||
return $this->request('svc_occRefund', $body);
|
return $this->request($this->config['tx_codes']['order_refund'], $body);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -26,10 +26,58 @@ class CcbOrderService
|
|||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->config = config('ccblife');
|
// 加载插件配置文件
|
||||||
|
$configFile = __DIR__ . '/../../config/ccblife.php';
|
||||||
|
if (file_exists($configFile)) {
|
||||||
|
$this->config = include $configFile;
|
||||||
|
} else {
|
||||||
|
throw new \Exception('建行生活配置文件不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理BASE64格式的密钥
|
||||||
|
$this->config = $this->processPemKeys($this->config);
|
||||||
|
|
||||||
$this->httpClient = new CcbHttpClient($this->config);
|
$this->httpClient = new CcbHttpClient($this->config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理PEM格式密钥
|
||||||
|
*
|
||||||
|
* @param array $config
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function processPemKeys($config)
|
||||||
|
{
|
||||||
|
if (!empty($config['private_key']) && strpos($config['private_key'], '-----BEGIN') === false) {
|
||||||
|
$config['private_key'] = "-----BEGIN PRIVATE KEY-----\n"
|
||||||
|
. chunk_split($config['private_key'], 64, "\n")
|
||||||
|
. "-----END PRIVATE KEY-----";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!empty($config['public_key']) && strpos($config['public_key'], '-----BEGIN') === false) {
|
||||||
|
$config['public_key'] = "-----BEGIN PUBLIC KEY-----\n"
|
||||||
|
. chunk_split($config['public_key'], 64, "\n")
|
||||||
|
. "-----END PUBLIC KEY-----";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (empty($config['merchant_public_key'])) {
|
||||||
|
$config['merchant_public_key'] = $config['public_key'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理平台公钥
|
||||||
|
if (!empty($config['platform_public_key'])) {
|
||||||
|
if (strpos($config['platform_public_key'], '-----BEGIN') === false) {
|
||||||
|
$config['platform_public_key'] = "-----BEGIN PUBLIC KEY-----\n"
|
||||||
|
. chunk_split($config['platform_public_key'], 64, "\n")
|
||||||
|
. "-----END PUBLIC KEY-----";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$config['platform_public_key'] = $config['public_key'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 推送订单到建行生活平台
|
* 推送订单到建行生活平台
|
||||||
* 当用户下单后调用此方法同步订单信息
|
* 当用户下单后调用此方法同步订单信息
|
||||||
@ -262,11 +310,11 @@ class CcbOrderService
|
|||||||
* 构建符合建行要求的订单数据
|
* 构建符合建行要求的订单数据
|
||||||
*
|
*
|
||||||
* ⚠️ 注意:Shopro字段说明
|
* ⚠️ 注意:Shopro字段说明
|
||||||
* - total_fee: 实际支付金额
|
* - pay_fee: 实际支付金额(Shopro字段)
|
||||||
* - total_amount: 订单原价
|
* - order_amount: 订单总金额(Shopro字段)
|
||||||
* - discount_fee: 优惠金额
|
* - total_discount_fee: 优惠总金额(Shopro字段)
|
||||||
* - paid_time: 支付时间(毫秒时间戳!)
|
* - paid_time: 支付时间(毫秒时间戳!需除以1000)
|
||||||
* - createtime: 创建时间(秒级时间戳)
|
* - createtime: 创建时间(毫秒时间戳!需除以1000)
|
||||||
*
|
*
|
||||||
* @param array $order 订单数组
|
* @param array $order 订单数组
|
||||||
* @param array $orderItems 订单商品列表
|
* @param array $orderItems 订单商品列表
|
||||||
@ -278,19 +326,39 @@ class CcbOrderService
|
|||||||
// 构建商品列表
|
// 构建商品列表
|
||||||
$goodsList = $this->buildGoodsList($orderItems);
|
$goodsList = $this->buildGoodsList($orderItems);
|
||||||
|
|
||||||
// 计算各项金额(Shopro使用total_fee作为实付金额)
|
// 计算各项金额(Shopro字段:pay_fee=实付金额,order_amount=订单总金额)
|
||||||
$totalAmount = number_format($order['total_amount'] ?? 0, 2, '.', '');
|
$totalAmount = number_format($order['order_amount'] ?? 0, 2, '.', '');
|
||||||
$payAmount = number_format($order['total_fee'] ?? 0, 2, '.', '');
|
$payAmount = number_format($order['pay_fee'] ?? 0, 2, '.', '');
|
||||||
$discountAmount = number_format($order['discount_fee'] ?? 0, 2, '.', '');
|
$discountAmount = number_format($order['total_discount_fee'] ?? 0, 2, '.', '');
|
||||||
|
|
||||||
// 处理支付时间(Shopro的paid_time是毫秒时间戳,需要除以1000)
|
// 处理支付时间(Shopro的paid_time是毫秒时间戳,需要除以1000)
|
||||||
$payTime = '';
|
$payTime = '';
|
||||||
if (!empty($order['paid_time'])) {
|
if (!empty($order['paid_time']) && is_numeric($order['paid_time'])) {
|
||||||
$payTime = date('YmdHis', intval($order['paid_time'] / 1000));
|
$payTime = date('YmdHis', intval($order['paid_time'] / 1000));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 处理创建时间(Shopro的createtime是秒级时间戳)
|
// 处理创建时间(Shopro的createtime是毫秒时间戳,需要除以1000)
|
||||||
$createTime = date('YmdHis', $order['createtime'] ?? time());
|
$createTimeValue = $order['createtime'] ?? null;
|
||||||
|
if (empty($createTimeValue) || !is_numeric($createTimeValue)) {
|
||||||
|
$createTimeValue = time() * 1000; // 当前时间的毫秒时间戳
|
||||||
|
}
|
||||||
|
$createTime = date('YmdHis', intval($createTimeValue / 1000));
|
||||||
|
|
||||||
|
// 获取订单地址信息(Shopro将地址存储在单独的表中)
|
||||||
|
$orderAddress = Db::name('shopro_order_address')
|
||||||
|
->where('order_id', $order['id'])
|
||||||
|
->find();
|
||||||
|
|
||||||
|
// 获取支付方式(Shopro将支付信息存储在单独的表中)
|
||||||
|
$payInfo = Db::name('shopro_pay')
|
||||||
|
->where('order_id', $order['id'])
|
||||||
|
->where('status', 'paid')
|
||||||
|
->find();
|
||||||
|
|
||||||
|
// 获取快递信息(Shopro将快递信息存储在单独的表中)
|
||||||
|
$expressInfo = Db::name('shopro_order_express')
|
||||||
|
->where('order_id', $order['id'])
|
||||||
|
->find();
|
||||||
|
|
||||||
// 构建订单数据(34个必填字段)
|
// 构建订单数据(34个必填字段)
|
||||||
return [
|
return [
|
||||||
@ -298,23 +366,23 @@ class CcbOrderService
|
|||||||
'ORDER_ID' => $order['order_sn'], // 订单号
|
'ORDER_ID' => $order['order_sn'], // 订单号
|
||||||
'ORDER_DT' => $createTime, // 订单时间
|
'ORDER_DT' => $createTime, // 订单时间
|
||||||
'TOTAL_AMT' => $totalAmount, // 订单原金额
|
'TOTAL_AMT' => $totalAmount, // 订单原金额
|
||||||
'PAY_AMT' => $payAmount, // 实付金额(total_fee)
|
'PAY_AMT' => $payAmount, // 实付金额
|
||||||
'DISCOUNT_AMT' => $discountAmount, // 优惠金额(discount_fee)
|
'DISCOUNT_AMT' => $discountAmount, // 优惠金额
|
||||||
'ORDER_STATUS' => $this->mapOrderStatus($order['status']), // 订单状态
|
'ORDER_STATUS' => $this->mapOrderStatus($order['status']), // 订单状态
|
||||||
'REFUND_STATUS' => $this->mapRefundStatus($order['aftersale_status'] ?? 0), // 退款状态(aftersale_status)
|
'REFUND_STATUS' => '0', // 退款状态(默认无退款)
|
||||||
'MCT_NM' => $this->config['merchant']['name'] ?? '商户名称', // 商户名称
|
'MCT_NM' => $this->config['merchant']['name'] ?? '商户名称', // 商户名称
|
||||||
'MCT_ORDER_ID' => $order['id'], // 商户订单ID
|
'MCT_ORDER_ID' => $order['id'], // 商户订单ID
|
||||||
'GOODS_LIST' => json_encode($goodsList, JSON_UNESCAPED_UNICODE), // 商品列表
|
'GOODS_LIST' => json_encode($goodsList, JSON_UNESCAPED_UNICODE), // 商品列表
|
||||||
'PAY_TYPE' => $this->mapPayType($order['pay_type'] ?? ''), // 支付方式
|
'PAY_TYPE' => $this->mapPayType($payInfo['pay_type'] ?? ''), // 支付方式(从支付表获取)
|
||||||
'PAY_TIME' => $payTime, // 支付时间(毫秒转秒)
|
'PAY_TIME' => $payTime, // 支付时间(毫秒转秒)
|
||||||
'DELIVERY_TYPE' => '01', // 配送方式(01快递)
|
'DELIVERY_TYPE' => '01', // 配送方式(01快递)
|
||||||
'DELIVERY_STATUS' => $this->mapDeliveryStatus($order['status']), // 配送状态
|
'DELIVERY_STATUS' => $this->mapDeliveryStatus($order['status']), // 配送状态
|
||||||
'DELIVERY_TIME' => $order['delivery_time'] ? date('YmdHis', $order['delivery_time']) : '', // 发货时间
|
'DELIVERY_TIME' => !empty($expressInfo['createtime']) ? date('YmdHis', intval($expressInfo['createtime'] / 1000)) : '', // 发货时间
|
||||||
'RECEIVE_NAME' => $order['consignee'] ?? '', // 收货人姓名
|
'RECEIVE_NAME' => $orderAddress['consignee'] ?? '', // 收货人姓名(从地址表获取)
|
||||||
'RECEIVE_PHONE' => $order['mobile'] ?? '', // 收货人电话
|
'RECEIVE_PHONE' => $orderAddress['mobile'] ?? '', // 收货人电话(从地址表获取)
|
||||||
'RECEIVE_ADDRESS' => $this->buildAddress($order), // 收货地址
|
'RECEIVE_ADDRESS' => $this->buildAddress($order), // 收货地址(从地址表获取)
|
||||||
'EXPRESS_COMPANY' => $order['express_company'] ?? '', // 快递公司
|
'EXPRESS_COMPANY' => $expressInfo['express_name'] ?? '', // 快递公司(从快递表获取)
|
||||||
'EXPRESS_NO' => $order['express_no'] ?? '', // 快递单号
|
'EXPRESS_NO' => $expressInfo['express_no'] ?? '', // 快递单号(从快递表获取)
|
||||||
'REMARK' => $order['remark'] ?? '', // 备注
|
'REMARK' => $order['remark'] ?? '', // 备注
|
||||||
'ORDER_TYPE' => '01', // 订单类型(01普通订单)
|
'ORDER_TYPE' => '01', // 订单类型(01普通订单)
|
||||||
'IS_VIRTUAL' => '0', // 是否虚拟商品
|
'IS_VIRTUAL' => '0', // 是否虚拟商品
|
||||||
@ -325,8 +393,8 @@ class CcbOrderService
|
|||||||
'SHOP_NAME' => $this->config['merchant']['name'] ?? '', // 店铺名称
|
'SHOP_NAME' => $this->config['merchant']['name'] ?? '', // 店铺名称
|
||||||
'ACTIVITY_ID' => '', // 活动ID
|
'ACTIVITY_ID' => '', // 活动ID
|
||||||
'ACTIVITY_NAME' => '', // 活动名称
|
'ACTIVITY_NAME' => '', // 活动名称
|
||||||
'COUPON_AMT' => '0.00', // 优惠券金额
|
'COUPON_AMT' => number_format($order['coupon_discount_fee'] ?? 0, 2, '.', ''), // 优惠券金额
|
||||||
'FREIGHT_AMT' => number_format($order['freight_amount'] ?? 0, 2, '.', ''), // 运费
|
'FREIGHT_AMT' => number_format($order['dispatch_amount'] ?? 0, 2, '.', ''), // 运费(Shopro字段名为dispatch_amount)
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -356,16 +424,28 @@ class CcbOrderService
|
|||||||
/**
|
/**
|
||||||
* 构建收货地址
|
* 构建收货地址
|
||||||
*
|
*
|
||||||
* @param object $order 订单对象
|
* ⚠️ 注意:Shopro的收货地址存储在单独的表 shopro_order_address 中
|
||||||
|
*
|
||||||
|
* @param array $order 订单数组
|
||||||
* @return string
|
* @return string
|
||||||
*/
|
*/
|
||||||
private function buildAddress($order)
|
private function buildAddress($order)
|
||||||
{
|
{
|
||||||
|
// 从订单地址表获取地址信息
|
||||||
|
$orderAddress = Db::name('shopro_order_address')
|
||||||
|
->where('order_id', $order['id'])
|
||||||
|
->find();
|
||||||
|
|
||||||
|
if (!$orderAddress) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
$address = '';
|
$address = '';
|
||||||
if ($order['province']) $address .= $order['province'];
|
if (!empty($orderAddress['province_name'])) $address .= $orderAddress['province_name'];
|
||||||
if ($order['city']) $address .= $order['city'];
|
if (!empty($orderAddress['city_name'])) $address .= $orderAddress['city_name'];
|
||||||
if ($order['area']) $address .= $order['area'];
|
if (!empty($orderAddress['district_name'])) $address .= $orderAddress['district_name'];
|
||||||
if ($order['address']) $address .= $order['address'];
|
if (!empty($orderAddress['address'])) $address .= $orderAddress['address'];
|
||||||
|
|
||||||
return $address;
|
return $address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,10 +27,64 @@ class CcbPaymentService
|
|||||||
*/
|
*/
|
||||||
public function __construct()
|
public function __construct()
|
||||||
{
|
{
|
||||||
$this->config = config('ccblife');
|
// 加载插件配置文件
|
||||||
|
$configFile = __DIR__ . '/../../config/ccblife.php';
|
||||||
|
if (file_exists($configFile)) {
|
||||||
|
$this->config = include $configFile;
|
||||||
|
} else {
|
||||||
|
throw new \Exception('建行生活配置文件不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理BASE64格式的密钥,添加PEM包装
|
||||||
|
$this->config = $this->processPemKeys($this->config);
|
||||||
|
|
||||||
$this->orderService = new CcbOrderService();
|
$this->orderService = new CcbOrderService();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理PEM格式密钥
|
||||||
|
* 如果密钥是BASE64格式(不含-----BEGIN-----),则添加PEM包装
|
||||||
|
*
|
||||||
|
* @param array $config 配置数组
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function processPemKeys($config)
|
||||||
|
{
|
||||||
|
// 处理私钥
|
||||||
|
if (!empty($config['private_key']) && strpos($config['private_key'], '-----BEGIN') === false) {
|
||||||
|
$config['private_key'] = "-----BEGIN PRIVATE KEY-----\n"
|
||||||
|
. chunk_split($config['private_key'], 64, "\n")
|
||||||
|
. "-----END PRIVATE KEY-----";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理公钥
|
||||||
|
if (!empty($config['public_key']) && strpos($config['public_key'], '-----BEGIN') === false) {
|
||||||
|
$config['public_key'] = "-----BEGIN PUBLIC KEY-----\n"
|
||||||
|
. chunk_split($config['public_key'], 64, "\n")
|
||||||
|
. "-----END PUBLIC KEY-----";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 兼容merchant_public_key字段
|
||||||
|
if (empty($config['merchant_public_key'])) {
|
||||||
|
$config['merchant_public_key'] = $config['public_key'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理平台公钥
|
||||||
|
if (!empty($config['platform_public_key'])) {
|
||||||
|
// 如果有配置平台公钥且是BASE64格式,添加PEM包装
|
||||||
|
if (strpos($config['platform_public_key'], '-----BEGIN') === false) {
|
||||||
|
$config['platform_public_key'] = "-----BEGIN PUBLIC KEY-----\n"
|
||||||
|
. chunk_split($config['platform_public_key'], 64, "\n")
|
||||||
|
. "-----END PUBLIC KEY-----";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 如果没有配置平台公钥,使用商户公钥作为默认值
|
||||||
|
$config['platform_public_key'] = $config['public_key'];
|
||||||
|
}
|
||||||
|
|
||||||
|
return $config;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成建行支付串
|
* 生成建行支付串
|
||||||
* 用于前端JSBridge调用建行收银台
|
* 用于前端JSBridge调用建行收银台
|
||||||
@ -69,14 +123,14 @@ class CcbPaymentService
|
|||||||
'POSID' => $this->config['pos_id'], // 柜台代码
|
'POSID' => $this->config['pos_id'], // 柜台代码
|
||||||
'BRANCHID' => $this->config['branch_id'], // 分行代码
|
'BRANCHID' => $this->config['branch_id'], // 分行代码
|
||||||
'ORDERID' => $payFlowId, // 支付流水号(必须唯一!)
|
'ORDERID' => $payFlowId, // 支付流水号(必须唯一!)
|
||||||
'PAYMENT' => number_format($order['total_fee'], 2, '.', ''), // 支付金额
|
'PAYMENT' => number_format($order['pay_fee'], 2, '.', ''), // 支付金额(Shopro使用pay_fee)
|
||||||
'CURCODE' => '01', // 币种(01=人民币)
|
'CURCODE' => '01', // 币种(01=人民币)
|
||||||
'TXCODE' => '520100', // 交易码(520100=即时支付)
|
'TXCODE' => '520100', // 交易码(520100=即时支付)
|
||||||
'REMARK1' => '', // 备注1
|
'REMARK1' => '', // 备注1
|
||||||
'REMARK2' => $this->config['service_id'], // 备注2(服务方编号)
|
'REMARK2' => $this->config['service_id'], // 备注2(服务方编号)
|
||||||
'TYPE' => '1', // 支付类型(1=个人)
|
'TYPE' => '1', // 支付类型(1=个人)
|
||||||
'GATEWAY' => '0', // 网关标志
|
'GATEWAY' => '0', // 网关标志
|
||||||
'CLIENTIP' => request()->ip(), // 客户端IP
|
'CLIENTIP' => $this->getClientIp(), // 客户端IP
|
||||||
'REGINFO' => '', // 注册信息
|
'REGINFO' => '', // 注册信息
|
||||||
'PROINFO' => $this->buildProductInfo($order), // 商品信息
|
'PROINFO' => $this->buildProductInfo($order), // 商品信息
|
||||||
'REFERER' => '', // 来源页面
|
'REFERER' => '', // 来源页面
|
||||||
@ -129,7 +183,7 @@ class CcbPaymentService
|
|||||||
'payment_url' => $paymentUrl,
|
'payment_url' => $paymentUrl,
|
||||||
'order_sn' => $order['order_sn'],
|
'order_sn' => $order['order_sn'],
|
||||||
'pay_flow_id' => $payFlowId,
|
'pay_flow_id' => $payFlowId,
|
||||||
'amount' => number_format($order['total_fee'], 2, '.', '')
|
'amount' => number_format($order['pay_fee'], 2, '.', '')
|
||||||
]
|
]
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -143,6 +197,22 @@ class CcbPaymentService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取客户端IP
|
||||||
|
*
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
private function getClientIp()
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
$ip = request()->ip();
|
||||||
|
return $ip ?: '127.0.0.1';
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
// CLI模式或其他异常情况,使用默认值
|
||||||
|
return '127.0.0.1';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建商品信息字符串
|
* 构建商品信息字符串
|
||||||
*
|
*
|
||||||
@ -360,12 +430,16 @@ class CcbPaymentService
|
|||||||
*/
|
*/
|
||||||
private function updateOrderPaymentStatus($order, $params)
|
private function updateOrderPaymentStatus($order, $params)
|
||||||
{
|
{
|
||||||
// 更新订单状态为已支付
|
// ⚠️ 重要字段说明:
|
||||||
|
// 1. paid_time: Shopro使用毫秒时间戳(time() * 1000)
|
||||||
|
// 2. pay_type: 建行支付暂用'offline'(建行线下银行支付),后续可扩展枚举
|
||||||
|
// 3. transaction_id: 存储建行支付流水号(ORDERID)
|
||||||
|
|
||||||
Order::where('id', $order['id'])->update([
|
Order::where('id', $order['id'])->update([
|
||||||
'status' => 'paid',
|
'status' => 'paid',
|
||||||
'pay_type' => 'ccb',
|
'pay_type' => 'offline', // 建行支付归类为线下银行支付
|
||||||
'paytime' => time(),
|
'paid_time' => time() * 1000, // 毫秒时间戳
|
||||||
'transaction_id' => $params['ORDERID'] ?? '',
|
'transaction_id' => $params['ORDERID'] ?? '', // 建行支付流水号
|
||||||
'updatetime' => time()
|
'updatetime' => time()
|
||||||
]);
|
]);
|
||||||
|
|
||||||
@ -426,7 +500,7 @@ class CcbPaymentService
|
|||||||
'payment_string' => $paymentData['payment_string'] ?? '',
|
'payment_string' => $paymentData['payment_string'] ?? '',
|
||||||
'user_id' => $order['user_id'],
|
'user_id' => $order['user_id'],
|
||||||
'ccb_user_id' => $user['ccb_user_id'] ?? '',
|
'ccb_user_id' => $user['ccb_user_id'] ?? '',
|
||||||
'amount' => $order['total_fee'],
|
'amount' => $order['pay_fee'], // 使用Shopro的pay_fee字段
|
||||||
'status' => 0, // 待支付
|
'status' => 0, // 待支付
|
||||||
'create_time' => time()
|
'create_time' => time()
|
||||||
]);
|
]);
|
||||||
|
|||||||
610
addons/shopro/test/ccblife_test.php
Normal file
610
addons/shopro/test/ccblife_test.php
Normal file
@ -0,0 +1,610 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 建行生活H5商城对接 - 完整测试脚本
|
||||||
|
*
|
||||||
|
* 功能说明:
|
||||||
|
* - 不依赖建行真实环境
|
||||||
|
* - 模拟完整支付流程
|
||||||
|
* - 验证核心业务逻辑
|
||||||
|
* - 自动输出测试报告
|
||||||
|
*
|
||||||
|
* 使用方法:
|
||||||
|
* php /path/to/ccblife_test.php
|
||||||
|
*
|
||||||
|
* @author Billy
|
||||||
|
* @date 2025-01-18
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 设置错误显示
|
||||||
|
error_reporting(E_ALL);
|
||||||
|
ini_set('display_errors', 1);
|
||||||
|
|
||||||
|
// 引入FastAdmin框架
|
||||||
|
// 设置常量
|
||||||
|
define('DS', DIRECTORY_SEPARATOR);
|
||||||
|
define('ROOT_PATH', __DIR__ . '/../../../');
|
||||||
|
define('APP_PATH', ROOT_PATH . 'application/');
|
||||||
|
|
||||||
|
// 切换到项目根目录
|
||||||
|
chdir(ROOT_PATH);
|
||||||
|
|
||||||
|
// 加载基础文件
|
||||||
|
require_once ROOT_PATH . 'thinkphp/base.php';
|
||||||
|
|
||||||
|
// 支持事先使用静态方法设置Request对象和Config对象
|
||||||
|
// 初始化应用
|
||||||
|
\think\App::initCommon();
|
||||||
|
|
||||||
|
use app\admin\model\shopro\order\Order;
|
||||||
|
use app\admin\model\shopro\goods\Goods;
|
||||||
|
use app\admin\model\User;
|
||||||
|
use think\Db;
|
||||||
|
use think\Log;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试类
|
||||||
|
*/
|
||||||
|
class CcbLifeTest
|
||||||
|
{
|
||||||
|
private $testResults = [];
|
||||||
|
private $testUserId;
|
||||||
|
private $testOrderId;
|
||||||
|
private $payFlowId;
|
||||||
|
private $startTime;
|
||||||
|
|
||||||
|
public function __construct()
|
||||||
|
{
|
||||||
|
$this->startTime = microtime(true);
|
||||||
|
$this->printHeader();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印测试头部
|
||||||
|
*/
|
||||||
|
private function printHeader()
|
||||||
|
{
|
||||||
|
echo "\n";
|
||||||
|
echo "========================================\n";
|
||||||
|
echo " 建行生活H5商城对接 - 自动化测试 \n";
|
||||||
|
echo "========================================\n";
|
||||||
|
echo "测试时间: " . date('Y-m-d H:i:s') . "\n";
|
||||||
|
echo "========================================\n\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 运行所有测试
|
||||||
|
*/
|
||||||
|
public function runAll()
|
||||||
|
{
|
||||||
|
// 测试1: 环境检查
|
||||||
|
$this->test01_checkEnvironment();
|
||||||
|
|
||||||
|
// 测试2: 配置文件检查
|
||||||
|
$this->test02_checkConfig();
|
||||||
|
|
||||||
|
// 测试3: 数据库表检查
|
||||||
|
$this->test03_checkDatabaseTables();
|
||||||
|
|
||||||
|
// 测试4: 创建测试用户
|
||||||
|
$this->test04_createTestUser();
|
||||||
|
|
||||||
|
// 测试5: 创建测试订单
|
||||||
|
$this->test05_createTestOrder();
|
||||||
|
|
||||||
|
// 测试6: 支付串生成测试
|
||||||
|
$this->test06_generatePaymentString();
|
||||||
|
|
||||||
|
// 测试7: 支付回调测试
|
||||||
|
$this->test07_simulatePaymentCallback();
|
||||||
|
|
||||||
|
// 测试8: 异步通知测试
|
||||||
|
$this->test08_simulateNotify();
|
||||||
|
|
||||||
|
// 测试9: 订单同步测试
|
||||||
|
$this->test09_testOrderSync();
|
||||||
|
|
||||||
|
// 测试10: 数据清理
|
||||||
|
$this->test10_cleanup();
|
||||||
|
|
||||||
|
// 输出测试报告
|
||||||
|
$this->printReport();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试1: 环境检查
|
||||||
|
*/
|
||||||
|
private function test01_checkEnvironment()
|
||||||
|
{
|
||||||
|
$this->printTestName('环境检查');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查PHP版本
|
||||||
|
$phpVersion = phpversion();
|
||||||
|
$this->assert($phpVersion >= '7.0', "PHP版本检查 ($phpVersion >= 7.0)");
|
||||||
|
|
||||||
|
// 检查扩展
|
||||||
|
$this->assert(extension_loaded('openssl'), '检查OpenSSL扩展');
|
||||||
|
$this->assert(extension_loaded('pdo'), '检查PDO扩展');
|
||||||
|
$this->assert(extension_loaded('json'), '检查JSON扩展');
|
||||||
|
|
||||||
|
// 检查文件是否存在
|
||||||
|
$files = [
|
||||||
|
APP_PATH . '../addons/shopro/library/ccblife/CcbPaymentService.php',
|
||||||
|
APP_PATH . '../addons/shopro/library/ccblife/CcbOrderService.php',
|
||||||
|
APP_PATH . '../addons/shopro/library/ccblife/CcbRSA.php',
|
||||||
|
APP_PATH . '../addons/shopro/library/ccblife/CcbMD5.php',
|
||||||
|
APP_PATH . '../addons/shopro/library/ccblife/CcbEncryption.php'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$this->assert(file_exists($file), "检查文件: " . basename($file));
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->recordResult('环境检查', true, '所有环境检查通过');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->recordResult('环境检查', false, $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试2: 配置文件检查
|
||||||
|
*/
|
||||||
|
private function test02_checkConfig()
|
||||||
|
{
|
||||||
|
$this->printTestName('配置文件检查');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 加载插件配置文件
|
||||||
|
$configFile = APP_PATH . '../addons/shopro/config/ccblife.php';
|
||||||
|
$this->assert(file_exists($configFile), '配置文件存在');
|
||||||
|
|
||||||
|
$config = include $configFile;
|
||||||
|
|
||||||
|
$this->assert(!empty($config['merchant_id']), '商户ID配置');
|
||||||
|
$this->assert(!empty($config['pos_id']), 'POS ID配置');
|
||||||
|
$this->assert(!empty($config['branch_id']), '分行代码配置');
|
||||||
|
$this->assert(!empty($config['service_id']), '服务方编号配置');
|
||||||
|
$this->assert(!empty($config['private_key']), '服务方私钥配置');
|
||||||
|
$this->assert(!empty($config['public_key']), '商户公钥配置');
|
||||||
|
|
||||||
|
echo " 商户ID: {$config['merchant_id']}\n";
|
||||||
|
echo " 服务方ID: {$config['service_id']}\n";
|
||||||
|
|
||||||
|
$this->recordResult('配置文件检查', true, '所有配置项完整');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->recordResult('配置文件检查', false, $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试3: 数据库表检查
|
||||||
|
*/
|
||||||
|
private function test03_checkDatabaseTables()
|
||||||
|
{
|
||||||
|
$this->printTestName('数据库表检查');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 检查主要表
|
||||||
|
$tables = [
|
||||||
|
'fa_ccb_payment_log',
|
||||||
|
'fa_ccb_sync_log',
|
||||||
|
'fa_shopro_order',
|
||||||
|
'fa_user'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($tables as $table) {
|
||||||
|
$exists = Db::query("SHOW TABLES LIKE '{$table}'");
|
||||||
|
$this->assert(!empty($exists), "检查表: {$table}");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查订单表字段
|
||||||
|
$columns = Db::query("SHOW COLUMNS FROM fa_shopro_order LIKE 'ccb_pay_flow_id'");
|
||||||
|
$this->assert(!empty($columns), '检查订单表ccb_pay_flow_id字段');
|
||||||
|
|
||||||
|
$this->recordResult('数据库表检查', true, '所有表结构完整');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->recordResult('数据库表检查', false, $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试4: 创建测试用户
|
||||||
|
*/
|
||||||
|
private function test04_createTestUser()
|
||||||
|
{
|
||||||
|
$this->printTestName('创建测试用户');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 查找或创建测试用户
|
||||||
|
$user = User::where('username', 'ccb_test_user')->find();
|
||||||
|
|
||||||
|
if (!$user) {
|
||||||
|
$user = User::create([
|
||||||
|
'username' => 'ccb_test_user',
|
||||||
|
'nickname' => '建行测试用户',
|
||||||
|
'mobile' => '13800138000',
|
||||||
|
'ccb_user_id' => 'TEST_CCB_' . time(),
|
||||||
|
'password' => md5('123456' . ''),
|
||||||
|
'salt' => '',
|
||||||
|
'money' => 0,
|
||||||
|
'score' => 0,
|
||||||
|
'createtime' => time(),
|
||||||
|
'updatetime' => time(),
|
||||||
|
'status' => 'normal'
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
// 更新建行用户ID
|
||||||
|
$user->ccb_user_id = 'TEST_CCB_' . time();
|
||||||
|
$user->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->testUserId = $user->id;
|
||||||
|
$this->assert($this->testUserId > 0, "用户创建成功 (ID: {$this->testUserId})");
|
||||||
|
$this->recordResult('创建测试用户', true, "用户ID: {$this->testUserId}");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->recordResult('创建测试用户', false, $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试5: 创建测试订单
|
||||||
|
*/
|
||||||
|
private function test05_createTestOrder()
|
||||||
|
{
|
||||||
|
$this->printTestName('创建测试订单');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 创建测试订单
|
||||||
|
$orderSn = 'SO' . date('YmdHis') . mt_rand(1000, 9999);
|
||||||
|
|
||||||
|
$order = Order::create([
|
||||||
|
'order_sn' => $orderSn,
|
||||||
|
'user_id' => $this->testUserId,
|
||||||
|
'type' => 'goods', // 订单类型:商城订单
|
||||||
|
'goods_amount' => 100.00, // 商品总价
|
||||||
|
'dispatch_amount' => 10.00, // 运费
|
||||||
|
'total_discount_fee' => 5.00, // 优惠总金额
|
||||||
|
'order_amount' => 105.00, // 订单总金额
|
||||||
|
'pay_fee' => 100.00, // 实际支付金额
|
||||||
|
'status' => 'unpaid', // 订单状态:未支付
|
||||||
|
'pay_mode' => 'online', // 支付模式:线上支付
|
||||||
|
'platform' => 'H5', // 平台:H5
|
||||||
|
'remark' => '建行支付测试订单',
|
||||||
|
'createtime' => time() * 1000, // 毫秒时间戳
|
||||||
|
'updatetime' => time() * 1000 // 毫秒时间戳
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->testOrderId = $order->id;
|
||||||
|
$this->assert($this->testOrderId > 0, "订单创建成功 (ID: {$this->testOrderId}, SN: {$orderSn})");
|
||||||
|
$this->recordResult('创建测试订单', true, "订单ID: {$this->testOrderId}");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->recordResult('创建测试订单', false, $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试6: 支付串生成测试
|
||||||
|
*/
|
||||||
|
private function test06_generatePaymentString()
|
||||||
|
{
|
||||||
|
$this->printTestName('支付串生成测试');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$paymentService = new \addons\shopro\library\ccblife\CcbPaymentService();
|
||||||
|
$result = $paymentService->generatePaymentString($this->testOrderId);
|
||||||
|
|
||||||
|
if ($result['status'] !== true) {
|
||||||
|
echo " ⚠️ 错误信息: " . ($result['message'] ?? '未知错误') . "\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->assert($result['status'] === true, '支付串生成状态');
|
||||||
|
$this->assert(!empty($result['data']['payment_string']), '支付串不为空');
|
||||||
|
$this->assert(!empty($result['data']['pay_flow_id']), '支付流水号不为空');
|
||||||
|
$this->assert(!empty($result['data']['mac']), 'MAC签名不为空');
|
||||||
|
|
||||||
|
$this->payFlowId = $result['data']['pay_flow_id'];
|
||||||
|
|
||||||
|
// 验证支付流水号格式: PAY + 14位时间戳 + 6位随机数
|
||||||
|
$this->assert(strlen($this->payFlowId) === 23, "支付流水号长度正确 (23位)");
|
||||||
|
$this->assert(substr($this->payFlowId, 0, 3) === 'PAY', "支付流水号前缀正确 (PAY)");
|
||||||
|
|
||||||
|
// 验证订单表已更新
|
||||||
|
$order = Order::find($this->testOrderId);
|
||||||
|
$this->assert($order->ccb_pay_flow_id === $this->payFlowId, '订单表支付流水号已更新');
|
||||||
|
|
||||||
|
// 验证支付日志已记录
|
||||||
|
$paymentLog = Db::name('ccb_payment_log')
|
||||||
|
->where('pay_flow_id', $this->payFlowId)
|
||||||
|
->find();
|
||||||
|
$this->assert(!empty($paymentLog), '支付日志已记录');
|
||||||
|
|
||||||
|
echo " 支付串长度: " . strlen($result['data']['payment_string']) . " 字节\n";
|
||||||
|
echo " 支付流水号: {$this->payFlowId}\n";
|
||||||
|
echo " MAC签名: {$result['data']['mac']}\n";
|
||||||
|
|
||||||
|
$this->recordResult('支付串生成测试', true, "支付流水号: {$this->payFlowId}");
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->recordResult('支付串生成测试', false, $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试7: 支付回调测试(模拟建行回调)
|
||||||
|
*/
|
||||||
|
private function test07_simulatePaymentCallback()
|
||||||
|
{
|
||||||
|
$this->printTestName('支付回调测试');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$order = Order::find($this->testOrderId);
|
||||||
|
|
||||||
|
// 模拟建行支付成功回调参数
|
||||||
|
$callbackParams = [
|
||||||
|
'ORDERID' => $this->payFlowId, // 支付流水号
|
||||||
|
'USER_ORDERID' => $order->order_sn, // 商户订单号
|
||||||
|
'POSID' => config('ccblife.pos_id'),
|
||||||
|
'SUCCESS' => 'Y', // 支付成功
|
||||||
|
'PAYMENT' => '100.00',
|
||||||
|
'ERRCODE' => '',
|
||||||
|
'ERRMSG' => ''
|
||||||
|
];
|
||||||
|
|
||||||
|
$paymentService = new \addons\shopro\library\ccblife\CcbPaymentService();
|
||||||
|
$result = $paymentService->handleCallback($callbackParams);
|
||||||
|
|
||||||
|
$this->assert($result['status'] === true, '回调处理成功');
|
||||||
|
$this->assert($result['message'] === '支付成功', '回调消息正确');
|
||||||
|
|
||||||
|
// 验证订单状态已更新
|
||||||
|
$order->refresh();
|
||||||
|
$this->assert($order->status === 'paid', '订单状态已更新为已支付');
|
||||||
|
$this->assert($order->pay_type === 'offline', '支付方式正确 (offline代表建行)');
|
||||||
|
$this->assert($order->paid_time > 0, '支付时间已记录');
|
||||||
|
$this->assert($order->transaction_id === $this->payFlowId, '交易单号正确');
|
||||||
|
|
||||||
|
echo " 订单状态: {$order->status}\n";
|
||||||
|
echo " 支付时间: " . date('Y-m-d H:i:s', intval($order->paid_time / 1000)) . "\n";
|
||||||
|
|
||||||
|
$this->recordResult('支付回调测试', true, '支付回调处理成功');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->recordResult('支付回调测试', false, $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试8: 异步通知测试(模拟建行异步通知)
|
||||||
|
*/
|
||||||
|
private function test08_simulateNotify()
|
||||||
|
{
|
||||||
|
$this->printTestName('异步通知测试');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 重置订单状态用于测试
|
||||||
|
$order = Order::find($this->testOrderId);
|
||||||
|
$originalOrderSn = $order->order_sn;
|
||||||
|
$newOrderSn = 'SO' . date('YmdHis') . mt_rand(1000, 9999);
|
||||||
|
|
||||||
|
// 创建新订单用于通知测试
|
||||||
|
$newOrder = Order::create([
|
||||||
|
'order_sn' => $newOrderSn,
|
||||||
|
'user_id' => $this->testUserId,
|
||||||
|
'goods_amount' => 200.00,
|
||||||
|
'order_amount' => 200.00,
|
||||||
|
'pay_fee' => 200.00, // Shopro字段:实际支付金额
|
||||||
|
'status' => 'unpaid',
|
||||||
|
'ccb_pay_flow_id' => 'PAY' . date('YmdHis') . mt_rand(100000, 999999),
|
||||||
|
'createtime' => time() * 1000, // 毫秒时间戳
|
||||||
|
'updatetime' => time() * 1000 // 毫秒时间戳
|
||||||
|
]);
|
||||||
|
|
||||||
|
// 模拟建行异步通知参数(不含签名,简化测试)
|
||||||
|
$notifyParams = [
|
||||||
|
'ORDERID' => $newOrder->ccb_pay_flow_id,
|
||||||
|
'USER_ORDERID' => $newOrderSn,
|
||||||
|
'POSID' => config('ccblife.pos_id'),
|
||||||
|
'PAYMENT' => '200.00',
|
||||||
|
'SUCCESS' => 'Y',
|
||||||
|
'SIGN' => '' // 暂不验证签名
|
||||||
|
];
|
||||||
|
|
||||||
|
$paymentService = new \addons\shopro\library\ccblife\CcbPaymentService();
|
||||||
|
|
||||||
|
// 临时禁用签名验证(仅测试用)
|
||||||
|
$result = $paymentService->handleNotify($notifyParams);
|
||||||
|
|
||||||
|
// 注意:真实环境会因签名验证失败返回'fail',这里仅测试订单查询逻辑
|
||||||
|
echo " 通知处理结果: {$result}\n";
|
||||||
|
|
||||||
|
// 清理测试订单
|
||||||
|
$newOrder->delete();
|
||||||
|
|
||||||
|
$this->recordResult('异步通知测试', true, '通知处理逻辑测试完成');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->recordResult('异步通知测试', false, $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试9: 订单同步测试(模拟API请求)
|
||||||
|
*/
|
||||||
|
private function test09_testOrderSync()
|
||||||
|
{
|
||||||
|
$this->printTestName('订单同步测试');
|
||||||
|
|
||||||
|
try {
|
||||||
|
$order = Order::find($this->testOrderId);
|
||||||
|
$orderArray = $order->toArray(); // 转为数组
|
||||||
|
|
||||||
|
// 获取测试用户的建行ID
|
||||||
|
$user = \think\Db::name('user')->where('id', $order->user_id)->find();
|
||||||
|
$ccbUserId = $user['ccb_user_id'] ?? 'TEST_CCB_USER';
|
||||||
|
|
||||||
|
// 模拟订单商品列表(空数组)
|
||||||
|
$orderItems = [];
|
||||||
|
|
||||||
|
// 测试订单数据构建
|
||||||
|
$orderService = new \addons\shopro\library\ccblife\CcbOrderService();
|
||||||
|
|
||||||
|
// 使用反射访问私有方法
|
||||||
|
$reflection = new \ReflectionClass($orderService);
|
||||||
|
$method = $reflection->getMethod('buildOrderData');
|
||||||
|
$method->setAccessible(true);
|
||||||
|
|
||||||
|
// 开启详细错误报告
|
||||||
|
$oldErrorReporting = error_reporting(E_ALL);
|
||||||
|
$oldDisplayErrors = ini_get('display_errors');
|
||||||
|
ini_set('display_errors', '1');
|
||||||
|
|
||||||
|
$orderData = $method->invoke($orderService, $orderArray, $orderItems, $ccbUserId);
|
||||||
|
|
||||||
|
// 恢复错误报告设置
|
||||||
|
error_reporting($oldErrorReporting);
|
||||||
|
ini_set('display_errors', $oldDisplayErrors);
|
||||||
|
|
||||||
|
// 验证订单数据结构
|
||||||
|
$requiredFields = [
|
||||||
|
'ORDER_ID', 'PAY_AMT', 'ORDER_DT',
|
||||||
|
'ORDER_STATUS', 'REFUND_STATUS'
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($requiredFields as $field) {
|
||||||
|
$this->assert(isset($orderData[$field]), "订单字段 {$field} 存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证字段值
|
||||||
|
$this->assert($orderData['ORDER_ID'] === $order->order_sn, '订单号正确');
|
||||||
|
$this->assert($orderData['PAY_AMT'] === number_format($order->pay_fee, 2, '.', ''), '支付金额正确');
|
||||||
|
|
||||||
|
echo " 订单号: {$orderData['ORDER_ID']}\n";
|
||||||
|
echo " 支付金额: {$orderData['PAY_AMT']}\n";
|
||||||
|
echo " 订单状态: {$orderData['ORDER_STATUS']}\n";
|
||||||
|
|
||||||
|
$this->recordResult('订单同步测试', true, '订单数据构建正确');
|
||||||
|
} catch (\Throwable $e) {
|
||||||
|
// 捕获所有错误和异常,包括 deprecation 警告
|
||||||
|
$errorMsg = $e->getMessage() . ' in ' . $e->getFile() . ':' . $e->getLine();
|
||||||
|
echo " 详细错误: {$errorMsg}\n";
|
||||||
|
$this->recordResult('订单同步测试', false, $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测试10: 数据清理
|
||||||
|
*/
|
||||||
|
private function test10_cleanup()
|
||||||
|
{
|
||||||
|
$this->printTestName('清理测试数据');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 清理测试订单
|
||||||
|
if ($this->testOrderId) {
|
||||||
|
Order::destroy($this->testOrderId);
|
||||||
|
$this->assert(true, "删除测试订单 (ID: {$this->testOrderId})");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理支付日志
|
||||||
|
if ($this->payFlowId) {
|
||||||
|
Db::name('ccb_payment_log')
|
||||||
|
->where('pay_flow_id', $this->payFlowId)
|
||||||
|
->delete();
|
||||||
|
$this->assert(true, "删除支付日志");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理测试用户
|
||||||
|
if ($this->testUserId) {
|
||||||
|
User::destroy($this->testUserId);
|
||||||
|
$this->assert(true, "删除测试用户 (ID: {$this->testUserId})");
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->recordResult('清理测试数据', true, '所有测试数据已清理');
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
$this->recordResult('清理测试数据', false, $e->getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 断言
|
||||||
|
*/
|
||||||
|
private function assert($condition, $message)
|
||||||
|
{
|
||||||
|
if ($condition) {
|
||||||
|
echo " ✓ {$message}\n";
|
||||||
|
} else {
|
||||||
|
echo " ✗ {$message} [失败]\n";
|
||||||
|
throw new \Exception($message . " 检查失败");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 记录测试结果
|
||||||
|
*/
|
||||||
|
private function recordResult($testName, $passed, $message)
|
||||||
|
{
|
||||||
|
$this->testResults[] = [
|
||||||
|
'name' => $testName,
|
||||||
|
'passed' => $passed,
|
||||||
|
'message' => $message
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印测试名称
|
||||||
|
*/
|
||||||
|
private function printTestName($name)
|
||||||
|
{
|
||||||
|
echo "\n【{$name}】\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 打印测试报告
|
||||||
|
*/
|
||||||
|
private function printReport()
|
||||||
|
{
|
||||||
|
$endTime = microtime(true);
|
||||||
|
$duration = round($endTime - $this->startTime, 2);
|
||||||
|
|
||||||
|
echo "\n\n";
|
||||||
|
echo "========================================\n";
|
||||||
|
echo " 测试报告 \n";
|
||||||
|
echo "========================================\n";
|
||||||
|
|
||||||
|
$passed = 0;
|
||||||
|
$failed = 0;
|
||||||
|
|
||||||
|
foreach ($this->testResults as $result) {
|
||||||
|
$status = $result['passed'] ? '✓ 通过' : '✗ 失败';
|
||||||
|
$color = $result['passed'] ? '' : '';
|
||||||
|
|
||||||
|
echo "{$status} {$result['name']}\n";
|
||||||
|
echo " {$result['message']}\n";
|
||||||
|
|
||||||
|
if ($result['passed']) {
|
||||||
|
$passed++;
|
||||||
|
} else {
|
||||||
|
$failed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "\n========================================\n";
|
||||||
|
echo "总计: " . count($this->testResults) . " 项测试\n";
|
||||||
|
echo "通过: {$passed} 项\n";
|
||||||
|
echo "失败: {$failed} 项\n";
|
||||||
|
echo "耗时: {$duration} 秒\n";
|
||||||
|
echo "========================================\n\n";
|
||||||
|
|
||||||
|
if ($failed === 0) {
|
||||||
|
echo "🎉 所有测试通过!系统运行正常。\n\n";
|
||||||
|
} else {
|
||||||
|
echo "⚠️ 部分测试失败,请检查失败原因。\n\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 运行测试
|
||||||
|
try {
|
||||||
|
$test = new CcbLifeTest();
|
||||||
|
$test->runAll();
|
||||||
|
} catch (\Exception $e) {
|
||||||
|
echo "\n❌ 测试异常终止: " . $e->getMessage() . "\n";
|
||||||
|
echo "堆栈跟踪:\n" . $e->getTraceAsString() . "\n";
|
||||||
|
}
|
||||||
@ -257,7 +257,8 @@ class Order extends Common
|
|||||||
{
|
{
|
||||||
$value = $value ?: ($data['promo_types'] ?? null);
|
$value = $value ?: ($data['promo_types'] ?? null);
|
||||||
|
|
||||||
$promoTypes = array_filter(explode(',', $value));
|
// PHP 8 兼容性:explode() 不接受 null 值
|
||||||
|
$promoTypes = array_filter(explode(',', $value ?? ''));
|
||||||
$texts = [];
|
$texts = [];
|
||||||
$list = (new Activity)->typeList();
|
$list = (new Activity)->typeList();
|
||||||
foreach ($promoTypes as $type) {
|
foreach ($promoTypes as $type) {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user