273 lines
8.8 KiB
Python
273 lines
8.8 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import datetime, timedelta, 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()
|
|
if expires_at is None:
|
|
expires_at = datetime.now(timezone.utc) + timedelta(days=1)
|
|
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.is_active == True).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.is_active == True).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,
|
|
"handled_at": req.updated_at if req.status != "pending" else None,
|
|
}
|
|
for req, username, phone in rows
|
|
]
|
|
|
|
|
|
async def get_all_requests(
|
|
db: AsyncSession,
|
|
team_id: str,
|
|
status: str | None = None,
|
|
) -> list[dict[str, Any]]:
|
|
"""获取团队所有加入申请列表,支持按状态筛选。"""
|
|
where = [TeamJoinRequest.team_id == team_id]
|
|
if status:
|
|
where.append(TeamJoinRequest.status == status)
|
|
|
|
result = await db.execute(
|
|
select(TeamJoinRequest, User.username, User.phone)
|
|
.join(User, User.id == TeamJoinRequest.user_id)
|
|
.where(*where)
|
|
.order_by(TeamJoinRequest.created_at.desc())
|
|
)
|
|
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,
|
|
"handled_at": req.updated_at if req.status != "pending" else None,
|
|
}
|
|
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())
|