feat: refactor super admin to use is_super_admin flag and enhance site deletion logic
- Changed super admin detection from role-based to `is_super_admin` flag in AdminUser model
- Added `requireDefaultAdminSiteId()` method to throw validation error when no integration site exists
- Enhanced site deletion to migrate platform role bindings to fallback site and auto-delete site-specific admin accounts
- Made agent line code optional with auto-generation fallback using `{site_code}-agent-{counter}` format
This commit is contained in:
147
app/Console/Commands/PurgeAgentDataCommand.php
Normal file
147
app/Console/Commands/PurgeAgentDataCommand.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\AgentNode;
|
||||
use App\Services\Agent\AgentNodeService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* 清空信用占成盘代理业务数据,并删除所有非根代理节点(保留各站点 depth=0 根节点)。
|
||||
*
|
||||
* 不删:期号、注单、钱包玩家、站点财务账号。
|
||||
*/
|
||||
final class PurgeAgentDataCommand extends Command
|
||||
{
|
||||
protected $signature = 'lottery:purge-agent-data
|
||||
{--dry-run : 只预览,不写入}
|
||||
{--force : 跳过交互确认(危险)}';
|
||||
|
||||
protected $description = '清空代理账期/授信流水,删除全部非根代理(保留站点根节点)';
|
||||
|
||||
public function handle(AgentNodeService $agentNodeService): int
|
||||
{
|
||||
$dryRun = (bool) $this->option('dry-run');
|
||||
$force = (bool) $this->option('force');
|
||||
|
||||
$connection = (string) config('database.default');
|
||||
$database = (string) config('database.connections.'.$connection.'.database');
|
||||
$environment = (string) config('app.env');
|
||||
|
||||
$nonRootAgents = AgentNode::query()
|
||||
->where('depth', '>', 0)
|
||||
->orderByDesc('depth')
|
||||
->orderByDesc('id')
|
||||
->get();
|
||||
|
||||
$nonRootIds = $nonRootAgents->pluck('id')->map(static fn ($id): int => (int) $id)->all();
|
||||
|
||||
$rootBySite = DB::table('agent_nodes')
|
||||
->where('depth', 0)
|
||||
->get(['id', 'admin_site_id'])
|
||||
->groupBy('admin_site_id')
|
||||
->map(static fn ($rows) => (int) $rows->first()->id);
|
||||
|
||||
$playersToReassign = $nonRootIds === []
|
||||
? 0
|
||||
: (int) DB::table('players')->whereIn('agent_node_id', $nonRootIds)->count();
|
||||
|
||||
$metrics = [
|
||||
['Database', $connection.' / '.$database],
|
||||
['Environment', $environment],
|
||||
['Non-root agents', (string) count($nonRootIds)],
|
||||
['Players to reassign to root', (string) $playersToReassign],
|
||||
['settlement_periods', (string) DB::table('settlement_periods')->count()],
|
||||
['settlement_bills', (string) DB::table('settlement_bills')->count()],
|
||||
['payment_records', (string) DB::table('payment_records')->count()],
|
||||
['settlement_adjustments', (string) DB::table('settlement_adjustments')->count()],
|
||||
['rebate_records', (string) DB::table('rebate_records')->count()],
|
||||
['share_ledger', (string) DB::table('share_ledger')->count()],
|
||||
['credit_ledger', (string) DB::table('credit_ledger')->count()],
|
||||
['agent_delegation_grants', (string) DB::table('agent_delegation_grants')->count()],
|
||||
];
|
||||
|
||||
$this->table(['Metric', 'Value'], $metrics);
|
||||
|
||||
if ($nonRootAgents->isNotEmpty()) {
|
||||
$this->line('Non-root agents to delete:');
|
||||
foreach ($nonRootAgents as $node) {
|
||||
$this->line(sprintf(
|
||||
' - #%d depth=%d %s (%s)',
|
||||
(int) $node->id,
|
||||
(int) $node->depth,
|
||||
(string) $node->code,
|
||||
(string) $node->name,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
if ($dryRun) {
|
||||
$this->info('Dry run only — no changes written.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
if (! $force && ! $this->confirm('This permanently deletes agent settlement data and non-root agents. Continue?', false)) {
|
||||
$this->warn('Aborted.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($agentNodeService, $nonRootAgents, $nonRootIds, $rootBySite): void {
|
||||
DB::table('settlement_adjustments')->delete();
|
||||
DB::table('payment_records')->delete();
|
||||
DB::table('rebate_allocations')->delete();
|
||||
DB::table('rebate_records')->delete();
|
||||
DB::table('share_ledger')->delete();
|
||||
DB::table('settlement_bills')->delete();
|
||||
DB::table('settlement_periods')->delete();
|
||||
DB::table('credit_ledger')->delete();
|
||||
|
||||
DB::table('player_credit_accounts')->update([
|
||||
'used_credit' => 0,
|
||||
'frozen_credit' => 0,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
DB::table('agent_profiles')->update([
|
||||
'allocated_credit' => 0,
|
||||
'used_credit' => 0,
|
||||
'updated_at' => now(),
|
||||
]);
|
||||
|
||||
if ($nonRootIds !== []) {
|
||||
DB::table('agent_delegation_grants')
|
||||
->whereIn('parent_agent_id', $nonRootIds)
|
||||
->orWhereIn('child_agent_id', $nonRootIds)
|
||||
->delete();
|
||||
|
||||
$players = DB::table('players')
|
||||
->whereIn('agent_node_id', $nonRootIds)
|
||||
->get(['id', 'agent_node_id', 'site_code']);
|
||||
|
||||
foreach ($players as $player) {
|
||||
$agentNodeId = (int) $player->agent_node_id;
|
||||
$siteId = (int) (DB::table('agent_nodes')->where('id', $agentNodeId)->value('admin_site_id') ?? 0);
|
||||
$rootId = $rootBySite->get($siteId);
|
||||
if ($rootId === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::table('players')
|
||||
->where('id', (int) $player->id)
|
||||
->update(['agent_node_id' => $rootId]);
|
||||
}
|
||||
|
||||
foreach ($nonRootAgents as $node) {
|
||||
$agentNodeService->destroy($node);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
$this->info('Agent settlement data cleared; non-root agents removed. Root nodes kept.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api\V1\Admin\Integration;
|
||||
|
||||
use App\Models\AdminSite;
|
||||
use App\Support\ApiMessage;
|
||||
use App\Support\ApiResponse;
|
||||
use App\Lottery\ErrorCode;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Services\AuditLogger;
|
||||
use App\Services\Integration\IntegrationSiteService;
|
||||
use App\Support\AdminIntegrationSiteAccess;
|
||||
use App\Support\AdminIntegrationSitePresenter;
|
||||
use App\Http\Middleware\RecordAdminApiAudit;
|
||||
|
||||
final class AdminIntegrationSiteDestroyController extends Controller
|
||||
{
|
||||
public function __invoke(
|
||||
Request $request,
|
||||
AdminSite $admin_site,
|
||||
IntegrationSiteService $service,
|
||||
): JsonResponse {
|
||||
$admin = $request->lotteryAdmin();
|
||||
abort_if($admin === null, 401);
|
||||
|
||||
if (! AdminIntegrationSiteAccess::canAccess($admin, $admin_site)) {
|
||||
return ApiMessage::errorResponse($request, 'admin.site_delete_denied', ErrorCode::AdminForbidden->value, null, 403);
|
||||
}
|
||||
|
||||
$before = AdminIntegrationSitePresenter::detail($admin_site);
|
||||
$service->destroy($admin_site);
|
||||
|
||||
AuditLogger::recordForAdmin(
|
||||
$admin,
|
||||
$request,
|
||||
moduleCode: 'integration',
|
||||
actionCode: 'destroy',
|
||||
targetType: 'admin_site',
|
||||
targetId: (string) $before['id'],
|
||||
beforeJson: $before,
|
||||
afterJson: null,
|
||||
);
|
||||
$request->attributes->set(RecordAdminApiAudit::ATTRIBUTE_AUDIT_RECORDED, true);
|
||||
|
||||
return ApiResponse::success(null);
|
||||
}
|
||||
}
|
||||
@@ -26,11 +26,10 @@ final class AdminUserDestroyController extends Controller
|
||||
return ApiMessage::errorResponse($request, 'admin.user_cannot_delete_self', ErrorCode::ValidationFailed->value, null, 422);
|
||||
}
|
||||
|
||||
$admin_user->load('roles');
|
||||
if ($admin_user->isSuperAdmin()) {
|
||||
$hasOther = AdminUser::query()
|
||||
->whereKeyNot($admin_user->getKey())
|
||||
->whereHas('roles', static fn ($q) => $q->where('admin_roles.slug', AdminUser::ROLE_SUPER_ADMIN))
|
||||
->where('is_super_admin', true)
|
||||
->exists();
|
||||
if (! $hasOther) {
|
||||
return ApiMessage::errorResponse($request, 'admin.user_cannot_delete_last_super_admin', ErrorCode::ValidationFailed->value, null, 422);
|
||||
|
||||
@@ -25,7 +25,7 @@ final class AdminUserPermissionSyncController extends Controller
|
||||
(array) ($input['permissions'] ?? $input['permission_slugs'] ?? []),
|
||||
static fn ($v) => is_string($v) && $v !== '',
|
||||
)));
|
||||
$siteId = AdminUser::defaultAdminSiteId();
|
||||
$siteId = AdminUser::requireDefaultAdminSiteId();
|
||||
|
||||
$codes = [];
|
||||
foreach ($slugs as $slug) {
|
||||
|
||||
@@ -40,7 +40,7 @@ final class AdminAgentLineStoreRequest extends ApiFormRequest
|
||||
{
|
||||
return [
|
||||
'site_code' => ['required', 'string', 'max:64', 'regex:/^[a-z0-9][a-z0-9_-]*$/', Rule::exists('admin_sites', 'code')],
|
||||
'code' => ['required', 'string', 'max:64', 'regex:/^[a-z0-9][a-z0-9_-]*$/', Rule::unique('agent_nodes', 'code')],
|
||||
'code' => ['sometimes', 'nullable', 'string', 'max:64', 'regex:/^[a-z0-9][a-z0-9_-]*$/', Rule::unique('agent_nodes', 'code')],
|
||||
'name' => ['required', 'string', 'max:128'],
|
||||
'username' => ['required', 'string', 'max:64', Rule::unique('admin_users', 'username')],
|
||||
'password' => ['required', 'string', 'min:8', 'max:128'],
|
||||
|
||||
@@ -29,6 +29,7 @@ final class AdminUser extends Authenticatable
|
||||
'email',
|
||||
'password',
|
||||
'status',
|
||||
'is_super_admin',
|
||||
];
|
||||
|
||||
protected $hidden = [
|
||||
@@ -42,27 +43,40 @@ final class AdminUser extends Authenticatable
|
||||
'email_verified_at' => 'datetime',
|
||||
'last_login_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
'is_super_admin' => 'boolean',
|
||||
];
|
||||
}
|
||||
|
||||
public static function defaultAdminSiteId(): int
|
||||
public static function defaultAdminSiteId(): ?int
|
||||
{
|
||||
static $cached = null;
|
||||
if ($cached !== null) {
|
||||
static $resolved = false;
|
||||
if ($resolved) {
|
||||
return $cached;
|
||||
}
|
||||
$resolved = true;
|
||||
|
||||
$id = DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||
if ($id === null) {
|
||||
$id = DB::table('admin_sites')->orderBy('id')->value('id');
|
||||
}
|
||||
if ($id === null) {
|
||||
throw new \RuntimeException('No admin_sites row found.');
|
||||
}
|
||||
$cached = (int) $id;
|
||||
$cached = $id !== null ? (int) $id : null;
|
||||
|
||||
return $cached;
|
||||
}
|
||||
|
||||
public static function requireDefaultAdminSiteId(): int
|
||||
{
|
||||
$siteId = self::requireDefaultAdminSiteId();
|
||||
if ($siteId === null) {
|
||||
throw ValidationException::withMessages([
|
||||
'admin_site_id' => [__('admin.no_integration_site')],
|
||||
]);
|
||||
}
|
||||
|
||||
return $siteId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户在各站点上的角色(多站点 RBAC)。
|
||||
*
|
||||
@@ -188,7 +202,7 @@ final class AdminUser extends Authenticatable
|
||||
|
||||
public function syncRoleSlugsForDefaultSite(array $slugs): void
|
||||
{
|
||||
$siteId = self::defaultAdminSiteId();
|
||||
$siteId = self::requireDefaultAdminSiteId();
|
||||
$slugs = array_values(array_unique($slugs));
|
||||
$roleIds = DB::table('admin_roles')
|
||||
->whereIn('slug', $slugs)
|
||||
@@ -225,7 +239,7 @@ final class AdminUser extends Authenticatable
|
||||
*/
|
||||
public function syncSystemRoleSlugs(array $slugs): void
|
||||
{
|
||||
$this->syncSystemRoleSlugsForSite(self::defaultAdminSiteId(), $slugs);
|
||||
$this->syncSystemRoleSlugsForSite(self::requireDefaultAdminSiteId(), $slugs);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,6 +250,8 @@ final class AdminUser extends Authenticatable
|
||||
public function syncSystemRoleSlugsForSite(int $siteId, array $slugs): void
|
||||
{
|
||||
$slugs = array_values(array_unique($slugs));
|
||||
\App\Support\SuperAdminAccount::assertNotSiteRoleAssignment($slugs);
|
||||
|
||||
$roleIds = DB::table('admin_roles')
|
||||
->where('scope_type', AdminRole::SCOPE_SYSTEM)
|
||||
->whereIn('slug', $slugs)
|
||||
@@ -269,11 +285,7 @@ final class AdminUser extends Authenticatable
|
||||
|
||||
public function isSuperAdmin(): bool
|
||||
{
|
||||
if ($this->relationLoaded('roles')) {
|
||||
return $this->roles->contains('slug', self::ROLE_SUPER_ADMIN);
|
||||
}
|
||||
|
||||
return $this->roles()->where('admin_roles.slug', self::ROLE_SUPER_ADMIN)->exists();
|
||||
return (bool) $this->is_super_admin;
|
||||
}
|
||||
|
||||
public function primaryAgentNodeId(): ?int
|
||||
@@ -348,16 +360,25 @@ final class AdminUser extends Authenticatable
|
||||
*/
|
||||
public function directMenuActionPermissionCodes(): array
|
||||
{
|
||||
if ($this->isSuperAdmin()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$siteId = self::defaultAdminSiteId();
|
||||
$rows = DB::table('admin_user_menu_actions as uma')
|
||||
$query = DB::table('admin_user_menu_actions as uma')
|
||||
->join('admin_menu_actions as ma', 'ma.id', '=', 'uma.menu_action_id')
|
||||
->where('uma.admin_user_id', $this->id)
|
||||
->where(function ($q) use ($siteId): void {
|
||||
->where('ma.status', 1);
|
||||
|
||||
if ($siteId !== null) {
|
||||
$query->where(function ($q) use ($siteId): void {
|
||||
$q->where('uma.site_id', $siteId)->orWhereNull('uma.site_id');
|
||||
})
|
||||
->where('ma.status', 1)
|
||||
->pluck('ma.permission_code')
|
||||
->all();
|
||||
});
|
||||
} else {
|
||||
$query->whereNull('uma.site_id');
|
||||
}
|
||||
|
||||
$rows = $query->pluck('ma.permission_code')->all();
|
||||
|
||||
$out = [];
|
||||
foreach ($rows as $code) {
|
||||
@@ -485,11 +506,17 @@ final class AdminUser extends Authenticatable
|
||||
{
|
||||
$this->loadMissing('roles');
|
||||
|
||||
return $this->roles
|
||||
$slugs = $this->roles
|
||||
->pluck('slug')
|
||||
->filter(static fn ($slug): bool => is_string($slug) && $slug !== '')
|
||||
->unique()
|
||||
->values()
|
||||
->all();
|
||||
|
||||
if ($this->isSuperAdmin()) {
|
||||
$slugs = array_values(array_unique(array_merge([self::ROLE_SUPER_ADMIN], $slugs)));
|
||||
}
|
||||
|
||||
return $slugs;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ use App\Support\AdminDataScope;
|
||||
use App\Support\AdminScopeContext;
|
||||
use App\Support\AdminAgentScope;
|
||||
use App\Support\AdminScopeContextResolver;
|
||||
use App\Support\SitePlatformRole;
|
||||
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
|
||||
|
||||
/**
|
||||
@@ -30,6 +31,7 @@ final class AdminDashboardSnapshotBuilder
|
||||
private readonly DrawHallSnapshotBuilder $hallSnapshot,
|
||||
private readonly AdminReportQueryService $reportQuery,
|
||||
private readonly AgentDashboardOverviewBuilder $agentOverview,
|
||||
private readonly SiteDashboardOverviewBuilder $siteOverview,
|
||||
) {}
|
||||
|
||||
/** @return array<string, mixed> */
|
||||
@@ -57,10 +59,13 @@ final class AdminDashboardSnapshotBuilder
|
||||
'wallet_transfer_view' => $canWallet,
|
||||
],
|
||||
'agent_overview' => null,
|
||||
'site_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);
|
||||
}
|
||||
|
||||
if ($canDraw) {
|
||||
|
||||
137
app/Services/Admin/SiteDashboardOverviewBuilder.php
Normal file
137
app/Services/Admin/SiteDashboardOverviewBuilder.php
Normal file
@@ -0,0 +1,137 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services\Admin;
|
||||
|
||||
use App\Models\AdminSite;
|
||||
use App\Models\AdminUser;
|
||||
use App\Models\AgentNode;
|
||||
use App\Models\Player;
|
||||
use App\Support\AdminScopeContext;
|
||||
use App\Support\AdminScopeContextResolver;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/** 站点管理员仪表盘:站点规模、今日/近 7 日经营、待结账单摘要。 */
|
||||
final class SiteDashboardOverviewBuilder
|
||||
{
|
||||
public function __construct(
|
||||
private readonly AdminReportQueryService $reportQuery,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function build(AdminUser $admin, AdminScopeContext $scope): ?array
|
||||
{
|
||||
if (! $admin->hasPermissionCode('dashboard.view')) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$site = $this->resolvePrimarySite($admin);
|
||||
if ($site === null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$siteId = (int) $site->id;
|
||||
$siteCode = (string) $site->code;
|
||||
$agentNodeIds = AgentNode::query()
|
||||
->where('admin_site_id', $siteId)
|
||||
->pluck('id')
|
||||
->map(static fn ($id): int => (int) $id)
|
||||
->all();
|
||||
|
||||
$playerCount = (int) Player::query()->where('site_code', $siteCode)->count();
|
||||
$today = now()->toDateString();
|
||||
$sevenDayFrom = now()->subDays(6)->toDateString();
|
||||
$scoped = AdminScopeContextResolver::fromValues(
|
||||
$admin,
|
||||
requestedSiteCode: $siteCode,
|
||||
);
|
||||
$todayTotals = $this->reportQuery->periodFinanceTotals($today, $today, $scoped);
|
||||
$sevenDayTotals = $this->reportQuery->periodFinanceTotals($sevenDayFrom, $today, $scoped);
|
||||
$currencyCode = $this->reportQuery->resolvePeriodCurrencyCode($today, $today, $scoped)
|
||||
?? $this->reportQuery->resolvePeriodCurrencyCode($sevenDayFrom, $today, $scoped);
|
||||
$todayActivity = $this->todayActivityStats($siteCode, $today);
|
||||
$pendingBills = $this->pendingBillStats($siteId);
|
||||
$topAgentToday = $this->topAgentToday($scoped, $today);
|
||||
|
||||
return [
|
||||
'admin_site_id' => $siteId,
|
||||
'site_code' => $siteCode,
|
||||
'site_name' => (string) $site->name,
|
||||
'agent_count' => count($agentNodeIds),
|
||||
'player_count' => $playerCount,
|
||||
'active_player_count_today' => $todayActivity['player_count'],
|
||||
'bet_order_count_today' => $todayActivity['order_count'],
|
||||
'today_bet_minor' => $todayTotals['total_bet_minor'],
|
||||
'today_payout_minor' => $todayTotals['total_payout_minor'],
|
||||
'today_profit_minor' => $todayTotals['approx_house_gross_minor'],
|
||||
'seven_day_bet_minor' => $sevenDayTotals['total_bet_minor'],
|
||||
'seven_day_payout_minor' => $sevenDayTotals['total_payout_minor'],
|
||||
'seven_day_profit_minor' => $sevenDayTotals['approx_house_gross_minor'],
|
||||
'profit_scope' => 'house_gross',
|
||||
'currency_code' => $currencyCode,
|
||||
'pending_bill_count' => $pendingBills['count'],
|
||||
'pending_unpaid_minor' => $pendingBills['unpaid_minor'],
|
||||
'latest_bet_at' => $todayActivity['latest_bet_at'],
|
||||
'top_agent_today' => $topAgentToday,
|
||||
];
|
||||
}
|
||||
|
||||
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}
|
||||
*/
|
||||
private function todayActivityStats(string $siteCode, string $today): array
|
||||
{
|
||||
$base = DB::table('ticket_orders as o')
|
||||
->join('players as p', 'p.id', '=', 'o.player_id')
|
||||
->where('p.site_code', $siteCode)
|
||||
->whereDate('o.created_at', $today);
|
||||
|
||||
$latestBetAt = (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_bet_at' => $latestBetAt !== null ? Carbon::parse((string) $latestBetAt)->toIso8601String() : null,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
private function topAgentToday(AdminScopeContext $scope, string $today): ?array
|
||||
{
|
||||
$rows = $this->reportQuery->agentRankingRows($today, $today, null, 1, $scope);
|
||||
|
||||
return $rows[0] ?? null;
|
||||
}
|
||||
}
|
||||
@@ -16,10 +16,33 @@ final class AgentSiteProvisioningService
|
||||
private readonly AgentProfileService $agentProfileService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Generate a unique agent code based on site code and counter.
|
||||
* Format: {site_code}-agent-{counter}
|
||||
*/
|
||||
private function generateUniqueAgentCode(int $siteId): string
|
||||
{
|
||||
$site = AdminSite::query()->find($siteId);
|
||||
if ($site === null) {
|
||||
throw new \RuntimeException('Site not found');
|
||||
}
|
||||
|
||||
$prefix = strtolower(trim($site->code));
|
||||
$counter = 1;
|
||||
|
||||
while (true) {
|
||||
$code = sprintf('%s-agent-%d', $prefix, $counter);
|
||||
if (!AgentNode::query()->where('code', $code)->exists()) {
|
||||
return $code;
|
||||
}
|
||||
$counter++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 在已存在的接入站点上创建一级代理(根节点)及后台登录账号。
|
||||
*
|
||||
* @param array<string, mixed> $payload site_code, code, name, username, password, email?, status?, profile fields
|
||||
* @param array<string, mixed> $payload site_code, code?, name, username, password, email?, status?, profile fields
|
||||
* @return array{site: AdminSite, agent_node: AgentNode}
|
||||
*/
|
||||
public function createRootAgent(AdminUser $actor, array $payload): array
|
||||
@@ -32,10 +55,9 @@ final class AgentSiteProvisioningService
|
||||
$email = isset($payload['email']) ? trim((string) $payload['email']) : null;
|
||||
$status = (int) ($payload['status'] ?? 1);
|
||||
|
||||
if ($siteCode === '' || $code === '' || $name === '' || $username === '' || $password === '') {
|
||||
if ($siteCode === '' || $name === '' || $username === '' || $password === '') {
|
||||
throw ValidationException::withMessages([
|
||||
'site_code' => $siteCode === '' ? ['required'] : [],
|
||||
'code' => $code === '' ? ['required'] : [],
|
||||
'name' => $name === '' ? ['required'] : [],
|
||||
'username' => $username === '' ? ['required'] : [],
|
||||
'password' => $password === '' ? ['required'] : [],
|
||||
@@ -47,8 +69,13 @@ final class AgentSiteProvisioningService
|
||||
throw ValidationException::withMessages(['site_code' => ['exists']]);
|
||||
}
|
||||
|
||||
if (AgentNode::query()->where('code', $code)->exists()) {
|
||||
throw ValidationException::withMessages(['code' => ['unique']]);
|
||||
// Auto-generate code if not provided
|
||||
if ($code === '') {
|
||||
$code = $this->generateUniqueAgentCode($site->id);
|
||||
} else {
|
||||
if (AgentNode::query()->where('code', $code)->exists()) {
|
||||
throw ValidationException::withMessages(['code' => ['unique']]);
|
||||
}
|
||||
}
|
||||
|
||||
if (AdminUser::query()->where('username', $username)->exists()) {
|
||||
|
||||
@@ -5,26 +5,14 @@ namespace App\Services\Integration;
|
||||
use App\Models\AdminSite;
|
||||
use App\Models\AdminRole;
|
||||
use App\Models\AdminUser;
|
||||
use App\Support\AdminPermissionInheritance;
|
||||
use App\Models\Player;
|
||||
use App\Support\SitePlatformRole;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
final class IntegrationSiteService
|
||||
{
|
||||
/** @var list<string> */
|
||||
private const SITE_ADMIN_PERMISSION_SLUGS = [
|
||||
'prd.agent.manage',
|
||||
'prd.agent.profile.manage',
|
||||
'prd.agent.user.manage',
|
||||
'prd.agent.role.manage',
|
||||
'prd.users.manage',
|
||||
'prd.tickets.view',
|
||||
'prd.report.view',
|
||||
'prd.settlement.agent.view',
|
||||
'prd.settlement.agent.manage',
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
private readonly PartnerSiteConfigResolver $configResolver,
|
||||
) {}
|
||||
@@ -59,7 +47,7 @@ final class IntegrationSiteService
|
||||
'wallet_api_key_encrypted' => encrypt($secrets['wallet_api_key']),
|
||||
]);
|
||||
|
||||
$role = $this->createSiteAdminRole($site);
|
||||
$role = SitePlatformRole::resolve();
|
||||
$adminUser = $this->createSiteAdminUser($site, $role, $adminAccount);
|
||||
|
||||
return [
|
||||
@@ -108,6 +96,79 @@ final class IntegrationSiteService
|
||||
return $site->fresh();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{site: AdminSite, secrets: array{sso_jwt_secret: string, wallet_api_key: string}}
|
||||
*/
|
||||
public function destroy(AdminSite $site): void
|
||||
{
|
||||
$siteCode = (string) $site->code;
|
||||
$siteId = (int) $site->id;
|
||||
$siteAdminRoleId = SitePlatformRole::resolve()->id;
|
||||
|
||||
if (AdminSite::query()->count() <= 1) {
|
||||
$fallbackSiteId = null;
|
||||
} else {
|
||||
$fallbackSiteId = (int) AdminSite::query()
|
||||
->where('id', '!=', $siteId)
|
||||
->orderBy('id')
|
||||
->value('id');
|
||||
}
|
||||
|
||||
DB::transaction(function () use ($site, $siteCode, $siteId, $siteAdminRoleId, $fallbackSiteId): void {
|
||||
$superRoleId = AdminRole::query()
|
||||
->where('slug', AdminUser::ROLE_SUPER_ADMIN)
|
||||
->value('id');
|
||||
|
||||
$platformBindings = DB::table('admin_user_site_roles')
|
||||
->where('site_id', $siteId)
|
||||
->when($siteAdminRoleId !== null, static fn ($query) => $query->where('role_id', '!=', $siteAdminRoleId))
|
||||
->when($superRoleId !== null, static fn ($query) => $query->where('role_id', '!=', $superRoleId))
|
||||
->get(['admin_user_id', 'role_id', 'granted_at']);
|
||||
|
||||
foreach ($platformBindings as $binding) {
|
||||
if ($fallbackSiteId === null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
DB::table('admin_user_site_roles')->updateOrInsert(
|
||||
[
|
||||
'admin_user_id' => (int) $binding->admin_user_id,
|
||||
'site_id' => $fallbackSiteId,
|
||||
'role_id' => (int) $binding->role_id,
|
||||
],
|
||||
['granted_at' => $binding->granted_at ?? now()],
|
||||
);
|
||||
}
|
||||
|
||||
Player::query()->where('site_code', $siteCode)->delete();
|
||||
|
||||
if ($siteAdminRoleId !== null) {
|
||||
$siteAdminUserIds = DB::table('admin_user_site_roles')
|
||||
->where('site_id', $siteId)
|
||||
->where('role_id', $siteAdminRoleId)
|
||||
->pluck('admin_user_id');
|
||||
|
||||
foreach ($siteAdminUserIds as $userId) {
|
||||
$bindings = DB::table('admin_user_site_roles')
|
||||
->where('admin_user_id', $userId)
|
||||
->get(['site_id', 'role_id']);
|
||||
|
||||
$onlyAutoSiteAdmin = $bindings->count() === 1
|
||||
&& (int) $bindings[0]->site_id === $siteId
|
||||
&& (int) $bindings[0]->role_id === (int) $siteAdminRoleId;
|
||||
|
||||
if ($onlyAutoSiteAdmin) {
|
||||
AdminUser::query()->where('id', $userId)->delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$site->delete();
|
||||
});
|
||||
|
||||
$this->configResolver->forgetCache($siteCode);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{site: AdminSite, secrets: array{sso_jwt_secret: string, wallet_api_key: string}}
|
||||
*/
|
||||
@@ -147,26 +208,6 @@ final class IntegrationSiteService
|
||||
return $trimmed === '' ? null : $trimmed;
|
||||
}
|
||||
|
||||
private function createSiteAdminRole(AdminSite $site): AdminRole
|
||||
{
|
||||
$slug = sprintf('site_admin_%s', (string) $site->code);
|
||||
$role = AdminRole::query()->create([
|
||||
'slug' => $slug,
|
||||
'name' => sprintf('%s 站点后台管理员', (string) $site->name),
|
||||
'description' => sprintf('自动创建:站点 %s (%s) 后台管理账号专用角色', (string) $site->name, (string) $site->code),
|
||||
'status' => 1,
|
||||
'is_system' => true,
|
||||
'sort_order' => 900,
|
||||
'scope_type' => AdminRole::SCOPE_SYSTEM,
|
||||
]);
|
||||
|
||||
$role->syncLegacyPermissionSlugs(
|
||||
AdminPermissionInheritance::expand(self::SITE_ADMIN_PERMISSION_SLUGS),
|
||||
);
|
||||
|
||||
return $role;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array{username: string, nickname: string, password: string, email?: string|null} $adminAccount
|
||||
*/
|
||||
@@ -191,7 +232,7 @@ final class IntegrationSiteService
|
||||
'status' => 0,
|
||||
]);
|
||||
|
||||
$user->syncSystemRoleSlugsForSite((int) $site->id, [(string) $role->slug]);
|
||||
$user->syncSystemRoleSlugsForSite((int) $site->id, [SitePlatformRole::SLUG]);
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
@@ -31,12 +31,14 @@ final class PlayerLedgerLogsService
|
||||
'prize' => ['settle_payout', 'prize', 'jackpot_manual_payout'],
|
||||
];
|
||||
|
||||
/** PRD 对外类型 → credit_ledger.reason */
|
||||
/** PRD 对外类型 → credit_ledger.reason(信用盘不用钱包「派彩」口径) */
|
||||
private const CREDIT_TYPE_TO_REASON = [
|
||||
'bet' => ['bet_hold', 'game_settlement_loss'],
|
||||
'reversal' => ['bet_hold_release'],
|
||||
'refund' => ['settlement_confirm'],
|
||||
'prize' => ['game_settlement_win', 'settlement_payout'],
|
||||
'win_credit' => ['game_settlement_win'],
|
||||
'credit_release' => ['game_settlement_win', 'settlement_confirm', 'bet_hold_release'],
|
||||
'bill_settlement' => ['settlement_payout'],
|
||||
'transfer_in' => [],
|
||||
'transfer_out' => [],
|
||||
];
|
||||
@@ -413,8 +415,18 @@ final class PlayerLedgerLogsService
|
||||
$items = $paginator->getCollection()
|
||||
->map(function (object $row) use (&$runningMinor, $player, $currency): array {
|
||||
$amount = (int) $row->amount;
|
||||
$formatted = $this->formatPlayerCreditRow($row, $player, $currency, $runningMinor);
|
||||
$runningMinor -= $amount;
|
||||
$reason = (string) $row->reason;
|
||||
$affectsBalance = $this->creditReasonAffectsAvailableBalance($reason);
|
||||
$formatted = $this->formatPlayerCreditRow(
|
||||
$row,
|
||||
$player,
|
||||
$currency,
|
||||
$affectsBalance ? $runningMinor : null,
|
||||
$affectsBalance,
|
||||
);
|
||||
if ($affectsBalance) {
|
||||
$runningMinor -= $amount;
|
||||
}
|
||||
|
||||
return $formatted;
|
||||
})
|
||||
@@ -530,24 +542,30 @@ final class PlayerLedgerLogsService
|
||||
object $row,
|
||||
Player $player,
|
||||
string $currency,
|
||||
int $balanceAfterMinor,
|
||||
?int $balanceAfterMinor,
|
||||
?bool $affectsAvailableBalance = null,
|
||||
): array {
|
||||
$amount = (int) $row->amount;
|
||||
$amountAbs = abs($amount);
|
||||
$publicType = $this->creditReasonToPublicType((string) $row->reason);
|
||||
$reason = (string) $row->reason;
|
||||
$publicType = $this->creditReasonToPublicType($reason);
|
||||
$affectsBalance = $affectsAvailableBalance ?? $this->creditReasonAffectsAvailableBalance($reason);
|
||||
|
||||
return [
|
||||
'log_id' => 'CL-'.$row->id,
|
||||
'type' => $publicType,
|
||||
'biz_type' => (string) $row->reason,
|
||||
'biz_type' => $reason,
|
||||
'amount' => $amount,
|
||||
'amount_formatted' => CurrencyFormatter::fromMinor($amountAbs),
|
||||
'amount_abs' => $amountAbs,
|
||||
'amount_abs_formatted' => CurrencyFormatter::fromMinor($amountAbs),
|
||||
'direction' => $amount >= 0 ? 'in' : 'out',
|
||||
'currency_code' => $currency,
|
||||
'balance_after' => $balanceAfterMinor,
|
||||
'balance_after_formatted' => CurrencyFormatter::fromMinor($balanceAfterMinor),
|
||||
'balance_after' => $affectsBalance ? $balanceAfterMinor : null,
|
||||
'balance_after_formatted' => $affectsBalance && $balanceAfterMinor !== null
|
||||
? CurrencyFormatter::fromMinor($balanceAfterMinor)
|
||||
: null,
|
||||
'affects_available_credit' => $affectsBalance,
|
||||
'ref_id' => $this->creditRefLabel($row),
|
||||
'idempotent_key' => null,
|
||||
'external_ref_no' => null,
|
||||
@@ -560,6 +578,12 @@ final class PlayerLedgerLogsService
|
||||
];
|
||||
}
|
||||
|
||||
/** 账期收付记账不改变 player_credit_accounts,不参与可用信用倒推。 */
|
||||
private function creditReasonAffectsAvailableBalance(string $reason): bool
|
||||
{
|
||||
return $reason !== 'settlement_payout';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
@@ -611,7 +635,8 @@ final class PlayerLedgerLogsService
|
||||
'bet_hold', 'game_settlement_loss' => 'bet',
|
||||
'bet_hold_release' => 'reversal',
|
||||
'settlement_confirm' => 'refund',
|
||||
'game_settlement_win', 'settlement_payout' => 'prize',
|
||||
'game_settlement_win' => 'win_credit',
|
||||
'settlement_payout' => 'bill_settlement',
|
||||
default => $reason,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -23,6 +23,22 @@ final class AdminAgentNodeAccess
|
||||
?? AdminSite::query()->orderBy('id')->value('id'));
|
||||
}
|
||||
|
||||
// Check if admin is a platform account (bound via admin_user_site_roles)
|
||||
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($accessibleSiteIds !== null) {
|
||||
// Platform account (site admin)
|
||||
if ($requestedSiteId !== null && $requestedSiteId > 0) {
|
||||
if (in_array($requestedSiteId, $accessibleSiteIds, true)) {
|
||||
return $requestedSiteId;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return first accessible site if no specific site requested
|
||||
return $accessibleSiteIds[0] ?? null;
|
||||
}
|
||||
|
||||
// Agent account (bound via agent node)
|
||||
$actor = AdminAgentScope::primaryAgentNode($admin);
|
||||
if ($actor === null) {
|
||||
return null;
|
||||
|
||||
@@ -32,6 +32,14 @@ final class AdminAgentScope
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if admin is a platform account (bound via admin_user_site_roles)
|
||||
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($accessibleSiteIds !== null) {
|
||||
// Platform account (site admin) can see all nodes in the site
|
||||
return in_array((int) $node->admin_site_id, $accessibleSiteIds, true);
|
||||
}
|
||||
|
||||
// Agent account (bound via agent node)
|
||||
$actor = self::primaryAgentNode($admin);
|
||||
if ($actor === null) {
|
||||
return false;
|
||||
@@ -90,6 +98,14 @@ final class AdminAgentScope
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if admin is a platform account (bound via admin_user_site_roles)
|
||||
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($accessibleSiteIds !== null) {
|
||||
// Platform account (site admin) can edit all nodes in the site
|
||||
return in_array((int) $node->admin_site_id, $accessibleSiteIds, true);
|
||||
}
|
||||
|
||||
// Agent account (bound via agent node)
|
||||
$actor = self::primaryAgentNode($admin);
|
||||
if ($actor === null) {
|
||||
return false;
|
||||
@@ -115,6 +131,17 @@ final class AdminAgentScope
|
||||
return $query;
|
||||
}
|
||||
|
||||
// Check if admin is a platform account (bound via admin_user_site_roles)
|
||||
$accessibleSiteIds = $admin->accessibleAdminSiteIds();
|
||||
if ($accessibleSiteIds !== null) {
|
||||
// Platform account (site admin) can see all nodes in the site
|
||||
if (in_array($adminSiteId, $accessibleSiteIds, true)) {
|
||||
return $query;
|
||||
}
|
||||
return $query->whereRaw('0 = 1');
|
||||
}
|
||||
|
||||
// Agent account (bound via agent node)
|
||||
$actor = self::primaryAgentNode($admin);
|
||||
if ($actor === null || (int) $actor->admin_site_id !== $adminSiteId) {
|
||||
return $query->whereRaw('0 = 1');
|
||||
|
||||
@@ -38,6 +38,7 @@ final class AdminAuthProfile
|
||||
* can_create_child_agent: bool,
|
||||
* can_create_player: bool
|
||||
* },
|
||||
* site: ?array{id: int, code: string, name: string},
|
||||
* is_super_admin: bool,
|
||||
* operational_permissions: list<string>,
|
||||
* delegation_ceiling: list<string>,
|
||||
@@ -58,6 +59,7 @@ final class AdminAuthProfile
|
||||
'permissions' => $permissionSlugs,
|
||||
'navigation' => AdminAuthorizationRegistry::visibleNavigationItems($permissionSlugs, $fresh),
|
||||
'agent' => $agent,
|
||||
'site' => self::siteContext($fresh),
|
||||
'is_super_admin' => $fresh->isSuperAdmin(),
|
||||
'operational_permissions' => $permissionSlugs,
|
||||
'delegation_ceiling' => AgentDelegationAuthorization::delegationLegacySlugsForAdminUser($fresh),
|
||||
@@ -71,19 +73,32 @@ final class AdminAuthProfile
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* id: int,
|
||||
* admin_site_id: int,
|
||||
* admin_site_name: string,
|
||||
* site_code: string,
|
||||
* path: string,
|
||||
* code: string,
|
||||
* name: string,
|
||||
* depth: int,
|
||||
* can_create_child_agent: bool,
|
||||
* can_create_player: bool
|
||||
* }|null
|
||||
* @return array{id: int, code: string, name: string}|null
|
||||
*/
|
||||
private static function siteContext(AdminUser $admin): ?array
|
||||
{
|
||||
if ($admin->isSuperAdmin() || $admin->primaryAgentNode() !== null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (! SitePlatformRole::userHasSiteAdminRole($admin)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$sites = AdminUserSiteBindingPresenter::accessibleSitesFor($admin);
|
||||
if ($sites === []) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$site = $sites[0];
|
||||
|
||||
return [
|
||||
'id' => (int) $site['id'],
|
||||
'code' => (string) $site['code'],
|
||||
'name' => (string) $site['name'],
|
||||
];
|
||||
}
|
||||
|
||||
private static function agentContext(AdminUser $admin): ?array
|
||||
{
|
||||
if ($admin->isSuperAdmin()) {
|
||||
|
||||
@@ -462,7 +462,7 @@ final class AdminAuthorizationRegistry
|
||||
['code' => 'admin.settlement-reports.summary', 'module_code' => 'settlement', 'name' => '代理结算报表摘要', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-reports/summary', 'route_name' => 'api.v1.admin.settlement-reports.summary', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['settlement.agent.view', 'settlement.agent.manage'], 'legacy_permission_slugs' => ['prd.settlement.agent.view', 'prd.settlement.agent.manage']],
|
||||
['code' => 'admin.settlement-reports.show', 'module_code' => 'settlement', 'name' => '信用占成盘报表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-reports', 'route_name' => 'api.v1.admin.settlement-reports.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['settlement.agent.view', 'settlement.agent.manage'], 'legacy_permission_slugs' => ['prd.settlement.agent.view', 'prd.settlement.agent.manage']],
|
||||
|
||||
['code' => 'admin.play-types.index', 'module_code' => 'config', 'name' => '玩法类型列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/play-types', 'route_name' => 'api.v1.admin.play-types.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view', 'prd.rebate.manage', 'prd.rebate.view']],
|
||||
['code' => 'admin.play-types.index', 'module_code' => 'config', 'name' => '玩法类型列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/play-types', 'route_name' => 'api.v1.admin.play-types.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view', 'prd.rebate.manage', 'prd.rebate.view', 'prd.report.view', 'prd.dashboard.view']],
|
||||
['code' => 'admin.play-types.patch', 'module_code' => 'config', 'name' => '玩法类型切换', 'http_method' => 'PATCH', 'uri_pattern' => '/api/v1/admin/play-types/{play_code}', 'route_name' => 'api.v1.admin.play-types.patch', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.play_switch.manage']],
|
||||
['code' => 'admin.config.play-versions.index', 'module_code' => 'config', 'name' => '玩法版本列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/play-versions', 'route_name' => 'api.v1.admin.config.play-versions.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
|
||||
['code' => 'admin.config.play-versions.show', 'module_code' => 'config', 'name' => '玩法版本详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/play-versions/{id}', 'route_name' => 'api.v1.admin.config.play-versions.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
|
||||
@@ -494,6 +494,7 @@ final class AdminAuthorizationRegistry
|
||||
['code' => 'admin.integration-sites.store', 'module_code' => 'integration', 'name' => '创建接入站点', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/integration-sites', 'route_name' => 'api.v1.admin.integration-sites.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.show', 'module_code' => 'integration', 'name' => '接入站点详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}', 'route_name' => 'api.v1.admin.integration-sites.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['integration.site.view', 'integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.update', 'module_code' => 'integration', 'name' => '更新接入站点', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}', 'route_name' => 'api.v1.admin.integration-sites.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.destroy', 'module_code' => 'integration', 'name' => '删除接入站点', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}', 'route_name' => 'api.v1.admin.integration-sites.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.rotate-secrets', 'module_code' => 'integration', 'name' => '重置接入密钥', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}/rotate-secrets', 'route_name' => 'api.v1.admin.integration-sites.rotate-secrets', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.connectivity-test', 'module_code' => 'integration', 'name' => '接入站点联通检测', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}/connectivity-test', 'route_name' => 'api.v1.admin.integration-sites.connectivity-test', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['integration.site.view', 'integration.site.manage']],
|
||||
['code' => 'admin.integration-sites.export', 'module_code' => 'integration', 'name' => '导出接入参数表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/integration-sites/{admin_site}/export', 'route_name' => 'api.v1.admin.integration-sites.export', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['integration.site.view', 'integration.site.manage']],
|
||||
|
||||
@@ -29,6 +29,7 @@ final class AdminIntegrationSitePresenter
|
||||
'wallet_api_key_masked' => is_string($site->wallet_api_key_encrypted) && $site->wallet_api_key_encrypted !== ''
|
||||
? '••••••••'
|
||||
: null,
|
||||
'is_default' => (bool) $site->is_default,
|
||||
'updated_at' => $site->updated_at?->toIso8601String(),
|
||||
];
|
||||
}
|
||||
@@ -38,16 +39,16 @@ final class AdminIntegrationSitePresenter
|
||||
*/
|
||||
public static function detail(AdminSite $site): array
|
||||
{
|
||||
return array_merge(self::listItem($site), [
|
||||
return [
|
||||
...self::listItem($site),
|
||||
'wallet_debit_path' => (string) $site->wallet_debit_path,
|
||||
'wallet_credit_path' => (string) $site->wallet_credit_path,
|
||||
'wallet_balance_path' => (string) $site->wallet_balance_path,
|
||||
'iframe_allowed_origins' => $site->iframe_allowed_origins ?? [],
|
||||
'lottery_h5_base_url' => $site->lottery_h5_base_url,
|
||||
'notes' => $site->notes,
|
||||
'is_default' => (bool) $site->is_default,
|
||||
'created_at' => $site->created_at?->toIso8601String(),
|
||||
]);
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -11,10 +11,12 @@ final class PlatformSystemRoles
|
||||
|
||||
public const SLUG_AGENT = 'agent';
|
||||
|
||||
public const SLUG_SITE_ADMIN = SitePlatformRole::SLUG;
|
||||
|
||||
/** @return list<string> */
|
||||
public static function fixedSlugs(): array
|
||||
{
|
||||
return [self::SLUG_SUPER_ADMIN, self::SLUG_AGENT];
|
||||
return [self::SLUG_SUPER_ADMIN, self::SLUG_SITE_ADMIN, self::SLUG_AGENT];
|
||||
}
|
||||
|
||||
public static function isFixedSlug(string $slug): bool
|
||||
@@ -49,6 +51,7 @@ final class PlatformSystemRoles
|
||||
public static function ensureAll(): void
|
||||
{
|
||||
self::ensureSuperAdminRole();
|
||||
SiteAdminDefaultRolePermissions::ensurePlatformSiteAdminRole();
|
||||
AgentDefaultRolePermissions::ensurePlatformAgentRole();
|
||||
}
|
||||
}
|
||||
|
||||
64
app/Support/SiteAdminDefaultRolePermissions.php
Normal file
64
app/Support/SiteAdminDefaultRolePermissions.php
Normal file
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminRole;
|
||||
|
||||
/**
|
||||
* 平台「站点管理员」系统角色(slug=site_admin)的默认 prd.* 模板。
|
||||
* 接入站点创建时自动绑定;权限可在「平台角色管理」调整。
|
||||
*/
|
||||
final class SiteAdminDefaultRolePermissions
|
||||
{
|
||||
/** @var list<string> */
|
||||
private const TEMPLATE_SLUGS = [
|
||||
'prd.dashboard.view',
|
||||
'prd.agent.view',
|
||||
'prd.agent.manage',
|
||||
'prd.agent.role.view',
|
||||
'prd.agent.role.manage',
|
||||
'prd.agent.user.view',
|
||||
'prd.agent.user.manage',
|
||||
'prd.agent.profile.manage',
|
||||
'prd.users.manage',
|
||||
'prd.tickets.view',
|
||||
'prd.report.view',
|
||||
'prd.settlement.agent.view',
|
||||
'prd.settlement.agent.manage',
|
||||
'prd.integration.view',
|
||||
];
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function templateSlugs(): array
|
||||
{
|
||||
return self::TEMPLATE_SLUGS;
|
||||
}
|
||||
|
||||
public static function ensurePlatformSiteAdminRole(): AdminRole
|
||||
{
|
||||
$role = AdminRole::query()->updateOrCreate(
|
||||
[
|
||||
'slug' => SitePlatformRole::SLUG,
|
||||
'scope_type' => AdminRole::SCOPE_SYSTEM,
|
||||
],
|
||||
[
|
||||
'code' => SitePlatformRole::SLUG,
|
||||
'name' => '站点管理员',
|
||||
'description' => '接入站点后台默认权限(代理/玩家/结算运营 + 站点仪表盘)',
|
||||
'status' => 1,
|
||||
'is_system' => true,
|
||||
'sort_order' => 40,
|
||||
'owner_agent_id' => null,
|
||||
'delegated_from_role_id' => null,
|
||||
],
|
||||
);
|
||||
|
||||
$role->syncLegacyPermissionSlugs(
|
||||
AdminPermissionInheritance::expand(self::TEMPLATE_SLUGS),
|
||||
);
|
||||
|
||||
return $role->fresh() ?? $role;
|
||||
}
|
||||
}
|
||||
54
app/Support/SitePlatformRole.php
Normal file
54
app/Support/SitePlatformRole.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminRole;
|
||||
use App\Models\AdminUser;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/** 接入站点后台账号统一使用平台系统角色 slug=site_admin。 */
|
||||
final class SitePlatformRole
|
||||
{
|
||||
public const SLUG = 'site_admin';
|
||||
|
||||
public static function resolve(): AdminRole
|
||||
{
|
||||
return SiteAdminDefaultRolePermissions::ensurePlatformSiteAdminRole();
|
||||
}
|
||||
|
||||
public static function id(): int
|
||||
{
|
||||
return (int) self::resolve()->id;
|
||||
}
|
||||
|
||||
public static function idOrFail(): int
|
||||
{
|
||||
$id = (int) (AdminRole::query()
|
||||
->where('scope_type', AdminRole::SCOPE_SYSTEM)
|
||||
->where('slug', self::SLUG)
|
||||
->where('status', 1)
|
||||
->value('id') ?? 0);
|
||||
|
||||
if ($id <= 0) {
|
||||
throw ValidationException::withMessages([
|
||||
'role' => ['platform_site_admin_role_missing: run php artisan lottery:admin-auth-sync'],
|
||||
]);
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
63
app/Support/SuperAdminAccount.php
Normal file
63
app/Support/SuperAdminAccount.php
Normal file
@@ -0,0 +1,63 @@
|
||||
<?php
|
||||
|
||||
namespace App\Support;
|
||||
|
||||
use App\Models\AdminUser;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
|
||||
/** 平台唯一超级管理员账号(不绑定站点)。 */
|
||||
final class SuperAdminAccount
|
||||
{
|
||||
public static function assign(AdminUser $user): AdminUser
|
||||
{
|
||||
return DB::transaction(function () use ($user): AdminUser {
|
||||
DB::table('admin_users')
|
||||
->where('id', '!=', $user->id)
|
||||
->update(['is_super_admin' => false]);
|
||||
|
||||
$user->forceFill(['is_super_admin' => true])->save();
|
||||
|
||||
self::removeLegacySiteRoleBinding((int) $user->id);
|
||||
|
||||
return $user->fresh() ?? $user;
|
||||
});
|
||||
}
|
||||
|
||||
public static function revoke(AdminUser $user): AdminUser
|
||||
{
|
||||
$user->forceFill(['is_super_admin' => false])->save();
|
||||
|
||||
return $user->fresh() ?? $user;
|
||||
}
|
||||
|
||||
public static function count(): int
|
||||
{
|
||||
return (int) AdminUser::query()->where('is_super_admin', true)->count();
|
||||
}
|
||||
|
||||
public static function assertNotSiteRoleAssignment(array $roleSlugs): void
|
||||
{
|
||||
if (in_array(AdminUser::ROLE_SUPER_ADMIN, $roleSlugs, true)) {
|
||||
throw ValidationException::withMessages([
|
||||
'role_slugs' => [__('admin.super_admin_not_site_role')],
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
private static function removeLegacySiteRoleBinding(int $userId): void
|
||||
{
|
||||
$superRoleId = DB::table('admin_roles')
|
||||
->where('slug', AdminUser::ROLE_SUPER_ADMIN)
|
||||
->value('id');
|
||||
|
||||
if ($superRoleId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table('admin_user_site_roles')
|
||||
->where('admin_user_id', $userId)
|
||||
->where('role_id', $superRoleId)
|
||||
->delete();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user