""" 数据库查询模块 负责从 xian_risk_factors、xian_meteorology 等表获取数据 """ import math from typing import Optional, List, Dict, Any from datetime import datetime from app.utils.db_helper import db_helper from app.config.paths import get_logger logger = get_logger("dbn") class DbnRepository: """数据库查询类 - 所有DBN相关查询统一管理""" # ==================== 风险因子查询 ==================== @staticmethod def get_all_points(region_code: Optional[str] = None) -> List[Dict[str, Any]]: """ 获取所有隐患点和风险点(从 xian_risk_factors 表) Args: region_code: 行政区划代码(如 '610104'),可选,匹配隐患点.county_code 和风险点.unit_code Returns: 点列表,每个元素包含:id, source_id, source_type, lon, lat, static_factors """ if region_code: # 通过源表的行政区划代码筛选 sql = """ SELECT rf.id, rf.source_id, rf.source_type, rf.lon, rf.lat, rf.static_factors FROM xian_risk_factors rf WHERE rf.is_delete = 0 AND ( (rf.source_type = 1 AND rf.source_id IN ( SELECT id FROM xian_hidden_danger_spots WHERE county_id = %s AND is_delete = 0 )) OR (rf.source_type = 2 AND rf.source_id IN ( SELECT id FROM xian_risk_spots WHERE unit_code = %s AND is_delete = 0 )) ) """ params = (region_code, region_code) else: sql = """ SELECT id, source_id, source_type, lon, lat, static_factors FROM xian_risk_factors WHERE is_delete = 0 """ params = None results = db_helper.execute_query(sql, params) points = [] for row in results: points.append({ 'id': row['id'], 'source_id': row['source_id'], 'source_type': row['source_type'], 'lon': float(row['lon']) if row['lon'] else None, 'lat': float(row['lat']) if row['lat'] else None, 'static_factors': row.get('static_factors') or {} }) return points @staticmethod def get_point_by_id(point_id: int) -> Optional[Dict[str, Any]]: """ 根据ID获取单个点信息 Args: point_id: xian_risk_factors 表的 ID Returns: 点信息 """ sql = """ SELECT rf.id, rf.source_id, rf.source_type, rf.lon, rf.lat, rf.static_factors FROM xian_risk_factors rf WHERE rf.id = %s AND rf.is_delete = 0 """ result = db_helper.execute_query_one(sql, (point_id,)) if not result: return None return { 'id': result['id'], 'source_id': result['source_id'], 'source_type': result['source_type'], 'lon': float(result['lon']) if result['lon'] else None, 'lat': float(result['lat']) if result['lat'] else None, 'static_factors': result.get('static_factors') or {} } @staticmethod def get_points_by_ids(point_ids: List[int]) -> List[Dict[str, Any]]: """ 批量获取点信息 Args: point_ids: 点ID列表 Returns: 点信息列表 """ if not point_ids: return [] placeholders = ','.join(['%s'] * len(point_ids)) sql = f""" SELECT rf.id, rf.source_id, rf.source_type, rf.lon, rf.lat, rf.static_factors FROM xian_risk_factors rf WHERE rf.id IN ({placeholders}) AND rf.is_delete = 0 """ results = db_helper.execute_query(sql, tuple(point_ids)) return [{ 'id': row['id'], 'source_id': row['source_id'], 'source_type': row['source_type'], 'lon': float(row['lon']) if row['lon'] else None, 'lat': float(row['lat']) if row['lat'] else None, 'static_factors': row.get('static_factors') or {} } for row in results] @staticmethod def get_static_factors(point_id: int) -> Dict[str, Any]: """ 获取点的静态因子数据 Args: point_id: xian_risk_factors 表的 ID Returns: 静态因子数据 """ sql = """ SELECT static_factors FROM xian_risk_factors WHERE id = %s AND is_delete = 0 """ result = db_helper.execute_query_one(sql, (point_id,)) if result and result.get('static_factors'): return result['static_factors'] return {} # ==================== 降雨数据查询 ==================== @staticmethod def get_nearest_station_rainfall(lon: float, lat: float, query_time: Optional[datetime] = None) -> Dict[str, Any]: """ 获取最近雨量站的降雨数据 Args: lon: 经度 lat: 纬度 query_time: 查询时间,若未提供则取当前时间 Returns: 降雨数据 """ if query_time is None: query_time = datetime.now() # noinspection SqlNoDataSourceInspection sql = """ WITH station_data AS ( SELECT lon, lat, SUM(CAST(rainfall_1h AS DOUBLE PRECISION)) as total_rainfall, COUNT(*) as record_count FROM xian_meteorology WHERE datetime BETWEEN 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 CAST(rainfall_1h AS DOUBLE PRECISION) > 0 GROUP BY lon, lat ) SELECT lon, lat, total_rainfall, record_count, ST_DistanceSphere( ST_SetSRID(ST_MakePoint(lon, lat), 4326), ST_SetSRID(ST_MakePoint(%s, %s), 4326) ) as distance FROM station_data ORDER BY distance LIMIT 1 """ result = db_helper.execute_query_one(sql, (query_time, query_time, lon, lat)) if result: return { 'rainfall': float(result['total_rainfall']) if result['total_rainfall'] else 0.0, 'record_count': int(result['record_count']) if result['record_count'] else 0, 'distance': float(result['distance']) if result['distance'] else 0.0, 'station_lon': float(result['lon']) if result['lon'] else None, 'station_lat': float(result['lat']) if result['lat'] else None } else: return { 'rainfall': 0.0, 'record_count': 0, 'distance': 0.0, 'station_lon': None, 'station_lat': None } @staticmethod def get_rainfall_data_with_duration(lon: float, lat: float, query_time: Optional[datetime] = None) -> Dict[str, Any]: """ 获取降雨数据,包括累计降雨量和持续时间 Args: lon: 经度 lat: 纬度 query_time: 查询时间,若未提供则取当前时间 Returns: 降雨数据:{accum_rain, duration_hours, rain_intensity} """ if query_time is None: query_time = datetime.now() # 查找最近的雨量站 # noinspection SqlNoDataSourceInspection sql = """ SELECT lon, lat, dist FROM ( SELECT DISTINCT lon, lat, ST_DistanceSphere( ST_SetSRID(ST_MakePoint(lon, lat), 4326), ST_SetSRID(ST_MakePoint(%s, %s), 4326) ) as dist FROM xian_meteorology ) t WHERE dist < 50000 ORDER BY dist LIMIT 1 """ station = db_helper.execute_query_one(sql, (lon, lat)) if not station: return {'accum_rain': 0.0, 'duration_hours': 0, 'rain_intensity': 0.0} station_lon = station['lon'] station_lat = station['lat'] # 查询该站点的降雨时序数据 # noinspection SqlNoDataSourceInspection sql = """ SELECT datetime, CAST(rainfall_1h AS DOUBLE PRECISION) as rainfall FROM xian_meteorology WHERE lon = %s AND lat = %s AND datetime BETWEEN CAST(EXTRACT(EPOCH FROM (%s::timestamp - INTERVAL '72 hours')) AS BIGINT) AND CAST(EXTRACT(EPOCH FROM %s::timestamp) AS BIGINT) ORDER BY datetime DESC """ results = db_helper.execute_query(sql, (station_lon, station_lat, query_time, query_time)) if not results: return {'accum_rain': 0.0, 'duration_hours': 0, 'rain_intensity': 0.0} # 计算累计降雨量和持续时间 accum_rain = 0.0 duration_hours = 0 consecutive_no_rain = 0 for row in results: rainfall = float(row['rainfall']) if row['rainfall'] 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 rain_intensity = accum_rain / duration_hours if duration_hours > 0 else 0.0 return { 'accum_rain': accum_rain, 'duration_hours': duration_hours, 'rain_intensity': rain_intensity } # ==================== 空间查询 ==================== @staticmethod def get_nearest_station(lon: float, lat: float, station_type: str = 'meteorology') -> Optional[Dict[str, Any]]: """ 获取最近的气象站点 Args: lon: 经度 lat: 纬度 station_type: 站点类型 Returns: 最近站点信息 """ sql = """ SELECT DISTINCT ON (lon, lat) lon, lat, ST_DistanceSphere( ST_SetSRID(ST_MakePoint(lon, lat), 4326), ST_SetSRID(ST_MakePoint(%s, %s), 4326) ) as distance FROM xian_meteorology WHERE is_delete = 0 ORDER BY lon, lat, distance LIMIT 1 """ result = db_helper.execute_query_one(sql, (lon, lat)) if result: return { 'lon': float(result['lon']), 'lat': float(result['lat']), 'distance': float(result['distance']) } return None @staticmethod def get_distance_to_river(lon: float, lat: float) -> float: """ 计算点到最近河流的距离 Args: lon: 经度 lat: 纬度 Returns: 距离(米) """ sql = """ SELECT MIN(ST_DistanceSphere( ST_SetSRID(ST_MakePoint(%s, %s), 4326), geom )) as min_distance FROM xian_rivers WHERE is_delete = 0 """ result = db_helper.execute_query_one(sql, (lon, lat)) if result and result['min_distance']: return float(result['min_distance']) return 0.0 @staticmethod def get_distance_to_fault(lon: float, lat: float) -> float: """ 计算点到最近断裂带的距离 Args: lon: 经度 lat: 纬度 Returns: 距离(米) """ sql = """ SELECT MIN(ST_DistanceSphere( ST_SetSRID(ST_MakePoint(%s, %s), 4326), geom )) as min_distance FROM xian_fault_lines WHERE is_delete = 0 """ result = db_helper.execute_query_one(sql, (lon, lat)) if result and result['min_distance']: return float(result['min_distance']) return 0.0 @staticmethod def get_pipe_density(lon: float, lat: float, buffer_radius: float = 500.0) -> float: """ 计算点周围缓冲区内的管网密度 Args: lon: 经度 lat: 纬度 buffer_radius: 缓冲区半径(米) Returns: 管网密度(m/m²) """ sql = """ SELECT COALESCE(SUM(ST_Length(wp.geom::geography)), 0) as total_length FROM xian_water_pipe wp WHERE wp.is_delete = 0 AND ST_DWithin( ST_SetSRID(ST_MakePoint(%s, %s), 4326)::geography, wp.geom::geography, %s ) """ result = db_helper.execute_query_one(sql, (lon, lat, buffer_radius)) if result and result['total_length']: total_length = float(result['total_length']) buffer_area = math.pi * buffer_radius ** 2 density = total_length / buffer_area return density return 0.0 @staticmethod def get_river_density(lon: float, lat: float, buffer_radius: float = 1000.0) -> float: """ 计算点周围缓冲区内的河流密度 Args: lon: 经度 lat: 纬度 buffer_radius: 缓冲区半径(米) Returns: 河流密度(m/m²) """ sql = """ SELECT COALESCE(SUM(ST_Length(r.geom::geography)), 0) as total_length FROM xian_rivers r WHERE r.is_delete = 0 AND ST_DWithin( ST_SetSRID(ST_MakePoint(%s, %s), 4326)::geography, r.geom::geography, %s ) """ result = db_helper.execute_query_one(sql, (lon, lat, buffer_radius)) if result and result['total_length']: total_length = float(result['total_length']) buffer_area = math.pi * buffer_radius ** 2 density = total_length / buffer_area return density return 0.0 @staticmethod def get_point_elevation(lon: float, lat: float) -> Optional[float]: """ 获取点的高程值 Args: lon: 经度 lat: 纬度 Returns: 高程值(米) """ sql = """ SELECT ST_Value(rast, ST_SetSRID(ST_MakePoint(%s, %s), 4326)) as elevation FROM xian_dem WHERE ST_Intersects(rast, ST_SetSRID(ST_MakePoint(%s, %s), 4326)) LIMIT 1 """ result = db_helper.execute_query_one(sql, (lon, lat, lon, lat)) if result and result['elevation']: return float(result['elevation']) return None @staticmethod def get_point_slope(lon: float, lat: float) -> Optional[float]: """ 获取点的坡度值 Args: lon: 经度 lat: 纬度 Returns: 坡度值(度) """ sql = """ SELECT ST_Value(rast, ST_SetSRID(ST_MakePoint(%s, %s), 4326)) as slope FROM xian_slope WHERE ST_Intersects(rast, ST_SetSRID(ST_MakePoint(%s, %s), 4326)) LIMIT 1 """ result = db_helper.execute_query_one(sql, (lon, lat, lon, lat)) if result and result['slope']: return float(result['slope']) return None @staticmethod def get_point_aspect(lon: float, lat: float) -> Optional[float]: """ 获取点的坡向值 Args: lon: 经度 lat: 纬度 Returns: 坡向值(度) """ sql = """ SELECT ST_Value(rast, ST_SetSRID(ST_MakePoint(%s, %s), 4326)) as aspect FROM xian_aspect WHERE ST_Intersects(rast, ST_SetSRID(ST_MakePoint(%s, %s), 4326)) LIMIT 1 """ result = db_helper.execute_query_one(sql, (lon, lat, lon, lat)) if result and result['aspect']: return float(result['aspect']) return None # 创建全局实例 dbn_repository = DbnRepository()