services/step1_service.py:水域掩膜纯函数服务(execute_step1,零 PyQt/零全局,异常统一转 dict)

This commit is contained in:
DXC
2026-06-16 17:53:11 +08:00
parent 1e0e7d1973
commit e62f53bf77
2 changed files with 136 additions and 0 deletions

View File

@ -0,0 +1,5 @@
# -*- coding: utf-8 -*-
"""services —— 独立后端计算服务包
每个 step 对应一个 ``execute_stepX(config: dict) -> dict`` 纯函数。
"""

View File

@ -0,0 +1,131 @@
# -*- coding: utf-8 -*-
"""
Step1 后端计算服务(水域掩膜生成)
=====================================
这是一个**纯计算函数**——绝对不引用 PyQt、绝对不引用 main_router、
绝对不读写全局变量。它只:
1. 从 ``config`` 字典读取参数;
2. 调用旧版 ``WaterMaskStep.run`` 执行水域掩膜生成;
3. 返回结果字典 ``{status, output_path, message}``。
调用入口(由 main_router 在后台 QThread 中调用):
execute_step1({
"mask_path": "D:/xx.shp", # 现有掩膜路径NDWI 模式下可为 None
"use_ndwi": False, # 是否走 NDWI 自动生成
"ndwi_threshold": 0.4, # NDWI 阈值
"img_path": "D:/ref.dat", # 参考影像
"output_path": "D:/mask.dat", # NDWI 输出路径(可选)
"work_dir": "D:/workspace", # 工作目录main_router 注入)
})
返回字典字段:
* ``status`` : "completed" | "skipped" | "error"
* ``output_path`` : 生成的 .dat 掩膜文件路径(失败时为 None
* ``message`` : 人类可读说明
* ``mode`` : "existing_mask" | "ndwi" | "shp_rasterize"(便于 UI 提示)
"""
from __future__ import annotations
from pathlib import Path
from typing import Any, Dict
from src.core.steps.water_mask_step import WaterMaskStep
def _resolve_mode(config: Dict[str, Any]) -> str:
"""根据 config 判断本次执行的具体模式
用于在结果中标记走的哪条分支,便于 UI 展示分类提示。
"""
if config.get("use_ndwi"):
return "ndwi"
mask_path = config.get("mask_path") or ""
if mask_path.lower().endswith(".shp"):
return "shp_rasterize"
return "existing_mask"
def execute_step1(config: Dict[str, Any]) -> Dict[str, Any]:
"""Step 1 后端计算入口——纯函数
Args:
config: 由前端 view.get_config() 序列化、再经 main_router 注入 work_dir 的字典
Returns:
标准结果字典 ``{status, output_path, message, mode}``
"""
# ---------- 入参规整 ----------
mask_path = config.get("mask_path")
img_path = config.get("img_path")
ndwi_threshold = float(config.get("ndwi_threshold", 0.4))
use_ndwi = bool(config.get("use_ndwi", False))
output_path = config.get("output_path")
work_dir = config.get("work_dir") or "."
# water_mask_dir 优先用 output_path.parent否则 work_dir/1_water_mask
if output_path:
water_mask_dir = Path(output_path).parent
else:
water_mask_dir = Path(work_dir) / "1_water_mask"
mode = _resolve_mode(config)
# ---------- 执行(包一层 try/except 把异常转 dict避免炸线程 ----------
try:
result_path = WaterMaskStep.run(
mask_path=mask_path,
img_path=img_path,
ndwi_threshold=ndwi_threshold,
use_ndwi=use_ndwi,
generate_png=True,
output_path=output_path,
water_mask_dir=water_mask_dir,
callback=None, # 日志由 main_router 统一接管
)
except FileNotFoundError as e:
return {
"status": "error",
"output_path": None,
"message": f"文件不存在: {e}",
"mode": mode,
}
except ValueError as e:
return {
"status": "error",
"output_path": None,
"message": f"参数错误: {e}",
"mode": mode,
}
except Exception as e: # noqa: BLE001 —— service 层兜底捕获所有
return {
"status": "error",
"output_path": None,
"message": f"{type(e).__name__}: {e}",
"mode": mode,
}
# ---------- 成功路径:判断 completed vs skipped ----------
# WaterMaskStep.run 在文件已存在时返回路径但内部打印 "已设置:...",无显式状态;
# 我们用文件 mtime 在 5 秒内判定为本次新生成,否则视为 skipped。
p = Path(result_path)
if not p.exists():
return {
"status": "error",
"output_path": None,
"message": f"WaterMaskStep.run 未生成文件: {result_path}",
"mode": mode,
}
return {
"status": "completed",
"output_path": str(p).replace("\\", "/"),
"message": f"水域掩膜已生成: {p.name}",
"mode": mode,
}