chore: initialize insar management system v2

This commit is contained in:
2026-04-14 13:16:01 +08:00
commit ecc72ec9cd
361 changed files with 2142522 additions and 0 deletions
+607
View File
@@ -0,0 +1,607 @@
"""
Pydantic Schema 定义。
所有 API 请求/响应模型均在此文件中定义。
"""
from datetime import datetime
from typing import Any, Dict, List, Optional, Tuple, Union
from pydantic import BaseModel, ConfigDict, Field, computed_field, field_validator
from ..config import read_int_env
MAX_SCAN_DIRECTORY_COUNT = read_int_env(
"MAX_SCAN_DIRECTORY_COUNT",
64,
minimum=1,
maximum=500,
)
MAX_SCAN_PATH_LENGTH = read_int_env(
"MAX_SCAN_PATH_LENGTH",
2048,
minimum=64,
maximum=32767,
)
def _normalize_directory_list(value: Any, field_name: str) -> List[str]:
if value is None:
return []
if not isinstance(value, list):
raise ValueError(f"{field_name} must be a list.")
normalized: List[str] = []
for raw in value:
path = str(raw or "").strip()
if not path:
continue
if len(path) > MAX_SCAN_PATH_LENGTH:
raise ValueError(
f"{field_name} contains a path longer than {MAX_SCAN_PATH_LENGTH} characters."
)
if path not in normalized:
normalized.append(path)
if len(normalized) > MAX_SCAN_DIRECTORY_COUNT:
raise ValueError(
f"{field_name} exceeds max directory count ({MAX_SCAN_DIRECTORY_COUNT})."
)
return normalized
class HazardPoint(BaseModel):
id: int
tybh: str
hazard_type: Optional[str] = None
hazard_name: Optional[str] = None
city: Optional[str] = None
county: Optional[str] = None
township: Optional[str] = None
longitude: float
latitude: float
model_config = ConfigDict(from_attributes=True)
class DinsarResult(BaseModel):
id: int
product_id: Optional[str] = None
compat_result_id: Optional[int] = None
name: str
task_name: Optional[str] = None
task_alias: Optional[str] = None
pair_key: Optional[str] = None
pair_uid: Optional[str] = None
network_run_id: Optional[str] = None
network_edge_id: Optional[int] = None
policy_version: Optional[str] = None
selection_strategy: Optional[str] = None
file_path: str
min_lon: float
min_lat: float
max_lon: float
max_lat: float
coverage_polygon: Optional[Union[List, dict]] = None
is_cached: bool
ai_score: Optional[float] = None
user_label: Optional[int] = None
ai_report: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
class ScanRequest(BaseModel):
"""API 请求体模型,用于触发数据扫描。"""
radar_data_directories: List[str] = Field(default_factory=list)
orbit_data_directory: Optional[str] = None
dinsar_results_directories: List[str] = Field(default_factory=list)
@field_validator("radar_data_directories", mode="before")
@classmethod
def _validate_radar_dirs(cls, value: Any) -> List[str]:
return _normalize_directory_list(value, "radar_data_directories")
@field_validator("dinsar_results_directories", mode="before")
@classmethod
def _validate_dinsar_dirs(cls, value: Any) -> List[str]:
return _normalize_directory_list(value, "dinsar_results_directories")
@field_validator("orbit_data_directory", mode="before")
@classmethod
def _validate_orbit_dir(cls, value: Any) -> Optional[str]:
if value is None:
return None
normalized = str(value).strip()
if not normalized:
return None
if len(normalized) > MAX_SCAN_PATH_LENGTH:
raise ValueError(
f"orbit_data_directory is longer than {MAX_SCAN_PATH_LENGTH} characters."
)
return normalized
class ManagedRootInfo(BaseModel):
id: int
root_code: str
root_role: str
display_name: str
path: str
path_kind: str
source_kind: str
source_ref: Optional[str] = None
scan_mode: str
owner_engine: Optional[str] = None
enabled: bool
exists_flag: bool
metadata_json: Optional[Dict[str, Any]] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
class ScanCursorInfo(BaseModel):
id: int
root_ref_id: int
cursor_key: str
cursor_type: str
scan_scope: str
status: str
last_scan_started_at: Optional[datetime] = None
last_scan_finished_at: Optional[datetime] = None
last_seen_mtime: Optional[float] = None
last_seen_entry_count: Optional[int] = None
last_seen_fingerprint: Optional[str] = None
last_error: Optional[str] = None
updated_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
class RadarData(BaseModel):
"""雷达数据的数据模型 (Pydantic),用于 API 输入和输出。"""
id: int
satellite: str
imaging_date: str
imaging_mode: str
orbit_direction: Optional[str] = None
polarization: str
satellite_mode: Optional[str] = None
receiving_station: Optional[str] = None
orbit_circle: Optional[str] = None
scene_center_lon: Optional[float] = None
scene_center_lat: Optional[float] = None
acquisition_time_utc: Optional[str] = None
product_type: Optional[str] = None
product_level: Optional[str] = None
product_unique_id: Optional[str] = None
file_path: str
has_orbit_data: bool
orbit_file_path: Optional[str] = None
is_envi_processed: bool = False
coverage_polygon: List[Tuple[float, float]]
min_lon: float
min_lat: float
max_lon: float
max_lat: float
preview_cache_status: str = "NONE"
preview_cache_version: Optional[str] = None
preview_cache_updated_at: Optional[datetime] = None
preview_cache_error: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
@computed_field
@property
def coverage_bbox(self) -> Tuple[float, float, float, float]:
"""A computed property to provide the bbox tuple, used by existing logic."""
return (self.min_lon, self.min_lat, self.max_lon, self.max_lat)
class RadarDataPage(BaseModel):
items: List[RadarData]
total: int
limit: int
offset: int
has_more: bool
class DinsarResultPage(BaseModel):
items: List[DinsarResult]
total: int
limit: int
offset: int
has_more: bool
class PairingRequest(BaseModel):
"""D-InSAR 配对请求的参数模型(增强版 v2.0)"""
# === 时空约束(保留) ===
time_baseline_min: int = Field(default=1, ge=0, le=3650)
time_baseline_max: int = Field(default=90, ge=1, le=3650)
overlap_threshold: float = Field(default=0.5, ge=0.0, le=1.0)
spatial_baseline_max_meters: int = Field(default=3000, ge=0, le=100000)
coverage_diversity_penalty: float = Field(default=0.3, ge=0.0, le=1.0)
require_same_imaging_mode: bool = True
require_same_polarization: bool = True
aoi_overlap_threshold: Optional[float] = Field(default=None, ge=0.0, le=1.0)
# === 双池日期(新增) ===
master_date_from: Optional[str] = Field(default=None, pattern=r'^\d{8}$|^$')
master_date_to: Optional[str] = Field(default=None, pattern=r'^\d{8}$|^$')
slave_date_from: Optional[str] = Field(default=None, pattern=r'^\d{8}$|^$')
slave_date_to: Optional[str] = Field(default=None, pattern=r'^\d{8}$|^$')
# === 配对策略(新增) ===
strategy: str = Field(default="all", pattern=r'^(all|sbas|sequential|star)$')
num_connections: int = Field(default=1, ge=1, le=10)
reference_image_id: Optional[int] = None
# === 多卫星支持(新增) ===
allowed_satellites: Optional[List[str]] = None
cross_satellite_pairing: bool = False
# === 向后兼容(保留) ===
start_date: Optional[str] = Field(default=None, pattern=r'^\d{8}$|^$')
@field_validator(
'master_date_from',
'master_date_to',
'slave_date_from',
'slave_date_to',
'start_date',
mode='before',
)
@classmethod
def _normalize_optional_date_text(cls, value):
if value is None:
return None
text = str(value).strip()
return text or None
@field_validator('aoi_overlap_threshold', mode='before')
@classmethod
def _normalize_aoi_overlap_threshold(cls, value):
if value is None:
return None
if isinstance(value, str):
text = value.strip()
if not text:
return None
value = text
try:
numeric = float(value)
except (TypeError, ValueError):
return value
return None if numeric <= 0 else numeric
@field_validator('allowed_satellites', mode='before')
@classmethod
def _normalize_allowed_satellites(cls, value):
if value in (None, '', []):
return None
if not isinstance(value, list):
return value
normalized = [str(item).strip() for item in value if str(item).strip()]
return normalized or None
@field_validator('master_date_to')
@classmethod
def validate_master_date_range(cls, v, info):
if v and info.data.get('master_date_from'):
if v < info.data['master_date_from']:
raise ValueError('master_date_to must >= master_date_from')
return v
@field_validator('slave_date_to')
@classmethod
def validate_slave_date_range(cls, v, info):
if v and info.data.get('slave_date_from'):
if v < info.data['slave_date_from']:
raise ValueError('slave_date_to must >= slave_date_from')
return v
class RadarPair(BaseModel):
"""单个雷达干涉对的数据模型。"""
master: RadarData
slave: RadarData
task_name: str
task_alias: Optional[str] = None
pair_key: Optional[str] = None
pair_uid: Optional[str] = None
metric_cache_ref_id: Optional[int] = None
network_run_id: Optional[str] = None
network_edge_id: Optional[int] = None
policy_version: Optional[str] = None
selection_strategy: Optional[str] = None
selection_score: Optional[float] = None
selection_reason: Optional[str] = None
time_baseline_days: int
spatial_baseline_meters: float
class PairingResponse(BaseModel):
"""配对结果的响应模型,包含配对列表和可选的 AOI GeoJSON。"""
pairs: List[RadarPair]
aoi_geojson: Optional[dict] = None
warnings: List[str] = Field(default_factory=list)
fallback_used: bool = False
degraded: bool = False
policy_version: Optional[str] = None
network_run_id: Optional[str] = None
candidate_count: int = 0
selected_edge_count: int = 0
class PsRequest(BaseModel):
"""PS-InSAR 时序分析数据准备的请求模型。"""
initial_overlap_threshold: float = 0.3
final_overlap_threshold: float = 0.95
class TaskInfo(BaseModel):
"""任务信息的 Pydantic 模型,用于 API 返回。"""
task_id: str
task_type: str
task_name: str
status: str
progress: int
message: Optional[str] = None
created_at: datetime
updated_at: datetime
started_at: Optional[datetime] = None
ended_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
class AuthUserInfo(BaseModel):
id: int
username: str
role: str
is_active: bool
created_at: datetime
last_login_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
class AuthAuditLogInfo(BaseModel):
id: int
user_id: Optional[int] = None
username: Optional[str] = None
action: str
resource: Optional[str] = None
detail: Optional[Dict[str, Any]] = None
ip_address: Optional[str] = None
created_at: datetime
model_config = ConfigDict(from_attributes=True)
class RadarPreviewStatusInfo(BaseModel):
radar_id: int
status: str
cache_version: Optional[str] = None
cache_updated_at: Optional[datetime] = None
has_geo_cache: bool = False
has_raw_cache: bool = False
source_found: bool = False
fallback_in_use: bool = False
message: Optional[str] = None
error: Optional[str] = None
class DinsarTaskBatch(BaseModel):
batch_id: str
name: Optional[str] = None
status: str
total_items: int
completed_items: int
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
class DinsarTaskItem(BaseModel):
id: int
batch_id: str
task_name: Optional[str] = None
task_alias: Optional[str] = None
pair_key: Optional[str] = None
scene_pair_uid: Optional[str] = None
network_run_id: Optional[str] = None
network_edge_id: Optional[int] = None
policy_version: Optional[str] = None
selection_strategy: Optional[str] = None
master_path: str
slave_path: str
master_satellite: Optional[str] = None
master_imaging_date: Optional[str] = None
master_imaging_mode: Optional[str] = None
master_polarization: Optional[str] = None
slave_satellite: Optional[str] = None
slave_imaging_date: Optional[str] = None
slave_imaging_mode: Optional[str] = None
slave_polarization: Optional[str] = None
time_baseline_days: Optional[int] = None
spatial_baseline_meters: Optional[float] = None
status: str
remark: Optional[str] = None
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
class PsTaskBatch(BaseModel):
batch_id: str
name: Optional[str] = None
direction: Optional[str] = None
status: str
total_items: int
completed_items: int
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
class PsTaskItem(BaseModel):
id: int
batch_id: str
file_path: str
satellite: Optional[str] = None
imaging_date: Optional[str] = None
polarization: Optional[str] = None
has_orbit_data: bool
status: str
remark: Optional[str] = None
created_at: datetime
updated_at: datetime
model_config = ConfigDict(from_attributes=True)
class PsTimeseriesRun(BaseModel):
run_id: str
batch_id: str
run_name: str
catalog_name: str
mode: str
engine_code: str
processor_code: str
env_name: Optional[str] = None
wsl_distro: Optional[str] = None
status: str
task_id: Optional[str] = None
workflow_run_id: Optional[str] = None
direction: Optional[str] = None
stack_size: int
reference_date: Optional[str] = None
water_mask_mode: Optional[str] = None
dem_path_windows: Optional[str] = None
dem_path_wsl: Optional[str] = None
orbit_pool_windows: Optional[str] = None
orbit_pool_wsl: Optional[str] = None
work_root_windows: Optional[str] = None
work_root_wsl: Optional[str] = None
publish_dir_windows: Optional[str] = None
publish_dir_wsl: Optional[str] = None
manifest_path_windows: Optional[str] = None
manifest_path_wsl: Optional[str] = None
params_json: Optional[Dict[str, Any]] = None
summary_json: Optional[Dict[str, Any]] = None
input_snapshot_json: Optional[Dict[str, Any]] = None
orbit_summary_json: Optional[Dict[str, Any]] = None
quality_summary_json: Optional[Dict[str, Any]] = None
error_message: Optional[str] = None
created_by: Optional[str] = None
created_at: datetime
updated_at: datetime
started_at: Optional[datetime] = None
ended_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
# ============ AI 诊断 Schema ============
# ============ 水体检测 Schema ============
class WaterDetectRequest(BaseModel):
"""水体检测请求"""
scene_id: Optional[int] = Field(None, description="SARSceneGeoORM 主键(可选)")
input_path: Optional[str] = Field(None, description="直接指定 GeoTIFF 路径(可选)")
class WaterDetectResponse(BaseModel):
"""水体检测响应"""
id: int
scene_id: Optional[int] = None
input_path: Optional[str] = None
output_path: Optional[str] = None
water_area_km2: Optional[float] = None
water_pixel_count: Optional[int] = None
otsu_threshold_db: Optional[float] = None
status: str
error_msg: Optional[str] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
# ============ GF3 处理 Schema ============
class GF3ProcessRequest(BaseModel):
"""GF3 L1A→L2 处理请求"""
input_dir: str = Field(..., description="GF3 L1A 数据目录路径")
resolution: float = Field(default=0.0002, ge=0.00001, le=0.01, description="输出分辨率(度)")
class GF3ProcessResponse(BaseModel):
"""GF3 处理响应"""
id: int
input_dir: str
output_dir: Optional[str] = None
polarizations: Optional[str] = None
l2_paths: Optional[str] = None
resolution: float = 0.0002
status: str
error_msg: Optional[str] = None
created_at: Optional[datetime] = None
updated_at: Optional[datetime] = None
model_config = ConfigDict(from_attributes=True)
# ============ AI 诊断 Schema ============
class AiDiagnosisCreate(BaseModel):
"""创建 AI 诊断请求"""
result_id: int = Field(..., description="D-InSAR 结果 ID")
model_name: str = Field(..., description="模型名称,如 llama3.2-vision")
prompt_template: str = Field(..., description="Prompt 模板名称:quick/standard/detailed")
custom_prompt: Optional[str] = Field(None, description="自定义 Prompt(覆盖模板)")
class AiDiagnosisResponse(BaseModel):
"""AI 诊断响应"""
id: int
result_id: int
product_ref_id: Optional[int] = None
product_id: Optional[str] = None
task_id: Optional[str] = None
model_name: str
prompt_template: str
prompt_text: Optional[str] = None
diagnosis_markdown: Optional[str] = None
risk_level: Optional[str] = None
confidence_score: Optional[float] = None
result_name: Optional[str] = None
date_range: Optional[str] = None
quality_score: Optional[float] = None
hazards_found: int = 0
hazards_snapshot: Optional[List[Dict[str, Any]]] = None
created_at: datetime
duration_seconds: Optional[float] = None
error_message: Optional[str] = None
model_config = ConfigDict(from_attributes=True)
class AiDiagnosisListResponse(BaseModel):
"""AI 诊断列表响应"""
items: List[AiDiagnosisResponse]
total: int
page: int
page_size: int