增加前端的代码,改编文件结构

This commit is contained in:
xbh 2025-10-19 15:13:01 +08:00
parent c1bfa0de21
commit 06996967ca
24 changed files with 5766 additions and 132 deletions

18
.gitignore vendored
View File

@ -32,12 +32,12 @@ douyin_cdp_play_vv_*.txt
# Chrome profiles and drivers # Chrome profiles and drivers
# 注意Chrome profile 包含大量缓存文件不应加入Git # 注意Chrome profile 包含大量缓存文件不应加入Git
scripts/config/chrome_profile/ backend/scripts/config/chrome_profile/
drivers/* backend/drivers/*
!drivers/chromedriver.exe !backend/drivers/chromedriver.exe
# Rankings config directory # Rankings config directory
handlers/Rankings/config/ backend/handlers/Rankings/config/
# Environment variables # Environment variables
.env .env
@ -48,6 +48,16 @@ ENV/
env.bak/ env.bak/
venv.bak/ venv.bak/
# Node.js
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.npm
.yarn-integrity
.pnp
.pnp.js
# IDE # IDE
.vscode/ .vscode/
.idea/ .idea/

48
README.md Normal file
View File

@ -0,0 +1,48 @@
# 榜单系统
这是一个全栈榜单系统项目包含后端API服务和前端Vue3应用。
## 项目结构
```
rank_backend/
├── backend/ # 后端代码
│ ├── app.py # Flask应用主文件
│ ├── config.py # 配置文件
│ ├── database.py # 数据库连接
│ ├── Timer_worker.py # 定时任务
│ ├── handlers/ # 业务处理器
│ └── routers/ # 路由定义
├── frontend/ # 前端Vue3应用
│ ├── src/ # 源代码
│ ├── public/ # 静态资源
│ └── package.json # 依赖配置
└── docs/ # 文档
├── API接口文档.md
└── requirements.txt
```
## 快速开始
### 后端服务
```bash
cd backend
python3 -m pip install -r ../docs/requirements.txt
python3 app.py
```
### 前端应用
```bash
cd frontend
npm install
npm run dev
```
## 开发说明
- 后端使用Flask框架提供RESTful API
- 前端使用Vue3 + Vite构建
- 数据库配置在backend/config.py中
- API文档位于docs/API接口文档.md

View File

@ -22,7 +22,9 @@ logging.basicConfig(
# 导入并注册蓝图 # 导入并注册蓝图
from routers.rank_api_routes import rank_bp from routers.rank_api_routes import rank_bp
from routers.article_routes import article_bp
app.register_blueprint(rank_bp) app.register_blueprint(rank_bp)
app.register_blueprint(article_bp)
if __name__ == '__main__': if __name__ == '__main__':

View File

@ -0,0 +1,268 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
文章API服务器
提供文章列表获取和文章详情获取的接口
"""
from flask import Blueprint, request, jsonify
from datetime import datetime, timedelta
import logging
from database import db
from bson import ObjectId
# 创建蓝图
article_bp = Blueprint('article', __name__, url_prefix='/api/article')
# 获取数据库集合
articles_collection = db['articles']
def format_time(time_obj):
"""格式化时间"""
if not time_obj:
return ""
if isinstance(time_obj, datetime):
return time_obj.strftime("%Y-%m-%d %H:%M:%S")
else:
return str(time_obj)
def format_article_item(doc):
"""格式化文章数据项"""
return {
"_id": str(doc.get("_id", "")),
"title": doc.get("title", ""),
"author_id": doc.get("author_id", ""),
"cover_image": doc.get("cover_image", ""),
"status": doc.get("status", ""),
"summary": doc.get("summary", ""),
"created_at": format_time(doc.get("created_at")),
"likes": doc.get("likes", []),
"likes_count": len(doc.get("likes", []))
}
def get_article_list(page=1, limit=20, sort_by="created_at", status=None):
"""获取文章列表(分页)"""
try:
# 计算跳过的数量
skip = (page - 1) * limit
# 构建查询条件
query_condition = {}
if status:
query_condition["status"] = status
# 设置排序字段
sort_field = sort_by if sort_by in ["created_at", "title"] else "created_at"
sort_order = -1 # 降序
# 查询数据
cursor = articles_collection.find(query_condition).sort(sort_field, sort_order).skip(skip).limit(limit)
docs = list(cursor)
# 获取总数
total = articles_collection.count_documents(query_condition)
# 格式化数据
article_list = []
for doc in docs:
item = format_article_item(doc)
article_list.append(item)
return {
"success": True,
"data": article_list,
"pagination": {
"page": page,
"limit": limit,
"total": total,
"pages": (total + limit - 1) // limit,
"has_next": page * limit < total,
"has_prev": page > 1
},
"sort_by": sort_by,
"status_filter": status,
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
except Exception as e:
logging.error(f"获取文章列表失败: {e}")
return {"success": False, "message": f"获取数据失败: {str(e)}"}
def search_articles(keyword, page=1, limit=10):
"""搜索文章"""
try:
if not keyword:
return {"success": False, "message": "请提供搜索关键词"}
# 计算跳过的数量
skip = (page - 1) * limit
# 构建搜索条件(模糊匹配标题和内容)
search_condition = {
"$or": [
{"title": {"$regex": keyword, "$options": "i"}},
{"content": {"$regex": keyword, "$options": "i"}},
{"summary": {"$regex": keyword, "$options": "i"}}
]
}
# 查询数据
cursor = articles_collection.find(search_condition).sort("created_at", -1).skip(skip).limit(limit)
docs = list(cursor)
# 获取搜索结果总数
total = articles_collection.count_documents(search_condition)
# 格式化数据
search_results = []
for doc in docs:
item = format_article_item(doc)
search_results.append(item)
return {
"success": True,
"data": search_results,
"keyword": keyword,
"pagination": {
"page": page,
"limit": limit,
"total": total,
"pages": (total + limit - 1) // limit,
"has_next": page * limit < total,
"has_prev": page > 1
},
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
except Exception as e:
logging.error(f"搜索文章失败: {e}")
return {"success": False, "message": f"搜索失败: {str(e)}"}
def get_article_detail(article_id):
"""获取文章详情"""
try:
# 尝试通过ObjectId查找
try:
doc = articles_collection.find_one({"_id": ObjectId(article_id)})
except:
# 如果ObjectId无效尝试其他字段
doc = articles_collection.find_one({
"$or": [
{"title": article_id},
{"author_id": article_id}
]
})
if not doc:
return {"success": False, "message": "未找到文章信息"}
# 格式化详细信息
detail = format_article_item(doc)
return {
"success": True,
"data": detail,
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
except Exception as e:
logging.error(f"获取文章详情失败: {e}")
return {"success": False, "message": f"获取详情失败: {str(e)}"}
def get_statistics():
"""获取统计信息"""
try:
# 基本统计
total_articles = articles_collection.count_documents({})
if total_articles == 0:
return {"success": False, "message": "暂无数据"}
# 按状态统计
status_stats = []
for status in ["draft", "published", "archived"]:
count = articles_collection.count_documents({"status": status})
status_stats.append({"status": status, "count": count})
# 获取最新更新时间
latest_doc = articles_collection.find().sort("created_at", -1).limit(1)
latest_time = ""
if latest_doc:
latest_list = list(latest_doc)
if latest_list:
latest_time = format_time(latest_list[0].get("created_at"))
return {
"success": True,
"data": {
"total_articles": total_articles,
"status_stats": status_stats,
"latest_update": latest_time
},
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
except Exception as e:
logging.error(f"获取统计信息失败: {e}")
return {"success": False, "message": f"获取统计失败: {str(e)}"}
# 路由定义
@article_bp.route('/list')
def get_articles():
"""获取文章列表"""
page = int(request.args.get('page', 1))
limit = int(request.args.get('limit', 20))
sort_by = request.args.get('sort', 'created_at')
status = request.args.get('status')
result = get_article_list(page, limit, sort_by, status)
return jsonify(result)
@article_bp.route('/search')
def search():
"""搜索文章"""
keyword = request.args.get('q', '')
page = int(request.args.get('page', 1))
limit = int(request.args.get('limit', 10))
result = search_articles(keyword, page, limit)
return jsonify(result)
@article_bp.route('/detail')
def get_detail():
"""获取文章详情"""
article_id = request.args.get('id', '')
result = get_article_detail(article_id)
return jsonify(result)
@article_bp.route('/stats')
def get_stats():
"""获取统计信息"""
result = get_statistics()
return jsonify(result)
@article_bp.route('/health')
def health_check():
"""健康检查"""
try:
# 检查数据库连接
total_records = articles_collection.count_documents({})
return jsonify({
"success": True,
"message": "服务正常",
"data": {
"database": "连接正常",
"total_records": total_records,
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
})
except Exception as e:
return jsonify({
"success": False,
"message": f"服务异常: {str(e)}",
"data": {
"database": "连接失败",
"timestamp": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
})

View File

@ -209,7 +209,7 @@ def get_mix_list(page=1, limit=20, sort_by="playcount"):
return {"success": False, "message": f"获取数据失败: {str(e)}"} return {"success": False, "message": f"获取数据失败: {str(e)}"}
def get_growth_mixes(page=1, limit=20, start_date=None, end_date=None): def get_growth_mixes(page=1, limit=20, start_date=None, end_date=None):
"""获取按播放量增长排序的合集列表 - 优先从定时器生成的数据中读取""" """获取按播放量增长排序的合集列表 - 仅从Ranking_storage读取预计算数据"""
try: try:
# 计算跳过的数量 # 计算跳过的数量
skip = (page - 1) * limit skip = (page - 1) * limit
@ -228,129 +228,101 @@ def get_growth_mixes(page=1, limit=20, start_date=None, end_date=None):
end_date_str = end_date.strftime("%Y-%m-%d") end_date_str = end_date.strftime("%Y-%m-%d")
start_date_str = start_date.strftime("%Y-%m-%d") start_date_str = start_date.strftime("%Y-%m-%d")
# 优先尝试从定时器生成的增长榜数据中读取 # 从Ranking_storage读取预计算的增长榜数据
try: growth_ranking = daily_rankings_collection.find_one({
"date": end_date_str,
"type": "comprehensive" # 使用comprehensive类型包含增长数据
}, sort=[("calculation_sequence", -1)]) # 获取最新的计算结果
if not growth_ranking or "data" not in growth_ranking:
# 如果没有找到comprehensive类型尝试查找growth类型
growth_ranking = daily_rankings_collection.find_one({ growth_ranking = daily_rankings_collection.find_one({
"date": end_date_str, "date": end_date_str,
"type": "growth", "type": "growth"
"start_date": start_date_str, }, sort=[("calculation_sequence", -1)])
"end_date": end_date_str
}, sort=[("calculation_sequence", -1)]) # 获取最新的计算结果
if growth_ranking and "data" in growth_ranking: if growth_ranking and "data" in growth_ranking:
logging.info(f"📈 从定时器生成的增长榜数据中读取 {end_date_str} 的增长榜") logging.info(f"📈 从Ranking_storage读取 {end_date_str} 的增长榜数据")
# 获取预先计算好的增长榜数据 # 获取预先计算好的增长榜数据
growth_data = growth_ranking["data"] growth_data = growth_ranking["data"]
# 分页处理 # 如果是comprehensive类型需要按增长值排序
total = len(growth_data) if growth_ranking.get("type") == "comprehensive":
paginated_data = growth_data[skip:skip + limit] # 按timeline_data中的play_vv_change排序
growth_data = sorted(growth_data,
key=lambda x: x.get("timeline_data", {}).get("play_vv_change", 0),
reverse=True)
return { # 分页处理
"success": True, total = len(growth_data)
"data": paginated_data, paginated_data = growth_data[skip:skip + limit]
"pagination": {
"page": page,
"limit": limit,
"total": total,
"pages": (total + limit - 1) // limit,
"has_next": page * limit < total,
"has_prev": page > 1
},
"sort_by": "growth",
"date_range": {
"start_date": start_date_str,
"end_date": end_date_str
},
"data_source": "timer_generated", # 标识数据来源
"update_time": growth_ranking.get("created_at", datetime.now()).strftime("%Y-%m-%d %H:%M:%S") if isinstance(growth_ranking.get("created_at"), datetime) else str(growth_ranking.get("created_at", ""))
}
except Exception as e:
logging.warning(f"从定时器数据读取增长榜失败,将使用动态计算: {e}")
# 如果定时器数据不存在或读取失败,回退到动态计算 # 为分页数据添加排名
logging.info(f"📊 动态计算 {start_date_str}{end_date_str} 的增长榜") for i, item in enumerate(paginated_data):
item["rank"] = skip + i + 1
# 查询结束日期的数据 return {
end_cursor = collection.find({ "success": True,
"batch_time": { "data": paginated_data,
"$gte": datetime(end_date.year, end_date.month, end_date.day), "pagination": {
"$lt": datetime(end_date.year, end_date.month, end_date.day) + timedelta(days=1) "page": page,
"limit": limit,
"total": total,
"pages": (total + limit - 1) // limit,
"has_next": page * limit < total,
"has_prev": page > 1
},
"sort_by": "growth",
"date_range": {
"start_date": start_date_str,
"end_date": end_date_str
},
"data_source": "ranking_storage", # 标识数据来源
"update_time": growth_ranking.get("created_at", datetime.now()).strftime("%Y-%m-%d %H:%M:%S") if isinstance(growth_ranking.get("created_at"), datetime) else str(growth_ranking.get("created_at", ""))
} }
}) else:
end_data = list(end_cursor) # 如果Ranking_storage中没有数据返回空结果
logging.warning(f"Ranking_storage中未找到 {end_date_str} 的增长榜数据")
# 查询开始日期的数据 return {
start_cursor = collection.find({ "success": True,
"batch_time": { "message": f"暂无 {end_date_str} 的增长榜数据,请等待定时任务生成",
"$gte": datetime(start_date.year, start_date.month, start_date.day), "data": [],
"$lt": datetime(start_date.year, start_date.month, start_date.day) + timedelta(days=1) "pagination": {
"page": page,
"limit": limit,
"total": 0,
"pages": 0,
"has_next": False,
"has_prev": False
},
"sort_by": "growth",
"date_range": {
"start_date": start_date_str,
"end_date": end_date_str
},
"data_source": "ranking_storage",
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
} }
})
start_data = list(start_cursor)
# 创建字典以便快速查找
end_dict = {item["mix_name"]: item for item in end_data}
start_dict = {item["mix_name"]: item for item in start_data}
# 计算增长数据
growth_data = []
for mix_name, end_item in end_dict.items():
if mix_name in start_dict:
start_item = start_dict[mix_name]
growth = end_item.get("play_vv", 0) - start_item.get("play_vv", 0)
# 只保留增长为正的数据
if growth > 0:
item = format_mix_item(end_item)
item["growth"] = growth
item["start_date"] = start_date_str
item["end_date"] = end_date_str
growth_data.append(item)
else:
# 如果开始日期没有数据,但结束日期有,也认为是新增长
item = format_mix_item(end_item)
item["growth"] = end_item.get("play_vv", 0)
item["start_date"] = start_date_str
item["end_date"] = end_date_str
growth_data.append(item)
# 按增长值降序排序
growth_data.sort(key=lambda x: x.get("growth", 0), reverse=True)
# 分页处理
total = len(growth_data)
paginated_data = growth_data[skip:skip + limit]
# 添加排名
for i, item in enumerate(paginated_data):
item["rank"] = skip + i + 1
return {
"success": True,
"data": paginated_data,
"pagination": {
"page": page,
"limit": limit,
"total": total,
"pages": (total + limit - 1) // limit,
"has_next": page * limit < total,
"has_prev": page > 1
},
"sort_by": "growth",
"date_range": {
"start_date": start_date_str,
"end_date": end_date_str
},
"data_source": "dynamic_calculation", # 标识数据来源
"update_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S")
}
except Exception as e: except Exception as e:
logging.error(f"获取增长合集列表失败: {e}") logging.error(f"获取增长合集列表失败: {e}")
# 如果增长计算失败,返回按播放量排序的数据作为备选 # 返回错误信息,不再回退到播放量排序
return get_mix_list(page, limit, "playcount") return {
"success": False,
"message": f"获取增长榜数据失败: {str(e)}",
"data": [],
"pagination": {
"page": page,
"limit": limit,
"total": 0,
"pages": 0,
"has_next": False,
"has_prev": False
},
"sort_by": "growth",
"data_source": "ranking_storage"
}
def get_top_mixes(limit=10): def get_top_mixes(limit=10):
"""获取热门合集TOP榜单""" """获取热门合集TOP榜单"""

View File

@ -2,10 +2,10 @@
## 概述 ## 概述
本API服务提供抖音播放量数据的查询、搜索、统计等功能专为小程序优化设计。 本API服务提供抖音播放量数据和文章内容的查询、搜索、统计等功能,专为小程序优化设计。
**基础信息** **基础信息**
- 服务地址:`http://localhost:5000` - 服务地址:`http://localhost:5001`
- 数据源MongoDB数据库 - 数据源MongoDB数据库
- 数据更新每晚24:00自动更新 - 数据更新每晚24:00自动更新
- 响应格式JSON - 响应格式JSON
@ -47,6 +47,37 @@
} }
``` ```
### 文章数据项
```json
{
"_id": "68f3ad112ba085e5d676537e",
"title": "文章标题",
"author_id": "test_user_1",
"cover_image": "封面图片URL",
"status": "draft",
"summary": "文章摘要",
"created_at": "2025-10-18 15:06:57",
"likes": [],
"likes_count": 0
}
```
### 文章详情数据项
```json
{
"_id": "68f3ad112ba085e5d676537e",
"title": "文章标题",
"content": "文章完整内容",
"author_id": "test_user_1",
"cover_image": "封面图片URL",
"status": "draft",
"summary": "文章摘要",
"created_at": "2025-10-18 15:06:57",
"likes": [],
"likes_count": 0
}
```
### 分页信息 ### 分页信息
```json ```json
{ {
@ -95,7 +126,12 @@ GET /
"/api/rank/rankings/dates": "获取可用榜单日期", "/api/rank/rankings/dates": "获取可用榜单日期",
"/api/rank/rankings/types": "获取榜单类型", "/api/rank/rankings/types": "获取榜单类型",
"/api/rank/rankings/latest": "获取最新榜单", "/api/rank/rankings/latest": "获取最新榜单",
"/api/rank/rankings/stats": "获取榜单统计" "/api/rank/rankings/stats": "获取榜单统计",
"/api/article/list": "获取文章列表 (支持分页和排序)",
"/api/article/search": "搜索文章",
"/api/article/detail": "获取文章详情",
"/api/article/stats": "获取文章统计信息",
"/api/article/health": "文章服务健康检查"
}, },
"features": [ "features": [
"分页支持", "分页支持",
@ -105,7 +141,9 @@ GET /
"统计分析", "统计分析",
"榜单查询", "榜单查询",
"动态排序", "动态排序",
"小程序优化" "小程序优化",
"文章管理",
"内容搜索"
] ]
} }
} }
@ -127,8 +165,12 @@ GET /api/rank/videos
| page | int | 否 | 1 | 页码 | | page | int | 否 | 1 | 页码 |
| limit | int | 否 | 20 | 每页数量 | | limit | int | 否 | 20 | 每页数量 |
| sort | string | 否 | playcount | 排序方式playcount(播放量) / growth(增长量) | | sort | string | 否 | playcount | 排序方式playcount(播放量) / growth(增长量) |
| start_date | string | 否 | 昨天 | 增长计算开始日期(格式: YYYY-MM-DD) | | start_date | string | 否 | 昨天 | 增长计算开始日期(格式: YYYY-MM-DD)仅在sort=growth时有效 |
| end_date | string | 否 | 今天 | 增长计算结束日期(格式: YYYY-MM-DD) | | end_date | string | 否 | 今天 | 增长计算结束日期(格式: YYYY-MM-DD)仅在sort=growth时有效 |
**数据源说明**
- `sort=playcount`从Rankings_list集合动态查询当日播放量数据
- `sort=growth`从Ranking_storage集合读取预计算的增长排名数据由定时任务生成
**使用示例** **使用示例**
``` ```
@ -157,7 +199,9 @@ GET /api/rank/videos?page=1&limit=20&sort=growth&start_date=2025-10-16&end_date=
"request_id": "request_xxx", "request_id": "request_xxx",
"rank": 1, "rank": 1,
"cover_image_url": "https://p3.douyinpic.com/xxx", "cover_image_url": "https://p3.douyinpic.com/xxx",
"cover_backup_urls": ["url1", "url2"] "cover_backup_urls": ["url1", "url2"],
"growth": 5000000,
"growth_rate": 4.35
} }
], ],
"pagination": { "pagination": {
@ -168,7 +212,8 @@ GET /api/rank/videos?page=1&limit=20&sort=growth&start_date=2025-10-16&end_date=
"has_next": true, "has_next": true,
"has_prev": false "has_prev": false
}, },
"sort_by": "playcount", "sort_by": "growth",
"data_source": "ranking_storage",
"update_time": "2025-10-17 15:30:00" "update_time": "2025-10-17 15:30:00"
} }
``` ```
@ -619,9 +664,199 @@ GET /api/rank/rankings/stats
### 常见错误 ### 常见错误
- `数据库连接失败`MongoDB连接异常 - `数据库连接失败`MongoDB连接异常
- `未找到合集信息`:查询的合集不存在 - `未找到合集信息`:查询的合集不存在
- `未找到文章信息`:查询的文章不存在
- `请提供搜索关键词`:搜索接口缺少关键词参数 - `请提供搜索关键词`:搜索接口缺少关键词参数
- `获取数据失败`:数据查询异常 - `获取数据失败`:数据查询异常
## 文章管理接口
### 1. 获取文章列表
**接口地址**
```
GET /api/article/list
```
**功能描述**
获取文章列表,支持分页和排序
**请求参数**
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| page | int | 否 | 1 | 页码 |
| limit | int | 否 | 10 | 每页数量 |
| sort | string | 否 | created_at | 排序字段 |
| order | string | 否 | desc | 排序方向 (asc/desc) |
**响应示例**
```json
{
"success": true,
"data": [
{
"_id": "68f3ad112ba085e5d676537e",
"title": "对于ai影视化的思考",
"author_id": "test_user_1",
"cover_image": "",
"status": "draft",
"summary": "AI影视化的发展趋势和思考",
"created_at": "2025-10-18 15:06:57",
"likes": [],
"likes_count": 0
}
],
"pagination": {
"current_page": 1,
"total_pages": 1,
"total_items": 1,
"has_next": false,
"has_prev": false
},
"message": "获取文章列表成功",
"update_time": "2025-10-18 15:06:57"
}
```
### 2. 搜索文章
**接口地址**
```
GET /api/article/search
```
**功能描述**
根据关键词搜索文章
**请求参数**
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| q | string | 是 | - | 搜索关键词 |
| page | int | 否 | 1 | 页码 |
| limit | int | 否 | 10 | 每页数量 |
**响应示例**
```json
{
"success": true,
"data": [
{
"_id": "68f3ad112ba085e5d676537e",
"title": "对于ai影视化的思考",
"author_id": "test_user_1",
"cover_image": "",
"status": "draft",
"summary": "AI影视化的发展趋势和思考",
"created_at": "2025-10-18 15:06:57",
"likes": [],
"likes_count": 0
}
],
"pagination": {
"current_page": 1,
"total_pages": 1,
"total_items": 1,
"has_next": false,
"has_prev": false
},
"message": "搜索文章成功",
"update_time": "2025-10-18 15:06:57"
}
```
### 3. 获取文章详情
**接口地址**
```
GET /api/article/detail
```
**功能描述**
根据文章ID获取文章详细信息
**请求参数**
| 参数名 | 类型 | 必填 | 默认值 | 说明 |
|--------|------|------|--------|------|
| id | string | 是 | - | 文章ID |
**响应示例**
```json
{
"success": true,
"data": {
"_id": "68f3ad112ba085e5d676537e",
"title": "对于ai影视化的思考",
"content": "# AI影视化的思考\n\n在2025年AI技术在影视行业的应用...",
"author_id": "test_user_1",
"cover_image": "",
"status": "draft",
"summary": "AI影视化的发展趋势和思考",
"created_at": "2025-10-18 15:06:57",
"likes": [],
"likes_count": 0
},
"message": "获取文章详情成功",
"update_time": "2025-10-18 15:06:57"
}
```
### 4. 获取文章统计信息
**接口地址**
```
GET /api/article/stats
```
**功能描述**
获取文章相关的统计信息
**请求参数**
**响应示例**
```json
{
"success": true,
"data": {
"total_articles": 1,
"latest_update": "2025-10-18 15:06:57",
"status_count": {
"draft": 1,
"published": 0,
"archived": 0
}
},
"message": "获取文章统计成功",
"update_time": "2025-10-18 15:06:57"
}
```
### 5. 文章服务健康检查
**接口地址**
```
GET /api/article/health
```
**功能描述**
检查文章服务和数据库连接状态
**请求参数**
**响应示例**
```json
{
"success": true,
"data": {
"status": "服务正常",
"database": "连接正常",
"article_count": 1
},
"message": "文章服务健康检查通过",
"update_time": "2025-10-18 15:06:57"
}
```
## 小程序使用建议 ## 小程序使用建议
### 1. 分页加载 ### 1. 分页加载
@ -661,17 +896,61 @@ wx.request({
- 合理使用缓存策略 - 合理使用缓存策略
- 定期检查服务健康状态 - 定期检查服务健康状态
### 5. 文章管理
推荐的文章列表加载方式:
```javascript
// 小程序端示例 - 文章列表
wx.request({
url: 'http://localhost:5001/api/article/list',
data: {
page: 1,
limit: 10,
sort: 'created_at',
order: 'desc'
},
success: (res) => {
if (res.data.success) {
this.setData({
articles: res.data.data,
hasNext: res.data.pagination.has_next
})
}
}
})
```
文章搜索示例:
```javascript
// 小程序端示例 - 文章搜索
wx.request({
url: 'http://localhost:5001/api/article/search',
data: {
q: 'AI',
page: 1,
limit: 5
},
success: (res) => {
if (res.data.success) {
this.setData({
searchResults: res.data.data
})
}
}
})
```
## 部署说明 ## 部署说明
### 启动服务 ### 启动服务
```bash ```bash
cd C:\Users\EDY\Desktop\rank_backend cd /Users/binghuixiong/剧变AI/rank_backend
python app.py python3 app.py
``` ```
### 服务信息 ### 服务信息
- 端口5000 - 端口5001
- 数据库MongoDB (localhost:27017) - 数据库MongoDB (localhost:27017)
- 数据库名kemeng_media
- 数据更新每晚24:00自动执行 - 数据更新每晚24:00自动执行
### 注意事项 ### 注意事项
@ -681,7 +960,7 @@ python app.py
--- ---
**文档版本**v3.0 **文档版本**v4.1
**最后更新**2025-01-17 **最后更新**2025-01-18
**维护者**:系统自动生成 **维护者**:系统自动生成
**更新内容**新增榜单查询相关API接口更新所有接口路径为/api/rank前缀 **更新内容**优化增长排名接口数据源策略现在专门使用Ranking_storage预计算数据提升查询性能和数据一致性

30
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1,30 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
.DS_Store
dist
dist-ssr
coverage
*.local
/cypress/videos/
/cypress/screenshots/
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
*.tsbuildinfo

38
frontend/README.md Normal file
View File

@ -0,0 +1,38 @@
# .
This template should help get you started developing with Vue 3 in Vite.
## Recommended IDE Setup
[VS Code](https://code.visualstudio.com/) + [Vue (Official)](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
## Recommended Browser Setup
- Chromium-based browsers (Chrome, Edge, Brave, etc.):
- [Vue.js devtools](https://chromewebstore.google.com/detail/vuejs-devtools/nhdogjmejiglipccpnnnanhbledajbpd)
- [Turn on Custom Object Formatter in Chrome DevTools](http://bit.ly/object-formatters)
- Firefox:
- [Vue.js devtools](https://addons.mozilla.org/en-US/firefox/addon/vue-js-devtools/)
- [Turn on Custom Object Formatter in Firefox DevTools](https://fxdx.dev/firefox-devtools-custom-object-formatters/)
## Customize configuration
See [Vite Configuration Reference](https://vite.dev/config/).
## Project Setup
```sh
npm install
```
### Compile and Hot-Reload for Development
```sh
npm run dev
```
### Compile and Minify for Production
```sh
npm run build
```

14
frontend/index.html Normal file
View File

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css">
<title>Vite App</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>

8
frontend/jsconfig.json Normal file
View File

@ -0,0 +1,8 @@
{
"compilerOptions": {
"paths": {
"@/*": ["./src/*"]
}
},
"exclude": ["node_modules", "dist"]
}

4295
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

25
frontend/package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "rank",
"version": "0.0.0",
"private": true,
"type": "module",
"engines": {
"node": "^20.19.0 || >=22.12.0"
},
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"axios": "^1.12.2",
"bootstrap": "^5.3.0-alpha1",
"bootstrap-icons": "^1.13.1",
"vue": "^3.5.22"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.1",
"vite": "^7.1.7",
"vite-plugin-vue-devtools": "^8.0.2"
}
}

BIN
frontend/public/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -0,0 +1,5 @@
<svg width="60" height="80" xmlns="http://www.w3.org/2000/svg">
<rect width="60" height="80" fill="#f0f0f0" stroke="#ddd" stroke-width="1"/>
<text x="30" y="35" text-anchor="middle" font-family="Arial" font-size="8" fill="#999">暂无</text>
<text x="30" y="50" text-anchor="middle" font-family="Arial" font-size="8" fill="#999">图片</text>
</svg>

After

Width:  |  Height:  |  Size: 355 B

559
frontend/src/App.vue Normal file
View File

@ -0,0 +1,559 @@
<script setup>
import { ref, onMounted, computed } from 'vue'
import axios from 'axios'
//
const currentTab = ref('ranking') // 'ranking' 'news'
const rankingData = ref([])
const loading = ref(false)
const selectedDate = ref('')
const currentPage = ref(1)
const totalPages = ref(1)
//
const initDate = () => {
const today = new Date()
selectedDate.value = today.toISOString().split('T')[0]
}
//
const fetchRankingData = async () => {
loading.value = true
try {
const response = await axios.get('http://localhost:5001/api/rank/videos', {
params: {
page: currentPage.value,
limit: 20,
sort: 'growth',
start_date: selectedDate.value,
end_date: selectedDate.value
}
})
if (response.data.success) {
rankingData.value = response.data.data
totalPages.value = response.data.pagination.pages
} else {
console.error('获取数据失败:', response.data.message)
rankingData.value = []
}
} catch (error) {
console.error('API调用失败:', error)
rankingData.value = []
} finally {
loading.value = false
}
}
//
const getCurrentTime = () => {
const now = new Date()
return `${now.getHours().toString().padStart(2, '0')}:${now.getMinutes().toString().padStart(2, '0')}`
}
const getRankClass = (rank) => {
if (rank === 1) return 'rank-first'
if (rank === 2) return 'rank-second'
if (rank === 3) return 'rank-third'
return 'rank-normal'
}
//
const formatPlayCount = (count) => {
if (!count) return '0'
if (count >= 100000000) {
return (count / 100000000).toFixed(1) + '亿'
} else if (count >= 10000) {
return (count / 10000).toFixed(1) + '万'
}
return count.toString()
}
//
const formatGrowth = (item) => {
const timelineData = item.timeline_data || {}
const change = timelineData.play_vv_change || 0
const changeRate = timelineData.play_vv_change_rate || 0
if (change > 0) {
return `+${formatPlayCount(change)} (${changeRate.toFixed(1)}%)`
}
return '暂无数据'
}
//
const switchTab = (tab) => {
currentTab.value = tab
if (tab === 'ranking') {
fetchRankingData()
}
}
//
const onDateChange = () => {
currentPage.value = 1
fetchRankingData()
}
//
onMounted(() => {
initDate()
fetchRankingData()
})
</script>
<template>
<div class="app">
<!-- 主内容区域 -->
<div class="main-content">
<!-- 排行榜页面 -->
<div v-if="currentTab === 'ranking'" class="ranking-page">
<!-- 标题 -->
<div class="header">
<div class="title-container">
<span class="lightning-icon"></span>
<h1 class="title">热播总榜</h1>
<span class="lightning-icon"></span>
</div>
<div class="update-time">
基于实时热度排行 {{ getCurrentTime() }}更新
<span class="refresh-icon">🔄</span>
</div>
</div>
<!-- 日期选择 -->
<div class="date-selector">
<label for="date-input">选择日期</label>
<input
id="date-input"
type="date"
v-model="selectedDate"
@change="onDateChange"
class="date-input"
/>
</div>
<!-- 加载状态 -->
<div v-if="loading" class="loading">
<div class="loading-spinner"></div>
<p>加载中...</p>
</div>
<!-- 榜单内容 -->
<div v-else class="ranking-list">
<div
v-for="(item, index) in rankingData"
:key="item._id || index"
class="ranking-item"
>
<!-- 排名 -->
<div class="rank-number" :class="getRankClass(index + 1)">
{{ index + 1 }}
</div>
<!-- 海报 -->
<div class="poster">
<img
:src="item.cover_image_url || '/placeholder-poster.svg'"
:alt="item.title || item.mix_name"
@error="$event.target.src='/placeholder-poster.svg'"
class="poster-img"
/>
</div>
<!-- 内容信息 -->
<div class="content-info">
<!-- 剧名 -->
<h3 class="drama-name">{{ item.title || item.mix_name || '未知剧名' }}</h3>
<!-- 当前播放量 -->
<div class="play-count">
<span class="play-label">总播放量</span>
<span class="play-value">{{ formatPlayCount(item.play_vv) }}</span>
</div>
<!-- 简介省略显示 -->
<div class="description">
{{ item.summary || item.title || item.mix_name || '暂无简介' }}
</div>
</div>
<!-- 增长数据 -->
<div class="growth-data">
<span class="growth-icon">🔥</span>
<span class="growth-number">{{ formatGrowth(item) }}</span>
</div>
</div>
<!-- 空状态 -->
<div v-if="rankingData.length === 0" class="empty-state">
<p>暂无排行榜数据</p>
</div>
</div>
</div>
<!-- 资讯页面占位 -->
<div v-else class="news-page">
<div class="header">
<h1 class="title">资讯中心</h1>
</div>
<div class="coming-soon">
<p>资讯功能即将上线...</p>
</div>
</div>
</div>
<!-- 底部导航 -->
<div class="bottom-nav">
<div
class="nav-item"
:class="{ active: currentTab === 'news' }"
@click="switchTab('news')"
>
<i class="bi bi-newspaper"></i>
<span class="nav-text">资讯</span>
</div>
<div
class="nav-item"
:class="{ active: currentTab === 'ranking' }"
@click="switchTab('ranking')"
>
<i class="bi bi-list-stars"></i>
<span class="nav-text">排行榜</span>
</div>
</div>
</div>
</template>
<style scoped>
/* 全局样式 */
* {
box-sizing: border-box;
}
.app {
min-height: 100vh;
background: linear-gradient(135deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding-bottom: 80px; /* 为底部导航留出空间 */
color: white;
padding: 0;
margin: 0;
}
/* 主内容区域 */
.main-content {
padding: 20px 16px 80px;
max-width: 100%;
}
/* 标题区域 */
.header {
text-align: center;
margin-bottom: 20px;
}
.title-container {
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-bottom: 8px;
}
.lightning-icon {
font-size: 20px;
color: #ffd700;
}
.title {
color: white;
font-size: 24px;
font-weight: bold;
margin: 0;
text-shadow: 0 2px 4px rgba(0,0,0,0.3);
}
.update-time {
font-size: 12px;
color: #999;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
}
.refresh-icon {
font-size: 12px;
color: #4CAF50;
}
/* 日期选择器 */
.date-selector {
background: rgba(255, 255, 255, 0.9);
padding: 15px;
border-radius: 12px;
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
.date-selector label {
font-weight: 500;
color: #333;
}
.date-input {
flex: 1;
padding: 8px 12px;
border: 2px solid #e1e5e9;
border-radius: 8px;
font-size: 16px;
background: white;
}
.date-input:focus {
outline: none;
border-color: #667eea;
}
/* 加载状态 */
.loading {
text-align: center;
padding: 40px 20px;
color: white;
}
.loading-spinner {
width: 40px;
height: 40px;
border: 4px solid rgba(255,255,255,0.3);
border-top: 4px solid white;
border-radius: 50%;
animation: spin 1s linear infinite;
margin: 0 auto 15px;
}
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* 榜单列表 */
.ranking-list {
display: flex;
flex-direction: column;
gap: 15px;
}
.ranking-item {
background: rgba(255, 255, 255, 0.95);
border-radius: 16px;
padding: 15px;
display: flex;
align-items: flex-start;
gap: 15px;
box-shadow: 0 4px 20px rgba(0,0,0,0.1);
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.ranking-item:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(0,0,0,0.15);
}
/* 排名数字 */
.rank-number {
background: linear-gradient(135deg, #ff6b6b, #ee5a24);
color: white;
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-weight: bold;
font-size: 14px;
flex-shrink: 0;
box-shadow: 0 2px 8px rgba(255,107,107,0.3);
}
/* 海报 */
.poster {
flex-shrink: 0;
}
.poster-img {
width: 60px;
height: 80px;
object-fit: cover;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
}
/* 内容信息 */
.content-info {
flex: 1;
min-width: 0;
}
.drama-name {
font-size: 16px;
font-weight: bold;
color: #2c3e50;
margin: 0 0 8px 0;
line-height: 1.3;
}
.growth-info, .play-count {
display: flex;
align-items: center;
margin-bottom: 6px;
font-size: 13px;
}
.growth-label, .play-label {
color: #7f8c8d;
margin-right: 5px;
}
.growth-value {
color: #e74c3c;
font-weight: 600;
}
.play-value {
color: #3498db;
font-weight: 600;
}
.description {
color: #7f8c8d;
font-size: 12px;
line-height: 1.4;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-top: 8px;
}
/* 空状态 */
.empty-state {
text-align: center;
padding: 60px 20px;
color: rgba(255,255,255,0.8);
font-size: 16px;
}
/* 资讯页面 */
.news-page .coming-soon {
text-align: center;
padding: 100px 20px;
color: rgba(255,255,255,0.8);
font-size: 18px;
}
/* 底部导航 */
.bottom-nav {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(10px);
display: flex;
border-top: 1px solid rgba(0,0,0,0.1);
box-shadow: 0 -2px 20px rgba(0,0,0,0.1);
}
.nav-item {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
padding: 12px 8px;
cursor: pointer;
transition: all 0.2s ease;
color: #7f8c8d;
}
.nav-item.active {
color: #667eea;
background: rgba(102, 126, 234, 0.1);
}
.nav-item:hover {
background: rgba(0,0,0,0.05);
}
.nav-icon {
font-size: 20px;
margin-bottom: 4px;
}
.nav-text {
font-size: 12px;
font-weight: 500;
}
/* 移动端适配 */
@media (max-width: 768px) {
.main-content {
padding: 15px;
}
.title {
font-size: 20px;
}
.ranking-item {
padding: 12px;
gap: 12px;
}
.poster-img {
width: 50px;
height: 67px;
}
.drama-name {
font-size: 15px;
}
.date-selector {
padding: 12px;
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.date-input {
width: 100%;
}
}
@media (max-width: 480px) {
.main-content {
padding: 10px;
}
.ranking-item {
padding: 10px;
gap: 10px;
}
.poster-img {
width: 45px;
height: 60px;
}
.rank-number {
width: 28px;
height: 28px;
font-size: 12px;
}
}
</style>

4
frontend/src/main.js Normal file
View File

@ -0,0 +1,4 @@
import { createApp } from 'vue'
import App from './App.vue'
createApp(App).mount('#app')

18
frontend/vite.config.js Normal file
View File

@ -0,0 +1,18 @@
import { fileURLToPath, URL } from 'node:url'
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url))
},
},
})

54
package-lock.json generated Normal file
View File

@ -0,0 +1,54 @@
{
"name": "rank_backend",
"lockfileVersion": 2,
"requires": true,
"packages": {
"": {
"dependencies": {
"bootstrap": "^5.3.0-alpha1"
}
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/bootstrap": {
"version": "5.3.0-alpha1",
"resolved": "https://registry.npmmirror.com/bootstrap/-/bootstrap-5.3.0-alpha1.tgz",
"integrity": "sha512-ABZpKK4ObS3kKlIqH+ZVDqoy5t/bhFG0oHTAzByUdon7YIom0lpCeTqRniDzJmbtcWkNe800VVPBiJgxSYTYew==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/twbs"
},
{
"type": "opencollective",
"url": "https://opencollective.com/bootstrap"
}
],
"peerDependencies": {
"@popperjs/core": "^2.11.6"
}
}
},
"dependencies": {
"@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"peer": true
},
"bootstrap": {
"version": "5.3.0-alpha1",
"resolved": "https://registry.npmmirror.com/bootstrap/-/bootstrap-5.3.0-alpha1.tgz",
"integrity": "sha512-ABZpKK4ObS3kKlIqH+ZVDqoy5t/bhFG0oHTAzByUdon7YIom0lpCeTqRniDzJmbtcWkNe800VVPBiJgxSYTYew==",
"requires": {}
}
}
}

5
package.json Normal file
View File

@ -0,0 +1,5 @@
{
"dependencies": {
"bootstrap": "^5.3.0-alpha1"
}
}