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,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);
}
}
}
}

View 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(),
]
];
}
}

View 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) . '%';
}
}

View 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]);
}
}