344 lines
11 KiB
Python
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()
|