11 KiB
11 KiB
教练业绩统计设计文档
版本: 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 的集合:
- 将所有教练按授课量升序排列
- 统计授课量严格小于当前教练的教练数
C_fewer 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 响应模型
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%);满员率防御除零 |