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")