update
This commit is contained in:
@@ -22,7 +22,9 @@
|
||||
"psr/log": "^2.0 || ^3.0",
|
||||
"hyperf/helper": "^3.1",
|
||||
"hyperf/carbon": "^3.1",
|
||||
"hyperf/validation": "^3.1"
|
||||
"hyperf/validation": "^3.1",
|
||||
"hyperf/http-server": "^3.1",
|
||||
"hyperf/rpc-client": "^3.1"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
|
||||
253
src/Exception/Handler/ApiExceptionHandler.php
Normal file
253
src/Exception/Handler/ApiExceptionHandler.php
Normal file
@@ -0,0 +1,253 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 网关 异常处理器
|
||||
* 返回http的json格式
|
||||
*/
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Dhd\Micro\Core\Exception\Handler;
|
||||
|
||||
use Hyperf\ExceptionHandler\ExceptionHandler;
|
||||
use Throwable;
|
||||
use Hyperf\HttpServer\Contract\RequestInterface;
|
||||
use Psr\Http\Message\ResponseInterface;
|
||||
use Hyperf\HttpMessage\Stream\SwooleStream;
|
||||
use Hyperf\JsonRpc\ResponseBuilder;
|
||||
|
||||
use Hyperf\RpcClient\Exception\RequestException;
|
||||
use Hyperf\JsonRpc\Exception\ClientException;
|
||||
use Dhd\Micro\Core\Exception\BusinessException;
|
||||
|
||||
use Dhd\Micro\Core\Trait\RpcContextTrait;
|
||||
|
||||
use Dhd\Micro\Core\Library\Util;
|
||||
|
||||
class ApiExceptionHandler extends ExceptionHandler
|
||||
{
|
||||
use RpcContextTrait;
|
||||
|
||||
protected RequestInterface $request;
|
||||
protected string $traceId;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->request = Util::get(RequestInterface::class);
|
||||
$this->traceId = $this->getTraceId();
|
||||
}
|
||||
|
||||
public function handle(Throwable $throwable, ResponseInterface $response): ResponseInterface
|
||||
{
|
||||
$this->stopPropagation(); // 阻止异常冒泡
|
||||
|
||||
$logData = $this->buildLogData($throwable);
|
||||
|
||||
// 按异常类型记录日志(业务异常warning,系统异常error)
|
||||
$this->logException($throwable, $logData);
|
||||
|
||||
// 解析错误信息
|
||||
[$code, $message, $data] = $this->resolveErrorInfo($throwable);
|
||||
|
||||
// 根据请求类型返回对应格式的响应(HTTP/RPC)
|
||||
if ($this->isRpcRequest()) {
|
||||
return $this->buildRpcResponse($response, $code, $message, $data);
|
||||
}
|
||||
|
||||
return $this->buildHttpResponse($response, $code, $message, $data);
|
||||
}
|
||||
|
||||
public function isValid(Throwable $throwable): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建日志数据(包含完整上下文,生产环境脱敏)
|
||||
*/
|
||||
private function buildLogData(Throwable $throwable): array
|
||||
{
|
||||
$logData = [
|
||||
'trace_id' => $this->traceId,
|
||||
'type' => get_class($throwable),
|
||||
'message' => $throwable->getMessage(),
|
||||
'code' => $throwable->getCode(),
|
||||
'file' => $throwable->getFile(),
|
||||
'line' => $throwable->getLine(),
|
||||
'trace' => $throwable->getTraceAsString(),
|
||||
'context' => [
|
||||
'url' => (string)$this->request->getUri(),
|
||||
'method' => $this->request->getMethod(),
|
||||
'user_id' => $this->request->header('X-User-Id') ?? 'unknown',
|
||||
'timestamp' => date('Y-m-d H:i:s'),
|
||||
],
|
||||
];
|
||||
|
||||
// 生产环境脱敏敏感参数
|
||||
if (env('APP_ENV') === 'prod') {
|
||||
$logData['context']['params'] = $this->desensitizeParams($this->request->all());
|
||||
} else {
|
||||
$logData['context']['params'] = $this->request->all();
|
||||
}
|
||||
|
||||
return $logData;
|
||||
}
|
||||
|
||||
/**
|
||||
* 按异常类型记录日志
|
||||
*/
|
||||
private function logException(Throwable $throwable, array $logData): void
|
||||
{
|
||||
if ($throwable instanceof BusinessException) {
|
||||
Util::recordLog($throwable->getMessage(), $logData, $this->traceId);
|
||||
} else {
|
||||
// 系统异常额外标记级别,便于日志告警
|
||||
Util::recordLog('系统异常', $logData, $this->traceId);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析错误信息(区分环境和异常类型)
|
||||
*/
|
||||
private function resolveErrorInfo(Throwable $throwable): array
|
||||
{
|
||||
// 业务异常:直接使用其错误码和消息
|
||||
if ($throwable instanceof BusinessException) {
|
||||
return [
|
||||
$throwable->getCode(),
|
||||
$throwable->getMessage(),
|
||||
$throwable->getData() ?? [],
|
||||
];
|
||||
}
|
||||
|
||||
// 微服务 JSON-RPC 调用异常:按 code 是否为系统异常保留值(-32000)区分业务错误与系统错误,
|
||||
// 不要求业务码必须落在 400-499(各微服务有自己的业务状态码,不是 HTTP 状态码)
|
||||
if ($throwable instanceof RequestException) {
|
||||
$code = $throwable->getCode();
|
||||
if ($code !== ResponseBuilder::SERVER_ERROR) {
|
||||
return [$code, $throwable->getMessage(), []];
|
||||
}
|
||||
$isDev = env('APP_ENV') === 'dev';
|
||||
return [500, $isDev ? $throwable->getMessage() : '服务异常,请稍后重试', []];
|
||||
}
|
||||
|
||||
// 系统异常:开发环境返回详细信息,生产环境返回通用消息
|
||||
$isDev = env('APP_ENV') === 'dev';
|
||||
$innerCode = $this->getInnerCode($throwable) ?? 500;
|
||||
|
||||
return [
|
||||
$innerCode,
|
||||
$throwable->getMessage() ?: '服务异常,请稍后重试',
|
||||
$isDev ? [
|
||||
'trace' => $throwable->getTraceAsString(),
|
||||
'file' => $throwable->getFile(),
|
||||
'line' => $throwable->getLine(),
|
||||
'trace_id' => $this->traceId, // 开发环境返回追踪ID便于调试
|
||||
] : ['trace_id' => $this->traceId], // 生产环境仅返回追踪ID供排查
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为RPC请求(如JSON-RPC)
|
||||
*/
|
||||
private function isRpcRequest(): bool
|
||||
{
|
||||
$contentType = $this->request->getHeaderLine('Content-Type');
|
||||
$path = $this->request->getUri()->getPath();
|
||||
|
||||
// 适配常见RPC场景:Content-Type为json且路径包含/rpc
|
||||
return str_contains($contentType, 'application/json')
|
||||
&& str_contains($path, '/rpc');
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建RPC响应(JSON-RPC 2.0规范)
|
||||
*/
|
||||
private function buildRpcResponse(ResponseInterface $response, int $code, string $message, array $data): ResponseInterface
|
||||
{
|
||||
$requestBody = $this->request->getBody()->getContents();
|
||||
$requestData = json_decode($requestBody, true) ?? [];
|
||||
$rpcId = $requestData['id'] ?? null;
|
||||
|
||||
$responseData = [
|
||||
'jsonrpc' => '2.0',
|
||||
'error' => [
|
||||
'code' => $code,
|
||||
'data' => $data,
|
||||
'message' => $message,
|
||||
],
|
||||
'id' => $rpcId,
|
||||
];
|
||||
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withHeader('X-Trace-Id', $this->traceId) // 透传追踪ID
|
||||
->withStatus(200) // RPC错误必须返回200状态码
|
||||
->withBody(new SwooleStream(json_encode($responseData, JSON_UNESCAPED_UNICODE)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建HTTP响应(通用JSON格式)
|
||||
*/
|
||||
private function buildHttpResponse(ResponseInterface $response, int $code, string $message, array $data): ResponseInterface
|
||||
{
|
||||
// HTTP状态码映射:业务码本身就是合法 HTTP 状态码时按原样用,
|
||||
// 否则统一回落 200(body 里带真实业务码,与本项目 success()/error() 助手函数约定一致),
|
||||
// 避免把非法状态码(如各服务自定义的业务码)发给真实 HTTP 客户端
|
||||
$statusCode = ($code >= 100 && $code <= 599) ? $code : 200;
|
||||
|
||||
$responseData = [
|
||||
'code' => $code,
|
||||
'data' => $data,
|
||||
'message' => $message,
|
||||
];
|
||||
|
||||
return $response
|
||||
->withHeader('Content-Type', 'application/json')
|
||||
->withHeader('X-Trace-Id', $this->traceId)
|
||||
->withStatus($statusCode)
|
||||
->withBody(new SwooleStream(json_encode($responseData, JSON_UNESCAPED_UNICODE)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 脱敏敏感参数(生产环境用)
|
||||
*/
|
||||
private function desensitizeParams(array $params): array
|
||||
{
|
||||
$sensitiveKeys = ['password', 'pwd', 'mobile', 'phone', 'id_card', 'credit_card'];
|
||||
foreach ($params as $key => &$value) {
|
||||
if (in_array(strtolower($key), $sensitiveKeys) && is_string($value)) {
|
||||
$value = $this->maskValue($value, $key);
|
||||
}
|
||||
}
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* 具体字段脱敏逻辑
|
||||
*/
|
||||
private function maskValue(string $value, string $key): string
|
||||
{
|
||||
return match (strtolower($key)) {
|
||||
'password', 'pwd' => '******',
|
||||
'mobile', 'phone' => substr($value, 0, 3) . '****' . substr($value, -4),
|
||||
'id_card' => substr($value, 0, 6) . '********' . substr($value, -4),
|
||||
default => substr($value, 0, 2) . '****' . substr($value, -2),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内部异常错误码
|
||||
*/
|
||||
private function getInnerCode(Throwable $throwable): ?int
|
||||
{
|
||||
if (method_exists($throwable, 'getThrowable') && is_array($inner = $throwable->getThrowable())) {
|
||||
return $inner['code'] ?? null;
|
||||
}
|
||||
// 兼容嵌套异常
|
||||
if ($throwable->getPrevious()) {
|
||||
return $throwable->getPrevious()->getCode() ?: null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
@@ -1,64 +0,0 @@
|
||||
<?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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user