This commit is contained in:
2026-07-30 13:50:51 +08:00
parent b6e3ce97b4
commit 61b88bf45d
10 changed files with 1257 additions and 35 deletions

View File

@@ -11,6 +11,21 @@ namespace Dhd\Micro\Core;
use Hyperf\DbConnection\Model\Model as HyperfModel;
use Hyperf\ModelCache\Cacheable;
use Hyperf\ModelCache\CacheableInterface;
use Closure;
use DateTime;
use Exception;
use Hyperf\Context\ApplicationContext;
use Hyperf\Database\Model\Builder;
use Hyperf\Database\Model\Collection;
use Hyperf\DbConnection\Db;
use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\ModelCache\Manager;
use Dhd\Micro\Core\Contract\TreeableInterface;
use Psr\Container\ContainerExceptionInterface;
use Psr\Container\NotFoundExceptionInterface;
use Throwable;
use Dhd\Micro\Core\Exception\BusinessException;
abstract class Model extends HyperfModel implements CacheableInterface
{
@@ -35,4 +50,515 @@ abstract class Model extends HyperfModel implements CacheableInterface
* - 数组: ['field' => '字段名', '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);
if ($this->sortable) {
$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();
}
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'];
$validSystemParams = array_intersect_key($params, array_flip($systemParams));
return array_merge($validSystemParams, $searchParams);
}
/**
* 快捷创建.
*/
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 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);
}
}
}