update
This commit is contained in:
24
src/Contract/TreeableInterface.php
Normal file
24
src/Contract/TreeableInterface.php
Normal 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;
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
46
src/Middleware/RequestContextMiddleware.php
Normal file
46
src/Middleware/RequestContextMiddleware.php
Normal 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);
|
||||
}
|
||||
}
|
||||
526
src/Model.php
526
src/Model.php
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
368
src/Service.php
368
src/Service.php
@@ -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
135
src/Validator.php
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user