Files
server-core/app/domain/park_transfer.py
T
Pine dbc29eee59 feat(membership): 成员制归属重构与叠加制身份(多对多成员表+转园两级审核)
- 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>
2026-08-31 20:36:26 +08:00

94 lines
4.0 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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|operatordecision ∈ 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 限本企业 adminuser 限本人。"""
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