This commit is contained in:
2025-11-28 21:20:40 +08:00
commit f940b95b67
73 changed files with 15721 additions and 0 deletions

1
routes/__init__.py Normal file
View File

@@ -0,0 +1 @@
# Routes package

768
routes/admin.py Normal file
View File

@@ -0,0 +1,768 @@
from flask import Blueprint, request, jsonify, session
from models import Admin, User, UserGroup, ParserAPI, SiteConfig, SMTPConfig, ParseLog, DailyParseStat, HealthCheckConfig
from models import db
from utils.security import hash_password, verify_password, get_client_ip
from utils.admin_auth import admin_required, verify_2fa, generate_2fa_secret, get_2fa_qrcode_url
from utils.email import EmailService
from datetime import datetime, timedelta, date
from sqlalchemy import func
import qrcode
import io
import base64
admin_bp = Blueprint('admin', __name__)
@admin_bp.route('/login', methods=['GET', 'POST'])
def login():
"""管理员登录"""
if request.method == 'GET':
from flask import render_template
return render_template('admin_login.html')
data = request.get_json()
username = data.get('username')
password = data.get('password')
code_2fa = data.get('code_2fa')
if not all([username, password]):
return jsonify({'success': False, 'message': '请填写完整信息'}), 400
admin = Admin.query.filter_by(username=username).first()
if not admin or not verify_password(password, admin.password):
return jsonify({'success': False, 'message': '用户名或密码错误'}), 401
# 检查2FA
if admin.is_2fa_enabled:
if not code_2fa:
return jsonify({'success': False, 'message': '请输入2FA验证码', 'require_2fa': True}), 400
if not verify_2fa(admin, code_2fa):
return jsonify({'success': False, 'message': '2FA验证码错误'}), 401
# 更新登录信息
admin.last_login_ip = get_client_ip(request)
admin.last_login_at = datetime.utcnow()
db.session.commit()
# 设置会话
session['admin_id'] = admin.id
session['admin_username'] = admin.username
return jsonify({'success': True, 'message': '登录成功'})
@admin_bp.route('/logout', methods=['POST'])
@admin_required
def logout():
"""管理员登出"""
session.pop('admin_id', None)
session.pop('admin_username', None)
return jsonify({'success': True, 'message': '已退出登录'})
@admin_bp.route('/dashboard', methods=['GET'])
@admin_required
def dashboard():
"""仪表板页面"""
from flask import render_template
return render_template('admin_dashboard.html')
@admin_bp.route('/api/dashboard', methods=['GET'])
@admin_required
def dashboard_api():
"""仪表板统计API"""
today = date.today()
# 今日统计
today_stats = db.session.query(
func.sum(DailyParseStat.parse_count).label('total'),
func.sum(DailyParseStat.success_count).label('success'),
func.sum(DailyParseStat.fail_count).label('fail')
).filter(DailyParseStat.date == today).first()
# 总用户数
total_users = User.query.count()
# 总解析次数
total_parses = ParseLog.query.count()
# 活跃API数
active_apis = ParserAPI.query.filter_by(is_enabled=True, health_status=True).count()
return jsonify({
'success': True,
'data': {
'today': {
'total': today_stats.total or 0,
'success': today_stats.success or 0,
'fail': today_stats.fail or 0
},
'total_users': total_users,
'total_parses': total_parses,
'active_apis': active_apis
}
})
@admin_bp.route('/users', methods=['GET'])
@admin_required
def users_page():
"""用户管理页面"""
from flask import render_template
return render_template('admin_users.html')
@admin_bp.route('/api/users', methods=['GET'])
@admin_required
def get_users():
"""获取用户列表API"""
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 20, type=int)
group_id = request.args.get('group_id', type=int)
# 构建查询
query = User.query
if group_id:
query = query.filter_by(group_id=group_id)
pagination = query.order_by(User.created_at.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
users = [{
'id': u.id,
'username': u.username,
'email': u.email,
'group_id': u.group_id,
'group_name': u.group.name if u.group else '',
'total_parse_count': u.total_parse_count,
'is_active': u.is_active,
'created_at': u.created_at.isoformat()
} for u in pagination.items]
return jsonify({
'success': True,
'data': users,
'pagination': {
'page': page,
'per_page': per_page,
'total': pagination.total,
'pages': pagination.pages
}
})
@admin_bp.route('/api/users/<int:user_id>', methods=['PUT'])
@admin_required
def update_user(user_id):
"""更新用户信息"""
user = User.query.get_or_404(user_id)
data = request.get_json()
if 'group_id' in data:
user.group_id = data['group_id']
if 'is_active' in data:
user.is_active = data['is_active']
db.session.commit()
return jsonify({'success': True, 'message': '更新成功'})
@admin_bp.route('/api/groups', methods=['GET'])
@admin_required
def get_groups():
"""获取用户分组列表"""
groups = UserGroup.query.all()
return jsonify({
'success': True,
'data': [{
'id': g.id,
'name': g.name,
'daily_limit': g.daily_limit,
'description': g.description
} for g in groups]
})
@admin_bp.route('/api/groups/<int:group_id>', methods=['PUT'])
@admin_required
def update_group(group_id):
"""更新用户分组"""
group = UserGroup.query.get_or_404(group_id)
data = request.get_json()
if 'daily_limit' in data:
group.daily_limit = data['daily_limit']
if 'description' in data:
group.description = data['description']
db.session.commit()
return jsonify({'success': True, 'message': '更新成功'})
@admin_bp.route('/apis', methods=['GET'])
@admin_required
def apis_page():
"""接口管理页面"""
from flask import render_template
return render_template('admin_apis.html')
@admin_bp.route('/api/apis', methods=['GET'])
@admin_required
def get_apis():
"""获取解析接口列表API"""
apis = ParserAPI.query.all()
return jsonify({
'success': True,
'data': [{
'id': a.id,
'name': a.name,
'platform': a.platform,
'api_url': a.api_url,
'weight': a.weight,
'is_enabled': a.is_enabled,
'health_status': a.health_status,
'total_calls': a.total_calls,
'success_calls': a.success_calls,
'avg_response_time': a.avg_response_time,
'last_check_at': a.last_check_at.isoformat() if a.last_check_at else None
} for a in apis]
})
@admin_bp.route('/api/apis', methods=['POST'])
@admin_required
def create_api():
"""创建解析接口"""
data = request.get_json()
api = ParserAPI(
name=data['name'],
platform=data['platform'],
api_url=data['api_url'],
api_key=data.get('api_key'),
weight=data.get('weight', 1),
is_enabled=data.get('is_enabled', True)
)
db.session.add(api)
db.session.commit()
return jsonify({'success': True, 'message': '创建成功', 'id': api.id})
@admin_bp.route('/api/apis/<int:api_id>', methods=['PUT'])
@admin_required
def update_api(api_id):
"""更新解析接口"""
api = ParserAPI.query.get_or_404(api_id)
data = request.get_json()
for key in ['name', 'api_url', 'api_key', 'weight', 'is_enabled']:
if key in data:
setattr(api, key, data[key])
db.session.commit()
return jsonify({'success': True, 'message': '更新成功'})
@admin_bp.route('/api/apis/<int:api_id>', methods=['DELETE'])
@admin_required
def delete_api(api_id):
"""删除解析接口"""
api = ParserAPI.query.get_or_404(api_id)
db.session.delete(api)
db.session.commit()
return jsonify({'success': True, 'message': '删除成功'})
@admin_bp.route('/api/apis/<int:api_id>/test', methods=['POST'])
@admin_required
def test_api(api_id):
"""测试解析接口"""
from parsers.factory import ParserFactory
import time
api = ParserAPI.query.get_or_404(api_id)
data = request.get_json()
test_url = data.get('test_url')
if not test_url:
return jsonify({'success': False, 'message': '请提供测试链接'}), 400
try:
parser = ParserFactory.create_parser(api)
start_time = time.time()
result = parser.parse(test_url)
response_time = int((time.time() - start_time) * 1000)
# 更新健康状态和统计
api.health_status = True
api.fail_count = 0
api.total_calls += 1
api.success_calls += 1
api.avg_response_time = int((api.avg_response_time * (api.total_calls - 1) + response_time) / api.total_calls)
api.last_check_at = datetime.utcnow()
db.session.commit()
return jsonify({
'success': True,
'message': '测试成功',
'data': result,
'response_time': response_time
})
except Exception as e:
# 更新失败状态
api.fail_count += 1
api.total_calls += 1
if api.fail_count >= 3:
api.health_status = False
api.last_check_at = datetime.utcnow()
db.session.commit()
return jsonify({'success': False, 'message': f'测试失败: {str(e)}'}), 500
@admin_bp.route('/config', methods=['GET'])
@admin_required
def config_page():
"""站点配置页面"""
from flask import render_template
return render_template('admin_config.html')
@admin_bp.route('/api/config', methods=['GET'])
@admin_required
def get_config():
"""获取站点配置API"""
configs = SiteConfig.query.all()
return jsonify({
'success': True,
'data': {c.config_key: c.config_value for c in configs}
})
@admin_bp.route('/api/config', methods=['PUT'])
@admin_required
def update_config():
"""更新站点配置"""
data = request.get_json()
for key, value in data.items():
config = SiteConfig.query.filter_by(config_key=key).first()
if config:
config.config_value = str(value)
else:
config = SiteConfig(config_key=key, config_value=str(value))
db.session.add(config)
db.session.commit()
return jsonify({'success': True, 'message': '更新成功'})
@admin_bp.route('/smtp', methods=['GET'])
@admin_required
def smtp_page():
"""SMTP配置页面"""
from flask import render_template
return render_template('admin_smtp.html')
@admin_bp.route('/api/smtp', methods=['GET'])
@admin_required
def get_smtp():
"""获取SMTP配置列表"""
smtps = SMTPConfig.query.all()
return jsonify({
'success': True,
'data': [{
'id': s.id,
'name': s.name,
'host': s.host,
'port': s.port,
'username': s.username,
'from_email': s.from_email,
'from_name': s.from_name,
'use_tls': s.use_tls,
'is_enabled': s.is_enabled,
'is_default': s.is_default,
'weight': s.weight,
'send_count': s.send_count,
'fail_count': s.fail_count
} for s in smtps]
})
@admin_bp.route('/api/smtp', methods=['POST'])
@admin_required
def create_smtp():
"""创建SMTP配置"""
data = request.get_json()
smtp = SMTPConfig(
name=data['name'],
host=data['host'],
port=data['port'],
username=data['username'],
password=data['password'],
from_email=data['from_email'],
from_name=data.get('from_name', ''),
use_tls=data.get('use_tls', True),
is_enabled=data.get('is_enabled', True),
weight=data.get('weight', 1)
)
db.session.add(smtp)
db.session.commit()
return jsonify({'success': True, 'message': '创建成功', 'id': smtp.id})
@admin_bp.route('/api/smtp/<int:smtp_id>', methods=['PUT'])
@admin_required
def update_smtp(smtp_id):
"""更新SMTP配置"""
smtp = SMTPConfig.query.get_or_404(smtp_id)
data = request.get_json()
for key in ['name', 'host', 'port', 'username', 'from_email', 'from_name', 'use_tls', 'is_enabled', 'weight']:
if key in data:
setattr(smtp, key, data[key])
# Only update password when user explicitly provides one, so empty edits won't wipe valid credentials.
if 'password' in data and data.get('password'):
smtp.password = data['password']
db.session.commit()
return jsonify({'success': True, 'message': '更新成功'})
@admin_bp.route('/api/smtp/test', methods=['POST'])
@admin_required
def test_smtp():
"""测试SMTP配置"""
data = request.get_json()
email = data.get('email')
if not email:
return jsonify({'success': False, 'message': '请提供测试邮箱'}), 400
try:
EmailService.send_email(
email,
'【短视频解析平台】SMTP测试邮件',
'<h2>测试成功</h2><p>如果您收到此邮件说明SMTP配置正常。</p>',
html=True
)
return jsonify({'success': True, 'message': '测试邮件已发送'})
except Exception as e:
import traceback
error_detail = traceback.format_exc()
print(f"SMTP测试失败详细信息:\n{error_detail}")
# 提供更友好的错误提示
error_msg = str(e)
if 'Connection unexpectedly closed' in error_msg:
error_msg = '连接被服务器关闭请检查1) 端口和加密方式是否匹配 2) QQ邮箱需使用授权码而非密码 3) 用户名是否正确'
elif 'Authentication failed' in error_msg or '535' in error_msg:
error_msg = '认证失败请检查用户名和密码QQ邮箱需使用授权码'
elif 'timed out' in error_msg:
error_msg = '连接超时,请检查网络和服务器地址'
return jsonify({'success': False, 'message': f'邮件发送失败: {error_msg}'}), 500
@admin_bp.route('/api/stats/parse', methods=['GET'])
@admin_required
def get_parse_stats():
"""获取解析统计"""
days = request.args.get('days', 7, type=int)
start_date = date.today() - timedelta(days=days-1)
stats = db.session.query(
DailyParseStat.date,
func.sum(DailyParseStat.parse_count).label('total'),
func.sum(DailyParseStat.success_count).label('success'),
func.sum(DailyParseStat.fail_count).label('fail')
).filter(DailyParseStat.date >= start_date).group_by(DailyParseStat.date).all()
return jsonify({
'success': True,
'data': [{
'date': s.date.isoformat(),
'total': s.total or 0,
'success': s.success or 0,
'fail': s.fail or 0
} for s in stats]
})
@admin_bp.route('/api/stats/platform', methods=['GET'])
@admin_required
def get_platform_stats():
"""获取平台统计"""
stats = db.session.query(
ParseLog.platform,
func.count(ParseLog.id).label('count')
).group_by(ParseLog.platform).all()
return jsonify({
'success': True,
'data': [{
'platform': s.platform,
'count': s.count
} for s in stats]
})
@admin_bp.route('/api/2fa/enable', methods=['POST'])
@admin_required
def enable_2fa():
"""启用2FA"""
admin_id = session.get('admin_id')
admin = Admin.query.get(admin_id)
if admin.is_2fa_enabled:
return jsonify({'success': False, 'message': '2FA已启用'}), 400
# 生成密钥
secret = generate_2fa_secret()
qr_url = get_2fa_qrcode_url(admin, secret)
# 生成二维码
qr = qrcode.QRCode(version=1, box_size=10, border=5)
qr.add_data(qr_url)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
# 转换为base64
buffer = io.BytesIO()
img.save(buffer, format='PNG')
img_str = base64.b64encode(buffer.getvalue()).decode()
# 临时保存密钥(需要验证后才正式启用)
session['temp_2fa_secret'] = secret
return jsonify({
'success': True,
'secret': secret,
'qr_code': f'data:image/png;base64,{img_str}'
})
@admin_bp.route('/api/2fa/verify', methods=['POST'])
@admin_required
def verify_2fa_setup():
"""验证并启用2FA"""
admin_id = session.get('admin_id')
admin = Admin.query.get(admin_id)
data = request.get_json()
code = data.get('code')
secret = session.get('temp_2fa_secret')
if not secret:
return jsonify({'success': False, 'message': '请先生成2FA密钥'}), 400
# 验证代码
import pyotp
totp = pyotp.TOTP(secret)
if not totp.verify(code, valid_window=1):
return jsonify({'success': False, 'message': '验证码错误'}), 400
# 启用2FA
admin.totp_secret = secret
admin.is_2fa_enabled = True
db.session.commit()
session.pop('temp_2fa_secret', None)
return jsonify({'success': True, 'message': '2FA已启用'})
@admin_bp.route('/api/2fa/disable', methods=['POST'])
@admin_required
def disable_2fa():
"""禁用2FA"""
admin_id = session.get('admin_id')
admin = Admin.query.get(admin_id)
data = request.get_json()
code = data.get('code')
if not admin.is_2fa_enabled:
return jsonify({'success': False, 'message': '2FA未启用'}), 400
# 验证代码
if not verify_2fa(admin, code):
return jsonify({'success': False, 'message': '验证码错误'}), 401
# 禁用2FA
admin.is_2fa_enabled = False
admin.totp_secret = None
db.session.commit()
return jsonify({'success': True, 'message': '2FA已禁用'})
@admin_bp.route('/api/smtp/<int:smtp_id>', methods=['DELETE'])
@admin_required
def delete_smtp(smtp_id):
"""删除SMTP配置"""
smtp = SMTPConfig.query.get_or_404(smtp_id)
db.session.delete(smtp)
db.session.commit()
return jsonify({'success': True, 'message': '删除成功'})
@admin_bp.route('/api/smtp/<int:smtp_id>/set-default', methods=['POST'])
@admin_required
def set_default_smtp(smtp_id):
"""设置默认SMTP"""
SMTPConfig.query.update({'is_default': False})
smtp = SMTPConfig.query.get_or_404(smtp_id)
smtp.is_default = True
db.session.commit()
return jsonify({'success': True, 'message': '已设置为默认SMTP'})
@admin_bp.route('/logs', methods=['GET'])
@admin_required
def logs_page():
"""解析日志页面"""
from flask import render_template
return render_template('admin_logs.html')
@admin_bp.route('/api/logs', methods=['GET'])
@admin_required
def get_logs():
"""获取解析日志"""
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 50, type=int)
platform = request.args.get('platform')
status = request.args.get('status')
query = ParseLog.query
if platform:
query = query.filter_by(platform=platform)
if status:
query = query.filter_by(status=status)
pagination = query.order_by(ParseLog.created_at.desc()).paginate(
page=page, per_page=per_page, error_out=False
)
logs = [{
'id': log.id,
'user_id': log.user_id,
'ip_address': log.ip_address,
'platform': log.platform,
'video_url': log.video_url,
'status': log.status,
'error_message': log.error_message,
'response_time': log.response_time,
'created_at': log.created_at.isoformat()
} for log in pagination.items]
return jsonify({
'success': True,
'data': logs,
'pagination': {
'page': page,
'per_page': per_page,
'total': pagination.total,
'pages': pagination.pages
}
})
@admin_bp.route('/health-checks', methods=['GET'])
@admin_required
def health_checks_page():
"""健康检查配置页面"""
from flask import render_template
return render_template('admin_health_checks.html')
@admin_bp.route('/api/health-checks', methods=['GET'])
@admin_required
def get_health_checks():
"""获取健康检查配置"""
configs = HealthCheckConfig.query.all()
return jsonify({
'success': True,
'data': [{
'id': c.id,
'platform': c.platform,
'test_url': c.test_url,
'check_interval': c.check_interval,
'is_enabled': c.is_enabled,
'alert_email': c.alert_email
} for c in configs]
})
@admin_bp.route('/api/health-checks', methods=['POST'])
@admin_required
def create_health_check():
"""创建健康检查配置"""
data = request.get_json()
config = HealthCheckConfig(
platform=data['platform'],
test_url=data['test_url'],
check_interval=data.get('check_interval', 300),
is_enabled=data.get('is_enabled', True),
alert_email=data.get('alert_email')
)
db.session.add(config)
db.session.commit()
return jsonify({'success': True, 'message': '创建成功', 'id': config.id})
@admin_bp.route('/api/health-checks/<int:config_id>', methods=['PUT'])
@admin_required
def update_health_check(config_id):
"""更新健康检查配置"""
config = HealthCheckConfig.query.get_or_404(config_id)
data = request.get_json()
for key in ['test_url', 'check_interval', 'is_enabled', 'alert_email']:
if key in data:
setattr(config, key, data[key])
db.session.commit()
return jsonify({'success': True, 'message': '更新成功'})
@admin_bp.route('/api/health-checks/<int:config_id>', methods=['DELETE'])
@admin_required
def delete_health_check(config_id):
"""删除健康检查配置"""
config = HealthCheckConfig.query.get_or_404(config_id)
db.session.delete(config)
db.session.commit()
return jsonify({'success': True, 'message': '删除成功'})
@admin_bp.route('/profile', methods=['GET'])
@admin_required
def profile_page():
"""账号管理页面"""
from flask import render_template
return render_template('admin_profile.html')
@admin_bp.route('/api/profile', methods=['GET'])
@admin_required
def get_profile():
"""获取管理员信息"""
admin_id = session.get('admin_id')
admin = Admin.query.get(admin_id)
return jsonify({
'success': True,
'data': {
'id': admin.id,
'username': admin.username,
'email': admin.email,
'is_2fa_enabled': admin.is_2fa_enabled,
'last_login_ip': admin.last_login_ip,
'last_login_at': admin.last_login_at.isoformat() if admin.last_login_at else None
}
})
@admin_bp.route('/api/profile/password', methods=['PUT'])
@admin_required
def change_password():
"""修改管理员密码"""
admin_id = session.get('admin_id')
admin = Admin.query.get(admin_id)
data = request.get_json()
old_password = data.get('old_password')
new_password = data.get('new_password')
if not all([old_password, new_password]):
return jsonify({'success': False, 'message': '请填写完整信息'}), 400
if not verify_password(old_password, admin.password):
return jsonify({'success': False, 'message': '原密码错误'}), 401
admin.password = hash_password(new_password)
db.session.commit()
return jsonify({'success': True, 'message': '密码修改成功'})
@admin_bp.route('/api/profile/email', methods=['PUT'])
@admin_required
def change_email():
"""修改管理员邮箱"""
admin_id = session.get('admin_id')
admin = Admin.query.get(admin_id)
data = request.get_json()
email = data.get('email')
if not email:
return jsonify({'success': False, 'message': '请提供邮箱地址'}), 400
admin.email = email
db.session.commit()
return jsonify({'success': True, 'message': '邮箱修改成功'})

276
routes/auth.py Normal file
View File

@@ -0,0 +1,276 @@
from flask import Blueprint, request, jsonify, session
from flask_login import login_user, logout_user, login_required, current_user
from models import User, EmailVerification
from models import db
from utils.security import hash_password, verify_password, generate_verification_code, get_client_ip
from utils.email import EmailService
from datetime import datetime, timedelta
import re
auth_bp = Blueprint('auth', __name__)
def validate_email(email):
"""验证邮箱格式"""
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
@auth_bp.route('/send-code', methods=['POST'])
def send_verification_code():
"""发送验证码"""
data = request.get_json()
email = data.get('email')
purpose = data.get('purpose', 'register')
if not email or not validate_email(email):
return jsonify({'success': False, 'message': '邮箱格式不正确'}), 400
# 检查用户是否已存在
if purpose == 'register':
if User.query.filter_by(email=email).first():
return jsonify({'success': False, 'message': '该邮箱已被注册'}), 400
elif purpose in ['reset_password', 'forgot_password']:
if not User.query.filter_by(email=email).first():
return jsonify({'success': False, 'message': '该邮箱未注册'}), 400
# 生成验证码
code = generate_verification_code(6)
expires_at = datetime.utcnow() + timedelta(minutes=10)
# 保存验证码
verification = EmailVerification(
email=email,
code=code,
purpose=purpose,
expires_at=expires_at
)
db.session.add(verification)
db.session.commit()
# 发送邮件
try:
EmailService.send_verification_code(email, code, purpose)
return jsonify({'success': True, 'message': '验证码已发送'})
except Exception as e:
return jsonify({'success': False, 'message': str(e)}), 500
@auth_bp.route('/register', methods=['GET', 'POST'])
def register():
"""用户注册"""
if request.method == 'GET':
from flask import render_template
return render_template('register.html')
data = request.get_json()
username = data.get('username')
email = data.get('email')
password = data.get('password')
code = data.get('code')
# 验证输入
if not all([username, email, password, code]):
return jsonify({'success': False, 'message': '请填写完整信息'}), 400
if not validate_email(email):
return jsonify({'success': False, 'message': '邮箱格式不正确'}), 400
if len(password) < 6:
return jsonify({'success': False, 'message': '密码长度至少6位'}), 400
# 检查用户名和邮箱是否已存在
if User.query.filter_by(username=username).first():
return jsonify({'success': False, 'message': '用户名已存在'}), 400
if User.query.filter_by(email=email).first():
return jsonify({'success': False, 'message': '邮箱已被注册'}), 400
# 验证验证码
verification = EmailVerification.query.filter_by(
email=email,
code=code,
purpose='register',
is_used=False
).filter(EmailVerification.expires_at > datetime.utcnow()).first()
if not verification:
return jsonify({'success': False, 'message': '验证码无效或已过期'}), 400
# 创建用户
user = User(
username=username,
email=email,
password=hash_password(password),
register_ip=get_client_ip(request),
group_id=2 # 默认普通用户
)
db.session.add(user)
# 标记验证码已使用
verification.is_used = True
db.session.commit()
return jsonify({'success': True, 'message': '注册成功'})
@auth_bp.route('/login', methods=['GET', 'POST'])
def login():
"""用户登录"""
if request.method == 'GET':
from flask import render_template
return render_template('login.html')
data = request.get_json()
email = data.get('email')
password = data.get('password')
if not all([email, password]):
return jsonify({'success': False, 'message': '请填写完整信息'}), 400
user = User.query.filter_by(email=email).first()
if not user or not verify_password(password, user.password):
return jsonify({'success': False, 'message': '邮箱或密码错误'}), 401
if not user.is_active:
return jsonify({'success': False, 'message': '账号已被禁用'}), 403
# 更新登录信息
user.last_login_ip = get_client_ip(request)
db.session.commit()
# 登录用户
login_user(user)
return jsonify({
'success': True,
'message': '登录成功',
'user': {
'id': user.id,
'username': user.username,
'email': user.email
}
})
@auth_bp.route('/logout', methods=['POST'])
@login_required
def logout():
"""用户登出"""
logout_user()
return jsonify({'success': True, 'message': '已退出登录'})
@auth_bp.route('/reset-password', methods=['POST'])
def reset_password():
"""重置密码"""
data = request.get_json()
email = data.get('email')
code = data.get('code')
new_password = data.get('new_password')
if not all([email, code, new_password]):
return jsonify({'success': False, 'message': '请填写完整信息'}), 400
if len(new_password) < 6:
return jsonify({'success': False, 'message': '密码长度至少6位'}), 400
# 验证验证码
verification = EmailVerification.query.filter_by(
email=email,
code=code,
is_used=False
).filter(
EmailVerification.purpose.in_(['reset_password', 'forgot_password'])
).filter(EmailVerification.expires_at > datetime.utcnow()).first()
if not verification:
return jsonify({'success': False, 'message': '验证码无效或已过期'}), 400
# 更新密码
user = User.query.filter_by(email=email).first()
if not user:
return jsonify({'success': False, 'message': '用户不存在'}), 404
user.password = hash_password(new_password)
verification.is_used = True
db.session.commit()
return jsonify({'success': True, 'message': '密码重置成功'})
@auth_bp.route('/user-info', methods=['GET'])
@login_required
def user_info():
"""获取当前用户信息"""
return jsonify({
'success': True,
'user': {
'id': current_user.id,
'username': current_user.username,
'email': current_user.email,
'group_id': current_user.group_id,
'total_parse_count': current_user.total_parse_count
}
})
@auth_bp.route('/profile', methods=['GET'])
@login_required
def profile():
"""用户个人中心页面"""
from flask import render_template
return render_template('profile.html')
@auth_bp.route('/api/profile', methods=['GET'])
@login_required
def get_profile():
"""获取用户个人中心数据"""
from models import UserGroup, DailyParseStat, ParseLog
from datetime import date
# 获取用户组信息
user_group = UserGroup.query.get(current_user.group_id)
daily_limit = user_group.daily_limit if user_group else 10
group_name = user_group.name if user_group else '普通用户'
# 获取今日使用次数
today = date.today()
today_stat = DailyParseStat.query.filter_by(
user_id=current_user.id,
date=today
).first()
today_used = today_stat.parse_count if today_stat else 0
today_remaining = max(0, daily_limit - today_used)
# 获取解析记录最近20条
parse_logs = ParseLog.query.filter_by(
user_id=current_user.id
).order_by(ParseLog.created_at.desc()).limit(20).all()
logs_data = [{
'id': log.id,
'platform': log.platform,
'video_url': log.video_url[:50] + '...' if len(log.video_url) > 50 else log.video_url,
'status': log.status,
'response_time': log.response_time,
'created_at': log.created_at.strftime('%Y-%m-%d %H:%M:%S') if log.created_at else ''
} for log in parse_logs]
return jsonify({
'success': True,
'data': {
'user': {
'id': current_user.id,
'username': current_user.username,
'email': current_user.email,
'created_at': current_user.created_at.strftime('%Y-%m-%d') if current_user.created_at else ''
},
'group': {
'id': current_user.group_id,
'name': group_name,
'daily_limit': daily_limit
},
'usage': {
'daily_limit': daily_limit,
'today_used': today_used,
'today_remaining': today_remaining,
'total_parse_count': current_user.total_parse_count or 0
},
'parse_logs': logs_data
}
})

26
routes/main.py Normal file
View File

@@ -0,0 +1,26 @@
from flask import Blueprint, render_template, redirect
main_bp = Blueprint('main', __name__)
@main_bp.route('/')
def index():
from models import SiteConfig
config = {}
configs = SiteConfig.query.all()
for c in configs:
config[c.config_key] = c.config_value
return render_template('index.html', config=config)
@main_bp.route('/favicon.ico')
def favicon():
"""处理 favicon 请求"""
from models import SiteConfig
favicon_config = SiteConfig.query.filter_by(config_key='site_favicon').first()
if favicon_config and favicon_config.config_value:
return redirect(favicon_config.config_value)
# 如果没有设置 favicon尝试使用 logo
logo_config = SiteConfig.query.filter_by(config_key='site_logo').first()
if logo_config and logo_config.config_value:
return redirect(logo_config.config_value)
# 返回空响应避免 404
return '', 204

202
routes/parser.py Normal file
View File

@@ -0,0 +1,202 @@
from flask import Blueprint, request, jsonify
from flask_login import current_user
from models import ParseLog
from models import db
from utils.security import get_client_ip
from utils.limiter import RateLimiter
from utils.queue import ParseQueue, ConcurrencyController
from parsers.factory import ParserFactory
import uuid
import time
from datetime import datetime
parser_bp = Blueprint('parser', __name__)
@parser_bp.route('/parse', methods=['POST'])
def parse_video():
"""解析视频"""
data = request.get_json()
video_url = data.get('url')
if not video_url:
return jsonify({'success': False, 'message': '请提供视频链接'}), 400
# 获取用户信息
user_id = current_user.id if current_user.is_authenticated else None
ip_address = get_client_ip(request)
# 检查限流
limit_check = RateLimiter.check_limit(user_id=user_id, ip_address=ip_address)
if not limit_check['allowed']:
return jsonify({
'success': False,
'message': f"今日解析次数已达上限({limit_check['limit']}次)",
'limit_info': limit_check
}), 429
# 检测平台
try:
platform = ParserFactory.detect_platform(video_url)
except ValueError as e:
return jsonify({'success': False, 'message': str(e)}), 400
# 生成任务ID
task_id = str(uuid.uuid4())
# 添加到队列
ParseQueue.add_task(task_id, video_url, user_id, ip_address)
# 尝试立即处理
if ConcurrencyController.can_process():
result = _process_task(task_id, video_url, platform, user_id, ip_address)
return jsonify(result)
else:
# 返回任务ID让前端轮询
return jsonify({
'success': True,
'status': 'queued',
'task_id': task_id,
'message': '任务已加入队列,请稍候...',
'queue_status': ParseQueue.get_queue_status()
})
@parser_bp.route('/task/<task_id>', methods=['GET'])
def get_task_result(task_id):
"""获取任务结果"""
result = ParseQueue.get_result(task_id)
if result:
return jsonify(result)
else:
# 检查是否还在队列中
queue_status = ParseQueue.get_queue_status()
return jsonify({
'success': False,
'status': 'processing',
'message': '任务处理中...',
'queue_status': queue_status
})
@parser_bp.route('/queue-status', methods=['GET'])
def queue_status():
"""获取队列状态"""
status = ParseQueue.get_queue_status()
return jsonify({
'success': True,
'queue_status': status
})
def _process_task(task_id, video_url, platform, user_id, ip_address):
"""处理解析任务"""
start_time = time.time()
# 获取该平台所有可用的API
from models import ParserAPI
available_apis = ParserAPI.query.filter_by(
platform=platform.lower(),
is_enabled=True
).all()
if not available_apis:
return {
'success': False,
'status': 'failed',
'message': f'没有可用的{platform}解析接口',
'response_time': int((time.time() - start_time) * 1000)
}
last_error = None
# 尝试所有可用的API
for api_config in available_apis:
try:
# 创建解析器
parser = ParserFactory.create_parser(api_config)
# 执行解析
result = parser.parse(video_url)
# 计算响应时间
response_time = int((time.time() - start_time) * 1000)
# 记录日志
log = ParseLog(
user_id=user_id,
ip_address=ip_address,
platform=platform,
video_url=video_url,
parser_api_id=api_config.id,
status='success',
response_time=response_time
)
db.session.add(log)
# 更新API统计
api_config.total_calls += 1
api_config.success_calls += 1
api_config.avg_response_time = int(
(api_config.avg_response_time * (api_config.total_calls - 1) + response_time) / api_config.total_calls
)
api_config.fail_count = 0 # 重置失败计数
# 更新用户统计
if user_id:
from models import User
user = User.query.get(user_id)
user.total_parse_count += 1
# 更新限流计数
RateLimiter.increment_count(user_id=user_id, ip_address=ip_address, success=True)
db.session.commit()
# 保存结果
response = {
'success': True,
'status': 'completed',
'data': result,
'response_time': response_time
}
ParseQueue.complete_task(task_id, response)
return response
except Exception as e:
# 记录失败继续尝试下一个API
last_error = str(e)
api_config.total_calls += 1
api_config.fail_count += 1
db.session.commit()
continue
# 所有API都失败了
# 计算响应时间
response_time = int((time.time() - start_time) * 1000)
# 记录失败日志
log = ParseLog(
user_id=user_id,
ip_address=ip_address,
platform=platform,
video_url=video_url,
status='failed',
error_message=last_error or '所有接口都失败',
response_time=response_time
)
db.session.add(log)
# 更新限流计数(失败也计数)
RateLimiter.increment_count(user_id=user_id, ip_address=ip_address, success=False)
db.session.commit()
# 保存错误结果
response = {
'success': False,
'status': 'failed',
'message': last_error or '所有解析接口都失败',
'response_time': response_time
}
ParseQueue.complete_task(task_id, response)
return response