1.下载微信官方的库和SDK
在项目目录中,通过composer命令行添加:
composer require wechatpay/wechatpay-guzzle-middleware
2.开通微信支付服务商平台
特别备注:微信支付API v3要求商户对请求进行签名。微信支付会在收到请求后进行签名的验证。如果签名验证不通过,微信支付API v3将会拒绝处理请求,并返回401 Unauthorized
什么是商户证书?什么是平台证书?
-
“商户证书”是指由商户申请的,包含商户的商户号、公司名称、公钥信息的证书。
-
”平台证书”是指由微信支付负责申请的,包含微信支付平台标识、公钥信息的证书。
商户在调用 API 时用自身的私钥签名,微信支付使用商户证书中的公钥来验签。微信支付在响应的报文中使用自身的私钥签名,商户使用平台证书中的公钥来验签。
平台证书如何获取?
获取平台公钥、平台序列号
(证书下载工具)
地址:GitHub - wechatpay-apiv3/CertificateDownloader: Java 微信支付 APIv3 平台证书的命令行下载工具
下载文件:CertificateDownloader-1.1.jar
该工具已经通过 Maven 打包成 CertificateDownloader.jar,可在 release 中下载。
在终端执行:
这里,必需参数有:
商户的私钥文件,即 -f
证书解密的密钥,即 -k
商户号,即 -m
保存证书的路径,即 -o
商户证书的序列号,即 -s
非必需参数有:
微信支付证书,用于验签,即 -c
完整命令如:
java -jar CertificateDownloader.jar -k ${apiV3key} -m ${mchId} -f ${mchPrivateKeyFilePath} -s ${mchSerialNo} -o ${outputFilePath} -c ${wechatpayCertificateFilePath}
执行成功之后,获取当前平台证书的路径
① 获取签名
$ echo -n -e \
"GET\n/v3/certificates\n1554208460\n593BEC0C930BF1AFEB40B4A08C8FB242\n\n" \
| openssl dgst -sha256 -sign apiclient_key.pem \
| openssl base64 -A
uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==
②最终我们可以组一个包含了签名的HTTP请求了
$ curl https://api.mch.weixin.qq.com/v3/certificates -H 'Authorization: WECHATPAY2-SHA256-RSA2048 mchid="1900009191",nonce_str="593BEC0C930BF1AFEB40B4A08C8FB242",signature="uOVRnA4qG/MNnYzdQxJanN+zU+lTgIcnU9BxGw5dKjK+VdEUz2FeIoC+D5sB/LN+nGzX3hfZg6r5wT1pl2ZobmIc6p0ldN7J6yDgUzbX8Uk3sD4a4eZVPTBvqNDoUqcYMlZ9uuDdCvNv4TM3c1WzsXUrExwVkI1XO5jCNbgDJ25nkT/c1gIFvqoogl7MdSFGc4W4xZsqCItnqbypR3RuGIlR9h9vlRsy7zJR9PBI83X8alLDIfR1ukt1P7tMnmogZ0cuDY8cZsd8ZlCgLadmvej58SLsIkVxFJ8XyUgx9FmutKSYTmYtWBZ0+tNvfGmbXU7cob8H/4nLBiCwIUFluw==",timestamp="1554208460",serial_no="1DDE55AD98ED71D6EDD4A4A16996DE7B47773A8C"'
执行成功之后,得到平台证书序列号:serial_no ,保存起来
以上步骤获取了平台的证书路径和平台证书序列号
以下是实际支付PHP代码:
①.发起微信扫码支付,生成待支付二维码
namespace app\api\controller;
use app\common\controller\Api;
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\HandlerStack;
use GuzzleHttp\Client;
use WechatPay\GuzzleMiddleware\WechatPayMiddleware;
use WechatPay\GuzzleMiddleware\Util\PemUtil;
use fast\Random;
/**
* 微信服务商多商户支付接口
*/
class Wechatpay extends Api
{
protected $noNeedLogin = [];
protected $noNeedRight = ['*'];
public function _initialize()
{
parent::_initialize();
// 商户相关配置,
$this->merchantId= Config('site.merchantId'); // 商户号
// 商户API证书序列号
$this->merchantSerialNumber= Config('site.merchantSerialNumber');
// 商户私钥文件路径
$this->merchantPrivateKey=PemUtil::loadPrivateKey(".".Config('site.wechat_merchantPrivateKey'));
// 微信支付平台配置,微信支付平台证书文件路径
$this->wechatpayCertificate=PemUtil::loadCertificate(".".Config('site.wechatpayCertificate'));
// 构造一个WechatPayMiddleware
$wechatpayMiddleware = WechatPayMiddleware::builder()
->withMerchant($this->merchantId, $this->merchantSerialNumber, $this->merchantPrivateKey) // 传入商户相关配置
->withWechatPay([ $this->wechatpayCertificate ]) // 可传入多个微信支付平台证书,参数类型为array
->build();
// 将WechatPayMiddleware添加到Guzzle的HandlerStack中
$HandlerStack = new HandlerStack();
$stack = $HandlerStack::create();
$stack->push($wechatpayMiddleware, 'wechatpay');
// 创建Guzzle HTTP Client时,将HandlerStack传入,接下来,正常使用Guzzle发起API请求,WechatPayMiddleware会自动地处理签名和验签
$client = new Client(['handler' => $stack]);
$this->client = $client;
}
/**
* @ApiTitle (微信服务商支付生成二维码)
* @ApiMethod (POST)
* @ApiRoute (/api/wechatpay/wechat_qrcode)
* @ApiHeaders (name=token, type=string, required=true, description="请求的TOKEN")
* @ApiHeaders (name=sub_appid, type=string, required=true, description="子商户申请的公众号appid")
* @ApiHeaders (name=sub_mchid, type=string, required=true, description="子商户的商户号")
* @ApiHeaders (name=description, type=string, required=true, description="商品描述")
* @ApiHeaders (name=notify_url, type=string, required=true, description="通知地址")
* @ApiHeaders (name=goods_tag, type=string, required=true, description="订单优惠标记 示例值:WXG ")
* @ApiHeaders (name=total, type=float, required=true, description="待支付总金额")
* @ApiSummary (商户Native下单接口,微信后台系统返回链接参数code_url,商户后台系统将code_url值生成二维码图片,用户使用微信客户端扫码后发起支付。code_url有效期为2小时,过期后扫码不能再发起支付。)
*/
public function wechat_qrcode()
{
$data = $this->request->post();
try {
$resp = $this->client->request(
'POST',
'https://api.mch.weixin.qq.com/v3/pay/partner/transactions/native', //请求URL
[
// JSON请求体
'json' => [
"sp_appid" => Config('site.sp_appid'), // 服务商申请的公众号appid
"sp_mchid" => $this->merchantId, //服务商户号,由微信支付生成并下发
"description" => $data['description'], //商品描述
"notify_url" => $data['notify_url'], //通知地址
"out_trade_no" => Random::numeric(16), //商户订单号
"goods_tag" => $data['goods_tag'], //订单优惠标记 示例值:WXG
"sub_appid" => $data['sub_appid'], //子商户申请的公众号appid
"sub_mchid" => $data['sub_mchid'], //子商户的商户号,由微信支付生成并下发。
"amount" => [
"total" => $data['total'] * 100, //以分为单位
"currency" => "CNY",
],
"settle_info"=>[
"profit_sharing" => true //是否指定分账,枚举值 true:是,false:否
]
],
'headers' => [ 'Accept' => 'application/json' ]
]
);
$statusCode = $resp->getStatusCode();
if ($statusCode == 200) { //处理成功
$url = json_decode($resp->getBody()->getContents(),true);
$code_img = Controller('Common')->qrcode($url['code_url'],'wechat_pay_server');
$img = urls()."/".$code_img;
$this->success('success',$img);
} else if ($statusCode == 204) { //处理成功,无返回Body
$this->success('success');
}
} catch (RequestException $e) {
// 进行错误处理
$data = [
'status'=> $e->getResponse()->getStatusCode(),
'message'=> json_decode($e->getResponse()->getBody(),true)
];
$this->error('ERROR',$data);
}
}
生成微信pc端扫码支付的二维码
②. 服务商添加分账接收方API
/**
* @ApiTitle (服务商添加分账接收方API)
* @ApiMethod (POST)
* @ApiRoute (/api/wechatpay/receivers)
* @ApiHeaders (name=token, type=string, required=true, description="请求的TOKEN")
* @ApiHeaders (name=sub_mchid, type=string, required=true, description="子商户的商户号,必填")
* @ApiHeaders (name=sub_appid, type=string, required=true, description="子商户的appid")
* @ApiSummary (服务商发起添加分账接收方请求,建立分账接收方列表。后续可通过发起分账请求,将分账方商户结算后的资金,分到该分账接收方 )
*/
public function receivers()
{
$data = $this->request->post();
if($data['sub_mchid']=='')
{
$this->error('子商户的商户号不能为空');
}
$name = $this->getEncrypt('分账房商户名称');
try {
$resp = $this->client->request(
'POST',
'https://api.mch.weixin.qq.com/v3/profitsharing/receivers/add', //请求URL
[
// JSON请求体
'json' => [
"sub_mchid" => $data['sub_mchid'], //分账出资的电商平台二级商户,填写微信支付分配的商户号。
"appid" => Config('site.sp_appid'), // 服务商申请的公众号appid
"sub_appid" => $data['sub_appid'], // 微信分配的子商户公众账号
//MERCHANT_ID:商户ID
//PERSONAL_OPENID:个人
//openid(由父商户APPID转换得到)
//PERSONAL_SUB_OPENID:个人
//sub_openid(由子商户APPID转换得到)
"type" => 'MERCHANT_ID',
"name" => $name, //分账接收方类型是MERCHANT_ID时,是商户全称(必传)
"account" => $this->merchantId, //分账接收方账号
"relation_type" => 'SERVICE_PROVIDER', //与分账方的关系类型
"custom_relation" => 'MEETCONF', //与分账方的关系类型
],
'headers' => [
'Accept' => 'application/json',
'Wechatpay-Serial'=> Config('site.Wechatpay-Serial')
]
]
);
$statusCode = $resp->getStatusCode();
if ($statusCode == 200) { //处理成功
$data = json_decode($resp->getBody()->getContents(),true);
$this->success('success',$data);
} else if ($statusCode == 204) { //处理成功,无返回Body
$this->success('success');
}
} catch (RequestException $e) {
// 进行错误处理
$data = [
'status'=> $e->getResponse()->getStatusCode(),
'message'=> json_decode($e->getResponse()->getBody(),true)
];
$this->error('ERROR',$data);
}
}
③.服务商发起请求分账API
/**
* @ApiTitle (服务商发起请求分账API)
* @ApiMethod (POST)
* @ApiRoute (/api/wechatpay/profitsharing)
* @ApiHeaders (name=token, type=string, required=true, description="请求的TOKEN")
* @ApiHeaders (name=sub_mchid, type=string, required=true, description="子商户的商户号,必填")
* @ApiHeaders (name=sub_appid, type=string, required=true, description="子商户的appid")
* @ApiHeaders (name=transaction_id, type=string, required=true, description="微信支付订单号")
* @ApiHeaders (name=out_order_no, type=string, required=true, description="商户系统内部的分账单号")
* @ApiHeaders (name=amount, type=float, required=true, description="分账金额,单位:元")
* @ApiSummary (微信订单支付成功后,由电商平台发起分账请求,将结算后的资金分给分账接收方。)
*/
/* 注意:
• 微信订单支付成功后,服务商代特约商户发起分账请求,将结算后的钱分到分账接收方。
• 对同一笔订单最多能发起50次分账请求,每次请求最多分给5个接收方。
• 此接口采用异步处理模式,即在接收到商户请求后,会先受理请求再异步处理,最终的分账结果可以通过查询分账接口获取。
• 分账资金的冻结期默认是180天。从订单支付成功之日起,180天内需要发起分账,若180天内未发起分账,待分账资金将会自动解冻给分账方。
*/
public function profitsharing()
{
$data = $this->request->post();
if($data['sub_mchid']=='')
{
$this->error('子商户的商户号不能为空');
}
try {
$resp = $this->client->request(
'POST',
'https://api.mch.weixin.qq.com/v3/profitsharing/orders', //请求URL
[
// JSON请求体
'json' => [
"sub_mchid" => $data['sub_mchid'], //分账出资的电商平台二级商户,填写微信支付分配的商户号。
"appid" => Config('site.sp_appid'), // 服务商申请的公众号appid
"sub_appid" => $data['sub_appid'], // 微信分配的子商户公众账号ID
"transaction_id" => $data['transaction_id'], //微信支付订单号
"out_order_no" => $data['out_order_no'], //商户系统内部的分账单号
//分账接收方列表,可以设置出资商户作为分账接受方,最多可有50个分账接收方
"receivers" => [
[
"type" => 'MERCHANT_ID', //分账接收方类型,枚举值:MERCHANT_ID:商户PERSONAL_OPENID:个人
"account" => $this->merchantId, //分账接收方账号(当前为服务商自己的商户号)
//分账金额,单位为分,只能为整数,不能超过原订单支付金额及最大分账比例金额。
"amount" => $data['amount'] * 100,
"description" => '分账给服务商'.$this->merchantId, //分账的原因描述,分账账单中需要体现。
],
],
/*
* 是否解冻剩余未分资金,
1、如果为true,该笔订单剩余未分账的金额会解冻回分账方商户;
2、如果为false,该笔订单剩余未分账的金额不会解冻回分账方商户,可以对该笔订单再次进行分账。
*/
"unfreeze_unsplit" => true //是否解冻剩余未分资金,
],
'headers' => [ 'Accept' => 'application/json' ]
]
);
$statusCode = $resp->getStatusCode();
if ($statusCode == 200) { //处理成功
$data = json_decode($resp->getBody()->getContents(),true);
$this->success('success',$data);
} else if ($statusCode == 204) { //处理成功,无返回Body
$this->success('success');
}
} catch (RequestException $e) {
// 进行错误处理
$data = [
'status'=> $e->getResponse()->getStatusCode(),
'message'=> json_decode($e->getResponse()->getBody(),true)
];
$this->error('ERROR',$data);
}
}
④. 敏感字符进行加密操作
/*
* 敏感字符进行加密操作
*/
private function getEncrypt($str) {
//$str是待加密字符串
$public_key_path = ".".Config('site.wechatpayCertificate');
$public_key = file_get_contents($public_key_path);
$encrypted = '';
if (openssl_public_encrypt($str, $encrypted, $public_key, OPENSSL_PKCS1_OAEP_PADDING)) {
//base64编码
$sign = base64_encode($encrypted);
} else {
throw new Exception('encrypt failed');
}
return $sign;
}