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:
@@ -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}
|
||||
@@ -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/admin,owner 不可移出)")
|
||||
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)
|
||||
@@ -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="")
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user