composer包

This commit is contained in:
Aether
2025-09-30 15:27:07 +08:00
parent 9a1c120c4f
commit 1c0989d52d
34 changed files with 92 additions and 2509 deletions

View File

@@ -0,0 +1,11 @@
<?php
namespace App\Exception;
use Aether\Exception\AetherException;
class CampusNotFound extends AetherException
{
protected $code = 404001;
protected $message = '文章不存在';
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
namespace App\Exception;
use Aether\Exception\AetherException;
class TeacherNotFound extends AetherException
{
protected $code = 40400;
protected $message = '教师不存在';
}

View File

@@ -5,6 +5,8 @@ declare(strict_types=1);
namespace App\JsonRpc\Service;
use Aether\Exception\BusinessException;
use App\Exception\CampusNotFound;
use App\Exception\TeacherNotFound;
use App\Model\Campus;
use App\Model\Teacher;
use App\Validator\CampusValidator;
@@ -45,7 +47,7 @@ class DataService implements DataServiceInterface
{
$campus = $this->campusModel->find($id);
if (! $campus || $campus->status != 1) {
throw new BusinessException('校区不存在或已禁用', 10001);
throw new CampusNotFound('校区不存在或已禁用');
}
return $campus->toArray();
}
@@ -122,7 +124,7 @@ class DataService implements DataServiceInterface
{
$teacher = $this->teacherModel->find($id);
if (! $teacher || $teacher->status != 1) {
throw new BusinessException('教师不存在或已禁用', 10001);
throw new TeacherNotFound();
}
return $teacher->toArray();
}

View File

@@ -13,6 +13,7 @@
"license": "Apache-2.0",
"require": {
"php": ">=8.1",
"aether/hyperf": "dev-master",
"hyperf/cache": "~3.1.0",
"hyperf/command": "~3.1.0",
"hyperf/config": "~3.1.0",
@@ -47,6 +48,12 @@
"phpstan/phpstan": "^1.0",
"swoole/ide-helper": "^5.0"
},
"repositories": [
{
"type": "git",
"url": "https://gitee.com/devAether666/aether-hyperf.git"
}
],
"suggest": {
"ext-openssl": "Required to use HTTPS.",
"ext-json": "Required to use JSON.",
@@ -57,7 +64,6 @@
"autoload": {
"psr-4": {
"App\\": "app/",
"Aether\\": "extend/Aether/PHP/Hyperf/",
"MicroService\\": "extend/MicroService/src/"
},
"files": []

58
composer.lock generated
View File

@@ -4,8 +4,56 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "c30923a4e3d3314c7a9c295b398631d8",
"content-hash": "8e6c542d87ccd4c4654a4adb71436e17",
"packages": [
{
"name": "aether/hyperf",
"version": "dev-master",
"source": {
"type": "git",
"url": "https://gitee.com/devAether666/aether-hyperf.git",
"reference": "11a16d4cb0693ebdb5beb291777395132c001ef4"
},
"require": {
"hyperf/db-connection": "~3.1.0",
"hyperf/di": "~3.1.0",
"hyperf/logger": "~3.1.0",
"hyperf/model-cache": "^3.1",
"hyperf/validation": "^3.1",
"php": ">=8.1"
},
"require-dev": {
"hyperf/testing": "~3.1.0"
},
"default-branch": true,
"type": "library",
"extra": {
"hyperf": {
"config": []
}
},
"autoload": {
"psr-4": {
"Aether\\": "src/PHP/Hyperf/"
}
},
"autoload-dev": {
"psr-4": {
"Aether\\Tests\\": "tests/"
}
},
"license": [
"MIT"
],
"authors": [
{
"name": "Aether",
"email": "aether.dev.666@gmail.com"
}
],
"description": "Aether Hyperf Common Components",
"time": "2025-09-30T07:14:33+00:00"
},
{
"name": "carbonphp/carbon-doctrine-types",
"version": "3.2.0",
@@ -10648,12 +10696,14 @@
],
"aliases": [],
"minimum-stability": "dev",
"stability-flags": {},
"stability-flags": {
"aether/hyperf": 20
},
"prefer-stable": true,
"prefer-lowest": false,
"platform": {
"php": ">=8.1"
},
"platform-dev": {},
"plugin-api-version": "2.6.0"
"platform-dev": [],
"plugin-api-version": "2.0.0"
}

View File

@@ -1,7 +1,8 @@
<?php
declare(strict_types=1);
use Aether\RpcException\JsonRpcExceptionHandler;
use Aether\Exception\Handler\JsonRpcExceptionHandler;
return [
'handler' => [

View File

@@ -1,79 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether;
use Hyperf\Contract\ContainerInterface;
use Hyperf\Di\Annotation\Inject;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\HttpServer\Contract\ResponseInterface;
use Throwable;
abstract class AetherController
{
#[Inject]
protected ContainerInterface $container;
#[Inject]
protected RequestInterface $request;
#[Inject]
protected ResponseInterface $response;
/**
* 获取资源列表 (RESTFul: GET resources/list).
*/
public function index(): array
{
$params = $this->request->all();
$result = $this->getService()->list($params);
return AetherResponse::success($result);
}
/**
* 获取单个资源 (RESTFul: GET resources/{id}).
*/
public function detail(int $id): array
{
$result = $this->getService()->detail($id);
return AetherResponse::success($result);
}
/**
* 创建资源 (RESTFul: POST resources).
* @throws Throwable
*/
public function create(): array
{
$data = $this->request->all();
$id = $this->getService()->create($data);
return AetherResponse::success(['id' => $id], '创建成功');
}
/**
* 更新资源 (RESTFul: PUT resources/{id}).
* @throws Throwable
*/
public function update(int $id): array
{
$data = $this->request->all();
$this->getService()->update($id, $data);
return AetherResponse::success(null, '更新成功');
}
/**
* 删除资源 (RESTFul: DELETE resources/{id}).
* @throws Throwable
*/
public function delete(int $id): array
{
$this->getService()->delete($id);
return AetherResponse::success(null, '删除成功');
}
/**
* 获取对应的服务类.
*/
abstract protected function getService(): AetherCrudService;
}

View File

@@ -1,33 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether;
interface AetherCrudInterface
{
/**
* 列表.
*/
public function list(): array;
/**
* 查询.
*/
public function detail(int $id): object;
/**
* 新增.
*/
public function create(array $data): int;
/**
* 更新.
*/
public function update(int $id, array $data): bool;
/**
* 删除.
*/
public function delete(int $id): bool;
}

View File

@@ -1,323 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether;
use Aether\Contract\TreeableInterface;
use Aether\Exception\BusinessException;
use Hyperf\Database\Model\Builder;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Logger\LoggerFactory;
use Psr\Log\LoggerInterface;
use ReflectionClass;
use Throwable;
/**
* 抽象CRUD服务基类封装通用逻辑.
*/
abstract class AetherCrudService extends AetherService implements AetherCrudInterface
{
protected array $search = [];
protected array $ignoreSearchFields = [];
/**
* 通用列表查询(支持分页和树形结构).
*/
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
{
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;
});
}
/**
* 根据模型的$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);
}
}
/**
* 软删除恢复.
* @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;
});
}
protected function getSearch(): array
{
return $this->search;
}
protected function getIgnoreSearchFields(): array
{
return $this->ignoreSearchFields;
}
/**
* 获取当前服务对应的模型实例(由子类实现).
*/
abstract protected function getModel(): AetherModel;
/**
* 获取当前服务对应的验证器实例(由子类实现).
*/
abstract protected function getValidator(): AetherValidator;
/**
* 钩子方法:更新时的特殊逻辑(子类可重写).
*/
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
{
// 默认不检查,需要的子类重写
}
/**
* 应用单个搜索规则.
* @param mixed $value
* @param mixed $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、>、< 等
}
}
}

View File

@@ -1,476 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether;
use Aether\Contract\TreeableInterface;
use Closure;
use DateTime;
use Exception;
use Hyperf\Context\ApplicationContext;
use Hyperf\Database\Model\Builder;
use Hyperf\Database\Model\Collection;
use Hyperf\Database\Model\ModelNotFoundException;
use Hyperf\DbConnection\Db;
use Hyperf\DbConnection\Model\Model as HyperfModel;
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 array|bool|string $sortable = false; // 'sort'; // 默认按sort字段升序
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
$this->bootBaseModel();
}
/**
* 获取排序配置.
* @return null|array [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 list(array $params = []): array
{
$query = static::query();
// 通过模型配置自动应用所有搜索条件
$this->applySearch($query, $params);
// 动态应用排序
if ($this->sortable) {
$sortConfig = $this->getSortConfig();
$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 [
'list' => $result->items(),
'total' => $result->total(),
];
}
// 无分页参数时返回完整数据集合
$items = $query->get()->toArray();
// 若模型支持树形结构则构建树形,否则返回普通数组
if ($this instanceof TreeableInterface) {
return $this::buildTree($items, (int) ($params['parent_id'] ?? 0));
}
return $items;
}
/**
* 快捷创建.
*/
public static function createOne(array $data): AetherModel|Builder|HyperfModel
{
return static::query()->create($data);
}
/**
* 快捷更新.
*/
public static function updateById(int $id, array $data): int
{
return static::query()->where('id', $id)->update($data);
}
/**
* 快捷删除指定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 的记录',
$id,
));
}
return static::query()->where('id', $id)->delete() > 0;
}
/**
* 快捷查找.
* @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查找记录不存在则抛出异常
* @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 的记录',
$id
));
}
return $record; // static::query()->findOrFail($id);
}
/**
* 获取器处理.
*/
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;
}
/**
* 应用搜索条件.
*/
protected function applySearch(Builder $query, array $conditions): void
{
foreach ($conditions as $field => $value) {
// 基础过滤:非字符串字段名或未设置值的参数直接跳过
if (! is_string($field) || ! isset($value)) {
continue;
}
// 核心限制:只处理$search数组中定义的字段
if (! isset($this->search[$field])) {
continue;
}
// 处理嵌套关系查询(如:user.name需在$search中配置完整键名
if (str_contains($field, '.')) {
[$relation, $relationField] = explode('.', $field, 2);
$query->whereHas($relation, function ($q) use ($relationField, $value) {
// 嵌套查询默认使用精确匹配,如需特殊规则可在$search中自定义处理
$q->where($relationField, $value);
});
continue;
}
// 优先使用自定义搜索器方法(仅对$search中存在的字段生效
$searchMethod = 'search' . ucfirst($field);
if (method_exists($this, $searchMethod)) {
$this->{$searchMethod}($query, $value);
continue;
}
// 应用$search中定义的搜索规则如=、like等
$this->applySearchRule($query, $field, $value, $this->search[$field]);
}
}
/**
* 应用搜索规则.
*/
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);
}
}
}

View File

@@ -1,69 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether;
class AetherResponse
{
/**
* 成功响应.
* @param null|mixed $data 数据
* @param string $message 消息
*/
public static function success(mixed $data = null, string $message = '操作成功'): array
{
return [
Config::RESPONSE_FIELD_KEY_DATA => $data,
Config::RESPONSE_FIELD_KEY_CODE => Config::RESPONSE_SUCCESS_CODE,
Config::RESPONSE_FIELD_KEY_MESSAGE => $message ?: Config::RESPONSE_SUCCESS_MESSAGE,
];
}
/**
* 错误响应.
* @param int $code 错误码
* @param string $message 错误消息
* @param null|mixed $data 附加数据
*/
public static function error(string $message = '', int $code = Config::RESPONSE_FAIL_CODE, mixed $data = null): array
{
return [
Config::RESPONSE_FIELD_KEY_CODE => $code,
Config::RESPONSE_FIELD_KEY_DATA => $data,
Config::RESPONSE_FIELD_KEY_MESSAGE => $message ?: self::getDefaultMessage($code),
];
}
public static function page($list, int $total, int $page, int $size): array
{
return [
Config::RESPONSE_FIELD_KEY_CODE => Config::RESPONSE_SUCCESS_CODE,
Config::RESPONSE_FIELD_KEY_MESSAGE => 'success',
Config::RESPONSE_FIELD_KEY_LIST => [
'list' => $list,
'page' => $page,
'size' => $size,
'total' => $total,
'pages' => (int) ceil($total / $size),
],
];
}
/**
* 获取默认错误消息.
*/
private static function getDefaultMessage(int $code): string
{
$messages = [
400 => '请求参数错误',
401 => '未授权',
403 => '禁止访问',
404 => '资源不存在',
500 => '服务器内部错误',
10001 => '校区不存在',
];
return $messages[$code] ?? '未知错误';
}
}

View File

@@ -1,47 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether;
use Aether\Exception\BusinessException;
use Hyperf\DbConnection\Db;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Logger\LoggerFactory;
use Psr\Log\LoggerInterface;
use Throwable;
abstract class AetherService
{
#[Inject]
protected LoggerFactory $loggerFactory;
/**
* 获取当前服务日志器.
*/
protected function logger(): LoggerInterface
{
$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);
}
}
}

View File

@@ -1,133 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether;
use Aether\Exception\ValidationFailedException;
use Hyperf\Context\ApplicationContext;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Validation\Contract\ValidatorFactoryInterface;
use Hyperf\Validation\Validator;
use RuntimeException;
abstract class AetherValidator
{
/**
* 当前场景名.
*/
public ?string $currentScene = null;
#[Inject]
protected ValidatorFactoryInterface $validationFactory;
/**
* 待验证数据.
*/
protected array $data = [];
/**
* 自定义验证规则子类可通过该属性注册无需重写registerRules
* 格式:['规则名' => 闭包/类方法].
*/
protected array $customRules = [];
/**
* 静态快捷验证方法(简化调用).
*/
public static function validate(string $scene, array $data = []): array
{
$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'] ?? []
);
}
/**
* 格式化验证错误信息(统一格式,供异常处理器复用).
*/
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;
}
/**
* 实际执行验证的逻辑(重命名方法名更清晰).
*/
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();
}
/**
* 自动注册自定义规则(优先使用$customRules属性.
*/
protected function registerRules(Validator $validator): void
{
foreach ($this->customRules as $ruleName => $rule) {
$validator->extend($ruleName, $rule);
}
}
/**
* 定义场景验证规则(子类实现).
*/
abstract protected function scenes(): array;
}

View File

@@ -1,43 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether;
use Hyperf\Context\Context;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\ResponseInterface;
use Throwable;
use function Hyperf\Support\env;
class ApiExceptionHandler extends ExceptionHandler
{
public function handle(Throwable $throwable, ResponseInterface $response): MessageInterface|ResponseInterface
{
// 格式化输出
$data = [
'code' => $throwable->getCode() ?: 500,
'message' => $throwable->getMessage() ?: '服务器内部错误',
'request_id' => Context::get('request_id', ''),
'timestamp' => time(),
];
// 开发环境显示堆栈信息
if (env('APP_ENV') === 'dev') {
$data['trace'] = $throwable->getTraceAsString();
}
$body = json_encode($data, JSON_UNESCAPED_UNICODE);
return $response->withHeader('Content-Type', 'application/json')
->withStatus($data['code'] >= 400 && $data['code'] < 500 ? $data['code'] : 500)
->withBody(new SwooleStream($body));
}
public function isValid(Throwable $throwable): bool
{
return true;
}
}

View File

@@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether;
class Config
{
public const RESPONSE_FIELD_KEY_CODE = 'code';
public const RESPONSE_FIELD_KEY_DATA = 'data';
public const RESPONSE_FIELD_KEY_MESSAGE = 'message';
public const RESPONSE_FIELD_KEY_LIST = 'list';
public const RESPONSE_SUCCESS_CODE = 200;
public const RESPONSE_SUCCESS_MESSAGE = 'success';
public const RESPONSE_FAIL_CODE = -1;
public const RESPONSE_FAIL_MESSAGE = 'fail';
}

View File

@@ -1,24 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether\Contract;
/**
* 树形结构接口,标识模型支持树形功能.
*/
interface TreeableInterface
{
/**
* 构建树形结构.
* @param array $items 原始数据
* @param int $parentId 根节点ID
*/
public static function buildTree(array $items, int $parentId = 0): array;
/**
* 获取子节点ID集合.
* @param int $id 节点ID
*/
public function getChildIds(int $id): array;
}

View File

@@ -1,143 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether\Exception;
use Aether\AetherValidator;
use Hyperf\Context\Context;
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 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)));
}
public function isValid(Throwable $throwable): bool
{
return true;
}
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(),
];
}
}

View File

@@ -1,126 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether\Exception;
use Aether\AetherValidator;
use Hyperf\Context\Context;
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 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)));
}
public function isValid(Throwable $throwable): bool
{
return true;
}
/**
* 统一错误响应格式.
*/
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(),
];
}
}

View File

@@ -1,45 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether\Exception;
use Hyperf\Server\Exception\ServerException;
use Throwable;
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;
}
}

View File

@@ -1,35 +0,0 @@
<?php
declare(strict_types=1);
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;
}
}

View File

@@ -1,79 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether;
use Aether\Exception\BusinessException;
use Aether\Exception\ValidationFailedException;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Database\Model\ModelNotFoundException;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\ResponseInterface;
use Throwable;
use function Hyperf\Support\env;
class GlobalExceptionHandler extends ExceptionHandler
{
protected StdoutLoggerInterface $logger;
public function __construct(StdoutLoggerInterface $logger)
{
$this->logger = $logger;
}
public function handle(Throwable $throwable, ResponseInterface $response): MessageInterface|ResponseInterface
{
if ($throwable instanceof ValidationFailedException) {
return $response->withBody(new SwooleStream($throwable->getMessage()));
}
// 处理业务异常
if ($throwable instanceof BusinessException) {
$data = [
Config::RESPONSE_FIELD_KEY_CODE => $throwable->getCode(),
Config::RESPONSE_FIELD_KEY_MESSAGE => $throwable->getMessage(),
Config::RESPONSE_FIELD_KEY_DATA => null,
];
}
// 数据库无记录异常
if ($throwable instanceof ModelNotFoundException) {
$data = [
Config::RESPONSE_FIELD_KEY_CODE => 404,
Config::RESPONSE_FIELD_KEY_MESSAGE => $throwable->getMessage() ?: '没有对应记录',
Config::RESPONSE_FIELD_KEY_DATA => null,
];
} else {
// 记录未知错误日志
$this->logger->error(sprintf(
'Unknown error: %s %s in %s:%d',
$throwable->getMessage(),
$throwable->getCode(),
$throwable->getFile(),
$throwable->getLine()
));
$data = [
Config::RESPONSE_FIELD_KEY_CODE => 500,
Config::RESPONSE_FIELD_KEY_MESSAGE => 'Server internal error',
Config::RESPONSE_FIELD_KEY_DATA => env('APP_ENV') === 'dev' ? [
'message' => $throwable->getMessage(),
'file' => $throwable->getFile(),
'line' => $throwable->getLine(),
'trace' => $throwable->getTraceAsString(),
] : null,
];
}
$body = json_encode($data, JSON_UNESCAPED_UNICODE);
return $response->withHeader('Content-Type', 'application/json')
->withBody(new SwooleStream($body));
}
public function isValid(Throwable $throwable): bool
{
return true;
}
}

View File

@@ -1,29 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether\Middleware;
use Hyperf\HttpServer\Contract\ResponseInterface as HttpResponse;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class Cors implements MiddlewareInterface
{
public function __construct(protected HttpResponse $response)
{
}
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$response = $handler->handle($request);
return $response
->withHeader('Access-Control-Allow-Origin', '*')
->withHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS')
->withHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-Requested-With')
->withHeader('Access-Control-Max-Age', '86400');
}
}

View File

@@ -1,96 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether\Middleware;
use Aether\RpcException\BusinessException;
use Aether\RpcException\ErrorCode;
use Hyperf\Context\ApplicationContext;
use Hyperf\Context\Context;
use Hyperf\Contract\StdoutLoggerInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Throwable;
use function Hyperf\support\env;
class GatewayExceptionHandler implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
Context::set(ServerRequestInterface::class, $request);
try {
return $handler->handle($request);
} catch (Throwable $e) {
// 尝试解析异常信息判断是否为RPC服务返回的业务异常
$parsed = $this->parseRpcException($e);
if ($parsed) {
// 转换为业务异常
throw new BusinessException(
$parsed['code'],
$parsed['message'],
$parsed['data'] ?? []
);
}
// 记录非业务异常日志
$logger = ApplicationContext::getContainer()->get(StdoutLoggerInterface::class);
$logger->error(sprintf('服务调用异常: %s', $e->getMessage()), [
'trace' => $e->getTraceAsString(),
'file' => $e->getFile(),
'line' => $e->getLine(),
]);
// 非业务异常,使用系统错误码
if ($e instanceof BusinessException) {
throw $e;
}
// 统一转换为系统错误
throw new BusinessException(
env('APP_ENV') === 'dev' ? $e->getMessage() : ErrorCode::getMessage(ErrorCode::RPC_CALL_ERROR),
ErrorCode::RPC_CALL_ERROR,
);
}
}
/**
* 解析RPC异常信息.
*/
private function parseRpcException(Throwable $e): ?array
{
try {
// 从异常消息中解析JSON数据
$message = $e->getMessage();
$data = json_decode($message, true);
// 检查是否为有效的JSON-RPC错误响应
if (json_last_error() === JSON_ERROR_NONE && isset($data['jsonrpc']) && $data['jsonrpc'] === '2.0') {
$error = $data['error'] ?? [];
// 检查是否包含业务异常标识
return [
'code' => $error['code'] ?? ErrorCode::RPC_CALL_ERROR,
'message' => $error['message'] ?? '服务调用异常',
'data' => $error['data'] ?? [],
];
// 普通RPC错误
}
// 检查是否为直接返回的错误数组
if (is_array($data) && isset($data['code'], $data['message'])) {
return $data;
}
} catch (Throwable $parseError) {
// 解析失败不影响主流程
}
return null;
}
}

View File

@@ -1,23 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether\Middleware;
use Hyperf\Context\Context;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
class RequestId implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$requestId = $request->getHeaderLine('X-Request-Id') ?: uniqid();
Context::set('request_id', $requestId);
$response = $handler->handle($request);
return $response->withHeader('X-Request-Id', $requestId);
}
}

View File

@@ -1,64 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether\RpcException;
use Aether\Config;
use App\Exception\BusinessException;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Psr\Http\Message\MessageInterface;
use Psr\Http\Message\ResponseInterface;
use Psr\Log\LoggerInterface;
use Throwable;
use function Hyperf\support\env;
class ApiExceptionHandler extends ExceptionHandler
{
protected LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
public function handle(Throwable $throwable, ResponseInterface $response): MessageInterface|ResponseInterface
{
$this->logger->error($throwable->getMessage());
// 业务异常
if ($throwable instanceof BusinessException) {
$code = $throwable->getCode();
$message = $throwable->getMessage();
$data = $throwable->getData();
} else {
// 其他异常
$code = $throwable->getCode(); // ErrorCode::SYSTEM_ERROR;
$data = env('APP_ENV') === 'dev' ? [
'trace' => $throwable->getTraceAsString(),
'file' => $throwable->getFile(),
'line' => $throwable->getLine(),
] : [];
$message = env('APP_ENV') === 'dev' ? $throwable->getMessage() : ErrorCode::getMessage($code);
}
$result = [
Config::RESPONSE_FIELD_KEY_CODE => $code,
Config::RESPONSE_FIELD_KEY_DATA => $data,
Config::RESPONSE_FIELD_KEY_MESSAGE => $message,
];
$body = new SwooleStream(json_encode($result, JSON_UNESCAPED_UNICODE));
return $response->withHeader('Content-Type', 'application/json')
->withStatus(200)
->withBody($body);
}
public function isValid(Throwable $throwable): bool
{
return true;
}
}

View File

@@ -1,41 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether\RpcException;
use Hyperf\Server\Exception\ServerException;
use Throwable;
class BusinessException extends ServerException
{
/**
* @var int 错误码
*/
protected $code;
/**
* @var array 额外数据
*/
protected array $data = [];
public function __construct(string $message = '', int $code = ErrorCode::SYSTEM_ERROR, array $data = [], ?Throwable $previous = null)
{
if (empty($message)) {
$message = ErrorCode::getMessage($code);
}
$this->code = $code;
$this->data = $data;
parent::__construct($message, $code, $previous);
}
/**
* 获取额外数据.
*/
public function getData(): array
{
return $this->data;
}
}

View File

@@ -1,47 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether\RpcException;
class ErrorCode
{
// 系统错误
public const SYSTEM_ERROR = 500;
public const PARAM_ERROR = 400;
public const AUTH_ERROR = 401;
public const FORBIDDEN_ERROR = 403;
public const NOT_FOUND = 404;
// 业务错误
public const DATA_NOT_FOUND = 10001;
public const ARTICLE_NOT_FOUND = 20001;
public const NOTICE_NOT_FOUND = 30001;
public const RPC_CALL_ERROR = 50001;
/**
* 获取错误信息.
*/
public static function getMessage(int $code): string
{
$messages = [
self::SYSTEM_ERROR => '系统错误',
self::PARAM_ERROR => '参数错误',
self::AUTH_ERROR => '未授权',
self::FORBIDDEN_ERROR => '权限不足',
self::NOT_FOUND => '资源不存在',
self::DATA_NOT_FOUND => '数据不存在',
self::ARTICLE_NOT_FOUND => '文章不存在',
self::NOTICE_NOT_FOUND => '公告不存在',
];
return $messages[$code] ?? '未知错误';
}
}

View File

@@ -1,57 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether\RpcException;
use Hyperf\Context\ApplicationContext;
use Hyperf\Contract\ConfigInterface;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\Rpc\Contract\ResponseInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Psr\Http\Message\MessageInterface;
use Psr\Log\LoggerInterface;
use Throwable;
class JsonRpcExceptionHandler
{
protected LoggerInterface $logger;
public function __construct(LoggerInterface $logger)
{
$this->logger = $logger;
}
/**
* JSON-RPC 异常处理.
* @throws ContainerExceptionInterface
* @throws NotFoundExceptionInterface
*/
public function handle(Throwable $throwable, ResponseInterface $response): MessageInterface|\Psr\Http\Message\ResponseInterface|ResponseInterface
{
$responseContents = $response->getBody()->getContents();
$responseContents = json_decode($responseContents, true);
if (! empty($responseContents['error'])) {
$this->logger->error($responseContents['error']);
$port = null;
$config = ApplicationContext::getContainer()->get(ConfigInterface::class);
$servers = $config->get('server.servers');
foreach ($servers as $k => $server) {
if ($server['name'] == 'jsonrpc-http') {
$port = $server['port'];
break;
}
}
$responseContents['error']['message'] .= " - {$config->get('app_name')}:{$port}";
}
$data = json_encode($responseContents, JSON_UNESCAPED_UNICODE);
return $response->withStatus(200)->withBody(new SwooleStream($data));
}
public function isValid(Throwable $throwable): bool
{
return true;
}
}

View File

@@ -1,38 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether\RpcException;
use Hyperf\Server\Exception\ServerException;
use Throwable;
class ValidateException extends ServerException
{
/**
* @var int 错误码
*/
protected $code;
/**
* @var array 额外数据
*/
protected array $data = [];
public function __construct(string $message = '', int $code = ErrorCode::PARAM_ERROR, array $data = [], ?Throwable $previous = null)
{
if (empty($message)) {
$message = ErrorCode::getMessage($code);
}
$this->code = $code;
$this->data = $data;
parent::__construct($message, $code, $previous);
}
public function getData(): array
{
return $this->data;
}
}

View File

@@ -1,104 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether;
use Hyperf\Context\Context;
use Hyperf\ExceptionHandler\ExceptionHandler;
use Hyperf\HttpMessage\Stream\SwooleStream;
use Psr\Http\Message\ResponseInterface;
use Throwable;
use function Hyperf\Support\env;
class RpcExceptionHandler extends ExceptionHandler
{
public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
{
try {
// 获取请求ID用于日志追踪
$requestId = Context::get('request_id', '');
// 从请求中获取可能的RPC ID
$rpcId = $this->getRpcIdFromRequest();
// 构建符合JSON-RPC 2.0规范的错误响应
$errorResponse = [
'jsonrpc' => '2.0',
'id' => $rpcId ?? $requestId,
'error' => [
'code' => $throwable->getCode() ?: -32603, // 默认服务器错误码
'message' => $throwable->getMessage() ?: 'Internal error',
'data' => [
'request_id' => $requestId,
'exception_type' => get_class($throwable),
// 开发环境下可以添加更多调试信息
'debug' => env('APP_ENV') === 'dev' ? [
'file' => $throwable->getFile(),
'line' => $throwable->getLine(),
] : null,
],
],
];
// JSON编码
$jsonResponse = json_encode($errorResponse, JSON_UNESCAPED_UNICODE);
// 检查JSON编码错误
if (json_last_error() !== JSON_ERROR_NONE) {
$jsonResponse = json_encode([
'jsonrpc' => '2.0',
'id' => $rpcId ?? $requestId,
'error' => [
'code' => -32603,
'message' => 'Failed to encode error response',
'data' => ['request_id' => $requestId],
],
]);
}
return $response
->withHeader('Content-Type', 'application/json')
->withBody(new SwooleStream((string) $jsonResponse));
} catch (Throwable $e) {
$fallbackResponse = json_encode([
'jsonrpc' => '2.0',
'id' => null,
'error' => [
'code' => -32603,
'message' => 'Fatal error occurred in exception handler',
'data' => ['original_error' => $throwable->getMessage()],
],
]);
return $response
->withHeader('Content-Type', 'application/json')
->withBody(new SwooleStream((string) $fallbackResponse));
}
}
public function isValid(Throwable $throwable): bool
{
return true;
}
/**
* 尝试从请求中获取RPC ID.
*/
private function getRpcIdFromRequest(): mixed
{
try {
$request = Context::get('hyperf.request');
if ($request) {
$body = $request->getParsedBody();
if (is_array($body) && isset($body['id'])) {
return $body['id'];
}
}
} catch (Throwable $e) {
// 获取失败时静默处理
}
return null;
}
}

View File

@@ -1,159 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether\Traits;
use App\Notice\Enum\NoticeStatusEnum;
use App\Data\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 枚举实例
*/
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 匹配的枚举实例无匹配时返回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
));
}
}
}

View File

@@ -1,10 +0,0 @@
<?php
declare(strict_types=1);
namespace Aether\Traits;
trait AetherSearchable
{
}

View File

@@ -1,26 +0,0 @@
<?php
declare(strict_types=1);
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';
}
}
}

View File

@@ -1,128 +0,0 @@
<?php
declare(strict_types=1);
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)
);
}
}
/**
* 构建树形结构.
* @param mixed $items
*/
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;
}
/**
* 获取指定节点的所有子节点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;
}
/**
* 获取节点的完整路径.
*/
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;
}
/**
* 抽象方法获取父ID字段名由子类实现.
*/
abstract protected function getParentIdField(): string;
/**
* 抽象方法:获取排序字段名(由子类实现).
*/
abstract protected function getSortField(): string;
/**
* 树形节点排序.
*/
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.
*/
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);
}
}
}
}