update
This commit is contained in:
41
src/Exception/BusinessException.php
Normal file
41
src/Exception/BusinessException.php
Normal file
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 业务异常类
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dhd\Micro\Core\Exception;
|
||||
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
class BusinessException extends RuntimeException
|
||||
{
|
||||
/**
|
||||
* @var int 错误码
|
||||
*/
|
||||
protected $code;
|
||||
|
||||
/**
|
||||
* @var array 额外数据
|
||||
*/
|
||||
protected array $data = [];
|
||||
|
||||
public function __construct(string $message = '', int $code = 500, array $data = [], ?Throwable $previous = null)
|
||||
{
|
||||
$this->code = $code;
|
||||
$this->data = $data;
|
||||
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取额外数据.
|
||||
*/
|
||||
public function getData(): array
|
||||
{
|
||||
return $this->data;
|
||||
}
|
||||
}
|
||||
64
src/Exception/Handler/GatewayExceptionHandler.php
Normal file
64
src/Exception/Handler/GatewayExceptionHandler.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 网关 异常处理器
|
||||
* 返回http的json格式
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dhd\Micro\Core\Exception\Handler;
|
||||
|
||||
use Hyperf\ExceptionHandler\ExceptionHandler;
|
||||
use Throwable;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Hyperf\HttpMessage\Stream\SwooleStream;
|
||||
|
||||
use Hyperf\JsonRpc\Exception\ClientException;
|
||||
use Dhd\Micro\Core\Exception\BusinessException;
|
||||
|
||||
use Dhd\Micro\Core\Trait\RpcContextTrait;
|
||||
|
||||
use Dhd\Micro\Core\Library\Util;
|
||||
|
||||
class GatewayExceptionHandler extends ExceptionHandler
|
||||
{
|
||||
use RpcContextTrait;
|
||||
|
||||
public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$this->stopPropagation(); // 阻止异常冒泡
|
||||
|
||||
$traceId = $this->getTraceId();
|
||||
|
||||
$code = 500;
|
||||
$message = '服务器内部错误';
|
||||
|
||||
if (($throwable instanceof BusinessException)) {
|
||||
Util::recordLog($throwable->getMessage(), $throwable, $traceId);
|
||||
$code = $throwable->getCode();
|
||||
$message = $throwable->getMessage();
|
||||
} else if ($throwable instanceof ClientException) {
|
||||
$code = $throwable->getCode() ?: 500;
|
||||
$message = $throwable->getMessage() ?: '下游服务异常';
|
||||
Util::recordLog($message, $throwable, $traceId);
|
||||
}
|
||||
|
||||
$responseData = [
|
||||
'code' => $code,
|
||||
'message' => $message,
|
||||
'data' => null,
|
||||
];
|
||||
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withHeader('X-Trace-Id', $traceId)
|
||||
->withStatus(200)
|
||||
->withBody(new SwooleStream(json_encode($responseData, JSON_UNESCAPED_UNICODE)));
|
||||
}
|
||||
|
||||
public function isValid(Throwable $throwable): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
60
src/Exception/Handler/JsonRpcBusinessExceptionHandler.php
Normal file
60
src/Exception/Handler/JsonRpcBusinessExceptionHandler.php
Normal file
@@ -0,0 +1,60 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* json-rpc 业务异常处理器
|
||||
* 返回 json-rpc 格式错误信息
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dhd\Micro\Core\Exception\Handler;
|
||||
|
||||
use Hyperf\ExceptionHandler\ExceptionHandler;
|
||||
use Throwable;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Psr\Http\Message\ServerRequestInterface;
|
||||
use Hyperf\HttpMessage\Stream\SwooleStream;
|
||||
|
||||
use Dhd\Micro\Core\Exception\BusinessException;
|
||||
|
||||
use Dhd\Micro\Core\Trait\RpcContextTrait;
|
||||
|
||||
use Dhd\Micro\Core\Library\Util;
|
||||
|
||||
class JsonRpcBusinessExceptionHandler extends ExceptionHandler
|
||||
{
|
||||
use RpcContextTrait;
|
||||
|
||||
public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$this->stopPropagation(); // 阻止异常冒泡
|
||||
|
||||
$traceId = $this->getTraceId();
|
||||
|
||||
$isError = !($throwable instanceof BusinessException);
|
||||
Util::recordLog($isError ? '服务器内部错误' : $throwable->getMessage(), $throwable, $traceId, $isError);
|
||||
|
||||
$request = Util::get(ServerRequestInterface::class);
|
||||
// 关键优化:从 Request 属性中动态获取 ID
|
||||
$id = $request ? $request->getAttribute('request_id', null) : null; // Hyperf 注入的 ID
|
||||
$responseData = [
|
||||
'jsonrpc' => '2.0',
|
||||
'id' => $id,
|
||||
'error' => [
|
||||
'code' => $isError ? 500 : $throwable->getCode(),
|
||||
'message' => $isError ? '服务器内部错误' : $throwable->getMessage(),
|
||||
'data' => method_exists($throwable, 'getData') ? $throwable->getData() : null,
|
||||
],
|
||||
];
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/json; charset=utf-8')
|
||||
->withHeader('X-Trace-Id', $traceId)
|
||||
->withStatus(200)
|
||||
->withBody(new SwooleStream(json_encode($responseData, JSON_UNESCAPED_UNICODE)));
|
||||
}
|
||||
|
||||
public function isValid(Throwable $throwable): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
300
src/Library/Util.php
Normal file
300
src/Library/Util.php
Normal file
@@ -0,0 +1,300 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 公共函数库
|
||||
*/
|
||||
|
||||
namespace Dhd\Micro\Core\Library;
|
||||
|
||||
use Hyperf\Context\ApplicationContext;
|
||||
use Hyperf\Collection\Arr;
|
||||
use Hyperf\Carbon\Carbon;
|
||||
use Hyperf\Logger\LoggerFactory;
|
||||
|
||||
final class Util
|
||||
{
|
||||
/**
|
||||
* 获取实例
|
||||
* @param string $name 类名
|
||||
* @return mixed
|
||||
*/
|
||||
public static function get(string $name): mixed
|
||||
{
|
||||
return ApplicationContext::getContainer()->get($name);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录日志信息
|
||||
*
|
||||
* 此方法使用Hyperf的LoggerFactory创建一个日志记录器,并将指定的消息和数据记录到日志中
|
||||
* 日志内容会被格式化为JSON字符串并以info级别记录
|
||||
*
|
||||
* @param string $msg 日志消息内容
|
||||
* @param mixed $data 需要记录的额外数据,可以是任意类型的数据
|
||||
* @param bool $isError 是否为异常日志,默认为false
|
||||
* @return void
|
||||
*
|
||||
* 示例:
|
||||
* Util::recordLog('用户登录', ['user_id' => 123, 'ip' => '192.168.1.1']);
|
||||
* // 这将在日志中记录类似 {"msg":"用户登录","data":{"user_id":123,"ip":"192.168.1.1"}} 的信息
|
||||
*/
|
||||
public static function recordLog(string $msg, mixed $data, string $traceId = '', bool $isError = false): void
|
||||
{
|
||||
$fatory = self::get(LoggerFactory::class);
|
||||
$logger = $fatory->get('log', 'default');
|
||||
$content = [
|
||||
'msg' => $msg,
|
||||
'trace_id' => $traceId,
|
||||
'data' => ($data instanceof \Throwable) ? [
|
||||
'message' => $data->getMessage(),
|
||||
'code' => $data->getCode(),
|
||||
'file' => $data->getFile(),
|
||||
'line' => $data->getLine(),
|
||||
'trace' => $isError ? $data->getTrace() : null,
|
||||
'data' => method_exists($data, 'getData') ? $data->getData() : null,
|
||||
] : $data,
|
||||
];
|
||||
if ($isError) {
|
||||
$logger->error(json_encode($content, JSON_UNESCAPED_UNICODE));
|
||||
} else {
|
||||
$logger->info(json_encode($content, JSON_UNESCAPED_UNICODE));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从原始数据中提取指定字段的值,并可进行键名映射、设置默认值和字符串裁剪
|
||||
*
|
||||
* @param array $originalData 原始数据数组
|
||||
* @param array $fields 字段配置数组,每个元素为一个字段的配置,格式如下:
|
||||
* [原键名, 默认值, 新键名, 是否裁剪(可选,默认true)]
|
||||
* - 原键名: 在原始数据中的键名
|
||||
* - 默认值: 如果原始数据中不存在该键,则使用此默认值
|
||||
* - 新键名: 提取后的新键名(可选,如果不填则使用原键名)
|
||||
* - 是否裁剪: 是否对字符串值进行trim操作(可选,默认true)
|
||||
* @return array 提取处理后的数据数组
|
||||
*
|
||||
* 示例:
|
||||
* $originalData = ['name' => ' test ', 'age' => 25, 'email' => 'test@example.com'];
|
||||
* $fields = [
|
||||
* ['name', '', 'new_name'], // 从'name'提取到'new_name',并裁剪空格
|
||||
* ['age', 0], // 从'age'提取,保持原键名
|
||||
* ['nonexistent', 'default_value'], // 键不存在,使用默认值
|
||||
* ['email', '', 'mail', false] // 从'email'提取到'mail',但不裁剪
|
||||
* ];
|
||||
*/
|
||||
public static function getParams(array $originalData, array $fields): array
|
||||
{
|
||||
$data = Arr::mapWithKeys($fields, static function ($field) use ($originalData) {
|
||||
if (!is_array($field)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$originalKey = Arr::get($field, 0); // 获取原始键名
|
||||
if (!$originalKey) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$value = Arr::get($originalData, $originalKey, Arr::get($field, 1)); // 获取原始数据中的值,如果不存在则使用默认值
|
||||
if (is_string($value) && Arr::get($field, 3, true)) { // 检查是否需要裁剪字符串
|
||||
$value = trim($value);
|
||||
}
|
||||
|
||||
$newKey = Arr::get($field, 2) ?: $originalKey; // 获取新键名,如果未指定则使用原始键名
|
||||
return [$newKey => $value]; // 返回新键值对
|
||||
});
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取数组最大嵌套深度(标准定义)
|
||||
*
|
||||
* @param mixed $arr 待检测的变量
|
||||
* @return int 深度值(空数组为 1,非数组为 0)
|
||||
*/
|
||||
public static function getArrayDepth(mixed $arr): int
|
||||
{
|
||||
if (!is_array($arr)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (empty($arr)) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
$stack = [[$arr, 1]]; // [当前数组, 当前层级]
|
||||
$maxDepth = 1;
|
||||
|
||||
while (!empty($stack)) {
|
||||
[$currentArray, $level] = array_pop($stack);
|
||||
$maxDepth = max($maxDepth, $level);
|
||||
|
||||
foreach ($currentArray as $value) {
|
||||
if (is_array($value)) {
|
||||
$stack[] = [$value, $level + 1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $maxDepth;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将扁平化的数组结构转换为树形结构
|
||||
*
|
||||
* 此方法通过指定的ID字段和父级ID字段,将一维数组转换为具有父子关系的多维树形结构
|
||||
* 支持自动生成子节点列表,如果根节点无法直接识别(没有id为0的项),则会尝试找出最顶层的节点作为根节点
|
||||
*
|
||||
* @param array $data 包含扁平化数据的数组,每个元素应包含id和parent_id字段
|
||||
* @param string $idKey 用于标识唯一性的字段名称,通常是主键字段名
|
||||
* @param string $parentIdKey 用于表示父级关系的字段名称,通常是外键字段名
|
||||
* @return array 转换后的树形结构数组
|
||||
*
|
||||
* 示例:
|
||||
* $data = [
|
||||
* ['id' => 1, 'name' => 'Parent 1', 'pid' => 0],
|
||||
* ['id' => 2, 'name' => 'Child 1', 'pid' => 1],
|
||||
* ['id' => 3, 'name' => 'Child 2', 'pid' => 1],
|
||||
* ['id' => 4, 'name' => 'Grandchild 1', 'pid' => 2]
|
||||
* ];
|
||||
* $tree = CustomUtil::generateTree($data, 'id', 'pid');
|
||||
*
|
||||
* 结果将是:
|
||||
* [
|
||||
* [
|
||||
* 'id' => 1,
|
||||
* 'name' => 'Parent 1',
|
||||
* 'pid' => 0,
|
||||
* 'child' => [
|
||||
* [
|
||||
* 'id' => 2,
|
||||
* 'name' => 'Child 1',
|
||||
* 'pid' => 1,
|
||||
* 'child' => [
|
||||
* // ...
|
||||
* ]
|
||||
* ],
|
||||
* // ...
|
||||
* ]
|
||||
* ]
|
||||
* ]
|
||||
*/
|
||||
public static function generateTree(array $data, string $idKey, string $parentIdKey): array
|
||||
{
|
||||
// 创建一个以父级ID为键的映射表,用于快速查找子节点
|
||||
$map = [];
|
||||
foreach ($data as &$item) {
|
||||
$map[$item[$parentIdKey]][] = &$item;
|
||||
}
|
||||
|
||||
// 遍历所有数据项,将子节点添加到父节点的child属性中
|
||||
foreach ($data as &$item) {
|
||||
if (isset($map[$item[$idKey]])) {
|
||||
$item['child'] = $map[$item[$idKey]];
|
||||
}
|
||||
}
|
||||
// 解除引用,避免潜在的内存泄漏或意外修改
|
||||
unset($item);
|
||||
|
||||
// 如果存在以0为键的项(通常代表顶级节点),则直接返回
|
||||
if (isset($map[0])) {
|
||||
return $map[0];
|
||||
} else {
|
||||
// 否则,寻找最深层级的节点作为根节点返回
|
||||
$depths = Arr::mapWithKeys($map, function ($item, $id) {
|
||||
return [$id => self::getArrayDepth($item)];
|
||||
});
|
||||
$maxDepth = max($depths);
|
||||
$parentIds = [];
|
||||
foreach ($depths as $parentId => $depth) {
|
||||
if ($depth === $maxDepth) {
|
||||
$parentIds[] = $parentId;
|
||||
}
|
||||
}
|
||||
return array_column(Arr::only($map, $parentIds), 0);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数组的值赋值给对象,指定某些字段
|
||||
* @param type $obj 对象实例
|
||||
* @param array $params 源数据数组,['k1'=>'v1']
|
||||
* @param array $only 指定的数组字段,['k1','k2']
|
||||
*/
|
||||
public static function setObjectValue($obj, array $params, array $only = [])
|
||||
{
|
||||
foreach ($params as $key => $value) {
|
||||
if (!empty($only) && !in_array($key, $only)) {
|
||||
continue;
|
||||
}
|
||||
$obj->$key = $value;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将数组的值赋值给对象,排除某些字段
|
||||
* @param type $obj 对象实例
|
||||
* @param array $params 源数据数组,['k1'=>'v1']
|
||||
* @param array $except 排除的数组字段,['k1','k2']
|
||||
*/
|
||||
public static function setObjectValueExcept($obj, array $params, array $except = [])
|
||||
{
|
||||
foreach ($params as $key => $value) {
|
||||
if (!in_array($key, $except)) {
|
||||
$obj->$key = $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 进行hash_hmac加密
|
||||
* @param string $config 待加密的字符串
|
||||
* @param string $key 密钥
|
||||
* @return string 加密后的字符串
|
||||
*/
|
||||
public static function hashHmac(string $config, string $key): string
|
||||
{
|
||||
$hmac = hash_hmac('md5', $config, $key, true); // true 返回原始二进制
|
||||
return base64_encode($hmac);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取下一天日期
|
||||
* @param string $date 日期
|
||||
* @return string
|
||||
*/
|
||||
public static function getNextDate(string $date): string
|
||||
{
|
||||
return Carbon::parse($date)->addDay()->format('Y-m-d');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取上一天日期
|
||||
* @param string $date 日期
|
||||
* @return string
|
||||
*/
|
||||
public static function getLastDate(string $date): string
|
||||
{
|
||||
return Carbon::parse($date)->subDay()->format('Y-m-d');
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断时间是否在时间段内
|
||||
* @param array $dateTimeData 时间段数据
|
||||
* @param string $dateTime 待判断的时间
|
||||
* @param string $startKey 开始时间字段名
|
||||
* @param string $endKey 结束时间字段名
|
||||
* @return bool
|
||||
*/
|
||||
public static function isInDateTime(array $dateTimeData, string $dateTime, string $startKey = 'start_date', string $endKey = 'end_date'): bool
|
||||
{
|
||||
foreach ($dateTimeData as $item) {
|
||||
$startDate = Arr::get($item, $startKey) ?? min($item);
|
||||
$endDate = Arr::get($item, $endKey) ?? max($item);
|
||||
if ($dateTime >= $startDate && $dateTime < $endDate) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
0
src/Middleware/HttpMiddleware.php
Normal file
0
src/Middleware/HttpMiddleware.php
Normal file
28
src/Middleware/JsonRpcMiddleware.php
Normal file
28
src/Middleware/JsonRpcMiddleware.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
38
src/Model.php
Normal file
38
src/Model.php
Normal file
@@ -0,0 +1,38 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 数据模型基类
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dhd\Micro\Core;
|
||||
|
||||
use Hyperf\DbConnection\Model\Model as HyperfModel;
|
||||
use Hyperf\ModelCache\Cacheable;
|
||||
use Hyperf\ModelCache\CacheableInterface;
|
||||
|
||||
abstract class Model extends HyperfModel implements CacheableInterface
|
||||
{
|
||||
use Cacheable;
|
||||
|
||||
/**
|
||||
* 时间戳格式.
|
||||
*/
|
||||
protected ?string $dateFormat = 'Y-m-d H:i:s';
|
||||
|
||||
/**
|
||||
* 隐藏字段.
|
||||
*/
|
||||
protected array $hidden = [
|
||||
'deleted_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* 排序配置:
|
||||
* - false: 禁用排序
|
||||
* - 字符串: 排序字段(默认升序)
|
||||
* - 数组: ['field' => '字段名', 'direction' => 'asc/desc'].
|
||||
*/
|
||||
protected array|bool|string $sortable = false; // 'sort'; // 默认按sort字段升序
|
||||
}
|
||||
0
src/Repository.php
Normal file
0
src/Repository.php
Normal file
0
src/Service.php
Normal file
0
src/Service.php
Normal file
39
src/Trait/RpcContextTrait.php
Normal file
39
src/Trait/RpcContextTrait.php
Normal file
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* RPC上下文内容
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dhd\Micro\Core\Trait;
|
||||
|
||||
use Hyperf\Rpc\Context as RpcContext;
|
||||
use Dhd\Micro\Core\Library\Util;
|
||||
|
||||
trait RpcContextTrait
|
||||
{
|
||||
/**
|
||||
* 获取traceId
|
||||
*/
|
||||
public function getTraceId(): ?string
|
||||
{
|
||||
return Util::get(RpcContext::class)->get('trace_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取app版本号
|
||||
*/
|
||||
public function getAppVersion(): ?string
|
||||
{
|
||||
return Util::get(RpcContext::class)->get('app_version');
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取平台名称
|
||||
*/
|
||||
public function getPlatformName(): ?string
|
||||
{
|
||||
return Util::get(RpcContext::class)->get('platform_name');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user