From a95dc54855bada9e373b96dad5b97f2110379711 Mon Sep 17 00:00:00 2001 From: Aryenys <1518497205@qq.com> Date: Sat, 22 Nov 2025 17:05:31 +0800 Subject: [PATCH] v1.0 --- .../.github/copilot-instructions.md | 63 ++ class-call-system (1)/.gitignore | 50 ++ class-call-system (1)/README.md | 592 ++++++++++++++++++ .../assets/templates/student.xlsx | Bin 0 -> 11513 bytes .../assets/templates/学生名单模板.xlsx | Bin 0 -> 9891 bytes class-call-system (1)/class-call-system | 1 - class-call-system (1)/config/settings.json | 36 ++ ...随机点名系统_技术实现方案.md | 591 +++++++++++++++++ .../docs/prototype/项目原型.md | 0 class-call-system (1)/main.py | 11 + class-call-system (1)/requirements.txt | 7 + .../src/core/animation_engine.py | 67 ++ .../src/core/data_manager.py | 94 +++ .../src/core/random_selector.py | 55 ++ class-call-system (1)/src/gui/components.py | 128 ++++ class-call-system (1)/src/gui/main_window.py | 163 +++++ class-call-system (1)/src/gui/styles.py | 48 ++ class-call-system (1)/src/utils/database.py | 246 ++++++++ .../src/utils/file_importer.py | 176 ++++++ class-call-system (1)/src/utils/helpers.py | 46 ++ 20 files changed, 2373 insertions(+), 1 deletion(-) create mode 100644 class-call-system (1)/.github/copilot-instructions.md create mode 100644 class-call-system (1)/.gitignore create mode 100644 class-call-system (1)/README.md create mode 100644 class-call-system (1)/assets/templates/student.xlsx create mode 100644 class-call-system (1)/assets/templates/学生名单模板.xlsx delete mode 160000 class-call-system (1)/class-call-system create mode 100644 class-call-system (1)/config/settings.json create mode 100644 class-call-system (1)/docs/prototype/课堂随机点名系统_技术实现方案.md create mode 100644 class-call-system (1)/docs/prototype/项目原型.md create mode 100644 class-call-system (1)/main.py create mode 100644 class-call-system (1)/requirements.txt create mode 100644 class-call-system (1)/src/core/animation_engine.py create mode 100644 class-call-system (1)/src/core/data_manager.py create mode 100644 class-call-system (1)/src/core/random_selector.py create mode 100644 class-call-system (1)/src/gui/components.py create mode 100644 class-call-system (1)/src/gui/main_window.py create mode 100644 class-call-system (1)/src/gui/styles.py create mode 100644 class-call-system (1)/src/utils/database.py create mode 100644 class-call-system (1)/src/utils/file_importer.py create mode 100644 class-call-system (1)/src/utils/helpers.py diff --git a/class-call-system (1)/.github/copilot-instructions.md b/class-call-system (1)/.github/copilot-instructions.md new file mode 100644 index 0000000..7640d9b --- /dev/null +++ b/class-call-system (1)/.github/copilot-instructions.md @@ -0,0 +1,63 @@ + + +# Copilot / AI Agent 使用说明(精简) + +本文件面向 AI 编码代理,提供该项目的「可操作」知识:架构概览、关键文件、常见数据形态、运行/调试指令与代码风格约定。 + +**High-level Architecture**: 本项目是一个基于 Python + Tkinter 的桌面应用。 +- **UI 层**: `src/gui/`(`main_window.py`, `components.py`, `styles.py`),使用 `ttkbootstrap` 主题。 +- **业务/算法层**: `src/core/`(`data_manager.py`, `random_selector.py`, `animation_engine.py`)。`DataManager` 是中间协调层。 +- **工具/基础层**: `src/utils/`(`database.py`, `file_importer.py`, `helpers.py`)。负责 DB、Excel 导入/导出、通用函数。 +- **配置**: `config/settings.json`(数据库类型、Excel 模板路径、动画与评分规则等)。 +- **入口**: `main.py`(使用 `ttkbootstrap.Window` 启动 `MainWindow`)。 + +**Important Singletons & side-effects**: +- `src/utils/database.py` 在模块导入时会创建 `db_instance`(会自动 connect 并 create_tables)。 +- `src/utils/file_importer.py` 在导入时会创建 `excel_importer` 单例并生成模板文件(会访问 `config/settings.json`)。 +- `src/core/random_selector.py` 会在模块末尾创建 `random_selector` 与 `order_selector` 单例。 +- `src/core/data_manager.py` 在模块导入时尝试创建 `data_manager`,若 DB 初始化失败会设为 `None`(注意上层消费需检查)。 + +这些单例会在模块导入阶段导致 I/O(数据库连接、文件写入、读取配置),AI 在修改这些模块时要谨慎:避免在导入时执行长时间或不必要的副作用。 + +**Config / Path Conventions**: +- 多数 util 模块通过计算 `project_root`(基于 `__file__` 的父目录)来定位 `config/settings.json`,请遵循相同做法以保证路径一致性。 +- `config/settings.json` 控制 `database.type`(`mysql` 或 `sqlite`)、表名、动画参数、导出模板路径等。 + +**Database specifics**: +- 默认使用 MySQL(项目当前配置连接到 `db4free.net`)。代码在多个位置调用 `db_instance.reconnect()` 来处理远程免费 MySQL 的断连问题。 +- `Database` 会根据 `database.type` 选择 MySQL 或 SQLite,并在初始化时创建所需表。 +- 学生行的数据结构在代码中被视为 tuple: `(student_id, name, major, total_score, random_call_num)`。许多函数依赖此固定索引顺序。 + +**Common return shapes / APIs**: +- Excel 导入/导出方法返回 `Tuple[bool, str]`(成功标志,信息),例如 `excel_importer.import_students(...)`。 +- 调用选人相关函数返回 `(student_tuple, message)` 或 `(None, error_msg)`,请在 UI 层检查返回值再使用。 + +**Key files to reference when making changes**: +- `main.py` — 运行入口,展示如何初始化 UI 与关闭 DB:`db_instance.close()`。 +- `src/core/data_manager.py` — 中央协调器,所有 UI 操作通过它与 DB/算法层交互。 +- `src/utils/database.py` — DB 连接、建表、增删改查;对 SQL 语句和事务敏感。 +- `src/utils/file_importer.py` — Excel 导入/导出、模板生成与日志记录(使用 `logging`)。 +- `src/gui/components.py` — 自定义组件定义(表格列名/列数在此处有硬编码),修改表结构时请同时更新这里。 + +**Developer workflows / commands**: +- 安装依赖:`pip install -r requirements.txt`(`requirements.txt` 列出了 `pandas`, `openpyxl`, `ttkbootstrap`, `Pillow`, `pymysql` 等)。 +- 运行应用:在项目根目录执行 `python main.py`。 +- 配置数据库:编辑 `config/settings.json` 中的 `database` 部分;代码对 `db4free` 环境做了重连与更长超时处理,但仍可能不稳定。 + +**Project-specific conventions & gotchas (useful for AI changes)**: +- 避免在模块导入时做大量阻塞 I/O;若需延迟初始化,优先把连接逻辑放到工厂函数或类的 `init` 方法中(项目当前有若干模块在导入时创建实例,修改时需注意影响范围)。 +- 多处使用 `db_instance.reconnect()`:数据库操作实现中常在入口处重连以处理长连接断开,新增 DB 调用请保留相同策略。 +- 硬编码的 tuple 索引(学生记录)是跨模块约定:在调整字段或列顺序前,先更新 `StudentTable` 的 `columns` 定义与所有消费处的索引。 +- 配置路径和模板路径均基于 `project_root` 拼接;遵循现有 path 计算逻辑可避免相对路径错误(参见 `file_importer.py` 与 `database.py`)。 + +**Example snippets (how to call things correctly)**: +- 获取学生列表(UI 层常用): `students = data_manager.get_all_students()` → 每个 item 是 `(id, name, major, total_score, random_call_num)`。 +- 随机点名(调用链): UI -> `data_manager.random_call()` -> `random_selector.select()` -> `AnimationEngine.start_roll()`。 +- 导入 Excel: UI 调用 `data_manager.import_excel(file_path)`;返回 `(True, msg)` 或 `(False, err_msg)`。 + +如果本文件有不完整或需要补充的地方,请指出你关心的具体区域(比如:数据库迁移、去导入模块的副作用、或将 UI 分离为服务端 API 等),我会据此迭代补充内容。 + +*** 结束 *** diff --git a/class-call-system (1)/.gitignore b/class-call-system (1)/.gitignore new file mode 100644 index 0000000..37c45e1 --- /dev/null +++ b/class-call-system (1)/.gitignore @@ -0,0 +1,50 @@ +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# 包文件 +*.egg +*.egg-info +dist/ +build/ +pip-wheel-metadata/ + +# IDE +.vscode/ +.idea/ +*.swp +*.swo +*~ + +# 系统文件 +.DS_Store +Thumbs.db +Desktop.ini + +# 数据库文件(SQLite) +*.db +*.sqlite3 +*.sqlite + +# 日志文件 +*.log +logs/ + +# 临时文件 +*.tmp +*.temp +~$* + +# 配置文件(如果包含敏感信息) +# config/settings.json + +# Excel 导出文件(可选) +export/temp_* \ No newline at end of file diff --git a/class-call-system (1)/README.md b/class-call-system (1)/README.md new file mode 100644 index 0000000..cd77e73 --- /dev/null +++ b/class-call-system (1)/README.md @@ -0,0 +1,592 @@ + +# 课堂随机点名系统技术实现方案 + +**文件名:** 课堂随机点名系统_技术实现方案.md + +
+

课堂随机点名系统技术实现方案

+

基于Python Tkinter的桌面应用开发方案

+
+ +| 文档版本 | 编写日期 | +|---------|----------| +| v1.0 | 2025年11月22日 | + +## 1. 技术栈选择说明 + +
+

💡 技术选型决策依据

+

基于教育场景的特殊需求,选择Python + Tkinter技术栈具备明显的优势对比

+
+ +### 1.1 Python + Tkinter技术优势分析 + +
+
+

🎯 开发效率优势

+ +
+
+

💻 部署便捷性

+ +
+
+

📊 教育场景适配

+ +
+
+ +### 1.2 技术栈对比分析 + +
+ +| 技术方案 | 开发难度 | 部署复杂度 | 性能表现 | 扩展性 | 维护成本 | +|---------|---------|-----------|---------|--------|---------| +| Python + Tkinter | | | 中等 | | | +| Web方案(HTML+JS) | 中等 | | | | 中等 | +| C# WinForms | 中等 | 中等 | | 中等 | 中等 | +| Java Swing | | | | | | + +
+ +### 1.3 核心依赖库选择 + +```python +# 核心依赖库配置 +required_libraries = { + "tkinter": "内置GUI框架", + "sqlite3": "内置轻量级数据库", + "pandas": "Excel文件处理和数据操作", + "openpyxl": "Excel文件读写支持", + "datetime": "日期时间处理", + "random": "随机算法实现", + "json": "配置文件处理" +} +``` + +
+⚠️ 注意事项: 优先使用Python内置库,减少外部依赖,确保部署稳定性 +
+ +## 2. 核心模块功能实现方案 + +### 2.1 系统架构设计 + +```mermaid +graph TB + A[用户界面层] --> B[业务逻辑层] + B --> C[数据访问层] + + A1[主窗口Manager] --> A2[界面组件] + A2 --> A3[事件处理] + + B1[点名引擎] --> B2[学生管理] + B2 --> B3[历史记录] + B3 --> B4[统计计算] + + C1[SQLite数据库] --> C2[Excel文件] + C2 --> C3[配置文件] +``` + +### 2.2 点名引擎模块实现 + +
+

🎲 随机算法核心实现

+
+ +```python +class RandomSelector: + def __init__(self, student_list): + self.students = student_list + self.selected_history = [] + + def weighted_random_selection(self): + """加权随机选择算法,优先选择未点到学生""" + if not self.students: + return None + + # 计算权重:未点到学生权重高,已点到学生权重低 + weights = [] + for student in self.students: + point_count = self.get_recent_point_count(student.id) + weight = max(1, 10 - point_count) # 最近点到次数越少,权重越高 + weights.append(weight) + + total_weight = sum(weights) + if total_weight == 0: + return random.choice(self.students) + + # 执行加权随机选择 + rand_val = random.uniform(0, total_weight) + cumulative = 0 + for i, weight in enumerate(weights): + cumulative += weight + if rand_val <= cumulative: + return self.students[i] +``` + +### 2.3 动画效果实现方案 + +```python +class AnimationManager: + def __init__(self, canvas_widget): + self.canvas = canvas_widget + self.animation_id = None + self.is_animating = False + + def start_roll_animation(self, duration=3000): + """开始滚动动画效果""" + self.is_animating = True + self.animation_start_time = time.time() + self.animate_roll() + + def animate_roll(self): + if not self.is_animating: + return + + elapsed = time.time() - self.animation_start_time + progress = min(elapsed / 3.0, 1.0) # 3秒动画周期 + + # 计算当前显示的学生索引(非线性缓动效果) + current_index = self.calculate_current_index(progress) + self.display_student(current_index) + + if progress < 1.0: + # 继续动画,使用缓动函数调整速度 + interval = self.calculate_interval(progress) + self.canvas.after(int(interval * 1000), self.animate_roll) + else: + self.finalize_selection() +``` + +### 2.4 文件导入模块实现 + +```python +class ExcelImporter: + def __init__(self): + self.supported_formats = ['.xlsx', '.xls', '.csv'] + + def import_students(self, file_path): + """导入Excel文件并解析学生数据""" + try: + if file_path.endswith('.csv'): + df = pd.read_csv(file_path) + else: + df = pd.read_excel(file_path, engine='openpyxl') + + students = [] + required_columns = ['学号', '姓名', '班级'] + + # 验证文件格式 + if not all(col in df.columns for col in required_columns): + raise ValueError("文件格式错误:缺少必要列") + + for _, row in df.iterrows(): + student = Student( + id=str(row['学号']), + name=str(row['姓名']), + class_name=str(row['班级']) + ) + students.append(student) + + return students + + except Exception as e: + raise Exception(f"文件导入失败:{str(e)}") +``` + +## 3. 数据库设计 + +### 3.1 数据库架构设计 + +```mermaid +erDiagram + STUDENT ||--o{ POINT_HISTORY : has + STUDENT { + string student_id PK "学号" + string name "姓名" + string class_name "班级" + datetime created_at "创建时间" + datetime updated_at "更新时间" + } + POINT_HISTORY { + int history_id PK "记录ID" + string student_id FK "学号" + datetime point_time "点名时间" + string point_type "点名类型" + string note "备注" + } + SYSTEM_CONFIG { + string config_key PK "配置键" + string config_value "配置值" + datetime updated_at "更新时间" + } +``` + +### 3.2 数据表详细设计 + +
+ +| 表名 | 字段名 | 数据类型 | 约束 | 说明 | +|------|--------|---------|------|------| +| **students** | student_id | VARCHAR(20) | PRIMARY KEY | 学生学号 | +| | name | VARCHAR(50) | NOT NULL | 学生姓名 | +| | class_name | VARCHAR(50) | NOT NULL | 班级名称 | +| | created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| | updated_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 更新时间 | +| **point_history** | history_id | INTEGER | PRIMARY KEY AUTOINCREMENT | 记录ID | +| | student_id | VARCHAR(20) | FOREIGN KEY | 学生学号 | +| | point_time | DATETIME | NOT NULL | 点名时间 | +| | point_type | VARCHAR(20) | DEFAULT 'normal' | 点名类型 | +| | note | TEXT | | 备注信息 | +| **system_config** | config_key | VARCHAR(50) | PRIMARY KEY | 配置键 | +| | config_value | TEXT | | 配置值 | +| | updated_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 更新时间 | + +
+ +### 3.3 数据库操作封装 + +```python +class DatabaseManager: + def __init__(self, db_path="classroom_points.db"): + self.db_path = db_path + self.init_database() + + def init_database(self): + """初始化数据库表结构""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 创建学生表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS students ( + student_id VARCHAR(20) PRIMARY KEY, + name VARCHAR(50) NOT NULL, + class_name VARCHAR(50) NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 创建点名历史表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS point_history ( + history_id INTEGER PRIMARY KEY AUTOINCREMENT, + student_id VARCHAR(20), + point_time DATETIME NOT NULL, + point_type VARCHAR(20) DEFAULT 'normal', + note TEXT, + FOREIGN KEY (student_id) REFERENCES students (student_id) + ) + ''') + + conn.commit() + conn.close() +``` + +## 4. 界面开发指南 + +### 4.1 主界面布局实现 + +```python +class MainApplication: + def __init__(self): + self.root = tk.Tk() + self.root.title("课堂点名系统") + self.root.geometry("1200x800") + self.root.configure(bg="#ECF0F1") + + self.setup_layout() + self.create_widgets() + + def setup_layout(self): + """设置主界面布局结构""" + # 顶部功能区 + self.top_frame = tk.Frame(self.root, height=80, bg="#3498DB") + self.top_frame.pack(fill=tk.X, padx=10, pady=5) + + # 主内容区 + self.main_frame = tk.Frame(self.root, bg="white") + self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 底部状态栏 + self.status_frame = tk.Frame(self.root, height=40, bg="#2C3E50") + self.status_frame.pack(fill=tk.X, side=tk.BOTTOM) +``` + +### 4.2 组件样式定制 + +```python +def setup_styles(self): + """配置Tkinter样式""" + style = ttk.Style() + + # 配置主按钮样式 + style.configure('Primary.TButton', + background='#3498DB', + foreground='white', + font=('微软雅黑', 11, 'bold'), + padding=(20, 10)) + + # 配置表格样式 + style.configure('Treeview', + font=('微软雅黑', 10), + rowheight=25) + style.configure('Treeview.Heading', + font=('微软雅黑', 11, 'bold')) +``` + +### 4.3 响应式布局适配 + +
+✅ 最佳实践: 使用Grid布局管理器实现灵活的响应式设计 +
+ +```python +def create_responsive_layout(self): + """创建响应式布局""" + # 左侧导航区 + self.nav_frame = tk.Frame(self.main_frame, width=250, bg="#F8F9FA") + self.nav_frame.grid(row=0, column=0, rowspan=2, sticky="nswe", padx=(0, 10)) + + # 中央内容区 + self.content_frame = tk.Frame(self.main_frame, bg="white") + self.content_frame.grid(row=0, column=1, sticky="nswe") + + # 右侧统计区 + self.stats_frame = tk.Frame(self.main_frame, width=200, bg="#F8F9FA") + self.stats_frame.grid(row=0, column=2, rowspan=2, sticky="nswe", padx=(10, 0)) + + # 配置权重使中央区域可伸缩 + self.main_frame.grid_columnconfigure(1, weight=1) + self.main_frame.grid_rowconfigure(0, weight=1) +``` + +## 5. 部署和运行说明 + +### 5.1 环境要求与依赖安装 + +
+ +| 环境组件 | 版本要求 | 安装方法 | 验证命令 | +|---------|---------|---------|---------| +| Python | 3.8+ | 官网下载安装 | `python --version` | +| pandas | 1.5.0+ | `pip install pandas` | `python -c "import pandas; print(pandas.__version__)"` | +| openpyxl | 3.0.0+ | `pip install openpyxl` | `python -c "import openpyxl; print(openpyxl.__version__)"` | +| 操作系统 | Win7+/macOS 10.12+/Ubuntu 16.04+ | - | - | + +
+ +### 5.2 项目目录结构 + +``` +课堂点名系统/ +├── main.py # 主程序入口 +├── requirements.txt # 依赖包列表 +├── config/ +│ ├── settings.json # 配置文件 +│ └── database.db # SQLite数据库 +├── src/ +│ ├── gui/ # 界面模块 +│ │ ├── main_window.py # 主窗口 +│ │ ├── components.py # 组件类 +│ │ └── styles.py # 样式定义 +│ ├── core/ # 核心逻辑 +│ │ ├── random_selector.py # 随机算法 +│ │ ├── data_manager.py # 数据管理 +│ │ └── animation_engine.py # 动画引擎 +│ └── utils/ # 工具类 +│ ├── file_importer.py # 文件导入 +│ ├── database.py # 数据库操作 +│ └── helpers.py # 辅助函数 +├── assets/ # 资源文件 +│ ├── icons/ # 图标资源 +│ └── templates/ # Excel模板 +└── docs/ # 文档 + └── 使用说明.md # 用户手册 +``` + +### 5.3 打包发布方案 + +```python +# pyinstaller打包配置(spec文件) +# classroom_points.spec + +block_cipher = None + +a = Analysis( + ['main.py'], + pathex=[], + binaries=[], + datas=[ + ('assets/', 'assets/'), + ('config/', 'config/') + ], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='课堂点名系统', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, # 设置为True可显示控制台窗口 + icon='assets/icon.ico' +) +``` + +### 5.4 部署操作指南 + +
+📋 部署步骤: 按照以下步骤完成系统部署 +
+ +**步骤1:环境准备** +```bash +# 1. 安装Python 3.8或更高版本 +# 2. 下载项目代码 +git clone <项目仓库> +cd 课堂点名系统 + +# 3. 安装依赖 +pip install -r requirements.txt +``` + +**步骤2:首次运行配置** +```bash +# 1. 运行主程序 +python main.py + +# 2. 系统将自动创建配置文件和数据文件 +# 3. 根据需要调整系统设置 +``` + +**步骤3:打包分发(可选)** +```bash +# 使用PyInstaller打包为可执行文件 +pip install pyinstaller +pyinstaller classroom_points.spec + +# 生成的exe文件在dist目录下 +``` + +### 5.5 故障排除指南 + +
+
+

❌ 常见问题1:依赖包安装失败

+

解决方案:使用国内镜像源安装
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt

+
+
+

❌ 常见问题2:Excel导入失败

+

解决方案:检查Excel文件格式,确保包含学号、姓名、班级三列

+
+
+

❌ 常见问题3:界面显示异常

+

解决方案:调整系统DPI设置或使用兼容模式运行

+
+
+ +## 6. 性能优化建议 + +### 6.1 数据库性能优化 + +```python +def optimize_database_performance(self): + """数据库性能优化配置""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 启用WAL模式提高并发性能 + cursor.execute("PRAGMA journal_mode=WAL") + + # 设置合适的缓存大小 + cursor.execute("PRAGMA cache_size=-64000") # 64MB缓存 + + # 创建索引提升查询性能 + cursor.execute("CREATE INDEX IF NOT EXISTS idx_student_class ON students(class_name)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_history_time ON point_history(point_time)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_history_student ON point_history(student_id)") + + conn.commit() + conn.close() +``` + +### 6.2 内存管理优化 + +
+

💡 内存优化策略

+ +
+ +## 7. 安全与稳定性保障 + +### 7.1 数据安全措施 + +- **数据备份机制**:自动定期备份数据库文件 +- **输入验证**:对所有用户输入进行严格验证 +- **异常处理**:完善的异常捕获和处理机制 +- **日志记录**:详细的操作日志记录系统 + +### 7.2 系统稳定性策略 + +```python +def setup_error_handling(self): + """设置全局异常处理""" + def global_exception_handler(exc_type, exc_value, exc_traceback): + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return + + logger.error("未捕获的异常:", exc_info=(exc_type, exc_value, exc_traceback)) + + # 显示用户友好的错误信息 + messagebox.showerror( + "系统错误", + "发生意外错误,程序将继续运行。\n错误信息已记录到日志。" + ) + + sys.excepthook = global_exception_handler +``` + +--- + +**文档完成时间:2025年11月22日** +"# class-call-system" diff --git a/class-call-system (1)/assets/templates/student.xlsx b/class-call-system (1)/assets/templates/student.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..872cf5a822874ccb2a657affb3729df26db7be79 GIT binary patch literal 11513 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 IDBtYC4(!0c;s%A!3-26OVp~ zZH(;ei97XV;zvM^7Lk(OzHh3a8Y^~toM?;0R{MKBSR&MI^ zK+dPtDIlK+wXtqHOfX)>GCQ#~YKn*tW~Cm-8D7hIf4AF&V$#^6Zxv;>DRUlNSdBmP zPS`vH<6EjYILtisnHSxg*f6PU*RF0$rLd96Fu~ZA26n!id13Gk-V%P~y_7V$r}3>$ zCuT-{^5UFz$IURl=FEgdEGFnu&|kXU@kZvY)mJ>FQD`-q0d?ORAEkU(*r|JfqDM<|B!Z3-IEF)jL@9`@_YsP8ZG*~Dtn;7-w1 zSTYSLU}~bDm-?%;>4aI%UJ0w1;S6mMl@A_RMW(-FOnC!p+3D5YsZb7do?+|ItE*(& z51ncF#9#x*qNqKv0Bydl15A!?hp+S1A0ub0W zA+SIFJM8*aHii$lhsX3i;Qr=Z>_&LiU5`r1Syh|{dOJiy3=^xede;>1sel3A$PJ&& zNM4~1c*OP~rGdq!nX{Yw3Soz)6giF(?oCELcKi(1rUfYMKz5Z^!Dn1u$Xgv?BB%X+ z?VD^FeUqw1>lLylrdY3>C34OAylfc3g0BGs(qpWS(%P8I!x|oQ+wOb^f&f%DG1>VgR`H|h9k9f?v2eLmO4V^G5)Z+O%(TywhnqTT7s=i}eaT0UqN?knTVCZfnRz^{Lz|9mU$T42jd?SNR4_`MjgV*$UM zNzdV$y0Wn5wH^C}7hSIVb>J$MA&aY5b-_Cu>iBN4wzs&W##^_EQMj&wfMO-qy~_;sNj6Xf4V1SC|0@ z@b|(!?#Yo!mIM}W1%#Sp0sG1lNtXVU)Z*L(%bpziGDmHAAl{UOzzseswU&@!9)JY` zg@P9>94|xSHbR3Wcr@=Qp_QdJUW2Y{mmv;qqgpp3>kcC1y1r zd}9Vv2ecferB~k(1^E^oAa>GzF#_lM!(~3InpUN_*+M}aXc`CKVNFvzRI$wVTiZ0M z9U}?2fGUI&eCk4gN^hQ?iW4O8i`E~mywbif2}Y+Do9=MFecZH#N!c1(3K+NQE7d31 z{g9#^Hm;_hLnG~!AHMP)U# zO4qJ?KCu}vMwQQI>J}=cp=P5>Unq6(hqWYf;TqeYE3eK+Pw?umy;xH7;#H@l>+hX= zaeH;?WF5}SAeI=D5^oniPyN<0c`#iWt@!$st;`ov0AX5-|Q6`)09Ip#fdjXpbzjhVxg%F+~Ie3S|zO>$`;uz5crc&SC z!qW~ZE;h^6-)OgEc+_Pzg;-y9$ zNg$FIe?rSQg1>5p*=vXa6qdR;z)G~^JVrjw+p_1y9{?^bzN_5&jG0L{W298w2`dcV zYa$)lzi@awO=)we9Z|$Zy!6WD8yxNZBU6e}QX8x(#f~!4T}=Lxc@n?w*yn+=flo0% z!pF%9`3Z48fk91Yh*=J=F^Oo3aI~b+Q>omU+t2`3 zjyd(DPNl_pFNA?{HT;kE)+Nd!>Z~1LV9WAT@dX=S<4dCJ)+WiZI&B=WP*l?5?4zI@ z#%dwBgU{6gzyslYLm(e1J#$bAb0=90Kcnv>!jozd8!|wU3(xio(`X?Bp)Sbz5Fx*% zH~_XdvLPC5H)QTsCuV)walaK;e99lX< zg9*fuQ`Dy;6~~HDX=Ya;=M(fF*)=N@0}8M3XVp#Pg$(hu7z`(E+qRDBJ<>jyUw0}K zkUsC!9%jG(Xwg+aJkXaZq7Nw{fkD9|RsVoR6bWaj6CPTVuB> zyJjiFlU%iX={pki44o(sY54LFY_-e3aX7&%bX4mBrC5>xGKkIGRY%{$6^|uu`6F>A z74`Am@MZuqkXN&-45d<jcpa93gr3?;Snwd`#JD8ccKG6D-Ohf)u zsDdAuT)*cpL6ICEa-xgd*T+l3^i{e(sKQ6(sbVJ#5%gS;(nQg?5k(v8nQRytv8yX{ zCclud^vG;)&^c~Xr^GgNdayU8bP5&%Puc|Q>jA?AB)cHBSy+C34_5Lgae=r%WXc)s z@tdaJiE-q4DIBIxdKS7`3pXS7TRum2{fC&~m&WQllNu@xHdhF(W6WifJNB-hQqrJx z77(RN>E0xr-iemR4%2zZfqKmnc?qJVL-^e(^40XeRU4LxL|*igT9U-wMhtJE;xl zWl#WbBWoy9@Q=Eq9mX3ZBPO;IQC z(u%X!ufS1HQC!N?Jny$J6;8G2CW*2I z(mcK&AK|Pysde06wNtg++&FWgGnnV2-=(wqNeh$^|`UFygQDQ-n=sKH=;?&z` z!KcNK`uH+u<038!V{cz>d&)lY%14r4ZjDHgRsy%o)uR}b%Ql8-e|s~IkXeReG6iW zt}^epfWE>`@qiTu(b!)TCckBnf5}as?flGETV(&xl%qpVK7e3|lTJ?{O5u@Q*jWRO z?}0Uk@CnZabQkTMdjdw+Tt1Eu4=X@K9WE#>5ou5I<(SNExXF#hvnXFo{abs33iJDVBepUZIkYdg=%AU?0u zBJbOB3Z>Q&OyA-zr;&Kjs7-r7KUi1;Ond@gS1J&V$b z2h1e=j{Y7rk);j1DEIqWLL8b^S^t=*D|hH{-$DX(dH?~+D7qpb-TV&4rr>@yrJ7Yr zy}j@q!4MP@@^v&aJjt~zQ0gT0bqR1^3rH@Blo&)4MEi{PEC?TieT=Q5@Z8~vKl*lF zQjTHkqD*J{b(+!8%OPnQriGLjF$W-a#JjD5E=rb{YZJPoLk4GqZ|J6#_FSrYXI(=> zHiO?JD-Bk`e8j5tQBFE5r*7k_Q`e{jvTB?&X$PuB~5N z6$pDkeZm6QAxNXPA!J>}Snx(ML^%N9!ea?)z&YY-qj{yVwd%EM?*aMA-}!XS>8I6h z+-3AP`V;#GdcK6RI(*RWns6}BEd*CnDPRUrP78H%cDjwzcsMO*UoUPF)GwzqQP5k! z&Rn7V9#5nxe3ia}#1q7DP*4w{H01Be)9mEK-w)Z z@4RdiLtko(LCOur-3wux*c+Sc%cj;`ittp1UpSibH^J#%e7j8@xK6jkyL&!<GN@ zP+`aE1D^Mu8!7jWtDhpL!w!Y|Vrw$HzU0#~$qk+OMN94MSRd^rPrlO*f?-nPD>OpY z3JpzXB`u?lB>VrCtzn|Pte|>!;4Kh66ZF2^w5eG9d2WwhU2Y@_orpAn=)~Ik-Y;0fQ z=scc2acdf?QmF;D7Pw$uLgU_rYlpQn0;`4V-k8EmVsjI=Jhqf+%Pwu@-$1+1`*H`) zYI>^JJ5@URCY=Kp^>c!qlWPL4%9u9-2hcY>7^fW0<@Lq(9jsrFjJ zKuyuN`(}dbA<$|>kV+bvx%QOjdHL&p_}T0)xgqno*UxtEIHC!L z5H8DN8K`_Ukv_{HGAtQ={+cIm29_e#?MpZccdyMn--u>Bi5*Tt+WwN4JIRSE&75-A zkwtdkyva%UkU9vyWbhcw+BHnTaVAQYHAtFqMQmx4uY%jotZ8@-gFE`nz01M9otVm= zR>?|;*5r|A*F1CWMza~cJk=*`ZXDVOMr>e0j_tvSco~QGl`(#M&r56~Be_v2Ko1IA zpHp}MliSCFY7uuieUQG8E-?)>UZ(?++Yy4zBy_j*w0U14#L7L7FS8!xtRcEx+&kLK*YR z5=b0uq6h_r_8*S0b2c~pK@M}8_mQ(=SoZ|M_h1`%fs`rfUCxCwc8$jAPn=tiQ^(Ta zU=*F78Bewa`>w>z35)q_tJNz+%X+tMkw@^vAH!-b3ToPGy5390?%#E^0IZD62a^p_ z>MbyM>~cG;Z|1L?&iPracv;TQ)>g`jzg66NubjRps#R0O`aF3&BXdxZ*=*nct)Q@% zxWdJv@6^RXn6DCXdo8ux!_XByrT%Q3Z1ypx>@XhdST^yd6&AAZMD*MzgkZAOxfB!U~0j{d9E3Y6ER{3j!H41 z$WB9}oZyUifg1q5UHLwewS6gWyKM9rWP1bpbo$2{vZv*0W&M7C|*`UKr+zlOCo!oi?LqphM%(Y3J9y3l-=or%-b z2zTb?%9xis8&cM+tW5 z<=w_m?hi9ElZ>M8$nqN+IIJdQ7Vxa z3_7lwdsZ?$grwE!<4dh73`XTCfY(R{_r|C(7h_?3d&A~UKKVKg-8Xmb2<{8>2S=46 z*DwC;$6SO-Ckv#)++?2Xy-luVGl^Bfg8P)^8y6_z}o9$x%rlz>LSm_0o56{uATrKXV>0fG8t^Gcl ze$KCDe5z5N(zCd7yks}I|8{(ex!=g%zV^&_Fe+m1Eg52V&^5N$*z=t%jkXip+S?@%zkbNI0x@{rm||J*3aJG?LlcO zdd4EoLZCz6!luG3!%~$lo)mp6HKM6y(~rUv{-N{d^@r12ext`4EOe#@>d6lhVY|jL z>sV;eI;T{ltYp`TInSp(dss*D)a(8E4{zA%uxOT!6Y|>kXnE?Y_hwR|EXmzB4ujkf zw9~&Sy9h1XFJNSOyKHrRc0c*t-!~nFI8=CMNv^kUd@ud{$@sv zfULWp3&rx9>s)>PtK0k5w;{yxQ=euBDriqb$lTL(Kq=ms6(cS$3Nfm%2cq}4S0l&v z8XA&;i`L?%dfJ5f(mAJ0$n0)fCWAGMk(OVMCN%!YaZG24?!}Z2r?qFemuc;*y zFrZ%DVKh|;A|J^`N6L{RiBa{7blW2?jT9qIjT9r!jTBpUb~kqwp1MJIWG6X@el7l% zTg7N*uz?ig-9~TDsl7~W+$@u{YErHw7vmH|F8v#mgYWLx4C!l@s^ppLwdRu`iI5dX zZtEqsZtb|0$20gavku*z&{g+-_-%gnPTe7N*D%)4{7@SceZ`}N8{+g%ulIwIW(qCO z&=}Sh9JVUFJX@Bz13_is@bf32R!exhSh=UI8p9L@2Xc!w-DM%zVpHa{9oEJeG;=sr z*s{7)Sh=JJLb*Kg6Ql(Qb2l~EvT_RKIjQ3X&3wCrQ_bRz-AC*CWsTM(9k0^sjDqCw ze1+%dgmz6;;?eapc{E-J`s*Rb8k=D&d_+`d>3Ee}X%r-$7-pyom?w)1Wsc zk0uMs@#)EWWWm5FC*|{KSQ^b5o{Vlx7u+O+ue2%ZoQ{0khT1J@M7F)@?S+rYdb=x_ ziOlHL%PQ|h{sD97u~SY)Uk`)4xKh*!GmC6u@$O;FHsS910Gg=c78Jv~{oPml4ENY* zj>HA^(DQCSV@Qh?4p8$cJ|g43HE}6fBJjR7@oa)fLAD_n#+vonJqjq3OcJ_bQM7R% z6JEOy&`V_MY#>t%I9bw0w)Wis3BUH;Oc46TVD{adCy-y8c`>(qLNw-uOxa(p_JnRJ zc(E%UH$e`;&j%sX%kLVluNWZ{dat~P*^^!;>IX@`l^d0A`SnlvUI}y4q`c~$VWMk=zxO$%g_+yM%F%MF)@5Tk zO|=Sgm`l^Qkog5k!6Yn(2FYqVg+>L}Pw#4<%=$DvI)D07;H>nH?Hae1zQTeC<>*8I zp|J3kz}Mb5mFLF(XV^mm8FSpr+Nt#%a@}M{X!a+Y!+F6QRx`moKHR43T{Ak zFZv*F;=SB|qk@buBo*+(l4rZ|c2Tdp=L*4h|IUSen zEnyzt_b<-#SDxHq_e6uSWAw|hJ_>T*9iA*_3y5IYb>akzwR_$z>O*lfUkcWUZs<0c ztMxuHl7XhI)b=U4(G43Z@qcX{SRRZ&oMVdOzYIhf6ha2(IHUM4j-aP?KRKgYQrt}c zMlcsKof&-M<&3IUE4X@6Jr&aZ=%Zp(aLVoF^fX9SWYtgRd{G7l&(qEJCjUB{rv-1W zneFcIyQ=ake3PY^UWA;71W=~$kbGx^o$qdA>tfxL%+@XfE?A6FmCG#A+n}Ay2S$5laPzt>`S28YEndt7Y zg#0?CsDJU(L^H8O+9io^)j*MfHlE$IKw8c4ZnddT)N-*8f(8t`3cv#KFo_a`Hm?Rf zG1s@^io{eU^HD07BvsGl2PkbIHn_~hewalV2_F_qd~krU>^lyz8;mSV7lvHWhMbck z4OteMO)cG;xt?PEi|7VJYTF-6`bw{`eyj$CJA=4d8e>Q0!CQ;y@o6uD&Cb{CC=xB6 zs6{CK28KCM1+?|;5T+r~$tKJyp42^mUd{FekSBtZ-H-p0`U&pM7Pr$f^{G_A*Myy%)1N zrZ0B}1NB#$0=?_1O_5SlhUz>rc zKzWH(a|x2!2|ll=PGB%SQvXR;T{(cnnm7l~3eE}vblxvEJ2%J~#3(g}Se`*6fxanS z0Xo2RV=Z@lM;!F2&gBsymXugUv6WS=6{pESLicI3h{I7%D^r&-bq^;#8x~2{U@Ci# zoGq|0Rxa6h{{daOyj;XF<1{@htQ?R#)UXs2)%^j%OfAO+#@CIYG)57zWz%lhAkqBC zYT$GRX0qs zl*=*V)$wlE_#e?$!I8SHF(k@Hga27D&=9t=v@^7{d#~sWFtpWrh_p-N`eb03Fayph zo7;0VSQG%dVY)g?hK1zIuw!SftS7)UpTn~>Y8@CM@33TX?!C*F`x_9dD6Lrtg~-_a zc**-yRcINP+_@4XqS%DvG^~$)3T;k^Y#Ojt!-rQNUa29r`Vb*YP9 zfVl|VX&Py&*sfF;y3k8=s2H~Kw6pyJ{7}$o^aR4)oFF!zkc?fS*M?P#EtJ|i++knW zW&Zj1fr|DT>eF82y@ACI`qwk=MO*Wy&C#_jbWPvE-}TvV5M(8xVK9GW#eO!J{JArb z2N(((iWoBQL#BV&r=JG?xq|$_I6ucjuZjxcFaG)6>7NYrp!3tI>t8xQ-k5)L_V7yn z91qSO#@~8qe)soh7tMq4Pk&T@SNl&-&7Tnc*$nbaI}P$k|K;?*ts#G^|JmK}OC1%G zbNZ$JSFgjr-Sk5d|L1tPX(LF26GAutrtmvT*+hTir#~0^Cy_n)`H52R-_`zy*8a5f zXZrP*9Zg7v?Vpmde_Hr6IrGbc7o17(d6u4hR0+;oo{PeqZ^YT^SF$KX +

课堂随机点名系统技术实现方案

+

基于Python Tkinter的桌面应用开发方案

+ + +| 文档版本 | 编写日期 | +|---------|----------| +| v1.0 | 2025年11月22日 | + +## 1. 技术栈选择说明 + +
+

💡 技术选型决策依据

+

基于教育场景的特殊需求,选择Python + Tkinter技术栈具备明显的优势对比

+
+ +### 1.1 Python + Tkinter技术优势分析 + +
+
+

🎯 开发效率优势

+
    +
  • Python语法简洁,开发周期缩短40%
  • +
  • 丰富的第三方库支持数据操作
  • +
  • 快速原型验证和迭代开发
  • +
+
+
+

💻 部署便捷性

+
    +
  • 跨平台兼容(Windows/macOS/Linux)
  • +
  • 无需复杂环境配置
  • +
  • 单文件打包分发
  • +
+
+
+

📊 教育场景适配

+
    +
  • 教育资源丰富,易于教学维护
  • +
  • 教师友好型界面设计
  • +
  • 离线使用能力保障
  • +
+
+
+ +### 1.2 技术栈对比分析 + +
+ +| 技术方案 | 开发难度 | 部署复杂度 | 性能表现 | 扩展性 | 维护成本 | +|---------|---------|-----------|---------|--------|---------| +| Python + Tkinter | | | 中等 | | | +| Web方案(HTML+JS) | 中等 | | | | 中等 | +| C# WinForms | 中等 | 中等 | | 中等 | 中等 | +| Java Swing | | | | | | + +
+ +### 1.3 核心依赖库选择 + +```python +# 核心依赖库配置 +required_libraries = { + "tkinter": "内置GUI框架", + "sqlite3": "内置轻量级数据库", + "pandas": "Excel文件处理和数据操作", + "openpyxl": "Excel文件读写支持", + "datetime": "日期时间处理", + "random": "随机算法实现", + "json": "配置文件处理" +} +``` + +
+⚠️ 注意事项: 优先使用Python内置库,减少外部依赖,确保部署稳定性 +
+ +## 2. 核心模块功能实现方案 + +### 2.1 系统架构设计 + +```mermaid +graph TB + A[用户界面层] --> B[业务逻辑层] + B --> C[数据访问层] + + A1[主窗口Manager] --> A2[界面组件] + A2 --> A3[事件处理] + + B1[点名引擎] --> B2[学生管理] + B2 --> B3[历史记录] + B3 --> B4[统计计算] + + C1[SQLite数据库] --> C2[Excel文件] + C2 --> C3[配置文件] +``` + +### 2.2 点名引擎模块实现 + +
+

🎲 随机算法核心实现

+
+ +```python +class RandomSelector: + def __init__(self, student_list): + self.students = student_list + self.selected_history = [] + + def weighted_random_selection(self): + """加权随机选择算法,优先选择未点到学生""" + if not self.students: + return None + + # 计算权重:未点到学生权重高,已点到学生权重低 + weights = [] + for student in self.students: + point_count = self.get_recent_point_count(student.id) + weight = max(1, 10 - point_count) # 最近点到次数越少,权重越高 + weights.append(weight) + + total_weight = sum(weights) + if total_weight == 0: + return random.choice(self.students) + + # 执行加权随机选择 + rand_val = random.uniform(0, total_weight) + cumulative = 0 + for i, weight in enumerate(weights): + cumulative += weight + if rand_val <= cumulative: + return self.students[i] +``` + +### 2.3 动画效果实现方案 + +```python +class AnimationManager: + def __init__(self, canvas_widget): + self.canvas = canvas_widget + self.animation_id = None + self.is_animating = False + + def start_roll_animation(self, duration=3000): + """开始滚动动画效果""" + self.is_animating = True + self.animation_start_time = time.time() + self.animate_roll() + + def animate_roll(self): + if not self.is_animating: + return + + elapsed = time.time() - self.animation_start_time + progress = min(elapsed / 3.0, 1.0) # 3秒动画周期 + + # 计算当前显示的学生索引(非线性缓动效果) + current_index = self.calculate_current_index(progress) + self.display_student(current_index) + + if progress < 1.0: + # 继续动画,使用缓动函数调整速度 + interval = self.calculate_interval(progress) + self.canvas.after(int(interval * 1000), self.animate_roll) + else: + self.finalize_selection() +``` + +### 2.4 文件导入模块实现 + +```python +class ExcelImporter: + def __init__(self): + self.supported_formats = ['.xlsx', '.xls', '.csv'] + + def import_students(self, file_path): + """导入Excel文件并解析学生数据""" + try: + if file_path.endswith('.csv'): + df = pd.read_csv(file_path) + else: + df = pd.read_excel(file_path, engine='openpyxl') + + students = [] + required_columns = ['学号', '姓名', '班级'] + + # 验证文件格式 + if not all(col in df.columns for col in required_columns): + raise ValueError("文件格式错误:缺少必要列") + + for _, row in df.iterrows(): + student = Student( + id=str(row['学号']), + name=str(row['姓名']), + class_name=str(row['班级']) + ) + students.append(student) + + return students + + except Exception as e: + raise Exception(f"文件导入失败:{str(e)}") +``` + +## 3. 数据库设计 + +### 3.1 数据库架构设计 + +```mermaid +erDiagram + STUDENT ||--o{ POINT_HISTORY : has + STUDENT { + string student_id PK "学号" + string name "姓名" + string class_name "班级" + datetime created_at "创建时间" + datetime updated_at "更新时间" + } + POINT_HISTORY { + int history_id PK "记录ID" + string student_id FK "学号" + datetime point_time "点名时间" + string point_type "点名类型" + string note "备注" + } + SYSTEM_CONFIG { + string config_key PK "配置键" + string config_value "配置值" + datetime updated_at "更新时间" + } +``` + +### 3.2 数据表详细设计 + +
+ +| 表名 | 字段名 | 数据类型 | 约束 | 说明 | +|------|--------|---------|------|------| +| **students** | student_id | VARCHAR(20) | PRIMARY KEY | 学生学号 | +| | name | VARCHAR(50) | NOT NULL | 学生姓名 | +| | class_name | VARCHAR(50) | NOT NULL | 班级名称 | +| | created_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 创建时间 | +| | updated_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 更新时间 | +| **point_history** | history_id | INTEGER | PRIMARY KEY AUTOINCREMENT | 记录ID | +| | student_id | VARCHAR(20) | FOREIGN KEY | 学生学号 | +| | point_time | DATETIME | NOT NULL | 点名时间 | +| | point_type | VARCHAR(20) | DEFAULT 'normal' | 点名类型 | +| | note | TEXT | | 备注信息 | +| **system_config** | config_key | VARCHAR(50) | PRIMARY KEY | 配置键 | +| | config_value | TEXT | | 配置值 | +| | updated_at | DATETIME | DEFAULT CURRENT_TIMESTAMP | 更新时间 | + +
+ +### 3.3 数据库操作封装 + +```python +class DatabaseManager: + def __init__(self, db_path="classroom_points.db"): + self.db_path = db_path + self.init_database() + + def init_database(self): + """初始化数据库表结构""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 创建学生表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS students ( + student_id VARCHAR(20) PRIMARY KEY, + name VARCHAR(50) NOT NULL, + class_name VARCHAR(50) NOT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + ''') + + # 创建点名历史表 + cursor.execute(''' + CREATE TABLE IF NOT EXISTS point_history ( + history_id INTEGER PRIMARY KEY AUTOINCREMENT, + student_id VARCHAR(20), + point_time DATETIME NOT NULL, + point_type VARCHAR(20) DEFAULT 'normal', + note TEXT, + FOREIGN KEY (student_id) REFERENCES students (student_id) + ) + ''') + + conn.commit() + conn.close() +``` + +## 4. 界面开发指南 + +### 4.1 主界面布局实现 + +```python +class MainApplication: + def __init__(self): + self.root = tk.Tk() + self.root.title("课堂点名系统") + self.root.geometry("1200x800") + self.root.configure(bg="#ECF0F1") + + self.setup_layout() + self.create_widgets() + + def setup_layout(self): + """设置主界面布局结构""" + # 顶部功能区 + self.top_frame = tk.Frame(self.root, height=80, bg="#3498DB") + self.top_frame.pack(fill=tk.X, padx=10, pady=5) + + # 主内容区 + self.main_frame = tk.Frame(self.root, bg="white") + self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 底部状态栏 + self.status_frame = tk.Frame(self.root, height=40, bg="#2C3E50") + self.status_frame.pack(fill=tk.X, side=tk.BOTTOM) +``` + +### 4.2 组件样式定制 + +```python +def setup_styles(self): + """配置Tkinter样式""" + style = ttk.Style() + + # 配置主按钮样式 + style.configure('Primary.TButton', + background='#3498DB', + foreground='white', + font=('微软雅黑', 11, 'bold'), + padding=(20, 10)) + + # 配置表格样式 + style.configure('Treeview', + font=('微软雅黑', 10), + rowheight=25) + style.configure('Treeview.Heading', + font=('微软雅黑', 11, 'bold')) +``` + +### 4.3 响应式布局适配 + +
+✅ 最佳实践: 使用Grid布局管理器实现灵活的响应式设计 +
+ +```python +def create_responsive_layout(self): + """创建响应式布局""" + # 左侧导航区 + self.nav_frame = tk.Frame(self.main_frame, width=250, bg="#F8F9FA") + self.nav_frame.grid(row=0, column=0, rowspan=2, sticky="nswe", padx=(0, 10)) + + # 中央内容区 + self.content_frame = tk.Frame(self.main_frame, bg="white") + self.content_frame.grid(row=0, column=1, sticky="nswe") + + # 右侧统计区 + self.stats_frame = tk.Frame(self.main_frame, width=200, bg="#F8F9FA") + self.stats_frame.grid(row=0, column=2, rowspan=2, sticky="nswe", padx=(10, 0)) + + # 配置权重使中央区域可伸缩 + self.main_frame.grid_columnconfigure(1, weight=1) + self.main_frame.grid_rowconfigure(0, weight=1) +``` + +## 5. 部署和运行说明 + +### 5.1 环境要求与依赖安装 + +
+ +| 环境组件 | 版本要求 | 安装方法 | 验证命令 | +|---------|---------|---------|---------| +| Python | 3.8+ | 官网下载安装 | `python --version` | +| pandas | 1.5.0+ | `pip install pandas` | `python -c "import pandas; print(pandas.__version__)"` | +| openpyxl | 3.0.0+ | `pip install openpyxl` | `python -c "import openpyxl; print(openpyxl.__version__)"` | +| 操作系统 | Win7+/macOS 10.12+/Ubuntu 16.04+ | - | - | + +
+ +### 5.2 项目目录结构 + +``` +课堂点名系统/ +├── main.py # 主程序入口 +├── requirements.txt # 依赖包列表 +├── config/ +│ ├── settings.json # 配置文件 +│ └── database.db # SQLite数据库 +├── src/ +│ ├── gui/ # 界面模块 +│ │ ├── main_window.py # 主窗口 +│ │ ├── components.py # 组件类 +│ │ └── styles.py # 样式定义 +│ ├── core/ # 核心逻辑 +│ │ ├── random_selector.py # 随机算法 +│ │ ├── data_manager.py # 数据管理 +│ │ └── animation_engine.py # 动画引擎 +│ └── utils/ # 工具类 +│ ├── file_importer.py # 文件导入 +│ ├── database.py # 数据库操作 +│ └── helpers.py # 辅助函数 +├── assets/ # 资源文件 +│ ├── icons/ # 图标资源 +│ └── templates/ # Excel模板 +└── docs/ # 文档 + └── 使用说明.md # 用户手册 +``` + +### 5.3 打包发布方案 + +```python +# pyinstaller打包配置(spec文件) +# classroom_points.spec + +block_cipher = None + +a = Analysis( + ['main.py'], + pathex=[], + binaries=[], + datas=[ + ('assets/', 'assets/'), + ('config/', 'config/') + ], + hiddenimports=[], + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) + +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) + +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name='课堂点名系统', + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + upx_exclude=[], + runtime_tmpdir=None, + console=False, # 设置为True可显示控制台窗口 + icon='assets/icon.ico' +) +``` + +### 5.4 部署操作指南 + +
+📋 部署步骤: 按照以下步骤完成系统部署 +
+ +**步骤1:环境准备** +```bash +# 1. 安装Python 3.8或更高版本 +# 2. 下载项目代码 +git clone <项目仓库> +cd 课堂点名系统 + +# 3. 安装依赖 +pip install -r requirements.txt +``` + +**步骤2:首次运行配置** +```bash +# 1. 运行主程序 +python main.py + +# 2. 系统将自动创建配置文件和数据文件 +# 3. 根据需要调整系统设置 +``` + +**步骤3:打包分发(可选)** +```bash +# 使用PyInstaller打包为可执行文件 +pip install pyinstaller +pyinstaller classroom_points.spec + +# 生成的exe文件在dist目录下 +``` + +### 5.5 故障排除指南 + +
+
+

❌ 常见问题1:依赖包安装失败

+

解决方案:使用国内镜像源安装
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt

+
+
+

❌ 常见问题2:Excel导入失败

+

解决方案:检查Excel文件格式,确保包含学号、姓名、班级三列

+
+
+

❌ 常见问题3:界面显示异常

+

解决方案:调整系统DPI设置或使用兼容模式运行

+
+
+ +## 6. 性能优化建议 + +### 6.1 数据库性能优化 + +```python +def optimize_database_performance(self): + """数据库性能优化配置""" + conn = sqlite3.connect(self.db_path) + cursor = conn.cursor() + + # 启用WAL模式提高并发性能 + cursor.execute("PRAGMA journal_mode=WAL") + + # 设置合适的缓存大小 + cursor.execute("PRAGMA cache_size=-64000") # 64MB缓存 + + # 创建索引提升查询性能 + cursor.execute("CREATE INDEX IF NOT EXISTS idx_student_class ON students(class_name)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_history_time ON point_history(point_time)") + cursor.execute("CREATE INDEX IF NOT EXISTS idx_history_student ON point_history(student_id)") + + conn.commit() + conn.close() +``` + +### 6.2 内存管理优化 + +
+

💡 内存优化策略

+
    +
  • 使用分页加载大量学生数据
  • +
  • 及时释放不再使用的界面组件
  • +
  • 优化图片和资源文件加载
  • +
  • 定期清理临时数据
  • +
+
+ +## 7. 安全与稳定性保障 + +### 7.1 数据安全措施 + +- **数据备份机制**:自动定期备份数据库文件 +- **输入验证**:对所有用户输入进行严格验证 +- **异常处理**:完善的异常捕获和处理机制 +- **日志记录**:详细的操作日志记录系统 + +### 7.2 系统稳定性策略 + +```python +def setup_error_handling(self): + """设置全局异常处理""" + def global_exception_handler(exc_type, exc_value, exc_traceback): + if issubclass(exc_type, KeyboardInterrupt): + sys.__excepthook__(exc_type, exc_value, exc_traceback) + return + + logger.error("未捕获的异常:", exc_info=(exc_type, exc_value, exc_traceback)) + + # 显示用户友好的错误信息 + messagebox.showerror( + "系统错误", + "发生意外错误,程序将继续运行。\n错误信息已记录到日志。" + ) + + sys.excepthook = global_exception_handler +``` + +--- + +**文档完成时间:2025年11月22日** diff --git a/class-call-system (1)/docs/prototype/项目原型.md b/class-call-system (1)/docs/prototype/项目原型.md new file mode 100644 index 0000000..e69de29 diff --git a/class-call-system (1)/main.py b/class-call-system (1)/main.py new file mode 100644 index 0000000..bac2456 --- /dev/null +++ b/class-call-system (1)/main.py @@ -0,0 +1,11 @@ +import ttkbootstrap as ttk +from src.gui.main_window import MainWindow +from src.utils.database import db_instance +import sys +import os + +if __name__ == "__main__": + root = ttk.Window(themename="flatly") + app = MainWindow(root) + root.mainloop() + db_instance.close() \ No newline at end of file diff --git a/class-call-system (1)/requirements.txt b/class-call-system (1)/requirements.txt new file mode 100644 index 0000000..ca6c731 --- /dev/null +++ b/class-call-system (1)/requirements.txt @@ -0,0 +1,7 @@ +pandas>=2.0.0 +openpyxl>=3.1.0 +matplotlib>=3.7.0 +ttkbootstrap>=1.10.0 +Pillow>=10.0.0 +pywin32>=306 +pymysql>=1.1.0 \ No newline at end of file diff --git a/class-call-system (1)/src/core/animation_engine.py b/class-call-system (1)/src/core/animation_engine.py new file mode 100644 index 0000000..406ccb9 --- /dev/null +++ b/class-call-system (1)/src/core/animation_engine.py @@ -0,0 +1,67 @@ +import random +import json +import ttkbootstrap as ttk +from src.utils.database import db_instance + +# 读取配置 +with open("config/settings.json", "r", encoding="utf-8") as f: + CONFIG = json.load(f) +ANIM_CONFIG = CONFIG["animation"] + +class AnimationEngine: + """点名动画引擎(名字滚动、闪烁)""" + def __init__(self, label: ttk.Label): + self.label = label # 展示动画的标签 + self.students = [] + self.is_running = False + self.roll_count = 0 + + def refresh_students(self): + """刷新学生列表""" + self.students = db_instance.get_all_students() + + def _roll_name(self, final_student: tuple = None): + """名字滚动动画""" + if not self.students: + self.label.config(text="暂无学生数据") + return + + if self.roll_count >= ANIM_CONFIG["roll_times"] and final_student: + # 动画结束,显示最终结果 + self.is_running = False + self.label.config( + text=f"🎉 点名结果 🎉\n学号:{final_student[0]}\n姓名:{final_student[1]}\n专业:{final_student[2]}", + font=("微软雅黑", 16, "bold") + ) + # 闪烁效果 + self._flash_label() + return + + # 随机显示学生名字(滚动) + random_student = random.choice(self.students) + self.label.config( + text=f"正在随机点名...\n{random_student[1]}", + font=("微软雅黑", 14) + ) + self.roll_count += 1 + # 递归调用,实现滚动 + self.label.after(ANIM_CONFIG["roll_speed"], self._roll_name, final_student) + + def _flash_label(self): + """标签闪烁效果""" + current_color = self.label.cget("foreground") + new_color = "red" if current_color != "red" else "black" + self.label.config(foreground=new_color) + # 持续闪烁1秒 + self.label.after(ANIM_CONFIG["flash_duration"], lambda: self.label.config(foreground="black")) + + def start_roll(self, final_student: tuple): + """启动滚动动画""" + self.is_running = True + self.roll_count = 0 + self.refresh_students() + self._roll_name(final_student) + +# 示例:创建动画引擎时传入ttk.Label对象 +# label = ttk.Label(text="测试") +# anim_engine = AnimationEngine(label) \ No newline at end of file diff --git a/class-call-system (1)/src/core/data_manager.py b/class-call-system (1)/src/core/data_manager.py new file mode 100644 index 0000000..b49df8f --- /dev/null +++ b/class-call-system (1)/src/core/data_manager.py @@ -0,0 +1,94 @@ +from src.utils.database import db_instance +from src.utils.file_importer import excel_importer +from src.core.random_selector import random_selector, order_selector +from src.core.animation_engine import AnimationEngine +from src.utils.helpers import get_answer_score + +class DataManager: + """数据管理中间层,协调各模块""" + def __init__(self): + self.db = db_instance + self.excel = excel_importer + self.random_selector = random_selector + self.order_selector = order_selector + self.anim_engine = None + # 新增:检查数据库实例是否初始化成功 + if self.db is None: + raise Exception("数据库连接初始化失败,请检查config/settings.json中的MySQL配置") + + def init_animation(self, label): + """初始化动画引擎""" + self.anim_engine = AnimationEngine(label) + + def import_excel(self, file_path): + """导入Excel学生名单(新增:操作前重连数据库)""" + if self.db: + self.db.reconnect() # 解决db4free空闲断开连接问题 + return self.excel.import_students(file_path) + + def export_excel(self, file_path=None): + """导出积分详单(新增:操作前重连数据库)""" + if self.db: + self.db.reconnect() + return self.excel.export_scores(file_path) + + def random_call(self): + """随机点名(含动画,新增:数据库重连+空值检查)""" + if self.db is None: + return None, "数据库未连接,无法获取学生数据" + self.db.reconnect() # 重连数据库 + self.random_selector.refresh_students() + student, msg = self.random_selector.select() + if student and self.anim_engine: + self.anim_engine.start_roll(student) + return student, msg + + def order_call(self): + """顺序点名(无动画,直接显示,优化:固定顺序不重置+空数据处理)""" + if self.db is None: + return None, "数据库未连接,无法获取学生数据" + + self.db.reconnect() # 重连数据库 + + # 仅在首次调用或学生列表为空时刷新数据(避免每次调用重置顺序) + if not hasattr(self.order_selector, 'students') or len(self.order_selector.students) == 0: + self.order_selector.refresh_students() + + # 检查学生列表是否为空 + if len(self.order_selector.students) == 0: + return None, "没有可用的学生数据,请先导入学生名单" + + student, msg = self.order_selector.select() + + # 当所有学生点完一轮后,自动重置顺序(可选逻辑,根据需求调整) + if "已完成一轮" in msg: + self.order_selector.refresh_students() # 重置为初始顺序 + msg += ",已自动重置顺序" + + return student, msg + + def record_call(self, student_id, call_mode, is_arrived, answer_level): + """记录点名结果(新增:数据库重连+异常捕获)""" + if self.db is None: + return False + self.db.reconnect() # 重连数据库 + try: + answer_score = get_answer_score(answer_level) + return self.db.add_call_record(student_id, call_mode, is_arrived, answer_score) + except Exception as e: + print(f"记录点名结果失败:{str(e)}") + return False + + def get_all_students(self): + """获取所有学生(新增:数据库重连+空值检查)""" + if self.db is None: + return [] + self.db.reconnect() # 重连数据库 + return self.db.get_all_students() + +# 单例(新增:异常捕获,避免数据库初始化失败导致程序崩溃) +try: + data_manager = DataManager() +except Exception as e: + print(f"DataManager初始化失败:{e}") + data_manager = None # 初始化失败时设为None \ No newline at end of file diff --git a/class-call-system (1)/src/core/random_selector.py b/class-call-system (1)/src/core/random_selector.py new file mode 100644 index 0000000..34dacb7 --- /dev/null +++ b/class-call-system (1)/src/core/random_selector.py @@ -0,0 +1,55 @@ +import random +from src.utils.database import db_instance +from src.utils.helpers import calculate_weight + +class RandomSelector: + """随机点名算法类""" + def __init__(self): + self.students = [] + self.refresh_students() + + def refresh_students(self): + """刷新学生列表""" + self.students = db_instance.get_all_students() + + def select(self) -> tuple: + """随机选择学生(积分越高概率越低)""" + if not self.students: + return None, "暂无学生数据,请先导入名单" + + # 提取学号和积分 + student_ids = [s[0] for s in self.students] + scores = [s[3] for s in self.students] + # 计算权重 + weights = calculate_weight(scores) + # 按权重随机选择 + selected_id = random.choices(student_ids, weights=weights, k=1)[0] + # 获取选中学生详情 + selected_student = [s for s in self.students if s[0] == selected_id][0] + return selected_student, "随机点名成功" + +class OrderSelector: + """顺序点名算法类""" + def __init__(self): + self.students = [] + self.index = 0 + self.refresh_students() + + def refresh_students(self): + """刷新学生列表""" + self.students = db_instance.get_all_students() + self.index = 0 # 重置索引 + + def select(self) -> tuple: + """按学号顺序选择学生""" + if not self.students: + return None, "暂无学生数据,请先导入名单" + + # 循环选择 + selected_student = self.students[self.index] + self.index = (self.index + 1) % len(self.students) + return selected_student, "顺序点名成功" + +# 单例 +random_selector = RandomSelector() +order_selector = OrderSelector() \ No newline at end of file diff --git a/class-call-system (1)/src/gui/components.py b/class-call-system (1)/src/gui/components.py new file mode 100644 index 0000000..e81f57e --- /dev/null +++ b/class-call-system (1)/src/gui/components.py @@ -0,0 +1,128 @@ +import ttkbootstrap as ttk +import tkinter as tk +from src.gui.styles import * +# 新增:导入Matplotlib字体配置和刻度模块 +import matplotlib +import matplotlib.ticker as ticker # 关键:用于设置刻度间隔 +matplotlib.rcParams["font.family"] = ["SimHei", "Microsoft YaHei", "DejaVu Sans"] # 优先中文字体 +matplotlib.rcParams["axes.unicode_minus"] = False # 解决负号显示为方块的问题 + +class CustomButton(ttk.Button): + """自定义按钮组件""" + def __init__(self, parent, text, command=None, style="Primary.TButton", **kwargs): + super().__init__( + parent, + text=text, + command=command, + style=style, + width=BUTTON_WIDTH,** kwargs + ) + +class ResultDisplay(ttk.Label): + """点名结果展示标签""" + def __init__(self, parent): + super().__init__( + parent, + text="等待点名...", + style="Result.TLabel", + anchor="center", + justify="center" + ) + +class StudentTable(ttk.Treeview): + """学生名单表格""" + def __init__(self, parent): + super().__init__(parent, show="headings") + # 定义列 + self["columns"] = ("学号", "姓名", "专业", "总积分", "随机点名次数") + # 设置列标题和宽度 + for col in self["columns"]: + self.heading(col, text=col) + self.column(col, width=100, anchor="center") + # 滚动条 + scroll_y = ttk.Scrollbar(parent, orient="vertical", command=self.yview) + self.configure(yscrollcommand=scroll_y.set) + scroll_y.pack(side="right", fill="y") + self.pack(fill="both", expand=True) + + def refresh_data(self, data): + """刷新表格数据""" + # 清空原有数据 + for item in self.get_children(): + self.delete(item) + # 添加新数据 + for row in data: + self.insert("", "end", values=row) + +class VisualizationFrame(ttk.Frame): + """可视化图表容器(简易版,结合matplotlib)""" + def __init__(self, parent): + super().__init__(parent) + self.canvas = None + + def show_chart(self, students): + """显示积分和点名次数图表(优化折线图纵轴刻度+数据读取)""" + from matplotlib.figure import Figure + from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg + import json + import os # 新增:处理路径 + + # 修复配置文件路径(沿用之前的绝对路径逻辑,避免相对路径错误) + current_file = os.path.abspath(__file__) + utils_dir = os.path.dirname(os.path.dirname(current_file)) # 上级目录 + project_root = os.path.dirname(utils_dir) + config_path = os.path.join(project_root, "config", "settings.json") + + with open(config_path, "r", encoding="utf-8") as f: + vis_config = json.load(f)["visualization"] + + # 创建图表 + fig = Figure(figsize=vis_config["fig_size"], dpi=100) + fig.suptitle("学生积分与点名次数可视化", fontsize=12) + + # 过滤有效数据(确保至少包含5个字段,避免索引错误) + valid_students = [s for s in students if len(s)>=5] + # 提取可视化数据(仅取TOP-N,适配配置) + top_n = vis_config["top_n"] + show_students = valid_students[:top_n] if len(valid_students)>=top_n else valid_students + names = [s[1] for s in show_students] + scores = [s[3] for s in show_students] + call_nums = [s[4] for s in show_students] + + # 子图1:积分柱形图 + ax1 = fig.add_subplot(121) + if names: # 有数据才绘制 + ax1.bar(names, scores, color=COLOR_PRIMARY) + else: + ax1.text(0.5, 0.5, "暂无学生数据", ha="center", va="center", transform=ax1.transAxes) + ax1.set_title(f"TOP{vis_config['top_n']}积分排名") + ax1.set_xlabel("姓名") + ax1.set_ylabel("总积分") + ax1.tick_params(axis="x", rotation=45) + + # 子图2:点名次数折线图(核心优化:纵轴从0开始+刻度为1) + ax2 = fig.add_subplot(122) + if names: # 有数据才绘制 + ax2.plot(names, call_nums, marker="o", color=COLOR_SECONDARY, linestyle="-", linewidth=2, markersize=6) + # 强制纵轴从0开始 + ax2.set_ylim(bottom=0) + # 设置纵轴刻度间隔为1 + ax2.yaxis.set_major_locator(ticker.MultipleLocator(1)) + # 可选:显示网格,方便查看刻度 + ax2.grid(axis="y", linestyle="--", alpha=0.7) + else: + ax2.text(0.5, 0.5, "暂无点名数据", ha="center", va="center", transform=ax2.transAxes) + ax2.set_title(f"TOP{vis_config['top_n']}随机点名次数") + ax2.set_xlabel("姓名") + ax2.set_ylabel("点名次数") + ax2.tick_params(axis="x", rotation=45) + + # 调整子图间距,避免标题/标签重叠 + fig.tight_layout() + + # 嵌入Tkinter + if self.canvas: + self.canvas.get_tk_widget().destroy() + self.canvas = FigureCanvasTkAgg(fig, master=self) + self.canvas.draw() + self.canvas.get_tk_widget().pack(fill="both", expand=True) \ No newline at end of file diff --git a/class-call-system (1)/src/gui/main_window.py b/class-call-system (1)/src/gui/main_window.py new file mode 100644 index 0000000..f048db6 --- /dev/null +++ b/class-call-system (1)/src/gui/main_window.py @@ -0,0 +1,163 @@ +import ttkbootstrap as ttk +from tkinter import filedialog, messagebox +from src.gui.components import CustomButton, ResultDisplay, StudentTable, VisualizationFrame +from src.core.data_manager import data_manager +from src.gui.styles import * +from src.utils.helpers import get_visual_top_n + +class MainWindow: + """主窗口类""" + def __init__(self, root): + self.root = root + self.root.title("课堂随机点名系统") + self.root.geometry("1000x700") + self.root.resizable(True, True) + + # 初始化数据管理器的动画引擎 + self.result_label = ResultDisplay(self.root) + data_manager.init_animation(self.result_label) + + # 构建界面 + self._create_layout() + # 刷新学生表格 + self._refresh_table() + + def _create_layout(self): + """构建界面布局""" + # 顶部标题栏 + title_frame = ttk.Frame(self.root) + title_frame.pack(fill="x", pady=PADDING_MEDIUM) + title_label = ttk.Label(title_frame, text="课堂随机点名系统", font=FONT_TITLE, foreground=COLOR_PRIMARY) + title_label.pack() + + # 功能按钮栏 + btn_frame = ttk.Frame(self.root) + btn_frame.pack(fill="x", padx=PADDING_LARGE, pady=PADDING_SMALL) + + # 导入Excel按钮 + CustomButton(btn_frame, text="导入学生名单", command=self._import_excel, style="Primary.TButton").pack(side="left", padx=PADDING_SMALL) + # 随机点名按钮 + CustomButton(btn_frame, text="随机点名", command=self._random_call, style="Success.TButton").pack(side="left", padx=PADDING_SMALL) + # 顺序点名按钮 + CustomButton(btn_frame, text="顺序点名", command=self._order_call, style="Success.TButton").pack(side="left", padx=PADDING_SMALL) + # 导出积分按钮 + CustomButton(btn_frame, text="导出积分详单", command=self._export_excel, style="Primary.TButton").pack(side="left", padx=PADDING_SMALL) + # 可视化按钮 + CustomButton(btn_frame, text="积分可视化", command=self._show_visual, style="Primary.TButton").pack(side="left", padx=PADDING_SMALL) + + # 点名结果展示区 + result_frame = ttk.Frame(self.root, borderwidth=2, relief="groove") + result_frame.pack(fill="x", padx=PADDING_LARGE, pady=PADDING_MEDIUM) + self.result_label.pack(pady=PADDING_MEDIUM) + + # 积分记录按钮区 + record_frame = ttk.Frame(self.root) + record_frame.pack(fill="x", padx=PADDING_LARGE, pady=PADDING_SMALL) + # 到课按钮 + CustomButton(record_frame, text="到课(+1.5分)", command=lambda: self._record_call(True, "及格"), style="Success.TButton").pack(side="left", padx=PADDING_SMALL) + # 未到课按钮 + CustomButton(record_frame, text="未到课(-1.0分)", command=lambda: self._record_call(False, "错误"), style="Primary.TButton").pack(side="left", padx=PADDING_SMALL) + # 回答优秀按钮 + CustomButton(record_frame, text="回答优秀(+4分)", command=lambda: self._record_call(True, "优秀"), style="Success.TButton").pack(side="left", padx=PADDING_SMALL) + # 重复问题正确 + CustomButton(record_frame, text="重复正确(+1.5分)", command=lambda: self._record_call(True, "重复正确"), style="Success.TButton").pack(side="left", padx=PADDING_SMALL) + # 重复问题错误 + CustomButton(record_frame, text="错误(+0分)", command=lambda: self._record_call(True, "重复错误"), style="Primary.TButton").pack(side="left", padx=PADDING_SMALL) + + # 学生表格区 + table_frame = ttk.Frame(self.root, borderwidth=2, relief="groove") + table_frame.pack(fill="both", expand=True, padx=PADDING_LARGE, pady=PADDING_MEDIUM) + table_label = ttk.Label(table_frame, text="学生名单", font=FONT_SUBTITLE, foreground=COLOR_TEXT) + table_label.pack(pady=PADDING_SMALL) + self.student_table = StudentTable(table_frame) + + # 可视化窗口(隐藏,点击按钮后显示) + self.visual_window = None + + def _import_excel(self): + """导入Excel学生名单""" + file_path = filedialog.askopenfilename(filetypes=[("Excel文件", "*.xlsx")]) + if not file_path: + return + success, msg = data_manager.import_excel(file_path) + if success: + messagebox.showinfo("导入成功", msg) + self._refresh_table() + else: + messagebox.showerror("导入失败", msg) + + def _export_excel(self): + """导出积分详单""" + file_path = filedialog.asksaveasfilename(defaultextension=".xlsx", filetypes=[("Excel文件", "*.xlsx")]) + if not file_path: + return + success, msg = data_manager.export_excel(file_path) + if success: + messagebox.showinfo("导出成功", msg) + else: + messagebox.showerror("导出失败", msg) + + def _random_call(self): + """随机点名""" + student, msg = data_manager.random_call() + if not student: + messagebox.showwarning("警告", msg) + # 动画由AnimationEngine自动处理,无需额外更新标签 + self.current_student = student # 保存当前点名学生 + # 记录本次点名模式,供记录积分时使用(避免从标签文本解析) + self.current_call_mode = "random" + + def _order_call(self): + """顺序点名""" + student, msg = data_manager.order_call() + if not student: + messagebox.showwarning("警告", msg) + return + # 显示顺序点名结果 + self.result_label.config( + text=f"📋 顺序点名结果 📋\n学号:{student[0]}\n姓名:{student[1]}\n专业:{student[2]}", + font=FONT_SUBTITLE + ) + self.current_student = student # 保存当前点名学生 + # 记录本次点名模式 + self.current_call_mode = "order" + + def _record_call(self, is_arrived, answer_level): + """记录点名结果(积分)""" + if not hasattr(self, "current_student") or not self.current_student: + messagebox.showwarning("警告", "请先进行点名") + return + student_id = self.current_student[0] + # 优先使用显式记录的点名模式(`current_call_mode`),避免依赖标签文本内容 + call_mode = getattr(self, "current_call_mode", None) + if call_mode not in ("random", "order"): + # 回退到原有文本解析逻辑(兼容旧文本) + call_mode = "random" if "随机" in self.result_label.cget("text") else "order" + success = data_manager.record_call(student_id, call_mode, is_arrived, answer_level) + if success: + messagebox.showinfo("成功", "积分记录成功") + self._refresh_table() + else: + messagebox.showerror("失败", "积分记录失败") + + def _show_visual(self): + """显示积分可视化""" + students = data_manager.get_all_students() + if not students: + messagebox.showwarning("警告", "暂无学生数据") + return + # 创建可视化窗口 + self.visual_window = ttk.Toplevel(self.root) + self.visual_window.title("积分可视化") + self.visual_window.geometry("800x500") + # 可视化组件 + visual_frame = VisualizationFrame(self.visual_window) + visual_frame.pack(fill="both", expand=True) + # 显示TOP N学生图表 + top_students = get_visual_top_n(students) + visual_frame.show_chart(top_students) + + def _refresh_table(self): + """刷新学生表格""" + students = data_manager.get_all_students() + self.student_table.refresh_data(students) \ No newline at end of file diff --git a/class-call-system (1)/src/gui/styles.py b/class-call-system (1)/src/gui/styles.py new file mode 100644 index 0000000..fdce1b2 --- /dev/null +++ b/class-call-system (1)/src/gui/styles.py @@ -0,0 +1,48 @@ +import ttkbootstrap as ttk + +# 颜色配置 +COLOR_PRIMARY = "#2E86AB" +COLOR_SECONDARY = "#A23B72" +COLOR_SUCCESS = "#F18F01" +COLOR_WARNING = "#C73E1D" +COLOR_TEXT = "#333333" + +# 字体配置 +FONT_TITLE = ("微软雅黑", 18, "bold") +FONT_SUBTITLE = ("微软雅黑", 14, "bold") +FONT_NORMAL = ("微软雅黑", 12) +FONT_SMALL = ("微软雅黑", 10) + +# 布局配置 +PADDING_SMALL = 5 +PADDING_MEDIUM = 10 +PADDING_LARGE = 20 +BUTTON_WIDTH = 15 +BUTTON_HEIGHT = 2 + +# 组件样式 +STYLE_BUTTON_PRIMARY = ttk.Style() +STYLE_BUTTON_PRIMARY.configure( + "Primary.TButton", + font=FONT_NORMAL, + foreground="white", + background=COLOR_PRIMARY, + padding=PADDING_SMALL +) + +STYLE_BUTTON_SUCCESS = ttk.Style() +STYLE_BUTTON_SUCCESS.configure( + "Success.TButton", + font=FONT_NORMAL, + foreground="white", + background=COLOR_SUCCESS, + padding=PADDING_SMALL +) + +STYLE_LABEL_RESULT = ttk.Style() +STYLE_LABEL_RESULT.configure( + "Result.TLabel", + font=FONT_SUBTITLE, + foreground=COLOR_TEXT, + padding=PADDING_MEDIUM +) \ No newline at end of file diff --git a/class-call-system (1)/src/utils/database.py b/class-call-system (1)/src/utils/database.py new file mode 100644 index 0000000..0dd3b58 --- /dev/null +++ b/class-call-system (1)/src/utils/database.py @@ -0,0 +1,246 @@ +import pymysql +import json +import os +from datetime import datetime + +# 读取配置文件 +# 在 database.py 中添加项目根目录计算(与 file_importer.py 逻辑相同) +current_file = os.path.abspath(__file__) # src/utils/database.py +utils_dir = os.path.dirname(current_file) +src_dir = os.path.dirname(utils_dir) +project_root = os.path.dirname(src_dir) +config_path = os.path.join(project_root, "config", "settings.json") + +# 读取配置文件 +with open(config_path, "r", encoding="utf-8") as f: + CONFIG = json.load(f) + +# 数据库配置 +DB_TYPE = CONFIG["database"]["type"] +MYSQL_CONFIG = CONFIG["database"]["mysql"] +TABLE_STUDENT = CONFIG["database"]["table_student"] +TABLE_RECORD = CONFIG["database"]["table_record"] + +class Database: + """MySQL 数据库操作类(适配 db4free 免费 MySQL)""" + def __init__(self): + self.conn = None + self.cursor = None + self.connect() # 初始化连接 + self.create_tables() # 初始化表结构 + + def connect(self): + """连接 MySQL 数据库(处理 db4free 连接特性)""" + try: + if DB_TYPE == "mysql": + self.conn = pymysql.connect( + host=MYSQL_CONFIG["host"], + port=MYSQL_CONFIG["port"], + user=MYSQL_CONFIG["user"], + password=MYSQL_CONFIG["password"], + database=MYSQL_CONFIG["dbname"], + charset=MYSQL_CONFIG["charset"], + connect_timeout=30 # db4free 外网连接超时设为30秒 + ) + self.cursor = self.conn.cursor() + print(f"成功连接 db4free MySQL 数据库:{MYSQL_CONFIG['dbname']}") + # 保留 SQLite 兼容(可选) + elif DB_TYPE == "sqlite": + import sqlite3 + db_path = "config/database.db" + os.makedirs(os.path.dirname(db_path), exist_ok=True) + self.conn = sqlite3.connect(db_path, check_same_thread=False) + self.cursor = self.conn.cursor() + except pymysql.MySQLError as e: + raise Exception(f"MySQL 连接失败:{e}") # 抛出异常让上层处理 + except Exception as e: + raise Exception(f"数据库连接失败:{e}") + + def create_tables(self): + """创建表结构(适配 MySQL 语法)""" + if DB_TYPE == "mysql": + # 学生表:指定 ENGINE=InnoDB 支持外键,TINYINT 替代布尔类型 + create_student_sql = f""" + CREATE TABLE IF NOT EXISTS {TABLE_STUDENT} ( + student_id VARCHAR(20) PRIMARY KEY COMMENT '学号(唯一标识)', + name VARCHAR(50) NOT NULL COMMENT '学生姓名', + major VARCHAR(50) NOT NULL COMMENT '所属专业', + total_score FLOAT DEFAULT 0 COMMENT '总积分', + random_call_num INT DEFAULT 0 COMMENT '随机点名次数' + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='学生信息表'; + """ + + # 点名记录表:自增主键 AUTO_INCREMENT,外键关联学生表 + create_record_sql = f""" + CREATE TABLE IF NOT EXISTS {TABLE_RECORD} ( + id INT PRIMARY KEY AUTO_INCREMENT COMMENT '自增主键', + student_id VARCHAR(20) NOT NULL COMMENT '关联学生学号', + call_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '点名时间', + call_mode VARCHAR(10) NOT NULL COMMENT '点名模式:random/order', + is_arrived TINYINT(1) NOT NULL COMMENT '是否到课:1=是,0=否', + answer_score FLOAT DEFAULT 0 COMMENT '回答问题加分值', + total_add FLOAT DEFAULT 0 COMMENT '本次总加分', + FOREIGN KEY (student_id) REFERENCES {TABLE_STUDENT}(student_id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='点名记录表'; + """ + else: + # 保留 SQLite 语法(兼容用) + create_student_sql = f""" + CREATE TABLE IF NOT EXISTS {TABLE_STUDENT} ( + student_id TEXT PRIMARY KEY, + name TEXT NOT NULL, + major TEXT NOT NULL, + total_score FLOAT DEFAULT 0, + random_call_num INT DEFAULT 0 + ); + """ + create_record_sql = f""" + CREATE TABLE IF NOT EXISTS {TABLE_RECORD} ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + student_id TEXT NOT NULL, + call_time DATETIME DEFAULT '{datetime.now().strftime("%Y-%m-%d %H:%M:%S")}', + call_mode TEXT NOT NULL, + is_arrived BOOLEAN NOT NULL, + answer_score FLOAT DEFAULT 0, + total_add FLOAT DEFAULT 0, + FOREIGN KEY (student_id) REFERENCES {TABLE_STUDENT}(student_id) + ); + """ + + try: + self.cursor.execute(create_student_sql) + self.cursor.execute(create_record_sql) + self.conn.commit() + print("表结构初始化成功") + except pymysql.MySQLError as e: + self.conn.rollback() # MySQL 事务回滚 + raise Exception(f"创建表失败:{e}") + except Exception as e: + raise Exception(f"创建表失败:{e}") + + def add_student(self, student_id: str, name: str, major: str) -> bool: + """添加单个学生(防重复插入)""" + try: + # 【修复】移除 SQL 语句中的注释,避免干扰参数替换 + sql = f""" + INSERT IGNORE INTO {TABLE_STUDENT} (student_id, name, major) + VALUES (%s, %s, %s) + """ + self.cursor.execute(sql, (student_id, name, major)) + self.conn.commit() + return True + except pymysql.MySQLError as e: + self.conn.rollback() + print(f"添加学生失败:{e}") + return False + + def batch_add_students(self, students: list) -> tuple: + """批量添加学生(列表元素:(学号, 姓名, 专业))""" + success_count = 0 + fail_list = [] + for s in students: + if self.add_student(*s): + success_count += 1 + else: + fail_list.append(s) + return success_count, fail_list + + def update_student_score(self, student_id: str, add_score: float, is_random: bool = False) -> bool: + """更新学生积分和随机点名次数""" + try: + # 查询当前积分和点名次数 + select_sql = f""" + SELECT total_score, random_call_num FROM {TABLE_STUDENT} WHERE student_id = %s; + """ + self.cursor.execute(select_sql, (student_id,)) + res = self.cursor.fetchone() + if not res: + return False + + # 计算新值 + new_score = res[0] + add_score + new_call_num = res[1] + 1 if is_random else res[1] + + # 更新数据 + update_sql = f""" + UPDATE {TABLE_STUDENT} SET total_score = %s, random_call_num = %s WHERE student_id = %s; + """ + self.cursor.execute(update_sql, (new_score, new_call_num, student_id)) + self.conn.commit() + return True + except pymysql.MySQLError as e: + self.conn.rollback() + print(f"更新积分失败:{e}") + return False + + def add_call_record(self, student_id: str, call_mode: str, is_arrived: bool, answer_score: float) -> bool: + """添加点名记录并同步更新积分""" + with open("config/settings.json", "r", encoding="utf-8") as f: + score_rules = json.load(f)["score_rules"] + + # 计算本次总加分 + total_add = score_rules["arrive_score"] if is_arrived else 0 + total_add += answer_score + + try: + # 插入点名记录(MySQL 布尔值用 1/0 存储) + insert_sql = f""" + INSERT INTO {TABLE_RECORD} (student_id, call_mode, is_arrived, answer_score, total_add) + VALUES (%s, %s, %s, %s, %s); + """ + self.cursor.execute(insert_sql, ( + student_id, call_mode, 1 if is_arrived else 0, answer_score, total_add + )) + + # 同步更新学生积分 + self.update_student_score(student_id, total_add, call_mode == "random") + self.conn.commit() + return True + except pymysql.MySQLError as e: + self.conn.rollback() + print(f"添加点名记录失败:{e}") + return False + + def get_all_students(self) -> list: + """获取所有学生信息(按学号排序)""" + try: + sql = f""" + SELECT student_id, name, major, total_score, random_call_num FROM {TABLE_STUDENT} ORDER BY student_id; + """ + self.cursor.execute(sql) + return self.cursor.fetchall() + except pymysql.MySQLError as e: + print(f"查询学生失败:{e}") + return [] + + def get_student_by_id(self, student_id: str) -> tuple: + """按学号查询学生""" + try: + sql = f""" + SELECT student_id, name, major, total_score, random_call_num FROM {TABLE_STUDENT} WHERE student_id = %s; + """ + self.cursor.execute(sql, (student_id,)) + return self.cursor.fetchone() + except pymysql.MySQLError as e: + print(f"查询单个学生失败:{e}") + return None + + def reconnect(self): + """重连数据库(解决 db4free 长时间空闲断开连接问题)""" + if not self.conn or not self.conn.open: + self.connect() + + def close(self): + """关闭数据库连接""" + if self.cursor: + self.cursor.close() + if self.conn: + self.conn.close() + print("数据库连接已关闭") + +# 单例模式:全局唯一数据库实例 +try: + db_instance = Database() +except Exception as e: + print(f"数据库初始化失败:{e}") + db_instance = None # 初始化失败时设为 None \ No newline at end of file diff --git a/class-call-system (1)/src/utils/file_importer.py b/class-call-system (1)/src/utils/file_importer.py new file mode 100644 index 0000000..56ae1ce --- /dev/null +++ b/class-call-system (1)/src/utils/file_importer.py @@ -0,0 +1,176 @@ +import pandas as pd +import os +import json +import logging +from typing import Tuple # 核心:导入大写的Tuple +from src.utils.database import db_instance + +# 配置日志 +logging.basicConfig( + level=logging.INFO, + format='%(asctime)s - %(levelname)s - %(message)s' +) +logger = logging.getLogger(__name__) + +# 【核心修复】计算项目根目录,指向正确的config/settings.json +current_file = os.path.abspath(__file__) # src/utils/file_importer.py +utils_dir = os.path.dirname(current_file) +src_dir = os.path.dirname(utils_dir) +project_root = os.path.dirname(src_dir) +config_path = os.path.join(project_root, "config", "settings.json") + +# 读取配置文件 +try: + with open(config_path, "r", encoding="utf-8") as f: + CONFIG = json.load(f) + EXCEL_CONFIG = CONFIG["excel"] +except FileNotFoundError: + logger.error(f"配置文件不存在: {config_path}") + raise +except KeyError as e: + logger.error(f"配置文件格式错误,缺少键: {e}") + raise +except json.JSONDecodeError: + logger.error(f"配置文件解析失败,不是有效的JSON格式: {config_path}") + raise + + +class ExcelImporter: + """Excel导入导出工具类,处理学生信息的导入导出""" + + def __init__(self): + # 确保模板目录存在(基于项目根目录拼接) + self.template_path = os.path.join(project_root, EXCEL_CONFIG["template_path"]) + template_dir = os.path.dirname(self.template_path) + os.makedirs(template_dir, exist_ok=True) + # 生成Excel模板 + self._create_template() + + def _create_template(self): + """创建学生名单Excel模板,包含示例数据指导用户填写""" + if not os.path.exists(self.template_path): + # 添加示例数据便于用户理解格式 + template_data = [ + ["学号", "姓名", "专业"], + ["2023001", "张三", "计算机科学与技术"], + ["2023002", "李四", "软件工程"] + ] + df = pd.DataFrame(template_data[1:], columns=template_data[0]) + try: + df.to_excel(self.template_path, index=False, engine="openpyxl") + logger.info(f"Excel模板已生成:{self.template_path}") + except PermissionError: + logger.error(f"没有权限写入模板文件:{self.template_path}") + except Exception as e: + logger.error(f"生成模板失败:{str(e)}") + + def import_students(self, file_path: str) -> Tuple[bool, str]: # 修正:Tuple[bool, str] + """ + 从Excel导入学生名单并验证数据合法性 + + Args: + file_path: Excel文件路径 + + Returns: + Tuple: (是否成功, 结果信息) + """ + # 基础校验 + if not file_path: + return False, "文件路径不能为空" + if not os.path.exists(file_path): + return False, f"文件不存在:{file_path}" + if not os.path.isfile(file_path): + return False, f"不是有效的文件:{file_path}" + + try: + # 读取Excel + df = pd.read_excel(file_path, engine="openpyxl") + + # 列名校验 + required_cols = ["学号", "姓名", "专业"] + if not all(col in df.columns for col in required_cols): + missing = [col for col in required_cols if col not in df.columns] + return False, f"Excel列名错误,缺少必要列:{missing},需包含:{required_cols}" + + # 数据清洗与校验 + # 删除空行(包含NaN的行) + df = df.dropna(subset=required_cols) + # 转换为字符串并去除首尾空格 + for col in required_cols: + df[col] = df[col].astype(str).str.strip() + + # 校验空字符串 + empty_rows = df[(df["学号"] == "") | (df["姓名"] == "") | (df["专业"] == "")] + if not empty_rows.empty: + return False, f"存在空值数据,行索引:{list(empty_rows.index + 2)}(注意Excel行号从1开始)" + + # 校验学号重复 + duplicate_ids = df[df["学号"].duplicated()]["学号"].unique() + if len(duplicate_ids) > 0: + return False, f"存在重复学号:{list(duplicate_ids)}" + + # 准备导入数据 + students = list(df[required_cols].itertuples(index=False, name=None)) + success_count, fail_list = db_instance.batch_add_students(students) + + # 构建返回信息 + result_msg = f"成功导入{success_count}名学生" + if fail_list: + # 兼容批量导入的失败详情展示 + fail_details = [f"{s}" for s in fail_list] + result_msg += f",失败{len(fail_list)}条:{'; '.join(fail_details)}" + + logger.info(result_msg) + return True, result_msg + + except PermissionError: + return False, f"没有权限读取文件:{file_path}" + except Exception as e: + logger.error(f"导入失败:{str(e)}", exc_info=True) + return False, f"导入失败:{str(e)}" + + def export_scores(self, file_path: str = None) -> Tuple[bool, str]: # 修正:Tuple[bool, str] + """ + 导出学生积分详单到Excel + + Args: + file_path: 导出文件路径,默认使用配置中的路径 + + Returns: + Tuple: (是否成功, 结果信息) + """ + try: + # 拼接默认导出路径(基于项目根目录) + default_export_path = os.path.join(project_root, EXCEL_CONFIG["export_default_path"]) + file_path = file_path or default_export_path + + # 确保导出目录存在 + export_dir = os.path.dirname(file_path) + os.makedirs(export_dir, exist_ok=True) + + # 获取学生数据 + students = db_instance.get_all_students() + if not students: + return False, "无学生数据可导出" + + # 验证数据结构(确保包含所需字段) + required_fields = 5 # 学号、姓名、专业、总积分、随机点名次数 + if any(len(student) != required_fields for student in students): + return False, "学生数据结构错误,无法导出" + + # 构造DataFrame并导出 + df = pd.DataFrame(students, columns=["学号", "姓名", "专业", "总积分", "随机点名次数"]) + df.to_excel(file_path, index=False, engine="openpyxl") + + logger.info(f"积分详单已导出至:{file_path}") + return True, f"积分详单已导出至:{file_path}" + + except PermissionError: + return False, f"没有权限写入文件:{file_path}" + except Exception as e: + logger.error(f"导出失败:{str(e)}", exc_info=True) + return False, f"导出失败:{str(e)}" + + +# 单例实例 +excel_importer = ExcelImporter() \ No newline at end of file diff --git a/class-call-system (1)/src/utils/helpers.py b/class-call-system (1)/src/utils/helpers.py new file mode 100644 index 0000000..b9c81a6 --- /dev/null +++ b/class-call-system (1)/src/utils/helpers.py @@ -0,0 +1,46 @@ +import json +import random +from datetime import datetime + +# 读取配置 +with open("config/settings.json", "r", encoding="utf-8") as f: + CONFIG = json.load(f) +SCORE_RULES = CONFIG["score_rules"] +VIS_CONFIG = CONFIG["visualization"] + +def format_time(timestamp: datetime = None) -> str: + """格式化时间为字符串""" + if not timestamp: + timestamp = datetime.now() + return timestamp.strftime("%Y-%m-%d %H:%M:%S") + +def calculate_weight(scores: list) -> list: + """计算随机点名权重:积分越高,权重越低""" + weights = [] + for score in scores: + if score >= 0: + # 正积分:权重=1/(积分+1),避免除以0 + weight = 1 / (score + 1) + else: + # 负积分:权重=绝对值+1,提高被点到概率 + weight = abs(score) + 1 + weights.append(weight) + return weights + +def get_answer_score(level: str) -> float: + """根据回答等级返回分数(简易版,可扩展)""" + level_map = { + "优秀": 3.0, + "良好": 2.0, + "及格": 0.5, + "错误": -1.0, + "重复正确": SCORE_RULES["repeat_correct"], + "重复错误": SCORE_RULES["repeat_wrong"] + } + return level_map.get(level, 0.0) + +def get_visual_top_n(students: list) -> list: + """获取可视化的TOP N学生""" + # 按积分降序排序,取前N个 + sorted_students = sorted(students, key=lambda x: x[3], reverse=True) + return sorted_students[:VIS_CONFIG["top_n"]] \ No newline at end of file