feat: enhance admin role management and reconciliation features
- Updated AGENTS.md to clarify site admin roles and their associated permissions. - Refactored reconciliation controllers to include admin user validation and improved access control based on admin roles. - Enhanced AdminReconcileJobService to support player-specific reconciliation and site-based job creation. - Removed deprecated rebate commission report functionality from the API and related services. - Improved dashboard overview builders to accommodate new site operator roles and their specific functionalities.
This commit is contained in:
@@ -2,12 +2,15 @@
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\Reconcile;
|
||||
|
||||
use App\Models\AdminSite;
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\TransferOrder;
|
||||
use App\Models\ReconcileJob;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\ReconcileItem;
|
||||
use App\Support\AdminAgentScope;
|
||||
use App\Support\AdminApiList;
|
||||
use App\Support\AdminReconcileScope;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Support\AdminTransferOrderCapabilities;
|
||||
@@ -28,11 +31,27 @@ final class ReconcileItemIndexController extends Controller
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
if ($denied = AdminReconcileScope::denyUnlessJobAccessible($admin, $reconcile_job)) {
|
||||
return $denied;
|
||||
}
|
||||
|
||||
$p = AdminApiList::readPaging($request, 50, 200);
|
||||
|
||||
$paginator = $reconcile_job->items()
|
||||
->orderBy('id')
|
||||
->paginate($p['perPage'], ['*'], 'page', $p['page']);
|
||||
$itemsQuery = $reconcile_job->items()->orderBy('id');
|
||||
if (! $admin->isSuperAdmin()) {
|
||||
$siteIds = $admin->accessibleAdminSiteIds() ?? [];
|
||||
$siteCodes = $siteIds === []
|
||||
? []
|
||||
: AdminSite::query()
|
||||
->whereIn('id', $siteIds)
|
||||
->pluck('code')
|
||||
->map(static fn ($code): string => (string) $code)
|
||||
->all();
|
||||
$agent = AdminAgentScope::primaryAgentNode($admin);
|
||||
AdminReconcileScope::applyItemsTransferPlayerScope($itemsQuery, $siteCodes, $agent);
|
||||
}
|
||||
|
||||
$paginator = $itemsQuery->paginate($p['perPage'], ['*'], 'page', $p['page']);
|
||||
|
||||
$transferNos = collect($paginator->items())
|
||||
->map(fn (ReconcileItem $item) => $item->side_a_ref)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
namespace App\Http\Controllers\Api\V1\Admin\Reconcile;
|
||||
|
||||
use App\Models\ReconcileJob;
|
||||
use App\Support\AdminReconcileScope;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Support\AdminApiList;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
@@ -13,6 +14,9 @@ final class ReconcileJobIndexController extends Controller
|
||||
{
|
||||
public function __invoke(Request $request): JsonResponse
|
||||
{
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
$p = AdminApiList::readPaging($request);
|
||||
$type = trim((string) $request->query('reconcile_type', ''));
|
||||
|
||||
@@ -21,6 +25,8 @@ final class ReconcileJobIndexController extends Controller
|
||||
$q->where('reconcile_type', $type);
|
||||
}
|
||||
|
||||
AdminReconcileScope::applyToJobsQuery($q, $admin);
|
||||
|
||||
$paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']);
|
||||
|
||||
return AdminApiList::json($paginator, fn (ReconcileJob $j) => $this->row($j));
|
||||
|
||||
@@ -3,15 +3,24 @@
|
||||
namespace App\Http\Controllers\Api\V1\Admin\Reconcile;
|
||||
|
||||
use App\Models\ReconcileJob;
|
||||
use App\Support\AdminReconcileScope;
|
||||
use App\Support\ApiResponse;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Http\Controllers\Controller;
|
||||
|
||||
/** GET /api/v1/admin/reconcile-jobs/{reconcile_job} */
|
||||
final class ReconcileJobShowController extends Controller
|
||||
{
|
||||
public function __invoke(ReconcileJob $reconcile_job): JsonResponse
|
||||
public function __invoke(Request $request, ReconcileJob $reconcile_job): JsonResponse
|
||||
{
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
if ($denied = AdminReconcileScope::denyUnlessJobAccessible($admin, $reconcile_job)) {
|
||||
return $denied;
|
||||
}
|
||||
|
||||
return ApiResponse::success([
|
||||
'id' => (int) $reconcile_job->id,
|
||||
'job_no' => $reconcile_job->job_no,
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\Reports;
|
||||
|
||||
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 */
|
||||
final class AdminReportRebateCommissionController extends Controller
|
||||
{
|
||||
public function __invoke(AdminReportQueryRequest $request, AdminReportQueryService $service): JsonResponse
|
||||
{
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
$validated = $request->validated();
|
||||
$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,
|
||||
$range['date_from'],
|
||||
$range['date_to'],
|
||||
$p['page'],
|
||||
$p['perPage'],
|
||||
$scope,
|
||||
);
|
||||
|
||||
return AdminApiList::jsonWith($paginator, static function (object $row): array {
|
||||
return [
|
||||
'play_code' => (string) $row->play_code,
|
||||
'total_rebate_minor' => (int) $row->total_rebate_minor,
|
||||
'order_count' => (int) $row->order_count,
|
||||
'ticket_item_count' => (int) $row->ticket_item_count,
|
||||
];
|
||||
}, [
|
||||
'currency_code' => $service->resolvePeriodCurrencyCode($range['date_from'], $range['date_to'], $scope),
|
||||
'disclaimer' => 'wallet_instant_rebate_not_agent_period_settlement',
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -48,7 +48,6 @@ final class ReportJobStoreRequest extends ApiFormRequest
|
||||
'hot_number_risk_report',
|
||||
'play_dimension_report',
|
||||
'sold_out_number_report',
|
||||
'rebate_commission_report',
|
||||
'audit_operation_report',
|
||||
'wallet_txns_daily',
|
||||
'transfer_orders_daily',
|
||||
|
||||
@@ -13,6 +13,7 @@ final class ReconcileJob extends Model
|
||||
protected $fillable = [
|
||||
'job_no',
|
||||
'admin_user_id',
|
||||
'admin_site_id',
|
||||
'reconcile_type',
|
||||
'status',
|
||||
'period_start',
|
||||
|
||||
@@ -17,7 +17,7 @@ use App\Support\AdminDataScope;
|
||||
use App\Support\AdminScopeContext;
|
||||
use App\Support\AdminAgentScope;
|
||||
use App\Support\AdminScopeContextResolver;
|
||||
use App\Support\SitePlatformRole;
|
||||
use App\Support\SiteOperatorRoles;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
|
||||
/**
|
||||
@@ -32,6 +32,8 @@ final class AdminDashboardSnapshotBuilder
|
||||
private readonly AdminReportQueryService $reportQuery,
|
||||
private readonly AgentDashboardOverviewBuilder $agentOverview,
|
||||
private readonly SiteDashboardOverviewBuilder $siteOverview,
|
||||
private readonly SiteFinanceDashboardOverviewBuilder $siteFinanceOverview,
|
||||
private readonly SiteCsDashboardOverviewBuilder $siteCsOverview,
|
||||
) {}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
@@ -60,12 +62,19 @@ final class AdminDashboardSnapshotBuilder
|
||||
],
|
||||
'agent_overview' => null,
|
||||
'site_overview' => null,
|
||||
'site_finance_overview' => null,
|
||||
'site_cs_overview' => null,
|
||||
];
|
||||
|
||||
if ($admin->primaryAgentNode() !== null) {
|
||||
$out['agent_overview'] = $this->agentOverview->build($admin);
|
||||
} elseif (SitePlatformRole::userHasSiteAdminRole($admin)) {
|
||||
$out['site_overview'] = $this->siteOverview->build($admin, $scope);
|
||||
} elseif (SiteOperatorRoles::userHasSiteOperatorRole($admin)) {
|
||||
$operatorSlug = SiteOperatorRoles::primarySiteOperatorSlug($admin);
|
||||
match ($operatorSlug) {
|
||||
SiteOperatorRoles::SLUG_SITE_FINANCE => $out['site_finance_overview'] = $this->siteFinanceOverview->build($admin, $scope),
|
||||
SiteOperatorRoles::SLUG_SITE_CS => $out['site_cs_overview'] = $this->siteCsOverview->build($admin),
|
||||
default => $out['site_overview'] = $this->siteOverview->build($admin, $scope),
|
||||
};
|
||||
}
|
||||
|
||||
if ($canDraw) {
|
||||
|
||||
@@ -4,11 +4,17 @@ namespace App\Services\Admin;
|
||||
|
||||
use Carbon\Carbon;
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\Player;
|
||||
use Illuminate\Support\Str;
|
||||
use App\Models\ReconcileJob;
|
||||
use Illuminate\Http\Request;
|
||||
use App\Models\ReconcileItem;
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Services\AuditLogger;
|
||||
use App\Support\AdminReconcileScope;
|
||||
use App\Support\AdminScopePolicy;
|
||||
use App\Support\AdminSiteScope;
|
||||
use App\Support\ApiMessage;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use App\Services\Wallet\WalletTransferReconcileDetector;
|
||||
|
||||
@@ -102,6 +108,23 @@ final class AdminReconcileJobService
|
||||
$periodEnd ??= now()->endOfDay();
|
||||
$periodStart ??= $periodEnd->copy()->startOfDay();
|
||||
|
||||
$player = null;
|
||||
if ($playerId !== null) {
|
||||
$player = Player::query()->find($playerId);
|
||||
if ($player === null || ! AdminSiteScope::playerAccessible($admin, $player)) {
|
||||
abort(ApiMessage::errorResponse(
|
||||
$request,
|
||||
'admin.site_player_access_denied',
|
||||
ErrorCode::AdminForbidden->value,
|
||||
null,
|
||||
403,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
$scope = AdminScopePolicy::resolveContext($request, $admin);
|
||||
$adminSiteId = AdminReconcileScope::resolveAdminSiteIdForJob($admin, $player);
|
||||
|
||||
[$items] = $this->detector->scanItems(
|
||||
$periodStart,
|
||||
$periodEnd,
|
||||
@@ -109,9 +132,17 @@ final class AdminReconcileJobService
|
||||
staleMinutes: 15,
|
||||
playerId: $playerId,
|
||||
includeMainSiteCheck: true,
|
||||
scope: $scope,
|
||||
);
|
||||
|
||||
$job = $this->detector->persistJob($items, $periodStart, $periodEnd, (int) $admin->getKey());
|
||||
$job = $this->detector->persistJob(
|
||||
$items,
|
||||
$periodStart,
|
||||
$periodEnd,
|
||||
(int) $admin->getKey(),
|
||||
$adminSiteId,
|
||||
$playerId,
|
||||
);
|
||||
|
||||
AuditLogger::recordForAdmin(
|
||||
$admin,
|
||||
|
||||
@@ -478,19 +478,6 @@ final class AdminReportQueryService
|
||||
return $query->paginate($perPage, ['*'], 'page', $page);
|
||||
}
|
||||
|
||||
public function rebateCommissionPaginated(
|
||||
?string $playCode,
|
||||
string $dateFrom,
|
||||
string $dateTo,
|
||||
int $page,
|
||||
int $perPage,
|
||||
AdminUser|AdminScopeContext|null $scope = null,
|
||||
): LengthAwarePaginator {
|
||||
$query = $this->rebateCommissionBaseQuery($playCode, $dateFrom, $dateTo, $scope);
|
||||
|
||||
return $query->paginate($perPage, ['*'], 'page', $page);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<int, string|int|float|null>>
|
||||
*/
|
||||
@@ -505,7 +492,6 @@ final class AdminReportQueryService
|
||||
'daily_profit_summary' => $this->dailyProfitExportRows($dateFrom, $dateTo, $scope),
|
||||
'player_win_loss' => $this->playerWinLossExportRows($filterJson, $dateFrom, $dateTo, $scope),
|
||||
'play_dimension_report' => $this->playDimensionExportRows($filterJson, $dateFrom, $dateTo, $scope),
|
||||
'rebate_commission_report' => $this->rebateCommissionExportRows($filterJson, $dateFrom, $dateTo, $scope),
|
||||
'audit_operation_report' => $this->auditExportRows($filterJson, $dateFrom, $dateTo),
|
||||
'wallet_transfer_report', 'transfer_orders_daily' => $this->transferOrdersExportRows($filterJson, $dateFrom, $dateTo, $scope),
|
||||
'wallet_txns_daily' => $this->walletTxnsExportRows($filterJson, $dateFrom, $dateTo, $scope),
|
||||
@@ -546,7 +532,6 @@ final class AdminReportQueryService
|
||||
'hot_number_risk_report' => '热门号码风险报表',
|
||||
'play_dimension_report' => '玩法维度报表',
|
||||
'sold_out_number_report' => '售罄号码报表',
|
||||
'rebate_commission_report' => '佣金回水报表',
|
||||
'audit_operation_report' => '后台操作审计报表',
|
||||
default => $reportType,
|
||||
};
|
||||
@@ -618,28 +603,6 @@ final class AdminReportQueryService
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<int, string|int|float|null>>
|
||||
*/
|
||||
private function rebateCommissionExportRows(?array $filterJson, string $dateFrom, string $dateTo, AdminUser|AdminScopeContext|null $scope = null): array
|
||||
{
|
||||
$playCode = isset($filterJson['play_code']) ? trim((string) $filterJson['play_code']) : null;
|
||||
$rows = [
|
||||
['玩法', '回水', '订单数', '注单数'],
|
||||
];
|
||||
$items = $this->rebateCommissionBaseQuery($playCode !== '' ? $playCode : null, $dateFrom, $dateTo, $scope)->get();
|
||||
foreach ($items as $row) {
|
||||
$rows[] = [
|
||||
(string) $row->play_code,
|
||||
(int) $row->total_rebate_minor,
|
||||
(int) $row->order_count,
|
||||
(int) $row->ticket_item_count,
|
||||
];
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<int, string|int|float|null>>
|
||||
*/
|
||||
@@ -739,31 +702,6 @@ final class AdminReportQueryService
|
||||
return $query;
|
||||
}
|
||||
|
||||
/** @return \Illuminate\Database\Query\Builder */
|
||||
private function rebateCommissionBaseQuery(?string $playCode, string $dateFrom, string $dateTo, AdminUser|AdminScopeContext|null $scope = null)
|
||||
{
|
||||
$context = $this->normalizeScope($scope);
|
||||
$query = DB::table('ticket_items as ti')
|
||||
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
|
||||
->join('draws as d', 'd.id', '=', 'o.draw_id')
|
||||
->selectRaw('ti.play_code')
|
||||
->selectRaw('SUM(ti.total_bet_amount - ti.actual_deduct_amount) as total_rebate_minor')
|
||||
->selectRaw('COUNT(DISTINCT o.id) as order_count')
|
||||
->selectRaw('COUNT(ti.id) as ticket_item_count')
|
||||
->whereDate('d.business_date', '>=', $dateFrom)
|
||||
->whereDate('d.business_date', '<=', $dateTo)
|
||||
->groupBy('ti.play_code')
|
||||
->orderBy('ti.play_code');
|
||||
|
||||
if ($playCode !== null && $playCode !== '') {
|
||||
$query->where('ti.play_code', $playCode);
|
||||
}
|
||||
|
||||
AdminDataScope::applyToTicketOrdersViaPlayer($query, $context?->admin, 'o');
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array<int, string|int|float|null>>
|
||||
*/
|
||||
|
||||
62
app/Services/Admin/SiteCsDashboardOverviewBuilder.php
Normal file
62
app/Services/Admin/SiteCsDashboardOverviewBuilder.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin;
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\Player;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/** 站点客服仪表盘:玩家规模与今日注单活跃度,不含全局盈亏。 */
|
||||
final class SiteCsDashboardOverviewBuilder
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function build(AdminUser $admin): ?array
|
||||
{
|
||||
if (! $admin->hasPermissionCode('dashboard.view')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$site = SiteDashboardSiteResolver::resolvePrimarySite($admin);
|
||||
if ($site === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$siteId = (int) $site->id;
|
||||
$siteCode = (string) $site->code;
|
||||
$today = now()->toDateString();
|
||||
$activity = $this->todayTicketStats($siteCode, $today);
|
||||
|
||||
return [
|
||||
'admin_site_id' => $siteId,
|
||||
'site_code' => $siteCode,
|
||||
'site_name' => (string) $site->name,
|
||||
'player_count' => (int) Player::query()->where('site_code', $siteCode)->count(),
|
||||
'ticket_order_count_today' => $activity['order_count'],
|
||||
'active_player_count_today' => $activity['player_count'],
|
||||
'latest_ticket_at' => $activity['latest_at'],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{player_count: int, order_count: int, latest_at: ?string}
|
||||
*/
|
||||
private function todayTicketStats(string $siteCode, string $today): array
|
||||
{
|
||||
$base = DB::table('ticket_orders as o')
|
||||
->join('draws as d', 'd.id', '=', 'o.draw_id')
|
||||
->join('players as p', 'p.id', '=', 'o.player_id')
|
||||
->where('p.site_code', $siteCode)
|
||||
->whereDate('d.business_date', $today);
|
||||
|
||||
$latestAt = (clone $base)->max('o.created_at');
|
||||
|
||||
return [
|
||||
'player_count' => (int) (clone $base)->distinct('o.player_id')->count('o.player_id'),
|
||||
'order_count' => (int) (clone $base)->count(),
|
||||
'latest_at' => $latestAt !== null ? Carbon::parse((string) $latestAt)->toIso8601String() : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
namespace App\Services\Admin;
|
||||
|
||||
use App\Models\AdminSite;
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\AgentNode;
|
||||
use App\Models\Player;
|
||||
@@ -27,7 +26,7 @@ final class SiteDashboardOverviewBuilder
|
||||
return null;
|
||||
}
|
||||
|
||||
$site = $this->resolvePrimarySite($admin);
|
||||
$site = SiteDashboardSiteResolver::resolvePrimarySite($admin);
|
||||
if ($site === null) {
|
||||
return null;
|
||||
}
|
||||
@@ -52,7 +51,11 @@ final class SiteDashboardOverviewBuilder
|
||||
$currencyCode = $this->reportQuery->resolvePeriodCurrencyCode($today, $today, $scoped)
|
||||
?? $this->reportQuery->resolvePeriodCurrencyCode($sevenDayFrom, $today, $scoped);
|
||||
$todayActivity = $this->todayActivityStats($siteCode, $today);
|
||||
$pendingBills = $this->pendingBillStats($siteId);
|
||||
$billStats = SiteSettlementBillStats::forSite($siteId);
|
||||
$pendingBills = [
|
||||
'count' => $billStats['pending_confirm_count'] + $billStats['payable_count'],
|
||||
'unpaid_minor' => $billStats['payable_unpaid_minor'],
|
||||
];
|
||||
$topAgentToday = $this->topAgentToday($scoped, $today);
|
||||
|
||||
return [
|
||||
@@ -78,18 +81,6 @@ final class SiteDashboardOverviewBuilder
|
||||
];
|
||||
}
|
||||
|
||||
private function resolvePrimarySite(AdminUser $admin): ?AdminSite
|
||||
{
|
||||
$siteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($siteIds === null || $siteIds === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return AdminSite::query()
|
||||
->where('id', (int) $siteIds[0])
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{player_count: int, order_count: int, latest_bet_at: ?string}
|
||||
*/
|
||||
@@ -110,22 +101,6 @@ final class SiteDashboardOverviewBuilder
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{count: int, unpaid_minor: int}
|
||||
*/
|
||||
private function pendingBillStats(int $siteId): array
|
||||
{
|
||||
$query = DB::table('settlement_bills as sb')
|
||||
->join('settlement_periods as sp', 'sp.id', '=', 'sb.settlement_period_id')
|
||||
->where('sp.admin_site_id', $siteId)
|
||||
->whereIn('sb.status', ['pending', 'pending_confirm', 'partial']);
|
||||
|
||||
return [
|
||||
'count' => (int) $query->count(),
|
||||
'unpaid_minor' => (int) $query->sum('sb.unpaid_amount'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
|
||||
22
app/Services/Admin/SiteDashboardSiteResolver.php
Normal file
22
app/Services/Admin/SiteDashboardSiteResolver.php
Normal file
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin;
|
||||
|
||||
use App\Models\AdminSite;
|
||||
use App\Models\AdminUser;
|
||||
|
||||
/** 站点运营仪表盘:解析账号绑定的主接入站点。 */
|
||||
final class SiteDashboardSiteResolver
|
||||
{
|
||||
public static function resolvePrimarySite(AdminUser $admin): ?AdminSite
|
||||
{
|
||||
$siteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($siteIds === null || $siteIds === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return AdminSite::query()
|
||||
->where('id', (int) $siteIds[0])
|
||||
->first();
|
||||
}
|
||||
}
|
||||
92
app/Services/Admin/SiteFinanceDashboardOverviewBuilder.php
Normal file
92
app/Services/Admin/SiteFinanceDashboardOverviewBuilder.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin;
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\Player;
|
||||
use App\Models\TransferOrder;
|
||||
use App\Support\AdminAgentScope;
|
||||
use App\Support\AdminDataScope;
|
||||
use App\Support\AdminScopeContext;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
|
||||
/** 站点财务仪表盘:对账异常、待确认/待收付账单、钱包玩家规模。 */
|
||||
final class SiteFinanceDashboardOverviewBuilder
|
||||
{
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function build(AdminUser $admin, AdminScopeContext $scope): ?array
|
||||
{
|
||||
if (! $admin->hasPermissionCode('dashboard.view')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$site = SiteDashboardSiteResolver::resolvePrimarySite($admin);
|
||||
if ($site === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$siteId = (int) $site->id;
|
||||
$siteCode = (string) $site->code;
|
||||
$billStats = SiteSettlementBillStats::forSite($siteId);
|
||||
$currencyCode = Player::query()
|
||||
->where('site_code', $siteCode)
|
||||
->whereNotNull('default_currency')
|
||||
->value('default_currency');
|
||||
|
||||
return [
|
||||
'admin_site_id' => $siteId,
|
||||
'site_code' => $siteCode,
|
||||
'site_name' => (string) $site->name,
|
||||
'wallet_player_count' => (int) Player::query()
|
||||
->where('site_code', $siteCode)
|
||||
->where('funding_mode', 'wallet')
|
||||
->count(),
|
||||
'credit_player_count' => (int) Player::query()
|
||||
->where('site_code', $siteCode)
|
||||
->where('funding_mode', 'credit')
|
||||
->count(),
|
||||
'pending_confirm_bill_count' => $billStats['pending_confirm_count'],
|
||||
'payable_bill_count' => $billStats['payable_count'],
|
||||
'payable_unpaid_minor' => $billStats['payable_unpaid_minor'],
|
||||
'pending_bill_count' => $billStats['pending_confirm_count'] + $billStats['payable_count'],
|
||||
'pending_unpaid_minor' => $billStats['payable_unpaid_minor'],
|
||||
'abnormal_transfer_count' => $this->abnormalTransferCount($admin, $scope),
|
||||
'currency_code' => is_string($currencyCode) && $currencyCode !== '' ? $currencyCode : null,
|
||||
];
|
||||
}
|
||||
|
||||
private function abnormalTransferCount(AdminUser $admin, AdminScopeContext $scope): int
|
||||
{
|
||||
$query = TransferOrder::query()
|
||||
->whereIn('status', ['processing', 'failed', 'pending_reconcile']);
|
||||
AdminDataScope::applyEloquentViaPlayer($query, $admin);
|
||||
$this->applyRequestedScopeViaPlayer($query, $scope);
|
||||
|
||||
return (int) $query->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param EloquentBuilder<mixed> $query
|
||||
*/
|
||||
private function applyRequestedScopeViaPlayer(EloquentBuilder $query, AdminScopeContext $scope): void
|
||||
{
|
||||
$siteCode = $scope->effectiveRequestedSiteCode();
|
||||
$agentNodeId = $scope->effectiveRequestedAgentNodeId();
|
||||
if ($siteCode === null && $agentNodeId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$admin = $scope->admin;
|
||||
$query->whereHas('player', static function (EloquentBuilder $playerQuery) use ($admin, $siteCode, $agentNodeId): void {
|
||||
if ($siteCode !== null) {
|
||||
$playerQuery->where('site_code', $siteCode);
|
||||
}
|
||||
|
||||
if ($agentNodeId !== null) {
|
||||
AdminAgentScope::applyRequestedAgentNodeFilter($playerQuery, $admin, $agentNodeId);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
36
app/Services/Admin/SiteSettlementBillStats.php
Normal file
36
app/Services/Admin/SiteSettlementBillStats.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin;
|
||||
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/** 站点信用账期账单摘要(待确认 / 待收付)。 */
|
||||
final class SiteSettlementBillStats
|
||||
{
|
||||
/**
|
||||
* @return array{
|
||||
* pending_confirm_count: int,
|
||||
* payable_count: int,
|
||||
* payable_unpaid_minor: int
|
||||
* }
|
||||
*/
|
||||
public static function forSite(int $siteId): array
|
||||
{
|
||||
$pendingConfirmQuery = DB::table('settlement_bills as sb')
|
||||
->join('settlement_periods as sp', 'sp.id', '=', 'sb.settlement_period_id')
|
||||
->where('sp.admin_site_id', $siteId)
|
||||
->where('sb.status', 'pending_confirm');
|
||||
|
||||
$payableQuery = DB::table('settlement_bills as sb')
|
||||
->join('settlement_periods as sp', 'sp.id', '=', 'sb.settlement_period_id')
|
||||
->where('sp.admin_site_id', $siteId)
|
||||
->whereIn('sb.status', ['confirmed', 'partial_paid', 'overdue'])
|
||||
->where('sb.unpaid_amount', '>', 0);
|
||||
|
||||
return [
|
||||
'pending_confirm_count' => (int) (clone $pendingConfirmQuery)->count(),
|
||||
'payable_count' => (int) (clone $payableQuery)->count(),
|
||||
'payable_unpaid_minor' => (int) (clone $payableQuery)->sum('sb.unpaid_amount'),
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ use App\Models\WalletTxn;
|
||||
use App\Models\TransferOrder;
|
||||
use App\Models\ReconcileJob;
|
||||
use App\Models\ReconcileItem;
|
||||
use App\Support\AdminScopeContext;
|
||||
use App\Support\AdminScopePolicy;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
@@ -56,6 +58,7 @@ final class WalletTransferReconcileDetector
|
||||
int $staleMinutes = 15,
|
||||
?int $playerId = null,
|
||||
bool $includeMainSiteCheck = true,
|
||||
?AdminScopeContext $scope = null,
|
||||
): array {
|
||||
$limit = max(1, $limit);
|
||||
$staleMinutes = max(1, $staleMinutes);
|
||||
@@ -77,6 +80,10 @@ final class WalletTransferReconcileDetector
|
||||
$query->where('player_id', $playerId);
|
||||
}
|
||||
|
||||
if ($scope !== null && ! $scope->isSuperAdmin()) {
|
||||
AdminScopePolicy::applyViaPlayer($query, $scope);
|
||||
}
|
||||
|
||||
$orders = $query->get();
|
||||
|
||||
if ($orders->isEmpty()) {
|
||||
@@ -118,13 +125,16 @@ final class WalletTransferReconcileDetector
|
||||
Carbon $periodStart,
|
||||
Carbon $periodEnd,
|
||||
?int $adminUserId,
|
||||
?int $adminSiteId = null,
|
||||
?int $playerId = null,
|
||||
): ReconcileJob {
|
||||
return DB::transaction(function () use ($items, $periodStart, $periodEnd, $adminUserId): ReconcileJob {
|
||||
return DB::transaction(function () use ($items, $periodStart, $periodEnd, $adminUserId, $adminSiteId, $playerId): ReconcileJob {
|
||||
$jobNo = 'REC'.now()->format('YmdHis').strtoupper(str_replace('-', '', Str::uuid()->toString()));
|
||||
|
||||
$job = ReconcileJob::query()->create([
|
||||
'job_no' => $jobNo,
|
||||
'admin_user_id' => $adminUserId,
|
||||
'admin_site_id' => $adminSiteId,
|
||||
'reconcile_type' => self::RECONCILE_TYPE,
|
||||
'status' => 'completed',
|
||||
'period_start' => $periodStart,
|
||||
@@ -132,6 +142,7 @@ final class WalletTransferReconcileDetector
|
||||
'summary_json' => [
|
||||
'item_count' => count($items),
|
||||
'mismatch_count' => count($items),
|
||||
'player_id' => $playerId,
|
||||
],
|
||||
'finished_at' => now(),
|
||||
]);
|
||||
|
||||
@@ -40,6 +40,7 @@ final class AdminAuthProfile
|
||||
* },
|
||||
* site: ?array{id: int, code: string, name: string},
|
||||
* is_super_admin: bool,
|
||||
* account_kind: 'super_admin'|'site_admin'|'site_finance'|'site_cs'|'agent_operator'|'platform_account',
|
||||
* operational_permissions: list<string>,
|
||||
* delegation_ceiling: list<string>,
|
||||
* accessible_sites?: list<array{id: int, code: string, name: string}>
|
||||
@@ -49,6 +50,7 @@ final class AdminAuthProfile
|
||||
{
|
||||
$fresh = $admin->fresh();
|
||||
$permissionSlugs = $fresh->adminPermissionSlugs();
|
||||
$operationalCodes = $fresh->effectiveMenuActionPermissionCodes();
|
||||
$agent = self::agentContext($fresh);
|
||||
|
||||
$payload = [
|
||||
@@ -61,7 +63,8 @@ final class AdminAuthProfile
|
||||
'agent' => $agent,
|
||||
'site' => self::siteContext($fresh),
|
||||
'is_super_admin' => $fresh->isSuperAdmin(),
|
||||
'operational_permissions' => $permissionSlugs,
|
||||
'account_kind' => self::accountKind($fresh),
|
||||
'operational_permissions' => $operationalCodes,
|
||||
'delegation_ceiling' => AgentDelegationAuthorization::delegationLegacySlugsForAdminUser($fresh),
|
||||
];
|
||||
|
||||
@@ -81,7 +84,7 @@ final class AdminAuthProfile
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! SitePlatformRole::userHasSiteAdminRole($admin)) {
|
||||
if (! SiteOperatorRoles::userHasSiteOperatorRole($admin)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -130,4 +133,22 @@ final class AdminAuthProfile
|
||||
'can_create_player' => (bool) ($profile?->can_create_player ?? false),
|
||||
];
|
||||
}
|
||||
|
||||
private static function accountKind(AdminUser $admin): string
|
||||
{
|
||||
if ($admin->isSuperAdmin()) {
|
||||
return 'super_admin';
|
||||
}
|
||||
|
||||
if ($admin->hasPrimaryAgentBinding()) {
|
||||
return 'agent_operator';
|
||||
}
|
||||
|
||||
$siteRole = SiteOperatorRoles::primarySiteOperatorSlug($admin);
|
||||
if ($siteRole !== null) {
|
||||
return $siteRole;
|
||||
}
|
||||
|
||||
return 'platform_account';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -565,7 +565,6 @@ final class AdminAuthorizationRegistry
|
||||
['code' => 'admin.reports.daily-profit', 'module_code' => 'report', 'name' => '每日盈亏汇总', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reports/daily-profit', 'route_name' => 'api.v1.admin.reports.daily-profit', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.report.view']],
|
||||
['code' => 'admin.reports.player-win-loss', 'module_code' => 'report', 'name' => '玩家输赢报表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reports/player-win-loss', 'route_name' => 'api.v1.admin.reports.player-win-loss', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.report.view']],
|
||||
['code' => 'admin.reports.play-dimension', 'module_code' => 'report', 'name' => '玩法维度报表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reports/play-dimension', 'route_name' => 'api.v1.admin.reports.play-dimension', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.report.view']],
|
||||
['code' => 'admin.reports.rebate-commission', 'module_code' => 'report', 'name' => '佣金回水报表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/reports/rebate-commission', 'route_name' => 'api.v1.admin.reports.rebate-commission', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.report.view']],
|
||||
['code' => 'admin.report-jobs.index', 'module_code' => 'report', 'name' => '报表任务列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/report-jobs', 'route_name' => 'api.v1.admin.report-jobs.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.report.view']],
|
||||
['code' => 'admin.report-jobs.store', 'module_code' => 'report', 'name' => '创建报表任务', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/report-jobs', 'route_name' => 'api.v1.admin.report-jobs.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['service.report.export']],
|
||||
['code' => 'admin.report-jobs.show', 'module_code' => 'report', 'name' => '报表任务详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/report-jobs/{report_job}', 'route_name' => 'api.v1.admin.report-jobs.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['service.report.view', 'service.report.export']],
|
||||
|
||||
206
app/Support/AdminReconcileScope.php
Normal file
206
app/Support/AdminReconcileScope.php
Normal file
@@ -0,0 +1,206 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Lottery\ErrorCode;
|
||||
use App\Models\AdminSite;
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\AgentNode;
|
||||
use App\Models\Player;
|
||||
use App\Models\ReconcileJob;
|
||||
use App\Support\ApiMessage;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
|
||||
/** 对账任务:按接入站点(及绑定代理子树)收敛可见范围。 */
|
||||
final class AdminReconcileScope
|
||||
{
|
||||
/**
|
||||
* @param Builder<ReconcileJob> $query
|
||||
*/
|
||||
public static function applyToJobsQuery(Builder $query, AdminUser $admin): void
|
||||
{
|
||||
if ($admin->isSuperAdmin()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$siteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($siteIds === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($siteIds === []) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$siteCodes = AdminSite::query()
|
||||
->whereIn('id', $siteIds)
|
||||
->pluck('code')
|
||||
->map(static fn ($code): string => (string) $code)
|
||||
->all();
|
||||
|
||||
$agent = AdminAgentScope::primaryAgentNode($admin);
|
||||
|
||||
$query->where(function (Builder $outer) use ($siteIds, $siteCodes, $agent): void {
|
||||
$outer->whereIn('admin_site_id', $siteIds);
|
||||
|
||||
if ($siteCodes !== []) {
|
||||
$outer->orWhere(function (Builder $legacy) use ($siteCodes, $agent): void {
|
||||
$legacy->whereNull('admin_site_id')
|
||||
->whereHas('items', function (Builder $items) use ($siteCodes, $agent): void {
|
||||
self::applyItemsTransferPlayerScope($items, $siteCodes, $agent);
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
if ($agent instanceof AgentNode) {
|
||||
$query->whereHas('items', function (Builder $items) use ($agent): void {
|
||||
self::applyItemsAgentSubtree($items, $agent);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
public static function jobAccessible(AdminUser $admin, ReconcileJob $job): bool
|
||||
{
|
||||
if ($admin->isSuperAdmin()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$siteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($siteIds === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if ($siteIds === []) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$jobSiteId = $job->admin_site_id !== null ? (int) $job->admin_site_id : null;
|
||||
if ($jobSiteId !== null) {
|
||||
if (! in_array($jobSiteId, $siteIds, true)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
$siteCodes = AdminSite::query()
|
||||
->whereIn('id', $siteIds)
|
||||
->pluck('code')
|
||||
->map(static fn ($code): string => (string) $code)
|
||||
->all();
|
||||
|
||||
$hasScopedItem = $job->items()
|
||||
->where(function (Builder $items) use ($siteCodes, $admin): void {
|
||||
$agent = AdminAgentScope::primaryAgentNode($admin);
|
||||
self::applyItemsTransferPlayerScope($items, $siteCodes, $agent);
|
||||
})
|
||||
->exists();
|
||||
|
||||
if (! $hasScopedItem) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$agent = AdminAgentScope::primaryAgentNode($admin);
|
||||
if ($agent === null) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return $job->items()
|
||||
->where(function (Builder $items) use ($agent): void {
|
||||
self::applyItemsAgentSubtree($items, $agent);
|
||||
})
|
||||
->exists();
|
||||
}
|
||||
|
||||
public static function resolveAdminSiteIdForJob(AdminUser $admin, ?Player $player = null): ?int
|
||||
{
|
||||
if ($player !== null) {
|
||||
$siteId = AdminSite::query()
|
||||
->where('code', (string) $player->site_code)
|
||||
->value('id');
|
||||
|
||||
return $siteId !== null ? (int) $siteId : null;
|
||||
}
|
||||
|
||||
if ($admin->isSuperAdmin()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$agent = AdminAgentScope::primaryAgentNode($admin);
|
||||
if ($agent !== null) {
|
||||
return (int) $agent->admin_site_id;
|
||||
}
|
||||
|
||||
$siteIds = $admin->accessibleAdminSiteIds();
|
||||
|
||||
return $siteIds[0] ?? null;
|
||||
}
|
||||
|
||||
public static function denyUnlessJobAccessible(AdminUser $admin, ReconcileJob $job): ?JsonResponse
|
||||
{
|
||||
if (! self::jobAccessible($admin, $job)) {
|
||||
return ApiMessage::errorResponse(
|
||||
request(),
|
||||
'admin.reconcile_job_access_denied',
|
||||
ErrorCode::AdminForbidden->value,
|
||||
null,
|
||||
403,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<\App\Models\ReconcileItem> $query
|
||||
* @param list<string> $siteCodes
|
||||
*/
|
||||
public static function applyItemsTransferPlayerScope(
|
||||
Builder $query,
|
||||
array $siteCodes,
|
||||
?AgentNode $agent = null,
|
||||
): void {
|
||||
if ($siteCodes === []) {
|
||||
$query->whereRaw('0 = 1');
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$query->whereExists(function ($sub) use ($siteCodes, $agent): void {
|
||||
$sub->selectRaw('1')
|
||||
->from('transfer_orders')
|
||||
->join('players', 'players.id', '=', 'transfer_orders.player_id')
|
||||
->whereColumn('transfer_orders.transfer_no', 'reconcile_items.side_a_ref');
|
||||
|
||||
$sub->whereIn('players.site_code', $siteCodes);
|
||||
|
||||
if ($agent instanceof AgentNode) {
|
||||
$sub->whereIn('players.agent_node_id', function ($nodes) use ($agent): void {
|
||||
$nodes->select('id')
|
||||
->from('agent_nodes')
|
||||
->where('path', 'like', $agent->path.'%');
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder<\App\Models\ReconcileItem> $query
|
||||
*/
|
||||
private static function applyItemsAgentSubtree(Builder $query, AgentNode $agent): void
|
||||
{
|
||||
$query->whereExists(function ($sub) use ($agent): void {
|
||||
$sub->selectRaw('1')
|
||||
->from('transfer_orders')
|
||||
->join('players', 'players.id', '=', 'transfer_orders.player_id')
|
||||
->whereColumn('transfer_orders.transfer_no', 'reconcile_items.side_a_ref')
|
||||
->whereIn('players.agent_node_id', function ($nodes) use ($agent): void {
|
||||
$nodes->select('id')
|
||||
->from('agent_nodes')
|
||||
->where('path', 'like', $agent->path.'%');
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,19 +4,29 @@ namespace App\Support;
|
||||
|
||||
use App\Models\AdminRole;
|
||||
|
||||
/** 平台角色管理仅维护的两个内置系统角色。 */
|
||||
/** 平台内置系统角色(超管 / 站点运营 / 代理)。 */
|
||||
final class PlatformSystemRoles
|
||||
{
|
||||
public const SLUG_SUPER_ADMIN = AdminRole::ROLE_SUPER_ADMIN;
|
||||
|
||||
public const SLUG_AGENT = 'agent';
|
||||
|
||||
public const SLUG_SITE_ADMIN = SitePlatformRole::SLUG;
|
||||
public const SLUG_SITE_ADMIN = SiteOperatorRoles::SLUG_SITE_ADMIN;
|
||||
|
||||
public const SLUG_SITE_FINANCE = SiteOperatorRoles::SLUG_SITE_FINANCE;
|
||||
|
||||
public const SLUG_SITE_CS = SiteOperatorRoles::SLUG_SITE_CS;
|
||||
|
||||
/** @return list<string> */
|
||||
public static function fixedSlugs(): array
|
||||
{
|
||||
return [self::SLUG_SUPER_ADMIN, self::SLUG_SITE_ADMIN, self::SLUG_AGENT];
|
||||
return [
|
||||
self::SLUG_SUPER_ADMIN,
|
||||
self::SLUG_SITE_ADMIN,
|
||||
self::SLUG_SITE_FINANCE,
|
||||
self::SLUG_SITE_CS,
|
||||
self::SLUG_AGENT,
|
||||
];
|
||||
}
|
||||
|
||||
public static function isFixedSlug(string $slug): bool
|
||||
@@ -52,6 +62,8 @@ final class PlatformSystemRoles
|
||||
{
|
||||
self::ensureSuperAdminRole();
|
||||
SiteAdminDefaultRolePermissions::ensurePlatformSiteAdminRole();
|
||||
SiteFinanceDefaultRolePermissions::ensurePlatformSiteFinanceRole();
|
||||
SiteCsDefaultRolePermissions::ensurePlatformSiteCsRole();
|
||||
AgentDefaultRolePermissions::ensurePlatformAgentRole();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,9 +23,11 @@ final class SiteAdminDefaultRolePermissions
|
||||
'prd.users.manage',
|
||||
'prd.tickets.view',
|
||||
'prd.report.view',
|
||||
'prd.report.export',
|
||||
'prd.draw_result.view',
|
||||
'prd.wallet_reconcile.manage',
|
||||
'prd.settlement.agent.view',
|
||||
'prd.settlement.agent.manage',
|
||||
'prd.integration.view',
|
||||
];
|
||||
|
||||
/**
|
||||
@@ -46,7 +48,7 @@ final class SiteAdminDefaultRolePermissions
|
||||
[
|
||||
'code' => SitePlatformRole::SLUG,
|
||||
'name' => '站点管理员',
|
||||
'description' => '接入站点后台默认权限(代理/玩家/结算运营 + 站点仪表盘)',
|
||||
'description' => '接入站点后台默认权限(本站代理/玩家/信用结算 + 钱包流水与经营报表 + 期号只读)',
|
||||
'status' => 1,
|
||||
'is_system' => true,
|
||||
'sort_order' => 40,
|
||||
|
||||
49
app/Support/SiteCsDefaultRolePermissions.php
Normal file
49
app/Support/SiteCsDefaultRolePermissions.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminRole;
|
||||
|
||||
/** 平台「站点客服」内置角色:单玩家查询,不含全局经营报表与代理管理。 */
|
||||
final class SiteCsDefaultRolePermissions
|
||||
{
|
||||
/** @var list<string> */
|
||||
private const TEMPLATE_SLUGS = [
|
||||
'prd.dashboard.view',
|
||||
'prd.users.view_cs',
|
||||
'prd.tickets.view',
|
||||
'prd.wallet_reconcile.view_cs',
|
||||
];
|
||||
|
||||
/** @return list<string> */
|
||||
public static function templateSlugs(): array
|
||||
{
|
||||
return self::TEMPLATE_SLUGS;
|
||||
}
|
||||
|
||||
public static function ensurePlatformSiteCsRole(): AdminRole
|
||||
{
|
||||
$role = AdminRole::query()->updateOrCreate(
|
||||
[
|
||||
'slug' => SiteOperatorRoles::SLUG_SITE_CS,
|
||||
'scope_type' => AdminRole::SCOPE_SYSTEM,
|
||||
],
|
||||
[
|
||||
'code' => SiteOperatorRoles::SLUG_SITE_CS,
|
||||
'name' => '站点客服',
|
||||
'description' => '单站客服:玩家/注单/单用户钱包查询工作台,不含全局报表与代理经营',
|
||||
'status' => 1,
|
||||
'is_system' => true,
|
||||
'sort_order' => 42,
|
||||
'owner_agent_id' => null,
|
||||
'delegated_from_role_id' => null,
|
||||
],
|
||||
);
|
||||
|
||||
$role->syncLegacyPermissionSlugs(
|
||||
AdminPermissionInheritance::expand(self::TEMPLATE_SLUGS),
|
||||
);
|
||||
|
||||
return $role->fresh() ?? $role;
|
||||
}
|
||||
}
|
||||
55
app/Support/SiteFinanceDefaultRolePermissions.php
Normal file
55
app/Support/SiteFinanceDefaultRolePermissions.php
Normal file
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminRole;
|
||||
|
||||
/** 平台「站点财务」内置角色:对账、报表导出、结算收付,不含代理树经营。 */
|
||||
final class SiteFinanceDefaultRolePermissions
|
||||
{
|
||||
/** @var list<string> */
|
||||
private const TEMPLATE_SLUGS = [
|
||||
'prd.dashboard.view',
|
||||
'prd.draw_result.view',
|
||||
'prd.users.view_finance',
|
||||
'prd.tickets.view',
|
||||
'prd.report.view',
|
||||
'prd.report.export',
|
||||
'prd.wallet_reconcile.manage',
|
||||
'prd.wallet_adjust.manage',
|
||||
'prd.settlement.agent.view',
|
||||
'prd.settlement.agent.manage',
|
||||
];
|
||||
|
||||
/** @return list<string> */
|
||||
public static function templateSlugs(): array
|
||||
{
|
||||
return self::TEMPLATE_SLUGS;
|
||||
}
|
||||
|
||||
public static function ensurePlatformSiteFinanceRole(): AdminRole
|
||||
{
|
||||
$role = AdminRole::query()->updateOrCreate(
|
||||
[
|
||||
'slug' => SiteOperatorRoles::SLUG_SITE_FINANCE,
|
||||
'scope_type' => AdminRole::SCOPE_SYSTEM,
|
||||
],
|
||||
[
|
||||
'code' => SiteOperatorRoles::SLUG_SITE_FINANCE,
|
||||
'name' => '站点财务',
|
||||
'description' => '单站财务/对账:钱包流水、对账、经营报表导出、信用结算收付',
|
||||
'status' => 1,
|
||||
'is_system' => true,
|
||||
'sort_order' => 41,
|
||||
'owner_agent_id' => null,
|
||||
'delegated_from_role_id' => null,
|
||||
],
|
||||
);
|
||||
|
||||
$role->syncLegacyPermissionSlugs(
|
||||
AdminPermissionInheritance::expand(self::TEMPLATE_SLUGS),
|
||||
);
|
||||
|
||||
return $role->fresh() ?? $role;
|
||||
}
|
||||
}
|
||||
62
app/Support/SiteOperatorRoles.php
Normal file
62
app/Support/SiteOperatorRoles.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/** 接入站点平台账号内置角色(绑定 admin_user_site_roles,无代理节点)。 */
|
||||
final class SiteOperatorRoles
|
||||
{
|
||||
public const SLUG_SITE_ADMIN = 'site_admin';
|
||||
|
||||
public const SLUG_SITE_FINANCE = 'site_finance';
|
||||
|
||||
public const SLUG_SITE_CS = 'site_cs';
|
||||
|
||||
/** @return list<string> */
|
||||
public static function slugs(): array
|
||||
{
|
||||
return [
|
||||
self::SLUG_SITE_ADMIN,
|
||||
self::SLUG_SITE_FINANCE,
|
||||
self::SLUG_SITE_CS,
|
||||
];
|
||||
}
|
||||
|
||||
public static function isSiteOperatorSlug(string $slug): bool
|
||||
{
|
||||
return in_array($slug, self::slugs(), true);
|
||||
}
|
||||
|
||||
public static function userHasSiteOperatorRole(AdminUser $user): bool
|
||||
{
|
||||
if ($user->isSuperAdmin() || $user->hasPrimaryAgentBinding()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::primarySiteOperatorSlug($user) !== null;
|
||||
}
|
||||
|
||||
public static function userHasSiteAdminRole(AdminUser $user): bool
|
||||
{
|
||||
return self::primarySiteOperatorSlug($user) === self::SLUG_SITE_ADMIN;
|
||||
}
|
||||
|
||||
public static function primarySiteOperatorSlug(AdminUser $user): ?string
|
||||
{
|
||||
if ($user->isSuperAdmin() || $user->hasPrimaryAgentBinding()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$slug = DB::table('admin_user_site_roles as usr')
|
||||
->join('admin_roles as r', 'r.id', '=', 'usr.role_id')
|
||||
->where('usr.admin_user_id', $user->id)
|
||||
->whereIn('r.slug', self::slugs())
|
||||
->orderBy('usr.site_id')
|
||||
->orderBy('r.sort_order')
|
||||
->value('r.slug');
|
||||
|
||||
return is_string($slug) && $slug !== '' ? $slug : null;
|
||||
}
|
||||
}
|
||||
@@ -7,10 +7,10 @@ use App\Models\AdminUser;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/** 接入站点后台账号统一使用平台系统角色 slug=site_admin。 */
|
||||
/** 接入站点后台主运营角色 slug=site_admin。 */
|
||||
final class SitePlatformRole
|
||||
{
|
||||
public const SLUG = 'site_admin';
|
||||
public const SLUG = SiteOperatorRoles::SLUG_SITE_ADMIN;
|
||||
|
||||
public static function resolve(): AdminRole
|
||||
{
|
||||
@@ -41,14 +41,6 @@ final class SitePlatformRole
|
||||
|
||||
public static function userHasSiteAdminRole(AdminUser $user): bool
|
||||
{
|
||||
if ($user->isSuperAdmin() || $user->hasPrimaryAgentBinding()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return DB::table('admin_user_site_roles as usr')
|
||||
->join('admin_roles as r', 'r.id', '=', 'usr.role_id')
|
||||
->where('usr.admin_user_id', $user->id)
|
||||
->where('r.slug', self::SLUG)
|
||||
->exists();
|
||||
return SiteOperatorRoles::userHasSiteAdminRole($user);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user