diff --git a/app/api/routers/auth.py b/app/api/routers/auth.py index e059f01..3dc335c 100644 --- a/app/api/routers/auth.py +++ b/app/api/routers/auth.py @@ -28,6 +28,7 @@ from ..schemas.auth import ( RevokeTokenRequest, SendCodeRequest, SendCodeResponse, + SelectIdentityRequest, UpdateProfileRequest, VerifyResponse, WxLoginRequest, @@ -138,6 +139,98 @@ def _is_phone(account: str) -> bool: _PROFILE_FIELDS = ("nickname", "account", "room", "avatar", "company_avatar", "email", "gender", "birthday") +async def _compute_identities(db: Database, user: dict) -> list[dict]: + """计算用户可用的所有身份列表(多角色叠加制)。 + + 身份类型: + - operator:平台运营方 + - carrier::园区管理员(每个园区一个) + - enterprise::企业管理员(每个企业一个) + - opc_member:个人用户(始终存在) + """ + from ...infrastructure.models import CompanyMember, ParkTenant, ParkCompany + from sqlalchemy import select + + uid = user.get("id", "") + identities: list[dict] = [] + role = (user.get("role") or "").lower() + + # 1. 运营方身份 + if role in ("operator", "operator_internal", "op_admin", "op_super_admin", "superadmin", "admin"): + identities.append({ + "id": "operator", + "port": "operator", + "role": "operator", + "sub_role": None, + "name": "平台运营方", + "org_id": None, + "region_id": user.get("region_id"), + "org_name": None, + "region_name": None, + "status": "active", + }) + + # 2. 园区管理员身份(park_tenants.operator_user_id) + park_rows = (await db.session.execute( + select(ParkTenant.id, ParkTenant.name).where(ParkTenant.operator_user_id == uid) + )).all() + for pid, pname in park_rows: + identities.append({ + "id": f"carrier:{pid}", + "port": "carrier", + "role": "carrier", + "sub_role": None, + "name": f"园区管理员 · {pname or pid}", + "org_id": pid, + "region_id": None, + "org_name": pname or "", + "region_name": None, + "status": "active", + }) + + # 3. 企业管理员身份(company_members.is_admin) + ent_rows = (await db.session.execute( + select(CompanyMember).where( + CompanyMember.user_id == uid, + CompanyMember.is_admin.is_(True), + CompanyMember.status == "active", + ) + )).scalars().all() + for m in ent_rows: + comp = (await db.session.execute( + select(ParkCompany).where(ParkCompany.id == m.company_id) + )).scalars().first() + comp_name = comp.name if comp else "" + identities.append({ + "id": f"enterprise:{m.company_id}", + "port": "enterprise", + "role": "opc_member", + "sub_role": "enterprise_admin", + "name": f"企业管理员 · {comp_name or m.company_id}", + "org_id": m.company_id, + "region_id": None, + "org_name": comp_name or "", + "region_name": None, + "status": "active", + }) + + # 4. 个人用户身份(始终存在) + identities.append({ + "id": "opc_member", + "port": "opc", + "role": "opc_member", + "sub_role": None, + "name": "个人用户", + "org_id": None, + "region_id": user.get("region_id"), + "org_name": None, + "region_name": None, + "status": "active", + }) + + return identities + + async def _issue_token( db: Database, user: dict, @@ -244,6 +337,7 @@ async def login(req: LoginRequest, db: Database = Depends(get_db)): token_record = await _issue_token(db, user, req.expires_in, identity=None) profile = await _profile_for(user, None, db) + identities = await _compute_identities(db, user) await db.audit.add( action="login", resource="auth", resource_id=user["id"], @@ -252,7 +346,49 @@ async def login(req: LoginRequest, db: Database = Depends(get_db)): ) resp = LoginResponse( token=token_record["token"], - identities=[], + identities=identities, + **profile, + ) + await _inject_mqtt_credentials(resp, user["id"]) + return resp + + +@router.post("/select-identity", response_model=LoginResponse, summary="切换当前身份") +async def select_identity( + req: SelectIdentityRequest, + db: Database = Depends(get_db), + user: dict = Depends(get_current_user), +): + """切换当前身份(运营方/园区管理员/企业管理员/个人),重新签发 JWT。 + + identity_id 格式: + - operator:平台运营方 + - carrier::园区管理员 + - enterprise::企业管理员 + - opc_member:个人用户 + """ + identities = await _compute_identities(db, user) + identity = next((i for i in identities if i["id"] == req.identity_id), None) + if identity is None: + raise HTTPException(status_code=400, detail="无效的身份 ID") + + # 构建身份字典供 _issue_token 使用 + identity_dict = { + "id": identity["id"], + "port": identity["port"], + "role": identity["role"], + "sub_role": identity.get("sub_role"), + "name": identity["name"], + "org_id": identity.get("org_id"), + "region_id": identity.get("region_id"), + } + + token_record = await _issue_token(db, user, None, identity=identity_dict) + profile = await _profile_for(user, identity_dict, db) + + resp = LoginResponse( + token=token_record["token"], + identities=identities, **profile, ) await _inject_mqtt_credentials(resp, user["id"]) diff --git a/app/api/routers/rbac_opc.py b/app/api/routers/rbac_opc.py index d7d0192..ff90c69 100644 --- a/app/api/routers/rbac_opc.py +++ b/app/api/routers/rbac_opc.py @@ -118,7 +118,7 @@ async def opc_policy( db: Database = Depends(get_db), _u: dict = Depends(require_roles("opc_member")), ): - items = await db.content.list(ctype="policy", status="published", public_only=True, scheduled_ready=True) + items = await db.content.list(ctype="policy", status="published", scheduled_ready=True) return {"items": [_resolve_content_media(it) for it in items]} @@ -148,7 +148,10 @@ async def opc_content_detail( user: dict | None = Depends(optional_current_user), ): item = await db.content.get(content_id) - if item is None or item.get("status") != "published" or not item.get("is_public"): + if item is None or item.get("status") != "published": + raise HTTPException(status_code=404, detail="内容不存在") + # 政策属于公开内容,不限制 is_public;其他类型要求 is_public=True + if item.get("type") != "policy" and not item.get("is_public"): raise HTTPException(status_code=404, detail="内容不存在") # 阅读数自增(每次详情浏览 +1) if item.get("status") == "published": diff --git a/app/api/schemas/auth.py b/app/api/schemas/auth.py index 9955350..adf31a3 100644 --- a/app/api/schemas/auth.py +++ b/app/api/schemas/auth.py @@ -47,6 +47,10 @@ class UpdateProfileRequest(BaseModel): company: str | None = None room: str | None = None avatar: str | None = None + +class SelectIdentityRequest(BaseModel): + """切换当前身份(运营方/园区管理员/企业管理员/个人)。""" + identity_id: str company_avatar: str | None = None class RevokeTokenRequest(BaseModel):