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:
461
app/Controller/AdminController.php
Normal file
461
app/Controller/AdminController.php
Normal file
@@ -0,0 +1,461 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Fendx\Core\Annotation\Controller;
|
||||
use Fendx\Web\Annotation\GetRoute;
|
||||
use Fendx\Web\Annotation\PostRoute;
|
||||
use Fendx\Web\Request\Request;
|
||||
use Fendx\Web\Response\Response;
|
||||
use Fendx\Monitor\Service\MonitorService;
|
||||
|
||||
#[Controller('/admin')]
|
||||
class AdminController
|
||||
{
|
||||
#[GetRoute('/dashboard')]
|
||||
public function dashboard(): array
|
||||
{
|
||||
// 检查管理员权限
|
||||
if (!$this->checkAdminPermission()) {
|
||||
return Response::error(403, 'Permission denied');
|
||||
}
|
||||
|
||||
// 获取仪表盘数据
|
||||
$health = MonitorService::getHealthStatus();
|
||||
$metrics = MonitorService::getMetricsSummary();
|
||||
$alerts = MonitorService::getActiveAlerts();
|
||||
$errors = MonitorService::getErrors([], 10);
|
||||
|
||||
$dashboard = [
|
||||
'overview' => [
|
||||
'status' => $health['status'],
|
||||
'uptime' => $metrics['system']['uptime'] ?? 0,
|
||||
'memory_usage' => $metrics['system']['memory_usage'] ?? 0,
|
||||
'cpu_usage' => $metrics['system']['cpu_usage'] ?? 0,
|
||||
'active_alerts' => count($alerts)
|
||||
],
|
||||
'health' => $health,
|
||||
'metrics' => $metrics,
|
||||
'alerts' => array_slice($alerts, 0, 5),
|
||||
'recent_errors' => array_slice($errors, 0, 10),
|
||||
'timestamp' => microtime(true)
|
||||
];
|
||||
|
||||
return Response::success($dashboard);
|
||||
}
|
||||
|
||||
#[GetRoute('/system/info')]
|
||||
public function systemInfo(): array
|
||||
{
|
||||
if (!$this->checkAdminPermission()) {
|
||||
return Response::error(403, 'Permission denied');
|
||||
}
|
||||
|
||||
$info = [
|
||||
'php_version' => PHP_VERSION,
|
||||
'php_sapi' => PHP_SAPI,
|
||||
'os' => PHP_OS,
|
||||
'server' => $_SERVER['SERVER_SOFTWARE'] ?? 'Unknown',
|
||||
'memory_limit' => ini_get('memory_limit'),
|
||||
'max_execution_time' => ini_get('max_execution_time'),
|
||||
'upload_max_filesize' => ini_get('upload_max_filesize'),
|
||||
'post_max_size' => ini_get('post_max_size'),
|
||||
'timezone' => date_default_timezone_get(),
|
||||
'loaded_extensions' => get_loaded_extensions(),
|
||||
'process_id' => getmypid(),
|
||||
'hostname' => gethostname() ?? 'unknown'
|
||||
];
|
||||
|
||||
return Response::success($info);
|
||||
}
|
||||
|
||||
#[GetRoute('/system/status')]
|
||||
public function systemStatus(): array
|
||||
{
|
||||
if (!$this->checkAdminPermission()) {
|
||||
return Response::error(403, 'Permission denied');
|
||||
}
|
||||
|
||||
$status = [
|
||||
'timestamp' => microtime(true),
|
||||
'uptime' => $this->getUptime(),
|
||||
'memory' => [
|
||||
'usage' => memory_get_usage(true),
|
||||
'peak' => memory_get_peak_usage(true),
|
||||
'limit' => $this->parseMemoryLimit(ini_get('memory_limit')),
|
||||
'usage_percent' => $this->getMemoryUsagePercent()
|
||||
],
|
||||
'cpu' => [
|
||||
'load_average' => function_exists('sys_getloadavg') ? sys_getloadavg() : null,
|
||||
'usage' => $this->getCpuUsage()
|
||||
],
|
||||
'disk' => $this->getDiskUsage(),
|
||||
'network' => $this->getNetworkInfo(),
|
||||
'processes' => $this->getProcessInfo()
|
||||
];
|
||||
|
||||
return Response::success($status);
|
||||
}
|
||||
|
||||
#[GetRoute('/config')]
|
||||
public function getConfig(): array
|
||||
{
|
||||
if (!$this->checkAdminPermission()) {
|
||||
return Response::error(403, 'Permission denied');
|
||||
}
|
||||
|
||||
// 获取监控配置(隐藏敏感信息)
|
||||
$config = include dirname(__DIR__, 2) . '/config/config.php';
|
||||
|
||||
$safeConfig = [
|
||||
'monitor' => $config['monitor'] ?? [],
|
||||
'database' => [
|
||||
'driver' => $config['database']['driver'] ?? 'unknown',
|
||||
'host' => $config['database']['host'] ?? 'unknown',
|
||||
'database' => $config['database']['database'] ?? 'unknown'
|
||||
],
|
||||
'cache' => [
|
||||
'driver' => $config['cache']['driver'] ?? 'unknown'
|
||||
]
|
||||
];
|
||||
|
||||
return Response::success($safeConfig);
|
||||
}
|
||||
|
||||
#[PostRoute('/config')]
|
||||
public function updateConfig(Request $request): array
|
||||
{
|
||||
if (!$this->checkAdminPermission()) {
|
||||
return Response::error(403, 'Permission denied');
|
||||
}
|
||||
|
||||
$data = $request->all();
|
||||
|
||||
// 验证配置数据
|
||||
$validation = $this->validateConfig($data);
|
||||
if (!$validation['valid']) {
|
||||
return Response::error(400, 'Invalid config: ' . implode(', ', $validation['errors']));
|
||||
}
|
||||
|
||||
// 这里应该更新配置文件
|
||||
// 为了安全,实际项目中应该有更严格的配置管理
|
||||
|
||||
return Response::success(null, 'Configuration updated successfully');
|
||||
}
|
||||
|
||||
#[PostRoute('/cache/clear')]
|
||||
public function clearCache(): array
|
||||
{
|
||||
if (!$this->checkAdminPermission()) {
|
||||
return Response::error(403, 'Permission denied');
|
||||
}
|
||||
|
||||
try {
|
||||
// 清理应用缓存
|
||||
$cacheDir = dirname(__DIR__, 2) . '/runtime/cache';
|
||||
if (is_dir($cacheDir)) {
|
||||
$this->clearDirectory($cacheDir);
|
||||
}
|
||||
|
||||
// 清理模板缓存
|
||||
$templateDir = dirname(__DIR__, 2) . '/runtime/templates';
|
||||
if (is_dir($templateDir)) {
|
||||
$this->clearDirectory($templateDir);
|
||||
}
|
||||
|
||||
return Response::success(null, 'Cache cleared successfully');
|
||||
} catch (\Exception $e) {
|
||||
return Response::error(500, 'Failed to clear cache: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[PostRoute('/logs/clear')]
|
||||
public function clearLogs(): array
|
||||
{
|
||||
if (!$this->checkAdminPermission()) {
|
||||
return Response::error(403, 'Permission denied');
|
||||
}
|
||||
|
||||
try {
|
||||
$logDir = dirname(__DIR__, 2) . '/runtime/logs';
|
||||
if (is_dir($logDir)) {
|
||||
$this->clearDirectory($logDir, ['.gitkeep']);
|
||||
}
|
||||
|
||||
return Response::success(null, 'Logs cleared successfully');
|
||||
} catch (\Exception $e) {
|
||||
return Response::error(500, 'Failed to clear logs: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[GetRoute('/users')]
|
||||
public function getUsers(): array
|
||||
{
|
||||
if (!$this->checkAdminPermission()) {
|
||||
return Response::error(403, 'Permission denied');
|
||||
}
|
||||
|
||||
// 这里应该从数据库获取用户列表
|
||||
// 暂时返回模拟数据
|
||||
$users = [
|
||||
['id' => 1, 'username' => 'admin', 'email' => 'admin@example.com', 'role' => 'admin', 'status' => 'active', 'created_at' => '2024-01-01 00:00:00'],
|
||||
['id' => 2, 'username' => 'user1', 'email' => 'user1@example.com', 'role' => 'user', 'status' => 'active', 'created_at' => '2024-01-02 00:00:00']
|
||||
];
|
||||
|
||||
return Response::success($users);
|
||||
}
|
||||
|
||||
#[PostRoute('/users/{id}/ban')]
|
||||
public function banUser(int $id): array
|
||||
{
|
||||
if (!$this->checkAdminPermission()) {
|
||||
return Response::error(403, 'Permission denied');
|
||||
}
|
||||
|
||||
// 这里应该更新数据库中的用户状态
|
||||
return Response::success(null, 'User banned successfully');
|
||||
}
|
||||
|
||||
#[PostRoute('/users/{id}/unban')]
|
||||
public function unbanUser(int $id): array
|
||||
{
|
||||
if (!$this->checkAdminPermission()) {
|
||||
return Response::error(403, 'Permission denied');
|
||||
}
|
||||
|
||||
// 这里应该更新数据库中的用户状态
|
||||
return Response::success(null, 'User unbanned successfully');
|
||||
}
|
||||
|
||||
#[GetRoute('/permissions')]
|
||||
public function getPermissions(): array
|
||||
{
|
||||
if (!$this->checkAdminPermission()) {
|
||||
return Response::error(403, 'Permission denied');
|
||||
}
|
||||
|
||||
$permissions = [
|
||||
'dashboard' => ['view'],
|
||||
'monitor' => ['view', 'manage'],
|
||||
'logs' => ['view', 'search', 'export'],
|
||||
'alerts' => ['view', 'acknowledge', 'resolve'],
|
||||
'users' => ['view', 'manage'],
|
||||
'config' => ['view', 'edit'],
|
||||
'system' => ['view', 'cache_clear', 'logs_clear']
|
||||
];
|
||||
|
||||
return Response::success($permissions);
|
||||
}
|
||||
|
||||
#[GetRoute('/audit')]
|
||||
public function getAuditLogs(Request $request): array
|
||||
{
|
||||
if (!$this->checkAdminPermission()) {
|
||||
return Response::error(403, 'Permission denied');
|
||||
}
|
||||
|
||||
$limit = (int)($request->get('limit', 50));
|
||||
$offset = (int)($request->get('offset', 0));
|
||||
$userId = $request->get('user_id');
|
||||
$action = $request->get('action');
|
||||
|
||||
// 这里应该从数据库获取审计日志
|
||||
// 暂时返回模拟数据
|
||||
$logs = [
|
||||
[
|
||||
'id' => 1,
|
||||
'user_id' => 1,
|
||||
'username' => 'admin',
|
||||
'action' => 'login',
|
||||
'resource' => 'admin',
|
||||
'ip' => '127.0.0.1',
|
||||
'user_agent' => 'Mozilla/5.0...',
|
||||
'created_at' => '2024-01-01 12:00:00'
|
||||
],
|
||||
[
|
||||
'id' => 2,
|
||||
'user_id' => 1,
|
||||
'username' => 'admin',
|
||||
'action' => 'config_update',
|
||||
'resource' => 'monitor',
|
||||
'ip' => '127.0.0.1',
|
||||
'user_agent' => 'Mozilla/5.0...',
|
||||
'created_at' => '2024-01-01 12:30:00'
|
||||
]
|
||||
];
|
||||
|
||||
return Response::success([
|
||||
'logs' => $logs,
|
||||
'total' => count($logs),
|
||||
'limit' => $limit,
|
||||
'offset' => $offset
|
||||
]);
|
||||
}
|
||||
|
||||
private function checkAdminPermission(): bool
|
||||
{
|
||||
// 这里应该实现实际的权限检查
|
||||
// 可以检查用户角色、session、token等
|
||||
|
||||
// 简单示例:检查是否有admin session
|
||||
if (session_status() === PHP_SESSION_NONE) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
return isset($_SESSION['user_role']) && $_SESSION['user_role'] === 'admin';
|
||||
}
|
||||
|
||||
private function getUptime(): int
|
||||
{
|
||||
if (function_exists('sys_getloadavg')) {
|
||||
$load = sys_getloadavg();
|
||||
return $load[0] ?? 0;
|
||||
}
|
||||
|
||||
// 尝试从/proc/uptime读取(Linux)
|
||||
if (file_exists('/proc/uptime')) {
|
||||
$uptime = file_get_contents('/proc/uptime');
|
||||
return (int)explode(' ', $uptime)[0];
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function parseMemoryLimit(string $limit): int
|
||||
{
|
||||
if ($limit === '-1') {
|
||||
return -1;
|
||||
}
|
||||
|
||||
$unit = strtoupper(substr($limit, -1));
|
||||
$value = (int)substr($limit, 0, -1);
|
||||
|
||||
return match ($unit) {
|
||||
'G' => $value * 1024 * 1024 * 1024,
|
||||
'M' => $value * 1024 * 1024,
|
||||
'K' => $value * 1024,
|
||||
default => (int)$limit
|
||||
};
|
||||
}
|
||||
|
||||
private function getMemoryUsagePercent(): float
|
||||
{
|
||||
$usage = memory_get_usage(true);
|
||||
$limit = $this->parseMemoryLimit(ini_get('memory_limit'));
|
||||
|
||||
if ($limit <= 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return ($usage / $limit) * 100;
|
||||
}
|
||||
|
||||
private function getCpuUsage(): float
|
||||
{
|
||||
// 简单的CPU使用率计算
|
||||
if (function_exists('sys_getloadavg')) {
|
||||
$load = sys_getloadavg();
|
||||
return $load[0] ?? 0;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function getDiskUsage(): array
|
||||
{
|
||||
$paths = [
|
||||
'root' => dirname(__DIR__, 2),
|
||||
'runtime' => dirname(__DIR__, 2) . '/runtime'
|
||||
];
|
||||
|
||||
$usage = [];
|
||||
foreach ($paths as $name => $path) {
|
||||
if (file_exists($path)) {
|
||||
$free = disk_free_space($path);
|
||||
$total = disk_total_space($path);
|
||||
$used = $total - $free;
|
||||
|
||||
$usage[$name] = [
|
||||
'path' => $path,
|
||||
'total' => $total,
|
||||
'used' => $used,
|
||||
'free' => $free,
|
||||
'usage_percent' => ($used / $total) * 100
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
return $usage;
|
||||
}
|
||||
|
||||
private function getNetworkInfo(): array
|
||||
{
|
||||
return [
|
||||
'hostname' => gethostname() ?? 'unknown',
|
||||
'ip' => $_SERVER['SERVER_ADDR'] ?? '127.0.0.1',
|
||||
'port' => $_SERVER['SERVER_PORT'] ?? 80,
|
||||
'scheme' => $_SERVER['REQUEST_SCHEME'] ?? 'http'
|
||||
];
|
||||
}
|
||||
|
||||
private function getProcessInfo(): array
|
||||
{
|
||||
return [
|
||||
'pid' => getmypid(),
|
||||
'memory_usage' => memory_get_usage(true),
|
||||
'memory_peak' => memory_get_peak_usage(true),
|
||||
'included_files' => count(get_included_files()),
|
||||
'classes' => count(get_declared_classes()),
|
||||
'functions' => count(get_defined_functions()['user'])
|
||||
];
|
||||
}
|
||||
|
||||
private function validateConfig(array $data): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
if (isset($data['sample_rate']) && ($data['sample_rate'] < 0 || $data['sample_rate'] > 1)) {
|
||||
$errors[] = 'Sample rate must be between 0 and 1';
|
||||
}
|
||||
|
||||
if (isset($data['retention']) && $data['retention'] < 0) {
|
||||
$errors[] = 'Retention period must be positive';
|
||||
}
|
||||
|
||||
if (isset($data['error_threshold']) && ($data['error_threshold'] < 0 || $data['error_threshold'] > 1)) {
|
||||
$errors[] = 'Error threshold must be between 0 and 1';
|
||||
}
|
||||
|
||||
if (isset($data['memory_threshold']) && ($data['memory_threshold'] < 0 || $data['memory_threshold'] > 1)) {
|
||||
$errors[] = 'Memory threshold must be between 0 and 1';
|
||||
}
|
||||
|
||||
return [
|
||||
'valid' => empty($errors),
|
||||
'errors' => $errors
|
||||
];
|
||||
}
|
||||
|
||||
private function clearDirectory(string $dir, array $exclude = []): void
|
||||
{
|
||||
if (!is_dir($dir)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$files = scandir($dir);
|
||||
foreach ($files as $file) {
|
||||
if ($file === '.' || $file === '..' || in_array($file, $exclude)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$path = $dir . DIRECTORY_SEPARATOR . $file;
|
||||
if (is_dir($path)) {
|
||||
$this->clearDirectory($path);
|
||||
rmdir($path);
|
||||
} else {
|
||||
unlink($path);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
37
app/Controller/HomeController.php
Normal file
37
app/Controller/HomeController.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Fendx\Core\Context\Context;
|
||||
|
||||
class HomeController
|
||||
{
|
||||
public function index(): array
|
||||
{
|
||||
return [
|
||||
'code' => 200,
|
||||
'message' => 'Welcome to FendxPHP Framework',
|
||||
'data' => [
|
||||
'framework' => 'FendxPHP',
|
||||
'version' => '1.0.0',
|
||||
'traceId' => Context::getTraceId(),
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
public function health(): array
|
||||
{
|
||||
return [
|
||||
'code' => 200,
|
||||
'message' => 'Health check passed',
|
||||
'data' => [
|
||||
'status' => 'healthy',
|
||||
'php_version' => PHP_VERSION,
|
||||
'memory_usage' => memory_get_usage(true),
|
||||
'traceId' => Context::getTraceId(),
|
||||
]
|
||||
];
|
||||
}
|
||||
}
|
||||
497
app/Controller/MonitorController.php
Normal file
497
app/Controller/MonitorController.php
Normal file
@@ -0,0 +1,497 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Fendx\Core\Annotation\Controller;
|
||||
use Fendx\Web\Annotation\GetRoute;
|
||||
use Fendx\Web\Request\Request;
|
||||
use Fendx\Web\Response\Response;
|
||||
use Fendx\Monitor\Service\MonitorService;
|
||||
|
||||
#[Controller('/monitor')]
|
||||
class MonitorController
|
||||
{
|
||||
#[GetRoute('/health')]
|
||||
public function health(): array
|
||||
{
|
||||
$health = MonitorService::getHealthStatus();
|
||||
|
||||
$httpCode = match ($health['status']) {
|
||||
'healthy' => 200,
|
||||
'warning' => 200,
|
||||
'critical' => 503,
|
||||
default => 200
|
||||
};
|
||||
|
||||
return Response::success($health, 'Health check completed', $httpCode);
|
||||
}
|
||||
|
||||
#[GetRoute('/health/{component}')]
|
||||
public function healthComponent(string $component): array
|
||||
{
|
||||
try {
|
||||
$health = MonitorService::checkIndividualHealth($component);
|
||||
|
||||
$httpCode = match ($health['status']) {
|
||||
'healthy' => 200,
|
||||
'warning' => 200,
|
||||
'critical' => 503,
|
||||
default => 200
|
||||
};
|
||||
|
||||
return Response::success($health, "Health check for {$component} completed", $httpCode);
|
||||
} catch (\InvalidArgumentException $e) {
|
||||
return Response::error(404, "Health check '{$component}' not found");
|
||||
}
|
||||
}
|
||||
|
||||
#[GetRoute('/health/checks')]
|
||||
public function healthChecks(): array
|
||||
{
|
||||
$checks = MonitorService::getAvailableHealthChecks();
|
||||
return Response::success($checks);
|
||||
}
|
||||
|
||||
#[GetRoute('/metrics')]
|
||||
public function metrics(Request $request): array
|
||||
{
|
||||
$format = $request->get('format', 'json');
|
||||
|
||||
if ($format === 'prometheus') {
|
||||
header('Content-Type: text/plain; version=0.0.4');
|
||||
echo MonitorService::exportMetrics('prometheus');
|
||||
exit;
|
||||
}
|
||||
|
||||
$metrics = MonitorService::getMetricsSummary();
|
||||
return Response::success($metrics);
|
||||
}
|
||||
|
||||
#[GetRoute('/alerts')]
|
||||
public function alerts(): array
|
||||
{
|
||||
$alerts = MonitorService::getAlerts();
|
||||
return Response::success($alerts);
|
||||
}
|
||||
|
||||
#[GetRoute('/alerts/active')]
|
||||
public function activeAlerts(): array
|
||||
{
|
||||
$alerts = MonitorService::getActiveAlerts();
|
||||
return Response::success($alerts);
|
||||
}
|
||||
|
||||
#[PostRoute('/alerts/{alertId}/acknowledge')]
|
||||
public function acknowledgeAlert(string $alertId, Request $request): array
|
||||
{
|
||||
$data = $request->all();
|
||||
$acknowledgedBy = $data['acknowledged_by'] ?? 'system';
|
||||
|
||||
$success = MonitorService::acknowledgeAlert($alertId, $acknowledgedBy);
|
||||
|
||||
if ($success) {
|
||||
return Response::success(null, 'Alert acknowledged');
|
||||
} else {
|
||||
return Response::error(404, 'Alert not found or already acknowledged');
|
||||
}
|
||||
}
|
||||
|
||||
#[PostRoute('/alerts/{alertId}/resolve')]
|
||||
public function resolveAlert(string $alertId): array
|
||||
{
|
||||
$success = MonitorService::resolveAlert($alertId);
|
||||
|
||||
if ($success) {
|
||||
return Response::success(null, 'Alert resolved');
|
||||
} else {
|
||||
return Response::error(404, 'Alert not found');
|
||||
}
|
||||
}
|
||||
|
||||
#[GetRoute('/errors')]
|
||||
public function errors(Request $request): array
|
||||
{
|
||||
$filters = [];
|
||||
|
||||
if ($type = $request->get('type')) {
|
||||
$filters['type'] = $type;
|
||||
}
|
||||
|
||||
if ($severity = $request->get('severity')) {
|
||||
$filters['severity'] = $severity;
|
||||
}
|
||||
|
||||
$errors = MonitorService::getErrors($filters);
|
||||
return Response::success($errors);
|
||||
}
|
||||
|
||||
#[GetRoute('/errors/statistics')]
|
||||
public function errorStatistics(): array
|
||||
{
|
||||
$stats = MonitorService::getErrorStatistics();
|
||||
return Response::success($stats);
|
||||
}
|
||||
|
||||
#[GetRoute('/errors/trends')]
|
||||
public function errorTrends(Request $request): array
|
||||
{
|
||||
$hours = (int)($request->get('hours', 24));
|
||||
$trends = MonitorService::getErrorTrends($hours);
|
||||
return Response::success($trends);
|
||||
}
|
||||
|
||||
#[GetRoute('/logs/search')]
|
||||
public function searchLogs(Request $request): array
|
||||
{
|
||||
$criteria = [
|
||||
'level' => $request->get('level'),
|
||||
'message' => $request->get('message'),
|
||||
'trace_id' => $request->get('trace_id'),
|
||||
'start_time' => $request->get('start_time'),
|
||||
'end_time' => $request->get('end_time'),
|
||||
'pattern' => $request->get('pattern'),
|
||||
'limit' => (int)($request->get('limit', 100)),
|
||||
'offset' => (int)($request->get('offset', 0)),
|
||||
'sort' => $request->get('sort', 'desc'),
|
||||
'sort_by' => $request->get('sort_by', 'timestamp')
|
||||
];
|
||||
|
||||
$results = MonitorService::searchLogs($criteria);
|
||||
return Response::success($results);
|
||||
}
|
||||
|
||||
#[GetRoute('/logs/aggregate')]
|
||||
public function aggregateLogs(Request $request): array
|
||||
{
|
||||
$criteria = [
|
||||
'time_range' => $request->get('time_range', '1h'),
|
||||
'group_by' => $request->get('group_by', 'level')
|
||||
];
|
||||
|
||||
$results = MonitorService::aggregateLogs($criteria);
|
||||
return Response::success($results);
|
||||
}
|
||||
|
||||
#[GetRoute('/logs/files')]
|
||||
public function getLogFiles(): array
|
||||
{
|
||||
$files = MonitorService::getLogFiles();
|
||||
return Response::success($files);
|
||||
}
|
||||
|
||||
#[GetRoute('/logs/content')]
|
||||
public function getLogContent(Request $request): array
|
||||
{
|
||||
$file = $request->get('file');
|
||||
$lines = (int)($request->get('lines', 100));
|
||||
$offset = (int)($request->get('offset', 0));
|
||||
|
||||
if (!$file) {
|
||||
return Response::error(400, 'File parameter is required');
|
||||
}
|
||||
|
||||
$content = MonitorService::getLogContent($file, $lines, $offset);
|
||||
return Response::success($content);
|
||||
}
|
||||
|
||||
#[GetRoute('/logs/realtime')]
|
||||
public function getRealTimeLogs(Request $request): array
|
||||
{
|
||||
$tail = (int)($request->get('tail', 100));
|
||||
$logs = MonitorService::getRealTimeLogs($tail);
|
||||
return Response::success($logs);
|
||||
}
|
||||
|
||||
#[GetRoute('/logs/export')]
|
||||
public function exportLogs(Request $request): void
|
||||
{
|
||||
$criteria = [
|
||||
'level' => $request->get('level'),
|
||||
'message' => $request->get('message'),
|
||||
'start_time' => $request->get('start_time'),
|
||||
'end_time' => $request->get('end_time'),
|
||||
'limit' => (int)($request->get('limit', 1000))
|
||||
];
|
||||
|
||||
$format = $request->get('format', 'json');
|
||||
$content = MonitorService::exportLogs($criteria, $format);
|
||||
|
||||
$filename = 'logs_' . date('Y-m-d_H-i-s') . '.' . $format;
|
||||
|
||||
header('Content-Type: ' . match ($format) {
|
||||
'json' => 'application/json',
|
||||
'csv' => 'text/csv',
|
||||
'txt' => 'text/plain',
|
||||
default => 'application/octet-stream'
|
||||
});
|
||||
header('Content-Disposition: attachment; filename="' . $filename . '"');
|
||||
echo $content;
|
||||
exit;
|
||||
}
|
||||
|
||||
#[GetRoute('/logs/charts/{chartType}')]
|
||||
public function generateLogChart(string $chartType, Request $request): void
|
||||
{
|
||||
$data = [];
|
||||
$title = $request->get('title', '');
|
||||
|
||||
switch ($chartType) {
|
||||
case 'timeline':
|
||||
case 'error_trend':
|
||||
$timeRange = $request->get('time_range', '1h');
|
||||
$aggregation = MonitorService::aggregateLogs(['time_range' => $timeRange]);
|
||||
$data = $aggregation['timeline'] ?? [];
|
||||
break;
|
||||
|
||||
case 'level_distribution':
|
||||
$aggregation = MonitorService::aggregateLogs();
|
||||
$data = $aggregation['groups'] ?? [];
|
||||
break;
|
||||
|
||||
case 'top_errors':
|
||||
$aggregation = MonitorService::aggregateLogs();
|
||||
$data = $aggregation['top_errors'] ?? [];
|
||||
break;
|
||||
|
||||
case 'bar':
|
||||
$aggregation = MonitorService::aggregateLogs();
|
||||
$data = $aggregation['groups'] ?? [];
|
||||
$title = $title ?: 'Log Distribution';
|
||||
break;
|
||||
|
||||
default:
|
||||
$data = [];
|
||||
}
|
||||
|
||||
$chart = MonitorService::generateLogChart($chartType, $data, $title);
|
||||
|
||||
header('Content-Type: image/svg+xml');
|
||||
header('Cache-Control: no-cache');
|
||||
echo $chart;
|
||||
exit;
|
||||
}
|
||||
|
||||
#[GetRoute('/stats')]
|
||||
public function stats(): array
|
||||
{
|
||||
$metrics = MonitorService::getMetricsSummary();
|
||||
|
||||
$stats = [
|
||||
'system' => $metrics['system'] ?? [],
|
||||
'http_requests' => $this->extractHttpStats($metrics),
|
||||
'database' => $this->extractDatabaseStats($metrics),
|
||||
'cache' => $this->extractCacheStats($metrics),
|
||||
'errors' => $this->extractErrorStats($metrics),
|
||||
'performance' => $this->extractPerformanceStats($metrics)
|
||||
];
|
||||
|
||||
return Response::success($stats);
|
||||
}
|
||||
|
||||
#[GetRoute('/dashboard')]
|
||||
public function dashboard(): array
|
||||
{
|
||||
$health = MonitorService::getHealthStatus();
|
||||
$metrics = MonitorService::getMetricsSummary();
|
||||
$alerts = MonitorService::getAlerts();
|
||||
|
||||
$dashboard = [
|
||||
'overview' => [
|
||||
'status' => $health['status'],
|
||||
'uptime' => $metrics['system']['uptime'] ?? 0,
|
||||
'memory_usage' => $metrics['system']['memory_usage'] ?? 0,
|
||||
'cpu_usage' => $metrics['system']['cpu_usage'] ?? 0,
|
||||
'active_alerts' => count($alerts)
|
||||
],
|
||||
'health_checks' => $health['checks'],
|
||||
'metrics' => [
|
||||
'requests_total' => $this->getTotalRequests($metrics),
|
||||
'errors_total' => $this->getTotalErrors($metrics),
|
||||
'avg_response_time' => $this->getAverageResponseTime($metrics),
|
||||
'success_rate' => $this->getSuccessRate($metrics)
|
||||
],
|
||||
'recent_alerts' => array_slice($alerts, 0, 10)
|
||||
];
|
||||
|
||||
return Response::success($dashboard);
|
||||
}
|
||||
|
||||
#[GetRoute('/clear')]
|
||||
public function clear(): array
|
||||
{
|
||||
MonitorService::clearMetrics();
|
||||
return Response::success(null, 'Metrics cleared');
|
||||
}
|
||||
|
||||
private function extractHttpStats(array $metrics): array
|
||||
{
|
||||
$httpStats = [];
|
||||
|
||||
if (isset($metrics['histograms']['http_request_duration'])) {
|
||||
$httpStats['duration'] = $metrics['histograms']['http_request_duration'];
|
||||
}
|
||||
|
||||
if (isset($metrics['counters'])) {
|
||||
$httpStats['requests_by_status'] = [];
|
||||
foreach ($metrics['counters'] as $key => $value) {
|
||||
if (str_contains($key, 'http_requests_total')) {
|
||||
$httpStats['requests_by_status'][$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $httpStats;
|
||||
}
|
||||
|
||||
private function extractDatabaseStats(array $metrics): array
|
||||
{
|
||||
$dbStats = [];
|
||||
|
||||
if (isset($metrics['histograms']['db_query_duration'])) {
|
||||
$dbStats['query_duration'] = $metrics['histograms']['db_query_duration'];
|
||||
}
|
||||
|
||||
if (isset($metrics['counters'])) {
|
||||
$dbStats['queries_by_type'] = [];
|
||||
foreach ($metrics['counters'] as $key => $value) {
|
||||
if (str_contains($key, 'db_queries_total')) {
|
||||
$dbStats['queries_by_type'][$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $dbStats;
|
||||
}
|
||||
|
||||
private function extractCacheStats(array $metrics): array
|
||||
{
|
||||
$cacheStats = [];
|
||||
|
||||
if (isset($metrics['counters'])) {
|
||||
$cacheStats['operations'] = [];
|
||||
$totalHits = 0;
|
||||
$totalMisses = 0;
|
||||
|
||||
foreach ($metrics['counters'] as $key => $value) {
|
||||
if (str_contains($key, 'cache_operations_total')) {
|
||||
$cacheStats['operations'][$key] = $value;
|
||||
|
||||
if (str_contains($key, 'hit:true')) {
|
||||
$totalHits += $value;
|
||||
} elseif (str_contains($key, 'hit:false')) {
|
||||
$totalMisses += $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$total = $totalHits + $totalMisses;
|
||||
$cacheStats['hit_rate'] = $total > 0 ? round(($totalHits / $total) * 100, 2) . '%' : '0%';
|
||||
}
|
||||
|
||||
return $cacheStats;
|
||||
}
|
||||
|
||||
private function extractErrorStats(array $metrics): array
|
||||
{
|
||||
$errorStats = [];
|
||||
|
||||
if (isset($metrics['counters'])) {
|
||||
$errorStats['errors_by_type'] = [];
|
||||
foreach ($metrics['counters'] as $key => $value) {
|
||||
if (str_contains($key, 'errors_total')) {
|
||||
$errorStats['errors_by_type'][$key] = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($metrics['metrics']['error'])) {
|
||||
$errorStats['recent_errors'] = array_slice($metrics['metrics']['error'], -10);
|
||||
}
|
||||
|
||||
return $errorStats;
|
||||
}
|
||||
|
||||
private function extractPerformanceStats(array $metrics): array
|
||||
{
|
||||
$perfStats = [
|
||||
'memory' => $metrics['system'] ?? [],
|
||||
'response_times' => [],
|
||||
'throughput' => []
|
||||
];
|
||||
|
||||
if (isset($metrics['histograms']['http_request_duration'])) {
|
||||
$perfStats['response_times'] = $metrics['histograms']['http_request_duration'];
|
||||
}
|
||||
|
||||
// 计算吞吐量(每秒请求数)
|
||||
if (isset($metrics['metrics']['http_requests_total'])) {
|
||||
$requests = $metrics['metrics']['http_requests_total'];
|
||||
$now = microtime(true);
|
||||
$requestsInLastMinute = 0;
|
||||
|
||||
foreach ($requests as $request) {
|
||||
if ($now - $request['timestamp'] < 60) {
|
||||
$requestsInLastMinute++;
|
||||
}
|
||||
}
|
||||
|
||||
$perfStats['throughput']['requests_per_minute'] = $requestsInLastMinute;
|
||||
$perfStats['throughput']['requests_per_second'] = round($requestsInLastMinute / 60, 2);
|
||||
}
|
||||
|
||||
return $perfStats;
|
||||
}
|
||||
|
||||
private function getTotalRequests(array $metrics): int
|
||||
{
|
||||
$total = 0;
|
||||
|
||||
if (isset($metrics['counters'])) {
|
||||
foreach ($metrics['counters'] as $key => $value) {
|
||||
if (str_contains($key, 'http_requests_total')) {
|
||||
$total += (int)$value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
private function getTotalErrors(array $metrics): int
|
||||
{
|
||||
$total = 0;
|
||||
|
||||
if (isset($metrics['counters'])) {
|
||||
foreach ($metrics['counters'] as $key => $value) {
|
||||
if (str_contains($key, 'errors_total')) {
|
||||
$total += (int)$value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $total;
|
||||
}
|
||||
|
||||
private function getAverageResponseTime(array $metrics): float
|
||||
{
|
||||
if (isset($metrics['histograms']['http_request_duration']['mean'])) {
|
||||
return round($metrics['histograms']['http_request_duration']['mean'], 3);
|
||||
}
|
||||
|
||||
return 0.0;
|
||||
}
|
||||
|
||||
private function getSuccessRate(array $metrics): string
|
||||
{
|
||||
$totalRequests = $this->getTotalRequests($metrics);
|
||||
$totalErrors = $this->getTotalErrors($metrics);
|
||||
|
||||
if ($totalRequests === 0) {
|
||||
return '100%';
|
||||
}
|
||||
|
||||
$successRate = (($totalRequests - $totalErrors) / $totalRequests) * 100;
|
||||
return round($successRate, 2) . '%';
|
||||
}
|
||||
}
|
||||
223
app/Controller/UserController.php
Normal file
223
app/Controller/UserController.php
Normal file
@@ -0,0 +1,223 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use Fendx\Core\Annotation\Controller;
|
||||
use Fendx\Core\Annotation\Inject;
|
||||
use Fendx\Web\Annotation\GetRoute;
|
||||
use Fendx\Web\Annotation\PostRoute;
|
||||
use Fendx\Web\Annotation\PutRoute;
|
||||
use Fendx\Web\Annotation\DeleteRoute;
|
||||
use Fendx\Web\Request\Request;
|
||||
use Fendx\Web\Response\Response;
|
||||
use Fendx\Web\Validator\Validator;
|
||||
use App\Service\UserService;
|
||||
|
||||
#[Controller('/api/users')]
|
||||
class UserController
|
||||
{
|
||||
#[Inject]
|
||||
private UserService $userService;
|
||||
|
||||
#[GetRoute('/')]
|
||||
public function index(Request $request): array
|
||||
{
|
||||
$page = (int)($request->get('page', 1));
|
||||
$pageSize = min((int)($request->get('pageSize', 10)), 100);
|
||||
|
||||
$result = $this->userService->getUsersPaginated($page, $pageSize);
|
||||
|
||||
return Response::paginated(
|
||||
$result['users'],
|
||||
$result['total'],
|
||||
$result['page'],
|
||||
$result['pageSize']
|
||||
);
|
||||
}
|
||||
|
||||
#[GetRoute('/{id}')]
|
||||
public function show(int $id): array
|
||||
{
|
||||
$user = $this->userService->getUser($id);
|
||||
|
||||
if (!$user) {
|
||||
return Response::notFound('User not found');
|
||||
}
|
||||
|
||||
return Response::success($user->toArray());
|
||||
}
|
||||
|
||||
#[PostRoute('')]
|
||||
public function store(Request $request): array
|
||||
{
|
||||
$data = [
|
||||
'username' => $request->post('username'),
|
||||
'email' => $request->post('email'),
|
||||
'password' => $request->post('password'),
|
||||
'status' => (int)($request->post('status', 1))
|
||||
];
|
||||
|
||||
try {
|
||||
$user = $this->userService->createUser($data);
|
||||
return Response::success($user->toArray(), 'User created successfully');
|
||||
} catch (\Exception $e) {
|
||||
return Response::error($e->getCode(), $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[PutRoute('/{id}')]
|
||||
public function update(int $id, Request $request): array
|
||||
{
|
||||
$data = $request->all();
|
||||
unset($data['id']); // 防止修改ID
|
||||
|
||||
try {
|
||||
$success = $this->userService->updateUser($id, $data);
|
||||
|
||||
if (!$success) {
|
||||
return Response::notFound('User not found');
|
||||
}
|
||||
|
||||
$user = $this->userService->getUser($id);
|
||||
return Response::success($user->toArray(), 'User updated successfully');
|
||||
} catch (\Exception $e) {
|
||||
return Response::error($e->getCode(), $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[DeleteRoute('/{id}')]
|
||||
public function destroy(int $id): array
|
||||
{
|
||||
try {
|
||||
$success = $this->userService->deleteUser($id);
|
||||
|
||||
if (!$success) {
|
||||
return Response::notFound('User not found');
|
||||
}
|
||||
|
||||
return Response::success(null, 'User deleted successfully');
|
||||
} catch (\Exception $e) {
|
||||
return Response::error($e->getCode(), $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[GetRoute('/search')]
|
||||
public function search(Request $request): array
|
||||
{
|
||||
$keyword = $request->get('keyword', '');
|
||||
$page = (int)($request->get('page', 1));
|
||||
$pageSize = min((int)($request->get('pageSize', 10)), 100);
|
||||
|
||||
if (empty($keyword)) {
|
||||
return Response::error(400, 'Keyword is required');
|
||||
}
|
||||
|
||||
$result = $this->userService->searchUsers($keyword, $page, $pageSize);
|
||||
|
||||
return Response::paginated(
|
||||
$result['users'],
|
||||
$result['total'],
|
||||
$result['page'],
|
||||
$result['pageSize']
|
||||
);
|
||||
}
|
||||
|
||||
#[GetRoute('/active')]
|
||||
public function active(): array
|
||||
{
|
||||
$users = $this->userService->getActiveUsers();
|
||||
return Response::success($users);
|
||||
}
|
||||
|
||||
#[GetRoute('/stats')]
|
||||
public function stats(): array
|
||||
{
|
||||
$stats = [
|
||||
'total_users' => $this->userService->getUsersCount(),
|
||||
'active_users' => $this->userService->getActiveUsersCount(),
|
||||
'inactive_users' => $this->userService->getUsersCount() - $this->userService->getActiveUsersCount()
|
||||
];
|
||||
|
||||
return Response::success($stats);
|
||||
}
|
||||
|
||||
#[PutRoute('/{id}/status')]
|
||||
public function toggleStatus(int $id): array
|
||||
{
|
||||
try {
|
||||
$success = $this->userService->toggleUserStatus($id);
|
||||
|
||||
if (!$success) {
|
||||
return Response::notFound('User not found');
|
||||
}
|
||||
|
||||
return Response::success(null, 'User status updated successfully');
|
||||
} catch (\Exception $e) {
|
||||
return Response::error($e->getCode(), $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[PutRoute('/{id}/password')]
|
||||
public function changePassword(int $id, Request $request): array
|
||||
{
|
||||
$data = [
|
||||
'old_password' => $request->post('old_password'),
|
||||
'new_password' => $request->post('new_password')
|
||||
];
|
||||
|
||||
$validator = Validator::make($data, [
|
||||
'old_password' => 'required',
|
||||
'new_password' => 'required|min:6'
|
||||
]);
|
||||
|
||||
if (!$validator->validate()) {
|
||||
return Response::validationError('Validation failed', $validator->errors());
|
||||
}
|
||||
|
||||
try {
|
||||
$success = $this->userService->changePassword($id, $data['old_password'], $data['new_password']);
|
||||
|
||||
if (!$success) {
|
||||
return Response::notFound('User not found');
|
||||
}
|
||||
|
||||
return Response::success(null, 'Password changed successfully');
|
||||
} catch (\Exception $e) {
|
||||
return Response::error($e->getCode(), $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
#[GetRoute('/{id}/exists')]
|
||||
public function exists(int $id): array
|
||||
{
|
||||
$user = $this->userService->getUser($id);
|
||||
return Response::success(['exists' => $user !== null]);
|
||||
}
|
||||
|
||||
#[PostRoute('/check-email')]
|
||||
public function checkEmail(Request $request): array
|
||||
{
|
||||
$email = $request->post('email');
|
||||
|
||||
if (empty($email)) {
|
||||
return Response::error(400, 'Email is required');
|
||||
}
|
||||
|
||||
$user = $this->userService->getUserByEmail($email);
|
||||
return Response::success(['exists' => $user !== null]);
|
||||
}
|
||||
|
||||
#[PostRoute('/check-username')]
|
||||
public function checkUsername(Request $request): array
|
||||
{
|
||||
$username = $request->post('username');
|
||||
|
||||
if (empty($username)) {
|
||||
return Response::error(400, 'Username is required');
|
||||
}
|
||||
|
||||
$user = $this->userService->getUserByUsername($username);
|
||||
return Response::success(['exists' => $user !== null]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user