|
|
@@ -17,14 +17,45 @@ import os
|
|
|
import sys
|
|
|
import argparse
|
|
|
import traceback
|
|
|
-from datetime import datetime
|
|
|
+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:
|
|
|
sys.path.insert(0, str(CURRENT_DIR))
|
|
|
|
|
|
+# 从 server/.env 文件加载环境变量
|
|
|
+def load_env_file():
|
|
|
+ """从 server/.env 文件加载环境变量"""
|
|
|
+ env_path = CURRENT_DIR.parent / '.env'
|
|
|
+ if env_path.exists():
|
|
|
+ print(f"[Config] Loading env from: {env_path}")
|
|
|
+ with open(env_path, 'r', encoding='utf-8') as f:
|
|
|
+ for line in f:
|
|
|
+ line = line.strip()
|
|
|
+ if line and not line.startswith('#') and '=' in line:
|
|
|
+ key, value = line.split('=', 1)
|
|
|
+ key = key.strip()
|
|
|
+ value = value.strip()
|
|
|
+ # 移除引号
|
|
|
+ if value.startswith('"') and value.endswith('"'):
|
|
|
+ value = value[1:-1]
|
|
|
+ elif value.startswith("'") and value.endswith("'"):
|
|
|
+ value = value[1:-1]
|
|
|
+ # 只在环境变量未设置时加载
|
|
|
+ if key not in os.environ:
|
|
|
+ os.environ[key] = value
|
|
|
+ print(f"[Config] Loaded: {key}=***" if 'PASSWORD' in key or 'SECRET' in key else f"[Config] Loaded: {key}={value}")
|
|
|
+ else:
|
|
|
+ print(f"[Config] .env file not found: {env_path}")
|
|
|
+
|
|
|
+# 加载环境变量
|
|
|
+load_env_file()
|
|
|
+
|
|
|
from flask import Flask, request, jsonify
|
|
|
from flask_cors import CORS
|
|
|
|
|
|
@@ -83,6 +114,31 @@ CORS(app)
|
|
|
# 全局配置
|
|
|
HEADLESS_MODE = os.environ.get('HEADLESS', 'true').lower() == '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)
|
|
|
+ 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)
|
|
|
+ raise
|
|
|
+
|
|
|
# ==================== 签名相关(小红书专用) ====================
|
|
|
|
|
|
@app.route("/sign", methods=["POST"])
|
|
|
@@ -425,6 +481,205 @@ def get_works():
|
|
|
return jsonify({"success": False, "error": str(e)}), 500
|
|
|
|
|
|
|
|
|
+# ==================== 保存作品日统计数据接口 ====================
|
|
|
+
|
|
|
+@app.route("/work_day_statistics", methods=["POST"])
|
|
|
+def save_work_day_statistics():
|
|
|
+ """
|
|
|
+ 保存作品每日统计数据
|
|
|
+ 当天的数据走更新流,日期变化走新增流
|
|
|
+
|
|
|
+ 请求体:
|
|
|
+ {
|
|
|
+ "statistics": [
|
|
|
+ {
|
|
|
+ "work_id": 1,
|
|
|
+ "fans_count": 1000,
|
|
|
+ "play_count": 5000,
|
|
|
+ "like_count": 200,
|
|
|
+ "comment_count": 50,
|
|
|
+ "share_count": 30,
|
|
|
+ "collect_count": 100
|
|
|
+ },
|
|
|
+ ...
|
|
|
+ ]
|
|
|
+ }
|
|
|
+
|
|
|
+ 响应:
|
|
|
+ {
|
|
|
+ "success": true,
|
|
|
+ "inserted": 5,
|
|
|
+ "updated": 3,
|
|
|
+ "message": "保存成功"
|
|
|
+ }
|
|
|
+ """
|
|
|
+ print("=" * 60, flush=True)
|
|
|
+ print("[DEBUG] ===== 进入 save_work_day_statistics 方法 =====", flush=True)
|
|
|
+ print(f"[DEBUG] 请求方法: {request.method}", flush=True)
|
|
|
+ print(f"[DEBUG] 请求数据: {request.json}", flush=True)
|
|
|
+ print("=" * 60, flush=True)
|
|
|
+
|
|
|
+ try:
|
|
|
+ data = request.json
|
|
|
+ statistics_list = data.get("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()
|
|
|
+
|
|
|
+ print(f"[WorkDayStatistics] 完成: 新增 {inserted_count} 条, 更新 {updated_count} 条")
|
|
|
+
|
|
|
+ return jsonify({
|
|
|
+ "success": True,
|
|
|
+ "inserted": inserted_count,
|
|
|
+ "updated": updated_count,
|
|
|
+ "message": f"保存成功: 新增 {inserted_count} 条, 更新 {updated_count} 条"
|
|
|
+ })
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ traceback.print_exc()
|
|
|
+ return jsonify({"success": False, "error": str(e)}), 500
|
|
|
+
|
|
|
+
|
|
|
+@app.route("/work_day_statistics/batch", methods=["POST"])
|
|
|
+def get_work_statistics_history():
|
|
|
+ """
|
|
|
+ 批量获取作品的历史统计数据
|
|
|
+
|
|
|
+ 请求体:
|
|
|
+ {
|
|
|
+ "work_ids": [1, 2, 3],
|
|
|
+ "start_date": "2025-01-01", # 可选
|
|
|
+ "end_date": "2025-01-21" # 可选
|
|
|
+ }
|
|
|
+
|
|
|
+ 响应:
|
|
|
+ {
|
|
|
+ "success": true,
|
|
|
+ "data": {
|
|
|
+ "1": [
|
|
|
+ {"record_date": "2025-01-20", "play_count": 100, ...},
|
|
|
+ {"record_date": "2025-01-21", "play_count": 150, ...}
|
|
|
+ ],
|
|
|
+ ...
|
|
|
+ }
|
|
|
+ }
|
|
|
+ """
|
|
|
+ try:
|
|
|
+ data = request.json
|
|
|
+ work_ids = data.get("work_ids", [])
|
|
|
+ start_date = data.get("start_date")
|
|
|
+ end_date = data.get("end_date")
|
|
|
+
|
|
|
+ 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']
|
|
|
+ })
|
|
|
+
|
|
|
+ return jsonify({
|
|
|
+ "success": True,
|
|
|
+ "data": grouped_data
|
|
|
+ })
|
|
|
+
|
|
|
+ except Exception as e:
|
|
|
+ traceback.print_exc()
|
|
|
+ return jsonify({"success": False, "error": str(e)}), 500
|
|
|
+
|
|
|
+
|
|
|
# ==================== 获取评论列表接口 ====================
|
|
|
|
|
|
@app.route("/comments", methods=["POST"])
|
|
|
@@ -636,7 +891,7 @@ def index():
|
|
|
"""首页"""
|
|
|
return jsonify({
|
|
|
"name": "多平台视频发布服务",
|
|
|
- "version": "1.1.0",
|
|
|
+ "version": "1.2.0",
|
|
|
"endpoints": {
|
|
|
"GET /": "服务信息",
|
|
|
"GET /health": "健康检查",
|
|
|
@@ -645,6 +900,8 @@ def index():
|
|
|
"POST /works": "获取作品列表",
|
|
|
"POST /comments": "获取作品评论",
|
|
|
"POST /all_comments": "获取所有作品评论",
|
|
|
+ "POST /work_day_statistics": "保存作品每日统计数据",
|
|
|
+ "POST /work_day_statistics/batch": "获取作品历史统计数据",
|
|
|
"POST /check_cookie": "检查 Cookie",
|
|
|
"POST /sign": "小红书签名"
|
|
|
},
|