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); + } + } + } +}