Files
playwrite/gui/settings_tab.py
Misaka aaa46ef282 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>
2026-02-09 22:39:14 +08:00

485 lines
20 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
设置标签页
管理系统配置ERP、数据库、浏览器、路径等
"""
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):
"""设置标签页"""
def __init__(self, parent, config: ConfigManager):
"""
初始化设置标签页
Args:
parent: 父容器
config: 配置管理器
"""
super().__init__(parent)
self.config = config
self.create_widgets()
self.load_settings()
def create_widgets(self):
"""创建界面组件"""
# 创建主容器,带滚动条
canvas = tk.Canvas(self)
scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
scrollable_frame = ttk.Frame(canvas)
scrollable_frame.bind(
"<Configure>", lambda e: canvas.configure(scrollregion=canvas.bbox("all"))
)
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# ERP 配置组
self._create_erp_group(scrollable_frame)
# 数据库配置组
self._create_database_group(scrollable_frame)
# 浏览器配置组
self._create_browser_group(scrollable_frame)
# 路径配置组
self._create_paths_group(scrollable_frame)
# 处理配置组
self._create_extraction_group(scrollable_frame)
# 校验配置组
self._create_validation_group(scrollable_frame)
# 按钮区域
button_frame = ttk.Frame(scrollable_frame)
button_frame.grid(row=6, column=0, columnspan=2, pady=20, sticky="ew")
ttk.Button(
button_frame, text="测试 ERP 连接", command=self.test_erp_connection
).pack(side="left", padx=5)
ttk.Button(
button_frame, text="测试数据库连接", command=self.test_db_connection
).pack(side="left", padx=5)
ttk.Button(button_frame, text="保存设置", command=self.save_settings).pack(
side="left", padx=5
)
ttk.Button(button_frame, text="恢复默认", command=self.reset_defaults).pack(
side="left", padx=5
)
# 布局
canvas.grid(row=0, column=0, sticky="nsew")
scrollbar.grid(row=0, column=1, sticky="ns")
self.rowconfigure(0, weight=1)
self.columnconfigure(0, weight=1)
def _create_erp_group(self, parent):
"""创建 ERP 配置组"""
group = ttk.LabelFrame(parent, text="ERP 系统配置", padding=10)
group.grid(row=0, column=0, columnspan=2, pady=10, padx=10, sticky="ew")
# URL
ttk.Label(group, text="ERP URL:").grid(row=0, column=0, sticky="w", pady=5)
self.erp_url_var = tk.StringVar()
ttk.Entry(group, textvariable=self.erp_url_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.erp_username_var = tk.StringVar()
ttk.Entry(group, textvariable=self.erp_username_var, width=50).grid(
row=1, column=1, pady=5, sticky="ew"
)
# 密码
ttk.Label(group, text="密码:").grid(row=2, column=0, sticky="w", pady=5)
self.erp_password_var = tk.StringVar()
entry = ttk.Entry(group, textvariable=self.erp_password_var, width=50, show="*")
entry.grid(row=2, column=1, pady=5, sticky="ew")
group.columnconfigure(1, weight=1)
def _create_database_group(self, parent):
"""创建数据库配置组"""
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)
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(self.sqlserver_frame, textvariable=self.db_server_var, width=50).grid(
row=0, 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(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)
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=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)
group.grid(row=2, column=0, pady=10, padx=10, sticky="ew")
self.browser_headless_var = tk.BooleanVar()
ttk.Checkbutton(
group, text="无头模式 (不显示浏览器)", variable=self.browser_headless_var
).grid(row=0, column=0, sticky="w", pady=5)
self.browser_ignore_https_var = tk.BooleanVar()
ttk.Checkbutton(
group, text="忽略 HTTPS 错误", variable=self.browser_ignore_https_var
).grid(row=1, column=0, sticky="w", pady=5)
self.browser_auto_close_var = tk.BooleanVar()
ttk.Checkbutton(
group, text="操作完成后自动关闭浏览器", variable=self.browser_auto_close_var
).grid(row=2, column=0, sticky="w", pady=5)
def _create_paths_group(self, parent):
"""创建路径配置组"""
group = ttk.LabelFrame(parent, text="路径设置", padding=10)
group.grid(row=2, column=1, pady=10, padx=10, sticky="nsew")
from gui.widgets import FileSelector
# 数据目录
ttk.Label(group, text="数据目录:").grid(row=0, column=0, sticky="w", pady=5)
self.data_dir_selector = FileSelector(
group,
label_text="",
file_type="directory",
initial_dir="D:/python/playwrite/data/",
)
self.data_dir_selector.grid(row=1, column=0, columnspan=2, sticky="ew", pady=5)
# 默认输出文件
ttk.Label(group, text="默认输出文件:").grid(row=2, column=0, sticky="w", pady=5)
self.default_output_var = tk.StringVar()
ttk.Entry(group, textvariable=self.default_output_var, width=40).grid(
row=3, column=0, columnspan=2, sticky="ew", pady=5
)
group.columnconfigure(0, weight=1)
def _create_extraction_group(self, parent):
"""创建处理配置组"""
group = ttk.LabelFrame(parent, text="数据提取设置", padding=10)
group.grid(row=3, column=0, columnspan=2, pady=10, padx=10, sticky="ew")
# 批次大小
ttk.Label(group, text="批次大小:").grid(row=0, column=0, sticky="w", pady=5)
self.batch_size_var = tk.IntVar(value=100)
ttk.Spinbox(
group, from_=10, to=500, textvariable=self.batch_size_var, width=10
).grid(row=0, column=1, sticky="w", pady=5)
# 详细日志
self.verbose_var = tk.BooleanVar()
ttk.Checkbutton(group, text="启用详细日志", variable=self.verbose_var).grid(
row=1, column=0, columnspan=2, sticky="w", pady=5
)
# 自动转换
self.auto_convert_var = tk.BooleanVar()
ttk.Checkbutton(
group, text="自动转换 Excel 格式", variable=self.auto_convert_var
).grid(row=2, column=0, columnspan=2, sticky="w", pady=5)
# 合并批次
self.merge_batches_var = tk.BooleanVar()
ttk.Checkbutton(
group, text="自动合并批次数据", variable=self.merge_batches_var
).grid(row=3, column=0, columnspan=2, sticky="w", pady=5)
# 数据库持久化
self.enable_db_persistence_var = tk.BooleanVar()
ttk.Checkbutton(
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):
"""创建物料校验配置组"""
group = ttk.LabelFrame(parent, text="物料校验设置", padding=10)
group.grid(row=4, column=0, columnspan=2, pady=10, padx=10, sticky="ew")
# 数据源选择
ttk.Label(group, text="默认数据源:").grid(row=0, column=0, sticky="w", pady=5)
self.validation_data_source_var = tk.StringVar()
data_source_combo = ttk.Combobox(
group,
textvariable=self.validation_data_source_var,
values=["database_full", "database_filtered", "excel_existing", "excel_full"],
state="readonly",
width=30,
)
data_source_combo.grid(row=0, column=1, sticky="w", pady=5)
# 使用数据库
self.validation_use_database_var = tk.BooleanVar()
ttk.Checkbutton(
group, text="使用数据库作为数据源", variable=self.validation_use_database_var
).grid(row=1, column=0, columnspan=2, sticky="w", pady=5)
# 批次大小
ttk.Label(group, text="数据库批次大小:").grid(row=2, column=0, sticky="w", pady=5)
self.validation_batch_size_var = tk.IntVar(value=2000)
ttk.Spinbox(
group, from_=100, to=2000, textvariable=self.validation_batch_size_var, width=10
).grid(row=2, column=1, sticky="w", pady=5)
# 匹配模式
ttk.Label(group, text="匹配模式:").grid(row=3, column=0, sticky="w", pady=5)
self.validation_match_mode_var = tk.StringVar()
match_mode_combo = ttk.Combobox(
group,
textvariable=self.validation_match_mode_var,
values=["substring", "exact"],
state="readonly",
width=30,
)
match_mode_combo.grid(row=3, column=1, sticky="w", pady=5)
# CRUD 操作
self.validation_enable_crud_var = tk.BooleanVar()
ttk.Checkbutton(
group, text="启用 CRUD 操作(管理待删除物料)", variable=self.validation_enable_crud_var
).grid(row=4, column=0, columnspan=2, sticky="w", pady=5)
# 默认负责人
ttk.Label(group, text="默认负责人:").grid(row=5, column=0, sticky="w", pady=5)
self.validation_default_manager_var = tk.StringVar()
ttk.Entry(group, textvariable=self.validation_default_manager_var, width=30).grid(
row=5, column=1, sticky="w", pady=5
)
group.columnconfigure(1, weight=1)
def load_settings(self):
"""从配置加载设置到界面"""
# ERP 设置
self.erp_url_var.set(self.config.get("erp.url", ""))
self.erp_username_var.set(self.config.get("erp.username", ""))
self.erp_password_var.set(self.config.get("erp.password", ""))
# 数据库设置
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(
self.config.get("erp.ignore_https_errors", True)
)
self.browser_auto_close_var.set(self.config.get("erp.auto_close_browser", True))
# 路径设置
self.data_dir_selector.set(self.config.get("paths.data_dir", ""))
self.default_output_var.set(self.config.get("paths.default_output", ""))
# 处理设置
self.batch_size_var.set(self.config.get("extraction.batch_size", 100))
self.verbose_var.set(self.config.get("extraction.verbose", True))
self.auto_convert_var.set(self.config.get("extraction.auto_convert", True))
self.merge_batches_var.set(self.config.get("extraction.merge_batches", True))
self.enable_db_persistence_var.set(self.config.get("extraction.enable_db_persistence", False))
# 校验设置
self.validation_data_source_var.set(self.config.get("validation.data_source", "database_full"))
self.validation_use_database_var.set(self.config.get("validation.use_database", True))
self.validation_batch_size_var.set(self.config.get("validation.batch_size", 2000))
self.validation_match_mode_var.set(self.config.get("validation.match_mode", "substring"))
self.validation_enable_crud_var.set(self.config.get("validation.enable_crud_operations", False))
self.validation_default_manager_var.set(self.config.get("validation.default_manager", ""))
def save_settings(self):
"""保存界面设置到配置"""
# ERP 设置
self.config.set("erp.url", self.erp_url_var.get())
self.config.set("erp.username", self.erp_username_var.get())
self.config.set("erp.password", self.erp_password_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())
# 浏览器设置(已合并到 ERP 配置中)
self.config.set("erp.headless", self.browser_headless_var.get())
self.config.set("erp.ignore_https_errors", self.browser_ignore_https_var.get())
self.config.set("erp.auto_close_browser", self.browser_auto_close_var.get())
# 路径设置
self.config.set("paths.data_dir", self.data_dir_selector.get())
self.config.set("paths.default_output", self.default_output_var.get())
# 处理设置
self.config.set("extraction.batch_size", self.batch_size_var.get())
self.config.set("extraction.verbose", self.verbose_var.get())
self.config.set("extraction.auto_convert", self.auto_convert_var.get())
self.config.set("extraction.merge_batches", self.merge_batches_var.get())
self.config.set("extraction.enable_db_persistence", self.enable_db_persistence_var.get())
# 校验设置
self.config.set("validation.data_source", self.validation_data_source_var.get())
self.config.set("validation.use_database", self.validation_use_database_var.get())
self.config.set("validation.batch_size", self.validation_batch_size_var.get())
self.config.set("validation.match_mode", self.validation_match_mode_var.get())
self.config.set("validation.enable_crud_operations", self.validation_enable_crud_var.get())
self.config.set("validation.default_manager", self.validation_default_manager_var.get())
# 保存到文件
if self.config.save():
messagebox.showinfo("成功", "设置已保存")
else:
messagebox.showerror("错误", "保存设置失败")
def test_db_connection(self):
"""测试数据库连接"""
db_type = self.db_type_var.get()
try:
if db_type == "mysql":
import mysql.connector
from mysql.connector import Error
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)}")
def test_erp_connection(self):
"""测试 ERP 连接"""
# ERP 连接测试需要实际启动浏览器
messagebox.showinfo("提示", "ERP 连接测试将在数据提取功能中自动验证")
def reset_defaults(self):
"""恢复默认设置"""
if messagebox.askyesno("确认", "确定要恢复默认设置吗?这将覆盖 .env 文件中的所有配置。"):
from config.schema import AppConfig
self.config.config = AppConfig.from_env() # 重新加载默认配置
self.config.save()
self.load_settings()
messagebox.showinfo("成功", "已恢复默认设置")