Files
anxinyan/server-api/tools/release_audit.php

279 lines
9.8 KiB
PHP
Raw Permalink 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
declare(strict_types=1);
require dirname(__DIR__) . '/vendor/autoload.php';
$dotenv = Dotenv\Dotenv::createImmutable(dirname(__DIR__));
$dotenv->safeLoad();
$projectRoot = dirname(__DIR__, 2);
$issues = [];
function add_issue(array &$issues, string $level, string $title, string $detail): void
{
$issues[] = compact('level', 'title', 'detail');
}
function check(bool $condition, array &$issues, string $failLevel, string $title, string $detail): void
{
if (!$condition) {
add_issue($issues, $failLevel, $title, $detail);
}
}
function parseJsonFile(string $path): ?array
{
$content = @file_get_contents($path);
if ($content === false) {
return null;
}
$content = preg_replace('/^\xEF\xBB\xBF/', '', $content);
$content = preg_replace('!/\*.*?\*/!s', '', $content);
$content = preg_replace('/^\s*\/\/.*$/m', '', $content);
$decoded = json_decode((string)$content, true);
return is_array($decoded) ? $decoded : null;
}
function isPlaceholderApiBase(string $apiBase): bool
{
if ($apiBase === '') {
return true;
}
$normalized = strtolower($apiBase);
$localHosts = ['127.0.0.' . '1', 'local' . 'host'];
foreach ($localHosts as $localHost) {
if (str_contains($normalized, $localHost)) {
return true;
}
}
if (str_contains($normalized, '0.0.0.0')) {
return true;
}
return str_contains($normalized, 'example.com');
}
function normalizeH5PageBaseUrl(string $value): string
{
$baseUrl = trim($value);
if ($baseUrl === '') {
return '';
}
$hashPos = strpos($baseUrl, '#');
if ($hashPos !== false) {
$baseUrl = substr($baseUrl, 0, $hashPos);
}
if (!preg_match('/^https?:\/\//i', $baseUrl)) {
$baseUrl = 'https://' . ltrim($baseUrl, '/');
}
return rtrim($baseUrl, '/');
}
function buildH5OAuthRedirectUrl(string $pageBaseUrl): string
{
$baseUrl = normalizeH5PageBaseUrl($pageBaseUrl);
return $baseUrl === '' ? '' : $baseUrl . '/#/pages/auth/login';
}
function buildShouqianbaNotifyUrl(array $env): string
{
$baseUrl = trim((string)($env['PUBLIC_FILE_BASE_URL'] ?? ''));
if ($baseUrl === '') {
return '';
}
if (!preg_match('/^https?:\/\//i', $baseUrl)) {
$baseUrl = 'https://' . ltrim($baseUrl, '/');
}
return rtrim($baseUrl, '/') . '/api/open/shouqianba/payment/notify';
}
function checkClientProductionApiBase(array &$issues, string $label, string $envPath): void
{
$env = @parse_ini_file($envPath);
if (!is_array($env)) {
add_issue($issues, 'FAIL', "{$label} 缺少生产环境变量", "无法解析 {$envPath}");
return;
}
$apiBase = (string)($env['VITE_API_BASE_URL'] ?? '');
if (isPlaceholderApiBase($apiBase)) {
add_issue($issues, 'FAIL', "{$label} 生产 API 未配置正式域名", "{$envPath} 的 VITE_API_BASE_URL 仍为本地或占位地址。");
return;
}
if (rtrim($apiBase, '/') !== 'https://api.anxinjianyan.com') {
add_issue($issues, 'FAIL', "{$label} 生产 API 域名不符合发布规则", "{$envPath} 的 VITE_API_BASE_URL 必须为 https://api.anxinjianyan.com。");
}
}
$env = $_ENV;
check(($env['APP_ENV'] ?? '') === 'production', $issues, 'FAIL', 'APP_ENV 非 production', '当前 .env 中 APP_ENV 不是 production。');
check(in_array(strtolower((string)($env['APP_DEBUG'] ?? '')), ['false', '0'], true), $issues, 'FAIL', 'APP_DEBUG 未关闭', '当前 .env 中 APP_DEBUG 仍然开启。');
$dsn = sprintf(
'mysql:host=%s;port=%s;dbname=%s;charset=%s',
$env['DB_HOST'] ?? '127.0.0.1',
$env['DB_PORT'] ?? '3306',
$env['DB_DATABASE'] ?? '',
$env['DB_CHARSET'] ?? 'utf8mb4'
);
$pdo = new PDO(
$dsn,
$env['DB_USERNAME'] ?? '',
$env['DB_PASSWORD'] ?? '',
[
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
]
);
$configRows = $pdo->query("SELECT config_group, config_key, config_value FROM system_configs")->fetchAll();
$configMap = [];
foreach ($configRows as $row) {
$configMap[$row['config_group'] . '.' . $row['config_key']] = (string)($row['config_value'] ?? '');
}
$configMap['h5.oauth_redirect_url'] = buildH5OAuthRedirectUrl((string)($configMap['h5.page_base_url'] ?? ''));
$configMap['payment.notify_url'] = buildShouqianbaNotifyUrl($env);
$requiredConfigKeys = [
'mini_program.app_id',
'mini_program.app_secret',
'mini_program.original_id',
'h5.app_id',
'h5.app_secret',
'h5.oauth_redirect_url',
'h5.page_base_url',
'sms.access_key_id',
'sms.access_key_secret',
'sms.sign_name',
'sms.login_template_code',
'payment.enabled',
'payment.api_domain',
'payment.appid',
'payment.brand_code',
'payment.store_sn',
'payment.merchant_private_key',
'payment.shouqianba_public_key',
'payment.notify_url',
'payment.mini_program_plugin_version',
];
foreach ($requiredConfigKeys as $key) {
check(($configMap[$key] ?? '') !== '', $issues, 'FAIL', "系统配置缺失: {$key}", "后台系统配置中 {$key} 仍为空。");
}
check(($configMap['payment.enabled'] ?? '') === 'enabled', $issues, 'FAIL', '收钱吧支付未启用', '后台系统配置 payment.enabled 必须为 enabled。');
foreach (['payment.api_domain' => '收钱吧 API 域名', 'payment.notify_url' => '收钱吧通知地址'] as $key => $label) {
if (($configMap[$key] ?? '') !== '') {
check(
preg_match('/^https?:\/\//i', (string)$configMap[$key]) === 1,
$issues,
'FAIL',
"{$label} 格式不正确",
"后台系统配置 {$key} 需以 http:// 或 https:// 开头。"
);
}
}
if (($configMap['h5.page_base_url'] ?? '') !== '' && isPlaceholderApiBase((string)$configMap['h5.page_base_url'])) {
add_issue($issues, 'FAIL', 'H5 页面根地址未配置正式域名', '后台系统配置 h5.page_base_url 仍为本地或占位地址,扫码公开链接将无法用于正式环境。');
}
$demoValues = [
'mini_program.app_id' => 'wx1234567890test',
'h5.app_id' => 'h5_app_demo',
'payment.api_domain' => 'https://example.com',
];
foreach ($demoValues as $key => $value) {
check(($configMap[$key] ?? '') !== $value, $issues, 'FAIL', "系统配置仍是演示值: {$key}", "后台系统配置 {$key} 仍为演示值 {$value}");
}
$admins = $pdo->query("SELECT id, mobile, password, status FROM admin_users ORDER BY id ASC")->fetchAll();
$hasDefaultAdmin = false;
$hasViewerAdmin = false;
foreach ($admins as $admin) {
if (($admin['mobile'] ?? '') === '13800138000') {
$hasDefaultAdmin = true;
if (password_verify('Admin@123456', (string)$admin['password'])) {
add_issue($issues, 'FAIL', '默认超级管理员密码未修改', '管理员 13800138000 仍可用默认密码 Admin@123456 登录。');
}
}
if (($admin['mobile'] ?? '') === '13800138001' && ($admin['status'] ?? '') === 'enabled') {
$hasViewerAdmin = true;
}
}
if ($hasViewerAdmin) {
add_issue($issues, 'WARN', '测试管理员仍启用', '测试管理员 13800138001 / Test@123456 仍处于启用状态。');
}
if (!$hasDefaultAdmin) {
add_issue($issues, 'WARN', '未检测到默认超级管理员', '未找到手机号 13800138000 的默认超级管理员账号。');
}
$user = $pdo->query("SELECT nickname FROM users WHERE id = 1")->fetch();
if ($user && str_contains((string)$user['nickname'], '测试')) {
add_issue($issues, 'WARN', '测试昵称未清理', '用户昵称仍包含“测试”字样。');
}
$manifestPath = $projectRoot . '/user-app/src/manifest.json';
$manifest = parseJsonFile($manifestPath);
if (!is_array($manifest)) {
add_issue($issues, 'FAIL', 'manifest.json 解析失败', '无法解析 user-app/src/manifest.json。');
} else {
check(($manifest['mp-weixin']['appid'] ?? '') !== '', $issues, 'FAIL', '小程序 manifest appid 为空', 'manifest.json 中 mp-weixin.appid 仍为空。');
if (($configMap['mini_program.app_id'] ?? '') !== '') {
check(
($manifest['mp-weixin']['appid'] ?? '') === $configMap['mini_program.app_id'],
$issues,
'FAIL',
'小程序 manifest appid 未同步后台配置',
'manifest.json 中 mp-weixin.appid 与后台系统配置 mini_program.app_id 不一致,请先执行配置同步。'
);
}
$plugin = $manifest['mp-weixin']['plugins']['lite-pos-plugin'] ?? null;
if (!is_array($plugin)) {
add_issue($issues, 'FAIL', '小程序未声明收钱吧插件', 'manifest.json 中缺少 mp-weixin.plugins.lite-pos-plugin请先执行配置同步。');
} else {
check(
($plugin['provider'] ?? '') === 'wx7903bb295ac26ac7',
$issues,
'FAIL',
'小程序收钱吧插件 provider 不正确',
'manifest.json 中 lite-pos-plugin.provider 必须为 wx7903bb295ac26ac7。'
);
if (($configMap['payment.mini_program_plugin_version'] ?? '') !== '') {
check(
($plugin['version'] ?? '') === $configMap['payment.mini_program_plugin_version'],
$issues,
'FAIL',
'小程序收钱吧插件版本未同步后台配置',
'manifest.json 中 lite-pos-plugin.version 与后台 payment.mini_program_plugin_version 不一致,请先执行配置同步。'
);
}
}
}
checkClientProductionApiBase($issues, 'admin-web', $projectRoot . '/admin-web/.env.production');
checkClientProductionApiBase($issues, 'user-app', $projectRoot . '/user-app/.env.production');
checkClientProductionApiBase($issues, 'work-app', $projectRoot . '/work-app/.env.production');
if (!$issues) {
echo "RELEASE_AUDIT_OK\n";
exit(0);
}
echo "RELEASE_AUDIT_ISSUES\n";
foreach ($issues as $item) {
echo "[{$item['level']}] {$item['title']} - {$item['detail']}\n";
}