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); }