19 KiB
19 KiB
老师学员匹配系统 - 开发文档
项目概述
一个用于老师和学生智能匹配的桌面应用程序,基于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
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 = [] # 匹配结果
核心数据结构
# 老师数据结构
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初始化
def setup_ui(self):
# 创建页面布局:三栏布局
# - 顶部标题栏
# - 操作按钮栏
# - 主内容区(老师信息 | 学生信息 | 匹配结果)
2. 数据导入
def import_teachers(self, e):
# 使用tkinter文件对话框选择Excel文件
# 解析Excel数据,提取老师信息
# 动态列(日期)作为可用时间段存储
# 新增:解析"老师类型"字段
def import_students(self, e):
# 使用tkinter文件对话框选择Excel文件
# 解析Excel数据,提取学生信息
# 历史匹配老师用逗号分隔
# 动态列(日期)作为可用时间段存储
3. 时间段解析
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. 匹配算法
def start_match(self, e):
# 匹配流程:
# 1. 初始化数据结构
# - teacher_assigned_days: 记录老师已分配的日期(新增)
# - teacher_match_count: 记录每个老师已匹配的学生数
#
# 2. 遍历每个学生
# a. 计算期望老师类型(新增逻辑)
# - 历史记录为空 → 旧老师
# - 历史记录奇数 → 新老师
# - 历史记录偶数 → 旧老师
# b. 收集所有可能的匹配
# - 遍历所有老师
# - 跳过历史匹配过的老师
# - 检查老师类型是否符合期望(新增)
# - 检查时间兼容性
# - 检查老师该日期是否已被分配(新增:按日期而非时段)
#
# c. 选择最优匹配
# - 按老师已匹配次数升序排序(优先选择匹配次数少的老师)
# - 选择第一个(匹配次数最少的)
#
# d. 更新状态
# - 记录匹配结果
# - 更新学生的历史匹配记录
# - 标记老师该日期已分配(新增:按日期)
# - 增加老师的匹配计数
#
# e. 处理未匹配情况
# - 如果没有可用的匹配,记录状态为"未匹配"
5. 结果导出
def export_results(self, e):
# 使用tkinter文件对话框让用户选择保存位置
# 导出为Excel文件,中文列标题
匹配算法详解
贪心匹配策略
本系统采用贪心算法实现匹配,优先考虑:
-
首次上课限制(硬约束)
- 如果学生没有历史匹配记录,只能匹配旧老师
- 确保新学生由经验丰富的老师指导
-
交替匹配规则(硬约束)
- 根据历史匹配次数的奇偶性决定期望老师类型
- 历史次数为偶数(包括0)→ 期望旧老师
- 历史次数为奇数 → 期望新老师
- 实现新/旧老师交替教学
-
历史匹配规避(硬约束)
- 如果学生与某老师有过匹配记录,跳过该老师
- 确保每个学生接触不同的老师
-
每日唯一性(硬约束)
- 每位老师每天只能接待一位学生
- 使用
teacher_assigned_days字典追踪已分配日期 - 老师某天被匹配后,当天其他时段不再接受预约
-
时间兼容性(硬约束)
- 学生和老师必须在同一天有相同的时间段
- 时间段必须完全一致(如都是9:00-10:30)
-
均衡分配(软约束/优化目标)
- 优先选择匹配次数较少的老师
- 让更多老师参与到匹配中
- 使用
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. │ │
│ ├─────────────────────┤ ├─────────────────┤ ├─────────────┤ │
│ │ 姓名 │ 类型 │ 备注 │ │ 姓名│历史│备注 │ │ 学生│老师│...│ │
│ ├─────────────────────┤ ├─────────────────┤ ├─────────────┤ │
│ │ │ │ │ │ │ │ │ │ │ │ │
│ └─────────────────────┘ └─────────────────┘ └─────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
窗口配置
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:
# 颜色使用字符串而非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作为替代:
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', '')
安装和运行
开发环境运行
pip install flet pandas openpyxl
python main.py
打包为exe
@echo off
pip install pyinstaller pandas openpyxl flet
pyinstaller --onefile --windowed main.py
打包配置优化
排除不必要的模块以减小打包体积:
pyinstaller --onefile --windowed ^
--exclude-module=torch ^
--exclude-module=torchvision ^
--exclude-module=transformers ^
--exclude-module=scipy ^
--exclude-module=matplotlib ^
--exclude-module=pandas.plotting ^
main.py
测试数据生成
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. 匹配结果不正确
- 检查时间段格式是否正确(使用分号分隔)
- 检查历史老师名称是否与老师姓名完全匹配
- 检查老师类型是否正确标记
项目交付清单
- main.py - 主程序
- build.bat - 打包脚本
- 需求文档.md - 需求说明文档
- 开发文档.md - 技术实现文档
- 老师信息示例.xlsx - 老师测试数据
- 学生信息示例.xlsx - 学生测试数据
- generate_test_data.py - 测试数据生成脚本
- dist/main.exe - 打包后的可执行文件
版本历史
| 版本 | 日期 | 更新内容 |
|---|---|---|
| 1.0 | 2026-04-19 | 初始版本,实现核心匹配功能 |
| 1.1 | 2026-04-25 | 新增每日唯一性约束(老师每天只能接待一位学生) |
| 1.2 | 2026-04-28 | 新增老师类型标记、首次上课限制、交替匹配规则 |