From dd2b32cc95a249d2e21fb4d12e2b79177dd797ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A0=82=E5=90=89=E8=AF=83=E5=BE=B7=E6=8B=89=E6=9B=BC?= =?UTF-8?q?=E6=9F=A5=E7=9A=84=E8=8B=B1=E8=B1=AA?= Date: Thu, 6 Aug 2026 09:26:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=A0=B8=E5=BF=83=E5=BA=93=E8=A1=A5=E9=BD=90Ae?= =?UTF-8?q?ther=E5=85=BC=E5=AE=B9=E6=96=B9=E6=B3=95=E4=B8=8E=E6=A0=91?= =?UTF-8?q?=E5=BD=A2Trait=EF=BC=8C=E4=B8=AD=E9=97=B4=E4=BB=B6=E5=A4=8D?= =?UTF-8?q?=E7=94=A8RPC=E4=B8=8A=E4=B8=8B=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Middleware/RequestContextMiddleware.php | 10 +- src/Model.php | 100 +++++++++++++-- src/Trait/TreeTrait.php | 130 ++++++++++++++++++++ 3 files changed, 228 insertions(+), 12 deletions(-) create mode 100644 src/Trait/TreeTrait.php diff --git a/src/Middleware/RequestContextMiddleware.php b/src/Middleware/RequestContextMiddleware.php index 456f424..b22c631 100644 --- a/src/Middleware/RequestContextMiddleware.php +++ b/src/Middleware/RequestContextMiddleware.php @@ -22,13 +22,15 @@ class RequestContextMiddleware implements MiddlewareInterface { $rpcContext = Util::get(RpcContext::class); - $traceId = $request->getHeaderLine('X-Trace-Id') - ?: uniqid('t-', true); + $traceId = $rpcContext->get('trace_id'); + if (empty($traceId)) { + $traceId = $request->getHeaderLine('X-Trace-Id') ?: uniqid('t-', true); + } Context::set('trace_id', $traceId); $rpcContext->set('trace_id', $traceId); // app版本 - $appVersion = $request->getHeaderLine('X-App-Version') ?: ''; + $appVersion = $rpcContext->get('app_version') ?: $request->getHeaderLine('X-App-Version') ?: ''; Context::set('app_version', $appVersion); $rpcContext->set('app_version', $appVersion); @@ -36,7 +38,7 @@ class RequestContextMiddleware implements MiddlewareInterface // APP: app // 微信小程序: wx_mini // 官网: web_official - $platformName = $request->getHeaderLine('X-Platform-Name') ?: ''; + $platformName = $rpcContext->get('platform_name') ?: $request->getHeaderLine('X-Platform-Name') ?: ''; Context::set('platform_name', $platformName); $rpcContext->set('platform_name', $platformName); diff --git a/src/Model.php b/src/Model.php index b06dda1..e19e87e 100644 --- a/src/Model.php +++ b/src/Model.php @@ -119,13 +119,7 @@ abstract class Model extends HyperfModel implements CacheableInterface } $this->applySearch($query, $params); - - if ($this->sortable) { - $sortConfig = $this->getSortConfig(); - if ($sortConfig) { - $query->orderBy($sortConfig['field'], $sortConfig['direction']); - } - } + $this->applyOrdering($query, $params); $withDeleted = filter_var($params['with_deleted'] ?? false, FILTER_VALIDATE_BOOLEAN); if ($withDeleted) { @@ -208,12 +202,48 @@ abstract class Model extends HyperfModel implements CacheableInterface $searchParams = array_intersect_key($params, array_flip($allowedSearchFields)); // 合并系统参数与合法搜索参数(排除其他未知参数) - $systemParams = ['page', 'size', 'with_deleted', 'parent_id']; + $systemParams = ['page', 'size', 'with_deleted', 'parent_id', 'order_by', 'order_direction']; $validSystemParams = array_intersect_key($params, array_flip($systemParams)); return array_merge($validSystemParams, $searchParams); } + /** + * 应用排序逻辑,支持自定义 order_by/order_direction. + */ + protected function applyOrdering(Builder $query, array $params): void + { + $orderField = $params['order_by'] ?? null; + $orderDirection = strtolower((string) ($params['order_direction'] ?? 'asc')); + + if (is_string($orderField) && $this->isValidOrderField($orderField)) { + $direction = $orderDirection === 'desc' ? 'desc' : 'asc'; + $query->orderBy($orderField, $direction); + if ($orderField !== 'id' && ! str_contains($orderField, '.')) { + $query->orderBy('id', 'asc'); + } + return; + } + + if ($this->sortable) { + $sortConfig = $this->getSortConfig(); + if ($sortConfig) { + $query->orderBy($sortConfig['field'], $sortConfig['direction']); + if ($sortConfig['field'] !== 'id' && ! str_contains($sortConfig['field'], '.')) { + $query->orderBy('id', 'asc'); + } + } + } + } + + /** + * 校验排序字段,避免 SQL 注入. + */ + protected function isValidOrderField(string $field): bool + { + return (bool) preg_match('/^[a-zA-Z0-9_\.]+$/', $field); + } + /** * 快捷创建. */ @@ -290,6 +320,60 @@ abstract class Model extends HyperfModel implements CacheableInterface return $record; } + /** + * 查找记录,不存在时抛出业务异常. + */ + public static function findOrThrow($id, string $message = '数据不存在'): static + { + $record = static::query()->find($id); + if (is_null($record)) { + throw new BusinessException($message, 404); + } + return $record; + } + + /** + * Aether 兼容:创建记录并返回 ID. + */ + public static function createBy(array $data): int + { + return static::query()->create($data)->id; + } + + /** + * Aether 兼容:按主键查找,不存在时抛出业务异常. + */ + public static function findByPk(int $id): array|Builder|Collection|HyperfModel + { + $record = static::query()->find($id); + if (is_null($record)) { + throw new BusinessException(sprintf('找不到 ID 为 %d 的记录', $id), 404); + } + return $record; + } + + /** + * Aether 兼容:按主键更新. + */ + public static function updateByPk(int $id, array $data): bool + { + if (! static::query()->where('id', $id)->exists()) { + throw new BusinessException(sprintf('找不到 ID 为 %d 的记录', $id), 404); + } + return static::query()->where('id', $id)->update($data) > 0; + } + + /** + * Aether 兼容:按主键删除. + */ + public static function deleteByPk(int $id): bool + { + if (! static::query()->where('id', $id)->exists()) { + throw new BusinessException(sprintf('找不到 ID 为 %d 的记录', $id), 404); + } + return static::query()->where('id', $id)->delete() > 0; + } + /** * 获取器处理. */ diff --git a/src/Trait/TreeTrait.php b/src/Trait/TreeTrait.php new file mode 100644 index 0000000..0a80bff --- /dev/null +++ b/src/Trait/TreeTrait.php @@ -0,0 +1,130 @@ +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(); + $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(); + $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 + { + if (empty($sortField)) { + return; + } + + usort($items, function ($a, $b) use ($sortField) { + $direction = $this->treeSortDirection ?? 'asc'; + $aValue = $a[$sortField] ?? 0; + $bValue = $b[$sortField] ?? 0; + return $direction === 'desc' + ? $bValue <=> $aValue + : $aValue <=> $bValue; + }); + } + + /** + * 递归收集子节点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); + } + } + } +}