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技术优势分析
+
+
+
+
🎯 开发效率优势
+
+- 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日**
+"# 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 0000000..872cf5a
Binary files /dev/null and b/class-call-system (1)/assets/templates/student.xlsx differ
diff --git a/class-call-system (1)/assets/templates/学生名单模板.xlsx b/class-call-system (1)/assets/templates/学生名单模板.xlsx
new file mode 100644
index 0000000..ebb5926
Binary files /dev/null and b/class-call-system (1)/assets/templates/学生名单模板.xlsx differ
diff --git a/class-call-system (1)/class-call-system b/class-call-system (1)/class-call-system
deleted file mode 160000
index 0b2bc70..0000000
--- a/class-call-system (1)/class-call-system
+++ /dev/null
@@ -1 +0,0 @@
-Subproject commit 0b2bc70b1ac7d23f389f21702fe031874e3ed5a6
diff --git a/class-call-system (1)/config/settings.json b/class-call-system (1)/config/settings.json
new file mode 100644
index 0000000..3c62c25
--- /dev/null
+++ b/class-call-system (1)/config/settings.json
@@ -0,0 +1,36 @@
+{
+ "database": {
+ "type": "mysql",
+ "mysql": {
+ "host": "db4free.net",
+ "port": 3306,
+ "user": "root111",
+ "password": "20040927",
+ "dbname": "class_callsystem",
+ "charset": "utf8mb4"
+ },
+ "table_student": "student",
+ "table_record": "call_record"
+ },
+ "excel": {
+ "template_path": "assets/templates/学生名单模板.xlsx",
+ "export_default_path": "积分详单.xlsx"
+ },
+ "animation": {
+ "roll_speed": 50,
+ "roll_times": 20,
+ "flash_duration": 1000
+ },
+ "visualization": {
+ "top_n": 10,
+ "fig_size": [10, 4],
+ "font": "SimHei"
+ },
+ "score_rules": {
+ "arrive_score": 1,
+ "repeat_correct": 0.5,
+ "repeat_wrong": -1,
+ "answer_min": 0.5,
+ "answer_max": 3
+ }
+}
\ No newline at end of file
diff --git a/class-call-system (1)/docs/prototype/课堂随机点名系统_技术实现方案.md b/class-call-system (1)/docs/prototype/课堂随机点名系统_技术实现方案.md
new file mode 100644
index 0000000..9dd8ccf
--- /dev/null
+++ b/class-call-system (1)/docs/prototype/课堂随机点名系统_技术实现方案.md
@@ -0,0 +1,591 @@
+
+# 课堂随机点名系统技术实现方案
+
+**文件名:** 课堂随机点名系统_技术实现方案.md
+
+
+
课堂随机点名系统技术实现方案
+
基于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