'字段名', 'direction' => 'asc/desc']. */ protected array|bool|string $sortable = false; // 'sort'; // 默认按sort字段升序 public function __construct(array $attributes = [], $needBoot = true) { parent::__construct($attributes); if ($needBoot) $this->bootBaseModel(); } public function scopeEnabled(Builder $query): Builder { return $query->where('status', 1); } /** * 获取排序配置. * @return null|array [field, direction] 或 null(禁用排序) */ public function getSortConfig(): ?array { if ($this->sortable === false) { return null; } // 处理字符串配置(如 'sort' 或 'created_at') if (is_string($this->sortable)) { return [ 'field' => $this->sortable, 'direction' => 'asc', ]; } // 处理数组配置(如 ['field' => 'id', 'direction' => 'desc']) if (is_array($this->sortable)) { $field = $this->sortable['field'] ?? null; $direction = $this->sortable['direction'] ?? 'asc'; // 验证字段有效性,避免空字段导致错误 if (!empty($field) && is_string($field)) { return [ 'field' => $field, 'direction' => strtolower($direction) === 'desc' ? 'desc' : 'asc', ]; } // 数组配置无效时返回null(禁用排序) return null; } // 其他无效类型(如true)返回null return null; } public function list(array $params = [], $isTree = true, $callback = null, $eachCallback = null): array { $params = $this->paramsHandler($params); $query = static::query(); $with = $params['with'] ?? []; if (!empty($with)) { if (is_string($with)) { $with = explode(',', $with); } if (is_array($with)) { $query->with($with); } } $this->applySearch($query, $params); $this->applyOrdering($query, $params); $withDeleted = filter_var($params['with_deleted'] ?? false, FILTER_VALIDATE_BOOLEAN); if ($withDeleted) { $query->withTrashed(); } if (is_callable($callback)) { call_user_func($callback, $query); } 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)); $result = $query->paginate($size, ['*'], 'page', $page); if (is_callable($eachCallback) && method_exists($result, 'each')) { $result->each($eachCallback); } return [ 'list' => $result->items(), 'page' => $page, 'size' => $size, 'total' => $result->total(), 'pages' => $result->lastPage() ]; } $items = $query->get()->toArray(); if ($isTree) { if ($this instanceof TreeableInterface) { return $this::buildTree($items, (int) ($params['parent_id'] ?? 0)); } } return $items; } /** * 处理列表查询参数 */ protected function paramsHandler(array $params): array { // 移除空键 $params = array_filter($params, function ($key) { return is_string($key) && trim($key) !== ''; }, ARRAY_FILTER_USE_KEY); // 移除null和空字符串(保留0、false等有意义的值) $params = array_filter($params, function ($value) { return $value !== null && $value !== ''; }); // 去除首尾空格 foreach ($params as $key => &$value) { if (is_string($value)) { $value = trim($value); } } unset($value); // 解除引用 // 分页参数处理:确保为有效整数 // $params['page'] = isset($params['page']) ? max(1, (int)$params['page']) : 1; // $params['size'] = isset($params['size']) ? max(1, min(100, (int)$params['size'])) : 10; // 布尔值参数处理(如with_deleted) if (isset($params['with_deleted'])) { $params['with_deleted'] = filter_var($params['with_deleted'], FILTER_VALIDATE_BOOLEAN); } // 树形结构参数处理(parent_id) if (isset($params['parent_id']) && $this instanceof TreeableInterface) { $params['parent_id'] = (int)$params['parent_id']; } // 搜索参数白名单:仅保留$search中定义的字段(防止无效参数注入) $allowedSearchFields = array_keys($this->search); $searchParams = array_intersect_key($params, array_flip($allowedSearchFields)); // 合并系统参数与合法搜索参数(排除其他未知参数) $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); } /** * 快捷创建. */ public static function createOne(array $data): Model|Builder|HyperfModel { return static::query()->create($data); } /** * 快捷更新. */ public static function updateById(int $id, array $data): int { $result = static::query()->where('id', $id)->update($data); // 主动清除缓存(query()->update() 不会触发模型事件) if ($result > 0) { try { $container = ApplicationContext::getContainer(); $manager = $container->get(Manager::class); $manager->destroy([$id], static::class); } catch (\Throwable $e) { // 缓存删除失败不影响主业务 } } return $result; } /** * 快捷删除指定ID的记录. * * @param int $id 要删除的记录ID * @return bool 成功删除返回true * @throws BusinessException 当记录不存在时抛出 * @throws Exception 当删除操作发生其他错误时抛出 */ public static function deleteById(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; } /** * 快捷查找. * @return Builder|Builder[]|Collection|HyperfModel * @throws Exception 当删除操作发生其他错误时抛出 * @throws BusinessException 当记录不存在时抛出 */ public static function findById(int $id, $with = []): array|Builder|Collection|HyperfModel { $record = static::query()->with($with)->find($id); if (is_null($record)) { throw new BusinessException(sprintf('找不到 ID 为 %d 的记录', $id), 404); } return $record; } /** * 快捷查找或失败. * @param int $id 要查找的记录ID * @return Model 根据ID查找记录,不存在则抛出异常 * @throws Exception 当删除操作发生其他错误时抛出 * @throws BusinessException 当记录不存在时抛出 */ public static function findOrFailById(int $id, $with = []): static { $record = static::query()->find($id); if (is_null($record)) { throw new BusinessException(sprintf('找不到 ID 为 %d 的记录', $id), 404); } 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; } /** * 获取器处理. */ 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 { try { $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)); } catch (\Throwable $e) { return []; } } 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); } } }