feat: 实现 CellData 预处理与单独刷新

This commit is contained in:
2026-06-25 04:28:24 +08:00
parent 66a2eebdf9
commit c36dd21838
13 changed files with 1027 additions and 13 deletions
+84
View File
@@ -1,4 +1,5 @@
import json
import re
from datetime import datetime
from typing import Any
from urllib.parse import quote
@@ -11,6 +12,7 @@ from app import state
from app.config import (
CellDataConfig,
DataMappingsConfig,
DEFAULT_CELL_DATA_MAPPING,
HistoryRetentionConfig,
MetrixConfig,
MySQLConfig,
@@ -103,6 +105,11 @@ async def update_cell_data_remote_config(config: dict[str, Any] = Body(...)):
state.config.cell_data = CellDataConfig(
remote_data=RemoteDataConfig.from_dict(config),
mysql=current.mysql,
scan_paths=current.scan_paths,
year_dir_regex=current.year_dir_regex,
file_name_regex=current.file_name_regex,
file_time_regex=current.file_time_regex,
mapping=current.mapping,
).normalized()
state.config.save()
return {"success": True, "message": "CellData 远程数据源配置已更新", "update": state.config.update}
@@ -115,6 +122,11 @@ async def update_cell_data_mysql_config(config: dict[str, Any] = Body(...)):
state.config.cell_data = CellDataConfig(
remote_data=current.remote_data,
mysql=MySQLConfig.from_dict(config, default_dbname="celldata"),
scan_paths=current.scan_paths,
year_dir_regex=current.year_dir_regex,
file_name_regex=current.file_name_regex,
file_time_regex=current.file_time_regex,
mapping=current.mapping,
).normalized()
state.config.save()
return {"success": True, "message": "CellData 数据库配置已更新", "update": state.config.update}
@@ -136,6 +148,36 @@ async def test_cell_data_mysql_connection(config: dict[str, Any] | None = Body(N
return _test_mysql_config(mysql_config)
@router.post("/api/config/cell-data/settings")
async def update_cell_data_settings(config: dict[str, Any] = Body(...)):
validation = _validate_cell_data_settings(config)
if not validation["success"]:
raise HTTPException(status_code=400, detail=validation["message"])
state.reload_config()
current = state.config.cell_data.normalized()
state.config.cell_data = CellDataConfig(
remote_data=current.remote_data,
mysql=current.mysql,
scan_paths=config.get("scan_paths", current.scan_paths),
year_dir_regex=str(config.get("year_dir_regex", current.year_dir_regex)),
file_name_regex=str(config.get("file_name_regex", current.file_name_regex)),
file_time_regex=str(config.get("file_time_regex", current.file_time_regex)),
mapping=config.get("mapping", current.mapping),
).normalized()
state.config.save()
return {"success": True, "message": "CellData 规则已更新", "update": state.config.update}
@router.post("/api/config/cell-data/settings/validate")
async def validate_cell_data_settings(config: dict[str, Any] = Body(...)):
return _validate_cell_data_settings(config)
@router.get("/api/config/cell-data/mapping/default")
async def get_default_cell_data_mapping():
return DEFAULT_CELL_DATA_MAPPING
@router.post("/api/config/history-retention")
async def update_history_retention(config: dict[str, Any] = Body(...)):
state.reload_config()
@@ -254,3 +296,45 @@ def _test_mysql_config(config: MySQLConfig) -> dict[str, Any]:
except Exception as exc:
return {"success": False, "message": str(exc)}
def _validate_cell_data_settings(config: dict[str, Any]) -> dict[str, Any]:
scan_paths = config.get("scan_paths", [])
if not isinstance(scan_paths, list) or not any(str(path).strip() for path in scan_paths):
return {"success": False, "message": "请至少配置一个扫描路径"}
for key in ("year_dir_regex", "file_name_regex", "file_time_regex"):
try:
re.compile(str(config.get(key, "")))
except re.error as exc:
return {"success": False, "message": f"{key} 正则无效: {exc}"}
mapping = config.get("mapping")
if not isinstance(mapping, dict):
return {"success": False, "message": "映射规则必须是 JSON 对象"}
if not str(mapping.get("target_table", "")).strip():
return {"success": False, "message": "映射规则缺少 target_table"}
key_config = mapping.get("key")
if not isinstance(key_config, dict) or not key_config.get("field") or not key_config.get("expr"):
return {"success": False, "message": "映射规则缺少 key.field 或 key.expr"}
sources = mapping.get("sources")
if not isinstance(sources, list) or not sources:
return {"success": False, "message": "映射规则至少需要一个 sources 项"}
for index, source in enumerate(sources, 1):
if not isinstance(source, dict):
return {"success": False, "message": f"sources 第 {index} 项必须是对象"}
if not source.get("band") or not source.get("file_prefix"):
return {"success": False, "message": f"sources 第 {index} 项缺少 band 或 file_prefix"}
fields = source.get("fields")
if not isinstance(fields, dict) or not fields:
return {"success": False, "message": f"sources 第 {index} 项缺少 fields"}
for target, rule in fields.items():
if not str(target).strip():
return {"success": False, "message": f"sources 第 {index} 项存在空目标字段"}
if isinstance(rule, str) and rule.strip():
continue
if isinstance(rule, dict) and "value" in rule:
continue
return {"success": False, "message": f"{target} 的映射规则无效"}
return {"success": True, "message": "映射规则有效"}