big update

This commit is contained in:
stardrophere
2026-03-11 20:52:58 +08:00
parent 8ed819a580
commit 966bcfbba4
44 changed files with 7124 additions and 650 deletions
+192 -46
View File
@@ -1,69 +1,215 @@
# app/api/endpoints/events.py
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from datetime import timedelta
from typing import List
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.orm import Session
from app.api.dependencies import get_db
from app.models.models import UnifiedEvent, TrendingEvent, InfoSource, RankingLog, utcnow
# 导入你上传的 Schema
from app.schemas.event_schema import UnifiedEventResponse, PlatformTrendResponse
from app.models.models import (
ExtractedTopic,
InfoSource,
RankingLog,
TargetType,
TrendingEvent,
UnifiedEvent,
utcnow,
)
from app.schemas.event_schema import (
PaginatedUnifiedEventResponse,
PlatformTrendResponse,
UnifiedEventResponse,
)
router = APIRouter()
# 排名轨迹最多返回多少个点,避免长时间跨度下数据过大
MAX_RANKING_POINTS = 30
@router.get("/unified", response_model=List[UnifiedEventResponse])
@router.get("/unified", response_model=PaginatedUnifiedEventResponse)
def list_unified_events(
min_hot: int = Query(5, description="热度过滤阈值"),
hours: int = Query(24, description="查询过去 X 小时的数据"),
db: Session = Depends(get_db)
min_hot: int = Query(5, ge=0, description="热度阈值,仅返回 hot_score >= 此值的事件"),
hours: int = Query(24, ge=1, le=720, description="查询最近多少小时的数据"),
skip: int = Query(0, ge=0, description="分页偏移量"),
limit: int = Query(10, ge=1, le=50, description="每页返回条数"),
db: Session = Depends(get_db),
):
"""
获取聚合大事件列表,完全适配前端 template.html 所需的数据结构
"""
# 计算时间水位线
"""分页返回统一事件,附带各平台热搜、排名轨迹和标签。"""
time_limit = utcnow() - timedelta(hours=hours)
# 1. 查询大事件(按热度降序,且满足时间范围)
events = db.query(UnifiedEvent).filter(
# 先查总数,用于前端判断是否还有更多
base_query = db.query(UnifiedEvent).filter(
UnifiedEvent.hot_score >= min_hot,
UnifiedEvent.created_at >= time_limit
).order_by(UnifiedEvent.hot_score.desc()).all()
UnifiedEvent.created_at >= time_limit,
)
total = base_query.count()
results = []
# 分页查询
events = (
base_query
.order_by(UnifiedEvent.hot_score.desc())
.offset(skip)
.limit(limit)
.all()
)
if not events:
return PaginatedUnifiedEventResponse(total=total, has_more=False, data=[])
event_ids = [ev.id for ev in events]
# 批量查询所有相关的热搜条目(避免 N+1)
trend_rows = (
db.query(TrendingEvent, InfoSource.source_name)
.join(InfoSource, TrendingEvent.source_id == InfoSource.id)
.filter(TrendingEvent.unified_event_id.in_(event_ids))
.all()
)
# 按 unified_event_id 分组
trend_map: dict[int, list[tuple]] = {}
trend_ids: list[int] = []
for trend, source_name in trend_rows:
trend_map.setdefault(trend.unified_event_id, []).append((trend, source_name))
trend_ids.append(trend.id)
# 批量查询排名日志(避免逐条查询)
ranking_map: dict[int, list[int]] = {}
if trend_ids:
ranking_rows = (
db.query(
RankingLog.event_id,
RankingLog.ranking_position,
)
.filter(
RankingLog.event_id.in_(trend_ids),
RankingLog.observed_at >= time_limit,
)
.order_by(RankingLog.event_id, RankingLog.observed_at.asc())
.all()
)
for event_id, position in ranking_rows:
ranking_map.setdefault(event_id, []).append(position)
# 批量查询标签
tag_map: dict[int, list[str]] = {}
tag_rows = (
db.query(ExtractedTopic.target_id, ExtractedTopic.topic_keyword)
.filter(
ExtractedTopic.target_type == TargetType.EVENT,
ExtractedTopic.target_id.in_(event_ids),
)
.order_by(ExtractedTopic.relevance_score.desc(), ExtractedTopic.created_at.desc())
.all()
)
for target_id, keyword in tag_rows:
tag_map.setdefault(target_id, []).append(keyword)
# 组装响应
results: list[UnifiedEventResponse] = []
for ev in events:
# 2. 联表查询:获取该大事件下关联的所有平台及其具体热搜信息
trends = db.query(TrendingEvent, InfoSource.source_name).join(
InfoSource, TrendingEvent.source_id == InfoSource.id
).filter(TrendingEvent.unified_event_id == ev.id).all()
platform_list: list[PlatformTrendResponse] = []
for trend, source_name in trend_map.get(ev.id, []):
history = ranking_map.get(trend.id, [])
# 截取尾部,只保留最近的点
if len(history) > MAX_RANKING_POINTS:
history = history[-MAX_RANKING_POINTS:]
platform_list = []
for trend, s_name in trends:
# 3. 获取排名历史轨迹 (用于前端渲染)
# 这里的排序顺序 asc 保证了数组从旧到新
logs = db.query(RankingLog.ranking_position).filter(
RankingLog.event_id == trend.id,
RankingLog.observed_at >= time_limit
).order_by(RankingLog.observed_at.asc()).all()
platform_list.append(
PlatformTrendResponse(
source_id=trend.source_id,
platform_name=source_name,
headline=trend.current_headline,
url=trend.event_url,
current_ranking=trend.current_ranking,
ranking_history=history,
)
)
# 组装符合 PlatformTrendResponse 结构的字典
platform_list.append(PlatformTrendResponse(
results.append(
UnifiedEventResponse(
event_id=ev.id,
unified_title=ev.unified_title if ev.unified_title else "暂无标题",
summary=ev.ai_comprehensive_summary,
hot_score=ev.hot_score,
created_at=ev.created_at,
platforms=platform_list,
tags=tag_map.get(ev.id, []),
)
)
has_more = (skip + limit) < total
return PaginatedUnifiedEventResponse(total=total, has_more=has_more, data=results)
@router.get("/unified/{event_id}", response_model=UnifiedEventResponse)
def get_unified_event(
event_id: int,
db: Session = Depends(get_db),
):
"""按 ID 查询单个统一事件,用于推荐跳转时的聚光灯展示。"""
ev = db.query(UnifiedEvent).filter(UnifiedEvent.id == event_id).first()
if not ev:
raise HTTPException(status_code=404, detail="Event not found")
time_limit = utcnow() - timedelta(hours=720)
trend_rows = (
db.query(TrendingEvent, InfoSource.source_name)
.join(InfoSource, TrendingEvent.source_id == InfoSource.id)
.filter(TrendingEvent.unified_event_id == event_id)
.all()
)
trend_ids = [t.id for t, _ in trend_rows]
ranking_map: dict[int, list[int]] = {}
if trend_ids:
ranking_rows = (
db.query(RankingLog.event_id, RankingLog.ranking_position)
.filter(
RankingLog.event_id.in_(trend_ids),
RankingLog.observed_at >= time_limit,
)
.order_by(RankingLog.event_id, RankingLog.observed_at.asc())
.all()
)
for eid, pos in ranking_rows:
ranking_map.setdefault(eid, []).append(pos)
tag_rows = (
db.query(ExtractedTopic.topic_keyword)
.filter(
ExtractedTopic.target_type == TargetType.EVENT,
ExtractedTopic.target_id == event_id,
)
.order_by(ExtractedTopic.relevance_score.desc())
.all()
)
tags = [row[0] for row in tag_rows]
platform_list: list[PlatformTrendResponse] = []
for trend, source_name in trend_rows:
history = ranking_map.get(trend.id, [])
if len(history) > MAX_RANKING_POINTS:
history = history[-MAX_RANKING_POINTS:]
platform_list.append(
PlatformTrendResponse(
source_id=trend.source_id,
platform_name=s_name,
platform_name=source_name,
headline=trend.current_headline,
url=trend.event_url,
current_ranking=trend.current_ranking,
ranking_history=[log[0] for log in logs]
))
ranking_history=history,
)
)
# 4. 组装符合 UnifiedEventResponse 结构的字典
results.append(UnifiedEventResponse(
event_id=ev.id,
unified_title=ev.unified_title if ev.unified_title else "暂无标题",
summary=ev.ai_comprehensive_summary,
hot_score=ev.hot_score,
created_at=ev.created_at,
platforms=platform_list
))
return results
return UnifiedEventResponse(
event_id=ev.id,
unified_title=ev.unified_title if ev.unified_title else "暂无标题",
summary=ev.ai_comprehensive_summary,
hot_score=ev.hot_score,
created_at=ev.created_at,
platforms=platform_list,
tags=tags,
)