"""T+ 管理接口合同;只暴露范围、数量和受控核验结果。""" from datetime import datetime from typing import Literal from uuid import UUID from pydantic import BaseModel, Field, model_validator from app.core.response import PageQuery EntityType = Literal[ "personnel", "products", "customers", "businesses", "shipments", "receipts", "business_items", "shipment_items", "receipt_items", ] TaskType = Literal["READ", "AUDIT"] TaskStatus = Literal["QUEUED", "RUNNING", "SUCCEEDED", "PARTIAL", "FAILED"] class TPlusCreateIn(BaseModel): """创建长任务;幂等键是调用意图的一部分,必须由调用方保留。""" task_type: TaskType = "READ" entity_types: list[EntityType] = Field(default_factory=list, max_length=9) request_key: UUID @model_validator(mode="after") def check_scope(self): if self.task_type == "READ" and not self.entity_types: raise ValueError("请选择至少一种读取数据") if self.task_type == "AUDIT" and self.entity_types: raise ValueError("关联核验不接受自定义读取范围") if len(set(self.entity_types)) != len(self.entity_types): raise ValueError("读取类型不能重复") return self class TPlusRetryIn(BaseModel): """失败批次重试不接受任意新范围。""" request_key: UUID class TPlusBatchQuery(PageQuery): """批次筛选,与读取状态和任务类型保持一致。""" status: TaskStatus | None = None task_type: TaskType | None = None class TPlusErrorQuery(PageQuery): """异常只能在指定批次内查询。""" entity_type: EntityType | None = None class TPlusEntityProgress(BaseModel): entity_type: str total_records: int | None = None processed_records: int | None = None status: str = "WAITING" class TPlusProgressOut(BaseModel): phase: str total_records: int | None = None processed_records: int = 0 percentage: float | None = None active_entity: str | None = None updated_at: datetime | None = None elapsed_seconds: int = 0 entities: list[TPlusEntityProgress] = Field(default_factory=list) class TPlusBatchOut(BaseModel): """批次列表不包含原始数据、源配置或数据库异常。""" batch_no: str task_type: TaskType status: str entity_types: list[str] | None source_record_count: int staged_record_count: int skipped_record_count: int failed_record_count: int parent_batch_no: str | None create_time: datetime started_at: datetime | None finished_at: datetime | None can_retry: bool error_summary: str | None = None trigger_type: str = "MANUAL" automatic_attempt: int = 0 progress: TPlusProgressOut | None = None class TPlusBatchDetailOut(TPlusBatchOut): """详情按需携带受控的关联核验结果。""" audit_result: dict | None = None class TPlusErrorOut(BaseModel): """异常使用固定说明,源定位仅开放整数 ID。""" id: int entity_type: str source_id: str | None error_code: str error_message: str retry_count: int create_time: datetime class TPlusAutomationAlert(BaseModel): code: str message: str batch_no: str | None = None class TPlusAutomationUpdate(BaseModel): enabled: bool = Field(strict=True) expected_version: int = Field(ge=0, strict=True) class TPlusRuntimeUpdate(BaseModel): desired_state: Literal["RUNNING", "STOPPED"] expected_version: int = Field(ge=0, strict=True) class TPlusRuntimeOut(BaseModel): desired_state: str = "STOPPED" actual_state: str = "OFFLINE" worker_phase: str = "UNKNOWN" heartbeat_at: datetime | None = None last_error: str | None = None host: str | None = None platform: str | None = None version: int = 0 updated_by: int | None = None editable: bool = False class TPlusAutomationOverview(BaseModel): enabled: bool = False version: int = 0 editable: bool = False configuration_origin: str = "ENVIRONMENT" weekday: int = 0 time: str = "02:00" timezone: str = "Asia/Shanghai" worker_phase: str = "UNKNOWN" heartbeat_at: datetime | None = None last_success_at: datetime | None = None next_retry_at: datetime | None = None stop_requested: bool = False alerts: list[TPlusAutomationAlert] = Field(default_factory=list) class TPlusOverviewOut(BaseModel): """配置就绪不等同于源库连通或兼容验收通过。""" is_configured: bool configuration_message: str status_counts: dict[str, int] total_staged_records: int = 0 current_batch: TPlusBatchOut | None = None compatibility_status: str = "PENDING" business_import_enabled: bool = False latest_audit: dict | None = None automation: TPlusAutomationOverview = Field(default_factory=TPlusAutomationOverview)