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": "登出成功"}