This commit is contained in:
Billy 2025-10-22 20:32:53 +08:00
parent 2ecb1eca63
commit 3b8735a09d
4 changed files with 480 additions and 145 deletions

View File

@ -27,7 +27,7 @@ class Ccbpayment extends Common
* 不需要登录的方法 (支付回调不需要登录)
* @var array
*/
protected $noNeedLogin = ['notify'];
protected $noNeedLogin = ['notify', 'refundNotify'];
/**
* 不需要权限的方法
@ -206,12 +206,23 @@ class Ccbpayment extends Common
*
* @return void
*/
/**
* 建行生活支付通知接口
*
* 📋 接口说明文档7.1
* - 建行生活主动推送支付结果
* - 不会附带服务方编号
* - 通过 REMARK2 字段识别服务方
* - SIGN 字段使用商户私钥签名,需用建行公钥验签
*
* @return void
*/
public function notify()
{
try {
// 1. 获取原始请求数据
$rawData = file_get_contents('php://input');
Log::info('[建行通知] 收到异步通知: ' . $rawData);
Log::info('[建行支付通知] 收到异步通知: ' . $rawData);
// 2. 解析POST参数
$params = $this->request->post();
@ -222,51 +233,243 @@ class Ccbpayment extends Common
}
// 4. 记录参数
Log::info('[建行通知] 解析参数: ' . json_encode($params, JSON_UNESCAPED_UNICODE));
Log::info('[建行支付通知] 解析参数: ' . json_encode($params, JSON_UNESCAPED_UNICODE));
// 5. 验证必需参数
if (empty($params['ORDERID'])) {
Log::error('[建行通知] 缺少ORDERID参数');
Log::error('[建行支付通知] 缺少ORDERID参数');
exit('FAIL');
}
// 6. 调用支付服务处理通知(返回订单ID)
if (empty($params['SIGN'])) {
Log::error('[建行支付通知] 缺少SIGN签名');
exit('FAIL');
}
if (empty($params['SUCCESS'])) {
Log::error('[建行支付通知] 缺少SUCCESS字段');
exit('FAIL');
}
// 6. ✅ 验证签名(使用建行公钥验签)
try {
$signature = $params['SIGN'];
$verifyParams = $params; // 复制参数用于验签
// 加载建行公钥配置
$configFile = __DIR__ . '/../config/ccblife.php';
if (!file_exists($configFile)) {
throw new \Exception('建行生活配置文件不存在');
}
$config = include $configFile;
// 验签
$verifyResult = \addons\shopro\library\ccblife\CcbRSA::verifyNotify(
$verifyParams,
$signature,
$config['ccb_public_key'] // 建行公钥
);
if (!$verifyResult) {
Log::error('[建行支付通知] 签名验证失败 ORDERID:' . $params['ORDERID']);
exit('FAIL');
}
Log::info('[建行支付通知] 签名验证成功 ORDERID:' . $params['ORDERID']);
} catch (\Exception $e) {
Log::error('[建行支付通知] 签名验证异常: ' . $e->getMessage());
exit('FAIL');
}
// 7. 检查支付状态
if ($params['SUCCESS'] !== 'Y') {
Log::warning('[建行支付通知] 支付未成功 ORDERID:' . $params['ORDERID'] . ' SUCCESS:' . $params['SUCCESS']);
exit('SUCCESS'); // ⚠️ 仍然返回SUCCESS表示通知已接收
}
// 8. 调用支付服务处理通知(返回订单ID)
$result = $this->paymentService->handleNotify($params);
// 7. ✅ 处理成功后更新订单状态到建行(步骤13:调用订单更新接口更新订单状态)
// 9. ✅ 处理成功后更新订单状态到建行(步骤13:调用订单更新接口更新订单状态)
if ($result['status'] === 'success' && !empty($result['order_id'])) {
// ⚠️ 只有新支付才更新,已支付的订单跳过更新
if ($result['already_paid'] === false) {
try {
// 调用订单更新接口,将订单状态从未支付更新为已支付
$updateResult = $this->orderService->updateOrderStatus($result['order_id']);
$updateResult = $this->orderService->updateOrderStatus($result['order_id'], '1'); // 1-支付成功
if ($updateResult['status']) {
Log::info('[建行通知] 订单状态更新成功 order_id:' . $result['order_id']);
Log::info('[建行支付通知] 订单状态更新成功 order_id:' . $result['order_id']);
} else {
// ⚠️ 更新失败不影响本地支付状态,记录日志后续补推
Log::warning('[建行通知] 订单状态更新失败(本地已支付) order_id:' . $result['order_id'] . ' error:' . $updateResult['message']);
Log::warning('[建行支付通知] 订单状态更新失败(本地已支付) order_id:' . $result['order_id'] . ' error:' . $updateResult['message']);
}
} catch (Exception $e) {
// ⚠️ 更新异常不影响支付成功,记录日志后续补推
Log::error('[建行通知] 订单状态更新异常(本地已支付) order_id:' . $result['order_id'] . ' error:' . $e->getMessage());
Log::error('[建行支付通知] 订单状态更新异常(本地已支付) order_id:' . $result['order_id'] . ' error:' . $e->getMessage());
}
} else {
Log::info('[建行通知] 订单已支付且已更新,跳过更新 order_id:' . $result['order_id']);
Log::info('[建行支付通知] 订单已支付且已更新,跳过更新 order_id:' . $result['order_id']);
}
}
// 8. 返回处理结果
// 10. 返回处理结果
// ⚠️ 重要: 必须使用exit直接退出,防止ThinkPHP框架追加额外内容
// 建行要求返回纯文本 'SUCCESS' 或 'FAIL',任何额外字符都会导致建行认为通知失败
$response = ($result['status'] === 'success') ? 'SUCCESS' : 'FAIL';
Log::info('[建行通知] 处理完成,返回: ' . $response);
Log::info('[建行支付通知] 处理完成,返回: ' . $response);
// 直接退出,确保只输出SUCCESS/FAIL
exit($response);
} catch (Exception $e) {
Log::error('[建行通知] 处理失败 error:' . $e->getMessage());
Log::error('[建行支付通知] 处理失败 error:' . $e->getMessage());
// 异常情况也要直接退出
exit('FAIL');
}
}
/**
* 建行生活退款操作通知接口
*
* 📋 接口说明文档7.2
* - 建行生活主动推送退款操作消息
* - 仅推送少量信息(商户号、订单号、退款时间)
* - ⚠️ 不能用于退款结果判断,需通过 A3341TP03 查询接口获取详细信息
* - SIGN 字段使用商户私钥签名,需用建行公钥验签
*
* @return void
*/
public function refundNotify()
{
try {
// 1. 获取原始请求数据
$rawData = file_get_contents('php://input');
Log::info('[建行退款通知] 收到异步通知: ' . $rawData);
// 2. 解析POST参数
$params = $this->request->post();
// 3. 如果POST为空尝试解析原始数据
if (empty($params) && $rawData) {
parse_str($rawData, $params);
}
// 4. 记录参数
Log::info('[建行退款通知] 解析参数: ' . json_encode($params, JSON_UNESCAPED_UNICODE));
// 5. 验证必需参数
if (empty($params['ORDERID'])) {
Log::error('[建行退款通知] 缺少ORDERID参数');
exit('FAIL');
}
if (empty($params['SIGN'])) {
Log::error('[建行退款通知] 缺少SIGN签名');
exit('FAIL');
}
if (empty($params['REFUND_DTM'])) {
Log::error('[建行退款通知] 缺少REFUND_DTM退款时间');
exit('FAIL');
}
// 6. ✅ 验证签名(使用建行公钥验签)
try {
$signature = $params['SIGN'];
$verifyParams = $params;
// 加载建行公钥配置
$configFile = __DIR__ . '/../config/ccblife.php';
if (!file_exists($configFile)) {
throw new \Exception('建行生活配置文件不存在');
}
$config = include $configFile;
// 验签
$verifyResult = \addons\shopro\library\ccblife\CcbRSA::verifyNotify(
$verifyParams,
$signature,
$config['ccb_public_key'] // 建行公钥
);
if (!$verifyResult) {
Log::error('[建行退款通知] 签名验证失败 ORDERID:' . $params['ORDERID']);
exit('FAIL');
}
Log::info('[建行退款通知] 签名验证成功 ORDERID:' . $params['ORDERID']);
} catch (\Exception $e) {
Log::error('[建行退款通知] 签名验证异常: ' . $e->getMessage());
exit('FAIL');
}
// 7. ⚠️ 重要提示:退款通知仅包含少量信息,不能用于退款结果判断
// 需要通过 A3341TP03 订单查询接口获取详细的退款信息
Log::info('[建行退款通知] 退款操作通知 ORDERID:' . $params['ORDERID'] . ' REFUND_DTM:' . $params['REFUND_DTM']);
// 8. 调用订单查询接口获取详细退款信息
try {
$orderSn = $params['ORDERID'];
$refundTime = $params['REFUND_DTM'];
// 计算查询时间范围退款时间前后1天
$refundTimestamp = strtotime($refundTime);
$startTime = date('YmdHis', $refundTimestamp - 86400); // 前1天
$endTime = date('YmdHis', $refundTimestamp + 86400); // 后1天
// 查询退款交易详情
$queryResult = $this->orderService->queryOrder(
$orderSn,
$startTime,
$endTime,
1,
'1', // 交易类型1-退款交易
'00' // 交易状态00-成功
);
if ($queryResult['status']) {
Log::info('[建行退款通知] 查询退款详情成功: ' . json_encode($queryResult['data'], JSON_UNESCAPED_UNICODE));
// 9. 根据查询结果处理本地订单退款状态
// 查找本地订单
$order = Db::name('shopro_order')
->where('pay_flow_id', $orderSn)
->find();
if ($order) {
// 更新订单退款状态到建行
$updateResult = $this->orderService->updateOrderStatus($order['id'], null, '2'); // 2-已退款
if ($updateResult['status']) {
Log::info('[建行退款通知] 订单退款状态更新成功 order_id:' . $order['id']);
} else {
Log::warning('[建行退款通知] 订单退款状态更新失败 order_id:' . $order['id'] . ' error:' . $updateResult['message']);
}
} else {
Log::warning('[建行退款通知] 未找到本地订单 pay_flow_id:' . $orderSn);
}
} else {
Log::error('[建行退款通知] 查询退款详情失败: ' . $queryResult['message']);
}
} catch (\Exception $e) {
Log::error('[建行退款通知] 查询退款详情异常: ' . $e->getMessage());
// ⚠️ 查询失败不影响通知接收仍然返回SUCCESS
}
// 10. 返回处理结果
// ⚠️ 重要: 必须使用exit直接退出,防止ThinkPHP框架追加额外内容
Log::info('[建行退款通知] 处理完成,返回: SUCCESS');
// 直接退出,确保只输出SUCCESS
exit('SUCCESS');
} catch (Exception $e) {
Log::error('[建行退款通知] 处理失败 error:' . $e->getMessage());
// 异常情况也要直接退出
exit('FAIL');

View File

@ -321,85 +321,147 @@ class CcbHttpClient
/**
* 订单状态更新A3341TP02
*
* @param string $userId 用户ID
* @param string $orderId 订单ID
* @param string $orderStatus 订单状态
* @param string $refundStatus 退款状态
* @param string $orderId 订单编号用户订单号对应收银台USER_ORDERID字段
* @param string $informId 通知类型0-支付状态修改 1-退款状态修改)
* @param string $payFlowId 支付流水号对应收银台ORDERID字段
* @param string $payMrchId 支付商户号
* @param array $additionalParams 额外参数PAY_STATUS、REFUND_STATUS、PAY_AMT、DISCOUNT_AMT、CUS_ORDER_URL等
* @return array 响应数据
* @throws \Exception
*/
public function updateOrderStatus($userId, $orderId, $orderStatus, $refundStatus = '0')
public function updateOrderStatus($orderId, $informId, $payFlowId, $payMrchId, $additionalParams = [])
{
// 验证通知类型
if (!in_array($informId, ['0', '1'])) {
throw new \Exception('通知类型INFORM_ID必须为0支付状态修改或1退款状态修改');
}
// 验证互斥规则
if ($informId == '0') {
// 支付状态修改时PAY_STATUS必填REFUND_STATUS为空
if (empty($additionalParams['PAY_STATUS'])) {
throw new \Exception('支付状态修改时PAY_STATUS不能为空');
}
$additionalParams['REFUND_STATUS'] = null;
} elseif ($informId == '1') {
// 退款状态修改时REFUND_STATUS必填PAY_STATUS为空
if (empty($additionalParams['REFUND_STATUS'])) {
throw new \Exception('退款状态修改时REFUND_STATUS不能为空');
}
$additionalParams['PAY_STATUS'] = null;
}
// 构建请求体(必填字段)
$body = [
'USER_ID' => $userId,
'ORDER_ID' => $orderId,
'ORDER_STATUS' => $orderStatus,
'REFUND_STATUS' => $refundStatus
'INFORM_ID' => $informId,
'PAY_FLOW_ID' => $payFlowId,
'PAY_MRCH_ID' => $payMrchId
];
// 合并额外参数
$body = array_merge($body, $additionalParams);
// 移除空值字段
$body = array_filter($body, function($value) {
return $value !== null && $value !== '';
});
return $this->request($this->config['tx_codes']['order_update'], $body);
}
/**
* 订单查询A3341TP03
*
* @param string $onlnPyTxnOrdrId 支付订单ID
* @param string $txnStatus 交易状态
* @param string $onlnPyTxnOrdrId 订单编号调用收银台时支付流水号对应字段ORDERID
* @param string|null $startTime 开始日期时间格式yyyyMMddHHmmss默认为7天前
* @param string|null $endTime 结束日期时间格式yyyyMMddHHmmss默认为当前时间
* @param int $page 当前页次默认1
* @param string $txType 交易类型0-支付交易 1-退款交易 a-查询可退款的订单)
* @param string $txnStatus 交易状态00-交易成功 01-交易失败 02-不确定)
* @param array $additionalParams 额外参数PLAT_MCT_ID、CUSTOMERID、BRANCHID、SCN_IDR等
* @return array 响应数据
* @throws \Exception
*/
public function queryOrder($onlnPyTxnOrdrId, $txnStatus = '00')
public function queryOrder($onlnPyTxnOrdrId, $startTime = null, $endTime = null, $page = 1, $txType = '0', $txnStatus = '00', $additionalParams = [])
{
// 默认查询最近7天
if (empty($startTime)) {
$startTime = date('YmdHis', strtotime('-7 days'));
}
if (empty($endTime)) {
$endTime = date('YmdHis');
}
// 构建请求体(必填字段)
$body = [
'TX_TYPE' => $txType,
'TXN_PRD_TPCD' => '99', // 99-自定义时间段查询(文档要求)
'STDT_TM' => $startTime,
'EDDT_TM' => $endTime,
'ONLN_PY_TXN_ORDR_ID' => $onlnPyTxnOrdrId,
'PAGE' => '1',
'TXN_PRD_TPCD' => '06',
'TXN_STATUS' => $txnStatus,
'TX_TYPE' => '0'
'PAGE' => (string)$page
];
// 合并额外参数(商户信息等)
$body = array_merge($body, $additionalParams);
// 移除空值字段
$body = array_filter($body, function($value) {
return $value !== null && $value !== '';
});
return $this->request($this->config['tx_codes']['order_query'], $body);
}
/**
* 退款接口A3341TP04
*
* @param string $orderId 订单ID
* @param string $refundAmount 退款金额
* @param string $refundReason 退款原因
* @param string $orderId 订单号调用收银台时支付流水号对应字段ORDERID
* @param float|string $refundAmount 退款金额(单位:元)
* @param string|int $payTime 支付时间(时间戳或日期时间字符串)
* @param string|null $refundCode 退款流水号(可选,建议填写用于查询退款结果)
* @param array $additionalParams 额外参数PLAT_MCT_ID、CUSTOMERID、BRANCHID等
* @return array 响应数据
* @throws \Exception
*/
public function refund($orderId, $refundAmount, $refundReason = '')
public function refund($orderId, $refundAmount, $payTime, $refundCode = null, $additionalParams = [])
{
// 计算时间范围支付时间前后4小时
$payTimestamp = is_numeric($payTime) ? $payTime : strtotime($payTime);
if (!$payTimestamp) {
throw new \Exception('支付时间格式错误,请传入时间戳或有效的日期时间字符串');
}
$stat_tm = date('YmdHis', $payTimestamp - 4*3600); // 支付时间往前4小时
$edit_tm = date('YmdHis', min($payTimestamp + 4*3600, time())); // 支付时间往后4小时但不超过当前时间
// 生成退款流水号(如果未提供)
if (empty($refundCode)) {
$refundCode = 'RF' . date('YmdHis') . str_pad(mt_rand(0, 9999), 4, '0', STR_PAD_LEFT);
}
// 格式化退款金额保留2位小数
$refundAmount = number_format((float)$refundAmount, 2, '.', '');
// 构建请求体(必填字段)
$body = [
'ORDER_ID' => $orderId,
'REFUND_AMOUNT' => $refundAmount,
'REFUND_REASON' => $refundReason,
'REFUND_TIME' => date('YmdHis')
'ORDER' => $orderId, // 注意:字段名是 ORDER不是 ORDER_ID
'MONEY' => $refundAmount, // 注意:字段名是 MONEY不是 REFUND_AMOUNT
'STDT_TM' => $stat_tm,
'EDDT_TM' => $edit_tm,
'REFUND_CODE' => $refundCode
];
// 合并额外参数(商户信息等)
$body = array_merge($body, $additionalParams);
// 移除空值字段
$body = array_filter($body, function($value) {
return $value !== null && $value !== '';
});
return $this->request($this->config['tx_codes']['order_refund'], $body);
}
/**
* 测试连接
* 使用查询接口测试连接是否正常
*
* @return bool 是否连接成功
*/
public function testConnection()
{
try {
// 使用一个不存在的订单号进行查询测试
$this->queryOrder('TEST' . time());
return true;
} catch (\Exception $e) {
// 如果是业务错误(订单不存在),说明连接正常
if (strpos($e->getMessage(), '业务处理失败') !== false) {
return true;
}
return false;
}
}
}

View File

@ -128,8 +128,8 @@ class CcbOrderService
* 更新订单状态到建行生活
*
* @param int $orderId 订单ID
* @param string $status 订单状态
* @param string $refundStatus 退款状态
* @param string|null $status 支付状态0-待支付 1-支付成功 2-已过期 3-支付失败 4-取消)
* @param string|null $refundStatus 退款状态0-无退款 1-退款申请 2-已退款 3-部分退款)
* @return array
*/
public function updateOrderStatus($orderId, $status = null, $refundStatus = null)
@ -138,7 +138,7 @@ class CcbOrderService
$txSeq = CcbMD5::generateTransactionSeq();
try {
// 获取订单信息
// 获取订单信息(包含支付流水号)
$order = Db::name('shopro_order')
->alias('o')
->join('user u', 'o.user_id = u.id', 'LEFT')
@ -150,31 +150,60 @@ class CcbOrderService
throw new \Exception('订单不存在');
}
// 获取建行用户ID
$ccbUserId = $order['ccb_user_id'];
if (!$ccbUserId) {
throw new \Exception('用户未绑定建行生活账号');
// 获取支付流水号
$payFlowId = $order['pay_flow_id'] ?? null;
if (empty($payFlowId)) {
throw new \Exception('订单缺少支付流水号,无法更新状态到建行');
}
// 获取支付商户号
$payMrchId = $this->config['merchant_id'] ?? null;
if (empty($payMrchId)) {
throw new \Exception('配置中缺少支付商户号merchant_id');
}
// 映射订单状态
$orderStatus = $status ?: $this->mapOrderStatus($order['status']);
$refundStatus = $refundStatus ?: $this->mapRefundStatus($order['refund_status'] ?? 0);
$payStatus = $status ?: $this->mapOrderStatus($order['status']);
$mappedRefundStatus = $refundStatus ?: $this->mapRefundStatus($order['refund_status'] ?? 0);
// 确定通知类型0-支付状态修改 1-退款状态修改)
$informId = !empty($refundStatus) && $refundStatus != '0' ? '1' : '0';
// 构建额外参数
$additionalParams = [];
if ($informId == '0') {
// 支付状态修改
$additionalParams['PAY_STATUS'] = $payStatus;
$additionalParams['PAY_AMT'] = number_format($order['pay_fee'] ?? 0, 2, '.', '');
} else {
// 退款状态修改
$additionalParams['REFUND_STATUS'] = $mappedRefundStatus;
$additionalParams['TOTAL_REFUND_AMT'] = number_format($order['refund_fee'] ?? 0, 2, '.', '');
}
// 添加其他可选参数
$additionalParams['DISCOUNT_AMT'] = number_format($order['total_discount_fee'] ?? 0, 2, '.', '');
if (!empty($order['goods_name'])) {
$additionalParams['GOODS_NM'] = mb_substr($order['goods_name'], 0, 200);
}
// 记录请求
$requestData = [
'ccb_user_id' => $ccbUserId,
'order_sn' => $order['order_sn'],
'order_status' => $orderStatus,
'refund_status' => $refundStatus
'order_id' => $order['order_sn'],
'inform_id' => $informId,
'pay_flow_id' => $payFlowId,
'pay_mrch_id' => $payMrchId,
'additional_params' => $additionalParams
];
$this->recordSyncLog($orderId, 'A3341TP02', $txSeq, $requestData, 'request');
// 调用建行API更新状态
// 调用建行API更新状态(使用新接口)
$response = $this->httpClient->updateOrderStatus(
$ccbUserId,
$order['order_sn'],
$orderStatus,
$refundStatus
$order['order_sn'], // 订单编号
$informId, // 通知类型
$payFlowId, // 支付流水号
$payMrchId, // 支付商户号
$additionalParams // 额外参数
);
// 记录响应
@ -203,14 +232,26 @@ class CcbOrderService
/**
* 查询建行订单信息
*
* @param string $orderSn 订单号
* @param string $orderSn 订单号支付流水号对应收银台ORDERID字段
* @param string|null $startTime 开始时间格式yyyyMMddHHmmss默认7天前
* @param string|null $endTime 结束时间格式yyyyMMddHHmmss默认当前时间
* @param int $page 页码默认1
* @param string $txType 交易类型0-支付交易 1-退款交易 a-查询可退款订单)
* @param string $txnStatus 交易状态00-成功 01-失败 02-不确定)
* @return array
*/
public function queryOrder($orderSn)
public function queryOrder($orderSn, $startTime = null, $endTime = null, $page = 1, $txType = '0', $txnStatus = '00')
{
try {
// 调用建行API查询订单
$response = $this->httpClient->queryOrder($orderSn);
// 调用建行API查询订单使用新接口
$response = $this->httpClient->queryOrder(
$orderSn,
$startTime,
$endTime,
$page,
$txType,
$txnStatus
);
return [
'status' => true,
@ -232,14 +273,14 @@ class CcbOrderService
* 处理订单退款
*
* @param int $orderId 订单ID
* @param float $refundAmount 退款金额
* @param string $refundReason 退款原因
* @param float $refundAmount 退款金额(单位:元)
* @param string|null $refundCode 退款流水号(可选,建议填写用于查询退款结果)
* @return array
*/
public function refundOrder($orderId, $refundAmount, $refundReason = '')
public function refundOrder($orderId, $refundAmount, $refundCode = null)
{
try {
// 获取订单信息
// 获取订单信息(需要支付流水号和支付时间)
$order = Db::name('shopro_order')->where('id', $orderId)->find();
if (!$order) {
throw new \Exception('订单不存在');
@ -250,11 +291,24 @@ class CcbOrderService
throw new \Exception('退款金额不能超过订单总额');
}
// 调用建行API发起退款
// 获取支付流水号(必须)
$payFlowId = $order['pay_flow_id'] ?? null;
if (empty($payFlowId)) {
throw new \Exception('订单缺少支付流水号,无法执行退款');
}
// 获取支付时间(用于计算查询时间范围)
$payTime = $order['pay_time'] ?? $order['createtime'];
if (empty($payTime)) {
throw new \Exception('订单缺少支付时间,无法执行退款');
}
// 调用建行API发起退款使用新接口
$response = $this->httpClient->refund(
$order['order_sn'],
number_format($refundAmount, 2, '.', ''),
$refundReason
$payFlowId, // 支付流水号对应收银台ORDERID
$refundAmount, // 退款金额
$payTime, // 支付时间(用于计算查询时间范围)
$refundCode // 退款流水号(可选)
);
// 更新订单退款状态
@ -559,62 +613,4 @@ class CcbOrderService
if ($refundStatus == 2) return '2'; // 已退款
return '0';
}
/**
* 批量同步订单
* 用于初始化或定时同步
*
* @param array $conditions 查询条件
* @param int $limit 批量数量
* @return array
*/
public function batchSync($conditions = [], $limit = 100)
{
$successCount = 0;
$failCount = 0;
$errors = [];
try {
// 构建查询
$query = Db::name('shopro_order');
if (!empty($conditions)) {
$query->where($conditions);
}
// 查询需要同步的订单
$orders = $query->where('status', '<>', 'cancelled')
->where('ccb_sync_status', 'in', [0, 2]) // 未同步或同步失败的
->limit($limit)
->select();
foreach ($orders as $order) {
$result = $this->pushOrder($order['id']);
if ($result['status']) {
$successCount++;
} else {
$failCount++;
$errors[] = "订单{$order['order_sn']}: {$result['message']}";
}
}
return [
'status' => true,
'message' => "批量同步完成",
'data' => [
'total' => count($orders),
'success' => $successCount,
'fail' => $failCount,
'errors' => $errors
]
];
} catch (\Exception $e) {
return [
'status' => false,
'message' => '批量同步失败: ' . $e->getMessage(),
'data' => null
];
}
}
}

View File

@ -239,4 +239,78 @@ class CcbRSA
'private_key' => $privateKey
];
}
/**
* RSA公钥验签用于建行回调通知
*
* 建行回调通知中的 SIGN 字段是使用商户私钥签名的,
* 服务方需要使用建行公钥进行验签
*
* @param string $data 待验签的原始数据
* @param string $signature 签名字符串(十六进制)
* @param string $publicKey 建行公钥BASE64编码
* @return bool 验签是否成功
* @throws \Exception
*/
public static function verify($data, $signature, $publicKey)
{
// 格式化公钥
$publicKey = self::formatPublicKey($publicKey);
// 加载公钥资源
$pubKey = openssl_pkey_get_public($publicKey);
if (!$pubKey) {
throw new \Exception('公钥格式错误: ' . openssl_error_string());
}
// 将十六进制签名转换为二进制
$signatureBinary = hex2bin($signature);
if ($signatureBinary === false) {
throw new \Exception('签名格式错误:无法从十六进制转换');
}
// 使用公钥验签SHA256算法
$result = openssl_verify($data, $signatureBinary, $pubKey, OPENSSL_ALGO_SHA256);
openssl_free_key($pubKey);
if ($result === 1) {
return true; // 验签成功
} elseif ($result === 0) {
return false; // 验签失败
} else {
throw new \Exception('验签过程出错: ' . openssl_error_string());
}
}
/**
* 建行通知验签(针对回调通知)
*
* 用于验证建行支付通知和退款通知的签名
*
* @param array $params 通知参数不包含SIGN字段
* @param string $signature SIGN字段的值
* @param string $ccbPublicKey 建行公钥
* @return bool 验签是否成功
* @throws \Exception
*/
public static function verifyNotify($params, $signature, $ccbPublicKey)
{
// 移除 SIGN 字段(如果存在)
unset($params['SIGN']);
// 按照建行规范拼接验签字符串
// 格式将参数按字典序排列后拼接key=value&key=value
ksort($params);
$signStr = '';
foreach ($params as $key => $value) {
if ($value !== '' && $value !== null) {
$signStr .= $key . '=' . $value . '&';
}
}
$signStr = rtrim($signStr, '&');
// 调用验签方法
return self::verify($signStr, $signature, $ccbPublicKey);
}
}