from typing import Optional, List from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import StreamingResponse from sqlalchemy import select, func, or_ from sqlalchemy.ext.asyncio import AsyncSession from io import BytesIO from app.core.database import get_db from app.core.security import get_current_user, hash_password, require_role from app.models.user import User from app.schemas.user import UserCreate, UserUpdate, UserResponse, PasswordReset from app.services.audit_log_service import AuditLogService router = APIRouter(prefix="/users", tags=["用户管理"]) @router.get("") async def list_users( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), search: Optional[str] = Query(None), role: Optional[str] = Query(None), is_active: Optional[bool] = Query(None), db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """搜索分页用户列表""" query = select(User) count_q = select(func.count(User.id)) if search: like = f"%{search}%" cond = or_(User.username.contains(search), User.display_name.contains(like), User.email.contains(like)) query = query.where(cond) count_q = count_q.where(cond) if role: query = query.where(User.role == role) count_q = count_q.where(User.role == role) if is_active is not None: query = query.where(User.is_active == is_active) count_q = count_q.where(User.is_active == is_active) total = (await db.execute(count_q)).scalar() or 0 query = query.order_by(User.id).offset((page - 1) * page_size).limit(page_size) result = await db.execute(query) users = result.scalars().all() return { "total": total, "page": page, "page_size": page_size, "items": [ { "id": u.id, "username": u.username, "display_name": u.display_name, "email": u.email, "phone": getattr(u, 'phone', None), "role": u.role, "is_active": u.is_active, "last_login": u.last_login.isoformat() if u.last_login else None, "created_at": u.created_at.isoformat() if u.created_at else None, } for u in users ], } @router.post("", response_model=UserResponse, status_code=201) async def create_user( data: UserCreate, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """创建用户""" exists = await db.execute(select(User).where(User.username == data.username)) if exists.scalar_one_or_none(): raise HTTPException(status_code=400, detail="用户名已存在") user = User( username=data.username, display_name=data.display_name, hashed_password=hash_password(data.password), email=data.email, phone=getattr(data, 'phone', None), role=data.role, ) db.add(user) await db.commit() await db.refresh(user) return user # ── 固定路径路由 (必须在 /{user_id} 之前) ── @router.post("/batch-import") async def batch_import_users( data: dict, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """批量导入用户 — 本地模式(Excel) 或 LDAP模式""" users_data = data.get("users", []) import_type = data.get("import_type", "local") skip_existing = data.get("skip_existing", True) if not users_data: raise HTTPException(status_code=400, detail="没有导入数据") success_count = 0 skipped_count = 0 failed_users = [] for u in users_data: username = u.get("username", "") display_name = u.get("display_name", "") if not username or not display_name: failed_users.append({"username": username, "error": "缺少用户名或姓名"}) continue exists = await db.execute(select(User).where(User.username == username)) if exists.scalar_one_or_none(): if skip_existing: skipped_count += 1 continue failed_users.append({"username": username, "error": "用户名已存在"}) continue user = User( username=username, display_name=display_name, hashed_password=hash_password(u.get("password", username)), email=u.get("email"), phone=u.get("phone"), role=u.get("role", "developer"), ) db.add(user) success_count += 1 await db.commit() return { "success_count": success_count, "skipped_count": skipped_count, "failed_count": len(failed_users), "failed_users": failed_users, } @router.post("/batch-import-ldap") async def batch_import_from_ldap( data: dict, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """从 LDAP 批量导入用户""" users_data = data.get("users", []) if not users_data: raise HTTPException(status_code=400, detail="没有导入数据") success_count = 0 skipped_count = 0 failed_users = [] for u in users_data: username = u.get("username", u.get("sAMAccountName", "")) display_name = u.get("display_name", u.get("displayName", "")) email = u.get("email", u.get("mail", "")) if not username: failed_users.append({"username": "", "error": "缺少用户名"}) continue exists = await db.execute(select(User).where(User.username == username)) if exists.scalar_one_or_none(): skipped_count += 1 continue user = User( username=username, display_name=display_name or username, hashed_password=hash_password(username), email=email or None, role="developer", ) db.add(user) success_count += 1 await db.commit() return { "success_count": success_count, "skipped_count": skipped_count, "failed_count": len(failed_users), "failed_users": failed_users, } @router.get("/import-template") async def download_import_template( current_user: User = Depends(require_role("admin")), ): """下载用户导入模板 — CSV 格式""" import csv, io output = io.StringIO() writer = csv.writer(output) writer.writerow(["姓名", "邮箱", "手机", "角色"]) writer.writerow(["张三", "zhangsan@example.com", "13800138000", "developer"]) writer.writerow(["李四", "lisi@example.com", "", "dba"]) return StreamingResponse( iter([output.getvalue().encode("utf-8-sig")]), media_type="text/csv;charset=utf-8", headers={"Content-Disposition": "attachment; filename=users_import_template.csv"}, ) # ── 动态路径路由 ── @router.get("/{user_id}", response_model=UserResponse) async def get_user( user_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """获取单个用户""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="用户不存在") return user @router.put("/{user_id}", response_model=UserResponse) async def update_user( user_id: int, data: UserUpdate, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """更新用户信息""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="用户不存在") update_data = data.model_dump(exclude_unset=True) if "password" in update_data: user.hashed_password = hash_password(update_data.pop("password")) for key, value in update_data.items(): setattr(user, key, value) await db.commit() await db.refresh(user) return user @router.delete("/{user_id}") async def delete_user( user_id: int, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """删除用户""" if user_id == current_user.id: raise HTTPException(status_code=400, detail="不能删除自己") result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="用户不存在") await db.delete(user) await db.commit() return {"message": "用户已删除"} @router.post("/{user_id}/reset-password") async def reset_password( user_id: int, data: PasswordReset, db: AsyncSession = Depends(get_db), current_user: User = Depends(require_role("admin")), ): """管理员重置用户密码""" result = await db.execute(select(User).where(User.id == user_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="用户不存在") user.hashed_password = hash_password(data.new_password) await db.commit() return {"message": "密码已重置"}