feat: 增强玩家端 API 鉴权中间件,支持多语言错误消息返回

This commit is contained in:
2026-05-08 15:20:36 +08:00
parent 9f8080cefe
commit fc0999664a
9 changed files with 199 additions and 4 deletions

View File

@@ -5,15 +5,19 @@ namespace App\Http\Middleware;
use App\Exceptions\PlayerAuthenticationException;
use App\Services\PlayerTokenResolver;
use App\Support\ApiResponse;
use App\Support\LotteryMessage;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* 玩家端受保护路由前置:解析 Authorization失败时直接返回 { code, msg, data },不进入控制器。
* 玩家端 API 鉴权中间件】
*
* 成功后在 request 上挂 `lottery_player`,控制器内使用 `$request->lotteryPlayer()`
*(由 AppServiceProvider 注册的宏,返回 ?Player
* - 成功:解析 Bearer Player写入 request attribute `lottery_player`
* - 失败:直接 JSON 返回,不进入控制器;其中 msg 经由 LotteryMessage::sso() 按请求语言翻译
* (依赖前置的 NegotiateLotteryLocalecode 仍为文档约定业务码(如 8001)。
*
* PlayerAuthenticationException getMessage() 仅作开发与日志用语,可与 API msg 语种不一致。
*/
class EnsurePlayerApi
{
@@ -22,7 +26,13 @@ class EnsurePlayerApi
try {
$player = app(PlayerTokenResolver::class)->resolve($request);
} catch (PlayerAuthenticationException $e) {
return ApiResponse::error($e->getMessage(), $e->lotteryCode, null, $e->httpStatus);
// msg多语言用户提示code / httpStatus仍来自异常内业务定义
return ApiResponse::error(
LotteryMessage::sso($request, $e->lotteryCode),
$e->lotteryCode,
null,
$e->httpStatus,
);
}
// 使用 attributes避免与 Laravel 内置 input 混淆

View File

@@ -0,0 +1,77 @@
<?php
namespace App\Http\Middleware;
use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;
/**
* 【API 语言协商中间件】
*
* - 挂载位置bootstrap/app.php 中对 api 分组 prepend所有 /api/* 请求都会先执行本中间件。
* - 职责:解析客户端语言 写入 $request->attributes['lottery_locale'] 调用 app()->setLocale()
* 便于 trans()、LotteryMessage 及后续按 zh/en/ne 选库表文案列时使用。
*
* 【协商顺序】① 请求头 X-Locale必须为 config 支持的 zh/en/ne 之一);② Accept-Language 首选项;③ fallback。
*
* 【与前端】浏览器或 App 可直接带 X-Locale或仅靠 Accept-Language无需前端维护文案 JSON。
*/
class NegotiateLotteryLocale
{
public function handle(Request $request, Closure $next): Response
{
$locale = $this->resolveLocale($request);
// attribute 名称固定为 lottery_locale与 Request::lotteryLocale() 宏一致
$request->attributes->set('lottery_locale', $locale);
app()->setLocale($locale);
return $next($request);
}
/** 综合 X-Locale 与 Accept-Language得到最终语言代码 */
private function resolveLocale(Request $request): string
{
/** @var list<string> $supported */
$supported = array_values(array_unique(config('lottery.locales.supported', ['en', 'zh', 'ne'])));
$fallback = (string) config('lottery.locales.fallback', 'en');
$header = strtolower(trim((string) $request->header('X-Locale', '')));
if ($header !== '' && in_array($header, $supported, true)) {
return $header;
}
return $this->fromAcceptLanguage((string) $request->header('Accept-Language', ''), $supported, $fallback);
}
/**
* 解析标准 Accept-Language可带权重 q=),只认主语言段 zh / en / ne。
*
* @param list<string> $supported config('lottery.locales.supported')
*/
private function fromAcceptLanguage(string $header, array $supported, string $fallback): string
{
if ($header === '') {
return $fallback;
}
foreach (explode(',', $header) as $part) {
$code = strtolower(trim(explode(';', trim($part), 2)[0] ?? ''));
if ($code === '') {
continue;
}
$primary = explode('-', $code, 2)[0] ?? $code;
if ($primary === 'zh' && in_array('zh', $supported, true)) {
return 'zh';
}
if ($primary === 'ne' && in_array('ne', $supported, true)) {
return 'ne';
}
if ($primary === 'en' && in_array('en', $supported, true)) {
return 'en';
}
}
return $fallback;
}
}