Browse Source

Merge remote-tracking branch 'origin/lili' into ding

# Conflicts:
#	app/models/FluxCd_output.py
#	app/models/county.py
#	main.py
yangtaodemon 2 days ago
parent
commit
9074d27d09

+ 16 - 6
app/api/vector.py

@@ -7,7 +7,7 @@ from ..services import vector_service
 from ..services.admin_boundary_service import get_boundary_geojson_by_name
 import os
 import shutil
-from typing import List
+from typing import List, Optional
 
 router = APIRouter()
 
@@ -52,18 +52,25 @@ async def export_vector_data(vector_id: int, db: Session = Depends(get_db)):
         background=BackgroundTasks().add_task(shutil.rmtree, result["temp_dir"]) if "temp_dir" in result else None
     )
 
-@router.get("/export/all", summary="导出矢量数据", description="将指定的矢量数据表导出为GeoJSON文件")
-async def export_all_vector_data_api(table_name: str = "vector_data", db: Session = Depends(get_db)):
-    """导出指定矢量数据表为GeoJSON文件
+@router.get("/export/all", summary="导出矢量数据", description="将指定的矢量数据表导出为GeoJSON或JSON文件")
+async def export_all_vector_data_api(table_name: str = "vector_data", db: Session = Depends(get_db),format:Optional[str] = "geojson"):
+    """导出指定矢量数据表为GeoJSON文件或普通JSON文件
     
     Args:
         table_name (str): 要导出的矢量数据表名,默认为'vector_data'
         db (Session): 数据库会话
+        format (str,optional):导出格式支持'geojson'和'json',默认为'geojson'
         
     Returns:
         FileResponse: GeoJSON文件下载响应
     """
-    result = vector_service.export_all_vector_data(db, table_name)
+    #验证格式参数
+    if format not in ["geojson" , "json"]:
+        raise HTTPException(
+            status_code=400,
+            detail="只支持'geojson'和'json'格式"
+        )
+    result = vector_service.export_all_vector_data(db, table_name,export_format=format)#传递导出格式
     
     # 检查文件是否存在
     if not os.path.exists(result["file_path"]):
@@ -71,11 +78,14 @@ async def export_all_vector_data_api(table_name: str = "vector_data", db: Sessio
             shutil.rmtree(result["temp_dir"])
         raise HTTPException(status_code=404, detail="导出文件不存在")
     
+    #根据格式设置正确的媒体类型
+    media_type = "application/geo+json" if format == "geojson" else "application/json"
+
     # 返回文件下载
     return FileResponse(
         path=result["file_path"],
         filename=os.path.basename(result["file_path"]),
-        media_type="application/json",
+        media_type=media_type,
         background=BackgroundTasks().add_task(shutil.rmtree, result["temp_dir"]) if "temp_dir" in result else None
     )
 

+ 30 - 3
app/database.py

@@ -1,4 +1,5 @@
-from sqlalchemy import create_engine
+from sqlalchemy import create_engine , text
+from app.models.base import Base
 from sqlalchemy.orm import sessionmaker
 from sqlalchemy.ext.declarative import declarative_base
 import os
@@ -13,7 +14,7 @@ logging.getLogger('sqlalchemy.engine').setLevel(logging.WARNING)
 logger = logging.getLogger(__name__)
 
 # 创建Base类
-Base = declarative_base()
+#Base = declarative_base()
 Base.metadata.clear()
 
 # 加载环境变量
@@ -98,11 +99,37 @@ def execute_sql(sql_statement):
 # 新增:自动创建数据库表(关键!)
 def create_tables():
     try:
+         # ✨ 新增:同时启用 PostGIS 核心和 Raster 扩展
+        with engine.begin() as conn:
+            # 启用 PostGIS 核心(矢量)
+            conn.execute(text("CREATE EXTENSION IF NOT EXISTS postgis;")) # pyright: ignore[reportUndefinedVariable]
+            # 启用 PostGIS Raster(栅格)
+            conn.execute(text("CREATE EXTENSION IF NOT EXISTS postgis_raster;")) # pyright: ignore[reportUndefinedVariable]
+            logger.info("PostGIS 核心及 Raster 扩展已启用(或已存在)")
         # 必须导入所有模型,否则 Base 不知道要创建哪些表
         # 替换成你项目中实际的模型文件路径(根据你的目录结构调整)
         from app.models.orm_models import Base  # 确保模型继承自这个 Base
         from app.models.vector import VectorData  # 导入需要创建的表
-        
+        #from app.models.raster import RasterData  # 补充其他模型
+        from app.models.county import County
+        from app.models.farmland import FarmlandData
+
+        from app.models.agricultural import AgriculturalData  # 假设类名是 Agricultural
+        from app.models.assessment import Assessment
+        from app.models.atmo_company import AtmoCompany
+        from app.models.atmo_sample import AtmoSampleData
+        from app.models.CropCd_input import CropCdInputData
+        from app.models.CropCd_output import CropCdOutputData
+        from app.models.cross_section import CrossSection
+        from app.models.EffCd_input import EffCdInputData
+        from app.models.EffCd_output import EffCdOutputData
+        from app.models.FluxCd_input import FluxCdInputData
+        from app.models.FluxCd_output import FluxCdOutputData
+        from app.models.MSM_input import MSMInputData
+        from app.models.MSM_output import MSMOutputData
+        from app.models.parameters import Parameters
+        from app.models.soil import SoilData  
+        from app.models.water_sample import WaterSampleData
         # 创建所有表
         Base.metadata.create_all(bind=engine)
         logger.info("数据库表自动创建成功!")

+ 1 - 1
app/models/CropCd_input.py

@@ -1,6 +1,6 @@
 from sqlalchemy import Column, Integer, Float
 from sqlalchemy import ForeignKeyConstraint
-from app.database import Base  # 统一的基础模型
+from app.models.base import Base
 
 
 class CropCdInputData(Base):

+ 1 - 1
app/models/CropCd_output.py

@@ -1,6 +1,6 @@
 from sqlalchemy import Column, Integer, Float
 from sqlalchemy import ForeignKeyConstraint
-from app.database import Base  # 统一的基础模型
+from app.models.base import Base
 
 
 class CropCdOutputData(Base):

+ 1 - 1
app/models/EffCd_input.py

@@ -1,6 +1,6 @@
 from sqlalchemy import Column, Integer, Float
 from sqlalchemy import ForeignKeyConstraint
-from app.database import Base  # 统一的Base导入
+from app.models.base import Base
 
 
 class EffCdInputData(Base):

+ 1 - 1
app/models/EffCd_output.py

@@ -1,7 +1,7 @@
 # app/models/effcd_output.py
 from sqlalchemy import Column, Integer, Float
 from sqlalchemy import ForeignKeyConstraint
-from app.database import Base  # 统一的基础模型
+from app.models.base import Base
 
 
 class EffCdOutputData(Base):

+ 1 - 1
app/models/FluxCd_input.py

@@ -1,6 +1,6 @@
 from sqlalchemy import Column, Integer, Float
 from sqlalchemy import ForeignKeyConstraint
-from app.database import Base  # 统一的基础模型
+from app.models.base import Base
 
 
 class FluxCdInputData(Base):

+ 2 - 59
app/models/FluxCd_output.py

@@ -1,6 +1,5 @@
 from sqlalchemy import Column, Integer, Float, ForeignKeyConstraint
-from app.database import Base
-from app.models import FluxCdInputData, SoilData
+from app.models.base import Base
 
 
 class FluxCdOutputData(Base):
@@ -32,60 +31,4 @@ 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
-
-    # 在 app/models/effcd_output.py 的 FluxCdOutputData 类中添加以下方法
-    def calculate_end_cd(self, db_session):
-        """
-        计算当年Cd浓度(mg/kg)
-        公式: (Initial_Cd + Net_Cd) / (2000 * bd020_90)
-
-        @param db_session: 数据库会话
-        @return: 计算后的Cd浓度值
-        """
-        try:
-            # 1. 获取初始Cd浓度(Initial_Cd) - 从FluxCdInputData表
-            input_data = db_session.query(FluxCdInputData).filter(
-                FluxCdInputData.farmland_id == self.farmland_id,
-                FluxCdInputData.sample_id == self.sample_id
-            ).first()
-
-            if not input_data:
-                raise ValueError("未找到对应的输入通量数据")
-
-            initial_cd = input_data.initial_cd  # 单位: g/ha
-
-            # 2. 获取土壤容重(bd020_90) - 从SoilData表
-            soil_data = db_session.query(SoilData).filter(
-                SoilData.farmland_id == self.farmland_id,
-                SoilData.sample_id == self.sample_id
-            ).first()
-
-            if not soil_data:
-                raise ValueError("未找到对应的土壤数据")
-
-            bd020_90 = soil_data.bd020_90  # 单位: g/cm³
-
-            # 3. 计算当年Cd浓度
-            # 注意单位转换:
-            # - Initial_Cd和Net_Cd单位是g/ha
-            # - 1 ha = 10,000 m²
-            # - 假设计算深度为20cm(0.2m),所以体积为2000 m³/ha
-            # - bd020_90单位是g/cm³ = 1000 kg/m³
-            # 公式推导:
-            # (g/ha) / (2000 m³/ha * 1000 kg/m³) = (g/ha) / (2,000,000 kg/ha) = mg/kg
-
-            end_cd = (initial_cd + self.net_cd) / (2000 * bd020_90)
-
-            # 更新并返回结果
-            self.end_cd = end_cd
-            return end_cd
-
-        except Exception as e:
-            raise ValueError(f"计算当年Cd浓度失败: {str(e)}")
+    )

+ 1 - 1
app/models/MSM_input.py

@@ -1,7 +1,7 @@
 from sqlalchemy import Column, Integer, Float, String
 from sqlalchemy import ForeignKey, ForeignKeyConstraint
 from geoalchemy2 import Geometry
-from app.database import Base
+from app.models.base import Base
 
 
 class MSMInputData(Base):

+ 1 - 1
app/models/MSM_output.py

@@ -1,6 +1,6 @@
 from sqlalchemy import Column, Integer, Float, Text
 from sqlalchemy import ForeignKey, ForeignKeyConstraint
-from app.database import Base  # 统一的基础模型
+from app.models.base import Base
 
 
 class MSMOutputData(Base):

+ 1 - 1
app/models/agricultural.py

@@ -1,6 +1,6 @@
 from sqlalchemy import Column, Integer, Float, String
 from sqlalchemy.dialects.postgresql import TEXT
-from app.database import Base
+from app.models.base import Base
 
 
 class AgriculturalData(Base):

+ 1 - 1
app/models/assessment.py

@@ -1,7 +1,7 @@
 from sqlalchemy import Column, Integer, Float, ForeignKey
 from sqlalchemy.orm import relationship, backref
 from geoalchemy2 import Geometry
-from app.database import Base
+from app.models.base import Base
 from sqlalchemy import ForeignKeyConstraint
 
 

+ 1 - 1
app/models/atmo_company.py

@@ -1,5 +1,5 @@
 from sqlalchemy import Column, Integer, Float, String
-from app.database import Base
+from app.models.base import Base
 
 
 class AtmoCompany(Base):

+ 1 - 1
app/models/atmo_sample.py

@@ -1,6 +1,6 @@
 from sqlalchemy import Column, String, Float
 from sqlalchemy.dialects.postgresql import TEXT
-from app.database import Base
+from app.models.base import Base
 
 
 class AtmoSampleData(Base):

+ 1 - 1
app/models/city.py

@@ -1,7 +1,7 @@
 from sqlalchemy import Column, Integer, String, Text, Index, JSON
 from sqlalchemy.dialects.postgresql import JSONB
 from geoalchemy2 import Geometry
-from app.database import Base
+from app.models.base import Base
 from shapely.geometry import shape, MultiPolygon, Polygon
 import json
 

+ 3 - 8
app/models/county.py

@@ -1,8 +1,7 @@
 from sqlalchemy import Column, Integer, String, Text, Index, JSON
 from sqlalchemy.dialects.postgresql import JSONB
 from geoalchemy2 import Geometry
-from app.database import Base
-from shapely.geometry import shape, MultiPolygon, Polygon
+from app.models.base import Base
 import json
 
 class County(Base):
@@ -60,13 +59,9 @@ class County(Base):
             for k, v in properties.items()
         }
         
-        # 将GeoJSON geometry转换为EWKT格式(SRID=4326)
+        # 将GeoJSON geometry转换为WKT格式
         geometry = feature['geometry']
-        geom = shape(geometry)
-        # 统一为 MultiPolygon 以匹配列类型
-        if isinstance(geom, Polygon):
-            geom = MultiPolygon([geom])
-        wkt = f"SRID=4326;{geom.wkt}"
+        wkt = f"SRID=4326;{json.dumps(geometry)}"
         
         # 创建实例
         county = cls(

+ 1 - 1
app/models/cross_section.py

@@ -1,5 +1,5 @@
 from sqlalchemy import Column, Integer, Float, String, Text
-from app.database import Base
+from app.models.base import Base
 
 
 class CrossSection(Base):

+ 1 - 1
app/models/farmland.py

@@ -6,7 +6,7 @@
 
 from sqlalchemy import Column, Integer, Float, ForeignKey
 from geoalchemy2 import Geometry
-from app.database import Base
+from app.models.base import Base
 
 
 class FarmlandData(Base):

+ 1 - 1
app/models/orm_models.py

@@ -140,7 +140,7 @@ class FiftyThousandSurveyDatum(Base):
     geom = Column(Geometry('POINT', from_text='ST_GeomFromEWKT', name='geometry'), index=True, comment='几何位置信息')
 
 class RasterTable(Base):
-    __tablename__ = 'raster_table'
+    __tablename__ = 'raster_table_v2'
 
     id = Column(Integer, primary_key=True, autoincrement=True)
     rast = Column(Raster(from_text='raster', name='raster'), index=False)

+ 1 - 1
app/models/parameters.py

@@ -1,6 +1,6 @@
 from sqlalchemy import Column, Integer, Float, String, create_engine
 from sqlalchemy.orm import declarative_base
-from app.database import Base
+from app.models.base import Base
 
 
 class Parameters(Base):

+ 1 - 1
app/models/province.py

@@ -1,7 +1,7 @@
 from sqlalchemy import Column, Integer, String, Text, Index, JSON
 from sqlalchemy.dialects.postgresql import JSONB
 from geoalchemy2 import Geometry
-from app.database import Base
+from app.models.base import Base
 from shapely.geometry import shape, MultiPolygon, Polygon
 import json
 

+ 1 - 1
app/models/raster.py

@@ -2,7 +2,7 @@
 
 from sqlalchemy import Column, Integer
 from geoalchemy2 import Raster
-from ..database import Base
+from app.models.base import Base
 
 class RasterData(Base):
     __tablename__ = "raster_table"

+ 1 - 1
app/models/soil.py

@@ -1,6 +1,6 @@
 from sqlalchemy import Column, Integer, Float, ForeignKey, ForeignKeyConstraint
 from geoalchemy2 import Geometry
-from app.database import Base
+from app.models.base import Base
 
 
 class SoilData(Base):

+ 2 - 2
app/models/vector.py

@@ -1,12 +1,12 @@
 # 从数据库配置里导入 Base(用来管理表)
-from app.database import Base
+from app.models.base import Base
 from sqlalchemy import Table, Column, Integer, String, Float, MetaData
 
 # 关联到 Base,让它能自动创建表
 metadata = Base.metadata
 # 新增 ORM 类定义(服务层需要的 VectorData)
 class VectorData(Base):
-    __tablename__ = "surveydata"  # 使用相同的表名
+    __tablename__ = "vector_surveydata"  # 使用相同的表名
     
     id = Column(Integer, primary_key=True)
     name = Column(String(100))

+ 1 - 1
app/models/water_sample.py

@@ -1,6 +1,6 @@
 from sqlalchemy import Column, Integer, Float, String, Text, DateTime
 from geoalchemy2 import Geometry
-from app.database import Base
+from app.models.base import Base
 
 
 class WaterSampleData(Base):

+ 26 - 4
app/services/vector_service.py

@@ -165,20 +165,42 @@ def export_vector_data_batch(db: Session, vector_ids: List[int]):
     return _export_vector_data_to_file(vector_data_list, f"export_batch_{'_'.join(map(str, vector_ids))}", "surveydata")
 
 
-def export_all_vector_data(db: Session, table_name: str = "surveydata"):
-    """导出指定的矢量数据表为GeoJSON格式并保存到文件"""
+def export_all_vector_data(
+    db: Session, 
+    table_name: str = "surveydata", 
+    export_format: str = "geojson"  # 新增:格式参数,默认geojson
+):
+    """导出指定表数据为GeoJSON或普通JSON格式"""
     try:
+        # 1. 查询表数据(保持原有逻辑)
         query = text(f'SELECT * FROM "{table_name}"')
         result = db.execute(query)
         columns = [col.name for col in result.cursor.description]
-        vector_data_list = [dict(zip(columns, row)) for row in result.fetchall()]  # 类型修正
-        return _export_vector_data_to_file(vector_data_list, f"export_{table_name}", table_name)
+        # 转换为字典列表(原生查询结果处理)
+        data_list = [dict(zip(columns, row)) for row in result.fetchall()]
+        
+        # 2. 根据格式生成不同文件
+        if export_format == "geojson":
+            # 原有逻辑:生成GeoJSON(依赖你的build_geojson工具)
+            return _export_vector_data_to_file(data_list, f"export_{table_name}", table_name)
+        else:
+            # 新增逻辑:生成普通JSON
+            return _export_json_data_to_file(data_list, f"export_{table_name}", table_name)
+            
     except Exception as e:
         raise HTTPException(
             status_code=500,
             detail=f"查询表{table_name}失败:{str(e)}"
         )
 
+# 新增函数:生成普通JSON文件
+def _export_json_data_to_file(data_list, base_filename, table_name):
+    temp_dir = tempfile.mkdtemp()
+    file_path = os.path.join(temp_dir, f"{base_filename}.json")  # 后缀改为json
+    # 直接写入原始数据(无需GeoJSON转换)
+    with open(file_path, "w", encoding="utf-8") as f:
+        json.dump(data_list, f, ensure_ascii=False, indent=2)
+    return {"file_path": file_path, "temp_dir": temp_dir}
 
 def parse_geom_field(geom_value) -> dict:
     """解析 geom 字段为 GeoJSON 格式的 geometry"""

+ 11 - 8
main.py

@@ -1,19 +1,22 @@
-from app.main import app
+# 1. 先创建 FastAPI 应用实例(只创建一次)
 from fastapi import FastAPI
 from fastapi.middleware.cors import CORSMiddleware  # 导入 CORS 模块
+
+app = FastAPI()  # 核心实例,只定义一次
+# 3. 注册路由(必须显式挂载,否则路由不生效)
 if __name__ == "__main__":
     import uvicorn
     # uvicorn.run(app, host="0.0.0.0", port=8000, ssl_keyfile="ssl/cert.key", ssl_certfile="ssl/cert.crt")
     uvicorn.run("app.main:app", host="0.0.0.0", port=8000, reload=True)
 
-# 创建 FastAPI 应用实例
-app = FastAPI()
 
-# ========= 新增 CORS 配置 =========
+# 2. 配置 CORS(在注册路由前配置)
 app.add_middleware(
     CORSMiddleware,
-    allow_origins=["http://localhost:5173"],  # 允许前端地址
-    allow_credentials=True,
-    allow_methods=["*"],  # 允许所有方法
-    allow_headers=["*"],  # 允许所有头
+    allow_origins=["http://localhost:5173"],  # 允许前端 Vite 服务地址
+    allow_credentials=True,  # 允许携带 cookies
+    allow_methods=["*"],  # 允许所有 HTTP 方法(GET/POST等)
+    allow_headers=["*"],  # 允许所有请求
 )
+
+

+ 0 - 0
scripts/python


+ 0 - 0
soilgd.sql