Files
server-core/app/api/routers/rbac_teams.py
T
Pine 1cd26e174e feat: 企业认领/企业入驻申请/团队 身份链路
- company_claim_requests:认领园区入驻企业(仅搜索);admin 认领仅当企业无管理员;
  审核人:admin→园区管理员,member→有企业管理员则企业管理员否则园区管理员;
  证明资料(姓名/身份证/手机号)仅存证
- company_apply_requests:已有企业加入平台;park_entered 园区管理员审,
  independent 运营方审;通过后建 ParkCompany 并设提交人为管理员
- teams/team_members:团队即建即用;支持升级为企业(复用企业入驻申请)
- 审核权限:园区管理员=ParkTenant.operator_user_id;企业管理员=is_admin;运营方=operator
- 新表由启动时 create_all 幂等创建
2026-09-07 16:15:35 +08:00

293 lines
11 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""团队(非企业,/ent/teams)。
创建即生效(无需审核);支持成员加入/退出;支持升级为企业
(升级 = 提交企业入驻申请,审核通过后团队标记 upgraded)。
"""
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Request
from pydantic import BaseModel
from sqlalchemy import select
from ..dependencies import get_db, get_current_user
from ...rbac import write_audit
from ...infrastructure.models import Team, TeamMember, User
from ...infrastructure.repositories import Database, new_id, utcnow_iso
router = APIRouter(prefix="/ent/teams", tags=["teams"])
def _now() -> str:
return utcnow_iso()
def _team_dict(t, member_count: int = 0) -> dict:
return {
"id": t.id, "name": t.name, "bio": t.bio,
"owner_user_id": t.owner_user_id, "status": t.status,
"upgraded_company_id": t.upgraded_company_id,
"member_count": member_count, "created_at": t.created_at,
}
def _member_dict(m) -> dict:
return {
"id": m.id, "team_id": m.team_id, "user_id": m.user_id,
"role": m.role, "status": m.status, "created_at": m.created_at,
"left_at": m.left_at,
}
async def _get_team(db: Database, tid: str) -> Team:
t = (await db.session.execute(select(Team).where(Team.id == tid))).scalars().first()
if t is None:
raise HTTPException(status_code=404, detail="团队不存在")
return t
async def _can_manage(db: Database, team: Team, user: dict, *, need_owner: bool = False) -> bool:
"""owner/admin 可管理;owner 专属(升级/解散)需 need_owner。"""
if team.owner_user_id == user.get("id", ""):
return True
if need_owner:
return False
m = await db.session.scalar(select(TeamMember).where(
TeamMember.team_id == team.id, TeamMember.user_id == user.get("id", ""),
TeamMember.status == "active"))
return bool(m and m.role in ("owner", "admin"))
class TeamCreate(BaseModel):
name: str
bio: str = ""
class TeamUpdate(BaseModel):
name: str = ""
bio: str = ""
class TeamMemberAdd(BaseModel):
user_id: str = "" # 二选一
keyword: str = "" # 用户名/手机号搜索
@router.post("", summary="创建团队(即建即用,无需审核)")
async def create_team(
body: TeamCreate,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
name = (body.name or "").strip()
if not name:
raise HTTPException(status_code=400, detail="请填写团队名称")
team = Team(id=new_id("team"), name=name, bio=body.bio.strip(),
owner_user_id=user["id"], status="active", created_at=_now())
db.session.add(team)
await db.session.flush()
db.session.add(TeamMember(id=new_id("tm"), team_id=team.id, user_id=user["id"],
role="owner", status="active", created_at=_now()))
await db.session.commit()
await write_audit(db, action="team.create", resource="team",
resource_id=team.id, detail=f"name={name}", user=user, request=request)
return {"ok": True, "team": _team_dict(team, 1)}
@router.get("/mine", summary="我的团队(我创建的 + 我加入的)")
async def my_teams(
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
items = []
rows = (await db.session.scalars(
select(Team).where(Team.owner_user_id == user["id"])
.order_by(Team.created_at.desc()))).all()
seen = {t.id for t in rows}
for t in rows:
cnt = len(await db.session.scalars(select(TeamMember).where(
TeamMember.team_id == t.id, TeamMember.status == "active")))
items.append(_team_dict(t, cnt))
joined = (await db.session.scalars(
select(Team).join(TeamMember, TeamMember.team_id == Team.id).where(
TeamMember.user_id == user["id"], TeamMember.status == "active",
Team.owner_user_id != user["id"]))).all()
for t in joined:
if t.id in seen:
continue
cnt = len(await db.session.scalars(select(TeamMember).where(
TeamMember.team_id == t.id, TeamMember.status == "active")))
items.append(_team_dict(t, cnt))
return {"items": items}
@router.get("/{tid}", summary="团队详情 + 成员列表")
async def team_detail(
tid: str,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
team = await _get_team(db, tid)
members = (await db.session.scalars(select(TeamMember).where(
TeamMember.team_id == tid, TeamMember.status == "active")
.order_by(TeamMember.created_at))).all()
mlist = []
for m in members:
d = _member_dict(m)
u = await db.users.get_by_id(m.user_id)
if u:
d["username"] = u.get("username", "")
d["nickname"] = u.get("nickname", "")
mlist.append(d)
my = await db.session.scalar(select(TeamMember).where(
TeamMember.team_id == tid, TeamMember.user_id == user["id"]))
return {"team": _team_dict(team, len(members)), "members": mlist,
"my_role": my.role if my else "", "is_member": bool(my and my.status == "active")}
@router.put("/{tid}", summary="编辑团队资料(owner/admin")
async def update_team(
tid: str,
body: TeamUpdate,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
team = await _get_team(db, tid)
if not await _can_manage(db, team, user):
raise HTTPException(status_code=403, detail="仅团队管理员可编辑")
if body.name.strip():
team.name = body.name.strip()
team.bio = body.bio.strip()
await db.session.commit()
await write_audit(db, action="team.update", resource="team",
resource_id=tid, user=user, request=request)
return {"ok": True}
@router.post("/{tid}/members", summary="添加成员(owner/admin,按用户搜索)")
async def add_member(
tid: str,
body: TeamMemberAdd,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
team = await _get_team(db, tid)
if not await _can_manage(db, team, user):
raise HTTPException(status_code=403, detail="仅团队管理员可添加成员")
target = None
if body.user_id.strip():
target = await db.users.get_by_id(body.user_id.strip())
elif body.keyword.strip():
hits = await db.users.search(body.keyword.strip(), limit=1)
target = hits[0] if hits else None
if target is None:
raise HTTPException(status_code=404, detail="用户不存在(可按用户名/手机号搜索)")
existing = await db.session.scalar(select(TeamMember).where(
TeamMember.team_id == tid, TeamMember.user_id == target["id"]))
if existing is not None:
if existing.status == "active":
raise HTTPException(status_code=400, detail="该用户已在团队中")
existing.status = "active"
existing.left_at = ""
await db.session.commit()
else:
db.session.add(TeamMember(id=new_id("tm"), team_id=tid, user_id=target["id"],
role="member", status="active", created_at=_now()))
await db.session.commit()
await write_audit(db, action="team.add_member", resource="team",
resource_id=tid, detail=f"target={target.get('username','')}",
user=user, request=request)
return {"ok": True}
@router.delete("/{tid}/members/{uid}", summary="移出成员(owner/adminowner 不可移出)")
async def remove_member(
tid: str,
uid: str,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
team = await _get_team(db, tid)
if not await _can_manage(db, team, user):
raise HTTPException(status_code=403, detail="仅团队管理员可移出成员")
m = await db.session.scalar(select(TeamMember).where(
TeamMember.team_id == tid, TeamMember.user_id == uid, TeamMember.status == "active"))
if m is None:
raise HTTPException(status_code=404, detail="成员不存在")
if m.role == "owner":
raise HTTPException(status_code=400, detail="不能移出团队创建者")
m.status = "left"
m.left_at = _now()
await db.session.commit()
await write_audit(db, action="team.remove_member", resource="team",
resource_id=tid, detail=f"target={uid}", user=user, request=request)
return {"ok": True}
@router.post("/{tid}/leave", summary="退出团队")
async def leave_team(
tid: str,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
team = await _get_team(db, tid)
if team.owner_user_id == user["id"]:
raise HTTPException(status_code=400, detail="团队创建者不能退出,可解散或升级")
m = await db.session.scalar(select(TeamMember).where(
TeamMember.team_id == tid, TeamMember.user_id == user["id"], TeamMember.status == "active"))
if m is None:
raise HTTPException(status_code=404, detail="你不在该团队中")
m.status = "left"
m.left_at = _now()
await db.session.commit()
await write_audit(db, action="team.leave", resource="team",
resource_id=tid, user=user, request=request)
return {"ok": True}
class TeamUpgrade(BaseModel):
"""升级为企业:走企业入驻申请流程(复用 /admin/ent/companies/apply)。"""
entry_type: str = "park_entered" # park_entered/independent
tenant_id: str = ""
company_name: str = ""
legal_person: str = ""
legal_phone: str = ""
contact_phone: str = ""
address: str = ""
industry: str = ""
registered_capital: str = ""
company_type: str = ""
bio: str = ""
docs: dict = {}
@router.post("/{tid}/upgrade", summary="团队升级为企业(提交企业入驻申请)")
async def upgrade_team(
tid: str,
body: TeamUpgrade,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
team = await _get_team(db, tid)
if team.owner_user_id != user["id"]:
raise HTTPException(status_code=403, detail="仅团队创建者可升级企业")
if team.status == "upgraded":
raise HTTPException(status_code=400, detail="该团队已升级为企业")
# 复用企业入驻申请
from .rbac_ent_onboard import CompanyApply, _apply_company_impl
payload = CompanyApply(
entry_type=body.entry_type, tenant_id=body.tenant_id,
company_name=body.company_name, legal_person=body.legal_person,
legal_phone=body.legal_phone, contact_phone=body.contact_phone,
address=body.address, industry=body.industry,
registered_capital=body.registered_capital, company_type=body.company_type,
bio=body.bio, docs=body.docs, upgrade_from_team_id=tid,
)
return await _apply_company_impl(db, user, request, payload)