使用最新APIv3为基础开发代金券,yii2框架(PHP)源码开发
近期公司要求开发代金券由于老的(xml)开发接口功能满足不了需求,所以就开发新的以APIv3为基础的签名。微信链接https://pay.weixin.qq.com/wiki/doc/apiv3/wxpay/marketing/convention/chapter3_3.shtml,文档在这里可以自己看
以下为php源码:
1、yii2框架下,这是封装的类:
<?php
namespace luweiss\wechat;
class Coupon
{
// 错误信息
private $error = '';
// 私钥 apiclient_key.pem(微信服务商-账户中心-API安全 自行下载 https://pay.weixin.qq.com/index.php/core/cert/api_cert)
//private $mch_private_key = __DIR__ . DIRECTORY_SEPARATOR . 'cert' . DIRECTORY_SEPARATOR . 'apiclient_key.pem';
public function __construct($args = []){
$this->id=$args['id'];
$this->mch_id = isset($args['mch_id']) ? $args['mch_id'] : null;//商户mchid
$this->mch_api_key = isset($args['mch_api_key']) ? $args['mch_api_key'] : null;//商户API v3密钥(微信服务商-账户中心-API安全 api v3密钥 https://pay.weixin.qq.com/index.php/core/cert/api_cert)
$this->serial_no =isset($args['serial_no']) ? $args['serial_no'] : null;//证书编号 (apiclient_cert.pem证书解析后获得)
$this->mch_private_key = isset($args['key_pem_url']) ? $args['key_pem_url'] : null;//私钥 apiclient_key.pem(微信服务商-账户中心-API安全 自行下载 https://pay.weixin.qq.com/index.php/core/cert/api_cert)
//$this->mch_private_key = __DIR__ . DIRECTORY_SEPARATOR . 'cert' . DIRECTORY_SEPARATOR .$this->id.'\\'.'apiclient_key.pem';
$this->public_key_path = __DIR__ . DIRECTORY_SEPARATOR . 'cert' . DIRECTORY_SEPARATOR .$this->id. DIRECTORY_SEPARATOR.'cert_ficates_v3.pem';// 支付平台公钥(接口获取)
}
/*
* 发送代金券
*/
public function sendCoupon($data)
{
$openid=$data['openid'];
unset($data['openid']);
$url = 'https://api.mch.weixin.qq.com/v3/marketing/favor/users/'.$openid.'/coupons';
// 获取支付平台证书编码(也可以用接口中返回的serial_no 来源:https://api.mch.weixin.qq.com/v3/certificates)
$serial_no = $this->parseSerialNo($this->getCertFicates());
$bodyData = json_encode($data);
// 获取认证信息
$authorization = $this->getAuthorization($url, 'POST', $bodyData);
$header = [
'Content-Type:application/json',
'Accept:application/json',
'User-Agent:*/*',
'Authorization:' . $authorization,
'Wechatpay-Serial:' . $serial_no
];
$json = $this->getCurl('POST', $url, $bodyData, $header);
$result = json_decode($json, true);
if(isset($result['coupon_id'])){
return $result;
}else{
return false;
}
}
/*
* 根据商户号查用户的券
*/
public function couponList($data)
{
$param=$data['openid'];
// 获取认证信息
$url = 'https://api.mch.weixin.qq.com/v3/marketing/favor/users/'.$param.'/coupons?appid='.$data['appid'].'&available_mchid='.$data['available_mchid'];
$authorization = $this->getAuthorization($url);
$header = [
'Content-Type:application/json',
'Accept:application/json',
'User-Agent:*/*',
'Authorization:' . $authorization
];
$json= $this->getCurl('GET', $url, '', $header);
$results = json_decode($json, true);
if(isset($results['data'])){
return $results['data'];
}else{
return false;
}
}
/*
* 查询代金券可用单品
*/
public function couponInfoGoods($data)
{
$param=$data['stock_id'];
// 获取认证信息
$url = 'https://api.mch.weixin.qq.com/v3/marketing/favor/stocks/'.$param.'/items?offset=10&limit=10&stock_creator_mchid='.$data['stock_creator_mchid'];
$authorization = $this->getAuthorization($url);
$header= [
'Content-Type:application/json',
'Accept:application/json',
'User-Agent:*/*',
'Authorization:' . $authorization
];
$json= $this->getCurl('GET', $url, '', $header);
$resultd = json_decode($json, true);
var_dump($resultd);die;
if(isset($results['data'])){
return $results['data'];
}else{
return false;
}
}
/**
* 获取微信支付平台证书
*/
public function certFicates()
{
$url = 'https://api.mch.weixin.qq.com/v3/certificates';
// 获取认证信息
$authorization = $this->getAuthorization($url);
$header = [
'Content-Type:application/json',
'Accept:application/json',
'User-Agent:*/*',
'Authorization:' . $authorization
];
$json = $this->getCurl('GET', $url, '', $header);
$data = json_decode($json, true);
if (isset($data['code']) && isset($data['message'])) {
$this->error = '[certFicates]请求错误 code:' . $data['code'] . ' msg:' . $data['message'];
return false;
}
if (empty($cfdata = $data['data'][0])) {
$this->error = '[certFicates]返回错误';
return false;
}
return $cfdata;
}
/**
* 获取认证信息
* @param string $url
* @param string $http_method
* @param string $body
* @return string
* @throws Exception
*/
private function getAuthorization($url, $http_method = 'GET', $body = '')
{
if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) {
throw new \Exception("当前PHP环境不支持SHA256withRSA");
}
//私钥地址
$mch_private_key = $this->mch_private_key;
//商户号
$merchant_id = $this->mch_id;
//当前时间戳
$timestamp = time();
//随机字符串
$nonce = $this->getNonceStr();
//证书编号
$serial_no = $this->serial_no;
$url_parts = parse_url($url);
$canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
$message = $http_method . "\n" .
$canonical_url . "\n" .
$timestamp . "\n" .
$nonce . "\n" .
$body . "\n";
openssl_sign($message, $raw_sign, \openssl_get_privatekey(\file_get_contents($mch_private_key)), 'sha256WithRSAEncryption');
$sign = base64_encode($raw_sign);
$schema = 'WECHATPAY2-SHA256-RSA2048';
$token = sprintf('mchid="%s",nonce_str="%s",timestamp="%d",serial_no="%s",signature="%s"',
$merchant_id, $nonce, $timestamp, $serial_no, $sign);
return $schema . ' ' . $token;
}
/**
* 敏感字符加密
* @param $str
* @return string
* @throws Exception
*/
private function getEncrypt($str)
{
static $content;
if (empty($content)) {
$content = $this->getCertFicates();
}
$encrypted = '';
if (openssl_public_encrypt($str, $encrypted, $content, OPENSSL_PKCS1_OAEP_PADDING)) {
//base64编码
$sign = base64_encode($encrypted);
}
else {
throw new \Exception('encrypt failed');
}
return $sign;
}
/**
* 获取支付平台证书
* @return false|string
*/
private function getCertFicates()
{
$public_key_path = $this->public_key_path;
if (!file_exists($public_key_path)) {
$path= __DIR__ . DIRECTORY_SEPARATOR . 'cert' . DIRECTORY_SEPARATOR .$this->id;
mkdir($path,0777);
$cfData = $this->certFicates();
$content = $this->decryptToString($cfData['encrypt_certificate']['associated_data'], $cfData['encrypt_certificate']['nonce'], $cfData['encrypt_certificate']['ciphertext'], $this->mch_api_key);
file_put_contents($public_key_path, $content);
}
else {
$content = file_get_contents($public_key_path);
}
return $content;
}
/**
* 业务编号
* @return string
*/
private function getBusinessCode()
{
return date('Ymd') . substr(time(), -5) . substr(microtime(), 2, 5) . sprintf('%02d', rand(0, 99));
}
/**
* 随机字符串
* @param int $length
* @return string
*/
private function getNonceStr($length = 16)
{
// 密码字符集,可任意添加你需要的字符
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= $chars[mt_rand(0, strlen($chars) - 1)];
}
return $str;
}
/**
* @param string $method
* @param string $url
* @param array|string $data
* @param array $headers
* @param int $timeout
* @return bool|string
*/
private function getCurl($method = 'GET', $url, $data, $headers = [], $timeout = 10)
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HEADER, false);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($curl, CURLOPT_TIMEOUT, $timeout);
if (!empty($headers)) {
curl_setopt($curl, CURLOPT_HTTPHEADER, $headers);
}
if ($method == 'POST') {
curl_setopt($curl, CURLOPT_POST, TRUE);
curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
}
else {
}
$result = curl_exec($curl);
curl_close($curl);
return $result;
}
/**
* Decrypt AEAD_AES_256_GCM ciphertext
*
* @param string $associatedData AES GCM additional authentication data
* @param string $nonceStr AES GCM nonce
* @param string $ciphertext AES GCM cipher text
*
* @return string|bool Decrypted string on success or FALSE on failure
*/
private function decryptToString($associatedData, $nonceStr, $ciphertext, $aesKey)
{
$auth_tag_length_byte = 16;
$ciphertext = \base64_decode($ciphertext);
if (strlen($ciphertext) <= $auth_tag_length_byte) {
return false;
}
// ext-sodium (default installed on >= PHP 7.2)
if (function_exists('\sodium_crypto_aead_aes256gcm_is_available') &&
\sodium_crypto_aead_aes256gcm_is_available()) {
return \sodium_crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
}
// ext-libsodium (need install libsodium-php 1.x via pecl)
if (function_exists('\Sodium\crypto_aead_aes256gcm_is_available') &&
\Sodium\crypto_aead_aes256gcm_is_available()) {
return \Sodium\crypto_aead_aes256gcm_decrypt($ciphertext, $associatedData, $nonceStr, $aesKey);
}
// openssl (PHP >= 7.1 support AEAD)
if (PHP_VERSION_ID >= 70100 && in_array('aes-256-gcm', \openssl_get_cipher_methods())) {
$ctext = substr($ciphertext, 0, -$auth_tag_length_byte);
$authTag = substr($ciphertext, -$auth_tag_length_byte);
return \openssl_decrypt($ctext, 'aes-256-gcm', $aesKey, \OPENSSL_RAW_DATA, $nonceStr,
$authTag, $associatedData);
}
throw new \Exception('AEAD_AES_256_GCM需要PHP 7.1以上或者安装libsodium-php');
}
/**
* 获取证书编号
* @param $certificate
* @return string
*/
private function parseSerialNo($certificate)
{
$info = \openssl_x509_parse($certificate);
if (!isset($info['serialNumber']) && !isset($info['serialNumberHex'])) {
throw new \InvalidArgumentException('证书格式错误');
}
$serialNo = '';
// PHP 7.0+ provides serialNumberHex field
if (isset($info['serialNumberHex'])) {
$serialNo = $info['serialNumberHex'];
}
else {
// PHP use i2s_ASN1_INTEGER in openssl to convert serial number to string,
// i2s_ASN1_INTEGER may produce decimal or hexadecimal format,
// depending on the version of openssl and length of data.
if (\strtolower(\substr($info['serialNumber'], 0, 2)) == '0x') { // HEX format
$serialNo = \substr($info['serialNumber'], 2);
}
else { // DEC format
$value = $info['serialNumber'];
$hexvalues = ['0', '1', '2', '3', '4', '5', '6', '7',
'8', '9', 'A', 'B', 'C', 'D', 'E', 'F'];
while ($value != '0') {
$serialNo = $hexvalues[\bcmod($value, '16')] . $serialNo;
$value = \bcdiv($value, '16', 0);
}
}
}
return \strtoupper($serialNo);
}
public function getError()
{
return $this->error;
}
}
使用yii2调用:
//发放代金券
public function actionCoupon(){
$params=\Yii::$app->request->get();
$list = WechatApp::findOne($this->store_id);
$mch_id=$list['mch_id'];
$userInfo = User::findOne(['access_token'=>$params['access_token']]);
//新的代金券发放
$coupon= new Coupon($list);
$resd = $coupon->sendCoupon([
'stock_id'=>'*****',
'out_request_no'=>$this->out_request_no($mch_id),
'openid'=>$userInfo['wechat_open_id'],
'appid'=>$list['app_id'],
'stock_creator_mchid'=>$mch_id,
]);
return new ApiResponse(200,'success',$resd);
}
//代金券列表
public function actionCouponInfo(){
$params=\Yii::$app->request->get();
$list = WechatApp::findOne($this->store_id);
$mch_id=$list['mch_id'];
$userInfo = User::findOne(['access_token'=>$params['access_token']]);
//新的代金券发放
$coupon= new Coupon($list);
$couponList = $coupon->couponList([
'openid'=>$userInfo['wechat_open_id'],
'appid'=>$list['app_id'],
'available_mchid'=>$mch_id,
]);
$listCoupon='';
if($couponList){
foreach ($couponList as $key => $val){
// if($val['singleitem'] == true){
// //单品劵
// $couponGoodsInfo = $coupon->couponInfoGoods([
// 'stock_creator_mchid'=>$val['stock_creator_mchid'],
// 'stock_id'=>$val['stock_id'],
// ]);
// var_dump($couponGoodsInfo);die;
// }
if($val['singleitem'] == false && $val['status'] == 'SENDED'){
$coupons['coupon_amount']=$val['normal_coupon_information']['coupon_amount']/100;
$coupons['transaction_minimum']=$val['normal_coupon_information']['transaction_minimum']/100;
$coupons['coupon_name']=$val['coupon_name'];
$listCoupon[]=$coupons;
}
}
}
return new ApiResponse(200,'success',$listCoupon);
}
代码已测试。可以运行。有问题大家可以交流。
记得在引用时加上new \luweiss\wechat\Coupon(); 这个路径不能错。