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
|