优化登录任务界面 + 修复时间显示 + 增加删除功能
- 筛选栏改为单行横排,概览精简为一行文字 - 任务列表全宽展示,日志移到底部可折叠 - 页面使用百分比布局,一屏展示无外层滚动 - 登录任务增加单条删除和批量删除功能 - 后端: datetime.utcnow 替换为 datetime.now(timezone.utc) - 后端: schemas 序列化统一输出带时区 ISO 格式 - 前端: 新增 utils/time.ts 统一时间格式化工具 - 前端: CookiePage/LoginTasksPage 时间列使用 formatTime() Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8771d91a30
commit
72bcd1274c
+11
-6
@@ -1,6 +1,6 @@
|
||||
"""ORM 模型"""
|
||||
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from sqlalchemy import (
|
||||
Column, Integer, String, Boolean, Text, DateTime, ForeignKey, JSON,
|
||||
)
|
||||
@@ -8,6 +8,11 @@ from sqlalchemy.orm import relationship
|
||||
from .database import Base
|
||||
|
||||
|
||||
def _utcnow():
|
||||
"""返回时区感知的 UTC 当前时间,替代 _utcnow()。"""
|
||||
return datetime.now(timezone.utc)
|
||||
|
||||
|
||||
class User(Base):
|
||||
"""系统用户"""
|
||||
__tablename__ = "users"
|
||||
@@ -19,8 +24,8 @@ class User(Base):
|
||||
is_active = Column(Boolean, default=True)
|
||||
remark = Column(String(256), default="")
|
||||
custom_permissions = Column(JSON, nullable=True, comment="自定义权限列表,null表示使用角色默认权限")
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
created_at = Column(DateTime, default=_utcnow)
|
||||
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
|
||||
|
||||
# 客服被分配的账号
|
||||
assigned_accounts = relationship("Account", back_populates="assigned_user", foreign_keys="Account.assigned_to")
|
||||
@@ -40,7 +45,7 @@ class Account(Base):
|
||||
assigned_to = Column(Integer, ForeignKey("users.id"), nullable=True, index=True)
|
||||
tag = Column(String(64), default="")
|
||||
remark = Column(String(256), default="")
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
created_at = Column(DateTime, default=_utcnow)
|
||||
|
||||
assigned_user = relationship("User", back_populates="assigned_accounts", foreign_keys=[assigned_to])
|
||||
login_tasks = relationship("LoginTask", back_populates="account")
|
||||
@@ -57,7 +62,7 @@ class LoginTask(Base):
|
||||
cookie = Column(Text, default="")
|
||||
message = Column(String(512), default="")
|
||||
created_by = Column(Integer, ForeignKey("users.id"), nullable=False)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
created_at = Column(DateTime, default=_utcnow)
|
||||
finished_at = Column(DateTime, nullable=True)
|
||||
|
||||
account = relationship("Account", back_populates="login_tasks")
|
||||
@@ -88,4 +93,4 @@ class AuditLog(Base):
|
||||
action = Column(String(128), nullable=False)
|
||||
target = Column(String(256), default="")
|
||||
detail = Column(Text, default="")
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
created_at = Column(DateTime, default=_utcnow)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
"""Cookie 管理路由"""
|
||||
|
||||
from datetime import timezone
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from fastapi.responses import StreamingResponse
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -11,6 +12,15 @@ from ..models import User, LoginTask, Account
|
||||
from ..deps import get_current_user, require_permission
|
||||
from ..permissions import has_permission
|
||||
|
||||
|
||||
def _fmt_dt(dt) -> str | None:
|
||||
"""将 datetime 格式化为带时区的 ISO 字符串。"""
|
||||
if dt is None:
|
||||
return None
|
||||
if dt.tzinfo is None:
|
||||
dt = dt.replace(tzinfo=timezone.utc)
|
||||
return dt.isoformat()
|
||||
|
||||
router = APIRouter(prefix="/api/cookies", tags=["Cookie管理"])
|
||||
|
||||
|
||||
@@ -39,7 +49,7 @@ def list_cookies(
|
||||
"account_username": acc.username if acc else "",
|
||||
"assigned_to": acc.assigned_to,
|
||||
"assigned_username": acc.assigned_user.username if acc and acc.assigned_user else None,
|
||||
"created_at": t.finished_at.isoformat() if t.finished_at else None,
|
||||
"created_at": _fmt_dt(t.finished_at),
|
||||
}
|
||||
# 只有有 cookie:view 权限才返回 cookie 内容
|
||||
if has_permission(current.role, "cookie:view"):
|
||||
@@ -70,7 +80,7 @@ def export_cookies(
|
||||
for t in tasks:
|
||||
acc = db.query(Account).filter(Account.id == t.account_id).first()
|
||||
username = acc.username if acc else ""
|
||||
writer.writerow([username, t.cookie or "", t.finished_at.isoformat() if t.finished_at else ""])
|
||||
writer.writerow([username, t.cookie or "", _fmt_dt(t.finished_at) or ""])
|
||||
|
||||
output.seek(0)
|
||||
return StreamingResponse(
|
||||
|
||||
@@ -111,6 +111,38 @@ def list_tasks(
|
||||
return result
|
||||
|
||||
|
||||
@router.delete("/tasks/{task_id}")
|
||||
def delete_task(
|
||||
task_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(require_permission("login:batch")),
|
||||
):
|
||||
"""删除单个登录任务。"""
|
||||
task = db.query(LoginTask).filter(LoginTask.id == task_id).first()
|
||||
if not task:
|
||||
raise HTTPException(status_code=404, detail="任务不存在")
|
||||
db.delete(task)
|
||||
db.commit()
|
||||
return {"message": "已删除", "success": True}
|
||||
|
||||
|
||||
@router.delete("/tasks")
|
||||
def delete_tasks(
|
||||
task_ids: str = "",
|
||||
db: Session = Depends(get_db),
|
||||
current: User = Depends(require_permission("login:batch")),
|
||||
):
|
||||
"""批量删除登录任务。"""
|
||||
if not task_ids:
|
||||
raise HTTPException(status_code=400, detail="请指定任务ID")
|
||||
ids = [int(x) for x in task_ids.split(",") if x.strip().isdigit()]
|
||||
if not ids:
|
||||
raise HTTPException(status_code=400, detail="无效的任务ID")
|
||||
deleted = db.query(LoginTask).filter(LoginTask.id.in_(ids)).delete(synchronize_session=False)
|
||||
db.commit()
|
||||
return {"message": f"已删除 {deleted} 个任务", "deleted": deleted, "success": True}
|
||||
|
||||
|
||||
@router.post("/stop/{batch_id}")
|
||||
def stop_batch(
|
||||
batch_id: str,
|
||||
|
||||
+58
-9
@@ -1,8 +1,17 @@
|
||||
"""Pydantic 请求/响应模型"""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
from pydantic import BaseModel, Field
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional, Any
|
||||
from pydantic import BaseModel, Field, ConfigDict, model_serializer
|
||||
|
||||
|
||||
def _ensure_tz(dt: Optional[datetime]) -> Optional[datetime]:
|
||||
"""确保 datetime 带有 UTC 时区信息,无时区的视为 UTC。"""
|
||||
if dt is None:
|
||||
return None
|
||||
if dt.tzinfo is None:
|
||||
return dt.replace(tzinfo=timezone.utc)
|
||||
return dt
|
||||
|
||||
|
||||
# ---- 认证 ----
|
||||
@@ -29,8 +38,20 @@ class UserInfo(BaseModel):
|
||||
permissions: list[str] = []
|
||||
custom_permissions: Optional[list[str]] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@model_serializer
|
||||
def _serialize(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"username": self.username,
|
||||
"role": self.role,
|
||||
"is_active": self.is_active,
|
||||
"remark": self.remark,
|
||||
"created_at": _ensure_tz(self.created_at).isoformat() if self.created_at else None,
|
||||
"permissions": self.permissions,
|
||||
"custom_permissions": self.custom_permissions,
|
||||
}
|
||||
|
||||
|
||||
# ---- 用户管理 ----
|
||||
@@ -82,8 +103,22 @@ class AccountOut(BaseModel):
|
||||
remark: str = ""
|
||||
created_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@model_serializer
|
||||
def _serialize(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"username": self.username,
|
||||
"password": self.password,
|
||||
"email": self.email,
|
||||
"email_password": self.email_password,
|
||||
"tag": self.tag,
|
||||
"assigned_to": self.assigned_to,
|
||||
"assigned_username": self.assigned_username,
|
||||
"remark": self.remark,
|
||||
"created_at": _ensure_tz(self.created_at).isoformat() if self.created_at else None,
|
||||
}
|
||||
|
||||
|
||||
# ---- 登录任务 ----
|
||||
@@ -105,8 +140,22 @@ class LoginTaskOut(BaseModel):
|
||||
created_at: Optional[datetime] = None
|
||||
finished_at: Optional[datetime] = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
|
||||
@model_serializer
|
||||
def _serialize(self) -> dict[str, Any]:
|
||||
return {
|
||||
"id": self.id,
|
||||
"batch_id": self.batch_id,
|
||||
"account_id": self.account_id,
|
||||
"account_username": self.account_username,
|
||||
"status": self.status,
|
||||
"cookie": self.cookie,
|
||||
"message": self.message,
|
||||
"created_by": self.created_by,
|
||||
"created_at": _ensure_tz(self.created_at).isoformat() if self.created_at else None,
|
||||
"finished_at": _ensure_tz(self.finished_at).isoformat() if self.finished_at else None,
|
||||
}
|
||||
|
||||
|
||||
# ---- 代理配置 ----
|
||||
|
||||
@@ -4,7 +4,7 @@ import asyncio
|
||||
import threading
|
||||
import uuid
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from datetime import datetime
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
@@ -144,7 +144,7 @@ class LoginBatchRunner:
|
||||
task.message = str(e)
|
||||
self._push_log("error", f"[{current}] {acc_info['username']} 登录异常: {e}")
|
||||
|
||||
task.finished_at = datetime.utcnow()
|
||||
task.finished_at = datetime.now(timezone.utc)
|
||||
worker_db.commit()
|
||||
|
||||
finally:
|
||||
@@ -211,7 +211,7 @@ class LoginBatchRunner:
|
||||
if task:
|
||||
task.status = "error"
|
||||
task.message = f"代理不可用: {proxy_msg}"
|
||||
task.finished_at = datetime.utcnow()
|
||||
task.finished_at = datetime.now(timezone.utc)
|
||||
worker_db.commit()
|
||||
finally:
|
||||
worker_db.close()
|
||||
|
||||
Reference in New Issue
Block a user