save docs
This commit is contained in:
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 | 新增老师类型标记、首次上课限制、交替匹配规则 |
|
||||
Reference in New Issue
Block a user