feat: 增强管理员功能与数据处理
- 在多个控制器中引入 agent_node_id,以支持基于代理节点的权限和数据过滤。 - 更新 AdminRole 和 AdminUser 模型,新增角色范围和代理节点相关功能,提升角色管理的灵活性。 - 在请求验证中添加 agent_node_id 字段,确保 API 接口支持代理节点的相关操作。 - 优化 LotterySettings 服务,支持批量写入设置,提升配置管理的效率。 - 更新仪表板和报告服务,增强数据统计功能,确保管理员能够获取更全面的统计信息。
This commit is contained in:
@@ -3,12 +3,16 @@
|
|||||||
namespace App\Http\Controllers\Api\V1\Admin;
|
namespace App\Http\Controllers\Api\V1\Admin;
|
||||||
|
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Middleware\RecordAdminApiAudit;
|
||||||
|
use App\Http\Requests\Admin\AdminSettingBatchUpdateRequest;
|
||||||
use App\Http\Requests\Admin\AdminSettingIndexRequest;
|
use App\Http\Requests\Admin\AdminSettingIndexRequest;
|
||||||
use App\Http\Requests\Admin\AdminSettingUpdateRequest;
|
use App\Http\Requests\Admin\AdminSettingUpdateRequest;
|
||||||
use App\Models\LotterySetting;
|
use App\Models\LotterySetting;
|
||||||
|
use App\Services\AuditLogger;
|
||||||
use App\Support\ApiResponse;
|
use App\Support\ApiResponse;
|
||||||
use App\Services\LotterySettings;
|
use App\Services\LotterySettings;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 后台:运营配置(KV 设置)读写。
|
* 后台:运营配置(KV 设置)读写。
|
||||||
@@ -54,4 +58,47 @@ final class AdminSettingController extends Controller
|
|||||||
'description' => $fresh->description_zh,
|
'description' => $fresh->description_zh,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function batchUpdate(AdminSettingBatchUpdateRequest $request): JsonResponse
|
||||||
|
{
|
||||||
|
/** @var list<array{key: string, value: mixed}> $items */
|
||||||
|
$items = $request->validated('items');
|
||||||
|
|
||||||
|
DB::transaction(static function () use ($items): void {
|
||||||
|
LotterySettings::putMany($items);
|
||||||
|
});
|
||||||
|
|
||||||
|
$keys = array_map(static fn (array $item): string => (string) $item['key'], $items);
|
||||||
|
$rows = LotterySetting::query()
|
||||||
|
->whereIn('setting_key', $keys)
|
||||||
|
->orderBy('setting_key')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
if ($admin !== null) {
|
||||||
|
AuditLogger::recordForAdmin(
|
||||||
|
$admin,
|
||||||
|
$request,
|
||||||
|
moduleCode: 'settings',
|
||||||
|
actionCode: 'batch_update',
|
||||||
|
targetType: 'lottery_settings',
|
||||||
|
targetId: 'batch',
|
||||||
|
beforeJson: null,
|
||||||
|
afterJson: [
|
||||||
|
'keys' => $keys,
|
||||||
|
'count' => count($keys),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
$request->attributes->set(RecordAdminApiAudit::ATTRIBUTE_AUDIT_RECORDED, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse::success([
|
||||||
|
'items' => $rows->map(fn (LotterySetting $s): array => [
|
||||||
|
'key' => $s->setting_key,
|
||||||
|
'value' => $s->value_json,
|
||||||
|
'group' => $s->group_name,
|
||||||
|
'description' => $s->description_zh,
|
||||||
|
])->values()->all(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use App\Services\AuditLogger;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\Agent\AgentAdminUserService;
|
||||||
|
use App\Support\AdminAgentNodeAccess;
|
||||||
|
use App\Support\AdminUserApiPresenter;
|
||||||
|
use App\Http\Requests\Admin\AgentAdminUserRoleSyncRequest;
|
||||||
|
|
||||||
|
final class AgentAdminUserRoleSyncController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(
|
||||||
|
AgentAdminUserRoleSyncRequest $request,
|
||||||
|
AdminUser $admin_user,
|
||||||
|
AgentAdminUserService $service,
|
||||||
|
): JsonResponse {
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
$agent = $admin_user->primaryAgentNode();
|
||||||
|
if ($agent === null) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$denied = AdminAgentNodeAccess::denyUnlessNodeVisible($admin, $agent);
|
||||||
|
if ($denied !== null) {
|
||||||
|
return $denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $admin->isSuperAdmin() && ! $admin->hasAdminPermission('prd.agent.user.manage')) {
|
||||||
|
return AdminAgentNodeAccess::denyUnlessCanManageParent($admin, $agent);
|
||||||
|
}
|
||||||
|
|
||||||
|
$before = AdminUserApiPresenter::listItem($admin_user);
|
||||||
|
$user = $service->syncRoles($agent, $admin_user, $request->validated('role_ids'));
|
||||||
|
$after = AdminUserApiPresenter::listItem($user);
|
||||||
|
|
||||||
|
AuditLogger::recordForAdmin(
|
||||||
|
$admin,
|
||||||
|
$request,
|
||||||
|
'agent',
|
||||||
|
'agent_admin_user.sync_roles',
|
||||||
|
'admin_user',
|
||||||
|
(string) $user->id,
|
||||||
|
$before,
|
||||||
|
$after,
|
||||||
|
);
|
||||||
|
|
||||||
|
return ApiResponse::success($after);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Support\AdminAgentNodeAccess;
|
||||||
|
use App\Support\AdminUserApiPresenter;
|
||||||
|
|
||||||
|
final class AgentNodeAdminUserIndexController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request, AgentNode $agent_node): JsonResponse
|
||||||
|
{
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
$denied = AdminAgentNodeAccess::denyUnlessNodeVisible($admin, $agent_node);
|
||||||
|
if ($denied !== null) {
|
||||||
|
return $denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
$userIds = DB::table('admin_user_agents')
|
||||||
|
->where('agent_node_id', $agent_node->id)
|
||||||
|
->pluck('admin_user_id');
|
||||||
|
|
||||||
|
$users = AdminUser::query()
|
||||||
|
->whereIn('id', $userIds)
|
||||||
|
->orderBy('username')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return ApiResponse::success([
|
||||||
|
'agent_node_id' => (int) $agent_node->id,
|
||||||
|
'items' => $users->map(static fn (AdminUser $user): array => AdminUserApiPresenter::listItem($user))->all(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use App\Services\AuditLogger;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\Agent\AgentAdminUserService;
|
||||||
|
use App\Support\AdminAgentNodeAccess;
|
||||||
|
use App\Support\AdminUserApiPresenter;
|
||||||
|
use App\Http\Requests\Admin\AgentAdminUserStoreRequest;
|
||||||
|
|
||||||
|
final class AgentNodeAdminUserStoreController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(
|
||||||
|
AgentAdminUserStoreRequest $request,
|
||||||
|
AgentNode $agent_node,
|
||||||
|
AgentAdminUserService $service,
|
||||||
|
): JsonResponse {
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
$denied = AdminAgentNodeAccess::denyUnlessNodeVisible($admin, $agent_node);
|
||||||
|
if ($denied !== null) {
|
||||||
|
return $denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $admin->isSuperAdmin() && ! $admin->hasAdminPermission('prd.agent.user.manage')) {
|
||||||
|
return AdminAgentNodeAccess::denyUnlessCanManageParent($admin, $agent_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
$user = $service->createUnderAgent($agent_node, $request->validated());
|
||||||
|
|
||||||
|
AuditLogger::recordForAdmin(
|
||||||
|
$admin,
|
||||||
|
$request,
|
||||||
|
'agent',
|
||||||
|
'agent_admin_user.create',
|
||||||
|
'admin_user',
|
||||||
|
(string) $user->id,
|
||||||
|
null,
|
||||||
|
AdminUserApiPresenter::listItem($user),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ApiResponse::success(AdminUserApiPresenter::listItem($user))->setStatusCode(201);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Support\AdminAgentNodeAccess;
|
||||||
|
use App\Support\AgentNodePresenter;
|
||||||
|
|
||||||
|
final class AgentNodeChildrenController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request, AgentNode $agent_node): JsonResponse
|
||||||
|
{
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
$denied = AdminAgentNodeAccess::denyUnlessNodeVisible($admin, $agent_node);
|
||||||
|
if ($denied !== null) {
|
||||||
|
return $denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
$items = $agent_node->children()
|
||||||
|
->orderBy('code')
|
||||||
|
->get()
|
||||||
|
->map(static fn (AgentNode $child): array => AgentNodePresenter::item($child))
|
||||||
|
->all();
|
||||||
|
|
||||||
|
return ApiResponse::success(['items' => $items]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Services\Agent\AgentDelegationService;
|
||||||
|
use App\Support\AdminAgentNodeAccess;
|
||||||
|
use App\Support\AgentDelegationAuthorization;
|
||||||
|
use App\Support\ApiMessage;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use App\Lottery\ErrorCode;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
|
||||||
|
final class AgentNodeDelegationGrantIndexController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(
|
||||||
|
Request $request,
|
||||||
|
AgentNode $agent_node,
|
||||||
|
AgentDelegationService $service,
|
||||||
|
): JsonResponse {
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
$denied = AdminAgentNodeAccess::denyUnlessNodeVisible($admin, $agent_node);
|
||||||
|
if ($denied !== null) {
|
||||||
|
return $denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($agent_node->isRoot()) {
|
||||||
|
return ApiMessage::errorResponse(
|
||||||
|
$request,
|
||||||
|
'admin.agent_delegation_root_denied',
|
||||||
|
ErrorCode::ValidationFailed->value,
|
||||||
|
null,
|
||||||
|
422,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! AgentDelegationAuthorization::childIsManageableBy($admin, $agent_node)) {
|
||||||
|
return ApiMessage::errorResponse(
|
||||||
|
$request,
|
||||||
|
'admin.agent_delegation_manage_denied',
|
||||||
|
ErrorCode::AdminForbidden->value,
|
||||||
|
null,
|
||||||
|
403,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse::success([
|
||||||
|
'child_agent_id' => (int) $agent_node->id,
|
||||||
|
'grants' => $service->listForChild($agent_node, $admin),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Http\Requests\Admin\AgentDelegationGrantSyncRequest;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Services\Agent\AgentDelegationService;
|
||||||
|
use App\Services\AuditLogger;
|
||||||
|
use App\Support\AdminAgentNodeAccess;
|
||||||
|
use App\Support\AgentDelegationAuthorization;
|
||||||
|
use App\Support\ApiMessage;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use App\Lottery\ErrorCode;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
final class AgentNodeDelegationGrantSyncController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(
|
||||||
|
AgentDelegationGrantSyncRequest $request,
|
||||||
|
AgentNode $agent_node,
|
||||||
|
AgentDelegationService $service,
|
||||||
|
): JsonResponse {
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
$denied = AdminAgentNodeAccess::denyUnlessNodeVisible($admin, $agent_node);
|
||||||
|
if ($denied !== null) {
|
||||||
|
return $denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($agent_node->isRoot()) {
|
||||||
|
return ApiMessage::errorResponse(
|
||||||
|
$request,
|
||||||
|
'admin.agent_delegation_root_denied',
|
||||||
|
ErrorCode::ValidationFailed->value,
|
||||||
|
null,
|
||||||
|
422,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! AgentDelegationAuthorization::childIsManageableBy($admin, $agent_node)) {
|
||||||
|
return ApiMessage::errorResponse(
|
||||||
|
$request,
|
||||||
|
'admin.agent_delegation_manage_denied',
|
||||||
|
ErrorCode::AdminForbidden->value,
|
||||||
|
null,
|
||||||
|
403,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$before = $service->listForChild($agent_node);
|
||||||
|
$grants = $request->validated('grants');
|
||||||
|
$after = $service->syncGrants($admin, $agent_node, $grants);
|
||||||
|
|
||||||
|
AuditLogger::recordForAdmin(
|
||||||
|
$admin,
|
||||||
|
$request,
|
||||||
|
'agent',
|
||||||
|
'agent.delegation.sync',
|
||||||
|
'agent_node',
|
||||||
|
(string) $agent_node->id,
|
||||||
|
['grants' => $before],
|
||||||
|
['grants' => $after],
|
||||||
|
);
|
||||||
|
|
||||||
|
return ApiResponse::success([
|
||||||
|
'child_agent_id' => (int) $agent_node->id,
|
||||||
|
'grants' => $after,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Lottery\ErrorCode;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Support\ApiMessage;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use App\Services\AuditLogger;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\Agent\AgentNodeService;
|
||||||
|
use App\Support\AdminAgentNodeAccess;
|
||||||
|
use App\Support\AdminAgentScope;
|
||||||
|
use App\Support\AgentNodePresenter;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
final class AgentNodeDestroyController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(
|
||||||
|
\Illuminate\Http\Request $request,
|
||||||
|
AgentNode $agent_node,
|
||||||
|
AgentNodeService $service,
|
||||||
|
): JsonResponse {
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
$denied = AdminAgentNodeAccess::denyUnlessNodeVisible($admin, $agent_node);
|
||||||
|
if ($denied !== null) {
|
||||||
|
return $denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! AdminAgentScope::nodeManageableBy($admin, $agent_node)) {
|
||||||
|
return AdminAgentNodeAccess::denyUnlessNodeVisible($admin, $agent_node)
|
||||||
|
?? AdminAgentNodeAccess::denyUnlessCanManageParent($admin, $agent_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($agent_node->isRoot()) {
|
||||||
|
return ApiMessage::errorResponse($request, 'admin.agent_root_delete_denied', ErrorCode::ValidationFailed->value, null, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($agent_node->children()->exists()) {
|
||||||
|
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()) {
|
||||||
|
return ApiMessage::errorResponse($request, 'admin.agent_node_has_users_cannot_delete', ErrorCode::ValidationFailed->value, null, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (DB::table('admin_roles')->where('owner_agent_id', (int) $agent_node->id)->exists()) {
|
||||||
|
return ApiMessage::errorResponse($request, 'admin.agent_node_has_roles_cannot_delete', ErrorCode::ValidationFailed->value, null, 422);
|
||||||
|
}
|
||||||
|
|
||||||
|
$before = AgentNodePresenter::item($agent_node);
|
||||||
|
$service->destroy($agent_node);
|
||||||
|
|
||||||
|
AuditLogger::recordForAdmin(
|
||||||
|
$admin,
|
||||||
|
$request,
|
||||||
|
'system',
|
||||||
|
'agent_node.destroy',
|
||||||
|
'agent_node',
|
||||||
|
(string) $agent_node->id,
|
||||||
|
$before,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return ApiResponse::success(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Models\AdminRole;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Support\AdminAgentNodeAccess;
|
||||||
|
use App\Support\AdminRoleApiPresenter;
|
||||||
|
|
||||||
|
final class AgentNodeRoleIndexController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request, AgentNode $agent_node): JsonResponse
|
||||||
|
{
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
$denied = AdminAgentNodeAccess::denyUnlessNodeVisible($admin, $agent_node);
|
||||||
|
if ($denied !== null) {
|
||||||
|
return $denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roles = AdminRole::query()
|
||||||
|
->where('scope_type', AdminRole::SCOPE_AGENT)
|
||||||
|
->where('owner_agent_id', $agent_node->id)
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->orderBy('id')
|
||||||
|
->get();
|
||||||
|
|
||||||
|
return ApiResponse::success([
|
||||||
|
'agent_node_id' => (int) $agent_node->id,
|
||||||
|
'items' => $roles->map(static fn (AdminRole $role): array => AdminRoleApiPresenter::item($role))->all(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use App\Services\AuditLogger;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\Agent\AgentRoleService;
|
||||||
|
use App\Support\AdminAgentNodeAccess;
|
||||||
|
use App\Support\AdminRoleApiPresenter;
|
||||||
|
use App\Http\Requests\Admin\AgentRoleStoreRequest;
|
||||||
|
|
||||||
|
final class AgentNodeRoleStoreController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(
|
||||||
|
AgentRoleStoreRequest $request,
|
||||||
|
AgentNode $agent_node,
|
||||||
|
AgentRoleService $service,
|
||||||
|
): JsonResponse {
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
$denied = AdminAgentNodeAccess::denyUnlessNodeVisible($admin, $agent_node);
|
||||||
|
if ($denied !== null) {
|
||||||
|
return $denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $admin->isSuperAdmin() && ! $admin->hasAdminPermission('prd.agent.role.manage')) {
|
||||||
|
return AdminAgentNodeAccess::denyUnlessCanManageParent($admin, $agent_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
$role = $service->createForAgent($admin, $agent_node, $request->validated());
|
||||||
|
|
||||||
|
AuditLogger::recordForAdmin(
|
||||||
|
$admin,
|
||||||
|
$request,
|
||||||
|
'agent',
|
||||||
|
'agent_role.create',
|
||||||
|
'admin_role',
|
||||||
|
(string) $role->id,
|
||||||
|
null,
|
||||||
|
AdminRoleApiPresenter::item($role),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ApiResponse::success(AdminRoleApiPresenter::item($role));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Support\AdminAgentNodeAccess;
|
||||||
|
use App\Support\AgentNodePresenter;
|
||||||
|
|
||||||
|
final class AgentNodeShowController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request, AgentNode $agent_node): JsonResponse
|
||||||
|
{
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
$denied = AdminAgentNodeAccess::denyUnlessNodeVisible($admin, $agent_node);
|
||||||
|
if ($denied !== null) {
|
||||||
|
return $denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiResponse::success(AgentNodePresenter::item($agent_node));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use App\Services\AuditLogger;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\Agent\AgentNodeService;
|
||||||
|
use App\Support\AdminAgentNodeAccess;
|
||||||
|
use App\Support\AgentNodePresenter;
|
||||||
|
use App\Http\Requests\Admin\AgentNodeStoreRequest;
|
||||||
|
|
||||||
|
final class AgentNodeStoreController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(AgentNodeStoreRequest $request, AgentNodeService $service): JsonResponse
|
||||||
|
{
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
$parent = AgentNode::query()->findOrFail((int) $request->validated('parent_id'));
|
||||||
|
$denied = AdminAgentNodeAccess::denyUnlessCanManageParent($admin, $parent);
|
||||||
|
if ($denied !== null) {
|
||||||
|
return $denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
$node = $service->createChild($admin, $request->validated());
|
||||||
|
|
||||||
|
AuditLogger::recordForAdmin(
|
||||||
|
$admin,
|
||||||
|
$request,
|
||||||
|
'system',
|
||||||
|
'agent_node.create',
|
||||||
|
'agent_node',
|
||||||
|
(string) $node->id,
|
||||||
|
null,
|
||||||
|
AgentNodePresenter::item($node),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ApiResponse::success(AgentNodePresenter::item($node));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Support\AdminAgentNodeAccess;
|
||||||
|
use App\Support\AdminAgentScope;
|
||||||
|
use App\Support\AgentNodePresenter;
|
||||||
|
|
||||||
|
final class AgentNodeTreeController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(Request $request): JsonResponse
|
||||||
|
{
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
$siteId = AdminAgentNodeAccess::resolveAdminSiteId(
|
||||||
|
$admin,
|
||||||
|
$request->integer('admin_site_id') ?: null,
|
||||||
|
);
|
||||||
|
|
||||||
|
$denied = AdminAgentNodeAccess::denyUnlessSiteResolved($admin, $siteId);
|
||||||
|
if ($denied !== null) {
|
||||||
|
return $denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
$nodes = AdminAgentScope::visibleNodesQuery($admin, (int) $siteId)->get();
|
||||||
|
|
||||||
|
return ApiResponse::success([
|
||||||
|
'admin_site_id' => (int) $siteId,
|
||||||
|
'tree' => AgentNodePresenter::tree($nodes),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use App\Services\AuditLogger;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\Agent\AgentNodeService;
|
||||||
|
use App\Support\AdminAgentNodeAccess;
|
||||||
|
use App\Support\AdminAgentScope;
|
||||||
|
use App\Support\AgentNodePresenter;
|
||||||
|
use App\Http\Requests\Admin\AgentNodeUpdateRequest;
|
||||||
|
|
||||||
|
final class AgentNodeUpdateController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(
|
||||||
|
AgentNodeUpdateRequest $request,
|
||||||
|
AgentNode $agent_node,
|
||||||
|
AgentNodeService $service,
|
||||||
|
): JsonResponse {
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
$denied = AdminAgentNodeAccess::denyUnlessNodeVisible($admin, $agent_node);
|
||||||
|
if ($denied !== null) {
|
||||||
|
return $denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! AdminAgentScope::nodeManageableBy($admin, $agent_node)) {
|
||||||
|
return AdminAgentNodeAccess::denyUnlessNodeVisible($admin, $agent_node)
|
||||||
|
?? AdminAgentNodeAccess::denyUnlessCanManageParent($admin, $agent_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($agent_node->isRoot() && ! $admin->isSuperAdmin()) {
|
||||||
|
return AdminAgentNodeAccess::denyUnlessCanManageParent($admin, $agent_node);
|
||||||
|
}
|
||||||
|
|
||||||
|
$before = AgentNodePresenter::item($agent_node);
|
||||||
|
$node = $service->update($agent_node, $request->validated());
|
||||||
|
$after = AgentNodePresenter::item($node);
|
||||||
|
|
||||||
|
AuditLogger::recordForAdmin(
|
||||||
|
$admin,
|
||||||
|
$request,
|
||||||
|
'system',
|
||||||
|
'agent_node.update',
|
||||||
|
'agent_node',
|
||||||
|
(string) $node->id,
|
||||||
|
$before,
|
||||||
|
$after,
|
||||||
|
);
|
||||||
|
|
||||||
|
return ApiResponse::success($after);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Models\AdminRole;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use App\Services\AuditLogger;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\Agent\AgentRoleService;
|
||||||
|
use App\Support\AgentRoleAuthorization;
|
||||||
|
use App\Support\AdminRoleApiPresenter;
|
||||||
|
|
||||||
|
final class AgentRoleDestroyController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(
|
||||||
|
\Illuminate\Http\Request $request,
|
||||||
|
AdminRole $admin_role,
|
||||||
|
AgentRoleService $service,
|
||||||
|
): JsonResponse {
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
if (! $admin_role->isAgentScoped()) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$denied = AgentRoleAuthorization::denyUnlessRoleManageable($admin, $admin_role);
|
||||||
|
if ($denied !== null) {
|
||||||
|
return $denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
$before = AdminRoleApiPresenter::item($admin_role);
|
||||||
|
$service->destroy($admin_role);
|
||||||
|
|
||||||
|
AuditLogger::recordForAdmin(
|
||||||
|
$admin,
|
||||||
|
$request,
|
||||||
|
'agent',
|
||||||
|
'agent_role.destroy',
|
||||||
|
'admin_role',
|
||||||
|
(string) $admin_role->id,
|
||||||
|
$before,
|
||||||
|
null,
|
||||||
|
);
|
||||||
|
|
||||||
|
return ApiResponse::success(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Models\AdminRole;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use App\Services\AuditLogger;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\Agent\AgentRoleService;
|
||||||
|
use App\Support\AgentRoleAuthorization;
|
||||||
|
use App\Support\AdminRoleApiPresenter;
|
||||||
|
use App\Http\Requests\Admin\AgentRolePermissionSyncRequest;
|
||||||
|
|
||||||
|
final class AgentRolePermissionSyncController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(
|
||||||
|
AgentRolePermissionSyncRequest $request,
|
||||||
|
AdminRole $admin_role,
|
||||||
|
AgentRoleService $service,
|
||||||
|
): JsonResponse {
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
if (! $admin_role->isAgentScoped()) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$denied = AgentRoleAuthorization::denyUnlessRoleManageable($admin, $admin_role);
|
||||||
|
if ($denied !== null) {
|
||||||
|
return $denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($admin_role->isReadOnlyTemplate()) {
|
||||||
|
return AgentRoleAuthorization::denyUnlessRoleManageable($admin, $admin_role);
|
||||||
|
}
|
||||||
|
|
||||||
|
$slugs = array_values(array_unique($request->validated('permission_slugs')));
|
||||||
|
$before = AdminRoleApiPresenter::item($admin_role);
|
||||||
|
$role = $service->syncPermissions($admin, $admin_role, $slugs);
|
||||||
|
|
||||||
|
AuditLogger::recordForAdmin(
|
||||||
|
$admin,
|
||||||
|
$request,
|
||||||
|
'agent',
|
||||||
|
'agent_role.sync_permissions',
|
||||||
|
'admin_role',
|
||||||
|
(string) $role->id,
|
||||||
|
$before,
|
||||||
|
AdminRoleApiPresenter::item($role),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ApiResponse::success(AdminRoleApiPresenter::item($role));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Api\V1\Admin\Agent;
|
||||||
|
|
||||||
|
use App\Models\AdminRole;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use App\Services\AuditLogger;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Services\Agent\AgentRoleService;
|
||||||
|
use App\Support\AgentRoleAuthorization;
|
||||||
|
use App\Support\AdminRoleApiPresenter;
|
||||||
|
use App\Http\Requests\Admin\AgentRoleUpdateRequest;
|
||||||
|
|
||||||
|
final class AgentRoleUpdateController extends Controller
|
||||||
|
{
|
||||||
|
public function __invoke(
|
||||||
|
AgentRoleUpdateRequest $request,
|
||||||
|
AdminRole $admin_role,
|
||||||
|
AgentRoleService $service,
|
||||||
|
): JsonResponse {
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
|
if (! $admin_role->isAgentScoped()) {
|
||||||
|
abort(404);
|
||||||
|
}
|
||||||
|
|
||||||
|
$denied = AgentRoleAuthorization::denyUnlessRoleManageable($admin, $admin_role);
|
||||||
|
if ($denied !== null) {
|
||||||
|
return $denied;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($admin_role->isReadOnlyTemplate()) {
|
||||||
|
return AgentRoleAuthorization::denyUnlessRoleManageable($admin, $admin_role);
|
||||||
|
}
|
||||||
|
|
||||||
|
$before = AdminRoleApiPresenter::item($admin_role);
|
||||||
|
$role = $service->update($admin_role, $request->validated());
|
||||||
|
|
||||||
|
AuditLogger::recordForAdmin(
|
||||||
|
$admin,
|
||||||
|
$request,
|
||||||
|
'agent',
|
||||||
|
'agent_role.update',
|
||||||
|
'admin_role',
|
||||||
|
(string) $role->id,
|
||||||
|
$before,
|
||||||
|
AdminRoleApiPresenter::item($role),
|
||||||
|
);
|
||||||
|
|
||||||
|
return ApiResponse::success(AdminRoleApiPresenter::item($role));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,10 +3,12 @@
|
|||||||
namespace App\Http\Controllers\Api\V1\Admin\Draw;
|
namespace App\Http\Controllers\Api\V1\Admin\Draw;
|
||||||
|
|
||||||
use Carbon\Carbon;
|
use Carbon\Carbon;
|
||||||
|
use App\Models\AdminUser;
|
||||||
use App\Models\Draw;
|
use App\Models\Draw;
|
||||||
use App\Models\TicketItem;
|
use App\Models\TicketItem;
|
||||||
use App\Models\TicketOrder;
|
use App\Models\TicketOrder;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
|
use App\Support\AdminSiteScope;
|
||||||
use App\Support\AdminApiList;
|
use App\Support\AdminApiList;
|
||||||
use App\Services\LotterySettings;
|
use App\Services\LotterySettings;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
@@ -20,9 +22,13 @@ final class AdminDrawIndexController extends Controller
|
|||||||
{
|
{
|
||||||
public function __invoke(Request $request): JsonResponse
|
public function __invoke(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
$p = AdminApiList::readPaging($request);
|
$p = AdminApiList::readPaging($request);
|
||||||
$drawNo = trim((string) $request->query('draw_no', ''));
|
$drawNo = trim((string) $request->query('draw_no', ''));
|
||||||
$status = trim((string) $request->query('status', ''));
|
$status = trim((string) $request->query('status', ''));
|
||||||
|
$agentNodeId = $request->integer('agent_node_id') ?: null;
|
||||||
|
|
||||||
$q = Draw::query()->orderByDesc('draw_time')->orderByDesc('id');
|
$q = Draw::query()->orderByDesc('draw_time')->orderByDesc('id');
|
||||||
|
|
||||||
@@ -39,6 +45,8 @@ final class AdminDrawIndexController extends Controller
|
|||||||
|
|
||||||
$statsByDrawId = $this->aggregateListStats(
|
$statsByDrawId = $this->aggregateListStats(
|
||||||
$paginator->getCollection()->pluck('id')->map(fn ($id) => (int) $id)->all(),
|
$paginator->getCollection()->pluck('id')->map(fn ($id) => (int) $id)->all(),
|
||||||
|
$admin,
|
||||||
|
$agentNodeId,
|
||||||
);
|
);
|
||||||
|
|
||||||
return AdminApiList::jsonWith($paginator, fn (Draw $row) => $this->row($row, $statsByDrawId), [
|
return AdminApiList::jsonWith($paginator, fn (Draw $row) => $this->row($row, $statsByDrawId), [
|
||||||
@@ -55,20 +63,22 @@ final class AdminDrawIndexController extends Controller
|
|||||||
* @param list<int> $drawIds
|
* @param list<int> $drawIds
|
||||||
* @return array<int, array{total_bet_minor: int, total_payout_minor: int, profit_loss_minor: int}>
|
* @return array<int, array{total_bet_minor: int, total_payout_minor: int, profit_loss_minor: int}>
|
||||||
*/
|
*/
|
||||||
private function aggregateListStats(array $drawIds): array
|
private function aggregateListStats(array $drawIds, AdminUser $admin, ?int $agentNodeId): array
|
||||||
{
|
{
|
||||||
if ($drawIds === []) {
|
if ($drawIds === []) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
$betByDraw = TicketOrder::query()
|
$betQuery = TicketOrder::query()->whereIn('draw_id', $drawIds);
|
||||||
->whereIn('draw_id', $drawIds)
|
$this->scopeOrdersToVisiblePlayers($betQuery, $admin, $agentNodeId);
|
||||||
|
$betByDraw = $betQuery
|
||||||
->groupBy('draw_id')
|
->groupBy('draw_id')
|
||||||
->selectRaw('draw_id, COALESCE(SUM(total_actual_deduct), 0) AS total_bet')
|
->selectRaw('draw_id, COALESCE(SUM(total_actual_deduct), 0) AS total_bet')
|
||||||
->pluck('total_bet', 'draw_id');
|
->pluck('total_bet', 'draw_id');
|
||||||
|
|
||||||
$payoutRows = TicketItem::query()
|
$payoutQuery = TicketItem::query()->whereIn('draw_id', $drawIds);
|
||||||
->whereIn('draw_id', $drawIds)
|
$this->scopeTicketItemsToVisiblePlayers($payoutQuery, $admin, $agentNodeId);
|
||||||
|
$payoutRows = $payoutQuery
|
||||||
->groupBy('draw_id')
|
->groupBy('draw_id')
|
||||||
->selectRaw(
|
->selectRaw(
|
||||||
'draw_id, COALESCE(SUM(win_amount), 0) AS win, COALESCE(SUM(jackpot_win_amount), 0) AS jackpot',
|
'draw_id, COALESCE(SUM(win_amount), 0) AS win, COALESCE(SUM(jackpot_win_amount), 0) AS jackpot',
|
||||||
@@ -91,6 +101,44 @@ final class AdminDrawIndexController extends Controller
|
|||||||
return $stats;
|
return $stats;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder<TicketOrder> $query
|
||||||
|
*/
|
||||||
|
private function scopeOrdersToVisiblePlayers($query, AdminUser $admin, ?int $agentNodeId): void
|
||||||
|
{
|
||||||
|
if ($admin->isSuperAdmin() && ($agentNodeId === null || $agentNodeId <= 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->whereHas('player', static function ($playerQuery) use ($admin, $agentNodeId): void {
|
||||||
|
AdminSiteScope::applyPlayerFilters(
|
||||||
|
$playerQuery,
|
||||||
|
$admin,
|
||||||
|
null,
|
||||||
|
$agentNodeId !== null && $agentNodeId > 0 ? $agentNodeId : null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder<TicketItem> $query
|
||||||
|
*/
|
||||||
|
private function scopeTicketItemsToVisiblePlayers($query, AdminUser $admin, ?int $agentNodeId): void
|
||||||
|
{
|
||||||
|
if ($admin->isSuperAdmin() && ($agentNodeId === null || $agentNodeId <= 0)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->whereHas('player', static function ($playerQuery) use ($admin, $agentNodeId): void {
|
||||||
|
AdminSiteScope::applyPlayerFilters(
|
||||||
|
$playerQuery,
|
||||||
|
$admin,
|
||||||
|
null,
|
||||||
|
$agentNodeId !== null && $agentNodeId > 0 ? $agentNodeId : null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param array<int, array{total_bet_minor: int, total_payout_minor: int, profit_loss_minor: int}> $statsByDrawId
|
* @param array<int, array{total_bet_minor: int, total_payout_minor: int, profit_loss_minor: int}> $statsByDrawId
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Support\AdminApiList;
|
|||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use App\Models\JackpotContribution;
|
use App\Models\JackpotContribution;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Support\AdminDataScope;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* GET /api/v1/admin/jackpot/contributions — Jackpot 蓄水流水。
|
* GET /api/v1/admin/jackpot/contributions — Jackpot 蓄水流水。
|
||||||
@@ -15,6 +16,9 @@ final class AdminJackpotContributionIndexController extends Controller
|
|||||||
{
|
{
|
||||||
public function __invoke(Request $request): JsonResponse
|
public function __invoke(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
$p = AdminApiList::readPaging($request);
|
$p = AdminApiList::readPaging($request);
|
||||||
$drawNo = trim((string) $request->query('draw_no', ''));
|
$drawNo = trim((string) $request->query('draw_no', ''));
|
||||||
|
|
||||||
@@ -26,6 +30,8 @@ final class AdminJackpotContributionIndexController extends Controller
|
|||||||
$q->whereHas('draw', fn ($d) => $d->where('draw_no', 'like', '%'.$drawNo.'%'));
|
$q->whereHas('draw', fn ($d) => $d->where('draw_no', 'like', '%'.$drawNo.'%'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AdminDataScope::applyEloquentViaPlayer($q, $admin);
|
||||||
|
|
||||||
$paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']);
|
$paginator = $q->paginate($p['perPage'], ['*'], 'page', $p['page']);
|
||||||
|
|
||||||
return AdminApiList::json($paginator, fn (JackpotContribution $r) => [
|
return AdminApiList::json($paginator, fn (JackpotContribution $r) => [
|
||||||
|
|||||||
@@ -23,15 +23,20 @@ final class AdminPlayerIndexController extends Controller
|
|||||||
$keyword = trim((string) $request->query('keyword', ''));
|
$keyword = trim((string) $request->query('keyword', ''));
|
||||||
$status = $request->query('status');
|
$status = $request->query('status');
|
||||||
$siteCode = $request->query('site_code');
|
$siteCode = $request->query('site_code');
|
||||||
|
$agentNodeId = $request->integer('agent_node_id') ?: null;
|
||||||
|
|
||||||
$q = Player::query()
|
$q = Player::query()
|
||||||
->with(['wallets' => static fn ($wq) => $wq->orderBy('wallet_type')->orderBy('currency_code')])
|
->with([
|
||||||
|
'wallets' => static fn ($wq) => $wq->orderBy('wallet_type')->orderBy('currency_code'),
|
||||||
|
'agentNode:id,code,name',
|
||||||
|
])
|
||||||
->orderByDesc('id');
|
->orderByDesc('id');
|
||||||
|
|
||||||
AdminSiteScope::applyPlayerFilters(
|
AdminSiteScope::applyPlayerFilters(
|
||||||
$q,
|
$q,
|
||||||
$admin,
|
$admin,
|
||||||
is_string($siteCode) ? $siteCode : null,
|
is_string($siteCode) ? $siteCode : null,
|
||||||
|
$agentNodeId,
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($keyword !== '') {
|
if ($keyword !== '') {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Lottery\ErrorCode;
|
|||||||
use App\Support\ApiMessage;
|
use App\Support\ApiMessage;
|
||||||
use App\Support\ApiResponse;
|
use App\Support\ApiResponse;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use App\Support\AdminAgentScope;
|
||||||
use App\Support\AdminSiteScope;
|
use App\Support\AdminSiteScope;
|
||||||
use App\Support\PlayerApiPresenter;
|
use App\Support\PlayerApiPresenter;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
@@ -34,8 +35,30 @@ final class AdminPlayerStoreController extends Controller
|
|||||||
return ApiMessage::errorResponse($request, 'admin.player_already_registered', ErrorCode::ValidationFailed->value, null, 422);
|
return ApiMessage::errorResponse($request, 'admin.player_already_registered', ErrorCode::ValidationFailed->value, null, 422);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$agentNodeId = $admin->isSuperAdmin()
|
||||||
|
? $this->resolveAgentNodeIdForSuperAdmin($request->validated('agent_node_id'), $siteCode)
|
||||||
|
: $admin->primaryAgentNodeId();
|
||||||
|
|
||||||
|
if ($agentNodeId === null) {
|
||||||
|
return ApiMessage::errorResponse(
|
||||||
|
$request,
|
||||||
|
'admin.player_create_agent_required',
|
||||||
|
ErrorCode::ValidationFailed->value,
|
||||||
|
null,
|
||||||
|
422,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $admin->isSuperAdmin()) {
|
||||||
|
$agent = AdminAgentScope::primaryAgentNode($admin);
|
||||||
|
if ($agent === null || (int) $agentNodeId !== (int) $agent->id) {
|
||||||
|
return ApiMessage::errorResponse($request, 'admin.player_create_agent_forbidden', ErrorCode::AdminForbidden->value, null, 403);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$player = Player::query()->create([
|
$player = Player::query()->create([
|
||||||
'site_code' => $request->validated('site_code'),
|
'site_code' => $request->validated('site_code'),
|
||||||
|
'agent_node_id' => $agentNodeId,
|
||||||
'site_player_id' => $request->validated('site_player_id'),
|
'site_player_id' => $request->validated('site_player_id'),
|
||||||
'username' => $request->validated('username'),
|
'username' => $request->validated('username'),
|
||||||
'nickname' => $request->validated('nickname'),
|
'nickname' => $request->validated('nickname'),
|
||||||
@@ -45,4 +68,23 @@ final class AdminPlayerStoreController extends Controller
|
|||||||
|
|
||||||
return ApiResponse::success(PlayerApiPresenter::listItem($player))->setStatusCode(201);
|
return ApiResponse::success(PlayerApiPresenter::listItem($player))->setStatusCode(201);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private function resolveAgentNodeIdForSuperAdmin(mixed $requested, string $siteCode): ?int
|
||||||
|
{
|
||||||
|
if ($requested !== null && (int) $requested > 0) {
|
||||||
|
return (int) $requested;
|
||||||
|
}
|
||||||
|
|
||||||
|
$siteId = \App\Models\AdminSite::query()->where('code', $siteCode)->value('id');
|
||||||
|
if ($siteId === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootId = \App\Models\AgentNode::query()
|
||||||
|
->where('admin_site_id', (int) $siteId)
|
||||||
|
->where('depth', 0)
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
return $rootId !== null ? (int) $rootId : null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ final class AdminReportDailyProfitController extends Controller
|
|||||||
{
|
{
|
||||||
public function __invoke(AdminReportQueryRequest $request, AdminReportQueryService $service): JsonResponse
|
public function __invoke(AdminReportQueryRequest $request, AdminReportQueryService $service): JsonResponse
|
||||||
{
|
{
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
$p = AdminApiList::readPaging($request);
|
$p = AdminApiList::readPaging($request);
|
||||||
$range = $service->resolveDateRange($validated);
|
$range = $service->resolveDateRange($validated);
|
||||||
@@ -22,6 +25,7 @@ final class AdminReportDailyProfitController extends Controller
|
|||||||
$range['date_to'],
|
$range['date_to'],
|
||||||
$p['page'],
|
$p['page'],
|
||||||
$p['perPage'],
|
$p['perPage'],
|
||||||
|
$admin,
|
||||||
);
|
);
|
||||||
|
|
||||||
return AdminApiList::json($paginator, static fn (array $row): array => $row);
|
return AdminApiList::json($paginator, static fn (array $row): array => $row);
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ final class AdminReportPlayDimensionController extends Controller
|
|||||||
{
|
{
|
||||||
public function __invoke(AdminReportQueryRequest $request, AdminReportQueryService $service): JsonResponse
|
public function __invoke(AdminReportQueryRequest $request, AdminReportQueryService $service): JsonResponse
|
||||||
{
|
{
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
$p = AdminApiList::readPaging($request);
|
$p = AdminApiList::readPaging($request);
|
||||||
$range = $service->resolveDateRange($validated);
|
$range = $service->resolveDateRange($validated);
|
||||||
@@ -24,6 +27,7 @@ final class AdminReportPlayDimensionController extends Controller
|
|||||||
$range['date_to'],
|
$range['date_to'],
|
||||||
$p['page'],
|
$p['page'],
|
||||||
$p['perPage'],
|
$p['perPage'],
|
||||||
|
$admin,
|
||||||
);
|
);
|
||||||
|
|
||||||
return AdminApiList::json($paginator, static function (object $row): array {
|
return AdminApiList::json($paginator, static function (object $row): array {
|
||||||
|
|||||||
@@ -13,10 +13,14 @@ final class AdminReportPlayerWinLossController extends Controller
|
|||||||
{
|
{
|
||||||
public function __invoke(AdminReportQueryRequest $request, AdminReportQueryService $service): JsonResponse
|
public function __invoke(AdminReportQueryRequest $request, AdminReportQueryService $service): JsonResponse
|
||||||
{
|
{
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
$p = AdminApiList::readPaging($request);
|
$p = AdminApiList::readPaging($request);
|
||||||
$range = $service->resolveDateRange($validated);
|
$range = $service->resolveDateRange($validated);
|
||||||
$playerId = isset($validated['player_id']) ? (int) $validated['player_id'] : null;
|
$playerId = isset($validated['player_id']) ? (int) $validated['player_id'] : null;
|
||||||
|
$agentNodeId = isset($validated['agent_node_id']) ? (int) $validated['agent_node_id'] : null;
|
||||||
|
|
||||||
$paginator = $service->playerWinLossPaginated(
|
$paginator = $service->playerWinLossPaginated(
|
||||||
$playerId,
|
$playerId,
|
||||||
@@ -24,11 +28,16 @@ final class AdminReportPlayerWinLossController extends Controller
|
|||||||
$range['date_to'],
|
$range['date_to'],
|
||||||
$p['page'],
|
$p['page'],
|
||||||
$p['perPage'],
|
$p['perPage'],
|
||||||
|
$admin,
|
||||||
|
$agentNodeId > 0 ? $agentNodeId : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
return AdminApiList::json($paginator, static function (object $row): array {
|
return AdminApiList::json($paginator, static function (object $row): array {
|
||||||
return [
|
return [
|
||||||
'player_id' => (int) $row->player_id,
|
'player_id' => (int) $row->player_id,
|
||||||
|
'agent_node_id' => isset($row->agent_node_id) ? (int) $row->agent_node_id : null,
|
||||||
|
'agent_code' => isset($row->agent_code) ? (string) $row->agent_code : null,
|
||||||
|
'agent_name' => isset($row->agent_name) ? (string) $row->agent_name : null,
|
||||||
'username' => $row->username !== null ? (string) $row->username : 'player#'.(int) $row->player_id,
|
'username' => $row->username !== null ? (string) $row->username : 'player#'.(int) $row->player_id,
|
||||||
'total_bet_minor' => (int) $row->total_bet_minor,
|
'total_bet_minor' => (int) $row->total_bet_minor,
|
||||||
'total_payout_minor' => (int) $row->total_payout_minor,
|
'total_payout_minor' => (int) $row->total_payout_minor,
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ final class AdminReportRebateCommissionController extends Controller
|
|||||||
{
|
{
|
||||||
public function __invoke(AdminReportQueryRequest $request, AdminReportQueryService $service): JsonResponse
|
public function __invoke(AdminReportQueryRequest $request, AdminReportQueryService $service): JsonResponse
|
||||||
{
|
{
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
$validated = $request->validated();
|
$validated = $request->validated();
|
||||||
$p = AdminApiList::readPaging($request);
|
$p = AdminApiList::readPaging($request);
|
||||||
$range = $service->resolveDateRange($validated);
|
$range = $service->resolveDateRange($validated);
|
||||||
@@ -24,6 +27,7 @@ final class AdminReportRebateCommissionController extends Controller
|
|||||||
$range['date_to'],
|
$range['date_to'],
|
||||||
$p['page'],
|
$p['page'],
|
||||||
$p['perPage'],
|
$p['perPage'],
|
||||||
|
$admin,
|
||||||
);
|
);
|
||||||
|
|
||||||
return AdminApiList::json($paginator, static function (object $row): array {
|
return AdminApiList::json($paginator, static function (object $row): array {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Http\Controllers\Api\V1\Admin\Reports;
|
namespace App\Http\Controllers\Api\V1\Admin\Reports;
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
use App\Models\ReportJob;
|
use App\Models\ReportJob;
|
||||||
use App\Services\Admin\AdminReportJobService;
|
use App\Services\Admin\AdminReportJobService;
|
||||||
use App\Services\Admin\AdminReportQueryService;
|
use App\Services\Admin\AdminReportQueryService;
|
||||||
@@ -29,7 +30,8 @@ final class ReportJobDownloadController
|
|||||||
$dateTo,
|
$dateTo,
|
||||||
);
|
);
|
||||||
$filename = $label.'_'.$pathSuffix.'.'.$report_job->export_format;
|
$filename = $label.'_'.$pathSuffix.'.'.$report_job->export_format;
|
||||||
$rows = $service->reportRows((string) $report_job->report_type, $filterJson);
|
$scopedAdmin = AdminUser::query()->find((int) $report_job->admin_user_id);
|
||||||
|
$rows = $service->reportRows((string) $report_job->report_type, $filterJson, $scopedAdmin);
|
||||||
|
|
||||||
if ((string) $report_job->export_format === 'xlsx') {
|
if ((string) $report_job->export_format === 'xlsx') {
|
||||||
return $spreadsheetExporter->streamDownload($rows, $filename);
|
return $spreadsheetExporter->streamDownload($rows, $filename);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ namespace App\Http\Controllers\Api\V1\Admin\Settlement;
|
|||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Support\AdminApiList;
|
use App\Support\AdminApiList;
|
||||||
|
use App\Support\AdminSiteScope;
|
||||||
|
use App\Support\AgentNodeApiPresenter;
|
||||||
use App\Models\SettlementBatch;
|
use App\Models\SettlementBatch;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
@@ -16,15 +18,33 @@ final class AdminSettlementBatchDetailsController extends Controller
|
|||||||
{
|
{
|
||||||
public function __invoke(Request $request, SettlementBatch $batch): JsonResponse
|
public function __invoke(Request $request, SettlementBatch $batch): JsonResponse
|
||||||
{
|
{
|
||||||
$p = AdminApiList::readPaging($request);
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
$paginator = TicketSettlementDetail::query()
|
$p = AdminApiList::readPaging($request);
|
||||||
|
$agentNodeId = $request->integer('agent_node_id') ?: null;
|
||||||
|
|
||||||
|
$detailQuery = TicketSettlementDetail::query()
|
||||||
->where('settlement_batch_id', $batch->id)
|
->where('settlement_batch_id', $batch->id)
|
||||||
->with([
|
->with([
|
||||||
'ticketItem:id,ticket_no,play_code,player_id',
|
'ticketItem:id,ticket_no,play_code,player_id',
|
||||||
'ticketItem.player:id,site_code,username,nickname,site_player_id',
|
'ticketItem.player:id,site_code,username,nickname,site_player_id,agent_node_id',
|
||||||
|
'ticketItem.player.agentNode:id,code,name',
|
||||||
'ticketItem.order:id,currency_code',
|
'ticketItem.order:id,currency_code',
|
||||||
])
|
]);
|
||||||
|
|
||||||
|
if (! $admin->isSuperAdmin() || ($agentNodeId !== null && $agentNodeId > 0)) {
|
||||||
|
$detailQuery->whereHas('ticketItem.player', static function ($playerQuery) use ($admin, $agentNodeId): void {
|
||||||
|
AdminSiteScope::applyPlayerFilters(
|
||||||
|
$playerQuery,
|
||||||
|
$admin,
|
||||||
|
null,
|
||||||
|
$agentNodeId !== null && $agentNodeId > 0 ? $agentNodeId : null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$paginator = $detailQuery
|
||||||
->orderBy('id')
|
->orderBy('id')
|
||||||
->paginate($p['perPage'], ['*'], 'page', $p['page']);
|
->paginate($p['perPage'], ['*'], 'page', $p['page']);
|
||||||
|
|
||||||
@@ -37,6 +57,7 @@ final class AdminSettlementBatchDetailsController extends Controller
|
|||||||
return [
|
return [
|
||||||
'id' => (int) $row->id,
|
'id' => (int) $row->id,
|
||||||
'ticket_item_id' => (int) $row->ticket_item_id,
|
'ticket_item_id' => (int) $row->ticket_item_id,
|
||||||
|
...AgentNodeApiPresenter::embed($player?->agentNode),
|
||||||
'ticket_no' => $item?->ticket_no,
|
'ticket_no' => $item?->ticket_no,
|
||||||
'play_code' => $item?->play_code,
|
'play_code' => $item?->play_code,
|
||||||
'currency_code' => $order?->currency_code,
|
'currency_code' => $order?->currency_code,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ namespace App\Http\Controllers\Api\V1\Admin\Settlement;
|
|||||||
|
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
use App\Support\AdminApiList;
|
use App\Support\AdminApiList;
|
||||||
|
use App\Support\AdminSiteScope;
|
||||||
use App\Models\SettlementBatch;
|
use App\Models\SettlementBatch;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
@@ -16,14 +17,29 @@ final class AdminSettlementBatchIndexController extends Controller
|
|||||||
{
|
{
|
||||||
public function __invoke(Request $request): JsonResponse
|
public function __invoke(Request $request): JsonResponse
|
||||||
{
|
{
|
||||||
|
$admin = $request->lotteryAdmin();
|
||||||
|
abort_if($admin === null, 401);
|
||||||
|
|
||||||
$p = AdminApiList::readPaging($request);
|
$p = AdminApiList::readPaging($request);
|
||||||
$drawNo = trim((string) $request->query('draw_no', ''));
|
$drawNo = trim((string) $request->query('draw_no', ''));
|
||||||
$status = trim((string) $request->query('status', ''));
|
$status = trim((string) $request->query('status', ''));
|
||||||
|
$agentNodeId = $request->integer('agent_node_id') ?: null;
|
||||||
|
|
||||||
$q = SettlementBatch::query()
|
$q = SettlementBatch::query()
|
||||||
->with(['draw:id,draw_no'])
|
->with(['draw:id,draw_no'])
|
||||||
->orderByDesc('id');
|
->orderByDesc('id');
|
||||||
|
|
||||||
|
if (! $admin->isSuperAdmin() || ($agentNodeId !== null && $agentNodeId > 0)) {
|
||||||
|
$q->whereHas('details.ticketItem.player', static function ($playerQuery) use ($admin, $agentNodeId): void {
|
||||||
|
AdminSiteScope::applyPlayerFilters(
|
||||||
|
$playerQuery,
|
||||||
|
$admin,
|
||||||
|
null,
|
||||||
|
$agentNodeId !== null && $agentNodeId > 0 ? $agentNodeId : null,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if ($drawNo !== '') {
|
if ($drawNo !== '') {
|
||||||
$q->whereHas('draw', fn ($d) => $d->where('draw_no', 'like', '%'.$drawNo.'%'));
|
$q->whereHas('draw', fn ($d) => $d->where('draw_no', 'like', '%'.$drawNo.'%'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use App\Support\ApiResponse;
|
|||||||
use App\Support\CurrencyFormatter;
|
use App\Support\CurrencyFormatter;
|
||||||
use App\Support\PaginationTrait;
|
use App\Support\PaginationTrait;
|
||||||
use App\Support\AdminSiteScope;
|
use App\Support\AdminSiteScope;
|
||||||
|
use App\Support\AgentNodeApiPresenter;
|
||||||
use App\Support\TicketItemListFilters;
|
use App\Support\TicketItemListFilters;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use Illuminate\Http\Request;
|
use Illuminate\Http\Request;
|
||||||
@@ -44,7 +45,8 @@ final class AdminTicketItemIndexController extends Controller
|
|||||||
->with([
|
->with([
|
||||||
'draw:id,draw_no,business_date',
|
'draw:id,draw_no,business_date',
|
||||||
'order:id,order_no,currency_code,created_at',
|
'order:id,order_no,currency_code,created_at',
|
||||||
'player:id,site_code,site_player_id,username,nickname',
|
'player:id,site_code,site_player_id,username,nickname,agent_node_id',
|
||||||
|
'player.agentNode:id,code,name',
|
||||||
])
|
])
|
||||||
->orderByDesc('ticket_items.id');
|
->orderByDesc('ticket_items.id');
|
||||||
|
|
||||||
@@ -86,10 +88,14 @@ final class AdminTicketItemIndexController extends Controller
|
|||||||
is_string($validated['end_date'] ?? null) ? $validated['end_date'] : null,
|
is_string($validated['end_date'] ?? null) ? $validated['end_date'] : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$agentNodeId = isset($validated['agent_node_id']) ? (int) $validated['agent_node_id'] : null;
|
||||||
|
|
||||||
AdminSiteScope::applyViaPlayerRelationWithSiteCode(
|
AdminSiteScope::applyViaPlayerRelationWithSiteCode(
|
||||||
$query,
|
$query,
|
||||||
$admin,
|
$admin,
|
||||||
is_string($validated['site_code'] ?? null) ? $validated['site_code'] : null,
|
is_string($validated['site_code'] ?? null) ? $validated['site_code'] : null,
|
||||||
|
'player',
|
||||||
|
$agentNodeId > 0 ? $agentNodeId : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
$paginator = $query->paginate(perPage: $perPage, page: $page, columns: ['*']);
|
$paginator = $query->paginate(perPage: $perPage, page: $page, columns: ['*']);
|
||||||
@@ -103,6 +109,7 @@ final class AdminTicketItemIndexController extends Controller
|
|||||||
return [
|
return [
|
||||||
'id' => $row->id,
|
'id' => $row->id,
|
||||||
'ticket_no' => $row->ticket_no,
|
'ticket_no' => $row->ticket_no,
|
||||||
|
...AgentNodeApiPresenter::embed($row->player?->agentNode),
|
||||||
'player_id' => $row->player_id,
|
'player_id' => $row->player_id,
|
||||||
'site_code' => $row->player?->site_code,
|
'site_code' => $row->player?->site_code,
|
||||||
'site_player_id' => $row->player?->site_player_id,
|
'site_player_id' => $row->player?->site_player_id,
|
||||||
|
|||||||
@@ -12,7 +12,11 @@ final class AdminRoleIndexController extends Controller
|
|||||||
{
|
{
|
||||||
public function __invoke(): JsonResponse
|
public function __invoke(): JsonResponse
|
||||||
{
|
{
|
||||||
$roles = AdminRole::query()->orderBy('sort_order')->orderBy('id')->get();
|
$roles = AdminRole::query()
|
||||||
|
->where('scope_type', AdminRole::SCOPE_SYSTEM)
|
||||||
|
->orderBy('sort_order')
|
||||||
|
->orderBy('id')
|
||||||
|
->get();
|
||||||
|
|
||||||
return ApiResponse::success([
|
return ApiResponse::success([
|
||||||
'items' => $roles->map(static fn (AdminRole $role): array => AdminRoleApiPresenter::item($role))->values()->all(),
|
'items' => $roles->map(static fn (AdminRole $role): array => AdminRoleApiPresenter::item($role))->values()->all(),
|
||||||
|
|||||||
@@ -26,6 +26,9 @@ final class AdminRoleStoreController extends Controller
|
|||||||
'status' => $request->validated('status', 1),
|
'status' => $request->validated('status', 1),
|
||||||
'is_system' => false,
|
'is_system' => false,
|
||||||
'sort_order' => 0,
|
'sort_order' => 0,
|
||||||
|
'scope_type' => AdminRole::SCOPE_SYSTEM,
|
||||||
|
'owner_agent_id' => null,
|
||||||
|
'delegated_from_role_id' => null,
|
||||||
]);
|
]);
|
||||||
$role->syncLegacyPermissionSlugs($permissionSlugs);
|
$role->syncLegacyPermissionSlugs($permissionSlugs);
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use App\Models\TransferOrder;
|
|||||||
use App\Support\PaginationTrait;
|
use App\Support\PaginationTrait;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use App\Support\AdminSiteScope;
|
use App\Support\AdminSiteScope;
|
||||||
|
use App\Support\AgentNodeApiPresenter;
|
||||||
use App\Support\CurrencyFormatter;
|
use App\Support\CurrencyFormatter;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\TransferOrderListRequest;
|
use App\Http\Requests\Admin\TransferOrderListRequest;
|
||||||
@@ -44,7 +45,10 @@ final class TransferOrderListController extends Controller
|
|||||||
$page = $this->page($request);
|
$page = $this->page($request);
|
||||||
|
|
||||||
$query = TransferOrder::query()
|
$query = TransferOrder::query()
|
||||||
->with(['player:id,site_code,site_player_id,username,nickname'])
|
->with([
|
||||||
|
'player:id,site_code,site_player_id,username,nickname,agent_node_id',
|
||||||
|
'player.agentNode:id,code,name',
|
||||||
|
])
|
||||||
->orderByDesc('id');
|
->orderByDesc('id');
|
||||||
|
|
||||||
if (! empty($validated['player_id'])) {
|
if (! empty($validated['player_id'])) {
|
||||||
@@ -85,10 +89,14 @@ final class TransferOrderListController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$agentNodeId = isset($validated['agent_node_id']) ? (int) $validated['agent_node_id'] : null;
|
||||||
|
|
||||||
AdminSiteScope::applyViaPlayerRelationWithSiteCode(
|
AdminSiteScope::applyViaPlayerRelationWithSiteCode(
|
||||||
$query,
|
$query,
|
||||||
$admin,
|
$admin,
|
||||||
is_string($validated['site_code'] ?? null) ? $validated['site_code'] : null,
|
is_string($validated['site_code'] ?? null) ? $validated['site_code'] : null,
|
||||||
|
'player',
|
||||||
|
$agentNodeId > 0 ? $agentNodeId : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
$paginator = $query->paginate($perPage, ['*'], 'page', $page);
|
$paginator = $query->paginate($perPage, ['*'], 'page', $page);
|
||||||
@@ -119,6 +127,7 @@ final class TransferOrderListController extends Controller
|
|||||||
return [
|
return [
|
||||||
'id' => $o->id,
|
'id' => $o->id,
|
||||||
'transfer_no' => $o->transfer_no,
|
'transfer_no' => $o->transfer_no,
|
||||||
|
...AgentNodeApiPresenter::embed($p?->agentNode),
|
||||||
'player_id' => $o->player_id,
|
'player_id' => $o->player_id,
|
||||||
'site_code' => $p?->site_code,
|
'site_code' => $p?->site_code,
|
||||||
'site_player_id' => $p?->site_player_id,
|
'site_player_id' => $p?->site_player_id,
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ use App\Support\ApiResponse;
|
|||||||
use App\Support\PaginationTrait;
|
use App\Support\PaginationTrait;
|
||||||
use Illuminate\Http\JsonResponse;
|
use Illuminate\Http\JsonResponse;
|
||||||
use App\Support\AdminSiteScope;
|
use App\Support\AdminSiteScope;
|
||||||
|
use App\Support\AgentNodeApiPresenter;
|
||||||
use App\Support\CurrencyFormatter;
|
use App\Support\CurrencyFormatter;
|
||||||
use App\Http\Controllers\Controller;
|
use App\Http\Controllers\Controller;
|
||||||
use App\Http\Requests\Admin\WalletTransactionListRequest;
|
use App\Http\Requests\Admin\WalletTransactionListRequest;
|
||||||
@@ -43,7 +44,10 @@ final class WalletTransactionListController extends Controller
|
|||||||
$page = $this->page($request);
|
$page = $this->page($request);
|
||||||
|
|
||||||
$query = WalletTxn::query()
|
$query = WalletTxn::query()
|
||||||
->with(['player:id,site_code,site_player_id,username,nickname'])
|
->with([
|
||||||
|
'player:id,site_code,site_player_id,username,nickname,agent_node_id',
|
||||||
|
'player.agentNode:id,code,name',
|
||||||
|
])
|
||||||
->orderByDesc('id');
|
->orderByDesc('id');
|
||||||
|
|
||||||
if (! empty($validated['player_id'])) {
|
if (! empty($validated['player_id'])) {
|
||||||
@@ -88,10 +92,14 @@ final class WalletTransactionListController extends Controller
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$agentNodeId = isset($validated['agent_node_id']) ? (int) $validated['agent_node_id'] : null;
|
||||||
|
|
||||||
AdminSiteScope::applyViaPlayerRelationWithSiteCode(
|
AdminSiteScope::applyViaPlayerRelationWithSiteCode(
|
||||||
$query,
|
$query,
|
||||||
$admin,
|
$admin,
|
||||||
is_string($validated['site_code'] ?? null) ? $validated['site_code'] : null,
|
is_string($validated['site_code'] ?? null) ? $validated['site_code'] : null,
|
||||||
|
'player',
|
||||||
|
$agentNodeId > 0 ? $agentNodeId : null,
|
||||||
);
|
);
|
||||||
|
|
||||||
$paginator = $query->paginate($perPage, ['*'], 'page', $page);
|
$paginator = $query->paginate($perPage, ['*'], 'page', $page);
|
||||||
@@ -118,6 +126,7 @@ final class WalletTransactionListController extends Controller
|
|||||||
return [
|
return [
|
||||||
'id' => $t->id,
|
'id' => $t->id,
|
||||||
'txn_no' => $t->txn_no,
|
'txn_no' => $t->txn_no,
|
||||||
|
...AgentNodeApiPresenter::embed($p?->agentNode),
|
||||||
'player_id' => $t->player_id,
|
'player_id' => $t->player_id,
|
||||||
'site_code' => $p?->site_code,
|
'site_code' => $p?->site_code,
|
||||||
'site_player_id' => $p?->site_player_id,
|
'site_player_id' => $p?->site_player_id,
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ final class AdminPlayerStoreRequest extends FormRequest
|
|||||||
'nickname' => ['nullable', 'string', 'max:128'],
|
'nickname' => ['nullable', 'string', 'max:128'],
|
||||||
'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'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ final class AdminReportQueryRequest extends FormRequest
|
|||||||
'date_to' => ['sometimes', 'nullable', 'date_format:Y-m-d', 'after_or_equal:date_from'],
|
'date_to' => ['sometimes', 'nullable', 'date_format:Y-m-d', 'after_or_equal:date_from'],
|
||||||
'player_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
'player_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||||
'play_code' => ['sometimes', 'nullable', 'string', 'max:32'],
|
'play_code' => ['sometimes', 'nullable', 'string', 'max:32'],
|
||||||
|
'agent_node_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
app/Http/Requests/Admin/AdminSettingBatchUpdateRequest.php
Normal file
22
app/Http/Requests/Admin/AdminSettingBatchUpdateRequest.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
final class AdminSettingBatchUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'items' => ['required', 'array', 'min:1', 'max:50'],
|
||||||
|
'items.*.key' => ['required', 'string', 'max:128'],
|
||||||
|
'items.*.value' => ['present'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ final class AdminSettingUpdateRequest extends FormRequest
|
|||||||
public function rules(): array
|
public function rules(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
'value' => ['required'],
|
'value' => ['present'],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
app/Http/Requests/Admin/AgentAdminUserRoleSyncRequest.php
Normal file
22
app/Http/Requests/Admin/AgentAdminUserRoleSyncRequest.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
final class AgentAdminUserRoleSyncRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'role_ids' => ['required', 'array'],
|
||||||
|
'role_ids.*' => ['integer', 'exists:admin_roles,id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/Http/Requests/Admin/AgentAdminUserStoreRequest.php
Normal file
28
app/Http/Requests/Admin/AgentAdminUserStoreRequest.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
final class AgentAdminUserStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'username' => ['required', 'string', 'max:64', Rule::unique('admin_users', 'username')],
|
||||||
|
'nickname' => ['required', 'string', 'max:128'],
|
||||||
|
'email' => ['nullable', 'email', 'max:255', Rule::unique('admin_users', 'email')],
|
||||||
|
'password' => ['required', 'string', 'min:8', 'max:128'],
|
||||||
|
'status' => ['sometimes', 'integer', 'in:0,1'],
|
||||||
|
'role_ids' => ['sometimes', 'array'],
|
||||||
|
'role_ids.*' => ['integer', 'exists:admin_roles,id'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
25
app/Http/Requests/Admin/AgentDelegationGrantSyncRequest.php
Normal file
25
app/Http/Requests/Admin/AgentDelegationGrantSyncRequest.php
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
final class AgentDelegationGrantSyncRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return $this->lotteryAdmin() !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'grants' => ['required', 'array'],
|
||||||
|
'grants.*.menu_action_id' => ['required', 'integer', 'min:1'],
|
||||||
|
'grants.*.can_delegate' => ['sometimes', 'boolean'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
24
app/Http/Requests/Admin/AgentNodeStoreRequest.php
Normal file
24
app/Http/Requests/Admin/AgentNodeStoreRequest.php
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
final class AgentNodeStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'parent_id' => ['required', 'integer', 'exists:agent_nodes,id'],
|
||||||
|
'code' => ['required', 'string', 'max:64', 'regex:/^[a-zA-Z0-9_-]+$/'],
|
||||||
|
'name' => ['required', 'string', 'max:128'],
|
||||||
|
'status' => ['sometimes', 'integer', 'in:0,1'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Http/Requests/Admin/AgentNodeUpdateRequest.php
Normal file
22
app/Http/Requests/Admin/AgentNodeUpdateRequest.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
final class AgentNodeUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['sometimes', 'string', 'max:128'],
|
||||||
|
'status' => ['sometimes', 'integer', 'in:0,1'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
22
app/Http/Requests/Admin/AgentRolePermissionSyncRequest.php
Normal file
22
app/Http/Requests/Admin/AgentRolePermissionSyncRequest.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
final class AgentRolePermissionSyncRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'permission_slugs' => ['required', 'array'],
|
||||||
|
'permission_slugs.*' => ['string', 'max:128'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/Http/Requests/Admin/AgentRoleStoreRequest.php
Normal file
39
app/Http/Requests/Admin/AgentRoleStoreRequest.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use App\Models\AdminRole;
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
use Illuminate\Validation\Rule;
|
||||||
|
|
||||||
|
final class AgentRoleStoreRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
$agentNode = $this->route('agent_node');
|
||||||
|
$ownerId = is_object($agentNode) ? (int) $agentNode->id : (int) $agentNode;
|
||||||
|
|
||||||
|
return [
|
||||||
|
'slug' => [
|
||||||
|
'required',
|
||||||
|
'string',
|
||||||
|
'max:64',
|
||||||
|
'regex:/^[a-z0-9_\\-]+$/',
|
||||||
|
Rule::unique('admin_roles', 'slug')->where(
|
||||||
|
static fn ($query) => $query->where('owner_agent_id', $ownerId),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
'name' => ['required', 'string', 'max:128'],
|
||||||
|
'description' => ['nullable', 'string', 'max:65535'],
|
||||||
|
'status' => ['sometimes', 'integer', 'in:0,1'],
|
||||||
|
'permission_slugs' => ['sometimes', 'array'],
|
||||||
|
'permission_slugs.*' => ['string', 'max:128'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
23
app/Http/Requests/Admin/AgentRoleUpdateRequest.php
Normal file
23
app/Http/Requests/Admin/AgentRoleUpdateRequest.php
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests\Admin;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
final class AgentRoleUpdateRequest extends FormRequest
|
||||||
|
{
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return array<string, mixed> */
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'name' => ['sometimes', 'string', 'max:128'],
|
||||||
|
'description' => ['nullable', 'string', 'max:65535'],
|
||||||
|
'status' => ['sometimes', 'integer', 'in:0,1'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ final class TicketItemListRequest extends FormRequest
|
|||||||
'player_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
'player_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||||
'player_account' => ['sometimes', 'nullable', 'string', 'max:128'],
|
'player_account' => ['sometimes', 'nullable', 'string', 'max:128'],
|
||||||
'site_code' => ['sometimes', 'nullable', 'string', 'max:64'],
|
'site_code' => ['sometimes', 'nullable', 'string', 'max:64'],
|
||||||
|
'agent_node_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||||
'draw_no' => ['sometimes', 'nullable', 'string', 'max:32'],
|
'draw_no' => ['sometimes', 'nullable', 'string', 'max:32'],
|
||||||
'status' => ['sometimes'],
|
'status' => ['sometimes'],
|
||||||
'status.*' => ['string', 'max:32'],
|
'status.*' => ['string', 'max:32'],
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ final class TransferOrderListRequest extends FormRequest
|
|||||||
'player_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
'player_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||||
'player_account' => ['sometimes', 'nullable', 'string', 'max:128'],
|
'player_account' => ['sometimes', 'nullable', 'string', 'max:128'],
|
||||||
'site_code' => ['sometimes', 'nullable', 'string', 'max:64'],
|
'site_code' => ['sometimes', 'nullable', 'string', 'max:64'],
|
||||||
|
'agent_node_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||||
'transfer_no' => ['sometimes', 'nullable', 'string', 'max:96'],
|
'transfer_no' => ['sometimes', 'nullable', 'string', 'max:96'],
|
||||||
'external_ref_no' => ['sometimes', 'nullable', 'string', 'max:96'],
|
'external_ref_no' => ['sometimes', 'nullable', 'string', 'max:96'],
|
||||||
'created_from' => ['sometimes', 'nullable', 'date_format:Y-m-d'],
|
'created_from' => ['sometimes', 'nullable', 'date_format:Y-m-d'],
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ final class WalletTransactionListRequest extends FormRequest
|
|||||||
'player_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
'player_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||||
'player_account' => ['sometimes', 'nullable', 'string', 'max:128'],
|
'player_account' => ['sometimes', 'nullable', 'string', 'max:128'],
|
||||||
'site_code' => ['sometimes', 'nullable', 'string', 'max:64'],
|
'site_code' => ['sometimes', 'nullable', 'string', 'max:64'],
|
||||||
|
'agent_node_id' => ['sometimes', 'nullable', 'integer', 'min:1'],
|
||||||
'txn_no' => ['sometimes', 'nullable', 'string', 'max:96'],
|
'txn_no' => ['sometimes', 'nullable', 'string', 'max:96'],
|
||||||
'external_ref_no' => ['sometimes', 'nullable', 'string', 'max:96'],
|
'external_ref_no' => ['sometimes', 'nullable', 'string', 'max:96'],
|
||||||
'created_from' => ['sometimes', 'nullable', 'date_format:Y-m-d'],
|
'created_from' => ['sometimes', 'nullable', 'date_format:Y-m-d'],
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ final class AdminRole extends Model
|
|||||||
{
|
{
|
||||||
public const ROLE_SUPER_ADMIN = 'super_admin';
|
public const ROLE_SUPER_ADMIN = 'super_admin';
|
||||||
|
|
||||||
|
public const SCOPE_SYSTEM = 'system';
|
||||||
|
|
||||||
|
public const SCOPE_AGENT = 'agent';
|
||||||
|
|
||||||
protected $table = 'admin_roles';
|
protected $table = 'admin_roles';
|
||||||
|
|
||||||
protected static function booted(): void
|
protected static function booted(): void
|
||||||
@@ -30,8 +34,32 @@ final class AdminRole extends Model
|
|||||||
'status',
|
'status',
|
||||||
'is_system',
|
'is_system',
|
||||||
'sort_order',
|
'sort_order',
|
||||||
|
'owner_agent_id',
|
||||||
|
'delegated_from_role_id',
|
||||||
|
'scope_type',
|
||||||
];
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'owner_agent_id' => 'integer',
|
||||||
|
'delegated_from_role_id' => 'integer',
|
||||||
|
'status' => 'integer',
|
||||||
|
'is_system' => 'boolean',
|
||||||
|
'sort_order' => 'integer',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAgentScoped(): bool
|
||||||
|
{
|
||||||
|
return $this->scope_type === self::SCOPE_AGENT && $this->owner_agent_id !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isReadOnlyTemplate(): bool
|
||||||
|
{
|
||||||
|
return $this->delegated_from_role_id !== null;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return BelongsToMany<AdminMenuAction, AdminRole>
|
* @return BelongsToMany<AdminMenuAction, AdminRole>
|
||||||
*/
|
*/
|
||||||
@@ -103,6 +131,15 @@ final class AdminRole extends Model
|
|||||||
|
|
||||||
public function assignedUserCount(): int
|
public function assignedUserCount(): int
|
||||||
{
|
{
|
||||||
|
$agentCount = (int) DB::table('admin_user_agent_roles')
|
||||||
|
->where('role_id', $this->id)
|
||||||
|
->distinct()
|
||||||
|
->count('admin_user_id');
|
||||||
|
|
||||||
|
if ($this->isAgentScoped()) {
|
||||||
|
return $agentCount;
|
||||||
|
}
|
||||||
|
|
||||||
return (int) DB::table('admin_user_site_roles')
|
return (int) DB::table('admin_user_site_roles')
|
||||||
->where('role_id', $this->id)
|
->where('role_id', $this->id)
|
||||||
->distinct()
|
->distinct()
|
||||||
|
|||||||
@@ -78,6 +78,78 @@ final class AdminUser extends Authenticatable
|
|||||||
*
|
*
|
||||||
* @param list<string> $slugs
|
* @param list<string> $slugs
|
||||||
*/
|
*/
|
||||||
|
/**
|
||||||
|
* @param list<int> $roleIds
|
||||||
|
*/
|
||||||
|
public function syncAgentRoleIds(int $agentNodeId, array $roleIds): void
|
||||||
|
{
|
||||||
|
$roleIds = array_values(array_unique(array_map(static fn ($id): int => (int) $id, $roleIds)));
|
||||||
|
|
||||||
|
DB::transaction(function () use ($agentNodeId, $roleIds): void {
|
||||||
|
DB::table('admin_user_agent_roles')
|
||||||
|
->where('admin_user_id', $this->id)
|
||||||
|
->where('agent_node_id', $agentNodeId)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
$now = now();
|
||||||
|
foreach ($roleIds as $roleId) {
|
||||||
|
DB::table('admin_user_agent_roles')->insert([
|
||||||
|
'admin_user_id' => $this->id,
|
||||||
|
'agent_node_id' => $agentNodeId,
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'granted_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$siteId = (int) (AgentNode::query()->where('id', $agentNodeId)->value('admin_site_id') ?? 0);
|
||||||
|
if ($siteId > 0) {
|
||||||
|
DB::table('admin_user_site_roles')
|
||||||
|
->where('admin_user_id', $this->id)
|
||||||
|
->where('site_id', $siteId)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
foreach ($roleIds as $roleId) {
|
||||||
|
DB::table('admin_user_site_roles')->insert([
|
||||||
|
'admin_user_id' => $this->id,
|
||||||
|
'site_id' => $siteId,
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'granted_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string>
|
||||||
|
*/
|
||||||
|
private function roleMenuActionPermissionCodes(): array
|
||||||
|
{
|
||||||
|
$agentId = $this->primaryAgentNodeId();
|
||||||
|
if ($agentId !== null) {
|
||||||
|
$fromAgent = DB::table('admin_user_agent_roles as uar')
|
||||||
|
->join('admin_role_menu_actions as rma', 'rma.role_id', '=', 'uar.role_id')
|
||||||
|
->join('admin_menu_actions as ma', 'ma.id', '=', 'rma.menu_action_id')
|
||||||
|
->where('uar.admin_user_id', $this->id)
|
||||||
|
->where('uar.agent_node_id', $agentId)
|
||||||
|
->where('ma.status', 1)
|
||||||
|
->pluck('ma.permission_code')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if ($fromAgent !== []) {
|
||||||
|
return $fromAgent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB::table('admin_user_site_roles as usr')
|
||||||
|
->join('admin_role_menu_actions as rma', 'rma.role_id', '=', 'usr.role_id')
|
||||||
|
->join('admin_menu_actions as ma', 'ma.id', '=', 'rma.menu_action_id')
|
||||||
|
->where('usr.admin_user_id', $this->id)
|
||||||
|
->where('ma.status', 1)
|
||||||
|
->pluck('ma.permission_code')
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
public function syncRoleSlugsForDefaultSite(array $slugs): void
|
public function syncRoleSlugsForDefaultSite(array $slugs): void
|
||||||
{
|
{
|
||||||
$siteId = self::defaultAdminSiteId();
|
$siteId = self::defaultAdminSiteId();
|
||||||
@@ -114,6 +186,25 @@ final class AdminUser extends Authenticatable
|
|||||||
return $this->roles()->where('admin_roles.slug', self::ROLE_SUPER_ADMIN)->exists();
|
return $this->roles()->where('admin_roles.slug', self::ROLE_SUPER_ADMIN)->exists();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function primaryAgentNodeId(): ?int
|
||||||
|
{
|
||||||
|
$id = DB::table('admin_user_agents')
|
||||||
|
->where('admin_user_id', $this->id)
|
||||||
|
->value('agent_node_id');
|
||||||
|
|
||||||
|
return $id !== null ? (int) $id : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function primaryAgentNode(): ?AgentNode
|
||||||
|
{
|
||||||
|
$id = $this->primaryAgentNodeId();
|
||||||
|
if ($id === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AgentNode::query()->find($id);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 可访问的 admin_sites.id 列表;`null` 表示不限制(超管)。
|
* 可访问的 admin_sites.id 列表;`null` 表示不限制(超管)。
|
||||||
*
|
*
|
||||||
@@ -125,6 +216,11 @@ final class AdminUser extends Authenticatable
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$agent = $this->primaryAgentNode();
|
||||||
|
if ($agent !== null) {
|
||||||
|
return [(int) $agent->admin_site_id];
|
||||||
|
}
|
||||||
|
|
||||||
$ids = DB::table('admin_user_site_roles')
|
$ids = DB::table('admin_user_site_roles')
|
||||||
->where('admin_user_id', $this->id)
|
->where('admin_user_id', $this->id)
|
||||||
->distinct()
|
->distinct()
|
||||||
@@ -193,13 +289,7 @@ final class AdminUser extends Authenticatable
|
|||||||
return array_keys($out);
|
return array_keys($out);
|
||||||
}
|
}
|
||||||
|
|
||||||
$fromRoles = DB::table('admin_user_site_roles as usr')
|
$fromRoles = $this->roleMenuActionPermissionCodes();
|
||||||
->join('admin_role_menu_actions as rma', 'rma.role_id', '=', 'usr.role_id')
|
|
||||||
->join('admin_menu_actions as ma', 'ma.id', '=', 'rma.menu_action_id')
|
|
||||||
->where('usr.admin_user_id', $this->id)
|
|
||||||
->where('ma.status', 1)
|
|
||||||
->pluck('ma.permission_code')
|
|
||||||
->all();
|
|
||||||
|
|
||||||
$merged = [];
|
$merged = [];
|
||||||
foreach (array_merge($fromRoles, $this->directMenuActionPermissionCodes()) as $c) {
|
foreach (array_merge($fromRoles, $this->directMenuActionPermissionCodes()) as $c) {
|
||||||
|
|||||||
82
app/Models/AgentNode.php
Normal file
82
app/Models/AgentNode.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
|
final class AgentNode extends Model
|
||||||
|
{
|
||||||
|
protected $table = 'agent_nodes';
|
||||||
|
|
||||||
|
protected $fillable = [
|
||||||
|
'admin_site_id',
|
||||||
|
'parent_id',
|
||||||
|
'path',
|
||||||
|
'depth',
|
||||||
|
'code',
|
||||||
|
'name',
|
||||||
|
'status',
|
||||||
|
'created_by',
|
||||||
|
'extra_json',
|
||||||
|
];
|
||||||
|
|
||||||
|
protected function casts(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'admin_site_id' => 'integer',
|
||||||
|
'parent_id' => 'integer',
|
||||||
|
'depth' => 'integer',
|
||||||
|
'status' => 'integer',
|
||||||
|
'extra_json' => 'array',
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isEnabled(): bool
|
||||||
|
{
|
||||||
|
return (int) $this->status === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isRoot(): bool
|
||||||
|
{
|
||||||
|
return $this->parent_id === null || (int) $this->depth === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<AdminSite, AgentNode> */
|
||||||
|
public function adminSite(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(AdminSite::class, 'admin_site_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return BelongsTo<AgentNode, AgentNode> */
|
||||||
|
public function parent(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(self::class, 'parent_id');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @return HasMany<AgentNode, AgentNode> */
|
||||||
|
public function children(): HasMany
|
||||||
|
{
|
||||||
|
return $this->hasMany(self::class, 'parent_id')->orderBy('code');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isDescendantOf(self $ancestor): bool
|
||||||
|
{
|
||||||
|
if ((int) $this->admin_site_id !== (int) $ancestor->admin_site_id) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ancestorPath = (string) $ancestor->path;
|
||||||
|
if ($ancestorPath === '') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return str_starts_with((string) $this->path, $ancestorPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isSameOrDescendantOf(self $ancestor): bool
|
||||||
|
{
|
||||||
|
return (int) $this->id === (int) $ancestor->id || $this->isDescendantOf($ancestor);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Models;
|
namespace App\Models;
|
||||||
|
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
use Illuminate\Database\Eloquent\Relations\HasMany;
|
use Illuminate\Database\Eloquent\Relations\HasMany;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -12,6 +13,7 @@ final class Player extends Model
|
|||||||
{
|
{
|
||||||
protected $fillable = [
|
protected $fillable = [
|
||||||
'site_code',
|
'site_code',
|
||||||
|
'agent_node_id',
|
||||||
'site_player_id',
|
'site_player_id',
|
||||||
'username',
|
'username',
|
||||||
'nickname',
|
'nickname',
|
||||||
@@ -23,6 +25,7 @@ final class Player extends Model
|
|||||||
protected function casts(): array
|
protected function casts(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
|
'agent_node_id' => 'integer',
|
||||||
'last_login_at' => 'datetime',
|
'last_login_at' => 'datetime',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
@@ -31,4 +34,9 @@ final class Player extends Model
|
|||||||
{
|
{
|
||||||
return $this->hasMany(PlayerWallet::class);
|
return $this->hasMany(PlayerWallet::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function agentNode(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(AgentNode::class, 'agent_node_id');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ final class AdminDashboardAnalyticsBuilder
|
|||||||
$dateFrom = $range['date_from'];
|
$dateFrom = $range['date_from'];
|
||||||
$dateTo = $range['date_to'];
|
$dateTo = $range['date_to'];
|
||||||
|
|
||||||
$trend = $this->reportQuery->dailyProfitSeriesFilled($dateFrom, $dateTo);
|
$trend = $this->reportQuery->dailyProfitSeriesFilled($dateFrom, $dateTo, scopedAdmin: $admin);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'period' => $period,
|
'period' => $period,
|
||||||
@@ -53,8 +53,8 @@ final class AdminDashboardAnalyticsBuilder
|
|||||||
'play_code' => $playCode,
|
'play_code' => $playCode,
|
||||||
'date_from' => $dateFrom,
|
'date_from' => $dateFrom,
|
||||||
'date_to' => $dateTo,
|
'date_to' => $dateTo,
|
||||||
'currency_code' => $this->reportQuery->resolvePeriodCurrencyCode($dateFrom, $dateTo),
|
'currency_code' => $this->reportQuery->resolvePeriodCurrencyCode($dateFrom, $dateTo, $admin),
|
||||||
'summary' => $this->reportQuery->periodFinanceTotals($dateFrom, $dateTo),
|
'summary' => $this->reportQuery->periodFinanceTotals($dateFrom, $dateTo, $admin),
|
||||||
'daily_series' => $trend['series'],
|
'daily_series' => $trend['series'],
|
||||||
'chart_meta' => [
|
'chart_meta' => [
|
||||||
'chart_date_from' => $trend['chart_date_from'],
|
'chart_date_from' => $trend['chart_date_from'],
|
||||||
@@ -66,6 +66,14 @@ final class AdminDashboardAnalyticsBuilder
|
|||||||
$dateFrom,
|
$dateFrom,
|
||||||
$dateTo,
|
$dateTo,
|
||||||
$playCode,
|
$playCode,
|
||||||
|
scopedAdmin: $admin,
|
||||||
|
),
|
||||||
|
'agent_breakdown' => $this->reportQuery->agentRankingRows(
|
||||||
|
$dateFrom,
|
||||||
|
$dateTo,
|
||||||
|
$playCode,
|
||||||
|
limit: 200,
|
||||||
|
scopedAdmin: $admin,
|
||||||
),
|
),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ use App\Models\SettlementBatch;
|
|||||||
use App\Models\DrawResultBatch;
|
use App\Models\DrawResultBatch;
|
||||||
use App\Lottery\DrawResultBatchStatus;
|
use App\Lottery\DrawResultBatchStatus;
|
||||||
use App\Services\Draw\DrawHallSnapshotBuilder;
|
use App\Services\Draw\DrawHallSnapshotBuilder;
|
||||||
|
use App\Support\AdminDataScope;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 后台首页仪表盘:聚合大厅快照、当期财务、期号面板、风控摘要、异常转账计数。
|
* 后台首页仪表盘:聚合大厅快照、当期财务、期号面板、风控摘要、异常转账计数。
|
||||||
@@ -52,11 +53,11 @@ final class AdminDashboardSnapshotBuilder
|
|||||||
];
|
];
|
||||||
|
|
||||||
if ($canDraw) {
|
if ($canDraw) {
|
||||||
$this->fillPlatformOverview($out);
|
$this->fillPlatformOverview($out, $admin);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($canWallet) {
|
if ($canWallet) {
|
||||||
$out['abnormal_transfer_total'] = $this->abnormalTransferTotal();
|
$out['abnormal_transfer_total'] = $this->abnormalTransferTotal($admin);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($hall === null) {
|
if ($hall === null) {
|
||||||
@@ -81,7 +82,7 @@ final class AdminDashboardSnapshotBuilder
|
|||||||
];
|
];
|
||||||
|
|
||||||
if ($canDraw) {
|
if ($canDraw) {
|
||||||
$out['finance'] = $this->financeSummary($draw);
|
$out['finance'] = $this->financeSummary($draw, $admin);
|
||||||
$out['draw'] = $this->drawPanel($draw);
|
$out['draw'] = $this->drawPanel($draw);
|
||||||
$out['risk'] = $this->riskPanel($draw);
|
$out['risk'] = $this->riskPanel($draw);
|
||||||
}
|
}
|
||||||
@@ -90,10 +91,10 @@ final class AdminDashboardSnapshotBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @param array<string, mixed> $out */
|
/** @param array<string, mixed> $out */
|
||||||
private function fillPlatformOverview(array &$out): void
|
private function fillPlatformOverview(array &$out, AdminUser $admin): void
|
||||||
{
|
{
|
||||||
$out['today_finance'] = $this->todayFinanceSummary();
|
$out['today_finance'] = $this->todayFinanceSummary($admin);
|
||||||
$out['lifetime_finance'] = $this->reportQuery->platformLifetimeTotals();
|
$out['lifetime_finance'] = $this->reportQuery->platformLifetimeTotals($admin);
|
||||||
$out['platform_risk'] = $this->platformRiskSummary();
|
$out['platform_risk'] = $this->platformRiskSummary();
|
||||||
$out['result_batch_queue'] = $this->resultBatchQueue();
|
$out['result_batch_queue'] = $this->resultBatchQueue();
|
||||||
}
|
}
|
||||||
@@ -114,11 +115,13 @@ final class AdminDashboardSnapshotBuilder
|
|||||||
|| $admin->hasAdminPermission('prd.wallet_reconcile.view_cs');
|
|| $admin->hasAdminPermission('prd.wallet_reconcile.view_cs');
|
||||||
}
|
}
|
||||||
|
|
||||||
private function abnormalTransferTotal(): int
|
private function abnormalTransferTotal(AdminUser $admin): int
|
||||||
{
|
{
|
||||||
return (int) TransferOrder::query()
|
$query = TransferOrder::query()
|
||||||
->whereIn('status', ['processing', 'failed', 'pending_reconcile'])
|
->whereIn('status', ['processing', 'failed', 'pending_reconcile']);
|
||||||
->count();
|
AdminDataScope::applyEloquentViaPlayer($query, $admin);
|
||||||
|
|
||||||
|
return (int) $query->count();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -126,10 +129,10 @@ final class AdminDashboardSnapshotBuilder
|
|||||||
*
|
*
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
private function todayFinanceSummary(): array
|
private function todayFinanceSummary(AdminUser $admin): array
|
||||||
{
|
{
|
||||||
$today = now()->toDateString();
|
$today = now()->toDateString();
|
||||||
$rows = $this->reportQuery->dailyProfitRows($today, $today);
|
$rows = $this->reportQuery->dailyProfitRows($today, $today, $admin);
|
||||||
$row = $rows[0] ?? [
|
$row = $rows[0] ?? [
|
||||||
'business_date' => $today,
|
'business_date' => $today,
|
||||||
'total_bet_minor' => 0,
|
'total_bet_minor' => 0,
|
||||||
@@ -152,20 +155,23 @@ final class AdminDashboardSnapshotBuilder
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @return array<string, mixed> */
|
/** @return array<string, mixed> */
|
||||||
private function financeSummary(Draw $draw): array
|
private function financeSummary(Draw $draw, AdminUser $admin): array
|
||||||
{
|
{
|
||||||
$drawId = (int) $draw->id;
|
$drawId = (int) $draw->id;
|
||||||
|
|
||||||
$totalBetMinor = (int) TicketOrder::query()->where('draw_id', $drawId)->sum('total_actual_deduct');
|
$orderQuery = TicketOrder::query()->where('draw_id', $drawId);
|
||||||
$orderCount = (int) TicketOrder::query()->where('draw_id', $drawId)->count();
|
$itemQuery = TicketItem::query()->where('draw_id', $drawId);
|
||||||
$itemCount = (int) TicketItem::query()->where('draw_id', $drawId)->count();
|
AdminDataScope::applyEloquentViaPlayer($orderQuery, $admin);
|
||||||
|
AdminDataScope::applyEloquentViaPlayer($itemQuery, $admin);
|
||||||
|
|
||||||
$currencyCode = (string) (TicketOrder::query()
|
$totalBetMinor = (int) $orderQuery->sum('total_actual_deduct');
|
||||||
->where('draw_id', $drawId)
|
$orderCount = (int) $orderQuery->count();
|
||||||
->value('currency_code') ?? '');
|
$itemCount = (int) $itemQuery->count();
|
||||||
|
|
||||||
$totalWinMinor = (int) TicketItem::query()->where('draw_id', $drawId)->sum('win_amount');
|
$currencyCode = (string) ((clone $orderQuery)->value('currency_code') ?? '');
|
||||||
$totalJackpotWinMinor = (int) TicketItem::query()->where('draw_id', $drawId)->sum('jackpot_win_amount');
|
|
||||||
|
$totalWinMinor = (int) $itemQuery->sum('win_amount');
|
||||||
|
$totalJackpotWinMinor = (int) (clone $itemQuery)->sum('jackpot_win_amount');
|
||||||
$totalPayoutMinor = $totalWinMinor + $totalJackpotWinMinor;
|
$totalPayoutMinor = $totalWinMinor + $totalJackpotWinMinor;
|
||||||
$approxHouseGrossMinor = $totalBetMinor - $totalPayoutMinor;
|
$approxHouseGrossMinor = $totalBetMinor - $totalPayoutMinor;
|
||||||
|
|
||||||
|
|||||||
@@ -68,9 +68,9 @@ final class AdminReportJobService
|
|||||||
/**
|
/**
|
||||||
* @return list<array<int, string|int|float|null>>
|
* @return list<array<int, string|int|float|null>>
|
||||||
*/
|
*/
|
||||||
public function reportRows(string $reportType, ?array $filterJson): array
|
public function reportRows(string $reportType, ?array $filterJson, ?AdminUser $scopedAdmin = null): array
|
||||||
{
|
{
|
||||||
return $this->queryService->reportRows($reportType, $filterJson);
|
return $this->queryService->reportRows($reportType, $filterJson, $scopedAdmin);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function reportLabel(string $reportType): string
|
public function reportLabel(string $reportType): string
|
||||||
|
|||||||
@@ -2,7 +2,9 @@
|
|||||||
|
|
||||||
namespace App\Services\Admin;
|
namespace App\Services\Admin;
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
use App\Models\AuditLog;
|
use App\Models\AuditLog;
|
||||||
|
use App\Support\AdminDataScope;
|
||||||
use App\Models\Draw;
|
use App\Models\Draw;
|
||||||
use App\Models\RiskPool;
|
use App\Models\RiskPool;
|
||||||
use App\Models\RiskPoolLockLog;
|
use App\Models\RiskPoolLockLog;
|
||||||
@@ -109,9 +111,9 @@ final class AdminReportQueryService
|
|||||||
* business_day_count: int
|
* business_day_count: int
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
public function periodFinanceTotals(string $dateFrom, string $dateTo): array
|
public function periodFinanceTotals(string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): array
|
||||||
{
|
{
|
||||||
$rows = $this->dailyProfitRows($dateFrom, $dateTo);
|
$rows = $this->dailyProfitRows($dateFrom, $dateTo, $scopedAdmin);
|
||||||
$totalBet = 0;
|
$totalBet = 0;
|
||||||
$totalPayout = 0;
|
$totalPayout = 0;
|
||||||
$totalGross = 0;
|
$totalGross = 0;
|
||||||
@@ -121,9 +123,11 @@ final class AdminReportQueryService
|
|||||||
$totalGross += (int) $row['approx_house_gross_minor'];
|
$totalGross += (int) $row['approx_house_gross_minor'];
|
||||||
}
|
}
|
||||||
|
|
||||||
$activity = DB::table('draws as d')
|
$activityQuery = DB::table('draws as d')
|
||||||
->join('ticket_orders as o', 'o.draw_id', '=', 'd.id')
|
->join('ticket_orders as o', 'o.draw_id', '=', 'd.id')
|
||||||
->whereBetween('d.business_date', [$dateFrom, $dateTo])
|
->whereBetween('d.business_date', [$dateFrom, $dateTo]);
|
||||||
|
AdminDataScope::applyToTicketOrdersViaPlayer($activityQuery, $scopedAdmin, 'o');
|
||||||
|
$activity = $activityQuery
|
||||||
->selectRaw('COUNT(DISTINCT d.id) as draw_count')
|
->selectRaw('COUNT(DISTINCT d.id) as draw_count')
|
||||||
->selectRaw('COUNT(DISTINCT d.business_date) as business_day_count')
|
->selectRaw('COUNT(DISTINCT d.business_date) as business_day_count')
|
||||||
->first();
|
->first();
|
||||||
@@ -142,7 +146,7 @@ final class AdminReportQueryService
|
|||||||
*
|
*
|
||||||
* @return list<array<string, mixed>>
|
* @return list<array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
public function dailyProfitSeriesFilled(string $dateFrom, string $dateTo, int $maxDays = 90): array
|
public function dailyProfitSeriesFilled(string $dateFrom, string $dateTo, int $maxDays = 90, ?AdminUser $scopedAdmin = null): array
|
||||||
{
|
{
|
||||||
$from = Carbon::parse($dateFrom)->startOfDay();
|
$from = Carbon::parse($dateFrom)->startOfDay();
|
||||||
$to = Carbon::parse($dateTo)->startOfDay();
|
$to = Carbon::parse($dateTo)->startOfDay();
|
||||||
@@ -156,7 +160,7 @@ final class AdminReportQueryService
|
|||||||
$truncated = true;
|
$truncated = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$indexed = collect($this->dailyProfitRows($chartFrom, $chartTo))->keyBy('business_date');
|
$indexed = collect($this->dailyProfitRows($chartFrom, $chartTo, $scopedAdmin))->keyBy('business_date');
|
||||||
$cursor = Carbon::parse($chartFrom)->startOfDay();
|
$cursor = Carbon::parse($chartFrom)->startOfDay();
|
||||||
$end = Carbon::parse($chartTo)->startOfDay();
|
$end = Carbon::parse($chartTo)->startOfDay();
|
||||||
$series = [];
|
$series = [];
|
||||||
@@ -189,8 +193,9 @@ final class AdminReportQueryService
|
|||||||
string $dateTo,
|
string $dateTo,
|
||||||
?string $playCode = null,
|
?string $playCode = null,
|
||||||
int $limit = 12,
|
int $limit = 12,
|
||||||
|
?AdminUser $scopedAdmin = null,
|
||||||
): array {
|
): array {
|
||||||
return $this->playDimensionBaseQuery($playCode, $dateFrom, $dateTo)
|
return $this->playDimensionBaseQuery($playCode, $dateFrom, $dateTo, $scopedAdmin)
|
||||||
->orderByDesc('total_bet_minor')
|
->orderByDesc('total_bet_minor')
|
||||||
->limit($limit)
|
->limit($limit)
|
||||||
->get()
|
->get()
|
||||||
@@ -207,20 +212,78 @@ final class AdminReportQueryService
|
|||||||
->all();
|
->all();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function resolvePeriodCurrencyCode(string $dateFrom, string $dateTo): ?string
|
/**
|
||||||
|
* 仪表盘「代理排行」:按代理子树聚合投注/派彩/盈亏(与 daily-profit 同口径)。
|
||||||
|
*
|
||||||
|
* @return list<array{
|
||||||
|
* agent_node_id: int,
|
||||||
|
* agent_code: string,
|
||||||
|
* agent_name: string,
|
||||||
|
* total_bet_minor: int,
|
||||||
|
* total_payout_minor: int,
|
||||||
|
* approx_house_gross_minor: int
|
||||||
|
* }>
|
||||||
|
*/
|
||||||
|
public function agentRankingRows(
|
||||||
|
string $dateFrom,
|
||||||
|
string $dateTo,
|
||||||
|
?string $playCode = null,
|
||||||
|
int $limit = 200,
|
||||||
|
?AdminUser $scopedAdmin = null,
|
||||||
|
): array {
|
||||||
|
$query = DB::table('ticket_items as ti')
|
||||||
|
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
|
||||||
|
->leftJoin('players as p', 'p.id', '=', 'ti.player_id')
|
||||||
|
->leftJoin('agent_nodes as an', 'an.id', '=', 'p.agent_node_id')
|
||||||
|
->selectRaw('p.agent_node_id as agent_node_id')
|
||||||
|
->selectRaw('an.code as agent_code')
|
||||||
|
->selectRaw('an.name as agent_name')
|
||||||
|
->selectRaw('SUM(ti.actual_deduct_amount) as total_bet_minor')
|
||||||
|
->selectRaw('SUM(ti.win_amount + ti.jackpot_win_amount) as total_payout_minor')
|
||||||
|
->selectRaw('SUM(ti.actual_deduct_amount) - SUM(ti.win_amount + ti.jackpot_win_amount) as approx_house_gross_minor')
|
||||||
|
->whereDate('o.created_at', '>=', $dateFrom)
|
||||||
|
->whereDate('o.created_at', '<=', $dateTo)
|
||||||
|
->whereNotNull('p.agent_node_id')
|
||||||
|
->groupBy('p.agent_node_id', 'an.code', 'an.name');
|
||||||
|
|
||||||
|
if ($playCode !== null && $playCode !== '') {
|
||||||
|
$query->where('ti.play_code', $playCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
AdminDataScope::applyToPlayersAlias($query, $scopedAdmin, 'p');
|
||||||
|
|
||||||
|
return $query
|
||||||
|
->orderByDesc('total_bet_minor')
|
||||||
|
->limit($limit)
|
||||||
|
->get()
|
||||||
|
->map(static function (object $row): array {
|
||||||
|
return [
|
||||||
|
'agent_node_id' => (int) $row->agent_node_id,
|
||||||
|
'agent_code' => (string) ($row->agent_code ?? ''),
|
||||||
|
'agent_name' => (string) ($row->agent_name ?? ''),
|
||||||
|
'total_bet_minor' => (int) $row->total_bet_minor,
|
||||||
|
'total_payout_minor' => (int) $row->total_payout_minor,
|
||||||
|
'approx_house_gross_minor' => (int) $row->approx_house_gross_minor,
|
||||||
|
];
|
||||||
|
})
|
||||||
|
->values()
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function resolvePeriodCurrencyCode(string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): ?string
|
||||||
{
|
{
|
||||||
$currencyCode = (string) (DB::table('ticket_orders as o')
|
$currencyQuery = DB::table('ticket_orders as o')
|
||||||
->join('draws as d', 'd.id', '=', 'o.draw_id')
|
->join('draws as d', 'd.id', '=', 'o.draw_id')
|
||||||
->whereBetween('d.business_date', [$dateFrom, $dateTo])
|
->whereBetween('d.business_date', [$dateFrom, $dateTo]);
|
||||||
->orderByDesc('o.id')
|
AdminDataScope::applyToTicketOrdersViaPlayer($currencyQuery, $scopedAdmin, 'o');
|
||||||
->value('o.currency_code') ?? '');
|
$currencyCode = (string) ($currencyQuery->orderByDesc('o.id')->value('o.currency_code') ?? '');
|
||||||
|
|
||||||
return $currencyCode !== '' ? $currencyCode : null;
|
return $currencyCode !== '' ? $currencyCode : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public function dailyProfitPaginated(string $dateFrom, string $dateTo, int $page, int $perPage): LengthAwarePaginator
|
public function dailyProfitPaginated(string $dateFrom, string $dateTo, int $page, int $perPage, ?AdminUser $scopedAdmin = null): LengthAwarePaginator
|
||||||
{
|
{
|
||||||
$rows = $this->dailyProfitRows($dateFrom, $dateTo);
|
$rows = $this->dailyProfitRows($dateFrom, $dateTo, $scopedAdmin);
|
||||||
$total = count($rows);
|
$total = count($rows);
|
||||||
$offset = max(0, ($page - 1) * $perPage);
|
$offset = max(0, ($page - 1) * $perPage);
|
||||||
$items = array_slice($rows, $offset, $perPage);
|
$items = array_slice($rows, $offset, $perPage);
|
||||||
@@ -233,14 +296,18 @@ final class AdminReportQueryService
|
|||||||
/**
|
/**
|
||||||
* @return list<array<string, mixed>>
|
* @return list<array<string, mixed>>
|
||||||
*/
|
*/
|
||||||
public function dailyProfitRows(string $dateFrom, string $dateTo): array
|
public function dailyProfitRows(string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): array
|
||||||
{
|
{
|
||||||
$betSub = DB::table('ticket_orders')
|
$betSub = DB::table('ticket_orders as o')
|
||||||
->selectRaw('draw_id, SUM(total_actual_deduct) as total_bet_minor')
|
->selectRaw('o.draw_id, SUM(o.total_actual_deduct) as total_bet_minor')
|
||||||
->groupBy('draw_id');
|
->groupBy('o.draw_id');
|
||||||
$payoutSub = DB::table('ticket_items')
|
AdminDataScope::applyToTicketOrdersViaPlayer($betSub, $scopedAdmin, 'o');
|
||||||
->selectRaw('draw_id, SUM(win_amount + jackpot_win_amount) as total_payout_minor')
|
|
||||||
->groupBy('draw_id');
|
$payoutSub = DB::table('ticket_items as ti')
|
||||||
|
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
|
||||||
|
->selectRaw('ti.draw_id, SUM(ti.win_amount + ti.jackpot_win_amount) as total_payout_minor')
|
||||||
|
->groupBy('ti.draw_id');
|
||||||
|
AdminDataScope::applyToTicketOrdersViaPlayer($payoutSub, $scopedAdmin, 'o');
|
||||||
|
|
||||||
return DB::table('draws as d')
|
return DB::table('draws as d')
|
||||||
->whereBetween('d.business_date', [$dateFrom, $dateTo])
|
->whereBetween('d.business_date', [$dateFrom, $dateTo])
|
||||||
@@ -284,19 +351,26 @@ final class AdminReportQueryService
|
|||||||
* date_to: ?string
|
* date_to: ?string
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
public function platformLifetimeTotals(): array
|
public function platformLifetimeTotals(?AdminUser $scopedAdmin = null): array
|
||||||
{
|
{
|
||||||
$totalBetMinor = (int) DB::table('ticket_orders')->sum('total_actual_deduct');
|
$betQuery = DB::table('ticket_orders as o');
|
||||||
|
AdminDataScope::applyToTicketOrdersViaPlayer($betQuery, $scopedAdmin, 'o');
|
||||||
|
$totalBetMinor = (int) $betQuery->sum('o.total_actual_deduct');
|
||||||
|
|
||||||
$payoutAgg = DB::table('ticket_items')
|
$payoutQuery = DB::table('ticket_items as ti')
|
||||||
->selectRaw('COALESCE(SUM(win_amount), 0) as win_minor, COALESCE(SUM(jackpot_win_amount), 0) as jackpot_minor')
|
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id');
|
||||||
|
AdminDataScope::applyToTicketOrdersViaPlayer($payoutQuery, $scopedAdmin, 'o');
|
||||||
|
$payoutAgg = $payoutQuery
|
||||||
|
->selectRaw('COALESCE(SUM(ti.win_amount), 0) as win_minor, COALESCE(SUM(ti.jackpot_win_amount), 0) as jackpot_minor')
|
||||||
->first();
|
->first();
|
||||||
$totalWinMinor = (int) ($payoutAgg->win_minor ?? 0);
|
$totalWinMinor = (int) ($payoutAgg->win_minor ?? 0);
|
||||||
$totalJackpotMinor = (int) ($payoutAgg->jackpot_minor ?? 0);
|
$totalJackpotMinor = (int) ($payoutAgg->jackpot_minor ?? 0);
|
||||||
$totalPayoutMinor = $totalWinMinor + $totalJackpotMinor;
|
$totalPayoutMinor = $totalWinMinor + $totalJackpotMinor;
|
||||||
|
|
||||||
$activity = DB::table('draws as d')
|
$activityQuery = DB::table('draws as d')
|
||||||
->join('ticket_orders as o', 'o.draw_id', '=', 'd.id')
|
->join('ticket_orders as o', 'o.draw_id', '=', 'd.id');
|
||||||
|
AdminDataScope::applyToTicketOrdersViaPlayer($activityQuery, $scopedAdmin, 'o');
|
||||||
|
$activity = $activityQuery
|
||||||
->selectRaw('COUNT(DISTINCT d.id) as draw_count')
|
->selectRaw('COUNT(DISTINCT d.id) as draw_count')
|
||||||
->selectRaw('COUNT(DISTINCT d.business_date) as business_day_count')
|
->selectRaw('COUNT(DISTINCT d.business_date) as business_day_count')
|
||||||
->selectRaw('MIN(d.business_date) as date_from')
|
->selectRaw('MIN(d.business_date) as date_from')
|
||||||
@@ -309,7 +383,15 @@ final class AdminReportQueryService
|
|||||||
$dateFrom = $this->formatBusinessDateValue($activity?->date_from);
|
$dateFrom = $this->formatBusinessDateValue($activity?->date_from);
|
||||||
$dateTo = $this->formatBusinessDateValue($activity?->date_to);
|
$dateTo = $this->formatBusinessDateValue($activity?->date_to);
|
||||||
|
|
||||||
$currencyCode = (string) (DB::table('ticket_orders')->orderByDesc('id')->value('currency_code') ?? '');
|
$currencyQuery = DB::table('ticket_orders as o');
|
||||||
|
AdminDataScope::applyToTicketOrdersViaPlayer($currencyQuery, $scopedAdmin, 'o');
|
||||||
|
$currencyCode = (string) ($currencyQuery->orderByDesc('o.id')->value('o.currency_code') ?? '');
|
||||||
|
|
||||||
|
$orderCountQuery = DB::table('ticket_orders as o');
|
||||||
|
AdminDataScope::applyToTicketOrdersViaPlayer($orderCountQuery, $scopedAdmin, 'o');
|
||||||
|
$itemCountQuery = DB::table('ticket_items as ti')
|
||||||
|
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id');
|
||||||
|
AdminDataScope::applyToTicketOrdersViaPlayer($itemCountQuery, $scopedAdmin, 'o');
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'currency_code' => $currencyCode !== '' ? $currencyCode : null,
|
'currency_code' => $currencyCode !== '' ? $currencyCode : null,
|
||||||
@@ -318,8 +400,8 @@ final class AdminReportQueryService
|
|||||||
'total_jackpot_minor' => $totalJackpotMinor,
|
'total_jackpot_minor' => $totalJackpotMinor,
|
||||||
'total_payout_minor' => $totalPayoutMinor,
|
'total_payout_minor' => $totalPayoutMinor,
|
||||||
'approx_house_gross_minor' => $totalBetMinor - $totalPayoutMinor,
|
'approx_house_gross_minor' => $totalBetMinor - $totalPayoutMinor,
|
||||||
'order_count' => (int) DB::table('ticket_orders')->count(),
|
'order_count' => (int) $orderCountQuery->count(),
|
||||||
'ticket_item_count' => (int) DB::table('ticket_items')->count(),
|
'ticket_item_count' => (int) $itemCountQuery->count('ti.id'),
|
||||||
'draw_count' => $drawCount,
|
'draw_count' => $drawCount,
|
||||||
'business_day_count' => $businessDayCount,
|
'business_day_count' => $businessDayCount,
|
||||||
'date_from' => $dateFrom,
|
'date_from' => $dateFrom,
|
||||||
@@ -333,8 +415,10 @@ final class AdminReportQueryService
|
|||||||
string $dateTo,
|
string $dateTo,
|
||||||
int $page,
|
int $page,
|
||||||
int $perPage,
|
int $perPage,
|
||||||
|
?AdminUser $scopedAdmin = null,
|
||||||
|
?int $requestedAgentNodeId = null,
|
||||||
): LengthAwarePaginator {
|
): LengthAwarePaginator {
|
||||||
$query = $this->playerWinLossBaseQuery($playerId, $dateFrom, $dateTo);
|
$query = $this->playerWinLossBaseQuery($playerId, $dateFrom, $dateTo, $scopedAdmin, $requestedAgentNodeId);
|
||||||
|
|
||||||
return $query->paginate($perPage, ['*'], 'page', $page);
|
return $query->paginate($perPage, ['*'], 'page', $page);
|
||||||
}
|
}
|
||||||
@@ -345,8 +429,9 @@ final class AdminReportQueryService
|
|||||||
string $dateTo,
|
string $dateTo,
|
||||||
int $page,
|
int $page,
|
||||||
int $perPage,
|
int $perPage,
|
||||||
|
?AdminUser $scopedAdmin = null,
|
||||||
): LengthAwarePaginator {
|
): LengthAwarePaginator {
|
||||||
$query = $this->playDimensionBaseQuery($playCode, $dateFrom, $dateTo);
|
$query = $this->playDimensionBaseQuery($playCode, $dateFrom, $dateTo, $scopedAdmin);
|
||||||
|
|
||||||
return $query->paginate($perPage, ['*'], 'page', $page);
|
return $query->paginate($perPage, ['*'], 'page', $page);
|
||||||
}
|
}
|
||||||
@@ -357,8 +442,9 @@ final class AdminReportQueryService
|
|||||||
string $dateTo,
|
string $dateTo,
|
||||||
int $page,
|
int $page,
|
||||||
int $perPage,
|
int $perPage,
|
||||||
|
?AdminUser $scopedAdmin = null,
|
||||||
): LengthAwarePaginator {
|
): LengthAwarePaginator {
|
||||||
$query = $this->rebateCommissionBaseQuery($playCode, $dateFrom, $dateTo);
|
$query = $this->rebateCommissionBaseQuery($playCode, $dateFrom, $dateTo, $scopedAdmin);
|
||||||
|
|
||||||
return $query->paginate($perPage, ['*'], 'page', $page);
|
return $query->paginate($perPage, ['*'], 'page', $page);
|
||||||
}
|
}
|
||||||
@@ -366,21 +452,21 @@ final class AdminReportQueryService
|
|||||||
/**
|
/**
|
||||||
* @return list<array<int, string|int|float|null>>
|
* @return list<array<int, string|int|float|null>>
|
||||||
*/
|
*/
|
||||||
public function reportRows(string $reportType, ?array $filterJson): array
|
public function reportRows(string $reportType, ?array $filterJson, ?AdminUser $scopedAdmin = null): array
|
||||||
{
|
{
|
||||||
$range = $this->resolveDateRange($filterJson);
|
$range = $this->resolveDateRange($filterJson);
|
||||||
$dateFrom = $range['date_from'];
|
$dateFrom = $range['date_from'];
|
||||||
$dateTo = $range['date_to'];
|
$dateTo = $range['date_to'];
|
||||||
|
|
||||||
return match ($reportType) {
|
return match ($reportType) {
|
||||||
'draw_profit_summary' => $this->drawProfitExportRows($filterJson),
|
'draw_profit_summary' => $this->drawProfitExportRows($filterJson, $scopedAdmin),
|
||||||
'daily_profit_summary' => $this->dailyProfitExportRows($dateFrom, $dateTo),
|
'daily_profit_summary' => $this->dailyProfitExportRows($dateFrom, $dateTo, $scopedAdmin),
|
||||||
'player_win_loss' => $this->playerWinLossExportRows($filterJson, $dateFrom, $dateTo),
|
'player_win_loss' => $this->playerWinLossExportRows($filterJson, $dateFrom, $dateTo, $scopedAdmin),
|
||||||
'play_dimension_report' => $this->playDimensionExportRows($filterJson, $dateFrom, $dateTo),
|
'play_dimension_report' => $this->playDimensionExportRows($filterJson, $dateFrom, $dateTo, $scopedAdmin),
|
||||||
'rebate_commission_report' => $this->rebateCommissionExportRows($filterJson, $dateFrom, $dateTo),
|
'rebate_commission_report' => $this->rebateCommissionExportRows($filterJson, $dateFrom, $dateTo, $scopedAdmin),
|
||||||
'audit_operation_report' => $this->auditExportRows($filterJson, $dateFrom, $dateTo),
|
'audit_operation_report' => $this->auditExportRows($filterJson, $dateFrom, $dateTo),
|
||||||
'wallet_transfer_report', 'transfer_orders_daily' => $this->transferOrdersExportRows($filterJson, $dateFrom, $dateTo),
|
'wallet_transfer_report', 'transfer_orders_daily' => $this->transferOrdersExportRows($filterJson, $dateFrom, $dateTo, $scopedAdmin),
|
||||||
'wallet_txns_daily' => $this->walletTxnsExportRows($filterJson, $dateFrom, $dateTo),
|
'wallet_txns_daily' => $this->walletTxnsExportRows($filterJson, $dateFrom, $dateTo, $scopedAdmin),
|
||||||
'hot_number_risk_report' => $this->hotNumberRiskExportRows($filterJson),
|
'hot_number_risk_report' => $this->hotNumberRiskExportRows($filterJson),
|
||||||
'sold_out_number_report' => $this->soldOutNumberExportRows($filterJson),
|
'sold_out_number_report' => $this->soldOutNumberExportRows($filterJson),
|
||||||
default => [
|
default => [
|
||||||
@@ -427,12 +513,12 @@ final class AdminReportQueryService
|
|||||||
/**
|
/**
|
||||||
* @return list<array<int, string|int|float|null>>
|
* @return list<array<int, string|int|float|null>>
|
||||||
*/
|
*/
|
||||||
private function dailyProfitExportRows(string $dateFrom, string $dateTo): array
|
private function dailyProfitExportRows(string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): array
|
||||||
{
|
{
|
||||||
$rows = [
|
$rows = [
|
||||||
['日期', '下注', '派彩', '盈亏'],
|
['日期', '下注', '派彩', '盈亏'],
|
||||||
];
|
];
|
||||||
foreach ($this->dailyProfitRows($dateFrom, $dateTo) as $row) {
|
foreach ($this->dailyProfitRows($dateFrom, $dateTo, $scopedAdmin) as $row) {
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
$row['business_date'],
|
$row['business_date'],
|
||||||
$row['total_bet_minor'],
|
$row['total_bet_minor'],
|
||||||
@@ -447,13 +533,13 @@ final class AdminReportQueryService
|
|||||||
/**
|
/**
|
||||||
* @return list<array<int, string|int|float|null>>
|
* @return list<array<int, string|int|float|null>>
|
||||||
*/
|
*/
|
||||||
private function playerWinLossExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
private function playerWinLossExportRows(?array $filterJson, string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): array
|
||||||
{
|
{
|
||||||
$playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null;
|
$playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null;
|
||||||
$rows = [
|
$rows = [
|
||||||
['玩家ID', '用户名', '下注', '派彩', '净输赢'],
|
['玩家ID', '用户名', '下注', '派彩', '净输赢'],
|
||||||
];
|
];
|
||||||
$items = $this->playerWinLossBaseQuery($playerId, $dateFrom, $dateTo)->get();
|
$items = $this->playerWinLossBaseQuery($playerId, $dateFrom, $dateTo, $scopedAdmin)->get();
|
||||||
foreach ($items as $row) {
|
foreach ($items as $row) {
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
(int) $row->player_id,
|
(int) $row->player_id,
|
||||||
@@ -470,13 +556,13 @@ final class AdminReportQueryService
|
|||||||
/**
|
/**
|
||||||
* @return list<array<int, string|int|float|null>>
|
* @return list<array<int, string|int|float|null>>
|
||||||
*/
|
*/
|
||||||
private function playDimensionExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
private function playDimensionExportRows(?array $filterJson, string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): array
|
||||||
{
|
{
|
||||||
$playCode = isset($filterJson['play_code']) ? trim((string) $filterJson['play_code']) : null;
|
$playCode = isset($filterJson['play_code']) ? trim((string) $filterJson['play_code']) : null;
|
||||||
$rows = [
|
$rows = [
|
||||||
['玩法', '维度', '下注', '派彩', '盈亏'],
|
['玩法', '维度', '下注', '派彩', '盈亏'],
|
||||||
];
|
];
|
||||||
$items = $this->playDimensionBaseQuery($playCode !== '' ? $playCode : null, $dateFrom, $dateTo)->get();
|
$items = $this->playDimensionBaseQuery($playCode !== '' ? $playCode : null, $dateFrom, $dateTo, $scopedAdmin)->get();
|
||||||
foreach ($items as $row) {
|
foreach ($items as $row) {
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
(string) $row->play_code,
|
(string) $row->play_code,
|
||||||
@@ -493,13 +579,13 @@ final class AdminReportQueryService
|
|||||||
/**
|
/**
|
||||||
* @return list<array<int, string|int|float|null>>
|
* @return list<array<int, string|int|float|null>>
|
||||||
*/
|
*/
|
||||||
private function rebateCommissionExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
private function rebateCommissionExportRows(?array $filterJson, string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): array
|
||||||
{
|
{
|
||||||
$playCode = isset($filterJson['play_code']) ? trim((string) $filterJson['play_code']) : null;
|
$playCode = isset($filterJson['play_code']) ? trim((string) $filterJson['play_code']) : null;
|
||||||
$rows = [
|
$rows = [
|
||||||
['玩法', '回水', '订单数', '注单数'],
|
['玩法', '回水', '订单数', '注单数'],
|
||||||
];
|
];
|
||||||
$items = $this->rebateCommissionBaseQuery($playCode !== '' ? $playCode : null, $dateFrom, $dateTo)->get();
|
$items = $this->rebateCommissionBaseQuery($playCode !== '' ? $playCode : null, $dateFrom, $dateTo, $scopedAdmin)->get();
|
||||||
foreach ($items as $row) {
|
foreach ($items as $row) {
|
||||||
$rows[] = [
|
$rows[] = [
|
||||||
(string) $row->play_code,
|
(string) $row->play_code,
|
||||||
@@ -545,30 +631,43 @@ final class AdminReportQueryService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** @return \Illuminate\Database\Query\Builder */
|
/** @return \Illuminate\Database\Query\Builder */
|
||||||
private function playerWinLossBaseQuery(?int $playerId, string $dateFrom, string $dateTo)
|
private function playerWinLossBaseQuery(
|
||||||
{
|
?int $playerId,
|
||||||
|
string $dateFrom,
|
||||||
|
string $dateTo,
|
||||||
|
?AdminUser $scopedAdmin = null,
|
||||||
|
?int $requestedAgentNodeId = null,
|
||||||
|
) {
|
||||||
$query = DB::table('ticket_items as ti')
|
$query = DB::table('ticket_items as ti')
|
||||||
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
|
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
|
||||||
->leftJoin('players as p', 'p.id', '=', 'ti.player_id')
|
->leftJoin('players as p', 'p.id', '=', 'ti.player_id')
|
||||||
|
->leftJoin('agent_nodes as an', 'an.id', '=', 'p.agent_node_id')
|
||||||
->selectRaw('ti.player_id')
|
->selectRaw('ti.player_id')
|
||||||
->selectRaw('p.username as username')
|
->selectRaw('p.username as username')
|
||||||
|
->selectRaw('p.agent_node_id as agent_node_id')
|
||||||
|
->selectRaw('an.code as agent_code')
|
||||||
|
->selectRaw('an.name as agent_name')
|
||||||
->selectRaw('SUM(ti.actual_deduct_amount) as total_bet_minor')
|
->selectRaw('SUM(ti.actual_deduct_amount) as total_bet_minor')
|
||||||
->selectRaw('SUM(ti.win_amount + ti.jackpot_win_amount) as total_payout_minor')
|
->selectRaw('SUM(ti.win_amount + ti.jackpot_win_amount) as total_payout_minor')
|
||||||
->selectRaw('SUM(ti.actual_deduct_amount) - SUM(ti.win_amount + ti.jackpot_win_amount) as net_win_loss_minor')
|
->selectRaw('SUM(ti.actual_deduct_amount) - SUM(ti.win_amount + ti.jackpot_win_amount) as net_win_loss_minor')
|
||||||
->whereDate('o.created_at', '>=', $dateFrom)
|
->whereDate('o.created_at', '>=', $dateFrom)
|
||||||
->whereDate('o.created_at', '<=', $dateTo)
|
->whereDate('o.created_at', '<=', $dateTo)
|
||||||
->groupBy('ti.player_id', 'p.username')
|
->groupBy('ti.player_id', 'p.username', 'p.agent_node_id', 'an.code', 'an.name')
|
||||||
->orderByDesc('net_win_loss_minor');
|
->orderByDesc('net_win_loss_minor');
|
||||||
|
|
||||||
if ($playerId !== null && $playerId > 0) {
|
if ($playerId !== null && $playerId > 0) {
|
||||||
$query->where('ti.player_id', $playerId);
|
$query->where('ti.player_id', $playerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($scopedAdmin !== null) {
|
||||||
|
AdminDataScope::applyToPlayersAlias($query, $scopedAdmin, 'p', $requestedAgentNodeId);
|
||||||
|
}
|
||||||
|
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return \Illuminate\Database\Query\Builder */
|
/** @return \Illuminate\Database\Query\Builder */
|
||||||
private function playDimensionBaseQuery(?string $playCode, string $dateFrom, string $dateTo)
|
private function playDimensionBaseQuery(?string $playCode, string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null)
|
||||||
{
|
{
|
||||||
$query = DB::table('ticket_items as ti')
|
$query = DB::table('ticket_items as ti')
|
||||||
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
|
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
|
||||||
@@ -587,11 +686,13 @@ final class AdminReportQueryService
|
|||||||
$query->where('ti.play_code', $playCode);
|
$query->where('ti.play_code', $playCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AdminDataScope::applyToTicketOrdersViaPlayer($query, $scopedAdmin, 'o');
|
||||||
|
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @return \Illuminate\Database\Query\Builder */
|
/** @return \Illuminate\Database\Query\Builder */
|
||||||
private function rebateCommissionBaseQuery(?string $playCode, string $dateFrom, string $dateTo)
|
private function rebateCommissionBaseQuery(?string $playCode, string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null)
|
||||||
{
|
{
|
||||||
$query = DB::table('ticket_items as ti')
|
$query = DB::table('ticket_items as ti')
|
||||||
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
|
->join('ticket_orders as o', 'o.id', '=', 'ti.order_id')
|
||||||
@@ -608,13 +709,15 @@ final class AdminReportQueryService
|
|||||||
$query->where('ti.play_code', $playCode);
|
$query->where('ti.play_code', $playCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AdminDataScope::applyToTicketOrdersViaPlayer($query, $scopedAdmin, 'o');
|
||||||
|
|
||||||
return $query;
|
return $query;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return list<array<int, string|int|float|null>>
|
* @return list<array<int, string|int|float|null>>
|
||||||
*/
|
*/
|
||||||
private function drawProfitExportRows(?array $filterJson): array
|
private function drawProfitExportRows(?array $filterJson, ?AdminUser $scopedAdmin = null): array
|
||||||
{
|
{
|
||||||
$draw = $this->resolveDrawForReport($filterJson);
|
$draw = $this->resolveDrawForReport($filterJson);
|
||||||
if ($draw === null) {
|
if ($draw === null) {
|
||||||
@@ -622,12 +725,19 @@ final class AdminReportQueryService
|
|||||||
}
|
}
|
||||||
|
|
||||||
$drawId = (int) $draw->id;
|
$drawId = (int) $draw->id;
|
||||||
$totalBetMinor = (int) TicketOrder::query()->where('draw_id', $drawId)->sum('total_actual_deduct');
|
$orderQuery = TicketOrder::query()->where('draw_id', $drawId);
|
||||||
$orderCount = (int) TicketOrder::query()->where('draw_id', $drawId)->count();
|
$itemQuery = TicketItem::query()->where('draw_id', $drawId);
|
||||||
$itemCount = (int) TicketItem::query()->where('draw_id', $drawId)->count();
|
if ($scopedAdmin !== null) {
|
||||||
$currencyCode = (string) (TicketOrder::query()->where('draw_id', $drawId)->value('currency_code') ?? '');
|
AdminDataScope::applyEloquentViaPlayer($orderQuery, $scopedAdmin);
|
||||||
$totalWinMinor = (int) TicketItem::query()->where('draw_id', $drawId)->sum('win_amount');
|
AdminDataScope::applyEloquentViaPlayer($itemQuery, $scopedAdmin);
|
||||||
$totalJackpotWinMinor = (int) TicketItem::query()->where('draw_id', $drawId)->sum('jackpot_win_amount');
|
}
|
||||||
|
|
||||||
|
$totalBetMinor = (int) $orderQuery->sum('total_actual_deduct');
|
||||||
|
$orderCount = (int) $orderQuery->count();
|
||||||
|
$itemCount = (int) $itemQuery->count();
|
||||||
|
$currencyCode = (string) ((clone $orderQuery)->value('currency_code') ?? '');
|
||||||
|
$totalWinMinor = (int) $itemQuery->sum('win_amount');
|
||||||
|
$totalJackpotWinMinor = (int) (clone $itemQuery)->sum('jackpot_win_amount');
|
||||||
$totalPayoutMinor = $totalWinMinor + $totalJackpotWinMinor;
|
$totalPayoutMinor = $totalWinMinor + $totalJackpotWinMinor;
|
||||||
$approxHouseGrossMinor = $totalBetMinor - $totalPayoutMinor;
|
$approxHouseGrossMinor = $totalBetMinor - $totalPayoutMinor;
|
||||||
|
|
||||||
@@ -842,7 +952,7 @@ final class AdminReportQueryService
|
|||||||
/**
|
/**
|
||||||
* @return list<array<int, string|int|float|null>>
|
* @return list<array<int, string|int|float|null>>
|
||||||
*/
|
*/
|
||||||
private function transferOrdersExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
private function transferOrdersExportRows(?array $filterJson, string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): array
|
||||||
{
|
{
|
||||||
$rows = [
|
$rows = [
|
||||||
['转账单号', '玩家ID', '用户名', '昵称', '方向', '币种', '金额', '状态', '外部单号', '失败原因', '创建时间', '完成时间'],
|
['转账单号', '玩家ID', '用户名', '昵称', '方向', '币种', '金额', '状态', '外部单号', '失败原因', '创建时间', '完成时间'],
|
||||||
@@ -852,6 +962,10 @@ final class AdminReportQueryService
|
|||||||
->with(['player:id,username,nickname'])
|
->with(['player:id,username,nickname'])
|
||||||
->orderByDesc('id');
|
->orderByDesc('id');
|
||||||
|
|
||||||
|
if ($scopedAdmin !== null) {
|
||||||
|
AdminDataScope::applyEloquentViaPlayer($query, $scopedAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
$playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null;
|
$playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null;
|
||||||
if ($playerId !== null && $playerId > 0) {
|
if ($playerId !== null && $playerId > 0) {
|
||||||
$query->where('player_id', $playerId);
|
$query->where('player_id', $playerId);
|
||||||
@@ -884,7 +998,7 @@ final class AdminReportQueryService
|
|||||||
/**
|
/**
|
||||||
* @return list<array<int, string|int|float|null>>
|
* @return list<array<int, string|int|float|null>>
|
||||||
*/
|
*/
|
||||||
private function walletTxnsExportRows(?array $filterJson, string $dateFrom, string $dateTo): array
|
private function walletTxnsExportRows(?array $filterJson, string $dateFrom, string $dateTo, ?AdminUser $scopedAdmin = null): array
|
||||||
{
|
{
|
||||||
$rows = [
|
$rows = [
|
||||||
['流水号', '玩家ID', '用户名', '业务类型', '业务单号', '方向', '金额', '变动前余额', '变动后余额', '状态', '外部单号', '备注', '创建时间'],
|
['流水号', '玩家ID', '用户名', '业务类型', '业务单号', '方向', '金额', '变动前余额', '变动后余额', '状态', '外部单号', '备注', '创建时间'],
|
||||||
@@ -894,6 +1008,10 @@ final class AdminReportQueryService
|
|||||||
->with(['player:id,username'])
|
->with(['player:id,username'])
|
||||||
->orderByDesc('id');
|
->orderByDesc('id');
|
||||||
|
|
||||||
|
if ($scopedAdmin !== null) {
|
||||||
|
AdminDataScope::applyEloquentViaPlayer($query, $scopedAdmin);
|
||||||
|
}
|
||||||
|
|
||||||
$playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null;
|
$playerId = isset($filterJson['player_id']) ? (int) $filterJson['player_id'] : null;
|
||||||
if ($playerId !== null && $playerId > 0) {
|
if ($playerId !== null && $playerId > 0) {
|
||||||
$query->where('player_id', $playerId);
|
$query->where('player_id', $playerId);
|
||||||
|
|||||||
85
app/Services/Agent/AgentAdminUserService.php
Normal file
85
app/Services/Agent/AgentAdminUserService.php
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Agent;
|
||||||
|
|
||||||
|
use App\Models\AdminRole;
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
final class AgentAdminUserService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array{
|
||||||
|
* username: string,
|
||||||
|
* nickname: string,
|
||||||
|
* email?: ?string,
|
||||||
|
* password: string,
|
||||||
|
* status?: int,
|
||||||
|
* role_ids?: list<int>
|
||||||
|
* } $payload
|
||||||
|
*/
|
||||||
|
public function createUnderAgent(AgentNode $agent, array $payload): AdminUser
|
||||||
|
{
|
||||||
|
$roleIds = array_values(array_unique(array_map('intval', $payload['role_ids'] ?? [])));
|
||||||
|
$this->assertRolesAssignable($agent, $roleIds);
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($agent, $payload, $roleIds): AdminUser {
|
||||||
|
$user = AdminUser::query()->create([
|
||||||
|
'username' => $payload['username'],
|
||||||
|
'name' => $payload['nickname'],
|
||||||
|
'email' => isset($payload['email']) ? trim((string) $payload['email']) : null,
|
||||||
|
'password' => $payload['password'],
|
||||||
|
'status' => (int) ($payload['status'] ?? 0),
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('admin_user_agents')->insert([
|
||||||
|
'admin_user_id' => $user->id,
|
||||||
|
'agent_node_id' => $agent->id,
|
||||||
|
'is_primary' => true,
|
||||||
|
'granted_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user->syncAgentRoleIds($agent->id, $roleIds);
|
||||||
|
|
||||||
|
return $user->fresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<int> $roleIds
|
||||||
|
*/
|
||||||
|
public function syncRoles(AgentNode $agent, AdminUser $user, array $roleIds): AdminUser
|
||||||
|
{
|
||||||
|
if ((int) $user->primaryAgentNodeId() !== (int) $agent->id) {
|
||||||
|
throw ValidationException::withMessages(['user' => ['agent_mismatch']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$roleIds = array_values(array_unique(array_map('intval', $roleIds)));
|
||||||
|
$this->assertRolesAssignable($agent, $roleIds);
|
||||||
|
$user->syncAgentRoleIds($agent->id, $roleIds);
|
||||||
|
|
||||||
|
return $user->fresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<int> $roleIds
|
||||||
|
*/
|
||||||
|
private function assertRolesAssignable(AgentNode $agent, array $roleIds): void
|
||||||
|
{
|
||||||
|
if ($roleIds === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$validCount = AdminRole::query()
|
||||||
|
->where('scope_type', AdminRole::SCOPE_AGENT)
|
||||||
|
->where('owner_agent_id', $agent->id)
|
||||||
|
->whereIn('id', $roleIds)
|
||||||
|
->count();
|
||||||
|
|
||||||
|
if ($validCount !== count($roleIds)) {
|
||||||
|
throw ValidationException::withMessages(['role_ids' => ['invalid_for_agent']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
98
app/Services/Agent/AgentDelegationService.php
Normal file
98
app/Services/Agent/AgentDelegationService.php
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Agent;
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Support\AgentDelegationAuthorization;
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
final class AgentDelegationService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return list<array{
|
||||||
|
* menu_action_id: int,
|
||||||
|
* permission_code: string,
|
||||||
|
* name: string,
|
||||||
|
* can_delegate: bool
|
||||||
|
* }>
|
||||||
|
*/
|
||||||
|
public function listForChild(AgentNode $child, ?AdminUser $actor = null): array
|
||||||
|
{
|
||||||
|
$rows = DB::table('agent_delegation_grants as g')
|
||||||
|
->join('admin_menu_actions as ma', 'ma.id', '=', 'g.menu_action_id')
|
||||||
|
->where('g.child_agent_id', $child->id)
|
||||||
|
->where('ma.status', 1)
|
||||||
|
->orderBy('ma.permission_code')
|
||||||
|
->get(['g.menu_action_id', 'g.can_delegate', 'ma.permission_code', 'ma.name']);
|
||||||
|
|
||||||
|
if ($rows->isNotEmpty()) {
|
||||||
|
return $rows->map(static fn ($row): array => [
|
||||||
|
'menu_action_id' => (int) $row->menu_action_id,
|
||||||
|
'permission_code' => (string) $row->permission_code,
|
||||||
|
'name' => (string) $row->name,
|
||||||
|
'can_delegate' => (bool) $row->can_delegate,
|
||||||
|
])->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($actor === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
$codes = $actor->effectiveMenuActionPermissionCodes();
|
||||||
|
if ($codes === []) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB::table('admin_menu_actions')
|
||||||
|
->where('status', 1)
|
||||||
|
->whereIn('permission_code', $codes)
|
||||||
|
->orderBy('permission_code')
|
||||||
|
->get(['id', 'permission_code', 'name'])
|
||||||
|
->map(static fn ($row): array => [
|
||||||
|
'menu_action_id' => (int) $row->id,
|
||||||
|
'permission_code' => (string) $row->permission_code,
|
||||||
|
'name' => (string) $row->name,
|
||||||
|
'can_delegate' => false,
|
||||||
|
])
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array{menu_action_id: int, can_delegate?: bool}> $grants
|
||||||
|
* @return list<array{menu_action_id: int, permission_code: string, name: string, can_delegate: bool}>
|
||||||
|
*/
|
||||||
|
public function syncGrants(AdminUser $actor, AgentNode $child, array $grants): array
|
||||||
|
{
|
||||||
|
AgentDelegationAuthorization::assertGrantsAllowed($actor, $child, $grants);
|
||||||
|
|
||||||
|
$parentId = (int) ($child->parent_id ?? 0);
|
||||||
|
if ($parentId <= 0) {
|
||||||
|
throw new \InvalidArgumentException('Child agent must have parent.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$now = Carbon::now();
|
||||||
|
|
||||||
|
DB::transaction(function () use ($actor, $child, $grants, $parentId, $now): void {
|
||||||
|
DB::table('agent_delegation_grants')
|
||||||
|
->where('child_agent_id', $child->id)
|
||||||
|
->delete();
|
||||||
|
|
||||||
|
foreach ($grants as $grant) {
|
||||||
|
DB::table('agent_delegation_grants')->insert([
|
||||||
|
'parent_agent_id' => $parentId,
|
||||||
|
'child_agent_id' => $child->id,
|
||||||
|
'menu_action_id' => (int) $grant['menu_action_id'],
|
||||||
|
'can_delegate' => ! empty($grant['can_delegate']),
|
||||||
|
'granted_by' => $actor->id,
|
||||||
|
'granted_at' => $now,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return $this->listForChild($child->fresh());
|
||||||
|
}
|
||||||
|
}
|
||||||
82
app/Services/Agent/AgentNodeService.php
Normal file
82
app/Services/Agent/AgentNodeService.php
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Agent;
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
final class AgentNodeService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array{parent_id: int, code: string, name: string, status?: int} $payload
|
||||||
|
*/
|
||||||
|
public function createChild(AdminUser $actor, array $payload): AgentNode
|
||||||
|
{
|
||||||
|
$parent = AgentNode::query()->findOrFail((int) $payload['parent_id']);
|
||||||
|
$code = trim((string) $payload['code']);
|
||||||
|
$name = trim((string) $payload['name']);
|
||||||
|
$status = (int) ($payload['status'] ?? 1);
|
||||||
|
|
||||||
|
if ($code === '' || $name === '') {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'code' => ['required'],
|
||||||
|
'name' => ['required'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AgentNode::query()->where('admin_site_id', $parent->admin_site_id)->where('code', $code)->exists()) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'code' => ['unique'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($actor, $parent, $code, $name, $status): AgentNode {
|
||||||
|
$node = AgentNode::query()->create([
|
||||||
|
'admin_site_id' => $parent->admin_site_id,
|
||||||
|
'parent_id' => $parent->id,
|
||||||
|
'path' => '/',
|
||||||
|
'depth' => (int) $parent->depth + 1,
|
||||||
|
'code' => $code,
|
||||||
|
'name' => $name,
|
||||||
|
'status' => $status === 0 ? 0 : 1,
|
||||||
|
'created_by' => $actor->id,
|
||||||
|
'extra_json' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$node->path = (string) $parent->path.$node->id.'/';
|
||||||
|
$node->save();
|
||||||
|
|
||||||
|
return $node->fresh(['adminSite']);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{name?: string, status?: int} $payload
|
||||||
|
*/
|
||||||
|
public function update(AgentNode $node, array $payload): AgentNode
|
||||||
|
{
|
||||||
|
if (array_key_exists('name', $payload)) {
|
||||||
|
$name = trim((string) $payload['name']);
|
||||||
|
if ($name !== '') {
|
||||||
|
$node->name = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('status', $payload)) {
|
||||||
|
$node->status = (int) $payload['status'] === 0 ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$node->save();
|
||||||
|
|
||||||
|
return $node->fresh(['adminSite']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(AgentNode $node): void
|
||||||
|
{
|
||||||
|
DB::transaction(static function () use ($node): void {
|
||||||
|
$node->delete();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
102
app/Services/Agent/AgentRoleService.php
Normal file
102
app/Services/Agent/AgentRoleService.php
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Services\Agent;
|
||||||
|
|
||||||
|
use App\Models\AdminRole;
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Support\AgentRoleAuthorization;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
final class AgentRoleService
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array{slug: string, name: string, description?: ?string, status?: int, permission_slugs?: list<string>} $payload
|
||||||
|
*/
|
||||||
|
public function createForAgent(AdminUser $actor, AgentNode $owner, array $payload): AdminRole
|
||||||
|
{
|
||||||
|
AgentRoleAuthorization::assertSlugsForAgentRole(
|
||||||
|
$actor,
|
||||||
|
$owner,
|
||||||
|
array_values(array_unique($payload['permission_slugs'] ?? [])),
|
||||||
|
);
|
||||||
|
|
||||||
|
$slug = trim((string) $payload['slug']);
|
||||||
|
if (AdminRole::query()
|
||||||
|
->where('owner_agent_id', $owner->id)
|
||||||
|
->where('slug', $slug)
|
||||||
|
->exists()) {
|
||||||
|
throw ValidationException::withMessages(['slug' => ['unique']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB::transaction(function () use ($payload, $owner, $slug): AdminRole {
|
||||||
|
$role = AdminRole::query()->create([
|
||||||
|
'slug' => $slug,
|
||||||
|
'code' => $slug,
|
||||||
|
'name' => trim((string) $payload['name']),
|
||||||
|
'description' => $payload['description'] ?? null,
|
||||||
|
'status' => (int) ($payload['status'] ?? 1) === 0 ? 0 : 1,
|
||||||
|
'is_system' => false,
|
||||||
|
'sort_order' => 0,
|
||||||
|
'scope_type' => AdminRole::SCOPE_AGENT,
|
||||||
|
'owner_agent_id' => $owner->id,
|
||||||
|
'delegated_from_role_id' => null,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$role->syncLegacyPermissionSlugs($payload['permission_slugs'] ?? []);
|
||||||
|
|
||||||
|
return $role->fresh();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array{name?: string, description?: ?string, status?: int} $payload
|
||||||
|
*/
|
||||||
|
public function update(AdminRole $role, array $payload): AdminRole
|
||||||
|
{
|
||||||
|
if (array_key_exists('name', $payload)) {
|
||||||
|
$name = trim((string) $payload['name']);
|
||||||
|
if ($name !== '') {
|
||||||
|
$role->name = $name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('description', $payload)) {
|
||||||
|
$role->description = $payload['description'];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (array_key_exists('status', $payload)) {
|
||||||
|
$role->status = (int) $payload['status'] === 0 ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$role->save();
|
||||||
|
|
||||||
|
return $role->fresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $permissionSlugs
|
||||||
|
*/
|
||||||
|
public function syncPermissions(AdminUser $actor, AdminRole $role, array $permissionSlugs): AdminRole
|
||||||
|
{
|
||||||
|
$owner = AgentNode::query()->findOrFail((int) $role->owner_agent_id);
|
||||||
|
AgentRoleAuthorization::assertSlugsForAgentRole($actor, $owner, $permissionSlugs);
|
||||||
|
$role->syncLegacyPermissionSlugs($permissionSlugs);
|
||||||
|
|
||||||
|
return $role->fresh();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function destroy(AdminRole $role): void
|
||||||
|
{
|
||||||
|
if ($role->is_system) {
|
||||||
|
throw ValidationException::withMessages(['role' => ['system_role']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($role->assignedUserCount() > 0) {
|
||||||
|
throw ValidationException::withMessages(['role' => ['in_use']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$role->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -160,6 +160,43 @@ final class LotterySettings
|
|||||||
Cache::put(self::cacheKey($key), self::normalizeValue($value), self::cacheTtlSeconds());
|
Cache::put(self::cacheKey($key), self::normalizeValue($value), self::cacheTtlSeconds());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 批量写入(管理端一次保存多块配置,避免 N 次 HTTP + 审计)。
|
||||||
|
*
|
||||||
|
* @param list<array{key: string, value: mixed}> $items
|
||||||
|
*/
|
||||||
|
public static function putMany(array $items): void
|
||||||
|
{
|
||||||
|
if ($items === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$keys = array_values(array_unique(array_map(
|
||||||
|
static fn (array $item): string => (string) $item['key'],
|
||||||
|
$items,
|
||||||
|
)));
|
||||||
|
|
||||||
|
/** @var array<string, LotterySetting> $existing */
|
||||||
|
$existing = LotterySetting::query()
|
||||||
|
->whereIn('setting_key', $keys)
|
||||||
|
->get()
|
||||||
|
->keyBy('setting_key')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
foreach ($items as $item) {
|
||||||
|
$key = (string) $item['key'];
|
||||||
|
$value = $item['value'];
|
||||||
|
$row = $existing[$key] ?? null;
|
||||||
|
|
||||||
|
self::put(
|
||||||
|
$key,
|
||||||
|
$value,
|
||||||
|
$row ? (string) $row->group_name : (explode('.', $key)[0] ?? 'general'),
|
||||||
|
$row ? $row->description_zh : null,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static function cacheKey(string $key): string
|
public static function cacheKey(string $key): string
|
||||||
{
|
{
|
||||||
return 'lottery_settings:'.$key;
|
return 'lottery_settings:'.$key;
|
||||||
|
|||||||
92
app/Support/AdminAgentNodeAccess.php
Normal file
92
app/Support/AdminAgentNodeAccess.php
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\AdminSite;
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Lottery\ErrorCode;
|
||||||
|
use App\Support\ApiMessage;
|
||||||
|
use App\Support\ApiResponse;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
|
||||||
|
final class AdminAgentNodeAccess
|
||||||
|
{
|
||||||
|
public static function resolveAdminSiteId(AdminUser $admin, ?int $requestedSiteId): ?int
|
||||||
|
{
|
||||||
|
if ($admin->isSuperAdmin()) {
|
||||||
|
if ($requestedSiteId !== null && $requestedSiteId > 0) {
|
||||||
|
return $requestedSiteId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) (AdminSite::query()->where('is_default', true)->value('id')
|
||||||
|
?? AdminSite::query()->orderBy('id')->value('id'));
|
||||||
|
}
|
||||||
|
|
||||||
|
$actor = AdminAgentScope::primaryAgentNode($admin);
|
||||||
|
if ($actor === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($requestedSiteId !== null && $requestedSiteId > 0 && $requestedSiteId !== (int) $actor->admin_site_id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $actor->admin_site_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function denyUnlessSiteResolved(AdminUser $admin, ?int $siteId): ?JsonResponse
|
||||||
|
{
|
||||||
|
if ($siteId !== null && $siteId > 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiMessage::errorResponse(
|
||||||
|
request(),
|
||||||
|
'admin.agent_site_access_denied',
|
||||||
|
ErrorCode::AdminForbidden->value,
|
||||||
|
null,
|
||||||
|
403,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function denyUnlessNodeVisible(AdminUser $admin, AgentNode $node): ?JsonResponse
|
||||||
|
{
|
||||||
|
if (AdminAgentScope::nodeVisibleTo($admin, $node)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiMessage::errorResponse(
|
||||||
|
request(),
|
||||||
|
'admin.agent_node_access_denied',
|
||||||
|
ErrorCode::AdminForbidden->value,
|
||||||
|
null,
|
||||||
|
403,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function denyUnlessCanManageParent(AdminUser $admin, AgentNode $parent): ?JsonResponse
|
||||||
|
{
|
||||||
|
if (! AdminAgentScope::nodeManageableBy($admin, $parent)) {
|
||||||
|
return ApiMessage::errorResponse(
|
||||||
|
request(),
|
||||||
|
'admin.agent_node_manage_denied',
|
||||||
|
ErrorCode::AdminForbidden->value,
|
||||||
|
null,
|
||||||
|
403,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($parent->isRoot() && ! $admin->isSuperAdmin()) {
|
||||||
|
return ApiMessage::errorResponse(
|
||||||
|
request(),
|
||||||
|
'admin.agent_root_create_denied',
|
||||||
|
ErrorCode::AdminForbidden->value,
|
||||||
|
null,
|
||||||
|
403,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
167
app/Support/AdminAgentScope.php
Normal file
167
app/Support/AdminAgentScope.php
Normal file
@@ -0,0 +1,167 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Models\Player;
|
||||||
|
use Illuminate\Database\Eloquent\Builder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理子树数据范围(P1:节点访问;P2 起叠加玩家 agent_node_id)。
|
||||||
|
*/
|
||||||
|
final class AdminAgentScope
|
||||||
|
{
|
||||||
|
public static function primaryAgentNode(AdminUser $admin): ?AgentNode
|
||||||
|
{
|
||||||
|
if ($admin->isSuperAdmin()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$agentId = $admin->primaryAgentNodeId();
|
||||||
|
if ($agentId === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AgentNode::query()->find($agentId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function nodeVisibleTo(AdminUser $admin, AgentNode $node): bool
|
||||||
|
{
|
||||||
|
if ($admin->isSuperAdmin()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$actor = self::primaryAgentNode($admin);
|
||||||
|
if ($actor === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $node->isSameOrDescendantOf($actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function playerAccessible(AdminUser $admin, Player $player): bool
|
||||||
|
{
|
||||||
|
if ($admin->isSuperAdmin()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$actor = self::primaryAgentNode($admin);
|
||||||
|
if ($actor === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($player->agent_node_id === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$playerAgent = AgentNode::query()->find((int) $player->agent_node_id);
|
||||||
|
if ($playerAgent === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $playerAgent->isSameOrDescendantOf($actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function nodeManageableBy(AdminUser $admin, AgentNode $node): bool
|
||||||
|
{
|
||||||
|
if ($admin->isSuperAdmin()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $admin->hasAdminPermission('prd.agent.manage')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::nodeVisibleTo($admin, $node);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return Builder<AgentNode>
|
||||||
|
*/
|
||||||
|
public static function visibleNodesQuery(AdminUser $admin, int $adminSiteId): Builder
|
||||||
|
{
|
||||||
|
$query = AgentNode::query()
|
||||||
|
->where('admin_site_id', $adminSiteId)
|
||||||
|
->orderBy('path');
|
||||||
|
|
||||||
|
if ($admin->isSuperAdmin()) {
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
$actor = self::primaryAgentNode($admin);
|
||||||
|
if ($actor === null || (int) $actor->admin_site_id !== $adminSiteId) {
|
||||||
|
return $query->whereRaw('0 = 1');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $query->where('path', 'like', $actor->path.'%');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 玩家必须落在当前代理子树(agent_node_id 必填,由迁移回填根代理)。
|
||||||
|
*
|
||||||
|
* @param Builder<Player> $query
|
||||||
|
*/
|
||||||
|
public static function applyToPlayerQuery(Builder $query, AdminUser $admin): void
|
||||||
|
{
|
||||||
|
if ($admin->isSuperAdmin()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$actor = self::primaryAgentNode($admin);
|
||||||
|
if ($actor === null) {
|
||||||
|
$query->whereRaw('0 = 1');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! \Illuminate\Support\Facades\Schema::hasColumn('players', 'agent_node_id')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subtreeIds = AgentNode::query()
|
||||||
|
->where('path', 'like', $actor->path.'%')
|
||||||
|
->pluck('id')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if ($subtreeIds === []) {
|
||||||
|
$query->whereRaw('0 = 1');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->whereIn('agent_node_id', $subtreeIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在已有站点/代理范围上,再按指定节点子树收窄(超管筛选用)。
|
||||||
|
*
|
||||||
|
* @param Builder<Player> $query
|
||||||
|
*/
|
||||||
|
public static function applyRequestedAgentNodeFilter(Builder $query, AdminUser $admin, int $agentNodeId): void
|
||||||
|
{
|
||||||
|
$node = AgentNode::query()->find($agentNodeId);
|
||||||
|
if ($node === null || ! self::nodeVisibleTo($admin, $node)) {
|
||||||
|
$query->whereRaw('0 = 1');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! \Illuminate\Support\Facades\Schema::hasColumn('players', 'agent_node_id')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subtreeIds = AgentNode::query()
|
||||||
|
->where('path', 'like', $node->path.'%')
|
||||||
|
->pluck('id')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if ($subtreeIds === []) {
|
||||||
|
$query->whereRaw('0 = 1');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->whereIn('agent_node_id', $subtreeIds);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
namespace App\Support;
|
namespace App\Support;
|
||||||
|
|
||||||
use App\Models\AdminUser;
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
|
||||||
final class AdminAuthProfile
|
final class AdminAuthProfile
|
||||||
{
|
{
|
||||||
@@ -17,9 +18,21 @@ final class AdminAuthProfile
|
|||||||
* segment: string,
|
* segment: string,
|
||||||
* label: string,
|
* label: string,
|
||||||
* href: string,
|
* href: string,
|
||||||
|
* nav_group: string,
|
||||||
|
* platform_only?: bool,
|
||||||
* activeMatchPrefix?: string,
|
* activeMatchPrefix?: string,
|
||||||
* requiredAny?: list<string>
|
* requiredAny?: list<string>
|
||||||
* }>
|
* }>,
|
||||||
|
* agent: ?array{
|
||||||
|
* id: int,
|
||||||
|
* admin_site_id: int,
|
||||||
|
* path: string,
|
||||||
|
* code: string,
|
||||||
|
* name: string,
|
||||||
|
* depth: int
|
||||||
|
* },
|
||||||
|
* is_super_admin: bool,
|
||||||
|
* delegation_ceiling: list<string>
|
||||||
* }
|
* }
|
||||||
*/
|
*/
|
||||||
public static function fromAdmin(AdminUser $admin): array
|
public static function fromAdmin(AdminUser $admin): array
|
||||||
@@ -33,7 +46,34 @@ final class AdminAuthProfile
|
|||||||
'nickname' => $fresh->name,
|
'nickname' => $fresh->name,
|
||||||
'email' => $fresh->email,
|
'email' => $fresh->email,
|
||||||
'permissions' => $permissionSlugs,
|
'permissions' => $permissionSlugs,
|
||||||
'navigation' => AdminAuthorizationRegistry::visibleNavigationItems($permissionSlugs),
|
'navigation' => AdminAuthorizationRegistry::visibleNavigationItems($permissionSlugs, $fresh),
|
||||||
|
'agent' => self::agentContext($fresh),
|
||||||
|
'is_super_admin' => $fresh->isSuperAdmin(),
|
||||||
|
'delegation_ceiling' => AgentDelegationAuthorization::delegationLegacySlugsForAdminUser($fresh),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return array{id: int, admin_site_id: int, path: string, code: string, name: string, depth: int}|null
|
||||||
|
*/
|
||||||
|
private static function agentContext(AdminUser $admin): ?array
|
||||||
|
{
|
||||||
|
if ($admin->isSuperAdmin()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$node = $admin->primaryAgentNode();
|
||||||
|
if (! $node instanceof AgentNode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'id' => (int) $node->id,
|
||||||
|
'admin_site_id' => (int) $node->admin_site_id,
|
||||||
|
'path' => (string) $node->path,
|
||||||
|
'code' => (string) $node->code,
|
||||||
|
'name' => (string) $node->name,
|
||||||
|
'depth' => (int) $node->depth,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
namespace App\Support;
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
|
||||||
final class AdminAuthorizationRegistry
|
final class AdminAuthorizationRegistry
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
@@ -25,6 +27,13 @@ final class AdminAuthorizationRegistry
|
|||||||
['slug' => 'prd.admin_user.manage', 'name' => '管理员列表·可管理', 'nav_segment' => 'admin_users', 'permission_codes' => ['system.admin_user.manage']],
|
['slug' => 'prd.admin_user.manage', 'name' => '管理员列表·可管理', 'nav_segment' => 'admin_users', 'permission_codes' => ['system.admin_user.manage']],
|
||||||
['slug' => 'prd.admin_role.manage', 'name' => '角色管理·可管理', 'nav_segment' => 'admin_roles', 'permission_codes' => ['system.admin_role.manage']],
|
['slug' => 'prd.admin_role.manage', 'name' => '角色管理·可管理', 'nav_segment' => 'admin_roles', 'permission_codes' => ['system.admin_role.manage']],
|
||||||
|
|
||||||
|
['slug' => 'prd.agent.view', 'name' => '代理管理·查看', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.view']],
|
||||||
|
['slug' => 'prd.agent.manage', 'name' => '代理管理·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.manage']],
|
||||||
|
['slug' => 'prd.agent.role.view', 'name' => '代理角色·查看', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.view']],
|
||||||
|
['slug' => 'prd.agent.role.manage', 'name' => '代理角色·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.manage']],
|
||||||
|
['slug' => 'prd.agent.user.view', 'name' => '代理账号·查看', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.view']],
|
||||||
|
['slug' => 'prd.agent.user.manage', 'name' => '代理账号·可管理', 'nav_segment' => 'agents', 'permission_codes' => ['agent.node.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']],
|
||||||
['slug' => 'prd.users.view_cs', 'name' => '用户管理·客服单用户', 'nav_segment' => 'players', 'permission_codes' => ['service.players.view', 'service.tickets.view']],
|
['slug' => 'prd.users.view_cs', 'name' => '用户管理·客服单用户', 'nav_segment' => 'players', 'permission_codes' => ['service.players.view', 'service.tickets.view']],
|
||||||
@@ -97,6 +106,7 @@ final class AdminAuthorizationRegistry
|
|||||||
'audit' => '审计日志',
|
'audit' => '审计日志',
|
||||||
'settings' => '系统设置',
|
'settings' => '系统设置',
|
||||||
'integration' => '接入站点',
|
'integration' => '接入站点',
|
||||||
|
'agents' => '代理管理',
|
||||||
];
|
];
|
||||||
|
|
||||||
return array_map(
|
return array_map(
|
||||||
@@ -108,13 +118,20 @@ final class AdminAuthorizationRegistry
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 侧栏分组顺序(与 {@see navigationDefinitions} 中 nav_group 一致) */
|
||||||
|
public const NAV_GROUP_ORDER = ['overview', 'agent', 'operations', 'finance', 'rules', 'platform'];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 后台菜单注册表。前端侧栏与面包屑都消费这里派生的结果。
|
* 后台菜单注册表。前端侧栏与面包屑都消费这里派生的结果。
|
||||||
*
|
*
|
||||||
|
* platform_only:仅超管可见(全局 RBAC、接入站点、赔率规则等);代理账号走代理控制台与子级授权。
|
||||||
|
*
|
||||||
* @return list<array{
|
* @return list<array{
|
||||||
* segment: string,
|
* segment: string,
|
||||||
* label: string,
|
* label: string,
|
||||||
* href: string,
|
* href: string,
|
||||||
|
* nav_group: string,
|
||||||
|
* platform_only?: bool,
|
||||||
* activeMatchPrefix?: string,
|
* activeMatchPrefix?: string,
|
||||||
* requiredAny?: list<string>
|
* requiredAny?: list<string>
|
||||||
* }>
|
* }>
|
||||||
@@ -122,30 +139,26 @@ final class AdminAuthorizationRegistry
|
|||||||
public static function navigationDefinitions(): array
|
public static function navigationDefinitions(): array
|
||||||
{
|
{
|
||||||
return [
|
return [
|
||||||
// 总览
|
['segment' => 'dashboard', 'label' => 'Dashboard', 'href' => '/admin', 'nav_group' => 'overview', 'requiredAny' => ['prd.dashboard.view']],
|
||||||
['segment' => 'dashboard', 'label' => 'Dashboard', 'href' => '/admin', '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' => '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', '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', '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', 'requiredAny' => ['prd.users.manage', 'prd.users.view_finance', 'prd.users.view_cs', 'prd.player_freeze.manage']],
|
['segment' => 'settlement', 'label' => 'Settlement', 'href' => '/admin/settlement-batches', 'nav_group' => 'operations', 'requiredAny' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.view']],
|
||||||
// 规则与参数
|
['segment' => 'wallet', 'label' => 'Wallet', 'href' => '/admin/wallet/transactions', 'nav_group' => 'finance', 'activeMatchPrefix' => '/admin/wallet', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage', 'prd.users.manage', 'prd.users.view_finance']],
|
||||||
['segment' => 'rules_plays', 'label' => 'Play rules', 'href' => '/admin/rules/plays', 'requiredAny' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
|
['segment' => 'reconcile', 'label' => 'Reconcile', 'href' => '/admin/reconcile', 'nav_group' => 'finance', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs']],
|
||||||
['segment' => 'rules_odds', 'label' => 'Odds & rebate', 'href' => '/admin/rules/odds', 'requiredAny' => ['prd.odds.manage', 'prd.rebate.manage', 'prd.rebate.view']],
|
['segment' => 'reports', 'label' => 'Reports', 'href' => '/admin/reports', 'nav_group' => 'finance', 'requiredAny' => ['prd.report.view']],
|
||||||
['segment' => 'jackpot', 'label' => 'Jackpot', 'href' => '/admin/jackpot', 'activeMatchPrefix' => '/admin/jackpot', 'requiredAny' => ['prd.jackpot.manage', 'prd.jackpot.view']],
|
['segment' => 'rules_plays', 'label' => 'Play rules', 'href' => '/admin/rules/plays', 'nav_group' => 'rules', 'platform_only' => true, 'requiredAny' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
|
||||||
['segment' => 'risk_cap', 'label' => 'Risk cap rules', 'href' => '/admin/risk/cap', 'activeMatchPrefix' => '/admin/risk/cap', 'requiredAny' => ['prd.risk_cap.manage', 'prd.risk_cap.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' => 'wallet', 'label' => 'Wallet', 'href' => '/admin/wallet/transactions', 'activeMatchPrefix' => '/admin/wallet', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs', 'prd.wallet_adjust.manage', 'prd.users.manage', 'prd.users.view_finance']],
|
['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' => 'settlement', 'label' => 'Settlement', 'href' => '/admin/settlement-batches', 'requiredAny' => ['prd.payout.manage', 'prd.payout.review', 'prd.payout.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' => 'reconcile', 'label' => 'Reconcile', 'href' => '/admin/reconcile', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.wallet_reconcile.view', 'prd.wallet_reconcile.view_cs']],
|
['segment' => 'currencies', 'label' => 'Currencies', 'href' => '/admin/currencies', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.currency.manage']],
|
||||||
['segment' => 'reports', 'label' => 'Reports', 'href' => '/admin/reports', 'requiredAny' => ['prd.report.view']],
|
['segment' => 'admin_users', 'label' => 'Admin Users', 'href' => '/admin/admin-users', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.admin_user.manage']],
|
||||||
['segment' => 'currencies', 'label' => 'Currencies', 'href' => '/admin/currencies', 'requiredAny' => ['prd.currency.manage']],
|
['segment' => 'admin_roles', 'label' => 'Admin Roles', 'href' => '/admin/admin-roles', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.admin_role.manage']],
|
||||||
['segment' => 'integration', 'label' => 'Integration sites', 'href' => '/admin/config/integration-sites', 'activeMatchPrefix' => '/admin/config/integration-sites', 'requiredAny' => AdminPermissionLanguage::requiredAnyPrdSlugs('integration-sites')],
|
['segment' => 'audit', 'label' => 'Audit Logs', 'href' => '/admin/audit-logs', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.audit.view']],
|
||||||
// 权限与系统
|
['segment' => 'settings', 'label' => 'Settings', 'href' => '/admin/settings', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.currency.manage']],
|
||||||
['segment' => 'admin_users', 'label' => 'Admin Users', 'href' => '/admin/admin-users', 'requiredAny' => ['prd.admin_user.manage']],
|
['segment' => 'risk', 'label' => 'Risk', 'href' => '/admin/risk', 'nav_group' => 'platform', 'platform_only' => true, 'requiredAny' => ['prd.risk.view', 'prd.risk.manage']],
|
||||||
['segment' => 'admin_roles', 'label' => 'Admin Roles', 'href' => '/admin/admin-roles', 'requiredAny' => ['prd.admin_role.manage']],
|
|
||||||
['segment' => 'risk', 'label' => 'Risk', 'href' => '/admin/risk', 'requiredAny' => ['prd.risk.view', 'prd.risk.manage']],
|
|
||||||
['segment' => 'audit', 'label' => 'Audit Logs', 'href' => '/admin/audit-logs', 'requiredAny' => ['prd.audit.view']],
|
|
||||||
['segment' => 'settings', 'label' => 'Settings', 'href' => '/admin/settings', 'requiredAny' => ['prd.wallet_reconcile.manage', 'prd.currency.manage']],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -202,6 +215,7 @@ 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'],
|
||||||
'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'],
|
||||||
@@ -254,17 +268,24 @@ final class AdminAuthorizationRegistry
|
|||||||
* segment: string,
|
* segment: string,
|
||||||
* label: string,
|
* label: string,
|
||||||
* href: string,
|
* href: string,
|
||||||
|
* nav_group: string,
|
||||||
|
* platform_only?: bool,
|
||||||
* activeMatchPrefix?: string,
|
* activeMatchPrefix?: string,
|
||||||
* requiredAny?: list<string>
|
* requiredAny?: list<string>
|
||||||
* }>
|
* }>
|
||||||
*/
|
*/
|
||||||
public static function visibleNavigationItems(array $permissionSlugs): array
|
public static function visibleNavigationItems(array $permissionSlugs, ?AdminUser $admin = null): array
|
||||||
{
|
{
|
||||||
$granted = array_fill_keys($permissionSlugs, true);
|
$granted = array_fill_keys($permissionSlugs, true);
|
||||||
|
$isSuperAdmin = $admin === null || $admin->isSuperAdmin();
|
||||||
|
|
||||||
return array_values(array_filter(
|
return array_values(array_filter(
|
||||||
self::navigationItems(),
|
self::navigationItems(),
|
||||||
static function (array $item) use ($granted): bool {
|
static function (array $item) use ($granted, $isSuperAdmin): bool {
|
||||||
|
if (($item['platform_only'] ?? false) && ! $isSuperAdmin) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
$required = $item['requiredAny'] ?? [];
|
$required = $item['requiredAny'] ?? [];
|
||||||
if ($required === []) {
|
if ($required === []) {
|
||||||
return true;
|
return true;
|
||||||
@@ -366,6 +387,26 @@ 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-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']],
|
||||||
|
['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.update', 'module_code' => 'agent', 'name' => '更新代理节点', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}', 'route_name' => 'api.v1.admin.agent-nodes.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
|
||||||
|
['code' => 'admin.agent-nodes.destroy', 'module_code' => 'agent', 'name' => '删除代理节点', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}', 'route_name' => 'api.v1.admin.agent-nodes.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
|
||||||
|
['code' => 'admin.agent-nodes.children', 'module_code' => 'agent', 'name' => '代理直属下级', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/children', 'route_name' => 'api.v1.admin.agent-nodes.children', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']],
|
||||||
|
|
||||||
|
['code' => 'admin.agent-roles.index', 'module_code' => 'agent', 'name' => '代理角色列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/roles', 'route_name' => 'api.v1.admin.agent-roles.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']],
|
||||||
|
['code' => 'admin.agent-roles.store', 'module_code' => 'agent', 'name' => '创建代理角色', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/roles', 'route_name' => 'api.v1.admin.agent-roles.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
|
||||||
|
['code' => 'admin.agent-roles.update', 'module_code' => 'agent', 'name' => '更新代理角色', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-roles/{admin_role}', 'route_name' => 'api.v1.admin.agent-roles.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
|
||||||
|
['code' => 'admin.agent-roles.permissions.sync', 'module_code' => 'agent', 'name' => '代理角色权限同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-roles/{admin_role}/permissions', 'route_name' => 'api.v1.admin.agent-roles.permissions.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
|
||||||
|
['code' => 'admin.agent-roles.destroy', 'module_code' => 'agent', 'name' => '删除代理角色', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/agent-roles/{admin_role}', 'route_name' => 'api.v1.admin.agent-roles.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
|
||||||
|
|
||||||
|
['code' => 'admin.agent-admin-users.index', 'module_code' => 'agent', 'name' => '代理账号列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/admin-users', 'route_name' => 'api.v1.admin.agent-admin-users.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'permission_codes' => ['agent.node.view', 'agent.node.manage']],
|
||||||
|
['code' => 'admin.agent-admin-users.store', 'module_code' => 'agent', 'name' => '创建代理账号', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/agent-nodes/{agent_node}/admin-users', 'route_name' => 'api.v1.admin.agent-admin-users.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['agent.node.manage']],
|
||||||
|
['code' => 'admin.agent-admin-users.roles.sync', 'module_code' => 'agent', 'name' => '代理账号角色同步', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/agent-admin-users/{admin_user}/roles', 'route_name' => 'api.v1.admin.agent-admin-users.roles.sync', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'permission_codes' => ['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.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']],
|
||||||
['code' => 'admin.config.play-versions.index', 'module_code' => 'config', 'name' => '玩法版本列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/play-versions', 'route_name' => 'api.v1.admin.config.play-versions.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
|
['code' => 'admin.config.play-versions.index', 'module_code' => 'config', 'name' => '玩法版本列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/config/play-versions', 'route_name' => 'api.v1.admin.config.play-versions.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.play_switch.manage', 'prd.odds.manage', 'prd.odds.view']],
|
||||||
@@ -387,6 +428,7 @@ final class AdminAuthorizationRegistry
|
|||||||
['code' => 'admin.config.risk-cap-versions.publish', 'module_code' => 'config', 'name' => '发布封顶版本', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/config/risk-cap-versions/{id}/publish', 'route_name' => 'api.v1.admin.config.risk-cap-versions.publish', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.risk_cap.manage']],
|
['code' => 'admin.config.risk-cap-versions.publish', 'module_code' => 'config', 'name' => '发布封顶版本', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/config/risk-cap-versions/{id}/publish', 'route_name' => 'api.v1.admin.config.risk-cap-versions.publish', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.risk_cap.manage']],
|
||||||
['code' => 'admin.config.risk-cap-versions.destroy', 'module_code' => 'config', 'name' => '删除封顶版本', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/config/risk-cap-versions/{id}', 'route_name' => 'api.v1.admin.config.risk-cap-versions.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.risk_cap.manage']],
|
['code' => 'admin.config.risk-cap-versions.destroy', 'module_code' => 'config', 'name' => '删除封顶版本', 'http_method' => 'DELETE', 'uri_pattern' => '/api/v1/admin/config/risk-cap-versions/{id}', 'route_name' => 'api.v1.admin.config.risk-cap-versions.destroy', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.risk_cap.manage']],
|
||||||
['code' => 'admin.settings.index', 'module_code' => 'settings', 'name' => '系统设置列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settings', 'route_name' => 'api.v1.admin.settings.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage', 'prd.rebate.manage', 'prd.rebate.view', 'prd.payout.manage']],
|
['code' => 'admin.settings.index', 'module_code' => 'settings', 'name' => '系统设置列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/settings', 'route_name' => 'api.v1.admin.settings.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage', 'prd.rebate.manage', 'prd.rebate.view', 'prd.payout.manage']],
|
||||||
|
['code' => 'admin.settings.batch-update', 'module_code' => 'settings', 'name' => '系统设置批量更新', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/settings/batch', 'route_name' => 'api.v1.admin.settings.batch-update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage', 'prd.rebate.manage', 'prd.payout.manage']],
|
||||||
['code' => 'admin.settings.update', 'module_code' => 'settings', 'name' => '系统设置更新', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/settings/{key}', 'route_name' => 'api.v1.admin.settings.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage', 'prd.rebate.manage', 'prd.payout.manage']],
|
['code' => 'admin.settings.update', 'module_code' => 'settings', 'name' => '系统设置更新', 'http_method' => 'PUT', 'uri_pattern' => '/api/v1/admin/settings/{key}', 'route_name' => 'api.v1.admin.settings.update', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.wallet_reconcile.manage', 'prd.rebate.manage', 'prd.payout.manage']],
|
||||||
['code' => 'admin.currencies.index', 'module_code' => 'settings', 'name' => '币种列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/currencies', 'route_name' => 'api.v1.admin.currencies.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.currency.manage']],
|
['code' => 'admin.currencies.index', 'module_code' => 'settings', 'name' => '币种列表', 'http_method' => 'GET', 'uri_pattern' => '/api/v1/admin/currencies', 'route_name' => 'api.v1.admin.currencies.index', 'auth_mode' => 'permission_required', 'is_audit_required' => false, 'legacy_permission_slugs' => ['prd.currency.manage']],
|
||||||
['code' => 'admin.currencies.store', 'module_code' => 'settings', 'name' => '创建币种', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/currencies', 'route_name' => 'api.v1.admin.currencies.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.currency.manage']],
|
['code' => 'admin.currencies.store', 'module_code' => 'settings', 'name' => '创建币种', 'http_method' => 'POST', 'uri_pattern' => '/api/v1/admin/currencies', 'route_name' => 'api.v1.admin.currencies.store', 'auth_mode' => 'permission_required', 'is_audit_required' => true, 'legacy_permission_slugs' => ['prd.currency.manage']],
|
||||||
|
|||||||
137
app/Support/AdminDataScope.php
Normal file
137
app/Support/AdminDataScope.php
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use Illuminate\Database\Query\Builder;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 站点 + 代理子树:用于报表等 Query Builder(players 表别名)。
|
||||||
|
*/
|
||||||
|
final class AdminDataScope
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Builder<mixed> $query 已 join `players as {alias}`
|
||||||
|
*/
|
||||||
|
public static function applyToPlayersAlias(
|
||||||
|
Builder $query,
|
||||||
|
AdminUser $admin,
|
||||||
|
string $alias = 'p',
|
||||||
|
?int $requestedAgentNodeId = null,
|
||||||
|
): void {
|
||||||
|
if ($admin->isSuperAdmin()) {
|
||||||
|
if ($requestedAgentNodeId !== null && $requestedAgentNodeId > 0) {
|
||||||
|
self::applyAgentNodeIdOnAlias($query, $admin, $alias, $requestedAgentNodeId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$codes = AdminSiteScope::accessibleSiteCodes($admin);
|
||||||
|
if ($codes !== null) {
|
||||||
|
if ($codes === []) {
|
||||||
|
$query->whereRaw('0 = 1');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->whereIn($alias.'.site_code', $codes);
|
||||||
|
}
|
||||||
|
|
||||||
|
$actor = AdminAgentScope::primaryAgentNode($admin);
|
||||||
|
if ($actor === null) {
|
||||||
|
$query->whereRaw('0 = 1');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! \Illuminate\Support\Facades\Schema::hasColumn('players', 'agent_node_id')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subtreeIds = AgentNode::query()
|
||||||
|
->where('path', 'like', $actor->path.'%')
|
||||||
|
->pluck('id')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if ($subtreeIds === []) {
|
||||||
|
$query->whereRaw('0 = 1');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->whereIn($alias.'.agent_node_id', $subtreeIds);
|
||||||
|
|
||||||
|
if ($requestedAgentNodeId !== null && $requestedAgentNodeId > 0) {
|
||||||
|
self::applyAgentNodeIdOnAlias($query, $admin, $alias, $requestedAgentNodeId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Builder<mixed> $query
|
||||||
|
*/
|
||||||
|
private static function applyAgentNodeIdOnAlias(
|
||||||
|
Builder $query,
|
||||||
|
AdminUser $admin,
|
||||||
|
string $alias,
|
||||||
|
int $agentNodeId,
|
||||||
|
): void {
|
||||||
|
$node = AgentNode::query()->find($agentNodeId);
|
||||||
|
if ($node === null || ! AdminAgentScope::nodeVisibleTo($admin, $node)) {
|
||||||
|
$query->whereRaw('0 = 1');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$subtreeIds = AgentNode::query()
|
||||||
|
->where('path', 'like', $node->path.'%')
|
||||||
|
->pluck('id')
|
||||||
|
->all();
|
||||||
|
|
||||||
|
if ($subtreeIds === []) {
|
||||||
|
$query->whereRaw('0 = 1');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->whereIn($alias.'.agent_node_id', $subtreeIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Eloquent 模型经 player 关联做站点 + 代理子树过滤。
|
||||||
|
*
|
||||||
|
* @param \Illuminate\Database\Eloquent\Builder<mixed> $query
|
||||||
|
*/
|
||||||
|
public static function applyEloquentViaPlayer(
|
||||||
|
\Illuminate\Database\Eloquent\Builder $query,
|
||||||
|
AdminUser $admin,
|
||||||
|
string $relation = 'player',
|
||||||
|
): void {
|
||||||
|
if ($admin->isSuperAdmin()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->whereHas($relation, static function (Builder $playerQuery) use ($admin): void {
|
||||||
|
AdminSiteScope::applyToPlayerQuery($playerQuery, $admin);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 约束 ticket_orders(别名 o)仅统计可见玩家。
|
||||||
|
*
|
||||||
|
* @param Builder<mixed> $query
|
||||||
|
*/
|
||||||
|
public static function applyToTicketOrdersViaPlayer(Builder $query, ?AdminUser $admin, string $orderAlias = 'o'): void
|
||||||
|
{
|
||||||
|
if ($admin === null || $admin->isSuperAdmin()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->whereExists(function (Builder $sub) use ($admin, $orderAlias): void {
|
||||||
|
$sub->from('players as scope_p')
|
||||||
|
->whereColumn('scope_p.id', $orderAlias.'.player_id');
|
||||||
|
self::applyToPlayersAlias($sub, $admin, 'scope_p');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,10 @@ final class AdminRoleApiPresenter
|
|||||||
'status' => (int) $role->status,
|
'status' => (int) $role->status,
|
||||||
'is_system' => (bool) $role->is_system,
|
'is_system' => (bool) $role->is_system,
|
||||||
'sort_order' => (int) $role->sort_order,
|
'sort_order' => (int) $role->sort_order,
|
||||||
|
'scope_type' => (string) ($role->scope_type ?? AdminRole::SCOPE_SYSTEM),
|
||||||
|
'owner_agent_id' => $role->owner_agent_id !== null ? (int) $role->owner_agent_id : null,
|
||||||
|
'delegated_from_role_id' => $role->delegated_from_role_id !== null ? (int) $role->delegated_from_role_id : null,
|
||||||
|
'is_read_only_template' => $role->isReadOnlyTemplate(),
|
||||||
'permission_slugs' => $role->legacyPermissionSlugs(),
|
'permission_slugs' => $role->legacyPermissionSlugs(),
|
||||||
'user_count' => $role->assignedUserCount(),
|
'user_count' => $role->assignedUserCount(),
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -53,7 +53,11 @@ final class AdminSiteScope
|
|||||||
|
|
||||||
public static function playerAccessible(AdminUser $admin, Player $player): bool
|
public static function playerAccessible(AdminUser $admin, Player $player): bool
|
||||||
{
|
{
|
||||||
return self::siteCodeAllowed($admin, (string) $player->site_code);
|
if (! self::siteCodeAllowed($admin, (string) $player->site_code)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AdminAgentScope::playerAccessible($admin, $player);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,6 +67,8 @@ final class AdminSiteScope
|
|||||||
{
|
{
|
||||||
$codes = self::accessibleSiteCodes($admin);
|
$codes = self::accessibleSiteCodes($admin);
|
||||||
if ($codes === null) {
|
if ($codes === null) {
|
||||||
|
AdminAgentScope::applyToPlayerQuery($query, $admin);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,6 +79,7 @@ final class AdminSiteScope
|
|||||||
}
|
}
|
||||||
|
|
||||||
$query->whereIn('site_code', $codes);
|
$query->whereIn('site_code', $codes);
|
||||||
|
AdminAgentScope::applyToPlayerQuery($query, $admin);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -80,22 +87,28 @@ final class AdminSiteScope
|
|||||||
*
|
*
|
||||||
* @param Builder<Player> $query
|
* @param Builder<Player> $query
|
||||||
*/
|
*/
|
||||||
public static function applyPlayerFilters(Builder $query, AdminUser $admin, ?string $requestedSiteCode): void
|
public static function applyPlayerFilters(
|
||||||
{
|
Builder $query,
|
||||||
|
AdminUser $admin,
|
||||||
|
?string $requestedSiteCode,
|
||||||
|
?int $requestedAgentNodeId = null,
|
||||||
|
): void {
|
||||||
self::applyToPlayerQuery($query, $admin);
|
self::applyToPlayerQuery($query, $admin);
|
||||||
|
|
||||||
$siteCode = is_string($requestedSiteCode) ? trim($requestedSiteCode) : '';
|
$siteCode = is_string($requestedSiteCode) ? trim($requestedSiteCode) : '';
|
||||||
if ($siteCode === '') {
|
if ($siteCode !== '') {
|
||||||
return;
|
if (! self::siteCodeAllowed($admin, $siteCode)) {
|
||||||
|
$query->whereRaw('0 = 1');
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$query->where('site_code', $siteCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (! self::siteCodeAllowed($admin, $siteCode)) {
|
if ($requestedAgentNodeId !== null && $requestedAgentNodeId > 0) {
|
||||||
$query->whereRaw('0 = 1');
|
AdminAgentScope::applyRequestedAgentNodeFilter($query, $admin, $requestedAgentNodeId);
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$query->where('site_code', $siteCode);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,19 +116,12 @@ final class AdminSiteScope
|
|||||||
*/
|
*/
|
||||||
public static function applyViaPlayerRelation(Builder $query, AdminUser $admin, string $relation = 'player'): void
|
public static function applyViaPlayerRelation(Builder $query, AdminUser $admin, string $relation = 'player'): void
|
||||||
{
|
{
|
||||||
$codes = self::accessibleSiteCodes($admin);
|
if ($admin->isSuperAdmin()) {
|
||||||
if ($codes === null) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($codes === []) {
|
$query->whereHas($relation, static function (Builder $playerQuery) use ($admin): void {
|
||||||
$query->whereRaw('0 = 1');
|
self::applyToPlayerQuery($playerQuery, $admin);
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$query->whereHas($relation, static function (Builder $playerQuery) use ($codes): void {
|
|
||||||
$playerQuery->whereIn('site_code', $codes);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,22 +133,27 @@ final class AdminSiteScope
|
|||||||
AdminUser $admin,
|
AdminUser $admin,
|
||||||
?string $requestedSiteCode,
|
?string $requestedSiteCode,
|
||||||
string $relation = 'player',
|
string $relation = 'player',
|
||||||
|
?int $requestedAgentNodeId = null,
|
||||||
): void {
|
): void {
|
||||||
self::applyViaPlayerRelation($query, $admin, $relation);
|
if ($admin->isSuperAdmin()) {
|
||||||
|
$siteCode = is_string($requestedSiteCode) ? trim($requestedSiteCode) : '';
|
||||||
|
$agentNodeId = $requestedAgentNodeId !== null && $requestedAgentNodeId > 0
|
||||||
|
? $requestedAgentNodeId
|
||||||
|
: null;
|
||||||
|
|
||||||
$siteCode = is_string($requestedSiteCode) ? trim($requestedSiteCode) : '';
|
if ($siteCode === '' && $agentNodeId === null) {
|
||||||
if ($siteCode === '') {
|
return;
|
||||||
return;
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (! self::siteCodeAllowed($admin, $siteCode)) {
|
$query->whereHas($relation, static function (Builder $playerQuery) use ($admin, $siteCode, $agentNodeId): void {
|
||||||
$query->whereRaw('0 = 1');
|
self::applyPlayerFilters($playerQuery, $admin, $siteCode !== '' ? $siteCode : null, $agentNodeId);
|
||||||
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$query->whereHas($relation, static function (Builder $playerQuery) use ($siteCode): void {
|
$query->whereHas($relation, static function (Builder $playerQuery) use ($admin, $requestedSiteCode, $requestedAgentNodeId): void {
|
||||||
$playerQuery->where('site_code', $siteCode);
|
self::applyPlayerFilters($playerQuery, $admin, $requestedSiteCode, $requestedAgentNodeId);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
151
app/Support/AgentDelegationAuthorization.php
Normal file
151
app/Support/AgentDelegationAuthorization.php
Normal file
@@ -0,0 +1,151 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
final class AgentDelegationAuthorization
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return list<string> menu_action.permission_code
|
||||||
|
*/
|
||||||
|
public static function delegationMenuActionCodesForAgent(AgentNode $agent): array
|
||||||
|
{
|
||||||
|
if ($agent->isRoot()) {
|
||||||
|
return DB::table('admin_menu_actions')->where('status', 1)->pluck('permission_code')->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
return DB::table('agent_delegation_grants as g')
|
||||||
|
->join('admin_menu_actions as ma', 'ma.id', '=', 'g.menu_action_id')
|
||||||
|
->where('g.child_agent_id', $agent->id)
|
||||||
|
->where('ma.status', 1)
|
||||||
|
->pluck('ma.permission_code')
|
||||||
|
->all();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string> prd.*
|
||||||
|
*/
|
||||||
|
public static function delegationLegacySlugsForAgent(AgentNode $agent): array
|
||||||
|
{
|
||||||
|
$codes = self::delegationMenuActionCodesForAgent($agent);
|
||||||
|
|
||||||
|
return AdminPermissionBridge::legacySlugsGrantedByMenuActionCodes($codes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return list<string> prd.*
|
||||||
|
*/
|
||||||
|
public static function delegationLegacySlugsForAdminUser(AdminUser $admin): array
|
||||||
|
{
|
||||||
|
if ($admin->isSuperAdmin()) {
|
||||||
|
return AdminPermissionBridge::allLegacySlugs();
|
||||||
|
}
|
||||||
|
|
||||||
|
$node = $admin->primaryAgentNode();
|
||||||
|
if ($node === null) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::delegationLegacySlugsForAgent($node);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function childIsManageableBy(AdminUser $admin, AgentNode $child): bool
|
||||||
|
{
|
||||||
|
if ($admin->isSuperAdmin()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! AdminAgentScope::nodeVisibleTo($admin, $child)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parentId = $child->parent_id;
|
||||||
|
if ($parentId === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$actor = AdminAgentScope::primaryAgentNode($admin);
|
||||||
|
if ($actor === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int) $parentId === (int) $actor->id
|
||||||
|
|| AdminAgentScope::nodeManageableBy($admin, AgentNode::query()->find($parentId) ?? $child);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<array{menu_action_id: int, can_delegate?: bool}> $grants
|
||||||
|
*/
|
||||||
|
public static function assertGrantsAllowed(AdminUser $actor, AgentNode $child, array $grants): void
|
||||||
|
{
|
||||||
|
if ($actor->isSuperAdmin()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! self::childIsManageableBy($actor, $child)) {
|
||||||
|
throw ValidationException::withMessages(['child_agent_id' => ['not_manageable']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$actorCodes = $actor->effectiveMenuActionPermissionCodes();
|
||||||
|
$actorCodeSet = array_fill_keys($actorCodes, true);
|
||||||
|
|
||||||
|
$parent = $child->parent;
|
||||||
|
if ($parent === null && $child->parent_id !== null) {
|
||||||
|
$parent = AgentNode::query()->find($child->parent_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
$parentCeiling = $parent !== null
|
||||||
|
? self::delegationMenuActionCodesForAgent($parent)
|
||||||
|
: $actorCodes;
|
||||||
|
|
||||||
|
if ($parent !== null && ! $parent->isRoot() && $parentCeiling === []) {
|
||||||
|
$parentCeiling = $actorCodes;
|
||||||
|
}
|
||||||
|
|
||||||
|
$parentCeilingSet = array_fill_keys($parentCeiling, true);
|
||||||
|
|
||||||
|
foreach ($grants as $grant) {
|
||||||
|
$actionId = (int) ($grant['menu_action_id'] ?? 0);
|
||||||
|
$code = DB::table('admin_menu_actions')->where('id', $actionId)->value('permission_code');
|
||||||
|
if (! is_string($code) || $code === '') {
|
||||||
|
throw ValidationException::withMessages(['grants' => ['invalid_menu_action']]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! isset($actorCodeSet[$code])) {
|
||||||
|
throw ValidationException::withMessages(['grants' => ['exceeds_actor: '.$code]]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($parent !== null && ! $parent->isRoot() && ! isset($parentCeilingSet[$code])) {
|
||||||
|
throw ValidationException::withMessages(['grants' => ['exceeds_parent_ceiling: '.$code]]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $permissionSlugs
|
||||||
|
*/
|
||||||
|
public static function assertRoleSlugsWithinAgentCeiling(
|
||||||
|
AgentNode $ownerAgent,
|
||||||
|
array $permissionSlugs,
|
||||||
|
AdminUser $actor,
|
||||||
|
): void {
|
||||||
|
$ceiling = self::delegationLegacySlugsForAgent($ownerAgent);
|
||||||
|
if ($ceiling === []) {
|
||||||
|
// 尚未配置下放上限:与 P2 一致,仅校验操作者自身权限
|
||||||
|
AgentRoleAuthorization::assertSlugsWithinActor($actor, $permissionSlugs);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$invalid = array_values(array_diff($permissionSlugs, $ceiling));
|
||||||
|
if ($invalid !== []) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'permission_slugs' => ['exceeds_delegation_ceiling: '.implode(', ', $invalid)],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
app/Support/AgentNodeApiPresenter.php
Normal file
33
app/Support/AgentNodeApiPresenter.php
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
|
||||||
|
/** 列表/详情中嵌入的代理节点摘要字段。 */
|
||||||
|
final class AgentNodeApiPresenter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* agent_node_id: ?int,
|
||||||
|
* agent_code: ?string,
|
||||||
|
* agent_name: ?string
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public static function embed(?AgentNode $node): array
|
||||||
|
{
|
||||||
|
if (! $node instanceof AgentNode) {
|
||||||
|
return [
|
||||||
|
'agent_node_id' => null,
|
||||||
|
'agent_code' => null,
|
||||||
|
'agent_name' => null,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
'agent_node_id' => (int) $node->id,
|
||||||
|
'agent_code' => (string) $node->code,
|
||||||
|
'agent_name' => (string) $node->name,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
79
app/Support/AgentNodePresenter.php
Normal file
79
app/Support/AgentNodePresenter.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
|
||||||
|
final class AgentNodePresenter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return array{
|
||||||
|
* id: int,
|
||||||
|
* admin_site_id: int,
|
||||||
|
* parent_id: ?int,
|
||||||
|
* path: string,
|
||||||
|
* depth: int,
|
||||||
|
* code: string,
|
||||||
|
* name: string,
|
||||||
|
* status: int,
|
||||||
|
* is_root: bool
|
||||||
|
* }
|
||||||
|
*/
|
||||||
|
public static function item(AgentNode $node): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'id' => (int) $node->id,
|
||||||
|
'admin_site_id' => (int) $node->admin_site_id,
|
||||||
|
'parent_id' => $node->parent_id !== null ? (int) $node->parent_id : null,
|
||||||
|
'path' => (string) $node->path,
|
||||||
|
'depth' => (int) $node->depth,
|
||||||
|
'code' => (string) $node->code,
|
||||||
|
'name' => (string) $node->name,
|
||||||
|
'status' => (int) $node->status,
|
||||||
|
'is_root' => $node->isRoot(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param iterable<AgentNode> $nodes
|
||||||
|
* @return list<array<string, mixed>>
|
||||||
|
*/
|
||||||
|
public static function tree(iterable $nodes): array
|
||||||
|
{
|
||||||
|
$items = [];
|
||||||
|
$byParent = [];
|
||||||
|
|
||||||
|
foreach ($nodes as $node) {
|
||||||
|
$row = self::item($node);
|
||||||
|
$row['children'] = [];
|
||||||
|
$items[(int) $node->id] = $row;
|
||||||
|
$parentKey = $node->parent_id !== null ? (int) $node->parent_id : 0;
|
||||||
|
$byParent[$parentKey][] = (int) $node->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attach = static function (int $id) use (&$attach, &$items, $byParent): array {
|
||||||
|
$node = $items[$id];
|
||||||
|
foreach ($byParent[$id] ?? [] as $childId) {
|
||||||
|
$node['children'][] = $attach($childId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $node;
|
||||||
|
};
|
||||||
|
|
||||||
|
$ids = array_keys($items);
|
||||||
|
$rootIds = [];
|
||||||
|
foreach ($items as $id => $row) {
|
||||||
|
$parentId = $row['parent_id'];
|
||||||
|
if ($parentId === null || ! in_array((int) $parentId, $ids, true)) {
|
||||||
|
$rootIds[] = (int) $id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$roots = [];
|
||||||
|
foreach ($rootIds as $rootId) {
|
||||||
|
$roots[] = $attach($rootId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $roots;
|
||||||
|
}
|
||||||
|
}
|
||||||
91
app/Support/AgentRoleAuthorization.php
Normal file
91
app/Support/AgentRoleAuthorization.php
Normal file
@@ -0,0 +1,91 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Support;
|
||||||
|
|
||||||
|
use App\Models\AdminRole;
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use Illuminate\Validation\ValidationException;
|
||||||
|
|
||||||
|
final class AgentRoleAuthorization
|
||||||
|
{
|
||||||
|
public static function roleVisibleTo(AdminUser $admin, AdminRole $role): bool
|
||||||
|
{
|
||||||
|
if ($admin->isSuperAdmin()) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($role->scope_type !== AdminRole::SCOPE_AGENT || $role->owner_agent_id === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$owner = AgentNode::query()->find((int) $role->owner_agent_id);
|
||||||
|
if ($owner === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return AdminAgentScope::nodeVisibleTo($admin, $owner);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function roleManageableBy(AdminUser $admin, AdminRole $role): bool
|
||||||
|
{
|
||||||
|
if ($role->delegated_from_role_id !== null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! self::roleVisibleTo($admin, $role)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $admin->isSuperAdmin() || $admin->hasAdminPermission('prd.agent.role.manage');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $permissionSlugs
|
||||||
|
*/
|
||||||
|
public static function assertSlugsWithinActor(AdminUser $actor, array $permissionSlugs): void
|
||||||
|
{
|
||||||
|
if ($actor->isSuperAdmin()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$allowed = $actor->adminPermissionSlugs();
|
||||||
|
$invalid = array_values(array_diff($permissionSlugs, $allowed));
|
||||||
|
if ($invalid !== []) {
|
||||||
|
throw ValidationException::withMessages([
|
||||||
|
'permission_slugs' => ['permission_exceeds_actor: '.implode(', ', $invalid)],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param list<string> $permissionSlugs
|
||||||
|
*/
|
||||||
|
public static function assertSlugsForAgentRole(
|
||||||
|
AdminUser $actor,
|
||||||
|
AgentNode $ownerAgent,
|
||||||
|
array $permissionSlugs,
|
||||||
|
): void {
|
||||||
|
if ($actor->isSuperAdmin()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self::assertSlugsWithinActor($actor, $permissionSlugs);
|
||||||
|
AgentDelegationAuthorization::assertRoleSlugsWithinAgentCeiling($ownerAgent, $permissionSlugs, $actor);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function denyUnlessRoleManageable(AdminUser $admin, AdminRole $role): ?\Illuminate\Http\JsonResponse
|
||||||
|
{
|
||||||
|
if (self::roleManageableBy($admin, $role)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ApiMessage::errorResponse(
|
||||||
|
request(),
|
||||||
|
'admin.agent_role_manage_denied',
|
||||||
|
\App\Lottery\ErrorCode::AdminForbidden->value,
|
||||||
|
null,
|
||||||
|
403,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,8 +24,13 @@ final class PlayerApiPresenter
|
|||||||
'status' => (int) $w->status,
|
'status' => (int) $w->status,
|
||||||
])->values()->all();
|
])->values()->all();
|
||||||
|
|
||||||
|
$agent = $player->relationLoaded('agentNode')
|
||||||
|
? $player->agentNode
|
||||||
|
: ($player->agent_node_id ? $player->agentNode()->first() : null);
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'id' => (int) $player->id,
|
'id' => (int) $player->id,
|
||||||
|
...AgentNodeApiPresenter::embed($agent),
|
||||||
'site_code' => $player->site_code,
|
'site_code' => $player->site_code,
|
||||||
'site_player_id' => $player->site_player_id,
|
'site_player_id' => $player->site_player_id,
|
||||||
'username' => $player->username,
|
'username' => $player->username,
|
||||||
|
|||||||
@@ -43,6 +43,10 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
$middleware->api(prepend: [
|
$middleware->api(prepend: [
|
||||||
NegotiateLotteryLocale::class,
|
NegotiateLotteryLocale::class,
|
||||||
]);
|
]);
|
||||||
|
$middleware->convertEmptyStringsToNull([
|
||||||
|
static fn (Request $request): bool => $request->is('api/v1/admin/settings')
|
||||||
|
|| $request->is('api/v1/admin/settings/*'),
|
||||||
|
]);
|
||||||
$middleware->alias([
|
$middleware->alias([
|
||||||
// 玩家端需登录路由使用;解析 Bearer → Player
|
// 玩家端需登录路由使用;解析 Bearer → Player
|
||||||
'lottery.player' => EnsurePlayerApi::class,
|
'lottery.player' => EnsurePlayerApi::class,
|
||||||
|
|||||||
@@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Support\AdminAuthorizationRegistry;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
private const RESOURCE_CODE = 'admin.settings.batch-update';
|
||||||
|
|
||||||
|
private const CLONE_BINDINGS_FROM = 'admin.settings.update';
|
||||||
|
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
$resource = collect(AdminAuthorizationRegistry::resources())
|
||||||
|
->first(fn (array $item): bool => $item['code'] === self::RESOURCE_CODE);
|
||||||
|
|
||||||
|
if ($resource === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$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();
|
||||||
|
|
||||||
|
$sourceResourceId = DB::table('admin_api_resources')
|
||||||
|
->where('code', self::CLONE_BINDINGS_FROM)
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($sourceResourceId !== null) {
|
||||||
|
$bindings = DB::table('admin_api_resource_bindings')
|
||||||
|
->where('api_resource_id', (int) $sourceResourceId)
|
||||||
|
->get(['menu_action_id']);
|
||||||
|
|
||||||
|
foreach ($bindings as $binding) {
|
||||||
|
DB::table('admin_api_resource_bindings')->insert([
|
||||||
|
'api_resource_id' => (int) $resourceId,
|
||||||
|
'menu_action_id' => (int) $binding->menu_action_id,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasTable('admin_role_api_resources')) {
|
||||||
|
$roleResourceRows = DB::table('admin_role_menu_actions as rma')
|
||||||
|
->join('admin_api_resource_bindings as arb', 'arb.menu_action_id', '=', 'rma.menu_action_id')
|
||||||
|
->where('arb.api_resource_id', (int) $resourceId)
|
||||||
|
->select('rma.role_id')
|
||||||
|
->distinct()
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($roleResourceRows as $row) {
|
||||||
|
DB::table('admin_role_api_resources')->updateOrInsert([
|
||||||
|
'role_id' => (int) $row->role_id,
|
||||||
|
'api_resource_id' => (int) $resourceId,
|
||||||
|
], []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$resourceId = DB::table('admin_api_resources')->where('code', self::RESOURCE_CODE)->value('id');
|
||||||
|
if ($resourceId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasTable('admin_role_api_resources')) {
|
||||||
|
DB::table('admin_role_api_resources')->where('api_resource_id', (int) $resourceId)->delete();
|
||||||
|
}
|
||||||
|
DB::table('admin_api_resource_bindings')->where('api_resource_id', (int) $resourceId)->delete();
|
||||||
|
DB::table('admin_api_resources')->where('id', (int) $resourceId)->delete();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
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_nodes', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('admin_site_id')->constrained('admin_sites')->cascadeOnDelete();
|
||||||
|
$table->foreignId('parent_id')->nullable()->constrained('agent_nodes')->nullOnDelete();
|
||||||
|
$table->string('path', 512);
|
||||||
|
$table->unsignedSmallInteger('depth')->default(0);
|
||||||
|
$table->string('code', 64);
|
||||||
|
$table->string('name', 128);
|
||||||
|
$table->unsignedTinyInteger('status')->default(1)->comment('1=enabled,0=disabled');
|
||||||
|
$table->foreignId('created_by')->nullable()->constrained('admin_users')->nullOnDelete();
|
||||||
|
$table->json('extra_json')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['admin_site_id', 'code'], 'uk_agent_nodes_site_code');
|
||||||
|
$table->index(['admin_site_id', 'parent_id'], 'idx_agent_nodes_site_parent');
|
||||||
|
$table->index('path', 'idx_agent_nodes_path');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('admin_user_agents', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('admin_user_id')->primary()->constrained('admin_users')->cascadeOnDelete();
|
||||||
|
$table->foreignId('agent_node_id')->constrained('agent_nodes')->cascadeOnDelete();
|
||||||
|
$table->boolean('is_primary')->default(true);
|
||||||
|
$table->timestamp('granted_at')->nullable();
|
||||||
|
});
|
||||||
|
|
||||||
|
$this->seedRootAgentNodes();
|
||||||
|
$this->backfillAdminUserAgents();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('admin_user_agents');
|
||||||
|
Schema::dropIfExists('agent_nodes');
|
||||||
|
}
|
||||||
|
|
||||||
|
private function seedRootAgentNodes(): void
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
$sites = DB::table('admin_sites')->orderBy('id')->get(['id', 'code', 'name']);
|
||||||
|
|
||||||
|
foreach ($sites as $site) {
|
||||||
|
if (DB::table('agent_nodes')->where('admin_site_id', (int) $site->id)->where('depth', 0)->exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = 'root-'.(string) $site->code;
|
||||||
|
$nodeId = DB::table('agent_nodes')->insertGetId([
|
||||||
|
'admin_site_id' => (int) $site->id,
|
||||||
|
'parent_id' => null,
|
||||||
|
'path' => '/',
|
||||||
|
'depth' => 0,
|
||||||
|
'code' => $code,
|
||||||
|
'name' => (string) $site->name,
|
||||||
|
'status' => 1,
|
||||||
|
'created_by' => null,
|
||||||
|
'extra_json' => null,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('agent_nodes')->where('id', $nodeId)->update([
|
||||||
|
'path' => '/'.$nodeId.'/',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function backfillAdminUserAgents(): void
|
||||||
|
{
|
||||||
|
$superRoleId = DB::table('admin_roles')->where('slug', 'super_admin')->value('id');
|
||||||
|
$now = Carbon::now();
|
||||||
|
|
||||||
|
$userIds = DB::table('admin_users')->pluck('id');
|
||||||
|
foreach ($userIds as $userId) {
|
||||||
|
$userId = (int) $userId;
|
||||||
|
if (DB::table('admin_user_agents')->where('admin_user_id', $userId)->exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($superRoleId !== null) {
|
||||||
|
$isSuper = DB::table('admin_user_site_roles')
|
||||||
|
->where('admin_user_id', $userId)
|
||||||
|
->where('role_id', (int) $superRoleId)
|
||||||
|
->exists();
|
||||||
|
if ($isSuper) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$siteId = DB::table('admin_user_site_roles')
|
||||||
|
->where('admin_user_id', $userId)
|
||||||
|
->orderBy('site_id')
|
||||||
|
->value('site_id');
|
||||||
|
|
||||||
|
if ($siteId === null) {
|
||||||
|
$siteId = DB::table('admin_sites')->where('is_default', true)->value('id')
|
||||||
|
?? DB::table('admin_sites')->orderBy('id')->value('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($siteId === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rootId = DB::table('agent_nodes')
|
||||||
|
->where('admin_site_id', (int) $siteId)
|
||||||
|
->where('depth', 0)
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
if ($rootId === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('admin_user_agents')->insert([
|
||||||
|
'admin_user_id' => $userId,
|
||||||
|
'agent_node_id' => (int) $rootId,
|
||||||
|
'is_primary' => true,
|
||||||
|
'granted_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,195 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Support\AdminAuthorizationRegistry;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$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;
|
||||||
|
}
|
||||||
|
|
||||||
|
$systemMenuId = DB::table('admin_menus')->where('code', 'system')->value('id');
|
||||||
|
if ($systemMenuId === null) {
|
||||||
|
$systemMenuId = DB::table('admin_menus')->insertGetId([
|
||||||
|
'parent_id' => null,
|
||||||
|
'menu_type' => 'directory',
|
||||||
|
'code' => 'system',
|
||||||
|
'name' => '系统',
|
||||||
|
'path' => null,
|
||||||
|
'route_name' => null,
|
||||||
|
'component' => null,
|
||||||
|
'icon' => null,
|
||||||
|
'active_menu_code' => null,
|
||||||
|
'sort_order' => 90,
|
||||||
|
'is_visible' => true,
|
||||||
|
'is_cache' => false,
|
||||||
|
'is_external' => false,
|
||||||
|
'status' => 1,
|
||||||
|
'meta_json' => null,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$agentMenuId = DB::table('admin_menus')->where('code', 'system.agents')->value('id');
|
||||||
|
if ($agentMenuId === null) {
|
||||||
|
$agentMenuId = DB::table('admin_menus')->insertGetId([
|
||||||
|
'parent_id' => (int) $systemMenuId,
|
||||||
|
'menu_type' => 'page',
|
||||||
|
'code' => 'system.agents',
|
||||||
|
'name' => '代理管理',
|
||||||
|
'path' => '/admin/agents',
|
||||||
|
'route_name' => 'admin.agents',
|
||||||
|
'component' => 'agents/index',
|
||||||
|
'icon' => null,
|
||||||
|
'active_menu_code' => null,
|
||||||
|
'sort_order' => 25,
|
||||||
|
'is_visible' => true,
|
||||||
|
'is_cache' => false,
|
||||||
|
'is_external' => false,
|
||||||
|
'status' => 1,
|
||||||
|
'meta_json' => null,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ([
|
||||||
|
['permission_code' => 'agent.node.view', 'action_id' => (int) $viewActionId, 'name' => '代理节点查看'],
|
||||||
|
['permission_code' => 'agent.node.manage', 'action_id' => (int) $manageActionId, 'name' => '代理节点管理'],
|
||||||
|
] as $row) {
|
||||||
|
if (DB::table('admin_menu_actions')->where('permission_code', $row['permission_code'])->exists()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('admin_menu_actions')->insert([
|
||||||
|
'menu_id' => (int) $agentMenuId,
|
||||||
|
'action_id' => $row['action_id'],
|
||||||
|
'permission_code' => $row['permission_code'],
|
||||||
|
'name' => $row['name'],
|
||||||
|
'status' => 1,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$menuActionIds = DB::table('admin_menu_actions')->pluck('id', 'permission_code');
|
||||||
|
$resources = array_values(array_filter(
|
||||||
|
AdminAuthorizationRegistry::resources(),
|
||||||
|
static fn (array $resource): bool => str_starts_with((string) $resource['code'], 'admin.agent-nodes.'),
|
||||||
|
));
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
$permissionCodes = $resource['permission_codes'] ?? [];
|
||||||
|
foreach ($permissionCodes 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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasTable('admin_role_api_resources')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$superRoleId = DB::table('admin_roles')->where('slug', 'super_admin')->value('id');
|
||||||
|
if ($superRoleId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($resources as $resource) {
|
||||||
|
$resourceId = DB::table('admin_api_resources')->where('code', $resource['code'])->value('id');
|
||||||
|
if ($resourceId === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('admin_role_api_resources')->updateOrInsert([
|
||||||
|
'role_id' => (int) $superRoleId,
|
||||||
|
'api_resource_id' => (int) $resourceId,
|
||||||
|
], []);
|
||||||
|
}
|
||||||
|
|
||||||
|
$menuActionIdList = DB::table('admin_menu_actions')
|
||||||
|
->whereIn('permission_code', ['agent.node.view', 'agent.node.manage'])
|
||||||
|
->pluck('id');
|
||||||
|
|
||||||
|
foreach ($menuActionIdList as $menuActionId) {
|
||||||
|
DB::table('admin_role_menu_actions')->updateOrInsert([
|
||||||
|
'role_id' => (int) $superRoleId,
|
||||||
|
'menu_action_id' => (int) $menuActionId,
|
||||||
|
], []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$codes = [
|
||||||
|
'admin.agent-nodes.tree',
|
||||||
|
'admin.agent-nodes.store',
|
||||||
|
'admin.agent-nodes.show',
|
||||||
|
'admin.agent-nodes.update',
|
||||||
|
'admin.agent-nodes.children',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($codes as $code) {
|
||||||
|
$resourceId = DB::table('admin_api_resources')->where('code', $code)->value('id');
|
||||||
|
if ($resourceId === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasTable('admin_role_api_resources')) {
|
||||||
|
DB::table('admin_role_api_resources')->where('api_resource_id', (int) $resourceId)->delete();
|
||||||
|
}
|
||||||
|
DB::table('admin_api_resource_bindings')->where('api_resource_id', (int) $resourceId)->delete();
|
||||||
|
DB::table('admin_api_resources')->where('id', (int) $resourceId)->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('admin_menu_actions')
|
||||||
|
->whereIn('permission_code', ['agent.node.view', 'agent.node.manage'])
|
||||||
|
->delete();
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
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('admin_roles', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('owner_agent_id')->nullable()->after('sort_order')->constrained('agent_nodes')->nullOnDelete();
|
||||||
|
$table->foreignId('delegated_from_role_id')->nullable()->after('owner_agent_id')->constrained('admin_roles')->nullOnDelete();
|
||||||
|
$table->string('scope_type', 16)->default('system')->after('delegated_from_role_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::create('admin_user_agent_roles', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('admin_user_id')->constrained('admin_users')->cascadeOnDelete();
|
||||||
|
$table->foreignId('agent_node_id')->constrained('agent_nodes')->cascadeOnDelete();
|
||||||
|
$table->foreignId('role_id')->constrained('admin_roles')->cascadeOnDelete();
|
||||||
|
$table->timestamp('granted_at')->nullable();
|
||||||
|
$table->primary(['admin_user_id', 'agent_node_id', 'role_id'], 'pk_admin_user_agent_roles');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::table('players', function (Blueprint $table): void {
|
||||||
|
$table->foreignId('agent_node_id')->nullable()->after('site_code')->constrained('agent_nodes')->nullOnDelete();
|
||||||
|
$table->index(['site_code', 'agent_node_id'], 'idx_players_site_agent');
|
||||||
|
});
|
||||||
|
|
||||||
|
DB::table('admin_roles')->update(['scope_type' => 'system']);
|
||||||
|
|
||||||
|
$this->backfillAdminUserAgentRoles();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('players', function (Blueprint $table): void {
|
||||||
|
$table->dropIndex('idx_players_site_agent');
|
||||||
|
$table->dropConstrainedForeignId('agent_node_id');
|
||||||
|
});
|
||||||
|
|
||||||
|
Schema::dropIfExists('admin_user_agent_roles');
|
||||||
|
|
||||||
|
Schema::table('admin_roles', function (Blueprint $table): void {
|
||||||
|
$table->dropConstrainedForeignId('delegated_from_role_id');
|
||||||
|
$table->dropConstrainedForeignId('owner_agent_id');
|
||||||
|
$table->dropColumn('scope_type');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private function backfillAdminUserAgentRoles(): void
|
||||||
|
{
|
||||||
|
$now = Carbon::now();
|
||||||
|
$rows = DB::table('admin_user_site_roles as usr')
|
||||||
|
->join('admin_user_agents as uaa', 'uaa.admin_user_id', '=', 'usr.admin_user_id')
|
||||||
|
->join('agent_nodes as an', static function ($join): void {
|
||||||
|
$join->on('an.id', '=', 'uaa.agent_node_id')
|
||||||
|
->on('an.admin_site_id', '=', 'usr.site_id');
|
||||||
|
})
|
||||||
|
->select(['usr.admin_user_id', 'uaa.agent_node_id', 'usr.role_id', 'usr.granted_at'])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
foreach ($rows as $row) {
|
||||||
|
DB::table('admin_user_agent_roles')->updateOrInsert(
|
||||||
|
[
|
||||||
|
'admin_user_id' => (int) $row->admin_user_id,
|
||||||
|
'agent_node_id' => (int) $row->agent_node_id,
|
||||||
|
'role_id' => (int) $row->role_id,
|
||||||
|
],
|
||||||
|
[
|
||||||
|
'granted_at' => $row->granted_at ?? $now,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Carbon;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
use App\Support\AdminAuthorizationRegistry;
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 代理角色/账号 API 绑定到已有 agent.node.view / agent.node.manage 动作(同菜单下 action 唯一)。
|
||||||
|
*/
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$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 => str_starts_with((string) $resource['code'], 'admin.agent-roles.')
|
||||||
|
|| str_starts_with((string) $resource['code'], 'admin.agent-admin-users.'),
|
||||||
|
));
|
||||||
|
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! Schema::hasTable('admin_role_api_resources')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$superRoleId = DB::table('admin_roles')->where('slug', 'super_admin')->value('id');
|
||||||
|
if ($superRoleId === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($resources as $resource) {
|
||||||
|
$resourceId = DB::table('admin_api_resources')->where('code', $resource['code'])->value('id');
|
||||||
|
if ($resourceId === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
DB::table('admin_role_api_resources')->updateOrInsert([
|
||||||
|
'role_id' => (int) $superRoleId,
|
||||||
|
'api_resource_id' => (int) $resourceId,
|
||||||
|
], []);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$codes = [
|
||||||
|
'admin.agent-roles.update',
|
||||||
|
'admin.agent-roles.destroy',
|
||||||
|
'admin.agent-roles.permissions.sync',
|
||||||
|
'admin.agent-roles.index',
|
||||||
|
'admin.agent-roles.store',
|
||||||
|
'admin.agent-admin-users.index',
|
||||||
|
'admin.agent-admin-users.store',
|
||||||
|
'admin.agent-admin-users.roles.sync',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($codes as $code) {
|
||||||
|
$resourceId = DB::table('admin_api_resources')->where('code', $code)->value('id');
|
||||||
|
if ($resourceId === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Schema::hasTable('admin_role_api_resources')) {
|
||||||
|
DB::table('admin_role_api_resources')->where('api_resource_id', (int) $resourceId)->delete();
|
||||||
|
}
|
||||||
|
DB::table('admin_api_resource_bindings')->where('api_resource_id', (int) $resourceId)->delete();
|
||||||
|
DB::table('admin_api_resources')->where('id', (int) $resourceId)->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<?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_delegation_grants', function (Blueprint $table): void {
|
||||||
|
$table->id();
|
||||||
|
$table->foreignId('parent_agent_id')->constrained('agent_nodes')->cascadeOnDelete();
|
||||||
|
$table->foreignId('child_agent_id')->constrained('agent_nodes')->cascadeOnDelete();
|
||||||
|
$table->foreignId('menu_action_id')->constrained('admin_menu_actions')->cascadeOnDelete();
|
||||||
|
$table->boolean('can_delegate')->default(false);
|
||||||
|
$table->foreignId('granted_by')->nullable()->constrained('admin_users')->nullOnDelete();
|
||||||
|
$table->timestamp('granted_at')->nullable();
|
||||||
|
$table->timestamps();
|
||||||
|
|
||||||
|
$table->unique(['child_agent_id', 'menu_action_id'], 'uk_agent_delegation_child_action');
|
||||||
|
$table->index(['parent_agent_id', 'child_agent_id'], 'idx_agent_delegation_parent_child');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('agent_delegation_grants');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Support\Facades\DB;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将未归属玩家挂到对应主站根代理(与 agent_nodes depth=0 1:1)。
|
||||||
|
*/
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if (! \Illuminate\Support\Facades\Schema::hasColumn('players', 'agent_node_id')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$roots = DB::table('agent_nodes as an')
|
||||||
|
->join('admin_sites as s', 's.id', '=', 'an.admin_site_id')
|
||||||
|
->where('an.depth', 0)
|
||||||
|
->get(['an.id as root_id', 's.code as site_code']);
|
||||||
|
|
||||||
|
foreach ($roots as $root) {
|
||||||
|
DB::table('players')
|
||||||
|
->where('site_code', (string) $root->site_code)
|
||||||
|
->whereNull('agent_node_id')
|
||||||
|
->update(['agent_node_id' => (int) $root->root_id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
// 不回滚归属,避免误清空业务绑定。
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -19,6 +19,10 @@ return [
|
|||||||
'role_cannot_delete_super_admin' => 'Cannot delete the super admin role.',
|
'role_cannot_delete_super_admin' => 'Cannot delete the super admin role.',
|
||||||
'role_builtin_cannot_delete' => 'Built-in roles cannot be deleted.',
|
'role_builtin_cannot_delete' => 'Built-in roles cannot be deleted.',
|
||||||
'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_node_has_children_cannot_delete' => 'This agent node has child nodes. Delete children first.',
|
||||||
|
'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.',
|
||||||
'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.',
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ return [
|
|||||||
'role_cannot_delete_super_admin' => 'सुपर एडमिन भूमिका मेटाउन मिल्दैन।',
|
'role_cannot_delete_super_admin' => 'सुपर एडमिन भूमिका मेटाउन मिल्दैन।',
|
||||||
'role_builtin_cannot_delete' => 'बिल्ट-इन भूमिका मेटाउन मिल्दैन।',
|
'role_builtin_cannot_delete' => 'बिल्ट-इन भूमिका मेटाउन मिल्दैन।',
|
||||||
'role_has_users_cannot_delete' => 'यो भूमिकामा अझै एडमिन छ, मेटाउन मिल्दैन।',
|
'role_has_users_cannot_delete' => 'यो भूमिकामा अझै एडमिन छ, मेटाउन मिल्दैन।',
|
||||||
|
'agent_root_delete_denied' => 'रुट एजेन्ट नोड मेटाउन मिल्दैन।',
|
||||||
|
'agent_node_has_children_cannot_delete' => 'यस एजेन्ट नोडमा चाइल्ड नोडहरू छन्, पहिले तिनीहरू हटाउनुहोस्।',
|
||||||
|
'agent_node_has_users_cannot_delete' => 'यस एजेन्ट नोडमा अझै एडमिन खाता जोडिएको छ, मेटाउन मिल्दैन।',
|
||||||
|
'agent_node_has_roles_cannot_delete' => 'यस एजेन्ट नोडमा अझै भूमिका जोडिएको छ, मेटाउन मिल्दैन।',
|
||||||
'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' => 'भूमिका व्यवस्थापन केवल सुपर एडमिनले गर्न सक्छ।',
|
||||||
|
|||||||
@@ -19,6 +19,10 @@ return [
|
|||||||
'role_cannot_delete_super_admin' => '不能删除超级管理员角色。',
|
'role_cannot_delete_super_admin' => '不能删除超级管理员角色。',
|
||||||
'role_builtin_cannot_delete' => '系统内置角色不允许删除。',
|
'role_builtin_cannot_delete' => '系统内置角色不允许删除。',
|
||||||
'role_has_users_cannot_delete' => '该角色下仍有关联管理员,不能删除。',
|
'role_has_users_cannot_delete' => '该角色下仍有关联管理员,不能删除。',
|
||||||
|
'agent_root_delete_denied' => '根节点不允许删除。',
|
||||||
|
'agent_node_has_children_cannot_delete' => '该代理节点存在下级代理,请先清空下级后再删除。',
|
||||||
|
'agent_node_has_users_cannot_delete' => '该代理节点下仍有关联账号,不能删除。',
|
||||||
|
'agent_node_has_roles_cannot_delete' => '该代理节点下仍有关联角色,不能删除。',
|
||||||
'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' => '仅超级管理员可管理角色。',
|
||||||
|
|||||||
@@ -33,6 +33,7 @@ Route::prefix('v1')->group(function (): void {
|
|||||||
require __DIR__.'/api/v1/admin/jackpot.php';
|
require __DIR__.'/api/v1/admin/jackpot.php';
|
||||||
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/report.php';
|
require __DIR__.'/api/v1/admin/report.php';
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
57
routes/api/v1/admin/agent.php
Normal file
57
routes/api/v1/admin/agent.php
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Route;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeTreeController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeShowController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeStoreController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeUpdateController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeDestroyController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeChildrenController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeRoleIndexController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeRoleStoreController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentRoleUpdateController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentRolePermissionSyncController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentRoleDestroyController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeAdminUserIndexController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeAdminUserStoreController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentAdminUserRoleSyncController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeDelegationGrantIndexController;
|
||||||
|
use App\Http\Controllers\Api\V1\Admin\Agent\AgentNodeDelegationGrantSyncController;
|
||||||
|
|
||||||
|
Route::middleware('admin.api-resource')
|
||||||
|
->group(function (): void {
|
||||||
|
Route::get('agent-nodes/tree', AgentNodeTreeController::class)
|
||||||
|
->name('api.v1.admin.agent-nodes.tree');
|
||||||
|
Route::post('agent-nodes', AgentNodeStoreController::class)
|
||||||
|
->name('api.v1.admin.agent-nodes.store');
|
||||||
|
Route::get('agent-nodes/{agent_node}/roles', AgentNodeRoleIndexController::class)
|
||||||
|
->name('api.v1.admin.agent-roles.index');
|
||||||
|
Route::post('agent-nodes/{agent_node}/roles', AgentNodeRoleStoreController::class)
|
||||||
|
->name('api.v1.admin.agent-roles.store');
|
||||||
|
Route::get('agent-nodes/{agent_node}/admin-users', AgentNodeAdminUserIndexController::class)
|
||||||
|
->name('api.v1.admin.agent-admin-users.index');
|
||||||
|
Route::post('agent-nodes/{agent_node}/admin-users', AgentNodeAdminUserStoreController::class)
|
||||||
|
->name('api.v1.admin.agent-admin-users.store');
|
||||||
|
Route::get('agent-nodes/{agent_node}/delegation-grants', AgentNodeDelegationGrantIndexController::class)
|
||||||
|
->name('api.v1.admin.agent-delegation-grants.index');
|
||||||
|
Route::put('agent-nodes/{agent_node}/delegation-grants', AgentNodeDelegationGrantSyncController::class)
|
||||||
|
->name('api.v1.admin.agent-delegation-grants.sync');
|
||||||
|
Route::get('agent-nodes/{agent_node}', AgentNodeShowController::class)
|
||||||
|
->name('api.v1.admin.agent-nodes.show');
|
||||||
|
Route::put('agent-nodes/{agent_node}', AgentNodeUpdateController::class)
|
||||||
|
->name('api.v1.admin.agent-nodes.update');
|
||||||
|
Route::delete('agent-nodes/{agent_node}', AgentNodeDestroyController::class)
|
||||||
|
->name('api.v1.admin.agent-nodes.destroy');
|
||||||
|
Route::get('agent-nodes/{agent_node}/children', AgentNodeChildrenController::class)
|
||||||
|
->name('api.v1.admin.agent-nodes.children');
|
||||||
|
|
||||||
|
Route::put('agent-roles/{admin_role}', AgentRoleUpdateController::class)
|
||||||
|
->name('api.v1.admin.agent-roles.update');
|
||||||
|
Route::put('agent-roles/{admin_role}/permissions', AgentRolePermissionSyncController::class)
|
||||||
|
->name('api.v1.admin.agent-roles.permissions.sync');
|
||||||
|
Route::delete('agent-roles/{admin_role}', AgentRoleDestroyController::class)
|
||||||
|
->name('api.v1.admin.agent-roles.destroy');
|
||||||
|
|
||||||
|
Route::put('agent-admin-users/{admin_user}/roles', AgentAdminUserRoleSyncController::class)
|
||||||
|
->name('api.v1.admin.agent-admin-users.roles.sync');
|
||||||
|
});
|
||||||
@@ -126,6 +126,8 @@ Route::middleware('admin.api-resource')
|
|||||||
->group(function (): void {
|
->group(function (): void {
|
||||||
Route::get('/', AdminSettingController::class.'@index')
|
Route::get('/', AdminSettingController::class.'@index')
|
||||||
->name('index');
|
->name('index');
|
||||||
|
Route::put('batch', AdminSettingController::class.'@batchUpdate')
|
||||||
|
->name('batch-update');
|
||||||
Route::put('{key}', AdminSettingController::class.'@update')
|
Route::put('{key}', AdminSettingController::class.'@update')
|
||||||
->name('update');
|
->name('update');
|
||||||
});
|
});
|
||||||
|
|||||||
192
tests/Feature/AdminAgentDelegationApiTest.php
Normal file
192
tests/Feature/AdminAgentDelegationApiTest.php
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
function grantDelegationAgentOperator(AdminUser $admin, AgentNode $agent, bool $manage = true): void
|
||||||
|
{
|
||||||
|
$now = now();
|
||||||
|
$roleId = DB::table('admin_roles')->insertGetId([
|
||||||
|
'slug' => 'agent_ops_'.$admin->id,
|
||||||
|
'code' => 'agent_ops_'.$admin->id,
|
||||||
|
'name' => 'Agent Ops',
|
||||||
|
'description' => null,
|
||||||
|
'status' => 1,
|
||||||
|
'is_system' => false,
|
||||||
|
'sort_order' => 0,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$codes = $manage
|
||||||
|
? ['agent.node.view', 'agent.node.manage']
|
||||||
|
: ['agent.node.view'];
|
||||||
|
|
||||||
|
$actionIds = DB::table('admin_menu_actions')
|
||||||
|
->whereIn('permission_code', $codes)
|
||||||
|
->pluck('id');
|
||||||
|
|
||||||
|
foreach ($actionIds as $actionId) {
|
||||||
|
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' => (int) $agent->admin_site_id,
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'granted_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('admin_user_agents')->insert([
|
||||||
|
'admin_user_id' => $admin->id,
|
||||||
|
'agent_node_id' => (int) $agent->id,
|
||||||
|
'is_primary' => true,
|
||||||
|
'granted_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('parent agent can sync delegation grants for direct child', function (): void {
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
|
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||||
|
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||||
|
$super = AdminUser::query()->create([
|
||||||
|
'username' => 'deleg_super',
|
||||||
|
'name' => 'Super',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($super);
|
||||||
|
|
||||||
|
$parent = $service->createChild($super, [
|
||||||
|
'parent_id' => $rootId,
|
||||||
|
'code' => 'deleg-parent',
|
||||||
|
'name' => 'Deleg Parent',
|
||||||
|
]);
|
||||||
|
$child = $service->createChild($super, [
|
||||||
|
'parent_id' => $parent->id,
|
||||||
|
'code' => 'deleg-child',
|
||||||
|
'name' => 'Deleg Child',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$parentAdmin = AdminUser::query()->create([
|
||||||
|
'username' => 'deleg_parent_admin',
|
||||||
|
'name' => 'Parent Admin',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantDelegationAgentOperator($parentAdmin, $parent, manage: true);
|
||||||
|
|
||||||
|
$viewActionId = (int) DB::table('admin_menu_actions')
|
||||||
|
->where('permission_code', 'agent.node.view')
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
$token = $parentAdmin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->putJson('/api/v1/admin/agent-nodes/'.$child->id.'/delegation-grants', [
|
||||||
|
'grants' => [
|
||||||
|
['menu_action_id' => $viewActionId, 'can_delegate' => true],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.child_agent_id', $child->id)
|
||||||
|
->assertJsonCount(1, 'data.grants');
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->getJson('/api/v1/admin/agent-nodes/'.$child->id.'/delegation-grants')
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.grants.0.can_delegate', true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('delegation ceiling blocks role permissions beyond child grants', function (): void {
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
|
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||||
|
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||||
|
$super = AdminUser::query()->create([
|
||||||
|
'username' => 'ceil_super',
|
||||||
|
'name' => 'Super',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($super);
|
||||||
|
|
||||||
|
$branch = $service->createChild($super, [
|
||||||
|
'parent_id' => $rootId,
|
||||||
|
'code' => 'ceil-branch',
|
||||||
|
'name' => 'Ceil Branch',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$viewActionId = (int) DB::table('admin_menu_actions')
|
||||||
|
->where('permission_code', 'agent.node.view')
|
||||||
|
->value('id');
|
||||||
|
|
||||||
|
DB::table('agent_delegation_grants')->insert([
|
||||||
|
'parent_agent_id' => $rootId,
|
||||||
|
'child_agent_id' => $branch->id,
|
||||||
|
'menu_action_id' => $viewActionId,
|
||||||
|
'can_delegate' => true,
|
||||||
|
'granted_at' => now(),
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$operator = AdminUser::query()->create([
|
||||||
|
'username' => 'ceil_ops',
|
||||||
|
'name' => 'Ops',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantDelegationAgentOperator($operator, $branch, manage: true);
|
||||||
|
$token = $operator->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->postJson('/api/v1/admin/agent-nodes/'.$branch->id.'/roles', [
|
||||||
|
'slug' => 'ceil_ok',
|
||||||
|
'name' => 'OK',
|
||||||
|
'permission_slugs' => ['prd.agent.view'],
|
||||||
|
])
|
||||||
|
->assertOk();
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->postJson('/api/v1/admin/agent-nodes/'.$branch->id.'/roles', [
|
||||||
|
'slug' => 'ceil_bad',
|
||||||
|
'name' => 'Bad',
|
||||||
|
'permission_slugs' => ['prd.dashboard.view'],
|
||||||
|
])
|
||||||
|
->assertStatus(422);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('auth me includes delegation ceiling for agent user', function (): void {
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
|
$root = AgentNode::query()->where('admin_site_id', $siteId)->where('depth', 0)->firstOrFail();
|
||||||
|
$admin = AdminUser::query()->create([
|
||||||
|
'username' => 'me_deleg',
|
||||||
|
'name' => 'Me',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantDelegationAgentOperator($admin, $root, manage: false);
|
||||||
|
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->getJson('/api/v1/admin/auth/me')
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonStructure(['data' => ['admin' => ['delegation_ceiling']]]);
|
||||||
|
});
|
||||||
280
tests/Feature/AdminAgentNodeApiTest.php
Normal file
280
tests/Feature/AdminAgentNodeApiTest.php
Normal file
@@ -0,0 +1,280 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
function agentRootNodeId(int $siteId): int
|
||||||
|
{
|
||||||
|
return (int) DB::table('agent_nodes')
|
||||||
|
->where('admin_site_id', $siteId)
|
||||||
|
->where('depth', 0)
|
||||||
|
->value('id');
|
||||||
|
}
|
||||||
|
|
||||||
|
function grantAgentOperatorRole(AdminUser $admin, AgentNode $agent, bool $manage = true): void
|
||||||
|
{
|
||||||
|
$now = now();
|
||||||
|
$roleId = DB::table('admin_roles')->insertGetId([
|
||||||
|
'slug' => 'agent_ops_'.$admin->id,
|
||||||
|
'code' => 'agent_ops_'.$admin->id,
|
||||||
|
'name' => 'Agent Ops',
|
||||||
|
'description' => null,
|
||||||
|
'status' => 1,
|
||||||
|
'is_system' => false,
|
||||||
|
'sort_order' => 0,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$codes = $manage
|
||||||
|
? ['agent.node.view', 'agent.node.manage']
|
||||||
|
: ['agent.node.view'];
|
||||||
|
|
||||||
|
$actionIds = DB::table('admin_menu_actions')
|
||||||
|
->whereIn('permission_code', $codes)
|
||||||
|
->pluck('id');
|
||||||
|
|
||||||
|
foreach ($actionIds as $actionId) {
|
||||||
|
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' => (int) $agent->admin_site_id,
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'granted_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('admin_user_agents')->insert([
|
||||||
|
'admin_user_id' => $admin->id,
|
||||||
|
'agent_node_id' => (int) $agent->id,
|
||||||
|
'is_primary' => true,
|
||||||
|
'granted_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('each admin site has exactly one root agent node after migration', function (): void {
|
||||||
|
$siteIds = DB::table('admin_sites')->pluck('id');
|
||||||
|
foreach ($siteIds as $siteId) {
|
||||||
|
expect(
|
||||||
|
DB::table('agent_nodes')->where('admin_site_id', (int) $siteId)->where('depth', 0)->count()
|
||||||
|
)->toBe(1);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('super admin can load full agent tree', function (): void {
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
|
$admin = AdminUser::query()->create([
|
||||||
|
'username' => 'agent_super',
|
||||||
|
'name' => '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/agent-nodes/tree?admin_site_id='.$siteId)
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('code', 0)
|
||||||
|
->assertJsonStructure(['data' => ['admin_site_id', 'tree']]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('agent operator only sees own subtree', function (): void {
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
|
$rootId = agentRootNodeId($siteId);
|
||||||
|
|
||||||
|
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||||
|
$super = AdminUser::query()->create([
|
||||||
|
'username' => 'bootstrap',
|
||||||
|
'name' => 'Bootstrap',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($super);
|
||||||
|
|
||||||
|
$nodeA = $service->createChild($super, [
|
||||||
|
'parent_id' => $rootId,
|
||||||
|
'code' => 'branch-a',
|
||||||
|
'name' => 'Branch A',
|
||||||
|
'status' => 1,
|
||||||
|
]);
|
||||||
|
$service->createChild($super, [
|
||||||
|
'parent_id' => $rootId,
|
||||||
|
'code' => 'branch-b',
|
||||||
|
'name' => 'Branch B',
|
||||||
|
'status' => 1,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$operator = AdminUser::query()->create([
|
||||||
|
'username' => 'agent_a_ops',
|
||||||
|
'name' => 'A Ops',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantAgentOperatorRole($operator, $nodeA);
|
||||||
|
|
||||||
|
$token = $operator->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
|
||||||
|
$response = $this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->getJson('/api/v1/admin/agent-nodes/tree');
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$tree = $response->json('data.tree');
|
||||||
|
expect($tree)->toBeArray()->and(count($tree))->toBe(1);
|
||||||
|
expect($tree[0]['code'])->toBe('branch-a');
|
||||||
|
expect(collect($tree)->pluck('code'))->not->toContain('branch-b');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('agent operator can create child under own node but not under sibling', function (): void {
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
|
$rootId = agentRootNodeId($siteId);
|
||||||
|
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||||
|
$super = AdminUser::query()->create([
|
||||||
|
'username' => 'bootstrap2',
|
||||||
|
'name' => 'Bootstrap',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($super);
|
||||||
|
|
||||||
|
$nodeA = $service->createChild($super, [
|
||||||
|
'parent_id' => $rootId,
|
||||||
|
'code' => 'branch-a2',
|
||||||
|
'name' => 'Branch A',
|
||||||
|
]);
|
||||||
|
$nodeB = $service->createChild($super, [
|
||||||
|
'parent_id' => $rootId,
|
||||||
|
'code' => 'branch-b2',
|
||||||
|
'name' => 'Branch B',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$operator = AdminUser::query()->create([
|
||||||
|
'username' => 'agent_a2_ops',
|
||||||
|
'name' => 'A Ops',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantAgentOperatorRole($operator, $nodeA);
|
||||||
|
$token = $operator->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->postJson('/api/v1/admin/agent-nodes', [
|
||||||
|
'parent_id' => $nodeA->id,
|
||||||
|
'code' => 'a-child',
|
||||||
|
'name' => 'A Child',
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.code', 'a-child');
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->postJson('/api/v1/admin/agent-nodes', [
|
||||||
|
'parent_id' => $nodeB->id,
|
||||||
|
'code' => 'hack-child',
|
||||||
|
'name' => 'Hack',
|
||||||
|
])
|
||||||
|
->assertForbidden();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('auth me returns agent context for bound operator', function (): void {
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
|
$rootId = agentRootNodeId($siteId);
|
||||||
|
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||||
|
$super = AdminUser::query()->create([
|
||||||
|
'username' => 'bootstrap3',
|
||||||
|
'name' => 'Bootstrap',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($super);
|
||||||
|
|
||||||
|
$node = $service->createChild($super, [
|
||||||
|
'parent_id' => $rootId,
|
||||||
|
'code' => 'me-branch',
|
||||||
|
'name' => 'Me Branch',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$operator = AdminUser::query()->create([
|
||||||
|
'username' => 'me_agent_ops',
|
||||||
|
'name' => 'Ops',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantAgentOperatorRole($operator, $node);
|
||||||
|
$token = $operator->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->getJson('/api/v1/admin/auth/me')
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.admin.agent.code', 'me-branch')
|
||||||
|
->assertJsonPath('data.admin.is_super_admin', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('agent node deletion requires leaf node without bindings', function (): void {
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
|
$rootId = agentRootNodeId($siteId);
|
||||||
|
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||||
|
|
||||||
|
$super = AdminUser::query()->create([
|
||||||
|
'username' => 'delete_agent_super',
|
||||||
|
'name' => 'Delete Super',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($super);
|
||||||
|
$token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
|
||||||
|
$nodeWithChild = $service->createChild($super, [
|
||||||
|
'parent_id' => $rootId,
|
||||||
|
'code' => 'delete-parent',
|
||||||
|
'name' => 'Delete Parent',
|
||||||
|
]);
|
||||||
|
$service->createChild($super, [
|
||||||
|
'parent_id' => $nodeWithChild->id,
|
||||||
|
'code' => 'delete-child',
|
||||||
|
'name' => 'Delete Child',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->deleteJson('/api/v1/admin/agent-nodes/'.$rootId)
|
||||||
|
->assertStatus(422)
|
||||||
|
->assertJsonPath('msg', __('admin.agent_root_delete_denied'));
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->deleteJson('/api/v1/admin/agent-nodes/'.$nodeWithChild->id)
|
||||||
|
->assertStatus(422)
|
||||||
|
->assertJsonPath('msg', __('admin.agent_node_has_children_cannot_delete'));
|
||||||
|
|
||||||
|
$leaf = $service->createChild($super, [
|
||||||
|
'parent_id' => $rootId,
|
||||||
|
'code' => 'delete-leaf',
|
||||||
|
'name' => 'Delete Leaf',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->deleteJson('/api/v1/admin/agent-nodes/'.$leaf->id)
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('code', 0);
|
||||||
|
|
||||||
|
expect(AgentNode::query()->find($leaf->id))->toBeNull();
|
||||||
|
});
|
||||||
205
tests/Feature/AdminAgentRoleApiTest.php
Normal file
205
tests/Feature/AdminAgentRoleApiTest.php
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Models\Player;
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
|
||||||
|
function grantAgentRoleManager(AdminUser $admin, AgentNode $agent): void
|
||||||
|
{
|
||||||
|
$now = now();
|
||||||
|
$roleId = DB::table('admin_roles')->insertGetId([
|
||||||
|
'slug' => 'agent_mgr_'.$admin->id,
|
||||||
|
'code' => 'agent_mgr_'.$admin->id,
|
||||||
|
'name' => 'Agent Manager',
|
||||||
|
'scope_type' => 'system',
|
||||||
|
'status' => 1,
|
||||||
|
'is_system' => false,
|
||||||
|
'sort_order' => 0,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$codes = ['agent.node.view', 'agent.node.manage', 'service.players.view'];
|
||||||
|
$actionIds = DB::table('admin_menu_actions')->whereIn('permission_code', $codes)->pluck('id');
|
||||||
|
foreach ($actionIds as $actionId) {
|
||||||
|
DB::table('admin_role_menu_actions')->insert([
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'menu_action_id' => (int) $actionId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$siteId = (int) $agent->admin_site_id;
|
||||||
|
DB::table('admin_user_site_roles')->insert([
|
||||||
|
'admin_user_id' => $admin->id,
|
||||||
|
'site_id' => $siteId,
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'granted_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('admin_user_agents')->insert([
|
||||||
|
'admin_user_id' => $admin->id,
|
||||||
|
'agent_node_id' => $agent->id,
|
||||||
|
'is_primary' => true,
|
||||||
|
'granted_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('admin_user_agent_roles')->insert([
|
||||||
|
'admin_user_id' => $admin->id,
|
||||||
|
'agent_node_id' => $agent->id,
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'granted_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
test('agent operator can create role with subset of own permissions', function (): void {
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
|
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||||
|
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||||
|
$super = AdminUser::query()->create([
|
||||||
|
'username' => 'super_role',
|
||||||
|
'name' => 'Super',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($super);
|
||||||
|
|
||||||
|
$branch = $service->createChild($super, [
|
||||||
|
'parent_id' => $rootId,
|
||||||
|
'code' => 'role-branch',
|
||||||
|
'name' => 'Role Branch',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$operator = AdminUser::query()->create([
|
||||||
|
'username' => 'role_ops',
|
||||||
|
'name' => 'Ops',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantAgentRoleManager($operator, $branch);
|
||||||
|
$token = $operator->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->postJson('/api/v1/admin/agent-nodes/'.$branch->id.'/roles', [
|
||||||
|
'slug' => 'branch_cs',
|
||||||
|
'name' => 'Branch CS',
|
||||||
|
'permission_slugs' => ['prd.agent.view', 'prd.agent.role.view'],
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.scope_type', 'agent')
|
||||||
|
->assertJsonPath('data.owner_agent_id', $branch->id);
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->postJson('/api/v1/admin/agent-nodes/'.$branch->id.'/roles', [
|
||||||
|
'slug' => 'branch_hack',
|
||||||
|
'name' => 'Hack',
|
||||||
|
'permission_slugs' => ['prd.dashboard.view'],
|
||||||
|
])
|
||||||
|
->assertStatus(422);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('agent scoped roles are excluded from global admin roles index', function (): void {
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
|
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||||
|
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||||
|
$super = AdminUser::query()->create([
|
||||||
|
'username' => 'super_idx',
|
||||||
|
'name' => 'Super',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($super);
|
||||||
|
$token = $super->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
|
||||||
|
$branch = $service->createChild($super, [
|
||||||
|
'parent_id' => $rootId,
|
||||||
|
'code' => 'idx-branch',
|
||||||
|
'name' => 'Idx Branch',
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('admin_roles')->insert([
|
||||||
|
'slug' => 'idx_agent_role',
|
||||||
|
'code' => 'idx_agent_role',
|
||||||
|
'name' => 'Idx Agent Role',
|
||||||
|
'scope_type' => 'agent',
|
||||||
|
'owner_agent_id' => $branch->id,
|
||||||
|
'status' => 1,
|
||||||
|
'is_system' => false,
|
||||||
|
'sort_order' => 0,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$slugs = collect($this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->getJson('/api/v1/admin/admin-roles')
|
||||||
|
->json('data.items'))
|
||||||
|
->pluck('slug');
|
||||||
|
|
||||||
|
expect($slugs)->not->toContain('idx_agent_role');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('players list respects agent subtree when agent_node_id is set', function (): void {
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
|
$siteCode = (string) DB::table('admin_sites')->where('id', $siteId)->value('code');
|
||||||
|
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||||
|
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||||
|
$super = AdminUser::query()->create([
|
||||||
|
'username' => 'super_player',
|
||||||
|
'name' => 'Super',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($super);
|
||||||
|
|
||||||
|
$branchA = $service->createChild($super, ['parent_id' => $rootId, 'code' => 'pa', 'name' => 'A']);
|
||||||
|
$branchB = $service->createChild($super, ['parent_id' => $rootId, 'code' => 'pb', 'name' => 'B']);
|
||||||
|
|
||||||
|
Player::query()->create([
|
||||||
|
'site_code' => $siteCode,
|
||||||
|
'agent_node_id' => $branchA->id,
|
||||||
|
'site_player_id' => 'p-a',
|
||||||
|
'username' => 'pa',
|
||||||
|
'nickname' => 'PA',
|
||||||
|
'default_currency' => 'NPR',
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
Player::query()->create([
|
||||||
|
'site_code' => $siteCode,
|
||||||
|
'agent_node_id' => $branchB->id,
|
||||||
|
'site_player_id' => 'p-b',
|
||||||
|
'username' => 'pb',
|
||||||
|
'nickname' => 'PB',
|
||||||
|
'default_currency' => 'NPR',
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$operator = AdminUser::query()->create([
|
||||||
|
'username' => 'player_ops',
|
||||||
|
'name' => 'Ops',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantAgentRoleManager($operator, $branchA);
|
||||||
|
|
||||||
|
$token = $operator->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
|
||||||
|
$response = $this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->getJson('/api/v1/admin/players?site_code='.$siteCode);
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$usernames = collect($response->json('data.items'))->pluck('username');
|
||||||
|
expect($usernames)->toContain('pa')->not->toContain('pb');
|
||||||
|
});
|
||||||
@@ -91,7 +91,10 @@ test('admin login returns bearer token when captcha passes validation', function
|
|||||||
->assertJsonPath('data.admin.nickname', '测试昵称')
|
->assertJsonPath('data.admin.nickname', '测试昵称')
|
||||||
->assertJsonPath('data.admin.navigation.0.segment', 'dashboard')
|
->assertJsonPath('data.admin.navigation.0.segment', 'dashboard')
|
||||||
->assertJsonPath('data.admin.navigation.0.href', '/admin')
|
->assertJsonPath('data.admin.navigation.0.href', '/admin')
|
||||||
->assertJsonPath('data.admin.navigation.1.segment', 'draws')
|
->assertJsonPath('data.admin.navigation.0.nav_group', 'overview')
|
||||||
|
->assertJsonPath('data.admin.navigation.1.segment', 'agents')
|
||||||
|
->assertJsonPath('data.admin.navigation.1.nav_group', 'agent')
|
||||||
|
->assertJsonPath('data.admin.navigation.2.segment', 'draws')
|
||||||
->assertJsonStructure(['data' => ['token', 'token_type', 'admin' => ['id', 'username', 'nickname', 'email', 'permissions', 'navigation']]]);
|
->assertJsonStructure(['data' => ['token', 'token_type', 'admin' => ['id', 'username', 'nickname', 'email', 'permissions', 'navigation']]]);
|
||||||
|
|
||||||
$token = $resp->json('data.token');
|
$token = $resp->json('data.token');
|
||||||
@@ -103,6 +106,67 @@ test('admin login returns bearer token when captcha passes validation', function
|
|||||||
->assertJsonPath('data.scope', 'admin');
|
->assertJsonPath('data.scope', 'admin');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('agent operator auth me omits platform-only navigation', function (): void {
|
||||||
|
$this->artisan('lottery:admin-auth-sync')->assertExitCode(0);
|
||||||
|
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
|
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||||
|
|
||||||
|
$admin = AdminUser::query()->create([
|
||||||
|
'username' => 'agent_nav_ops',
|
||||||
|
'name' => 'Agent Nav',
|
||||||
|
'email' => null,
|
||||||
|
'password' => 'secret-strong',
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$now = now();
|
||||||
|
$roleId = DB::table('admin_roles')->insertGetId([
|
||||||
|
'slug' => 'agent_nav_ops_role',
|
||||||
|
'code' => 'agent_nav_ops_role',
|
||||||
|
'name' => 'Agent Nav Ops',
|
||||||
|
'description' => null,
|
||||||
|
'status' => 1,
|
||||||
|
'is_system' => false,
|
||||||
|
'sort_order' => 0,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$codes = ['agent.node.view', 'service.report.view'];
|
||||||
|
$actionIds = DB::table('admin_menu_actions')->whereIn('permission_code', $codes)->pluck('id');
|
||||||
|
foreach ($actionIds as $actionId) {
|
||||||
|
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,
|
||||||
|
]);
|
||||||
|
DB::table('admin_user_agents')->insert([
|
||||||
|
'admin_user_id' => $admin->id,
|
||||||
|
'agent_node_id' => $rootId,
|
||||||
|
'is_primary' => true,
|
||||||
|
'granted_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$token = $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
|
||||||
|
$segments = $this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->getJson('/api/v1/admin/auth/me')
|
||||||
|
->assertOk()
|
||||||
|
->json('data.admin.navigation');
|
||||||
|
|
||||||
|
$keys = array_column($segments, 'segment');
|
||||||
|
expect($keys)->toContain('agents', 'reports')
|
||||||
|
->and($keys)->not->toContain('admin_users', 'admin_roles', 'settings', 'integration', 'rules_plays');
|
||||||
|
});
|
||||||
|
|
||||||
test('admin captcha exposes key and image base64', function () {
|
test('admin captcha exposes key and image base64', function () {
|
||||||
$resp = $this->getJson('/api/v1/admin/auth/captcha');
|
$resp = $this->getJson('/api/v1/admin/auth/captcha');
|
||||||
|
|
||||||
|
|||||||
149
tests/Feature/AdminPlayerAgentBindingTest.php
Normal file
149
tests/Feature/AdminPlayerAgentBindingTest.php
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\Player;
|
||||||
|
use App\Models\TransferOrder;
|
||||||
|
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('orphan players can be backfilled to site root agent', function (): void {
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
|
$siteCode = (string) DB::table('admin_sites')->where('id', $siteId)->value('code');
|
||||||
|
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||||
|
|
||||||
|
$playerId = Player::query()->create([
|
||||||
|
'site_code' => $siteCode,
|
||||||
|
'site_player_id' => 'orphan-1',
|
||||||
|
'username' => 'orphan',
|
||||||
|
'nickname' => null,
|
||||||
|
'default_currency' => 'NPR',
|
||||||
|
'status' => 0,
|
||||||
|
'agent_node_id' => null,
|
||||||
|
])->id;
|
||||||
|
|
||||||
|
DB::table('players')
|
||||||
|
->where('site_code', $siteCode)
|
||||||
|
->whereNull('agent_node_id')
|
||||||
|
->update(['agent_node_id' => $rootId]);
|
||||||
|
|
||||||
|
expect((int) Player::query()->find($playerId)?->agent_node_id)->toBe($rootId);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('transfer list excludes players outside agent subtree', function (): void {
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
|
$siteCode = (string) DB::table('admin_sites')->where('id', $siteId)->value('code');
|
||||||
|
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||||
|
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||||
|
$super = AdminUser::query()->create([
|
||||||
|
'username' => 'bind_super',
|
||||||
|
'name' => 'Super',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($super);
|
||||||
|
|
||||||
|
$branch = $service->createChild($super, ['parent_id' => $rootId, 'code' => 'bind-a', 'name' => 'A']);
|
||||||
|
$other = $service->createChild($super, ['parent_id' => $rootId, 'code' => 'bind-b', 'name' => 'B']);
|
||||||
|
|
||||||
|
$inPlayer = Player::query()->create([
|
||||||
|
'site_code' => $siteCode,
|
||||||
|
'agent_node_id' => $branch->id,
|
||||||
|
'site_player_id' => 'bind-in',
|
||||||
|
'username' => 'bind_in',
|
||||||
|
'nickname' => null,
|
||||||
|
'default_currency' => 'NPR',
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
$outPlayer = Player::query()->create([
|
||||||
|
'site_code' => $siteCode,
|
||||||
|
'agent_node_id' => $other->id,
|
||||||
|
'site_player_id' => 'bind-out',
|
||||||
|
'username' => 'bind_out',
|
||||||
|
'nickname' => null,
|
||||||
|
'default_currency' => 'NPR',
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ([$inPlayer, $outPlayer] as $index => $player) {
|
||||||
|
TransferOrder::query()->create([
|
||||||
|
'transfer_no' => 'TR-BIND-'.$index,
|
||||||
|
'player_id' => $player->id,
|
||||||
|
'direction' => 'in',
|
||||||
|
'currency_code' => 'NPR',
|
||||||
|
'amount' => 100,
|
||||||
|
'status' => 'completed',
|
||||||
|
'idempotent_key' => 'idem-bind-'.$index,
|
||||||
|
'external_ref_no' => 'ext-'.$index,
|
||||||
|
'fail_reason' => null,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
'finished_at' => now(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$operator = AdminUser::query()->create([
|
||||||
|
'username' => 'bind_ops',
|
||||||
|
'name' => 'Ops',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantAgentOpsForBinding($operator, $branch);
|
||||||
|
|
||||||
|
$token = $operator->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
|
||||||
|
$response = $this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->getJson('/api/v1/admin/wallet/transfer-orders?site_code='.$siteCode);
|
||||||
|
|
||||||
|
$response->assertOk();
|
||||||
|
$playerIds = collect($response->json('data.items'))->pluck('player_id')->map(static fn ($id): int => (int) $id);
|
||||||
|
expect($playerIds)->toContain($inPlayer->id)->not->toContain($outPlayer->id);
|
||||||
|
});
|
||||||
|
|
||||||
|
function grantAgentOpsForBinding(AdminUser $admin, \App\Models\AgentNode $agent): void
|
||||||
|
{
|
||||||
|
$now = now();
|
||||||
|
$roleId = DB::table('admin_roles')->insertGetId([
|
||||||
|
'slug' => 'bind_ops_'.$admin->id,
|
||||||
|
'code' => 'bind_ops_'.$admin->id,
|
||||||
|
'name' => 'Bind Ops',
|
||||||
|
'scope_type' => 'system',
|
||||||
|
'status' => 1,
|
||||||
|
'is_system' => false,
|
||||||
|
'sort_order' => 0,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach (['agent.node.view', 'service.wallet.view', 'service.reconcile.view'] as $code) {
|
||||||
|
$actionId = DB::table('admin_menu_actions')->where('permission_code', $code)->value('id');
|
||||||
|
if ($actionId) {
|
||||||
|
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' => (int) $agent->admin_site_id,
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'granted_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('admin_user_agents')->insert([
|
||||||
|
'admin_user_id' => $admin->id,
|
||||||
|
'agent_node_id' => (int) $agent->id,
|
||||||
|
'is_primary' => true,
|
||||||
|
'granted_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
184
tests/Feature/AdminReportAgentScopeTest.php
Normal file
184
tests/Feature/AdminReportAgentScopeTest.php
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\AgentNode;
|
||||||
|
use App\Models\Draw;
|
||||||
|
use App\Models\Player;
|
||||||
|
use App\Models\TicketItem;
|
||||||
|
use App\Models\TicketOrder;
|
||||||
|
use App\Lottery\DrawStatus;
|
||||||
|
use App\Services\Admin\AdminReportQueryService;
|
||||||
|
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('player win loss report excludes players outside agent subtree', function (): void {
|
||||||
|
$siteId = (int) DB::table('admin_sites')->where('is_default', true)->value('id');
|
||||||
|
$siteCode = (string) DB::table('admin_sites')->where('id', $siteId)->value('code');
|
||||||
|
$rootId = (int) DB::table('agent_nodes')->where('admin_site_id', $siteId)->where('depth', 0)->value('id');
|
||||||
|
$service = app(\App\Services\Agent\AgentNodeService::class);
|
||||||
|
$super = AdminUser::query()->create([
|
||||||
|
'username' => 'rpt_super',
|
||||||
|
'name' => 'Super',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantSuperAdminRole($super);
|
||||||
|
|
||||||
|
$branch = $service->createChild($super, [
|
||||||
|
'parent_id' => $rootId,
|
||||||
|
'code' => 'rpt-branch',
|
||||||
|
'name' => 'Report Branch',
|
||||||
|
]);
|
||||||
|
$otherBranch = $service->createChild($super, [
|
||||||
|
'parent_id' => $rootId,
|
||||||
|
'code' => 'rpt-other',
|
||||||
|
'name' => 'Other Branch',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$today = now()->toDateString();
|
||||||
|
$draw = Draw::query()->create([
|
||||||
|
'draw_no' => 'RPT-'.now()->format('YmdHis'),
|
||||||
|
'business_date' => $today,
|
||||||
|
'sequence_no' => 901,
|
||||||
|
'status' => DrawStatus::Open->value,
|
||||||
|
'start_time' => now()->subMinute(),
|
||||||
|
'close_time' => now()->addHour(),
|
||||||
|
'draw_time' => now()->addHours(2),
|
||||||
|
'cooling_end_time' => null,
|
||||||
|
'result_source' => null,
|
||||||
|
'current_result_version' => 0,
|
||||||
|
'settle_version' => 0,
|
||||||
|
'is_reopened' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$inScope = Player::query()->create([
|
||||||
|
'site_code' => $siteCode,
|
||||||
|
'site_player_id' => 'rpt-in',
|
||||||
|
'username' => 'rpt_in',
|
||||||
|
'nickname' => null,
|
||||||
|
'default_currency' => 'NPR',
|
||||||
|
'status' => 0,
|
||||||
|
'agent_node_id' => $branch->id,
|
||||||
|
]);
|
||||||
|
$outScope = Player::query()->create([
|
||||||
|
'site_code' => $siteCode,
|
||||||
|
'site_player_id' => 'rpt-out',
|
||||||
|
'username' => 'rpt_out',
|
||||||
|
'nickname' => null,
|
||||||
|
'default_currency' => 'NPR',
|
||||||
|
'status' => 0,
|
||||||
|
'agent_node_id' => $otherBranch->id,
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ([$inScope, $outScope] as $index => $player) {
|
||||||
|
$order = TicketOrder::query()->create([
|
||||||
|
'order_no' => 'RPT-ORD-'.$index,
|
||||||
|
'player_id' => $player->id,
|
||||||
|
'draw_id' => $draw->id,
|
||||||
|
'currency_code' => 'NPR',
|
||||||
|
'total_bet_amount' => 1000,
|
||||||
|
'total_rebate_amount' => 0,
|
||||||
|
'total_actual_deduct' => 1000,
|
||||||
|
'total_estimated_payout' => 0,
|
||||||
|
'status' => 'placed',
|
||||||
|
'submit_source' => 'h5',
|
||||||
|
'client_trace_id' => 'rpt-trace-'.$index,
|
||||||
|
'created_at' => now(),
|
||||||
|
'updated_at' => now(),
|
||||||
|
]);
|
||||||
|
TicketItem::query()->create([
|
||||||
|
'ticket_no' => 'RPT-TK-'.$index,
|
||||||
|
'order_id' => $order->id,
|
||||||
|
'player_id' => $player->id,
|
||||||
|
'draw_id' => $draw->id,
|
||||||
|
'original_number' => '1234',
|
||||||
|
'normalized_number' => '1234',
|
||||||
|
'play_code' => 'big',
|
||||||
|
'dimension' => 4,
|
||||||
|
'digit_slot' => null,
|
||||||
|
'bet_mode' => 'single',
|
||||||
|
'unit_bet_amount' => 1000,
|
||||||
|
'total_bet_amount' => 1000,
|
||||||
|
'rebate_rate_snapshot' => '0.0000',
|
||||||
|
'commission_rate_snapshot' => '0.0000',
|
||||||
|
'actual_deduct_amount' => 1000,
|
||||||
|
'win_amount' => 0,
|
||||||
|
'jackpot_win_amount' => 0,
|
||||||
|
'status' => 'placed',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$operator = AdminUser::query()->create([
|
||||||
|
'username' => 'rpt_ops',
|
||||||
|
'name' => 'Ops',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
grantReportAgentManager($operator, $branch);
|
||||||
|
|
||||||
|
$query = app(AdminReportQueryService::class);
|
||||||
|
$scoped = $query->playerWinLossPaginated(null, $today, $today, 1, 20, $operator);
|
||||||
|
$all = $query->playerWinLossPaginated(null, $today, $today, 1, 20, null);
|
||||||
|
|
||||||
|
$scopedUsernames = collect($scoped->items())->pluck('username')->all();
|
||||||
|
$allUsernames = collect($all->items())->pluck('username')->all();
|
||||||
|
|
||||||
|
expect($scopedUsernames)->toContain('rpt_in')->not->toContain('rpt_out')
|
||||||
|
->and($allUsernames)->toContain('rpt_in', 'rpt_out');
|
||||||
|
});
|
||||||
|
|
||||||
|
function grantReportAgentManager(AdminUser $admin, AgentNode $agent): void
|
||||||
|
{
|
||||||
|
$now = now();
|
||||||
|
$roleId = DB::table('admin_roles')->insertGetId([
|
||||||
|
'slug' => 'rpt_mgr_'.$admin->id,
|
||||||
|
'code' => 'rpt_mgr_'.$admin->id,
|
||||||
|
'name' => 'Report Manager',
|
||||||
|
'scope_type' => 'system',
|
||||||
|
'status' => 1,
|
||||||
|
'is_system' => false,
|
||||||
|
'sort_order' => 0,
|
||||||
|
'created_at' => $now,
|
||||||
|
'updated_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$codes = ['agent.node.view', 'agent.node.manage', 'service.players.view', 'service.report.view'];
|
||||||
|
$actionIds = DB::table('admin_menu_actions')->whereIn('permission_code', $codes)->pluck('id');
|
||||||
|
foreach ($actionIds as $actionId) {
|
||||||
|
DB::table('admin_role_menu_actions')->insert([
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'menu_action_id' => (int) $actionId,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
$siteId = (int) $agent->admin_site_id;
|
||||||
|
DB::table('admin_user_site_roles')->insert([
|
||||||
|
'admin_user_id' => $admin->id,
|
||||||
|
'site_id' => $siteId,
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'granted_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('admin_user_agents')->insert([
|
||||||
|
'admin_user_id' => $admin->id,
|
||||||
|
'agent_node_id' => (int) $agent->id,
|
||||||
|
'is_primary' => true,
|
||||||
|
'granted_at' => $now,
|
||||||
|
]);
|
||||||
|
|
||||||
|
DB::table('admin_user_agent_roles')->insert([
|
||||||
|
'admin_user_id' => $admin->id,
|
||||||
|
'agent_node_id' => (int) $agent->id,
|
||||||
|
'role_id' => $roleId,
|
||||||
|
'granted_at' => $now,
|
||||||
|
]);
|
||||||
|
}
|
||||||
103
tests/Feature/AdminSettingBatchUpdateTest.php
Normal file
103
tests/Feature/AdminSettingBatchUpdateTest.php
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use App\Models\AdminRole;
|
||||||
|
use App\Models\AdminUser;
|
||||||
|
use App\Models\LotterySetting;
|
||||||
|
use App\Services\LotterySettings;
|
||||||
|
use Illuminate\Support\Facades\Hash;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
function settingsAdminToken(): string
|
||||||
|
{
|
||||||
|
$admin = AdminUser::query()->create([
|
||||||
|
'username' => 'settings_editor',
|
||||||
|
'name' => 'Settings Editor',
|
||||||
|
'email' => null,
|
||||||
|
'password' => Hash::make('secret-strong'),
|
||||||
|
'status' => 0,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$role = AdminRole::query()->create([
|
||||||
|
'slug' => 'settings_role',
|
||||||
|
'name' => 'Settings Role',
|
||||||
|
]);
|
||||||
|
$role->syncLegacyPermissionSlugs(['prd.payout.manage']);
|
||||||
|
|
||||||
|
$admin->roles()->sync([
|
||||||
|
(int) $role->id => [
|
||||||
|
'site_id' => AdminUser::defaultAdminSiteId(),
|
||||||
|
'granted_at' => now(),
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $admin->createToken('test', ['*'], now()->addDay())->plainTextToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
test('admin can batch update settings in one request', function (): void {
|
||||||
|
LotterySettings::put('draw.interval_minutes', 5, 'draw');
|
||||||
|
LotterySettings::put('draw.cooldown_minutes', 15, 'draw');
|
||||||
|
|
||||||
|
$token = settingsAdminToken();
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->putJson('/api/v1/admin/settings/batch', [
|
||||||
|
'items' => [
|
||||||
|
['key' => 'draw.interval_minutes', 'value' => 10],
|
||||||
|
['key' => 'draw.cooldown_minutes', 'value' => 20],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.items.0.key', 'draw.cooldown_minutes');
|
||||||
|
|
||||||
|
expect(LotterySetting::query()->where('setting_key', 'draw.interval_minutes')->value('value_json'))->toBe(10)
|
||||||
|
->and(LotterySetting::query()->where('setting_key', 'draw.cooldown_minutes')->value('value_json'))->toBe(20);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('admin settings batch update rejects empty items', function (): void {
|
||||||
|
$token = settingsAdminToken();
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->putJson('/api/v1/admin/settings/batch', ['items' => []])
|
||||||
|
->assertUnprocessable();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('admin can batch update settings with false and empty string values', function (): void {
|
||||||
|
LotterySettings::put('settlement.auto_run_on_tick', true, 'settlement');
|
||||||
|
LotterySettings::put('frontend.play_rules_html_zh', '<div>old</div>', 'frontend');
|
||||||
|
|
||||||
|
$token = settingsAdminToken();
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->putJson('/api/v1/admin/settings/batch', [
|
||||||
|
'items' => [
|
||||||
|
['key' => 'settlement.auto_run_on_tick', 'value' => false],
|
||||||
|
['key' => 'frontend.play_rules_html_zh', 'value' => ''],
|
||||||
|
],
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.items.0.key', 'frontend.play_rules_html_zh')
|
||||||
|
->assertJsonPath('data.items.0.value', '')
|
||||||
|
->assertJsonPath('data.items.1.key', 'settlement.auto_run_on_tick')
|
||||||
|
->assertJsonPath('data.items.1.value', false);
|
||||||
|
|
||||||
|
expect(LotterySetting::query()->where('setting_key', 'settlement.auto_run_on_tick')->value('value_json'))->toBeFalse()
|
||||||
|
->and(LotterySetting::query()->where('setting_key', 'frontend.play_rules_html_zh')->value('value_json'))->toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('admin can update single setting with false value', function (): void {
|
||||||
|
LotterySettings::put('settlement.apply_rebate_to_payout', true, 'settlement');
|
||||||
|
|
||||||
|
$token = settingsAdminToken();
|
||||||
|
|
||||||
|
$this->withHeader('Authorization', 'Bearer '.$token)
|
||||||
|
->putJson('/api/v1/admin/settings/settlement.apply_rebate_to_payout', [
|
||||||
|
'value' => false,
|
||||||
|
])
|
||||||
|
->assertOk()
|
||||||
|
->assertJsonPath('data.key', 'settlement.apply_rebate_to_payout')
|
||||||
|
->assertJsonPath('data.value', false);
|
||||||
|
|
||||||
|
expect(LotterySetting::query()->where('setting_key', 'settlement.apply_rebate_to_payout')->value('value_json'))->toBeFalse();
|
||||||
|
});
|
||||||
@@ -148,30 +148,32 @@ test('permission catalog groups permissions by admin navigation order', function
|
|||||||
|
|
||||||
expect(array_column($groups, 'key'))->toBe([
|
expect(array_column($groups, 'key'))->toBe([
|
||||||
'dashboard',
|
'dashboard',
|
||||||
|
'agents',
|
||||||
'draws',
|
'draws',
|
||||||
'tickets',
|
'tickets',
|
||||||
'players',
|
'players',
|
||||||
|
'settlement',
|
||||||
|
'wallet',
|
||||||
|
'reconcile',
|
||||||
|
'reports',
|
||||||
'rules_plays',
|
'rules_plays',
|
||||||
'rules_odds',
|
'rules_odds',
|
||||||
'jackpot',
|
'jackpot',
|
||||||
'risk_cap',
|
'risk_cap',
|
||||||
'wallet',
|
|
||||||
'settlement',
|
|
||||||
'reconcile',
|
|
||||||
'reports',
|
|
||||||
'currencies',
|
|
||||||
'integration',
|
'integration',
|
||||||
|
'currencies',
|
||||||
'admin_users',
|
'admin_users',
|
||||||
'admin_roles',
|
'admin_roles',
|
||||||
'risk',
|
|
||||||
'audit',
|
'audit',
|
||||||
'settings',
|
'settings',
|
||||||
|
'risk',
|
||||||
]);
|
]);
|
||||||
expect($groups[1]['key'])->toBe('draws');
|
expect($groups[1]['key'])->toBe('agents');
|
||||||
expect($groups[14]['label'])->toBe('管理列表');
|
expect($groups[2]['key'])->toBe('draws');
|
||||||
expect(array_column($groups[14]['permissions'], 'slug'))->toBe(['prd.admin_user.manage']);
|
expect($groups[15]['label'])->toBe('管理列表');
|
||||||
expect($groups[15]['label'])->toBe('角色管理');
|
expect(array_column($groups[15]['permissions'], 'slug'))->toBe(['prd.admin_user.manage']);
|
||||||
expect(array_column($groups[15]['permissions'], 'slug'))->toBe(['prd.admin_role.manage']);
|
expect($groups[16]['label'])->toBe('角色管理');
|
||||||
|
expect(array_column($groups[16]['permissions'], 'slug'))->toBe(['prd.admin_role.manage']);
|
||||||
|
|
||||||
$groupsByKey = collect($groups)->keyBy('key');
|
$groupsByKey = collect($groups)->keyBy('key');
|
||||||
expect(array_column($groupsByKey['tickets']['permissions'], 'slug'))->toBe([
|
expect(array_column($groupsByKey['tickets']['permissions'], 'slug'))->toBe([
|
||||||
|
|||||||
@@ -71,3 +71,16 @@ function grantSuperAdminRole(AdminUser $admin): void
|
|||||||
['granted_at' => $now],
|
['granted_at' => $now],
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 为后台测试账号挂上代理节点(需已存在 agent_nodes / admin_user_agents 表)。 */
|
||||||
|
function bindAdminUserToAgent(AdminUser $admin, int $agentNodeId): void
|
||||||
|
{
|
||||||
|
DB::table('admin_user_agents')->updateOrInsert(
|
||||||
|
['admin_user_id' => $admin->id],
|
||||||
|
[
|
||||||
|
'agent_node_id' => $agentNodeId,
|
||||||
|
'is_primary' => true,
|
||||||
|
'granted_at' => now(),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user