一、开发前准备
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,
];
-
汇付支付核心工具类(app/Services/Pay/HuifuPayService.php)
<?phpnamespace 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 ]; }}
-
支付控制器(app/controller/pay/HuifuPayController.php)
<?phpnamespace 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()); } }}
-
路由配置(route/pay.php)
<?phpuse 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 的订单服务,关键代码说明:
$orderService = new \app\services\order\StoreOrderServices();:实例化 CRMEB 订单服务类;$orderService->updateOrderPaid($orderNo, $tradeNo, 'huifu');:调用 CRMEB 内置的订单支付成功方法,更新订单状态、记录支付日志等;- 金额验证:必须验证回调的金额与订单金额一致,防止篡改。
四、测试用例
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"
}
-
响应示例
{
"code": 0,
"msg": "success",
"data": {
"pay_url": "https://pay.huifu.com/h5/pay?token=xxx",
"out_trade_no": "CRMEB20260129001"
}
}
五、注意事项
- 私钥 / 公钥格式:汇付要求使用 PKCS8 格式的 RSA2 密钥,需确保密钥格式正确;
- 回调地址:必须是公网可访问的地址,且不能带参数,建议使用 HTTPS;
- 日志记录:所有支付相关操作(下单、回调、查询)必须记录详细日志,便于排查问题;
- 幂等性处理:回调接口需做幂等性校验,防止重复处理订单;
- 正式环境:关闭
verify => false,开启 SSL 验证,确保通信安全。