From 118dbd18cfde0d56ba9b342beeac0aba3e5b8180 Mon Sep 17 00:00:00 2001
From: wzy-warehouse <18135009705@163.com>
Date: Fri, 12 Jun 2026 09:45:35 +0800
Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E6=9E=84DBN=E6=A8=A1=E5=9E=8B?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
app/api/earthquake.py | 4 +-
app/api/rainfall.py | 4 +-
app/config/dbn/discretization.yaml | 122 +++++++++++---------
app/config/dbn/earthquake_cpt_params.yaml | 4 +-
app/config/dbn/earthquake_dbn_graph.yaml | 10 +-
app/config/dbn/rainfall_cpt_params.yaml | 4 +-
app/config/dbn/rainfall_dbn_graph.yaml | 10 +-
app/core/launcher.py | 9 +-
app/models/dbn/earthquake/earthquake_dbn.py | 6 +-
app/models/dbn/rainfall/rainfall_dbn.py | 6 +-
app/utils/logger.py | 97 ++++++++--------
requirements.txt | 3 +-
12 files changed, 148 insertions(+), 131 deletions(-)
diff --git a/app/api/earthquake.py b/app/api/earthquake.py
index 36e1725..095bb09 100644
--- a/app/api/earthquake.py
+++ b/app/api/earthquake.py
@@ -30,7 +30,7 @@ def _build_prediction_items(results: List[Dict[str, Any]]) -> List[PredictionIte
max_hazard = max(probs, key=probs.get)
items.append(PredictionItem(
- id=r["point_id"],
+ id=r["source_id"], # 使用 source_id(隐患点/风险点ID)而非 xian_risk_factors.id
type=SOURCE_TYPE_MAP.get(r.get("source_type"), "未知"),
probability=round(probs[max_hazard], 4),
level=LEVEL_MAP.get(levels.get(max_hazard, "none"), "无"),
@@ -70,7 +70,7 @@ def _predict_sync(point_ids: Optional[List[int]], region_code: Optional[str],
save_results = [
{
- "point_id": r.get("point_id"),
+ "point_id": r.get("source_id"), # 使用 source_id(隐患点/风险点ID)而非 xian_risk_factors.id
"source_type": r.get("source_type"),
"lon": r.get("lon"),
"lat": r.get("lat"),
diff --git a/app/api/rainfall.py b/app/api/rainfall.py
index e2bf4ea..e163a95 100644
--- a/app/api/rainfall.py
+++ b/app/api/rainfall.py
@@ -31,7 +31,7 @@ def _build_prediction_items(results: List[Dict[str, Any]]) -> List[PredictionIte
max_hazard = max(probs, key=probs.get)
items.append(PredictionItem(
- id=r["point_id"],
+ id=r["source_id"], # 使用 source_id(隐患点/风险点ID)而非 xian_risk_factors.id
type=SOURCE_TYPE_MAP.get(r.get("source_type"), "未知"),
probability=round(probs[max_hazard], 4),
level=LEVEL_MAP.get(levels.get(max_hazard, "none"), "无"),
@@ -73,7 +73,7 @@ def _predict_sync(point_ids: Optional[List[int]], region_code: Optional[str],
}
save_results = [
{
- "point_id": r.get("point_id"),
+ "point_id": r.get("source_id"), # 使用 source_id(隐患点/风险点ID)而非 xian_risk_factors.id
"source_type": r.get("source_type"),
"lon": r.get("lon"),
"lat": r.get("lat"),
diff --git a/app/config/dbn/discretization.yaml b/app/config/dbn/discretization.yaml
index 8237553..41feea5 100644
--- a/app/config/dbn/discretization.yaml
+++ b/app/config/dbn/discretization.yaml
@@ -2,8 +2,8 @@
# 定义所有连续因子的分箱规则
# 包含暴雨灾害链和地震灾害链的全部因子
#
-# 2026-06-06: 基于1201个样本的实际数据分布,采用分位数分箱(等频分箱)
-# 替代原有等宽分箱,使每个区间样本量更均匀
+# 2026-06-11: 基于1365个样本(796隐患点+569风险点)的实际数据分布
+# 连续因子采用分位数分箱(等频分箱),分类因子基于实际编码映射
# ============================================
# 暴雨触发层离散化规则(保持气象标准不变)
@@ -62,17 +62,17 @@ seismic_intensity:
elevation:
description: "高程"
unit: "m"
- # 数据: [356, 1934], 均值764.3±317.89, 偏度0.973
- # 分位数: [356, 470, 624, 792, 1016, 1934]
- bins: [356, 470, 624, 792, 1016, 1934]
+ # 数据: [354, 1926], 均值789.36±325.64
+ # 分位数: [354, 482, 637, 817, 1074, 1926]
+ bins: [354, 482, 637, 817, 1074, 1926]
labels: [very_low, low, medium, high, very_high]
slope:
description: "坡度"
unit: "度"
- # 数据: [0.11, 47.14], 均值9.42±8.57, 偏度1.433
- # 分位数: [0.11, 1.81, 5.43, 9.48, 15.13, 47.14]
- bins: [0.11, 1.81, 5.43, 9.48, 15.13, 47.14]
+ # 数据: [0.0, 47.0], 均值10.46±9.12
+ # 分位数: [0.0, 2.0, 6.0, 11.0, 17.0, 47.0]
+ bins: [0.0, 2.0, 6.0, 11.0, 17.0, 47.0]
labels: [very_low, low, medium, high, very_high]
aspect:
@@ -85,13 +85,20 @@ aspect:
soil_type:
description: "土壤分类(中国土壤分类系统)"
- unit: "分类代码"
+ unit: "3位数编码"
+ # 中国土壤分类系统25个亚类,本数据库出现8种
+ # 编码规则:1xx淋溶土、2xx钙层/均腐土、4xx初育土
mapping:
- 0: ultisol # 老成土
- 6: entisol # 初育土
- 11: fluvo_aquic # 潮土
- 18: yellow_brown # 黄棕壤
- default: entisol
+ 110: brown_soil # 棕壤(淋溶土,秦岭北麓山地)
+ 120: brown_soil # 暗棕壤(淋溶土,高海拔山地)
+ 130: yellow_brown # 黄棕壤(淋溶土,暖温带过渡区)
+ 150: yellow_brown # 黄褐土(淋溶土,黄土塬区)
+ 210: cinnamon # 褐土(钙层土,黄土塬区主要旱作土壤)
+ 240: black_lu # 黑垆土(均腐土,古土壤残余)
+ 410: alluvial # 新积土(初育土,渭河冲积平原)
+ 420: aeolian # 风沙土(初育土,风积沙质土壤)
+ 255: unknown # GIS背景值(1个样本)
+ default: cinnamon # 褐土占比最大(38.8%),作为默认值
lithology:
description: "岩性(中国地质分类)"
@@ -105,18 +112,19 @@ lithology:
11: mixed_clastic # 混合碎屑沉积岩(砂岩+泥岩互层)
13: terrigenous # 陆源碎屑岩(砂岩、粉砂岩)
14: unconsolidated # 松散堆积物(黄土、冲洪积)
+ 255: unknown # 无数据(GIS栅格背景值)
default: unconsolidated
landuse:
description: "土地利用类型"
unit: "分类代码"
mapping:
- 10: forest # 林地
- 30: farmland # 农田
- 40: urban # 城市
- 50: water # 水域
- 60: barren # 裸地
- 80: farmland # 耕地(合并入农田)
+ 1: forest # 林地(GIS栅格编码1)
+ 2: farmland # 农田(GIS栅格编码2)
+ 3: urban # 城市(GIS栅格编码3)
+ 4: water # 水域(GIS栅格编码4)
+ 5: barren # 裸地(GIS栅格编码5)
+ 8: farmland # 耕地(GIS栅格编码8,合并入农田)
default: farmland
terrain:
@@ -130,80 +138,82 @@ terrain:
5: gentle_hill # 低缓丘陵(塬边过渡带)
6: low_mountain # 低山(骊山等)
7: flat_plain # 平缓平原(冲积平原)
+ 255: unknown # 无数据(GIS栅格背景值)
default: hill
impervious:
description: "不透水率"
- unit: "百分比"
- # 数据: [0.0, 97.2], 均值16.40±25.99, 偏度1.787
- # 26.9%为0.0(无硬化地表),非零值右偏分布
- # 分箱策略:0单独一类,其余4等分(分位数分箱)
- # 分位数(非零): [2.0, 9.95, 31.8, 97.2]
- bins: [0.0, 0.01, 2.0, 10.0, 32.0, 97.2]
+ unit: "小数比例(0-1)"
+ # 数据: [0.0, 1.0], 均值0.31±0.46
+ # 68.9%为0.0(无硬化地表),非零值右偏分布
+ # 分箱策略:0单独一类,其余4等分
+ bins: [0.0, 0.01, 0.25, 0.50, 0.75, 1.0]
labels: [none, very_low, low, medium, high]
ndvi:
description: "植被指数"
- unit: "NDVI值"
- # 数据: [1.25, 38.68], 均值20.67±5.87, 偏度-0.106
- # 分位数: [1.25, 17.09, 20.3, 22.4, 25.2, 38.68]
- bins: [1.25, 17.09, 20.3, 22.4, 25.2, 38.68]
+ unit: "NDVI值(×1000缩放)"
+ # 数据: [-1.0, 5336.0], 均值2045.95±689.47
+ # 分位数: [-1.0, 1616.2, 1891.0, 2172.0, 2496.0, 5336.0]
+ bins: [-1.0, 1616.0, 1891.0, 2172.0, 2496.0, 5336.0]
labels: [very_low, low, medium, high, very_high]
sand_content:
description: "土壤含沙量"
unit: "百分比"
- # 数据: [23.0, 52.0], 均值34.43±4.29, 偏度0.538
- # 分位数: [23.0, 31.0, 33.0, 35.0, 37.0, 52.0]
- bins: [23.0, 31.0, 33.0, 35.0, 37.0, 52.0]
+ # 数据: [23.0, 255.0], 均值35.14±7.75
+ # 255为异常值(缺失值编码),正常范围[23, 52]
+ # 分位数(正常值): [23.0, 31.0, 34.0, 35.0, 38.0, 52.0]
+ bins: [23.0, 31.0, 34.0, 35.0, 38.0, 255.0]
labels: [very_low, low, medium, high, very_high]
ph:
description: "土壤PH值"
- unit: "PH值"
- # 数据: [59.0, 81.0], 均值71.79±4.14, 偏度-0.398
- # 分位数: [59.0, 68.0, 72.0, 74.0, 76.0, 81.0]
- bins: [59.0, 68.0, 72.0, 74.0, 76.0, 81.0]
+ unit: "PH值(×10缩放,如71=7.1)"
+ # 数据: [60.0, 255.0], 均值71.82±6.91
+ # 255为异常值(缺失值编码),正常范围[59, 81]
+ # 分位数(正常值): [60.0, 67.0, 71.0, 74.0, 76.0, 81.0]
+ bins: [60.0, 67.0, 71.0, 74.0, 76.0, 255.0]
labels: [very_low, low, medium, high, very_high]
soil_moisture:
description: "土壤湿度"
- unit: "百分比"
- # 数据: [0.0, 41.1], 均值32.02±14.92, 偏度-1.676
- # 约10%为0.0(缺失/极端干燥),其余集中在37-41
- # 分位数: [0.0, 37.7, 38.6, 38.9, 39.4, 41.1]
- bins: [0.0, 37.0, 38.5, 39.5, 41.1]
- labels: [very_low, low, medium, high]
+ unit: "小数比例(0-1)"
+ # 数据: [-1.0, 0.28], 均值0.15±0.08, 约10%为-1(缺失值)
+ # 正常值范围: [0.0, 0.28]
+ # 分位数(正常值): [0.0, 0.12, 0.14, 0.16, 0.19, 0.28]
+ # 分箱策略:-1视为缺失/极端干燥,其余4等分
+ bins: [-1.0, 0.0, 0.10, 0.14, 0.18, 0.28]
+ labels: [very_low, low, medium, high, very_high]
organic_carbon:
description: "有机碳"
unit: "百分比"
- # 数据: [0.0, 73.0], 均值38.36±19.14, 偏度-1.187
- # 分位数: [0.0, 34.0, 41.0, 47.0, 53.0, 73.0]
- bins: [0.0, 34.0, 41.0, 47.0, 53.0, 73.0]
+ # 数据: [0.0, 65.0], 均值39.03±19.13
+ # 分位数: [0.0, 34.0, 42.0, 48.0, 53.0, 65.0]
+ bins: [0.0, 34.0, 42.0, 48.0, 53.0, 65.0]
labels: [very_low, low, medium, high, very_high]
dist_to_river:
description: "距离河道距离"
unit: "米"
- # 数据: [12.21, 29904.99], 均值11003.92±6582.23, 偏度0.271
- # 分位数: [12.21, 5165.0, 9003.0, 12424.97, 16431.82, 29904.99]
- bins: [12.21, 5165.0, 9003.0, 12424.97, 16431.82, 29904.99]
+ # 数据: [12.21, 29968.26], 均值11378.07±6704.59
+ # 分位数: [12.21, 5409.3, 9522.82, 12667.75, 16952.46, 29968.26]
+ bins: [12.21, 5409.3, 9522.82, 12667.75, 16952.46, 29968.26]
labels: [very_close, close, moderate, far, very_far]
dist_to_fault:
description: "距离断裂带距离"
unit: "米"
- # 数据: [1.74, 14542.53], 均值3448.52±3406.56, 偏度1.055
- # 分位数: [1.74, 476.69, 1433.62, 3334.87, 6502.28, 14542.53]
- bins: [1.74, 476.69, 1433.62, 3334.87, 6502.28, 14542.53]
+ # 数据: [1.72, 14685.31], 均值3527.70±3400.55
+ # 分位数: [1.72, 515.98, 1451.71, 3577.36, 6545.45, 14685.31]
+ bins: [1.72, 515.98, 1451.71, 3577.36, 6545.45, 14685.31]
labels: [very_close, close, moderate, far, very_far]
pipe_density:
description: "供水管网密度"
unit: "m/m²"
- # 数据: [0.0, 0.07], 约80%为0.0,90%分位数0.013,95%分位数0.023
- # 分箱策略:0单独一类,其余3等分(分位数分箱)
- # 分位数(非零): [0.013, 0.023, 0.065]
- bins: [0.0, 0.001, 0.013, 0.023, 0.065]
+ # 数据: [0.0, 0.07], 约83.9%为0.0,非零值分位数[0.000438, 0.007136, 0.015399, 0.024523, 0.065431]
+ # 分箱策略:0单独一类,其余3等分
+ bins: [0.0, 0.001, 0.010, 0.025, 0.065]
labels: [none, low, medium, high]
diff --git a/app/config/dbn/earthquake_cpt_params.yaml b/app/config/dbn/earthquake_cpt_params.yaml
index f998a31..184531d 100644
--- a/app/config/dbn/earthquake_cpt_params.yaml
+++ b/app/config/dbn/earthquake_cpt_params.yaml
@@ -102,8 +102,8 @@ landslide:
# 大地震+近场+丘陵地形:黄土塬边
- condition: {magnitude: [major, great], epicenter_distance: [very_near, near], terrain: [hill, gentle_hill]}
probability: 0.55
- # 强震+近场+初育土(黄土)
- - condition: {magnitude: [strong, major], epicenter_distance: [very_near, near], soil_type: [entisol]}
+ # 强震+近场+初育土(新积土/风沙土,黄土)
+ - condition: {magnitude: [strong, major], epicenter_distance: [very_near, near], soil_type: [alluvial, aeolian]}
probability: 0.52
# === 中风险(0.20-0.40)===
diff --git a/app/config/dbn/earthquake_dbn_graph.yaml b/app/config/dbn/earthquake_dbn_graph.yaml
index 9e0f938..5464c46 100644
--- a/app/config/dbn/earthquake_dbn_graph.yaml
+++ b/app/config/dbn/earthquake_dbn_graph.yaml
@@ -147,18 +147,18 @@ node_states:
seismic_intensity: [minor, light, moderate, severe, extreme]
# 环境层(与暴雨模型共享,状态名与 discretization.yaml 一致)
- # 基于1201个样本的实际数据分布,2026-06-06更新
+ # 基于1365个样本的实际数据分布,2026-06-11更新
elevation: [very_low, low, medium, high, very_high]
slope: [very_low, low, medium, high, very_high]
aspect: [flat, north, east, south, west]
- soil_type: [ultisol, entisol, fluvo_aquic, yellow_brown]
- lithology: [acid_rock, basic_rock, carbonate, metamorphic, mixed_clastic, terrigenous, unconsolidated]
+ soil_type: [brown_soil, yellow_brown, cinnamon, black_lu, alluvial, aeolian, unknown]
+ lithology: [acid_rock, basic_rock, carbonate, metamorphic, mixed_clastic, terrigenous, unconsolidated, unknown]
landuse: [forest, farmland, urban, water, barren]
- terrain: [mountain, plain, deep_valley, hill, gentle_hill, low_mountain, flat_plain]
+ terrain: [mountain, plain, deep_valley, hill, gentle_hill, low_mountain, flat_plain, unknown]
ndvi: [very_low, low, medium, high, very_high]
sand_content: [very_low, low, medium, high, very_high]
ph: [very_low, low, medium, high, very_high]
- soil_moisture: [very_low, low, medium, high]
+ soil_moisture: [very_low, low, medium, high, very_high]
organic_carbon: [very_low, low, medium, high, very_high]
dist_to_river: [very_close, close, moderate, far, very_far]
dist_to_fault: [very_close, close, moderate, far, very_far]
diff --git a/app/config/dbn/rainfall_cpt_params.yaml b/app/config/dbn/rainfall_cpt_params.yaml
index 15c296d..8c5a5d9 100644
--- a/app/config/dbn/rainfall_cpt_params.yaml
+++ b/app/config/dbn/rainfall_cpt_params.yaml
@@ -72,8 +72,8 @@ landslide:
# 城区开挖坡脚/弃土加载+暴雨:工程滑坡
- condition: {landuse: [urban], slope: [medium, high, very_high], rain_intensity: [heavy, storm, downpour, extreme]}
probability: 0.52
- # 大雨+陡坡+初育土(黄土):黄土塬边滑坡
- - condition: {rain_intensity: [heavy, storm, downpour], slope: [high, very_high], soil_type: [entisol]}
+ # 大雨+陡坡+初育土(新积土/风沙土,黄土):黄土塬边滑坡
+ - condition: {rain_intensity: [heavy, storm, downpour], slope: [high, very_high], soil_type: [alluvial, aeolian]}
probability: 0.50
# === 中风险(0.20-0.40)===
diff --git a/app/config/dbn/rainfall_dbn_graph.yaml b/app/config/dbn/rainfall_dbn_graph.yaml
index f0740e2..ee6de20 100644
--- a/app/config/dbn/rainfall_dbn_graph.yaml
+++ b/app/config/dbn/rainfall_dbn_graph.yaml
@@ -143,19 +143,19 @@ node_states:
accum_rain: [trace, light, moderate, heavy, extreme]
# 环境层(离散字段状态名与数据库编码一一对应,连续字段用分位数分箱)
- # 基于1201个样本的实际数据分布,2026-06-06更新
+ # 基于1365个样本的实际数据分布,2026-06-11更新
elevation: [very_low, low, medium, high, very_high]
slope: [very_low, low, medium, high, very_high]
aspect: [flat, north, east, south, west]
- soil_type: [ultisol, entisol, fluvo_aquic, yellow_brown]
- lithology: [acid_rock, basic_rock, carbonate, metamorphic, mixed_clastic, terrigenous, unconsolidated]
+ soil_type: [brown_soil, yellow_brown, cinnamon, black_lu, alluvial, aeolian, unknown]
+ lithology: [acid_rock, basic_rock, carbonate, metamorphic, mixed_clastic, terrigenous, unconsolidated, unknown]
landuse: [forest, farmland, urban, water, barren]
- terrain: [mountain, plain, deep_valley, hill, gentle_hill, low_mountain, flat_plain]
+ terrain: [mountain, plain, deep_valley, hill, gentle_hill, low_mountain, flat_plain, unknown]
impervious: [none, very_low, low, medium, high]
ndvi: [very_low, low, medium, high, very_high]
sand_content: [very_low, low, medium, high, very_high]
ph: [very_low, low, medium, high, very_high]
- soil_moisture: [very_low, low, medium, high]
+ soil_moisture: [very_low, low, medium, high, very_high]
organic_carbon: [very_low, low, medium, high, very_high]
dist_to_river: [very_close, close, moderate, far, very_far]
dist_to_fault: [very_close, close, moderate, far, very_far]
diff --git a/app/core/launcher.py b/app/core/launcher.py
index 5fb3633..6e308ed 100644
--- a/app/core/launcher.py
+++ b/app/core/launcher.py
@@ -34,9 +34,6 @@ class AppLauncher:
# 检查虚拟环境
check_virtualenv(self.project_root)
- # 检查安装依赖
- check_dependencies(self.project_root)
-
# 检查是否正在使用虚拟环境运行
import platform
import sys
@@ -57,12 +54,16 @@ class AppLauncher:
print("检测到未使用虚拟环境,正在切换到虚拟环境...")
print("=" * 50)
- # 使用虚拟环境的Python重新启动应用
+ # 使用虚拟环境的Python重新启动应用(不传递参数避免重复检查)
import subprocess
cmd = [str(venv_python)] + sys.argv
subprocess.run(cmd, check=True)
return
+ # 以下代码仅在虚拟环境中执行
+ # 检查安装依赖(只执行一次)
+ check_dependencies(self.project_root)
+
# 启动应用
print("\n" + "=" * 50)
print("✓ 所有检查通过,准备启动应用...")
diff --git a/app/models/dbn/earthquake/earthquake_dbn.py b/app/models/dbn/earthquake/earthquake_dbn.py
index 5d6e829..8de5adb 100644
--- a/app/models/dbn/earthquake/earthquake_dbn.py
+++ b/app/models/dbn/earthquake/earthquake_dbn.py
@@ -214,11 +214,12 @@ class EarthquakeDBN:
预测结果
"""
point_id = point.get('id')
+ source_id = point.get('source_id')
lon = point.get('lon')
lat = point.get('lat')
source_type = point.get('source_type')
- logger.debug(f"地震预测点 ID={point_id}, source_type={source_type}")
+ logger.debug(f"地震预测点 ID={point_id}, source_id={source_id}, source_type={source_type}")
# 计算震中距(如果未直接提供)
if epicenter_distance is None:
@@ -270,6 +271,7 @@ class EarthquakeDBN:
# 构造输出
result = {
'point_id': point_id,
+ 'source_id': source_id, # 隐患点/风险点的真实ID
'source_type': source_type,
'lon': lon,
'lat': lat,
@@ -376,6 +378,7 @@ class EarthquakeDBN:
logger.error(f"预测点 {point.get('id')} 失败: {e}")
results.append({
'point_id': point.get('id'),
+ 'source_id': point.get('source_id'),
'source_type': point.get('source_type'),
'lon': point.get('lon'),
'lat': point.get('lat'),
@@ -423,6 +426,7 @@ class EarthquakeDBN:
logger.error(f"预测点 {point.get('id')} 失败: {e}")
results.append({
'point_id': point.get('id'),
+ 'source_id': point.get('source_id'),
'source_type': point.get('source_type'),
'lon': point.get('lon'),
'lat': point.get('lat'),
diff --git a/app/models/dbn/rainfall/rainfall_dbn.py b/app/models/dbn/rainfall/rainfall_dbn.py
index b9d970d..b79726b 100644
--- a/app/models/dbn/rainfall/rainfall_dbn.py
+++ b/app/models/dbn/rainfall/rainfall_dbn.py
@@ -230,11 +230,12 @@ class RainfallDBN:
预测结果
"""
point_id = point.get('id')
+ source_id = point.get('source_id')
lon = point.get('lon')
lat = point.get('lat')
source_type = point.get('source_type')
- logger.debug(f"预测点 ID={point_id}, source_type={source_type}")
+ logger.debug(f"预测点 ID={point_id}, source_id={source_id}, source_type={source_type}")
# 获取降雨数据
if rainfall is not None and duration is not None:
@@ -287,6 +288,7 @@ class RainfallDBN:
# 构造输出
result = {
'point_id': point_id,
+ 'source_id': source_id, # 隐患点/风险点的真实ID
'source_type': source_type,
'lon': lon,
'lat': lat,
@@ -369,6 +371,7 @@ class RainfallDBN:
logger.error(f"预测点 {point.get('id')} 失败: {e}")
results.append({
'point_id': point.get('id'),
+ 'source_id': point.get('source_id'),
'source_type': point.get('source_type'),
'lon': point.get('lon'),
'lat': point.get('lat'),
@@ -417,6 +420,7 @@ class RainfallDBN:
logger.error(f"预测点 {point.get('id')} 失败: {e}")
results.append({
'point_id': point.get('id'),
+ 'source_id': point.get('source_id'),
'source_type': point.get('source_type'),
'lon': point.get('lon'),
'lat': point.get('lat'),
diff --git a/app/utils/logger.py b/app/utils/logger.py
index 7b3a466..aad19b8 100644
--- a/app/utils/logger.py
+++ b/app/utils/logger.py
@@ -1,81 +1,78 @@
"""
日志工具类
-支持按天分割、自动清理过期日志
+使用 loguru 提供增强的日志功能,支持按天分割、自动清理过期日志
"""
-import logging
+import sys
from pathlib import Path
-from logging.handlers import TimedRotatingFileHandler
-from datetime import datetime, timedelta
+from loguru import logger
class LoggerManager:
- """日志管理器"""
+ """日志管理器 - 基于 loguru"""
- _loggers = {}
+ _configured = False
@classmethod
- def get_logger(cls, name: str = "algorithm", log_dir: str = "logs") -> logging.Logger:
+ def get_logger(cls, name: str = "algorithm", log_dir: str = "logs"):
"""
- 获取日志记录器
+ 获取日志记录器(loguru 不需要传统意义上的 logger 实例)
+
+ Args:
+ name: 日志名称(用于文件命名)
+ log_dir: 日志目录
+
+ Returns:
+ loguru.logger 实例
+ """
+ if not cls._configured:
+ cls._configure_logger(name, log_dir)
+ return logger
+
+ @classmethod
+ def _configure_logger(cls, name: str, log_dir: str):
+ """
+ 配置 loguru 日志处理器
Args:
name: 日志名称
log_dir: 日志目录
-
- Returns:
- logging.Logger 实例
"""
- if name in cls._loggers:
- return cls._loggers[name]
+ # 移除默认的 stderr handler
+ logger.remove()
# 创建日志目录
log_path = Path(log_dir)
log_path.mkdir(parents=True, exist_ok=True)
- # 创建 logger
- logger = logging.getLogger(name)
- logger.setLevel(logging.DEBUG)
-
- # 避免重复添加 handler
- if logger.handlers:
- cls._loggers[name] = logger
- return logger
-
- # 日志格式
- formatter = logging.Formatter(
- '%(asctime)s [%(threadName)s] %(levelname)-5s %(name)s - %(message)s',
- datefmt='%Y-%m-%d %H:%M:%S'
+ # 控制台 Handler - 彩色输出
+ logger.add(
+ sink=sys.stderr,
+ level="INFO",
+ format="{time:YYYY-MM-DD HH:mm:ss} [{thread.name}] {level: <5} {name} - {message}",
+ colorize=True
)
- # 控制台 Handler
- console_handler = logging.StreamHandler()
- console_handler.setLevel(logging.INFO)
- console_handler.setFormatter(formatter)
- logger.addHandler(console_handler)
-
- # 文件 Handler - 按天分割
+ # 文件 Handler - 按大小分割,Windows 兼容
log_file = log_path / f"{name}.log"
- file_handler = TimedRotatingFileHandler(
- filename=str(log_file),
- when='midnight',
- interval=1,
- backupCount=7,
- encoding='utf-8'
+ logger.add(
+ sink=str(log_file),
+ level="DEBUG",
+ format="{time:YYYY-MM-DD HH:mm:ss} [{thread.name}] {level: <5} {name} - {message}",
+ rotation="50 MB", # 按大小轮转,避免Windows文件锁定问题
+ retention="7 days", # 保留7天
+ compression="zip", # 压缩旧日志文件
+ encoding="utf-8",
+ enqueue=True, # 异步写入,避免文件锁定
+ delay=True, # 延迟打开文件,减少锁定时间
+ backtrace=True, # 完整堆栈跟踪
+ diagnose=True # 详细错误诊断
)
- file_handler.setLevel(logging.DEBUG)
- file_handler.setFormatter(formatter)
- # 设置日志文件命名格式
- file_handler.suffix = "%Y-%m-%d.log"
-
- logger.addHandler(file_handler)
-
- cls._loggers[name] = logger
- return logger
+ cls._configured = True
# 便捷函数
-def get_logger(name: str = "algorithm", log_dir: str = "logs") -> logging.Logger:
+def get_logger(name: str = "algorithm", log_dir: str = "logs"):
"""
获取日志记录器的便捷函数
@@ -84,6 +81,6 @@ def get_logger(name: str = "algorithm", log_dir: str = "logs") -> logging.Logger
log_dir: 日志目录
Returns:
- logging.Logger 实例
+ loguru.logger 实例
"""
return LoggerManager.get_logger(name, log_dir)
diff --git a/requirements.txt b/requirements.txt
index 1851c1c..3c5752c 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -7,4 +7,5 @@ matplotlib == 3.10.9
Pillow == 12.2.0
pyyaml == 6.0.3
fastapi == 0.136.3
-uvicorn[standard] == 0.48.0
\ No newline at end of file
+uvicorn[standard] == 0.48.0
+loguru == 0.7.3
\ No newline at end of file