Pārlūkot izejas kodu

补充输出通量计算和绘图,地下渗漏和地表径流绘图

yangtaodemon 1 dienu atpakaļ
vecāks
revīzija
f557935ecf

+ 1 - 0
.gitignore

@@ -13,3 +13,4 @@ myenv/
 .vscode/
 *.log
 Cd_Prediction_Integrated_System/output/raster/meanTemp.tif.aux.xml
+/app/static/

+ 202 - 20
app/api/cd_flux.py

@@ -12,12 +12,19 @@ from fastapi.responses import FileResponse, JSONResponse
 
 from app.services.cd_flux_service import FluxCdVisualizationService
 
+# 固定文件名
+DEFAULT_INPUT_MAP_FILENAME = "fluxcd_input_map.jpg"
+DEFAULT_INPUT_HIST_FILENAME = "fluxcd_input_histogram.jpg"
+DEFAULT_INPUT_CSV_FILENAME = "fluxcd_input.csv"
 
+DEFAULT_OUTPUT_MAP_FILENAME = "fluxcd_output_map.jpg"
+DEFAULT_OUTPUT_HIST_FILENAME = "fluxcd_output_histogram.jpg"
+DEFAULT_OUTPUT_CSV_FILENAME = "fluxcd_output.csv"
+
+DEFAULT_NET_MAP_FILENAME = "fluxcd_net_map.jpg"
+DEFAULT_NET_HIST_FILENAME = "fluxcd_net_histogram.jpg"
+DEFAULT_NET_CSV_FILENAME = "fluxcd_net.csv"
 
-# 固定文件名
-DEFAULT_MAP_FILENAME = "fluxcd_input_map.jpg"
-DEFAULT_HIST_FILENAME = "fluxcd_input_histogram.jpg"
-DEFAULT_CSV_FILENAME = "fluxcd_input.csv"
 LATEST_RESULT_FILENAME = "latest_result.json"
 # 配置日志 - 避免重复日志输出
 logger = logging.getLogger(__name__)
@@ -93,12 +100,36 @@ async def update_and_generate_fluxcd_visualization(
         static_dir = get_static_dir()
         os.makedirs(static_dir, exist_ok=True)
 
-        generation_result = service.generate_cd_input_flux_map(
+        # 生成三种通量结果
+        input_result = service.generate_cd_input_flux_map(
+            output_dir=static_dir,
+            boundary_shp=boundary_shp
+        )
+
+        output_result = service.generate_cd_output_flux_map(
+            output_dir=static_dir,
+            boundary_shp=boundary_shp
+        )
+
+        net_result = service.generate_cd_net_flux_map(
             output_dir=static_dir,
             boundary_shp=boundary_shp
         )
 
-        return generation_result
+        # 保存最新结果信息
+        latest_result_path = os.path.join(static_dir, LATEST_RESULT_FILENAME)
+        with open(latest_result_path, "w", encoding="utf-8") as f:
+            json.dump({
+                "input": input_result,
+                "output": output_result,
+                "net": net_result
+            }, f, ensure_ascii=False, indent=2)
+
+        return {
+            "input": input_result,
+            "output": output_result,
+            "net": net_result
+        }
 
     except HTTPException as he:
         raise he
@@ -110,13 +141,13 @@ async def update_and_generate_fluxcd_visualization(
         background_tasks.add_task(shutil.rmtree, temp_dir, ignore_errors=True)
 
 
-@router.get("/map",
+@router.get("/input/map",
             summary="获取Cd输入通量空间分布图",
             description="返回默认的Cd输入通量空间分布图")
 async def get_fluxcd_map() -> FileResponse:
     try:
         # 直接返回静态目录中的默认地图
-        map_path = get_default_file_path("fluxcd_input_map.jpg")
+        map_path = get_default_file_path(DEFAULT_INPUT_MAP_FILENAME)
         if os.path.exists(map_path):
             return FileResponse(map_path, media_type="image/jpeg")
         else:
@@ -126,13 +157,45 @@ async def get_fluxcd_map() -> FileResponse:
         raise HTTPException(status_code=500, detail=f"获取地图失败: {str(e)}")
 
 
-@router.get("/histogram",
+@router.get("/output/map",
+            summary="获取Cd输出通量空间分布图",
+            description="返回默认的Cd输出通量空间分布图")
+async def get_fluxcd_output_map() -> FileResponse:
+    try:
+        # 直接返回静态目录中的默认地图
+        map_path = get_default_file_path(DEFAULT_OUTPUT_MAP_FILENAME)
+        if os.path.exists(map_path):
+            return FileResponse(map_path, media_type="image/jpeg")
+        else:
+            raise HTTPException(status_code=404, detail="默认输出通量地图不存在,请先生成")
+    except Exception as e:
+        logger.error(f"获取输出通量地图失败: {str(e)}")
+        raise HTTPException(status_code=500, detail=f"获取输出通量地图失败: {str(e)}")
+
+
+@router.get("/net/map",
+            summary="获取Cd净通量空间分布图",
+            description="返回默认的Cd净通量空间分布图")
+async def get_fluxcd_net_map() -> FileResponse:
+    try:
+        # 直接返回静态目录中的默认地图
+        map_path = get_default_file_path(DEFAULT_NET_MAP_FILENAME)
+        if os.path.exists(map_path):
+            return FileResponse(map_path, media_type="image/jpeg")
+        else:
+            raise HTTPException(status_code=404, detail="默认净通量地图不存在,请先生成")
+    except Exception as e:
+        logger.error(f"获取净通量地图失败: {str(e)}")
+        raise HTTPException(status_code=500, detail=f"获取净通量地图失败: {str(e)}")
+
+
+@router.get("/input/histogram",
             summary="获取Cd输入通量直方图",
             description="返回默认的Cd输入通量直方图")
 async def get_fluxcd_histogram() -> FileResponse:
     try:
         # 直接返回静态目录中的默认直方图
-        hist_path = get_default_file_path("fluxcd_input_histogram.jpg")
+        hist_path = get_default_file_path(DEFAULT_INPUT_HIST_FILENAME)
         if os.path.exists(hist_path):
             return FileResponse(hist_path, media_type="image/jpeg")
         else:
@@ -142,10 +205,39 @@ async def get_fluxcd_histogram() -> FileResponse:
         raise HTTPException(status_code=500, detail=f"获取直方图失败: {str(e)}")
 
 
-@router.get("/statistics",
-            summary="获取Cd输入通量统计信息",
-            description="返回默认的Cd输入通量统计信息")
-@router.get("/statistics",
+@router.get("/output/histogram",
+            summary="获取Cd输出通量直方图",
+            description="返回默认的Cd输出通量直方图")
+async def get_fluxcd_output_histogram() -> FileResponse:
+    try:
+        # 直接返回静态目录中的默认直方图
+        hist_path = get_default_file_path(DEFAULT_OUTPUT_HIST_FILENAME)
+        if os.path.exists(hist_path):
+            return FileResponse(hist_path, media_type="image/jpeg")
+        else:
+            raise HTTPException(status_code=404, detail="默认输出通量直方图不存在,请先生成")
+    except Exception as e:
+        logger.error(f"获取输出通量直方图失败: {str(e)}")
+        raise HTTPException(status_code=500, detail=f"获取输出通量直方图失败: {str(e)}")
+
+
+@router.get("/net/histogram",
+            summary="获取Cd净通量直方图",
+            description="返回默认的Cd净通量直方图")
+async def get_fluxcd_net_histogram() -> FileResponse:
+    try:
+        # 直接返回静态目录中的默认直方图
+        hist_path = get_default_file_path(DEFAULT_NET_HIST_FILENAME)
+        if os.path.exists(hist_path):
+            return FileResponse(hist_path, media_type="image/jpeg")
+        else:
+            raise HTTPException(status_code=404, detail="默认净通量直方图不存在,请先生成")
+    except Exception as e:
+        logger.error(f"获取净通量直方图失败: {str(e)}")
+        raise HTTPException(status_code=500, detail=f"获取净通量直方图失败: {str(e)}")
+
+
+@router.get("/input/statistics",
             summary="获取Cd输入通量统计信息",
             description="返回最新生成的Cd输入通量统计信息")
 async def get_fluxcd_statistics() -> JSONResponse:
@@ -157,10 +249,10 @@ async def get_fluxcd_statistics() -> JSONResponse:
         with open(latest_result_path, "r", encoding="utf-8") as f:
             result = json.load(f)
 
-        if not result.get("success"):
-            raise HTTPException(status_code=404, detail="结果无效")
+        if not result.get("input") or not result["input"].get("success"):
+            raise HTTPException(status_code=404, detail="输入通量结果无效")
 
-        stats = result.get("data", {}).get("statistics")
+        stats = result["input"].get("data", {}).get("statistics")
         if stats:
             return JSONResponse(content=stats)
         else:
@@ -170,13 +262,63 @@ async def get_fluxcd_statistics() -> JSONResponse:
         raise HTTPException(status_code=500, detail=f"获取统计信息失败: {str(e)}")
 
 
-@router.get("/export-csv",
+@router.get("/output/statistics",
+            summary="获取Cd输出通量统计信息",
+            description="返回最新生成的Cd输出通量统计信息")
+async def get_fluxcd_output_statistics() -> JSONResponse:
+    try:
+        latest_result_path = os.path.join(STATIC_DIR, LATEST_RESULT_FILENAME)
+        if not os.path.exists(latest_result_path):
+            raise HTTPException(status_code=404, detail="无可用统计信息,请先生成")
+
+        with open(latest_result_path, "r", encoding="utf-8") as f:
+            result = json.load(f)
+
+        if not result.get("output") or not result["output"].get("success"):
+            raise HTTPException(status_code=404, detail="输出通量结果无效")
+
+        stats = result["output"].get("data", {}).get("statistics")
+        if stats:
+            return JSONResponse(content=stats)
+        else:
+            raise HTTPException(status_code=404, detail="输出通量统计信息不存在")
+    except Exception as e:
+        logger.error(f"获取输出通量统计信息失败: {str(e)}")
+        raise HTTPException(status_code=500, detail=f"获取输出通量统计信息失败: {str(e)}")
+
+
+@router.get("/net/statistics",
+            summary="获取Cd净通量统计信息",
+            description="返回最新生成的Cd净通量统计信息")
+async def get_fluxcd_net_statistics() -> JSONResponse:
+    try:
+        latest_result_path = os.path.join(STATIC_DIR, LATEST_RESULT_FILENAME)
+        if not os.path.exists(latest_result_path):
+            raise HTTPException(status_code=404, detail="无可用统计信息,请先生成")
+
+        with open(latest_result_path, "r", encoding="utf-8") as f:
+            result = json.load(f)
+
+        if not result.get("net") or not result["net"].get("success"):
+            raise HTTPException(status_code=404, detail="净通量结果无效")
+
+        stats = result["net"].get("data", {}).get("statistics")
+        if stats:
+            return JSONResponse(content=stats)
+        else:
+            raise HTTPException(status_code=404, detail="净通量统计信息不存在")
+    except Exception as e:
+        logger.error(f"获取净通量统计信息失败: {str(e)}")
+        raise HTTPException(status_code=500, detail=f"获取净通量统计信息失败: {str(e)}")
+
+
+@router.get("/input/export-csv",
             summary="导出fluxcd_input.csv文件",
             description="导出默认的fluxcd_input.csv文件")
 async def export_fluxcd_csv() -> FileResponse:
     try:
         # 直接返回静态目录中的CSV文件
-        csv_path = get_default_file_path("fluxcd_input.csv")
+        csv_path = get_default_file_path(DEFAULT_INPUT_CSV_FILENAME)
         if os.path.exists(csv_path):
             return FileResponse(
                 csv_path,
@@ -187,4 +329,44 @@ async def export_fluxcd_csv() -> FileResponse:
             raise HTTPException(status_code=404, detail="CSV文件不存在,请先生成")
     except Exception as e:
         logger.error(f"导出CSV失败: {str(e)}")
-        raise HTTPException(status_code=500, detail=f"导出CSV失败: {str(e)}")
+        raise HTTPException(status_code=500, detail=f"导出CSV失败: {str(e)}")
+
+
+@router.get("/output/export-csv",
+            summary="导出fluxcd_output.csv文件",
+            description="导出默认的fluxcd_output.csv文件")
+async def export_fluxcd_output_csv() -> FileResponse:
+    try:
+        # 直接返回静态目录中的CSV文件
+        csv_path = get_default_file_path(DEFAULT_OUTPUT_CSV_FILENAME)
+        if os.path.exists(csv_path):
+            return FileResponse(
+                csv_path,
+                filename="fluxcd_output.csv",
+                media_type="text/csv"
+            )
+        else:
+            raise HTTPException(status_code=404, detail="输出通量CSV文件不存在,请先生成")
+    except Exception as e:
+        logger.error(f"导出输出通量CSV失败: {str(e)}")
+        raise HTTPException(status_code=500, detail=f"导出输出通量CSV失败: {str(e)}")
+
+
+@router.get("/net/export-csv",
+            summary="导出fluxcd_net.csv文件",
+            description="导出默认的fluxcd_net.csv文件")
+async def export_fluxcd_net_csv() -> FileResponse:
+    try:
+        # 直接返回静态目录中的CSV文件
+        csv_path = get_default_file_path(DEFAULT_NET_CSV_FILENAME)
+        if os.path.exists(csv_path):
+            return FileResponse(
+                csv_path,
+                filename="fluxcd_net.csv",
+                media_type="text/csv"
+            )
+        else:
+            raise HTTPException(status_code=404, detail="净通量CSV文件不存在,请先生成")
+    except Exception as e:
+        logger.error(f"导出净通量CSV失败: {str(e)}")
+        raise HTTPException(status_code=500, detail=f"导出净通量CSV失败: {str(e)}")

+ 123 - 1
app/api/cd_flux_removal.py

@@ -351,5 +351,127 @@ async def export_flux_data(
             detail=f"数据导出失败: {str(e)}"
         )
 
+    # 地下渗流通量可视化接口
 
- 
+
+@router.get("/groundwater_leaching/visualize",
+            summary="生成地下渗流Cd通量可视化图表",
+            description="生成地下渗流Cd通量的空间分布图")
+async def visualize_groundwater_leaching(
+        area: str = Query(..., description="地区名称,如:乐昌市"),
+        level: str = Query(..., description="行政层级,必须为: county | city | province"),
+        colormap: str = Query("blues", description="色彩方案"),
+        resolution_factor: float = Query(4.0, description="分辨率因子(默认4.0,更快)"),
+        enable_interpolation: bool = Query(False, description="是否启用空间插值(默认关闭以提升性能)"),
+        cleanup_intermediate: bool = Query(True, description="是否清理中间文件(默认是)")
+):
+    """
+    生成地下渗流Cd通量可视化图表
+
+    @param area: 地区名称
+    @param level: 行政层级
+    @returns: 栅格地图文件
+
+    功能包括:
+    1. 从数据库获取地下渗流Cd通量数据
+    2. 生成指定地区边界的栅格地图
+    3. 直接返回图片文件
+    """
+    try:
+        service = CdFluxRemovalService()
+
+        # 行政层级校验
+        if level not in ("county", "city", "province"):
+            raise HTTPException(status_code=400, detail="参数 level 必须为 'county' | 'city' | 'province'")
+
+        # 创建可视化
+        visualization_files = service.create_groundwater_leaching_visualization(
+            area=area,
+            level=level,
+            colormap=colormap,
+            resolution_factor=resolution_factor,
+            enable_interpolation=enable_interpolation,
+            cleanup_intermediate=cleanup_intermediate
+        )
+
+        # 检查地图文件是否生成成功
+        map_file = visualization_files.get("map")
+        if not map_file or not os.path.exists(map_file):
+            raise HTTPException(status_code=500, detail="地图文件生成失败")
+
+        return FileResponse(
+            path=map_file,
+            filename=f"{area}_groundwater_leaching_cd_flux_map.jpg",
+            media_type="image/jpeg"
+        )
+
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"生成地区 '{area}' 的地下渗流Cd通量可视化失败: {str(e)}")
+        raise HTTPException(
+            status_code=500,
+            detail=f"可视化生成失败: {str(e)}"
+        )
+
+
+# 地表径流通量可视化接口
+@router.get("/surface_runoff/visualize",
+            summary="生成地表径流Cd通量可视化图表",
+            description="生成地表径流Cd通量的空间分布图")
+async def visualize_surface_runoff(
+        area: str = Query(..., description="地区名称,如:乐昌市"),
+        level: str = Query(..., description="行政层级,必须为: county | city | province"),
+        colormap: str = Query("blues", description="色彩方案"),
+        resolution_factor: float = Query(4.0, description="分辨率因子(默认4.0,更快)"),
+        enable_interpolation: bool = Query(False, description="是否启用空间插值(默认关闭以提升性能)"),
+        cleanup_intermediate: bool = Query(True, description="是否清理中间文件(默认是)")
+):
+    """
+    生成地表径流Cd通量可视化图表
+
+    @param area: 地区名称
+    @param level: 行政层级
+    @returns: 栅格地图文件
+
+    功能包括:
+    1. 从数据库获取地表径流Cd通量数据
+    2. 生成指定地区边界的栅格地图
+    3. 直接返回图片文件
+    """
+    try:
+        service = CdFluxRemovalService()
+
+        # 行政层级校验
+        if level not in ("county", "city", "province"):
+            raise HTTPException(status_code=400, detail="参数 level 必须为 'county' | 'city' | 'province'")
+
+        # 创建可视化
+        visualization_files = service.create_surface_runoff_visualization(
+            area=area,
+            level=level,
+            colormap=colormap,
+            resolution_factor=resolution_factor,
+            enable_interpolation=enable_interpolation,
+            cleanup_intermediate=cleanup_intermediate
+        )
+
+        # 检查地图文件是否生成成功
+        map_file = visualization_files.get("map")
+        if not map_file or not os.path.exists(map_file):
+            raise HTTPException(status_code=500, detail="地图文件生成失败")
+
+        return FileResponse(
+            path=map_file,
+            filename=f"{area}_surface_runoff_cd_flux_map.jpg",
+            media_type="image/jpeg"
+        )
+
+    except HTTPException:
+        raise
+    except Exception as e:
+        logger.error(f"生成地区 '{area}' 的地表径流Cd通量可视化失败: {str(e)}")
+        raise HTTPException(
+            status_code=500,
+            detail=f"可视化生成失败: {str(e)}"
+        )

+ 8 - 1
app/models/FluxCd_output.py

@@ -1,5 +1,6 @@
 from sqlalchemy import Column, Integer, Float, ForeignKeyConstraint
 from app.database import Base
+from app.models import FluxCdInputData
 
 
 class FluxCdOutputData(Base):
@@ -31,4 +32,10 @@ class FluxCdOutputData(Base):
             ['Farmland_data.Farmland_ID', 'Farmland_data.Sample_ID']
         ),
         {'comment': '通量镉预测模型输出数据'}
-    )
+    )
+
+    def calculate_fluxes(self, input_data: FluxCdInputData):
+        """根据输入数据计算通量"""
+        self.in_cd = input_data.input_flux()
+        self.out_cd = input_data.output_flux()  # 使用输出通量方法
+        self.net_cd = self.in_cd - self.out_cd

+ 406 - 2
app/services/cd_flux_removal_service.py

@@ -14,6 +14,7 @@ from typing import Dict, Any, List, Optional
 from sqlalchemy.orm import sessionmaker, Session
 from sqlalchemy import create_engine, and_
 from ..database import SessionLocal, engine
+from ..models import FluxCdInputData
 from ..models.parameters import Parameters
 from ..models.CropCd_output import CropCdOutputData
 from ..models.farmland import FarmlandData
@@ -609,5 +610,408 @@ class CdFluxRemovalService:
             self.logger.warning(f"从数据库创建边界文件失败: {str(e)}")
             
         return None
-    
- 
+
+
+    def create_surface_runoff_visualization(self, area: str, level: str,
+                                            output_dir: str = "app/static/surface_runoff",
+                                            template_raster: str = "app/static/cd_flux/meanTemp.tif",
+                                            boundary_shp: str = None,
+                                            colormap: str = "green_yellow_red_purple",
+                                            resolution_factor: float = 4.0,
+                                            enable_interpolation: bool = False,
+                                            cleanup_intermediate: bool = True) -> Dict[str, str]:
+        """
+        创建地表径流Cd通量可视化图表
+
+        @param area: 地区名称
+        @param level: 行政层级
+        @param output_dir: 输出目录
+        @param template_raster: 模板栅格文件路径
+        @param boundary_shp: 边界shapefile路径
+        @param colormap: 色彩方案
+        @param resolution_factor: 分辨率因子
+        @param enable_interpolation: 是否启用空间插值
+        @param cleanup_intermediate: 是否清理中间文件
+        @returns: 生成的图片文件路径字典
+        """
+        try:
+            # 从数据库获取地表径流数据
+            results_with_coords = []
+            with SessionLocal() as db:
+                # 查询地表径流数据(DB_Cd)及对应坐标
+                runoff_data = db.query(
+                    FarmlandData.farmland_id,
+                    FarmlandData.sample_id,
+                    FarmlandData.lon,
+                    FarmlandData.lan,
+                    FluxCdInputData.surface_runoff
+                ).join(
+                    FluxCdInputData,
+                    (FluxCdInputData.farmland_id == FarmlandData.farmland_id) &
+                    (FluxCdInputData.sample_id == FarmlandData.sample_id)
+                ).all()
+
+                if not runoff_data:
+                    self.logger.warning("未找到地表径流数据")
+                    return {}
+
+                for data in runoff_data:
+                    results_with_coords.append({
+                        "farmland_id": data.farmland_id,
+                        "sample_id": data.sample_id,
+                        "longitude": data.lon,
+                        "latitude": data.lan,
+                        "flux_value": data.surface_runoff
+                    })
+
+            # 确保输出目录存在
+            os.makedirs(output_dir, exist_ok=True)
+            generated_files: Dict[str, str] = {}
+
+            # 生成时间戳
+            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+
+            # 创建CSV文件用于绘图
+            csv_filename = f"surface_runoff_{area}_temp_{timestamp}.csv"
+            csv_path = os.path.join(output_dir, csv_filename)
+
+            # 准备绘图数据
+            plot_data = []
+            for result in results_with_coords:
+                plot_data.append({
+                    "longitude": result["longitude"],
+                    "latitude": result["latitude"],
+                    "flux_value": result["flux_value"]
+                })
+
+            # 保存为CSV
+            df = pd.DataFrame(plot_data)
+            df.to_csv(csv_path, index=False, encoding='utf-8-sig')
+
+            # 初始化绘图工具
+            mapper = MappingUtils()
+
+            # 生成输出文件路径
+            map_output = os.path.join(output_dir, f"surface_runoff_{area}_map_{timestamp}")
+            histogram_output = os.path.join(output_dir, f"surface_runoff_{area}_histogram_{timestamp}")
+
+            # 检查模板文件是否存在
+            if not os.path.exists(template_raster):
+                self.logger.warning(f"模板栅格文件不存在: {template_raster}")
+                template_raster = None
+
+            # 动态获取边界数据(严格使用指定层级)
+            if not level:
+                raise ValueError("必须提供行政层级 level:county | city | province")
+
+            # 直接从数据库获取边界GeoDataFrame
+            boundary_gdf = self._get_boundary_gdf_from_database(area, level)
+            boundary_shp = None  # 不再需要临时边界文件
+
+            if boundary_gdf is None:
+                self.logger.warning(f"未找到地区 '{area}' 的边界数据,将不使用边界裁剪")
+            else:
+                # 在绘图前进行样点边界包含性统计
+                try:
+                    if boundary_gdf is not None and len(boundary_gdf) > 0:
+                        boundary_union = boundary_gdf.unary_union
+                        total_points = len(results_with_coords)
+                        inside_count = 0
+                        outside_points: List[Dict[str, Any]] = []
+                        for r in results_with_coords:
+                            pt = Point(float(r["longitude"]), float(r["latitude"]))
+                            if boundary_union.contains(pt) or boundary_union.touches(pt):
+                                inside_count += 1
+                            else:
+                                outside_points.append({
+                                    "farmland_id": r.get("farmland_id"),
+                                    "sample_id": r.get("sample_id"),
+                                    "longitude": r.get("longitude"),
+                                    "latitude": r.get("latitude"),
+                                    "flux_value": r.get("flux_value")
+                                })
+
+                        outside_count = total_points - inside_count
+                        inside_pct = (inside_count / total_points * 100.0) if total_points > 0 else 0.0
+
+                        self.logger.info(
+                            f"样点边界检查 - 总数: {total_points}, 边界内: {inside_count} ({inside_pct:.2f}%), 边界外: {outside_count}")
+                        if outside_count > 0:
+                            sample_preview = outside_points[:10]
+                            self.logger.warning(
+                                f"存在 {outside_count} 个样点位于边界之外,绘图时将被掩膜隐藏。示例(最多10条): {sample_preview}")
+                except Exception as check_err:
+                    self.logger.warning(f"样点边界包含性检查失败: {str(check_err)}")
+
+            # 创建shapefile
+            shapefile_path = csv_path.replace('.csv', '_points.shp')
+            mapper.csv_to_shapefile(csv_path, shapefile_path,
+                                    lon_col='longitude', lat_col='latitude', value_col='flux_value')
+
+            # 合并已生成文件映射
+            generated_files.update({"csv": csv_path, "shapefile": shapefile_path})
+
+            # 如果有模板栅格文件,创建栅格地图
+            if template_raster:
+                try:
+                    # 创建栅格
+                    raster_path = csv_path.replace('.csv', '_raster.tif')
+                    raster_path, stats = mapper.vector_to_raster(
+                        shapefile_path, template_raster, raster_path, 'flux_value',
+                        resolution_factor=resolution_factor, boundary_shp=boundary_shp, boundary_gdf=boundary_gdf,
+                        interpolation_method='nearest', enable_interpolation=enable_interpolation
+                    )
+                    generated_files["raster"] = raster_path
+
+                    # 创建栅格地图 - 使用英文标题避免中文乱码
+                    map_title = "Surface Runoff Cd Flux"
+
+                    map_file = mapper.create_raster_map(
+                        boundary_shp if boundary_shp else None,
+                        raster_path,
+                        map_output,
+                        colormap=colormap,
+                        title=map_title,
+                        output_size=12,
+                        dpi=300,
+                        resolution_factor=4.0,
+                        enable_interpolation=False,
+                        interpolation_method='nearest',
+                        boundary_gdf=boundary_gdf
+                    )
+                    generated_files["map"] = map_file
+
+                    # 创建直方图 - 使用英文标题避免中文乱码
+                    histogram_title = "Surface Runoff Cd Flux Distribution"
+
+                    histogram_file = mapper.create_histogram(
+                        raster_path,
+                        f"{histogram_output}.jpg",
+                        title=histogram_title,
+                        xlabel='Cd Flux (g/ha/a)',
+                        ylabel='Frequency Density'
+                    )
+                    generated_files["histogram"] = histogram_file
+
+                except Exception as viz_error:
+                    self.logger.warning(f"栅格可视化创建失败: {str(viz_error)}")
+                    # 即使栅格可视化失败,也返回已生成的文件
+
+            # 清理中间文件(默认开启,仅保留最终可视化)
+            if cleanup_intermediate:
+                try:
+                    self._cleanup_intermediate_files(generated_files, None)
+                except Exception as cleanup_err:
+                    self.logger.warning(f"中间文件清理失败: {str(cleanup_err)}")
+
+            self.logger.info(f"✓ 成功创建地表径流Cd通量可视化,生成文件: {list(generated_files.keys())}")
+            return generated_files
+
+        except Exception as e:
+            self.logger.error(f"创建地表径流可视化失败: {str(e)}")
+            raise
+
+
+    def get_groundwater_leaching_data(self, area: str) -> List[Dict[str, Any]]:
+        """
+        获取地下渗流(地下水渗漏)通量数据及坐标
+
+        @param area: 地区名称
+        @returns: 包含坐标和通量值的结果列表
+        """
+        try:
+            with SessionLocal() as db:
+                # 查询地下渗流数据及对应坐标
+                leaching_data = db.query(
+                    FluxCdInputData.groundwater_leaching,
+                    FarmlandData.farmland_id,
+                    FarmlandData.sample_id,
+                    FarmlandData.lon,
+                    FarmlandData.lan
+                ).join(
+                    FarmlandData,
+                    and_(
+                        FluxCdInputData.farmland_id == FarmlandData.farmland_id,
+                        FluxCdInputData.sample_id == FarmlandData.sample_id
+                    )
+                ).all()
+
+                if not leaching_data:
+                    self.logger.warning(f"未找到地区 '{area}' 的地下渗流数据")
+                    return []
+
+                results = []
+                for data in leaching_data:
+                    results.append({
+                        "farmland_id": data.farmland_id,
+                        "sample_id": data.sample_id,
+                        "longitude": data.lon,
+                        "latitude": data.lan,
+                        "flux_value": data.groundwater_leaching
+                    })
+
+                self.logger.info(f"成功获取 {len(results)} 个样点的地下渗流数据")
+                return results
+
+        except Exception as e:
+            self.logger.error(f"获取地下渗流数据失败: {str(e)}")
+            raise
+
+
+    def create_groundwater_leaching_visualization(self, area: str, level: str,
+                                                  output_dir: str = "app/static/groundwater_leaching",
+                                                  template_raster: str = "app/static/cd_flux/meanTemp.tif",
+                                                  colormap: str = "blues",
+                                                  resolution_factor: float = 4.0,
+                                                  enable_interpolation: bool = False,
+                                                  cleanup_intermediate: bool = True) -> Dict[str, str]:
+        """
+        创建地下渗流(地下水渗漏)通量可视化图表
+
+        @param area: 地区名称
+        @param level: 行政层级
+        @param output_dir: 输出目录
+        @param template_raster: 模板栅格文件路径
+        @param colormap: 色彩方案
+        @param resolution_factor: 分辨率因子
+        @param enable_interpolation: 是否启用空间插值
+        @param cleanup_intermediate: 是否清理中间文件
+        @returns: 生成的图片文件路径字典
+        """
+        try:
+            # 获取地下渗流数据
+            leaching_results = self.get_groundwater_leaching_data(area)
+
+            if not leaching_results:
+                return {
+                    "success": False,
+                    "message": f"没有找到地区 '{area}' 的地下渗流数据",
+                    "data": None
+                }
+
+            # 确保输出目录存在
+            os.makedirs(output_dir, exist_ok=True)
+            generated_files: Dict[str, str] = {}
+
+            # 生成时间戳
+            timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
+
+            # 创建CSV文件用于绘图
+            csv_filename = f"groundwater_leaching_{area}_temp_{timestamp}.csv"
+            csv_path = os.path.join(output_dir, csv_filename)
+
+            # 准备绘图数据
+            plot_data = []
+            for result in leaching_results:
+                plot_data.append({
+                    "longitude": result["longitude"],
+                    "latitude": result["latitude"],
+                    "flux_value": result["flux_value"]
+                })
+
+            # 保存为CSV
+            df = pd.DataFrame(plot_data)
+            df.to_csv(csv_path, index=False, encoding='utf-8-sig')
+
+            # 初始化绘图工具
+            mapper = MappingUtils()
+
+            # 生成输出文件路径
+            map_output = os.path.join(output_dir, f"groundwater_leaching_{area}_map_{timestamp}")
+            histogram_output = os.path.join(output_dir, f"groundwater_leaching_{area}_histogram_{timestamp}")
+
+            # 检查模板文件是否存在
+            if not os.path.exists(template_raster):
+                self.logger.warning(f"模板栅格文件不存在: {template_raster}")
+                template_raster = None
+
+            # 直接从数据库获取边界GeoDataFrame
+            boundary_gdf = self._get_boundary_gdf_from_database(area, level)
+            boundary_shp = None  # 不再需要临时边界文件
+
+            # 在绘图前进行样点边界包含性统计
+            if boundary_gdf is not None and len(boundary_gdf) > 0:
+                boundary_union = boundary_gdf.unary_union
+                total_points = len(leaching_results)
+                inside_count = 0
+                for r in leaching_results:
+                    pt = Point(float(r["longitude"]), float(r["latitude"]))
+                    if boundary_union.contains(pt) or boundary_union.touches(pt):
+                        inside_count += 1
+
+                outside_count = total_points - inside_count
+                inside_pct = (inside_count / total_points * 100.0) if total_points > 0 else 0.0
+
+                self.logger.info(
+                    f"样点边界检查 - 总数: {total_points}, 边界内: {inside_count} ({inside_pct:.2f}%), 边界外: {outside_count}")
+
+            # 创建shapefile
+            shapefile_path = csv_path.replace('.csv', '_points.shp')
+            mapper.csv_to_shapefile(csv_path, shapefile_path,
+                                    lon_col='longitude', lat_col='latitude', value_col='flux_value')
+
+            # 合并已生成文件映射
+            generated_files.update({"csv": csv_path, "shapefile": shapefile_path})
+
+            # 如果有模板栅格文件,创建栅格地图
+            if template_raster:
+                try:
+                    # 创建栅格
+                    raster_path = csv_path.replace('.csv', '_raster.tif')
+                    raster_path, stats = mapper.vector_to_raster(
+                        shapefile_path, template_raster, raster_path, 'flux_value',
+                        resolution_factor=resolution_factor, boundary_gdf=boundary_gdf,
+                        interpolation_method='nearest', enable_interpolation=enable_interpolation
+                    )
+                    generated_files["raster"] = raster_path
+
+                    # 创建栅格地图 - 使用特定标题
+                    map_title = "Groundwater Leaching Cd Flux"
+
+                    map_file = mapper.create_raster_map(
+                        boundary_shp,
+                        raster_path,
+                        map_output,
+                        colormap=colormap,
+                        title=map_title,
+                        output_size=12,
+                        dpi=300,
+                        resolution_factor=4.0,
+                        enable_interpolation=False,
+                        interpolation_method='nearest',
+                        boundary_gdf=boundary_gdf
+                    )
+                    generated_files["map"] = map_file
+
+                    # 创建直方图 - 使用特定标题
+                    histogram_title = "Groundwater Leaching Cd Flux Distribution"
+
+                    histogram_file = mapper.create_histogram(
+                        raster_path,
+                        f"{histogram_output}.jpg",
+                        title=histogram_title,
+                        xlabel='Cd Flux (g/ha/a)',
+                        ylabel='Frequency Density'
+                    )
+                    generated_files["histogram"] = histogram_file
+
+                except Exception as viz_error:
+                    self.logger.warning(f"栅格可视化创建失败: {str(viz_error)}")
+
+            # 清理中间文件
+            if cleanup_intermediate:
+                try:
+                    self._cleanup_intermediate_files(generated_files, None)
+                except Exception as cleanup_err:
+                    self.logger.warning(f"中间文件清理失败: {str(cleanup_err)}")
+
+            self.logger.info(f"✓ 成功创建地下渗流通量可视化,生成文件: {list(generated_files.keys())}")
+            return generated_files
+
+        except Exception as e:
+            self.logger.error(f"创建地下渗流可视化失败: {str(e)}")
+            return {
+                "success": False,
+                "message": f"可视化创建失败: {str(e)}",
+                "data": None
+            }

+ 119 - 28
app/services/cd_flux_service.py

@@ -16,8 +16,10 @@ from app.utils.mapping_utils import MappingUtils, csv_to_raster_workflow
 
 # 配置日志
 from app.log.logger import get_logger
+
 logger = get_logger(__name__)
 
+
 def get_base_dir():
     """获取基础目录路径(与土地数据处理函数一致)"""
     if getattr(sys, 'frozen', False):
@@ -48,6 +50,7 @@ def get_default_boundary_shp():
 
     return None
 
+
 class FluxCdVisualizationService:
     """
     农田Cd通量可视化服务类
@@ -70,24 +73,81 @@ class FluxCdVisualizationService:
         """
         生成输入镉通量(In_Cd)的空间分布图和直方图
 
+        @param output_dir: 输出文件目录
+        @param boundary_shp: 边界Shapefile文件路径
+        @return: 包含输出文件路径的字典
+        """
+        return self._generate_cd_flux_map(field='in_cd', title_prefix="input",
+                                          output_dir=output_dir, boundary_shp=boundary_shp)
+
+    def generate_cd_output_flux_map(self, output_dir: str = None, boundary_shp: str = None) -> Dict[str, Any]:
+        """
+        生成输出镉通量(Out_Cd)的空间分布图和直方图
+
+        @param output_dir: 输出文件目录
+        @param boundary_shp: 边界Shapefile文件路径
+        @return: 包含输出文件路径的字典
+        """
+        return self._generate_cd_flux_map(field='out_cd', title_prefix="output",
+                                          output_dir=output_dir, boundary_shp=boundary_shp)
+
+    def generate_cd_net_flux_map(self, output_dir: str = None, boundary_shp: str = None) -> Dict[str, Any]:
+        """
+        生成净镉通量(Net_Cd)的空间分布图和直方图
+
+        @param output_dir: 输出文件目录
+        @param boundary_shp: 边界Shapefile文件路径
+        @return: 包含输出文件路径的字典
+        """
+        return self._generate_cd_flux_map(field='net_cd', title_prefix="net",
+                                          output_dir=output_dir, boundary_shp=boundary_shp)
+
+    def _generate_cd_flux_map(self, field: str, title_prefix: str,
+                              output_dir: str = None, boundary_shp: str = None) -> Dict[str, Any]:
+        """
+        通用的Cd通量地图生成方法
+
+        @param field: 要可视化的字段('in_cd'、'out_cd'或'net_cd')
+        @param title_prefix: 标题前缀('input'、'output'或'net')
         @param output_dir: 输出文件目录
         @param boundary_shp: 边界Shapefile文件路径
         @return: 包含输出文件路径的字典
         """
         try:
+            # 字段到标题的映射
+            field_titles = {
+                'in_cd': "Input",
+                'out_cd': "Output",
+                'net_cd': "Net"
+            }
+
+            # 字段到单位的映射
+            field_units = {
+                'in_cd': "g/ha/a",
+                'out_cd': "g/ha/a",
+                'net_cd': "g/ha/a"
+            }
+
+            # 字段到轴标签的映射
+            field_labels = {
+                'in_cd': 'input Cd flux',
+                'out_cd': 'output Cd flux',
+                'net_cd': 'net Cd flux'
+            }
+
             # 如果未提供数据库会话,则创建新的会话
             db = self.db if self.db else SessionLocal()
             should_close = self.db is None
 
             # 1. 从数据库查询数据
-            data = self._fetch_fluxcd_data(db)
+            data = self._fetch_fluxcd_data(db, field)
 
             if not data:
                 if should_close:
                     db.close()
                 return {
                     "success": False,
-                    "message": "数据库中未找到任何农田Cd通量数据",
+                    "message": f"数据库中未找到任何农田{field_titles[field]}Cd通量数据",
                     "data": None
                 }
 
@@ -106,8 +166,9 @@ class FluxCdVisualizationService:
                     self.logger.warning("未找到默认边界文件,将不使用边界裁剪")
 
             # 4. 生成CSV文件
-            csv_path = os.path.join(output_dir, "fluxcd_input.csv")
-            self._generate_csv(data, csv_path)
+            base_name = f"fluxcd_{title_prefix}"
+            csv_path = os.path.join(output_dir, f"{base_name}.csv")
+            self._generate_csv(data, csv_path, field_names=['lon', 'lan', field])
 
             # 5. 设置模板TIFF路径(使用土地数据处理中的模板)
             template_tif = os.path.join(static_dir, "meanTemp.tif")
@@ -118,7 +179,6 @@ class FluxCdVisualizationService:
                     raise FileNotFoundError(f"未找到模板TIFF文件")
 
             # 6. 使用csv_to_raster_workflow将CSV转换为栅格
-            base_name = "fluxcd_input"
             raster_path = os.path.join(output_dir, f"{base_name}_raster.tif")
 
             # 关键修改:确保传递边界文件
@@ -128,7 +188,7 @@ class FluxCdVisualizationService:
                 output_dir=output_dir,
                 resolution_factor=4.0,
                 interpolation_method='linear',
-                field_name='In_Cd',
+                field_name=field,
                 lon_col=0,  # CSV中经度列索引
                 lat_col=1,  # CSV中纬度列索引
                 value_col=2,  # CSV中数值列索引
@@ -146,7 +206,7 @@ class FluxCdVisualizationService:
                 tif_path=raster_path,  # 栅格文件
                 output_path=map_path,
                 colormap='green_yellow_red_purple',
-                title="input Cd flux map",
+                title=f"{title_prefix} Cd flux map",
                 output_size=12,
                 dpi=300,
                 enable_interpolation=False,
@@ -157,15 +217,15 @@ class FluxCdVisualizationService:
             histogram_path = self.mapper.create_histogram(
                 raster_path,
                 save_path=os.path.join(output_dir, f"{base_name}_histogram.jpg"),
-                xlabel='input Cd flux(g/ha/a)',
+                xlabel=f'{field_labels[field]}({field_units[field]})',
                 ylabel='frequency',
-                title='input Cd flux histogram',
+                title=f'{title_prefix} Cd flux histogram',
                 bins=100
             )
 
             result = {
                 "success": True,
-                "message": "成功生成Cd通量可视化结果",
+                "message": f"成功生成{field_titles[field]}Cd通量可视化结果",
                 "data": {
                     "csv": csv_path,
                     "raster": raster_path,
@@ -194,11 +254,12 @@ class FluxCdVisualizationService:
         update_service = FluxCdUpdateService(db=self.db)
         return update_service.update_from_csv(csv_file_path)
 
-    def _fetch_fluxcd_data(self, db: Session) -> list:
+    def _fetch_fluxcd_data(self, db: Session, field: str = 'in_cd') -> list:
         """
         从数据库查询需要的数据
 
         @param db: 数据库会话
+        @param field: 要查询的字段('in_cd', 'out_cd'或'net_cd')
         @returns: 查询结果列表
         """
         try:
@@ -206,7 +267,7 @@ class FluxCdVisualizationService:
             query = db.query(
                 FarmlandData.lon,
                 FarmlandData.lan,
-                FluxCdOutputData.in_cd
+                getattr(FluxCdOutputData, field)
             ).join(
                 FluxCdOutputData,
                 (FarmlandData.farmland_id == FluxCdOutputData.farmland_id) &
@@ -219,18 +280,22 @@ class FluxCdVisualizationService:
             self.logger.error(f"查询农田Cd通量数据时发生错误: {str(e)}")
             return []
 
-    def _generate_csv(self, data: list, output_path: str):
+    def _generate_csv(self, data: list, output_path: str, field_names: list = None):
         """
         将查询结果生成CSV文件
 
         @param data: 查询结果列表
         @param output_path: 输出CSV文件路径
+        @param field_names: CSV列名列表
         """
+        if field_names is None:
+            field_names = ['lon', 'lan', 'In_Cd']
+
         try:
             with open(output_path, 'w', newline='', encoding='utf-8') as csvfile:
                 writer = csv.writer(csvfile)
                 # 写入表头
-                writer.writerow(['lon', 'lan', 'In_Cd'])
+                writer.writerow(field_names)
 
                 # 写入数据
                 for row in data:
@@ -343,10 +408,10 @@ class FluxCdUpdateService:
                 agro_chemicals_input=row['NCP_Cd'],
                 # 设置合理的默认值
                 initial_cd=0.0,
-                groundwater_leaching=0.023,
-                surface_runoff=0.368,
-                grain_removal=0.0,
-                straw_removal=0.0
+                groundwater_leaching=row.get('DX_Cd', 0.023),  # 添加默认值处理
+                surface_runoff=row.get('DB_Cd', 0.368),
+                grain_removal=row.get('ZL_Cd', 0.0),
+                straw_removal=row.get('JG_Cd', 0.0)
             )
             self.db.add(input_data)
             self.logger.info("输入通量记录创建成功")
@@ -354,8 +419,11 @@ class FluxCdUpdateService:
             # 计算输入总通量
             in_cd = row['DQCJ_Cd'] + row['GGS_Cd'] + row['NCP_Cd']
 
-            # 计算输出总通量(假设默认值)
-            out_cd = 0.023 + 0.368 + 0.0 + 0.0
+            # 计算输出总通量
+            out_cd = (input_data.groundwater_leaching +
+                      input_data.surface_runoff +
+                      input_data.grain_removal +
+                      input_data.straw_removal)
 
             # 创建新的输出通量记录
             output_data = FluxCdOutputData(
@@ -388,10 +456,18 @@ class FluxCdUpdateService:
     def _validate_csv(self, df: pd.DataFrame):
         """验证CSV文件包含必要的列"""
         required_columns = {'lon', 'lan', 'DQCJ_Cd', 'GGS_Cd', 'NCP_Cd'}
+        # 添加输出通量可选字段
+        output_fields = {'DX_Cd', 'DB_Cd', 'ZL_Cd', 'JG_Cd'}
+
         if not required_columns.issubset(df.columns):
             missing = required_columns - set(df.columns)
             raise ValueError(f"CSV缺少必要列: {', '.join(missing)}")
 
+        # 检查是否包含输出通量字段(可选)
+        if not output_fields.issubset(df.columns):
+            missing_output = output_fields - set(df.columns)
+            self.logger.warning(f"CSV缺少输出通量字段: {', '.join(missing_output)},将使用默认值")
+
     def _find_farmland(self, lon: float, lan: float) -> FarmlandData:
         """根据经纬度查找农田样点"""
         # 使用容差匹配(0.001度≈100米)
@@ -412,7 +488,7 @@ class FluxCdUpdateService:
             self.logger.warning(f"未找到输入数据: Farmland_ID={farmland.farmland_id}, Sample_ID={farmland.sample_id}")
             return False
 
-            # 检查是否需要更新
+        # 检查是否需要更新
         updated = False
         if input_data.atmospheric_deposition != row['DQCJ_Cd']:
             input_data.atmospheric_deposition = row['DQCJ_Cd']
@@ -446,11 +522,16 @@ class FluxCdUpdateService:
             FluxCdInputData.sample_id == farmland.sample_id
         ).first()
 
-        # 重新计算并更新
+        # 计算输出总通量
+        out_cd = (input_data.groundwater_leaching +  # DX_Cd
+                  input_data.surface_runoff +  # DB_Cd
+                  input_data.grain_removal +  # ZL_Cd
+                  input_data.straw_removal)  # JG_Cd
+
+        # 更新输出通量记录
         output_data.in_cd = input_data.input_flux()
-        # 注意:输出总通量out_cd不会由用户上传的CSV更新,所以我们保持原值
-        # 重新计算净通量
-        output_data.net_cd = output_data.in_cd - output_data.out_cd
+        output_data.out_cd = out_cd  # 添加输出通量计算
+        output_data.net_cd = output_data.in_cd - out_cd  # 更新净通量计算
 
         self.logger.info(f"更新输出通量: Farmland_ID={farmland.farmland_id}, Sample_ID={farmland.sample_id}")
 
@@ -475,6 +556,16 @@ if __name__ == "__main__":
 
         # 测试生成可视化结果
         print("\n>>> 测试生成Cd输入通量可视化地图")
+        input_result = service.generate_cd_input_flux_map()
+        print(json.dumps(input_result, indent=2, ensure_ascii=False))
+
+        print("\n>>> 测试生成Cd输出通量可视化地图")
+        output_result = service.generate_cd_output_flux_map()
+        print(json.dumps(output_result, indent=2, ensure_ascii=False))
+
+        print("\n>>> 测试生成Cd净通量可视化地图")
+        net_result = service.generate_cd_net_flux_map()
+        print(json.dumps(net_result, indent=2, ensure_ascii=False))
 
         # 测试更新服务
         print("\n>>> 测试从CSV更新Cd通量数据")
@@ -485,9 +576,9 @@ if __name__ == "__main__":
 
         # 创建测试CSV文件
         test_data = [
-            "lon,lan,DQCJ_Cd,GGS_Cd,NCP_Cd",
-            "113.123,25.456,1.24,4.56,7.89",
-            "113.125,25.457,2.35,5.67,8.90"
+            "lon,lan,DQCJ_Cd,GGS_Cd,NCP_Cd,DX_Cd,DB_Cd,ZL_Cd,JG_Cd",
+            "113.123,25.456,1.24,4.56,7.89,0.1,0.2,0.05,0.03",
+            "113.125,25.457,2.35,5.67,8.90,0.15,0.25,0.06,0.04"
         ]
 
         with open(test_csv_path, 'w', encoding='utf-8') as f:

+ 1 - 1
app/static/cd_flux/fluxcd_input.csv

@@ -1,4 +1,4 @@
-lon,lan,In_Cd
+lon,lan,in_cd
 113.289928438,25.5185734083,4.47340547365967
 113.289933749,25.5176707937,4.22450504615878
 113.290928469,25.5176756095,4.20535304615878

BIN
app/static/cd_flux/fluxcd_input_histogram.jpg


BIN
app/static/cd_flux/fluxcd_input_points.dbf


BIN
app/static/cd_flux/fluxcd_input_raster.tif


+ 56 - 17
app/static/cd_flux/latest_result.json

@@ -1,20 +1,59 @@
 {
-  "success": true,
-  "message": "成功生成Cd通量可视化结果",
-  "data": {
-    "csv": "C:\\Users\\17417\\AppData\\Local\\Temp\\tmp6vp19yip\\fluxcd_input.csv",
-    "raster": "C:\\Users\\17417\\AppData\\Local\\Temp\\tmp6vp19yip\\fluxcd_input_raster.tif",
-    "map": "C:\\Users\\17417\\AppData\\Local\\Temp\\tmp6vp19yip\\fluxcd_input_map.jpg",
-    "histogram": "C:\\Users\\17417\\AppData\\Local\\Temp\\tmp6vp19yip\\fluxcd_input_histogram.jpg",
-    "statistics": {
-      "min": 3.4116151332855225,
-      "max": 100.34626007080078,
-      "mean": 4.829534696702168,
-      "std": 1.7983457065881827,
-      "valid_pixels": 528396,
-      "total_pixels": 715504
-    },
-    "boundary_used": "D:\\17417\\Documents\\backend\\app\\services\\..\\static\\cd_flux\\lechang.shp"
+  "input": {
+    "success": true,
+    "message": "成功生成InputCd通量可视化结果",
+    "data": {
+      "csv": "D:\\17417\\Documents\\backend\\app\\api\\..\\static\\cd_flux\\fluxcd_input.csv",
+      "raster": "D:\\17417\\Documents\\backend\\app\\api\\..\\static\\cd_flux\\fluxcd_input_raster.tif",
+      "map": "D:\\17417\\Documents\\backend\\app\\api\\..\\static\\cd_flux\\fluxcd_input_map.jpg",
+      "histogram": "D:\\17417\\Documents\\backend\\app\\api\\..\\static\\cd_flux\\fluxcd_input_histogram.jpg",
+      "statistics": {
+        "min": 3.4116151332855225,
+        "max": 100.34626007080078,
+        "mean": 5.61509136418512,
+        "std": 3.798631197888755,
+        "valid_pixels": 62068,
+        "total_pixels": 715504
+      },
+      "boundary_used": "D:\\17417\\Documents\\backend\\app\\services\\..\\static\\cd_flux\\lechang.shp"
+    }
   },
-  "timestamp": "2025-08-08T14:25:42.795499"
+  "output": {
+    "success": true,
+    "message": "成功生成OutputCd通量可视化结果",
+    "data": {
+      "csv": "D:\\17417\\Documents\\backend\\app\\api\\..\\static\\cd_flux\\fluxcd_output.csv",
+      "raster": "D:\\17417\\Documents\\backend\\app\\api\\..\\static\\cd_flux\\fluxcd_output_raster.tif",
+      "map": "D:\\17417\\Documents\\backend\\app\\api\\..\\static\\cd_flux\\fluxcd_output_map.jpg",
+      "histogram": "D:\\17417\\Documents\\backend\\app\\api\\..\\static\\cd_flux\\fluxcd_output_histogram.jpg",
+      "statistics": {
+        "min": 0.39100000262260437,
+        "max": 11.176603317260742,
+        "mean": 3.3491851019653964,
+        "std": 2.2461722103177286,
+        "valid_pixels": 62068,
+        "total_pixels": 715504
+      },
+      "boundary_used": "D:\\17417\\Documents\\backend\\app\\services\\..\\static\\cd_flux\\lechang.shp"
+    }
+  },
+  "net": {
+    "success": true,
+    "message": "成功生成NetCd通量可视化结果",
+    "data": {
+      "csv": "D:\\17417\\Documents\\backend\\app\\api\\..\\static\\cd_flux\\fluxcd_net.csv",
+      "raster": "D:\\17417\\Documents\\backend\\app\\api\\..\\static\\cd_flux\\fluxcd_net_raster.tif",
+      "map": "D:\\17417\\Documents\\backend\\app\\api\\..\\static\\cd_flux\\fluxcd_net_map.jpg",
+      "histogram": "D:\\17417\\Documents\\backend\\app\\api\\..\\static\\cd_flux\\fluxcd_net_histogram.jpg",
+      "statistics": {
+        "min": -7.432056903839111,
+        "max": 96.24049377441406,
+        "mean": 2.2659062665456235,
+        "std": 4.301549697243313,
+        "valid_pixels": 62068,
+        "total_pixels": 715504
+      },
+      "boundary_used": "D:\\17417\\Documents\\backend\\app\\services\\..\\static\\cd_flux\\lechang.shp"
+    }
+  }
 }