From 0527c7c392b17ac535bbcb66088b1d4b65db9cd2 Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 3 Jun 2026 10:56:36 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=BC=BA=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E5=91=98=E6=9D=83=E9=99=90=E4=B8=8E=E8=A7=92=E8=89=B2=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在 SyncAdminAuthorizationCommand 中新增对代理和抽奖菜单操作的同步功能,确保缺失的菜单操作行能够被创建。 - 更新多个控制器中的权限检查逻辑,使用 hasPermissionCode 替代原有的权限验证方式,提升权限管理的灵活性。 - 引入 ApiMessage 统一错误响应格式,确保在权限不足时返回一致的错误信息。 - 更新 AdminRole 和 AdminUser 模型,增强角色与用户的权限管理功能,支持更细粒度的权限控制。 --- .../SyncAdminAuthorizationCommand.php | 12 + .../Agent/AgentAdminUserDestroyController.php | 63 ++++ .../AgentAdminUserRoleSyncController.php | 12 +- .../AgentNodeAdminUserStoreController.php | 12 +- .../Agent/AgentNodeDestroyController.php | 2 +- .../Agent/AgentNodeRoleStoreController.php | 12 +- .../Admin/AdminCurrencyStoreRequest.php | 4 +- .../Admin/AdminCurrencyUpdateRequest.php | 4 +- ...IntegrationSiteConnectivityTestRequest.php | 4 +- .../AdminIntegrationSiteStoreRequest.php | 4 +- .../AdminIntegrationSiteUpdateRequest.php | 4 +- app/Http/Requests/Admin/AdminLoginRequest.php | 16 +- .../Admin/AdminPlayerIndexRequest.php | 4 +- .../Admin/AdminPlayerStoreRequest.php | 4 +- .../Admin/AdminPlayerTicketItemsRequest.php | 4 +- .../Admin/AdminPlayerUpdateRequest.php | 4 +- .../Admin/AdminReportQueryRequest.php | 4 +- .../Admin/AdminRolePermissionSyncRequest.php | 4 +- .../Requests/Admin/AdminRoleStoreRequest.php | 4 +- .../Requests/Admin/AdminRoleUpdateRequest.php | 4 +- .../Admin/AdminSettingBatchUpdateRequest.php | 4 +- .../Admin/AdminSettingIndexRequest.php | 4 +- .../Admin/AdminSettingUpdateRequest.php | 4 +- .../Admin/AdminUserPermissionSyncRequest.php | 4 +- .../Admin/AdminUserRoleSyncRequest.php | 4 +- .../Requests/Admin/AdminUserStoreRequest.php | 4 +- .../Requests/Admin/AdminUserUpdateRequest.php | 4 +- .../Admin/AgentAdminUserRoleSyncRequest.php | 4 +- .../Admin/AgentAdminUserStoreRequest.php | 4 +- .../Admin/AgentDelegationGrantSyncRequest.php | 4 +- .../Requests/Admin/AgentNodeStoreRequest.php | 4 +- .../Requests/Admin/AgentNodeUpdateRequest.php | 4 +- .../Admin/AgentRolePermissionSyncRequest.php | 4 +- .../Requests/Admin/AgentRoleStoreRequest.php | 4 +- .../Requests/Admin/AgentRoleUpdateRequest.php | 4 +- .../Admin/DashboardAnalyticsRequest.php | 4 +- .../DrawManualResultBatchStoreRequest.php | 6 +- app/Http/Requests/Admin/DrawReopenRequest.php | 4 +- app/Http/Requests/Admin/DrawStoreRequest.php | 4 +- .../Jackpot/AdminJackpotPoolAdjustRequest.php | 4 +- .../Admin/ReconcileJobStoreRequest.php | 4 +- .../Requests/Admin/ReportJobStoreRequest.php | 4 +- .../Admin/SettlementBatchReviewRequest.php | 4 +- .../SettlementPayoutAdjustmentRequest.php | 4 +- .../Requests/Admin/TicketItemListRequest.php | 4 +- .../Admin/TransferOrderListRequest.php | 4 +- .../TransferOrderCompleteCreditRequest.php | 4 +- .../TransferOrderManuallyProcessRequest.php | 4 +- .../Wallet/TransferOrderReverseRequest.php | 4 +- .../Admin/WalletTransactionListRequest.php | 4 +- app/Http/Requests/ApiFormRequest.php | 24 ++ app/Http/Requests/Ticket/TicketBetRequest.php | 4 +- .../Requests/Wallet/WalletTransferRequest.php | 4 +- app/Models/AdminRole.php | 17 +- app/Models/AdminUser.php | 10 +- app/Rules/WalletApiUrlRule.php | 2 +- app/Services/Agent/AgentAdminUserService.php | 27 ++ app/Services/Agent/AgentNodeService.php | 14 + app/Services/Agent/AgentRoleService.php | 6 +- .../AdminAgentPermissionMenuActionSync.php | 85 ++++++ app/Support/AdminAuthorizationRegistry.php | 31 +- .../AdminDrawPermissionMenuActionSync.php | 36 +++ app/Support/AdminPermissionInheritance.php | 4 +- app/Support/AgentAdminUserAuthorization.php | 57 ++++ app/Support/AgentRoleAuthorization.php | 2 +- app/Support/ApiValidationErrors.php | 286 ++++++++++++++++++ bootstrap/app.php | 10 +- ...000_split_agent_permission_granularity.php | 177 +++++++++++ ..._agent_admin_user_destroy_api_resource.php | 87 ++++++ database/seeders/AdminRbacAndUserSeeder.php | 5 + lang/en/admin.php | 2 + lang/en/validation.php | 11 + lang/en/validation_attributes.php | 62 ++++ lang/en/validation_business.php | 16 + lang/en/validation_custom.php | 44 +++ lang/en/validation_exact.php | 6 + lang/en/validation_rules.php | 54 ++++ lang/ne/admin.php | 2 + lang/ne/validation.php | 3 + lang/zh/admin.php | 2 + lang/zh/validation.php | 11 + lang/zh/validation_attributes.php | 162 ++++++++++ lang/zh/validation_business.php | 17 ++ lang/zh/validation_custom.php | 60 ++++ lang/zh/validation_exact.php | 7 + lang/zh/validation_rules.php | 113 +++++++ routes/api/v1/admin/agent.php | 3 + .../AdminAgentAccountRoleDestroyTest.php | 126 ++++++++ tests/Feature/AdminAgentDelegationApiTest.php | 16 +- tests/Feature/AdminAgentNodeApiTest.php | 16 +- .../AdminAgentPermissionGranularityTest.php | 244 +++++++++++++++ tests/Feature/AdminAgentRoleApiTest.php | 10 +- .../AdminAgentRolePermissionPersistTest.php | 62 ++++ .../AdminDrawRolePermissionRoundTripTest.php | 60 ++++ tests/Pest.php | 2 + tests/Unit/ApiValidationErrorsTest.php | 52 ++++ 96 files changed, 2215 insertions(+), 139 deletions(-) create mode 100644 app/Http/Controllers/Api/V1/Admin/Agent/AgentAdminUserDestroyController.php create mode 100644 app/Http/Requests/ApiFormRequest.php create mode 100644 app/Support/AdminAgentPermissionMenuActionSync.php create mode 100644 app/Support/AdminDrawPermissionMenuActionSync.php create mode 100644 app/Support/AgentAdminUserAuthorization.php create mode 100644 app/Support/ApiValidationErrors.php create mode 100644 database/migrations/2026_06_03_120000_split_agent_permission_granularity.php create mode 100644 database/migrations/2026_06_03_140000_ensure_agent_admin_user_destroy_api_resource.php create mode 100644 lang/en/validation.php create mode 100644 lang/en/validation_attributes.php create mode 100644 lang/en/validation_business.php create mode 100644 lang/en/validation_custom.php create mode 100644 lang/en/validation_exact.php create mode 100644 lang/en/validation_rules.php create mode 100644 lang/ne/validation.php create mode 100644 lang/zh/validation.php create mode 100644 lang/zh/validation_attributes.php create mode 100644 lang/zh/validation_business.php create mode 100644 lang/zh/validation_custom.php create mode 100644 lang/zh/validation_exact.php create mode 100644 lang/zh/validation_rules.php create mode 100644 tests/Feature/AdminAgentAccountRoleDestroyTest.php create mode 100644 tests/Feature/AdminAgentPermissionGranularityTest.php create mode 100644 tests/Feature/AdminAgentRolePermissionPersistTest.php create mode 100644 tests/Feature/AdminDrawRolePermissionRoundTripTest.php create mode 100644 tests/Unit/ApiValidationErrorsTest.php diff --git a/app/Console/Commands/SyncAdminAuthorizationCommand.php b/app/Console/Commands/SyncAdminAuthorizationCommand.php index a8776a7..346eb2c 100644 --- a/app/Console/Commands/SyncAdminAuthorizationCommand.php +++ b/app/Console/Commands/SyncAdminAuthorizationCommand.php @@ -5,7 +5,9 @@ namespace App\Console\Commands; use Illuminate\Support\Carbon; use Illuminate\Console\Command; use Illuminate\Support\Facades\DB; +use App\Support\AdminAgentPermissionMenuActionSync; use App\Support\AdminAuthorizationRegistry; +use App\Support\AdminDrawPermissionMenuActionSync; final class SyncAdminAuthorizationCommand extends Command { @@ -17,6 +19,16 @@ final class SyncAdminAuthorizationCommand extends Command public function handle(): int { $now = Carbon::now(); + $agentCreated = AdminAgentPermissionMenuActionSync::syncMissing(); + if ($agentCreated > 0) { + $this->info(sprintf('Created %d missing agent menu_action row(s).', $agentCreated)); + } + + $drawCreated = AdminDrawPermissionMenuActionSync::syncMissing(); + if ($drawCreated > 0) { + $this->info(sprintf('Created %d missing draw menu_action row(s).', $drawCreated)); + } + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); foreach (AdminAuthorizationRegistry::resources() as $resource) { diff --git a/app/Http/Controllers/Api/V1/Admin/Agent/AgentAdminUserDestroyController.php b/app/Http/Controllers/Api/V1/Admin/Agent/AgentAdminUserDestroyController.php new file mode 100644 index 0000000..4593938 --- /dev/null +++ b/app/Http/Controllers/Api/V1/Admin/Agent/AgentAdminUserDestroyController.php @@ -0,0 +1,63 @@ +lotteryAdmin(); + abort_if($admin === null, 401); + + $agent = $admin_user->primaryAgentNode(); + if ($agent === null) { + abort(404); + } + + $denied = AgentAdminUserAuthorization::denyUnlessUserManageable($admin, $admin_user); + if ($denied !== null) { + return $denied; + } + + if ((int) $admin->id === (int) $admin_user->id) { + return ApiMessage::errorResponse( + $request, + 'admin.user_cannot_delete_self', + ErrorCode::ValidationFailed->value, + null, + 422, + ); + } + + $before = AdminUserApiPresenter::listItem($admin_user); + $id = (int) $admin_user->id; + $service->destroyUnderAgent($agent, $admin_user); + + AuditLogger::recordForAdmin( + $admin, + $request, + 'agent', + 'agent_admin_user.destroy', + 'admin_user', + (string) $id, + $before, + null, + ); + + return ApiResponse::success(['deleted' => true, 'id' => $id]); + } +} diff --git a/app/Http/Controllers/Api/V1/Admin/Agent/AgentAdminUserRoleSyncController.php b/app/Http/Controllers/Api/V1/Admin/Agent/AgentAdminUserRoleSyncController.php index 3d1293d..26283db 100644 --- a/app/Http/Controllers/Api/V1/Admin/Agent/AgentAdminUserRoleSyncController.php +++ b/app/Http/Controllers/Api/V1/Admin/Agent/AgentAdminUserRoleSyncController.php @@ -8,8 +8,10 @@ use App\Services\AuditLogger; use Illuminate\Http\JsonResponse; use App\Http\Controllers\Controller; use App\Services\Agent\AgentAdminUserService; +use App\Lottery\ErrorCode; use App\Support\AdminAgentNodeAccess; use App\Support\AdminUserApiPresenter; +use App\Support\ApiMessage; use App\Http\Requests\Admin\AgentAdminUserRoleSyncRequest; final class AgentAdminUserRoleSyncController extends Controller @@ -32,8 +34,14 @@ final class AgentAdminUserRoleSyncController extends Controller return $denied; } - if (! $admin->isSuperAdmin() && ! $admin->hasPermissionCode('agent.node.manage')) { - return AdminAgentNodeAccess::denyUnlessCanManageParent($admin, $agent); + if (! $admin->isSuperAdmin() && ! $admin->hasPermissionCode('agent.user.manage')) { + return ApiMessage::errorResponse( + $request, + 'admin.agent_user_manage_denied', + ErrorCode::AdminForbidden->value, + null, + 403, + ); } $before = AdminUserApiPresenter::listItem($admin_user); diff --git a/app/Http/Controllers/Api/V1/Admin/Agent/AgentNodeAdminUserStoreController.php b/app/Http/Controllers/Api/V1/Admin/Agent/AgentNodeAdminUserStoreController.php index 7a05323..3fc6799 100644 --- a/app/Http/Controllers/Api/V1/Admin/Agent/AgentNodeAdminUserStoreController.php +++ b/app/Http/Controllers/Api/V1/Admin/Agent/AgentNodeAdminUserStoreController.php @@ -8,8 +8,10 @@ use App\Services\AuditLogger; use Illuminate\Http\JsonResponse; use App\Http\Controllers\Controller; use App\Services\Agent\AgentAdminUserService; +use App\Lottery\ErrorCode; use App\Support\AdminAgentNodeAccess; use App\Support\AdminUserApiPresenter; +use App\Support\ApiMessage; use App\Http\Requests\Admin\AgentAdminUserStoreRequest; final class AgentNodeAdminUserStoreController extends Controller @@ -27,8 +29,14 @@ final class AgentNodeAdminUserStoreController extends Controller return $denied; } - if (! $admin->isSuperAdmin() && ! $admin->hasPermissionCode('agent.node.manage')) { - return AdminAgentNodeAccess::denyUnlessCanManageParent($admin, $agent_node); + if (! $admin->isSuperAdmin() && ! $admin->hasPermissionCode('agent.user.manage')) { + return ApiMessage::errorResponse( + $request, + 'admin.agent_user_manage_denied', + ErrorCode::AdminForbidden->value, + null, + 403, + ); } $user = $service->createUnderAgent($agent_node, $request->validated()); diff --git a/app/Http/Controllers/Api/V1/Admin/Agent/AgentNodeDestroyController.php b/app/Http/Controllers/Api/V1/Admin/Agent/AgentNodeDestroyController.php index 94c5bd1..a26d619 100644 --- a/app/Http/Controllers/Api/V1/Admin/Agent/AgentNodeDestroyController.php +++ b/app/Http/Controllers/Api/V1/Admin/Agent/AgentNodeDestroyController.php @@ -47,7 +47,7 @@ final class AgentNodeDestroyController extends Controller return ApiMessage::errorResponse($request, 'admin.agent_node_has_users_cannot_delete', ErrorCode::ValidationFailed->value, null, 422); } - if (DB::table('admin_roles')->where('owner_agent_id', (int) $agent_node->id)->exists()) { + if ($service->hasBlockingCustomRoles($agent_node)) { return ApiMessage::errorResponse($request, 'admin.agent_node_has_roles_cannot_delete', ErrorCode::ValidationFailed->value, null, 422); } diff --git a/app/Http/Controllers/Api/V1/Admin/Agent/AgentNodeRoleStoreController.php b/app/Http/Controllers/Api/V1/Admin/Agent/AgentNodeRoleStoreController.php index b3faa70..e2de0e6 100644 --- a/app/Http/Controllers/Api/V1/Admin/Agent/AgentNodeRoleStoreController.php +++ b/app/Http/Controllers/Api/V1/Admin/Agent/AgentNodeRoleStoreController.php @@ -8,8 +8,10 @@ use App\Services\AuditLogger; use Illuminate\Http\JsonResponse; use App\Http\Controllers\Controller; use App\Services\Agent\AgentRoleService; +use App\Lottery\ErrorCode; use App\Support\AdminAgentNodeAccess; use App\Support\AdminRoleApiPresenter; +use App\Support\ApiMessage; use App\Http\Requests\Admin\AgentRoleStoreRequest; final class AgentNodeRoleStoreController extends Controller @@ -27,8 +29,14 @@ final class AgentNodeRoleStoreController extends Controller return $denied; } - if (! $admin->isSuperAdmin() && ! $admin->hasPermissionCode('agent.node.manage')) { - return AdminAgentNodeAccess::denyUnlessCanManageParent($admin, $agent_node); + if (! $admin->isSuperAdmin() && ! $admin->hasPermissionCode('agent.role.manage')) { + return ApiMessage::errorResponse( + $request, + 'admin.agent_role_manage_denied', + ErrorCode::AdminForbidden->value, + null, + 403, + ); } $role = $service->createForAgent($admin, $agent_node, $request->validated()); diff --git a/app/Http/Requests/Admin/AdminCurrencyStoreRequest.php b/app/Http/Requests/Admin/AdminCurrencyStoreRequest.php index d01a9c1..80b60b3 100644 --- a/app/Http/Requests/Admin/AdminCurrencyStoreRequest.php +++ b/app/Http/Requests/Admin/AdminCurrencyStoreRequest.php @@ -3,9 +3,9 @@ namespace App\Http\Requests\Admin; use Illuminate\Validation\Rule; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class AdminCurrencyStoreRequest extends FormRequest +final class AdminCurrencyStoreRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminCurrencyUpdateRequest.php b/app/Http/Requests/Admin/AdminCurrencyUpdateRequest.php index a82a085..1268805 100644 --- a/app/Http/Requests/Admin/AdminCurrencyUpdateRequest.php +++ b/app/Http/Requests/Admin/AdminCurrencyUpdateRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class AdminCurrencyUpdateRequest extends FormRequest +final class AdminCurrencyUpdateRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminIntegrationSiteConnectivityTestRequest.php b/app/Http/Requests/Admin/AdminIntegrationSiteConnectivityTestRequest.php index badb3a9..d22c947 100644 --- a/app/Http/Requests/Admin/AdminIntegrationSiteConnectivityTestRequest.php +++ b/app/Http/Requests/Admin/AdminIntegrationSiteConnectivityTestRequest.php @@ -2,12 +2,12 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; /** * @see \App\Http\Controllers\Api\V1\Admin\Integration\AdminIntegrationSiteConnectivityTestController */ -final class AdminIntegrationSiteConnectivityTestRequest extends FormRequest +final class AdminIntegrationSiteConnectivityTestRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminIntegrationSiteStoreRequest.php b/app/Http/Requests/Admin/AdminIntegrationSiteStoreRequest.php index 5e6c0d0..6990116 100644 --- a/app/Http/Requests/Admin/AdminIntegrationSiteStoreRequest.php +++ b/app/Http/Requests/Admin/AdminIntegrationSiteStoreRequest.php @@ -2,11 +2,11 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; use Illuminate\Validation\Rule; use App\Rules\WalletApiUrlRule; -final class AdminIntegrationSiteStoreRequest extends FormRequest +final class AdminIntegrationSiteStoreRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminIntegrationSiteUpdateRequest.php b/app/Http/Requests/Admin/AdminIntegrationSiteUpdateRequest.php index 34b6397..ca39941 100644 --- a/app/Http/Requests/Admin/AdminIntegrationSiteUpdateRequest.php +++ b/app/Http/Requests/Admin/AdminIntegrationSiteUpdateRequest.php @@ -2,10 +2,10 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; use App\Rules\WalletApiUrlRule; -final class AdminIntegrationSiteUpdateRequest extends FormRequest +final class AdminIntegrationSiteUpdateRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminLoginRequest.php b/app/Http/Requests/Admin/AdminLoginRequest.php index 7b87e9e..5fa46a2 100644 --- a/app/Http/Requests/Admin/AdminLoginRequest.php +++ b/app/Http/Requests/Admin/AdminLoginRequest.php @@ -2,14 +2,14 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; /** * 管理员登录请求。 * * @see LoginController */ -final class AdminLoginRequest extends FormRequest +final class AdminLoginRequest extends ApiFormRequest { public function authorize(): bool { @@ -29,16 +29,4 @@ final class AdminLoginRequest extends FormRequest ]; } - /** - * @return array - */ - public function attributes(): array - { - return [ - 'account' => 'account', - 'password' => 'password', - 'captcha_key' => 'captcha_key', - 'captcha_code' => 'captcha_code', - ]; - } } diff --git a/app/Http/Requests/Admin/AdminPlayerIndexRequest.php b/app/Http/Requests/Admin/AdminPlayerIndexRequest.php index 0ab00b7..84daf58 100644 --- a/app/Http/Requests/Admin/AdminPlayerIndexRequest.php +++ b/app/Http/Requests/Admin/AdminPlayerIndexRequest.php @@ -2,14 +2,14 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; /** * 玩家列表查询请求。 * * @see AdminPlayerIndexController */ -final class AdminPlayerIndexRequest extends FormRequest +final class AdminPlayerIndexRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminPlayerStoreRequest.php b/app/Http/Requests/Admin/AdminPlayerStoreRequest.php index 5ee3430..7894c35 100644 --- a/app/Http/Requests/Admin/AdminPlayerStoreRequest.php +++ b/app/Http/Requests/Admin/AdminPlayerStoreRequest.php @@ -3,14 +3,14 @@ namespace App\Http\Requests\Admin; use Illuminate\Validation\Rule; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; /** * 玩家创建请求。 * * @see AdminPlayerStoreController */ -final class AdminPlayerStoreRequest extends FormRequest +final class AdminPlayerStoreRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminPlayerTicketItemsRequest.php b/app/Http/Requests/Admin/AdminPlayerTicketItemsRequest.php index 30cac3d..7f679b2 100644 --- a/app/Http/Requests/Admin/AdminPlayerTicketItemsRequest.php +++ b/app/Http/Requests/Admin/AdminPlayerTicketItemsRequest.php @@ -2,14 +2,14 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; /** * 管理员查看玩家注单列表请求。 * * @see AdminPlayerTicketItemsIndexController */ -final class AdminPlayerTicketItemsRequest extends FormRequest +final class AdminPlayerTicketItemsRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminPlayerUpdateRequest.php b/app/Http/Requests/Admin/AdminPlayerUpdateRequest.php index 346b65c..1c8b1f9 100644 --- a/app/Http/Requests/Admin/AdminPlayerUpdateRequest.php +++ b/app/Http/Requests/Admin/AdminPlayerUpdateRequest.php @@ -3,14 +3,14 @@ namespace App\Http\Requests\Admin; use Illuminate\Validation\Rule; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; /** * 玩家更新请求。 * * @see AdminPlayerUpdateController */ -final class AdminPlayerUpdateRequest extends FormRequest +final class AdminPlayerUpdateRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminReportQueryRequest.php b/app/Http/Requests/Admin/AdminReportQueryRequest.php index ba52254..782a595 100644 --- a/app/Http/Requests/Admin/AdminReportQueryRequest.php +++ b/app/Http/Requests/Admin/AdminReportQueryRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class AdminReportQueryRequest extends FormRequest +final class AdminReportQueryRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminRolePermissionSyncRequest.php b/app/Http/Requests/Admin/AdminRolePermissionSyncRequest.php index c61340c..9619a4b 100644 --- a/app/Http/Requests/Admin/AdminRolePermissionSyncRequest.php +++ b/app/Http/Requests/Admin/AdminRolePermissionSyncRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class AdminRolePermissionSyncRequest extends FormRequest +final class AdminRolePermissionSyncRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminRoleStoreRequest.php b/app/Http/Requests/Admin/AdminRoleStoreRequest.php index 02d5f64..b58eeaa 100644 --- a/app/Http/Requests/Admin/AdminRoleStoreRequest.php +++ b/app/Http/Requests/Admin/AdminRoleStoreRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class AdminRoleStoreRequest extends FormRequest +final class AdminRoleStoreRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminRoleUpdateRequest.php b/app/Http/Requests/Admin/AdminRoleUpdateRequest.php index c3e4af0..c29008c 100644 --- a/app/Http/Requests/Admin/AdminRoleUpdateRequest.php +++ b/app/Http/Requests/Admin/AdminRoleUpdateRequest.php @@ -3,9 +3,9 @@ namespace App\Http\Requests\Admin; use Illuminate\Validation\Rule; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class AdminRoleUpdateRequest extends FormRequest +final class AdminRoleUpdateRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminSettingBatchUpdateRequest.php b/app/Http/Requests/Admin/AdminSettingBatchUpdateRequest.php index d3f8067..96e9e71 100644 --- a/app/Http/Requests/Admin/AdminSettingBatchUpdateRequest.php +++ b/app/Http/Requests/Admin/AdminSettingBatchUpdateRequest.php @@ -3,9 +3,9 @@ namespace App\Http\Requests\Admin; use App\Models\AdminUser; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class AdminSettingBatchUpdateRequest extends FormRequest +final class AdminSettingBatchUpdateRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminSettingIndexRequest.php b/app/Http/Requests/Admin/AdminSettingIndexRequest.php index b9fdf4d..6ac91d9 100644 --- a/app/Http/Requests/Admin/AdminSettingIndexRequest.php +++ b/app/Http/Requests/Admin/AdminSettingIndexRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class AdminSettingIndexRequest extends FormRequest +final class AdminSettingIndexRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminSettingUpdateRequest.php b/app/Http/Requests/Admin/AdminSettingUpdateRequest.php index a570044..2cb2ac6 100644 --- a/app/Http/Requests/Admin/AdminSettingUpdateRequest.php +++ b/app/Http/Requests/Admin/AdminSettingUpdateRequest.php @@ -3,9 +3,9 @@ namespace App\Http\Requests\Admin; use App\Models\AdminUser; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class AdminSettingUpdateRequest extends FormRequest +final class AdminSettingUpdateRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminUserPermissionSyncRequest.php b/app/Http/Requests/Admin/AdminUserPermissionSyncRequest.php index 43881ba..7378394 100644 --- a/app/Http/Requests/Admin/AdminUserPermissionSyncRequest.php +++ b/app/Http/Requests/Admin/AdminUserPermissionSyncRequest.php @@ -2,14 +2,14 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; /** * 管理员用户权限同步请求。 * * @see AdminUserPermissionSyncController */ -final class AdminUserPermissionSyncRequest extends FormRequest +final class AdminUserPermissionSyncRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminUserRoleSyncRequest.php b/app/Http/Requests/Admin/AdminUserRoleSyncRequest.php index 2485e56..1defbb2 100644 --- a/app/Http/Requests/Admin/AdminUserRoleSyncRequest.php +++ b/app/Http/Requests/Admin/AdminUserRoleSyncRequest.php @@ -2,14 +2,14 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; /** * 管理员用户角色同步请求。 * * @see AdminUserRoleSyncController */ -final class AdminUserRoleSyncRequest extends FormRequest +final class AdminUserRoleSyncRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AdminUserStoreRequest.php b/app/Http/Requests/Admin/AdminUserStoreRequest.php index 5cab7c7..59c4235 100644 --- a/app/Http/Requests/Admin/AdminUserStoreRequest.php +++ b/app/Http/Requests/Admin/AdminUserStoreRequest.php @@ -3,14 +3,14 @@ namespace App\Http\Requests\Admin; use Illuminate\Support\Str; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; /** * 管理员用户创建请求。 * * @see AdminUserStoreController */ -final class AdminUserStoreRequest extends FormRequest +final class AdminUserStoreRequest extends ApiFormRequest { /** * Determine if the user is authorized to make this request. diff --git a/app/Http/Requests/Admin/AdminUserUpdateRequest.php b/app/Http/Requests/Admin/AdminUserUpdateRequest.php index df5f258..46c8f46 100644 --- a/app/Http/Requests/Admin/AdminUserUpdateRequest.php +++ b/app/Http/Requests/Admin/AdminUserUpdateRequest.php @@ -3,14 +3,14 @@ namespace App\Http\Requests\Admin; use Illuminate\Validation\Rule; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; /** * 管理员用户更新请求。 * * @see AdminUserUpdateController */ -final class AdminUserUpdateRequest extends FormRequest +final class AdminUserUpdateRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AgentAdminUserRoleSyncRequest.php b/app/Http/Requests/Admin/AgentAdminUserRoleSyncRequest.php index 8092dcc..78928c9 100644 --- a/app/Http/Requests/Admin/AgentAdminUserRoleSyncRequest.php +++ b/app/Http/Requests/Admin/AgentAdminUserRoleSyncRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class AgentAdminUserRoleSyncRequest extends FormRequest +final class AgentAdminUserRoleSyncRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AgentAdminUserStoreRequest.php b/app/Http/Requests/Admin/AgentAdminUserStoreRequest.php index 3f7f068..d659453 100644 --- a/app/Http/Requests/Admin/AgentAdminUserStoreRequest.php +++ b/app/Http/Requests/Admin/AgentAdminUserStoreRequest.php @@ -2,10 +2,10 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; use Illuminate\Validation\Rule; -final class AgentAdminUserStoreRequest extends FormRequest +final class AgentAdminUserStoreRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AgentDelegationGrantSyncRequest.php b/app/Http/Requests/Admin/AgentDelegationGrantSyncRequest.php index c831b6c..5c746de 100644 --- a/app/Http/Requests/Admin/AgentDelegationGrantSyncRequest.php +++ b/app/Http/Requests/Admin/AgentDelegationGrantSyncRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class AgentDelegationGrantSyncRequest extends FormRequest +final class AgentDelegationGrantSyncRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AgentNodeStoreRequest.php b/app/Http/Requests/Admin/AgentNodeStoreRequest.php index c55718b..9d185b5 100644 --- a/app/Http/Requests/Admin/AgentNodeStoreRequest.php +++ b/app/Http/Requests/Admin/AgentNodeStoreRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class AgentNodeStoreRequest extends FormRequest +final class AgentNodeStoreRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AgentNodeUpdateRequest.php b/app/Http/Requests/Admin/AgentNodeUpdateRequest.php index 791e2a6..a7935bc 100644 --- a/app/Http/Requests/Admin/AgentNodeUpdateRequest.php +++ b/app/Http/Requests/Admin/AgentNodeUpdateRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class AgentNodeUpdateRequest extends FormRequest +final class AgentNodeUpdateRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AgentRolePermissionSyncRequest.php b/app/Http/Requests/Admin/AgentRolePermissionSyncRequest.php index e7b1824..589718b 100644 --- a/app/Http/Requests/Admin/AgentRolePermissionSyncRequest.php +++ b/app/Http/Requests/Admin/AgentRolePermissionSyncRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class AgentRolePermissionSyncRequest extends FormRequest +final class AgentRolePermissionSyncRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AgentRoleStoreRequest.php b/app/Http/Requests/Admin/AgentRoleStoreRequest.php index 799a1d0..25b54aa 100644 --- a/app/Http/Requests/Admin/AgentRoleStoreRequest.php +++ b/app/Http/Requests/Admin/AgentRoleStoreRequest.php @@ -3,10 +3,10 @@ namespace App\Http\Requests\Admin; use App\Models\AdminRole; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; use Illuminate\Validation\Rule; -final class AgentRoleStoreRequest extends FormRequest +final class AgentRoleStoreRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/AgentRoleUpdateRequest.php b/app/Http/Requests/Admin/AgentRoleUpdateRequest.php index 0769ad6..02069cc 100644 --- a/app/Http/Requests/Admin/AgentRoleUpdateRequest.php +++ b/app/Http/Requests/Admin/AgentRoleUpdateRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class AgentRoleUpdateRequest extends FormRequest +final class AgentRoleUpdateRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/DashboardAnalyticsRequest.php b/app/Http/Requests/Admin/DashboardAnalyticsRequest.php index 4f1de30..01aaf11 100644 --- a/app/Http/Requests/Admin/DashboardAnalyticsRequest.php +++ b/app/Http/Requests/Admin/DashboardAnalyticsRequest.php @@ -2,10 +2,10 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; use Illuminate\Validation\Rule; -final class DashboardAnalyticsRequest extends FormRequest +final class DashboardAnalyticsRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/DrawManualResultBatchStoreRequest.php b/app/Http/Requests/Admin/DrawManualResultBatchStoreRequest.php index 55e8189..89bbfba 100644 --- a/app/Http/Requests/Admin/DrawManualResultBatchStoreRequest.php +++ b/app/Http/Requests/Admin/DrawManualResultBatchStoreRequest.php @@ -3,10 +3,10 @@ namespace App\Http\Requests\Admin; use Illuminate\Validation\Rule; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; use App\Services\Draw\DrawPrizeLayout; -final class DrawManualResultBatchStoreRequest extends FormRequest +final class DrawManualResultBatchStoreRequest extends ApiFormRequest { public function authorize(): bool { @@ -33,7 +33,7 @@ final class DrawManualResultBatchStoreRequest extends FormRequest sort($actual); if ($actual !== $expected) { - $validator->errors()->add('items', 'items must contain the complete 23 draw prize slots.'); + $validator->errors()->add('items', __('validation.exact.items_must_contain_23_slots')); } }); } diff --git a/app/Http/Requests/Admin/DrawReopenRequest.php b/app/Http/Requests/Admin/DrawReopenRequest.php index 6cc0145..d27fb95 100644 --- a/app/Http/Requests/Admin/DrawReopenRequest.php +++ b/app/Http/Requests/Admin/DrawReopenRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class DrawReopenRequest extends FormRequest +final class DrawReopenRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/DrawStoreRequest.php b/app/Http/Requests/Admin/DrawStoreRequest.php index 965f4c0..5554f5f 100644 --- a/app/Http/Requests/Admin/DrawStoreRequest.php +++ b/app/Http/Requests/Admin/DrawStoreRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class DrawStoreRequest extends FormRequest +final class DrawStoreRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/Jackpot/AdminJackpotPoolAdjustRequest.php b/app/Http/Requests/Admin/Jackpot/AdminJackpotPoolAdjustRequest.php index 250c356..752edd1 100644 --- a/app/Http/Requests/Admin/Jackpot/AdminJackpotPoolAdjustRequest.php +++ b/app/Http/Requests/Admin/Jackpot/AdminJackpotPoolAdjustRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin\Jackpot; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class AdminJackpotPoolAdjustRequest extends FormRequest +final class AdminJackpotPoolAdjustRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/ReconcileJobStoreRequest.php b/app/Http/Requests/Admin/ReconcileJobStoreRequest.php index 1e364e3..02f8433 100644 --- a/app/Http/Requests/Admin/ReconcileJobStoreRequest.php +++ b/app/Http/Requests/Admin/ReconcileJobStoreRequest.php @@ -2,14 +2,14 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; /** * 对账任务创建请求。 * * @see ReconcileJobStoreController */ -final class ReconcileJobStoreRequest extends FormRequest +final class ReconcileJobStoreRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/ReportJobStoreRequest.php b/app/Http/Requests/Admin/ReportJobStoreRequest.php index dcd2f6b..d0a791c 100644 --- a/app/Http/Requests/Admin/ReportJobStoreRequest.php +++ b/app/Http/Requests/Admin/ReportJobStoreRequest.php @@ -2,11 +2,11 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; use Illuminate\Validation\Rule; /** @see ReportJobStoreController */ -final class ReportJobStoreRequest extends FormRequest +final class ReportJobStoreRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/SettlementBatchReviewRequest.php b/app/Http/Requests/Admin/SettlementBatchReviewRequest.php index af9f9a8..1381492 100644 --- a/app/Http/Requests/Admin/SettlementBatchReviewRequest.php +++ b/app/Http/Requests/Admin/SettlementBatchReviewRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class SettlementBatchReviewRequest extends FormRequest +final class SettlementBatchReviewRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/SettlementPayoutAdjustmentRequest.php b/app/Http/Requests/Admin/SettlementPayoutAdjustmentRequest.php index eeb0c6c..b756f2a 100644 --- a/app/Http/Requests/Admin/SettlementPayoutAdjustmentRequest.php +++ b/app/Http/Requests/Admin/SettlementPayoutAdjustmentRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class SettlementPayoutAdjustmentRequest extends FormRequest +final class SettlementPayoutAdjustmentRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/TicketItemListRequest.php b/app/Http/Requests/Admin/TicketItemListRequest.php index 4213fa0..90a2764 100644 --- a/app/Http/Requests/Admin/TicketItemListRequest.php +++ b/app/Http/Requests/Admin/TicketItemListRequest.php @@ -2,14 +2,14 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; /** * 管理员注单列表查询请求。 * * @see AdminTicketItemIndexController */ -final class TicketItemListRequest extends FormRequest +final class TicketItemListRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/TransferOrderListRequest.php b/app/Http/Requests/Admin/TransferOrderListRequest.php index afd823d..b61a0b4 100644 --- a/app/Http/Requests/Admin/TransferOrderListRequest.php +++ b/app/Http/Requests/Admin/TransferOrderListRequest.php @@ -2,14 +2,14 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; /** * 转账单列表查询请求。 * * @see TransferOrderListController */ -final class TransferOrderListRequest extends FormRequest +final class TransferOrderListRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/Wallet/TransferOrderCompleteCreditRequest.php b/app/Http/Requests/Admin/Wallet/TransferOrderCompleteCreditRequest.php index 086c576..11a9f2c 100644 --- a/app/Http/Requests/Admin/Wallet/TransferOrderCompleteCreditRequest.php +++ b/app/Http/Requests/Admin/Wallet/TransferOrderCompleteCreditRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin\Wallet; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class TransferOrderCompleteCreditRequest extends FormRequest +final class TransferOrderCompleteCreditRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/Wallet/TransferOrderManuallyProcessRequest.php b/app/Http/Requests/Admin/Wallet/TransferOrderManuallyProcessRequest.php index da2975e..bf3146e 100644 --- a/app/Http/Requests/Admin/Wallet/TransferOrderManuallyProcessRequest.php +++ b/app/Http/Requests/Admin/Wallet/TransferOrderManuallyProcessRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin\Wallet; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class TransferOrderManuallyProcessRequest extends FormRequest +final class TransferOrderManuallyProcessRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/Wallet/TransferOrderReverseRequest.php b/app/Http/Requests/Admin/Wallet/TransferOrderReverseRequest.php index 3704edc..ba45711 100644 --- a/app/Http/Requests/Admin/Wallet/TransferOrderReverseRequest.php +++ b/app/Http/Requests/Admin/Wallet/TransferOrderReverseRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Admin\Wallet; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -final class TransferOrderReverseRequest extends FormRequest +final class TransferOrderReverseRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Admin/WalletTransactionListRequest.php b/app/Http/Requests/Admin/WalletTransactionListRequest.php index e8d08e5..eec71a9 100644 --- a/app/Http/Requests/Admin/WalletTransactionListRequest.php +++ b/app/Http/Requests/Admin/WalletTransactionListRequest.php @@ -2,14 +2,14 @@ namespace App\Http\Requests\Admin; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; /** * 钱包流水列表查询请求。 * * @see WalletTransactionListController */ -final class WalletTransactionListRequest extends FormRequest +final class WalletTransactionListRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/ApiFormRequest.php b/app/Http/Requests/ApiFormRequest.php new file mode 100644 index 0000000..f946fa6 --- /dev/null +++ b/app/Http/Requests/ApiFormRequest.php @@ -0,0 +1,24 @@ + + */ + public function attributes(): array + { + $locale = $this->request->attributes->get('lottery_locale') + ?? config('lottery.locales.fallback', 'en'); + + $labels = trans('validation.attributes', [], (string) $locale); + + return is_array($labels) ? $labels : []; + } +} diff --git a/app/Http/Requests/Ticket/TicketBetRequest.php b/app/Http/Requests/Ticket/TicketBetRequest.php index 03c10cf..7fcaeca 100644 --- a/app/Http/Requests/Ticket/TicketBetRequest.php +++ b/app/Http/Requests/Ticket/TicketBetRequest.php @@ -2,9 +2,9 @@ namespace App\Http\Requests\Ticket; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; -abstract class TicketBetRequest extends FormRequest +abstract class TicketBetRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Http/Requests/Wallet/WalletTransferRequest.php b/app/Http/Requests/Wallet/WalletTransferRequest.php index a8d752d..ff4b6ae 100644 --- a/app/Http/Requests/Wallet/WalletTransferRequest.php +++ b/app/Http/Requests/Wallet/WalletTransferRequest.php @@ -2,12 +2,12 @@ namespace App\Http\Requests\Wallet; -use Illuminate\Foundation\Http\FormRequest; +use App\Http\Requests\ApiFormRequest; /** * 转入 / 转出共用请求体:最小货币单位金额、幂等键、可选币种。 */ -final class WalletTransferRequest extends FormRequest +final class WalletTransferRequest extends ApiFormRequest { public function authorize(): bool { diff --git a/app/Models/AdminRole.php b/app/Models/AdminRole.php index 47b8e9f..98b8826 100644 --- a/app/Models/AdminRole.php +++ b/app/Models/AdminRole.php @@ -4,7 +4,9 @@ namespace App\Models; use Illuminate\Support\Facades\DB; use App\Support\AdminPermissionBridge; +use App\Support\AdminPermissionInheritance; use Illuminate\Database\Eloquent\Model; +use Illuminate\Validation\ValidationException; use Illuminate\Database\Eloquent\Relations\BelongsToMany; final class AdminRole extends Model @@ -106,7 +108,9 @@ final class AdminRole extends Model */ public function syncLegacyPermissionSlugs(array $slugs): void { - $legacySlugs = AdminPermissionBridge::normalizeCanonicalLegacySlugs($slugs); + $legacySlugs = AdminPermissionInheritance::expand( + AdminPermissionBridge::normalizeCanonicalLegacySlugs($slugs), + ); $codes = []; foreach ($legacySlugs as $slug) { @@ -127,6 +131,17 @@ final class AdminRole extends Model 'menu_action_id' => (int) $mid, ]); } + + $granted = $this->legacyPermissionSlugs(); + $missing = array_values(array_diff($legacySlugs, $granted)); + if ($missing !== []) { + throw ValidationException::withMessages([ + 'permission_slugs' => [ + 'permission_catalog_incomplete: '.implode(', ', $missing) + .' (run: php artisan migrate && php artisan lottery:admin-auth-sync --audit)', + ], + ]); + } } public function assignedUserCount(): int diff --git a/app/Models/AdminUser.php b/app/Models/AdminUser.php index da72929..2473fd9 100644 --- a/app/Models/AdminUser.php +++ b/app/Models/AdminUser.php @@ -128,7 +128,10 @@ final class AdminUser extends Authenticatable { $agentId = $this->primaryAgentNodeId(); if ($agentId !== null) { - return $this->agentRoleMenuActionPermissionCodes($agentId); + $fromAgent = $this->agentRoleMenuActionPermissionCodes($agentId); + if ($fromAgent !== []) { + return $fromAgent; + } } return $this->siteRoleMenuActionPermissionCodes(); @@ -197,6 +200,11 @@ final class AdminUser extends Authenticatable 'granted_at' => $now, ]); } + + $agentId = $this->primaryAgentNodeId(); + if ($agentId !== null) { + $this->syncAgentRoleIds($agentId, array_map('intval', $roleIds)); + } }); } diff --git a/app/Rules/WalletApiUrlRule.php b/app/Rules/WalletApiUrlRule.php index 0c0c406..a094142 100644 --- a/app/Rules/WalletApiUrlRule.php +++ b/app/Rules/WalletApiUrlRule.php @@ -14,7 +14,7 @@ final class WalletApiUrlRule implements Rule public function message(): string { - return 'wallet_api_url 必须是 https 的公开域名根地址,并拒绝 localhost/内网 IP 与带路径/查询的地址。'; + return (string) __('validation.custom.wallet_api_url.wallet_api_url'); } } diff --git a/app/Services/Agent/AgentAdminUserService.php b/app/Services/Agent/AgentAdminUserService.php index c1a50da..ae91985 100644 --- a/app/Services/Agent/AgentAdminUserService.php +++ b/app/Services/Agent/AgentAdminUserService.php @@ -82,4 +82,31 @@ final class AgentAdminUserService throw ValidationException::withMessages(['role_ids' => ['invalid_for_agent']]); } } + + public function destroyUnderAgent(AgentNode $agent, AdminUser $user): void + { + if ((int) $user->primaryAgentNodeId() !== (int) $agent->id) { + throw ValidationException::withMessages(['user' => ['agent_mismatch']]); + } + + DB::transaction(static function () use ($agent, $user): void { + DB::table('admin_user_agent_roles') + ->where('admin_user_id', $user->id) + ->where('agent_node_id', $agent->id) + ->delete(); + + DB::table('admin_user_agents') + ->where('admin_user_id', $user->id) + ->where('agent_node_id', $agent->id) + ->delete(); + + $siteId = (int) $agent->admin_site_id; + DB::table('admin_user_site_roles') + ->where('admin_user_id', $user->id) + ->where('site_id', $siteId) + ->delete(); + + $user->delete(); + }); + } } diff --git a/app/Services/Agent/AgentNodeService.php b/app/Services/Agent/AgentNodeService.php index b2b66e2..2a8f09c 100644 --- a/app/Services/Agent/AgentNodeService.php +++ b/app/Services/Agent/AgentNodeService.php @@ -2,6 +2,7 @@ namespace App\Services\Agent; +use App\Models\AdminRole; use App\Models\AdminUser; use App\Models\AgentNode; use Illuminate\Support\Facades\DB; @@ -76,7 +77,20 @@ final class AgentNodeService public function destroy(AgentNode $node): void { DB::transaction(static function () use ($node): void { + AdminRole::query() + ->where('owner_agent_id', $node->id) + ->whereNotNull('delegated_from_role_id') + ->each(static fn (AdminRole $role): bool => (bool) $role->delete()); + $node->delete(); }); } + + public function hasBlockingCustomRoles(AgentNode $node): bool + { + return AdminRole::query() + ->where('owner_agent_id', $node->id) + ->whereNull('delegated_from_role_id') + ->exists(); + } } diff --git a/app/Services/Agent/AgentRoleService.php b/app/Services/Agent/AgentRoleService.php index 072c6b2..292e187 100644 --- a/app/Services/Agent/AgentRoleService.php +++ b/app/Services/Agent/AgentRoleService.php @@ -100,7 +100,11 @@ final class AgentRoleService } if ($role->assignedUserCount() > 0) { - throw ValidationException::withMessages(['role' => ['in_use']]); + throw ValidationException::withMessages([ + 'role' => [ + __('admin.agent_role_in_use', ['count' => $role->assignedUserCount()]), + ], + ]); } $role->delete(); diff --git a/app/Support/AdminAgentPermissionMenuActionSync.php b/app/Support/AdminAgentPermissionMenuActionSync.php new file mode 100644 index 0000000..bebe5ff --- /dev/null +++ b/app/Support/AdminAgentPermissionMenuActionSync.php @@ -0,0 +1,85 @@ +where('code', 'view')->value('id'); + $manageActionId = DB::table('admin_action_catalog')->where('code', 'manage')->value('id'); + if ($viewActionId === null || $manageActionId === null) { + return 0; + } + + $agentMenuId = (int) DB::table('admin_menus')->where('code', 'system.agents')->value('id'); + if ($agentMenuId === 0) { + return 0; + } + + $rolesMenuId = self::ensureChildMenu($agentMenuId, 'system.agents.roles', '代理角色', $now); + $usersMenuId = self::ensureChildMenu($agentMenuId, 'system.agents.users', '代理账号', $now); + + $created = 0; + $created += self::ensureMenuAction((int) $rolesMenuId, (int) $viewActionId, 'agent.role.view', '代理角色查看', $now) ? 1 : 0; + $created += self::ensureMenuAction((int) $rolesMenuId, (int) $manageActionId, 'agent.role.manage', '代理角色管理', $now) ? 1 : 0; + $created += self::ensureMenuAction((int) $usersMenuId, (int) $viewActionId, 'agent.user.view', '代理账号查看', $now) ? 1 : 0; + $created += self::ensureMenuAction((int) $usersMenuId, (int) $manageActionId, 'agent.user.manage', '代理账号管理', $now) ? 1 : 0; + + return $created; + } + + private static function ensureChildMenu(int $parentId, string $code, string $name, Carbon $now): int + { + $existing = DB::table('admin_menus')->where('code', $code)->value('id'); + if ($existing !== null) { + return (int) $existing; + } + + return (int) DB::table('admin_menus')->insertGetId([ + 'parent_id' => $parentId, + 'menu_type' => 'button', + 'code' => $code, + 'name' => $name, + 'path' => null, + 'route_name' => null, + 'component' => null, + 'icon' => null, + 'active_menu_code' => 'system.agents', + 'sort_order' => 0, + 'is_visible' => false, + 'is_cache' => false, + 'is_external' => false, + 'status' => 1, + 'meta_json' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + private static function ensureMenuAction(int $menuId, int $actionId, string $permissionCode, string $name, Carbon $now): bool + { + if (DB::table('admin_menu_actions')->where('permission_code', $permissionCode)->exists()) { + return false; + } + + DB::table('admin_menu_actions')->insert([ + 'menu_id' => $menuId, + 'action_id' => $actionId, + 'permission_code' => $permissionCode, + 'name' => $name, + 'status' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + return true; + } +} diff --git a/app/Support/AdminAuthorizationRegistry.php b/app/Support/AdminAuthorizationRegistry.php index 1026499..8fc60fb 100644 --- a/app/Support/AdminAuthorizationRegistry.php +++ b/app/Support/AdminAuthorizationRegistry.php @@ -29,10 +29,10 @@ final class AdminAuthorizationRegistry ['slug' => 'prd.agent.view', 'name' => '代理管理·查看', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.view']], ['slug' => 'prd.agent.manage', 'name' => '代理管理·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.manage']], - ['slug' => 'prd.agent.role.view', 'name' => '代理角色·查看', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.view']], - ['slug' => 'prd.agent.role.manage', 'name' => '代理角色·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.manage']], - ['slug' => 'prd.agent.user.view', 'name' => '代理账号·查看', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.view']], - ['slug' => 'prd.agent.user.manage', 'name' => '代理账号·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.manage']], + ['slug' => 'prd.agent.role.view', 'name' => '代理角色·查看', 'nav_segment' => 'agents', 'permission_codes' => ['agent.role.view', 'agent.node.view']], + ['slug' => 'prd.agent.role.manage', 'name' => '代理角色·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.role.manage']], + ['slug' => 'prd.agent.user.view', 'name' => '代理账号·查看', 'nav_segment' => 'agents', 'permission_codes' => ['agent.user.view', 'agent.node.view']], + ['slug' => 'prd.agent.user.manage', 'name' => '代理账号·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.user.manage']], ['slug' => 'prd.users.manage', 'name' => '用户管理·可管理', 'nav_segment' => 'players', 'permission_codes' => ['service.players.manage']], ['slug' => 'prd.users.view_finance', 'name' => '用户管理·财务查看', 'nav_segment' => 'players', 'permission_codes' => ['service.players.view', 'service.wallet.view']], @@ -49,9 +49,9 @@ final class AdminAuthorizationRegistry ['slug' => 'prd.wallet_reconcile.view_cs', 'name' => '钱包对账·客服单用户', 'nav_segment' => 'wallet', 'permission_codes' => ['service.wallet.view', 'service.reconcile.view']], ['slug' => 'prd.wallet_adjust.manage', 'name' => '补单/冲正·可管理', 'nav_segment' => 'wallet', 'permission_codes' => ['service.wallet.adjust']], - ['slug' => 'prd.draw_result.manage', 'name' => '开奖结果录入·可管理', 'nav_segment' => 'draws', 'permission_codes' => ['draw.results.view', 'draw.review.review', 'draw.review.publish']], + ['slug' => 'prd.draw_result.manage', 'name' => '开奖结果录入·可管理', 'nav_segment' => 'draws', 'permission_codes' => ['draw.review.review', 'draw.review.publish']], ['slug' => 'prd.draw_result.view', 'name' => '开奖结果·查看', 'nav_segment' => 'draws', 'permission_codes' => ['draw.results.view']], - ['slug' => 'prd.draw_reopen.manage', 'name' => '开奖结果重开·可管理', 'nav_segment' => 'draws', 'permission_codes' => ['draw.review.publish']], + ['slug' => 'prd.draw_reopen.manage', 'name' => '开奖结果重开·可管理', 'nav_segment' => 'draws', 'permission_codes' => ['draw.reopen.manage']], ['slug' => 'prd.risk.view', 'name' => '风控中心·查看', 'nav_segment' => 'risk', 'permission_codes' => ['risk.monitor.view']], ['slug' => 'prd.risk.manage', 'name' => '风控中心·可管理', 'nav_segment' => 'risk', 'permission_codes' => ['risk.monitor.manage']], @@ -386,22 +386,23 @@ final class AdminAuthorizationRegistry ['code' => 'admin.admin-roles.destroy', 'module_code' => 'system', 'name' => '删除角色', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/admin-roles/{admin_role}', 'route_name' => 'api.v1.admin.admin-roles.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.admin_role.manage']], ['code' => 'admin.admin-roles.permissions.sync', 'module_code' => 'system', 'name' => '角色权限同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/admin-roles/{admin_role}/permissions', 'route_name' => 'api.v1.admin.admin-roles.permissions.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.admin_role.manage']], - ['code' => 'admin.agent-nodes.tree', 'module_code' => 'agent', 'name' => '代理树', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/tree', 'route_name' => 'api.v1.admin.agent-nodes.tree', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']], + ['code' => 'admin.agent-nodes.tree', 'module_code' => 'agent', 'name' => '代理树', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/tree', 'route_name' => 'api.v1.admin.agent-nodes.tree', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage', 'agent.role.view', 'agent.role.manage', 'agent.user.view', 'agent.user.manage']], ['code' => 'admin.agent-nodes.store', 'module_code' => 'agent', 'name' => '创建下级代理', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/agent-nodes', 'route_name' => 'api.v1.admin.agent-nodes.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']], ['code' => 'admin.agent-nodes.show', 'module_code' => 'agent', 'name' => '代理节点详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}', 'route_name' => 'api.v1.admin.agent-nodes.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']], ['code' => 'admin.agent-nodes.update', 'module_code' => 'agent', 'name' => '更新代理节点', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}', 'route_name' => 'api.v1.admin.agent-nodes.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']], ['code' => 'admin.agent-nodes.destroy', 'module_code' => 'agent', 'name' => '删除代理节点', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}', 'route_name' => 'api.v1.admin.agent-nodes.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']], ['code' => 'admin.agent-nodes.children', 'module_code' => 'agent', 'name' => '代理直属下级', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/children', 'route_name' => 'api.v1.admin.agent-nodes.children', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']], - ['code' => 'admin.agent-roles.index', 'module_code' => 'agent', 'name' => '代理角色列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/roles', 'route_name' => 'api.v1.admin.agent-roles.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']], - ['code' => 'admin.agent-roles.store', 'module_code' => 'agent', 'name' => '创建代理角色', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/roles', 'route_name' => 'api.v1.admin.agent-roles.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']], - ['code' => 'admin.agent-roles.update', 'module_code' => 'agent', 'name' => '更新代理角色', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-roles/{admin_role}', 'route_name' => 'api.v1.admin.agent-roles.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']], - ['code' => 'admin.agent-roles.permissions.sync', 'module_code' => 'agent', 'name' => '代理角色权限同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-roles/{admin_role}/permissions', 'route_name' => 'api.v1.admin.agent-roles.permissions.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']], - ['code' => 'admin.agent-roles.destroy', 'module_code' => 'agent', 'name' => '删除代理角色', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/agent-roles/{admin_role}', 'route_name' => 'api.v1.admin.agent-roles.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']], + ['code' => 'admin.agent-roles.index', 'module_code' => 'agent', 'name' => '代理角色列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/roles', 'route_name' => 'api.v1.admin.agent-roles.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.role.view', 'agent.role.manage', 'agent.node.view', 'agent.node.manage']], + ['code' => 'admin.agent-roles.store', 'module_code' => 'agent', 'name' => '创建代理角色', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/roles', 'route_name' => 'api.v1.admin.agent-roles.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.role.manage']], + ['code' => 'admin.agent-roles.update', 'module_code' => 'agent', 'name' => '更新代理角色', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-roles/{admin_role}', 'route_name' => 'api.v1.admin.agent-roles.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.role.manage']], + ['code' => 'admin.agent-roles.permissions.sync', 'module_code' => 'agent', 'name' => '代理角色权限同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-roles/{admin_role}/permissions', 'route_name' => 'api.v1.admin.agent-roles.permissions.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.role.manage']], + ['code' => 'admin.agent-roles.destroy', 'module_code' => 'agent', 'name' => '删除代理角色', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/agent-roles/{admin_role}', 'route_name' => 'api.v1.admin.agent-roles.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.role.manage']], - ['code' => 'admin.agent-admin-users.index', 'module_code' => 'agent', 'name' => '代理账号列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/admin-users', 'route_name' => 'api.v1.admin.agent-admin-users.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']], - ['code' => 'admin.agent-admin-users.store', 'module_code' => 'agent', 'name' => '创建代理账号', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/admin-users', 'route_name' => 'api.v1.admin.agent-admin-users.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']], - ['code' => 'admin.agent-admin-users.roles.sync', 'module_code' => 'agent', 'name' => '代理账号角色同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-admin-users/{admin_user}/roles', 'route_name' => 'api.v1.admin.agent-admin-users.roles.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']], + ['code' => 'admin.agent-admin-users.index', 'module_code' => 'agent', 'name' => '代理账号列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/admin-users', 'route_name' => 'api.v1.admin.agent-admin-users.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.user.view', 'agent.user.manage', 'agent.node.view', 'agent.node.manage']], + ['code' => 'admin.agent-admin-users.store', 'module_code' => 'agent', 'name' => '创建代理账号', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/admin-users', 'route_name' => 'api.v1.admin.agent-admin-users.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.user.manage']], + ['code' => 'admin.agent-admin-users.roles.sync', 'module_code' => 'agent', 'name' => '代理账号角色同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-admin-users/{admin_user}/roles', 'route_name' => 'api.v1.admin.agent-admin-users.roles.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.user.manage']], + ['code' => 'admin.agent-admin-users.destroy', 'module_code' => 'agent', 'name' => '删除代理账号', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/agent-admin-users/{admin_user}', 'route_name' => 'api.v1.admin.agent-admin-users.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.user.manage']], ['code' => 'admin.agent-delegation-grants.index', 'module_code' => 'agent', 'name' => '代理下放上限查看', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/delegation-grants', 'route_name' => 'api.v1.admin.agent-delegation-grants.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']], ['code' => 'admin.agent-delegation-grants.sync', 'module_code' => 'agent', 'name' => '代理下放上限同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/delegation-grants', 'route_name' => 'api.v1.admin.agent-delegation-grants.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']], diff --git a/app/Support/AdminDrawPermissionMenuActionSync.php b/app/Support/AdminDrawPermissionMenuActionSync.php new file mode 100644 index 0000000..0803b98 --- /dev/null +++ b/app/Support/AdminDrawPermissionMenuActionSync.php @@ -0,0 +1,36 @@ +where('permission_code', 'draw.reopen.manage')->exists()) { + return 0; + } + + $menuId = (int) DB::table('admin_menus')->where('code', 'draw.results')->value('id'); + $actionId = DB::table('admin_action_catalog')->where('code', 'manage')->value('id'); + if ($menuId === 0 || $actionId === null) { + return 0; + } + + $now = Carbon::now(); + DB::table('admin_menu_actions')->insert([ + 'menu_id' => $menuId, + 'action_id' => (int) $actionId, + 'permission_code' => 'draw.reopen.manage', + 'name' => '期号重开', + 'status' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ]); + + return 1; + } +} diff --git a/app/Support/AdminPermissionInheritance.php b/app/Support/AdminPermissionInheritance.php index dea5c80..e1f4a6b 100644 --- a/app/Support/AdminPermissionInheritance.php +++ b/app/Support/AdminPermissionInheritance.php @@ -9,8 +9,8 @@ final class AdminPermissionInheritance */ private const IMPLIED_BY_SLUG = [ 'prd.agent.manage' => ['prd.agent.view'], - 'prd.agent.role.manage' => ['prd.agent.role.view'], - 'prd.agent.user.manage' => ['prd.agent.user.view'], + 'prd.agent.role.manage' => ['prd.agent.role.view', 'prd.agent.view'], + 'prd.agent.user.manage' => ['prd.agent.user.view', 'prd.agent.view'], 'prd.integration.manage' => ['prd.integration.view'], 'prd.wallet_reconcile.manage' => ['prd.wallet_reconcile.view'], 'prd.draw_result.manage' => ['prd.draw_result.view'], diff --git a/app/Support/AgentAdminUserAuthorization.php b/app/Support/AgentAdminUserAuthorization.php new file mode 100644 index 0000000..932b805 --- /dev/null +++ b/app/Support/AgentAdminUserAuthorization.php @@ -0,0 +1,57 @@ +isSuperAdmin()) { + return true; + } + + $agent = $target->primaryAgentNode(); + if ($agent === null) { + return false; + } + + return AdminAgentScope::nodeVisibleTo($admin, $agent); + } + + public static function userManageableBy(AdminUser $admin, AdminUser $target): bool + { + if (! self::userVisibleTo($admin, $target)) { + return false; + } + + if ($admin->isSuperAdmin()) { + return true; + } + + if (! $admin->hasPermissionCode('agent.user.manage')) { + return false; + } + + $agent = $target->primaryAgentNode(); + + return $agent !== null && AdminAgentScope::nodeManageableBy($admin, $agent); + } + + public static function denyUnlessUserManageable(AdminUser $admin, AdminUser $target): ?\Illuminate\Http\JsonResponse + { + if (self::userManageableBy($admin, $target)) { + return null; + } + + return ApiMessage::errorResponse( + request(), + 'admin.agent_user_manage_denied', + \App\Lottery\ErrorCode::AdminForbidden->value, + null, + 403, + ); + } +} diff --git a/app/Support/AgentRoleAuthorization.php b/app/Support/AgentRoleAuthorization.php index 28984fe..280f417 100644 --- a/app/Support/AgentRoleAuthorization.php +++ b/app/Support/AgentRoleAuthorization.php @@ -37,7 +37,7 @@ final class AgentRoleAuthorization return false; } - return $admin->isSuperAdmin() || $admin->hasPermissionCode('agent.node.manage'); + return $admin->isSuperAdmin() || $admin->hasPermissionCode('agent.role.manage'); } /** diff --git a/app/Support/ApiValidationErrors.php b/app/Support/ApiValidationErrors.php new file mode 100644 index 0000000..ed97984 --- /dev/null +++ b/app/Support/ApiValidationErrors.php @@ -0,0 +1,286 @@ +|string> $errors + * @return array> + */ + public static function normalize(array $errors, string $locale): array + { + $out = []; + + foreach ($errors as $field => $messages) { + $fieldKey = (string) $field; + $normalized = []; + foreach ((array) $messages as $message) { + $normalized[] = self::present($fieldKey, (string) $message, $locale); + } + $out[$fieldKey] = $normalized; + } + + return $out; + } + + /** + * @param array> $normalized + */ + public static function summary(array $normalized, int $maxParts = 3): ?string + { + $parts = []; + + foreach ($normalized as $messages) { + foreach ($messages as $message) { + $parts[] = $message; + if (count($parts) >= $maxParts) { + return implode(';', $parts).'…'; + } + } + } + + return $parts === [] ? null : implode(';', $parts); + } + + private static function present(string $field, string $message, string $locale): string + { + $trimmed = trim($message); + if ($trimmed === '') { + return $message; + } + + if (preg_match('/\p{Han}/u', $trimmed) === 1) { + return $trimmed; + } + + $exact = self::exactMessage($trimmed, $locale); + if ($exact !== null) { + return $exact; + } + + if (preg_match( + '/^(exceeds_actor|exceeds_parent_ceiling|exceeds_delegation_ceiling|permission_exceeds_actor|permission_catalog_incomplete)\s*:\s*(.+)$/u', + $trimmed, + $matches, + ) === 1) { + $detail = trim(preg_replace('/\s*\(run:.*$/i', '', $matches[2]) ?? $matches[2]); + $line = trans('validation.business.'.$matches[1], ['detail' => $detail], $locale); + if ($line !== 'validation.business.'.$matches[1]) { + return $line; + } + } + + $attribute = self::attributeLabel($field, $locale); + + $businessKey = 'validation.business.'.$trimmed; + $businessLine = trans($businessKey, ['attribute' => $attribute], $locale); + if ($businessLine !== $businessKey) { + return $businessLine; + } + + $customLine = self::customRuleLine($field, $trimmed, $attribute, $locale); + if ($customLine !== null) { + return $customLine; + } + + $ruleLine = self::standardRuleLine($trimmed, $attribute, $locale); + if ($ruleLine !== null) { + return $ruleLine; + } + + $humanized = self::humanizeLaravelEnglish($field, $trimmed, $locale, $attribute); + if ($humanized !== null) { + return $humanized; + } + + return $trimmed; + } + + private static function exactMessage(string $message, string $locale): ?string + { + $map = trans('validation.exact', [], $locale); + if (! is_array($map)) { + return null; + } + + return $map[$message] ?? null; + } + + private static function customRuleLine( + string $field, + string $rule, + string $attribute, + string $locale, + ): ?string { + $candidates = array_values(array_unique([ + 'validation.custom.'.$field.'.'.$rule, + 'validation.custom.'.self::flatFieldName($field).'.'.$rule, + ])); + + foreach ($candidates as $key) { + $line = trans($key, ['attribute' => $attribute], $locale); + if ($line !== $key) { + return $line; + } + } + + return null; + } + + private static function standardRuleLine(string $rule, string $attribute, string $locale): ?string + { + if (! preg_match('/^[a-z0-9_.]+$/i', $rule)) { + return null; + } + + $key = 'validation.'.$rule; + $line = trans($key, ['attribute' => $attribute], $locale); + + return $line !== $key ? $line : null; + } + + private static function humanizeLaravelEnglish( + string $field, + string $message, + string $locale, + string $attribute, + ): ?string { + if (preg_match('/^The (.+?) field (.+)$/i', $message, $matches) !== 1) { + if (preg_match('/^The (.+?) has already been taken\.?$/i', $message, $taken) === 1) { + $attribute = self::attributeLabelFromEnglish($taken[1], $field, $locale); + + return trans('validation.unique', ['attribute' => $attribute], $locale); + } + + if (preg_match('/^The selected (.+?) is invalid\.?$/i', $message, $selected) === 1) { + $attribute = self::attributeLabelFromEnglish($selected[1], $field, $locale); + + return trans('validation.exists', ['attribute' => $attribute], $locale); + } + + return null; + } + + $englishName = $matches[1]; + $tail = $matches[2]; + $attribute = self::attributeLabelFromEnglish($englishName, $field, $locale); + + $tailMap = [ + 'is required' => 'validation.required', + 'must be a valid email address' => 'validation.email', + 'must be a valid UUID' => 'validation.uuid', + 'must be an integer' => 'validation.integer', + 'must be a string' => 'validation.string', + 'must be a number' => 'validation.numeric', + 'must be a valid JSON string' => 'validation.json', + 'must be true or false' => 'validation.boolean', + 'must be an array' => 'validation.array', + 'format is invalid' => null, + 'is not allowed' => 'validation.prohibited', + 'must be present' => 'validation.present', + ]; + + foreach ($tailMap as $suffix => $ruleKey) { + if (stripos($tail, $suffix) === false) { + continue; + } + + if ($ruleKey === null) { + return self::customRuleLine($field, 'regex', $attribute, $locale) + ?? trans('validation.regex', ['attribute' => $attribute], $locale); + } + + $line = trans($ruleKey, ['attribute' => $attribute], $locale); + + return $line !== $ruleKey ? $line : null; + } + + if (preg_match('/must be at least (\d+) characters/i', $tail, $min)) { + $customKey = 'validation.custom.'.self::flatFieldName($field).'.min'; + $customLine = trans($customKey, ['attribute' => $attribute, 'min' => $min[1]], $locale); + if ($customLine !== $customKey) { + return $customLine; + } + + return trans('validation.min.string', ['attribute' => $attribute, 'min' => $min[1]], $locale); + } + + if (preg_match('/must not be greater than (\d+) characters/i', $tail, $max)) { + return trans('validation.max.string', ['attribute' => $attribute, 'max' => $max[1]], $locale); + } + + if (preg_match('/must contain (\d+) items/i', $tail, $size)) { + return trans('validation.size.array', ['attribute' => $attribute, 'size' => $size[1]], $locale); + } + + if (preg_match('/must have at least (\d+) items/i', $tail, $minItems)) { + return trans('validation.min.array', ['attribute' => $attribute, 'min' => $minItems[1]], $locale); + } + + if (preg_match('/must not have more than (\d+) items/i', $tail, $maxItems)) { + return trans('validation.max.array', ['attribute' => $attribute, 'max' => $maxItems[1]], $locale); + } + + if (preg_match('/must be between ([\d.]+) and ([\d.]+)/i', $tail, $between)) { + return trans('validation.between.numeric', [ + 'attribute' => $attribute, + 'min' => $between[1], + 'max' => $between[2], + ], $locale); + } + + if (preg_match('/must match the format (.+)$/i', $tail, $format)) { + return trans('validation.date_format', [ + 'attribute' => $attribute, + 'format' => trim($format[1]), + ], $locale); + } + + return null; + } + + private static function attributeLabelFromEnglish(string $englishName, string $field, string $locale): string + { + $normalized = strtolower(trim($englishName)); + if (preg_match('/^selected (.+)$/i', $normalized, $selected) === 1) { + $normalized = $selected[1]; + } + + $guess = str_replace(' ', '_', $normalized); + + return self::attributeLabel($guess !== '' ? $guess : $field, $locale); + } + + private static function attributeLabel(string $field, string $locale): string + { + $candidates = array_values(array_unique([ + $field, + self::flatFieldName($field), + preg_replace('/\.\d+\./', '.*.', $field) ?? $field, + preg_replace('/\.\d+\./', '.', $field) ?? $field, + ])); + + foreach ($candidates as $candidate) { + $key = 'validation.attributes.'.$candidate; + $label = trans($key, [], $locale); + if ($label !== $key) { + return $label; + } + } + + return self::flatFieldName($field); + } + + private static function flatFieldName(string $field): string + { + if (preg_match('/\.([a-zA-Z0-9_]+)$/', $field, $matches) === 1) { + return $matches[1]; + } + + return $field; + } +} diff --git a/bootstrap/app.php b/bootstrap/app.php index acccdaf..504235d 100644 --- a/bootstrap/app.php +++ b/bootstrap/app.php @@ -11,6 +11,7 @@ use App\Lottery\ErrorCode; use App\Support\ApiResponse; +use App\Support\ApiValidationErrors; use Illuminate\Http\Request; use App\Support\LotteryLocale; use Illuminate\Foundation\Application; @@ -83,10 +84,15 @@ return Application::configure(basePath: dirname(__DIR__)) return null; } + $loc = $locale($request); + $errors = ApiValidationErrors::normalize($e->errors(), $loc); + $msg = ApiValidationErrors::summary($errors) + ?? trans('api.validation_failed', [], $loc); + return ApiResponse::error( - trans('api.validation_failed', [], $locale($request)), + $msg, ErrorCode::ValidationFailed->value, - ['errors' => $e->errors()], + ['errors' => $errors], 422, ); }); diff --git a/database/migrations/2026_06_03_120000_split_agent_permission_granularity.php b/database/migrations/2026_06_03_120000_split_agent_permission_granularity.php new file mode 100644 index 0000000..a6483db --- /dev/null +++ b/database/migrations/2026_06_03_120000_split_agent_permission_granularity.php @@ -0,0 +1,177 @@ +where('code', 'view')->value('id'); + $manageActionId = DB::table('admin_action_catalog')->where('code', 'manage')->value('id'); + if ($viewActionId === null || $manageActionId === null) { + return; + } + + $agentMenuId = (int) DB::table('admin_menus')->where('code', 'system.agents')->value('id'); + if ($agentMenuId === 0) { + return; + } + + $rolesMenuId = $this->ensureChildMenu($agentMenuId, 'system.agents.roles', '代理角色', $now); + $usersMenuId = $this->ensureChildMenu($agentMenuId, 'system.agents.users', '代理账号', $now); + + $this->ensureMenuAction((int) $rolesMenuId, (int) $viewActionId, 'agent.role.view', '代理角色查看', $now); + $this->ensureMenuAction((int) $rolesMenuId, (int) $manageActionId, 'agent.role.manage', '代理角色管理', $now); + $this->ensureMenuAction((int) $usersMenuId, (int) $viewActionId, 'agent.user.view', '代理账号查看', $now); + $this->ensureMenuAction((int) $usersMenuId, (int) $manageActionId, 'agent.user.manage', '代理账号管理', $now); + + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + $nodeViewId = $menuActionIds['agent.node.view'] ?? null; + $nodeManageId = $menuActionIds['agent.node.manage'] ?? null; + $roleViewId = $menuActionIds['agent.role.view'] ?? null; + $roleManageId = $menuActionIds['agent.role.manage'] ?? null; + $userViewId = $menuActionIds['agent.user.view'] ?? null; + $userManageId = $menuActionIds['agent.user.manage'] ?? null; + + if ($nodeViewId !== null && $roleViewId !== null && $userViewId !== null) { + $roleIdsWithNodeView = DB::table('admin_role_menu_actions') + ->where('menu_action_id', (int) $nodeViewId) + ->pluck('role_id') + ->unique() + ->all(); + + foreach ($roleIdsWithNodeView as $roleId) { + foreach ([$roleViewId, $userViewId] as $actionId) { + $this->attachRoleMenuAction((int) $roleId, (int) $actionId, $now); + } + } + } + + if ($nodeManageId !== null && $roleManageId !== null && $userManageId !== null) { + $roleIdsWithNodeManage = DB::table('admin_role_menu_actions') + ->where('menu_action_id', (int) $nodeManageId) + ->pluck('role_id') + ->unique() + ->all(); + + foreach ($roleIdsWithNodeManage as $roleId) { + foreach ([$roleManageId, $userManageId] as $actionId) { + $this->attachRoleMenuAction((int) $roleId, (int) $actionId, $now); + } + } + } + + $resources = array_values(array_filter( + AdminAuthorizationRegistry::resources(), + static fn (array $resource): bool => str_starts_with((string) $resource['code'], 'admin.agent-') + )); + + foreach ($resources as $resource) { + $resourceId = DB::table('admin_api_resources')->where('code', $resource['code'])->value('id'); + if ($resourceId === null) { + continue; + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] ?? [] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + } + + private function ensureChildMenu(int $parentId, string $code, string $name, Carbon $now): int + { + $existing = DB::table('admin_menus')->where('code', $code)->value('id'); + if ($existing !== null) { + return (int) $existing; + } + + return (int) DB::table('admin_menus')->insertGetId([ + 'parent_id' => $parentId, + 'menu_type' => 'button', + 'code' => $code, + 'name' => $name, + 'path' => null, + 'route_name' => null, + 'component' => null, + 'icon' => null, + 'active_menu_code' => 'system.agents', + 'sort_order' => 0, + 'is_visible' => false, + 'is_cache' => false, + 'is_external' => false, + 'status' => 1, + 'meta_json' => null, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + private function ensureMenuAction(int $menuId, int $actionId, string $permissionCode, string $name, Carbon $now): void + { + if (DB::table('admin_menu_actions')->where('permission_code', $permissionCode)->exists()) { + return; + } + + DB::table('admin_menu_actions')->insert([ + 'menu_id' => $menuId, + 'action_id' => $actionId, + 'permission_code' => $permissionCode, + 'name' => $name, + 'status' => 1, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + + private function attachRoleMenuAction(int $roleId, int $menuActionId, Carbon $now): void + { + $exists = DB::table('admin_role_menu_actions') + ->where('role_id', $roleId) + ->where('menu_action_id', $menuActionId) + ->exists(); + + if ($exists) { + return; + } + + DB::table('admin_role_menu_actions')->insert([ + 'role_id' => $roleId, + 'menu_action_id' => $menuActionId, + ]); + } + + public function down(): void + { + $codes = ['agent.role.view', 'agent.role.manage', 'agent.user.view', 'agent.user.manage']; + $actionIds = DB::table('admin_menu_actions')->whereIn('permission_code', $codes)->pluck('id')->all(); + + if ($actionIds !== []) { + DB::table('admin_role_menu_actions')->whereIn('menu_action_id', $actionIds)->delete(); + DB::table('admin_api_resource_bindings')->whereIn('menu_action_id', $actionIds)->delete(); + DB::table('admin_menu_actions')->whereIn('permission_code', $codes)->delete(); + } + + DB::table('admin_menus')->whereIn('code', ['system.agents.roles', 'system.agents.users'])->delete(); + } +}; diff --git a/database/migrations/2026_06_03_140000_ensure_agent_admin_user_destroy_api_resource.php b/database/migrations/2026_06_03_140000_ensure_agent_admin_user_destroy_api_resource.php new file mode 100644 index 0000000..378a089 --- /dev/null +++ b/database/migrations/2026_06_03_140000_ensure_agent_admin_user_destroy_api_resource.php @@ -0,0 +1,87 @@ +firstWhere('code', self::RESOURCE_CODE); + + if ($resource === null) { + return; + } + + $now = Carbon::now(); + $menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code'); + + $resourceId = DB::table('admin_api_resources') + ->where('code', self::RESOURCE_CODE) + ->value('id'); + + $payload = [ + 'module_code' => $resource['module_code'], + 'name' => $resource['name'], + 'http_method' => $resource['http_method'], + 'uri_pattern' => $resource['uri_pattern'], + 'route_name' => $resource['route_name'], + 'auth_mode' => $resource['auth_mode'], + 'is_audit_required' => $resource['is_audit_required'], + 'status' => 1, + 'meta_json' => null, + 'updated_at' => $now, + ]; + + if ($resourceId === null) { + $resourceId = DB::table('admin_api_resources')->insertGetId($payload + [ + 'code' => self::RESOURCE_CODE, + 'created_at' => $now, + ]); + } else { + DB::table('admin_api_resources') + ->where('id', (int) $resourceId) + ->update($payload); + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + + foreach ($resource['permission_codes'] as $permissionCode) { + $menuActionId = $menuActionIds[$permissionCode] ?? null; + if ($menuActionId === null) { + continue; + } + + DB::table('admin_api_resource_bindings')->insert([ + 'api_resource_id' => (int) $resourceId, + 'menu_action_id' => (int) $menuActionId, + 'created_at' => $now, + 'updated_at' => $now, + ]); + } + } + + public function down(): void + { + $resourceId = DB::table('admin_api_resources') + ->where('code', self::RESOURCE_CODE) + ->value('id'); + + if ($resourceId === null) { + return; + } + + DB::table('admin_api_resource_bindings') + ->where('api_resource_id', (int) $resourceId) + ->delete(); + DB::table('admin_api_resources')->where('id', (int) $resourceId)->delete(); + } +}; diff --git a/database/seeders/AdminRbacAndUserSeeder.php b/database/seeders/AdminRbacAndUserSeeder.php index 3a584b7..c8c2b3e 100644 --- a/database/seeders/AdminRbacAndUserSeeder.php +++ b/database/seeders/AdminRbacAndUserSeeder.php @@ -5,6 +5,8 @@ namespace Database\Seeders; use App\Models\AdminRole; use App\Models\AdminUser; use Illuminate\Database\Seeder; +use App\Support\AdminAgentPermissionMenuActionSync; +use App\Support\AdminDrawPermissionMenuActionSync; use App\Support\AdminPermissionBridge; /** @@ -28,6 +30,9 @@ final class AdminRbacAndUserSeeder extends Seeder public function run(): void { + AdminAgentPermissionMenuActionSync::syncMissing(); + AdminDrawPermissionMenuActionSync::syncMissing(); + $super = AdminRole::query()->updateOrCreate( ['slug' => AdminUser::ROLE_SUPER_ADMIN], ['code' => AdminUser::ROLE_SUPER_ADMIN, 'name' => '超级管理员'], diff --git a/lang/en/admin.php b/lang/en/admin.php index 2c618e2..288539a 100644 --- a/lang/en/admin.php +++ b/lang/en/admin.php @@ -23,6 +23,8 @@ return [ 'agent_node_has_children_cannot_delete' => 'This agent node has child nodes. Delete children first.', 'agent_node_has_users_cannot_delete' => 'This agent node still has bound admin accounts and cannot be deleted.', 'agent_node_has_roles_cannot_delete' => 'This agent node still has bound roles and cannot be deleted.', + 'agent_role_in_use' => 'This role is still assigned to :count account(s). Unbind them in Accounts before deleting.', + 'agent_role_read_only' => 'Read-only template roles cannot be changed or deleted.', 'user_cannot_delete_self' => 'Cannot delete your own account.', 'user_cannot_delete_last_super_admin' => 'Cannot delete the last super admin.', 'super_admin_only_for_roles' => 'Only super admins can manage roles.', diff --git a/lang/en/validation.php b/lang/en/validation.php new file mode 100644 index 0000000..1e1eedf --- /dev/null +++ b/lang/en/validation.php @@ -0,0 +1,11 @@ + require __DIR__.'/validation_attributes.php', + 'custom' => require __DIR__.'/validation_custom.php', + 'business' => require __DIR__.'/validation_business.php', + 'exact' => require __DIR__.'/validation_exact.php', + ], +); diff --git a/lang/en/validation_attributes.php b/lang/en/validation_attributes.php new file mode 100644 index 0000000..550deb4 --- /dev/null +++ b/lang/en/validation_attributes.php @@ -0,0 +1,62 @@ + 'account', + 'password' => 'password', + 'captcha_key' => 'captcha', + 'captcha_code' => 'captcha code', + 'username' => 'username', + 'nickname' => 'nickname', + 'name' => 'name', + 'code' => 'code', + 'slug' => 'slug', + 'status' => 'status', + 'email' => 'email', + 'group' => 'settings group', + 'value' => 'value', + 'key' => 'key', + 'items' => 'items', + 'parent_id' => 'parent node', + 'role_ids' => 'roles', + 'role_slugs' => 'roles', + 'permission_slugs' => 'permissions', + 'grants' => 'delegation grants', + 'child_agent_id' => 'child agent', + 'role' => 'role', + 'user' => 'user', + 'site_code' => 'site code', + 'site_player_id' => 'site player ID', + 'player_id' => 'player', + 'agent_node_id' => 'agent node', + 'currency_code' => 'currency', + 'currency' => 'currency', + 'amount' => 'amount', + 'amount_delta' => 'adjustment amount', + 'reason' => 'reason', + 'remark' => 'remark', + 'draw_id' => 'draw', + 'draw_no' => 'draw number', + 'draw_time' => 'draw time', + 'start_time' => 'start time', + 'close_time' => 'close time', + 'period' => 'period', + 'date_from' => 'start date', + 'date_to' => 'end date', + 'play_code' => 'play', + 'page' => 'page', + 'per_page' => 'per page', + 'lines' => 'bet lines', + 'lines.*.number' => 'number', + 'lines.*.play_code' => 'play', + 'lines.*.amount' => 'stake', + 'idempotent_key' => 'idempotency key', + 'wallet_api_url' => 'wallet API URL', + 'prize_type' => 'prize type', + 'prize_index' => 'prize index', + 'number_4d' => '4-digit number', + 'normalized_number' => 'number', + 'items.*.play_code' => 'play', + 'items.*.odds_value' => 'odds', + 'items.*.display_name' => 'display name', + 'report_type' => 'report type', +]; diff --git a/lang/en/validation_business.php b/lang/en/validation_business.php new file mode 100644 index 0000000..d7cf293 --- /dev/null +++ b/lang/en/validation_business.php @@ -0,0 +1,16 @@ + 'This value already exists. Please choose another.', + 'required' => ':attribute is required.', + 'system_role' => 'System roles cannot be deleted.', + 'agent_mismatch' => 'This user does not belong to the current agent node.', + 'invalid_for_agent' => 'One or more roles are not valid for this agent.', + 'not_manageable' => 'You cannot manage this child agent.', + 'invalid_menu_action' => 'Invalid or unknown permission item.', + 'exceeds_actor' => 'These permissions exceed what you may grant: :detail', + 'exceeds_parent_ceiling' => 'These permissions exceed the parent delegation ceiling: :detail', + 'exceeds_delegation_ceiling' => 'These permissions exceed this node\'s delegation ceiling: :detail', + 'permission_exceeds_actor' => 'These permissions exceed what you may grant: :detail', + 'permission_catalog_incomplete' => 'Permission catalog is incomplete (missing: :detail). Run migrate and admin-auth-sync.', +]; diff --git a/lang/en/validation_custom.php b/lang/en/validation_custom.php new file mode 100644 index 0000000..a463fa1 --- /dev/null +++ b/lang/en/validation_custom.php @@ -0,0 +1,44 @@ + [ + 'regex' => 'Code may only contain letters, digits, underscores, and hyphens; site codes must start with a lowercase letter or digit.', + 'unique' => 'This code is already in use.', + ], + 'slug' => [ + 'regex' => 'Slug may only contain lowercase letters, digits, underscores, and hyphens.', + 'unique' => 'This slug is already in use.', + ], + 'account' => [ + 'regex' => 'Account may only contain letters, digits, dots, underscores, and hyphens.', + ], + 'username' => [ + 'regex' => 'Username may only contain letters, digits, dots, underscores, and hyphens.', + 'unique' => 'This username is already in use.', + ], + 'draw_no' => [ + 'regex' => 'Draw number must match YYYYMMDD-sequence (e.g. 20260101-001).', + ], + 'number_4d' => [ + 'regex' => 'Number must be exactly 4 digits.', + ], + 'normalized_number' => [ + 'regex' => 'Number must be exactly 4 digits.', + 'size' => 'Number must be exactly 4 digits.', + ], + 'wallet_api_url' => [ + 'wallet_api_url' => 'Wallet API URL must be an https public root URL (no localhost, private IP, path, or query).', + ], + 'amount_delta' => [ + 'not_in' => 'Adjustment amount cannot be zero.', + ], + 'password' => [ + 'min' => 'Password must be at least :min characters.', + ], + 'reason' => [ + 'min' => 'Reason must be at least :min characters.', + ], + 'items' => [ + 'size' => ':attribute must contain exactly :size items.', + ], +]; diff --git a/lang/en/validation_exact.php b/lang/en/validation_exact.php new file mode 100644 index 0000000..e6f8292 --- /dev/null +++ b/lang/en/validation_exact.php @@ -0,0 +1,6 @@ + 'All 23 draw prize slots must be provided (first, second, third, starter, and consolation).', + 'items_must_contain_23_slots' => 'All 23 draw prize slots must be provided (first, second, third, starter, and consolation).', +]; diff --git a/lang/en/validation_rules.php b/lang/en/validation_rules.php new file mode 100644 index 0000000..f226f51 --- /dev/null +++ b/lang/en/validation_rules.php @@ -0,0 +1,54 @@ + 'You must accept :attribute.', + 'array' => ':attribute must be a list.', + 'boolean' => ':attribute must be true or false.', + 'confirmed' => ':attribute confirmation does not match.', + 'date' => ':attribute must be a valid date.', + 'date_format' => ':attribute must match the format :format.', + 'email' => ':attribute must be a valid email address.', + 'exists' => 'The selected :attribute is invalid or unavailable.', + 'filled' => ':attribute must have a value.', + 'in' => 'The selected :attribute is invalid.', + 'integer' => ':attribute must be an integer.', + 'json' => ':attribute must be valid JSON.', + 'max' => [ + 'array' => ':attribute may not have more than :max items.', + 'numeric' => ':attribute may not be greater than :max.', + 'string' => ':attribute may not exceed :max characters.', + ], + 'min' => [ + 'array' => ':attribute must have at least :min items.', + 'numeric' => ':attribute must be at least :min.', + 'string' => ':attribute must be at least :min characters.', + ], + 'not_in' => 'The selected :attribute is invalid.', + 'not_regex' => ':attribute format is invalid.', + 'numeric' => ':attribute must be a number.', + 'present' => ':attribute must be present.', + 'prohibited' => ':attribute is not allowed.', + 'regex' => ':attribute format is invalid.', + 'required' => ':attribute is required.', + 'required_if' => ':attribute is required when :other is :value.', + 'required_with' => ':attribute is required when :values is present.', + 'required_with_all' => ':attribute is required when :values are present.', + 'required_without' => ':attribute is required when :values is not present.', + 'same' => ':attribute must match :other.', + 'size' => [ + 'array' => ':attribute must contain :size items.', + 'numeric' => ':attribute must be :size.', + 'string' => ':attribute must be :size characters.', + ], + 'string' => ':attribute must be text.', + 'unique' => ':attribute is already taken.', + 'url' => ':attribute must be a valid URL.', + 'uuid' => ':attribute must be a valid UUID.', + 'between' => [ + 'array' => ':attribute must have between :min and :max items.', + 'numeric' => ':attribute must be between :min and :max.', + 'string' => ':attribute must be between :min and :max characters.', + ], + 'after_or_equal' => ':attribute must be on or after :date.', + 'distinct' => ':attribute has a duplicate value.', +]; diff --git a/lang/ne/admin.php b/lang/ne/admin.php index 3cc0e6d..80192c6 100644 --- a/lang/ne/admin.php +++ b/lang/ne/admin.php @@ -23,6 +23,8 @@ return [ 'agent_node_has_children_cannot_delete' => 'यस एजेन्ट नोडमा चाइल्ड नोडहरू छन्, पहिले तिनीहरू हटाउनुहोस्।', 'agent_node_has_users_cannot_delete' => 'यस एजेन्ट नोडमा अझै एडमिन खाता जोडिएको छ, मेटाउन मिल्दैन।', 'agent_node_has_roles_cannot_delete' => 'यस एजेन्ट नोडमा अझै भूमिका जोडिएको छ, मेटाउन मिल्दैन।', + 'agent_role_in_use' => 'यो भूमिका अझै :count खातामा प्रयोगमा छ। पहिले खाता ट्याबमा हटाउनुहोस्।', + 'agent_role_read_only' => 'Read-only टेम्प्लेट भूमिका मेटाउन वा सम्पादन गर्न मिल्दैन।', 'user_cannot_delete_self' => 'आफ्नै खाता मेटाउन मिल्दैन।', 'user_cannot_delete_last_super_admin' => 'अन्तिम सुपर एडमिन मेटाउन मिल्दैन।', 'super_admin_only_for_roles' => 'भूमिका व्यवस्थापन केवल सुपर एडमिनले गर्न सक्छ।', diff --git a/lang/ne/validation.php b/lang/ne/validation.php new file mode 100644 index 0000000..54e923a --- /dev/null +++ b/lang/ne/validation.php @@ -0,0 +1,3 @@ + '该代理节点存在下级代理,请先清空下级后再删除。', 'agent_node_has_users_cannot_delete' => '该代理节点下仍有关联账号,不能删除。', 'agent_node_has_roles_cannot_delete' => '该代理节点下仍有关联角色,不能删除。', + 'agent_role_in_use' => '该角色仍有 :count 个账号在使用,请先在「账号」里解除绑定后再删除。', + 'agent_role_read_only' => '只读模板角色不可删除或修改。', 'user_cannot_delete_self' => '不能删除当前登录账号。', 'user_cannot_delete_last_super_admin' => '不能删除最后一个超级管理员。', 'super_admin_only_for_roles' => '仅超级管理员可管理角色。', diff --git a/lang/zh/validation.php b/lang/zh/validation.php new file mode 100644 index 0000000..1e1eedf --- /dev/null +++ b/lang/zh/validation.php @@ -0,0 +1,11 @@ + require __DIR__.'/validation_attributes.php', + 'custom' => require __DIR__.'/validation_custom.php', + 'business' => require __DIR__.'/validation_business.php', + 'exact' => require __DIR__.'/validation_exact.php', + ], +); diff --git a/lang/zh/validation_attributes.php b/lang/zh/validation_attributes.php new file mode 100644 index 0000000..5504425 --- /dev/null +++ b/lang/zh/validation_attributes.php @@ -0,0 +1,162 @@ + '登录账号', + 'password' => '密码', + 'captcha_key' => '验证码', + 'captcha_code' => '验证码', + 'username' => '用户名', + 'nickname' => '昵称', + 'name' => '名称', + 'code' => '编码', + 'slug' => '标识', + 'status' => '状态', + 'email' => '邮箱', + 'group' => '配置分组', + 'value' => '配置值', + 'key' => '配置键', + 'items' => '配置项列表', + 'parent_id' => '上级节点', + 'role_ids' => '角色', + 'role_ids.*' => '角色', + 'role_slugs' => '角色', + 'role_slugs.*' => '角色', + 'permission_slugs' => '权限', + 'permission_slugs.*' => '权限项', + 'permissions' => '权限', + 'permissions.*' => '权限项', + 'grants' => '下放权限', + 'grants.*.menu_action_id' => '权限项', + 'grants.*.can_delegate' => '是否可继续下放', + 'child_agent_id' => '下级代理', + 'role' => '角色', + 'user' => '账号', + 'site_id' => '站点', + 'site_code' => '站点编码', + 'site_player_id' => '主站玩家 ID', + 'player_id' => '玩家', + 'player_account' => '玩家账号', + 'agent_node_id' => '代理节点', + 'currency_code' => '币种', + 'currency' => '币种', + 'amount' => '金额', + 'amount_delta' => '调整金额', + 'reason' => '原因', + 'remark' => '备注', + 'draw_id' => '期号 ID', + 'draw_no' => '期号', + 'draw_time' => '开奖时间', + 'start_time' => '开始时间', + 'close_time' => '封盘时间', + 'business_date' => '业务日期', + 'sequence_no' => '流水号', + 'period' => '统计周期', + 'date_from' => '开始日期', + 'date_to' => '结束日期', + 'metric' => '指标', + 'play_code' => '玩法', + 'keyword' => '关键词', + 'default_currency' => '默认币种', + 'page' => '页码', + 'per_page' => '每页条数', + 'size' => '每页条数', + 'number' => '号码', + 'start_date' => '开始日期', + 'end_date' => '结束日期', + 'transfer_no' => '转账单号', + 'external_ref_no' => '外部参考号', + 'txn_no' => '流水号', + 'biz_type' => '业务类型', + 'created_from' => '创建开始日期', + 'created_to' => '创建结束日期', + 'report_type' => '报表类型', + 'export_format' => '导出格式', + 'parameters' => '报表参数', + 'parameters.date_from' => '开始日期', + 'parameters.date_to' => '结束日期', + 'parameters.player_id' => '玩家', + 'parameters.play_code' => '玩法', + 'parameters.operator_id' => '操作员', + 'parameters.draw_id' => '期号', + 'parameters.draw_no' => '期号', + 'parameters.normalized_number' => '号码', + 'filter_json' => '筛选条件', + 'filter_json.date_from' => '开始日期', + 'filter_json.date_to' => '结束日期', + 'filter_json.draw_id' => '期号', + 'filter_json.draw_no' => '期号', + 'filter_json.normalized_number' => '号码', + 'reconcile_type' => '对账类型', + 'period_start' => '周期开始', + 'period_end' => '周期结束', + 'decimal_places' => '小数位数', + 'is_enabled' => '是否启用', + 'is_bettable' => '是否可下注', + 'description' => '描述', + 'wallet_api_url' => '钱包 API 地址', + 'wallet_debit_path' => '扣款路径', + 'wallet_credit_path' => '加款路径', + 'wallet_balance_path' => '余额查询路径', + 'wallet_timeout_seconds' => '请求超时(秒)', + 'iframe_allowed_origins' => 'iframe 允许来源', + 'iframe_allowed_origins.*' => 'iframe 来源', + 'lottery_h5_base_url' => 'H5 基础地址', + 'notes' => '备注', + 'clone_from_version_id' => '克隆来源版本', + 'current_amount' => '当前金额', + 'contribution_rate' => '贡献比例', + 'trigger_threshold' => '触发阈值', + 'payout_rate' => '派彩比例', + 'force_trigger_draw_gap' => '强制触发期数间隔', + 'min_bet_amount' => '最小下注额', + 'combo_trigger_play_codes' => '组合触发玩法', + 'combo_trigger_play_codes.*' => '组合触发玩法', + 'lines' => '注单明细', + 'lines.*.number' => '投注号码', + 'lines.*.play_code' => '玩法', + 'lines.*.amount' => '下注金额', + 'lines.*.digit_slot' => '数位', + 'lines.*.dimension' => '维度', + 'client_trace_id' => '客户端追踪 ID', + 'idempotent_key' => '幂等键', + 'expected_config_versions' => '期望配置版本', + 'expected_config_versions.play_config_version_no' => '玩法配置版本号', + 'expected_config_versions.odds_version_no' => '赔率配置版本号', + 'expected_config_versions.risk_cap_version_no' => '封顶配置版本号', + 'prize_type' => '奖项类型', + 'prize_index' => '奖项序号', + 'number_4d' => '四位号码', + 'normalized_number' => '号码', + 'items.*.key' => '配置键', + 'items.*.value' => '配置值', + 'items.*.play_code' => '玩法', + 'items.*.prize_scope' => '奖项档位', + 'items.*.odds_value' => '赔率', + 'items.*.rebate_rate' => '返水比例', + 'items.*.commission_rate' => '佣金比例', + 'items.*.currency_code' => '币种', + 'items.*.category' => '分类', + 'items.*.dimension' => '维度', + 'items.*.bet_mode' => '下注模式', + 'items.*.display_name' => '显示名称', + 'items.*.display_order' => '排序', + 'items.*.min_bet_amount' => '最小下注额', + 'items.*.max_bet_amount' => '最大下注额', + 'items.*.supports_multi_number' => '是否支持多号', + 'items.*.cap_amount' => '封顶金额', + 'items.*.cap_type' => '封顶类型', + 'items.*.draw_id' => '期号', + 'items.*.prize_type' => '奖项类型', + 'items.*.prize_index' => '奖项序号', + 'items.*.number_4d' => '四位号码', + 'items.*.normalized_number' => '号码', + 'items.*.side_a_ref' => '对账侧 A 参考', + 'items.*.side_b_ref' => '对账侧 B 参考', + 'items.*.difference_amount' => '差异金额', + 'items.*.status' => '状态', + 'sort_order' => '排序', + 'supports_multi_number' => '是否支持多号', + 'reserved_rule_json' => '预留规则', + 'extra_config_json' => '扩展配置', +]; diff --git a/lang/zh/validation_business.php b/lang/zh/validation_business.php new file mode 100644 index 0000000..dd35ccf --- /dev/null +++ b/lang/zh/validation_business.php @@ -0,0 +1,17 @@ + '该内容已存在,请更换后重试。', + 'required' => ':attribute 不能为空。', + 'system_role' => '系统角色不可删除。', + 'agent_mismatch' => '该账号不属于当前代理节点。', + 'invalid_for_agent' => '所选角色与当前代理不匹配。', + 'not_manageable' => '无权管理该下级代理。', + 'invalid_menu_action' => '权限项无效或不存在。', + 'exceeds_actor' => '下列权限超出您可分配的范围::detail', + 'exceeds_parent_ceiling' => '下列权限超出上级允许下放的范围::detail', + 'exceeds_delegation_ceiling' => '下列权限超出本节点下放上限::detail', + 'permission_exceeds_actor' => '下列权限超出您可分配的范围::detail', + 'permission_catalog_incomplete' => '权限目录不完整,缺少::detail。请联系管理员执行 migrate 与 admin-auth-sync。', +]; diff --git a/lang/zh/validation_custom.php b/lang/zh/validation_custom.php new file mode 100644 index 0000000..668aa76 --- /dev/null +++ b/lang/zh/validation_custom.php @@ -0,0 +1,60 @@ + [ + 'regex' => '编码只能使用字母、数字、下划线(_)和连字符(-);站点编码须以小写字母或数字开头。', + 'unique' => '该编码已被占用,请更换。', + ], + 'slug' => [ + 'regex' => '标识只能使用小写字母、数字、下划线和连字符。', + 'unique' => '该标识已被占用,请更换。', + ], + 'account' => [ + 'regex' => '登录账号只能使用字母、数字、点(.)、下划线和连字符。', + ], + 'username' => [ + 'regex' => '用户名只能使用字母、数字、点(.)、下划线和连字符。', + 'unique' => '该用户名已被占用,请更换。', + ], + 'email' => [ + 'unique' => '该邮箱已被占用,请更换。', + ], + 'draw_no' => [ + 'regex' => '期号格式须为 YYYYMMDD-流水号(例如 20260101-001)。', + ], + 'number_4d' => [ + 'regex' => '号码必须是 4 位数字。', + ], + 'normalized_number' => [ + 'regex' => '号码必须是 4 位数字。', + 'size' => '号码必须是 4 位数字。', + ], + 'wallet_api_url' => [ + 'wallet_api_url' => '钱包 API 地址必须是 https 的公开域名根地址,不能为 localhost、内网 IP,且不能带路径或查询参数。', + ], + 'amount_delta' => [ + 'not_in' => '调整金额不能为 0。', + ], + 'password' => [ + 'min' => '密码至少需要 :min 个字符。', + ], + 'reason' => [ + 'min' => '原因至少需要 :min 个字符。', + ], + 'lines' => [ + 'max' => '单次最多提交 :max 注。', + 'min' => '至少需要提交 :min 注。', + ], + 'items' => [ + 'min' => '至少需要 :min 条配置。', + 'max' => '最多只能提交 :max 条配置。', + 'size' => '必须恰好包含 :size 条配置。', + ], + 'role_slugs' => [ + 'min' => '至少须选择一个角色。', + ], + 'role_ids' => [ + 'required' => '请选择角色。', + ], +]; diff --git a/lang/zh/validation_exact.php b/lang/zh/validation_exact.php new file mode 100644 index 0000000..5bedbbf --- /dev/null +++ b/lang/zh/validation_exact.php @@ -0,0 +1,7 @@ + '须提交完整的 23 个开奖奖项(头奖、二奖、三奖、入围、安慰奖各档位)。', + 'items_must_contain_23_slots' => '须提交完整的 23 个开奖奖项(头奖、二奖、三奖、入围、安慰奖各档位)。', +]; diff --git a/lang/zh/validation_rules.php b/lang/zh/validation_rules.php new file mode 100644 index 0000000..830c076 --- /dev/null +++ b/lang/zh/validation_rules.php @@ -0,0 +1,113 @@ + '请勾选接受 :attribute。', + 'accepted_if' => '当 :other 为 :value 时,必须接受 :attribute。', + 'active_url' => ':attribute 必须是有效的网址。', + 'after' => ':attribute 必须晚于 :date。', + 'after_or_equal' => ':attribute 必须不早于 :date。', + 'alpha' => ':attribute 只能包含字母。', + 'alpha_dash' => ':attribute 只能包含字母、数字、下划线和连字符。', + 'alpha_num' => ':attribute 只能包含字母和数字。', + 'array' => ':attribute 必须是列表。', + 'ascii' => ':attribute 只能包含半角字母、数字和符号。', + 'before' => ':attribute 必须早于 :date。', + 'before_or_equal' => ':attribute 必须不晚于 :date。', + 'between' => [ + 'array' => ':attribute 项数须在 :min 到 :max 之间。', + 'file' => ':attribute 大小须在 :min 到 :max KB 之间。', + 'numeric' => ':attribute 须在 :min 到 :max 之间。', + 'string' => ':attribute 长度须在 :min 到 :max 个字符之间。', + ], + 'boolean' => ':attribute 必须是是或否。', + 'confirmed' => ':attribute 两次输入不一致。', + 'date' => ':attribute 必须是有效日期。', + 'date_equals' => ':attribute 必须等于 :date。', + 'date_format' => ':attribute 格式须为 :format。', + 'decimal' => ':attribute 须保留 :decimal 位小数。', + 'declined' => '必须拒绝 :attribute。', + 'different' => ':attribute 与 :other 不能相同。', + 'digits' => ':attribute 必须是 :digits 位数字。', + 'digits_between' => ':attribute 须在 :min 到 :max 位数字之间。', + 'distinct' => ':attribute 存在重复值。', + 'email' => ':attribute 必须是有效的邮箱地址。', + 'ends_with' => ':attribute 必须以以下之一结尾::values。', + 'enum' => '所选的 :attribute 无效。', + 'exists' => '所选的 :attribute 不存在或不可用。', + 'file' => ':attribute 必须是文件。', + 'filled' => ':attribute 不能为空。', + 'gt' => [ + 'array' => ':attribute 项数须多于 :value 个。', + 'file' => ':attribute 须大于 :value KB。', + 'numeric' => ':attribute 须大于 :value。', + 'string' => ':attribute 须多于 :value 个字符。', + ], + 'gte' => [ + 'array' => ':attribute 至少须有 :value 项。', + 'file' => ':attribute 须不小于 :value KB。', + 'numeric' => ':attribute 须不小于 :value。', + 'string' => ':attribute 至少须 :value 个字符。', + ], + 'image' => ':attribute 必须是图片。', + 'in' => ':attribute 取值无效。', + 'in_array' => ':attribute 不存在于 :other 中。', + 'integer' => ':attribute 必须是整数。', + 'ip' => ':attribute 必须是有效的 IP 地址。', + 'json' => ':attribute 必须是有效的 JSON。', + 'lowercase' => ':attribute 必须是小写。', + 'lt' => [ + 'array' => ':attribute 项数须少于 :value 个。', + 'file' => ':attribute 须小于 :value KB。', + 'numeric' => ':attribute 须小于 :value。', + 'string' => ':attribute 须少于 :value 个字符。', + ], + 'lte' => [ + 'array' => ':attribute 最多 :value 项。', + 'file' => ':attribute 须不大于 :value KB。', + 'numeric' => ':attribute 须不大于 :value。', + 'string' => ':attribute 最多 :value 个字符。', + ], + 'max' => [ + 'array' => ':attribute 最多 :max 项。', + 'file' => ':attribute 不能超过 :max KB。', + 'numeric' => ':attribute 不能大于 :max。', + 'string' => ':attribute 不能超过 :max 个字符。', + ], + 'min' => [ + 'array' => ':attribute 至少 :min 项。', + 'file' => ':attribute 至少 :min KB。', + 'numeric' => ':attribute 不能小于 :min。', + 'string' => ':attribute 至少 :min 个字符。', + ], + 'not_in' => ':attribute 取值无效。', + 'not_regex' => ':attribute 格式不正确。', + 'numeric' => ':attribute 必须是数字。', + 'present' => ':attribute 必须提交(可为空)。', + 'prohibited' => ':attribute 不允许提交。', + 'prohibited_if' => '当 :other 为 :value 时,不允许提交 :attribute。', + 'regex' => ':attribute 格式不正确。', + 'required' => ':attribute 不能为空。', + 'required_if' => '当 :other 为 :value 时,:attribute 不能为空。', + 'required_unless' => '除非 :other 在 :values 中,否则 :attribute 不能为空。', + 'required_with' => '当存在 :values 时,:attribute 不能为空。', + 'required_with_all' => '当存在 :values 时,:attribute 不能为空。', + 'required_without' => '当不存在 :values 时,:attribute 不能为空。', + 'required_without_all' => '当 :values 均不存在时,:attribute 不能为空。', + 'required_array_keys' => ':attribute 须包含::values。', + 'same' => ':attribute 必须与 :other 一致。', + 'size' => [ + 'array' => ':attribute 必须包含 :size 项。', + 'file' => ':attribute 必须为 :size KB。', + 'numeric' => ':attribute 必须为 :size。', + 'string' => ':attribute 必须为 :size 个字符。', + ], + 'starts_with' => ':attribute 必须以以下之一开头::values。', + 'string' => ':attribute 必须是文本。', + 'timezone' => ':attribute 必须是有效的时区。', + 'unique' => ':attribute 已被占用,请更换。', + 'uploaded' => ':attribute 上传失败。', + 'uppercase' => ':attribute 必须是大写。', + 'url' => ':attribute 必须是有效的网址。', + 'uuid' => ':attribute 必须是有效的 UUID。', +]; diff --git a/routes/api/v1/admin/agent.php b/routes/api/v1/admin/agent.php index 0a3e81e..6600968 100644 --- a/routes/api/v1/admin/agent.php +++ b/routes/api/v1/admin/agent.php @@ -15,6 +15,7 @@ use App\Http\Controllers\Api\V1\Admin\Agent\AgentRoleDestroyController; use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeAdminUserIndexController; use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeAdminUserStoreController; use App\Http\Controllers\Api\V1\Admin\Agent\AgentAdminUserRoleSyncController; +use App\Http\Controllers\Api\V1\Admin\Agent\AgentAdminUserDestroyController; use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeDelegationGrantIndexController; use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeDelegationGrantSyncController; @@ -54,4 +55,6 @@ Route::middleware('admin.api-resource') Route::put('agent-admin-users/{admin_user}/roles', AgentAdminUserRoleSyncController::class) ->name('api.v1.admin.agent-admin-users.roles.sync'); + Route::delete('agent-admin-users/{admin_user}', AgentAdminUserDestroyController::class) + ->name('api.v1.admin.agent-admin-users.destroy'); }); diff --git a/tests/Feature/AdminAgentAccountRoleDestroyTest.php b/tests/Feature/AdminAgentAccountRoleDestroyTest.php new file mode 100644 index 0000000..90c7ec3 --- /dev/null +++ b/tests/Feature/AdminAgentAccountRoleDestroyTest.php @@ -0,0 +1,126 @@ +artisan('lottery:admin-auth-sync')->assertExitCode(0); +}); + +test('agent role can be deleted when no users assigned', function (): void { + $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); + $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); + + $super = AdminUser::query()->create([ + 'username' => 'destroy_role_super', + 'name' => 'Super', + 'email' => null, + 'password' => Hash::make('secret-strong'), + 'status' => 0, + ]); + grantSuperAdminRole($super); + $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; + + $branch = app(\App\Services\Agent\AgentNodeService::class)->createChild($super, [ + 'parent_id' => $rootId, + 'code' => 'destroy-role-branch', + 'name' => 'Destroy Role Branch', + ]); + + $role = AdminRole::query()->create([ + 'slug' => 'deletable_role', + 'name' => 'Deletable', + 'scope_type' => AdminRole::SCOPE_AGENT, + 'owner_agent_id' => $branch->id, + ]); + $role->syncLegacyPermissionSlugs(['prd.agent.role.view']); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->deleteJson('/api/v1/admin/agent-roles/'.$role->id) + ->assertOk(); + + expect(AdminRole::query()->find($role->id))->toBeNull(); +}); + +test('agent role cannot be deleted while assigned to a user', function (): void { + $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); + $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); + + $super = AdminUser::query()->create([ + 'username' => 'destroy_role_blocked_super', + 'name' => 'Super', + 'email' => null, + 'password' => Hash::make('secret-strong'), + 'status' => 0, + ]); + grantSuperAdminRole($super); + $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; + + $branch = app(\App\Services\Agent\AgentNodeService::class)->createChild($super, [ + 'parent_id' => $rootId, + 'code' => 'destroy-role-blocked', + 'name' => 'Blocked Branch', + ]); + + $role = AdminRole::query()->create([ + 'slug' => 'blocked_role', + 'name' => 'Blocked', + 'scope_type' => AdminRole::SCOPE_AGENT, + 'owner_agent_id' => $branch->id, + ]); + $role->syncLegacyPermissionSlugs(['prd.agent.role.view']); + + $user = app(\App\Services\Agent\AgentAdminUserService::class)->createUnderAgent($branch, [ + 'username' => 'blocked_user', + 'nickname' => 'Blocked User', + 'password' => 'secret-strong-2', + 'role_ids' => [(int) $role->id], + ]); + + expect($user->id)->toBeGreaterThan(0); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->deleteJson('/api/v1/admin/agent-roles/'.$role->id) + ->assertStatus(422); +}); + +test('agent admin user can be deleted under agent node', function (): void { + $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); + $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); + + $super = AdminUser::query()->create([ + 'username' => 'destroy_user_super', + 'name' => 'Super', + 'email' => null, + 'password' => Hash::make('secret-strong'), + 'status' => 0, + ]); + grantSuperAdminRole($super); + $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; + + $branch = app(\App\Services\Agent\AgentNodeService::class)->createChild($super, [ + 'parent_id' => $rootId, + 'code' => 'destroy-user-branch', + 'name' => 'Destroy User Branch', + ]); + + $user = app(\App\Services\Agent\AgentAdminUserService::class)->createUnderAgent($branch, [ + 'username' => 'agent_delete_me', + 'nickname' => 'Delete Me', + 'password' => 'secret-strong-3', + 'role_ids' => [], + ]); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->deleteJson('/api/v1/admin/agent-admin-users/'.$user->id) + ->assertOk() + ->assertJsonPath('data.deleted', true); + + expect(AdminUser::query()->find($user->id))->toBeNull(); + expect(DB::table('admin_user_agents')->where('admin_user_id', $user->id)->exists())->toBeFalse(); +}); diff --git a/tests/Feature/AdminAgentDelegationApiTest.php b/tests/Feature/AdminAgentDelegationApiTest.php index 5ad5847..eb3431b 100644 --- a/tests/Feature/AdminAgentDelegationApiTest.php +++ b/tests/Feature/AdminAgentDelegationApiTest.php @@ -28,7 +28,14 @@ function grantDelegationAgentOperator(AdminUser $admin, AgentNode $agent, bool $ ]); $codes = $manage - ? ['agent.node.view', 'agent.node.manage'] + ? [ + 'agent.node.view', + 'agent.node.manage', + 'agent.role.view', + 'agent.role.manage', + 'agent.user.view', + 'agent.user.manage', + ] : ['agent.node.view']; $actionIds = DB::table('admin_menu_actions') @@ -55,6 +62,13 @@ function grantDelegationAgentOperator(AdminUser $admin, AgentNode $agent, bool $ 'is_primary' => true, 'granted_at' => $now, ]); + + DB::table('admin_user_agent_roles')->insert([ + 'admin_user_id' => $admin->id, + 'agent_node_id' => (int) $agent->id, + 'role_id' => $roleId, + 'granted_at' => $now, + ]); } test('parent agent can sync delegation grants for direct child', function (): void { diff --git a/tests/Feature/AdminAgentNodeApiTest.php b/tests/Feature/AdminAgentNodeApiTest.php index 8d679e3..65e0202 100644 --- a/tests/Feature/AdminAgentNodeApiTest.php +++ b/tests/Feature/AdminAgentNodeApiTest.php @@ -36,7 +36,14 @@ function grantAgentOperatorRole(AdminUser $admin, AgentNode $agent, bool $manage ]); $codes = $manage - ? ['agent.node.view', 'agent.node.manage'] + ? [ + 'agent.node.view', + 'agent.node.manage', + 'agent.role.view', + 'agent.role.manage', + 'agent.user.view', + 'agent.user.manage', + ] : ['agent.node.view']; $actionIds = DB::table('admin_menu_actions') @@ -63,6 +70,13 @@ function grantAgentOperatorRole(AdminUser $admin, AgentNode $agent, bool $manage 'is_primary' => true, 'granted_at' => $now, ]); + + DB::table('admin_user_agent_roles')->insert([ + 'admin_user_id' => $admin->id, + 'agent_node_id' => (int) $agent->id, + 'role_id' => $roleId, + 'granted_at' => $now, + ]); } test('each admin site has exactly one root agent node after migration', function (): void { diff --git a/tests/Feature/AdminAgentPermissionGranularityTest.php b/tests/Feature/AdminAgentPermissionGranularityTest.php new file mode 100644 index 0000000..5c55272 --- /dev/null +++ b/tests/Feature/AdminAgentPermissionGranularityTest.php @@ -0,0 +1,244 @@ +artisan('lottery:admin-auth-sync')->assertExitCode(0); +}); + +test('role with only agent role view slug does not receive manage slugs in profile', function (): void { + $admin = AdminUser::query()->create([ + 'username' => 'agent_role_view_only', + 'name' => 'View Only', + 'email' => null, + 'password' => Hash::make('secret-strong'), + 'status' => 0, + ]); + + $role = AdminRole::query()->create([ + 'slug' => 'agent_role_view_only', + 'name' => 'Agent role view only', + ]); + $role->syncLegacyPermissionSlugs(['prd.agent.role.view']); + + $siteId = AdminUser::defaultAdminSiteId(); + $admin->roles()->sync([ + (int) $role->id => ['site_id' => $siteId, 'granted_at' => now()], + ]); + + $profile = AdminAuthProfile::fromAdmin($admin->fresh()); + + expect($profile['permissions'])->toContain('prd.agent.role.view') + ->and($profile['permissions'])->toContain('prd.agent.view') + ->not->toContain('prd.agent.role.manage') + ->not->toContain('prd.agent.user.manage') + ->not->toContain('prd.agent.manage'); +}); + +test('role with only agent role manage can create roles but not agent nodes', function (): void { + $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); + $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); + + $super = AdminUser::query()->create([ + 'username' => 'granularity_super', + 'name' => 'Super', + 'email' => null, + 'password' => Hash::make('secret-strong'), + 'status' => 0, + ]); + grantSuperAdminRole($super); + + $service = app(\App\Services\Agent\AgentNodeService::class); + $branch = $service->createChild($super, [ + 'parent_id' => $rootId, + 'code' => 'granularity-branch', + 'name' => 'Granularity Branch', + ]); + + $admin = AdminUser::query()->create([ + 'username' => 'agent_role_manage_only', + 'name' => 'Role Manage', + 'email' => null, + 'password' => Hash::make('secret-strong'), + 'status' => 0, + ]); + + $role = AdminRole::query()->create([ + 'slug' => 'agent_role_manage_only', + 'name' => 'Agent role manage only', + ]); + $role->syncLegacyPermissionSlugs(['prd.agent.role.manage']); + + $siteId = (int) $branch->admin_site_id; + DB::table('admin_user_site_roles')->insert([ + 'admin_user_id' => $admin->id, + 'site_id' => $siteId, + 'role_id' => $role->id, + 'granted_at' => now(), + ]); + DB::table('admin_user_agents')->insert([ + 'admin_user_id' => $admin->id, + 'agent_node_id' => $branch->id, + 'is_primary' => true, + 'granted_at' => now(), + ]); + DB::table('admin_user_agent_roles')->insert([ + 'admin_user_id' => $admin->id, + 'agent_node_id' => $branch->id, + 'role_id' => $role->id, + 'granted_at' => now(), + ]); + + $token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken; + + $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson('/api/v1/admin/agent-nodes/'.$branch->id.'/roles', [ + 'slug' => 'ops_role_only', + 'name' => 'Ops Role', + 'permission_slugs' => ['prd.agent.role.view'], + ]) + ->assertOk(); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson('/api/v1/admin/agent-nodes', [ + 'parent_id' => $branch->id, + 'code' => 'should-fail', + 'name' => 'Should Fail', + ]) + ->assertForbidden(); +}); + +test('role with only agent node manage cannot create roles or admin users', function (): void { + $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); + $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); + + $super = AdminUser::query()->create([ + 'username' => 'granularity_super_node', + 'name' => 'Super', + 'email' => null, + 'password' => Hash::make('secret-strong'), + 'status' => 0, + ]); + grantSuperAdminRole($super); + + $service = app(\App\Services\Agent\AgentNodeService::class); + $branch = $service->createChild($super, [ + 'parent_id' => $rootId, + 'code' => 'granularity-node-only', + 'name' => 'Node Only Branch', + ]); + + $admin = AdminUser::query()->create([ + 'username' => 'agent_node_manage_only', + 'name' => 'Node Manage', + 'email' => null, + 'password' => Hash::make('secret-strong'), + 'status' => 0, + ]); + + $role = AdminRole::query()->create([ + 'slug' => 'agent_node_manage_only', + 'name' => 'Agent node manage only', + ]); + $role->syncLegacyPermissionSlugs(['prd.agent.manage']); + + $siteId = (int) $branch->admin_site_id; + DB::table('admin_user_site_roles')->insert([ + 'admin_user_id' => $admin->id, + 'site_id' => $siteId, + 'role_id' => $role->id, + 'granted_at' => now(), + ]); + DB::table('admin_user_agents')->insert([ + 'admin_user_id' => $admin->id, + 'agent_node_id' => $branch->id, + 'is_primary' => true, + 'granted_at' => now(), + ]); + DB::table('admin_user_agent_roles')->insert([ + 'admin_user_id' => $admin->id, + 'agent_node_id' => $branch->id, + 'role_id' => $role->id, + 'granted_at' => now(), + ]); + + $token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken; + + $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson('/api/v1/admin/agent-nodes/'.$branch->id.'/roles', [ + 'slug' => 'should_fail_role', + 'name' => 'Should Fail Role', + 'permission_slugs' => ['prd.agent.role.view'], + ]) + ->assertForbidden(); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson('/api/v1/admin/agent-nodes/'.$branch->id.'/admin-users', [ + 'username' => 'should_fail_user', + 'name' => 'Should Fail User', + 'password' => 'secret-strong-2', + 'role_ids' => [], + ]) + ->assertForbidden(); +}); + +test('agent-bound admin with only site role bindings still receives manage slugs in profile', function (): void { + $siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id'); + $rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id'); + + $super = AdminUser::query()->create([ + 'username' => 'granularity_super_site_fallback', + 'name' => 'Super', + 'email' => null, + 'password' => Hash::make('secret-strong'), + 'status' => 0, + ]); + grantSuperAdminRole($super); + + $service = app(\App\Services\Agent\AgentNodeService::class); + $branch = $service->createChild($super, [ + 'parent_id' => $rootId, + 'code' => 'site-fallback-branch', + 'name' => 'Site Fallback Branch', + ]); + + $role = AdminRole::query()->create([ + 'slug' => 'agent_role_manage_site_only', + 'name' => 'Agent role manage site bind', + 'scope_type' => \App\Models\AdminRole::SCOPE_AGENT, + 'owner_agent_id' => $branch->id, + ]); + $role->syncLegacyPermissionSlugs(['prd.agent.role.manage']); + + $admin = AdminUser::query()->create([ + 'username' => 'agent_site_roles_only', + 'name' => 'Site Roles Only', + 'email' => null, + 'password' => Hash::make('secret-strong'), + 'status' => 0, + ]); + + DB::table('admin_user_agents')->insert([ + 'admin_user_id' => $admin->id, + 'agent_node_id' => $branch->id, + 'is_primary' => true, + 'granted_at' => now(), + ]); + DB::table('admin_user_site_roles')->insert([ + 'admin_user_id' => $admin->id, + 'site_id' => $siteId, + 'role_id' => $role->id, + 'granted_at' => now(), + ]); + + $profile = AdminAuthProfile::fromAdmin($admin->fresh()); + + expect($profile['permissions'])->toContain('prd.agent.role.manage'); + expect(DB::table('admin_user_agent_roles')->where('admin_user_id', $admin->id)->count())->toBe(0); +}); diff --git a/tests/Feature/AdminAgentRoleApiTest.php b/tests/Feature/AdminAgentRoleApiTest.php index 45fc374..ac33e6a 100644 --- a/tests/Feature/AdminAgentRoleApiTest.php +++ b/tests/Feature/AdminAgentRoleApiTest.php @@ -28,7 +28,15 @@ function grantAgentRoleManager(AdminUser $admin, AgentNode $agent): void 'updated_at' => $now, ]); - $codes = ['agent.node.view', 'agent.node.manage', 'service.players.view']; + $codes = [ + 'agent.node.view', + 'agent.node.manage', + 'agent.role.view', + 'agent.role.manage', + 'agent.user.view', + 'agent.user.manage', + 'service.players.view', + ]; $actionIds = DB::table('admin_menu_actions')->whereIn('permission_code', $codes)->pluck('id'); foreach ($actionIds as $actionId) { DB::table('admin_role_menu_actions')->insert([ diff --git a/tests/Feature/AdminAgentRolePermissionPersistTest.php b/tests/Feature/AdminAgentRolePermissionPersistTest.php new file mode 100644 index 0000000..f1dca30 --- /dev/null +++ b/tests/Feature/AdminAgentRolePermissionPersistTest.php @@ -0,0 +1,62 @@ +artisan('lottery:admin-auth-sync')->assertExitCode(0); +}); + +test('platform role sync persists agent role and user manage slugs', function (): void { + $super = AdminUser::query()->create([ + 'username' => 'persist_super', + 'name' => 'Super', + 'email' => null, + 'password' => Hash::make('secret-strong'), + 'status' => 0, + ]); + grantSuperAdminRole($super); + $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; + + $role = AdminRole::query()->create([ + 'slug' => 'agent_persist_test', + 'name' => 'Agent Persist Test', + ]); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->putJson('/api/v1/admin/admin-roles/'.$role->id.'/permissions', [ + 'permission_slugs' => [ + 'prd.agent.view', + 'prd.agent.manage', + 'prd.agent.role.view', + 'prd.agent.role.manage', + 'prd.agent.user.view', + 'prd.agent.user.manage', + ], + ]) + ->assertOk() + ->assertJsonPath('data.permission_slugs', function ($slugs): bool { + $list = is_array($slugs) ? $slugs : []; + + return in_array('prd.agent.role.manage', $list, true) + && in_array('prd.agent.user.manage', $list, true); + }); +}); + +test('sync fails with clear error when agent manage menu actions are missing', function (): void { + DB::table('admin_menu_actions') + ->whereIn('permission_code', ['agent.role.manage', 'agent.user.manage']) + ->delete(); + + $role = AdminRole::query()->create([ + 'slug' => 'agent_missing_catalog', + 'name' => 'Missing Catalog', + ]); + + expect(fn () => $role->syncLegacyPermissionSlugs(['prd.agent.role.manage'])) + ->toThrow(\Illuminate\Validation\ValidationException::class); +}); diff --git a/tests/Feature/AdminDrawRolePermissionRoundTripTest.php b/tests/Feature/AdminDrawRolePermissionRoundTripTest.php new file mode 100644 index 0000000..9746523 --- /dev/null +++ b/tests/Feature/AdminDrawRolePermissionRoundTripTest.php @@ -0,0 +1,60 @@ +artisan('lottery:admin-auth-sync')->assertExitCode(0); +}); + +test('role with only draw view slug does not round-trip as manage', function (): void { + $role = AdminRole::query()->create([ + 'slug' => 'draw_view_only', + 'name' => 'Draw view only', + ]); + $role->syncLegacyPermissionSlugs(['prd.draw_result.view']); + + expect($role->legacyPermissionSlugs()) + ->toContain('prd.draw_result.view') + ->not->toContain('prd.draw_result.manage'); +}); + +test('platform role sync can drop draw manage while keeping view', function (): void { + $super = AdminUser::query()->create([ + 'username' => 'draw_perm_super', + 'name' => 'Super', + 'email' => null, + 'password' => Hash::make('secret-strong'), + 'status' => 0, + ]); + grantSuperAdminRole($super); + $token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken; + + $role = AdminRole::query()->create([ + 'slug' => 'draw_toggle_test', + 'name' => 'Draw toggle test', + ]); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->putJson('/api/v1/admin/admin-roles/'.$role->id.'/permissions', [ + 'permission_slugs' => ['prd.draw_result.view', 'prd.draw_result.manage'], + ]) + ->assertOk() + ->assertJsonPath('data.permission_slugs', fn ($slugs): bool => in_array('prd.draw_result.manage', $slugs, true)); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->putJson('/api/v1/admin/admin-roles/'.$role->id.'/permissions', [ + 'permission_slugs' => ['prd.draw_result.view'], + ]) + ->assertOk() + ->assertJsonPath('data.permission_slugs', function ($slugs): bool { + $list = is_array($slugs) ? $slugs : []; + + return in_array('prd.draw_result.view', $list, true) + && ! in_array('prd.draw_result.manage', $list, true); + }); +}); diff --git a/tests/Pest.php b/tests/Pest.php index 46aae35..5bf9b9b 100644 --- a/tests/Pest.php +++ b/tests/Pest.php @@ -20,6 +20,8 @@ pest()->extend(TestCase::class) // ->use(RefreshDatabase::class) ->in('Feature'); +pest()->extend(TestCase::class)->in('Unit'); + /* |-------------------------------------------------------------------------- | Expectations diff --git a/tests/Unit/ApiValidationErrorsTest.php b/tests/Unit/ApiValidationErrorsTest.php new file mode 100644 index 0000000..29a72a0 --- /dev/null +++ b/tests/Unit/ApiValidationErrorsTest.php @@ -0,0 +1,52 @@ + ['The code field format is invalid.']], + 'zh', + ); + + expect($errors['code'][0])->toContain('编码只能使用字母、数字'); +}); + +test('normalizes business shorthand unique in zh', function (): void { + $errors = ApiValidationErrors::normalize(['code' => ['unique']], 'zh'); + + expect($errors['code'][0])->toBe('该内容已存在,请更换后重试。'); +}); + +test('summary joins multiple field errors', function (): void { + $normalized = [ + 'code' => ['编码格式不正确。'], + 'name' => ['名称不能为空。'], + ]; + + expect(ApiValidationErrors::summary($normalized))->toBe('编码格式不正确。;名称不能为空。'); +}); + +test('keeps already localized chinese messages', function (): void { + $message = '最小下注额不能小于 0'; + $errors = ApiValidationErrors::normalize(['items.0.min_bet_amount' => [$message]], 'zh'); + + expect($errors['items.0.min_bet_amount'][0])->toBe($message); +}); + +test('normalizes english min length for password in zh', function (): void { + $errors = ApiValidationErrors::normalize( + ['password' => ['The password field must be at least 8 characters.']], + 'zh', + ); + + expect($errors['password'][0])->toBe('密码至少需要 8 个字符。'); +}); + +test('normalizes exact draw items message in zh', function (): void { + $errors = ApiValidationErrors::normalize( + ['items' => ['items must contain the complete 23 draw prize slots.']], + 'zh', + ); + + expect($errors['items'][0])->toContain('23'); +});