update tests and doc
This commit is contained in:
@@ -166,6 +166,40 @@ use OpenApi\Attributes as OA;
|
|||||||
new OA\Property(property: 'failed', type: 'integer', example: 8),
|
new OA\Property(property: 'failed', type: 'integer', example: 8),
|
||||||
]
|
]
|
||||||
)]
|
)]
|
||||||
|
#[OA\Schema(
|
||||||
|
schema: 'ApiRequestLogList',
|
||||||
|
type: 'object',
|
||||||
|
description: 'API 请求日志列表项',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||||
|
new OA\Property(property: 'user_id', type: 'integer', example: 1, nullable: true),
|
||||||
|
new OA\Property(property: 'method', type: 'string', example: 'GET'),
|
||||||
|
new OA\Property(property: 'path', type: 'string', example: '/api/v1/users'),
|
||||||
|
new OA\Property(property: 'status_code', type: 'integer', example: 200),
|
||||||
|
new OA\Property(property: 'ip', type: 'string', example: '127.0.0.1', nullable: true),
|
||||||
|
new OA\Property(property: 'response_code', type: 'integer', example: 0, nullable: true),
|
||||||
|
new OA\Property(property: 'duration_ms', type: 'integer', example: 42),
|
||||||
|
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
|
#[OA\Schema(
|
||||||
|
schema: 'ApiRequestLogDetail',
|
||||||
|
type: 'object',
|
||||||
|
description: 'API 请求日志详情(含完整请求体和 User-Agent)',
|
||||||
|
properties: [
|
||||||
|
new OA\Property(property: 'id', type: 'integer', example: 1),
|
||||||
|
new OA\Property(property: 'user_id', type: 'integer', example: 1, nullable: true),
|
||||||
|
new OA\Property(property: 'method', type: 'string', example: 'POST'),
|
||||||
|
new OA\Property(property: 'path', type: 'string', example: '/api/v1/users'),
|
||||||
|
new OA\Property(property: 'status_code', type: 'integer', example: 200),
|
||||||
|
new OA\Property(property: 'ip', type: 'string', example: '127.0.0.1', nullable: true),
|
||||||
|
new OA\Property(property: 'user_agent', type: 'string', example: 'Mozilla/5.0', nullable: true),
|
||||||
|
new OA\Property(property: 'request_body', type: 'object', description: '请求体(脱敏后)', nullable: true),
|
||||||
|
new OA\Property(property: 'response_code', type: 'integer', example: 0, nullable: true),
|
||||||
|
new OA\Property(property: 'duration_ms', type: 'integer', example: 42),
|
||||||
|
new OA\Property(property: 'created_at', type: 'string', format: 'date-time'),
|
||||||
|
]
|
||||||
|
)]
|
||||||
class OpenApiSpec
|
class OpenApiSpec
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,399 @@ servers:
|
|||||||
url: /api/v1
|
url: /api/v1
|
||||||
description: 'API v1'
|
description: 'API v1'
|
||||||
paths:
|
paths:
|
||||||
|
/api/v1/dashboard/overview:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Dashboard
|
||||||
|
summary: 获取概览统计
|
||||||
|
description: 返回今日/本周/本月的成功和失败同步数,以及按数据类型的本月分组统计。
|
||||||
|
operationId: 867e0544fefdad33775c25bb6188d484
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 获取成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
code: { type: integer, example: 0 }
|
||||||
|
message: { type: string, example: 获取成功 }
|
||||||
|
data: { $ref: '#/components/schemas/DashboardOverview' }
|
||||||
|
type: object
|
||||||
|
'401':
|
||||||
|
description: 未认证
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: 无权限
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
/api/v1/dashboard/trend:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Dashboard
|
||||||
|
summary: 获取趋势数据
|
||||||
|
description: 返回指定时间范围内按日/周/月聚合的成功和失败同步趋势。
|
||||||
|
operationId: d3850b6a1ce1436c8ddf4b92fc0b7e44
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: from
|
||||||
|
in: query
|
||||||
|
description: 起始日期(默认30天前)
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
example: '2026-02-15'
|
||||||
|
-
|
||||||
|
name: to
|
||||||
|
in: query
|
||||||
|
description: 结束日期(默认今天)
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
example: '2026-03-17'
|
||||||
|
-
|
||||||
|
name: group_by
|
||||||
|
in: query
|
||||||
|
description: 聚合粒度
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
default: day
|
||||||
|
enum:
|
||||||
|
- day
|
||||||
|
- week
|
||||||
|
- month
|
||||||
|
-
|
||||||
|
name: data_type
|
||||||
|
in: query
|
||||||
|
description: 数据类型筛选
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- order
|
||||||
|
- product
|
||||||
|
- refund
|
||||||
|
- inventory
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 获取成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
code: { type: integer, example: 0 }
|
||||||
|
message: { type: string, example: 获取成功 }
|
||||||
|
data: { type: array, items: { $ref: '#/components/schemas/DashboardTrendItem' } }
|
||||||
|
type: object
|
||||||
|
'400':
|
||||||
|
description: 参数错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'401':
|
||||||
|
description: 未认证
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: 无权限
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
/api/v1/dashboard/breakdown:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- Dashboard
|
||||||
|
summary: 获取分组统计
|
||||||
|
description: 按公司/平台/店铺维度统计成功和失败同步数。
|
||||||
|
operationId: 91349b224bcf13f440df27de0a198f54
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: dimension
|
||||||
|
in: query
|
||||||
|
description: 分组维度
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- company
|
||||||
|
- platform
|
||||||
|
- store
|
||||||
|
-
|
||||||
|
name: from
|
||||||
|
in: query
|
||||||
|
description: 起始日期(默认今天)
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
example: '2026-03-17'
|
||||||
|
-
|
||||||
|
name: to
|
||||||
|
in: query
|
||||||
|
description: 结束日期(默认今天)
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
example: '2026-03-17'
|
||||||
|
-
|
||||||
|
name: data_type
|
||||||
|
in: query
|
||||||
|
description: 数据类型筛选
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- order
|
||||||
|
- product
|
||||||
|
- refund
|
||||||
|
- inventory
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 获取成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
code: { type: integer, example: 0 }
|
||||||
|
message: { type: string, example: 获取成功 }
|
||||||
|
data: { type: array, items: { $ref: '#/components/schemas/DashboardBreakdownItem' } }
|
||||||
|
type: object
|
||||||
|
'400':
|
||||||
|
description: 参数错误
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'401':
|
||||||
|
description: 未认证
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: 无权限
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
/api/v1/failed-messages:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- 'Failed Messages'
|
||||||
|
summary: 失败消息列表
|
||||||
|
description: '获取 Consumer 处理失败的消息列表,支持分页、按数据类型/平台/时间筛选。仅 admin 可访问。'
|
||||||
|
operationId: 6dac9bdff8b35f75565ed5c6b8638937
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: page
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 1
|
||||||
|
-
|
||||||
|
name: per_page
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 15
|
||||||
|
maximum: 100
|
||||||
|
-
|
||||||
|
name: data_type
|
||||||
|
in: query
|
||||||
|
description: 数据类型精确筛选(order/product/refund)
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- order
|
||||||
|
- product
|
||||||
|
- refund
|
||||||
|
-
|
||||||
|
name: platform_id
|
||||||
|
in: query
|
||||||
|
description: '平台 ID 精确筛选'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
-
|
||||||
|
name: error_type
|
||||||
|
in: query
|
||||||
|
description: 异常类名模糊搜索
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
-
|
||||||
|
name: failed_at_from
|
||||||
|
in: query
|
||||||
|
description: 失败时间起始(含)
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
example: '2026-01-01'
|
||||||
|
-
|
||||||
|
name: failed_at_to
|
||||||
|
in: query
|
||||||
|
description: 失败时间截止(含)
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
example: '2026-12-31'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 获取成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
code: { type: integer, example: 0 }
|
||||||
|
message: { type: string, example: 获取成功 }
|
||||||
|
data: { properties: { items: { type: array, items: { $ref: '#/components/schemas/FailedMessageList' } }, total: { type: integer, example: 100 }, page: { type: integer, example: 1 }, per_page: { type: integer, example: 15 } }, type: object }
|
||||||
|
type: object
|
||||||
|
'401':
|
||||||
|
description: 未认证
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: 无权限
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
'/api/v1/failed-messages/{id}':
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- 'Failed Messages'
|
||||||
|
summary: 失败消息详情
|
||||||
|
description: '获取失败消息详情,含完整错误堆栈和原始消息体。仅 admin 可访问。'
|
||||||
|
operationId: 4f6b7410222ebc0fa65afdc1d168c15a
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: id
|
||||||
|
in: path
|
||||||
|
description: '失败消息 ID'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 获取成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
code: { type: integer, example: 0 }
|
||||||
|
message: { type: string, example: 获取成功 }
|
||||||
|
data: { $ref: '#/components/schemas/FailedMessageDetail' }
|
||||||
|
type: object
|
||||||
|
'401':
|
||||||
|
description: 未认证
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: 无权限
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'404':
|
||||||
|
description: 数据不存在
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
/api/v1/mq/status:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- 'MQ Status'
|
||||||
|
summary: 获取消息队列状态
|
||||||
|
description: '查询 RabbitMQ 各队列的消息数、消费者数和运行状态。支持按队列类型筛选。仅 admin 角色可访问。'
|
||||||
|
operationId: 3f5b041034fafc9738ecf65f166b6193
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: queue
|
||||||
|
in: query
|
||||||
|
description: 队列类型筛选(orders/products/refunds/inventory)
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- orders
|
||||||
|
- products
|
||||||
|
- refunds
|
||||||
|
- inventory
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 获取成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
code: { type: integer, example: 0 }
|
||||||
|
message: { type: string, example: 获取成功 }
|
||||||
|
data: { $ref: '#/components/schemas/MqQueueStatus' }
|
||||||
|
type: object
|
||||||
|
'400':
|
||||||
|
description: 无效的队列类型参数
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'401':
|
||||||
|
description: 未认证
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: 无权限
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'500':
|
||||||
|
description: 'RabbitMQ 连接异常'
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
/orders:
|
/orders:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
@@ -1017,6 +1410,13 @@ paths:
|
|||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
-
|
||||||
|
name: platform_product_id
|
||||||
|
in: query
|
||||||
|
description: '平台商品 ID 精确筛选'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
-
|
-
|
||||||
name: created_date_from
|
name: created_date_from
|
||||||
in: query
|
in: query
|
||||||
@@ -1348,6 +1748,13 @@ paths:
|
|||||||
required: false
|
required: false
|
||||||
schema:
|
schema:
|
||||||
type: string
|
type: string
|
||||||
|
-
|
||||||
|
name: platform_product_id
|
||||||
|
in: query
|
||||||
|
description: '平台商品 ID 精确筛选'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
-
|
-
|
||||||
name: created_date_from
|
name: created_date_from
|
||||||
in: query
|
in: query
|
||||||
@@ -1439,6 +1846,154 @@ paths:
|
|||||||
security:
|
security:
|
||||||
-
|
-
|
||||||
bearerAuth: []
|
bearerAuth: []
|
||||||
|
/api/v1/logs/requests:
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- 'Request Logs'
|
||||||
|
summary: 请求日志列表
|
||||||
|
description: '获取 API 请求日志列表,支持分页、按用户/方法/路径/状态码/时间筛选。仅 admin 可访问。'
|
||||||
|
operationId: db0b73fd478b1f9dfacb819a397254ff
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: page
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 1
|
||||||
|
-
|
||||||
|
name: per_page
|
||||||
|
in: query
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
default: 15
|
||||||
|
maximum: 100
|
||||||
|
-
|
||||||
|
name: user_id
|
||||||
|
in: query
|
||||||
|
description: '用户 ID 精确筛选'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
-
|
||||||
|
name: method
|
||||||
|
in: query
|
||||||
|
description: 'HTTP 方法精确筛选'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- GET
|
||||||
|
- POST
|
||||||
|
- PUT
|
||||||
|
- PATCH
|
||||||
|
- DELETE
|
||||||
|
-
|
||||||
|
name: path
|
||||||
|
in: query
|
||||||
|
description: 请求路径模糊搜索
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
-
|
||||||
|
name: status_code
|
||||||
|
in: query
|
||||||
|
description: 'HTTP 状态码精确筛选'
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
-
|
||||||
|
name: created_at_from
|
||||||
|
in: query
|
||||||
|
description: 创建时间起始(含)
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
example: '2026-01-01'
|
||||||
|
-
|
||||||
|
name: created_at_to
|
||||||
|
in: query
|
||||||
|
description: 创建时间截止(含)
|
||||||
|
required: false
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
example: '2026-12-31'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 获取成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
code: { type: integer, example: 0 }
|
||||||
|
message: { type: string, example: 获取成功 }
|
||||||
|
data: { properties: { items: { type: array, items: { $ref: '#/components/schemas/ApiRequestLogList' } }, total: { type: integer, example: 100 }, page: { type: integer, example: 1 }, per_page: { type: integer, example: 15 } }, type: object }
|
||||||
|
type: object
|
||||||
|
'401':
|
||||||
|
description: 未认证
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: 无权限
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
|
'/api/v1/logs/requests/{id}':
|
||||||
|
get:
|
||||||
|
tags:
|
||||||
|
- 'Request Logs'
|
||||||
|
summary: 请求日志详情
|
||||||
|
description: '获取请求日志详情,含完整请求体和 User-Agent。仅 admin 可访问。'
|
||||||
|
operationId: f730ba32caa6f4a153d5d394a1102912
|
||||||
|
parameters:
|
||||||
|
-
|
||||||
|
name: id
|
||||||
|
in: path
|
||||||
|
description: '请求日志 ID'
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: integer
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: 获取成功
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
properties:
|
||||||
|
code: { type: integer, example: 0 }
|
||||||
|
message: { type: string, example: 获取成功 }
|
||||||
|
data: { $ref: '#/components/schemas/ApiRequestLogDetail' }
|
||||||
|
type: object
|
||||||
|
'401':
|
||||||
|
description: 未认证
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'403':
|
||||||
|
description: 无权限
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
'404':
|
||||||
|
description: 数据不存在
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ErrorResponse'
|
||||||
|
security:
|
||||||
|
-
|
||||||
|
bearerAuth: []
|
||||||
/me/api-keys:
|
/me/api-keys:
|
||||||
get:
|
get:
|
||||||
tags:
|
tags:
|
||||||
@@ -3463,6 +4018,348 @@ components:
|
|||||||
type: object
|
type: object
|
||||||
nullable: true
|
nullable: true
|
||||||
type: object
|
type: object
|
||||||
|
MqQueueInfo:
|
||||||
|
description: 单个队列的状态信息
|
||||||
|
properties:
|
||||||
|
queue:
|
||||||
|
description: 队列名称
|
||||||
|
type: string
|
||||||
|
example: orders.queue
|
||||||
|
messages:
|
||||||
|
description: '消息数量(异常时为 N/A)'
|
||||||
|
example: 5
|
||||||
|
oneOf:
|
||||||
|
-
|
||||||
|
type: integer
|
||||||
|
-
|
||||||
|
type: string
|
||||||
|
consumers:
|
||||||
|
description: '消费者数量(异常时为 N/A)'
|
||||||
|
example: 1
|
||||||
|
oneOf:
|
||||||
|
-
|
||||||
|
type: integer
|
||||||
|
-
|
||||||
|
type: string
|
||||||
|
status:
|
||||||
|
description: 队列状态
|
||||||
|
type: string
|
||||||
|
example: active
|
||||||
|
enum:
|
||||||
|
- high_load
|
||||||
|
- processing
|
||||||
|
- active
|
||||||
|
- empty
|
||||||
|
- error
|
||||||
|
type: object
|
||||||
|
MqQueueStatus:
|
||||||
|
description: 消息队列全量状态
|
||||||
|
properties:
|
||||||
|
business_queues:
|
||||||
|
description: 业务队列列表
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/MqQueueInfo'
|
||||||
|
retry_queues:
|
||||||
|
description: 重试队列列表
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
$ref: '#/components/schemas/MqQueueInfo'
|
||||||
|
error_queue:
|
||||||
|
$ref: '#/components/schemas/MqQueueInfo'
|
||||||
|
summary:
|
||||||
|
description: 汇总统计
|
||||||
|
properties:
|
||||||
|
total_messages:
|
||||||
|
description: 消息总数
|
||||||
|
type: integer
|
||||||
|
example: 7
|
||||||
|
total_consumers:
|
||||||
|
description: 消费者总数
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
type: object
|
||||||
|
fetched_at:
|
||||||
|
description: 数据获取时间
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
example: '2026-03-13 12:00:00'
|
||||||
|
type: object
|
||||||
|
FailedMessageList:
|
||||||
|
description: 失败消息列表项
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
error_id:
|
||||||
|
type: string
|
||||||
|
example: err_67890abc
|
||||||
|
data_type:
|
||||||
|
type: string
|
||||||
|
example: order
|
||||||
|
enum:
|
||||||
|
- order
|
||||||
|
- product
|
||||||
|
- refund
|
||||||
|
platform:
|
||||||
|
type: string
|
||||||
|
example: Tmall
|
||||||
|
nullable: true
|
||||||
|
platform_id:
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
nullable: true
|
||||||
|
company_id:
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
nullable: true
|
||||||
|
store_id:
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
nullable: true
|
||||||
|
error_type:
|
||||||
|
type: string
|
||||||
|
example: RuntimeException
|
||||||
|
error_message:
|
||||||
|
type: string
|
||||||
|
example: 'Connection refused'
|
||||||
|
retry_count:
|
||||||
|
type: integer
|
||||||
|
example: 3
|
||||||
|
message_id:
|
||||||
|
type: string
|
||||||
|
example: msg_12345
|
||||||
|
nullable: true
|
||||||
|
failed_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
type: object
|
||||||
|
FailedMessageDetail:
|
||||||
|
description: 失败消息详情(含完整错误堆栈和原始消息体)
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
error_id:
|
||||||
|
type: string
|
||||||
|
example: err_67890abc
|
||||||
|
data_type:
|
||||||
|
type: string
|
||||||
|
example: order
|
||||||
|
enum:
|
||||||
|
- order
|
||||||
|
- product
|
||||||
|
- refund
|
||||||
|
platform:
|
||||||
|
type: string
|
||||||
|
example: Tmall
|
||||||
|
nullable: true
|
||||||
|
platform_id:
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
nullable: true
|
||||||
|
company_id:
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
nullable: true
|
||||||
|
store_id:
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
nullable: true
|
||||||
|
error_type:
|
||||||
|
type: string
|
||||||
|
example: RuntimeException
|
||||||
|
error_message:
|
||||||
|
type: string
|
||||||
|
example: 'Connection refused'
|
||||||
|
error_code:
|
||||||
|
type: integer
|
||||||
|
example: 0
|
||||||
|
error_trace:
|
||||||
|
description: 完整异常堆栈
|
||||||
|
type: string
|
||||||
|
original_message:
|
||||||
|
description: '原始消息体 JSON'
|
||||||
|
type: object
|
||||||
|
retry_count:
|
||||||
|
type: integer
|
||||||
|
example: 3
|
||||||
|
message_id:
|
||||||
|
type: string
|
||||||
|
example: msg_12345
|
||||||
|
nullable: true
|
||||||
|
failed_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
type: object
|
||||||
|
DashboardOverview:
|
||||||
|
description: 'Dashboard 概览统计'
|
||||||
|
properties:
|
||||||
|
today:
|
||||||
|
description: 今日统计
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: integer
|
||||||
|
example: 120
|
||||||
|
failed:
|
||||||
|
type: integer
|
||||||
|
example: 3
|
||||||
|
type: object
|
||||||
|
this_week:
|
||||||
|
description: 本周统计
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: integer
|
||||||
|
example: 850
|
||||||
|
failed:
|
||||||
|
type: integer
|
||||||
|
example: 15
|
||||||
|
type: object
|
||||||
|
this_month:
|
||||||
|
description: 本月统计
|
||||||
|
properties:
|
||||||
|
success:
|
||||||
|
type: integer
|
||||||
|
example: 3200
|
||||||
|
failed:
|
||||||
|
type: integer
|
||||||
|
example: 42
|
||||||
|
type: object
|
||||||
|
by_type:
|
||||||
|
description: 按数据类型分组统计(本月窗口)
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
properties:
|
||||||
|
data_type:
|
||||||
|
type: string
|
||||||
|
example: order
|
||||||
|
enum:
|
||||||
|
- order
|
||||||
|
- product
|
||||||
|
- refund
|
||||||
|
- inventory
|
||||||
|
success:
|
||||||
|
type: integer
|
||||||
|
example: 1000
|
||||||
|
failed:
|
||||||
|
type: integer
|
||||||
|
example: 10
|
||||||
|
type: object
|
||||||
|
type: object
|
||||||
|
DashboardTrendItem:
|
||||||
|
description: 趋势数据点
|
||||||
|
properties:
|
||||||
|
date:
|
||||||
|
type: string
|
||||||
|
format: date
|
||||||
|
example: '2026-03-17'
|
||||||
|
success:
|
||||||
|
type: integer
|
||||||
|
example: 120
|
||||||
|
failed:
|
||||||
|
type: integer
|
||||||
|
example: 3
|
||||||
|
type: object
|
||||||
|
DashboardBreakdownItem:
|
||||||
|
description: 分组统计项
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
description: '维度 ID(公司/平台/店铺)'
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
name:
|
||||||
|
description: 维度名称
|
||||||
|
type: string
|
||||||
|
example: Tmall
|
||||||
|
success:
|
||||||
|
type: integer
|
||||||
|
example: 500
|
||||||
|
failed:
|
||||||
|
type: integer
|
||||||
|
example: 8
|
||||||
|
type: object
|
||||||
|
ApiRequestLogList:
|
||||||
|
description: 'API 请求日志列表项'
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
user_id:
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
nullable: true
|
||||||
|
method:
|
||||||
|
type: string
|
||||||
|
example: GET
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
example: /api/v1/users
|
||||||
|
status_code:
|
||||||
|
type: integer
|
||||||
|
example: 200
|
||||||
|
ip:
|
||||||
|
type: string
|
||||||
|
example: 127.0.0.1
|
||||||
|
nullable: true
|
||||||
|
response_code:
|
||||||
|
type: integer
|
||||||
|
example: 0
|
||||||
|
nullable: true
|
||||||
|
duration_ms:
|
||||||
|
type: integer
|
||||||
|
example: 42
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
type: object
|
||||||
|
ApiRequestLogDetail:
|
||||||
|
description: 'API 请求日志详情(含完整请求体和 User-Agent)'
|
||||||
|
properties:
|
||||||
|
id:
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
user_id:
|
||||||
|
type: integer
|
||||||
|
example: 1
|
||||||
|
nullable: true
|
||||||
|
method:
|
||||||
|
type: string
|
||||||
|
example: POST
|
||||||
|
path:
|
||||||
|
type: string
|
||||||
|
example: /api/v1/users
|
||||||
|
status_code:
|
||||||
|
type: integer
|
||||||
|
example: 200
|
||||||
|
ip:
|
||||||
|
type: string
|
||||||
|
example: 127.0.0.1
|
||||||
|
nullable: true
|
||||||
|
user_agent:
|
||||||
|
type: string
|
||||||
|
example: Mozilla/5.0
|
||||||
|
nullable: true
|
||||||
|
request_body:
|
||||||
|
description: 请求体(脱敏后)
|
||||||
|
type: object
|
||||||
|
nullable: true
|
||||||
|
response_code:
|
||||||
|
type: integer
|
||||||
|
example: 0
|
||||||
|
nullable: true
|
||||||
|
duration_ms:
|
||||||
|
type: integer
|
||||||
|
example: 42
|
||||||
|
created_at:
|
||||||
|
type: string
|
||||||
|
format: date-time
|
||||||
|
type: object
|
||||||
securitySchemes:
|
securitySchemes:
|
||||||
bearerAuth:
|
bearerAuth:
|
||||||
type: http
|
type: http
|
||||||
@@ -3473,6 +4370,15 @@ components:
|
|||||||
name: X-API-Key
|
name: X-API-Key
|
||||||
in: header
|
in: header
|
||||||
tags:
|
tags:
|
||||||
|
-
|
||||||
|
name: Dashboard
|
||||||
|
description: 'Dashboard 数据同步统计'
|
||||||
|
-
|
||||||
|
name: 'Failed Messages'
|
||||||
|
description: 失败消息查看
|
||||||
|
-
|
||||||
|
name: 'MQ Status'
|
||||||
|
description: 消息队列状态监控
|
||||||
-
|
-
|
||||||
name: Orders
|
name: Orders
|
||||||
description: 订单管理
|
description: 订单管理
|
||||||
@@ -3500,6 +4406,9 @@ tags:
|
|||||||
-
|
-
|
||||||
name: 'Refund Items'
|
name: 'Refund Items'
|
||||||
description: 退款项管理
|
description: 退款项管理
|
||||||
|
-
|
||||||
|
name: 'Request Logs'
|
||||||
|
description: 'API 请求日志查看'
|
||||||
-
|
-
|
||||||
name: ApiKeys
|
name: ApiKeys
|
||||||
description: 'API Key 管理'
|
description: 'API Key 管理'
|
||||||
|
|||||||
@@ -0,0 +1,214 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace HyperfTest\Cases\Integration\System;
|
||||||
|
|
||||||
|
use App\Model\ApiRequestLog;
|
||||||
|
use HyperfTest\TestCase;
|
||||||
|
use HyperfTest\Traits\AuthenticatedTestTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RequestLogController 集成测试
|
||||||
|
*
|
||||||
|
* 覆盖列表分页/筛选、详情(含完整请求体和 User-Agent)、仅 admin 可访问、404
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class RequestLogControllerTest extends TestCase
|
||||||
|
{
|
||||||
|
use AuthenticatedTestTrait;
|
||||||
|
|
||||||
|
private static ?int $testRecordId = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确保有测试数据
|
||||||
|
*/
|
||||||
|
protected function ensureTestData(): int
|
||||||
|
{
|
||||||
|
if (self::$testRecordId !== null) {
|
||||||
|
return self::$testRecordId;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $this->runInCoroutine(static function (): int {
|
||||||
|
$record = ApiRequestLog::query()->create([
|
||||||
|
'user_id' => 1,
|
||||||
|
'method' => 'POST',
|
||||||
|
'path' => '/api/v1/test/integration',
|
||||||
|
'status_code' => 200,
|
||||||
|
'ip' => '127.0.0.1',
|
||||||
|
'user_agent' => 'PHPUnit/Integration-Test',
|
||||||
|
'request_body' => ['username' => 'test', 'action' => 'create'],
|
||||||
|
'response_code' => 0,
|
||||||
|
'duration_ms' => 42,
|
||||||
|
]);
|
||||||
|
return $record->id;
|
||||||
|
});
|
||||||
|
|
||||||
|
self::$testRecordId = $id;
|
||||||
|
return $id;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function tearDown(): void
|
||||||
|
{
|
||||||
|
parent::tearDown();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function tearDownAfterClass(): void
|
||||||
|
{
|
||||||
|
if (self::$testRecordId !== null) {
|
||||||
|
$id = self::$testRecordId;
|
||||||
|
if (\Swoole\Coroutine::getCid() > 0) {
|
||||||
|
ApiRequestLog::query()->where('id', $id)->delete();
|
||||||
|
} else {
|
||||||
|
\Swoole\Coroutine\run(static function () use ($id): void {
|
||||||
|
ApiRequestLog::query()->where('id', $id)->delete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self::$testRecordId = null;
|
||||||
|
}
|
||||||
|
parent::tearDownAfterClass();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 列表接口 ==========
|
||||||
|
|
||||||
|
public function test_list_returns_paginated_data(): void
|
||||||
|
{
|
||||||
|
$this->ensureTestData();
|
||||||
|
|
||||||
|
$response = $this->get('/api/v1/logs/requests', [], $this->authHeaders());
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('code', 0);
|
||||||
|
$response->assertJsonStructure([
|
||||||
|
'code',
|
||||||
|
'message',
|
||||||
|
'data' => [
|
||||||
|
'items',
|
||||||
|
'total',
|
||||||
|
'page',
|
||||||
|
'per_page',
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_respects_per_page(): void
|
||||||
|
{
|
||||||
|
$this->ensureTestData();
|
||||||
|
|
||||||
|
$response = $this->get('/api/v1/logs/requests', ['per_page' => 5], $this->authHeaders());
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('data.per_page', 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_excludes_request_body(): void
|
||||||
|
{
|
||||||
|
$this->ensureTestData();
|
||||||
|
|
||||||
|
$response = $this->get('/api/v1/logs/requests', [], $this->authHeaders());
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$items = $response->json('data.items');
|
||||||
|
if (!empty($items)) {
|
||||||
|
$first = $items[0];
|
||||||
|
$this->assertArrayNotHasKey('request_body', $first);
|
||||||
|
$this->assertArrayNotHasKey('user_agent', $first);
|
||||||
|
$this->assertArrayHasKey('method', $first);
|
||||||
|
$this->assertArrayHasKey('path', $first);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 筛选 ==========
|
||||||
|
|
||||||
|
public function test_list_filter_by_method(): void
|
||||||
|
{
|
||||||
|
$this->ensureTestData();
|
||||||
|
|
||||||
|
$response = $this->get('/api/v1/logs/requests', ['method' => 'POST'], $this->authHeaders());
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$items = $response->json('data.items');
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$this->assertSame('POST', $item['method']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_filter_by_status_code(): void
|
||||||
|
{
|
||||||
|
$this->ensureTestData();
|
||||||
|
|
||||||
|
$response = $this->get('/api/v1/logs/requests', ['status_code' => 200], $this->authHeaders());
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$items = $response->json('data.items');
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$this->assertSame(200, $item['status_code']);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_filter_by_path_like(): void
|
||||||
|
{
|
||||||
|
$this->ensureTestData();
|
||||||
|
|
||||||
|
$response = $this->get('/api/v1/logs/requests', ['path' => 'integration'], $this->authHeaders());
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$items = $response->json('data.items');
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$this->assertStringContainsString('integration', strtolower($item['path']));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_list_filter_by_created_at_range(): void
|
||||||
|
{
|
||||||
|
$this->ensureTestData();
|
||||||
|
|
||||||
|
$response = $this->get('/api/v1/logs/requests', [
|
||||||
|
'created_at_from' => '2026-03-01',
|
||||||
|
'created_at_to' => '2026-03-31',
|
||||||
|
], $this->authHeaders());
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('code', 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 详情接口 ==========
|
||||||
|
|
||||||
|
public function test_detail_contains_full_fields(): void
|
||||||
|
{
|
||||||
|
$id = $this->ensureTestData();
|
||||||
|
|
||||||
|
$response = $this->get("/api/v1/logs/requests/{$id}", [], $this->authHeaders());
|
||||||
|
|
||||||
|
$response->assertStatus(200);
|
||||||
|
$response->assertJsonPath('code', 0);
|
||||||
|
|
||||||
|
$data = $response->json('data');
|
||||||
|
$this->assertArrayHasKey('request_body', $data);
|
||||||
|
$this->assertArrayHasKey('user_agent', $data);
|
||||||
|
$this->assertArrayHasKey('method', $data);
|
||||||
|
$this->assertArrayHasKey('path', $data);
|
||||||
|
$this->assertArrayHasKey('ip', $data);
|
||||||
|
$this->assertArrayHasKey('duration_ms', $data);
|
||||||
|
$this->assertSame('POST', $data['method']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_detail_not_found_returns_404(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/api/v1/logs/requests/999999', [], $this->authHeaders());
|
||||||
|
|
||||||
|
$response->assertStatus(404);
|
||||||
|
$this->assertSame(404, $response->json('code'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== 认证检查 ==========
|
||||||
|
|
||||||
|
public function test_list_without_token_returns_401(): void
|
||||||
|
{
|
||||||
|
$response = $this->get('/api/v1/logs/requests');
|
||||||
|
|
||||||
|
$response->assertStatus(401);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace HyperfTest\Cases\Unit\Middleware;
|
||||||
|
|
||||||
|
use App\Middleware\RequestLogMiddleware;
|
||||||
|
use GuzzleHttp\Psr7\Response;
|
||||||
|
use GuzzleHttp\Psr7\ServerRequest;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* RequestLogMiddleware 静态辅助方法单元测试
|
||||||
|
*
|
||||||
|
* 覆盖 sanitizeBody 敏感字段脱敏、extractResponseCode JSON 提取、getClientIp IP 获取
|
||||||
|
*
|
||||||
|
* @internal
|
||||||
|
* @coversNothing
|
||||||
|
*/
|
||||||
|
class RequestLogMiddlewareTest extends TestCase
|
||||||
|
{
|
||||||
|
// ========== sanitizeBody ==========
|
||||||
|
|
||||||
|
public function test_sanitize_body_replaces_password_with_asterisks(): void
|
||||||
|
{
|
||||||
|
$body = ['username' => 'admin', 'password' => 'secret123'];
|
||||||
|
$result = RequestLogMiddleware::sanitizeBody($body);
|
||||||
|
|
||||||
|
$this->assertSame('admin', $result['username']);
|
||||||
|
$this->assertSame('***', $result['password']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_sanitize_body_handles_nested_password_fields(): void
|
||||||
|
{
|
||||||
|
$body = [
|
||||||
|
'user' => [
|
||||||
|
'name' => 'test',
|
||||||
|
'password' => 'nested_secret',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
$result = RequestLogMiddleware::sanitizeBody($body);
|
||||||
|
|
||||||
|
$this->assertSame('test', $result['user']['name']);
|
||||||
|
$this->assertSame('***', $result['user']['password']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_sanitize_body_handles_all_password_variants(): void
|
||||||
|
{
|
||||||
|
$body = [
|
||||||
|
'password' => 'p1',
|
||||||
|
'old_password' => 'p2',
|
||||||
|
'new_password' => 'p3',
|
||||||
|
'password_confirmation' => 'p4',
|
||||||
|
];
|
||||||
|
$result = RequestLogMiddleware::sanitizeBody($body);
|
||||||
|
|
||||||
|
$this->assertSame('***', $result['password']);
|
||||||
|
$this->assertSame('***', $result['old_password']);
|
||||||
|
$this->assertSame('***', $result['new_password']);
|
||||||
|
$this->assertSame('***', $result['password_confirmation']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_sanitize_body_preserves_non_sensitive_fields(): void
|
||||||
|
{
|
||||||
|
$body = ['username' => 'admin', 'email' => 'a@b.com', 'status' => 1];
|
||||||
|
$result = RequestLogMiddleware::sanitizeBody($body);
|
||||||
|
|
||||||
|
$this->assertSame($body, $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_sanitize_body_handles_empty_array(): void
|
||||||
|
{
|
||||||
|
$result = RequestLogMiddleware::sanitizeBody([]);
|
||||||
|
$this->assertSame([], $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== extractResponseCode ==========
|
||||||
|
|
||||||
|
public function test_extract_response_code_from_json_response(): void
|
||||||
|
{
|
||||||
|
$response = new Response(200, ['Content-Type' => 'application/json'], json_encode([
|
||||||
|
'code' => 0,
|
||||||
|
'message' => 'success',
|
||||||
|
'data' => [],
|
||||||
|
]));
|
||||||
|
|
||||||
|
$code = RequestLogMiddleware::extractResponseCode($response);
|
||||||
|
$this->assertSame(0, $code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_extract_response_code_returns_null_for_non_json(): void
|
||||||
|
{
|
||||||
|
$response = new Response(200, ['Content-Type' => 'text/html'], '<html></html>');
|
||||||
|
|
||||||
|
$code = RequestLogMiddleware::extractResponseCode($response);
|
||||||
|
$this->assertNull($code);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_extract_response_code_returns_null_when_no_code_field(): void
|
||||||
|
{
|
||||||
|
$response = new Response(200, ['Content-Type' => 'application/json'], json_encode([
|
||||||
|
'message' => 'ok',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$code = RequestLogMiddleware::extractResponseCode($response);
|
||||||
|
$this->assertNull($code);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ========== getClientIp ==========
|
||||||
|
|
||||||
|
public function test_get_client_ip_from_x_forwarded_for(): void
|
||||||
|
{
|
||||||
|
$request = new ServerRequest('GET', '/test', ['X-Forwarded-For' => '1.2.3.4, 5.6.7.8']);
|
||||||
|
|
||||||
|
$ip = RequestLogMiddleware::getClientIp($request);
|
||||||
|
$this->assertSame('1.2.3.4', $ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_client_ip_from_x_real_ip(): void
|
||||||
|
{
|
||||||
|
$request = new ServerRequest('GET', '/test', ['X-Real-IP' => '10.0.0.1']);
|
||||||
|
|
||||||
|
$ip = RequestLogMiddleware::getClientIp($request);
|
||||||
|
$this->assertSame('10.0.0.1', $ip);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_client_ip_from_server_params(): void
|
||||||
|
{
|
||||||
|
$request = new ServerRequest('GET', '/test', [], null, '1.1', ['remote_addr' => '192.168.1.1']);
|
||||||
|
|
||||||
|
$ip = RequestLogMiddleware::getClientIp($request);
|
||||||
|
$this->assertSame('192.168.1.1', $ip);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user