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,29 @@
{
"name": "fendx/starter",
"description": "FendxPHP Starter Module - 启动器、自动装配、入口",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Lawson",
"email": "lawson@fendx.cn"
}
],
"require": {
"php": ">=8.1",
"fendx/common": "^1.0",
"fendx/core": "^1.0",
"fendx/web": "^1.0",
"fendx/db": "^1.0",
"fendx/cache": "^1.0",
"fendx/security": "^1.0",
"fendx/log": "^1.0"
},
"autoload": {
"psr-4": {
"Fendx\\Starter\\": "src/"
}
},
"minimum-stability": "stable",
"prefer-stable": true
}

View File

@@ -0,0 +1,282 @@
<?php
declare(strict_types=1);
namespace Fendx\Starter;
use Fendx\Common\Exception\BusinessException;
use Fendx\Core\Config\Config;
use Fendx\Core\Container\Container;
use Fendx\Core\Context\Context;
use Fendx\Core\Event\EventDispatcher;
final class Application
{
private static ?self $instance = null;
private Container $container;
private EventDispatcher $eventDispatcher;
private bool $booted = false;
private function __construct()
{
$this->container = Container::getInstance();
$this->eventDispatcher = EventDispatcher::getInstance($this->container);
}
public static function getInstance(): self
{
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
public function bootstrap(array $config): self
{
if ($this->booted) {
return $this;
}
$this->checkEnvironment();
$this->loadConfig($config);
$this->registerCoreServices();
$this->scanAnnotations();
$this->bootProviders();
$this->booted = true;
return $this;
}
private function checkEnvironment(): void
{
if (version_compare(PHP_VERSION, '8.1.0', '<')) {
throw new BusinessException(500, 'PHP_VERSION_TOO_LOW', ['version' => PHP_VERSION]);
}
$requiredExtensions = ['pdo', 'json', 'mbstring'];
foreach ($requiredExtensions as $ext) {
if (!extension_loaded($ext)) {
throw new BusinessException(500, 'EXTENSION_NOT_LOADED', ['extension' => $ext]);
}
}
}
private function loadConfig(array $config): void
{
Config::load($config);
// 设置时区
$timezone = Config::get('app.timezone', 'Asia/Shanghai');
date_default_timezone_set($timezone);
}
private function registerCoreServices(): void
{
// 注册核心服务
$this->container->singleton(Container::class, fn() => $this->container);
$this->container->singleton(EventDispatcher::class, fn() => $this->eventDispatcher);
$this->container->singleton(Config::class, fn() => new Config());
}
private function scanAnnotations(): void
{
$scanner = $this->container->get(AnnotationScanner::class);
// 扫描应用目录
$scanPaths = [
app_path(),
fendx_framework_path(),
];
foreach ($scanPaths as $path) {
if (is_dir($path)) {
$scanner->scan($path);
}
}
// 注册注解处理器
$this->registerAnnotationProcessors();
}
private function registerAnnotationProcessors(): void
{
// 注册控制器注解处理器
$this->eventDispatcher->addListener('annotation.controller', function($data) {
$this->container->singleton($data['class'], $data['class']);
});
// 注册服务注解处理器
$this->eventDispatcher->addListener('annotation.service', function($data) {
$this->container->singleton($data['class'], $data['class']);
});
// 注册路由注解处理器
$this->eventDispatcher->addListener('annotation.route', function($data) {
$router = $this->container->get(\Fendx\Web\Router\Router::class);
$router->addRoute($data['method'], $data['path'], $data['handler']);
});
}
private function bootProviders(): void
{
// 启动缓存服务
$cacheConfig = Config::get('cache', []);
if (!empty($cacheConfig)) {
$this->container->singleton(\Fendx\Cache\Cache::class, function() use ($cacheConfig) {
return new \Fendx\Cache\Cache($cacheConfig);
});
}
// 启动数据库服务
$dbConfig = Config::get('database', []);
if (!empty($dbConfig)) {
$this->container->singleton(\Fendx\Db\DB::class, function() use ($dbConfig) {
return new \Fendx\Db\DB($dbConfig);
});
}
// 启动日志服务
$logConfig = Config::get('logging', []);
if (!empty($logConfig)) {
$this->container->singleton(\Fendx\Log\Logger::class, function() use ($logConfig) {
return new \Fendx\Log\Logger($logConfig);
});
}
}
public function run(): void
{
if (!$this->booted) {
throw new BusinessException(500, 'APPLICATION_NOT_BOOTED');
}
// 初始化请求上下文
Context::init();
// 处理HTTP请求
$this->handleRequest();
}
private function handleRequest(): void
{
$request = \Fendx\Web\Request\Request::createFromGlobals();
$router = $this->container->get(\Fendx\Web\Router\Router::class);
try {
// 路由匹配
$route = $router->match($request);
if (!$route) {
$this->send404Response();
return;
}
// 执行路由处理器
$response = $this->executeRoute($route, $request);
// 发送响应
$this->sendResponse($response);
} catch (\Exception $e) {
$this->handleException($e);
}
}
private function executeRoute($route, \Fendx\Web\Request\Request $request): \Fendx\Web\Response\HttpResponse
{
$handler = $route->getHandler();
if (is_string($handler)) {
// 控制器方法格式: Controller@method
if (strpos($handler, '@') !== false) {
[$controllerClass, $method] = explode('@', $handler);
$controller = $this->container->get($controllerClass);
return $controller->$method($request);
}
// 直接调用类方法
return $this->container->get($handler)($request);
}
if (is_callable($handler)) {
return $handler($request);
}
throw new BusinessException(500, 'Invalid route handler');
}
private function sendResponse(\Fendx\Web\Response\HttpResponse $response): void
{
$response->send();
}
private function send404Response(): void
{
$response = new \Fendx\Web\Response\HttpResponse();
$response->setStatusCode(404)
->json([
'code' => 404,
'message' => 'Not Found',
'data' => null,
'trace_id' => Context::getTraceId(),
])
->send();
}
private function handleException(\Exception $e): void
{
$response = new \Fendx\Web\Response\HttpResponse();
if ($e instanceof BusinessException) {
$response->setStatusCode($e->getCode())
->json([
'code' => $e->getCode(),
'message' => $e->getMessage(),
'data' => null,
'trace_id' => Context::getTraceId(),
]);
} else {
// 记录异常日志
$this->logException($e);
$response->setStatusCode(500)
->json([
'code' => 500,
'message' => 'Internal Server Error',
'data' => null,
'trace_id' => Context::getTraceId(),
]);
}
$response->send();
}
private function logException(\Exception $e): void
{
$logger = $this->container->get(\Fendx\Log\Logger::class);
$logger->error('Unhandled exception', [
'exception' => $e->getMessage(),
'file' => $e->getFile(),
'line' => $e->getLine(),
'trace' => $e->getTraceAsString(),
'trace_id' => Context::getTraceId(),
]);
}
public function getContainer(): Container
{
return $this->container;
}
public function getEventDispatcher(): EventDispatcher
{
return $this->eventDispatcher;
}
public function terminate(): void
{
// 清理资源
Context::clear();
$this->container->flush();
$this->booted = false;
}
}

View File

@@ -0,0 +1,230 @@
<?php
declare(strict_types=1);
namespace Fendx\Starter;
use Fendx\Common\Exception\BusinessException;
use Fendx\Core\Config\Config;
use Fendx\Core\Context\Context;
use Fendx\Core\Container\Container;
use Fendx\Core\Scanner\AnnotationScanner;
use Fendx\Web\Route\Router;
use Fendx\Web\Request\Request;
use Fendx\Web\Response\Response;
use Fendx\Web\Scanner\RouteScanner;
use Fendx\Web\Interceptor\InterceptorManager;
use Fendx\Web\Interceptor\AuthInterceptor;
use Fendx\Cache\Cache;
use Fendx\Security\Auth\Auth;
use Fendx\Security\Token\TokenManager;
use Fendx\Log\Logger;
use Fendx\Db\DB;
use Fendx\Db\Transaction\TransactionManager;
use Fendx\Job\Scheduler\Scheduler;
use Fendx\File\FileManager;
use Fendx\Monitor\Service\MonitorService;
use Fendx\Monitor\Interceptor\MonitorInterceptor;
// 检查是否在命令行环境中运行
$isCli = php_sapi_name() === 'cli';
// 只在非CLI环境中设置CORS headers
if (!$isCli) {
header('Access-Control-Allow-Origin: *');
header('Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS');
header('Access-Control-Allow-Headers: Content-Type, Authorization, X-Requested-With');
header('Access-Control-Allow-Credentials: true');
header('Access-Control-Max-Age: 86400');
// 处理预检请求
if (isset($_SERVER['REQUEST_METHOD']) && $_SERVER['REQUEST_METHOD'] == 'OPTIONS') {
http_response_code(200);
exit(0);
}
}
// 自动加载器
spl_autoload_register(static function (string $class): void {
$prefixes = [
'App\\' => dirname(__DIR__, 3) . '/app/',
'Fendx\\' => dirname(__DIR__) . '/',
];
foreach ($prefixes as $prefix => $baseDir) {
if (!str_starts_with($class, $prefix)) {
continue;
}
$relative = substr($class, strlen($prefix));
$path = $baseDir . str_replace('\\', '/', $relative) . '.php';
if (is_file($path)) {
require $path;
}
return;
}
});
final class Bootstrap
{
private Application $application;
public function __construct()
{
$this->application = Application::getInstance();
}
public function run(): void
{
try {
// 初始化TraceId
Context::setTraceId(uniqid('trace_', true));
// 加载配置
$config = require dirname(__DIR__, 3) . '/config/config.php';
// 启动应用
$this->application->bootstrap($config);
// 初始化核心组件
$this->initializeCoreComponents($config);
// 扫描注解和注册Bean
$this->scanAnnotations();
// 创建路由器并注册路由
$router = $this->initializeRouter();
// 处理请求
$request = Request::createFromGlobals();
$response = $router->dispatch($request);
// 发送响应
$response->send();
} catch (BusinessException $e) {
$this->handleException($e);
} catch (\Throwable $e) {
$this->handleException(new BusinessException(500, 'SERVER_ERROR', ['message' => $e->getMessage()]));
}
}
private function initializeCoreComponents(array $config): void
{
// 初始化缓存
if (isset($config['cache'])) {
Cache::configure($config['cache']);
}
// 初始化安全组件
if (isset($config['security'])) {
$tokenManager = new TokenManager($config['security']['token']);
Auth::initialize($tokenManager);
}
// 初始化日志
if (isset($config['log'])) {
$logger = Logger::create('fendx', $config['log']);
$this->application->getContainer()->singleton(Logger::class, $logger);
}
// 初始化数据库
if (isset($config['database'])) {
DB::configure($config['database']);
}
// 初始化文件管理器
$fileConfig = $config['file'] ?? [
'type' => 'local',
'root' => dirname(__DIR__, 3) . '/runtime/storage'
];
$fileManager = FileManager::getInstance($fileConfig);
$this->application->getContainer()->singleton(FileManager::class, $fileManager);
// 初始化事务管理器
$transactionManager = TransactionManager::createTransactionalAspect();
$this->application->getContainer()->singleton('transactionAspect', $transactionManager);
// 初始化监控服务
if (isset($config['monitor'])) {
MonitorService::initialize($config['monitor']);
$this->application->getContainer()->singleton(MonitorService::class, MonitorService::class);
}
}
private function scanAnnotations(): void
{
$container = $this->application->getContainer();
$scanner = new AnnotationScanner($container);
// 扫描应用目录
$appPath = dirname(__DIR__, 3) . '/app';
$scanner->scan($appPath);
// 扫描框架目录
$frameworkPath = dirname(__DIR__) . '/fendx-framework';
if (is_dir($frameworkPath)) {
$scanner->scan($frameworkPath);
}
}
private function initializeRouter(): Router
{
$container = $this->application->getContainer();
$router = new Router();
// 扫描路由注解
$routeScanner = new RouteScanner($router);
$appPath = dirname(__DIR__, 3) . '/app';
$routeScanner->scan($appPath);
// 加载配置文件路由
$routesFile = dirname(__DIR__, 3) . '/config/routes.php';
if (file_exists($routesFile)) {
$router->loadRoutes($routesFile);
}
// 初始化拦截器
$this->initializeInterceptors($router);
return $router;
}
private function initializeInterceptors(Router $router): void
{
$container = $this->application->getContainer();
$interceptorManager = new InterceptorManager();
// 添加监控拦截器
if (MonitorService::isEnabled()) {
$monitorInterceptor = new MonitorInterceptor();
$interceptorManager->addGlobalInterceptor($monitorInterceptor, 1000); // 优先级较低,最后执行
}
// 添加认证拦截器
$authInterceptor = new AuthInterceptor();
$interceptorManager->addGlobalInterceptor($authInterceptor, 100);
// 注册拦截器管理器
$container->singleton(InterceptorManager::class, $interceptorManager);
// 设置路由拦截器
$router->setInterceptorManager($interceptorManager);
}
private function handleException(BusinessException $e): void
{
if (php_sapi_name() === 'cli') {
echo "Error: " . $e->getMessage() . PHP_EOL;
exit(1);
}
header('Content-Type: application/json; charset=utf-8');
http_response_code(500);
echo json_encode([
'code' => $e->getErrorCode(),
'message' => $e->getMessage(),
'data' => $e->getData(),
'traceId' => Context::getTraceId(),
], JSON_UNESCAPED_UNICODE);
}
}