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 幂等创建
This commit is contained in:
Pine
2026-09-07 16:15:35 +08:00
parent a97c09086a
commit 1cd26e174e
4 changed files with 900 additions and 0 deletions
+504
View File
@@ -0,0 +1,504 @@
# -*- coding: utf-8 -*-
"""企业身份链路(/admin/ent 扩展):认领企业 + 企业入驻申请。
**认领企业**(未绑定企业用户 → 认领园区入驻企业):
- 仅支持搜索(选园区 → 搜企业),不支持列表;
- 认领目标必须是「园区入驻企业」(tenant_id 非空);
- claim_role=admin:仅当企业无管理员时可认领,园区管理员审核;
- claim_role=member:有企业管理员则企业管理员审核,无则园区管理员审核;
- 证明资料(姓名/身份证/手机号)仅存证供审核端查看。
**企业入驻申请**(已有企业加入平台):
- entry_type=park_entered:入驻园区,园区管理员审核;
- entry_type=independent:不入驻园区,运营方审核;
- 审核通过后创建 ParkCompany,提交人自动成为企业管理员。
"""
from __future__ import annotations
import json
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 (
CompanyApplyRequest, CompanyClaimRequest, CompanyMember,
ParkCompany, ParkTenant, User,
)
from ...infrastructure.repositories import Database, new_id, utcnow_iso
router = APIRouter(prefix="/admin/ent", tags=["enterprise-onboard"])
def _now() -> str:
return utcnow_iso()
def _claim_dict(c) -> dict:
return {
"id": c.id, "user_id": c.user_id, "username": c.username,
"company_id": c.company_id, "company_name": c.company_name,
"tenant_id": c.tenant_id, "tenant_name": c.tenant_name,
"claim_role": c.claim_role, "real_name": c.real_name,
"id_card": c.id_card, "phone": c.phone,
"status": c.status, "review_level": c.review_level,
"reviewer_id": c.reviewer_id, "reviewer_name": c.reviewer_name,
"review_comment": c.review_comment, "reviewed_at": c.reviewed_at,
"created_at": c.created_at,
}
def _apply_dict(a) -> dict:
try:
docs = json.loads(a.docs_json or "{}")
except Exception: # noqa: BLE001
docs = {}
return {
"id": a.id, "user_id": a.user_id, "username": a.username,
"entry_type": a.entry_type, "tenant_id": a.tenant_id,
"tenant_name": a.tenant_name, "company_name": a.company_name,
"legal_person": a.legal_person, "legal_phone": a.legal_phone,
"contact_phone": a.contact_phone, "address": a.address,
"industry": a.industry, "registered_capital": a.registered_capital,
"company_type": a.company_type, "bio": a.bio, "docs": docs,
"upgrade_from_team_id": a.upgrade_from_team_id,
"status": a.status, "review_level": a.review_level,
"reviewer_id": a.reviewer_id, "reviewer_name": a.reviewer_name,
"review_comment": a.review_comment, "reviewed_at": a.reviewed_at,
"created_at": a.created_at,
}
# ---------------------------------------------------------------------------
# 认领企业
# ---------------------------------------------------------------------------
class ClaimSubmit(BaseModel):
company_id: str
claim_role: str = "member" # admin/member
real_name: str = ""
id_card: str = ""
phone: str = ""
@router.get("/claims/search", summary="认领企业搜索(仅园区入驻企业,按园区过滤)")
async def claim_search(
tenant_id: str,
q: str = "",
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
tenant_id = (tenant_id or "").strip()
if not tenant_id:
raise HTTPException(status_code=400, detail="请先选择园区")
t = (await db.session.execute(
select(ParkTenant).where(ParkTenant.id == tenant_id))).scalars().first()
if t is None:
raise HTTPException(status_code=404, detail="园区不存在")
stmt = select(ParkCompany).where(
ParkCompany.tenant_id == tenant_id,
ParkCompany.status == "active",
ParkCompany.company_kind == "park_entered",
)
kw = (q or "").strip()
if kw:
stmt = stmt.where(ParkCompany.name.like(f"%{kw}%"))
rows = (await db.session.scalars(stmt.order_by(ParkCompany.created_at.desc()).limit(50))).all()
items = []
for c in rows:
admins = (await db.session.scalars(
select(CompanyMember).where(CompanyMember.company_id == c.id,
CompanyMember.is_admin.is_(True),
CompanyMember.status == "active"))).all()
items.append({
"id": c.id, "name": c.name, "zone": c.zone, "room": c.room,
"industry": c.industry, "bio": c.bio,
"has_admin": bool(admins),
})
return {"items": items}
@router.post("/claims", summary="提交企业认领申请")
async def submit_claim(
body: ClaimSubmit,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
cid = (body.company_id or "").strip()
role = (body.claim_role or "member").strip()
if role not in ("admin", "member"):
raise HTTPException(status_code=400, detail="认领角色不合法(admin/member")
if not (body.real_name.strip() and body.id_card.strip() and body.phone.strip()):
raise HTTPException(status_code=400, detail="请填写姓名、身份证号码、手机号证明资料")
comp = (await db.session.execute(
select(ParkCompany).where(ParkCompany.id == cid))).scalars().first()
if comp is None:
raise HTTPException(status_code=404, detail="企业不存在")
if comp.tenant_id is None or not comp.tenant_id:
raise HTTPException(status_code=400, detail="仅可认领园区入驻企业")
# 已绑定该企业则无需认领
existing = await db.company_members.get(user["id"], cid)
if existing is not None and existing.get("status") == "active":
raise HTTPException(status_code=400, detail="你已在该企业中,无需认领")
dup = (await db.session.scalar(
select(CompanyClaimRequest).where(
CompanyClaimRequest.user_id == user["id"],
CompanyClaimRequest.company_id == cid,
CompanyClaimRequest.status == "pending")))
if dup is not None:
raise HTTPException(status_code=400, detail="已有待审核的认领申请")
admins = (await db.session.scalars(
select(CompanyMember).where(CompanyMember.company_id == cid,
CompanyMember.is_admin.is_(True),
CompanyMember.status == "active"))).all()
has_admin = bool(admins)
if role == "admin" and has_admin:
raise HTTPException(status_code=400, detail="该企业已有管理员,只能认领为企业成员")
# 审核人:admin→园区管理员;member→有企业管理员则企业管理员,否则园区管理员
review_level = "park_admin"
if role == "member" and has_admin:
review_level = "company_admin"
t = (await db.session.execute(
select(ParkTenant).where(ParkTenant.id == comp.tenant_id))).scalars().first()
req = CompanyClaimRequest(
id=new_id("claim"), user_id=user["id"], username=user.get("username", ""),
company_id=cid, company_name=comp.name,
tenant_id=comp.tenant_id or "", tenant_name=(t.name if t else "") or "",
claim_role=role, real_name=body.real_name.strip(),
id_card=body.id_card.strip(), phone=body.phone.strip(),
status="pending", review_level=review_level, created_at=_now(),
)
db.session.add(req)
await db.session.commit()
await write_audit(db, action="ent.claim_submit", resource="company_claim_request",
resource_id=req.id, detail=f"company={comp.name} role={role}", user=user, request=request)
return {"ok": True, "id": req.id, "review_level": review_level}
@router.get("/claims/mine", summary="我的认领申请")
async def my_claims(
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
rows = (await db.session.scalars(
select(CompanyClaimRequest).where(CompanyClaimRequest.user_id == user["id"])
.order_by(CompanyClaimRequest.created_at.desc()))).all()
return {"items": [_claim_dict(r) for r in rows]}
async def _is_park_admin(db: Database, tenant_id: str, user: dict) -> bool:
"""当前用户是否为该园区管理员(载体账号绑定)。"""
if not tenant_id:
return False
t = (await db.session.execute(
select(ParkTenant).where(ParkTenant.id == tenant_id))).scalars().first()
return bool(t and t.operator_user_id and t.operator_user_id == user.get("id", ""))
@router.get("/claims/reviews", summary="待我审核的认领申请(园区管理员/企业管理员)")
async def claim_reviews(
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
items = []
# 园区管理员:本园区的 park_admin 认领(admin 认领 + 无管理员的成员认领)
tids = (await db.session.scalars(
select(ParkTenant.id).where(ParkTenant.operator_user_id == user.get("id", "")))).all()
if tids:
park_rows = (await db.session.scalars(
select(CompanyClaimRequest).where(
CompanyClaimRequest.tenant_id.in_(tids),
CompanyClaimRequest.status == "pending",
CompanyClaimRequest.review_level == "park_admin")
.order_by(CompanyClaimRequest.created_at.desc()))).all()
items += [_claim_dict(r) for r in park_rows]
# 企业管理员:本企业的 company_admin 认领
for m in await db.company_members.admin_of(user["id"]):
co_rows = (await db.session.scalars(
select(CompanyClaimRequest).where(
CompanyClaimRequest.company_id == m["company_id"],
CompanyClaimRequest.status == "pending",
CompanyClaimRequest.review_level == "company_admin")
.order_by(CompanyClaimRequest.created_at.desc()))).all()
items += [_claim_dict(r) for r in co_rows]
return {"items": items}
class ReviewAction(BaseModel):
action: str = "approve" # approve/reject
comment: str = ""
@router.post("/claims/{cid}/review", summary="认领申请审核")
async def review_claim(
cid: str,
body: ReviewAction,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
if body.action not in ("approve", "reject"):
raise HTTPException(status_code=400, detail="审核动作不合法")
req = (await db.session.execute(
select(CompanyClaimRequest).where(CompanyClaimRequest.id == cid))).scalars().first()
if req is None:
raise HTTPException(status_code=404, detail="申请不存在")
if req.status != "pending":
raise HTTPException(status_code=400, detail="该申请已处理")
# 审核权校验
if req.review_level == "park_admin":
if not await _is_park_admin(db, req.tenant_id, user):
raise HTTPException(status_code=403, detail="仅该园区管理员可审核")
else: # company_admin
mine = await db.company_members.get(user["id"], req.company_id)
if mine is None or not mine.get("is_admin") or mine.get("status") != "active":
raise HTTPException(status_code=403, detail="仅该企业管理员可审核")
if body.action == "reject":
req.status = "rejected"
req.review_comment = body.comment.strip()
req.reviewer_id = user.get("id", "")
req.reviewer_name = user.get("username", "")
req.reviewed_at = _now()
await db.session.commit()
await write_audit(db, action="ent.claim_reject", resource="company_claim_request",
resource_id=req.id, detail=f"company={req.company_name}", user=user, request=request)
return {"ok": True, "status": "rejected"}
# approve:复查约束后落成员关系
admins = (await db.session.scalars(
select(CompanyMember).where(CompanyMember.company_id == req.company_id,
CompanyMember.is_admin.is_(True),
CompanyMember.status == "active"))).all()
if req.claim_role == "admin" and admins:
req.status = "rejected"
req.review_comment = "该企业已有管理员,认领失败"
req.reviewer_id = user.get("id", "")
req.reviewer_name = user.get("username", "")
req.reviewed_at = _now()
await db.session.commit()
return {"ok": False, "status": "rejected", "reason": "该企业已有管理员"}
is_admin = req.claim_role == "admin"
await db.company_members.add(
req.user_id, req.company_id,
is_admin=is_admin,
member_type="admin" if is_admin else "staff",
created_by=user.get("id", ""),
)
if is_admin:
comp = (await db.session.execute(
select(ParkCompany).where(ParkCompany.id == req.company_id))).scalars().first()
if comp is not None:
comp.owner_user_id = req.user_id
req.status = "approved"
req.review_comment = body.comment.strip()
req.reviewer_id = user.get("id", "")
req.reviewer_name = user.get("username", "")
req.reviewed_at = _now()
await db.session.commit()
await write_audit(db, action="ent.claim_approve", resource="company_claim_request",
resource_id=req.id, detail=f"company={req.company_name} role={req.claim_role}",
user=user, request=request)
return {"ok": True, "status": "approved"}
# ---------------------------------------------------------------------------
# 企业入驻申请
# ---------------------------------------------------------------------------
class CompanyApply(BaseModel):
entry_type: str = "park_entered" # park_entered/independent
tenant_id: str = "" # park_entered 必填
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 = {}
upgrade_from_team_id: str = ""
@router.post("/companies/apply", summary="企业入驻申请(已有企业加入平台)")
async def apply_company(
body: CompanyApply,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
return await _apply_company_impl(db, user, request, body)
async def _apply_company_impl(db: Database, user: dict, request: Request, body: CompanyApply) -> dict:
"""企业入驻申请校验+落库(供 /ent/teams/{tid}/upgrade 复用)。"""
entry = (body.entry_type or "park_entered").strip()
if entry not in ("park_entered", "independent"):
raise HTTPException(status_code=400, detail="入驻类型不合法")
name = (body.company_name or "").strip()
if not name:
raise HTTPException(status_code=400, detail="请填写企业名称")
tenant_id = (body.tenant_id or "").strip()
if entry == "park_entered":
if not tenant_id:
raise HTTPException(status_code=400, detail="入驻园区必须选择园区")
t = (await db.session.execute(
select(ParkTenant).where(ParkTenant.id == tenant_id))).scalars().first()
if t is None:
raise HTTPException(status_code=404, detail="园区不存在")
tenant_name = t.name
else:
tenant_id = ""
tenant_name = ""
# 同名企业已存在则不能重复入驻
dup_co = (await db.session.scalar(
select(ParkCompany).where(ParkCompany.name == name)))
if dup_co is not None:
raise HTTPException(status_code=400, detail="该企业已入驻平台,可前往「认领企业」认领")
dup = (await db.session.scalar(
select(CompanyApplyRequest).where(
CompanyApplyRequest.user_id == user["id"],
CompanyApplyRequest.company_name == name,
CompanyApplyRequest.status == "pending")))
if dup is not None:
raise HTTPException(status_code=400, detail="已有同名企业待审核申请")
review_level = "park_admin" if entry == "park_entered" else "operator"
rec = CompanyApplyRequest(
id=new_id("apply"), user_id=user["id"], username=user.get("username", ""),
entry_type=entry, tenant_id=tenant_id, tenant_name=tenant_name,
company_name=name, legal_person=body.legal_person.strip(),
legal_phone=body.legal_phone.strip(), contact_phone=body.contact_phone.strip(),
address=body.address.strip(), industry=body.industry.strip(),
registered_capital=body.registered_capital.strip(),
company_type=body.company_type.strip(), bio=body.bio.strip(),
docs_json=json.dumps(body.docs or {}, ensure_ascii=False),
upgrade_from_team_id=(body.upgrade_from_team_id or "").strip(),
status="pending", review_level=review_level, created_at=_now(),
)
db.session.add(rec)
await db.session.commit()
await write_audit(db, action="ent.company_apply", resource="company_apply_request",
resource_id=rec.id, detail=f"company={name} entry={entry}",
user=user, request=request)
return {"ok": True, "id": rec.id, "review_level": review_level}
@router.get("/companies/applies/mine", summary="我的企业入驻申请")
async def my_applies(
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
rows = (await db.session.scalars(
select(CompanyApplyRequest).where(CompanyApplyRequest.user_id == user["id"])
.order_by(CompanyApplyRequest.created_at.desc()))).all()
return {"items": [_apply_dict(r) for r in rows]}
@router.get("/companies/applies/reviews", summary="待审企业入驻申请(园区管理员/运营方)")
async def apply_reviews(
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
items = []
if user.get("role") == "operator":
op_rows = (await db.session.scalars(
select(CompanyApplyRequest).where(
CompanyApplyRequest.status == "pending",
CompanyApplyRequest.review_level == "operator")
.order_by(CompanyApplyRequest.created_at.desc()))).all()
items += [_apply_dict(r) for r in op_rows]
tids = (await db.session.scalars(
select(ParkTenant.id).where(ParkTenant.operator_user_id == user.get("id", "")))).all()
if tids:
park_rows = (await db.session.scalars(
select(CompanyApplyRequest).where(
CompanyApplyRequest.tenant_id.in_(tids),
CompanyApplyRequest.status == "pending",
CompanyApplyRequest.review_level == "park_admin")
.order_by(CompanyApplyRequest.created_at.desc()))).all()
items += [_apply_dict(r) for r in park_rows]
return {"items": items}
@router.post("/companies/applies/{aid}/review", summary="企业入驻申请审核")
async def review_apply(
aid: str,
body: ReviewAction,
request: Request,
db: Database = Depends(get_db),
user: dict = Depends(get_current_user),
):
if body.action not in ("approve", "reject"):
raise HTTPException(status_code=400, detail="审核动作不合法")
rec = (await db.session.execute(
select(CompanyApplyRequest).where(CompanyApplyRequest.id == aid))).scalars().first()
if rec is None:
raise HTTPException(status_code=404, detail="申请不存在")
if rec.status != "pending":
raise HTTPException(status_code=400, detail="该申请已处理")
# 审核权校验
if rec.review_level == "operator":
if user.get("role") != "operator":
raise HTTPException(status_code=403, detail="仅运营方可审核该申请")
else:
if not await _is_park_admin(db, rec.tenant_id, user):
raise HTTPException(status_code=403, detail="仅该园区管理员可审核")
if body.action == "reject":
rec.status = "rejected"
rec.review_comment = body.comment.strip()
rec.reviewer_id = user.get("id", "")
rec.reviewer_name = user.get("username", "")
rec.reviewed_at = _now()
await db.session.commit()
await write_audit(db, action="ent.company_apply_reject", resource="company_apply_request",
resource_id=rec.id, detail=f"company={rec.company_name}", user=user, request=request)
return {"ok": True, "status": "rejected"}
# approve:同名企业复查 → 创建企业 + 提交人设管理员
dup_co = (await db.session.scalar(
select(ParkCompany).where(ParkCompany.name == rec.company_name)))
if dup_co is not None:
rec.status = "rejected"
rec.review_comment = "企业已存在,请改用认领"
rec.reviewer_id = user.get("id", "")
rec.reviewer_name = user.get("username", "")
rec.reviewed_at = _now()
await db.session.commit()
return {"ok": False, "status": "rejected", "reason": "企业已存在"}
kind = "park_entered" if rec.entry_type == "park_entered" else "independent"
comp = ParkCompany(
id=new_id("PC"), tenant_id=rec.tenant_id or None,
company_kind=kind, name=rec.company_name,
industry=rec.industry, bio=rec.bio, founder=rec.legal_person,
status="active", legal_person=rec.legal_person, legal_phone=rec.legal_phone,
contact_phone=rec.contact_phone, address=rec.address,
registered_capital=rec.registered_capital, company_type=rec.company_type,
owner_user_id=rec.user_id, created_at=_now(),
)
db.session.add(comp)
await db.session.flush()
await db.company_members.add(rec.user_id, comp.id, is_admin=True,
member_type="owner", created_by=user.get("id", ""))
rec.status = "approved"
rec.review_comment = body.comment.strip()
rec.reviewer_id = user.get("id", "")
rec.reviewer_name = user.get("username", "")
rec.reviewed_at = _now()
await db.session.commit()
# 团队升级:标记团队已升级并关联企业
if rec.upgrade_from_team_id:
from ...infrastructure.models import Team
team = (await db.session.execute(
select(Team).where(Team.id == rec.upgrade_from_team_id))).scalars().first()
if team is not None and team.owner_user_id == rec.user_id:
team.status = "upgraded"
team.upgraded_company_id = comp.id
await db.session.commit()
await write_audit(db, action="ent.company_apply_approve", resource="company_apply_request",
resource_id=rec.id, detail=f"company={rec.company_name} company_id={comp.id}",
user=user, request=request)
return {"ok": True, "status": "approved", "company_id": comp.id}
+292
View File
@@ -0,0 +1,292 @@
# -*- 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)
+100
View File
@@ -2342,3 +2342,103 @@ class ContentReport(Base):
processed_at: Mapped[str] = mapped_column(String(32), default="")
processor_id: Mapped[str] = mapped_column(String(64), default="")
result: Mapped[str] = mapped_column(String(255), default="")
# ===========================================================================
# 企业认领 / 企业入驻申请 / 团队(0062 身份链路上线)
# ===========================================================================
class CompanyClaimRequest(Base):
"""企业认领申请(未绑定企业用户 → 认领园区入驻企业)。
claim_role: admin(认领为企业管理员,仅当企业无管理员时允许)/ member(认领为成员)。
审核人:admin 认领 → 园区管理员;member 认领 → 有企业管理员则企业管理员,否则园区管理员。
证明资料(姓名/身份证/手机号)仅存证供审核端查看。
"""
__tablename__ = "company_claim_requests"
id: Mapped[str] = mapped_column(String, primary_key=True)
user_id: Mapped[str] = mapped_column(String, index=True) # 申请人平台用户 id
username: Mapped[str] = mapped_column(String, default="") # 申请人登录名
company_id: Mapped[str] = mapped_column(String, index=True) # 目标企业
company_name: Mapped[str] = mapped_column(String, default="") # 冗余企业名
tenant_id: Mapped[str] = mapped_column(String, default="") # 冗余所属园区
tenant_name: Mapped[str] = mapped_column(String, default="")
claim_role: Mapped[str] = mapped_column(String, default="member") # admin/member
# 证明资料(存证,审核端查看)
real_name: Mapped[str] = mapped_column(String, default="")
id_card: Mapped[str] = mapped_column(String, default="")
phone: Mapped[str] = mapped_column(String, default="")
status: Mapped[str] = mapped_column(String, default="pending") # pending/approved/rejected
review_level: Mapped[str] = mapped_column(String, default="park_admin") # park_admin/company_admin
reviewer_id: Mapped[str] = mapped_column(String, default="")
reviewer_name: Mapped[str] = mapped_column(String, default="")
review_comment: Mapped[str] = mapped_column(String, default="")
reviewed_at: Mapped[str] = mapped_column(String, default="")
created_at: Mapped[str] = mapped_column(String, default="")
class CompanyApplyRequest(Base):
"""企业入驻申请(已有企业加入平台)。
entry_type: park_entered(入驻园区,园区管理员审核)/ independent(不入驻园区,运营方审核)。
审核通过后创建 ParkCompany(kind 对应)并把提交人设为企业管理员。
"""
__tablename__ = "company_apply_requests"
id: Mapped[str] = mapped_column(String, primary_key=True)
user_id: Mapped[str] = mapped_column(String, index=True)
username: Mapped[str] = mapped_column(String, default="")
entry_type: Mapped[str] = mapped_column(String, default="park_entered") # park_entered/independent
tenant_id: Mapped[str] = mapped_column(String, default="") # 入驻园区时必填
tenant_name: Mapped[str] = mapped_column(String, default="")
company_name: Mapped[str] = mapped_column(String, default="")
legal_person: Mapped[str] = mapped_column(String, default="") # 法定代表人
legal_phone: Mapped[str] = mapped_column(String, default="")
contact_phone: Mapped[str] = mapped_column(String, default="")
address: Mapped[str] = mapped_column(String, default="")
industry: Mapped[str] = mapped_column(String, default="")
registered_capital: Mapped[str] = mapped_column(String, default="") # 注册资本(万元)
company_type: Mapped[str] = mapped_column(String, default="")
bio: Mapped[str] = mapped_column(Text, default="")
docs_json: Mapped[str] = mapped_column(Text, default="{}") # 营业执照等资料 URL
status: Mapped[str] = mapped_column(String, default="pending") # pending/approved/rejected
review_level: Mapped[str] = mapped_column(String, default="park_admin") # park_admin/operator
upgrade_from_team_id: Mapped[str] = mapped_column(String, default="") # 团队升级来源(非空=团队升级申请)
reviewer_id: Mapped[str] = mapped_column(String, default="")
reviewer_name: Mapped[str] = mapped_column(String, default="")
review_comment: Mapped[str] = mapped_column(String, default="")
reviewed_at: Mapped[str] = mapped_column(String, default="")
created_at: Mapped[str] = mapped_column(String, default="")
class Team(Base):
"""团队(非企业,创建即生效,无需审核;支持后续升级为企业)。"""
__tablename__ = "teams"
id: Mapped[str] = mapped_column(String, primary_key=True)
name: Mapped[str] = mapped_column(String, default="")
bio: Mapped[str] = mapped_column(Text, default="")
owner_user_id: Mapped[str] = mapped_column(String, index=True)
status: Mapped[str] = mapped_column(String, default="active") # active/upgraded
upgraded_company_id: Mapped[str] = mapped_column(String, default="") # 升级后的企业 id
created_at: Mapped[str] = mapped_column(String, default="")
class TeamMember(Base):
"""团队成员关系(多对多)。"""
__tablename__ = "team_members"
__table_args__ = (UniqueConstraint("user_id", "team_id", name="uq_team_member"),)
id: Mapped[str] = mapped_column(String, primary_key=True)
team_id: Mapped[str] = mapped_column(String, index=True)
user_id: Mapped[str] = mapped_column(String, index=True)
role: Mapped[str] = mapped_column(String, default="member") # owner/admin/member
status: Mapped[str] = mapped_column(String, default="active") # active/left
created_at: Mapped[str] = mapped_column(String, default="")
left_at: Mapped[str] = mapped_column(String, default="")
+4
View File
@@ -32,6 +32,8 @@ from app.api.routers import rbac_training as rbac_training_router
from app.api.routers import rbac_credit as rbac_credit_router
from app.api.routers import rbac_ecosystem as rbac_ecosystem_router
from app.api.routers import rbac_enterprise as rbac_enterprise_router
from app.api.routers import rbac_ent_onboard as rbac_ent_onboard_router
from app.api.routers import rbac_teams as rbac_teams_router
from app.api.routers import rbac_desktop_updates as rbac_desktop_updates_router
from app.api.routers import rbac_moderation as rbac_moderation_router
from app.api.routers import templates as templates_router
@@ -113,6 +115,8 @@ app.include_router(rbac_training_router.router)
app.include_router(rbac_credit_router.router)
app.include_router(rbac_ecosystem_router.router)
app.include_router(rbac_enterprise_router.router)
app.include_router(rbac_ent_onboard_router.router)
app.include_router(rbac_teams_router.router)
app.include_router(rbac_desktop_updates_router.router)
app.include_router(rbac_desktop_updates_router.public_router)
app.include_router(rbac_hall_router.community_router)