feat: add AgentNodeIndexController for node listing and remove settlement_cycle field from AgentProfile logic

This commit is contained in:
2026-06-11 18:01:58 +08:00
parent 4d1c2b3d63
commit e14b7b4569
30 changed files with 383 additions and 91 deletions

View File

@@ -0,0 +1,38 @@
<?php
namespace App\Http\Controllers\Api\V1\Admin\Agent;
use App\Support\ApiResponse;
use Illuminate\Http\Request;
use Illuminate\Http\JsonResponse;
use App\Http\Controllers\Controller;
use App\Support\AdminAgentScope;
use App\Support\AgentNodePresenter;
use App\Models\AgentNode;
final class AgentNodeIndexController extends Controller
{
public function __invoke(Request $request): JsonResponse
{
$admin = $request->lotteryAdmin();
abort_if($admin === null, 401);
$query = AgentNode::query()->orderBy('admin_site_id')->orderBy('path');
if (! $admin->isSuperAdmin()) {
$actor = AdminAgentScope::primaryAgentNode($admin);
if ($actor === null) {
$query->whereRaw('0 = 1');
} else {
$query
->where('admin_site_id', (int) $actor->admin_site_id)
->where('path', 'like', $actor->path.'%');
}
}
return ApiResponse::success([
'admin_site_id' => null,
'items' => AgentNodePresenter::list($query->get()),
]);
}
}

View File

@@ -27,6 +27,12 @@ final class AdminIntegrationSiteStoreController extends Controller
AdminIntegrationSitePresenter::detail($site), AdminIntegrationSitePresenter::detail($site),
$result['secrets'], $result['secrets'],
); );
$payload['admin_user'] = [
'id' => (int) $result['admin_user']->id,
'username' => (string) $result['admin_user']->username,
'nickname' => (string) $result['admin_user']->name,
'email' => $result['admin_user']->email,
];
AuditLogger::recordForAdmin( AuditLogger::recordForAdmin(
$admin, $admin,

View File

@@ -3,23 +3,25 @@
namespace App\Http\Controllers\Api\V1\Admin\Player; namespace App\Http\Controllers\Api\V1\Admin\Player;
use App\Models\Player; use App\Models\Player;
use App\Models\AdminSite;
use App\Models\AgentNode;
use App\Lottery\ErrorCode; use App\Lottery\ErrorCode;
use App\Support\ApiMessage; use App\Support\ApiMessage;
use App\Support\ApiResponse; use App\Support\ApiResponse;
use Illuminate\Http\JsonResponse;
use App\Support\AdminAgentScope;
use App\Support\AdminSiteScope; use App\Support\AdminSiteScope;
use App\Support\AdminAgentScope;
use App\Support\PlayerAuthSource;
use Illuminate\Http\JsonResponse;
use App\Support\PlayerFundingMode;
use Illuminate\Support\Facades\DB;
use App\Support\PlayerApiPresenter; use App\Support\PlayerApiPresenter;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AdminPlayerStoreRequest; use Illuminate\Support\Facades\Hash;
use App\Models\AgentNode;
use App\Services\Agent\AgentProfileService; use App\Services\Agent\AgentProfileService;
use App\Services\Agent\RebateLimitValidator; use App\Services\Agent\RebateLimitValidator;
use App\Services\Player\PlayerCreditService; use App\Services\Player\PlayerCreditService;
use App\Support\PlayerAuthSource; use Illuminate\Validation\ValidationException;
use App\Support\PlayerFundingMode; use App\Http\Requests\Admin\AdminPlayerStoreRequest;
use Illuminate\Support\Facades\Hash;
use Illuminate\Support\Str;
/** POST /api/v1/admin/players */ /** POST /api/v1/admin/players */
final class AdminPlayerStoreController extends Controller final class AdminPlayerStoreController extends Controller
@@ -35,7 +37,7 @@ final class AdminPlayerStoreController extends Controller
try { try {
$agentProfileService->assertActorMayCreatePlayer($admin); $agentProfileService->assertActorMayCreatePlayer($admin);
} catch (\Illuminate\Validation\ValidationException $e) { } catch (ValidationException $e) {
return ApiMessage::errorResponse( return ApiMessage::errorResponse(
$request, $request,
'admin.player_create_capability_forbidden', 'admin.player_create_capability_forbidden',
@@ -110,11 +112,15 @@ final class AdminPlayerStoreController extends Controller
} }
$agent = AgentNode::query()->findOrFail($agentNodeId); $agent = AgentNode::query()->findOrFail($agentNodeId);
$rebateRate = 0.0;
$extraRebateRate = 0.0;
if ($request->has('rebate_rate')) { if ($request->has('rebate_rate')) {
$rebateRate = (float) $request->input('rebate_rate', 0) / 100;
$extraRebateRate = (float) $request->input('extra_rebate_rate', 0) / 100;
$rebateLimitValidator->assertPlayerRebateWithinAgent( $rebateLimitValidator->assertPlayerRebateWithinAgent(
$agent, $agent,
(float) $request->input('rebate_rate', 0), $rebateRate,
(float) $request->input('extra_rebate_rate', 0), $extraRebateRate,
); );
} }
@@ -122,7 +128,7 @@ final class AdminPlayerStoreController extends Controller
? (int) $request->input('credit_limit', 0) ? (int) $request->input('credit_limit', 0)
: ($isNative ? 0 : 0); : ($isNative ? 0 : 0);
$player = \Illuminate\Support\Facades\DB::transaction(function () use ( $player = DB::transaction(function () use (
$agent, $agent,
$agentProfileService, $agentProfileService,
$playerCreditService, $playerCreditService,
@@ -132,6 +138,8 @@ final class AdminPlayerStoreController extends Controller
$agentNodeId, $agentNodeId,
$sitePlayerId, $sitePlayerId,
$creditLimit, $creditLimit,
$rebateRate,
$extraRebateRate,
): Player { ): Player {
$agentProfileService->assertMayIncreasePlayerCredit($agent, $creditLimit); $agentProfileService->assertMayIncreasePlayerCredit($agent, $creditLimit);
@@ -157,12 +165,12 @@ final class AdminPlayerStoreController extends Controller
$agentProfileService->refreshAllocatedCredit($agent); $agentProfileService->refreshAllocatedCredit($agent);
if ($request->has('rebate_rate')) { if ($request->has('rebate_rate')) {
\Illuminate\Support\Facades\DB::table('player_rebate_profiles')->insert([ DB::table('player_rebate_profiles')->insert([
'player_id' => $player->id, 'player_id' => $player->id,
'game_type' => '*', 'game_type' => '*',
'inherit_from_agent' => false, 'inherit_from_agent' => false,
'rebate_rate' => (float) $request->input('rebate_rate', 0), 'rebate_rate' => $rebateRate,
'extra_rebate_rate' => (float) $request->input('extra_rebate_rate', 0), 'extra_rebate_rate' => $extraRebateRate,
'created_at' => now(), 'created_at' => now(),
'updated_at' => now(), 'updated_at' => now(),
]); ]);
@@ -180,12 +188,12 @@ final class AdminPlayerStoreController extends Controller
return (int) $requested; return (int) $requested;
} }
$siteId = \App\Models\AdminSite::query()->where('code', $siteCode)->value('id'); $siteId = AdminSite::query()->where('code', $siteCode)->value('id');
if ($siteId === null) { if ($siteId === null) {
return null; return null;
} }
$rootId = \App\Models\AgentNode::query() $rootId = AgentNode::query()
->where('admin_site_id', (int) $siteId) ->where('admin_site_id', (int) $siteId)
->where('depth', 0) ->where('depth', 0)
->value('id'); ->value('id');

View File

@@ -2,19 +2,19 @@
namespace App\Http\Controllers\Api\V1\Admin\Player; namespace App\Http\Controllers\Api\V1\Admin\Player;
use App\Http\Controllers\Controller;
use App\Http\Requests\Admin\AdminPlayerUpdateRequest;
use App\Models\AgentNode;
use App\Models\Player; use App\Models\Player;
use App\Models\AgentNode;
use App\Support\ApiResponse;
use App\Support\AdminSiteScope;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use App\Support\PlayerApiPresenter;
use App\Http\Controllers\Controller;
use App\Services\Agent\AgentProfileService; use App\Services\Agent\AgentProfileService;
use App\Services\Agent\RebateLimitValidator; use App\Services\Agent\RebateLimitValidator;
use App\Services\Player\PlayerCreditService; use App\Services\Player\PlayerCreditService;
use App\Services\Player\PlayerRebateProfileService; use App\Services\Player\PlayerRebateProfileService;
use App\Support\AdminSiteScope; use App\Http\Requests\Admin\AdminPlayerUpdateRequest;
use App\Support\ApiResponse;
use App\Support\PlayerApiPresenter;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
/** PUT /api/v1/admin/players/{player} */ /** PUT /api/v1/admin/players/{player} */
final class AdminPlayerUpdateController extends Controller final class AdminPlayerUpdateController extends Controller
@@ -44,11 +44,15 @@ final class AdminPlayerUpdateController extends Controller
? AgentNode::query()->find((int) $player->agent_node_id) ? AgentNode::query()->find((int) $player->agent_node_id)
: null; : null;
$rebateRate = 0.0;
$extraRebateRate = 0.0;
if ($agent !== null && $request->has('rebate_rate')) { if ($agent !== null && $request->has('rebate_rate')) {
$rebateRate = (float) $request->input('rebate_rate', 0) / 100;
$extraRebateRate = (float) $request->input('extra_rebate_rate', 0) / 100;
$rebateLimitValidator->assertPlayerRebateWithinAgent( $rebateLimitValidator->assertPlayerRebateWithinAgent(
$agent, $agent,
(float) $request->input('rebate_rate', 0), $rebateRate,
(float) $request->input('extra_rebate_rate', 0), $extraRebateRate,
); );
} }
@@ -68,8 +72,8 @@ final class AdminPlayerUpdateController extends Controller
['player_id' => $player->id, 'game_type' => '*'], ['player_id' => $player->id, 'game_type' => '*'],
[ [
'inherit_from_agent' => false, 'inherit_from_agent' => false,
'rebate_rate' => (float) $request->input('rebate_rate', 0), 'rebate_rate' => $rebateRate,
'extra_rebate_rate' => (float) $request->input('extra_rebate_rate', 0), 'extra_rebate_rate' => $extraRebateRate,
'updated_at' => now(), 'updated_at' => now(),
'created_at' => now(), 'created_at' => now(),
], ],

View File

@@ -30,6 +30,11 @@ final class AdminIntegrationSiteStoreRequest extends ApiFormRequest
'iframe_allowed_origins.*' => ['string', 'max:512'], 'iframe_allowed_origins.*' => ['string', 'max:512'],
'lottery_h5_base_url' => ['nullable', 'string', 'max:512'], 'lottery_h5_base_url' => ['nullable', 'string', 'max:512'],
'notes' => ['nullable', 'string', 'max:5000'], 'notes' => ['nullable', 'string', 'max:5000'],
'admin_account' => ['required', 'array'],
'admin_account.username' => ['required', 'string', 'max:64', Rule::unique('admin_users', 'username')],
'admin_account.nickname' => ['required', 'string', 'max:64'],
'admin_account.password' => ['required', 'string', 'min:8', 'max:255'],
'admin_account.email' => ['nullable', 'email', 'max:255'],
]; ];
} }
} }

View File

@@ -29,8 +29,8 @@ final class AdminPlayerStoreRequest extends ApiFormRequest
'status' => ['sometimes', 'integer', 'in:0,1,2'], 'status' => ['sometimes', 'integer', 'in:0,1,2'],
'agent_node_id' => ['sometimes', 'nullable', 'integer', 'min:1'], 'agent_node_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
'credit_limit' => ['sometimes', 'integer', 'min:0'], 'credit_limit' => ['sometimes', 'integer', 'min:0'],
'rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:1'], 'rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'],
'extra_rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:1'], 'extra_rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'],
]; ];
} }

View File

@@ -25,12 +25,12 @@ final class AdminPlayerUpdateRequest extends ApiFormRequest
'default_currency' => ['sometimes', 'string', 'max:16', Rule::exists('currencies', 'code')], 'default_currency' => ['sometimes', 'string', 'max:16', Rule::exists('currencies', 'code')],
'status' => ['sometimes', 'integer', Rule::in([0, 1, 2])], 'status' => ['sometimes', 'integer', Rule::in([0, 1, 2])],
'credit_limit' => ['sometimes', 'integer', 'min:0'], 'credit_limit' => ['sometimes', 'integer', 'min:0'],
'rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:1'], 'rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'],
'extra_rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:1'], 'extra_rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'],
'rebate_profiles' => ['sometimes', 'array'], 'rebate_profiles' => ['sometimes', 'array'],
'rebate_profiles.*.game_type' => ['required_with:rebate_profiles', 'string', 'max:32'], 'rebate_profiles.*.game_type' => ['required_with:rebate_profiles', 'string', 'max:32'],
'rebate_profiles.*.rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:1'], 'rebate_profiles.*.rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'],
'rebate_profiles.*.extra_rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:1'], 'rebate_profiles.*.extra_rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'],
'rebate_profiles.*.inherit_from_agent' => ['sometimes', 'boolean'], 'rebate_profiles.*.inherit_from_agent' => ['sometimes', 'boolean'],
'risk_tags' => ['sometimes', 'array'], 'risk_tags' => ['sometimes', 'array'],
'risk_tags.*' => ['string', 'max:64'], 'risk_tags.*' => ['string', 'max:64'],

View File

@@ -2,8 +2,6 @@
namespace App\Http\Requests\Admin\Concerns; namespace App\Http\Requests\Admin\Concerns;
use App\Support\AgentSettlementCycle;
trait AgentProfileFieldRules trait AgentProfileFieldRules
{ {
/** @return array<string, mixed> */ /** @return array<string, mixed> */
@@ -15,7 +13,6 @@ trait AgentProfileFieldRules
'credit_limit' => ['sometimes', 'integer', 'min:0'], 'credit_limit' => ['sometimes', 'integer', 'min:0'],
'rebate_limit' => ['sometimes', 'numeric', 'min:0', 'max:100'], 'rebate_limit' => ['sometimes', 'numeric', 'min:0', 'max:100'],
'default_player_rebate' => ['sometimes', 'numeric', 'min:0', 'max:100'], 'default_player_rebate' => ['sometimes', 'numeric', 'min:0', 'max:100'],
'settlement_cycle' => ['sometimes', 'string', 'in:'.implode(',', AgentSettlementCycle::VALUES)],
'can_grant_extra_rebate' => ['sometimes', 'boolean'], 'can_grant_extra_rebate' => ['sometimes', 'boolean'],
'can_create_child_agent' => ['sometimes', 'boolean'], 'can_create_child_agent' => ['sometimes', 'boolean'],
'can_create_player' => ['sometimes', 'boolean'], 'can_create_player' => ['sometimes', 'boolean'],
@@ -26,12 +23,6 @@ trait AgentProfileFieldRules
protected function prepareAgentProfileFieldsForValidation(): void protected function prepareAgentProfileFieldsForValidation(): void
{ {
if (! $this->has('settlement_cycle')) { // 预处理字段(如需要)
return;
}
$this->merge([
'settlement_cycle' => AgentSettlementCycle::normalize($this->input('settlement_cycle')),
]);
} }
} }

View File

@@ -21,7 +21,6 @@ final class AgentProfile extends Model
'used_credit', 'used_credit',
'rebate_limit', 'rebate_limit',
'default_player_rebate', 'default_player_rebate',
'settlement_cycle',
'can_grant_extra_rebate', 'can_grant_extra_rebate',
'can_create_child_agent', 'can_create_child_agent',
'can_create_player', 'can_create_player',

View File

@@ -77,7 +77,6 @@ final class AgentDashboardOverviewBuilder
(int) ($profile?->credit_limit ?? 0) - (int) ($profile?->allocated_credit ?? 0), (int) ($profile?->credit_limit ?? 0) - (int) ($profile?->allocated_credit ?? 0),
), ),
'total_share_rate' => (float) ($profile?->total_share_rate ?? 0), 'total_share_rate' => (float) ($profile?->total_share_rate ?? 0),
'settlement_cycle' => (string) ($profile?->settlement_cycle ?? 'weekly'),
'can_create_child_agent' => (bool) ($profile?->can_create_child_agent ?? false), 'can_create_child_agent' => (bool) ($profile?->can_create_child_agent ?? false),
'can_create_player' => (bool) ($profile?->can_create_player ?? false), 'can_create_player' => (bool) ($profile?->can_create_player ?? false),
'direct_child_count' => $directChildCount, 'direct_child_count' => $directChildCount,

View File

@@ -30,7 +30,6 @@ final class AgentNodeService
* credit_limit?: int, * credit_limit?: int,
* rebate_limit?: float|int, * rebate_limit?: float|int,
* default_player_rebate?: float|int, * default_player_rebate?: float|int,
* settlement_cycle?: string,
* can_grant_extra_rebate?: bool, * can_grant_extra_rebate?: bool,
* can_create_child_agent?: bool, * can_create_child_agent?: bool,
* can_create_player?: bool * can_create_player?: bool
@@ -125,7 +124,6 @@ final class AgentNodeService
'credit_limit' => (int) ($payload['credit_limit'] ?? 0), 'credit_limit' => (int) ($payload['credit_limit'] ?? 0),
'rebate_limit' => (float) ($payload['rebate_limit'] ?? 0), 'rebate_limit' => (float) ($payload['rebate_limit'] ?? 0),
'default_player_rebate' => (float) ($payload['default_player_rebate'] ?? 0), 'default_player_rebate' => (float) ($payload['default_player_rebate'] ?? 0),
'settlement_cycle' => (string) ($payload['settlement_cycle'] ?? 'weekly'),
'can_grant_extra_rebate' => (bool) ($payload['can_grant_extra_rebate'] ?? false), 'can_grant_extra_rebate' => (bool) ($payload['can_grant_extra_rebate'] ?? false),
'can_create_child_agent' => (bool) ($payload['can_create_child_agent'] ?? false), 'can_create_child_agent' => (bool) ($payload['can_create_child_agent'] ?? false),
'can_create_player' => (bool) ($payload['can_create_player'] ?? true), 'can_create_player' => (bool) ($payload['can_create_player'] ?? true),

View File

@@ -7,7 +7,6 @@ use App\Models\AgentNode;
use App\Models\AgentProfile; use App\Models\AgentProfile;
use App\Support\AdminAgentScope; use App\Support\AdminAgentScope;
use App\Support\AgentOverdueGuard; use App\Support\AgentOverdueGuard;
use App\Support\AgentSettlementCycle;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException; use Illuminate\Validation\ValidationException;
@@ -86,9 +85,6 @@ final class AgentProfileService
'credit_limit' => $creditLimit, 'credit_limit' => $creditLimit,
'rebate_limit' => $rebateLimit, 'rebate_limit' => $rebateLimit,
'default_player_rebate' => $defaultRebate, 'default_player_rebate' => $defaultRebate,
'settlement_cycle' => AgentSettlementCycle::normalize(
$payload['settlement_cycle'] ?? $profile->settlement_cycle ?? 'weekly',
),
'can_grant_extra_rebate' => (bool) ($payload['can_grant_extra_rebate'] ?? $profile->can_grant_extra_rebate ?? false), 'can_grant_extra_rebate' => (bool) ($payload['can_grant_extra_rebate'] ?? $profile->can_grant_extra_rebate ?? false),
'can_create_child_agent' => (bool) ($payload['can_create_child_agent'] ?? ($isNew ? false : $profile->can_create_child_agent)), 'can_create_child_agent' => (bool) ($payload['can_create_child_agent'] ?? ($isNew ? false : $profile->can_create_child_agent)),
'can_create_player' => (bool) ($payload['can_create_player'] ?? ($isNew ? true : $profile->can_create_player ?? true)), 'can_create_player' => (bool) ($payload['can_create_player'] ?? ($isNew ? true : $profile->can_create_player ?? true)),
@@ -126,7 +122,6 @@ final class AgentProfileService
'available_credit' => $available, 'available_credit' => $available,
'rebate_limit' => round((float) $profile->rebate_limit * 100, 4), 'rebate_limit' => round((float) $profile->rebate_limit * 100, 4),
'default_player_rebate' => round((float) $profile->default_player_rebate * 100, 4), 'default_player_rebate' => round((float) $profile->default_player_rebate * 100, 4),
'settlement_cycle' => AgentSettlementCycle::normalize($profile->settlement_cycle),
'can_grant_extra_rebate' => (bool) $profile->can_grant_extra_rebate, 'can_grant_extra_rebate' => (bool) $profile->can_grant_extra_rebate,
'can_create_child_agent' => (bool) $profile->can_create_child_agent, 'can_create_child_agent' => (bool) $profile->can_create_child_agent,
'can_create_player' => (bool) $profile->can_create_player, 'can_create_player' => (bool) $profile->can_create_player,

View File

@@ -105,7 +105,6 @@ final class AgentSiteProvisioningService
'credit_limit' => (int) ($payload['credit_limit'] ?? $defaults['credit_limit'] ?? 0), 'credit_limit' => (int) ($payload['credit_limit'] ?? $defaults['credit_limit'] ?? 0),
'rebate_limit' => (float) ($payload['rebate_limit'] ?? $defaults['rebate_limit'] ?? 0), 'rebate_limit' => (float) ($payload['rebate_limit'] ?? $defaults['rebate_limit'] ?? 0),
'default_player_rebate' => (float) ($payload['default_player_rebate'] ?? $defaults['default_player_rebate'] ?? 0), 'default_player_rebate' => (float) ($payload['default_player_rebate'] ?? $defaults['default_player_rebate'] ?? 0),
'settlement_cycle' => (string) ($payload['settlement_cycle'] ?? $defaults['settlement_cycle'] ?? 'weekly'),
'can_grant_extra_rebate' => (bool) ($payload['can_grant_extra_rebate'] ?? true), 'can_grant_extra_rebate' => (bool) ($payload['can_grant_extra_rebate'] ?? true),
'can_create_child_agent' => (bool) ($payload['can_create_child_agent'] ?? true), 'can_create_child_agent' => (bool) ($payload['can_create_child_agent'] ?? true),
'can_create_player' => (bool) ($payload['can_create_player'] ?? true), 'can_create_player' => (bool) ($payload['can_create_player'] ?? true),

View File

@@ -15,6 +15,7 @@ final class RebateLimitValidator
return; return;
} }
// Both $rebateRate and $profile->rebate_limit are ratios (0-1)
$limit = (float) $profile->rebate_limit; $limit = (float) $profile->rebate_limit;
if ($rebateRate > $limit) { if ($rebateRate > $limit) {
throw ValidationException::withMessages([ throw ValidationException::withMessages([

View File

@@ -2,9 +2,9 @@
namespace App\Services\AgentSettlement; namespace App\Services\AgentSettlement;
use App\Models\Player;
use App\Models\AgentNode; use App\Models\AgentNode;
use App\Models\AgentProfile; use App\Models\AgentProfile;
use App\Models\Player;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
final class BetSettlementSnapshotBuilder final class BetSettlementSnapshotBuilder

View File

@@ -3,25 +3,45 @@
namespace App\Services\Integration; namespace App\Services\Integration;
use App\Models\AdminSite; use App\Models\AdminSite;
use App\Models\AdminRole;
use App\Models\AdminUser;
use App\Support\AdminPermissionInheritance;
use Illuminate\Support\Str; use Illuminate\Support\Str;
use Illuminate\Validation\ValidationException;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
final class IntegrationSiteService 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( public function __construct(
private readonly PartnerSiteConfigResolver $configResolver, private readonly PartnerSiteConfigResolver $configResolver,
) {} ) {}
/** /**
* @param array<string, mixed> $data * @param array<string, mixed> $data
* @return array{site: AdminSite, secrets: array{sso_jwt_secret: string, wallet_api_key: string}} * @return array{site: AdminSite, secrets: array{sso_jwt_secret: string, wallet_api_key: string}, admin_user: AdminUser}
*/ */
public function create(array $data): array public function create(array $data): array
{ {
$secrets = $this->generateSecrets(); $secrets = $this->generateSecrets();
$site = DB::transaction(function () use ($data, $secrets): AdminSite { ['site' => $site, 'admin_user' => $adminUser] = DB::transaction(function () use ($data, $secrets): array {
return AdminSite::query()->create([ /** @var array{username: string, nickname: string, password: string, email?: string|null} $adminAccount */
$adminAccount = $data['admin_account'];
$site = AdminSite::query()->create([
'code' => (string) $data['code'], 'code' => (string) $data['code'],
'name' => (string) $data['name'], 'name' => (string) $data['name'],
'currency_code' => (string) ($data['currency_code'] ?? 'NPR'), 'currency_code' => (string) ($data['currency_code'] ?? 'NPR'),
@@ -38,11 +58,23 @@ final class IntegrationSiteService
'sso_jwt_secret_encrypted' => encrypt($secrets['sso_jwt_secret']), 'sso_jwt_secret_encrypted' => encrypt($secrets['sso_jwt_secret']),
'wallet_api_key_encrypted' => encrypt($secrets['wallet_api_key']), 'wallet_api_key_encrypted' => encrypt($secrets['wallet_api_key']),
]); ]);
$role = $this->createSiteAdminRole($site);
$adminUser = $this->createSiteAdminUser($site, $role, $adminAccount);
return [
'site' => $site,
'admin_user' => $adminUser,
];
}); });
$this->configResolver->forgetCache((string) $site->code); $this->configResolver->forgetCache((string) $site->code);
return ['site' => $site->fresh(), 'secrets' => $secrets]; return [
'site' => $site->fresh(),
'secrets' => $secrets,
'admin_user' => $adminUser->fresh(),
];
} }
/** /**
@@ -114,4 +146,53 @@ final class IntegrationSiteService
return $trimmed === '' ? null : $trimmed; 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
*/
private function createSiteAdminUser(AdminSite $site, AdminRole $role, array $adminAccount): AdminUser
{
$username = trim((string) ($adminAccount['username'] ?? ''));
$nickname = trim((string) ($adminAccount['nickname'] ?? ''));
$password = (string) ($adminAccount['password'] ?? '');
$email = $this->nullableTrim($adminAccount['email'] ?? null);
if ($username === '' || $nickname === '' || $password === '') {
throw ValidationException::withMessages([
'admin_account' => ['站点后台管理账号信息不完整。'],
]);
}
$user = AdminUser::query()->create([
'username' => $username,
'name' => $nickname,
'email' => $email,
'password' => $password,
'status' => 0,
]);
$user->syncSystemRoleSlugsForSite((int) $site->id, [(string) $role->slug]);
return $user;
}
} }

View File

@@ -3,8 +3,8 @@
namespace App\Services\Player; namespace App\Services\Player;
use App\Models\AgentNode; use App\Models\AgentNode;
use App\Services\Agent\RebateLimitValidator;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
use App\Services\Agent\RebateLimitValidator;
final class PlayerRebateProfileService final class PlayerRebateProfileService
{ {
@@ -25,8 +25,8 @@ final class PlayerRebateProfileService
foreach ($profiles as $row) { foreach ($profiles as $row) {
$gameType = trim((string) ($row['game_type'] ?? '*')) ?: '*'; $gameType = trim((string) ($row['game_type'] ?? '*')) ?: '*';
$inherit = (bool) ($row['inherit_from_agent'] ?? false); $inherit = (bool) ($row['inherit_from_agent'] ?? false);
$rebateRate = (float) ($row['rebate_rate'] ?? 0); $rebateRate = (float) ($row['rebate_rate'] ?? 0) / 100;
$extraRate = (float) ($row['extra_rebate_rate'] ?? 0); $extraRate = (float) ($row['extra_rebate_rate'] ?? 0) / 100;
if (! $inherit) { if (! $inherit) {
$this->rebateLimitValidator->assertPlayerRebateWithinAgent($agent, $rebateRate, $extraRate); $this->rebateLimitValidator->assertPlayerRebateWithinAgent($agent, $rebateRate, $extraRate);
@@ -56,8 +56,8 @@ final class PlayerRebateProfileService
->get() ->get()
->map(static fn (object $row): array => [ ->map(static fn (object $row): array => [
'game_type' => (string) $row->game_type, 'game_type' => (string) $row->game_type,
'rebate_rate' => (float) $row->rebate_rate, 'rebate_rate' => round((float) $row->rebate_rate * 100, 4),
'extra_rebate_rate' => (float) $row->extra_rebate_rate, 'extra_rebate_rate' => round((float) $row->extra_rebate_rate * 100, 4),
'inherit_from_agent' => (bool) $row->inherit_from_agent, 'inherit_from_agent' => (bool) $row->inherit_from_agent,
]) ])
->all(); ->all();

View File

@@ -139,6 +139,7 @@ final class AdminAuthorizationRegistry
* platform_only?: bool, * platform_only?: bool,
* agent_hidden?: bool, * agent_hidden?: bool,
* activeMatchPrefix?: string, * activeMatchPrefix?: string,
* activeExact?: bool,
* requiredAny?: list<string> * requiredAny?: list<string>
* }> * }>
*/ */
@@ -146,7 +147,8 @@ final class AdminAuthorizationRegistry
{ {
return [ return [
['segment' => 'dashboard', 'label' => 'Dashboard', 'href' => '/admin', 'nav_group' => 'overview', 'requiredAny' => ['prd.dashboard.view']], ['segment' => 'dashboard', 'label' => 'Dashboard', 'href' => '/admin', 'nav_group' => 'overview', 'requiredAny' => ['prd.dashboard.view']],
['segment' => 'agents', 'label' => 'Agent lines', 'href' => '/admin/agents', 'nav_group' => 'agent', 'activeMatchPrefix' => '/admin/agents', 'requiredAny' => array_values(array_unique(array_merge(['prd.agent.view', 'prd.agent.manage', 'prd.agent.role.view', 'prd.agent.role.manage', 'prd.agent.user.view', 'prd.agent.user.manage', 'prd.agent-line.provision', 'prd.agent.profile.manage'], AdminPermissionLanguage::requiredAnyPrdSlugs('integration-sites'))))], ['segment' => 'agents', 'label' => 'Agent lines', 'href' => '/admin/agents', 'nav_group' => 'agent', 'activeExact' => true, 'requiredAny' => array_values(array_unique(array_merge(['prd.agent.view', 'prd.agent.manage', 'prd.agent.role.view', 'prd.agent.role.manage', 'prd.agent.user.view', 'prd.agent.user.manage', 'prd.agent-line.provision', 'prd.agent.profile.manage'], AdminPermissionLanguage::requiredAnyPrdSlugs('integration-sites'))))],
['segment' => 'agent_list', 'label' => 'Agent list', 'href' => '/admin/agents/list', 'nav_group' => 'agent', 'activeMatchPrefix' => '/admin/agents/list', 'requiredAny' => ['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']],
['segment' => 'settlement_center', 'label' => 'Credit settlement', 'href' => '/admin/settlement-center', 'nav_group' => 'agent', 'activeMatchPrefix' => '/admin/settlement-center', 'requiredAny' => ['prd.settlement.agent.view', 'prd.settlement.agent.manage']], ['segment' => 'settlement_center', 'label' => 'Credit settlement', 'href' => '/admin/settlement-center', 'nav_group' => 'agent', 'activeMatchPrefix' => '/admin/settlement-center', 'requiredAny' => ['prd.settlement.agent.view', 'prd.settlement.agent.manage']],
['segment' => 'draws', 'label' => 'Draws', 'href' => '/admin/draws', 'nav_group' => 'operations', 'requiredAny' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.draw_reopen.manage']], ['segment' => 'draws', 'label' => 'Draws', 'href' => '/admin/draws', 'nav_group' => 'operations', 'requiredAny' => ['prd.draw_result.manage', 'prd.draw_result.view', 'prd.draw_reopen.manage']],
['segment' => 'tickets', 'label' => 'Tickets', 'href' => '/admin/tickets', 'nav_group' => 'operations', 'requiredAny' => ['prd.tickets.view']], ['segment' => 'tickets', 'label' => 'Tickets', 'href' => '/admin/tickets', 'nav_group' => 'operations', 'requiredAny' => ['prd.tickets.view']],
@@ -155,10 +157,10 @@ final class AdminAuthorizationRegistry
['segment' => 'wallet', 'label' => 'Wallet', 'href' => '/admin/wallet/transactions', 'nav_group' => 'finance', 'activeMatchPrefix' => '/admin/wallet', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage'], 'agent_hidden' => true], ['segment' => 'wallet', 'label' => 'Wallet', 'href' => '/admin/wallet/transactions', 'nav_group' => 'finance', 'activeMatchPrefix' => '/admin/wallet', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage'], 'agent_hidden' => true],
['segment' => 'reconcile', 'label' => 'Reconcile', 'href' => '/admin/reconcile', 'nav_group' => 'finance', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs'], 'agent_hidden' => true], ['segment' => 'reconcile', 'label' => 'Reconcile', 'href' => '/admin/reconcile', 'nav_group' => 'finance', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs'], 'agent_hidden' => true],
['segment' => 'reports', 'label' => 'Reports', 'href' => '/admin/reports', 'nav_group' => 'finance', 'requiredAny' => ['prd.report.view']], ['segment' => 'reports', 'label' => 'Reports', 'href' => '/admin/reports', 'nav_group' => 'finance', 'requiredAny' => ['prd.report.view']],
['segment' => 'rules_plays', 'label' => 'Play rules', 'href' => '/admin/rules/plays', 'nav_group' => 'rules', 'platform_only' => true, 'requiredAny' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']], ['segment' => 'rules_plays', 'label' => 'Play rules', 'href' => '/admin/rules/plays', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
['segment' => 'rules_odds', 'label' => 'Odds & rebate', 'href' => '/admin/rules/odds', 'nav_group' => 'rules', 'platform_only' => true, 'requiredAny' => ['prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view']], ['segment' => 'rules_odds', 'label' => 'Odds & rebate', 'href' => '/admin/rules/odds', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view']],
['segment' => 'jackpot', 'label' => 'Jackpot', 'href' => '/admin/jackpot', 'nav_group' => 'rules', 'platform_only' => true, 'activeMatchPrefix' => '/admin/jackpot', 'requiredAny' => ['prd.jackpot.manage', 'prd.jackpot.view']], ['segment' => 'jackpot', 'label' => 'Jackpot', 'href' => '/admin/jackpot', 'nav_group' => 'platform', 'platform_only' => true, 'activeMatchPrefix' => '/admin/jackpot', 'requiredAny' => ['prd.jackpot.manage', 'prd.jackpot.view']],
['segment' => 'risk_cap', 'label' => 'Risk cap rules', 'href' => '/admin/risk/cap', 'nav_group' => 'rules', 'platform_only' => true, 'activeMatchPrefix' => '/admin/risk/cap', 'requiredAny' => ['prd.risk_cap.manage', 'prd.risk_cap.view']], ['segment' => 'risk_cap', 'label' => 'Risk cap rules', 'href' => '/admin/risk/cap', 'nav_group' => 'platform', 'platform_only' => true, 'activeMatchPrefix' => '/admin/risk/cap', 'requiredAny' => ['prd.risk_cap.manage', 'prd.risk_cap.view']],
['segment' => 'currencies', 'label' => 'Currencies', 'href' => '/admin/currencies', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.currency.manage']], ['segment' => 'currencies', 'label' => 'Currencies', 'href' => '/admin/currencies', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.currency.manage']],
['segment' => 'integration', 'label' => 'Integration sites', 'href' => '/admin/config/integration-sites', 'nav_group' => 'platform', 'platform_only' => true, 'activeMatchPrefix' => '/admin/config/integration-sites', 'requiredAny' => AdminPermissionLanguage::requiredAnyPrdSlugs('integration-sites')], ['segment' => 'integration', 'label' => 'Integration sites', 'href' => '/admin/config/integration-sites', 'nav_group' => 'platform', 'platform_only' => true, 'activeMatchPrefix' => '/admin/config/integration-sites', 'requiredAny' => AdminPermissionLanguage::requiredAnyPrdSlugs('integration-sites')],
['segment' => 'admin_users', 'label' => 'Admin Users', 'href' => '/admin/admin-users', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.admin_user.manage']], ['segment' => 'admin_users', 'label' => 'Admin Users', 'href' => '/admin/admin-users', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.admin_user.manage']],
@@ -279,6 +281,7 @@ final class AdminAuthorizationRegistry
* platform_only?: bool, * platform_only?: bool,
* agent_hidden?: bool, * agent_hidden?: bool,
* activeMatchPrefix?: string, * activeMatchPrefix?: string,
* activeExact?: bool,
* requiredAny?: list<string> * requiredAny?: list<string>
* }> * }>
*/ */
@@ -297,6 +300,7 @@ final class AdminAuthorizationRegistry
* platform_only?: bool, * platform_only?: bool,
* agent_hidden?: bool, * agent_hidden?: bool,
* activeMatchPrefix?: string, * activeMatchPrefix?: string,
* activeExact?: bool,
* requiredAny?: list<string> * requiredAny?: list<string>
* }> * }>
*/ */
@@ -420,6 +424,7 @@ final class AdminAuthorizationRegistry
['code' => 'admin.agent-lines.store', 'module_code' => 'agent', 'name' => '开通代理线路', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/agent-lines', 'route_name' => 'api.v1.admin.agent-lines.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.line.provision'], 'legacy_permission_slugs' => ['prd.agent-line.provision', 'prd.agent.manage']], ['code' => 'admin.agent-lines.store', 'module_code' => 'agent', 'name' => '开通代理线路', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/agent-lines', 'route_name' => 'api.v1.admin.agent-lines.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.line.provision'], 'legacy_permission_slugs' => ['prd.agent-line.provision', 'prd.agent.manage']],
['code' => 'admin.agent-lines.show', 'module_code' => 'agent', 'name' => '代理线路详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-lines/{admin_site}', 'route_name' => 'api.v1.admin.agent-lines.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.line.provision', 'agent.node.view', 'agent.node.manage'], 'legacy_permission_slugs' => ['prd.agent-line.provision', 'prd.agent.view', 'prd.agent.manage']], ['code' => 'admin.agent-lines.show', 'module_code' => 'agent', 'name' => '代理线路详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-lines/{admin_site}', 'route_name' => 'api.v1.admin.agent-lines.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.line.provision', 'agent.node.view', 'agent.node.manage'], 'legacy_permission_slugs' => ['prd.agent-line.provision', 'prd.agent.view', 'prd.agent.manage']],
['code' => 'admin.agent-nodes.tree', 'module_code' => 'agent', 'name' => '代理树', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/tree', 'route_name' => 'api.v1.admin.agent-nodes.tree', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage', 'agent.role.view', 'agent.role.manage', 'agent.user.view', 'agent.user.manage']], ['code' => 'admin.agent-nodes.tree', 'module_code' => 'agent', 'name' => '代理树', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/tree', 'route_name' => 'api.v1.admin.agent-nodes.tree', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage', 'agent.role.view', 'agent.role.manage', 'agent.user.view', 'agent.user.manage']],
['code' => 'admin.agent-nodes.index', 'module_code' => 'agent', 'name' => '代理列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes', 'route_name' => 'api.v1.admin.agent-nodes.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage', 'agent.role.view', 'agent.role.manage', 'agent.user.view', 'agent.user.manage']],
['code' => 'admin.agent-nodes.store', 'module_code' => 'agent', 'name' => '创建下级代理', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/agent-nodes', 'route_name' => 'api.v1.admin.agent-nodes.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']], ['code' => 'admin.agent-nodes.store', 'module_code' => 'agent', 'name' => '创建下级代理', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/agent-nodes', 'route_name' => 'api.v1.admin.agent-nodes.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
['code' => 'admin.agent-nodes.show', 'module_code' => 'agent', 'name' => '代理节点详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}', 'route_name' => 'api.v1.admin.agent-nodes.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']], ['code' => 'admin.agent-nodes.show', 'module_code' => 'agent', 'name' => '代理节点详情', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}', 'route_name' => 'api.v1.admin.agent-nodes.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']],
['code' => 'admin.agent-nodes.update', 'module_code' => 'agent', 'name' => '更新代理节点', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}', 'route_name' => 'api.v1.admin.agent-nodes.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']], ['code' => 'admin.agent-nodes.update', 'module_code' => 'agent', 'name' => '更新代理节点', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}', 'route_name' => 'api.v1.admin.agent-nodes.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],

View File

@@ -36,9 +36,8 @@ final class AgentNodePresenter
'allocated_credit' => (int) $profile->allocated_credit, 'allocated_credit' => (int) $profile->allocated_credit,
'used_credit' => (int) $profile->used_credit, 'used_credit' => (int) $profile->used_credit,
'available_credit' => max(0, (int) $profile->credit_limit - (int) $profile->allocated_credit), 'available_credit' => max(0, (int) $profile->credit_limit - (int) $profile->allocated_credit),
'rebate_limit' => (float) $profile->rebate_limit, 'rebate_limit' => round((float) $profile->rebate_limit * 100, 4),
'default_player_rebate' => (float) $profile->default_player_rebate, 'default_player_rebate' => round((float) $profile->default_player_rebate * 100, 4),
'settlement_cycle' => AgentSettlementCycle::normalize($profile->settlement_cycle),
]; ];
} }
@@ -126,4 +125,26 @@ final class AgentNodePresenter
return $roots; return $roots;
} }
/**
* @param iterable<AgentNode> $nodes
* @return list<array<string, mixed>>
*/
public static function list(iterable $nodes): array
{
$nodeList = $nodes instanceof Collection ? $nodes : collect($nodes);
$profiles = AgentProfile::query()
->whereIn('agent_node_id', $nodeList->pluck('id'))
->get()
->keyBy('agent_node_id');
return $nodeList
->map(function (AgentNode $node) use ($profiles): array {
$profile = $profiles->get($node->id);
return self::item($node, $profile instanceof AgentProfile ? $profile : null);
})
->values()
->all();
}
} }

View File

@@ -2,10 +2,10 @@
namespace App\Support; namespace App\Support;
use App\Models\AgentProfile;
use App\Models\Player; use App\Models\Player;
use App\Models\AgentNode;
use App\Models\AgentProfile;
use App\Models\PlayerWallet; use App\Models\PlayerWallet;
use App\Support\PlayerFundingMode;
use Illuminate\Support\Facades\DB; use Illuminate\Support\Facades\DB;
/** 玩家 API 统一 JSON 形状(列表行 / 详情)。 */ /** 玩家 API 统一 JSON 形状(列表行 / 详情)。 */
@@ -68,8 +68,8 @@ final class PlayerApiPresenter
->get() ->get()
->map(static fn (object $row): array => [ ->map(static fn (object $row): array => [
'game_type' => (string) $row->game_type, 'game_type' => (string) $row->game_type,
'rebate_rate' => (float) $row->rebate_rate, 'rebate_rate' => round((float) $row->rebate_rate * 100, 4),
'extra_rebate_rate' => (float) $row->extra_rebate_rate, 'extra_rebate_rate' => round((float) $row->extra_rebate_rate * 100, 4),
'inherit_from_agent' => (bool) $row->inherit_from_agent, 'inherit_from_agent' => (bool) $row->inherit_from_agent,
]) ])
->all(), ->all(),
@@ -79,7 +79,7 @@ final class PlayerApiPresenter
/** /**
* @return array{0: ?float, 1: bool} rebate rate (ratio) and whether inherited from agent * @return array{0: ?float, 1: bool} rebate rate (ratio) and whether inherited from agent
*/ */
private static function resolveListRebate(Player $player, ?\App\Models\AgentNode $agent): array private static function resolveListRebate(Player $player, ?AgentNode $agent): array
{ {
$row = DB::table('player_rebate_profiles') $row = DB::table('player_rebate_profiles')
->where('player_id', $player->id) ->where('player_id', $player->id)
@@ -87,13 +87,13 @@ final class PlayerApiPresenter
->first(); ->first();
if ($row !== null && ! (bool) $row->inherit_from_agent) { if ($row !== null && ! (bool) $row->inherit_from_agent) {
return [(float) $row->rebate_rate, false]; return [round((float) $row->rebate_rate * 100, 4), false];
} }
if ($agent !== null) { if ($agent !== null) {
$profile = AgentProfile::query()->where('agent_node_id', $agent->id)->first(); $profile = AgentProfile::query()->where('agent_node_id', $agent->id)->first();
return [(float) ($profile?->default_player_rebate ?? 0), true]; return [round((float) ($profile?->default_player_rebate ?? 0) * 100, 4), true];
} }
return [null, false]; return [null, false];

View File

@@ -1,8 +1,8 @@
<?php <?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema; use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration return new class extends Migration
{ {
@@ -34,8 +34,8 @@ return new class extends Migration
$table->foreignId('player_id')->constrained('players')->cascadeOnDelete(); $table->foreignId('player_id')->constrained('players')->cascadeOnDelete();
$table->string('game_type', 32)->default('*'); $table->string('game_type', 32)->default('*');
$table->boolean('inherit_from_agent')->default(true); $table->boolean('inherit_from_agent')->default(true);
$table->decimal('rebate_rate', 8, 4)->default(0); $table->decimal('rebate_rate', 8, 4)->default(0)->comment('回水比例 0-100');
$table->decimal('extra_rebate_rate', 8, 4)->default(0); $table->decimal('extra_rebate_rate', 8, 4)->default(0)->comment('额外回水比例 0-100');
$table->timestamps(); $table->timestamps();
$table->unique(['player_id', 'game_type']); $table->unique(['player_id', 'game_type']);
}); });

View File

@@ -0,0 +1,89 @@
<?php
use App\Support\AdminAuthorizationRegistry;
use Illuminate\Database\Migrations\Migration;
use Illuminate\Support\Carbon;
use Illuminate\Support\Facades\DB;
/** 补齐代理列表 API 资源,避免 api_resource_not_configured。 */
return new class extends Migration
{
private const RESOURCE_CODE = 'admin.agent-nodes.index';
public function up(): void
{
$resource = collect(AdminAuthorizationRegistry::resources())
->firstWhere('code', self::RESOURCE_CODE);
if (! is_array($resource)) {
return;
}
$now = Carbon::now();
$menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code');
$resourceId = DB::table('admin_api_resources')
->where('code', self::RESOURCE_CODE)
->value('id');
$payload = [
'module_code' => $resource['module_code'],
'name' => $resource['name'],
'http_method' => $resource['http_method'],
'uri_pattern' => $resource['uri_pattern'],
'route_name' => $resource['route_name'],
'auth_mode' => $resource['auth_mode'],
'is_audit_required' => $resource['is_audit_required'],
'status' => 1,
'meta_json' => null,
'updated_at' => $now,
];
if ($resourceId === null) {
$resourceId = DB::table('admin_api_resources')->insertGetId($payload + [
'code' => self::RESOURCE_CODE,
'created_at' => $now,
]);
} else {
DB::table('admin_api_resources')
->where('id', (int) $resourceId)
->update($payload);
}
DB::table('admin_api_resource_bindings')
->where('api_resource_id', (int) $resourceId)
->delete();
foreach ($resource['permission_codes'] as $permissionCode) {
$menuActionId = $menuActionIds[$permissionCode] ?? null;
if ($menuActionId === null) {
continue;
}
DB::table('admin_api_resource_bindings')->insert([
'api_resource_id' => (int) $resourceId,
'menu_action_id' => (int) $menuActionId,
'created_at' => $now,
'updated_at' => $now,
]);
}
}
public function down(): void
{
$resourceId = DB::table('admin_api_resources')
->where('code', self::RESOURCE_CODE)
->value('id');
if ($resourceId === null) {
return;
}
DB::table('admin_api_resource_bindings')
->where('api_resource_id', (int) $resourceId)
->delete();
DB::table('admin_api_resources')
->where('id', (int) $resourceId)
->delete();
}
};

View File

@@ -2,6 +2,7 @@
use Illuminate\Support\Facades\Route; use Illuminate\Support\Facades\Route;
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeTreeController; use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeTreeController;
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeIndexController;
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeShowController; use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeShowController;
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeStoreController; use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeStoreController;
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeUpdateController; use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeUpdateController;
@@ -30,6 +31,8 @@ Route::middleware('admin.api-resource')
->name('api.v1.admin.agent-lines.show'); ->name('api.v1.admin.agent-lines.show');
Route::get('agent-nodes/tree', AgentNodeTreeController::class) Route::get('agent-nodes/tree', AgentNodeTreeController::class)
->name('api.v1.admin.agent-nodes.tree'); ->name('api.v1.admin.agent-nodes.tree');
Route::get('agent-nodes', AgentNodeIndexController::class)
->name('api.v1.admin.agent-nodes.index');
Route::post('agent-nodes', AgentNodeStoreController::class) Route::post('agent-nodes', AgentNodeStoreController::class)
->name('api.v1.admin.agent-nodes.store'); ->name('api.v1.admin.agent-nodes.store');
Route::get('agent-nodes/{agent_node}/roles', AgentNodeRoleIndexController::class) Route::get('agent-nodes/{agent_node}/roles', AgentNodeRoleIndexController::class)

View File

@@ -44,7 +44,6 @@ test('super admin can update agent profile with capability flags', function ():
'credit_limit' => 1200, 'credit_limit' => 1200,
'rebate_limit' => 1, 'rebate_limit' => 1,
'default_player_rebate' => 0.5, 'default_player_rebate' => 0.5,
'settlement_cycle' => 'weekly',
'can_grant_extra_rebate' => false, 'can_grant_extra_rebate' => false,
'can_create_child_agent' => true, 'can_create_child_agent' => true,
'can_create_player' => true, 'can_create_player' => true,

View File

@@ -38,7 +38,6 @@ test('agent profile switches strip create player and child manage from effective
'used_credit' => 0, 'used_credit' => 0,
'rebate_limit' => 0, 'rebate_limit' => 0,
'default_player_rebate' => 0, 'default_player_rebate' => 0,
'settlement_cycle' => 'weekly',
'can_grant_extra_rebate' => false, 'can_grant_extra_rebate' => false,
'can_create_child_agent' => false, 'can_create_child_agent' => false,
'can_create_player' => false, 'can_create_player' => false,
@@ -98,7 +97,6 @@ test('agent profile switches on grant create capabilities even when platform age
'used_credit' => 0, 'used_credit' => 0,
'rebate_limit' => 0, 'rebate_limit' => 0,
'default_player_rebate' => 0, 'default_player_rebate' => 0,
'settlement_cycle' => 'weekly',
'can_grant_extra_rebate' => false, 'can_grant_extra_rebate' => false,
'can_create_child_agent' => true, 'can_create_child_agent' => true,
'can_create_player' => true, 'can_create_player' => true,

View File

@@ -40,15 +40,22 @@ test('super admin can create integration site and receive secrets once', functio
'name' => 'Partner A', 'name' => 'Partner A',
'wallet_api_url' => 'https://wallet.partner-a.test', 'wallet_api_url' => 'https://wallet.partner-a.test',
'status' => 1, 'status' => 1,
'admin_account' => [
'username' => 'partner_a_admin',
'nickname' => 'Partner A Admin',
'password' => 'secret-strong',
],
]); ]);
$response->assertCreated() $response->assertCreated()
->assertJsonPath('code', 0) ->assertJsonPath('code', 0)
->assertJsonPath('data.code', 'partner-a') ->assertJsonPath('data.code', 'partner-a')
->assertJsonPath('data.secrets_display_once', true) ->assertJsonPath('data.secrets_display_once', true)
->assertJsonPath('data.admin_user.username', 'partner_a_admin')
->assertJsonStructure([ ->assertJsonStructure([
'data' => [ 'data' => [
'secrets' => ['sso_jwt_secret', 'wallet_api_key'], 'secrets' => ['sso_jwt_secret', 'wallet_api_key'],
'admin_user' => ['id', 'username', 'nickname', 'email'],
], ],
]); ]);
@@ -66,6 +73,11 @@ test('super admin can reveal integration site secrets for copy', function (): vo
->postJson('/api/v1/admin/integration-sites', [ ->postJson('/api/v1/admin/integration-sites', [
'code' => 'partner-secrets', 'code' => 'partner-secrets',
'name' => 'Partner Secrets', 'name' => 'Partner Secrets',
'admin_account' => [
'username' => 'partner_secrets_admin',
'nickname' => 'Partner Secrets Admin',
'password' => 'secret-strong',
],
]) ])
->assertCreated(); ->assertCreated();
@@ -94,6 +106,11 @@ test('integration site code cannot be changed on update', function (): void {
->postJson('/api/v1/admin/integration-sites', [ ->postJson('/api/v1/admin/integration-sites', [
'code' => 'partner-b', 'code' => 'partner-b',
'name' => 'Partner B', 'name' => 'Partner B',
'admin_account' => [
'username' => 'partner_b_admin',
'nickname' => 'Partner B Admin',
'password' => 'secret-strong',
],
]); ]);
$create->assertCreated(); $create->assertCreated();
$id = (int) $create->json('data.id'); $id = (int) $create->json('data.id');
@@ -154,6 +171,11 @@ test('rotate secrets returns new plaintext once', function (): void {
->postJson('/api/v1/admin/integration-sites', [ ->postJson('/api/v1/admin/integration-sites', [
'code' => 'partner-rotate', 'code' => 'partner-rotate',
'name' => 'Rotate', 'name' => 'Rotate',
'admin_account' => [
'username' => 'partner_rotate_admin',
'nickname' => 'Partner Rotate Admin',
'password' => 'secret-strong',
],
]); ]);
$id = (int) $create->json('data.id'); $id = (int) $create->json('data.id');
$oldSecret = (string) $create->json('data.secrets.sso_jwt_secret'); $oldSecret = (string) $create->json('data.secrets.sso_jwt_secret');
@@ -183,6 +205,11 @@ test('connectivity test probes partner balance api', function (): void {
'code' => 'probe-site', 'code' => 'probe-site',
'name' => 'Probe', 'name' => 'Probe',
'wallet_api_url' => 'https://wallet.probe.test', 'wallet_api_url' => 'https://wallet.probe.test',
'admin_account' => [
'username' => 'probe_site_admin',
'nickname' => 'Probe Site Admin',
'password' => 'secret-strong',
],
]); ]);
$id = (int) $create->json('data.id'); $id = (int) $create->json('data.id');
@@ -205,6 +232,11 @@ test('export parameter sheet excludes plaintext secrets', function (): void {
'code' => 'export-site', 'code' => 'export-site',
'name' => 'Export', 'name' => 'Export',
'wallet_api_url' => 'https://wallet.export.test', 'wallet_api_url' => 'https://wallet.export.test',
'admin_account' => [
'username' => 'export_site_admin',
'nickname' => 'Export Site Admin',
'password' => 'secret-strong',
],
]); ]);
$id = (int) $create->json('data.id'); $id = (int) $create->json('data.id');
@@ -225,6 +257,11 @@ test('site scoped admin only sees bound integration sites', function (): void {
->postJson('/api/v1/admin/integration-sites', [ ->postJson('/api/v1/admin/integration-sites', [
'code' => 'site-a', 'code' => 'site-a',
'name' => 'Site A', 'name' => 'Site A',
'admin_account' => [
'username' => 'site_a_admin',
'nickname' => 'Site A Admin',
'password' => 'secret-strong',
],
]) ])
->assertCreated(); ->assertCreated();
@@ -232,6 +269,11 @@ test('site scoped admin only sees bound integration sites', function (): void {
->postJson('/api/v1/admin/integration-sites', [ ->postJson('/api/v1/admin/integration-sites', [
'code' => 'site-b', 'code' => 'site-b',
'name' => 'Site B', 'name' => 'Site B',
'admin_account' => [
'username' => 'site_b_admin',
'nickname' => 'Site B Admin',
'password' => 'secret-strong',
],
]) ])
->assertCreated(); ->assertCreated();
@@ -360,6 +402,11 @@ test('wallet_api_url rejects non-https', function (): void {
'code' => 'bad-https-1', 'code' => 'bad-https-1',
'name' => 'Bad HTTPS 1', 'name' => 'Bad HTTPS 1',
'wallet_api_url' => 'http://wallet.bad.test', 'wallet_api_url' => 'http://wallet.bad.test',
'admin_account' => [
'username' => 'bad_https_admin_1',
'nickname' => 'Bad HTTPS Admin 1',
'password' => 'secret-strong',
],
]) ])
->assertStatus(422) ->assertStatus(422)
->assertJsonPath('data.errors.wallet_api_url.0', 'wallet_api_url 必须是 https 的公开域名根地址,并拒绝 localhost/内网 IP 与带路径/查询的地址。'); ->assertJsonPath('data.errors.wallet_api_url.0', 'wallet_api_url 必须是 https 的公开域名根地址,并拒绝 localhost/内网 IP 与带路径/查询的地址。');
@@ -373,6 +420,11 @@ test('wallet_api_url rejects localhost', function (): void {
'code' => 'bad-https-2', 'code' => 'bad-https-2',
'name' => 'Bad HTTPS 2', 'name' => 'Bad HTTPS 2',
'wallet_api_url' => 'https://localhost:8080', 'wallet_api_url' => 'https://localhost:8080',
'admin_account' => [
'username' => 'bad_https_admin_2',
'nickname' => 'Bad HTTPS Admin 2',
'password' => 'secret-strong',
],
]) ])
->assertStatus(422) ->assertStatus(422)
->assertJsonPath('data.errors.wallet_api_url.0', 'wallet_api_url 必须是 https 的公开域名根地址,并拒绝 localhost/内网 IP 与带路径/查询的地址。'); ->assertJsonPath('data.errors.wallet_api_url.0', 'wallet_api_url 必须是 https 的公开域名根地址,并拒绝 localhost/内网 IP 与带路径/查询的地址。');
@@ -386,6 +438,11 @@ test('wallet_api_url rejects private ip with path', function (): void {
'code' => 'bad-https-3', 'code' => 'bad-https-3',
'name' => 'Bad HTTPS 3', 'name' => 'Bad HTTPS 3',
'wallet_api_url' => 'https://127.0.0.1/wallet', 'wallet_api_url' => 'https://127.0.0.1/wallet',
'admin_account' => [
'username' => 'bad_https_admin_3',
'nickname' => 'Bad HTTPS Admin 3',
'password' => 'secret-strong',
],
]) ])
->assertStatus(422) ->assertStatus(422)
->assertJsonPath('data.errors.wallet_api_url.0', 'wallet_api_url 必须是 https 的公开域名根地址,并拒绝 localhost/内网 IP 与带路径/查询的地址。'); ->assertJsonPath('data.errors.wallet_api_url.0', 'wallet_api_url 必须是 https 的公开域名根地址,并拒绝 localhost/内网 IP 与带路径/查询的地址。');

View File

@@ -281,7 +281,6 @@ test('admin can set player credit limit without clobbering used credit', functio
'used_credit' => 0, 'used_credit' => 0,
'rebate_limit' => 0.01, 'rebate_limit' => 0.01,
'default_player_rebate' => 0.005, 'default_player_rebate' => 0.005,
'settlement_cycle' => 'weekly',
], ],
); );

View File

@@ -38,7 +38,6 @@ function createAgentLineForAllocation(string $code, int $creditLimit): AgentNode
'used_credit' => 0, 'used_credit' => 0,
'rebate_limit' => 0.01, 'rebate_limit' => 0.01,
'default_player_rebate' => 0.005, 'default_player_rebate' => 0.005,
'settlement_cycle' => 'weekly',
]); ]);
return AgentNode::query()->findOrFail($rootId); return AgentNode::query()->findOrFail($rootId);

View File

@@ -49,7 +49,6 @@ function createSiteWithRoot(string $code): array
'used_credit' => 0, 'used_credit' => 0,
'rebate_limit' => 0.01, 'rebate_limit' => 0.01,
'default_player_rebate' => 0.005, 'default_player_rebate' => 0.005,
'settlement_cycle' => 'weekly',
]); ]);
return ['site_id' => $siteId, 'site_code' => $code, 'root_id' => $rootId]; return ['site_id' => $siteId, 'site_code' => $code, 'root_id' => $rootId];

View File

@@ -37,7 +37,6 @@ test('share snapshot uses profile at build time not after change', function ():
'used_credit' => 0, 'used_credit' => 0,
'rebate_limit' => 0.01, 'rebate_limit' => 0.01,
'default_player_rebate' => 0.005, 'default_player_rebate' => 0.005,
'settlement_cycle' => 'weekly',
]); ]);
$playerId = (int) DB::table('players')->insertGetId([ $playerId = (int) DB::table('players')->insertGetId([