添加降雨量与持续时间接口

This commit is contained in:
wzy-warehouse
2026-06-28 09:26:13 +08:00
parent 54f7300557
commit 1f01ceb062
6 changed files with 240 additions and 16 deletions
+1 -1
View File
@@ -134,7 +134,7 @@ async def predict_earthquake(req: EarthquakePredictRequest):
"point_ids": req.point_ids, "point_ids": req.point_ids,
"region_code": req.region_code, "region_code": req.region_code,
"magnitude": req.magnitude, "magnitude": req.magnitude,
"depth": req.depth, # 已有默认值 10.0 "depth": req.depth,
"epicenter_lon": req.epicenter_lon, "epicenter_lon": req.epicenter_lon,
"epicenter_lat": req.epicenter_lat, "epicenter_lat": req.epicenter_lat,
"occurred_time": occurred_time.isoformat() if hasattr(occurred_time, 'isoformat') else str(occurred_time) "occurred_time": occurred_time.isoformat() if hasattr(occurred_time, 'isoformat') else str(occurred_time)
+74 -6
View File
@@ -7,11 +7,16 @@ from typing import List, Dict, Any, Optional
from fastapi import APIRouter, HTTPException from fastapi import APIRouter, HTTPException
from app.schemas.api_schemas import RainfallPredictRequest, PredictResponse, PredictData, UpdateMonitoringTimeRequest from app.schemas.api_schemas import (
RainfallPredictRequest, PredictResponse, PredictData,
UpdateMonitoringTimeRequest, DistrictSummaryRequest, DistrictSummaryItem
)
from app.utils.api_deps import get_rainfall_model, get_prediction_semaphore from app.utils.api_deps import get_rainfall_model, get_prediction_semaphore
from app.repositories.dbn_repository import dbn_repository from app.repositories.dbn_repository import dbn_repository
from app.repositories.rainfall_repository import rainfall_repository as rain_repo
from app.core.rainfall_manager import rainfall_manager from app.core.rainfall_manager import rainfall_manager
from app.config.paths import get_logger from app.config.paths import get_logger
from app.utils.db_helper import db_helper
from app.utils.time_converter import TimeConverter from app.utils.time_converter import TimeConverter
router = APIRouter(prefix="/rainfall", tags=["暴雨灾害链"]) router = APIRouter(prefix="/rainfall", tags=["暴雨灾害链"])
@@ -141,13 +146,17 @@ async def update_monitoring_time(req: UpdateMonitoringTimeRequest):
@router.post("/predict", response_model=PredictResponse, summary="暴雨灾害链预测") @router.post("/predict", response_model=PredictResponse, summary="暴雨灾害链预测")
async def predict_rainfall(req: RainfallPredictRequest): async def predict_rainfall(req: RainfallPredictRequest):
""" """
根据降雨量和持续时间,批量预测隐患点/风险点的灾害概率。 批量预测隐患点/风险点的灾害概率。两种模式(二选一):
**自动推演模式**rainfall、duration、region_code 全部不传
→ 从气象表自动获取各点最近站降雨数据,不限区域
**指定条件模式**rainfall、duration、region_code 全部传入
→ 按指定降雨条件和区域预测
- **disaster_name**: 灾害名称 - **disaster_name**: 灾害名称
- **point_ids**: 点位ID列表(可选,不传则查询所有点 - **point_ids**: 点位ID列表(可选)
- **region_code**: 行政区划代码(可选,不传则不限区域 - **occurred_time**: 事件发生时间(可选,不传则为当前时间
- **rainfall**: 累计降雨量(mm),不传则从气象表自动获取
- **duration**: 降雨持续时间(h),不传则从气象表自动获取
- **operation_type**: 操作类型(如 '实时监测', '情景模拟', '应急评估' - **operation_type**: 操作类型(如 '实时监测', '情景模拟', '应急评估'
""" """
semaphore = get_prediction_semaphore() semaphore = get_prediction_semaphore()
@@ -180,3 +189,62 @@ async def predict_rainfall(req: RainfallPredictRequest):
logger.error(f"保存推理结果失败: {e}", exc_info=True) logger.error(f"保存推理结果失败: {e}", exc_info=True)
return PredictResponse(code=200, message="success", data=PredictData(record_id=record_id, list=result_map_with_location)) return PredictResponse(code=200, message="success", data=PredictData(record_id=record_id, list=result_map_with_location))
@router.post("/district-summary", summary="获取各区降雨概况")
async def district_summary(req: DistrictSummaryRequest):
"""
根据推理结果ID获取各行政区的降雨概况(用于报告生成)。
逻辑:
- 如果预测时传了 rainfall → 直接用 condition 中的值
- 如果预测时没传 rainfall → 从 xian_meteorology 按区聚合实际气象数据
- **inference_id**: 推理结果ID
"""
# 1. 查推理结果
record = dbn_repository.get_inference_result(req.inference_id)
if not record:
raise HTTPException(status_code=404, detail=f"推理结果不存在: {req.inference_id}")
condition = record.get('condition') or {}
occurred_time = record.get('occurred_time')
# 2. 如果传了 rainfall → 直接用 condition 中的值
if condition.get('rainfall') is not None:
region_code = condition.get('region_code')
rainfall_val = float(condition['rainfall'])
duration_val = float(condition.get('duration', 0))
if region_code:
# 查 district 名称
district_row = db_helper.execute_query_one(
"SELECT name FROM xian_district WHERE code = %s AND is_delete = 0", (region_code,)
)
district_name = district_row['name'] if district_row else region_code
items = [{
'district_name': district_name,
'district_code': region_code,
'rainfall': rainfall_val,
'duration_hours': duration_val
}]
else:
items = [{
'district_name': '全市',
'district_code': '',
'rainfall': rainfall_val,
'duration_hours': duration_val
}]
else:
# 3. 自动推演模式 → 从气象表按区聚合
items = rain_repo.get_district_rainfall_summary(occurred_time)
if not items:
return {"code": 200, "message": "success", "data": []}
# 转换为响应格式
result_items = [
DistrictSummaryItem(**item).model_dump()
for item in items
]
return {"code": 200, "message": "success", "data": result_items}
+20 -1
View File
@@ -195,6 +195,7 @@ class DbnRepository:
COUNT(*) as record_count COUNT(*) as record_count
FROM xian_meteorology FROM xian_meteorology
WHERE datetime BETWEEN %s AND %s WHERE datetime BETWEEN %s AND %s
AND is_delete = 0
AND rainfall_1h IS NOT NULL AND rainfall_1h IS NOT NULL
AND CAST(rainfall_1h AS DOUBLE PRECISION) > 0 AND CAST(rainfall_1h AS DOUBLE PRECISION) > 0
GROUP BY lon, lat GROUP BY lon, lat
@@ -259,6 +260,7 @@ class DbnRepository:
ST_SetSRID(ST_MakePoint(%s, %s), 4326) ST_SetSRID(ST_MakePoint(%s, %s), 4326)
) as dist ) as dist
FROM xian_meteorology FROM xian_meteorology
WHERE is_delete = 0
) t ) t
WHERE dist < 50000 WHERE dist < 50000
ORDER BY dist ORDER BY dist
@@ -283,6 +285,7 @@ class DbnRepository:
CAST(rainfall_1h AS DOUBLE PRECISION) as rainfall CAST(rainfall_1h AS DOUBLE PRECISION) as rainfall
FROM xian_meteorology FROM xian_meteorology
WHERE lon = %s AND lat = %s WHERE lon = %s AND lat = %s
AND is_delete = 0
AND datetime BETWEEN %s AND %s AND datetime BETWEEN %s AND %s
ORDER BY datetime DESC ORDER BY datetime DESC
""" """
@@ -326,7 +329,7 @@ class DbnRepository:
"""一次性加载所有气象站点坐标到内存(188个站点,约2KB)""" """一次性加载所有气象站点坐标到内存(188个站点,约2KB)"""
if cls._cached_stations is not None: if cls._cached_stations is not None:
return cls._cached_stations return cls._cached_stations
sql = "SELECT DISTINCT lon, lat FROM xian_meteorology" sql = "SELECT DISTINCT lon, lat FROM xian_meteorology WHERE is_delete = 0"
cls._cached_stations = db_helper.execute_query(sql) cls._cached_stations = db_helper.execute_query(sql)
logger.info(f"已缓存 {len(cls._cached_stations)} 个气象站点坐标") logger.info(f"已缓存 {len(cls._cached_stations)} 个气象站点坐标")
return cls._cached_stations return cls._cached_stations
@@ -407,6 +410,7 @@ class DbnRepository:
SELECT lon, lat, datetime, CAST(rainfall_1h AS DOUBLE PRECISION) as rainfall SELECT lon, lat, datetime, CAST(rainfall_1h AS DOUBLE PRECISION) as rainfall
FROM xian_meteorology FROM xian_meteorology
WHERE (lon, lat) IN ({placeholders}) WHERE (lon, lat) IN ({placeholders})
AND is_delete = 0
AND datetime BETWEEN %s AND %s AND datetime BETWEEN %s AND %s
ORDER BY lon, lat, datetime DESC ORDER BY lon, lat, datetime DESC
""" """
@@ -688,6 +692,21 @@ class DbnRepository:
return float(result['aspect']) return float(result['aspect'])
return None return None
@staticmethod
def get_inference_result(inference_id: int) -> Optional[Dict[str, Any]]:
"""
根据ID获取推理结果
Returns:
{id, name, event_type, occurred_time, operation_type, condition, result}
"""
sql = """
SELECT id, name, event_type, occurred_time, operation_type, condition, result
FROM xian_inference_result
WHERE id = %s AND is_delete = 0
"""
return db_helper.execute_query_one(sql, (inference_id,))
@staticmethod @staticmethod
def save_inference_result(disaster_name: str, event_type: str, occurred_time, operation_type: str, def save_inference_result(disaster_name: str, event_type: str, occurred_time, operation_type: str,
condition: dict, result: list) -> int: condition: dict, result: list) -> int:
+1 -1
View File
@@ -12,7 +12,7 @@ class QgisRepository:
sql = """ sql = """
SELECT id, name, event_type, occurred_time, condition SELECT id, name, event_type, occurred_time, condition
FROM xian_inference_result FROM xian_inference_result
WHERE id = %s WHERE id = %s AND is_delete = 0
""" """
rows = db_helper.execute_query(sql, (inference_id,)) rows = db_helper.execute_query(sql, (inference_id,))
if not rows: if not rows:
+97
View File
@@ -28,6 +28,7 @@ class RainfallRepository:
SELECT max(id) as max_id SELECT max(id) as max_id
FROM xian_meteorology FROM xian_meteorology
WHERE datetime BETWEEN %s AND %s WHERE datetime BETWEEN %s AND %s
AND is_delete = 0
""" """
result = db_helper.execute_query_one(sql, (start_time, end_time)) result = db_helper.execute_query_one(sql, (start_time, end_time))
@@ -58,6 +59,7 @@ class RainfallRepository:
CAST(rainfall_1h AS DOUBLE PRECISION) as rainfall_1h CAST(rainfall_1h AS DOUBLE PRECISION) as rainfall_1h
FROM xian_meteorology FROM xian_meteorology
WHERE datetime BETWEEN %s AND %s WHERE datetime BETWEEN %s AND %s
AND is_delete = 0
ORDER BY lon, lat, datetime DESC ORDER BY lon, lat, datetime DESC
""" """
@@ -100,5 +102,100 @@ class RainfallRepository:
return station_data return station_data
def get_district_rainfall_summary(self, query_time, region_code: Optional[str] = None) -> List[Dict[str, Any]]:
"""
按行政区聚合降雨统计(取区内最大站点值)
Args:
query_time: 查询时间
region_code: 行政区划代码,不传则返回所有区
Returns:
[{district_name, district_code, rainfall, duration_hours}, ...]
"""
start_time, end_time = time_converter.to_db_time_range(query_time, hours=72)
# 一次查询:area_code + lon + lat + rainfall_1h
sql = """
SELECT area_code, lon, lat, datetime,
CAST(rainfall_1h AS DOUBLE PRECISION) as rainfall_1h
FROM xian_meteorology
WHERE datetime BETWEEN %s AND %s
AND is_delete = 0
ORDER BY area_code, lon, lat, datetime DESC
"""
rows = db_helper.execute_query(sql, (start_time, end_time))
if not rows:
return []
# 按站点分组,计算 accum_rain + duration_hours(复用现有算法)
from itertools import groupby
station_stats: Dict[tuple, Dict[str, Any]] = {} # (area_code, lon, lat) → stats
for (area_code, lon, lat), group in groupby(rows,
key=lambda r: (r['area_code'], r['lon'], r['lat'])):
accum_rain = 0.0
duration_hours = 0
consecutive_no_rain = 0
for row in group:
rainfall = float(row['rainfall_1h']) if row['rainfall_1h'] else 0.0
if rainfall > 0:
accum_rain += rainfall
duration_hours += 1
consecutive_no_rain = 0
else:
consecutive_no_rain += 1
if consecutive_no_rain >= 3:
break
if accum_rain > 0:
duration_hours += 1
key = (area_code, lon, lat)
station_stats[key] = {
'area_code': area_code,
'accum_rain': accum_rain,
'duration_hours': duration_hours
}
# 按 area_code 聚合:取区内最大降雨站点
district_max: Dict[str, Dict[str, Any]] = {}
for key, stats in station_stats.items():
code = stats['area_code']
if region_code and code != region_code:
continue
if code not in district_max or stats['accum_rain'] > district_max[code]['rainfall']:
district_max[code] = {
'district_code': code,
'rainfall': round(stats['accum_rain'], 1),
'duration_hours': stats['duration_hours']
}
if not district_max:
return []
# 查 xian_district 获取名称
codes = list(district_max.keys())
placeholders = ', '.join(['%s'] * len(codes))
sql = f"SELECT code, name FROM xian_district WHERE code IN ({placeholders}) AND is_delete = 0"
district_rows = db_helper.execute_query(sql, tuple(codes))
code_to_name = {r['code']: r['name'] for r in district_rows}
result = []
for code, info in district_max.items():
name = code_to_name.get(code)
if name is None:
continue # 跳过 xian_district 中不存在的代码
result.append({
'district_name': name,
'district_code': code,
'rainfall': info['rainfall'],
'duration_hours': info['duration_hours']
})
# 按名称排序
result.sort(key=lambda x: x['district_name'])
return result
# 创建全局实例 # 创建全局实例
rainfall_repository = RainfallRepository() rainfall_repository = RainfallRepository()
+47 -7
View File
@@ -3,7 +3,7 @@ API 请求/响应数据模型
""" """
from datetime import datetime from datetime import datetime
from typing import List, Optional, Dict, Any from typing import List, Optional, Dict, Any
from pydantic import BaseModel, Field from pydantic import BaseModel, Field, model_validator
# ============================================================ # ============================================================
@@ -11,19 +11,35 @@ from pydantic import BaseModel, Field
# ============================================================ # ============================================================
class RainfallPredictRequest(BaseModel): class RainfallPredictRequest(BaseModel):
"""暴雨灾害链预测请求""" """
暴雨灾害链预测请求
参数规则(二选一):
1. 自动推演模式:rainfall、duration、region_code 全部不传 → 从气象表自动获取,不限区域
2. 指定条件模式:rainfall、duration、region_code 全部传入 → 按指定条件预测
"""
disaster_name: str = Field(min_length=1, max_length=255) disaster_name: str = Field(min_length=1, max_length=255)
point_ids: Optional[List[int]] = Field(None, max_length=500, point_ids: Optional[List[int]] = Field(None, max_length=500,
description="点位ID列表,不传则查询所有点") description="点位ID列表,不传则查询所有点")
region_code: Optional[str] = Field(None, description="行政区划代码(如 '610104',不传则不限区域") region_code: Optional[str] = Field(None, description="行政区划代码(如 '610104'")
rainfall: Optional[float] = Field(None, ge=0, rainfall: Optional[float] = Field(None, ge=0, description="累计降雨量(mm)")
description="累计降雨量(mm),不传则从气象表自动获取") duration: Optional[float] = Field(None, ge=0, description="降雨持续时间(h)")
duration: Optional[float] = Field(None, ge=0,
description="降雨持续时间(h),不传则从气象表自动获取")
occurred_time: Optional[datetime] = Field(None, description="事件发生时间,不传则为当前时间") occurred_time: Optional[datetime] = Field(None, description="事件发生时间,不传则为当前时间")
operation_type: str = Field("模拟", min_length=1, max_length=50, operation_type: str = Field("模拟", min_length=1, max_length=50,
description="操作类型(如 '模拟', '实时监测', '应急评估'") description="操作类型(如 '模拟', '实时监测', '应急评估'")
@model_validator(mode='after')
def validate_mode_exclusivity(self):
"""校验 rainfall / duration / region_code 必须要么全不传,要么全传"""
trio = (self.rainfall, self.duration, self.region_code)
none_count = sum(1 for v in trio if v is None)
if none_count not in (0, 3):
raise ValueError(
"rainfall、duration、region_code 必须全部传入或全部不传,"
"不允许只传部分参数"
)
return self
# ============================================================ # ============================================================
# 地震预测 # 地震预测
@@ -86,3 +102,27 @@ class HealthResponse(BaseModel):
status: str = "ok" status: str = "ok"
rainfall_model_loaded: bool = False rainfall_model_loaded: bool = False
earthquake_model_loaded: bool = False earthquake_model_loaded: bool = False
# ============================================================
# 各区降雨概况
# ============================================================
class DistrictSummaryRequest(BaseModel):
"""各区降雨概况请求"""
inference_id: int = Field(..., ge=1, description="推理结果IDxian_inference_result.id")
class DistrictSummaryItem(BaseModel):
"""单个区的降雨概况"""
district_name: str = Field(..., description="行政区名称(如 '碑林区'")
district_code: str = Field(..., description="行政区划代码(如 '610103'")
rainfall: float = Field(..., description="累计降雨量(mm)")
duration_hours: float = Field(..., description="持续降雨时间(h)")
class DistrictSummaryResponse(BaseModel):
"""各区降雨概况响应"""
code: int = Field(200, description="状态码")
message: str = Field("success", description="提示信息")
data: Optional[List[DistrictSummaryItem]] = Field(None, description="各区降雨概况列表")