feat(database): 添加用户角色权限系统及相关监控功能

- 创建用户表(users)包含基本信息和认证字段
- 创建角色表(roles)用于权限控制
- 创建权限表(permissions)定义系统权限
- 创建用户角色关联表(user_roles)建立用户与角色关系
- 创建角色权限关联表(role_permissions)建立角色与权限关系
- 创建迁移记录表(migrations)追踪数据库变更
- 添加AdminController提供管理员面板功能
- 实现系统监控、配置管理、缓存清理等功能
- 添加AOP切面编程支持的各种通知类型
- 实现告警管理AlertManager支持多渠道告警
- 添加文档注解接口规范
This commit is contained in:
Lawson
2026-04-08 17:00:28 +08:00
commit 2782d765fb
270 changed files with 107192 additions and 0 deletions

View File

@@ -0,0 +1,24 @@
{
"name": "fendx/job",
"description": "FendxPHP Job Module - 定时任务、队列处理",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Lawson",
"email": "lawson@fendx.cn"
}
],
"require": {
"php": ">=8.1",
"fendx/common": "^1.0",
"fendx/core": "^1.0"
},
"autoload": {
"psr-4": {
"Fendx\\Job\\": "src/"
}
},
"minimum-stability": "stable",
"prefer-stable": true
}

View File

@@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
namespace Fendx\Job\Annotation;
#[\Attribute(\Attribute::TARGET_METHOD)]
final class Scheduled
{
public string $cron;
public string $description;
public function __construct(string $cron, string $description = '')
{
$this->cron = $cron;
$this->description = $description;
}
}

View File

@@ -0,0 +1,207 @@
<?php
declare(strict_types=1);
namespace Fendx\Job\Scheduler;
use Fendx\Job\Annotation\Scheduled;
use Fendx\Core\Container\Container;
use Fendx\Log\Logger;
final class Scheduler
{
private array $jobs = [];
private Container $container;
private Logger $logger;
private bool $running = false;
public function __construct(Container $container, Logger $logger)
{
$this->container = $container;
$this->logger = $logger;
}
public function addJob(string $className, string $method, Scheduled $scheduled): void
{
$this->jobs[] = [
'class' => $className,
'method' => $method,
'cron' => $scheduled->cron,
'description' => $scheduled->description,
'last_run' => null,
'next_run' => $this->getNextRunTime($scheduled->cron)
];
}
public function start(): void
{
$this->running = true;
$this->logger->info('Scheduler started');
while ($this->running) {
$this->runDueJobs();
sleep(1);
}
}
public function stop(): void
{
$this->running = false;
$this->logger->info('Scheduler stopped');
}
public function runDueJobs(): void
{
$now = time();
foreach ($this->jobs as $job) {
if ($job['next_run'] <= $now) {
$this->executeJob($job);
$job['last_run'] = $now;
$job['next_run'] = $this->getNextRunTime($job['cron']);
}
}
}
private function executeJob(array $job): void
{
try {
$this->logger->info("Executing job: {$job['class']}::{$job['method']}");
$instance = $this->container->make($job['class']);
$method = $job['method'];
$start = microtime(true);
$instance->$method();
$duration = round((microtime(true) - $start) * 1000, 2);
$this->logger->info("Job completed: {$job['class']}::{$job['method']} in {$duration}ms");
} catch (\Throwable $e) {
$this->logger->error("Job failed: {$job['class']}::{$job['method']}", [
'error' => $e->getMessage(),
'trace' => $e->getTraceAsString()
]);
}
}
private function getNextRunTime(string $cron): int
{
// 简单的cron解析实现
// 支持格式:* * * * * (分 时 日 月 周)
$parts = explode(' ', $cron);
if (count($parts) !== 5) {
return time() + 60; // 默认1分钟后
}
$now = getdate();
$next = mktime(
$this->getNextValue($parts[1], $now['hours']), // 时
$this->getNextValue($parts[0], $now['minutes']), // 分
$now['mday'], // 日
$now['mon'], // 月
$now['year'] // 年
);
return $next;
}
private function getNextValue(string $part, int $current): int
{
if ($part === '*') {
return $current;
}
if (is_numeric($part)) {
$value = (int)$part;
return $value > $current ? $value : $current + 1;
}
// 支持简单表达式如 */5
if (str_starts_with($part, '*/')) {
$interval = (int)substr($part, 2);
return $current + ($interval - ($current % $interval));
}
return $current + 1;
}
public function getJobs(): array
{
return $this->jobs;
}
public function isRunning(): bool
{
return $this->running;
}
public function scanJobs(string $scanPath): void
{
if (!is_dir($scanPath)) {
return;
}
$files = glob($scanPath . '/**/*.php');
foreach ($files as $file) {
$this->scanJobFile($file);
}
}
private function scanJobFile(string $file): void
{
$className = $this->getClassNameFromFile($file);
if ($className === null || class_exists($className) === false) {
return;
}
try {
$reflection = new \ReflectionClass($className);
$methods = $reflection->getMethods(\ReflectionMethod::IS_PUBLIC);
foreach ($methods as $method) {
if ($method->getName() === '__construct') {
continue;
}
$scheduledAttributes = $method->getAttributes(Scheduled::class);
foreach ($scheduledAttributes as $attribute) {
$scheduled = $attribute->newInstance();
$this->addJob($className, $method->getName(), $scheduled);
}
}
} catch (\ReflectionException $e) {
$this->logger->error("Failed to scan job file $file: " . $e->getMessage());
}
}
private function getClassNameFromFile(string $file): ?string
{
$content = file_get_contents($file);
if (preg_match('/namespace\s+([^;]+);/', $content, $matches)) {
$namespace = trim($matches[1]);
$className = basename($file, '.php');
return $namespace . '\\' . $className;
}
return null;
}
public function runJob(string $jobName): void
{
foreach ($this->jobs as $job) {
$jobIdentifier = "{$job['class']}::{$job['method']}";
if ($jobIdentifier === $jobName) {
$this->executeJob($job);
return;
}
}
throw new \InvalidArgumentException("Job not found: $jobName");
}
}