微信支付分是微信比较新的一种支付方式,针对特殊场景,对接过程还是有点小坑,相关教程比较少。
记录一下源码,以作备忘。微信接口版本为最新版本 。
<?php
/**
* 微信支付分
*/
class WxPayscore
{
private $wxConfig = [
'appid' => '',//公众号appid
'appSecret' => '',//公众号秘钥
'mch_id' => '',//商户号
'service_id' => '',//商户号服务id
'key' => '',//v3秘钥
'serial_no' => "",//这个是证书号
'notify_url' => '',// 异步回调地址
'pay_sign_page' => '',// 签约后跳转页面
];
private $mch_private_key = '';// 私钥秘钥
public function __construct()
{
}
/**
* 创建支付订单
* @param $order_no
* @param number $total_fee
* @param $openid
* @return int|void
*/
public function createOrder($order_no, $total_fee, $openid)
{
$website_name = '支付名称';
$total_fee = (int)bcmul($total_fee, 100, 0);// 传给微信的单位是分
$para = [
'out_order_no' => $order_no,
'appid' => $this->wxConfig['appid'],
'service_id' => $this->wxConfig['service_id'],
'service_introduction' => $website_name,
'risk_fund' => ['name' => 'ESTIMATE_ORDER_COST', 'amount' => $total_fee],
'time_range' => ['start_time' => 'OnAccept'],
'notify_url' => $this->wxConfig['notify_url'],
'openid' => $openid,
'need_user_confirm' => false,
];
$reData = $this->wxV3Post('https://api.mch.weixin.qq.com/v3/payscore/serviceorder', $para);
if ($reData['code'] == 200) {
$result = $this->queryOrder($order_no);
if ($this->isMyError($result)) {
return $this->myError($result['msg']);
}
if ($result != 'ing') {
return $this->myError('创建支付单失败');
}
return true;
} else {
return $this->myError($reData['data']['message']);
}
}
public function queryOrder($out_order_no)
{
$para = [
'appid' => $this->wxConfig['appid'],
'service_id' => $this->wxConfig['service_id'],
'out_order_no' => $out_order_no
];
$reData = $this->wxV3Get("https://api.mch.weixin.qq.com/v3/payscore/serviceorder", $para);
/* state 表示当前单据状态
枚举值:
CREATED:商户已创建服务订单;
DOING:服务订单进行中;
DONE:服务订单完成;
REVOKED:商户取消服务订单;
EXPIRED:服务订单已失效,"商户已创建服务订单"状态超过30天未变动,则订单失效
示例值:CREATED*/
if ($reData['code'] != 200) {
return $this->myError($reData['message']);
}
$state = $reData['data']['state'];
if ($state == 'DONE') {
// 已完成
return 'done';
} else if ($state == 'REVOKED' || 'EXPIRED' == $state) {
// 已结束
return 'closed';
} else if ('DOING' == $state) {
// 进行中
return 'ing';
} else {
return $this->myError('状态异常');
}
}
/**
* 查询授权记录
* @param $openid
*/
public function permissions($openid)
{
$para = [ // JSON请求体
// 'out_order_no' => $order_no,//订单号
'appid' => $this->wxConfig['appid'],
'service_id' => $this->wxConfig['service_id'],
];
$reData = $this->wxV3Get("https://api.mch.weixin.qq.com/v3/payscore/permissions/openid/{$openid}", $para);
if ($reData['code'] == 200) {
if ($reData['data']['authorization_state'] == 'AVAILABLE') {
return true;
}
}
return false;
}
public function getOpenid($redirect_uri)
{
$appid = $this->wxConfig['appid'];
$secret = $this->wxConfig['appSecret'];
if (isset($_GET['code']) && !empty($_GET['code'])) {
$code = $_GET['code'];
// 获取openid
$weixin = file_get_contents("https://api.weixin.qq.com/sns/oauth2/access_token?appid=$appid&secret=$secret&code=$code&grant_type=authorization_code");
//通过code换取网页授权access_token
$jsondecode = json_decode($weixin); //对JSON格式的字符串进行编码
$array = get_object_vars($jsondecode);//转换成数组
if (!isset($array['openid'])) {
die('get openid fail!');
}
$openid = $array['openid'];//输出openid
//第二步:根据全局access_token和openid查询用户信息
$access_token = $array["access_token"];
$openid = $array['openid'];
$get_user_info_url = "https://api.weixin.qq.com/sns/userinfo?access_token=$access_token&openid=$openid&lang=zh_CN";
$userInfo = $this->getJson($get_user_info_url);
return $openid;
} else {
// 获取code
$redirect_uri = urlencode($redirect_uri);
$url = 'https://open.weixin.qq.com/connect/oauth2/authorize?appid=' . $appid . '&redirect_uri=' . $redirect_uri . '&response_type=code&scope=snsapi_base&state=1#wechat_redirect';
header('location:' . $url);
die();
}
}
function getJson($url)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$output = curl_exec($ch);
curl_close($ch);
return json_decode($output, true);
}
/**
* 完结订单-扣款
* @param $order_no
* @param $price
* @return array
*/
public function endOrder($order_no, $price)
{
$out_order_no = $order_no;
// $price = 0;
// $price = 0.01;
$price = (int)bcmul($price, 100, 0);
if ($price == 0) {
$para = [
// 'out_order_no' => $order_no,
'appid' => $this->wxConfig['appid'],
'service_id' => $this->wxConfig['service_id'],
'finish_type' => 1,//1取消,2完结
'cancel_reason' => '购物取消',
'total_amount' => 0,
'post_payments' => [['name' => '支付类目名称', 'amount' => 0]],
'time_range' => ['end_time' => date('YmdHis', time() - 1)],
];
} else {
$para = [
// 'out_order_no' => $order_no,
'appid' => $this->wxConfig['appid'],
'service_id' => $this->wxConfig['service_id'],
'finish_type' => 2,//1取消,2完结
'cancel_reason' => '称重鲜售货柜',
'real_service_end_time' => date('YmdHis', time()),
'post_payments' => [['name' => "支付类目名称", 'amount' => $price]],
'total_amount' => $price,//总金额
'time_range' => ['end_time' => date('YmdHis', time() - 1)],
];
}
$reData = $this->wxV3Post("https://api.mch.weixin.qq.com/v3/payscore/serviceorder/{$out_order_no}/complete", $para);
if ($reData['code'] == 200) {
return true;
} else {
return false;
}
}
public function isMyError($result)
{
if (isset($result['code']) && $result['code'] != 0) {
return true;
}
return false;
}
public function myError($msg, $code = 1)
{
return ['msg' => $msg, 'code' => $code, 'data' => []];
}
protected function wxV3Post($url, $para)
{
$url_parts = parse_url($url);
$canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
//当前时间戳
$timestamp = time();
//随机字符串
$nonce = $this->getRandomStr(32);
//POST请求时
$body = json_encode($para);
$message = "POST\n" . $canonical_url . "\n" . $timestamp . "\n" . $nonce . "\n" . $body . "\n";
//生成签名
openssl_sign($message, $raw_sign, openssl_get_privatekey($this->mch_private_key), 'sha256WithRSAEncryption');
$sign = base64_encode($raw_sign);
//Authorization 类型
$schema = 'WECHATPAY2-SHA256-RSA2048';
//生成token
$token = sprintf('mchid="%s",serial_no="%s",nonce_str="%s",timestamp="%d",signature="%s"', $this->wxConfig['mch_id'], $this->wxConfig['serial_no'], $nonce, $timestamp, $sign);
$header = [
'Content-Type:application/json',
'Accept:application/json',
'User-Agent:*/*',
'Authorization: ' . $schema . ' ' . $token
];
// print_r($para);
// print_r($header);
// exit;
$curl = curl_init(); // 启动一个CURL会话
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $body);
curl_setopt($curl, CURLOPT_HEADER, 0);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); // 跳过证书检查
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); // 从证书中检查SSL加密算法是否存在
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
$tmpInfo = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
return ['code' => $httpCode, 'data' => json_decode($tmpInfo, true)];
}
private function wxV3Get($url, $para)
{
$para['service_id'] = $this->wxConfig['service_id'];
$para['appid'] = $this->wxConfig['appid'];
$url = "$url?" . http_build_query($para);
$header = $this->createAuthorization($url);
$curl = curl_init(); // 启动一个CURL会话
curl_setopt($curl, CURLOPT_URL, $url);
curl_setopt($curl, CURLOPT_HEADER, 0);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); // 跳过证书检查
curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); // 从证书中检查SSL加密算法是否存在
curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
$tmpInfo = curl_exec($curl);
$httpCode = curl_getinfo($curl, CURLINFO_HTTP_CODE);
curl_close($curl);
return ['code' => $httpCode, 'data' => json_decode($tmpInfo, true)];
}
/**
* 验证是否开启支付分
*/
public function verifySign($openid)
{
$para = [
'appid' => $this->wxConfig['appid'],
'service_id' => $this->wxConfig['service_id'],
'openid' => $openid
];
$gR = $this->wxV3Get("https://api.mch.weixin.qq.com/v3/payscore/user-service-state", $para);
if ($gR['code'] == 200) {
if ("AVAILABLE" == $gR['data']['use_service_state']) {
return 1;
} else {
return 0;
}
} else {
return 0;
}
}
/**
* 微信支付分签约页面
* @return string
*/
public function paySign()
{
//以下是jsapi签名动作
$time = time();
$noncestr = $this->getRandomStr(16);
//获取access_token
$access_token = '';
if (!$access_token) {
$appid = $this->wxConfig['appid'];
$appSecret = $this->wxConfig['appSecret'];
$url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$appid&secret=$appSecret";
$reData = file_get_contents($url);
$reData = json_decode($reData, true);
if (!isset($reData['access_token'])) {
echo $reData['errmsg'];
exit;
}
$access_token = $reData['access_token'];
// Cache::set('wx_access_token', $access_token, 7000);
}
//获取jsapi_ticket
$jsapi_ticket = '';
if (!$jsapi_ticket) {
$url = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?access_token=$access_token&type=jsapi";
$reData = file_get_contents($url);
$reData = json_decode($reData, true);
$jsapi_ticket = $reData['ticket'];
// Cache::set('wx_jsapi_ticket', $jsapi_ticket, 7000);
}
//执行签约跳转
$jsapi = [
'noncestr' => $noncestr,
'jsapi_ticket' => $jsapi_ticket,
'timestamp' => $time,
'url' => "https://xxx/api/payment/paySign"//此处注意,需完整的URL地址,例如伪静态生成的.html或带参数get地址
];
$jsapi['signature'] = sha1($this->asc_sort($jsapi));
$jsapi['appid'] = $this->wxConfig['appid'];
//以下是跳转签约页面的动作
$singData = [
'mch_id' => $this->wxConfig['mch_id'],
'nonce_str' => $noncestr,//使用签名的随机字符串
'out_request_no' => $this->getRandomStr(16),
'service_id' => $this->wxConfig['service_id'],
'sign_type' => 'HMAC-SHA256',
'timestamp' => $time,
];
$stringSignTemp = $this->asc_sort($singData);
$stringSignTemp .= "&key=" . $this->wxConfig['key'];
$singData['sign'] = strtoupper(hash_hmac('sha256', $stringSignTemp, $this->wxConfig['key']));//签名值,mch_id、service_id、out_request_no、timestamp、nonce_str、sign_type
$data['jsapi'] = $jsapi;
$data['para'] = $singData;
//签约成功后跳转的页面
// $data['paySignPage'] = $this->wxConfig['pay_sign_page'];
$data['paySignPage'] = session('pay_sign_page');
// 模板输出
return view('wx.paySign', $data);
}
//生成v3 Authorization
protected function createAuthorization($url)
{
if (!in_array('sha256WithRSAEncryption', \openssl_get_md_methods(true))) {
throw new \RuntimeException("当前PHP环境不支持SHA256withRSA");
}
$url_parts = parse_url($url);
$canonical_url = ($url_parts['path'] . (!empty($url_parts['query']) ? "?${url_parts['query']}" : ""));
//当前时间戳
$timestamp = time();
//随机字符串
$nonce = $this->getRandomStr(32);
//POST请求时
$body = "";
$message = "GET\n" . $canonical_url . "\n" . $timestamp . "\n" . $nonce . "\n" . $body . "\n";
//生成签名
openssl_sign($message, $raw_sign, openssl_get_privatekey($this->mch_private_key), 'sha256WithRSAEncryption');
$sign = base64_encode($raw_sign);
//Authorization 类型
$schema = 'WECHATPAY2-SHA256-RSA2048';
//生成token
$token = sprintf('mchid="%s",serial_no="%s",nonce_str="%s",timestamp="%d",signature="%s"', $this->wxConfig['mch_id'], $this->wxConfig['serial_no'], $nonce, $timestamp, $sign);
$header = [
'Content-Type:application/json',
'Accept:application/json',
'User-Agent:*/*',
'Authorization: ' . $schema . ' ' . $token
];
return $header;
}
/**
* 获得随机字符串
* @param $len 需要的长度
* @param $special 是否需要特殊符号
* @return string 返回随机字符串
*/
function getRandomStr($len, $special = false)
{
$chars = array(
"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k",
"l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v",
"w", "x", "y", "z", "A", "B", "C", "D", "E", "F", "G",
"H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R",
"S", "T", "U", "V", "W", "X", "Y", "Z", "0", "1", "2",
"3", "4", "5", "6", "7", "8", "9"
);
if ($special) {
$chars = array_merge($chars, array(
"!", "@", "#", "$", "?", "|", "{", "/", ":", ";",
"%", "^", "&", "*", "(", ")", "-", "_", "[", "]",
"}", "<", ">", "~", "+", "=", ",", "."
));
}
$charsLen = count($chars) - 1;
shuffle($chars); //打乱数组顺序
$str = '';
for ($i = 0; $i < $len; $i++) {
$str .= $chars[mt_rand(0, $charsLen)]; //随机取出一位
}
return $str;
}
/**ascii码从小到大排序
* @param array $params
* @return bool|string
*/
private function asc_sort($params = array())
{
if (!empty($params)) {
$p = ksort($params);
if ($p) {
$str = '';
foreach ($params as $k => $val) {
$str .= $k . '=' . $val . '&';
}
$strs = rtrim($str, '&');
return $strs;
}
}
return false;
}
}