1.优化接口/api/v1/getPlayerGameRecord
2.增加接口日志记录接口request和response
This commit is contained in:
@@ -23,6 +23,14 @@ use app\api\cache\UserCache;
|
||||
*/
|
||||
class GameController extends BaseController
|
||||
{
|
||||
/** 游戏记录 / 钱包流水 / 中奖券记录等拉取类接口的条数上限(与对接文档一致) */
|
||||
private const PLAYER_PULL_RECORD_MAX_LIMIT = 2000;
|
||||
|
||||
private const PLAYER_PULL_RECORD_DEFAULT_LIMIT = 20;
|
||||
|
||||
/** 拉取类流水仅允许查询「当前时间起向前」的最大天数(含起止) */
|
||||
private const PLAYER_PULL_RECORD_MAX_RANGE_DAYS = 7;
|
||||
|
||||
private const GAME_PUBLIC_FIELDS = [
|
||||
'provider',
|
||||
'provider_code',
|
||||
@@ -144,9 +152,110 @@ class GameController extends BaseController
|
||||
return $this->success($info);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析拉取类流水接口的 limit(与 getPlayerGameRecord / getPlayerWalletRecord / getPlayerTicketRecord 共用)
|
||||
*/
|
||||
private function resolvePullRecordLimit(Request $request): int
|
||||
{
|
||||
$limit = (int) $request->post('limit', self::PLAYER_PULL_RECORD_DEFAULT_LIMIT);
|
||||
if ($limit < 1 || $limit > self::PLAYER_PULL_RECORD_MAX_LIMIT) {
|
||||
$limit = self::PLAYER_PULL_RECORD_DEFAULT_LIMIT;
|
||||
}
|
||||
|
||||
return $limit;
|
||||
}
|
||||
|
||||
/**
|
||||
* 将 start_create_time / end_create_time 规范为「最近 7 天内」且跨度不超过 7 天的闭区间。
|
||||
*
|
||||
* @return array{ok: true, start: string, end: string}|array{ok: false, message: string}
|
||||
*/
|
||||
private function resolvePullRecordTimeWindow(string $startRaw, string $endRaw): array
|
||||
{
|
||||
$nowTs = time();
|
||||
$weekAgoTs = strtotime('-' . self::PLAYER_PULL_RECORD_MAX_RANGE_DAYS . ' days', $nowTs);
|
||||
if ($weekAgoTs === false) {
|
||||
$weekAgoTs = $nowTs - self::PLAYER_PULL_RECORD_MAX_RANGE_DAYS * 86400;
|
||||
}
|
||||
$nowStr = date('Y-m-d H:i:s', $nowTs);
|
||||
$weekAgoStr = date('Y-m-d H:i:s', $weekAgoTs);
|
||||
|
||||
if ($startRaw === '' && $endRaw === '') {
|
||||
return ['ok' => true, 'start' => $weekAgoStr, 'end' => $nowStr];
|
||||
}
|
||||
|
||||
$startTs = $startRaw === '' ? null : strtotime($startRaw);
|
||||
$endTs = $endRaw === '' ? null : strtotime($endRaw);
|
||||
|
||||
if ($startRaw !== '' && $startTs === false) {
|
||||
return ['ok' => false, 'message' => 'Invalid start_create_time'];
|
||||
}
|
||||
if ($endRaw !== '' && $endTs === false) {
|
||||
return ['ok' => false, 'message' => 'Invalid end_create_time'];
|
||||
}
|
||||
|
||||
if ($startRaw === '' && $endRaw !== '' && $endTs !== null) {
|
||||
$endTs = min($endTs, $nowTs);
|
||||
if ($endTs < $weekAgoTs) {
|
||||
return ['ok' => false, 'message' => 'end_create_time must be within the last 7 days'];
|
||||
}
|
||||
$startTs = strtotime('-' . self::PLAYER_PULL_RECORD_MAX_RANGE_DAYS . ' days', $endTs);
|
||||
if ($startTs === false) {
|
||||
$startTs = $endTs - self::PLAYER_PULL_RECORD_MAX_RANGE_DAYS * 86400;
|
||||
}
|
||||
if ($startTs < $weekAgoTs) {
|
||||
$startTs = $weekAgoTs;
|
||||
}
|
||||
|
||||
return ['ok' => true, 'start' => date('Y-m-d H:i:s', $startTs), 'end' => date('Y-m-d H:i:s', $endTs)];
|
||||
}
|
||||
|
||||
if ($startRaw !== '' && $startTs !== null && $endRaw === '') {
|
||||
if ($startTs > $nowTs) {
|
||||
return ['ok' => false, 'message' => 'start_create_time cannot be in the future'];
|
||||
}
|
||||
if ($startTs < $weekAgoTs) {
|
||||
return ['ok' => false, 'message' => 'start_create_time cannot be earlier than 7 days ago'];
|
||||
}
|
||||
$endTs = strtotime('+' . self::PLAYER_PULL_RECORD_MAX_RANGE_DAYS . ' days', $startTs);
|
||||
if ($endTs === false) {
|
||||
$endTs = $startTs + self::PLAYER_PULL_RECORD_MAX_RANGE_DAYS * 86400;
|
||||
}
|
||||
$endTs = min($endTs, $nowTs);
|
||||
|
||||
return ['ok' => true, 'start' => date('Y-m-d H:i:s', $startTs), 'end' => date('Y-m-d H:i:s', $endTs)];
|
||||
}
|
||||
|
||||
if ($startTs !== null && $endTs !== null) {
|
||||
if ($endTs > $nowTs) {
|
||||
$endTs = $nowTs;
|
||||
}
|
||||
if ($startTs > $endTs) {
|
||||
return ['ok' => false, 'message' => 'start_create_time cannot be after end_create_time'];
|
||||
}
|
||||
if ($startTs < $weekAgoTs) {
|
||||
return ['ok' => false, 'message' => 'start_create_time cannot be earlier than 7 days ago'];
|
||||
}
|
||||
if ($endTs < $weekAgoTs) {
|
||||
return ['ok' => false, 'message' => 'end_create_time must be within the last 7 days'];
|
||||
}
|
||||
$maxEndForStart = strtotime('+' . self::PLAYER_PULL_RECORD_MAX_RANGE_DAYS . ' days', $startTs);
|
||||
if ($maxEndForStart === false) {
|
||||
$maxEndForStart = $startTs + self::PLAYER_PULL_RECORD_MAX_RANGE_DAYS * 86400;
|
||||
}
|
||||
if ($endTs > $maxEndForStart) {
|
||||
return ['ok' => false, 'message' => 'Time range cannot exceed 7 days'];
|
||||
}
|
||||
|
||||
return ['ok' => true, 'start' => date('Y-m-d H:i:s', $startTs), 'end' => date('Y-m-d H:i:s', $endTs)];
|
||||
}
|
||||
|
||||
return ['ok' => false, 'message' => 'Invalid time parameters'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取游戏记录
|
||||
* POST 参数:username(非必填,没填则不按用户筛选), start_create_time, end_create_time(非必填,没填则不筛选时间)
|
||||
* POST 参数:username(非必填), start_create_time, end_create_time(可选,须在「最近 7 天」内且跨度不超过 7 天;均不传则默认最近 7 天), limit(非必填,返回条数上限)
|
||||
* 返回 DicePlayRecord 中非敏感信息
|
||||
*/
|
||||
public function getPlayerGameRecord(Request $request): Response
|
||||
@@ -154,15 +263,11 @@ class GameController extends BaseController
|
||||
$username = trim((string) ($request->post('username', '')));
|
||||
$startCreateTime = trim((string) ($request->post('start_create_time', '')));
|
||||
$endCreateTime = trim((string) ($request->post('end_create_time', '')));
|
||||
$page = (int) $request->post('page', 1);
|
||||
$limit = (int) $request->post('limit', 20);
|
||||
|
||||
if ($page < 1) {
|
||||
$page = 1;
|
||||
}
|
||||
if ($limit < 1 || $limit > 100) {
|
||||
$limit = 20;
|
||||
$window = $this->resolvePullRecordTimeWindow($startCreateTime, $endCreateTime);
|
||||
if (!$window['ok']) {
|
||||
return $this->fail($window['message'], ReturnCode::PARAMS_ERROR);
|
||||
}
|
||||
$limit = $this->resolvePullRecordLimit($request);
|
||||
|
||||
$query = DicePlayRecord::order('id', 'desc');
|
||||
|
||||
@@ -174,14 +279,10 @@ class GameController extends BaseController
|
||||
$query->where('player_id', (int) $player->id);
|
||||
}
|
||||
|
||||
if ($startCreateTime !== '') {
|
||||
$query->where('create_time', '>=', $startCreateTime);
|
||||
}
|
||||
if ($endCreateTime !== '') {
|
||||
$query->where('create_time', '<=', $endCreateTime);
|
||||
}
|
||||
$query->where('create_time', '>=', $window['start']);
|
||||
$query->where('create_time', '<=', $window['end']);
|
||||
|
||||
$list = $query->page($page, $limit)->select()->toArray();
|
||||
$list = $query->limit($limit)->select()->toArray();
|
||||
$playerIds = array_unique(array_column($list, 'player_id'));
|
||||
if (!empty($playerIds)) {
|
||||
$players = DicePlayer::whereIn('id', $playerIds)->field('id,username,phone')->select()->toArray();
|
||||
@@ -199,7 +300,7 @@ class GameController extends BaseController
|
||||
|
||||
/**
|
||||
* 获取钱包流水
|
||||
* POST 参数:username(非必填,没填则不按用户筛选), start_create_time, end_create_time(非必填,没填则不筛选时间)
|
||||
* POST 参数:username(非必填), start_create_time, end_create_time(可选,规则同 getPlayerGameRecord), limit(非必填)
|
||||
* 返回 DicePlayerWalletRecord 中非敏感信息
|
||||
*/
|
||||
public function getPlayerWalletRecord(Request $request): Response
|
||||
@@ -207,15 +308,11 @@ class GameController extends BaseController
|
||||
$username = trim((string) ($request->post('username', '')));
|
||||
$startCreateTime = trim((string) ($request->post('start_create_time', '')));
|
||||
$endCreateTime = trim((string) ($request->post('end_create_time', '')));
|
||||
$page = (int) $request->post('page', 1);
|
||||
$limit = (int) $request->post('limit', 20);
|
||||
|
||||
if ($page < 1) {
|
||||
$page = 1;
|
||||
}
|
||||
if ($limit < 1 || $limit > 100) {
|
||||
$limit = 20;
|
||||
$window = $this->resolvePullRecordTimeWindow($startCreateTime, $endCreateTime);
|
||||
if (!$window['ok']) {
|
||||
return $this->fail($window['message'], ReturnCode::PARAMS_ERROR);
|
||||
}
|
||||
$limit = $this->resolvePullRecordLimit($request);
|
||||
|
||||
$query = DicePlayerWalletRecord::order('id', 'desc');
|
||||
|
||||
@@ -227,23 +324,19 @@ class GameController extends BaseController
|
||||
$query->where('player_id', (int) $player->id);
|
||||
}
|
||||
|
||||
if ($startCreateTime !== '') {
|
||||
$query->where('create_time', '>=', $startCreateTime);
|
||||
}
|
||||
if ($endCreateTime !== '') {
|
||||
$query->where('create_time', '<=', $endCreateTime);
|
||||
}
|
||||
$query->where('create_time', '>=', $window['start']);
|
||||
$query->where('create_time', '<=', $window['end']);
|
||||
|
||||
$list = $query->with(['dicePlayer' => function ($q) {
|
||||
$q->field('id,username,phone');
|
||||
}])->page($page, $limit)->select()->toArray();
|
||||
}])->limit($limit)->select()->toArray();
|
||||
|
||||
return $this->success($list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户中奖券获取记录
|
||||
* POST 参数:username(非必填,没填则不按用户筛选), start_create_time, end_create_time(非必填,没填则不筛选时间)
|
||||
* POST 参数:username(非必填), start_create_time, end_create_time(可选,规则同 getPlayerGameRecord), limit(非必填)
|
||||
* 返回 DicePlayerTicketRecord 中非敏感信息
|
||||
*/
|
||||
public function getPlayerTicketRecord(Request $request): Response
|
||||
@@ -251,15 +344,11 @@ class GameController extends BaseController
|
||||
$username = trim((string) ($request->post('username', '')));
|
||||
$startCreateTime = trim((string) ($request->post('start_create_time', '')));
|
||||
$endCreateTime = trim((string) ($request->post('end_create_time', '')));
|
||||
$page = (int) $request->post('page', 1);
|
||||
$limit = (int) $request->post('limit', 20);
|
||||
|
||||
if ($page < 1) {
|
||||
$page = 1;
|
||||
}
|
||||
if ($limit < 1 || $limit > 100) {
|
||||
$limit = 20;
|
||||
$window = $this->resolvePullRecordTimeWindow($startCreateTime, $endCreateTime);
|
||||
if (!$window['ok']) {
|
||||
return $this->fail($window['message'], ReturnCode::PARAMS_ERROR);
|
||||
}
|
||||
$limit = $this->resolvePullRecordLimit($request);
|
||||
|
||||
$query = DicePlayerTicketRecord::order('id', 'desc');
|
||||
|
||||
@@ -271,16 +360,12 @@ class GameController extends BaseController
|
||||
$query->where('player_id', (int) $player->id);
|
||||
}
|
||||
|
||||
if ($startCreateTime !== '') {
|
||||
$query->where('create_time', '>=', $startCreateTime);
|
||||
}
|
||||
if ($endCreateTime !== '') {
|
||||
$query->where('create_time', '<=', $endCreateTime);
|
||||
}
|
||||
$query->where('create_time', '>=', $window['start']);
|
||||
$query->where('create_time', '<=', $window['end']);
|
||||
|
||||
$list = $query->with(['dicePlayer' => function ($q) {
|
||||
$q->field('id,username,phone');
|
||||
}])->page($page, $limit)->select()->toArray();
|
||||
}])->limit($limit)->select()->toArray();
|
||||
|
||||
return $this->success($list);
|
||||
}
|
||||
|
||||
201
server/app/api/middleware/ApiAccessLogMiddleware.php
Normal file
201
server/app/api/middleware/ApiAccessLogMiddleware.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace app\api\middleware;
|
||||
|
||||
use support\Log;
|
||||
use Throwable;
|
||||
use Webman\Http\Request;
|
||||
use Webman\Http\Response;
|
||||
use Webman\MiddlewareInterface;
|
||||
|
||||
/**
|
||||
* 记录 /api 相关接口的访问参数与响应摘要(敏感头、敏感字段脱敏)
|
||||
*/
|
||||
class ApiAccessLogMiddleware implements MiddlewareInterface
|
||||
{
|
||||
private const REQUEST_PARAM_MAX_LEN = 4096;
|
||||
|
||||
private const RESPONSE_BODY_MAX_LEN = 8192;
|
||||
|
||||
/** 请求头名称(小写) */
|
||||
private const SENSITIVE_HEADER_NAMES = [
|
||||
'auth-token',
|
||||
'token',
|
||||
'authorization',
|
||||
'cookie',
|
||||
];
|
||||
|
||||
/** 参数键名(小写匹配) */
|
||||
private const SENSITIVE_PARAM_KEYS = [
|
||||
'password',
|
||||
'secret',
|
||||
'signature',
|
||||
'token',
|
||||
'auth-token',
|
||||
'auth_token',
|
||||
'old_token',
|
||||
];
|
||||
|
||||
public function process(Request $request, callable $handler): Response
|
||||
{
|
||||
$startedAt = microtime(true);
|
||||
$response = null;
|
||||
$thrown = null;
|
||||
try {
|
||||
$response = $handler($request);
|
||||
return $response;
|
||||
} catch (Throwable $e) {
|
||||
$thrown = $e;
|
||||
throw $e;
|
||||
} finally {
|
||||
$durationMs = round((microtime(true) - $startedAt) * 1000, 3);
|
||||
$requestLog = $this->buildRequestLog($request);
|
||||
$responseLog = null;
|
||||
if ($response instanceof Response) {
|
||||
$responseLog = $this->buildResponseLog($response);
|
||||
}
|
||||
|
||||
Log::info('api_access', [
|
||||
'request' => $requestLog,
|
||||
'response' => $responseLog,
|
||||
'duration_ms' => $durationMs,
|
||||
'exception' => $thrown ? [
|
||||
'class' => $thrown::class,
|
||||
'message' => $thrown->getMessage(),
|
||||
] : null,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildRequestLog(Request $request): array
|
||||
{
|
||||
$headers = $request->header();
|
||||
if (!is_array($headers)) {
|
||||
$headers = [];
|
||||
}
|
||||
$headersOut = [];
|
||||
foreach ($headers as $name => $value) {
|
||||
$nameStr = strtolower((string) $name);
|
||||
if ($this->isSensitiveHeaderName($nameStr)) {
|
||||
$headersOut[$nameStr] = $this->maskToken(is_string($value) ? $value : (string) $value);
|
||||
} else {
|
||||
$headersOut[$nameStr] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
$get = $request->get();
|
||||
$post = $request->post();
|
||||
$params = [];
|
||||
if (is_array($get)) {
|
||||
$params = array_merge($params, $get);
|
||||
}
|
||||
if (is_array($post)) {
|
||||
$params = array_merge($params, $post);
|
||||
}
|
||||
$params = $this->sanitizeForLog($params);
|
||||
|
||||
$paramsJson = json_encode($params, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
if (!is_string($paramsJson)) {
|
||||
$paramsJson = '';
|
||||
}
|
||||
if (strlen($paramsJson) > self::REQUEST_PARAM_MAX_LEN) {
|
||||
$paramsJson = substr($paramsJson, 0, self::REQUEST_PARAM_MAX_LEN) . '...(truncated)';
|
||||
}
|
||||
|
||||
return [
|
||||
'method' => $request->method(),
|
||||
'path' => $request->path(),
|
||||
'uri' => $request->uri(),
|
||||
'ip' => method_exists($request, 'getRealIp') ? $request->getRealIp() : ($request->getRemoteIp() ?? ''),
|
||||
'content_type' => $request->header('content-type', ''),
|
||||
'headers' => $headersOut,
|
||||
'params' => $paramsJson,
|
||||
'agent_id' => $request->agent_id ?? null,
|
||||
'player_id' => $request->player_id ?? null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function buildResponseLog(Response $response): array
|
||||
{
|
||||
$raw = method_exists($response, 'rawBody') ? $response->rawBody() : '';
|
||||
if (!is_string($raw)) {
|
||||
$raw = '';
|
||||
}
|
||||
$bodyForLog = $raw;
|
||||
$decoded = json_decode($raw, true);
|
||||
if (is_array($decoded)) {
|
||||
$sanitized = $this->sanitizeForLog($decoded);
|
||||
$encoded = json_encode($sanitized, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
|
||||
if (is_string($encoded)) {
|
||||
$bodyForLog = $encoded;
|
||||
}
|
||||
}
|
||||
if (strlen($bodyForLog) > self::RESPONSE_BODY_MAX_LEN) {
|
||||
$bodyForLog = substr($bodyForLog, 0, self::RESPONSE_BODY_MAX_LEN) . '...(truncated)';
|
||||
}
|
||||
|
||||
return [
|
||||
'status' => $response->getStatusCode(),
|
||||
'body' => $bodyForLog,
|
||||
];
|
||||
}
|
||||
|
||||
private function isSensitiveHeaderName(string $name): bool
|
||||
{
|
||||
return in_array($name, self::SENSITIVE_HEADER_NAMES, true);
|
||||
}
|
||||
|
||||
private function isSensitiveParamKey(string $keyLower): bool
|
||||
{
|
||||
foreach (self::SENSITIVE_PARAM_KEYS as $s) {
|
||||
if ($keyLower === $s) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private function maskToken(string $value): string
|
||||
{
|
||||
$t = trim($value);
|
||||
if ($t === '') {
|
||||
return '';
|
||||
}
|
||||
if (strlen($t) <= 12) {
|
||||
return '***';
|
||||
}
|
||||
return substr($t, 0, 8) . '***' . substr($t, -4);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed $value
|
||||
* @return mixed
|
||||
*/
|
||||
private function sanitizeForLog($value)
|
||||
{
|
||||
if (!is_array($value)) {
|
||||
return $value;
|
||||
}
|
||||
$out = [];
|
||||
foreach ($value as $k => $v) {
|
||||
$keyLower = is_string($k) ? strtolower($k) : '';
|
||||
if ($keyLower !== '' && $this->isSensitiveParamKey($keyLower)) {
|
||||
$out[$k] = '***';
|
||||
continue;
|
||||
}
|
||||
if (is_array($v)) {
|
||||
$out[$k] = $this->sanitizeForLog($v);
|
||||
continue;
|
||||
}
|
||||
$out[$k] = $v;
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user