CRMEB 单商户对接汇付支付完整实现

一、开发前准备

1. 汇付支付所需配置项

在 CRMEB 项目的.env文件中添加汇付支付配置:

复制代码
# 汇付支付配置
HUIFU_MERCHANT_ID=你的汇付商户号
HUIFU_APP_ID=你的汇付应用ID
HUIFU_PRIVATE_KEY=你的汇付私钥(PKCS8格式)
HUIFU_PUBLIC_KEY=汇付公钥
HUIFU_GATEWAY=https://api.huifu.com/v1/(正式环境)
# 测试环境网关:https://api.test.huifu.com/v1/
HUIFU_NOTIFY_URL=https://你的域名/pay/huifu/notify(支付回调地址)
2. 核心依赖

确保项目安装了openssl扩展(用于签名验签)、guzzlehttp/guzzle(用于 HTTP 请求):

复制代码
composer require guzzlehttp/guzzle

二、核心代码实现

1. 汇付支付配置类(config/huifu.php)
复制代码
<?php
return [
    // 商户号
    'merchant_id' => env('HUIFU_MERCHANT_ID'),
    // 应用ID
    'app_id' => env('HUIFU_APP_ID'),
    // 私钥(PKCS8格式)
    'private_key' => env('HUIFU_PRIVATE_KEY'),
    // 汇付公钥
    'public_key' => env('HUIFU_PUBLIC_KEY'),
    // 支付网关
    'gateway' => env('HUIFU_GATEWAY'),
    // 回调地址
    'notify_url' => env('HUIFU_NOTIFY_URL'),
    // 签名算法
    'sign_type' => 'RSA2',
    // 字符编码
    'charset' => 'UTF-8',
    // 超时时间
    'timeout' => 30,
];
  1. 汇付支付核心工具类(app/Services/Pay/HuifuPayService.php)

    <?php

    namespace app\Services\Pay;

    use GuzzleHttp\Client;
    use think\facade\Config;

    class HuifuPayService
    {
    /**
    * 配置信息
    * @var array
    */
    private $config;

    复制代码
     /**
      * HTTP客户端
      * @var Client
      */
     private $httpClient;
    
     public function __construct()
     {
         // 加载配置
         $this->config = Config::get('huifu');
         // 初始化HTTP客户端
         $this->httpClient = new Client([
             'timeout' => $this->config['timeout'],
             'verify' => false, // 测试环境关闭SSL验证,正式环境开启
         ]);
     }
    
     /**
      * 生成RSA2签名
      * @param array $params 待签名参数
      * @return string
      */
     private function generateSign(array $params): string
     {
         // 1. 按ASCII码升序排序参数
         ksort($params);
         // 2. 拼接参数为key=value&key=value格式
         $signStr = '';
         foreach ($params as $k => $v) {
             if ($v !== '' && !is_null($v)) {
                 $signStr .= $k . '=' . $v . '&';
             }
         }
         $signStr = rtrim($signStr, '&');
    
         // 3. 使用私钥签名
         openssl_sign($signStr, $sign, $this->formatPriKey($this->config['private_key']), OPENSSL_ALGO_SHA256);
         return base64_encode($sign);
     }
    
     /**
      * 格式化私钥
      * @param string $priKey 原始私钥
      * @return string
      */
     private function formatPriKey(string $priKey): string
     {
         if (strpos($priKey, '-----BEGIN PRIVATE KEY-----') === false) {
             $priKey = "-----BEGIN PRIVATE KEY-----\n" .
                 chunk_split($priKey, 64, "\n") .
                 "-----END PRIVATE KEY-----";
         }
         return $priKey;
     }
    
     /**
      * 格式化公钥
      * @param string $pubKey 原始公钥
      * @return string
      */
     private function formatPubKey(string $pubKey): string
     {
         if (strpos($pubKey, '-----BEGIN PUBLIC KEY-----') === false) {
             $pubKey = "-----BEGIN PUBLIC KEY-----\n" .
                 chunk_split($pubKey, 64, "\n") .
                 "-----END PUBLIC KEY-----";
         }
         return $pubKey;
     }
    
     /**
      * 验证签名
      * @param array $params 待验证参数
      * @param string $sign 签名值
      * @return bool
      */
     public function verifySign(array $params, string $sign): bool
     {
         // 1. 移除sign参数
         unset($params['sign']);
         // 2. 排序并拼接参数
         ksort($params);
         $signStr = '';
         foreach ($params as $k => $v) {
             if ($v !== '' && !is_null($v)) {
                 $signStr .= $k . '=' . $v . '&';
             }
         }
         $signStr = rtrim($signStr, '&');
    
         // 3. 验签
         return openssl_verify($signStr, base64_decode($sign), $this->formatPubKey($this->config['public_key']), OPENSSL_ALGO_SHA256) === 1;
     }
    
     /**
      * 统一下单(微信H5/支付宝H5通用)
      * @param string $outTradeNo 商户订单号
      * @param string $totalAmount 订单金额(单位:元,保留2位小数)
      * @param string $subject 商品标题
      * @param string $payType 支付方式:WECHAT_H5/ALIPAY_H5
      * @param string $clientIp 客户端IP
      * @return array
      * @throws \GuzzleHttp\Exception\GuzzleException
      */
     public function unifiedOrder(string $outTradeNo, string $totalAmount, string $subject, string $payType, string $clientIp): array
     {
         // 1. 构造请求参数
         $params = [
             'merchant_id' => $this->config['merchant_id'],
             'app_id' => $this->config['app_id'],
             'out_trade_no' => $outTradeNo,
             'total_amount' => $totalAmount,
             'subject' => $subject,
             'pay_type' => $payType,
             'client_ip' => $clientIp,
             'notify_url' => $this->config['notify_url'],
             'charset' => $this->config['charset'],
             'sign_type' => $this->config['sign_type'],
             'timestamp' => date('YmdHis'),
             'version' => '1.0',
         ];
    
         // 2. 生成签名
         $params['sign'] = $this->generateSign($params);
    
         // 3. 发起请求
         $response = $this->httpClient->post($this->config['gateway'] . 'pay/unifiedorder', [
             'form_params' => $params,
             'headers' => [
                 'Content-Type' => 'application/x-www-form-urlencoded',
             ],
         ]);
    
         // 4. 解析响应
         $result = json_decode($response->getBody()->getContents(), true);
         if (!$result || $result['code'] != '0000') {
             throw new \Exception('汇付下单失败:' . ($result['msg'] ?? '未知错误'));
         }
    
         // 5. 返回支付链接(H5支付返回跳转URL)
         return [
             'code' => 0,
             'msg' => 'success',
             'data' => [
                 'pay_url' => $result['data']['pay_url'], // 支付跳转链接
                 'out_trade_no' => $outTradeNo,
             ],
         ];
     }
    
     /**
      * 查询订单状态
      * @param string $outTradeNo 商户订单号
      * @return array
      * @throws \GuzzleHttp\Exception\GuzzleException
      */
     public function queryOrder(string $outTradeNo): array
     {
         $params = [
             'merchant_id' => $this->config['merchant_id'],
             'app_id' => $this->config['app_id'],
             'out_trade_no' => $outTradeNo,
             'charset' => $this->config['charset'],
             'sign_type' => $this->config['sign_type'],
             'timestamp' => date('YmdHis'),
             'version' => '1.0',
         ];
    
         $params['sign'] = $this->generateSign($params);
    
         $response = $this->httpClient->post($this->config['gateway'] . 'pay/query', [
             'form_params' => $params,
         ]);
    
         $result = json_decode($response->getBody()->getContents(), true);
         if (!$result || $result['code'] != '0000') {
             throw new \Exception('订单查询失败:' . ($result['msg'] ?? '未知错误'));
         }
    
         return [
             'code' => 0,
             'msg' => 'success',
             'data' => $result['data'], // 包含订单状态:SUCCESS/FAIL/PENDING
         ];
     }

    }

  2. 支付控制器(app/controller/pay/HuifuPayController.php)

    <?php

    namespace app\controller\pay;

    use app\Services\Pay\HuifuPayService;
    use app\common\controller\ApiController;
    use think\facade\Log;

    class HuifuPayController extends ApiController
    {
    /**
    * 汇付支付下单
    * @return \think\Response
    */
    public function unifiedOrder()
    {
    try {
    // 1. 获取前端参数
    params = this->request->post();
    // 参数验证
    this->validate(params, [
    'out_trade_no|商户订单号' => 'require',
    'total_amount|订单金额' => 'require|float|>:0',
    'subject|商品标题' => 'require',
    'pay_type|支付方式' => 'require|in:WECHAT_H5,ALIPAY_H5',
    'client_ip|客户端IP' => 'require|ip',
    ]);

    复制代码
             // 2. 调用汇付支付服务
             $huifuPay = new HuifuPayService();
             $result = $huifuPay->unifiedOrder(
                 $params['out_trade_no'],
                 sprintf('%.2f', $params['total_amount']),
                 $params['subject'],
                 $params['pay_type'],
                 $params['client_ip']
             );
    
             return $this->success('下单成功', $result['data']);
         } catch (\Exception $e) {
             Log::error('汇付下单异常:' . $e->getMessage());
             return $this->fail('下单失败:' . $e->getMessage());
         }
     }
    
     /**
      * 汇付支付回调处理
      * @return string
      */
     public function notify()
     {
         // 1. 获取回调参数
         $params = $this->request->post();
         Log::info('汇付支付回调参数:' . json_encode($params));
    
         try {
             // 2. 验证签名
             $huifuPay = new HuifuPayService();
             if (!$huifuPay->verifySign($params, $params['sign'])) {
                 Log::error('汇付回调签名验证失败');
                 return 'fail';
             }
    
             // 3. 验证订单状态
             if ($params['trade_status'] != 'SUCCESS') {
                 Log::error('汇付订单支付失败,订单号:' . $params['out_trade_no']);
                 return 'fail';
             }
    
             // 4. 处理订单(对接CRMEB订单逻辑)
             // 此处替换为CRMEB实际的订单处理逻辑
             $orderNo = $params['out_trade_no'];
             $payAmount = $params['total_amount'];
             $tradeNo = $params['trade_no']; // 汇付交易号
    
             // 示例:更新订单状态为已支付
             $orderService = new \app\services\order\StoreOrderServices();
             $order = $orderService->getOne(['order_id' => $orderNo]);
             if (!$order) {
                 Log::error('汇付回调订单不存在:' . $orderNo);
                 return 'fail';
             }
    
             // 验证金额
             if (sprintf('%.2f', $order['pay_price']) != $payAmount) {
                 Log::error('汇付回调金额不一致,订单号:' . $orderNo);
                 return 'fail';
             }
    
             // 更新订单状态
             $orderService->updateOrderPaid($orderNo, $tradeNo, 'huifu');
    
             // 5. 返回成功标识(必须返回success,否则汇付会重复回调)
             return 'success';
         } catch (\Exception $e) {
             Log::error('汇付回调处理异常:' . $e->getMessage());
             return 'fail';
         }
     }
    
     /**
      * 订单查询
      * @return \think\Response
      */
     public function queryOrder()
     {
         try {
             $outTradeNo = $this->request->get('out_trade_no');
             if (!$outTradeNo) {
                 return $this->fail('缺少商户订单号');
             }
    
             $huifuPay = new HuifuPayService();
             $result = $huifuPay->queryOrder($outTradeNo);
    
             return $this->success('查询成功', $result['data']);
         } catch (\Exception $e) {
             Log::error('汇付订单查询异常:' . $e->getMessage());
             return $this->fail('查询失败:' . $e->getMessage());
         }
     }

    }

  3. 路由配置(route/pay.php)

    <?php

    use think\facade\Route;

    // 汇付支付相关路由
    Route::group('huifu', function () {
    // 下单接口
    Route::post('unifiedOrder', 'pay/HuifuPayController@unifiedOrder');
    // 支付回调
    Route::post('notify', 'pay/HuifuPayController@notify');
    // 订单查询
    Route::get('queryOrder', 'pay/HuifuPayController@queryOrder');
    })->middleware([\app\middleware\AllowOriginMiddleware::class]);

三、CRMEB 订单对接关键逻辑说明

在回调处理方法notify()中,核心是对接 CRMEB 的订单服务,关键代码说明:

  1. $orderService = new \app\services\order\StoreOrderServices();:实例化 CRMEB 订单服务类;
  2. $orderService->updateOrderPaid($orderNo, $tradeNo, 'huifu');:调用 CRMEB 内置的订单支付成功方法,更新订单状态、记录支付日志等;
  3. 金额验证:必须验证回调的金额与订单金额一致,防止篡改。

四、测试用例

1. 下单接口请求示例
复制代码
POST https://你的域名/pay/huifu/unifiedOrder
Content-Type: application/json

{
    "out_trade_no": "CRMEB20260129001",
    "total_amount": 0.01,
    "subject": "测试商品",
    "pay_type": "WECHAT_H5",
    "client_ip": "127.0.0.1"
}
  1. 响应示例

    {
    "code": 0,
    "msg": "success",
    "data": {
    "pay_url": "https://pay.huifu.com/h5/pay?token=xxx",
    "out_trade_no": "CRMEB20260129001"
    }
    }

五、注意事项

  1. 私钥 / 公钥格式:汇付要求使用 PKCS8 格式的 RSA2 密钥,需确保密钥格式正确;
  2. 回调地址:必须是公网可访问的地址,且不能带参数,建议使用 HTTPS;
  3. 日志记录:所有支付相关操作(下单、回调、查询)必须记录详细日志,便于排查问题;
  4. 幂等性处理:回调接口需做幂等性校验,防止重复处理订单;
  5. 正式环境:关闭verify => false,开启 SSL 验证,确保通信安全。
相关推荐
weixin_440784112 小时前
OkHttp使用指南
android·java·okhttp
Xxtaoaooo2 小时前
React Native 跨平台鸿蒙开发实战:调试与真机测试全流程
android·react native·harmonyos
TheNextByte12 小时前
将视频从电脑传输到Android (超简单指南)
android·电脑·音视频
TheNextByte12 小时前
如何将照片从Android手机传输到Chromebook电脑
android·智能手机·电脑
程序员清洒10 小时前
Flutter for OpenHarmony:GridView — 网格布局实现
android·前端·学习·flutter·华为
running up that hill11 小时前
Android的线性布局
android
m0_7482299911 小时前
Laravel9.x核心特性全解析
android
2603_9494621014 小时前
Flutter for OpenHarmony社团管理App实战:意见反馈实现
android·flutter
错把套路当深情14 小时前
android两种渠道支持一键打包 + 随意组合各种渠道
android