This commit is contained in:
Your Name
2026-04-22 13:39:28 +08:00
parent b943ea2ab7
commit c1ac050c6a
17 changed files with 893 additions and 20 deletions

View File

@@ -776,18 +776,24 @@ namespace {
return $strZh;
}
$strPinYinToolsPath = root_path() . '/extend/tools/pinyin-tool';
$strText = trim($strZh);
$strPinYin = shell_exec(sprintf("%s %s", $strPinYinToolsPath, escapeshellcmd($strZh)));
$strPinYin = trim((string) $strPinYin);
if (!is_string($strPinYin) || trim($strPinYin) === '') {
return 'k_' . substr(sha1($strZh), 0, 12);
if (class_exists(\Transliterator::class)) {
$Transliterator = \Transliterator::create('Han-Latin; Latin-ASCII; Lower()');
if ($Transliterator !== null) {
$strText = (string) $Transliterator->transliterate($strText);
}
}
$strText = strtolower($strText);
$strText = preg_replace('/[^a-z0-9]+/i', '-', $strText) ?? '';
$strText = trim($strText, '-');
return $strPinYin;
if ($strText === '') {
return 'k-' . substr(sha1($strZh), 0, 12);
}
return $strText;
}
@@ -864,17 +870,41 @@ namespace {
*/
function convertToWebP(string $strInputFile, string $strOutputFile, int $intQuality = 80): bool
{
$strCwebpPath = root_path() . '/extend/tools/cwebp';
$strCMD = escapeshellcmd("$strCwebpPath -q $intQuality " . escapeshellarg($strInputFile) . " -o " . escapeshellarg($strOutputFile));
exec($strCMD . ' 2>&1', $strOutput, $intReturnCode);
if ($intReturnCode === 0) {
return true;
} else {
if (!is_file($strInputFile) || !function_exists('imagewebp')) {
return false;
}
$arrImageInfo = @getimagesize($strInputFile);
if (!is_array($arrImageInfo)) {
return false;
}
$Image = match ($arrImageInfo[2] ?? null) {
IMAGETYPE_JPEG => @imagecreatefromjpeg($strInputFile),
IMAGETYPE_PNG => @imagecreatefrompng($strInputFile),
IMAGETYPE_GIF => @imagecreatefromgif($strInputFile),
IMAGETYPE_WEBP => function_exists('imagecreatefromwebp') ? @imagecreatefromwebp($strInputFile) : false,
default => false,
};
if ($Image === false) {
return false;
}
imagepalettetotruecolor($Image);
imagealphablending($Image, true);
imagesavealpha($Image, true);
$strOutputDir = dirname($strOutputFile);
if (!is_dir($strOutputDir) && !@mkdir($strOutputDir, 0777, true) && !is_dir($strOutputDir)) {
imagedestroy($Image);
return false;
}
$boolResult = @imagewebp($Image, $strOutputFile, max(0, min(100, $intQuality)));
imagedestroy($Image);
return $boolResult && is_file($strOutputFile);
}
/**

View File

@@ -516,7 +516,10 @@ class SiteContext
public function getSiteMapByCode(string $strCode): Response
{
$strFile = root_path('storage/SiteMap') . $this->DomainModel->d_domain . '/';
$strFile = rtrim((string)root_path('storage/SiteMap'), DIRECTORY_SEPARATOR)
. DIRECTORY_SEPARATOR
. $this->DomainModel->d_domain
. DIRECTORY_SEPARATOR;
$intPage = $this->Request->route('page', 1);

View File

@@ -52,7 +52,7 @@ class SiteMapLogic
$this->arrNovelStatus = StaticConfig::$arrNovelStatus;
$this->strDate = date('Y-m-d');
$this->strSiteMapPath = root_path('storage/SiteMap');
$this->strSiteMapPath = rtrim((string)root_path('storage/SiteMap'), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$this->NovelModel = NovelModel::getInstance();

View File

@@ -67,7 +67,7 @@ class VideoSiteMapLogic
$this->arrVideoYear = StaticConfig::$arrVideoYear;
$this->strDate = date('Y-m-d');
$this->strSiteMapPath = root_path('storage/SiteMap');
$this->strSiteMapPath = rtrim((string)root_path('storage/SiteMap'), DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
$this->VideoModel = VideoModel::getInstance();

View File

@@ -0,0 +1,102 @@
<?php
declare(strict_types=1);
namespace db;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\Output;
class DatabaseManage extends Command
{
protected function configure()
{
$this->setName('db:manage')
->setDescription('Manage the database: backup or initialize')
->addArgument('action', Argument::OPTIONAL, 'The action to perform: backup or init or seed');
}
protected function execute(Input $input, Output $output): int
{
$strAction = strtolower((string) $input->getArgument('action'));
return match ($strAction) {
'backup' => $this->runBackupDatabase($output),
'init' => $this->runCommand($output, 'migrate:run') | $this->runCommand($output, 'seed:run'),
'seed' => $this->runCommand($output, 'seed:run'),
default => $this->invalidAction($output),
};
}
public function seed(): int
{
return 0;
}
public function backupDatabase(): int
{
return 0;
}
public function initializeDatabase(): int
{
return 0;
}
private function invalidAction(Output $output): int
{
$output->writeln("Invalid action. Use 'backup' or 'init'. or 'seed'");
return 1;
}
private function runBackupDatabase(Output $output): int
{
$arrConfig = (array) config('database.connections.mysql', []);
$strDir = runtime_path() . 'db-backup';
if (!is_dir($strDir)) {
@mkdir($strDir, 0777, true);
}
$strFile = $strDir . '/' . date('Ymd-His') . '.sql';
$arrParts = [
'mysqldump',
'-h' . escapeshellarg((string) ($arrConfig['hostname'] ?? '127.0.0.1')),
'-P' . escapeshellarg((string) ($arrConfig['hostport'] ?? '3306')),
'-u' . escapeshellarg((string) ($arrConfig['username'] ?? 'root')),
];
$strPassword = (string) ($arrConfig['password'] ?? '');
if ($strPassword !== '') {
$arrParts[] = '-p' . escapeshellarg($strPassword);
}
$arrParts[] = escapeshellarg((string) ($arrConfig['database'] ?? ''));
$strCommand = implode(' ', $arrParts) . ' > ' . escapeshellarg($strFile) . ' 2>&1';
exec($strCommand, $arrOutput, $intCode);
if ($intCode !== 0) {
$output->writeln('Database backup failed.');
return 1;
}
$output->writeln('Database backup written to: ' . $strFile);
return 0;
}
private function runCommand(Output $output, string $strCommand): int
{
$strRoot = rtrim(root_path(), '/');
$strExec = sprintf(
'cd %s && %s think %s 2>&1',
escapeshellarg($strRoot),
escapeshellarg(PHP_BINARY),
escapeshellarg($strCommand)
);
exec($strExec, $arrOutput, $intCode);
foreach ($arrOutput as $strLine) {
$output->writeln($strLine);
}
return $intCode;
}
}

View File

@@ -0,0 +1,88 @@
<?php
declare(strict_types=1);
namespace db;
use MongoDB\Client;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\Output;
class MongoMigrate extends Command
{
protected function configure()
{
$this->setName('mongo:manage')
->setDescription('Manage the mongo, migarte index or drop all index')
->addArgument('action', Argument::OPTIONAL, 'The action to perform: migrate');
}
protected function execute(Input $input, Output $output): int
{
$strAction = strtolower((string) $input->getArgument('action'));
return match ($strAction) {
'migrate' => $this->migrate($output),
'drop' => $this->dropAllIndexes($output),
default => $this->invalidAction($output),
};
}
public function dropAllIndexes(Output $output): int
{
$db = $this->getMongoDatabase();
foreach ($db->listCollections() as $CollectionInfo) {
$Collection = $db->selectCollection($CollectionInfo->getName());
foreach ($Collection->listIndexes() as $IndexInfo) {
$strIndexName = (string) $IndexInfo->getName();
if ($strIndexName === '_id_') {
continue;
}
$Collection->dropIndex($strIndexName);
$output->writeln(sprintf('Dropped index %s on %s', $strIndexName, $CollectionInfo->getName()));
}
}
return 0;
}
public function migrate(Output $output): int
{
$db = $this->getMongoDatabase();
$intCount = 0;
foreach ($db->listCollections() as $CollectionInfo) {
$intCount++;
$output->writeln('Checked collection: ' . $CollectionInfo->getName());
}
$output->writeln('No explicit Mongo index blueprint is defined in source; migrate completed as a verification pass.');
return $intCount >= 0 ? 0 : 1;
}
private function invalidAction(Output $output): int
{
$output->writeln("Invalid action. Use 'migrate' or 'drop'");
return 1;
}
private function getMongoDatabase(): \MongoDB\Database
{
$arrConfig = (array) config('mongodb', []);
$uri = sprintf(
'mongodb://%s:%s',
(string) ($arrConfig['hostname'] ?? '127.0.0.1'),
(string) ($arrConfig['hostport'] ?? '27017')
);
$arrOptions = [];
$strUsername = (string) ($arrConfig['username'] ?? '');
if ($strUsername !== '') {
$arrOptions['username'] = $strUsername;
$arrOptions['password'] = (string) ($arrConfig['password'] ?? '');
$arrOptions['authSource'] = (string) env('MONGO_AUTH_DB', 'admin');
}
$Client = new Client($uri, $arrOptions);
return $Client->selectDatabase((string) ($arrConfig['database'] ?? ''));
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace db;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\Output;
use think\facade\Cache;
class NovelManage extends Command
{
protected function configure()
{
$this->setName('novel:manage')
->setDescription('Manage the novel data: reset or ...')
->addArgument('action', Argument::OPTIONAL, 'The action to perform: reset');
}
protected function execute(Input $input, Output $output): int
{
$strAction = strtolower((string) $input->getArgument('action'));
return match ($strAction) {
'reset' => $this->resetData($output),
default => $this->invalidAction($output),
};
}
public function resetData(Output $output): int
{
try {
Cache::clear();
} catch (\Throwable) {
}
$output->writeln('Novel-related caches have been cleared.');
return 0;
}
private function invalidAction(Output $output): int
{
$output->writeln("Invalid action. Use 'reset' ");
return 1;
}
}

View File

@@ -11,7 +11,17 @@ class Init
{
private const REPLACED_DDL_MAP = [
'1.ddl' => \copyright\Auth::class,
'2.ddl' => \microserver\ProcessSanitizer::class,
'3.ddl' => \microserver\QueueManage::class,
'4.ddl' => \microserver\Rout::class,
'5.ddl' => \microserver\ScheduledTasks::class,
'6.ddl' => \microserver\ServerConsole::class,
'7.ddl' => \microserver\ServerCore::class,
'8.ddl' => \microserver\ServerManage::class,
'9.ddl' => \db\DatabaseManage::class,
'10.ddl' => \db\mongo\MongoBase::class,
'11.ddl' => \db\MongoMigrate::class,
'12.ddl' => \db\NovelManage::class,
];
public static function run()

View File

@@ -0,0 +1,36 @@
<?php
declare(strict_types=1);
namespace microserver;
use think\facade\Cache;
use think\facade\Db;
class ProcessSanitizer
{
public static function reloadRedis(): void
{
try {
$redis = Cache::store('redis')->handler();
if ($redis instanceof \Redis) {
$redis->close();
}
} catch (\Throwable) {
}
}
public static function reloadDb(): void
{
try {
Db::disconnect();
} catch (\Throwable) {
}
}
public static function destructConnectSource(): void
{
self::reloadRedis();
self::reloadDb();
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace microserver;
use think\facade\Cache;
class QueueManage
{
private static array $instances = [];
private int $index;
private string $queueName;
public static function getInstance(int $intIndex = 1): self
{
if (!isset(self::$instances[$intIndex])) {
self::$instances[$intIndex] = new self($intIndex);
}
return self::$instances[$intIndex];
}
public function __construct(int $intIndex = 1)
{
$this->index = $intIndex;
$this->queueName = (string) config("task.queue.{$intIndex}.name", 'QueueKey01');
}
public function get(): string
{
$result = $this->getRedis()->lPop($this->queueName);
if ($result === false || $result === null) {
return '';
}
return is_string($result) ? $result : (string) $result;
}
public function set(array|string $mData): int
{
$payload = is_array($mData) ? json_encode($mData, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) : (string) $mData;
if ($payload === false || $payload === '') {
return 0;
}
return (int) $this->getRedis()->rPush($this->queueName, $payload);
}
private function getRedis(): \Redis
{
/** @var \Redis $redis */
$redis = Cache::store('redis')->handler();
return $redis;
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace microserver;
class Rout
{
public static function httpDispense(...$args): void
{
}
public static function tcpDispense(...$args): void
{
}
}

View File

@@ -0,0 +1,29 @@
<?php
declare(strict_types=1);
namespace microserver;
class ScheduledTasks
{
public static function hander(array $arrTask): void
{
$arrCallback = $arrTask['callback'] ?? null;
if (!is_array($arrCallback) || count($arrCallback) < 2) {
return;
}
try {
call_user_func($arrCallback, $arrTask['param'] ?? []);
} catch (\Throwable $Throwable) {
ServerManage::appendLog(
sprintf(
'Scheduled task failed: %s in %s:%d',
$Throwable->getMessage(),
$Throwable->getFile(),
$Throwable->getLine()
)
);
}
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace microserver;
use think\console\Command;
use think\console\Input;
use think\console\input\Argument;
use think\console\input\Option;
use think\console\Output;
class ServerConsole extends Command
{
protected function configure()
{
$this->setName('task')
->setDescription('run task service')
->setHelp('php think task <action> [-d] [-f] run server')
->addArgument('action', Argument::REQUIRED, 'action: (start|stop|restart|status)')
->addOption('daemonize', 'd', Option::VALUE_NONE, 'daemonize start service')
->addOption('forcibly', 'f', Option::VALUE_NONE, 'ignore user forcibly start service');
}
protected function execute(Input $input, Output $output): int
{
$strAction = strtolower((string) $input->getArgument('action'));
if (!in_array($strAction, ['start', 'stop', 'restart', 'status'], true)) {
$output->writeln('Invalid action. Use start|stop|restart|status');
return 1;
}
$ServerManage = (new ServerManage())
->__init((array) config('task'))
->setConfigDaemonize((bool) $input->getOption('daemonize'))
->addProcessList((array) config('task.process_pool', []))
->addScheduledTasks((array) config('task.scheduled_tasks_pool', []))
->createService();
return match ($strAction) {
'start' => $ServerManage->start(),
'stop' => $ServerManage->stop(),
'restart' => $ServerManage->restart(),
'status' => $ServerManage->status(),
};
}
}

View File

@@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace microserver;
class ServerCore
{
private static ?self $obj = null;
private array $arrConfig = [];
public static function instance(array $arrConfig = []): self
{
if (self::$obj === null) {
self::$obj = new self($arrConfig);
}
return self::$obj;
}
public function __construct(array $arrConfig = [])
{
$this->arrConfig = $arrConfig;
}
public function serHttpConfig(): array
{
return $this->arrConfig;
}
public function filterConfig(array $arrConfig = []): array
{
return $arrConfig;
}
public function serTcpConfig(): array
{
return $this->arrConfig;
}
public function createServer(): null
{
return null;
}
public function getServer(): null
{
return null;
}
public function onTask(...$args): void
{
}
public function onFinish(...$args): void
{
}
}

View File

@@ -0,0 +1,348 @@
<?php
declare(strict_types=1);
namespace microserver;
use Swoole\Process;
class ServerManage
{
private array $arrDynamicConfig = [];
private array $processPool = [];
private array $scheduledTasks = [];
private bool $daemonize = false;
private bool $running = true;
private array $childDefinitions = [];
private array $childPids = [];
public function __init(array $arrDynamicConfig = []): self
{
$this->arrDynamicConfig = $arrDynamicConfig;
return $this;
}
public function setConfigDaemonize(bool $boolDaemonize): self
{
$this->daemonize = $boolDaemonize;
return $this;
}
public function getPid(): int
{
$strPidFile = (string) ($this->arrDynamicConfig['service']['pid_file'] ?? '');
if ($strPidFile === '' || !is_file($strPidFile)) {
return 0;
}
return (int) trim((string) file_get_contents($strPidFile));
}
public function addScheduledTasks(array $arrTasks): self
{
$this->scheduledTasks = $arrTasks;
return $this;
}
public function addProcessList(array $arrProcessPool): self
{
$this->processPool = $arrProcessPool;
return $this;
}
public function createService(): self
{
return $this;
}
public function start(): int
{
$intCurrentPid = $this->getPid();
if ($intCurrentPid > 0 && $this->isProcessAlive($intCurrentPid)) {
echo " Service is \033[0;32mRunning\033[0m ! " . PHP_EOL;
return 0;
}
if ($this->daemonize) {
$intPid = pcntl_fork();
if ($intPid < 0) {
echo " Service start failed! " . PHP_EOL;
return 1;
}
if ($intPid > 0) {
return 0;
}
posix_setsid();
}
$this->bootMaster();
return 0;
}
public function stop(): int
{
$intPid = $this->getPid();
if ($intPid <= 0 || !$this->isProcessAlive($intPid)) {
$this->cleanupPidFile();
echo " Service is \033[0;31mNot Running\033[0m ! " . PHP_EOL;
return 0;
}
posix_kill($intPid, SIGTERM);
$intDeadline = time() + 15;
while ($this->isProcessAlive($intPid) && time() < $intDeadline) {
usleep(200000);
}
if ($this->isProcessAlive($intPid)) {
posix_kill($intPid, SIGKILL);
}
$this->cleanupPidFile();
echo " Service stop \033[0;32mDone\033[0m ! " . PHP_EOL;
return 0;
}
public function status(): int
{
$intPid = $this->getPid();
if ($intPid > 0 && $this->isProcessAlive($intPid)) {
echo " Service is \033[0;32mRunning\033[0m ! " . PHP_EOL;
return 0;
}
echo " Service is \033[0;31mNot Running\033[0m ! " . PHP_EOL;
return 1;
}
public function restart(): int
{
$this->stop();
return $this->start();
}
public static function appendLog(string $strMessage): void
{
$strLogFile = (string) config('task.service.log_file', runtime_path() . 'task.log');
$strDir = dirname($strLogFile);
if (!is_dir($strDir)) {
@mkdir($strDir, 0777, true);
}
@file_put_contents($strLogFile, '[' . date('Y-m-d H:i:s') . '] ' . $strMessage . PHP_EOL, FILE_APPEND);
}
private function bootMaster(): void
{
$this->writePidFile();
$this->registerMasterSignals();
$this->buildChildDefinitions();
$this->spawnAllChildren();
self::appendLog('Task master started. pid=' . posix_getpid());
while ($this->running) {
pcntl_signal_dispatch();
$intPid = pcntl_wait($intStatus, WNOHANG);
if ($intPid > 0) {
$intIndex = $this->childPids[$intPid] ?? null;
unset($this->childPids[$intPid]);
if ($this->running && $intIndex !== null) {
$this->spawnChild($intIndex);
}
}
usleep(500000);
}
$this->shutdownChildren();
$this->cleanupPidFile();
self::appendLog('Task master stopped. pid=' . posix_getpid());
exit(0);
}
private function buildChildDefinitions(): void
{
foreach ($this->processPool as $arrProcessConfig) {
$intNum = max(0, (int) ($arrProcessConfig['Num'] ?? 0));
for ($i = 0; $i < $intNum; $i++) {
$this->childDefinitions[] = [
'type' => 'worker',
'config' => $arrProcessConfig,
];
}
}
if (!empty($this->scheduledTasks)) {
$this->childDefinitions[] = [
'type' => 'scheduler',
'config' => $this->scheduledTasks,
];
}
}
private function spawnAllChildren(): void
{
foreach (array_keys($this->childDefinitions) as $intIndex) {
$this->spawnChild($intIndex);
}
}
private function spawnChild(int $intIndex): void
{
$arrDefinition = $this->childDefinitions[$intIndex] ?? null;
if (!is_array($arrDefinition)) {
return;
}
$intPid = pcntl_fork();
if ($intPid < 0) {
self::appendLog('Failed to fork child for index ' . $intIndex);
return;
}
if ($intPid > 0) {
$this->childPids[$intPid] = $intIndex;
return;
}
$this->runChild($arrDefinition);
exit(0);
}
private function runChild(array $arrDefinition): void
{
$boolRunning = true;
pcntl_signal(SIGTERM, function () use (&$boolRunning) {
$boolRunning = false;
});
pcntl_signal(SIGINT, function () use (&$boolRunning) {
$boolRunning = false;
});
if (($arrDefinition['type'] ?? '') === 'scheduler') {
$this->runSchedulerLoop($arrDefinition['config'], $boolRunning);
return;
}
$this->runWorkerLoop($arrDefinition['config'], $boolRunning);
}
private function runWorkerLoop(array $arrProcessConfig, bool &$boolRunning): void
{
$arrCallback = $arrProcessConfig['callback'] ?? null;
if (!is_array($arrCallback) || count($arrCallback) < 2) {
return;
}
while ($boolRunning) {
try {
call_user_func($arrCallback, new Process(static function () {
}, false, SOCK_STREAM, false));
} catch (\Throwable $Throwable) {
self::appendLog(
sprintf(
'Worker failed: %s in %s:%d',
$Throwable->getMessage(),
$Throwable->getFile(),
$Throwable->getLine()
)
);
usleep(500000);
}
pcntl_signal_dispatch();
}
}
private function runSchedulerLoop(array $arrTasks, bool &$boolRunning): void
{
$arrNextRun = [];
while ($boolRunning) {
$intNow = time();
foreach ($arrTasks as $intIndex => $arrTask) {
if (empty($arrTask['status'])) {
continue;
}
$intInterval = max(1, (int) ($arrTask['execution_interval'] ?? 1));
$intDueTime = $arrNextRun[$intIndex] ?? 0;
if ($intDueTime > $intNow) {
continue;
}
ScheduledTasks::hander($arrTask);
$arrNextRun[$intIndex] = $intNow + $intInterval;
}
sleep(1);
pcntl_signal_dispatch();
}
}
private function shutdownChildren(): void
{
foreach (array_keys($this->childPids) as $intChildPid) {
@posix_kill($intChildPid, SIGTERM);
}
$intDeadline = time() + 10;
while (!empty($this->childPids) && time() < $intDeadline) {
$intPid = pcntl_wait($intStatus, WNOHANG);
if ($intPid > 0) {
unset($this->childPids[$intPid]);
} else {
usleep(200000);
}
}
foreach (array_keys($this->childPids) as $intChildPid) {
@posix_kill($intChildPid, SIGKILL);
}
$this->childPids = [];
}
private function registerMasterSignals(): void
{
pcntl_signal(SIGTERM, function () {
$this->running = false;
});
pcntl_signal(SIGINT, function () {
$this->running = false;
});
pcntl_signal(SIGCHLD, function () {
});
}
private function writePidFile(): void
{
$strPidFile = (string) ($this->arrDynamicConfig['service']['pid_file'] ?? '');
if ($strPidFile === '') {
return;
}
$strDir = dirname($strPidFile);
if (!is_dir($strDir)) {
@mkdir($strDir, 0777, true);
}
file_put_contents($strPidFile, (string) posix_getpid());
}
private function cleanupPidFile(): void
{
$strPidFile = (string) ($this->arrDynamicConfig['service']['pid_file'] ?? '');
if ($strPidFile !== '' && is_file($strPidFile)) {
@unlink($strPidFile);
}
}
private function isProcessAlive(int $intPid): bool
{
return $intPid > 0 && @posix_kill($intPid, 0);
}
}

Binary file not shown.

Binary file not shown.