From b931e8db53b897fee8fd7032cb01dabf326d68d6 Mon Sep 17 00:00:00 2001 From: Harmon Date: Tue, 2 Jun 2026 15:11:07 +0800 Subject: [PATCH] Harden DInSAR and SBAS production edges --- backend/app/routers/dependencies.py | 2 + backend/app/routers/dinsar.py | 15 +--- backend/app/routers/dinsar_production.py | 6 -- backend/app/routers/dinsar_products.py | 4 +- .../services/sbas_insar_production_service.py | 77 +++++++++++++++++++ frontend/src/api/dinsar.js | 5 +- 6 files changed, 88 insertions(+), 21 deletions(-) diff --git a/backend/app/routers/dependencies.py b/backend/app/routers/dependencies.py index 1c85865..e2b2eba 100644 --- a/backend/app/routers/dependencies.py +++ b/backend/app/routers/dependencies.py @@ -78,6 +78,8 @@ HIGH_RISK_WRITE_PATH_PREFIXES = ( "/api/scan-data", "/api/scan-dinsar-results", "/api/dinsar-results/", + "/api/dinsar-production", + "/api/sbas-insar-production", "/api/ai/", "/api/idl/launch-workbench", "/api/idl/worker/", diff --git a/backend/app/routers/dinsar.py b/backend/app/routers/dinsar.py index 58635ac..9cc52e2 100644 --- a/backend/app/routers/dinsar.py +++ b/backend/app/routers/dinsar.py @@ -106,19 +106,6 @@ def _is_postgresql_session(db: AsyncSession) -> bool: return (dialect_name or "").lower() == "postgresql" except Exception: return False - """ - try: - scan_dirs = list(request.results_directories) if request else [] - if not scan_dirs: - scan_dirs = _get_default_dinsar_scan_dirs() - if not scan_dirs: - raise HTTPException(status_code=400, detail="未提供结果目录,且 MONITOR_DINSAR_DIRS 未配置。") - bind = db.get_bind() - dialect_name = getattr(getattr(bind, "dialect", None), "name", "") - return (dialect_name or "").lower() == "postgresql" - except Exception: - return False - """ async def _apply_list_query_statement_timeout(db: AsyncSession) -> None: @@ -249,6 +236,8 @@ async def scan_dinsar_results_endpoint( ) await db.commit() return {"message": "D-InSAR 统一扫描任务已进入队列", "task_id": task_id} + except HTTPException: + raise except ValueError as e: raise HTTPException(status_code=409, detail=str(e)) except Exception as e: diff --git a/backend/app/routers/dinsar_production.py b/backend/app/routers/dinsar_production.py index f3589be..8767d2c 100644 --- a/backend/app/routers/dinsar_production.py +++ b/backend/app/routers/dinsar_production.py @@ -232,12 +232,6 @@ async def submit_run( status_code=400, detail=f"PyINT 输入资产预检未通过: {detail or '请先修复阻塞项。'}", ) - if not pyint_preview.get("allow_submit"): - detail = summarize_preview_blockers(pyint_preview) - raise HTTPException( - status_code=400, - detail=f"PyINT 输入资产预检未通过: {detail or '请先修复阻塞项。'}", - ) payload = { "engine_code": req.engine_code, diff --git a/backend/app/routers/dinsar_products.py b/backend/app/routers/dinsar_products.py index 47fe868..2e4dd3d 100644 --- a/backend/app/routers/dinsar_products.py +++ b/backend/app/routers/dinsar_products.py @@ -1,5 +1,6 @@ from __future__ import annotations +import mimetypes import os from typing import List, Optional @@ -258,4 +259,5 @@ async def get_dinsar_product_preview( preview_path = str(detail.get("preview_path") or "").strip() if not preview_path or not os.path.isfile(preview_path): raise HTTPException(status_code=404, detail="Preview not found") - return FileResponse(preview_path, media_type="image/webp") + media_type = mimetypes.guess_type(preview_path)[0] or "application/octet-stream" + return FileResponse(preview_path, media_type=media_type) diff --git a/backend/app/services/sbas_insar_production_service.py b/backend/app/services/sbas_insar_production_service.py index 6b1913d..3c442c6 100644 --- a/backend/app/services/sbas_insar_production_service.py +++ b/backend/app/services/sbas_insar_production_service.py @@ -794,6 +794,12 @@ class SbasInsarProductionService: ) -> dict[str, Any]: source_paths = self._resolve_source_roots(source_roots) orbit_paths = self._resolve_orbit_roots(orbit_roots) + root_warnings = self._build_root_resolution_warnings( + source_roots=source_roots, + orbit_roots=orbit_roots, + source_paths=source_paths, + orbit_paths=orbit_paths, + ) normalized_mode = self._normalize_discovery_mode(discovery_mode) min_aoi_coverage_ratio = max(0.0, min(1.0, float(min_aoi_coverage_ratio or 0.0))) min_common_overlap_ratio = max(0.0, min(1.0, float(min_common_overlap_ratio or 0.0))) @@ -818,6 +824,8 @@ class SbasInsarProductionService: if not force_refresh: cached = self._read_discovery_cache(cache_key) if cached is not None: + cached = dict(cached) + cached["warnings"] = root_warnings return cached scenes: list[dict[str, Any]] = [] @@ -919,6 +927,7 @@ class SbasInsarProductionService: "scene_count": len(scenes), "candidate_count": len(candidates), "errors": errors[:50], + "warnings": root_warnings, "items": candidates, } snapshot_path = self._write_runtime_json( @@ -3698,6 +3707,58 @@ class SbasInsarProductionService: raw_values = [r"D:\orbit_pools\envi"] return self._dedupe_existing_dirs(raw_values) + def _build_root_resolution_warnings( + self, + *, + source_roots: list[str] | None, + orbit_roots: list[str] | None, + source_paths: list[Path], + orbit_paths: list[Path], + ) -> list[dict[str, Any]]: + warnings: list[dict[str, Any]] = [] + source_requested = source_roots or self._split_config_paths(settings.GAMMA_SBAS_SOURCE_ROOTS) or [r"D:\LuTan1_Image_Pool"] + orbit_requested = orbit_roots or self._split_config_paths(settings.GAMMA_SBAS_ORBIT_ROOTS) or [r"D:\orbit_pools\envi"] + + source_missing = self._missing_root_values(source_requested) + orbit_missing = self._missing_root_values(orbit_requested) + if source_missing: + warnings.append( + { + "code": "SOURCE_ROOTS_NOT_FOUND", + "message": "Some configured SBAS source roots do not exist and were ignored.", + "requested_roots": source_requested, + "missing_roots": source_missing, + "resolved_roots": [str(path) for path in source_paths], + } + ) + if orbit_missing: + warnings.append( + { + "code": "ORBIT_ROOTS_NOT_FOUND", + "message": "Some configured SBAS orbit roots do not exist and were ignored.", + "requested_roots": orbit_requested, + "missing_roots": orbit_missing, + "resolved_roots": [str(path) for path in orbit_paths], + } + ) + if source_requested and not source_paths: + warnings.append( + { + "code": "NO_VALID_SOURCE_ROOTS", + "message": "No valid SBAS source roots were resolved; discovery will return no scenes.", + "requested_roots": source_requested, + } + ) + if orbit_requested and not orbit_paths: + warnings.append( + { + "code": "NO_VALID_ORBIT_ROOTS", + "message": "No valid SBAS orbit roots were resolved; orbit matching will be unavailable.", + "requested_roots": orbit_requested, + } + ) + return warnings + @staticmethod def _discovery_cache_key( *, @@ -3785,6 +3846,22 @@ class SbasInsarProductionService: roots.append(path) return roots + @staticmethod + def _missing_root_values(values: list[str]) -> list[str]: + missing: list[str] = [] + seen: set[str] = set() + for value in values: + text = str(value or "").strip().strip('"').strip("'") + if not text: + continue + key = os.path.normcase(text) + if key in seen: + continue + seen.add(key) + if not SbasInsarProductionService._existing_path_variants(text): + missing.append(text) + return missing + @staticmethod def _existing_path_variants(value: str) -> list[Path]: text = str(value or "").strip().strip('"').strip("'") diff --git a/frontend/src/api/dinsar.js b/frontend/src/api/dinsar.js index 3ebf92b..69647aa 100644 --- a/frontend/src/api/dinsar.js +++ b/frontend/src/api/dinsar.js @@ -2,7 +2,10 @@ import apiClient from './client'; export const getDinsarResults = (params) => apiClient.get('/dinsar-results', { params }).then(r => r.data); export const labelDinsarResult = (resultId, label) => - apiClient.post(`/dinsar-results/${resultId}/label`, { label }).then(r => r.data); + apiClient.post( + `/dinsar-results/${resultId}/label`, + new URLSearchParams(label === null || label === undefined ? {} : { label: String(label) }) + ).then(r => r.data); export const scanDinsarResults = (params) => apiClient.post('/scan-dinsar-results', params).then(r => r.data); export const exportDinsarResults = (resultIds, targetDir) => apiClient.post('/dinsar-results/export', { result_ids: resultIds, target_dir: targetDir }).then(r => r.data);