Enable LT1 and GF3 result delivery
This commit is contained in:
@@ -43,6 +43,7 @@ MIGRATION_FILES = [
|
|||||||
"011_source_metadata_documents.sql",
|
"011_source_metadata_documents.sql",
|
||||||
"012_source_archive_integrity.sql",
|
"012_source_archive_integrity.sql",
|
||||||
"013_result_delivery_requests.sql",
|
"013_result_delivery_requests.sql",
|
||||||
|
"014_result_delivery_ortho_sources.sql",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -377,6 +377,8 @@ class ResultDeliveryItemORM(Base):
|
|||||||
source_product_id = Column(Integer, ForeignKey("result_products.id", ondelete="SET NULL"), nullable=True, index=True)
|
source_product_id = Column(Integer, ForeignKey("result_products.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||||
source_result_id = Column(Integer, ForeignKey("dinsar_results.id", ondelete="SET NULL"), nullable=True, index=True)
|
source_result_id = Column(Integer, ForeignKey("dinsar_results.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||||
source_asset_id = Column(Integer, ForeignKey("result_assets.id", ondelete="SET NULL"), nullable=True, index=True)
|
source_asset_id = Column(Integer, ForeignKey("result_assets.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||||
|
source_radar_data_id = Column(Integer, ForeignKey("radar_data.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||||
|
source_scene_geo_id = Column(Integer, ForeignKey("sar_scene_geo.id", ondelete="SET NULL"), nullable=True, index=True)
|
||||||
display_name = Column(String(255), nullable=False)
|
display_name = Column(String(255), nullable=False)
|
||||||
source_path = Column(String, nullable=False)
|
source_path = Column(String, nullable=False)
|
||||||
relative_path = Column(String, nullable=True)
|
relative_path = Column(String, nullable=True)
|
||||||
@@ -391,10 +393,14 @@ class ResultDeliveryItemORM(Base):
|
|||||||
product = relationship("ResultProductORM", foreign_keys=[source_product_id])
|
product = relationship("ResultProductORM", foreign_keys=[source_product_id])
|
||||||
compat_result = relationship("DinsarResultORM", foreign_keys=[source_result_id])
|
compat_result = relationship("DinsarResultORM", foreign_keys=[source_result_id])
|
||||||
asset = relationship("ResultAssetORM", foreign_keys=[source_asset_id])
|
asset = relationship("ResultAssetORM", foreign_keys=[source_asset_id])
|
||||||
|
radar_data = relationship("RadarDataORM", foreign_keys=[source_radar_data_id])
|
||||||
|
scene_geo = relationship("SARSceneGeoORM", foreign_keys=[source_scene_geo_id])
|
||||||
|
|
||||||
__table_args__ = (
|
__table_args__ = (
|
||||||
Index("idx_result_delivery_items_delivery_status", "delivery_id", "status"),
|
Index("idx_result_delivery_items_delivery_status", "delivery_id", "status"),
|
||||||
Index("idx_result_delivery_items_product", "source_product_id"),
|
Index("idx_result_delivery_items_product", "source_product_id"),
|
||||||
|
Index("idx_result_delivery_items_radar", "source_radar_data_id"),
|
||||||
|
Index("idx_result_delivery_items_scene_geo", "source_scene_geo_id"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ class ResultDeliveryCreateRequest(BaseModel):
|
|||||||
channel: str
|
channel: str
|
||||||
product_ids: Optional[List[int]] = None
|
product_ids: Optional[List[int]] = None
|
||||||
compat_result_ids: Optional[List[int]] = None
|
compat_result_ids: Optional[List[int]] = None
|
||||||
|
item_ids: Optional[List[int]] = None
|
||||||
package_mode: str = "directory"
|
package_mode: str = "directory"
|
||||||
include_checksums: Optional[bool] = None
|
include_checksums: Optional[bool] = None
|
||||||
|
|
||||||
@@ -64,6 +65,7 @@ async def create_result_delivery(
|
|||||||
channel=request.channel,
|
channel=request.channel,
|
||||||
product_ids=request.product_ids,
|
product_ids=request.product_ids,
|
||||||
compat_result_ids=request.compat_result_ids,
|
compat_result_ids=request.compat_result_ids,
|
||||||
|
item_ids=request.item_ids,
|
||||||
package_mode=request.package_mode,
|
package_mode=request.package_mode,
|
||||||
include_checksums=request.include_checksums,
|
include_checksums=request.include_checksums,
|
||||||
)
|
)
|
||||||
@@ -89,6 +91,28 @@ async def create_result_delivery(
|
|||||||
return result_delivery_service.serialize_delivery(delivery)
|
return result_delivery_service.serialize_delivery(delivery)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/result-deliveries/catalog/{channel}")
|
||||||
|
async def list_result_delivery_catalog(
|
||||||
|
channel: str,
|
||||||
|
limit: int = 100,
|
||||||
|
offset: int = 0,
|
||||||
|
query: Optional[str] = None,
|
||||||
|
db: AsyncSession = Depends(get_db),
|
||||||
|
current_user: AuthUserORM = Depends(_get_current_user),
|
||||||
|
):
|
||||||
|
_ = current_user
|
||||||
|
try:
|
||||||
|
return await result_delivery_service.list_channel_catalog(
|
||||||
|
db,
|
||||||
|
channel=channel,
|
||||||
|
limit=limit,
|
||||||
|
offset=offset,
|
||||||
|
query=query,
|
||||||
|
)
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(status_code=400, detail=str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
@router.get("/result-deliveries")
|
@router.get("/result-deliveries")
|
||||||
async def list_result_deliveries(
|
async def list_result_deliveries(
|
||||||
mine: bool = True,
|
mine: bool = True,
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ from ..config import settings
|
|||||||
from ..models import (
|
from ..models import (
|
||||||
AuthUserORM,
|
AuthUserORM,
|
||||||
DinsarResultORM,
|
DinsarResultORM,
|
||||||
|
RadarDataORM,
|
||||||
ResultAssetORM,
|
ResultAssetORM,
|
||||||
ResultDeliveryItemORM,
|
ResultDeliveryItemORM,
|
||||||
ResultDeliveryRequestORM,
|
ResultDeliveryRequestORM,
|
||||||
ResultProductORM,
|
ResultProductORM,
|
||||||
|
SARSceneGeoORM,
|
||||||
)
|
)
|
||||||
from .dinsar_read_service import dinsar_read_service
|
from .dinsar_read_service import dinsar_read_service
|
||||||
from .job_queue_service import job_queue_service
|
from .job_queue_service import job_queue_service
|
||||||
@@ -46,7 +48,17 @@ ITEM_STATUS_FAILED = "FAILED"
|
|||||||
ITEM_STATUS_SKIPPED = "SKIPPED"
|
ITEM_STATUS_SKIPPED = "SKIPPED"
|
||||||
|
|
||||||
CHANNEL_DINSAR = "dinsar"
|
CHANNEL_DINSAR = "dinsar"
|
||||||
SUPPORTED_READY_CHANNELS = {CHANNEL_DINSAR}
|
CHANNEL_LT1_ORTHO = "lt1_ortho"
|
||||||
|
CHANNEL_GF3_ORTHO = "gf3_ortho"
|
||||||
|
CHANNEL_SBAS = "sbas"
|
||||||
|
CHANNEL_S1_ORTHO = "s1_ortho"
|
||||||
|
SUPPORTED_READY_CHANNELS = {CHANNEL_DINSAR, CHANNEL_LT1_ORTHO, CHANNEL_GF3_ORTHO}
|
||||||
|
|
||||||
|
LT1_ANALYSIS_ENGINE = "lt_gamma"
|
||||||
|
LT1_ANALYSIS_PROFILE = "lt1_gamma_geocoded_mli"
|
||||||
|
GF3_STANDARD_SOURCE_FORMAT = "GF3_SARSCAPE_L2"
|
||||||
|
GF3_NATIVE_PREVIEW_SOURCE_FORMAT = "GF3_SARSCAPE_NATIVE_PREVIEW"
|
||||||
|
GF3_DELIVERABLE_SOURCE_FORMATS = {GF3_STANDARD_SOURCE_FORMAT, GF3_NATIVE_PREVIEW_SOURCE_FORMAT}
|
||||||
|
|
||||||
PACKAGE_MODE_DIRECTORY = "directory"
|
PACKAGE_MODE_DIRECTORY = "directory"
|
||||||
PACKAGE_MODE_ZIP = "zip"
|
PACKAGE_MODE_ZIP = "zip"
|
||||||
@@ -58,11 +70,16 @@ _SAFE_ID_RE = re.compile(r"[^0-9A-Za-z_-]+")
|
|||||||
|
|
||||||
@dataclass(frozen=True)
|
@dataclass(frozen=True)
|
||||||
class DeliverySource:
|
class DeliverySource:
|
||||||
product: ResultProductORM
|
|
||||||
compat_row: Optional[DinsarResultORM]
|
|
||||||
display_name: str
|
display_name: str
|
||||||
source_path: str
|
source_path: str
|
||||||
|
product: Optional[ResultProductORM] = None
|
||||||
|
compat_row: Optional[DinsarResultORM] = None
|
||||||
source_asset_id: Optional[int] = None
|
source_asset_id: Optional[int] = None
|
||||||
|
source_radar_data_id: Optional[int] = None
|
||||||
|
source_scene_geo_id: Optional[int] = None
|
||||||
|
product_id: Optional[str] = None
|
||||||
|
task_name: Optional[str] = None
|
||||||
|
source_kind: str = "result_product"
|
||||||
|
|
||||||
|
|
||||||
def _new_session() -> AsyncSession:
|
def _new_session() -> AsyncSession:
|
||||||
@@ -208,6 +225,59 @@ def _resolve_source_path(product: ResultProductORM, assets: List[ResultAssetORM]
|
|||||||
return "", None
|
return "", None
|
||||||
|
|
||||||
|
|
||||||
|
def _scene_product_id(scene: SARSceneGeoORM) -> str:
|
||||||
|
return f"sar_scene_geo:{scene.id}"
|
||||||
|
|
||||||
|
|
||||||
|
def _scene_display_name(scene: SARSceneGeoORM, radar: RadarDataORM) -> str:
|
||||||
|
for value in (
|
||||||
|
radar.product_unique_id,
|
||||||
|
radar.unique_id,
|
||||||
|
radar.source_product_token,
|
||||||
|
os.path.basename(str(radar.file_path or "").rstrip("/\\")),
|
||||||
|
_scene_product_id(scene),
|
||||||
|
):
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if text:
|
||||||
|
return text
|
||||||
|
return f"scene_{scene.id}"
|
||||||
|
|
||||||
|
|
||||||
|
def _radar_display_name(radar: RadarDataORM) -> str:
|
||||||
|
for value in (
|
||||||
|
radar.product_unique_id,
|
||||||
|
radar.unique_id,
|
||||||
|
radar.source_product_token,
|
||||||
|
os.path.basename(str(radar.file_path or "").rstrip("/\\")),
|
||||||
|
f"radar_{radar.id}",
|
||||||
|
):
|
||||||
|
text = str(value or "").strip()
|
||||||
|
if text:
|
||||||
|
return text
|
||||||
|
return f"radar_{radar.id}"
|
||||||
|
|
||||||
|
|
||||||
|
def _manifest_path_for_scene(scene: SARSceneGeoORM) -> Optional[str]:
|
||||||
|
metadata = scene.analysis_metadata_json if isinstance(scene.analysis_metadata_json, dict) else {}
|
||||||
|
text = str(metadata.get("manifest_path") or "").strip()
|
||||||
|
if text:
|
||||||
|
return text
|
||||||
|
if scene.analysis_dir:
|
||||||
|
return os.path.join(scene.analysis_dir, "manifest.json")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _quality_path_for_scene(scene: SARSceneGeoORM) -> Optional[str]:
|
||||||
|
if scene.analysis_dir:
|
||||||
|
return os.path.join(scene.analysis_dir, "quality.json")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _catalog_file_size(path: Any) -> int:
|
||||||
|
text = str(path or "").strip()
|
||||||
|
return _file_size(text) if text else 0
|
||||||
|
|
||||||
|
|
||||||
class ResultDeliveryService:
|
class ResultDeliveryService:
|
||||||
def channels(self) -> List[Dict[str, Any]]:
|
def channels(self) -> List[Dict[str, Any]]:
|
||||||
return [
|
return [
|
||||||
@@ -220,7 +290,7 @@ class ResultDeliveryService:
|
|||||||
"description": "已登记 D-InSAR catalog,可创建后台交付包并下载到本地。",
|
"description": "已登记 D-InSAR catalog,可创建后台交付包并下载到本地。",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "sbas",
|
"key": CHANNEL_SBAS,
|
||||||
"group": "InSAR 成果",
|
"group": "InSAR 成果",
|
||||||
"label": "SBAS-InSAR 结果",
|
"label": "SBAS-InSAR 结果",
|
||||||
"state": "planned",
|
"state": "planned",
|
||||||
@@ -228,15 +298,15 @@ class ResultDeliveryService:
|
|||||||
"description": "SBAS 结果 catalog 已有基础能力,本阶段暂不开放交付打包。",
|
"description": "SBAS 结果 catalog 已有基础能力,本阶段暂不开放交付打包。",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "lt1_ortho",
|
"key": CHANNEL_LT1_ORTHO,
|
||||||
"group": "正射成果",
|
"group": "正射成果",
|
||||||
"label": "LT-1 正射结果",
|
"label": "LT-1 正射结果",
|
||||||
"state": "placeholder",
|
"state": "ready",
|
||||||
"state_text": "待接入",
|
"state_text": "可交付",
|
||||||
"description": "陆探一正射生产将由 LandSAR 生产链注册后接入统一交付。",
|
"description": "服务器生产的 LT-1 分析就绪正射 GeoTIFF 已接入交付,可打包下载到本地。",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "s1_ortho",
|
"key": CHANNEL_S1_ORTHO,
|
||||||
"group": "正射成果",
|
"group": "正射成果",
|
||||||
"label": "Sentinel-1 正射结果",
|
"label": "Sentinel-1 正射结果",
|
||||||
"state": "placeholder",
|
"state": "placeholder",
|
||||||
@@ -244,12 +314,12 @@ class ResultDeliveryService:
|
|||||||
"description": "Sentinel-1 正射生产尚未接入,当前只保留交付通道占位。",
|
"description": "Sentinel-1 正射生产尚未接入,当前只保留交付通道占位。",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"key": "gf3_ortho",
|
"key": CHANNEL_GF3_ORTHO,
|
||||||
"group": "正射成果",
|
"group": "正射成果",
|
||||||
"label": "GF3 SARscape _geo",
|
"label": "GF3 SARscape _geo",
|
||||||
"state": "placeholder",
|
"state": "ready",
|
||||||
"state_text": "待接入",
|
"state_text": "可交付",
|
||||||
"description": "GF3 外部生产成果登记后再接入统一交付。",
|
"description": "已登记的 GF3 SARscape 标准化正射成品可直接创建交付包。",
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
@@ -282,11 +352,14 @@ class ResultDeliveryService:
|
|||||||
seen_products.add(int(product.id))
|
seen_products.add(int(product.id))
|
||||||
sources.append(
|
sources.append(
|
||||||
DeliverySource(
|
DeliverySource(
|
||||||
product=product,
|
|
||||||
compat_row=None,
|
|
||||||
display_name=dinsar_read_service.get_display_name(product),
|
display_name=dinsar_read_service.get_display_name(product),
|
||||||
source_path=source_path,
|
source_path=source_path,
|
||||||
|
product=product,
|
||||||
|
compat_row=None,
|
||||||
source_asset_id=source_asset_id,
|
source_asset_id=source_asset_id,
|
||||||
|
product_id=product.product_id,
|
||||||
|
task_name=product.task_alias or product.task_name,
|
||||||
|
source_kind="dinsar_product",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -319,16 +392,344 @@ class ResultDeliveryService:
|
|||||||
seen_products.add(int(product.id))
|
seen_products.add(int(product.id))
|
||||||
sources.append(
|
sources.append(
|
||||||
DeliverySource(
|
DeliverySource(
|
||||||
product=product,
|
|
||||||
compat_row=record.compat_row,
|
|
||||||
display_name=record.display_name,
|
display_name=record.display_name,
|
||||||
source_path=source_path,
|
source_path=source_path,
|
||||||
|
product=product,
|
||||||
|
compat_row=record.compat_row,
|
||||||
source_asset_id=source_asset_id,
|
source_asset_id=source_asset_id,
|
||||||
|
product_id=product.product_id,
|
||||||
|
task_name=product.task_alias or product.task_name,
|
||||||
|
source_kind="dinsar_product",
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
return sources
|
return sources
|
||||||
|
|
||||||
|
async def _resolve_lt1_sources(
|
||||||
|
self,
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
item_ids: List[int],
|
||||||
|
product_ids: List[int],
|
||||||
|
) -> List[DeliverySource]:
|
||||||
|
selected_ids = item_ids or product_ids
|
||||||
|
if not selected_ids:
|
||||||
|
return []
|
||||||
|
result = await db.execute(
|
||||||
|
select(SARSceneGeoORM, RadarDataORM)
|
||||||
|
.join(RadarDataORM, SARSceneGeoORM.radar_data_id == RadarDataORM.id)
|
||||||
|
.where(
|
||||||
|
SARSceneGeoORM.id.in_(selected_ids),
|
||||||
|
SARSceneGeoORM.status == "DONE",
|
||||||
|
SARSceneGeoORM.analysis_tif_path.isnot(None),
|
||||||
|
SARSceneGeoORM.analysis_engine == LT1_ANALYSIS_ENGINE,
|
||||||
|
SARSceneGeoORM.analysis_profile == LT1_ANALYSIS_PROFILE,
|
||||||
|
)
|
||||||
|
.order_by(SARSceneGeoORM.id.asc())
|
||||||
|
)
|
||||||
|
sources: List[DeliverySource] = []
|
||||||
|
for scene, radar in result.all():
|
||||||
|
analysis_dir = str(scene.analysis_dir or "").strip()
|
||||||
|
analysis_tif = str(scene.analysis_tif_path or "").strip()
|
||||||
|
source_path = analysis_dir if analysis_dir and os.path.isdir(analysis_dir) else analysis_tif
|
||||||
|
if not source_path:
|
||||||
|
continue
|
||||||
|
product_id = _scene_product_id(scene)
|
||||||
|
display_name = _scene_display_name(scene, radar)
|
||||||
|
sources.append(
|
||||||
|
DeliverySource(
|
||||||
|
display_name=display_name,
|
||||||
|
source_path=source_path,
|
||||||
|
source_radar_data_id=int(radar.id) if radar.id is not None else None,
|
||||||
|
source_scene_geo_id=int(scene.id) if scene.id is not None else None,
|
||||||
|
product_id=product_id,
|
||||||
|
task_name=display_name,
|
||||||
|
source_kind="lt1_scene_geo",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sources
|
||||||
|
|
||||||
|
async def _resolve_gf3_sources(
|
||||||
|
self,
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
item_ids: List[int],
|
||||||
|
) -> List[DeliverySource]:
|
||||||
|
if not item_ids:
|
||||||
|
return []
|
||||||
|
result = await db.execute(
|
||||||
|
select(RadarDataORM, SARSceneGeoORM)
|
||||||
|
.outerjoin(SARSceneGeoORM, SARSceneGeoORM.radar_data_id == RadarDataORM.id)
|
||||||
|
.where(
|
||||||
|
RadarDataORM.id.in_(item_ids),
|
||||||
|
RadarDataORM.source_format.in_(GF3_DELIVERABLE_SOURCE_FORMATS),
|
||||||
|
RadarDataORM.geocoded_flag.is_(True),
|
||||||
|
)
|
||||||
|
.order_by(RadarDataORM.id.asc())
|
||||||
|
)
|
||||||
|
sources: List[DeliverySource] = []
|
||||||
|
for radar, scene in result.all():
|
||||||
|
source_path = ""
|
||||||
|
source_scene_geo_id = None
|
||||||
|
if scene is not None and scene.analysis_tif_path:
|
||||||
|
analysis_dir = str(scene.analysis_dir or "").strip()
|
||||||
|
analysis_tif = str(scene.analysis_tif_path or "").strip()
|
||||||
|
source_path = analysis_dir if analysis_dir and os.path.isdir(analysis_dir) else analysis_tif
|
||||||
|
source_scene_geo_id = int(scene.id) if scene.id is not None else None
|
||||||
|
if not source_path:
|
||||||
|
source_path = str(radar.file_path or "").strip()
|
||||||
|
if not source_path:
|
||||||
|
continue
|
||||||
|
display_name = _radar_display_name(radar)
|
||||||
|
sources.append(
|
||||||
|
DeliverySource(
|
||||||
|
display_name=display_name,
|
||||||
|
source_path=source_path,
|
||||||
|
source_radar_data_id=int(radar.id) if radar.id is not None else None,
|
||||||
|
source_scene_geo_id=source_scene_geo_id,
|
||||||
|
product_id=f"gf3_sarscape:{radar.id}",
|
||||||
|
task_name=display_name,
|
||||||
|
source_kind="gf3_radar_data",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return sources
|
||||||
|
|
||||||
|
async def _resolve_sources_for_channel(
|
||||||
|
self,
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
channel: str,
|
||||||
|
product_ids: List[int],
|
||||||
|
compat_result_ids: List[int],
|
||||||
|
item_ids: List[int],
|
||||||
|
) -> List[DeliverySource]:
|
||||||
|
if channel == CHANNEL_DINSAR:
|
||||||
|
return await self._resolve_dinsar_sources(
|
||||||
|
db,
|
||||||
|
product_ids=product_ids,
|
||||||
|
compat_result_ids=compat_result_ids,
|
||||||
|
)
|
||||||
|
if channel == CHANNEL_LT1_ORTHO:
|
||||||
|
return await self._resolve_lt1_sources(db, item_ids=item_ids, product_ids=product_ids)
|
||||||
|
if channel == CHANNEL_GF3_ORTHO:
|
||||||
|
return await self._resolve_gf3_sources(db, item_ids=item_ids or product_ids)
|
||||||
|
return []
|
||||||
|
|
||||||
|
async def list_channel_catalog(
|
||||||
|
self,
|
||||||
|
db: AsyncSession,
|
||||||
|
*,
|
||||||
|
channel: str,
|
||||||
|
limit: int = 100,
|
||||||
|
offset: int = 0,
|
||||||
|
query: Optional[str] = None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
channel = _normalize_channel(channel)
|
||||||
|
safe_limit = min(500, max(1, int(limit or 100)))
|
||||||
|
safe_offset = max(0, int(offset or 0))
|
||||||
|
query_text = str(query or "").strip()
|
||||||
|
|
||||||
|
if channel == CHANNEL_LT1_ORTHO:
|
||||||
|
filters = [
|
||||||
|
SARSceneGeoORM.status == "DONE",
|
||||||
|
SARSceneGeoORM.analysis_tif_path.isnot(None),
|
||||||
|
SARSceneGeoORM.analysis_engine == LT1_ANALYSIS_ENGINE,
|
||||||
|
SARSceneGeoORM.analysis_profile == LT1_ANALYSIS_PROFILE,
|
||||||
|
]
|
||||||
|
if query_text:
|
||||||
|
like = f"%{query_text}%"
|
||||||
|
filters.append(
|
||||||
|
or_(
|
||||||
|
RadarDataORM.product_unique_id.ilike(like),
|
||||||
|
RadarDataORM.unique_id.ilike(like),
|
||||||
|
RadarDataORM.file_path.ilike(like),
|
||||||
|
RadarDataORM.imaging_date.ilike(like),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
total_result = await db.execute(
|
||||||
|
select(func.count(SARSceneGeoORM.id))
|
||||||
|
.join(RadarDataORM, SARSceneGeoORM.radar_data_id == RadarDataORM.id)
|
||||||
|
.where(*filters)
|
||||||
|
)
|
||||||
|
rows_result = await db.execute(
|
||||||
|
select(SARSceneGeoORM, RadarDataORM)
|
||||||
|
.join(RadarDataORM, SARSceneGeoORM.radar_data_id == RadarDataORM.id)
|
||||||
|
.where(*filters)
|
||||||
|
.order_by(SARSceneGeoORM.updated_at.desc().nullslast(), SARSceneGeoORM.id.desc())
|
||||||
|
.limit(safe_limit)
|
||||||
|
.offset(safe_offset)
|
||||||
|
)
|
||||||
|
items = []
|
||||||
|
for scene, radar in rows_result.all():
|
||||||
|
display_name = _scene_display_name(scene, radar)
|
||||||
|
manifest_path = _manifest_path_for_scene(scene)
|
||||||
|
quality_path = _quality_path_for_scene(scene)
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"id": scene.id,
|
||||||
|
"item_id": scene.id,
|
||||||
|
"source_kind": "lt1_scene_geo",
|
||||||
|
"product_id": _scene_product_id(scene),
|
||||||
|
"catalog_name": "sar_scene_geo",
|
||||||
|
"product_family": "lt1_analysis_ready_geotiff",
|
||||||
|
"product_type": "analysis_ready_geotiff",
|
||||||
|
"display_name": display_name,
|
||||||
|
"task_name": display_name,
|
||||||
|
"status": scene.status,
|
||||||
|
"health_status": "OK",
|
||||||
|
"engine_code": scene.analysis_engine,
|
||||||
|
"profile_code": scene.analysis_profile,
|
||||||
|
"radar_data_id": radar.id,
|
||||||
|
"scene_geo_id": scene.id,
|
||||||
|
"imaging_date": radar.imaging_date,
|
||||||
|
"polarization": radar.polarization,
|
||||||
|
"pixel_size_m": scene.pixel_size_m,
|
||||||
|
"backscatter_unit": scene.analysis_backscatter_unit,
|
||||||
|
"publish_dir": scene.analysis_dir,
|
||||||
|
"manifest_path": manifest_path,
|
||||||
|
"quality_path": quality_path,
|
||||||
|
"primary_asset_path": scene.analysis_tif_path,
|
||||||
|
"file_size": _catalog_file_size(scene.analysis_tif_path),
|
||||||
|
"summary": {
|
||||||
|
"radar_data_id": radar.id,
|
||||||
|
"source_asset_ids": [radar.source_product_ref_id] if radar.source_product_ref_id else [],
|
||||||
|
"imaging_date": radar.imaging_date,
|
||||||
|
"polarization": radar.polarization,
|
||||||
|
"pixel_size_m": scene.pixel_size_m,
|
||||||
|
},
|
||||||
|
"produced_at": scene.updated_at.isoformat() if scene.updated_at else None,
|
||||||
|
"published_at": scene.updated_at.isoformat() if scene.updated_at else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"items": items,
|
||||||
|
"total": int(total_result.scalar_one() or 0),
|
||||||
|
"limit": safe_limit,
|
||||||
|
"offset": safe_offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel == CHANNEL_GF3_ORTHO:
|
||||||
|
filters = [
|
||||||
|
RadarDataORM.source_format.in_(GF3_DELIVERABLE_SOURCE_FORMATS),
|
||||||
|
RadarDataORM.geocoded_flag.is_(True),
|
||||||
|
]
|
||||||
|
if query_text:
|
||||||
|
like = f"%{query_text}%"
|
||||||
|
filters.append(
|
||||||
|
or_(
|
||||||
|
RadarDataORM.product_unique_id.ilike(like),
|
||||||
|
RadarDataORM.unique_id.ilike(like),
|
||||||
|
RadarDataORM.file_path.ilike(like),
|
||||||
|
RadarDataORM.imaging_date.ilike(like),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
total_result = await db.execute(select(func.count(RadarDataORM.id)).where(*filters))
|
||||||
|
rows_result = await db.execute(
|
||||||
|
select(RadarDataORM, SARSceneGeoORM)
|
||||||
|
.outerjoin(SARSceneGeoORM, SARSceneGeoORM.radar_data_id == RadarDataORM.id)
|
||||||
|
.where(*filters)
|
||||||
|
.order_by(RadarDataORM.acquisition_start_time_utc.desc().nullslast(), RadarDataORM.id.desc())
|
||||||
|
.limit(safe_limit)
|
||||||
|
.offset(safe_offset)
|
||||||
|
)
|
||||||
|
items = []
|
||||||
|
for radar, scene in rows_result.all():
|
||||||
|
display_name = _radar_display_name(radar)
|
||||||
|
primary_path = (
|
||||||
|
str(scene.analysis_tif_path or "").strip()
|
||||||
|
if scene is not None and scene.analysis_tif_path
|
||||||
|
else str(radar.file_path or "").strip()
|
||||||
|
)
|
||||||
|
publish_dir = (
|
||||||
|
str(scene.analysis_dir or "").strip()
|
||||||
|
if scene is not None and scene.analysis_dir
|
||||||
|
else str(radar.file_path or "").strip()
|
||||||
|
)
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"id": radar.id,
|
||||||
|
"item_id": radar.id,
|
||||||
|
"source_kind": "gf3_radar_data",
|
||||||
|
"product_id": f"gf3_sarscape:{radar.id}",
|
||||||
|
"catalog_name": "radar_data",
|
||||||
|
"product_family": "gf3_ortho",
|
||||||
|
"product_type": "sarscape_l2_geotiff" if radar.source_format == GF3_STANDARD_SOURCE_FORMAT else "sarscape_native_geo",
|
||||||
|
"display_name": display_name,
|
||||||
|
"task_name": display_name,
|
||||||
|
"status": "READY",
|
||||||
|
"health_status": "OK",
|
||||||
|
"engine_code": (scene.analysis_engine if scene is not None else None) or "gf3_sarscape",
|
||||||
|
"profile_code": (
|
||||||
|
(scene.analysis_profile if scene is not None else None)
|
||||||
|
or ("gf3_standard_geotiff" if radar.source_format == GF3_STANDARD_SOURCE_FORMAT else "gf3_native_geo")
|
||||||
|
),
|
||||||
|
"source_format": radar.source_format,
|
||||||
|
"radar_data_id": radar.id,
|
||||||
|
"scene_geo_id": scene.id if scene is not None else None,
|
||||||
|
"imaging_date": radar.imaging_date,
|
||||||
|
"polarization": radar.polarization,
|
||||||
|
"pixel_size_m": scene.pixel_size_m if scene is not None else None,
|
||||||
|
"backscatter_unit": scene.analysis_backscatter_unit if scene is not None else None,
|
||||||
|
"publish_dir": publish_dir,
|
||||||
|
"manifest_path": _manifest_path_for_scene(scene) if scene is not None else None,
|
||||||
|
"primary_asset_path": primary_path,
|
||||||
|
"file_size": _catalog_file_size(primary_path),
|
||||||
|
"summary": {
|
||||||
|
"radar_data_id": radar.id,
|
||||||
|
"source_asset_ids": [radar.source_product_ref_id] if radar.source_product_ref_id else [],
|
||||||
|
"imaging_date": radar.imaging_date,
|
||||||
|
"polarization": radar.polarization,
|
||||||
|
},
|
||||||
|
"produced_at": (
|
||||||
|
scene.updated_at.isoformat() if scene is not None and scene.updated_at else None
|
||||||
|
),
|
||||||
|
"published_at": (
|
||||||
|
scene.updated_at.isoformat() if scene is not None and scene.updated_at else None
|
||||||
|
),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"items": items,
|
||||||
|
"total": int(total_result.scalar_one() or 0),
|
||||||
|
"limit": safe_limit,
|
||||||
|
"offset": safe_offset,
|
||||||
|
}
|
||||||
|
|
||||||
|
if channel == CHANNEL_DINSAR:
|
||||||
|
total = await dinsar_read_service.count_compat_records(db)
|
||||||
|
records = await dinsar_read_service.list_compat_records(db, limit=safe_limit, offset=safe_offset)
|
||||||
|
items = []
|
||||||
|
for record in records:
|
||||||
|
product = record.product
|
||||||
|
compat_row = record.compat_row
|
||||||
|
items.append(
|
||||||
|
{
|
||||||
|
"id": int(compat_row.id if compat_row is not None else product.id),
|
||||||
|
"item_id": int(compat_row.id if compat_row is not None else product.id),
|
||||||
|
"source_kind": "dinsar_result",
|
||||||
|
"product_id": product.product_id,
|
||||||
|
"catalog_name": product.catalog_name,
|
||||||
|
"product_family": product.product_family,
|
||||||
|
"product_type": product.product_type,
|
||||||
|
"display_name": record.display_name,
|
||||||
|
"task_name": product.task_alias or product.task_name,
|
||||||
|
"status": product.status,
|
||||||
|
"health_status": product.health_status,
|
||||||
|
"engine_code": product.engine_code,
|
||||||
|
"profile_code": product.profile_code,
|
||||||
|
"pair_key": product.pair_key,
|
||||||
|
"publish_dir": product.publish_dir,
|
||||||
|
"manifest_path": product.manifest_path,
|
||||||
|
"primary_asset_path": product.primary_asset_path or product.source_primary_path,
|
||||||
|
"file_size": _catalog_file_size(product.primary_asset_path or product.source_primary_path),
|
||||||
|
"produced_at": product.produced_at.isoformat() if product.produced_at else None,
|
||||||
|
"published_at": product.published_at.isoformat() if product.published_at else None,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return {"items": items, "total": total, "limit": safe_limit, "offset": safe_offset}
|
||||||
|
|
||||||
|
return {"items": [], "total": 0, "limit": safe_limit, "offset": safe_offset}
|
||||||
|
|
||||||
async def create_delivery(
|
async def create_delivery(
|
||||||
self,
|
self,
|
||||||
db: AsyncSession,
|
db: AsyncSession,
|
||||||
@@ -337,6 +738,7 @@ class ResultDeliveryService:
|
|||||||
channel: str,
|
channel: str,
|
||||||
product_ids: Optional[List[int]] = None,
|
product_ids: Optional[List[int]] = None,
|
||||||
compat_result_ids: Optional[List[int]] = None,
|
compat_result_ids: Optional[List[int]] = None,
|
||||||
|
item_ids: Optional[List[int]] = None,
|
||||||
package_mode: str = PACKAGE_MODE_DIRECTORY,
|
package_mode: str = PACKAGE_MODE_DIRECTORY,
|
||||||
include_checksums: Optional[bool] = None,
|
include_checksums: Optional[bool] = None,
|
||||||
) -> ResultDeliveryRequestORM:
|
) -> ResultDeliveryRequestORM:
|
||||||
@@ -348,20 +750,20 @@ class ResultDeliveryService:
|
|||||||
max_items = max(1, int(settings.RESULT_DELIVERY_MAX_ITEMS or 500))
|
max_items = max(1, int(settings.RESULT_DELIVERY_MAX_ITEMS or 500))
|
||||||
normalized_product_ids = _normalize_int_ids(product_ids, max_count=max_items)
|
normalized_product_ids = _normalize_int_ids(product_ids, max_count=max_items)
|
||||||
normalized_compat_ids = _normalize_int_ids(compat_result_ids, max_count=max_items)
|
normalized_compat_ids = _normalize_int_ids(compat_result_ids, max_count=max_items)
|
||||||
if not normalized_product_ids and not normalized_compat_ids:
|
normalized_item_ids = _normalize_int_ids(item_ids, max_count=max_items)
|
||||||
|
if not normalized_product_ids and not normalized_compat_ids and not normalized_item_ids:
|
||||||
raise ValueError("select at least one result")
|
raise ValueError("select at least one result")
|
||||||
|
|
||||||
if len(normalized_product_ids) + len(normalized_compat_ids) > max_items:
|
if len(normalized_product_ids) + len(normalized_compat_ids) + len(normalized_item_ids) > max_items:
|
||||||
raise ValueError(f"selected item count exceeds max limit ({max_items})")
|
raise ValueError(f"selected item count exceeds max limit ({max_items})")
|
||||||
|
|
||||||
if channel == CHANNEL_DINSAR:
|
sources = await self._resolve_sources_for_channel(
|
||||||
sources = await self._resolve_dinsar_sources(
|
db,
|
||||||
db,
|
channel=channel,
|
||||||
product_ids=normalized_product_ids,
|
product_ids=normalized_product_ids,
|
||||||
compat_result_ids=normalized_compat_ids,
|
compat_result_ids=normalized_compat_ids,
|
||||||
)
|
item_ids=normalized_item_ids,
|
||||||
else:
|
)
|
||||||
sources = []
|
|
||||||
|
|
||||||
if not sources:
|
if not sources:
|
||||||
raise ValueError("selected results do not have deliverable files")
|
raise ValueError("selected results do not have deliverable files")
|
||||||
@@ -376,6 +778,7 @@ class ResultDeliveryService:
|
|||||||
"channel": channel,
|
"channel": channel,
|
||||||
"product_ids": normalized_product_ids,
|
"product_ids": normalized_product_ids,
|
||||||
"compat_result_ids": normalized_compat_ids,
|
"compat_result_ids": normalized_compat_ids,
|
||||||
|
"item_ids": normalized_item_ids,
|
||||||
"package_mode": package_mode,
|
"package_mode": package_mode,
|
||||||
"include_checksums": (
|
"include_checksums": (
|
||||||
bool(settings.RESULT_DELIVERY_CHECKSUM_ENABLED)
|
bool(settings.RESULT_DELIVERY_CHECKSUM_ENABLED)
|
||||||
@@ -544,6 +947,8 @@ class ResultDeliveryService:
|
|||||||
"source_product_id": item.source_product_id,
|
"source_product_id": item.source_product_id,
|
||||||
"source_result_id": item.source_result_id,
|
"source_result_id": item.source_result_id,
|
||||||
"source_asset_id": item.source_asset_id,
|
"source_asset_id": item.source_asset_id,
|
||||||
|
"source_radar_data_id": item.source_radar_data_id,
|
||||||
|
"source_scene_geo_id": item.source_scene_geo_id,
|
||||||
"display_name": item.display_name,
|
"display_name": item.display_name,
|
||||||
"relative_path": item.relative_path,
|
"relative_path": item.relative_path,
|
||||||
"file_size": item.file_size,
|
"file_size": item.file_size,
|
||||||
@@ -636,14 +1041,14 @@ class ResultDeliveryService:
|
|||||||
)
|
)
|
||||||
delivery = result.scalar_one()
|
delivery = result.scalar_one()
|
||||||
request_json = delivery.request_json if isinstance(delivery.request_json, dict) else {}
|
request_json = delivery.request_json if isinstance(delivery.request_json, dict) else {}
|
||||||
if delivery.channel == CHANNEL_DINSAR:
|
max_items = int(settings.RESULT_DELIVERY_MAX_ITEMS or 500)
|
||||||
sources = await self._resolve_dinsar_sources(
|
sources = await self._resolve_sources_for_channel(
|
||||||
db,
|
db,
|
||||||
product_ids=_normalize_int_ids(request_json.get("product_ids"), max_count=int(settings.RESULT_DELIVERY_MAX_ITEMS or 500)),
|
channel=delivery.channel,
|
||||||
compat_result_ids=_normalize_int_ids(request_json.get("compat_result_ids"), max_count=int(settings.RESULT_DELIVERY_MAX_ITEMS or 500)),
|
product_ids=_normalize_int_ids(request_json.get("product_ids"), max_count=max_items),
|
||||||
)
|
compat_result_ids=_normalize_int_ids(request_json.get("compat_result_ids"), max_count=max_items),
|
||||||
else:
|
item_ids=_normalize_int_ids(request_json.get("item_ids"), max_count=max_items),
|
||||||
sources = []
|
)
|
||||||
task_id = delivery.task_id
|
task_id = delivery.task_id
|
||||||
|
|
||||||
if not sources:
|
if not sources:
|
||||||
@@ -672,10 +1077,9 @@ class ResultDeliveryService:
|
|||||||
total_sources = len(sources)
|
total_sources = len(sources)
|
||||||
for source_index, source in enumerate(sources, start=1):
|
for source_index, source in enumerate(sources, start=1):
|
||||||
folder = _sanitize_segment(
|
folder = _sanitize_segment(
|
||||||
source.product.task_alias
|
source.task_name
|
||||||
or source.product.task_name
|
|
||||||
or source.display_name
|
or source.display_name
|
||||||
or source.product.product_id,
|
or source.product_id,
|
||||||
default=f"product_{source_index}",
|
default=f"product_{source_index}",
|
||||||
)
|
)
|
||||||
source_files = _iter_associated_files(source.source_path)
|
source_files = _iter_associated_files(source.source_path)
|
||||||
@@ -684,9 +1088,11 @@ class ResultDeliveryService:
|
|||||||
item_payloads.append(
|
item_payloads.append(
|
||||||
{
|
{
|
||||||
"delivery_id": delivery_id,
|
"delivery_id": delivery_id,
|
||||||
"source_product_id": source.product.id,
|
"source_product_id": source.product.id if source.product else None,
|
||||||
"source_result_id": source.compat_row.id if source.compat_row else None,
|
"source_result_id": source.compat_row.id if source.compat_row else None,
|
||||||
"source_asset_id": source.source_asset_id,
|
"source_asset_id": source.source_asset_id,
|
||||||
|
"source_radar_data_id": source.source_radar_data_id,
|
||||||
|
"source_scene_geo_id": source.source_scene_geo_id,
|
||||||
"display_name": source.display_name,
|
"display_name": source.display_name,
|
||||||
"source_path": source.source_path,
|
"source_path": source.source_path,
|
||||||
"relative_path": None,
|
"relative_path": None,
|
||||||
@@ -736,9 +1142,11 @@ class ResultDeliveryService:
|
|||||||
|
|
||||||
item_payload = {
|
item_payload = {
|
||||||
"delivery_id": delivery_id,
|
"delivery_id": delivery_id,
|
||||||
"source_product_id": source.product.id,
|
"source_product_id": source.product.id if source.product else None,
|
||||||
"source_result_id": source.compat_row.id if source.compat_row else None,
|
"source_result_id": source.compat_row.id if source.compat_row else None,
|
||||||
"source_asset_id": source.source_asset_id if file_index == 1 else None,
|
"source_asset_id": source.source_asset_id if file_index == 1 else None,
|
||||||
|
"source_radar_data_id": source.source_radar_data_id,
|
||||||
|
"source_scene_geo_id": source.source_scene_geo_id,
|
||||||
"display_name": source.display_name,
|
"display_name": source.display_name,
|
||||||
"source_path": source_file,
|
"source_path": source_file,
|
||||||
"relative_path": relative_path,
|
"relative_path": relative_path,
|
||||||
@@ -751,9 +1159,12 @@ class ResultDeliveryService:
|
|||||||
manifest_items.append(
|
manifest_items.append(
|
||||||
{
|
{
|
||||||
"display_name": source.display_name,
|
"display_name": source.display_name,
|
||||||
"product_id": source.product.product_id,
|
"product_id": source.product_id or (source.product.product_id if source.product else None),
|
||||||
"product_ref_id": source.product.id,
|
"product_ref_id": source.product.id if source.product else None,
|
||||||
"source_result_id": source.compat_row.id if source.compat_row else None,
|
"source_result_id": source.compat_row.id if source.compat_row else None,
|
||||||
|
"source_radar_data_id": source.source_radar_data_id,
|
||||||
|
"source_scene_geo_id": source.source_scene_geo_id,
|
||||||
|
"source_kind": source.source_kind,
|
||||||
"relative_path": relative_path.replace(os.sep, "/"),
|
"relative_path": relative_path.replace(os.sep, "/"),
|
||||||
"file_size": size,
|
"file_size": size,
|
||||||
"checksum_sha256": checksum,
|
"checksum_sha256": checksum,
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
ALTER TABLE result_delivery_items ADD COLUMN IF NOT EXISTS source_radar_data_id INTEGER NULL REFERENCES radar_data(id) ON DELETE SET NULL;
|
||||||
|
ALTER TABLE result_delivery_items ADD COLUMN IF NOT EXISTS source_scene_geo_id INTEGER NULL REFERENCES sar_scene_geo(id) ON DELETE SET NULL;
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_result_delivery_items_source_radar_data_id ON result_delivery_items(source_radar_data_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS ix_result_delivery_items_source_scene_geo_id ON result_delivery_items(source_scene_geo_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_result_delivery_items_radar ON result_delivery_items(source_radar_data_id);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_result_delivery_items_scene_geo ON result_delivery_items(source_scene_geo_id);
|
||||||
@@ -14,8 +14,9 @@
|
|||||||
## 目标
|
## 目标
|
||||||
|
|
||||||
- 所有登录用户都可以申请成果交付。
|
- 所有登录用户都可以申请成果交付。
|
||||||
- D-InSAR 已登记成果先接入真实交付下载。
|
- D-InSAR 已登记成果接入真实交付下载。
|
||||||
- SBAS、LT-1 正射、Sentinel-1 正射、GF3 正射在页面和接口中保留清晰占位,不暴露假执行能力。
|
- LT-1 正射与 GF3 正射接入真实交付下载。
|
||||||
|
- SBAS 与 Sentinel-1 正射在页面和接口中保留清晰占位,不暴露假执行能力。
|
||||||
- 大文件交付改为后台任务,不再由 HTTP 请求同步复制。
|
- 大文件交付改为后台任务,不再由 HTTP 请求同步复制。
|
||||||
- 用户最终可以把成果下载到本地;服务器交付区只是临时缓存。
|
- 用户最终可以把成果下载到本地;服务器交付区只是临时缓存。
|
||||||
- 每次交付可审计、可过期清理、可校验。
|
- 每次交付可审计、可过期清理、可校验。
|
||||||
@@ -25,6 +26,7 @@
|
|||||||
|
|
||||||
- 本阶段不实现 Sentinel-1 正射生产。
|
- 本阶段不实现 Sentinel-1 正射生产。
|
||||||
- 本阶段不实现 SBAS 成果交付打包,只显示目录已接入但交付未接入。
|
- 本阶段不实现 SBAS 成果交付打包,只显示目录已接入但交付未接入。
|
||||||
|
- 本阶段不触发 GF3 生产;GF3 交付只面向已经登记的 SARscape 标准化正射成品。
|
||||||
- 本阶段不做跨节点对象存储或外部网盘。
|
- 本阶段不做跨节点对象存储或外部网盘。
|
||||||
- 本阶段不把普通用户开放到任意服务器路径写入。
|
- 本阶段不把普通用户开放到任意服务器路径写入。
|
||||||
|
|
||||||
@@ -43,6 +45,13 @@
|
|||||||
|
|
||||||
## 交付模式
|
## 交付模式
|
||||||
|
|
||||||
|
## 正射成果口径
|
||||||
|
|
||||||
|
- LT-1 正射:由服务器侧单景正射流水线生产,登记在 `sar_scene_geo`,主交付物是 `analysis_ready.tif`,随包包含预览、manifest、quality 等 sidecar 文件。
|
||||||
|
- GF3 正射:系统拿到的就是外部 SARscape 已生产成品,当前可登记为 `GF3_SARSCAPE_NATIVE_PREVIEW`,标准化后也可登记为 `GF3_SARSCAPE_L2`;交付阶段只做受控打包和下载,不触发 GF3 生产。
|
||||||
|
- Sentinel-1 正射:生产链尚未接入,结果提取页面保留占位,避免用户误操作。
|
||||||
|
- SBAS-InSAR:成果目录可查,但交付打包尚未接入。
|
||||||
|
|
||||||
### 1. 目录交付
|
### 1. 目录交付
|
||||||
|
|
||||||
默认模式。后台将选中的结果文件复制到:
|
默认模式。后台将选中的结果文件复制到:
|
||||||
@@ -97,6 +106,7 @@
|
|||||||
|
|
||||||
- `delivery_id`。
|
- `delivery_id`。
|
||||||
- `source_product_id` / `source_result_id`。
|
- `source_product_id` / `source_result_id`。
|
||||||
|
- `source_radar_data_id` / `source_scene_geo_id`:用于 LT-1/GF3 单景正射资产追踪。
|
||||||
- `display_name`。
|
- `display_name`。
|
||||||
- `source_path`。
|
- `source_path`。
|
||||||
- `relative_path`。
|
- `relative_path`。
|
||||||
@@ -133,9 +143,23 @@ GET /api/result-deliveries/channels
|
|||||||
|
|
||||||
- `dinsar`: `ready`
|
- `dinsar`: `ready`
|
||||||
- `sbas`: `planned`
|
- `sbas`: `planned`
|
||||||
- `lt1_ortho`: `planned`
|
- `lt1_ortho`: `ready`
|
||||||
- `s1_ortho`: `placeholder`
|
- `s1_ortho`: `placeholder`
|
||||||
- `gf3_ortho`: `placeholder`
|
- `gf3_ortho`: `ready`
|
||||||
|
|
||||||
|
### 获取可交付目录
|
||||||
|
|
||||||
|
```http
|
||||||
|
GET /api/result-deliveries/catalog/{channel}
|
||||||
|
```
|
||||||
|
|
||||||
|
第一版支持:
|
||||||
|
|
||||||
|
- `dinsar`:兼容已有 D-InSAR catalog。
|
||||||
|
- `lt1_ortho`:读取 `sar_scene_geo` 中 `lt_gamma / lt1_gamma_geocoded_mli` 且 `DONE` 的分析就绪 GeoTIFF。
|
||||||
|
- `gf3_ortho`:读取 `radar_data.source_format in (GF3_SARSCAPE_NATIVE_PREVIEW, GF3_SARSCAPE_L2)` 且 `geocoded_flag = true` 的 SARscape 正射成品。
|
||||||
|
|
||||||
|
返回统一字段包括 `item_id`、`source_kind`、`product_id`、`display_name`、`primary_asset_path`、`publish_dir`、`radar_data_id`、`scene_geo_id`。
|
||||||
|
|
||||||
### 创建交付任务
|
### 创建交付任务
|
||||||
|
|
||||||
@@ -150,6 +174,7 @@ POST /api/result-deliveries
|
|||||||
"channel": "dinsar",
|
"channel": "dinsar",
|
||||||
"product_ids": ["..."],
|
"product_ids": ["..."],
|
||||||
"compat_result_ids": [1, 2, 3],
|
"compat_result_ids": [1, 2, 3],
|
||||||
|
"item_ids": [101, 102],
|
||||||
"package_mode": "directory",
|
"package_mode": "directory",
|
||||||
"include_manifest": true,
|
"include_manifest": true,
|
||||||
"include_checksums": true
|
"include_checksums": true
|
||||||
@@ -159,7 +184,9 @@ POST /api/result-deliveries
|
|||||||
约束:
|
约束:
|
||||||
|
|
||||||
- 普通用户只创建自己的任务。
|
- 普通用户只创建自己的任务。
|
||||||
- `channel != dinsar` 时第一版返回 409 或 422,提示“通道尚未接入交付”。
|
- `dinsar` 使用 `compat_result_ids` 或 `product_ids`。
|
||||||
|
- `lt1_ortho` 使用 `item_ids = sar_scene_geo.id`。
|
||||||
|
- `gf3_ortho` 使用 `item_ids = radar_data.id`。
|
||||||
- 每次最大数量由 `RESULT_DELIVERY_MAX_ITEMS` 控制。
|
- 每次最大数量由 `RESULT_DELIVERY_MAX_ITEMS` 控制。
|
||||||
- 不允许传入任意服务器输出路径。
|
- 不允许传入任意服务器输出路径。
|
||||||
|
|
||||||
@@ -200,7 +227,7 @@ RESULT_DELIVERY_BUILD
|
|||||||
处理流程:
|
处理流程:
|
||||||
|
|
||||||
1. 将 delivery 标记为 `RUNNING`。
|
1. 将 delivery 标记为 `RUNNING`。
|
||||||
2. 解析 D-InSAR catalog 中的文件路径。
|
2. 按 channel 解析可交付源文件路径。
|
||||||
3. 复制到交付目录。
|
3. 复制到交付目录。
|
||||||
4. 生成 `manifest.json`。
|
4. 生成 `manifest.json`。
|
||||||
5. 可选计算 checksum。
|
5. 可选计算 checksum。
|
||||||
@@ -244,8 +271,8 @@ RESULT_DELIVERY_CHECKSUM_ENABLED=true
|
|||||||
|
|
||||||
页面结构:
|
页面结构:
|
||||||
|
|
||||||
- 通道栏:D-InSAR 可用,SBAS/LT-1/Sentinel-1/GF3 明确显示“未接入交付”。
|
- 通道栏:D-InSAR、LT-1 正射、GF3 正射可用;SBAS/Sentinel-1 显示未接入交付。
|
||||||
- 成果选择区:复用现有 D-InSAR catalog 列表。
|
- 成果选择区:D-InSAR 复用现有 catalog 列表,LT-1/GF3 使用统一交付 catalog 查询。
|
||||||
- 交付选项:目录交付 / 压缩包交付。
|
- 交付选项:目录交付 / 压缩包交付。
|
||||||
- 我的交付包:状态、大小、文件数、过期时间、下载入口。
|
- 我的交付包:状态、大小、文件数、过期时间、下载入口。
|
||||||
|
|
||||||
@@ -283,7 +310,7 @@ RESULT_DELIVERY_CHECKSUM_ENABLED=true
|
|||||||
|
|
||||||
- 文档落地。
|
- 文档落地。
|
||||||
- 数据表和自维护 migration。
|
- 数据表和自维护 migration。
|
||||||
- D-InSAR 目录交付后台任务。
|
- D-InSAR、LT-1 正射、GF3 正射目录交付后台任务。
|
||||||
- 我的交付包列表和详情。
|
- 我的交付包列表和详情。
|
||||||
|
|
||||||
### 阶段 2
|
### 阶段 2
|
||||||
@@ -295,12 +322,12 @@ RESULT_DELIVERY_CHECKSUM_ENABLED=true
|
|||||||
### 阶段 3
|
### 阶段 3
|
||||||
|
|
||||||
- SBAS 交付接入。
|
- SBAS 交付接入。
|
||||||
- LT-1 正射、Sentinel-1 正射、GF3 正射在生产 catalog 完成后接入。
|
- Sentinel-1 正射在生产 catalog 完成后接入。
|
||||||
- exporter/operator 角色拆分。
|
- exporter/operator 角色拆分。
|
||||||
|
|
||||||
## 验收标准
|
## 验收标准
|
||||||
|
|
||||||
- 普通登录用户能创建 D-InSAR 成果交付任务。
|
- 普通登录用户能创建 D-InSAR、LT-1 正射、GF3 正射成果交付任务。
|
||||||
- HTTP 请求只排队任务,不再同步复制大文件。
|
- HTTP 请求只排队任务,不再同步复制大文件。
|
||||||
- 交付完成后用户能下载到本地。
|
- 交付完成后用户能下载到本地。
|
||||||
- 普通用户不能指定任意服务器目录。
|
- 普通用户不能指定任意服务器目录。
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
getResultDeliveryArchiveUrl,
|
getResultDeliveryArchiveUrl,
|
||||||
getResultDeliveryDownloadUrl,
|
getResultDeliveryDownloadUrl,
|
||||||
getResultDeliveryManifestUrl,
|
getResultDeliveryManifestUrl,
|
||||||
|
listResultDeliveryCatalog,
|
||||||
listResultDeliveries,
|
listResultDeliveries,
|
||||||
} from './api/resultDeliveries';
|
} from './api/resultDeliveries';
|
||||||
import { getDinsarEngineMeta } from './utils/dinsarEngines';
|
import { getDinsarEngineMeta } from './utils/dinsarEngines';
|
||||||
@@ -34,9 +35,9 @@ const PRODUCT_CHANNELS = [
|
|||||||
key: 'lt1_ortho',
|
key: 'lt1_ortho',
|
||||||
group: '正射成果',
|
group: '正射成果',
|
||||||
label: 'LT-1 正射结果',
|
label: 'LT-1 正射结果',
|
||||||
state: 'placeholder',
|
state: 'ready',
|
||||||
stateText: '待接入',
|
stateText: '可交付',
|
||||||
description: '陆探一正射生产将由 LandSAR 生产链注册到统一成果目录后开放交付。',
|
description: '服务器生产的 LT-1 分析就绪正射 GeoTIFF 已接入交付,可打包下载到本地。',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 's1_ortho',
|
key: 's1_ortho',
|
||||||
@@ -50,9 +51,9 @@ const PRODUCT_CHANNELS = [
|
|||||||
key: 'gf3_ortho',
|
key: 'gf3_ortho',
|
||||||
group: '正射成果',
|
group: '正射成果',
|
||||||
label: 'GF3 SARscape _geo',
|
label: 'GF3 SARscape _geo',
|
||||||
state: 'placeholder',
|
state: 'ready',
|
||||||
stateText: '待接入',
|
stateText: '可交付',
|
||||||
description: 'GF3 外部生产成果登记后再接入统一交付。',
|
description: '已登记的 GF3 SARscape 标准化正射成品可直接创建交付包。',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -85,17 +86,89 @@ function extractTotal(payload, fallback = 0) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resultDisplayName(result) {
|
function resultDisplayName(result) {
|
||||||
return String(result?.name || result?.task_alias || result?.task_name || result?.product_id || `#${result?.id || ''}`).trim();
|
return String(result?.display_name || result?.name || result?.task_alias || result?.task_name || result?.product_id || `#${result?.id || ''}`).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
function resultItemId(result) {
|
||||||
|
return result?.item_id ?? result?.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectionKey(channelKey, id) {
|
||||||
|
return `${channelKey}:${id}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function resultDateText(result) {
|
function resultDateText(result) {
|
||||||
const name = resultDisplayName(result);
|
const name = resultDisplayName(result);
|
||||||
|
if (result?.imaging_date) return String(result.imaging_date);
|
||||||
const matches = name.match(/(\d{8})/g);
|
const matches = name.match(/(\d{8})/g);
|
||||||
if (matches?.length >= 2) return `${matches[0]} / ${matches[1]}`;
|
if (matches?.length >= 2) return `${matches[0]} / ${matches[1]}`;
|
||||||
if (matches?.length === 1) return matches[0];
|
if (matches?.length === 1) return matches[0];
|
||||||
return '-';
|
return '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function channelMetricLabel(channelKey) {
|
||||||
|
return {
|
||||||
|
dinsar: 'D-InSAR 可交付',
|
||||||
|
sbas: 'SBAS 目录',
|
||||||
|
lt1_ortho: 'LT-1 正射',
|
||||||
|
gf3_ortho: 'GF3 正射',
|
||||||
|
s1_ortho: 'Sentinel-1 正射',
|
||||||
|
}[channelKey] || '可交付结果';
|
||||||
|
}
|
||||||
|
|
||||||
|
function channelListTitle(channelKey) {
|
||||||
|
return {
|
||||||
|
dinsar: 'D-InSAR 结果列表',
|
||||||
|
lt1_ortho: 'LT-1 正射结果列表',
|
||||||
|
gf3_ortho: 'GF3 正射结果列表',
|
||||||
|
}[channelKey] || '结果列表';
|
||||||
|
}
|
||||||
|
|
||||||
|
function channelEmptyText(channelKey) {
|
||||||
|
return {
|
||||||
|
dinsar: '当前条件下没有可交付的 D-InSAR 结果。',
|
||||||
|
lt1_ortho: '当前条件下没有可交付的 LT-1 正射结果。',
|
||||||
|
gf3_ortho: '当前条件下没有可交付的 GF3 正射结果。',
|
||||||
|
}[channelKey] || '当前条件下没有可交付结果。';
|
||||||
|
}
|
||||||
|
|
||||||
|
function channelCreateTitle(channelKey) {
|
||||||
|
return {
|
||||||
|
dinsar: 'D-InSAR 成果交付',
|
||||||
|
lt1_ortho: 'LT-1 正射成果交付',
|
||||||
|
gf3_ortho: 'GF3 正射成果交付',
|
||||||
|
}[channelKey] || '成果交付';
|
||||||
|
}
|
||||||
|
|
||||||
|
function channelCreateSubtitle(channelKey) {
|
||||||
|
return {
|
||||||
|
dinsar: '选择已登记结果并生成下载包',
|
||||||
|
lt1_ortho: '选择服务器已生产正射 GeoTIFF 并生成下载包',
|
||||||
|
gf3_ortho: '选择已登记 GF3 SARscape 正射成品并生成下载包',
|
||||||
|
}[channelKey] || '选择结果并生成下载包';
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemMetaText(item, channelKey) {
|
||||||
|
if (channelKey === 'dinsar') {
|
||||||
|
return `${resultDateText(item)} · ${item.pair_key || item.product_id || '-'}`;
|
||||||
|
}
|
||||||
|
const parts = [
|
||||||
|
item.imaging_date || resultDateText(item),
|
||||||
|
item.polarization,
|
||||||
|
item.pixel_size_m ? `${item.pixel_size_m} m` : null,
|
||||||
|
item.product_id,
|
||||||
|
].filter(Boolean);
|
||||||
|
return parts.join(' · ') || '-';
|
||||||
|
}
|
||||||
|
|
||||||
|
function itemStatusText(item, channelKey) {
|
||||||
|
if (channelKey === 'dinsar') {
|
||||||
|
return item.is_cached ? '预览就绪' : '预览待建';
|
||||||
|
}
|
||||||
|
if (item.file_size) return formatBytes(item.file_size);
|
||||||
|
return item.health_status || item.status || 'READY';
|
||||||
|
}
|
||||||
|
|
||||||
function stateClass(state) {
|
function stateClass(state) {
|
||||||
if (state === 'ready') return 'ready';
|
if (state === 'ready') return 'ready';
|
||||||
if (state === 'planned') return 'planned';
|
if (state === 'planned') return 'planned';
|
||||||
@@ -125,6 +198,8 @@ export default function ResultExtractionPanel({ readOnly = false }) {
|
|||||||
const [activeChannel, setActiveChannel] = useState('dinsar');
|
const [activeChannel, setActiveChannel] = useState('dinsar');
|
||||||
const [dinsarPayload, setDinsarPayload] = useState({ items: [], total: 0 });
|
const [dinsarPayload, setDinsarPayload] = useState({ items: [], total: 0 });
|
||||||
const [sbasPayload, setSbasPayload] = useState({ items: [], total: 0 });
|
const [sbasPayload, setSbasPayload] = useState({ items: [], total: 0 });
|
||||||
|
const [lt1Payload, setLt1Payload] = useState({ items: [], total: 0 });
|
||||||
|
const [gf3Payload, setGf3Payload] = useState({ items: [], total: 0 });
|
||||||
const [deliveriesPayload, setDeliveriesPayload] = useState({ items: [], total: 0 });
|
const [deliveriesPayload, setDeliveriesPayload] = useState({ items: [], total: 0 });
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [deliveryLoading, setDeliveryLoading] = useState(false);
|
const [deliveryLoading, setDeliveryLoading] = useState(false);
|
||||||
@@ -161,13 +236,21 @@ export default function ResultExtractionPanel({ readOnly = false }) {
|
|||||||
listSbasInsarProducts({ limit: 30, offset: 0 }),
|
listSbasInsarProducts({ limit: 30, offset: 0 }),
|
||||||
listResultDeliveries({ mine: true, limit: 20, offset: 0 }),
|
listResultDeliveries({ mine: true, limit: 20, offset: 0 }),
|
||||||
]);
|
]);
|
||||||
|
const [lt1Data, gf3Data] = await Promise.all([
|
||||||
|
listResultDeliveryCatalog('lt1_ortho', { limit: PAGE_SIZE, offset: 0 }),
|
||||||
|
listResultDeliveryCatalog('gf3_ortho', { limit: PAGE_SIZE, offset: 0 }),
|
||||||
|
]);
|
||||||
const dinsarItems = normalizeItems(dinsarData);
|
const dinsarItems = normalizeItems(dinsarData);
|
||||||
setDinsarPayload({ ...dinsarData, items: dinsarItems, total: extractTotal(dinsarData, dinsarItems.length) });
|
setDinsarPayload({ ...dinsarData, items: dinsarItems, total: extractTotal(dinsarData, dinsarItems.length) });
|
||||||
const sbasItems = normalizeItems(sbasData);
|
const sbasItems = normalizeItems(sbasData);
|
||||||
setSbasPayload({ ...sbasData, items: sbasItems, total: extractTotal(sbasData, sbasItems.length) });
|
setSbasPayload({ ...sbasData, items: sbasItems, total: extractTotal(sbasData, sbasItems.length) });
|
||||||
|
const lt1Items = normalizeItems(lt1Data);
|
||||||
|
setLt1Payload({ ...lt1Data, items: lt1Items, total: extractTotal(lt1Data, lt1Items.length) });
|
||||||
|
const gf3Items = normalizeItems(gf3Data);
|
||||||
|
setGf3Payload({ ...gf3Data, items: gf3Items, total: extractTotal(gf3Data, gf3Items.length) });
|
||||||
const deliveryItems = normalizeItems(deliveryData);
|
const deliveryItems = normalizeItems(deliveryData);
|
||||||
setDeliveriesPayload({ ...deliveryData, items: deliveryItems, total: extractTotal(deliveryData, deliveryItems.length) });
|
setDeliveriesPayload({ ...deliveryData, items: deliveryItems, total: extractTotal(deliveryData, deliveryItems.length) });
|
||||||
setSelectedIds(new Set(dinsarItems.map(item => item.id).filter(id => id !== undefined && id !== null)));
|
setSelectedIds(new Set(dinsarItems.map(item => resultItemId(item)).filter(id => id !== undefined && id !== null).map(id => selectionKey('dinsar', id))));
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
setError(err?.response?.data?.detail || err.message || '结果目录加载失败');
|
setError(err?.response?.data?.detail || err.message || '结果目录加载失败');
|
||||||
} finally {
|
} finally {
|
||||||
@@ -191,41 +274,63 @@ export default function ResultExtractionPanel({ readOnly = false }) {
|
|||||||
return () => window.clearInterval(timer);
|
return () => window.clearInterval(timer);
|
||||||
}, [deliveriesPayload.items]);
|
}, [deliveriesPayload.items]);
|
||||||
|
|
||||||
const filteredDinsar = useMemo(() => {
|
const activePayload = activeChannel === 'lt1_ortho'
|
||||||
|
? lt1Payload
|
||||||
|
: activeChannel === 'gf3_ortho'
|
||||||
|
? gf3Payload
|
||||||
|
: activeChannel === 'sbas'
|
||||||
|
? sbasPayload
|
||||||
|
: activeChannel === 's1_ortho'
|
||||||
|
? { items: [], total: 0 }
|
||||||
|
: dinsarPayload;
|
||||||
|
const activeItems = activePayload.items || [];
|
||||||
|
|
||||||
|
const filteredResults = useMemo(() => {
|
||||||
const value = query.trim().toLowerCase();
|
const value = query.trim().toLowerCase();
|
||||||
const items = dinsarPayload.items || [];
|
const items = activeItems;
|
||||||
if (!value) return items;
|
if (!value) return items;
|
||||||
return items.filter(item => {
|
return items.filter(item => {
|
||||||
const haystack = [
|
const haystack = [
|
||||||
|
item.display_name,
|
||||||
item.name,
|
item.name,
|
||||||
item.task_name,
|
item.task_name,
|
||||||
item.task_alias,
|
item.task_alias,
|
||||||
item.pair_key,
|
item.pair_key,
|
||||||
item.product_id,
|
item.product_id,
|
||||||
item.engine_code,
|
item.engine_code,
|
||||||
|
item.profile_code,
|
||||||
|
item.imaging_date,
|
||||||
|
item.polarization,
|
||||||
item.file_path,
|
item.file_path,
|
||||||
|
item.primary_asset_path,
|
||||||
|
item.publish_dir,
|
||||||
].filter(Boolean).join(' ').toLowerCase();
|
].filter(Boolean).join(' ').toLowerCase();
|
||||||
return haystack.includes(value);
|
return haystack.includes(value);
|
||||||
});
|
});
|
||||||
}, [dinsarPayload.items, query]);
|
}, [activeItems, query]);
|
||||||
|
|
||||||
const filteredIds = useMemo(
|
const filteredIds = useMemo(
|
||||||
() => filteredDinsar.map(item => item.id).filter(id => id !== undefined && id !== null),
|
() => filteredResults.map(item => resultItemId(item)).filter(id => id !== undefined && id !== null),
|
||||||
[filteredDinsar],
|
[filteredResults],
|
||||||
);
|
);
|
||||||
|
|
||||||
const selectedCountInView = filteredIds.filter(id => selectedIds.has(id)).length;
|
const filteredKeys = useMemo(
|
||||||
|
() => filteredIds.map(id => selectionKey(activeChannel, id)),
|
||||||
|
[activeChannel, filteredIds],
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedCountInView = filteredKeys.filter(key => selectedIds.has(key)).length;
|
||||||
const allVisibleSelected = filteredIds.length > 0 && selectedCountInView === filteredIds.length;
|
const allVisibleSelected = filteredIds.length > 0 && selectedCountInView === filteredIds.length;
|
||||||
const latestDelivery = deliveriesPayload.items?.[0] || null;
|
const latestDelivery = deliveriesPayload.items?.[0] || null;
|
||||||
|
|
||||||
const orthoPlaceholderCount = PRODUCT_CHANNELS.filter(channel => channel.group === '正射成果').length;
|
const readyOrthoCount = Number(lt1Payload.total || 0) + Number(gf3Payload.total || 0);
|
||||||
const currentCatalogTotal = Number(dinsarPayload.total || 0) + Number(sbasPayload.total || 0);
|
const currentCatalogTotal = Number(dinsarPayload.total || 0) + Number(sbasPayload.total || 0) + readyOrthoCount;
|
||||||
|
|
||||||
const metrics = [
|
const metrics = [
|
||||||
{
|
{
|
||||||
label: 'D-InSAR 可交付',
|
label: channelMetricLabel(activeChannel),
|
||||||
value: dinsarPayload.total,
|
value: activePayload.total,
|
||||||
note: `当前载入 ${filteredDinsar.length}/${dinsarPayload.items.length} 条`,
|
note: `当前载入 ${filteredResults.length}/${activeItems.length} 条`,
|
||||||
tone: 'primary',
|
tone: 'primary',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -237,24 +342,25 @@ export default function ResultExtractionPanel({ readOnly = false }) {
|
|||||||
{
|
{
|
||||||
label: '当前接入目录',
|
label: '当前接入目录',
|
||||||
value: currentCatalogTotal,
|
value: currentCatalogTotal,
|
||||||
note: 'D-InSAR + SBAS 已接入清单',
|
note: 'D-InSAR + SBAS + LT-1/GF3 正射',
|
||||||
tone: 'neutral',
|
tone: 'neutral',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
label: '正射通道',
|
label: '可交付正射',
|
||||||
value: orthoPlaceholderCount,
|
value: readyOrthoCount,
|
||||||
note: 'LT-1 / S1 / GF3 占位',
|
note: 'LT-1 与 GF3 已接入,S1 预留',
|
||||||
tone: 'warning',
|
tone: 'neutral',
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const toggleOne = (id) => {
|
const toggleOne = (id) => {
|
||||||
|
const key = selectionKey(activeChannel, id);
|
||||||
setSelectedIds(prev => {
|
setSelectedIds(prev => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
if (next.has(id)) {
|
if (next.has(key)) {
|
||||||
next.delete(id);
|
next.delete(key);
|
||||||
} else {
|
} else {
|
||||||
next.add(id);
|
next.add(key);
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
@@ -264,30 +370,35 @@ export default function ResultExtractionPanel({ readOnly = false }) {
|
|||||||
setSelectedIds(prev => {
|
setSelectedIds(prev => {
|
||||||
const next = new Set(prev);
|
const next = new Set(prev);
|
||||||
if (allVisibleSelected) {
|
if (allVisibleSelected) {
|
||||||
filteredIds.forEach(id => next.delete(id));
|
filteredKeys.forEach(key => next.delete(key));
|
||||||
} else {
|
} else {
|
||||||
filteredIds.forEach(id => next.add(id));
|
filteredKeys.forEach(key => next.add(key));
|
||||||
}
|
}
|
||||||
return next;
|
return next;
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCreateDelivery = async () => {
|
const handleCreateDelivery = async () => {
|
||||||
const ids = [...selectedIds].filter(id => filteredIds.includes(id));
|
const ids = filteredIds.filter(id => selectedIds.has(selectionKey(activeChannel, id)));
|
||||||
if (ids.length === 0) {
|
if (ids.length === 0) {
|
||||||
setCreateError('请至少选择一条 D-InSAR 结果。');
|
setCreateError(`请至少选择一条${selectedChannel.label}。`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setCreating(true);
|
setCreating(true);
|
||||||
setCreateError('');
|
setCreateError('');
|
||||||
setCreateResult(null);
|
setCreateResult(null);
|
||||||
try {
|
try {
|
||||||
const response = await createResultDelivery({
|
const payload = {
|
||||||
channel: 'dinsar',
|
channel: activeChannel,
|
||||||
compat_result_ids: ids,
|
|
||||||
package_mode: packageMode,
|
package_mode: packageMode,
|
||||||
include_checksums: includeChecksums,
|
include_checksums: includeChecksums,
|
||||||
});
|
};
|
||||||
|
if (activeChannel === 'dinsar') {
|
||||||
|
payload.compat_result_ids = ids;
|
||||||
|
} else {
|
||||||
|
payload.item_ids = ids;
|
||||||
|
}
|
||||||
|
const response = await createResultDelivery(payload);
|
||||||
setCreateResult(response);
|
setCreateResult(response);
|
||||||
await loadDeliveries();
|
await loadDeliveries();
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@@ -353,12 +464,12 @@ export default function ResultExtractionPanel({ readOnly = false }) {
|
|||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
|
|
||||||
const renderDinsarWorkspace = () => (
|
const renderReadyWorkspace = () => (
|
||||||
<section className="result-extraction-main-card">
|
<section className="result-extraction-main-card">
|
||||||
<div className="result-extraction-card-head">
|
<div className="result-extraction-card-head">
|
||||||
<div>
|
<div>
|
||||||
<span>D-InSAR 成果交付</span>
|
<span>{channelCreateTitle(activeChannel)}</span>
|
||||||
<strong>选择已登记结果并生成下载包</strong>
|
<strong>{channelCreateSubtitle(activeChannel)}</strong>
|
||||||
</div>
|
</div>
|
||||||
<button type="button" onClick={loadCatalogs} disabled={loading || creating}>
|
<button type="button" onClick={loadCatalogs} disabled={loading || creating}>
|
||||||
刷新目录
|
刷新目录
|
||||||
@@ -372,7 +483,7 @@ export default function ResultExtractionPanel({ readOnly = false }) {
|
|||||||
type="text"
|
type="text"
|
||||||
value={query}
|
value={query}
|
||||||
onChange={event => setQuery(event.target.value)}
|
onChange={event => setQuery(event.target.value)}
|
||||||
placeholder="任务名、日期、pair_key、引擎"
|
placeholder="任务名、日期、产品号、极化、引擎"
|
||||||
disabled={loading || creating}
|
disabled={loading || creating}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
@@ -424,36 +535,34 @@ export default function ResultExtractionPanel({ readOnly = false }) {
|
|||||||
<div className="result-extraction-workspace-split">
|
<div className="result-extraction-workspace-split">
|
||||||
<div className="result-extraction-result-column">
|
<div className="result-extraction-result-column">
|
||||||
<div className="result-extraction-list-head">
|
<div className="result-extraction-list-head">
|
||||||
<span>结果列表</span>
|
<span>{channelListTitle(activeChannel)}</span>
|
||||||
<strong>{selectedCountInView}/{filteredDinsar.length}</strong>
|
<strong>{selectedCountInView}/{filteredResults.length}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div className="result-extraction-result-list">
|
<div className="result-extraction-result-list">
|
||||||
{loading ? (
|
{loading ? (
|
||||||
<div className="result-extraction-empty">正在加载成果目录...</div>
|
<div className="result-extraction-empty">正在加载成果目录...</div>
|
||||||
) : filteredDinsar.length === 0 ? (
|
) : filteredResults.length === 0 ? (
|
||||||
<div className="result-extraction-empty">当前条件下没有可交付的 D-InSAR 结果。</div>
|
<div className="result-extraction-empty">{channelEmptyText(activeChannel)}</div>
|
||||||
) : (
|
) : (
|
||||||
filteredDinsar.map(result => {
|
filteredResults.map(result => {
|
||||||
const id = result.id;
|
const id = resultItemId(result);
|
||||||
const engineMeta = getDinsarEngineMeta(result.engine_code);
|
const engineMeta = activeChannel === 'dinsar'
|
||||||
|
? getDinsarEngineMeta(result.engine_code)
|
||||||
|
: { tone: 'landsar', shortLabel: result.engine_code || result.profile_code || 'ORTHO' };
|
||||||
return (
|
return (
|
||||||
<label key={id} className="result-extraction-result-row">
|
<label key={`${activeChannel}-${id}`} className="result-extraction-result-row">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={selectedIds.has(id)}
|
checked={selectedIds.has(selectionKey(activeChannel, id))}
|
||||||
onChange={() => toggleOne(id)}
|
onChange={() => toggleOne(id)}
|
||||||
disabled={creating}
|
disabled={creating}
|
||||||
/>
|
/>
|
||||||
<span className="result-extraction-result-main">
|
<span className="result-extraction-result-main">
|
||||||
<strong title={result.file_path || resultDisplayName(result)}>{resultDisplayName(result)}</strong>
|
<strong title={result.file_path || result.primary_asset_path || resultDisplayName(result)}>{resultDisplayName(result)}</strong>
|
||||||
<span>
|
<span>{itemMetaText(result, activeChannel)}</span>
|
||||||
{resultDateText(result)}
|
|
||||||
{' · '}
|
|
||||||
{result.pair_key || result.product_id || '-'}
|
|
||||||
</span>
|
|
||||||
</span>
|
</span>
|
||||||
<span className={`dinsar-engine-badge tone-${engineMeta.tone}`}>{engineMeta.shortLabel}</span>
|
<span className={`dinsar-engine-badge tone-${engineMeta.tone}`}>{engineMeta.shortLabel}</span>
|
||||||
<span className="result-extraction-status-chip">{result.is_cached ? '预览就绪' : '预览待建'}</span>
|
<span className="result-extraction-status-chip">{itemStatusText(result, activeChannel)}</span>
|
||||||
</label>
|
</label>
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
@@ -480,7 +589,7 @@ export default function ResultExtractionPanel({ readOnly = false }) {
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>交付接口</span>
|
<span>交付接口</span>
|
||||||
<strong>{selectedChannel.key === 'dinsar' ? '已接入' : '待实现'}</strong>
|
<strong>{selectedChannel.state === 'ready' ? '已接入' : '待实现'}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>用户权限</span>
|
<span>用户权限</span>
|
||||||
@@ -513,13 +622,14 @@ export default function ResultExtractionPanel({ readOnly = false }) {
|
|||||||
<span>成果交付出口</span>
|
<span>成果交付出口</span>
|
||||||
<strong>结果提取工作台</strong>
|
<strong>结果提取工作台</strong>
|
||||||
<p>
|
<p>
|
||||||
将 D-InSAR 成果交付下载和后续正射成果交付统一管理。当前 D-InSAR 支持后台交付包,
|
将 D-InSAR、LT-1 正射和 GF3 正射成果交付下载统一管理。后台生成受控交付包,用户完成后下载到本地;
|
||||||
SBAS、LT-1 正射、Sentinel-1 正射和 GF3 正射先保留清晰占位。
|
SBAS 和 Sentinel-1 正射保留清晰接入状态。
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="result-extraction-hero-meta">
|
<div className="result-extraction-hero-meta">
|
||||||
<span>D-InSAR {formatNumber(dinsarPayload.total)}</span>
|
<span>D-InSAR {formatNumber(dinsarPayload.total)}</span>
|
||||||
<span>SBAS {formatNumber(sbasPayload.total)}</span>
|
<span>LT-1 {formatNumber(lt1Payload.total)}</span>
|
||||||
|
<span>GF3 {formatNumber(gf3Payload.total)}</span>
|
||||||
<span>{readOnly ? '自助交付' : '管理员'}</span>
|
<span>{readOnly ? '自助交付' : '管理员'}</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -549,7 +659,7 @@ export default function ResultExtractionPanel({ readOnly = false }) {
|
|||||||
</button>
|
</button>
|
||||||
))}
|
))}
|
||||||
</aside>
|
</aside>
|
||||||
{activeChannel === 'dinsar' ? renderDinsarWorkspace() : renderPlaceholderWorkspace()}
|
{selectedChannel.state === 'ready' ? renderReadyWorkspace() : renderPlaceholderWorkspace()}
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ export const createResultDelivery = payload =>
|
|||||||
export const listResultDeliveries = (params = {}) =>
|
export const listResultDeliveries = (params = {}) =>
|
||||||
apiClient.get('/result-deliveries', { params }).then(r => r.data);
|
apiClient.get('/result-deliveries', { params }).then(r => r.data);
|
||||||
|
|
||||||
|
export const listResultDeliveryCatalog = (channel, params = {}) =>
|
||||||
|
apiClient.get(`/result-deliveries/catalog/${encodeURIComponent(channel)}`, { params }).then(r => r.data);
|
||||||
|
|
||||||
export const getResultDelivery = deliveryId =>
|
export const getResultDelivery = deliveryId =>
|
||||||
apiClient.get(`/result-deliveries/${encodeURIComponent(deliveryId)}`).then(r => r.data);
|
apiClient.get(`/result-deliveries/${encodeURIComponent(deliveryId)}`).then(r => r.data);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user