76351cc91a
- _normalize_org_member 统一 user_id/name/username/avatar/role/is_admin - 机构成员无头像时批量从 users 表补查
183 lines
7.7 KiB
Python
183 lines
7.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""组织/租户端点:企业 / 载体 / 服务商(按角色 + 组织作用域)。"""
|
||
from __future__ import annotations
|
||
|
||
import logging
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException
|
||
from pydantic import BaseModel
|
||
from sqlalchemy import select
|
||
|
||
from ..dependencies import get_db, get_user_organizations, get_user_primary_org
|
||
from ..schemas.org import OrgMemberAdd
|
||
from ...rbac import require_roles
|
||
from ...infrastructure.repositories import Database
|
||
from ...infrastructure.oss import resolve_url
|
||
from ...infrastructure.models import User
|
||
from ...park import tenants as tnt
|
||
|
||
log = logging.getLogger("rbac.org")
|
||
|
||
router = APIRouter(tags=["org"])
|
||
|
||
|
||
async def _normalize_org_member(raw: dict) -> dict:
|
||
"""统一机构/企业成员返回格式,确保前端 OrgMember 类型字段齐全。
|
||
|
||
前端期望字段:user_id, name, username, avatar, role, is_admin
|
||
企业成员(tnt.company_members)返回:id, username, nickname, avatar, role, is_admin
|
||
机构成员(db.org_members.list_for_org)返回:user_id, username, nickname, role, is_admin(无avatar)
|
||
"""
|
||
user_id = str(raw.get("user_id") or raw.get("id") or "")
|
||
username = raw.get("username") or ""
|
||
nickname = raw.get("nickname") or ""
|
||
name = nickname or username or user_id
|
||
avatar = raw.get("avatar") or ""
|
||
return {
|
||
"user_id": user_id,
|
||
"name": name,
|
||
"username": username,
|
||
"avatar": resolve_url(avatar),
|
||
"role": raw.get("role") or "",
|
||
"is_admin": bool(raw.get("is_admin", False)),
|
||
}
|
||
|
||
|
||
@router.get("/org/me", summary="当前组织信息")
|
||
async def org_me(
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("opc_member", "carrier", "operator")),
|
||
):
|
||
"""返回用户绑定的所有组织(优先 organization_members,回退 users.org_id)。"""
|
||
orgs = await get_user_organizations(db, user["id"])
|
||
region = await db.regions.get(user["region_id"]) if user.get("region_id") else None
|
||
primary_org = orgs[0] if orgs else None
|
||
|
||
# 企业成员关系(company_members 多对多;前端据此渲染企业切换/企业权限)
|
||
company_memberships = []
|
||
for m in await db.company_members.list_by_user(user["id"]):
|
||
co = await tnt.get_company(m["company_id"])
|
||
company_memberships.append({
|
||
"company_id": m["company_id"],
|
||
"company_name": (co or {}).get("name", ""),
|
||
"is_admin": bool(m.get("is_admin")),
|
||
"member_type": m.get("member_type", ""),
|
||
"status": m.get("status", ""),
|
||
"joined_at": m.get("created_at", ""),
|
||
})
|
||
|
||
return {
|
||
"user_id": user["id"],
|
||
"role": user["role"],
|
||
"sub_role": user["sub_role"],
|
||
"org": primary_org,
|
||
"organizations": orgs,
|
||
"region": region,
|
||
"company_memberships": company_memberships,
|
||
}
|
||
|
||
|
||
@router.get("/carrier/enterprises", summary="载体下辖企业")
|
||
async def carrier_enterprises(
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("carrier")),
|
||
):
|
||
"""返回绑定到当前载体(parent_id == 载体 org_id)的企业。"""
|
||
carrier_org = await get_user_primary_org(db, user, org_type="carrier")
|
||
if not carrier_org:
|
||
return []
|
||
return await db.orgs.list_by_parent(carrier_org["org_id"])
|
||
|
||
|
||
# ── 机构成员(机构主账号 + 机构内子账号,账号可挂多机构)────────────────────
|
||
|
||
@router.get("/org/{org_id}/members", summary="机构成员列表")
|
||
async def org_members(
|
||
org_id: str,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("opc_member", "carrier", "operator")),
|
||
):
|
||
# 叠加制权限:operator 全量;本 org 的载体机构管理员;本企业的企业成员(org_id 亦可能是 company_id)
|
||
caps = user.get("capabilities") or []
|
||
if "operator" in caps:
|
||
log.info(f"[org_members] operator access, org_id={org_id}, user_id={user['id']}")
|
||
elif await db.org_members.is_admin(org_id, user["id"]):
|
||
log.info(f"[org_members] org admin access, org_id={org_id}, user_id={user['id']}")
|
||
elif await db.company_members.get(user["id"], org_id):
|
||
log.info(f"[org_members] company member access, org_id={org_id}, user_id={user['id']}")
|
||
else:
|
||
log.warning(f"[org_members] 403 forbidden, org_id={org_id}, user_id={user['id']}, caps={caps}")
|
||
raise HTTPException(status_code=403, detail="无权查看该组织成员")
|
||
|
||
# org_id 可能是 organization_members.org_id(载体机构),也可能被前端当作 company_id 传入:
|
||
# 优先返回企业成员(命中企业时),否则回退到机构成员。
|
||
try:
|
||
company_members = await tnt.company_members(org_id)
|
||
log.info(f"[org_members] tnt.company_members returned {len(company_members)} members for org_id={org_id}")
|
||
except Exception as e:
|
||
log.error(f"[org_members] tnt.company_members failed for org_id={org_id}: {e}", exc_info=True)
|
||
company_members = []
|
||
|
||
if company_members:
|
||
items = [await _normalize_org_member(m) for m in company_members]
|
||
log.info(f"[org_members] returning {len(items)} company members for org_id={org_id}")
|
||
return {"items": items}
|
||
|
||
try:
|
||
org_members_list = await db.org_members.list_for_org(org_id)
|
||
log.info(f"[org_members] db.org_members.list_for_org returned {len(org_members_list)} members for org_id={org_id}")
|
||
except Exception as e:
|
||
log.error(f"[org_members] db.org_members.list_for_org failed for org_id={org_id}: {e}", exc_info=True)
|
||
org_members_list = []
|
||
|
||
# 机构成员没有 avatar 字段,批量从 users 表补查
|
||
user_ids = [str(m.get("user_id") or "") for m in org_members_list if m.get("user_id")]
|
||
avatar_map = {}
|
||
if user_ids:
|
||
try:
|
||
users = (await db.session.execute(
|
||
select(User).where(User.id.in_(user_ids))
|
||
)).scalars().all()
|
||
avatar_map = {u.id: (u.avatar or "") for u in users}
|
||
except Exception as e:
|
||
log.error(f"[org_members] failed to fetch avatars: {e}", exc_info=True)
|
||
avatar_map = {}
|
||
for m in org_members_list:
|
||
uid = str(m.get("user_id") or "")
|
||
if uid and not m.get("avatar"):
|
||
m["avatar"] = avatar_map.get(uid, "")
|
||
items = [await _normalize_org_member(m) for m in org_members_list]
|
||
log.info(f"[org_members] returning {len(items)} org members for org_id={org_id}")
|
||
return {"items": items}
|
||
|
||
|
||
@router.post("/org/{org_id}/members", summary="添加机构成员(子账号)")
|
||
async def org_add_member(
|
||
org_id: str,
|
||
req: OrgMemberAdd,
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("carrier", "operator")),
|
||
):
|
||
if "operator" not in (user.get("capabilities") or []) and not await db.org_members.is_admin(org_id, user["id"]):
|
||
raise HTTPException(status_code=403, detail="Forbidden: not org admin")
|
||
target_id = req.user_id
|
||
if not target_id and req.username:
|
||
found = await db.users.get_by_username(req.username)
|
||
if found is None:
|
||
raise HTTPException(status_code=404, detail="账号不存在")
|
||
target_id = found["id"]
|
||
if not target_id:
|
||
raise HTTPException(status_code=400, detail="缺少 user_id 或 username")
|
||
return await db.org_members.add_member(org_id, target_id, req.role, req.is_admin)
|
||
|
||
|
||
@router.get("/me/orgs", summary="我加入的机构")
|
||
async def my_orgs(
|
||
db: Database = Depends(get_db),
|
||
user: dict = Depends(require_roles("opc_member", "carrier", "operator")),
|
||
):
|
||
"""返回当前账号挂靠的所有机构及成员角色(支持一账号多机构)。"""
|
||
from ...services.org_service import OrgService
|
||
|
||
return await OrgService(db).my_orgs(user["id"])
|