"""组织管理 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()