|
|
@@ -17,12 +17,10 @@ import os
|
|
|
import sys
|
|
|
import argparse
|
|
|
import traceback
|
|
|
+import requests
|
|
|
from datetime import datetime, date
|
|
|
from pathlib import Path
|
|
|
|
|
|
-import pymysql
|
|
|
-from pymysql.cursors import DictCursor
|
|
|
-
|
|
|
# 确保当前目录在 Python 路径中
|
|
|
CURRENT_DIR = Path(__file__).parent.resolve()
|
|
|
if str(CURRENT_DIR) not in sys.path:
|
|
|
@@ -116,29 +114,32 @@ HEADLESS_MODE = os.environ.get('HEADLESS', 'true').lower() == 'true'
|
|
|
print(f"[Config] HEADLESS env value: '{os.environ.get('HEADLESS', 'NOT SET')}'", flush=True)
|
|
|
print(f"[Config] HEADLESS_MODE: {HEADLESS_MODE}", flush=True)
|
|
|
|
|
|
-# 数据库配置
|
|
|
-DB_CONFIG = {
|
|
|
- 'host': os.environ.get('DB_HOST', 'localhost'),
|
|
|
- 'port': int(os.environ.get('DB_PORT', 3306)),
|
|
|
- 'user': os.environ.get('DB_USERNAME', 'root'),
|
|
|
- 'password': os.environ.get('DB_PASSWORD', ''),
|
|
|
- 'database': os.environ.get('DB_DATABASE', 'media_manager'),
|
|
|
- 'charset': 'utf8mb4',
|
|
|
- 'cursorclass': DictCursor
|
|
|
-}
|
|
|
-print(f"[DB_CONFIG] host={DB_CONFIG['host']}, port={DB_CONFIG['port']}, user={DB_CONFIG['user']}, db={DB_CONFIG['database']}, pwd_len={len(DB_CONFIG['password'])}", flush=True)
|
|
|
-
|
|
|
-
|
|
|
-def get_db_connection():
|
|
|
- """获取数据库连接"""
|
|
|
- print(f"[DEBUG DB] 正在连接数据库...", flush=True)
|
|
|
- print(f"[DEBUG DB] host={DB_CONFIG['host']}, port={DB_CONFIG['port']}, user={DB_CONFIG['user']}, db={DB_CONFIG['database']}", flush=True)
|
|
|
+# Node.js API 配置
|
|
|
+NODEJS_API_BASE_URL = os.environ.get('NODEJS_API_URL', 'http://localhost:3000')
|
|
|
+INTERNAL_API_KEY = os.environ.get('INTERNAL_API_KEY', 'internal-api-key-default')
|
|
|
+print(f"[API Config] Node.js API: {NODEJS_API_BASE_URL}", flush=True)
|
|
|
+
|
|
|
+
|
|
|
+def call_nodejs_api(method: str, endpoint: str, data: dict = None, params: dict = None) -> dict:
|
|
|
+ """调用 Node.js 内部 API"""
|
|
|
+ url = f"{NODEJS_API_BASE_URL}/api/internal{endpoint}"
|
|
|
+ headers = {
|
|
|
+ 'Content-Type': 'application/json',
|
|
|
+ 'X-Internal-API-Key': INTERNAL_API_KEY,
|
|
|
+ }
|
|
|
+
|
|
|
try:
|
|
|
- conn = pymysql.connect(**DB_CONFIG)
|
|
|
- print(f"[DEBUG DB] 数据库连接成功!", flush=True)
|
|
|
- return conn
|
|
|
- except Exception as e:
|
|
|
- print(f"[DEBUG DB] 数据库连接失败: {e}", flush=True)
|
|
|
+ if method.upper() == 'GET':
|
|
|
+ response = requests.get(url, headers=headers, params=params, timeout=30)
|
|
|
+ elif method.upper() == 'POST':
|
|
|
+ response = requests.post(url, headers=headers, json=data, timeout=30)
|
|
|
+ else:
|
|
|
+ raise ValueError(f"Unsupported HTTP method: {method}")
|
|
|
+
|
|
|
+ response.raise_for_status()
|
|
|
+ return response.json()
|
|
|
+ except requests.exceptions.RequestException as e:
|
|
|
+ print(f"[API Error] 调用 Node.js API 失败: {e}", flush=True)
|
|
|
raise
|
|
|
|
|
|
# ==================== 签名相关(小红书专用) ====================
|
|
|
@@ -643,70 +644,16 @@ def save_work_day_statistics():
|
|
|
if not statistics_list:
|
|
|
return jsonify({"success": False, "error": "缺少 statistics 参数"}), 400
|
|
|
|
|
|
- today = date.today()
|
|
|
- inserted_count = 0
|
|
|
- updated_count = 0
|
|
|
-
|
|
|
print(f"[WorkDayStatistics] 收到请求: {len(statistics_list)} 条统计数据")
|
|
|
|
|
|
- conn = get_db_connection()
|
|
|
- try:
|
|
|
- with conn.cursor() as cursor:
|
|
|
- for stat in statistics_list:
|
|
|
- work_id = stat.get("work_id")
|
|
|
- if not work_id:
|
|
|
- continue
|
|
|
-
|
|
|
- fans_count = stat.get("fans_count", 0)
|
|
|
- play_count = stat.get("play_count", 0)
|
|
|
- like_count = stat.get("like_count", 0)
|
|
|
- comment_count = stat.get("comment_count", 0)
|
|
|
- share_count = stat.get("share_count", 0)
|
|
|
- collect_count = stat.get("collect_count", 0)
|
|
|
-
|
|
|
- # 检查当天是否已有记录
|
|
|
- cursor.execute(
|
|
|
- "SELECT id FROM work_day_statistics WHERE work_id = %s AND record_date = %s",
|
|
|
- (work_id, today)
|
|
|
- )
|
|
|
- existing = cursor.fetchone()
|
|
|
-
|
|
|
- if existing:
|
|
|
- # 更新已有记录
|
|
|
- cursor.execute(
|
|
|
- """UPDATE work_day_statistics
|
|
|
- SET fans_count = %s, play_count = %s, like_count = %s,
|
|
|
- comment_count = %s, share_count = %s, collect_count = %s,
|
|
|
- updated_at = NOW()
|
|
|
- WHERE id = %s""",
|
|
|
- (fans_count, play_count, like_count, comment_count,
|
|
|
- share_count, collect_count, existing['id'])
|
|
|
- )
|
|
|
- updated_count += 1
|
|
|
- else:
|
|
|
- # 插入新记录
|
|
|
- cursor.execute(
|
|
|
- """INSERT INTO work_day_statistics
|
|
|
- (work_id, record_date, fans_count, play_count, like_count,
|
|
|
- comment_count, share_count, collect_count, created_at, updated_at)
|
|
|
- VALUES (%s, %s, %s, %s, %s, %s, %s, %s, NOW(), NOW())""",
|
|
|
- (work_id, today, fans_count, play_count, like_count,
|
|
|
- comment_count, share_count, collect_count)
|
|
|
- )
|
|
|
- inserted_count += 1
|
|
|
-
|
|
|
- conn.commit()
|
|
|
- finally:
|
|
|
- conn.close()
|
|
|
+ # 调用 Node.js API 保存数据
|
|
|
+ result = call_nodejs_api('POST', '/work-day-statistics', {
|
|
|
+ 'statistics': statistics_list
|
|
|
+ })
|
|
|
|
|
|
- print(f"[WorkDayStatistics] 完成: 新增 {inserted_count} 条, 更新 {updated_count} 条")
|
|
|
+ print(f"[WorkDayStatistics] 完成: 新增 {result.get('inserted', 0)} 条, 更新 {result.get('updated', 0)} 条")
|
|
|
|
|
|
- return jsonify({
|
|
|
- "success": True,
|
|
|
- "inserted": inserted_count,
|
|
|
- "updated": updated_count,
|
|
|
- "message": f"保存成功: 新增 {inserted_count} 条, 更新 {updated_count} 条"
|
|
|
- })
|
|
|
+ return jsonify(result)
|
|
|
|
|
|
except Exception as e:
|
|
|
traceback.print_exc()
|
|
|
@@ -749,114 +696,20 @@ def get_statistics_trend():
|
|
|
if not user_id:
|
|
|
return jsonify({"success": False, "error": "缺少 user_id 参数"}), 400
|
|
|
|
|
|
- conn = get_db_connection()
|
|
|
- try:
|
|
|
- with conn.cursor() as cursor:
|
|
|
- # 构建查询:关联 works 表获取用户的作品,然后汇总统计数据
|
|
|
- # 注意:粉丝数是账号级别的数据,每个账号每天只取一个值(使用 MAX)
|
|
|
- # 其他指标(播放、点赞等)是作品级别的数据,需要累加
|
|
|
- sql = """
|
|
|
- SELECT
|
|
|
- record_date,
|
|
|
- SUM(account_fans) as total_fans,
|
|
|
- SUM(account_views) as total_views,
|
|
|
- SUM(account_likes) as total_likes,
|
|
|
- SUM(account_comments) as total_comments,
|
|
|
- SUM(account_shares) as total_shares,
|
|
|
- SUM(account_collects) as total_collects
|
|
|
- FROM (
|
|
|
- SELECT
|
|
|
- wds.record_date,
|
|
|
- w.accountId,
|
|
|
- MAX(wds.fans_count) as account_fans,
|
|
|
- SUM(wds.play_count) as account_views,
|
|
|
- SUM(wds.like_count) as account_likes,
|
|
|
- SUM(wds.comment_count) as account_comments,
|
|
|
- SUM(wds.share_count) as account_shares,
|
|
|
- SUM(wds.collect_count) as account_collects
|
|
|
- FROM work_day_statistics wds
|
|
|
- INNER JOIN works w ON wds.work_id = w.id
|
|
|
- WHERE w.userId = %s
|
|
|
- """
|
|
|
- params = [user_id]
|
|
|
-
|
|
|
- # 支持两种日期筛选方式:start_date/end_date 或 days
|
|
|
- if start_date and end_date:
|
|
|
- sql += " AND wds.record_date >= %s AND wds.record_date <= %s"
|
|
|
- params.extend([start_date, end_date])
|
|
|
- else:
|
|
|
- # 默认使用 days 参数
|
|
|
- days_value = min(int(days or 7), 30)
|
|
|
- sql += " AND wds.record_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)"
|
|
|
- params.append(days_value)
|
|
|
-
|
|
|
- if account_id:
|
|
|
- sql += " AND w.accountId = %s"
|
|
|
- params.append(account_id)
|
|
|
-
|
|
|
- sql += """
|
|
|
- GROUP BY wds.record_date, w.accountId
|
|
|
- ) as account_stats
|
|
|
- GROUP BY record_date
|
|
|
- ORDER BY record_date ASC
|
|
|
- """
|
|
|
-
|
|
|
- cursor.execute(sql, params)
|
|
|
- results = cursor.fetchall()
|
|
|
-
|
|
|
- # 构建响应数据
|
|
|
- dates = []
|
|
|
- fans = []
|
|
|
- views = []
|
|
|
- likes = []
|
|
|
- comments = []
|
|
|
- shares = []
|
|
|
- collects = []
|
|
|
-
|
|
|
- for row in results:
|
|
|
- # 格式化日期为 "MM-DD" 格式
|
|
|
- record_date = row['record_date']
|
|
|
- if isinstance(record_date, str):
|
|
|
- dates.append(record_date[5:10]) # "2026-01-16" -> "01-16"
|
|
|
- else:
|
|
|
- dates.append(record_date.strftime("%m-%d"))
|
|
|
-
|
|
|
- # 确保返回整数类型
|
|
|
- fans.append(int(row['total_fans'] or 0))
|
|
|
- views.append(int(row['total_views'] or 0))
|
|
|
- likes.append(int(row['total_likes'] or 0))
|
|
|
- comments.append(int(row['total_comments'] or 0))
|
|
|
- shares.append(int(row['total_shares'] or 0))
|
|
|
- collects.append(int(row['total_collects'] or 0))
|
|
|
-
|
|
|
- # 如果没有数据,生成空的日期范围
|
|
|
- if not dates:
|
|
|
- from datetime import timedelta
|
|
|
- today = date.today()
|
|
|
- for i in range(days, 0, -1):
|
|
|
- d = today - timedelta(days=i-1)
|
|
|
- dates.append(d.strftime("%m-%d"))
|
|
|
- fans.append(0)
|
|
|
- views.append(0)
|
|
|
- likes.append(0)
|
|
|
- comments.append(0)
|
|
|
- shares.append(0)
|
|
|
- collects.append(0)
|
|
|
-
|
|
|
- return jsonify({
|
|
|
- "success": True,
|
|
|
- "data": {
|
|
|
- "dates": dates,
|
|
|
- "fans": fans,
|
|
|
- "views": views,
|
|
|
- "likes": likes,
|
|
|
- "comments": comments,
|
|
|
- "shares": shares,
|
|
|
- "collects": collects
|
|
|
- }
|
|
|
- })
|
|
|
- finally:
|
|
|
- conn.close()
|
|
|
+ # 调用 Node.js API 获取数据
|
|
|
+ params = {"user_id": user_id}
|
|
|
+ if days:
|
|
|
+ params["days"] = days
|
|
|
+ if start_date:
|
|
|
+ params["start_date"] = start_date
|
|
|
+ if end_date:
|
|
|
+ params["end_date"] = end_date
|
|
|
+ if account_id:
|
|
|
+ params["account_id"] = account_id
|
|
|
+
|
|
|
+ result = call_nodejs_api('GET', '/work-day-statistics/trend', params=params)
|
|
|
+
|
|
|
+ return jsonify(result)
|
|
|
|
|
|
except Exception as e:
|
|
|
traceback.print_exc()
|
|
|
@@ -905,111 +758,20 @@ def get_statistics_by_platform():
|
|
|
if not user_id:
|
|
|
return jsonify({"success": False, "error": "缺少 user_id 参数"}), 400
|
|
|
|
|
|
- conn = get_db_connection()
|
|
|
- try:
|
|
|
- with conn.cursor() as cursor:
|
|
|
- # 简化查询:按平台分组获取统计数据
|
|
|
- # 1. 从 platform_accounts 获取当前粉丝数(注意:字段名是下划线命名 fans_count, user_id)
|
|
|
- # 2. 从 work_day_statistics 获取播放量等累计数据
|
|
|
-
|
|
|
- # 根据日期参数构建日期条件
|
|
|
- if start_date and end_date:
|
|
|
- date_condition = "wds.record_date >= %s AND wds.record_date <= %s"
|
|
|
- date_params = [start_date, end_date]
|
|
|
- else:
|
|
|
- days_value = min(int(days or 30), 30)
|
|
|
- date_condition = "wds.record_date >= DATE_SUB(CURDATE(), INTERVAL %s DAY)"
|
|
|
- date_params = [days_value]
|
|
|
-
|
|
|
- # 注意:数据库存储的是累积值,所以区间增量 = 最后一天 - 第一天
|
|
|
- sql = f"""
|
|
|
- SELECT
|
|
|
- pa.platform,
|
|
|
- pa.fans_count as current_fans,
|
|
|
- COALESCE(last_day.views, 0) - COALESCE(first_day.views, 0) as viewsIncrease,
|
|
|
- COALESCE(last_day.likes, 0) - COALESCE(first_day.likes, 0) as likesIncrease,
|
|
|
- COALESCE(last_day.comments, 0) - COALESCE(first_day.comments, 0) as commentsIncrease,
|
|
|
- COALESCE(last_day.collects, 0) - COALESCE(first_day.collects, 0) as collectsIncrease,
|
|
|
- COALESCE(first_day.fans, pa.fans_count) as earliest_fans
|
|
|
- FROM platform_accounts pa
|
|
|
- LEFT JOIN (
|
|
|
- -- 获取每个账号在区间内第一天的累积值
|
|
|
- SELECT
|
|
|
- accountId, fans, views, likes, comments, collects
|
|
|
- FROM (
|
|
|
- SELECT
|
|
|
- w.accountId,
|
|
|
- MAX(wds.fans_count) as fans,
|
|
|
- SUM(wds.play_count) as views,
|
|
|
- SUM(wds.like_count) as likes,
|
|
|
- SUM(wds.comment_count) as comments,
|
|
|
- SUM(wds.collect_count) as collects,
|
|
|
- wds.record_date,
|
|
|
- ROW_NUMBER() OVER (PARTITION BY w.accountId ORDER BY wds.record_date ASC) as rn
|
|
|
- FROM work_day_statistics wds
|
|
|
- INNER JOIN works w ON wds.work_id = w.id
|
|
|
- WHERE w.userId = %s
|
|
|
- AND {date_condition}
|
|
|
- GROUP BY w.accountId, wds.record_date
|
|
|
- ) ranked
|
|
|
- WHERE rn = 1
|
|
|
- ) first_day ON pa.id = first_day.accountId
|
|
|
- LEFT JOIN (
|
|
|
- -- 获取每个账号在区间内最后一天的累积值
|
|
|
- SELECT
|
|
|
- accountId, fans, views, likes, comments, collects
|
|
|
- FROM (
|
|
|
- SELECT
|
|
|
- w.accountId,
|
|
|
- MAX(wds.fans_count) as fans,
|
|
|
- SUM(wds.play_count) as views,
|
|
|
- SUM(wds.like_count) as likes,
|
|
|
- SUM(wds.comment_count) as comments,
|
|
|
- SUM(wds.collect_count) as collects,
|
|
|
- wds.record_date,
|
|
|
- ROW_NUMBER() OVER (PARTITION BY w.accountId ORDER BY wds.record_date DESC) as rn
|
|
|
- FROM work_day_statistics wds
|
|
|
- INNER JOIN works w ON wds.work_id = w.id
|
|
|
- WHERE w.userId = %s
|
|
|
- AND {date_condition}
|
|
|
- GROUP BY w.accountId, wds.record_date
|
|
|
- ) ranked
|
|
|
- WHERE rn = 1
|
|
|
- ) last_day ON pa.id = last_day.accountId
|
|
|
- WHERE pa.user_id = %s
|
|
|
- ORDER BY current_fans DESC
|
|
|
- """
|
|
|
-
|
|
|
- # 构建参数列表:first_day子查询(user_id + date_params) + last_day子查询(user_id + date_params) + 主查询(user_id)
|
|
|
- params = [user_id] + date_params + [user_id] + date_params + [user_id]
|
|
|
- cursor.execute(sql, params)
|
|
|
- results = cursor.fetchall()
|
|
|
-
|
|
|
- # 构建响应数据
|
|
|
- platform_data = []
|
|
|
- for row in results:
|
|
|
- current_fans = int(row['current_fans'] or 0)
|
|
|
- earliest_fans = int(row['earliest_fans'] or current_fans)
|
|
|
- fans_increase = current_fans - earliest_fans
|
|
|
-
|
|
|
- platform_data.append({
|
|
|
- "platform": row['platform'],
|
|
|
- "fansCount": current_fans,
|
|
|
- "fansIncrease": fans_increase,
|
|
|
- "viewsCount": int(row['viewsIncrease'] or 0), # 区间增量
|
|
|
- "likesCount": int(row['likesIncrease'] or 0), # 区间增量
|
|
|
- "commentsCount": int(row['commentsIncrease'] or 0), # 区间增量
|
|
|
- "collectsCount": int(row['collectsIncrease'] or 0), # 区间增量
|
|
|
- })
|
|
|
-
|
|
|
- print(f"[PlatformStats] 返回 {len(platform_data)} 个平台的数据")
|
|
|
-
|
|
|
- return jsonify({
|
|
|
- "success": True,
|
|
|
- "data": platform_data
|
|
|
- })
|
|
|
- finally:
|
|
|
- conn.close()
|
|
|
+ # 调用 Node.js API 获取数据
|
|
|
+ params = {"user_id": user_id}
|
|
|
+ if days:
|
|
|
+ params["days"] = days
|
|
|
+ if start_date:
|
|
|
+ params["start_date"] = start_date
|
|
|
+ if end_date:
|
|
|
+ params["end_date"] = end_date
|
|
|
+
|
|
|
+ result = call_nodejs_api('GET', '/work-day-statistics/platforms', params=params)
|
|
|
+
|
|
|
+ print(f"[PlatformStats] 返回 {len(result.get('data', []))} 个平台的数据")
|
|
|
+
|
|
|
+ return jsonify(result)
|
|
|
|
|
|
except Exception as e:
|
|
|
traceback.print_exc()
|
|
|
@@ -1049,51 +811,16 @@ def get_work_statistics_history():
|
|
|
if not work_ids:
|
|
|
return jsonify({"success": False, "error": "缺少 work_ids 参数"}), 400
|
|
|
|
|
|
- conn = get_db_connection()
|
|
|
- try:
|
|
|
- with conn.cursor() as cursor:
|
|
|
- # 构建查询
|
|
|
- placeholders = ', '.join(['%s'] * len(work_ids))
|
|
|
- sql = f"""SELECT work_id, record_date, fans_count, play_count, like_count,
|
|
|
- comment_count, share_count, collect_count
|
|
|
- FROM work_day_statistics
|
|
|
- WHERE work_id IN ({placeholders})"""
|
|
|
- params = list(work_ids)
|
|
|
-
|
|
|
- if start_date:
|
|
|
- sql += " AND record_date >= %s"
|
|
|
- params.append(start_date)
|
|
|
- if end_date:
|
|
|
- sql += " AND record_date <= %s"
|
|
|
- params.append(end_date)
|
|
|
-
|
|
|
- sql += " ORDER BY work_id, record_date"
|
|
|
-
|
|
|
- cursor.execute(sql, params)
|
|
|
- results = cursor.fetchall()
|
|
|
- finally:
|
|
|
- conn.close()
|
|
|
-
|
|
|
- # 按 work_id 分组
|
|
|
- grouped_data = {}
|
|
|
- for row in results:
|
|
|
- work_id = str(row['work_id'])
|
|
|
- if work_id not in grouped_data:
|
|
|
- grouped_data[work_id] = []
|
|
|
- grouped_data[work_id].append({
|
|
|
- 'record_date': row['record_date'].strftime('%Y-%m-%d') if row['record_date'] else None,
|
|
|
- 'fans_count': row['fans_count'],
|
|
|
- 'play_count': row['play_count'],
|
|
|
- 'like_count': row['like_count'],
|
|
|
- 'comment_count': row['comment_count'],
|
|
|
- 'share_count': row['share_count'],
|
|
|
- 'collect_count': row['collect_count']
|
|
|
- })
|
|
|
+ # 调用 Node.js API 获取数据
|
|
|
+ request_data = {"work_ids": work_ids}
|
|
|
+ if start_date:
|
|
|
+ request_data["start_date"] = start_date
|
|
|
+ if end_date:
|
|
|
+ request_data["end_date"] = end_date
|
|
|
|
|
|
- return jsonify({
|
|
|
- "success": True,
|
|
|
- "data": grouped_data
|
|
|
- })
|
|
|
+ result = call_nodejs_api('POST', '/work-day-statistics/batch', data=request_data)
|
|
|
+
|
|
|
+ return jsonify(result)
|
|
|
|
|
|
except Exception as e:
|
|
|
traceback.print_exc()
|