diff --git a/addons/shopro/config/ccblife.php b/addons/shopro/config/ccblife.php index 8d98b0b..44b496f 100644 --- a/addons/shopro/config/ccblife.php +++ b/addons/shopro/config/ccblife.php @@ -22,11 +22,7 @@ if (file_exists($envFile)) { return [ // API基础地址 (生产环境) - 'api_base_url' => 'https://yunbusiness.ccb.com/tp_service/txCtrl/server', - - // 收银台地址 (生产环境) - 'cashier_url' => 'https://yunbusiness.ccb.com/clp_service/txCtrl', - + 'api_base_url' => 'https://yunbusiness.ccb.com/tp_service/server', // 交易代码映射 'tx_codes' => [ 'order_push' => 'A3341TP01', // 订单推送 @@ -92,6 +88,142 @@ return [ 'timeout_minutes' => 30, // 支付超时时间(分钟) ], + // ========== 可选支付参数配置 ========== + // 以下参数按需配置,不配置则不会添加到支付串中 + + /** + * 外部平台商户号 (与建行商户号二选一) + * 说明: 使用外部平台商户号时,会自动移除MERCHANTID、POSID、BRANCHID + */ + 'plat_mct_id' => Env::get('ccb.plat_mct_id', ''), + + /** + * 微信支付19位终端号 + * 说明: 微信支付场景下使用 + */ + 'pos_id_19' => Env::get('ccb.pos_id_19', ''), + + /** + * 支付位图 - 控制支付方式 + * 格式: 6位字符串,每位对应一个支付方式(1=开启,0=关闭) + * 位置: [生活钱包][龙支付][微信][数字人民币][信用付][快贷] + * 示例: '111111' = 全部开启, '110000' = 仅生活钱包和龙支付 + */ + 'pay_bitmap' => Env::get('ccb.pay_bitmap', '110000'), + + /** + * 账户位图 - 控制支付账户类型 + * 格式: 5位字符串,每位对应一个账户类型(1=开启,0=关闭) + * 位置: [建行借记卡][建行贷记卡][他行借记卡][他行贷记卡][建行钱包] + * 示例: '11111' = 全部开启, '11000' = 仅建行卡 + */ + 'account_bitmap' => Env::get('ccb.account_bitmap', '11000'), + + /** + * 分期付款期数 + * 说明: 分期支付场景下使用 + */ + 'install_num' => Env::get('ccb.install_num', ''), + + /** + * 积分二级活动编号 + * 说明: 积分抵扣场景下使用 + */ + 'point_avy_id' => Env::get('ccb.point_avy_id', ''), + + /** + * 固定抵扣积分值 + * 说明: 固定积分抵扣场景下使用 + */ + 'fixed_point_val' => Env::get('ccb.fixed_point_val', ''), + + /** + * 最小使用积分抵扣限制 + * 说明: 积分抵扣最小值限制 + */ + 'min_point_limit' => Env::get('ccb.min_point_limit', ''), + + /** + * 有价券活动编号 + * 说明: 优惠券活动场景下使用 + */ + 'coupon_avy_id' => Env::get('ccb.coupon_avy_id', ''), + + /** + * 限制信用卡支付标志 + * 说明: 1=仅限信用卡支付 + */ + 'only_credit_pay_flag' => Env::get('ccb.only_credit_pay_flag', ''), + + /** + * 扩展域参数 + * 格式: JSON字符串(配置时无需urlencode,代码会自动处理) + * 示例: '{"key1":"value1","key2":"value2"}' + */ + 'extend_params' => Env::get('ccb.extend_params', ''), + + // ========== 数字人民币(DCEP)配置 ========== + + /** + * 数字人民币商户类型 + * 1=融合商户(使用普通商户号) + * 2=非融合商户(需单独配置数币商户号) + */ + 'dcep_mct_type' => Env::get('ccb.dcep_mct_type', ''), + + /** + * 数字人民币商户号 (dcep_mct_type=2时必填) + */ + 'dcep_merchant_id' => Env::get('ccb.dcep_merchant_id', ''), + + /** + * 数字人民币柜台号 (dcep_mct_type=2时必填) + */ + 'dcep_pos_id' => Env::get('ccb.dcep_pos_id', ''), + + /** + * 数字人民币分行号 (dcep_mct_type=2时必填) + */ + 'dcep_branch_id' => Env::get('ccb.dcep_branch_id', ''), + + /** + * 数字人民币存款账号 + */ + 'dcep_dep_acc_no' => Env::get('ccb.dcep_dep_acc_no', ''), + + // ========== 二级商户配置(平台类服务方使用) ========== + + /** + * 二级商户编号 + * 说明: 平台型服务方为下级商户收款时使用 + */ + 'sub_mct_id' => Env::get('ccb.sub_mct_id', ''), + + /** + * 二级商户名称 + */ + 'sub_mct_name' => Env::get('ccb.sub_mct_name', ''), + + /** + * 二级商户MCC码 + * 说明: 商户类别码,标识商户行业类型 + */ + 'sub_mct_mcc' => Env::get('ccb.sub_mct_mcc', ''), + + // ========== 场景编号配置(埋点使用,不参与MAC签名) ========== + + /** + * 场景编号 + * 说明: 用于数据埋点分析,不参与MAC校验 + */ + 'scn_id' => Env::get('ccb.scn_id', ''), + + /** + * 场景平台编号 + * 说明: 用于数据埋点分析,不参与MAC校验 + */ + 'scn_pltfrm_id' => Env::get('ccb.scn_pltfrm_id', ''), + // 日志配置 'log' => [ 'enabled' => true, @@ -108,7 +240,7 @@ return [ // 商户信息 'merchant' => [ - 'name' => Env::get('ccb.merchant_name', '商户名称'), + 'name' => Env::get('ccb.merchant_name', '丰科贸易(荷西嘉园店)'), 'logo_url' => Env::get('ccb.merchant_logo', ''), 'order_detail_url' => Env::get('app_url', 'http://fengketrade.test') . '/pages/order/detail?id=', ], diff --git a/addons/shopro/controller/Ccblife.php b/addons/shopro/controller/Ccblife.php index 7e0c8a7..d436e07 100644 --- a/addons/shopro/controller/Ccblife.php +++ b/addons/shopro/controller/Ccblife.php @@ -140,55 +140,7 @@ class Ccblife extends Common ]); } } - - /** - * 建行用户自动登录(JSBridge方式) - * H5在建行App内打开时,通过JSBridge获取用户信息后调用 - * - * POST /addons/shopro/ccblife/autoLogin - */ - public function autoLogin() - { - try { - // 获取请求参数 - $ccbUserId = $this->request->post('ccb_user_id', ''); - $mobile = $this->request->post('mobile', ''); - $nickname = $this->request->post('nickname', ''); - $avatar = $this->request->post('avatar', ''); - - // 验证必需参数 - if (empty($ccbUserId)) { - $this->error('建行用户ID不能为空'); - } - - // 处理用户登录/注册 - $userInfo = $this->processUserLogin($ccbUserId, $mobile, [ - 'nickname' => $nickname, - 'avatar' => $avatar - ]); - - // 使用Auth系统登录并生成Token - $this->auth->direct($userInfo['user_id']); - $token = $this->auth->getToken(); - - // 返回结果 - $this->success('登录成功', [ - 'token' => $token, - 'user_id' => $userInfo['user_id'], - 'is_new_user' => $userInfo['is_new'], - 'userInfo' => $userInfo - ]); - - } catch (\think\exception\HttpResponseException $e) { - // HttpResponseException 是框架正常的响应机制,直接向上抛出 - throw $e; - } catch (\Exception $e) { - Log::error('建行自动登录失败: ' . $e->getMessage()); - Log::error('错误堆栈: ' . $e->getTraceAsString()); - $this->error('登录失败: ' . $e->getMessage()); - } - } - + /** * 处理用户登录/注册 * diff --git a/addons/shopro/controller/Ccbpayment.php b/addons/shopro/controller/Ccbpayment.php index 42ded4d..3d500ec 100644 --- a/addons/shopro/controller/Ccbpayment.php +++ b/addons/shopro/controller/Ccbpayment.php @@ -87,34 +87,43 @@ class Ccbpayment extends Common $this->error('订单已支付或已关闭'); } - // 4. 生成支付串 + // 4. ✅ 生成支付流水号(统一标识,用于订单推送和支付串生成) + // 格式: PAY + 年月日时分秒(14位) + 随机数(6位) = 23位 + $payFlowId = 'PAY' . date('YmdHis') . mt_rand(100000, 999999); + Log::info('[建行支付] 生成支付流水号 pay_flow_id:' . $payFlowId . ' order_id:' . $orderId); + + // 5. ✅ 先推送订单到建行生活(步骤2:调用A3341TP01订单推送接口) + // ⚠️ 重要:必须先推送订单,收银台才能校验订单信息 + // 根据《5.6.2 业务流程说明》步骤2:由服务方调用订单推送接口向建行生活推送订单信息 + try { + $pushResult = $this->orderService->pushOrder($orderId, $payFlowId); + + if (!$pushResult['status']) { + // ⚠️ 推送失败必须阻塞支付流程!收银台会找不到订单 + Log::error('[建行支付] 订单推送失败(阻塞支付) order_id:' . $orderId . ' error:' . $pushResult['message']); + $this->error('订单推送失败,无法生成支付串: ' . $pushResult['message']); + } + + Log::info('[建行支付] 订单推送成功 order_id:' . $orderId); + + } catch (Exception $e) { + // ⚠️ 推送异常必须阻塞支付流程 + Log::error('[建行支付] 订单推送异常(阻塞支付) order_id:' . $orderId . ' error:' . $e->getMessage()); + $this->error('订单推送异常,无法生成支付串: ' . $e->getMessage()); + } + + // 6. 生成支付串(步骤3:调用收银台) // ⚠️ 注意: generatePaymentString()内部已经完成了以下操作: // - 更新订单的ccb_pay_flow_id字段 // - 记录支付日志到ccb_payment_log表 // 控制器不应该重复操作,否则会导致数据重复写入! - $result = $this->paymentService->generatePaymentString($orderId); + $result = $this->paymentService->generatePaymentString($orderId, $payFlowId); if (!$result['status']) { $this->error('支付串生成失败: ' . $result['message']); } - // 5. ✅ 推送订单到建行(步骤2:调用订单推送接口将已提交订单) - // ⚠️ 注意:此时推送的是未支付状态的订单 - try { - $pushResult = $this->orderService->pushOrder($orderId); - - if ($pushResult['status']) { - Log::info('[建行支付] 订单推送成功 order_id:' . $orderId); - } else { - // ⚠️ 推送失败不阻塞支付流程,只记录日志 - Log::warning('[建行支付] 订单推送失败(不阻塞支付) order_id:' . $orderId . ' error:' . $pushResult['message']); - } - } catch (Exception $e) { - // ⚠️ 推送异常不阻塞支付流程 - Log::error('[建行支付] 订单推送异常(不阻塞支付) order_id:' . $orderId . ' error:' . $e->getMessage()); - } - - // 6. 返回支付串 + // 7. 返回支付串给前端调用收银台 $this->success('支付串生成成功', $result['data']); } catch (Exception $e) { diff --git a/addons/shopro/controller/Ccbtest.php b/addons/shopro/controller/Ccbtest.php deleted file mode 100644 index e200768..0000000 --- a/addons/shopro/controller/Ccbtest.php +++ /dev/null @@ -1,527 +0,0 @@ -request->domain() . '/addons/shopro/ccbtest'; - - $tests = [ - '基础功能测试' => [ - [ - 'name' => '测试RSA加密解密', - 'url' => $baseUrl . '/testRsa', - 'method' => 'GET', - 'desc' => '测试RSA加密和解密功能是否正常' - ], - [ - 'name' => '测试MD5签名', - 'url' => $baseUrl . '/testMd5', - 'method' => 'GET', - 'desc' => '测试MD5签名生成功能' - ], - [ - 'name' => '测试URL参数解密', - 'url' => $baseUrl . '/testUrlDecrypt', - 'method' => 'GET', - 'desc' => '测试建行URL参数解密功能' - ], - ], - '订单功能测试' => [ - [ - 'name' => '测试订单推送', - 'url' => $baseUrl . '/testOrderPush?order_id=1', - 'method' => 'GET', - 'desc' => '测试推送订单到建行(需要提供order_id)' - ], - [ - 'name' => '测试订单查询', - 'url' => $baseUrl . '/testOrderQuery?order_sn=ORDER123', - 'method' => 'GET', - 'desc' => '测试查询建行订单状态(需要提供order_sn)' - ], - [ - 'name' => '测试批量同步', - 'url' => $baseUrl . '/testBatchSync', - 'method' => 'GET', - 'desc' => '测试批量同步未同步的订单' - ], - ], - '支付功能测试' => [ - [ - 'name' => '测试生成支付串', - 'url' => $baseUrl . '/testPaymentString?order_id=1', - 'method' => 'GET', - 'desc' => '测试生成建行支付串(需要提供order_id)' - ], - [ - 'name' => '测试支付验证', - 'url' => $baseUrl . '/testVerifyPayment?order_sn=ORDER123', - 'method' => 'GET', - 'desc' => '测试验证支付结果(需要提供order_sn)' - ], - ], - '用户功能测试' => [ - [ - 'name' => '模拟用户登录', - 'url' => $baseUrl . '/testUserLogin', - 'method' => 'POST', - 'desc' => '模拟建行用户登录流程', - 'params' => [ - 'ccb_user_id' => 'ccb_test_user_001', - 'mobile' => '13800138000' - ] - ], - ], - '环境检查' => [ - [ - 'name' => '检查配置', - 'url' => $baseUrl . '/checkConfig', - 'method' => 'GET', - 'desc' => '检查建行配置是否正确' - ], - [ - 'name' => '检查数据库', - 'url' => $baseUrl . '/checkDatabase', - 'method' => 'GET', - 'desc' => '检查数据库表结构是否正确' - ], - ] - ]; - - $this->success('建行生活接口测试', $tests); - } - - /** - * 测试RSA加密解密 - */ - public function testRsa() - { - try { - $config = config('ccblife'); - - // 测试数据 - $testData = '这是一个测试字符串,用于验证RSA加密解密功能是否正常工作。包含中文、English、数字123456和特殊字符!@#$%^&*()'; - - // 加密 - $encrypted = CcbRSA::encrypt($testData, $config['public_key']); - - // 解密 - $decrypted = CcbRSA::decrypt($encrypted, $config['private_key']); - - // 验证 - $isSuccess = ($decrypted === $testData); - - $result = [ - '测试结果' => $isSuccess ? '成功' : '失败', - '原始数据' => $testData, - '加密后数据(BASE64)' => substr($encrypted, 0, 100) . '...', - '加密后长度' => strlen($encrypted), - '解密后数据' => $decrypted, - '数据一致性' => $isSuccess ? '✓ 一致' : '✗ 不一致' - ]; - - $this->success('RSA加密解密测试完成', $result); - - } catch (Exception $e) { - $this->error('RSA测试失败:' . $e->getMessage()); - } - } - - /** - * 测试MD5签名 - */ - public function testMd5() - { - try { - $config = config('ccblife'); - - // 测试API消息签名 - $apiMessage = '{"TX_CODE":"A3341TP01","USER_ID":"test_user"}'; - $apiSign = CcbMD5::signApiMessage($apiMessage, $config['private_key']); - - // 测试支付串签名 - $paymentResult = CcbMD5::generatePaymentSignature( - $config['merchant_id'], - $config['pos_id'], - $config['branch_id'], - 'TEST_ORDER_' . time(), - '100.00', - $config['private_key'], - '01', - '530550' - ); - - $result = [ - 'API消息签名' => [ - '原始消息' => $apiMessage, - '签名结果' => $apiSign, - '签名长度' => strlen($apiSign) - ], - '支付串签名' => [ - '支付串' => $paymentResult['payment_string'], - 'MAC签名' => $paymentResult['mac'], - '参数列表' => $paymentResult['params'] - ] - ]; - - $this->success('MD5签名测试完成', $result); - - } catch (Exception $e) { - $this->error('MD5测试失败:' . $e->getMessage()); - } - } - - /** - * 测试URL参数解密 - */ - public function testUrlDecrypt() - { - try { - $config = config('ccblife'); - - // 模拟建行传递的加密参数 - // 注意:这里需要真实的ccbParamSJ参数才能测试 - $ccbParamSJ = $this->request->get('ccbParamSJ', ''); - - if (empty($ccbParamSJ)) { - // 如果没有提供参数,生成一个测试用的 - $testParams = [ - 'userid' => 'test_ccb_user_001', - 'mobile' => '13800138000', - 'openid' => 'test_openid_001' - ]; - - $result = [ - '提示' => '未提供ccbParamSJ参数,无法进行实际解密测试', - '说明' => '请从建行App跳转时携带ccbParamSJ参数', - '服务方编号' => $config['service_id'], - '加密方式' => 'RSA(使用服务方私钥解密)', - '模拟数据' => $testParams - ]; - } else { - // 尝试解密(使用服务方私钥) - $decryptedParams = CcbUrlDecrypt::decrypt($ccbParamSJ, $config['private_key']); - - $result = [ - '解密结果' => $decryptedParams ? '成功' : '失败', - '原始参数' => $ccbParamSJ, - '解密数据' => $decryptedParams, - '服务方编号' => $config['service_id'], - '加密方式' => 'RSA' - ]; - } - - $this->success('URL参数解密测试', $result); - - } catch (Exception $e) { - $this->error('URL解密测试失败:' . $e->getMessage()); - } - } - - /** - * 测试订单推送 - */ - public function testOrderPush() - { - try { - $orderId = $this->request->get('order_id', 1); - - $orderService = new CcbOrderService(); - $result = $orderService->pushOrder($orderId); - - $this->success('订单推送测试完成', $result); - - } catch (Exception $e) { - $this->error('订单推送失败:' . $e->getMessage()); - } - } - - /** - * 测试订单查询 - */ - public function testOrderQuery() - { - try { - $orderSn = $this->request->get('order_sn', ''); - - if (empty($orderSn)) { - $this->error('请提供订单号参数 order_sn'); - } - - $orderService = new CcbOrderService(); - $result = $orderService->queryOrder($orderSn); - - $this->success('订单查询测试完成', $result); - - } catch (Exception $e) { - $this->error('订单查询失败:' . $e->getMessage()); - } - } - - /** - * 测试批量同步 - */ - public function testBatchSync() - { - try { - $orderService = new CcbOrderService(); - $result = $orderService->batchSync(5); // 同步5个订单 - - $this->success('批量同步测试完成', $result); - - } catch (Exception $e) { - $this->error('批量同步失败:' . $e->getMessage()); - } - } - - /** - * 测试生成支付串 - */ - public function testPaymentString() - { - try { - $orderId = $this->request->get('order_id', 1); - - $paymentService = new CcbPaymentService(); - $result = $paymentService->generatePaymentString($orderId); - - $this->success('支付串生成测试完成', $result); - - } catch (Exception $e) { - $this->error('支付串生成失败:' . $e->getMessage()); - } - } - - /** - * 测试支付验证 - */ - public function testVerifyPayment() - { - try { - $orderSn = $this->request->get('order_sn', ''); - - if (empty($orderSn)) { - $this->error('请提供订单号参数 order_sn'); - } - - $paymentService = new CcbPaymentService(); - $isSuccess = $paymentService->verifyPayment($orderSn); - - $result = [ - '订单号' => $orderSn, - '支付状态' => $isSuccess ? '已支付' : '未支付', - '验证结果' => $isSuccess - ]; - - $this->success('支付验证测试完成', $result); - - } catch (Exception $e) { - $this->error('支付验证失败:' . $e->getMessage()); - } - } - - /** - * 模拟用户登录 - */ - public function testUserLogin() - { - try { - $ccbUserId = $this->request->post('ccb_user_id', 'ccb_test_user_001'); - $mobile = $this->request->post('mobile', '13800138000'); - - // 查询或创建用户 - $user = Db::name('user')->where('ccb_user_id', $ccbUserId)->find(); - - if (!$user) { - // 创建测试用户 - $userData = [ - 'ccb_user_id' => $ccbUserId, - 'username' => 'test_' . substr(md5($ccbUserId), 0, 8), - 'nickname' => '测试用户' . substr($ccbUserId, -4), - 'mobile' => $mobile, - 'avatar' => '/assets/img/avatar.png', - 'status' => 'normal', - 'salt' => \fast\Random::alnum(), - 'password' => '', - 'joinip' => $this->request->ip(), - 'jointime' => time(), - 'logintime' => time(), - 'loginip' => $this->request->ip(), - 'createtime' => time(), - 'updatetime' => time() - ]; - - // 设置随机密码 - $userData['password'] = md5(md5(\fast\Random::alnum(32)) . $userData['salt']); - - $userId = Db::name('user')->insertGetId($userData); - $user = Db::name('user')->where('id', $userId)->find(); - } - - // 生成Token - $this->auth->direct($user['id']); - $token = $this->auth->getToken(); - - $result = [ - '用户ID' => $user['id'], - '建行用户ID' => $ccbUserId, - '手机号' => $mobile, - 'Token' => $token, - '是否新用户' => !isset($userId) ? '否' : '是' - ]; - - $this->success('用户登录测试成功', $result); - - } catch (Exception $e) { - $this->error('用户登录测试失败:' . $e->getMessage()); - } - } - - /** - * 检查配置 - */ - public function checkConfig() - { - try { - $config = config('ccblife'); - - $checks = [ - '基础配置' => [ - '收银台地址' => $config['cashier_url'] ?? '未配置', - '服务方编号' => $config['service_id'] ?? '未配置', - ], - '商户信息' => [ - '商户号' => $config['merchant_id'] ?? '未配置', - 'POS号' => $config['pos_id'] ?? '未配置', - '分行号' => $config['branch_id'] ?? '未配置', - ], - '密钥配置' => [ - '私钥长度' => strlen($config['private_key'] ?? '') . ' 字符', - '公钥长度' => strlen($config['public_key'] ?? '') . ' 字符', - '私钥格式' => $this->validateKey($config['private_key'] ?? ''), - '公钥格式' => $this->validateKey($config['public_key'] ?? ''), - ], - '交易代码' => $config['tx_codes'] ?? [], - 'HTTP配置' => $config['http'] ?? [], - '安全配置' => $config['security'] ?? [] - ]; - - $this->success('配置检查完成', $checks); - - } catch (Exception $e) { - $this->error('配置检查失败:' . $e->getMessage()); - } - } - - /** - * 检查数据库 - */ - public function checkDatabase() - { - try { - $checks = []; - - // 检查用户表 - $userColumns = Db::query("SHOW COLUMNS FROM fa_user LIKE 'ccb_user_id'"); - $checks['用户表(fa_user)'] = [ - 'ccb_user_id字段' => !empty($userColumns) ? '✓ 存在' : '✗ 不存在' - ]; - - // 检查订单表 - $orderColumns = Db::query("SHOW COLUMNS FROM fa_shopro_order WHERE Field IN ('ccb_user_id', 'ccb_pay_flow_id', 'ccb_sync_status', 'ccb_sync_time')"); - $orderFields = array_column($orderColumns, 'Field'); - $checks['订单表(fa_shopro_order)'] = [ - 'ccb_user_id字段' => in_array('ccb_user_id', $orderFields) ? '✓ 存在' : '✗ 不存在', - 'ccb_pay_flow_id字段' => in_array('ccb_pay_flow_id', $orderFields) ? '✓ 存在' : '✗ 不存在', - 'ccb_sync_status字段' => in_array('ccb_sync_status', $orderFields) ? '✓ 存在' : '✗ 不存在', - 'ccb_sync_time字段' => in_array('ccb_sync_time', $orderFields) ? '✓ 存在' : '✗ 不存在', - ]; - - // 检查支付日志表 - $paymentLogExists = Db::query("SHOW TABLES LIKE 'fa_ccb_payment_log'"); - $checks['支付日志表(fa_ccb_payment_log)'] = [ - '表是否存在' => !empty($paymentLogExists) ? '✓ 存在' : '✗ 不存在' - ]; - - // 检查同步日志表 - $syncLogExists = Db::query("SHOW TABLES LIKE 'fa_ccb_sync_log'"); - $checks['同步日志表(fa_ccb_sync_log)'] = [ - '表是否存在' => !empty($syncLogExists) ? '✓ 存在' : '✗ 不存在' - ]; - - // 统计数据 - if (!empty($syncLogExists)) { - $syncStats = Db::name('ccb_sync_log') - ->field('sync_status, COUNT(*) as count') - ->group('sync_status') - ->select(); - - $checks['同步统计'] = []; - foreach ($syncStats as $stat) { - $status = $stat['sync_status'] == 1 ? '成功' : '失败'; - $checks['同步统计'][$status] = $stat['count'] . ' 条'; - } - } - - $this->success('数据库检查完成', $checks); - - } catch (Exception $e) { - $this->error('数据库检查失败:' . $e->getMessage()); - } - } - - /** - * 验证密钥格式 - */ - private function validateKey($key) - { - if (empty($key)) { - return '✗ 未配置'; - } - - // 检查是否为BASE64格式 - if (base64_encode(base64_decode($key)) === $key) { - return '✓ BASE64格式'; - } - - return '✗ 格式异常'; - } -} \ No newline at end of file diff --git a/addons/shopro/controller/Pay.php b/addons/shopro/controller/Pay.php index b69bc38..c1cf25e 100755 --- a/addons/shopro/controller/Pay.php +++ b/addons/shopro/controller/Pay.php @@ -344,7 +344,7 @@ class Pay extends Common * * @param object|string $result * @param string|null $payment - * @return void + * @return object|string|\think\Response|null */ private function payResponse($result = null, $payment = null) { @@ -363,7 +363,7 @@ class Pay extends Common * 根据订单号获取订单实例 * * @param [type] $order_sn - * @return void + * @return array */ private function getOrderInstance($order_sn) { diff --git a/addons/shopro/controller/order/Order.php b/addons/shopro/controller/order/Order.php index a8501aa..1f278e0 100755 --- a/addons/shopro/controller/order/Order.php +++ b/addons/shopro/controller/order/Order.php @@ -77,7 +77,6 @@ class Order extends Common $this->error(__('No Results were found')); } - $order->pay_types_text = $order->pay_types_text; // 处理未支付订单 item status_code $order = $order->setOrderItemStatusByOrder($order); // 这里订单转 数组了 diff --git a/addons/shopro/library/ccblife/CcbEncryption.php b/addons/shopro/library/ccblife/CcbEncryption.php index 30dcee5..c3efbd9 100644 --- a/addons/shopro/library/ccblife/CcbEncryption.php +++ b/addons/shopro/library/ccblife/CcbEncryption.php @@ -7,12 +7,19 @@ use think\Exception; /** * 建行生活加密解密核心类 * - * 功能: - * - RSA加密与解密 - * - MD5签名生成与验证 - * - 报文构造与解析 - * - 交易流水号生成 + * ⚠️ 已废弃:请使用 CcbRSA、CcbMD5、CcbHttpClient 类替代 * + * 废弃原因: + * 1. formatKey() 方法存在密钥格式化错误(PKCS#1 vs PKCS#8 混淆) + * 2. chunk_split() 使用不当导致 OpenSSL ASN1 解析错误 + * 3. 与 CcbRSA 类功能重复,维护成本高 + * + * 迁移指南: + * - RSA加密/解密 → 使用 CcbRSA::encrypt() / CcbRSA::decrypt() + * - MD5签名 → 使用 CcbMD5::signApiMessage() / CcbMD5::verifyApiSignature() + * - 加密商户公钥 → 在 CcbPaymentService 中使用 encryptPublicKeyLast30() + * + * @deprecated 2025-01-21 统一使用 CcbRSA、CcbMD5 类 * @author Billy * @date 2025-01-16 */ @@ -36,11 +43,6 @@ class CcbEncryption */ private $publicKey; - /** - * 建行平台公钥 - * @var string - */ - private $platformPublicKey; /** * 构造函数 @@ -75,8 +77,8 @@ class CcbEncryption throw new Exception('服务方私钥未配置'); } - if (empty($this->platformPublicKey)) { - throw new Exception('建行平台公钥未配置'); + if (empty($this->publicKey)) { + throw new Exception('服务方公钥未配置'); } } @@ -90,7 +92,7 @@ class CcbEncryption public function rsaEncrypt($data) { // 格式化公钥 - $publicKey = $this->formatKey($this->platformPublicKey, 'PUBLIC'); + $publicKey = $this->formatKey($this->publicKey); // 获取公钥资源 $pubKeyId = openssl_pkey_get_public($publicKey); @@ -382,7 +384,7 @@ class CcbEncryption $signString = http_build_query($params); // 3. 追加平台公钥 - $signString .= '&PLATFORMPUB=' . $this->platformPublicKey; + $signString .= '&PLATFORMPUB=' . $this->publicKey; // 4. 生成MD5签名 (转为大写,与Java一致) return strtoupper(md5($signString . $this->privateKey)); diff --git a/addons/shopro/library/ccblife/CcbHttpClient.php b/addons/shopro/library/ccblife/CcbHttpClient.php index 0d46db5..a7fe9a2 100644 --- a/addons/shopro/library/ccblife/CcbHttpClient.php +++ b/addons/shopro/library/ccblife/CcbHttpClient.php @@ -2,6 +2,8 @@ namespace addons\shopro\library\ccblife; +use think\Log; + /** * 建行生活HTTP客户端 * 处理与建行API的通信,包括加密、签名、发送请求和解密响应 @@ -48,6 +50,10 @@ class CcbHttpClient // 构建请求报文 $message = $this->buildMessage($txCode, $body, $txSeq); + // 📝 记录原始请求报文(加密前) + Log::info('建行生活API原始请求报文 [txCode=' . $txCode . '] [txSeq=' . $txSeq . ']'); + Log::info('原始报文内容: ' . $message); + // 加密报文 $encryptedMessage = CcbRSA::encryptForCcb($message, $this->config['public_key']); @@ -99,6 +105,9 @@ class CcbHttpClient */ private function sendHttpRequest($txCode, $cnt, $mac) { + // 记录请求开始时间 + $startTime = microtime(true); + // 构建完整的API URL(基础URL + ?txcode=交易代码) $apiUrl = $this->config['api_base_url'] . '?txcode=' . $txCode; @@ -108,6 +117,11 @@ class CcbHttpClient 'mac' => $mac ]; + // 📝 记录请求参数(加密后的完整内容) + Log::info('建行生活API加密请求参数 [txCode=' . $txCode . '] [url=' . $apiUrl . '] [timeout=' . self::DEFAULT_TIMEOUT . 's]'); + Log::info('加密参数 cnt: ' . $cnt); + Log::info('加密参数 mac: ' . $mac); + // 初始化CURL $ch = curl_init(); @@ -130,17 +144,32 @@ class CcbHttpClient $response = curl_exec($ch); $error = curl_error($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $curlInfo = curl_getinfo($ch); curl_close($ch); + // 计算请求耗时 + $costTime = round((microtime(true) - $startTime) * 1000, 2); // 毫秒 + // 检查错误 if ($error) { + // 📝 记录错误日志 + Log::error('建行生活API请求失败 [txCode=' . $txCode . '] [url=' . $apiUrl . '] [error=' . $error . '] [cost_time=' . $costTime . 'ms] [total_time=' . ($curlInfo['total_time'] ?? 0) . 's] [connect_time=' . ($curlInfo['connect_time'] ?? 0) . 's]'); throw new \Exception('HTTP请求失败: ' . $error); } if ($httpCode !== 200) { + // 📝 记录HTTP状态码异常日志 + $responsePreview = mb_substr($response, 0, 300); + Log::error('建行生活API响应状态码异常 [txCode=' . $txCode . '] [url=' . $apiUrl . '] [http_code=' . $httpCode . '] [cost_time=' . $costTime . 'ms] [response=' . $responsePreview . ']'); throw new \Exception('HTTP状态码异常: ' . $httpCode . ', 响应内容: ' . $response); } + // 📝 记录成功响应日志 + $totalTime = round(($curlInfo['total_time'] ?? 0) * 1000, 2); + $connectTime = round(($curlInfo['connect_time'] ?? 0) * 1000, 2); + $responseLength = mb_strlen($response); + Log::info('建行生活API请求成功 [txCode=' . $txCode . '] [url=' . $apiUrl . '] [http_code=' . $httpCode . '] [response_length=' . $responseLength . '] [cost_time=' . $costTime . 'ms] [total_time=' . $totalTime . 'ms] [connect_time=' . $connectTime . 'ms]'); + return $response; } @@ -153,6 +182,9 @@ class CcbHttpClient */ private function handleResponse($response) { + // 📝 记录原始响应内容 + Log::info('建行生活API原始响应内容: ' . $response); + // 解析JSON响应 $responseData = json_decode($response, true); if (json_last_error() !== JSON_ERROR_NONE) { @@ -164,15 +196,25 @@ class CcbHttpClient throw new \Exception('响应格式错误,缺少cnt或mac字段'); } + // 📝 记录加密响应参数 + Log::info('加密响应参数 cnt: ' . $responseData['cnt']); + Log::info('加密响应参数 mac: ' . $responseData['mac']); + // 解密响应内容 $decryptedContent = CcbRSA::decryptFromCcb($responseData['cnt'], $this->config['private_key']); + // 📝 记录解密后的响应内容 + Log::info('解密后响应内容: ' . $decryptedContent); + // 验证签名 $isValid = CcbMD5::verifyApiSignature($decryptedContent, $responseData['mac'], $this->config['private_key']); if (!$isValid) { + Log::error('响应签名验证失败 [expected_mac=' . $responseData['mac'] . ']'); throw new \Exception('响应签名验证失败'); } + Log::info('响应签名验证成功'); + // 解析解密后的内容 $decryptedData = json_decode($decryptedContent, true); if (json_last_error() !== JSON_ERROR_NONE) { diff --git a/addons/shopro/library/ccblife/CcbOrderService.php b/addons/shopro/library/ccblife/CcbOrderService.php index 350837e..83ee6c1 100644 --- a/addons/shopro/library/ccblife/CcbOrderService.php +++ b/addons/shopro/library/ccblife/CcbOrderService.php @@ -34,63 +34,32 @@ class CcbOrderService throw new \Exception('建行生活配置文件不存在'); } - // 处理BASE64格式的密钥 - $this->config = $this->processPemKeys($this->config); + // ✅ 修复: 删除processPemKeys()调用 + // 密钥格式化统一由CcbRSA类处理,避免重复格式化导致OpenSSL ASN1解析错误 + // CcbRSA::formatPublicKey/formatPrivateKey 会在加密/解密时自动处理密钥格式 $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; - } - /** * 推送订单到建行生活平台 * 当用户下单后调用此方法同步订单信息 * * @param int $orderId Shopro订单ID + * @param string $payFlowId 支付流水号(由控制器统一生成) * @return array ['status' => bool, 'message' => string, 'data' => array] * @throws \Exception */ - public function pushOrder($orderId) + public function pushOrder($orderId, $payFlowId) { $startTime = microtime(true); try { + // ✅ 验证支付流水号 + if (empty($payFlowId)) { + throw new \Exception('支付流水号不能为空'); + } + // 获取订单信息 $order = Db::name('shopro_order') ->alias('o') @@ -115,7 +84,8 @@ class CcbOrderService ->select(); // 构建订单数据(符合A3341TP01接口规范) - $orderData = $this->buildOrderData($order, $orderItems, $ccbUserId); + // ✅ 传入统一的支付流水号 + $orderData = $this->buildOrderData($order, $orderItems, $ccbUserId, $payFlowId); // 记录请求数据(同步日志) $txSeq = CcbMD5::generateTransactionSeq(); @@ -270,13 +240,13 @@ class CcbOrderService { try { // 获取订单信息 - $order = Order::find($orderId); + $order = Db::name('shopro_order')->where('id', $orderId)->find(); if (!$order) { throw new \Exception('订单不存在'); } // 验证退款金额 - if ($refundAmount > $order['total_amount']) { + if ($refundAmount > $order['order_amount']) { throw new \Exception('退款金额不能超过订单总额'); } @@ -307,146 +277,183 @@ class CcbOrderService } /** - * 构建符合建行要求的订单数据 + * 构建符合建行 A3341TP01 接口规范的订单数据 * - * ⚠️ 注意:Shopro字段说明 - * - pay_fee: 实际支付金额(Shopro字段) - * - order_amount: 订单总金额(Shopro字段) - * - total_discount_fee: 优惠总金额(Shopro字段) - * - paid_time: 支付时间(毫秒时间戳!需除以1000) - * - createtime: 创建时间(毫秒时间戳!需除以1000) + * 📋 建行生活订单推送接口规范说明(v1.1.6): + * + * 必填字段(11个): + * - USER_ID: 客户编号(建行用户ID) + * - ORDER_ID: 订单号 + * - ORDER_DT: 订单日期(yyyyMMddHHmmss格式) + * - TOTAL_AMT: 订单原金额 + * - ORDER_STATUS: 订单状态 + * - REFUND_STATUS: 退款状态 + * - MCT_NM: 商户名称 + * - CUS_ORDER_URL: 订单详情链接 + * - PAY_FLOW_ID: 支付流水号 + * - PAY_MRCH_ID: 支付商户号 + * - SKU_LIST: 商品信息JSON字符串 + * + * 重要可选字段(建议必填): + * - PAY_AMT: 订单实际支付金额(文档要求:如为空必须在状态变更时推送) + * - DISCOUNT_AMT: 第三方平台优惠金额(文档要求:如为空必须在状态变更时推送) + * - DISCOUNT_AMT_DESC: 第三方平台优惠说明 + * - INV_DT: 订单过期日期 + * - GOODS_NM: 商品名称 + * - PREFTL_MRCH_ID: 门店商户号 + * - PLAT_MCT_ID: 服务商门店编号 + * - PLAT_ORDER_TYPE: 服务方订单类型 + * - PLATFORM: 下单场景 + * + * ⚠️ 注意:Shopro字段映射 + * - pay_fee → PAY_AMT(实际支付金额) + * - order_amount → TOTAL_AMT(订单总金额) + * - total_discount_fee → DISCOUNT_AMT(优惠总金额) + * - createtime → ORDER_DT(毫秒时间戳需除以1000) + * - expiry_time → INV_DT(过期时间) * * @param array $order 订单数组 * @param array $orderItems 订单商品列表 * @param string $ccbUserId 建行用户ID * @return array */ - private function buildOrderData($order, $orderItems, $ccbUserId) + private function buildOrderData($order, $orderItems, $ccbUserId, $payFlowId) { - // 构建商品列表 - $goodsList = $this->buildGoodsList($orderItems); - - // 计算各项金额(Shopro字段:pay_fee=实付金额,order_amount=订单总金额) - $totalAmount = number_format($order['order_amount'] ?? 0, 2, '.', ''); - $payAmount = number_format($order['pay_fee'] ?? 0, 2, '.', ''); - $discountAmount = number_format($order['total_discount_fee'] ?? 0, 2, '.', ''); - - // 处理支付时间(Shopro的paid_time是毫秒时间戳,需要除以1000) - $payTime = ''; - if (!empty($order['paid_time']) && is_numeric($order['paid_time'])) { - $payTime = date('YmdHis', intval($order['paid_time'] / 1000)); + // ✅ 使用控制器传入的统一支付流水号(确保与支付串生成使用同一流水号) + if (empty($payFlowId)) { + throw new \Exception('支付流水号不能为空'); } - // 处理创建时间(Shopro的createtime是毫秒时间戳,需要除以1000) + // 构建SKU商品列表(JSON字符串格式) + $skuList = $this->buildSkuList($orderItems); + + // 计算各项金额(保留2位小数) + $totalAmount = number_format($order['order_amount'] ?? 0, 2, '.', ''); + $payAmount = number_format($order['pay_fee'] ?? $order['order_amount'] ?? 0, 2, '.', ''); + $discountAmount = number_format($order['total_discount_fee'] ?? 0, 2, '.', ''); + $totalRefundAmount = number_format($order['refund_fee'] ?? 0, 2, '.', ''); + + // 处理订单时间(Shopro的createtime是毫秒时间戳,需要除以1000) $createTimeValue = $order['createtime'] ?? null; if (empty($createTimeValue) || !is_numeric($createTimeValue)) { - $createTimeValue = time() * 1000; // 当前时间的毫秒时间戳 + $createTimeValue = time() * 1000; } - $createTime = date('YmdHis', intval($createTimeValue / 1000)); + $orderDt = date('YmdHis', intval($createTimeValue / 1000)); - // 获取订单地址信息(Shopro将地址存储在单独的表中) - $orderAddress = Db::name('shopro_order_address') - ->where('order_id', $order['id']) - ->find(); + // 处理订单过期时间 + $invDt = ''; + if (!empty($order['expiry_time'])) { + // Shopro 的 expiry_time 可能是时间戳或日期字符串 + if (is_numeric($order['expiry_time'])) { + // 如果是毫秒时间戳,需要除以1000 + $timestamp = intval($order['expiry_time']); + if ($timestamp > 9999999999) { + $timestamp = intval($timestamp / 1000); + } + $invDt = date('YmdHis', $timestamp); + } else { + $invDt = date('YmdHis', strtotime($order['expiry_time'])); + } + } - // 获取支付方式(Shopro将支付信息存储在单独的表中) - $payInfo = Db::name('shopro_pay') - ->where('order_id', $order['id']) - ->where('status', 'paid') - ->find(); + // 获取商品名称(取第一个商品) + $goodsName = ''; + if (!empty($orderItems)) { + $goodsName = $orderItems[0]['goods_title'] ?? ''; + // 如果有多个商品,可以拼接 + if (count($orderItems) > 1) { + $goodsName .= ' 等' . count($orderItems) . '件商品'; + } + } - // 获取快递信息(Shopro将快递信息存储在单独的表中) - $expressInfo = Db::name('shopro_order_express') - ->where('order_id', $order['id']) - ->find(); + // 构建优惠说明(如果有优惠金额) + $discountAmtDesc = ''; + if ($discountAmount > 0) { + // 格式:名称=金额|@|名称=金额 + // 这里简化处理,实际应该根据具体优惠券信息构建 + $discountAmtDesc = '平台优惠=' . $discountAmount; + } - // 构建订单数据(34个必填字段) - return [ - 'USER_ID' => $ccbUserId, // 建行用户ID + // 构建符合A3341TP01接口规范的订单数据 + $orderData = [ + // ========== 必填字段 ========== + 'USER_ID' => $ccbUserId, // 客户编号 'ORDER_ID' => $order['order_sn'], // 订单号 - 'ORDER_DT' => $createTime, // 订单时间 + 'ORDER_DT' => $orderDt, // 订单日期(yyyyMMddHHmmss) 'TOTAL_AMT' => $totalAmount, // 订单原金额 - 'PAY_AMT' => $payAmount, // 实付金额 - 'DISCOUNT_AMT' => $discountAmount, // 优惠金额 'ORDER_STATUS' => $this->mapOrderStatus($order['status']), // 订单状态 - 'REFUND_STATUS' => '0', // 退款状态(默认无退款) + 'REFUND_STATUS' => $this->mapRefundStatus($order['refund_status'] ?? 0), // 退款状态 'MCT_NM' => $this->config['merchant']['name'] ?? '商户名称', // 商户名称 - 'MCT_ORDER_ID' => $order['id'], // 商户订单ID - 'GOODS_LIST' => json_encode($goodsList, JSON_UNESCAPED_UNICODE), // 商品列表 - 'PAY_TYPE' => $this->mapPayType($payInfo['pay_type'] ?? ''), // 支付方式(从支付表获取) - 'PAY_TIME' => $payTime, // 支付时间(毫秒转秒) - 'DELIVERY_TYPE' => '01', // 配送方式(01快递) - 'DELIVERY_STATUS' => $this->mapDeliveryStatus($order['status']), // 配送状态 - 'DELIVERY_TIME' => !empty($expressInfo['createtime']) ? date('YmdHis', intval($expressInfo['createtime'] / 1000)) : '', // 发货时间 - 'RECEIVE_NAME' => $orderAddress['consignee'] ?? '', // 收货人姓名(从地址表获取) - 'RECEIVE_PHONE' => $orderAddress['mobile'] ?? '', // 收货人电话(从地址表获取) - 'RECEIVE_ADDRESS' => $this->buildAddress($order), // 收货地址(从地址表获取) - 'EXPRESS_COMPANY' => $expressInfo['express_name'] ?? '', // 快递公司(从快递表获取) - 'EXPRESS_NO' => $expressInfo['express_no'] ?? '', // 快递单号(从快递表获取) - 'REMARK' => $order['remark'] ?? '', // 备注 - 'ORDER_TYPE' => '01', // 订单类型(01普通订单) - 'IS_VIRTUAL' => '0', // 是否虚拟商品 - 'ORDER_URL' => $this->config['merchant']['order_detail_url'] . $order['id'], // 订单详情链接 - 'CREATE_TIME' => $createTime, // 创建时间 - 'UPDATE_TIME' => date('YmdHis'), // 更新时间 - 'SHOP_ID' => '1', // 店铺ID - 'SHOP_NAME' => $this->config['merchant']['name'] ?? '', // 店铺名称 - 'ACTIVITY_ID' => '', // 活动ID - 'ACTIVITY_NAME' => '', // 活动名称 - 'COUPON_AMT' => number_format($order['coupon_discount_fee'] ?? 0, 2, '.', ''), // 优惠券金额 - 'FREIGHT_AMT' => number_format($order['dispatch_amount'] ?? 0, 2, '.', ''), // 运费(Shopro字段名为dispatch_amount) + 'PAY_FLOW_ID' => $payFlowId, // ✅ 支付流水号(使用控制器传入的统一流水号) + 'PAY_MRCH_ID' => $this->config['merchant_id'], // 支付商户号(必填!) + 'SKU_LIST' => $skuList, // 商品信息JSON字符串(必填!) + + // ========== 重要可选字段(强烈建议填写) ========== + 'PAY_AMT' => $payAmount, // 订单实际支付金额 + 'DISCOUNT_AMT' => $discountAmount, // 第三方平台优惠金额 + 'PLAT_ORDER_TYPE' => 'T0000', // 服务方订单类型(T0000-普通类型) + 'PLATFORM' => '99', // 下单场景(99-建行生活APP) ]; + + // ========== 条件可选字段(有值才添加) ========== + + // 优惠说明 + if (!empty($discountAmtDesc)) { + $orderData['DISCOUNT_AMT_DESC'] = $discountAmtDesc; + } + + // 订单过期时间 + if (!empty($invDt)) { + $orderData['INV_DT'] = $invDt; + } + + // 商品名称 + if (!empty($goodsName)) { + $orderData['GOODS_NM'] = mb_substr($goodsName, 0, 200); // 限制长度200字符 + } + + // 累计退款金额(如果有退款) + if ($totalRefundAmount > 0) { + $orderData['TOTAL_REFUND_AMT'] = $totalRefundAmount; + } + + return $orderData; } /** - * 构建商品列表 + * 构建符合建行规范的SKU商品列表(JSON字符串格式) * - * @param array $items 订单商品项 - * @return array + * 📋 建行 SKU_LIST 字段规范: + * + * 必填字段(4个): + * - SKU_NAME: 商品名称(必填) + * - SKU_REF_PRICE: 商品参考价(必填,支持小数最多2位) + * - SKU_NUM: 商品数量(必填,支持小数最多1位) + * - SKU_SELL_PRICE: 商品售价(必填,支持小数最多2位) + * + * ⚠️ 注意:Shopro字段映射 + * - goods_title → SKU_NAME(商品名称) + * - goods_original_price → SKU_REF_PRICE(商品原价作为参考价) + * - goods_num → SKU_NUM(购买数量) + * - goods_price → SKU_SELL_PRICE(商品实际售价) + * + * @param array $items 订单商品项数组 + * @return string JSON字符串格式的SKU列表 */ - private function buildGoodsList($items) + private function buildSkuList($items) { - $goodsList = []; + $skuList = []; foreach ($items as $item) { - $goodsList[] = [ - 'goods_id' => $item['goods_id'], - 'goods_name' => $item['goods_title'], - 'goods_price' => number_format($item['goods_price'], 2, '.', ''), - 'goods_num' => $item['goods_num'], - 'goods_amount' => number_format($item['goods_amount'], 2, '.', ''), - 'goods_image' => $item['goods_image'] ?? '', - 'goods_sku' => $item['goods_sku_text'] ?? '' + $skuList[] = [ + 'SKU_NAME' => $item['goods_title'], // 商品名称(必填) + 'SKU_REF_PRICE' => number_format($item['goods_original_price'] ?? $item['goods_price'], 2, '.', ''), // 商品参考价(必填) + 'SKU_NUM' => $item['goods_num'], // 商品数量(必填) + 'SKU_SELL_PRICE' => number_format($item['goods_price'], 2, '.', ''), // 商品售价(必填) ]; } - return $goodsList; - } - /** - * 构建收货地址 - * - * ⚠️ 注意:Shopro的收货地址存储在单独的表 shopro_order_address 中 - * - * @param array $order 订单数组 - * @return string - */ - private function buildAddress($order) - { - // 从订单地址表获取地址信息 - $orderAddress = Db::name('shopro_order_address') - ->where('order_id', $order['id']) - ->find(); - - if (!$orderAddress) { - return ''; - } - - $address = ''; - if (!empty($orderAddress['province_name'])) $address .= $orderAddress['province_name']; - if (!empty($orderAddress['city_name'])) $address .= $orderAddress['city_name']; - if (!empty($orderAddress['district_name'])) $address .= $orderAddress['district_name']; - if (!empty($orderAddress['address'])) $address .= $orderAddress['address']; - - return $address; + // 返回JSON字符串(不转义Unicode,保持中文可读) + return json_encode($skuList, JSON_UNESCAPED_UNICODE); } /** @@ -543,38 +550,6 @@ class CcbOrderService return '0'; } - /** - * 映射支付方式 - * - * @param string $payType 支付类型 - * @return string - */ - private function mapPayType($payType) - { - $payMap = [ - 'wechat' => '01', // 微信支付 - 'alipay' => '02', // 支付宝 - 'ccb' => '03', // 建行支付 - 'balance' => '04' // 余额支付 - ]; - - return $payMap[$payType] ?? '00'; - } - - /** - * 映射配送状态 - * - * @param string $status 订单状态 - * @return string - */ - private function mapDeliveryStatus($status) - { - if (in_array($status, ['unpaid', 'paid'])) return '0'; // 未发货 - if ($status == 'shipped') return '1'; // 已发货 - if (in_array($status, ['received', 'completed'])) return '2'; // 已收货 - return '0'; - } - /** * 批量同步订单 * 用于初始化或定时同步 diff --git a/addons/shopro/library/ccblife/CcbPaymentService.php b/addons/shopro/library/ccblife/CcbPaymentService.php index c0cc2a9..4b22e81 100644 --- a/addons/shopro/library/ccblife/CcbPaymentService.php +++ b/addons/shopro/library/ccblife/CcbPaymentService.php @@ -35,56 +35,13 @@ class CcbPaymentService throw new \Exception('建行生活配置文件不存在'); } - // 处理BASE64格式的密钥,添加PEM包装 - $this->config = $this->processPemKeys($this->config); + // ✅ 修复: 删除processPemKeys()调用 + // 密钥格式化统一由CcbRSA类处理,避免重复格式化导致OpenSSL ASN1解析错误 + // CcbRSA::formatPublicKey/formatPrivateKey 会在加密/解密时自动处理密钥格式 $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调用建行收银台 @@ -92,9 +49,10 @@ class CcbPaymentService * ⚠️ 注意:必须包含所有必需参数,签名前按ASCII排序 * * @param int $orderId Shopro订单ID + * @param string $payFlowId 支付流水号(由控制器统一生成) * @return array ['status' => bool, 'message' => string, 'data' => array] */ - public function generatePaymentString($orderId) + public function generatePaymentString($orderId, $payFlowId) { // ⚠️ 开启事务保护,确保数据一致性 Db::startTrans(); @@ -111,37 +69,133 @@ class CcbPaymentService throw new \Exception('订单状态不正确'); } - // 获取用户建行ID - $user = Db::name('user')->where('id', $order['user_id'])->field('ccb_user_id')->find(); + // 获取用户建行生活ID(用于订单推送) + $user = Db::name('user')->where('id', $order['user_id']) + ->field('ccb_user_id') + ->find(); if (empty($user['ccb_user_id'])) { - throw new \Exception('用户未绑定建行账号'); + throw new \Exception('用户未绑定建行生活账号'); } - // 生成支付流水号(使用订单号作为唯一标识) - $payFlowId = 'PAY' . date('YmdHis') . mt_rand(100000, 999999); + // ✅ 使用控制器传入的统一支付流水号(确保与订单推送使用同一流水号) + if (empty($payFlowId)) { + throw new \Exception('支付流水号不能为空'); + } - // 构建完整的支付参数(34个参数) + // ✅ 构建完整的48个支付参数(按照建行文档5.4完整参数定义) + // 基础商户参数(必须二选一:建行商户号组合 或 外部平台商户号) $paymentParams = [ - 'MERCHANTID' => $this->config['merchant_id'], // 商户代码 - 'POSID' => $this->config['pos_id'], // 柜台代码 - 'BRANCHID' => $this->config['branch_id'], // 分行代码 - 'ORDERID' => $payFlowId, // 支付流水号(必须唯一!) - 'PAYMENT' => number_format($order['pay_fee'], 2, '.', ''), // 支付金额(Shopro使用pay_fee) - 'CURCODE' => '01', // 币种(01=人民币) - 'TXCODE' => '520100', // 交易码(520100=即时支付) - 'REMARK1' => '', // 备注1 - 'REMARK2' => $this->config['service_id'], // 备注2(服务方编号) - 'TYPE' => '1', // 支付类型(1=个人) - 'GATEWAY' => '0', // 网关标志 - 'CLIENTIP' => $this->getClientIp(), // 客户端IP - 'REGINFO' => '', // 注册信息 - 'PROINFO' => $this->buildProductInfo($order), // 商品信息 - 'REFERER' => '', // 来源页面 - 'THIRDAPPINFO' => 'comccbpay1234567890cloudmerchant', // 第三方应用信息(固定值) - 'USER_ORDERID' => $order['order_sn'], // 商户订单号 - 'TIMEOUT' => date('YmdHis', strtotime('+30 minutes')) // 超时时间 + 'MERCHANTID' => $this->config['merchant_id'], // 商户代码(F=可选,但不用外部商户号时必填) + 'POSID' => $this->config['pos_id'], // 柜台代码(F) + 'BRANCHID' => $this->config['branch_id'], // 分行代码(F) + 'ORDERID' => $payFlowId, // 支付流水号(T=必送) + 'USER_ORDERID' => $order['order_sn'], // 用户订单号(T) + 'PAYMENT' => number_format($order['pay_fee'], 2, '.', ''), // 支付金额(T) + 'CURCODE' => '01', // 币种(T,01=人民币) + 'TXCODE' => '520100', // 交易码(T,520100=即时支付) + 'REMARK1' => '', // 备注1(T) + 'REMARK2' => $this->config['service_id'], // 备注2(T,服务方编号) + 'TYPE' => '1', // 接口类型(T,1=防钓鱼) + 'GATEWAY' => '0', // 网关类型(T) + 'CLIENTIP' => $this->getClientIp(), // 客户端IP(T) + 'REGINFO' => '', // 客户注册信息(T,中文需escape编码) + 'PROINFO' => $this->buildProductInfo($order), // 商品信息(T,中文已escape编码) + 'REFERER' => '', // 商户URL(T) + 'THIRDAPPINFO' => 'comccbpay1234567890cloudmerchant', // 客户端标识(T,固定值) + 'TIMEOUT' => date('YmdHis', strtotime('+30 minutes')), // 超时时间(F,格式YYYYMMDDHHmmss) ]; + // ✅ 可选参数(根据实际场景添加) + // 外部平台商户号(与建行商户号二选一) + if (!empty($this->config['plat_mct_id'])) { + $paymentParams['PLATMCTID'] = $this->config['plat_mct_id']; + // 使用外部商户号时,删除建行商户号 + unset($paymentParams['MERCHANTID'], $paymentParams['POSID'], $paymentParams['BRANCHID']); + } + + // 微信支付19位终端号 + if (!empty($this->config['pos_id_19'])) { + $paymentParams['POSID19'] = $this->config['pos_id_19']; + } + + // 支付位图(控制支付方式:生活钱包/龙支付/微信/数币/信用付/快贷) + if (!empty($this->config['pay_bitmap'])) { + $paymentParams['PAYBITMAP'] = $this->config['pay_bitmap']; + } + + // 账户位图(控制支付账户:建行借记卡/贷记卡/他行借记卡/贷记卡/建行钱包) + if (!empty($this->config['account_bitmap'])) { + $paymentParams['ACCOUNTBITMAP'] = $this->config['account_bitmap']; + } + + // 分期期数 + if (!empty($this->config['install_num'])) { + $paymentParams['INSTALLNUM'] = $this->config['install_num']; + } + + // 积分二级活动编号 + if (!empty($this->config['point_avy_id'])) { + $paymentParams['POINTAVYID'] = $this->config['point_avy_id']; + } + + // 数字人民币参数 + if (!empty($this->config['dcep_mct_type'])) { + $paymentParams['DCEP_MCT_TYPE'] = $this->config['dcep_mct_type']; + if ($this->config['dcep_mct_type'] == '2') { + // 非融合商户需要填写数币商户号 + $paymentParams['DCEP_MERCHANTID'] = $this->config['dcep_merchant_id'] ?? ''; + $paymentParams['DCEP_POSID'] = $this->config['dcep_pos_id'] ?? ''; + $paymentParams['DCEP_BRANCHID'] = $this->config['dcep_branch_id'] ?? ''; + } + if (!empty($this->config['dcep_dep_acc_no'])) { + $paymentParams['DCEPDEPACCNO'] = $this->config['dcep_dep_acc_no']; + } + } + + // 有价券活动编号 + if (!empty($this->config['coupon_avy_id'])) { + $paymentParams['COUPONAVYID'] = $this->config['coupon_avy_id']; + } + + // 限制信用卡支付标志 + if (!empty($this->config['only_credit_pay_flag'])) { + $paymentParams['ONLY_CREDIT_PAY_FLAG'] = $this->config['only_credit_pay_flag']; + } + + // 固定抵扣积分值 + if (!empty($this->config['fixed_point_val'])) { + $paymentParams['FIXEDPOINTVAL'] = $this->config['fixed_point_val']; + } + + // 最小使用积分抵扣限制 + if (!empty($this->config['min_point_limit'])) { + $paymentParams['MINPOINTLIMIT'] = $this->config['min_point_limit']; + } + + // 扩展域(JSON格式,需encodeURI) + if (!empty($this->config['extend_params'])) { + $paymentParams['EXTENDPARAMS'] = urlencode($this->config['extend_params']); + } + + // 二级商户参数(平台类服务方使用) + if (!empty($this->config['sub_mct_id'])) { + $paymentParams['SUB_MCT_ID'] = $this->config['sub_mct_id']; + } + if (!empty($this->config['sub_mct_name'])) { + $paymentParams['SUB_MCT_NAME'] = $this->config['sub_mct_name']; + } + if (!empty($this->config['sub_mct_mcc'])) { + $paymentParams['SUB_MCT_MCC'] = $this->config['sub_mct_mcc']; + } + + // 场景编号(埋点使用,不参与MAC校验) + if (!empty($this->config['scn_id'])) { + $paymentParams['SCNID'] = $this->config['scn_id']; + } + if (!empty($this->config['scn_pltfrm_id'])) { + $paymentParams['SCN_PLTFRM_ID'] = $this->config['scn_pltfrm_id']; + } + // 按ASCII排序 ksort($paymentParams); @@ -151,13 +205,13 @@ class CcbPaymentService // ⚠️ 建行支付串签名规则(v2.2版本): // 1. PLATFORMPUB字段仅参与MD5计算,不作为HTTP参数传递 // 2. 签名 = MD5(参数字符串 + &PLATFORMPUB= + 服务方公钥内容) - // 3. 生成32位大写MD5字符串(对照MD5Util.java第30行) + // 3. 生成32位小写MD5字符串(根据建行文档5.4.1要求) $platformPubKey = $this->config['public_key']; // 服务方公钥 - $mac = strtoupper(md5($signString . '&PLATFORMPUB=' . $platformPubKey)); + $mac = strtolower(md5($signString . '&PLATFORMPUB=' . $platformPubKey)); - // 使用RSA加密商户公钥后30位(用于ENCPUB字段) - $encryption = new CcbEncryption($this->config); - $encpub = $encryption->encryptMerchantPublicKeyLast30(); + // ✅ 修复:使用 CcbRSA 加密商户公钥后30位(用于ENCPUB字段) + // 删除 CcbEncryption 类,统一使用 CcbRSA 处理密钥格式化 + $encpub = $this->encryptPublicKeyLast30(); // 组装最终支付串 $finalPaymentString = $signString . '&MAC=' . $mac . '&PLATFORMID=' . $this->config['service_id'] . '&ENCPUB=' . urlencode($encpub); @@ -168,9 +222,6 @@ class CcbPaymentService 'updatetime' => time() ]); - // 构建完整的支付URL - $paymentUrl = $this->config['cashier_url'] . '?' . $finalPaymentString; - // 记录支付请求 $this->recordPaymentRequest($orderId, [ 'payment_string' => $finalPaymentString, @@ -188,7 +239,6 @@ class CcbPaymentService 'data' => [ 'payment_string' => $finalPaymentString, 'mac' => $mac, - 'payment_url' => $paymentUrl, 'order_sn' => $order['order_sn'], 'pay_flow_id' => $payFlowId, 'amount' => number_format($order['pay_fee'], 2, '.', ''), @@ -225,11 +275,47 @@ class CcbPaymentService } } + /** + * 实现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; + } + /** * 构建商品信息字符串 + * 根据建行文档4.3:商品信息中文需使用escape()编码 * * @param object $order 订单对象 - * @return string + * @return string escape编码后的商品信息 */ private function buildProductInfo($order) { @@ -240,11 +326,51 @@ class CcbPaymentService ->column('goods_title'); if (empty($orderItems)) { - return '商城订单'; + $productInfo = '商城订单'; + } else { + // 拼接商品名称,建议不超过50字符(编码前) + $productInfo = mb_substr(implode(',', $orderItems), 0, 50, 'UTF-8'); } - // 拼接商品名称 - return implode(',', $orderItems); + // ✅ 使用JavaScript的escape()编码中文 + return $this->jsEscape($productInfo); + } + + /** + * 加密商户公钥后30位(用于支付串的ENCPUB字段) + * + * 根据建行文档v2.2规范: + * "使用服务方公钥对商户公钥后30位进行RSA加密并base64后的密文" + * + * @return string BASE64编码的加密密文 + * @throws \Exception + */ + private function encryptPublicKeyLast30() + { + $publicKey = $this->config['public_key'] ?? ''; + + if (empty($publicKey)) { + throw new \Exception('服务方公钥未配置'); + } + + // 去除 PEM 格式的头尾和空白字符,获取纯 BASE64 内容 + $publicKeyContent = str_replace([ + '-----BEGIN PUBLIC KEY-----', + '-----END PUBLIC KEY-----', + '-----BEGIN RSA PUBLIC KEY-----', + '-----END RSA PUBLIC KEY-----', + "\r", "\n", " ", "\t" + ], '', $publicKey); + + // 取后30位 + $last30Chars = substr($publicKeyContent, -30); + + if (strlen($last30Chars) < 30) { + throw new \Exception('商户公钥长度不足30位'); + } + + // ✅ 使用 CcbRSA 类进行加密(统一密钥格式化逻辑) + return CcbRSA::encrypt($last30Chars, $publicKey); } /** @@ -298,8 +424,8 @@ class CcbPaymentService // 支付成功,更新订单状态 $this->updateOrderPaymentStatus($order, $params); - // 同步订单到建行 - $this->orderService->pushOrder($order['id']); + // ✅ 更新订单状态到建行(订单已在createPayment时推送,这里只需更新状态) + $this->orderService->updateOrderStatus($order['id']); return [ 'status' => true, @@ -431,26 +557,6 @@ class CcbPaymentService } } - /** - * 构建支付URL - * - * @param array $params 支付参数 - * @param string $mac 签名 - * @return string - */ - private function buildPaymentUrl($params, $mac) - { - // 添加必要参数 - $params['MAC'] = $mac; - $params['REMARK2'] = $this->config['service_id']; // 服务方编号 - - // 生成查询字符串 - $queryString = http_build_query($params); - - // 返回完整URL(实际使用时通过JSBridge调用,不直接访问) - return $this->config['cashier_url'] . '?' . $queryString; - } - /** * 更新订单支付状态 * diff --git a/addons/shopro/library/ccblife/CcbRSA.php b/addons/shopro/library/ccblife/CcbRSA.php index 402e3f4..69437a2 100644 --- a/addons/shopro/library/ccblife/CcbRSA.php +++ b/addons/shopro/library/ccblife/CcbRSA.php @@ -140,9 +140,10 @@ class CcbRSA return $publicKey; } - // 格式化为PEM格式 + // ✅ 修复: chunk_split()会在末尾添加换行符,需要用rtrim()去除 + // 否则会导致PEM格式中密钥内容和尾部之间有多余空行,OpenSSL解析失败 $pem = "-----BEGIN PUBLIC KEY-----\n"; - $pem .= chunk_split($publicKey, 64, "\n"); + $pem .= rtrim(chunk_split($publicKey, 64, "\n"), "\n") . "\n"; $pem .= "-----END PUBLIC KEY-----\n"; return $pem; @@ -166,9 +167,10 @@ class CcbRSA return $privateKey; } - // 格式化为PEM格式 + // ✅ 修复: chunk_split()会在末尾添加换行符,需要用rtrim()去除 + // 否则会导致PEM格式中密钥内容和尾部之间有多余空行,OpenSSL解析失败 $pem = "-----BEGIN RSA PRIVATE KEY-----\n"; - $pem .= chunk_split($privateKey, 64, "\n"); + $pem .= rtrim(chunk_split($privateKey, 64, "\n"), "\n") . "\n"; $pem .= "-----END RSA PRIVATE KEY-----\n"; return $pem; diff --git a/doc/CCBLife小程序API使用说明_v1.1_20230511.html b/doc/CCBLife小程序API使用说明_v1.1_20230511.html deleted file mode 100644 index b1f76f2..0000000 --- a/doc/CCBLife小程序API使用说明_v1.1_20230511.html +++ /dev/null @@ -1,599 +0,0 @@ - - - - - -CCBLife小程序API使用说明 - -
-

CCBLife小程序API使用说明_v1.1_20230511

文档修订记录

版本日期修订说明
1.02023.02.14同步在线文档接口说明
1.12023.05.11新增实名认证api

文档目录

1. 文档说明

本文档所描述API适用于建行生活App端内运行的JUMP小程序。

2. 接口说明

回调函数统一格式:

回调结果参数(Object res)

属性类型说明最低版本
dataobject返回内容-
statestring状态码-
msgstring状态信息|报错信息-

响应内容封装在data的Json对象里

2.1 login

用途说明

登录|获取用户信息。提供客户端认证模式与服务端认证模式两种模式。若建行生活处于未登录状态会跳转建行生活APP的登录页进行登录(行内单点登录使用)。

请求参数

属性类型默认值必填说明最低版本
typenumber-登录类型-
PLATFORM_IDstring--服务方ID-
Opn_Chnl_IDstring-合作方渠道编号-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

type 的合法值:

说明最低版本
0客户端认证模式-
1服务端认证模式-

响应内容

属性类型说明最低版本
encryptedDatastring"userid=xxx&mobile=xxx&PreAhr_ID=xxx"的加密字符串。(userid:建行生活用户编号,mobile:手机号,PreAhr_ID:用户中心预授权编码)-

注意

2.2 ccblife_login

用途说明

登录|获取用户信息。提供客户端认证模式与服务端认证模式两种模式。若建行生活处于未登录状态会跳转建行生活APP的登录页进行登录。

请求参数

属性类型默认值必填说明最低版本
typenumber-登录类型-
PLATFORM_IDstring--服务方ID-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

type 的合法值:

说明最低版本
0客户端认证模式-
1服务端认证模式-

响应内容

属性类型说明最低版本
encryptedDatastring"unionid=xxx&phone=xxx&locationCityCode=xxx"的加密字符串。(unionid:建行生活用户编号,phone:手机号,locationCityCode:用户选择城市码)-

注意

2.3 checkSession

用途说明

检查登录态是否过期。

请求参数

属性类型默认值必填说明最低版本
PLATFORM_IDstring--服务方ID-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

响应内容

属性类型说明最低版本
isVaildboolean登录态是否有效-

2.4 getUserInfo

用途说明

获取用户信息。目前能返回的信息均为登录态敏感信息,加密。

请求参数

属性类型默认值必填说明最低版本
withCredentialsbooleantrue是否带上登录态信息。-
loginTypenumber-当前登录类型-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

loginType的合法值:

说明最低版本
0客户端认证模式-
1服务端认证模式-

响应内容

state的合法值:

说明最低版本
0获取成功-
1获取失败-
2获取失败:未授权-

data:

属性类型说明最低版本
userInfoObject用户信息,不包含敏感数据-
encryptedDatastring包括敏感数据在内的完整用户信息的加密数据-
signaturestring用户数据签名-
saltstring签名使用的字符串-
ivstring加密算法的初始向量-

encryptedData 解密:

属性类型说明最低版本
unionidstring建行生活平台帐号的唯一标识-
openidstring用户在当前小程序的唯一标识-
cityCodeString用户选择城市编码 
locationCityCodestring用户当前定位城市编码-
registerCityCodestring用户归属城市编码,即用户注册地-
phonestring用户手机号-

注意:

2.5 authorize

用途说明

提前向用户发起授权请求。

请求参数

属性类型默认值必填说明最低版本
scopestring-需要获取权限的 scope-
successfunction-接口调用成功的回调函数(授权成功)-
failfunction-接口调用失败的回调函数(授权失败)-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

scope说明:

scope对应接口说明最低版本
scope.userInfogetUserInfo、login用户信息-
scope.camerascanCode摄像头-

2.6 requestPayment

用途说明

调用建行生活收银台。

请求参数

属性类型默认值必填说明最低版本
payInfostring-支付参数-

payInfo参数内容:

属性类型可为空必填说明最低版本
MERCHANTIDchar(15)YF商户代码;由建行统一分配-
POSIDchar(9)YF柜台代码;由建行统一分配-
BRANCHIDchar(9)YF分行代码;由建行统一分配-
POSID19char(19)NF商户19位终端号;由建行统一分配,使用微信支付时上送。仅作为参数传递,不参与MAC校验-
PLATMCTIDchar(19)YF外部平台商户号;当使用外部商户号时,建行商户号、柜台号、分行号及终端号无需上送。当该字段有值时参与MAC校验,否则不参与MAC校验-
ORDERIDchar(30)YT订单号;由商户提供,最长30位-
PAYMENTnumber(16,2)YT付款金额;由商户提供,最长30位-
CURCODEchar(2)YT币种;缺省为01-人民币(只支持人民币支付)-
TXCODEchar(6)YT交易码;由建行统一分配为520100-
REMARK1char(30)NT备注1;网银不处理,直接传到城综网,该字段只支持送数字和英文-
REMARK2char(30)NT备注2;上送YS开头的服务方编号,与PLATFORMID保持一致-
TYPEchar(1)YT接口类型;1- 防钓鱼接口-
GATEWAYchar(100)YT网关类型;默认送0-
CLIENTIPchar(40)NT客户端IP;客户在商户系统中的IP-
REGINFOchar(256)NT客户注册信息;客户在商户系统中注册的信息,中文需使用escape编码-
PROINFOchar(256)NT商品信息;客户购买的商品,中文需使用escape编码-
REFERERchar(100)NT商户URL;商户送空值即可-
INSTALLNUMchar(2)NF分期期数;信用卡支付分期期数,一般为 3、6、12 等,必须为大于 1 的整数。 仅当分期支付时上送该字段,无此字段上送时,则视为普通支付。-
THIRDAPPINFOchar(40)YT客户端标识;通过建行生活APP下单场景,订单中客户端标识固定设为comccbpay1234567890cloudmerchant-
TIMEOUTchar(14)NF订单超时时间;格式:YYYYMMDDHHMMSS(如:20120214143005)
银行系统时间> TIMEOUT时拒绝交易,若送空值则不判断超时。
当该字段有值时参与MAC校验,否则不参与MAC校验。
-
USERIDchar(100)NF在中国建设银行App环境需提供。 当该字段有值时参与MAC校验,否则不参与MAC校验-
TOKENchar(100)NF在中国建设银行App环境需提供。 当该字段有值时参与MAC校验,否则不参与MAC校验-
PAYSUCCESSURLchar(100)NF在中国建设银行App环境考虑,如需指定支付成功页面需提供,需对URL编码,生产环境必须为HTTPS。未提供则默认跳转到建行生活的支付成功页面 当该字段有值时参与MAC校验,否则不参与MAC校验-
PAYBITMAPchar(10)NF支付位图;默认为空,只需要展示龙支付时请送0100000000
当该字段有值时参与MAC校验,否则不参与MAC校验。
-
POINTAVYIDvarchar(6)NF积分二级活动编号;默认为空,特定场景使用。龙支付积分二级活动上送 010051-
DCEPDEPACCNOvarchar(32)NF数字人民币收款钱包编号;默认为空,特定场景使用。数字人民币商户绑定的收款钱包编号-
COUPONAVYIDvarchar(32)NF有价券活动编号;默认为空,特定场景使用。-
ONLY_CREDIT_PAY_FLAGvarchar(1)NF限制信用卡支付标志;默认为空,特定场景使用。当有价券活动编号不为空时生效,送Y限制仅信用卡能支付,送N或空不作限制-
FIXEDPOINTVALvarchar(16)NF固定抵扣积分值;默认为空,特定场景使用。上送该值时,若用户不满足积分使用条件将拒绝支付-
EXTENDPARAMSvarchar(256)NF积分二级活动编号;默认为空,特定场景使用。上送约定JSON格式字符串-
PLATFORMPUBvarchar(256)YF服务方公钥;仅作为源串参加MD5摘要,不作为参数传递-
MACchar(32)TTMD5加密串;采用标准MD5算法,对以上字段进行MAC加密(32位小写),由商户实现。-
PLATFORMIDchar(16)YT服务方编号;仅作为参数传递,不参与MAC校验-
ENCPUBvarchar(512)YF商户公钥密文;使用服务方公钥对商户公钥后30位进行RSA加密并base64后的密文。
若商户已经上架建行生活并同步公钥,可以不再上送商户公钥。仅作为参数传递,不参与MAC校验
-
SCNIDchar(32)NF场景编号;默认为空,埋点使用。特色场景的唯一标识。仅作为参数传递,不参与MAC校验-
SCN_PLTFRM_IDchar(32)NF场景平台编号;默认为空,埋点使用。场景平台唯一标识。仅作为参数传递,不参与MAC校验 

注意:

2.7 ccblife_requestPayment

用途说明

调用建行生活收银台。

请求参数

属性类型默认值必填说明最低版本
miniIdstring-本小程序id-
successPagestring-成功页面的路径,不设置则跳转建行生活APP的支付成功页面-
payInfoobject-支付参数-

payInfo参数内容

属性类型可为空必填说明最低版本
MERCHANTIDchar(15)商户代码;由建行统一分配-
POSIDchar(9)柜台代码;由建行统一分配-
BRANCHIDchar(9)分行代码;由建行统一分配-
POSID19char(19)商户19位终端号;由建行统一分配,仅作为参数传递,不参与MAC校验-
ORDERIDchar(30)订单号;由商户提供,最长30位-
PAYMENTnumber(16,2)付款金额;由商户提供,最长30位-
CURCODEchar(2)币种;缺省为01-人民币(只支持人民币支付)-
TXCODEchar(6)交易码;由建行统一分配为520100-
REMARK1char(30)备注1;网银不处理,直接传到城综网,该字段只支持送数字和英文-
REMARK2char(30)备注2;上送YS开头的服务方编号-
TYPEchar(1)接口类型;1- 防钓鱼接口-
GATEWAYchar(100)网关类型;默认送0-
CLIENTIPchar(40)客户端IP;客户在商户系统中的IP-
REGINFOchar(256)客户注册信息;客户在商户系统中注册的信息,中文需使用escape编码-
PROINFOchar(256)商品信息;客户购买的商品,中文需使用escape编码-
REFERERchar(100)商户URL;商户送空值即可-
THIRDAPPINFOchar(40)客户端标识;通过建行生活APP下单场景,订单中客户端标识固定设为comccbpay1234567890cloudmerchant-
TIMEOUTchar(14)订单超时时间;格式:YYYYMMDDHHMMSS(如:20120214143005)
银行系统时间> TIMEOUT时拒绝交易,若送空值则不判断超时。
当该字段有值时参与MAC校验,否则不参与MAC校验。
-
PAYBITMAPchar(10)支付位图;默认为空,只需要展示龙支付时请送0100000000
当该字段有值时参与MAC校验,否则不参与MAC校验。
-
PLATFORMPUBvarchar(256)服务方公钥;仅作为源串参加MD5摘要,不作为参数传递-
MACchar(32)MD5加密串;采用标准MD5算法,对以上字段进行MAC加密(32位小写),由商户实现。-
PLATFORMIDchar(16)服务方编号;仅作为参数传递,不参与MAC校验-
ENCPUBvarchar(512)商户公钥密文;使用服务方公钥对商户公钥后30位进行RSA加密并base64后的密文。
若商户已经上架建行生活并同步公钥,可以不再上送商户公钥。仅作为参数传递,不参与MAC校验
-

注意:

2.8 navigateTo

用途说明

跳转建行生活页面 | 外部H5页|建信小程序 | 微信小程序。

请求参数

属性类型默认值必填说明最低版本
typenumber-跳转页面类型-
toPagestring-跳转路径,类型为小程序本值为空时调起首页。-
isNewViewbooleanfalse是否打开新WebView,当type为1或2时有效。值为false时用进入小程序前入口所在页面的webview打开,若不存在则用新webview打开。-
isShowHeaderbooleanfalse是否展示通用标题栏,当type为1或2时有效-
headerNamestring-标题栏名称,isShowHeader为true时有效-
headerRightTypenumber0展示标题栏时右边按钮的类型-
paramstring-跳转携带的参数,type非0时拼接到最终URL后-
miniIdstring-跳转小程序id,type为3或4时有效-
miniVersionnumber0微信小程序版本,type为4时有效 
successfunction-接口调用的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

type 的合法值:

说明最低版本
0建行生活原生页面-
1建行生活H5页面-
2外部H5页面-
3Jump小程序-
4微信小程序-

headerRightType 的合法值:

说明最低版本
0关闭按钮-
1分享按钮-

miniVersion 的合法值:

说明最低版本
0发布版-
1预览版-
2测试版-

响应内容

state 的合法值:

说明最低版本
0跳转成功-
1跳转失败-

2.9 scanCode

用途说明

建行生活App的扫码功能,扫描建行生活提供的业务二维码(扫码支付等)需要用本接口而非jump.scanCode

请求参数

属性类型默认值必填说明最低版本
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

响应内容

属性类型说明最低版本
contentstring扫码结果-

state 的合法值:

说明最低版本
0扫码成功-
1非有效业务二维码,无法解析-

2.10 openPayCode

用途说明

打开支付码。

请求参数

属性类型默认值必填说明最低版本
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

响应内容

state 的合法值:

说明最低版本
0打开成功-
1打开失败-

2.11 callMap

用途说明

调起手机内的地图App。支持苹果地图|高德地图|百度地图。

请求参数

属性类型默认值必填说明最低版本
paramsobject-参数-
needNavigationbooleanfalse是否需要导航-
addressstring-商户地址-
lgtnumber-商户纬度-
lttnumber-商户经度-
cityNamestring-城市名称-
business_namestring-商户名称-
self_lgtnumber-客户维度-
self_lttnumber-客户经度-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

注意:

响应内容

state 的合法值:

说明最低版本
0调起地图成功-
1调起地图失败-

2.12 startFaceScan

用途说明

刷脸认证|人脸校验

请求参数

属性类型默认值必填说明最低版本
PLATFORM_IDstring-服务方ID-
namestring--姓名-
cardTypestring-卡片类型(身份证)-
cardNumstring-身份证号码-
phoneNumstring-手机号-
showErrorstring-报错弹窗:1-显示 0-不显示-
scanOnlystring-仅刷脸:1-只刷脸,不发校验刷脸流水的交易-
Stm_Chnl_IDstring-渠道号,默认为建行生活渠道-
Stm_Chnl_Txn_CDstring-渠道交易码,默认为建行生活渠道交易码-
txCodestring-安全交易码,默认为建行生活安全交易码-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

响应内容

属性类型说明最低版本
successstring刷脸认证是否成功:0-失败,1-成功-
Comm_Auth_FieldsstringUUID-
Apl_Aply_TrcNostring全局流水号-

2.13 userStatus

用途说明

获取用户状态信息

请求参数

属性类型默认值必填说明最低版本
PLATFORM_IDstring--服务方ID-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

响应内容

属性类型说明最低版本
userTypestring用户类型:00游客/未登录 01钱包用户 02已注册未开钱包-
isLoginstring登录状态:0:未登录 1:已登录-

2.14 share

用途说明

分享。

请求参数

属性类型默认值必填说明最低版本
share_idstring-分享id,存在分享ID时先调接口获取分享内容-
textstring--分享的描述-
titlestring--标题-
urlstring--链接-
imagestring--图片链接-
typestring--0--分享链接,1--分享微信朋友,2--分享朋友圈-
base64Picstring--图片base64格式化-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

响应内容

属性类型说明最低版本
typestring0: 取消分享 1:分享到微信 2:分享到朋友圈-

2.15 checkUser

用途说明

校验用户身份。

请求参数

属性类型默认值必填说明最低版本
platformIdstring-服务方编号,非空-
sceneIdstring--场景ID-
checkTypestring--校验类型 1-校验平台支付密码 2-校验平台登录密码-
checkScopestring--验密有效范围,0-App内有效(默认值) 1-同类场景有效 2-场景内有效 3-场景内同功能有效 4-一次性有效-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

响应内容

属性类型说明最低版本
tokenstring唯一验密流水号,每次重新生成-
codestring校验结果状态码
0:校验完成
1:已在其他功能场景校验通过,且在有效期内
-1:用户取消校验
-2:校验失败,当前场景未配置校验类型、用户状态异常等原因
-

注意:

2.15 RealNameAuthorization

用途说明

实名认证。

请求参数

属性类型默认值必填说明最低版本
platformIdstring-服务方编号,非空-
successfunction-接口调用成功的回调函数-
failfunction-接口调用失败的回调函数-
completefunction-接口调用结束的回调函数(调用成功、失败都会执行)-

响应内容

属性类型说明最低版本
successstring实名结果 0-失败,1-成功-
- - \ No newline at end of file diff --git a/doc/UrlMain跳转链接解密可参考此demo2.java b/doc/UrlMain跳转链接解密可参考此demo2.java deleted file mode 100644 index 1fdd5cc..0000000 --- a/doc/UrlMain跳转链接解密可参考此demo2.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.example.filedemo.util.fuwufang; - -import com.example.filedemo.util.RSAUtil; -import sun.misc.BASE64Decoder; -import sun.misc.BASE64Encoder; - -public class UrlMain { - public static void main(String[] args) throws Exception { - String msg = "BGCOLOR=&userid=YSM202111170063936&mobile=18242028306&cityid=330100&userCityId=330100&orderid=&PLATFLOWNO=0000A2UNK1639016304462982&openid=&lgt=113.3295774824442<t=23.12339638654285&Usr_Name=&USERID=YSM202111170063936&MOBILE=18242028306&CITYID=330100&USERCITYID=330100&ORDERID=&OPENID=&LGT=113.3295774824442<T=23.12339638654285"; - - //String enc_msg = "SDB0dllqYmxFS2xHRmlqa1ZaOFk0OHBXY0I5TitoREdJaVB3K1pjM2M3dy9jek4zN016ZUoxZENTNTVLWVFFV3VSYzlYOVlXRkpBcQpWRUgwaDJUMG04V2lmNHJyS3krdG5QUDJHalhEQlNma21oR3JrV0lsbFRibC9vbWJONGxqeVk1TXZQWjVWc2t5N2ZVRlZTYlNlYjIzCnJ5cFN4dTRNSDUrTjFRTU5NVFE9Cg%3D%3D"; - // 公钥 - String publicKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQClMNB2rs4PMyxHdV+HeISWBbe55WQkmSYQQvFq8M4MMczhYihhp1Z9p723wD8cv9m/PQQcQZuNIehGGIIbZnMZFkqwDYUODH0DF8N5o7BiUhw/XUr3nl49/hsjlE6L7k/7jYzxZ+r3CXhz7qVXZNW6tD2RM+AI4qomQr0p1VNxhQIDAQAB"; - // 私钥 - String privateKey = "MIICdgIBADANBgkqhkiG9w0BAQEFAASCAmAwggJcAgEAAoGBAKUw0Hauzg8zLEd1X4d4hJYFt7nlZCSZJhBC8WrwzgwxzOFiKGGnVn2nvbfAPxy/2b89BBxBm40h6EYYghtmcxkWSrANhQ4MfQMXw3mjsGJSHD9dSveeXj3+GyOUTovuT/uNjPFn6vcJeHPupVdk1bq0PZEz4AjiqiZCvSnVU3GFAgMBAAECgYAyTZQdoAulu0qPlCF8CmotmR4ioMUHFA/wQcJsc1n7gqrGM3LikeeXqh3ut79ATPfM8ZKv3Ba3Oo0V017DY0ZG7j2stXxFhm2ln/q6nfaDsfx5ae22kIdNFCrDfwYByBiVsZPNCrj+8qDb/DPiVveEpsj7hn6thZY8QnjwEi0O3QJBAOia3cqup/rLMTYwtl43OREyMDt3qWS+aRQz1jQJlQSONV76qsZpZZUVxQEglvf6+afRCyn1mAqNa2dek6gbHTMCQQC1zijBYb6b4kghbKg/ZC37A79kBuRKtl/yIMYtFLWrtIntv047HavVPHZLEl++44Hk+9rfzNw1J12uXigGVoZnAkBGh6745jzJLxOc+uhRaS1EqZM2dPJIOfRiy9UHsmAdIYHNavSddRf4PMGfteIRD2jkGd7oui+AA6Gtll/veUlBAkAwybEwK/3NsUywA4um70hTiy7qNds/nW9j952W7W7PNDSrY2IoBQ9eusn33WdqP31VKK0Uz9HsRbMjHstY4BFTAkEAisda+CJkO/Epdj693ewIr4GbGORGSVB2pCjLGPqhuvu37d/T9+9T85BoeaMwm31aVNGOPIUCSPOMelKRUoj3Gw=="; - - // 公钥加密得到密文并使用base64处理 - String enc_msg = RSAUtil.encrypt(msg, publicKey); - //enc_msg = ""; - //enc_msg = "TVpoZ040QTdVcTZMem11cGdTVjdWeFEvWU5kZWZqcFVBK3JSS0hGK0NsSFo0Ly82RnV3blVvT1hrMXNlZ25odXlkVytvRkdGT0xoYwpRSlhjYWI4eWJkUWh3UTgzUU1MQzBzYWtManZCOFNhR29VWDhiMHZoRXhWdkVIQ3BTcnlVRUQwUU9zQzVodXoxQTk3djRIeXNlMFR5CjFnWm1zaTRLZm9sWHM1TndJcXMvQ3lVdE9MTjNqZGZoajNHTXQvSHN1c01PVzFBekwxbTFTWVk2VGhuL3lvODA4NGRRaGN2aC9KVU0KdUdWZUNtdEJJbytHVVJaOXNCZW5BaUZVTEFWTHJINHhRK0pWUnpRRUZvRlhhNVVaRVQ0cDJTME1TWXBTZ1R4eStOeFloTER1S280OQpsV3RONFVKVkJiTVY1dUJrdEthMEF6MEROczA1bndDTnJtL3FReFdQMmFtV3pRT24ybHl6MVRCMXhjZGpaVVpDc3prOVZoM3FtS05SCmNVTnV2M0x6cDB2SzIzOVFyM205R0taWGl6TFdKcktQcE9UaDkrODQyY0s3L1ZmTDh6S2NBZG1QN21QMXhKZE5NVlEzZFZnaW9XWlQKd2xWWVc4N2FLZ0xLNlZIaEhJNTVGV3RNaFYrTzdNbUZlSzBWckhaS3NHSDJIUGg5ZUQyVkFVWVNWaU9HdisyUDRFczJ5V0lCNjkxNgpjMStRWjc5RXdwRmwrQjBJUG0yLy9rbDVXSXVYaDdXY1FPUzBBMm4yMnYxWnlMK29vdVNUTWNGMm9TNWx1RjRzRVc5VDkrb0tjRjU5CjBVOGVvY1E0R2Q5dkxwTnVpSG1CNUpvWFF6NG1EZGFBdG13eS9zMHFXVTgyQjlWZEVETlNPZGx0NnNoQXBTZ01ObGtGS0hyQllHMD0K"; - - BASE64Encoder encoder = new BASE64Encoder(); - enc_msg = encoder.encode(enc_msg.getBytes("UTF-8")); - enc_msg = enc_msg.replaceAll("\r\n", "").replaceAll("\r", "").replaceAll("\n", ""); - - - System.out.println("公钥加密得到密文并使用base64处理:"); - //enc_msg = "TVpoZ040QTdVcTZMem11cGdTVjdWeFEvWU5kZWZqcFVBK3JSS0hGK0NsSFo0Ly82RnV3blVvT1hrMXNlZ25odXlkVytvRkdGT0xoYwpRSlhjYWI4eWJkUWh3UTgzUU1MQzBzYWtManZCOFNhR29VWDhiMHZoRXhWdkVIQ3BTcnlVRUQwUU9zQzVodXoxQTk3djRIeXNlMFR5CjFnWm1zaTRLZm9sWHM1TndJcXMvQ3lVdE9MTjNqZGZoajNHTXQvSHN1c01PVzFBekwxbTFTWVk2VGhuL3lvODA4NGRRaGN2aC9KVU0KdUdWZUNtdEJJbytHVVJaOXNCZW5BaUZVTEFWTHJINHhRK0pWUnpRRUZvRlhhNVVaRVQ0cDJTME1TWXBTZ1R4eStOeFloTER1S280OQpsV3RONFVKVkJiTVY1dUJrdEthMEF6MEROczA1bndDTnJtL3FReFdQMmFtV3pRT24ybHl6MVRCMXhjZGpaVVpDc3prOVZoM3FtS05SCmNVTnV2M0x6cDB2SzIzOVFyM205R0taWGl6TFdKcktQcE9UaDkrODQyY0s3L1ZmTDh6S2NBZG1QN21QMXhKZE5NVlEzZFZnaW9XWlQKd2xWWVc4N2FLZ0xLNlZIaEhJNTVGV3RNaFYrTzdNbUZlSzBWckhaS3NHSDJIUGg5ZUQyVkFVWVNWaU9HdisyUDRFczJ5V0lCNjkxNgpjMStRWjc5RXdwRmwrQjBJUG0yLy9rbDVXSXVYaDdXY1FPUzBBMm4yMnYxWnlMK29vdVNUTWNGMm9TNWx1RjRzRVc5VDkrb0tjRjU5CjBVOGVvY1E0R2Q5dkxwTnVpSG1CNUpvWFF6NG1EZGFBdG13eS9zMHFXVTgyQjlWZEVETlNPZGx0NnNoQXBTZ01ObGtGS0hyQllHMD0K"; - System.out.println(enc_msg); - - //enc_msg = "UVRreXJPVm1GRlgrYldkWFNjd1pwM0dTMWxuNkJYMThUZEs1U1dLQWU2cFdkV0JoUXBFeU1nci90L1J1YWpTSks0RXo2a250cXJGK0hoclRXQ3I5Nk8vUEw4aWFKS3J5SllpUm9jTE1NMVdEcWsyakIvMWxkWXE1WGx4Qk9lenR3aTI0alV4MVV4dTBZY0ZWaUIvdGFRd0xIaWdzdE1nT1pEYnlqcnhKdUdGdkpJVG9hNkJDbGM4RXpnRWN3bzZiQnBDR3BXSCtkQk5LZE5yN0dDYnAzRTltUGRxOEh4Y01NNFBiNXdyeFBrZlpCMkl2NXpGRnNmOStUTHEzajVQT2JTa2t6dXR2VGhVS2VKN1dYZ01Vdzhvbk9rYzE2S3Q3VTg3dEFJVlpJYTY2RDdTMGd5ZWNrN01oVE5KM2tkYXNhUmVtbEQ2cys0WkNtb2NqYWVWbVpuT09yNGtsS3Z6U2VZVE5sOWNpMXRCblFBV0M0VzE5dVN0RXp6OGxFY21idHBqZHVSbUxGODNyNm83YWZ6N1dDbGMwUDEyakMxODRxZDNNUUpRQ0l0OE1OZThzNTZsNVJ1blJnRmNGNGJEb2UyTU94QUUzb05Rd3JCMldRemcxNE9mRFp2UVdlMW9JVUNMU0cyZGc1OUNUN09KdG5lZndDaEJQUGNmc2tBVE8%3D"; - //enc_msg = "T2tpcTVMeC9uVCt6VXNSRWxaT09VVGh1YlZtMkpXQXhzcWErZkxsQ0pUWktMQVZNdTFTYm1VZnN6aVVGaTNnbUE0VEx6LzJLL3pkVmpzY0pHSDlzUmJ6MFRWTUM3QkZ4ZXV5bVJZMW43bGVNOG1wRVhheGNpTEIyVzNMV0lmakh0d0o0QTRUNWtwMnhUOVprRXFhZ1RKUEZ3RUgwSmdqem9CRHpjMzZNWkxlRS9DUzBCR0RoQzdTODFweXBMaktuUWdhK0RJNUFOQUdrQnhjeHcrQWFGeUdNRmRVMWVaMU9GWUtYVjRzeUJVZnZ6dk1UN2ZmODIvLzZBa1VRMFN3a2p5TmliRjg4VkJCODJGckRCOC9TRW1CWVJnWWtRVklhWEFPZXo1aXlSR0laam1KN3Z6bXFKTDZSVzVGWTFPYms2YWJaU1FnVnZwNXoxbStHdG1KdkRYczJxeE01Unk2N0RtNlhpOGRyRERvVW83YUdzbW5Tamp5VzNUSVE0WS9iSzVyMEo5UndwcjUvTTFYMGg1T3d4MWJoRWVVTUJVZlMzV1BZTVNwMVR4WFVsRkFjTk8yQk9wZ3lvcWJYcmRFV0c2RmFIUXNxYS82ay80SmpseVpCbDd6cUUwYU9SM3lZMXY0ZG9iVHlDb0JENkNhcFp1SWs0NFlibDRaMFdTRmFlRE1lcndiZmdUcU1nWmFNL0RjWmVWN2V1akVGNytaWjNLTExZdmU2VlV3bVlJbmM2bHg2N2FwdG5UM0hic3BWei8rTnlHek1FRWRmQlpGTVFnbDhSeTBoeTlDcGRxRng2dUhrdm5wRHJrenZkUVAzWm55bkRzZHgwdlBoUW9XeEQyQWRDVi9UdVdIOTIxeG52b0NVa3U2UCtkSFJyUm9kd1BVSjBWOURiYnc9"; - // base64逆处理并用私钥解密 - BASE64Decoder decoder = new BASE64Decoder(); - enc_msg = new String(decoder.decodeBuffer(enc_msg),"UTF-8"); - String dec_msg = RSAUtil.decrypt(enc_msg, privateKey); - - System.out.println("base64逆处理并用私钥解密:"); - System.out.println(dec_msg); - - - - } -} diff --git a/doc/建行支付对接修复报告.md b/doc/建行支付对接修复报告.md deleted file mode 100644 index ea968f9..0000000 --- a/doc/建行支付对接修复报告.md +++ /dev/null @@ -1,538 +0,0 @@ -# 建行支付对接修复报告 - -**项目**: Shopro商城建行支付集成 -**修复时间**: 2025-01-20 -**文档版本**: v2.0 (修订版) -**建行接口版本**: v2.20 (2025-07-25) - ---- - -## ⚠️ 重要修订说明 - -本报告v2.0版本修正了v1.0中关于"建行平台公钥"的**严重错误理解**: - -- ❌ **错误**: 文档中不存在"建行平台公钥"这个概念 -- ✅ **正确**: 应该是"建行生活支付验签公钥"(需联系建行生活技术支持获取) - ---- - -## 📋 修复概览 - -本次对建行支付对接代码进行了**5项严重错误修复**和**1项性能优化**,基于建行官方Java示例代码和接口文档v2.20规范。 - -### 修复文件清单 - -| 文件路径 | 修复项 | 风险等级 | -|---------|--------|---------| -| `addons/shopro/library/ccblife/CcbPaymentService.php` | MAC签名算法、SIGN验签逻辑 | 🔴 致命 | -| `addons/shopro/library/ccblife/CcbEncryption.php` | ENCPUB生成、RSA分段加密 | 🔴 致命 | -| `addons/shopro/controller/Ccbpayment.php` | 防重复支付、notify返回格式 | 🟡 严重 | - ---- - -## 🔴 致命错误修复 - -### 1. 支付串MAC签名算法错误 - -**位置**: `CcbPaymentService.php:148-153` - -#### 修复前 ❌ -```php -// 错误: 使用私钥签名 -$mac = md5($signString . $this->config['private_key']); -``` - -#### 修复后 ✅ -```php -// 正确: 使用服务方公钥参与MD5计算(建行v2.2规范) -$platformPubKey = $this->config['public_key']; // 服务方公钥 -$mac = strtoupper(md5($signString . '&PLATFORMPUB=' . $platformPubKey)); -``` - -#### 技术说明 -根据建行文档v2.2版本和官方MD5Util.java示例: -- **PLATFORMPUB字段**: 仅参与MD5摘要计算,不作为HTTP参数传递 -- **签名格式**: `MD5(参数串 + &PLATFORMPUB= + 服务方公钥内容)` -- **输出格式**: 32位**大写**MD5字符串 (对照MD5Util.java第30行: `toUpperCase()`) - -**影响**: 修复前建行会拒绝所有支付请求,因签名验证100%失败。 - ---- - -### 2. ENCPUB字段生成逻辑错误 - -**位置**: `CcbEncryption.php:387-420` - -#### 修复前 ❌ -```php -// 错误: 加密整个商户公钥 -return $this->rsaEncrypt($this->publicKey); -``` - -#### 修复后 ✅ -```php -// 正确: 只加密商户公钥后30位 -$publicKeyContent = str_replace([ - '-----BEGIN PUBLIC KEY-----', - '-----END PUBLIC KEY-----', - "\r", "\n", " " -], '', $this->publicKey); - -$last30Chars = substr($publicKeyContent, -30); -return $this->rsaEncrypt($last30Chars); -``` - -#### 技术说明 -建行文档明确要求: -> "使用服务方公钥对**商户公钥后30位**进行RSA加密并base64后的密文" - -**影响**: 修复前ENCPUB字段内容错误,可能导致建行无法验证商户公钥。 - ---- - -### 3. 异步通知SIGN验签逻辑优化 - -**位置**: `CcbPaymentService.php:467-570` - -#### 修复前 ❌ -```php -// 错误: 使用MD5验签 -$expectedSign = md5($signStr . $this->config['private_key']); -return strtolower($signature) === strtolower($expectedSign); -``` - -#### 修复后 ✅ -```php -// 智能验签方案: 如果配置了验签公钥则使用RSA,否则降级为POSID验证 -$ccbVerifyPublicKey = $this->config['ccb_payment_verify_public_key'] ?? ''; - -if (empty($ccbVerifyPublicKey)) { - // 降级方案: POSID验证 - return ($params['POSID'] ?? '') === $this->config['pos_id']; -} - -// 完整方案: RSA验签(尝试SHA256和SHA1) -$signBinary = hex2bin($params['SIGN']); -$pubKey = openssl_pkey_get_public($ccbVerifyPublicKey); - -$result = openssl_verify($signStr, $signBinary, $pubKey, OPENSSL_ALGO_SHA256); -if ($result !== 1) { - $result = openssl_verify($signStr, $signBinary, $pubKey, OPENSSL_ALGO_SHA1); -} - -return $result === 1; -``` - -#### 技术说明 -根据建行文档7.2.3章节: -- **SIGN字段**: 256个十六进制字符 (2048位RSA签名) -- **验签密钥**: "建行生活分配的服务商支付验签公钥" (NT_TYPE=YS时) -- **验签算法**: RSA-SHA256或SHA1 (文档未明确,代码会自动尝试) -- **获取方式**: 联系建行生活平台技术支持 - -#### 降级方案说明 -由于建行未提供验签公钥和示例代码,代码实现了两级验证: - -1. **优先**: 如果配置了`ccb_payment_verify_public_key`,使用RSA验签 -2. **降级**: 如果未配置,验证POSID和订单号是否匹配 - -**建议**: 尽快联系建行技术支持获取验签公钥,补全配置后获得完整安全保障。 - -**影响**: 修复后验签逻辑更健壮,未配置公钥时也能正常运行(安全性降低但不会中断业务)。 - ---- - -## 🟡 严重问题修复 - -### 4. 订单状态更新缺少防重复逻辑 - -**位置**: `Ccbpayment.php:170-197` - -#### 修复前 ❌ -```php -// 直接更新,没有并发控制 -$order->status = 'paid'; -$order->save(); -``` - -#### 修复后 ✅ -```php -// 使用原子性更新,防止并发重复支付 -$affectedRows = Db::name('shopro_order') - ->where('id', $order->id) - ->where('status', 'unpaid') // 只更新未支付的订单 - ->update([ - 'status' => 'paid', - 'paid_time' => time() * 1000, - 'updatetime' => time() - ]); - -if ($affectedRows === 0) { - // 订单已支付或状态异常 - throw new Exception('订单状态异常,无法更新为已支付'); -} -``` - -**影响**: 修复前在高并发场景下可能出现重复支付或状态覆盖。 - ---- - -### 5. notify接口返回格式不规范 - -**位置**: `Ccbpayment.php:271-283` - -#### 修复前 ❌ -```php -// ThinkPHP框架会追加额外内容 -echo $result; // 'SUCCESS' 或 'FAIL' -``` - -#### 修复后 ✅ -```php -// 直接exit,确保只返回纯文本 -exit(strtoupper($result)); // 'SUCCESS' 或 'FAIL' -``` - -#### 技术说明 -建行要求异步通知响应: -- **HTTP 200** 状态码 -- **纯文本** 响应体: `SUCCESS` 或 `FAIL` -- **不允许**任何额外字符(HTML/JSON等) - -**影响**: 修复前ThinkPHP框架可能追加调试信息,导致建行认为通知失败并重复推送。 - ---- - -## ⚡ 性能优化 - -### 6. RSA加密分段大小动态计算 - -**位置**: `CcbEncryption.php:102-129` - -#### 优化前 ⚠️ -```php -// 写死1024位RSA的chunk size -$chunkSize = 117; // 1024位RSA密钥,每次最多加密117字节 -``` - -#### 优化后 ✅ -```php -// 动态获取RSA密钥大小 -$keyDetails = openssl_pkey_get_details($pubKeyId); -$keySize = $keyDetails['bits'] / 8; // 1024位=128字节, 2048位=256字节 -$chunkSize = $keySize - 11; // PKCS1填充需要预留11字节 -``` - -**优势**: -- 自动适配1024位/2048位/4096位RSA密钥 -- 减少不必要的分段次数,提升加密性能 -- 避免密钥升级后的兼容性问题 - ---- - -## 🔐 建行接口签名规则总结 - -### 支付串生成流程 - -```mermaid -graph LR - A[34个参数] --> B[按ASCII排序ksort] - B --> C[http_build_query拼接] - C --> D[追加&PLATFORMPUB=服务方公钥] - D --> E[MD5签名,32位小写] - E --> F[ENCPUB=RSA加密商户公钥后30位] - F --> G[最终支付串=参数+MAC+PLATFORMID+ENCPUB] -``` - -### 异步通知验签流程 - -```mermaid -graph LR - A[接收SIGN字段] --> B[hex2bin转二进制] - B --> C[移除SIGN,剩余参数ksort排序] - C --> D[拼接签名原串] - D --> E[使用建行公钥RSA-SHA256验签] - E --> F{验签结果} - F -->|成功| G[返回SUCCESS] - F -->|失败| H[返回FAIL] -``` - ---- - -## ✅ 验证检查清单 - -修复完成后,请逐项检查以下配置: - -### 1. 配置文件检查 - -**文件**: `addons/shopro/config/ccblife.php` - -```php -return [ - // 建行商户信息 - 'merchant_id' => 'YOUR_MERCHANT_ID', - 'pos_id' => 'YOUR_POS_ID', - 'branch_id' => 'YOUR_BRANCH_ID', - 'service_id' => 'YOUR_SERVICE_ID', - - // ✅ 服务方公钥(用于MAC签名) - 'public_key' => '-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... ------END PUBLIC KEY-----', - - // ✅ 服务方私钥(用于解密) - 'private_key' => '-----BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC... ------END PRIVATE KEY-----', - - // ✅ 建行平台公钥(用于SIGN验签) - 新增必填! - 'platform_public_key' => '-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... ------END PUBLIC KEY-----', - - // 建行收银台URL - 'cashier_url' => 'https://ibsbjstar.ccb.com.cn/CCBIS/ccbMain', -]; -``` - -### 2. 密钥格式验证 - -运行以下PHP脚本验证密钥格式: - -```php -generatePaymentString($orderId); - -// 验证点: -// 1. MAC长度为32位 -// 2. ENCPUB字段存在且不为空 -// 3. 支付串包含所有34个必需参数 -``` - -#### TC2: 异步通知验签测试 -```php -// 模拟建行回调数据 -$params = [ - 'ORDERID' => 'test123', - 'PAYMENT' => '100.00', - 'SUCCESS' => 'Y', - 'SIGN' => '256字符十六进制字符串...' -]; - -$result = $service->handleNotify($params); -// 预期: 返回'success'或'fail' -``` - -#### TC3: 并发支付测试 -使用Apache Bench进行并发测试: -```bash -ab -n 100 -c 10 http://your-domain/api/ccbpayment/callback -``` -验证订单状态不会重复更新。 - ---- - -## ⚠️ 上线前必读 - -### 1. 建行生活支付验签公钥获取(重要!) - -**关键**: 需要向建行生活技术支持索要**"建行生活支付验签公钥"**,用于异步通知SIGN验签。 - -#### 为什么需要这个公钥? - -- 建行用自己的私钥对异步通知进行RSA签名(生成SIGN字段) -- 你需要用建行的公钥来验证SIGN,确保通知是建行发送的 -- 这个公钥**不是**你自己生成的公钥,是建行生活平台分配给你的 - -#### 如何获取? - -1. 联系建行生活平台运营人员或技术支持 -2. 说明需要获取"建行生活支付验签公钥"(NT_TYPE=YS的验签公钥) -3. 提供你的商户号和服务方编号 -4. 获取后配置到`.env`文件中 - -```ini -# .env文件 -ccb_payment_verify_public_key="-----BEGIN PUBLIC KEY----- -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... ------END PUBLIC KEY-----" -``` - -#### 未配置的影响 - -- 异步通知验签会降级为POSID验证 -- 安全性降低,无法完全确认通知来源 -- 但不会中断业务,系统仍可正常运行 - -### 2. 验证密钥格式 - -运行以下PHP脚本验证密钥配置是否正确: - -```php ->Backend: POST /createPayment(生成支付串) - Backend->>Backend: 保存订单(未支付状态) - - Note over Backend,CCBBackend: 步骤2: 推送订单 ✅ 推送未支付订单! - Backend->>Merchant: 调用订单推送接口(A3341TP01) - Merchant-->>Backend: 返回推送结果 - Backend-->>H5: {payment_string} - - Note over H5,CCBApp: 步骤3: 调起建行收银台 - H5->>CCBApp: JSBridge.ccbpay(支付串) - activate CCBBackend - CCBApp->>CCBBackend: 校验登录 - CCBApp->>CCBApp: 调用支付组件 - H5->>CCBApp: 确认支付、输入密码 - CCBApp->>CCBBackend: 发送支付请求 - Note right of CCBBackend: 用户在建行APP中完成支付 - - Note over CCBBackend,Backend: 步骤10-12: 建行异步通知 - CCBBackend->>Merchant: 返回支付成功通知 - Merchant->>Backend: 推送服务器通知(notify) - Backend->>Backend: 验证SIGN签名 - Backend->>Backend: 原子更新本地订单状态为paid - deactivate CCBBackend - - Note over Backend,Merchant: 步骤13: 更新订单状态 ✅ 更新为已支付! - Backend->>Merchant: 调用订单更新接口(A3341TP02) - Merchant-->>Backend: 返回更新结果 - Backend-->>Merchant: 返回SUCCESS - - Note over H5,Backend: 步骤15-16: 前端轮询查询状态 (未收到通知时) - loop 每2秒轮询(最多60秒) - H5->>Backend: GET /queryPaymentStatus - Backend-->>H5: {status: 'paid'或'unpaid'} - alt status == 'paid' - H5->>H5: 跳转到支付成功页 - end - end -``` - -**关键流程说明**: -1. **步骤2**: 生成支付串后**立即推送未支付订单**到建行外联系统(A3341TP01) -2. **步骤13**: 收到支付成功通知后**更新订单状态为已支付**(A3341TP02) -3. **步骤15**: 前端轮询查询订单状态(用于未收到通知的降级方案) - -### 修复前的错误实现 - -```mermaid -sequenceDiagram - participant H5 as 前端H5页面 - participant Callback as callback接口 - participant Notify as notify接口 - participant CCBApp as 建行APP(黑盒) - - Note over H5,Callback: ❌ 错误1: 前端callback通知支付成功 - H5->>Callback: POST callback(order_id, trans_id) ❌ 可伪造! - Callback->>Callback: verifyPayment()主动查询建行? - Callback->>Callback: ❌ 更新订单为已支付 - Callback->>Callback: ❌ 推送订单到外联 - Callback-->>H5: 返回success - - Note over CCBApp,Notify: ❌ 错误2: 建行异步通知被边缘化 - CCBApp-->>Notify: POST notify(ORDERID, SIGN等) - Notify->>Notify: ❌ 再次更新订单? - Notify->>Notify: ❌ 再次推送订单? - Notify-->>CCBApp: 返回SUCCESS - - Note over H5,Callback: ⚠️ 严重问题: 两条并行路径! - rect rgb(255, 200, 200) - Note right of Callback: 路径A: 前端callback触发更新
路径B: 建行notify触发更新
可能导致重复处理或竞态条件! - end -``` - ---- - -## 🔥 严重安全漏洞详解 - -### 漏洞1: 前端callback可伪造支付成功 - -**问题代码** (`frontend/sheep/platform/pay.js:325-336`): - -```javascript -// ❌ 错误: 前端主动调用callback通知后端支付成功 -if (result.code === 0) { - // 支付成功,通知后端 ❌ 严重安全漏洞! - const callbackResult = await ccbApi.paymentCallback({ - order_id: orderInfo.data.id, - trans_id: result.data?.trans_id || '', // ❌ 前端可伪造 - pay_time: new Date().getTime() // ❌ 前端可伪造 - }); - - if (callbackResult.code === 1) { - that.payResult('success'); // ❌ 跳转到成功页 - } -} -``` - -**攻击方式**: - -1. 用户在浏览器控制台执行: -```javascript -ccbApi.paymentCallback({ - order_id: 12345, - trans_id: 'fake_trans_id', - pay_time: Date.now() -}) -``` - -2. 后端callback()接口收到请求,**没有验证签名**,直接更新订单为已支付! - -3. 攻击者不花一分钱就能白嫖商品 🔥 - -**风险等级**: 🔴 **致命** - 可直接导致商户资金损失 - ---- - -### 漏洞2: callback()与notify()双通道竞态条件 - -**问题代码** (`addons/shopro/controller/Ccbpayment.php`): - -```php -// ❌ callback()中更新订单状态 -public function callback() -{ - // ...省略代码... - - // 更新订单状态 - $affectedRows = Db::name('shopro_order') - ->where('id', $order->id) - ->where('status', 'unpaid') - ->update(['status' => 'paid']); - - // 推送订单到建行 - $this->pushOrderToCcb($order); // ❌ 在callback中推送 -} - -// ❌ notify()中也更新订单状态 -public function notify() -{ - // ...省略代码... - - $this->paymentService->handleNotify($params); // ❌ 内部再次更新订单 - - // handleNotify()内部还会调用pushOrder() ❌ 重复推送! -} -``` - -**竞态条件场景**: - -1. 用户在建行APP完成支付 -2. 建行异步通知服务器 → 触发`notify()` → 更新订单为paid -3. **同时**前端H5页面返回 → 调用`callback()` → 再次尝试更新订单 -4. 如果notify还未完成,callback会成功更新 → **订单被更新两次** -5. pushOrderToCcb()被调用两次 → **外联系统收到重复订单** - -**风险等级**: 🔴 **致命** - 可能导致订单状态异常或重复扣款 - ---- - -### 漏洞3: 订单推送时机错误 - -**问题**: callback()在前端触发时就推送订单,但此时: -- 建行可能还未真正扣款 -- callback可能是攻击者伪造的请求 -- 订单状态可能还未真正更新为paid - -**正确时机**: 只在`notify()`收到建行异步通知并验签成功后推送! - ---- - -## ✅ 修复方案 - -### 修复1: callback()改造为纯查询接口 - -**修复后** (`addons/shopro/controller/Ccbpayment.php:129-164`): - -```php -/** - * 查询订单支付状态 (前端轮询用) - * - * ⚠️ 重要: 本接口只查询订单状态,不执行任何业务逻辑! - * 订单状态由建行异步通知(notify)接口更新,前端只负责轮询查询。 - */ -public function queryPaymentStatus() -{ - try { - $orderId = $this->request->get('order_id', 0); - - if (empty($orderId)) { - $this->error('订单ID不能为空'); - } - - // ✅ 只查询,不更新! - $order = OrderModel::where('id', $orderId) - ->where('user_id', $this->auth->id) - ->field('id, order_sn, status, paid_time, ccb_pay_flow_id') - ->find(); - - if (!$order) { - $this->error('订单不存在'); - } - - // ✅ 返回订单状态(只读操作,绝不修改数据!) - $this->success('查询成功', [ - 'order_id' => $order->id, - 'order_sn' => $order->order_sn, - 'status' => $order->status, - 'is_paid' => in_array($order->status, ['paid', 'completed', 'success']), - 'paid_time' => $order->paid_time, - 'pay_flow_id' => $order->ccb_pay_flow_id, - ]); - - } catch (Exception $e) { - Log::error('[建行支付] 查询订单状态失败 error:' . $e->getMessage()); - $this->error('查询失败: ' . $e->getMessage()); - } -} - -/** - * ⚠️ 已废弃: 支付回调 (前端调用) - * @deprecated 2025-01-20 该方法存在严重安全漏洞,已被queryPaymentStatus()替代 - */ -public function callback() -{ - // 向后兼容:直接调用查询接口 - Log::warning('[建行支付] callback()已废弃,请前端改用queryPaymentStatus()接口'); - - $_GET['order_id'] = $this->request->post('order_id', 0); - return $this->queryPaymentStatus(); -} -``` - -**修复要点**: -- ✅ 只查询,不更新任何数据 -- ✅ 只返回订单状态,前端根据状态判断是否跳转 -- ✅ 支持用户权限验证(`where('user_id', $this->auth->id)`) -- ✅ 向后兼容旧版callback接口 - ---- - -### 修复2: notify()成为唯一的支付确认通道 - -**修复后** (`addons/shopro/controller/Ccbpayment.php:200-258`): - -```php -/** - * 建行支付通知 (建行服务器回调) - * - * ✅ 正确流程: - * 1. 验证签名 - * 2. 更新订单状态(由handleNotify()完成) - * 3. 推送订单到建行外联系统(本方法完成) - * 4. 返回SUCCESS给建行 - */ -public function notify() -{ - try { - // 1-5. 解析和验证参数 - $rawData = file_get_contents('php://input'); - Log::info('[建行通知] 收到异步通知: ' . $rawData); - - $params = $this->request->post(); - if (empty($params) && $rawData) { - parse_str($rawData, $params); - } - - if (empty($params['ORDERID'])) { - Log::error('[建行通知] 缺少ORDERID参数'); - exit('FAIL'); - } - - // 6. 调用服务层处理通知(返回订单ID) - $result = $this->paymentService->handleNotify($params); - - // 7. ✅ 处理成功后推送订单到建行外联系统 - if ($result['status'] === 'success' && !empty($result['order_id'])) { - // ⚠️ 只有新支付才推送,已支付的订单跳过推送 - if ($result['already_paid'] === false) { - try { - $this->pushOrderToCcb($result['order_id']); - Log::info('[建行通知] 订单推送成功 order_id:' . $result['order_id']); - } catch (Exception $e) { - // ⚠️ 推送失败不影响支付成功,记录日志后续补推 - Log::error('[建行通知] 订单推送失败 order_id:' . $result['order_id'] . ' error:' . $e->getMessage()); - } - } else { - Log::info('[建行通知] 订单已支付且已推送,跳过推送'); - } - } - - // 8. 返回处理结果 - $response = ($result['status'] === 'success') ? 'SUCCESS' : 'FAIL'; - Log::info('[建行通知] 处理完成,返回: ' . $response); - - exit($response); // 直接退出,确保只输出SUCCESS/FAIL - - } catch (Exception $e) { - Log::error('[建行通知] 处理失败 error:' . $e->getMessage()); - exit('FAIL'); - } -} -``` - -**修复要点**: -- ✅ 验证SIGN签名(由handleNotify()完成) -- ✅ 原子更新订单状态(由handleNotify()完成) -- ✅ 推送订单到外联系统(本方法完成,在订单状态更新成功后) -- ✅ 幂等性保护(已支付的订单跳过推送) -- ✅ 推送失败不影响支付成功(记录日志后续补推) - ---- - -### 修复3: createPayment()推送未支付订单 - -**修复后** (`addons/shopro/controller/Ccbpayment.php:101-118`): - -```php -// 5. ✅ 推送订单到建行(步骤2:调用订单推送接口将已提交订单) -// ⚠️ 注意:此时推送的是未支付状态的订单 -try { - $pushResult = $this->orderService->pushOrder($orderId); - - if ($pushResult['status']) { - Log::info('[建行支付] 订单推送成功 order_id:' . $orderId); - } else { - // ⚠️ 推送失败不阻塞支付流程,只记录日志 - Log::warning('[建行支付] 订单推送失败(不阻塞支付) order_id:' . $orderId . ' error:' . $pushResult['message']); - } -} catch (Exception $e) { - // ⚠️ 推送异常不阻塞支付流程 - Log::error('[建行支付] 订单推送异常(不阻塞支付) order_id:' . $orderId . ' error:' . $e->getMessage()); -} - -// 6. 返回支付串 -$this->success('支付串生成成功', $result['data']); -``` - -**修复要点**: -- ✅ 生成支付串后立即调用`pushOrder()`推送未支付订单(A3341TP01) -- ✅ 推送失败不阻塞支付流程,用户仍可继续支付 -- ✅ 记录推送结果日志,失败的可后续补推 - ---- - -### 修复4: notify()更新订单状态为已支付 - -**修复后** (`addons/shopro/controller/Ccbpayment.php:227-248`): - -```php -// 7. ✅ 处理成功后更新订单状态到建行(步骤13:调用订单更新接口更新订单状态) -if ($result['status'] === 'success' && !empty($result['order_id'])) { - // ⚠️ 只有新支付才更新,已支付的订单跳过更新 - if ($result['already_paid'] === false) { - try { - // 调用订单更新接口,将订单状态从未支付更新为已支付 - $updateResult = $this->orderService->updateOrderStatus($result['order_id']); - - if ($updateResult['status']) { - Log::info('[建行通知] 订单状态更新成功 order_id:' . $result['order_id']); - } else { - // ⚠️ 更新失败不影响本地支付状态,记录日志后续补推 - Log::warning('[建行通知] 订单状态更新失败(本地已支付) order_id:' . $result['order_id'] . ' error:' . $updateResult['message']); - } - } catch (Exception $e) { - // ⚠️ 更新异常不影响支付成功,记录日志后续补推 - Log::error('[建行通知] 订单状态更新异常(本地已支付) order_id:' . $result['order_id'] . ' error:' . $e->getMessage()); - } - } else { - Log::info('[建行通知] 订单已支付且已更新,跳过更新 order_id:' . $result['order_id']); - } -} -``` - -**修复要点**: -- ✅ 收到支付成功通知后调用`updateOrderStatus()`(A3341TP02) -- ✅ 将订单状态从"未支付"更新为"已支付" -- ✅ 更新失败不影响本地支付状态(本地订单已标记为paid) -- ✅ 幂等性保护(已支付的订单跳过更新) - ---- - -### 修复5: handleNotify()返回订单ID - -**修复后** (`addons/shopro/library/ccblife/CcbPaymentService.php:349-403`): - -```php -/** - * 处理异步通知 - * - * ⚠️ 注意:这是唯一可信的支付确认来源! - * 返回订单ID供控制器调用pushOrderToCcb()推送到外联系统 - * - * @param array $params 通知参数 - * @return array ['status' => 'success'|'fail', 'order_id' => int, 'order_sn' => string] - */ -public function handleNotify($params) -{ - try { - // 1. 验证签名 - if (!$this->verifyNotifySignature($params)) { - throw new \Exception('签名验证失败'); - } - - // 2. 查询订单 - $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(); - } - - if (!$order) { - throw new \Exception('订单不存在'); - } - - // 3. ✅ 幂等性检查: 如果订单已支付,直接返回成功 - if ($order['status'] == 'paid') { - Log::info('[建行通知] 订单已支付,跳过处理 order_id:' . $order->id); - return [ - 'status' => 'success', - 'order_id' => $order->id, - 'order_sn' => $order->order_sn, - 'already_paid' => true, // ✅ 标记为已支付 - ]; - } - - // 4. 更新订单状态 - $this->updateOrderPaymentStatus($order, $params); - - Log::info('[建行通知] 订单状态更新成功 order_id:' . $order->id); - - // 5. ✅ 返回订单ID供控制器推送到外联系统 - return [ - 'status' => 'success', - 'order_id' => $order->id, - 'order_sn' => $order->order_sn, - 'already_paid' => false, // ✅ 新支付 - ]; - - } catch (\Exception $e) { - Log::error('[建行通知] 处理失败: ' . $e->getMessage()); - return [ - 'status' => 'fail', - 'message' => $e->getMessage(), - ]; - } -} -``` - -**修复要点**: -- ✅ 返回订单ID供控制器推送 -- ✅ 返回already_paid标志防止重复推送 -- ✅ 幂等性保护(已支付的订单直接返回成功) - ---- - -### ~~修复6: pushOrderToCcb()增加幂等性检查~~ - -**⚠️ 已废弃**: 根据建行流程图,`pushOrderToCcb()`方法已废弃。 - -**正确流程**: -1. `createPayment()` → 调用`orderService->pushOrder()` → 推送未支付订单(A3341TP01) -2. `notify()` → 调用`orderService->updateOrderStatus()` → 更新为已支付(A3341TP02) - ---- - -**⚠️ 已废弃**: 根据建行流程图,订单推送和更新的幂等性由`CcbOrderService`内部保证。 - ---- - -### 修复6: 前端改为轮询查询 - -**修复后** (`frontend/sheep/platform/pay.js:325-386`): - -```javascript -// 建行生活支付 -async ccbPay() { - // ...省略订单信息获取和支付串生成... - - // 调起建行支付 - const result = await CcbLifePlatform.payment({ - payment_string: paymentResult.data.payment_string, - order_id: orderId, - order_sn: this.orderSN - }); - - if (result.code === 0) { - // ✅ 支付调起成功,开始轮询查询订单状态 - console.log('[建行支付] 支付调起成功,开始轮询查询订单状态'); - - uni.showLoading({ - title: '支付确认中...', - mask: true - }); - - // ✅ 轮询查询订单状态(最多30次,每次间隔2秒,总共60秒) - let pollCount = 0; - const MAX_POLL_COUNT = 30; - const POLL_INTERVAL = 2000; - - const pollPaymentStatus = async () => { - pollCount++; - - try { - const statusResult = await ccbApi.queryPaymentStatus(orderId); - - if (statusResult.code === 1 && statusResult.data.is_paid) { - // ✅ 支付成功 - uni.hideLoading(); - console.log('[建行支付] 订单已支付'); - that.payResult('success'); - return; - } - - // 未支付,继续轮询 - if (pollCount < MAX_POLL_COUNT) { - setTimeout(pollPaymentStatus, POLL_INTERVAL); - } else { - // 超时 - uni.hideLoading(); - uni.showModal({ - title: '提示', - content: '支付确认超时,请稍后在订单列表中查看支付状态', - showCancel: false, - confirmText: '知道了', - success: () => { - sheep.$router.redirect('/pages/order/list'); - } - }); - } - } catch (error) { - console.error('[建行支付] 查询状态失败:', error); - - // 继续轮询(网络错误不中断) - if (pollCount < MAX_POLL_COUNT) { - setTimeout(pollPaymentStatus, POLL_INTERVAL); - } else { - uni.hideLoading(); - sheep.$helper.toast('支付状态查询失败,请稍后在订单列表中查看'); - that.payResult('fail'); - } - } - }; - - // 延迟1秒后开始轮询(给建行异步通知留点时间) - setTimeout(pollPaymentStatus, 1000); - } -} -``` - -**修复要点**: -- ✅ 轮询查询订单状态(每2秒一次) -- ✅ 最多轮询30次(总共60秒) -- ✅ 网络错误不中断轮询 -- ✅ 超时友好提示用户去订单列表查看 - ---- - -## 📊 修复前后对比 - -| 对比项 | 修复前(错误) | 修复后(正确) | -|-------|------------|------------| -| **支付确认来源** | 前端callback + 建行notify (双通道) ❌ | 只依赖建行notify (单通道) ✅ | -| **前端职责** | 调用callback通知后端支付成功 ❌ | 轮询查询订单状态 ✅ | -| **安全性** | 可伪造前端请求触发支付成功 🔴 | 只信任建行签名验证 ✅ | -| **订单推送时机** | 支付成功后推送 ❌ | **创建订单时推送未支付状态** ✅ | -| **订单更新时机** | 未更新到建行 ❌ | **支付成功后更新为已支付** ✅ | -| **竞态风险** | callback和notify可能同时执行 🔴 | 只有notify会更新订单 ✅ | -| **幂等性** | 无幂等保护 ❌ | 支持重复调用,已推送/已更新的跳过 ✅ | -| **符合建行规范** | 否 ❌ | **完全符合流程图** ✅ | -| **订单状态一致性** | 可能重复更新或状态异常 🔴 | 原子更新,状态一致 ✅ | -| **建行订单同步** | 不同步或错误时机同步 ❌ | **按流程图正确同步** ✅ | - ---- - -## ✅ 验证清单 - -修复完成后,请逐项验证: - -### 1. 后端验证 - -```bash -# 验证queryPaymentStatus接口 -curl -X GET "http://your-domain/addons/shopro/ccbpayment/queryPaymentStatus?order_id=123" \ - -H "Authorization: Bearer YOUR_TOKEN" - -# 预期返回: -{ - "code": 1, - "msg": "查询成功", - "data": { - "order_id": 123, - "order_sn": "202501200001", - "status": "unpaid", - "is_paid": false, - "paid_time": null, - "pay_flow_id": "" - } -} -``` - -### 2. 安全测试 - -**测试1: 验证callback()已不能触发支付成功** - -```bash -# 尝试伪造callback请求 -curl -X POST "http://your-domain/addons/shopro/ccbpayment/callback" \ - -d "order_id=123&trans_id=fake_trans&pay_time=123456789" \ - -H "Authorization: Bearer YOUR_TOKEN" - -# ✅ 预期: 只返回订单状态,不会更新订单为已支付 -``` - -**测试2: 验证notify()是否幂等** - -```bash -# 模拟建行重复发送通知 -curl -X POST "http://your-domain/addons/shopro/ccbpayment/notify" \ - -d "ORDERID=PAY20250120001&SUCCESS=Y&SIGN=..." - -# ✅ 预期: -# - 第1次调用: 更新订单+推送外联,返回SUCCESS -# - 第2次调用: 跳过处理,直接返回SUCCESS -# - 日志中应有"订单已支付,跳过处理" -``` - -### 3. 前端验证 - -1. 在建行APP中发起支付 -2. 观察浏览器控制台: - - 应该看到"支付调起成功,开始轮询查询订单状态" - - 每2秒调用一次`queryPaymentStatus`接口 - - 收到`is_paid=true`后跳转到成功页 - -3. 网络中断测试: - - 支付完成后断开网络 - - 前端应继续轮询(虽然失败) - - 60秒后提示超时,引导用户去订单列表查看 - -### 4. 日志验证 - -```bash -# 查看创建支付串日志 -tail -f runtime/log/$(date +%Y%m)/*.log | grep '建行支付' - -# ✅ 正常流程应该看到: -# [建行支付] 订单推送成功 order_id:123 ← 步骤2: 推送未支付订单 - -# 查看notify日志 -tail -f runtime/log/$(date +%Y%m)/*.log | grep '建行通知' - -# ✅ 正常流程应该看到: -# [建行通知] 收到异步通知: ORDERID=... -# [建行通知] 解析参数: {...} -# [建行通知] 订单状态更新成功 order_id:123 ← 步骤13: 更新为已支付 -# [建行通知] 处理完成,返回: SUCCESS - -# 查看幂等性日志(重复通知时) -# [建行通知] 订单已支付,跳过处理 order_id:123 -# [建行通知] 订单已支付且已更新,跳过更新 order_id:123 -``` - ---- - -## 📝 数据库字段说明 - -确保订单表包含以下字段: - -```sql -ALTER TABLE `fa_shopro_order` -ADD COLUMN `ccb_pay_flow_id` VARCHAR(64) DEFAULT '' COMMENT '建行支付流水号', -ADD COLUMN `ccb_sync_status` TINYINT(1) DEFAULT 0 COMMENT '建行同步状态:0-未同步 1-已同步 2-失败', -ADD COLUMN `ccb_sync_time` INT(10) DEFAULT 0 COMMENT '建行同步时间', -ADD COLUMN `ccb_sync_error` VARCHAR(255) DEFAULT '' COMMENT '建行同步失败原因'; -``` - -**字段说明**: -- `ccb_sync_status`: 0=未同步 / 1=已同步 / 2=失败 -- `ccb_sync_error`: 推送失败时记录错误原因,供后续补推 - ---- - -## 🚀 部署步骤 - -### 1. 备份现有代码 - -```bash -# 备份控制器 -cp addons/shopro/controller/Ccbpayment.php addons/shopro/controller/Ccbpayment.php.bak - -# 备份服务类 -cp addons/shopro/library/ccblife/CcbPaymentService.php addons/shopro/library/ccblife/CcbPaymentService.php.bak - -# 备份前端代码 -cp frontend/sheep/platform/pay.js frontend/sheep/platform/pay.js.bak -cp frontend/sheep/platform/provider/ccblife/api.js frontend/sheep/platform/provider/ccblife/api.js.bak -``` - -### 2. 部署后端代码 - -```bash -# 上传修复后的文件 -# - addons/shopro/controller/Ccbpayment.php -# - addons/shopro/library/ccblife/CcbPaymentService.php - -# 清除缓存 -php think clear -``` - -### 3. 部署前端代码 - -```bash -cd frontend - -# 上传修复后的文件 -# - sheep/platform/pay.js -# - sheep/platform/provider/ccblife/api.js - -# 重新打包发布 -``` - -### 4. 数据库迁移(如果字段缺失) - -```sql --- 检查字段是否存在 -SHOW COLUMNS FROM `fa_shopro_order` LIKE 'ccb_sync_error'; - --- 如果不存在,添加字段 -ALTER TABLE `fa_shopro_order` -ADD COLUMN `ccb_sync_error` VARCHAR(255) DEFAULT '' COMMENT '建行同步失败原因'; -``` - -### 5. 监控上线 - -```bash -# 实时监控notify日志 -tail -f runtime/log/$(date +%Y%m)/*.log | grep '建行' - -# 监控查询接口调用 -tail -f runtime/log/$(date +%Y%m)/*.log | grep 'queryPaymentStatus' -``` - ---- - -## ⚠️ 回滚方案 - -如遇紧急问题,可立即回滚: - -```bash -# 回滚后端 -mv addons/shopro/controller/Ccbpayment.php.bak addons/shopro/controller/Ccbpayment.php -mv addons/shopro/library/ccblife/CcbPaymentService.php.bak addons/shopro/library/ccblife/CcbPaymentService.php - -# 清除缓存 -php think clear - -# 回滚前端(重新发布旧版本代码) -``` - ---- - -## 📞 技术支持 - -**开发者**: Billy -**修复日期**: 2025-01-20 -**建行文档版本**: v2.20 (2025-07-25) - -如有疑问,请查阅: -- 建行接入文档: `/doc/建行相关App服务方接入文档v2.20_20250725.html` -- 本修复报告: `/doc/建行支付架构修复报告.md` -- 加密修复报告: `/doc/建行支付对接修复报告.md` - ---- - -## 📝 变更历史 - -| 版本 | 日期 | 修改内容 | -|-----|------|---------| -| v1.0 | 2025-01-20 | 初始版本,完成严重安全漏洞和架构偏离修复 | -| v2.0 | 2025-01-20 | **重大修正**: 根据建行流程图修正订单同步时机 - 推送未支付订单在createPayment,更新已支付订单在notify | - ---- - -**修复完成,已做好生产环境部署准备!** ✅ diff --git a/doc/建行生活API接口文档.md b/doc/建行生活API接口文档.md new file mode 100644 index 0000000..ed3a366 --- /dev/null +++ b/doc/建行生活API接口文档.md @@ -0,0 +1,1558 @@ +# 建行生活API接口文档 + +> 文档版本: v2.0.0 +> 更新时间: 2025-10-21 +> 来源: +> - 建行生活输入通讯报文v1.1.6【最新】.xlsx +> - 建行生活原生与h5交互规范接口1.3(新).html +> - 建行相关App服务方接入文档v2.20_20250725.html + +--- + +## 📑 目录 + +- [1. 修订记录](#1-修订记录) +- [2. 接口规范](#2-接口规范) + - [2.1 公共报文头](#21-公共报文头) + - [2.2 通讯地址](#22-通讯地址) + - [2.3 接口索引](#23-接口索引) +- [3. 核心接口](#3-核心接口) + - [3.1 A3341TP01 - 服务方订单推送](#31-a3341tp01---服务方订单推送) + - [3.2 A3341TP02 - 服务方订单更新](#32-a3341tp02---服务方订单更新) + - [3.3 A3341TP03 - 服务方订单查询](#33-a3341tp03---服务方订单查询) + - [3.4 A3341TP04 - 服务方订单退款](#34-a3341tp04---服务方订单退款) + - [3.5 A3341TP05 - 订单支付权益查询](#35-a3341tp05---订单支付权益查询) + - [3.6 A3341TP13 - 服务方订单补充信息](#36-a3341tp13---服务方订单补充信息) +- [4. H5 与原生交互接口(JS Bridge)](#4-h5-与原生交互接口js-bridge) + - [4.1 交互规范说明](#41-交互规范说明) + - [4.2 callCamera - 调用相机/相册](#42-callcamera---调用相机相册) + - [4.3 getPosition - 获取定位](#43-getposition---获取定位) +- [5. 服务方接入规范](#5-服务方接入规范) + - [5.1 跳转 URL 规范](#51-跳转-url-规范) + - [5.2 建行相关 App 环境识别](#52-建行相关-app-环境识别) + - [5.3 收银台调用方法](#53-收银台调用方法) + - [5.4 商户下单支付参数定义(完整)](#54-商户下单支付参数定义完整) + - [5.5 支付成功页面回调设置](#55-支付成功页面回调设置) + - [5.6 其他说明](#56-其他说明) +- [6. 报文规范与加密](#6-报文规范与加密) + - [6.1 报文结构](#61-报文结构) + - [6.2 报文加密及签名](#62-报文加密及签名) +- [7. 回调通知接口](#7-回调通知接口) + - [7.1 建行生活支付通知接口](#71-建行生活支付通知接口) + - [7.2 建行生活退款操作通知接口](#72-建行生活退款操作通知接口) + +--- + +## 1. 修订记录 + +| 修订日期 | 修订内容 | 上线状态 | +|---------|---------|----------| +| 2021-11-22 | 接口增加字段PLAT_MCT_ID、增加退款和订单查询接口 | 已上线 | +| 2022-11-11 | 增加订单子系统新接口 | 已上线 | +| 2023-03-28 | 订单推送新增GOODS_NM/PLATFORM_POINT两个字段 | 已上线 | +| 2024-01-24 | 修改报文顺序、内容 | 已上线 | +| 2024-02-23 | 订单更新新增PLATFORM_POINT、PAY_MODE、GOODS_NM字段 | 已上线 | +| 2024-02-26 | 增加权益信息查询接口 | 已上线 | +| 2025-06-09 | 增加SKU_LIST字段 | 待上线 | +| 2025-10-21 | **整合多个文档**:新增 H5 与原生交互接口(JS Bridge)、服务方接入规范、报文加密与签名、回调通知接口等章节 | 文档整合 | + +--- + +## 2. 接口规范 + +### 2.1 公共报文头 + +#### 请求体(CLD_HEADER) + +| 栏位项目名称 | 中文名称 | 栏位属性 | 必须 | 数据项说明 | +|------------|---------|---------|------|------------| +| CLD_TX_CHNL | 通讯渠道号 | C | Y | 跟svcid相同,YS44开头的字符串 | +| CLD_TX_TIME | 通讯时间 | C | Y | yyyyMMddHHmmss(请求通讯时间) | +| CLD_TX_CODE | 服务ID | C | Y | 如A3341TP01 | +| CLD_TX_SEQ | 全局事件流水号 | C | Y | 唯一流水号 | + +#### 响应体(CLD_HEADER) + +| 栏位项目名称 | 中文名称 | 栏位属性 | 必须 | 数据项说明 | +|------------|---------|---------|------|------------| +| CLD_TX_CHNL | 通讯渠道号 | C | Y | 跟svcid相同,YS44开头的字符串 | +| CLD_TX_TIME | 通讯时间 | C | Y | yyyyMMddHHmmss(响应通讯时间) | +| CLD_TX_CODE | 服务ID | C | Y | 如A3341TP01 | +| CLD_TX_SEQ | 全局事件流水号 | C | Y | 唯一流水号 | +| CLD_CODE | 响应码 | C | Y | 详见响应字典 | +| CLD_DESC | 响应描述 | C | N | 响应描述信息 | + +### 2.2 通讯地址 + +#### UAT 测试环境 +``` +http://128.192.179.60/uat_new/tp_service/txCtrl/server?txcode=XX +``` + +#### 生产环境 +``` +https://yunbusiness.ccb.com/tp_service/txCtrl/server?txcode=XX +``` + +> **注意**: XX 为服务ID,每个接口不同 + +#### 接口地址示例 + +| 接口 | 服务ID | 完整URL | +|------|--------|---------| +| 订单推送 | A3341TP01 | https://yunbusiness.ccb.com/tp_service/txCtrl/server?txcode=A3341TP01 | +| 订单更新 | A3341TP02 | https://yunbusiness.ccb.com/tp_service/txCtrl/server?txcode=A3341TP02 | +| 订单查询 | A3341TP03 | https://yunbusiness.ccb.com/tp_service/txCtrl/server?txcode=A3341TP03 | +| 订单退款 | A3341TP04 | https://yunbusiness.ccb.com/tp_service/txCtrl/server?txcode=A3341TP04 | +| 权益查询 | A3341TP05 | https://yunbusiness.ccb.com/tp_service/txCtrl/server?txcode=A3341TP05 | +| 订单补充信息 | A3341TP13 | https://yunbusiness.ccb.com/tp_service/txCtrl/server?txcode=A3341TP13 | + +> **提示**: 当调用接口出现404时,请求头中加上: +> - `Accept: application/json` +> - `Content-Type: application/json` + +### 2.3 接口索引 + +| 服务码 | 服务名称 | 服务类型 | 版本 | 备注 | +|--------|---------|---------|------|------| +| A3341TP01 | 服务方订单推送 | 直接调用 | 01 | 新接口 | +| A3341TP02 | 服务方订单更新 | 直接调用 | 01 | 新接口 | +| A3341TP03 | 服务方订单查询 | 直接调用 | 01 | 新接口 | +| A3341TP04 | 服务方订单退款 | 直接调用 | 01 | 新接口 | +| A3341TP05 | 订单支付权益查询 | 直接调用 | 01 | 新接口 | +| A3341TP13 | 服务方订单补充信息 | 直接调用 | 01 | 新接口 | + +--- + +## 3. 核心接口 + +### 3.1 A3341TP01 - 服务方订单推送 + +#### 接口说明 + +| 项目 | 说明 | +|------|------| +| **服务码** | A3341TP01 | +| **服务名称** | 服务方订单推送 | +| **服务类型** | 直接调用 | +| **功能描述** | 用户在服务方下单成功后推送,支付时会校验订单信息 | +| **URL** | https://yunbusiness.ccb.com/tp_service/txCtrl/server?txcode=A3341TP01 | + +#### 请求参数(CLD_BODY) + +| 字段名 | 中文名称 | 数据类型 | 必须 | 说明 | +|--------|---------|----------|------|------| +| USER_ID | 客户编号 | varChar(30) | Y | 建行生活的会员编号 | +| ORDER_ID | 订单编号 | varChar(30) | Y | 用户订单号,建行生活订单列表展示的订单订单号(对应收银台USER_ORDERID字段,支付时会校验订单信息) | +| ORDER_DT | 订单日期 | Char(14) | Y | yyyyMMddHHmmss | +| TOTAL_AMT | 订单原金额 | number(15,2) | Y | 第三方原始金额 | +| PAY_AMT | 订单实际支付金额 | number(15,2) | N | 支付网关支付金额。此处如果为空必须在状态变更时推送 | +| DISCOUNT_AMT | 第三方平台优惠金额 | number(15,2) | N | 第三方平台优惠金额。此处如果为空必须在状态变更时推送 | +| DISCOUNT_AMT_DESC | 第三方平台优惠定义 | varChar(1000) | N | 各金额之和等于第三方平台优惠金额,格式:名称=金额\|@\|名称=金额 | +| ORDER_STATUS | 订单状态 | Char(1) | Y | 0-待支付 1-支付成功 2-已过期 3-支付失败 4-取消 | +| REFUND_STATUS | 退款状态 | Char(1) | Y | 0-无退款 1-退款申请 2-已退款 3-部分退款 | +| INV_DT | 订单过期日期 | Char(14) | N | yyyyMMddHHmmss | +| MCT_NM | 商户名称 | varChar(218) | Y | 商户名称 | +| CUS_ORDER_URL | 自定义订单链接 | varChar(256) | N | 订单详情地址(需要推送完整的订单详情URL) | +| OCC_MCT_LOGO_URL | 服务方商户logo图片地址 | varChar(512) | N | 必须以http://或https://开头 | +| PAY_FLOW_ID | 支付流水号 | varChar(30) | Y | 支付流水号,调用收银台时上送的支付流水号(对应收银台ORDERID字段) | +| PAY_USER_ID | 支付客户编号 | varChar(30) | N | 支付客户编号 | +| TOTAL_REFUND_AMT | 累计退款金额 | number(15,2) | N | 若支持多次退款,此次推送的金额为多次退款累计已退金额 | +| PREFTL_MRCH_ID | 门店商户号 | varChar(50) | N | 999的门店商户号,商户终端对应的建行生活门店编号 | +| PAY_MRCH_ID | 支付商户号 | Char(15) | Y | 调用收银台时上送的商户号 | +| PLAT_MCT_ID | 服务商门店编号 | varChar(21) | N | 外部平台商户号,不为空以这个字段为准 | +| OCCCOUP_DISCOUNT_AMT | 建行专属优惠金额 | number(15,2) | N | 第三方平台的建行专属优惠金额 | +| OCCCOUP_DISCOUNT_AMT_DESC | 建行专属优惠定义 | varChar(1000) | N | 格式:券实例号=金额\|@\|券实例号=金额 | +| SPECIAL_STATUS | 特殊附加状态 | Char(5) | N | P0000-正常状态 P0001-待使用 P0002-待审核 P0003-已支付 P0004-待退款 P0005-退款中 P0006-待发货 P0007-已发货 P0008-进行中 P0009-预定中 P0010-预定成功 P0011-预定失败 P0012-已入住 P0013-已离店 P0014-未入住 P0015-配送中 P0016-出券中 | +| PLAT_ORDER_TYPE | 服务方订单类型 | Char(5) | N | T0000-普通类型 T0001-洗车 T0002-加油 T0003-停车 T0004-修车 T0005-充电 T0006-年检代办 T0007-道路救援 T0008-云南中石油充值 | +| GOODS_NM | 商品名称 | varChar(200) | N | 用户购买商品名称 | +| PLATFORM_POINT | 积分值 | number(15,2) | N | 订单使用积分抵扣积分值 | +| PAY_MODE | 支付方式 | varChar(8) | N | STSL-刷脸 STSK-刷卡 | +| PLATFORM | 下单场景 | Char(2) | N | 99-建行生活APP 98-微应用 | +| **SKU_LIST** | **商品信息** | **varchar(3000)** | **N** | **商品信息列表(JSON字符串),商品售价SKU_SELL_PRICE不能大于商品定价SKU_REF_PRICE** | + +#### SKU_LIST 字段说明(JSON数组) + +```json +[ + { + "SKU_NAME": "商品名称", + "SKU_REF_PRICE": 15.01, + "SKU_NUM": 1, + "SKU_SELL_PRICE": 10.01 + }, + { + "SKU_NAME": "商品名称2", + "SKU_REF_PRICE": 15.01, + "SKU_NUM": 2, + "SKU_SELL_PRICE": 10.01 + } +] +``` + +| 字段名 | 中文名称 | 数据类型 | 必须 | 说明 | +|--------|---------|----------|------|------| +| SKU_NAME | 商品名称 | varChar | Y | 商品名称 | +| SKU_REF_PRICE | 商品参考价 | number(15,2) | Y | 商品参考价,支持两位小数 | +| SKU_NUM | 商品数量 | number(11,2) | Y | 商品数量 | +| SKU_SELL_PRICE | 商品单价 | number(15,2) | Y | 商品单价,支持两位小数 | + +#### 响应参数 + +| 字段名 | 中文名称 | 数据类型 | 必须 | 说明 | +|--------|---------|----------|------|------| +| CCB_DISCOUNT_AMT | 建行支付侧优惠金额 | number(15,2) | N | 因业务调整,后续下线字段 | +| CCB_DISCOUNT_AMT_DESC | 建行支付侧优惠定义 | varChar(1000) | N | 因业务调整,后续下线字段 | + +--- + +### 3.2 A3341TP02 - 服务方订单更新 + +#### 接口说明 + +| 项目 | 说明 | +|------|------| +| **服务码** | A3341TP02 | +| **服务名称** | 服务方订单更新 | +| **服务类型** | 直接调用 | +| **功能描述** | 在用户订单状态有变动时调用,比如电影出票成功、外卖配送中等 | +| **URL** | https://yunbusiness.ccb.com/tp_service/txCtrl/server?txcode=A3341TP02 | +| **重要说明** | 通知类型为"支付状态修改"时,支付状态不能为空,退款状态为空;通知类型为"退款状态修改"时,退款状态不能为空,支付状态为空 | + +#### 请求参数 + +| 字段名 | 中文名称 | 数据类型 | 必须 | 说明 | +|--------|---------|----------|------|------| +| ORDER_ID | 订单编号 | varChar(30) | Y | 用户订单号(对应收银台USER_ORDERID字段) | +| INFORM_ID | 通知类型 | Char(1) | Y | 0-支付状态修改 1-退款状态修改 | +| PAY_STATUS | 支付状态 | Char(1) | N | 0-待支付 1-支付成功 2-已过期 3-支付失败 4-取消 | +| REFUND_STATUS | 退款状态 | Char(1) | N | 0-无退款 1-退款申请 2-已退款 3-部分退款 | +| PAY_AMT | 订单实际支付金额 | number(15,2) | N | 订单实际支付金额 | +| DISCOUNT_AMT | 第三方平台优惠金额 | number(15,2) | N | 第三方平台优惠金额 | +| DISCOUNT_AMT_DESC | 第三方平台优惠定义 | varChar(1000) | N | 格式:名称=金额\|@\|名称=金额 | +| CUS_ORDER_URL | 自定义订单链接 | varChar(256) | N | 订单详情地址(需要推送完整的订单详情URL) | +| OCC_MCT_LOGO_URL | 服务方商户logo图片地址 | varChar(512) | N | http:// 或 https:// 为起始 | +| PAY_FLOW_ID | 支付流水号 | varChar(30) | Y | 调用收银台时上送的支付流水号(对应收银台ORDERID字段) | +| PAY_USER_ID | 支付客户编号 | varChar(150) | N | 支付客户编号 | +| TOTAL_REFUND_AMT | 累计退款金额 | number(15,2) | N | 若支持多次退款,此次推送的金额为多次退款累计已退金额 | +| PREFTL_MRCH_ID | 门店商户号 | varChar(50) | N | 999的门店商户号 | +| PAY_MRCH_ID | 支付商户号 | Char(15) | Y | 调用收银台时上送的商户号 | +| PLAT_MCT_ID | 服务商门店编号 | varChar(21) | N | 外部平台商户号,不为空以这个字段为准 | +| OCCCOUP_DISCOUNT_AMT | 建行专属优惠金额 | number(15,2) | N | 第三方平台的建行专属优惠金额 | +| OCCCOUP_DISCOUNT_AMT_DESC | 建行专属优惠定义 | varChar(1000) | N | 格式:券实例号=金额\|@\|券实例号=金额 | +| SPECIAL_STATUS | 特殊附加状态 | Char(5) | N | 同订单推送 | +| PLAT_ORDER_TYPE | 服务方订单类型 | Char(5) | N | 同订单推送 | +| GOODS_NM | 商品名称 | varChar(200) | N | 用户购买商品名称 | +| PLATFORM_POINT | 积分值 | number(15,2) | N | 订单使用积分抵扣积分值 | +| PAY_MODE | 支付方式 | varChar(8) | N | STSL-刷脸 STSK-刷卡 | +| PLATFORM | 下单场景 | Char(2) | N | 99-建行生活APP 98-微应用 | +| SKU_LIST | 商品信息 | varchar(3000) | N | 商品信息列表(JSON字符串),格式同订单推送 | + +#### 响应参数 + +| 字段名 | 中文名称 | 数据类型 | 必须 | 说明 | +|--------|---------|----------|------|------| +| IS_SUCCESS | 是否更新成功 | Char(1) | Y | 0-否 1-是 | +| CCB_DISCOUNT_AMT | 建行支付侧优惠金额 | number(15,2) | N | 因业务调整,后续下线字段 | +| CCB_DISCOUNT_AMT_DESC | 建行支付侧优惠定义 | varChar(1000) | N | 因业务调整,后续下线字段 | + +--- + +### 3.3 A3341TP03 - 服务方订单查询 + +#### 接口说明 + +| 项目 | 说明 | +|------|------| +| **服务码** | A3341TP03 | +| **服务名称** | 服务方订单查询 | +| **服务类型** | 直接调用 | +| **功能描述** | 订单主动查询,在未收到支付结果通知时使用。查询结果存在延时情况,接口返回未查到可以适当重复查询 | +| **URL** | https://yunbusiness.ccb.com/tp_service/txCtrl/server?txcode=A3341TP03 | +| **流控限制** | 默认流控值为 100/TPM | + +#### 请求参数 + +| 字段名 | 中文名称 | 数据类型 | 必须 | 说明 | +|--------|---------|----------|------|------| +| TX_TYPE | 交易类型 | varChar(1) | Y | 0-支付交易 1-退款交易 a-查询可退款的订单 | +| TXN_PRD_TPCD | 查询时间范围类型 | varChar(2) | Y | 99-自定义时间段查询 | +| STDT_TM | 开始日期时间 | Char(14) | N | 查询时间范围类型为99时必输,格式yyyyMMddhhmiss | +| EDDT_TM | 结束日期时间 | Char(14) | N | 查询时间范围类型为99时必输,格式yyyyMMddhhmiss | +| ONLN_PY_TXN_ORDR_ID | 订单编号 | varChar(60) | N | 调用收银台时支付流水号,对应字段ORDERID | +| SCN_IDR | 场景标识 | varChar(3) | N | BHK-本行卡 THK-他行卡 ZFB-支付宝 CFT-微信 | +| PLAT_MCT_ID | 服务商门店编号 | varChar(21) | N | 外部平台商户号,不为空以这个字段为准 | +| CUSTOMERID | 商户号 | varChar(21) | N | 建行商户编号,与外部平台商户号不能同时为空 | +| BRANCHID | 一级分行号 | varChar(9) | N | 商户一级分行号,用建行商户编号时不能为空 | +| POS_CODE | 柜台号 | varChar(9) | N | 柜台号 | +| POS_ID | POS终端编号 | varChar(19) | N | POS终端编号 | +| TXN_STATUS | 交易状态 | varChar(2) | Y | 00-交易成功 01-交易失败 02-不确定 | +| MSGRP_JRNL_NO | 商户的流水号 | varChar(60) | N | 商户支付流水号或者退款流水号 | +| PAGE | 当前页次 | number(10) | Y | 当前页次 | + +#### 响应参数(摘要) + +| 字段名 | 中文名称 | 数据类型 | 必须 | 说明 | +|--------|---------|----------|------|------| +| CUR_PAGE | 当前页次 | number(10) | N | 每页最多返回10条记录 | +| PAGE_COUNT | 总页次 | number(10) | N | 总页次 | +| ED_CRD_PRTY_IDR_CD | 商户号 | varChar(15) | N | 商户号 | +| PY_AMT | 支付金额 | number(16,2) | N | 单位:元 | +| MRCH_RFND_AMT | 商户退款金额 | number(16,2) | N | 单位:元 | +| LIST | 订单列表 | Array | N | 订单详情列表(见下表) | + +#### LIST 数组字段(订单列表) + +| 字段名 | 中文名称 | 数据类型 | 说明 | +|--------|---------|----------|------| +| ONLN_PY_TXN_ORDR_ID | 订单编号 | varChar(120) | 订单编号 | +| CLRG_STM_DT_TM | 交易时间 | varChar(14) | 格式yyyyMMddhhmiss | +| ACQ_FNDS_CLRG_DT | 记账日期 | varChar(8) | 格式yyyyMMdd | +| ORDR_TM | 原支付订单时间 | varChar(42) | 格式yyyyMMddhhmiss | +| AHN_TXNAMT | 交易金额 | number(16,2) | 单位:元 | +| ORDR_PYRFD_AMT | 退款总额 | number(16,2) | 单位:元 | +| TXN_CLRGAMT | 结算金额 | number(16,2) | 单位:元 | +| MRCHCMSN_AMT | 手续费金额 | number(16,2) | 单位:元 | +| ORIG_AMT | 订单金额 | number(16,2) | 单位:元 | +| DISCOUNT_AMT | 优惠金额 | number(16,2) | 订单金额-交易金额=优惠金额 | +| RETGDS_ORIG_TXNAMT | 原支付金额 | number(16,2) | 单位:元 | +| CST_ACCNO | 支付卡号 | varChar(32) | 支付卡号 | +| CCYCD | 币种 | varChar(3) | 币种 | +| TXN_STATUS | 交易状态 | varChar(2) | 00-成功 01-失败 02/04-不确定 TO-超时 | +| ORIOVRLSTTNEV_TRCK_NO | 银行流水号 | varChar(25) | 银行流水号 | +| MSGRP_JRNL_NO | 商户流水号 | varChar(240) | 退款时商户上送的退款流水号 | +| PAY_MODE | 支付方式 | Char(3) | BHK-建行 THK-他行 ZFB-支付宝 CFT-微信 | + +> **更多字段请参考Excel原文档** + +--- + +### 3.4 A3341TP04 - 服务方订单退款 + +#### 接口说明 + +| 项目 | 说明 | +|------|------| +| **服务码** | A3341TP04 | +| **服务名称** | 服务方订单退款 | +| **服务类型** | 直接调用 | +| **功能描述** | 订单退款时使用。在退款结果未返回结果时,建议先查询退款结果再做后续处理 | +| **URL** | https://yunbusiness.ccb.com/tp_service/txCtrl/server?txcode=A3341TP04 | + +#### 请求参数 + +| 字段名 | 中文名称 | 数据类型 | 必须 | 说明 | +|--------|---------|----------|------|------| +| PLAT_MCT_ID | 服务商门店编号 | varChar(21) | N | 外部平台商户号,不为空以这个字段为准 | +| CUSTOMERID | 商户号 | varChar(21) | N | 建行商户编号,与外部平台商户号不能同时为空 | +| BRANCHID | 一级分行号 | varChar(9) | N | 商户一级分行号,用建行商户编号时不能为空 | +| MONEY | 退款金额 | number(16,2) | Y | 单位:元 | +| ORDER | 订单号 | varChar(30) | Y | 调用收银台时支付流水号,对应字段ORDERID | +| STDT_TM | 开始日期时间 | Char(14) | Y | 根据支付时间往前加4小时,格式yyyyMMddhhmiss | +| EDDT_TM | 结束日期时间 | Char(14) | Y | 根据支付时间往后加4小时,但日期不能超过当前日期,格式yyyyMMddhhmiss | +| REFUND_CODE | 退款流水号 | varChar(30) | N | 可不填,商户可根据需要填写,退款流水号由商户的系统生成(在未收到退款接口返回时,可用此字段查询退款结果) | + +#### 响应参数 + +| 字段名 | 中文名称 | 数据类型 | 必须 | 说明 | +|--------|---------|----------|------|------| +| ORDER_NUM | 订单号 | varChar(30) | Y | 订单号 | +| PAY_AMOUNT | 支付金额 | number(16,2) | Y | 单位:元 | +| AMOUNT | 退款金额 | number(16,2) | Y | 单位:元 | +| Cst_AccNo | 客户账户 | varChar(32) | N | 脱敏后客户退款账号 | + +--- + +### 3.5 A3341TP05 - 订单支付权益查询 + +#### 接口说明 + +| 项目 | 说明 | +|------|------| +| **服务码** | A3341TP05 | +| **服务名称** | 订单支付权益查询 | +| **服务类型** | 直接调用 | +| **功能描述** | 查询订单支付时使用的权益信息。此接口跟订单支付结果一起计算流控TPM | +| **URL** | https://yunbusiness.ccb.com/tp_service/txCtrl/server?txcode=A3341TP05 | + +#### 请求参数 + +| 字段名 | 中文名称 | 数据类型 | 必须 | 说明 | +|--------|---------|----------|------|------| +| PLAT_MCT_ID | 服务商门店编号 | varChar(21) | N | 外部平台商户号(暂不支持) | +| CUSTOMERID | 商户号 | varChar(21) | N | 建行商户编号,与外部平台商户号不能同时为空 | +| BRANCHID | 一级分行号 | varChar(9) | N | 商户一级分行号,用建行商户编号时不能为空 | +| TX_SPECIAL_EC | 交易类型 | varChar(1) | Y | 0-支付交易 1-退款交易 | +| ORDER_ID | 订单号 | varChar(30) | Y | 调用收银台时支付流水号,对应字段ORDERID | +| OriOvrlsttnEV_Trck_No | 原银行流水号 | varChar(25) | N | 原银行流水号 | +| Txn_Prd_TpCd | 交易期间类型代码 | varChar(2) | N | 08-1个小时内,送空则为08 | +| POS_ID | 终端号 | varChar(19) | N | 终端号 | +| Jrnl_TpCd | 流水类型代码 | varChar(2) | N | XM-数字人民币 PT-普通收单 YH-单纯查券优惠 AL-全部,送空则为YH | + +#### 响应参数(摘要) + +| 字段名 | 中文名称 | 数据类型 | 说明 | +|--------|---------|----------|------| +| Py_Amt | 总支付金额 | varChar(19) | 总支付金额 | +| Mrch_Rfnd_Amt | 总退款金额 | varChar(10) | 总退款金额 | +| Py_Ordr_Amt | 总支付订单金额 | varChar(18) | 总支付订单金额 | +| Rght_Cgy_Inf | 权益类信息 | varChar(3000) | 权益信息(见下方说明) | +| LIST | 订单列表 | Array | 订单详情列表 | + +#### Rght_Cgy_Inf 权益类信息说明 + +权益位图格式示例: +``` +P6:{FL3:有价折扣券,AM1:30.00,AM2:30.00},P3:{FL2:02,FL3:满减活动,AM1:30.00,AM2:30.00} +``` + +**字段说明**: +- P6: 商户券 +- FL1: 优惠度量(如积分数、券数量) +- FL2: 清算方式(01-后结算 02-在线一笔清算 03-在线异步清算) +- FL3: 优惠类型(如满减活动、满减券、有价券等) +- FL4: 优惠名称(如活动描述或券名称) +- AM1: 优惠金额 +- AM2: 清算金额 +- AM3: 银行出资金额 +- AM4: 商户出资金额 +- AM5: 优惠券面额 +- AM6: 客户实付金额 +- AM7: 积分抵扣金额 + +**P6 优惠券类型**: +- 免费折扣券 +- 免费代金券 +- 免费实物券 +- 免费定额券 +- 有价折扣券 +- 有价代金券 +- 有价实物券 +- 有价定额券 + +--- + +### 3.6 A3341TP13 - 服务方订单补充信息 + +#### 接口说明 + +| 项目 | 说明 | +|------|------| +| **服务码** | A3341TP13 | +| **服务名称** | 服务方订单补充信息 | +| **服务类型** | 直接调用 | +| **功能描述** | 补充订单的商品信息(SKU_LIST) | +| **URL** | https://yunbusiness.ccb.com/tp_service/txCtrl/server?txcode=A3341TP13 | + +#### 请求参数 + +| 字段名 | 中文名称 | 数据类型 | 必须 | 说明 | +|--------|---------|----------|------|------| +| USER_ID | 会员编号 | varChar(40) | Y | 会员编号 | +| ORDER_ID | 订单编号 | varChar(40) | Y | 订单编号 | +| SKU_LIST | 商品信息列表 | varChar(3000) | N | 商品信息列表(JSON字符串),格式同订单推送接口 | + +#### SKU_LIST 字段说明 + +同订单推送接口的 SKU_LIST 字段,包含: +- SKU_NAME:商品名称 +- SKU_REF_PRICE:商品参考价 +- SKU_NUM:商品数量 +- SKU_SELL_PRICE:商品单价 + +⚠️ **重要提示**:商品售价 SKU_SELL_PRICE 不能大于商品定价 SKU_REF_PRICE,字段长度为字节,注意中文字段长度。 + +--- + +## 附录 + +### A. 订单状态枚举 + +#### 订单状态(ORDER_STATUS / PAY_STATUS) +- `0` - 待支付 +- `1` - 支付成功 +- `2` - 已过期 +- `3` - 支付失败 +- `4` - 取消 + +#### 退款状态(REFUND_STATUS) +- `0` - 无退款 +- `1` - 退款申请 +- `2` - 已退款 +- `3` - 部分退款 + +#### 特殊附加状态(SPECIAL_STATUS) +- `P0000` - 正常状态 +- `P0001` - 待使用 +- `P0002` - 待审核 +- `P0003` - 已支付 +- `P0004` - 待退款 +- `P0005` - 退款中 +- `P0006` - 待发货 +- `P0007` - 已发货 +- `P0008` - 进行中 +- `P0009` - 预定中 +- `P0010` - 预定成功 +- `P0011` - 预定失败 +- `P0012` - 已入住 +- `P0013` - 已离店 +- `P0014` - 未入住 +- `P0015` - 配送中 +- `P0016` - 出券中 + +### B. 服务方订单类型(PLAT_ORDER_TYPE) +- `T0000` - 普通类型 +- `T0001` - 洗车 +- `T0002` - 加油 +- `T0003` - 停车 +- `T0004` - 修车 +- `T0005` - 充电 +- `T0006` - 年检代办 +- `T0007` - 道路救援 +- `T0008` - 云南中石油充值 + +### C. 支付方式(PAY_MODE / Scn_Idr) +- `STSL` - 刷脸 +- `STSK` - 刷卡 +- `BHK` - 本行卡 +- `THK` - 他行卡 +- `ZFB` - 支付宝 +- `CFT` - 微信 + +### D. 交易状态(TXN_STATUS / Txn_Status) +- `00` - 交易成功 +- `01` - 交易失败 +- `02` - 不确定 +- `04` - 不确定(前端无须发冲正) +- `TO` - 交易超时 + +--- + +## 开发指南 + +### 1. 接口调用流程 + +``` +1. 构建公共报文头(CLD_HEADER) + ├── CLD_TX_CHNL:服务方ID(如YS44000009001853) + ├── CLD_TX_TIME:当前时间(YmdHis格式) + ├── CLD_TX_CODE:服务码(如A3341TP01) + └── CLD_TX_SEQ:唯一流水号 + +2. 构建业务参数(CLD_BODY) + └── 根据具体接口填充业务参数 + +3. 整体报文加密 + ├── 将CLD_HEADER + CLD_BODY转JSON + ├── 使用建行公钥RSA加密 + └── 生成MD5签名(mac字段) + +4. 发送HTTP POST请求 + ├── URL:基础地址 + ?txcode=服务码 + ├── 参数:cnt(加密内容)、mac(签名) + └── 请求头:Content-Type: application/x-www-form-urlencoded + +5. 处理响应 + ├── 使用商户私钥解密响应cnt字段 + ├── 验证响应mac签名 + └── 解析CLD_HEADER和CLD_BODY +``` + +### 2. 重要提示 + +⚠️ **密钥管理** +- 商户私钥:用于解密建行返回的数据 +- 商户公钥:参与支付串的MD5签名计算 +- 建行公钥:用于加密发送给建行的数据 + +⚠️ **字段校验** +- SKU_SELL_PRICE(商品售价)不能大于 SKU_REF_PRICE(商品定价) +- 通知类型为"支付状态修改"时,PAY_STATUS 必填,REFUND_STATUS 为空 +- 通知类型为"退款状态修改"时,REFUND_STATUS 必填,PAY_STATUS 为空 + +⚠️ **流控限制** +- 订单查询接口(A3341TP03):默认流控值为 100/TPM +- 权益查询接口(A3341TP05):与订单支付结果一起计算流控TPM + +⚠️ **时间格式** +- 所有时间字段统一使用:`yyyyMMddHHmmss` 格式 +- 示例:`20250121120000` + +### 3. 错误处理 + +- 接口返回404:请在请求头中添加 `Accept: application/json` 和 `Content-Type: application/json` +- 查询结果延时:订单查询接口存在延时,返回未查到时可适当重复查询 +- 退款未返回:建议先调用订单查询接口查询退款结果,再做后续处理 + +--- + +## 4. H5 与原生交互接口(JS Bridge) + +> 版本: v1.1 +> 来源: 建行生活原生与h5交互规范接口1.3(新).html + +### 4.1 交互规范说明 + +#### 4.1.1 H5 请求 JS Bridge API 格式 + +用于 H5 调用由 Native 提供的 js api 能力,如扫一扫、定位、登录等能力。 + +**调用格式:** + +```javascript +// H5 请求 +/** + * H5 与 native api 交互 + * service: api类型 + * action: api名称 + * params: 参数 + * callBack: 回调方法名,H5自定义,通过变量的方式传递到客户端,供客户端执行指令后回调 + */ +CCBMofeBridge.exec(service, action, params, callBackTmp) +``` + +**Native 回调格式:** + +```javascript +// native 回调 callBackTmp 格式定义 +/** + * 客户端回调 H5 调用的 js 方法示例 + * data: 返回的数据,json格式 + * status: 返回状态,0:成功,1:失败,-2:功能未开通 + */ +callBackTmp('{ + "data":"", // jsonStr api响应字典转Json + "status":"0", // 状态码 +}') +``` + +--- + +### 4.2 callCamera - 调用相机/相册 + +#### 接口说明 + +调用手机相机拍照或选择相册图片。 + +#### 请求参数 + +| 字段名 | 类型 | 默认值 | 必填 | 说明 | +|--------|------|--------|------|------| +| sourceType | string | - | 是 | `album` 相册 / `camera` 拍照 | +| maxSize | string | - | 否 | 图片最大大小,单位为 k | +| count | string | 1 | 否 | 最多可以选择的图片张数,目前仅建行生活支持,仅调用相册时支持 | + +#### 成功返回参数 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| imgs | array | 图片 base64 串数组 | + +#### 错误返回参数 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| errCode | string | 错误码:`1014` 用户未授权,`0001` 参数错误,`-1` 用户取消 | +| errMsg | string | 错误信息 | + +#### 调用示例 + +```javascript +var param = { + "sourceType": "album", + "maxSize": "1024", + "count": "2" +} +function callback(res) { + console.log(res); +} +window.callback = callback +window.CCBMofeBridge.exec("baseAPI", "callCamera", JSON.stringify(param), 'callback'); +``` + +#### 响应示例 + +**成功响应:** + +```json +{ + "data": { + "imgs": ["data:image/png;base64,xxxxxxxx", "data:image/png;base64,xxxxxxxx"] + }, + "status": "0" // 0:成功,1:失败,-2:功能未开通 +} +``` + +**失败响应:** + +```json +{ + "data": { + "errCode": "1014", + "errMsg": "用户未授权" + }, + "status": "1" +} +``` + +--- + +### 4.3 getPosition - 获取定位 + +#### 接口说明 + +获取用户当前位置信息。 + +#### 请求参数 + +| 字段名 | 类型 | 默认值 | 建行生活必填 | 手机银行必填 | 说明 | +|--------|------|--------|--------------|--------------|------| +| Can_Ahn_Inst | string | - | 否 | 是 | 机构号 | +| appName | string | - | 否 | 是 | 名称 | +| cdnLogoUrl | string | - | 否 | 否 | logoUrl 地址 | +| mode | string | 1 | 否 | 否 | 定位模式:
`0` 缓存优先,先返回上一次定位缓存再异步更新缓存
`1` 即时定位,loading等待即时定位后返回,并更新缓存(默认)
`2` 兜底定位,先实时定位,超时后返回缓存定位 | +| timeout | string | 2 | 否 | 否 | 定位 API 超时时间,默认值为 2s,支持通用技术参数配置,字段为 locationCacheTime | + +#### 成功返回参数 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| cityName | string | 城市名称,如:广州市 | +| cityCode | string | 城市代码,如:440100 | +| latitude | string | 纬度 | +| longitude | string | 经度 | +| address | string | 地址 | +| adCode | string | 区域行政编码,如:440106 | +| gpsType | string | 坐标系,`bd09` 为百度,`gcj02` 为高德 | + +#### 错误返回参数 + +| 字段名 | 类型 | 说明 | +|--------|------|------| +| gpsType | string | 坐标系,`bd09` 为百度,`gcj02` 为高德 | +| bizCode | string | 地图 sdk 原始定位结果错误码 | +| errCode | string | 错误码:
`1014` 用户未授权
`1016` 手机GPS开关未开启
`1017` 地图sdk返回失败
`0001` 参数错误 | +| errMsg | string | 错误信息 | + +#### 调用示例 + +```javascript +var param = {} +function callback(res) { + console.log(res); +} +window.callback = callback +window.CCBMofeBridge.exec("baseAPI", "getPosition", JSON.stringify(param), 'callback'); +``` + +#### 响应示例 + +**成功响应:** + +```json +{ + "data": { + "gpsType": "bd09", + "adCode": "440106", + "address": "中国广东省广州市天河区猎德街道珠江新城华夏路10号", + "cityCode": "440100", + "cityName": "广州市", + "latitude": "23.123691", + "longitude": "113.329941" + }, + "status": "0" +} +``` + +**失败响应:** + +```json +{ + "data": { + "gpsType": "bd09", + "bizCode": "06", + "errCode": "1017", + "errMsg": "定位失败" + }, + "status": "1" +} +``` + +--- + +## 5. 服务方接入规范 + +> 版本: v2.20 +> 来源: 建行相关App服务方接入文档v2.20_20250725.html + +### 5.1 跳转 URL 规范 + +建行相关 App 通过分类入口跳转或订单详情跳转至服务方链接时,使用 URL 格式为: + +``` +url?platform=ccblife&channel=mbs&ccbParamSJ=xxxxxx&CITYID=330100&USERCITYID=440100 +``` + +**参数说明:** + +- **url**: 服务方页面链接地址(一般为中间页面,用于对跳转参数的解密验签处理及二次跳转) +- **platform**: `ccblife` 为建行生活平台标识符 +- **channel**: 当前运行环境所在的 App + - `channel=mbs` 表示在中国建设银行 App 运行 + - 无此参数则默认为在建行生活 App 运行 +- **CITYID**: 用户选择城市代码(6位中国城市代码) +- **USERCITYID**: 用户定位城市代码(6位中国城市代码) +- **ccbParamSJ**: 使用服务方公钥 RSA 加密后再 base64 + encodeURIComponent 的加密密文 + +#### ccbParamSJ 密文携带的参数 + +解密后明文格式为: `USERID=xxx&MOBILE=xxx...`(先进行 URLDecode 之后,再进行解密) + +| 参数名 | 参数类型 | 中文名 | 备注 | +|--------|----------|--------|------| +| BGCOLOR | String | 背景色 | 默认为空 | +| PLATFLOWNO | String | 登录校验流水号 | | +| TIMESTAMP | String | 跳转服务方时间戳 | 13位毫秒级 | +| USERID | String | 建行生活用户ID | | +| MOBILE | String | 手机号码 | | +| CITYID | String | 用户选择城市代码 | 使用标准的中国城市代码 | +| USERCITYID | String | 用户定位城市代码 | | +| LGT | String | 经度 | | +| LTT | String | 纬度 | | +| GPS_TYPE | String | GPS坐标系 | `gcj02`(高德地图坐标)、`bd09`(百度地图坐标) | +| APPID | String | 小程序appid | 小程序专用参数,APP不存在 | +| OPENID | String | 用户在小程序下唯一标识 | 小程序专用参数,APP不存在 | +| ORDERID | String | 订单号 | 通过订单详情跳转会携带此字段 | +| TOKEN | String | 建行生活用户TOKEN | 仅通过手机银行访问建行生活场景时携带 | + +#### 加密串验证建议 + +⚠️ **重要提示**:为防止加密串被非法窃取导致的越权风险,建议服务方对加密串进行唯一请求校验。 + +可选校验方案: +1. 使用 PLATFLOWNO 作为一次性令牌,使用后立即失效 +2. 验证 TIMESTAMP 时间戳,拒绝超过一定时间(如5分钟)的请求 +3. 结合 USERID 和 PLATFLOWNO 进行会话绑定 + +--- + +### 5.2 建行相关 App 环境识别 + +服务方生活场景,按本章节方法识别当前运行的 App 环境是否为建行相关 App。 + +#### 5.2.1 建行生活 App 环境判断 + +通过检测 URL 参数中的 `platform=ccblife` 和 `channel` 参数来判断: + +```javascript +// 判断是否为建行生活 App +function isCCBLifeApp() { + const urlParams = new URLSearchParams(window.location.search); + const platform = urlParams.get('platform'); + const channel = urlParams.get('channel'); + + return platform === 'ccblife' && !channel; +} +``` + +#### 5.2.2 中国建设银行 App 环境判断 + +通过检测 URL 参数中的 `channel=mbs` 来判断: + +```javascript +// 判断是否为中国建设银行 App +function isCCBApp() { + const urlParams = new URLSearchParams(window.location.search); + const channel = urlParams.get('channel'); + + return channel === 'mbs'; +} +``` + +--- + +### 5.3 收银台调用方法 + +#### 5.3.1 在建行生活 App 环境收银台调用 + +##### (1) iPhone 系统调用 + +```javascript +// iOS 系统调用收银台 +function callCashierIOS(paymentParams) { + // paymentParams 为支付参数串 + window.location.href = 'ccbpayment://openCashier?params=' + encodeURIComponent(paymentParams); +} +``` + +##### (2) Android 系统调用 + +```javascript +// Android 系统调用收银台 +function callCashierAndroid(paymentParams) { + // paymentParams 为支付参数串 + window.CCBAndroid.callCashier(paymentParams); +} +``` + +#### 5.3.2 在中国建设银行 App 环境收银台调用 + +```javascript +// 在建设银行 App 中调用收银台 +function callCashierInCCBApp(paymentParams) { + window.CCBMofeBridge.exec("paymentAPI", "callCashier", paymentParams, 'paymentCallback'); +} + +function paymentCallback(result) { + // 处理支付结果 + console.log('支付结果:', result); +} +``` + +--- + +### 5.5 支付成功页面回调设置 + +#### 5.5.1 回调地址设置方法 + +支付完成后默认跳转到建行生活的支付成功页面,如服务方需要跳转到自己的成功页,请调用 `setCache` 方法来设置。 + +⚠️ **重要提示**:`setCache` 操作须在调起收银台(5.3.1 在建行生活App环境收银台调用)**之前**进行。 + +**设置方法(仅在建行生活 App 环境有效):** + +```javascript +var requestObj = { + action: 'setCache', // 设置回调地址 + params: { + key: '', // 回调的 key,与下单参数的 remark2 保持一致,上送服务方编号 + value: '' // 回调的 url,支付成功后将由建行生活收银台跳转至此 url + } +} + +window.CCBBridge.requestNative(JSON.stringify(requestObj), 'callBackName'); + +// 注:callBackName 是回调函数的名称(可自行定义),请先将回调函数挂载到 window 对象下 +// 该回调仅作为地址设置成功的参考 +``` + +**在中国建设银行 App 环境:** + +在中国建设银行 App 环境需通过《5.4 商户下单支付参数定义》中的 **PAYSUCCESSURL** 参数提供支付成功页面 URL。 + +#### 5.5.2 回调 URL 携带参数 + +支付成功跳转后,设置的回调 URL 会增加携带如下参数: + +| 字段名 | 中文名 | 备注 | +|--------|--------|------| +| MERCHANTID | 商户号 | | +| POSID | 柜台号 | | +| ORDERID | 订单号 | | +| PAYMENT | 订单金额 | | +| SUCCESS | 支付成功标识 | 固定为 Y | +| REMARK2 | 支付备注二 | | +| realPayment | 实付金额 | 优惠后实际支付的金额 | +| ccbParamSJ | 用户信息加密串 | 与跳转服务方携带的相同 | + +⚠️ **重要提示**:最终支付结果请以服务器通知(7.1 建行生活支付通知接口)为准,前端回调仅作为页面跳转使用。 + +--- + +### 5.6 其他说明 + +#### 5.6.1 支付流程要点 + +1. **调起支付**:建行相关 App 根据订单信息调起支付 +2. **完成支付**:在支付模块完成支付后,点击完成支付,关闭支付模块,跳转到支付成功页面 +3. **结果确认**:⚠️ **支付成功结果最终以服务器通知为准**(参考 7.1 建行生活支付通知接口) + +#### 5.6.2 业务流程说明 + +**以建行生活 App 环境为例(仅供参考):** + +**重点步骤说明:** + +1. **步骤2 - 订单推送**: + - 由服务方调用订单推送接口(**A3341TP01**)向建行生活推送订单信息 + - 参考《3.1 A3341TP01 - 服务方订单推送》 + +2. **步骤3 - 调用收银台**: + - 参考《5.3 收银台调用方法》调用收银台 + - 参考《5.4 商户下单支付参数定义》构建支付参数 + +3. **步骤10、11 - 支付通知**: + - 支付通知有两种推送方式,具体细节参考《7.1 建行生活支付通知接口》: + - **方式1**:网银通知建行生活,再由建行生活转发给服务方(推荐) + - **方式2**:网银直接通知服务方 + +4. **步骤13、16 - 订单状态更新**: + - 由服务方调用订单更新接口(**A3341TP02**)向建行生活更新订单状态 + - 参考《3.2 A3341TP02 - 服务方订单更新》 + +5. **步骤14、15 - 主动查询**: + - 服务方可以向外联平台主动查询订单交易流水,判断订单的支付状态 + - 也可以使用订单查询接口(**A3341TP03**)查询订单状态 + - 参考《3.3 A3341TP03 - 服务方订单查询》 + +**业务流程图:** + +``` +用户 → 服务方H5页面 → 调用订单推送接口(A3341TP01) → 建行生活 + ↓ + 调用收银台(支付参数) + ↓ + 建行生活/建行App收银台 + ↓ + 用户支付 + ↓ + ┌───────────┴───────────┐ + ↓ ↓ + 支付成功页面 支付通知(方式1或方式2) + ↓ ↓ + 回调服务方H5 服务方后台接收通知 + ↓ ↓ + 用户确认订单 更新订单状态(A3341TP02) + ↓ + 主动查询确认(A3341TP03) +``` + +--- + +### 5.4 商户下单支付参数定义(完整) + +#### 参数说明 + +| 字段名 | 中文名 | 类型 | 是否非空 | 是否必送 | 备注 | +|--------|--------|------|----------|----------|------| +| **MERCHANTID** | 商户代码 | CHAR(15) | Y | F | 由建行统一分配 | +| **POSID** | 柜台代码 | CHAR(9) | Y | F | 由建行统一分配,9位柜台号 | +| **BRANCHID** | 分行代码 | CHAR(9) | Y | F | 由建行统一分配 | +| POSID19 | 商户19位终端号 | CHAR(19) | N | F | 由建行统一分配,使用微信支付时上送。仅作为参数传递,不参与MAC校验 | +| **PLATMCTID** | 外部平台商户号 | CHAR(19) | Y | F | 当使用外部商户号时,建行商户号、柜台号、分行号及终端号无需上送。当该字段有值时参与MAC校验,否则不参与MAC校验 | +| **ORDERID** | 支付流水号 | CHAR(30) | Y | T | 由商户提供,最长30位,支付时上送到支付中台,支付结果查询和退款使用 | +| **USER_ORDERID** | 用户订单号 | CHAR(30) | Y | T | 由商户提供,最长30位,用户订单列表订单信息,订单推送和更新时订单号,支付校验订单信息 | +| **PAYMENT** | 付款金额 | NUMBER(16,2) | Y | T | 由商户提供,最长30位 | +| **CURCODE** | 币种 | CHAR(2) | Y | T | 缺省为 01-人民币(只支持人民币支付) | +| **TXCODE** | 交易码 | CHAR(6) | Y | T | 由建行统一分配为 520100 | +| REMARK1 | 备注1 | CHAR(30) | N | T | 网银不处理,直接传到城综网,该字段只支持送数字和英文 | +| **REMARK2** | 备注2 | CHAR(30) | Y | T | 上送 YS 开头的服务方编号,与 PLATFORMID 保持一致 | +| **TYPE** | 接口类型 | CHAR(1) | Y | T | 默认送 1 - 防钓鱼接口 | +| **GATEWAY** | 网关类型 | CHAR(100) | Y | T | 默认送 0 | +| CLIENTIP | 客户端IP | CHAR(40) | N | T | 送空值即可 | +| REGINFO | 客户注册信息 | CHAR(256) | N | T | 客户在商户系统中注册的信息,中文需使用 escape 编码。送空值即可 | +| **PROINFO** | 商品信息 | CHAR(256) | N | T | 客户购买的商品信息,收银台会展示该信息,中文需使用 escape 编码。建议编码前长度不超过50位 | +| REFERER | 商户URL | CHAR(100) | N | T | 商户送空值即可 | +| INSTALLNUM | 分期期数 | CHAR(2) | N | F | 信用卡支付分期期数,一般为 3、6、12 等,必须为大于 1 的整数。仅当分期支付时上送该字段,无此字段上送时,则视为普通支付 | +| **THIRDAPPINFO** | 客户端标识 | CHAR(40) | Y | T | 通过建行相关App下单场景,订单中客户端标识固定设为 `comccbpay1234567890cloudmerchant` | +| TIMEOUT | 订单超时时间 | CHAR(14) | N | F | 格式:YYYYMMDDHHMMSS(如:20120214143005)银行系统时间 > TIMEOUT 时拒绝交易,若送空值则不判断超时。当该字段有值时参与MAC校验,否则不参与MAC校验 | +| USERID | 建行生活用户ID | CHAR(32) | N | F | 仅在中国建设银行App环境使用。当该字段有值时参与MAC校验,否则不参与MAC校验 | +| TOKEN | 建行生活用户TOKEN | CHAR(32) | N | F | 仅在中国建设银行App环境使用。当该字段有值时参与MAC校验,否则不参与MAC校验 | +| PAYSUCCESSURL | 支付成功页面URL | CHAR(128) | N | F | 仅在中国建设银行App环境使用,如需指定支付成功页面时提供,需对URL编码,生产环境必须为HTTPS。未提供则默认跳转到建行生活的支付成功页面。当该字段有值时参与MAC校验,否则不参与MAC校验 | +| PAYBITMAP | 支付位图 | CHAR(10) | N | F | 默认为空,特定场景使用 | +| ACCOUNTBITMAP | 支付账户位图 | CHAR(10) | N | F | 默认为空,特定场景使用 | +| POINTAVYID | 积分二级活动编号 | VARCHAR(6) | N | F | 默认为空,特定场景使用。龙支付积分二级活动上送 010051 | +| DCEPDEPACCNO | 数字人民币收款钱包编号 | VARCHAR(32) | N | F | 默认为空,特定场景使用。数字人民币商户绑定的收款钱包编号 | +| COUPONAVYID | 有价券活动编号 | VARCHAR(32) | N | F | 默认为空,特定场景使用 | +| ONLY_CREDIT_PAY_FLAG | 限制信用卡支付标志 | CHAR(1) | N | F | 默认为空,特定场景使用。当有价券活动编号不为空时生效,送Y限制仅信用卡能支付,送N或空不作限制 | +| FIXEDPOINTVAL | 固定抵扣积分值 | VARCHAR(16) | N | F | 默认为空,特定场景使用。上送该值时,若用户不满足积分使用条件将拒绝支付 | +| MINPOINTLIMIT | 最小使用积分抵扣限制 | VARCHAR(16) | N | F | 默认为空,特定场景使用。上送整数值时,视为最小积分抵扣数额;上送小于1的小数时,视为最小积分抵扣比例。若用户不满足积分使用条件将拒绝支付 | +| IDENTITYCODE | 身份证后6位 | VARCHAR(256) | N | F | 默认为空,特定场景使用。仅中石化服务方可用。身份证号后6位加密串,用于身份识别。加密说明:用服务方公钥对身份证后6位进行RSA加密,再进行base64,再进行encodeURIComponent一次 | +| NOTIFY_URL | 支付异步通知地址 | VARCHAR(512) | N | F | 默认为空,特定场景使用。仅中石化服务方可用。支付结果异步通知地址的encodeURIComponent编码值,多个通知地址时,分隔符用英文半角符号 , 分隔 | +| DCEP_MCT_TYPE | 数币商户类型 | CHAR(1) | N | F | 默认为空,特定场景使用。0\空-不识别为数币商户;1-融合商户;2-非融合商户 | +| DCEP_MERCHANTID | 数字人民币商户号 | CHAR(15) | N | F | 默认为空,特定场景使用。当 DCEP_MCT_TYPE 为 2 时上送值 | +| DCEP_POSID | 数字人民币柜台号 | CHAR(9) | N | F | 默认为空,特定场景使用。当 DCEP_MCT_TYPE 为 2 时上送值 | +| DCEP_BRANCHID | 数字人民币分行号 | CHAR(9) | N | F | 默认为空,特定场景使用。当 DCEP_MCT_TYPE 为 2 时上送值 | +| SUB_MCT_ID | 服务方二级商户编号 | VARCHAR(20) | N | F | 默认为空,涉及银联反欺诈时使用。若支付发起方为平台类服务方,需上送第三方平台下实际发起支付的二级商户所对应的商户编号 | +| SUB_MCT_NAME | 服务方二级商户名称 | VARCHAR(40) | N | F | 默认为空,涉及银联反欺诈时使用。若支付发起方为平台类服务方,需上送第三方平台下实际发起支付的二级商户所对应的商户名称 | +| SUB_MCT_MCC | 服务方二级商户类别 | CHAR(4) | N | F | 默认为空,涉及银联反欺诈时使用。若支付发起方为平台类服务方,需上送第三方平台下实际发起支付的二级商户所对应的商户MCC码 | +| EXTENDPARAMS | 扩展域 | VARCHAR(256) | N | F | 默认为空,特定场景使用。上送约定JSON格式字符串 | +| **PLATFORMPUB** | 服务方公钥 | VARCHAR(256) | Y | F | 仅作为源串参加 MD5 摘要,不作为参数传递 | +| **MAC** | MD5加密串 | CHAR(32) | Y | T | 采用标准 MD5 算法,对以上字段进行 MAC 加密(32位小写),由商户实现 | +| **PLATFORMID** | 服务方编号 | CHAR(16) | Y | T | 以 YS 开头的16位编号。仅作为参数传递,不参与MAC校验 | +| ENCPUB | 商户公钥密文 | VARCHAR(512) | Y | F | 使用服务方公钥对商户公钥后30位进行 RSA 加密并 base64 后的密文。若商户已经上架建行生活并同步公钥,或是使用外部商户号时,可以不再上送商户公钥。仅作为参数传递,不参与MAC校验 | +| SCNID | 场景编号 | VARCHAR(32) | N | F | 默认为空,埋点使用。特色场景的唯一标识。仅作为参数传递,不参与MAC校验 | +| SCN_PLTFRM_ID | 场景平台编号 | VARCHAR(32) | N | F | 默认为空,埋点使用。场景平台唯一标识。仅作为参数传递,不参与MAC校验 | + +#### 字段说明 + +**是否非空**: +- `Y` - 上送该字段时,值不可为空 +- `N` - 值可以为空 + +**是否必送**: +- `T` - 该字段必须上送,无论是否有值 +- `F` - 字段可以不送 + +**重点字段**(已用粗体标注): +1. **商户标识**:MERCHANTID、POSID、BRANCHID、PLATMCTID +2. **订单标识**:ORDERID、USER_ORDERID +3. **金额信息**:PAYMENT、CURCODE +4. **交易信息**:TXCODE、TYPE、GATEWAY +5. **服务方信息**:REMARK2、PLATFORMID、PLATFORMPUB +6. **客户端标识**:THIRDAPPINFO +7. **商品信息**:PROINFO +8. **签名信息**:MAC + +#### MD5 签名计算详解 + +**签名规则:** + +采用标准 MD5 摘要算法对字符串数据签名(32位小写),得到 MAC。 + +> **注意**:和交易通讯报文的 mac 区分,签名时不需要额外拼接 privateKey + +**参与签名的字段:** + +1. **必须参与签名的字段**(按顺序): + - MERCHANTID(或 PLATMCTID) + - POSID + - BRANCHID + - ORDERID + - USER_ORDERID + - PAYMENT + - CURCODE + - TXCODE + - REMARK1 + - REMARK2 + - TYPE + - GATEWAY + - CLIENTIP + - REGINFO + - PROINFO + - REFERER + - THIRDAPPINFO + +2. **有值时参与签名的字段**(橙色字段): + - PLATMCTID(使用外部平台商户号时) + - TIMEOUT + - USERID + - TOKEN + - PAYSUCCESSURL + - DCEP_MCT_TYPE + - 其他特定场景字段 + +3. **最后拼接**: + - PLATFORMPUB(服务方公钥) + +**签名计算示例:** + +``` +参与签名的字符串(按顺序拼接,格式:字段名=值&字段名=值): + +MERCHANTID=105910100194086&POSID=313368474&BRANCHID=441000000&ORDERID=202209020000000061&USER_ORDERID=202209020000000061&PAYMENT=22.39&CURCODE=01&TXCODE=520100&REMARK1=&REMARK2=YS44000098000600&TYPE=1&GATEWAY=0&CLIENTIP=®INFO=&PROINFO=&REFERER=&THIRDAPPINFO=comccbpay1234567890cloudmerchant&TIMEOUT=20220902103420&DCEP_MCT_TYPE=1&PLATFORMPUB=MIGfMA0GCSqGSIb...... + +然后对整个字符串进行 MD5 加密(32位小写): +MAC=f07ef63236e3bbbc1dc221b06e631f3d +``` + +#### 中文信息编码 + +**REGINFO(客户注册信息)** 和 **PROINFO(商品信息)** 中的中文需要使用 JavaScript 的 `escape()` 方法进行编码,数字字母信息不需编码。 + +**编码示例:** + +```javascript +escape("小飞侠") = "%u5C0F%u98DE%u4FA0" +escape("A1级牛排") = "A1%u7EA7%u725B%u6392" +``` + +#### 商户公钥密文(ENCPUB) + +**ENCPUB** 是各服务方使用自己的**服务方公钥**对**商户公钥后30位**进行 RSA 加密,再进行 base64 后,生成的密文串。 + +**生成步骤:** + +1. 取商户公钥的后30位字符 +2. 使用服务方公钥进行 RSA 加密 +3. 对加密结果进行 base64 编码 +4. 得到 ENCPUB 密文串 + +> **注意**:若商户已经上架建行生活并同步公钥,或是关联了外部平台商户号,可以不再上送商户公钥。 +> +> 公钥加密方法见《6.2 报文加密及签名》章节。 + +#### 外部平台商户号说明(PLATMCTID) + +外部平台商户号用于服务方已在建行生活完成商户关联,且收款商户已上架建行生活的场景。 + +**使用规则:** + +- **使用外部平台商户号时**: + - 必须上送 PLATMCTID + - 无需上送 MERCHANTID、POSID、BRANCHID + - PLATMCTID 参与 MAC 校验 + +- **不使用外部平台商户号时**: + - 必须上送 MERCHANTID、POSID、BRANCHID + - 无需上送 PLATMCTID + +#### 扩展域说明(EXTENDPARAMS) + +该字段仅提供给特定服务方场景使用,内容为双方约定值,格式为 **JSON 字符串**,上送时需要 **encodeURI 编码**。 + +**示例 - 善融积分:** + +```json +{ + "scene": "pointPay", // 场景 + "pointAmount": "1.50", // 积分金额 + "pointNum": "1050" // 积分数量 +} +``` + +**使用步骤:** + +1. 构建 JSON 字符串 +2. 使用 encodeURI 进行编码 +3. 将编码后的字符串作为 EXTENDPARAMS 的值 + +> 更多扩展域格式请参考原文档的"扩展域列表"章节 + +#### PAYBITMAP 和 ACCOUNTBITMAP 说明 + +两个字段分别为定长 **10位数字的位图**,位图为 `1` 表示可用,为 `0` 表示不可用。 + +**PAYBITMAP(支付方式位图):** + +| 位数 | 支付方式 | 说明 | +|------|----------|------| +| 第1位 | 生活钱包支付 | 1=可用,0=不可用 | +| 第2位 | 龙支付 | 1=可用,0=不可用 | +| 第3位 | 微信支付 | 1=可用,0=不可用 | +| 第4位 | 数币支付 | 1=可用,0=不可用 | +| 第5位 | 信用付 | 1=可用,0=不可用 | +| 第6位 | 快贷支付 | 1=可用,0=不可用 | +| 第7-10位 | 保留 | 预留位 | + +**ACCOUNTBITMAP(支付账户位图,仅对龙支付及生活钱包下的卡账户生效):** + +| 位数 | 账户类型 | 说明 | +|------|----------|------| +| 第1位 | 建行借记卡 | 1=可用,0=不可用 | +| 第2位 | 建行贷记卡 | 1=可用,0=不可用 | +| 第3位 | 他行借记卡 | 1=可用,0=不可用 | +| 第4位 | 他行贷记卡 | 1=可用,0=不可用 | +| 第5位 | 建行钱包 | 1=可用,0=不可用 | +| 第6-10位 | 保留 | 预留位 | + +**使用说明:** + +- 两个字段可单独使用 +- 未上送时默认全部支付方式都支持,即全为 `1`(1111111111) +- 该位图字段在判断支付方式是否可用时**优先级最低** +- 仅当商户已支持某一种支付方式时,服务方可对其决定是否使用 +- 例如商户不支持微信支付,即使位图上送 `1` 也不会生效 + +**示例:** + +``` +PAYBITMAP=1110000000 // 仅支持生活钱包、龙支付、微信支付 +ACCOUNTBITMAP=1100000000 // 仅支持建行借记卡和建行贷记卡 +``` + +#### ORDERID 和 USER_ORDERID 的区别 + +| 字段 | 用途 | 说明 | +|------|------|------| +| **ORDERID** | 商户支付流水号 | 用于建行收单支付流程,包括在支付、查询支付结果及退款交易中使用 | +| **USER_ORDERID** | 用户订单号 | 用于同步用户在建行生活中的订单状态,包括在订单推送及状态更新时使用 | + +**使用场景:** + +- **ORDERID**:调用收银台时上送的支付流水号(对应收银台 ORDERID 字段) +- **USER_ORDERID**:建行生活订单列表展示的订单号(对应收银台 USER_ORDERID 字段,支付时会校验订单信息) + +#### 最终参数串示例 + +完整的支付参数串由以下部分组成: + +``` +参与验签字符串 + MAC + PLATFORMID + ENCPUB + +示例: +MERCHANTID=105910100194086&POSID=313368474&BRANCHID=441000000&ORDERID=202209020000000061&USER_ORDERID=202209020000000061&PAYMENT=22.39&CURCODE=01&TXCODE=520100&REMARK1=&REMARK2=YS44000098000600&TYPE=1&GATEWAY=0&CLIENTIP=®INFO=&PROINFO=&REFERER=&THIRDAPPINFO=comccbpay1234567890cloudmerchant&TIMEOUT=20220902103420&DCEP_MCT_TYPE=1&MAC=f07ef63236e3bbbc1dc221b06e631f3d&PLATFORMID=YS44000098000600&ENCPUB=YzNxRGtKSkFYZURRczYvNDN6WVZkYk...... +``` + +> **注意**: +> - MAC 不参与 MAC 校验,但需要包含在最终参数串中 +> - PLATFORMID 仅作为参数传递,不参与 MAC 校验 +> - ENCPUB 仅作为参数传递,不参与 MAC 校验 + +--- + +## 6. 报文规范与加密 + +### 6.1 报文结构 + +#### 6.1.1 请求报文格式 + +请求报文由两部分组成:**交易请求报文头(CLD_HEADER)** 和 **交易请求报文体(CLD_BODY)**。 + +**报文头字段(CLD_HEADER):** + +| 字段标识 | 字段中文名 | 字段说明 | 数据属性 | 是否必填 | 备注 | +|----------|------------|----------|----------|----------|------| +| CLD_TX_CHNL | 报文来源渠道 | 消息来源渠道 | CHAR(20) | Y | 以 YS 开头的服务方编号 | +| CLD_TX_TIME | 报文交易时间 | 交易时间 | CHAR(14) | Y | 上送交易时间,格式: yyyyMMddHHmmss | +| CLD_TX_CODE | 交易服务ID | 服务ID | CHAR(64) | Y | 调用的交易名称 | +| CLD_TX_SEQ | 交易流水标识 | 流水号 | CHAR(32) | Y | 用于标识唯一性 | + +**请求报文示例:** + +```json +{ + "CLD_HEADER": { + "CLD_TX_CHNL": "YSTEST", + "CLD_TX_TIME": "20241231090000", + "CLD_TX_CODE": "A3341TP01", + "CLD_TX_SEQ": "1010114131620897033814262" + }, + "CLD_BODY": { + // 具体业务参数 + } +} +``` + +#### 6.1.2 响应报文格式 + +响应报文由两部分组成:**交易响应报文头(CLD_HEADER)** 和 **交易响应报文体(CLD_BODY)**。 + +**报文头字段(CLD_HEADER):** + +| 字段标识 | 字段中文名 | 字段说明 | 数据属性 | 是否必填 | 备注 | +|----------|------------|----------|----------|----------|------| +| CLD_TX_CHNL | 报文来源渠道 | 消息来源渠道 | CHAR(20) | Y | 以 YS 开头的服务方编号 | +| CLD_TX_TIME | 报文交易时间 | 交易时间 | CHAR(14) | Y | 格式: yyyyMMddHHmmss | +| CLD_TX_CODE | 交易服务ID | 服务ID | CHAR(64) | Y | 调用的交易名称 | +| CLD_TX_SEQ | 交易流水标识 | 流水号 | CHAR(32) | Y | 用于标识唯一性 | +| CLD_CODE | 交易响应码 | 响应码 | CHAR(32) | Y | 详见响应字典 | +| CLD_DESC | 交易响应内容 | 响应描述 | CHAR(32) | Y | 响应描述信息 | + +**响应报文示例:** + +```json +{ + "CLD_HEADER": { + "CLD_TX_CHNL": "YSTEST", + "CLD_TX_TIME": "20151231090000", + "CLD_TX_CODE": "A3341TP01", + "CLD_TX_SEQ": "1010114131620897033914221", + "CLD_TX_RESP": { + "CLD_CODE": "0000", + "CLD_DESC": "成功" + } + }, + "CLD_BODY": { + // 具体业务返回数据 + } +} +``` + +#### 6.1.3 报文编码说明 + +- 报文使用 **UTF-8** 编码 +- 非二进制类型报元单个报元长度建议在 9999 位以内 +- 整体报文大小建议在 20K 以内 +- 登录类交易可以在 2M 以内(考虑到账户数量较多的情况) + +--- + +### 6.2 报文加密及签名 + +#### 6.2.1 加密流程说明 + +服务方主动调用建行生活的接口时,需传递的是 **服务方编号**、**RSA密文** 及 **签名** 三个数据。 + +**加密后报文格式:** + +```json +{ + "svcid": "服务方ID,以YS开头", + "cnt": "密文xxxx", + "mac": "签名yyyy" +} +``` + +**错误响应格式:** + +```json +{ + "code": "错误码xxxx", + "msg": "错误描述yyyy" +} +``` + +#### 6.2.2 加密签名方法(Java 示例) + +**加密及获取签名:** + +```java +// 公钥 +String publicKey = ""; +// 私钥 +String privateKey = ""; + +// 源报文(未加密) +String msg = ""; + +// 1. 公钥加密得到密文并使用 base64 处理 +String enc_msg = RSAUtil.encrypt(msg, publicKey); +Encoder encoder = Base64.getEncoder(); +enc_msg = encoder.encodeToString(enc_msg.getBytes("UTF-8")); +enc_msg = enc_msg.replaceAll("\r\n", "").replaceAll("\r", "").replaceAll("\n", ""); + +// 2. 根据源报文+私钥获得 MD5 签名 +String mac_info = MD5Util.getMD5(msg + privateKey); +``` + +**解密及验签:** + +```java +// 1. base64 逆处理并用私钥解密 +Decoder decoder = Base64.getMimeDecoder(); +enc_msg = new String(decoder.decode(enc_msg), "UTF-8"); +String dec_msg = RSAUtil.decrypt(enc_msg, privateKey); + +// 2. 验签 +String dec_mac = MD5Util.getMD5(dec_msg + privateKey); +if (mac_info.equals(dec_mac)) { + System.out.println("验签通过"); +} else { + System.out.println("验签失败"); +} +``` + +> **提示**:如果使用的 JDK 版本高于 1.8,请将 base64 方法由 `sun.misc.BASE64Encoder` 替换为 `java.util.Base64` + +#### 6.2.3 加密报文完整示例 + +**原始报文:** + +```json +{ + "CLD_HEADER": { + "CLD_TX_CHNL": "YSTEST", + "CLD_TX_TIME": "20191112145911", + "CLD_TX_CODE": "A3341TP01", + "CLD_TX_SEQ": "" + }, + "CLD_BODY": { + "USER_ID": "user123", + "ORDER_ID": "order123", + "ORDER_DT": "20191112145811", + "TOTAL_AMT": "100.00", + "PAY_AMT": "90.00", + "DISCOUNT_AMT": "10.00", + "ORDER_STATUS": "1", + "REFUND_STATUS": "0", + "MCT_NM": "XXX商户" + } +} +``` + +**加密后报文:** + +```json +{ + "cnt": "UWl3VVVIT0IzSHp6VUtpNFVmb25VcWJjMnBKdU9IclJUZ3lISGJLR3Myd21saWQvMTVvenNMNk5xWmFwUzl1clh5K2VkVmJuU1BYMg0KTzUwamNkZWRmdjc5NHdDRmJSdjF4WDl4bzAwejMwU2k2NXJ6cTVaVVU4dUt2ZmNPeVJ1cDJkTC9SOUgvODVUblNLbEVXUnErSGo3dw0KMzB2YjFaeU1acU5iRVZnb25VSTUrcmlPV2wweWUwb2xQTVFoei9CVWhDcU9qOExqR21hTDBCdGpVbVdiTyttU3Vpa0s5Y25md3g0SQ0KV0xUSk1BWjl3OHdkZ0FqamI5bXhJbFJUM3JrU3VId1RMdGF0VkdJcGovVDAvdk5nQzNUcWgvODJOQnZvYXdoYnl0Z3AwRW9FNWp5bw0KdVhmYVZDZE9IeGp4ZkpyclFFSCt5THFoZXl2MEx2OGJ1b0x0N0pYMi9zM0pJS0J1bFdSZWJHRTMrcmpEMm9sUVJiYklONkdpeDlodQ0KUEprWkFkK0UrcTV1ZFR4OXpFRlJyWVBHVURzL0RLK0JFSk5iYWZGRnllZTBrRzh2YVlmU1NxeFBWcVBrblRzZ2oxdXFqME0xTGFCNQ0KZ1drVU9vVnl4bUtXUEpYd3JqYXlkSUxVeEZ2dkwzZEpHYmV6cnUwK3d1V3A2QXBQVnRkOElaSm4NCg==", + "mac": "1659E42FC9D2A2186228C4607A566D78", + "svcid": "YSTEST" +} +``` + +--- + +## 7. 回调通知接口 + +### 7.1 建行生活支付通知接口 + +#### 7.1.1 背景描述 + +如服务方生活场景涉及支付环节,必须按本章节要求完成支付回调通知对接。 + +⚠️ **重要提示**: +- 支付结果推送接口属于建行生活主动推送给服务方,所以不会附带服务方编号 +- 服务方接收支付通知的系统地址,需要配置在建行生活运营后台,请联系运维进行配置 +- 因网络问题存在支付通知丢失的可能性,请服务方考虑通过主动查询支付流水的方式作为兜底(参考《3.3 A3341TP03 - 服务方订单查询》) + +#### 7.1.2 接口推送报文示例 + +```json +{ + "ACC_TYPE": "WX", + "BRANCHID": "310000000", + "CCB_DISCOUNT_AMT": "", + "CCB_DISCOUNT_AMT_DESC": "", + "CLIENTIP": "11.123.44.55", + "CURCODE": "01", + "MRCH_ID": "", + "NT_TYPE": "YS", + "ORDERID": "210520133844test3yhv", + "PAYMENT": "0.01", + "PAY_TYPE": "01", + "POSID": "123442025", + "REFERER": "", + "REMARK1": "", + "REMARK2": "YS1234009000111**WXZF", + "SIGN": "1a4aca34f02c0efab014ff823153a2a07a35a8a8410591a992d4aa9cd7fa1f961b952236a45b4fe37867ad4a7214ecbeae21957dee9c19d3d2f5dcf09c8faca81ceb68119c8dcd53bd1e70c23f60fc03e7cd96b8c0082a1243d688c13aac0bcd9e177c559e40e782e3856ada32de8d8ce0054e35fca2022b8f91b2a1ca2c5e10", + "SUCCESS": "Y", + "TYPE": "1" +} +``` + +#### 7.1.3 字段说明 + +##### REMARK2 字段说明 + +发起支付时(调用商户下单支付接口),把服务方编号通过 `REMARK2` 字段送到支付渠道,让支付渠道再推送结果给建行生活时,建行生活根据该字段来判断订单归属哪个服务方,然后指定推送订单结果给对应的服务方(需要在后台维护服务方系统地址)。 + +##### SIGN 字段说明 + +使用商户私钥对报文进行 RSA 签名,服务方需要使用建行公钥验签。 + +##### CCB_DISCOUNT_AMT 和 CCB_DISCOUNT_AMT_DESC 字段说明 + +- 由建行生活推送给服务方的支付通知会额外携带该笔订单所使用的优惠金额 +- 但是由网银直接推送的支付通知,只含原订单金额,不包含优惠金额,需要服务方自行向外联平台查询 + +--- + +### 7.2 建行生活退款操作通知接口 + +#### 7.2.1 背景描述 + +如服务方场景需要接收退款操作消息通知,必须按本章节要求完成退款回调通知对接。 + +⚠️ **重要提示**: +- **不能用于退款结果判断**:退款操作消息推送仅推送商户号、订单号等少量用于查询使用的信息 +- 若需获知退款状态、金额、优惠信息和支付方式等信息,参考《3.3 A3341TP03 - 服务方订单查询》 +- 如服务方场景仅通过 A3341TP04 服务方退款交易完成退款,已实现退款流程的闭环,可略过本章节 +- 订单退款操作消息推送属于建行生活主动推送给服务方,不会附带服务方编号 +- 服务方接收退款操作消息通知的回调地址,需要配置在建行生活运营后台,请联系运维进行配置 + +#### 7.2.2 接口推送报文示例 + +```json +{ + "MRCH_ID": "1050000000000000000", + "NT_TYPE": "YS", + "ORDERID": "210520133844test3yhv", + "REFUND_DTM": "20250601120000", + "SIGN": "317b7dd349c1fbcabc26a20ba117a778da5a685c588be5e7378682651062a25b0885e36ee237c19a143f7271c9529a0e9bf198c8735709dc74233d96e1a276cec9d4835422bee597100b0a47d11b44dbba74bdf9cbde0587f138141ce79a3536733d5f6b53ed119c13708dca52ee8d3fcf7e67dcdb20053889adff1989a8c859" +} +``` + +#### 7.2.3 字段说明 + +| 字段名 | 中文名 | 类型 | 说明 | +|--------|--------|------|------| +| MRCH_ID | 商户号 | String | 建行商户编号 | +| NT_TYPE | 通知类型 | String | 固定值 "YS" | +| ORDERID | 订单号 | String | 支付流水号 | +| REFUND_DTM | 退款时间 | String | 格式: yyyyMMddHHmmss | +| SIGN | 签名 | String | RSA 签名,需使用建行公钥验签 | + +--- + +**文档结束**