feat: implement user type-based UI access control

Restrict settings interface and material validation features based on user type:

- SettingsTab: User type only sees "Test ERP Connection" and "Test Database Connection" buttons; Admin sees full configuration interface
- MaterialValidationTab: Hide manager filter and data source options from non-admin users; default to database_filtered mode
- test_db_connection(): Read from config directly to support User type without UI variables
- Add backward compatibility: no session_manager defaults to full interface

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-02-10 17:30:26 +08:00
parent db461e39f9
commit d18bedcd77
4 changed files with 219 additions and 79 deletions

View File

@@ -0,0 +1,105 @@
# 用户类型限制设置界面功能
## 概述
实现了基于用户类型的设置界面访问控制。对于 `User` 类型的用户,设置界面仅显示"测试ERP连接"和"测试数据库连接"两个按钮,隐藏所有配置组。`Admin` 类型用户可以看到完整的设置界面。
## 修改的文件
### 1. `gui/settings_tab.py`
#### 构造函数修改
- 添加了 `session_manager` 可选参数
- 保存 `session_manager` 实例用于用户类型判断
#### `create_widgets()` 方法修改
- 添加用户类型判断逻辑
- 对于 `User` 类型用户,跳过创建所有配置组
- 根据用户类型显示不同的按钮组合
#### `load_settings()` 方法修改
- 添加用户类型判断
- 对于 `User` 类型用户,跳过加载配置到界面(因为没有界面元素)
#### `test_db_connection()` 方法修改
-`self.config` 直接读取配置而不是从 UI 变量
- 这样即使没有 UI 变量User 也能测试数据库连接
### 2. `gui/main_window.py`
#### `create_widgets()` 方法修改
- 传入 `session_manager``SettingsTab` 构造函数
## 功能特性
### User 类型用户
- **可见内容**: 仅两个按钮
- 测试 ERP 连接
- 测试数据库连接
- **隐藏内容**: 所有配置组
- ERP 系统配置
- 数据库配置
- 浏览器设置
- 路径设置
- 数据提取设置
- 物料校验设置
- **隐藏按钮**:
- 保存设置
- 恢复默认
### Admin 类型用户
- 显示完整的设置界面(所有配置组和所有按钮)
### 向后兼容
- 如果不传入 `session_manager` 参数,默认显示完整界面
- 这确保了不破坏现有代码
## 测试
### 自动化测试
运行 `tests/test_user_settings_restriction.py`:
```bash
.venv/Scripts/python tests/test_user_settings_restriction.py
```
测试覆盖:
1. User 类型用户不显示任何配置组
2. Admin 类型用户显示所有配置组
3. 无 session_manager 时显示所有配置(向后兼容)
### 手动测试
运行 `tests/manual_test_user_settings.py`:
```bash
.venv/Scripts/python tests/manual_test_user_settings.py
```
选择测试场景后,可以直观地查看设置界面的显示效果。
## 验证步骤
1. 启动应用,使用 `User` 类型用户登录
2. 切换到"设置"标签页
3. 确认只显示"测试ERP连接"和"测试数据库连接"两个按钮
4. 确认所有配置组都不显示
5. 使用 `Admin` 类型用户登录,确认设置界面显示完整内容
## 技术细节
### 用户类型判断
```python
is_user_only = self.session_manager and self.session_manager.get_user_type() == 'User'
```
### 配置读取
对于测试功能,直接从 `ConfigManager` 读取配置:
```python
db_type = self.config.get("database.db_type", "sqlserver")
```
这确保了即使没有 UI 变量,测试功能也能正常工作。
## 未来扩展
如果需要为不同用户类型提供更多自定义功能,可以在 `create_widgets()` 方法中添加更多条件判断,例如:
- 为不同用户类型显示不同的配置组
- 为不同用户类型提供不同的默认值
- 添加更多用户类型(如 `Guest`, `Manager` 等)

View File

@@ -83,8 +83,8 @@ class MainWindow:
self.query_tab = DataQueryTab(self.notebook, self.config)
self.notebook.add(self.query_tab, text="数据查询")
# 设置标签页
self.settings_tab = SettingsTab(self.notebook, self.config)
# 设置标签页(传入 session_manager
self.settings_tab = SettingsTab(self.notebook, self.config, self.session_manager)
self.notebook.add(self.settings_tab, text="设置")
def create_status_bar(self):

View File

@@ -171,13 +171,18 @@ class MaterialValidationTab(ttk.Frame):
control_frame = ttk.Frame(main_container)
control_frame.pack(fill=tk.X, pady=(0, 10))
# 负责人筛选区域
manager_filter_frame = ttk.LabelFrame(
main_container,
text="筛选(按负责人)",
padding=10
)
manager_filter_frame.pack(fill=tk.X, pady=(0, 10))
# 负责人筛选区域 - 仅管理员可见
# PERMISSION CHECK: 非管理员用户不显示筛选区域
if self.session_manager.is_admin():
manager_filter_frame = ttk.LabelFrame(
main_container,
text="筛选(按负责人)",
padding=10
)
manager_filter_frame.pack(fill=tk.X, pady=(0, 10))
else:
# 普通用户:创建一个空容器,避免后续代码出错
manager_filter_frame = ttk.Frame(main_container)
# 中部:结果表格
result_frame = ttk.LabelFrame(main_container, text="校验结果", padding=5)
@@ -197,28 +202,33 @@ class MaterialValidationTab(ttk.Frame):
def _create_control_panel(self, parent):
"""创建控制面板"""
# 数据来源选择
source_group = ttk.LabelFrame(parent, text="数据来源", padding=10)
source_group.pack(fill=tk.X, pady=5)
# 数据来源选择 - 仅管理员可见
# PERMISSION CHECK: 非管理员用户不显示数据来源选项
if self.session_manager.is_admin():
source_group = ttk.LabelFrame(parent, text="数据来源", padding=10)
source_group.pack(fill=tk.X, pady=5)
self.source_mode = tk.StringVar(value="database_full")
self.source_mode = tk.StringVar(value="database_full")
# 数据库模式
ttk.Radiobutton(
source_group,
text="数据库 - 全表校验",
variable=self.source_mode,
value="database_full",
command=self._on_source_mode_change,
).grid(row=0, column=0, sticky="w", padx=5)
# 数据库模式
ttk.Radiobutton(
source_group,
text="数据库 - 全表校验",
variable=self.source_mode,
value="database_full",
command=self._on_source_mode_change,
).grid(row=0, column=0, sticky="w", padx=5)
ttk.Radiobutton(
source_group,
text="数据库 - ProductionID 过滤",
variable=self.source_mode,
value="database_filtered",
command=self._on_source_mode_change,
).grid(row=0, column=1, sticky="w", padx=5)
ttk.Radiobutton(
source_group,
text="数据库 - ProductionID 过滤",
variable=self.source_mode,
value="database_filtered",
command=self._on_source_mode_change,
).grid(row=0, column=1, sticky="w", padx=5)
else:
# 普通用户:默认使用 database_filtered 模式
self.source_mode = tk.StringVar(value="database_filtered")
# 文件选择
file_group = ttk.LabelFrame(parent, text="文件选择", padding=10)
@@ -243,18 +253,15 @@ class MaterialValidationTab(ttk.Frame):
)
self.db_filtered_production_id_selector.pack(fill=tk.X)
# 输出文件
output_frame = ttk.Frame(file_group)
output_frame.grid(row=1, column=0, columnspan=2, sticky="ew", pady=(10, 0))
# 输出文件(对于所有用户都可见)
self.output_file_selector = FileSelector(
output_frame,
file_group,
label_text="输出文件:",
file_type="file",
file_types=[("Excel 文件", "*.xlsx"), ("所有文件", "*.*")],
initial_dir=self.config.get("paths.data_dir", "data/"),
)
self.output_file_selector.pack(fill=tk.X)
self.output_file_selector.pack(fill=tk.X, pady=(10, 0))
# 设置默认输出
default_output = os.path.join(
@@ -370,20 +377,31 @@ class MaterialValidationTab(ttk.Frame):
def _on_source_mode_change(self):
"""数据源模式切换"""
# PERMISSION CHECK: 普通用户固定为 database_filtered 模式
if not self.session_manager.is_admin():
# 对于普通用户pack 数据库过滤模式框架
self.db_full_frame.pack_forget()
self.db_filtered_frame.pack(fill=tk.X, pady=(0, 5))
return
mode = self.source_mode.get()
# 隐藏所有文件选择框架
self.db_full_frame.grid_remove()
self.db_filtered_frame.grid_remove()
self.db_full_frame.pack_forget()
self.db_filtered_frame.pack_forget()
# 根据模式显示对应的文件选择器
if mode == "database_full":
self.db_full_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
self.db_full_frame.pack(fill=tk.X, pady=(0, 5))
elif mode == "database_filtered":
self.db_filtered_frame.grid(row=0, column=0, columnspan=2, sticky="ew")
self.db_filtered_frame.pack(fill=tk.X, pady=(0, 5))
def _create_manager_filter_area(self, parent):
"""创建负责人筛选区域"""
# PERMISSION CHECK: 非管理员用户不显示筛选区域
if not self.session_manager.is_admin():
return
# 创建复选框容器
canvas_container = ttk.Frame(parent)
canvas_container.pack(fill=tk.BOTH, expand=True)

View File

@@ -16,21 +16,26 @@ from config.schema import DatabaseType
class SettingsTab(ttk.Frame):
"""设置标签页"""
def __init__(self, parent, config: ConfigManager):
def __init__(self, parent, config: ConfigManager, session_manager=None):
"""
初始化设置标签页
Args:
parent: 父容器
config: 配置管理器
session_manager: 会话管理器,用于获取用户类型
"""
super().__init__(parent)
self.config = config
self.session_manager = session_manager
self.create_widgets()
self.load_settings()
def create_widgets(self):
"""创建界面组件"""
# 判断用户类型
is_user_only = self.session_manager and self.session_manager.get_user_type() == 'User'
# 创建主容器,带滚动条
canvas = tk.Canvas(self)
scrollbar = ttk.Scrollbar(self, orient="vertical", command=canvas.yview)
@@ -43,40 +48,42 @@ class SettingsTab(ttk.Frame):
canvas.create_window((0, 0), window=scrollable_frame, anchor="nw")
canvas.configure(yscrollcommand=scrollbar.set)
# ERP 配置组
self._create_erp_group(scrollable_frame)
# 根据用户类型创建配置组
if not is_user_only:
# 管理员显示所有配置组
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)
# 数据库配置组
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
)
if is_user_only:
# 普通用户只显示测试按钮
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)
else:
# 管理员显示所有按钮
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")
@@ -326,6 +333,15 @@ class SettingsTab(ttk.Frame):
def load_settings(self):
"""从配置加载设置到界面"""
# 判断是否为仅测试用户模式
is_user_only = self.session_manager and self.session_manager.get_user_type() == 'User'
if is_user_only:
# 普通用户模式 - 只需要加载测试连接所需的配置
# 不需要加载设置到界面,因为界面没有配置输入框
return
# 管理员模式 - 加载所有配置
# ERP 设置
self.erp_url_var.set(self.config.get("erp.url", ""))
self.erp_username_var.set(self.config.get("erp.username", ""))
@@ -431,7 +447,8 @@ class SettingsTab(ttk.Frame):
def test_db_connection(self):
"""测试数据库连接"""
db_type = self.db_type_var.get()
# 从配置读取而不是从 UI 变量(支持 User 类型用户)
db_type = self.config.get("database.db_type", "sqlserver")
try:
if db_type == "mysql":
@@ -439,11 +456,11 @@ class SettingsTab(ttk.Frame):
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(),
host=self.config.get("database.mysql.host", ""),
port=self.config.get("database.mysql.port", 3306),
database=self.config.get("database.database", ""),
user=self.config.get("database.username", ""),
password=self.config.get("database.password", ""),
connection_timeout=5
)
conn.close()
@@ -451,10 +468,10 @@ class SettingsTab(ttk.Frame):
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"SERVER={self.config.get('database.server', '')};"
f"DATABASE={self.config.get('database.database', '')};"
f"UID={self.config.get('database.username', '')};"
f"PWD={self.config.get('database.password', '')};"
f"TrustServerCertificate=yes;"
)
conn = pyodbc.connect(conn_str, timeout=5)