Database (3→6 tables): - hotspot_source: support api/browser/crawler types with type-specific fields - hotspot_schedule: cron/interval daemon config per source - hotspot_fetch_log: per-run audit trail (status/duration/items/errors) - hotspot_item: add heat_velocity, engagement/comment/share counts, sentiment - hotspot_analysis: 5-dimension scoring (time/heat/content/propagation/audience) - hotspot_alert: alert rules (heat threshold/velocity/sentiment triggers) Fetch engine (3 modes): - API: GET/POST + headers + Bearer auth + JSON extraction - Browser: HTML title/link extraction + og:title fallback - Crawler: recursive with depth/domain allowlist/link following Analysis engine: - Exponential decay model (7-day half-life) - Auto-classify: emerging→rising→hot→cooling→expired - 5-dimension scoring with detail JSON per dimension Dashboard: 6 tabs + 7 stat cards + workflow guide Permission paths: 42 paths registered (6 tables × 7 CRUD ops)
64 lines
2.2 KiB
Plaintext
64 lines
2.2 KiB
Plaintext
# 热点统计:全维度
|
|
from datetime import datetime, timedelta
|
|
from sqlor.dbpools import DBPools
|
|
|
|
async def main(request):
|
|
db = DBPools()
|
|
now = datetime.now()
|
|
day_ago = (now - timedelta(hours=24)).strftime('%Y-%m-%d %H:%M:%S')
|
|
|
|
async with db.sqlorContext('sage') as sor:
|
|
items = await sor.R('hotspot_item', {'order': 'heat_score desc'})
|
|
sources = await sor.R('hotspot_source', {})
|
|
logs = await sor.sqlExe(
|
|
"select * from hotspot_fetch_log where start_time >= ${t}$",
|
|
{'t': day_ago}
|
|
)
|
|
|
|
status_count = {'emerging': 0, 'rising': 0, 'hot': 0, 'cooling': 0, 'expired': 0}
|
|
for item in items:
|
|
s = item.get('status', 'emerging')
|
|
status_count[s] = status_count.get(s, 0) + 1
|
|
|
|
sources_active = sum(1 for s in sources if s.get('enabled') == '1')
|
|
failures = sum(1 for log in logs if log.get('status') == 'failed')
|
|
|
|
# Top sources by item count
|
|
src_count = {}
|
|
for item in items:
|
|
sid = item.get('source_id', '')
|
|
src_count[sid] = src_count.get(sid, 0) + 1
|
|
|
|
top_src = sorted(src_count.items(), key=lambda x: x[1], reverse=True)[:5]
|
|
src_names = {s['id']: s.get('name', s['id']) for s in sources}
|
|
|
|
# Avg heat by category
|
|
cat_heat = {}
|
|
for item in items:
|
|
cat = item.get('category', '未分类') or '未分类'
|
|
h = float(item.get('heat_score', 0))
|
|
if cat not in cat_heat:
|
|
cat_heat[cat] = {'sum': 0, 'cnt': 0}
|
|
cat_heat[cat]['sum'] += h
|
|
cat_heat[cat]['cnt'] += 1
|
|
|
|
top_categories = sorted(
|
|
[{'name': k, 'avg': round(v['sum']/v['cnt'], 1), 'cnt': v['cnt']}
|
|
for k, v in cat_heat.items()],
|
|
key=lambda x: x['cnt'], reverse=True
|
|
)[:10]
|
|
|
|
return {
|
|
'total': len(items),
|
|
'emerging': status_count['emerging'],
|
|
'rising': status_count['rising'],
|
|
'hot': status_count['hot'],
|
|
'cooling': status_count['cooling'],
|
|
'expired': status_count['expired'],
|
|
'sources': sources_active,
|
|
'failures': failures,
|
|
'top_sources': [{'name': src_names.get(k, k), 'count': v} for k, v in top_src],
|
|
'top_categories': top_categories,
|
|
'log_count_24h': len(logs),
|
|
}
|