refactor: 更新权限管理与请求验证逻辑
- 在多个控制器中将权限检查从 hasAdminPermission 更新为 hasPermissionCode,以增强权限管理的灵活性。 - 引入 AdminScopePolicy,优化基于代理节点的权限和数据过滤逻辑,确保管理员能够更精确地控制访问权限。 - 在请求验证中添加 agent_node_id 字段,确保 API 接口支持代理节点的相关操作。 - 更新 AdminUser 模型,新增 hasPermissionCode 方法,以支持更细粒度的权限检查。 - 优化审计日志记录逻辑,确保在处理请求时能够准确记录管理员的操作。
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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']);
|
||||
|
||||
|
||||
@@ -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, '%_\\').'%';
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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: ['*']);
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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([
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -60,9 +60,36 @@ final class RecordAdminApiAudit
|
||||
return $response;
|
||||
}
|
||||
|
||||
private static function isAuditAlreadyRecorded(Request $request): bool
|
||||
{
|
||||
foreach (self::auditRecordedRequests($request) as $candidate) {
|
||||
if ($candidate->attributes->get(self::ATTRIBUTE_AUDIT_RECORDED) === true) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @return list<Request> */
|
||||
private static function auditRecordedRequests(Request $request): array
|
||||
{
|
||||
$requests = [$request];
|
||||
|
||||
try {
|
||||
$resolved = request();
|
||||
if ($resolved !== $request) {
|
||||
$requests[] = $resolved;
|
||||
}
|
||||
} catch (\Throwable) {
|
||||
}
|
||||
|
||||
return $requests;
|
||||
}
|
||||
|
||||
private function shouldRecord(Request $request, Response $response): bool
|
||||
{
|
||||
if ($request->attributes->get(self::ATTRIBUTE_AUDIT_RECORDED) === true) {
|
||||
if (self::isAuditAlreadyRecorded($request)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,31 @@
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
final class AdminSettingBatchUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
$admin = $this->lotteryAdmin();
|
||||
if (! $admin instanceof AdminUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/** @var list<array{key?: mixed}>|null $items */
|
||||
$items = $this->input('items');
|
||||
if (! is_array($items)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach ($items as $item) {
|
||||
$key = is_array($item) ? (string) ($item['key'] ?? '') : '';
|
||||
if (str_starts_with($key, 'settlement.')) {
|
||||
return $admin->hasAdminPermission('prd.payout.manage');
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,12 +2,23 @@
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
final class AdminSettingUpdateRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
$admin = $this->lotteryAdmin();
|
||||
if (! $admin instanceof AdminUser) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$key = (string) $this->route('key', '');
|
||||
if (str_starts_with($key, 'settlement.')) {
|
||||
return $admin->hasAdminPermission('prd.payout.manage');
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@ final class DashboardAnalyticsRequest extends FormRequest
|
||||
'date_to' => ['nullable', 'date_format:Y-m-d', 'required_if:period,custom', 'after_or_equal:date_from'],
|
||||
'metric' => ['sometimes', 'string', Rule::in(['overview', 'bet', 'payout', 'profit'])],
|
||||
'play_code' => ['nullable', 'string', 'max:64'],
|
||||
'site_code' => ['nullable', 'string', 'max:32'],
|
||||
'agent_node_id' => ['nullable', 'integer', 'min:1'],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -40,6 +42,8 @@ final class DashboardAnalyticsRequest extends FormRequest
|
||||
'date_to' => $this->input('date_to'),
|
||||
'metric' => (string) $this->input('metric', 'overview'),
|
||||
'play_code' => $this->input('play_code'),
|
||||
'site_code' => $this->input('site_code'),
|
||||
'agent_node_id' => $this->integer('agent_node_id') ?: null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Requests\Admin;
|
||||
|
||||
use Illuminate\Foundation\Http\FormRequest;
|
||||
|
||||
final class SettlementPayoutAdjustmentRequest extends FormRequest
|
||||
{
|
||||
public function authorize(): bool
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
public function rules(): array
|
||||
{
|
||||
return [
|
||||
'player_id' => ['required', 'integer', 'min:1'],
|
||||
'amount_delta' => ['required', 'integer', 'not_in:0'],
|
||||
'reason' => ['required', 'string', 'min:3', 'max:500'],
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user