210 lines
5.6 KiB
Python
210 lines
5.6 KiB
Python
"""移动市场专员/地区经理的现场字段范围。"""
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
from typing import Literal
|
|
|
|
from fastapi import HTTPException
|
|
from sqlalchemy import or_
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.core.data_scope import apply_data_scope
|
|
from app.models.models import (
|
|
BizCollectionProtocol,
|
|
BizCollectionRegionStaff,
|
|
BizPresignOrder,
|
|
BizProtocolCollectionInfo,
|
|
)
|
|
from app.models.research_donation import (
|
|
BizResearchDonation,
|
|
BizResearchDonationCollection,
|
|
)
|
|
|
|
ScopeKind = Literal["specialist", "manager"]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class MarketFieldContext:
|
|
"""由固定移动路由创建的现场范围上下文。"""
|
|
|
|
tenant_no: str
|
|
user_id: str
|
|
scope_kind: ScopeKind
|
|
|
|
def __post_init__(self) -> None:
|
|
tenant_no = (self.tenant_no or "").strip()
|
|
user_id = (self.user_id or "").strip()
|
|
if not tenant_no or not user_id:
|
|
raise ValueError("现场范围要求明确的租户和用户")
|
|
if self.scope_kind not in ("specialist", "manager"):
|
|
raise ValueError("不支持的现场范围类型")
|
|
object.__setattr__(self, "tenant_no", tenant_no)
|
|
object.__setattr__(self, "user_id", user_id)
|
|
|
|
|
|
def leader_region_ids_query(db: Session, ctx: MarketFieldContext):
|
|
"""返回当前经理在当前租户有效负责的地区 ID 查询。"""
|
|
return db.query(BizCollectionRegionStaff.region_id).filter(
|
|
BizCollectionRegionStaff.tenant_no == ctx.tenant_no,
|
|
BizCollectionRegionStaff.del_flag == 0,
|
|
BizCollectionRegionStaff.user_id == ctx.user_id,
|
|
BizCollectionRegionStaff.is_leader == 1,
|
|
)
|
|
|
|
|
|
def _collection_scope_condition(model, db: Session, ctx: MarketFieldContext):
|
|
if ctx.scope_kind == "specialist":
|
|
return model.sales_user_id == ctx.user_id
|
|
return or_(
|
|
model.region_id.in_(leader_region_ids_query(db, ctx)),
|
|
model.region_manager_user_id == ctx.user_id,
|
|
)
|
|
|
|
|
|
def apply_market_data_scope(
|
|
query,
|
|
model,
|
|
db: Session,
|
|
ctx: MarketFieldContext,
|
|
user_info: dict,
|
|
):
|
|
"""组合租户、软删与通用多角色数据权限。"""
|
|
user_id = (
|
|
str(user_info.get("user_id") or "").strip()
|
|
if isinstance(user_info, dict)
|
|
else ""
|
|
)
|
|
if not user_id or user_id != ctx.user_id:
|
|
raise PermissionError("用户身份与现场范围上下文不一致")
|
|
query = query.filter(
|
|
model.tenant_no == ctx.tenant_no,
|
|
model.del_flag == 0,
|
|
)
|
|
return apply_data_scope(
|
|
query,
|
|
model,
|
|
user_info,
|
|
db,
|
|
tenant_no=ctx.tenant_no,
|
|
)
|
|
|
|
|
|
def apply_presign_scope(
|
|
query,
|
|
db: Session,
|
|
ctx: MarketFieldContext,
|
|
*,
|
|
user_info: dict,
|
|
):
|
|
"""组合租户、软删、通用数据权限与预签现场范围。"""
|
|
query = apply_market_data_scope(
|
|
query, BizPresignOrder, db, ctx, user_info
|
|
)
|
|
return query.filter(_collection_scope_condition(BizPresignOrder, db, ctx))
|
|
|
|
|
|
def apply_cell_protocol_scope(
|
|
query,
|
|
db: Session,
|
|
ctx: MarketFieldContext,
|
|
*,
|
|
user_info: dict,
|
|
):
|
|
"""按细胞协议的有效采集信息施加现场范围。"""
|
|
query = apply_market_data_scope(
|
|
query, BizCollectionProtocol, db, ctx, user_info
|
|
)
|
|
collection_exists = (
|
|
db.query(BizProtocolCollectionInfo.id)
|
|
.filter(
|
|
BizProtocolCollectionInfo.collection_protocol_id
|
|
== BizCollectionProtocol.id,
|
|
BizProtocolCollectionInfo.tenant_no == ctx.tenant_no,
|
|
BizProtocolCollectionInfo.del_flag == 0,
|
|
_collection_scope_condition(BizProtocolCollectionInfo, db, ctx),
|
|
)
|
|
.exists()
|
|
.correlate(BizCollectionProtocol)
|
|
)
|
|
return query.filter(collection_exists)
|
|
|
|
|
|
def apply_donation_scope(
|
|
query,
|
|
db: Session,
|
|
ctx: MarketFieldContext,
|
|
*,
|
|
user_info: dict,
|
|
):
|
|
"""按科研捐赠的有效采集信息施加现场范围。"""
|
|
query = apply_market_data_scope(
|
|
query, BizResearchDonation, db, ctx, user_info
|
|
)
|
|
collection_exists = (
|
|
db.query(BizResearchDonationCollection.id)
|
|
.filter(
|
|
BizResearchDonationCollection.research_donation_id
|
|
== BizResearchDonation.id,
|
|
BizResearchDonationCollection.tenant_no == ctx.tenant_no,
|
|
BizResearchDonationCollection.del_flag == 0,
|
|
_collection_scope_condition(
|
|
BizResearchDonationCollection, db, ctx
|
|
),
|
|
)
|
|
.exists()
|
|
.correlate(BizResearchDonation)
|
|
)
|
|
return query.filter(collection_exists)
|
|
|
|
|
|
def _assert_access(query, record_id: str):
|
|
row = query.filter_by(id=record_id).first()
|
|
if row is None:
|
|
raise HTTPException(status_code=404, detail="数据不存在或无权访问")
|
|
return row
|
|
|
|
|
|
def assert_presign_access(
|
|
db: Session,
|
|
record_id: str,
|
|
ctx: MarketFieldContext,
|
|
*,
|
|
user_info: dict,
|
|
):
|
|
return _assert_access(
|
|
apply_presign_scope(
|
|
db.query(BizPresignOrder), db, ctx, user_info=user_info
|
|
),
|
|
record_id,
|
|
)
|
|
|
|
|
|
def assert_cell_protocol_access(
|
|
db: Session,
|
|
record_id: str,
|
|
ctx: MarketFieldContext,
|
|
*,
|
|
user_info: dict,
|
|
):
|
|
return _assert_access(
|
|
apply_cell_protocol_scope(
|
|
db.query(BizCollectionProtocol), db, ctx, user_info=user_info
|
|
),
|
|
record_id,
|
|
)
|
|
|
|
|
|
def assert_donation_access(
|
|
db: Session,
|
|
record_id: str,
|
|
ctx: MarketFieldContext,
|
|
*,
|
|
user_info: dict,
|
|
):
|
|
return _assert_access(
|
|
apply_donation_scope(
|
|
db.query(BizResearchDonation), db, ctx, user_info=user_info
|
|
),
|
|
record_id,
|
|
)
|