1.添加前端后台管理,并且成功分类

2.数据库整理清晰
This commit is contained in:
qiaoyirui0819 2025-11-04 21:37:23 +08:00
parent fafb0aee4f
commit 36be77948f
12 changed files with 3294 additions and 426 deletions

View File

@ -277,6 +277,9 @@ class DouyinAutoScheduler:
"data": [] "data": []
} }
# 获取Rankings_management集合用于补充详细信息
rankings_management_collection = db['Rankings_management']
# 生成排序后的榜单数据 # 生成排序后的榜单数据
for i, item in enumerate(videos_with_growth, 1): for i, item in enumerate(videos_with_growth, 1):
video = item["video"] video = item["video"]
@ -290,16 +293,46 @@ class DouyinAutoScheduler:
yesterday_rank = item["yesterday_data"].get("rank", 0) yesterday_rank = item["yesterday_data"].get("rank", 0)
rank_change = yesterday_rank - i rank_change = yesterday_rank - i
# 🔍 从Rankings_management获取详细信息
management_data = rankings_management_collection.find_one({"mix_name": mix_name})
ranking_item = { ranking_item = {
# 🎯 核心榜单字段
"rank": i, "rank": i,
"title": mix_name, "title": mix_name,
"mix_name": mix_name, # 确保包含mix_name字段用于同步
"play_vv": current_play_vv, "play_vv": current_play_vv,
"author": video.get("author", ""), "series_author": video.get("series_author", ""),
"video_id": video_id, "video_id": video_id,
"video_url": video.get("video_url", ""), "video_url": video.get("video_url", ""),
"cover_image_url": video.get("cover_image_url", ""), "cover_image_url": video.get("cover_image_url", ""),
"playcount_str": video.get("playcount", ""), "playcount_str": video.get("playcount", ""),
# 时间轴对比数据
# 📋 从Rankings_management获取的详细字段
"batch_id": management_data.get("batch_id", "") if management_data else "",
"batch_time": management_data.get("batch_time") if management_data else None,
"item_sequence": management_data.get("item_sequence", 0) if management_data else 0,
"mix_id": management_data.get("mix_id", "") if management_data else "",
"playcount": management_data.get("playcount", "") if management_data else "",
"request_id": management_data.get("request_id", "") if management_data else "",
"cover_image_url_original": management_data.get("cover_image_url_original", "") if management_data else "",
"cover_upload_success": management_data.get("cover_upload_success", True) if management_data else True,
"cover_backup_urls": management_data.get("cover_backup_urls", []) if management_data else [],
"desc": management_data.get("desc", "") if management_data else "",
"updated_to_episode": management_data.get("updated_to_episode", 0) if management_data else 0,
"episode_video_ids": management_data.get("episode_video_ids", []) if management_data else [],
"episode_details": management_data.get("episode_details", []) if management_data else [],
"data_status": management_data.get("data_status", "") if management_data else "",
"realtime_saved": management_data.get("realtime_saved", True) if management_data else True,
"created_at": management_data.get("created_at") if management_data else None,
"last_updated": management_data.get("last_updated") if management_data else None,
"Manufacturing_Field": management_data.get("Manufacturing_Field", "") if management_data else "",
"Copyright_field": management_data.get("Copyright_field", "") if management_data else "",
"Novel_IDs": management_data.get("Novel_IDs", []) if management_data else [],
"Anime_IDs": management_data.get("Anime_IDs", []) if management_data else [],
"Drama_IDs": management_data.get("Drama_IDs", []) if management_data else [],
# 📊 时间轴对比数据(重要:包含播放量差值)
"timeline_data": { "timeline_data": {
"is_new": item["is_new"], "is_new": item["is_new"],
"rank_change": rank_change, "rank_change": rank_change,
@ -330,6 +363,29 @@ class DouyinAutoScheduler:
logging.info(f"📝 创建了新的今日榜单数据(第{existing_count + 1}次计算,包含最新差值)") logging.info(f"📝 创建了新的今日榜单数据(第{existing_count + 1}次计算,包含最新差值)")
logging.info(f"🔖 计算ID: {comprehensive_ranking['calculation_id']}") logging.info(f"🔖 计算ID: {comprehensive_ranking['calculation_id']}")
# 📊 检查数据完整性统计从Rankings_management成功获取详细信息的项目数量
total_items = len(comprehensive_ranking["data"])
items_with_management_data = 0
items_with_manufacturing = 0
items_with_copyright = 0
for item in comprehensive_ranking["data"]:
# 检查是否从Rankings_management获取到了数据
if item.get("batch_id") or item.get("desc") or item.get("Manufacturing_Field") or item.get("Copyright_field"):
items_with_management_data += 1
if item.get("Manufacturing_Field"):
items_with_manufacturing += 1
if item.get("Copyright_field"):
items_with_copyright += 1
print(f"📊 数据完整性统计:")
print(f" 总项目数: {total_items}")
print(f" 从Rankings_management获取到详细信息: {items_with_management_data}")
print(f" 包含Manufacturing_Field: {items_with_manufacturing}")
print(f" 包含Copyright_field: {items_with_copyright}")
logging.info(f"📊 数据完整性: 总{total_items}项,获取详细信息{items_with_management_data}Manufacturing_Field: {items_with_manufacturing}Copyright_field: {items_with_copyright}")
# 统计信息 # 统计信息
new_count = sum(1 for item in comprehensive_ranking["data"] if item["timeline_data"]["is_new"]) new_count = sum(1 for item in comprehensive_ranking["data"] if item["timeline_data"]["is_new"])
print(f"✅ 时间轴对比榜单生成成功") print(f"✅ 时间轴对比榜单生成成功")
@ -358,13 +414,165 @@ class DouyinAutoScheduler:
import traceback import traceback
logging.error(f"详细错误信息: {traceback.format_exc()}") logging.error(f"详细错误信息: {traceback.format_exc()}")
def check_and_sync_missing_fields(self):
"""实时检查并同步当天缺失字段"""
try:
from database import db
# 只检查当天的数据
today = date.today()
today_str = today.strftime('%Y-%m-%d')
# 首先检查 Rankings_management 是否有当天的数据
rankings_management_collection = db['Rankings_management']
management_count = rankings_management_collection.count_documents({})
if management_count == 0:
# Rankings_management 没有数据,说明还没有抓取,直接返回
return
rankings_collection = db['Ranking_storage']
key_fields = ['Manufacturing_Field', 'Copyright_field', 'desc', 'series_author']
# 检查今天是否有缺失字段的数据
missing_conditions = []
for field in key_fields:
missing_conditions.extend([
{field: {"$exists": False}},
{field: None},
{field: ""}
])
today_missing_count = rankings_collection.count_documents({
"date": today_str,
"$or": missing_conditions
})
# 如果今天没有缺失数据,静默返回
if today_missing_count == 0:
return
logging.info(f"🔍 检测到今天有 {today_missing_count} 条缺失字段Rankings_management有 {management_count} 条数据,开始实时同步...")
# 只处理当天的数据
dates_to_check = [today_str]
total_missing = 0
total_synced = 0
for check_date in dates_to_check:
# 查询该日期缺失字段的数据
rankings_collection = db['Ranking_storage']
# 检查多个关键字段(包括新增的分类字段)
key_fields = ['Manufacturing_Field', 'Copyright_field', 'desc', 'series_author', 'Novel_IDs', 'Anime_IDs', 'Drama_IDs']
missing_conditions = []
for field in key_fields:
missing_conditions.extend([
{field: {"$exists": False}},
{field: None},
{field: ""}
])
missing_query = {
"date": check_date,
"$or": missing_conditions
}
missing_count = rankings_collection.count_documents(missing_query)
# 详细统计每个字段的缺失情况
field_stats = {}
total_items = rankings_collection.count_documents({"date": check_date})
for field in key_fields:
missing_field_count = rankings_collection.count_documents({
"date": check_date,
"$or": [
{field: {"$exists": False}},
{field: None},
{field: ""}
]
})
field_stats[field] = {
"missing": missing_field_count,
"completion_rate": ((total_items - missing_field_count) / total_items * 100) if total_items > 0 else 0
}
if missing_count > 0:
logging.info(f"📅 今日({check_date}): 发现 {missing_count} 条记录缺失字段(总计 {total_items} 条)")
# 输出详细的字段统计
for field, stats in field_stats.items():
if stats["missing"] > 0:
logging.info(f" - {field}: 缺失 {stats['missing']} 条 ({stats['completion_rate']:.1f}% 完整)")
total_missing += missing_count
# 尝试同步
try:
from routers.rank_api_routes import sync_ranking_storage_fields
# 使用改进的重试机制
sync_result = sync_ranking_storage_fields(
target_date=check_date,
force_update=False,
max_retries=2, # 定期检查时重试2次
retry_delay=15 # 15秒重试间隔
)
if sync_result.get("success", False):
stats = sync_result.get("stats", {})
synced = stats.get("updated_items", 0)
retry_count = stats.get("retry_count", 0)
pending_final = stats.get("pending_items_final", 0)
total_synced += synced
if synced > 0:
logging.info(f"✅ 今日({check_date}): 成功同步 {synced} 条记录")
if retry_count > 0:
logging.info(f"🔄 今日({check_date}): 使用了 {retry_count} 次重试")
if pending_final > 0:
logging.warning(f"⚠️ 今日({check_date}): {pending_final} 条记录在 Rankings_management 中仍未找到")
else:
logging.warning(f"⚠️ 今日({check_date}): 同步失败 - {sync_result.get('message', '')}")
except Exception as sync_error:
logging.error(f"💥 今日({check_date}): 同步过程出错 - {sync_error}")
else:
if total_items > 0:
logging.info(f"📅 {check_date}: 所有字段完整(总计 {total_items} 条记录)")
# 显示完整性统计
for field, stats in field_stats.items():
logging.info(f" - {field}: {stats['completion_rate']:.1f}% 完整")
else:
logging.info(f"📅 {check_date}: 无数据")
if total_missing > 0:
logging.info(f"🔍 当天同步完成:发现 {total_missing} 条缺失记录,成功同步 {total_synced}")
print(f"🔍 当天字段同步:发现 {total_missing} 条缺失,同步 {total_synced}")
else:
# 当天没有缺失数据时,不输出日志(静默模式)
pass
except Exception as e:
logging.error(f"💥 检查缺失字段时发生异常: {e}")
import traceback
logging.error(f"详细错误信息: {traceback.format_exc()}")
def setup_schedule(self): def setup_schedule(self):
"""设置定时任务""" """设置定时任务"""
# 每小时的整点执行抖音播放量抓取 # 每小时的整点执行抖音播放量抓取
schedule.every().hour.at(":00").do(self.run_douyin_scraper) schedule.every().hour.at(":00").do(self.run_douyin_scraper)
# 每1分钟检查一次缺失字段并尝试同步实时同步
schedule.every(1).minutes.do(self.check_and_sync_missing_fields)
logging.info(f"⏰ 定时器已设置:每小时整点执行抖音播放量抓取") logging.info(f"⏰ 定时器已设置:每小时整点执行抖音播放量抓取")
logging.info(f"⏰ 定时器已设置每1分钟检查缺失字段并同步实时模式")
def show_next_run(self): def show_next_run(self):
"""显示下次执行时间""" """显示下次执行时间"""

View File

@ -143,9 +143,13 @@
{ {
"video_id": "7558378239337467174", "video_id": "7558378239337467174",
"episode_num": 0 "episode_num": 0
},
{
"video_id": "7567050545257516331",
"episode_num": 0
} }
], ],
"total_count": 36, "total_count": 37,
"last_update": "2025-10-22T09:55:32.073567", "last_update": "2025-10-31T09:50:18.533027",
"mix_name": "末世系列" "mix_name": "末世系列"
} }

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.2 KiB

View File

@ -1,5 +0,0 @@
<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>

Before

Width:  |  Height:  |  Size: 355 B

1178
frontend/src/AdminPanel.vue Normal file

File diff suppressed because it is too large Load Diff

View File

@ -1,7 +1,11 @@
<script setup> <script setup>
import { ref, onMounted, computed } from 'vue' import { ref, onMounted, computed } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import axios from 'axios' import axios from 'axios'
const router = useRouter()
const route = useRoute()
// //
const currentTab = ref('ranking') // 'ranking' 'news' const currentTab = ref('ranking') // 'ranking' 'news'
const rankingData = ref([]) const rankingData = ref([])
@ -59,14 +63,22 @@ const generateDateOptions = () => {
const fetchRankingData = async () => { const fetchRankingData = async () => {
loading.value = true loading.value = true
try { try {
const response = await axios.get('http://localhost:5001/api/rank/videos', { // API
params: { const params = {
page: currentPage.value, page: currentPage.value,
limit: 20, limit: 100,
sort: 'growth', sort: 'growth',
start_date: selectedDate.value, start_date: selectedDate.value,
end_date: selectedDate.value end_date: selectedDate.value
} }
//
if (selectedCategory.value !== 'all') {
params.classification_type = selectedCategory.value
}
const response = await axios.get('http://localhost:5001/api/rank/videos', {
params: params
}) })
if (response.data.success) { if (response.data.success) {
@ -74,6 +86,7 @@ const fetchRankingData = async () => {
totalPages.value = response.data.pagination.pages totalPages.value = response.data.pagination.pages
// //
updateTime.value = response.data.update_time || '' updateTime.value = response.data.update_time || ''
console.log(`获取${selectedCategory.value === 'all' ? '全部' : selectedCategory.value}分类数据成功,共${response.data.data.length}`)
} else { } else {
console.error('获取数据失败:', response.data.message) console.error('获取数据失败:', response.data.message)
rankingData.value = [] rankingData.value = []
@ -212,7 +225,9 @@ const selectDate = (dateValue) => {
// //
const switchCategory = (category) => { const switchCategory = (category) => {
selectedCategory.value = category selectedCategory.value = category
// currentPage.value = 1 //
fetchRankingData() //
console.log(`切换到分类: ${category}`)
} }
// //
@ -237,6 +252,11 @@ const getRankBadgeClass = (rank) => {
return 'rank-normal' return 'rank-normal'
} }
//
const goToAdmin = () => {
router.push('/admin')
}
// //
onMounted(() => { onMounted(() => {
initDate() initDate()
@ -246,12 +266,16 @@ onMounted(() => {
<template> <template>
<div class="app"> <div class="app">
<!-- 主容器 --> <!-- 路由视图 -->
<div class="main-container"> <router-view v-if="route.path !== '/'" />
<!-- 主容器 - 仅在首页显示 -->
<div v-if="route.path === '/'" class="main-container">
<!-- 顶部标题区域 --> <!-- 顶部标题区域 -->
<div class="header-section"> <div class="header-section">
<div class="title-wrapper"> <div class="title-wrapper">
<h1 class="main-title">AI棒榜</h1> <h1 class="main-title">AI棒榜</h1>
<button class="admin-btn" @click="goToAdmin">管理</button>
</div> </div>
</div> </div>
@ -347,9 +371,9 @@ onMounted(() => {
<img src="./images/版权icon.svg" alt="版权" class="detail-icon" /> <img src="./images/版权icon.svg" alt="版权" class="detail-icon" />
</div> </div>
<div class="detail-text"> <div class="detail-text">
<p>剧场名爱微剧场</p> <p>剧场名{{ item.series_author || '爱微剧场' }}</p>
<p>承制妙想制片厂</p> <p>承制{{ item.Manufacturing_Field || '妙想制片厂' }}</p>
<p>版权可梦</p> <p>版权{{ item.Copyright_field || '可梦' }}</p>
</div> </div>
</div> </div>
@ -361,7 +385,7 @@ onMounted(() => {
</div> </div>
<div class="stat-item"> <div class="stat-item">
<img src="./images/点赞icon.svg" alt="点赞" class="stat-icon" /> <img src="./images/点赞icon.svg" alt="点赞" class="stat-icon" />
<span class="stat-value">374W</span> <span class="stat-value">{{ item.total_likes_formatted || '0' }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -402,6 +426,8 @@ onMounted(() => {
</div> </div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
@ -433,6 +459,7 @@ onMounted(() => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
gap: 10px; gap: 10px;
position: relative;
} }
.logo-icon { .logo-icon {
width: 40px; width: 40px;
@ -446,6 +473,25 @@ onMounted(() => {
font-family: Alatsi, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimHei, Arial, Helvetica, sans-serif; font-family: Alatsi, 'PingFang SC', 'Hiragino Sans GB', 'Microsoft YaHei', SimHei, Arial, Helvetica, sans-serif;
} }
.admin-btn {
position: absolute;
right: 20px;
top: 50%;
transform: translateY(-50%);
background: #4a90e2;
color: white;
border: none;
border-radius: 6px;
padding: 6px 12px;
font-size: 12px;
cursor: pointer;
transition: background-color 0.3s ease;
}
.admin-btn:hover {
background: #357abd;
}
/* 横幅区域 */ /* 横幅区域 */
.banner-section { .banner-section {
margin: 20px 16px; margin: 20px 16px;

123
frontend/src/RootApp.vue Normal file
View File

@ -0,0 +1,123 @@
<script setup>
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
const navigateToHome = () => {
router.push('/')
}
const navigateToAdmin = () => {
router.push('/admin')
}
</script>
<template>
<div class="root-app">
<!-- 导航栏 -->
<nav class="navigation">
<div class="nav-container">
<div class="nav-brand">
<h1>AI棒榜系统</h1>
</div>
<div class="nav-links">
<button
class="nav-btn"
:class="{ active: route.path === '/admin' }"
@click="navigateToAdmin"
>
后台管理
</button>
</div>
</div>
</nav>
<!-- 路由视图 -->
<main class="main-content">
<router-view />
</main>
</div>
</template>
<style scoped>
.root-app {
min-height: 100vh;
background: #ebedf2;
}
/* 导航栏样式 */
.navigation {
background: white;
border-bottom: 1px solid #e0e0e0;
position: sticky;
top: 0;
z-index: 100;
}
.nav-container {
max-width: 375px;
margin: 0 auto;
padding: 12px 16px;
display: flex;
justify-content: space-between;
align-items: center;
}
.nav-brand h1 {
font-size: 18px;
font-weight: bold;
color: #333;
margin: 0;
}
.nav-links {
display: flex;
gap: 8px;
}
.nav-btn {
padding: 8px 16px;
border: 1px solid #ddd;
background: white;
color: #666;
border-radius: 6px;
cursor: pointer;
font-size: 14px;
font-weight: 500;
transition: all 0.3s ease;
}
.nav-btn:hover {
background: #f8f9fa;
border-color: #4a90e2;
}
.nav-btn.active {
background: #4a90e2;
color: white;
border-color: #4a90e2;
}
/* 主内容区域 */
.main-content {
min-height: calc(100vh - 60px);
}
/* 响应式设计 */
@media (max-width: 480px) {
.nav-container {
max-width: 100%;
padding: 10px 12px;
}
.nav-brand h1 {
font-size: 16px;
}
.nav-btn {
padding: 6px 12px;
font-size: 13px;
}
}
</style>

View File

@ -1,4 +1,8 @@
import { createApp } from 'vue' import { createApp } from 'vue'
import './style.css'
import App from './App.vue' import App from './App.vue'
import router from './router'
createApp(App).mount('#app') const app = createApp(App)
app.use(router).mount('#app')

View File

@ -0,0 +1,17 @@
import { createRouter, createWebHistory } from 'vue-router'
import AdminPanel from '../AdminPanel.vue'
const routes = [
{
path: '/admin',
name: 'Admin',
component: AdminPanel
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router

60
frontend/src/style.css Normal file
View File

@ -0,0 +1,60 @@
/* 全局样式重置 */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: #ebedf2;
color: #333;
}
#app {
width: 100%;
min-height: 100vh;
}
/* 通用按钮样式 */
button {
cursor: pointer;
border: none;
outline: none;
font-family: inherit;
}
/* 通用输入框样式 */
input, textarea, select {
font-family: inherit;
outline: none;
}
/* 滚动条样式 */
::-webkit-scrollbar {
width: 6px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* 响应式设计 */
@media (max-width: 480px) {
body {
font-size: 14px;
}
}