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:
60
app/Command/SchedulerCommand.php
Normal file
60
app/Command/SchedulerCommand.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use Fendx\Core\Annotation\Service;
|
||||
use Fendx\Job\Scheduler\Scheduler;
|
||||
use Fendx\Log\Logger;
|
||||
|
||||
#[Service]
|
||||
class SchedulerCommand
|
||||
{
|
||||
private Scheduler $scheduler;
|
||||
|
||||
public function __construct(Scheduler $scheduler)
|
||||
{
|
||||
$this->scheduler = $scheduler;
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
Logger::info('Starting scheduler');
|
||||
|
||||
try {
|
||||
$this->scheduler->start();
|
||||
} catch (\Throwable $e) {
|
||||
Logger::error('Scheduler error: ' . $e->getMessage());
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
public function list(): void
|
||||
{
|
||||
$jobs = $this->scheduler->getJobs();
|
||||
|
||||
echo "Scheduled Jobs:\n";
|
||||
echo "================\n";
|
||||
|
||||
foreach ($jobs as $job) {
|
||||
echo sprintf(
|
||||
"Job: %s::%s\nCron: %s\nDescription: %s\nNext run: %s\n\n",
|
||||
$job['class'],
|
||||
$job['method'],
|
||||
$job['cron'],
|
||||
$job['description'],
|
||||
date('Y-m-d H:i:s', $job['next_run'])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
public function runJob(string $jobName): void
|
||||
{
|
||||
try {
|
||||
$this->scheduler->runJob($jobName);
|
||||
echo "Job '$jobName' executed successfully\n";
|
||||
} catch (\Exception $e) {
|
||||
echo "Job execution failed: " . $e->getMessage() . "\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
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]);
|
||||
}
|
||||
}
|
||||
106
app/Dao/UserDao.php
Normal file
106
app/Dao/UserDao.php
Normal file
@@ -0,0 +1,106 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Dao;
|
||||
|
||||
use Fendx\Core\Annotation\Dao;
|
||||
use App\Entity\User;
|
||||
use Fendx\Db\ORM\Model;
|
||||
|
||||
#[Dao]
|
||||
class UserDao
|
||||
{
|
||||
public function findById(int $id): ?User
|
||||
{
|
||||
return User::find($id);
|
||||
}
|
||||
|
||||
public function findByEmail(string $email): ?User
|
||||
{
|
||||
return User::where('email', $email)->first();
|
||||
}
|
||||
|
||||
public function findByUsername(string $username): ?User
|
||||
{
|
||||
return User::where('username', $username)->first();
|
||||
}
|
||||
|
||||
public function findAllActive(): array
|
||||
{
|
||||
return User::where('status', 1)->orderBy('created_at', 'desc')->get();
|
||||
}
|
||||
|
||||
public function findPaginated(int $page = 1, int $pageSize = 10): array
|
||||
{
|
||||
$offset = ($page - 1) * $pageSize;
|
||||
|
||||
$users = User::orderBy('created_at', 'desc')
|
||||
->limit($pageSize)
|
||||
->offset($offset)
|
||||
->get();
|
||||
|
||||
$total = User::count();
|
||||
|
||||
return [
|
||||
'users' => $users,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
'totalPages' => ceil($total / $pageSize)
|
||||
];
|
||||
}
|
||||
|
||||
public function create(array $data): User
|
||||
{
|
||||
$data['created_at'] = date('Y-m-d H:i:s');
|
||||
$data['updated_at'] = date('Y-m-d H:i:s');
|
||||
|
||||
return User::create($data);
|
||||
}
|
||||
|
||||
public function update(int $id, array $data): bool
|
||||
{
|
||||
$data['updated_at'] = date('Y-m-d H:i:s');
|
||||
|
||||
return User::find($id)?->update($data) ?? false;
|
||||
}
|
||||
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
return User::find($id)?->delete() ?? false;
|
||||
}
|
||||
|
||||
public function count(): int
|
||||
{
|
||||
return User::count();
|
||||
}
|
||||
|
||||
public function countActive(): int
|
||||
{
|
||||
return User::where('status', 1)->count();
|
||||
}
|
||||
|
||||
public function search(string $keyword, int $page = 1, int $pageSize = 10): array
|
||||
{
|
||||
$offset = ($page - 1) * $pageSize;
|
||||
|
||||
$users = User::where('username', 'LIKE', "%{$keyword}%")
|
||||
->orWhere('email', 'LIKE', "%{$keyword}%")
|
||||
->orderBy('created_at', 'desc')
|
||||
->limit($pageSize)
|
||||
->offset($offset)
|
||||
->get();
|
||||
|
||||
$total = User::where('username', 'LIKE', "%{$keyword}%")
|
||||
->orWhere('email', 'LIKE', "%{$keyword}%")
|
||||
->count();
|
||||
|
||||
return [
|
||||
'users' => $users,
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'pageSize' => $pageSize,
|
||||
'totalPages' => ceil($total / $pageSize)
|
||||
];
|
||||
}
|
||||
}
|
||||
298
app/Dto/ApiResponseDto.php
Normal file
298
app/Dto/ApiResponseDto.php
Normal file
@@ -0,0 +1,298 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
/**
|
||||
* API响应数据传输对象
|
||||
*/
|
||||
class ApiResponseDto extends BaseDto
|
||||
{
|
||||
private int $code = 0;
|
||||
|
||||
private string $message = 'success';
|
||||
|
||||
private mixed $data = null;
|
||||
|
||||
private string $traceId = '';
|
||||
|
||||
private int $timestamp = 0;
|
||||
|
||||
private array $meta = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->timestamp = time();
|
||||
$this->traceId = \Fendx\Core\Context\Context::getTraceId();
|
||||
}
|
||||
|
||||
public function getCode(): int
|
||||
{
|
||||
return $this->code;
|
||||
}
|
||||
|
||||
public function setCode(int $code): self
|
||||
{
|
||||
$this->code = $code;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMessage(): string
|
||||
{
|
||||
return $this->message;
|
||||
}
|
||||
|
||||
public function setMessage(string $message): self
|
||||
{
|
||||
$this->message = $message;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getData(): mixed
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
public function setData(mixed $data): self
|
||||
{
|
||||
$this->data = $data;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTraceId(): string
|
||||
{
|
||||
return $this->traceId;
|
||||
}
|
||||
|
||||
public function setTraceId(string $traceId): self
|
||||
{
|
||||
$this->traceId = $traceId;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTimestamp(): int
|
||||
{
|
||||
return $this->timestamp;
|
||||
}
|
||||
|
||||
public function setTimestamp(int $timestamp): self
|
||||
{
|
||||
$this->timestamp = $timestamp;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getMeta(): array
|
||||
{
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
public function setMeta(array $meta): self
|
||||
{
|
||||
$this->meta = $meta;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加元数据
|
||||
*/
|
||||
public function addMeta(string $key, mixed $value): self
|
||||
{
|
||||
$this->meta[$key] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成功响应
|
||||
*/
|
||||
public static function success(mixed $data = null, string $message = 'success'): self
|
||||
{
|
||||
return (new self())
|
||||
->setCode(0)
|
||||
->setMessage($message)
|
||||
->setData($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建错误响应
|
||||
*/
|
||||
public static function error(string $message, int $code = 400, mixed $data = null): self
|
||||
{
|
||||
return (new self())
|
||||
->setCode($code)
|
||||
->setMessage($message)
|
||||
->setData($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建分页响应
|
||||
*/
|
||||
public static function paginate(array $items, int $total, int $page, int $pageSize, string $message = 'success'): self
|
||||
{
|
||||
$data = [
|
||||
'items' => $items,
|
||||
'pagination' => [
|
||||
'total' => $total,
|
||||
'page' => $page,
|
||||
'page_size' => $pageSize,
|
||||
'total_pages' => ceil($total / $pageSize),
|
||||
'has_more' => $page * $pageSize < $total,
|
||||
'has_prev' => $page > 1,
|
||||
]
|
||||
];
|
||||
|
||||
return (new self())
|
||||
->setCode(0)
|
||||
->setMessage($message)
|
||||
->setData($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建未找到响应
|
||||
*/
|
||||
public static function notFound(string $message = 'Resource not found'): self
|
||||
{
|
||||
return self::error($message, 404);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建未授权响应
|
||||
*/
|
||||
public static function unauthorized(string $message = 'Unauthorized'): self
|
||||
{
|
||||
return self::error($message, 401);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建禁止访问响应
|
||||
*/
|
||||
public static function forbidden(string $message = 'Forbidden'): self
|
||||
{
|
||||
return self::error($message, 403);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建验证错误响应
|
||||
*/
|
||||
public static function validationError(string $message = 'Validation failed', array $errors = []): self
|
||||
{
|
||||
return self::error($message, 422, $errors);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建服务器错误响应
|
||||
*/
|
||||
public static function serverError(string $message = 'Internal server error'): self
|
||||
{
|
||||
return self::error($message, 500);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为成功响应
|
||||
*/
|
||||
public function isSuccess(): bool
|
||||
{
|
||||
return $this->code === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为错误响应
|
||||
*/
|
||||
public function isError(): bool
|
||||
{
|
||||
return $this->code !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取HTTP状态码
|
||||
*/
|
||||
public function getHttpStatusCode(): int
|
||||
{
|
||||
return match ($this->code) {
|
||||
0 => 200,
|
||||
400 => 400,
|
||||
401 => 401,
|
||||
403 => 403,
|
||||
404 => 404,
|
||||
422 => 422,
|
||||
500 => 500,
|
||||
default => 200,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置分页元数据
|
||||
*/
|
||||
public function setPaginationMeta(int $total, int $page, int $pageSize): self
|
||||
{
|
||||
return $this->addMeta('total', $total)
|
||||
->addMeta('page', $page)
|
||||
->addMeta('page_size', $pageSize)
|
||||
->addMeta('total_pages', ceil($total / $pageSize));
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置时间元数据
|
||||
*/
|
||||
public function setTimeMeta(float $executionTime = null, string $timezone = null): self
|
||||
{
|
||||
if ($executionTime !== null) {
|
||||
$this->addMeta('execution_time', round($executionTime, 3));
|
||||
}
|
||||
|
||||
if ($timezone !== null) {
|
||||
$this->addMeta('timezone', $timezone);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置请求元数据
|
||||
*/
|
||||
public function setRequestMeta(string $method = null, string $path = null, string $ip = null): self
|
||||
{
|
||||
if ($method !== null) {
|
||||
$this->addMeta('method', $method);
|
||||
}
|
||||
|
||||
if ($path !== null) {
|
||||
$this->addMeta('path', $path);
|
||||
}
|
||||
|
||||
if ($ip !== null) {
|
||||
$this->addMeta('ip', $ip);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为数组(格式化输出)
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'code' => $this->code,
|
||||
'message' => $this->message,
|
||||
'data' => $this->data,
|
||||
'trace_id' => $this->traceId,
|
||||
'timestamp' => $this->timestamp,
|
||||
'meta' => empty($this->meta) ? null : $this->meta,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为HTTP响应数组
|
||||
*/
|
||||
public function toHttpResponse(): array
|
||||
{
|
||||
return [
|
||||
'status_code' => $this->getHttpStatusCode(),
|
||||
'headers' => [
|
||||
'Content-Type' => 'application/json',
|
||||
'X-Trace-Id' => $this->traceId,
|
||||
],
|
||||
'body' => $this->toArray(),
|
||||
];
|
||||
}
|
||||
}
|
||||
252
app/Dto/BaseDto.php
Normal file
252
app/Dto/BaseDto.php
Normal file
@@ -0,0 +1,252 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
/**
|
||||
* DTO基类
|
||||
* 所有数据传输对象都应该继承此类
|
||||
*/
|
||||
abstract class BaseDto
|
||||
{
|
||||
/**
|
||||
* 数组转DTO
|
||||
*/
|
||||
public static function fromArray(array $data): static
|
||||
{
|
||||
$dto = new static();
|
||||
|
||||
foreach ($data as $key => $value) {
|
||||
$method = 'set' . str_replace('_', '', ucwords($key, '_'));
|
||||
|
||||
if (method_exists($dto, $method)) {
|
||||
$dto->$method($value);
|
||||
} elseif (property_exists($dto, $key)) {
|
||||
$dto->$key = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $dto;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO转数组
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
$data = [];
|
||||
$reflection = new \ReflectionClass($this);
|
||||
|
||||
foreach ($reflection->getProperties() as $property) {
|
||||
$propertyName = $property->getName();
|
||||
$method = 'get' . str_replace('_', '', ucwords($propertyName, '_'));
|
||||
|
||||
if (method_exists($this, $method)) {
|
||||
$data[$propertyName] = $this->$method();
|
||||
} else {
|
||||
$data[$propertyName] = $this->$propertyName ?? null;
|
||||
}
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* DTO转JSON
|
||||
*/
|
||||
public function toJson(): string
|
||||
{
|
||||
return json_encode($this->toArray(), JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* JSON转DTO
|
||||
*/
|
||||
public static function fromJson(string $json): static
|
||||
{
|
||||
$data = json_decode($json, true);
|
||||
|
||||
if (!is_array($data)) {
|
||||
throw new \InvalidArgumentException('Invalid JSON data');
|
||||
}
|
||||
|
||||
return static::fromArray($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证DTO数据
|
||||
*/
|
||||
public function validate(): array
|
||||
{
|
||||
$errors = [];
|
||||
$reflection = new \ReflectionClass($this);
|
||||
|
||||
foreach ($reflection->getProperties() as $property) {
|
||||
$propertyName = $property->getName();
|
||||
$value = $this->$propertyName ?? null;
|
||||
|
||||
// 检查必填字段
|
||||
$attributes = $property->getAttributes('Required');
|
||||
if (!empty($attributes) && ($value === null || $value === '')) {
|
||||
$errors[$propertyName] = "Field {$propertyName} is required";
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查数据类型
|
||||
$type = $property->getType();
|
||||
if ($type && $value !== null) {
|
||||
$typeName = $type->getName();
|
||||
|
||||
if (!$this->validateType($value, $typeName)) {
|
||||
$errors[$propertyName] = "Field {$propertyName} must be of type {$typeName}";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证数据类型
|
||||
*/
|
||||
private function validateType(mixed $value, string $type): bool
|
||||
{
|
||||
return match ($type) {
|
||||
'int', 'integer' => is_int($value),
|
||||
'float', 'double' => is_float($value),
|
||||
'string' => is_string($value),
|
||||
'bool', 'boolean' => is_bool($value),
|
||||
'array' => is_array($value),
|
||||
'object' => is_object($value),
|
||||
default => true,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有属性
|
||||
*/
|
||||
public function getProperties(): array
|
||||
{
|
||||
$reflection = new \ReflectionClass($this);
|
||||
$properties = [];
|
||||
|
||||
foreach ($reflection->getProperties() as $property) {
|
||||
$properties[$property->getName()] = $this->{$property->getName()} ?? null;
|
||||
}
|
||||
|
||||
return $properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置属性
|
||||
*/
|
||||
public function setProperty(string $name, mixed $value): void
|
||||
{
|
||||
$method = 'set' . str_replace('_', '', ucwords($name, '_'));
|
||||
|
||||
if (method_exists($this, $method)) {
|
||||
$this->$method($value);
|
||||
} elseif (property_exists($this, $name)) {
|
||||
$this->$name = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性
|
||||
*/
|
||||
public function getProperty(string $name): mixed
|
||||
{
|
||||
$method = 'get' . str_replace('_', '', ucwords($name, '_'));
|
||||
|
||||
if (method_exists($this, $method)) {
|
||||
return $this->$method();
|
||||
}
|
||||
|
||||
return $this->$name ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查属性是否存在
|
||||
*/
|
||||
public function hasProperty(string $name): bool
|
||||
{
|
||||
return property_exists($this, $name) || method_exists($this, 'get' . str_replace('_', '', ucwords($name, '_')));
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆DTO
|
||||
*/
|
||||
public function clone(): static
|
||||
{
|
||||
return clone $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并另一个DTO
|
||||
*/
|
||||
public function merge(BaseDto $other): static
|
||||
{
|
||||
$newDto = $this->clone();
|
||||
$otherData = $other->toArray();
|
||||
|
||||
foreach ($otherData as $key => $value) {
|
||||
if ($value !== null) {
|
||||
$newDto->setProperty($key, $value);
|
||||
}
|
||||
}
|
||||
|
||||
return $newDto;
|
||||
}
|
||||
|
||||
/**
|
||||
* 魔术方法:转换为字符串
|
||||
*/
|
||||
public function __toString(): string
|
||||
{
|
||||
return $this->toJson();
|
||||
}
|
||||
|
||||
/**
|
||||
* 魔术方法:调试输出
|
||||
*/
|
||||
public function __debugInfo(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 必填字段注解
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||
class Required
|
||||
{
|
||||
public function __construct(public string $message = '') {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段长度注解
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||
class Length
|
||||
{
|
||||
public function __construct(public int $min = 0, public int $max = 255) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 字段范围注解
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||
class Range
|
||||
{
|
||||
public function __construct(public mixed $min = null, public mixed $max = null) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* 正则表达式注解
|
||||
*/
|
||||
#[\Attribute(\Attribute::TARGET_PROPERTY)]
|
||||
class Pattern
|
||||
{
|
||||
public function __construct(public string $regex) {}
|
||||
}
|
||||
440
app/Dto/CollectionDto.php
Normal file
440
app/Dto/CollectionDto.php
Normal file
@@ -0,0 +1,440 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
/**
|
||||
* 数据集合传输对象
|
||||
*/
|
||||
class CollectionDto extends BaseDto
|
||||
{
|
||||
private array $items = [];
|
||||
|
||||
private int $count = 0;
|
||||
|
||||
private array $meta = [];
|
||||
|
||||
public function __construct(array $items = [])
|
||||
{
|
||||
$this->items = $items;
|
||||
$this->count = count($items);
|
||||
}
|
||||
|
||||
public function getItems(): array
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
public function setItems(array $items): self
|
||||
{
|
||||
$this->items = $items;
|
||||
$this->count = count($items);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCount(): int
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
|
||||
public function getMeta(): array
|
||||
{
|
||||
return $this->meta;
|
||||
}
|
||||
|
||||
public function setMeta(array $meta): self
|
||||
{
|
||||
$this->meta = $meta;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加项目
|
||||
*/
|
||||
public function add(mixed $item): self
|
||||
{
|
||||
$this->items[] = $item;
|
||||
$this->count++;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加元数据
|
||||
*/
|
||||
public function addMeta(string $key, mixed $value): self
|
||||
{
|
||||
$this->meta[$key] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为空
|
||||
*/
|
||||
public function isEmpty(): bool
|
||||
{
|
||||
return $this->count === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否不为空
|
||||
*/
|
||||
public function isNotEmpty(): bool
|
||||
{
|
||||
return $this->count > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取第一个项目
|
||||
*/
|
||||
public function first(): mixed
|
||||
{
|
||||
return $this->items[0] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最后一个项目
|
||||
*/
|
||||
public function last(): mixed
|
||||
{
|
||||
return $this->items[$this->count - 1] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定索引的项目
|
||||
*/
|
||||
public function get(int $index): mixed
|
||||
{
|
||||
return $this->items[$index] ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射集合
|
||||
*/
|
||||
public function map(callable $callback): self
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->items = array_map($callback, $this->items);
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤集合
|
||||
*/
|
||||
public function filter(callable $callback): self
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->items = array_filter($this->items, $callback);
|
||||
$new->count = count($new->items);
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* 排序集合
|
||||
*/
|
||||
public function sort(callable $callback): self
|
||||
{
|
||||
$new = clone $this;
|
||||
$items = $this->items;
|
||||
usort($items, $callback);
|
||||
$new->items = $items;
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* 反转集合
|
||||
*/
|
||||
public function reverse(): self
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->items = array_reverse($this->items);
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取唯一的集合
|
||||
*/
|
||||
public function unique(): self
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->items = array_unique($this->items);
|
||||
$new->count = count($new->items);
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* 切片集合
|
||||
*/
|
||||
public function slice(int $offset, ?int $length = null): self
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->items = array_slice($this->items, $offset, $length);
|
||||
$new->count = count($new->items);
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* 限制集合大小
|
||||
*/
|
||||
public function take(int $limit): self
|
||||
{
|
||||
return $this->slice(0, $limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 跳过指定数量
|
||||
*/
|
||||
public function skip(int $count): self
|
||||
{
|
||||
return $this->slice($count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分块集合
|
||||
*/
|
||||
public function chunk(int $size): array
|
||||
{
|
||||
return array_chunk($this->items, $size);
|
||||
}
|
||||
|
||||
/**
|
||||
* 求和
|
||||
*/
|
||||
public function sum(callable|string $key = null): float
|
||||
{
|
||||
if ($key === null) {
|
||||
return array_sum($this->items);
|
||||
}
|
||||
|
||||
if (is_callable($key)) {
|
||||
return array_sum(array_map($key, $this->items));
|
||||
}
|
||||
|
||||
return array_sum(array_column($this->items, $key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 求平均值
|
||||
*/
|
||||
public function avg(callable|string $key = null): float
|
||||
{
|
||||
if ($this->isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
return $this->sum($key) / $this->count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 求最大值
|
||||
*/
|
||||
public function max(callable|string $key = null): mixed
|
||||
{
|
||||
if ($this->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($key === null) {
|
||||
return max($this->items);
|
||||
}
|
||||
|
||||
if (is_callable($key)) {
|
||||
$values = array_map($key, $this->items);
|
||||
return max($values);
|
||||
}
|
||||
|
||||
return max(array_column($this->items, $key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 求最小值
|
||||
*/
|
||||
public function min(callable|string $key = null): mixed
|
||||
{
|
||||
if ($this->isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($key === null) {
|
||||
return min($this->items);
|
||||
}
|
||||
|
||||
if (is_callable($key)) {
|
||||
$values = array_map($key, $this->items);
|
||||
return min($values);
|
||||
}
|
||||
|
||||
return min(array_column($this->items, $key));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查找项目
|
||||
*/
|
||||
public function find(callable $callback): mixed
|
||||
{
|
||||
foreach ($this->items as $item) {
|
||||
if ($callback($item)) {
|
||||
return $item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否存在项目
|
||||
*/
|
||||
public function contains(mixed $item): bool
|
||||
{
|
||||
return in_array($item, $this->items, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否包含满足条件的项目
|
||||
*/
|
||||
public function containsWhere(callable $callback): bool
|
||||
{
|
||||
return $this->find($callback) !== null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有键
|
||||
*/
|
||||
public function keys(): array
|
||||
{
|
||||
return array_keys($this->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有值
|
||||
*/
|
||||
public function values(): array
|
||||
{
|
||||
return array_values($this->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* 合并其他集合
|
||||
*/
|
||||
public function merge(self $other): self
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->items = array_merge($this->items, $other->getItems());
|
||||
$new->count = count($new->items);
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为分页对象
|
||||
*/
|
||||
public function toPagination(int $page = 1, int $pageSize = 10): PaginationDto
|
||||
{
|
||||
$offset = ($page - 1) * $pageSize;
|
||||
$items = array_slice($this->items, $offset, $pageSize);
|
||||
|
||||
return PaginationDto::create($items, $this->count, $page, $pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建空集合
|
||||
*/
|
||||
public static function empty(): self
|
||||
{
|
||||
return new self([]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从数组创建集合
|
||||
*/
|
||||
public static function fromArray(array $items): self
|
||||
{
|
||||
return new self($items);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建包含单个项目的集合
|
||||
*/
|
||||
public static function of(mixed $item): self
|
||||
{
|
||||
return new self([$item]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建范围集合
|
||||
*/
|
||||
public static function range(int $start, int $end): self
|
||||
{
|
||||
return new self(range($start, $end));
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为数组
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'items' => $this->items,
|
||||
'count' => $this->count,
|
||||
'meta' => empty($this->meta) ? null : $this->meta,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为JSON
|
||||
*/
|
||||
public function toJson(): string
|
||||
{
|
||||
return json_encode($this->toArray(), JSON_UNESCAPED_UNICODE);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现ArrayAccess接口
|
||||
*/
|
||||
public function offsetExists(mixed $offset): bool
|
||||
{
|
||||
return isset($this->items[$offset]);
|
||||
}
|
||||
|
||||
public function offsetGet(mixed $offset): mixed
|
||||
{
|
||||
return $this->items[$offset] ?? null;
|
||||
}
|
||||
|
||||
public function offsetSet(mixed $offset, mixed $value): void
|
||||
{
|
||||
if ($offset === null) {
|
||||
$this->items[] = $value;
|
||||
} else {
|
||||
$this->items[$offset] = $value;
|
||||
}
|
||||
$this->count = count($this->items);
|
||||
}
|
||||
|
||||
public function offsetUnset(mixed $offset): void
|
||||
{
|
||||
unset($this->items[$offset]);
|
||||
$this->count = count($this->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现Countable接口
|
||||
*/
|
||||
public function count(): int
|
||||
{
|
||||
return $this->count;
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现IteratorAggregate接口
|
||||
*/
|
||||
public function getIterator(): \ArrayIterator
|
||||
{
|
||||
return new \ArrayIterator($this->items);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现JsonSerializable接口
|
||||
*/
|
||||
public function jsonSerialize(): array
|
||||
{
|
||||
return $this->toArray();
|
||||
}
|
||||
}
|
||||
362
app/Dto/PaginationDto.php
Normal file
362
app/Dto/PaginationDto.php
Normal file
@@ -0,0 +1,362 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
/**
|
||||
* 分页数据传输对象
|
||||
*/
|
||||
class PaginationDto extends BaseDto
|
||||
{
|
||||
private array $items = [];
|
||||
|
||||
#[Required]
|
||||
private int $total = 0;
|
||||
|
||||
#[Required]
|
||||
private int $page = 1;
|
||||
|
||||
#[Required]
|
||||
private int $pageSize = 10;
|
||||
|
||||
private ?int $totalPages = null;
|
||||
|
||||
private ?bool $hasMore = null;
|
||||
|
||||
private ?bool $hasPrev = null;
|
||||
|
||||
private ?bool $hasNext = null;
|
||||
|
||||
private ?int $from = null;
|
||||
|
||||
private ?int $to = null;
|
||||
|
||||
private array $filters = [];
|
||||
|
||||
private array $sorts = [];
|
||||
|
||||
public function getItems(): array
|
||||
{
|
||||
return $this->items;
|
||||
}
|
||||
|
||||
public function setItems(array $items): self
|
||||
{
|
||||
$this->items = $items;
|
||||
$this->calculateDerivedValues();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTotal(): int
|
||||
{
|
||||
return $this->total;
|
||||
}
|
||||
|
||||
public function setTotal(int $total): self
|
||||
{
|
||||
$this->total = $total;
|
||||
$this->calculateDerivedValues();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPage(): int
|
||||
{
|
||||
return $this->page;
|
||||
}
|
||||
|
||||
public function setPage(int $page): self
|
||||
{
|
||||
$this->page = max(1, $page);
|
||||
$this->calculateDerivedValues();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPageSize(): int
|
||||
{
|
||||
return $this->pageSize;
|
||||
}
|
||||
|
||||
public function setPageSize(int $pageSize): self
|
||||
{
|
||||
$this->pageSize = max(1, $pageSize);
|
||||
$this->calculateDerivedValues();
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getTotalPages(): ?int
|
||||
{
|
||||
return $this->totalPages;
|
||||
}
|
||||
|
||||
public function getHasMore(): ?bool
|
||||
{
|
||||
return $this->hasMore;
|
||||
}
|
||||
|
||||
public function getHasPrev(): ?bool
|
||||
{
|
||||
return $this->hasPrev;
|
||||
}
|
||||
|
||||
public function getHasNext(): ?bool
|
||||
{
|
||||
return $this->hasNext;
|
||||
}
|
||||
|
||||
public function getFrom(): ?int
|
||||
{
|
||||
return $this->from;
|
||||
}
|
||||
|
||||
public function getTo(): ?int
|
||||
{
|
||||
return $this->to;
|
||||
}
|
||||
|
||||
public function getFilters(): array
|
||||
{
|
||||
return $this->filters;
|
||||
}
|
||||
|
||||
public function setFilters(array $filters): self
|
||||
{
|
||||
$this->filters = $filters;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getSorts(): array
|
||||
{
|
||||
return $this->sorts;
|
||||
}
|
||||
|
||||
public function setSorts(array $sorts): self
|
||||
{
|
||||
$this->sorts = $sorts;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加过滤器
|
||||
*/
|
||||
public function addFilter(string $key, mixed $value): self
|
||||
{
|
||||
$this->filters[$key] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加排序
|
||||
*/
|
||||
public function addSort(string $field, string $direction = 'asc'): self
|
||||
{
|
||||
$this->sorts[$field] = strtolower($direction);
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算派生值
|
||||
*/
|
||||
private function calculateDerivedValues(): void
|
||||
{
|
||||
$this->totalPages = $this->pageSize > 0 ? (int) ceil($this->total / $this->pageSize) : 0;
|
||||
$this->hasMore = $this->page * $this->pageSize < $this->total;
|
||||
$this->hasPrev = $this->page > 1;
|
||||
$this->hasNext = $this->page < $this->totalPages;
|
||||
$this->from = $this->total > 0 ? (($this->page - 1) * $this->pageSize) + 1 : null;
|
||||
$this->to = min($this->page * $this->pageSize, $this->total);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取偏移量
|
||||
*/
|
||||
public function getOffset(): int
|
||||
{
|
||||
return ($this->page - 1) * $this->pageSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取限制数量
|
||||
*/
|
||||
public function getLimit(): int
|
||||
{
|
||||
return $this->pageSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为第一页
|
||||
*/
|
||||
public function isFirstPage(): bool
|
||||
{
|
||||
return $this->page === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为最后一页
|
||||
*/
|
||||
public function isLastPage(): bool
|
||||
{
|
||||
return !$this->hasNext;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上一页页码
|
||||
*/
|
||||
public function getPrevPage(): ?int
|
||||
{
|
||||
return $this->hasPrev ? $this->page - 1 : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一页页码
|
||||
*/
|
||||
public function getNextPage(): ?int
|
||||
{
|
||||
return $this->hasNext ? $this->page + 1 : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建分页对象
|
||||
*/
|
||||
public static function create(array $items, int $total, int $page = 1, int $pageSize = 10): self
|
||||
{
|
||||
return (new self())
|
||||
->setItems($items)
|
||||
->setTotal($total)
|
||||
->setPage($page)
|
||||
->setPageSize($pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* 从查询结果创建分页对象
|
||||
*/
|
||||
public static function fromQuery(array $items, int $total, array $params = []): self
|
||||
{
|
||||
$page = (int) ($params['page'] ?? 1);
|
||||
$pageSize = (int) ($params['page_size'] ?? $params['limit'] ?? 10);
|
||||
$filters = $params['filters'] ?? [];
|
||||
$sorts = $params['sorts'] ?? [];
|
||||
|
||||
return (new self())
|
||||
->setItems($items)
|
||||
->setTotal($total)
|
||||
->setPage($page)
|
||||
->setPageSize($pageSize)
|
||||
->setFilters($filters)
|
||||
->setSorts($sorts);
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为数组
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'items' => $this->items,
|
||||
'pagination' => [
|
||||
'total' => $this->total,
|
||||
'page' => $this->page,
|
||||
'page_size' => $this->pageSize,
|
||||
'total_pages' => $this->totalPages,
|
||||
'has_more' => $this->hasMore,
|
||||
'has_prev' => $this->hasPrev,
|
||||
'has_next' => $this->hasNext,
|
||||
'from' => $this->from,
|
||||
'to' => $this->to,
|
||||
'prev_page' => $this->getPrevPage(),
|
||||
'next_page' => $this->getNextPage(),
|
||||
],
|
||||
'filters' => empty($this->filters) ? null : $this->filters,
|
||||
'sorts' => empty($this->sorts) ? null : $this->sorts,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取分页信息数组
|
||||
*/
|
||||
public function getPaginationInfo(): array
|
||||
{
|
||||
return [
|
||||
'total' => $this->total,
|
||||
'page' => $this->page,
|
||||
'page_size' => $this->pageSize,
|
||||
'total_pages' => $this->totalPages,
|
||||
'has_more' => $this->hasMore,
|
||||
'has_prev' => $this->hasPrev,
|
||||
'has_next' => $this->hasNext,
|
||||
'from' => $this->from,
|
||||
'to' => $this->to,
|
||||
'prev_page' => $this->getPrevPage(),
|
||||
'next_page' => $this->getNextPage(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取SQL LIMIT子句
|
||||
*/
|
||||
public function getSqlLimit(): string
|
||||
{
|
||||
return "LIMIT {$this->pageSize} OFFSET " . $this->getOffset();
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证分页参数
|
||||
*/
|
||||
public function validate(): array
|
||||
{
|
||||
$errors = parent::validate();
|
||||
|
||||
if ($this->page < 1) {
|
||||
$errors['page'] = 'Page must be greater than 0';
|
||||
}
|
||||
|
||||
if ($this->pageSize < 1 || $this->pageSize > 1000) {
|
||||
$errors['page_size'] = 'Page size must be between 1 and 1000';
|
||||
}
|
||||
|
||||
if ($this->total < 0) {
|
||||
$errors['total'] = 'Total must be greater than or equal to 0';
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆分页对象,修改页码
|
||||
*/
|
||||
public function withPage(int $page): self
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->setPage($page);
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆分页对象,修改页大小
|
||||
*/
|
||||
public function withPageSize(int $pageSize): self
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->setPageSize($pageSize);
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆分页对象,修改过滤器
|
||||
*/
|
||||
public function withFilters(array $filters): self
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->setFilters($filters);
|
||||
return $new;
|
||||
}
|
||||
|
||||
/**
|
||||
* 克隆分页对象,修改排序
|
||||
*/
|
||||
public function withSorts(array $sorts): self
|
||||
{
|
||||
$new = clone $this;
|
||||
$new->setSorts($sorts);
|
||||
return $new;
|
||||
}
|
||||
}
|
||||
324
app/Dto/UserDto.php
Normal file
324
app/Dto/UserDto.php
Normal file
@@ -0,0 +1,324 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Dto;
|
||||
|
||||
/**
|
||||
* 用户数据传输对象
|
||||
*/
|
||||
class UserDto extends BaseDto
|
||||
{
|
||||
#[Required]
|
||||
private ?int $id = null;
|
||||
|
||||
#[Required]
|
||||
#[Length(min: 2, max: 50)]
|
||||
private string $username = '';
|
||||
|
||||
#[Required]
|
||||
#[Length(min: 6, max: 100)]
|
||||
private string $email = '';
|
||||
|
||||
#[Required]
|
||||
#[Length(min: 6, max: 255)]
|
||||
private string $password = '';
|
||||
|
||||
#[Length(max: 100)]
|
||||
private string $nickname = '';
|
||||
|
||||
#[Length(max: 20)]
|
||||
private string $phone = '';
|
||||
|
||||
#[Length(max: 255)]
|
||||
private string $avatar = '';
|
||||
|
||||
private ?int $status = null;
|
||||
|
||||
private ?int $roleId = null;
|
||||
|
||||
private ?string $roleName = '';
|
||||
|
||||
private ?\DateTime $createdAt = null;
|
||||
|
||||
private ?\DateTime $updatedAt = null;
|
||||
|
||||
private ?\DateTime $lastLoginAt = null;
|
||||
|
||||
private array $permissions = [];
|
||||
|
||||
private array $roles = [];
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(int $id): self
|
||||
{
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function setUsername(string $username): self
|
||||
{
|
||||
$this->username = $username;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEmail(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(string $email): self
|
||||
{
|
||||
$this->email = $email;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPassword(): string
|
||||
{
|
||||
return $this->password;
|
||||
}
|
||||
|
||||
public function setPassword(string $password): self
|
||||
{
|
||||
$this->password = $password;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNickname(): string
|
||||
{
|
||||
return $this->nickname;
|
||||
}
|
||||
|
||||
public function setNickname(string $nickname): self
|
||||
{
|
||||
$this->nickname = $nickname;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPhone(): string
|
||||
{
|
||||
return $this->phone;
|
||||
}
|
||||
|
||||
public function setPhone(string $phone): self
|
||||
{
|
||||
$this->phone = $phone;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAvatar(): string
|
||||
{
|
||||
return $this->avatar;
|
||||
}
|
||||
|
||||
public function setAvatar(string $avatar): self
|
||||
{
|
||||
$this->avatar = $avatar;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): ?int
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(int $status): self
|
||||
{
|
||||
$this->status = $status;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRoleId(): ?int
|
||||
{
|
||||
return $this->roleId;
|
||||
}
|
||||
|
||||
public function setRoleId(int $roleId): self
|
||||
{
|
||||
$this->roleId = $roleId;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRoleName(): ?string
|
||||
{
|
||||
return $this->roleName;
|
||||
}
|
||||
|
||||
public function setRoleName(string $roleName): self
|
||||
{
|
||||
$this->roleName = $roleName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): ?\DateTime
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setCreatedAt(\DateTime $createdAt): self
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): ?\DateTime
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
public function setUpdatedAt(\DateTime $updatedAt): self
|
||||
{
|
||||
$this->updatedAt = $updatedAt;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLastLoginAt(): ?\DateTime
|
||||
{
|
||||
return $this->lastLoginAt;
|
||||
}
|
||||
|
||||
public function setLastLoginAt(\DateTime $lastLoginAt): self
|
||||
{
|
||||
$this->lastLoginAt = $lastLoginAt;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPermissions(): array
|
||||
{
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
public function setPermissions(array $permissions): self
|
||||
{
|
||||
$this->permissions = $permissions;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRoles(): array
|
||||
{
|
||||
return $this->roles;
|
||||
}
|
||||
|
||||
public function setRoles(array $roles): self
|
||||
{
|
||||
$this->roles = $roles;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加权限
|
||||
*/
|
||||
public function addPermission(string $permission): self
|
||||
{
|
||||
if (!in_array($permission, $this->permissions)) {
|
||||
$this->permissions[] = $permission;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加角色
|
||||
*/
|
||||
public function addRole(string $role): self
|
||||
{
|
||||
if (!in_array($role, $this->roles)) {
|
||||
$this->roles[] = $role;
|
||||
}
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有指定权限
|
||||
*/
|
||||
public function hasPermission(string $permission): bool
|
||||
{
|
||||
return in_array($permission, $this->permissions);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否有指定角色
|
||||
*/
|
||||
public function hasRole(string $role): bool
|
||||
{
|
||||
return in_array($role, $this->roles);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用于API响应的数据(隐藏敏感信息)
|
||||
*/
|
||||
public function toApiResponse(): array
|
||||
{
|
||||
$data = $this->toArray();
|
||||
|
||||
// 移除敏感信息
|
||||
unset($data['password']);
|
||||
|
||||
// 格式化日期
|
||||
if ($this->createdAt) {
|
||||
$data['created_at'] = $this->createdAt->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
if ($this->updatedAt) {
|
||||
$data['updated_at'] = $this->updatedAt->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
if ($this->lastLoginAt) {
|
||||
$data['last_login_at'] = $this->lastLoginAt->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用于登录的用户DTO
|
||||
*/
|
||||
public static function forLogin(string $username, string $password): self
|
||||
{
|
||||
return (new self())
|
||||
->setUsername($username)
|
||||
->setPassword($password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用于注册的用户DTO
|
||||
*/
|
||||
public static function forRegister(string $username, string $email, string $password): self
|
||||
{
|
||||
return (new self())
|
||||
->setUsername($username)
|
||||
->setEmail($email)
|
||||
->setPassword($password);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱格式
|
||||
*/
|
||||
public function validateEmail(): bool
|
||||
{
|
||||
return filter_var($this->email, FILTER_VALIDATE_EMAIL) !== false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证手机号格式
|
||||
*/
|
||||
public function validatePhone(): bool
|
||||
{
|
||||
return preg_match('/^1[3-9]\d{9}$/', $this->phone) === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户名格式
|
||||
*/
|
||||
public function validateUsername(): bool
|
||||
{
|
||||
return preg_match('/^[a-zA-Z0-9_]{2,50}$/', $this->username) === 1;
|
||||
}
|
||||
}
|
||||
66
app/Entity/User.php
Normal file
66
app/Entity/User.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Fendx\Db\Annotation\Table;
|
||||
use Fendx\Db\Annotation\Id;
|
||||
use Fendx\Db\Annotation\Column;
|
||||
use Fendx\Db\ORM\Model;
|
||||
|
||||
#[Table('users')]
|
||||
class User extends Model
|
||||
{
|
||||
#[Id]
|
||||
public int $id;
|
||||
|
||||
#[Column('username')]
|
||||
public string $username;
|
||||
|
||||
#[Column('email')]
|
||||
public string $email;
|
||||
|
||||
#[Column('password')]
|
||||
public string $password;
|
||||
|
||||
#[Column('status', 'tinyint')]
|
||||
public int $status = 1;
|
||||
|
||||
#[Column('created_at', 'datetime')]
|
||||
public string $createdAt;
|
||||
|
||||
#[Column('updated_at', 'datetime')]
|
||||
public string $updatedAt;
|
||||
|
||||
public function getCreatedAt(): string
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setCreatedAt(string $createdAt): void
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): string
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
public function setUpdatedAt(string $updatedAt): void
|
||||
{
|
||||
$this->updatedAt = $updatedAt;
|
||||
}
|
||||
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->status === 1;
|
||||
}
|
||||
|
||||
public function toArray(): array
|
||||
{
|
||||
$data = parent::toArray();
|
||||
unset($data['password']); // 不返回密码
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
163
app/Interceptor/AuthInterceptor.php
Normal file
163
app/Interceptor/AuthInterceptor.php
Normal file
@@ -0,0 +1,163 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Interceptor;
|
||||
|
||||
use Fendx\Core\Aop\JoinPoint;
|
||||
use Fendx\Core\Aop\Advice\AroundAdvice;
|
||||
use Fendx\Web\Request\Request;
|
||||
use Fendx\Web\Response\HttpResponse;
|
||||
use Fendx\Security\Auth\JwtManager;
|
||||
|
||||
/**
|
||||
* 认证拦截器
|
||||
*/
|
||||
class AuthInterceptor implements AroundAdvice
|
||||
{
|
||||
private JwtManager $jwtManager;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->jwtManager = new JwtManager([
|
||||
'secret_key' => config('jwt.secret_key', 'your-secret-key'),
|
||||
'algorithm' => config('jwt.algorithm', 'HS256'),
|
||||
'expires_in' => config('jwt.expires_in', 3600),
|
||||
]);
|
||||
}
|
||||
|
||||
public function invoke(JoinPoint $joinPoint): mixed
|
||||
{
|
||||
$request = $this->getRequest();
|
||||
|
||||
// 检查是否需要认证
|
||||
if ($this->isPublicRoute($request)) {
|
||||
return $joinPoint->proceed();
|
||||
}
|
||||
|
||||
// 获取令牌
|
||||
$token = $this->extractToken($request);
|
||||
|
||||
if (!$token) {
|
||||
return $this->unauthorizedResponse('Missing authentication token');
|
||||
}
|
||||
|
||||
// 验证令牌
|
||||
try {
|
||||
$payload = $this->jwtManager->validateToken($token);
|
||||
} catch (\Exception $e) {
|
||||
return $this->unauthorizedResponse('Invalid or expired token');
|
||||
}
|
||||
|
||||
// 设置用户信息到上下文
|
||||
$this->setUserContext($payload);
|
||||
|
||||
return $joinPoint->proceed();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前请求对象
|
||||
*/
|
||||
private function getRequest(): Request
|
||||
{
|
||||
return Request::createFromGlobals();
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否为公开路由
|
||||
*/
|
||||
private function isPublicRoute(Request $request): bool
|
||||
{
|
||||
$path = $request->path();
|
||||
$method = $request->method();
|
||||
|
||||
$publicRoutes = [
|
||||
// 登录相关
|
||||
'POST:/api/auth/login',
|
||||
'POST:/api/auth/register',
|
||||
'POST:/api/auth/refresh',
|
||||
|
||||
// 公开API
|
||||
'GET:/api/health',
|
||||
'GET:/api/version',
|
||||
|
||||
// 静态资源
|
||||
'GET:/',
|
||||
'GET:/favicon.ico',
|
||||
];
|
||||
|
||||
$currentRoute = "{$method}:{$path}";
|
||||
|
||||
// 精确匹配
|
||||
if (in_array($currentRoute, $publicRoutes)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 模糊匹配
|
||||
foreach ($publicRoutes as $route) {
|
||||
if (str_ends_with($route, '*')) {
|
||||
$prefix = substr($route, 0, -1);
|
||||
if (str_starts_with($currentRoute, $prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求中提取令牌
|
||||
*/
|
||||
private function extractToken(Request $request): ?string
|
||||
{
|
||||
// 从Authorization头获取
|
||||
$authHeader = $request->header('Authorization');
|
||||
if ($authHeader) {
|
||||
$token = $this->jwtManager->extractTokenFromHeader($authHeader);
|
||||
if ($token) {
|
||||
return $token;
|
||||
}
|
||||
}
|
||||
|
||||
// 从Cookie获取
|
||||
$token = $request->cookie('token');
|
||||
if ($token) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
// 从查询参数获取(不推荐,仅用于调试)
|
||||
$token = $request->get('token');
|
||||
if ($token) {
|
||||
return $token;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置用户上下文
|
||||
*/
|
||||
private function setUserContext(array $payload): void
|
||||
{
|
||||
// 这里应该设置到全局上下文中
|
||||
// Context::set('user_id', $payload['user_id']);
|
||||
// Context::set('username', $payload['username']);
|
||||
// Context::set('roles', $payload['roles'] ?? []);
|
||||
// Context::set('permissions', $payload['permissions'] ?? []);
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回未授权响应
|
||||
*/
|
||||
private function unauthorizedResponse(string $message): HttpResponse
|
||||
{
|
||||
return (new HttpResponse())
|
||||
->setStatusCode(401)
|
||||
->json([
|
||||
'code' => 401,
|
||||
'message' => $message,
|
||||
'data' => null,
|
||||
'trace_id' => \Fendx\Core\Context\Context::getTraceId(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
197
app/Interceptor/LogInterceptor.php
Normal file
197
app/Interceptor/LogInterceptor.php
Normal file
@@ -0,0 +1,197 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Interceptor;
|
||||
|
||||
use Fendx\Core\Aop\JoinPoint;
|
||||
use Fendx\Core\Aop\Advice\AroundAdvice;
|
||||
|
||||
/**
|
||||
* 日志拦截器
|
||||
*/
|
||||
class LogInterceptor implements AroundAdvice
|
||||
{
|
||||
public function invoke(JoinPoint $joinPoint): mixed
|
||||
{
|
||||
$startTime = microtime(true);
|
||||
$traceId = \Fendx\Core\Context\Context::getTraceId();
|
||||
|
||||
// 记录请求开始
|
||||
$this->logRequestStart($joinPoint, $traceId);
|
||||
|
||||
try {
|
||||
// 执行目标方法
|
||||
$result = $joinPoint->proceed();
|
||||
|
||||
// 记录请求成功
|
||||
$this->logRequestSuccess($joinPoint, $result, $startTime, $traceId);
|
||||
|
||||
return $result;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
// 记录请求异常
|
||||
$this->logRequestException($joinPoint, $e, $startTime, $traceId);
|
||||
|
||||
throw $e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录请求开始
|
||||
*/
|
||||
private function logRequestStart(JoinPoint $joinPoint, string $traceId): void
|
||||
{
|
||||
$request = $this->getRequest();
|
||||
$method = $joinPoint->getMethod();
|
||||
$class = $joinPoint->getTarget()::class;
|
||||
|
||||
$logData = [
|
||||
'trace_id' => $traceId,
|
||||
'event' => 'request_start',
|
||||
'class' => $class,
|
||||
'method' => $method,
|
||||
'request_uri' => $request->uri(),
|
||||
'request_method' => $request->method(),
|
||||
'client_ip' => $request->getClientIp(),
|
||||
'user_agent' => $request->userAgent(),
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
// 记录请求参数(排除敏感信息)
|
||||
$params = $this->filterSensitiveData($request->all());
|
||||
if (!empty($params)) {
|
||||
$logData['request_params'] = $params;
|
||||
}
|
||||
|
||||
$this->writeLog('INFO', 'Request started', $logData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录请求成功
|
||||
*/
|
||||
private function logRequestSuccess(JoinPoint $joinPoint, mixed $result, float $startTime, string $traceId): void
|
||||
{
|
||||
$executionTime = round((microtime(true) - $startTime) * 1000, 2);
|
||||
$method = $joinPoint->getMethod();
|
||||
$class = $joinPoint->getTarget()::class;
|
||||
|
||||
$logData = [
|
||||
'trace_id' => $traceId,
|
||||
'event' => 'request_success',
|
||||
'class' => $class,
|
||||
'method' => $method,
|
||||
'execution_time' => $executionTime,
|
||||
'memory_usage' => memory_get_usage(true),
|
||||
'memory_peak' => memory_get_peak_usage(true),
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
// 记录响应数据(排除敏感信息)
|
||||
if ($result !== null && !is_resource($result)) {
|
||||
$responseData = $this->filterSensitiveData($result);
|
||||
if (is_array($responseData) && count($responseData) > 100) {
|
||||
$logData['response_size'] = count($responseData);
|
||||
$logData['response_type'] = gettype($result);
|
||||
} else {
|
||||
$logData['response_data'] = $responseData;
|
||||
}
|
||||
}
|
||||
|
||||
// 性能警告
|
||||
if ($executionTime > 1000) {
|
||||
$logData['performance_warning'] = 'Slow request detected';
|
||||
$this->writeLog('WARNING', 'Slow request completed', $logData);
|
||||
} else {
|
||||
$this->writeLog('INFO', 'Request completed successfully', $logData);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录请求异常
|
||||
*/
|
||||
private function logRequestException(JoinPoint $joinPoint, \Exception $e, float $startTime, string $traceId): void
|
||||
{
|
||||
$executionTime = round((microtime(true) - $startTime) * 1000, 2);
|
||||
$method = $joinPoint->getMethod();
|
||||
$class = $joinPoint->getTarget()::class;
|
||||
|
||||
$logData = [
|
||||
'trace_id' => $traceId,
|
||||
'event' => 'request_exception',
|
||||
'class' => $class,
|
||||
'method' => $method,
|
||||
'execution_time' => $executionTime,
|
||||
'exception_type' => get_class($e),
|
||||
'exception_code' => $e->getCode(),
|
||||
'exception_message' => $e->getMessage(),
|
||||
'exception_file' => $e->getFile(),
|
||||
'exception_line' => $e->getLine(),
|
||||
'stack_trace' => $e->getTraceAsString(),
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
];
|
||||
|
||||
$this->writeLog('ERROR', 'Request failed with exception', $logData);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前请求对象
|
||||
*/
|
||||
private function getRequest(): \Fendx\Web\Request\Request
|
||||
{
|
||||
return \Fendx\Web\Request\Request::createFromGlobals();
|
||||
}
|
||||
|
||||
/**
|
||||
* 过滤敏感数据
|
||||
*/
|
||||
private function filterSensitiveData(mixed $data): mixed
|
||||
{
|
||||
if (!is_array($data)) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$sensitiveFields = [
|
||||
'password', 'passwd', 'secret', 'token', 'key',
|
||||
'credit_card', 'bank_account', 'ssn', 'id_card'
|
||||
];
|
||||
|
||||
$filtered = [];
|
||||
foreach ($data as $key => $value) {
|
||||
if (in_array(strtolower($key), $sensitiveFields)) {
|
||||
$filtered[$key] = '***';
|
||||
} elseif (is_array($value)) {
|
||||
$filtered[$key] = $this->filterSensitiveData($value);
|
||||
} else {
|
||||
$filtered[$key] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
return $filtered;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入日志
|
||||
*/
|
||||
private function writeLog(string $level, string $message, array $data): void
|
||||
{
|
||||
$logEntry = [
|
||||
'level' => $level,
|
||||
'message' => $message,
|
||||
'data' => $data,
|
||||
];
|
||||
|
||||
// 这里应该调用实际的日志系统
|
||||
// Log::write($level, $message, $data);
|
||||
|
||||
// 临时实现:写入文件
|
||||
$logFile = runtime_path('logs/app_' . date('Y-m-d') . '.log');
|
||||
$logLine = date('Y-m-d H:i:s') . " [{$level}] {$message} " . json_encode($logEntry, JSON_UNESCAPED_UNICODE) . PHP_EOL;
|
||||
|
||||
$logDir = dirname($logFile);
|
||||
if (!is_dir($logDir)) {
|
||||
mkdir($logDir, 0755, true);
|
||||
}
|
||||
|
||||
file_put_contents($logFile, $logLine, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
}
|
||||
102
app/Job/CleanupJob.php
Normal file
102
app/Job/CleanupJob.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Job;
|
||||
|
||||
use Fendx\Core\Annotation\Service;
|
||||
use Fendx\Job\Annotation\Scheduled;
|
||||
use Fendx\Log\Logger;
|
||||
|
||||
#[Service]
|
||||
class CleanupJob
|
||||
{
|
||||
#[Scheduled('0 2 * * *', 'Daily cleanup at 2 AM')]
|
||||
public function dailyCleanup(): void
|
||||
{
|
||||
Logger::info('Starting daily cleanup job');
|
||||
|
||||
// 清理过期日志文件
|
||||
$this->cleanupOldLogs();
|
||||
|
||||
// 清理临时文件
|
||||
$this->cleanupTempFiles();
|
||||
|
||||
// 清理过期缓存
|
||||
$this->cleanupExpiredCache();
|
||||
|
||||
Logger::info('Daily cleanup job completed');
|
||||
}
|
||||
|
||||
#[Scheduled('*/5 * * * *', 'Every 5 minutes')]
|
||||
public function healthCheck(): void
|
||||
{
|
||||
// 检查系统健康状态
|
||||
$healthy = $this->checkSystemHealth();
|
||||
|
||||
if (!$healthy) {
|
||||
Logger::warning('System health check failed');
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupOldLogs(): void
|
||||
{
|
||||
$logDir = dirname(__DIR__, 2) . '/runtime/logs';
|
||||
$maxAge = 30 * 24 * 60 * 60; // 30天
|
||||
|
||||
if (is_dir($logDir)) {
|
||||
$files = glob($logDir . '/*.log');
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (filemtime($file) < time() - $maxAge) {
|
||||
unlink($file);
|
||||
Logger::info("Deleted old log file: $file");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupTempFiles(): void
|
||||
{
|
||||
$tempDir = dirname(__DIR__, 2) . '/runtime/temp';
|
||||
$maxAge = 24 * 60 * 60; // 24小时
|
||||
|
||||
if (is_dir($tempDir)) {
|
||||
$files = glob($tempDir . '/*');
|
||||
|
||||
foreach ($files as $file) {
|
||||
if (is_file($file) && filemtime($file) < time() - $maxAge) {
|
||||
unlink($file);
|
||||
Logger::info("Deleted temp file: $file");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function cleanupExpiredCache(): void
|
||||
{
|
||||
// 这里可以添加Redis缓存清理逻辑
|
||||
Logger::info('Cache cleanup completed');
|
||||
}
|
||||
|
||||
private function checkSystemHealth(): bool
|
||||
{
|
||||
// 检查数据库连接
|
||||
try {
|
||||
$pdo = new \PDO('mysql:host=127.0.0.1;dbname=fendx', 'root', '');
|
||||
$pdo->query('SELECT 1');
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查Redis连接
|
||||
try {
|
||||
$redis = new \Redis();
|
||||
$redis->connect('127.0.0.1', 6379);
|
||||
$redis->ping();
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
198
app/Service/UserService.php
Normal file
198
app/Service/UserService.php
Normal file
@@ -0,0 +1,198 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Service;
|
||||
|
||||
use Fendx\Core\Annotation\Service;
|
||||
use Fendx\Core\Annotation\Inject;
|
||||
use Fendx\Db\Annotation\Transactional;
|
||||
use Fendx\Cache\Annotation\Cacheable;
|
||||
use Fendx\Cache\Annotation\CacheUpdate;
|
||||
use Fendx\Cache\Annotation\CacheEvict;
|
||||
use App\Dao\UserDao;
|
||||
use App\Entity\User;
|
||||
use Fendx\Web\Validator\Validator;
|
||||
use Fendx\Common\Exception\BusinessException;
|
||||
|
||||
#[Service]
|
||||
class UserService
|
||||
{
|
||||
#[Inject]
|
||||
private UserDao $userDao;
|
||||
|
||||
#[Cacheable(key: "user:{id}", ttl: 3600)]
|
||||
public function getUser(int $id): ?User
|
||||
{
|
||||
return $this->userDao->findById($id);
|
||||
}
|
||||
|
||||
public function getUserByEmail(string $email): ?User
|
||||
{
|
||||
return $this->userDao->findByEmail($email);
|
||||
}
|
||||
|
||||
public function getUserByUsername(string $username): ?User
|
||||
{
|
||||
return $this->userDao->findByUsername($username);
|
||||
}
|
||||
|
||||
#[Cacheable(key: "users:active", ttl: 1800)]
|
||||
public function getActiveUsers(): array
|
||||
{
|
||||
return $this->userDao->findAllActive();
|
||||
}
|
||||
|
||||
public function getUsersPaginated(int $page = 1, int $pageSize = 10): array
|
||||
{
|
||||
return $this->userDao->findPaginated($page, $pageSize);
|
||||
}
|
||||
|
||||
#[Transactional]
|
||||
#[CacheUpdate(key: "user:{id}")]
|
||||
public function createUser(array $data): User
|
||||
{
|
||||
// 验证数据
|
||||
$this->validateUserData($data);
|
||||
|
||||
// 检查邮箱和用户名是否已存在
|
||||
if ($this->userDao->findByEmail($data['email'])) {
|
||||
throw new BusinessException(400, 'Email already exists');
|
||||
}
|
||||
|
||||
if ($this->userDao->findByUsername($data['username'])) {
|
||||
throw new BusinessException(400, 'Username already exists');
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
|
||||
return $this->userDao->create($data);
|
||||
}
|
||||
|
||||
#[Transactional]
|
||||
#[CacheUpdate(key: "user:{id}")]
|
||||
public function updateUser(int $id, array $data): bool
|
||||
{
|
||||
$user = $this->userDao->findById($id);
|
||||
if (!$user) {
|
||||
throw new BusinessException(404, 'User not found');
|
||||
}
|
||||
|
||||
// 如果更新邮箱,检查是否已存在
|
||||
if (isset($data['email']) && $data['email'] !== $user->email) {
|
||||
if ($this->userDao->findByEmail($data['email'])) {
|
||||
throw new BusinessException(400, 'Email already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// 如果更新用户名,检查是否已存在
|
||||
if (isset($data['username']) && $data['username'] !== $user->username) {
|
||||
if ($this->userDao->findByUsername($data['username'])) {
|
||||
throw new BusinessException(400, 'Username already exists');
|
||||
}
|
||||
}
|
||||
|
||||
// 如果更新密码,需要加密
|
||||
if (isset($data['password'])) {
|
||||
$data['password'] = password_hash($data['password'], PASSWORD_DEFAULT);
|
||||
}
|
||||
|
||||
return $this->userDao->update($id, $data);
|
||||
}
|
||||
|
||||
#[Transactional]
|
||||
#[CacheEvict(key: "user:{id}")]
|
||||
public function deleteUser(int $id): bool
|
||||
{
|
||||
$user = $this->userDao->findById($id);
|
||||
if (!$user) {
|
||||
throw new BusinessException(404, 'User not found');
|
||||
}
|
||||
|
||||
return $this->userDao->delete($id);
|
||||
}
|
||||
|
||||
public function searchUsers(string $keyword, int $page = 1, int $pageSize = 10): array
|
||||
{
|
||||
return $this->userDao->search($keyword, $page, $pageSize);
|
||||
}
|
||||
|
||||
public function getUsersCount(): int
|
||||
{
|
||||
return $this->userDao->count();
|
||||
}
|
||||
|
||||
public function getActiveUsersCount(): int
|
||||
{
|
||||
return $this->userDao->countActive();
|
||||
}
|
||||
|
||||
public function validatePassword(string $password, string $hash): bool
|
||||
{
|
||||
return password_verify($password, $hash);
|
||||
}
|
||||
|
||||
public function changePassword(int $userId, string $oldPassword, string $newPassword): bool
|
||||
{
|
||||
$user = $this->userDao->findById($userId);
|
||||
if (!$user) {
|
||||
throw new BusinessException(404, 'User not found');
|
||||
}
|
||||
|
||||
if (!$this->validatePassword($oldPassword, $user->password)) {
|
||||
throw new BusinessException(400, 'Invalid old password');
|
||||
}
|
||||
|
||||
return $this->updateUser($userId, ['password' => $newPassword]);
|
||||
}
|
||||
|
||||
public function toggleUserStatus(int $id): bool
|
||||
{
|
||||
$user = $this->userDao->findById($id);
|
||||
if (!$user) {
|
||||
throw new BusinessException(404, 'User not found');
|
||||
}
|
||||
|
||||
$newStatus = $user->status === 1 ? 0 : 1;
|
||||
|
||||
return $this->updateUser($id, ['status' => $newStatus]);
|
||||
}
|
||||
|
||||
private function validateUserData(array $data): void
|
||||
{
|
||||
$validator = Validator::make($data, [
|
||||
'username' => 'required|min:3|max:50',
|
||||
'email' => 'required|email',
|
||||
'password' => 'required|min:6'
|
||||
]);
|
||||
|
||||
if (!$validator->validate()) {
|
||||
throw new BusinessException(422, 'Validation failed', $validator->errors());
|
||||
}
|
||||
}
|
||||
|
||||
public function validateUserUpdateData(array $data): void
|
||||
{
|
||||
$rules = [];
|
||||
|
||||
if (isset($data['username'])) {
|
||||
$rules['username'] = 'required|min:3|max:50';
|
||||
}
|
||||
|
||||
if (isset($data['email'])) {
|
||||
$rules['email'] = 'required|email';
|
||||
}
|
||||
|
||||
if (isset($data['password'])) {
|
||||
$rules['password'] = 'required|min:6';
|
||||
}
|
||||
|
||||
if (!empty($rules)) {
|
||||
$validator = Validator::make($data, $rules);
|
||||
|
||||
if (!$validator->validate()) {
|
||||
throw new BusinessException(422, 'Validation failed', $validator->errors());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
426
app/Validate/BaseValidator.php
Normal file
426
app/Validate/BaseValidator.php
Normal file
@@ -0,0 +1,426 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Validate;
|
||||
|
||||
use Fendx\Web\Request\Request;
|
||||
|
||||
/**
|
||||
* 验证器基类
|
||||
*/
|
||||
abstract class BaseValidator
|
||||
{
|
||||
/**
|
||||
* 验证规则
|
||||
*/
|
||||
abstract protected function getRules(): array;
|
||||
|
||||
/**
|
||||
* 验证请求数据
|
||||
*/
|
||||
public function validate(Request $request): array
|
||||
{
|
||||
return $this->validateData($request->all(), $this->getRules());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证数组数据
|
||||
*/
|
||||
public function validateData(array $data, array $rules): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
foreach ($rules as $field => $rule) {
|
||||
$value = $data[$field] ?? null;
|
||||
$fieldErrors = $this->validateField($field, $value, $rule, $data);
|
||||
|
||||
if (!empty($fieldErrors)) {
|
||||
$errors[$field] = $fieldErrors;
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证单个字段
|
||||
*/
|
||||
protected function validateField(string $field, mixed $value, array $rule, array $data): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
// 必填验证
|
||||
if ($rule['required'] && ($value === null || $value === '')) {
|
||||
$errors[] = $rule['message'] ?? "字段 {$field} 必填";
|
||||
return $errors;
|
||||
}
|
||||
|
||||
// 如果字段不是必填且为空,跳过其他验证
|
||||
if (!$rule['required'] && ($value === null || $value === '')) {
|
||||
return $errors;
|
||||
}
|
||||
|
||||
// 类型验证
|
||||
if (isset($rule['type'])) {
|
||||
$typeError = $this->validateType($field, $value, $rule['type']);
|
||||
if ($typeError) {
|
||||
$errors[] = $typeError;
|
||||
}
|
||||
}
|
||||
|
||||
// 长度验证
|
||||
if (isset($rule['min'])) {
|
||||
$minError = $this->validateMin($field, $value, $rule['min']);
|
||||
if ($minError) {
|
||||
$errors[] = $minError;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($rule['max'])) {
|
||||
$maxError = $this->validateMax($field, $value, $rule['max']);
|
||||
if ($maxError) {
|
||||
$errors[] = $maxError;
|
||||
}
|
||||
}
|
||||
|
||||
// 数值范围验证
|
||||
if (isset($rule['min_value'])) {
|
||||
$minValueError = $this->validateMinValue($field, $value, $rule['min_value']);
|
||||
if ($minValueError) {
|
||||
$errors[] = $minValueError;
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($rule['max_value'])) {
|
||||
$maxValueError = $this->validateMaxValue($field, $value, $rule['max_value']);
|
||||
if ($maxValueError) {
|
||||
$errors[] = $maxValueError;
|
||||
}
|
||||
}
|
||||
|
||||
// 正则表达式验证
|
||||
if (isset($rule['pattern'])) {
|
||||
$patternError = $this->validatePattern($field, $value, $rule['pattern']);
|
||||
if ($patternError) {
|
||||
$errors[] = $patternError;
|
||||
}
|
||||
}
|
||||
|
||||
// 枚举值验证
|
||||
if (isset($rule['enum'])) {
|
||||
$enumError = $this->validateEnum($field, $value, $rule['enum']);
|
||||
if ($enumError) {
|
||||
$errors[] = $enumError;
|
||||
}
|
||||
}
|
||||
|
||||
// 邮箱验证
|
||||
if (isset($rule['email']) && $rule['email']) {
|
||||
$emailError = $this->validateEmail($field, $value);
|
||||
if ($emailError) {
|
||||
$errors[] = $emailError;
|
||||
}
|
||||
}
|
||||
|
||||
// URL验证
|
||||
if (isset($rule['url']) && $rule['url']) {
|
||||
$urlError = $this->validateUrl($field, $value);
|
||||
if ($urlError) {
|
||||
$errors[] = $urlError;
|
||||
}
|
||||
}
|
||||
|
||||
// 日期验证
|
||||
if (isset($rule['date']) && $rule['date']) {
|
||||
$dateError = $this->validateDate($field, $value, $rule['format'] ?? 'Y-m-d');
|
||||
if ($dateError) {
|
||||
$errors[] = $dateError;
|
||||
}
|
||||
}
|
||||
|
||||
// 自定义验证
|
||||
if (isset($rule['custom']) && is_callable($rule['custom'])) {
|
||||
$customError = $rule['custom']($value, $data, $field);
|
||||
if ($customError) {
|
||||
$errors[] = $customError;
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 类型验证
|
||||
*/
|
||||
protected function validateType(string $field, mixed $value, string $type): ?string
|
||||
{
|
||||
switch ($type) {
|
||||
case 'int':
|
||||
case 'integer':
|
||||
if (!is_int($value) && !ctype_digit((string)$value)) {
|
||||
return "字段 {$field} 必须是整数";
|
||||
}
|
||||
break;
|
||||
case 'float':
|
||||
case 'double':
|
||||
if (!is_float($value) && !is_numeric($value)) {
|
||||
return "字段 {$field} 必须是数字";
|
||||
}
|
||||
break;
|
||||
case 'string':
|
||||
if (!is_string($value)) {
|
||||
return "字段 {$field} 必须是字符串";
|
||||
}
|
||||
break;
|
||||
case 'bool':
|
||||
case 'boolean':
|
||||
if (!is_bool($value) && !in_array($value, [0, 1, '0', '1', 'true', 'false'])) {
|
||||
return "字段 {$field} 必须是布尔值";
|
||||
}
|
||||
break;
|
||||
case 'array':
|
||||
if (!is_array($value)) {
|
||||
return "字段 {$field} 必须是数组";
|
||||
}
|
||||
break;
|
||||
case 'date':
|
||||
if (!($value instanceof \DateTime) && !strtotime($value)) {
|
||||
return "字段 {$field} 必须是有效日期";
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 最小长度验证
|
||||
*/
|
||||
protected function validateMin(string $field, mixed $value, int $min): ?string
|
||||
{
|
||||
$length = is_array($value) ? count($value) : strlen((string)$value);
|
||||
if ($length < $min) {
|
||||
return "字段 {$field} 长度不能少于 {$min}";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 最大长度验证
|
||||
*/
|
||||
protected function validateMax(string $field, mixed $value, int $max): ?string
|
||||
{
|
||||
$length = is_array($value) ? count($value) : strlen((string)$value);
|
||||
if ($length > $max) {
|
||||
return "字段 {$field} 长度不能超过 {$max}";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 最小值验证
|
||||
*/
|
||||
protected function validateMinValue(string $field, mixed $value, int|float $min): ?string
|
||||
{
|
||||
if (!is_numeric($value) || $value < $min) {
|
||||
return "字段 {$field} 值不能小于 {$min}";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 最大值验证
|
||||
*/
|
||||
protected function validateMaxValue(string $field, mixed $value, int|float $max): ?string
|
||||
{
|
||||
if (!is_numeric($value) || $value > $max) {
|
||||
return "字段 {$field} 值不能大于 {$max}";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 正则表达式验证
|
||||
*/
|
||||
protected function validatePattern(string $field, mixed $value, string $pattern): ?string
|
||||
{
|
||||
if (!preg_match($pattern, (string)$value)) {
|
||||
return "字段 {$field} 格式不正确";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 枚举值验证
|
||||
*/
|
||||
protected function validateEnum(string $field, mixed $value, array $enum): ?string
|
||||
{
|
||||
if (!in_array($value, $enum)) {
|
||||
return "字段 {$field} 值必须是: " . implode(', ', $enum);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮箱验证
|
||||
*/
|
||||
protected function validateEmail(string $field, mixed $value): ?string
|
||||
{
|
||||
if (!filter_var($value, FILTER_VALIDATE_EMAIL)) {
|
||||
return "字段 {$field} 必须是有效邮箱地址";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* URL验证
|
||||
*/
|
||||
protected function validateUrl(string $field, mixed $value): ?string
|
||||
{
|
||||
if (!filter_var($value, FILTER_VALIDATE_URL)) {
|
||||
return "字段 {$field} 必须是有效URL";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 日期验证
|
||||
*/
|
||||
protected function validateDate(string $field, mixed $value, string $format): ?string
|
||||
{
|
||||
if ($value instanceof \DateTime) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$date = \DateTime::createFromFormat($format, $value);
|
||||
if (!$date || $date->format($format) !== $value) {
|
||||
return "字段 {$field} 必须是有效日期,格式: {$format}";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建验证规则
|
||||
*/
|
||||
protected function rule(string $type = 'string', bool $required = false): array
|
||||
{
|
||||
return [
|
||||
'type' => $type,
|
||||
'required' => $required
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加必填规则
|
||||
*/
|
||||
protected function required(string $message = null): array
|
||||
{
|
||||
return [
|
||||
'required' => true,
|
||||
'message' => $message
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加长度规则
|
||||
*/
|
||||
protected function length(int $min = null, int $max = null, string $message = null): array
|
||||
{
|
||||
$rule = [];
|
||||
if ($min !== null) {
|
||||
$rule['min'] = $min;
|
||||
}
|
||||
if ($max !== null) {
|
||||
$rule['max'] = $max;
|
||||
}
|
||||
if ($message) {
|
||||
$rule['message'] = $message;
|
||||
}
|
||||
return $rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加数值范围规则
|
||||
*/
|
||||
protected function range(int|float $min = null, int|float $max = null, string $message = null): array
|
||||
{
|
||||
$rule = [];
|
||||
if ($min !== null) {
|
||||
$rule['min_value'] = $min;
|
||||
}
|
||||
if ($max !== null) {
|
||||
$rule['max_value'] = $max;
|
||||
}
|
||||
if ($message) {
|
||||
$rule['message'] = $message;
|
||||
}
|
||||
return $rule;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加正则表达式规则
|
||||
*/
|
||||
protected function pattern(string $regex, string $message = null): array
|
||||
{
|
||||
return [
|
||||
'pattern' => $regex,
|
||||
'message' => $message ?? '格式不正确'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加枚举规则
|
||||
*/
|
||||
protected function enum(array $values, string $message = null): array
|
||||
{
|
||||
return [
|
||||
'enum' => $values,
|
||||
'message' => $message ?? '值必须在指定范围内'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加邮箱规则
|
||||
*/
|
||||
protected function email(string $message = null): array
|
||||
{
|
||||
return [
|
||||
'email' => true,
|
||||
'message' => $message ?? '必须是有效邮箱地址'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加URL规则
|
||||
*/
|
||||
protected function url(string $message = null): array
|
||||
{
|
||||
return [
|
||||
'url' => true,
|
||||
'message' => $message ?? '必须是有效URL'
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加日期规则
|
||||
*/
|
||||
protected function date(string $format = 'Y-m-d', string $message = null): array
|
||||
{
|
||||
return [
|
||||
'date' => true,
|
||||
'format' => $format,
|
||||
'message' => $message ?? "必须是有效日期,格式: {$format}"
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加自定义验证规则
|
||||
*/
|
||||
protected function custom(callable $callback, string $message = null): array
|
||||
{
|
||||
return [
|
||||
'custom' => $callback,
|
||||
'message' => $message
|
||||
];
|
||||
}
|
||||
}
|
||||
303
app/Validate/UserValidator.php
Normal file
303
app/Validate/UserValidator.php
Normal file
@@ -0,0 +1,303 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Validate;
|
||||
|
||||
use Fendx\Web\Request\Request;
|
||||
|
||||
/**
|
||||
* 用户验证器
|
||||
*/
|
||||
class UserValidator
|
||||
{
|
||||
/**
|
||||
* 注册验证规则
|
||||
*/
|
||||
public function getRegisterRules(): array
|
||||
{
|
||||
return [
|
||||
'username' => [
|
||||
'required' => true,
|
||||
'min' => 2,
|
||||
'max' => 50,
|
||||
'pattern' => '/^[a-zA-Z0-9_]+$/',
|
||||
'message' => '用户名必填,2-50位,只能包含字母、数字、下划线'
|
||||
],
|
||||
'email' => [
|
||||
'required' => true,
|
||||
'max' => 100,
|
||||
'pattern' => '/^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/',
|
||||
'message' => '邮箱格式不正确'
|
||||
],
|
||||
'password' => [
|
||||
'required' => true,
|
||||
'min' => 6,
|
||||
'max' => 255,
|
||||
'pattern' => '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{6,}$/',
|
||||
'message' => '密码至少6位,必须包含大小写字母和数字'
|
||||
],
|
||||
'nickname' => [
|
||||
'required' => false,
|
||||
'max' => 100,
|
||||
'message' => '昵称最多100位'
|
||||
],
|
||||
'phone' => [
|
||||
'required' => false,
|
||||
'pattern' => '/^1[3-9]\d{9}$/',
|
||||
'message' => '手机号格式不正确'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录验证规则
|
||||
*/
|
||||
public function getLoginRules(): array
|
||||
{
|
||||
return [
|
||||
'username' => [
|
||||
'required' => true,
|
||||
'message' => '用户名必填'
|
||||
],
|
||||
'password' => [
|
||||
'required' => true,
|
||||
'message' => '密码必填'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新验证规则
|
||||
*/
|
||||
public function getUpdateRules(): array
|
||||
{
|
||||
return [
|
||||
'nickname' => [
|
||||
'required' => false,
|
||||
'max' => 100,
|
||||
'message' => '昵称最多100位'
|
||||
],
|
||||
'phone' => [
|
||||
'required' => false,
|
||||
'pattern' => '/^1[3-9]\d{9}$/',
|
||||
'message' => '手机号格式不正确'
|
||||
],
|
||||
'avatar' => [
|
||||
'required' => false,
|
||||
'max' => 255,
|
||||
'message' => '头像URL最多255位'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码修改验证规则
|
||||
*/
|
||||
public function getPasswordRules(): array
|
||||
{
|
||||
return [
|
||||
'old_password' => [
|
||||
'required' => true,
|
||||
'message' => '原密码必填'
|
||||
],
|
||||
'new_password' => [
|
||||
'required' => true,
|
||||
'min' => 6,
|
||||
'max' => 255,
|
||||
'pattern' => '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z\d@$!%*?&]{6,}$/',
|
||||
'message' => '新密码至少6位,必须包含大小写字母和数字'
|
||||
],
|
||||
'confirm_password' => [
|
||||
'required' => true,
|
||||
'message' => '确认密码必填'
|
||||
]
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证注册数据
|
||||
*/
|
||||
public function validateRegister(Request $request): array
|
||||
{
|
||||
return $this->validate($request->all(), $this->getRegisterRules());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证登录数据
|
||||
*/
|
||||
public function validateLogin(Request $request): array
|
||||
{
|
||||
return $this->validate($request->all(), $this->getLoginRules());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证更新数据
|
||||
*/
|
||||
public function validateUpdate(Request $request): array
|
||||
{
|
||||
return $this->validate($request->all(), $this->getUpdateRules());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证密码修改数据
|
||||
*/
|
||||
public function validatePassword(Request $request): array
|
||||
{
|
||||
$data = $request->all();
|
||||
$errors = $this->validate($data, $this->getPasswordRules());
|
||||
|
||||
// 验证确认密码
|
||||
if (!isset($errors['new_password']) && !isset($errors['confirm_password'])) {
|
||||
if ($data['new_password'] !== $data['confirm_password']) {
|
||||
$errors['confirm_password'] = '两次输入的密码不一致';
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用验证方法
|
||||
*/
|
||||
private function validate(array $data, array $rules): array
|
||||
{
|
||||
$errors = [];
|
||||
|
||||
foreach ($rules as $field => $rule) {
|
||||
$value = $data[$field] ?? null;
|
||||
|
||||
// 必填验证
|
||||
if ($rule['required'] && ($value === null || $value === '')) {
|
||||
$errors[$field] = $rule['message'] ?? "字段 {$field} 必填";
|
||||
continue;
|
||||
}
|
||||
|
||||
// 如果字段不是必填且为空,跳过其他验证
|
||||
if (!$rule['required'] && ($value === null || $value === '')) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 最小长度验证
|
||||
if (isset($rule['min']) && strlen($value) < $rule['min']) {
|
||||
$errors[$field] = $rule['message'] ?? "字段 {$field} 长度不能少于 {$rule['min']} 位";
|
||||
}
|
||||
|
||||
// 最大长度验证
|
||||
if (isset($rule['max']) && strlen($value) > $rule['max']) {
|
||||
$errors[$field] = $rule['message'] ?? "字段 {$field} 长度不能超过 {$rule['max']} 位";
|
||||
}
|
||||
|
||||
// 正则表达式验证
|
||||
if (isset($rule['pattern']) && !preg_match($rule['pattern'], $value)) {
|
||||
$errors[$field] = $rule['message'] ?? "字段 {$field} 格式不正确";
|
||||
}
|
||||
|
||||
// 自定义验证方法
|
||||
if (isset($rule['custom']) && is_callable($rule['custom'])) {
|
||||
$customError = $rule['custom']($value, $data);
|
||||
if ($customError) {
|
||||
$errors[$field] = $customError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户名唯一性
|
||||
*/
|
||||
public function validateUniqueUsername(string $username, ?int $excludeId = null): ?string
|
||||
{
|
||||
// 这里应该调用UserService检查用户名唯一性
|
||||
// $userService = app(UserService::class);
|
||||
// if ($userService->existsByUsername($username, $excludeId)) {
|
||||
// return '用户名已存在';
|
||||
// }
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱唯一性
|
||||
*/
|
||||
public function validateUniqueEmail(string $email, ?int $excludeId = null): ?string
|
||||
{
|
||||
// 这里应该调用UserService检查邮箱唯一性
|
||||
// $userService = app(UserService::class);
|
||||
// if ($userService->existsByEmail($email, $excludeId)) {
|
||||
// return '邮箱已存在';
|
||||
// }
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证手机号唯一性
|
||||
*/
|
||||
public function validateUniquePhone(string $phone, ?int $excludeId = null): ?string
|
||||
{
|
||||
if (empty($phone)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 这里应该调用UserService检查手机号唯一性
|
||||
// $userService = app(UserService::class);
|
||||
// if ($userService->existsByPhone($phone, $excludeId)) {
|
||||
// return '手机号已存在';
|
||||
// }
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整的注册验证(包含唯一性检查)
|
||||
*/
|
||||
public function validateRegisterFull(Request $request): array
|
||||
{
|
||||
$data = $request->all();
|
||||
$errors = $this->validateRegister($request);
|
||||
|
||||
if (empty($errors)) {
|
||||
// 检查用户名唯一性
|
||||
$usernameError = $this->validateUniqueUsername($data['username']);
|
||||
if ($usernameError) {
|
||||
$errors['username'] = $usernameError;
|
||||
}
|
||||
|
||||
// 检查邮箱唯一性
|
||||
$emailError = $this->validateUniqueEmail($data['email']);
|
||||
if ($emailError) {
|
||||
$errors['email'] = $emailError;
|
||||
}
|
||||
|
||||
// 检查手机号唯一性(如果提供)
|
||||
if (!empty($data['phone'])) {
|
||||
$phoneError = $this->validateUniquePhone($data['phone']);
|
||||
if ($phoneError) {
|
||||
$errors['phone'] = $phoneError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 完整的更新验证(包含唯一性检查)
|
||||
*/
|
||||
public function validateUpdateFull(Request $request, int $userId): array
|
||||
{
|
||||
$data = $request->all();
|
||||
$errors = $this->validateUpdate($request);
|
||||
|
||||
if (empty($errors)) {
|
||||
// 检查手机号唯一性(如果提供)
|
||||
if (!empty($data['phone'])) {
|
||||
$phoneError = $this->validateUniquePhone($data['phone'], $userId);
|
||||
if ($phoneError) {
|
||||
$errors['phone'] = $phoneError;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
}
|
||||
456
app/Vo/UserVo.php
Normal file
456
app/Vo/UserVo.php
Normal file
@@ -0,0 +1,456 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Vo;
|
||||
|
||||
use App\Dto\UserDto;
|
||||
|
||||
/**
|
||||
* 用户前端展示对象
|
||||
*/
|
||||
class UserVo
|
||||
{
|
||||
private ?int $id = null;
|
||||
|
||||
private string $username = '';
|
||||
|
||||
private string $email = '';
|
||||
|
||||
private string $nickname = '';
|
||||
|
||||
private string $phone = '';
|
||||
|
||||
private string $avatar = '';
|
||||
|
||||
private ?int $status = null;
|
||||
|
||||
private ?string $statusText = null;
|
||||
|
||||
private ?string $roleName = null;
|
||||
|
||||
private ?string $createdAt = null;
|
||||
|
||||
private ?string $updatedAt = null;
|
||||
|
||||
private ?string $lastLoginAt = null;
|
||||
|
||||
private array $permissions = [];
|
||||
|
||||
private array $roles = [];
|
||||
|
||||
private array $extra = [];
|
||||
|
||||
public function __construct(UserDto $userDto = null)
|
||||
{
|
||||
if ($userDto) {
|
||||
$this->fromDto($userDto);
|
||||
}
|
||||
}
|
||||
|
||||
public function getId(): ?int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function setId(int $id): self
|
||||
{
|
||||
$this->id = $id;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
|
||||
public function setUsername(string $username): self
|
||||
{
|
||||
$this->username = $username;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getEmail(): string
|
||||
{
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
public function setEmail(string $email): self
|
||||
{
|
||||
$this->email = $email;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getNickname(): string
|
||||
{
|
||||
return $this->nickname;
|
||||
}
|
||||
|
||||
public function setNickname(string $nickname): self
|
||||
{
|
||||
$this->nickname = $nickname;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPhone(): string
|
||||
{
|
||||
return $this->phone;
|
||||
}
|
||||
|
||||
public function setPhone(string $phone): self
|
||||
{
|
||||
$this->phone = $phone;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getAvatar(): string
|
||||
{
|
||||
return $this->avatar;
|
||||
}
|
||||
|
||||
public function setAvatar(string $avatar): self
|
||||
{
|
||||
$this->avatar = $avatar;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatus(): ?int
|
||||
{
|
||||
return $this->status;
|
||||
}
|
||||
|
||||
public function setStatus(int $status): self
|
||||
{
|
||||
$this->status = $status;
|
||||
$this->statusText = $this->getStatusText($status);
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getStatusText(): ?string
|
||||
{
|
||||
return $this->statusText;
|
||||
}
|
||||
|
||||
public function getRoleName(): ?string
|
||||
{
|
||||
return $this->roleName;
|
||||
}
|
||||
|
||||
public function setRoleName(string $roleName): self
|
||||
{
|
||||
$this->roleName = $roleName;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getCreatedAt(): ?string
|
||||
{
|
||||
return $this->createdAt;
|
||||
}
|
||||
|
||||
public function setCreatedAt(string $createdAt): self
|
||||
{
|
||||
$this->createdAt = $createdAt;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getUpdatedAt(): ?string
|
||||
{
|
||||
return $this->updatedAt;
|
||||
}
|
||||
|
||||
public function setUpdatedAt(string $updatedAt): self
|
||||
{
|
||||
$this->updatedAt = $updatedAt;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getLastLoginAt(): ?string
|
||||
{
|
||||
return $this->lastLoginAt;
|
||||
}
|
||||
|
||||
public function setLastLoginAt(string $lastLoginAt): self
|
||||
{
|
||||
$this->lastLoginAt = $lastLoginAt;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getPermissions(): array
|
||||
{
|
||||
return $this->permissions;
|
||||
}
|
||||
|
||||
public function setPermissions(array $permissions): self
|
||||
{
|
||||
$this->permissions = $permissions;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getRoles(): array
|
||||
{
|
||||
return $this->roles;
|
||||
}
|
||||
|
||||
public function setRoles(array $roles): self
|
||||
{
|
||||
$this->roles = $roles;
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getExtra(): array
|
||||
{
|
||||
return $this->extra;
|
||||
}
|
||||
|
||||
public function setExtra(array $extra): self
|
||||
{
|
||||
$this->extra = $extra;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加额外信息
|
||||
*/
|
||||
public function addExtra(string $key, mixed $value): self
|
||||
{
|
||||
$this->extra[$key] = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 从DTO转换
|
||||
*/
|
||||
public function fromDto(UserDto $dto): self
|
||||
{
|
||||
$this->id = $dto->getId();
|
||||
$this->username = $dto->getUsername();
|
||||
$this->email = $dto->getEmail();
|
||||
$this->nickname = $dto->getNickname();
|
||||
$this->phone = $dto->getPhone();
|
||||
$this->avatar = $dto->getAvatar();
|
||||
$this->status = $dto->getStatus();
|
||||
$this->statusText = $this->getStatusText($this->status);
|
||||
$this->roleName = $dto->getRoleName();
|
||||
$this->permissions = $dto->getPermissions();
|
||||
$this->roles = $dto->getRoles();
|
||||
|
||||
// 格式化日期
|
||||
if ($dto->getCreatedAt()) {
|
||||
$this->createdAt = $dto->getCreatedAt()->format('Y-m-d H:i:s');
|
||||
}
|
||||
if ($dto->getUpdatedAt()) {
|
||||
$this->updatedAt = $dto->getUpdatedAt()->format('Y-m-d H:i:s');
|
||||
}
|
||||
if ($dto->getLastLoginAt()) {
|
||||
$this->lastLoginAt = $dto->getLastLoginAt()->format('Y-m-d H:i:s');
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取状态文本
|
||||
*/
|
||||
private function getStatusText(int $status): string
|
||||
{
|
||||
return match ($status) {
|
||||
1 => '正常',
|
||||
2 => '禁用',
|
||||
3 => '待审核',
|
||||
0 => '删除',
|
||||
default => '未知'
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取显示名称(优先使用昵称)
|
||||
*/
|
||||
public function getDisplayName(): string
|
||||
{
|
||||
return !empty($this->nickname) ? $this->nickname : $this->username;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取头像URL(默认头像)
|
||||
*/
|
||||
public function getAvatarUrl(): string
|
||||
{
|
||||
if (!empty($this->avatar)) {
|
||||
return $this->avatar;
|
||||
}
|
||||
|
||||
// 默认头像
|
||||
return 'https://via.placeholder.com/100x100?text=' . urlencode(substr($this->getDisplayName(), 0, 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取手机号显示格式(隐藏中间4位)
|
||||
*/
|
||||
public function getPhoneDisplay(): string
|
||||
{
|
||||
if (empty($this->phone)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return substr($this->phone, 0, 3) . '****' . substr($this->phone, -4);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取邮箱显示格式(隐藏部分字符)
|
||||
*/
|
||||
public function getEmailDisplay(): string
|
||||
{
|
||||
if (empty($this->email)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$parts = explode('@', $this->email);
|
||||
if (count($parts) !== 2) {
|
||||
return $this->email;
|
||||
}
|
||||
|
||||
$username = $parts[0];
|
||||
$domain = $parts[1];
|
||||
|
||||
if (strlen($username) <= 3) {
|
||||
$maskedUsername = str_repeat('*', strlen($username));
|
||||
} else {
|
||||
$maskedUsername = substr($username, 0, 2) . str_repeat('*', strlen($username) - 2);
|
||||
}
|
||||
|
||||
return $maskedUsername . '@' . $domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否在线
|
||||
*/
|
||||
public function isOnline(): bool
|
||||
{
|
||||
if (!$this->lastLoginAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lastLoginTime = strtotime($this->lastLoginAt);
|
||||
$onlineThreshold = 30 * 60; // 30分钟内在线
|
||||
|
||||
return (time() - $lastLoginTime) < $onlineThreshold;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否活跃用户
|
||||
*/
|
||||
public function isActive(): bool
|
||||
{
|
||||
return $this->status === 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户标签
|
||||
*/
|
||||
public function getTags(): array
|
||||
{
|
||||
$tags = [];
|
||||
|
||||
if ($this->isActive()) {
|
||||
$tags[] = ['text' => '正常', 'type' => 'success'];
|
||||
} else {
|
||||
$tags[] = ['text' => $this->statusText, 'type' => 'danger'];
|
||||
}
|
||||
|
||||
if ($this->isOnline()) {
|
||||
$tags[] = ['text' => '在线', 'type' => 'primary'];
|
||||
}
|
||||
|
||||
if (!empty($this->roleName)) {
|
||||
$tags[] = ['text' => $this->roleName, 'type' => 'info'];
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为数组(用于API响应)
|
||||
*/
|
||||
public function toArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'username' => $this->username,
|
||||
'email' => $this->email,
|
||||
'nickname' => $this->nickname,
|
||||
'phone' => $this->phone,
|
||||
'avatar' => $this->avatar,
|
||||
'status' => $this->status,
|
||||
'status_text' => $this->statusText,
|
||||
'role_name' => $this->roleName,
|
||||
'created_at' => $this->createdAt,
|
||||
'updated_at' => $this->updatedAt,
|
||||
'last_login_at' => $this->lastLoginAt,
|
||||
'permissions' => $this->permissions,
|
||||
'roles' => $this->roles,
|
||||
'extra' => $this->extra,
|
||||
// 计算属性
|
||||
'display_name' => $this->getDisplayName(),
|
||||
'avatar_url' => $this->getAvatarUrl(),
|
||||
'phone_display' => $this->getPhoneDisplay(),
|
||||
'email_display' => $this->getEmailDisplay(),
|
||||
'is_online' => $this->isOnline(),
|
||||
'is_active' => $this->isActive(),
|
||||
'tags' => $this->getTags(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为精简数组(用于列表显示)
|
||||
*/
|
||||
public function toSimpleArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'username' => $this->username,
|
||||
'nickname' => $this->nickname,
|
||||
'avatar' => $this->getAvatarUrl(),
|
||||
'status' => $this->status,
|
||||
'status_text' => $this->statusText,
|
||||
'role_name' => $this->roleName,
|
||||
'is_online' => $this->isOnline(),
|
||||
'tags' => $this->getTags(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 转换为公开数组(隐藏敏感信息)
|
||||
*/
|
||||
public function toPublicArray(): array
|
||||
{
|
||||
return [
|
||||
'id' => $this->id,
|
||||
'username' => $this->username,
|
||||
'nickname' => $this->nickname,
|
||||
'avatar' => $this->getAvatarUrl(),
|
||||
'role_name' => $this->roleName,
|
||||
'created_at' => $this->createdAt,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量转换DTO数组为VO数组
|
||||
*/
|
||||
public static function fromDtoArray(array $dtos): array
|
||||
{
|
||||
return array_map(fn($dto) => new self($dto), $dtos);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量转换为数组
|
||||
*/
|
||||
public static function toArrayBatch(array $vos, string $type = 'full'): array
|
||||
{
|
||||
return array_map(function($vo) use ($type) {
|
||||
return match ($type) {
|
||||
'simple' => $vo->toSimpleArray(),
|
||||
'public' => $vo->toPublicArray(),
|
||||
default => $vo->toArray()
|
||||
};
|
||||
}, $vos);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user