617 lines
18 KiB
PHP
617 lines
18 KiB
PHP
<?php
|
||
|
||
declare(strict_types=1);
|
||
|
||
namespace Aether;
|
||
|
||
use Aether\Contract\TreeableInterface;
|
||
use Closure;
|
||
use DateTime;
|
||
use Exception;
|
||
use Hyperf\Context\ApplicationContext;
|
||
use Hyperf\Contract\LengthAwarePaginatorInterface;
|
||
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 = '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 = $this->newQuery();
|
||
//
|
||
// // 通过模型配置自动应用所有搜索条件
|
||
// $this->applySearch($query, $params);
|
||
//
|
||
// // 动态应用排序
|
||
// $sortConfig = $this->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));
|
||
// $result = $query->paginate($size, ['*'], 'page', $page);
|
||
// return [
|
||
// 'total' => $result->total(),
|
||
// 'list' => $result->items(),
|
||
// ];
|
||
// }
|
||
//
|
||
// // 无分页参数时返回完整数据集合
|
||
// $items = $query->get()->toArray();
|
||
//
|
||
// // 若模型支持树形结构则构建树形,否则返回普通数组
|
||
// if ($this instanceof TreeableInterface) {
|
||
// return $this::buildTree($items, (int) ($params['parent_id'] ?? 0));
|
||
// }
|
||
//
|
||
// return $items;
|
||
// }
|
||
|
||
/**
|
||
* 列表查询.
|
||
*/
|
||
public function list(array $params = []): array
|
||
{
|
||
$query = $this->buildQueryFromParams($params);
|
||
return $this->listResult($query, $params);
|
||
}
|
||
|
||
/**
|
||
* 快捷创建.
|
||
*/
|
||
public static function createOne(array $data): AetherModel|Builder|HyperfModel
|
||
{
|
||
return static::query()->create($data);
|
||
}
|
||
|
||
/**
|
||
* 快捷更新.
|
||
*/
|
||
public static function updateById(int $id, array $data): bool
|
||
{
|
||
return static::query()->where('id', $id)->update($data) > 0;
|
||
}
|
||
|
||
/**
|
||
* 快捷删除指定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查找记录,不存在则抛出异常
|
||
* 根据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 static function getList(
|
||
array $conditions = [],
|
||
array $columns = ['*'],
|
||
array $orders = []
|
||
): Collection {
|
||
$query = static::buildQuery($conditions, $orders);
|
||
return $query->get($columns);
|
||
}
|
||
|
||
/**
|
||
* 分页查询列表.
|
||
*/
|
||
public static function getPageList(
|
||
array $conditions = [],
|
||
int $page = 1,
|
||
int $pageSize = 10,
|
||
array $columns = ['*'],
|
||
array $orderBy = []
|
||
): LengthAwarePaginatorInterface {
|
||
// 直接通过静态方法链构建查询
|
||
$query = static::query();
|
||
|
||
// 应用条件
|
||
foreach ($conditions as $field => $value) {
|
||
$query->where($field, $value);
|
||
}
|
||
|
||
// 应用排序
|
||
foreach ($orderBy as $field => $direction) {
|
||
$query->orderBy($field, $direction);
|
||
}
|
||
|
||
// 执行分页
|
||
return $query->paginate($pageSize, $columns, 'page', $page);
|
||
}
|
||
|
||
/**
|
||
* 获取器处理.
|
||
*/
|
||
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 buildQueryFromParams(array $params = []): Builder
|
||
{
|
||
// 创建查询构建器
|
||
$query = static::query();
|
||
|
||
// 应用搜索条件
|
||
// if (isset($params['search'])) {
|
||
// $this->applySearch($query, $params['search']);
|
||
// }
|
||
$this->applySearch($query, $params);
|
||
// 应用排序
|
||
$this->applySorting($query, $params);
|
||
|
||
// 处理软删除
|
||
if (isset($params['withTrashed']) && $params['withTrashed']) {
|
||
$query->withTrashed();
|
||
}
|
||
|
||
return $query;
|
||
}
|
||
|
||
/**
|
||
* 应用排序.
|
||
*/
|
||
protected function applySorting(Builder $query, array $params = []): void
|
||
{
|
||
// 优先使用传入的排序参数
|
||
if (isset($params['sort_field'], $params['sort_direction'])) {
|
||
$query->orderBy($params['sort_field'], $params['sort_direction']);
|
||
return;
|
||
}
|
||
|
||
// 使用模型配置的排序
|
||
$sortConfig = $this->getSortConfig();
|
||
if (! empty($sortConfig)) {
|
||
$query->orderBy($sortConfig['field'], $sortConfig['direction']);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 根据分页参数获取结果.
|
||
*/
|
||
protected function listResult(Builder $query, array $params = []): array
|
||
{
|
||
// 分页处理
|
||
if (isset($params['page'], $params['size'])) {
|
||
$page = max(1, (int) $params['page']);
|
||
$size = max(1, min(100, (int) $params['size']));
|
||
$result = $query->paginate($size, ['*'], 'page', $page);
|
||
|
||
return [
|
||
'list' => $result->items(),
|
||
'total' => $result->total(),
|
||
];
|
||
}
|
||
|
||
// 获取所有数据
|
||
$result = $query->get()->toArray();
|
||
|
||
// 如果实现了树结构接口,构建树
|
||
if ($this instanceof TreeableInterface) {
|
||
return $this->buildTree($result, $params['parent_id'] ?? 0);
|
||
}
|
||
|
||
return $result;
|
||
}
|
||
|
||
/**
|
||
* 初始化模型.
|
||
*/
|
||
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;
|
||
}
|
||
|
||
// 修正 buildQuery 方法,避免使用 new static()
|
||
protected static function buildQuery(array $conditions = [], array $orderBy = []): Builder
|
||
{
|
||
// 使用静态 query() 方法获取查询构建器
|
||
$query = static::query();
|
||
|
||
// 处理搜索条件
|
||
foreach ($conditions as $field => $value) {
|
||
$query->where($field, $value);
|
||
}
|
||
|
||
// 处理排序
|
||
foreach ($orderBy as $field => $direction) {
|
||
$query->orderBy($field, $direction);
|
||
}
|
||
|
||
return $query;
|
||
}
|
||
|
||
/**
|
||
* 应用搜索条件.
|
||
*/
|
||
protected function applySearch(Builder $query, array $conditions): void
|
||
{
|
||
foreach ($conditions as $field => $value) {
|
||
// 跳过非字符串的字段名(防止索引数组键导致的类型错误)
|
||
if (! is_string($field)) {
|
||
continue;
|
||
}
|
||
|
||
// 处理嵌套关系查询(如:user.name)
|
||
if (str_contains($field, '.')) {
|
||
[$relation, $relationField] = explode('.', $field, 2);
|
||
$query->whereHas($relation, function ($q) use ($relationField, $value) {
|
||
$q->where($relationField, $value);
|
||
});
|
||
continue;
|
||
}
|
||
|
||
// 检查是否有自定义搜索器方法
|
||
$searchMethod = 'search' . ucfirst($field);
|
||
if (method_exists($this, $searchMethod)) {
|
||
$this->{$searchMethod}($query, $value);
|
||
continue;
|
||
}
|
||
|
||
// 应用搜索规则配置
|
||
if (isset($this->search[$field])) {
|
||
$this->applySearchRule($query, $field, $value, $this->search[$field]);
|
||
continue;
|
||
}
|
||
|
||
// 默认精确匹配(仅对$search中允许的字段生效,因已通过白名单过滤)
|
||
$query->where($field, $value);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 应用搜索规则.
|
||
*/
|
||
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);
|
||
}
|
||
}
|
||
}
|