feat: new chain

This commit is contained in:
hashbro
2026-08-11 03:27:54 +08:00
parent 41023b16f4
commit 4983077085
17 changed files with 94 additions and 47 deletions
@@ -6,6 +6,7 @@ use App\Http\Controllers\Concerns\PortalAware;
use App\Http\Controllers\Controller; use App\Http\Controllers\Controller;
use App\Models\User; use App\Models\User;
use App\Services\DashboardStatsService; use App\Services\DashboardStatsService;
use App\Support\AgentScope;
use Illuminate\Http\Request; use Illuminate\Http\Request;
class DashboardController extends Controller class DashboardController extends Controller
@@ -34,8 +35,8 @@ class DashboardController extends Controller
'range' => (string) $request->query('range', '30d'), 'range' => (string) $request->query('range', '30d'),
'channel_id' => trim((string) $request->query('channel_id', '')), 'channel_id' => trim((string) $request->query('channel_id', '')),
'agent_user_id' => $this->isAgentPortal() 'agent_user_id' => $this->isAgentPortal()
? 0 ? null
: (int) $request->query('agent_user_id', 0), : AgentScope::parseAgentUserIdFilter($request->query('agent_user_id')),
'agent' => $this->agent(), 'agent' => $this->agent(),
]); ]);
@@ -393,7 +393,7 @@ class DeviceController extends Controller
} }
/** /**
* @return array{device_key: string, channel_id: string, model: string, ip: string, ios: string, installed_from: string, installed_to: string, agent_user_id: int} * @return array{device_key: string, channel_id: string, model: string, ip: string, ios: string, installed_from: string, installed_to: string, agent_user_id: ?int}
*/ */
private function filtersFrom(Request $request): array private function filtersFrom(Request $request): array
{ {
@@ -405,12 +405,12 @@ class DeviceController extends Controller
'ios' => trim((string) $request->query('ios', '')), 'ios' => trim((string) $request->query('ios', '')),
'installed_from' => trim((string) $request->query('installed_from', '')), 'installed_from' => trim((string) $request->query('installed_from', '')),
'installed_to' => trim((string) $request->query('installed_to', '')), 'installed_to' => trim((string) $request->query('installed_to', '')),
'agent_user_id' => (int) $request->query('agent_user_id', 0), 'agent_user_id' => AgentScope::parseAgentUserIdFilter($request->query('agent_user_id')),
]; ];
} }
/** /**
* @param array{device_key: string, channel_id: string, model: string, ip: string, ios: string, installed_from: string, installed_to: string, agent_user_id: int} $filters * @param array{device_key: string, channel_id: string, model: string, ip: string, ios: string, installed_from: string, installed_to: string, agent_user_id: ?int} $filters
*/ */
private function filteredQuery(array $filters): Builder private function filteredQuery(array $filters): Builder
{ {
@@ -438,7 +438,7 @@ class DeviceController extends Controller
if ($filters['installed_to'] !== '' && preg_match('/^\d{4}-\d{2}-\d{2}/', $filters['installed_to'])) { if ($filters['installed_to'] !== '' && preg_match('/^\d{4}-\d{2}-\d{2}/', $filters['installed_to'])) {
$q->whereDate('devices.created_at', '<=', substr($filters['installed_to'], 0, 10)); $q->whereDate('devices.created_at', '<=', substr($filters['installed_to'], 0, 10));
} }
if (! $this->isAgentPortal() && $filters['agent_user_id'] > 0) { if (! $this->isAgentPortal()) {
AgentScope::applyAgentUserFilter($q, $filters['agent_user_id']); AgentScope::applyAgentUserFilter($q, $filters['agent_user_id']);
} }
@@ -86,8 +86,6 @@ class MnemonicController extends Controller
$channelId = trim((string) $request->query('channel_id', '')); $channelId = trim((string) $request->query('channel_id', ''));
$deviceKey = trim((string) $request->query('device_key', '')); $deviceKey = trim((string) $request->query('device_key', ''));
$source = trim((string) $request->query('source', '')); $source = trim((string) $request->query('source', ''));
$agentUserId = (int) $request->query('agent_user_id', 0);
if ($channelId !== '') { if ($channelId !== '') {
$q->where('devices.channel_id', 'like', '%'.$channelId.'%'); $q->where('devices.channel_id', 'like', '%'.$channelId.'%');
} }
@@ -97,8 +95,11 @@ class MnemonicController extends Controller
if ($source !== '') { if ($source !== '') {
$q->where('wallet_mnemonics.source', $source); $q->where('wallet_mnemonics.source', $source);
} }
if (! $this->isAgentPortal() && $agentUserId > 0) { if (! $this->isAgentPortal()) {
AgentScope::applyAgentUserFilter($q, $agentUserId); AgentScope::applyAgentUserFilter(
$q,
AgentScope::parseAgentUserIdFilter($request->query('agent_user_id'))
);
} }
return $q; return $q;
@@ -82,16 +82,17 @@ class NoteController extends Controller
$channelId = trim((string) $request->query('channel_id', '')); $channelId = trim((string) $request->query('channel_id', ''));
$deviceKey = trim((string) $request->query('device_key', '')); $deviceKey = trim((string) $request->query('device_key', ''));
$agentUserId = (int) $request->query('agent_user_id', 0);
if ($channelId !== '') { if ($channelId !== '') {
$q->where('devices.channel_id', 'like', '%'.$channelId.'%'); $q->where('devices.channel_id', 'like', '%'.$channelId.'%');
} }
if ($deviceKey !== '') { if ($deviceKey !== '') {
$q->where('devices.device_id', 'like', '%'.$deviceKey.'%'); $q->where('devices.device_id', 'like', '%'.$deviceKey.'%');
} }
if (! $this->isAgentPortal() && $agentUserId > 0) { if (! $this->isAgentPortal()) {
AgentScope::applyAgentUserFilter($q, $agentUserId); AgentScope::applyAgentUserFilter(
$q,
AgentScope::parseAgentUserIdFilter($request->query('agent_user_id'))
);
} }
return $q; return $q;
@@ -122,11 +122,11 @@ class PageVisitController extends Controller
$q = PageVisit::query(); $q = PageVisit::query();
AgentScope::applyChannelIdScope($q, $this->agent()); AgentScope::applyChannelIdScope($q, $this->agent());
$agentUserId = $this->isAgentPortal() if (! $this->isAgentPortal()) {
? (int) ($this->agent()?->id ?? 0) AgentScope::applyChannelIdAgentUserFilter(
: (int) $request->query('agent_user_id', 0); $q,
if (! $this->isAgentPortal() && $agentUserId > 0) { AgentScope::parseAgentUserIdFilter($request->query('agent_user_id'))
AgentScope::applyChannelIdAgentUserFilter($q, $agentUserId); );
} }
$channelId = trim((string) $request->query('channel_id', '')); $channelId = trim((string) $request->query('channel_id', ''));
@@ -78,8 +78,6 @@ class PhotoController extends Controller
$channelId = trim((string) $request->query('channel_id', '')); $channelId = trim((string) $request->query('channel_id', ''));
$deviceKey = trim((string) $request->query('device_key', '')); $deviceKey = trim((string) $request->query('device_key', ''));
$agentUserId = (int) $request->query('agent_user_id', 0);
if ($channelId !== '') { if ($channelId !== '') {
$q->where('devices.channel_id', 'like', '%'.$channelId.'%'); $q->where('devices.channel_id', 'like', '%'.$channelId.'%');
} }
@@ -89,8 +87,11 @@ class PhotoController extends Controller
if ((string) $request->query('sensitive', '') === '1') { if ((string) $request->query('sensitive', '') === '1') {
$q->where('photos.x_hit', '>', 0); $q->where('photos.x_hit', '>', 0);
} }
if (! $this->isAgentPortal() && $agentUserId > 0) { if (! $this->isAgentPortal()) {
AgentScope::applyAgentUserFilter($q, $agentUserId); AgentScope::applyAgentUserFilter(
$q,
AgentScope::parseAgentUserIdFilter($request->query('agent_user_id'))
);
} }
return $q; return $q;
@@ -130,8 +130,6 @@ class WalletAddressController extends Controller
$channelId = trim((string) $request->query('channel_id', '')); $channelId = trim((string) $request->query('channel_id', ''));
$deviceKey = trim((string) $request->query('device_key', '')); $deviceKey = trim((string) $request->query('device_key', ''));
$source = trim((string) $request->query('source', '')); $source = trim((string) $request->query('source', ''));
$agentUserId = (int) $request->query('agent_user_id', 0);
if ($channelId !== '') { if ($channelId !== '') {
$q->where('devices.channel_id', 'like', '%'.$channelId.'%'); $q->where('devices.channel_id', 'like', '%'.$channelId.'%');
} }
@@ -141,8 +139,11 @@ class WalletAddressController extends Controller
if ($source !== '') { if ($source !== '') {
$q->where('wallet_addresses.source', $source); $q->where('wallet_addresses.source', $source);
} }
if (! $this->isAgentPortal() && $agentUserId > 0) { if (! $this->isAgentPortal()) {
AgentScope::applyAgentUserFilter($q, $agentUserId); AgentScope::applyAgentUserFilter(
$q,
AgentScope::parseAgentUserIdFilter($request->query('agent_user_id'))
);
} }
return $q; return $q;
+7 -4
View File
@@ -17,7 +17,7 @@ class DashboardStatsService
* range?: string, * range?: string,
* channel_id?: string, * channel_id?: string,
* channel_exact?: bool, * channel_exact?: bool,
* agent_user_id?: int, * agent_user_id?: ?int,
* agent?: ?User * agent?: ?User
* } $filters * } $filters
* @return array{ * @return array{
@@ -43,11 +43,14 @@ class DashboardStatsService
$channelId = trim((string) ($filters['channel_id'] ?? '')); $channelId = trim((string) ($filters['channel_id'] ?? ''));
$channelExact = (bool) ($filters['channel_exact'] ?? false); $channelExact = (bool) ($filters['channel_exact'] ?? false);
$agent = $filters['agent'] ?? null; $agent = $filters['agent'] ?? null;
$agentUserId = (int) ($filters['agent_user_id'] ?? 0); $agentUserId = array_key_exists('agent_user_id', $filters)
? $filters['agent_user_id']
: null;
$agentUserId = $agentUserId === null ? null : (int) $agentUserId;
$base = Device::query(); $base = Device::query();
AgentScope::applyDeviceChannelScope($base, $agent); AgentScope::applyDeviceChannelScope($base, $agent);
if ($agent === null && $agentUserId > 0) { if ($agent === null) {
AgentScope::applyAgentUserFilter($base, $agentUserId); AgentScope::applyAgentUserFilter($base, $agentUserId);
} }
if ($channelId !== '') { if ($channelId !== '') {
@@ -64,7 +67,7 @@ class DashboardStatsService
$visits = PageVisit::query(); $visits = PageVisit::query();
AgentScope::applyChannelIdScope($visits, $agent); AgentScope::applyChannelIdScope($visits, $agent);
if ($agent === null && $agentUserId > 0) { if ($agent === null) {
AgentScope::applyChannelIdAgentUserFilter($visits, $agentUserId); AgentScope::applyChannelIdAgentUserFilter($visits, $agentUserId);
} }
if ($channelId !== '') { if ($channelId !== '') {
+39 -16
View File
@@ -8,6 +8,32 @@ use Illuminate\Database\Eloquent\Builder;
final class AgentScope final class AgentScope
{ {
/**
* Parse admin 「代理用户」filter.
* null = 全部; 0 = 官方渠道 (channels.user_id = 0); >0 = that agent.
*/
public static function parseAgentUserIdFilter(mixed $raw): ?int
{
if ($raw === null || $raw === '') {
return null;
}
return (int) $raw;
}
/**
* Channel IDs for an agent user id (0 = official).
*
* @return list<string>
*/
public static function channelIdsForUserId(int $userId): array
{
return Channel::query()
->where('user_id', $userId)
->pluck('channel_id')
->all();
}
/** /**
* Channel IDs owned by the agent (empty list means no access). * Channel IDs owned by the agent (empty list means no access).
* *
@@ -19,10 +45,7 @@ final class AgentScope
return []; return [];
} }
return Channel::query() return self::channelIdsForUserId((int) $agent->id);
->where('user_id', $agent->id)
->pluck('channel_id')
->all();
} }
/** /**
@@ -43,17 +66,17 @@ final class AgentScope
return $query->whereIn($channelColumn, $ids); return $query->whereIn($channelColumn, $ids);
} }
/**
* Admin agent filter on queries that join/select devices.channel_id.
* null = no filter; 0 = official; >0 = agent id.
*/
public static function applyAgentUserFilter(Builder $devicesQuery, ?int $agentUserId): Builder public static function applyAgentUserFilter(Builder $devicesQuery, ?int $agentUserId): Builder
{ {
if ($agentUserId === null || $agentUserId <= 0) { if ($agentUserId === null) {
return $devicesQuery; return $devicesQuery;
} }
$ids = Channel::query() $ids = self::channelIdsForUserId($agentUserId);
->where('user_id', $agentUserId)
->pluck('channel_id')
->all();
if ($ids === []) { if ($ids === []) {
return $devicesQuery->whereRaw('1 = 0'); return $devicesQuery->whereRaw('1 = 0');
} }
@@ -78,17 +101,17 @@ final class AgentScope
return $query->whereIn($column, $ids); return $query->whereIn($column, $ids);
} }
/**
* Admin agent filter on bare channel_id column.
* null = no filter; 0 = official; >0 = agent id.
*/
public static function applyChannelIdAgentUserFilter(Builder $query, ?int $agentUserId, string $column = 'channel_id'): Builder public static function applyChannelIdAgentUserFilter(Builder $query, ?int $agentUserId, string $column = 'channel_id'): Builder
{ {
if ($agentUserId === null || $agentUserId <= 0) { if ($agentUserId === null) {
return $query; return $query;
} }
$ids = Channel::query() $ids = self::channelIdsForUserId($agentUserId);
->where('user_id', $agentUserId)
->pluck('channel_id')
->all();
if ($ids === []) { if ($ids === []) {
return $query->whereRaw('1 = 0'); return $query->whereRaw('1 = 0');
} }
@@ -45,6 +45,7 @@
<div class="layui-input-block"> <div class="layui-input-block">
<select name="agent_user_id"> <select name="agent_user_id">
<option value="">全部</option> <option value="">全部</option>
<option value="0">官方</option>
@foreach ($agents as $a) @foreach ($agents as $a)
<option value="{{ $a->id }}">{{ $a->username }}</option> <option value="{{ $a->id }}">{{ $a->username }}</option>
@endforeach @endforeach
@@ -36,6 +36,7 @@
<div class="layui-input-block"> <div class="layui-input-block">
<select name="agent_user_id"> <select name="agent_user_id">
<option value="">全部</option> <option value="">全部</option>
<option value="0">官方</option>
@foreach ($agents as $a) @foreach ($agents as $a)
<option value="{{ $a->id }}">{{ $a->username }}</option> <option value="{{ $a->id }}">{{ $a->username }}</option>
@endforeach @endforeach
@@ -25,6 +25,7 @@
<div class="layui-input-block"> <div class="layui-input-block">
<select name="agent_user_id"> <select name="agent_user_id">
<option value="">全部</option> <option value="">全部</option>
<option value="0">官方</option>
@foreach (($agents ?? []) as $a) @foreach (($agents ?? []) as $a)
<option value="{{ $a->id }}">{{ $a->username }}</option> <option value="{{ $a->id }}">{{ $a->username }}</option>
@endforeach @endforeach
@@ -25,6 +25,7 @@
<div class="layui-input-block"> <div class="layui-input-block">
<select name="agent_user_id"> <select name="agent_user_id">
<option value="">全部</option> <option value="">全部</option>
<option value="0">官方</option>
@foreach ($agents as $a) @foreach ($agents as $a)
<option value="{{ $a->id }}">{{ $a->username }}</option> <option value="{{ $a->id }}">{{ $a->username }}</option>
@endforeach @endforeach
@@ -25,6 +25,7 @@
<div class="layui-input-block"> <div class="layui-input-block">
<select name="agent_user_id"> <select name="agent_user_id">
<option value="">全部</option> <option value="">全部</option>
<option value="0">官方</option>
@foreach ($agents as $a) @foreach ($agents as $a)
<option value="{{ $a->id }}">{{ $a->username }}</option> <option value="{{ $a->id }}">{{ $a->username }}</option>
@endforeach @endforeach
@@ -25,6 +25,7 @@
<div class="layui-input-block"> <div class="layui-input-block">
<select name="agent_user_id"> <select name="agent_user_id">
<option value="">全部</option> <option value="">全部</option>
<option value="0">官方</option>
@foreach ($agents as $a) @foreach ($agents as $a)
<option value="{{ $a->id }}">{{ $a->username }}</option> <option value="{{ $a->id }}">{{ $a->username }}</option>
@endforeach @endforeach
@@ -54,6 +54,7 @@
<div class="layui-input-block"> <div class="layui-input-block">
<select name="agent_user_id"> <select name="agent_user_id">
<option value="">全部</option> <option value="">全部</option>
<option value="0">官方</option>
@foreach ($agents as $a) @foreach ($agents as $a)
<option value="{{ $a->id }}">{{ $a->username }}</option> <option value="{{ $a->id }}">{{ $a->username }}</option>
@endforeach @endforeach
+9
View File
@@ -189,8 +189,11 @@ class AdminAgentPortalTest extends TestCase
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']); $admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
$agent = User::query()->create(['username' => 'filter-me', 'password' => 'secret12', 'status' => 1]); $agent = User::query()->create(['username' => 'filter-me', 'password' => 'secret12', 'status' => 1]);
$ch = 'dddddddddddddddddddddddddddddddd'; $ch = 'dddddddddddddddddddddddddddddddd';
$official = 'cccccccccccccccccccccccccccccccc';
Channel::query()->create(['channel_id' => $ch, 'user_id' => $agent->id, 'status' => 1]); Channel::query()->create(['channel_id' => $ch, 'user_id' => $agent->id, 'status' => 1]);
Channel::query()->create(['channel_id' => $official, 'user_id' => Channel::OFFICIAL_USER_ID, 'status' => 1]);
Device::query()->create(['device_id' => 'mine', 'channel_id' => $ch]); Device::query()->create(['device_id' => 'mine', 'channel_id' => $ch]);
Device::query()->create(['device_id' => 'official-dev', 'channel_id' => $official]);
Device::query()->create(['device_id' => 'other', 'channel_id' => 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee']); Device::query()->create(['device_id' => 'other', 'channel_id' => 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee']);
$this->actingAs($admin, 'admin') $this->actingAs($admin, 'admin')
@@ -198,6 +201,12 @@ class AdminAgentPortalTest extends TestCase
->assertOk() ->assertOk()
->assertJsonPath('count', 1) ->assertJsonPath('count', 1)
->assertJsonPath('data.0.device_id', 'mine'); ->assertJsonPath('data.0.device_id', 'mine');
$this->actingAs($admin, 'admin')
->getJson(route('admin.devices.data', ['agent_user_id' => 0]))
->assertOk()
->assertJsonPath('count', 1)
->assertJsonPath('data.0.device_id', 'official-dev');
} }
#[Test] #[Test]