feat: 新增平台
This commit is contained in:
286
routes/admin.py
286
routes/admin.py
@@ -113,6 +113,8 @@ def users_page():
|
||||
@admin_required
|
||||
def get_users():
|
||||
"""获取用户列表API"""
|
||||
from models import UserGroupExpiry
|
||||
|
||||
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)
|
||||
@@ -126,16 +128,28 @@ def get_users():
|
||||
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]
|
||||
users = []
|
||||
for u in pagination.items:
|
||||
# 获取套餐到期时间
|
||||
expiry = UserGroupExpiry.query.filter_by(user_id=u.id).first()
|
||||
expires_at = None
|
||||
is_expired = False
|
||||
if expiry and expiry.expires_at:
|
||||
expires_at = expiry.expires_at.strftime('%Y-%m-%dT%H:%M')
|
||||
is_expired = expiry.expires_at < datetime.utcnow()
|
||||
|
||||
users.append({
|
||||
'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(),
|
||||
'expires_at': expires_at,
|
||||
'is_expired': is_expired
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
@@ -152,6 +166,8 @@ def get_users():
|
||||
@admin_required
|
||||
def update_user(user_id):
|
||||
"""更新用户信息"""
|
||||
from models import UserGroupExpiry
|
||||
|
||||
user = User.query.get_or_404(user_id)
|
||||
data = request.get_json()
|
||||
|
||||
@@ -160,6 +176,29 @@ def update_user(user_id):
|
||||
if 'is_active' in data:
|
||||
user.is_active = data['is_active']
|
||||
|
||||
# 处理套餐到期时间
|
||||
if 'expires_at' in data:
|
||||
expiry = UserGroupExpiry.query.filter_by(user_id=user_id).first()
|
||||
|
||||
if data['expires_at']:
|
||||
# 解析时间字符串
|
||||
expires_at = datetime.strptime(data['expires_at'], '%Y-%m-%dT%H:%M')
|
||||
|
||||
if expiry:
|
||||
expiry.group_id = user.group_id
|
||||
expiry.expires_at = expires_at
|
||||
else:
|
||||
expiry = UserGroupExpiry(
|
||||
user_id=user_id,
|
||||
group_id=user.group_id,
|
||||
expires_at=expires_at
|
||||
)
|
||||
db.session.add(expiry)
|
||||
else:
|
||||
# 清空到期时间(游客/普通用户)
|
||||
if expiry:
|
||||
db.session.delete(expiry)
|
||||
|
||||
db.session.commit()
|
||||
return jsonify({'success': True, 'message': '更新成功'})
|
||||
|
||||
@@ -766,3 +805,230 @@ def change_email():
|
||||
admin.email = email
|
||||
db.session.commit()
|
||||
return jsonify({'success': True, 'message': '邮箱修改成功'})
|
||||
|
||||
|
||||
# ==================== 用户组 API ====================
|
||||
|
||||
@admin_bp.route('/api/user-groups', methods=['GET'])
|
||||
@admin_required
|
||||
def get_user_groups():
|
||||
"""获取用户组列表(仅返回可兑换的套餐,排除游客和普通用户)"""
|
||||
# 排除游客(id=1)和普通用户(id=2)
|
||||
groups = UserGroup.query.filter(UserGroup.id > 2).all()
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': [{'id': g.id, 'name': g.name, 'daily_limit': g.daily_limit} for g in groups]
|
||||
})
|
||||
|
||||
|
||||
# ==================== 兑换码管理 ====================
|
||||
|
||||
@admin_bp.route('/redeem-codes')
|
||||
@admin_required
|
||||
def redeem_codes_page():
|
||||
"""兑换码管理页面"""
|
||||
from flask import render_template
|
||||
return render_template('admin_redeem_codes.html')
|
||||
|
||||
|
||||
@admin_bp.route('/api/redeem-codes', methods=['GET'])
|
||||
@admin_required
|
||||
def get_redeem_codes():
|
||||
"""获取兑换码列表"""
|
||||
from models import RedeemCode
|
||||
|
||||
page = request.args.get('page', 1, type=int)
|
||||
per_page = request.args.get('per_page', 20, type=int)
|
||||
batch_id = request.args.get('batch_id', '')
|
||||
status = request.args.get('status', '') # unused, used, expired
|
||||
|
||||
query = RedeemCode.query
|
||||
|
||||
if batch_id:
|
||||
query = query.filter(RedeemCode.batch_id == batch_id)
|
||||
|
||||
if status == 'unused':
|
||||
query = query.filter(RedeemCode.is_used == False)
|
||||
elif status == 'used':
|
||||
query = query.filter(RedeemCode.is_used == True)
|
||||
elif status == 'expired':
|
||||
query = query.filter(RedeemCode.is_used == False, RedeemCode.expires_at < datetime.utcnow())
|
||||
|
||||
pagination = query.order_by(RedeemCode.created_at.desc()).paginate(page=page, per_page=per_page)
|
||||
|
||||
codes = []
|
||||
for code in pagination.items:
|
||||
codes.append({
|
||||
'id': code.id,
|
||||
'code': code.code,
|
||||
'batch_id': code.batch_id,
|
||||
'target_group': code.target_group.name if code.target_group else '',
|
||||
'target_group_id': code.target_group_id,
|
||||
'duration_days': code.duration_days,
|
||||
'is_used': code.is_used,
|
||||
'used_by': code.user.username if code.user else None,
|
||||
'used_at': code.used_at.strftime('%Y-%m-%d %H:%M') if code.used_at else None,
|
||||
'expires_at': code.expires_at.strftime('%Y-%m-%d %H:%M') if code.expires_at else None,
|
||||
'is_expired': code.expires_at and code.expires_at < datetime.utcnow() and not code.is_used,
|
||||
'remark': code.remark,
|
||||
'created_at': code.created_at.strftime('%Y-%m-%d %H:%M')
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': codes,
|
||||
'pagination': {
|
||||
'page': page,
|
||||
'per_page': per_page,
|
||||
'total': pagination.total,
|
||||
'pages': pagination.pages
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@admin_bp.route('/api/redeem-codes/generate', methods=['POST'])
|
||||
@admin_required
|
||||
def generate_redeem_codes():
|
||||
"""批量生成兑换码"""
|
||||
from models import RedeemCode
|
||||
import secrets
|
||||
import string
|
||||
|
||||
data = request.get_json()
|
||||
count = data.get('count', 1)
|
||||
target_group_id = data.get('target_group_id')
|
||||
duration_days = data.get('duration_days', 30)
|
||||
expires_days = data.get('expires_days') # 兑换码有效期(天)
|
||||
remark = data.get('remark', '')
|
||||
prefix = data.get('prefix', '') # 兑换码前缀
|
||||
|
||||
if not target_group_id:
|
||||
return jsonify({'success': False, 'message': '请选择目标用户组'}), 400
|
||||
|
||||
if count < 1 or count > 1000:
|
||||
return jsonify({'success': False, 'message': '生成数量必须在1-1000之间'}), 400
|
||||
|
||||
# 验证用户组存在
|
||||
group = UserGroup.query.get(target_group_id)
|
||||
if not group:
|
||||
return jsonify({'success': False, 'message': '用户组不存在'}), 400
|
||||
|
||||
# 生成批次ID
|
||||
batch_id = datetime.now().strftime('%Y%m%d%H%M%S') + secrets.token_hex(4)
|
||||
|
||||
# 计算过期时间
|
||||
expires_at = None
|
||||
if expires_days:
|
||||
expires_at = datetime.utcnow() + timedelta(days=expires_days)
|
||||
|
||||
# 生成兑换码
|
||||
codes = []
|
||||
chars = string.ascii_uppercase + string.digits
|
||||
|
||||
for _ in range(count):
|
||||
# 生成随机码
|
||||
random_part = ''.join(secrets.choice(chars) for _ in range(12))
|
||||
code_str = f"{prefix}{random_part}" if prefix else random_part
|
||||
|
||||
code = RedeemCode(
|
||||
code=code_str,
|
||||
batch_id=batch_id,
|
||||
target_group_id=target_group_id,
|
||||
duration_days=duration_days,
|
||||
expires_at=expires_at,
|
||||
remark=remark
|
||||
)
|
||||
db.session.add(code)
|
||||
codes.append(code_str)
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'成功生成 {count} 个兑换码',
|
||||
'data': {
|
||||
'batch_id': batch_id,
|
||||
'codes': codes
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@admin_bp.route('/api/redeem-codes/<int:code_id>', methods=['DELETE'])
|
||||
@admin_required
|
||||
def delete_redeem_code(code_id):
|
||||
"""删除兑换码"""
|
||||
from models import RedeemCode
|
||||
|
||||
code = RedeemCode.query.get(code_id)
|
||||
if not code:
|
||||
return jsonify({'success': False, 'message': '兑换码不存在'}), 404
|
||||
|
||||
if code.is_used:
|
||||
return jsonify({'success': False, 'message': '已使用的兑换码不能删除'}), 400
|
||||
|
||||
db.session.delete(code)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'message': '删除成功'})
|
||||
|
||||
|
||||
@admin_bp.route('/api/redeem-codes/batch/<batch_id>', methods=['DELETE'])
|
||||
@admin_required
|
||||
def delete_batch_codes(batch_id):
|
||||
"""删除整批兑换码"""
|
||||
from models import RedeemCode
|
||||
|
||||
# 只删除未使用的
|
||||
deleted = RedeemCode.query.filter_by(batch_id=batch_id, is_used=False).delete()
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({'success': True, 'message': f'成功删除 {deleted} 个未使用的兑换码'})
|
||||
|
||||
|
||||
@admin_bp.route('/api/redeem-codes/batches', methods=['GET'])
|
||||
@admin_required
|
||||
def get_batch_list():
|
||||
"""获取批次列表"""
|
||||
from models import RedeemCode
|
||||
|
||||
batches = db.session.query(
|
||||
RedeemCode.batch_id,
|
||||
func.count(RedeemCode.id).label('total'),
|
||||
func.sum(db.case((RedeemCode.is_used == True, 1), else_=0)).label('used'),
|
||||
func.min(RedeemCode.created_at).label('created_at')
|
||||
).group_by(RedeemCode.batch_id).order_by(func.min(RedeemCode.created_at).desc()).all()
|
||||
|
||||
result = []
|
||||
for batch in batches:
|
||||
result.append({
|
||||
'batch_id': batch.batch_id,
|
||||
'total': batch.total,
|
||||
'used': int(batch.used or 0),
|
||||
'unused': batch.total - int(batch.used or 0),
|
||||
'created_at': batch.created_at.strftime('%Y-%m-%d %H:%M') if batch.created_at else ''
|
||||
})
|
||||
|
||||
return jsonify({'success': True, 'data': result})
|
||||
|
||||
|
||||
@admin_bp.route('/api/redeem-codes/export/<batch_id>', methods=['GET'])
|
||||
@admin_required
|
||||
def export_batch_codes(batch_id):
|
||||
"""导出批次兑换码"""
|
||||
from models import RedeemCode
|
||||
|
||||
codes = RedeemCode.query.filter_by(batch_id=batch_id).all()
|
||||
|
||||
if not codes:
|
||||
return jsonify({'success': False, 'message': '批次不存在'}), 404
|
||||
|
||||
code_list = [code.code for code in codes]
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'batch_id': batch_id,
|
||||
'codes': code_list,
|
||||
'total': len(code_list)
|
||||
}
|
||||
})
|
||||
|
||||
212
routes/api_v1.py
Normal file
212
routes/api_v1.py
Normal file
@@ -0,0 +1,212 @@
|
||||
from flask import Blueprint, request, jsonify
|
||||
from models import ParserAPI, ParseLog, UserApiKey, ApiKeyDailyStat, db
|
||||
from parsers.factory import ParserFactory
|
||||
from utils.security import get_client_ip
|
||||
from datetime import datetime, date
|
||||
import time
|
||||
|
||||
api_v1_bp = Blueprint('api_v1', __name__, url_prefix='/api/v1')
|
||||
|
||||
|
||||
def validate_and_get_key(api_key):
|
||||
"""验证 API Key 并返回 key 对象"""
|
||||
if not api_key:
|
||||
return None, 'API Key 不能为空'
|
||||
|
||||
key_obj = UserApiKey.query.filter_by(api_key=api_key).first()
|
||||
if not key_obj:
|
||||
return None, 'API Key 无效'
|
||||
if not key_obj.is_active:
|
||||
return None, 'API Key 已被禁用'
|
||||
if not key_obj.user.is_active:
|
||||
return None, '用户账号已被禁用'
|
||||
|
||||
# 检查每日限额
|
||||
today_stat = ApiKeyDailyStat.query.filter_by(
|
||||
api_key_id=key_obj.id,
|
||||
date=date.today()
|
||||
).first()
|
||||
if today_stat and today_stat.call_count >= key_obj.daily_limit:
|
||||
return None, f'已达到每日调用限额({key_obj.daily_limit}次)'
|
||||
|
||||
return key_obj, None
|
||||
|
||||
|
||||
def record_api_call(key_obj, ip_address, success=True):
|
||||
"""记录 API 调用"""
|
||||
key_obj.total_calls = (key_obj.total_calls or 0) + 1
|
||||
key_obj.last_used_at = datetime.utcnow()
|
||||
key_obj.last_used_ip = ip_address
|
||||
|
||||
today_stat = ApiKeyDailyStat.query.filter_by(
|
||||
api_key_id=key_obj.id,
|
||||
date=date.today()
|
||||
).first()
|
||||
|
||||
if not today_stat:
|
||||
today_stat = ApiKeyDailyStat(api_key_id=key_obj.id, date=date.today())
|
||||
db.session.add(today_stat)
|
||||
|
||||
today_stat.call_count = (today_stat.call_count or 0) + 1
|
||||
if success:
|
||||
today_stat.success_count = (today_stat.success_count or 0) + 1
|
||||
else:
|
||||
today_stat.fail_count = (today_stat.fail_count or 0) + 1
|
||||
|
||||
|
||||
@api_v1_bp.route('/parse', methods=['GET'])
|
||||
def parse_video():
|
||||
"""
|
||||
对外解析API - 简化版
|
||||
|
||||
请求方式: GET
|
||||
参数:
|
||||
key: API Key
|
||||
url: 视频链接
|
||||
|
||||
示例: /api/v1/parse?key=sk_xxx&url=https://v.douyin.com/xxx
|
||||
|
||||
返回:
|
||||
{
|
||||
"code": 200,
|
||||
"msg": "解析成功",
|
||||
"data": {
|
||||
"cover": "封面URL",
|
||||
"title": "标题",
|
||||
"description": "简介",
|
||||
"author": "作者",
|
||||
"video_url": "无水印视频链接"
|
||||
}
|
||||
}
|
||||
"""
|
||||
# 获取参数
|
||||
api_key = request.args.get('key')
|
||||
video_url = request.args.get('url')
|
||||
|
||||
# 验证 API Key
|
||||
key_obj, error = validate_and_get_key(api_key)
|
||||
if error:
|
||||
return jsonify({'code': 401, 'msg': error})
|
||||
|
||||
# 验证 URL
|
||||
if not video_url:
|
||||
return jsonify({'code': 400, 'msg': '请提供视频链接'})
|
||||
|
||||
# 检测平台
|
||||
try:
|
||||
platform = ParserFactory.detect_platform(video_url)
|
||||
except ValueError as e:
|
||||
return jsonify({'code': 400, 'msg': str(e)})
|
||||
|
||||
# 展开短链接
|
||||
video_url = ParserFactory.expand_short_url(video_url)
|
||||
|
||||
# 获取客户端IP
|
||||
ip_address = get_client_ip(request)
|
||||
start_time = time.time()
|
||||
|
||||
# 获取该平台所有可用的API
|
||||
available_apis = ParserAPI.query.filter_by(
|
||||
platform=platform.lower(),
|
||||
is_enabled=True
|
||||
).all()
|
||||
|
||||
if not available_apis:
|
||||
return jsonify({'code': 503, 'msg': f'没有可用的{platform}解析接口'})
|
||||
|
||||
last_error = None
|
||||
user_id = key_obj.user_id
|
||||
|
||||
# 尝试所有可用的API(failover机制)
|
||||
for api_config in available_apis:
|
||||
try:
|
||||
parser = ParserFactory.create_parser(api_config)
|
||||
result = parser.parse(video_url)
|
||||
|
||||
# 验证解析结果,video_url 不能为空
|
||||
if not result.get('video_url'):
|
||||
raise Exception('未能获取到视频链接')
|
||||
|
||||
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 = (api_config.total_calls or 0) + 1
|
||||
api_config.success_calls = (api_config.success_calls or 0) + 1
|
||||
avg_time = api_config.avg_response_time or 0
|
||||
api_config.avg_response_time = int(
|
||||
(avg_time * (api_config.total_calls - 1) + response_time) / api_config.total_calls
|
||||
)
|
||||
api_config.fail_count = 0
|
||||
|
||||
# 记录 API Key 调用
|
||||
record_api_call(key_obj, ip_address, success=True)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': '解析成功',
|
||||
'data': {
|
||||
'cover': result.get('cover', ''),
|
||||
'title': result.get('title', ''),
|
||||
'description': result.get('description', ''),
|
||||
'author': result.get('author', ''),
|
||||
'video_url': result.get('video_url', '')
|
||||
}
|
||||
})
|
||||
|
||||
except Exception as e:
|
||||
last_error = str(e)
|
||||
api_config.total_calls = (api_config.total_calls or 0) + 1
|
||||
api_config.fail_count = (api_config.fail_count or 0) + 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)
|
||||
record_api_call(key_obj, ip_address, success=False)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'code': 500,
|
||||
'msg': last_error or '解析失败,请稍后重试'
|
||||
})
|
||||
|
||||
|
||||
@api_v1_bp.route('/platforms', methods=['GET'])
|
||||
def get_platforms():
|
||||
"""获取支持的平台列表"""
|
||||
return jsonify({
|
||||
'code': 200,
|
||||
'msg': '获取成功',
|
||||
'data': [
|
||||
{'name': 'douyin', 'display_name': '抖音'},
|
||||
{'name': 'tiktok', 'display_name': 'TikTok'},
|
||||
{'name': 'bilibili', 'display_name': '哔哩哔哩'},
|
||||
{'name': 'kuaishou', 'display_name': '快手'},
|
||||
{'name': 'pipixia', 'display_name': '皮皮虾'},
|
||||
{'name': 'weibo', 'display_name': '微博'}
|
||||
]
|
||||
})
|
||||
174
routes/apikey.py
Normal file
174
routes/apikey.py
Normal file
@@ -0,0 +1,174 @@
|
||||
from flask import Blueprint, request, jsonify, render_template
|
||||
from flask_login import login_required, current_user
|
||||
from models import db, UserApiKey, ApiKeyDailyStat
|
||||
from datetime import datetime, date
|
||||
import secrets
|
||||
|
||||
apikey_bp = Blueprint('apikey', __name__, url_prefix='/user/apikey')
|
||||
|
||||
|
||||
def generate_api_key():
|
||||
"""生成 API Key"""
|
||||
return f"sk_{secrets.token_hex(24)}"
|
||||
|
||||
|
||||
@apikey_bp.route('/')
|
||||
@login_required
|
||||
def manage_page():
|
||||
"""API Key 管理页面"""
|
||||
return render_template('apikey/manage.html')
|
||||
|
||||
|
||||
@apikey_bp.route('/list', methods=['GET'])
|
||||
@login_required
|
||||
def list_keys():
|
||||
"""获取用户的 API Key 列表"""
|
||||
keys = UserApiKey.query.filter_by(user_id=current_user.id).order_by(UserApiKey.created_at.desc()).all()
|
||||
|
||||
result = []
|
||||
for key in keys:
|
||||
# 获取今日调用次数
|
||||
today_stat = ApiKeyDailyStat.query.filter_by(
|
||||
api_key_id=key.id,
|
||||
date=date.today()
|
||||
).first()
|
||||
|
||||
result.append({
|
||||
'id': key.id,
|
||||
'name': key.name,
|
||||
'api_key': key.api_key[:12] + '...' + key.api_key[-4:], # 隐藏中间部分
|
||||
'is_active': key.is_active,
|
||||
'daily_limit': key.daily_limit,
|
||||
'total_calls': key.total_calls,
|
||||
'today_calls': today_stat.call_count if today_stat else 0,
|
||||
'last_used_at': key.last_used_at.strftime('%Y-%m-%d %H:%M:%S') if key.last_used_at else None,
|
||||
'created_at': key.created_at.strftime('%Y-%m-%d %H:%M:%S')
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': result
|
||||
})
|
||||
|
||||
|
||||
@apikey_bp.route('/create', methods=['POST'])
|
||||
@login_required
|
||||
def create_key():
|
||||
"""创建新的 API Key"""
|
||||
data = request.get_json()
|
||||
name = data.get('name', '').strip()
|
||||
|
||||
if not name:
|
||||
return jsonify({'success': False, 'message': '请输入 Key 名称'}), 400
|
||||
|
||||
if len(name) > 100:
|
||||
return jsonify({'success': False, 'message': 'Key 名称不能超过100个字符'}), 400
|
||||
|
||||
# 检查用户已有的 Key 数量(限制每个用户最多5个)
|
||||
key_count = UserApiKey.query.filter_by(user_id=current_user.id).count()
|
||||
if key_count >= 5:
|
||||
return jsonify({'success': False, 'message': '每个用户最多创建5个 API Key'}), 400
|
||||
|
||||
# 生成新的 API Key
|
||||
api_key = generate_api_key()
|
||||
|
||||
# 根据用户等级设置每日限制
|
||||
daily_limit = 100 # 默认
|
||||
if current_user.group:
|
||||
if current_user.group.id == 3: # VIP
|
||||
daily_limit = 500
|
||||
elif current_user.group.id == 4: # SVIP
|
||||
daily_limit = 2000
|
||||
|
||||
new_key = UserApiKey(
|
||||
user_id=current_user.id,
|
||||
name=name,
|
||||
api_key=api_key,
|
||||
daily_limit=daily_limit
|
||||
)
|
||||
|
||||
db.session.add(new_key)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'API Key 创建成功',
|
||||
'data': {
|
||||
'id': new_key.id,
|
||||
'name': new_key.name,
|
||||
'api_key': api_key, # 只在创建时返回完整的 Key
|
||||
'daily_limit': new_key.daily_limit
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@apikey_bp.route('/delete/<int:key_id>', methods=['DELETE'])
|
||||
@login_required
|
||||
def delete_key(key_id):
|
||||
"""删除 API Key"""
|
||||
key = UserApiKey.query.filter_by(id=key_id, user_id=current_user.id).first()
|
||||
|
||||
if not key:
|
||||
return jsonify({'success': False, 'message': 'API Key 不存在'}), 404
|
||||
|
||||
db.session.delete(key)
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': 'API Key 已删除'
|
||||
})
|
||||
|
||||
|
||||
@apikey_bp.route('/toggle/<int:key_id>', methods=['POST'])
|
||||
@login_required
|
||||
def toggle_key(key_id):
|
||||
"""启用/禁用 API Key"""
|
||||
key = UserApiKey.query.filter_by(id=key_id, user_id=current_user.id).first()
|
||||
|
||||
if not key:
|
||||
return jsonify({'success': False, 'message': 'API Key 不存在'}), 404
|
||||
|
||||
key.is_active = not key.is_active
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f"API Key 已{'启用' if key.is_active else '禁用'}",
|
||||
'is_active': key.is_active
|
||||
})
|
||||
|
||||
|
||||
@apikey_bp.route('/stats/<int:key_id>', methods=['GET'])
|
||||
@login_required
|
||||
def get_stats(key_id):
|
||||
"""获取 API Key 统计数据"""
|
||||
key = UserApiKey.query.filter_by(id=key_id, user_id=current_user.id).first()
|
||||
|
||||
if not key:
|
||||
return jsonify({'success': False, 'message': 'API Key 不存在'}), 404
|
||||
|
||||
# 获取最近7天的统计
|
||||
from datetime import timedelta
|
||||
stats = ApiKeyDailyStat.query.filter(
|
||||
ApiKeyDailyStat.api_key_id == key_id,
|
||||
ApiKeyDailyStat.date >= date.today() - timedelta(days=6)
|
||||
).order_by(ApiKeyDailyStat.date.asc()).all()
|
||||
|
||||
result = []
|
||||
for stat in stats:
|
||||
result.append({
|
||||
'date': stat.date.strftime('%Y-%m-%d'),
|
||||
'call_count': stat.call_count,
|
||||
'success_count': stat.success_count,
|
||||
'fail_count': stat.fail_count
|
||||
})
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'data': {
|
||||
'key_name': key.name,
|
||||
'total_calls': key.total_calls,
|
||||
'daily_stats': result
|
||||
}
|
||||
})
|
||||
@@ -219,14 +219,22 @@ def profile():
|
||||
@login_required
|
||||
def get_profile():
|
||||
"""获取用户个人中心数据"""
|
||||
from models import UserGroup, DailyParseStat, ParseLog
|
||||
from datetime import date
|
||||
from models import UserGroup, DailyParseStat, ParseLog, UserGroupExpiry
|
||||
from datetime import date, datetime
|
||||
|
||||
# 获取用户组信息
|
||||
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 '普通用户'
|
||||
|
||||
# 获取套餐到期时间
|
||||
group_expiry = UserGroupExpiry.query.filter_by(user_id=current_user.id).first()
|
||||
expires_at = None
|
||||
is_expired = False
|
||||
if group_expiry:
|
||||
expires_at = group_expiry.expires_at.strftime('%Y-%m-%d %H:%M') if group_expiry.expires_at else None
|
||||
is_expired = group_expiry.expires_at < datetime.utcnow() if group_expiry.expires_at else False
|
||||
|
||||
# 获取今日使用次数
|
||||
today = date.today()
|
||||
today_stat = DailyParseStat.query.filter_by(
|
||||
@@ -263,7 +271,9 @@ def get_profile():
|
||||
'group': {
|
||||
'id': current_user.group_id,
|
||||
'name': group_name,
|
||||
'daily_limit': daily_limit
|
||||
'daily_limit': daily_limit,
|
||||
'expires_at': expires_at,
|
||||
'is_expired': is_expired
|
||||
},
|
||||
'usage': {
|
||||
'daily_limit': daily_limit,
|
||||
@@ -274,3 +284,72 @@ def get_profile():
|
||||
'parse_logs': logs_data
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@auth_bp.route('/api/redeem', methods=['POST'])
|
||||
@login_required
|
||||
def redeem_code():
|
||||
"""用户兑换码"""
|
||||
from models import RedeemCode, UserGroupExpiry
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
data = request.get_json()
|
||||
code_str = data.get('code', '').strip().upper()
|
||||
|
||||
if not code_str:
|
||||
return jsonify({'success': False, 'message': '请输入兑换码'}), 400
|
||||
|
||||
# 查找兑换码
|
||||
code = RedeemCode.query.filter_by(code=code_str).first()
|
||||
|
||||
if not code:
|
||||
return jsonify({'success': False, 'message': '兑换码不存在'}), 404
|
||||
|
||||
if code.is_used:
|
||||
return jsonify({'success': False, 'message': '兑换码已被使用'}), 400
|
||||
|
||||
if code.expires_at and code.expires_at < datetime.utcnow():
|
||||
return jsonify({'success': False, 'message': '兑换码已过期'}), 400
|
||||
|
||||
# 执行兑换
|
||||
user = current_user
|
||||
|
||||
# 更新用户组
|
||||
old_group_id = user.group_id
|
||||
user.group_id = code.target_group_id
|
||||
|
||||
# 计算到期时间
|
||||
expires_at = datetime.utcnow() + timedelta(days=code.duration_days)
|
||||
|
||||
# 更新或创建到期记录
|
||||
expiry = UserGroupExpiry.query.filter_by(user_id=user.id).first()
|
||||
if expiry:
|
||||
# 如果当前套餐未过期且是同一套餐,则叠加时间
|
||||
if expiry.group_id == code.target_group_id and expiry.expires_at > datetime.utcnow():
|
||||
expires_at = expiry.expires_at + timedelta(days=code.duration_days)
|
||||
expiry.group_id = code.target_group_id
|
||||
expiry.expires_at = expires_at
|
||||
else:
|
||||
expiry = UserGroupExpiry(
|
||||
user_id=user.id,
|
||||
group_id=code.target_group_id,
|
||||
expires_at=expires_at
|
||||
)
|
||||
db.session.add(expiry)
|
||||
|
||||
# 标记兑换码已使用
|
||||
code.is_used = True
|
||||
code.used_by = user.id
|
||||
code.used_at = datetime.utcnow()
|
||||
|
||||
db.session.commit()
|
||||
|
||||
return jsonify({
|
||||
'success': True,
|
||||
'message': f'兑换成功!您已升级为 {code.target_group.name},有效期至 {expires_at.strftime("%Y-%m-%d %H:%M")}',
|
||||
'data': {
|
||||
'group_name': code.target_group.name,
|
||||
'expires_at': expires_at.strftime('%Y-%m-%d %H:%M'),
|
||||
'duration_days': code.duration_days
|
||||
}
|
||||
})
|
||||
|
||||
112
routes/main.py
112
routes/main.py
@@ -1,4 +1,5 @@
|
||||
from flask import Blueprint, render_template, redirect
|
||||
from flask import Blueprint, render_template, redirect, request, Response
|
||||
import requests
|
||||
|
||||
main_bp = Blueprint('main', __name__)
|
||||
|
||||
@@ -11,6 +12,12 @@ def index():
|
||||
config[c.config_key] = c.config_value
|
||||
return render_template('index.html', config=config)
|
||||
|
||||
@main_bp.route('/api-docs')
|
||||
def api_docs():
|
||||
"""API 文档页面"""
|
||||
return render_template('api_docs.html')
|
||||
|
||||
|
||||
@main_bp.route('/favicon.ico')
|
||||
def favicon():
|
||||
"""处理 favicon 请求"""
|
||||
@@ -24,3 +31,106 @@ def favicon():
|
||||
return redirect(logo_config.config_value)
|
||||
# 返回空响应避免 404
|
||||
return '', 204
|
||||
|
||||
|
||||
@main_bp.route('/proxy/download')
|
||||
def proxy_download():
|
||||
"""代理下载视频/图片,绕过防盗链,支持 Range 请求"""
|
||||
url = request.args.get('url')
|
||||
if not url:
|
||||
return 'Missing url parameter', 400
|
||||
|
||||
# 限制只能代理特定域名
|
||||
allowed_domains = [
|
||||
'weibocdn.com', 'sinaimg.cn', 'weibo.com',
|
||||
'ppxvod.com', 'byteimg.com', 'pipix.com', '365yg.com', 'ixigua.com',
|
||||
'kwimgs.com', 'kwaicdn.com', 'kuaishou.com', 'gifshow.com', 'yximgs.com',
|
||||
'douyinvod.com', 'douyin.com', 'tiktokcdn.com', 'bytedance.com',
|
||||
'bilivideo.com', 'bilibili.com', 'hdslb.com',
|
||||
'zjcdn.com', 'douyinpic.com'
|
||||
]
|
||||
|
||||
from urllib.parse import urlparse
|
||||
parsed = urlparse(url)
|
||||
domain = parsed.netloc.lower()
|
||||
|
||||
is_allowed = any(d in domain for d in allowed_domains)
|
||||
if not is_allowed:
|
||||
return f'Domain not allowed: {domain}', 403
|
||||
|
||||
# 根据域名设置不同的 Referer
|
||||
referer_map = {
|
||||
'weibo': 'https://weibo.com/',
|
||||
'sina': 'https://weibo.com/',
|
||||
'pipix': 'https://www.pipix.com/',
|
||||
'365yg': 'https://www.pipix.com/',
|
||||
'ixigua': 'https://www.ixigua.com/',
|
||||
'kuaishou': 'https://www.kuaishou.com/',
|
||||
'kwimgs': 'https://www.kuaishou.com/',
|
||||
'kwaicdn': 'https://www.kuaishou.com/',
|
||||
'yximgs': 'https://www.kuaishou.com/',
|
||||
'douyin': 'https://www.douyin.com/',
|
||||
'bytedance': 'https://www.douyin.com/',
|
||||
'zjcdn': 'https://www.douyin.com/',
|
||||
'douyinpic': 'https://www.douyin.com/',
|
||||
'bilibili': 'https://www.bilibili.com/',
|
||||
'hdslb': 'https://www.bilibili.com/',
|
||||
}
|
||||
|
||||
referer = 'https://www.google.com/'
|
||||
for key, ref in referer_map.items():
|
||||
if key in domain:
|
||||
referer = ref
|
||||
break
|
||||
|
||||
try:
|
||||
headers = {
|
||||
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
|
||||
'Referer': referer
|
||||
}
|
||||
|
||||
# 转发 Range 请求头,支持视频进度条拖动
|
||||
range_header = request.headers.get('Range')
|
||||
if range_header:
|
||||
headers['Range'] = range_header
|
||||
|
||||
resp = requests.get(url, headers=headers, stream=True, timeout=30)
|
||||
|
||||
# 获取文件类型
|
||||
content_type = resp.headers.get('Content-Type', 'application/octet-stream')
|
||||
|
||||
# 设置下载文件名
|
||||
if 'video' in content_type:
|
||||
filename = f'video_{int(__import__("time").time())}.mp4'
|
||||
elif 'image' in content_type:
|
||||
filename = f'image_{int(__import__("time").time())}.jpg'
|
||||
else:
|
||||
filename = f'file_{int(__import__("time").time())}'
|
||||
|
||||
def generate():
|
||||
for chunk in resp.iter_content(chunk_size=8192):
|
||||
yield chunk
|
||||
|
||||
# 构建响应头
|
||||
response_headers = {
|
||||
'Content-Type': content_type,
|
||||
'Accept-Ranges': 'bytes'
|
||||
}
|
||||
|
||||
# 转发相关响应头
|
||||
if resp.headers.get('Content-Length'):
|
||||
response_headers['Content-Length'] = resp.headers.get('Content-Length')
|
||||
if resp.headers.get('Content-Range'):
|
||||
response_headers['Content-Range'] = resp.headers.get('Content-Range')
|
||||
|
||||
# 根据是否是 Range 请求返回不同状态码
|
||||
status_code = resp.status_code
|
||||
|
||||
return Response(
|
||||
generate(),
|
||||
status=status_code,
|
||||
headers=response_headers
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
return f'Download failed: {str(e)}', 500
|
||||
|
||||
@@ -40,6 +40,9 @@ def parse_video():
|
||||
except ValueError as e:
|
||||
return jsonify({'success': False, 'message': str(e)}), 400
|
||||
|
||||
# 展开短链接
|
||||
video_url = ParserFactory.expand_short_url(video_url)
|
||||
|
||||
# 生成任务ID
|
||||
task_id = str(uuid.uuid4())
|
||||
|
||||
@@ -116,6 +119,10 @@ def _process_task(task_id, video_url, platform, user_id, ip_address):
|
||||
# 执行解析
|
||||
result = parser.parse(video_url)
|
||||
|
||||
# 验证解析结果,video_url 不能为空
|
||||
if not result.get('video_url'):
|
||||
raise Exception('未能获取到视频链接')
|
||||
|
||||
# 计算响应时间
|
||||
response_time = int((time.time() - start_time) * 1000)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user