yunzer/app/index/controller/WechatController.php
2025-06-06 18:18:31 +08:00

690 lines
26 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace app\index\controller;
use think\facade\Request;
use think\facade\Log;
use think\facade\Cache;
use GuzzleHttp\Client;
use app\index\model\User;
use app\index\model\AdminConfig;
class WechatController extends BaseController
{
/**
* 测试接口是否正常工作
*/
public function test()
{
try {
// 设置响应头
header('Content-Type: application/json; charset=utf-8');
$data = [
'time' => date('Y-m-d H:i:s'),
'status' => 'ok',
'server' => [
'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'] ?? '',
'REQUEST_URI' => $_SERVER['REQUEST_URI'] ?? '',
'HTTP_HOST' => $_SERVER['HTTP_HOST'] ?? '',
'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'] ?? '',
]
];
// 记录详细日志
Log::info('接口测试 - 请求信息:' . json_encode($data, JSON_UNESCAPED_UNICODE));
return json($data, JSON_UNESCAPED_UNICODE);
} catch (\Exception $e) {
$error = [
'error' => $e->getMessage(),
'time' => date('Y-m-d H:i:s')
];
Log::error('接口测试错误:' . json_encode($error, JSON_UNESCAPED_UNICODE));
return json($error, JSON_UNESCAPED_UNICODE);
}
}
public function index()
{
try {
// 设置响应头
header('Content-Type: text/html; charset=utf-8');
// 记录原始请求信息
$requestInfo = [
'time' => date('Y-m-d H:i:s'),
'method' => Request::method(),
'url' => Request::url(true),
'ip' => Request::ip(),
'params' => Request::param(),
'headers' => getallheaders(),
'server' => [
'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'] ?? '',
'REQUEST_URI' => $_SERVER['REQUEST_URI'] ?? '',
'HTTP_HOST' => $_SERVER['HTTP_HOST'] ?? '',
'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'] ?? '',
'QUERY_STRING' => $_SERVER['QUERY_STRING'] ?? '',
'HTTP_USER_AGENT' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'HTTP_REFERER' => $_SERVER['HTTP_REFERER'] ?? '',
]
];
Log::info('微信接口访问请求信息:' . json_encode($requestInfo, JSON_UNESCAPED_UNICODE));
// 获取原始数据
$rawData = file_get_contents("php://input");
if (!empty($rawData)) {
$rawData = mb_convert_encoding($rawData, 'UTF-8', 'UTF-8,GBK,GB2312');
Log::info('微信接口原始数据:' . $rawData);
}
// 检查请求方法
if (Request::method() == 'GET') {
Log::info('微信接口GET请求进行签名验证');
// 首次验证服务器地址的有效性
$this->checkSignature();
} elseif (Request::method() == 'POST') {
Log::info('微信接口POST请求处理消息');
// 接收消息并回复
$response = $this->receiveMessage();
// 直接输出回复的XML字符串
echo $response;
exit;
} else {
Log::error('微信接口:不支持的请求方法 - ' . Request::method());
echo '';
exit;
}
} catch (\Exception $e) {
Log::error('微信接口错误:' . $e->getMessage());
Log::error('错误堆栈:' . $e->getTraceAsString());
// 返回空字符串,避免微信服务器重试
echo '';
exit;
}
}
// 验证签名
protected function checkSignature()
{
try {
$signature = Request::get('signature');
$timestamp = Request::get('timestamp');
$nonce = Request::get('nonce');
$echostr = Request::get('echostr');
$debugInfo = [
'signature' => $signature,
'timestamp' => $timestamp,
'nonce' => $nonce,
'echostr' => $echostr,
'url' => Request::url(true),
'time' => date('Y-m-d H:i:s')
];
Log::info('微信验证参数:', $debugInfo);
if (empty($signature) || empty($timestamp) || empty($nonce)) {
Log::error('微信验证参数缺失', $debugInfo);
exit;
}
$token = AdminConfig::where('config_name', 'wechat_token')->value('config_value');
if (empty($token)) {
Log::error('微信token未配置');
exit;
}
Log::info('微信token' . $token);
$tmpArr = array($token, $timestamp, $nonce);
sort($tmpArr, SORT_STRING);
$tmpStr = implode($tmpArr);
$tmpStr = sha1($tmpStr);
$verifyInfo = [
'tmpStr' => $tmpStr,
'signature' => $signature,
'token' => $token,
'timestamp' => $timestamp,
'nonce' => $nonce
];
Log::info('签名验证:', $verifyInfo);
if ($tmpStr == $signature) {
Log::info('微信签名验证成功');
echo $echostr;
exit;
} else {
Log::error('微信签名验证失败', $verifyInfo);
exit;
}
} catch (\Exception $e) {
Log::error('微信验证签名错误:' . $e->getMessage());
Log::error('错误堆栈:' . $e->getTraceAsString());
exit;
}
}
// 接收消息
protected function receiveMessage()
{
try {
$postStr = file_get_contents("php://input");
if (empty($postStr)) {
Log::error('微信消息:接收数据为空');
return '';
}
// 转换编码
$postStr = mb_convert_encoding($postStr, 'UTF-8', 'UTF-8,GBK,GB2312');
Log::info('微信消息:' . $postStr);
libxml_disable_entity_loader(true);
$postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
if ($postObj === false) {
Log::error('微信消息XML解析失败');
return '';
}
// 检查消息类型
$msgType = strtolower((string) $postObj->MsgType);
Log::info('微信消息类型:' . $msgType);
// 处理事件消息
if ($msgType == 'event') {
$event = strtolower((string) $postObj->Event);
Log::info('微信事件类型:' . $event);
// 处理扫码事件和关注事件
if ($event == 'scan' || $event == 'subscribe') {
$scene_str = trim((string) $postObj->EventKey);
// 如果是关注事件,需要去掉前缀 'qrscene_'
if ($event == 'subscribe' && strpos($scene_str, 'qrscene_') === 0) {
$scene_str = substr($scene_str, 8);
}
$fromUsername = trim((string) $postObj->FromUserName);
$toUsername = trim((string) $postObj->ToUserName);
$ticket = (string) $postObj->Ticket;
Log::info('微信扫码/关注事件', [
'event' => $event,
'scene_str' => $scene_str,
'fromUsername' => $fromUsername,
'toUsername' => $toUsername,
'ticket' => $ticket,
'time' => date('Y-m-d H:i:s')
]);
// 创建票据保存目录
$upload_dir = 'public/storage/uploads/ticket/';
if (!is_dir($upload_dir)) {
mkdir($upload_dir, 0755, true);
Log::info('创建票据保存目录:' . $upload_dir);
}
// 保存扫码数据
$data = [
'openid' => $fromUsername,
'scene_str' => $scene_str,
'ticket' => $ticket,
'scan_time' => time(),
'event' => $event
];
$content = json_encode($data, JSON_UNESCAPED_UNICODE);
$file = $upload_dir . $scene_str . "_" . $ticket . ".json";
file_put_contents($file, $content);
Log::info('保存扫码数据到文件:' . $file);
// 获取用户信息
try {
$accessToken = $this->getGZHAccessToken();
$url = "https://api.weixin.qq.com/cgi-bin/user/info?access_token={$accessToken}&openid={$fromUsername}&lang=zh_CN";
$client = new Client(['verify' => false]);
$response = $client->get($url);
$userInfo = json_decode($response->getBody(), true);
Log::info('获取用户信息结果:' . json_encode($userInfo, JSON_UNESCAPED_UNICODE));
if (isset($userInfo['openid'])) {
// 保存用户信息到数据库
$user = User::where('openid', $fromUsername)->find();
if (!$user) {
$user = new User;
$user->openid = $fromUsername;
$user->name = $userInfo['nickname'] ?? '';
$user->avatar = $userInfo['headimgurl'] ?? '';
$user->save();
Log::info('创建新用户', ['user' => $user->toArray()]);
} else {
// 更新用户信息
$user->name = $userInfo['nickname'] ?? $user->name;
$user->avatar = $userInfo['headimgurl'] ?? $user->avatar;
$user->save();
Log::info('更新用户信息', ['user' => $user->toArray()]);
}
// 更新票据文件中的用户信息
$data['user_info'] = [
'uid' => $user->uid,
'name' => $user->name,
'avatar' => $user->avatar,
'openid' => $user->openid
];
file_put_contents($file, json_encode($data, JSON_UNESCAPED_UNICODE));
Log::info('更新票据文件中的用户信息');
}
} catch (\Exception $e) {
Log::error('获取用户信息失败:' . $e->getMessage());
Log::error('错误堆栈:' . $e->getTraceAsString());
}
return 'success';
}
}
return 'success';
} catch (\Exception $e) {
Log::error('处理微信消息错误:' . $e->getMessage());
Log::error('错误堆栈:' . $e->getTraceAsString());
return 'success';
}
}
// 发送小程序卡片消息的方法
private function sendMiniProgramCard($openid)
{
$accessToken = $this->getGZHAccessToken();
print_r($accessToken);
if (!$accessToken) {
// 处理获取access_token失败的情况
return;
}
$postData = json_encode([
'touser' => $openid, // 接收者用户的openid
'msgtype' => 'miniprogrampage',
'miniprogrampage' => [
'title' => '小程序标题',
'appid' => '小程序appid',
'pagepath' => '小程序路径',
'thumb_media_id' => '你的thumb_media_id'
]
], JSON_UNESCAPED_UNICODE);
$url = "https://api.weixin.qq.com/cgi-bin/message/custom/send?access_token={$accessToken}";
$result = json_decode(file_get_contents($url, false, stream_context_create([
'http' => [
'method' => 'POST',
'header' => "Content-type: application/json\r\n",
'content' => $postData,
'timeout' => 60 // 超时时间(单位:s
]
])), true);
// 处理返回结果
if ($result['errcode'] == 0) {
// 发送成功
} else {
// 发送失败
}
}
// 获取公众号Access token
public function getGZHAccessToken()
{
// 从数据库获取配置
$appid = AdminConfig::where('config_name', 'wechat_appid')->value('config_value');
$secret = AdminConfig::where('config_name', 'wechat_appsecret')->value('config_value');
if (empty($appid) || empty($secret)) {
throw new \Exception('微信配置信息未设置');
}
// 构建请求URL
$url = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={$appid}&secret={$secret}";
try {
// 使用 GuzzleHttp 发送请求禁用SSL验证
$client = new Client([
'verify' => false
]);
$response = $client->get($url);
$data = json_decode($response->getBody(), true);
if (!isset($data['access_token'])) {
throw new \Exception("获取access_token失败: {$data['errmsg']}", $data['errcode'] ?? -1);
}
return $data['access_token'];
} catch (\Exception $e) {
Log::error('获取access_token失败' . $e->getMessage());
throw $e;
}
}
/**
* 获取微信登录二维码
* @return \think\response\Json
*/
public function getLoginTicket()
{
try {
Log::info('开始获取微信登录二维码');
// 获取access_token
$access_token = $this->getGZHAccessToken();
Log::info('获取access_token成功' . $access_token);
// 构建请求URL - 使用正确的接口生成临时二维码
$url = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={$access_token}";
// 生成唯一场景值
$scene_str = md5(uniqid() . time());
Log::info('生成场景值:' . $scene_str);
// 准备请求数据 - 生成临时二维码有效期5分钟
$postData = json_encode([
'expire_seconds' => 300, // 5分钟有效期
'action_name' => 'QR_STR_SCENE',
'action_info' => [
'scene' => [
'scene_str' => $scene_str
]
]
]);
// 发送请求获取ticket
$client = new Client(['verify' => false]);
$response = $client->post($url, [
'body' => $postData,
'headers' => [
'Content-Type' => 'application/json'
]
]);
$result = json_decode($response->getBody(), true);
Log::info('微信返回结果:' . json_encode($result, JSON_UNESCAPED_UNICODE));
if (isset($result['errcode']) && $result['errcode'] != 0) {
Log::error('获取二维码失败:' . $result['errmsg']);
return json(['code' => 1, 'msg' => '获取二维码失败:' . $result['errmsg']]);
}
// 使用ticket获取二维码图片URL
$ticket = urlencode($result['ticket']);
$qrcodeUrl = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={$ticket}";
// 将场景值保存到缓存中,用于后续验证
Cache::set('wx_login_scene_' . $result['ticket'], [
'scene_str' => $scene_str,
'create_time' => time(),
'expire_time' => time() + 300
], 300);
Log::info('二维码生成成功', [
'ticket' => $result['ticket'],
'scene_str' => $scene_str,
'expire_time' => time() + 300
]);
return json([
'code' => 0,
'msg' => '获取二维码成功',
'data' => [
'ticket' => $result['ticket'],
'expire_seconds' => $result['expire_seconds'],
'url' => $qrcodeUrl,
'scene_str' => $scene_str
]
]);
} catch (\Exception $e) {
Log::error('获取微信登录二维码失败:' . $e->getMessage());
Log::error('错误堆栈:' . $e->getTraceAsString());
return json(['code' => 1, 'msg' => '获取二维码失败:' . $e->getMessage()]);
}
}
/**
* 重新生成二维码
* @return \think\response\Json
*/
public function reGenerateQrcode()
{
try {
$scene_str = Request::post('scene_str');
if (empty($scene_str)) {
return json(['code' => 1, 'msg' => '参数错误']);
}
// 清除旧的缓存
Cache::delete('wx_login_scene_' . $scene_str);
// 获取access_token
$access_token = $this->getGZHAccessToken();
// 构建请求URL
$url = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token={$access_token}";
// 生成新的场景值
$new_scene_str = md5(uniqid() . time());
// 准备请求数据
$postData = json_encode([
'expire_seconds' => 300,
'action_name' => 'QR_STR_SCENE',
'action_info' => [
'scene' => [
'scene_str' => $new_scene_str
]
]
]);
// 发送请求获取ticket
$client = new Client(['verify' => false]);
$response = $client->post($url, [
'body' => $postData,
'headers' => [
'Content-Type' => 'application/json'
]
]);
$result = json_decode($response->getBody(), true);
if (isset($result['errcode']) && $result['errcode'] != 0) {
return json(['code' => 1, 'msg' => '获取二维码失败:' . $result['errmsg']]);
}
// 使用ticket获取二维码图片URL
$ticket = urlencode($result['ticket']);
$qrcodeUrl = "https://mp.weixin.qq.com/cgi-bin/showqrcode?ticket={$ticket}";
// 将新的场景值保存到缓存中
Cache::set('wx_login_scene_' . $new_scene_str, [
'scene_str' => $new_scene_str,
'create_time' => time(),
'expire_time' => time() + 300
], 300);
return json([
'code' => 0,
'msg' => '获取二维码成功',
'data' => [
'ticket' => $result['ticket'],
'expire_seconds' => $result['expire_seconds'],
'url' => $qrcodeUrl,
'scene_str' => $new_scene_str
]
]);
} catch (\Exception $e) {
Log::error('重新生成二维码失败:' . $e->getMessage());
return json(['code' => 1, 'msg' => '重新生成二维码失败:' . $e->getMessage()]);
}
}
/**
* 检查微信扫码登录状态
* @return \think\response\Json
*/
public function checkLoginStatus()
{
try {
$scene_str = Request::post('scene_str');
$ticket = Request::post('ticket');
Log::info('检查登录状态', [
'scene_str' => $scene_str,
'ticket' => $ticket,
'time' => date('Y-m-d H:i:s')
]);
if (empty($scene_str) || empty($ticket)) {
Log::warning('检查登录状态参数错误', [
'scene_str' => $scene_str,
'ticket' => $ticket
]);
return json(['code' => 0, 'msg' => '参数错误']);
}
// 检查票据文件
$file = 'public/storage/uploads/ticket/' . $scene_str . "_" . $ticket . ".json";
Log::info('检查票据文件:' . $file);
if (file_exists($file)) {
$loginData = json_decode(file_get_contents($file), true);
Log::info('票据文件内容:' . json_encode($loginData, JSON_UNESCAPED_UNICODE));
if ($loginData) {
// 检查是否包含用户信息
if (isset($loginData['user_info'])) {
Log::info('用户已扫码登录', [
'user_info' => $loginData['user_info']
]);
// 设置登录session
session('user_id', $loginData['user_info']['uid']);
session('user_info', $loginData['user_info']);
session('openid', $loginData['user_info']['openid']);
// 删除临时文件
unlink($file);
return json(['code' => 1, 'msg' => '登录成功']);
} else {
Log::info('等待获取用户信息');
return json(['code' => 0, 'msg' => '正在获取用户信息,请稍候...']);
}
}
}
Log::info('等待扫码');
return json(['code' => 0, 'msg' => '等待扫码']);
} catch (\Exception $e) {
Log::error('检查登录状态失败:' . $e->getMessage());
Log::error('错误堆栈:' . $e->getTraceAsString());
return json(['code' => 0, 'msg' => '系统错误']);
}
}
/**
* 根据openid获取用户信息
* @param string $openid
* @return array|null
*/
private function getUserInfoByOpenid($openid)
{
try {
// 这里需要根据你的用户表结构来实现
// 示例从用户表中查询openid对应的用户信息
$user = User::where('openid', $openid)->find();
if ($user) {
return $user->toArray();
}
return null;
} catch (\Exception $e) {
Log::error('获取用户信息失败:' . $e->getMessage());
return null;
}
}
/**
* 测试微信服务器访问
*/
public function testWechat()
{
try {
// 记录所有请求信息
$requestInfo = [
'time' => date('Y-m-d H:i:s'),
'method' => Request::method(),
'url' => Request::url(true),
'ip' => Request::ip(),
'params' => Request::param(),
'headers' => getallheaders(),
'server' => [
'REQUEST_METHOD' => $_SERVER['REQUEST_METHOD'] ?? '',
'REQUEST_URI' => $_SERVER['REQUEST_URI'] ?? '',
'HTTP_HOST' => $_SERVER['HTTP_HOST'] ?? '',
'REMOTE_ADDR' => $_SERVER['REMOTE_ADDR'] ?? '',
'QUERY_STRING' => $_SERVER['QUERY_STRING'] ?? '',
'HTTP_USER_AGENT' => $_SERVER['HTTP_USER_AGENT'] ?? '',
'HTTP_REFERER' => $_SERVER['HTTP_REFERER'] ?? '',
]
];
Log::info('微信测试接口访问:' . json_encode($requestInfo, JSON_UNESCAPED_UNICODE));
// 获取原始数据
$rawData = file_get_contents("php://input");
if (!empty($rawData)) {
$rawData = mb_convert_encoding($rawData, 'UTF-8', 'UTF-8,GBK,GB2312');
Log::info('微信测试接口原始数据:' . $rawData);
}
// 如果是GET请求进行签名验证
if (Request::method() == 'GET') {
$signature = Request::get('signature');
$timestamp = Request::get('timestamp');
$nonce = Request::get('nonce');
$echostr = Request::get('echostr');
Log::info('微信测试接口验证参数:', [
'signature' => $signature,
'timestamp' => $timestamp,
'nonce' => $nonce,
'echostr' => $echostr
]);
if (!empty($signature) && !empty($timestamp) && !empty($nonce)) {
$token = AdminConfig::where('config_name', 'wechat_token')->value('config_value');
if (empty($token)) {
Log::error('微信token未配置');
return 'token not configured';
}
$tmpArr = array($token, $timestamp, $nonce);
sort($tmpArr, SORT_STRING);
$tmpStr = implode($tmpArr);
$tmpStr = sha1($tmpStr);
if ($tmpStr == $signature) {
Log::info('微信测试接口验证成功');
return $echostr;
} else {
Log::error('微信测试接口验证失败');
return 'signature verification failed';
}
}
}
return 'success';
} catch (\Exception $e) {
Log::error('微信测试接口错误:' . $e->getMessage());
Log::error('错误堆栈:' . $e->getTraceAsString());
return 'error';
}
}
}