Aryenys 3 months ago
parent 3f80202931
commit a95dc54855

@ -0,0 +1,63 @@
<!--
自动生成:为 AI 编码代理准备的仓库说明,帮助快速理解架构、约定与常用操作。
请在修改代码风格或重要约定时同步更新本文件。
-->
# 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 等),我会据此迭代补充内容。
*** 结束 ***

@ -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_*

@ -0,0 +1,592 @@
# 课堂随机点名系统技术实现方案
**文件名:** 课堂随机点名系统_技术实现方案.md
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px; color: white; margin: 20px 0;">
<h1 style="color: white; text-align: center; margin: 0; font-size: 2.5em;">课堂随机点名系统技术实现方案</h1>
<p style="color: white; text-align: center; margin: 10px 0 0 0; font-size: 1.2em;">基于Python Tkinter的桌面应用开发方案</p>
</div>
| 文档版本 | 编写日期 |
|---------|----------|
| v1.0 | 2025年11月22日 |
## 1. 技术栈选择说明
<div style="background-color: #e8f4fd; border-left: 4px solid #007bff; padding: 20px; margin: 20px 0; border-radius: 8px;">
<h3 style="color: #0056b3; margin-top: 0;">💡 技术选型决策依据</h3>
<p style="color: #333;">基于教育场景的特殊需求选择Python + Tkinter技术栈具备明显的优势对比</p>
</div>
### 1.1 Python + Tkinter技术优势分析
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin: 25px 0;">
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
<h4 style="color: #007bff; margin-top: 0;">🎯 开发效率优势</h4>
<ul>
<li style="color: #333;">Python语法简洁开发周期缩短40%</li>
<li style="color: #333;">丰富的第三方库支持数据操作</li>
<li style="color: #333;">快速原型验证和迭代开发</li>
</ul>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
<h4 style="color: #28a745; margin-top: 0;">💻 部署便捷性</h4>
<ul>
<li style="color: #333;">跨平台兼容Windows/macOS/Linux</li>
<li style="color: #333;">无需复杂环境配置</li>
<li style="color: #333;">单文件打包分发</li>
</ul>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
<h4 style="color: #dc3545; margin-top: 0;">📊 教育场景适配</h4>
<ul>
<li style="color: #333;">教育资源丰富,易于教学维护</li>
<li style="color: #333;">教师友好型界面设计</li>
<li style="color: #333;">离线使用能力保障</li>
</ul>
</div>
</div>
### 1.2 技术栈对比分析
<div style="overflow-x: auto; margin: 20px 0;">
| 技术方案 | 开发难度 | 部署复杂度 | 性能表现 | 扩展性 | 维护成本 |
|---------|---------|-----------|---------|--------|---------|
| <span style="color: #007bff; font-weight: bold;">Python + Tkinter</span> | <span style="color: #28a745;"></span> | <span style="color: #28a745;"></span> | <span style="color: #ffc107;">中等</span> | <span style="color: #28a745;"></span> | <span style="color: #28a745;"></span> |
| Web方案(HTML+JS) | <span style="color: #ffc107;">中等</span> | <span style="color: #dc3545;"></span> | <span style="color: #28a745;"></span> | <span style="color: #28a745;"></span> | <span style="color: #ffc107;">中等</span> |
| C# WinForms | <span style="color: #ffc107;">中等</span> | <span style="color: #ffc107;">中等</span> | <span style="color: #28a745;"></span> | <span style="color: #ffc107;">中等</span> | <span style="color: #ffc107;">中等</span> |
| Java Swing | <span style="color: #dc3545;"></span> | <span style="color: #dc3545;"></span> | <span style="color: #28a745;"></span> | <span style="color: #28a745;"></span> | <span style="color: #dc3545;"></span> |
</div>
### 1.3 核心依赖库选择
```python
# 核心依赖库配置
required_libraries = {
"tkinter": "内置GUI框架",
"sqlite3": "内置轻量级数据库",
"pandas": "Excel文件处理和数据操作",
"openpyxl": "Excel文件读写支持",
"datetime": "日期时间处理",
"random": "随机算法实现",
"json": "配置文件处理"
}
```
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 15px 0; border-radius: 5px;">
<strong style="color: #856404;">⚠️ 注意事项:</strong><span style="color: #856404;"> 优先使用Python内置库减少外部依赖确保部署稳定性</span>
</div>
## 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 点名引擎模块实现
<div style="background-color: #f8f9fa; border-left: 4px solid #6f42c1; padding: 20px; margin: 20px 0; border-radius: 8px;">
<h4 style="color: #6f42c1; margin-top: 0;">🎲 随机算法核心实现</h4>
</div>
```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 数据表详细设计
<div style="overflow-x: auto; margin: 20px 0;">
| 表名 | 字段名 | 数据类型 | 约束 | 说明 |
|------|--------|---------|------|------|
| **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 | 更新时间 |
</div>
### 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 响应式布局适配
<div style="background-color: #d4edda; border-left: 4px solid #28a745; padding: 15px; margin: 15px 0; border-radius: 5px;">
<strong style="color: #155724;">✅ 最佳实践:</strong><span style="color: #155724;"> 使用Grid布局管理器实现灵活的响应式设计</span>
</div>
```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 环境要求与依赖安装
<div style="overflow-x: auto; margin: 20px 0;">
| 环境组件 | 版本要求 | 安装方法 | 验证命令 |
|---------|---------|---------|---------|
| 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+ | - | - |
</div>
### 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 部署操作指南
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 15px 0; border-radius: 5px;">
<strong style="color: #856404;">📋 部署步骤:</strong><span style="color: #856404;"> 按照以下步骤完成系统部署</span>
</div>
**步骤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 故障排除指南
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin: 25px 0;">
<div style="background: #f8d7da; padding: 15px; border-radius: 8px;">
<h4 style="color: #721c24; margin-top: 0;">❌ 常见问题1依赖包安装失败</h4>
<p style="color: #721c24;"><strong>解决方案:</strong>使用国内镜像源安装<br>pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt</p>
</div>
<div style="background: #d1ecf1; padding: 15px; border-radius: 8px;">
<h4 style="color: #0c5460; margin-top: 0;">❌ 常见问题2Excel导入失败</h4>
<p style="color: #0c5460;"><strong>解决方案:</strong>检查Excel文件格式确保包含学号、姓名、班级三列</p>
</div>
<div style="background: #ffeaa7; padding: 15px; border-radius: 8px;">
<h4 style="color: #856404; margin-top: 0;">❌ 常见问题3界面显示异常</h4>
<p style="color: #856404;"><strong>解决方案:</strong>调整系统DPI设置或使用兼容模式运行</p>
</div>
</div>
## 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 内存管理优化
<div style="background-color: #e8f4fd; border-left: 4px solid #007bff; padding: 20px; margin: 20px 0; border-radius: 8px;">
<h4 style="color: #0056b3; margin-top: 0;">💡 内存优化策略</h4>
<ul>
<li style="color: #333;">使用分页加载大量学生数据</li>
<li style="color: #333;">及时释放不再使用的界面组件</li>
<li style="color: #333;">优化图片和资源文件加载</li>
<li style="color: #333;">定期清理临时数据</li>
</ul>
</div>
## 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"

@ -1 +0,0 @@
Subproject commit 0b2bc70b1ac7d23f389f21702fe031874e3ed5a6

@ -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
}
}

@ -0,0 +1,591 @@
# 课堂随机点名系统技术实现方案
**文件名:** 课堂随机点名系统_技术实现方案.md
<div style="background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); padding: 30px; border-radius: 10px; color: white; margin: 20px 0;">
<h1 style="color: white; text-align: center; margin: 0; font-size: 2.5em;">课堂随机点名系统技术实现方案</h1>
<p style="color: white; text-align: center; margin: 10px 0 0 0; font-size: 1.2em;">基于Python Tkinter的桌面应用开发方案</p>
</div>
| 文档版本 | 编写日期 |
|---------|----------|
| v1.0 | 2025年11月22日 |
## 1. 技术栈选择说明
<div style="background-color: #e8f4fd; border-left: 4px solid #007bff; padding: 20px; margin: 20px 0; border-radius: 8px;">
<h3 style="color: #0056b3; margin-top: 0;">💡 技术选型决策依据</h3>
<p style="color: #333;">基于教育场景的特殊需求选择Python + Tkinter技术栈具备明显的优势对比</p>
</div>
### 1.1 Python + Tkinter技术优势分析
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin: 25px 0;">
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
<h4 style="color: #007bff; margin-top: 0;">🎯 开发效率优势</h4>
<ul>
<li style="color: #333;">Python语法简洁开发周期缩短40%</li>
<li style="color: #333;">丰富的第三方库支持数据操作</li>
<li style="color: #333;">快速原型验证和迭代开发</li>
</ul>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
<h4 style="color: #28a745; margin-top: 0;">💻 部署便捷性</h4>
<ul>
<li style="color: #333;">跨平台兼容Windows/macOS/Linux</li>
<li style="color: #333;">无需复杂环境配置</li>
<li style="color: #333;">单文件打包分发</li>
</ul>
</div>
<div style="background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0,0,0,0.08);">
<h4 style="color: #dc3545; margin-top: 0;">📊 教育场景适配</h4>
<ul>
<li style="color: #333;">教育资源丰富,易于教学维护</li>
<li style="color: #333;">教师友好型界面设计</li>
<li style="color: #333;">离线使用能力保障</li>
</ul>
</div>
</div>
### 1.2 技术栈对比分析
<div style="overflow-x: auto; margin: 20px 0;">
| 技术方案 | 开发难度 | 部署复杂度 | 性能表现 | 扩展性 | 维护成本 |
|---------|---------|-----------|---------|--------|---------|
| <span style="color: #007bff; font-weight: bold;">Python + Tkinter</span> | <span style="color: #28a745;"></span> | <span style="color: #28a745;"></span> | <span style="color: #ffc107;">中等</span> | <span style="color: #28a745;"></span> | <span style="color: #28a745;"></span> |
| Web方案(HTML+JS) | <span style="color: #ffc107;">中等</span> | <span style="color: #dc3545;"></span> | <span style="color: #28a745;"></span> | <span style="color: #28a745;"></span> | <span style="color: #ffc107;">中等</span> |
| C# WinForms | <span style="color: #ffc107;">中等</span> | <span style="color: #ffc107;">中等</span> | <span style="color: #28a745;"></span> | <span style="color: #ffc107;">中等</span> | <span style="color: #ffc107;">中等</span> |
| Java Swing | <span style="color: #dc3545;"></span> | <span style="color: #dc3545;"></span> | <span style="color: #28a745;"></span> | <span style="color: #28a745;"></span> | <span style="color: #dc3545;"></span> |
</div>
### 1.3 核心依赖库选择
```python
# 核心依赖库配置
required_libraries = {
"tkinter": "内置GUI框架",
"sqlite3": "内置轻量级数据库",
"pandas": "Excel文件处理和数据操作",
"openpyxl": "Excel文件读写支持",
"datetime": "日期时间处理",
"random": "随机算法实现",
"json": "配置文件处理"
}
```
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 15px 0; border-radius: 5px;">
<strong style="color: #856404;">⚠️ 注意事项:</strong><span style="color: #856404;"> 优先使用Python内置库减少外部依赖确保部署稳定性</span>
</div>
## 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 点名引擎模块实现
<div style="background-color: #f8f9fa; border-left: 4px solid #6f42c1; padding: 20px; margin: 20px 0; border-radius: 8px;">
<h4 style="color: #6f42c1; margin-top: 0;">🎲 随机算法核心实现</h4>
</div>
```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 数据表详细设计
<div style="overflow-x: auto; margin: 20px 0;">
| 表名 | 字段名 | 数据类型 | 约束 | 说明 |
|------|--------|---------|------|------|
| **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 | 更新时间 |
</div>
### 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 响应式布局适配
<div style="background-color: #d4edda; border-left: 4px solid #28a745; padding: 15px; margin: 15px 0; border-radius: 5px;">
<strong style="color: #155724;">✅ 最佳实践:</strong><span style="color: #155724;"> 使用Grid布局管理器实现灵活的响应式设计</span>
</div>
```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 环境要求与依赖安装
<div style="overflow-x: auto; margin: 20px 0;">
| 环境组件 | 版本要求 | 安装方法 | 验证命令 |
|---------|---------|---------|---------|
| 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+ | - | - |
</div>
### 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 部署操作指南
<div style="background-color: #fff3cd; border-left: 4px solid #ffc107; padding: 15px; margin: 15px 0; border-radius: 5px;">
<strong style="color: #856404;">📋 部署步骤:</strong><span style="color: #856404;"> 按照以下步骤完成系统部署</span>
</div>
**步骤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 故障排除指南
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin: 25px 0;">
<div style="background: #f8d7da; padding: 15px; border-radius: 8px;">
<h4 style="color: #721c24; margin-top: 0;">❌ 常见问题1依赖包安装失败</h4>
<p style="color: #721c24;"><strong>解决方案:</strong>使用国内镜像源安装<br>pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt</p>
</div>
<div style="background: #d1ecf1; padding: 15px; border-radius: 8px;">
<h4 style="color: #0c5460; margin-top: 0;">❌ 常见问题2Excel导入失败</h4>
<p style="color: #0c5460;"><strong>解决方案:</strong>检查Excel文件格式确保包含学号、姓名、班级三列</p>
</div>
<div style="background: #ffeaa7; padding: 15px; border-radius: 8px;">
<h4 style="color: #856404; margin-top: 0;">❌ 常见问题3界面显示异常</h4>
<p style="color: #856404;"><strong>解决方案:</strong>调整系统DPI设置或使用兼容模式运行</p>
</div>
</div>
## 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 内存管理优化
<div style="background-color: #e8f4fd; border-left: 4px solid #007bff; padding: 20px; margin: 20px 0; border-radius: 8px;">
<h4 style="color: #0056b3; margin-top: 0;">💡 内存优化策略</h4>
<ul>
<li style="color: #333;">使用分页加载大量学生数据</li>
<li style="color: #333;">及时释放不再使用的界面组件</li>
<li style="color: #333;">优化图片和资源文件加载</li>
<li style="color: #333;">定期清理临时数据</li>
</ul>
</div>
## 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日**

@ -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()

@ -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

@ -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)

@ -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

@ -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()

@ -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)

@ -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)

@ -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
)

@ -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

@ -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()

@ -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"]]
Loading…
Cancel
Save