# 教练业绩统计设计文档 **版本**: 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 2. Flux: countCompletedCoursesByCoach() ─→ Map ┐ 3. Flux: countAttendedStudentsByCoach() ─→ Map │ 4. Flux: countTotalBookingsByCoach() ─→ Map ├─ Mono.zip 5. Flux: getFillRateDetailByCoach() ─→ Map> │ 6. Flux: countViolationsByCoach() ─→ Map ┘ │ flatMapMany: 逐教练计算指标 │ sorted: 按综合评分降序 │ Flux ``` --- ## 七、边界情况处理 | 场景 | 行为 | |------|------| | 教练无任何已完成课程 | 所有指标为 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%);满员率防御除零 |