|
|
|
|
@ -0,0 +1,211 @@
|
|
|
|
|
"""
|
|
|
|
|
VIP服务模块
|
|
|
|
|
处理VIP邀请码验证、VIP权限检查等功能
|
|
|
|
|
使用Redis存储邀请码
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
import logging
|
|
|
|
|
import secrets
|
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
|
from typing import Optional
|
|
|
|
|
from functools import wraps
|
|
|
|
|
from flask import jsonify
|
|
|
|
|
|
|
|
|
|
from app import db
|
|
|
|
|
from app.database import User
|
|
|
|
|
from app.services.cache import RedisClient
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
# Redis键前缀
|
|
|
|
|
VIP_CODE_PREFIX = "vip_code:"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class VipService:
|
|
|
|
|
"""VIP服务类"""
|
|
|
|
|
|
|
|
|
|
# 默认的VIP邀请码(用于测试)
|
|
|
|
|
DEFAULT_VIP_CODES = ['VIP2024', 'VIP2025', 'PREMIUM']
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def _get_redis(cls) -> RedisClient:
|
|
|
|
|
"""获取Redis客户端"""
|
|
|
|
|
return RedisClient()
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def _get_code_key(cls, code: str) -> str:
|
|
|
|
|
"""获取邀请码的Redis键"""
|
|
|
|
|
return f"{VIP_CODE_PREFIX}{code}"
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def verify_vip_code(cls, code: str) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
验证VIP邀请码是否有效
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
code: VIP邀请码
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
True 如果邀请码有效,否则 False
|
|
|
|
|
"""
|
|
|
|
|
if not code:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
# 检查是否是默认的测试邀请码
|
|
|
|
|
if code in cls.DEFAULT_VIP_CODES:
|
|
|
|
|
redis_client = cls._get_redis()
|
|
|
|
|
key = cls._get_code_key(code)
|
|
|
|
|
data = redis_client.get(key)
|
|
|
|
|
# 默认邀请码如果没有被使用过,则有效
|
|
|
|
|
if data is None:
|
|
|
|
|
return True
|
|
|
|
|
# 如果已存储,检查是否已使用
|
|
|
|
|
try:
|
|
|
|
|
code_info = json.loads(data)
|
|
|
|
|
return not code_info.get('used', False)
|
|
|
|
|
except (json.JSONDecodeError, TypeError):
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
# 检查动态生成的邀请码
|
|
|
|
|
redis_client = cls._get_redis()
|
|
|
|
|
key = cls._get_code_key(code)
|
|
|
|
|
data = redis_client.get(key)
|
|
|
|
|
|
|
|
|
|
if data is None:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
try:
|
|
|
|
|
code_info = json.loads(data)
|
|
|
|
|
# 检查是否已使用
|
|
|
|
|
if code_info.get('used'):
|
|
|
|
|
return False
|
|
|
|
|
return True
|
|
|
|
|
except (json.JSONDecodeError, TypeError):
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def mark_vip_code_used(cls, code: str, user_id: int) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
标记VIP邀请码已使用
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
code: VIP邀请码
|
|
|
|
|
user_id: 使用该邀请码的用户ID
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
True 如果标记成功,否则 False
|
|
|
|
|
"""
|
|
|
|
|
if not code:
|
|
|
|
|
return False
|
|
|
|
|
|
|
|
|
|
if code in cls.DEFAULT_VIP_CODES:
|
|
|
|
|
return True
|
|
|
|
|
|
|
|
|
|
redis_client = cls._get_redis()
|
|
|
|
|
key = cls._get_code_key(code)
|
|
|
|
|
|
|
|
|
|
code_info = {
|
|
|
|
|
'used': True,
|
|
|
|
|
'used_by': user_id,
|
|
|
|
|
'used_at': datetime.utcnow().isoformat()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 已使用的邀请码保留90天记录
|
|
|
|
|
success = redis_client.set(key, json.dumps(code_info), ex=90 * 24 * 3600)
|
|
|
|
|
if success:
|
|
|
|
|
logger.info(f"VIP邀请码 {code} 已被用户 {user_id} 使用")
|
|
|
|
|
return success
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def generate_vip_code(cls, expires_days: int = 30) -> str:
|
|
|
|
|
"""
|
|
|
|
|
生成新的VIP邀请码
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
expires_days: 邀请码有效天数
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
生成的邀请码
|
|
|
|
|
"""
|
|
|
|
|
code = f"VIP-{secrets.token_hex(4).upper()}"
|
|
|
|
|
|
|
|
|
|
redis_client = cls._get_redis()
|
|
|
|
|
key = cls._get_code_key(code)
|
|
|
|
|
|
|
|
|
|
code_info = {
|
|
|
|
|
'used': False,
|
|
|
|
|
'used_by': None,
|
|
|
|
|
'used_at': None,
|
|
|
|
|
'created_at': datetime.utcnow().isoformat(),
|
|
|
|
|
'expires_days': expires_days
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# 设置过期时间
|
|
|
|
|
expires_seconds = expires_days * 24 * 3600
|
|
|
|
|
redis_client.set(key, json.dumps(code_info), ex=expires_seconds)
|
|
|
|
|
|
|
|
|
|
logger.info(f"生成新的VIP邀请码: {code}, 有效期: {expires_days}天")
|
|
|
|
|
return code
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def is_user_vip(cls, user: User) -> bool:
|
|
|
|
|
"""
|
|
|
|
|
检查用户是否为VIP
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
user: 用户对象
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
True 如果用户是VIP或管理员,否则 False
|
|
|
|
|
"""
|
|
|
|
|
if not user or not user.role:
|
|
|
|
|
return False
|
|
|
|
|
return user.role.role_code in ('vip', 'admin')
|
|
|
|
|
|
|
|
|
|
@classmethod
|
|
|
|
|
def get_vip_features(cls, user: User) -> dict:
|
|
|
|
|
"""
|
|
|
|
|
获取用户的VIP特权信息
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
user: 用户对象
|
|
|
|
|
|
|
|
|
|
Returns:
|
|
|
|
|
VIP特权字典
|
|
|
|
|
"""
|
|
|
|
|
is_vip = cls.is_user_vip(user)
|
|
|
|
|
return {
|
|
|
|
|
'is_vip': is_vip,
|
|
|
|
|
'max_concurrent_tasks': user.role.max_concurrent_tasks if user and user.role else 1,
|
|
|
|
|
'can_use_all_datasets': is_vip,
|
|
|
|
|
'can_upload_finetune': is_vip,
|
|
|
|
|
'priority_queue': is_vip
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def vip_required(f):
|
|
|
|
|
"""
|
|
|
|
|
VIP权限装饰器
|
|
|
|
|
用于保护需要VIP权限的接口
|
|
|
|
|
"""
|
|
|
|
|
@wraps(f)
|
|
|
|
|
def decorated_function(*args, **kwargs):
|
|
|
|
|
# 从kwargs中获取current_user_id(由int_jwt_required装饰器注入)
|
|
|
|
|
current_user_id = kwargs.get('current_user_id')
|
|
|
|
|
if not current_user_id:
|
|
|
|
|
return jsonify({'error': '未授权访问'}), 401
|
|
|
|
|
|
|
|
|
|
user = User.query.get(current_user_id)
|
|
|
|
|
if not user:
|
|
|
|
|
return jsonify({'error': '用户不存在'}), 404
|
|
|
|
|
|
|
|
|
|
if not VipService.is_user_vip(user):
|
|
|
|
|
return jsonify({
|
|
|
|
|
'error': '此功能仅限VIP用户使用',
|
|
|
|
|
'upgrade_hint': '请使用VIP邀请码升级为VIP用户'
|
|
|
|
|
}), 403
|
|
|
|
|
|
|
|
|
|
return f(*args, **kwargs)
|
|
|
|
|
|
|
|
|
|
return decorated_function
|