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