2025-10-17 16:32:16 +08:00
|
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
|
|
namespace addons\shopro\library\ccblife;
|
|
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
use app\admin\model\shopro\order\Order;
|
|
|
|
|
|
use think\Db;
|
|
|
|
|
|
use think\Log;
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 建行生活支付服务类
|
|
|
|
|
|
* 处理支付串生成、支付回调、支付验证等业务
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
|
|
|
|
|
class CcbPaymentService
|
|
|
|
|
|
{
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 配置信息
|
|
|
|
|
|
*/
|
|
|
|
|
|
private $config;
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 订单服务实例
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-17 17:18:15 +08:00
|
|
|
|
private $orderService;
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 构造函数
|
|
|
|
|
|
*/
|
2025-10-17 17:18:15 +08:00
|
|
|
|
public function __construct()
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
2025-10-18 15:47:25 +08:00
|
|
|
|
// 加载插件配置文件
|
|
|
|
|
|
$configFile = __DIR__ . '/../../config/ccblife.php';
|
|
|
|
|
|
if (file_exists($configFile)) {
|
|
|
|
|
|
$this->config = include $configFile;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new \Exception('建行生活配置文件不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-21 10:17:40 +08:00
|
|
|
|
// ✅ 修复: 删除processPemKeys()调用
|
|
|
|
|
|
// 密钥格式化统一由CcbRSA类处理,避免重复格式化导致OpenSSL ASN1解析错误
|
|
|
|
|
|
// CcbRSA::formatPublicKey/formatPrivateKey 会在加密/解密时自动处理密钥格式
|
2025-10-18 15:47:25 +08:00
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
$this->orderService = new CcbOrderService();
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 生成建行支付串
|
|
|
|
|
|
* 用于前端JSBridge调用建行收银台
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*
|
2025-10-18 01:16:25 +08:00
|
|
|
|
* ⚠️ 注意:必须包含所有必需参数,签名前按ASCII排序
|
|
|
|
|
|
*
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @param int $orderId Shopro订单ID
|
2025-10-21 14:33:20 +08:00
|
|
|
|
* @param string $payFlowId 支付流水号(由控制器统一生成)
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @return array ['status' => bool, 'message' => string, 'data' => array]
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-21 14:33:20 +08:00
|
|
|
|
public function generatePaymentString($orderId, $payFlowId)
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
2025-10-20 15:51:06 +08:00
|
|
|
|
// ⚠️ 开启事务保护,确保数据一致性
|
|
|
|
|
|
Db::startTrans();
|
|
|
|
|
|
|
2025-10-17 16:32:16 +08:00
|
|
|
|
try {
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 获取订单信息
|
|
|
|
|
|
$order = Order::find($orderId);
|
|
|
|
|
|
if (!$order) {
|
|
|
|
|
|
throw new \Exception('订单不存在');
|
|
|
|
|
|
}
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 检查订单状态
|
|
|
|
|
|
if ($order['status'] != 'unpaid') {
|
|
|
|
|
|
throw new \Exception('订单状态不正确');
|
|
|
|
|
|
}
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-21 14:33:20 +08:00
|
|
|
|
// 获取用户建行生活ID(用于订单推送)
|
|
|
|
|
|
$user = Db::name('user')->where('id', $order['user_id'])
|
|
|
|
|
|
->field('ccb_user_id')
|
|
|
|
|
|
->find();
|
2025-10-17 17:18:15 +08:00
|
|
|
|
if (empty($user['ccb_user_id'])) {
|
2025-10-21 14:33:20 +08:00
|
|
|
|
throw new \Exception('用户未绑定建行生活账号');
|
2025-10-17 17:18:15 +08:00
|
|
|
|
}
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-21 14:33:20 +08:00
|
|
|
|
// ✅ 使用控制器传入的统一支付流水号(确保与订单推送使用同一流水号)
|
|
|
|
|
|
if (empty($payFlowId)) {
|
|
|
|
|
|
throw new \Exception('支付流水号不能为空');
|
|
|
|
|
|
}
|
2025-10-18 01:16:25 +08:00
|
|
|
|
|
2025-10-22 21:42:09 +08:00
|
|
|
|
// ⚠️ 关键:建行要求参数按照文档表格定义的顺序拼接,不是ASCII排序!
|
|
|
|
|
|
// 根据建行文档4.1和4.2,必须严格按照参数表顺序构建签名字符串
|
|
|
|
|
|
|
|
|
|
|
|
// 1. 定义参与MAC签名的参数数组(按文档表格顺序)
|
|
|
|
|
|
$macParams = [];
|
|
|
|
|
|
|
|
|
|
|
|
// 1.1 商户信息(必填,二选一:建行商户号组合 或 外部平台商户号)
|
|
|
|
|
|
$usePlatMctId = !empty($this->config['plat_mct_id']);
|
|
|
|
|
|
if ($usePlatMctId) {
|
|
|
|
|
|
// 使用外部平台商户号
|
|
|
|
|
|
$macParams['PLATMCTID'] = $this->config['plat_mct_id'];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 使用建行商户号组合
|
|
|
|
|
|
$macParams['MERCHANTID'] = $this->config['merchant_id'];
|
|
|
|
|
|
$macParams['POSID'] = $this->config['pos_id'];
|
|
|
|
|
|
$macParams['BRANCHID'] = $this->config['branch_id'];
|
2025-10-21 14:33:20 +08:00
|
|
|
|
}
|
2025-10-22 21:42:09 +08:00
|
|
|
|
|
|
|
|
|
|
// 1.2 订单信息(必填)
|
|
|
|
|
|
$macParams['ORDERID'] = $payFlowId; // 支付流水号
|
|
|
|
|
|
$macParams['USER_ORDERID'] = $order['order_sn']; // 用户订单号
|
2025-10-23 16:00:25 +08:00
|
|
|
|
$payment = number_format($order['pay_fee'], 2, '.', '');
|
|
|
|
|
|
$macParams['PAYMENT'] = $payment; // 支付金额
|
2025-10-22 21:42:09 +08:00
|
|
|
|
$macParams['CURCODE'] = '01'; // 币种(01=人民币)
|
|
|
|
|
|
$macParams['TXCODE'] = '520100'; // 交易码
|
|
|
|
|
|
$macParams['REMARK1'] = ''; // 备注1(空字符串也要传)
|
|
|
|
|
|
$macParams['REMARK2'] = $this->config['service_id']; // 备注2(服务方编号)
|
|
|
|
|
|
$macParams['TYPE'] = '1'; // 接口类型(1=防钓鱼)
|
|
|
|
|
|
$macParams['GATEWAY'] = '0'; // 网关类型
|
|
|
|
|
|
$macParams['CLIENTIP'] = ''; // 客户端IP(建行生活环境送空)
|
|
|
|
|
|
$macParams['REGINFO'] = ''; // 客户注册信息(空字符串)
|
2025-10-23 16:00:25 +08:00
|
|
|
|
|
|
|
|
|
|
// 商品信息(escape编码)
|
|
|
|
|
|
$proinfo = $this->buildProductInfo($order);
|
|
|
|
|
|
$macParams['PROINFO'] = $proinfo;
|
|
|
|
|
|
|
2025-10-22 21:42:09 +08:00
|
|
|
|
$macParams['REFERER'] = ''; // 商户URL(空字符串)
|
2025-10-23 16:00:25 +08:00
|
|
|
|
|
2025-10-23 17:00:15 +08:00
|
|
|
|
// ⚠️ 关键修复:INSTALLNUM必须在THIRDAPPINFO之前(严格按照文档4.1表格顺序)
|
2025-10-22 21:42:09 +08:00
|
|
|
|
// 1.3 可选参数(按文档表格顺序,有值才参与MAC)
|
2025-10-23 17:00:15 +08:00
|
|
|
|
// 注意:根据文档4.2,橙色字段有值时才参与MAC,空值不参与
|
2025-10-22 21:42:09 +08:00
|
|
|
|
|
2025-10-23 17:00:15 +08:00
|
|
|
|
// 分期期数(在REFERER之后,THIRDAPPINFO之前)
|
2025-10-22 21:42:09 +08:00
|
|
|
|
if (!empty($this->config['install_num'])) {
|
|
|
|
|
|
$macParams['INSTALLNUM'] = $this->config['install_num'];
|
2025-10-21 14:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-23 17:00:15 +08:00
|
|
|
|
$macParams['THIRDAPPINFO'] = 'comccbpay1234567890cloudmerchant'; // 客户端标识(固定值)
|
|
|
|
|
|
|
|
|
|
|
|
// 记录关键参数
|
|
|
|
|
|
Log::info('[建行支付] 关键参数 order_id:' . $orderId . ' pay_flow_id:' . $payFlowId . ' user_orderid:' . $order['order_sn'] . ' payment:' . $payment);
|
|
|
|
|
|
Log::info('[建行支付] 商户信息 merchant_id:' . ($macParams['MERCHANTID'] ?? 'N/A') . ' pos_id:' . ($macParams['POSID'] ?? 'N/A') . ' branch_id:' . ($macParams['BRANCHID'] ?? 'N/A'));
|
|
|
|
|
|
Log::info('[建行支付] 商品信息 proinfo:' . $proinfo);
|
|
|
|
|
|
|
2025-10-22 21:42:09 +08:00
|
|
|
|
// 超时时间
|
|
|
|
|
|
if (!empty($this->config['timeout'])) {
|
|
|
|
|
|
$macParams['TIMEOUT'] = $this->config['timeout'];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 默认30分钟超时
|
|
|
|
|
|
$macParams['TIMEOUT'] = date('YmdHis', strtotime('+30 minutes'));
|
2025-10-21 14:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-22 21:42:09 +08:00
|
|
|
|
// 中国建设银行App环境参数
|
|
|
|
|
|
if (!empty($this->config['user_id'])) {
|
|
|
|
|
|
$macParams['USERID'] = $this->config['user_id'];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!empty($this->config['token'])) {
|
|
|
|
|
|
$macParams['TOKEN'] = $this->config['token'];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!empty($this->config['pay_success_url'])) {
|
2025-10-23 00:55:42 +08:00
|
|
|
|
// ⚠️ 不要在这里urlencode,会在构建最终支付串时统一编码
|
|
|
|
|
|
$macParams['PAYSUCCESSURL'] = $this->config['pay_success_url'];
|
2025-10-22 21:42:09 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 支付位图和账户位图
|
|
|
|
|
|
if (!empty($this->config['pay_bitmap'])) {
|
|
|
|
|
|
$macParams['PAYBITMAP'] = $this->config['pay_bitmap'];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!empty($this->config['account_bitmap'])) {
|
|
|
|
|
|
$macParams['ACCOUNTBITMAP'] = $this->config['account_bitmap'];
|
2025-10-21 14:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-22 21:42:09 +08:00
|
|
|
|
// 积分相关
|
2025-10-21 14:33:20 +08:00
|
|
|
|
if (!empty($this->config['point_avy_id'])) {
|
2025-10-22 21:42:09 +08:00
|
|
|
|
$macParams['POINTAVYID'] = $this->config['point_avy_id'];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!empty($this->config['fixed_point_val'])) {
|
|
|
|
|
|
$macParams['FIXEDPOINTVAL'] = $this->config['fixed_point_val'];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!empty($this->config['min_point_limit'])) {
|
|
|
|
|
|
$macParams['MINPOINTLIMIT'] = $this->config['min_point_limit'];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 有价券相关
|
|
|
|
|
|
if (!empty($this->config['coupon_avy_id'])) {
|
|
|
|
|
|
$macParams['COUPONAVYID'] = $this->config['coupon_avy_id'];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!empty($this->config['only_credit_pay_flag'])) {
|
|
|
|
|
|
$macParams['ONLY_CREDIT_PAY_FLAG'] = $this->config['only_credit_pay_flag'];
|
2025-10-21 14:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 数字人民币参数
|
|
|
|
|
|
if (!empty($this->config['dcep_mct_type'])) {
|
2025-10-22 21:42:09 +08:00
|
|
|
|
$macParams['DCEP_MCT_TYPE'] = $this->config['dcep_mct_type'];
|
2025-10-21 14:33:20 +08:00
|
|
|
|
if ($this->config['dcep_mct_type'] == '2') {
|
|
|
|
|
|
// 非融合商户需要填写数币商户号
|
2025-10-22 21:42:09 +08:00
|
|
|
|
if (!empty($this->config['dcep_merchant_id'])) {
|
|
|
|
|
|
$macParams['DCEP_MERCHANTID'] = $this->config['dcep_merchant_id'];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!empty($this->config['dcep_pos_id'])) {
|
|
|
|
|
|
$macParams['DCEP_POSID'] = $this->config['dcep_pos_id'];
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!empty($this->config['dcep_branch_id'])) {
|
|
|
|
|
|
$macParams['DCEP_BRANCHID'] = $this->config['dcep_branch_id'];
|
|
|
|
|
|
}
|
2025-10-21 14:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (!empty($this->config['dcep_dep_acc_no'])) {
|
2025-10-22 21:42:09 +08:00
|
|
|
|
$macParams['DCEPDEPACCNO'] = $this->config['dcep_dep_acc_no'];
|
2025-10-21 14:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-22 21:42:09 +08:00
|
|
|
|
// 二级商户参数
|
|
|
|
|
|
if (!empty($this->config['sub_mct_id'])) {
|
|
|
|
|
|
$macParams['SUB_MCT_ID'] = $this->config['sub_mct_id'];
|
2025-10-21 14:33:20 +08:00
|
|
|
|
}
|
2025-10-22 21:42:09 +08:00
|
|
|
|
if (!empty($this->config['sub_mct_name'])) {
|
|
|
|
|
|
$macParams['SUB_MCT_NAME'] = $this->config['sub_mct_name'];
|
2025-10-21 14:33:20 +08:00
|
|
|
|
}
|
2025-10-22 21:42:09 +08:00
|
|
|
|
if (!empty($this->config['sub_mct_mcc'])) {
|
|
|
|
|
|
$macParams['SUB_MCT_MCC'] = $this->config['sub_mct_mcc'];
|
2025-10-21 14:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-22 21:42:09 +08:00
|
|
|
|
// 扩展域
|
2025-10-21 14:33:20 +08:00
|
|
|
|
if (!empty($this->config['extend_params'])) {
|
2025-10-23 00:55:42 +08:00
|
|
|
|
// ⚠️ 不要在这里urlencode,会在构建最终支付串时统一编码
|
|
|
|
|
|
$macParams['EXTENDPARAMS'] = $this->config['extend_params'];
|
2025-10-21 14:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-22 21:42:09 +08:00
|
|
|
|
// 特殊字段(中石化专用)
|
|
|
|
|
|
if (!empty($this->config['identity_code'])) {
|
|
|
|
|
|
$macParams['IDENTITYCODE'] = $this->config['identity_code'];
|
2025-10-21 14:33:20 +08:00
|
|
|
|
}
|
2025-10-22 21:42:09 +08:00
|
|
|
|
if (!empty($this->config['notify_url'])) {
|
2025-10-23 00:55:42 +08:00
|
|
|
|
// ⚠️ 不要在这里urlencode,会在构建最终支付串时统一编码
|
|
|
|
|
|
$macParams['NOTIFY_URL'] = $this->config['notify_url'];
|
2025-10-21 14:33:20 +08:00
|
|
|
|
}
|
2025-10-22 21:42:09 +08:00
|
|
|
|
|
2025-10-23 00:55:42 +08:00
|
|
|
|
// 2. 手动构建签名字符串(不能用http_build_query,避免URL编码破坏escape格式)
|
|
|
|
|
|
// ⚠️ 关键:按照建行文档4.2示例,签名字符串不进行URL编码
|
|
|
|
|
|
$signParts = [];
|
|
|
|
|
|
foreach ($macParams as $key => $value) {
|
|
|
|
|
|
$signParts[] = $key . '=' . $value;
|
|
|
|
|
|
}
|
|
|
|
|
|
$signString = implode('&', $signParts);
|
2025-10-22 21:42:09 +08:00
|
|
|
|
|
|
|
|
|
|
// 3. 添加PLATFORMPUB参与MD5签名(但不作为HTTP参数传递)
|
|
|
|
|
|
$platformPubKey = $this->config['public_key']; // 服务方公钥
|
|
|
|
|
|
$macSignString = $signString . '&PLATFORMPUB=' . $platformPubKey;
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 生成MAC签名(32位小写MD5)
|
|
|
|
|
|
$mac = strtolower(md5($macSignString));
|
|
|
|
|
|
|
2025-10-23 00:55:42 +08:00
|
|
|
|
Log::info('[建行支付] MAC签名字符串(前500字符): ' . mb_substr($macSignString, 0, 500));
|
|
|
|
|
|
Log::info('[建行支付] 生成MAC: ' . $mac);
|
|
|
|
|
|
|
2025-10-22 21:42:09 +08:00
|
|
|
|
// 5. 构建不参与MAC的参数
|
|
|
|
|
|
$nonMacParams = [];
|
|
|
|
|
|
|
|
|
|
|
|
// 微信支付19位终端号(不参与MAC校验)
|
|
|
|
|
|
if (!empty($this->config['pos_id_19'])) {
|
|
|
|
|
|
$nonMacParams['POSID19'] = $this->config['pos_id_19'];
|
2025-10-21 14:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 场景编号(埋点使用,不参与MAC校验)
|
|
|
|
|
|
if (!empty($this->config['scn_id'])) {
|
2025-10-22 21:42:09 +08:00
|
|
|
|
$nonMacParams['SCNID'] = $this->config['scn_id'];
|
2025-10-21 14:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
if (!empty($this->config['scn_pltfrm_id'])) {
|
2025-10-22 21:42:09 +08:00
|
|
|
|
$nonMacParams['SCN_PLTFRM_ID'] = $this->config['scn_pltfrm_id'];
|
2025-10-21 14:33:20 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-22 21:42:09 +08:00
|
|
|
|
// 6. 生成ENCPUB(商户公钥密文,不参与MAC校验)
|
|
|
|
|
|
$encpub = $this->encryptPublicKeyLast30();
|
2025-10-18 01:16:25 +08:00
|
|
|
|
|
2025-10-23 00:55:42 +08:00
|
|
|
|
// 7. 组装最终支付串(传给建行收银台的URL参数)
|
|
|
|
|
|
// ⚠️ 注意:最终支付串需要对特殊字符进行URL编码
|
2025-10-22 21:42:09 +08:00
|
|
|
|
// 格式:参与MAC的参数 + 不参与MAC的参数 + MAC + PLATFORMID + ENCPUB
|
2025-10-20 15:29:15 +08:00
|
|
|
|
|
2025-10-23 00:55:42 +08:00
|
|
|
|
// 7.1 构建参与MAC的参数部分(需要URL编码)
|
|
|
|
|
|
$finalParts = [];
|
|
|
|
|
|
foreach ($macParams as $key => $value) {
|
|
|
|
|
|
// 对值进行URL编码(建行收银台需要)
|
|
|
|
|
|
$finalParts[] = $key . '=' . urlencode($value);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 7.2 添加不参与MAC的参数
|
2025-10-22 21:42:09 +08:00
|
|
|
|
if (!empty($nonMacParams)) {
|
2025-10-23 00:55:42 +08:00
|
|
|
|
foreach ($nonMacParams as $key => $value) {
|
|
|
|
|
|
$finalParts[] = $key . '=' . urlencode($value);
|
|
|
|
|
|
}
|
2025-10-22 21:42:09 +08:00
|
|
|
|
}
|
2025-10-18 01:16:25 +08:00
|
|
|
|
|
2025-10-23 00:55:42 +08:00
|
|
|
|
// 7.3 添加MAC、PLATFORMID、ENCPUB(ENCPUB已经是base64,需要URL编码)
|
|
|
|
|
|
$finalParts[] = 'MAC=' . $mac;
|
|
|
|
|
|
$finalParts[] = 'PLATFORMID=' . $this->config['service_id'];
|
|
|
|
|
|
$finalParts[] = 'ENCPUB=' . urlencode($encpub);
|
|
|
|
|
|
|
|
|
|
|
|
// 7.4 拼接最终支付串
|
|
|
|
|
|
$finalPaymentString = implode('&', $finalParts);
|
|
|
|
|
|
|
|
|
|
|
|
Log::info('[建行支付] 最终支付串(前500字符): ' . mb_substr($finalPaymentString, 0, 500));
|
2025-10-18 01:16:25 +08:00
|
|
|
|
|
|
|
|
|
|
// 保存支付流水号到订单
|
|
|
|
|
|
Order::where('id', $orderId)->update([
|
|
|
|
|
|
'ccb_pay_flow_id' => $payFlowId,
|
|
|
|
|
|
'updatetime' => time()
|
|
|
|
|
|
]);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 记录支付请求
|
2025-10-18 01:16:25 +08:00
|
|
|
|
$this->recordPaymentRequest($orderId, [
|
|
|
|
|
|
'payment_string' => $finalPaymentString,
|
2025-10-22 21:43:28 +08:00
|
|
|
|
'params' => $macParams,
|
2025-10-18 01:16:25 +08:00
|
|
|
|
'mac' => $mac,
|
|
|
|
|
|
'pay_flow_id' => $payFlowId
|
|
|
|
|
|
]);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-20 15:51:06 +08:00
|
|
|
|
// ✅ 提交事务
|
|
|
|
|
|
Db::commit();
|
|
|
|
|
|
|
2025-10-17 16:32:16 +08:00
|
|
|
|
return [
|
2025-10-17 17:18:15 +08:00
|
|
|
|
'status' => true,
|
|
|
|
|
|
'message' => '支付串生成成功',
|
|
|
|
|
|
'data' => [
|
2025-10-18 01:16:25 +08:00
|
|
|
|
'payment_string' => $finalPaymentString,
|
|
|
|
|
|
'mac' => $mac,
|
2025-10-17 17:18:15 +08:00
|
|
|
|
'order_sn' => $order['order_sn'],
|
2025-10-18 01:16:25 +08:00
|
|
|
|
'pay_flow_id' => $payFlowId,
|
2025-10-20 15:51:06 +08:00
|
|
|
|
'amount' => number_format($order['pay_fee'], 2, '.', ''),
|
|
|
|
|
|
'order_id' => $orderId // 补充order_id字段
|
2025-10-17 17:18:15 +08:00
|
|
|
|
]
|
2025-10-17 16:32:16 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
} catch (\Exception $e) {
|
2025-10-20 15:51:06 +08:00
|
|
|
|
// ❌ 回滚事务
|
|
|
|
|
|
Db::rollback();
|
|
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
Log::error('建行支付串生成失败: ' . $e->getMessage());
|
2025-10-17 16:32:16 +08:00
|
|
|
|
return [
|
2025-10-17 17:18:15 +08:00
|
|
|
|
'status' => false,
|
|
|
|
|
|
'message' => $e->getMessage(),
|
|
|
|
|
|
'data' => null
|
2025-10-17 16:32:16 +08:00
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-18 15:47:25 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 获取客户端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';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-21 14:33:20 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 实现JavaScript的escape()编码
|
|
|
|
|
|
* 用于REGINFO和PROINFO字段的中文编码
|
|
|
|
|
|
*
|
|
|
|
|
|
* 根据建行文档4.3:
|
|
|
|
|
|
* "使用js的escape()方法对REGINFO(客户注册信息)和PROINFO(商品信息)进行编码"
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param string $str 要编码的字符串
|
|
|
|
|
|
* @return string 编码后的字符串
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function jsEscape($str)
|
|
|
|
|
|
{
|
|
|
|
|
|
if (empty($str)) {
|
|
|
|
|
|
return '';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$result = '';
|
|
|
|
|
|
$length = mb_strlen($str, 'UTF-8');
|
|
|
|
|
|
|
|
|
|
|
|
for ($i = 0; $i < $length; $i++) {
|
|
|
|
|
|
$char = mb_substr($str, $i, 1, 'UTF-8');
|
|
|
|
|
|
|
|
|
|
|
|
// ASCII字符(数字、字母、部分符号)不编码
|
|
|
|
|
|
if (preg_match('/^[A-Za-z0-9@*_+\-.\\/]$/', $char)) {
|
|
|
|
|
|
$result .= $char;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 非ASCII字符转为 %uXXXX 格式(如:小 -> %u5C0F)
|
|
|
|
|
|
$unicode = mb_ord($char, 'UTF-8');
|
|
|
|
|
|
$result .= '%u' . strtoupper(str_pad(dechex($unicode), 4, '0', STR_PAD_LEFT));
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-18 01:16:25 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 构建商品信息字符串
|
2025-10-21 14:33:20 +08:00
|
|
|
|
* 根据建行文档4.3:商品信息中文需使用escape()编码
|
2025-10-18 01:16:25 +08:00
|
|
|
|
*
|
|
|
|
|
|
* @param object $order 订单对象
|
2025-10-21 14:33:20 +08:00
|
|
|
|
* @return string escape编码后的商品信息
|
2025-10-18 01:16:25 +08:00
|
|
|
|
*/
|
|
|
|
|
|
private function buildProductInfo($order)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 获取订单商品
|
|
|
|
|
|
$orderItems = Db::name('shopro_order_item')
|
|
|
|
|
|
->where('order_id', $order['id'])
|
|
|
|
|
|
->limit(3) // 最多取3个商品
|
|
|
|
|
|
->column('goods_title');
|
|
|
|
|
|
|
|
|
|
|
|
if (empty($orderItems)) {
|
2025-10-21 14:33:20 +08:00
|
|
|
|
$productInfo = '商城订单';
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 拼接商品名称,建议不超过50字符(编码前)
|
|
|
|
|
|
$productInfo = mb_substr(implode(',', $orderItems), 0, 50, 'UTF-8');
|
2025-10-18 01:16:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-21 14:33:20 +08:00
|
|
|
|
// ✅ 使用JavaScript的escape()编码中文
|
|
|
|
|
|
return $this->jsEscape($productInfo);
|
2025-10-18 01:16:25 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-21 10:17:40 +08:00
|
|
|
|
/**
|
2025-10-23 17:00:15 +08:00
|
|
|
|
* 生成ENCPUB(商户公钥密文)
|
2025-10-21 10:17:40 +08:00
|
|
|
|
*
|
2025-10-23 17:00:15 +08:00
|
|
|
|
* 根据建行文档4.4:
|
|
|
|
|
|
* "使用服务方公钥对商户公钥后30位进行RSA加密,再进行base64后,生成的密文串"
|
|
|
|
|
|
*
|
|
|
|
|
|
* 注意:这里的"商户公钥后30位"是指RSA公钥模数(modulus)的十六进制表示的后30位
|
|
|
|
|
|
* 不是BASE64字符串的后30位!
|
2025-10-21 10:17:40 +08:00
|
|
|
|
*
|
|
|
|
|
|
* @return string BASE64编码的加密密文
|
|
|
|
|
|
* @throws \Exception
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function encryptPublicKeyLast30()
|
|
|
|
|
|
{
|
2025-10-23 17:00:15 +08:00
|
|
|
|
// 获取商户公钥(被加密的公钥)
|
|
|
|
|
|
$merchantPublicKey = $this->config['merchant_public_key'] ?? '';
|
|
|
|
|
|
|
|
|
|
|
|
// 获取服务方公钥(用于加密的公钥)
|
|
|
|
|
|
$servicePublicKey = $this->config['public_key'] ?? '';
|
2025-10-21 10:17:40 +08:00
|
|
|
|
|
2025-10-23 17:00:15 +08:00
|
|
|
|
if (empty($merchantPublicKey)) {
|
|
|
|
|
|
throw new \Exception('商户公钥未配置');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (empty($servicePublicKey)) {
|
2025-10-21 10:17:40 +08:00
|
|
|
|
throw new \Exception('服务方公钥未配置');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-23 17:00:15 +08:00
|
|
|
|
// ✅ 关键修复:提取商户公钥模数的十六进制表示的后30位
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 格式化商户公钥为PEM格式
|
|
|
|
|
|
$merchantPemKey = $this->formatPublicKeyToPem($merchantPublicKey);
|
|
|
|
|
|
|
|
|
|
|
|
// 解析商户公钥,提取模数(modulus)
|
|
|
|
|
|
$keyResource = openssl_pkey_get_public($merchantPemKey);
|
|
|
|
|
|
if (!$keyResource) {
|
|
|
|
|
|
throw new \Exception('商户公钥格式错误: ' . openssl_error_string());
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
$keyDetails = openssl_pkey_get_details($keyResource);
|
|
|
|
|
|
openssl_free_key($keyResource);
|
|
|
|
|
|
|
|
|
|
|
|
if (!isset($keyDetails['rsa']['n'])) {
|
|
|
|
|
|
throw new \Exception('无法提取商户公钥RSA模数');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 将模数转换为十六进制字符串(去掉前导0x)
|
|
|
|
|
|
$modulusHex = bin2hex($keyDetails['rsa']['n']);
|
2025-10-21 10:17:40 +08:00
|
|
|
|
|
2025-10-23 17:00:15 +08:00
|
|
|
|
// 取后30位十六进制字符
|
|
|
|
|
|
$last30Chars = substr($modulusHex, -30);
|
2025-10-21 10:17:40 +08:00
|
|
|
|
|
2025-10-23 17:00:15 +08:00
|
|
|
|
if (strlen($last30Chars) < 30) {
|
|
|
|
|
|
throw new \Exception('商户公钥模数长度不足30位');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Log::info('[建行支付] 商户公钥模数后30位: ' . $last30Chars);
|
|
|
|
|
|
|
|
|
|
|
|
// 使用服务方公钥加密这30位十六进制字符串
|
|
|
|
|
|
// CcbRSA::encrypt 会自动进行base64编码
|
|
|
|
|
|
return CcbRSA::encrypt($last30Chars, $servicePublicKey);
|
|
|
|
|
|
|
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
|
Log::error('[建行支付] ENCPUB生成失败: ' . $e->getMessage());
|
|
|
|
|
|
throw $e;
|
2025-10-21 10:17:40 +08:00
|
|
|
|
}
|
2025-10-23 17:00:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 格式化公钥为PEM格式
|
|
|
|
|
|
*
|
2025-10-27 14:20:37 +08:00
|
|
|
|
* 支持三种输入格式:
|
|
|
|
|
|
* 1. PEM格式(已包含BEGIN/END标记)
|
|
|
|
|
|
* 2. BASE64格式(不含PEM头尾)
|
|
|
|
|
|
* 3. 十六进制格式(DER编码的十六进制字符串)
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param string $publicKey 公钥(PEM/BASE64/HEX格式)
|
2025-10-23 17:00:15 +08:00
|
|
|
|
* @return string PEM格式的公钥
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function formatPublicKeyToPem($publicKey)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 移除可能存在的空格和换行
|
|
|
|
|
|
$publicKey = preg_replace('/\s+/', '', $publicKey);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果已经是PEM格式,直接返回
|
|
|
|
|
|
if (strpos($publicKey, '-----BEGIN PUBLIC KEY-----') !== false) {
|
|
|
|
|
|
return $publicKey;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-27 14:20:37 +08:00
|
|
|
|
// ✅ 判断是否为十六进制格式(只包含0-9a-fA-F字符)
|
|
|
|
|
|
if (ctype_xdigit($publicKey)) {
|
|
|
|
|
|
// 十六进制格式 → 解码为二进制 → 转BASE64
|
|
|
|
|
|
$binaryKey = hex2bin($publicKey);
|
|
|
|
|
|
if ($binaryKey === false) {
|
|
|
|
|
|
throw new \Exception('十六进制公钥解码失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
$publicKey = base64_encode($binaryKey);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-23 17:00:15 +08:00
|
|
|
|
// 转换为PEM格式
|
|
|
|
|
|
$pem = "-----BEGIN PUBLIC KEY-----\n";
|
|
|
|
|
|
$pem .= rtrim(chunk_split($publicKey, 64, "\n"), "\n") . "\n";
|
|
|
|
|
|
$pem .= "-----END PUBLIC KEY-----\n";
|
2025-10-21 10:17:40 +08:00
|
|
|
|
|
2025-10-23 17:00:15 +08:00
|
|
|
|
return $pem;
|
2025-10-21 10:17:40 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 16:32:16 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 处理支付回调
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 建行支付完成后的同步回调
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param array $params URL参数
|
|
|
|
|
|
* @return array
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function handleCallback($params)
|
|
|
|
|
|
{
|
|
|
|
|
|
try {
|
2025-10-20 09:23:30 +08:00
|
|
|
|
// 解密ccbParamSJ参数(使用服务方私钥)
|
2025-10-17 17:18:15 +08:00
|
|
|
|
if (isset($params['ccbParamSJ'])) {
|
2025-10-20 09:23:30 +08:00
|
|
|
|
$decryptedParams = CcbUrlDecrypt::decrypt($params['ccbParamSJ'], $this->config['private_key']);
|
2025-10-17 17:18:15 +08:00
|
|
|
|
if ($decryptedParams) {
|
|
|
|
|
|
$params = array_merge($params, $decryptedParams);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取关键参数
|
2025-10-18 01:16:25 +08:00
|
|
|
|
$payFlowId = $params['ORDERID'] ?? ''; // 支付流水号
|
|
|
|
|
|
$userOrderId = $params['USER_ORDERID'] ?? ''; // 商户订单号
|
2025-10-17 17:18:15 +08:00
|
|
|
|
$posId = $params['POSID'] ?? '';
|
|
|
|
|
|
$success = $params['SUCCESS'] ?? 'N';
|
|
|
|
|
|
|
|
|
|
|
|
// 验证参数
|
2025-10-18 01:16:25 +08:00
|
|
|
|
if (empty($payFlowId)) {
|
|
|
|
|
|
throw new \Exception('支付流水号不能为空');
|
2025-10-17 17:18:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证POS号
|
|
|
|
|
|
if ($posId != $this->config['pos_id']) {
|
|
|
|
|
|
throw new \Exception('POS号验证失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-18 01:16:25 +08:00
|
|
|
|
// ⚠️ 重要:ORDERID是支付流水号,不是订单号!
|
|
|
|
|
|
// 优先使用USER_ORDERID查询,如果没有则用ccb_pay_flow_id查询
|
|
|
|
|
|
if (!empty($userOrderId)) {
|
|
|
|
|
|
$order = Order::where('order_sn', $userOrderId)->find();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$order = Order::where('ccb_pay_flow_id', $payFlowId)->find();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
if (!$order) {
|
|
|
|
|
|
throw new \Exception('订单不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理支付结果
|
|
|
|
|
|
if ($success == 'Y') {
|
|
|
|
|
|
// 支付成功,更新订单状态
|
|
|
|
|
|
$this->updateOrderPaymentStatus($order, $params);
|
|
|
|
|
|
|
2025-10-21 14:33:20 +08:00
|
|
|
|
// ✅ 更新订单状态到建行(订单已在createPayment时推送,这里只需更新状态)
|
|
|
|
|
|
$this->orderService->updateOrderStatus($order['id']);
|
2025-10-17 17:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
return [
|
|
|
|
|
|
'status' => true,
|
|
|
|
|
|
'message' => '支付成功',
|
|
|
|
|
|
'data' => [
|
|
|
|
|
|
'order_id' => $order['id'],
|
2025-10-18 01:16:25 +08:00
|
|
|
|
'order_sn' => $order['order_sn'], // ✅ 返回真正的订单号
|
|
|
|
|
|
'pay_flow_id' => $payFlowId, // 支付流水号
|
2025-10-17 17:18:15 +08:00
|
|
|
|
'amount' => $params['PAYMENT'] ?? ''
|
|
|
|
|
|
]
|
|
|
|
|
|
];
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 支付失败
|
|
|
|
|
|
return [
|
|
|
|
|
|
'status' => false,
|
|
|
|
|
|
'message' => '支付失败',
|
|
|
|
|
|
'data' => [
|
|
|
|
|
|
'order_id' => $order['id'],
|
2025-10-18 01:16:25 +08:00
|
|
|
|
'order_sn' => $order['order_sn'], // ✅ 返回真正的订单号
|
|
|
|
|
|
'pay_flow_id' => $payFlowId, // 支付流水号
|
2025-10-17 17:18:15 +08:00
|
|
|
|
'error_code' => $params['ERRCODE'] ?? '',
|
|
|
|
|
|
'error_msg' => $params['ERRMSG'] ?? ''
|
|
|
|
|
|
]
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
|
Log::error('建行支付回调处理失败: ' . $e->getMessage());
|
|
|
|
|
|
return [
|
|
|
|
|
|
'status' => false,
|
|
|
|
|
|
'message' => $e->getMessage(),
|
|
|
|
|
|
'data' => null
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 处理异步通知
|
|
|
|
|
|
* 建行支付异步通知处理
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*
|
2025-10-20 16:36:06 +08:00
|
|
|
|
* ⚠️ 注意:这是唯一可信的支付确认来源!
|
|
|
|
|
|
* 返回订单ID供控制器调用pushOrderToCcb()推送到外联系统
|
|
|
|
|
|
*
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @param array $params 通知参数
|
2025-10-20 16:36:06 +08:00
|
|
|
|
* @return array ['status' => 'success'|'fail', 'order_id' => int, 'order_sn' => string]
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-17 17:18:15 +08:00
|
|
|
|
public function handleNotify($params)
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
2025-10-17 17:18:15 +08:00
|
|
|
|
try {
|
|
|
|
|
|
// 验证签名
|
|
|
|
|
|
if (!$this->verifyNotifySignature($params)) {
|
|
|
|
|
|
throw new \Exception('签名验证失败');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-18 01:16:25 +08:00
|
|
|
|
// ⚠️ 重要:ORDERID是支付流水号,不是订单号!
|
|
|
|
|
|
// 优先使用USER_ORDERID查询,如果没有则用ccb_pay_flow_id查询
|
|
|
|
|
|
$payFlowId = $params['ORDERID'] ?? ''; // 支付流水号
|
|
|
|
|
|
$userOrderId = $params['USER_ORDERID'] ?? ''; // 商户订单号
|
|
|
|
|
|
|
|
|
|
|
|
if (!empty($userOrderId)) {
|
|
|
|
|
|
$order = Order::where('order_sn', $userOrderId)->find();
|
|
|
|
|
|
} else {
|
|
|
|
|
|
$order = Order::where('ccb_pay_flow_id', $payFlowId)->find();
|
|
|
|
|
|
}
|
2025-10-17 17:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
if (!$order) {
|
|
|
|
|
|
throw new \Exception('订单不存在');
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-20 16:36:06 +08:00
|
|
|
|
// 如果订单已支付,直接返回成功(幂等性)
|
2025-10-17 17:18:15 +08:00
|
|
|
|
if ($order['status'] == 'paid') {
|
2025-10-20 16:36:06 +08:00
|
|
|
|
Log::info('[建行通知] 订单已支付,跳过处理 order_id:' . $order->id);
|
|
|
|
|
|
return [
|
|
|
|
|
|
'status' => 'success',
|
|
|
|
|
|
'order_id' => $order->id,
|
|
|
|
|
|
'order_sn' => $order->order_sn,
|
|
|
|
|
|
'already_paid' => true, // 标记为已支付
|
|
|
|
|
|
];
|
2025-10-17 17:18:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 更新订单状态
|
|
|
|
|
|
$this->updateOrderPaymentStatus($order, $params);
|
|
|
|
|
|
|
2025-10-20 16:36:06 +08:00
|
|
|
|
Log::info('[建行通知] 订单状态更新成功 order_id:' . $order->id . ' order_sn:' . $order->order_sn);
|
2025-10-17 17:18:15 +08:00
|
|
|
|
|
2025-10-20 16:36:06 +08:00
|
|
|
|
// ✅ 返回订单ID供控制器推送到外联系统
|
|
|
|
|
|
return [
|
|
|
|
|
|
'status' => 'success',
|
|
|
|
|
|
'order_id' => $order->id,
|
|
|
|
|
|
'order_sn' => $order->order_sn,
|
|
|
|
|
|
'already_paid' => false, // 新支付
|
|
|
|
|
|
];
|
2025-10-17 17:18:15 +08:00
|
|
|
|
|
|
|
|
|
|
} catch (\Exception $e) {
|
2025-10-20 16:36:06 +08:00
|
|
|
|
Log::error('[建行通知] 处理失败: ' . $e->getMessage());
|
|
|
|
|
|
return [
|
|
|
|
|
|
'status' => 'fail',
|
|
|
|
|
|
'message' => $e->getMessage(),
|
|
|
|
|
|
];
|
2025-10-17 17:18:15 +08:00
|
|
|
|
}
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 验证支付结果
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 主动查询订单支付状态
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @param string $orderSn 订单号
|
|
|
|
|
|
* @return bool
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-17 17:18:15 +08:00
|
|
|
|
public function verifyPayment($orderSn)
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
2025-10-17 17:18:15 +08:00
|
|
|
|
try {
|
|
|
|
|
|
// 查询建行订单状态
|
|
|
|
|
|
$result = $this->orderService->queryOrder($orderSn);
|
|
|
|
|
|
|
|
|
|
|
|
if ($result['status']) {
|
|
|
|
|
|
$data = $result['data']['CLD_BODY'] ?? [];
|
|
|
|
|
|
$txnStatus = $data['TXN_STATUS'] ?? '';
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
// 00=交易成功
|
|
|
|
|
|
return $txnStatus == '00';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
|
|
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
|
Log::error('建行支付验证失败: ' . $e->getMessage());
|
|
|
|
|
|
return false;
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
2025-10-17 17:18:15 +08:00
|
|
|
|
}
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 更新订单支付状态
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @param object $order 订单对象
|
|
|
|
|
|
* @param array $params 支付参数
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-17 17:18:15 +08:00
|
|
|
|
private function updateOrderPaymentStatus($order, $params)
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
2025-10-18 15:47:25 +08:00
|
|
|
|
// ⚠️ 重要字段说明:
|
|
|
|
|
|
// 1. paid_time: Shopro使用毫秒时间戳(time() * 1000)
|
|
|
|
|
|
// 2. pay_type: 建行支付暂用'offline'(建行线下银行支付),后续可扩展枚举
|
|
|
|
|
|
// 3. transaction_id: 存储建行支付流水号(ORDERID)
|
|
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
Order::where('id', $order['id'])->update([
|
|
|
|
|
|
'status' => 'paid',
|
2025-10-18 15:47:25 +08:00
|
|
|
|
'pay_type' => 'offline', // 建行支付归类为线下银行支付
|
|
|
|
|
|
'paid_time' => time() * 1000, // 毫秒时间戳
|
|
|
|
|
|
'transaction_id' => $params['ORDERID'] ?? '', // 建行支付流水号
|
2025-10-17 17:18:15 +08:00
|
|
|
|
'updatetime' => time()
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
// 记录支付日志
|
|
|
|
|
|
$this->recordPaymentLog($order['id'], 'payment_success', $params);
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* 验证异步通知签名
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*
|
2025-10-20 15:29:15 +08:00
|
|
|
|
* ⚠️ 建行异步通知签名规则:
|
|
|
|
|
|
* 1. SIGN字段为256字符十六进制字符串(2048位RSA签名)
|
|
|
|
|
|
* 2. NT_TYPE=YS时,使用"建行生活分配的服务商支付验签公钥"
|
|
|
|
|
|
* 3. 签名算法: RSA-SHA256或SHA1(需建行技术支持确认)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 📌 配置说明:
|
|
|
|
|
|
* - 如果配置了ccb_payment_verify_public_key: 使用RSA验签
|
|
|
|
|
|
* - 如果未配置: 降级为POSID验证(临时方案)
|
|
|
|
|
|
*
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @param array $params 通知参数
|
|
|
|
|
|
* @return bool
|
2025-10-17 16:32:16 +08:00
|
|
|
|
*/
|
2025-10-17 17:18:15 +08:00
|
|
|
|
private function verifyNotifySignature($params)
|
2025-10-17 16:32:16 +08:00
|
|
|
|
{
|
2025-10-20 15:29:15 +08:00
|
|
|
|
try {
|
|
|
|
|
|
// 1. 提取SIGN字段
|
|
|
|
|
|
$sign = $params['SIGN'] ?? '';
|
|
|
|
|
|
if (empty($sign)) {
|
|
|
|
|
|
Log::error('[建行验签] SIGN字段为空');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证SIGN长度(256个十六进制字符 = 2048位RSA签名)
|
|
|
|
|
|
if (strlen($sign) !== 256) {
|
|
|
|
|
|
Log::error('[建行验签] SIGN长度错误: ' . strlen($sign) . ', 应为256');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-20 15:29:15 +08:00
|
|
|
|
// 2. 检查是否配置了建行支付验签公钥
|
2025-10-27 14:20:37 +08:00
|
|
|
|
$ccbVerifyPublicKey = $this->config['public_key'] ?? '';
|
2025-10-17 17:18:15 +08:00
|
|
|
|
|
2025-10-20 15:29:15 +08:00
|
|
|
|
if (empty($ccbVerifyPublicKey)) {
|
|
|
|
|
|
// 降级方案: 未配置验签公钥时,使用POSID验证
|
2025-10-27 14:20:37 +08:00
|
|
|
|
Log::warning('[建行验签] 未配置public_key,使用降级验证方案');
|
2025-10-20 15:29:15 +08:00
|
|
|
|
|
|
|
|
|
|
// 验证POSID是否匹配
|
|
|
|
|
|
if (($params['POSID'] ?? '') !== $this->config['pos_id']) {
|
|
|
|
|
|
Log::error('[建行验签] POSID不匹配,预期: ' . $this->config['pos_id'] . ', 实际: ' . ($params['POSID'] ?? ''));
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证订单号是否存在
|
|
|
|
|
|
$orderSn = $params['USER_ORDERID'] ?? $params['ORDERID'] ?? '';
|
|
|
|
|
|
if (empty($orderSn)) {
|
|
|
|
|
|
Log::error('[建行验签] 订单号为空');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
Log::warning('[建行验签] 降级验证通过,建议联系建行技术支持获取验签公钥');
|
|
|
|
|
|
return true;
|
2025-10-17 17:18:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-10-20 15:29:15 +08:00
|
|
|
|
// 3. 移除SIGN字段,构建签名原串
|
|
|
|
|
|
$verifyParams = $params;
|
|
|
|
|
|
unset($verifyParams['SIGN']);
|
|
|
|
|
|
|
|
|
|
|
|
// 4. 按key排序
|
|
|
|
|
|
ksort($verifyParams);
|
2025-10-17 17:18:15 +08:00
|
|
|
|
|
2025-10-20 15:29:15 +08:00
|
|
|
|
// 5. 构建签名原串(非空参数)
|
|
|
|
|
|
$signStr = '';
|
|
|
|
|
|
foreach ($verifyParams as $key => $value) {
|
|
|
|
|
|
// 跳过空值参数
|
|
|
|
|
|
if ($value !== '' && $value !== null) {
|
|
|
|
|
|
$signStr .= $key . '=' . $value . '&';
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
$signStr = rtrim($signStr, '&');
|
|
|
|
|
|
|
|
|
|
|
|
Log::info('[建行验签] 签名原串: ' . $signStr);
|
|
|
|
|
|
Log::info('[建行验签] 收到SIGN前32位: ' . substr($sign, 0, 32) . '...');
|
|
|
|
|
|
|
|
|
|
|
|
// 6. 将十六进制SIGN转为二进制
|
|
|
|
|
|
$signBinary = hex2bin($sign);
|
|
|
|
|
|
if ($signBinary === false) {
|
|
|
|
|
|
Log::error('[建行验签] SIGN十六进制转换失败');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 7. 加载建行支付验签公钥
|
|
|
|
|
|
$pubKey = openssl_pkey_get_public($ccbVerifyPublicKey);
|
|
|
|
|
|
if (!$pubKey) {
|
|
|
|
|
|
Log::error('[建行验签] 验签公钥加载失败: ' . openssl_error_string());
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 8. 先尝试SHA256验签
|
|
|
|
|
|
$verifyResult = openssl_verify($signStr, $signBinary, $pubKey, OPENSSL_ALGO_SHA256);
|
|
|
|
|
|
|
|
|
|
|
|
if ($verifyResult !== 1) {
|
|
|
|
|
|
// SHA256失败,尝试SHA1
|
|
|
|
|
|
Log::info('[建行验签] SHA256验签失败,尝试SHA1');
|
|
|
|
|
|
$verifyResult = openssl_verify($signStr, $signBinary, $pubKey, OPENSSL_ALGO_SHA1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// PHP 8+ 资源自动释放
|
|
|
|
|
|
if (PHP_VERSION_ID < 80000) {
|
|
|
|
|
|
openssl_free_key($pubKey);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if ($verifyResult === 1) {
|
|
|
|
|
|
Log::info('[建行验签] RSA验签成功');
|
|
|
|
|
|
return true;
|
|
|
|
|
|
} elseif ($verifyResult === 0) {
|
|
|
|
|
|
Log::error('[建行验签] RSA验签失败,签名不匹配');
|
|
|
|
|
|
return false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
Log::error('[建行验签] RSA验签过程发生错误: ' . openssl_error_string());
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
|
Log::error('[建行验签] 验签异常: ' . $e->getMessage());
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
2025-10-17 17:18:15 +08:00
|
|
|
|
}
|
2025-10-17 16:32:16 +08:00
|
|
|
|
|
2025-10-17 17:18:15 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 记录支付请求
|
|
|
|
|
|
*
|
2025-10-22 21:22:24 +08:00
|
|
|
|
* ⚠️ 幂等性说明:
|
|
|
|
|
|
* 1. 当用户多次点击支付按钮时,会复用同一个pay_flow_id
|
|
|
|
|
|
* 2. 本方法会先检查是否已存在记录,如果存在则更新,否则插入
|
|
|
|
|
|
* 3. 这样可以避免唯一键冲突,并记录最新的支付串生成时间
|
|
|
|
|
|
*
|
2025-10-17 17:18:15 +08:00
|
|
|
|
* @param int $orderId 订单ID
|
|
|
|
|
|
* @param array $paymentData 支付数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function recordPaymentRequest($orderId, $paymentData)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 获取订单信息
|
|
|
|
|
|
$order = Order::find($orderId);
|
|
|
|
|
|
$user = Db::name('user')->where('id', $order['user_id'])->field('ccb_user_id')->find();
|
|
|
|
|
|
|
2025-10-22 21:22:24 +08:00
|
|
|
|
$payFlowId = $paymentData['pay_flow_id'] ?? '';
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否已存在记录(根据pay_flow_id唯一键)
|
|
|
|
|
|
$existingLog = Db::name('ccb_payment_log')
|
|
|
|
|
|
->where('pay_flow_id', $payFlowId)
|
|
|
|
|
|
->find();
|
|
|
|
|
|
|
|
|
|
|
|
$logData = [
|
2025-10-17 17:18:15 +08:00
|
|
|
|
'order_id' => $orderId,
|
|
|
|
|
|
'order_sn' => $order['order_sn'],
|
|
|
|
|
|
'payment_string' => $paymentData['payment_string'] ?? '',
|
|
|
|
|
|
'user_id' => $order['user_id'],
|
|
|
|
|
|
'ccb_user_id' => $user['ccb_user_id'] ?? '',
|
2025-10-18 15:47:25 +08:00
|
|
|
|
'amount' => $order['pay_fee'], // 使用Shopro的pay_fee字段
|
2025-10-17 17:18:15 +08:00
|
|
|
|
'status' => 0, // 待支付
|
2025-10-22 21:22:24 +08:00
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
if ($existingLog) {
|
|
|
|
|
|
// 已存在记录,更新支付串(保留原create_time,重置status为待支付)
|
|
|
|
|
|
// 注意:用户重复点击支付时,会生成新的支付串,需要更新
|
|
|
|
|
|
Db::name('ccb_payment_log')
|
|
|
|
|
|
->where('pay_flow_id', $payFlowId)
|
|
|
|
|
|
->update([
|
|
|
|
|
|
'payment_string' => $logData['payment_string'],
|
|
|
|
|
|
'status' => 0, // 重置为待支付(因为是新的支付串)
|
|
|
|
|
|
'amount' => $logData['amount'], // 更新金额(订单金额可能变化)
|
|
|
|
|
|
]);
|
|
|
|
|
|
|
|
|
|
|
|
Log::info('[建行支付] 更新支付日志 pay_flow_id:' . $payFlowId . ' order_id:' . $orderId);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 不存在记录,插入新记录
|
|
|
|
|
|
$logData['pay_flow_id'] = $payFlowId;
|
|
|
|
|
|
$logData['create_time'] = time();
|
|
|
|
|
|
Db::name('ccb_payment_log')->insert($logData);
|
|
|
|
|
|
|
|
|
|
|
|
Log::info('[建行支付] 插入支付日志 pay_flow_id:' . $payFlowId . ' order_id:' . $orderId);
|
|
|
|
|
|
}
|
2025-10-17 17:18:15 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 记录支付日志
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param int $orderId 订单ID
|
|
|
|
|
|
* @param string $type 日志类型
|
|
|
|
|
|
* @param array $data 数据
|
|
|
|
|
|
*/
|
|
|
|
|
|
private function recordPaymentLog($orderId, $type, $data)
|
|
|
|
|
|
{
|
|
|
|
|
|
// 更新建行支付日志
|
|
|
|
|
|
if ($type == 'payment_success') {
|
|
|
|
|
|
Db::name('ccb_payment_log')
|
|
|
|
|
|
->where('order_id', $orderId)
|
|
|
|
|
|
->update([
|
|
|
|
|
|
'status' => 1, // 支付成功
|
|
|
|
|
|
'pay_time' => time(),
|
|
|
|
|
|
'trans_id' => $data['ORDERID'] ?? '',
|
|
|
|
|
|
'callback_data' => json_encode($data, JSON_UNESCAPED_UNICODE)
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 生成退款申请
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param int $orderId 订单ID
|
|
|
|
|
|
* @param float $refundAmount 退款金额
|
|
|
|
|
|
* @param string $refundReason 退款原因
|
|
|
|
|
|
* @return array
|
|
|
|
|
|
*/
|
|
|
|
|
|
public function refund($orderId, $refundAmount, $refundReason = '')
|
|
|
|
|
|
{
|
|
|
|
|
|
try {
|
|
|
|
|
|
// 调用订单服务处理退款
|
|
|
|
|
|
$result = $this->orderService->refundOrder($orderId, $refundAmount, $refundReason);
|
|
|
|
|
|
|
|
|
|
|
|
if ($result['status']) {
|
|
|
|
|
|
// 记录退款日志
|
|
|
|
|
|
$this->recordPaymentLog($orderId, 'refund_request', [
|
|
|
|
|
|
'amount' => $refundAmount,
|
|
|
|
|
|
'reason' => $refundReason
|
|
|
|
|
|
]);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
|
|
|
|
|
|
|
} catch (\Exception $e) {
|
|
|
|
|
|
Log::error('建行退款申请失败: ' . $e->getMessage());
|
|
|
|
|
|
return [
|
|
|
|
|
|
'status' => false,
|
|
|
|
|
|
'message' => $e->getMessage(),
|
|
|
|
|
|
'data' => null
|
|
|
|
|
|
];
|
|
|
|
|
|
}
|
2025-10-17 16:32:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
}
|