backend v2.1: 公告管理功能 + 系统重构
- 新增 Announcement 数据模型,支持公告的增删改查 - 后台管理新增"公告管理"Tab(创建/编辑/删除/启用禁用) - 客户端 /api/announcement 改为从数据库读取 - 账号服务重构,新增无感换号、自动分析等功能 - 新增后台任务调度器、数据库迁移脚本 - Schema/Service/Config 全面升级至 v2.1 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,6 @@
|
||||
from app.services.account_service import AccountService, KeyService, LogService, GlobalSettingsService, BatchService
|
||||
from app.services.account_service import (
|
||||
AccountService, KeyService, LogService, GlobalSettingsService, BatchService, generate_key
|
||||
)
|
||||
from app.services.auth_service import authenticate_admin, create_access_token, get_current_user
|
||||
from app.services.cursor_usage_service import (
|
||||
CursorUsageService,
|
||||
@@ -7,5 +9,9 @@ from app.services.cursor_usage_service import (
|
||||
check_account_valid,
|
||||
get_account_usage,
|
||||
batch_check_accounts,
|
||||
check_and_classify_account
|
||||
check_and_classify_account,
|
||||
analyze_account_from_token,
|
||||
quick_validate_token,
|
||||
map_membership_to_account_type,
|
||||
calculate_usage_percent
|
||||
)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
from jose import JWTError, jwt
|
||||
from passlib.context import CryptContext
|
||||
@@ -21,9 +21,9 @@ def get_password_hash(password: str) -> str:
|
||||
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
||||
to_encode = data.copy()
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
expire = datetime.now(timezone.utc) + expires_delta
|
||||
else:
|
||||
expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
expire = datetime.now(timezone.utc) + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
to_encode.update({"exp": expire})
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
"""
|
||||
Cursor 官方用量 API 服务
|
||||
Cursor 官方用量 API 服务 v2.1
|
||||
用于验证账号有效性和查询用量信息
|
||||
"""
|
||||
import httpx
|
||||
@@ -7,6 +7,7 @@ import asyncio
|
||||
from typing import Optional, Dict, Any, Tuple, List
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from decimal import Decimal
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -289,27 +290,30 @@ async def get_account_usage(token: str) -> Dict[str, Any]:
|
||||
}
|
||||
|
||||
|
||||
async def batch_check_accounts(tokens: List[str]) -> List[Dict[str, Any]]:
|
||||
async def batch_check_accounts(tokens: List[str], max_concurrency: int = 5) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
批量检查多个账号
|
||||
批量检查多个账号(并发,带限流)
|
||||
"""
|
||||
results = []
|
||||
for token in tokens:
|
||||
info = await cursor_usage_service.validate_and_get_usage(token)
|
||||
results.append({
|
||||
"token": token[:20] + "...", # 脱敏
|
||||
"is_valid": info.is_valid,
|
||||
"is_usable": info.is_usable,
|
||||
"pool_type": info.pool_type, # pro/auto
|
||||
"membership_type": info.membership_type if info.is_valid else None,
|
||||
"days_remaining_on_trial": info.days_remaining_on_trial,
|
||||
"plan_used": info.plan_used if info.is_valid else 0,
|
||||
"plan_limit": info.plan_limit if info.is_valid else 0,
|
||||
"plan_remaining": info.plan_remaining if info.is_valid else 0,
|
||||
"total_requests": info.total_requests if info.is_valid else 0,
|
||||
"error": info.error_message
|
||||
})
|
||||
return results
|
||||
semaphore = asyncio.Semaphore(max_concurrency)
|
||||
|
||||
async def _check_one(token: str) -> Dict[str, Any]:
|
||||
async with semaphore:
|
||||
info = await cursor_usage_service.validate_and_get_usage(token)
|
||||
return {
|
||||
"token": token[:20] + "...",
|
||||
"is_valid": info.is_valid,
|
||||
"is_usable": info.is_usable,
|
||||
"pool_type": info.pool_type,
|
||||
"membership_type": info.membership_type if info.is_valid else None,
|
||||
"days_remaining_on_trial": info.days_remaining_on_trial,
|
||||
"plan_used": info.plan_used if info.is_valid else 0,
|
||||
"plan_limit": info.plan_limit if info.is_valid else 0,
|
||||
"plan_remaining": info.plan_remaining if info.is_valid else 0,
|
||||
"total_requests": info.total_requests if info.is_valid else 0,
|
||||
"error": info.error_message
|
||||
}
|
||||
|
||||
return await asyncio.gather(*[_check_one(t) for t in tokens])
|
||||
|
||||
|
||||
async def check_and_classify_account(token: str) -> Dict[str, Any]:
|
||||
@@ -335,3 +339,95 @@ async def check_and_classify_account(token: str) -> Dict[str, Any]:
|
||||
"total_requests": info.total_requests,
|
||||
"recommendation": f"建议放入 {'Pro' if info.pool_type == 'pro' else 'Auto'} 号池"
|
||||
}
|
||||
|
||||
|
||||
# ============ 数据库集成函数 ============
|
||||
|
||||
def map_membership_to_account_type(membership_type: str) -> str:
|
||||
"""
|
||||
将 Cursor API 的 membershipType 映射到 AccountType
|
||||
"""
|
||||
mapping = {
|
||||
"free_trial": "free_trial",
|
||||
"pro": "pro",
|
||||
"free": "free",
|
||||
"business": "business",
|
||||
"enterprise": "business",
|
||||
}
|
||||
return mapping.get(membership_type, "unknown")
|
||||
|
||||
|
||||
def calculate_usage_percent(used: int, limit: int) -> Decimal:
|
||||
"""计算用量百分比"""
|
||||
if limit <= 0:
|
||||
return Decimal("0")
|
||||
return Decimal(str(round(used / limit * 100, 2)))
|
||||
|
||||
|
||||
async def analyze_account_from_token(token: str) -> Dict[str, Any]:
|
||||
"""
|
||||
分析账号Token,返回所有需要更新到数据库的字段
|
||||
用于后台分析任务
|
||||
"""
|
||||
info = await cursor_usage_service.validate_and_get_usage(token)
|
||||
|
||||
if not info.is_valid:
|
||||
return {
|
||||
"success": False,
|
||||
"error": info.error_message,
|
||||
"status": "invalid"
|
||||
}
|
||||
|
||||
# 计算用量百分比
|
||||
usage_percent = calculate_usage_percent(info.plan_used, info.plan_limit)
|
||||
|
||||
# 确定账号状态
|
||||
if usage_percent >= Decimal("95"):
|
||||
status = "exhausted"
|
||||
else:
|
||||
status = "available"
|
||||
|
||||
# 解析计费周期时间
|
||||
billing_start = None
|
||||
billing_end = None
|
||||
try:
|
||||
if info.billing_cycle_start:
|
||||
billing_start = datetime.fromisoformat(info.billing_cycle_start.replace('Z', '+00:00'))
|
||||
if info.billing_cycle_end:
|
||||
billing_end = datetime.fromisoformat(info.billing_cycle_end.replace('Z', '+00:00'))
|
||||
except:
|
||||
pass
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"status": status,
|
||||
"account_type": map_membership_to_account_type(info.membership_type),
|
||||
"membership_type": info.membership_type,
|
||||
"billing_cycle_start": billing_start,
|
||||
"billing_cycle_end": billing_end,
|
||||
"trial_days_remaining": info.days_remaining_on_trial or 0,
|
||||
"usage_limit": info.plan_limit,
|
||||
"usage_used": info.plan_used,
|
||||
"usage_remaining": info.plan_remaining,
|
||||
"usage_percent": usage_percent,
|
||||
"total_requests": info.total_requests,
|
||||
"total_input_tokens": info.total_input_tokens,
|
||||
"total_output_tokens": info.total_output_tokens,
|
||||
"total_cost_cents": Decimal(str(info.total_cost_cents)),
|
||||
"last_analyzed_at": datetime.now(),
|
||||
"analyze_error": None
|
||||
}
|
||||
|
||||
|
||||
async def quick_validate_token(token: str) -> Tuple[bool, Optional[str]]:
|
||||
"""
|
||||
快速验证Token有效性(仅调用usage-summary)
|
||||
用于激活时的快速检查
|
||||
"""
|
||||
try:
|
||||
resp = await cursor_usage_service.get_usage_summary(token)
|
||||
if resp["success"]:
|
||||
return True, None
|
||||
return False, resp.get("error", "验证失败")
|
||||
except Exception as e:
|
||||
return False, str(e)
|
||||
|
||||
Reference in New Issue
Block a user