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:
@@ -4,6 +4,7 @@
|
||||
配置管理器
|
||||
|
||||
负责加载、保存和管理用户配置。
|
||||
支持从环境变量和 .env 文件加载配置。
|
||||
"""
|
||||
import os
|
||||
from typing import TYPE_CHECKING
|
||||
@@ -19,15 +20,17 @@ if TYPE_CHECKING:
|
||||
class ConfigManager:
|
||||
"""配置管理器"""
|
||||
|
||||
def __init__(self, config_file: str = "config/user_settings.json"):
|
||||
def __init__(self, config_file: str = "config/user_settings.json", use_env: bool = True):
|
||||
"""
|
||||
初始化配置管理器
|
||||
|
||||
Args:
|
||||
config_file: 配置文件路径
|
||||
config_file: 配置文件路径(向后兼容)
|
||||
use_env: 是否使用环境变量,默认为 True
|
||||
"""
|
||||
self.config_file = config_file
|
||||
self.config: AppConfig = ConfigLoader.load(config_file)
|
||||
self.use_env = use_env
|
||||
self.config: AppConfig = ConfigLoader.load(config_file, use_env=use_env)
|
||||
|
||||
# 验证配置
|
||||
errors = self.config.validate()
|
||||
@@ -40,10 +43,16 @@ class ConfigManager:
|
||||
"""
|
||||
保存配置到文件
|
||||
|
||||
如果使用环境变量,则保存到 .env 文件
|
||||
否则保存到 JSON 文件(向后兼容)
|
||||
|
||||
Returns:
|
||||
保存是否成功
|
||||
"""
|
||||
return ConfigLoader.save(self.config, self.config_file)
|
||||
if self.use_env:
|
||||
return ConfigLoader.save_to_env(self.config, ".env")
|
||||
else:
|
||||
return ConfigLoader.save(self.config, self.config_file)
|
||||
|
||||
def get(self, key: str, default=None):
|
||||
"""
|
||||
@@ -90,7 +99,7 @@ class ConfigManager:
|
||||
|
||||
def reset_to_defaults(self) -> None:
|
||||
"""重置为默认配置"""
|
||||
self.config = ConfigLoader.load("default") # 重新加载默认配置
|
||||
self.config = AppConfig.from_env() # 重新从环境变量加载默认配置
|
||||
self.save()
|
||||
|
||||
@property
|
||||
|
||||
@@ -10,6 +10,7 @@ import tkinter as tk
|
||||
from tkinter import ttk, messagebox
|
||||
import pyodbc
|
||||
from gui.config_manager import ConfigManager
|
||||
from config.schema import DatabaseType
|
||||
|
||||
|
||||
class SettingsTab(ttk.Frame):
|
||||
@@ -116,35 +117,74 @@ class SettingsTab(ttk.Frame):
|
||||
group = ttk.LabelFrame(parent, text="数据库配置", padding=10)
|
||||
group.grid(row=1, column=0, columnspan=2, pady=10, padx=10, sticky="ew")
|
||||
|
||||
# 服务器
|
||||
ttk.Label(group, text="服务器:").grid(row=0, column=0, sticky="w", pady=5)
|
||||
# 数据库类型选择
|
||||
ttk.Label(group, text="数据库类型:").grid(row=0, column=0, sticky="w", pady=5)
|
||||
self.db_type_var = tk.StringVar()
|
||||
db_type_combo = ttk.Combobox(
|
||||
group,
|
||||
textvariable=self.db_type_var,
|
||||
values=["sqlserver", "mysql"],
|
||||
state="readonly",
|
||||
width=30,
|
||||
)
|
||||
db_type_combo.grid(row=0, column=1, sticky="w", pady=5)
|
||||
db_type_combo.bind("<<ComboboxSelected>>", self._on_db_type_changed)
|
||||
|
||||
# SQL Server 配置
|
||||
self.sqlserver_frame = ttk.Frame(group)
|
||||
self.sqlserver_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=5)
|
||||
|
||||
ttk.Label(self.sqlserver_frame, text="服务器:").grid(row=0, column=0, sticky="w", pady=5)
|
||||
self.db_server_var = tk.StringVar()
|
||||
ttk.Entry(group, textvariable=self.db_server_var, width=50).grid(
|
||||
ttk.Entry(self.sqlserver_frame, textvariable=self.db_server_var, width=50).grid(
|
||||
row=0, column=1, pady=5, sticky="ew"
|
||||
)
|
||||
|
||||
# 数据库名
|
||||
ttk.Label(group, text="数据库:").grid(row=1, column=0, sticky="w", pady=5)
|
||||
self.db_name_var = tk.StringVar()
|
||||
ttk.Entry(group, textvariable=self.db_name_var, width=50).grid(
|
||||
row=1, column=1, pady=5, sticky="ew"
|
||||
# MySQL 配置
|
||||
self.mysql_frame = ttk.Frame(group)
|
||||
|
||||
ttk.Label(self.mysql_frame, text="主机:").grid(row=0, column=0, sticky="w", pady=5)
|
||||
self.mysql_host_var = tk.StringVar()
|
||||
ttk.Entry(self.mysql_frame, textvariable=self.mysql_host_var, width=50).grid(
|
||||
row=0, column=1, pady=5, sticky="ew"
|
||||
)
|
||||
|
||||
# 用户名
|
||||
ttk.Label(group, text="用户名:").grid(row=2, column=0, sticky="w", pady=5)
|
||||
self.db_username_var = tk.StringVar()
|
||||
ttk.Entry(group, textvariable=self.db_username_var, width=50).grid(
|
||||
ttk.Label(self.mysql_frame, text="端口:").grid(row=1, column=0, sticky="w", pady=5)
|
||||
self.mysql_port_var = tk.IntVar(value=3306)
|
||||
ttk.Spinbox(
|
||||
self.mysql_frame, from_=1, to=65535, textvariable=self.mysql_port_var, width=10
|
||||
).grid(row=1, column=1, sticky="w", pady=5)
|
||||
|
||||
# 通用配置(两种数据库都需要)
|
||||
ttk.Label(group, text="数据库:").grid(row=2, column=0, sticky="w", pady=5)
|
||||
self.db_name_var = tk.StringVar()
|
||||
ttk.Entry(group, textvariable=self.db_name_var, width=50).grid(
|
||||
row=2, column=1, pady=5, sticky="ew"
|
||||
)
|
||||
|
||||
# 密码
|
||||
ttk.Label(group, text="密码:").grid(row=3, column=0, sticky="w", pady=5)
|
||||
ttk.Label(group, text="用户名:").grid(row=3, column=0, sticky="w", pady=5)
|
||||
self.db_username_var = tk.StringVar()
|
||||
ttk.Entry(group, textvariable=self.db_username_var, width=50).grid(
|
||||
row=3, column=1, pady=5, sticky="ew"
|
||||
)
|
||||
|
||||
ttk.Label(group, text="密码:").grid(row=4, column=0, sticky="w", pady=5)
|
||||
self.db_password_var = tk.StringVar()
|
||||
entry = ttk.Entry(group, textvariable=self.db_password_var, width=50, show="*")
|
||||
entry.grid(row=3, column=1, pady=5, sticky="ew")
|
||||
entry.grid(row=4, column=1, pady=5, sticky="ew")
|
||||
|
||||
group.columnconfigure(1, weight=1)
|
||||
|
||||
def _on_db_type_changed(self, event=None):
|
||||
"""数据库类型改变时的回调"""
|
||||
db_type = self.db_type_var.get()
|
||||
if db_type == "mysql":
|
||||
self.sqlserver_frame.grid_forget()
|
||||
self.mysql_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=5)
|
||||
else:
|
||||
self.mysql_frame.grid_forget()
|
||||
self.sqlserver_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=5)
|
||||
|
||||
def _create_browser_group(self, parent):
|
||||
"""创建浏览器配置组"""
|
||||
group = ttk.LabelFrame(parent, text="浏览器设置", padding=10)
|
||||
@@ -224,7 +264,7 @@ class SettingsTab(ttk.Frame):
|
||||
# 数据库持久化
|
||||
self.enable_db_persistence_var = tk.BooleanVar()
|
||||
ttk.Checkbutton(
|
||||
group, text="保存到数据库 (同时写入 SQL Server)", variable=self.enable_db_persistence_var
|
||||
group, text="保存到数据库", variable=self.enable_db_persistence_var
|
||||
).grid(row=4, column=0, columnspan=2, sticky="w", pady=5)
|
||||
|
||||
def _create_validation_group(self, parent):
|
||||
@@ -292,11 +332,23 @@ class SettingsTab(ttk.Frame):
|
||||
self.erp_password_var.set(self.config.get("erp.password", ""))
|
||||
|
||||
# 数据库设置
|
||||
self.db_server_var.set(self.config.get("database.server", ""))
|
||||
db_type = self.config.get("database.db_type", "sqlserver")
|
||||
self.db_type_var.set(db_type)
|
||||
|
||||
if db_type == "mysql":
|
||||
self.db_server_var.set(self.config.get("database.server", ""))
|
||||
self.mysql_host_var.set(self.config.get("database.mysql.host", ""))
|
||||
self.mysql_port_var.set(self.config.get("database.mysql.port", 3306))
|
||||
else:
|
||||
self.db_server_var.set(self.config.get("database.server", ""))
|
||||
|
||||
self.db_name_var.set(self.config.get("database.database", ""))
|
||||
self.db_username_var.set(self.config.get("database.username", ""))
|
||||
self.db_password_var.set(self.config.get("database.password", ""))
|
||||
|
||||
# 更新界面显示
|
||||
self._on_db_type_changed()
|
||||
|
||||
# 浏览器设置(已合并到 ERP 配置中)
|
||||
self.browser_headless_var.set(self.config.get("erp.headless", True))
|
||||
self.browser_ignore_https_var.set(
|
||||
@@ -331,7 +383,18 @@ class SettingsTab(ttk.Frame):
|
||||
self.config.set("erp.password", self.erp_password_var.get())
|
||||
|
||||
# 数据库设置
|
||||
self.config.set("database.server", self.db_server_var.get())
|
||||
db_type = self.db_type_var.get()
|
||||
self.config.set("database.db_type", db_type)
|
||||
|
||||
if db_type == "mysql":
|
||||
# MySQL: 使用 host 字段
|
||||
self.config.set("database.server", self.mysql_host_var.get())
|
||||
self.config.set("database.mysql.host", self.mysql_host_var.get())
|
||||
self.config.set("database.mysql.port", self.mysql_port_var.get())
|
||||
else:
|
||||
# SQL Server: 使用 server 字段
|
||||
self.config.set("database.server", self.db_server_var.get())
|
||||
|
||||
self.config.set("database.database", self.db_name_var.get())
|
||||
self.config.set("database.username", self.db_username_var.get())
|
||||
self.config.set("database.password", self.db_password_var.get())
|
||||
@@ -368,20 +431,41 @@ class SettingsTab(ttk.Frame):
|
||||
|
||||
def test_db_connection(self):
|
||||
"""测试数据库连接"""
|
||||
db_type = self.db_type_var.get()
|
||||
|
||||
try:
|
||||
conn_str = (
|
||||
f"DRIVER={{ODBC Driver 18 for SQL Server}};"
|
||||
f"SERVER={self.db_server_var.get()};"
|
||||
f"DATABASE={self.db_name_var.get()};"
|
||||
f"UID={self.db_username_var.get()};"
|
||||
f"PWD={self.db_password_var.get()};"
|
||||
f"TrustServerCertificate=yes;"
|
||||
)
|
||||
if db_type == "mysql":
|
||||
import mysql.connector
|
||||
from mysql.connector import Error
|
||||
|
||||
conn = pyodbc.connect(conn_str, timeout=5)
|
||||
conn.close()
|
||||
messagebox.showinfo("成功", "数据库连接测试成功!")
|
||||
conn = mysql.connector.connect(
|
||||
host=self.mysql_host_var.get(),
|
||||
port=self.mysql_port_var.get(),
|
||||
database=self.db_name_var.get(),
|
||||
user=self.db_username_var.get(),
|
||||
password=self.db_password_var.get(),
|
||||
connection_timeout=5
|
||||
)
|
||||
conn.close()
|
||||
messagebox.showinfo("成功", "MySQL 数据库连接测试成功!")
|
||||
else:
|
||||
conn_str = (
|
||||
f"DRIVER={{ODBC Driver 18 for SQL Server}};"
|
||||
f"SERVER={self.db_server_var.get()};"
|
||||
f"DATABASE={self.db_name_var.get()};"
|
||||
f"UID={self.db_username_var.get()};"
|
||||
f"PWD={self.db_password_var.get()};"
|
||||
f"TrustServerCertificate=yes;"
|
||||
)
|
||||
conn = pyodbc.connect(conn_str, timeout=5)
|
||||
conn.close()
|
||||
messagebox.showinfo("成功", "SQL Server 数据库连接测试成功!")
|
||||
|
||||
except ImportError:
|
||||
if db_type == "mysql":
|
||||
messagebox.showerror("错误", "未安装 mysql-connector-python,请运行:\npip install mysql-connector-python")
|
||||
else:
|
||||
messagebox.showerror("错误", "未安装 pyodbc,请运行:\npip install pyodbc")
|
||||
except Exception as e:
|
||||
messagebox.showerror("错误", f"数据库连接失败:\n{str(e)}")
|
||||
|
||||
@@ -392,7 +476,9 @@ class SettingsTab(ttk.Frame):
|
||||
|
||||
def reset_defaults(self):
|
||||
"""恢复默认设置"""
|
||||
if messagebox.askyesno("确认", "确定要恢复默认设置吗?"):
|
||||
self.config.reset_to_defaults()
|
||||
if messagebox.askyesno("确认", "确定要恢复默认设置吗?这将覆盖 .env 文件中的所有配置。"):
|
||||
from config.schema import AppConfig
|
||||
self.config.config = AppConfig.from_env() # 重新加载默认配置
|
||||
self.config.save()
|
||||
self.load_settings()
|
||||
messagebox.showinfo("成功", "已恢复默认设置")
|
||||
|
||||
Reference in New Issue
Block a user