composer包
This commit is contained in:
11
app/Exception/CampusNotFound.php
Normal file
11
app/Exception/CampusNotFound.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Exception;
|
||||||
|
|
||||||
|
use Aether\Exception\AetherException;
|
||||||
|
|
||||||
|
class CampusNotFound extends AetherException
|
||||||
|
{
|
||||||
|
protected $code = 404001;
|
||||||
|
protected $message = '文章不存在';
|
||||||
|
}
|
||||||
14
app/Exception/TeacherNotFound.php
Normal file
14
app/Exception/TeacherNotFound.php
Normal 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 = '教师不存在';
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@ declare(strict_types=1);
|
|||||||
namespace App\JsonRpc\Service;
|
namespace App\JsonRpc\Service;
|
||||||
|
|
||||||
use Aether\Exception\BusinessException;
|
use Aether\Exception\BusinessException;
|
||||||
|
use App\Exception\CampusNotFound;
|
||||||
|
use App\Exception\TeacherNotFound;
|
||||||
use App\Model\Campus;
|
use App\Model\Campus;
|
||||||
use App\Model\Teacher;
|
use App\Model\Teacher;
|
||||||
use App\Validator\CampusValidator;
|
use App\Validator\CampusValidator;
|
||||||
@@ -45,7 +47,7 @@ class DataService implements DataServiceInterface
|
|||||||
{
|
{
|
||||||
$campus = $this->campusModel->find($id);
|
$campus = $this->campusModel->find($id);
|
||||||
if (! $campus || $campus->status != 1) {
|
if (! $campus || $campus->status != 1) {
|
||||||
throw new BusinessException('校区不存在或已禁用', 10001);
|
throw new CampusNotFound('校区不存在或已禁用');
|
||||||
}
|
}
|
||||||
return $campus->toArray();
|
return $campus->toArray();
|
||||||
}
|
}
|
||||||
@@ -122,7 +124,7 @@ class DataService implements DataServiceInterface
|
|||||||
{
|
{
|
||||||
$teacher = $this->teacherModel->find($id);
|
$teacher = $this->teacherModel->find($id);
|
||||||
if (! $teacher || $teacher->status != 1) {
|
if (! $teacher || $teacher->status != 1) {
|
||||||
throw new BusinessException('教师不存在或已禁用', 10001);
|
throw new TeacherNotFound();
|
||||||
}
|
}
|
||||||
return $teacher->toArray();
|
return $teacher->toArray();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"require": {
|
"require": {
|
||||||
"php": ">=8.1",
|
"php": ">=8.1",
|
||||||
|
"aether/hyperf": "dev-master",
|
||||||
"hyperf/cache": "~3.1.0",
|
"hyperf/cache": "~3.1.0",
|
||||||
"hyperf/command": "~3.1.0",
|
"hyperf/command": "~3.1.0",
|
||||||
"hyperf/config": "~3.1.0",
|
"hyperf/config": "~3.1.0",
|
||||||
@@ -47,6 +48,12 @@
|
|||||||
"phpstan/phpstan": "^1.0",
|
"phpstan/phpstan": "^1.0",
|
||||||
"swoole/ide-helper": "^5.0"
|
"swoole/ide-helper": "^5.0"
|
||||||
},
|
},
|
||||||
|
"repositories": [
|
||||||
|
{
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://gitee.com/devAether666/aether-hyperf.git"
|
||||||
|
}
|
||||||
|
],
|
||||||
"suggest": {
|
"suggest": {
|
||||||
"ext-openssl": "Required to use HTTPS.",
|
"ext-openssl": "Required to use HTTPS.",
|
||||||
"ext-json": "Required to use JSON.",
|
"ext-json": "Required to use JSON.",
|
||||||
@@ -57,7 +64,6 @@
|
|||||||
"autoload": {
|
"autoload": {
|
||||||
"psr-4": {
|
"psr-4": {
|
||||||
"App\\": "app/",
|
"App\\": "app/",
|
||||||
"Aether\\": "extend/Aether/PHP/Hyperf/",
|
|
||||||
"MicroService\\": "extend/MicroService/src/"
|
"MicroService\\": "extend/MicroService/src/"
|
||||||
},
|
},
|
||||||
"files": []
|
"files": []
|
||||||
|
|||||||
58
composer.lock
generated
58
composer.lock
generated
@@ -4,8 +4,56 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "c30923a4e3d3314c7a9c295b398631d8",
|
"content-hash": "8e6c542d87ccd4c4654a4adb71436e17",
|
||||||
"packages": [
|
"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",
|
"name": "carbonphp/carbon-doctrine-types",
|
||||||
"version": "3.2.0",
|
"version": "3.2.0",
|
||||||
@@ -10648,12 +10696,14 @@
|
|||||||
],
|
],
|
||||||
"aliases": [],
|
"aliases": [],
|
||||||
"minimum-stability": "dev",
|
"minimum-stability": "dev",
|
||||||
"stability-flags": {},
|
"stability-flags": {
|
||||||
|
"aether/hyperf": 20
|
||||||
|
},
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": ">=8.1"
|
"php": ">=8.1"
|
||||||
},
|
},
|
||||||
"platform-dev": {},
|
"platform-dev": [],
|
||||||
"plugin-api-version": "2.6.0"
|
"plugin-api-version": "2.0.0"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
use Aether\RpcException\JsonRpcExceptionHandler;
|
|
||||||
|
use Aether\Exception\Handler\JsonRpcExceptionHandler;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'handler' => [
|
'handler' => [
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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、>、< 等
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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] ?? '未知错误';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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';
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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(),
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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] ?? '未知错误';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Aether\Traits;
|
|
||||||
|
|
||||||
trait AetherSearchable
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -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';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user