geo/backend/app/api/organization.py

344 lines
11 KiB
Python

"""组织管理 API — /api/v1/organization/*"""
import uuid
from typing import Optional
from fastapi import APIRouter, Depends, HTTPException, status
from pydantic import BaseModel, EmailStr, Field
from sqlalchemy import select, func
from sqlalchemy.ext.asyncio import AsyncSession
from app.api.deps import get_current_user
from app.database import get_db
from app.models.organization import Organization, OrgMember
from app.models.user import User
router = APIRouter(prefix="/api/v1/organization", tags=["组织管理"])
class OrgInfoResponse(BaseModel):
id: str
name: str
slug: str
description: Optional[str] = None
logo_url: Optional[str] = None
plan: str
max_members: int
member_count: int = 0
model_config = {"from_attributes": True}
class MemberResponse(BaseModel):
id: str
user_id: str
name: str
email: str
role: str
joined_at: str
invited_by: Optional[str] = None
model_config = {"from_attributes": True}
class InviteMemberRequest(BaseModel):
email: EmailStr
role: str = Field(default="viewer", pattern="^(owner|admin|editor|viewer)$")
class UpdateMemberRoleRequest(BaseModel):
role: str = Field(..., pattern="^(owner|admin|editor|viewer)$")
class InviteResponse(BaseModel):
id: str
email: str
role: str
message: str
@router.get("/info", response_model=OrgInfoResponse)
async def get_org_info(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""获取当前用户所属组织的基本信息"""
if not current_user.organization_id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="用户未加入任何组织",
)
stmt = select(Organization).where(Organization.id == current_user.organization_id)
result = await db.execute(stmt)
org = result.scalar_one_or_none()
if not org:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="组织不存在",
)
count_stmt = select(func.count(OrgMember.id)).where(
OrgMember.organization_id == org.id
)
count_result = await db.execute(count_stmt)
member_count = count_result.scalar() or 0
return OrgInfoResponse(
id=str(org.id),
name=org.name,
slug=org.slug,
description=org.description,
logo_url=org.logo_url,
plan=org.plan,
max_members=org.max_members,
member_count=member_count,
)
@router.get("/members", response_model=list[MemberResponse])
async def list_members(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""获取当前用户所属组织的所有成员"""
if not current_user.organization_id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="用户未加入任何组织",
)
stmt = (
select(OrgMember, User)
.join(User, User.id == OrgMember.user_id)
.where(OrgMember.organization_id == current_user.organization_id)
.order_by(OrgMember.joined_at.desc())
)
result = await db.execute(stmt)
rows = result.all()
members = []
for member, user in rows:
members.append(
MemberResponse(
id=str(member.id),
user_id=str(member.user_id),
name=user.name or "",
email=user.email,
role=member.role,
joined_at=member.joined_at.isoformat() if member.joined_at else "",
invited_by=str(member.invited_by) if member.invited_by else None,
)
)
return members
@router.post("/members/invite", response_model=InviteResponse, status_code=status.HTTP_201_CREATED)
async def invite_member(
body: InviteMemberRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""邀请新成员加入组织"""
if not current_user.organization_id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="用户未加入任何组织",
)
current_membership_stmt = select(OrgMember).where(
OrgMember.organization_id == current_user.organization_id,
OrgMember.user_id == current_user.id,
)
current_membership_result = await db.execute(current_membership_stmt)
current_membership = current_membership_result.scalar_one_or_none()
if not current_membership or current_membership.role not in ["owner", "admin"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="仅管理员可以邀请新成员",
)
target_user_stmt = select(User).where(User.email == body.email)
target_user_result = await db.execute(target_user_stmt)
target_user = target_user_result.scalar_one_or_none()
if not target_user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="用户不存在",
)
existing_stmt = select(OrgMember).where(
OrgMember.organization_id == current_user.organization_id,
OrgMember.user_id == target_user.id,
)
existing_result = await db.execute(existing_stmt)
existing_membership = existing_result.scalar_one_or_none()
if existing_membership:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="该用户已经是组织成员",
)
count_stmt = select(func.count(OrgMember.id)).where(
OrgMember.organization_id == current_user.organization_id
)
count_result = await db.execute(count_stmt)
current_member_count = count_result.scalar() or 0
org_stmt = select(Organization).where(Organization.id == current_user.organization_id)
org_result = await db.execute(org_stmt)
org = org_result.scalar_one_or_none()
if org and current_member_count >= org.max_members:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=f"成员数量已达上限({org.max_members}人)",
)
new_membership = OrgMember(
id=uuid.uuid4(),
organization_id=current_user.organization_id,
user_id=target_user.id,
role=body.role,
invited_by=current_user.id,
)
db.add(new_membership)
await db.commit()
await db.refresh(new_membership)
return InviteResponse(
id=str(new_membership.id),
email=target_user.email,
role=new_membership.role,
message=f"成功邀请 {target_user.email} 加入组织",
)
@router.put("/members/{user_id}/role", response_model=MemberResponse)
async def update_member_role(
user_id: uuid.UUID,
body: UpdateMemberRoleRequest,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""更新成员角色"""
if not current_user.organization_id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="用户未加入任何组织",
)
current_membership_stmt = select(OrgMember).where(
OrgMember.organization_id == current_user.organization_id,
OrgMember.user_id == current_user.id,
)
current_membership_result = await db.execute(current_membership_stmt)
current_membership = current_membership_result.scalar_one_or_none()
if not current_membership or current_membership.role not in ["owner", "admin"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="仅管理员可以修改成员角色",
)
target_membership_stmt = select(OrgMember, User).join(
User, User.id == OrgMember.user_id
).where(
OrgMember.organization_id == current_user.organization_id,
OrgMember.user_id == user_id,
)
target_result = await db.execute(target_membership_stmt)
target_row = target_result.first()
if not target_row:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="成员不存在",
)
target_membership, target_user = target_row
if target_membership.role == "owner":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="不能修改所有者角色",
)
if body.role == "owner":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="不能将成员设为所有者",
)
target_membership.role = body.role
await db.commit()
await db.refresh(target_membership)
return MemberResponse(
id=str(target_membership.id),
user_id=str(target_membership.user_id),
name=target_user.name or "",
email=target_user.email,
role=target_membership.role,
joined_at=target_membership.joined_at.isoformat() if target_membership.joined_at else "",
invited_by=str(target_membership.invited_by) if target_membership.invited_by else None,
)
@router.delete("/members/{user_id}", status_code=status.HTTP_204_NO_CONTENT)
async def remove_member(
user_id: uuid.UUID,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""移除组织成员"""
if not current_user.organization_id:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="用户未加入任何组织",
)
current_membership_stmt = select(OrgMember).where(
OrgMember.organization_id == current_user.organization_id,
OrgMember.user_id == current_user.id,
)
current_membership_result = await db.execute(current_membership_stmt)
current_membership = current_membership_result.scalar_one_or_none()
if not current_membership or current_membership.role not in ["owner", "admin"]:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="仅管理员可以移除成员",
)
target_membership_stmt = select(OrgMember).where(
OrgMember.organization_id == current_user.organization_id,
OrgMember.user_id == user_id,
)
target_result = await db.execute(target_membership_stmt)
target_membership = target_result.scalar_one_or_none()
if not target_membership:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="成员不存在",
)
if target_membership.role == "owner":
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="不能移除所有者",
)
if user_id == current_user.id:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="不能移除自己",
)
await db.delete(target_membership)
await db.commit()