1.配置新版支付模块-菜单和接口都已重构
2.优化充值提现页面 3.菜单翻译问题 4.备份数据库
This commit is contained in:
232
app/common/middleware/InterfaceRequestLog.php
Normal file
232
app/common/middleware/InterfaceRequestLog.php
Normal file
@@ -0,0 +1,232 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\common\middleware;
|
||||
|
||||
use support\Log;
|
||||
use Throwable;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use Webman\MiddlewareInterface;
|
||||
|
||||
/**
|
||||
* 接口调用日志中间件
|
||||
*
|
||||
* 目标:
|
||||
* - 统一记录接口调用(请求 + 响应)
|
||||
* - 成功记录返回参数
|
||||
* - 失败记录失败原因
|
||||
*/
|
||||
class InterfaceRequestLog implements MiddlewareInterface
|
||||
{
|
||||
/**
|
||||
* 仅记录接口路由(避免静态资源噪音)
|
||||
*/
|
||||
private const API_PATH_PREFIXES = ['api/', 'admin/'];
|
||||
|
||||
/**
|
||||
* 需要脱敏的字段名(不区分大小写)
|
||||
*/
|
||||
private const SENSITIVE_KEYS = [
|
||||
'password',
|
||||
'oldpassword',
|
||||
'newpassword',
|
||||
'token',
|
||||
'user-token',
|
||||
'user_token',
|
||||
'auth-token',
|
||||
'auth_token',
|
||||
'refresh_token',
|
||||
'secret',
|
||||
'signature',
|
||||
'sign',
|
||||
];
|
||||
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$path = trim($request->path(), '/');
|
||||
if (!$this->shouldLogPath($path)) {
|
||||
return $handler($request);
|
||||
}
|
||||
|
||||
$start = microtime(true);
|
||||
$requestPayload = $this->buildRequestPayload($request);
|
||||
|
||||
try {
|
||||
$response = $handler($request);
|
||||
} catch (Throwable $e) {
|
||||
$costMs = $this->costMs($start);
|
||||
Log::error('[InterfaceRequestLog] ' . $this->encodeJson([
|
||||
'path' => '/' . $path,
|
||||
'method' => strtoupper($request->method()),
|
||||
'ip' => $request->getRealIp(),
|
||||
'cost_ms' => $costMs,
|
||||
'request' => $requestPayload,
|
||||
'success' => false,
|
||||
'fail_reason' => $e->getMessage(),
|
||||
'exception_class' => get_class($e),
|
||||
'exception_trace' => $this->truncateText($e->getTraceAsString(), 3000),
|
||||
]));
|
||||
throw $e;
|
||||
}
|
||||
|
||||
$costMs = $this->costMs($start);
|
||||
$statusCode = method_exists($response, 'getStatusCode') ? intval($response->getStatusCode()) : 200;
|
||||
$responseBodyRaw = $this->extractResponseBody($response);
|
||||
$responseDecoded = $this->decodeJsonArray($responseBodyRaw);
|
||||
|
||||
$success = false;
|
||||
$failReason = '';
|
||||
$responseData = null;
|
||||
|
||||
if (is_array($responseDecoded)) {
|
||||
if (array_key_exists('code', $responseDecoded)) {
|
||||
$success = intval($responseDecoded['code']) === 1;
|
||||
} else {
|
||||
$success = $statusCode >= 200 && $statusCode < 400;
|
||||
}
|
||||
$responseData = $responseDecoded['data'] ?? $responseDecoded;
|
||||
if (!$success) {
|
||||
$failReasonRaw = $responseDecoded['message'] ?? ($responseDecoded['msg'] ?? '');
|
||||
$failReason = is_string($failReasonRaw) ? $failReasonRaw : strval($failReasonRaw);
|
||||
}
|
||||
} else {
|
||||
$success = $statusCode >= 200 && $statusCode < 400;
|
||||
if ($success) {
|
||||
$responseData = $this->truncateText($responseBodyRaw, 2000);
|
||||
} else {
|
||||
$failReason = $this->truncateText($responseBodyRaw, 2000);
|
||||
}
|
||||
}
|
||||
|
||||
$logPayload = [
|
||||
'path' => '/' . $path,
|
||||
'method' => strtoupper($request->method()),
|
||||
'ip' => $request->getRealIp(),
|
||||
'status_code' => $statusCode,
|
||||
'cost_ms' => $costMs,
|
||||
'request' => $requestPayload,
|
||||
'success' => $success,
|
||||
];
|
||||
|
||||
if ($success) {
|
||||
$logPayload['response_data'] = $this->maskMixed($responseData);
|
||||
Log::info('[InterfaceRequestLog] ' . $this->encodeJson($logPayload));
|
||||
} else {
|
||||
$logPayload['fail_reason'] = $failReason !== '' ? $failReason : 'Unknown error';
|
||||
if (is_array($responseDecoded)) {
|
||||
$logPayload['api_response'] = $this->maskMixed($responseDecoded);
|
||||
}
|
||||
Log::error('[InterfaceRequestLog] ' . $this->encodeJson($logPayload));
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
private function shouldLogPath(string $path): bool
|
||||
{
|
||||
foreach (self::API_PATH_PREFIXES as $prefix) {
|
||||
if (str_starts_with($path, $prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function buildRequestPayload(Request $request): array
|
||||
{
|
||||
$query = $request->get();
|
||||
$post = $request->post();
|
||||
if (!is_array($query)) {
|
||||
$query = [];
|
||||
}
|
||||
if (!is_array($post)) {
|
||||
$post = [];
|
||||
}
|
||||
|
||||
$rawBody = method_exists($request, 'rawBody') ? strval($request->rawBody()) : '';
|
||||
$jsonBody = [];
|
||||
if ($rawBody !== '') {
|
||||
$jsonDecoded = json_decode($rawBody, true);
|
||||
if (is_array($jsonDecoded)) {
|
||||
$jsonBody = $jsonDecoded;
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
'query' => $this->maskMixed($query),
|
||||
'post' => $this->maskMixed($post),
|
||||
'json' => $this->maskMixed($jsonBody),
|
||||
];
|
||||
}
|
||||
|
||||
private function extractResponseBody(Response $response): string
|
||||
{
|
||||
if (method_exists($response, 'rawBody')) {
|
||||
return strval($response->rawBody());
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
private function decodeJsonArray(string $content): ?array
|
||||
{
|
||||
if ($content === '') {
|
||||
return null;
|
||||
}
|
||||
$decoded = json_decode($content, true);
|
||||
return is_array($decoded) ? $decoded : null;
|
||||
}
|
||||
|
||||
private function maskMixed($value, ?string $parentKey = null)
|
||||
{
|
||||
if (is_array($value)) {
|
||||
$result = [];
|
||||
foreach ($value as $k => $v) {
|
||||
$keyName = is_string($k) ? $k : $parentKey;
|
||||
$result[$k] = $this->maskMixed($v, $keyName);
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
if ($parentKey !== null && $this->isSensitiveKey($parentKey)) {
|
||||
return '***';
|
||||
}
|
||||
return $value;
|
||||
}
|
||||
|
||||
private function isSensitiveKey(string $key): bool
|
||||
{
|
||||
$normalized = strtolower(trim($key));
|
||||
foreach (self::SENSITIVE_KEYS as $sensitive) {
|
||||
if ($normalized === $sensitive) {
|
||||
return true;
|
||||
}
|
||||
if (str_contains($normalized, $sensitive)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function truncateText(string $text, int $maxLen): string
|
||||
{
|
||||
if ($maxLen <= 0) {
|
||||
return '';
|
||||
}
|
||||
if (mb_strlen($text) <= $maxLen) {
|
||||
return $text;
|
||||
}
|
||||
return mb_substr($text, 0, $maxLen) . '...';
|
||||
}
|
||||
|
||||
private function encodeJson(array $payload): string
|
||||
{
|
||||
return json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
}
|
||||
|
||||
private function costMs(float $start): int
|
||||
{
|
||||
return intval((microtime(true) - $start) * 1000);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user