feat: 多身份切换功能 + 政策详情公开访问修复

1. 政策详情修复: 政策(type=policy)属于公开内容,详情接口不再要求is_public=True
   - 之前错误地加了public_only=True过滤,已撤销
   - 详情接口对政策类型绕过is_public检查

2. 多身份切换功能:
   - 新增_compute_identities函数,动态计算用户所有可用身份
     - operator: 平台运营方
     - carrier:<park_id>: 园区管理员(每个园区一个)
     - enterprise:<company_id>: 企业管理员(每个企业一个)
     - opc_member: 个人用户(始终存在)
   - 登录接口返回identities列表
   - 新增/auth/select-identity接口,切换身份后重新签发JWT
   - 新增SelectIdentityRequest schema
This commit is contained in:
Pine
2026-09-13 23:28:36 +08:00
parent d80550cb36
commit 407a8fcda4
3 changed files with 146 additions and 3 deletions
+137 -1
View File
@@ -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:<park_id>:园区管理员(每个园区一个)
- enterprise:<company_id>:企业管理员(每个企业一个)
- 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:<park_id>:园区管理员
- enterprise:<company_id>:企业管理员
- 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"])
+5 -2
View File
@@ -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":
+4
View File
@@ -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):