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 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技术优势分析 + +
+
+

🎯 开发效率优势

+ +
+
+

💻 部署便捷性

+ +
+
+

📊 教育场景适配

+ +
+
+ +### 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