withRouting( web: __DIR__.'/../routes/web.php', // 自动加前缀 `api` + middleware `api`,见 routes/api.php api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', channels: __DIR__.'/../routes/channels.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { // 多语言:必须在其他 api 中间件之前执行,以便鉴权失败时也能按语言返回 msg $middleware->api(prepend: [ NegotiateLotteryLocale::class, ]); $middleware->alias([ // 玩家端需登录路由使用;解析 Bearer → Player 'lottery.player' => EnsurePlayerApi::class, // 后台 API 预留:Sanctum / RBAC 'lottery.admin' => EnsureAdminApi::class, 'admin.permission' => EnsureAdminPermission::class, 'admin.api-resource' => EnsureAdminApiResourcePermission::class, ]); }) ->withExceptions(function (Exceptions $exceptions): void { /** * 统一 JSON:与 {@see ApiResponse} 一致,供三端消费;多语言与 {@see LotteryLocale} 对齐。 * 覆盖:校验失败、模型/路由 404、限流、未处理异常(生产不泄露堆栈文案)。 */ $locale = static function (Request $request): string { return (string) ($request->attributes->get('lottery_locale') ?? LotteryLocale::resolve($request)); }; $exceptions->render(function (AuthenticationException $e, Request $request) use ($locale) { if (! $request->is('api/*')) { return null; } return ApiResponse::error( trans('admin.unauthenticated', [], $locale($request)), ErrorCode::AdminUnauthenticated->value, null, 401, ); }); $exceptions->render(function (ValidationException $e, Request $request) use ($locale) { if (! $request->is('api/*')) { return null; } return ApiResponse::error( trans('api.validation_failed', [], $locale($request)), ErrorCode::ValidationFailed->value, ['errors' => $e->errors()], 422, ); }); $exceptions->render(function (ModelNotFoundException $e, Request $request) use ($locale) { if (! $request->is('api/*')) { return null; } return ApiResponse::error( trans('api.not_found', [], $locale($request)), ErrorCode::NotFound->value, null, 404, ); }); $exceptions->render(function (NotFoundHttpException $e, Request $request) use ($locale) { if (! $request->is('api/*')) { return null; } return ApiResponse::error( trans('api.not_found', [], $locale($request)), ErrorCode::NotFound->value, null, 404, ); }); $exceptions->render(function (TooManyRequestsHttpException $e, Request $request) use ($locale) { if (! $request->is('api/*')) { return null; } return ApiResponse::error( trans('api.too_many_requests', [], $locale($request)), ErrorCode::TooManyRequests->value, null, 429, ); }); $exceptions->render(function (HttpException $e, Request $request) use ($locale) { if (! $request->is('api/*')) { return null; } if ($e instanceof NotFoundHttpException || $e instanceof TooManyRequestsHttpException) { return null; } $status = $e->getStatusCode(); $msg = $e->getMessage(); if ($msg === '') { $msg = trans('api.client_error', [], $locale($request)); } $code = $status >= 500 ? ErrorCode::InternalError->value : ErrorCode::ClientHttpError->value; return ApiResponse::error($msg, $code, null, $status); }); $exceptions->render(function (Throwable $e, Request $request) use ($locale) { if (! $request->is('api/*')) { return null; } if ($e instanceof ValidationException || $e instanceof ModelNotFoundException || $e instanceof NotFoundHttpException || $e instanceof TooManyRequestsHttpException || $e instanceof HttpException ) { return null; } $showDetails = (bool) config('app.debug'); $msg = $showDetails ? $e->getMessage() : trans('api.server_error', [], $locale($request)); return ApiResponse::error( $msg !== '' ? $msg : trans('api.server_error', [], $locale($request)), ErrorCode::InternalError->value, $showDetails ? ['exception' => $e::class] : null, 500, ); }); }) ->withSchedule(function (Schedule $schedule): void { $schedule->command('lottery:draw-tick')->everyMinute(); $schedule->command('lottery:wallet-transfer-reconcile --lookback-hours=24 --stale-minutes=15 --limit=1000') ->everyTenMinutes() ->withoutOverlapping(); $schedule->command('lottery:ticket-pending-confirm-reconcile --stale-minutes=5 --limit=500') ->everyMinute() ->withoutOverlapping(); /** @see docs/01-界面文档.md §2.1 `draw.countdown` */ if (config('lottery.realtime_hall_countdown', true)) { $schedule->command('lottery:hall-countdown')->everySecond(); } }) ->create();