|
|
|
|
@ -0,0 +1,218 @@
|
|
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* 微信小程序码 / access_token 一键诊断
|
|
|
|
|
*
|
|
|
|
|
* 用法(任选其一):
|
|
|
|
|
* 1) 命令行:php wechat_qrcode_diag.php
|
|
|
|
|
* 2) 命令行指定课程:php wechat_qrcode_diag.php 188
|
|
|
|
|
* 3) 浏览器(用完务必删除本文件):
|
|
|
|
|
* https://你的域名/wechat_qrcode_diag.php?key=stbc-diag-2026&course_id=188
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
$cli = PHP_SAPI === 'cli';
|
|
|
|
|
$secret = 'stbc-diag-2026';
|
|
|
|
|
$courseId = $cli
|
|
|
|
|
? (int) (isset($argv[1]) ? $argv[1] : 188)
|
|
|
|
|
: (int) (isset($_GET['course_id']) ? $_GET['course_id'] : 188);
|
|
|
|
|
|
|
|
|
|
if (!$cli) {
|
|
|
|
|
header('Content-Type: text/plain; charset=utf-8');
|
|
|
|
|
if (($_GET['key'] ?? '') !== $secret) {
|
|
|
|
|
http_response_code(403);
|
|
|
|
|
echo "forbidden\n";
|
|
|
|
|
exit(1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$lines = [];
|
|
|
|
|
$put = static function (string $text) use (&$lines): void {
|
|
|
|
|
$lines[] = $text;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
$put('=== WeChat QRCode Diagnose ===');
|
|
|
|
|
$put('time: ' . date('Y-m-d H:i:s'));
|
|
|
|
|
$put('php: ' . PHP_VERSION);
|
|
|
|
|
$put('cwd: ' . getcwd());
|
|
|
|
|
$put('course_id: ' . $courseId);
|
|
|
|
|
$put('');
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
require __DIR__ . '/vendor/autoload.php';
|
|
|
|
|
$app = require __DIR__ . '/bootstrap/app.php';
|
|
|
|
|
$kernel = $app->make(Illuminate\Contracts\Console\Kernel::class);
|
|
|
|
|
$kernel->bootstrap();
|
|
|
|
|
$put('[bootstrap] ok');
|
|
|
|
|
} catch (Throwable $e) {
|
|
|
|
|
$put('[bootstrap] FAIL: ' . $e->getMessage());
|
|
|
|
|
output_and_exit($lines, $cli, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$put('');
|
|
|
|
|
$put('--- config ---');
|
|
|
|
|
$appId = (string) config('app.applet_appid');
|
|
|
|
|
$secretCfg = (string) config('app.applet_secret');
|
|
|
|
|
$put('APPLET_APPID=[' . $appId . ']');
|
|
|
|
|
$put('APPLET_SECRET_set=' . ($secretCfg !== '' ? 'yes(len=' . strlen($secretCfg) . ')' : 'EMPTY'));
|
|
|
|
|
$put('CACHE_DRIVER=[' . config('cache.default') . ']');
|
|
|
|
|
$put('APP_URL=[' . config('app.url') . ']');
|
|
|
|
|
$put('LOG_CHANNEL=[' . config('logging.default') . ']');
|
|
|
|
|
$put('LOG_LEVEL=[' . env('LOG_LEVEL', 'debug') . ']');
|
|
|
|
|
$put('public.root=[' . config('filesystems.disks.public.root') . ']');
|
|
|
|
|
$put('public.url=[' . config('filesystems.disks.public.url') . ']');
|
|
|
|
|
|
|
|
|
|
$put('');
|
|
|
|
|
$put('--- cache dir ---');
|
|
|
|
|
$cacheDir = storage_path('framework/cache/data');
|
|
|
|
|
$put('cache_dir=' . $cacheDir);
|
|
|
|
|
$put('exists=' . (is_dir($cacheDir) ? 'yes' : 'no'));
|
|
|
|
|
$put('writable=' . (is_writable($cacheDir) ? 'yes' : 'no'));
|
|
|
|
|
if (is_dir($cacheDir)) {
|
|
|
|
|
$put('perms=' . substr(sprintf('%o', fileperms($cacheDir)), -4));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$put('');
|
|
|
|
|
$put('--- laravel cache ---');
|
|
|
|
|
try {
|
|
|
|
|
Illuminate\Support\Facades\Cache::put('wechat_qrcode_diag', 'ok', 60);
|
|
|
|
|
$val = Illuminate\Support\Facades\Cache::get('wechat_qrcode_diag');
|
|
|
|
|
$has = Illuminate\Support\Facades\Cache::has('wechat_qrcode_diag');
|
|
|
|
|
$put('put_get=' . ($val === 'ok' ? 'ok' : 'FAIL'));
|
|
|
|
|
$put('has=' . ($has ? 'yes' : 'no'));
|
|
|
|
|
Illuminate\Support\Facades\Cache::forget('wechat_qrcode_diag');
|
|
|
|
|
} catch (Throwable $e) {
|
|
|
|
|
$put('cache_exception: ' . $e->getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$put('');
|
|
|
|
|
$put('--- storage public ---');
|
|
|
|
|
$publicRoot = (string) config('filesystems.disks.public.root');
|
|
|
|
|
$qrDir = rtrim($publicRoot, '/') . '/course_qrcode';
|
|
|
|
|
$qrPath = $qrDir . '/' . $courseId . '.png';
|
|
|
|
|
$put('qr_dir=' . $qrDir);
|
|
|
|
|
$put('qr_dir_exists=' . (is_dir($qrDir) ? 'yes' : 'no'));
|
|
|
|
|
$put('qr_dir_writable=' . ((is_dir($qrDir) ? is_writable($qrDir) : is_writable(dirname($qrDir))) ? 'yes' : 'no'));
|
|
|
|
|
$put('qr_file_exists=' . (is_file($qrPath) ? 'yes' : 'no'));
|
|
|
|
|
if (is_file($qrPath)) {
|
|
|
|
|
$put('qr_file_size=' . filesize($qrPath));
|
|
|
|
|
}
|
|
|
|
|
try {
|
|
|
|
|
if (!is_dir($qrDir)) {
|
|
|
|
|
mkdir($qrDir, 0755, true);
|
|
|
|
|
}
|
|
|
|
|
$testFile = $qrDir . '/_diag_write_test';
|
|
|
|
|
file_put_contents($testFile, 'ok');
|
|
|
|
|
$ok = is_file($testFile) && trim((string) file_get_contents($testFile)) === 'ok';
|
|
|
|
|
@unlink($testFile);
|
|
|
|
|
$put('write_test=' . ($ok ? 'ok' : 'FAIL'));
|
|
|
|
|
} catch (Throwable $e) {
|
|
|
|
|
$put('write_test_exception: ' . $e->getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$put('');
|
|
|
|
|
$put('--- easywechat token ---');
|
|
|
|
|
if ($appId === '' || $secretCfg === '') {
|
|
|
|
|
$put('skip: missing appid/secret');
|
|
|
|
|
} else {
|
|
|
|
|
try {
|
|
|
|
|
$mini = EasyWeChat\Factory::miniProgram([
|
|
|
|
|
'app_id' => $appId,
|
|
|
|
|
'secret' => $secretCfg,
|
|
|
|
|
]);
|
|
|
|
|
$token = $mini->access_token->getToken(true);
|
|
|
|
|
$put('token_ok=yes');
|
|
|
|
|
$put('expires_in=' . ($token['expires_in'] ?? 'n/a'));
|
|
|
|
|
$put('token_prefix=' . substr((string) ($token['access_token'] ?? ''), 0, 10) . '...');
|
|
|
|
|
} catch (Throwable $e) {
|
|
|
|
|
$put('token_FAIL: ' . $e->getMessage());
|
|
|
|
|
output_summary($lines);
|
|
|
|
|
output_and_exit($lines, $cli, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$put('');
|
|
|
|
|
$put('--- easywechat app_code ---');
|
|
|
|
|
try {
|
|
|
|
|
$tmp = $mini->app_code->get('packages/course/detail?id=' . $courseId, [
|
|
|
|
|
'env_version' => 'release',
|
|
|
|
|
]);
|
|
|
|
|
$body = $tmp instanceof EasyWeChat\Kernel\Http\StreamResponse
|
|
|
|
|
? $tmp->getBodyContents()
|
|
|
|
|
: (string) $tmp;
|
|
|
|
|
$put('bytes=' . strlen($body));
|
|
|
|
|
$isJson = strpos(ltrim($body), '{') === 0;
|
|
|
|
|
$put('is_json=' . ($isJson ? 'yes' : 'no'));
|
|
|
|
|
$put('looks_like_image=' . (strlen($body) > 100 && !$isJson ? 'yes' : 'no'));
|
|
|
|
|
if ($isJson) {
|
|
|
|
|
$put('wechat_body=' . substr($body, 0, 500));
|
|
|
|
|
} else {
|
|
|
|
|
// 尝试落盘,验证完整链路
|
|
|
|
|
if (!is_dir($qrDir)) {
|
|
|
|
|
mkdir($qrDir, 0755, true);
|
|
|
|
|
}
|
|
|
|
|
file_put_contents($qrPath, $body);
|
|
|
|
|
$put('saved_to=' . $qrPath);
|
|
|
|
|
$put('saved_ok=' . (is_file($qrPath) ? 'yes' : 'no'));
|
|
|
|
|
}
|
|
|
|
|
} catch (Throwable $e) {
|
|
|
|
|
$put('app_code_FAIL: ' . $e->getMessage());
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$put('');
|
|
|
|
|
$put('--- course model generate ---');
|
|
|
|
|
try {
|
|
|
|
|
if (class_exists(App\Models\Course::class)) {
|
|
|
|
|
$course = App\Models\Course::find($courseId);
|
|
|
|
|
$put('course_exists=' . ($course ? 'yes' : 'no'));
|
|
|
|
|
if ($course) {
|
|
|
|
|
$put('course_name=' . ($course->name ?? ''));
|
|
|
|
|
$url = (new App\Models\Course())->getCourseQrcode($courseId, true);
|
|
|
|
|
$put('getCourseQrcode=[' . $url . ']');
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$put('Course model not found');
|
|
|
|
|
}
|
|
|
|
|
} catch (Throwable $e) {
|
|
|
|
|
$put('course_generate_FAIL: ' . $e->getMessage());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$put('');
|
|
|
|
|
output_summary($lines);
|
|
|
|
|
$put('DONE. 用完请删除本文件:wechat_qrcode_diag.php');
|
|
|
|
|
output_and_exit($lines, $cli, 0);
|
|
|
|
|
|
|
|
|
|
function output_summary(array &$lines)
|
|
|
|
|
{
|
|
|
|
|
$text = implode("\n", $lines);
|
|
|
|
|
$lines[] = '--- summary ---';
|
|
|
|
|
if (strpos($text, 'SECRET_set=EMPTY') !== false || strpos($text, 'APPLET_APPID=[]') !== false) {
|
|
|
|
|
$lines[] = '结论倾向: 微信配置未生效(APPLET_APPID/SECRET)';
|
|
|
|
|
} elseif (strpos($text, 'cache_exception') !== false || preg_match('/has=no/', $text)) {
|
|
|
|
|
$lines[] = '结论倾向: 缓存不可用(易触发 Failed to cache access token)';
|
|
|
|
|
} elseif (strpos($text, 'token_FAIL') !== false) {
|
|
|
|
|
$lines[] = '结论倾向: 获取 access_token 失败(配置/网络/IP白名单)';
|
|
|
|
|
} elseif (strpos($text, 'is_json=yes') !== false) {
|
|
|
|
|
$lines[] = '结论倾向: 微信拒绝生成小程序码(看 wechat_body)';
|
|
|
|
|
} elseif (strpos($text, 'write_test=FAIL') !== false || strpos($text, 'write_test_exception') !== false) {
|
|
|
|
|
$lines[] = '结论倾向: 二维码目录不可写';
|
|
|
|
|
} elseif (strpos($text, 'getCourseQrcode=[http') !== false || strpos($text, 'saved_ok=yes') !== false) {
|
|
|
|
|
$lines[] = '结论倾向: 生成链路正常,可再试管理后台生成按钮';
|
|
|
|
|
} else {
|
|
|
|
|
$lines[] = '结论倾向: 请把完整输出发开发排查';
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function output_and_exit(array $lines, bool $cli, int $code): void
|
|
|
|
|
{
|
|
|
|
|
$out = implode("\n", $lines) . "\n";
|
|
|
|
|
echo $out;
|
|
|
|
|
|
|
|
|
|
// 同时落盘,方便宝塔直接下载结果
|
|
|
|
|
$resultFile = __DIR__ . '/wechat_qrcode_diag_result.txt';
|
|
|
|
|
@file_put_contents($resultFile, $out);
|
|
|
|
|
if (!$cli) {
|
|
|
|
|
echo "\n结果已写入: wechat_qrcode_diag_result.txt\n";
|
|
|
|
|
} else {
|
|
|
|
|
fwrite(STDERR, "结果已写入: {$resultFile}\n");
|
|
|
|
|
}
|
|
|
|
|
exit($code);
|
|
|
|
|
}
|