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)

    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)

    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)

    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 验证,确保通信安全。
相关推荐
千里马学框架15 小时前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台15 小时前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone15 小时前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc16 小时前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo17 小时前
Kotlin 协程中的 Job 结构化并发与取消
android
sun00770018 小时前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼18 小时前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone19 小时前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen19 小时前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone20 小时前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui