Files
hyperf_micro_service_core_app/src/Model.php
2026-07-30 17:06:32 +08:00

567 lines
18 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
/**
* 数据模型基类
*/
declare(strict_types=1);
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
{
use Cacheable;
/**
* 时间戳格式.
*/
protected ?string $dateFormat = 'Y-m-d H:i:s';
/**
* 隐藏字段.
*/
protected array $hidden = [
'deleted_at',
];
protected array $append = [];
/**
* 排序配置:
* - false: 禁用排序
* - 字符串: 排序字段(默认升序)
* - 数组: ['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);
}
}
}