From 78aaebcd89a0af376cc0fe7561f5f3faa721bdaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=A4=A9=E7=81=B5=E7=81=B5?= Date: Thu, 7 May 2026 14:56:53 +0800 Subject: [PATCH] save docs --- .gitignore | 7 +- debug_match.py | 105 +++++++++ 匹配结果示例.xlsx | Bin 0 -> 6659 bytes 学生信息示例.xlsx | Bin 0 -> 9138 bytes 开发文档.md | 558 ++++++++++++++++++++++++++++++++++++++++++++++ 老师信息示例.xlsx | Bin 0 -> 6390 bytes 需求文档.md | 155 +++++++++++++ 7 files changed, 819 insertions(+), 6 deletions(-) create mode 100644 debug_match.py create mode 100644 匹配结果示例.xlsx create mode 100644 学生信息示例.xlsx create mode 100644 开发文档.md create mode 100644 老师信息示例.xlsx create mode 100644 需求文档.md diff --git a/.gitignore b/.gitignore index 5294833..c6d0e80 100644 --- a/.gitignore +++ b/.gitignore @@ -31,10 +31,6 @@ Thumbs.db *.log *.txt -# Excel files (示例数据) -老师信息示例.xlsx -学生信息示例.xlsx -匹配结果示例.xlsx # Certificates *.pfx @@ -48,5 +44,4 @@ Thumbs.db *.zip # Documentation (auto-generated) -*.html -*.md \ No newline at end of file +*.html \ No newline at end of file diff --git a/debug_match.py b/debug_match.py new file mode 100644 index 0000000..e9e810e --- /dev/null +++ b/debug_match.py @@ -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' ✗ 未找到匹配的老师') \ No newline at end of file diff --git a/匹配结果示例.xlsx b/匹配结果示例.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..f72a959042ce56c61cf0c42437e85f4084f8b7aa GIT binary patch literal 6659 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 literal 0 HcmV?d00001 diff --git a/开发文档.md b/开发文档.md new file mode 100644 index 0000000..174e415 --- /dev/null +++ b/开发文档.md @@ -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 | 新增老师类型标记、首次上课限制、交替匹配规则 | \ No newline at end of file diff --git a/老师信息示例.xlsx b/老师信息示例.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..021a630488a02115abe1067d73590ccf68bfa96a GIT binary patch literal 6390 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 literal 0 HcmV?d00001 diff --git a/需求文档.md b/需求文档.md new file mode 100644 index 0000000..4174929 --- /dev/null +++ b/需求文档.md @@ -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. 系统运行稳定,无明显错误 \ No newline at end of file