Compare commits
1 Commits
037be95a7d
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 78aaebcd89 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -31,10 +31,6 @@ Thumbs.db
|
||||
*.log
|
||||
*.txt
|
||||
|
||||
# Excel files (示例数据)
|
||||
老师信息示例.xlsx
|
||||
学生信息示例.xlsx
|
||||
匹配结果示例.xlsx
|
||||
|
||||
# Certificates
|
||||
*.pfx
|
||||
@@ -49,4 +45,3 @@ Thumbs.db
|
||||
|
||||
# Documentation (auto-generated)
|
||||
*.html
|
||||
*.md
|
||||
105
debug_match.py
Normal file
105
debug_match.py
Normal file
@@ -0,0 +1,105 @@
|
||||
import pandas as pd
|
||||
|
||||
df_teachers = pd.read_excel('老师信息示例.xlsx')
|
||||
df_students = pd.read_excel('学生信息示例.xlsx')
|
||||
|
||||
teachers = []
|
||||
for _, row in df_teachers.iterrows():
|
||||
availability = {}
|
||||
for col in df_teachers.columns:
|
||||
if col not in ['老师姓名', '老师类型']:
|
||||
val = row.get(col, '')
|
||||
if pd.notna(val) and str(val).strip():
|
||||
availability[col] = str(val)
|
||||
teachers.append({
|
||||
'id': row['老师姓名'],
|
||||
'name': row['老师姓名'],
|
||||
'type': row['老师类型'],
|
||||
'availability': availability
|
||||
})
|
||||
|
||||
students = []
|
||||
for _, row in df_students.iterrows():
|
||||
history_str = row.get('历史匹配老师', '')
|
||||
history = []
|
||||
if pd.notna(history_str) and str(history_str).strip():
|
||||
history = [t.strip() for t in str(history_str).split(',')]
|
||||
|
||||
availability = {}
|
||||
for col in df_students.columns:
|
||||
if col not in ['学生姓名', '历史匹配老师']:
|
||||
val = row.get(col, '')
|
||||
if pd.notna(val) and str(val).strip():
|
||||
availability[col] = str(val)
|
||||
|
||||
students.append({
|
||||
'id': row['学生姓名'],
|
||||
'name': row['学生姓名'],
|
||||
'availability': availability,
|
||||
'history': history
|
||||
})
|
||||
|
||||
print('=== 检查数据导入 ===')
|
||||
print(f'老师数量: {len(teachers)}')
|
||||
print(f'学生数量: {len(students)}')
|
||||
print()
|
||||
|
||||
print('=== 检查老师类型分布 ===')
|
||||
new_count = sum(1 for t in teachers if t['type'] == '新')
|
||||
old_count = sum(1 for t in teachers if t['type'] == '旧')
|
||||
print(f'新老师: {new_count}, 旧老师: {old_count}')
|
||||
print()
|
||||
|
||||
print('=== 检查学生历史记录分布 ===')
|
||||
history_counts = {}
|
||||
for s in students:
|
||||
cnt = len(s['history'])
|
||||
history_counts[cnt] = history_counts.get(cnt, 0) + 1
|
||||
print('学生历史记录数量分布:')
|
||||
for cnt in sorted(history_counts.keys()):
|
||||
print(f' {cnt} 个历史老师: {history_counts[cnt]} 人')
|
||||
print()
|
||||
|
||||
def parse_slots(time_str):
|
||||
slots = []
|
||||
for slot in time_str.split(';'):
|
||||
slot = slot.strip()
|
||||
if slot and '-' in slot:
|
||||
parts = slot.split('-')
|
||||
if len(parts) == 2:
|
||||
slots.append((parts[0].strip(), parts[1].strip()))
|
||||
return slots
|
||||
|
||||
print('=== 检查时间段匹配情况 ===')
|
||||
for student in students[:5]:
|
||||
history_count = len(student['history'])
|
||||
expected_type = '旧' if history_count == 0 else ('新' if history_count % 2 == 1 else '旧')
|
||||
|
||||
print(f'\n学生: {student["name"]}')
|
||||
print(f' 期望老师类型: {expected_type}')
|
||||
print(f' 可用时间: {student["availability"]}')
|
||||
print(f' 历史老师: {student["history"]}')
|
||||
|
||||
found_match = False
|
||||
for teacher in teachers:
|
||||
if teacher['type'] != expected_type:
|
||||
continue
|
||||
if teacher['id'] in student['history']:
|
||||
continue
|
||||
|
||||
for day, s_time in student['availability'].items():
|
||||
if day not in teacher['availability']:
|
||||
continue
|
||||
|
||||
t_time = teacher['availability'][day]
|
||||
s_slots = parse_slots(s_time)
|
||||
t_slots = parse_slots(t_time)
|
||||
|
||||
for s_start, s_end in s_slots:
|
||||
for t_start, t_end in t_slots:
|
||||
if s_start == t_start and s_end == t_end:
|
||||
print(f' ✓ {teacher["name"]} 在 {day} 有匹配时间段: {s_start}-{s_end}')
|
||||
found_match = True
|
||||
|
||||
if not found_match:
|
||||
print(f' ✗ 未找到匹配的老师')
|
||||
BIN
匹配结果示例.xlsx
Normal file
BIN
匹配结果示例.xlsx
Normal file
Binary file not shown.
BIN
学生信息示例.xlsx
Normal file
BIN
学生信息示例.xlsx
Normal file
Binary file not shown.
558
开发文档.md
Normal file
558
开发文档.md
Normal file
@@ -0,0 +1,558 @@
|
||||
# 老师学员匹配系统 - 开发文档
|
||||
|
||||
## 项目概述
|
||||
|
||||
一个用于老师和学生智能匹配的桌面应用程序,基于Flet框架开发,支持时间兼容性检查、历史匹配记录规避、新/旧老师交替匹配、每日唯一性约束等功能。
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 技术 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| Python | 3.13+ | 开发语言 |
|
||||
| Flet | 0.80.4+ | UI框架(基于Flutter) |
|
||||
| pandas | 2.3+ | Excel数据处理 |
|
||||
| openpyxl | 3.1+ | Excel文件读写 |
|
||||
| PyInstaller | 6.16+ | 打包工具 |
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
paike/
|
||||
├── main.py # 主程序入口
|
||||
├── build.bat # 打包脚本
|
||||
├── requirements.txt # 依赖列表(可选)
|
||||
├── 老师信息示例.xlsx # 老师数据示例
|
||||
├── 学生信息示例.xlsx # 学生数据示例
|
||||
├── generate_test_data.py # 测试数据生成脚本
|
||||
├── dist/ # 打包输出目录
|
||||
│ ├── main.exe # 打包后的可执行文件
|
||||
│ ├── 老师信息示例.xlsx
|
||||
│ └── 学生信息示例.xlsx
|
||||
└── build/ # PyInstaller构建缓存
|
||||
```
|
||||
|
||||
## 核心功能需求
|
||||
|
||||
### 1. 数据导入
|
||||
- 从Excel文件导入老师信息(包含老师类型)
|
||||
- 从Excel文件导入学生信息(包含历史匹配)
|
||||
- 支持的文件格式:`.xlsx`
|
||||
|
||||
### 2. 匹配规则(按优先级)
|
||||
| 优先级 | 规则 | 类型 | 说明 |
|
||||
|-------|------|------|------|
|
||||
| 1 | 首次上课限制 | 硬约束 | 学生第一次上课只能匹配旧老师 |
|
||||
| 2 | 交替匹配规则 | 硬约束 | 后续上课按"新老师→旧老师→新老师→..."交替 |
|
||||
| 3 | 历史匹配规避 | 硬约束 | 学生不能匹配之前上过课的老师 |
|
||||
| 4 | 每日唯一性 | 硬约束 | 每位老师每天只能接待一位学生 |
|
||||
| 5 | 均衡分配 | 软约束 | 优先匹配次数少的老师 |
|
||||
|
||||
### 3. 结果导出
|
||||
- 导出匹配结果到Excel文件
|
||||
- 用户可选择保存位置
|
||||
- 中文列标题
|
||||
|
||||
## 数据格式规范
|
||||
|
||||
### 老师信息Excel格式
|
||||
|
||||
| 列名 | 数据类型 | 说明 | 示例 |
|
||||
|------|----------|------|------|
|
||||
| 老师姓名 | 字符串 | 老师唯一标识 | 张1老师 |
|
||||
| 老师类型 | 字符串 | 新老师/旧老师 | 旧老师 |
|
||||
| 备注 | 字符串 | 备注信息(可选) | 数学老师 |
|
||||
| 1月1日 | 字符串 | 可用时间段(分号分隔) | 9:00-10:30; 14:00-15:30 |
|
||||
| 1月2日 | 字符串 | 可用时间段(分号分隔) | 10:30-12:00 |
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
**注意**:
|
||||
- 时间段格式:`开始时间-结束时间`
|
||||
- 多个时间段用分号`;`分隔
|
||||
- 没有可用时间的日期留空
|
||||
|
||||
### 学生信息Excel格式
|
||||
|
||||
| 列名 | 数据类型 | 说明 | 示例 |
|
||||
|------|----------|------|------|
|
||||
| 学生姓名 | 字符串 | 学生唯一标识 | 学生1 |
|
||||
| 历史匹配老师 | 字符串 | 历史老师姓名(逗号分隔) | 张1老师,张2老师 |
|
||||
| 备注 | 字符串 | 备注信息(可选) | |
|
||||
| 1月1日 | 字符串 | 可用时间段(分号分隔) | 9:00-10:30; 14:00-15:30 |
|
||||
| 1月2日 | 字符串 | 可用时间段(分号分隔) | 10:30-12:00 |
|
||||
| ... | ... | ... | ... |
|
||||
|
||||
### 匹配结果Excel格式
|
||||
|
||||
| 列名 | 说明 |
|
||||
|------|------|
|
||||
| 学生 | 学生姓名 |
|
||||
| 老师 | 匹配的老师姓名 |
|
||||
| 日期 | 匹配日期 |
|
||||
| 时段 | 匹配时间段 |
|
||||
| 状态 | 成功/未匹配 |
|
||||
|
||||
## 代码架构
|
||||
|
||||
### 主类:TeacherStudentMatcher
|
||||
|
||||
```python
|
||||
class TeacherStudentMatcher:
|
||||
def __init__(self, page: ft.Page):
|
||||
# 初始化页面配置
|
||||
self.page.title = "老师学员匹配系统"
|
||||
self.page.window_width = 1400
|
||||
self.page.window_height = 800
|
||||
|
||||
# 数据存储
|
||||
self.teachers = [] # 老师列表
|
||||
self.students = [] # 学生列表
|
||||
self.matches = [] # 匹配结果
|
||||
```
|
||||
|
||||
### 核心数据结构
|
||||
|
||||
```python
|
||||
# 老师数据结构
|
||||
teacher = {
|
||||
"id": "老师姓名",
|
||||
"name": "老师姓名",
|
||||
"type": "旧老师", # 新增:老师类型
|
||||
"availability": {
|
||||
"1月1日": "9:00-10:30; 14:00-15:30",
|
||||
"1月2日": "10:30-12:00",
|
||||
# ...
|
||||
},
|
||||
"other": "备注信息"
|
||||
}
|
||||
|
||||
# 学生数据结构
|
||||
student = {
|
||||
"id": "学生姓名",
|
||||
"name": "学生姓名",
|
||||
"availability": {
|
||||
"1月1日": "9:00-10:30; 14:00-15:30",
|
||||
# ...
|
||||
},
|
||||
"history": ["张1老师", "张2老师"], # 历史匹配老师列表
|
||||
"other": "备注信息"
|
||||
}
|
||||
|
||||
# 匹配结果结构
|
||||
match = {
|
||||
"student_name": "学生姓名",
|
||||
"teacher_name": "老师姓名",
|
||||
"match_date": "1月1日",
|
||||
"match_period": "9:00-10:30",
|
||||
"status": "成功"
|
||||
}
|
||||
```
|
||||
|
||||
### 核心方法
|
||||
|
||||
#### 1. UI初始化
|
||||
```python
|
||||
def setup_ui(self):
|
||||
# 创建页面布局:三栏布局
|
||||
# - 顶部标题栏
|
||||
# - 操作按钮栏
|
||||
# - 主内容区(老师信息 | 学生信息 | 匹配结果)
|
||||
```
|
||||
|
||||
#### 2. 数据导入
|
||||
```python
|
||||
def import_teachers(self, e):
|
||||
# 使用tkinter文件对话框选择Excel文件
|
||||
# 解析Excel数据,提取老师信息
|
||||
# 动态列(日期)作为可用时间段存储
|
||||
# 新增:解析"老师类型"字段
|
||||
|
||||
def import_students(self, e):
|
||||
# 使用tkinter文件对话框选择Excel文件
|
||||
# 解析Excel数据,提取学生信息
|
||||
# 历史匹配老师用逗号分隔
|
||||
# 动态列(日期)作为可用时间段存储
|
||||
```
|
||||
|
||||
#### 3. 时间段解析
|
||||
```python
|
||||
def parse_time_slots(self, time_str):
|
||||
# 输入: "9:00-10:30; 14:00-15:30"
|
||||
# 输出: [("9:00", "10:30"), ("14:00", "15:30")]
|
||||
#
|
||||
# 解析逻辑:
|
||||
# 1. 按分号分割时间段
|
||||
# 2. 每个时间段按短横线分割起始和结束时间
|
||||
# 3. 返回时间对元组列表
|
||||
```
|
||||
|
||||
#### 4. 匹配算法
|
||||
```python
|
||||
def start_match(self, e):
|
||||
# 匹配流程:
|
||||
# 1. 初始化数据结构
|
||||
# - teacher_assigned_days: 记录老师已分配的日期(新增)
|
||||
# - teacher_match_count: 记录每个老师已匹配的学生数
|
||||
#
|
||||
# 2. 遍历每个学生
|
||||
# a. 计算期望老师类型(新增逻辑)
|
||||
# - 历史记录为空 → 旧老师
|
||||
# - 历史记录奇数 → 新老师
|
||||
# - 历史记录偶数 → 旧老师
|
||||
# b. 收集所有可能的匹配
|
||||
# - 遍历所有老师
|
||||
# - 跳过历史匹配过的老师
|
||||
# - 检查老师类型是否符合期望(新增)
|
||||
# - 检查时间兼容性
|
||||
# - 检查老师该日期是否已被分配(新增:按日期而非时段)
|
||||
#
|
||||
# c. 选择最优匹配
|
||||
# - 按老师已匹配次数升序排序(优先选择匹配次数少的老师)
|
||||
# - 选择第一个(匹配次数最少的)
|
||||
#
|
||||
# d. 更新状态
|
||||
# - 记录匹配结果
|
||||
# - 更新学生的历史匹配记录
|
||||
# - 标记老师该日期已分配(新增:按日期)
|
||||
# - 增加老师的匹配计数
|
||||
#
|
||||
# e. 处理未匹配情况
|
||||
# - 如果没有可用的匹配,记录状态为"未匹配"
|
||||
```
|
||||
|
||||
#### 5. 结果导出
|
||||
```python
|
||||
def export_results(self, e):
|
||||
# 使用tkinter文件对话框让用户选择保存位置
|
||||
# 导出为Excel文件,中文列标题
|
||||
```
|
||||
|
||||
## 匹配算法详解
|
||||
|
||||
### 贪心匹配策略
|
||||
|
||||
本系统采用贪心算法实现匹配,优先考虑:
|
||||
|
||||
1. **首次上课限制**(硬约束)
|
||||
- 如果学生没有历史匹配记录,只能匹配旧老师
|
||||
- 确保新学生由经验丰富的老师指导
|
||||
|
||||
2. **交替匹配规则**(硬约束)
|
||||
- 根据历史匹配次数的奇偶性决定期望老师类型
|
||||
- 历史次数为偶数(包括0)→ 期望旧老师
|
||||
- 历史次数为奇数 → 期望新老师
|
||||
- 实现新/旧老师交替教学
|
||||
|
||||
3. **历史匹配规避**(硬约束)
|
||||
- 如果学生与某老师有过匹配记录,跳过该老师
|
||||
- 确保每个学生接触不同的老师
|
||||
|
||||
4. **每日唯一性**(硬约束)
|
||||
- 每位老师每天只能接待一位学生
|
||||
- 使用`teacher_assigned_days`字典追踪已分配日期
|
||||
- 老师某天被匹配后,当天其他时段不再接受预约
|
||||
|
||||
5. **时间兼容性**(硬约束)
|
||||
- 学生和老师必须在同一天有相同的时间段
|
||||
- 时间段必须完全一致(如都是9:00-10:30)
|
||||
|
||||
6. **均衡分配**(软约束/优化目标)
|
||||
- 优先选择匹配次数较少的老师
|
||||
- 让更多老师参与到匹配中
|
||||
- 使用`teacher_match_count`字典实现
|
||||
|
||||
### 伪代码
|
||||
|
||||
```
|
||||
for each student in students:
|
||||
possible_matches = []
|
||||
|
||||
# 计算期望老师类型(新增逻辑)
|
||||
history_count = len(student.history)
|
||||
if history_count == 0:
|
||||
expected_type = "旧老师" # 首次上课
|
||||
elif history_count % 2 == 1:
|
||||
expected_type = "新老师" # 第2,4,6...次
|
||||
else:
|
||||
expected_type = "旧老师" # 第3,5,7...次
|
||||
|
||||
for each teacher in teachers:
|
||||
if teacher in student.history:
|
||||
continue # 跳过历史老师
|
||||
|
||||
if teacher.type != expected_type:
|
||||
continue # 跳过类型不匹配的老师(新增)
|
||||
|
||||
for each (day, student_time) in student.availability:
|
||||
if day not in teacher.availability:
|
||||
continue
|
||||
|
||||
if day in teacher_assigned_days[teacher]:
|
||||
continue # 老师当天已被预约(新增)
|
||||
|
||||
teacher_time = teacher.availability[day]
|
||||
|
||||
if student_time and teacher_time:
|
||||
student_slots = parse_time_slots(student_time)
|
||||
teacher_slots = parse_time_slots(teacher_time)
|
||||
|
||||
for each (s_start, s_end) in student_slots:
|
||||
for each (t_start, t_end) in teacher_slots:
|
||||
if s_start == t_start and s_end == t_end:
|
||||
possible_matches.append((teacher, day, s_start, s_end))
|
||||
|
||||
if possible_matches:
|
||||
# 按老师匹配次数升序排序
|
||||
possible_matches.sort(key=lambda x: teacher_match_count[x[0]])
|
||||
|
||||
teacher, day, s_start, s_end = possible_matches[0]
|
||||
|
||||
# 创建匹配记录
|
||||
create_match(student, teacher, day, s_start, s_end, "成功")
|
||||
|
||||
# 更新状态
|
||||
student.history.append(teacher)
|
||||
teacher_assigned_days[teacher].add(day) # 标记日期已分配(修改)
|
||||
teacher_match_count[teacher] += 1
|
||||
else:
|
||||
# 没有可用匹配
|
||||
create_match(student, null, "-", "-", "-", "未匹配")
|
||||
```
|
||||
|
||||
## UI设计规范
|
||||
|
||||
### 配色方案
|
||||
|
||||
| 用途 | 颜色代码 | 说明 |
|
||||
|------|----------|------|
|
||||
| 标题文字 | #1a1a2e | 深色标题 |
|
||||
| 主色调 | #2563eb | 主要操作按钮(蓝色) |
|
||||
| 成功色 | #10b981 | 成功状态(绿色) |
|
||||
| 警告色 | #f59e0b | 警告状态(橙色) |
|
||||
| 错误色 | #ef4444 | 错误状态(红色) |
|
||||
| 背景色 | #f8f9fa | 页面背景 |
|
||||
| 卡片背景 | #ffffff | 卡片白色背景 |
|
||||
| 边框色 | #f0f0f0 | 细微边框 |
|
||||
| 新老师标识 | #2563eb | 蓝色 |
|
||||
| 旧老师标识 | #10b981 | 绿色 |
|
||||
|
||||
### 布局结构
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 老师学员匹配系统 [按钮组] │
|
||||
│ Teacher-Student Matching System │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 数据操作 │ 导入老师 │ 导入学生 │ 匹配操作 │ 开始匹配 │ 导出 │ 清空 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ ┌─────────────────────┐ ┌─────────────────┐ ┌─────────────┐ │
|
||||
│ │ 老师信息 │ │ 学生信息 │ │ 匹配结果 │ │
|
||||
│ │ Teacher Info │ │ Student Info │ │ Matching R. │ │
|
||||
│ ├─────────────────────┤ ├─────────────────┤ ├─────────────┤ │
|
||||
│ │ 姓名 │ 类型 │ 备注 │ │ 姓名│历史│备注 │ │ 学生│老师│...│ │
|
||||
│ ├─────────────────────┤ ├─────────────────┤ ├─────────────┤ │
|
||||
│ │ │ │ │ │ │ │ │ │ │ │ │
|
||||
│ └─────────────────────┘ └─────────────────┘ └─────────────┘ │
|
||||
│ │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 窗口配置
|
||||
|
||||
```python
|
||||
self.page.window_width = 1400 # 窗口宽度
|
||||
self.page.window_height = 800 # 窗口高度
|
||||
self.page.theme_mode = ft.ThemeMode.LIGHT # 浅色主题
|
||||
```
|
||||
|
||||
## Flet API兼容性说明
|
||||
|
||||
### Flet 0.80.4 版本API
|
||||
|
||||
本项目针对Flet 0.80.4版本编写,使用了以下兼容API:
|
||||
|
||||
```python
|
||||
# 颜色使用字符串而非ft.colors枚举
|
||||
ft.Text("text", color="#333333") # ✓ 正确
|
||||
ft.Text("text", color=ft.colors.WHITE) # ✗ 已废弃
|
||||
|
||||
# 对齐使用Alignment类
|
||||
ft.alignment.Alignment(0, 0) # ✓ 正确
|
||||
ft.alignment.top_left # ✗ 已废弃
|
||||
|
||||
# 边框使用Border类
|
||||
ft.Border.all(1, "#e0e0e0") # ✓ 正确
|
||||
ft.border.all(1, "#e0e0e0") # ✗ 已废弃
|
||||
|
||||
# 内边距使用Padding类
|
||||
ft.Padding.only(left=20, right=20) # ✓ 正确
|
||||
ft.padding.only(left=20, right=20) # ✗ 已废弃
|
||||
|
||||
# 外边距使用Margin类
|
||||
ft.Margin.only(left=20, right=20) # ✓ 正确
|
||||
ft.margin.only(left=20, right=20) # ✗ 已废弃
|
||||
|
||||
# 按钮使用Button而非ElevatedButton
|
||||
ft.Button(...) # ✓ 正确
|
||||
ft.ElevatedButton(...) # ✗ 已废弃
|
||||
|
||||
# SnackBar使用page.snack_bar属性
|
||||
self.page.snack_bar = ft.SnackBar(...)
|
||||
self.page.snack_bar.open = True # ✓ 正确
|
||||
self.page.show_snack_bar(...) # ✗ 已废弃
|
||||
|
||||
# 应用入口使用ft.run
|
||||
ft.run(main) # ✓ 正确
|
||||
ft.app(target=main) # ✗ 已废弃
|
||||
|
||||
# 表格边框使用BorderSide
|
||||
ft.border.BorderSide(1, "#e0e0e0") # ✓ 正确
|
||||
```
|
||||
|
||||
## 文件选择对话框
|
||||
|
||||
由于Flet的FilePicker在不同版本中存在兼容性问题,本项目使用tkinter的filedialog作为替代:
|
||||
|
||||
```python
|
||||
import threading
|
||||
from tkinter import filedialog
|
||||
import tkinter as tk
|
||||
|
||||
def ask_file():
|
||||
root = tk.Tk()
|
||||
root.withdraw()
|
||||
root.attributes('-topmost', True) # 置顶显示
|
||||
file_path = filedialog.askopenfilename(
|
||||
title="选择文件",
|
||||
filetypes=[("Excel files", "*.xlsx"), ("All files", "*.*")]
|
||||
)
|
||||
result['path'] = file_path
|
||||
root.destroy()
|
||||
|
||||
thread = threading.Thread(target=ask_file)
|
||||
thread.start()
|
||||
thread.join()
|
||||
file_path = result.get('path', '')
|
||||
```
|
||||
|
||||
## 安装和运行
|
||||
|
||||
### 开发环境运行
|
||||
```bash
|
||||
pip install flet pandas openpyxl
|
||||
python main.py
|
||||
```
|
||||
|
||||
### 打包为exe
|
||||
```batch
|
||||
@echo off
|
||||
pip install pyinstaller pandas openpyxl flet
|
||||
pyinstaller --onefile --windowed main.py
|
||||
```
|
||||
|
||||
### 打包配置优化
|
||||
|
||||
排除不必要的模块以减小打包体积:
|
||||
```batch
|
||||
pyinstaller --onefile --windowed ^
|
||||
--exclude-module=torch ^
|
||||
--exclude-module=torchvision ^
|
||||
--exclude-module=transformers ^
|
||||
--exclude-module=scipy ^
|
||||
--exclude-module=matplotlib ^
|
||||
--exclude-module=pandas.plotting ^
|
||||
main.py
|
||||
```
|
||||
|
||||
## 测试数据生成
|
||||
|
||||
```python
|
||||
import pandas as pd
|
||||
import random
|
||||
|
||||
random.seed(42)
|
||||
|
||||
days = ['1月1日', '1月2日', '1月3日', '1月4日', '1月5日', '1月6日', '1月7日']
|
||||
time_slots = ['9:00-10:30', '10:30-12:00', '14:00-15:30', '15:30-17:00', '18:00-19:30']
|
||||
|
||||
# 生成10个老师(5旧5新)
|
||||
teacher_names = [f"张{i}老师" for i in range(1, 11)]
|
||||
teacher_types = ["旧老师", "旧老师", "旧老师", "新老师", "新老师", "旧老师", "新老师", "旧老师", "新老师", "旧老师"]
|
||||
teachers = []
|
||||
for i, name in enumerate(teacher_names):
|
||||
teacher = {"老师姓名": name}
|
||||
teacher["老师类型"] = teacher_types[i]
|
||||
available_days = random.sample(days, 2) # 每人随机2天空闲
|
||||
for day in days:
|
||||
if day in available_days:
|
||||
num_slots = random.randint(2, 3) # 每天2-3个时间段
|
||||
teacher[day] = '; '.join(random.sample(time_slots, num_slots))
|
||||
else:
|
||||
teacher[day] = ''
|
||||
teacher["备注"] = ""
|
||||
teachers.append(teacher)
|
||||
|
||||
df_teachers = pd.DataFrame(teachers)
|
||||
df_teachers.to_excel('老师信息示例.xlsx', index=False)
|
||||
|
||||
# 生成20个学生
|
||||
student_names = [f"学生{i}" for i in range(1, 21)]
|
||||
students = []
|
||||
for i, name in enumerate(student_names):
|
||||
student = {"学生姓名": name}
|
||||
available_days = random.sample(days, random.randint(3, 5)) # 3-5天空闲
|
||||
for day in days:
|
||||
if day in available_days:
|
||||
num_slots = random.randint(2, 3)
|
||||
student[day] = '; '.join(random.sample(time_slots, num_slots))
|
||||
else:
|
||||
student[day] = ''
|
||||
|
||||
# 每个学生有0-3个历史老师
|
||||
history_count = min(3, i)
|
||||
history_teachers = random.sample([t for t in teacher_names if t != name], history_count)
|
||||
student["历史匹配老师"] = ', '.join(history_teachers)
|
||||
student["备注"] = ""
|
||||
students.append(student)
|
||||
|
||||
df_students = pd.DataFrame(students)
|
||||
df_students.to_excel('学生信息示例.xlsx', index=False)
|
||||
```
|
||||
|
||||
## 常见问题排查
|
||||
|
||||
### 1. 打包后文件选择对话框不显示
|
||||
- 确保使用tkinter的filedialog而非Flet的FilePicker
|
||||
- 在独立线程中运行对话框
|
||||
|
||||
### 2. SnackBar不显示
|
||||
- 使用`page.snack_bar`属性而非`show_snack_bar()`方法
|
||||
- 设置`open = True`来显示
|
||||
|
||||
### 3. 表格不显示数据
|
||||
- 确保调用`page.update()`刷新界面
|
||||
- 检查数据是否正确解析
|
||||
|
||||
### 4. 匹配结果不正确
|
||||
- 检查时间段格式是否正确(使用分号分隔)
|
||||
- 检查历史老师名称是否与老师姓名完全匹配
|
||||
- 检查老师类型是否正确标记
|
||||
|
||||
## 项目交付清单
|
||||
|
||||
- [x] main.py - 主程序
|
||||
- [x] build.bat - 打包脚本
|
||||
- [x] 需求文档.md - 需求说明文档
|
||||
- [x] 开发文档.md - 技术实现文档
|
||||
- [x] 老师信息示例.xlsx - 老师测试数据
|
||||
- [x] 学生信息示例.xlsx - 学生测试数据
|
||||
- [x] generate_test_data.py - 测试数据生成脚本
|
||||
- [x] dist/main.exe - 打包后的可执行文件
|
||||
|
||||
## 版本历史
|
||||
|
||||
| 版本 | 日期 | 更新内容 |
|
||||
|------|------|----------|
|
||||
| 1.0 | 2026-04-19 | 初始版本,实现核心匹配功能 |
|
||||
| 1.1 | 2026-04-25 | 新增每日唯一性约束(老师每天只能接待一位学生) |
|
||||
| 1.2 | 2026-04-28 | 新增老师类型标记、首次上课限制、交替匹配规则 |
|
||||
BIN
老师信息示例.xlsx
Normal file
BIN
老师信息示例.xlsx
Normal file
Binary file not shown.
155
需求文档.md
Normal file
155
需求文档.md
Normal file
@@ -0,0 +1,155 @@
|
||||
# 老师学员匹配系统需求文档
|
||||
|
||||
## 1. 项目概述
|
||||
本系统旨在实现老师和学员的自动匹配功能,根据双方的可用时间进行智能匹配,并生成匹配结果。
|
||||
|
||||
## 2. 功能需求
|
||||
|
||||
### 2.1 基础功能
|
||||
- **老师管理**:支持通过Excel导入老师信息,包含老师类型标记
|
||||
- **时间数据导入**:通过Excel文件导入老师和学生的可用时间
|
||||
- **智能匹配**:根据时间兼容性自动匹配老师和学生
|
||||
- **匹配结果导出**:生成Excel格式的匹配结果
|
||||
- **历史匹配记录**:记录学生与老师的历史匹配情况,避免重复匹配
|
||||
|
||||
### 2.2 核心匹配规则(按优先级)
|
||||
1. **首次上课限制**:学生第一次上课只能匹配旧老师
|
||||
2. **交替匹配规则**:后续上课按"新老师→旧老师→新老师→..."交替进行
|
||||
3. **历史匹配规避**:学生不能匹配之前上过课的老师
|
||||
4. **每日唯一性**:每位老师每天只能接待一位学生
|
||||
5. **均衡分配**:优先匹配次数少的老师,让更多老师参与
|
||||
|
||||
### 2.3 核心流程
|
||||
1. 导入老师信息(包含老师类型)和可用时间
|
||||
2. 导入学生信息(包含历史匹配)和可用时间
|
||||
3. 系统自动匹配老师和学生
|
||||
4. 生成匹配结果并导出
|
||||
5. 记录匹配历史
|
||||
|
||||
## 3. 数据结构
|
||||
|
||||
### 3.1 老师数据结构
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
|-------|---------|------|
|
||||
| 老师ID | 字符串 | 唯一标识 |
|
||||
| 姓名 | 字符串 | 老师姓名 |
|
||||
| 老师类型 | 字符串 | 新老师/旧老师 |
|
||||
| 可用时间 | 时间列表 | 每个日期的可用时间段 |
|
||||
| 其他信息 | 字符串 | 可选的额外信息 |
|
||||
|
||||
### 3.2 学生数据结构
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
|-------|---------|------|
|
||||
| 学生ID | 字符串 | 唯一标识 |
|
||||
| 姓名 | 字符串 | 学生姓名 |
|
||||
| 可用时间 | 时间列表 | 每个日期的可用时间段 |
|
||||
| 历史匹配老师 | 老师ID列表 | 曾经匹配过的老师 |
|
||||
| 其他信息 | 字符串 | 可选的额外信息 |
|
||||
|
||||
### 3.3 匹配结果数据结构
|
||||
| 字段名 | 数据类型 | 说明 |
|
||||
|-------|---------|------|
|
||||
| 匹配ID | 字符串 | 唯一标识 |
|
||||
| 学生ID | 字符串 | 学生标识 |
|
||||
| 老师ID | 字符串 | 老师标识 |
|
||||
| 匹配日期 | 日期 | 匹配的具体日期 |
|
||||
| 匹配时段 | 时间段 | 具体上课时间段 |
|
||||
| 状态 | 字符串 | 匹配状态(成功/未匹配) |
|
||||
|
||||
## 4. Excel格式设计
|
||||
|
||||
### 4.1 老师信息Excel格式
|
||||
| 列名 | 说明 |
|
||||
|-----|------|
|
||||
| 老师姓名 | 唯一标识 |
|
||||
| 老师类型 | 新老师/旧老师 |
|
||||
| 备注 | 可选的额外信息 |
|
||||
| 1月1日 | 可用时间段(如:9:00-11:00;14:00-16:00) |
|
||||
| 1月2日 | 可用时间段 |
|
||||
| ... | ... |
|
||||
|
||||
### 4.2 学生信息Excel格式
|
||||
| 列名 | 说明 |
|
||||
|-----|------|
|
||||
| 学生姓名 | 唯一标识 |
|
||||
| 历史匹配老师 | 曾经匹配过的老师姓名,用逗号分隔 |
|
||||
| 备注 | 可选的额外信息 |
|
||||
| 1月1日 | 可用时间段 |
|
||||
| 1月2日 | 可用时间段 |
|
||||
| ... | ... |
|
||||
|
||||
### 4.3 匹配结果Excel格式
|
||||
| 列名 | 说明 |
|
||||
|-----|------|
|
||||
| 学生 | 学生姓名 |
|
||||
| 老师 | 匹配的老师姓名 |
|
||||
| 日期 | 匹配日期 |
|
||||
| 时段 | 匹配的时间段 |
|
||||
| 状态 | 匹配状态 |
|
||||
|
||||
## 5. 技术要求
|
||||
|
||||
### 5.1 开发环境
|
||||
- 操作系统:Windows
|
||||
- 开发语言:Python(打包为EXE)
|
||||
- 依赖库:
|
||||
- pandas(处理Excel文件)
|
||||
- openpyxl(Excel文件读写)
|
||||
- Flet(GUI界面)
|
||||
- pyinstaller(打包为EXE)
|
||||
|
||||
### 5.2 性能要求
|
||||
- 支持至少100名学生和20名老师的匹配
|
||||
- 匹配过程响应时间不超过10秒
|
||||
|
||||
## 6. 界面设计
|
||||
|
||||
### 6.1 主界面
|
||||
- 老师管理区域:显示老师列表(包含类型标识)
|
||||
- 数据导入区域:提供Excel文件选择和导入按钮
|
||||
- 匹配控制区域:开始匹配按钮
|
||||
- 结果显示区域:显示匹配结果
|
||||
- 导出区域:导出匹配结果按钮
|
||||
|
||||
### 6.2 操作流程
|
||||
1. 用户打开软件
|
||||
2. 导入老师Excel文件(包含类型信息)
|
||||
3. 导入学生Excel文件(包含历史匹配)
|
||||
4. 点击开始匹配按钮
|
||||
5. 查看匹配结果
|
||||
6. 导出匹配结果到Excel文件
|
||||
|
||||
## 7. 其他需要讨论的点
|
||||
|
||||
1. **匹配算法**:具体的匹配规则和优先级如何确定?
|
||||
2. **时间冲突处理**:如果多个学生同时匹配到同一个老师的同一天,如何处理?
|
||||
3. **数据验证**:如何验证导入的Excel数据格式是否正确?
|
||||
4. **错误处理**:当匹配失败或数据异常时,如何提示用户?
|
||||
5. **历史数据存储**:历史匹配记录如何存储和管理?
|
||||
6. **用户权限**:是否需要不同级别的用户权限?
|
||||
7. **界面语言**:是否需要支持多语言?
|
||||
8. **系统兼容性**:支持的Windows版本范围?
|
||||
9. **安装部署**:是否需要安装程序或绿色版?
|
||||
10. **更新机制**:如何进行系统更新?
|
||||
|
||||
## 8. 项目计划
|
||||
|
||||
1. **需求分析与设计**:1天
|
||||
2. **数据结构实现**:1天
|
||||
3. **Excel文件处理**:1天
|
||||
4. **匹配算法实现**:2天
|
||||
5. **GUI界面开发**:2天
|
||||
6. **测试与调试**:2天
|
||||
7. **打包与部署**:1天
|
||||
|
||||
## 9. 验收标准
|
||||
|
||||
1. 系统能够正确导入老师和学生的Excel文件
|
||||
2. 系统能够根据时间兼容性自动匹配老师和学生
|
||||
3. 系统能够避免重复匹配历史上已经匹配过的老师和学生
|
||||
4. 系统能够确保每位老师每天只接待一位学生
|
||||
5. 系统能够按规则分配新/旧老师(首次→旧老师,后续交替)
|
||||
6. 系统能够优先分配匹配次数少的老师
|
||||
7. 系统能够生成正确的匹配结果Excel文件
|
||||
8. 系统界面友好,操作简单
|
||||
9. 系统运行稳定,无明显错误
|
||||
Reference in New Issue
Block a user