feat: 添加软删除功能,更新用户模型和相关路由以支持用户注销和状态检查
This commit is contained in:
parent
43ec5a4a71
commit
c8ea1e7179
@ -14,6 +14,7 @@ services:
|
||||
- ./src:/app/src
|
||||
- ./saves:/app/saves
|
||||
- ./test:/app/test
|
||||
- ./scripts:/app/scripts
|
||||
- ${MODEL_DIR:-./models}:/models # 使用默认值处理未定义的环境变量
|
||||
ports:
|
||||
- "5050:5050"
|
||||
|
||||
@ -108,6 +108,14 @@ async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 检查用户是否已被删除
|
||||
if user.is_deleted:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_403_FORBIDDEN,
|
||||
detail="该账户已注销",
|
||||
headers={"WWW-Authenticate": "Bearer"},
|
||||
)
|
||||
|
||||
# 检查用户是否处于登录锁定状态
|
||||
if user.is_login_locked():
|
||||
remaining_time = user.get_remaining_lock_time()
|
||||
@ -391,14 +399,14 @@ async def create_user(
|
||||
async def read_users(
|
||||
skip: int = 0, limit: int = 100, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db)
|
||||
):
|
||||
users = db.query(User).offset(skip).limit(limit).all()
|
||||
users = db.query(User).filter(User.is_deleted == 0).offset(skip).limit(limit).all()
|
||||
return [user.to_dict() for user in users]
|
||||
|
||||
|
||||
# 路由:获取特定用户信息(管理员权限)
|
||||
@auth.get("/users/{user_id}", response_model=UserResponse)
|
||||
async def read_user(user_id: int, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db)):
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
user = db.query(User).filter(User.id == user_id, User.is_deleted == 0).first()
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
@ -416,7 +424,7 @@ async def update_user(
|
||||
current_user: User = Depends(get_admin_user),
|
||||
db: Session = Depends(get_db),
|
||||
):
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
user = db.query(User).filter(User.id == user_id, User.is_deleted == 0).first()
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
@ -472,7 +480,7 @@ async def update_user(
|
||||
async def delete_user(
|
||||
user_id: int, request: Request, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db)
|
||||
):
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
user = db.query(User).filter(User.id == user_id, User.is_deleted == 0).first()
|
||||
if user is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
@ -489,7 +497,7 @@ async def delete_user(
|
||||
)
|
||||
|
||||
# 检查是否是最后一个超级管理员
|
||||
superadmin_count = db.query(User).filter(User.role == "superadmin").count()
|
||||
superadmin_count = db.query(User).filter(User.role == "superadmin", User.is_deleted == 0).count()
|
||||
if superadmin_count <= 1:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
@ -503,13 +511,28 @@ async def delete_user(
|
||||
detail="不能删除自己的账户",
|
||||
)
|
||||
|
||||
# 检查是否已经被删除
|
||||
if user.is_deleted:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_400_BAD_REQUEST,
|
||||
detail="该用户已经被删除",
|
||||
)
|
||||
|
||||
deletion_detail = f"删除用户: {user.username}, ID: {user.id}, 角色: {user.role}"
|
||||
|
||||
# 清理关联的操作日志,避免外键约束报错
|
||||
db.query(OperationLog).filter(OperationLog.user_id == user.id).delete(synchronize_session=False)
|
||||
# 软删除:标记删除状态并脱敏
|
||||
import hashlib
|
||||
|
||||
# 生成4位哈希(基于user_id保证唯一性)
|
||||
hash_suffix = hashlib.md5(user.user_id.encode()).hexdigest()[:4]
|
||||
|
||||
user.is_deleted = 1
|
||||
user.deleted_at = datetime.now()
|
||||
user.username = f"已注销用户-{hash_suffix}"
|
||||
user.phone_number = None # 清空手机号,释放该手机号供其他用户使用
|
||||
user.password_hash = "DELETED" # 禁止登录
|
||||
user.avatar = None # 清空头像
|
||||
|
||||
# 删除用户
|
||||
db.delete(user)
|
||||
db.commit()
|
||||
|
||||
# 记录操作
|
||||
|
||||
@ -9,7 +9,7 @@ from datetime import datetime, timedelta
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import distinct, func
|
||||
from sqlalchemy import String, cast, distinct, func, or_
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from server.routers.auth_router import get_admin_user
|
||||
@ -233,32 +233,45 @@ async def get_user_activity_stats(
|
||||
|
||||
now = datetime.utcnow()
|
||||
|
||||
# 基础用户统计
|
||||
total_users = db.query(func.count(User.id)).scalar() or 0
|
||||
# Conversations may store either the numeric user primary key or the login user_id string.
|
||||
# Join condition accounts for both representations.
|
||||
user_join_condition = or_(
|
||||
Conversation.user_id == User.user_id,
|
||||
Conversation.user_id == cast(User.id, String),
|
||||
)
|
||||
|
||||
# 不同时间段的活跃用户数(基于对话活动)
|
||||
# 基础用户统计(排除已删除用户)
|
||||
total_users = db.query(func.count(User.id)).filter(User.is_deleted == 0).scalar() or 0
|
||||
|
||||
# 不同时间段的活跃用户数(基于对话活动,排除已删除用户)
|
||||
active_users_24h = (
|
||||
db.query(func.count(distinct(Conversation.user_id)))
|
||||
.filter(Conversation.updated_at >= now - timedelta(days=1))
|
||||
db.query(func.count(distinct(User.id)))
|
||||
.select_from(Conversation)
|
||||
.join(User, user_join_condition)
|
||||
.filter(Conversation.updated_at >= now - timedelta(days=1), User.is_deleted == 0)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
|
||||
active_users_30d = (
|
||||
db.query(func.count(distinct(Conversation.user_id)))
|
||||
.filter(Conversation.updated_at >= now - timedelta(days=30))
|
||||
db.query(func.count(distinct(User.id)))
|
||||
.select_from(Conversation)
|
||||
.join(User, user_join_condition)
|
||||
.filter(Conversation.updated_at >= now - timedelta(days=30), User.is_deleted == 0)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
# 最近7天每日活跃用户
|
||||
# 最近7天每日活跃用户(排除已删除用户)
|
||||
daily_active_users = []
|
||||
for i in range(7):
|
||||
day_start = now - timedelta(days=i + 1)
|
||||
day_end = now - timedelta(days=i)
|
||||
|
||||
active_count = (
|
||||
db.query(func.count(distinct(Conversation.user_id)))
|
||||
.filter(Conversation.updated_at >= day_start, Conversation.updated_at < day_end)
|
||||
db.query(func.count(distinct(User.id)))
|
||||
.select_from(Conversation)
|
||||
.join(User, user_join_condition)
|
||||
.filter(Conversation.updated_at >= day_start, Conversation.updated_at < day_end, User.is_deleted == 0)
|
||||
.scalar()
|
||||
or 0
|
||||
)
|
||||
@ -603,7 +616,7 @@ async def get_dashboard_stats(
|
||||
db.query(func.count(Conversation.id)).filter(Conversation.status == "active").scalar() or 0
|
||||
)
|
||||
total_messages = db.query(func.count(Message.id)).scalar() or 0
|
||||
total_users = db.query(func.count(User.id)).scalar() or 0
|
||||
total_users = db.query(func.count(User.id)).filter(User.is_deleted == 0).scalar() or 0
|
||||
|
||||
# Feedback statistics
|
||||
total_feedbacks = db.query(func.count(MessageFeedback.id)).scalar() or 0
|
||||
|
||||
@ -175,6 +175,10 @@ class User(Base):
|
||||
last_failed_login = Column(DateTime, nullable=True) # 最后一次登录失败时间
|
||||
login_locked_until = Column(DateTime, nullable=True) # 锁定到什么时候
|
||||
|
||||
# 软删除相关字段
|
||||
is_deleted = Column(Integer, nullable=False, default=0, index=True) # 是否已删除:0=否,1=是
|
||||
deleted_at = Column(DateTime, nullable=True) # 删除时间
|
||||
|
||||
# 关联操作日志
|
||||
operation_logs = relationship("OperationLog", back_populates="user", cascade="all, delete-orphan")
|
||||
|
||||
@ -191,6 +195,8 @@ class User(Base):
|
||||
"login_failed_count": self.login_failed_count,
|
||||
"last_failed_login": self.last_failed_login.isoformat() if self.last_failed_login else None,
|
||||
"login_locked_until": self.login_locked_until.isoformat() if self.login_locked_until else None,
|
||||
"is_deleted": self.is_deleted,
|
||||
"deleted_at": self.deleted_at.isoformat() if self.deleted_at else None,
|
||||
}
|
||||
if include_password:
|
||||
result["password_hash"] = self.password_hash
|
||||
|
||||
Loading…
Reference in New Issue
Block a user