You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
think-library/src/service/wechat/WebAppService.php

783 lines
26 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

<?php
// +----------------------------------------------------------------------
// | ThinkLibrary 6.0 for ThinkPhP 6.0
// +----------------------------------------------------------------------
// | 版权所有 2017~2020 [ https://www.dtapp.net ]
// +----------------------------------------------------------------------
// | 官方网站: https://gitee.com/liguangchun/ThinkLibrary
// +----------------------------------------------------------------------
// | 开源协议 ( https://mit-license.org )
// +----------------------------------------------------------------------
// | gitee 仓库地址 https://gitee.com/liguangchun/ThinkLibrary
// | github 仓库地址 https://github.com/GC0202/ThinkLibrary
// | gitlab 仓库地址 https://gitlab.com/liguangchun/thinklibrary
// | weixin 仓库地址 https://git.weixin.qq.com/liguangchun/ThinkLibrary
// | huaweicloud 仓库地址 https://codehub-cn-south-1.devcloud.huaweicloud.com/composer00001/ThinkLibrary.git
// | Packagist 地址 https://packagist.org/packages/liguangchun/think-library
// +----------------------------------------------------------------------
namespace DtApp\ThinkLibrary\service\wechat;
use DtApp\ThinkLibrary\exception\DtaException;
use DtApp\ThinkLibrary\facade\Pregs;
use DtApp\ThinkLibrary\facade\Randoms;
use DtApp\ThinkLibrary\facade\Urls;
use DtApp\ThinkLibrary\facade\Xmls;
use DtApp\ThinkLibrary\Service;
use DtApp\ThinkLibrary\service\curl\HttpService;
use think\db\exception\DbException;
/**
* 公众号
* Class WebAppService
* @package DtApp\ThinkLibrary\service\WeChat
*/
class WebAppService extends Service
{
private $open_url = "https://open.weixin.qq.com/";
private $api_url = "https://api.weixin.qq.com/";
/**
* 公众号的唯一标识
* @var
*/
private $app_id;
/**
* 公众号的appsecret
* @var
*/
private $app_secret;
/**
* 授权后重定向的回调链接地址, 请使用 urlEncode 对链接进行处理
* @var
*/
private $redirect_uri;
/**
* 返回类型请填写code
* @var string
*/
private $response_type = 'code';
private $scope = "snsapi_base";
private $state = "";
private $grant_type = "authorization_code";
/**
* 驱动方式
* @var string
*/
private $cache = "file";
/**
* 商户平台设置的密钥key
* @var
*/
private $mch_key;
/**
* @param string $mchKey
* @return $this
*/
public function mchKey(string $mchKey)
{
$this->mch_key = $mchKey;
return $this;
}
/**
* 商户号
* @var
*/
private $mch_id;
/**
* 商户号
* @param string $mchId
* @return $this
*/
public function mchId(string $mchId)
{
$this->mch_id = $mchId;
return $this;
}
/**
* 公众号的唯一标识
* @param string $appId
* @return $this
*/
public function appId(string $appId)
{
$this->app_id = $appId;
return $this;
}
/**
* 公众号的appsecret
* @param string $appSecret
* @return $this
*/
public function appSecret(string $appSecret)
{
$this->app_secret = $appSecret;
return $this;
}
/**
* 获取配置信息
* @return $this
*/
private function getConfig()
{
$this->cache = config('dtapp.wechat.webapp.cache');
$this->app_id = config('dtapp.wechat.webapp.app_id');
$this->app_secret = config('dtapp.wechat.webapp.app_secret');
return $this;
}
/**
* 授权后重定向的回调链接地址, 请使用 urlEncode 对链接进行处理
* @param string $redirectUri
* @return $this
* @throws DtaException
*/
public function redirectUri(string $redirectUri)
{
if (empty(Pregs::isLink($redirectUri))) {
throw new DtaException("请检查redirectUri是否正确");
}
$this->redirect_uri = Urls::lenCode($redirectUri);
return $this;
}
/**
* 应用授权作用域snsapi_base 不弹出授权页面直接跳转只能获取用户openidsnsapi_userinfo 弹出授权页面可通过openid拿到昵称、性别、所在地。并且 即使在未关注的情况下,只要用户授权,也能获取其信息
* @param string $scope
* @return $this
* @throws DtaException
*/
public function scope(string $scope)
{
if ($scope === "snsapi_base") {
$this->scope = $scope;
} elseif ($scope === "snsapi_userinfo") {
$this->scope = $scope;
} else {
throw new DtaException("请检查scope参数");
}
return $this;
}
/**
* 重定向后会带上state参数开发者可以填写a-zA-Z0-9的参数值最多128字节
* @param string $state
* @return $this
*/
public function state(string $state)
{
$this->state = $state;
return $this;
}
/**
* 驱动方式
* @param string $cache
* @return $this
*/
public function cache(string $cache)
{
$this->cache = $cache;
return $this;
}
/**
* 网页授权
* https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html#0
* @return void
* @throws DtaException
*/
public function oauth2()
{
if (empty($this->app_id)) {
$this->getConfig();
}
if (strlen($this->state) > 128) {
throw new DtaException("请检查state参数最多128字节");
}
$params = Urls::toParams([
'appid' => $this->app_id,
'redirect_uri' => $this->redirect_uri,
'response_type' => $this->response_type,
'scope' => $this->scope,
'state' => $this->state
]);
return header("Location:{$this->open_url}connect/oauth2/authorize?$params#wechat_redirect");
}
/**
* 通过code换取网页授权access_token
* @param string $code
* @param bool $is
* @return array|bool|mixed|string
* @throws DtaException
*/
public function accessToken(string $code, bool $is = true)
{
if (empty($this->app_id) || empty($this->app_secret)) {
$this->getConfig();
}
if (empty($this->app_id)) {
throw new DtaException('请检查app_id参数');
}
if (empty($this->app_secret)) {
throw new DtaException('请检查app_secret参数');
}
return HttpService::instance()
->url("{$this->api_url}sns/oauth2/access_token?appid={$this->app_id}&secret={$this->app_secret}&code={$code}&grant_type={$this->grant_type}")
->toArray($is);
}
/**
* 刷新access_token如果需要
* @param string $refreshToken
* @param bool $is
* @return array|bool|mixed|string
* @throws DtaException
*/
public function refreshToken(string $refreshToken, bool $is = true)
{
if (empty($this->app_id)) {
$this->getConfig();
}
if (empty($this->app_id)) {
throw new DtaException('请检查app_id参数');
}
$this->grant_type = "refresh_token";
return HttpService::instance()
->url("{$this->api_url}sns/oauth2/refresh_token?appid={$this->app_id}&grant_type={$this->grant_type}&refresh_token={$refreshToken}")
->toArray($is);
}
/**
* 拉取用户信息(需scope为 snsapi_userinfo)
* @param string $accessToken
* @param string $openid
* @param string $lang
* @param bool $is
* @return array|bool|mixed|string
*/
public function useInfo(string $accessToken, string $openid, $lang = "zh_CN", bool $is = true)
{
if (empty($this->app_id) || empty($this->app_secret)) {
$this->getConfig();
}
return HttpService::instance()
->url("{$this->api_url}sns/userinfo?access_token={$accessToken}&openid={$openid}&lang={$lang}")
->toArray($is);
}
/**
* 检验授权凭证access_token是否有效
* @param string $accessToken
* @param string $openid
* @param bool $is
* @return array|bool|mixed|string
*/
public function auth(string $accessToken, string $openid, bool $is = true)
{
if (empty($this->app_id) || empty($this->app_secret)) {
$this->getConfig();
}
return HttpService::instance()
->url("{$this->api_url}sns/auth?access_token={$accessToken}&openid={$openid}")
->toArray($is);
}
/**
* 分享
* @return array
* @throws DbException
* @throws DtaException
*/
public function share()
{
if (empty($this->app_id)) {
$this->getConfig();
}
if (empty($this->app_id)) {
throw new DtaException('请检查app_id参数');
}
// 获取数据
$accessToken = $this->getAccessToken();
if (!isset($accessToken['access_token'])) {
throw new DtaException("获取access_token错误" . $accessToken['errmsg']);
}
$res = HttpService::instance()
->url("{$this->api_url}cgi-bin/ticket/getticket?access_token={$accessToken['access_token']}&type=jsapi")
->toArray();
if (!empty($res['errcode'])) {
// 获取数据
$accessToken = $this->getAccessToken();
if (!isset($accessToken['access_token'])) {
throw new DtaException("获取access_token错误" . $accessToken['errmsg']);
}
$res = HttpService::instance()
->url("{$this->api_url}cgi-bin/ticket/getticket?access_token={$accessToken['access_token']}&type=jsapi")
->toArray();
if (!empty($res['errcode'])) {
throw new DtaException('accessToken已过期');
}
}
// 注意 URL 一定要动态获取,不能 hardcode.
$protocol = ((!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') || $_SERVER['SERVER_PORT'] == 443) ? "https://" : "http://";
$url = "$protocol$_SERVER[HTTP_HOST]$_SERVER[REQUEST_URI]";
$timestamp = time();
$nonceStr = $this->createNonceStr();
// 获得jsapi_ticket之后就可以生成JS-SDK权限验证的签名了。
$jsapiTicket = $res['ticket'];
// 这里参数的顺序要按照 key 值 ASCII 码升序排序
$string = "jsapi_ticket=$jsapiTicket&noncestr=$nonceStr&timestamp=$timestamp&url=$url";
return [
"appId" => $this->app_id,
"nonceStr" => $nonceStr,
"timestamp" => $timestamp,
"url" => $url,
"signature" => sha1($string),
"rawString" => $string
];
}
private function createNonceStr($length = 16)
{
$chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
$str = "";
for ($i = 0; $i < $length; $i++) {
$str .= $chars[random_int(0, strlen($chars) - 1)];
}
return $str;
}
/**
* 生成二维码
* @param array $data
* @return array|bool|mixed|string
* @throws DbException
* @throws DtaException
*/
public function qrCode(array $data)
{
// 获取数据
$accessToken = $this->getAccessToken();
return HttpService::instance()
->url("{$this->api_url}cgi-bin/qrcode/create?access_token={$accessToken['access_token']}")
->data($data)
->post()
->toArray();
}
/**
* 发送模板消息
* @param array $data
* @return array|bool|mixed|string
* @throws DbException
* @throws DtaException
*/
public function messageTemplateSend(array $data = [])
{
// 获取数据
$accessToken = $this->getAccessToken();
$url = "{$this->api_url}cgi-bin/message/template/send?access_token={$accessToken['access_token']}";
return HttpService::instance()
->url($url)
->data($data)
->toArray();
}
/**
* 设置所属行业
* https://developers.weixin.qq.com/doc/offiaccount/Message_Management/Template_Message_Interface.html#0
* @param string $access_token
* @param array $data
* @return bool|mixed|string
* @throws DbException
* @throws DtaException
*/
public function setIndustry(string $access_token, array $data = [])
{
// 获取数据
$accessToken = $this->getAccessToken();
$url = "{$this->api_url}cgi-bin/template/api_set_industry?access_token={$accessToken['access_token']}";
return HttpService::instance()
->url($url)
->data($data)
->toArray();
}
/**
* 将一条长链接转成短链接
* @param string $long_url
* @return bool
* @throws DbException
* @throws DtaException
*/
public function shortUrl(string $long_url)
{
// 获取数据
$accessToken = $this->getAccessToken();
$url = "{$this->api_url}cgi-bin/shorturl?access_token={$accessToken['access_token']}";
return HttpService::instance()
->url($url)
->data([
'action' => 'long2short',
'long_url' => $long_url
])
->toArray();
}
/**
* 连Wi-Fi完成页跳转小程序
* https://developers.weixin.qq.com/doc/offiaccount/WiFi_via_WeChat/WiFi_mini_programs.html
* @param array $data
* @return array|bool|mixed|string
* @throws DbException
* @throws DtaException
*/
public function fiNihPageSet(array $data = [])
{
// 获取数据
$accessToken = $this->getAccessToken();
$url = "{$this->api_url}bizwifi/finishpage/set?access_token={$accessToken['access_token']}";
return HttpService::instance()
->url($url)
->post()
->data($data)
->toArray();
}
/**
* 自定义菜单 获取自定义菜单配置
* https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Getting_Custom_Menu_Configurations.html
* @return array|bool|mixed|string
* @throws DbException
* @throws DtaException
*/
public function menuGet()
{
// 获取数据
$accessToken = $this->getAccessToken();
$url = "{$this->api_url}cgi-bin/menu/get?access_token={$accessToken['access_token']}";
return HttpService::instance()
->url($url)
->toArray();
}
/**
* 自定义菜单 创建个性化菜单
* https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Personalized_menu_interface.html
* @param array $data
* @return array|bool|mixed|string
* @throws DbException
* @throws DtaException
*/
public function menuAddConditional(array $data = [])
{
// 获取数据
$accessToken = $this->getAccessToken();
$url = "{$this->api_url}cgi-bin/menu/addconditional?access_token={$accessToken['access_token']}";
return HttpService::instance()
->url($url)
->post()
->data($data)
->toArray();
}
/**
* 自定义菜单 删除个性化菜单
* https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Personalized_menu_interface.html
* @param array $data
* @return array|bool|mixed|string
* @throws DbException
* @throws DtaException
*/
public function menuDelConditional(array $data = [])
{
// 获取数据
$accessToken = $this->getAccessToken();
$url = "{$this->api_url}cgi-bin/menu/delconditional?access_token={$accessToken['access_token']}";
return HttpService::instance()
->url($url)
->post()
->data($data)
->toArray();
}
/**
* 自定义菜单 测试个性化菜单匹配结果
* https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Personalized_menu_interface.html
* @param array $data
* @return array|bool|mixed|string
* @throws DbException
* @throws DtaException
*/
public function menuTryMatch(array $data = [])
{
// 获取数据
$accessToken = $this->getAccessToken();
$url = "{$this->api_url}cgi-bin/menu/trymatch?access_token={$accessToken['access_token']}";
return HttpService::instance()
->url($url)
->post()
->data($data)
->toArray();
}
/**
* 自定义菜单 删除接口
* https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Deleting_Custom-Defined_Menu.html
* @return array|bool|mixed|string
* @throws DbException
* @throws DtaException
*/
public function menuDelete()
{
// 获取数据
$accessToken = $this->getAccessToken();
$url = "{$this->api_url}cgi-bin/menu/delete?access_token={$accessToken['access_token']}";
return HttpService::instance()
->url($url)
->toArray();
}
/**
* 自定义菜单 查询接口
* https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Querying_Custom_Menus.html
* @return array|bool|mixed|string
* @throws DbException
* @throws DtaException
*/
public function getCurrentSelfmenuInfo()
{
// 获取数据
$accessToken = $this->getAccessToken();
$url = "{$this->api_url}cgi-bin/get_current_selfmenu_info?access_token={$accessToken['access_token']}";
return HttpService::instance()
->url($url)
->toArray();
}
/**
* 自定义菜单 创建接口
* https://developers.weixin.qq.com/doc/offiaccount/Custom_Menus/Creating_Custom-Defined_Menu.html
* @param array $data
* @return array|bool|mixed|string
* @throws DbException
* @throws DtaException
*/
public function menuCreate(array $data = [])
{
// 获取数据
$accessToken = $this->getAccessToken();
$url = "{$this->api_url}cgi-bin/menu/create?access_token={$accessToken['access_token']}";
return HttpService::instance()
->url($url)
->post()
->data($data)
->toArray();
}
/**
* 获取access_token信息
* @return array|bool|mixed|string|string[]
* @throws DbException
* @throws DtaException
*/
private function getAccessToken()
{
if (empty($this->cache) || empty($this->app_id) || empty($this->app_secret)) {
$this->getConfig();
}
if (empty($this->cache)) {
throw new DtaException('请检查cache参数');
}
if (empty($this->app_id)) {
throw new DtaException('请检查app_id参数');
}
if (empty($this->app_secret)) {
throw new DtaException('请检查app_secret参数');
}
$this->grant_type = "client_credential";
if ($this->cache == "file") {
// 文件名
$file = "{$this->app->getRootPath()}runtime/{$this->app_id}_access_token.json";
// 获取数据
$accessToken = file_exists($file) ? json_decode(file_get_contents($file), true) : [];
if (empty($accessToken) || !is_array($accessToken)) {
$accessToken = [
'access_token' => '',
'expires_in' => '',
'expires_time' => '',
];
}
if (empty($accessToken['expires_time'])) {
// 文件不存在
$accessToken_res = HttpService::instance()
->url("{$this->api_url}cgi-bin/token?grant_type={$this->grant_type}&appid={$this->app_id}&secret={$this->app_secret}")
->toArray();
$accessToken_res['expires_time'] = time() + 6000;
file_put_contents($file, json_encode($accessToken_res, JSON_UNESCAPED_UNICODE));
$accessToken = $accessToken_res;
} else if (!isset($accessToken['access_token'])) {
// 内容不存在
$accessToken_res = HttpService::instance()
->url("{$this->api_url}cgi-bin/token?grant_type={$this->grant_type}&appid={$this->app_id}&secret={$this->app_secret}")
->toArray();
$accessToken_res['expires_time'] = time() + 6000;
file_put_contents($file, json_encode($accessToken_res, JSON_UNESCAPED_UNICODE));
$accessToken = $accessToken_res;
} else if ($accessToken['expires_time'] <= time()) {
$accessToken_res = HttpService::instance()
->url("{$this->api_url}cgi-bin/token?grant_type={$this->grant_type}&appid={$this->app_id}&secret={$this->app_secret}")
->toArray();
$accessToken_res['expires_time'] = time() + 6000;
file_put_contents($file, json_encode($accessToken_res, JSON_UNESCAPED_UNICODE));
$accessToken = $accessToken_res;
}
$judge = HttpService::instance()
->url("{$this->api_url}cgi-bin/getcallbackip?access_token={$accessToken['access_token']}")
->toArray();
if (!isset($judge['ip_list'])) {
$accessToken_res = HttpService::instance()
->url("{$this->api_url}cgi-bin/token?grant_type={$this->grant_type}&appid={$this->app_id}&secret={$this->app_secret}")
->toArray();
$accessToken_res['expires_time'] = time() + 6000;
file_put_contents($file, json_encode($accessToken_res, JSON_UNESCAPED_UNICODE));
$accessToken = $accessToken_res;
}
return $accessToken;
} else if ($this->cache == "mysql") {
$access_token = [];
// 文件名
$file = "{$this->app_id}_access_token";
// 获取数据
$cache_mysql_value = dtacache($file);
if (!empty($cache_mysql_value)) {
$access_token['access_token'] = $cache_mysql_value;
} else {
// 获取远程Token
$accessToken_res = HttpService::instance()
->url("{$this->api_url}cgi-bin/token?grant_type={$this->grant_type}&appid={$this->app_id}&secret={$this->app_secret}")
->toArray();
// 保存到数据库
dtacache($file, $accessToken_res['access_token'], 6000);
$access_token['access_token'] = $accessToken_res['access_token'];
}
// 判断token是否可以使用
$judge = HttpService::instance()
->url("{$this->api_url}cgi-bin/getcallbackip?access_token={$access_token['access_token']}")
->toArray();
if (!isset($judge['ip_list'])) {
$accessToken_res = HttpService::instance()
->url("{$this->api_url}cgi-bin/token?grant_type={$this->grant_type}&appid={$this->app_id}&secret={$this->app_secret}")
->toArray();
dtacache($file, $accessToken_res['access_token'], 6000);
$access_token['access_token'] = $accessToken_res['access_token'];
}
return $access_token;
} else {
throw new DtaException("驱动方式错误");
}
}
/**
* 微信支付
* https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=9_1
* @param array $array
* @return bool|string
*/
public function payUnfIedOrder(array $array)
{
$array['appid'] = $this->app_id;
$array['mch_id'] = $this->mch_id;
$array['nonce_str'] = Randoms::generate(32, 3);
$array['sign_type'] = 'HMAC-SHA256';
$array['sign'] = $this->paySign($array);
$res = $this->postXmlCurl(Xmls::toXml($array));
return Xmls::toArray($res);
}
/**
* 微信内H5调起支付
* @param string $prepay_id
* @return array
*/
public function h5Pay(string $prepay_id)
{
$array['appId'] = $this->app_id;
$array['timeStamp'] = time();
$array['nonceStr'] = Randoms::generate(32, 3);
$array['package'] = "prepay_id={$prepay_id}";
$array['signType'] = 'HMAC-SHA256';
$array['paySign'] = $this->paySign($array);
return $array;
}
/**
* 生成支付签名
* @param array $array 参与签名的内容组成的数组
* @param bool $hmacsha256 是否使用 HMAC-SHA256算法否则使用MD5
* @return string
*/
private function paySign(array $array, bool $hmacsha256 = true)
{
// 排序
ksort($array);
// 转成字符串
$stringA = Urls::toParams($array);
// 在字符串接商户支付秘钥
$stringSignTemp = "{$stringA}&key=" . $this->mch_key;
//步骤四MD5或HMAC-SHA256C加密
if ($hmacsha256) {
$str = hash_hmac("sha256", $stringSignTemp, $this->mch_key);
} else {
$str = md5($stringSignTemp);
}
//符转大写
return strtoupper($str);
}
private function postXmlCurl($xml)
{
$ch = curl_init();
//设置超时
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_URL, "https://api.mch.weixin.qq.com/pay/unifiedorder");
//设置header
curl_setopt($ch, CURLOPT_HEADER, FALSE);
//要求结果为字符串且输出到屏幕上
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
//试试手气新增,增加之后 curl 不报 60# 错误,可以请求到微信的响应
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); //不验证 SSL 证书
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);//不验证 SSL 证书域名
//post提交方式
curl_setopt($ch, CURLOPT_POST, TRUE);
curl_setopt($ch, CURLOPT_POSTFIELDS, $xml);
//运行curl
$data = curl_exec($ch);
//返回结果
if ($data) {
curl_close($ch);
return $data;
} else {
$error = curl_errno($ch);
curl_close($ch);
return "curl error, error code " . $error;
}
}
}