- 新增 RJData 配置块,支持周数据目录和字段映射 - 自动调度增加 RJ 周文件检查(与现有 7 天检查为 AND 关系) - 处理器支持 RJ 表专用字段映射 - 目标表: 2_6GRJGD, 2_6GRJYD - 字段: 开始时间, 结束时间, gNBId, cellId, gNBplmn, 上下行总流量_GB Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
358 lines
12 KiB
Python
358 lines
12 KiB
Python
"""
|
|
配置管理模块
|
|
"""
|
|
import json
|
|
import os
|
|
import sys
|
|
from datetime import datetime
|
|
from dataclasses import dataclass, field
|
|
from pathlib import Path
|
|
from typing import Any, Dict, List
|
|
|
|
|
|
def _resolve_base_dir() -> Path:
|
|
env_base_dir = os.environ.get("CAPAREPORT_BASE_DIR")
|
|
if env_base_dir:
|
|
return Path(env_base_dir).expanduser().resolve()
|
|
if getattr(sys, "frozen", False):
|
|
return Path(sys.executable).resolve().parent
|
|
return Path(__file__).resolve().parent.parent
|
|
|
|
|
|
BASE_DIR = _resolve_base_dir()
|
|
CACHE_DIR = BASE_DIR / "cache"
|
|
CONFIG_FILE = BASE_DIR / "Configure.json"
|
|
SQL_SCRIPT = BASE_DIR / "ReportScript.sql"
|
|
|
|
|
|
@dataclass
|
|
class MySQLConfig:
|
|
host: str = "localhost"
|
|
port: int = 3306
|
|
user: str = "root"
|
|
passwd: str = ""
|
|
dbname: str = "CapacityReport"
|
|
|
|
|
|
@dataclass
|
|
class AutoSchedulerConfig:
|
|
enabled: bool = False
|
|
check_interval_hours: int = 1
|
|
expected_directories: List[str] = field(default_factory=list)
|
|
week_offset: int = 0
|
|
|
|
def normalized(self) -> "AutoSchedulerConfig":
|
|
try:
|
|
check_interval_hours = int(self.check_interval_hours)
|
|
except (TypeError, ValueError):
|
|
check_interval_hours = 1
|
|
try:
|
|
week_offset = int(self.week_offset)
|
|
except (TypeError, ValueError):
|
|
week_offset = 0
|
|
|
|
directories = []
|
|
seen = set()
|
|
for directory in self.expected_directories or []:
|
|
normalized = str(directory).replace("\\", "/").strip().strip("/")
|
|
if normalized and normalized not in seen:
|
|
directories.append(normalized)
|
|
seen.add(normalized)
|
|
|
|
return AutoSchedulerConfig(
|
|
enabled=bool(self.enabled),
|
|
check_interval_hours=max(check_interval_hours, 1),
|
|
expected_directories=directories,
|
|
week_offset=week_offset,
|
|
)
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
normalized = self.normalized()
|
|
return {
|
|
"enabled": normalized.enabled,
|
|
"check_interval_hours": normalized.check_interval_hours,
|
|
"expected_directories": normalized.expected_directories,
|
|
"week_offset": normalized.week_offset,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any] | None) -> "AutoSchedulerConfig":
|
|
data = data or {}
|
|
directories = data.get("expected_directories", [])
|
|
return cls(
|
|
enabled=bool(data.get("enabled", False)),
|
|
check_interval_hours=data.get("check_interval_hours", 1),
|
|
expected_directories=directories if isinstance(directories, list) else [],
|
|
week_offset=data.get("week_offset", 0),
|
|
).normalized()
|
|
|
|
|
|
@dataclass
|
|
class RemoteDataConfig:
|
|
enabled: bool = False
|
|
protocol: str = "sftp"
|
|
host: str = ""
|
|
port: int = 22
|
|
user: str = ""
|
|
passwd: str = ""
|
|
remote_dir: str = "/"
|
|
passive: bool = True
|
|
timeout: int = 30
|
|
auto_delete_source: bool = False
|
|
auto_scheduler: AutoSchedulerConfig = field(default_factory=AutoSchedulerConfig)
|
|
|
|
def normalized(self) -> "RemoteDataConfig":
|
|
protocol = self.protocol.lower().strip()
|
|
if protocol not in {"ftp", "sftp"}:
|
|
protocol = "sftp"
|
|
|
|
port = self.port or (22 if protocol == "sftp" else 21)
|
|
scheduler = self.auto_scheduler.normalized()
|
|
return RemoteDataConfig(
|
|
enabled=bool(self.enabled) or scheduler.enabled,
|
|
protocol=protocol,
|
|
host=self.host.strip(),
|
|
port=port,
|
|
user=self.user.strip(),
|
|
passwd=self.passwd,
|
|
remote_dir=(self.remote_dir or "/").strip() or "/",
|
|
passive=bool(self.passive),
|
|
timeout=max(int(self.timeout or 30), 1),
|
|
auto_delete_source=bool(self.auto_delete_source) or scheduler.enabled,
|
|
auto_scheduler=scheduler,
|
|
)
|
|
|
|
def to_dict(self, include_password: bool = False) -> Dict[str, Any]:
|
|
data = {
|
|
"enabled": self.enabled,
|
|
"protocol": self.protocol,
|
|
"host": self.host,
|
|
"port": self.port,
|
|
"user": self.user,
|
|
"remote_dir": self.remote_dir,
|
|
"passive": self.passive,
|
|
"timeout": self.timeout,
|
|
"auto_delete_source": self.auto_delete_source,
|
|
"auto_scheduler": self.auto_scheduler.normalized().to_dict(),
|
|
}
|
|
if include_password:
|
|
data["passwd"] = self.passwd
|
|
return data
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any] | None) -> "RemoteDataConfig":
|
|
data = data or {}
|
|
protocol = str(data.get("protocol", "sftp")).lower()
|
|
default_port = 22 if protocol == "sftp" else 21
|
|
try:
|
|
port = int(data.get("port") or default_port)
|
|
except (TypeError, ValueError):
|
|
port = default_port
|
|
try:
|
|
timeout = int(data.get("timeout") or 30)
|
|
except (TypeError, ValueError):
|
|
timeout = 30
|
|
return cls(
|
|
enabled=bool(data.get("enabled", False)),
|
|
protocol=protocol,
|
|
host=str(data.get("host", "")),
|
|
port=port,
|
|
user=str(data.get("user", "")),
|
|
passwd=str(data.get("passwd", "")),
|
|
remote_dir=str(data.get("remote_dir", "/")),
|
|
passive=bool(data.get("passive", True)),
|
|
timeout=timeout,
|
|
auto_delete_source=bool(data.get("auto_delete_source", False)),
|
|
auto_scheduler=AutoSchedulerConfig.from_dict(data.get("auto_scheduler")),
|
|
).normalized()
|
|
|
|
|
|
@dataclass
|
|
class RJDataConfig:
|
|
"""RJ数据配置 - 周数据处理"""
|
|
enabled: bool = False
|
|
weekly_directories: List[str] = field(default_factory=list)
|
|
# 字段映射: {表名: [{Source, Target, Type}, ...]}
|
|
table_field_mappings: Dict[str, List[Dict[str, Any]]] = field(default_factory=dict)
|
|
|
|
def normalized(self) -> "RJDataConfig":
|
|
directories = []
|
|
seen = set()
|
|
for d in self.weekly_directories or []:
|
|
normalized = str(d).replace("\\", "/").strip().strip("/")
|
|
if normalized and normalized not in seen:
|
|
directories.append(normalized)
|
|
seen.add(normalized)
|
|
|
|
# 标准化字段映射
|
|
mappings = {}
|
|
for table_name, fields in (self.table_field_mappings or {}).items():
|
|
if isinstance(fields, list):
|
|
mappings[table_name] = [
|
|
{k: v for k, v in f.items() if k in ("Source", "Target", "Type")}
|
|
for f in fields
|
|
if isinstance(f, dict) and "Source" in f and "Target" in f
|
|
]
|
|
|
|
return RJDataConfig(
|
|
enabled=bool(self.enabled),
|
|
weekly_directories=directories,
|
|
table_field_mappings=mappings,
|
|
)
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
normalized = self.normalized()
|
|
return {
|
|
"enabled": normalized.enabled,
|
|
"weekly_directories": normalized.weekly_directories,
|
|
"table_field_mappings": normalized.table_field_mappings,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any] | None) -> "RJDataConfig":
|
|
data = data or {}
|
|
return cls(
|
|
enabled=bool(data.get("enabled", False)),
|
|
weekly_directories=data.get("weekly_directories", []) if isinstance(data.get("weekly_directories"), list) else [],
|
|
table_field_mappings=data.get("table_field_mappings", {}) if isinstance(data.get("table_field_mappings"), dict) else {},
|
|
).normalized()
|
|
|
|
|
|
@dataclass
|
|
class HistoryRetentionConfig:
|
|
enabled: bool = False
|
|
keep_count: int = 20
|
|
|
|
def normalized(self) -> "HistoryRetentionConfig":
|
|
try:
|
|
keep_count = int(self.keep_count)
|
|
except (TypeError, ValueError):
|
|
keep_count = 20
|
|
|
|
return HistoryRetentionConfig(
|
|
enabled=bool(self.enabled),
|
|
keep_count=max(keep_count, 0),
|
|
)
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
normalized = self.normalized()
|
|
return {
|
|
"enabled": normalized.enabled,
|
|
"keep_count": normalized.keep_count,
|
|
}
|
|
|
|
@classmethod
|
|
def from_dict(cls, data: Dict[str, Any] | None) -> "HistoryRetentionConfig":
|
|
data = data or {}
|
|
return cls(
|
|
enabled=bool(data.get("enabled", False)),
|
|
keep_count=data.get("keep_count", 20),
|
|
).normalized()
|
|
|
|
|
|
@dataclass
|
|
class AppConfig:
|
|
update: str = ""
|
|
mysql: MySQLConfig = field(default_factory=MySQLConfig)
|
|
remote_data: RemoteDataConfig = field(default_factory=RemoteDataConfig)
|
|
history_retention: HistoryRetentionConfig = field(default_factory=HistoryRetentionConfig)
|
|
rj_data: RJDataConfig = field(default_factory=RJDataConfig)
|
|
sheet_filter: List[str] = field(default_factory=list)
|
|
extract_fields: List[Dict[str, Any]] = field(default_factory=list)
|
|
|
|
@classmethod
|
|
def load(cls) -> "AppConfig":
|
|
"""从 Configure.json 加载配置"""
|
|
if not CONFIG_FILE.exists():
|
|
return cls()
|
|
|
|
with open(CONFIG_FILE, 'r', encoding='utf-8') as f:
|
|
data = json.load(f)
|
|
|
|
mysql_data = data.get("MySQL_DBInfo", {})
|
|
mysql_config = MySQLConfig(
|
|
host=mysql_data.get("host", "localhost"),
|
|
port=mysql_data.get("port", 3306),
|
|
user=mysql_data.get("user", "root"),
|
|
passwd=mysql_data.get("passwd", ""),
|
|
dbname=mysql_data.get("dbname", "CapacityReport")
|
|
)
|
|
remote_config = RemoteDataConfig.from_dict(data.get("RemoteData"))
|
|
history_retention = HistoryRetentionConfig.from_dict(data.get("HistoryRetention"))
|
|
rj_data = RJDataConfig.from_dict(data.get("RJData"))
|
|
|
|
return cls(
|
|
update=data.get("Update", ""),
|
|
mysql=mysql_config,
|
|
remote_data=remote_config,
|
|
history_retention=history_retention,
|
|
rj_data=rj_data,
|
|
sheet_filter=data.get("SheetFilter", []),
|
|
extract_fields=data.get("ExtractField", [])
|
|
)
|
|
|
|
def save(self):
|
|
"""保存配置到 Configure.json,并自动更新 Update 时间"""
|
|
self.update = datetime.now().strftime("%Y/%m/%d %H:%M:%S")
|
|
data = self.to_file_dict()
|
|
|
|
with open(CONFIG_FILE, 'w', encoding='utf-8') as f:
|
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
|
|
def to_file_dict(self) -> Dict[str, Any]:
|
|
"""转换为配置文件结构(包含敏感字段,用于保存和下载)"""
|
|
return {
|
|
"Update": self.update,
|
|
"MySQL_DBInfo": {
|
|
"host": self.mysql.host,
|
|
"port": self.mysql.port,
|
|
"user": self.mysql.user,
|
|
"passwd": self.mysql.passwd,
|
|
"dbname": self.mysql.dbname
|
|
},
|
|
"RemoteData": self.remote_data.normalized().to_dict(include_password=True),
|
|
"HistoryRetention": self.history_retention.normalized().to_dict(),
|
|
"RJData": self.rj_data.normalized().to_dict(),
|
|
"SheetFilter": self.sheet_filter,
|
|
"ExtractField": self.extract_fields
|
|
}
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
"""转换为字典(用于返回给前端,隐藏密码)"""
|
|
return {
|
|
"update": self.update,
|
|
"mysql": {
|
|
"host": self.mysql.host,
|
|
"port": self.mysql.port,
|
|
"user": self.mysql.user,
|
|
"dbname": self.mysql.dbname
|
|
},
|
|
"remote_data": self.remote_data.normalized().to_dict(),
|
|
"history_retention": self.history_retention.normalized().to_dict(),
|
|
"rj_data": self.rj_data.normalized().to_dict(),
|
|
"sheet_filter": self.sheet_filter,
|
|
"extract_fields": self.extract_fields
|
|
}
|
|
|
|
def to_dict_full(self) -> Dict[str, Any]:
|
|
"""转换为完整字典(包含密码,用于编辑时回显)"""
|
|
return {
|
|
"update": self.update,
|
|
"mysql": {
|
|
"host": self.mysql.host,
|
|
"port": self.mysql.port,
|
|
"user": self.mysql.user,
|
|
"passwd": self.mysql.passwd,
|
|
"dbname": self.mysql.dbname
|
|
},
|
|
"remote_data": self.remote_data.normalized().to_dict(include_password=True),
|
|
"history_retention": self.history_retention.normalized().to_dict(),
|
|
"rj_data": self.rj_data.normalized().to_dict(),
|
|
"sheet_filter": self.sheet_filter,
|
|
"extract_fields": self.extract_fields
|
|
}
|
|
|
|
|
|
# 确保缓存目录存在
|
|
CACHE_DIR.mkdir(exist_ok=True)
|