From 91761b6754b2ca55b5f1a5317c1fc603a76794aa Mon Sep 17 00:00:00 2001 From: Qyir <13521889462@163.com> Date: Thu, 13 Nov 2025 17:49:48 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=9F=AD=E5=89=A7=E7=89=88?= =?UTF-8?q?=E6=9D=83=E6=96=B9=E8=AE=A4=E8=AF=81=E9=A1=B5=E9=9D=A2=20?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E7=94=B3=E8=AF=B7=E7=AE=A1=E7=90=86=E9=A1=B5?= =?UTF-8?q?=E9=9D=A2=E4=B8=8A=E4=BC=A0=E8=87=B3TOS?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app.py | 2 - backend/routers/article_routes.py | 268 ---------- backend/routers/rank_api_routes.py | 680 +++++++++++++++++++++++++- frontend/src/AdminPanel.vue | 10 +- frontend/src/ClaimApplications.vue | 682 ++++++++++++++++++++++++++ frontend/src/ClaimPage.vue | 759 +++++++++++++++++++++++++++++ frontend/src/DramaDetail.vue | 19 +- frontend/src/router/index.js | 12 + 8 files changed, 2156 insertions(+), 276 deletions(-) delete mode 100644 backend/routers/article_routes.py create mode 100644 frontend/src/ClaimApplications.vue create mode 100644 frontend/src/ClaimPage.vue diff --git a/backend/app.py b/backend/app.py index 4eea4dd..99e868f 100644 --- a/backend/app.py +++ b/backend/app.py @@ -42,9 +42,7 @@ logging.basicConfig( # 导入并注册蓝图 from routers.rank_api_routes import rank_bp -from routers.article_routes import article_bp app.register_blueprint(rank_bp) -app.register_blueprint(article_bp) if __name__ == '__main__': diff --git a/backend/routers/article_routes.py b/backend/routers/article_routes.py deleted file mode 100644 index 9d99260..0000000 --- a/backend/routers/article_routes.py +++ /dev/null @@ -1,268 +0,0 @@ -#!/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") - } - }) \ No newline at end of file diff --git a/backend/routers/rank_api_routes.py b/backend/routers/rank_api_routes.py index f83e5aa..8038f0e 100644 --- a/backend/routers/rank_api_routes.py +++ b/backend/routers/rank_api_routes.py @@ -9,7 +9,10 @@ from flask import Blueprint, request, jsonify from datetime import datetime, timedelta import logging import re +import uuid +from werkzeug.utils import secure_filename from database import db +from handlers.Rankings.tos_client import oss_client # 创建蓝图 rank_bp = Blueprint('rank', __name__, url_prefix='/api/rank') @@ -17,6 +20,7 @@ rank_bp = Blueprint('rank', __name__, url_prefix='/api/rank') # 获取数据库集合 collection = db['Ranking_storage'] # 主要数据源:榜单存储表(包含data数组) rankings_management_collection = db['Rankings_management'] # 管理数据库(字段同步源) +claim_applications_collection = db['Claim_Applications'] # 认领申请集合 def format_playcount(playcount_str): """格式化播放量字符串为数字""" @@ -2258,4 +2262,678 @@ def get_drama_detail_by_id(drama_id): return jsonify({ "success": False, "message": f"获取短剧详情失败: {str(e)}" - }) \ No newline at end of file + }) + +def upload_certification_file(file): + """ + 上传认领证明文件到TOS + + Args: + file: 上传的文件对象 + + Returns: + str: TOS永久链接URL + """ + try: + # 获取文件扩展名 + filename = secure_filename(file.filename) + file_extension = '' + if '.' in filename: + file_extension = '.' + filename.rsplit('.', 1)[1].lower() + + # 验证文件类型 + allowed_image_extensions = ['.jpg', '.jpeg', '.png', '.gif'] + allowed_doc_extensions = ['.pdf', '.doc', '.docx'] + + if file_extension not in allowed_image_extensions + allowed_doc_extensions: + raise ValueError(f"不支持的文件类型: {file_extension}") + + # 验证文件大小 + file.seek(0, 2) # 移动到文件末尾 + file_size = file.tell() # 获取文件大小 + file.seek(0) # 重置文件指针 + + max_size = 10 * 1024 * 1024 # 10MB for images + if file_extension in allowed_doc_extensions: + max_size = 20 * 1024 * 1024 # 20MB for documents + + if file_size > max_size: + raise ValueError(f"文件大小超过限制: {file_size / 1024 / 1024:.2f}MB") + + # 生成唯一文件名 + random_filename = f"{uuid.uuid4().hex}{file_extension}" + object_key = f"media/rank/Certification/{random_filename}" + + # 上传到TOS + tos_url = oss_client.upload_bytes( + data=file.read(), + object_key=object_key, + content_type=file.content_type or 'application/octet-stream', + return_url=True + ) + + logging.info(f"文件上传成功: {filename} -> {tos_url}") + return tos_url + + except Exception as e: + logging.error(f"文件上传失败: {str(e)}") + raise + + +@rank_bp.route('/claim', methods=['POST']) +def submit_claim(): + """ + 提交认领申请(新版本:上传文件到TOS并创建待审核申请) + """ + try: + # 获取表单数据 + drama_id = request.form.get('drama_id') + field_type = request.form.get('field_type') # 'copyright' 或 'manufacturing' + company_name = request.form.get('company_name') + description = request.form.get('description', '') + + # 验证必填字段 + if not all([drama_id, field_type, company_name]): + return jsonify({ + "success": False, + "message": "缺少必填字段" + }), 400 + + # 验证字段类型 + if field_type not in ['copyright', 'manufacturing']: + return jsonify({ + "success": False, + "message": "无效的字段类型" + }), 400 + + # 获取短剧信息 + drama_info = rankings_management_collection.find_one({"mix_id": drama_id}) + if not drama_info: + return jsonify({ + "success": False, + "message": "未找到对应的短剧" + }), 404 + + drama_name = drama_info.get('mix_name', '未知短剧') + + # 处理上传的文件并上传到TOS + uploaded_files = request.files.getlist('files') + tos_file_urls = [] + + if uploaded_files: + for file in uploaded_files: + if file and file.filename: + try: + tos_url = upload_certification_file(file) + tos_file_urls.append(tos_url) + except ValueError as ve: + return jsonify({ + "success": False, + "message": str(ve) + }), 400 + except Exception as e: + return jsonify({ + "success": False, + "message": f"文件上传失败: {str(e)}" + }), 500 + + if not tos_file_urls: + return jsonify({ + "success": False, + "message": "请至少上传一个证明文件" + }), 400 + + # 检查是否存在该短剧+该字段类型的待审核申请 + existing_application = claim_applications_collection.find_one({ + "drama_id": drama_id, + "field_type": field_type, + "status": "pending" + }) + + # 如果存在待审核申请,删除旧的(但保留TOS文件) + if existing_application: + claim_applications_collection.delete_one({"_id": existing_application["_id"]}) + logging.info(f"删除旧的待审核申请: {existing_application.get('application_id')}") + + # 创建新的申请记录 + application_id = str(uuid.uuid4()) + application_data = { + "application_id": application_id, + "drama_id": drama_id, + "drama_name": drama_name, + "field_type": field_type, + "company_name": company_name, + "description": description, + "tos_file_urls": tos_file_urls, + "status": "pending", + "submit_time": datetime.now(), + "review_time": None, + "reviewer": None, + "reject_reason": None + } + + claim_applications_collection.insert_one(application_data) + + logging.info(f"认领申请创建成功: application_id={application_id}, drama_id={drama_id}, field_type={field_type}") + + return jsonify({ + "success": True, + "message": "认领申请提交成功,等待管理员审核", + "data": { + "application_id": application_id, + "drama_id": drama_id, + "field_type": field_type, + "company_name": company_name, + "file_count": len(tos_file_urls) + } + }) + + except Exception as e: + logging.error(f"提交认领申请失败: {e}") + return jsonify({ + "success": False, + "message": f"提交认领申请失败: {str(e)}" + }), 500 + + +# 获取申请列表 +@rank_bp.route('/claim/applications', methods=['GET']) +def get_claim_applications(): + """ + 获取认领申请列表 + 支持筛选和分页 + """ + try: + # 获取查询参数 + status = request.args.get('status', 'all') # all/pending/approved/rejected + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 20)) + + # 构建查询条件 + query = {} + if status != 'all': + query['status'] = status + + # 查询总数 + total = claim_applications_collection.count_documents(query) + + # 查询数据(按提交时间倒序) + applications = list(claim_applications_collection.find(query) + .sort('submit_time', -1) + .skip((page - 1) * limit) + .limit(limit)) + + # 格式化数据 + formatted_applications = [] + for app in applications: + formatted_applications.append({ + "application_id": app.get('application_id'), + "drama_id": app.get('drama_id'), + "drama_name": app.get('drama_name'), + "field_type": app.get('field_type'), + "field_type_label": "版权方" if app.get('field_type') == 'copyright' else "承制方", + "company_name": app.get('company_name'), + "status": app.get('status'), + "status_label": { + "pending": "待审核", + "approved": "已通过", + "rejected": "已拒绝" + }.get(app.get('status'), "未知"), + "submit_time": app.get('submit_time').strftime("%Y-%m-%d %H:%M:%S") if app.get('submit_time') else "", + "file_count": len(app.get('tos_file_urls', [])) + }) + + return jsonify({ + "success": True, + "data": formatted_applications, + "pagination": { + "page": page, + "limit": limit, + "total": total, + "pages": (total + limit - 1) // limit + } + }) + + except Exception as e: + logging.error(f"获取申请列表失败: {e}") + return jsonify({ + "success": False, + "message": f"获取申请列表失败: {str(e)}" + }), 500 + + +# 获取申请详情 +@rank_bp.route('/claim/application/', methods=['GET']) +def get_claim_application_detail(application_id): + """ + 获取认领申请详情 + """ + try: + application = claim_applications_collection.find_one({"application_id": application_id}) + + if not application: + return jsonify({ + "success": False, + "message": "申请不存在" + }), 404 + + # 格式化数据 + formatted_data = { + "application_id": application.get('application_id'), + "drama_id": application.get('drama_id'), + "drama_name": application.get('drama_name'), + "field_type": application.get('field_type'), + "field_type_label": "版权方" if application.get('field_type') == 'copyright' else "承制方", + "company_name": application.get('company_name'), + "description": application.get('description', ''), + "tos_file_urls": application.get('tos_file_urls', []), + "status": application.get('status'), + "status_label": { + "pending": "待审核", + "approved": "已通过", + "rejected": "已拒绝" + }.get(application.get('status'), "未知"), + "submit_time": application.get('submit_time').strftime("%Y-%m-%d %H:%M:%S") if application.get('submit_time') else "", + "review_time": application.get('review_time').strftime("%Y-%m-%d %H:%M:%S") if application.get('review_time') else None, + "reviewer": application.get('reviewer'), + "reject_reason": application.get('reject_reason') + } + + return jsonify({ + "success": True, + "data": formatted_data + }) + + except Exception as e: + logging.error(f"获取申请详情失败: {e}") + return jsonify({ + "success": False, + "message": f"获取申请详情失败: {str(e)}" + }), 500 + + +# 审核申请 +@rank_bp.route('/claim/review', methods=['POST']) +def review_claim_application(): + """ + 审核认领申请 + """ + try: + data = request.get_json() + application_id = data.get('application_id') + action = data.get('action') # 'approve' 或 'reject' + reject_reason = data.get('reject_reason', '') + reviewer = data.get('reviewer', 'admin') # 审核人 + + # 验证参数 + if not application_id or not action: + return jsonify({ + "success": False, + "message": "缺少必填参数" + }), 400 + + if action not in ['approve', 'reject']: + return jsonify({ + "success": False, + "message": "无效的操作类型" + }), 400 + + if action == 'reject' and not reject_reason: + return jsonify({ + "success": False, + "message": "拒绝时必须填写理由" + }), 400 + + # 查找申请 + application = claim_applications_collection.find_one({"application_id": application_id}) + if not application: + return jsonify({ + "success": False, + "message": "申请不存在" + }), 404 + + if application.get('status') != 'pending': + return jsonify({ + "success": False, + "message": "该申请已经被审核过了" + }), 400 + + # 执行审核操作 + if action == 'approve': + # 通过:更新短剧字段并锁定 + drama_id = application.get('drama_id') + field_type = application.get('field_type') + company_name = application.get('company_name') + description = application.get('description', '') + tos_file_urls = application.get('tos_file_urls', []) + + field_name = 'Copyright_field' if field_type == 'copyright' else 'Manufacturing_field' + + # 更新 Rankings_management 数据库 + update_data = { + field_name: company_name, + f"{field_name}_claim_description": description, + f"{field_name}_claim_images": tos_file_urls, + f"{field_name}_claim_time": datetime.now(), + "last_updated": datetime.now() + } + + # 设置锁定状态 + lock_status_update = { + f"field_lock_status.{field_name}": True, + f"field_lock_status.{field_name}_claim_description": True, + f"field_lock_status.{field_name}_claim_images": True, + f"field_lock_status.{field_name}_claim_time": True + } + update_data.update(lock_status_update) + + rankings_management_collection.update_one( + {"mix_id": drama_id}, + {"$set": update_data} + ) + + # 同步更新 Ranking_storage 数据库 + ranking_storage_update = { + f"data.$[elem].{field_name}": company_name, + f"data.$[elem].{field_name}_claim_description": description, + f"data.$[elem].{field_name}_claim_images": tos_file_urls, + f"data.$[elem].{field_name}_claim_time": datetime.now(), + f"data.$[elem].field_lock_status.{field_name}": True, + f"data.$[elem].field_lock_status.{field_name}_claim_description": True, + f"data.$[elem].field_lock_status.{field_name}_claim_images": True, + f"data.$[elem].field_lock_status.{field_name}_claim_time": True + } + + collection.update_many( + {"data.mix_id": drama_id}, + {"$set": ranking_storage_update}, + array_filters=[{"elem.mix_id": drama_id}] + ) + + # 更新申请状态 + claim_applications_collection.update_one( + {"application_id": application_id}, + {"$set": { + "status": "approved", + "review_time": datetime.now(), + "reviewer": reviewer + }} + ) + + logging.info(f"认领申请审核通过: application_id={application_id}, drama_id={drama_id}") + + return jsonify({ + "success": True, + "message": "申请已通过,短剧信息已更新" + }) + + else: # reject + # 拒绝:只更新申请状态 + claim_applications_collection.update_one( + {"application_id": application_id}, + {"$set": { + "status": "rejected", + "review_time": datetime.now(), + "reviewer": reviewer, + "reject_reason": reject_reason + }} + ) + + logging.info(f"认领申请已拒绝: application_id={application_id}, reason={reject_reason}") + + return jsonify({ + "success": True, + "message": "申请已拒绝" + }) + + except Exception as e: + logging.error(f"审核申请失败: {e}") + return jsonify({ + "success": False, + "message": f"审核申请失败: {str(e)}" + }), 500 + + +# 获取待审核数量 +@rank_bp.route('/claim/pending-count', methods=['GET']) +def get_pending_claim_count(): + """ + 获取待审核的认领申请数量 + """ + try: + count = claim_applications_collection.count_documents({"status": "pending"}) + + return jsonify({ + "success": True, + "count": count + }) + + except Exception as e: + logging.error(f"获取待审核数量失败: {e}") + return jsonify({ + "success": False, + "message": f"获取待审核数量失败: {str(e)}" + }), 500 + + +# ==================== 文章相关API ==================== +# 获取数据库集合 +articles_collection = db['articles'] + +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_data(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_data(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_data(article_id): + """获取文章详情""" + try: + from bson import ObjectId + try: + doc = articles_collection.find_one({"_id": ObjectId(article_id)}) + except: + 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_article_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)}"} + +# 文章路由定义 +@rank_bp.route('/article/list') +def get_articles_route(): + """获取文章列表""" + 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_data(page, limit, sort_by, status) + return jsonify(result) + +@rank_bp.route('/article/search') +def search_articles_route(): + """搜索文章""" + keyword = request.args.get('q', '') + page = int(request.args.get('page', 1)) + limit = int(request.args.get('limit', 10)) + result = search_articles_data(keyword, page, limit) + return jsonify(result) + +@rank_bp.route('/article/detail') +def get_article_detail_route(): + """获取文章详情""" + article_id = request.args.get('id', '') + result = get_article_detail_data(article_id) + return jsonify(result) + +@rank_bp.route('/article/stats') +def get_article_stats_route(): + """获取统计信息""" + result = get_article_statistics() + return jsonify(result) + +@rank_bp.route('/article/health') +def article_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") + } + }) diff --git a/frontend/src/AdminPanel.vue b/frontend/src/AdminPanel.vue index e553e52..c746abc 100644 --- a/frontend/src/AdminPanel.vue +++ b/frontend/src/AdminPanel.vue @@ -342,6 +342,11 @@ const goBack = () => { router.push('/') } +// 跳转到认领申请管理页面 +const goToClaimApplications = () => { + router.push('/admin/claim-applications') +} + // 页面加载时初始化 onMounted(() => { fetchRankingData() @@ -359,6 +364,9 @@ onMounted(() => {

AI棒榜 - 后台管理

+ @@ -586,7 +594,7 @@ export default { /* 主容器 */ .main-container { - max-width: 375px; + max-width: 428px; margin: 0 auto; background: #f5f5f5; min-height: 100vh; diff --git a/frontend/src/ClaimApplications.vue b/frontend/src/ClaimApplications.vue new file mode 100644 index 0000000..0db6e54 --- /dev/null +++ b/frontend/src/ClaimApplications.vue @@ -0,0 +1,682 @@ + + + + + diff --git a/frontend/src/ClaimPage.vue b/frontend/src/ClaimPage.vue new file mode 100644 index 0000000..25114b5 --- /dev/null +++ b/frontend/src/ClaimPage.vue @@ -0,0 +1,759 @@ + + +