diff --git a/addons/shopro/config/ccblife.php b/addons/shopro/config/ccblife.php index 86465b9..355ea2f 100644 --- a/addons/shopro/config/ccblife.php +++ b/addons/shopro/config/ccblife.php @@ -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' => [ diff --git a/addons/shopro/controller/Ccblife.php b/addons/shopro/controller/Ccblife.php index 016079f..8136569 100644 --- a/addons/shopro/controller/Ccblife.php +++ b/addons/shopro/controller/Ccblife.php @@ -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; + } + } + /** * 手机号脱敏 * diff --git a/addons/shopro/controller/Ccbpayment.php b/addons/shopro/controller/Ccbpayment.php index 4e60122..12668a7 100644 --- a/addons/shopro/controller/Ccbpayment.php +++ b/addons/shopro/controller/Ccbpayment.php @@ -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; diff --git a/addons/shopro/controller/Ccbtest.php b/addons/shopro/controller/Ccbtest.php new file mode 100644 index 0000000..3548323 --- /dev/null +++ b/addons/shopro/controller/Ccbtest.php @@ -0,0 +1,527 @@ +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 '✗ 格式异常'; + } +} \ No newline at end of file diff --git a/addons/shopro/library/ccblife/CcbHttpClient.php b/addons/shopro/library/ccblife/CcbHttpClient.php index c80d5b0..90f019a 100644 --- a/addons/shopro/library/ccblife/CcbHttpClient.php +++ b/addons/shopro/library/ccblife/CcbHttpClient.php @@ -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; + } } } diff --git a/addons/shopro/library/ccblife/CcbMD5.php b/addons/shopro/library/ccblife/CcbMD5.php new file mode 100644 index 0000000..9ad5d0b --- /dev/null +++ b/addons/shopro/library/ccblife/CcbMD5.php @@ -0,0 +1,168 @@ + $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; + } +} \ No newline at end of file diff --git a/addons/shopro/library/ccblife/CcbOrderService.php b/addons/shopro/library/ccblife/CcbOrderService.php index 1121d81..186d55e 100644 --- a/addons/shopro/library/ccblife/CcbOrderService.php +++ b/addons/shopro/library/ccblife/CcbOrderService.php @@ -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 + ]; + } } } diff --git a/addons/shopro/library/ccblife/CcbPaymentService.php b/addons/shopro/library/ccblife/CcbPaymentService.php index 9eb5d30..04cf0d1 100644 --- a/addons/shopro/library/ccblife/CcbPaymentService.php +++ b/addons/shopro/library/ccblife/CcbPaymentService.php @@ -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 + ]; + } } } diff --git a/addons/shopro/library/ccblife/CcbRSA.php b/addons/shopro/library/ccblife/CcbRSA.php new file mode 100644 index 0000000..402e3f4 --- /dev/null +++ b/addons/shopro/library/ccblife/CcbRSA.php @@ -0,0 +1,213 @@ + $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 + ]; + } +} \ No newline at end of file diff --git a/addons/shopro/library/ccblife/CcbUrlDecrypt.php b/addons/shopro/library/ccblife/CcbUrlDecrypt.php new file mode 100644 index 0000000..0a6f6fe --- /dev/null +++ b/addons/shopro/library/ccblife/CcbUrlDecrypt.php @@ -0,0 +1,227 @@ + 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; + } +} \ No newline at end of file diff --git a/完整技术实现方案CLAUDE.md b/fangan.md similarity index 96% rename from 完整技术实现方案CLAUDE.md rename to fangan.md index 143bcf6..27ae36a 100644 --- a/完整技术实现方案CLAUDE.md +++ b/fangan.md @@ -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='建行订单同步日志表'; ``` --- diff --git a/public/assets/js/ccblife-bridge.js b/public/assets/js/ccblife-bridge.js new file mode 100644 index 0000000..d31db03 --- /dev/null +++ b/public/assets/js/ccblife-bridge.js @@ -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); \ No newline at end of file diff --git a/public/ccblife-demo.html b/public/ccblife-demo.html new file mode 100644 index 0000000..7d5639e --- /dev/null +++ b/public/ccblife-demo.html @@ -0,0 +1,411 @@ + + +
+ + +处理中...
+