feat: 增强玩家端 API 鉴权中间件,支持多语言错误消息返回
This commit is contained in:
@@ -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 混淆
|
||||
|
||||
77
app/Http/Middleware/NegotiateLotteryLocale.php
Normal file
77
app/Http/Middleware/NegotiateLotteryLocale.php
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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'));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
36
app/Support/LotteryMessage.php
Normal file
36
app/Support/LotteryMessage.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* 【业务文案翻译辅助类】
|
||||
*
|
||||
* 从 lang/{locale}/sso.php 等语言包取字符串,供 JSON 里「msg」字段使用。
|
||||
* 当前实现:玩家 SSO / Bearer 鉴权相关错误码(8001–8004),见 docs/04 错误码段。
|
||||
*
|
||||
* 依赖 NegotiateLotteryLocale 已写入 lottery_locale;若未写则使用 fallback 语言包。
|
||||
*/
|
||||
final class LotteryMessage
|
||||
{
|
||||
/**
|
||||
* 取 SSO 鉴权类错误的用户可见文案(与 ApiResponse 的 msg 对应)。
|
||||
*
|
||||
* @param int $code 业务错误码,如 8001
|
||||
*/
|
||||
public static function sso(Request $request, int $code): string
|
||||
{
|
||||
$fallback = (string) config('lottery.locales.fallback', 'en');
|
||||
$locale = (string) ($request->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);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,14 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| 【应用入口】路由与中间件
|
||||
|--------------------------------------------------------------------------
|
||||
| api 分组 prepend了 NegotiateLotteryLocale:
|
||||
| 保证任意 API 在未进入控制器前已确定 lottery_locale / app locale,便于统一翻译 msg。
|
||||
|--------------------------------------------------------------------------
|
||||
*/
|
||||
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Foundation\Configuration\Exceptions;
|
||||
use Illuminate\Foundation\Configuration\Middleware;
|
||||
@@ -13,6 +22,10 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
health: '/up',
|
||||
)
|
||||
->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,
|
||||
|
||||
@@ -1,7 +1,20 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 彩票业务配置(多语言、主站 SSO、玩家鉴权等)。
|
||||
*
|
||||
* 【locales】API 返回 msg 时使用的语言集合,与库表多语言列命名一致(zh / en / ne)。
|
||||
* - supported:合法语言列表;NegotiateLotteryLocale 只会在其中选择。
|
||||
* - fallback:无匹配头或缺翻译键时的兜底语言(通常用 en)。
|
||||
*/
|
||||
|
||||
return [
|
||||
|
||||
'locales' => [
|
||||
'supported' => ['zh', 'en', 'ne'],
|
||||
'fallback' => 'en',
|
||||
],
|
||||
|
||||
'default_currency' => env('LOTTERY_DEFAULT_CURRENCY', 'NPR'),
|
||||
|
||||
'main_site' => [
|
||||
|
||||
14
lang/en/sso.php
Normal file
14
lang/en/sso.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 【英文】玩家端 SSO / Bearer 鉴权相关的 API 文案(对应 JSON 的 msg 字段)。
|
||||
*
|
||||
* - 键名与业务错误码字符串一致(如 8001),与 LotteryMessage::sso()、docs/04 错误码对齐。
|
||||
* - 英文字段可作开发兜底及未翻译场景的 fallback。
|
||||
*/
|
||||
return [
|
||||
'8001' => '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)
|
||||
];
|
||||
11
lang/ne/sso.php
Normal file
11
lang/ne/sso.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 【尼泊尔语 नेपाली】同上,结构与 en/zh 完全一致,键仍为错误码字符串。
|
||||
*/
|
||||
return [
|
||||
'8001' => 'अनुमति हेडर छैन वा अमान्य',
|
||||
'8002' => 'टोकन अमान्य वा समयावधि सकियो',
|
||||
'8003' => 'खेलाडी दर्ता छैन',
|
||||
'8004' => 'SSO गोप्य सेट छैन',
|
||||
];
|
||||
11
lang/zh/sso.php
Normal file
11
lang/zh/sso.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* 【简体中文】同上,参见 lang/en/sso.php 注释。
|
||||
*/
|
||||
return [
|
||||
'8001' => '缺少或无效的 Authorization',
|
||||
'8002' => '令牌无效或已过期',
|
||||
'8003' => '玩家未建档',
|
||||
'8004' => '未配置 SSO 密钥',
|
||||
];
|
||||
Reference in New Issue
Block a user