mirror of
https://github.com/stardrophere/InsightRadar.git
synced 2026-06-05 23:07:51 +08:00
缓存优化
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
# app/api/endpoints/events.py
|
# app/api/endpoints/events.py
|
||||||
|
import time
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from typing import List
|
from typing import Dict, List, Tuple
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
||||||
from sqlalchemy.orm import Session
|
from sqlalchemy.orm import Session
|
||||||
@@ -26,6 +27,10 @@ router = APIRouter()
|
|||||||
# 排名轨迹最多返回多少个点,避免长时间跨度下数据过大
|
# 排名轨迹最多返回多少个点,避免长时间跨度下数据过大
|
||||||
MAX_RANKING_POINTS = 30
|
MAX_RANKING_POINTS = 30
|
||||||
|
|
||||||
|
# --- 轻量级接口缓存配置 ---
|
||||||
|
_UNIFIED_EVENTS_CACHE: Dict[str, Tuple[float, PaginatedUnifiedEventResponse]] = {}
|
||||||
|
CACHE_TTL_SECONDS = 60
|
||||||
|
# ---------------------------
|
||||||
|
|
||||||
@router.get("/unified", response_model=PaginatedUnifiedEventResponse)
|
@router.get("/unified", response_model=PaginatedUnifiedEventResponse)
|
||||||
def list_unified_events(
|
def list_unified_events(
|
||||||
@@ -37,6 +42,17 @@ def list_unified_events(
|
|||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""分页返回统一事件,附带各平台热搜、排名轨迹和标签。"""
|
"""分页返回统一事件,附带各平台热搜、排名轨迹和标签。"""
|
||||||
|
|
||||||
|
# --- 1. 尝试从缓存读取 ---
|
||||||
|
cache_key = f"{min_hot}:{hours}:{sort_by}:{skip}:{limit}"
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
if cache_key in _UNIFIED_EVENTS_CACHE:
|
||||||
|
expire_time, cached_data = _UNIFIED_EVENTS_CACHE[cache_key]
|
||||||
|
if current_time < expire_time:
|
||||||
|
return cached_data
|
||||||
|
# -----------------------
|
||||||
|
|
||||||
time_limit = utcnow() - timedelta(hours=hours)
|
time_limit = utcnow() - timedelta(hours=hours)
|
||||||
|
|
||||||
# 先查总数,用于前端判断是否还有更多
|
# 先查总数,用于前端判断是否还有更多
|
||||||
@@ -149,7 +165,17 @@ def list_unified_events(
|
|||||||
)
|
)
|
||||||
|
|
||||||
has_more = (skip + limit) < total
|
has_more = (skip + limit) < total
|
||||||
return PaginatedUnifiedEventResponse(total=total, has_more=has_more, data=results)
|
response = PaginatedUnifiedEventResponse(total=total, has_more=has_more, data=results)
|
||||||
|
|
||||||
|
# --- 2. 写入缓存 ---
|
||||||
|
if len(_UNIFIED_EVENTS_CACHE) > 1000:
|
||||||
|
# 防止内存无限增长
|
||||||
|
_UNIFIED_EVENTS_CACHE.clear()
|
||||||
|
|
||||||
|
_UNIFIED_EVENTS_CACHE[cache_key] = (current_time + CACHE_TTL_SECONDS, response)
|
||||||
|
# ------------------
|
||||||
|
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@router.get("/unified/{event_id}", response_model=UnifiedEventResponse)
|
@router.get("/unified/{event_id}", response_model=UnifiedEventResponse)
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
from typing import List
|
import time
|
||||||
|
from typing import Any, Dict, List, Tuple
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||||
from sqlalchemy.exc import IntegrityError
|
from sqlalchemy.exc import IntegrityError
|
||||||
@@ -16,6 +17,16 @@ from app.services.matching_service import recommend_events_for_user
|
|||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
|
# --- 轻量级接口缓存配置 ---
|
||||||
|
_RECOMMEND_CACHE: Dict[str, Tuple[float, Any]] = {}
|
||||||
|
CACHE_TTL_SECONDS = 60
|
||||||
|
|
||||||
|
def _invalidate_user_cache(user_id: int):
|
||||||
|
"""清除某个用户的推荐结果缓存(当用户新增或删除关键词时调用)"""
|
||||||
|
keys_to_delete = [k for k in _RECOMMEND_CACHE.keys() if k.startswith(f"{user_id}:")]
|
||||||
|
for k in keys_to_delete:
|
||||||
|
_RECOMMEND_CACHE.pop(k, None)
|
||||||
|
# ---------------------------
|
||||||
|
|
||||||
def _ensure_self_access(path_user_id: int, current_user: AppUser) -> None:
|
def _ensure_self_access(path_user_id: int, current_user: AppUser) -> None:
|
||||||
"""校验路径 user_id 是否为当前登录用户本人。"""
|
"""校验路径 user_id 是否为当前登录用户本人。"""
|
||||||
@@ -79,6 +90,7 @@ def create_user_preference(
|
|||||||
)
|
)
|
||||||
|
|
||||||
db.refresh(db_obj)
|
db.refresh(db_obj)
|
||||||
|
_invalidate_user_cache(user_id) # 失效推荐缓存
|
||||||
return db_obj
|
return db_obj
|
||||||
|
|
||||||
|
|
||||||
@@ -107,6 +119,7 @@ def delete_user_preference(
|
|||||||
|
|
||||||
db.delete(preference)
|
db.delete(preference)
|
||||||
db.commit()
|
db.commit()
|
||||||
|
_invalidate_user_cache(user_id) # 失效推荐缓存
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
@@ -127,6 +140,16 @@ def recommend_events(
|
|||||||
"""基于用户兴趣词推荐事件(精确匹配 + 语义匹配)。"""
|
"""基于用户兴趣词推荐事件(精确匹配 + 语义匹配)。"""
|
||||||
_ensure_self_access(user_id, current_user)
|
_ensure_self_access(user_id, current_user)
|
||||||
|
|
||||||
|
# --- 1. 尝试从缓存读取 ---
|
||||||
|
cache_key = f"{user_id}:{min_hot}:{hours}:{limit}:{semantic_threshold}:{sort_by}"
|
||||||
|
current_time = time.time()
|
||||||
|
|
||||||
|
if cache_key in _RECOMMEND_CACHE:
|
||||||
|
expire_time, cached_data = _RECOMMEND_CACHE[cache_key]
|
||||||
|
if current_time < expire_time:
|
||||||
|
return cached_data
|
||||||
|
# -----------------------
|
||||||
|
|
||||||
matched = recommend_events_for_user(
|
matched = recommend_events_for_user(
|
||||||
db,
|
db,
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
@@ -155,8 +178,18 @@ def recommend_events(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return UserPreferenceRecommendationResponse(
|
response = UserPreferenceRecommendationResponse(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
total=len(result_data),
|
total=len(result_data),
|
||||||
data=result_data,
|
data=result_data,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# --- 2. 写入缓存 ---
|
||||||
|
if len(_RECOMMEND_CACHE) > 2000:
|
||||||
|
# 防止内存无限增长
|
||||||
|
_RECOMMEND_CACHE.clear()
|
||||||
|
|
||||||
|
_RECOMMEND_CACHE[cache_key] = (current_time + CACHE_TTL_SECONDS, response)
|
||||||
|
# ------------------
|
||||||
|
|
||||||
|
return response
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
# 由 APScheduler 每分钟调用,检查当前时刻是否有用户需要接收推送,
|
# 由 APScheduler 每分钟调用,检查当前时刻是否有用户需要接收推送,
|
||||||
# 如匹配则生成摘要邮件并发送,同时写入 DeliveryHistory 防重复。
|
# 如匹配则生成摘要邮件并发送,同时写入 DeliveryHistory 防重复。
|
||||||
import logging
|
import logging
|
||||||
|
import os
|
||||||
from logging.handlers import TimedRotatingFileHandler
|
from logging.handlers import TimedRotatingFileHandler
|
||||||
from dataclasses import dataclass, field
|
from dataclasses import dataclass, field
|
||||||
from datetime import datetime, time as dt_time, timedelta, timezone, tzinfo
|
from datetime import datetime, time as dt_time, timedelta, timezone, tzinfo
|
||||||
@@ -50,17 +51,17 @@ logger.setLevel(logging.INFO)
|
|||||||
logger.propagate = False
|
logger.propagate = False
|
||||||
|
|
||||||
# 推送时间窗口:实际执行时刻与设定时间的最大容差(分钟)
|
# 推送时间窗口:实际执行时刻与设定时间的最大容差(分钟)
|
||||||
DELIVERY_WINDOW_MINUTES = 2
|
DELIVERY_WINDOW_MINUTES = int(os.getenv("DELIVERY_WINDOW_MINUTES", 2))
|
||||||
# 同一用户两次推送之间的最小间隔(分钟)
|
# 同一用户两次推送之间的最小间隔(分钟)
|
||||||
MIN_PUSH_INTERVAL_MINUTES = 30
|
MIN_PUSH_INTERVAL_MINUTES = int(os.getenv("MIN_PUSH_INTERVAL_MINUTES", 30))
|
||||||
# 单次推送最多携带的事件数
|
# 单次推送最多携带的事件数
|
||||||
MAX_EVENTS_PER_PUSH = 12
|
MAX_EVENTS_PER_PUSH = int(os.getenv("MAX_EVENTS_PER_PUSH", 12))
|
||||||
# 默认模式热度阈值(无关键词或无匹配时使用)
|
# 默认模式热度阈值(无关键词或无匹配时使用)
|
||||||
DEFAULT_MODE_HOT_THRESHOLD = 3
|
DEFAULT_MODE_HOT_THRESHOLD = int(os.getenv("DEFAULT_MODE_HOT_THRESHOLD", 3))
|
||||||
# 默认模式查询时间窗口(小时)
|
# 默认模式查询时间窗口(小时)
|
||||||
DEFAULT_MODE_HOURS = 48
|
DEFAULT_MODE_HOURS = int(os.getenv("DEFAULT_MODE_HOURS", 24))
|
||||||
# 用户时区无效时的兜底时区
|
# 用户时区无效时的兜底时区
|
||||||
DEFAULT_FALLBACK_TIMEZONE = "Asia/Shanghai"
|
DEFAULT_FALLBACK_TIMEZONE = os.getenv("DEFAULT_FALLBACK_TIMEZONE", "Asia/Shanghai")
|
||||||
|
|
||||||
|
|
||||||
# ==========================================
|
# ==========================================
|
||||||
|
|||||||
@@ -147,9 +147,9 @@ function getPlatformColor(name: string): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getHotLevel(score: number): { label: string; color: string; bg: string } {
|
function getHotLevel(score: number): { label: string; color: string; bg: string } {
|
||||||
if (score >= 50) return { label: '全网沸腾', color: '#ef4444', bg: 'rgba(239,68,68,0.15)' }
|
if (score >= 10) return { label: '全网沸腾', color: '#ef4444', bg: 'rgba(239,68,68,0.15)' }
|
||||||
if (score >= 20) return { label: '高度关注', color: '#f97316', bg: 'rgba(249,115,22,0.15)' }
|
if (score >= 5) return { label: '高度关注', color: '#f97316', bg: 'rgba(249,115,22,0.15)' }
|
||||||
if (score >= 10) return { label: '上升中', color: '#3b82f6', bg: 'rgba(59,130,246,0.15)' }
|
if (score >= 3) return { label: '上升中', color: '#3b82f6', bg: 'rgba(59,130,246,0.15)' }
|
||||||
return { label: '一般关注', color: '#6b7280', bg: 'rgba(107,114,128,0.15)' }
|
return { label: '一般关注', color: '#6b7280', bg: 'rgba(107,114,128,0.15)' }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1171,6 +1171,7 @@ watch(() => route.query.event, (newId) => {
|
|||||||
gap: 10px;
|
gap: 10px;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
font-weight:500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.platform-info i {
|
.platform-info i {
|
||||||
|
|||||||
@@ -82,11 +82,14 @@ const revisionChains = computed<RevisionChain[]>(() => {
|
|||||||
|
|
||||||
const chains: RevisionChain[] = []
|
const chains: RevisionChain[] = []
|
||||||
for (const [event_id, items] of groups) {
|
for (const [event_id, items] of groups) {
|
||||||
|
const first = items[0]
|
||||||
|
const last = items[items.length - 1]
|
||||||
|
if (!first || !last) continue
|
||||||
// 组内按时间升序
|
// 组内按时间升序
|
||||||
items.sort((a, b) => safeParseTime(a.created_at) - safeParseTime(b.created_at))
|
items.sort((a, b) => safeParseTime(a.created_at) - safeParseTime(b.created_at))
|
||||||
|
|
||||||
// 拼接标题链,避免重复(相邻记录的 revised 与下一条 previous 通常相同)
|
// 拼接标题链,避免重复(相邻记录的 revised 与下一条 previous 通常相同)
|
||||||
const titles: string[] = [items[0].previous_headline]
|
const titles: string[] = [first.previous_headline]
|
||||||
const change_times: string[] = []
|
const change_times: string[] = []
|
||||||
for (const item of items) {
|
for (const item of items) {
|
||||||
// 若链条末尾与本条 previous 不同,说明有断层,仍然追加
|
// 若链条末尾与本条 previous 不同,说明有断层,仍然追加
|
||||||
@@ -100,13 +103,13 @@ const revisionChains = computed<RevisionChain[]>(() => {
|
|||||||
|
|
||||||
chains.push({
|
chains.push({
|
||||||
event_id,
|
event_id,
|
||||||
source_name: items[0].source_name,
|
source_name: first.source_name,
|
||||||
titles,
|
titles,
|
||||||
change_times,
|
change_times,
|
||||||
first_at: items[0].created_at,
|
first_at: first.created_at,
|
||||||
last_at: items[items.length - 1].created_at,
|
last_at: last.created_at,
|
||||||
change_count: items.length,
|
change_count: items.length,
|
||||||
url: items[0].url,
|
url: first.url,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user