BookSystem/backend/auth/apps/users/urls.py
jayhgq 19d6db6ecc feat: 添加登录日志和操作日志功能
1. 新增日志相关数据库模型、API路由、前端页面与权限配置
2. 为现有业务接口添加操作日志记录功能
3. 优化导入顺序与权限初始化数据
2026-06-12 22:41:41 +08:00

326 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

from fastapi import APIRouter, Depends, HTTPException, Query, status, Request
from fastapi.responses import Response
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select, and_
from fastcrud import FastCRUD
from datetime import timedelta, datetime
import redis.asyncio as redis
from database import get_db
from models import User
from schemas import (
UserCreate,
UserResponse,
UserLogin,
Token,
UserCreateRequest,
UserUpdate,
)
from utils.password import verify_password, get_password_hash
from utils.jwt import create_access_token
from config import app_settings
from utils.redis_client import get_redis, set_token_in_redis, delete_token_from_redis
from utils.captcha import create_captcha, validate_captcha
from middleware import get_current_user
from utils.like_filter import ilike_contains
from utils.log_utils import get_client_ip, get_user_agent, save_login_log, create_operation_log
router = APIRouter(prefix="/users", tags=["users"])
# 创建FastCRUD实例
user_crud = FastCRUD(User, UserResponse)
@router.get("/captcha")
async def get_captcha(redis_conn: redis.Redis = Depends(get_redis)):
"""获取验证码图片"""
# 创建验证码
captcha_id, image_bytes = await create_captcha(redis_conn)
# 返回验证码图片同时在响应头中返回验证码ID
response = Response(content=image_bytes, media_type="image/png")
response.headers["X-Captcha-ID"] = captcha_id
return response
@router.post("/login", response_model=Token)
async def login(
user_data: UserLogin,
request: Request,
db: AsyncSession = Depends(get_db),
redis_conn: redis.Redis = Depends(get_redis),
):
"""用户登录"""
ip = get_client_ip(request)
ua = get_user_agent(request)
# 验证验证码
is_captcha_valid = await validate_captcha(
redis_conn, user_data.captcha_id, user_data.captcha_code
)
if not is_captcha_valid:
await save_login_log(db, user_data.username, ip, ua, "failure", "验证码错误或已过期")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="验证码错误或已过期",
headers={"WWW-Authenticate": "Bearer"},
)
# 查找用户
result = await db.execute(select(User).filter(User.username == user_data.username))
user = result.scalar_one_or_none()
# 验证用户是否存在且密码正确
if not user or not verify_password(user_data.password, user.password_hash):
await save_login_log(db, user_data.username, ip, ua, "failure", "用户名或密码错误")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="用户名或密码错误",
headers={"WWW-Authenticate": "Bearer"},
)
# 创建访问令牌
access_token_expires = timedelta(minutes=app_settings.access_token_expire_minutes)
access_token = create_access_token(
data={"sub": str(user.id), "username": user.username},
expires_delta=access_token_expires,
)
# 将令牌存储到Redis
expire_seconds = int(access_token_expires.total_seconds())
await set_token_in_redis(redis_conn, user.id, access_token, expire_seconds)
# 更新用户最后登录时间
user.lastlogintime = datetime.utcnow()
await db.commit()
# 记录登录成功日志
await save_login_log(db, user.username, ip, ua, "success", None)
return {"access_token": access_token, "token_type": "bearer"}
@router.post("/", response_model=UserResponse)
async def create_user(
user_data: UserCreateRequest,
request: Request,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""创建用户"""
# 检查用户名是否已存在
result = await db.execute(select(User).filter(User.username == user_data.username))
existing_user = result.scalar_one_or_none()
if existing_user:
raise HTTPException(status_code=400, detail="用户名已存在")
# 生成密码哈希值
hashed_password = get_password_hash(user_data.password)
# 准备创建数据
create_data = user_data.model_dump()
create_data["password_hash"] = hashed_password
del create_data["password"] # 删除明文密码
# 转换为UserCreate模型
user_data_create = UserCreate(**create_data)
# 使用FastCRUD创建用户
created_user = await user_crud.create(
db, user_data_create, schema_to_select=UserResponse, return_as_model=True
)
# 记录操作日志
await create_operation_log(
db, current_user.id, current_user.username, "create", "user",
created_user.id, created_user.username,
{"username": user_data.username, "nickname": user_data.nickname},
get_client_ip(request), get_user_agent(request),
)
await db.commit()
return created_user
@router.post("/search", response_model=list[UserResponse])
async def search_users(
query: dict = {},
request: Request = None,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""分页列表用户POST方式支持登录名、昵称、邮箱、手机的模糊查询与组合查询AND"""
stmt = select(User)
filters = []
for col, key in (
(User.username, "username"),
(User.nickname, "nickname"),
(User.email, "email"),
(User.phone, "phone"),
):
raw = query.get(key)
cond = ilike_contains(col, raw)
if cond is not None:
filters.append(cond)
if filters:
stmt = stmt.where(and_(*filters))
result = await db.execute(stmt)
rows = result.scalars().all()
# 记录查询日志
await create_operation_log(
db, current_user.id, current_user.username, "search", "user",
None, None, {"filters": {k: v for k, v in query.items() if v}},
get_client_ip(request), get_user_agent(request),
)
await db.commit()
return [UserResponse.model_validate(u) for u in rows]
@router.get("/me", response_model=UserResponse)
async def get_current_user_info(
request: Request,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""获取当前用户信息(须声明在 /{user_id} 之前,否则会被当成 id"""
# 记录查询日志
await create_operation_log(
db, current_user.id, current_user.username, "search", "user",
current_user.id, current_user.username, None,
get_client_ip(request), get_user_agent(request),
)
await db.commit()
return current_user
@router.get("/{user_id}", response_model=UserResponse)
async def get_user(
user_id: int,
request: Request,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""获取单个用户"""
user = await user_crud.get(db, id=user_id)
if not user:
raise HTTPException(status_code=404, detail="用户不存在")
# 记录查询日志
await create_operation_log(
db, current_user.id, current_user.username, "search", "user",
user_id, user.username, None,
get_client_ip(request), get_user_agent(request),
)
await db.commit()
return user
@router.put("/{user_id}", response_model=UserResponse)
async def update_user(
user_id: int,
user_data: UserUpdate,
request: Request,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""更新用户"""
# 获取原用户数据用于记录变更
result = await db.execute(select(User).filter(User.id == user_id))
old_user = result.scalar_one_or_none()
if not old_user:
raise HTTPException(status_code=404, detail="用户不存在")
# 检查用户名是否被其他用户使用
result = await db.execute(
select(User).filter(User.username == user_data.username, User.id != user_id)
)
existing_user = result.scalar_one_or_none()
if existing_user:
raise HTTPException(status_code=400, detail="用户名已存在")
# 生成密码哈希值
hashed_password = (
get_password_hash(user_data.password) if user_data.password else None
)
# 准备更新数据
update_data = user_data.model_dump()
if hashed_password:
update_data["password_hash"] = hashed_password
del update_data["password"] # 删除明文密码
# 使用FastCRUD更新用户
updated_user = await user_crud.update(
db, update_data, id=user_id, schema_to_select=UserResponse, return_as_model=True
)
if not updated_user:
raise HTTPException(status_code=404, detail="用户不存在")
# 记录操作日志(计算变更字段)
changed = {}
for field in ["username", "nickname", "email", "phone", "role_id"]:
old_val = getattr(old_user, field, None)
new_val = update_data.get(field)
if old_val != new_val:
changed[field] = {"old": old_val, "new": new_val}
await create_operation_log(
db, current_user.id, current_user.username, "update", "user",
user_id, old_user.username, changed if changed else None,
get_client_ip(request), get_user_agent(request),
)
await db.commit()
return updated_user
@router.delete("/{user_id}")
async def delete_user(
user_id: int,
request: Request,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
):
"""删除用户"""
# 检查用户是否存在
result = await db.execute(select(User).filter(User.id == user_id))
existing_user = result.scalar_one_or_none()
if not existing_user:
raise HTTPException(status_code=404, detail="用户不存在")
elif existing_user.id == current_user.id:
raise HTTPException(status_code=400, detail="不能删除当前登录用户")
elif existing_user.role_id == 1:
raise HTTPException(status_code=400, detail="不能删除管理员用户")
else:
# 记录操作日志(删除前记录被删用户信息)
await create_operation_log(
db, current_user.id, current_user.username, "delete", "user",
user_id, existing_user.username,
{"username": existing_user.username, "nickname": existing_user.nickname},
get_client_ip(request), get_user_agent(request),
)
# 使用FastCRUD删除用户
deleted = await user_crud.delete(db, id=user_id)
return {"message": "用户删除成功"}
@router.post("/logout")
async def logout(
request: Request,
db: AsyncSession = Depends(get_db),
current_user: User = Depends(get_current_user),
redis_conn: redis.Redis = Depends(get_redis),
):
"""用户登出"""
await delete_token_from_redis(redis_conn, current_user.id)
# 记录登出日志
await save_login_log(
db, current_user.username,
get_client_ip(request), get_user_agent(request),
"success", None,
)
return {"message": "登出成功"}