- 在 SyncAdminAuthorizationCommand 中新增对代理和抽奖菜单操作的同步功能,确保缺失的菜单操作行能够被创建。 - 更新多个控制器中的权限检查逻辑,使用 hasPermissionCode 替代原有的权限验证方式,提升权限管理的灵活性。 - 引入 ApiMessage 统一错误响应格式,确保在权限不足时返回一致的错误信息。 - 更新 AdminRole 和 AdminUser 模型,增强角色与用户的权限管理功能,支持更细粒度的权限控制。
204 lines
7.9 KiB
PHP
204 lines
7.9 KiB
PHP
<?php
|
||
|
||
/*
|
||
|--------------------------------------------------------------------------
|
||
| 【应用入口】路由与中间件
|
||
|--------------------------------------------------------------------------
|
||
| api 分组 prepend了 NegotiateLotteryLocale:
|
||
| 保证任意 API 在未进入控制器前已确定 lottery_locale / app locale,便于统一翻译 msg。
|
||
|--------------------------------------------------------------------------
|
||
*/
|
||
|
||
use App\Lottery\ErrorCode;
|
||
use App\Support\ApiResponse;
|
||
use App\Support\ApiValidationErrors;
|
||
use Illuminate\Http\Request;
|
||
use App\Support\LotteryLocale;
|
||
use Illuminate\Foundation\Application;
|
||
use App\Http\Middleware\EnsureAdminApi;
|
||
use App\Http\Middleware\EnsurePlayerApi;
|
||
use App\Http\Middleware\RecordAdminApiAudit;
|
||
use Illuminate\Console\Scheduling\Schedule;
|
||
use Illuminate\Auth\AuthenticationException;
|
||
use App\Http\Middleware\EnsureAdminApiResourcePermission;
|
||
use Illuminate\Validation\ValidationException;
|
||
use App\Http\Middleware\NegotiateLotteryLocale;
|
||
use Illuminate\Foundation\Configuration\Exceptions;
|
||
use Illuminate\Foundation\Configuration\Middleware;
|
||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||
use Symfony\Component\HttpKernel\Exception\HttpException;
|
||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||
use Symfony\Component\HttpKernel\Exception\TooManyRequestsHttpException;
|
||
|
||
return Application::configure(basePath: dirname(__DIR__))
|
||
->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->convertEmptyStringsToNull([
|
||
static fn (Request $request): bool => $request->is('api/v1/admin/settings')
|
||
|| $request->is('api/v1/admin/settings/*'),
|
||
]);
|
||
$middleware->alias([
|
||
// 玩家端需登录路由使用;解析 Bearer → Player
|
||
'lottery.player' => EnsurePlayerApi::class,
|
||
// 后台 API 预留:Sanctum / RBAC
|
||
'lottery.admin' => EnsureAdminApi::class,
|
||
'admin.api-resource' => EnsureAdminApiResourcePermission::class,
|
||
'admin.audit' => RecordAdminApiAudit::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;
|
||
}
|
||
|
||
$loc = $locale($request);
|
||
$errors = ApiValidationErrors::normalize($e->errors(), $loc);
|
||
$msg = ApiValidationErrors::summary($errors)
|
||
?? trans('api.validation_failed', [], $loc);
|
||
|
||
return ApiResponse::error(
|
||
$msg,
|
||
ErrorCode::ValidationFailed->value,
|
||
['errors' => $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 {
|
||
/** 开奖时刻后尽快跑 RNG/冷静期,避免大厅在 0:00 卡住最多 1 分钟 */
|
||
$schedule->command('lottery:draw-tick')
|
||
->everyTenSeconds()
|
||
->withoutOverlapping()
|
||
->onOneServer();
|
||
$schedule->command('lottery:wallet-transfer-reconcile --lookback-hours=24 --stale-minutes=15 --limit=1000')
|
||
->everyTenMinutes()
|
||
->withoutOverlapping()
|
||
->onOneServer();
|
||
$schedule->command('lottery:ticket-pending-confirm-reconcile --stale-minutes=5 --limit=500')
|
||
->everyMinute()
|
||
->withoutOverlapping()
|
||
->onOneServer();
|
||
/** @see docs/01-界面文档.md §2.1 `draw.countdown` */
|
||
if (config('lottery.realtime_hall_countdown', true)) {
|
||
$schedule->command('lottery:hall-countdown')
|
||
->everySecond()
|
||
->withoutOverlapping()
|
||
->onOneServer();
|
||
}
|
||
})
|
||
->create();
|