mirror of
https://gitee.com/liuxioabin/fengketrade.git
synced 2026-04-17 12:57:32 +08:00
技术方案
This commit is contained in:
parent
6a726b314e
commit
3af024ae75
80
addons/shopro/config/ccblife.php
Normal file
80
addons/shopro/config/ccblife.php
Normal file
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* 建行生活对接配置文件
|
||||
*
|
||||
* 重要说明:
|
||||
* - 建行只提供生产环境,没有测试环境
|
||||
* - 配置优先从.env读取,如果没有则使用默认值
|
||||
* - 请在.env文件中配置敏感信息(密钥等)
|
||||
*
|
||||
* @author Billy
|
||||
* @date 2025-01-16
|
||||
*/
|
||||
|
||||
use think\Env;
|
||||
|
||||
return [
|
||||
// API基础地址 (生产环境)
|
||||
'api_base_url' => Env::get('ccb.api_base_url', 'https://yunbusiness.ccb.com/tp_service/txCtrl/server'),
|
||||
|
||||
// 收银台地址 (生产环境)
|
||||
'cashier_url' => Env::get('ccb.cashier_url', 'https://yunbusiness.ccb.com/clp_service/txCtrl'),
|
||||
|
||||
// 交易代码映射
|
||||
'tx_codes' => [
|
||||
'order_push' => 'A3341TP01', // 订单推送
|
||||
'order_update' => 'A3341TP02', // 订单更新
|
||||
'order_query' => 'A3341TP03', // 订单查询
|
||||
'order_refund' => 'A3341TP04', // 订单退款
|
||||
],
|
||||
|
||||
// 服务方信息(已确认)
|
||||
'service_id' => Env::get('ccb.service_id', 'YS44000098000600'),
|
||||
|
||||
// 商户信息(已确认)
|
||||
'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', ''),
|
||||
|
||||
// HTTP请求配置
|
||||
'http' => [
|
||||
'timeout' => 30, // 超时时间(秒)
|
||||
'retry_times' => 3, // 重试次数
|
||||
'retry_delay' => [1, 2, 5], // 重试间隔(秒)
|
||||
],
|
||||
|
||||
// 支付配置
|
||||
'payment' => [
|
||||
'currency_code' => '01', // 币种: 01-人民币
|
||||
'tx_code' => '520100', // 支付交易码
|
||||
'third_app_info' => 'comccbpay1234567890cloudmerchant',
|
||||
'timeout_minutes' => 30, // 支付超时时间(分钟)
|
||||
],
|
||||
|
||||
// 日志配置
|
||||
'log' => [
|
||||
'enabled' => true,
|
||||
'level' => Env::get('ccb.log_level', 'info'), // debug, info, warning, error
|
||||
'path' => runtime_path() . 'log/ccblife/',
|
||||
],
|
||||
|
||||
// 安全配置
|
||||
'security' => [
|
||||
'encrypt_enabled' => true, // 是否启用加密
|
||||
'sign_enabled' => true, // 是否启用签名
|
||||
'verify_sign' => true, // 是否验证响应签名
|
||||
],
|
||||
|
||||
// 商户信息
|
||||
'merchant' => [
|
||||
'name' => Env::get('ccb.merchant_name', '商户名称'),
|
||||
'logo_url' => Env::get('ccb.merchant_logo', ''),
|
||||
'order_detail_url' => Env::get('app_url', 'http://fengketrade.test') . '/pages/order/detail?id=',
|
||||
],
|
||||
];
|
||||
148
addons/shopro/controller/Ccblife.php
Normal file
148
addons/shopro/controller/Ccblife.php
Normal file
@ -0,0 +1,148 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\controller;
|
||||
|
||||
use addons\shopro\controller\Common;
|
||||
use app\admin\model\User as UserModel;
|
||||
use think\Exception;
|
||||
use think\Log;
|
||||
|
||||
/**
|
||||
* 建行生活用户登录控制器
|
||||
*
|
||||
* 功能:
|
||||
* - 建行用户自动登录 (替代原有登录注册)
|
||||
* - 用户信息同步
|
||||
*
|
||||
* @author Billy
|
||||
* @date 2025-01-16
|
||||
*/
|
||||
class Ccblife extends Common
|
||||
{
|
||||
/**
|
||||
* 不需要登录的方法
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['autoLogin'];
|
||||
|
||||
/**
|
||||
* 不需要权限的方法
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedRight = ['*'];
|
||||
|
||||
/**
|
||||
* 建行用户自动登录
|
||||
*
|
||||
* 说明:
|
||||
* 1. 弃用商城原有的登录注册功能
|
||||
* 2. H5在建行App内打开时,自动通过JSBridge获取建行用户信息
|
||||
* 3. 如果建行用户ID已存在则登录,不存在则自动创建商城用户并绑定
|
||||
* 4. 返回商城Token用于后续API调用
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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),
|
||||
],
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('[建行登录] 登录失败 ' . json_encode([
|
||||
'error' => $e->getMessage(),
|
||||
'trace' => $e->getTraceAsString(),
|
||||
], JSON_UNESCAPED_UNICODE));
|
||||
|
||||
$this->error('登录失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 手机号脱敏
|
||||
*
|
||||
* @param string $mobile 手机号
|
||||
* @return string 脱敏后的手机号
|
||||
*/
|
||||
private function maskMobile($mobile)
|
||||
{
|
||||
if (empty($mobile) || strlen($mobile) !== 11) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return substr($mobile, 0, 3) . '****' . substr($mobile, -4);
|
||||
}
|
||||
}
|
||||
305
addons/shopro/controller/Ccbpayment.php
Normal file
305
addons/shopro/controller/Ccbpayment.php
Normal file
@ -0,0 +1,305 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\controller;
|
||||
|
||||
use addons\shopro\controller\Common;
|
||||
use addons\shopro\library\ccblife\CcbPaymentService;
|
||||
use addons\shopro\library\ccblife\CcbOrderService;
|
||||
use app\admin\model\shopro\order\Order as OrderModel;
|
||||
use think\Db;
|
||||
use think\Exception;
|
||||
use think\Log;
|
||||
|
||||
/**
|
||||
* 建行支付控制器
|
||||
*
|
||||
* 功能:
|
||||
* - 生成支付串
|
||||
* - 处理支付回调
|
||||
* - 验证支付结果
|
||||
*
|
||||
* @author Billy
|
||||
* @date 2025-01-16
|
||||
*/
|
||||
class Ccbpayment extends Common
|
||||
{
|
||||
/**
|
||||
* 不需要登录的方法 (支付回调不需要登录)
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedLogin = ['callback', 'notify'];
|
||||
|
||||
/**
|
||||
* 不需要权限的方法
|
||||
* @var array
|
||||
*/
|
||||
protected $noNeedRight = ['*'];
|
||||
|
||||
/**
|
||||
* 支付服务
|
||||
* @var CcbPaymentService
|
||||
*/
|
||||
private $paymentService;
|
||||
|
||||
/**
|
||||
* 订单服务
|
||||
* @var CcbOrderService
|
||||
*/
|
||||
private $orderService;
|
||||
|
||||
/**
|
||||
* 初始化
|
||||
*/
|
||||
public function _initialize()
|
||||
{
|
||||
parent::_initialize();
|
||||
|
||||
$this->paymentService = new CcbPaymentService();
|
||||
$this->orderService = new CcbOrderService();
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成支付串
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function createPayment()
|
||||
{
|
||||
try {
|
||||
// 1. 获取订单ID
|
||||
$orderId = $this->request->post('order_id', 0);
|
||||
|
||||
if (empty($orderId)) {
|
||||
$this->error('订单ID不能为空');
|
||||
}
|
||||
|
||||
// 2. 查询订单
|
||||
$order = OrderModel::where('id', $orderId)
|
||||
->where('user_id', $this->auth->id)
|
||||
->find();
|
||||
|
||||
if (!$order) {
|
||||
$this->error('订单不存在');
|
||||
}
|
||||
|
||||
// 3. 检查订单状态
|
||||
if ($order['status'] != 'unpaid') {
|
||||
$this->error('订单已支付或已关闭');
|
||||
}
|
||||
|
||||
// 4. 生成支付串
|
||||
$result = $this->paymentService->generatePaymentString($order->toArray());
|
||||
|
||||
if (!$result['success']) {
|
||||
$this->error('支付串生成失败: ' . $result['error']);
|
||||
}
|
||||
|
||||
// 5. 保存支付流水号到订单
|
||||
$order->ccb_pay_flow_id = $result['pay_flow_id'];
|
||||
$order->save();
|
||||
|
||||
// 6. 记录支付日志
|
||||
$this->savePaymentLog($order, $result['payment_string'], $result['pay_flow_id']);
|
||||
|
||||
// 7. 返回支付串
|
||||
$this->success('支付串生成成功', [
|
||||
'payment_string' => $result['payment_string'],
|
||||
'order_id' => $order->id,
|
||||
'order_sn' => $order->order_sn,
|
||||
'pay_flow_id' => $result['pay_flow_id'],
|
||||
'amount' => $order->pay_amount,
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('[建行支付] 生成支付串失败 order_id:' . ($orderId ?? 0) . ' error:' . $e->getMessage());
|
||||
|
||||
$this->error('生成支付串失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 支付回调 (前端调用)
|
||||
*
|
||||
* 说明:
|
||||
* 前端调起支付后,建行App会跳转回H5页面
|
||||
* H5页面需要调用此接口通知后端支付成功
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function callback()
|
||||
{
|
||||
try {
|
||||
// 1. 获取参数
|
||||
$orderId = $this->request->post('order_id', 0);
|
||||
$transId = $this->request->post('trans_id', '');
|
||||
$payTime = $this->request->post('pay_time', '');
|
||||
|
||||
if (empty($orderId)) {
|
||||
$this->error('订单ID不能为空');
|
||||
}
|
||||
|
||||
// 2. 查询订单
|
||||
$order = OrderModel::where('id', $orderId)->find();
|
||||
|
||||
if (!$order) {
|
||||
$this->error('订单不存在');
|
||||
}
|
||||
|
||||
// 3. 检查订单状态
|
||||
if ($order['status'] == 'paid' || $order['status'] == 'completed') {
|
||||
$this->success('订单已支付', [
|
||||
'order_id' => $order->id,
|
||||
'order_sn' => $order->order_sn,
|
||||
'status' => $order['status'],
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// 4. 验证支付结果 (调用建行查询接口)
|
||||
$verifyResult = $this->paymentService->verifyPayment($order->order_sn);
|
||||
|
||||
if (!$verifyResult) {
|
||||
$this->error('支付验证失败,请稍后再试');
|
||||
}
|
||||
|
||||
// 5. 更新订单状态
|
||||
Db::startTrans();
|
||||
try {
|
||||
$order->status = 'paid';
|
||||
$order->paid_time = time() * 1000; // Shopro使用毫秒时间戳
|
||||
$order->save();
|
||||
|
||||
// 6. 推送订单状态到建行
|
||||
$this->pushOrderToCcb($order);
|
||||
|
||||
// 7. 更新支付日志
|
||||
$this->updatePaymentLog($order->ccb_pay_flow_id, [
|
||||
'status' => 1,
|
||||
'pay_time' => time(),
|
||||
'trans_id' => $transId,
|
||||
]);
|
||||
|
||||
Db::commit();
|
||||
|
||||
Log::info('[建行支付] 支付成功 order_id:' . $order->id . ' order_sn:' . $order->order_sn . ' trans_id:' . $transId);
|
||||
|
||||
$this->success('支付成功', [
|
||||
'order_id' => $order->id,
|
||||
'order_sn' => $order->order_sn,
|
||||
'status' => 'paid',
|
||||
]);
|
||||
|
||||
} catch (Exception $e) {
|
||||
Db::rollback();
|
||||
throw $e;
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('[建行支付] 支付回调失败 order_id:' . ($orderId ?? 0) . ' error:' . $e->getMessage());
|
||||
|
||||
$this->error('支付处理失败: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 建行支付通知 (建行服务器回调)
|
||||
*
|
||||
* 说明:
|
||||
* 建行支付成功后,会向notify_url发送支付通知
|
||||
* 这是服务器到服务器的回调,需要验签
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function notify()
|
||||
{
|
||||
try {
|
||||
// TODO: 实现建行支付通知处理逻辑
|
||||
// 1. 接收建行推送的支付结果
|
||||
// 2. 验签
|
||||
// 3. 更新订单状态
|
||||
// 4. 返回success给建行
|
||||
|
||||
$this->success('SUCCESS');
|
||||
|
||||
} catch (Exception $e) {
|
||||
Log::error('[建行通知] 处理失败 error:' . $e->getMessage());
|
||||
|
||||
$this->error('FAIL');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送订单到建行
|
||||
*
|
||||
* @param object $order 订单对象
|
||||
* @return void
|
||||
*/
|
||||
private function pushOrderToCcb($order)
|
||||
{
|
||||
// 构造订单数据 (使用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_pay_flow_id' => $order->ccb_pay_flow_id,
|
||||
'goods_list' => [], // TODO: 从订单详情中获取商品列表
|
||||
];
|
||||
|
||||
// 推送到建行
|
||||
$result = $this->orderService->pushOrder($orderData);
|
||||
|
||||
if (!$result['success']) {
|
||||
Log::warning('[建行推送] 订单推送失败 order_id:' . $order->id . ' error:' . ($result['error'] ?? ''));
|
||||
} else {
|
||||
// 更新同步状态
|
||||
$order->ccb_sync_status = 1;
|
||||
$order->ccb_sync_time = time();
|
||||
$order->save();
|
||||
|
||||
Log::info('[建行推送] 订单推送成功 order_id:' . $order->id . ' order_sn:' . $order->order_sn);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存支付日志
|
||||
*
|
||||
* @param object $order 订单对象
|
||||
* @param string $paymentString 支付串
|
||||
* @param string $payFlowId 支付流水号
|
||||
* @return void
|
||||
*/
|
||||
private function savePaymentLog($order, $paymentString, $payFlowId)
|
||||
{
|
||||
Db::name('ccb_payment_log')->insert([
|
||||
'order_id' => $order->id,
|
||||
'order_sn' => $order->order_sn,
|
||||
'pay_flow_id' => $payFlowId,
|
||||
'payment_string' => $paymentString,
|
||||
'user_id' => $order->user_id,
|
||||
'ccb_user_id' => $order->ccb_user_id,
|
||||
'amount' => $order->pay_amount,
|
||||
'status' => 0, // 待支付
|
||||
'create_time' => time(),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新支付日志
|
||||
*
|
||||
* @param string $payFlowId 支付流水号
|
||||
* @param array $data 更新数据
|
||||
* @return void
|
||||
*/
|
||||
private function updatePaymentLog($payFlowId, $data)
|
||||
{
|
||||
Db::name('ccb_payment_log')
|
||||
->where('pay_flow_id', $payFlowId)
|
||||
->update($data);
|
||||
}
|
||||
}
|
||||
382
addons/shopro/library/ccblife/CcbEncryption.php
Normal file
382
addons/shopro/library/ccblife/CcbEncryption.php
Normal file
@ -0,0 +1,382 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\ccblife;
|
||||
|
||||
use think\Exception;
|
||||
|
||||
/**
|
||||
* 建行生活加密解密核心类
|
||||
*
|
||||
* 功能:
|
||||
* - RSA加密与解密
|
||||
* - MD5签名生成与验证
|
||||
* - 报文构造与解析
|
||||
* - 交易流水号生成
|
||||
*
|
||||
* @author Billy
|
||||
* @date 2025-01-16
|
||||
*/
|
||||
class CcbEncryption
|
||||
{
|
||||
/**
|
||||
* 配置信息
|
||||
* @var array
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* 服务方私钥
|
||||
* @var string
|
||||
*/
|
||||
private $privateKey;
|
||||
|
||||
/**
|
||||
* 服务方公钥
|
||||
* @var string
|
||||
*/
|
||||
private $publicKey;
|
||||
|
||||
/**
|
||||
* 建行平台公钥
|
||||
* @var string
|
||||
*/
|
||||
private $platformPublicKey;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param array $config 配置数组
|
||||
* @throws Exception
|
||||
*/
|
||||
public function __construct($config = [])
|
||||
{
|
||||
// 如果没有传入配置,从配置文件读取
|
||||
if (empty($config)) {
|
||||
$config = config('ccblife');
|
||||
}
|
||||
|
||||
$this->config = $config;
|
||||
|
||||
// 加载密钥
|
||||
$this->loadKeys();
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载密钥
|
||||
*
|
||||
* @throws Exception
|
||||
*/
|
||||
private function loadKeys()
|
||||
{
|
||||
$this->privateKey = $this->config['private_key'] ?? '';
|
||||
$this->publicKey = $this->config['public_key'] ?? '';
|
||||
$this->platformPublicKey = $this->config['platform_public_key'] ?? '';
|
||||
|
||||
if (empty($this->privateKey)) {
|
||||
throw new Exception('服务方私钥未配置');
|
||||
}
|
||||
|
||||
if (empty($this->platformPublicKey)) {
|
||||
throw new Exception('建行平台公钥未配置');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA加密 (使用建行平台公钥加密)
|
||||
*
|
||||
* @param string $data 原始数据
|
||||
* @return string BASE64编码的加密数据
|
||||
* @throws Exception
|
||||
*/
|
||||
public function rsaEncrypt($data)
|
||||
{
|
||||
// 格式化公钥
|
||||
$publicKey = $this->formatKey($this->platformPublicKey, 'PUBLIC');
|
||||
|
||||
// 获取公钥资源
|
||||
$pubKeyId = openssl_pkey_get_public($publicKey);
|
||||
if (!$pubKeyId) {
|
||||
throw new Exception('建行平台公钥格式错误');
|
||||
}
|
||||
|
||||
// RSA加密 (分段加密,每段117字节)
|
||||
$encrypted = '';
|
||||
$dataLen = strlen($data);
|
||||
$chunkSize = 117; // 1024位RSA密钥,每次最多加密117字节
|
||||
|
||||
for ($i = 0; $i < $dataLen; $i += $chunkSize) {
|
||||
$chunk = substr($data, $i, $chunkSize);
|
||||
$encryptedChunk = '';
|
||||
|
||||
if (!openssl_public_encrypt($chunk, $encryptedChunk, $pubKeyId)) {
|
||||
throw new Exception('RSA加密失败: ' . openssl_error_string());
|
||||
}
|
||||
|
||||
$encrypted .= $encryptedChunk;
|
||||
}
|
||||
|
||||
// BASE64编码并去除换行符
|
||||
return str_replace(["\r", "\n"], '', base64_encode($encrypted));
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA解密 (使用服务方私钥解密)
|
||||
*
|
||||
* @param string $data BASE64编码的加密数据
|
||||
* @return string 解密后的原始数据
|
||||
* @throws Exception
|
||||
*/
|
||||
public function rsaDecrypt($data)
|
||||
{
|
||||
// 格式化私钥
|
||||
$privateKey = $this->formatKey($this->privateKey, 'PRIVATE');
|
||||
|
||||
// 获取私钥资源
|
||||
$privKeyId = openssl_pkey_get_private($privateKey);
|
||||
if (!$privKeyId) {
|
||||
throw new Exception('服务方私钥格式错误');
|
||||
}
|
||||
|
||||
// BASE64解码
|
||||
$encrypted = base64_decode($data);
|
||||
|
||||
// RSA解密 (分段解密,每段128字节)
|
||||
$decrypted = '';
|
||||
$encryptedLen = strlen($encrypted);
|
||||
$chunkSize = 128; // 1024位RSA密钥,密文长度为128字节
|
||||
|
||||
for ($i = 0; $i < $encryptedLen; $i += $chunkSize) {
|
||||
$chunk = substr($encrypted, $i, $chunkSize);
|
||||
$decryptedChunk = '';
|
||||
|
||||
if (!openssl_private_decrypt($chunk, $decryptedChunk, $privKeyId)) {
|
||||
throw new Exception('RSA解密失败: ' . openssl_error_string());
|
||||
}
|
||||
|
||||
$decrypted .= $decryptedChunk;
|
||||
}
|
||||
|
||||
return $decrypted;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成MD5签名
|
||||
*
|
||||
* @param string $data 原始数据(JSON字符串)
|
||||
* @return string 32位大写MD5签名 (与Java保持一致)
|
||||
*/
|
||||
public function generateSign($data)
|
||||
{
|
||||
// 签名规则: MD5(原始数据 + 服务方私钥)
|
||||
$signString = $data . $this->privateKey;
|
||||
return strtoupper(md5($signString)); // 转为大写,与Java一致
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证MD5签名
|
||||
*
|
||||
* @param string $data 原始数据
|
||||
* @param string $sign 签名
|
||||
* @return bool 验证结果
|
||||
*/
|
||||
public function verifySign($data, $sign)
|
||||
{
|
||||
$expectedSign = $this->generateSign($data);
|
||||
return $expectedSign === $sign;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造完整加密报文
|
||||
*
|
||||
* @param string $txCode 交易代码 (如: A3341TP01)
|
||||
* @param array $bodyData 业务数据
|
||||
* @return array 加密后的完整报文 ['cnt' => '...', 'mac' => '...', 'svcid' => '...']
|
||||
* @throws Exception
|
||||
*/
|
||||
public function buildEncryptedMessage($txCode, $bodyData)
|
||||
{
|
||||
// 1. 构造原始报文
|
||||
$txSeq = $this->generateTransSeq();
|
||||
$txTime = date('YmdHis');
|
||||
|
||||
$message = [
|
||||
'CLD_HEADER' => [
|
||||
'CLD_TX_CHNL' => $this->config['service_id'],
|
||||
'CLD_TX_TIME' => $txTime,
|
||||
'CLD_TX_CODE' => $txCode,
|
||||
'CLD_TX_SEQ' => $txSeq,
|
||||
],
|
||||
'CLD_BODY' => $bodyData,
|
||||
];
|
||||
|
||||
// 2. 转换为JSON (不转义中文和斜杠)
|
||||
$jsonData = json_encode($message, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if ($jsonData === false) {
|
||||
throw new Exception('JSON编码失败: ' . json_last_error_msg());
|
||||
}
|
||||
|
||||
// 3. RSA加密
|
||||
$encryptedData = $this->rsaEncrypt($jsonData);
|
||||
|
||||
// 4. 生成MD5签名
|
||||
$sign = $this->generateSign($jsonData);
|
||||
|
||||
// 5. 组装最终报文
|
||||
return [
|
||||
'cnt' => $encryptedData,
|
||||
'mac' => $sign,
|
||||
'svcid' => $this->config['service_id'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析响应报文
|
||||
*
|
||||
* @param string $response 响应JSON字符串
|
||||
* @return array 解析后的业务数据
|
||||
* @throws Exception
|
||||
*/
|
||||
public function parseResponse($response)
|
||||
{
|
||||
// 1. 解析JSON
|
||||
$result = json_decode($response, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new Exception('响应不是有效的JSON: ' . json_last_error_msg());
|
||||
}
|
||||
|
||||
// 2. 检查是否包含加密字段
|
||||
if (!isset($result['cnt']) || !isset($result['mac'])) {
|
||||
// 可能是错误响应,直接返回
|
||||
return $result;
|
||||
}
|
||||
|
||||
// 3. 验证签名 (如果启用)
|
||||
if ($this->config['security']['verify_sign'] ?? true) {
|
||||
// 解密后验证签名
|
||||
$decryptedData = $this->rsaDecrypt($result['cnt']);
|
||||
|
||||
if (!$this->verifySign($decryptedData, $result['mac'])) {
|
||||
throw new Exception('响应签名验证失败');
|
||||
}
|
||||
|
||||
// 解析业务数据
|
||||
$businessData = json_decode($decryptedData, true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
throw new Exception('业务数据解析失败: ' . json_last_error_msg());
|
||||
}
|
||||
|
||||
return $businessData;
|
||||
} else {
|
||||
// 不验证签名,直接解密
|
||||
$decryptedData = $this->rsaDecrypt($result['cnt']);
|
||||
return json_decode($decryptedData, true);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成唯一交易流水号
|
||||
*
|
||||
* 格式: YmdHis + 微秒 + 4位随机数
|
||||
* 示例: 202501161200001234567890
|
||||
*
|
||||
* @return string 24位交易流水号
|
||||
*/
|
||||
public function generateTransSeq()
|
||||
{
|
||||
$date = date('YmdHis'); // 14位
|
||||
$microtime = substr(microtime(), 2, 6); // 6位微秒
|
||||
$random = str_pad(mt_rand(0, 9999), 4, '0', STR_PAD_LEFT); // 4位随机数
|
||||
|
||||
return $date . $microtime . $random; // 24位
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成支付流水号
|
||||
*
|
||||
* 格式: PAY + YmdHis + 8位随机数
|
||||
* 示例: PAY2025011612000012345678
|
||||
*
|
||||
* @return string 支付流水号
|
||||
*/
|
||||
public function generatePayFlowId()
|
||||
{
|
||||
$prefix = 'PAY';
|
||||
$date = date('YmdHis'); // 14位
|
||||
$random = str_pad(mt_rand(0, 99999999), 8, '0', STR_PAD_LEFT); // 8位随机数
|
||||
|
||||
return $prefix . $date . $random; // 3 + 14 + 8 = 25位
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化密钥 (添加PEM头尾)
|
||||
*
|
||||
* 密钥格式说明:
|
||||
* - 公钥: X.509格式 (BEGIN PUBLIC KEY)
|
||||
* - 私钥: PKCS#8格式 (BEGIN PRIVATE KEY) - 与Java保持一致
|
||||
*
|
||||
* @param string $key 密钥内容 (BASE64字符串,不含头尾)
|
||||
* @param string $type 类型: PUBLIC 或 PRIVATE
|
||||
* @return string 格式化后的PEM密钥
|
||||
*/
|
||||
private function formatKey($key, $type = 'PUBLIC')
|
||||
{
|
||||
// 如果已经包含头尾,直接返回
|
||||
if (strpos($key, '-----BEGIN') !== false) {
|
||||
return $key;
|
||||
}
|
||||
|
||||
// 添加头尾 (注意: 私钥使用PKCS#8格式,与Java的PKCS8EncodedKeySpec一致)
|
||||
if ($type === 'PUBLIC') {
|
||||
$header = "-----BEGIN PUBLIC KEY-----\n";
|
||||
$footer = "\n-----END PUBLIC KEY-----";
|
||||
} else {
|
||||
// 使用PKCS#8格式 (不是RSA PRIVATE KEY)
|
||||
$header = "-----BEGIN PRIVATE KEY-----\n";
|
||||
$footer = "\n-----END PRIVATE KEY-----";
|
||||
}
|
||||
|
||||
// 每64个字符换行
|
||||
$key = chunk_split($key, 64, "\n");
|
||||
|
||||
return $header . $key . $footer;
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成支付串签名
|
||||
*
|
||||
* 用于生成建行支付串的MAC签名
|
||||
*
|
||||
* @param array $params 支付参数数组
|
||||
* @return string 32位大写MD5签名 (与Java保持一致)
|
||||
*/
|
||||
public function generatePaymentSign($params)
|
||||
{
|
||||
// 1. 按参数名ASCII排序
|
||||
ksort($params);
|
||||
|
||||
// 2. 拼接成字符串: key1=value1&key2=value2&...
|
||||
$signString = http_build_query($params);
|
||||
|
||||
// 3. 追加平台公钥
|
||||
$signString .= '&PLATFORMPUB=' . $this->platformPublicKey;
|
||||
|
||||
// 4. 生成MD5签名 (转为大写,与Java一致)
|
||||
return strtoupper(md5($signString . $this->privateKey));
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密商户公钥 (用于支付串的ENCPUB字段)
|
||||
*
|
||||
* @return string BASE64编码的加密公钥
|
||||
* @throws Exception
|
||||
*/
|
||||
public function encryptMerchantPublicKey()
|
||||
{
|
||||
if (empty($this->publicKey)) {
|
||||
throw new Exception('服务方公钥未配置');
|
||||
}
|
||||
|
||||
// 使用建行平台公钥加密商户公钥
|
||||
return $this->rsaEncrypt($this->publicKey);
|
||||
}
|
||||
}
|
||||
287
addons/shopro/library/ccblife/CcbHttpClient.php
Normal file
287
addons/shopro/library/ccblife/CcbHttpClient.php
Normal file
@ -0,0 +1,287 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\ccblife;
|
||||
|
||||
use think\Exception;
|
||||
use think\Log;
|
||||
|
||||
/**
|
||||
* 建行生活HTTP客户端类
|
||||
*
|
||||
* 功能:
|
||||
* - 发送HTTP请求到建行
|
||||
* - 处理HTTP响应
|
||||
* - 失败重试机制
|
||||
* - 超时控制
|
||||
*
|
||||
* @author Billy
|
||||
* @date 2025-01-16
|
||||
*/
|
||||
class CcbHttpClient
|
||||
{
|
||||
/**
|
||||
* 配置信息
|
||||
* @var array
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* 加密实例
|
||||
* @var CcbEncryption
|
||||
*/
|
||||
private $encryption;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param array $config 配置数组
|
||||
*/
|
||||
public function __construct($config = [])
|
||||
{
|
||||
if (empty($config)) {
|
||||
$config = config('ccblife');
|
||||
}
|
||||
|
||||
$this->config = $config;
|
||||
$this->encryption = new CcbEncryption($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送请求到建行
|
||||
*
|
||||
* @param string $txCode 交易代码 (如: A3341TP01)
|
||||
* @param array $bodyData 业务数据
|
||||
* @return array 解密后的响应数据
|
||||
* @throws Exception
|
||||
*/
|
||||
public function request($txCode, $bodyData)
|
||||
{
|
||||
// 记录开始时间
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送POST请求
|
||||
*
|
||||
* @param string $url 请求URL
|
||||
* @param array $data 请求数据
|
||||
* @return string 响应内容
|
||||
* @throws Exception
|
||||
*/
|
||||
private function post($url, $data)
|
||||
{
|
||||
// 初始化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',
|
||||
]);
|
||||
|
||||
// 如果是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);
|
||||
curl_close($ch);
|
||||
|
||||
// 检查CURL错误
|
||||
if ($error) {
|
||||
throw new Exception('CURL错误: ' . $error);
|
||||
}
|
||||
|
||||
// 检查HTTP状态码
|
||||
if ($httpCode == 404) {
|
||||
throw new Exception('接口404,请检查请求头是否包含Accept和Content-Type');
|
||||
}
|
||||
|
||||
if ($httpCode !== 200) {
|
||||
throw new Exception('HTTP错误码: ' . $httpCode . ', 响应: ' . $response);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重试机制
|
||||
*
|
||||
* @param callable $callable 要执行的函数
|
||||
* @param int|null $maxRetries 最大重试次数
|
||||
* @return mixed 执行结果
|
||||
* @throws Exception
|
||||
*/
|
||||
private function retry($callable, $maxRetries = null)
|
||||
{
|
||||
if ($maxRetries === null) {
|
||||
$maxRetries = $this->config['http']['retry_times'] ?? 3;
|
||||
}
|
||||
|
||||
$delays = $this->config['http']['retry_delay'] ?? [1, 2, 5];
|
||||
$lastException = null;
|
||||
|
||||
for ($i = 0; $i <= $maxRetries; $i++) {
|
||||
try {
|
||||
return $callable();
|
||||
} catch (Exception $e) {
|
||||
$lastException = $e;
|
||||
|
||||
// 如果是最后一次尝试,不再重试
|
||||
if ($i >= $maxRetries) {
|
||||
break;
|
||||
}
|
||||
|
||||
// 等待后重试
|
||||
$delay = $delays[$i] ?? 5;
|
||||
$this->logRetry($i + 1, $maxRetries, $delay, $e->getMessage());
|
||||
sleep($delay);
|
||||
}
|
||||
}
|
||||
|
||||
throw new Exception('请求失败,已重试' . $maxRetries . '次: ' . $lastException->getMessage());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取API地址
|
||||
*
|
||||
* @param string $txCode 交易代码
|
||||
* @return string 完整API地址
|
||||
*/
|
||||
private function getApiUrl($txCode)
|
||||
{
|
||||
$baseUrl = $this->config['api_base_url'] ?? '';
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录请求日志
|
||||
*
|
||||
* @param string $txCode 交易代码
|
||||
* @param array $bodyData 业务数据
|
||||
* @param array $requestData 加密后的请求数据
|
||||
*/
|
||||
private function logRequest($txCode, $bodyData, $requestData)
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录响应日志
|
||||
*
|
||||
* @param string $txCode 交易代码
|
||||
* @param array $result 响应数据
|
||||
* @param float $costTime 耗时(毫秒)
|
||||
*/
|
||||
private function logResponse($txCode, $result, $costTime)
|
||||
{
|
||||
if (!($this->config['log']['enabled'] ?? true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log::info('[建行响应] ' . $txCode . ' ret_code:' . ($result['CLD_HEADER']['RET_CODE'] ?? '') . ' ret_msg:' . ($result['CLD_HEADER']['RET_MSG'] ?? '') . ' cost_time:' . $costTime . 'ms');
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录错误日志
|
||||
*
|
||||
* @param string $txCode 交易代码
|
||||
* @param string $errorMsg 错误信息
|
||||
* @param float $costTime 耗时(毫秒)
|
||||
*/
|
||||
private function logError($txCode, $errorMsg, $costTime)
|
||||
{
|
||||
if (!($this->config['log']['enabled'] ?? true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log::error('[建行错误] ' . $txCode . ' error:' . $errorMsg . ' cost_time:' . $costTime . 'ms');
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录重试日志
|
||||
*
|
||||
* @param int $currentRetry 当前重试次数
|
||||
* @param int $maxRetries 最大重试次数
|
||||
* @param int $delay 延迟秒数
|
||||
* @param string $reason 重试原因
|
||||
*/
|
||||
private function logRetry($currentRetry, $maxRetries, $delay, $reason)
|
||||
{
|
||||
if (!($this->config['log']['enabled'] ?? true)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Log::warning('[建行重试] retry:' . $currentRetry . '/' . $maxRetries . ' delay:' . $delay . 's reason:' . $reason);
|
||||
}
|
||||
}
|
||||
253
addons/shopro/library/ccblife/CcbOrderService.php
Normal file
253
addons/shopro/library/ccblife/CcbOrderService.php
Normal file
@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\ccblife;
|
||||
|
||||
use think\Exception;
|
||||
|
||||
/**
|
||||
* 建行订单服务类
|
||||
*
|
||||
* 功能:
|
||||
* - 订单推送到建行 (A3341TP01)
|
||||
* - 订单状态更新 (A3341TP02)
|
||||
* - 订单查询 (A3341TP03)
|
||||
* - 订单退款 (A3341TP04)
|
||||
*
|
||||
* @author Billy
|
||||
* @date 2025-01-16
|
||||
*/
|
||||
class CcbOrderService
|
||||
{
|
||||
/**
|
||||
* HTTP客户端
|
||||
* @var CcbHttpClient
|
||||
*/
|
||||
private $httpClient;
|
||||
|
||||
/**
|
||||
* 配置信息
|
||||
* @var array
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param array $config 配置数组
|
||||
*/
|
||||
public function __construct($config = [])
|
||||
{
|
||||
if (empty($config)) {
|
||||
$config = config('ccblife');
|
||||
}
|
||||
|
||||
$this->config = $config;
|
||||
$this->httpClient = new CcbHttpClient($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 推送订单到建行 (A3341TP01)
|
||||
*
|
||||
* @param array $orderData 订单数据
|
||||
* @return array 返回结果 ['success' => bool, 'data' => array, 'error' => string]
|
||||
*/
|
||||
public function pushOrder($orderData)
|
||||
{
|
||||
try {
|
||||
// 构造订单推送数据
|
||||
$bodyData = $this->buildOrderPushData($orderData);
|
||||
|
||||
// 发送请求
|
||||
$result = $this->httpClient->request('A3341TP01', $bodyData);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $result,
|
||||
'ccb_discount_amt' => $result['CLD_BODY']['CCB_DISCOUNT_AMT'] ?? '0.00',
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新订单状态 (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 float $refundAmount 退款金额
|
||||
* @param string $refundReason 退款原因
|
||||
* @return array 返回结果
|
||||
*/
|
||||
public function refundOrder($orderId, $refundAmount, $refundReason = '')
|
||||
{
|
||||
try {
|
||||
$bodyData = [
|
||||
'ORDER_ID' => $orderId,
|
||||
'REFUND_AMT' => number_format($refundAmount, 2, '.', ''),
|
||||
'REFUND_REASON' => $refundReason ?: '用户申请退款',
|
||||
];
|
||||
|
||||
$result = $this->httpClient->request('A3341TP04', $bodyData);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'data' => $result,
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造订单推送数据 (根据建行接口规范)
|
||||
*
|
||||
* @param array $orderData 商城订单数据
|
||||
* @return array 建行接口要求的数据格式
|
||||
*/
|
||||
private function buildOrderPushData($orderData)
|
||||
{
|
||||
// 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, '.', ''),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// 商户信息
|
||||
$merchantConfig = $this->config['merchant'] ?? [];
|
||||
|
||||
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),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射订单状态
|
||||
*
|
||||
* Shopro订单状态 => 建行订单状态
|
||||
* Shopro: closed=交易关闭, cancel=已取消, unpaid=未支付, paid=已支付, completed=已完成, pending=待定
|
||||
* 建行: 0-待支付 1-已支付 2-已过期 3-失败 4-取消
|
||||
*
|
||||
* @param string $status Shopro订单状态
|
||||
* @return string 建行订单状态
|
||||
*/
|
||||
private function mapOrderStatus($status)
|
||||
{
|
||||
$map = [
|
||||
'unpaid' => '0', // 未支付 => 待支付
|
||||
'paid' => '1', // 已支付 => 已支付
|
||||
'completed' => '1', // 已完成 => 已支付
|
||||
'closed' => '2', // 交易关闭 => 已过期
|
||||
'cancel' => '4', // 已取消 => 取消
|
||||
'pending' => '0', // 待定 => 待支付
|
||||
];
|
||||
|
||||
return $map[$status] ?? '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射退款状态
|
||||
*
|
||||
* 0-无退款 1-申请 2-已退款 3-部分退款
|
||||
*
|
||||
* @param int $status 退款状态
|
||||
* @return string 建行退款状态
|
||||
*/
|
||||
private function mapRefundStatus($status)
|
||||
{
|
||||
$map = [
|
||||
0 => '0', // 无退款
|
||||
1 => '1', // 申请
|
||||
2 => '2', // 已退款
|
||||
3 => '3', // 部分退款
|
||||
];
|
||||
|
||||
return $map[$status] ?? '0';
|
||||
}
|
||||
}
|
||||
188
addons/shopro/library/ccblife/CcbPaymentService.php
Normal file
188
addons/shopro/library/ccblife/CcbPaymentService.php
Normal file
@ -0,0 +1,188 @@
|
||||
<?php
|
||||
|
||||
namespace addons\shopro\library\ccblife;
|
||||
|
||||
use think\Exception;
|
||||
|
||||
/**
|
||||
* 建行支付服务类
|
||||
*
|
||||
* 功能:
|
||||
* - 生成建行支付串
|
||||
* - 处理支付回调
|
||||
* - 验证支付结果
|
||||
*
|
||||
* @author Billy
|
||||
* @date 2025-01-16
|
||||
*/
|
||||
class CcbPaymentService
|
||||
{
|
||||
/**
|
||||
* 配置信息
|
||||
* @var array
|
||||
*/
|
||||
private $config;
|
||||
|
||||
/**
|
||||
* 加密实例
|
||||
* @var CcbEncryption
|
||||
*/
|
||||
private $encryption;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param array $config 配置数组
|
||||
*/
|
||||
public function __construct($config = [])
|
||||
{
|
||||
if (empty($config)) {
|
||||
$config = config('ccblife');
|
||||
}
|
||||
|
||||
$this->config = $config;
|
||||
$this->encryption = new CcbEncryption($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成支付串
|
||||
*
|
||||
* @param array $order 订单数据
|
||||
* @return array 返回结果 ['success' => bool, 'payment_string' => string, 'pay_flow_id' => string]
|
||||
*/
|
||||
public function generatePaymentString($order)
|
||||
{
|
||||
try {
|
||||
// 1. 生成支付流水号
|
||||
$payFlowId = $this->encryption->generatePayFlowId();
|
||||
|
||||
// 2. 构造支付参数
|
||||
$params = $this->buildPaymentParams($order, $payFlowId);
|
||||
|
||||
// 3. 生成支付串签名
|
||||
$mac = $this->encryption->generatePaymentSign($params);
|
||||
|
||||
// 4. 加密商户公钥
|
||||
$encPub = $this->encryption->encryptMerchantPublicKey();
|
||||
|
||||
// 5. 添加签名和加密字段
|
||||
$params['MAC'] = $mac;
|
||||
$params['PLATFORMID'] = $this->config['service_id'];
|
||||
$params['ENCPUB'] = $encPub;
|
||||
|
||||
// 6. 生成支付串
|
||||
$paymentString = http_build_query($params);
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'payment_string' => $paymentString,
|
||||
'pay_flow_id' => $payFlowId,
|
||||
];
|
||||
|
||||
} catch (Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $e->getMessage(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理支付回调
|
||||
*
|
||||
* @param array $callbackData 回调数据
|
||||
* @return array 返回结果
|
||||
*/
|
||||
public function handleCallback($callbackData)
|
||||
{
|
||||
// 支付回调处理逻辑
|
||||
// 建行支付成功后会通过notify_url回调
|
||||
|
||||
return [
|
||||
'success' => true,
|
||||
'trans_id' => $callbackData['trans_id'] ?? '',
|
||||
'order_id' => $callbackData['order_id'] ?? '',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证支付结果
|
||||
*
|
||||
* @param string $orderId 订单ID
|
||||
* @return bool 是否支付成功
|
||||
*/
|
||||
public function verifyPayment($orderId)
|
||||
{
|
||||
// 通过订单查询接口验证支付结果
|
||||
$orderService = new CcbOrderService($this->config);
|
||||
$result = $orderService->queryOrder($orderId);
|
||||
|
||||
if ($result['success']) {
|
||||
$orderStatus = $result['data']['CLD_BODY']['ORDER_STATUS'] ?? '0';
|
||||
return $orderStatus === '1'; // 1-已支付
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造支付参数
|
||||
*
|
||||
* @param array $order 订单数据
|
||||
* @param string $payFlowId 支付流水号
|
||||
* @return array 支付参数
|
||||
*/
|
||||
private function buildPaymentParams($order, $payFlowId)
|
||||
{
|
||||
// 获取支付配置
|
||||
$paymentConfig = $this->config['payment'] ?? [];
|
||||
|
||||
// 计算超时时间
|
||||
$timeoutMinutes = $paymentConfig['timeout_minutes'] ?? 30;
|
||||
$timeout = date('YmdHis', time() + $timeoutMinutes * 60);
|
||||
|
||||
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,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取客户端IP
|
||||
*
|
||||
* @return string IP地址
|
||||
*/
|
||||
private function getClientIp()
|
||||
{
|
||||
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';
|
||||
}
|
||||
|
||||
// 如果是多个IP,取第一个
|
||||
if (strpos($ip, ',') !== false) {
|
||||
$ips = explode(',', $ip);
|
||||
$ip = trim($ips[0]);
|
||||
}
|
||||
|
||||
return $ip;
|
||||
}
|
||||
}
|
||||
2279
完整技术实现方案CLAUDE.md
Normal file
2279
完整技术实现方案CLAUDE.md
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user