Compare commits
28 Commits
d0acbeea51
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 6d3eeeca34 | |||
| e82f3275cf | |||
| e744b1a898 | |||
| d6a9b93f29 | |||
| 38a6f81b39 | |||
| 9e3c82086c | |||
| b1f99153b2 | |||
|
|
c0586c93cd | ||
|
|
1647ae803d | ||
| 30e2cc5943 | |||
| ddc49e5b6a | |||
| 76dc5308a1 | |||
| ff7adab00d | |||
|
|
64aec80889 | ||
|
|
454139ac83 | ||
|
|
2bdfafe32b | ||
|
|
afe9a663b8 | ||
|
|
977a42d40c | ||
|
|
dd2b32cc95 | ||
| 8ca756d0ee | |||
| b90552553b | |||
| d371031dac | |||
| e5dd2b81f1 | |||
| 30c531afbb | |||
| 40e82ca161 | |||
| 74315314e9 | |||
| 6e1799dfa0 | |||
| 0e3e01cfa0 |
@@ -30,6 +30,9 @@
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Dhd\\Micro\\Core\\": "src/"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"src/helpers.php"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
65
src/Library/LogHelper.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
107
src/Model.php
107
src/Model.php
@@ -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
69
src/Response.php
Normal 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] ?? '未知错误';
|
||||
}
|
||||
}
|
||||
@@ -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
126
src/Trait/EnumTrait.php
Normal 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
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
130
src/Trait/TreeTrait.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
158
src/helpers.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user