feat: add MySQL database support alongside SQL Server
This commit implements multi-database support, allowing the system to switch
between SQL Server and MySQL databases seamlessly.
## New Features
- Database type selection (SQL Server or MySQL) via configuration
- Automatic table name conversion between formats ([dbo].[table] → dbo_table)
- Automatic parameter placeholder handling (? for SQL Server, %s for MySQL)
- GUI settings tab now includes database type dropdown and MySQL configuration
## Database Abstraction Layer
- db/base_connection.py: Abstract base class for database connections
- db/sqlserver_connection.py: SQL Server implementation
- db/mysql_connection.py: MySQL implementation using mysql-connector-python
- db/connection_factory.py: Factory pattern for creating connections
- db/table_name_converter.py: Table name format conversion utility
## DAO Base Class
- db/base_dao.py: Base DAO with helper methods for SQL conversion and placeholders
## Updated Components
- config/schema.py: Extended with DatabaseType enum and MySQL/SQLServer config classes
- config/defaults.py: Added MySQL default configuration
- config/loader.py: Updated to handle new database structure
- db/connection.py: Refactored to use factory pattern and load user config
- All DAO files: Updated to inherit from BaseDAO with automatic conversion
## Dependencies
- Added mysql-connector-python>=8.0.0 to requirements.txt
## Configuration
To use MySQL, set db_type to "mysql" in config/user_settings.json:
{
"database": {
"db_type": "mysql",
"mysql": {
"host": "192.168.31.83",
"port": 3306,
"database": "BLD_DB",
"username": "remote_user",
"password": "3.1415926Beeke"
}
}
}
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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)}")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user