Files
server-core/app/api/routers/rbac_org.py
T
Pine 76351cc91a fix(org): 机构/企业成员统一返回格式(补齐姓名、头像)
- _normalize_org_member 统一 user_id/name/username/avatar/role/is_admin
- 机构成员无头像时批量从 users 表补查
2026-09-13 11:14:20 +08:00

183 lines
7.7 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 -*-
"""组织/租户端点:企业 / 载体 / 服务商(按角色 + 组织作用域)。"""
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"])