Files
video-gen/video-gen-api/app/services/team_invitation_service.py
T
2026-07-06 14:06:25 +08:00

237 lines
7.7 KiB
Python

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())