diff --git a/backend/app/api/endpoints/auth.py b/backend/app/api/endpoints/auth.py
index e09d9a9..f01a519 100644
--- a/backend/app/api/endpoints/auth.py
+++ b/backend/app/api/endpoints/auth.py
@@ -69,7 +69,7 @@ def _normalize_email(email: str) -> str:
def _build_verification_email(code: str, purpose_text: str, expire_minutes: int) -> str:
return f"""
-
InsightRadar 邮箱验证
+
聚势智见邮箱验证
您的{purpose_text}验证码是:
{code}
该验证码在 {expire_minutes} 分钟内有效。请勿泄露给他人。
@@ -203,7 +203,7 @@ async def send_register_code(
await send_html_email(
to_email=email,
- subject=f"【{code}】InsightRadar 注册验证码",
+ subject=f"【{code}】聚势智见 注册验证码",
html_content=_build_verification_email(
code, "注册", REGISTER_CODE_EXPIRE_MINUTES
),
@@ -241,7 +241,7 @@ async def send_login_code(
await send_html_email(
to_email=email,
- subject=f"【{code}】InsightRadar 登录验证码",
+ subject=f"【{code}】聚势智见 登录验证码",
html_content=_build_verification_email(
code, "登录", LOGIN_CODE_EXPIRE_MINUTES
),
diff --git a/backend/app/prompts/digest_email_template.py b/backend/app/prompts/digest_email_template.py
index d34ea52..29db03d 100644
--- a/backend/app/prompts/digest_email_template.py
+++ b/backend/app/prompts/digest_email_template.py
@@ -86,7 +86,7 @@ body{{margin:0;padding:0;background:#0d1117;color:#e6edf3;font-family:-apple-sys
@@ -94,8 +94,8 @@ body{{margin:0;padding:0;background:#0d1117;color:#e6edf3;font-family:-apple-sys
{event_cards_html}
diff --git a/backend/app/services/delivery_service.py b/backend/app/services/delivery_service.py
index 541dd0f..cec2b38 100644
--- a/backend/app/services/delivery_service.py
+++ b/backend/app/services/delivery_service.py
@@ -377,7 +377,7 @@ def _prepare_user_push(db: Session, user: AppUser, schedule: UserDeliverySchedul
return _PendingPush(
user_id=user_id,
email_targets=[ep.channel_account for ep in email_endpoints],
- subject=f"InsightRadar {subject_suffix} · {time_str}",
+ subject=f"聚势智见 {subject_suffix} · {time_str}",
html_body=html_body,
event_ids=event_ids,
)
diff --git a/backend/app/services/fetcher_service.py b/backend/app/services/fetcher_service.py
index 93e0305..4cc71cc 100644
--- a/backend/app/services/fetcher_service.py
+++ b/backend/app/services/fetcher_service.py
@@ -26,9 +26,9 @@ SIMILARITY_THRESHOLD = float(os.getenv("SIMILARITY_THRESHOLD", 0.72))
API_BASE_URL = os.getenv("API_BASE_URL", "https://newsnow.busiyi.world/api/s")
EMBEDDING_MODEL_PATH = os.getenv("EMBEDDING_MODEL_PATH", "")
-print("正在加载向量模型...")
+print("正在加载 BAAI/bge-m3 向量模型...")
# 全局单例
-embedder_model = SentenceTransformer(EMBEDDING_MODEL_PATH, local_files_only=True)
+embedder_model = SentenceTransformer(EMBEDDING_MODEL_PATH, local_files_only=True, device="cuda")
print("模型加载完成。")
diff --git a/backend/app/services/matching_service.py b/backend/app/services/matching_service.py
index 0c48de5..09a814a 100644
--- a/backend/app/services/matching_service.py
+++ b/backend/app/services/matching_service.py
@@ -1,6 +1,6 @@
"""
匹配服务:根据用户兴趣关键词(精确 + 语义)推荐事件
-打分融合:匹配分 + 标签相关度 + 热度 + 新鲜度加成
+打分融合:标签/标题匹配分 + 标签相关度 + 热度 + 新鲜度加成
"""
import os
from dataclasses import dataclass
@@ -14,7 +14,7 @@ from app.models.models import ExtractedTopic, TargetType, UnifiedEvent, UserTopi
from app.services.fetcher_service import embedder_model
-# 语义匹配阈值:用户关键词和事件标签向量相似度达到该值才计入语义命中
+# 语义匹配阈值:用户关键词和事件标签/标题向量相似度达到该值才计入语义命中
DEFAULT_PREFERENCE_SEMANTIC_THRESHOLD = 0.78
PREFERENCE_SEMANTIC_THRESHOLD = float(
os.getenv("PREFERENCE_SEMANTIC_THRESHOLD", str(DEFAULT_PREFERENCE_SEMANTIC_THRESHOLD))
@@ -41,6 +41,31 @@ def _normalize_text(text: str) -> str:
return text.strip().casefold()
+def _find_exact_preference_match(
+ target_text: str,
+ normalized_preferences: list[tuple[str, str]],
+) -> str | None:
+ """
+ 判断目标文本是否与某个用户兴趣词形成“精确命中”。
+ 命中条件:
+ 1. 标准化后完全相等
+ 2. 二者互为包含关系
+ 返回命中的原始兴趣词,未命中则返回 None。
+ """
+ normalized_target = _normalize_text(target_text)
+ if not normalized_target:
+ return None
+
+ for raw_pref, normalized_pref in normalized_preferences:
+ if not normalized_pref:
+ continue
+ if normalized_target == normalized_pref:
+ return raw_pref
+ if normalized_pref in normalized_target or normalized_target in normalized_pref:
+ return raw_pref
+ return None
+
+
_EMBEDDING_CACHE: dict[str, np.ndarray] = {}
MAX_CACHE_SIZE = 10000
@@ -86,6 +111,26 @@ def _build_keyword_embedding_map(keywords: list[str]) -> dict[str, np.ndarray]:
return result
+def _find_best_semantic_match(
+ target_text: str,
+ target_vec_map: dict[str, np.ndarray],
+ pref_vec_map: dict[str, np.ndarray],
+) -> tuple[str | None, float]:
+ """返回与目标文本最接近的兴趣词及其余弦相似度。"""
+ target_vec = target_vec_map.get(target_text)
+ if target_vec is None:
+ return None, -1.0
+
+ best_pref = None
+ best_sim = -1.0
+ for pref_keyword, pref_vec in pref_vec_map.items():
+ sim = float(np.dot(target_vec, pref_vec))
+ if sim > best_sim:
+ best_sim = sim
+ best_pref = pref_keyword
+ return best_pref, best_sim
+
+
def _ensure_aware(dt: datetime) -> datetime:
"""SQLite 读出的 datetime 不带时区信息,统一补上 UTC 后才能和 utcnow() 做减法。"""
if dt.tzinfo is None:
@@ -116,8 +161,8 @@ def recommend_events_for_user(
) -> list[MatchedEventResult]:
"""
用户兴趣推荐主流程:
- 1) 精确匹配:用户词 == EVENT 标签
- 2) 语义匹配:用户词向量 vs EVENT 标签向量(超过阈值)
+ 1) 精确匹配:用户词 vs EVENT 标签/标题
+ 2) 语义匹配:用户词向量 vs EVENT 标签/标题向量(超过阈值)
3) 打分融合:匹配分 + 标签相关度 + 热度 + 新鲜度
"""
final_limit = max(1, min(limit, PREFERENCE_RECOMMEND_MAX_LIMIT))
@@ -167,8 +212,6 @@ def recommend_events_for_user(
)
.all()
)
- if not topic_rows:
- return []
# 组织事件标签映射:event_id -> [(tag, relevance_score), ...]
event_topics: dict[int, list[tuple[str, float | None]]] = {}
@@ -177,10 +220,6 @@ def recommend_events_for_user(
continue
event_topics.setdefault(event_id, []).append((topic_keyword, relevance_score))
- # 如果某事件没有标签,就不参与推荐
- if not event_topics:
- return []
-
# 3. 批量编码用户词与标签词,减少模型调用次数
unique_preference_keywords = list(dict.fromkeys(preference_keywords))
unique_topic_keywords = list(dict.fromkeys([row[1] for row in topic_rows if row[1]]))
@@ -188,13 +227,21 @@ def recommend_events_for_user(
topic_vec_map = _build_keyword_embedding_map(unique_topic_keywords)
# 预先建立“标准化后用户词集合”,用于精确匹配
- normalized_pref_set = {_normalize_text(word) for word in unique_preference_keywords}
+ normalized_preference_pairs = [
+ (word, _normalize_text(word))
+ for word in unique_preference_keywords
+ if _normalize_text(word)
+ ]
+ unique_event_titles = list(
+ dict.fromkeys(
+ [event.unified_title.strip() for event in events if event.unified_title and event.unified_title.strip()]
+ )
+ )
+ title_vec_map = _build_keyword_embedding_map(unique_event_titles)
scored_results: list[MatchedEventResult] = []
for event in events:
topic_list = event_topics.get(event.id, [])
- if not topic_list:
- continue
exact_hits: list[str] = []
semantic_hits: list[dict[str, Any]] = []
@@ -202,37 +249,18 @@ def recommend_events_for_user(
# 对每个事件标签做精确匹配或语义匹配
for topic_keyword, topic_relevance in topic_list:
- normalized_topic = _normalize_text(topic_keyword)
topic_relevance_score = float(topic_relevance) if topic_relevance is not None else 50.0
# 1) 精确命中(包括完全相等与包含关系)
- matched_exact = False
- if normalized_topic in normalized_pref_set:
- matched_exact = True
- else:
- for pref_word in normalized_pref_set:
- if pref_word and (pref_word in normalized_topic or normalized_topic in pref_word):
- matched_exact = True
- break
-
- if matched_exact:
+ matched_pref = _find_exact_preference_match(topic_keyword, normalized_preference_pairs)
+ if matched_pref is not None:
exact_hits.append(topic_keyword)
# 精确命中给较高基础分,标签自身相关度作为增益
score += 45.0 + topic_relevance_score * 0.2
continue
# 2) 语义命中(未精确命中时再算)
- topic_vec = topic_vec_map.get(topic_keyword)
- if topic_vec is None:
- continue
-
- best_pref = None
- best_sim = -1.0
- for pref_keyword, pref_vec in pref_vec_map.items():
- sim = float(np.dot(topic_vec, pref_vec))
- if sim > best_sim:
- best_sim = sim
- best_pref = pref_keyword
+ best_pref, best_sim = _find_best_semantic_match(topic_keyword, topic_vec_map, pref_vec_map)
if best_pref is not None and best_sim >= similarity_threshold:
semantic_hits.append(
@@ -245,6 +273,25 @@ def recommend_events_for_user(
# 语义命中分略低于精确命中,并由相似度放大
score += best_sim * 35.0 + topic_relevance_score * 0.12
+ # 标题也参与匹配,但权重低于结构化标签,避免长标题过度主导排序。
+ event_title = (event.unified_title or "").strip()
+ if event_title:
+ title_exact_pref = _find_exact_preference_match(event_title, normalized_preference_pairs)
+ if title_exact_pref is not None:
+ exact_hits.append(f"标题:{title_exact_pref}")
+ score += 30.0
+ else:
+ best_pref, best_sim = _find_best_semantic_match(event_title, title_vec_map, pref_vec_map)
+ if best_pref is not None and best_sim >= similarity_threshold:
+ semantic_hits.append(
+ {
+ "preference_keyword": best_pref,
+ "topic_keyword": f"标题:{best_pref}",
+ "similarity": round(best_sim, 4),
+ }
+ )
+ score += best_sim * 24.0
+
# 如果精确和语义都没命中,直接跳过
if not exact_hits and not semantic_hits:
continue
diff --git a/frontend/index.html b/frontend/index.html
index ad5abb2..cc7a62f 100644
--- a/frontend/index.html
+++ b/frontend/index.html
@@ -4,7 +4,7 @@
-
InsightRadar - 全网热点监控中枢
+
聚势智见 - 基于语义聚类与大模型的热点资讯聚合平台
diff --git a/frontend/src/components/UnifiedEventCard.vue b/frontend/src/components/UnifiedEventCard.vue
index cd0aa4e..9d0000f 100644
--- a/frontend/src/components/UnifiedEventCard.vue
+++ b/frontend/src/components/UnifiedEventCard.vue
@@ -111,6 +111,14 @@ function getRankingChartOptions(history: number[], platformColor: string) {
height: 56,
sparkline: { enabled: true },
animations: { enabled: true, easing: 'easeinout' as const, speed: 400 },
+ events: {
+ mounted: (chartContext: any) => {
+ chartContext.el?.querySelector('.apexcharts-svg > title')?.remove()
+ },
+ updated: (chartContext: any) => {
+ chartContext.el?.querySelector('.apexcharts-svg > title')?.remove()
+ }
+ }
},
stroke: { curve: 'smooth' as const, width: 2 },
fill: {
diff --git a/frontend/src/layouts/DashboardLayout.vue b/frontend/src/layouts/DashboardLayout.vue
index 61087d6..3003db4 100644
--- a/frontend/src/layouts/DashboardLayout.vue
+++ b/frontend/src/layouts/DashboardLayout.vue
@@ -57,7 +57,7 @@ function toggleSidebar() {
diff --git a/frontend/src/views/AboutView.vue b/frontend/src/views/AboutView.vue
index 4b0f3e3..aff26f1 100644
--- a/frontend/src/views/AboutView.vue
+++ b/frontend/src/views/AboutView.vue
@@ -1,7 +1,7 @@
-
关于 InsightRadar
+ 关于 聚势智见
diff --git a/frontend/src/views/DashboardView.vue b/frontend/src/views/DashboardView.vue
index d7521e9..ab5d240 100644
--- a/frontend/src/views/DashboardView.vue
+++ b/frontend/src/views/DashboardView.vue
@@ -182,6 +182,14 @@ function getRankingChartOptions(history: number[], platformColor: string) {
height: 56,
sparkline: { enabled: true },
animations: { enabled: true, easing: 'easeinout' as const, speed: 400 },
+ events: {
+ mounted: (chartContext: any) => {
+ chartContext.el?.querySelector('.apexcharts-svg > title')?.remove()
+ },
+ updated: (chartContext: any) => {
+ chartContext.el?.querySelector('.apexcharts-svg > title')?.remove()
+ }
+ }
},
stroke: { curve: 'smooth' as const, width: 2 },
fill: {
diff --git a/frontend/src/views/HomeView.vue b/frontend/src/views/HomeView.vue
index c3b4587..ba4c576 100644
--- a/frontend/src/views/HomeView.vue
+++ b/frontend/src/views/HomeView.vue
@@ -31,7 +31,7 @@ async function handleLogout() {
diff --git a/frontend/src/views/LoginView.vue b/frontend/src/views/LoginView.vue
index 304176e..38bd7ec 100644
--- a/frontend/src/views/LoginView.vue
+++ b/frontend/src/views/LoginView.vue
@@ -150,7 +150,7 @@ onUnmounted(() => {
- InsightRadar
+ 聚势智见
洞察全网热点
让信息更聚焦
@@ -192,7 +192,7 @@ onUnmounted(() => {