from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, update from pydantic import BaseModel from app.core.database import get_db from app.models.models import User, Debt, AuditLog, Group, SystemSetting from app.schemas.schemas import ( AdminUserResponse, AdminDashboard, SystemSettingUpdate, GroupCreate, GroupResponse, GroupMemberAdd, EmailServiceConfig, ) from app.core.deps import require_admin from app.core.security import hash_password router = APIRouter(prefix="/admin", tags=["管理后台"]) @router.get("/dashboard", response_model=AdminDashboard) async def admin_dashboard( admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): users_count = await db.execute(select(func.count(User.id))) pending_count = await db.execute(select(func.count(User.id)).where(User.status == "pending")) active_count = await db.execute(select(func.count(User.id)).where(User.status == "active")) disabled_count = await db.execute(select(func.count(User.id)).where(User.status == "disabled")) return AdminDashboard( total_users=users_count.scalar(), active_debts=0, total_amount=0, overdue_count=0, rate_distribution={}, status_distribution={"pending": pending_count.scalar(), "active": active_count.scalar(), "disabled": disabled_count.scalar()}, ) @router.get("/users", response_model=list[AdminUserResponse]) async def list_users( page: int = Query(1, ge=1), size: int = Query(50, ge=1, le=200), search: str = "", status_filter: str = "", admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): q = select(User) if search: q = q.where(User.email.ilike(f"%{search}%") | User.nickname.ilike(f"%{search}%")) if status_filter: q = q.where(User.status == status_filter) q = q.order_by(User.created_at.desc()).offset((page - 1) * size).limit(size) result = await db.execute(q) users = result.scalars().all() groups_result = await db.execute(select(Group)) groups_map = {g.id: g.name for g in groups_result.scalars().all()} out = [] for u in users: dc = await db.execute(select(func.count(Debt.id)).where(Debt.user_id == u.id)) ta = await db.execute(select(func.coalesce(func.sum(Debt.amount), 0)).where(Debt.user_id == u.id)) ur = AdminUserResponse( id=u.id, account=u.email, nickname=u.nickname, real_email=u.real_email or "", role=u.role, status=u.status, group_id=u.group_id, created_at=u.created_at, debt_count=dc.scalar(), total_amount=ta.scalar(), group_name=groups_map.get(u.group_id, ""), ) out.append(ur) return out @router.put("/users/{user_id}/status") async def set_user_status( user_id: int, status: str = "", admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): 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="用户不存在") if status: user.status = status else: user.status = "active" if user.status != "active" else "disabled" await db.commit() return {"message": f"用户状态已更新为 {user.status}"} @router.delete("/users/{user_id}") async def delete_user( user_id: int, admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): 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="用户不存在") if user.role == "admin": raise HTTPException(status_code=400, detail="不能删除管理员") debts_result = await db.execute(select(Debt).where(Debt.user_id == user_id)) for d in debts_result.scalars().all(): await db.delete(d) await db.delete(user) await db.commit() return {"message": "用户已删除"} @router.get("/users/{user_id}/debts") async def view_user_debts( user_id: int, admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(Debt).where(Debt.user_id == user_id)) return [{"id": d.id, "creditor": d.creditor_name, "amount": d.amount, "status": d.status} for d in result.scalars().all()] @router.get("/logs") async def get_audit_logs( page: int = Query(1, ge=1), size: int = Query(50, ge=1, le=200), admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): q = select(AuditLog).order_by(AuditLog.created_at.desc()).offset((page - 1) * size).limit(size) result = await db.execute(q) return [{"id": l.id, "user_id": l.user_id, "action": l.action, "details": l.details, "created_at": l.created_at.isoformat()} for l in result.scalars().all()] @router.get("/groups", response_model=list[GroupResponse]) async def list_groups( admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(Group).order_by(Group.created_at.desc())) groups = result.scalars().all() out = [] for g in groups: mc = await db.execute(select(func.count(User.id)).where(User.group_id == g.id)) gr = GroupResponse.model_validate(g) gr.member_count = mc.scalar() out.append(gr) return out @router.post("/groups", response_model=GroupResponse, status_code=201) async def create_group( req: GroupCreate, admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): group = Group(name=req.name, description=req.description) db.add(group) await db.commit() await db.refresh(group) gr = GroupResponse.model_validate(group) gr.member_count = 0 return gr @router.delete("/groups/{group_id}") async def delete_group( group_id: int, admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(Group).where(Group.id == group_id)) group = result.scalar_one_or_none() if not group: raise HTTPException(status_code=404, detail="组不存在") await db.execute(select(User).where(User.group_id == group_id).update({"group_id": None})) await db.delete(group) await db.commit() return {"message": "组已删除"} @router.post("/groups/{group_id}/members") async def add_group_member( group_id: int, req: GroupMemberAdd, admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): group = await db.execute(select(Group).where(Group.id == group_id)) if not group.scalar_one_or_none(): raise HTTPException(status_code=404, detail="组不存在") user = await db.execute(select(User).where(User.id == req.user_id)) u = user.scalar_one_or_none() if not u: raise HTTPException(status_code=404, detail="用户不存在") u.group_id = group_id await db.commit() return {"message": f"用户 {u.email} 已加入组"} @router.delete("/groups/{group_id}/members/{user_id}") async def remove_group_member( group_id: int, user_id: int, admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(User).where(User.id == user_id, User.group_id == group_id)) user = result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="用户不在此组中") user.group_id = None await db.commit() return {"message": f"用户 {user.email} 已从组中移除"} class AdminPasswordChange(BaseModel): new_password: str class AdminUserCreate(BaseModel): account: str password: str nickname: str = "" real_email: str = "" role: str = "user" group_id: int | None = None class AdminUserUpdate(BaseModel): account: str | None = None nickname: str | None = None real_email: str | None = None role: str | None = None group_id: int | None = None @router.post("/users", response_model=AdminUserResponse, status_code=201) async def create_user( req: AdminUserCreate, admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): account = req.account.strip().lower() existing = await db.execute(select(User).where(User.email == account)) if existing.scalar_one_or_none(): raise HTTPException(status_code=400, detail="账号已存在") user = User( email=account, password_hash=hash_password(req.password), nickname=req.nickname, real_email=req.real_email, role=req.role, status="active", group_id=req.group_id, ) db.add(user) await db.commit() await db.refresh(user) return AdminUserResponse( id=user.id, account=user.email, nickname=user.nickname, role=user.role, status=user.status, group_id=user.group_id, created_at=user.created_at, debt_count=0, total_amount=0, group_name="", ) @router.put("/users/{user_id}") async def update_user( user_id: int, req: AdminUserUpdate, admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): 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="用户不存在") if req.account is not None: account = req.account.strip().lower() existing = await db.execute(select(User).where(User.email == account, User.id != user_id)) if existing.scalar_one_or_none(): raise HTTPException(status_code=400, detail="账号已存在") user.email = account if req.nickname is not None: user.nickname = req.nickname if req.real_email is not None: user.real_email = req.real_email if req.role is not None: if req.role not in ("user", "admin"): raise HTTPException(status_code=400, detail="角色值无效") user.role = req.role if req.group_id is not None or req.group_id == "": user.group_id = req.group_id if req.group_id else None await db.commit() return {"message": "用户信息已更新"} RESET_TOKEN_TTL_HOURS = 24 @router.put("/users/{user_id}/password") async def admin_reset_password( user_id: int, admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): import secrets from datetime import datetime, timedelta from app.models.models import SystemSetting 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="用户不存在") if not user.real_email: raise HTTPException(status_code=400, detail="该用户未绑定邮箱,无法发送重置链接") token = secrets.token_urlsafe(32) expires_at = (datetime.utcnow() + timedelta(hours=RESET_TOKEN_TTL_HOURS)).isoformat() key = f"pwd_reset:{user.id}" result = await db.execute(select(SystemSetting).where(SystemSetting.key == key)) setting = result.scalar_one_or_none() value = f"{token}|{expires_at}" if setting: setting.value = value else: db.add(SystemSetting(key=key, value=value)) await db.commit() from app.services.email_service import send_password_reset_link_email, get_site_url site_url = await get_site_url() reset_url = f"{site_url}/reset-password.html?token={token}&uid={user.id}" await send_password_reset_link_email(user, reset_url, RESET_TOKEN_TTL_HOURS, f"管理员 {admin.email}") return {"message": f"重置链接已发送至 {user.real_email}"} SMTP_KEYS = ["smtp_host", "smtp_port", "smtp_user", "smtp_password", "smtp_from", "smtp_use_tls"] @router.get("/email-config", response_model=EmailServiceConfig) async def get_email_config( admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): result = await db.execute(select(SystemSetting).where(SystemSetting.key.in_(SMTP_KEYS))) settings = {r.key: r.value for r in result.scalars().all()} return EmailServiceConfig( smtp_host=settings.get("smtp_host", ""), smtp_port=int(settings.get("smtp_port", "465")), smtp_user=settings.get("smtp_user", ""), smtp_password="***" if settings.get("smtp_password") else "", smtp_from=settings.get("smtp_from", ""), smtp_use_tls=settings.get("smtp_use_tls", "true") == "true", ) @router.put("/email-config") async def update_email_config( req: EmailServiceConfig, admin: User = Depends(require_admin), db: AsyncSession = Depends(get_db), ): values = { "smtp_host": req.smtp_host, "smtp_port": str(req.smtp_port), "smtp_user": req.smtp_user, "smtp_password": req.smtp_password, "smtp_from": req.smtp_from, "smtp_use_tls": "true" if req.smtp_use_tls else "false", } for key, value in values.items(): result = await db.execute(select(SystemSetting).where(SystemSetting.key == key)) setting = result.scalar_one_or_none() if setting: setting.value = value else: db.add(SystemSetting(key=key, value=value)) await db.commit() return {"message": "邮件服务配置已保存"} class EmailTestRequest(BaseModel): to_email: str @router.post("/email-test") async def test_email( req: EmailTestRequest, admin: User = Depends(require_admin), ): from app.services.email_service import send_email html = """
您好,
这是一封债务管理系统的邮件服务测试邮件。
如果您收到此邮件,说明邮件服务器配置正确。
✅
邮件服务正常
—— 债务管理系统