From 5d2dbdbe1d6a3b230637bdbaf3c85b07db57a48e Mon Sep 17 00:00:00 2001 From: kang Date: Wed, 13 May 2026 11:36:24 +0800 Subject: [PATCH] =?UTF-8?q?refactor=EF=BC=9A=E7=94=A8=20AdminApiList=20?= =?UTF-8?q?=E7=BB=9F=E4=B8=80=E5=90=8E=E5=8F=B0=E5=88=97=E8=A1=A8=E7=B1=BB?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E7=9A=84=E5=93=8D=E5=BA=94=E6=A0=BC=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Admin/Audit/AuditLogIndexController.php | 17 +-- .../Config/OddsVersionIndexController.php | 16 +-- .../PlayConfigVersionIndexController.php | 16 +-- .../Config/RiskCapVersionIndexController.php | 16 +-- .../Admin/Draw/AdminDrawIndexController.php | 19 +-- ...dminJackpotContributionIndexController.php | 39 +++--- .../AdminJackpotPayoutLogIndexController.php | 37 ++---- .../ReconcileItemIndexController.php | 32 ++--- .../Reconcile/ReconcileJobIndexController.php | 17 +-- .../Reports/ReportJobIndexController.php | 17 +-- .../Risk/AdminRiskPoolIndexController.php | 15 +-- .../AdminRiskPoolLockLogIndexController.php | 15 +-- .../Risk/AdminRiskPoolShowController.php | 15 +-- .../AdminSettlementBatchDetailsController.php | 54 ++++---- .../AdminSettlementBatchIndexController.php | 17 +-- .../Admin/User/AdminUserDestroyController.php | 64 +++++++++ .../Admin/User/AdminUserIndexController.php | 35 +---- .../User/AdminUserRoleSyncController.php | 25 +--- .../V1/Admin/User/AdminUserShowController.php | 20 +++ .../Admin/User/AdminUserStoreController.php | 75 +++++++++++ .../Admin/User/AdminUserUpdateController.php | 77 +++++++++++ app/Models/AdminUser.php | 32 +++++ app/Services/Config/OddsStreamService.php | 4 +- .../Config/PlayConfigStreamService.php | 4 +- app/Services/Config/RiskCapStreamService.php | 4 +- app/Support/AdminApiList.php | 75 +++++++++++ app/Support/AdminUserApiPresenter.php | 26 ++++ routes/api.php | 8 ++ tests/Feature/AdminUserPermissionApiTest.php | 124 ++++++++++++++++++ 29 files changed, 622 insertions(+), 293 deletions(-) create mode 100644 app/Http/Controllers/Api/V1/Admin/User/AdminUserDestroyController.php create mode 100644 app/Http/Controllers/Api/V1/Admin/User/AdminUserShowController.php create mode 100644 app/Http/Controllers/Api/V1/Admin/User/AdminUserStoreController.php create mode 100644 app/Http/Controllers/Api/V1/Admin/User/AdminUserUpdateController.php create mode 100644 app/Support/AdminApiList.php create mode 100644 app/Support/AdminUserApiPresenter.php diff --git a/app/Http/Controllers/Api/V1/Admin/Audit/AuditLogIndexController.php b/app/Http/Controllers/Api/V1/Admin/Audit/AuditLogIndexController.php index 0161f9f..e7a48ff 100644 --- a/app/Http/Controllers/Api/V1/Admin/Audit/AuditLogIndexController.php +++ b/app/Http/Controllers/Api/V1/Admin/Audit/AuditLogIndexController.php @@ -4,7 +4,7 @@ namespace App\Http\Controllers\Api\V1\Admin\Audit; use App\Http\Controllers\Controller; use App\Models\AuditLog; -use App\Support\ApiResponse; +use App\Support\AdminApiList; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -15,8 +15,7 @@ final class AuditLogIndexController extends Controller { public function __invoke(Request $request): JsonResponse { - $perPage = min(max((int) $request->integer('per_page', 25), 1), 100); - $page = max((int) $request->integer('page', 1), 1); + $p = AdminApiList::readPaging($request); $module = trim((string) $request->query('module_code', '')); $action = trim((string) $request->query('action_code', '')); $operatorType = trim((string) $request->query('operator_type', '')); @@ -33,17 +32,9 @@ final class AuditLogIndexController extends Controller $q->where('operator_type', $operatorType); } - $paginator = $q->paginate($perPage, ['*'], 'page', $page); + $paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']); - return ApiResponse::success([ - 'items' => collect($paginator->items())->map(fn (AuditLog $r) => $this->row($r))->all(), - 'meta' => [ - 'current_page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), - 'total' => $paginator->total(), - 'last_page' => $paginator->lastPage(), - ], - ]); + return AdminApiList::json($paginator, fn (AuditLog $r) => $this->row($r)); } /** @return array */ diff --git a/app/Http/Controllers/Api/V1/Admin/Config/OddsVersionIndexController.php b/app/Http/Controllers/Api/V1/Admin/Config/OddsVersionIndexController.php index f19187b..1c509fe 100644 --- a/app/Http/Controllers/Api/V1/Admin/Config/OddsVersionIndexController.php +++ b/app/Http/Controllers/Api/V1/Admin/Config/OddsVersionIndexController.php @@ -5,8 +5,8 @@ namespace App\Http\Controllers\Api\V1\Admin\Config; use App\Http\Controllers\Controller; use App\Models\OddsVersion; use App\Services\Config\OddsStreamService; +use App\Support\AdminApiList; use App\Support\AdminConfigPresenter; -use App\Support\ApiResponse; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -15,19 +15,11 @@ final class OddsVersionIndexController extends Controller { public function __invoke(Request $request, OddsStreamService $service): JsonResponse { - $perPage = min(max((int) $request->integer('per_page', 25), 1), 100); + $p = AdminApiList::readPaging($request); $status = trim((string) $request->query('status', '')); - $paginator = $service->paginate($status === '' ? null : $status, $perPage); + $paginator = $service->paginate($status === '' ? null : $status, $p['perPage'], $p['page']); - return ApiResponse::success([ - 'items' => collect($paginator->items())->map(fn (OddsVersion $v) => AdminConfigPresenter::oddsVersionSummary($v))->all(), - 'meta' => [ - 'current_page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), - 'total' => $paginator->total(), - 'last_page' => $paginator->lastPage(), - ], - ]); + return AdminApiList::json($paginator, fn (OddsVersion $v) => AdminConfigPresenter::oddsVersionSummary($v)); } } diff --git a/app/Http/Controllers/Api/V1/Admin/Config/PlayConfigVersionIndexController.php b/app/Http/Controllers/Api/V1/Admin/Config/PlayConfigVersionIndexController.php index 586484d..67b82a0 100644 --- a/app/Http/Controllers/Api/V1/Admin/Config/PlayConfigVersionIndexController.php +++ b/app/Http/Controllers/Api/V1/Admin/Config/PlayConfigVersionIndexController.php @@ -5,8 +5,8 @@ namespace App\Http\Controllers\Api\V1\Admin\Config; use App\Http\Controllers\Controller; use App\Models\PlayConfigVersion; use App\Services\Config\PlayConfigStreamService; +use App\Support\AdminApiList; use App\Support\AdminConfigPresenter; -use App\Support\ApiResponse; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -15,19 +15,11 @@ final class PlayConfigVersionIndexController extends Controller { public function __invoke(Request $request, PlayConfigStreamService $service): JsonResponse { - $perPage = min(max((int) $request->integer('per_page', 25), 1), 100); + $p = AdminApiList::readPaging($request); $status = trim((string) $request->query('status', '')); - $paginator = $service->paginate($status === '' ? null : $status, $perPage); + $paginator = $service->paginate($status === '' ? null : $status, $p['perPage'], $p['page']); - return ApiResponse::success([ - 'items' => collect($paginator->items())->map(fn (PlayConfigVersion $v) => AdminConfigPresenter::playConfigVersionSummary($v))->all(), - 'meta' => [ - 'current_page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), - 'total' => $paginator->total(), - 'last_page' => $paginator->lastPage(), - ], - ]); + return AdminApiList::json($paginator, fn (PlayConfigVersion $v) => AdminConfigPresenter::playConfigVersionSummary($v)); } } diff --git a/app/Http/Controllers/Api/V1/Admin/Config/RiskCapVersionIndexController.php b/app/Http/Controllers/Api/V1/Admin/Config/RiskCapVersionIndexController.php index 3eb9a90..a32d3eb 100644 --- a/app/Http/Controllers/Api/V1/Admin/Config/RiskCapVersionIndexController.php +++ b/app/Http/Controllers/Api/V1/Admin/Config/RiskCapVersionIndexController.php @@ -5,8 +5,8 @@ namespace App\Http\Controllers\Api\V1\Admin\Config; use App\Http\Controllers\Controller; use App\Models\RiskCapVersion; use App\Services\Config\RiskCapStreamService; +use App\Support\AdminApiList; use App\Support\AdminConfigPresenter; -use App\Support\ApiResponse; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -15,19 +15,11 @@ final class RiskCapVersionIndexController extends Controller { public function __invoke(Request $request, RiskCapStreamService $service): JsonResponse { - $perPage = min(max((int) $request->integer('per_page', 25), 1), 100); + $p = AdminApiList::readPaging($request); $status = trim((string) $request->query('status', '')); - $paginator = $service->paginate($status === '' ? null : $status, $perPage); + $paginator = $service->paginate($status === '' ? null : $status, $p['perPage'], $p['page']); - return ApiResponse::success([ - 'items' => collect($paginator->items())->map(fn (RiskCapVersion $v) => AdminConfigPresenter::riskCapVersionSummary($v))->all(), - 'meta' => [ - 'current_page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), - 'total' => $paginator->total(), - 'last_page' => $paginator->lastPage(), - ], - ]); + return AdminApiList::json($paginator, fn (RiskCapVersion $v) => AdminConfigPresenter::riskCapVersionSummary($v)); } } diff --git a/app/Http/Controllers/Api/V1/Admin/Draw/AdminDrawIndexController.php b/app/Http/Controllers/Api/V1/Admin/Draw/AdminDrawIndexController.php index edc58c9..4cc2f05 100644 --- a/app/Http/Controllers/Api/V1/Admin/Draw/AdminDrawIndexController.php +++ b/app/Http/Controllers/Api/V1/Admin/Draw/AdminDrawIndexController.php @@ -4,8 +4,9 @@ namespace App\Http\Controllers\Api\V1\Admin\Draw; use App\Http\Controllers\Controller; use App\Models\Draw; -use App\Support\ApiResponse; +use App\Support\AdminApiList; use Carbon\Carbon; +use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -16,7 +17,7 @@ final class AdminDrawIndexController extends Controller { public function __invoke(Request $request): JsonResponse { - $perPage = min(max((int) $request->integer('per_page', 25), 1), 100); + $p = AdminApiList::readPaging($request); $drawNo = trim((string) $request->query('draw_no', '')); $status = trim((string) $request->query('status', '')); @@ -30,18 +31,10 @@ final class AdminDrawIndexController extends Controller $q->where('status', $status); } - /** @var \Illuminate\Contracts\Pagination\LengthAwarePaginator $paginator */ - $paginator = $q->paginate($perPage); + /** @var LengthAwarePaginator $paginator */ + $paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']); - return ApiResponse::success([ - 'items' => collect($paginator->items())->map(fn (Draw $row) => $this->row($row))->all(), - 'meta' => [ - 'current_page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), - 'total' => $paginator->total(), - 'last_page' => $paginator->lastPage(), - ], - ]); + return AdminApiList::json($paginator, fn (Draw $row) => $this->row($row)); } /** @return array */ diff --git a/app/Http/Controllers/Api/V1/Admin/Jackpot/AdminJackpotContributionIndexController.php b/app/Http/Controllers/Api/V1/Admin/Jackpot/AdminJackpotContributionIndexController.php index c04e208..a5ddb60 100644 --- a/app/Http/Controllers/Api/V1/Admin/Jackpot/AdminJackpotContributionIndexController.php +++ b/app/Http/Controllers/Api/V1/Admin/Jackpot/AdminJackpotContributionIndexController.php @@ -4,7 +4,7 @@ namespace App\Http\Controllers\Api\V1\Admin\Jackpot; use App\Http\Controllers\Controller; use App\Models\JackpotContribution; -use App\Support\ApiResponse; +use App\Support\AdminApiList; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -15,8 +15,7 @@ final class AdminJackpotContributionIndexController extends Controller { public function __invoke(Request $request): JsonResponse { - $perPage = min(max((int) $request->integer('per_page', 25), 1), 100); - $page = max((int) $request->integer('page', 1), 1); + $p = AdminApiList::readPaging($request); $drawNo = trim((string) $request->query('draw_no', '')); $q = JackpotContribution::query() @@ -27,28 +26,20 @@ final class AdminJackpotContributionIndexController extends Controller $q->whereHas('draw', fn ($d) => $d->where('draw_no', 'like', '%'.$drawNo.'%')); } - $paginator = $q->paginate($perPage, ['*'], 'page', $page); + $paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']); - return ApiResponse::success([ - 'items' => collect($paginator->items())->map(fn (JackpotContribution $r) => [ - 'id' => (int) $r->id, - 'draw_id' => (int) $r->draw_id, - 'draw_no' => $r->draw?->draw_no, - 'jackpot_pool_id' => (int) $r->jackpot_pool_id, - 'currency_code' => $r->pool?->currency_code, - 'player_id' => (int) $r->player_id, - 'player_username' => $r->player?->username, - 'ticket_item_id' => $r->ticket_item_id !== null ? (int) $r->ticket_item_id : null, - 'ticket_no' => $r->ticketItem?->ticket_no, - 'contribution_amount' => (int) $r->contribution_amount, - 'created_at' => $r->created_at?->toIso8601String(), - ])->all(), - 'meta' => [ - 'current_page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), - 'total' => $paginator->total(), - 'last_page' => $paginator->lastPage(), - ], + return AdminApiList::json($paginator, fn (JackpotContribution $r) => [ + 'id' => (int) $r->id, + 'draw_id' => (int) $r->draw_id, + 'draw_no' => $r->draw?->draw_no, + 'jackpot_pool_id' => (int) $r->jackpot_pool_id, + 'currency_code' => $r->pool?->currency_code, + 'player_id' => (int) $r->player_id, + 'player_username' => $r->player?->username, + 'ticket_item_id' => $r->ticket_item_id !== null ? (int) $r->ticket_item_id : null, + 'ticket_no' => $r->ticketItem?->ticket_no, + 'contribution_amount' => (int) $r->contribution_amount, + 'created_at' => $r->created_at?->toIso8601String(), ]); } } diff --git a/app/Http/Controllers/Api/V1/Admin/Jackpot/AdminJackpotPayoutLogIndexController.php b/app/Http/Controllers/Api/V1/Admin/Jackpot/AdminJackpotPayoutLogIndexController.php index bc82f61..29fb6e3 100644 --- a/app/Http/Controllers/Api/V1/Admin/Jackpot/AdminJackpotPayoutLogIndexController.php +++ b/app/Http/Controllers/Api/V1/Admin/Jackpot/AdminJackpotPayoutLogIndexController.php @@ -4,7 +4,7 @@ namespace App\Http\Controllers\Api\V1\Admin\Jackpot; use App\Http\Controllers\Controller; use App\Models\JackpotPayoutLog; -use App\Support\ApiResponse; +use App\Support\AdminApiList; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -15,8 +15,7 @@ final class AdminJackpotPayoutLogIndexController extends Controller { public function __invoke(Request $request): JsonResponse { - $perPage = min(max((int) $request->integer('per_page', 25), 1), 100); - $page = max((int) $request->integer('page', 1), 1); + $p = AdminApiList::readPaging($request); $drawNo = trim((string) $request->query('draw_no', '')); $q = JackpotPayoutLog::query() @@ -27,27 +26,19 @@ final class AdminJackpotPayoutLogIndexController extends Controller $q->whereHas('draw', fn ($d) => $d->where('draw_no', 'like', '%'.$drawNo.'%')); } - $paginator = $q->paginate($perPage, ['*'], 'page', $page); + $paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']); - return ApiResponse::success([ - 'items' => collect($paginator->items())->map(fn (JackpotPayoutLog $r) => [ - 'id' => (int) $r->id, - 'draw_id' => (int) $r->draw_id, - 'draw_no' => $r->draw?->draw_no, - 'jackpot_pool_id' => (int) $r->jackpot_pool_id, - 'currency_code' => $r->pool?->currency_code, - 'trigger_type' => $r->trigger_type, - 'total_payout_amount' => (int) $r->total_payout_amount, - 'winner_count' => (int) $r->winner_count, - 'trigger_snapshot_json' => $r->trigger_snapshot_json, - 'created_at' => $r->created_at?->toIso8601String(), - ])->all(), - 'meta' => [ - 'current_page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), - 'total' => $paginator->total(), - 'last_page' => $paginator->lastPage(), - ], + return AdminApiList::json($paginator, fn (JackpotPayoutLog $r) => [ + 'id' => (int) $r->id, + 'draw_id' => (int) $r->draw_id, + 'draw_no' => $r->draw?->draw_no, + 'jackpot_pool_id' => (int) $r->jackpot_pool_id, + 'currency_code' => $r->pool?->currency_code, + 'trigger_type' => $r->trigger_type, + 'total_payout_amount' => (int) $r->total_payout_amount, + 'winner_count' => (int) $r->winner_count, + 'trigger_snapshot_json' => $r->trigger_snapshot_json, + 'created_at' => $r->created_at?->toIso8601String(), ]); } } diff --git a/app/Http/Controllers/Api/V1/Admin/Reconcile/ReconcileItemIndexController.php b/app/Http/Controllers/Api/V1/Admin/Reconcile/ReconcileItemIndexController.php index ca7a507..2f67383 100644 --- a/app/Http/Controllers/Api/V1/Admin/Reconcile/ReconcileItemIndexController.php +++ b/app/Http/Controllers/Api/V1/Admin/Reconcile/ReconcileItemIndexController.php @@ -5,7 +5,7 @@ namespace App\Http\Controllers\Api\V1\Admin\Reconcile; use App\Http\Controllers\Controller; use App\Models\ReconcileItem; use App\Models\ReconcileJob; -use App\Support\ApiResponse; +use App\Support\AdminApiList; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -14,31 +14,23 @@ final class ReconcileItemIndexController extends Controller { public function __invoke(Request $request, ReconcileJob $reconcile_job): JsonResponse { - $perPage = min(max((int) $request->integer('per_page', 50), 1), 200); - $page = max((int) $request->integer('page', 1), 1); + $p = AdminApiList::readPaging($request, 50, 200); $paginator = $reconcile_job->items() ->orderBy('id') - ->paginate($perPage, ['*'], 'page', $page); + ->paginate($p['perPage'], ['*'], 'page', $p['page']); - return ApiResponse::success([ + return AdminApiList::jsonWith($paginator, fn (ReconcileItem $r) => [ + 'id' => (int) $r->id, + 'side_a_ref' => $r->side_a_ref, + 'side_b_ref' => $r->side_b_ref, + 'difference_amount' => (int) $r->difference_amount, + 'status' => $r->status, + 'resolved_at' => $r->resolved_at?->toIso8601String(), + 'created_at' => $r->created_at?->toIso8601String(), + ], [ 'job_id' => (int) $reconcile_job->id, 'job_no' => $reconcile_job->job_no, - 'items' => collect($paginator->items())->map(fn (ReconcileItem $r) => [ - 'id' => (int) $r->id, - 'side_a_ref' => $r->side_a_ref, - 'side_b_ref' => $r->side_b_ref, - 'difference_amount' => (int) $r->difference_amount, - 'status' => $r->status, - 'resolved_at' => $r->resolved_at?->toIso8601String(), - 'created_at' => $r->created_at?->toIso8601String(), - ])->all(), - 'meta' => [ - 'current_page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), - 'total' => $paginator->total(), - 'last_page' => $paginator->lastPage(), - ], ]); } } diff --git a/app/Http/Controllers/Api/V1/Admin/Reconcile/ReconcileJobIndexController.php b/app/Http/Controllers/Api/V1/Admin/Reconcile/ReconcileJobIndexController.php index 1ac6077..40fa5ea 100644 --- a/app/Http/Controllers/Api/V1/Admin/Reconcile/ReconcileJobIndexController.php +++ b/app/Http/Controllers/Api/V1/Admin/Reconcile/ReconcileJobIndexController.php @@ -4,7 +4,7 @@ namespace App\Http\Controllers\Api\V1\Admin\Reconcile; use App\Http\Controllers\Controller; use App\Models\ReconcileJob; -use App\Support\ApiResponse; +use App\Support\AdminApiList; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -13,8 +13,7 @@ final class ReconcileJobIndexController extends Controller { public function __invoke(Request $request): JsonResponse { - $perPage = min(max((int) $request->integer('per_page', 25), 1), 100); - $page = max((int) $request->integer('page', 1), 1); + $p = AdminApiList::readPaging($request); $type = trim((string) $request->query('reconcile_type', '')); $q = ReconcileJob::query()->orderByDesc('id'); @@ -22,17 +21,9 @@ final class ReconcileJobIndexController extends Controller $q->where('reconcile_type', $type); } - $paginator = $q->paginate($perPage, ['*'], 'page', $page); + $paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']); - return ApiResponse::success([ - 'items' => collect($paginator->items())->map(fn (ReconcileJob $j) => $this->row($j))->all(), - 'meta' => [ - 'current_page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), - 'total' => $paginator->total(), - 'last_page' => $paginator->lastPage(), - ], - ]); + return AdminApiList::json($paginator, fn (ReconcileJob $j) => $this->row($j)); } /** @return array */ diff --git a/app/Http/Controllers/Api/V1/Admin/Reports/ReportJobIndexController.php b/app/Http/Controllers/Api/V1/Admin/Reports/ReportJobIndexController.php index 29a97f6..6112c90 100644 --- a/app/Http/Controllers/Api/V1/Admin/Reports/ReportJobIndexController.php +++ b/app/Http/Controllers/Api/V1/Admin/Reports/ReportJobIndexController.php @@ -4,7 +4,7 @@ namespace App\Http\Controllers\Api\V1\Admin\Reports; use App\Http\Controllers\Controller; use App\Models\ReportJob; -use App\Support\ApiResponse; +use App\Support\AdminApiList; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -13,22 +13,13 @@ final class ReportJobIndexController extends Controller { public function __invoke(Request $request): JsonResponse { - $perPage = min(max((int) $request->integer('per_page', 25), 1), 100); - $page = max((int) $request->integer('page', 1), 1); + $p = AdminApiList::readPaging($request); $paginator = ReportJob::query() ->orderByDesc('id') - ->paginate($perPage, ['*'], 'page', $page); + ->paginate($p['perPage'], ['*'], 'page', $p['page']); - return ApiResponse::success([ - 'items' => collect($paginator->items())->map(fn (ReportJob $j) => $this->row($j))->all(), - 'meta' => [ - 'current_page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), - 'total' => $paginator->total(), - 'last_page' => $paginator->lastPage(), - ], - ]); + return AdminApiList::json($paginator, fn (ReportJob $j) => $this->row($j)); } /** @return array */ diff --git a/app/Http/Controllers/Api/V1/Admin/Risk/AdminRiskPoolIndexController.php b/app/Http/Controllers/Api/V1/Admin/Risk/AdminRiskPoolIndexController.php index 4ce3398..4f71647 100644 --- a/app/Http/Controllers/Api/V1/Admin/Risk/AdminRiskPoolIndexController.php +++ b/app/Http/Controllers/Api/V1/Admin/Risk/AdminRiskPoolIndexController.php @@ -5,7 +5,7 @@ namespace App\Http\Controllers\Api\V1\Admin\Risk; use App\Http\Controllers\Controller; use App\Models\Draw; use App\Models\RiskPool; -use App\Support\ApiResponse; +use App\Support\AdminApiList; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -19,7 +19,7 @@ final class AdminRiskPoolIndexController extends Controller { public function __invoke(Request $request, Draw $draw): JsonResponse { - $perPage = min(max((int) $request->integer('per_page', 25), 1), 100); + $p = AdminApiList::readPaging($request); $soldOutOnly = $request->boolean('sold_out_only'); $sort = trim((string) $request->query('sort', 'usage_desc')); @@ -39,18 +39,11 @@ final class AdminRiskPoolIndexController extends Controller }; /** @var LengthAwarePaginator $paginator */ - $paginator = $q->paginate($perPage); + $paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']); - return ApiResponse::success([ + return AdminApiList::jsonWith($paginator, fn (RiskPool $row) => $this->row($row), [ 'draw_id' => (int) $draw->id, 'draw_no' => $draw->draw_no, - 'items' => collect($paginator->items())->map(fn (RiskPool $row) => $this->row($row))->all(), - 'meta' => [ - 'current_page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), - 'total' => $paginator->total(), - 'last_page' => $paginator->lastPage(), - ], ]); } diff --git a/app/Http/Controllers/Api/V1/Admin/Risk/AdminRiskPoolLockLogIndexController.php b/app/Http/Controllers/Api/V1/Admin/Risk/AdminRiskPoolLockLogIndexController.php index 875db77..e5c2b9c 100644 --- a/app/Http/Controllers/Api/V1/Admin/Risk/AdminRiskPoolLockLogIndexController.php +++ b/app/Http/Controllers/Api/V1/Admin/Risk/AdminRiskPoolLockLogIndexController.php @@ -5,7 +5,7 @@ namespace App\Http\Controllers\Api\V1\Admin\Risk; use App\Http\Controllers\Controller; use App\Models\Draw; use App\Models\RiskPoolLockLog; -use App\Support\ApiResponse; +use App\Support\AdminApiList; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -17,7 +17,7 @@ final class AdminRiskPoolLockLogIndexController extends Controller { public function __invoke(Request $request, Draw $draw): JsonResponse { - $perPage = min(max((int) $request->integer('per_page', 25), 1), 100); + $p = AdminApiList::readPaging($request); $action = trim((string) $request->query('action_type', '')); $number = trim((string) $request->query('normalized_number', '')); @@ -36,18 +36,11 @@ final class AdminRiskPoolLockLogIndexController extends Controller } /** @var LengthAwarePaginator $paginator */ - $paginator = $q->paginate($perPage); + $paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']); - return ApiResponse::success([ + return AdminApiList::jsonWith($paginator, fn (RiskPoolLockLog $log) => $this->row($log), [ 'draw_id' => (int) $draw->id, 'draw_no' => $draw->draw_no, - 'items' => collect($paginator->items())->map(fn (RiskPoolLockLog $log) => $this->row($log))->all(), - 'meta' => [ - 'current_page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), - 'total' => $paginator->total(), - 'last_page' => $paginator->lastPage(), - ], ]); } diff --git a/app/Http/Controllers/Api/V1/Admin/Risk/AdminRiskPoolShowController.php b/app/Http/Controllers/Api/V1/Admin/Risk/AdminRiskPoolShowController.php index 43a47c6..a7dbdab 100644 --- a/app/Http/Controllers/Api/V1/Admin/Risk/AdminRiskPoolShowController.php +++ b/app/Http/Controllers/Api/V1/Admin/Risk/AdminRiskPoolShowController.php @@ -7,6 +7,7 @@ use App\Lottery\ErrorCode; use App\Models\Draw; use App\Models\RiskPool; use App\Models\RiskPoolLockLog; +use App\Support\AdminApiList; use App\Support\ApiResponse; use Illuminate\Contracts\Pagination\LengthAwarePaginator; use Illuminate\Http\JsonResponse; @@ -32,7 +33,7 @@ final class AdminRiskPoolShowController extends Controller return ApiResponse::error('该期尚无此号码的风险池记录', ErrorCode::NotFound->value, null, 404); } - $perPage = min(max((int) $request->integer('per_page', 20), 1), 100); + $p = AdminApiList::readPaging($request, 20, AdminApiList::MAX_PER_PAGE); /** @var LengthAwarePaginator $paginator */ $paginator = RiskPoolLockLog::query() @@ -41,7 +42,7 @@ final class AdminRiskPoolShowController extends Controller ->with(['ticketItem:id,ticket_no,play_code,player_id']) ->orderByDesc('created_at') ->orderByDesc('id') - ->paginate($perPage); + ->paginate($p['perPage'], ['*'], 'page', $p['page']); $cap = (int) $pool->total_cap_amount; $locked = (int) $pool->locked_amount; @@ -59,15 +60,7 @@ final class AdminRiskPoolShowController extends Controller 'usage_ratio' => $cap > 0 ? round($locked / $cap, 6) : null, 'version' => (int) $pool->version, ], - 'logs' => [ - 'items' => collect($paginator->items())->map(fn (RiskPoolLockLog $log) => $this->logRow($log))->all(), - 'meta' => [ - 'current_page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), - 'total' => $paginator->total(), - 'last_page' => $paginator->lastPage(), - ], - ], + 'logs' => AdminApiList::payload($paginator, fn (RiskPoolLockLog $log) => $this->logRow($log)), ]); } diff --git a/app/Http/Controllers/Api/V1/Admin/Settlement/AdminSettlementBatchDetailsController.php b/app/Http/Controllers/Api/V1/Admin/Settlement/AdminSettlementBatchDetailsController.php index 06dc2d3..4cdaec7 100644 --- a/app/Http/Controllers/Api/V1/Admin/Settlement/AdminSettlementBatchDetailsController.php +++ b/app/Http/Controllers/Api/V1/Admin/Settlement/AdminSettlementBatchDetailsController.php @@ -5,7 +5,7 @@ namespace App\Http\Controllers\Api\V1\Admin\Settlement; use App\Http\Controllers\Controller; use App\Models\SettlementBatch; use App\Models\TicketSettlementDetail; -use App\Support\ApiResponse; +use App\Support\AdminApiList; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -16,8 +16,7 @@ final class AdminSettlementBatchDetailsController extends Controller { public function __invoke(Request $request, SettlementBatch $batch): JsonResponse { - $perPage = min(max((int) $request->integer('per_page', 25), 1), 100); - $page = max((int) $request->integer('page', 1), 1); + $p = AdminApiList::readPaging($request); $paginator = TicketSettlementDetail::query() ->where('settlement_batch_id', $batch->id) @@ -26,36 +25,27 @@ final class AdminSettlementBatchDetailsController extends Controller 'ticketItem.player:id,username,site_player_id', ]) ->orderBy('id') - ->paginate($perPage, ['*'], 'page', $page); + ->paginate($p['perPage'], ['*'], 'page', $p['page']); - return ApiResponse::success([ - 'batch_id' => (int) $batch->id, - 'items' => collect($paginator->items())->map(function ($row) { - /** @var TicketSettlementDetail $row */ - $item = $row->ticketItem; - $player = $item?->player; + return AdminApiList::jsonWith($paginator, function ($row) { + /** @var TicketSettlementDetail $row */ + $item = $row->ticketItem; + $player = $item?->player; - return [ - 'id' => (int) $row->id, - 'ticket_item_id' => (int) $row->ticket_item_id, - 'ticket_no' => $item?->ticket_no, - 'play_code' => $item?->play_code, - 'player_id' => $item?->player_id, - 'player_username' => $player?->username, - 'site_player_id' => $player?->site_player_id, - 'matched_prize_tier' => $row->matched_prize_tier, - 'win_amount' => (int) $row->win_amount, - 'jackpot_allocation_amount' => (int) $row->jackpot_allocation_amount, - 'match_detail_json' => $row->match_detail_json, - 'created_at' => $row->created_at?->toIso8601String(), - ]; - })->all(), - 'meta' => [ - 'current_page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), - 'total' => $paginator->total(), - 'last_page' => $paginator->lastPage(), - ], - ]); + return [ + 'id' => (int) $row->id, + 'ticket_item_id' => (int) $row->ticket_item_id, + 'ticket_no' => $item?->ticket_no, + 'play_code' => $item?->play_code, + 'player_id' => $item?->player_id, + 'player_username' => $player?->username, + 'site_player_id' => $player?->site_player_id, + 'matched_prize_tier' => $row->matched_prize_tier, + 'win_amount' => (int) $row->win_amount, + 'jackpot_allocation_amount' => (int) $row->jackpot_allocation_amount, + 'match_detail_json' => $row->match_detail_json, + 'created_at' => $row->created_at?->toIso8601String(), + ]; + }, ['batch_id' => (int) $batch->id]); } } diff --git a/app/Http/Controllers/Api/V1/Admin/Settlement/AdminSettlementBatchIndexController.php b/app/Http/Controllers/Api/V1/Admin/Settlement/AdminSettlementBatchIndexController.php index eb806b2..8dfd3c6 100644 --- a/app/Http/Controllers/Api/V1/Admin/Settlement/AdminSettlementBatchIndexController.php +++ b/app/Http/Controllers/Api/V1/Admin/Settlement/AdminSettlementBatchIndexController.php @@ -4,7 +4,7 @@ namespace App\Http\Controllers\Api\V1\Admin\Settlement; use App\Http\Controllers\Controller; use App\Models\SettlementBatch; -use App\Support\ApiResponse; +use App\Support\AdminApiList; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -15,8 +15,7 @@ final class AdminSettlementBatchIndexController extends Controller { public function __invoke(Request $request): JsonResponse { - $perPage = min(max((int) $request->integer('per_page', 25), 1), 100); - $page = max((int) $request->integer('page', 1), 1); + $p = AdminApiList::readPaging($request); $drawNo = trim((string) $request->query('draw_no', '')); $status = trim((string) $request->query('status', '')); @@ -32,17 +31,9 @@ final class AdminSettlementBatchIndexController extends Controller $q->where('status', $status); } - $paginator = $q->paginate($perPage, ['*'], 'page', $page); + $paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']); - return ApiResponse::success([ - 'items' => collect($paginator->items())->map(fn (SettlementBatch $b) => $this->row($b))->all(), - 'meta' => [ - 'current_page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), - 'total' => $paginator->total(), - 'last_page' => $paginator->lastPage(), - ], - ]); + return AdminApiList::json($paginator, fn (SettlementBatch $b) => $this->row($b)); } /** @return array */ diff --git a/app/Http/Controllers/Api/V1/Admin/User/AdminUserDestroyController.php b/app/Http/Controllers/Api/V1/Admin/User/AdminUserDestroyController.php new file mode 100644 index 0000000..22b230e --- /dev/null +++ b/app/Http/Controllers/Api/V1/Admin/User/AdminUserDestroyController.php @@ -0,0 +1,64 @@ +lotteryAdmin(); + + if ((int) $actor->getKey() === (int) $admin_user->getKey()) { + return ApiResponse::error( + '不能删除当前登录账号', + ErrorCode::ValidationFailed->value, + null, + 422, + ); + } + + $admin_user->load('roles'); + if ($admin_user->isSuperAdmin()) { + $hasOther = AdminUser::query() + ->whereKeyNot($admin_user->getKey()) + ->whereHas('roles', static fn ($q) => $q->where('admin_roles.slug', AdminUser::ROLE_SUPER_ADMIN)) + ->exists(); + if (! $hasOther) { + return ApiResponse::error( + '不能删除最后一个超级管理员', + ErrorCode::ValidationFailed->value, + null, + 422, + ); + } + } + + $before = AdminUserApiPresenter::listItem($admin_user); + $id = (int) $admin_user->id; + $admin_user->delete(); + + AuditLogger::recordForAdmin( + $actor, + $request, + 'system', + 'admin_user.delete', + 'admin_user', + (string) $id, + $before, + null, + ); + + return ApiResponse::success(['deleted' => true, 'id' => $id]); + } +} diff --git a/app/Http/Controllers/Api/V1/Admin/User/AdminUserIndexController.php b/app/Http/Controllers/Api/V1/Admin/User/AdminUserIndexController.php index 1495389..4c92503 100644 --- a/app/Http/Controllers/Api/V1/Admin/User/AdminUserIndexController.php +++ b/app/Http/Controllers/Api/V1/Admin/User/AdminUserIndexController.php @@ -4,7 +4,8 @@ namespace App\Http\Controllers\Api\V1\Admin\User; use App\Http\Controllers\Controller; use App\Models\AdminUser; -use App\Support\ApiResponse; +use App\Support\AdminApiList; +use App\Support\AdminUserApiPresenter; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; @@ -13,8 +14,7 @@ final class AdminUserIndexController extends Controller { public function __invoke(Request $request): JsonResponse { - $perPage = min(max((int) $request->integer('per_page', 25), 1), 100); - $page = max((int) $request->integer('page', 1), 1); + $p = AdminApiList::readPaging($request); $keyword = trim((string) $request->query('keyword', '')); $q = AdminUser::query() @@ -29,33 +29,8 @@ final class AdminUserIndexController extends Controller }); } - $paginator = $q->paginate($perPage, ['*'], 'page', $page); + $paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']); - return ApiResponse::success([ - 'items' => collect($paginator->items())->map( - fn (AdminUser $user): array => $this->row($user) - )->all(), - 'meta' => [ - 'current_page' => $paginator->currentPage(), - 'per_page' => $paginator->perPage(), - 'total' => $paginator->total(), - 'last_page' => $paginator->lastPage(), - ], - ]); - } - - /** @return array */ - private function row(AdminUser $user): array - { - return [ - 'id' => (int) $user->id, - 'username' => $user->username, - 'nickname' => $user->name, - 'email' => $user->email, - 'status' => (int) $user->status, - 'roles' => $user->adminRoleSlugs(), - 'direct_permissions' => $user->directLegacyPermissionSlugs(), - 'effective_permissions' => $user->adminPermissionSlugs(), - ]; + return AdminApiList::json($paginator, fn (AdminUser $user): array => AdminUserApiPresenter::listItem($user)); } } diff --git a/app/Http/Controllers/Api/V1/Admin/User/AdminUserRoleSyncController.php b/app/Http/Controllers/Api/V1/Admin/User/AdminUserRoleSyncController.php index 2114993..fcf2c65 100644 --- a/app/Http/Controllers/Api/V1/Admin/User/AdminUserRoleSyncController.php +++ b/app/Http/Controllers/Api/V1/Admin/User/AdminUserRoleSyncController.php @@ -7,7 +7,6 @@ use App\Models\AdminUser; use App\Support\ApiResponse; use Illuminate\Http\JsonResponse; use Illuminate\Http\Request; -use Illuminate\Support\Facades\DB; use Illuminate\Validation\Rule; /** PUT /api/v1/admin/admin-users/{admin_user}/roles */ @@ -22,29 +21,7 @@ final class AdminUserRoleSyncController extends Controller ])->validate(); $slugs = array_values(array_unique($data['role_slugs'])); - $siteId = AdminUser::defaultAdminSiteId(); - - $roleIds = DB::table('admin_roles') - ->whereIn('slug', $slugs) - ->pluck('id') - ->all(); - - DB::transaction(function () use ($admin_user, $siteId, $roleIds): void { - DB::table('admin_user_site_roles') - ->where('admin_user_id', $admin_user->id) - ->where('site_id', $siteId) - ->delete(); - - $now = now(); - foreach ($roleIds as $rid) { - DB::table('admin_user_site_roles')->insert([ - 'admin_user_id' => $admin_user->id, - 'site_id' => $siteId, - 'role_id' => (int) $rid, - 'granted_at' => $now, - ]); - } - }); + $admin_user->syncRoleSlugsForDefaultSite($slugs); $admin_user->load('roles'); diff --git a/app/Http/Controllers/Api/V1/Admin/User/AdminUserShowController.php b/app/Http/Controllers/Api/V1/Admin/User/AdminUserShowController.php new file mode 100644 index 0000000..3fd1949 --- /dev/null +++ b/app/Http/Controllers/Api/V1/Admin/User/AdminUserShowController.php @@ -0,0 +1,20 @@ +load('roles'); + + return ApiResponse::success(AdminUserApiPresenter::listItem($admin_user)); + } +} diff --git a/app/Http/Controllers/Api/V1/Admin/User/AdminUserStoreController.php b/app/Http/Controllers/Api/V1/Admin/User/AdminUserStoreController.php new file mode 100644 index 0000000..dd4a185 --- /dev/null +++ b/app/Http/Controllers/Api/V1/Admin/User/AdminUserStoreController.php @@ -0,0 +1,75 @@ +lotteryAdmin(); + + $payload = $request->all(); + if (isset($payload['username']) && is_string($payload['username'])) { + $payload['username'] = Str::lower(trim($payload['username'])); + } + if (array_key_exists('email', $payload) && $payload['email'] === '') { + $payload['email'] = null; + } + + $data = validator($payload, [ + 'username' => ['required', 'string', 'min:2', 'max:64', 'regex:/^[a-zA-Z0-9._-]+$/u', 'unique:admin_users,username'], + 'nickname' => ['required', 'string', 'max:128'], + 'email' => ['nullable', 'string', 'email', 'max:255'], + 'password' => ['required', 'string', 'min:8', 'max:256'], + 'status' => ['sometimes', 'integer', 'in:0,1'], + 'role_slugs' => ['required', 'array', 'min:1'], + 'role_slugs.*' => ['string', 'max:64', 'distinct', 'exists:admin_roles,slug'], + ])->validate(); + + $email = is_string($data['email'] ?? null) && trim($data['email']) !== '' + ? trim($data['email']) + : null; + + $roleSlugs = array_values(array_unique($data['role_slugs'])); + + $user = DB::transaction(function () use ($data, $email, $roleSlugs): AdminUser { + $created = AdminUser::query()->create([ + 'username' => $data['username'], + 'name' => $data['nickname'], + 'email' => $email, + 'password' => $data['password'], + 'status' => array_key_exists('status', $data) ? (int) $data['status'] : 0, + ]); + $created->syncRoleSlugsForDefaultSite($roleSlugs); + + return $created; + }); + + $user->load('roles'); + + AuditLogger::recordForAdmin( + $actor, + $request, + 'system', + 'admin_user.create', + 'admin_user', + (string) $user->getKey(), + null, + AdminUserApiPresenter::listItem($user), + ); + + return ApiResponse::success(AdminUserApiPresenter::listItem($user)); + } +} diff --git a/app/Http/Controllers/Api/V1/Admin/User/AdminUserUpdateController.php b/app/Http/Controllers/Api/V1/Admin/User/AdminUserUpdateController.php new file mode 100644 index 0000000..4924f57 --- /dev/null +++ b/app/Http/Controllers/Api/V1/Admin/User/AdminUserUpdateController.php @@ -0,0 +1,77 @@ +lotteryAdmin(); + + $admin_user->load('roles'); + $before = AdminUserApiPresenter::listItem($admin_user); + + $payload = $request->all(); + if (array_key_exists('email', $payload) && $payload['email'] === '') { + $payload['email'] = null; + } + + /** @var array{nickname?:string,email?:?string,password?:?string,status?:int} $data */ + $data = validator($payload, [ + 'nickname' => ['sometimes', 'string', 'max:128'], + 'email' => ['sometimes', 'nullable', 'string', 'email', 'max:255', Rule::unique('admin_users', 'email')->ignore($admin_user->id)], + 'password' => ['sometimes', 'nullable', 'string', 'min:8', 'max:256'], + 'status' => ['sometimes', 'integer', Rule::in([0, 1])], + ])->validate(); + + $updates = []; + if (array_key_exists('nickname', $data)) { + $updates['name'] = $data['nickname']; + } + if (array_key_exists('email', $data)) { + $updates['email'] = ($data['email'] !== null && trim((string) $data['email']) !== '') + ? trim((string) $data['email']) + : null; + } + if (array_key_exists('status', $data)) { + $updates['status'] = (int) $data['status']; + } + if (array_key_exists('password', $data) && is_string($data['password']) && $data['password'] !== '') { + $updates['password'] = $data['password']; + } + + if ($updates === []) { + return ApiResponse::success($before); + } + + $admin_user->fill($updates); + $admin_user->save(); + $admin_user->load('roles'); + + $after = AdminUserApiPresenter::listItem($admin_user); + + AuditLogger::recordForAdmin( + $actor, + $request, + 'system', + 'admin_user.update', + 'admin_user', + (string) $admin_user->getKey(), + $before, + $after, + ); + + return ApiResponse::success($after); + } +} diff --git a/app/Models/AdminUser.php b/app/Models/AdminUser.php index 1fc6d2f..7e64c92 100644 --- a/app/Models/AdminUser.php +++ b/app/Models/AdminUser.php @@ -73,6 +73,38 @@ class AdminUser extends Authenticatable )->withPivot(['site_id', 'granted_at']); } + /** + * 将用户在默认站点上的角色设为指定 slug 集合(全量替换该站点 pivot)。 + * + * @param list $slugs + */ + public function syncRoleSlugsForDefaultSite(array $slugs): void + { + $siteId = self::defaultAdminSiteId(); + $slugs = array_values(array_unique($slugs)); + $roleIds = DB::table('admin_roles') + ->whereIn('slug', $slugs) + ->pluck('id') + ->all(); + + DB::transaction(function () use ($siteId, $roleIds): void { + DB::table('admin_user_site_roles') + ->where('admin_user_id', $this->id) + ->where('site_id', $siteId) + ->delete(); + + $now = now(); + foreach ($roleIds as $rid) { + DB::table('admin_user_site_roles')->insert([ + 'admin_user_id' => $this->id, + 'site_id' => $siteId, + 'role_id' => (int) $rid, + 'granted_at' => $now, + ]); + } + }); + } + public function isSuperAdmin(): bool { if ($this->relationLoaded('roles')) { diff --git a/app/Services/Config/OddsStreamService.php b/app/Services/Config/OddsStreamService.php index c146c77..6ae62f4 100644 --- a/app/Services/Config/OddsStreamService.php +++ b/app/Services/Config/OddsStreamService.php @@ -18,14 +18,14 @@ use Illuminate\Support\Facades\DB; final class OddsStreamService { /** @return LengthAwarePaginator */ - public function paginate(?string $status, int $perPage): LengthAwarePaginator + public function paginate(?string $status, int $perPage, int $page = 1): LengthAwarePaginator { $q = OddsVersion::query()->orderByDesc('id'); if ($status !== null && $status !== '') { $q->where('status', $status); } - return $q->paginate($perPage); + return $q->paginate($perPage, ['*'], 'page', max(1, $page)); } public function createDraft(AdminUser $admin, ?string $reason, ?int $cloneFromVersionId): OddsVersion diff --git a/app/Services/Config/PlayConfigStreamService.php b/app/Services/Config/PlayConfigStreamService.php index 4679038..aadbcfe 100644 --- a/app/Services/Config/PlayConfigStreamService.php +++ b/app/Services/Config/PlayConfigStreamService.php @@ -16,14 +16,14 @@ use Illuminate\Support\Facades\DB; final class PlayConfigStreamService { /** @return LengthAwarePaginator */ - public function paginate(?string $status, int $perPage): LengthAwarePaginator + public function paginate(?string $status, int $perPage, int $page = 1): LengthAwarePaginator { $q = PlayConfigVersion::query()->orderByDesc('id'); if ($status !== null && $status !== '') { $q->where('status', $status); } - return $q->paginate($perPage); + return $q->paginate($perPage, ['*'], 'page', max(1, $page)); } public function createDraft(AdminUser $admin, ?string $reason, ?int $cloneFromVersionId): PlayConfigVersion diff --git a/app/Services/Config/RiskCapStreamService.php b/app/Services/Config/RiskCapStreamService.php index e90af5c..f0f0186 100644 --- a/app/Services/Config/RiskCapStreamService.php +++ b/app/Services/Config/RiskCapStreamService.php @@ -15,14 +15,14 @@ use Illuminate\Support\Facades\DB; final class RiskCapStreamService { /** @return LengthAwarePaginator */ - public function paginate(?string $status, int $perPage): LengthAwarePaginator + public function paginate(?string $status, int $perPage, int $page = 1): LengthAwarePaginator { $q = RiskCapVersion::query()->orderByDesc('id'); if ($status !== null && $status !== '') { $q->where('status', $status); } - return $q->paginate($perPage); + return $q->paginate($perPage, ['*'], 'page', max(1, $page)); } public function createDraft(AdminUser $admin, ?string $reason, ?int $cloneFromVersionId): RiskCapVersion diff --git a/app/Support/AdminApiList.php b/app/Support/AdminApiList.php new file mode 100644 index 0000000..08bd2a4 --- /dev/null +++ b/app/Support/AdminApiList.php @@ -0,0 +1,75 @@ +integer('per_page', $defaultPerPage), 1), $maxPerPage); + $page = max((int) $request->integer('page', 1), 1); + + return ['page' => $page, 'perPage' => $perPage]; + } + + /** + * @param callable(object): array $row + * @return array{items: list>, meta: array{current_page: int, per_page: int, total: int, last_page: int}} + */ + public static function payload(LengthAwarePaginator $paginator, callable $row): array + { + return [ + 'items' => collect($paginator->items())->map($row)->values()->all(), + 'meta' => self::meta($paginator), + ]; + } + + /** + * @return array{current_page: int, per_page: int, total: int, last_page: int} + */ + public static function meta(LengthAwarePaginator $paginator): array + { + return [ + 'current_page' => $paginator->currentPage(), + 'per_page' => $paginator->perPage(), + 'total' => $paginator->total(), + 'last_page' => $paginator->lastPage(), + ]; + } + + /** + * @param callable(object): array $row + */ + public static function json(LengthAwarePaginator $paginator, callable $row): JsonResponse + { + return ApiResponse::success(self::payload($paginator, $row)); + } + + /** + * 在标准列表外包一层字段(如 draw_id、job_no),与 items、meta 同级合并。 + * + * @param callable(object): array $row + * @param array $extra + */ + public static function jsonWith(LengthAwarePaginator $paginator, callable $row, array $extra = []): JsonResponse + { + return ApiResponse::success(array_merge($extra, self::payload($paginator, $row))); + } +} diff --git a/app/Support/AdminUserApiPresenter.php b/app/Support/AdminUserApiPresenter.php new file mode 100644 index 0000000..7e20e64 --- /dev/null +++ b/app/Support/AdminUserApiPresenter.php @@ -0,0 +1,26 @@ + */ + public static function listItem(AdminUser $user): array + { + $user->loadMissing('roles'); + + return [ + 'id' => (int) $user->id, + 'username' => $user->username, + 'nickname' => $user->name, + 'email' => $user->email, + 'status' => (int) $user->status, + 'roles' => $user->adminRoleSlugs(), + 'direct_permissions' => $user->directLegacyPermissionSlugs(), + 'effective_permissions' => $user->adminPermissionSlugs(), + ]; + } +} diff --git a/routes/api.php b/routes/api.php index 380c6c6..1f45e4c 100644 --- a/routes/api.php +++ b/routes/api.php @@ -48,9 +48,13 @@ use App\Http\Controllers\Api\V1\Admin\Settlement\AdminSettlementBatchDetailsCont use App\Http\Controllers\Api\V1\Admin\Settlement\AdminSettlementBatchIndexController; use App\Http\Controllers\Api\V1\Admin\Settlement\AdminSettlementBatchShowController; use App\Http\Controllers\Api\V1\Admin\User\AdminPermissionCatalogController; +use App\Http\Controllers\Api\V1\Admin\User\AdminUserDestroyController; use App\Http\Controllers\Api\V1\Admin\User\AdminUserIndexController; use App\Http\Controllers\Api\V1\Admin\User\AdminUserPermissionSyncController; use App\Http\Controllers\Api\V1\Admin\User\AdminUserRoleSyncController; +use App\Http\Controllers\Api\V1\Admin\User\AdminUserShowController; +use App\Http\Controllers\Api\V1\Admin\User\AdminUserStoreController; +use App\Http\Controllers\Api\V1\Admin\User\AdminUserUpdateController; use App\Http\Controllers\Api\V1\Admin\Wallet\TransferOrderListController; use App\Http\Controllers\Api\V1\Admin\Wallet\WalletTransactionListController; use App\Http\Controllers\Api\V1\Draw\DrawCurrentController; @@ -315,6 +319,10 @@ Route::prefix('v1')->group(function (): void { /** 后台账号与权限分配:仅可管理账户执行。 */ Route::middleware('admin.permission:prd.admin_user.manage')->group(function (): void { Route::get('admin-users', AdminUserIndexController::class)->name('admin-users.index'); + Route::post('admin-users', AdminUserStoreController::class)->name('admin-users.store'); + Route::get('admin-users/{admin_user}', AdminUserShowController::class)->name('admin-users.show'); + Route::put('admin-users/{admin_user}', AdminUserUpdateController::class)->name('admin-users.update'); + Route::delete('admin-users/{admin_user}', AdminUserDestroyController::class)->name('admin-users.destroy'); Route::get('admin-user-permission-catalog', AdminPermissionCatalogController::class) ->name('admin-users.permission-catalog'); Route::put('admin-users/{admin_user}/permissions', AdminUserPermissionSyncController::class) diff --git a/tests/Feature/AdminUserPermissionApiTest.php b/tests/Feature/AdminUserPermissionApiTest.php index 37fe206..cea510d 100644 --- a/tests/Feature/AdminUserPermissionApiTest.php +++ b/tests/Feature/AdminUserPermissionApiTest.php @@ -153,3 +153,127 @@ test('admin can sync user roles for default site', function (): void { sort($slugs); expect($slugs)->toBe(['role_sync_a', 'role_sync_b']); }); + +test('admin can create update and delete users with crud rules', function (): void { + $token = makeAdminWithPermissions('crud_actor', ['prd.admin_user.manage']); + + $crudRole = AdminRole::query()->create(['slug' => 'crud_new_user_role', 'name' => 'Crud Role']); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson('/api/v1/admin/admin-users', [ + 'username' => 'NewUser_XX', + 'nickname' => '新用户', + 'email' => 'newuser@example.com', + 'password' => 'secret-long', + 'status' => 0, + 'role_slugs' => ['crud_new_user_role'], + ]) + ->assertOk() + ->assertJsonPath('code', ErrorCode::Success->value) + ->assertJsonPath('data.username', 'newuser_xx') + ->assertJsonPath('data.roles.0', 'crud_new_user_role'); + + $created = AdminUser::query()->where('username', 'newuser_xx')->firstOrFail(); + expect($created->adminRoleSlugs())->toContain('crud_new_user_role'); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson('/api/v1/admin/admin-users', [ + 'username' => 'newuser_xx', + 'nickname' => 'dup', + 'email' => null, + 'password' => 'secret-long', + 'role_slugs' => [$crudRole->slug], + ]) + ->assertStatus(422) + ->assertJsonPath('code', ErrorCode::ValidationFailed->value); + + $target = AdminUser::query()->where('username', 'newuser_xx')->firstOrFail(); + $this->withHeader('Authorization', 'Bearer '.$token) + ->putJson('/api/v1/admin/admin-users/'.$target->id, [ + 'nickname' => '已改名', + 'email' => null, + 'password' => 'new-secret-9', + ]) + ->assertOk() + ->assertJsonPath('data.nickname', '已改名'); + + expect(Hash::check('new-secret-9', $target->fresh()->password))->toBeTrue(); + + $victim = AdminUser::query()->create([ + 'username' => 'to_delete', + 'name' => 'Delete Me', + 'email' => null, + 'password' => Hash::make('secret-strong'), + 'status' => 0, + ]); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->deleteJson('/api/v1/admin/admin-users/'.$victim->id) + ->assertOk() + ->assertJsonPath('data.deleted', true); + + expect(AdminUser::query()->whereKey($victim->id)->exists())->toBeFalse(); +}); + +test('admin user create requires at least one role slug', function (): void { + $token = makeAdminWithPermissions('create_need_roles', ['prd.admin_user.manage']); + AdminRole::query()->create(['slug' => 'role_for_create_gate', 'name' => 'Gate Role']); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->postJson('/api/v1/admin/admin-users', [ + 'username' => 'no_roles_user', + 'nickname' => 'NR', + 'email' => null, + 'password' => 'secret-long', + 'role_slugs' => [], + ]) + ->assertStatus(422) + ->assertJsonPath('code', ErrorCode::ValidationFailed->value); +}); + +test('admin cannot delete self', function (): void { + $token = makeAdminWithPermissions('self_guard', ['prd.admin_user.manage']); + $me = AdminUser::query()->where('username', 'self_guard')->firstOrFail(); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->deleteJson('/api/v1/admin/admin-users/'.$me->id) + ->assertStatus(422) + ->assertJsonPath('code', ErrorCode::ValidationFailed->value) + ->assertJsonPath('msg', '不能删除当前登录账号'); +}); + +test('admin cannot delete the last super admin', function (): void { + $token = makeAdminWithPermissions('super_deleter', ['prd.admin_user.manage']); + + $s1 = AdminUser::query()->create([ + 'username' => 'super_one', + 'name' => 'S1', + 'email' => null, + 'password' => Hash::make('secret-strong'), + 'status' => 0, + ]); + grantSuperAdminRole($s1); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->deleteJson('/api/v1/admin/admin-users/'.$s1->id) + ->assertStatus(422) + ->assertJsonPath('msg', '不能删除最后一个超级管理员'); + + $s2 = AdminUser::query()->create([ + 'username' => 'super_two', + 'name' => 'S2', + 'email' => null, + 'password' => Hash::make('secret-strong'), + 'status' => 0, + ]); + grantSuperAdminRole($s2); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->deleteJson('/api/v1/admin/admin-users/'.$s1->id) + ->assertOk(); + + $this->withHeader('Authorization', 'Bearer '.$token) + ->deleteJson('/api/v1/admin/admin-users/'.$s2->id) + ->assertStatus(422) + ->assertJsonPath('msg', '不能删除最后一个超级管理员'); +});