Files
HuiPaiKe/开发文档.md
2026-05-07 14:56:53 +08:00

19 KiB
Raw Permalink Blame History

老师学员匹配系统 - 开发文档

项目概述

一个用于老师和学生智能匹配的桌面应用程序基于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文件中文列标题

匹配算法详解

贪心匹配策略

本系统采用贪心算法实现匹配,优先考虑:

  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. │  │
│  ├─────────────────────┤  ├─────────────────┤  ├─────────────┤  │
│  │ 姓名 │ 类型 │ 备注  │  │ 姓名│历史│备注  │  │ 学生│老师│...│  │
│  ├─────────────────────┤  ├─────────────────┤  ├─────────────┤  │
│  │      │      │       │  │      │     │     │  │      │     │   │
│  └─────────────────────┘  └─────────────────┘  └─────────────┘  │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘

窗口配置

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 新增老师类型标记、首次上课限制、交替匹配规则