from __future__ import annotations from datetime import datetime, timezone from typing import Any from fastapi import HTTPException from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.models.team import Team from app.models.team_invitation import TeamInvitation from app.models.team_join_request import TeamJoinRequest from app.models.user import User from app.utils.id_gen import generate_id import secrets def _generate_invite_code() -> str: """生成唯一邀请码。""" return secrets.token_urlsafe(12) async def _get_team_or_404(db: AsyncSession, team_id: str) -> Team: result = await db.execute( select(Team).where(Team.id == team_id, Team.deleted_at.is_(None)).limit(1) ) team = result.scalar_one_or_none() if not team: raise HTTPException(status_code=404, detail="团队不存在") return team async def _assert_is_manager(db: AsyncSession, user_id: str, team_id: str) -> None: team = await _get_team_or_404(db, team_id) if team.manager_id != user_id: raise HTTPException(status_code=403, detail="只有团队管理人才能操作") async def create_invitation( db: AsyncSession, team_id: str, created_by: str, max_uses: int | None = None, expires_at: datetime | None = None, ) -> TeamInvitation: """创建邀请码(仅团队管理人)。""" await _assert_is_manager(db, created_by, team_id) code = _generate_invite_code() invitation = TeamInvitation( id=generate_id(), team_id=team_id, code=code, created_by=created_by, status="active", max_uses=max_uses, expires_at=expires_at, ) db.add(invitation) await db.flush() await db.refresh(invitation) return invitation async def get_invitation_by_code(db: AsyncSession, code: str) -> TeamInvitation | None: """根据邀请码获取邀请记录(验证有效)。""" result = await db.execute( select(TeamInvitation).where( TeamInvitation.code == code, TeamInvitation.deleted_at.is_(None), ).limit(1) ) invitation = result.scalar_one_or_none() if not invitation or invitation.status != "active": return None if invitation.expires_at and invitation.expires_at < datetime.now(timezone.utc): return None if invitation.max_uses and invitation.use_count >= invitation.max_uses: return None return invitation async def revoke_invitation(db: AsyncSession, invitation_id: str, revoked_by: str) -> None: """撤销邀请码。""" result = await db.execute( select(TeamInvitation).where( TeamInvitation.id == invitation_id, TeamInvitation.deleted_at.is_(None), ).limit(1) ) invitation = result.scalar_one_or_none() if not invitation: raise HTTPException(status_code=404, detail="邀请码不存在") await _assert_is_manager(db, revoked_by, invitation.team_id) invitation.status = "revoked" await db.flush() async def create_join_request( db: AsyncSession, user_id: str, invitation_code: str, ) -> TeamJoinRequest: """通过邀请码创建加入申请。""" invitation = await get_invitation_by_code(db, invitation_code) if not invitation: raise HTTPException(status_code=400, detail="邀请码无效或已过期/已用完") # 验证用户存在 user_result = await db.execute( select(User).where(User.id == user_id, User.deleted_at.is_(None)).limit(1) ) user = user_result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="用户不存在") # 检查是否已在该团队 if user.team_id == invitation.team_id: raise HTTPException(status_code=400, detail="您已在此团队中") # 检查是否已有待审批的申请 existing = await db.execute( select(TeamJoinRequest).where( TeamJoinRequest.user_id == user_id, TeamJoinRequest.team_id == invitation.team_id, TeamJoinRequest.status == "pending", ).limit(1) ) if existing.scalar_one_or_none(): raise HTTPException(status_code=400, detail="您已提交申请,请等待审批") # 检查是否已有待审批的其他团队申请 existing_any = await db.execute( select(TeamJoinRequest).where( TeamJoinRequest.user_id == user_id, TeamJoinRequest.status == "pending", ).limit(1) ) if existing_any.scalar_one_or_none(): raise HTTPException(status_code=400, detail="您已有一个待审批的团队申请,请先等待处理") request = TeamJoinRequest( id=generate_id(), team_id=invitation.team_id, user_id=user_id, invitation_id=invitation.id, status="pending", ) db.add(request) # 增加邀请码使用次数 invitation.use_count = (invitation.use_count or 0) + 1 if invitation.max_uses and invitation.use_count >= invitation.max_uses: invitation.status = "revoked" await db.flush() return request async def handle_join_request( db: AsyncSession, request_id: str, manager_id: str, action: str, note: str | None = None, ) -> None: """审批/拒绝加入申请(仅团队管理人)。""" result = await db.execute( select(TeamJoinRequest).where(TeamJoinRequest.id == request_id).limit(1) ) request = result.scalar_one_or_none() if not request: raise HTTPException(status_code=404, detail="申请不存在") if request.status != "pending": raise HTTPException(status_code=400, detail="该申请已处理") await _assert_is_manager(db, manager_id, request.team_id) if action == "approve": # 检查用户是否已在其他团队 user_result = await db.execute( select(User).where(User.id == request.user_id, User.deleted_at.is_(None)).limit(1) ) user = user_result.scalar_one_or_none() if not user: raise HTTPException(status_code=404, detail="用户不存在") if user.team_id and user.team_id != request.team_id: raise HTTPException(status_code=400, detail="用户已在其他团队中,无法加入") user.team_id = request.team_id request.status = "approved" elif action == "reject": request.status = "rejected" request.note = note else: raise HTTPException(status_code=400, detail="无效操作") request.handled_by = manager_id await db.flush() async def get_pending_requests(db: AsyncSession, team_id: str) -> list[dict[str, Any]]: """获取团队待审批的申请列表。""" result = await db.execute( select(TeamJoinRequest, User.username, User.phone) .join(User, User.id == TeamJoinRequest.user_id) .where( TeamJoinRequest.team_id == team_id, TeamJoinRequest.status == "pending", ) .order_by(TeamJoinRequest.created_at.asc()) ) rows = result.all() return [ { "id": req.id, "team_id": req.team_id, "user_id": req.user_id, "username": username, "phone": phone, "status": req.status, "note": req.note, "created_at": req.created_at, } for req, username, phone in rows ] async def get_invitations_for_team(db: AsyncSession, team_id: str) -> list[TeamInvitation]: """获取团队的所有邀请码。""" result = await db.execute( select(TeamInvitation).where( TeamInvitation.team_id == team_id, TeamInvitation.deleted_at.is_(None), ).order_by(TeamInvitation.created_at.desc()) ) return list(result.scalars().all())