init
This commit is contained in:
40
extend/Aether/PHP/Hyperf/AetherCrudInterface.php
Normal file
40
extend/Aether/PHP/Hyperf/AetherCrudInterface.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Aether;
|
||||
|
||||
interface AetherCrudInterface
|
||||
{
|
||||
/**
|
||||
* 列表
|
||||
* @return array
|
||||
*/
|
||||
public function list(): array;
|
||||
|
||||
/**
|
||||
* 查询
|
||||
* @param int $id
|
||||
* @return object
|
||||
*/
|
||||
public function detail(int $id): object;
|
||||
/**
|
||||
* 新增
|
||||
* @param array $data
|
||||
* @return int
|
||||
*/
|
||||
public function create(array $data): int;
|
||||
|
||||
/**
|
||||
* 更新
|
||||
* @param int $id
|
||||
* @param array $data
|
||||
* @return bool
|
||||
*/
|
||||
public function update(int $id, array $data): bool;
|
||||
|
||||
/**
|
||||
* 删除
|
||||
* @param int $id
|
||||
* @return bool
|
||||
*/
|
||||
public function delete(int $id): bool;
|
||||
}
|
||||
318
extend/Aether/PHP/Hyperf/AetherCrudService.php
Normal file
318
extend/Aether/PHP/Hyperf/AetherCrudService.php
Normal file
@@ -0,0 +1,318 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether;
|
||||
|
||||
use Aether\Contract\TreeableInterface;
|
||||
use Aether\Exception\BusinessException;
|
||||
use Hyperf\Database\Model\Builder;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* 抽象CRUD服务基类,封装通用逻辑
|
||||
*/
|
||||
abstract class AetherCrudService extends AetherService implements AetherCrudInterface
|
||||
{
|
||||
protected array $search = [];
|
||||
|
||||
protected function getSearch(): array
|
||||
{
|
||||
return $this->search;
|
||||
}
|
||||
|
||||
protected array $ignoreSearchFields = [];
|
||||
|
||||
protected function getIgnoreSearchFields(): array
|
||||
{
|
||||
return $this->ignoreSearchFields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前服务对应的模型实例(由子类实现)
|
||||
*/
|
||||
protected abstract function getModel(): AetherModel;
|
||||
|
||||
/**
|
||||
* 获取当前服务对应的验证器实例(由子类实现)
|
||||
*/
|
||||
protected abstract function getValidator(): AetherValidator;
|
||||
|
||||
|
||||
/**
|
||||
* 通用列表查询(支持分页和树形结构)
|
||||
*/
|
||||
public function list(array $params = []): array
|
||||
{
|
||||
$model = $this->getModel();
|
||||
$query = $model->newQuery();
|
||||
|
||||
// 通过模型配置自动应用所有搜索条件
|
||||
$this->applySearch($query, $params);
|
||||
|
||||
// 动态应用排序
|
||||
$sortConfig = $model->getSortConfig();
|
||||
if ($sortConfig) {
|
||||
$query->orderBy($sortConfig['field'], $sortConfig['direction']);
|
||||
}
|
||||
|
||||
$withDeleted = filter_var($params['with_deleted'] ?? false, FILTER_VALIDATE_BOOLEAN);
|
||||
if ($withDeleted) {
|
||||
$query->withTrashed();
|
||||
}
|
||||
|
||||
// 存在分页参数(page或size)则进行分页查询
|
||||
if (isset($params['page']) || isset($params['size'])) {
|
||||
$page = (int)($params['page'] ?? 1);
|
||||
$size = (int)($params['size'] ?? 10);
|
||||
// 确保分页参数合法性
|
||||
$page = max(1, $page);
|
||||
$size = max(1, min(100, $size)); // 限制最大页大小为100
|
||||
|
||||
$result = $query->paginate($size, ['*'], 'page', $page);
|
||||
return [
|
||||
'total' => $result->total(),
|
||||
'list' => $result->items()
|
||||
];
|
||||
}
|
||||
|
||||
// 无分页参数时返回完整数据集合
|
||||
$items = $query->get()->toArray();
|
||||
|
||||
// 若模型支持树形结构则构建树形,否则返回普通数组
|
||||
if ($model instanceof TreeableInterface) {
|
||||
return $model::buildTree($items, (int)($params['parent_id'] ?? 0));
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用详情查询
|
||||
*/
|
||||
public function detail(int $id): object
|
||||
{var_dump('detail');
|
||||
return $this->getModel()->findOrFailById($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用创建逻辑
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function create(array $data): int
|
||||
{
|
||||
// 数据验证(使用子类指定的验证器)
|
||||
$this->getValidator()->scene('create', $data)->check();
|
||||
|
||||
return $this->transaction(function () use ($data) {
|
||||
$model = $this->getModel()->createOne($data);
|
||||
$this->logger()->info('创建资源', [
|
||||
'id' => $model->id,
|
||||
'code' => $data['code'] ?? $model->code
|
||||
]);
|
||||
return $model->id;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用更新逻辑
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function update(int $id, array $data): bool
|
||||
{
|
||||
$model = $this->getModel();
|
||||
$resource = $model->findById($id);
|
||||
$this->checkResourceExists($resource);
|
||||
|
||||
// 数据验证
|
||||
$this->getValidator()->scene('update', $data)->check();
|
||||
|
||||
// 钩子:处理更新时的特殊逻辑(如禁止自身为父级)
|
||||
$this->handleUpdateSpecialLogic($id, $data);
|
||||
|
||||
return $this->transaction(function () use ($id, $data) {
|
||||
$result = $this->getModel()->updateById($id, $data);
|
||||
$this->logger()->info('更新资源', [
|
||||
'id' => $id,
|
||||
'data' => $data
|
||||
]);
|
||||
return $result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用删除逻辑
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function delete(int $id): bool
|
||||
{
|
||||
$model = $this->getModel();
|
||||
$resource = $model->findById($id);
|
||||
|
||||
$this->checkResourceExists($resource);
|
||||
|
||||
// 钩子:删除前检查(如子级存在性)
|
||||
$this->checkChildrenBeforeDelete($id);
|
||||
|
||||
return $this->transaction(function () use ($id) {
|
||||
$result = $this->getModel()->deleteById($id);
|
||||
$this->logger()->info('删除资源', ['id' => $id]);
|
||||
return $result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 钩子方法:更新时的特殊逻辑(子类可重写)
|
||||
*/
|
||||
protected function handleUpdateSpecialLogic(int $id, array &$data): void
|
||||
{
|
||||
// 通用逻辑:禁止将自身设为父级(适用于有parent_id的场景)
|
||||
if (isset($data['parent_id']) && $data['parent_id'] == $id) {
|
||||
throw new BusinessException('不能将自身设为父级', 400);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 钩子方法:删除前检查子级(子类可重写)
|
||||
*/
|
||||
protected function checkChildrenBeforeDelete(int $id): void
|
||||
{
|
||||
// 默认不检查,需要的子类重写
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型的$search配置,自动应用搜索条件到查询构建器
|
||||
*/
|
||||
public function applySearch(Builder $query, array $params): void
|
||||
{
|
||||
foreach ($this->search as $field => $rule) {
|
||||
// 跳过未传递的参数
|
||||
if (!isset($params[$field])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$value = $params[$field];
|
||||
$this->applySearchRule($query, $field, $value, $rule);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用单个搜索规则
|
||||
*/
|
||||
protected function applySearchRule(Builder $query, string $field, $value, $rule): void
|
||||
{
|
||||
// 处理规则格式(支持字符串简写或数组配置)
|
||||
$config = is_array($rule) ? $rule : ['type' => $rule];
|
||||
$type = $config['type'];
|
||||
|
||||
switch ($type) {
|
||||
case '=': // 精确匹配
|
||||
$query->where($field, $value);
|
||||
break;
|
||||
case 'like': // 模糊匹配
|
||||
$query->where($field, 'like', "%{$value}%");
|
||||
break;
|
||||
case 'between': // 范围查询(支持数组或两个参数)
|
||||
$values = is_array($value) ? $value : [$value, $params[$field . '_end'] ?? $value];
|
||||
$query->whereBetween($field, $values);
|
||||
break;
|
||||
case 'callback': // 自定义回调
|
||||
if (isset($config['handler']) && is_callable($config['handler'])) {
|
||||
call_user_func($config['handler'], $query, $value);
|
||||
}
|
||||
break;
|
||||
// 可扩展其他类型:in、>、< 等
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除恢复
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function restore(int $id): bool
|
||||
{
|
||||
$model = $this->getModel();
|
||||
// 必须使用withTrashed()才能查询到已删除记录
|
||||
$resource = $model->newQuery()->withTrashed()->find($id);
|
||||
$this->checkResourceExists($resource, '恢复的资源不存在');
|
||||
|
||||
return $this->transaction(function () use ($id) {
|
||||
$result = $this->getModel()->newQuery()->withTrashed()
|
||||
->where('id', $id)->restore();
|
||||
|
||||
$this->logger()->info('恢复软删除资源', ['id' => $id]);
|
||||
return $result;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量软删除
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function batchDelete(array $ids): bool
|
||||
{
|
||||
if (empty($ids)) {
|
||||
throw new BusinessException('请选择要删除的记录', 400);
|
||||
}
|
||||
|
||||
$model = $this->getModel();
|
||||
$exists = $model->whereIn('id', $ids)->exists();
|
||||
if (!$exists) {
|
||||
throw new BusinessException('部分记录不存在', 404);
|
||||
}
|
||||
|
||||
return $this->transaction(function () use ($ids) {
|
||||
$result = $this->getModel()->whereIn('id', $ids)->delete();
|
||||
$this->logger()->info('批量软删除资源', ['ids' => $ids]);
|
||||
return $result > 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量恢复软删除
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function batchRestore(array $ids): bool
|
||||
{
|
||||
if (empty($ids)) {
|
||||
throw new BusinessException('请选择要恢复的记录', 400);
|
||||
}
|
||||
|
||||
$model = $this->getModel();
|
||||
$exists = $model->newQuery()->withTrashed()
|
||||
->whereIn('id', $ids)
|
||||
->whereNotNull('deleted_at')
|
||||
->exists();
|
||||
|
||||
if (!$exists) {
|
||||
throw new BusinessException('部分记录不存在或未被删除', 404);
|
||||
}
|
||||
|
||||
return $this->transaction(function () use ($ids) {
|
||||
$result = $this->getModel()->newQuery()->withTrashed()
|
||||
->whereIn('id', $ids)->restore();
|
||||
|
||||
$this->logger()->info('批量恢复资源', ['ids' => $ids]);
|
||||
return $result > 0;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 永久删除(物理删除)
|
||||
* @throws BusinessException|Throwable
|
||||
*/
|
||||
public function forceDelete(int $id): bool
|
||||
{
|
||||
$model = $this->getModel();
|
||||
$resource = $model->newQuery()->withTrashed()->find($id);
|
||||
$this->checkResourceExists($resource, '要删除的资源不存在');
|
||||
|
||||
return $this->transaction(function () use ($id) {
|
||||
$result = $this->getModel()->newQuery()->withTrashed()
|
||||
->where('id', $id)->forceDelete();
|
||||
|
||||
$this->logger()->info('永久删除资源', ['id' => $id]);
|
||||
return $result;
|
||||
});
|
||||
}
|
||||
}
|
||||
494
extend/Aether/PHP/Hyperf/AetherModel.php
Normal file
494
extend/Aether/PHP/Hyperf/AetherModel.php
Normal file
@@ -0,0 +1,494 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether;
|
||||
|
||||
use Closure;
|
||||
use DateTime;
|
||||
use Exception;
|
||||
use Hyperf\Context\ApplicationContext;
|
||||
use Hyperf\Contract\LengthAwarePaginatorInterface;
|
||||
use Hyperf\Database\Model\Builder;
|
||||
use Hyperf\Database\Model\Collection;
|
||||
use Hyperf\Database\Model\ModelNotFoundException;
|
||||
use Hyperf\DbConnection\Model\Model as HyperfModel;
|
||||
use Hyperf\DbConnection\Db;
|
||||
use Hyperf\HttpServer\Contract\RequestInterface;
|
||||
use Hyperf\ModelCache\Cacheable;
|
||||
use Hyperf\ModelCache\CacheableInterface;
|
||||
use Psr\Container\ContainerExceptionInterface;
|
||||
use Psr\Container\NotFoundExceptionInterface;
|
||||
use Throwable;
|
||||
|
||||
abstract class AetherModel extends HyperfModel implements CacheableInterface
|
||||
{
|
||||
use Cacheable;
|
||||
// use AetherSoftDelete; 移除,由子类决定是否使用
|
||||
|
||||
/**
|
||||
* 批量赋值白名单.
|
||||
*/
|
||||
protected array $fillable = [];
|
||||
|
||||
/**
|
||||
* 时间戳格式.
|
||||
*/
|
||||
protected ?string $dateFormat = 'Y-m-d H:i:s';
|
||||
|
||||
/**
|
||||
* 搜索器规则配置.
|
||||
*/
|
||||
protected array $search = [];
|
||||
|
||||
/**
|
||||
* 获取器规则配置.
|
||||
*/
|
||||
protected array $append = [];
|
||||
|
||||
/**
|
||||
* 隐藏字段.
|
||||
*/
|
||||
protected array $hidden = [
|
||||
'created_at',
|
||||
'updated_at',
|
||||
'deleted_at',
|
||||
// 'deleted_at' // 移除,由子类决定是否使用
|
||||
];
|
||||
|
||||
/**
|
||||
* 排序配置:
|
||||
* - false: 禁用排序
|
||||
* - 字符串: 排序字段(默认升序)
|
||||
* - 数组: ['field' => '字段名', 'direction' => 'asc/desc']
|
||||
*/
|
||||
protected string|array|bool $sortable = 'sort'; // 默认按sort字段升序
|
||||
|
||||
/**
|
||||
* 获取排序配置
|
||||
* @return array|null [field, direction] 或 null(禁用排序)
|
||||
*/
|
||||
public function getSortConfig(): ?array
|
||||
{
|
||||
if ($this->sortable === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 处理字符串配置(如 'sort' 或 'create_time')
|
||||
if (is_string($this->sortable)) {
|
||||
return [
|
||||
'field' => $this->sortable,
|
||||
'direction' => 'asc'
|
||||
];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
parent::__construct($attributes);
|
||||
$this->bootBaseModel();
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷创建.
|
||||
*/
|
||||
public static function createOne(array $data): AetherModel|Builder|HyperfModel
|
||||
{
|
||||
return static::query()->create($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷更新.
|
||||
*/
|
||||
public static function updateById(int $id, array $data): bool
|
||||
{
|
||||
return static::query()->where('id', $id)->update($data) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷删除指定ID的记录
|
||||
*
|
||||
* @param int $id 要删除的记录ID
|
||||
* @return bool 成功删除返回true
|
||||
* @throws ModelNotFoundException 当记录不存在时抛出
|
||||
* @throws Exception 当删除操作发生其他错误时抛出
|
||||
*/
|
||||
public static function deleteById(int $id): bool
|
||||
{
|
||||
if (!static::query()->where('id', $id)->exists()) {
|
||||
throw new ModelNotFoundException(sprintf(
|
||||
'找不到ID为 %d 的 %s 记录',
|
||||
$id,
|
||||
static::class
|
||||
));
|
||||
}
|
||||
return static::query()->where('id', $id)->delete() > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷查找.
|
||||
* @param int $id
|
||||
* @return Builder|Builder[]|Collection|HyperfModel
|
||||
* @throws Exception 当删除操作发生其他错误时抛出
|
||||
* @throws ModelNotFoundException
|
||||
*/
|
||||
public static function findById(int $id): array|Builder|Collection|HyperfModel
|
||||
{
|
||||
$record = static::query()->find($id);
|
||||
if (is_null($record)) {
|
||||
throw new ModelNotFoundException(sprintf('找不到 ID 为 %d 的记录', $id));
|
||||
}
|
||||
return $record;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快捷查找或失败.
|
||||
* @param int $id 要查找的记录ID
|
||||
* @return AetherModel 根据ID查找记录,不存在则抛出异常
|
||||
* 根据ID查找记录,不存在则抛出异常
|
||||
* @throws Exception 当删除操作发生其他错误时抛出
|
||||
* @throws ModelNotFoundException 当记录不存在时抛出
|
||||
*/
|
||||
public static function findOrFailById(int $id): static
|
||||
{
|
||||
$record = static::query()->find($id);
|
||||
if(is_null($record)) {
|
||||
throw new ModelNotFoundException(sprintf(
|
||||
'找不到ID为 %d 的 %s 记录',
|
||||
$id,
|
||||
static::class
|
||||
));
|
||||
}
|
||||
return $record;// static::query()->findOrFail($id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取列表.
|
||||
*/
|
||||
public static function getList(
|
||||
array $conditions = [],
|
||||
array $columns = ['*'],
|
||||
array $orders = []
|
||||
): Collection {
|
||||
$query = static::buildQuery($conditions, $orders);
|
||||
return $query->get($columns);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询列表.
|
||||
*/
|
||||
public static function getPageList(
|
||||
array $conditions = [],
|
||||
int $page = 1,
|
||||
int $pageSize = 10,
|
||||
array $columns = ['*'],
|
||||
array $orderBy = []
|
||||
): LengthAwarePaginatorInterface {
|
||||
// 直接通过静态方法链构建查询
|
||||
$query = static::query();
|
||||
|
||||
// 应用条件
|
||||
foreach ($conditions as $field => $value) {
|
||||
$query->where($field, $value);
|
||||
}
|
||||
|
||||
// 应用排序
|
||||
foreach ($orderBy as $field => $direction) {
|
||||
$query->orderBy($field, $direction);
|
||||
}
|
||||
|
||||
// 执行分页
|
||||
return $query->paginate($pageSize, $columns, 'page', $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取器处理.
|
||||
*/
|
||||
public function getAttribute(string $key): mixed
|
||||
{
|
||||
$value = parent::getAttribute($key);
|
||||
|
||||
// 检查是否有自定义获取器
|
||||
$getterMethod = 'get' . ucfirst($key) . 'Attr';
|
||||
if (method_exists($this, $getterMethod)) {
|
||||
return $this->{$getterMethod}($value);
|
||||
}
|
||||
|
||||
// 应用获取器规则
|
||||
if (isset($this->append[$key])) {
|
||||
return $this->applyGetterRule($value, $this->append[$key]);
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新.
|
||||
*/
|
||||
public static function batchUpdate(array $conditions, array $data): int
|
||||
{
|
||||
return static::query()->where($conditions)->update($data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务处理.
|
||||
* @throws Throwable
|
||||
*/
|
||||
public static function transaction(Closure $closure): mixed
|
||||
{
|
||||
return Db::transaction($closure);
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化模型.
|
||||
*/
|
||||
protected function bootBaseModel(): void
|
||||
{
|
||||
// 自动注册搜索器和获取器
|
||||
$this->registerSearchers();
|
||||
$this->registerGetters();
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册搜索器.
|
||||
*/
|
||||
protected function registerSearchers(): void
|
||||
{
|
||||
// 为模型查询添加全局作用域,自动应用搜索规则
|
||||
static::addGlobalScope('auto_search', function (Builder $query) {
|
||||
$searchConditions = $this->getSearchConditions();
|
||||
if (empty($searchConditions)) {
|
||||
return;
|
||||
}
|
||||
// 应用搜索条件到查询
|
||||
$this->applySearch($query, $searchConditions);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取搜索条件.
|
||||
* @throws ContainerExceptionInterface
|
||||
* @throws NotFoundExceptionInterface
|
||||
*/
|
||||
protected function getSearchConditions(): array
|
||||
{
|
||||
$request = ApplicationContext::getContainer()->get(RequestInterface::class);
|
||||
$allParams = $request->all();
|
||||
|
||||
if (is_array($allParams) && count($allParams) === 1 && is_array($allParams[0])) {
|
||||
$allParams = $allParams[0];
|
||||
}
|
||||
|
||||
$allowedFields = array_keys($this->search);
|
||||
return array_intersect_key($allParams, array_flip($allowedFields));
|
||||
}
|
||||
|
||||
protected function registerGetters(): void
|
||||
{
|
||||
foreach ($this->append as $field => $rule) {
|
||||
// 为字段注册获取器回调
|
||||
$this->registerGetter($field, $rule);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 为单个字段注册获取器.
|
||||
* @param string $field 字段名
|
||||
* @param array|Closure|string $rule 规则(类型/带参数的类型/闭包)
|
||||
*/
|
||||
protected function registerGetter(string $field, array|Closure|string $rule): void
|
||||
{
|
||||
// 生成获取器方法名(遵循 Hyperf 模型获取器规范:get{Field}Attr)
|
||||
$getterMethod = 'get' . ucfirst($field) . 'Attr';
|
||||
|
||||
// 若已存在自定义获取器方法,则不覆盖
|
||||
if (method_exists($this, $getterMethod)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 动态定义获取器方法
|
||||
$this->{$getterMethod} = function ($value) use ($rule) {
|
||||
// 闭包规则直接执行
|
||||
if ($rule instanceof Closure) {
|
||||
return $rule($value, $this); // 传入当前模型实例方便关联字段处理
|
||||
}
|
||||
|
||||
// 解析规则类型和参数
|
||||
if (is_array($rule)) {
|
||||
$type = $rule[0];
|
||||
$params = $rule[1] ?? [];
|
||||
} else {
|
||||
$type = $rule;
|
||||
$params = [];
|
||||
}
|
||||
|
||||
// 应用规则(复用/扩展 applyGetterRule 方法)
|
||||
return $this->applyGetterRule($value, $type, $params);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用获取器规则(支持参数).
|
||||
* @param mixed $value 字段原始值
|
||||
* @param string $type 规则类型
|
||||
* @param array $params 规则参数
|
||||
*/
|
||||
protected function applyGetterRule(mixed $value, string $type, array $params = []): mixed
|
||||
{
|
||||
return match ($type) {
|
||||
// 基础规则
|
||||
'date' => $value instanceof DateTime ? $value->format('Y-m-d') : $value,
|
||||
'datetime' => $value instanceof DateTime ? $value->format('Y-m-d H:i:s') : $value,
|
||||
'timestamp' => $value instanceof DateTime ? $value->getTimestamp() : $value,
|
||||
'boolean' => (bool) $value,
|
||||
'json' => is_string($value) ? json_decode($value, true) : $value,
|
||||
|
||||
// 参数化规则
|
||||
'number' => $this->formatNumber($value, $params), // 数字格式化
|
||||
'truncate' => $this->truncateString($value, $params), // 字符串截断
|
||||
'enum' => $this->mapEnum($value, $params), // 枚举映射
|
||||
default => $value,
|
||||
};
|
||||
}
|
||||
|
||||
// 数字格式化(示例参数化实现)
|
||||
protected function formatNumber($value, array $params): string
|
||||
{
|
||||
$precision = $params['precision'] ?? 2; // 默认保留2位小数
|
||||
return number_format((float) $value, $precision);
|
||||
}
|
||||
|
||||
// 字符串截断(示例参数化实现)
|
||||
protected function truncateString($value, array $params): string
|
||||
{
|
||||
$length = $params['length'] ?? 20; // 默认截断到20字符
|
||||
$suffix = $params['suffix'] ?? '...'; // 省略符
|
||||
if (! is_string($value)) {
|
||||
$value = (string) $value;
|
||||
}
|
||||
return mb_strlen($value) > $length
|
||||
? mb_substr($value, 0, $length) . $suffix
|
||||
: $value;
|
||||
}
|
||||
|
||||
// 枚举映射(示例参数化实现)
|
||||
protected function mapEnum($value, array $params): mixed
|
||||
{
|
||||
$map = $params['map'] ?? []; // 枚举映射表,如 [1 => '男', 2 => '女']
|
||||
return $map[$value] ?? $value;
|
||||
}
|
||||
|
||||
// 修正 buildQuery 方法,避免使用 new static()
|
||||
protected static function buildQuery(array $conditions = [], array $orderBy = []): Builder
|
||||
{
|
||||
// 使用静态 query() 方法获取查询构建器
|
||||
$query = static::query();
|
||||
|
||||
// 处理搜索条件
|
||||
foreach ($conditions as $field => $value) {
|
||||
$query->where($field, $value);
|
||||
}
|
||||
|
||||
// 处理排序
|
||||
foreach ($orderBy as $field => $direction) {
|
||||
$query->orderBy($field, $direction);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用搜索条件.
|
||||
*/
|
||||
protected function applySearch(Builder $query, array $conditions): void
|
||||
{
|
||||
foreach ($conditions as $field => $value) {
|
||||
// 跳过非字符串的字段名(防止索引数组键导致的类型错误)
|
||||
if (!is_string($field)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 处理嵌套关系查询(如:user.name)
|
||||
if (str_contains($field, '.')) {
|
||||
[$relation, $relationField] = explode('.', $field, 2);
|
||||
$query->whereHas($relation, function ($q) use ($relationField, $value) {
|
||||
$q->where($relationField, $value);
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 检查是否有自定义搜索器方法
|
||||
$searchMethod = 'search' . ucfirst($field);
|
||||
if (method_exists($this, $searchMethod)) {
|
||||
$this->{$searchMethod}($query, $value);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 应用搜索规则配置
|
||||
if (isset($this->search[$field])) {
|
||||
$this->applySearchRule($query, $field, $value, $this->search[$field]);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 默认精确匹配(仅对$search中允许的字段生效,因已通过白名单过滤)
|
||||
$query->where($field, $value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 应用搜索规则.
|
||||
*/
|
||||
protected function applySearchRule(Builder $query, string $field, mixed $value, array|string $rule): void
|
||||
{
|
||||
if (is_array($rule)) {
|
||||
$type = $rule[0];
|
||||
$params = $rule[1] ?? [];
|
||||
} else {
|
||||
$type = $rule;
|
||||
$params = [];
|
||||
}
|
||||
|
||||
switch ($type) {
|
||||
case 'like':
|
||||
$query->where($field, 'like', "%{$value}%");
|
||||
break;
|
||||
case 'like_left':
|
||||
$query->where($field, 'like', "%{$value}");
|
||||
break;
|
||||
case 'like_right':
|
||||
$query->where($field, 'like', "{$value}%");
|
||||
break;
|
||||
case 'in':
|
||||
$query->whereIn($field, (array) $value);
|
||||
break;
|
||||
case 'not_in':
|
||||
$query->whereNotIn($field, (array) $value);
|
||||
break;
|
||||
case 'gt':
|
||||
$query->where($field, '>', $value);
|
||||
break;
|
||||
case 'lt':
|
||||
$query->where($field, '<', $value);
|
||||
break;
|
||||
case 'gte':
|
||||
$query->where($field, '>=', $value);
|
||||
break;
|
||||
case 'lte':
|
||||
$query->where($field, '<=', $value);
|
||||
break;
|
||||
case 'between':
|
||||
$query->whereBetween($field, (array) $value);
|
||||
break;
|
||||
case 'null':
|
||||
$query->whereNull($field);
|
||||
break;
|
||||
case 'not_null':
|
||||
$query->whereNotNull($field);
|
||||
break;
|
||||
default:
|
||||
$query->where($field, $type, $value);
|
||||
}
|
||||
}
|
||||
}
|
||||
49
extend/Aether/PHP/Hyperf/AetherService.php
Normal file
49
extend/Aether/PHP/Hyperf/AetherService.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether;
|
||||
|
||||
use Aether\Exception\BusinessException;
|
||||
use Hyperf\Di\Annotation\Inject;
|
||||
use Hyperf\Logger\LoggerFactory;
|
||||
use Hyperf\DbConnection\Db;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Throwable;
|
||||
|
||||
abstract class AetherService
|
||||
{
|
||||
#[Inject]
|
||||
protected LoggerFactory $loggerFactory;
|
||||
|
||||
/**
|
||||
* 获取当前服务日志器
|
||||
*/
|
||||
protected function logger(): LoggerInterface
|
||||
{
|
||||
// return $this->loggerFactory->get(substr(strrchr(static::class, '\\'), 1));
|
||||
// 提取类名(如ExamTypeService)作为日志通道名
|
||||
$className = substr(strrchr(static::class, '\\'), 1);
|
||||
return $this->loggerFactory->get($className);
|
||||
}
|
||||
|
||||
/**
|
||||
* 事务处理
|
||||
* @throws Throwable
|
||||
*/
|
||||
protected function transaction(callable $callback)
|
||||
{
|
||||
return Db::transaction($callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查资源是否存在(不存在则抛出异常)
|
||||
* @throws BusinessException
|
||||
*/
|
||||
protected function checkResourceExists(?object $resource, string $message = '资源不存在'): void
|
||||
{
|
||||
if (empty($resource)) {
|
||||
throw new BusinessException($message, BusinessException::RESOURCE_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
}
|
||||
134
extend/Aether/PHP/Hyperf/AetherValidator.php
Normal file
134
extend/Aether/PHP/Hyperf/AetherValidator.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether;
|
||||
|
||||
use Aether\Exception\ValidationFailedException;
|
||||
use Hyperf\Di\Annotation\Inject;
|
||||
use Hyperf\Validation\Contract\ValidatorFactoryInterface;
|
||||
use Hyperf\Validation\Validator;
|
||||
use Hyperf\Context\ApplicationContext;
|
||||
|
||||
abstract class AetherValidator
|
||||
{
|
||||
#[Inject]
|
||||
protected ValidatorFactoryInterface $validationFactory;
|
||||
|
||||
/**
|
||||
* 当前场景名
|
||||
*/
|
||||
public ?string $currentScene = null;
|
||||
|
||||
/**
|
||||
* 待验证数据
|
||||
*/
|
||||
protected array $data = [];
|
||||
|
||||
/**
|
||||
* 自定义验证规则(子类可通过该属性注册,无需重写registerRules)
|
||||
* 格式:['规则名' => 闭包/类方法]
|
||||
*/
|
||||
protected array $customRules = [];
|
||||
|
||||
/**
|
||||
* 静态快捷验证方法(简化调用)
|
||||
*/
|
||||
public static function validate(string $scene, array $data = []): array
|
||||
{
|
||||
// return (new static())->scene($scene, $data)->check();
|
||||
// 从容器中获取当前类的实例(确保依赖注入生效)
|
||||
$instance = ApplicationContext::getContainer()->get(static::class);
|
||||
return $instance->scene($scene, $data)->check();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置验证场景和数据(支持链式调用)
|
||||
*/
|
||||
public function scene(string $scene, array $data = []): self
|
||||
{
|
||||
$this->currentScene = $scene;
|
||||
$this->data = $data;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行验证(失败抛出异常)
|
||||
*/
|
||||
public function check(): array
|
||||
{
|
||||
if (empty($this->currentScene)) {
|
||||
throw new \RuntimeException('请先设置验证场景');
|
||||
}
|
||||
|
||||
$scenes = $this->scenes();
|
||||
if (!isset($scenes[$this->currentScene])) {
|
||||
throw new \RuntimeException("验证场景不存在:{$this->currentScene}");
|
||||
}
|
||||
|
||||
$sceneConfig = $scenes[$this->currentScene];
|
||||
return $this->validateData(
|
||||
$this->data,
|
||||
$sceneConfig['rules'],
|
||||
$sceneConfig['messages'] ?? [],
|
||||
$sceneConfig['attributes'] ?? []
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 实际执行验证的逻辑(重命名方法名更清晰)
|
||||
*/
|
||||
protected function validateData(array $data, array $rules, array $messages = [], array $attributes = []): array
|
||||
{
|
||||
$validator = $this->validationFactory->make($data, $rules, $messages, $attributes);
|
||||
$this->registerRules($validator);
|
||||
|
||||
if ($validator->fails()) {
|
||||
throw new ValidationFailedException(
|
||||
$validator,
|
||||
$this->currentScene ?? '',
|
||||
$validator->errors()->first()
|
||||
);
|
||||
}
|
||||
|
||||
return $validator->validated();
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化验证错误信息(统一格式,供异常处理器复用)
|
||||
*/
|
||||
public function formatValidationErrors(Validator $validator): array
|
||||
{
|
||||
$errors = [];
|
||||
$failedRules = $validator->failed();
|
||||
$errorMessages = $validator->errors()->getMessages();
|
||||
$attributes = $validator->attributes();
|
||||
|
||||
foreach ($failedRules as $field => $rules) {
|
||||
$errors[] = [
|
||||
'field' => $field,
|
||||
'field_label' => $attributes[$field] ?? $field,
|
||||
'message' => $errorMessages[$field][0] ?? '',
|
||||
'rules' => array_keys($rules),
|
||||
'value' => $validator->getValue($field)
|
||||
];
|
||||
}
|
||||
|
||||
return $errors;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动注册自定义规则(优先使用$customRules属性)
|
||||
*/
|
||||
protected function registerRules(Validator $validator): void
|
||||
{
|
||||
foreach ($this->customRules as $ruleName => $rule) {
|
||||
$validator->extend($ruleName, $rule);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 定义场景验证规则(子类实现)
|
||||
*/
|
||||
abstract protected function scenes(): array;
|
||||
}
|
||||
24
extend/Aether/PHP/Hyperf/Contract/TreeableInterface.php
Normal file
24
extend/Aether/PHP/Hyperf/Contract/TreeableInterface.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Aether\Contract;
|
||||
|
||||
/**
|
||||
* 树形结构接口,标识模型支持树形功能
|
||||
*/
|
||||
interface TreeableInterface
|
||||
{
|
||||
/**
|
||||
* 构建树形结构
|
||||
* @param array $items 原始数据
|
||||
* @param int $parentId 根节点ID
|
||||
* @return array
|
||||
*/
|
||||
public static function buildTree(array $items, int $parentId = 0): array;
|
||||
|
||||
/**
|
||||
* 获取子节点ID集合
|
||||
* @param int $id 节点ID
|
||||
* @return array
|
||||
*/
|
||||
public function getChildIds(int $id): array;
|
||||
}
|
||||
136
extend/Aether/PHP/Hyperf/Exception/AetherExceptionHandler.php
Normal file
136
extend/Aether/PHP/Hyperf/Exception/AetherExceptionHandler.php
Normal file
@@ -0,0 +1,136 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether\Exception;
|
||||
|
||||
use Aether\AetherValidator;
|
||||
use Hyperf\Contract\StdoutLoggerInterface;
|
||||
use Hyperf\Database\Model\ModelNotFoundException; // 引入模型未找到异常
|
||||
use Hyperf\ExceptionHandler\ExceptionHandler;
|
||||
use Hyperf\HttpMessage\Stream\SwooleStream;
|
||||
use Hyperf\Validation\ValidationException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Throwable;
|
||||
use Hyperf\Context\Context;
|
||||
use function Hyperf\Support\env;
|
||||
|
||||
class AetherExceptionHandler extends ExceptionHandler
|
||||
{
|
||||
public function __construct(protected StdoutLoggerInterface $logger)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$requestId = Context::get('request_id', uniqid());
|
||||
$result = $this->formatErrorResponse($throwable, $requestId);
|
||||
|
||||
// 记录错误日志(包含完整堆栈)
|
||||
$this->logger->error(sprintf(
|
||||
'Exception [%s] | RequestId: %s | Message: %s in %s:%d',
|
||||
get_class($throwable),
|
||||
$requestId,
|
||||
$throwable->getMessage(),
|
||||
$throwable->getFile(),
|
||||
$throwable->getLine()
|
||||
));
|
||||
if (env('APP_ENV') === 'dev') {
|
||||
$this->logger->error($throwable->getTraceAsString());
|
||||
}
|
||||
|
||||
// 确保状态码合法(100-599)
|
||||
$statusCode = $result['code'] ?? 500;
|
||||
if ($statusCode < 100 || $statusCode >= 600) {
|
||||
$statusCode = 500;
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withStatus($statusCode)
|
||||
->withBody(new SwooleStream(json_encode($result, JSON_UNESCAPED_UNICODE)));
|
||||
}
|
||||
|
||||
private function formatErrorResponse(Throwable $throwable, string $requestId): array
|
||||
{
|
||||
// 模型未找到异常
|
||||
if ($throwable instanceof ModelNotFoundException) {
|
||||
return [
|
||||
'code' => 404, // 资源不存在标准状态码
|
||||
'message' => $throwable->getMessage() ?: '请求的资源不存在',
|
||||
'data' => env('APP_ENV') === 'dev' ? [
|
||||
'file' => $throwable->getFile(),
|
||||
'line' => $throwable->getLine()
|
||||
] : null,
|
||||
'request_id' => $requestId,
|
||||
'timestamp' => time()
|
||||
];
|
||||
}
|
||||
|
||||
// 自定义验证异常
|
||||
if ($throwable instanceof ValidationFailedException) {
|
||||
return $this->formatValidationError($throwable, $requestId);
|
||||
}
|
||||
|
||||
// 原生验证异常
|
||||
if ($throwable instanceof ValidationException) {
|
||||
return $this->formatNativeValidationError($throwable, $requestId);
|
||||
}
|
||||
|
||||
// 其他异常(如RuntimeException等)
|
||||
return [
|
||||
'code' => 600,
|
||||
'message' => env('APP_ENV') === 'dev' ? $throwable->getMessage() : '服务暂时不可用',
|
||||
'data' => env('APP_ENV') === 'dev' ? [
|
||||
'file' => $throwable->getFile(),
|
||||
'line' => $throwable->getLine(),
|
||||
'trace' => explode("\n", $throwable->getTraceAsString())
|
||||
] : null,
|
||||
'request_id' => $requestId,
|
||||
'timestamp' => time()
|
||||
];
|
||||
}
|
||||
|
||||
private function formatValidationError(ValidationFailedException $e, string $requestId): array
|
||||
{
|
||||
$validatorInstance = new class extends AetherValidator {
|
||||
protected function scenes(): array { return []; }
|
||||
};
|
||||
|
||||
return [
|
||||
'code' => 422,
|
||||
'message' => $e->getMessage(),
|
||||
'data' => [
|
||||
'errors' => $validatorInstance->formatValidationErrors($e->validator),
|
||||
'scene' => $e->getScene(),
|
||||
'validated_data' => env('APP_ENV') === 'dev' ? $e->validator->getData() : null
|
||||
],
|
||||
'request_id' => $requestId,
|
||||
'timestamp' => time()
|
||||
];
|
||||
}
|
||||
|
||||
private function formatNativeValidationError(ValidationException $e, string $requestId): array
|
||||
{
|
||||
$validatorInstance = new class extends AetherValidator {
|
||||
protected function scenes(): array { return []; }
|
||||
};
|
||||
|
||||
return [
|
||||
'code' => 422,
|
||||
'message' => '参数验证失败',
|
||||
'data' => [
|
||||
'errors' => $validatorInstance->formatValidationErrors($e->validator),
|
||||
'validated_data' => env('APP_ENV') === 'dev' ? $e->validator->getData() : null
|
||||
],
|
||||
'request_id' => $requestId,
|
||||
'timestamp' => time()
|
||||
];
|
||||
}
|
||||
|
||||
public function isValid(Throwable $throwable): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
119
extend/Aether/PHP/Hyperf/Exception/AppExceptionHandler.php
Normal file
119
extend/Aether/PHP/Hyperf/Exception/AppExceptionHandler.php
Normal file
@@ -0,0 +1,119 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether\Exception;
|
||||
|
||||
use Aether\AetherValidator;
|
||||
use Hyperf\Contract\StdoutLoggerInterface;
|
||||
use Hyperf\ExceptionHandler\ExceptionHandler;
|
||||
use Hyperf\HttpMessage\Stream\SwooleStream;
|
||||
use Hyperf\Validation\ValidationException;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Throwable;
|
||||
use Hyperf\Context\Context;
|
||||
use function Hyperf\Support\env;
|
||||
|
||||
class AppExceptionHandler extends ExceptionHandler
|
||||
{
|
||||
public function __construct(protected StdoutLoggerInterface $logger)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$requestId = Context::get('request_id', uniqid());
|
||||
$result = $this->formatErrorResponse($throwable, $requestId);
|
||||
|
||||
$this->logger->error(sprintf(
|
||||
'Exception: %s[%s] in %s:%d',
|
||||
get_class($throwable),
|
||||
$throwable->getMessage(),
|
||||
$throwable->getFile(),
|
||||
$throwable->getLine()
|
||||
));
|
||||
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withStatus($result['code'] ?? 500)
|
||||
->withBody(new SwooleStream(json_encode($result, JSON_UNESCAPED_UNICODE)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 统一错误响应格式
|
||||
*/
|
||||
private function formatErrorResponse(Throwable $throwable, string $requestId): array
|
||||
{
|
||||
// 处理自定义验证异常
|
||||
if ($throwable instanceof ValidationFailedException) {
|
||||
return $this->formatValidationError($throwable, $requestId);
|
||||
}
|
||||
|
||||
// 处理原生验证异常
|
||||
if ($throwable instanceof ValidationException) {
|
||||
return $this->formatNativeValidationError($throwable, $requestId);
|
||||
}
|
||||
|
||||
// 处理其他异常
|
||||
return [
|
||||
'code' => 500,
|
||||
'message' => env('APP_ENV') === 'dev' ? $throwable->getMessage() : '服务暂时不可用',
|
||||
'data' => env('APP_ENV') === 'dev' ? [
|
||||
'file' => $throwable->getFile(),
|
||||
'line' => $throwable->getLine(),
|
||||
'trace' => explode("\n", $throwable->getTraceAsString())
|
||||
] : null,
|
||||
'request_id' => $requestId,
|
||||
'timestamp' => time()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化自定义验证异常
|
||||
*/
|
||||
private function formatValidationError(ValidationFailedException $e, string $requestId): array
|
||||
{
|
||||
// 复用AetherValidator的错误格式化方法
|
||||
$validatorInstance = new class extends AetherValidator {
|
||||
protected function scenes(): array { return []; }
|
||||
};
|
||||
|
||||
return [
|
||||
'code' => 422,
|
||||
'message' => $e->getMessage(),
|
||||
'data' => [
|
||||
'errors' => $validatorInstance->formatValidationErrors($e->validator),
|
||||
'scene' => $e->getScene(), // 直接从异常获取场景
|
||||
'validated_data' => env('APP_ENV') === 'dev' ? $e->validator->getData() : null
|
||||
],
|
||||
'request_id' => $requestId,
|
||||
'timestamp' => time()
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化原生验证异常(保持格式一致)
|
||||
*/
|
||||
private function formatNativeValidationError(ValidationException $e, string $requestId): array
|
||||
{
|
||||
$validatorInstance = new class extends AetherValidator {
|
||||
protected function scenes(): array { return []; }
|
||||
};
|
||||
|
||||
return [
|
||||
'code' => 422,
|
||||
'message' => '参数验证失败',
|
||||
'data' => [
|
||||
'errors' => $validatorInstance->formatValidationErrors($e->validator),
|
||||
'validated_data' => env('APP_ENV') === 'dev' ? $e->validator->getData() : null
|
||||
],
|
||||
'request_id' => $requestId,
|
||||
'timestamp' => time()
|
||||
];
|
||||
}
|
||||
|
||||
public function isValid(Throwable $throwable): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
40
extend/Aether/PHP/Hyperf/Exception/BusinessException.php
Normal file
40
extend/Aether/PHP/Hyperf/Exception/BusinessException.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether\Exception;
|
||||
|
||||
use Hyperf\Server\Exception\ServerException;
|
||||
|
||||
class BusinessException extends ServerException
|
||||
{
|
||||
// 错误码常量(按业务模块划分)
|
||||
public const VALIDATION_ERROR = 400; // 参数验证失败
|
||||
public const AUTH_ERROR = 401; // 认证失败
|
||||
public const PERMISSION_DENY = 403; // 权限不足
|
||||
public const RESOURCE_NOT_FOUND = 404; // 资源不存在
|
||||
public const SCENE_NOT_FOUND = 400;
|
||||
|
||||
/**
|
||||
* 额外错误数据(如验证详情)
|
||||
*/
|
||||
protected ?array $errorData = null;
|
||||
|
||||
public function __construct(
|
||||
string $message,
|
||||
int $code = 500,
|
||||
?\Throwable $previous = null,
|
||||
?array $errorData = null
|
||||
) {
|
||||
parent::__construct($message, $code, $previous);
|
||||
$this->errorData = $errorData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取额外错误数据
|
||||
*/
|
||||
public function getErrorData(): ?array
|
||||
{
|
||||
return $this->errorData;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Aether\Exception;
|
||||
|
||||
use Hyperf\Validation\ValidationException;
|
||||
use Hyperf\Validation\Validator;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
|
||||
class ValidationFailedException extends ValidationException
|
||||
{
|
||||
/**
|
||||
* 验证场景
|
||||
*/
|
||||
protected string $scene;
|
||||
|
||||
public function __construct(
|
||||
Validator $validator,
|
||||
string $scene,
|
||||
string $message = '参数验证失败',
|
||||
?ResponseInterface $response = null // 新增response参数,符合父类要求
|
||||
) {
|
||||
// 父类构造函数仅接受 $validator 和 $response
|
||||
parent::__construct($validator, $response);
|
||||
// 单独设置消息(父类的 $message 为 protected 属性,可直接赋值)
|
||||
$this->message = $message;
|
||||
$this->scene = $scene;
|
||||
}
|
||||
|
||||
public function getScene(): string
|
||||
{
|
||||
return $this->scene;
|
||||
}
|
||||
}
|
||||
159
extend/Aether/PHP/Hyperf/Traits/AetherEnum.php
Normal file
159
extend/Aether/PHP/Hyperf/Traits/AetherEnum.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether\Traits;
|
||||
|
||||
use App\Notice\Enum\NoticeStatusEnum;
|
||||
use App\Notice\Model\NoticeStatsModel;
|
||||
use InvalidArgumentException;
|
||||
use ReflectionClass;
|
||||
|
||||
trait AetherEnum
|
||||
{
|
||||
/**
|
||||
* 获取所有枚举值数组(严格保持定义顺序).
|
||||
* @return array<int|string> 枚举值集合
|
||||
*/
|
||||
public static function values(): array
|
||||
{
|
||||
self::validateEnumStructure();
|
||||
|
||||
$values = [];
|
||||
foreach (self::cases() as $case) {
|
||||
$values[] = $case->value;
|
||||
}
|
||||
return $values;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有枚举描述数组(与values()顺序一一对应).
|
||||
* @return array<string> 描述文本集合
|
||||
*/
|
||||
public static function descriptions(): array
|
||||
{
|
||||
self::validateEnumStructure();
|
||||
|
||||
$descriptions = [];
|
||||
foreach (self::cases() as $case) {
|
||||
$descriptions[] = $case->description();
|
||||
}
|
||||
return $descriptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取值-描述映射数组(用于下拉选择等场景).
|
||||
* @return array<int|string, string> 键为枚举值,值为描述文本
|
||||
*/
|
||||
public static function valueMap(): array
|
||||
{
|
||||
self::validateEnumStructure();
|
||||
|
||||
$map = [];
|
||||
foreach (self::cases() as $case) {
|
||||
$map[$case->value] = $case->description();
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据值获取枚举实例(严格模式).
|
||||
* @param int|string $value 枚举值
|
||||
* @return AetherEnum|NoticeStatsModel|NoticeStatusEnum 枚举实例
|
||||
*/
|
||||
public static function fromValue(int|string $value): self
|
||||
{
|
||||
self::validateEnumStructure();
|
||||
|
||||
// 检查值类型是否与枚举类型匹配(基于第一个case的类型)
|
||||
$firstCase = self::cases()[0] ?? null;
|
||||
if ($firstCase) {
|
||||
$expectedType = gettype($firstCase->value);
|
||||
$actualType = gettype($value);
|
||||
if ($expectedType !== $actualType) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'枚举值类型不匹配,%s期望%s类型,实际为%s',
|
||||
self::class,
|
||||
$expectedType,
|
||||
$actualType
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$enum = self::tryFrom($value);
|
||||
if (! $enum) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'无效的%s值: %s,允许值: %s',
|
||||
self::class,
|
||||
$value,
|
||||
implode(', ', self::values())
|
||||
));
|
||||
}
|
||||
return $enum;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据描述获取枚举实例(精确匹配).
|
||||
* @param string $description 描述文本
|
||||
* @return null|AetherEnum|NoticeStatsModel|NoticeStatusEnum 匹配的枚举实例,无匹配时返回null
|
||||
*/
|
||||
public static function fromDescription(string $description): ?self
|
||||
{
|
||||
self::validateEnumStructure();
|
||||
|
||||
foreach (self::cases() as $case) {
|
||||
if ($case->description() === $description) {
|
||||
return $case;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查值是否为有效的枚举值(严格类型检查).
|
||||
* @param int|string $value 待检查的值
|
||||
* @return bool 是否有效
|
||||
*/
|
||||
public static function isValidValue(int|string $value): bool
|
||||
{
|
||||
self::validateEnumStructure();
|
||||
|
||||
foreach (self::cases() as $case) {
|
||||
if ($case->value === $value) { // 严格相等,避免类型松散匹配
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验枚举结构合法性(替代__init,私有静态方法).
|
||||
*/
|
||||
private static function validateEnumStructure(): void
|
||||
{
|
||||
// 检查当前类是否为枚举
|
||||
if (! (new ReflectionClass(self::class))->isEnum()) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'AetherEnum trait仅允许枚举类使用,%s不是枚举',
|
||||
self::class
|
||||
));
|
||||
}
|
||||
|
||||
// 检查枚举是否实现了description()方法
|
||||
if (! method_exists(self::class, 'description')) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'枚举类%s必须实现description()方法(返回字符串描述)',
|
||||
self::class
|
||||
));
|
||||
}
|
||||
|
||||
// 检查description()方法返回值是否为字符串
|
||||
$sampleCase = self::cases()[0] ?? null;
|
||||
if ($sampleCase && ! is_string($sampleCase->description())) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
'枚举类%s的description()方法必须返回字符串',
|
||||
self::class
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
10
extend/Aether/PHP/Hyperf/Traits/AetherSearchable.php
Normal file
10
extend/Aether/PHP/Hyperf/Traits/AetherSearchable.php
Normal file
@@ -0,0 +1,10 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Aether\Traits;
|
||||
|
||||
trait AetherSearchable
|
||||
{
|
||||
|
||||
}
|
||||
24
extend/Aether/PHP/Hyperf/Traits/AetherSoftDelete.php
Normal file
24
extend/Aether/PHP/Hyperf/Traits/AetherSoftDelete.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Aether\Traits;
|
||||
|
||||
use Hyperf\Database\Model\SoftDeletes;
|
||||
|
||||
/**
|
||||
* 通用软删除Trait,供需要软删除的模型使用
|
||||
*/
|
||||
trait AetherSoftDelete
|
||||
{
|
||||
use SoftDeletes;
|
||||
|
||||
/**
|
||||
* 初始化软删除相关配置(自动隐藏deleted_at字段)
|
||||
*/
|
||||
protected function initializeAetherSoftDeletes(): void
|
||||
{
|
||||
// 自动将deleted_at添加到隐藏字段,避免序列化时暴露
|
||||
if (!in_array('deleted_at', $this->hidden, true)) {
|
||||
$this->hidden[] = 'deleted_at';
|
||||
}
|
||||
}
|
||||
}
|
||||
125
extend/Aether/PHP/Hyperf/Traits/AetherTree.php
Normal file
125
extend/Aether/PHP/Hyperf/Traits/AetherTree.php
Normal file
@@ -0,0 +1,125 @@
|
||||
<?php
|
||||
|
||||
namespace Aether\Traits;
|
||||
|
||||
use Aether\AetherModel;
|
||||
use Hyperf\Database\Model\Collection;
|
||||
use LogicException;
|
||||
|
||||
trait AetherTree
|
||||
{
|
||||
// 初始化时检查当前类是否继承AetherModel
|
||||
public function __construct(array $attributes = [])
|
||||
{
|
||||
parent::__construct($attributes);
|
||||
|
||||
if (!$this instanceof AetherModel) {
|
||||
throw new LogicException(
|
||||
"使用AetherTree trait的类必须继承AetherModel,当前类: " . get_class($this)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 抽象方法:获取父ID字段名(由子类实现)
|
||||
*/
|
||||
abstract protected function getParentIdField(): string;
|
||||
|
||||
/**
|
||||
* 抽象方法:获取排序字段名(由子类实现)
|
||||
*/
|
||||
abstract protected function getSortField(): string;
|
||||
|
||||
/**
|
||||
* 构建树形结构
|
||||
*/
|
||||
public static function buildTree($items, int $parentId = 0): array
|
||||
{
|
||||
$self = new static();
|
||||
$parentField = $self->getParentIdField();
|
||||
$sortField = $self->getSortField();
|
||||
|
||||
$items = $items instanceof Collection ? $items->toArray() : $items;
|
||||
$tree = [];
|
||||
|
||||
foreach ($items as $item) {
|
||||
if ($item[$parentField] == $parentId) {
|
||||
$children = static::buildTree($items, $item['id']);
|
||||
if (!empty($children)) {
|
||||
$item['children'] = $children;
|
||||
}
|
||||
$tree[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$self->sortTreeItems($tree, $sortField);
|
||||
return $tree;
|
||||
}
|
||||
|
||||
/**
|
||||
* 树形节点排序
|
||||
*/
|
||||
protected function sortTreeItems(array &$items, string $sortField): void
|
||||
{
|
||||
usort($items, function ($a, $b) use ($sortField) {
|
||||
$direction = $this->treeSortDirection ?? 'asc';
|
||||
return $direction === 'desc'
|
||||
? $b[$sortField] <=> $a[$sortField]
|
||||
: $a[$sortField] <=> $b[$sortField];
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取指定节点的所有子节点ID
|
||||
*/
|
||||
public function getChildIds(int $id): array
|
||||
{
|
||||
$parentField = $this->getParentIdField();
|
||||
// 现在可以安全调用newQuery(),因为已通过继承检查
|
||||
$allItems = $this->newQuery()->get(['id', $parentField])->toArray();
|
||||
$ids = [$id];
|
||||
|
||||
$this->collectChildIds($allItems, $id, $parentField, $ids);
|
||||
return $ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归收集子节点ID
|
||||
*/
|
||||
private function collectChildIds(array $items, int $parentId, string $parentField, array &$ids): void
|
||||
{
|
||||
foreach ($items as $item) {
|
||||
if ($item[$parentField] == $parentId) {
|
||||
$ids[] = $item['id'];
|
||||
$this->collectChildIds($items, $item['id'], $parentField, $ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取节点的完整路径
|
||||
*/
|
||||
public function getPath(int $id): array
|
||||
{
|
||||
$parentField = $this->getParentIdField();
|
||||
// 安全调用newQuery()
|
||||
$node = $this->newQuery()->find($id);
|
||||
if (!$node) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$path = [$node->toArray()];
|
||||
$parentId = $node[$parentField];
|
||||
|
||||
while ($parentId > 0) {
|
||||
$parent = $this->newQuery()->find($parentId);
|
||||
if (!$parent) {
|
||||
break;
|
||||
}
|
||||
array_unshift($path, $parent->toArray());
|
||||
$parentId = $parent[$parentField];
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user