""" 蜂鸟Pro 账号服务 v2.1 基于系统设计文档重构 """ import secrets import string from datetime import datetime, timedelta from typing import Optional, List, Tuple, Dict, Any from sqlalchemy.orm import Session from sqlalchemy import func, and_, or_ from decimal import Decimal from app.models.models import ( CursorAccount, ActivationKey, KeyDevice, UsageLog, GlobalSettings, AccountStatus, AccountType, KeyMembershipType, KeyStatus ) def generate_key(length: int = 32) -> str: """生成随机激活码""" chars = string.ascii_uppercase + string.digits return ''.join(secrets.choice(chars) for _ in range(length)) # ==================== 账号服务 ==================== class AccountService: """Cursor 账号管理服务""" @staticmethod def create( db: Session, email: str, token: Optional[str] = None, password: str = None, remark: str = None, access_token: Optional[str] = None, refresh_token: Optional[str] = None, workos_session_token: Optional[str] = None ) -> CursorAccount: """ 创建账号 (状态为 pending,等待后台分析) """ resolved_token = workos_session_token or token or access_token if not resolved_token: raise ValueError("需要至少提供一个 Token (Workos 或 Access)") db_account = CursorAccount( email=email, token=resolved_token, access_token=access_token, refresh_token=refresh_token, workos_session_token=workos_session_token or token, password=password, status=AccountStatus.PENDING, remark=remark ) db.add(db_account) db.commit() db.refresh(db_account) return db_account @staticmethod def get_by_id(db: Session, account_id: int) -> Optional[CursorAccount]: return db.query(CursorAccount).filter(CursorAccount.id == account_id).first() @staticmethod def get_by_email(db: Session, email: str) -> Optional[CursorAccount]: return db.query(CursorAccount).filter(CursorAccount.email == email).first() @staticmethod def get_all(db: Session, skip: int = 0, limit: int = 100, status: AccountStatus = None) -> List[CursorAccount]: query = db.query(CursorAccount) if status: query = query.filter(CursorAccount.status == status) return query.order_by(CursorAccount.id.desc()).offset(skip).limit(limit).all() @staticmethod def get_available_for_key(db: Session, key: ActivationKey) -> Optional[CursorAccount]: """ 为密钥分配一个可用账号 根据密钥类型选择合适的账号池 """ query = db.query(CursorAccount).filter( CursorAccount.status == AccountStatus.AVAILABLE ) # 根据密钥类型选择账号 if key.membership_type == KeyMembershipType.AUTO: # Auto密钥优先选择 free_trial 类型账号 query = query.filter( CursorAccount.account_type.in_([AccountType.FREE_TRIAL, AccountType.FREE]) ) else: # Pro密钥优先选择 pro 类型账号 query = query.filter( CursorAccount.account_type.in_([AccountType.PRO, AccountType.FREE_TRIAL, AccountType.BUSINESS]) ) # 按用量百分比升序排列(选择用量最低的) return query.order_by(CursorAccount.usage_percent.asc()).first() @staticmethod def get_pending_accounts(db: Session, limit: int = 10) -> List[CursorAccount]: """获取待分析的账号""" return db.query(CursorAccount).filter( CursorAccount.status.in_([AccountStatus.PENDING, AccountStatus.AVAILABLE]), or_( CursorAccount.last_analyzed_at == None, CursorAccount.last_analyzed_at < datetime.now() - timedelta(minutes=30) ) ).limit(limit).all() @staticmethod def update_from_analysis(db: Session, account_id: int, analysis_data: Dict[str, Any]) -> Optional[CursorAccount]: """ 根据 Cursor API 分析结果更新账号信息 """ account = db.query(CursorAccount).filter(CursorAccount.id == account_id).first() if not account: return None # 更新分析结果 if analysis_data.get("success"): account.status = AccountStatus[analysis_data["status"].upper()] account.account_type = AccountType[analysis_data["account_type"].upper()] account.membership_type = analysis_data.get("membership_type") account.billing_cycle_start = analysis_data.get("billing_cycle_start") account.billing_cycle_end = analysis_data.get("billing_cycle_end") account.trial_days_remaining = analysis_data.get("trial_days_remaining", 0) account.usage_limit = analysis_data.get("usage_limit", 0) account.usage_used = analysis_data.get("usage_used", 0) account.usage_remaining = analysis_data.get("usage_remaining", 0) account.usage_percent = analysis_data.get("usage_percent", Decimal("0")) account.total_requests = analysis_data.get("total_requests", 0) account.total_input_tokens = analysis_data.get("total_input_tokens", 0) account.total_output_tokens = analysis_data.get("total_output_tokens", 0) account.total_cost_cents = analysis_data.get("total_cost_cents", Decimal("0")) account.last_analyzed_at = analysis_data.get("last_analyzed_at", datetime.now()) account.analyze_error = None else: # 分析失败 account.status = AccountStatus.INVALID account.analyze_error = analysis_data.get("error", "Unknown error") account.last_analyzed_at = datetime.now() db.commit() db.refresh(account) return account @staticmethod def lock_account(db: Session, account: CursorAccount, key_id: int) -> CursorAccount: """锁定账号给指定密钥使用""" account.status = AccountStatus.IN_USE account.locked_by_key_id = key_id account.locked_at = datetime.now() db.commit() db.refresh(account) return account @staticmethod def release_account(db: Session, account: CursorAccount) -> CursorAccount: """释放账号""" # 根据用量决定状态(使用配置阈值,默认98%) threshold = GlobalSettingsService.get_int(db, "auto_switch_threshold") or 98 if account.usage_percent and float(account.usage_percent) >= threshold: account.status = AccountStatus.EXHAUSTED else: account.status = AccountStatus.AVAILABLE account.locked_by_key_id = None account.locked_at = None db.commit() db.refresh(account) return account @staticmethod def update(db: Session, account_id: int, **kwargs) -> Optional[CursorAccount]: account = db.query(CursorAccount).filter(CursorAccount.id == account_id).first() if account: for key, value in kwargs.items(): if hasattr(account, key) and value is not None: setattr(account, key, value) db.commit() db.refresh(account) return account @staticmethod def delete(db: Session, account_id: int) -> bool: account = db.query(CursorAccount).filter(CursorAccount.id == account_id).first() if account: db.delete(account) db.commit() return True return False @staticmethod def count(db: Session) -> dict: """统计账号数量""" rows = db.query( CursorAccount.status, func.count(CursorAccount.id) ).group_by(CursorAccount.status).all() counts = {status.value: cnt for status, cnt in rows} total = sum(counts.values()) return { "total": total, "available": counts.get(AccountStatus.AVAILABLE.value, 0), "in_use": counts.get(AccountStatus.IN_USE.value, 0), "exhausted": counts.get(AccountStatus.EXHAUSTED.value, 0), "pending": counts.get(AccountStatus.PENDING.value, 0), "invalid": counts.get(AccountStatus.INVALID.value, 0), } # ==================== 激活码服务 ==================== class KeyService: """激活码管理服务""" @staticmethod def _get_bool_setting(db: Session, key: str, default: bool = False) -> bool: value = GlobalSettingsService.get(db, key) if value is None: return default return str(value).strip().lower() in ("true", "1", "yes", "y", "on") @staticmethod def _get_key_max_devices(db: Session) -> int: limit = GlobalSettingsService.get_int(db, "key_max_devices") return limit if limit > 0 else 2 @staticmethod def _is_device_bound(db: Session, key_id: int, device_id: str) -> bool: if not device_id: return False return db.query(KeyDevice).filter( KeyDevice.key_id == key_id, KeyDevice.device_id == device_id ).first() is not None @staticmethod def _ensure_device_bound(db: Session, key: ActivationKey, device_id: str) -> Tuple[bool, str]: if not device_id: return True, "" existing = db.query(KeyDevice).filter( KeyDevice.key_id == key.id, KeyDevice.device_id == device_id ).first() now = datetime.now() if existing: existing.last_active_at = now return True, "" limit = KeyService._get_key_max_devices(db) count = db.query(KeyDevice).filter(KeyDevice.key_id == key.id).count() if count >= limit: return False, f"设备数量已达上限({limit}),请先解绑旧设备" db.add(KeyDevice( key_id=key.id, device_id=device_id, last_active_at=now )) return True, "" @staticmethod def _find_master_for_device(db: Session, membership_type: KeyMembershipType, device_id: str) -> Optional[ActivationKey]: if not device_id: return None # 新逻辑:以 key_devices 为准 master = db.query(ActivationKey).join( KeyDevice, KeyDevice.key_id == ActivationKey.id ).filter( KeyDevice.device_id == device_id, ActivationKey.membership_type == membership_type, ActivationKey.status != KeyStatus.DISABLED, ActivationKey.master_key_id == None ).order_by(ActivationKey.id.desc()).first() if master: return master # 兼容旧数据:activation_keys.device_id 绑定 return db.query(ActivationKey).filter( ActivationKey.device_id == device_id, ActivationKey.membership_type == membership_type, ActivationKey.status != KeyStatus.DISABLED, ActivationKey.master_key_id == None ).order_by(ActivationKey.id.desc()).first() @staticmethod def create( db: Session, count: int = 1, membership_type: KeyMembershipType = KeyMembershipType.PRO, duration_days: int = 30, quota: int = 500, max_devices: int = 2, remark: str = None ) -> List[ActivationKey]: """创建激活码(支持批量)""" keys = [] max_retries = 5 for _ in range(count): for retry in range(max_retries): key_str = generate_key() existing = db.query(ActivationKey).filter(ActivationKey.key == key_str).first() if not existing: break if retry == max_retries - 1: raise ValueError("无法生成唯一激活码,请重试") db_key = ActivationKey( key=key_str, status=KeyStatus.UNUSED, membership_type=membership_type, duration_days=duration_days if membership_type == KeyMembershipType.AUTO else 0, quota_contribution=quota if membership_type == KeyMembershipType.PRO else 0, quota=quota if membership_type == KeyMembershipType.PRO else 0, max_devices=max_devices, remark=remark ) db.add(db_key) keys.append(db_key) db.commit() for k in keys: db.refresh(k) return keys @staticmethod def get_by_key(db: Session, key: str) -> Optional[ActivationKey]: return db.query(ActivationKey).filter(ActivationKey.key == key).first() @staticmethod def get_by_id(db: Session, key_id: int) -> Optional[ActivationKey]: return db.query(ActivationKey).filter(ActivationKey.id == key_id).first() @staticmethod def get_all(db: Session, skip: int = 0, limit: int = 100) -> List[ActivationKey]: return db.query(ActivationKey).order_by(ActivationKey.id.desc()).offset(skip).limit(limit).all() @staticmethod def activate(db: Session, key: ActivationKey, device_id: str = None) -> Tuple[bool, str, Optional[ActivationKey]]: """ 激活密钥 (不分配账号!) 返回: (成功, 消息, 密钥) """ now = datetime.now() # 已合并的子密钥:默认禁止继续使用 if key.master_key_id: master_key = KeyService.get_by_id(db, key.master_key_id) if not master_key: return False, "该密钥已合并,但主密钥不存在,请联系管理员", None # 仅当当前设备已绑定到主密钥时,允许自动“跳转到主密钥”(用于同设备迁移/兼容) if device_id and KeyService._is_device_bound(db, master_key.id, device_id): ok, bind_msg = KeyService._ensure_device_bound(db, master_key, device_id) if not ok: return False, bind_msg, None master_key.device_id = device_id master_key.last_active_at = now db.commit() return True, "该密钥已合并,已为你切换到主密钥", master_key return False, "该密钥已合并,请使用主密钥", None # 检查密钥状态 if key.status == KeyStatus.DISABLED: return False, "该密钥已被禁用", None if key.status == KeyStatus.EXPIRED: return False, "该密钥已过期", None # 已激活的密钥 if key.status == KeyStatus.ACTIVE: # 检查是否过期 if key.is_expired: key.status = KeyStatus.EXPIRED db.commit() return False, "该密钥已过期", None # 更新设备ID和活跃时间 if device_id: ok, bind_msg = KeyService._ensure_device_bound(db, key, device_id) if not ok: return False, bind_msg, None key.device_id = device_id key.last_active_at = now db.commit() return True, "密钥已激活", key # 查找该设备同类型的主密钥(用于合并) master_key = None auto_merge_enabled = KeyService._get_bool_setting(db, "auto_merge_enabled", default=True) if auto_merge_enabled and device_id: master_key = KeyService._find_master_for_device(db, key.membership_type, device_id) if master_key: # 合并到现有主密钥 key.master_key_id = master_key.id key.merged_at = now key.device_id = device_id key.status = KeyStatus.ACTIVE # 合并密钥标记为已使用(通过 master_key_id 判定不可直接使用) # 叠加资源 if key.membership_type == KeyMembershipType.PRO: master_key.quota += key.quota_contribution else: base = master_key.expire_at if master_key.expire_at and master_key.expire_at > now else now master_key.expire_at = base + timedelta(days=key.duration_days) # 合并后主密钥可能从“过期”恢复为可用 if master_key.status in (KeyStatus.EXPIRED, KeyStatus.UNUSED): master_key.status = KeyStatus.ACTIVE master_key.merged_count += 1 if device_id: ok, bind_msg = KeyService._ensure_device_bound(db, master_key, device_id) if not ok: return False, bind_msg, None master_key.device_id = device_id master_key.last_active_at = now db.commit() return True, f"密钥已合并,{'积分' if key.membership_type == KeyMembershipType.PRO else '时长'}已叠加", master_key else: # 成为主密钥 key.status = KeyStatus.ACTIVE key.device_id = device_id key.first_activated_at = now key.last_active_at = now # 设置初始到期时间(Auto) if key.membership_type == KeyMembershipType.AUTO and key.duration_days > 0: key.expire_at = now + timedelta(days=key.duration_days) if device_id: ok, bind_msg = KeyService._ensure_device_bound(db, key, device_id) if not ok: return False, bind_msg, None db.commit() return True, "激活成功", key @staticmethod def enable_seamless(db: Session, key: ActivationKey, device_id: str) -> Tuple[bool, str, Optional[CursorAccount]]: """ 启用无感换号并分配账号 返回: (成功, 消息, 账号) """ # 检查密钥状态 if key.status != KeyStatus.ACTIVE: return False, "密钥未激活", None if key.is_expired: key.status = KeyStatus.EXPIRED db.commit() return False, "密钥已过期", None # 如果已启用且有账号,直接返回 if key.seamless_enabled and key.current_account_id: account = AccountService.get_by_id(db, key.current_account_id) if account and account.status == AccountStatus.IN_USE: return True, "无感换号已启用", account # 分配账号 account = AccountService.get_available_for_key(db, key) if not account: return False, "无可用账号", None # 锁定账号 AccountService.lock_account(db, account, key.id) # 更新密钥 key.seamless_enabled = True key.current_account_id = account.id key.device_id = device_id key.last_active_at = datetime.now() db.commit() return True, "无感换号已启用", account @staticmethod def disable_seamless(db: Session, key: ActivationKey) -> Tuple[bool, str]: """禁用无感换号""" if key.current_account_id: account = AccountService.get_by_id(db, key.current_account_id) if account: AccountService.release_account(db, account) key.seamless_enabled = False key.current_account_id = None db.commit() return True, "无感换号已禁用" @staticmethod def switch_account(db: Session, key: ActivationKey) -> Tuple[bool, str, Optional[CursorAccount]]: """ 换号 返回: (成功, 消息, 新账号) """ # 检查状态 if key.status != KeyStatus.ACTIVE: return False, "密钥未激活", None if not key.seamless_enabled: return False, "未启用无感换号", None if key.is_expired: key.status = KeyStatus.EXPIRED db.commit() return False, "密钥已过期", None # Pro密钥检查积分 if key.membership_type == KeyMembershipType.PRO: quota_cost = GlobalSettingsService.get_int(db, "pro_quota_per_switch") if key.quota_remaining < quota_cost: return False, f"积分不足,需要{quota_cost},剩余{key.quota_remaining}", None # Auto密钥检查换号冷却时间 if key.membership_type == KeyMembershipType.AUTO: interval_minutes = GlobalSettingsService.get_int(db, "auto_switch_interval") or 0 if interval_minutes > 0 and key.last_switch_at: elapsed = (datetime.now() - key.last_switch_at).total_seconds() remaining_seconds = interval_minutes * 60 - elapsed if remaining_seconds > 0: remaining_minutes = int(remaining_seconds / 60) + 1 return False, f"换号冷却中,请{remaining_minutes}分钟后再试", None # 先查找新账号,再释放旧账号(避免竞态条件) old_account = None if key.current_account_id: old_account = AccountService.get_by_id(db, key.current_account_id) # 分配新账号(排除当前账号) new_account = AccountService.get_available_for_key(db, key) if not new_account: return False, "无可用账号", None # 确认有新账号后,再释放旧账号 if old_account: AccountService.release_account(db, old_account) # 锁定新账号 AccountService.lock_account(db, new_account, key.id) # 更新密钥 key.current_account_id = new_account.id # Pro扣除积分 if key.membership_type == KeyMembershipType.PRO: quota_cost = GlobalSettingsService.get_int(db, "pro_quota_per_switch") key.quota_used += quota_cost key.switch_count += 1 key.last_switch_at = datetime.now() db.commit() # 记录日志 LogService.log( db, key.id, "switch", account_id=new_account.id, success=True, message=f"从 {old_account.email if old_account else 'N/A'} 切换到 {new_account.email}", usage_snapshot={ "old_account": old_account.to_dict() if old_account else None, "new_account": new_account.to_dict() } ) return True, "换号成功", new_account @staticmethod def get_status(db: Session, key: ActivationKey) -> Dict[str, Any]: """获取密钥完整状态""" data = key.to_dict(include_account=True) # 添加账号信息 if key.seamless_enabled and key.current_account_id: account = AccountService.get_by_id(db, key.current_account_id) if account: data["account_info"] = account.to_dict() return data @staticmethod def update(db: Session, key_id: int, **kwargs) -> Optional[ActivationKey]: key = db.query(ActivationKey).filter(ActivationKey.id == key_id).first() if key: for k, v in kwargs.items(): if hasattr(key, k) and v is not None: setattr(key, k, v) db.commit() db.refresh(key) return key @staticmethod def delete(db: Session, key_id: int) -> bool: key = db.query(ActivationKey).filter(ActivationKey.id == key_id).first() if key: # 释放关联账号 if key.current_account_id: account = AccountService.get_by_id(db, key.current_account_id) if account: AccountService.release_account(db, account) # 删除关联设备 db.query(KeyDevice).filter(KeyDevice.key_id == key_id).delete() db.delete(key) db.commit() return True return False @staticmethod def count(db: Session) -> dict: """统计激活码数量""" status_rows = db.query( ActivationKey.status, func.count(ActivationKey.id) ).group_by(ActivationKey.status).all() sc = {s.value: c for s, c in status_rows} type_rows = db.query( ActivationKey.membership_type, func.count(ActivationKey.id) ).group_by(ActivationKey.membership_type).all() tc = {t.value: c for t, c in type_rows} total = sum(sc.values()) return { "total": total, "unused": sc.get(KeyStatus.UNUSED.value, 0), "active": sc.get(KeyStatus.ACTIVE.value, 0), "expired": sc.get(KeyStatus.EXPIRED.value, 0), "auto": tc.get(KeyMembershipType.AUTO.value, 0), "pro": tc.get(KeyMembershipType.PRO.value, 0), } # ==================== 日志服务 ==================== class LogService: """日志服务""" @staticmethod def log( db: Session, key_id: int, action: str, account_id: int = None, ip_address: str = None, user_agent: str = None, device_id: str = None, success: bool = True, message: str = None, usage_snapshot: Dict = None ): log = UsageLog( key_id=key_id, account_id=account_id, action=action, ip_address=ip_address, user_agent=user_agent, device_id=device_id, success=success, message=message, usage_snapshot=usage_snapshot ) db.add(log) db.commit() @staticmethod def get_by_key(db: Session, key_id: int, limit: int = 50) -> List[UsageLog]: return db.query(UsageLog).filter( UsageLog.key_id == key_id ).order_by(UsageLog.created_at.desc()).limit(limit).all() @staticmethod def get_today_count(db: Session) -> int: today = datetime.now().date() return db.query(UsageLog).filter( func.date(UsageLog.created_at) == today ).count() # ==================== 全局设置服务 ==================== class GlobalSettingsService: """全局设置服务""" @staticmethod def init_settings(db: Session): """初始化默认设置""" defaults = GlobalSettings.get_default_settings() for setting in defaults: existing = db.query(GlobalSettings).filter(GlobalSettings.key == setting["key"]).first() if not existing: db.add(GlobalSettings(**setting)) db.commit() @staticmethod def get(db: Session, key: str) -> Optional[str]: setting = db.query(GlobalSettings).filter(GlobalSettings.key == key).first() if setting: return setting.value # 返回默认值 defaults = {s["key"]: s["value"] for s in GlobalSettings.get_default_settings()} return defaults.get(key) @staticmethod def get_int(db: Session, key: str) -> int: value = GlobalSettingsService.get(db, key) return int(value) if value else 0 @staticmethod def set(db: Session, key: str, value: str, description: str = None): setting = db.query(GlobalSettings).filter(GlobalSettings.key == key).first() if setting: setting.value = value if description: setting.description = description else: setting = GlobalSettings(key=key, value=value, description=description) db.add(setting) db.commit() @staticmethod def get_all(db: Session) -> dict: settings = db.query(GlobalSettings).all() result = {} for s in settings: if s.value_type == "int": result[s.key] = int(s.value) elif s.value_type == "float": result[s.key] = float(s.value) elif s.value_type == "bool": result[s.key] = s.value.lower() in ("true", "1", "yes") else: result[s.key] = s.value return result @staticmethod def update_all(db: Session, **kwargs): """批量更新设置""" for key, value in kwargs.items(): if value is not None: GlobalSettingsService.set(db, key, str(value)) # ==================== 批量操作服务 ==================== class BatchService: """批量操作服务 - 用于管理后台批量处理密钥""" @staticmethod def extend_keys( db: Session, key_ids: List[int], extend_days: int = 0, add_quota: int = 0 ) -> dict: """ 批量延长密钥 - extend_days: 延长天数 (Auto和Pro都可用) - add_quota: 增加额度 (仅Pro有效) """ success = 0 failed = 0 errors = [] for key_id in key_ids: try: key = db.query(ActivationKey).filter(ActivationKey.id == key_id).first() if not key: failed += 1 errors.append(f"ID {key_id}: 密钥不存在") continue # 延长时间 if extend_days > 0: if key.expire_at: # 如果已过期,从今天开始算 if key.expire_at < datetime.now(): key.expire_at = datetime.now() + timedelta(days=extend_days) else: key.expire_at = key.expire_at + timedelta(days=extend_days) else: # 首次设置过期时间 key.expire_at = datetime.now() + timedelta(days=extend_days) # 增加额度 (仅Pro) if add_quota > 0 and key.membership_type == KeyMembershipType.PRO: key.quota = (key.quota or 0) + add_quota success += 1 except Exception as e: failed += 1 errors.append(f"ID {key_id}: {str(e)}") db.commit() return { "success": success, "failed": failed, "errors": errors[:20] } @staticmethod def get_keys_for_compensation( db: Session, membership_type: KeyMembershipType = None, activated_before: datetime = None, not_expired_on: datetime = None ) -> List[ActivationKey]: """ 获取符合补偿条件的密钥列表 参数: - membership_type: 筛选密钥类型 (AUTO/PRO) - activated_before: 在此日期之前激活 - not_expired_on: 在此日期时还未过期 """ query = db.query(ActivationKey).filter( ActivationKey.first_activated_at != None # 必须已激活 ) # 类型筛选 if membership_type: query = query.filter(ActivationKey.membership_type == membership_type) # 激活时间筛选 if activated_before: query = query.filter(ActivationKey.first_activated_at < activated_before) # 在指定日期时未过期 (expire_at > not_expired_on 或 expire_at 为空) if not_expired_on: query = query.filter( or_( ActivationKey.expire_at > not_expired_on, ActivationKey.expire_at == None ) ) return query.all() @staticmethod def batch_compensate( db: Session, membership_type: KeyMembershipType = None, activated_before: datetime = None, not_expired_on: datetime = None, extend_days: int = 0, add_quota: int = 0 ) -> dict: """ 批量补偿密钥 补偿逻辑: - 如果密钥当前未过期: expire_at += extend_days - 如果密钥已过期(但符合补偿条件): expire_at = 今天 + extend_days 参数: - membership_type: 筛选密钥类型 - activated_before: 在此日期之前激活 - not_expired_on: 在此日期时还未过期 - extend_days: 延长天数 - add_quota: 增加额度 (仅Pro) """ # 获取符合条件的密钥 keys = BatchService.get_keys_for_compensation( db, membership_type, activated_before, not_expired_on ) if not keys: return { "success": 0, "failed": 0, "total_matched": 0, "message": "没有找到符合条件的密钥" } success = 0 failed = 0 errors = [] now = datetime.now() for key in keys: try: # 延长时间 if extend_days > 0: if key.expire_at: if key.expire_at < now: # 已过期,从今天开始 key.expire_at = now + timedelta(days=extend_days) # 恢复状态 if key.status == KeyStatus.EXPIRED: key.status = KeyStatus.ACTIVE else: # 未过期,在原基础上延长 key.expire_at = key.expire_at + timedelta(days=extend_days) else: key.expire_at = now + timedelta(days=extend_days) # 增加额度 (仅Pro) if add_quota > 0 and key.membership_type == KeyMembershipType.PRO: key.quota = (key.quota or 0) + add_quota success += 1 except Exception as e: failed += 1 errors.append(f"密钥 {key.key[:8]}...: {str(e)}") db.commit() return { "success": success, "failed": failed, "total_matched": len(keys), "extend_days": extend_days, "add_quota": add_quota, "errors": errors[:20], "message": f"成功补偿 {success} 个密钥" + (f",{failed} 个失败" if failed > 0 else "") }