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:
+137
-1
@@ -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"])
|
||||
|
||||
@@ -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":
|
||||
|
||||
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user