refactor: centralize configuration management with type-safe schema

Major changes:
- Add dataclass-based configuration schema with validation (config/schema.py)
- Create centralized config loader and default values (config/defaults.py, config/loader.py)
- Remove duplicate database_config.py, merge into unified structure
- Consolidate browser settings into ERP config
- Add batch_size parameter support to extractor

Bug fixes:
- Fix settings save error by updating config paths (browser.* → erp.*)
- Fix batch_size not being applied in data extraction

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-02-05 14:27:28 +08:00
parent 885d87dff8
commit 13bc4520bf
11 changed files with 431 additions and 136 deletions

View File

@@ -1,9 +0,0 @@
# ================= SQL Server 配置 =================
SQL_SERVER_CONFIG = {
'driver': 'ODBC Driver 18 for SQL Server',
'server': '192.168.110.114',
'database': 'CompanyDB',
'username': 'peng',
'password': 'Cqbld123456.',
'TrustServerCertificate': 'yes'
}

51
config/defaults.py Normal file
View File

@@ -0,0 +1,51 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
默认配置值
定义所有配置项的默认值。
"""
from config.schema import (
ERPConfig,
DatabaseConfig,
PathConfig,
ExtractionConfig,
AppConfig
)
# 默认配置
DEFAULT_APP_CONFIG = AppConfig(
erp=ERPConfig(
url="https://68.11.34.30:8082/",
username="BLDpengqiangqiang",
password="Cqbld123456.",
headless=True,
ignore_https_errors=True,
auto_close_browser=True,
),
database=DatabaseConfig(
server="192.168.110.114",
database="CompanyDB",
username="peng",
password="Cqbld123456.",
driver="ODBC Driver 18 for SQL Server",
trust_server_certificate="yes",
),
paths=PathConfig(
data_dir="D:/python/playwrite/data/",
production_id_file="ProductionID.txt",
default_output="离散备料计划维护_合并.xlsx",
validation_output="物料状态校验结果.xlsx",
),
extraction=ExtractionConfig(
batch_size=100,
verbose=True,
auto_convert=True,
merge_batches=True,
)
)
# 兼容旧版本的字典格式
DEFAULT_SETTINGS_DICT = DEFAULT_APP_CONFIG.to_dict()

138
config/loader.py Normal file
View File

@@ -0,0 +1,138 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
配置加载器
负责加载、合并和验证配置。
"""
import json
import os
from typing import Any, Dict
from config.schema import AppConfig
from config.defaults import DEFAULT_APP_CONFIG, DEFAULT_SETTINGS_DICT
class ConfigLoader:
"""配置加载器"""
@staticmethod
def load(config_file: str = "config/user_settings.json") -> AppConfig:
"""
加载配置文件
Args:
config_file: 配置文件路径
Returns:
应用配置对象
"""
if os.path.exists(config_file):
try:
with open(config_file, 'r', encoding='utf-8') as f:
loaded_settings = json.load(f)
# 合并默认配置和加载的配置
merged_settings = ConfigLoader._merge_settings(DEFAULT_SETTINGS_DICT, loaded_settings)
return ConfigLoader._dict_to_config(merged_settings)
except (json.JSONDecodeError, IOError) as e:
print(f"加载配置文件失败: {e},使用默认配置")
return DEFAULT_APP_CONFIG
else:
# 首次运行,创建默认配置文件
ConfigLoader.save(DEFAULT_APP_CONFIG, config_file)
return DEFAULT_APP_CONFIG
@staticmethod
def save(config: AppConfig, config_file: str = "config/user_settings.json") -> bool:
"""
保存配置到文件
Args:
config: 应用配置对象
config_file: 配置文件路径
Returns:
保存是否成功
"""
try:
# 确保配置目录存在
os.makedirs(os.path.dirname(config_file), exist_ok=True)
with open(config_file, 'w', encoding='utf-8') as f:
json.dump(config.to_dict(), f, ensure_ascii=False, indent=2)
return True
except IOError as e:
print(f"保存配置文件失败: {e}")
return False
@staticmethod
def _merge_settings(defaults: Dict, loaded: Dict) -> Dict:
"""
合并默认配置和加载的配置
Args:
defaults: 默认配置
loaded: 加载的配置
Returns:
合并后的配置
"""
result = defaults.copy()
for key, value in loaded.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = ConfigLoader._merge_settings(result[key], value)
else:
result[key] = value
return result
@staticmethod
def _dict_to_config(settings: Dict) -> AppConfig:
"""
将字典转换为配置对象
Args:
settings: 配置字典
Returns:
应用配置对象
"""
erp_dict = settings.get("erp", {})
database_dict = settings.get("database", {})
paths_dict = settings.get("paths", {})
extraction_dict = settings.get("extraction", {})
return AppConfig(
erp=ERPConfig(
url=erp_dict.get("url", ""),
username=erp_dict.get("username", ""),
password=erp_dict.get("password", ""),
headless=erp_dict.get("headless", True),
ignore_https_errors=erp_dict.get("ignore_https_errors", True),
auto_close_browser=erp_dict.get("auto_close_browser", True),
),
database=DatabaseConfig(
server=database_dict.get("server", ""),
database=database_dict.get("database", ""),
username=database_dict.get("username", ""),
password=database_dict.get("password", ""),
driver=database_dict.get("driver", "ODBC Driver 18 for SQL Server"),
trust_server_certificate=database_dict.get("trust_server_certificate", "yes"),
),
paths=PathConfig(
data_dir=paths_dict.get("data_dir", ""),
production_id_file=paths_dict.get("production_id_file", ""),
default_output=paths_dict.get("default_output", "离散备料计划维护_合并.xlsx"),
validation_output=paths_dict.get("validation_output", "物料状态校验结果.xlsx"),
),
extraction=ExtractionConfig(
batch_size=extraction_dict.get("batch_size", 100),
verbose=extraction_dict.get("verbose", True),
auto_convert=extraction_dict.get("auto_convert", True),
merge_batches=extraction_dict.get("merge_batches", True),
)
)
# 为了兼容旧代码,导入必要的类型
from config.schema import ERPConfig, DatabaseConfig, PathConfig, ExtractionConfig

143
config/schema.py Normal file
View File

@@ -0,0 +1,143 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
配置结构定义
使用 dataclass 定义所有配置项的结构和类型。
"""
from dataclasses import dataclass, field
from typing import Optional
from pathlib import Path
@dataclass
class ERPConfig:
"""ERP 系统配置"""
url: str
username: str
password: str
headless: bool = True
ignore_https_errors: bool = True
auto_close_browser: bool = True
def validate(self) -> list[str]:
"""验证配置,返回错误列表"""
errors = []
if not self.url:
errors.append("ERP URL 不能为空")
if not self.username:
errors.append("ERP 用户名不能为空")
if not self.password:
errors.append("ERP 密码不能为空")
return errors
@dataclass
class DatabaseConfig:
"""数据库配置"""
server: str
database: str
username: str
password: str
driver: str = "ODBC Driver 18 for SQL Server"
trust_server_certificate: str = "yes"
def validate(self) -> list[str]:
"""验证配置,返回错误列表"""
errors = []
if not self.server:
errors.append("数据库服务器地址不能为空")
if not self.database:
errors.append("数据库名称不能为空")
if not self.username:
errors.append("数据库用户名不能为空")
if not self.password:
errors.append("数据库密码不能为空")
return errors
@dataclass
class PathConfig:
"""文件路径配置"""
data_dir: str
production_id_file: str
default_output: str = "离散备料计划维护_合并.xlsx"
validation_output: str = "物料状态校验结果.xlsx"
def validate(self) -> list[str]:
"""验证配置,返回错误列表"""
errors = []
if not self.data_dir:
errors.append("数据目录路径不能为空")
if not self.production_id_file:
errors.append("ProductionID 文件路径不能为空")
return errors
@dataclass
class ExtractionConfig:
"""数据提取配置"""
batch_size: int = 100
verbose: bool = True
auto_convert: bool = True
merge_batches: bool = True
def validate(self) -> list[str]:
"""验证配置,返回错误列表"""
errors = []
if self.batch_size <= 0:
errors.append("批次大小必须大于 0")
if self.batch_size > 1000:
errors.append("批次大小不应超过 1000")
return errors
@dataclass
class AppConfig:
"""应用总配置"""
erp: ERPConfig
database: DatabaseConfig
paths: PathConfig
extraction: ExtractionConfig
def validate(self) -> list[str]:
"""验证所有配置,返回错误列表"""
errors = []
errors.extend(self.erp.validate())
errors.extend(self.database.validate())
errors.extend(self.paths.validate())
errors.extend(self.extraction.validate())
return errors
def to_dict(self) -> dict:
"""转换为字典格式(用于保存到 JSON"""
return {
"erp": {
"url": self.erp.url,
"username": self.erp.username,
"password": self.erp.password,
"headless": self.erp.headless,
"ignore_https_errors": self.erp.ignore_https_errors,
"auto_close_browser": self.erp.auto_close_browser,
},
"database": {
"server": self.database.server,
"database": self.database.database,
"username": self.database.username,
"password": self.database.password,
"driver": self.database.driver,
"trust_server_certificate": self.database.trust_server_certificate,
},
"paths": {
"data_dir": self.paths.data_dir,
"production_id_file": self.paths.production_id_file,
"default_output": self.paths.default_output,
"validation_output": self.paths.validation_output,
},
"extraction": {
"batch_size": self.extraction.batch_size,
"verbose": self.extraction.verbose,
"auto_convert": self.extraction.auto_convert,
"merge_batches": self.extraction.merge_batches,
}
}

View File

@@ -1,40 +1,23 @@
"""
用户配置模板
用户配置模板(已废弃,保留用于向后兼容)
此文件包含用户配置的默认值和结构说明。
实际配置保存在 config/user_settings.json
此文件已迁移到:
- config/schema.py - 配置结构定义
- config/defaults.py - 默认配置值
请使用以下方式导入:
```python
from config.defaults import DEFAULT_APP_CONFIG
from config.schema import AppConfig, ERPConfig, DatabaseConfig
```
"""
DEFAULT_SETTINGS = {
"erp": {
"url": "https://68.11.34.30:8082/",
"username": "BLDpengqiangqiang",
"password": "Cqbld123456.",
"headless": True,
"ignore_https_errors": True,
"auto_close_browser": True
},
"database": {
"server": "192.168.110.114",
"database": "CompanyDB",
"username": "peng",
"password": "Cqbld123456."
},
"browser": {
"headless": True,
"ignore_https_errors": True,
"auto_close": True
},
"paths": {
"data_dir": "D:/python/playwrite/data/",
"production_id_file": "ProductionID.txt",
"default_output": "离散备料计划维护_合并.xlsx",
"validation_output": "物料状态校验结果.xlsx"
},
"extraction": {
"batch_size": 100,
"verbose": True,
"auto_convert": True,
"merge_batches": True
}
}
# 为了向后兼容,保留旧版本导入
from config.defaults import DEFAULT_SETTINGS_DICT as DEFAULT_SETTINGS
from config.schema import (
AppConfig,
ERPConfig,
DatabaseConfig,
PathConfig,
ExtractionConfig,
)