diff --git a/server/app/api/cache/UserCache.php b/server/app/api/cache/UserCache.php index fa95c09..293e5da 100644 --- a/server/app/api/cache/UserCache.php +++ b/server/app/api/cache/UserCache.php @@ -238,6 +238,17 @@ class UserCache return (int) config('api.player_cache_ttl', 300); } + /** /api/v1/getPlayerInfo 快照 key 前缀 */ + private static function playerInfoSnapshotPrefix(): string + { + return config('api.player_info_snapshot_prefix', 'api:v1:player_info:'); + } + + private static function playerInfoSnapshotTtl(): int + { + return (int) config('api.player_info_snapshot_ttl', 180); + } + /** * 按 username 缓存玩家信息(仅 id + username,供中间件注入 request->player 后使用) * 登录/信息变更时需调用 deletePlayerByUsername 失效 @@ -280,6 +291,54 @@ class UserCache return false; } $key = self::playerCachePrefix() . $username; - return Cache::delete($key); + $r1 = Cache::delete($key); + $snapKey = self::playerInfoSnapshotPrefix() . $username; + $r2 = Cache::delete($snapKey); + return $r1 || $r2; + } + + /** + * 读取 getPlayerInfo 接口用的玩家公开信息快照(未命中返回 null) + * @return array|null + */ + public static function getPlayerInfoSnapshotByUsername(string $username): ?array + { + if ($username === '') { + return null; + } + if (self::playerInfoSnapshotTtl() <= 0) { + return null; + } + $key = self::playerInfoSnapshotPrefix() . $username; + $val = Cache::get($key); + if ($val === null || $val === '') { + return null; + } + $data = json_decode((string) $val, true); + if (!is_array($data) || !array_key_exists('username', $data)) { + return null; + } + return $data; + } + + /** + * 写入 getPlayerInfo 返回体快照(已脱敏数组) + * @param array $info + */ + public static function setPlayerInfoSnapshotByUsername(string $username, array $info): bool + { + if ($username === '' || empty($info)) { + return false; + } + $ttl = self::playerInfoSnapshotTtl(); + if ($ttl <= 0) { + return true; + } + $key = self::playerInfoSnapshotPrefix() . $username; + $payload = json_encode($info, JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE); + if ($payload === false) { + return false; + } + return Cache::set($key, $payload, $ttl); } } diff --git a/server/app/api/controller/v1/GameController.php b/server/app/api/controller/v1/GameController.php index 4fb2f81..8f7425d 100644 --- a/server/app/api/controller/v1/GameController.php +++ b/server/app/api/controller/v1/GameController.php @@ -44,6 +44,14 @@ class GameController extends BaseController 'sort', ]; + /** getPlayerInfo 仅查询需返回的列,减轻 IO(敏感/内部字段不入库查询) */ + private const PLAYER_INFO_DB_FIELDS = [ + 'id', 'username', 'phone', 'uid', 'name', 'status', 'coin', 'is_up', 'admin_id', + 'total_ticket_count', 'paid_ticket_count', 'free_ticket_count', 'free_ticket', + 'total_win_coin', + 'create_time', 'update_time', + ]; + /** * 获取游戏列表 * POST 参数:lang(可选,zh/en,默认 zh) @@ -130,24 +138,31 @@ class GameController extends BaseController /** * 获取用户信息 - * POST 参数:username - * 返回 DicePlayer 中非敏感信息 + * 参数:username(POST JSON / 表单 / Query 均可,input 合并读取降低偶发空参) + * 返回 DicePlayer 中非敏感信息;短期 Redis 快照 + 窄字段查询降低延迟 */ public function getPlayerInfo(Request $request): Response { - $username = trim((string) ($request->post('username', ''))); + $usernameRaw = $request->input('username', ''); + $username = is_string($usernameRaw) ? trim($usernameRaw) : ''; if ($username === '') { return $this->fail('username is required', ReturnCode::PARAMS_ERROR); } - $player = DicePlayer::where('username', $username)->find(); + $cached = UserCache::getPlayerInfoSnapshotByUsername($username); + if ($cached !== null) { + return $this->success($cached); + } + + $player = DicePlayer::field(self::PLAYER_INFO_DB_FIELDS)->where('username', $username)->find(); if (!$player) { return $this->fail('User not found', ReturnCode::NOT_FOUND); } $hidden = ['password', 'lottery_config_id', 't1_weight', 't2_weight', 't3_weight', 't4_weight', 't5_weight', 'delete_time']; $info = $player->hidden($hidden)->toArray(); + UserCache::setPlayerInfoSnapshotByUsername($username, $info); return $this->success($info); } diff --git a/server/app/api/middleware/AuthTokenMiddleware.php b/server/app/api/middleware/AuthTokenMiddleware.php index 5d0bed2..e90a02c 100644 --- a/server/app/api/middleware/AuthTokenMiddleware.php +++ b/server/app/api/middleware/AuthTokenMiddleware.php @@ -47,8 +47,9 @@ class AuthTokenMiddleware implements MiddlewareInterface throw new ApiException('auth-token invalid', ReturnCode::TOKEN_INVALID); } - $currentToken = AuthTokenCache::getTokenByAgentId($agentId); - if ($currentToken === null || $currentToken !== $token) { + // 单次 Redis:token → agent_id,与 JWT 内 agent_id 一致即有效(减少一次按 agent 取当前 token 的往返) + $agentIdFromStore = AuthTokenCache::getAgentIdByToken($token); + if ($agentIdFromStore === null || $agentIdFromStore !== $agentId) { throw new ApiException('auth-token invalid or expired', ReturnCode::TOKEN_INVALID); } diff --git a/server/config/api.php b/server/config/api.php index 085ab67..8e84cf9 100644 --- a/server/config/api.php +++ b/server/config/api.php @@ -34,4 +34,7 @@ return [ // 玩家信息按 username 缓存(Token 中间件用),0 表示不缓存 'player_cache_ttl' => (int) env('API_PLAYER_CACHE_TTL', 300), 'player_cache_prefix' => env('API_PLAYER_CACHE_PREFIX', 'api:player:'), + // /api/v1/getPlayerInfo 返回体快照(Redis),0 表示不缓存;变更玩家数据时需 deletePlayerByUsername 失效 + 'player_info_snapshot_ttl' => (int) env('API_PLAYER_INFO_SNAPSHOT_TTL', 180), + 'player_info_snapshot_prefix' => env('API_PLAYER_INFO_SNAPSHOT_PREFIX', 'api:v1:player_info:'), ];