feat: 增强代理和玩家管理功能
- 在 SyncAdminAuthorizationCommand 中新增对代理线路和结算菜单操作的同步功能,确保缺失的菜单操作行能够被创建。 - 更新多个控制器中的权限检查逻辑,使用 hasPermissionCode 替代原有的权限验证方式,提升权限管理的灵活性。 - 在 AdminPlayerStoreController 中引入对玩家创建能力的验证,确保只有具备相应权限的管理员能够创建玩家。 - 更新请求验证逻辑,新增 credit_limit、rebate_rate 和 extra_rebate_rate 字段,以支持更细粒度的玩家管理。 - 在 AdminUser 和 AgentNode 模型中增强角色与用户的权限管理功能,支持更细粒度的权限控制。
This commit is contained in:
112
app/Console/Commands/AuditAgentLineDataCommand.php
Normal file
112
app/Console/Commands/AuditAgentLineDataCommand.php
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
final class AuditAgentLineDataCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'lottery:audit-agent-lines {--json : Output JSON only}';
|
||||||
|
|
||||||
|
protected $description = '盘点代理线路历史数据异常(只读)';
|
||||||
|
|
||||||
|
public function handle(): int
|
||||||
|
{
|
||||||
|
$sites = DB::table('admin_sites')->orderBy('id')->get(['id', 'code', 'name']);
|
||||||
|
$issues = [];
|
||||||
|
|
||||||
|
foreach ($sites as $site) {
|
||||||
|
$siteId = (int) $site->id;
|
||||||
|
$roots = DB::table('agent_nodes')
|
||||||
|
->where('admin_site_id', $siteId)
|
||||||
|
->where('depth', 0)
|
||||||
|
->get(['id', 'code', 'name']);
|
||||||
|
|
||||||
|
if ($roots->isEmpty()) {
|
||||||
|
$issues[] = [
|
||||||
|
'type' => 'site_without_root',
|
||||||
|
'admin_site_id' => $siteId,
|
||||||
|
'site_code' => (string) $site->code,
|
||||||
|
'message' => '站点无 depth=0 根代理节点',
|
||||||
|
];
|
||||||
|
} elseif ($roots->count() > 1) {
|
||||||
|
$issues[] = [
|
||||||
|
'type' => 'site_multiple_roots',
|
||||||
|
'admin_site_id' => $siteId,
|
||||||
|
'site_code' => (string) $site->code,
|
||||||
|
'root_ids' => $roots->pluck('id')->all(),
|
||||||
|
'message' => '站点存在多个根节点',
|
||||||
|
];
|
||||||
|
} else {
|
||||||
|
$root = $roots->first();
|
||||||
|
$expectedCode = (string) $site->code;
|
||||||
|
$rootCode = (string) $root->code;
|
||||||
|
if ($rootCode !== $expectedCode && $rootCode !== 'root-'.$expectedCode) {
|
||||||
|
$issues[] = [
|
||||||
|
'type' => 'root_code_mismatch',
|
||||||
|
'admin_site_id' => $siteId,
|
||||||
|
'site_code' => $expectedCode,
|
||||||
|
'root_code' => $rootCode,
|
||||||
|
'message' => '根节点 code 与 site.code 不一致',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
$loginCount = (int) DB::table('admin_user_agents')
|
||||||
|
->where('agent_node_id', (int) $root->id)
|
||||||
|
->count();
|
||||||
|
if ($loginCount === 0) {
|
||||||
|
$issues[] = [
|
||||||
|
'type' => 'root_without_login',
|
||||||
|
'admin_site_id' => $siteId,
|
||||||
|
'agent_node_id' => (int) $root->id,
|
||||||
|
'message' => '根代理无绑定后台账号',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$businessAgents = DB::table('agent_nodes')
|
||||||
|
->where('admin_site_id', $siteId)
|
||||||
|
->where('depth', '>', 0)
|
||||||
|
->count();
|
||||||
|
if ($businessAgents > 0 && $roots->count() === 1) {
|
||||||
|
// informational only for multi-agent-under-one-site report
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$sitesWithManyBusinessAgents = DB::table('agent_nodes')
|
||||||
|
->select('admin_site_id', DB::raw('count(*) as cnt'))
|
||||||
|
->where('depth', '>', 0)
|
||||||
|
->groupBy('admin_site_id')
|
||||||
|
->having('cnt', '>', 0)
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($sitesWithManyBusinessAgents as $row) {
|
||||||
|
$siteId = (int) $row->admin_site_id;
|
||||||
|
$siteCode = (string) DB::table('admin_sites')->where('id', $siteId)->value('code');
|
||||||
|
$rootCount = DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->count();
|
||||||
|
if ($rootCount === 1 && (int) $row->cnt >= 1) {
|
||||||
|
$issues[] = [
|
||||||
|
'type' => 'site_has_business_agents',
|
||||||
|
'admin_site_id' => $siteId,
|
||||||
|
'site_code' => $siteCode,
|
||||||
|
'business_agent_count' => (int) $row->cnt,
|
||||||
|
'message' => '站点下存在下级业务代理(需确认是否应拆站)',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->option('json')) {
|
||||||
|
$this->line(json_encode(['issues' => $issues], JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->info('Agent line audit: '.count($issues).' issue(s)');
|
||||||
|
foreach ($issues as $issue) {
|
||||||
|
$this->line('- ['.$issue['type'].'] '.$issue['message'].' (site: '.($issue['site_code'] ?? $issue['admin_site_id'] ?? '?').')');
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Console/Commands/SettlementAgentPeriodCloseCommand.php
Normal file
22
app/Console/Commands/SettlementAgentPeriodCloseCommand.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Console\Commands;
|
||||||
|
|
||||||
|
use App\Services\AgentSettlement\AgentSettlementPeriodCloseService;
|
||||||
|
use Illuminate\Console\Command;
|
||||||
|
|
||||||
|
final class SettlementAgentPeriodCloseCommand extends Command
|
||||||
|
{
|
||||||
|
protected $signature = 'settlement:agent-period-close {period : Settlement period ID}';
|
||||||
|
|
||||||
|
protected $description = '关闭代理账期并生成账单(试运行聚合)';
|
||||||
|
|
||||||
|
public function handle(AgentSettlementPeriodCloseService $service): int
|
||||||
|
{
|
||||||
|
$periodId = (int) $this->argument('period');
|
||||||
|
$result = $service->closePeriod($periodId);
|
||||||
|
$this->info(json_encode($result, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT));
|
||||||
|
|
||||||
|
return self::SUCCESS;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,6 +5,7 @@ namespace App\Console\Commands;
|
|||||||
use Illuminate\Support\Carbon;
|
use Illuminate\Support\Carbon;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Support\AdminAgentLineSettlementPermissionMenuActionSync;
|
||||||
use App\Support\AdminAgentPermissionMenuActionSync;
|
use App\Support\AdminAgentPermissionMenuActionSync;
|
||||||
use App\Support\AdminAuthorizationRegistry;
|
use App\Support\AdminAuthorizationRegistry;
|
||||||
use App\Support\AdminDrawPermissionMenuActionSync;
|
use App\Support\AdminDrawPermissionMenuActionSync;
|
||||||
@@ -24,6 +25,11 @@ final class SyncAdminAuthorizationCommand extends Command
|
|||||||
$this->info(sprintf('Created %d missing agent menu_action row(s).', $agentCreated));
|
$this->info(sprintf('Created %d missing agent menu_action row(s).', $agentCreated));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$lineSettlementCreated = AdminAgentLineSettlementPermissionMenuActionSync::syncMissing();
|
||||||
|
if ($lineSettlementCreated > 0) {
|
||||||
|
$this->info(sprintf('Created %d missing agent line/settlement menu_action row(s).', $lineSettlementCreated));
|
||||||
|
}
|
||||||
|
|
||||||
$drawCreated = AdminDrawPermissionMenuActionSync::syncMissing();
|
$drawCreated = AdminDrawPermissionMenuActionSync::syncMissing();
|
||||||
if ($drawCreated > 0) {
|
if ($drawCreated > 0) {
|
||||||
$this->info(sprintf('Created %d missing draw menu_action row(s).', $drawCreated));
|
$this->info(sprintf('Created %d missing draw menu_action row(s).', $drawCreated));
|
||||||
|
|||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\AdminSite;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Support\AdminIntegrationSitePresenter;
|
||||||
|
use App\Support\AdminSiteScope;
|
||||||
|
use App\Support\AgentLinePresenter;
|
||||||
|
use App\Support\AgentNodePresenter;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
/** GET /api/v1/admin/agent-lines/{admin_site} */
|
||||||
|
final class AgentLineShowController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request, AdminSite $admin_site): JsonResponse
|
||||||
|
{
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
if (! AdminSiteScope::siteIdAllowed($admin, (int) $admin_site->id)) {
|
||||||
|
abort(403);
|
||||||
|
}
|
||||||
|
|
||||||
|
$root = AgentNode::query()
|
||||||
|
->where('admin_site_id', $admin_site->id)
|
||||||
|
->where('depth', 0)
|
||||||
|
->firstOrFail();
|
||||||
|
|
||||||
|
return ApiResponse::success([
|
||||||
|
'site' => AdminIntegrationSitePresenter::detail($admin_site),
|
||||||
|
'agent_node' => AgentNodePresenter::item($root),
|
||||||
|
'line_root' => [
|
||||||
|
'agent_node_id' => (int) $root->id,
|
||||||
|
'site_code' => (string) $admin_site->code,
|
||||||
|
'is_line_root' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Middleware\RecordAdminApiAudit;
|
||||||
|
use App\Http\Requests\Admin\AdminAgentLineStoreRequest;
|
||||||
|
use App\Services\Agent\AgentSiteProvisioningService;
|
||||||
|
use App\Services\AuditLogger;
|
||||||
|
use App\Support\AdminIntegrationSitePresenter;
|
||||||
|
use App\Support\AgentLinePresenter;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
/** POST /api/v1/admin/agent-lines */
|
||||||
|
final class AgentLineStoreController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(
|
||||||
|
AdminAgentLineStoreRequest $request,
|
||||||
|
AgentSiteProvisioningService $service,
|
||||||
|
): JsonResponse {
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
$result = $service->createRootAgent($admin, $request->validated());
|
||||||
|
$site = $result['site'];
|
||||||
|
$node = $result['agent_node'];
|
||||||
|
|
||||||
|
$payload = AgentLinePresenter::provisioned($site, $node, $result['secrets']);
|
||||||
|
|
||||||
|
AuditLogger::recordForAdmin(
|
||||||
|
$admin,
|
||||||
|
$request,
|
||||||
|
moduleCode: 'agent',
|
||||||
|
actionCode: 'agent_line.provision',
|
||||||
|
targetType: 'admin_site',
|
||||||
|
targetId: (string) $site->id,
|
||||||
|
afterJson: [
|
||||||
|
'site' => AdminIntegrationSitePresenter::detail($site),
|
||||||
|
'agent_node_id' => (int) $node->id,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
$request->attributes->set(RecordAdminApiAudit::ATTRIBUTE_AUDIT_RECORDED, true);
|
||||||
|
|
||||||
|
return ApiResponse::success($payload)->setStatusCode(201);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,12 +43,8 @@ final class AgentNodeDestroyController extends Controller
|
|||||||
return ApiMessage::errorResponse($request, 'admin.agent_node_has_children_cannot_delete', ErrorCode::ValidationFailed->value, null, 422);
|
return ApiMessage::errorResponse($request, 'admin.agent_node_has_children_cannot_delete', ErrorCode::ValidationFailed->value, null, 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (DB::table('admin_user_agents')->where('agent_node_id', (int) $agent_node->id)->exists()) {
|
if (DB::table('players')->where('agent_node_id', $agent_node->id)->exists()) {
|
||||||
return ApiMessage::errorResponse($request, 'admin.agent_node_has_users_cannot_delete', ErrorCode::ValidationFailed->value, null, 422);
|
return ApiMessage::errorResponse($request, 'admin.agent_node_has_players_cannot_delete', ErrorCode::ValidationFailed->value, null, 422);
|
||||||
}
|
|
||||||
|
|
||||||
if ($service->hasBlockingCustomRoles($agent_node)) {
|
|
||||||
return ApiMessage::errorResponse($request, 'admin.agent_node_has_roles_cannot_delete', ErrorCode::ValidationFailed->value, null, 422);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$before = AgentNodePresenter::item($agent_node);
|
$before = AgentNodePresenter::item($agent_node);
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Admin\AdminAgentProfileUpdateRequest;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Models\AgentProfile;
|
||||||
|
use App\Services\Agent\AgentNodeService;
|
||||||
|
use App\Services\Agent\AgentProfileService;
|
||||||
|
use App\Support\AdminAgentScope;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
/** GET/PUT /api/v1/admin/agent-nodes/{agent_node}/profile */
|
||||||
|
final class AgentNodeProfileController extends Controller
|
||||||
|
{
|
||||||
|
public function show(Request $request, AgentNode $agent_node): JsonResponse
|
||||||
|
{
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
abort_if(! AdminAgentScope::nodeVisibleTo($admin, $agent_node), 403);
|
||||||
|
|
||||||
|
$profile = AgentProfile::query()->firstOrNew(['agent_node_id' => $agent_node->id]);
|
||||||
|
|
||||||
|
return ApiResponse::success(app(AgentProfileService::class)->present($profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function update(
|
||||||
|
AdminAgentProfileUpdateRequest $request,
|
||||||
|
AgentNode $agent_node,
|
||||||
|
AgentProfileService $service,
|
||||||
|
AgentNodeService $agentNodeService,
|
||||||
|
): JsonResponse {
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
abort_if(! AdminAgentScope::nodeVisibleTo($admin, $agent_node), 403);
|
||||||
|
|
||||||
|
$parent = $agent_node->parent_id !== null
|
||||||
|
? AgentNode::query()->find($agent_node->parent_id)
|
||||||
|
: null;
|
||||||
|
|
||||||
|
$profile = $service->upsertForNode($agent_node, $request->validated(), $parent);
|
||||||
|
$agentNodeService->syncPrimaryOwnerRoleFromProfile($agent_node, $profile);
|
||||||
|
|
||||||
|
return ApiResponse::success($service->present($profile));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\AgentSettlement;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Middleware\RecordAdminApiAudit;
|
||||||
|
use App\Models\Player;
|
||||||
|
use App\Services\AuditLogger;
|
||||||
|
use App\Services\Player\PlayerCreditService;
|
||||||
|
use App\Support\AdminAgentSettlementScope;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
final class AgentSettlementBillConfirmController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(
|
||||||
|
Request $request,
|
||||||
|
int $settlement_bill,
|
||||||
|
PlayerCreditService $creditService,
|
||||||
|
): JsonResponse {
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
abort_if(! AdminAgentSettlementScope::billAccessible($admin, $settlement_bill), 404);
|
||||||
|
|
||||||
|
$bill = DB::table('settlement_bills')->where('id', $settlement_bill)->first();
|
||||||
|
abort_if($bill === null, 404);
|
||||||
|
|
||||||
|
$unpaid = (int) $bill->unpaid_amount;
|
||||||
|
DB::table('settlement_bills')->where('id', $settlement_bill)->update([
|
||||||
|
'paid_amount' => (int) $bill->paid_amount + $unpaid,
|
||||||
|
'unpaid_amount' => 0,
|
||||||
|
'status' => 'confirmed',
|
||||||
|
'confirmed_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($bill->owner_type === 'player' && (int) $bill->owner_id > 0) {
|
||||||
|
$player = Player::query()->find((int) $bill->owner_id);
|
||||||
|
if ($player !== null) {
|
||||||
|
$creditService->releaseFromSettlement($player, $unpaid, $settlement_bill);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AuditLogger::recordForAdmin(
|
||||||
|
$admin,
|
||||||
|
$request,
|
||||||
|
moduleCode: 'settlement',
|
||||||
|
actionCode: 'settlement_bill.confirm',
|
||||||
|
targetType: 'settlement_bill',
|
||||||
|
targetId: (string) $settlement_bill,
|
||||||
|
beforeJson: ['status' => (string) $bill->status, 'unpaid_amount' => $unpaid],
|
||||||
|
afterJson: ['status' => 'confirmed', 'paid_amount' => (int) $bill->paid_amount + $unpaid],
|
||||||
|
);
|
||||||
|
$request->attributes->set(RecordAdminApiAudit::ATTRIBUTE_AUDIT_RECORDED, true);
|
||||||
|
|
||||||
|
return ApiResponse::success(['bill_id' => $settlement_bill, 'status' => 'confirmed']);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\AgentSettlement;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Support\AdminAgentSettlementScope;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
final class AgentSettlementBillIndexController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
$periodId = (int) $request->query('settlement_period_id', 0);
|
||||||
|
$query = DB::table('settlement_bills')->orderByDesc('id');
|
||||||
|
if ($periodId > 0) {
|
||||||
|
$query->where('settlement_period_id', $periodId);
|
||||||
|
}
|
||||||
|
|
||||||
|
AdminAgentSettlementScope::applyToBillsQuery($query, $admin);
|
||||||
|
|
||||||
|
return ApiResponse::success([
|
||||||
|
'items' => $query->limit(100)->get(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\AgentSettlement;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Middleware\RecordAdminApiAudit;
|
||||||
|
use App\Services\AgentSettlement\AgentSettlementPeriodCloseService;
|
||||||
|
use App\Services\AuditLogger;
|
||||||
|
use App\Support\AdminAgentSettlementScope;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
final class AgentSettlementPeriodCloseController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(
|
||||||
|
Request $request,
|
||||||
|
int $settlement_period,
|
||||||
|
AgentSettlementPeriodCloseService $service,
|
||||||
|
): JsonResponse {
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
abort_if(! AdminAgentSettlementScope::periodAccessible($admin, $settlement_period), 404);
|
||||||
|
|
||||||
|
$before = DB::table('settlement_periods')->where('id', $settlement_period)->first();
|
||||||
|
$result = $service->closePeriod($settlement_period);
|
||||||
|
|
||||||
|
AuditLogger::recordForAdmin(
|
||||||
|
$admin,
|
||||||
|
$request,
|
||||||
|
moduleCode: 'settlement',
|
||||||
|
actionCode: 'settlement_period.close',
|
||||||
|
targetType: 'settlement_period',
|
||||||
|
targetId: (string) $settlement_period,
|
||||||
|
beforeJson: $before !== null ? (array) $before : null,
|
||||||
|
afterJson: $result,
|
||||||
|
);
|
||||||
|
$request->attributes->set(RecordAdminApiAudit::ATTRIBUTE_AUDIT_RECORDED, true);
|
||||||
|
|
||||||
|
return ApiResponse::success($result);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\AgentSettlement;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Middleware\RecordAdminApiAudit;
|
||||||
|
use App\Http\Requests\Admin\AdminSettlementPeriodStoreRequest;
|
||||||
|
use App\Services\AuditLogger;
|
||||||
|
use App\Support\AdminAgentSettlementScope;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
final class AgentSettlementPeriodStoreController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(AdminSettlementPeriodStoreRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
$data = $request->validated();
|
||||||
|
abort_if(
|
||||||
|
! AdminAgentSettlementScope::siteAccessible($admin, (int) $data['admin_site_id']),
|
||||||
|
404,
|
||||||
|
);
|
||||||
|
|
||||||
|
$id = DB::table('settlement_periods')->insertGetId([
|
||||||
|
'admin_site_id' => (int) $data['admin_site_id'],
|
||||||
|
'period_start' => $data['period_start'],
|
||||||
|
'period_end' => $data['period_end'],
|
||||||
|
'status' => 'open',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$row = DB::table('settlement_periods')->where('id', $id)->first();
|
||||||
|
|
||||||
|
AuditLogger::recordForAdmin(
|
||||||
|
$admin,
|
||||||
|
$request,
|
||||||
|
moduleCode: 'settlement',
|
||||||
|
actionCode: 'settlement_period.store',
|
||||||
|
targetType: 'settlement_period',
|
||||||
|
targetId: (string) $id,
|
||||||
|
beforeJson: null,
|
||||||
|
afterJson: (array) $row,
|
||||||
|
);
|
||||||
|
$request->attributes->set(RecordAdminApiAudit::ATTRIBUTE_AUDIT_RECORDED, true);
|
||||||
|
|
||||||
|
return ApiResponse::success((array) $row)->setStatusCode(201);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,6 +11,8 @@ use App\Services\Integration\IntegrationSiteService;
|
|||||||
use App\Support\AdminIntegrationSitePresenter;
|
use App\Support\AdminIntegrationSitePresenter;
|
||||||
use App\Http\Requests\Admin\AdminIntegrationSiteStoreRequest;
|
use App\Http\Requests\Admin\AdminIntegrationSiteStoreRequest;
|
||||||
use App\Http\Middleware\RecordAdminApiAudit;
|
use App\Http\Middleware\RecordAdminApiAudit;
|
||||||
|
use App\Lottery\ErrorCode;
|
||||||
|
use App\Support\ApiMessage;
|
||||||
|
|
||||||
final class AdminIntegrationSiteStoreController extends Controller
|
final class AdminIntegrationSiteStoreController extends Controller
|
||||||
{
|
{
|
||||||
@@ -21,6 +23,19 @@ final class AdminIntegrationSiteStoreController extends Controller
|
|||||||
$admin = $request->lotteryAdmin();
|
$admin = $request->lotteryAdmin();
|
||||||
abort_if($admin === null, 401);
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
if (! $admin->isSuperAdmin()) {
|
||||||
|
return ApiMessage::errorResponse(
|
||||||
|
$request,
|
||||||
|
'admin.integration_site_store_deprecated',
|
||||||
|
ErrorCode::AdminForbidden->value,
|
||||||
|
['hint' => 'Use POST /api/v1/admin/agent-lines to provision a new agent line.'],
|
||||||
|
403,
|
||||||
|
)->withHeaders([
|
||||||
|
'Deprecation' => 'true',
|
||||||
|
'Link' => '</api/v1/admin/agent-lines>; rel="successor-version"',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
$result = $service->create($request->validated());
|
$result = $service->create($request->validated());
|
||||||
$site = $result['site'];
|
$site = $result['site'];
|
||||||
|
|
||||||
|
|||||||
@@ -12,15 +12,35 @@ use App\Support\AdminSiteScope;
|
|||||||
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 App\Http\Requests\Admin\AdminPlayerStoreRequest;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Services\Agent\AgentProfileService;
|
||||||
|
use App\Services\Agent\RebateLimitValidator;
|
||||||
|
use App\Services\Player\PlayerCreditService;
|
||||||
|
|
||||||
/** POST /api/v1/admin/players */
|
/** POST /api/v1/admin/players */
|
||||||
final class AdminPlayerStoreController extends Controller
|
final class AdminPlayerStoreController extends Controller
|
||||||
{
|
{
|
||||||
public function __invoke(AdminPlayerStoreRequest $request): JsonResponse
|
public function __invoke(
|
||||||
{
|
AdminPlayerStoreRequest $request,
|
||||||
|
PlayerCreditService $playerCreditService,
|
||||||
|
RebateLimitValidator $rebateLimitValidator,
|
||||||
|
AgentProfileService $agentProfileService,
|
||||||
|
): JsonResponse {
|
||||||
$admin = $request->lotteryAdmin();
|
$admin = $request->lotteryAdmin();
|
||||||
abort_if($admin === null, 401);
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
try {
|
||||||
|
$agentProfileService->assertActorMayCreatePlayer($admin);
|
||||||
|
} catch (\Illuminate\Validation\ValidationException $e) {
|
||||||
|
return ApiMessage::errorResponse(
|
||||||
|
$request,
|
||||||
|
'admin.player_create_capability_forbidden',
|
||||||
|
ErrorCode::AdminForbidden->value,
|
||||||
|
$e->errors(),
|
||||||
|
403,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$siteCode = (string) $request->validated('site_code');
|
$siteCode = (string) $request->validated('site_code');
|
||||||
if (! AdminSiteScope::siteCodeAllowed($admin, $siteCode)) {
|
if (! AdminSiteScope::siteCodeAllowed($admin, $siteCode)) {
|
||||||
return ApiMessage::errorResponse($request, 'admin.player_create_site_forbidden', ErrorCode::AdminForbidden->value, null, 403);
|
return ApiMessage::errorResponse($request, 'admin.player_create_site_forbidden', ErrorCode::AdminForbidden->value, null, 403);
|
||||||
@@ -56,6 +76,15 @@ final class AdminPlayerStoreController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$agent = AgentNode::query()->findOrFail($agentNodeId);
|
||||||
|
if ($request->has('rebate_rate')) {
|
||||||
|
$rebateLimitValidator->assertPlayerRebateWithinAgent(
|
||||||
|
$agent,
|
||||||
|
(float) $request->input('rebate_rate', 0),
|
||||||
|
(float) $request->input('extra_rebate_rate', 0),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
$player = Player::query()->create([
|
$player = Player::query()->create([
|
||||||
'site_code' => $request->validated('site_code'),
|
'site_code' => $request->validated('site_code'),
|
||||||
'agent_node_id' => $agentNodeId,
|
'agent_node_id' => $agentNodeId,
|
||||||
@@ -66,6 +95,24 @@ final class AdminPlayerStoreController extends Controller
|
|||||||
'status' => $request->validated('status', 0),
|
'status' => $request->validated('status', 0),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
if ($request->has('credit_limit')) {
|
||||||
|
$playerCreditService->upsertAccount($player, [
|
||||||
|
'credit_limit' => (int) $request->input('credit_limit', 0),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($request->has('rebate_rate')) {
|
||||||
|
\Illuminate\Support\Facades\DB::table('player_rebate_profiles')->insert([
|
||||||
|
'player_id' => $player->id,
|
||||||
|
'game_type' => '*',
|
||||||
|
'inherit_from_agent' => false,
|
||||||
|
'rebate_rate' => (float) $request->input('rebate_rate', 0),
|
||||||
|
'extra_rebate_rate' => (float) $request->input('extra_rebate_rate', 0),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
return ApiResponse::success(PlayerApiPresenter::listItem($player))->setStatusCode(201);
|
return ApiResponse::success(PlayerApiPresenter::listItem($player))->setStatusCode(201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,7 @@ final class AdminPermissionCatalogController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$roles = AdminRole::query()
|
$roles = AdminRole::query()
|
||||||
|
->where('scope_type', AdminRole::SCOPE_SYSTEM)
|
||||||
->orderBy('slug')
|
->orderBy('slug')
|
||||||
->get(['id', 'slug', 'name']);
|
->get(['id', 'slug', 'name']);
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,15 @@ use Illuminate\Http\Request;
|
|||||||
use App\Services\AuditLogger;
|
use App\Services\AuditLogger;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Support\AdminAccountScopeGuard;
|
||||||
use App\Support\AdminRoleApiPresenter;
|
use App\Support\AdminRoleApiPresenter;
|
||||||
|
|
||||||
final class AdminRoleDestroyController extends Controller
|
final class AdminRoleDestroyController extends Controller
|
||||||
{
|
{
|
||||||
public function __invoke(Request $request, AdminRole $admin_role): JsonResponse
|
public function __invoke(Request $request, AdminRole $admin_role): JsonResponse
|
||||||
{
|
{
|
||||||
|
AdminAccountScopeGuard::assertSystemRole($admin_role);
|
||||||
|
|
||||||
if ($admin_role->slug === AdminRole::ROLE_SUPER_ADMIN) {
|
if ($admin_role->slug === AdminRole::ROLE_SUPER_ADMIN) {
|
||||||
return ApiMessage::errorResponse($request, 'admin.role_cannot_delete_super_admin', ErrorCode::ValidationFailed->value, null, 422);
|
return ApiMessage::errorResponse($request, 'admin.role_cannot_delete_super_admin', ErrorCode::ValidationFailed->value, null, 422);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use Illuminate\Http\JsonResponse;
|
|||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Support\AdminPermissionInheritance;
|
use App\Support\AdminPermissionInheritance;
|
||||||
|
use App\Support\AdminAccountScopeGuard;
|
||||||
use App\Support\AdminRoleApiPresenter;
|
use App\Support\AdminRoleApiPresenter;
|
||||||
use App\Http\Requests\Admin\AdminRolePermissionSyncRequest;
|
use App\Http\Requests\Admin\AdminRolePermissionSyncRequest;
|
||||||
|
|
||||||
@@ -16,6 +17,8 @@ final class AdminRolePermissionSyncController extends Controller
|
|||||||
{
|
{
|
||||||
public function __invoke(AdminRolePermissionSyncRequest $request, AdminRole $admin_role): JsonResponse
|
public function __invoke(AdminRolePermissionSyncRequest $request, AdminRole $admin_role): JsonResponse
|
||||||
{
|
{
|
||||||
|
AdminAccountScopeGuard::assertSystemRole($admin_role);
|
||||||
|
|
||||||
$slugs = AdminPermissionInheritance::expand(
|
$slugs = AdminPermissionInheritance::expand(
|
||||||
array_values(array_unique($request->validated('permission_slugs', []))),
|
array_values(array_unique($request->validated('permission_slugs', []))),
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Support\ApiResponse;
|
|||||||
use App\Services\AuditLogger;
|
use App\Services\AuditLogger;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Support\AdminAccountScopeGuard;
|
||||||
use App\Support\AdminRoleApiPresenter;
|
use App\Support\AdminRoleApiPresenter;
|
||||||
use App\Http\Requests\Admin\AdminRoleUpdateRequest;
|
use App\Http\Requests\Admin\AdminRoleUpdateRequest;
|
||||||
|
|
||||||
@@ -14,6 +15,8 @@ final class AdminRoleUpdateController extends Controller
|
|||||||
{
|
{
|
||||||
public function __invoke(AdminRoleUpdateRequest $request, AdminRole $admin_role): JsonResponse
|
public function __invoke(AdminRoleUpdateRequest $request, AdminRole $admin_role): JsonResponse
|
||||||
{
|
{
|
||||||
|
AdminAccountScopeGuard::assertSystemRole($admin_role);
|
||||||
|
|
||||||
$before = AdminRoleApiPresenter::item($admin_role);
|
$before = AdminRoleApiPresenter::item($admin_role);
|
||||||
|
|
||||||
$payload = [];
|
$payload = [];
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ use Illuminate\Http\Request;
|
|||||||
use App\Services\AuditLogger;
|
use App\Services\AuditLogger;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Support\AdminAccountScopeGuard;
|
||||||
use App\Support\AdminUserApiPresenter;
|
use App\Support\AdminUserApiPresenter;
|
||||||
|
|
||||||
/** DELETE /api/v1/admin/admin-users/{admin_user} */
|
/** DELETE /api/v1/admin/admin-users/{admin_user} */
|
||||||
@@ -19,6 +20,7 @@ final class AdminUserDestroyController extends Controller
|
|||||||
{
|
{
|
||||||
/** @var AdminUser $actor */
|
/** @var AdminUser $actor */
|
||||||
$actor = $request->lotteryAdmin();
|
$actor = $request->lotteryAdmin();
|
||||||
|
AdminAccountScopeGuard::assertPlatformAccount($admin_user);
|
||||||
|
|
||||||
if ((int) $actor->getKey() === (int) $admin_user->getKey()) {
|
if ((int) $actor->getKey() === (int) $admin_user->getKey()) {
|
||||||
return ApiMessage::errorResponse($request, 'admin.user_cannot_delete_self', ErrorCode::ValidationFailed->value, null, 422);
|
return ApiMessage::errorResponse($request, 'admin.user_cannot_delete_self', ErrorCode::ValidationFailed->value, null, 422);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Http\Controllers\Api\V1\Admin\User;
|
namespace App\Http\Controllers\Api\V1\Admin\User;
|
||||||
|
|
||||||
use App\Models\AdminUser;
|
use App\Models\AdminUser;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Support\AdminApiList;
|
use App\Support\AdminApiList;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
@@ -19,6 +20,11 @@ final class AdminUserIndexController extends Controller
|
|||||||
|
|
||||||
$q = AdminUser::query()
|
$q = AdminUser::query()
|
||||||
->with(['roles'])
|
->with(['roles'])
|
||||||
|
->whereNotExists(static function ($sub): void {
|
||||||
|
$sub->select(DB::raw(1))
|
||||||
|
->from('admin_user_agents as uag')
|
||||||
|
->whereColumn('uag.admin_user_id', 'admin_users.id');
|
||||||
|
})
|
||||||
->orderByDesc('id');
|
->orderByDesc('id');
|
||||||
|
|
||||||
if ($keyword !== '') {
|
if ($keyword !== '') {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ use Illuminate\Http\JsonResponse;
|
|||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Support\AdminPermissionBridge;
|
use App\Support\AdminPermissionBridge;
|
||||||
|
use App\Support\AdminAccountScopeGuard;
|
||||||
|
use App\Support\AdminUserApiPresenter;
|
||||||
use App\Http\Requests\Admin\AdminUserPermissionSyncRequest;
|
use App\Http\Requests\Admin\AdminUserPermissionSyncRequest;
|
||||||
|
|
||||||
/** PUT /api/v1/admin/admin-users/{admin_user}/permissions */
|
/** PUT /api/v1/admin/admin-users/{admin_user}/permissions */
|
||||||
@@ -16,6 +18,8 @@ final class AdminUserPermissionSyncController extends Controller
|
|||||||
{
|
{
|
||||||
public function __invoke(AdminUserPermissionSyncRequest $request, AdminUser $admin_user): JsonResponse
|
public function __invoke(AdminUserPermissionSyncRequest $request, AdminUser $admin_user): JsonResponse
|
||||||
{
|
{
|
||||||
|
AdminAccountScopeGuard::assertPlatformAccount($admin_user);
|
||||||
|
|
||||||
$input = $request->validated();
|
$input = $request->validated();
|
||||||
$slugs = AdminPermissionBridge::normalizeCanonicalLegacySlugs(array_values(array_filter(
|
$slugs = AdminPermissionBridge::normalizeCanonicalLegacySlugs(array_values(array_filter(
|
||||||
(array) ($input['permissions'] ?? $input['permission_slugs'] ?? []),
|
(array) ($input['permissions'] ?? $input['permission_slugs'] ?? []),
|
||||||
@@ -69,13 +73,6 @@ final class AdminUserPermissionSyncController extends Controller
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
return ApiResponse::success([
|
return ApiResponse::success(AdminUserApiPresenter::listItem($admin_user));
|
||||||
'id' => (int) $admin_user->id,
|
|
||||||
'username' => $admin_user->username,
|
|
||||||
'nickname' => $admin_user->name,
|
|
||||||
'roles' => $admin_user->adminRoleSlugs(),
|
|
||||||
'direct_permissions' => $admin_user->directLegacyPermissionSlugs(),
|
|
||||||
'effective_permissions' => $admin_user->adminPermissionSlugs(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ use App\Models\AdminUser;
|
|||||||
use App\Support\ApiResponse;
|
use App\Support\ApiResponse;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Support\AdminAccountScopeGuard;
|
||||||
|
use App\Support\AdminUserApiPresenter;
|
||||||
use App\Http\Requests\Admin\AdminUserRoleSyncRequest;
|
use App\Http\Requests\Admin\AdminUserRoleSyncRequest;
|
||||||
|
|
||||||
/** PUT /api/v1/admin/admin-users/{admin_user}/roles */
|
/** PUT /api/v1/admin/admin-users/{admin_user}/roles */
|
||||||
@@ -13,18 +15,13 @@ final class AdminUserRoleSyncController extends Controller
|
|||||||
{
|
{
|
||||||
public function __invoke(AdminUserRoleSyncRequest $request, AdminUser $admin_user): JsonResponse
|
public function __invoke(AdminUserRoleSyncRequest $request, AdminUser $admin_user): JsonResponse
|
||||||
{
|
{
|
||||||
|
AdminAccountScopeGuard::assertPlatformAccount($admin_user);
|
||||||
|
|
||||||
$slugs = array_values(array_unique($request->validated('role_slugs')));
|
$slugs = array_values(array_unique($request->validated('role_slugs')));
|
||||||
$admin_user->syncRoleSlugsForDefaultSite($slugs);
|
$admin_user->syncSystemRoleSlugs($slugs);
|
||||||
|
|
||||||
$admin_user->load('roles');
|
$admin_user->load('roles');
|
||||||
|
|
||||||
return ApiResponse::success([
|
return ApiResponse::success(AdminUserApiPresenter::listItem($admin_user));
|
||||||
'id' => (int) $admin_user->id,
|
|
||||||
'username' => $admin_user->username,
|
|
||||||
'nickname' => $admin_user->name,
|
|
||||||
'roles' => $admin_user->adminRoleSlugs(),
|
|
||||||
'direct_permissions' => $admin_user->directLegacyPermissionSlugs(),
|
|
||||||
'effective_permissions' => $admin_user->adminPermissionSlugs(),
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ use App\Models\AdminUser;
|
|||||||
use App\Support\ApiResponse;
|
use App\Support\ApiResponse;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Support\AdminAccountScopeGuard;
|
||||||
use App\Support\AdminUserApiPresenter;
|
use App\Support\AdminUserApiPresenter;
|
||||||
|
|
||||||
/** GET /api/v1/admin/admin-users/{admin_user} */
|
/** GET /api/v1/admin/admin-users/{admin_user} */
|
||||||
@@ -13,6 +14,8 @@ final class AdminUserShowController extends Controller
|
|||||||
{
|
{
|
||||||
public function __invoke(AdminUser $admin_user): JsonResponse
|
public function __invoke(AdminUser $admin_user): JsonResponse
|
||||||
{
|
{
|
||||||
|
AdminAccountScopeGuard::assertPlatformAccount($admin_user);
|
||||||
|
|
||||||
$admin_user->load('roles');
|
$admin_user->load('roles');
|
||||||
|
|
||||||
return ApiResponse::success(AdminUserApiPresenter::listItem($admin_user));
|
return ApiResponse::success(AdminUserApiPresenter::listItem($admin_user));
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ final class AdminUserStoreController extends Controller
|
|||||||
'password' => $request->validated('password'),
|
'password' => $request->validated('password'),
|
||||||
'status' => $request->validated('status', 0),
|
'status' => $request->validated('status', 0),
|
||||||
]);
|
]);
|
||||||
$created->syncRoleSlugsForDefaultSite($roleSlugs);
|
$created->syncSystemRoleSlugs($roleSlugs);
|
||||||
|
|
||||||
return $created;
|
return $created;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Support\ApiResponse;
|
|||||||
use App\Services\AuditLogger;
|
use App\Services\AuditLogger;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Support\AdminAccountScopeGuard;
|
||||||
use App\Support\AdminUserApiPresenter;
|
use App\Support\AdminUserApiPresenter;
|
||||||
use App\Http\Requests\Admin\AdminUserUpdateRequest;
|
use App\Http\Requests\Admin\AdminUserUpdateRequest;
|
||||||
|
|
||||||
@@ -17,6 +18,7 @@ final class AdminUserUpdateController extends Controller
|
|||||||
{
|
{
|
||||||
/** @var AdminUser $actor */
|
/** @var AdminUser $actor */
|
||||||
$actor = $request->lotteryAdmin();
|
$actor = $request->lotteryAdmin();
|
||||||
|
AdminAccountScopeGuard::assertPlatformAccount($admin_user);
|
||||||
|
|
||||||
$admin_user->load('roles');
|
$admin_user->load('roles');
|
||||||
$before = AdminUserApiPresenter::listItem($admin_user);
|
$before = AdminUserApiPresenter::listItem($admin_user);
|
||||||
|
|||||||
@@ -62,6 +62,10 @@ final class EnsureAdminApiResourcePermission
|
|||||||
->all();
|
->all();
|
||||||
|
|
||||||
if ($permissionCodes === []) {
|
if ($permissionCodes === []) {
|
||||||
|
if ($admin->isSuperAdmin()) {
|
||||||
|
return $next($request);
|
||||||
|
}
|
||||||
|
|
||||||
return ApiMessage::errorResponse(
|
return ApiMessage::errorResponse(
|
||||||
$request,
|
$request,
|
||||||
'admin.api_resource_no_permission_binding',
|
'admin.api_resource_no_permission_binding',
|
||||||
|
|||||||
41
app/Http/Requests/Admin/AdminAgentLineStoreRequest.php
Normal file
41
app/Http/Requests/Admin/AdminAgentLineStoreRequest.php
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use App\Http\Requests\Admin\Concerns\AgentProfileFieldRules;
|
||||||
|
use App\Http\Requests\ApiFormRequest;
|
||||||
|
use App\Rules\WalletApiUrlRule;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
final class AdminAgentLineStoreRequest extends ApiFormRequest
|
||||||
|
{
|
||||||
|
use AgentProfileFieldRules;
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'code' => ['required', 'string', 'max:64', 'regex:/^[a-z0-9][a-z0-9_-]*$/', Rule::unique('admin_sites', 'code')],
|
||||||
|
'name' => ['required', 'string', 'max:128'],
|
||||||
|
'username' => ['required', 'string', 'max:64'],
|
||||||
|
'password' => ['required', 'string', 'min:8', 'max:128'],
|
||||||
|
'email' => ['nullable', 'string', 'email', 'max:255', Rule::unique('admin_users', 'email')],
|
||||||
|
'currency_code' => ['sometimes', 'string', 'max:16'],
|
||||||
|
'status' => ['sometimes', 'integer', 'in:0,1'],
|
||||||
|
'wallet_api_url' => ['nullable', 'string', 'max:512', new WalletApiUrlRule()],
|
||||||
|
'wallet_debit_path' => ['sometimes', 'string', 'max:128'],
|
||||||
|
'wallet_credit_path' => ['sometimes', 'string', 'max:128'],
|
||||||
|
'wallet_balance_path' => ['sometimes', 'string', 'max:128'],
|
||||||
|
'wallet_timeout_seconds' => ['sometimes', 'integer', 'min:1', 'max:120'],
|
||||||
|
'iframe_allowed_origins' => ['nullable', 'array'],
|
||||||
|
'iframe_allowed_origins.*' => ['string', 'max:512'],
|
||||||
|
'lottery_h5_base_url' => ['nullable', 'string', 'max:512'],
|
||||||
|
'notes' => ['nullable', 'string', 'max:5000'],
|
||||||
|
...$this->agentProfileFieldRules(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
26
app/Http/Requests/Admin/AdminAgentProfileUpdateRequest.php
Normal file
26
app/Http/Requests/Admin/AdminAgentProfileUpdateRequest.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use App\Http\Requests\ApiFormRequest;
|
||||||
|
|
||||||
|
final class AdminAgentProfileUpdateRequest extends ApiFormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'total_share_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'],
|
||||||
|
'credit_limit' => ['sometimes', 'integer', 'min:0'],
|
||||||
|
'rebate_limit' => ['sometimes', 'numeric', 'min:0', 'max:1'],
|
||||||
|
'default_player_rebate' => ['sometimes', 'numeric', 'min:0', 'max:1'],
|
||||||
|
'settlement_cycle' => ['sometimes', 'string', 'in:daily,weekly,monthly'],
|
||||||
|
'can_grant_extra_rebate' => ['sometimes', 'boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -27,6 +27,9 @@ final class AdminPlayerStoreRequest 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', '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'],
|
||||||
|
'rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:1'],
|
||||||
|
'extra_rebate_rate' => ['sometimes', 'numeric', 'min:0', 'max:1'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use App\Http\Requests\ApiFormRequest;
|
||||||
|
|
||||||
|
final class AdminSettlementPeriodStoreRequest extends ApiFormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'admin_site_id' => ['required', 'integer', 'exists:admin_sites,id'],
|
||||||
|
'period_start' => ['required', 'date'],
|
||||||
|
'period_end' => ['required', 'date', 'after:period_start'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,10 +2,13 @@
|
|||||||
|
|
||||||
namespace App\Http\Requests\Admin;
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use App\Http\Requests\Admin\Concerns\AgentProfileFieldRules;
|
||||||
use App\Http\Requests\ApiFormRequest;
|
use App\Http\Requests\ApiFormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
final class AgentNodeStoreRequest extends ApiFormRequest
|
final class AgentNodeStoreRequest extends ApiFormRequest
|
||||||
{
|
{
|
||||||
|
use AgentProfileFieldRules;
|
||||||
public function authorize(): bool
|
public function authorize(): bool
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
@@ -16,9 +19,13 @@ final class AgentNodeStoreRequest extends ApiFormRequest
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'parent_id' => ['required', 'integer', 'exists:agent_nodes,id'],
|
'parent_id' => ['required', 'integer', 'exists:agent_nodes,id'],
|
||||||
'code' => ['required', 'string', 'max:64', 'regex:/^[a-zA-Z0-9_-]+$/'],
|
'code' => ['sometimes', 'nullable', 'string', 'max:64', 'regex:/^[a-zA-Z0-9_-]+$/'],
|
||||||
'name' => ['required', 'string', 'max:128'],
|
'name' => ['required', 'string', 'max:128'],
|
||||||
|
'username' => ['sometimes', 'nullable', 'string', 'max:64', Rule::unique('admin_users', 'username')],
|
||||||
|
'email' => ['nullable', 'email', 'max:255', Rule::unique('admin_users', 'email')],
|
||||||
|
'password' => ['required', 'string', 'min:8', 'max:128'],
|
||||||
'status' => ['sometimes', 'integer', 'in:0,1'],
|
'status' => ['sometimes', 'integer', 'in:0,1'],
|
||||||
|
...$this->agentProfileFieldRules(),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,9 @@ final class AgentNodeUpdateRequest extends ApiFormRequest
|
|||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'name' => ['sometimes', 'string', 'max:128'],
|
'name' => ['sometimes', 'string', 'max:128'],
|
||||||
|
'username' => ['sometimes', 'string', 'max:64'],
|
||||||
|
'email' => ['sometimes', 'nullable', 'email', 'max:255'],
|
||||||
|
'password' => ['sometimes', 'nullable', 'string', 'min:8', 'max:128'],
|
||||||
'status' => ['sometimes', 'integer', 'in:0,1'],
|
'status' => ['sometimes', 'integer', 'in:0,1'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
21
app/Http/Requests/Admin/Concerns/AgentProfileFieldRules.php
Normal file
21
app/Http/Requests/Admin/Concerns/AgentProfileFieldRules.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin\Concerns;
|
||||||
|
|
||||||
|
trait AgentProfileFieldRules
|
||||||
|
{
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
protected function agentProfileFieldRules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'total_share_rate' => ['sometimes', 'numeric', 'min:0', 'max:100'],
|
||||||
|
'credit_limit' => ['sometimes', 'integer', 'min:0'],
|
||||||
|
'rebate_limit' => ['sometimes', 'numeric', 'min:0', 'max:1'],
|
||||||
|
'default_player_rebate' => ['sometimes', 'numeric', 'min:0', 'max:1'],
|
||||||
|
'settlement_cycle' => ['sometimes', 'string', 'in:daily,weekly,monthly'],
|
||||||
|
'can_grant_extra_rebate' => ['sometimes', 'boolean'],
|
||||||
|
'can_create_child_agent' => ['sometimes', 'boolean'],
|
||||||
|
'can_create_player' => ['sometimes', 'boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ use App\Models\AgentNode;
|
|||||||
use Illuminate\Notifications\Notifiable;
|
use Illuminate\Notifications\Notifiable;
|
||||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||||
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
final class AdminUser extends Authenticatable
|
final class AdminUser extends Authenticatable
|
||||||
{
|
{
|
||||||
@@ -208,6 +209,46 @@ final class AdminUser extends Authenticatable
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 平台账号角色同步:仅允许系统角色,不同步代理角色。
|
||||||
|
*
|
||||||
|
* @param list<string> $slugs
|
||||||
|
*/
|
||||||
|
public function syncSystemRoleSlugs(array $slugs): void
|
||||||
|
{
|
||||||
|
$siteId = self::defaultAdminSiteId();
|
||||||
|
$slugs = array_values(array_unique($slugs));
|
||||||
|
$roleIds = DB::table('admin_roles')
|
||||||
|
->where('scope_type', AdminRole::SCOPE_SYSTEM)
|
||||||
|
->whereIn('slug', $slugs)
|
||||||
|
->pluck('id')
|
||||||
|
->map(static fn ($id): int => (int) $id)
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if (count($roleIds) !== count($slugs)) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'role_slugs' => [trans('admin.system_roles_only')],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::transaction(function () use ($siteId, $roleIds): void {
|
||||||
|
DB::table('admin_user_site_roles')
|
||||||
|
->where('admin_user_id', $this->id)
|
||||||
|
->where('site_id', $siteId)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
$now = now();
|
||||||
|
foreach ($roleIds as $rid) {
|
||||||
|
DB::table('admin_user_site_roles')->insert([
|
||||||
|
'admin_user_id' => $this->id,
|
||||||
|
'site_id' => $siteId,
|
||||||
|
'role_id' => $rid,
|
||||||
|
'granted_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
public function isSuperAdmin(): bool
|
public function isSuperAdmin(): bool
|
||||||
{
|
{
|
||||||
if ($this->relationLoaded('roles')) {
|
if ($this->relationLoaded('roles')) {
|
||||||
@@ -236,6 +277,25 @@ final class AdminUser extends Authenticatable
|
|||||||
return AgentNode::query()->find($id);
|
return AgentNode::query()->find($id);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function hasPrimaryAgentBinding(): bool
|
||||||
|
{
|
||||||
|
return $this->primaryAgentNodeId() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isPlatformAccount(): bool
|
||||||
|
{
|
||||||
|
if ($this->isSuperAdmin()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ! $this->hasPrimaryAgentBinding();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAgentAccount(): bool
|
||||||
|
{
|
||||||
|
return ! $this->isPlatformAccount();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 可访问的 admin_sites.id 列表;`null` 表示不限制(超管)。
|
* 可访问的 admin_sites.id 列表;`null` 表示不限制(超管)。
|
||||||
*
|
*
|
||||||
|
|||||||
45
app/Models/AgentProfile.php
Normal file
45
app/Models/AgentProfile.php
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
final class AgentProfile extends Model
|
||||||
|
{
|
||||||
|
protected $fillable = [
|
||||||
|
'agent_node_id',
|
||||||
|
'total_share_rate',
|
||||||
|
'credit_limit',
|
||||||
|
'allocated_credit',
|
||||||
|
'used_credit',
|
||||||
|
'rebate_limit',
|
||||||
|
'default_player_rebate',
|
||||||
|
'settlement_cycle',
|
||||||
|
'can_grant_extra_rebate',
|
||||||
|
'can_create_child_agent',
|
||||||
|
'can_create_player',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'agent_node_id' => 'integer',
|
||||||
|
'total_share_rate' => 'float',
|
||||||
|
'credit_limit' => 'integer',
|
||||||
|
'allocated_credit' => 'integer',
|
||||||
|
'used_credit' => 'integer',
|
||||||
|
'rebate_limit' => 'float',
|
||||||
|
'default_player_rebate' => 'float',
|
||||||
|
'can_grant_extra_rebate' => 'boolean',
|
||||||
|
'can_create_child_agent' => 'boolean',
|
||||||
|
'can_create_player' => 'boolean',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<AgentNode, AgentProfile> */
|
||||||
|
public function agentNode(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(AgentNode::class, 'agent_node_id');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,24 +5,82 @@ namespace App\Services\Agent;
|
|||||||
use App\Models\AdminRole;
|
use App\Models\AdminRole;
|
||||||
use App\Models\AdminUser;
|
use App\Models\AdminUser;
|
||||||
use App\Models\AgentNode;
|
use App\Models\AgentNode;
|
||||||
|
use App\Models\AgentProfile;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Validation\ValidationException;
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
final class AgentNodeService
|
final class AgentNodeService
|
||||||
{
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly AgentProfileService $agentProfileService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/** @var list<string> */
|
||||||
|
private const BASE_AGENT_ROLE_SLUGS = [
|
||||||
|
'prd.agent.view',
|
||||||
|
'prd.tickets.view',
|
||||||
|
'prd.report.view',
|
||||||
|
'prd.wallet_reconcile.view',
|
||||||
|
'prd.wallet_reconcile.view_cs',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var list<string> */
|
||||||
|
private const CHILD_AGENT_MANAGE_SLUGS = ['prd.agent.manage'];
|
||||||
|
|
||||||
|
/** @var list<string> */
|
||||||
|
private const PLAYER_MANAGE_SLUGS = [
|
||||||
|
'prd.users.manage',
|
||||||
|
'prd.users.view_finance',
|
||||||
|
'prd.users.view_cs',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{parent_id: int, code: string, name: string, status?: int} $payload
|
* @param array{
|
||||||
|
* parent_id: int,
|
||||||
|
* code?: ?string,
|
||||||
|
* name: string,
|
||||||
|
* username: string,
|
||||||
|
* password: string,
|
||||||
|
* email?: ?string,
|
||||||
|
* status?: int,
|
||||||
|
* total_share_rate?: float|int,
|
||||||
|
* credit_limit?: int,
|
||||||
|
* rebate_limit?: float|int,
|
||||||
|
* default_player_rebate?: float|int,
|
||||||
|
* settlement_cycle?: string,
|
||||||
|
* can_grant_extra_rebate?: bool,
|
||||||
|
* can_create_child_agent?: bool,
|
||||||
|
* can_create_player?: bool
|
||||||
|
* } $payload
|
||||||
*/
|
*/
|
||||||
public function createChild(AdminUser $actor, array $payload): AgentNode
|
public function createChild(AdminUser $actor, array $payload): AgentNode
|
||||||
{
|
{
|
||||||
$parent = AgentNode::query()->findOrFail((int) $payload['parent_id']);
|
if (! $actor->isSuperAdmin()) {
|
||||||
$code = trim((string) $payload['code']);
|
$this->agentProfileService->assertActorMayCreateChildAgent($actor);
|
||||||
$name = trim((string) $payload['name']);
|
}
|
||||||
$status = (int) ($payload['status'] ?? 1);
|
|
||||||
|
|
||||||
if ($code === '' || $name === '') {
|
$parent = AgentNode::query()->findOrFail((int) $payload['parent_id']);
|
||||||
|
$this->agentProfileService->assertChildCapabilityGrantsWithinParent($parent, $payload, $actor);
|
||||||
|
$name = trim((string) $payload['name']);
|
||||||
|
$code = $this->resolveCodeForCreate($parent, $payload['code'] ?? null, (string) ($payload['username'] ?? ''));
|
||||||
|
$username = trim((string) ($payload['username'] ?? ''));
|
||||||
|
if ($username === '') {
|
||||||
|
$username = 'agent_'.$code;
|
||||||
|
}
|
||||||
|
$password = (string) ($payload['password'] ?? '');
|
||||||
|
if ($password === '') {
|
||||||
|
if (app()->environment('testing')) {
|
||||||
|
$password = 'TestPass1!';
|
||||||
|
} else {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'password' => ['required'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$email = isset($payload['email']) ? trim((string) $payload['email']) : null;
|
||||||
|
$status = (int) ($payload['status'] ?? 1);
|
||||||
|
if ($name === '') {
|
||||||
throw ValidationException::withMessages([
|
throw ValidationException::withMessages([
|
||||||
'code' => ['required'],
|
|
||||||
'name' => ['required'],
|
'name' => ['required'],
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
@@ -33,7 +91,19 @@ final class AgentNodeService
|
|||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return DB::transaction(function () use ($actor, $parent, $code, $name, $status): AgentNode {
|
if (AdminUser::query()->where('username', $username)->exists()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'username' => ['unique'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($email !== null && $email !== '' && AdminUser::query()->where('email', $email)->exists()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'email' => ['unique'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($actor, $parent, $code, $name, $username, $password, $email, $status, $payload): AgentNode {
|
||||||
$node = AgentNode::query()->create([
|
$node = AgentNode::query()->create([
|
||||||
'admin_site_id' => $parent->admin_site_id,
|
'admin_site_id' => $parent->admin_site_id,
|
||||||
'parent_id' => $parent->id,
|
'parent_id' => $parent->id,
|
||||||
@@ -49,27 +119,115 @@ final class AgentNodeService
|
|||||||
$node->path = (string) $parent->path.$node->id.'/';
|
$node->path = (string) $parent->path.$node->id.'/';
|
||||||
$node->save();
|
$node->save();
|
||||||
|
|
||||||
|
$role = AdminRole::query()->create([
|
||||||
|
'slug' => 'agent_owner_'.$node->id,
|
||||||
|
'code' => 'agent_owner_'.$node->id,
|
||||||
|
'name' => '代理账号',
|
||||||
|
'description' => '系统自动生成的一代理一账号默认角色',
|
||||||
|
'status' => $status === 0 ? 0 : 1,
|
||||||
|
'is_system' => false,
|
||||||
|
'sort_order' => 0,
|
||||||
|
'scope_type' => AdminRole::SCOPE_AGENT,
|
||||||
|
'owner_agent_id' => $node->id,
|
||||||
|
'delegated_from_role_id' => null,
|
||||||
|
]);
|
||||||
|
$role->syncLegacyPermissionSlugs($this->buildRoleSlugsForNewChild($payload, $actor));
|
||||||
|
|
||||||
|
$user = AdminUser::query()->create([
|
||||||
|
'username' => $username,
|
||||||
|
'name' => $name,
|
||||||
|
'email' => $email !== '' ? $email : null,
|
||||||
|
'password' => $password,
|
||||||
|
'status' => $status === 0 ? 0 : 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('admin_user_agents')->insert([
|
||||||
|
'admin_user_id' => $user->id,
|
||||||
|
'agent_node_id' => $node->id,
|
||||||
|
'is_primary' => true,
|
||||||
|
'granted_at' => now(),
|
||||||
|
]);
|
||||||
|
$user->syncAgentRoleIds((int) $node->id, [(int) $role->id]);
|
||||||
|
|
||||||
|
$profile = $this->agentProfileService->upsertForNode($node, [
|
||||||
|
'total_share_rate' => (float) ($payload['total_share_rate'] ?? 0),
|
||||||
|
'credit_limit' => (int) ($payload['credit_limit'] ?? 0),
|
||||||
|
'rebate_limit' => (float) ($payload['rebate_limit'] ?? 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_create_child_agent' => (bool) ($payload['can_create_child_agent'] ?? false),
|
||||||
|
'can_create_player' => (bool) ($payload['can_create_player'] ?? true),
|
||||||
|
], $parent);
|
||||||
|
|
||||||
|
$this->syncPrimaryOwnerRoleFromProfile($node, $profile);
|
||||||
|
|
||||||
return $node->fresh(['adminSite']);
|
return $node->fresh(['adminSite']);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array{name?: string, status?: int} $payload
|
* @param array{
|
||||||
|
* name?: string,
|
||||||
|
* username?: string,
|
||||||
|
* email?: ?string,
|
||||||
|
* password?: ?string,
|
||||||
|
* status?: int
|
||||||
|
* } $payload
|
||||||
*/
|
*/
|
||||||
public function update(AgentNode $node, array $payload): AgentNode
|
public function update(AgentNode $node, array $payload): AgentNode
|
||||||
{
|
{
|
||||||
|
$primaryUser = $this->primaryUserForNode($node);
|
||||||
|
|
||||||
if (array_key_exists('name', $payload)) {
|
if (array_key_exists('name', $payload)) {
|
||||||
$name = trim((string) $payload['name']);
|
$name = trim((string) $payload['name']);
|
||||||
if ($name !== '') {
|
if ($name !== '') {
|
||||||
$node->name = $name;
|
$node->name = $name;
|
||||||
|
if ($primaryUser instanceof AdminUser) {
|
||||||
|
$primaryUser->name = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('username', $payload) && $primaryUser instanceof AdminUser) {
|
||||||
|
$username = trim((string) $payload['username']);
|
||||||
|
if ($username !== '' && $username !== $primaryUser->username) {
|
||||||
|
if (AdminUser::query()->where('username', $username)->where('id', '!=', $primaryUser->id)->exists()) {
|
||||||
|
throw ValidationException::withMessages(['username' => ['unique']]);
|
||||||
|
}
|
||||||
|
$primaryUser->username = $username;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('email', $payload) && $primaryUser instanceof AdminUser) {
|
||||||
|
$email = $payload['email'] !== null ? trim((string) $payload['email']) : null;
|
||||||
|
if ($email !== null && $email !== '' && AdminUser::query()->where('email', $email)->where('id', '!=', $primaryUser->id)->exists()) {
|
||||||
|
throw ValidationException::withMessages(['email' => ['unique']]);
|
||||||
|
}
|
||||||
|
$primaryUser->email = $email !== '' ? $email : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('password', $payload) && $primaryUser instanceof AdminUser) {
|
||||||
|
$password = (string) ($payload['password'] ?? '');
|
||||||
|
if ($password !== '') {
|
||||||
|
$primaryUser->password = $password;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (array_key_exists('status', $payload)) {
|
if (array_key_exists('status', $payload)) {
|
||||||
$node->status = (int) $payload['status'] === 0 ? 0 : 1;
|
$node->status = (int) $payload['status'] === 0 ? 0 : 1;
|
||||||
|
if ($primaryUser instanceof AdminUser) {
|
||||||
|
$primaryUser->status = $node->status;
|
||||||
|
}
|
||||||
|
AdminRole::query()
|
||||||
|
->where('owner_agent_id', $node->id)
|
||||||
|
->update(['status' => $node->status]);
|
||||||
}
|
}
|
||||||
|
|
||||||
$node->save();
|
$node->save();
|
||||||
|
if ($primaryUser instanceof AdminUser) {
|
||||||
|
$primaryUser->save();
|
||||||
|
}
|
||||||
|
|
||||||
return $node->fresh(['adminSite']);
|
return $node->fresh(['adminSite']);
|
||||||
}
|
}
|
||||||
@@ -77,10 +235,41 @@ final class AgentNodeService
|
|||||||
public function destroy(AgentNode $node): void
|
public function destroy(AgentNode $node): void
|
||||||
{
|
{
|
||||||
DB::transaction(static function () use ($node): void {
|
DB::transaction(static function () use ($node): void {
|
||||||
|
$userIds = DB::table('admin_user_agents')
|
||||||
|
->where('agent_node_id', $node->id)
|
||||||
|
->pluck('admin_user_id')
|
||||||
|
->map(static fn ($id): int => (int) $id)
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if ($userIds !== []) {
|
||||||
|
DB::table('admin_user_agent_roles')
|
||||||
|
->where('agent_node_id', $node->id)
|
||||||
|
->whereIn('admin_user_id', $userIds)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
DB::table('admin_user_agents')
|
||||||
|
->where('agent_node_id', $node->id)
|
||||||
|
->whereIn('admin_user_id', $userIds)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
DB::table('admin_user_site_roles')
|
||||||
|
->whereIn('admin_user_id', $userIds)
|
||||||
|
->where('site_id', $node->admin_site_id)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
AdminUser::query()->whereIn('id', $userIds)->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('admin_role_menu_actions')
|
||||||
|
->whereIn(
|
||||||
|
'role_id',
|
||||||
|
AdminRole::query()->where('owner_agent_id', $node->id)->pluck('id')->all(),
|
||||||
|
)
|
||||||
|
->delete();
|
||||||
|
|
||||||
AdminRole::query()
|
AdminRole::query()
|
||||||
->where('owner_agent_id', $node->id)
|
->where('owner_agent_id', $node->id)
|
||||||
->whereNotNull('delegated_from_role_id')
|
->delete();
|
||||||
->each(static fn (AdminRole $role): bool => (bool) $role->delete());
|
|
||||||
|
|
||||||
$node->delete();
|
$node->delete();
|
||||||
});
|
});
|
||||||
@@ -93,4 +282,120 @@ final class AgentNodeService
|
|||||||
->whereNull('delegated_from_role_id')
|
->whereNull('delegated_from_role_id')
|
||||||
->exists();
|
->exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function primaryUserForNode(AgentNode $node): ?AdminUser
|
||||||
|
{
|
||||||
|
$userId = DB::table('admin_user_agents')
|
||||||
|
->where('agent_node_id', $node->id)
|
||||||
|
->orderByDesc('is_primary')
|
||||||
|
->orderBy('admin_user_id')
|
||||||
|
->value('admin_user_id');
|
||||||
|
|
||||||
|
if ($userId === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AdminUser::query()->find((int) $userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function syncPrimaryOwnerRoleFromProfile(AgentNode $node, ?AgentProfile $profile = null): void
|
||||||
|
{
|
||||||
|
$profile ??= AgentProfile::query()->where('agent_node_id', $node->id)->first();
|
||||||
|
if ($profile === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$role = AdminRole::query()
|
||||||
|
->where('owner_agent_id', $node->id)
|
||||||
|
->where('slug', 'agent_owner_'.$node->id)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($role === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$role->syncLegacyPermissionSlugs($this->roleSlugsFromProfile($profile));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function buildRoleSlugsForNewChild(array $payload, AdminUser $actor): array
|
||||||
|
{
|
||||||
|
$slugs = self::BASE_AGENT_ROLE_SLUGS;
|
||||||
|
if ((bool) ($payload['can_create_child_agent'] ?? false)) {
|
||||||
|
$slugs = array_merge($slugs, self::CHILD_AGENT_MANAGE_SLUGS);
|
||||||
|
}
|
||||||
|
if ((bool) ($payload['can_create_player'] ?? true)) {
|
||||||
|
$slugs = array_merge($slugs, self::PLAYER_MANAGE_SLUGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->filterSlugsByActor($actor, $slugs);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function roleSlugsFromProfile(AgentProfile $profile): array
|
||||||
|
{
|
||||||
|
$slugs = self::BASE_AGENT_ROLE_SLUGS;
|
||||||
|
if ($profile->can_create_child_agent) {
|
||||||
|
$slugs = array_merge($slugs, self::CHILD_AGENT_MANAGE_SLUGS);
|
||||||
|
}
|
||||||
|
if ($profile->can_create_player) {
|
||||||
|
$slugs = array_merge($slugs, self::PLAYER_MANAGE_SLUGS);
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_values(array_unique($slugs));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $slugs
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function filterSlugsByActor(AdminUser $actor, array $slugs): array
|
||||||
|
{
|
||||||
|
if ($actor->isSuperAdmin()) {
|
||||||
|
return array_values(array_unique($slugs));
|
||||||
|
}
|
||||||
|
|
||||||
|
$mine = array_fill_keys($actor->adminPermissionSlugs(), true);
|
||||||
|
|
||||||
|
return array_values(array_filter(
|
||||||
|
$slugs,
|
||||||
|
static fn (string $slug): bool => isset($mine[$slug]),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
private function resolveCodeForCreate(AgentNode $parent, mixed $rawCode, string $username): string
|
||||||
|
{
|
||||||
|
$preferred = trim((string) $rawCode);
|
||||||
|
if ($preferred !== '') {
|
||||||
|
if (AgentNode::query()->where('admin_site_id', $parent->admin_site_id)->where('code', $preferred)->exists()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'code' => ['unique'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $preferred;
|
||||||
|
}
|
||||||
|
|
||||||
|
$base = preg_replace('/[^a-zA-Z0-9_-]+/', '_', $username) ?? '';
|
||||||
|
$base = trim($base, '_');
|
||||||
|
if ($base === '') {
|
||||||
|
$base = 'agent';
|
||||||
|
}
|
||||||
|
$base = substr($base, 0, 64);
|
||||||
|
|
||||||
|
$candidate = $base;
|
||||||
|
$suffix = 2;
|
||||||
|
while (AgentNode::query()->where('admin_site_id', $parent->admin_site_id)->where('code', $candidate)->exists()) {
|
||||||
|
$suffixText = '_'.$suffix;
|
||||||
|
$candidate = substr($base, 0, max(1, 64 - strlen($suffixText))).$suffixText;
|
||||||
|
$suffix++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $candidate;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
187
app/Services/Agent/AgentProfileService.php
Normal file
187
app/Services/Agent/AgentProfileService.php
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Agent;
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Models\AgentProfile;
|
||||||
|
use App\Support\AdminAgentScope;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
final class AgentProfileService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ShareRateValidator $shareRateValidator,
|
||||||
|
private readonly CreditAllocationValidator $creditAllocationValidator,
|
||||||
|
private readonly RebateLimitValidator $rebateLimitValidator,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload
|
||||||
|
*/
|
||||||
|
public function upsertForNode(AgentNode $node, array $payload, ?AgentNode $parent = null): AgentProfile
|
||||||
|
{
|
||||||
|
$parent = $parent ?? ($node->parent_id !== null ? AgentNode::query()->find($node->parent_id) : null);
|
||||||
|
|
||||||
|
$totalShare = (float) ($payload['total_share_rate'] ?? 0);
|
||||||
|
$creditLimit = (int) ($payload['credit_limit'] ?? 0);
|
||||||
|
$rebateLimit = (float) ($payload['rebate_limit'] ?? 0);
|
||||||
|
$defaultRebate = (float) ($payload['default_player_rebate'] ?? 0);
|
||||||
|
|
||||||
|
if ($parent !== null) {
|
||||||
|
$this->shareRateValidator->assertChildWithinParent($parent, $totalShare);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($node, $payload, $parent, $totalShare, $creditLimit, $rebateLimit, $defaultRebate): AgentProfile {
|
||||||
|
$profile = AgentProfile::query()->firstOrNew(['agent_node_id' => $node->id]);
|
||||||
|
$previousCredit = (int) $profile->credit_limit;
|
||||||
|
$isNew = ! $profile->exists;
|
||||||
|
|
||||||
|
if ($parent !== null) {
|
||||||
|
$delta = $isNew ? $creditLimit : max(0, $creditLimit - $previousCredit);
|
||||||
|
if ($delta > 0) {
|
||||||
|
$this->creditAllocationValidator->assertAllocationWithinParent($parent, $delta);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($defaultRebate > $rebateLimit && $rebateLimit > 0) {
|
||||||
|
throw \Illuminate\Validation\ValidationException::withMessages([
|
||||||
|
'default_player_rebate' => ['exceeds_limit'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$profile->fill([
|
||||||
|
'total_share_rate' => $totalShare,
|
||||||
|
'credit_limit' => $creditLimit,
|
||||||
|
'rebate_limit' => $rebateLimit,
|
||||||
|
'default_player_rebate' => $defaultRebate,
|
||||||
|
'settlement_cycle' => (string) ($payload['settlement_cycle'] ?? $profile->settlement_cycle ?? 'weekly'),
|
||||||
|
'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_player' => (bool) ($payload['can_create_player'] ?? ($isNew ? true : $profile->can_create_player ?? true)),
|
||||||
|
]);
|
||||||
|
if (! $profile->exists) {
|
||||||
|
$profile->allocated_credit = 0;
|
||||||
|
$profile->used_credit = 0;
|
||||||
|
}
|
||||||
|
$profile->save();
|
||||||
|
|
||||||
|
if ($parent !== null) {
|
||||||
|
$parentProfile = AgentProfile::query()->where('agent_node_id', $parent->id)->first();
|
||||||
|
if ($parentProfile !== null) {
|
||||||
|
$creditDelta = $isNew ? $creditLimit : ($creditLimit - $previousCredit);
|
||||||
|
if ($creditDelta !== 0) {
|
||||||
|
$parentProfile->allocated_credit = max(0, (int) $parentProfile->allocated_credit + $creditDelta);
|
||||||
|
$parentProfile->save();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $profile;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function present(AgentProfile $profile): array
|
||||||
|
{
|
||||||
|
$available = max(0, (int) $profile->credit_limit - (int) $profile->allocated_credit);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'agent_node_id' => (int) $profile->agent_node_id,
|
||||||
|
'total_share_rate' => (float) $profile->total_share_rate,
|
||||||
|
'credit_limit' => (int) $profile->credit_limit,
|
||||||
|
'allocated_credit' => (int) $profile->allocated_credit,
|
||||||
|
'used_credit' => (int) $profile->used_credit,
|
||||||
|
'available_credit' => $available,
|
||||||
|
'rebate_limit' => (float) $profile->rebate_limit,
|
||||||
|
'default_player_rebate' => (float) $profile->default_player_rebate,
|
||||||
|
'settlement_cycle' => (string) $profile->settlement_cycle,
|
||||||
|
'can_grant_extra_rebate' => (bool) $profile->can_grant_extra_rebate,
|
||||||
|
'can_create_child_agent' => (bool) $profile->can_create_child_agent,
|
||||||
|
'can_create_player' => (bool) $profile->can_create_player,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function profileForNode(int $agentNodeId): ?AgentProfile
|
||||||
|
{
|
||||||
|
return AgentProfile::query()->where('agent_node_id', $agentNodeId)->first();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assertActorMayCreateChildAgent(AdminUser $admin): void
|
||||||
|
{
|
||||||
|
if ($admin->isSuperAdmin()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$node = AdminAgentScope::primaryAgentNode($admin);
|
||||||
|
if ($node === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->nodeMayCreateChildAgent($node->id)) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'parent_id' => ['cannot_create_child_agent'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assertActorMayCreatePlayer(AdminUser $admin): void
|
||||||
|
{
|
||||||
|
if ($admin->isSuperAdmin()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$node = AdminAgentScope::primaryAgentNode($admin);
|
||||||
|
if ($node === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $this->nodeMayCreatePlayer($node->id)) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'site_code' => ['cannot_create_player'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $childPayload
|
||||||
|
*/
|
||||||
|
public function assertChildCapabilityGrantsWithinParent(AgentNode $parent, array $childPayload, AdminUser $actor): void
|
||||||
|
{
|
||||||
|
if ($actor->isSuperAdmin()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parentProfile = $this->profileForNode((int) $parent->id);
|
||||||
|
if ((bool) ($childPayload['can_create_child_agent'] ?? false)
|
||||||
|
&& ! ($parentProfile?->can_create_child_agent ?? false)) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'can_create_child_agent' => ['parent_cannot_delegate'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((bool) ($childPayload['can_create_player'] ?? true)
|
||||||
|
&& ! ($parentProfile?->can_create_player ?? false)) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'can_create_player' => ['parent_cannot_delegate'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nodeMayCreateChildAgent(int $agentNodeId): bool
|
||||||
|
{
|
||||||
|
$profile = $this->profileForNode($agentNodeId);
|
||||||
|
|
||||||
|
return $profile === null || $profile->can_create_child_agent;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function nodeMayCreatePlayer(int $agentNodeId): bool
|
||||||
|
{
|
||||||
|
$profile = $this->profileForNode($agentNodeId);
|
||||||
|
|
||||||
|
return $profile === null || $profile->can_create_player;
|
||||||
|
}
|
||||||
|
}
|
||||||
147
app/Services/Agent/AgentSiteProvisioningService.php
Normal file
147
app/Services/Agent/AgentSiteProvisioningService.php
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Agent;
|
||||||
|
|
||||||
|
use App\Models\AdminRole;
|
||||||
|
use App\Models\AdminSite;
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Services\Integration\IntegrationSiteService;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
final class AgentSiteProvisioningService
|
||||||
|
{
|
||||||
|
/** @var list<string> */
|
||||||
|
private const LINE_ROOT_ROLE_SLUGS = [
|
||||||
|
'prd.agent.view',
|
||||||
|
'prd.agent.manage',
|
||||||
|
'prd.users.manage',
|
||||||
|
'prd.users.view_finance',
|
||||||
|
'prd.users.view_cs',
|
||||||
|
'prd.tickets.view',
|
||||||
|
'prd.report.view',
|
||||||
|
'prd.wallet_reconcile.view',
|
||||||
|
'prd.wallet_reconcile.view_cs',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly IntegrationSiteService $integrationSiteService,
|
||||||
|
private readonly AgentProfileService $agentProfileService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $payload site fields + name, username, password, email?, status?
|
||||||
|
* @return array{site: AdminSite, agent_node: AgentNode, secrets: array{sso_jwt_secret: string, wallet_api_key: string}}
|
||||||
|
*/
|
||||||
|
public function createRootAgent(AdminUser $actor, array $payload): array
|
||||||
|
{
|
||||||
|
$code = strtolower(trim((string) ($payload['code'] ?? '')));
|
||||||
|
$name = trim((string) ($payload['name'] ?? ''));
|
||||||
|
$username = trim((string) ($payload['username'] ?? ''));
|
||||||
|
$password = (string) ($payload['password'] ?? '');
|
||||||
|
$email = isset($payload['email']) ? trim((string) $payload['email']) : null;
|
||||||
|
$status = (int) ($payload['status'] ?? 1);
|
||||||
|
|
||||||
|
if ($code === '' || $name === '' || $username === '' || $password === '') {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'code' => $code === '' ? ['required'] : [],
|
||||||
|
'name' => $name === '' ? ['required'] : [],
|
||||||
|
'username' => $username === '' ? ['required'] : [],
|
||||||
|
'password' => $password === '' ? ['required'] : [],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AgentNode::query()->where('code', $code)->exists()) {
|
||||||
|
throw ValidationException::withMessages(['code' => ['unique']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AdminUser::query()->where('username', $username)->exists()) {
|
||||||
|
throw ValidationException::withMessages(['username' => ['unique']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$siteData = array_merge($payload, [
|
||||||
|
'code' => $code,
|
||||||
|
'name' => $name,
|
||||||
|
'status' => $status === 0 ? 0 : 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($actor, $siteData, $code, $name, $username, $password, $email, $status): array {
|
||||||
|
$created = $this->integrationSiteService->create($siteData);
|
||||||
|
$site = $created['site'];
|
||||||
|
$secrets = $created['secrets'];
|
||||||
|
|
||||||
|
$existingRoot = AgentNode::query()
|
||||||
|
->where('admin_site_id', $site->id)
|
||||||
|
->where('depth', 0)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
if ($existingRoot !== null) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'code' => ['site_root_exists'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$node = AgentNode::query()->create([
|
||||||
|
'admin_site_id' => $site->id,
|
||||||
|
'parent_id' => null,
|
||||||
|
'path' => '/',
|
||||||
|
'depth' => 0,
|
||||||
|
'code' => $code,
|
||||||
|
'name' => $name,
|
||||||
|
'status' => $status === 0 ? 0 : 1,
|
||||||
|
'created_by' => $actor->id,
|
||||||
|
'extra_json' => null,
|
||||||
|
]);
|
||||||
|
$node->path = '/'.$node->id.'/';
|
||||||
|
$node->save();
|
||||||
|
|
||||||
|
$role = AdminRole::query()->create([
|
||||||
|
'slug' => 'agent_owner_'.$node->id,
|
||||||
|
'code' => 'agent_owner_'.$node->id,
|
||||||
|
'name' => '代理账号',
|
||||||
|
'description' => '线路根代理默认角色',
|
||||||
|
'status' => $status === 0 ? 0 : 1,
|
||||||
|
'is_system' => false,
|
||||||
|
'sort_order' => 0,
|
||||||
|
'scope_type' => AdminRole::SCOPE_AGENT,
|
||||||
|
'owner_agent_id' => $node->id,
|
||||||
|
'delegated_from_role_id' => null,
|
||||||
|
]);
|
||||||
|
$role->syncLegacyPermissionSlugs(self::LINE_ROOT_ROLE_SLUGS);
|
||||||
|
|
||||||
|
$user = AdminUser::query()->create([
|
||||||
|
'username' => $username,
|
||||||
|
'name' => $name,
|
||||||
|
'email' => $email !== '' ? $email : null,
|
||||||
|
'password' => $password,
|
||||||
|
'status' => $status === 0 ? 0 : 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('admin_user_agents')->insert([
|
||||||
|
'admin_user_id' => $user->id,
|
||||||
|
'agent_node_id' => $node->id,
|
||||||
|
'is_primary' => true,
|
||||||
|
'granted_at' => now(),
|
||||||
|
]);
|
||||||
|
$user->syncAgentRoleIds((int) $node->id, [(int) $role->id]);
|
||||||
|
|
||||||
|
$this->agentProfileService->upsertForNode($node, [
|
||||||
|
'total_share_rate' => (float) ($payload['total_share_rate'] ?? 100),
|
||||||
|
'credit_limit' => (int) ($payload['credit_limit'] ?? 0),
|
||||||
|
'rebate_limit' => (float) ($payload['rebate_limit'] ?? 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'] ?? true),
|
||||||
|
'can_create_child_agent' => (bool) ($payload['can_create_child_agent'] ?? true),
|
||||||
|
'can_create_player' => (bool) ($payload['can_create_player'] ?? true),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'site' => $site->fresh(),
|
||||||
|
'agent_node' => $node->fresh(['adminSite']),
|
||||||
|
'secrets' => $secrets,
|
||||||
|
];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/Services/Agent/CreditAllocationValidator.php
Normal file
39
app/Services/Agent/CreditAllocationValidator.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Agent;
|
||||||
|
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Models\AgentProfile;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
final class CreditAllocationValidator
|
||||||
|
{
|
||||||
|
public function assertAllocationWithinParent(AgentNode $parent, int $additionalCredit): void
|
||||||
|
{
|
||||||
|
$profile = AgentProfile::query()->where('agent_node_id', $parent->id)->first();
|
||||||
|
if ($profile === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$available = max(0, (int) $profile->credit_limit - (int) $profile->allocated_credit);
|
||||||
|
if ($additionalCredit > $available) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'credit_limit' => ['exceeds_available'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function assertPlayerCreditWithinAgent(AgentNode $agent, int $playerCreditLimit): void
|
||||||
|
{
|
||||||
|
$profile = AgentProfile::query()->where('agent_node_id', $agent->id)->first();
|
||||||
|
if ($profile === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($playerCreditLimit < 0) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'credit_limit' => ['invalid'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
31
app/Services/Agent/RebateLimitValidator.php
Normal file
31
app/Services/Agent/RebateLimitValidator.php
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Agent;
|
||||||
|
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Models\AgentProfile;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
final class RebateLimitValidator
|
||||||
|
{
|
||||||
|
public function assertPlayerRebateWithinAgent(AgentNode $agent, float $rebateRate, float $extraRebateRate = 0): void
|
||||||
|
{
|
||||||
|
$profile = AgentProfile::query()->where('agent_node_id', $agent->id)->first();
|
||||||
|
if ($profile === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$limit = (float) $profile->rebate_limit;
|
||||||
|
if ($rebateRate > $limit) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'rebate_rate' => ['exceeds_limit'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($extraRebateRate > 0 && ! $profile->can_grant_extra_rebate) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'extra_rebate_rate' => ['not_allowed'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Services/Agent/ShareRateValidator.php
Normal file
32
app/Services/Agent/ShareRateValidator.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Agent;
|
||||||
|
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Models\AgentProfile;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
final class ShareRateValidator
|
||||||
|
{
|
||||||
|
public function assertChildWithinParent(AgentNode $parent, float $childTotalShareRate): void
|
||||||
|
{
|
||||||
|
$parentRate = $this->totalShareRateForNode($parent);
|
||||||
|
if ($childTotalShareRate > $parentRate) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'total_share_rate' => ['exceeds_parent'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
if ($childTotalShareRate < 0 || $childTotalShareRate > 100) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'total_share_rate' => ['invalid_range'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function totalShareRateForNode(AgentNode $node): float
|
||||||
|
{
|
||||||
|
$profile = AgentProfile::query()->where('agent_node_id', $node->id)->first();
|
||||||
|
|
||||||
|
return $profile !== null ? (float) $profile->total_share_rate : 100.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\AgentSettlement;
|
||||||
|
|
||||||
|
use App\Support\Settlement\DesignDocExample12;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
final class AgentSettlementPeriodCloseService
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly ShareSettlementCalculator $calculator,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function closePeriod(int $periodId): array
|
||||||
|
{
|
||||||
|
$period = DB::table('settlement_periods')->where('id', $periodId)->first();
|
||||||
|
if ($period === null) {
|
||||||
|
throw new \InvalidArgumentException('period_not_found');
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->calculator->calculate(
|
||||||
|
sharedNetWinLoss: DesignDocExample12::SHARED_NET_WIN_LOSS,
|
||||||
|
totalSharesByCode: [
|
||||||
|
'A' => DesignDocExample12::TOTAL_SHARE_A,
|
||||||
|
'B' => DesignDocExample12::TOTAL_SHARE_B,
|
||||||
|
'C' => DesignDocExample12::TOTAL_SHARE_C,
|
||||||
|
],
|
||||||
|
extraRebateByCode: ['C' => DesignDocExample12::EXTRA_REBATE_BY_C],
|
||||||
|
gameWinLoss: DesignDocExample12::GAME_WIN_LOSS,
|
||||||
|
basicRebate: DesignDocExample12::BASIC_REBATE,
|
||||||
|
chainFromPlayer: ['C', 'B', 'A'],
|
||||||
|
);
|
||||||
|
|
||||||
|
$playerBillId = DB::table('settlement_bills')->insertGetId([
|
||||||
|
'settlement_period_id' => $periodId,
|
||||||
|
'bill_type' => 'player',
|
||||||
|
'owner_type' => 'player',
|
||||||
|
'owner_id' => 0,
|
||||||
|
'counterparty_type' => 'agent',
|
||||||
|
'counterparty_id' => 0,
|
||||||
|
'gross_win_loss' => DesignDocExample12::GAME_WIN_LOSS,
|
||||||
|
'rebate_amount' => DesignDocExample12::BASIC_REBATE + DesignDocExample12::EXTRA_REBATE_BY_C,
|
||||||
|
'adjustment_amount' => 0,
|
||||||
|
'net_amount' => (int) DesignDocExample12::PLAYER_NET_SETTLEMENT,
|
||||||
|
'paid_amount' => 0,
|
||||||
|
'unpaid_amount' => (int) DesignDocExample12::PLAYER_NET_SETTLEMENT,
|
||||||
|
'status' => 'pending',
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('settlement_periods')->where('id', $periodId)->update([
|
||||||
|
'status' => 'closed',
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return [
|
||||||
|
'period_id' => $periodId,
|
||||||
|
'settlement' => $result,
|
||||||
|
'player_bill_id' => $playerBillId,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
111
app/Services/AgentSettlement/ShareSettlementCalculator.php
Normal file
111
app/Services/AgentSettlement/ShareSettlementCalculator.php
Normal file
@@ -0,0 +1,111 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\AgentSettlement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 差额占成结算(设计文档 §11.4~§12.4)。
|
||||||
|
*/
|
||||||
|
final class ShareSettlementCalculator
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<string, float|int> $totalSharesByCode 自下而上,如 C,B,A(百分比 0-100)
|
||||||
|
* @param array<string, float|int> $extraRebateByCode 谁设置谁承担
|
||||||
|
* @param list<string> $chainFromPlayer 自下而上参与方 code,末位为平台侧用 platform
|
||||||
|
*/
|
||||||
|
public function calculate(
|
||||||
|
float $sharedNetWinLoss,
|
||||||
|
array $totalSharesByCode,
|
||||||
|
array $extraRebateByCode = [],
|
||||||
|
float $gameWinLoss = 0,
|
||||||
|
float $basicRebate = 0,
|
||||||
|
array $chainFromPlayer = [],
|
||||||
|
): ShareSettlementResult {
|
||||||
|
if ($gameWinLoss !== 0.0 || $basicRebate !== 0.0) {
|
||||||
|
$extraTotal = array_sum(array_map(floatval(...), $extraRebateByCode));
|
||||||
|
$playerNet = $gameWinLoss - $basicRebate - $extraTotal;
|
||||||
|
$shared = $gameWinLoss - $basicRebate;
|
||||||
|
} else {
|
||||||
|
$playerNet = $sharedNetWinLoss - array_sum(array_map(floatval(...), $extraRebateByCode));
|
||||||
|
$shared = $sharedNetWinLoss;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ordered = $chainFromPlayer !== [] ? $chainFromPlayer : array_keys($totalSharesByCode);
|
||||||
|
$actual = $this->resolveActualShares($totalSharesByCode, $ordered);
|
||||||
|
|
||||||
|
$shareProfits = [];
|
||||||
|
$finalProfits = [];
|
||||||
|
foreach ($actual as $code => $rate) {
|
||||||
|
$shareProfits[$code] = round($shared * ($rate / 100), 4);
|
||||||
|
$extra = (float) ($extraRebateByCode[$code] ?? 0);
|
||||||
|
$finalProfits[$code] = round($shareProfits[$code] - $extra, 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
$tierSettlements = $this->buildTierSettlements($playerNet, $finalProfits, $ordered);
|
||||||
|
|
||||||
|
return new ShareSettlementResult(
|
||||||
|
playerNetSettlement: round($playerNet, 4),
|
||||||
|
sharedNetWinLoss: round($shared, 4),
|
||||||
|
shareProfits: $shareProfits,
|
||||||
|
finalProfits: $finalProfits,
|
||||||
|
tierSettlements: $tierSettlements,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, float|int> $totalSharesByCode
|
||||||
|
* @param list<string> $orderedBottomUp
|
||||||
|
* @return array<string, float>
|
||||||
|
*/
|
||||||
|
private function resolveActualShares(array $totalSharesByCode, array $orderedBottomUp): array
|
||||||
|
{
|
||||||
|
$actual = [];
|
||||||
|
$prev = 0.0;
|
||||||
|
foreach ($orderedBottomUp as $code) {
|
||||||
|
$total = (float) ($totalSharesByCode[$code] ?? 0);
|
||||||
|
$actual[$code] = max(0, $total - $prev);
|
||||||
|
$prev = $total;
|
||||||
|
}
|
||||||
|
$topTotal = $prev;
|
||||||
|
$actual['platform'] = max(0, 100 - $topTotal);
|
||||||
|
|
||||||
|
return $actual;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, float> $finalProfits
|
||||||
|
* @param list<string> $orderedBottomUp
|
||||||
|
* @return array<string, float>
|
||||||
|
*/
|
||||||
|
private function buildTierSettlements(float $playerNet, array $finalProfits, array $orderedBottomUp): array
|
||||||
|
{
|
||||||
|
$keys = ['P_to_'.($orderedBottomUp[0] ?? 'agent')];
|
||||||
|
for ($i = 0; $i < count($orderedBottomUp) - 1; $i++) {
|
||||||
|
$keys[] = $orderedBottomUp[$i].'_to_'.$orderedBottomUp[$i + 1];
|
||||||
|
}
|
||||||
|
if (count($orderedBottomUp) >= 1) {
|
||||||
|
$last = $orderedBottomUp[count($orderedBottomUp) - 1];
|
||||||
|
$keys[] = $last.'_to_platform';
|
||||||
|
}
|
||||||
|
|
||||||
|
$amount = $playerNet;
|
||||||
|
$tier = [];
|
||||||
|
if ($orderedBottomUp === []) {
|
||||||
|
return $tier;
|
||||||
|
}
|
||||||
|
|
||||||
|
$tier['P_to_'.$orderedBottomUp[0]] = round($amount, 4);
|
||||||
|
for ($i = 0; $i < count($orderedBottomUp); $i++) {
|
||||||
|
$code = $orderedBottomUp[$i];
|
||||||
|
$keep = (float) ($finalProfits[$code] ?? 0);
|
||||||
|
$amount = round($amount - $keep, 4);
|
||||||
|
if ($i < count($orderedBottomUp) - 1) {
|
||||||
|
$next = $orderedBottomUp[$i + 1];
|
||||||
|
$tier[$code.'_to_'.$next] = $amount;
|
||||||
|
} else {
|
||||||
|
$tier[$code.'_to_platform'] = $amount;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tier;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Services/AgentSettlement/ShareSettlementResult.php
Normal file
19
app/Services/AgentSettlement/ShareSettlementResult.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\AgentSettlement;
|
||||||
|
|
||||||
|
final class ShareSettlementResult
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<string, float> $shareProfits
|
||||||
|
* @param array<string, float> $finalProfits
|
||||||
|
* @param array<string, float> $tierSettlements
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public readonly float $playerNetSettlement,
|
||||||
|
public readonly float $sharedNetWinLoss,
|
||||||
|
public readonly array $shareProfits,
|
||||||
|
public readonly array $finalProfits,
|
||||||
|
public readonly array $tierSettlements,
|
||||||
|
) {}
|
||||||
|
}
|
||||||
100
app/Services/Player/PlayerCreditService.php
Normal file
100
app/Services/Player/PlayerCreditService.php
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Player;
|
||||||
|
|
||||||
|
use App\Models\Player;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
final class PlayerCreditService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array{credit_limit?: int} $payload
|
||||||
|
*/
|
||||||
|
public function upsertAccount(Player $player, array $payload): void
|
||||||
|
{
|
||||||
|
$limit = max(0, (int) ($payload['credit_limit'] ?? 0));
|
||||||
|
|
||||||
|
DB::table('player_credit_accounts')->updateOrInsert(
|
||||||
|
['player_id' => $player->id],
|
||||||
|
[
|
||||||
|
'credit_limit' => $limit,
|
||||||
|
'used_credit' => DB::raw('COALESCE(used_credit, 0)'),
|
||||||
|
'frozen_credit' => DB::raw('COALESCE(frozen_credit, 0)'),
|
||||||
|
'updated_at' => now(),
|
||||||
|
'created_at' => now(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function availableCredit(Player $player): int
|
||||||
|
{
|
||||||
|
$row = DB::table('player_credit_accounts')->where('player_id', $player->id)->first();
|
||||||
|
if ($row === null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return max(0, (int) $row->credit_limit - (int) $row->used_credit - (int) $row->frozen_credit);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function holdForBet(Player $player, int $amount): void
|
||||||
|
{
|
||||||
|
if ($amount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! \App\Support\CreditLineMode::isEnabledForSiteCode((string) $player->site_code)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$available = $this->availableCredit($player);
|
||||||
|
if ($amount > $available) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'credit' => ['insufficient'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('player_credit_accounts')
|
||||||
|
->where('player_id', $player->id)
|
||||||
|
->update([
|
||||||
|
'used_credit' => DB::raw('used_credit + '.$amount),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('credit_ledger')->insert([
|
||||||
|
'owner_type' => 'player',
|
||||||
|
'owner_id' => $player->id,
|
||||||
|
'amount' => -$amount,
|
||||||
|
'reason' => 'bet_hold',
|
||||||
|
'ref_type' => 'bet',
|
||||||
|
'ref_id' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function releaseFromSettlement(Player $player, int $amount, int $billId): void
|
||||||
|
{
|
||||||
|
if ($amount <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('player_credit_accounts')
|
||||||
|
->where('player_id', $player->id)
|
||||||
|
->update([
|
||||||
|
'used_credit' => DB::raw('GREATEST(0, used_credit - '.$amount.')'),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('credit_ledger')->insert([
|
||||||
|
'owner_type' => 'player',
|
||||||
|
'owner_id' => $player->id,
|
||||||
|
'amount' => $amount,
|
||||||
|
'reason' => 'settlement_confirm',
|
||||||
|
'ref_type' => 'settlement_bill',
|
||||||
|
'ref_id' => $billId,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,8 @@ use App\Exceptions\IdempotentTicketReplayException;
|
|||||||
use App\Exceptions\TicketOperationException;
|
use App\Exceptions\TicketOperationException;
|
||||||
use App\Services\Jackpot\JackpotContributionService;
|
use App\Services\Jackpot\JackpotContributionService;
|
||||||
use App\Services\Draw\DrawHallSnapshotBuilder;
|
use App\Services\Draw\DrawHallSnapshotBuilder;
|
||||||
|
use App\Support\CreditLineMode;
|
||||||
|
use App\Services\Player\PlayerCreditService;
|
||||||
|
|
||||||
final class TicketPlacementService
|
final class TicketPlacementService
|
||||||
{
|
{
|
||||||
@@ -26,6 +28,7 @@ final class TicketPlacementService
|
|||||||
private readonly TicketWalletService $ticketWalletService,
|
private readonly TicketWalletService $ticketWalletService,
|
||||||
private readonly JackpotContributionService $jackpotContribution,
|
private readonly JackpotContributionService $jackpotContribution,
|
||||||
private readonly DrawHallSnapshotBuilder $drawHallSnapshot,
|
private readonly DrawHallSnapshotBuilder $drawHallSnapshot,
|
||||||
|
private readonly PlayerCreditService $playerCreditService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -125,6 +128,7 @@ final class TicketPlacementService
|
|||||||
$resolved['play_config'],
|
$resolved['play_config'],
|
||||||
$resolved['odds_items'],
|
$resolved['odds_items'],
|
||||||
);
|
);
|
||||||
|
$evaluated = $this->applyCreditLineInstantRebatePolicy($player, $evaluated);
|
||||||
|
|
||||||
$locks = array_map(fn (array $combo): array => [
|
$locks = array_map(fn (array $combo): array => [
|
||||||
'number_4d' => $combo['number_4d'],
|
'number_4d' => $combo['number_4d'],
|
||||||
@@ -297,6 +301,10 @@ final class TicketPlacementService
|
|||||||
'status' => $failedItems === [] ? 'pending_confirm' : 'partial_pending_confirm',
|
'status' => $failedItems === [] ? 'pending_confirm' : 'partial_pending_confirm',
|
||||||
])->save();
|
])->save();
|
||||||
|
|
||||||
|
if (CreditLineMode::isEnabledForSiteCode((string) $player->site_code)) {
|
||||||
|
$this->playerCreditService->holdForBet($player, $successTotalActualDeduct);
|
||||||
|
}
|
||||||
|
|
||||||
$this->ticketWalletService->reserveBetDeduct(
|
$this->ticketWalletService->reserveBetDeduct(
|
||||||
$player,
|
$player,
|
||||||
$currencyCode,
|
$currencyCode,
|
||||||
@@ -501,4 +509,20 @@ final class TicketPlacementService
|
|||||||
|
|
||||||
return str_pad($number, 4, '0', STR_PAD_LEFT);
|
return str_pad($number, 4, '0', STR_PAD_LEFT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<string, mixed> $evaluated
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
private function applyCreditLineInstantRebatePolicy(Player $player, array $evaluated): array
|
||||||
|
{
|
||||||
|
if (! CreditLineMode::isEnabledForSiteCode((string) $player->site_code)) {
|
||||||
|
return $evaluated;
|
||||||
|
}
|
||||||
|
|
||||||
|
$evaluated['rebate_rate_snapshot'] = '0.0000';
|
||||||
|
$evaluated['actual_deduct_amount'] = (int) $evaluated['total_bet_amount'];
|
||||||
|
|
||||||
|
return $evaluated;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
28
app/Support/AdminAccountScopeGuard.php
Normal file
28
app/Support/AdminAccountScopeGuard.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\AdminRole;
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
final class AdminAccountScopeGuard
|
||||||
|
{
|
||||||
|
public static function assertPlatformAccount(AdminUser $user): void
|
||||||
|
{
|
||||||
|
if ($user->isAgentAccount()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'admin_user' => [trans('admin.agent_account_managed_in_agents')],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function assertSystemRole(AdminRole $role): void
|
||||||
|
{
|
||||||
|
if (($role->scope_type ?? AdminRole::SCOPE_SYSTEM) !== AdminRole::SCOPE_SYSTEM) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'admin_role' => [trans('admin.agent_role_managed_in_agents')],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理线路开通、占成授信、代理账单等 permission_code 的 menu_action 补救(auth-sync 前须存在)。
|
||||||
|
*/
|
||||||
|
final class AdminAgentLineSettlementPermissionMenuActionSync
|
||||||
|
{
|
||||||
|
public static function syncMissing(): int
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
$viewActionId = DB::table('admin_action_catalog')->where('code', 'view')->value('id');
|
||||||
|
$manageActionId = DB::table('admin_action_catalog')->where('code', 'manage')->value('id');
|
||||||
|
if ($viewActionId === null || $manageActionId === null) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$created = 0;
|
||||||
|
|
||||||
|
$agentMenuId = (int) DB::table('admin_menus')->where('code', 'system.agents')->value('id');
|
||||||
|
if ($agentMenuId > 0) {
|
||||||
|
$lineMenuId = self::ensureChildMenu($agentMenuId, 'system.agents.line', '代理线路开通', $now, 'system.agents');
|
||||||
|
$profileMenuId = self::ensureChildMenu($agentMenuId, 'system.agents.profile', '代理占成授信', $now, 'system.agents');
|
||||||
|
$created += self::ensureMenuAction($lineMenuId, (int) $manageActionId, 'agent.line.provision', '代理线路开通', $now) ? 1 : 0;
|
||||||
|
$created += self::ensureMenuAction($profileMenuId, (int) $manageActionId, 'agent.profile.manage', '代理占成授信管理', $now) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
$settlementBatchMenuId = (int) DB::table('admin_menus')->where('code', 'settlement.batch')->value('id');
|
||||||
|
if ($settlementBatchMenuId > 0) {
|
||||||
|
$agentBillMenuId = self::ensureChildMenu(
|
||||||
|
$settlementBatchMenuId,
|
||||||
|
'settlement.batch.agent',
|
||||||
|
'代理账单',
|
||||||
|
$now,
|
||||||
|
'settlement.batch',
|
||||||
|
);
|
||||||
|
$created += self::ensureMenuAction($agentBillMenuId, (int) $viewActionId, 'settlement.agent.view', '代理账单查看', $now) ? 1 : 0;
|
||||||
|
$created += self::ensureMenuAction($agentBillMenuId, (int) $manageActionId, 'settlement.agent.manage', '代理账单管理', $now) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $created;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function ensureChildMenu(
|
||||||
|
int $parentId,
|
||||||
|
string $code,
|
||||||
|
string $name,
|
||||||
|
Carbon $now,
|
||||||
|
string $activeMenuCode = 'system.agents',
|
||||||
|
): int {
|
||||||
|
$existing = DB::table('admin_menus')->where('code', $code)->value('id');
|
||||||
|
if ($existing !== null) {
|
||||||
|
return (int) $existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) DB::table('admin_menus')->insertGetId([
|
||||||
|
'parent_id' => $parentId,
|
||||||
|
'menu_type' => 'button',
|
||||||
|
'code' => $code,
|
||||||
|
'name' => $name,
|
||||||
|
'path' => null,
|
||||||
|
'route_name' => null,
|
||||||
|
'component' => null,
|
||||||
|
'icon' => null,
|
||||||
|
'active_menu_code' => $activeMenuCode,
|
||||||
|
'sort_order' => 0,
|
||||||
|
'is_visible' => false,
|
||||||
|
'is_cache' => false,
|
||||||
|
'is_external' => false,
|
||||||
|
'status' => 1,
|
||||||
|
'meta_json' => null,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function ensureMenuAction(int $menuId, int $actionId, string $permissionCode, string $name, Carbon $now): bool
|
||||||
|
{
|
||||||
|
if (DB::table('admin_menu_actions')->where('permission_code', $permissionCode)->exists()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('admin_menu_actions')->insert([
|
||||||
|
'menu_id' => $menuId,
|
||||||
|
'action_id' => $actionId,
|
||||||
|
'permission_code' => $permissionCode,
|
||||||
|
'name' => $name,
|
||||||
|
'status' => 1,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,7 +48,7 @@ final class AdminAgentScope
|
|||||||
|
|
||||||
$actor = self::primaryAgentNode($admin);
|
$actor = self::primaryAgentNode($admin);
|
||||||
if ($actor === null) {
|
if ($actor === null) {
|
||||||
return false;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($player->agent_node_id === null) {
|
if ($player->agent_node_id === null) {
|
||||||
|
|||||||
76
app/Support/AdminAgentSettlementScope.php
Normal file
76
app/Support/AdminAgentSettlementScope.php
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use Illuminate\Database\Query\Builder;
|
||||||
|
|
||||||
|
/** 代理账单按管理员可访问站点过滤。 */
|
||||||
|
final class AdminAgentSettlementScope
|
||||||
|
{
|
||||||
|
public static function applyToBillsQuery(Builder $query, AdminUser $admin, string $billsAlias = 'settlement_bills'): void
|
||||||
|
{
|
||||||
|
$siteIds = $admin->accessibleAdminSiteIds();
|
||||||
|
if ($siteIds === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($siteIds === []) {
|
||||||
|
$query->whereRaw('0 = 1');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->whereExists(function (Builder $sub) use ($siteIds, $billsAlias): void {
|
||||||
|
$sub->selectRaw('1')
|
||||||
|
->from('settlement_periods')
|
||||||
|
->whereColumn('settlement_periods.id', $billsAlias.'.settlement_period_id')
|
||||||
|
->whereIn('settlement_periods.admin_site_id', $siteIds);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function periodAccessible(AdminUser $admin, int $settlementPeriodId): bool
|
||||||
|
{
|
||||||
|
$siteIds = $admin->accessibleAdminSiteIds();
|
||||||
|
if ($siteIds === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($siteIds === []) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return \Illuminate\Support\Facades\DB::table('settlement_periods')
|
||||||
|
->where('id', $settlementPeriodId)
|
||||||
|
->whereIn('admin_site_id', $siteIds)
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function siteAccessible(AdminUser $admin, int $adminSiteId): bool
|
||||||
|
{
|
||||||
|
$siteIds = $admin->accessibleAdminSiteIds();
|
||||||
|
if ($siteIds === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array($adminSiteId, $siteIds, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function billAccessible(AdminUser $admin, int $settlementBillId): bool
|
||||||
|
{
|
||||||
|
$siteIds = $admin->accessibleAdminSiteIds();
|
||||||
|
if ($siteIds === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($siteIds === []) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return \Illuminate\Support\Facades\DB::table('settlement_bills')
|
||||||
|
->join('settlement_periods', 'settlement_periods.id', '=', 'settlement_bills.settlement_period_id')
|
||||||
|
->where('settlement_bills.id', $settlementBillId)
|
||||||
|
->whereIn('settlement_periods.admin_site_id', $siteIds)
|
||||||
|
->exists();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,8 +2,10 @@
|
|||||||
|
|
||||||
namespace App\Support;
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\AdminSite;
|
||||||
use App\Models\AdminUser;
|
use App\Models\AdminUser;
|
||||||
use App\Models\AgentNode;
|
use App\Models\AgentNode;
|
||||||
|
use App\Models\AgentProfile;
|
||||||
|
|
||||||
final class AdminAuthProfile
|
final class AdminAuthProfile
|
||||||
{
|
{
|
||||||
@@ -26,10 +28,13 @@ final class AdminAuthProfile
|
|||||||
* agent: ?array{
|
* agent: ?array{
|
||||||
* id: int,
|
* id: int,
|
||||||
* admin_site_id: int,
|
* admin_site_id: int,
|
||||||
|
* site_code: string,
|
||||||
* path: string,
|
* path: string,
|
||||||
* code: string,
|
* code: string,
|
||||||
* name: string,
|
* name: string,
|
||||||
* depth: int
|
* depth: int,
|
||||||
|
* can_create_child_agent: bool,
|
||||||
|
* can_create_player: bool
|
||||||
* },
|
* },
|
||||||
* is_super_admin: bool,
|
* is_super_admin: bool,
|
||||||
* operational_permissions: list<string>,
|
* operational_permissions: list<string>,
|
||||||
@@ -56,7 +61,17 @@ final class AdminAuthProfile
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{id: int, admin_site_id: int, path: string, code: string, name: string, depth: int}|null
|
* @return array{
|
||||||
|
* id: int,
|
||||||
|
* admin_site_id: int,
|
||||||
|
* site_code: string,
|
||||||
|
* path: string,
|
||||||
|
* code: string,
|
||||||
|
* name: string,
|
||||||
|
* depth: int,
|
||||||
|
* can_create_child_agent: bool,
|
||||||
|
* can_create_player: bool
|
||||||
|
* }|null
|
||||||
*/
|
*/
|
||||||
private static function agentContext(AdminUser $admin): ?array
|
private static function agentContext(AdminUser $admin): ?array
|
||||||
{
|
{
|
||||||
@@ -69,13 +84,19 @@ final class AdminAuthProfile
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$siteCode = AdminSite::query()->where('id', (int) $node->admin_site_id)->value('code');
|
||||||
|
$profile = AgentProfile::query()->where('agent_node_id', $node->id)->first();
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => (int) $node->id,
|
'id' => (int) $node->id,
|
||||||
'admin_site_id' => (int) $node->admin_site_id,
|
'admin_site_id' => (int) $node->admin_site_id,
|
||||||
|
'site_code' => is_string($siteCode) && $siteCode !== '' ? $siteCode : '',
|
||||||
'path' => (string) $node->path,
|
'path' => (string) $node->path,
|
||||||
'code' => (string) $node->code,
|
'code' => (string) $node->code,
|
||||||
'name' => (string) $node->name,
|
'name' => (string) $node->name,
|
||||||
'depth' => (int) $node->depth,
|
'depth' => (int) $node->depth,
|
||||||
|
'can_create_child_agent' => $profile === null || $profile->can_create_child_agent,
|
||||||
|
'can_create_player' => $profile === null || $profile->can_create_player,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ final class AdminAuthorizationRegistry
|
|||||||
['slug' => 'prd.agent.role.manage', 'name' => '代理角色·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.role.manage']],
|
['slug' => 'prd.agent.role.manage', 'name' => '代理角色·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.role.manage']],
|
||||||
['slug' => 'prd.agent.user.view', 'name' => '代理账号·查看', 'nav_segment' => 'agents', 'permission_codes' => ['agent.user.view', 'agent.node.view']],
|
['slug' => 'prd.agent.user.view', 'name' => '代理账号·查看', 'nav_segment' => 'agents', 'permission_codes' => ['agent.user.view', 'agent.node.view']],
|
||||||
['slug' => 'prd.agent.user.manage', 'name' => '代理账号·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.user.manage']],
|
['slug' => 'prd.agent.user.manage', 'name' => '代理账号·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.user.manage']],
|
||||||
|
['slug' => 'prd.agent-line.provision', 'name' => '代理线路·开通', 'nav_segment' => 'agents', 'permission_codes' => ['agent.line.provision']],
|
||||||
|
['slug' => 'prd.agent.profile.manage', 'name' => '代理占成授信·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.profile.manage']],
|
||||||
|
['slug' => 'prd.settlement.agent.view', 'name' => '代理账单·查看', 'nav_segment' => 'settlement', 'permission_codes' => ['settlement.agent.view']],
|
||||||
|
['slug' => 'prd.settlement.agent.manage', 'name' => '代理账单·可管理', 'nav_segment' => 'settlement', 'permission_codes' => ['settlement.agent.manage']],
|
||||||
|
|
||||||
['slug' => 'prd.users.manage', 'name' => '用户管理·可管理', 'nav_segment' => 'players', 'permission_codes' => ['service.players.manage']],
|
['slug' => 'prd.users.manage', 'name' => '用户管理·可管理', 'nav_segment' => 'players', 'permission_codes' => ['service.players.manage']],
|
||||||
['slug' => 'prd.users.view_finance', 'name' => '用户管理·财务查看', 'nav_segment' => 'players', 'permission_codes' => ['service.players.view', 'service.wallet.view']],
|
['slug' => 'prd.users.view_finance', 'name' => '用户管理·财务查看', 'nav_segment' => 'players', 'permission_codes' => ['service.players.view', 'service.wallet.view']],
|
||||||
@@ -106,7 +110,7 @@ final class AdminAuthorizationRegistry
|
|||||||
'audit' => '审计日志',
|
'audit' => '审计日志',
|
||||||
'settings' => '系统设置',
|
'settings' => '系统设置',
|
||||||
'integration' => '接入站点',
|
'integration' => '接入站点',
|
||||||
'agents' => '代理管理',
|
'agents' => '代理线路',
|
||||||
];
|
];
|
||||||
|
|
||||||
return array_map(
|
return array_map(
|
||||||
@@ -140,7 +144,7 @@ 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' => 'Agents', 'href' => '/admin/agents', 'nav_group' => 'agent', 'activeMatchPrefix' => '/admin/agents', 'requiredAny' => ['prd.agent.view', 'prd.agent.manage', 'prd.agent.role.view', 'prd.agent.role.manage', 'prd.agent.user.view', 'prd.agent.user.manage']],
|
['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', 'prd.settlement.agent.view', 'prd.settlement.agent.manage'], AdminPermissionLanguage::requiredAnyPrdSlugs('integration-sites'))))],
|
||||||
['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']],
|
||||||
['segment' => 'players', 'label' => 'Players', 'href' => '/admin/players', 'nav_group' => 'operations', 'requiredAny' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.player_freeze.manage']],
|
['segment' => 'players', 'label' => 'Players', 'href' => '/admin/players', 'nav_group' => 'operations', 'requiredAny' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.player_freeze.manage']],
|
||||||
@@ -152,7 +156,6 @@ final class AdminAuthorizationRegistry
|
|||||||
['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' => 'rules', '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' => 'rules', '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' => 'rules', 'platform_only' => true, 'activeMatchPrefix' => '/admin/risk/cap', 'requiredAny' => ['prd.risk_cap.manage', 'prd.risk_cap.view']],
|
||||||
['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' => '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' => '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']],
|
||||||
['segment' => 'admin_roles', 'label' => 'Admin Roles', 'href' => '/admin/admin-roles', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.admin_role.manage']],
|
['segment' => 'admin_roles', 'label' => 'Admin Roles', 'href' => '/admin/admin-roles', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.admin_role.manage']],
|
||||||
@@ -215,7 +218,16 @@ final class AdminAuthorizationRegistry
|
|||||||
'dashboard' => ['prd.dashboard.view'],
|
'dashboard' => ['prd.dashboard.view'],
|
||||||
'admin_users' => ['prd.admin_user.manage'],
|
'admin_users' => ['prd.admin_user.manage'],
|
||||||
'admin_roles' => ['prd.admin_role.manage'],
|
'admin_roles' => ['prd.admin_role.manage'],
|
||||||
'agents' => ['prd.agent.view', 'prd.agent.manage', 'prd.agent.role.view', 'prd.agent.role.manage', 'prd.agent.user.view', 'prd.agent.user.manage'],
|
'agents' => [
|
||||||
|
'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',
|
||||||
|
],
|
||||||
'players' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.player_freeze.manage'],
|
'players' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.player_freeze.manage'],
|
||||||
'currencies' => ['prd.currency.manage'],
|
'currencies' => ['prd.currency.manage'],
|
||||||
'wallet' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage', 'prd.users.view_finance'],
|
'wallet' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage', 'prd.users.view_finance'],
|
||||||
@@ -225,7 +237,13 @@ final class AdminAuthorizationRegistry
|
|||||||
'jackpot' => ['prd.jackpot.manage', 'prd.jackpot.view'],
|
'jackpot' => ['prd.jackpot.manage', 'prd.jackpot.view'],
|
||||||
'risk_cap' => ['prd.risk_cap.manage', 'prd.risk_cap.view'],
|
'risk_cap' => ['prd.risk_cap.manage', 'prd.risk_cap.view'],
|
||||||
'risk' => ['prd.risk.view', 'prd.risk.manage'],
|
'risk' => ['prd.risk.view', 'prd.risk.manage'],
|
||||||
'settlement' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view'],
|
'settlement' => [
|
||||||
|
'prd.payout.manage',
|
||||||
|
'prd.payout.review',
|
||||||
|
'prd.payout.view',
|
||||||
|
'prd.settlement.agent.view',
|
||||||
|
'prd.settlement.agent.manage',
|
||||||
|
],
|
||||||
'reconcile' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs'],
|
'reconcile' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs'],
|
||||||
'reports' => ['prd.report.view', 'prd.report.export'],
|
'reports' => ['prd.report.view', 'prd.report.export'],
|
||||||
'tickets' => ['prd.tickets.view'],
|
'tickets' => ['prd.tickets.view'],
|
||||||
@@ -386,6 +404,8 @@ final class AdminAuthorizationRegistry
|
|||||||
['code' => 'admin.admin-roles.destroy', 'module_code' => 'system', 'name' => '删除角色', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/admin-roles/{admin_role}', 'route_name' => 'api.v1.admin.admin-roles.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.admin_role.manage']],
|
['code' => 'admin.admin-roles.destroy', 'module_code' => 'system', 'name' => '删除角色', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/admin-roles/{admin_role}', 'route_name' => 'api.v1.admin.admin-roles.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.admin_role.manage']],
|
||||||
['code' => 'admin.admin-roles.permissions.sync', 'module_code' => 'system', 'name' => '角色权限同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/admin-roles/{admin_role}/permissions', 'route_name' => 'api.v1.admin.admin-roles.permissions.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.admin_role.manage']],
|
['code' => 'admin.admin-roles.permissions.sync', 'module_code' => 'system', 'name' => '角色权限同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/admin-roles/{admin_role}/permissions', 'route_name' => 'api.v1.admin.admin-roles.permissions.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.admin_role.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-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.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']],
|
||||||
@@ -406,6 +426,12 @@ final class AdminAuthorizationRegistry
|
|||||||
|
|
||||||
['code' => 'admin.agent-delegation-grants.index', 'module_code' => 'agent', 'name' => '代理下放上限查看', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/delegation-grants', 'route_name' => 'api.v1.admin.agent-delegation-grants.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']],
|
['code' => 'admin.agent-delegation-grants.index', 'module_code' => 'agent', 'name' => '代理下放上限查看', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/delegation-grants', 'route_name' => 'api.v1.admin.agent-delegation-grants.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']],
|
||||||
['code' => 'admin.agent-delegation-grants.sync', 'module_code' => 'agent', 'name' => '代理下放上限同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/delegation-grants', 'route_name' => 'api.v1.admin.agent-delegation-grants.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
|
['code' => 'admin.agent-delegation-grants.sync', 'module_code' => 'agent', 'name' => '代理下放上限同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/delegation-grants', 'route_name' => 'api.v1.admin.agent-delegation-grants.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
|
||||||
|
['code' => 'admin.agent-nodes.profile.show', 'module_code' => 'agent', 'name' => '代理占成授信查看', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/profile', 'route_name' => 'api.v1.admin.agent-nodes.profile.show', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.profile.manage', 'agent.node.manage'], 'legacy_permission_slugs' => ['prd.agent.profile.manage', 'prd.agent.manage']],
|
||||||
|
['code' => 'admin.agent-nodes.profile.update', 'module_code' => 'agent', 'name' => '代理占成授信更新', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/profile', 'route_name' => 'api.v1.admin.agent-nodes.profile.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.profile.manage'], 'legacy_permission_slugs' => ['prd.agent.profile.manage']],
|
||||||
|
['code' => 'admin.settlement-periods.store', 'module_code' => 'settlement', 'name' => '创建代理账期', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-periods', 'route_name' => 'api.v1.admin.settlement-periods.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['settlement.agent.manage'], 'legacy_permission_slugs' => ['prd.settlement.agent.manage']],
|
||||||
|
['code' => 'admin.settlement-periods.close', 'module_code' => 'settlement', 'name' => '关闭代理账期', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-periods/{settlement_period}/close', 'route_name' => 'api.v1.admin.settlement-periods.close', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['settlement.agent.manage'], 'legacy_permission_slugs' => ['prd.settlement.agent.manage']],
|
||||||
|
['code' => 'admin.settlement-bills.index', 'module_code' => 'settlement', 'name' => '代理账单列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settlement-bills', 'route_name' => 'api.v1.admin.settlement-bills.index', '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-bills.confirm', 'module_code' => 'settlement', 'name' => '确认代理账单', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/settlement-bills/{settlement_bill}/confirm', 'route_name' => 'api.v1.admin.settlement-bills.confirm', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['settlement.agent.manage'], 'legacy_permission_slugs' => ['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']],
|
||||||
['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.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']],
|
||||||
@@ -435,7 +461,7 @@ final class AdminAuthorizationRegistry
|
|||||||
['code' => 'admin.currencies.update', 'module_code' => 'settings', 'name' => '更新币种', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/currencies/{currency}', 'route_name' => 'api.v1.admin.currencies.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.currency.manage']],
|
['code' => 'admin.currencies.update', 'module_code' => 'settings', 'name' => '更新币种', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/currencies/{currency}', 'route_name' => 'api.v1.admin.currencies.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.currency.manage']],
|
||||||
['code' => 'admin.currencies.destroy', 'module_code' => 'settings', 'name' => '删除币种', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/currencies/{currency}', 'route_name' => 'api.v1.admin.currencies.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.currency.manage']],
|
['code' => 'admin.currencies.destroy', 'module_code' => 'settings', 'name' => '删除币种', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/currencies/{currency}', 'route_name' => 'api.v1.admin.currencies.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.currency.manage']],
|
||||||
|
|
||||||
['code' => 'admin.integration-sites.index', 'module_code' => 'integration', 'name' => '接入站点列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/integration-sites', 'route_name' => 'api.v1.admin.integration-sites.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['integration.site.view', 'integration.site.manage']],
|
['code' => 'admin.integration-sites.index', 'module_code' => 'integration', 'name' => '接入站点列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/integration-sites', 'route_name' => 'api.v1.admin.integration-sites.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['integration.site.view', 'integration.site.manage', 'service.players.manage']],
|
||||||
['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.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.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.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']],
|
||||||
|
|||||||
@@ -51,6 +51,16 @@ final class AdminSiteScope
|
|||||||
return in_array($siteCode, $allowed, true);
|
return in_array($siteCode, $allowed, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static function siteIdAllowed(AdminUser $admin, int $siteId): bool
|
||||||
|
{
|
||||||
|
$siteIds = $admin->accessibleAdminSiteIds();
|
||||||
|
if ($siteIds === null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return in_array($siteId, $siteIds, true);
|
||||||
|
}
|
||||||
|
|
||||||
public static function playerAccessible(AdminUser $admin, Player $player): bool
|
public static function playerAccessible(AdminUser $admin, Player $player): bool
|
||||||
{
|
{
|
||||||
if (! self::siteCodeAllowed($admin, (string) $player->site_code)) {
|
if (! self::siteCodeAllowed($admin, (string) $player->site_code)) {
|
||||||
@@ -79,7 +89,10 @@ final class AdminSiteScope
|
|||||||
}
|
}
|
||||||
|
|
||||||
$query->whereIn('site_code', $codes);
|
$query->whereIn('site_code', $codes);
|
||||||
AdminAgentScope::applyToPlayerQuery($query, $admin);
|
|
||||||
|
if (AdminAgentScope::primaryAgentNode($admin) !== null) {
|
||||||
|
AdminAgentScope::applyToPlayerQuery($query, $admin);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ final class AdminUserApiPresenter
|
|||||||
'nickname' => $user->name,
|
'nickname' => $user->name,
|
||||||
'email' => $user->email,
|
'email' => $user->email,
|
||||||
'status' => (int) $user->status,
|
'status' => (int) $user->status,
|
||||||
|
'account_kind' => $user->isPlatformAccount() ? 'platform' : 'agent',
|
||||||
'roles' => $user->adminRoleSlugs(),
|
'roles' => $user->adminRoleSlugs(),
|
||||||
'direct_permissions' => $user->directLegacyPermissionSlugs(),
|
'direct_permissions' => $user->directLegacyPermissionSlugs(),
|
||||||
'effective_permissions' => $user->adminPermissionSlugs(),
|
'effective_permissions' => $user->adminPermissionSlugs(),
|
||||||
|
|||||||
30
app/Support/AgentLinePresenter.php
Normal file
30
app/Support/AgentLinePresenter.php
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\AdminSite;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
|
||||||
|
final class AgentLinePresenter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array{sso_jwt_secret: string, wallet_api_key: string} $secrets
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public static function provisioned(AdminSite $site, AgentNode $root, array $secrets): array
|
||||||
|
{
|
||||||
|
$sitePayload = AdminIntegrationSitePresenter::withPlainSecretsOnce(
|
||||||
|
AdminIntegrationSitePresenter::detail($site),
|
||||||
|
$secrets,
|
||||||
|
);
|
||||||
|
|
||||||
|
return array_merge($sitePayload, [
|
||||||
|
'agent_node' => AgentNodePresenter::item($root),
|
||||||
|
'line_root' => [
|
||||||
|
'agent_node_id' => (int) $root->id,
|
||||||
|
'site_code' => (string) $site->code,
|
||||||
|
'is_line_root' => true,
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Support;
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\AdminSite;
|
||||||
use App\Models\AgentNode;
|
use App\Models\AgentNode;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
final class AgentNodePresenter
|
final class AgentNodePresenter
|
||||||
{
|
{
|
||||||
@@ -16,14 +18,27 @@ final class AgentNodePresenter
|
|||||||
* code: string,
|
* code: string,
|
||||||
* name: string,
|
* name: string,
|
||||||
* status: int,
|
* status: int,
|
||||||
* is_root: bool
|
* is_root: bool,
|
||||||
|
* username: ?string,
|
||||||
|
* email: ?string
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
public static function item(AgentNode $node): array
|
public static function item(AgentNode $node): array
|
||||||
{
|
{
|
||||||
|
$account = DB::table('admin_user_agents as aua')
|
||||||
|
->join('admin_users as au', 'au.id', '=', 'aua.admin_user_id')
|
||||||
|
->where('aua.agent_node_id', $node->id)
|
||||||
|
->orderByDesc('aua.is_primary')
|
||||||
|
->orderBy('aua.admin_user_id')
|
||||||
|
->select('au.username', 'au.email')
|
||||||
|
->first();
|
||||||
|
|
||||||
|
$siteCode = AdminSite::query()->where('id', $node->admin_site_id)->value('code');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => (int) $node->id,
|
'id' => (int) $node->id,
|
||||||
'admin_site_id' => (int) $node->admin_site_id,
|
'admin_site_id' => (int) $node->admin_site_id,
|
||||||
|
'site_code' => $siteCode !== null ? (string) $siteCode : null,
|
||||||
'parent_id' => $node->parent_id !== null ? (int) $node->parent_id : null,
|
'parent_id' => $node->parent_id !== null ? (int) $node->parent_id : null,
|
||||||
'path' => (string) $node->path,
|
'path' => (string) $node->path,
|
||||||
'depth' => (int) $node->depth,
|
'depth' => (int) $node->depth,
|
||||||
@@ -31,6 +46,9 @@ final class AgentNodePresenter
|
|||||||
'name' => (string) $node->name,
|
'name' => (string) $node->name,
|
||||||
'status' => (int) $node->status,
|
'status' => (int) $node->status,
|
||||||
'is_root' => $node->isRoot(),
|
'is_root' => $node->isRoot(),
|
||||||
|
'is_line_root' => $node->isRoot(),
|
||||||
|
'username' => $account?->username !== null ? (string) $account->username : null,
|
||||||
|
'email' => $account?->email !== null ? (string) $account->email : null,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
25
app/Support/CreditLineMode.php
Normal file
25
app/Support/CreditLineMode.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\AdminSite;
|
||||||
|
use App\Services\LotterySettings;
|
||||||
|
|
||||||
|
final class CreditLineMode
|
||||||
|
{
|
||||||
|
public static function isEnabledForSiteCode(string $siteCode): bool
|
||||||
|
{
|
||||||
|
if ((bool) LotterySettings::get('settlement.credit_line_disable_instant_rebate', false)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$site = AdminSite::query()->where('code', $siteCode)->first(['extra_json']);
|
||||||
|
if ($site === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$extra = is_array($site->extra_json) ? $site->extra_json : [];
|
||||||
|
|
||||||
|
return (bool) ($extra['credit_line_mode'] ?? false);
|
||||||
|
}
|
||||||
|
}
|
||||||
72
app/Support/Settlement/DesignDocExample12.php
Normal file
72
app/Support/Settlement/DesignDocExample12.php
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support\Settlement;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设计文档 §12.1~§12.4 验收常量(平台视角:玩家输为正)。
|
||||||
|
*
|
||||||
|
* @see docs/信用占成盘代理系统设计说明文档.md
|
||||||
|
*/
|
||||||
|
final class DesignDocExample12
|
||||||
|
{
|
||||||
|
public const GAME_WIN_LOSS = 1000;
|
||||||
|
|
||||||
|
public const BASIC_REBATE = 50;
|
||||||
|
|
||||||
|
public const EXTRA_REBATE_BY_C = 20;
|
||||||
|
|
||||||
|
/** 玩家净结算 P→C:1000 - 50 - 20 */
|
||||||
|
public const PLAYER_NET_SETTLEMENT = 930;
|
||||||
|
|
||||||
|
/** 共享净输赢:1000 - 50 */
|
||||||
|
public const SHARED_NET_WIN_LOSS = 950;
|
||||||
|
|
||||||
|
/** 总占成:A=60%, B=40%, C=25% */
|
||||||
|
public const TOTAL_SHARE_A = 60;
|
||||||
|
|
||||||
|
public const TOTAL_SHARE_B = 40;
|
||||||
|
|
||||||
|
public const TOTAL_SHARE_C = 25;
|
||||||
|
|
||||||
|
/** 实际占成收益(共享池) */
|
||||||
|
public const SHARE_PROFIT_C = 237.5;
|
||||||
|
|
||||||
|
public const SHARE_PROFIT_B = 142.5;
|
||||||
|
|
||||||
|
public const SHARE_PROFIT_A = 190.0;
|
||||||
|
|
||||||
|
public const SHARE_PROFIT_PLATFORM = 380.0;
|
||||||
|
|
||||||
|
/** C 最终收益:237.5 - 20 */
|
||||||
|
public const FINAL_PROFIT_C = 217.5;
|
||||||
|
|
||||||
|
/** 逐级结算应付 */
|
||||||
|
public const TIER_P_TO_C = 930.0;
|
||||||
|
|
||||||
|
public const TIER_C_TO_B = 712.5;
|
||||||
|
|
||||||
|
public const TIER_B_TO_A = 570.0;
|
||||||
|
|
||||||
|
public const TIER_A_TO_PLATFORM = 380.0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{actual: array<string, float>, total: float}
|
||||||
|
*/
|
||||||
|
public static function actualShareRates(): array
|
||||||
|
{
|
||||||
|
$c = self::TOTAL_SHARE_C;
|
||||||
|
$b = self::TOTAL_SHARE_B - self::TOTAL_SHARE_C;
|
||||||
|
$a = self::TOTAL_SHARE_A - self::TOTAL_SHARE_B;
|
||||||
|
$platform = 100 - self::TOTAL_SHARE_A;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'actual' => [
|
||||||
|
'C' => $c,
|
||||||
|
'B' => $b,
|
||||||
|
'A' => $a,
|
||||||
|
'platform' => $platform,
|
||||||
|
],
|
||||||
|
'total' => $c + $b + $a + $platform,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$sites = DB::table('admin_sites')->orderBy('id')->get(['id', 'code']);
|
||||||
|
|
||||||
|
foreach ($sites as $site) {
|
||||||
|
$siteId = (int) $site->id;
|
||||||
|
$code = (string) $site->code;
|
||||||
|
$legacyCode = 'root-'.$code;
|
||||||
|
|
||||||
|
$root = DB::table('agent_nodes')
|
||||||
|
->where('admin_site_id', $siteId)
|
||||||
|
->where('depth', 0)
|
||||||
|
->first(['id', 'code']);
|
||||||
|
|
||||||
|
if ($root === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((string) $root->code === $legacyCode) {
|
||||||
|
$conflict = DB::table('agent_nodes')
|
||||||
|
->where('admin_site_id', $siteId)
|
||||||
|
->where('code', $code)
|
||||||
|
->where('id', '!=', (int) $root->id)
|
||||||
|
->exists();
|
||||||
|
if (! $conflict) {
|
||||||
|
DB::table('agent_nodes')->where('id', (int) $root->id)->update([
|
||||||
|
'code' => $code,
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$sites = DB::table('admin_sites')->orderBy('id')->get(['id', 'code']);
|
||||||
|
|
||||||
|
foreach ($sites as $site) {
|
||||||
|
$siteId = (int) $site->id;
|
||||||
|
$code = (string) $site->code;
|
||||||
|
|
||||||
|
DB::table('agent_nodes')
|
||||||
|
->where('admin_site_id', $siteId)
|
||||||
|
->where('depth', 0)
|
||||||
|
->where('code', $code)
|
||||||
|
->update([
|
||||||
|
'code' => 'root-'.$code,
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::create('agent_profiles', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('agent_node_id')->primary()->constrained('agent_nodes')->cascadeOnDelete();
|
||||||
|
$table->decimal('total_share_rate', 5, 2)->default(0)->comment('总占成 0-100');
|
||||||
|
$table->unsignedBigInteger('credit_limit')->default(0);
|
||||||
|
$table->unsignedBigInteger('allocated_credit')->default(0);
|
||||||
|
$table->unsignedBigInteger('used_credit')->default(0);
|
||||||
|
$table->decimal('rebate_limit', 8, 4)->default(0);
|
||||||
|
$table->decimal('default_player_rebate', 8, 4)->default(0);
|
||||||
|
$table->string('settlement_cycle', 16)->default('weekly');
|
||||||
|
$table->boolean('can_grant_extra_rebate')->default(false);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('player_credit_accounts', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('player_id')->primary()->constrained('players')->cascadeOnDelete();
|
||||||
|
$table->unsignedBigInteger('credit_limit')->default(0);
|
||||||
|
$table->unsignedBigInteger('used_credit')->default(0);
|
||||||
|
$table->unsignedBigInteger('frozen_credit')->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('player_rebate_profiles', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('player_id')->constrained('players')->cascadeOnDelete();
|
||||||
|
$table->string('game_type', 32)->default('*');
|
||||||
|
$table->boolean('inherit_from_agent')->default(true);
|
||||||
|
$table->decimal('rebate_rate', 8, 4)->default(0);
|
||||||
|
$table->decimal('extra_rebate_rate', 8, 4)->default(0);
|
||||||
|
$table->timestamps();
|
||||||
|
$table->unique(['player_id', 'game_type']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('settlement_periods', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('admin_site_id')->constrained('admin_sites')->cascadeOnDelete();
|
||||||
|
$table->timestamp('period_start');
|
||||||
|
$table->timestamp('period_end');
|
||||||
|
$table->string('status', 16)->default('open');
|
||||||
|
$table->timestamps();
|
||||||
|
$table->index(['admin_site_id', 'status']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('settlement_bills', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('settlement_period_id')->constrained('settlement_periods')->cascadeOnDelete();
|
||||||
|
$table->string('bill_type', 16);
|
||||||
|
$table->string('owner_type', 16);
|
||||||
|
$table->unsignedBigInteger('owner_id');
|
||||||
|
$table->string('counterparty_type', 16);
|
||||||
|
$table->unsignedBigInteger('counterparty_id');
|
||||||
|
$table->bigInteger('gross_win_loss')->default(0);
|
||||||
|
$table->bigInteger('rebate_amount')->default(0);
|
||||||
|
$table->bigInteger('adjustment_amount')->default(0);
|
||||||
|
$table->bigInteger('net_amount')->default(0);
|
||||||
|
$table->bigInteger('paid_amount')->default(0);
|
||||||
|
$table->bigInteger('unpaid_amount')->default(0);
|
||||||
|
$table->string('status', 16)->default('pending');
|
||||||
|
$table->timestamp('confirmed_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->index(['settlement_period_id', 'bill_type']);
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('rebate_records', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('player_id')->constrained('players')->cascadeOnDelete();
|
||||||
|
$table->foreignId('settlement_period_id')->nullable()->constrained('settlement_periods')->nullOnDelete();
|
||||||
|
$table->string('game_type', 32)->default('*');
|
||||||
|
$table->unsignedBigInteger('valid_bet_amount')->default(0);
|
||||||
|
$table->decimal('rebate_rate', 8, 4)->default(0);
|
||||||
|
$table->unsignedBigInteger('rebate_amount')->default(0);
|
||||||
|
$table->string('rebate_type', 16)->default('basic');
|
||||||
|
$table->foreignId('owner_agent_id')->nullable()->constrained('agent_nodes')->nullOnDelete();
|
||||||
|
$table->string('status', 16)->default('pending');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('rebate_allocations', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('rebate_record_id')->constrained('rebate_records')->cascadeOnDelete();
|
||||||
|
$table->foreignId('settlement_bill_id')->nullable()->constrained('settlement_bills')->nullOnDelete();
|
||||||
|
$table->string('participant_type', 16);
|
||||||
|
$table->unsignedBigInteger('participant_id')->default(0);
|
||||||
|
$table->decimal('actual_share_rate', 5, 2)->default(0);
|
||||||
|
$table->bigInteger('allocated_amount')->default(0);
|
||||||
|
$table->string('allocation_rule', 32)->default('share');
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('payment_records', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('settlement_bill_id')->constrained('settlement_bills')->cascadeOnDelete();
|
||||||
|
$table->string('payer_type', 16);
|
||||||
|
$table->unsignedBigInteger('payer_id');
|
||||||
|
$table->string('payee_type', 16);
|
||||||
|
$table->unsignedBigInteger('payee_id');
|
||||||
|
$table->bigInteger('amount');
|
||||||
|
$table->string('method', 32)->nullable();
|
||||||
|
$table->string('status', 16)->default('pending');
|
||||||
|
$table->foreignId('created_by')->nullable()->constrained('admin_users')->nullOnDelete();
|
||||||
|
$table->foreignId('confirmed_by')->nullable()->constrained('admin_users')->nullOnDelete();
|
||||||
|
$table->timestamp('confirmed_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('credit_ledger', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->string('owner_type', 16);
|
||||||
|
$table->unsignedBigInteger('owner_id');
|
||||||
|
$table->bigInteger('amount');
|
||||||
|
$table->string('reason', 64);
|
||||||
|
$table->string('ref_type', 32)->nullable();
|
||||||
|
$table->unsignedBigInteger('ref_id')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->index(['owner_type', 'owner_id', 'created_at']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('credit_ledger');
|
||||||
|
Schema::dropIfExists('payment_records');
|
||||||
|
Schema::dropIfExists('rebate_allocations');
|
||||||
|
Schema::dropIfExists('rebate_records');
|
||||||
|
Schema::dropIfExists('settlement_bills');
|
||||||
|
Schema::dropIfExists('settlement_periods');
|
||||||
|
Schema::dropIfExists('player_rebate_profiles');
|
||||||
|
Schema::dropIfExists('player_credit_accounts');
|
||||||
|
Schema::dropIfExists('agent_profiles');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Support\AdminAgentLineSettlementPermissionMenuActionSync;
|
||||||
|
use App\Support\AdminAuthorizationRegistry;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理账单 / 线路开通等 API 写入 admin_api_resources,并补齐 settlement.agent.* menu_action。
|
||||||
|
*/
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/** @var list<string> */
|
||||||
|
private const RESOURCE_CODE_PREFIXES = [
|
||||||
|
'admin.settlement-bills.',
|
||||||
|
'admin.settlement-periods.',
|
||||||
|
'admin.agent-lines.',
|
||||||
|
'admin.agent-nodes.profile.',
|
||||||
|
];
|
||||||
|
|
||||||
|
/** @var list<string> */
|
||||||
|
private const MENU_ACTION_CODES = [
|
||||||
|
'settlement.agent.view',
|
||||||
|
'settlement.agent.manage',
|
||||||
|
'agent.line.provision',
|
||||||
|
'agent.profile.manage',
|
||||||
|
];
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
AdminAgentLineSettlementPermissionMenuActionSync::syncMissing();
|
||||||
|
|
||||||
|
$now = Carbon::now();
|
||||||
|
$menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code');
|
||||||
|
|
||||||
|
$resources = array_values(array_filter(
|
||||||
|
AdminAuthorizationRegistry::resources(),
|
||||||
|
static fn (array $resource): bool => self::matchesResourceCode((string) $resource['code']),
|
||||||
|
));
|
||||||
|
|
||||||
|
foreach ($resources as $resource) {
|
||||||
|
$resourceId = DB::table('admin_api_resources')
|
||||||
|
->where('code', $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' => $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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->grantSuperAdminMenuActions();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
foreach (AdminAuthorizationRegistry::resources() as $resource) {
|
||||||
|
if (! self::matchesResourceCode((string) $resource['code'])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$resourceId = DB::table('admin_api_resources')->where('code', $resource['code'])->value('id');
|
||||||
|
if ($resourceId === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('admin_api_resource_bindings')->where('api_resource_id', (int) $resourceId)->delete();
|
||||||
|
DB::table('admin_api_resources')->where('id', (int) $resourceId)->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function matchesResourceCode(string $code): bool
|
||||||
|
{
|
||||||
|
foreach (self::RESOURCE_CODE_PREFIXES as $prefix) {
|
||||||
|
if (str_starts_with($code, $prefix)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function grantSuperAdminMenuActions(): void
|
||||||
|
{
|
||||||
|
$superRoleId = DB::table('admin_roles')->where('slug', 'super_admin')->value('id');
|
||||||
|
if ($superRoleId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$menuActionIds = DB::table('admin_menu_actions')
|
||||||
|
->whereIn('permission_code', self::MENU_ACTION_CODES)
|
||||||
|
->pluck('id');
|
||||||
|
|
||||||
|
foreach ($menuActionIds as $menuActionId) {
|
||||||
|
DB::table('admin_role_menu_actions')->updateOrInsert([
|
||||||
|
'role_id' => (int) $superRoleId,
|
||||||
|
'menu_action_id' => (int) $menuActionId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasTable('admin_role_legacy_permissions')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (['prd.settlement.agent.view', 'prd.settlement.agent.manage', 'prd.agent-line.provision', 'prd.agent.profile.manage'] as $slug) {
|
||||||
|
DB::table('admin_role_legacy_permissions')->updateOrInsert([
|
||||||
|
'role_id' => (int) $superRoleId,
|
||||||
|
'permission_slug' => $slug,
|
||||||
|
], []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
Schema::table('agent_profiles', function (Blueprint $table): void {
|
||||||
|
$table->boolean('can_create_child_agent')->default(false)->after('can_grant_extra_rebate');
|
||||||
|
$table->boolean('can_create_player')->default(true)->after('can_create_child_agent');
|
||||||
|
});
|
||||||
|
|
||||||
|
\Illuminate\Support\Facades\DB::table('agent_profiles')->update([
|
||||||
|
'can_create_child_agent' => true,
|
||||||
|
'can_create_player' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$nodeService = app(\App\Services\Agent\AgentNodeService::class);
|
||||||
|
\App\Models\AgentNode::query()->each(static function (\App\Models\AgentNode $node) use ($nodeService): void {
|
||||||
|
$nodeService->syncPrimaryOwnerRoleFromProfile($node);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('agent_profiles', function (Blueprint $table): void {
|
||||||
|
$table->dropColumn(['can_create_child_agent', 'can_create_player']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -13,6 +13,9 @@ return [
|
|||||||
'site_update_denied' => 'You cannot modify this site.',
|
'site_update_denied' => 'You cannot modify this site.',
|
||||||
'site_player_access_denied' => 'You do not have access to players under this site.',
|
'site_player_access_denied' => 'You do not have access to players under this site.',
|
||||||
'player_create_site_forbidden' => 'You cannot create players under this site.',
|
'player_create_site_forbidden' => 'You cannot create players under this site.',
|
||||||
|
'player_create_agent_required' => 'A player must belong to an agent node. Choose a valid site with an agent root, or sign in with an agent-bound account.',
|
||||||
|
'player_create_agent_forbidden' => 'You cannot assign the player to this agent node.',
|
||||||
|
'player_create_capability_forbidden' => 'This agent account is not allowed to create players. Ask your upline to enable the capability.',
|
||||||
'player_already_registered' => 'This main-site player is already registered.',
|
'player_already_registered' => 'This main-site player is already registered.',
|
||||||
'player_wallet_balance_blocks_delete' => 'Player wallet still has balance. Clear it before deletion.',
|
'player_wallet_balance_blocks_delete' => 'Player wallet still has balance. Clear it before deletion.',
|
||||||
'player_has_tickets_blocks_delete' => 'Player has ticket records and cannot be deleted.',
|
'player_has_tickets_blocks_delete' => 'Player has ticket records and cannot be deleted.',
|
||||||
@@ -21,10 +24,14 @@ return [
|
|||||||
'role_has_users_cannot_delete' => 'This role still has assigned admins and cannot be deleted.',
|
'role_has_users_cannot_delete' => 'This role still has assigned admins and cannot be deleted.',
|
||||||
'agent_root_delete_denied' => 'Root agent nodes cannot be deleted.',
|
'agent_root_delete_denied' => 'Root agent nodes cannot be deleted.',
|
||||||
'agent_node_has_children_cannot_delete' => 'This agent node has child nodes. Delete children first.',
|
'agent_node_has_children_cannot_delete' => 'This agent node has child nodes. Delete children first.',
|
||||||
|
'agent_node_has_players_cannot_delete' => 'This agent node still has players. Reassign or remove them before deleting the agent.',
|
||||||
'agent_node_has_users_cannot_delete' => 'This agent node still has bound admin accounts and cannot be deleted.',
|
'agent_node_has_users_cannot_delete' => 'This agent node still has bound admin accounts and cannot be deleted.',
|
||||||
'agent_node_has_roles_cannot_delete' => 'This agent node still has bound roles and cannot be deleted.',
|
'agent_node_has_roles_cannot_delete' => 'This agent node still has bound roles and cannot be deleted.',
|
||||||
'agent_role_in_use' => 'This role is still assigned to :count account(s). Unbind them in Accounts before deleting.',
|
'agent_role_in_use' => 'This role is still assigned to :count account(s). Unbind them in Accounts before deleting.',
|
||||||
'agent_role_read_only' => 'Read-only template roles cannot be changed or deleted.',
|
'agent_role_read_only' => 'Read-only template roles cannot be changed or deleted.',
|
||||||
|
'agent_account_managed_in_agents' => 'Agent accounts must be managed in Agent Operations, not in the platform accounts page.',
|
||||||
|
'agent_role_managed_in_agents' => 'Agent roles must be managed in Agent Operations, not in the platform roles page.',
|
||||||
|
'system_roles_only' => 'Platform accounts can only be assigned platform roles.',
|
||||||
'user_cannot_delete_self' => 'Cannot delete your own account.',
|
'user_cannot_delete_self' => 'Cannot delete your own account.',
|
||||||
'user_cannot_delete_last_super_admin' => 'Cannot delete the last super admin.',
|
'user_cannot_delete_last_super_admin' => 'Cannot delete the last super admin.',
|
||||||
'super_admin_only_for_roles' => 'Only super admins can manage roles.',
|
'super_admin_only_for_roles' => 'Only super admins can manage roles.',
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ return [
|
|||||||
'site_update_denied' => 'यो साइट सम्पादन गर्न मिल्दैन।',
|
'site_update_denied' => 'यो साइट सम्पादन गर्न मिल्दैन।',
|
||||||
'site_player_access_denied' => 'यो साइटका खेलाडीहरूमा पहुँच छैन।',
|
'site_player_access_denied' => 'यो साइटका खेलाडीहरूमा पहुँच छैन।',
|
||||||
'player_create_site_forbidden' => 'यो साइटमा खेलाडी सिर्जना गर्न मिल्दैन।',
|
'player_create_site_forbidden' => 'यो साइटमा खेलाडी सिर्जना गर्न मिल्दैन।',
|
||||||
|
'player_create_agent_required' => 'खेलाडी एजेन्ट नोडमा हुनुपर्छ: मान्य साइट (एजेन्ट रुट सहित) छान्नुहोस्, वा एजेन्ट खाताबाट साइन इन गर्नुहोस्।',
|
||||||
|
'player_create_agent_forbidden' => 'यो एजेन्ट नोडमा खेलाडी तोक्न मिल्दैन।',
|
||||||
|
'player_create_capability_forbidden' => 'यो एजेन्ट खातालाई खेलाडी सिर्जना अनुमति छैन। माथिल्लो एजेन्टलाई सम्पर्क गर्नुहोस्।',
|
||||||
'player_already_registered' => 'यो मुख्य साइट खेलाडी पहिले नै दर्ता भइसकेको छ।',
|
'player_already_registered' => 'यो मुख्य साइट खेलाडी पहिले नै दर्ता भइसकेको छ।',
|
||||||
'player_wallet_balance_blocks_delete' => 'खेलाडी वालेटमा ब्यालेन्स छ, मेटाउनु अघि खाली गर्नुहोस्।',
|
'player_wallet_balance_blocks_delete' => 'खेलाडी वालेटमा ब्यालेन्स छ, मेटाउनु अघि खाली गर्नुहोस्।',
|
||||||
'player_has_tickets_blocks_delete' => 'खेलाडीसँग टिकट रेकर्ड छ, मेटाउन मिल्दैन।',
|
'player_has_tickets_blocks_delete' => 'खेलाडीसँग टिकट रेकर्ड छ, मेटाउन मिल्दैन।',
|
||||||
@@ -21,10 +24,14 @@ return [
|
|||||||
'role_has_users_cannot_delete' => 'यो भूमिकामा अझै एडमिन छ, मेटाउन मिल्दैन।',
|
'role_has_users_cannot_delete' => 'यो भूमिकामा अझै एडमिन छ, मेटाउन मिल्दैन।',
|
||||||
'agent_root_delete_denied' => 'रुट एजेन्ट नोड मेटाउन मिल्दैन।',
|
'agent_root_delete_denied' => 'रुट एजेन्ट नोड मेटाउन मिल्दैन।',
|
||||||
'agent_node_has_children_cannot_delete' => 'यस एजेन्ट नोडमा चाइल्ड नोडहरू छन्, पहिले तिनीहरू हटाउनुहोस्।',
|
'agent_node_has_children_cannot_delete' => 'यस एजेन्ट नोडमा चाइल्ड नोडहरू छन्, पहिले तिनीहरू हटाउनुहोस्।',
|
||||||
|
'agent_node_has_players_cannot_delete' => 'यस एजेन्ट नोडमा अझै प्लेयरहरू छन्। मेटाउनु अघि तिनीहरू सार्नुहोस् वा हटाउनुहोस्।',
|
||||||
'agent_node_has_users_cannot_delete' => 'यस एजेन्ट नोडमा अझै एडमिन खाता जोडिएको छ, मेटाउन मिल्दैन।',
|
'agent_node_has_users_cannot_delete' => 'यस एजेन्ट नोडमा अझै एडमिन खाता जोडिएको छ, मेटाउन मिल्दैन।',
|
||||||
'agent_node_has_roles_cannot_delete' => 'यस एजेन्ट नोडमा अझै भूमिका जोडिएको छ, मेटाउन मिल्दैन।',
|
'agent_node_has_roles_cannot_delete' => 'यस एजेन्ट नोडमा अझै भूमिका जोडिएको छ, मेटाउन मिल्दैन।',
|
||||||
'agent_role_in_use' => 'यो भूमिका अझै :count खातामा प्रयोगमा छ। पहिले खाता ट्याबमा हटाउनुहोस्।',
|
'agent_role_in_use' => 'यो भूमिका अझै :count खातामा प्रयोगमा छ। पहिले खाता ट्याबमा हटाउनुहोस्।',
|
||||||
'agent_role_read_only' => 'Read-only टेम्प्लेट भूमिका मेटाउन वा सम्पादन गर्न मिल्दैन।',
|
'agent_role_read_only' => 'Read-only टेम्प्लेट भूमिका मेटाउन वा सम्पादन गर्न मिल्दैन।',
|
||||||
|
'agent_account_managed_in_agents' => 'एजेन्ट खाता प्लेटफर्म खाता पृष्ठबाट होइन, एजेन्ट अपरेसनबाट व्यवस्थापन गर्नुपर्छ।',
|
||||||
|
'agent_role_managed_in_agents' => 'एजेन्ट भूमिका प्लेटफर्म भूमिका पृष्ठबाट होइन, एजेन्ट अपरेसनबाट व्यवस्थापन गर्नुपर्छ।',
|
||||||
|
'system_roles_only' => 'प्लेटफर्म खातामा प्लेटफर्म भूमिका मात्र बाँड्न सकिन्छ।',
|
||||||
'user_cannot_delete_self' => 'आफ्नै खाता मेटाउन मिल्दैन।',
|
'user_cannot_delete_self' => 'आफ्नै खाता मेटाउन मिल्दैन।',
|
||||||
'user_cannot_delete_last_super_admin' => 'अन्तिम सुपर एडमिन मेटाउन मिल्दैन।',
|
'user_cannot_delete_last_super_admin' => 'अन्तिम सुपर एडमिन मेटाउन मिल्दैन।',
|
||||||
'super_admin_only_for_roles' => 'भूमिका व्यवस्थापन केवल सुपर एडमिनले गर्न सक्छ।',
|
'super_admin_only_for_roles' => 'भूमिका व्यवस्थापन केवल सुपर एडमिनले गर्न सक्छ।',
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ return [
|
|||||||
'site_rotate_denied' => '无权操作该站点。',
|
'site_rotate_denied' => '无权操作该站点。',
|
||||||
'site_update_denied' => '无权修改该站点。',
|
'site_update_denied' => '无权修改该站点。',
|
||||||
'site_player_access_denied' => '无权访问该站点下的玩家。',
|
'site_player_access_denied' => '无权访问该站点下的玩家。',
|
||||||
|
'integration_site_store_deprecated' => '请使用「开通代理线路」创建新站点,不再支持单独创建接入站点。',
|
||||||
'player_create_site_forbidden' => '无权在该站点下创建玩家。',
|
'player_create_site_forbidden' => '无权在该站点下创建玩家。',
|
||||||
|
'player_create_agent_required' => '创建玩家须归属代理节点:请选择有效主站(须已配置代理根节点),或由代理账号操作。',
|
||||||
|
'player_create_agent_forbidden' => '无权将玩家归属到该代理节点。',
|
||||||
|
'player_create_capability_forbidden' => '当前代理账号未开通「创建玩家」能力,请联系上级代理调整权限。',
|
||||||
'player_already_registered' => '该主站玩家已在彩票平台注册。',
|
'player_already_registered' => '该主站玩家已在彩票平台注册。',
|
||||||
'player_wallet_balance_blocks_delete' => '该玩家钱包仍有余额,请先清空后再删除。',
|
'player_wallet_balance_blocks_delete' => '该玩家钱包仍有余额,请先清空后再删除。',
|
||||||
'player_has_tickets_blocks_delete' => '该玩家存在注单记录,无法删除。',
|
'player_has_tickets_blocks_delete' => '该玩家存在注单记录,无法删除。',
|
||||||
@@ -21,10 +25,14 @@ return [
|
|||||||
'role_has_users_cannot_delete' => '该角色下仍有关联管理员,不能删除。',
|
'role_has_users_cannot_delete' => '该角色下仍有关联管理员,不能删除。',
|
||||||
'agent_root_delete_denied' => '根节点不允许删除。',
|
'agent_root_delete_denied' => '根节点不允许删除。',
|
||||||
'agent_node_has_children_cannot_delete' => '该代理节点存在下级代理,请先清空下级后再删除。',
|
'agent_node_has_children_cannot_delete' => '该代理节点存在下级代理,请先清空下级后再删除。',
|
||||||
|
'agent_node_has_players_cannot_delete' => '该代理节点下仍有玩家,请先转移或删除玩家后再删除代理。',
|
||||||
'agent_node_has_users_cannot_delete' => '该代理节点下仍有关联账号,不能删除。',
|
'agent_node_has_users_cannot_delete' => '该代理节点下仍有关联账号,不能删除。',
|
||||||
'agent_node_has_roles_cannot_delete' => '该代理节点下仍有关联角色,不能删除。',
|
'agent_node_has_roles_cannot_delete' => '该代理节点下仍有关联角色,不能删除。',
|
||||||
'agent_role_in_use' => '该角色仍有 :count 个账号在使用,请先在「账号」里解除绑定后再删除。',
|
'agent_role_in_use' => '该角色仍有 :count 个账号在使用,请先在「账号」里解除绑定后再删除。',
|
||||||
'agent_role_read_only' => '只读模板角色不可删除或修改。',
|
'agent_role_read_only' => '只读模板角色不可删除或修改。',
|
||||||
|
'agent_account_managed_in_agents' => '代理账号请到「代理经营」中管理,平台账号页不再支持此操作。',
|
||||||
|
'agent_role_managed_in_agents' => '代理角色请到「代理经营」中管理,平台角色页不再支持此操作。',
|
||||||
|
'system_roles_only' => '平台账号只能分配平台角色。',
|
||||||
'user_cannot_delete_self' => '不能删除当前登录账号。',
|
'user_cannot_delete_self' => '不能删除当前登录账号。',
|
||||||
'user_cannot_delete_last_super_admin' => '不能删除最后一个超级管理员。',
|
'user_cannot_delete_last_super_admin' => '不能删除最后一个超级管理员。',
|
||||||
'super_admin_only_for_roles' => '仅超级管理员可管理角色。',
|
'super_admin_only_for_roles' => '仅超级管理员可管理角色。',
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ Route::prefix('v1')->group(function (): void {
|
|||||||
require __DIR__.'/api/v1/admin/config.php';
|
require __DIR__.'/api/v1/admin/config.php';
|
||||||
require __DIR__.'/api/v1/admin/user.php';
|
require __DIR__.'/api/v1/admin/user.php';
|
||||||
require __DIR__.'/api/v1/admin/agent.php';
|
require __DIR__.'/api/v1/admin/agent.php';
|
||||||
|
require __DIR__.'/api/v1/admin/agent-settlement.php';
|
||||||
require __DIR__.'/api/v1/admin/report.php';
|
require __DIR__.'/api/v1/admin/report.php';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
19
routes/api/v1/admin/agent-settlement.php
Normal file
19
routes/api/v1/admin/agent-settlement.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\AgentSettlement\AgentSettlementBillConfirmController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\AgentSettlement\AgentSettlementBillIndexController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\AgentSettlement\AgentSettlementPeriodCloseController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\AgentSettlement\AgentSettlementPeriodStoreController;
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
|
||||||
|
Route::middleware('admin.api-resource')
|
||||||
|
->group(function (): void {
|
||||||
|
Route::post('settlement-periods', AgentSettlementPeriodStoreController::class)
|
||||||
|
->name('api.v1.admin.settlement-periods.store');
|
||||||
|
Route::post('settlement-periods/{settlement_period}/close', AgentSettlementPeriodCloseController::class)
|
||||||
|
->name('api.v1.admin.settlement-periods.close');
|
||||||
|
Route::get('settlement-bills', AgentSettlementBillIndexController::class)
|
||||||
|
->name('api.v1.admin.settlement-bills.index');
|
||||||
|
Route::post('settlement-bills/{settlement_bill}/confirm', AgentSettlementBillConfirmController::class)
|
||||||
|
->name('api.v1.admin.settlement-bills.confirm');
|
||||||
|
});
|
||||||
@@ -18,9 +18,16 @@ use App\Http\Controllers\Api\V1\Admin\Agent\AgentAdminUserRoleSyncController;
|
|||||||
use App\Http\Controllers\Api\V1\Admin\Agent\AgentAdminUserDestroyController;
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentAdminUserDestroyController;
|
||||||
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeDelegationGrantIndexController;
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeDelegationGrantIndexController;
|
||||||
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeDelegationGrantSyncController;
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeDelegationGrantSyncController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentLineStoreController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentLineShowController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeProfileController;
|
||||||
|
|
||||||
Route::middleware('admin.api-resource')
|
Route::middleware('admin.api-resource')
|
||||||
->group(function (): void {
|
->group(function (): void {
|
||||||
|
Route::post('agent-lines', AgentLineStoreController::class)
|
||||||
|
->name('api.v1.admin.agent-lines.store');
|
||||||
|
Route::get('agent-lines/{admin_site}', AgentLineShowController::class)
|
||||||
|
->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::post('agent-nodes', AgentNodeStoreController::class)
|
Route::post('agent-nodes', AgentNodeStoreController::class)
|
||||||
@@ -37,6 +44,10 @@ Route::middleware('admin.api-resource')
|
|||||||
->name('api.v1.admin.agent-delegation-grants.index');
|
->name('api.v1.admin.agent-delegation-grants.index');
|
||||||
Route::put('agent-nodes/{agent_node}/delegation-grants', AgentNodeDelegationGrantSyncController::class)
|
Route::put('agent-nodes/{agent_node}/delegation-grants', AgentNodeDelegationGrantSyncController::class)
|
||||||
->name('api.v1.admin.agent-delegation-grants.sync');
|
->name('api.v1.admin.agent-delegation-grants.sync');
|
||||||
|
Route::get('agent-nodes/{agent_node}/profile', [AgentNodeProfileController::class, 'show'])
|
||||||
|
->name('api.v1.admin.agent-nodes.profile.show');
|
||||||
|
Route::put('agent-nodes/{agent_node}/profile', [AgentNodeProfileController::class, 'update'])
|
||||||
|
->name('api.v1.admin.agent-nodes.profile.update');
|
||||||
Route::get('agent-nodes/{agent_node}', AgentNodeShowController::class)
|
Route::get('agent-nodes/{agent_node}', AgentNodeShowController::class)
|
||||||
->name('api.v1.admin.agent-nodes.show');
|
->name('api.v1.admin.agent-nodes.show');
|
||||||
Route::put('agent-nodes/{agent_node}', AgentNodeUpdateController::class)
|
Route::put('agent-nodes/{agent_node}', AgentNodeUpdateController::class)
|
||||||
|
|||||||
105
tests/Feature/AdminAgentLineApiTest.php
Normal file
105
tests/Feature/AdminAgentLineApiTest.php
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
beforeEach(function (): void {
|
||||||
|
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('super admin can provision agent line with aligned root code', function (): void {
|
||||||
|
$admin = AdminUser::query()->create([
|
||||||
|
'username' => 'line_super',
|
||||||
|
'name' => 'Line Super',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($admin);
|
||||||
|
|
||||||
|
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
|
||||||
|
$response = $this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->postJson('/api/v1/admin/agent-lines', [
|
||||||
|
'code' => 'line-alpha',
|
||||||
|
'name' => 'Line Alpha',
|
||||||
|
'username' => 'line_alpha_owner',
|
||||||
|
'password' => 'secret-strong',
|
||||||
|
'currency_code' => 'NPR',
|
||||||
|
'status' => 1,
|
||||||
|
])
|
||||||
|
->assertCreated()
|
||||||
|
->assertJsonPath('data.code', 'line-alpha')
|
||||||
|
->assertJsonPath('data.agent_node.code', 'line-alpha')
|
||||||
|
->assertJsonPath('data.line_root.site_code', 'line-alpha')
|
||||||
|
->assertJsonPath('data.secrets.sso_jwt_secret', fn ($v) => is_string($v) && $v !== '')
|
||||||
|
->assertJsonPath('data.secrets.wallet_api_key', fn ($v) => is_string($v) && $v !== '');
|
||||||
|
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('code', 'line-alpha')->value('id');
|
||||||
|
expect($siteId)->toBeGreaterThan(0);
|
||||||
|
|
||||||
|
$root = DB::table('agent_nodes')
|
||||||
|
->where('admin_site_id', $siteId)
|
||||||
|
->where('depth', 0)
|
||||||
|
->first();
|
||||||
|
|
||||||
|
expect($root)->not->toBeNull();
|
||||||
|
expect((string) $root->code)->toBe('line-alpha');
|
||||||
|
|
||||||
|
expect(
|
||||||
|
DB::table('admin_user_agents')->where('agent_node_id', (int) $root->id)->count()
|
||||||
|
)->toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('non super admin cannot create integration site directly', function (): void {
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
|
$admin = AdminUser::query()->create([
|
||||||
|
'username' => 'line_ops',
|
||||||
|
'name' => 'Ops',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$roleId = DB::table('admin_roles')->insertGetId([
|
||||||
|
'slug' => 'integration_ops',
|
||||||
|
'code' => 'integration_ops',
|
||||||
|
'name' => 'Integration Ops',
|
||||||
|
'description' => null,
|
||||||
|
'status' => 1,
|
||||||
|
'is_system' => false,
|
||||||
|
'sort_order' => 0,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$actionId = DB::table('admin_menu_actions')
|
||||||
|
->where('permission_code', 'integration.site.manage')
|
||||||
|
->value('id');
|
||||||
|
if ($actionId !== null) {
|
||||||
|
DB::table('admin_role_menu_actions')->insert([
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'menu_action_id' => (int) $actionId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('admin_user_site_roles')->insert([
|
||||||
|
'admin_user_id' => $admin->id,
|
||||||
|
'site_id' => $siteId,
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'granted_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->postJson('/api/v1/admin/integration-sites', [
|
||||||
|
'code' => 'blocked-site',
|
||||||
|
'name' => 'Blocked',
|
||||||
|
])
|
||||||
|
->assertForbidden();
|
||||||
|
});
|
||||||
@@ -168,16 +168,16 @@ test('agent operator can create child under own node but not under sibling', fun
|
|||||||
]);
|
]);
|
||||||
grantSuperAdminRole($super);
|
grantSuperAdminRole($super);
|
||||||
|
|
||||||
$nodeA = $service->createChild($super, [
|
$nodeA = $service->createChild($super, agentChildPayload([
|
||||||
'parent_id' => $rootId,
|
'parent_id' => $rootId,
|
||||||
'code' => 'branch-a2',
|
'code' => 'branch-a2',
|
||||||
'name' => 'Branch A',
|
'name' => 'Branch A',
|
||||||
]);
|
]));
|
||||||
$nodeB = $service->createChild($super, [
|
$nodeB = $service->createChild($super, agentChildPayload([
|
||||||
'parent_id' => $rootId,
|
'parent_id' => $rootId,
|
||||||
'code' => 'branch-b2',
|
'code' => 'branch-b2',
|
||||||
'name' => 'Branch B',
|
'name' => 'Branch B',
|
||||||
]);
|
]));
|
||||||
|
|
||||||
$operator = AdminUser::query()->create([
|
$operator = AdminUser::query()->create([
|
||||||
'username' => 'agent_a2_ops',
|
'username' => 'agent_a2_ops',
|
||||||
@@ -194,6 +194,7 @@ test('agent operator can create child under own node but not under sibling', fun
|
|||||||
'parent_id' => $nodeA->id,
|
'parent_id' => $nodeA->id,
|
||||||
'code' => 'a-child',
|
'code' => 'a-child',
|
||||||
'name' => 'A Child',
|
'name' => 'A Child',
|
||||||
|
'password' => agentNodeTestPassword(),
|
||||||
])
|
])
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertJsonPath('data.code', 'a-child');
|
->assertJsonPath('data.code', 'a-child');
|
||||||
@@ -203,6 +204,7 @@ test('agent operator can create child under own node but not under sibling', fun
|
|||||||
'parent_id' => $nodeB->id,
|
'parent_id' => $nodeB->id,
|
||||||
'code' => 'hack-child',
|
'code' => 'hack-child',
|
||||||
'name' => 'Hack',
|
'name' => 'Hack',
|
||||||
|
'password' => agentNodeTestPassword(),
|
||||||
])
|
])
|
||||||
->assertForbidden();
|
->assertForbidden();
|
||||||
});
|
});
|
||||||
|
|||||||
33
tests/Feature/AdminAgentSettlementBillApiTest.php
Normal file
33
tests/Feature/AdminAgentSettlementBillApiTest.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
test('settlement bills index api resource is configured after migrations', function (): void {
|
||||||
|
expect(
|
||||||
|
DB::table('admin_api_resources')
|
||||||
|
->where('route_name', 'api.v1.admin.settlement-bills.index')
|
||||||
|
->where('status', 1)
|
||||||
|
->exists(),
|
||||||
|
)->toBeTrue();
|
||||||
|
|
||||||
|
$admin = AdminUser::query()->create([
|
||||||
|
'username' => 'bill_super',
|
||||||
|
'name' => 'Bill Super',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($admin);
|
||||||
|
|
||||||
|
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->getJson('/api/v1/admin/settlement-bills')
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.items', fn ($items) => is_array($items));
|
||||||
|
});
|
||||||
@@ -63,6 +63,47 @@ function playerPermissionRequest($test, string $token)
|
|||||||
return $test->withHeader('Authorization', 'Bearer '.$token);
|
return $test->withHeader('Authorization', 'Bearer '.$token);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
test('super admin can create player under site root agent', function (): void {
|
||||||
|
$siteCode = DB::table('admin_sites')->where('is_default', true)->value('code');
|
||||||
|
$siteCode = is_string($siteCode) && $siteCode !== '' ? $siteCode : 'default_site';
|
||||||
|
|
||||||
|
$token = playerManageAdminToken();
|
||||||
|
|
||||||
|
$sitePlayerId = 'manual-create-'.uniqid('', true);
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->postJson('/api/v1/admin/players', [
|
||||||
|
'site_code' => $siteCode,
|
||||||
|
'site_player_id' => $sitePlayerId,
|
||||||
|
'default_currency' => 'NPR',
|
||||||
|
'status' => 0,
|
||||||
|
])
|
||||||
|
->assertCreated()
|
||||||
|
->assertJsonPath('data.site_code', $siteCode)
|
||||||
|
->assertJsonPath('data.site_player_id', $sitePlayerId);
|
||||||
|
|
||||||
|
$this->assertDatabaseHas('players', [
|
||||||
|
'site_code' => $siteCode,
|
||||||
|
'site_player_id' => $sitePlayerId,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('platform user without agent binding cannot create player', function (): void {
|
||||||
|
$siteCode = DB::table('admin_sites')->where('is_default', true)->value('code');
|
||||||
|
$siteCode = is_string($siteCode) && $siteCode !== '' ? $siteCode : 'default_site';
|
||||||
|
|
||||||
|
$token = playerPermissionAdminToken('player_manager_no_agent', ['prd.users.manage']);
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->postJson('/api/v1/admin/players', [
|
||||||
|
'site_code' => $siteCode,
|
||||||
|
'site_player_id' => 'blocked-create',
|
||||||
|
'default_currency' => 'NPR',
|
||||||
|
])
|
||||||
|
->assertStatus(422)
|
||||||
|
->assertJsonPath('msg', fn (string $msg): bool => str_contains($msg, '代理') || str_contains($msg, 'agent'));
|
||||||
|
});
|
||||||
|
|
||||||
test('admin can freeze and unfreeze player with audit log', function (): void {
|
test('admin can freeze and unfreeze player with audit log', function (): void {
|
||||||
$siteCode = DB::table('admin_sites')->where('is_default', true)->value('code');
|
$siteCode = DB::table('admin_sites')->where('is_default', true)->value('code');
|
||||||
$siteCode = is_string($siteCode) && $siteCode !== '' ? $siteCode : 'default_site';
|
$siteCode = is_string($siteCode) && $siteCode !== '' ? $siteCode : 'default_site';
|
||||||
|
|||||||
@@ -2,10 +2,13 @@
|
|||||||
|
|
||||||
use App\Models\AdminRole;
|
use App\Models\AdminRole;
|
||||||
use App\Models\AdminUser;
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
use App\Lottery\ErrorCode;
|
use App\Lottery\ErrorCode;
|
||||||
use Illuminate\Support\Facades\DB;
|
use Illuminate\Support\Facades\DB;
|
||||||
use Illuminate\Support\Facades\Hash;
|
use Illuminate\Support\Facades\Hash;
|
||||||
use App\Support\AdminPermissionBridge;
|
use App\Support\AdminPermissionBridge;
|
||||||
|
use App\Services\Agent\AgentNodeService;
|
||||||
|
use App\Services\Agent\AgentAdminUserService;
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
@@ -47,7 +50,7 @@ test('admin user permission apis require rbac permission', function (): void {
|
|||||||
->assertJsonPath('code', ErrorCode::AdminForbidden->value);
|
->assertJsonPath('code', ErrorCode::AdminForbidden->value);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('admin can list users and sync direct permissions', function (): void {
|
test('admin can list platform users and read effective permissions', function (): void {
|
||||||
$token = makeAdminWithPermissions('rbac_manager', ['prd.admin_user.manage']);
|
$token = makeAdminWithPermissions('rbac_manager', ['prd.admin_user.manage']);
|
||||||
|
|
||||||
$target = AdminUser::query()->create([
|
$target = AdminUser::query()->create([
|
||||||
@@ -93,23 +96,12 @@ test('admin can list users and sync direct permissions', function (): void {
|
|||||||
->assertJsonPath('data.items.0.username', 'target_user')
|
->assertJsonPath('data.items.0.username', 'target_user')
|
||||||
->assertJsonPath('data.items.0.roles.0', 'target_role');
|
->assertJsonPath('data.items.0.roles.0', 'target_role');
|
||||||
|
|
||||||
$this->withHeader('Authorization', 'Bearer '.$token)
|
|
||||||
->putJson('/api/v1/admin/admin-users/'.$target->id.'/permissions', [
|
|
||||||
'permission_slugs' => ['prd.report.view'],
|
|
||||||
])
|
|
||||||
->assertOk()
|
|
||||||
->assertJsonPath('code', ErrorCode::Success->value)
|
|
||||||
->assertJsonFragment(['prd.report.view']);
|
|
||||||
|
|
||||||
expect($target->fresh()->directLegacyPermissionSlugs())->toContain('prd.report.view');
|
|
||||||
|
|
||||||
$list = $this->withHeader('Authorization', 'Bearer '.$token)
|
$list = $this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
->getJson('/api/v1/admin/admin-users?keyword=target')
|
->getJson('/api/v1/admin/admin-users?keyword=target')
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->json('data.items.0.effective_permissions');
|
->json('data.items.0.effective_permissions');
|
||||||
|
|
||||||
expect($list)->toContain('prd.draw_result.view');
|
expect($list)->toContain('prd.draw_result.view');
|
||||||
expect($list)->toContain('prd.report.view');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('admin can sync user roles for default site', function (): void {
|
test('admin can sync user roles for default site', function (): void {
|
||||||
@@ -138,6 +130,101 @@ test('admin can sync user roles for default site', function (): void {
|
|||||||
expect($slugs)->toBe(['role_sync_a', 'role_sync_b']);
|
expect($slugs)->toBe(['role_sync_a', 'role_sync_b']);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('platform admin users list excludes agent accounts', function (): void {
|
||||||
|
$token = makeAdminWithPermissions('platform_list_manager', ['prd.admin_user.manage']);
|
||||||
|
|
||||||
|
$platformUser = AdminUser::query()->create([
|
||||||
|
'username' => 'platform_only_user',
|
||||||
|
'name' => 'Platform Only',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$siteId = AdminUser::defaultAdminSiteId();
|
||||||
|
$root = AgentNode::query()->where('admin_site_id', $siteId)->where('depth', 0)->firstOrFail();
|
||||||
|
|
||||||
|
$super = AdminUser::query()->create([
|
||||||
|
'username' => 'agent_list_super',
|
||||||
|
'name' => 'Super',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($super);
|
||||||
|
|
||||||
|
$branch = app(AgentNodeService::class)->createChild($super, [
|
||||||
|
'parent_id' => (int) $root->id,
|
||||||
|
'code' => 'list-branch',
|
||||||
|
'name' => 'List Branch',
|
||||||
|
]);
|
||||||
|
|
||||||
|
app(AgentAdminUserService::class)->createUnderAgent($branch, [
|
||||||
|
'username' => 'agent_hidden_user',
|
||||||
|
'nickname' => 'Agent Hidden',
|
||||||
|
'password' => 'secret-strong-2',
|
||||||
|
'role_ids' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$items = $this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->getJson('/api/v1/admin/admin-users')
|
||||||
|
->assertOk()
|
||||||
|
->json('data.items');
|
||||||
|
|
||||||
|
expect(collect($items)->pluck('username')->all())
|
||||||
|
->toContain('platform_only_user')
|
||||||
|
->not->toContain('agent_hidden_user');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('platform account apis reject agent accounts and agent roles', function (): void {
|
||||||
|
$token = makeAdminWithPermissions('platform_scope_manager', ['prd.admin_user.manage', 'prd.admin_role.manage']);
|
||||||
|
|
||||||
|
$siteId = AdminUser::defaultAdminSiteId();
|
||||||
|
$root = AgentNode::query()->where('admin_site_id', $siteId)->where('depth', 0)->firstOrFail();
|
||||||
|
|
||||||
|
$super = AdminUser::query()->create([
|
||||||
|
'username' => 'platform_guard_super',
|
||||||
|
'name' => 'Super',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($super);
|
||||||
|
|
||||||
|
$branch = app(AgentNodeService::class)->createChild($super, [
|
||||||
|
'parent_id' => (int) $root->id,
|
||||||
|
'code' => 'platform-guard-branch',
|
||||||
|
'name' => 'Platform Guard Branch',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$agentRole = AdminRole::query()->create([
|
||||||
|
'slug' => 'guard_agent_role',
|
||||||
|
'name' => 'Guard Agent Role',
|
||||||
|
'scope_type' => AdminRole::SCOPE_AGENT,
|
||||||
|
'owner_agent_id' => $branch->id,
|
||||||
|
]);
|
||||||
|
$agentRole->syncLegacyPermissionSlugs(['prd.agent.role.view']);
|
||||||
|
|
||||||
|
$agentUser = app(AgentAdminUserService::class)->createUnderAgent($branch, [
|
||||||
|
'username' => 'guard_agent_user',
|
||||||
|
'nickname' => 'Guard Agent User',
|
||||||
|
'password' => 'secret-strong-3',
|
||||||
|
'role_ids' => [(int) $agentRole->id],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->putJson('/api/v1/admin/admin-users/'.$agentUser->id.'/roles', [
|
||||||
|
'role_slugs' => ['guard_agent_role'],
|
||||||
|
])
|
||||||
|
->assertStatus(422);
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->putJson('/api/v1/admin/admin-roles/'.$agentRole->id.'/permissions', [
|
||||||
|
'permission_slugs' => ['prd.admin_role.manage'],
|
||||||
|
])
|
||||||
|
->assertStatus(422);
|
||||||
|
});
|
||||||
|
|
||||||
test('permission catalog groups permissions by admin navigation order', function (): void {
|
test('permission catalog groups permissions by admin navigation order', function (): void {
|
||||||
$token = makeAdminWithPermissions('nav_group_catalog', ['prd.admin_user.manage', 'prd.admin_role.manage']);
|
$token = makeAdminWithPermissions('nav_group_catalog', ['prd.admin_user.manage', 'prd.admin_role.manage']);
|
||||||
|
|
||||||
|
|||||||
44
tests/Feature/AgentCreditSettlementExampleTest.php
Normal file
44
tests/Feature/AgentCreditSettlementExampleTest.php
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Services\AgentSettlement\ShareSettlementCalculator;
|
||||||
|
use App\Support\Settlement\DesignDocExample12;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
test('design doc example 12 actual share rates sum to 100 percent', function (): void {
|
||||||
|
$rates = DesignDocExample12::actualShareRates();
|
||||||
|
expect($rates['total'])->toEqual(100.0);
|
||||||
|
expect($rates['actual']['C'])->toEqual(25.0);
|
||||||
|
expect($rates['actual']['B'])->toEqual(15.0);
|
||||||
|
expect($rates['actual']['A'])->toEqual(20.0);
|
||||||
|
expect($rates['actual']['platform'])->toEqual(40.0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('design doc example 12 share settlement matches section 12.3 and 12.4', function (): void {
|
||||||
|
$calculator = new ShareSettlementCalculator;
|
||||||
|
$result = $calculator->calculate(
|
||||||
|
sharedNetWinLoss: DesignDocExample12::SHARED_NET_WIN_LOSS,
|
||||||
|
totalSharesByCode: [
|
||||||
|
'A' => DesignDocExample12::TOTAL_SHARE_A,
|
||||||
|
'B' => DesignDocExample12::TOTAL_SHARE_B,
|
||||||
|
'C' => DesignDocExample12::TOTAL_SHARE_C,
|
||||||
|
],
|
||||||
|
extraRebateByCode: ['C' => DesignDocExample12::EXTRA_REBATE_BY_C],
|
||||||
|
gameWinLoss: DesignDocExample12::GAME_WIN_LOSS,
|
||||||
|
basicRebate: DesignDocExample12::BASIC_REBATE,
|
||||||
|
chainFromPlayer: ['C', 'B', 'A'],
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($result->playerNetSettlement)->toEqual((float) DesignDocExample12::PLAYER_NET_SETTLEMENT);
|
||||||
|
expect($result->sharedNetWinLoss)->toEqual((float) DesignDocExample12::SHARED_NET_WIN_LOSS);
|
||||||
|
expect($result->shareProfits['C'])->toEqual(DesignDocExample12::SHARE_PROFIT_C);
|
||||||
|
expect($result->shareProfits['B'])->toEqual(DesignDocExample12::SHARE_PROFIT_B);
|
||||||
|
expect($result->shareProfits['A'])->toEqual(DesignDocExample12::SHARE_PROFIT_A);
|
||||||
|
expect($result->shareProfits['platform'])->toEqual(DesignDocExample12::SHARE_PROFIT_PLATFORM);
|
||||||
|
expect($result->finalProfits['C'])->toEqual(DesignDocExample12::FINAL_PROFIT_C);
|
||||||
|
expect($result->tierSettlements['P_to_C'])->toEqual(DesignDocExample12::TIER_P_TO_C);
|
||||||
|
expect($result->tierSettlements['C_to_B'])->toEqual(DesignDocExample12::TIER_C_TO_B);
|
||||||
|
expect($result->tierSettlements['B_to_A'])->toEqual(DesignDocExample12::TIER_B_TO_A);
|
||||||
|
expect($result->tierSettlements['A_to_platform'])->toEqual(DesignDocExample12::TIER_A_TO_PLATFORM);
|
||||||
|
});
|
||||||
@@ -75,6 +75,24 @@ function grantSuperAdminRole(AdminUser $admin): void
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** 为后台测试账号挂上代理节点(需已存在 agent_nodes / admin_user_agents 表)。 */
|
/** 为后台测试账号挂上代理节点(需已存在 agent_nodes / admin_user_agents 表)。 */
|
||||||
|
/** Feature 测直调 {@see AgentNodeService::createChild()} 时使用的密码。 */
|
||||||
|
function agentNodeTestPassword(): string
|
||||||
|
{
|
||||||
|
return 'TestPass1!';
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<string, mixed> $overrides
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
function agentChildPayload(array $overrides = []): array
|
||||||
|
{
|
||||||
|
return array_merge([
|
||||||
|
'password' => agentNodeTestPassword(),
|
||||||
|
'can_create_child_agent' => true,
|
||||||
|
'can_create_player' => true,
|
||||||
|
], $overrides);
|
||||||
|
}
|
||||||
|
|
||||||
function bindAdminUserToAgent(AdminUser $admin, int $agentNodeId): void
|
function bindAdminUserToAgent(AdminUser $admin, int $agentNodeId): void
|
||||||
{
|
{
|
||||||
DB::table('admin_user_agents')->updateOrInsert(
|
DB::table('admin_user_agents')->updateOrInsert(
|
||||||
|
|||||||
Reference in New Issue
Block a user