feat: 增强管理员 API 鉴权,新增 token 有效天数配置,更新相关异常处理与错误码引用
This commit is contained in:
@@ -2,12 +2,13 @@
|
||||
|
||||
namespace App\Exceptions;
|
||||
|
||||
use App\Lottery\ErrorCode;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* 玩家端 Bearer 鉴权失败时抛出,由 EnsurePlayerApi 捕获并转为 JSON。
|
||||
*
|
||||
* @property-read int $lotteryCode 业务错误码(对齐 docs/04-领域字典 §10,SSO 段 8000–8999)
|
||||
* @property-read int $lotteryCode 业务错误码({@see ErrorCode})
|
||||
* @property-read int $httpStatus HTTP 状态码(401 未授权、503 服务未配置等)
|
||||
*/
|
||||
final class PlayerAuthenticationException extends RuntimeException
|
||||
|
||||
@@ -70,7 +70,12 @@ final class LoginController
|
||||
);
|
||||
}
|
||||
|
||||
$plainToken = $admin->createToken('admin-api')->plainTextToken;
|
||||
$ttlDays = (int) config('lottery.admin_api.token_ttl_days', 7);
|
||||
$plainToken = $admin->createToken(
|
||||
'admin-api',
|
||||
['*'],
|
||||
now()->addDays(max(1, $ttlDays)),
|
||||
)->plainTextToken;
|
||||
|
||||
$admin->forceFill(['last_login_at' => now()])->save();
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Api\V1\Wallet;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Models\Player;
|
||||
use App\Models\PlayerWallet;
|
||||
use App\Support\ApiResponse;
|
||||
@@ -58,7 +59,7 @@ class WalletBalanceController extends Controller
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|JsonResponse 合法币种码或错误响应(code 1003:参数非法)
|
||||
* @return string|JsonResponse 合法币种码或错误响应({@see ErrorCode::WalletInvalidCurrency})
|
||||
*/
|
||||
private function resolveCurrencyCode(Request $request, Player $player): string|JsonResponse
|
||||
{
|
||||
@@ -72,10 +73,9 @@ class WalletBalanceController extends Controller
|
||||
|
||||
// 币种码:字母数字,长度 1–16,与 migrations 字段一致
|
||||
if (! preg_match('/^[A-Z0-9]{1,16}$/', $code)) {
|
||||
// 业务码占用 1000–1999 钱包段;1003 已在 PRD 保留为「金额超出限制」,币种非法单用 1005
|
||||
return ApiResponse::error(
|
||||
__('wallet.invalid_currency'),
|
||||
1005,
|
||||
ErrorCode::WalletInvalidCurrency->value,
|
||||
null,
|
||||
400,
|
||||
);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Middleware;
|
||||
|
||||
use App\Exceptions\PlayerAuthenticationException;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Services\PlayerTokenResolver;
|
||||
use App\Support\ApiResponse;
|
||||
use App\Support\LotteryMessage;
|
||||
@@ -15,7 +16,7 @@ use Symfony\Component\HttpFoundation\Response;
|
||||
*
|
||||
* - 成功:解析 Bearer → Player,写入 request attribute `lottery_player`。
|
||||
* - 失败:直接 JSON 返回,不进入控制器;其中 msg 经由 LotteryMessage::sso() 按请求语言翻译
|
||||
* (依赖前置的 NegotiateLotteryLocale),code 仍为文档约定业务码(如 8001)。
|
||||
* (依赖前置的 NegotiateLotteryLocale),code 为 {@see ErrorCode} 中玩家鉴权段。
|
||||
*
|
||||
* PlayerAuthenticationException 的 getMessage() 仅作开发与日志用语,可与 API msg 语种不一致。
|
||||
*/
|
||||
|
||||
@@ -3,35 +3,87 @@
|
||||
namespace App\Lottery;
|
||||
|
||||
/**
|
||||
* HTTP JSON 中与 `ApiResponse` 对齐的业务码常量(对齐 docs/04 §10)。
|
||||
* SSO 等特殊场景仍由各模块直接写整数(如 EnsurePlayerApi 使用的 8001)。
|
||||
* HTTP JSON 业务码 `code`(与 `ApiResponse`、docs/04 §10 对齐)。
|
||||
*
|
||||
* 区间约定:0 成功;1000–1999 钱包;2000–2999 下注;8000–8999 SSO/权限;9000–9999 系统。
|
||||
* 新增错误时在此登记,业务代码引用本枚举,勿散落魔法数字。
|
||||
*/
|
||||
enum ErrorCode: int
|
||||
{
|
||||
/** 管理端 API:未登录或 Token 无效 */
|
||||
/* ========== 成功 ========== */
|
||||
|
||||
/** 业务成功(与 `ApiResponse::success` 默认一致) */
|
||||
case Success = 0;
|
||||
|
||||
/* ========== 1000–1999 钱包 / 转账 ========== */
|
||||
|
||||
/** PRD:余额不足 */
|
||||
case WalletInsufficientBalance = 1001;
|
||||
|
||||
/** PRD:处理中(转账) */
|
||||
case WalletTransferPending = 1002;
|
||||
|
||||
/** PRD:金额超出限制 */
|
||||
case WalletAmountExceedsLimit = 1003;
|
||||
|
||||
/**
|
||||
* PRD:钱包查询等场景下请求参数无效;当前用于 `currency` 非法(与 1003 语义区分)。
|
||||
*/
|
||||
case WalletInvalidCurrency = 1005;
|
||||
|
||||
/* ========== 2000–2999 下注 / 注单(PRD 保留,业务未实现时亦可提前登记) ========== */
|
||||
|
||||
/** PRD:当期已封盘 */
|
||||
case DrawClosed = 2001;
|
||||
|
||||
/** PRD:玩法已关闭 */
|
||||
case PlayModeClosed = 2002;
|
||||
|
||||
/** PRD:下注语境余额不足(可与 1001 同语义) */
|
||||
case BetInsufficientBalance = 2003;
|
||||
|
||||
/* ========== 8000–8999 玩家 SSO / Bearer 鉴权 ========== */
|
||||
|
||||
/** 无 Bearer / 格式错误 / token 为空 */
|
||||
case PlayerAuthorizationInvalid = 8001;
|
||||
|
||||
/** JWT 无效或过期、dev: 格式错误、缺少站点或玩家标识等 */
|
||||
case PlayerTokenInvalid = 8002;
|
||||
|
||||
/** 库中无对应玩家(未建档) */
|
||||
case PlayerNotRegistered = 8003;
|
||||
|
||||
/** 未配置 `MAIN_SITE_SSO_JWT_SECRET`(通常 HTTP 503) */
|
||||
case PlayerSsoSecretNotConfigured = 8004;
|
||||
|
||||
/* ========== 8100–8199 管理端 API ========== */
|
||||
|
||||
/** 未登录或 Token 无效 */
|
||||
case AdminUnauthenticated = 8110;
|
||||
|
||||
/** 管理端登录:验证码错误或过期 */
|
||||
/** 登录:验证码错误或过期 */
|
||||
case AdminCaptchaInvalid = 8111;
|
||||
|
||||
/** 管理端登录:账号或密码不匹配(对外统一措辞) */
|
||||
/** 登录:账号或密码不匹配(对外统一措辞) */
|
||||
case AdminCredentialsInvalid = 8112;
|
||||
|
||||
/** 管理端登录:账号已禁用 */
|
||||
/** 登录:账号已禁用 */
|
||||
case AdminAccountDisabled = 8113;
|
||||
|
||||
/** 表单 / Query 校验失败(见 ValidationException → 422) */
|
||||
/* ========== 9000–9999 系统 / 框架 ========== */
|
||||
|
||||
/** 表单或 Query 校验失败(ValidationException → 422) */
|
||||
case ValidationFailed = 9001;
|
||||
|
||||
/** 资源或路由不存在 */
|
||||
/** 模型或路由不存在 */
|
||||
case NotFound = 9004;
|
||||
|
||||
/** `abort(4xx)` 等客户端类 Http 异常归类 */
|
||||
case ClientHttpError = 9010;
|
||||
|
||||
/** 请求过于频繁 */
|
||||
case TooManyRequests = 9031;
|
||||
|
||||
/** `abort(4xx)` 等客户端类 Http 异常(HTTP 状态码见响应头;本码作业务归类) */
|
||||
case ClientHttpError = 9010;
|
||||
|
||||
/** 未分类服务端异常(生产环境不向客户端暴露细节) */
|
||||
/** 未分类服务端异常 */
|
||||
case InternalError = 9999;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Services;
|
||||
|
||||
use App\Exceptions\PlayerAuthenticationException;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Models\Player;
|
||||
use Firebase\JWT\JWT;
|
||||
use Firebase\JWT\Key;
|
||||
@@ -17,11 +18,7 @@ use Illuminate\Http\Request;
|
||||
* 2) 生产:使用 `MAIN_SITE_SSO_JWT_SECRET` 验签 JWT(默认 HS256),
|
||||
* 从 payload 读取 `site_code`、`site_player_id`(字段名可 env 覆盖)再查 players 表。
|
||||
*
|
||||
* 错误码约定(见中间件返回 JSON 的 code):
|
||||
* - 8001:无 Bearer / Token 空 / Header 格式不对
|
||||
* - 8002:JWT 无效、过期或缺少站点/玩家字段
|
||||
* - 8003:数据库无对应玩家(未建档)
|
||||
* - 8004:未配置 MAIN_SITE_SSO_JWT_SECRET(HTTP 503)
|
||||
* 错误码约定(见 {@see ErrorCode} 玩家 SSO 段):
|
||||
*/
|
||||
final class PlayerTokenResolver
|
||||
{
|
||||
@@ -29,13 +26,16 @@ final class PlayerTokenResolver
|
||||
{
|
||||
$header = $request->header('Authorization', '');
|
||||
if (! is_string($header) || ! str_starts_with($header, 'Bearer ')) {
|
||||
throw new PlayerAuthenticationException('缺少或非法 Authorization', 8001);
|
||||
throw new PlayerAuthenticationException(
|
||||
'缺少或非法 Authorization',
|
||||
ErrorCode::PlayerAuthorizationInvalid->value,
|
||||
);
|
||||
}
|
||||
|
||||
// 标准:`Authorization: Bearer <token>`,此处去掉前缀 7 字节 "Bearer "
|
||||
$token = trim(substr($header, 7));
|
||||
if ($token === '') {
|
||||
throw new PlayerAuthenticationException('Token 为空', 8001);
|
||||
throw new PlayerAuthenticationException('Token 为空', ErrorCode::PlayerAuthorizationInvalid->value);
|
||||
}
|
||||
|
||||
// 本地 dev: 优先于 JWT,避免未配密钥时仍能测需登录接口
|
||||
@@ -46,7 +46,11 @@ final class PlayerTokenResolver
|
||||
// 与 .env 中 MAIN_SITE_SSO_JWT_SECRET 一致,用于 firebase/php-jwt 验签
|
||||
$secret = config('lottery.main_site.sso_jwt_secret');
|
||||
if (! is_string($secret) || $secret === '') {
|
||||
throw new PlayerAuthenticationException('SSO 未配置(MAIN_SITE_SSO_JWT_SECRET)', 8004, 503);
|
||||
throw new PlayerAuthenticationException(
|
||||
'SSO 未配置(MAIN_SITE_SSO_JWT_SECRET)',
|
||||
ErrorCode::PlayerSsoSecretNotConfigured->value,
|
||||
503,
|
||||
);
|
||||
}
|
||||
|
||||
return $this->resolveJwt($token, $secret);
|
||||
@@ -61,12 +65,15 @@ final class PlayerTokenResolver
|
||||
private function resolveDevToken(string $token): Player
|
||||
{
|
||||
if (! preg_match('/^dev:(\d+)$/', $token, $m)) {
|
||||
throw new PlayerAuthenticationException('开发 Token 格式应为 dev:{玩家ID}', 8002);
|
||||
throw new PlayerAuthenticationException(
|
||||
'开发 Token 格式应为 dev:{玩家ID}',
|
||||
ErrorCode::PlayerTokenInvalid->value,
|
||||
);
|
||||
}
|
||||
|
||||
$player = Player::query()->find((int) $m[1]);
|
||||
if ($player === null) {
|
||||
throw new PlayerAuthenticationException('玩家不存在', 8003);
|
||||
throw new PlayerAuthenticationException('玩家不存在', ErrorCode::PlayerNotRegistered->value);
|
||||
}
|
||||
|
||||
return $player;
|
||||
@@ -81,7 +88,7 @@ final class PlayerTokenResolver
|
||||
$claims = JWT::decode($jwt, new Key($secret, $alg));
|
||||
} catch (\Throwable $e) {
|
||||
// 签名错误、exp 过期、格式损坏等均归 8002
|
||||
throw new PlayerAuthenticationException('Token 无效或已过期', 8002);
|
||||
throw new PlayerAuthenticationException('Token 无效或已过期', ErrorCode::PlayerTokenInvalid->value);
|
||||
}
|
||||
|
||||
// 与主站约定 JWT 里字段名;若主站用 sub/iss 等可改 env LOTTERY_JWT_CLAIM_*
|
||||
@@ -92,7 +99,7 @@ final class PlayerTokenResolver
|
||||
$sitePlayerId = data_get($claims, $pidKey);
|
||||
|
||||
if (! is_string($siteCode) || $siteCode === '' || ! is_string($sitePlayerId) || $sitePlayerId === '') {
|
||||
throw new PlayerAuthenticationException('JWT 缺少站点或玩家标识', 8002);
|
||||
throw new PlayerAuthenticationException('JWT 缺少站点或玩家标识', ErrorCode::PlayerTokenInvalid->value);
|
||||
}
|
||||
|
||||
// 首期:库中必须先有该行;若需「首次进入自动建档」可在此处 firstOrCreate
|
||||
@@ -102,7 +109,7 @@ final class PlayerTokenResolver
|
||||
->first();
|
||||
|
||||
if ($player === null) {
|
||||
throw new PlayerAuthenticationException('玩家未建档', 8003);
|
||||
throw new PlayerAuthenticationException('玩家未建档', ErrorCode::PlayerNotRegistered->value);
|
||||
}
|
||||
|
||||
return $player;
|
||||
|
||||
@@ -7,7 +7,7 @@ use Illuminate\Http\JsonResponse;
|
||||
/**
|
||||
* 对外 API 统一 JSON 结构:{ code, msg, data }。
|
||||
*
|
||||
* - code=0 表示成功;非 0 为业务码(见 docs/04-领域字典与编码规范.md)。
|
||||
* - `code=0` 即 {@see \App\Lottery\ErrorCode::Success};非 0 见 `ErrorCode` 与 docs/04 §10。
|
||||
* - error() 的 HTTP 状态可与 code 独立(如鉴权失败 401 + code 8001)。
|
||||
*/
|
||||
final class ApiResponse
|
||||
|
||||
@@ -2,13 +2,14 @@
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Lottery\ErrorCode;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
/**
|
||||
* 【业务文案翻译辅助类】
|
||||
*
|
||||
* 从 lang/{locale}/sso.php 等语言包取字符串,供 JSON 里「msg」字段使用。
|
||||
* 当前实现:玩家 SSO / Bearer 鉴权相关错误码(8001–8004),见 docs/04 错误码段。
|
||||
* `App\Lottery\ErrorCode` 中玩家 SSO 段(8001–8004)的各语言 `msg`。
|
||||
*
|
||||
* 依赖 NegotiateLotteryLocale 已写入 lottery_locale;若未写则使用 fallback 语言包。
|
||||
*/
|
||||
@@ -17,7 +18,7 @@ final class LotteryMessage
|
||||
/**
|
||||
* 取 SSO 鉴权类错误的用户可见文案(与 ApiResponse 的 msg 对应)。
|
||||
*
|
||||
* @param int $code 业务错误码,如 8001
|
||||
* @param int $code {@see ErrorCode} 玩家 SSO 段(8001–8004)
|
||||
*/
|
||||
public static function sso(Request $request, int $code): string
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user