feat: 增强管理员功能与数据处理

- 在多个控制器中引入 agent_node_id,以支持基于代理节点的权限和数据过滤。
- 更新 AdminRole 和 AdminUser 模型,新增角色范围和代理节点相关功能,提升角色管理的灵活性。
- 在请求验证中添加 agent_node_id 字段,确保 API 接口支持代理节点的相关操作。
- 优化 LotterySettings 服务,支持批量写入设置,提升配置管理的效率。
- 更新仪表板和报告服务,增强数据统计功能,确保管理员能够获取更全面的统计信息。
This commit is contained in:
2026-06-02 14:36:58 +08:00
parent d5232c756f
commit 0841fbed32
96 changed files with 5004 additions and 182 deletions

View 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']]);
}
}
}

View 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());
}
}

View 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();
});
}
}

View 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();
}
}