mirror of
https://devops.lemonos.cn/lawson/FendxPHP.git
synced 2026-06-15 23:12:49 +08:00
feat(database): 添加用户角色权限系统及相关监控功能
- 创建用户表(users)包含基本信息和认证字段 - 创建角色表(roles)用于权限控制 - 创建权限表(permissions)定义系统权限 - 创建用户角色关联表(user_roles)建立用户与角色关系 - 创建角色权限关联表(role_permissions)建立角色与权限关系 - 创建迁移记录表(migrations)追踪数据库变更 - 添加AdminController提供管理员面板功能 - 实现系统监控、配置管理、缓存清理等功能 - 添加AOP切面编程支持的各种通知类型 - 实现告警管理AlertManager支持多渠道告警 - 添加文档注解接口规范
This commit is contained in:
29
fendx-framework/fendx-starter/composer.json
Normal file
29
fendx-framework/fendx-starter/composer.json
Normal 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
|
||||
}
|
||||
282
fendx-framework/fendx-starter/src/Application.php
Normal file
282
fendx-framework/fendx-starter/src/Application.php
Normal 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;
|
||||
}
|
||||
}
|
||||
230
fendx-framework/fendx-starter/src/Bootstrap.php
Normal file
230
fendx-framework/fendx-starter/src/Bootstrap.php
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user