diff --git a/app/db/onchain_db.py b/app/db/onchain_db.py index 1637d84..ff709db 100644 --- a/app/db/onchain_db.py +++ b/app/db/onchain_db.py @@ -766,6 +766,17 @@ def get_onchain_token_detail(symbol, hours=72): """, (symbol, cutoff), ).fetchall() + raw_events = conn.execute( + """ + SELECT * FROM onchain_raw_events + WHERE mapped_symbol=%s AND detected_at >= %s + AND mapping_status='mapped' + AND source=%s + ORDER BY detected_at::timestamp DESC, importance DESC, id DESC + LIMIT 100 + """, + (symbol, cutoff, STANDARD_SIGNAL_SOURCE), + ).fetchall() rec = conn.execute( """ SELECT id, rec_time, action_status, execution_status, display_bucket, entry_price, current_price @@ -781,6 +792,8 @@ def get_onchain_token_detail(symbol, hours=72): "hours": int(hours or 72), "mappings": [_with_raw(row) for row in mappings], "events": [_with_raw(row) for row in events], + "raw_events": [_format_raw_event(row) for row in raw_events], + "raw_event_count": len(raw_events), "metrics": [_with_raw(row) for row in metrics], "recommendation": dict(rec) if rec else None, } diff --git a/static/onchain.html b/static/onchain.html index 2d1778f..8b7d223 100644 --- a/static/onchain.html +++ b/static/onchain.html @@ -75,7 +75,7 @@ async function reloadRawEvents(offset){state.rawOffset=offset||0;$('rawFeed').in async function reloadTokens(offset){state.offset=offset||0;$('tokenTable').innerHTML='加载中...';try{var qs='hours='+$('hoursSel').value+'&limit='+state.limit+'&offset='+state.offset+'&chain='+encodeURIComponent($('chainSel').value)+'&signal='+encodeURIComponent($('signalSel').value);var d=await (await fetch(API+'/api/onchain/tokens?'+qs)).json();state.total=d.total||0;var items=d.items||[];if(!items.length){$('tokenTable').innerHTML='暂无链上异动 token'}else{$('tokenTable').innerHTML=items.map(function(t){return ''+esc(t.symbol)+''+esc(t.chain)+''+Number(t.onchain_score||0).toFixed(0)+''+Number(t.risk_score||0).toFixed(0)+''+Number(t.event_count||t.mapped_event_count||0).toFixed(0)+''+esc(t.latest_event_at?fmtTime(t.latest_event_at):'--')+''+esc(t.source||'nodereal')+''+recLabel(t.recommendation)+''}).join('');if(!state.selected&&items[0])loadDetail(items[0].symbol)}updatePager()}catch(e){$('tokenTable').innerHTML='加载失败'}} function updatePager(){var page=Math.floor(state.offset/state.limit)+1,totalPages=Math.max(1,Math.ceil((state.total||0)/state.limit));$('pageInfo').textContent='第 '+page+' / '+totalPages+' 页,共 '+state.total+' 个';$('prevBtn').disabled=state.offset<=0;$('nextBtn').disabled=state.offset+state.limit>=state.total} function page(step){var next=state.offset+step*state.limit;if(next<0||next>=state.total)return;reloadTokens(next)} -async function loadDetail(symbol){state.selected=symbol;$('detailNote').textContent=symbol;$('detailBody').innerHTML='
加载详情...
';try{var d=await (await fetch(API+'/api/onchain/tokens/'+encodeURIComponent(symbol)+'?hours=168')).json();var latest=(d.metrics||[])[0]||{};var rec=d.recommendation;var metrics='
'+[['链上分',Number(latest.onchain_score||0).toFixed(0)],['风险分',Number(latest.risk_score||0).toFixed(0)],['映射事件',Number(latest.event_count||latest.mapped_event_count||0).toFixed(0)],['数据源',latest.source||'nodereal']].map(function(x){return '
'+x[0]+''+x[1]+'
'}).join('')+'
';var recHtml=rec?'
主链路状态:'+esc(rec.action_status||rec.execution_status||'观察')+' · 推荐 #'+esc(rec.id)+'
':'
尚未形成主链路推荐;若链上信号质量足够,会先进入技术检查。
';var events=(d.events||[]).slice(0,20).map(function(e){var cls=e.direction==='risk'?'risk':e.direction==='positive'?'pos':'blue';return '
'+esc(e.signal_label||e.signal_code)+'
'+esc(e.severity||e.direction)+'
'+fmtTime(e.detected_at)+' · '+esc(e.chain)+' · '+fmtUsd(e.value_usd)+'
'+esc(e.wallet_label||e.counterparty_label||e.source||'链上事件')+'
'}).join('')||'
暂无事件明细
';$('detailBody').innerHTML='
'+esc(d.symbol)+'
'+(d.mappings||[]).length+' 个合约映射 · 近 7 天
'+metrics+recHtml+'
'+events+'
'}catch(e){$('detailBody').innerHTML='
详情加载失败
'}} +async function loadDetail(symbol){state.selected=symbol;$('detailNote').textContent=symbol;$('detailBody').innerHTML='
加载详情...
';try{var d=await (await fetch(API+'/api/onchain/tokens/'+encodeURIComponent(symbol)+'?hours=168')).json();var latest=(d.metrics||[])[0]||{};var rec=d.recommendation;var rawCount=Number(d.raw_event_count||latest.event_count||latest.mapped_event_count||0);var metrics='
'+[['链上分',Number(latest.onchain_score||0).toFixed(0)],['风险分',Number(latest.risk_score||0).toFixed(0)],['映射事件',rawCount.toFixed(0)],['数据源',latest.source||'nodereal']].map(function(x){return '
'+x[0]+''+x[1]+'
'}).join('')+'
';var recHtml=rec?'
主链路状态:'+esc(rec.action_status||rec.execution_status||'观察')+' · 推荐 #'+esc(rec.id)+'
':'
尚未形成主链路推荐;若链上信号质量足够,会先进入技术检查。
';var standardEvents=(d.events||[]).slice(0,10).map(function(e){var cls=e.direction==='risk'?'risk':e.direction==='positive'?'pos':'blue';return '
'+esc(e.signal_label||e.signal_code)+'
'+esc(e.severity||e.direction)+'
'+fmtTime(e.detected_at)+' · '+esc(e.chain)+' · '+fmtUsd(e.value_usd)+'
'+esc(e.wallet_label||e.counterparty_label||e.source||'链上事件')+'
'});var rawEvents=(d.raw_events||[]).slice(0,20).map(function(e){var amount=e.total_amount||e.amount||0;var link=e.url?' · 查看来源':'';return '
'+esc(e.event_label||e.title||e.event_type||'NodeReal 原始事件')+'
已映射
'+fmtTime(e.detected_at)+' · '+esc(e.chain)+' · '+esc(e.mapped_symbol||d.symbol)+' · 热度 '+fmtAmount(amount)+link+'
'+esc(e.pipeline_note||'已映射,可进入后续链上信号分析。')+'
'});var events=standardEvents.concat(rawEvents).join('')||'
暂无事件明细
';$('detailBody').innerHTML='
'+esc(d.symbol)+'
'+(d.mappings||[]).length+' 个合约映射 · 近 7 天
'+metrics+recHtml+'
'+events+'
'}catch(e){$('detailBody').innerHTML='
详情加载失败
'}} function reloadAll(){state.offset=0;state.rawOffset=0;state.selected='';loadOverview();reloadRawEvents(0);reloadTokens(0)} reloadAll(); setInterval(reloadAll,300000); diff --git a/tests/test_onchain_tracking.py b/tests/test_onchain_tracking.py index a663c90..67c444f 100644 --- a/tests/test_onchain_tracking.py +++ b/tests/test_onchain_tracking.py @@ -232,6 +232,32 @@ def test_overview_ignores_legacy_signals_and_surfaces_mapped_raw_feed(monkeypatc assert overview["signals"] == [] +def test_token_detail_includes_mapped_raw_events(monkeypatch, tmp_path): + _temp_db(monkeypatch, tmp_path) + onchain_db.insert_onchain_raw_event( + { + "source": "nodereal", + "chain": "bsc", + "event_type": "evm_transfer", + "token_address": "0xbeam", + "title": "NodeReal ERC-20 原始转账", + "amount": 300, + "total_amount": 300, + "importance": 78, + "mapped_symbol": "BEAM/USDT", + "mapping_status": "mapped", + "detected_at": datetime.now().isoformat(), + } + ) + + detail = onchain_db.get_onchain_token_detail("BEAM/USDT", hours=24) + + assert detail["events"] == [] + assert detail["raw_event_count"] == 1 + assert detail["raw_events"][0]["mapped_symbol"] == "BEAM/USDT" + assert detail["raw_events"][0]["pipeline_note"] == "已映射,可进入后续链上信号分析。" + + def test_nodereal_events_generate_metrics_and_normalized_event(monkeypatch, tmp_path): _temp_db(monkeypatch, tmp_path) monkeypatch.setenv("ALPHAX_NODEREAL_API_KEY", "test-key")