mirror of
https://gitee.com/liuxioabin/fengketrade.git
synced 2026-04-17 12:57:32 +08:00
封装ccb
This commit is contained in:
parent
3af024ae75
commit
fd51bd8e8e
@ -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' => [
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号脱敏
|
||||
*
|
||||
|
||||
@ -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;
|
||||
|
||||
527
addons/shopro/controller/Ccbtest.php
Normal file
527
addons/shopro/controller/Ccbtest.php
Normal 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 '✗ 格式异常';
|
||||
}
|
||||
}
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
168
addons/shopro/library/ccblife/CcbMD5.php
Normal file
168
addons/shopro/library/ccblife/CcbMD5.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
213
addons/shopro/library/ccblife/CcbRSA.php
Normal file
213
addons/shopro/library/ccblife/CcbRSA.php
Normal 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
|
||||
];
|
||||
}
|
||||
}
|
||||
227
addons/shopro/library/ccblife/CcbUrlDecrypt.php
Normal file
227
addons/shopro/library/ccblife/CcbUrlDecrypt.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -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='建行订单同步日志表';
|
||||
```
|
||||
|
||||
---
|
||||
437
public/assets/js/ccblife-bridge.js
Normal file
437
public/assets/js/ccblife-bridge.js
Normal 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
411
public/ccblife-demo.html
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user