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

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': '邮箱修改成功'})