Files
kang a44679665d feat: 增强代理和玩家管理功能
- 在多个控制器中更新权限检查逻辑,确保管理员能够更灵活地管理代理和玩家。
- 在 AdminPlayerStoreController 中引入对玩家创建能力的验证,确保只有具备相应权限的管理员能够创建玩家。
- 更新请求验证逻辑,新增 credit_limit、rebate_rate 和 extra_rebate_rate 字段,以支持更细粒度的玩家管理。
- 在 AgentNodeProfileController 中添加对父代理能力授予的验证,确保子代理的权限在父代理范围内。
- 引入 AgentProfileFieldRules 以简化代理资料更新请求的规则定义,提升代码复用性。
2026-06-04 18:00:50 +08:00

215 lines
8.3 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?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->redirectGuestsTo(static function (Request $request): ?string {
if ($request->is('api/*')) {
return null;
}
return '/login';
});
$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();
$schedule->command('settlement:mark-overdue-bills --days=7')
->dailyAt('02:00')
->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();