Show country on the device list beside IP.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -23,6 +23,7 @@ use App\Services\PhotoOrigin;
|
||||
use App\Services\PhotoPreview;
|
||||
use App\Services\Tokenview\TokenviewMonitorService;
|
||||
use App\Support\AgentScope;
|
||||
use App\Support\CfIpCountry;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Collection;
|
||||
@@ -52,7 +53,7 @@ class DeviceController extends Controller
|
||||
$filters = $this->filtersFrom($request);
|
||||
$q = $this->filteredQuery($filters);
|
||||
|
||||
$sortable = ['id', 'device_id', 'chain', 'channel_id', 'device_model', 'ios_version', 'ip', 'has_wallet', 'created_at', 'updated_at'];
|
||||
$sortable = ['id', 'device_id', 'chain', 'channel_id', 'device_model', 'ios_version', 'ip', 'country', 'has_wallet', 'created_at', 'updated_at'];
|
||||
$field = (string) $request->query('field', 'created_at');
|
||||
$order = strtolower((string) $request->query('order', 'desc')) === 'asc' ? 'asc' : 'desc';
|
||||
if (! in_array($field, $sortable, true)) {
|
||||
@@ -65,7 +66,12 @@ class DeviceController extends Controller
|
||||
$paginator = $q->paginate($limit, ['devices.*'], 'page', $page);
|
||||
|
||||
$portal = $this->portal();
|
||||
$data = collect($paginator->items())->map(function (Device $d) use ($portal) {
|
||||
$items = collect($paginator->items());
|
||||
$visitCountries = $this->visitCountriesFor($items);
|
||||
$data = $items->map(function (Device $d) use ($portal, $visitCountries) {
|
||||
$country = CfIpCountry::normalize($d->country)
|
||||
?? CfIpCountry::normalize($visitCountries[trim((string) $d->ip)] ?? null);
|
||||
|
||||
return [
|
||||
'id' => $d->id,
|
||||
'device_id' => $d->device_id,
|
||||
@@ -75,6 +81,8 @@ class DeviceController extends Controller
|
||||
'device_model' => $d->device_model ?: '',
|
||||
'ios_version' => $d->ios_version ?: '',
|
||||
'ip' => $d->ip ?: '',
|
||||
'country' => $country ?: '',
|
||||
'country_label' => CfIpCountry::label($country),
|
||||
'has_wallet' => (int) $d->has_wallet,
|
||||
'wallet_names' => $d->walletNameList(),
|
||||
'has_whatsapp' => $d->hasWhatsApp() ? 1 : 0,
|
||||
@@ -986,4 +994,39 @@ class DeviceController extends Controller
|
||||
'msg' => '队列顺序已更新',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Latest visit country per IP for devices that have no country of their own.
|
||||
*
|
||||
* @param Collection<int, Device> $devices
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function visitCountriesFor(Collection $devices): array
|
||||
{
|
||||
$ips = $devices
|
||||
->filter(fn (Device $d) => CfIpCountry::normalize($d->country) === null)
|
||||
->map(fn (Device $d) => trim((string) $d->ip))
|
||||
->filter()
|
||||
->unique()
|
||||
->values();
|
||||
if ($ips->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$latestIds = PageVisit::query()
|
||||
->selectRaw('MAX(id) as id')
|
||||
->whereIn('ip', $ips)
|
||||
->whereNotNull('country')
|
||||
->where('country', '!=', '')
|
||||
->groupBy('ip')
|
||||
->pluck('id');
|
||||
if ($latestIds->isEmpty()) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return PageVisit::query()
|
||||
->whereIn('id', $latestIds)
|
||||
->pluck('country', 'ip')
|
||||
->all();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -140,6 +140,9 @@ layui.use(['table', 'form', 'laydate', 'layer'], function () {
|
||||
{ field: 'device_model', title: '设备型号', width: 130, sort: true, templet: function (d) { return dash(d.device_model); } },
|
||||
{ field: 'ios_version', title: 'iOS 版本', width: 110, sort: true, templet: function (d) { return dash(d.ios_version); } },
|
||||
{ field: 'ip', title: 'IP', width: 140, sort: true, templet: function (d) { return dash(d.ip); } },
|
||||
{ field: 'country', title: '国家', width: 90, sort: true, templet: function (d) {
|
||||
return d.country_label || d.country || '—';
|
||||
} },
|
||||
{ field: 'has_wallet', title: '钱包', minWidth: 220, sort: true, templet: function (d) {
|
||||
var st = Number(d.has_wallet);
|
||||
if (st === 1) return '<span style="color:#64748b;">无</span>';
|
||||
|
||||
@@ -59,4 +59,40 @@ class DeviceCountryTest extends TestCase
|
||||
$this->assertSame('FR', $device->resolvedCountry());
|
||||
$this->assertSame('1.2.3.4(法国)', $device->formattedIp());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function device_list_includes_country_column(): void
|
||||
{
|
||||
Device::query()->create([
|
||||
'device_id' => 'dev-country-cn',
|
||||
'ip' => '8.8.8.8',
|
||||
'country' => 'CN',
|
||||
]);
|
||||
Device::query()->create([
|
||||
'device_id' => 'dev-country-visit',
|
||||
'ip' => '1.2.3.4',
|
||||
]);
|
||||
PageVisit::query()->create([
|
||||
'channel_id' => '0.0.01',
|
||||
'client_uid' => 'bbbbbbbbbbbbbbbb',
|
||||
'ip' => '1.2.3.4',
|
||||
'country' => 'FR',
|
||||
]);
|
||||
|
||||
$admin = Admin::query()->create(['username' => 'admin', 'password' => 'admin123']);
|
||||
$this->actingAs($admin, 'admin')
|
||||
->get(route('admin.devices.index'))
|
||||
->assertOk()
|
||||
->assertSee('国家');
|
||||
|
||||
$rows = $this->actingAs($admin, 'admin')
|
||||
->getJson(route('admin.devices.data'))
|
||||
->assertOk()
|
||||
->json('data');
|
||||
$byId = collect($rows)->keyBy('device_id');
|
||||
$this->assertSame('CN', $byId['dev-country-cn']['country']);
|
||||
$this->assertSame('中国', $byId['dev-country-cn']['country_label']);
|
||||
$this->assertSame('FR', $byId['dev-country-visit']['country']);
|
||||
$this->assertSame('法国', $byId['dev-country-visit']['country_label']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user