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:
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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user