初始化-安装依赖
This commit is contained in:
182
server/plugin/saiadmin/command/SaiOrm.php
Normal file
182
server/plugin/saiadmin/command/SaiOrm.php
Normal file
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\saiadmin\command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
use Symfony\Component\Console\Question\ChoiceQuestion;
|
||||
|
||||
/**
|
||||
* SaiAdmin ORM 切换命令
|
||||
* 用于切换 saiadmin 插件的 ORM 实现 (think / eloquent)
|
||||
*/
|
||||
class SaiOrm extends Command
|
||||
{
|
||||
protected static $defaultName = 'sai:orm';
|
||||
protected static $defaultDescription = '切换 SaiAdmin 使用的 ORM';
|
||||
|
||||
/**
|
||||
* ORM 源文件目录
|
||||
*/
|
||||
protected string $ormSourcePath;
|
||||
|
||||
/**
|
||||
* 目标插件目录
|
||||
*/
|
||||
protected string $pluginAppPath;
|
||||
|
||||
/**
|
||||
* ORM 选项配置
|
||||
*/
|
||||
protected array $ormOptions = [
|
||||
'think' => '1. ThinkORM (TopThink)',
|
||||
'eloquent' => '2. Eloquent ORM (Laravel)',
|
||||
'exit' => '3. 退出,什么也不做',
|
||||
];
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('sai:orm')
|
||||
->setDescription('切换 SaiAdmin 使用的 ORM');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$io->title('SaiAdmin ORM 切换工具');
|
||||
$io->text('此命令只切换 saiadmin 框架核心使用的 ORM, 不影响其他模块功能, 多种 ORM 可以同时使用!');
|
||||
$io->newLine();
|
||||
|
||||
// 创建选择问题(编号从1开始)
|
||||
$helper = $this->getHelper('question');
|
||||
$choices = [
|
||||
1 => '1. ThinkORM (TopThink)',
|
||||
2 => '2. Eloquent ORM (Laravel)',
|
||||
3 => '3. 退出,什么也不做',
|
||||
];
|
||||
$question = new ChoiceQuestion(
|
||||
'请选择要使用的 ORM 框架:',
|
||||
$choices,
|
||||
1 // 默认选中第一个
|
||||
);
|
||||
$question->setErrorMessage('选项 %s 无效');
|
||||
|
||||
// 获取用户选择
|
||||
$selected = $helper->ask($input, $output, $question);
|
||||
|
||||
// 根据选择的文本反查 key
|
||||
$selectedKey = array_search($selected, $choices);
|
||||
|
||||
// 如果选择退出
|
||||
if ($selectedKey == 3) {
|
||||
$io->newLine();
|
||||
$io->info('已退出,什么也没做。');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// 映射选项到 ORM 类型
|
||||
$ormMap = [1 => 'think', 2 => 'eloquent'];
|
||||
$orm = $ormMap[$selectedKey];
|
||||
|
||||
$io->newLine();
|
||||
$io->section("您选择了: {$selected}");
|
||||
|
||||
// 确认操作
|
||||
if (!$io->confirm('确定要切换吗?这将覆盖 saiadmin 核心代码文件', true)) {
|
||||
$io->warning('操作已取消');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
// 设置路径
|
||||
$this->ormSourcePath = BASE_PATH . '/vendor/saithink/saiadmin/src/orm/' . $orm . '/app';
|
||||
$this->pluginAppPath = BASE_PATH . '/plugin/saiadmin/app';
|
||||
|
||||
// 检查源目录是否存在
|
||||
if (!is_dir($this->ormSourcePath)) {
|
||||
$io->error("ORM 源目录不存在: {$this->ormSourcePath}");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$io->section('开始复制文件...');
|
||||
|
||||
try {
|
||||
$copiedFiles = $this->copyDirectory($this->ormSourcePath, $this->pluginAppPath, $io);
|
||||
|
||||
$io->newLine();
|
||||
$io->success([
|
||||
"ORM 切换成功!",
|
||||
"已切换到: {$selected}",
|
||||
"复制文件数: {$copiedFiles}"
|
||||
]);
|
||||
|
||||
$io->note([
|
||||
'请重启 webman 服务使更改生效',
|
||||
'命令: php webman restart 或 php windows.php'
|
||||
]);
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (\Exception $e) {
|
||||
$io->error("切换失败: " . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归复制目录
|
||||
* @param string $source 源目录
|
||||
* @param string $dest 目标目录
|
||||
* @param SymfonyStyle $io 输出接口
|
||||
* @return int 复制的文件数量
|
||||
*/
|
||||
protected function copyDirectory(string $source, string $dest, SymfonyStyle $io): int
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
if (!is_dir($dest)) {
|
||||
mkdir($dest, 0755, true);
|
||||
}
|
||||
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
// 先计算文件总数用于进度条
|
||||
$files = [];
|
||||
foreach ($iterator as $item) {
|
||||
if (!$item->isDir()) {
|
||||
$files[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建进度条
|
||||
$io->progressStart(count($files));
|
||||
|
||||
// 重新遍历并复制
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $item) {
|
||||
$destPath = $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName();
|
||||
|
||||
if ($item->isDir()) {
|
||||
if (!is_dir($destPath)) {
|
||||
mkdir($destPath, 0755, true);
|
||||
}
|
||||
} else {
|
||||
copy($item->getPathname(), $destPath);
|
||||
$count++;
|
||||
$io->progressAdvance();
|
||||
}
|
||||
}
|
||||
|
||||
$io->progressFinish();
|
||||
|
||||
return $count;
|
||||
}
|
||||
}
|
||||
297
server/plugin/saiadmin/command/SaiPlugin.php
Normal file
297
server/plugin/saiadmin/command/SaiPlugin.php
Normal file
@@ -0,0 +1,297 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\saiadmin\command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* SaiAdmin 插件创建命令
|
||||
* 用于创建 saiadmin 插件
|
||||
*/
|
||||
class SaiPlugin extends Command
|
||||
{
|
||||
protected static $defaultName = 'sai:plugin';
|
||||
protected static $defaultDescription = '创建 SaiAdmin 插件';
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this->addArgument('name', InputArgument::REQUIRED, 'App plugin name');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @return int
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
$io->title('SaiAdmin 插件创建工具');
|
||||
$io->text('此命令用于创建基于webman的 saiadmin 插件, 用于扩展 saiadmin 框架功能!');
|
||||
$io->newLine();
|
||||
|
||||
$name = $input->getArgument('name');
|
||||
$io->text("创建 SaiAdmin 插件 $name");
|
||||
|
||||
if (strpos($name, '/') !== false) {
|
||||
$io->error('名称错误,名称必须不包含字符 \'/\'');
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
// Create dir config/plugin/$name
|
||||
if (is_dir($plugin_config_path = base_path() . "/plugin/$name")) {
|
||||
$io->error("目录 $plugin_config_path 已存在");
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$this->createAll($name);
|
||||
|
||||
$io->newLine();
|
||||
$io->success("SaiAdmin 插件创建成功!");
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $name
|
||||
* @return void
|
||||
*/
|
||||
protected function createAll($name)
|
||||
{
|
||||
$base_path = base_path();
|
||||
$this->mkdir("$base_path/plugin/$name/app", 0777, true);
|
||||
$this->mkdir("$base_path/plugin/$name/app/admin/controller", 0777, true);
|
||||
$this->mkdir("$base_path/plugin/$name/app/admin/logic", 0777, true);
|
||||
$this->mkdir("$base_path/plugin/$name/app/api/controller", 0777, true);
|
||||
$this->mkdir("$base_path/plugin/$name/app/api/logic", 0777, true);
|
||||
$this->mkdir("$base_path/plugin/$name/app/cache", 0777, true);
|
||||
$this->mkdir("$base_path/plugin/$name/app/event", 0777, true);
|
||||
$this->mkdir("$base_path/plugin/$name/app/model", 0777, true);
|
||||
$this->mkdir("$base_path/plugin/$name/app/middleware", 0777, true);
|
||||
$this->mkdir("$base_path/plugin/$name/config", 0777, true);
|
||||
$this->createControllerFile("$base_path/plugin/$name/app/api/controller/IndexController.php", $name);
|
||||
$this->createFunctionsFile("$base_path/plugin/$name/app/functions.php");
|
||||
$this->createConfigFiles("$base_path/plugin/$name/config", $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $path
|
||||
* @return void
|
||||
*/
|
||||
protected function mkdir($path, $mode = 0777, $recursive = false)
|
||||
{
|
||||
if (is_dir($path)) {
|
||||
return;
|
||||
}
|
||||
echo "Create $path\r\n";
|
||||
mkdir($path, $mode, $recursive);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $path
|
||||
* @param $name
|
||||
* @return void
|
||||
*/
|
||||
protected function createControllerFile($path, $name)
|
||||
{
|
||||
$content = <<<EOF
|
||||
<?php
|
||||
|
||||
namespace plugin\\$name\\app\\api\\controller;
|
||||
|
||||
use plugin\\saiadmin\\basic\\OpenController;
|
||||
|
||||
class IndexController extends OpenController
|
||||
{
|
||||
|
||||
public function index()
|
||||
{
|
||||
return \$this->success([
|
||||
'app' => '$name',
|
||||
'version' => '1.0.0',
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
EOF;
|
||||
file_put_contents($path, $content);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $file
|
||||
* @return void
|
||||
*/
|
||||
protected function createFunctionsFile($file)
|
||||
{
|
||||
$content = <<<EOF
|
||||
<?php
|
||||
/**
|
||||
* Here is your custom functions.
|
||||
*/
|
||||
|
||||
|
||||
|
||||
EOF;
|
||||
file_put_contents($file, $content);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param $base
|
||||
* @param $name
|
||||
* @return void
|
||||
*/
|
||||
protected function createConfigFiles($base, $name)
|
||||
{
|
||||
// app.php
|
||||
$content = <<<EOF
|
||||
<?php
|
||||
|
||||
use support\\Request;
|
||||
|
||||
return [
|
||||
'debug' => true,
|
||||
'controller_suffix' => 'Controller',
|
||||
'controller_reuse' => false,
|
||||
'version' => '1.0.0'
|
||||
];
|
||||
|
||||
EOF;
|
||||
file_put_contents("$base/app.php", $content);
|
||||
|
||||
// autoload.php
|
||||
$content = <<<EOF
|
||||
<?php
|
||||
return [
|
||||
'files' => [
|
||||
base_path() . '/plugin/$name/app/functions.php',
|
||||
]
|
||||
];
|
||||
EOF;
|
||||
file_put_contents("$base/autoload.php", $content);
|
||||
|
||||
// container.php
|
||||
$content = <<<EOF
|
||||
<?php
|
||||
return new Webman\\Container;
|
||||
|
||||
EOF;
|
||||
file_put_contents("$base/container.php", $content);
|
||||
|
||||
|
||||
// exception.php
|
||||
$content = <<<EOF
|
||||
<?php
|
||||
|
||||
return [
|
||||
'' => \\plugin\\saiadmin\\app\\exception\\Handler::class,
|
||||
];
|
||||
|
||||
EOF;
|
||||
file_put_contents("$base/exception.php", $content);
|
||||
|
||||
// log.php
|
||||
$content = <<<EOF
|
||||
<?php
|
||||
|
||||
return [
|
||||
'default' => [
|
||||
'handlers' => [
|
||||
[
|
||||
'class' => Monolog\\Handler\\RotatingFileHandler::class,
|
||||
'constructor' => [
|
||||
runtime_path() . '/logs/$name.log',
|
||||
7,
|
||||
Monolog\\Logger::DEBUG,
|
||||
],
|
||||
'formatter' => [
|
||||
'class' => Monolog\\Formatter\\LineFormatter::class,
|
||||
'constructor' => [null, 'Y-m-d H:i:s', true],
|
||||
],
|
||||
]
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
EOF;
|
||||
file_put_contents("$base/log.php", $content);
|
||||
|
||||
// middleware.php
|
||||
$content = <<<EOF
|
||||
<?php
|
||||
|
||||
use plugin\saiadmin\app\middleware\SystemLog;
|
||||
use plugin\saiadmin\app\middleware\CheckLogin;
|
||||
use plugin\saiadmin\app\middleware\CheckAuth;
|
||||
|
||||
return [
|
||||
'admin' => [
|
||||
CheckLogin::class,
|
||||
CheckAuth::class,
|
||||
SystemLog::class,
|
||||
],
|
||||
'api' => [
|
||||
]
|
||||
];
|
||||
|
||||
EOF;
|
||||
file_put_contents("$base/middleware.php", $content);
|
||||
|
||||
// process.php
|
||||
$content = <<<EOF
|
||||
<?php
|
||||
return [];
|
||||
|
||||
EOF;
|
||||
file_put_contents("$base/process.php", $content);
|
||||
|
||||
// route.php
|
||||
$content = <<<EOF
|
||||
<?php
|
||||
|
||||
use Webman\\Route;
|
||||
|
||||
|
||||
EOF;
|
||||
file_put_contents("$base/route.php", $content);
|
||||
|
||||
// static.php
|
||||
$content = <<<EOF
|
||||
<?php
|
||||
|
||||
return [
|
||||
'enable' => true,
|
||||
'middleware' => [], // Static file Middleware
|
||||
];
|
||||
|
||||
EOF;
|
||||
file_put_contents("$base/static.php", $content);
|
||||
|
||||
// translation.php
|
||||
$content = <<<EOF
|
||||
<?php
|
||||
|
||||
return [
|
||||
// Default language
|
||||
'locale' => 'zh_CN',
|
||||
// Fallback language
|
||||
'fallback_locale' => ['zh_CN', 'en'],
|
||||
// Folder where language files are stored
|
||||
'path' => base_path() . "/plugin/$name/resource/translations",
|
||||
];
|
||||
|
||||
EOF;
|
||||
file_put_contents("$base/translation.php", $content);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
198
server/plugin/saiadmin/command/SaiUpgrade.php
Normal file
198
server/plugin/saiadmin/command/SaiUpgrade.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
|
||||
namespace plugin\saiadmin\command;
|
||||
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* SaiAdmin 升级命令
|
||||
* 用于从 vendor 目录升级 saiadmin 插件到最新版本
|
||||
*/
|
||||
class SaiUpgrade extends Command
|
||||
{
|
||||
protected static $defaultName = 'sai:upgrade';
|
||||
protected static $defaultDescription = '升级 SaiAdmin 插件到最新版本';
|
||||
|
||||
/**
|
||||
* 升级源目录
|
||||
*/
|
||||
protected string $sourcePath;
|
||||
|
||||
/**
|
||||
* 目标插件目录
|
||||
*/
|
||||
protected string $targetPath;
|
||||
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('sai:upgrade')
|
||||
->setDescription('升级 SaiAdmin 插件到最新版本');
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$io->title('SaiAdmin 升级工具');
|
||||
$io->text([
|
||||
'此命令将从 vendor 目录复制最新版本的 saiadmin 插件文件到 plugin 目录',
|
||||
'源目录: vendor/saithink/saiadmin/src/plugin/saiadmin',
|
||||
'目标目录: plugin/saiadmin',
|
||||
]);
|
||||
$io->newLine();
|
||||
|
||||
// 设置路径
|
||||
$this->sourcePath = BASE_PATH . '/vendor/saithink/saiadmin/src/plugin/saiadmin';
|
||||
$this->targetPath = BASE_PATH . '/plugin/saiadmin';
|
||||
|
||||
// 检查源目录是否存在
|
||||
if (!is_dir($this->sourcePath)) {
|
||||
$io->error([
|
||||
"升级源目录不存在: {$this->sourcePath}",
|
||||
"请确保已通过 composer 安装了 saithink/saiadmin 包",
|
||||
]);
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
// 获取版本信息
|
||||
$currentVersion = $this->getVersion($this->targetPath);
|
||||
$latestVersion = $this->getVersion($this->sourcePath);
|
||||
|
||||
// 显示版本信息
|
||||
$io->section('版本信息');
|
||||
$io->table(
|
||||
['项目', '版本'],
|
||||
[
|
||||
['当前版本', $currentVersion ?: '未知'],
|
||||
['最新版本', $latestVersion ?: '未知'],
|
||||
]
|
||||
);
|
||||
|
||||
// 版本对比提示
|
||||
if ($currentVersion && $latestVersion) {
|
||||
if (version_compare($currentVersion, $latestVersion, '>=')) {
|
||||
$io->success('当前已是最新版本!');
|
||||
if (!$io->confirm('是否仍要继续覆盖安装?', false)) {
|
||||
$io->info('操作已取消');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
} else {
|
||||
$io->info("发现新版本: {$currentVersion} → {$latestVersion}");
|
||||
}
|
||||
}
|
||||
|
||||
// 警告信息
|
||||
$io->warning([
|
||||
"注意:此操作将覆盖 {$this->targetPath} 目录的现有文件!",
|
||||
"建议在执行前备份您的自定义修改。",
|
||||
]);
|
||||
|
||||
// 确认操作
|
||||
if (!$io->confirm('确定要执行升级操作吗?', false)) {
|
||||
$io->info('操作已取消');
|
||||
return Command::SUCCESS;
|
||||
}
|
||||
|
||||
$io->section('开始升级...');
|
||||
|
||||
try {
|
||||
$copiedFiles = $this->copyDirectory($this->sourcePath, $this->targetPath, $io);
|
||||
|
||||
$io->newLine();
|
||||
$io->success([
|
||||
"SaiAdmin 升级成功!",
|
||||
"复制文件数: {$copiedFiles}",
|
||||
]);
|
||||
|
||||
$io->note([
|
||||
'请重启 webman 服务使更改生效',
|
||||
'命令: php webman restart 或 php windows.php',
|
||||
]);
|
||||
|
||||
return Command::SUCCESS;
|
||||
} catch (\Exception $e) {
|
||||
$io->error("升级失败: " . $e->getMessage());
|
||||
return Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归复制目录
|
||||
* @param string $source 源目录
|
||||
* @param string $dest 目标目录
|
||||
* @param SymfonyStyle $io 输出接口
|
||||
* @return int 复制的文件数量
|
||||
*/
|
||||
protected function copyDirectory(string $source, string $dest, SymfonyStyle $io): int
|
||||
{
|
||||
$count = 0;
|
||||
|
||||
if (!is_dir($dest)) {
|
||||
mkdir($dest, 0755, true);
|
||||
}
|
||||
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
// 先计算文件总数用于进度条
|
||||
$files = [];
|
||||
foreach ($iterator as $item) {
|
||||
if (!$item->isDir()) {
|
||||
$files[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建进度条
|
||||
$io->progressStart(count($files));
|
||||
|
||||
// 重新遍历并复制
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
|
||||
\RecursiveIteratorIterator::SELF_FIRST
|
||||
);
|
||||
|
||||
foreach ($iterator as $item) {
|
||||
$destPath = $dest . DIRECTORY_SEPARATOR . $iterator->getSubPathName();
|
||||
|
||||
if ($item->isDir()) {
|
||||
if (!is_dir($destPath)) {
|
||||
mkdir($destPath, 0755, true);
|
||||
}
|
||||
} else {
|
||||
copy($item->getPathname(), $destPath);
|
||||
$count++;
|
||||
$io->progressAdvance();
|
||||
}
|
||||
}
|
||||
|
||||
$io->progressFinish();
|
||||
|
||||
return $count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从目录中获取版本号
|
||||
* @param string $path 插件目录路径
|
||||
* @return string|null 版本号
|
||||
*/
|
||||
protected function getVersion(string $path): ?string
|
||||
{
|
||||
$configFile = $path . '/config/app.php';
|
||||
|
||||
if (!file_exists($configFile)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$config = include $configFile;
|
||||
return $config['version'] ?? null;
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user