From fc0999664a143c98c669a5c7cb04173130f7fef8 Mon Sep 17 00:00:00 2001 From: kang Date: Fri, 8 May 2026 15:20:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E7=8E=A9=E5=AE=B6?= =?UTF-8?q?=E7=AB=AF=20API=20=E9=89=B4=E6=9D=83=E4=B8=AD=E9=97=B4=E4=BB=B6?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E5=A4=9A=E8=AF=AD=E8=A8=80=E9=94=99?= =?UTF-8?q?=E8=AF=AF=E6=B6=88=E6=81=AF=E8=BF=94=E5=9B=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/Http/Middleware/EnsurePlayerApi.php | 18 ++++- .../Middleware/NegotiateLotteryLocale.php | 77 +++++++++++++++++++ app/Providers/AppServiceProvider.php | 10 +++ app/Support/LotteryMessage.php | 36 +++++++++ bootstrap/app.php | 13 ++++ config/lottery.php | 13 ++++ lang/en/sso.php | 14 ++++ lang/ne/sso.php | 11 +++ lang/zh/sso.php | 11 +++ 9 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 app/Http/Middleware/NegotiateLotteryLocale.php create mode 100644 app/Support/LotteryMessage.php create mode 100644 lang/en/sso.php create mode 100644 lang/ne/sso.php create mode 100644 lang/zh/sso.php diff --git a/app/Http/Middleware/EnsurePlayerApi.php b/app/Http/Middleware/EnsurePlayerApi.php index 28d072a..be84e61 100644 --- a/app/Http/Middleware/EnsurePlayerApi.php +++ b/app/Http/Middleware/EnsurePlayerApi.php @@ -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() 按请求语言翻译 + * (依赖前置的 NegotiateLotteryLocale),code 仍为文档约定业务码(如 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 混淆 diff --git a/app/Http/Middleware/NegotiateLotteryLocale.php b/app/Http/Middleware/NegotiateLotteryLocale.php new file mode 100644 index 0000000..1c65f4f --- /dev/null +++ b/app/Http/Middleware/NegotiateLotteryLocale.php @@ -0,0 +1,77 @@ +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 $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 $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; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index eef6601..b58ce0d 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -26,5 +26,15 @@ class AppServiceProvider extends ServiceProvider /** @var Request $this */ return $this->attributes->get('lottery_player'); }); + + /** + * 【当前请求语言】zh / en / ne,与 lottery.locales.supported 对齐。 + * 由 NegotiateLotteryLocale 写入 attribute;控制台等非 HTTP 或无该中间件时回落到 fallback。 + */ + Request::macro('lotteryLocale', function (): string { + /** @var Request $this */ + return (string) ($this->attributes->get('lottery_locale') + ?? config('lottery.locales.fallback', 'en')); + }); } } diff --git a/app/Support/LotteryMessage.php b/app/Support/LotteryMessage.php new file mode 100644 index 0000000..4a836c8 --- /dev/null +++ b/app/Support/LotteryMessage.php @@ -0,0 +1,36 @@ +attributes->get('lottery_locale') ?? $fallback); + $key = 'sso.'.$code; // 对应 lang/{locale}/sso.php 内键名,如 '8001' + + $msg = trans($key, [], $locale); + // Laravel 无键时返回整条 key 字符串,此时改用 fallback 语言再试一次 + if ($msg !== $key) { + return $msg; + } + + return trans($key, [], $fallback); + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index 9c6ec00..f98c50d 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -1,5 +1,14 @@ withMiddleware(function (Middleware $middleware): void { + // 多语言:必须在其他 api 中间件之前执行,以便鉴权失败时也能按语言返回 msg + $middleware->api(prepend: [ + \App\Http\Middleware\NegotiateLotteryLocale::class, + ]); $middleware->alias([ // 玩家端需登录路由使用;解析 Bearer → Player 'lottery.player' => \App\Http\Middleware\EnsurePlayerApi::class, diff --git a/config/lottery.php b/config/lottery.php index 05a0fd5..853625c 100644 --- a/config/lottery.php +++ b/config/lottery.php @@ -1,7 +1,20 @@ [ + 'supported' => ['zh', 'en', 'ne'], + 'fallback' => 'en', + ], + 'default_currency' => env('LOTTERY_DEFAULT_CURRENCY', 'NPR'), 'main_site' => [ diff --git a/lang/en/sso.php b/lang/en/sso.php new file mode 100644 index 0000000..aa7d0f2 --- /dev/null +++ b/lang/en/sso.php @@ -0,0 +1,14 @@ + 'Missing or invalid Authorization header', // 无 Bearer / 格式错误 / token 为空 + '8002' => 'Token invalid or expired', // JWT 无效、过期或 dev: 格式错误等 + '8003' => 'Player not registered', // 库中无对应玩家 + '8004' => 'SSO secret not configured', // 未配置 MAIN_SITE_SSO_JWT_SECRET(通常返回 503) +]; diff --git a/lang/ne/sso.php b/lang/ne/sso.php new file mode 100644 index 0000000..7453bf1 --- /dev/null +++ b/lang/ne/sso.php @@ -0,0 +1,11 @@ + 'अनुमति हेडर छैन वा अमान्य', + '8002' => 'टोकन अमान्य वा समयावधि सकियो', + '8003' => 'खेलाडी दर्ता छैन', + '8004' => 'SSO गोप्य सेट छैन', +]; diff --git a/lang/zh/sso.php b/lang/zh/sso.php new file mode 100644 index 0000000..30d728a --- /dev/null +++ b/lang/zh/sso.php @@ -0,0 +1,11 @@ + '缺少或无效的 Authorization', + '8002' => '令牌无效或已过期', + '8003' => '玩家未建档', + '8004' => '未配置 SSO 密钥', +];