From cb1492f2d6b5707d0a4a93f59f80e6791b8cb19a Mon Sep 17 00:00:00 2001 From: Pine Date: Thu, 3 Sep 2026 09:43:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E5=AE=8C=E6=95=B4=E5=AE=A1=E6=9F=A5?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=20-=205=E4=B8=AA=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. _resolve_identity: permissions_for 未归一化 role → 使用 account_type+permission_role 2. rbac_org.py: 用 user.role 判断 operator → 改用 capabilities 3. membership_service: sync_user_affiliation 不同步 organization_members → 新增 _sync_org_members 4. repositories.to_profile: capabilities 只查旧表 → 增加 organization_members 查询 5. Header.RoleBadge: account_type_label 被 t() 重复翻译 → 优先直接显示中文 --- app/api/dependencies.py | 7 ++-- app/api/routers/rbac_org.py | 4 +- app/infrastructure/repositories.py | 35 ++++++++++++++++- app/services/membership_service.py | 63 +++++++++++++++++++++++++++++- 4 files changed, 100 insertions(+), 9 deletions(-) diff --git a/app/api/dependencies.py b/app/api/dependencies.py index a6f99a0..eab15ef 100644 --- a/app/api/dependencies.py +++ b/app/api/dependencies.py @@ -154,9 +154,10 @@ async def _resolve_identity(user: dict, db: Database, payload: dict | None = Non capabilities 优先从 JWT payload 读取(登录时已计算并写入),避免每次请求查库; 若 JWT 中无 capabilities(旧 token),则回退到实时计算。 """ - user["permissions"] = await db.roles.permissions_for( - user.get("role", "opc_member"), None, - ) + from ..domain.account_types import account_type, permission_role + atype = account_type(user.get("role")) + user["role"] = atype # 归一化为三种账号类型之一 + user["permissions"] = await db.roles.permissions_for(permission_role(atype), None) user["scope_region_ids"] = await db.regions.visible_region_ids(user.get("region_id")) user["scope_level"] = await db.regions.level(user.get("region_id")) if payload and payload.get("capabilities"): diff --git a/app/api/routers/rbac_org.py b/app/api/routers/rbac_org.py index a164c0e..a5754b2 100644 --- a/app/api/routers/rbac_org.py +++ b/app/api/routers/rbac_org.py @@ -53,7 +53,7 @@ async def org_members( user: dict = Depends(require_roles("carrier", "operator")), ): # 机构管理员本人 或 平台运营 可查看 - if user.get("role") not in ("operator",) and not await db.org_members.is_admin(org_id, user["id"]): + 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") return {"items": await db.org_members.list_for_org(org_id)} @@ -65,7 +65,7 @@ async def org_add_member( db: Database = Depends(get_db), user: dict = Depends(require_roles("carrier", "operator")), ): - if user.get("role") not in ("operator",) and not await db.org_members.is_admin(org_id, user["id"]): + 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: diff --git a/app/infrastructure/repositories.py b/app/infrastructure/repositories.py index a94f681..2d8e7a1 100644 --- a/app/infrastructure/repositories.py +++ b/app/infrastructure/repositories.py @@ -680,12 +680,43 @@ class UserRepository: pass profile["company_admin_ids"] = company_admin_ids caps = ["opc_member"] - if _acct_type(user.get("role")) == "operator": + atype = _acct_type(user.get("role")) + if atype == "operator": caps.append("operator") - if user.get("role") in ("carrier", "park", "park_staff", "carrier_staff") or is_park_admin: + # carrier 能力:role 为 carrier 或 统一表 organization_members 管理员 或 旧表管理员 + is_carrier = atype == "carrier" + if not is_carrier: + from .models import Organization, OrganizationMember + carrier_orgs = (await self.session.scalars( + select(Organization.id).join( + OrganizationMember, OrganizationMember.org_id == Organization.id + ).where( + OrganizationMember.user_id == user.get("id", ""), + OrganizationMember.role == "admin", + OrganizationMember.status == "active", + Organization.type == "carrier", + ) + )).all() + is_carrier = bool(carrier_orgs) or is_park_admin + if is_carrier: caps.append("carrier") + # enterprise 能力:统一表 organization_members 管理员 或 旧表管理员 if company_admin_ids: caps.append("enterprise") + else: + from .models import Organization, OrganizationMember + ent_orgs = (await self.session.scalars( + select(Organization.id).join( + OrganizationMember, OrganizationMember.org_id == Organization.id + ).where( + OrganizationMember.user_id == user.get("id", ""), + OrganizationMember.role == "admin", + OrganizationMember.status == "active", + Organization.type == "enterprise", + ) + )).all() + if ent_orgs: + caps.append("enterprise") profile["capabilities"] = caps profile["permissions"] = user.get("permissions", []) profile["scope_region_ids"] = user.get("scope_region_ids", []) diff --git a/app/services/membership_service.py b/app/services/membership_service.py index 2182686..53766e5 100644 --- a/app/services/membership_service.py +++ b/app/services/membership_service.py @@ -2,18 +2,77 @@ """成员关系服务:派生归属同步 + 转园迁移执行(users 单值归属的唯一合法写路径)。""" from __future__ import annotations -from ..infrastructure.models import ParkCompany, ParkTenant +from ..infrastructure.models import Organization, ParkCompany, ParkTenant from ..infrastructure.repositories import Database, utcnow_iso +async def _ensure_org(db: Database, org_id: str, org_type: str, name: str = "") -> Organization: + """确保 organization 记录存在,不存在则创建。""" + from sqlalchemy import select + + org = await db.session.get(Organization, org_id) + if org is None: + org = Organization( + id=org_id, name=name or org_id, type=org_type, + region_id=None, parent_id=None, created_at=utcnow_iso(), + ) + db.session.add(org) + await db.session.flush() + return org + + +async def _sync_org_members(db: Database, user_id: str) -> None: + """把 park_members.admin / company_members.is_admin 同步到 organization_members。 + + 统一成员关系表是权限计算的首选来源,必须与旧表保持一致。 + """ + from sqlalchemy import select + from ..infrastructure.models import OrganizationMember + + now = utcnow_iso() + + # 同步园区管理员 → organization_members (type=carrier) + parks = await db.park_members.list_by_user(user_id) + for pm in parks: + if pm.get("member_type") != "admin" or pm.get("status") != "active": + continue + tenant = await db.session.scalar(select(ParkTenant).where(ParkTenant.id == pm["park_id"])) + await _ensure_org(db, pm["park_id"], "carrier", tenant.name if tenant else "") + existing = await db.session.get(OrganizationMember, (pm["park_id"], user_id)) + if existing is None: + db.session.add(OrganizationMember( + org_id=pm["park_id"], user_id=user_id, + role="admin", is_admin=True, status="active", joined_at=now, + )) + + # 同步企业管理员 → organization_members (type=enterprise) + comps = await db.company_members.list_by_user(user_id) + for cm in comps: + if not cm.get("is_admin") or cm.get("status") != "active": + continue + comp = await db.session.scalar(select(ParkCompany).where(ParkCompany.id == cm["company_id"])) + await _ensure_org(db, cm["company_id"], "enterprise", comp.name if comp else "") + existing = await db.session.get(OrganizationMember, (cm["company_id"], user_id)) + if existing is None: + db.session.add(OrganizationMember( + org_id=cm["company_id"], user_id=user_id, + role="admin", is_admin=True, status="active", joined_at=now, + )) + + await db.session.flush() + + async def sync_user_affiliation(db: Database, user_id: str) -> None: - """重算 users.affiliation / park_id / park_name(派生展示缓存)。 + """重算 users.affiliation / park_id / park_name(派生展示缓存),并同步 organization_members。 主归属规则:member_type=admin 优先,其次最早创建的 active 园区成员; 无任何园区成员 → independent。企业展示字段取 is_admin 企业名(否则最早一条)。 """ from sqlalchemy import select + # 先同步统一成员关系表 + await _sync_org_members(db, user_id) + parks = await db.park_members.list_by_user(user_id) fields: dict = {} if parks: