389 lines
11 KiB
PHP
389 lines
11 KiB
PHP
<?php
|
|
|
|
namespace app\common\service;
|
|
|
|
use app\common\model\User;
|
|
|
|
use EasyWeChat\MiniApp\Application as xcxApplication;
|
|
use EasyWeChat\OpenPlatform\Application;
|
|
use Exception;
|
|
use support\Log;
|
|
use support\Redis;
|
|
use GuzzleHttp\Client;
|
|
use support\exception\BusinessException;
|
|
|
|
class WechatService
|
|
{
|
|
|
|
protected array $web;
|
|
protected array $mini;
|
|
protected array $login;
|
|
protected $app;
|
|
|
|
public function __construct()
|
|
{
|
|
$cfg = config('wechat');
|
|
$this->web = $cfg['web'];
|
|
$this->mini = $cfg['mini'];
|
|
$this->login = $cfg['login'];
|
|
}
|
|
|
|
/**
|
|
* 获取微信二维码
|
|
*/
|
|
public function makeQrUrl()
|
|
{
|
|
try {
|
|
$state = bin2hex(random_bytes(16));
|
|
$ttl = (int)($this->login['state_ttl'] ?? 300);
|
|
|
|
// 记录 state
|
|
Redis::setEx("wx:login:state:$state", $ttl, json_encode([
|
|
'redirect' => $this->web['redirect'],
|
|
'ts' => time(),
|
|
], JSON_UNESCAPED_UNICODE));
|
|
|
|
// 组装跳转地址
|
|
$qrUrl = sprintf(
|
|
'https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=%s&scope=%s&state=%s#wechat_redirect',
|
|
$this->web['app_id'],
|
|
urlencode($this->web['redirect']),
|
|
'code',
|
|
'snsapi_login',
|
|
$state,
|
|
);
|
|
|
|
$data = [
|
|
'state' => $state,
|
|
'qr_url' => $qrUrl
|
|
];
|
|
return $data;
|
|
} catch (\Throwable $th) {
|
|
// var_dump($th->getFile().'|'.$th->getLine().'|'.$th->getMessage());
|
|
throw new BusinessException('获取微信二维码失败');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取微信access_token
|
|
*/
|
|
public function wechatWebAccessToken(string $code, string $state): array
|
|
{
|
|
try {
|
|
// 校验 state
|
|
$stateJson = Redis::get("wx:login:state:$state");
|
|
if (!$stateJson) {
|
|
throw new \RuntimeException('state 已过期或无效');
|
|
}
|
|
|
|
// 拼接地址
|
|
$tokenUrl = sprintf(
|
|
'https://api.weixin.qq.com/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code',
|
|
$this->web['app_id'],
|
|
$this->web['app_secret'],
|
|
$code
|
|
);
|
|
|
|
// code 换 token
|
|
$token = $this->httpGetJson($tokenUrl);
|
|
if (!isset($token['access_token'], $token['openid'])) {
|
|
throw new \RuntimeException('换取 access_token 失败:'. json_encode($token, JSON_UNESCAPED_UNICODE));
|
|
}
|
|
return $token;
|
|
} catch (\Throwable $th) {
|
|
throw new BusinessException('获取 access_token 失败');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 获取微信用户信息
|
|
*
|
|
* @param array $token
|
|
* @return array
|
|
*/
|
|
public function wechatWebUserInfo(array $accessToken): array
|
|
{
|
|
try {
|
|
// 拉取用户信息
|
|
return $this->httpGetJson(sprintf(
|
|
'https://api.weixin.qq.com/sns/userinfo?access_token=%s&openid=%s',
|
|
$accessToken['access_token'],
|
|
$accessToken['openid']
|
|
));
|
|
} catch (\Throwable $th) {
|
|
// var_dump($th->getFile().'|'.$th->getLine().'|'.$th->getMessage());
|
|
throw new BusinessException('获取用户信息失败');
|
|
}
|
|
}
|
|
|
|
/**
|
|
* tools - httpGetJson
|
|
*/
|
|
protected function httpGetJson(string $url): array
|
|
{
|
|
$client = new Client([
|
|
'verify' => false, // 禁用 SSL 验证
|
|
'timeout' => 10,
|
|
]);
|
|
$json = $client->get($url)->getBody()->getContents();
|
|
return json_decode($json, true) ?: [];
|
|
}
|
|
|
|
|
|
//----------------------------小程序登录----------------------------------------
|
|
|
|
/**
|
|
* 获取 openid / session_key
|
|
* @param $code
|
|
* @return mixed
|
|
*/
|
|
public function miniCode2Session($code)
|
|
{
|
|
$appid = $this->mini['app_id'];
|
|
$secret = $this->mini['app_secret'];
|
|
$url = "https://api.weixin.qq.com/sns/jscode2session?appid=$appid&secret=$secret&js_code=$code&grant_type=authorization_code";
|
|
return json_decode(file_get_contents($url), true);
|
|
}
|
|
|
|
/**
|
|
* 获取 access_token
|
|
* @return mixed
|
|
*/
|
|
public function getMiniAccessToken()
|
|
{
|
|
$appid = $this->mini['app_id'];
|
|
$secret = $this->mini['app_secret'];
|
|
$url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=$appid&secret=$secret";
|
|
return json_decode(file_get_contents($url), true);
|
|
}
|
|
|
|
/**
|
|
* 调用微信接口取手机号
|
|
* @param $access_token
|
|
* @param $phone_code
|
|
* @return mixed
|
|
*/
|
|
public function getMiniPhoneNumber($access_token, $phone_code)
|
|
{
|
|
$url = "https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token={$access_token}";
|
|
|
|
$data = json_encode([
|
|
'code' => $phone_code
|
|
]);
|
|
|
|
$options = [
|
|
'http' => [
|
|
'header' => "Content-type: application/json",
|
|
'method' => 'POST',
|
|
'content' => $data,
|
|
],
|
|
];
|
|
|
|
return json_decode(file_get_contents($url, false, stream_context_create($options)), true);
|
|
}
|
|
|
|
/**
|
|
* 小程序获取openid
|
|
* @param string $code
|
|
* @return array
|
|
* @throws BusinessException
|
|
*/
|
|
public function xcxOpenid(string $code): array
|
|
{
|
|
|
|
$config = [
|
|
'app_id' => getenv('WECHAT_MINI_APPID'),
|
|
'secret' => getenv('WECHAT_MINI_SECRET'),
|
|
'response_type' => 'array',
|
|
'log' => [
|
|
'level' => 'debug',
|
|
'file' => runtime_path().'/mzl/wechat.log',
|
|
]
|
|
];
|
|
// 创建微信小程序对象
|
|
$wechat = new xcxApplication($config);
|
|
// 获取 session_key 和 openid
|
|
return $wechat->getUtils()->codeToSession($code);
|
|
}
|
|
|
|
/**
|
|
* 小程序获取手机号
|
|
* @param string $phone_code
|
|
* @return mixed
|
|
* @throws BusinessException
|
|
*/
|
|
public function xcxPhoneNumber(string $phone_code)
|
|
{
|
|
$config = [
|
|
'app_id' => getenv('WECHAT_MINI_APPID'),
|
|
'secret' => getenv('WECHAT_MINI_SECRET'),
|
|
'response_type' => 'array',
|
|
'log' => [
|
|
'level' => 'debug',
|
|
'file' => runtime_path().'/mzl/wechat.log',
|
|
]
|
|
];
|
|
// 创建微信小程序对象
|
|
$wechat = new xcxApplication($config);
|
|
// getPhoneNumber
|
|
$data = ['code' => $phone_code];
|
|
$result = $wechat->getClient()->postJson('wxa/business/getuserphonenumber', $data);
|
|
if (isset($result['phone_info'])) {
|
|
// 业务处理 $result['phone_info']['phoneNumber']
|
|
return $result;
|
|
} else {
|
|
throw new BusinessException($result['errmsg']);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 文本安全检测
|
|
* @param string $content 待检测文本内容
|
|
* @param string $openid 用户openid(可选)
|
|
* @param string $scene 场景值(1-资料,2-评论,3-论坛,4-社交日志)
|
|
* @throws Exception
|
|
*/
|
|
public function checkText($content, $openid, $accessToken, $scene = 1)
|
|
{
|
|
// 1. 参数校验
|
|
if (empty($content)) {
|
|
throw new Exception('检测内容不能为空');
|
|
}
|
|
|
|
// 2. 长度限制检查(API限制为2500字节)
|
|
$contentLength = strlen($content);
|
|
if ($contentLength > 2500) {
|
|
throw new Exception('文本内容过长,最大支持2500字节');
|
|
}
|
|
|
|
// 3. 构建请求数据
|
|
$requestData = [
|
|
'content' => $content,
|
|
'version' => 2, // 使用v2接口
|
|
'scene' => intval($scene), // 场景值
|
|
'openid' => $openid ?: ''
|
|
];
|
|
|
|
// 4. 调用微信API
|
|
$apiUrl = "https://api.weixin.qq.com/wxa/msg_sec_check?access_token={$accessToken}";
|
|
$result = $this->httpPost($apiUrl, json_encode($requestData, JSON_UNESCAPED_UNICODE));
|
|
|
|
// 5. 解析响应
|
|
$response = json_decode($result, true);
|
|
|
|
if (empty($response)) {
|
|
throw new Exception('API响应解析失败');
|
|
}
|
|
// 6. 处理结果
|
|
if($response['errcode'] == 0){
|
|
foreach ($response['detail'] as $detail) {
|
|
if (isset($detail['suggest'])&& ($detail['suggest'] == 'risky' || $detail['suggest'] == 'review')){
|
|
throw new Exception('内容含有违法违规内容');
|
|
}
|
|
}
|
|
return true;
|
|
}elseif($response['errcode'] == 87014){
|
|
throw new Exception('内容含有违法违规内容');
|
|
}
|
|
Log::info('checkText',[$response]);
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* 解析API响应结果
|
|
* @param array $response API响应
|
|
* @return array
|
|
*/
|
|
private function parseResult($response)
|
|
{
|
|
$result = [
|
|
'success' => false,
|
|
'safe' => false,
|
|
'message' => '',
|
|
'errCode' => $response['errcode'] ?? -1,
|
|
'data' => $response
|
|
];
|
|
|
|
// 错误码处理
|
|
switch ($response['errcode']) {
|
|
case 0:
|
|
// 检测通过
|
|
return true;
|
|
break;
|
|
|
|
case 87014:
|
|
// 风险内容
|
|
throw new Exception('内容含有违法违规内容');
|
|
break;
|
|
|
|
case 40001:
|
|
$result['message'] = 'access_token无效或已过期';
|
|
break;
|
|
|
|
case 40003:
|
|
$result['message'] = 'openid无效';
|
|
break;
|
|
|
|
case 40097:
|
|
$result['message'] = '参数错误';
|
|
break;
|
|
|
|
case 48001:
|
|
$result['message'] = '接口功能未授权,请确认小程序已开通此接口权限';
|
|
break;
|
|
|
|
default:
|
|
$result['message'] = $response['errmsg'] ?? '未知错误';
|
|
break;
|
|
}
|
|
|
|
// 如果有详细结果,添加到返回数据中
|
|
if (isset($response['result'])) {
|
|
$result['detail'] = $response['result'];
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* HTTP POST请求
|
|
* @param string $url 请求地址
|
|
* @param string $data POST数据
|
|
* @return string 响应内容
|
|
* @throws Exception
|
|
*/
|
|
private function httpPost($url, $data)
|
|
{
|
|
$ch = curl_init();
|
|
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_URL => $url,
|
|
CURLOPT_POST => true,
|
|
CURLOPT_POSTFIELDS => $data,
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_TIMEOUT => 10,
|
|
CURLOPT_SSL_VERIFYPEER => false,
|
|
CURLOPT_SSL_VERIFYHOST => false,
|
|
CURLOPT_HTTPHEADER => [
|
|
'Content-Type: application/json',
|
|
'Content-Length: ' . strlen($data)
|
|
]
|
|
]);
|
|
|
|
$response = curl_exec($ch);
|
|
$error = curl_error($ch);
|
|
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
|
|
curl_close($ch);
|
|
|
|
if ($error) {
|
|
throw new Exception("HTTP请求失败: {$error}");
|
|
}
|
|
|
|
if ($httpCode !== 200) {
|
|
throw new Exception("HTTP状态码异常: {$httpCode}");
|
|
}
|
|
|
|
return $response;
|
|
}
|
|
} |