jerry 3 месяцев назад
Родитель
Сommit
8a85d967e0
5 измененных файлов с 176 добавлено и 1 удалено
  1. 35 0
      app/api/router.py
  2. 20 0
      app/models/account.py
  3. 29 0
      app/schemas/account.py
  4. 91 0
      app/services/account_service.py
  5. 1 1
      app/services/task_handlers.py

+ 35 - 0
app/api/router.py

@@ -52,6 +52,7 @@ from app.schemas.wechat import WechatIn
 from app.schemas.resource import FileUploadOut
 from app.schemas.statistics import VasStatisticsOverviewOut
 from app.schemas.llm import ParseUserInputsPayload, ParseUserInputsOut
+from app.schemas.account import AccountResponse, AccountCreate, LockRequest
 from app.schemas.docker_remote import RemoteServerConfig, DockerStatusOut, DockerLogsRequest, DockerLogsOut, ConfigReadOut, ConfigReadRequest, ConfigUpdateRequest, LogReadRequest, LogReadOut, LogListOut, DockerContainerStatus, DockerActionRequest, ServerConfigItem, ServerListOut, RemoteActionRequest
 from app.services.docker_remote_service import DockerRemoteService
 from app.services.configuration_service import ConfigurationService
@@ -83,6 +84,7 @@ from app.services.slot_snapshot_service import SlotSnapshotService
 from app.services.statistics_service import StatisticsService
 from app.services.llm_service import LlmService
 from app.services.slot_refresh_status_service import SlotRefreshStatusService
+from app.services.account_service import AccountService
 
 # 公共路由
 public_router = APIRouter()
@@ -558,6 +560,39 @@ async def email_authorizations_send_email_bulk(
     )
     return success(data={"body": result})
 
+@admin_required_router.get("/account/next", summary="获取下一个账号", tags=["账号管理"], response_model=ApiResponse[AccountResponse])
+async def account_next(
+    pool_name: str, 
+    lock_duration: float = 60.0, 
+    db: AsyncSession = Depends(get_db)
+):
+    account = await AccountService.get_next_account(db, pool_name, lock_duration)
+    return success(data=account)
+
+@admin_required_router.post("/account/add", summary="新增一个账号", tags=["账号管理"], response_model=ApiResponse[AccountResponse])
+async def account_add(
+    payload: AccountCreate,
+    db: AsyncSession = Depends(get_db)
+):
+    account = await AccountService.add_account(db, payload)
+    return success(data=account)
+   
+@admin_required_router.post("/account/lock", summary="锁定账号", tags=["账号管理"], response_model=ApiResponse)
+async def account_lock(
+    payload: LockRequest,
+    db: AsyncSession = Depends(get_db)
+):
+    await AccountManager.manual_lock(db, payload)
+    return success()
+
+@admin_required_router.post("/account/disable", summary="禁用账号", tags=["账号管理"], response_model=ApiResponse)
+async def account_disable(
+    payload: LockRequest,
+    db: AsyncSession = Depends(get_db)
+):
+    await AccountManager.disable_account(db, payload)
+    return success()
+
 @public_router.post("/resource/upload_file", summary="上传文件", tags=["文件管理"], response_model=ApiResponse[FileUploadOut])
 async def resource_upload_file(file: UploadFile = File(...)):
     result = await SeaweedFSService.upload(file)

+ 20 - 0
app/models/account.py

@@ -0,0 +1,20 @@
+from sqlalchemy import Column, Integer, String, Text, Float, JSON, TIMESTAMP
+from sqlalchemy.sql import func
+from sqlalchemy.ext.declarative import declarative_base
+from app.core.database import Base
+
+
+class Account(Base):
+    __tablename__ = "account"
+
+    id = Column(Integer, primary_key=True, index=True)
+    pool_name = Column(String(50), nullable=False, index=True)
+    username = Column(String(100), nullable=False)
+    password = Column(String(255), nullable=True)
+    extra_data = Column(JSON, nullable=True) # 存储 Cookies 等
+    
+    # 锁定截止时间戳 (0 表示未锁定,> time.time() 表示锁定中)
+    lock_until = Column(Float, default=0, index=True)
+    
+    # 状态: active, disabled, removed
+    status = Column(String(20), default="active", index=True)

+ 29 - 0
app/schemas/account.py

@@ -0,0 +1,29 @@
+from pydantic import BaseModel
+from typing import Optional, Dict, Any
+from app.schemas.common import ApiResponse
+from app.schemas.user import VasUserOut
+
+
+# --- 4. 请求/响应 Schema (Pydantic) ---
+class AccountCreate(BaseModel):
+    pool_name: str
+    username: str
+    password: Optional[str] = None
+    extra_data: Optional[Dict[str, Any]] = None
+
+class AccountResponse(BaseModel):
+    id: int
+    pool_name: str
+    username: str
+    password: Optional[str] = None
+    lock_until: float
+    extra_data: Optional[Dict[str, Any]] = None
+    
+    model_config = {
+        "from_attributes": True
+    }
+
+class LockRequest(BaseModel):
+    pool_name: str
+    username: str
+    duration: Optional[int] = None

+ 91 - 0
app/services/account_service.py

@@ -0,0 +1,91 @@
+import time
+from typing import Optional
+from sqlalchemy.ext.asyncio import AsyncSession
+from sqlalchemy import select, delete
+from app.core.biz_exception import NotFoundError
+from app.models.account import Account
+from app.schemas.account import AccountCreate, LockRequest
+
+class AccountService:
+    
+    @staticmethod
+    async def add_account(db: AsyncSession, payload: AccountCreate):
+        """
+        添加新账号到数据库
+        """
+        # 检查是否存在
+        stmt = select(Account).where(
+            Account.pool_name == payload.pool_name,
+            Account.username == payload.username,
+        )
+        obj = (await db.execute(stmt)).scalar_one_or_none()
+        
+        if obj:
+            # 如果存在,更新密码,并重置为 active
+            obj.password = payload.password
+            obj.status = "active"
+            if payload.extra_data:
+                obj.extra_data = payload.extra_data
+            await db.commit()
+            await db.refresh(obj)
+            return obj
+        else:
+            new_acc = Account(
+                pool_name=payload.pool_name,
+                username=payload.username, 
+                password=payload.password,
+                extra_data=payload.extra_data
+            )
+            db.add(new_acc)
+            await db.commit()
+            await db.refresh(new_acc)
+            return new_acc
+    
+    @staticmethod
+    async def get_next_account(db: AsyncSession, pool_name: str, lock_duration: float) -> Account:
+        now = time.time()        
+        stmt = select(Account).where(
+            Account.pool_name == pool_name,
+            Account.status == 'active',
+            Account.lock_until < now
+        ).order_by(
+            Account.lock_until.asc()
+        ).limit(1).with_for_update()
+        
+        obj = (await db.execute(stmt)).scalar_one_or_none()
+        
+        if not obj:
+            raise NotFoundError('Account not found')
+        
+        new_lock_time = now + lock_duration
+        obj.lock_until = new_lock_time
+        await db.commit()
+        await db.refresh(obj)
+        return obj
+                
+    @staticmethod
+    async def manual_lock(db: AsyncSession, payload: LockRequest):        
+        stmt = select(Account).where(
+            Account.pool_name == payload.pool_name,
+            Account.username == payload.username,
+        )
+        obj = (await db.execute(stmt)).scalar_one_or_none()
+        
+        if not obj:
+            raise NotFoundError('Account not found')
+        
+        obj.lock_until = time.time() + payload.duration
+        await db.commit()
+
+    @staticmethod
+    async def disable_account(db: AsyncSession, payload: LockRequest):
+        stmt = select(Account).where(
+            Account.pool_name == payload.pool_name,
+            Account.username == payload.username,
+        )
+        obj = (await db.execute(stmt)).scalar_one_or_none()
+        
+        if not obj:
+            raise NotFoundError('Account not found')
+        obj.status = "disabled"
+        await db.commit()

+ 1 - 1
app/services/task_handlers.py

@@ -50,7 +50,7 @@ class TaskHandlerRegistry:
         
         if not handler:
             # 如果没有注册处理器,通常直接跳过即可
-            logger.debug(f"ℹ️ [TaskHandler] No handler found for routing_key: {routing_key}, skipping.")
+            logger.info(f"ℹ️ [TaskHandler] No handler found for routing_key: {routing_key}, skipping.")
             return
 
         try: