feat: migrate configuration to .env environment variables

This commit implements a complete migration from JSON-based configuration
to .env environment variables, providing better security and flexibility.

Key Changes:
- Add python-dotenv dependency for environment variable support
- Create config/env_loader.py with type conversion utilities
- Add from_env() class methods to all config dataclasses
- Update ConfigLoader to prioritize environment variables
- Add save_to_env() method for .env file management
- Implement database connection factory pattern
- Add base DAO and connection classes for better abstraction
- Support both SQL Server and MySQL with unified interface
- Create migration script (scripts/migrate_to_env.py)
- Update GUI to read/write .env files
- Add comprehensive migration documentation

New Files:
- config/env_loader.py - Environment variable loader
- db/base_connection.py - Base database connection interface
- db/base_dao.py - Base DAO with common utilities
- db/connection_factory.py - Factory for creating connections
- db/mysql_connection.py - MySQL-specific connection
- db/sqlserver_connection.py - SQL Server-specific connection
- db/table_name_converter.py - SQL dialect converter
- scripts/migrate_to_env.py - Configuration migration tool
- docs/ENV_MIGRATION.md - Complete migration guide
- .env.example - Environment variable template

Testing:
- Verified MySQL connection (8.0.44)
- Tested all DAO operations
- Confirmed 150 tables accessible
- Validated configuration loading

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Misaka
2026-02-09 22:39:14 +08:00
parent 04b99292ad
commit aaa46ef282
22 changed files with 2545 additions and 563 deletions

View File

@@ -8,6 +8,13 @@
from dataclasses import dataclass, field
from typing import Optional
from pathlib import Path
from enum import Enum
class DatabaseType(str, Enum):
"""数据库类型枚举"""
SQLSERVER = "sqlserver"
MYSQL = "mysql"
@dataclass
@@ -21,6 +28,20 @@ class ERPConfig:
ignore_https_errors: bool = True
auto_close_browser: bool = True
@classmethod
def from_env(cls) -> "ERPConfig":
"""从环境变量创建配置"""
from config.env_loader import get_env, get_env_bool
return cls(
url=get_env("ERP_URL", "https://68.11.34.30:8082/"),
username=get_env("ERP_USERNAME", "BLDpengqiangqiang"),
password=get_env("ERP_PASSWORD", ""),
headless=get_env_bool("ERP_HEADLESS", True),
ignore_https_errors=get_env_bool("ERP_IGNORE_HTTPS_ERRORS", True),
auto_close_browser=get_env_bool("ERP_AUTO_CLOSE_BROWSER", True),
)
def validate(self) -> list[str]:
"""验证配置,返回错误列表"""
errors = []
@@ -33,28 +54,98 @@ class ERPConfig:
return errors
@dataclass
class SQLServerConfig:
"""SQL Server 特定配置"""
driver: str = "ODBC Driver 18 for SQL Server"
trust_server_certificate: str = "yes"
@classmethod
def from_env(cls) -> "SQLServerConfig":
"""从环境变量创建配置"""
from config.env_loader import get_env
return cls(
driver=get_env("DB_SQLSERVER_DRIVER", "ODBC Driver 18 for SQL Server"),
trust_server_certificate=get_env("DB_TRUST_SERVER_CERTIFICATE", "yes"),
)
@dataclass
class MySQLConfig:
"""MySQL 特定配置"""
host: str = ""
port: int = 3306
charset: str = "utf8mb4"
@classmethod
def from_env(cls) -> "MySQLConfig":
"""从环境变量创建配置"""
from config.env_loader import get_env, get_env_int
return cls(
host=get_env("DB_MYSQL_HOST", "192.168.31.83"),
port=get_env_int("DB_MYSQL_PORT", 3306),
charset=get_env("DB_MYSQL_CHARSET", "utf8mb4"),
)
@dataclass
class DatabaseConfig:
"""数据库配置"""
server: str
database: str
username: str
password: str
driver: str = "ODBC Driver 18 for SQL Server"
trust_server_certificate: str = "yes"
db_type: DatabaseType = DatabaseType.SQLSERVER
server: str = "" # SQL Server 服务器地址
database: str = ""
username: str = ""
password: str = ""
sqlserver: Optional[SQLServerConfig] = None
mysql: Optional[MySQLConfig] = None
@classmethod
def from_env(cls) -> "DatabaseConfig":
"""从环境变量创建配置"""
from config.env_loader import get_env, get_env_int
db_type_str = get_env("DB_TYPE", "sqlserver")
try:
db_type = DatabaseType(db_type_str)
except ValueError:
db_type = DatabaseType.SQLSERVER
return cls(
db_type=db_type,
server=get_env("DB_SERVER", "192.168.110.114"),
database=get_env("DB_NAME", "CompanyDB"),
username=get_env("DB_USERNAME", "peng"),
password=get_env("DB_PASSWORD", ""),
sqlserver=SQLServerConfig.from_env(),
mysql=MySQLConfig.from_env(),
)
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("数据库密码不能为空")
if self.db_type == DatabaseType.SQLSERVER:
if not self.server:
errors.append("SQL Server 服务器地址不能为空")
if not self.database:
errors.append("数据库名不能为空")
if not self.username:
errors.append("数据库用户名不能为空")
if not self.password:
errors.append("数据库密码不能为空")
elif self.db_type == DatabaseType.MYSQL:
if self.mysql and not self.mysql.host:
errors.append("MySQL 主机地址不能为空")
if not self.database:
errors.append("数据库名称不能为空")
if not self.username:
errors.append("数据库用户名不能为空")
if not self.password:
errors.append("数据库密码不能为空")
return errors
@@ -67,6 +158,18 @@ class PathConfig:
default_output: str = "离散备料计划维护_合并.xlsx"
validation_output: str = "物料状态校验结果.xlsx"
@classmethod
def from_env(cls) -> "PathConfig":
"""从环境变量创建配置"""
from config.env_loader import get_env
return cls(
data_dir=get_env("PATH_DATA_DIR", "D:/python/playwrite/data/"),
production_id_file=get_env("PATH_PRODUCTION_ID_FILE", "ProductionID.txt"),
default_output=get_env("PATH_DEFAULT_OUTPUT", "离散备料计划维护_合并.xlsx"),
validation_output=get_env("PATH_VALIDATION_OUTPUT", "物料状态校验结果.xlsx"),
)
def validate(self) -> list[str]:
"""验证配置,返回错误列表"""
errors = []
@@ -87,6 +190,19 @@ class ExtractionConfig:
merge_batches: bool = True
enable_db_persistence: bool = False
@classmethod
def from_env(cls) -> "ExtractionConfig":
"""从环境变量创建配置"""
from config.env_loader import get_env_int, get_env_bool
return cls(
batch_size=get_env_int("EXTRACTION_BATCH_SIZE", 100),
verbose=get_env_bool("EXTRACTION_VERBOSE", True),
auto_convert=get_env_bool("EXTRACTION_AUTO_CONVERT", True),
merge_batches=get_env_bool("EXTRACTION_MERGE_BATCHES", True),
enable_db_persistence=get_env_bool("EXTRACTION_ENABLE_DB_PERSISTENCE", False),
)
def validate(self) -> list[str]:
"""验证配置,返回错误列表"""
errors = []
@@ -108,6 +224,20 @@ class ValidationConfig:
default_manager: str = ""
match_mode: str = "substring"
@classmethod
def from_env(cls) -> "ValidationConfig":
"""从环境变量创建配置"""
from config.env_loader import get_env, get_env_int, get_env_bool
return cls(
data_source=get_env("VALIDATION_DATA_SOURCE", "database_full"),
use_database=get_env_bool("VALIDATION_USE_DATABASE", True),
batch_size=get_env_int("VALIDATION_BATCH_SIZE", 2000),
enable_crud_operations=get_env_bool("VALIDATION_ENABLE_CRUD", False),
default_manager=get_env("VALIDATION_DEFAULT_MANAGER", ""),
match_mode=get_env("VALIDATION_MATCH_MODE", "substring"),
)
def validate(self) -> list[str]:
"""验证配置,返回错误列表"""
errors = []
@@ -149,6 +279,17 @@ class AppConfig:
extraction: ExtractionConfig
validation: ValidationConfig
@classmethod
def from_env(cls) -> "AppConfig":
"""从环境变量创建配置"""
return cls(
erp=ERPConfig.from_env(),
database=DatabaseConfig.from_env(),
paths=PathConfig.from_env(),
extraction=ExtractionConfig.from_env(),
validation=ValidationConfig.from_env(),
)
def validate(self) -> list[str]:
"""验证所有配置,返回错误列表"""
errors = []
@@ -171,12 +312,20 @@ class AppConfig:
"auto_close_browser": self.erp.auto_close_browser,
},
"database": {
"db_type": self.database.db_type.value,
"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,
"sqlserver": {
"driver": self.database.sqlserver.driver if self.database.sqlserver else "ODBC Driver 18 for SQL Server",
"trust_server_certificate": self.database.sqlserver.trust_server_certificate if self.database.sqlserver else "yes",
},
"mysql": {
"host": self.database.mysql.host if self.database.mysql else "",
"port": self.database.mysql.port if self.database.mysql else 3306,
"charset": self.database.mysql.charset if self.database.mysql else "utf8mb4",
},
},
"paths": {
"data_dir": self.paths.data_dir,