dbc29eee59
- park_members/company_members 多对多成员表(0028),users.affiliation 为派生缓存由 sync_user_affiliation 刷新 - 转园两级审核状态机(0029):pending_from→pending_to→approved,支持用户/企业整体转园与运营端兜底 - 叠加制身份(0030):身份恒为运营/载体/OPC,企业管理员为能力叠加非身份,去除 enterprise/carrier 直写角色 - rbac 新增 memberships/park-transfers/企业管理员工作台 /admin/ent/* 接口 Co-Authored-By: Claude <noreply@anthropic.com>
94 lines
4.0 KiB
Python
94 lines
4.0 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""转园申请两级审核状态机(纯规则,无 DB)。
|
||
|
||
流程:
|
||
pending_from(待转出园区审)
|
||
→ 转出园区 approved → pending_to(待转入园区审)
|
||
→ 转入园区 approved → approved(执行迁移)
|
||
任一级 rejected → rejected(终态)
|
||
operator 任意时刻可 force-approve / reject(兜底终审)。
|
||
园区未设管理员时该级自动通过(apply 时直接跳级)。
|
||
|
||
发起人:operator / from-park carrier / to-park carrier / 企业管理员 / 用户本人。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
STAGES = ("pending_from", "pending_to", "approved", "rejected", "cancelled")
|
||
INITIATOR_ROLES = ("operator", "carrier", "enterprise", "user")
|
||
TARGET_KINDS = ("user", "company")
|
||
|
||
|
||
class TransferError(Exception):
|
||
"""转园流程违规(路由层转 400/403)。"""
|
||
|
||
|
||
def has_park_admin(tenant: dict | None) -> bool:
|
||
"""园区是否已绑定管理员(operator_user_id 或旧 admin_username)。"""
|
||
if not tenant:
|
||
return False
|
||
return bool(tenant.get("operator_user_id") or tenant.get("admin_username"))
|
||
|
||
|
||
def initial_stage(from_tenant: dict | None) -> str:
|
||
"""发起时的初始阶段:转出园区无管理员 → 第一级自动通过,直接进第二级。"""
|
||
return "pending_from" if has_park_admin(from_tenant) else "pending_to"
|
||
|
||
|
||
def review(stage: str, *, side: str, decision: str) -> dict:
|
||
"""推进一步状态机。side ∈ from|to|operator;decision ∈ approved|rejected。
|
||
|
||
返回 {"stage": 新阶段, "from_status":…, "to_status":…};不合法流转 raise TransferError。
|
||
"""
|
||
if decision not in ("approved", "rejected"):
|
||
raise TransferError("审核决定仅支持 approved/rejected")
|
||
if stage in ("approved", "rejected", "cancelled"):
|
||
raise TransferError("该申请已终态,无法再审核")
|
||
|
||
if side == "operator":
|
||
# 运营端兜底:任意未终态阶段可直接终审
|
||
return {"stage": decision, "from_status": decision,
|
||
"to_status": decision if stage == "pending_to" or decision == "rejected" else "pending"}
|
||
|
||
if side == "from":
|
||
if stage != "pending_from":
|
||
raise TransferError("当前不在转出园区审核阶段")
|
||
if decision == "rejected":
|
||
return {"stage": "rejected", "from_status": "rejected", "to_status": "pending"}
|
||
return {"stage": "pending_to", "from_status": "approved", "to_status": "pending"}
|
||
|
||
if side == "to":
|
||
if stage == "pending_from":
|
||
raise TransferError("转出园区尚未审核")
|
||
if stage != "pending_to":
|
||
raise TransferError("该申请已终态,无法再审核")
|
||
if decision == "rejected":
|
||
return {"stage": "rejected", "from_status": "approved", "to_status": "rejected"}
|
||
return {"stage": "approved", "from_status": "approved", "to_status": "approved"}
|
||
|
||
raise TransferError("非法审核方")
|
||
|
||
|
||
def can_initiate(actor_role: str, *, from_park_id: str, to_park_id: str,
|
||
my_park_id: str = "", is_company_admin: bool = False,
|
||
target_park_id: str = "") -> bool:
|
||
"""发起资格:operator 任意;carrier 限 from/to 本园区;enterprise 限本企业 admin;user 限本人。"""
|
||
if actor_role == "operator":
|
||
return True
|
||
if actor_role == "carrier":
|
||
return bool(my_park_id) and my_park_id in (from_park_id, to_park_id)
|
||
if actor_role == "enterprise":
|
||
return is_company_admin
|
||
if actor_role == "user":
|
||
return bool(target_park_id) and target_park_id == from_park_id
|
||
return False
|
||
|
||
|
||
def review_side(transfer: dict, my_park_id: str) -> str | None:
|
||
"""园区端审核时判定自己审哪一级:from 园区审第一级,to 园区审第二级。"""
|
||
stage = transfer.get("stage")
|
||
if stage == "pending_from" and transfer.get("from_park_id") == my_park_id:
|
||
return "from"
|
||
if stage == "pending_to" and transfer.get("to_park_id") == my_park_id:
|
||
return "to"
|
||
return None
|