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

@@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Dhd\Micro\Core\Contract;
/**
* 树形结构接口,标识模型支持树形功能.
*/
interface TreeableInterface
{
/**
* 构建树形结构.
* @param array $items 原始数据
* @param int $parentId 根节点ID
*/
public static function buildTree(array $items, int $parentId = 0): array;
/**
* 获取子节点ID集合.
* @param int $id 节点ID
*/
public function getChildIds(int $id): array;
}

View File

@@ -21,31 +21,31 @@ class Logger
private array $context = [];
private bool $isError = false;
public function setMessage(string $message): self
public function setMessage(string $message): static
{
$this->message = $message;
return $this;
}
public function setData(array $data): self
public function setData(array $data): static
{
$this->data = $data;
return $this;
}
public function setThrowable(Throwable $throwable): self
public function setThrowable(Throwable $throwable): static
{
$this->throwable = $throwable;
return $this;
}
public function setTraceId(string $traceId): self
public function setTraceId(string $traceId): static
{
$this->traceId = $traceId;
return $this;
}
public function setContext(array $context): self
public function setContext(array $context): static
{
$this->context = $context;
return $this;
@@ -56,7 +56,7 @@ class Logger
return $this->isError;
}
public function setErrorStatus(bool $isError): self
public function setErrorStatus(bool $isError): static
{
$this->isError = $isError;
return $this;

View File

@@ -1,28 +0,0 @@
<?php
/**
* jsonrpc中间件
*/
declare(strict_types=1);
namespace Dhd\Micro\Core\Middleware;
use Hyperf\Rpc\Context as RpcContext;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Dhd\Micro\Core\Library\Util;
class JsonRpcMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$traceId = $request->getHeaderLine('X-Trace-Id')
?: uniqid('', true) . bin2hex(random_bytes(2));
Util::get(RpcContext::class)->set('trace_id', $traceId);
return $handler->handle($request);
}
}

View File

@@ -0,0 +1,46 @@
<?php
/**
* http中间件
*/
declare(strict_types=1);
namespace Dhd\Micro\Core\Middleware;
use Hyperf\Context\Context;
use Hyperf\Rpc\Context as RpcContext;
use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Server\RequestHandlerInterface;
use Dhd\Micro\Core\Library\Util;
class RequestContextMiddleware implements MiddlewareInterface
{
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
$rpcContext = Util::get(RpcContext::class);
$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') ?: '';
Context::set('app_version', $appVersion);
$rpcContext->set('app_version', $appVersion);
//平台名称
// APP: app
// 微信小程序: wx_mini
// 官网: web_official
$platformName = $request->getHeaderLine('X-Platform-Name') ?: '';
Context::set('platform_name', $platformName);
$rpcContext->set('platform_name', $platformName);
return $handler->handle($request);
}
}

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

View File

@@ -0,0 +1,150 @@
<?php
/**
* 数据库操作基类
*/
declare(strict_types=1);
namespace Dhd\Micro\Core;
use Hyperf\Collection\Collection;
use Hyperf\Contract\LengthAwarePaginatorInterface;
use Hyperf\Database\Model\Builder;
use Hyperf\DbConnection\Model\Model;
use Hyperf\Paginator\AbstractPaginator;
/**
* @template T of Model
* @property T $model
*/
abstract class Repository
{
public const PER_PAGE_PARAM_NAME = 'per_page';
public function handleSearch(Builder $query, array $params): Builder
{
return $query;
}
public function handleItems(Collection $items): Collection
{
return $items;
}
public function handlePage(LengthAwarePaginatorInterface $paginator): array
{
if ($paginator instanceof AbstractPaginator) {
$items = $paginator->getCollection();
} else {
$items = Collection::make($paginator->items());
}
$items = $this->handleItems($items);
return [
'list' => $items->toArray(),
'total' => $paginator->total(),
];
}
public function list(array $params = []): Collection
{
return $this->handleItems($this->perQuery($this->getQuery(), $params)->get());
}
public function count(array $params = []): int
{
return $this->perQuery($this->getQuery(), $params)->count();
}
public function page(array $params = [], ?int $page = null, ?int $pageSize = null): array
{
$result = $this->perQuery($this->getQuery(), $params)->paginate(
perPage: $pageSize,
pageName: static::PER_PAGE_PARAM_NAME,
page: $page,
);
return $this->handlePage($result);
}
/**
* @return T
*/
public function create(array $data): mixed
{
// @phpstan-ignore-next-line
return $this->getQuery()->create($data);
}
public function updateById(mixed $id, array $data): bool
{
return (bool) $this->getQuery()->whereKey($id)->first()?->update($data);
}
/**
* @return null|T
*/
public function saveById(mixed $id, array $data): mixed
{
$model = $this->getQuery()->whereKey($id)->first();
if ($model) {
$model->fill($data)->save();
return $model;
}
return null;
}
public function deleteById(mixed $id): int
{
// @phpstan-ignore-next-line
return $this->model::destroy($id);
}
public function forceDeleteById(mixed $id): bool
{
return (bool) $this->getQuery()->whereKey($id)->forceDelete();
}
/**
* @return null|T
*/
public function findById(mixed $id): mixed
{
return $this->getQuery()->whereKey($id)->first();
}
public function findByField(mixed $id, string $field): mixed
{
return $this->getQuery()->whereKey($id)->value($field);
}
/**
* @return null|T
*/
public function findByFilter(array $params): mixed
{
return $this->perQuery($this->getQuery(), $params)->first();
}
public function perQuery(Builder $query, array $params): Builder
{
return $this->handleSearch($query, $params);
}
public function getQuery(): Builder
{
return $this->model->newQuery();
}
public function existsById(mixed $id): bool
{
return (bool) $this->getQuery()->whereKey($id)->exists();
}
/**
* @return T
*/
public function getModel()
{
return $this->model;
}
}

View File

@@ -0,0 +1,368 @@
<?php
/**
* 服务基类
*/
declare(strict_types=1);
namespace Dhd\Micro\Core;
use Dhd\Micro\Core\Repository;
use Hyperf\Collection\Collection;
use Hyperf\DbConnection\Model\Model;
use Exception;
use Hyperf\Database\Model\Builder;
use Dhd\Micro\Core\Contract\TreeableInterface;
use Dhd\Micro\Core\Exception\BusinessException;
use Throwable;
use Dhd\Micro\Core\Validator;
use Hyperf\DbConnection\Db;
/**
* @template T of Model
* @property Repository<T> $repository
* @property Validator<T> $validator
*/
abstract class Service
{
protected array $search = [];
protected array $ignoreSearchFields = [];
public function count(array $params): int
{
return $this->repository->count($params);
}
public function page(array $params, int $page = 1, int $pageSize = 10): array
{
return $this->repository->page($params, $page, $pageSize);
}
public function getList(array $paras): Collection
{
return $this->repository->list($paras);
}
/**
* @return T
*/
public function create(array $data): mixed
{
$this->validator->scene('create', $data)->check();
return $this->repository->create($data);
}
/**
* @return T
*/
public function save(array $data): mixed
{
$this->validator->scene('create', $data)->check();
return $this->create($data);
}
/**
* @return null|mixed|T
*/
public function updateById(mixed $id, array $data): mixed
{
// 数据验证
$this->validator->scene('update', $data)->check();
return $this->repository->updateById($id, $data);
}
public function deleteById(mixed $id): int
{
return $this->repository->deleteById($id);
}
/**
* @return null|T
*/
public function findById(mixed $id): mixed
{
return $this->repository->findById($id);
}
public function existsById(mixed $id): bool
{
return $this->repository->existsById($id);
}
/**
* 通用列表查询(支持分页和树形结构).
*/
public function list(array $params = []): array
{
$model = $this->repository->getQuery();
$query = $model->newQuery();
// 通过模型配置自动应用所有搜索条件
$this->applySearch($query, $params);
// 动态应用排序
$sortConfig = $model->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)); // 限制最大页大小为100
$result = $query->paginate($size, ['*'], 'page', $page);
return [
'list' => $result->items(),
'page' => $page,
'size' => $size,
'total' => $result->total(),
'pages' => $result->lastPage()
];
}
// 无分页参数时返回完整数据集合
$items = $query->get()->toArray();
// 若模型支持树形结构则构建树形,否则返回普通数组
if ($model instanceof TreeableInterface) {
return $model::buildTree($items, (int) ($params['parent_id'] ?? 0));
}
return $items;
}
/**
* 通用更新逻辑.
* @throws BusinessException|Throwable
*/
public function update(int $id, array $data): bool
{
$model = $this->repository->getQuery();
$resource = $model->findById($id);
$this->checkResourceExists($resource);
// 数据验证
$this->validator->scene('update', $data)->check();
// 钩子:处理更新时的特殊逻辑(如禁止自身为父级)
$this->handleUpdateSpecialLogic($id, $data);
return $this->repository->getModel()
->updateById($id, $data) > 0;
}
/**
* 通用删除逻辑.
* @throws BusinessException|Throwable
*/
public function delete(int $id): bool
{
$model = $this->repository->getQuery();
$resource = $model->findById($id);
$this->checkResourceExists($resource);
// 钩子:删除前检查(如子级存在性)
$this->checkChildrenBeforeDelete($id);
return $this->repository->getModel()->deleteById($id);
}
/**
* 软删除恢复.
* @throws BusinessException|Throwable
*/
public function restore(int $id): bool
{
$model = $this->repository->getQuery();
// 必须使用withTrashed()才能查询到已删除记录
$resource = $model->withTrashed()->find($id);
$this->checkResourceExists($resource, '恢复的资源不存在');
$result = $this->repository->getQuery()->withTrashed()
->where('id', $id)->restore();
return $result;
}
/**
* 批量软删除.
* @throws BusinessException|Throwable
*/
public function batchDelete(array $ids): bool
{
if (empty($ids)) {
throw new BusinessException('请选择要删除的记录', 400);
}
$model = $this->repository->getQuery();
$exists = $model->whereIn('id', $ids)->exists();
if (! $exists) {
throw new BusinessException('部分记录不存在', 404);
}
return $this->transaction(function () use ($ids) {
$result = $this->repository->getQuery()->whereIn('id', $ids)->delete();
return $result > 0;
});
}
/**
* 批量恢复软删除.
* @throws BusinessException|Throwable
*/
public function batchRestore(array $ids): bool
{
if (empty($ids)) {
throw new BusinessException('请选择要恢复的记录', 400);
}
$model = $this->repository->getQuery();
$exists = $model->withTrashed()
->whereIn('id', $ids)
->whereNotNull('deleted_at')
->exists();
if (! $exists) {
throw new BusinessException('部分记录不存在或未被删除', 404);
}
return $this->transaction(function () use ($ids) {
$result = $this->repository->getQuery()->withTrashed()
->whereIn('id', $ids)->restore();
return $result > 0;
});
}
/**
* 永久删除(物理删除).
* @throws BusinessException|Throwable
*/
public function forceDelete(int $id): bool
{
$model = $this->repository->getQuery();
$resource = $model->withTrashed()->find($id);
$this->checkResourceExists($resource, '要删除的资源不存在');
$result = $this->repository->getQuery()->withTrashed()
->where('id', $id)->forceDelete();
return $result;
}
protected function getSearch(): array
{
return $this->search;
}
protected function getIgnoreSearchFields(): array
{
return $this->ignoreSearchFields;
}
/**
* 根据模型的$search配置,自动应用搜索条件到查询构建器.
*/
public function applySearch(Builder $query, array $params): void
{
foreach ($this->search as $field => $rule) {
// 跳过未传递的参数
if (! isset($params[$field])) {
continue;
}
$value = $params[$field];
$this->applySearchRule($query, $field, $value, $rule);
}
}
/**
* 应用单个搜索规则.
* @param mixed $value
* @param mixed $rule
*/
protected function applySearchRule(Builder $query, string $field, $value, $rule): void
{
// 处理规则格式(支持字符串简写或数组配置)
$config = is_array($rule) ? $rule : ['type' => $rule];
$type = $config['type'];
switch ($type) {
case '=': // 精确匹配
$query->where($field, $value);
break;
case 'like': // 模糊匹配
$query->where($field, 'like', "%{$value}%");
break;
case 'between': // 范围查询(支持数组或两个参数)
$values = is_array($value) ? $value : [$value, $params[$field . '_end'] ?? $value];
$query->whereBetween($field, $values);
break;
case 'callback': // 自定义回调
if (isset($config['handler']) && is_callable($config['handler'])) {
call_user_func($config['handler'], $query, $value);
}
break;
// 可扩展其他类型:in、>、< 等
}
}
/**
* 通用详情查询.
* @throws Exception
*/
public function detail(int $id): array
{
return $this->repository->getQuery()->findOrFailById($id)->toArray();
}
/**
* 检查资源是否存在(不存在则抛出异常).
* @throws BusinessException
*/
protected function checkResourceExists(?object $resource, string $message = '资源不存在'): void
{
if (empty($resource)) {
throw new BusinessException($message, 404);
}
}
/**
* 钩子方法:更新时的特殊逻辑(子类可重写).
*/
protected function handleUpdateSpecialLogic(int $id, array &$data): void
{
// 通用逻辑:禁止将自身设为父级(适用于有parent_id的场景)
if (isset($data['parent_id']) && $data['parent_id'] == $id) {
throw new BusinessException('不能将自身设为父级', 400);
}
}
/**
* 钩子方法:删除前检查子级(子类可重写).
*/
protected function checkChildrenBeforeDelete(int $id): void
{
// 默认不检查,需要的子类重写
}
/**
* 事务处理.
* @throws Throwable
*/
protected function transaction(callable $callback)
{
return Db::transaction($callback);
}
}

135
src/Validator.php Normal file
View File

@@ -0,0 +1,135 @@
<?php
/**
* 验证器基类
*/
declare(strict_types=1);
namespace Dhd\Micro\Core;
use Dhd\Micro\Core\Exception\ValidateException;
use Hyperf\Context\ApplicationContext;
use Hyperf\Di\Annotation\Inject;
use Hyperf\Validation\Contract\ValidatorFactoryInterface;
use Hyperf\Validation\Validator as HyperfValidator;
use RuntimeException;
abstract class Validator
{
/**
* 当前场景名.
*/
public ?string $currentScene = null;
#[Inject]
protected ValidatorFactoryInterface $validationFactory;
/**
* 待验证数据.
*/
protected array $data = [];
/**
* 自定义验证规则(子类可通过该属性注册,无需重写registerRules)
* 格式:['规则名' => 闭包/类方法].
*/
protected array $customRules = [];
/**
* 静态快捷验证方法(简化调用).
*/
public static function validate(string $scene, array $data = []): array
{
$instance = ApplicationContext::getContainer()->get(static::class);
return $instance->scene($scene, $data)->check();
}
/**
* 设置验证场景和数据(支持链式调用).
*/
public function scene(string $scene, array $data = []): self
{
$this->currentScene = $scene;
$this->data = $data;
return $this;
}
/**
* 执行验证(失败抛出异常).
*/
public function check(): array
{
if (empty($this->currentScene)) {
throw new RuntimeException('请先设置验证场景');
}
$scenes = $this->scenes();
if (! isset($scenes[$this->currentScene])) {
return $this->data;
}
$sceneConfig = $scenes[$this->currentScene];
return $this->validateData(
$this->data,
$sceneConfig['rules'],
$sceneConfig['messages'] ?? [],
$sceneConfig['attributes'] ?? []
);
}
/**
* 格式化验证错误信息(统一格式,供异常处理器复用).
*/
public function formatValidationErrors(HyperfValidator $validator): array
{
$errors = [];
$failedRules = $validator->failed();
$errorMessages = $validator->errors()->getMessages();
$attributes = $validator->attributes();
foreach ($failedRules as $field => $rules) {
$errors[] = [
'field' => $field,
'field_label' => $attributes[$field] ?? $field,
'message' => $errorMessages[$field][0] ?? '',
'rules' => array_keys($rules),
'value' => $validator->getValue($field),
];
}
return $errors;
}
/**
* 实际执行验证的逻辑(重命名方法名更清晰).
*/
protected function validateData(array $data, array $rules, array $messages = [], array $attributes = []): array
{
$validator = $this->validationFactory->make($data, $rules, $messages, $attributes);
$this->registerRules($validator);
if ($validator->fails()) {
throw new ValidateException(
$validator->errors()->first(),
);
}
return $validator->validated();
}
/**
* 自动注册自定义规则(优先使用$customRules属性).
*/
protected function registerRules(HyperfValidator $validator): void
{
foreach ($this->customRules as $ruleName => $rule) {
$validator->extend($ruleName, $rule);
}
}
/**
* 定义场景验证规则(子类实现).
*/
abstract protected function scenes(): array;
}