refactor: 更新权限管理与请求验证逻辑

- 在多个控制器中将权限检查从 hasAdminPermission 更新为 hasPermissionCode,以增强权限管理的灵活性。
- 引入 AdminScopePolicy,优化基于代理节点的权限和数据过滤逻辑,确保管理员能够更精确地控制访问权限。
- 在请求验证中添加 agent_node_id 字段,确保 API 接口支持代理节点的相关操作。
- 更新 AdminUser 模型,新增 hasPermissionCode 方法,以支持更细粒度的权限检查。
- 优化审计日志记录逻辑,确保在处理请求时能够准确记录管理员的操作。
This commit is contained in:
2026-06-03 10:07:38 +08:00
parent 0841fbed32
commit 1dcd4716c5
64 changed files with 2054 additions and 344 deletions

View File

@@ -32,7 +32,7 @@ final class AgentAdminUserRoleSyncController extends Controller
return $denied;
}
if (! $admin->isSuperAdmin() && ! $admin->hasAdminPermission('prd.agent.user.manage')) {
if (! $admin->isSuperAdmin() && ! $admin->hasPermissionCode('agent.node.manage')) {
return AdminAgentNodeAccess::denyUnlessCanManageParent($admin, $agent);
}

View File

@@ -27,7 +27,7 @@ final class AgentNodeAdminUserStoreController extends Controller
return $denied;
}
if (! $admin->isSuperAdmin() && ! $admin->hasAdminPermission('prd.agent.user.manage')) {
if (! $admin->isSuperAdmin() && ! $admin->hasPermissionCode('agent.node.manage')) {
return AdminAgentNodeAccess::denyUnlessCanManageParent($admin, $agent_node);
}

View File

@@ -27,7 +27,7 @@ final class AgentNodeRoleStoreController extends Controller
return $denied;
}
if (! $admin->isSuperAdmin() && ! $admin->hasAdminPermission('prd.agent.role.manage')) {
if (! $admin->isSuperAdmin() && ! $admin->hasPermissionCode('agent.node.manage')) {
return AdminAgentNodeAccess::denyUnlessCanManageParent($admin, $agent_node);
}

View File

@@ -8,6 +8,7 @@ use App\Services\AuditLogger;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Services\Agent\AgentRoleService;
use App\Support\AdminPermissionInheritance;
use App\Support\AgentRoleAuthorization;
use App\Support\AdminRoleApiPresenter;
use App\Http\Requests\Admin\AgentRolePermissionSyncRequest;
@@ -35,7 +36,9 @@ final class AgentRolePermissionSyncController extends Controller
return AgentRoleAuthorization::denyUnlessRoleManageable($admin, $admin_role);
}
$slugs = array_values(array_unique($request->validated('permission_slugs')));
$slugs = AdminPermissionInheritance::expand(
array_values(array_unique($request->validated('permission_slugs'))),
);
$before = AdminRoleApiPresenter::item($admin_role);
$role = $service->syncPermissions($admin, $admin_role, $slugs);

View File

@@ -5,8 +5,10 @@ namespace App\Http\Controllers\Api\V1\Admin\Audit;
use App\Models\AuditLog;
use Illuminate\Http\Request;
use App\Support\AdminApiList;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Support\AuditLogApiPresenter;
/**
* GET /api/v1/admin/audit-logs 运营/客服查询审计留痕。
@@ -46,25 +48,6 @@ final class AuditLogIndexController extends Controller
$paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']);
return AdminApiList::json($paginator, fn (AuditLog $r) => $this->row($r));
}
/** @return array<string, mixed> */
private function row(AuditLog $r): array
{
return [
'id' => (int) $r->id,
'operator_type' => $r->operator_type,
'operator_id' => (int) $r->operator_id,
'module_code' => $r->module_code,
'action_code' => $r->action_code,
'target_type' => $r->target_type,
'target_id' => $r->target_id,
'before_json' => $r->before_json,
'after_json' => $r->after_json,
'ip' => $r->ip,
'user_agent' => $r->user_agent,
'created_at' => $r->created_at?->toIso8601String(),
];
return ApiResponse::success(AuditLogApiPresenter::listPayload($paginator));
}
}

View File

@@ -8,6 +8,7 @@ use App\Support\ApiResponse;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Support\AdminScopeContextResolver;
use App\Services\Admin\AdminDashboardSnapshotBuilder;
/**
@@ -31,6 +32,8 @@ final class AdminDashboardController extends Controller
);
}
return ApiResponse::success($this->dashboard->build($admin));
$scope = AdminScopeContextResolver::fromRequest($request, $admin);
return ApiResponse::success($this->dashboard->build($scope));
}
}

View File

@@ -5,10 +5,12 @@ namespace App\Http\Controllers\Api\V1\Admin\Draw;
use App\Models\Draw;
use App\Models\TicketItem;
use App\Models\TicketOrder;
use App\Models\AdminUser;
use App\Support\ApiResponse;
use App\Models\SettlementBatch;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Support\AdminScopePolicy;
/**
* GET /api/v1/admin/draws/{draw}/finance-summary 单期投注/派彩汇总(客服/财务视角PRD §15.4)。
@@ -17,20 +19,27 @@ use App\Http\Controllers\Controller;
*/
final class AdminDrawFinanceSummaryController extends Controller
{
public function __invoke(Draw $draw): JsonResponse
public function __invoke(\Illuminate\Http\Request $request, Draw $draw): JsonResponse
{
$admin = $request->lotteryAdmin();
abort_if(! $admin instanceof AdminUser, 401);
$scope = AdminScopePolicy::resolveContext($request, $admin);
$drawId = (int) $draw->id;
$totalBetMinor = (int) TicketOrder::query()->where('draw_id', $drawId)->sum('total_actual_deduct');
$orderCount = (int) TicketOrder::query()->where('draw_id', $drawId)->count();
$itemCount = (int) TicketItem::query()->where('draw_id', $drawId)->count();
$orders = TicketOrder::query()->where('draw_id', $drawId);
$items = TicketItem::query()->where('draw_id', $drawId);
AdminScopePolicy::applyViaPlayer($orders, $scope);
AdminScopePolicy::applyViaPlayer($items, $scope);
$currencyCode = (string) (TicketOrder::query()
->where('draw_id', $drawId)
->value('currency_code') ?? '');
$totalBetMinor = (int) (clone $orders)->sum('total_actual_deduct');
$orderCount = (int) (clone $orders)->count();
$itemCount = (int) (clone $items)->count();
$totalWinMinor = (int) TicketItem::query()->where('draw_id', $drawId)->sum('win_amount');
$totalJackpotWinMinor = (int) TicketItem::query()->where('draw_id', $drawId)->sum('jackpot_win_amount');
$currencyCode = (string) ((clone $orders)->value('currency_code') ?? '');
$totalWinMinor = (int) (clone $items)->sum('win_amount');
$totalJackpotWinMinor = (int) (clone $items)->sum('jackpot_win_amount');
$totalPayoutMinor = $totalWinMinor + $totalJackpotWinMinor;
$approxHouseGrossMinor = $totalBetMinor - $totalPayoutMinor;

View File

@@ -8,8 +8,9 @@ use App\Models\Draw;
use App\Models\TicketItem;
use App\Models\TicketOrder;
use Illuminate\Http\Request;
use App\Support\AdminSiteScope;
use App\Support\AdminApiList;
use App\Support\AdminScopeContext;
use App\Support\AdminScopePolicy;
use App\Services\LotterySettings;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
@@ -28,7 +29,7 @@ final class AdminDrawIndexController extends Controller
$p = AdminApiList::readPaging($request);
$drawNo = trim((string) $request->query('draw_no', ''));
$status = trim((string) $request->query('status', ''));
$agentNodeId = $request->integer('agent_node_id') ?: null;
$scope = AdminScopePolicy::resolveContext($request, $admin);
$q = Draw::query()->orderByDesc('draw_time')->orderByDesc('id');
@@ -45,8 +46,7 @@ final class AdminDrawIndexController extends Controller
$statsByDrawId = $this->aggregateListStats(
$paginator->getCollection()->pluck('id')->map(fn ($id) => (int) $id)->all(),
$admin,
$agentNodeId,
$scope,
);
return AdminApiList::jsonWith($paginator, fn (Draw $row) => $this->row($row, $statsByDrawId), [
@@ -63,21 +63,21 @@ final class AdminDrawIndexController extends Controller
* @param list<int> $drawIds
* @return array<int, array{total_bet_minor: int, total_payout_minor: int, profit_loss_minor: int}>
*/
private function aggregateListStats(array $drawIds, AdminUser $admin, ?int $agentNodeId): array
private function aggregateListStats(array $drawIds, AdminScopeContext $scope): array
{
if ($drawIds === []) {
return [];
}
$betQuery = TicketOrder::query()->whereIn('draw_id', $drawIds);
$this->scopeOrdersToVisiblePlayers($betQuery, $admin, $agentNodeId);
$this->scopeOrdersToVisiblePlayers($betQuery, $scope);
$betByDraw = $betQuery
->groupBy('draw_id')
->selectRaw('draw_id, COALESCE(SUM(total_actual_deduct), 0) AS total_bet')
->pluck('total_bet', 'draw_id');
$payoutQuery = TicketItem::query()->whereIn('draw_id', $drawIds);
$this->scopeTicketItemsToVisiblePlayers($payoutQuery, $admin, $agentNodeId);
$this->scopeTicketItemsToVisiblePlayers($payoutQuery, $scope);
$payoutRows = $payoutQuery
->groupBy('draw_id')
->selectRaw(
@@ -104,38 +104,28 @@ final class AdminDrawIndexController extends Controller
/**
* @param \Illuminate\Database\Eloquent\Builder<TicketOrder> $query
*/
private function scopeOrdersToVisiblePlayers($query, AdminUser $admin, ?int $agentNodeId): void
private function scopeOrdersToVisiblePlayers($query, AdminScopeContext $scope): void
{
if ($admin->isSuperAdmin() && ($agentNodeId === null || $agentNodeId <= 0)) {
if ($scope->isSuperAdmin() && $scope->effectiveRequestedAgentNodeId() === null) {
return;
}
$query->whereHas('player', static function ($playerQuery) use ($admin, $agentNodeId): void {
AdminSiteScope::applyPlayerFilters(
$playerQuery,
$admin,
null,
$agentNodeId !== null && $agentNodeId > 0 ? $agentNodeId : null,
);
$query->whereHas('player', function ($playerQuery) use ($scope): void {
AdminScopePolicy::applyPlayerFilters($playerQuery, $scope);
});
}
/**
* @param \Illuminate\Database\Eloquent\Builder<TicketItem> $query
*/
private function scopeTicketItemsToVisiblePlayers($query, AdminUser $admin, ?int $agentNodeId): void
private function scopeTicketItemsToVisiblePlayers($query, AdminScopeContext $scope): void
{
if ($admin->isSuperAdmin() && ($agentNodeId === null || $agentNodeId <= 0)) {
if ($scope->isSuperAdmin() && $scope->effectiveRequestedAgentNodeId() === null) {
return;
}
$query->whereHas('player', static function ($playerQuery) use ($admin, $agentNodeId): void {
AdminSiteScope::applyPlayerFilters(
$playerQuery,
$admin,
null,
$agentNodeId !== null && $agentNodeId > 0 ? $agentNodeId : null,
);
$query->whereHas('player', function ($playerQuery) use ($scope): void {
AdminScopePolicy::applyPlayerFilters($playerQuery, $scope);
});
}

View File

@@ -7,7 +7,7 @@ use App\Support\AdminApiList;
use Illuminate\Http\JsonResponse;
use App\Models\JackpotContribution;
use App\Http\Controllers\Controller;
use App\Support\AdminDataScope;
use App\Support\AdminScopePolicy;
/**
* GET /api/v1/admin/jackpot/contributions Jackpot 蓄水流水。
@@ -18,6 +18,7 @@ final class AdminJackpotContributionIndexController extends Controller
{
$admin = $request->lotteryAdmin();
abort_if($admin === null, 401);
$scope = AdminScopePolicy::resolveContext($request, $admin);
$p = AdminApiList::readPaging($request);
$drawNo = trim((string) $request->query('draw_no', ''));
@@ -30,7 +31,7 @@ final class AdminJackpotContributionIndexController extends Controller
$q->whereHas('draw', fn ($d) => $d->where('draw_no', 'like', '%'.$drawNo.'%'));
}
AdminDataScope::applyEloquentViaPlayer($q, $admin);
AdminScopePolicy::applyViaPlayer($q, $scope);
$paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']);

View File

@@ -8,7 +8,7 @@ use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Support\AdminApiList;
use App\Support\AdminSiteScope;
use App\Support\AdminScopePolicy;
use App\Support\PlayerApiPresenter;
/** GET /api/v1/admin/players */
@@ -22,8 +22,7 @@ final class AdminPlayerIndexController extends Controller
$p = AdminApiList::readPaging($request);
$keyword = trim((string) $request->query('keyword', ''));
$status = $request->query('status');
$siteCode = $request->query('site_code');
$agentNodeId = $request->integer('agent_node_id') ?: null;
$scope = AdminScopePolicy::resolveContext($request, $admin);
$q = Player::query()
->with([
@@ -32,12 +31,7 @@ final class AdminPlayerIndexController extends Controller
])
->orderByDesc('id');
AdminSiteScope::applyPlayerFilters(
$q,
$admin,
is_string($siteCode) ? $siteCode : null,
$agentNodeId,
);
AdminScopePolicy::applyPlayerFilters($q, $scope);
if ($keyword !== '') {
$term = '%'.addcslashes($keyword, '%_\\').'%';

View File

@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AdminReportQueryRequest;
use App\Services\Admin\AdminReportQueryService;
use App\Support\AdminApiList;
use App\Support\AdminScopePolicy;
use Illuminate\Http\JsonResponse;
/** GET /api/v1/admin/reports/daily-profit */
@@ -19,13 +20,14 @@ final class AdminReportDailyProfitController extends Controller
$validated = $request->validated();
$p = AdminApiList::readPaging($request);
$range = $service->resolveDateRange($validated);
$scope = AdminScopePolicy::resolveContext($request, $admin, 'site_code', 'agent_node_id');
$paginator = $service->dailyProfitPaginated(
$range['date_from'],
$range['date_to'],
$p['page'],
$p['perPage'],
$admin,
$scope,
);
return AdminApiList::json($paginator, static fn (array $row): array => $row);

View File

@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AdminReportQueryRequest;
use App\Services\Admin\AdminReportQueryService;
use App\Support\AdminApiList;
use App\Support\AdminScopePolicy;
use Illuminate\Http\JsonResponse;
/** GET /api/v1/admin/reports/play-dimension */
@@ -20,6 +21,7 @@ final class AdminReportPlayDimensionController extends Controller
$p = AdminApiList::readPaging($request);
$range = $service->resolveDateRange($validated);
$playCode = isset($validated['play_code']) ? trim((string) $validated['play_code']) : null;
$scope = AdminScopePolicy::resolveContext($request, $admin, 'site_code', 'agent_node_id');
$paginator = $service->playDimensionPaginated(
$playCode !== '' ? $playCode : null,
@@ -27,7 +29,7 @@ final class AdminReportPlayDimensionController extends Controller
$range['date_to'],
$p['page'],
$p['perPage'],
$admin,
$scope,
);
return AdminApiList::json($paginator, static function (object $row): array {

View File

@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AdminReportQueryRequest;
use App\Services\Admin\AdminReportQueryService;
use App\Support\AdminApiList;
use App\Support\AdminScopePolicy;
use Illuminate\Http\JsonResponse;
/** GET /api/v1/admin/reports/player-win-loss */
@@ -20,7 +21,7 @@ final class AdminReportPlayerWinLossController extends Controller
$p = AdminApiList::readPaging($request);
$range = $service->resolveDateRange($validated);
$playerId = isset($validated['player_id']) ? (int) $validated['player_id'] : null;
$agentNodeId = isset($validated['agent_node_id']) ? (int) $validated['agent_node_id'] : null;
$scope = AdminScopePolicy::resolveContext($request, $admin, 'site_code', 'agent_node_id');
$paginator = $service->playerWinLossPaginated(
$playerId,
@@ -28,8 +29,7 @@ final class AdminReportPlayerWinLossController extends Controller
$range['date_to'],
$p['page'],
$p['perPage'],
$admin,
$agentNodeId > 0 ? $agentNodeId : null,
$scope,
);
return AdminApiList::json($paginator, static function (object $row): array {

View File

@@ -6,6 +6,7 @@ use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AdminReportQueryRequest;
use App\Services\Admin\AdminReportQueryService;
use App\Support\AdminApiList;
use App\Support\AdminScopePolicy;
use Illuminate\Http\JsonResponse;
/** GET /api/v1/admin/reports/rebate-commission */
@@ -20,6 +21,7 @@ final class AdminReportRebateCommissionController extends Controller
$p = AdminApiList::readPaging($request);
$range = $service->resolveDateRange($validated);
$playCode = isset($validated['play_code']) ? trim((string) $validated['play_code']) : null;
$scope = AdminScopePolicy::resolveContext($request, $admin, 'site_code', 'agent_node_id');
$paginator = $service->rebateCommissionPaginated(
$playCode !== '' ? $playCode : null,
@@ -27,7 +29,7 @@ final class AdminReportRebateCommissionController extends Controller
$range['date_to'],
$p['page'],
$p['perPage'],
$admin,
$scope,
);
return AdminApiList::json($paginator, static function (object $row): array {

View File

@@ -0,0 +1,56 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Settlement;
use App\Models\AdminUser;
use App\Support\ApiMessage;
use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use App\Models\SettlementBatch;
use App\Http\Controllers\Controller;
use App\Support\LotteryMessage;
use App\Exceptions\TicketOperationException;
use App\Services\Settlement\SettlementPayoutCorrectionService;
use App\Http\Requests\Admin\SettlementPayoutAdjustmentRequest;
final class AdminSettlementBatchAdjustmentController extends Controller
{
public function __construct(
private readonly SettlementPayoutCorrectionService $service,
) {}
public function __invoke(SettlementPayoutAdjustmentRequest $request, SettlementBatch $batch): JsonResponse
{
$admin = $request->user();
if (! $admin instanceof AdminUser) {
return ApiResponse::error(
trans('admin.unauthenticated', [], $request->lotteryLocale()),
\App\Lottery\ErrorCode::AdminUnauthenticated->value,
null,
401,
);
}
try {
$result = $this->service->apply(
$batch,
$admin,
(int) $request->validated('player_id'),
(int) $request->validated('amount_delta'),
(string) $request->validated('reason'),
$request,
);
} catch (TicketOperationException $e) {
return ApiResponse::error(
LotteryMessage::wallet($request, $e->lotteryCode),
$e->lotteryCode,
$e->payload,
$e->httpStatus,
);
} catch (\RuntimeException $e) {
return ApiMessage::runtimeErrorResponse($request, $e, 0, 422);
}
return ApiResponse::success($result);
}
}

View File

@@ -4,7 +4,7 @@ namespace App\Http\Controllers\Api\V1\Admin\Settlement;
use Illuminate\Http\Request;
use App\Support\AdminApiList;
use App\Support\AdminSiteScope;
use App\Support\AdminScopePolicy;
use App\Support\AgentNodeApiPresenter;
use App\Models\SettlementBatch;
use Illuminate\Http\JsonResponse;
@@ -22,7 +22,7 @@ final class AdminSettlementBatchDetailsController extends Controller
abort_if($admin === null, 401);
$p = AdminApiList::readPaging($request);
$agentNodeId = $request->integer('agent_node_id') ?: null;
$scope = AdminScopePolicy::resolveContext($request, $admin);
$detailQuery = TicketSettlementDetail::query()
->where('settlement_batch_id', $batch->id)
@@ -33,14 +33,9 @@ final class AdminSettlementBatchDetailsController extends Controller
'ticketItem.order:id,currency_code',
]);
if (! $admin->isSuperAdmin() || ($agentNodeId !== null && $agentNodeId > 0)) {
$detailQuery->whereHas('ticketItem.player', static function ($playerQuery) use ($admin, $agentNodeId): void {
AdminSiteScope::applyPlayerFilters(
$playerQuery,
$admin,
null,
$agentNodeId !== null && $agentNodeId > 0 ? $agentNodeId : null,
);
if (! $scope->isSuperAdmin() || $scope->effectiveRequestedAgentNodeId() !== null) {
$detailQuery->whereHas('ticketItem.player', function ($playerQuery) use ($scope): void {
AdminScopePolicy::applyPlayerFilters($playerQuery, $scope);
});
}

View File

@@ -4,7 +4,7 @@ namespace App\Http\Controllers\Api\V1\Admin\Settlement;
use Illuminate\Http\Request;
use App\Support\AdminApiList;
use App\Support\AdminSiteScope;
use App\Support\AdminScopePolicy;
use App\Models\SettlementBatch;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
@@ -23,20 +23,15 @@ final class AdminSettlementBatchIndexController extends Controller
$p = AdminApiList::readPaging($request);
$drawNo = trim((string) $request->query('draw_no', ''));
$status = trim((string) $request->query('status', ''));
$agentNodeId = $request->integer('agent_node_id') ?: null;
$scope = AdminScopePolicy::resolveContext($request, $admin);
$q = SettlementBatch::query()
->with(['draw:id,draw_no'])
->orderByDesc('id');
if (! $admin->isSuperAdmin() || ($agentNodeId !== null && $agentNodeId > 0)) {
$q->whereHas('details.ticketItem.player', static function ($playerQuery) use ($admin, $agentNodeId): void {
AdminSiteScope::applyPlayerFilters(
$playerQuery,
$admin,
null,
$agentNodeId !== null && $agentNodeId > 0 ? $agentNodeId : null,
);
if (! $scope->isSuperAdmin() || $scope->effectiveRequestedAgentNodeId() !== null) {
$q->whereHas('details.ticketItem.player', function ($playerQuery) use ($scope): void {
AdminScopePolicy::applyPlayerFilters($playerQuery, $scope);
});
}

View File

@@ -8,7 +8,7 @@ use App\Models\TicketItem;
use App\Support\ApiResponse;
use App\Support\CurrencyFormatter;
use App\Support\PaginationTrait;
use App\Support\AdminSiteScope;
use App\Support\AdminScopePolicy;
use App\Support\AgentNodeApiPresenter;
use App\Support\TicketItemListFilters;
use Illuminate\Http\JsonResponse;
@@ -37,6 +37,7 @@ final class AdminTicketItemIndexController extends Controller
abort_if($admin === null, 401);
$validated = $request->validated();
$scope = AdminScopePolicy::resolveContext($request, $admin);
$perPage = $this->perPage($request, 'per_page', 10, 100);
$page = $this->page($request);
@@ -88,15 +89,7 @@ final class AdminTicketItemIndexController extends Controller
is_string($validated['end_date'] ?? null) ? $validated['end_date'] : null,
);
$agentNodeId = isset($validated['agent_node_id']) ? (int) $validated['agent_node_id'] : null;
AdminSiteScope::applyViaPlayerRelationWithSiteCode(
$query,
$admin,
is_string($validated['site_code'] ?? null) ? $validated['site_code'] : null,
'player',
$agentNodeId > 0 ? $agentNodeId : null,
);
AdminScopePolicy::applyViaPlayerRelationWithContext($query, $scope, 'player');
$paginator = $query->paginate(perPage: $perPage, page: $page, columns: ['*']);

View File

@@ -8,6 +8,7 @@ use App\Services\AuditLogger;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
use App\Support\AdminPermissionInheritance;
use App\Support\AdminRoleApiPresenter;
use App\Http\Requests\Admin\AdminRolePermissionSyncRequest;
@@ -15,7 +16,9 @@ final class AdminRolePermissionSyncController extends Controller
{
public function __invoke(AdminRolePermissionSyncRequest $request, AdminRole $admin_role): JsonResponse
{
$slugs = array_values(array_unique($request->validated('permission_slugs', [])));
$slugs = AdminPermissionInheritance::expand(
array_values(array_unique($request->validated('permission_slugs', []))),
);
$before = AdminRoleApiPresenter::item($admin_role);
DB::transaction(function () use ($admin_role, $slugs): void {

View File

@@ -8,6 +8,7 @@ use App\Services\AuditLogger;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use App\Http\Controllers\Controller;
use App\Support\AdminPermissionInheritance;
use App\Support\AdminRoleApiPresenter;
use App\Http\Requests\Admin\AdminRoleStoreRequest;
@@ -15,7 +16,9 @@ final class AdminRoleStoreController extends Controller
{
public function __invoke(AdminRoleStoreRequest $request): JsonResponse
{
$permissionSlugs = array_values(array_unique($request->validated('permission_slugs', [])));
$permissionSlugs = AdminPermissionInheritance::expand(
array_values(array_unique($request->validated('permission_slugs', []))),
);
$role = DB::transaction(function () use ($request, $permissionSlugs): AdminRole {
$role = AdminRole::query()->create([

View File

@@ -7,9 +7,10 @@ use App\Support\ApiResponse;
use App\Models\TransferOrder;
use App\Support\PaginationTrait;
use Illuminate\Http\JsonResponse;
use App\Support\AdminSiteScope;
use App\Support\AdminScopePolicy;
use App\Support\AgentNodeApiPresenter;
use App\Support\CurrencyFormatter;
use App\Services\Wallet\LotteryTransferService;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\TransferOrderListRequest;
@@ -34,12 +35,17 @@ final class TransferOrderListController extends Controller
private const ALLOWED_STATUS = ['processing', 'success', 'failed', 'pending_reconcile', 'reversed', 'manually_processed'];
public function __construct(
private readonly LotteryTransferService $transferService,
) {}
public function __invoke(TransferOrderListRequest $request): JsonResponse
{
$admin = $request->lotteryAdmin();
abort_if($admin === null, 401);
$validated = $request->validated();
$scope = AdminScopePolicy::resolveContext($request, $admin);
$perPage = $this->perPage($request, 'per_page', 10, 100);
$page = $this->page($request);
@@ -89,15 +95,7 @@ final class TransferOrderListController extends Controller
}
}
$agentNodeId = isset($validated['agent_node_id']) ? (int) $validated['agent_node_id'] : null;
AdminSiteScope::applyViaPlayerRelationWithSiteCode(
$query,
$admin,
is_string($validated['site_code'] ?? null) ? $validated['site_code'] : null,
'player',
$agentNodeId > 0 ? $agentNodeId : null,
);
AdminScopePolicy::applyViaPlayerRelationWithContext($query, $scope, 'player');
$paginator = $query->paginate($perPage, ['*'], 'page', $page);
$items = $paginator->getCollection()->map(
@@ -120,8 +118,9 @@ final class TransferOrderListController extends Controller
$p = $o->player;
$amount = (int) $o->amount;
$canWriteWallet = $admin !== null && (
$admin->hasAdminPermission('prd.wallet_adjust.manage')
|| $admin->hasAdminPermission('prd.wallet_reconcile.manage')
$admin->hasPermissionCode('service.wallet.adjust')
|| $admin->hasPermissionCode('service.reconcile.manage')
|| $admin->hasPermissionCode('service.wallet.manage')
);
return [
@@ -139,15 +138,16 @@ final class TransferOrderListController extends Controller
'amount_formatted' => CurrencyFormatter::fromMinor($amount),
'idempotent_key' => $o->idempotent_key,
'status' => $o->status,
'can_reverse' => $canWriteWallet && $o->status === 'pending_reconcile',
'can_reverse' => $canWriteWallet
&& $o->status === 'pending_reconcile'
&& ($o->direction === 'out' || $this->transferService->isEligibleForTransferInReverse($o)),
'can_complete_credit' => $canWriteWallet
&& $o->direction === 'in'
&& $o->status === 'pending_reconcile'
&& $o->fail_reason === 'lottery_credit_failed'
&& trim((string) $o->external_ref_no) !== '',
'can_manually_process' => $canWriteWallet
&& in_array($o->status, ['processing', 'failed', 'pending_reconcile'], true)
&& ! ($o->direction === 'out' && $o->status === 'pending_reconcile'),
&& $this->transferService->isEligibleForManualProcess($o),
'external_ref_no' => $o->external_ref_no,
'external_request_payload' => $o->external_request_payload,
'external_response_payload' => $o->external_response_payload,

View File

@@ -6,6 +6,7 @@ use App\Lottery\ErrorCode;
use App\Models\TransferOrder;
use App\Support\ApiMessage;
use App\Support\ApiResponse;
use App\Support\AdminScopePolicy;
use App\Support\LotteryMessage;
use App\Exceptions\WalletOperationException;
use App\Services\Wallet\LotteryTransferService;
@@ -16,8 +17,8 @@ use App\Http\Requests\Admin\Wallet\TransferOrderCompleteCreditRequest;
use Illuminate\Http\JsonResponse;
/**
* 后台:转账订单对账操作(冲正 / 人工处理)。
* PRD §12待对账 -> 已冲正 / 人工处理
* 后台:转账订单对账操作(冲正 / 补入账 / 标记结案)。
* PRD §12待对账 -> 已冲正 / 结案manually_processed仅改状态
*/
final class TransferOrderReconcileController extends Controller
{
@@ -27,10 +28,16 @@ final class TransferOrderReconcileController extends Controller
public function reverse(TransferOrderReverseRequest $request, string $transferNo): JsonResponse
{
$admin = $request->lotteryAdmin();
abort_if($admin === null, 401);
$order = TransferOrder::query()->where('transfer_no', $transferNo)->first();
if ($order === null) {
return ApiMessage::errorResponse($request, 'wallet.order_not_found', ErrorCode::ClientHttpError->value, null, 404);
}
if (! AdminScopePolicy::transferOrderAccessible($admin, $order)) {
return ApiMessage::errorResponse($request, 'admin.site_player_access_denied', ErrorCode::AdminForbidden->value, null, 403);
}
try {
$this->transferService->reconcileTransferOrder(
@@ -52,10 +59,16 @@ final class TransferOrderReconcileController extends Controller
public function manuallyProcess(TransferOrderManuallyProcessRequest $request, string $transferNo): JsonResponse
{
$admin = $request->lotteryAdmin();
abort_if($admin === null, 401);
$order = TransferOrder::query()->where('transfer_no', $transferNo)->first();
if ($order === null) {
return ApiMessage::errorResponse($request, 'wallet.order_not_found', ErrorCode::ClientHttpError->value, null, 404);
}
if (! AdminScopePolicy::transferOrderAccessible($admin, $order)) {
return ApiMessage::errorResponse($request, 'admin.site_player_access_denied', ErrorCode::AdminForbidden->value, null, 403);
}
try {
$this->transferService->reconcileTransferOrder(
@@ -77,10 +90,16 @@ final class TransferOrderReconcileController extends Controller
public function completeCredit(TransferOrderCompleteCreditRequest $request, string $transferNo): JsonResponse
{
$admin = $request->lotteryAdmin();
abort_if($admin === null, 401);
$order = TransferOrder::query()->where('transfer_no', $transferNo)->first();
if ($order === null) {
return ApiMessage::errorResponse($request, 'wallet.order_not_found', ErrorCode::ClientHttpError->value, null, 404);
}
if (! AdminScopePolicy::transferOrderAccessible($admin, $order)) {
return ApiMessage::errorResponse($request, 'admin.site_player_access_denied', ErrorCode::AdminForbidden->value, null, 403);
}
try {
$this->transferService->reconcileTransferOrder(

View File

@@ -6,7 +6,7 @@ use App\Models\WalletTxn;
use App\Support\ApiResponse;
use App\Support\PaginationTrait;
use Illuminate\Http\JsonResponse;
use App\Support\AdminSiteScope;
use App\Support\AdminScopePolicy;
use App\Support\AgentNodeApiPresenter;
use App\Support\CurrencyFormatter;
use App\Http\Controllers\Controller;
@@ -39,6 +39,7 @@ final class WalletTransactionListController extends Controller
abort_if($admin === null, 401);
$validated = $request->validated();
$scope = AdminScopePolicy::resolveContext($request, $admin);
$perPage = $this->perPage($request, 'per_page', 10, 100);
$page = $this->page($request);
@@ -92,15 +93,7 @@ final class WalletTransactionListController extends Controller
}
}
$agentNodeId = isset($validated['agent_node_id']) ? (int) $validated['agent_node_id'] : null;
AdminSiteScope::applyViaPlayerRelationWithSiteCode(
$query,
$admin,
is_string($validated['site_code'] ?? null) ? $validated['site_code'] : null,
'player',
$agentNodeId > 0 ? $agentNodeId : null,
);
AdminScopePolicy::applyViaPlayerRelationWithContext($query, $scope, 'player');
$paginator = $query->paginate($perPage, ['*'], 'page', $page);
$items = $paginator->getCollection()->map(fn (WalletTxn $t) => $this->formatRow($t));