新增到课签到时间窗口与迟到签到时间窗口配置,优化教练评分机制(未测试)
This commit is contained in:
@@ -0,0 +1,294 @@
|
||||
# 教练业绩统计设计文档
|
||||
|
||||
**版本**: v2.0
|
||||
**日期**: 2026-07-26
|
||||
**状态**: 已确定
|
||||
|
||||
---
|
||||
|
||||
## 一、功能概述
|
||||
|
||||
教练业绩统计为体育馆管理系统提供按教练维度的绩效评估,帮助管理者横向对比教练表现、激励教练提升教学质量。
|
||||
|
||||
### 核心能力
|
||||
|
||||
- **教练排行榜**:按综合评分降序排列所有教练
|
||||
- **教练详情**:查看单个教练的六项指标详情
|
||||
- **自查看板**:教练查看自己的业绩表现
|
||||
|
||||
---
|
||||
|
||||
## 二、指标体系
|
||||
|
||||
### 2.1 六项指标总览
|
||||
|
||||
| 序号 | 指标 | 类型 | 含义 | 数据源 |
|
||||
|------|------|------|------|--------|
|
||||
| 1 | 授课量 | 基础指标 | 统计周期内已完成的团课数量 | `group_course.status IN ('2','6')` |
|
||||
| 2 | 出席人次 | 基础指标 | 学员实际参与的人次 | `group_course_booking.status IN ('2','4','5')` |
|
||||
| 3 | 总预约数 | 基础指标 | 学员预约该教练课程的次数(仅已预约状态) | `group_course_booking.status = '0'` |
|
||||
| 4 | 出勤率 | 派生指标 | 出席人次 / 总预约数 * 100 | 指标2 + 指标3 |
|
||||
| 5 | 满员率 | 派生指标 | 各课程出席人数/满员上限的平均值 | `group_course.max_members` + 指标2明细 |
|
||||
| 6 | 违规次数 | 基础指标 | 统计周期内违规记录数 | `coach_violation` |
|
||||
| 7 | 综合评分 | 派生指标 | 加权综合得分(详见第三章) | 指标1-6 |
|
||||
|
||||
### 2.2 指标口径详解
|
||||
|
||||
#### 授课量
|
||||
|
||||
```
|
||||
SELECT coach_id, COUNT(*) FROM group_course
|
||||
WHERE end_time >= :startTime AND end_time < :endTime
|
||||
AND status IN ('2', '6') AND deleted_at IS NULL
|
||||
GROUP BY coach_id
|
||||
```
|
||||
|
||||
- **status='2'**: 教练手动结课
|
||||
- **status='6'**: 系统自动结课
|
||||
- **排除**: 已取消(status='1')、教练缺席(status='5')的课程
|
||||
|
||||
#### 出席人次
|
||||
|
||||
```
|
||||
SELECT gc.coach_id, COUNT(*) FROM group_course_booking b
|
||||
INNER JOIN group_course gc ON b.course_id = gc.id
|
||||
WHERE b.status IN ('2', '4', '5') AND b.deleted_at IS NULL
|
||||
AND gc.deleted_at IS NULL
|
||||
AND gc.end_time >= :startTime AND gc.end_time < :endTime
|
||||
GROUP BY gc.coach_id
|
||||
```
|
||||
|
||||
- **status='2'**: 已出席 — 学员正常到课
|
||||
- **status='4'**: 教练缺席 — 教练未到,学员仍需记录
|
||||
- **status='5'**: 迟到 — 学员迟到但仍到场参与
|
||||
|
||||
> **设计意图**: 教练缺席和迟到时,学员仍到达了现场(或尝试参与),责任在教练而非学员,故计入出席人次。学员无故缺席(status='3')不计入,因其既非教练功劳也非教练责任。
|
||||
|
||||
#### 总预约数
|
||||
|
||||
```
|
||||
SELECT gc.coach_id, COUNT(*) FROM group_course_booking b
|
||||
INNER JOIN group_course gc ON b.course_id = gc.id
|
||||
WHERE b.status = '0' AND b.deleted_at IS NULL
|
||||
AND gc.deleted_at IS NULL
|
||||
AND gc.end_time >= :startTime AND gc.end_time < :endTime
|
||||
GROUP BY gc.coach_id
|
||||
```
|
||||
|
||||
- **仅 status='0'(已预约)**: 作为出勤率分母,表示"承诺来上课的学员"。
|
||||
|
||||
#### 满员率
|
||||
|
||||
```
|
||||
SELECT gc.coach_id, gc.max_members, COUNT(b.id) AS attended
|
||||
FROM group_course gc
|
||||
LEFT JOIN group_course_booking b ON gc.id = b.course_id
|
||||
AND b.status IN ('2','4','5') AND b.deleted_at IS NULL
|
||||
WHERE gc.end_time >= :startTime AND gc.end_time < :endTime
|
||||
AND gc.status IN ('2', '6') AND gc.deleted_at IS NULL
|
||||
GROUP BY gc.coach_id, gc.id, gc.max_members
|
||||
```
|
||||
|
||||
- 对每个已完成课程,计算 `出席人数 / max_members`
|
||||
- 所有课程的比值取平均值
|
||||
- `max_members = 0` 的课程被跳过(除零防御)
|
||||
|
||||
#### 违规次数
|
||||
|
||||
```
|
||||
SELECT coach_id, COUNT(*) FROM coach_violation
|
||||
WHERE violation_time >= :startTime AND violation_time < :endTime
|
||||
AND deleted_at IS NULL
|
||||
GROUP BY coach_id
|
||||
```
|
||||
|
||||
- 违规类型: `COACH_LATE`(迟到)、`COACH_ABSENT`(缺席)、`NOT_MANUAL_END`(未手动结课)
|
||||
|
||||
#### 时间基准
|
||||
|
||||
所有指标均基于 `group_course.end_time` 过滤时间范围。跨月课程归属于结束时间所在的月份。
|
||||
|
||||
---
|
||||
|
||||
## 三、综合评分算法
|
||||
|
||||
### 3.1 最终公式
|
||||
|
||||
```
|
||||
综合评分 = 授课量归一化分 * 0.35
|
||||
+ 出勤率 * 0.25
|
||||
+ 满员率 * 0.25
|
||||
+ 违规分 * 0.15
|
||||
```
|
||||
|
||||
### 3.2 授课量归一化:百分位排名法(方案 B)
|
||||
|
||||
对于教练数为 N 的集合:
|
||||
|
||||
1. 将所有教练按授课量升序排列
|
||||
2. 统计授课量严格小于当前教练的教练数 `C_fewer`
|
||||
3. `normalizedCourses = C_fewer / (N - 1) * 100`(N=1 时取 100)
|
||||
|
||||
**示例**(4 位教练):
|
||||
|
||||
| 教练 | 授课量 | 小于其的教练数 | 归一化分 |
|
||||
|------|--------|---------------|---------|
|
||||
| A | 20 | 3 | 100.0 |
|
||||
| B | 15 | 2 | 66.7 |
|
||||
| C | 10 | 1 | 33.3 |
|
||||
| D | 5 | 0 | 0.0 |
|
||||
|
||||
**设计意图**: 百分位排名在"相对比较"和"公平性"之间取得平衡。授课量最大的教练得满分,最少的得 0 分,中间按排名线性分布。不受极端值影响——即使第一名开 100 节课、第二名只开 20 节,第二名的排名分数依然是 `2/3 * 100 ≈ 66.7`。
|
||||
|
||||
### 3.3 违规分:线性扣分法(方案 A)
|
||||
|
||||
```
|
||||
violationScore = max(0, 100 - violations * 20)
|
||||
```
|
||||
|
||||
| 违规次数 | 违规分 |
|
||||
|----------|--------|
|
||||
| 0 | 100 |
|
||||
| 1 | 80 |
|
||||
| 2 | 60 |
|
||||
| 3 | 40 |
|
||||
| 4 | 20 |
|
||||
| 5+ | 0 |
|
||||
|
||||
**设计意图**: 线性扣分简单直观,每次违规固定扣 20 分,累计 5 次后清零。在 15% 的权重下,每次违规对综合评分的影响约为 `20 * 0.15 = 3 分`。
|
||||
|
||||
### 3.4 出勤率 & 满员率
|
||||
|
||||
这两项直接使用原始百分比(0-100),无需归一化——它们天然在 0-100 范围内且具有绝对含义。
|
||||
|
||||
```
|
||||
出勤率 = 出席人次 / 总预约数 * 100(分母为 0 时取 0)
|
||||
满员率 = avg(单个课程出席人数 / max_members) * 100(跳过 max_members=0 的课程)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、方案选择记录
|
||||
|
||||
### 4.1 授课量归一化方案
|
||||
|
||||
| 方案 | 公式 | 优点 | 缺点 | 决定 |
|
||||
|------|------|------|------|------|
|
||||
| **B: 百分位排名** | `C_fewer / (N-1) * 100` | 直观、不受极端值影响 | 对教练总数敏感(N<3 时分布粗糙) | **采纳** |
|
||||
| A: 最大值归一化 | `courses / max(courses) * 100` | 数学简洁 | 极端值压缩中游得分 | 初版方案,已废弃 |
|
||||
| C: 对数归一化 | `ln(x+1)/ln(m+1)*100` | 压制极端值 | 解释性弱 | 已拒绝 |
|
||||
| D: 固定目标 | `min(x/target*100, 100)` | 达标制、不互卷 | target 难校准 | 已拒绝 |
|
||||
|
||||
### 4.2 违规扣分方案
|
||||
|
||||
| 方案 | 公式 | 优点 | 缺点 | 决定 |
|
||||
|------|------|------|------|------|
|
||||
| **A: 线性扣分** | `max(0, 100 - v*20)` | 简单直白,每次等量扣分 | 多次违规后惩罚不再加剧 | **采纳** |
|
||||
| B: 阶梯扣分 | 0→100, 1→70, 2→40, 3→10 | 首次违规惩罚重,有威慑力 | 第 1 到第 2 次差距 30 分,太激进 | 已拒绝 |
|
||||
| C: 归一化扣分 | `(1 - v/max)*100` | 相对最差教练 | 依赖数据集,全员无违规则无意义 | 已拒绝 |
|
||||
|
||||
### 4.3 权重分配方案
|
||||
|
||||
| 方案 | 授课量 | 出勤率 | 满员率 | 违规 | 决定 |
|
||||
|------|--------|--------|--------|------|------|
|
||||
| **A: 轻违规** | 35% | 25% | 25% | 15% | **采纳** |
|
||||
| B: 中违规 | 30% | 25% | 25% | 20% | 已拒绝 |
|
||||
| C: 重违规 | 30% | 23% | 22% | 25% | 已拒绝 |
|
||||
|
||||
### 4.4 出勤率口径方案
|
||||
|
||||
| 方案 | 分子 | 分母 | 决定 |
|
||||
|------|------|------|------|
|
||||
| **当前** | status IN ('2','4','5') | status='0' | **采纳** |
|
||||
| 初版 | status='2' | status!='1' | 已废弃 |
|
||||
| 替代方案1 | status='2' | status IN ('0','2','3') | 已拒绝(无故缺席应排除) |
|
||||
| 替代方案2 | status='2' | status IN ('0','2') | 已拒绝(不能区分取消预约) |
|
||||
|
||||
---
|
||||
|
||||
## 五、数据模型
|
||||
|
||||
### 5.1 API 响应模型
|
||||
|
||||
```java
|
||||
public class CoachPerformance {
|
||||
Long coachId; // 教练ID
|
||||
String coachName; // 教练昵称
|
||||
String avatar; // 头像URL
|
||||
Long completedCourses; // 授课量
|
||||
Long attendedStudents; // 出席人次
|
||||
Long totalBookings; // 总预约数
|
||||
Double attendanceRate; // 出勤率 (%)
|
||||
Double fillRate; // 满员率 (%)
|
||||
Long violationCount; // 违规次数
|
||||
Double compositeScore; // 综合评分
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 API 接口
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| GET | `/api/datacount/coach-performance/ranking` | 全部教练业绩排行榜 |
|
||||
| GET | `/api/datacount/coach-performance/{coachId}` | 单个教练业绩详情 |
|
||||
| GET | `/api/datacount/coach-performance/mine?coachId=` | 教练自查看板 |
|
||||
|
||||
**查询参数**: `statType`, `periodType`(DAY/WEEK/MONTH/LAST_30_DAYS/LAST_90_DAYS/YEAR), `startTime`, `endTime`
|
||||
|
||||
---
|
||||
|
||||
## 六、代码架构
|
||||
|
||||
```
|
||||
CoachPerformanceHandler ── Reactive Router Function,解析请求
|
||||
│
|
||||
IDataStatisticsService ── 接口定义
|
||||
│
|
||||
DataStatisticsServiceImpl ── 6 并行查询 + 聚合计算
|
||||
│
|
||||
DataStatisticsDao ── DatabaseClient SQL 聚合
|
||||
│
|
||||
┌───┼───┬───┬───┬───┐
|
||||
▼ ▼ ▼ ▼ ▼ ▼
|
||||
sys_user group_course group_course_booking coach_violation
|
||||
```
|
||||
|
||||
### 查询执行流程
|
||||
|
||||
```
|
||||
1. Flux: getAllCoachesWithInfo() ─→ Map<coachId, 基本信息>
|
||||
2. Flux: countCompletedCoursesByCoach() ─→ Map<coachId, 授课量> ┐
|
||||
3. Flux: countAttendedStudentsByCoach() ─→ Map<coachId, 出席人次> │
|
||||
4. Flux: countTotalBookingsByCoach() ─→ Map<coachId, 总预约数> ├─ Mono.zip
|
||||
5. Flux: getFillRateDetailByCoach() ─→ Map<coachId, List<明细>> │
|
||||
6. Flux: countViolationsByCoach() ─→ Map<coachId, 违规次数> ┘
|
||||
│
|
||||
flatMapMany: 逐教练计算指标
|
||||
│
|
||||
sorted: 按综合评分降序
|
||||
│
|
||||
Flux<CoachPerformance>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、边界情况处理
|
||||
|
||||
| 场景 | 行为 |
|
||||
|------|------|
|
||||
| 教练无任何已完成课程 | 所有指标为 0,综合评分 = 0 + 0 + 0 + 15 = **15**(违规分满分 100 * 0.15) |
|
||||
| 教练有课程但无人预约 | 授课量 > 0,出勤率/满员率 = 0 |
|
||||
| 课程 max_members = 0 | 该课程跳过,不参与满员率计算 |
|
||||
| 仅有 1 位教练 | 百分位排名直接返回 100 |
|
||||
| 所有教练授课量相同 | 所有教练 `C_fewer = 0`,授课量归一化分均为 0 |
|
||||
| 跨月课程 | 以 end_time 所在月份归类 |
|
||||
| 查询单个教练不存在 | 返回零值 `CoachPerformance`,coachName="未知教练" |
|
||||
|
||||
---
|
||||
|
||||
## 八、变更历史
|
||||
|
||||
| 日期 | 版本 | 变更内容 |
|
||||
|------|------|---------|
|
||||
| 2026-07-22 | v1.0 | 初版:最大值归一化 + 三维度评分(4:3:3),违规仅展示 |
|
||||
| 2026-07-26 | v2.0 | 时间基准改为 end_time;出席人次扩展为(2,4,5);出勤率分母改为仅 status='0';授课量归一化改为百分位排名;违规纳入综合评分(权重 15%);满员率防御除零 |
|
||||
Reference in New Issue
Block a user