封装ccb

This commit is contained in:
Billy 2025-10-17 17:18:15 +08:00
parent 3af024ae75
commit fd51bd8e8e
13 changed files with 3279 additions and 635 deletions

View File

@ -15,32 +15,30 @@ use think\Env;
return [
// API基础地址 (生产环境)
'api_base_url' => Env::get('ccb.api_base_url', 'https://yunbusiness.ccb.com/tp_service/txCtrl/server'),
'api_base_url' => 'https://life.ccb.com/tran/merchant/channel/api.jhtml',
// 收银台地址 (生产环境)
'cashier_url' => Env::get('ccb.cashier_url', 'https://yunbusiness.ccb.com/clp_service/txCtrl'),
'cashier_url' => 'https://yunbusiness.ccb.com/clp_service/txCtrl',
// 交易代码映射
'tx_codes' => [
'order_push' => 'A3341TP01', // 订单推送
'order_update' => 'A3341TP02', // 订单更新
'order_query' => 'A3341TP03', // 订单查询
'order_refund' => 'A3341TP04', // 订单退款
'order_push' => 'svc_occMebOrderPush', // 订单推送
'order_update' => 'svc_occMebOrderStatusUpdate', // 订单状态更新
'order_query' => 'svc_occPlatOrderQry', // 订单查询
'order_refund' => 'svc_occRefund', // 订单退款
],
// 服务方信息(已确认)
'service_id' => Env::get('ccb.service_id', 'YS44000098000600'),
// 服务方信息(生产环境)
'service_id' => Env::get('ccb.service_id', 'YS44000009001853'),
// 商户信息(已确认)
// 商户信息(从.env读取)
'merchant_id' => Env::get('ccb.merchant_id', '105003953998037'),
'pos_id' => Env::get('ccb.pos_id', '068295530'),
'branch_id' => Env::get('ccb.branch_id', '340650000'),
// ⚠️ 密钥配置 (必须在.env中配置)
// 格式示例:
// ccb.private_key = "-----BEGIN RSA PRIVATE KEY-----\nMIICXAI...\n-----END RSA PRIVATE KEY-----"
'private_key' => Env::get('ccb.private_key', ''),
'public_key' => Env::get('ccb.public_key', ''),
// 密钥配置 (从.env读取BASE64格式不含PEM头尾)
'private_key' => Env::get('ccb.private_key', 'MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALrJmPmtQfP6mURtMxLEXqJHLldN3zYukoaRxG0lw2IdcC86H9C9brFz4YlJ+98z2mdELJaQWu8VWI4actSuPKgHTBr9MSpaii0QQpdINpwXJD9AglIrT7MxhMLYx3qAYDhjKUlC5hnWVYOg4sG32k/3dCebRHY8RDlrXUfHB2+VAgMBAAECgYArgn5R2pv8WymMmOtGudtZbb9LsuYF1v9mvVnGGv/SQQ060w1KMHYye83TjxpOueNsHqNMR0AHZS+Fmn+ZLyUNj9S77oQvUx5HQvY2/TDnsKbETzEMDybIWB+XdLsUkOrB3peVLTbk25i6oSNPOT2Fvd8TWbDqzBL9Ci27uJH72QJBAP/DfDLYoYx9OIRCykkxrDdQVFEkzhXj0wIkLa0Wnf8kP/JfBqvr0AGUPF8nEfh7fLVXYQlh5ab2FL5KvUifSL8CQQC69crW0fryyDHePp6OIVRUbw0T93h52vbGXnoQ6wdvKxZeL3MsfdNUvsJYeSxmtyY+LLgz1p3qOoEn6UpLvCirAkEA4N7qUvY+y3vJdhgXLNV8mkGJcLKQc5SUkJxogHeTQKGJi7ra7ctuXgUMM4jxduxz0CjcS1iEhxBzWn/x/mj1lwJBALgtv39VKLTXx1i7s5Ms/liXdfi/iC3zKbxOAk58WryHY+exMvMXmYMY0Xg7FySxNLl3cJeQy8ydifL5fbmSSTUCQQCj/YUbcTP8BQ6N0AgFdBwmXJyiNkB9zaDI5cEtpSCgq72m8lfn883GJ1MT7nKVXeX69/q5yDQUYiYPBXH4lCEC'),
'public_key' => Env::get('ccb.public_key', 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC6yZj5rUHz+plEbTMSxF6iRy5XTd82LpKGkcRtJcNiHXAvOh/QvW6xc+GJSfvfM9pnRCyWkFrvFViOGnLUrjyoB0wa/TEqWootEEKXSDacFyQ/QIJSK0+zMYTC2Md6gGA4YylJQuYZ1lWDoOLBt9pP93Qnm0R2PEQ5a11HxwdvlQIDAQAB'),
// HTTP请求配置
'http' => [

View File

@ -3,19 +3,14 @@
namespace addons\shopro\controller;
use addons\shopro\controller\Common;
use app\admin\model\User as UserModel;
use think\Exception;
use addons\shopro\library\ccblife\CcbUrlDecrypt;
use app\admin\model\shopro\user\User;
use think\Db;
use think\Log;
/**
* 建行生活用户登录控制器
*
* 功能:
* - 建行用户自动登录 (替代原有登录注册)
* - 用户信息同步
*
* @author Billy
* @date 2025-01-16
* 处理建行用户登录、绑定、参数解密等功能
*/
class Ccblife extends Common
{
@ -23,7 +18,7 @@ class Ccblife extends Common
* 不需要登录的方法
* @var array
*/
protected $noNeedLogin = ['autoLogin'];
protected $noNeedLogin = ['autoLogin', 'login', 'callback'];
/**
* 不需要权限的方法
@ -32,105 +27,199 @@ class Ccblife extends Common
protected $noNeedRight = ['*'];
/**
* 建行用户自动登录
* 建行生活用户登录URL跳转方式
* 建行App会携带加密参数跳转到此地址
*
* 说明:
* 1. 弃用商城原有的登录注册功能
* 2. H5在建行App内打开时,自动通过JSBridge获取建行用户信息
* 3. 如果建行用户ID已存在则登录,不存在则自动创建商城用户并绑定
* 4. 返回商城Token用于后续API调用
* GET /addons/shopro/ccblife/login
*/
public function login()
{
try {
// 获取URL参数
$ccbParamSJ = $this->request->get('ccbParamSJ', '');
$otherParams = $this->request->get();
// 验证必要参数
if (empty($ccbParamSJ)) {
$this->error('缺少必要参数');
}
// 获取配置
$config = config('ccblife');
// 解密参数
$decryptedParams = CcbUrlDecrypt::decrypt($ccbParamSJ, $config['service_id']);
if (!$decryptedParams) {
$this->error('参数解密失败');
}
// 合并所有参数
$allParams = array_merge($otherParams, $decryptedParams);
// 获取建行用户信息
$ccbUserId = $allParams['userid'] ?? '';
$mobile = $allParams['mobile'] ?? '';
$openId = $allParams['openid'] ?? '';
if (empty($ccbUserId)) {
$this->error('用户信息获取失败');
}
// 处理用户登录/注册
$userInfo = $this->processUserLogin($ccbUserId, $mobile, $openId, $allParams);
// 生成商城Token
$this->auth->direct($userInfo['user_id']);
$token = $this->auth->getToken();
// 构建跳转URL
$redirectUrl = $allParams['redirect_url'] ?? '/pages/index/index';
$this->success('登录成功', [
'token' => $token,
'user_info' => $userInfo,
'redirect_url' => $redirectUrl
]);
} catch (\Exception $e) {
Log::error('建行生活登录失败: ' . $e->getMessage());
$this->error($e->getMessage());
}
}
/**
* 建行用户自动登录JSBridge方式
* H5在建行App内打开时,通过JSBridge获取用户信息后调用
*
* @return void
* POST /addons/shopro/ccblife/autoLogin
*/
public function autoLogin()
{
try {
// 1. 获取请求参数
// 获取请求参数
$ccbUserId = $this->request->post('ccb_user_id', '');
$ccbParamSJ = $this->request->post('ccb_param_sj', '');
$mobile = $this->request->post('mobile', '');
$nickname = $this->request->post('nickname', '');
$avatar = $this->request->post('avatar', '');
// 2. 验证必需参数
// 验证必需参数
if (empty($ccbUserId)) {
$this->error('建行用户ID不能为空');
}
// 3. 查询用户是否已存在
$user = UserModel::where('ccb_user_id', $ccbUserId)->find();
if ($user) {
// 用户已存在 - 直接登录
$isNewUser = false;
// 更新最后登录时间
$user->logintime = time();
$user->save();
Log::info('[建行登录] 用户登录 ' . json_encode([
'ccb_user_id' => $ccbUserId,
'user_id' => $user->id,
'is_new' => false,
], JSON_UNESCAPED_UNICODE));
} else {
// 用户不存在 - 创建新用户
$isNewUser = true;
$user = new UserModel();
$user->ccb_user_id = $ccbUserId;
$user->username = 'user_ccb_' . $ccbUserId; // 用户名: user_ccb_xxx
$user->nickname = $nickname ?: '建行用户_' . substr($ccbUserId, -4);
$user->mobile = $mobile;
$user->avatar = $avatar;
$user->salt = \fast\Random::alnum(16);
$user->password = md5(md5(\fast\Random::alnum(32)) . $user->salt); // 随机密码
$user->status = 'normal';
$user->joinip = $this->request->ip();
$user->jointime = time();
$user->logintime = time();
$user->loginip = $this->request->ip();
$user->prevtime = time();
$user->save();
Log::info('[建行登录] 新用户创建 ' . json_encode([
'ccb_user_id' => $ccbUserId,
'user_id' => $user->id,
'username' => $user->username,
], JSON_UNESCAPED_UNICODE));
}
// 4. 使用Auth系统登录并生成Token
$this->auth->direct($user->id);
$token = $this->auth->getToken();
// 5. 返回结果
$this->success('登录成功', [
'token' => $token,
'user_id' => $user->id,
'is_new_user' => $isNewUser,
'userInfo' => [
'id' => $user->id,
'username' => $user->username,
'nickname' => $user->nickname,
'mobile' => $this->maskMobile($user->mobile),
'avatar' => $user->avatar,
'ccb_user_id' => $user->ccb_user_id,
'create_time' => date('Y-m-d H:i:s', $user->jointime),
],
// 处理用户登录/注册
$userInfo = $this->processUserLogin($ccbUserId, $mobile, '', [
'nickname' => $nickname,
'avatar' => $avatar
]);
} catch (Exception $e) {
Log::error('[建行登录] 登录失败 ' . json_encode([
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString(),
], JSON_UNESCAPED_UNICODE));
// 使用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 (\Exception $e) {
Log::error('建行自动登录失败: ' . $e->getMessage());
$this->error('登录失败: ' . $e->getMessage());
}
}
/**
* 处理用户登录/注册
*
* @param string $ccbUserId 建行用户ID
* @param string $mobile 手机号
* @param string $openId OpenID
* @param array $params 其他参数
* @return array 用户信息
*/
private function processUserLogin($ccbUserId, $mobile, $openId, $params)
{
Db::startTrans();
try {
// 查询是否已存在建行用户
$user = Db::name('user')->where('ccb_user_id', $ccbUserId)->find();
if ($user) {
// 用户已存在,更新登录信息
$isNew = false;
Db::name('user')->where('id', $user['id'])->update([
'logintime' => time(),
'loginip' => $this->request->ip(),
'updatetime' => time()
]);
} else {
// 用户不存在,先尝试通过手机号查找
if ($mobile) {
$user = Db::name('user')->where('mobile', $mobile)->find();
}
if ($user) {
// 手机号已存在更新建行用户ID
$isNew = false;
Db::name('user')->where('id', $user['id'])->update([
'ccb_user_id' => $ccbUserId,
'logintime' => time(),
'loginip' => $this->request->ip(),
'updatetime' => time()
]);
} else {
// 创建新用户
$isNew = true;
$userData = [
'ccb_user_id' => $ccbUserId,
'username' => 'ccb_' . substr(md5($ccbUserId), 0, 8),
'nickname' => $params['nickname'] ?? '建行用户' . substr($ccbUserId, -4),
'mobile' => $mobile,
'avatar' => $params['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();
}
}
Db::commit();
return [
'user_id' => $user['id'],
'nickname' => $user['nickname'],
'avatar' => $user['avatar'],
'mobile' => $this->maskMobile($user['mobile']),
'is_new' => $isNew,
'ccb_user_id' => $ccbUserId
];
} catch (\Exception $e) {
Db::rollback();
throw $e;
}
}
/**
* 手机号脱敏
*

View File

@ -88,26 +88,28 @@ class Ccbpayment extends Common
}
// 4. 生成支付串
$result = $this->paymentService->generatePaymentString($order->toArray());
$result = $this->paymentService->generatePaymentString($orderId);
if (!$result['success']) {
$this->error('支付串生成失败: ' . $result['error']);
if (!$result['status']) {
$this->error('支付串生成失败: ' . $result['message']);
}
// 5. 保存支付流水号到订单
$order->ccb_pay_flow_id = $result['pay_flow_id'];
$order->ccb_pay_flow_id = $result['data']['order_sn']; // 使用订单号作为流水号
$order->save();
// 6. 记录支付日志
$this->savePaymentLog($order, $result['payment_string'], $result['pay_flow_id']);
$this->savePaymentLog($order, $result['data']['payment_string'], $result['data']['order_sn']);
// 7. 返回支付串
$this->success('支付串生成成功', [
'payment_string' => $result['payment_string'],
'payment_string' => $result['data']['payment_string'],
'payment_url' => $result['data']['payment_url'],
'mac' => $result['data']['mac'],
'order_id' => $order->id,
'order_sn' => $order->order_sn,
'pay_flow_id' => $result['pay_flow_id'],
'amount' => $order->pay_amount,
'pay_flow_id' => $result['data']['order_sn'],
'amount' => $result['data']['amount'],
]);
} catch (Exception $e) {
@ -236,26 +238,48 @@ class Ccbpayment extends Common
*/
private function pushOrderToCcb($order)
{
// 获取订单商品列表
$orderItems = Db::name('shopro_order_item')
->where('order_id', $order->id)
->field('goods_id, goods_sku_text, goods_title, goods_price, goods_num, discount_fee')
->select();
$goodsList = [];
foreach ($orderItems as $item) {
$goodsList[] = [
'goods_id' => $item['goods_id'],
'goods_name' => $item['goods_title'],
'goods_sku' => $item['goods_sku_text'],
'goods_price' => $item['goods_price'],
'goods_num' => $item['goods_num'],
'discount_amount' => $item['discount_fee'] ?? 0
];
}
// 获取用户的建行用户ID
$user = Db::name('user')->where('id', $order->user_id)->field('ccb_user_id')->find();
// 构造订单数据 (使用Shopro实际字段名)
$orderData = [
'id' => $order->id,
'order_sn' => $order->order_sn,
'ccb_user_id' => $order->ccb_user_id,
'total_amount' => $order->order_amount, // Shopro字段名
'pay_amount' => $order->pay_fee, // Shopro字段名
'discount_amount' => $order->total_discount_fee, // Shopro字段名
'status' => $order->status, // Shopro使用status枚举
'refund_status' => 0,
'create_time' => intval($order->createtime / 1000), // Shopro使用毫秒,转秒
'ccb_user_id' => $user['ccb_user_id'] ?? '',
'total_amount' => $order->total_amount, // 订单总金额
'pay_amount' => $order->total_fee, // 实际支付金额
'discount_amount' => $order->discount_fee, // 优惠金额
'status' => $order->status, // Shopro使用status枚举
'refund_status' => $order->aftersale_status ?? 0, // 售后状态
'create_time' => $order->createtime, // Shopro使用秒级时间戳
'paid_time' => $order->paid_time, // 支付时间
'ccb_pay_flow_id' => $order->ccb_pay_flow_id,
'goods_list' => [], // TODO: 从订单详情中获取商品列表
'goods_list' => $goodsList,
];
// 推送到建行
$result = $this->orderService->pushOrder($orderData);
$result = $this->orderService->pushOrder($order->id);
if (!$result['success']) {
Log::warning('[建行推送] 订单推送失败 order_id:' . $order->id . ' error:' . ($result['error'] ?? ''));
if (!$result['status']) {
Log::warning('[建行推送] 订单推送失败 order_id:' . $order->id . ' error:' . ($result['message'] ?? ''));
} else {
// 更新同步状态
$order->ccb_sync_status = 1;

View File

@ -0,0 +1,527 @@
<?php
namespace addons\shopro\controller;
use addons\shopro\controller\Common;
use addons\shopro\library\ccblife\CcbRSA;
use addons\shopro\library\ccblife\CcbMD5;
use addons\shopro\library\ccblife\CcbUrlDecrypt;
use addons\shopro\library\ccblife\CcbHttpClient;
use addons\shopro\library\ccblife\CcbOrderService;
use addons\shopro\library\ccblife\CcbPaymentService;
use app\admin\model\shopro\order\Order as OrderModel;
use think\Db;
use think\Exception;
/**
* 建行生活测试控制器
*
* 用于测试和验证各个功能模块是否正常工作
*
* @author Billy
* @date 2025-01-17
*/
class Ccbtest extends Common
{
/**
* 不需要登录的方法
* @var array
*/
protected $noNeedLogin = ['*'];
/**
* 不需要权限的方法
* @var array
*/
protected $noNeedRight = ['*'];
/**
* 测试主页
* 展示所有可用的测试接口
*/
public function index()
{
$baseUrl = $this->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'],
'DES密钥前8位' => substr($config['service_id'], 0, 8),
'模拟数据' => $testParams
];
} else {
// 尝试解密
$decryptedParams = CcbUrlDecrypt::decrypt($ccbParamSJ, $config['service_id']);
$result = [
'解密结果' => $decryptedParams ? '成功' : '失败',
'原始参数' => $ccbParamSJ,
'解密数据' => $decryptedParams,
'服务方编号' => $config['service_id']
];
}
$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 = [
'基础配置' => [
'API地址' => $config['api_base_url'] ?? '未配置',
'收银台地址' => $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 '✗ 格式异常';
}
}

View File

@ -2,286 +2,331 @@
namespace addons\shopro\library\ccblife;
use think\Exception;
use think\Log;
/**
* 建行生活HTTP客户端类
*
* 功能:
* - 发送HTTP请求到建行
* - 处理HTTP响应
* - 失败重试机制
* - 超时控制
*
* @author Billy
* @date 2025-01-16
* 建行生活HTTP客户端
* 处理与建行API的通信包括加密、签名、发送请求和解密响应
*/
class CcbHttpClient
{
/**
* 配置信息
* @var array
* 建行API生产环境地址
*/
const API_URL = 'https://life.ccb.com/tran/merchant/channel/api.jhtml';
/**
* 默认超时时间(秒)
*/
const DEFAULT_TIMEOUT = 30;
/**
* 商户配置
*/
private $config;
/**
* 加密实例
* @var CcbEncryption
*/
private $encryption;
/**
* 构造函数
*
* @param array $config 配置数组
* @param array $config 配置数组包含merchant_id, pos_id, branch_id, private_key, public_key, service_id
*/
public function __construct($config = [])
public function __construct($config)
{
if (empty($config)) {
$config = config('ccblife');
}
$this->config = $config;
$this->encryption = new CcbEncryption($config);
$this->validateConfig();
}
/**
* 发送请求到建行
* 发送API请求
*
* @param string $txCode 交易代码 (: A3341TP01)
* @param array $bodyData 业务数据
* @return array 解密后的响应数据
* @throws Exception
* @param string $txCode 交易码如svc_occMebOrderPush
* @param array $body 请求体数据
* @param string $txSeq 交易流水号,不传则自动生成
* @return array 响应数据
* @throws \Exception
*/
public function request($txCode, $bodyData)
public function request($txCode, $body, $txSeq = null)
{
// 记录开始时间
$startTime = microtime(true);
try {
// 1. 获取接口地址
$url = $this->getApiUrl($txCode);
// 2. 构建加密报文
$requestData = $this->encryption->buildEncryptedMessage($txCode, $bodyData);
// 3. 记录请求日志
$this->logRequest($txCode, $bodyData, $requestData);
// 4. 发送HTTP请求 (带重试机制)
$response = $this->retry(function () use ($url, $requestData) {
return $this->post($url, $requestData);
});
// 5. 解析响应
$result = $this->encryption->parseResponse($response);
// 6. 记录响应日志
$costTime = round((microtime(true) - $startTime) * 1000, 2);
$this->logResponse($txCode, $result, $costTime);
// 7. 检查业务返回码
$this->checkReturnCode($result);
return $result;
} catch (Exception $e) {
// 记录错误日志
$costTime = round((microtime(true) - $startTime) * 1000, 2);
$this->logError($txCode, $e->getMessage(), $costTime);
throw $e;
// 生成交易流水号
if (empty($txSeq)) {
$txSeq = CcbMD5::generateTransactionSeq();
}
// 构建请求报文
$message = $this->buildMessage($txCode, $body, $txSeq);
// 加密报文
$encryptedMessage = CcbRSA::encryptForCcb($message, $this->config['public_key']);
// 移除BASE64中的换行符
$encryptedMessage = str_replace(["\r", "\n", "\r\n"], '', $encryptedMessage);
// 生成签名
$mac = CcbMD5::signApiMessage($message, $this->config['private_key']);
// 发送HTTP请求
$response = $this->sendHttpRequest($encryptedMessage, $mac);
// 处理响应
return $this->handleResponse($response);
}
/**
* 发送POST请求
* 构建请求报文
*
* @param string $url 请求URL
* @param array $data 请求数据
* @return string 响应内容
* @throws Exception
* @param string $txCode 交易码
* @param array $body 请求体
* @param string $txSeq 交易流水号
* @return string JSON格式的报文
*/
private function post($url, $data)
private function buildMessage($txCode, $body, $txSeq)
{
$message = [
'CLD_HEADER' => [
'CLD_TX_CHNL' => $this->config['service_id'],
'CLD_TX_TIME' => date('YmdHis'),
'CLD_TX_CODE' => $txCode,
'CLD_TX_SEQ' => $txSeq
],
'CLD_BODY' => $body
];
// 转换为JSON不转义中文
return json_encode($message, JSON_UNESCAPED_UNICODE);
}
/**
* 发送HTTP请求
*
* @param string $cnt 加密后的报文内容
* @param string $mac 签名
* @return string 响应内容
* @throws \Exception
*/
private function sendHttpRequest($cnt, $mac)
{
// 构建请求参数
$params = [
'cnt' => $cnt,
'mac' => $mac
];
// 初始化CURL
$ch = curl_init();
// 设置CURL选项
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data));
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, $this->config['http']['timeout'] ?? 30);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
'Accept: application/json',
'Content-Type: application/json',
curl_setopt_array($ch, [
CURLOPT_URL => self::API_URL,
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => http_build_query($params),
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => self::DEFAULT_TIMEOUT,
CURLOPT_SSL_VERIFYPEER => true,
CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_HTTPHEADER => [
'Content-Type: application/x-www-form-urlencoded',
'Accept: application/json'
]
]);
// 如果是HTTPS,验证证书
if (strpos($url, 'https') === 0) {
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
}
// 执行请求
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
// 检查CURL错误
// 检查错误
if ($error) {
throw new Exception('CURL错误: ' . $error);
}
// 检查HTTP状态码
if ($httpCode == 404) {
throw new Exception('接口404,请检查请求头是否包含Accept和Content-Type');
throw new \Exception('HTTP请求失败: ' . $error);
}
if ($httpCode !== 200) {
throw new Exception('HTTP错误码: ' . $httpCode . ', 响应: ' . $response);
throw new \Exception('HTTP状态码异常: ' . $httpCode . ', 响应内容: ' . $response);
}
return $response;
}
/**
* 重试机制
* 处理响应
*
* @param callable $callable 要执行的函数
* @param int|null $maxRetries 最大重试次数
* @return mixed 执行结果
* @throws Exception
* @param string $response 原始响应内容
* @return array 解密后的响应数据
* @throws \Exception
*/
private function retry($callable, $maxRetries = null)
private function handleResponse($response)
{
if ($maxRetries === null) {
$maxRetries = $this->config['http']['retry_times'] ?? 3;
// 解析JSON响应
$responseData = json_decode($response, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('响应JSON解析失败: ' . json_last_error_msg());
}
$delays = $this->config['http']['retry_delay'] ?? [1, 2, 5];
$lastException = null;
// 检查响应结构
if (!isset($responseData['cnt']) || !isset($responseData['mac'])) {
throw new \Exception('响应格式错误缺少cnt或mac字段');
}
for ($i = 0; $i <= $maxRetries; $i++) {
try {
return $callable();
} catch (Exception $e) {
$lastException = $e;
// 解密响应内容
$decryptedContent = CcbRSA::decryptFromCcb($responseData['cnt'], $this->config['private_key']);
// 如果是最后一次尝试,不再重试
if ($i >= $maxRetries) {
break;
}
// 验证签名
$isValid = CcbMD5::verifyApiSignature($decryptedContent, $responseData['mac'], $this->config['private_key']);
if (!$isValid) {
throw new \Exception('响应签名验证失败');
}
// 等待后重试
$delay = $delays[$i] ?? 5;
$this->logRetry($i + 1, $maxRetries, $delay, $e->getMessage());
sleep($delay);
// 解析解密后的内容
$decryptedData = json_decode($decryptedContent, true);
if (json_last_error() !== JSON_ERROR_NONE) {
throw new \Exception('解密后的JSON解析失败: ' . json_last_error_msg());
}
// 检查业务响应码
$this->checkBusinessResponse($decryptedData);
return $decryptedData;
}
/**
* 检查业务响应码
*
* @param array $data 响应数据
* @throws \Exception
*/
private function checkBusinessResponse($data)
{
// 检查响应头
if (!isset($data['CLD_HEADER']) || !isset($data['CLD_BODY'])) {
throw new \Exception('响应数据结构错误');
}
// 检查响应码(如果存在)
if (isset($data['CLD_BODY']['CLD_RESP_CODE'])) {
$respCode = $data['CLD_BODY']['CLD_RESP_CODE'];
$respMsg = isset($data['CLD_BODY']['CLD_RESP_MSG']) ? $data['CLD_BODY']['CLD_RESP_MSG'] : '';
if ($respCode !== '000000') {
throw new \Exception('业务处理失败[' . $respCode . ']: ' . $respMsg);
}
}
throw new Exception('请求失败,已重试' . $maxRetries . '次: ' . $lastException->getMessage());
}
/**
* 获取API地址
* 验证配置
*
* @param string $txCode 交易代码
* @return string 完整API地址
* @throws \Exception
*/
private function getApiUrl($txCode)
private function validateConfig()
{
$baseUrl = $this->config['api_base_url'] ?? '';
$requiredFields = [
'merchant_id',
'pos_id',
'branch_id',
'private_key',
'public_key',
'service_id'
];
if (empty($baseUrl)) {
throw new Exception('API基础地址未配置');
}
return $baseUrl . '?txcode=' . $txCode;
}
/**
* 检查业务返回码
*
* @param array $result 响应数据
* @throws Exception
*/
private function checkReturnCode($result)
{
// 检查CLD_HEADER中的RET_CODE
$retCode = $result['CLD_HEADER']['RET_CODE'] ?? '';
$retMsg = $result['CLD_HEADER']['RET_MSG'] ?? '未知错误';
if ($retCode !== '000000') {
throw new Exception('建行接口返回错误[' . $retCode . ']: ' . $retMsg);
foreach ($requiredFields as $field) {
if (!isset($this->config[$field]) || empty($this->config[$field])) {
throw new \Exception('配置缺少必要字段: ' . $field);
}
}
}
/**
* 记录请求日志
* 订单推送A3341TP01
*
* @param string $txCode 交易代码
* @param array $bodyData 业务数据
* @param array $requestData 加密后的请求数据
* @param array $orderData 订单数据
* @return array 响应数据
* @throws \Exception
*/
private function logRequest($txCode, $bodyData, $requestData)
public function pushOrder($orderData)
{
if (!($this->config['log']['enabled'] ?? true)) {
return;
}
Log::info('[建行请求] ' . $txCode . ' svcid:' . $requestData['svcid'] . ' mac:' . $requestData['mac'] . ' cnt_length:' . strlen($requestData['cnt']) . ' body_data:' . json_encode($bodyData, JSON_UNESCAPED_UNICODE));
return $this->request('svc_occMebOrderPush', $orderData);
}
/**
* 记录响应日志
* 订单状态更新A3341TP02
*
* @param string $txCode 交易代码
* @param array $result 响应数据
* @param float $costTime 耗时(毫秒)
* @param string $userId 用户ID
* @param string $orderId 订单ID
* @param string $orderStatus 订单状态
* @param string $refundStatus 退款状态
* @return array 响应数据
* @throws \Exception
*/
private function logResponse($txCode, $result, $costTime)
public function updateOrderStatus($userId, $orderId, $orderStatus, $refundStatus = '0')
{
if (!($this->config['log']['enabled'] ?? true)) {
return;
}
$body = [
'USER_ID' => $userId,
'ORDER_ID' => $orderId,
'ORDER_STATUS' => $orderStatus,
'REFUND_STATUS' => $refundStatus
];
Log::info('[建行响应] ' . $txCode . ' ret_code:' . ($result['CLD_HEADER']['RET_CODE'] ?? '') . ' ret_msg:' . ($result['CLD_HEADER']['RET_MSG'] ?? '') . ' cost_time:' . $costTime . 'ms');
return $this->request('svc_occMebOrderStatusUpdate', $body);
}
/**
* 记录错误日志
* 订单查询A3341TP03
*
* @param string $txCode 交易代码
* @param string $errorMsg 错误信息
* @param float $costTime 耗时(毫秒)
* @param string $onlnPyTxnOrdrId 支付订单ID
* @param string $txnStatus 交易状态
* @return array 响应数据
* @throws \Exception
*/
private function logError($txCode, $errorMsg, $costTime)
public function queryOrder($onlnPyTxnOrdrId, $txnStatus = '00')
{
if (!($this->config['log']['enabled'] ?? true)) {
return;
}
$body = [
'ONLN_PY_TXN_ORDR_ID' => $onlnPyTxnOrdrId,
'PAGE' => '1',
'TXN_PRD_TPCD' => '06',
'TXN_STATUS' => $txnStatus,
'TX_TYPE' => '0'
];
Log::error('[建行错误] ' . $txCode . ' error:' . $errorMsg . ' cost_time:' . $costTime . 'ms');
return $this->request('svc_occPlatOrderQry', $body);
}
/**
* 记录重试日志
* 退款接口A3341TP04
*
* @param int $currentRetry 当前重试次数
* @param int $maxRetries 最大重试次数
* @param int $delay 延迟秒数
* @param string $reason 重试原因
* @param string $orderId 订单ID
* @param string $refundAmount 退款金额
* @param string $refundReason 退款原因
* @return array 响应数据
* @throws \Exception
*/
private function logRetry($currentRetry, $maxRetries, $delay, $reason)
public function refund($orderId, $refundAmount, $refundReason = '')
{
if (!($this->config['log']['enabled'] ?? true)) {
return;
}
$body = [
'ORDER_ID' => $orderId,
'REFUND_AMOUNT' => $refundAmount,
'REFUND_REASON' => $refundReason,
'REFUND_TIME' => date('YmdHis')
];
Log::warning('[建行重试] retry:' . $currentRetry . '/' . $maxRetries . ' delay:' . $delay . 's reason:' . $reason);
return $this->request('svc_occRefund', $body);
}
/**
* 测试连接
* 使用查询接口测试连接是否正常
*
* @return bool 是否连接成功
*/
public function testConnection()
{
try {
// 使用一个不存在的订单号进行查询测试
$this->queryOrder('TEST' . time());
return true;
} catch (\Exception $e) {
// 如果是业务错误(订单不存在),说明连接正常
if (strpos($e->getMessage(), '业务处理失败') !== false) {
return true;
}
return false;
}
}
}

View File

@ -0,0 +1,168 @@
<?php
namespace addons\shopro\library\ccblife;
/**
* 建行生活MD5签名类
* 处理API消息签名和支付字符串签名
*/
class CcbMD5
{
/**
* 生成API消息签名
* 使用大写MD5(源报文 + 私钥)
*
* @param string $message JSON格式的源报文
* @param string $privateKey 商户私钥BASE64格式
* @return string 大写的32位MD5签名
*/
public static function signApiMessage($message, $privateKey)
{
// 移除私钥中的空格和换行
$privateKey = preg_replace('/\s+/', '', $privateKey);
// 计算MD5并转大写
return strtoupper(md5($message . $privateKey));
}
/**
* 验证API消息签名
*
* @param string $message JSON格式的源报文
* @param string $signature 待验证的签名
* @param string $privateKey 商户私钥BASE64格式
* @return bool 签名是否有效
*/
public static function verifyApiSignature($message, $signature, $privateKey)
{
$expectedSignature = self::signApiMessage($message, $privateKey);
return $expectedSignature === strtoupper($signature);
}
/**
* 生成支付字符串签名
* 使用小写MD5(支付字符串)
* 支付字符串格式: MERCHANTID=xxx&POSID=xxx&BRANCHID=xxx&ORDERID=xxx&PAYMENT=xxx&CURCODE=01&TXCODE=530550&REMARK1=&REMARK2=&TIMEOUT=商户私钥
*
* @param array $params 支付参数
* @param string $privateKey 商户私钥
* @return string 小写的32位MD5签名
*/
public static function signPaymentString($params, $privateKey)
{
// 构建支付字符串(按照建行要求的顺序)
$fields = [
'MERCHANTID',
'POSID',
'BRANCHID',
'ORDERID',
'PAYMENT',
'CURCODE',
'TXCODE',
'REMARK1',
'REMARK2',
'TIMEOUT'
];
$parts = [];
foreach ($fields as $field) {
$value = isset($params[$field]) ? $params[$field] : '';
$parts[] = $field . '=' . $value;
}
// 拼接支付字符串并添加私钥
$paymentString = implode('&', $parts) . $privateKey;
// 计算MD5保持小写
return md5($paymentString);
}
/**
* 根据完整技术方案生成标准支付字符串签名
* 按照文档要求的格式生成签名
*
* @param string $merchantId 商户号
* @param string $posId POS号
* @param string $branchId 分行号
* @param string $orderId 订单号
* @param string $payment 支付金额
* @param string $privateKey 商户私钥
* @param string $curCode 币种默认01人民币
* @param string $txCode 交易码默认530550
* @param string $timeout 超时时间,默认空
* @return array 包含支付字符串和签名
*/
public static function generatePaymentSignature($merchantId, $posId, $branchId, $orderId, $payment, $privateKey, $curCode = '01', $txCode = '530550', $timeout = '')
{
// 构建支付参数
$params = [
'MERCHANTID' => $merchantId,
'POSID' => $posId,
'BRANCHID' => $branchId,
'ORDERID' => $orderId,
'PAYMENT' => $payment,
'CURCODE' => $curCode,
'TXCODE' => $txCode,
'REMARK1' => '',
'REMARK2' => '',
'TIMEOUT' => $timeout
];
// 生成签名
$mac = self::signPaymentString($params, $privateKey);
// 构建完整的支付字符串(不包含私钥)
$fields = [];
foreach ($params as $key => $value) {
$fields[] = $key . '=' . $value;
}
$paymentString = implode('&', $fields);
return [
'payment_string' => $paymentString,
'mac' => $mac,
'params' => $params
];
}
/**
* 验证支付字符串签名
*
* @param array $params 支付参数
* @param string $mac 待验证的MAC值
* @param string $privateKey 商户私钥
* @return bool 签名是否有效
*/
public static function verifyPaymentSignature($params, $mac, $privateKey)
{
$expectedMac = self::signPaymentString($params, $privateKey);
return $expectedMac === strtolower($mac);
}
/**
* 生成交易流水号
* 格式YYYYMMDDHHMMSS + 6位随机数
*
* @return string 20位交易流水号
*/
public static function generateTransactionSeq()
{
$timestamp = date('YmdHis');
$random = str_pad(mt_rand(0, 999999), 6, '0', STR_PAD_LEFT);
return $timestamp . $random;
}
/**
* 生成订单号
* 格式:前缀 + YYYYMMDDHHMMSS + 4位随机数
*
* @param string $prefix 订单号前缀,默认'CCB'
* @return string 订单号
*/
public static function generateOrderId($prefix = 'CCB')
{
$timestamp = date('YmdHis');
$random = str_pad(mt_rand(0, 9999), 4, '0', STR_PAD_LEFT);
return $prefix . $timestamp . $random;
}
}

View File

@ -2,252 +2,538 @@
namespace addons\shopro\library\ccblife;
use think\Exception;
use think\Db;
use think\Log;
/**
* 建行订单服务类
*
* 功能:
* - 订单推送到建行 (A3341TP01)
* - 订单状态更新 (A3341TP02)
* - 订单查询 (A3341TP03)
* - 订单退款 (A3341TP04)
*
* @author Billy
* @date 2025-01-16
* 建行生活订单服务类
* 处理订单同步、状态更新、查询等业务逻辑
*/
class CcbOrderService
{
/**
* HTTP客户端
* @var CcbHttpClient
* HTTP客户端实例
*/
private $httpClient;
/**
* 配置信息
* @var array
*/
private $config;
/**
* 构造函数
*
* @param array $config 配置数组
*/
public function __construct($config = [])
public function __construct()
{
if (empty($config)) {
$config = config('ccblife');
}
$this->config = $config;
$this->httpClient = new CcbHttpClient($config);
$this->config = config('ccblife');
$this->httpClient = new CcbHttpClient($this->config);
}
/**
* 推送订单到建行 (A3341TP01)
* 推送订单到建行生活平台
* 当用户下单后调用此方法同步订单信息
*
* @param array $orderData 订单数据
* @return array 返回结果 ['success' => bool, 'data' => array, 'error' => string]
* @param int $orderId Shopro订单ID
* @return array ['status' => bool, 'message' => string, 'data' => array]
* @throws \Exception
*/
public function pushOrder($orderData)
public function pushOrder($orderId)
{
$startTime = microtime(true);
try {
// 获取订单信息
$order = Db::name('shopro_order')
->alias('o')
->join('user u', 'o.user_id = u.id', 'LEFT')
->where('o.id', $orderId)
->field('o.*, u.ccb_user_id')
->find();
if (!$order) {
throw new \Exception('订单不存在');
}
// 获取建行用户ID
$ccbUserId = $order['ccb_user_id'];
if (!$ccbUserId) {
throw new \Exception('用户未绑定建行生活账号');
}
// 获取订单商品列表
$orderItems = Db::name('shopro_order_item')
->where('order_id', $orderId)
->select();
// 构建订单数据符合A3341TP01接口规范
$orderData = $this->buildOrderData($order, $orderItems, $ccbUserId);
// 记录请求数据(同步日志)
$txSeq = CcbMD5::generateTransactionSeq();
$this->recordSyncLog($orderId, 'A3341TP01', $txSeq, $orderData, 'request');
// 调用建行API推送订单
$response = $this->httpClient->pushOrder($orderData);
// 记录响应数据和耗时
$costTime = round((microtime(true) - $startTime) * 1000, 2);
$this->recordSyncLog($orderId, 'A3341TP01', $txSeq, $response, 'response', true, $costTime);
// 更新订单同步状态
$this->updateOrderSyncStatus($orderId, 1);
return [
'status' => true,
'message' => '订单推送成功',
'data' => $response
];
} catch (\Exception $e) {
// 记录错误
$costTime = round((microtime(true) - $startTime) * 1000, 2);
$this->recordSyncLog($orderId, 'A3341TP01', '', null, 'error', false, $costTime, $e->getMessage());
// 更新同步状态为失败
$this->updateOrderSyncStatus($orderId, 2);
Log::error('建行订单推送失败: ' . $e->getMessage());
return [
'status' => false,
'message' => $e->getMessage(),
'data' => null
];
}
}
/**
* 更新订单状态到建行生活
*
* @param int $orderId 订单ID
* @param string $status 订单状态
* @param string $refundStatus 退款状态
* @return array
*/
public function updateOrderStatus($orderId, $status = null, $refundStatus = null)
{
$startTime = microtime(true);
$txSeq = CcbMD5::generateTransactionSeq();
try {
// 获取订单信息
$order = Db::name('shopro_order')
->alias('o')
->join('user u', 'o.user_id = u.id', 'LEFT')
->where('o.id', $orderId)
->field('o.*, u.ccb_user_id')
->find();
if (!$order) {
throw new \Exception('订单不存在');
}
// 获取建行用户ID
$ccbUserId = $order['ccb_user_id'];
if (!$ccbUserId) {
throw new \Exception('用户未绑定建行生活账号');
}
// 映射订单状态
$orderStatus = $status ?: $this->mapOrderStatus($order['status']);
$refundStatus = $refundStatus ?: $this->mapRefundStatus($order['refund_status'] ?? 0);
// 记录请求
$requestData = [
'ccb_user_id' => $ccbUserId,
'order_sn' => $order['order_sn'],
'order_status' => $orderStatus,
'refund_status' => $refundStatus
];
$this->recordSyncLog($orderId, 'A3341TP02', $txSeq, $requestData, 'request');
// 调用建行API更新状态
$response = $this->httpClient->updateOrderStatus(
$ccbUserId,
$order['order_sn'],
$orderStatus,
$refundStatus
);
// 记录响应
$costTime = round((microtime(true) - $startTime) * 1000, 2);
$this->recordSyncLog($orderId, 'A3341TP02', $txSeq, $response, 'response', true, $costTime);
return [
'status' => true,
'message' => '订单状态更新成功',
'data' => $response
];
} catch (\Exception $e) {
$costTime = round((microtime(true) - $startTime) * 1000, 2);
$this->recordSyncLog($orderId, 'A3341TP02', $txSeq, null, 'error', false, $costTime, $e->getMessage());
Log::error('建行订单状态更新失败: ' . $e->getMessage());
return [
'status' => false,
'message' => $e->getMessage(),
'data' => null
];
}
}
/**
* 查询建行订单信息
*
* @param string $orderSn 订单号
* @return array
*/
public function queryOrder($orderSn)
{
try {
// 构造订单推送数据
$bodyData = $this->buildOrderPushData($orderData);
// 发送请求
$result = $this->httpClient->request('A3341TP01', $bodyData);
// 调用建行API查询订单
$response = $this->httpClient->queryOrder($orderSn);
return [
'success' => true,
'data' => $result,
'ccb_discount_amt' => $result['CLD_BODY']['CCB_DISCOUNT_AMT'] ?? '0.00',
'status' => true,
'message' => '订单查询成功',
'data' => $response
];
} catch (Exception $e) {
} catch (\Exception $e) {
Log::error('建行订单查询失败: ' . $e->getMessage());
return [
'success' => false,
'error' => $e->getMessage(),
'status' => false,
'message' => $e->getMessage(),
'data' => null
];
}
}
/**
* 更新订单状态 (A3341TP02)
* 处理订单退款
*
* @param string $orderId 订单ID
* @param array $updateData 更新数据
* @return array 返回结果
*/
public function updateOrder($orderId, $updateData)
{
try {
// 构造订单更新数据
$bodyData = array_merge([
'ORDER_ID' => $orderId,
], $updateData);
// 发送请求
$result = $this->httpClient->request('A3341TP02', $bodyData);
return [
'success' => true,
'data' => $result,
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage(),
];
}
}
/**
* 查询订单 (A3341TP03)
*
* @param string $orderId 订单ID
* @return array 返回结果
*/
public function queryOrder($orderId)
{
try {
$bodyData = [
'ORDER_ID' => $orderId,
];
$result = $this->httpClient->request('A3341TP03', $bodyData);
return [
'success' => true,
'data' => $result,
];
} catch (Exception $e) {
return [
'success' => false,
'error' => $e->getMessage(),
];
}
}
/**
* 订单退款 (A3341TP04)
*
* @param string $orderId 订单ID
* @param int $orderId 订单ID
* @param float $refundAmount 退款金额
* @param string $refundReason 退款原因
* @return array 返回结果
* @return array
*/
public function refundOrder($orderId, $refundAmount, $refundReason = '')
{
try {
$bodyData = [
'ORDER_ID' => $orderId,
'REFUND_AMT' => number_format($refundAmount, 2, '.', ''),
'REFUND_REASON' => $refundReason ?: '用户申请退款',
];
// 获取订单信息
$order = Order::find($orderId);
if (!$order) {
throw new \Exception('订单不存在');
}
$result = $this->httpClient->request('A3341TP04', $bodyData);
// 验证退款金额
if ($refundAmount > $order['total_amount']) {
throw new \Exception('退款金额不能超过订单总额');
}
// 调用建行API发起退款
$response = $this->httpClient->refund(
$order['order_sn'],
number_format($refundAmount, 2, '.', ''),
$refundReason
);
// 更新订单退款状态
$this->updateOrderStatus($orderId, null, '2');
return [
'success' => true,
'data' => $result,
'status' => true,
'message' => '退款申请成功',
'data' => $response
];
} catch (Exception $e) {
} catch (\Exception $e) {
Log::error('建行订单退款失败: ' . $e->getMessage());
return [
'success' => false,
'error' => $e->getMessage(),
'status' => false,
'message' => $e->getMessage(),
'data' => null
];
}
}
/**
* 构造订单推送数据 (根据建行接口规范)
* 构建符合建行要求的订单数据
*
* @param array $orderData 商城订单数据
* @return array 建行接口要求的数据格式
* @param array $order 订单数组
* @param array $orderItems 订单商品列表
* @param string $ccbUserId 建行用户ID
* @return array
*/
private function buildOrderPushData($orderData)
private function buildOrderData($order, $orderItems, $ccbUserId)
{
// SKU商品列表
$skuList = [];
if (!empty($orderData['goods_list'])) {
foreach ($orderData['goods_list'] as $goods) {
$skuList[] = [
'SKU_NAME' => $goods['goods_name'] ?? '',
'SKU_REF_PRICE' => number_format($goods['price'] ?? 0, 2, '.', ''),
'SKU_NUM' => $goods['num'] ?? 1,
'SKU_SELL_PRICE' => number_format($goods['price'] ?? 0, 2, '.', ''),
];
}
}
// 构建商品列表
$goodsList = $this->buildGoodsList($orderItems);
// 商户信息
$merchantConfig = $this->config['merchant'] ?? [];
// 计算各项金额
$totalAmount = number_format($order['total_amount'], 2, '.', '');
$payAmount = number_format($order['pay_amount'] ?? $order['total_amount'], 2, '.', '');
$discountAmount = number_format($order['discount_amount'] ?? 0, 2, '.', '');
// 构建订单数据34个必填字段
return [
'USER_ID' => $orderData['ccb_user_id'] ?? '',
'ORDER_ID' => $orderData['order_sn'] ?? '',
'ORDER_DT' => date('YmdHis', $orderData['create_time'] ?? time()),
'TOTAL_AMT' => number_format($orderData['total_amount'] ?? 0, 2, '.', ''),
'PAY_AMT' => number_format($orderData['pay_amount'] ?? 0, 2, '.', ''),
'DISCOUNT_AMT' => number_format($orderData['discount_amount'] ?? 0, 2, '.', ''),
'ORDER_STATUS' => $this->mapOrderStatus($orderData['pay_status'] ?? 0),
'REFUND_STATUS' => $this->mapRefundStatus($orderData['refund_status'] ?? 0),
'MCT_NM' => $merchantConfig['name'] ?? '商户名称',
'CUS_ORDER_URL' => $merchantConfig['order_detail_url'] . $orderData['id'],
'OCC_MCT_LOGO_URL' => $merchantConfig['logo_url'] ?? '',
'PAY_FLOW_ID' => $orderData['ccb_pay_flow_id'] ?? '',
'PAY_MRCH_ID' => $this->config['merchant_id'] ?? '',
'SKU_LIST' => json_encode($skuList, JSON_UNESCAPED_UNICODE),
'USER_ID' => $ccbUserId, // 建行用户ID
'ORDER_ID' => $order['order_sn'], // 订单号
'ORDER_DT' => date('YmdHis', strtotime($order['createtime'])), // 订单时间
'TOTAL_AMT' => $totalAmount, // 订单总金额
'PAY_AMT' => $payAmount, // 实付金额
'DISCOUNT_AMT' => $discountAmount, // 优惠金额
'ORDER_STATUS' => $this->mapOrderStatus($order['status']), // 订单状态
'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($order['pay_type'] ?? ''), // 支付方式
'PAY_TIME' => $order['paytime'] ? date('YmdHis', $order['paytime']) : '', // 支付时间
'DELIVERY_TYPE' => '01', // 配送方式01快递
'DELIVERY_STATUS' => $this->mapDeliveryStatus($order['status']), // 配送状态
'DELIVERY_TIME' => $order['delivery_time'] ? date('YmdHis', $order['delivery_time']) : '', // 发货时间
'RECEIVE_NAME' => $order['consignee'] ?? '', // 收货人姓名
'RECEIVE_PHONE' => $order['mobile'] ?? '', // 收货人电话
'RECEIVE_ADDRESS' => $this->buildAddress($order), // 收货地址
'EXPRESS_COMPANY' => $order['express_company'] ?? '', // 快递公司
'EXPRESS_NO' => $order['express_no'] ?? '', // 快递单号
'REMARK' => $order['remark'] ?? '', // 备注
'ORDER_TYPE' => '01', // 订单类型01普通订单
'IS_VIRTUAL' => '0', // 是否虚拟商品
'ORDER_URL' => $this->config['merchant']['order_detail_url'] . $order['id'], // 订单详情链接
'CREATE_TIME' => date('YmdHis'), // 创建时间
'UPDATE_TIME' => date('YmdHis'), // 更新时间
'SHOP_ID' => '1', // 店铺ID
'SHOP_NAME' => $this->config['merchant']['name'] ?? '', // 店铺名称
'ACTIVITY_ID' => '', // 活动ID
'ACTIVITY_NAME' => '', // 活动名称
'COUPON_AMT' => '0.00', // 优惠券金额
'FREIGHT_AMT' => number_format($order['freight_amount'] ?? 0, 2, '.', ''), // 运费
];
}
/**
* 映射订单状态
* 构建商品列表
*
* Shopro订单状态 => 建行订单状态
* Shopro: closed=交易关闭, cancel=已取消, unpaid=未支付, paid=已支付, completed=已完成, pending=待定
* 建行: 0-待支付 1-已支付 2-已过期 3-失败 4-取消
* @param array $items 订单商品项
* @return array
*/
private function buildGoodsList($items)
{
$goodsList = [];
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'] ?? ''
];
}
return $goodsList;
}
/**
* 构建收货地址
*
* @param object $order 订单对象
* @return string
*/
private function buildAddress($order)
{
$address = '';
if ($order['province']) $address .= $order['province'];
if ($order['city']) $address .= $order['city'];
if ($order['area']) $address .= $order['area'];
if ($order['address']) $address .= $order['address'];
return $address;
}
/**
* 记录同步日志
*
* @param int $orderId 订单ID
* @param string $txCode 交易代码
* @param string $txSeq 交易流水号
* @param mixed $data 数据
* @param string $type 类型request/response/error
* @param bool $success 是否成功
* @param float $costTime 耗时(毫秒)
* @param string $errorMsg 错误信息
*/
private function recordSyncLog($orderId, $txCode, $txSeq, $data, $type = 'request', $success = true, $costTime = 0, $errorMsg = '')
{
try {
// 获取订单号
$orderSn = Db::name('shopro_order')->where('id', $orderId)->value('order_sn');
$logData = [
'order_id' => $orderId,
'order_sn' => $orderSn ?: '',
'tx_code' => $txCode,
'tx_seq' => $txSeq,
'sync_status' => $success ? 1 : 0,
'sync_time' => time(),
'cost_time' => intval($costTime),
'retry_times' => 0
];
if ($type == 'request') {
$logData['request_data'] = is_array($data) ? json_encode($data, JSON_UNESCAPED_UNICODE) : $data;
} elseif ($type == 'response') {
$logData['response_data'] = is_array($data) ? json_encode($data, JSON_UNESCAPED_UNICODE) : $data;
} elseif ($type == 'error') {
$logData['error_msg'] = $errorMsg;
}
Db::name('ccb_sync_log')->insert($logData);
} catch (\Exception $e) {
Log::error('记录同步日志失败: ' . $e->getMessage());
}
}
/**
* 更新订单同步状态
*
* @param int $orderId 订单ID
* @param int $status 同步状态0-未同步 1-已同步 2-同步失败
*/
private function updateOrderSyncStatus($orderId, $status)
{
Db::name('shopro_order')->where('id', $orderId)->update([
'ccb_sync_status' => $status,
'ccb_sync_time' => time(),
'updatetime' => time()
]);
}
/**
* 映射订单状态
*
* @param string $status Shopro订单状态
* @return string 建行订单状态
*/
private function mapOrderStatus($status)
{
$map = [
'unpaid' => '0', // 未支付 => 待支付
'paid' => '1', // 已支付 => 已支付
'completed' => '1', // 已完成 => 已支付
'closed' => '2', // 交易关闭 => 已过期
'cancel' => '4', // 已取消 => 取消
'pending' => '0', // 待定 => 待支付
$statusMap = [
'unpaid' => '0', // 待支付
'paid' => '1', // 已支付
'shipped' => '2', // 已发货
'received' => '3', // 已收货
'completed' => '4', // 已完成
'cancelled' => '5', // 已取消
'refunded' => '6' // 已退款
];
return $map[$status] ?? '0';
return $statusMap[$status] ?? '0';
}
/**
* 映射退款状态
*
* 0-无退款 1-申请 2-已退款 3-部分退款
*
* @param int $status 退款状态
* @return string 建行退款状态
* @param int $refundStatus 退款状态
* @return string
*/
private function mapRefundStatus($status)
private function mapRefundStatus($refundStatus)
{
$map = [
0 => '0', // 无退款
1 => '1', // 申请
2 => '2', // 已退款
3 => '3', // 部分退款
if ($refundStatus == 0) return '0'; // 无退款
if ($refundStatus == 1) return '1'; // 退款中
if ($refundStatus == 2) return '2'; // 已退款
return '0';
}
/**
* 映射支付方式
*
* @param string $payType 支付类型
* @return string
*/
private function mapPayType($payType)
{
$payMap = [
'wechat' => '01', // 微信支付
'alipay' => '02', // 支付宝
'ccb' => '03', // 建行支付
'balance' => '04' // 余额支付
];
return $map[$status] ?? '0';
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';
}
/**
* 批量同步订单
* 用于初始化或定时同步
*
* @param array $conditions 查询条件
* @param int $limit 批量数量
* @return array
*/
public function batchSync($conditions = [], $limit = 100)
{
$successCount = 0;
$failCount = 0;
$errors = [];
try {
// 构建查询
$query = Db::name('shopro_order');
if (!empty($conditions)) {
$query->where($conditions);
}
// 查询需要同步的订单
$orders = $query->where('status', '<>', 'cancelled')
->where('ccb_sync_status', 'in', [0, 2]) // 未同步或同步失败的
->limit($limit)
->select();
foreach ($orders as $order) {
$result = $this->pushOrder($order['id']);
if ($result['status']) {
$successCount++;
} else {
$failCount++;
$errors[] = "订单{$order['order_sn']}: {$result['message']}";
}
}
return [
'status' => true,
'message' => "批量同步完成",
'data' => [
'total' => count($orders),
'success' => $successCount,
'fail' => $failCount,
'errors' => $errors
]
];
} catch (\Exception $e) {
return [
'status' => false,
'message' => '批量同步失败: ' . $e->getMessage(),
'data' => null
];
}
}
}

View File

@ -2,187 +2,406 @@
namespace addons\shopro\library\ccblife;
use think\Exception;
use app\admin\model\shopro\order\Order;
use think\Db;
use think\Log;
/**
* 建行支付服务类
*
* 功能:
* - 生成建行支付串
* - 处理支付回调
* - 验证支付结果
*
* @author Billy
* @date 2025-01-16
* 建行生活支付服务类
* 处理支付串生成、支付回调、支付验证等业务
*/
class CcbPaymentService
{
/**
* 配置信息
* @var array
*/
private $config;
/**
* 加密实例
* @var CcbEncryption
* 订单服务实例
*/
private $encryption;
private $orderService;
/**
* 构造函数
*
* @param array $config 配置数组
*/
public function __construct($config = [])
public function __construct()
{
if (empty($config)) {
$config = config('ccblife');
}
$this->config = $config;
$this->encryption = new CcbEncryption($config);
$this->config = config('ccblife');
$this->orderService = new CcbOrderService();
}
/**
* 生成支付串
* 生成建行支付串
* 用于前端JSBridge调用建行收银台
*
* @param array $order 订单数据
* @return array 返回结果 ['success' => bool, 'payment_string' => string, 'pay_flow_id' => string]
* @param int $orderId Shopro订单ID
* @return array ['status' => bool, 'message' => string, 'data' => array]
*/
public function generatePaymentString($order)
public function generatePaymentString($orderId)
{
try {
// 1. 生成支付流水号
$payFlowId = $this->encryption->generatePayFlowId();
// 获取订单信息
$order = Order::find($orderId);
if (!$order) {
throw new \Exception('订单不存在');
}
// 2. 构造支付参数
$params = $this->buildPaymentParams($order, $payFlowId);
// 检查订单状态
if ($order['status'] != 'unpaid') {
throw new \Exception('订单状态不正确');
}
// 3. 生成支付串签名
$mac = $this->encryption->generatePaymentSign($params);
// 获取用户建行ID
$user = Db::name('user')->where('id', $order['user_id'])->field('ccb_user_id')->find();
if (empty($user['ccb_user_id'])) {
throw new \Exception('用户未绑定建行账号');
}
// 4. 加密商户公钥
$encPub = $this->encryption->encryptMerchantPublicKey();
// 生成支付串签名Shopro 使用 total_fee 作为支付金额字段)
$result = CcbMD5::generatePaymentSignature(
$this->config['merchant_id'],
$this->config['pos_id'],
$this->config['branch_id'],
$order['order_sn'],
number_format($order['total_fee'], 2, '.', ''), // 使用 total_fee
$this->config['private_key'],
'01', // 币种:人民币
'530550' // 交易码
);
// 5. 添加签名和加密字段
$params['MAC'] = $mac;
$params['PLATFORMID'] = $this->config['service_id'];
$params['ENCPUB'] = $encPub;
// 构建完整的支付URL
$paymentUrl = $this->buildPaymentUrl($result['params'], $result['mac']);
// 6. 生成支付串
$paymentString = http_build_query($params);
// 记录支付请求
$this->recordPaymentRequest($orderId, $result);
return [
'success' => true,
'payment_string' => $paymentString,
'pay_flow_id' => $payFlowId,
'status' => true,
'message' => '支付串生成成功',
'data' => [
'payment_string' => $result['payment_string'],
'mac' => $result['mac'],
'payment_url' => $paymentUrl,
'order_sn' => $order['order_sn'],
'amount' => number_format($order['total_fee'], 2, '.', '')
]
];
} catch (Exception $e) {
} catch (\Exception $e) {
Log::error('建行支付串生成失败: ' . $e->getMessage());
return [
'success' => false,
'error' => $e->getMessage(),
'status' => false,
'message' => $e->getMessage(),
'data' => null
];
}
}
/**
* 处理支付回调
* 建行支付完成后的同步回调
*
* @param array $callbackData 回调数据
* @return array 返回结果
* @param array $params URL参数
* @return array
*/
public function handleCallback($callbackData)
public function handleCallback($params)
{
// 支付回调处理逻辑
// 建行支付成功后会通过notify_url回调
try {
// 解密ccbParamSJ参数
if (isset($params['ccbParamSJ'])) {
$decryptedParams = CcbUrlDecrypt::decrypt($params['ccbParamSJ'], $this->config['service_id']);
if ($decryptedParams) {
$params = array_merge($params, $decryptedParams);
}
}
return [
'success' => true,
'trans_id' => $callbackData['trans_id'] ?? '',
'order_id' => $callbackData['order_id'] ?? '',
];
// 获取关键参数
$orderSn = $params['ORDERID'] ?? '';
$posId = $params['POSID'] ?? '';
$success = $params['SUCCESS'] ?? 'N';
// 验证参数
if (empty($orderSn)) {
throw new \Exception('订单号不能为空');
}
// 验证POS号
if ($posId != $this->config['pos_id']) {
throw new \Exception('POS号验证失败');
}
// 查询订单
$order = Order::where('order_sn', $orderSn)->find();
if (!$order) {
throw new \Exception('订单不存在');
}
// 处理支付结果
if ($success == 'Y') {
// 支付成功,更新订单状态
$this->updateOrderPaymentStatus($order, $params);
// 同步订单到建行
$this->orderService->pushOrder($order['id']);
return [
'status' => true,
'message' => '支付成功',
'data' => [
'order_id' => $order['id'],
'order_sn' => $orderSn,
'amount' => $params['PAYMENT'] ?? ''
]
];
} else {
// 支付失败
return [
'status' => false,
'message' => '支付失败',
'data' => [
'order_id' => $order['id'],
'order_sn' => $orderSn,
'error_code' => $params['ERRCODE'] ?? '',
'error_msg' => $params['ERRMSG'] ?? ''
]
];
}
} catch (\Exception $e) {
Log::error('建行支付回调处理失败: ' . $e->getMessage());
return [
'status' => false,
'message' => $e->getMessage(),
'data' => null
];
}
}
/**
* 处理异步通知
* 建行支付异步通知处理
*
* @param array $params 通知参数
* @return string 'success' 'fail'
*/
public function handleNotify($params)
{
try {
// 验证签名
if (!$this->verifyNotifySignature($params)) {
throw new \Exception('签名验证失败');
}
// 获取订单信息
$orderSn = $params['ORDERID'] ?? '';
$order = Order::where('order_sn', $orderSn)->find();
if (!$order) {
throw new \Exception('订单不存在');
}
// 如果订单已支付,直接返回成功
if ($order['status'] == 'paid') {
return 'success';
}
// 更新订单状态
$this->updateOrderPaymentStatus($order, $params);
// 同步到建行
$this->orderService->pushOrder($order['id']);
return 'success';
} catch (\Exception $e) {
Log::error('建行支付异步通知处理失败: ' . $e->getMessage());
return 'fail';
}
}
/**
* 验证支付结果
* 主动查询订单支付状态
*
* @param string $orderId 订单ID
* @return bool 是否支付成功
* @param string $orderSn 订单号
* @return bool
*/
public function verifyPayment($orderId)
public function verifyPayment($orderSn)
{
// 通过订单查询接口验证支付结果
$orderService = new CcbOrderService($this->config);
$result = $orderService->queryOrder($orderId);
try {
// 查询建行订单状态
$result = $this->orderService->queryOrder($orderSn);
if ($result['success']) {
$orderStatus = $result['data']['CLD_BODY']['ORDER_STATUS'] ?? '0';
return $orderStatus === '1'; // 1-已支付
if ($result['status']) {
$data = $result['data']['CLD_BODY'] ?? [];
$txnStatus = $data['TXN_STATUS'] ?? '';
// 00=交易成功
return $txnStatus == '00';
}
return false;
} catch (\Exception $e) {
Log::error('建行支付验证失败: ' . $e->getMessage());
return false;
}
return false;
}
/**
* 构造支付参数
* 建支付URL
*
* @param array $order 订单数据
* @param string $payFlowId 支付流水号
* @return array 支付参数
* @param array $params 支付参数
* @param string $mac 签名
* @return string
*/
private function buildPaymentParams($order, $payFlowId)
private function buildPaymentUrl($params, $mac)
{
// 获取支付配置
$paymentConfig = $this->config['payment'] ?? [];
// 添加必要参数
$params['MAC'] = $mac;
$params['REMARK2'] = $this->config['service_id']; // 服务方编号
// 计算超时时间
$timeoutMinutes = $paymentConfig['timeout_minutes'] ?? 30;
$timeout = date('YmdHis', time() + $timeoutMinutes * 60);
// 生成查询字符串
$queryString = http_build_query($params);
return [
'MERCHANTID' => $this->config['merchant_id'],
'POSID' => $this->config['pos_id'],
'BRANCHID' => $this->config['branch_id'],
'ORDERID' => $payFlowId, // 支付流水号
'USER_ORDERID' => $order['order_sn'], // 商城订单号
'PAYMENT' => number_format($order['pay_amount'], 2, '.', ''),
'CURCODE' => $paymentConfig['currency_code'] ?? '01',
'TXCODE' => $paymentConfig['tx_code'] ?? '520100',
'REMARK1' => '',
'REMARK2' => $this->config['service_id'], // 重要: 必须填写服务方编号
'TYPE' => '1',
'GATEWAY' => '0',
'CLIENTIP' => $this->getClientIp(),
'THIRDAPPINFO' => $paymentConfig['third_app_info'] ?? 'comccbpay1234567890cloudmerchant',
'TIMEOUT' => $timeout,
];
// 返回完整URL实际使用时通过JSBridge调用不直接访问
return $this->config['cashier_url'] . '?' . $queryString;
}
/**
* 获取客户端IP
* 更新订单支付状态
*
* @return string IP地址
* @param object $order 订单对象
* @param array $params 支付参数
*/
private function getClientIp()
private function updateOrderPaymentStatus($order, $params)
{
if (isset($_SERVER['HTTP_X_FORWARDED_FOR'])) {
$ip = $_SERVER['HTTP_X_FORWARDED_FOR'];
} elseif (isset($_SERVER['HTTP_CLIENT_IP'])) {
$ip = $_SERVER['HTTP_CLIENT_IP'];
} elseif (isset($_SERVER['REMOTE_ADDR'])) {
$ip = $_SERVER['REMOTE_ADDR'];
} else {
$ip = '127.0.0.1';
// 更新订单状态为已支付
Order::where('id', $order['id'])->update([
'status' => 'paid',
'pay_type' => 'ccb',
'paytime' => time(),
'transaction_id' => $params['ORDERID'] ?? '',
'updatetime' => time()
]);
// 记录支付日志
$this->recordPaymentLog($order['id'], 'payment_success', $params);
}
/**
* 验证异步通知签名
*
* @param array $params 通知参数
* @return bool
*/
private function verifyNotifySignature($params)
{
// 获取签名
$signature = $params['SIGN'] ?? '';
if (empty($signature)) {
return false;
}
// 如果是多个IP,取第一个
if (strpos($ip, ',') !== false) {
$ips = explode(',', $ip);
$ip = trim($ips[0]);
}
// 移除签名字段
unset($params['SIGN']);
return $ip;
// 按照建行要求的方式构建签名字符串
ksort($params);
$signStr = '';
foreach ($params as $key => $value) {
if ($value !== '') {
$signStr .= $key . '=' . $value . '&';
}
}
$signStr = rtrim($signStr, '&');
// 使用私钥计算签名
$expectedSign = md5($signStr . $this->config['private_key']);
return strtolower($signature) === strtolower($expectedSign);
}
/**
* 记录支付请求
*
* @param int $orderId 订单ID
* @param array $paymentData 支付数据
*/
private function recordPaymentRequest($orderId, $paymentData)
{
// 获取订单信息
$order = Order::find($orderId);
$user = Db::name('user')->where('id', $order['user_id'])->field('ccb_user_id')->find();
// 记录到建行支付日志表
Db::name('ccb_payment_log')->insert([
'order_id' => $orderId,
'order_sn' => $order['order_sn'],
'pay_flow_id' => $order['order_sn'], // 使用订单号作为流水号
'payment_string' => $paymentData['payment_string'] ?? '',
'user_id' => $order['user_id'],
'ccb_user_id' => $user['ccb_user_id'] ?? '',
'amount' => $order['total_fee'],
'status' => 0, // 待支付
'create_time' => time()
]);
}
/**
* 记录支付日志
*
* @param int $orderId 订单ID
* @param string $type 日志类型
* @param array $data 数据
*/
private function recordPaymentLog($orderId, $type, $data)
{
// 更新建行支付日志
if ($type == 'payment_success') {
Db::name('ccb_payment_log')
->where('order_id', $orderId)
->update([
'status' => 1, // 支付成功
'pay_time' => time(),
'trans_id' => $data['ORDERID'] ?? '',
'callback_data' => json_encode($data, JSON_UNESCAPED_UNICODE)
]);
}
}
/**
* 生成退款申请
*
* @param int $orderId 订单ID
* @param float $refundAmount 退款金额
* @param string $refundReason 退款原因
* @return array
*/
public function refund($orderId, $refundAmount, $refundReason = '')
{
try {
// 调用订单服务处理退款
$result = $this->orderService->refundOrder($orderId, $refundAmount, $refundReason);
if ($result['status']) {
// 记录退款日志
$this->recordPaymentLog($orderId, 'refund_request', [
'amount' => $refundAmount,
'reason' => $refundReason
]);
}
return $result;
} catch (\Exception $e) {
Log::error('建行退款申请失败: ' . $e->getMessage());
return [
'status' => false,
'message' => $e->getMessage(),
'data' => null
];
}
}
}

View File

@ -0,0 +1,213 @@
<?php
namespace addons\shopro\library\ccblife;
/**
* 建行生活RSA加密解密类
* 支持1024位RSA密钥117字节分段加密128字节分段解密
*/
class CcbRSA
{
/**
* RSA公钥加密
* 对于长数据使用117字节分段加密1024位RSA密钥
*
* @param string $data 待加密数据
* @param string $publicKey BASE64编码的公钥字符串
* @return string 加密后的BASE64字符串
* @throws \Exception
*/
public static function encrypt($data, $publicKey)
{
// 格式化公钥
$publicKey = self::formatPublicKey($publicKey);
// 加载公钥资源
$pubKey = openssl_pkey_get_public($publicKey);
if (!$pubKey) {
throw new \Exception('公钥格式错误: ' . openssl_error_string());
}
// 获取密钥详情
$keyDetails = openssl_pkey_get_details($pubKey);
$keySize = $keyDetails['bits'] / 8; // 密钥字节数
$maxEncryptBlock = $keySize - 11; // RSA_PKCS1_PADDING 模式下最大加密块大小
// 将数据分段加密
$dataBytes = str_split($data, $maxEncryptBlock);
$encrypted = '';
foreach ($dataBytes as $block) {
$encryptedBlock = '';
$success = openssl_public_encrypt($block, $encryptedBlock, $pubKey, OPENSSL_PKCS1_PADDING);
if (!$success) {
throw new \Exception('RSA加密失败: ' . openssl_error_string());
}
$encrypted .= $encryptedBlock;
}
openssl_free_key($pubKey);
// 返回BASE64编码的结果
return base64_encode($encrypted);
}
/**
* RSA私钥解密
* 对于长数据使用128字节分段解密1024位RSA密钥
*
* @param string $encryptedData BASE64编码的加密数据
* @param string $privateKey BASE64编码的私钥字符串
* @return string 解密后的原始数据
* @throws \Exception
*/
public static function decrypt($encryptedData, $privateKey)
{
// 格式化私钥
$privateKey = self::formatPrivateKey($privateKey);
// 加载私钥资源
$priKey = openssl_pkey_get_private($privateKey);
if (!$priKey) {
throw new \Exception('私钥格式错误: ' . openssl_error_string());
}
// 获取密钥详情
$keyDetails = openssl_pkey_get_details($priKey);
$keySize = $keyDetails['bits'] / 8; // 密钥字节数128字节
// BASE64解码
$encryptedData = base64_decode($encryptedData);
// 将数据分段解密
$dataBytes = str_split($encryptedData, $keySize);
$decrypted = '';
foreach ($dataBytes as $block) {
$decryptedBlock = '';
$success = openssl_private_decrypt($block, $decryptedBlock, $priKey, OPENSSL_PKCS1_PADDING);
if (!$success) {
throw new \Exception('RSA解密失败: ' . openssl_error_string());
}
$decrypted .= $decryptedBlock;
}
openssl_free_key($priKey);
return $decrypted;
}
/**
* 使用建行公钥加密数据API请求
*
* @param string $data 待加密数据
* @param string $ccbPublicKey 建行提供的公钥
* @return string 加密后的BASE64字符串
* @throws \Exception
*/
public static function encryptForCcb($data, $ccbPublicKey)
{
return self::encrypt($data, $ccbPublicKey);
}
/**
* 使用商户私钥解密数据API响应
*
* @param string $encryptedData 加密的数据
* @param string $merchantPrivateKey 商户私钥
* @return string 解密后的数据
* @throws \Exception
*/
public static function decryptFromCcb($encryptedData, $merchantPrivateKey)
{
return self::decrypt($encryptedData, $merchantPrivateKey);
}
/**
* 格式化公钥字符串
* 将BASE64字符串格式化为PEM格式
*
* @param string $publicKey BASE64格式的公钥
* @return string PEM格式的公钥
*/
private static function formatPublicKey($publicKey)
{
// 移除可能存在的空格和换行
$publicKey = preg_replace('/\s+/', '', $publicKey);
// 如果已经是PEM格式直接返回
if (strpos($publicKey, '-----BEGIN PUBLIC KEY-----') !== false) {
return $publicKey;
}
// 格式化为PEM格式
$pem = "-----BEGIN PUBLIC KEY-----\n";
$pem .= chunk_split($publicKey, 64, "\n");
$pem .= "-----END PUBLIC KEY-----\n";
return $pem;
}
/**
* 格式化私钥字符串
* 将BASE64字符串格式化为PEM格式
*
* @param string $privateKey BASE64格式的私钥
* @return string PEM格式的私钥
*/
private static function formatPrivateKey($privateKey)
{
// 移除可能存在的空格和换行
$privateKey = preg_replace('/\s+/', '', $privateKey);
// 如果已经是PEM格式直接返回
if (strpos($privateKey, '-----BEGIN RSA PRIVATE KEY-----') !== false ||
strpos($privateKey, '-----BEGIN PRIVATE KEY-----') !== false) {
return $privateKey;
}
// 格式化为PEM格式
$pem = "-----BEGIN RSA PRIVATE KEY-----\n";
$pem .= chunk_split($privateKey, 64, "\n");
$pem .= "-----END RSA PRIVATE KEY-----\n";
return $pem;
}
/**
* 生成RSA密钥对用于测试
*
* @param int $bits 密钥长度默认1024
* @return array 包含public_key和private_key的数组
* @throws \Exception
*/
public static function generateKeyPair($bits = 1024)
{
$config = [
'private_key_bits' => $bits,
'private_key_type' => OPENSSL_KEYTYPE_RSA,
];
// 生成密钥对
$resource = openssl_pkey_new($config);
if (!$resource) {
throw new \Exception('生成密钥对失败: ' . openssl_error_string());
}
// 导出私钥
openssl_pkey_export($resource, $privateKey);
// 获取公钥
$details = openssl_pkey_get_details($resource);
$publicKey = $details['key'];
// 转换为BASE64格式去除PEM头尾
$privateKey = str_replace(['-----BEGIN RSA PRIVATE KEY-----', '-----END RSA PRIVATE KEY-----', "\n"], '', $privateKey);
$publicKey = str_replace(['-----BEGIN PUBLIC KEY-----', '-----END PUBLIC KEY-----', "\n"], '', $publicKey);
return [
'public_key' => $publicKey,
'private_key' => $privateKey
];
}
}

View File

@ -0,0 +1,227 @@
<?php
namespace addons\shopro\library\ccblife;
/**
* 建行生活URL参数解密类
* 处理ccbParamSJ参数的DES解密
*/
class CcbUrlDecrypt
{
/**
* 解密建行URL参数ccbParamSJ
* 流程双层BASE64解码 -> DES解密
*
* @param string $ccbParamSJ 加密的参数字符串
* @param string $serviceId 服务ID用于生成DES密钥
* @return array|false 解密后的参数数组失败返回false
*/
public static function decrypt($ccbParamSJ, $serviceId)
{
try {
// 第一次BASE64解码
$firstDecode = base64_decode($ccbParamSJ);
if ($firstDecode === false) {
throw new \Exception('第一次BASE64解码失败');
}
// 第二次BASE64解码
$secondDecode = base64_decode($firstDecode);
if ($secondDecode === false) {
throw new \Exception('第二次BASE64解码失败');
}
// 获取DES密钥服务ID前8位
$desKey = substr($serviceId, 0, 8);
// DES解密
$decrypted = self::desDecrypt($secondDecode, $desKey);
if ($decrypted === false) {
throw new \Exception('DES解密失败');
}
// 解析参数字符串为数组
parse_str($decrypted, $params);
return $params;
} catch (\Exception $e) {
// 记录错误日志
trace('建行URL参数解密失败: ' . $e->getMessage(), 'error');
return false;
}
}
/**
* DES解密
* 使用ECB模式PKCS5Padding填充
*
* @param string $encryptedData 加密的数据
* @param string $key 密钥8字节
* @return string|false 解密后的数据失败返回false
*/
private static function desDecrypt($encryptedData, $key)
{
// 确保密钥长度为8字节
if (strlen($key) !== 8) {
return false;
}
// 使用openssl进行DES解密
$decrypted = openssl_decrypt(
$encryptedData,
'DES-ECB',
$key,
OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING
);
if ($decrypted === false) {
return false;
}
// 移除PKCS5填充
$decrypted = self::removePKCS5Padding($decrypted);
return $decrypted;
}
/**
* DES加密用于测试
* 使用ECB模式PKCS5Padding填充
*
* @param string $data 待加密的数据
* @param string $key 密钥8字节
* @return string|false 加密后的数据失败返回false
*/
public static function desEncrypt($data, $key)
{
// 确保密钥长度为8字节
if (strlen($key) !== 8) {
return false;
}
// 添加PKCS5填充
$data = self::addPKCS5Padding($data, 8);
// 使用openssl进行DES加密
$encrypted = openssl_encrypt(
$data,
'DES-ECB',
$key,
OPENSSL_RAW_DATA | OPENSSL_ZERO_PADDING
);
return $encrypted;
}
/**
* 生成建行URL参数ccbParamSJ用于测试
*
* @param array $params 参数数组
* @param string $serviceId 服务ID
* @return string 加密后的ccbParamSJ参数
*/
public static function encrypt($params, $serviceId)
{
// 将参数数组转换为查询字符串
$queryString = http_build_query($params);
// 获取DES密钥服务ID前8位
$desKey = substr($serviceId, 0, 8);
// DES加密
$encrypted = self::desEncrypt($queryString, $desKey);
// 双层BASE64编码
$firstEncode = base64_encode($encrypted);
$secondEncode = base64_encode($firstEncode);
return $secondEncode;
}
/**
* 添加PKCS5填充
*
* @param string $text 待填充的文本
* @param int $blocksize 块大小
* @return string 填充后的文本
*/
private static function addPKCS5Padding($text, $blocksize)
{
$pad = $blocksize - (strlen($text) % $blocksize);
return $text . str_repeat(chr($pad), $pad);
}
/**
* 移除PKCS5填充
*
* @param string $text 已填充的文本
* @return string 移除填充后的文本
*/
private static function removePKCS5Padding($text)
{
$pad = ord($text[strlen($text) - 1]);
if ($pad > strlen($text)) {
return false;
}
if (strspn($text, chr($pad), strlen($text) - $pad) != $pad) {
return false;
}
return substr($text, 0, -1 * $pad);
}
/**
* 解析建行跳转URL中的所有参数
* 处理URL中的ccbParamSJ和其他参数
*
* @param string $url 完整的URL或查询字符串
* @param string $serviceId 服务ID
* @return array 包含所有参数的数组
*/
public static function parseUrl($url, $serviceId)
{
// 解析URL获取查询参数
$urlParts = parse_url($url);
$queryString = isset($urlParts['query']) ? $urlParts['query'] : $url;
// 解析查询字符串
parse_str($queryString, $params);
// 如果存在ccbParamSJ参数进行解密
if (isset($params['ccbParamSJ']) && !empty($params['ccbParamSJ'])) {
$decryptedParams = self::decrypt($params['ccbParamSJ'], $serviceId);
if ($decryptedParams !== false) {
// 合并解密后的参数
$params = array_merge($params, $decryptedParams);
}
}
return $params;
}
/**
* 生成测试URL
* 用于生成包含加密参数的完整URL
*
* @param string $baseUrl 基础URL
* @param array $encryptedParams 需要加密的参数
* @param array $plainParams 明文参数
* @param string $serviceId 服务ID
* @return string 完整的URL
*/
public static function generateUrl($baseUrl, $encryptedParams, $plainParams, $serviceId)
{
// 加密参数
$ccbParamSJ = self::encrypt($encryptedParams, $serviceId);
// 合并所有参数
$allParams = array_merge($plainParams, ['ccbParamSJ' => $ccbParamSJ]);
// 构建查询字符串
$queryString = http_build_query($allParams);
// 拼接URL
$separator = strpos($baseUrl, '?') === false ? '?' : '&';
return $baseUrl . $separator . $queryString;
}
}

View File

@ -1188,10 +1188,10 @@ ADD UNIQUE KEY `uk_ccb_user_id` (`ccb_user_id`);
```sql
-- 订单表增加建行相关字段
ALTER TABLE `fa_shopro_order`
ADD COLUMN `ccb_user_id` varchar(30) DEFAULT NULL COMMENT '建行用户ID' AFTER `user_id`,
ADD COLUMN `ccb_pay_flow_id` varchar(50) DEFAULT NULL COMMENT '建行支付流水号' AFTER `order_sn`,
ADD COLUMN `ccb_sync_status` tinyint(1) DEFAULT '0' COMMENT '建行同步状态 0-未同步 1-已同步 2-同步失败' AFTER `pay_status`,
ADD COLUMN `ccb_sync_time` int(11) DEFAULT NULL COMMENT '建行同步时间' AFTER `ccb_sync_status`,
ADD COLUMN `ccb_user_id` varchar(30) DEFAULT NULL COMMENT '建行用户ID',
ADD COLUMN `ccb_pay_flow_id` varchar(50) DEFAULT NULL COMMENT '建行支付流水号',
ADD COLUMN `ccb_sync_status` tinyint(1) DEFAULT '0' COMMENT '建行同步状态 0-未同步 1-已同步 2-同步失败',
ADD COLUMN `ccb_sync_time` int(11) DEFAULT NULL COMMENT '建行同步时间',
ADD INDEX `idx_ccb_user_id` (`ccb_user_id`),
ADD INDEX `idx_ccb_pay_flow_id` (`ccb_pay_flow_id`),
ADD INDEX `idx_ccb_sync_status` (`ccb_sync_status`);
@ -1200,56 +1200,56 @@ ADD INDEX `idx_ccb_sync_status` (`ccb_sync_status`);
### 5.3 建行支付日志表
```sql
CREATE TABLE `fa_ccb_payment_log` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_id` varchar(50) NOT NULL COMMENT '商城订单ID',
`order_sn` varchar(50) NOT NULL COMMENT '商城订单号',
`pay_flow_id` varchar(50) NOT NULL COMMENT '支付流水号(对应建行ORDERID)',
`payment_string` text COMMENT '支付串',
`trans_id` varchar(100) DEFAULT NULL COMMENT '建行交易ID',
`user_id` int(11) DEFAULT NULL COMMENT '用户ID',
`ccb_user_id` varchar(30) DEFAULT NULL COMMENT '建行用户ID',
`amount` decimal(10,2) NOT NULL COMMENT '支付金额',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0-待支付 1-支付成功 2-支付失败 3-已取消',
`create_time` int(11) NOT NULL COMMENT '创建时间',
`pay_time` int(11) DEFAULT NULL COMMENT '支付时间',
`callback_data` text COMMENT '回调数据',
`error_msg` varchar(500) DEFAULT NULL COMMENT '错误信息',
PRIMARY KEY (`id`),
UNIQUE KEY `pay_flow_id` (`pay_flow_id`),
KEY `order_id` (`order_id`),
KEY `order_sn` (`order_sn`),
KEY `trans_id` (`trans_id`),
KEY `status` (`status`),
KEY `create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='建行支付日志表';
CREATE TABLE IF NOT EXISTS `fa_ccb_payment_log` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_id` int(11) NOT NULL COMMENT '商城订单ID',
`order_sn` varchar(50) NOT NULL COMMENT '商城订单号',
`pay_flow_id` varchar(50) NOT NULL COMMENT '支付流水号(对应建行ORDERID)',
`payment_string` text COMMENT '支付串',
`trans_id` varchar(100) DEFAULT NULL COMMENT '建行交易ID',
`user_id` int(11) DEFAULT NULL COMMENT '用户ID',
`ccb_user_id` varchar(30) DEFAULT NULL COMMENT '建行用户ID',
`amount` decimal(10,2) NOT NULL COMMENT '支付金额',
`status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0-待支付 1-支付成功 2-支付失败 3-已取消',
`create_time` int(11) NOT NULL COMMENT '创建时间',
`pay_time` int(11) DEFAULT NULL COMMENT '支付时间',
`callback_data` text COMMENT '回调数据',
`error_msg` varchar(500) DEFAULT NULL COMMENT '错误信息',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_pay_flow_id` (`pay_flow_id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_order_sn` (`order_sn`),
KEY `idx_trans_id` (`trans_id`),
KEY `idx_status` (`status`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='建行支付日志表';
```
### 5.4 建行订单同步日志表
```sql
CREATE TABLE `fa_ccb_sync_log` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_id` varchar(50) NOT NULL COMMENT '商城订单ID',
`order_sn` varchar(50) NOT NULL COMMENT '商城订单号',
`tx_code` varchar(20) NOT NULL COMMENT '交易代码 A3341TP01/02/03/04',
`tx_seq` varchar(50) DEFAULT NULL COMMENT '交易流水号',
`request_data` text COMMENT '请求数据(加密前)',
`encrypted_data` text COMMENT '加密后数据',
`response_data` text COMMENT '响应数据',
`sync_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0-失败 1-成功',
`sync_time` int(11) NOT NULL COMMENT '同步时间',
`retry_times` int(11) NOT NULL DEFAULT '0' COMMENT '重试次数',
`error_msg` varchar(500) DEFAULT NULL COMMENT '错误信息',
`cost_time` int(11) DEFAULT NULL COMMENT '耗时(毫秒)',
PRIMARY KEY (`id`),
KEY `order_id` (`order_id`),
KEY `order_sn` (`order_sn`),
KEY `tx_code` (`tx_code`),
KEY `tx_seq` (`tx_seq`),
KEY `sync_status` (`sync_status`),
KEY `sync_time` (`sync_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='建行订单同步日志表';
CREATE TABLE IF NOT EXISTS `fa_ccb_sync_log` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`order_id` int(11) NOT NULL COMMENT '商城订单ID',
`order_sn` varchar(50) NOT NULL COMMENT '商城订单号',
`tx_code` varchar(20) NOT NULL COMMENT '交易代码 A3341TP01/02/03/04',
`tx_seq` varchar(50) DEFAULT NULL COMMENT '交易流水号',
`request_data` text COMMENT '请求数据(加密前)',
`encrypted_data` text COMMENT '加密后数据',
`response_data` text COMMENT '响应数据',
`sync_status` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0-失败 1-成功',
`sync_time` int(11) NOT NULL COMMENT '同步时间',
`retry_times` int(11) NOT NULL DEFAULT '0' COMMENT '重试次数',
`error_msg` varchar(500) DEFAULT NULL COMMENT '错误信息',
`cost_time` int(11) DEFAULT NULL COMMENT '耗时(毫秒)',
PRIMARY KEY (`id`),
KEY `idx_order_id` (`order_id`),
KEY `idx_order_sn` (`order_sn`),
KEY `idx_tx_code` (`tx_code`),
KEY `idx_tx_seq` (`tx_seq`),
KEY `idx_sync_status` (`sync_status`),
KEY `idx_sync_time` (`sync_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='建行订单同步日志表';
```
---

View File

@ -0,0 +1,437 @@
/**
* 建行生活 JSBridge 集成库
*
* 功能说明
* 1. 检测是否在建行生活 App 内运行
* 2. 获取建行用户信息
* 3. 调起建行支付
* 4. 处理 URL 参数解密
*
* @author Billy
* @date 2025-01-17
*/
(function(window) {
'use strict';
// 建行生活 JSBridge 对象
var CcbLifeBridge = {
// 配置信息
config: {
// API 基础地址
apiBaseUrl: '/addons/shopro',
// 调试模式
debug: false,
// 超时时间(毫秒)
timeout: 10000,
// 重试次数
retryTimes: 3
},
// 初始化状态
isReady: false,
readyCallbacks: [],
/**
* 初始化
* @param {Object} options 配置选项
*/
init: function(options) {
// 合并配置
if (options) {
Object.assign(this.config, options);
}
// 监听 JSBridge 就绪事件
this.setupBridge();
// 自动登录(如果在建行 App 内)
if (this.isInCcbApp()) {
this.autoLogin();
}
this.log('CcbLifeBridge 初始化完成');
},
/**
* 设置 JSBridge
*/
setupBridge: function() {
var self = this;
// iOS WebViewJavascriptBridge
if (window.WebViewJavascriptBridge) {
self.bridge = window.WebViewJavascriptBridge;
self.onBridgeReady();
} else {
document.addEventListener('WebViewJavascriptBridgeReady', function() {
self.bridge = window.WebViewJavascriptBridge;
self.onBridgeReady();
}, false);
}
// Android 直接通过 window 对象调用
if (window.mbspay && !self.bridge) {
self.bridge = window.mbspay;
self.onBridgeReady();
}
// 设置超时检查
setTimeout(function() {
if (!self.isReady) {
self.log('JSBridge 未就绪,可能不在建行 App 内');
}
}, 3000);
},
/**
* Bridge 就绪回调
*/
onBridgeReady: function() {
this.isReady = true;
this.log('JSBridge 已就绪');
// 执行所有等待的回调
this.readyCallbacks.forEach(function(callback) {
callback();
});
this.readyCallbacks = [];
},
/**
* 等待 Bridge 就绪
* @param {Function} callback 回调函数
*/
ready: function(callback) {
if (this.isReady) {
callback();
} else {
this.readyCallbacks.push(callback);
}
},
/**
* 检测是否在建行生活 App
* @returns {Boolean}
*/
isInCcbApp: function() {
var ua = navigator.userAgent.toLowerCase();
// 检查 User-Agent
if (ua.indexOf('ccblife') > -1 || ua.indexOf('ccb') > -1) {
return true;
}
// 检查 URL 参数
var urlParams = this.getUrlParams();
if (urlParams.ccbParamSJ || urlParams.from === 'ccblife') {
return true;
}
// 检查是否有 JSBridge
if (window.WebViewJavascriptBridge || window.mbspay) {
return true;
}
return false;
},
/**
* 获取 URL 参数
* @returns {Object}
*/
getUrlParams: function() {
var params = {};
var search = window.location.search.substring(1);
if (search) {
var pairs = search.split('&');
pairs.forEach(function(pair) {
var parts = pair.split('=');
if (parts.length === 2) {
params[decodeURIComponent(parts[0])] = decodeURIComponent(parts[1]);
}
});
}
return params;
},
/**
* 获取建行用户信息
* @param {Function} callback 回调函数
*/
getUserInfo: function(callback) {
var self = this;
if (!this.isInCcbApp()) {
callback({
success: false,
error: '不在建行生活 App 内'
});
return;
}
this.ready(function() {
self.callNative('getUserInfo', {}, function(result) {
if (result && result.userid) {
callback({
success: true,
data: {
ccb_user_id: result.userid,
mobile: result.mobile || '',
nickname: result.nickname || '',
avatar: result.avatar || ''
}
});
} else {
callback({
success: false,
error: '获取用户信息失败'
});
}
});
});
},
/**
* 自动登录
*/
autoLogin: function() {
var self = this;
// 检查是否已登录
if (localStorage.getItem('ccb_token')) {
this.log('用户已登录');
return;
}
// 获取用户信息并登录
this.getUserInfo(function(result) {
if (result.success) {
self.doLogin(result.data);
} else {
self.log('获取用户信息失败:' + result.error);
}
});
},
/**
* 执行登录
* @param {Object} userInfo 用户信息
*/
doLogin: function(userInfo) {
var self = this;
// 调用后端登录接口
this.ajax({
url: this.config.apiBaseUrl + '/ccblife/autoLogin',
method: 'POST',
data: userInfo,
success: function(response) {
if (response.code === 1) {
// 保存 Token
localStorage.setItem('ccb_token', response.data.token);
localStorage.setItem('ccb_user_info', JSON.stringify(response.data.userInfo));
self.log('自动登录成功');
// 触发登录成功事件
self.triggerEvent('ccb:login:success', response.data);
} else {
self.log('登录失败:' + response.msg);
}
},
error: function(error) {
self.log('登录请求失败:' + error);
}
});
},
/**
* 调起建行支付
* @param {Object} options 支付参数
* @param {Function} callback 回调函数
*/
payment: function(options, callback) {
var self = this;
if (!this.isInCcbApp()) {
callback({
success: false,
error: '不在建行生活 App 内'
});
return;
}
// 必需参数检查
if (!options.payment_string) {
callback({
success: false,
error: '缺少支付串参数'
});
return;
}
this.ready(function() {
// 区分 iOS 和 Android
if (self.isIOS()) {
// iOS 使用 URL Scheme
self.paymentForIOS(options, callback);
} else {
// Android 使用 JSBridge
self.paymentForAndroid(options, callback);
}
});
},
/**
* iOS 支付
*/
paymentForIOS: function(options, callback) {
var paymentUrl = 'comccbpay://pay?' + options.payment_string;
// 尝试打开支付页面
window.location.href = paymentUrl;
// 设置回调检查
setTimeout(function() {
callback({
success: true,
message: '已调起支付,请在建行 App 内完成支付'
});
}, 1000);
},
/**
* Android 支付
*/
paymentForAndroid: function(options, callback) {
var self = this;
// 调用原生支付方法
this.callNative('payment', {
payment_string: options.payment_string
}, function(result) {
if (result && result.success) {
callback({
success: true,
data: result
});
} else {
callback({
success: false,
error: result ? result.error : '支付失败'
});
}
});
},
/**
* 调用原生方法
* @param {String} method 方法名
* @param {Object} params 参数
* @param {Function} callback 回调函数
*/
callNative: function(method, params, callback) {
var self = this;
try {
if (this.isIOS() && this.bridge && this.bridge.callHandler) {
// iOS WebViewJavascriptBridge
this.bridge.callHandler(method, params, callback);
} else if (window.mbspay && window.mbspay[method]) {
// Android 直接调用
var result = window.mbspay[method](JSON.stringify(params));
if (callback) {
callback(typeof result === 'string' ? JSON.parse(result) : result);
}
} else {
self.log('原生方法不存在:' + method);
if (callback) {
callback({
success: false,
error: '原生方法不存在'
});
}
}
} catch (e) {
self.log('调用原生方法失败:' + e.message);
if (callback) {
callback({
success: false,
error: e.message
});
}
}
},
/**
* 判断是否 iOS
*/
isIOS: function() {
return /iPhone|iPad|iPod/i.test(navigator.userAgent);
},
/**
* 触发事件
*/
triggerEvent: function(eventName, data) {
var event = new CustomEvent(eventName, {
detail: data
});
window.dispatchEvent(event);
},
/**
* AJAX 请求
*/
ajax: function(options) {
var xhr = new XMLHttpRequest();
var self = this;
xhr.open(options.method || 'GET', options.url, true);
xhr.setRequestHeader('Content-Type', 'application/json');
// 添加 Token
var token = localStorage.getItem('ccb_token');
if (token) {
xhr.setRequestHeader('token', token);
}
xhr.onload = function() {
if (xhr.status >= 200 && xhr.status < 300) {
var response = JSON.parse(xhr.responseText);
if (options.success) {
options.success(response);
}
} else {
if (options.error) {
options.error('请求失败:' + xhr.status);
}
}
};
xhr.onerror = function() {
if (options.error) {
options.error('网络错误');
}
};
xhr.send(options.data ? JSON.stringify(options.data) : null);
},
/**
* 日志输出
*/
log: function(message) {
if (this.config.debug) {
console.log('[CcbLifeBridge] ' + message);
}
}
};
// 暴露到全局
window.CcbLifeBridge = CcbLifeBridge;
})(window);

411
public/ccblife-demo.html Normal file
View File

@ -0,0 +1,411 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
<title>建行生活 H5 集成示例</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
background: #f5f5f5;
padding: 20px;
}
.container {
max-width: 600px;
margin: 0 auto;
background: #fff;
border-radius: 8px;
padding: 20px;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
h1 {
font-size: 24px;
margin-bottom: 20px;
color: #333;
text-align: center;
}
.info-section {
background: #f8f9fa;
border-radius: 4px;
padding: 15px;
margin-bottom: 20px;
}
.info-item {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
font-size: 14px;
}
.info-item:last-child {
margin-bottom: 0;
}
.label {
color: #666;
}
.value {
color: #333;
font-weight: 500;
}
.btn {
display: block;
width: 100%;
padding: 12px 20px;
background: #0066cc;
color: #fff;
border: none;
border-radius: 4px;
font-size: 16px;
cursor: pointer;
margin-bottom: 15px;
transition: background 0.3s;
}
.btn:hover {
background: #0052a3;
}
.btn:disabled {
background: #ccc;
cursor: not-allowed;
}
.btn-secondary {
background: #6c757d;
}
.btn-secondary:hover {
background: #545b62;
}
.btn-success {
background: #28a745;
}
.btn-success:hover {
background: #218838;
}
.result {
background: #f0f8ff;
border: 1px solid #b6d4fe;
border-radius: 4px;
padding: 15px;
margin-top: 20px;
white-space: pre-wrap;
font-family: monospace;
font-size: 12px;
max-height: 300px;
overflow-y: auto;
}
.status {
display: inline-block;
padding: 4px 8px;
border-radius: 3px;
font-size: 12px;
margin-left: 10px;
}
.status-success {
background: #d4edda;
color: #155724;
}
.status-error {
background: #f8d7da;
color: #721c24;
}
.status-warning {
background: #fff3cd;
color: #856404;
}
.loading {
display: none;
text-align: center;
padding: 20px;
}
.loading.active {
display: block;
}
.spinner {
border: 3px solid #f3f3f3;
border-top: 3px solid #0066cc;
border-radius: 50%;
width: 30px;
height: 30px;
animation: spin 1s linear infinite;
margin: 0 auto;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
.divider {
height: 1px;
background: #e9ecef;
margin: 20px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>建行生活 H5 集成示例</h1>
<!-- 环境信息 -->
<div class="info-section">
<h3>环境信息</h3>
<div class="info-item">
<span class="label">当前环境:</span>
<span class="value" id="env-status">检测中...</span>
</div>
<div class="info-item">
<span class="label">JSBridge 状态:</span>
<span class="value" id="bridge-status">未就绪</span>
</div>
<div class="info-item">
<span class="label">登录状态:</span>
<span class="value" id="login-status">未登录</span>
</div>
<div class="info-item">
<span class="label">建行用户ID</span>
<span class="value" id="ccb-user-id">-</span>
</div>
</div>
<div class="divider"></div>
<!-- 功能测试 -->
<h3 style="margin-bottom: 15px;">功能测试</h3>
<button class="btn" onclick="getUserInfo()">获取建行用户信息</button>
<button class="btn btn-secondary" onclick="doAutoLogin()">执行自动登录</button>
<button class="btn btn-success" onclick="testPayment()">测试支付功能</button>
<div class="divider"></div>
<!-- 调试功能 -->
<h3 style="margin-bottom: 15px;">调试功能</h3>
<button class="btn btn-secondary" onclick="checkUrlParams()">检查 URL 参数</button>
<button class="btn btn-secondary" onclick="clearCache()">清除缓存</button>
<button class="btn btn-secondary" onclick="toggleDebug()">切换调试模式</button>
<!-- 加载状态 -->
<div class="loading" id="loading">
<div class="spinner"></div>
<p style="margin-top: 10px;">处理中...</p>
</div>
<!-- 结果显示 -->
<div class="result" id="result" style="display: none;"></div>
</div>
<!-- 引入 JSBridge -->
<script src="/assets/js/ccblife-bridge.js"></script>
<script>
// 初始化配置
window.onload = function() {
// 初始化 CcbLifeBridge
CcbLifeBridge.init({
debug: true,
apiBaseUrl: '/addons/shopro'
});
// 更新环境状态
updateEnvStatus();
// 监听登录成功事件
window.addEventListener('ccb:login:success', function(e) {
console.log('登录成功事件:', e.detail);
updateLoginStatus(e.detail);
});
// 监听 Bridge 就绪
CcbLifeBridge.ready(function() {
document.getElementById('bridge-status').innerHTML =
'<span class="status status-success">已就绪</span>';
});
};
// 更新环境状态
function updateEnvStatus() {
var isInApp = CcbLifeBridge.isInCcbApp();
var envEl = document.getElementById('env-status');
if (isInApp) {
envEl.innerHTML = '<span class="status status-success">建行生活 App 内</span>';
} else {
envEl.innerHTML = '<span class="status status-warning">普通浏览器</span>';
}
// 检查登录状态
var token = localStorage.getItem('ccb_token');
var userInfo = localStorage.getItem('ccb_user_info');
if (token && userInfo) {
try {
var user = JSON.parse(userInfo);
updateLoginStatus({ userInfo: user });
} catch(e) {
console.error('解析用户信息失败', e);
}
}
}
// 更新登录状态
function updateLoginStatus(data) {
document.getElementById('login-status').innerHTML =
'<span class="status status-success">已登录</span>';
if (data.userInfo && data.userInfo.ccb_user_id) {
document.getElementById('ccb-user-id').textContent = data.userInfo.ccb_user_id;
}
}
// 获取用户信息
function getUserInfo() {
showLoading(true);
CcbLifeBridge.getUserInfo(function(result) {
showLoading(false);
if (result.success) {
showResult('获取用户信息成功:\n' + JSON.stringify(result.data, null, 2));
} else {
showResult('获取用户信息失败:' + result.error);
}
});
}
// 执行自动登录
function doAutoLogin() {
if (!CcbLifeBridge.isInCcbApp()) {
showResult('错误:不在建行生活 App 内,无法执行自动登录');
return;
}
showLoading(true);
CcbLifeBridge.getUserInfo(function(result) {
if (result.success) {
CcbLifeBridge.doLogin(result.data);
showLoading(false);
showResult('正在登录...\n用户信息' + JSON.stringify(result.data, null, 2));
} else {
showLoading(false);
showResult('获取用户信息失败:' + result.error);
}
});
}
// 测试支付功能
function testPayment() {
// 这里需要先调用后端接口生成支付串
var orderId = prompt('请输入订单ID测试用', '1');
if (!orderId) {
return;
}
showLoading(true);
// 模拟调用后端生成支付串
fetch('/addons/shopro/ccbpayment/createPayment', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'token': localStorage.getItem('ccb_token') || ''
},
body: JSON.stringify({
order_id: orderId
})
})
.then(response => response.json())
.then(data => {
showLoading(false);
if (data.code === 1) {
// 调起支付
CcbLifeBridge.payment({
payment_string: data.data.payment_string
}, function(result) {
if (result.success) {
showResult('支付调起成功,请在建行 App 内完成支付');
} else {
showResult('支付调起失败:' + result.error);
}
});
} else {
showResult('生成支付串失败:' + data.msg);
}
})
.catch(error => {
showLoading(false);
showResult('请求失败:' + error.message);
});
}
// 检查 URL 参数
function checkUrlParams() {
var params = CcbLifeBridge.getUrlParams();
showResult('URL 参数:\n' + JSON.stringify(params, null, 2));
}
// 清除缓存
function clearCache() {
localStorage.removeItem('ccb_token');
localStorage.removeItem('ccb_user_info');
document.getElementById('login-status').innerHTML =
'<span class="status status-warning">未登录</span>';
document.getElementById('ccb-user-id').textContent = '-';
showResult('缓存已清除');
}
// 切换调试模式
function toggleDebug() {
CcbLifeBridge.config.debug = !CcbLifeBridge.config.debug;
showResult('调试模式:' + (CcbLifeBridge.config.debug ? '开启' : '关闭'));
}
// 显示加载状态
function showLoading(show) {
var loading = document.getElementById('loading');
if (show) {
loading.classList.add('active');
} else {
loading.classList.remove('active');
}
}
// 显示结果
function showResult(message) {
var resultEl = document.getElementById('result');
resultEl.style.display = 'block';
resultEl.textContent = message;
}
</script>
</body>
</html>