Compare commits

..

28 Commits

Author SHA1 Message Date
zj
6d3eeeca34 update 2026-09-15 14:58:50 +08:00
zj
e82f3275cf update 2026-08-20 15:03:52 +08:00
zj
e744b1a898 完善日志打印 2026-08-20 15:01:24 +08:00
zj
d6a9b93f29 update 2026-08-18 17:17:08 +08:00
zj
38a6f81b39 新增分页打包 2026-08-18 09:15:31 +08:00
zj
9e3c82086c 修复分页回调each问题 2026-08-13 10:14:14 +08:00
zj
b1f99153b2 新增获取页码和每页数量 2026-08-12 19:35:32 +08:00
堂吉诃德拉曼查的英豪
c0586c93cd Merge remote-tracking branch 'origin/main' 2026-08-12 15:18:49 +08:00
堂吉诃德拉曼查的英豪
1647ae803d 核心helpers内部日志改用LogHelper 2026-08-12 15:11:44 +08:00
zj
30e2cc5943 update 2026-08-11 11:27:23 +08:00
zj
ddc49e5b6a 新增Response类 2026-08-06 16:47:04 +08:00
zj
76dc5308a1 新增字节自动格式化易读单位 2026-08-06 15:53:59 +08:00
zj
ff7adab00d 新增:使用正则替换内容中的图片路径公共方法 2026-08-06 15:47:46 +08:00
堂吉诃德拉曼查的英豪
64aec80889 异常处理器兼容ModelNotFoundException返回404 2026-08-06 15:34:13 +08:00
堂吉诃德拉曼查的英豪
454139ac83 核心库新增EnumTrait 2026-08-06 15:28:07 +08:00
堂吉诃德拉曼查的英豪
2bdfafe32b Model补search默认属性兼容Aether模型 2026-08-06 14:06:08 +08:00
堂吉诃德拉曼查的英豪
afe9a663b8 Logger支持通道与级别,LogHelper统一委托Logger 2026-08-06 13:51:42 +08:00
堂吉诃德拉曼查的英豪
977a42d40c 核心库新增公共helpers与LogHelper 2026-08-06 11:37:04 +08:00
堂吉诃德拉曼查的英豪
dd2b32cc95 核心库补齐Aether兼容方法与树形Trait,中间件复用RPC上下文 2026-08-06 09:26:01 +08:00
zj
8ca756d0ee 新增公共函数:简单日志记录 2026-08-06 09:05:21 +08:00
zj
b90552553b 新增比较APP版本号方法 2026-08-05 17:30:01 +08:00
zj
d371031dac 增加公共方法 2026-08-05 17:03:22 +08:00
zj
e5dd2b81f1 update 2026-08-05 15:24:46 +08:00
zj
30c531afbb update 2026-08-05 15:21:14 +08:00
zj
40e82ca161 update 2026-08-05 15:19:14 +08:00
zj
74315314e9 新增文件域名相关函数 2026-08-05 15:07:07 +08:00
zj
6e1799dfa0 update 2026-07-31 10:25:17 +08:00
zj
0e3e01cfa0 update 2026-07-31 09:07:29 +08:00
15 changed files with 984 additions and 54 deletions

View File

@@ -30,6 +30,9 @@
"autoload": {
"psr-4": {
"Dhd\\Micro\\Core\\": "src/"
}
},
"files": [
"src/helpers.php"
]
}
}

View File

@@ -17,6 +17,7 @@ use Hyperf\HttpServer\Contract\RequestInterface;
use Hyperf\RpcClient\Exception\RequestException;
use Dhd\Micro\Core\Exception\BusinessException;
use Dhd\Micro\Core\Exception\ValidateException;
use Dhd\Micro\Core\Trait\RpcContextTrait;
@@ -31,24 +32,21 @@ class GatewayExceptionHandler extends ExceptionHandler
$this->stopPropagation(); // 阻止异常冒泡
$traceId = $this->getTraceId();
$isError = !($throwable instanceof BusinessException)
&& !($throwable instanceof ValidateException)
&& !($throwable instanceof RequestException);
$code = 500;
$message = '服务器内部错误';
if (($throwable instanceof BusinessException)) {
$code = $throwable->getCode();
$message = $throwable->getMessage();
if ($isError) {
$code = 500;
$message = '服务器内部错误';
} else {
$code = $throwable->getCode() ?: 500;
$message = $throwable->getMessage() ?: '服务异常,请稍后重试';
$code = $throwable->getCode() ?? 500;
$message = $throwable->getMessage() ?? '服务器内部错误';
}
$request = Util::get(RequestInterface::class);
Util::logger()
->setErrorStatus(
!($throwable instanceof BusinessException)
&& !($throwable instanceof RequestException)
)
->setErrorStatus($isError)
->setMessage($message)
->setThrowable($throwable)
->setTraceId($traceId)

View File

@@ -18,6 +18,7 @@ use Hyperf\HttpMessage\Stream\SwooleStream;
use Hyperf\RpcClient\Exception\RequestException;
use Dhd\Micro\Core\Exception\BusinessException;
use Dhd\Micro\Core\Exception\ValidateException;
use Hyperf\Database\Model\ModelNotFoundException;
use Dhd\Micro\Core\Trait\RpcContextTrait;
@@ -36,9 +37,13 @@ class JsonRpcBusinessExceptionHandler extends ExceptionHandler
$id = $request ? $request->getAttribute('request_id', null) : null; // Hyperf 注入的 ID
$isError = !($throwable instanceof BusinessException)
&& !($throwable instanceof ValidateException)
&& !($throwable instanceof RequestException);
&& !($throwable instanceof RequestException)
&& !($throwable instanceof ModelNotFoundException);
if ($isError) {
if ($throwable instanceof ModelNotFoundException) {
$code = 404;
$message = $throwable->getMessage();
} elseif ($isError) {
$code = 500;
$message = '服务器内部错误';
} else {

65
src/Library/LogHelper.php Normal file
View File

@@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Dhd\Micro\Core\Library;
use Hyperf\Rpc\Context as RpcContext;
use Hyperf\Context\ApplicationContext;
/**
* 统一日志静态入口,内部委托核心 Logger 实现,支持 trace_id 注入与多通道.
*/
class LogHelper
{
public static function error(string $message, array $context = []): void
{
self::log('error', $message, $context, 'error');
}
public static function warning(string $message, array $context = []): void
{
self::log('warning', $message, $context);
}
public static function info(string $message, array $context = []): void
{
self::log('info', $message, $context);
}
public static function debug(string $message, array $context = []): void
{
self::log('debug', $message, $context);
}
public static function business(string $message, array $context = []): void
{
self::log('info', $message, $context, 'business');
}
public static function sql(string $sql, array $bindings = [], float $time = 0): void
{
$message = sprintf('SQL: %s | Bindings: %s | Time: %.2fms', $sql, json_encode($bindings), $time);
self::log('debug', $message, [], 'sql');
}
protected static function log(string $level, string $message, array $context = [], string $channel = 'default'): void
{
Util::logger()
->setChannel($channel)
->setLevel($level)
->setMessage($message)
->setContext($context)
->setTraceId(self::getTraceId())
->record();
}
protected static function getTraceId(): ?string
{
try {
return ApplicationContext::getContainer()->get(RpcContext::class)->get('trace_id');
} catch (Throwable $e) {
return null;
}
}
}

View File

@@ -17,9 +17,11 @@ class Logger
private string $message = '';
private ?array $data = null;
private ?Throwable $throwable = null;
private string $traceId = '';
private ?string $traceId = '';
private array $context = [];
private bool $isError = false;
private string $channel = 'default';
private ?string $level = null;
public function setMessage(string $message): static
{
@@ -39,7 +41,7 @@ class Logger
return $this;
}
public function setTraceId(string $traceId): static
public function setTraceId(?string $traceId): static
{
$this->traceId = $traceId;
return $this;
@@ -62,10 +64,28 @@ class Logger
return $this;
}
/**
* 设置日志通道(对应 logger.php 的 group,如 default/error/business/sql).
*/
public function setChannel(string $channel): static
{
$this->channel = $channel;
return $this;
}
/**
* 设置日志级别(debug/info/warning/error).
*/
public function setLevel(?string $level): static
{
$this->level = $level;
return $this;
}
public function record(): void
{
$fatory = ApplicationContext::getContainer()->get(LoggerFactory::class);
$logger = $fatory->get('log', 'default');
$logger = $fatory->get('log', $this->channel);
$content = [
'message' => $this->message,
'timestamp' => date('Y-m-d H:i:s'),
@@ -76,20 +96,20 @@ class Logger
if ($this->throwable !== null) {
$throwable = $this->throwable;
$previous = $throwable->getPrevious();
$content['throwable'] = [
'message' => $throwable->getMessage(),
'code' => $throwable->getCode(),
'file' => $throwable->getFile(),
'line' => $throwable->getLine(),
'trace' => $this->isError ? $throwable->getTrace() : null,
'previous_trace' => $previous ? $previous->getTrace() : null,
'previous_message' => $previous ? $previous->getMessage() : null,
'data' => method_exists($throwable, 'getData') ? $throwable->getData() : null,
];
}
if ($this->isError) {
$logger->error(json_encode($content, JSON_UNESCAPED_UNICODE));
} else {
$logger->info(json_encode($content, JSON_UNESCAPED_UNICODE));
}
$level = $this->level ?? ($this->isError ? 'error' : 'info');
$logger->log($level, json_encode($content, JSON_UNESCAPED_UNICODE));
}
}

View File

@@ -12,6 +12,8 @@ use Hyperf\Context\ApplicationContext;
use Hyperf\Collection\Arr;
use Hyperf\Carbon\Carbon;
use Dhd\Micro\Core\Library\Logger;
use Hyperf\Contract\LengthAwarePaginatorInterface;
use Hyperf\Paginator\AbstractPaginator;
final class Util
{
@@ -25,6 +27,17 @@ final class Util
return ApplicationContext::getContainer()->get($name);
}
/**
* 获取nacos配置
* @param string $key 键
* @param mixed $default 默认值
* @return mixed
*/
public static function getNacosConfig(string $key, $default = null)
{
return config('nacos_config.' . $key, $default);
}
/**
* 获取日志记录器
*/
@@ -33,6 +46,22 @@ final class Util
return new Logger();
}
/**
* 简单日志记录
* @param string $message 日志信息
* @param array|null $data 日志数据
* @return void
*/
public static function simpleLog(string $message, ?array $data = null): void
{
$logger = self::logger();
$logger->setMessage($message);
if ($data !== null) {
$logger->setData($data);
}
$logger->record();
}
/**
* 从原始数据中提取指定字段的值,并可进行键名映射、设置默认值和字符串裁剪
*
@@ -189,11 +218,11 @@ final class Util
/**
* 将数组的值赋值给对象,指定某些字段
* @param type $obj 对象实例
* @param object $obj 对象实例
* @param array $params 源数据数组,['k1'=>'v1']
* @param array $only 指定的数组字段,['k1','k2']
*/
public static function setObjectValue($obj, array $params, array $only = [])
public static function setObjectValue(object $obj, array $params, array $only = [])
{
foreach ($params as $key => $value) {
if (!empty($only) && !in_array($key, $only)) {
@@ -205,11 +234,11 @@ final class Util
/**
* 将数组的值赋值给对象,排除某些字段
* @param type $obj 对象实例
* @param object $obj 对象实例
* @param array $params 源数据数组,['k1'=>'v1']
* @param array $except 排除的数组字段,['k1','k2']
*/
public static function setObjectValueExcept($obj, array $params, array $except = [])
public static function setObjectValueExcept(object $obj, array $params, array $except = [])
{
foreach ($params as $key => $value) {
if (!in_array($key, $except)) {
@@ -269,4 +298,167 @@ final class Util
}
return false;
}
/**
* 获取文件中心域名
* @return string
*/
public static function getFileDomainFromNacos(): string
{
return self::getNacosConfig('file_domain', '');
}
/**
* 为内容中的图片路径添加文件域名
* @param string|array|null $content 内容
* @param bool $simple 是否简单处理
* @param string|null $fileDomain 文件域名
* @return array|string|null
*/
public static function addFileDomain(array|string|null $content, bool $simple = false, ?string $fileDomain = null): array|string|null
{
if (empty($content)) {
return $content;
}
$fileDomain = $fileDomain ?? self::getFileDomainFromNacos() . '/';
// 简单处理
if ($simple) {
return is_string($content) ? $fileDomain . ltrim($content, '/') : $content;
}
$content = preg_replace_callback('/<img([^>]*src\s*=\s*["\'])([^"\']*)(["\'][^>]*)>/i', function ($matches) use ($fileDomain) {
$fullMatch = $matches[0];
$prefix = $matches[1];
$src = $matches[2];
$suffix = $matches[3];
$newSrc = $fileDomain . $src;
return '<img' . $prefix . $newSrc . $suffix . '>';
}, $content);
return $content;
}
/**
* 移除内容中图片的文件域名
* @param string|array|null $content 内容
* @param string|null $fileDomain 文件域名
* @return array|string
*/
public static function removeFileDomain(array|string|null $content, ?string $fileDomain = null): array|string
{
if (empty($content)) {
return $content;
}
$fileDomain = $fileDomain ?? self::getFileDomainFromNacos() . '/';
return str_replace($fileDomain, '', $content);
}
/**
* 使用正则替换内容中的图片路径
* @param array|string $content 内容
* @param string|null $oldDomain 旧域名
* @param string $search 搜索字符串
* @param string $replace 替换字符串
* @return array|string
*/
public static function replaceImgDomainPreg(array|string $content, ?string $oldDomain, string $search = '', string $replace = ''): array
{
if (empty($content)) {
return $content;
}
$content = self::removeFileDomain($content, $oldDomain);
$filePath = [];
$content = preg_replace_callback('/<img([^>]*src\s*=\s*["\'])([^"\']*)(["\'][^>]*)>/i', function ($matches) use ($search, $replace, &$filePath) {
$prefix = $matches[1];
$src = $matches[2];
$suffix = $matches[3];
if (!empty($search)) {
$newSrc = str_replace($search, $replace, $src);
} else {
$newSrc = $src;
}
$filePath[] = [
'old' => $src,
'new' => $newSrc
];
return '<img' . $prefix . $newSrc . $suffix . '>';
}, $content);
return [$content, $filePath];
}
/**
* 字节自动格式化易读单位
* @param float $bytes
* @return string
*/
public static function prettyBytes(float $bytes): string
{
$step = pow(1024, 4);
if ($bytes >= $step) {
return round($bytes / $step, 2) . 'TB';
}
$step = pow(1024, 3);
if ($bytes >= $step) {
return round($bytes / $step, 2) . 'GB';
}
$step = pow(1024, 2);
if ($bytes >= $step) {
return round($bytes / $step, 2) . 'MB';
}
$step = 1024;
return round($bytes / $step, 2) . 'KB';
}
/**
* 获取分页参数中的页码
* @param array $params
* @return int
*/
public static function getPageFromParams(array $params): int
{
$page = (int)Arr::get($params, 'page', 1) ?: 1;
return max($page, 1);
}
/**
* 获取分页参数中的每页数量
* @param array $params
* @return int
*/
public static function getPageSizeFromParams(array $params, int $maxSize = 100): int
{
$pageSize = (int)Arr::get(
$params,
'size',
Arr::get(
$params,
'page_size',
Arr::get($params, 'pageSize', 15)
)
) ?: 15;
return min(max($pageSize, 1), $maxSize);
}
/**
* 分页打包
* @param LengthAwarePaginatorInterface $paginator 分页对象
* @return array
*/
public static function handlePage(LengthAwarePaginatorInterface $paginator): array
{
if ($paginator instanceof AbstractPaginator) {
$items = $paginator->getCollection();
} else {
$items = Collection::make($paginator->items());
}
return [
'list' => $items->toArray(),
'total' => $paginator->total(),
];
}
}

View File

@@ -22,13 +22,15 @@ class RequestContextMiddleware implements MiddlewareInterface
{
$rpcContext = Util::get(RpcContext::class);
$traceId = $request->getHeaderLine('X-Trace-Id')
?: uniqid('t-', true);
$traceId = $rpcContext->get('trace_id');
if (empty($traceId)) {
$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') ?: '';
$appVersion = $rpcContext->get('app_version') ?: $request->getHeaderLine('X-App-Version') ?: '';
Context::set('app_version', $appVersion);
$rpcContext->set('app_version', $appVersion);
@@ -36,7 +38,7 @@ class RequestContextMiddleware implements MiddlewareInterface
// APP: app
// 微信小程序: wx_mini
// 官网: web_official
$platformName = $request->getHeaderLine('X-Platform-Name') ?: '';
$platformName = $rpcContext->get('platform_name') ?: $request->getHeaderLine('X-Platform-Name') ?: '';
Context::set('platform_name', $platformName);
$rpcContext->set('platform_name', $platformName);

View File

@@ -45,6 +45,11 @@ abstract class Model extends HyperfModel implements CacheableInterface
protected array $append = [];
/**
* 搜索器规则配置.
*/
protected array $search = [];
/**
* 排序配置:
* - false: 禁用排序
@@ -119,13 +124,7 @@ abstract class Model extends HyperfModel implements CacheableInterface
}
$this->applySearch($query, $params);
if ($this->sortable) {
$sortConfig = $this->getSortConfig();
if ($sortConfig) {
$query->orderBy($sortConfig['field'], $sortConfig['direction']);
}
}
$this->applyOrdering($query, $params);
$withDeleted = filter_var($params['with_deleted'] ?? false, FILTER_VALIDATE_BOOLEAN);
if ($withDeleted) {
@@ -143,7 +142,7 @@ abstract class Model extends HyperfModel implements CacheableInterface
$size = max(1, min(100, $size));
$result = $query->paginate($size, ['*'], 'page', $page);
if (is_callable($eachCallback) && method_exists($result, 'each')) {
if (is_callable($eachCallback)) {
$result->each($eachCallback);
}
return [
@@ -208,12 +207,48 @@ abstract class Model extends HyperfModel implements CacheableInterface
$searchParams = array_intersect_key($params, array_flip($allowedSearchFields));
// 合并系统参数与合法搜索参数(排除其他未知参数)
$systemParams = ['page', 'size', 'with_deleted', 'parent_id'];
$systemParams = ['page', 'size', 'with_deleted', 'parent_id', 'order_by', 'order_direction'];
$validSystemParams = array_intersect_key($params, array_flip($systemParams));
return array_merge($validSystemParams, $searchParams);
}
/**
* 应用排序逻辑,支持自定义 order_by/order_direction.
*/
protected function applyOrdering(Builder $query, array $params): void
{
$orderField = $params['order_by'] ?? null;
$orderDirection = strtolower((string) ($params['order_direction'] ?? 'asc'));
if (is_string($orderField) && $this->isValidOrderField($orderField)) {
$direction = $orderDirection === 'desc' ? 'desc' : 'asc';
$query->orderBy($orderField, $direction);
if ($orderField !== 'id' && ! str_contains($orderField, '.')) {
$query->orderBy('id', 'asc');
}
return;
}
if ($this->sortable) {
$sortConfig = $this->getSortConfig();
if ($sortConfig) {
$query->orderBy($sortConfig['field'], $sortConfig['direction']);
if ($sortConfig['field'] !== 'id' && ! str_contains($sortConfig['field'], '.')) {
$query->orderBy('id', 'asc');
}
}
}
}
/**
* 校验排序字段,避免 SQL 注入.
*/
protected function isValidOrderField(string $field): bool
{
return (bool) preg_match('/^[a-zA-Z0-9_\.]+$/', $field);
}
/**
* 快捷创建.
*/
@@ -290,6 +325,60 @@ abstract class Model extends HyperfModel implements CacheableInterface
return $record;
}
/**
* 查找记录,不存在时抛出业务异常.
*/
public static function findOrThrow($id, string $message = '数据不存在'): static
{
$record = static::query()->find($id);
if (is_null($record)) {
throw new BusinessException($message, 404);
}
return $record;
}
/**
* Aether 兼容:创建记录并返回 ID.
*/
public static function createBy(array $data): int
{
return static::query()->create($data)->id;
}
/**
* Aether 兼容:按主键查找,不存在时抛出业务异常.
*/
public static function findByPk(int $id): array|Builder|Collection|HyperfModel
{
$record = static::query()->find($id);
if (is_null($record)) {
throw new BusinessException(sprintf('找不到 ID 为 %d 的记录', $id), 404);
}
return $record;
}
/**
* Aether 兼容:按主键更新.
*/
public static function updateByPk(int $id, array $data): bool
{
if (! static::query()->where('id', $id)->exists()) {
throw new BusinessException(sprintf('找不到 ID 为 %d 的记录', $id), 404);
}
return static::query()->where('id', $id)->update($data) > 0;
}
/**
* Aether 兼容:按主键删除.
*/
public static function deleteByPk(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;
}
/**
* 获取器处理.
*/

69
src/Response.php Normal file
View File

@@ -0,0 +1,69 @@
<?php
declare(strict_types=1);
namespace Dhd\Micro\Core;
class Response
{
/**
* 成功响应.
* @param null|mixed $data 数据
* @param string $message 消息
*/
public static function success(mixed $data = null, string $message = 'ok'): array
{
return [
'data' => $data,
'code' => 200,
'message' => $message ?: 'success',
];
}
/**
* 错误响应.
* @param int $code 错误码
* @param string $message 错误消息
* @param null|mixed $data 附加数据
*/
public static function error(string $message = 'fail', int $code = -1, mixed $data = null): array
{
return [
'code' => $code,
'data' => $data,
'message' => $message ?: self::getDefaultMessage($code),
];
}
public static function page($list, int $total, int $page, int $size): array
{
return [
'code' => 200,
'message' => 'ok',
'list' => [
'list' => $list,
'page' => $page,
'size' => $size,
'total' => $total,
'pages' => (int) ceil($total / $size),
],
];
}
/**
* 获取默认错误消息.
*/
private static function getDefaultMessage(int $code): string
{
$messages = [
400 => '请求参数错误',
401 => '未授权',
403 => '禁止访问',
404 => '资源不存在',
500 => '服务器内部错误',
10001 => '校区不存在',
];
return $messages[$code] ?? '未知错误';
}
}

View File

@@ -18,6 +18,7 @@ use Dhd\Micro\Core\Exception\BusinessException;
use Throwable;
use Dhd\Micro\Core\Validator;
use Hyperf\DbConnection\Db;
use Dhd\Micro\Core\Library\Util;
/**
* @template T of Model
@@ -97,8 +98,8 @@ abstract class Service
*/
public function list(array $params = []): array
{
$model = $this->repository->getQuery();
$query = $model->newQuery();
$model = $this->repository->getModel();
$query = $this->repository->getQuery();
// 通过模型配置自动应用所有搜索条件
$this->applySearch($query, $params);
@@ -149,8 +150,7 @@ abstract class Service
*/
public function update(int $id, array $data): bool
{
$model = $this->repository->getQuery();
$resource = $model->findById($id);
$resource = $this->findById($id);
$this->checkResourceExists($resource);
// 数据验证
@@ -169,8 +169,7 @@ abstract class Service
*/
public function delete(int $id): bool
{
$model = $this->repository->getQuery();
$resource = $model->findById($id);
$resource = $this->findById($id);
$this->checkResourceExists($resource);
@@ -186,9 +185,8 @@ abstract class Service
*/
public function restore(int $id): bool
{
$model = $this->repository->getQuery();
// 必须使用withTrashed()才能查询到已删除记录
$resource = $model->withTrashed()->find($id);
$resource = $this->repository->getQuery()->withTrashed()->find($id);
$this->checkResourceExists($resource, '恢复的资源不存在');
$result = $this->repository->getQuery()->withTrashed()
@@ -206,8 +204,7 @@ abstract class Service
throw new BusinessException('请选择要删除的记录', 400);
}
$model = $this->repository->getQuery();
$exists = $model->whereIn('id', $ids)->exists();
$exists = $this->repository->getQuery()->whereIn('id', $ids)->exists();
if (! $exists) {
throw new BusinessException('部分记录不存在', 404);
}
@@ -228,8 +225,8 @@ abstract class Service
throw new BusinessException('请选择要恢复的记录', 400);
}
$model = $this->repository->getQuery();
$exists = $model->withTrashed()
$exists = $this->repository->getQuery()
->withTrashed()
->whereIn('id', $ids)
->whereNotNull('deleted_at')
->exists();
@@ -251,8 +248,7 @@ abstract class Service
*/
public function forceDelete(int $id): bool
{
$model = $this->repository->getQuery();
$resource = $model->withTrashed()->find($id);
$resource = $this->repository->getQuery()->withTrashed()->find($id);
$this->checkResourceExists($resource, '要删除的资源不存在');
$result = $this->repository->getQuery()->withTrashed()
@@ -366,4 +362,34 @@ abstract class Service
{
return Db::transaction($callback);
}
/**
* 将数组的值赋值给对象,指定某些字段
* @param object $obj 对象实例
* @param array $params 源数据数组,['k1'=>'v1']
* @param array $only 指定的数组字段,['k1','k2']
*/
protected function setObjectValue(object $obj, array $params, array $only = [])
{
Util::setObjectValue($obj, $params, $only);
}
/**
* 将数组的值赋值给对象,排除某些字段
* @param object $obj 对象实例
* @param array $params 源数据数组,['k1'=>'v1']
* @param array $except 排除的数组字段,['k1','k2']
*/
protected function setObjectValueExcept(object $obj, array $params, array $except = [])
{
Util::setObjectValueExcept($obj, $params, $except);
}
/**
* @return null|T
*/
public function findByFilter(array $params): mixed
{
return $this->repository->findByFilter($params);
}
}

126
src/Trait/EnumTrait.php Normal file
View File

@@ -0,0 +1,126 @@
<?php
declare(strict_types=1);
namespace Dhd\Micro\Core\Trait;
use InvalidArgumentException;
use ReflectionClass;
/**
* PHP 原生枚举辅助方法(values/descriptions/valueMap/fromValue 等).
*/
trait EnumTrait
{
public static function values(): array
{
self::validateEnumStructure();
$values = [];
foreach (self::cases() as $case) {
$values[] = $case->value;
}
return $values;
}
public static function descriptions(): array
{
self::validateEnumStructure();
$descriptions = [];
foreach (self::cases() as $case) {
$descriptions[] = $case->description();
}
return $descriptions;
}
public static function valueMap(): array
{
self::validateEnumStructure();
$map = [];
foreach (self::cases() as $case) {
$map[$case->value] = $case->description();
}
return $map;
}
public static function fromValue(int|string $value): self
{
self::validateEnumStructure();
$firstCase = self::cases()[0] ?? null;
if ($firstCase) {
$expectedType = gettype($firstCase->value);
$actualType = gettype($value);
if ($expectedType !== $actualType) {
throw new InvalidArgumentException(sprintf(
'枚举值类型不匹配,%s期望%s类型,实际为%s',
self::class,
$expectedType,
$actualType
));
}
}
$enum = self::tryFrom($value);
if (! $enum) {
throw new InvalidArgumentException(sprintf(
'无效的%s值: %s,允许值: %s',
self::class,
$value,
implode(', ', self::values())
));
}
return $enum;
}
public static function fromDescription(string $description): ?self
{
self::validateEnumStructure();
foreach (self::cases() as $case) {
if ($case->description() === $description) {
return $case;
}
}
return null;
}
public static function isValidValue(int|string $value): bool
{
self::validateEnumStructure();
foreach (self::cases() as $case) {
if ($case->value === $value) {
return true;
}
}
return false;
}
private static function validateEnumStructure(): void
{
if (! (new ReflectionClass(self::class))->isEnum()) {
throw new InvalidArgumentException(sprintf(
'EnumTrait仅允许枚举类使用,%s不是枚举',
self::class
));
}
if (! method_exists(self::class, 'description')) {
throw new InvalidArgumentException(sprintf(
'枚举类%s必须实现description()方法(返回字符串描述)',
self::class
));
}
$sampleCase = self::cases()[0] ?? null;
if ($sampleCase && ! is_string($sampleCase->description())) {
throw new InvalidArgumentException(sprintf(
'枚举类%s的description()方法必须返回字符串',
self::class
));
}
}
}

View File

@@ -10,6 +10,7 @@ namespace Dhd\Micro\Core\Trait;
use Hyperf\Rpc\Context as RpcContext;
use Dhd\Micro\Core\Library\Util;
use Hyperf\Collection\Arr;
trait RpcContextTrait
{
@@ -36,4 +37,38 @@ trait RpcContextTrait
{
return Util::get(RpcContext::class)->get('platform_name');
}
/**
* 与APP当前版本比较版本号大小,APP版本在方法中自动获取
* @param string $version 要比较的版本号
* @return int ---------------------------------------------------返回 -1、0 或 1,分别表示 请求的版本 小于、等于或大于 $version
*/
public function compareVersion(string $version): int
{
$appVersion = $this->getAppVersion();
if (empty($appVersion)) {
return -1;
}
$v1Parts = explode('.', $appVersion);
$v2Parts = explode('.', $version);
// 获取最大长度以进行逐位比较
$maxLength = max(count($v1Parts), count($v2Parts));
for ($i = 0; $i < $maxLength; $i++) {
$num1 = intval(Arr::get($v1Parts, $i, 0));
$num2 = intval(Arr::get($v2Parts, $i, 0));
if ($num1 < $num2) {
return -1;
} elseif ($num1 > $num2) {
return 1;
}
// 如果相等,继续比较下一位
}
// 所有部分都相等
return 0;
}
}

130
src/Trait/TreeTrait.php Normal file
View File

@@ -0,0 +1,130 @@
<?php
declare(strict_types=1);
namespace Dhd\Micro\Core\Trait;
use Dhd\Micro\Core\Model;
use Hyperf\Database\Model\Collection;
use LogicException;
trait TreeTrait
{
public function __construct(array $attributes = [])
{
parent::__construct($attributes);
if (! $this instanceof Model) {
throw new LogicException(
'使用TreeTrait的类必须继承Dhd\Micro\Core\Model,当前类: ' . get_class($this)
);
}
}
/**
* 构建树形结构.
*/
public static function buildTree($items, int $parentId = 0): array
{
$self = new static();
$parentField = $self->getParentIdField();
$sortField = $self->getSortField();
$items = $items instanceof Collection ? $items->toArray() : $items;
$tree = [];
foreach ($items as $item) {
if ($item[$parentField] == $parentId) {
$children = static::buildTree($items, $item['id']);
if (! empty($children)) {
$item['children'] = $children;
}
$tree[] = $item;
}
}
$self->sortTreeItems($tree, $sortField);
return $tree;
}
/**
* 获取指定节点的所有子节点ID.
*/
public function getChildIds(int $id): array
{
$parentField = $this->getParentIdField();
$allItems = $this->newQuery()->get(['id', $parentField])->toArray();
$ids = [$id];
$this->collectChildIds($allItems, $id, $parentField, $ids);
return $ids;
}
/**
* 获取节点的完整路径.
*/
public function getPath(int $id): array
{
$parentField = $this->getParentIdField();
$node = $this->newQuery()->find($id);
if (! $node) {
return [];
}
$path = [$node->toArray()];
$parentId = $node[$parentField];
while ($parentId > 0) {
$parent = $this->newQuery()->find($parentId);
if (! $parent) {
break;
}
array_unshift($path, $parent->toArray());
$parentId = $parent[$parentField];
}
return $path;
}
/**
* 获取父ID字段名(子类实现).
*/
abstract protected function getParentIdField(): string;
/**
* 获取排序字段名(子类实现).
*/
abstract protected function getSortField(): string;
/**
* 树形节点排序.
*/
protected function sortTreeItems(array &$items, string $sortField): void
{
if (empty($sortField)) {
return;
}
usort($items, function ($a, $b) use ($sortField) {
$direction = $this->treeSortDirection ?? 'asc';
$aValue = $a[$sortField] ?? 0;
$bValue = $b[$sortField] ?? 0;
return $direction === 'desc'
? $bValue <=> $aValue
: $aValue <=> $bValue;
});
}
/**
* 递归收集子节点ID.
*/
private function collectChildIds(array $items, int $parentId, string $parentField, array &$ids): void
{
foreach ($items as $item) {
if ($item[$parentField] == $parentId) {
$ids[] = $item['id'];
$this->collectChildIds($items, $item['id'], $parentField, $ids);
}
}
}
}

View File

@@ -14,6 +14,7 @@ use Hyperf\Di\Annotation\Inject;
use Hyperf\Validation\Contract\ValidatorFactoryInterface;
use Hyperf\Validation\Validator as HyperfValidator;
use RuntimeException;
use Hyperf\Collection\Arr;
abstract class Validator
{
@@ -128,6 +129,17 @@ abstract class Validator
}
}
/**
* 获取待验证数据中指定键的值.
* 如果不存在该键,则返回null.
* @param string $key 待获取的键名
* @return mixed 键对应的值
*/
protected function getDataByKey(string $key): mixed
{
return Arr::get($this->data, $key);
}
/**
* 定义场景验证规则(子类实现).
*/

158
src/helpers.php Normal file
View File

@@ -0,0 +1,158 @@
<?php
declare(strict_types=1);
use Hyperf\Context\ApplicationContext;
use Hyperf\Contract\StdoutLoggerInterface;
use Hyperf\Logger\LoggerFactory;
use Dhd\Micro\Core\Library\LogHelper;
use function Hyperf\Config\config;
if (! function_exists('getNacosConfig')) {
/**
* 获取nacos配置.
*/
function getNacosConfig(string $key, $default = null)
{
return config('nacos_config.' . $key, $default);
}
}
if (! function_exists('get_file_domain')) {
/**
* 获取文件中心域名.
*/
function get_file_domain()
{
return getNacosConfig('file_domain', '');
}
}
if (! function_exists('c_get')) {
function c_get($name)
{
return ApplicationContext::getContainer()->get($name);
}
}
if (! function_exists('f_log')) {
/**
* 程序日志.
*/
function f_log(string $msg, mixed $data)
{
$fatory = ApplicationContext::getContainer()->get(LoggerFactory::class);
$logger = $fatory->get('log', 'default');
$content = [
'msg' => $msg,
'data' => $data,
];
$logger->info(json_encode($content, JSON_UNESCAPED_UNICODE));
}
}
if (! function_exists('f_debug')) {
/**
* debug日志.
*/
function f_debug($msg)
{
$logger = ApplicationContext::getContainer()->get(StdoutLoggerInterface::class);
$logger->info((is_array($msg) || is_object($msg)) ? json_encode($msg, JSON_UNESCAPED_UNICODE) : $msg);
}
}
if (! function_exists('add_file_domain')) {
/**
* 为内容中的图片路径添加文件域名.
*/
function add_file_domain($content, $simple = false, $fileDomain = null)
{
if (empty($content)) {
return $content;
}
$fileDomain = $fileDomain ?? get_file_domain() . '/';
if ($simple) {
if (is_string($content) && preg_match('#^(?:https?:)?//#i', $content)) {
return $content;
}
return $fileDomain . ltrim($content, '/');
}
$content = preg_replace_callback('/<img([^>]*src\s*=\s*["\'])([^"\']*)(["\'][^>]*)>/i', function ($matches) use ($fileDomain) {
$prefix = $matches[1];
$src = $matches[2];
$suffix = $matches[3];
if ($src === '' || preg_match('#^(?:https?:)?//#i', $src) || str_starts_with($src, 'data:')) {
return $matches[0];
}
$newSrc = rtrim($fileDomain, '/') . '/' . ltrim($src, '/');
return '<img' . $prefix . $newSrc . $suffix . '>';
}, $content);
return $content;
}
}
if (! function_exists('remove_file_domain')) {
/**
* 移除内容中图片的文件域名.
*/
function remove_file_domain($content, $fileDomain = null)
{
if (empty($content)) {
return $content;
}
$fileDomain = $fileDomain ?? get_file_domain();
$fileDomain = $fileDomain ? rtrim($fileDomain, '/') : '';
if ($fileDomain === '') {
return $content;
}
$pattern = '#' . preg_quote($fileDomain, '#') . '/+#';
return preg_replace($pattern, '', $content);
}
}
if (! function_exists('copy_file_safely')) {
/**
* 复制文件到指定路径,若目标目录不存在则递归创建.
*/
function copy_file_safely($sourceFile, $destinationFile)
{
$safeLog = function ($msg, $data) {
try {
LogHelper::error($msg, ['data' => $data]);
} catch (Throwable $e) {
// 忽略日志错误
}
};
if (! file_exists($sourceFile)) {
$safeLog('源文件不存在', $sourceFile);
return false;
}
$destinationDir = dirname($destinationFile);
if (! is_dir($destinationDir)) {
if (! mkdir($destinationDir, 0755, true)) {
$safeLog('无法创建目标目录', $destinationDir);
return false;
}
}
if (! is_writable($destinationDir)) {
$safeLog('目标目录不可写', $destinationDir);
return false;
}
if (copy($sourceFile, $destinationFile)) {
return true;
}
$error = error_get_last();
$safeLog('文件复制失败', $error['message'] ?? '未知错误');
return false;
}
}