|
|
|
|
|
<?php
|
|
|
|
|
|
/**
|
|
|
|
|
|
* 微信小程序码 / access_token 一键诊断(可长期保留)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 默认仅允许命令行,避免被公网访问:
|
|
|
|
|
|
* php wechat_qrcode_diag.php
|
|
|
|
|
|
* php wechat_qrcode_diag.php 188
|
|
|
|
|
|
*
|
|
|
|
|
|
* 结果文件:
|
|
|
|
|
|
* wechat_qrcode_diag_result.txt
|
|
|
|
|
|
*
|
|
|
|
|
|
* 如确需浏览器访问,把下面 $allowWeb 改成 true,并自行改 $webSecret。
|
|
|
|
|
|
* 用完建议改回 false。
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
$cli = PHP_SAPI === 'cli';
|
|
|
|
|
|
$allowWeb = false; // 长期保留时请保持 false
|
|
|
|
|
|
$webSecret = 'CHANGE_ME_TO_A_LONG_RANDOM_SECRET';
|
|
|
|
|
|
$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 (!$allowWeb) {
|
|
|
|
|
|
http_response_code(403);
|
|
|
|
|
|
echo "web access disabled, use: php wechat_qrcode_diag.php 188\n";
|
|
|
|
|
|
exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
if ($webSecret === '' || $webSecret === 'CHANGE_ME_TO_A_LONG_RANDOM_SECRET' || ($_GET['key'] ?? '') !== $webSecret) {
|
|
|
|
|
|
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(function_exists('miniProgramConfig') ? miniProgramConfig([
|
|
|
|
|
|
'app_id' => $appId,
|
|
|
|
|
|
'secret' => $secretCfg,
|
|
|
|
|
|
]) : [
|
|
|
|
|
|
'app_id' => $appId,
|
|
|
|
|
|
'secret' => $secretCfg,
|
|
|
|
|
|
'log' => [
|
|
|
|
|
|
'default' => 'prod',
|
|
|
|
|
|
'channels' => [
|
|
|
|
|
|
'prod' => [
|
|
|
|
|
|
'driver' => 'single',
|
|
|
|
|
|
'path' => storage_path('logs/easywechat.log'),
|
|
|
|
|
|
'level' => 'error',
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
],
|
|
|
|
|
|
]);
|
|
|
|
|
|
$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('--- deploy check ---');
|
|
|
|
|
|
try {
|
|
|
|
|
|
$ref = new ReflectionMethod(App\Models\Course::class, 'getCourseQrcode');
|
|
|
|
|
|
$put('getCourseQrcode_params=' . $ref->getNumberOfParameters());
|
|
|
|
|
|
$put('has_resolveMiniProgramQrcode=' . (method_exists(App\Models\Course::class, 'resolveMiniProgramQrcode') ? 'yes' : 'no'));
|
|
|
|
|
|
$appends = (new ReflectionClass(App\Models\Course::class))->getDefaultProperties()['appends'] ?? [];
|
|
|
|
|
|
$put('qrcode_in_appends=' . (in_array('qrcode', $appends, true) ? 'yes' : 'no'));
|
|
|
|
|
|
$ctrl = file_get_contents(__DIR__ . '/app/Http/Controllers/Admin/CourseController.php');
|
|
|
|
|
|
$put('controller_has_generate_flag=' . (strpos($ctrl, "generate") !== false && strpos($ctrl, 'getCourseQrcode') !== false ? 'yes' : 'no'));
|
|
|
|
|
|
$put('controller_has_try_catch_qrcode=' . (strpos($ctrl, 'courses/qrcode failed') !== false ? 'yes' : 'no'));
|
|
|
|
|
|
} catch (Throwable $e) {
|
|
|
|
|
|
$put('deploy_check_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);
|
|
|
|
|
|
}
|