feat: 热词热力图改为Treemap矩形堆砌布局,面积代表提及次数;赛道分析页面布局调整
This commit is contained in:
+87
-27
@@ -45,9 +45,11 @@ def now_iso() -> str:
|
||||
|
||||
|
||||
def db() -> sqlite3.Connection:
|
||||
conn = sqlite3.connect(DB_PATH)
|
||||
conn = sqlite3.connect(DB_PATH, timeout=10)
|
||||
conn.row_factory = sqlite3.Row
|
||||
conn.execute("PRAGMA journal_mode = WAL")
|
||||
conn.execute("PRAGMA foreign_keys = ON")
|
||||
conn.execute("PRAGMA busy_timeout = 5000")
|
||||
return conn
|
||||
|
||||
|
||||
@@ -160,7 +162,7 @@ def dashboard():
|
||||
"type": r["type"] or "经营干货",
|
||||
"score": r["score"] or 0,
|
||||
"summary": r["ai_summary"] or "",
|
||||
"entities": json.loads(r["entities"]) if r["entities"] else [],
|
||||
"entities": [b["name"] if isinstance(b, dict) else b for b in (json.loads(r["entities"]) if r["entities"] else [])],
|
||||
}
|
||||
for r in top_intel
|
||||
],
|
||||
@@ -251,7 +253,7 @@ def intel_list():
|
||||
"summary": r["ai_summary"] or r["summary"] or "",
|
||||
"keyPoints": json.loads(r["key_points"]) if r["key_points"] else [],
|
||||
"score": r["score"] or 0,
|
||||
"entities": json.loads(r["entities"]) if r["entities"] else [],
|
||||
"entities": [b["name"] if isinstance(b, dict) else b for b in (json.loads(r["entities"]) if r["entities"] else [])],
|
||||
"timeliness": r["timeliness"] or "中",
|
||||
"tags": json.loads(r["tags"]) if r["tags"] else [],
|
||||
"url": r["canonical_url"],
|
||||
@@ -393,26 +395,41 @@ def brand_detail(brand_id: int):
|
||||
@app.get("/api/analysis")
|
||||
def analysis():
|
||||
with db() as conn:
|
||||
# 赛道热度趋势:按月统计各赛道的平均评分
|
||||
# 赛道热度趋势:按月统计各赛道的文章数和平均评分,热度=文章数×平均评分/10
|
||||
heat_rows = conn.execute(
|
||||
"""
|
||||
SELECT strftime('%m', a.published_at) AS month,
|
||||
SELECT strftime('%Y-%m', a.published_at) AS month,
|
||||
a.category,
|
||||
AVG(CAST(json_extract(x.result_json, '$.score') AS REAL)) AS heat
|
||||
COUNT(*) AS article_cnt,
|
||||
AVG(CAST(json_extract(x.result_json, '$.score') AS REAL)) AS avg_score
|
||||
FROM articles a
|
||||
LEFT JOIN ai_analyses x ON x.article_id = a.id AND x.analysis_type = 'editorial'
|
||||
WHERE a.crawl_status = 'success' AND a.category IS NOT NULL
|
||||
WHERE a.crawl_status = 'success' AND a.category IS NOT NULL AND a.category != ''
|
||||
AND a.published_at >= date('now', '-12 months')
|
||||
GROUP BY month, a.category
|
||||
ORDER BY month
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
# 开关店对比(从文章中提取的品类趋势数据)
|
||||
category_counts = conn.execute(
|
||||
# 品牌门店规模(按赛道聚合真实门店总数)
|
||||
brand_stores = conn.execute(
|
||||
"""
|
||||
SELECT a.category, COUNT(*) AS cnt
|
||||
SELECT category, SUM(stores) AS total_stores, COUNT(*) AS brand_cnt
|
||||
FROM brands WHERE category IS NOT NULL AND category != '' AND stores > 0
|
||||
GROUP BY category
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
# 舆情分布(按赛道统计正面/负面/中性文章数)
|
||||
sentiment_rows = conn.execute(
|
||||
"""
|
||||
SELECT a.category,
|
||||
SUM(CASE WHEN json_extract(x.result_json, '$.sentiment') = 'positive' THEN 1 ELSE 0 END) AS positive_cnt,
|
||||
SUM(CASE WHEN json_extract(x.result_json, '$.sentiment') = 'negative' THEN 1 ELSE 0 END) AS negative_cnt,
|
||||
SUM(CASE WHEN json_extract(x.result_json, '$.sentiment') = 'neutral' OR json_extract(x.result_json, '$.sentiment') IS NULL THEN 1 ELSE 0 END) AS neutral_cnt
|
||||
FROM articles a
|
||||
WHERE a.crawl_status = 'success' AND a.category IS NOT NULL
|
||||
JOIN ai_analyses x ON x.article_id = a.id AND x.analysis_type = 'editorial'
|
||||
WHERE a.crawl_status = 'success' AND a.category IS NOT NULL AND a.category != ''
|
||||
GROUP BY a.category
|
||||
"""
|
||||
).fetchall()
|
||||
@@ -432,29 +449,55 @@ def analysis():
|
||||
"""
|
||||
).fetchone()
|
||||
|
||||
# 热词
|
||||
tag_rows = conn.execute(
|
||||
# 热词热力图:近7天每天各标签出现次数
|
||||
tag_heatmap = conn.execute(
|
||||
"""
|
||||
SELECT value AS tag, COUNT(*) AS cnt
|
||||
FROM ai_analyses, json_each(json_extract(result_json, '$.tags'))
|
||||
GROUP BY value
|
||||
ORDER BY cnt DESC
|
||||
LIMIT 8
|
||||
SELECT value AS tag,
|
||||
strftime('%Y-%m-%d', a.published_at) AS day,
|
||||
COUNT(*) AS cnt
|
||||
FROM ai_analyses x
|
||||
JOIN articles a ON a.id = x.article_id
|
||||
, json_each(json_extract(x.result_json, '$.tags'))
|
||||
WHERE a.published_at >= date('now', '-6 days')
|
||||
AND value IS NOT NULL AND value != ''
|
||||
GROUP BY tag, day
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
# 构建热度趋势数据
|
||||
# 热词总量排行(用于选取 TOP 标签)
|
||||
tag_total = conn.execute(
|
||||
"""
|
||||
SELECT value AS tag, COUNT(*) AS cnt
|
||||
FROM ai_analyses x
|
||||
JOIN articles a ON a.id = x.article_id
|
||||
, json_each(json_extract(x.result_json, '$.tags'))
|
||||
WHERE a.published_at >= date('now', '-6 days')
|
||||
AND value IS NOT NULL AND value != ''
|
||||
GROUP BY value
|
||||
ORDER BY cnt DESC
|
||||
LIMIT 10
|
||||
"""
|
||||
).fetchall()
|
||||
|
||||
# 构建热度趋势数据(热度 = 文章数 × 平均评分 / 10)
|
||||
heat_trend = {}
|
||||
for r in heat_rows:
|
||||
month_key = f"{r['month']}月" if r["month"] else "未知"
|
||||
month_key = f"{r['month'][-2:]}月" if r["month"] else "未知"
|
||||
if month_key not in heat_trend:
|
||||
heat_trend[month_key] = {"month": month_key}
|
||||
heat_trend[month_key][r["category"]] = round(r["heat"] or 0)
|
||||
heat_val = round((r["article_cnt"] or 0) * (r["avg_score"] or 0) / 10)
|
||||
heat_trend[month_key][r["category"]] = heat_val
|
||||
|
||||
# 构建开关店数据
|
||||
open_close = [
|
||||
{"category": r["category"], "新开": r["cnt"], "关闭": max(0, r["cnt"] - int(r["cnt"] * 0.85))}
|
||||
for r in category_counts
|
||||
# 构建品牌门店规模数据
|
||||
brand_stores_data = [
|
||||
{"category": r["category"], "stores": r["total_stores"] or 0, "brands": r["brand_cnt"]}
|
||||
for r in brand_stores
|
||||
]
|
||||
|
||||
# 构建舆情分布数据
|
||||
sentiment_data = [
|
||||
{"category": r["category"], "正面": r["positive_cnt"] or 0, "负面": r["negative_cnt"] or 0, "中性": r["neutral_cnt"] or 0}
|
||||
for r in sentiment_rows
|
||||
]
|
||||
|
||||
# 构建价格分布
|
||||
@@ -468,11 +511,28 @@ def analysis():
|
||||
{"band": "150元以上", "pct": round((price_rows["band6"] or 0) * 100 / total_brands)},
|
||||
]
|
||||
|
||||
# 构建热词热力图数据(用 UTC 日期与 SQLite date('now') 保持一致)
|
||||
from datetime import timedelta, timezone
|
||||
today_utc = datetime.now(timezone.utc).date()
|
||||
days = [(today_utc - timedelta(days=i)).isoformat() for i in range(6, -1, -1)]
|
||||
top_tags = [r["tag"] for r in tag_total]
|
||||
# 构建矩阵 {tag: {day: cnt}}
|
||||
matrix = {tag: {day: 0 for day in days} for tag in top_tags}
|
||||
for r in tag_heatmap:
|
||||
if r["tag"] in matrix and r["day"] in matrix[r["tag"]]:
|
||||
matrix[r["tag"]][r["day"]] = r["cnt"]
|
||||
hot_keywords = {
|
||||
"days": days,
|
||||
"tags": top_tags,
|
||||
"matrix": matrix,
|
||||
}
|
||||
|
||||
return jsonify({
|
||||
"categoryHeatTrend": list(heat_trend.values()),
|
||||
"openCloseByCategory": open_close,
|
||||
"brandStoresByCategory": brand_stores_data,
|
||||
"sentimentByCategory": sentiment_data,
|
||||
"priceBandData": price_bands,
|
||||
"hotKeywords": [{"word": r["tag"], "change": min(r["cnt"] * 5, 50)} for r in tag_rows],
|
||||
"hotKeywords": hot_keywords,
|
||||
})
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user