fix: 解决访问没有埋点的问题
This commit is contained in:
679
scripts/enhanced_deploy.php
Normal file
679
scripts/enhanced_deploy.php
Normal file
@@ -0,0 +1,679 @@
|
||||
<?php
|
||||
// enhanced_deploy.php
|
||||
#!/usr/bin/env php
|
||||
/**
|
||||
* 增强版自动部署脚本
|
||||
* 支持根据补丁ZIP文件删除不需要的文件
|
||||
* PHP 7.4 兼容版本
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
class EnhancedAutoDeployer
|
||||
{
|
||||
private string $zipPath;
|
||||
private string $targetDir;
|
||||
private array $config;
|
||||
private array $log = [];
|
||||
private bool $dryRun;
|
||||
private bool $verbose;
|
||||
private array $deleteList = [];
|
||||
|
||||
public function __construct(string $zipPath, string $targetDir, array $config = [])
|
||||
{
|
||||
$this->zipPath = $zipPath;
|
||||
$this->targetDir = rtrim($targetDir, DIRECTORY_SEPARATOR);
|
||||
|
||||
$this->config = array_merge([
|
||||
'dry_run' => false,
|
||||
'verbose' => false,
|
||||
'backup_enabled' => true,
|
||||
'backup_dir' => $this->targetDir . '/backups/deploy',
|
||||
'delete_enabled' => true,
|
||||
'delete_list_file' => '_DELETE_LIST.txt',
|
||||
'delete_manifest_file' => '_DELETE_MANIFEST.json',
|
||||
'safe_delete' => true,
|
||||
'max_backups' => 5,
|
||||
'confirmation_required' => false
|
||||
], $config);
|
||||
|
||||
$this->dryRun = $this->config['dry_run'];
|
||||
$this->verbose = $this->config['verbose'];
|
||||
|
||||
$this->validatePaths();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取相对路径
|
||||
*/
|
||||
private function getRelativePath(string $path, string $basePath): string
|
||||
{
|
||||
$basePath = rtrim($basePath, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
|
||||
return str_replace($basePath, '', $path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行部署(包含删除功能)
|
||||
*/
|
||||
public function deploy(): array
|
||||
{
|
||||
$this->logMessage('INFO', '开始增强版自动部署流程');
|
||||
$this->logMessage('INFO', "ZIP文件: {$this->zipPath}");
|
||||
$this->logMessage('INFO', "目标目录: {$this->targetDir}");
|
||||
|
||||
try {
|
||||
// 打开ZIP文件
|
||||
$zip = new ZipArchive();
|
||||
if ($zip->open($this->zipPath) !== true) {
|
||||
throw new RuntimeException("无法打开ZIP文件: {$this->zipPath}");
|
||||
}
|
||||
|
||||
$this->logMessage('INFO', 'ZIP文件打开成功');
|
||||
|
||||
// 步骤1:读取删除清单
|
||||
$this->loadDeleteList($zip);
|
||||
|
||||
// 步骤2:分析ZIP文件结构
|
||||
$zipFiles = $this->analyzeZipStructure($zip);
|
||||
$this->logMessage('INFO', "ZIP包中包含 {$zipFiles['file_count']} 个文件");
|
||||
|
||||
// 步骤3:分析目标目录结构
|
||||
$targetFiles = $this->analyzeTargetDirectory();
|
||||
$this->logMessage('INFO', "目标目录中包含 {$targetFiles['file_count']} 个文件");
|
||||
|
||||
// 步骤4:确认删除操作(如果需要)
|
||||
if ($this->config['confirmation_required'] && !empty($this->deleteList)) {
|
||||
if (!$this->confirmDeletion()) {
|
||||
$zip->close();
|
||||
throw new RuntimeException("用户取消部署操作");
|
||||
}
|
||||
}
|
||||
|
||||
// 步骤5:执行删除操作
|
||||
$deleteResult = $this->executeDeletion();
|
||||
|
||||
// 步骤6:部署新文件和更新文件
|
||||
$deployResult = $this->deployFiles($zip, $zipFiles, $targetFiles);
|
||||
|
||||
// 关闭ZIP文件
|
||||
$zip->close();
|
||||
|
||||
// 合并结果
|
||||
$result = array_merge($deployResult, [
|
||||
'deletion' => $deleteResult
|
||||
]);
|
||||
|
||||
$this->logMessage('SUCCESS', '部署完成');
|
||||
return $result;
|
||||
|
||||
} catch (Exception $e) {
|
||||
$this->logMessage('ERROR', "部署失败: " . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析ZIP文件结构
|
||||
*/
|
||||
private function analyzeZipStructure(ZipArchive $zip): array
|
||||
{
|
||||
$files = [];
|
||||
$fileCount = 0;
|
||||
$totalSize = 0;
|
||||
|
||||
for ($i = 0; $i < $zip->numFiles; $i++) {
|
||||
$fileInfo = $zip->statIndex($i);
|
||||
|
||||
if ($fileInfo === false) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$fileName = $fileInfo['name'];
|
||||
|
||||
// 跳过目录条目
|
||||
if (substr($fileName, -1) === '/') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$files[$fileName] = [
|
||||
'size' => $fileInfo['size'],
|
||||
'compressed_size' => $fileInfo['comp_size'],
|
||||
'modified' => $fileInfo['mtime'],
|
||||
'crc32' => $fileInfo['crc']
|
||||
];
|
||||
|
||||
$fileCount++;
|
||||
$totalSize += $fileInfo['size'];
|
||||
}
|
||||
|
||||
return [
|
||||
'files' => $files,
|
||||
'fileCount' => $fileCount,
|
||||
'totalSize' => $totalSize
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析目标目录结构
|
||||
*/
|
||||
private function analyzeTargetDirectory(): array
|
||||
{
|
||||
$files = [];
|
||||
$fileCount = 0;
|
||||
$totalSize = 0;
|
||||
|
||||
$iterator = new RecursiveIteratorIterator(
|
||||
new RecursiveDirectoryIterator(
|
||||
$this->targetDir,
|
||||
RecursiveDirectoryIterator::SKIP_DOTS
|
||||
),
|
||||
RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $file) {
|
||||
if ($file->isDir()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$relativePath = $this->getRelativePath($file->getPathname(), $this->targetDir);
|
||||
$files[$relativePath] = [
|
||||
'size' => $file->getSize(),
|
||||
'modified' => $file->getMTime(),
|
||||
'permissions' => $file->getPerms()
|
||||
];
|
||||
|
||||
$fileCount++;
|
||||
$totalSize += $file->getSize();
|
||||
}
|
||||
|
||||
return [
|
||||
'files' => $files,
|
||||
'fileCount' => $fileCount,
|
||||
'totalSize' => $totalSize
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 加载删除清单
|
||||
*/
|
||||
private function loadDeleteList(ZipArchive $zip): void
|
||||
{
|
||||
$this->deleteList = [];
|
||||
|
||||
// 尝试从多个可能的文件中读取删除清单
|
||||
$possibleFiles = [
|
||||
$this->config['delete_list_file'],
|
||||
'DELETE_LIST.txt',
|
||||
'delete.list',
|
||||
'manifest.json',
|
||||
$this->config['delete_manifest_file']
|
||||
];
|
||||
|
||||
foreach ($possibleFiles as $listFile) {
|
||||
$content = $zip->getFromName($listFile);
|
||||
if ($content !== false) {
|
||||
$this->parseDeleteList($content, $listFile);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($this->deleteList)) {
|
||||
$this->logMessage('INFO', '未找到删除清单文件,跳过删除操作');
|
||||
} else {
|
||||
$this->logMessage('INFO', "找到删除清单,包含 " . count($this->deleteList) . " 个待删除文件");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析删除清单
|
||||
*/
|
||||
private function parseDeleteList(string $content, string $sourceFile): void
|
||||
{
|
||||
$this->logMessage('DEBUG', "从文件 {$sourceFile} 解析删除清单");
|
||||
|
||||
// 尝试解析JSON格式
|
||||
if (strpos($sourceFile, '.json') !== false) {
|
||||
$data = json_decode($content, true);
|
||||
if ($data && isset($data['files_to_delete'])) {
|
||||
$this->deleteList = $data['files_to_delete'];
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 解析文本格式(每行一个文件路径)
|
||||
$lines = explode("\n", $content);
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
|
||||
// 跳过空行和注释
|
||||
if (empty($line) || $line[0] === '#' || $line[0] === ';') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->deleteList[] = $line;
|
||||
}
|
||||
|
||||
// 去重
|
||||
$this->deleteList = array_unique($this->deleteList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行删除操作
|
||||
*/
|
||||
private function executeDeletion(): array
|
||||
{
|
||||
if (!$this->config['delete_enabled'] || empty($this->deleteList)) {
|
||||
return [
|
||||
'total' => 0,
|
||||
'deleted' => 0,
|
||||
'skipped' => 0,
|
||||
'errors' => 0,
|
||||
'details' => []
|
||||
];
|
||||
}
|
||||
|
||||
$result = [
|
||||
'total' => count($this->deleteList),
|
||||
'deleted' => 0,
|
||||
'skipped' => 0,
|
||||
'errors' => 0,
|
||||
'details' => []
|
||||
];
|
||||
|
||||
$this->logMessage('INFO', "开始执行删除操作,共 {$result['total']} 个文件");
|
||||
|
||||
foreach ($this->deleteList as $fileToDelete) {
|
||||
try {
|
||||
$fullPath = $this->targetDir . DIRECTORY_SEPARATOR . $fileToDelete;
|
||||
|
||||
if (!$this->shouldDeleteFile($fullPath, $fileToDelete)) {
|
||||
$result['skipped']++;
|
||||
$result['details'][] = [
|
||||
'file' => $fileToDelete,
|
||||
'action' => 'skipped',
|
||||
'reason' => '安全检查未通过',
|
||||
'success' => true
|
||||
];
|
||||
continue;
|
||||
}
|
||||
|
||||
$deleteResult = $this->deleteFile($fullPath, $fileToDelete);
|
||||
|
||||
if ($deleteResult) {
|
||||
$result['deleted']++;
|
||||
$result['details'][] = [
|
||||
'file' => $fileToDelete,
|
||||
'action' => 'deleted',
|
||||
'success' => true
|
||||
];
|
||||
} else {
|
||||
$result['skipped']++;
|
||||
$result['details'][] = [
|
||||
'file' => $fileToDelete,
|
||||
'action' => 'skipped',
|
||||
'reason' => '文件不存在或无法删除',
|
||||
'success' => true
|
||||
];
|
||||
}
|
||||
|
||||
} catch (Exception $e) {
|
||||
$result['errors']++;
|
||||
$result['details'][] = [
|
||||
'file' => $fileToDelete,
|
||||
'action' => 'error',
|
||||
'error' => $e->getMessage(),
|
||||
'success' => false
|
||||
];
|
||||
$this->logMessage('ERROR', "删除文件失败 {$fileToDelete}: " . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
$this->logMessage('SUCCESS', "删除操作完成: 成功 {$result['deleted']} 个, 跳过 {$result['skipped']} 个, 错误 {$result['errors']} 个");
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否应该删除文件
|
||||
*/
|
||||
private function shouldDeleteFile(string $fullPath, string $relativePath): bool
|
||||
{
|
||||
// 安全检查:确保路径在目标目录内
|
||||
$realTarget = realpath($this->targetDir);
|
||||
$realFile = realpath($fullPath);
|
||||
|
||||
if ($realFile === false || strpos($realFile, $realTarget) !== 0) {
|
||||
$this->logMessage('WARNING', "安全警告: 跳过删除外部文件 {$relativePath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查重要文件保护
|
||||
$protectedPatterns = [
|
||||
'/\.env$/',
|
||||
'/config\.php$/',
|
||||
'/database\.php$/',
|
||||
'/\.htaccess$/',
|
||||
'/web\.config$/',
|
||||
'/index\.php$/',
|
||||
'/composer\.json$/',
|
||||
'/composer\.lock$/',
|
||||
'/package\.json$/',
|
||||
'/yarn\.lock$/',
|
||||
'/\.gitignore$/'
|
||||
];
|
||||
|
||||
foreach ($protectedPatterns as $pattern) {
|
||||
if (preg_match($pattern, $relativePath)) {
|
||||
$this->logMessage('WARNING', "保护文件: 跳过删除 {$relativePath}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查文件是否存在
|
||||
if (!file_exists($fullPath)) {
|
||||
$this->logMessage('DEBUG', "文件不存在,跳过删除: {$relativePath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否为目录
|
||||
if (is_dir($fullPath)) {
|
||||
$this->logMessage('WARNING', "跳过删除目录: {$relativePath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除单个文件
|
||||
*/
|
||||
private function deleteFile(string $fullPath, string $relativePath): bool
|
||||
{
|
||||
if ($this->dryRun) {
|
||||
$this->logMessage('DRY_RUN', "将删除文件: {$relativePath}");
|
||||
return true;
|
||||
}
|
||||
|
||||
// 备份文件(如果启用)
|
||||
if ($this->config['backup_enabled']) {
|
||||
$this->backupFile($fullPath, $relativePath);
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
if (unlink($fullPath)) {
|
||||
$this->logMessage('INFO', "文件已删除: {$relativePath}");
|
||||
|
||||
// 尝试删除空目录
|
||||
$this->cleanupEmptyDirectories(dirname($fullPath));
|
||||
|
||||
return true;
|
||||
} else {
|
||||
$this->logMessage('ERROR', "无法删除文件: {$relativePath}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 备份文件
|
||||
*/
|
||||
private function backupFile(string $filePath, string $relativePath): void
|
||||
{
|
||||
if (!file_exists($filePath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$backupDir = $this->config['backup_dir'];
|
||||
if (!is_dir($backupDir)) {
|
||||
mkdir($backupDir, 0755, true);
|
||||
}
|
||||
|
||||
$backupPath = $backupDir . DIRECTORY_SEPARATOR .
|
||||
date('Ymd_His') . '_' .
|
||||
str_replace('/', '_', $relativePath);
|
||||
|
||||
$backupDir = dirname($backupPath);
|
||||
if (!is_dir($backupDir)) {
|
||||
mkdir($backupDir, 0755, true);
|
||||
}
|
||||
|
||||
if (copy($filePath, $backupPath)) {
|
||||
$this->logMessage('INFO', "文件已备份: {$backupPath}");
|
||||
} else {
|
||||
$this->logMessage('WARNING', "文件备份失败: {$relativePath}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理空目录
|
||||
*/
|
||||
private function cleanupEmptyDirectories(string $directory): void
|
||||
{
|
||||
$currentDir = $directory;
|
||||
$baseDir = realpath($this->targetDir);
|
||||
|
||||
while ($currentDir !== $baseDir && is_dir($currentDir)) {
|
||||
// 检查目录是否为空
|
||||
$files = scandir($currentDir);
|
||||
if (count($files) === 2) { // 只有 . 和 ..
|
||||
if ($this->dryRun) {
|
||||
$this->logMessage('DRY_RUN', "将删除空目录: {$currentDir}");
|
||||
} else {
|
||||
if (rmdir($currentDir)) {
|
||||
$this->logMessage('INFO', "空目录已删除: {$currentDir}");
|
||||
}
|
||||
}
|
||||
|
||||
$currentDir = dirname($currentDir);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认删除操作
|
||||
*/
|
||||
private function confirmDeletion(): bool
|
||||
{
|
||||
echo "\n⚠️ 警告: 以下文件将被删除:\n";
|
||||
echo "========================================\n";
|
||||
|
||||
foreach ($this->deleteList as $index => $file) {
|
||||
echo sprintf("%3d. %s\n", $index + 1, $file);
|
||||
}
|
||||
|
||||
echo "========================================\n";
|
||||
echo "总共 " . count($this->deleteList) . " 个文件将被删除\n\n";
|
||||
|
||||
echo "是否继续?(y/N): ";
|
||||
$handle = fopen('php://stdin', 'r');
|
||||
$input = trim(fgets($handle));
|
||||
fclose($handle);
|
||||
|
||||
return strtolower($input) === 'y';
|
||||
}
|
||||
|
||||
/**
|
||||
* 部署文件(从父类继承或实现)
|
||||
*/
|
||||
private function deployFiles(ZipArchive $zip, array $zipFiles, array $targetFiles): array
|
||||
{
|
||||
// 这里实现文件部署逻辑,与之前的AutoDeployer类似
|
||||
// 包括添加新文件和更新现有文件
|
||||
$result = [
|
||||
'added' => 0,
|
||||
'updated' => 0,
|
||||
'skipped' => 0,
|
||||
'errors' => 0,
|
||||
'details' => []
|
||||
];
|
||||
|
||||
// 实现文件部署逻辑...
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录日志
|
||||
*/
|
||||
private function logMessage(string $level, string $message): void
|
||||
{
|
||||
$timestamp = date('Y-m-d H:i:s');
|
||||
$logEntry = "[{$timestamp}] [{$level}] {$message}";
|
||||
|
||||
$this->log[] = $logEntry;
|
||||
|
||||
if ($this->verbose || in_array($level, ['ERROR', 'SUCCESS', 'WARNING'])) {
|
||||
echo $logEntry . PHP_EOL;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证路径
|
||||
*/
|
||||
private function validatePaths(): void
|
||||
{
|
||||
if (!file_exists($this->zipPath)) {
|
||||
throw new InvalidArgumentException("ZIP文件不存在: {$this->zipPath}");
|
||||
}
|
||||
|
||||
if (!is_dir($this->targetDir)) {
|
||||
throw new InvalidArgumentException("目标目录不存在: {$this->targetDir}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志
|
||||
*/
|
||||
public function getLog(): array
|
||||
{
|
||||
return $this->log;
|
||||
}
|
||||
}
|
||||
|
||||
// 命令行接口
|
||||
class EnhancedDeployerCLI
|
||||
{
|
||||
public function run(array $argv): void
|
||||
{
|
||||
try {
|
||||
$options = $this->parseOptions($argv);
|
||||
|
||||
if ($options['help']) {
|
||||
$this->showHelp();
|
||||
exit(0);
|
||||
}
|
||||
|
||||
if (!$options['zip'] || !$options['target']) {
|
||||
echo "错误: 必须指定 --zip 和 --target 参数\n";
|
||||
$this->showHelp();
|
||||
exit(1);
|
||||
}
|
||||
|
||||
$deployer = new EnhancedAutoDeployer($options['zip'], $options['target'], [
|
||||
'dry_run' => $options['dry-run'],
|
||||
'verbose' => $options['verbose'],
|
||||
'confirmation_required' => $options['confirm']
|
||||
]);
|
||||
|
||||
$result = $deployer->deploy();
|
||||
$this->showResults($result);
|
||||
|
||||
} catch (Exception $e) {
|
||||
echo "错误: " . $e->getMessage() . PHP_EOL;
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
private function parseOptions(array $argv): array
|
||||
{
|
||||
$options = [
|
||||
'help' => false,
|
||||
'dry-run' => false,
|
||||
'verbose' => false,
|
||||
'confirm' => false,
|
||||
'zip' => null,
|
||||
'target' => null
|
||||
];
|
||||
|
||||
for ($i = 1; $i < count($argv); $i++) {
|
||||
switch ($argv[$i]) {
|
||||
case '--help':
|
||||
case '-h':
|
||||
$options['help'] = true;
|
||||
break;
|
||||
|
||||
case '--dry-run':
|
||||
case '-d':
|
||||
$options['dry-run'] = true;
|
||||
break;
|
||||
|
||||
case '--verbose':
|
||||
case '-v':
|
||||
$options['verbose'] = true;
|
||||
break;
|
||||
|
||||
case '--confirm':
|
||||
case '-c':
|
||||
$options['confirm'] = true;
|
||||
break;
|
||||
|
||||
case '--zip':
|
||||
$options['zip'] = $argv[++$i] ?? null;
|
||||
break;
|
||||
|
||||
case '--target':
|
||||
$options['target'] = $argv[++$i] ?? null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
private function showHelp(): void
|
||||
{
|
||||
echo "增强版自动部署工具\n\n";
|
||||
echo "用法: php enhanced_deploy.php [选项]\n\n";
|
||||
echo "选项:\n";
|
||||
echo " --zip <path> ZIP文件路径 (必需)\n";
|
||||
echo " --target <path> 目标目录路径 (必需)\n";
|
||||
echo " --dry-run, -d 试运行,不实际修改文件\n";
|
||||
echo " --verbose, -v 显示详细输出\n";
|
||||
echo " --confirm, -c 确认删除操作\n";
|
||||
echo " --help, -h 显示此帮助信息\n\n";
|
||||
echo "示例:\n";
|
||||
echo " php enhanced_deploy.php --zip update.zip --target /var/www/html\n";
|
||||
echo " php enhanced_deploy.php --zip patch.zip --target ./app --dry-run\n";
|
||||
echo " php enhanced_deploy.php --zip deploy.zip --target /var/www --confirm\n";
|
||||
}
|
||||
|
||||
private function showResults(array $result): void
|
||||
{
|
||||
echo "\n部署结果:\n";
|
||||
echo "========================================\n";
|
||||
echo "文件部署:\n";
|
||||
echo " 新增: {$result['added']} 个文件\n";
|
||||
echo " 更新: {$result['updated']} 个文件\n";
|
||||
echo " 跳过: {$result['skipped']} 个文件\n";
|
||||
echo " 错误: {$result['errors']} 个文件\n\n";
|
||||
|
||||
echo "文件删除:\n";
|
||||
echo " 总计: {$result['deletion']['total']} 个文件\n";
|
||||
echo " 删除: {$result['deletion']['deleted']} 个文件\n";
|
||||
echo " 跳过: {$result['deletion']['skipped']} 个文件\n";
|
||||
echo " 错误: {$result['deletion']['errors']} 个文件\n";
|
||||
echo "========================================\n";
|
||||
}
|
||||
}
|
||||
|
||||
// 主程序入口
|
||||
if (PHP_SAPI === 'cli') {
|
||||
$cli = new EnhancedDeployerCLI();
|
||||
$cli->run($argv);
|
||||
} else {
|
||||
echo "此脚本只能在命令行模式下运行\n";
|
||||
exit(1);
|
||||
}
|
||||
?>
|
||||
Reference in New Issue
Block a user