fix: harden user management operations

This commit is contained in:
yml2213
2026-08-13 15:07:33 +08:00
parent b07c5fdbf6
commit 090698e6b2
9 changed files with 286 additions and 33 deletions
@@ -0,0 +1,36 @@
"""支持用户软删除并保留历史任务归属"""
from typing import Sequence, Union
from alembic import op
import sqlalchemy as sa
revision: str = "20260813_0023"
down_revision: Union[str, None] = "20260812_0022"
branch_labels: Union[str, Sequence[str], None] = None
depends_on: Union[str, Sequence[str], None] = None
def _columns(bind, table_name: str) -> set[str]:
return {column["name"] for column in sa.inspect(bind).get_columns(table_name)}
def upgrade() -> None:
bind = op.get_bind()
columns = _columns(bind, "users")
if "deleted_at" not in columns:
op.add_column("users", sa.Column("deleted_at", sa.DateTime(), nullable=True))
op.create_index("ix_users_deleted_at", "users", ["deleted_at"])
if "deleted_username" not in columns:
op.add_column("users", sa.Column("deleted_username", sa.String(length=64), nullable=True))
def downgrade() -> None:
bind = op.get_bind()
columns = _columns(bind, "users")
if "deleted_username" in columns:
op.drop_column("users", "deleted_username")
if "deleted_at" in columns:
op.drop_index("ix_users_deleted_at", table_name="users")
op.drop_column("users", "deleted_at")
+2
View File
@@ -34,6 +34,8 @@ class User(Base):
custom_permissions = Column(JSON, nullable=True, comment="自定义权限列表,null表示使用角色默认权限")
created_at = Column(DateTime, default=_utcnow)
updated_at = Column(DateTime, default=_utcnow, onupdate=_utcnow)
deleted_at = Column(DateTime, nullable=True, index=True)
deleted_username = Column(String(64), default="")
# 客服被分配的账号
assigned_accounts = relationship("Account", back_populates="assigned_user", foreign_keys="Account.assigned_to")
+1
View File
@@ -6,6 +6,7 @@ PERMISSIONS = {
"user:view": "查看用户列表",
"user:create": "创建用户",
"user:edit": "编辑用户",
"user:rename": "修改用户名",
"user:delete": "删除用户",
"user:assign_permissions": "分配权限",
# 账号管理
+84 -8
View File
@@ -1,14 +1,18 @@
"""用户管理路由(超管)"""
from datetime import datetime, timezone
from uuid import uuid4
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from ..database import get_db
from ..models import User, AuditLog
from ..models import Account, AuditLog, HuyaAccount, User
from ..security import hash_password
from ..permissions import ROLE_LABELS, PERMISSIONS, get_user_permissions
from ..schemas import UserCreate, UserUpdate, UserInfo
from ..deps import require_permission, get_current_user
from ..permissions import PERMISSIONS, get_user_permissions
from ..schemas import UserCreate, UserRename, UserUpdate, UserInfo
from ..deps import require_permission
router = APIRouter(prefix="/api/users", tags=["用户管理"])
@@ -18,7 +22,7 @@ def list_users(
db: Session = Depends(get_db),
_: User = Depends(require_permission("user:view")),
):
users = db.query(User).order_by(User.id).all()
users = db.query(User).filter(User.deleted_at.is_(None)).order_by(User.id).all()
result = []
for u in users:
result.append(UserInfo(
@@ -76,6 +80,8 @@ def update_user(
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if user.deleted_at is not None:
raise HTTPException(status_code=404, detail="用户不存在")
if req.password:
user.password_hash = hash_password(req.password)
@@ -103,6 +109,51 @@ def update_user(
)
@router.patch("/{user_id}/username", response_model=UserInfo)
def rename_user(
user_id: int,
req: UserRename,
db: Session = Depends(get_db),
current: User = Depends(require_permission("user:rename")),
):
"""仅变更用户名,保留用户 ID 和全部关联业务数据。"""
user = db.query(User).filter(User.id == user_id, User.deleted_at.is_(None)).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
username = req.username.strip()
if len(username) < 2:
raise HTTPException(status_code=400, detail="用户名至少 2 个字符")
if username == user.username:
return UserInfo(
id=user.id, username=user.username, role=user.role,
is_active=user.is_active, remark=user.remark or "",
permissions=get_user_permissions(user), custom_permissions=user.custom_permissions,
)
if db.query(User.id).filter(User.username == username).first():
raise HTTPException(status_code=400, detail="用户名已存在")
old_username = user.username
user.username = username
db.add(AuditLog(
user_id=current.id,
username=current.username,
action="user:rename",
target=f"{old_username} -> {username}",
))
try:
db.commit()
except IntegrityError as exc:
db.rollback()
raise HTTPException(status_code=400, detail="用户名已存在") from exc
db.refresh(user)
return UserInfo(
id=user.id, username=user.username, role=user.role,
is_active=user.is_active, remark=user.remark or "",
permissions=get_user_permissions(user), custom_permissions=user.custom_permissions,
)
@router.delete("/{user_id}")
def delete_user(
user_id: int,
@@ -112,14 +163,39 @@ def delete_user(
user = db.query(User).filter(User.id == user_id).first()
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
if user.deleted_at is not None:
raise HTTPException(status_code=404, detail="用户不存在")
if user.role == "super_admin":
raise HTTPException(status_code=400, detail="不能删除超级管理员")
if user.id == current.id:
raise HTTPException(status_code=400, detail="不能删除当前登录用户")
# 保留任务与审计记录的创建者 ID,避免删除用户时破坏历史数据。
released_accounts = (
db.query(Account)
.filter(Account.assigned_to == user.id)
.update({Account.assigned_to: None}, synchronize_session=False)
)
released_huya_accounts = (
db.query(HuyaAccount)
.filter(HuyaAccount.assigned_to == user.id)
.update({HuyaAccount.assigned_to: None}, synchronize_session=False)
)
deleted_username = user.username
db.add(AuditLog(user_id=current.id, username=current.username,
action="user:delete", target=user.username))
db.delete(user)
action="user:delete", target=deleted_username))
user.is_active = False
user.deleted_at = datetime.now(timezone.utc)
user.deleted_username = deleted_username
# 释放用户名供新用户使用,同时不改变历史任务通过 user.id 保存的归属。
user.username = f"deleted-{user.id}-{uuid4().hex[:12]}"
db.commit()
return {"message": "已删除", "success": True}
return {
"message": "已删除",
"success": True,
"released_accounts": released_accounts,
"released_huya_accounts": released_huya_accounts,
}
@router.get("/permissions/list")
+4
View File
@@ -97,6 +97,10 @@ class UserUpdate(BaseModel):
custom_permissions: Optional[list[str]] = None
class UserRename(BaseModel):
username: str = Field(..., min_length=2, max_length=64)
# ---- 账号 ----
class AccountImport(BaseModel):
"""批量导入,文本格式:用户名|密码|邮箱|邮箱密码|标签(可选)