统一计算逻辑以及时间转换逻辑

This commit is contained in:
wzy-warehouse
2026-06-12 14:53:35 +08:00
parent a07c5a1107
commit 316177c2ba
5 changed files with 215 additions and 49 deletions
+3 -1
View File
@@ -3,6 +3,7 @@
""" """
import sys import sys
from pathlib import Path from pathlib import Path
from datetime import datetime
from app.core.env_checker import check_environment from app.core.env_checker import check_environment
from app.core.venv_manager import check_virtualenv from app.core.venv_manager import check_virtualenv
@@ -61,7 +62,7 @@ class AppLauncher:
return return
# 以下代码仅在虚拟环境中执行 # 以下代码仅在虚拟环境中执行
# 检查安装依赖(只执行一次) # 检查安装依赖
check_dependencies(self.project_root) check_dependencies(self.project_root)
# 启动应用 # 启动应用
@@ -104,6 +105,7 @@ def start():
# 启动降雨站点监测 # 启动降雨站点监测
logger.info("启动降雨站点监测服务...") logger.info("启动降雨站点监测服务...")
rainfall_manager.monitoring_rainfall_station_id('2025-09-16 20:00:00') rainfall_manager.monitoring_rainfall_station_id('2025-09-16 20:00:00')
# 阻塞主线程,防止程序立即退出 # 阻塞主线程,防止程序立即退出
+23 -19
View File
@@ -3,9 +3,10 @@
负责从 xian_risk_factors、xian_meteorology 等表获取数据 负责从 xian_risk_factors、xian_meteorology 等表获取数据
""" """
import math import math
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any, Union
from datetime import datetime from datetime import datetime
from app.utils.db_helper import db_helper from app.utils.db_helper import db_helper
from app.utils.time_converter import time_converter
from app.config.paths import get_logger from app.config.paths import get_logger
logger = get_logger("dbn") logger = get_logger("dbn")
@@ -166,20 +167,23 @@ class DbnRepository:
@staticmethod @staticmethod
def get_nearest_station_rainfall(lon: float, lat: float, def get_nearest_station_rainfall(lon: float, lat: float,
query_time: Optional[datetime] = None) -> Dict[str, Any]: query_time: Optional[Union[datetime, str]] = None) -> Dict[str, Any]:
""" """
获取最近雨量站的降雨数据 获取最近雨量站的降雨数据
Args: Args:
lon: 经度 lon: 经度
lat: 纬度 lat: 纬度
query_time: 查询时间,若未提供则取当前时间 query_time: 查询时间,若未提供则取当前时间。支持datetime对象或标准时间字符串(如'2025-07-04 20:00:00'
Returns: Returns:
降雨数据 降雨数据
""" """
if query_time is None: if query_time is None:
query_time = datetime.now() query_time = datetime.now()
# 获取时间范围(24小时窗口)
start_time, end_time = time_converter.to_db_time_range(query_time, hours=24)
# noinspection SqlNoDataSourceInspection # noinspection SqlNoDataSourceInspection
sql = """ sql = """
@@ -190,9 +194,7 @@ class DbnRepository:
SUM(CAST(rainfall_1h AS DOUBLE PRECISION)) as total_rainfall, SUM(CAST(rainfall_1h AS DOUBLE PRECISION)) as total_rainfall,
COUNT(*) as record_count COUNT(*) as record_count
FROM xian_meteorology FROM xian_meteorology
WHERE datetime BETWEEN WHERE datetime BETWEEN %s AND %s
CAST(EXTRACT(EPOCH FROM (%s::timestamp - INTERVAL '24 hours')) AS BIGINT)
AND CAST(EXTRACT(EPOCH FROM %s::timestamp) AS BIGINT)
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
@@ -210,7 +212,7 @@ class DbnRepository:
ORDER BY distance ORDER BY distance
LIMIT 1 LIMIT 1
""" """
result = db_helper.execute_query_one(sql, (query_time, query_time, lon, lat)) result = db_helper.execute_query_one(sql, (start_time, end_time, lon, lat))
if result: if result:
return { return {
@@ -231,14 +233,14 @@ class DbnRepository:
@staticmethod @staticmethod
def get_rainfall_data_with_duration(lon: float, lat: float, def get_rainfall_data_with_duration(lon: float, lat: float,
query_time: Optional[datetime] = None) -> Dict[str, Any]: query_time: Optional[Union[datetime, str]] = None) -> Dict[str, Any]:
""" """
获取降雨数据,包括累计降雨量和持续时间 获取降雨数据,包括累计降雨量和持续时间
Args: Args:
lon: 经度 lon: 经度
lat: 纬度 lat: 纬度
query_time: 查询时间,若未提供则取当前时间 query_time: 查询时间,若未提供则取当前时间。支持datetime对象或标准时间字符串(如'2025-07-04 20:00:00'
Returns: Returns:
降雨数据:{accum_rain, duration_hours, rain_intensity} 降雨数据:{accum_rain, duration_hours, rain_intensity}
@@ -270,6 +272,9 @@ class DbnRepository:
station_lon = station['lon'] station_lon = station['lon']
station_lat = station['lat'] station_lat = station['lat']
# 获取时间范围(72小时窗口)
start_time, end_time = time_converter.to_db_time_range(query_time, hours=72)
# 查询该站点的降雨时序数据 # 查询该站点的降雨时序数据
# noinspection SqlNoDataSourceInspection # noinspection SqlNoDataSourceInspection
sql = """ sql = """
@@ -278,12 +283,10 @@ 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 datetime BETWEEN AND datetime BETWEEN %s AND %s
CAST(EXTRACT(EPOCH FROM (%s::timestamp - INTERVAL '72 hours')) AS BIGINT)
AND CAST(EXTRACT(EPOCH FROM %s::timestamp) AS BIGINT)
ORDER BY datetime DESC ORDER BY datetime DESC
""" """
results = db_helper.execute_query(sql, (station_lon, station_lat, query_time, query_time)) results = db_helper.execute_query(sql, (station_lon, station_lat, start_time, end_time))
if not results: if not results:
return {'accum_rain': 0.0, 'duration_hours': 0, 'rain_intensity': 0.0} return {'accum_rain': 0.0, 'duration_hours': 0, 'rain_intensity': 0.0}
@@ -357,13 +360,13 @@ class DbnRepository:
@classmethod @classmethod
def get_rainfall_data_batch(cls, points: List[Dict[str, Any]], def get_rainfall_data_batch(cls, points: List[Dict[str, Any]],
query_time: Optional[datetime] = None) -> Dict[str, Dict[str, Any]]: query_time: Optional[Union[datetime, str]] = None) -> Dict[str, Dict[str, Any]]:
""" """
批量获取多个点的降雨数据(2次DB查询替代 N×2次) 批量获取多个点的降雨数据(2次DB查询替代 N×2次)
Args: Args:
points: 预测点列表,每个含 {'id': str, 'lon': float, 'lat': float} points: 预测点列表,每个含 {'id': str, 'lon': float, 'lat': float}
query_time: 查询时间 query_time: 查询时间,支持datetime对象或标准时间字符串(如'2025-07-04 20:00:00'
Returns: Returns:
{point_id: {accum_rain, duration_hours, rain_intensity}} {point_id: {accum_rain, duration_hours, rain_intensity}}
@@ -394,16 +397,17 @@ class DbnRepository:
params: List[Any] = [] params: List[Any] = []
for slon, slat in station_keys: for slon, slat in station_keys:
params.extend([slon, slat]) params.extend([slon, slat])
params.extend([query_time, query_time])
# 获取时间范围(72小时窗口)
start_time, end_time = time_converter.to_db_time_range(query_time, hours=72)
params.extend([start_time, end_time])
# noinspection SqlNoDataSourceInspection # noinspection SqlNoDataSourceInspection
sql = f""" sql = f"""
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 datetime BETWEEN AND datetime BETWEEN %s AND %s
CAST(EXTRACT(EPOCH FROM (%s::timestamp - INTERVAL '72 hours')) AS BIGINT)
AND CAST(EXTRACT(EPOCH FROM %s::timestamp) AS BIGINT)
ORDER BY lon, lat, datetime DESC ORDER BY lon, lat, datetime DESC
""" """
rows = db_helper.execute_query(sql, tuple(params)) rows = db_helper.execute_query(sql, tuple(params))
+53 -28
View File
@@ -2,74 +2,99 @@
降雨数据仓库 降雨数据仓库
负责数据库查询操作 负责数据库查询操作
""" """
from typing import Optional, List, Dict, Any from typing import Optional, List, Dict, Any, Union
from datetime import datetime from datetime import datetime
from app.utils.db_helper import db_helper from app.utils.db_helper import db_helper
from app.utils.time_converter import time_converter
class RainfallRepository: class RainfallRepository:
"""降雨数据仓库""" """降雨数据仓库"""
def get_max_rainfall_id(self, query_time: datetime) -> Optional[int]: def get_max_rainfall_id(self, query_time: Union[datetime, str]) -> Optional[int]:
""" """
查询数据库中指定时间窗口内的最大ID 查询数据库中指定时间窗口内的最大ID72小时窗口)
Args: Args:
query_time: 查询时间 query_time: 查询时间(datetime对象或标准时间字符串,如'2025-07-04 20:00:00'
Returns: Returns:
最大ID,如果没有数据则返回None 最大ID,如果没有数据则返回None
""" """
# 获取时间范围
start_time, end_time = time_converter.to_db_time_range(query_time, hours=72)
sql = """ sql = """
SELECT max(id) as max_id SELECT max(id) as max_id
FROM xian_meteorology FROM xian_meteorology
WHERE datetime BETWEEN ( WHERE datetime BETWEEN %s AND %s
to_char(%s::timestamp - interval '12 hours', 'YYYYMMDDHH24MISS')
)::bigint AND (
to_char(%s::timestamp, 'YYYYMMDDHH24MISS')
)::bigint
""" """
result = db_helper.execute_query_one(sql, (query_time, query_time)) result = db_helper.execute_query_one(sql, (start_time, end_time))
if result and result.get('max_id'): if result and result.get('max_id'):
return int(result['max_id']) return int(result['max_id'])
return None return None
def get_rainfall_stations_data(self, query_time: datetime) -> List[Dict[str, Any]]: def get_rainfall_stations_data(self, query_time: Union[datetime, str]) -> List[Dict[str, Any]]:
""" """
查询雨量站点降雨数据 查询雨量站点降雨数据
Args: Args:
query_time: 查询时间 query_time: 查询时间(datetime对象或标准时间字符串,如'2025-07-04 20:00:00'
Returns: Returns:
站点数据列表,每个元素包含lon, lat, rainfall 站点数据列表,每个元素包含lon, lat, rainfall, duration_hours
""" """
# 获取时间范围
start_time, end_time = time_converter.to_db_time_range(query_time, hours=72)
# 查询72小时内的降雨时序数据
sql = """ sql = """
SELECT SELECT
lon, lon,
lat, lat,
SUM(rainfall_1h::numeric) AS rainfall datetime,
CAST(rainfall_1h AS DOUBLE PRECISION) as rainfall_1h
FROM xian_meteorology FROM xian_meteorology
WHERE datetime BETWEEN ( WHERE datetime BETWEEN %s AND %s
to_char(%s::timestamp - interval '12 hours', 'YYYYMMDDHH24MISS') ORDER BY lon, lat, datetime DESC
)::bigint AND (
to_char(%s::timestamp, 'YYYYMMDDHH24MISS')
)::bigint
GROUP BY lon, lat
""" """
results = db_helper.execute_query(sql, (start_time, end_time))
results = db_helper.execute_query(sql, (query_time, query_time)) if not results:
return []
# 按站点分组处理
from itertools import groupby
# 转换数据格式
station_data = [] station_data = []
for row in results: for (lon, lat), group in groupby(results, key=lambda r: (r['lon'], r['lat'])):
if row.get('lon') and row.get('lat'): accum_rain = 0.0
duration_hours = 0
consecutive_no_rain = 0
# 应用"连续3小时无雨截断"规则
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 # 连续3小时无雨,停止累加
if accum_rain > 0:
duration_hours += 1
if accum_rain > 0 or duration_hours > 0:
station_data.append({ station_data.append({
'lon': float(row['lon']), 'lon': float(lon),
'lat': float(row['lat']), 'lat': float(lat),
'rainfall': float(row['rainfall']) if row.get('rainfall') else 0.0 'rainfall': accum_rain, # 累计降雨量
'duration_hours': duration_hours # 持续时间
}) })
return station_data return station_data
+9 -1
View File
@@ -120,6 +120,10 @@ class RainfallGridService:
def interpolate_rainfall(self, station_data: List[Dict[str, Any]]) -> Dict[str, Any]: def interpolate_rainfall(self, station_data: List[Dict[str, Any]]) -> Dict[str, Any]:
""" """
使用优化的反距离权重法(IDW)进行降雨插值 使用优化的反距离权重法(IDW)进行降雨插值
注意:station_data 现在包含 'rainfall'(累计降雨量)和 'duration_hours'(持续时间)
与DBN推演使用相同的降雨量计算逻辑(72小时回溯 + 3小时无雨截断)
改进: 改进:
1. 高斯核衰减替代简单幂律 1. 高斯核衰减替代简单幂律
2. 自适应距离阈值 2. 自适应距离阈值
@@ -127,7 +131,11 @@ class RainfallGridService:
4. 高斯平滑减少突变 4. 高斯平滑减少突变
Args: Args:
station_data: 站点数据列表 station_data: 站点数据列表,格式:
[
{'lon': x, 'lat': y, 'rainfall': z, 'duration_hours': h},
...
]
Returns: Returns:
插值结果字典 插值结果字典
+127
View File
@@ -0,0 +1,127 @@
"""
时间转换工具
统一处理标准时间输入与数据库时间格式的转换
"""
from datetime import datetime, timedelta
from typing import Union
class TimeConverter:
"""时间转换器"""
# 数据库时间格式:YYYYMMDDHHmmss(如 20250907070000
DB_FORMAT = '%Y%m%d%H%M%S'
# 标准输入格式:2025-07-04 20:00:00 或 2025-07-04T20:00:00
INPUT_FORMATS = [
'%Y-%m-%d %H:%M:%S', # 2025-07-04 20:00:00
'%Y-%m-%dT%H:%M:%S', # 2025-07-04T20:00:00
'%Y-%m-%d %H:%M', # 2025-07-04 20:00
'%Y-%m-%dT%H:%M', # 2025-07-04T20:00
]
@staticmethod
def parse_input_time(time_str: str) -> datetime:
"""
解析标准时间输入字符串
支持格式:
- 2025-07-04 20:00:00
- 2025-07-04T20:00:00
- 2025-07-04 20:00
- 2025-07-04T20:00
Args:
time_str: 时间字符串
Returns:
datetime对象
Raises:
ValueError: 时间格式无法识别
"""
if not time_str:
raise ValueError("时间字符串不能为空")
# 尝试所有支持的格式
for fmt in TimeConverter.INPUT_FORMATS:
try:
return datetime.strptime(time_str, fmt)
except ValueError:
continue
raise ValueError(f"无法识别的时间格式: {time_str},支持格式: {TimeConverter.INPUT_FORMATS}")
@staticmethod
def to_db_format(dt: datetime) -> int:
"""
将datetime对象转换为数据库时间格式(YYYYMMDDHHmmss整数)
Args:
dt: datetime对象
Returns:
YYYYMMDDHHmmss格式的整数
"""
return int(dt.strftime(TimeConverter.DB_FORMAT))
@staticmethod
def from_db_format(db_time: int) -> datetime:
"""
将数据库时间格式(YYYYMMDDHHmmss整数)转换为datetime对象
Args:
db_time: YYYYMMDDHHmmss格式的整数
Returns:
datetime对象
"""
return datetime.strptime(str(db_time), TimeConverter.DB_FORMAT)
@staticmethod
def to_db_time_range(query_time: Union[datetime, str], hours: int = 72) -> tuple:
"""
将查询时间转换为数据库时间范围
Args:
query_time: 查询时间(datetime对象或标准时间字符串)
hours: 时间窗口(小时),默认72小时
Returns:
(start_time, end_time) 元组,均为YYYYMMDDHHmmss格式的整数
"""
# 如果是字符串,先解析为datetime
if isinstance(query_time, str):
query_time = TimeConverter.parse_input_time(query_time)
# 计算时间范围
end_time = query_time
start_time = query_time - timedelta(hours=hours)
# 转换为数据库格式
return (TimeConverter.to_db_format(start_time), TimeConverter.to_db_format(end_time))
@staticmethod
def get_sql_time_condition(column: str = 'datetime',
query_time: Union[datetime, str] = None,
hours: int = 72) -> str:
"""
生成SQL时间条件片段
Args:
column: 时间列名,默认'datetime'
query_time: 查询时间
hours: 时间窗口(小时)
Returns:
SQL条件字符串,如 "datetime BETWEEN 20250904070000 AND 20250907070000"
"""
if query_time is None:
query_time = datetime.now()
start, end = TimeConverter.to_db_time_range(query_time, hours)
return f"{column} BETWEEN {start} AND {end}"
# 创建全局实例
time_converter = TimeConverter()