新增到课签到时间窗口与迟到签到时间窗口配置,优化教练评分机制(未测试)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
# ADR-0001: 教练业绩统计功能设计
|
||||
|
||||
**日期**: 2026-07-22
|
||||
**日期**: 2026-07-22(初版)/ 2026-07-26(修订)
|
||||
**状态**: 已决定
|
||||
**决策者**: 通过 grill-with-docs 追问明确
|
||||
|
||||
@@ -19,16 +19,18 @@
|
||||
**选择**: 在 `gym-dataCount` 模块中新增 CoachPerformance 相关的 Handler + Service + DAO,而非新建独立模块。
|
||||
|
||||
**理由**:
|
||||
- `gym-dataCount` 模块已有成熟的统计架构(DatabaseClient + Reactive + Redis 缓存 + 时间范围推导)
|
||||
- `gym-dataCount` 模块已有成熟的统计架构(DatabaseClient + Reactive + 时间范围推导)
|
||||
- 现有 `DataStatisticsDao` 已有教练相关的 SQL 聚合查询,可直接复用
|
||||
- 避免模块膨胀,将"统计"职责收敛在一个模块中
|
||||
- `manage-app` 已依赖 `gym-dataCount`,路由注册零成本
|
||||
|
||||
**替代方案被拒绝**: 新建 `gym-coach-performance` 独立模块。理由:功能规模不足以支撑独立模块,且会引入额外的模块间依赖管理成本。
|
||||
|
||||
---
|
||||
|
||||
### 2. 数据源:完全基于团课预约数据
|
||||
|
||||
**选择**: 业绩统计的"出席人次"和"出勤率"完全基于 `group_course_booking` 表(status='2'=已出席),而非 `sign_in_record` 签到表。
|
||||
**选择**: 业绩统计的"出席人次"和"出勤率"完全基于 `group_course_booking` 表,而非 `sign_in_record` 签到表。
|
||||
|
||||
**理由**:
|
||||
- `sign_in_record` 表中没有 `coach_id` 字段,签到只关联会员(member_id),不关联教练
|
||||
@@ -37,6 +39,8 @@
|
||||
|
||||
**风险**: 如果未来签到记录需要关联教练(例如一对一的私教签到),需要重新评估此决策。
|
||||
|
||||
---
|
||||
|
||||
### 3. 授课量定义:仅计入已完成课程
|
||||
|
||||
**选择**: 只统计 `status IN (2, 6)` 的课程(已结束 + 自动结束)。
|
||||
@@ -45,23 +49,75 @@
|
||||
- 所有非取消课程:会包含教练缺席(status=5)的课程,不应算作业绩
|
||||
- 所有排课:会包含已取消的课程,不能反映真实工作量
|
||||
|
||||
### 4. 满员率:按出席人数计算
|
||||
---
|
||||
|
||||
**选择**: 满员率 = 各课程(出席人数 / max_members)的平均值。
|
||||
### 4. 时间基准:以课程结束时间为准
|
||||
|
||||
**选择**: 所有时间范围过滤均使用 `group_course.end_time`,而非 `start_time`。
|
||||
|
||||
**理由**: 课程可能跨统计周期边界(如月末 23:00 开课、次月 01:00 结束)。以开始时间为准会导致跨月课程被错误归因到上月。以结束时间为准更符合"这个月完成了哪些课程"的直观理解。
|
||||
|
||||
**变更历史**(2026-07-26): 从 `start_time` 改为 `end_time`。
|
||||
|
||||
---
|
||||
|
||||
### 5. 出席人次口径:参与型状态
|
||||
|
||||
**选择**: 出席人次统计 `booking.status IN ('2', '4', '5')`,即已出席(2) + 教练缺席(4) + 迟到(5)。
|
||||
|
||||
**拒绝的定义**: 仅统计 status='2'(已出席)。理由:教练缺席和迟到同样意味着学员到达了现场(或至少尝试了参与),应计入出席人次;实际缺席责任在教练而非学员。
|
||||
|
||||
**变更历史**(2026-07-26): 从仅 `status='2'` 扩展为 `IN ('2','4','5')`。
|
||||
|
||||
---
|
||||
|
||||
### 6. 出勤率分母:仅已预约
|
||||
|
||||
**选择**: 出勤率分母仅统计 `booking.status = '0'`(已预约),而非 `status != '1'`(所有非取消)。
|
||||
|
||||
**理由**:
|
||||
- status='3'(学员缺席)不应出现在分母中——学员预约后无故缺席,既不应计入分子也不应计入分母,因为这既非教练的功劳也非教练的责任
|
||||
- 出勤率语义变为"在已预约的学员中,实际参与的比例"
|
||||
- 排除了预约后取消(status='1')和学员缺席(status='3')的噪声
|
||||
|
||||
**变更历史**(2026-07-26): 从 `status != '1'` 改为 `status = '0'`。
|
||||
|
||||
---
|
||||
|
||||
### 7. 满员率:按出席人数计算 + 防御除零
|
||||
|
||||
**选择**: 满员率 = 各已完成课程(出席人数 / max_members)的平均值,其中出席人数按 status IN ('2','4','5') 统计。`max_members = 0` 的课程被跳过不参与计算。
|
||||
|
||||
**拒绝的定义**: 按预约人数(current_members)计算。理由:预约了但没来的学员不能算"满员",出席人数更真实地反映了课程实际到场情况。
|
||||
|
||||
### 5. 综合评分权重:授课量 40% + 出勤率 30% + 满员率 30%
|
||||
**变更历史**(2026-07-26): 满员率明细的出席人数口径从 `status='2'` 扩展为 `IN ('2','4','5')`,与出席人次保持一致。
|
||||
|
||||
**选择**: 授课量占比最高,体现工作量;出勤率和满员率体现教学质量。
|
||||
---
|
||||
|
||||
**归一化规则**: 授课量按所有教练中最大值归一化到 0-100。这样即使只有少数教练开课多,评分也能合理分布。
|
||||
### 8. 综合评分
|
||||
|
||||
**拒绝的替代方案**:
|
||||
- 三指标等权重(33/33/34):弱化了工作量差异
|
||||
- 授课量 50%:过度强调数量而忽视质量
|
||||
**最终选择**(2026-07-26 修订):
|
||||
|
||||
### 6. 不包含学员留存率
|
||||
| 指标 | 权重 | 归一化方式 |
|
||||
|------|------|-----------|
|
||||
| 授课量 | 35% | 百分位排名(授课量排序,小于当前教练的教练数 / (总教练数-1) * 100) |
|
||||
| 出勤率 | 25% | 原始百分比(0-100) |
|
||||
| 满员率 | 25% | 原始百分比(0-100) |
|
||||
| 违规扣分 | 15% | 线性扣分:max(0, 100 - 违规次数 * 20) |
|
||||
|
||||
```
|
||||
综合评分 = 授课量归一化分 * 0.35 + 出勤率 * 0.25 + 满员率 * 0.25 + 违规分 * 0.15
|
||||
```
|
||||
|
||||
**公式变更历史**:
|
||||
- 初版(2026-07-22): `授课量归一化(最大值归一化) * 0.4 + 出勤率 * 0.3 + 满员率 * 0.3`,违规仅展示不参与评分
|
||||
- 修订(2026-07-26): 授课量归一化改为百分位排名,违规纳入评分,权重重新分配
|
||||
|
||||
**拒绝的替代方案**: 详见设计文档 `docs/coach-performance-design.md`。
|
||||
|
||||
---
|
||||
|
||||
### 9. 不包含学员留存率
|
||||
|
||||
**选择**: 首版不计算学员留存率。
|
||||
|
||||
@@ -69,11 +125,30 @@
|
||||
|
||||
---
|
||||
|
||||
### 10. 不引入 Redis 缓存
|
||||
|
||||
**选择**: 教练业绩统计数据不进行 Redis 缓存,每次请求实时计算。
|
||||
|
||||
**理由**: 业绩数据需要准实时性,缓存可能导致教练查看时数据滞后;且当前教练数量级下,6 条聚合查询的响应时间可接受。
|
||||
|
||||
---
|
||||
|
||||
### 11. getCoachPerformanceById 复用全量查询
|
||||
|
||||
**选择**: 查询单个教练业绩时,内部调用 `getCoachPerformanceList` 获取全量后过滤。暂不新增按教练 ID 的单独 DAO 方法。
|
||||
|
||||
**理由**: 当前教练数量有限,全量查询后再过滤的性能损耗可接受,优先保持代码简洁。
|
||||
|
||||
**风险**: 教练数量增长后需要重新评估,届时可新增按 coach_id 直查的 DAO 方法。
|
||||
|
||||
---
|
||||
|
||||
## 影响
|
||||
|
||||
### 后端变更
|
||||
- `gym-dataCount` 模块新增:`CoachPerformance` domain、`CoachPerformanceHandler`、`CoachPerformanceDao`
|
||||
- `manage-app` 的 `SystemRouter` 中新增 2 条路由
|
||||
- `gym-dataCount` 模块新增:`CoachPerformance` domain、`CoachPerformanceHandler`、`DataStatisticsDao`(教练业绩相关方法)
|
||||
- `manage-app` 的 `SystemRouter` 中新增 3 条路由
|
||||
- `DataStatisticsServiceImpl` 新增 `getCoachPerformanceList`、`getCoachPerformanceById`、`calculateFillRate` 方法
|
||||
|
||||
### 前端变更
|
||||
- `StatisticsDashboard.vue` 新增"教练业绩"Tab
|
||||
@@ -96,3 +171,28 @@
|
||||
新建 `gym-coach-performance` 独立 Maven 模块。
|
||||
- 优点:职责隔离清晰
|
||||
- 缺点:模块碎片化,增加编译和依赖管理成本
|
||||
|
||||
### 方案 C:授课量最大值归一化(已拒绝,初版方案)
|
||||
`normalizedCourses = courses / maxCourses * 100`
|
||||
- 优点:数学简洁
|
||||
- 缺点:若有一位教练授课量远超其他,中游教练得分被严重压缩;鼓励"互卷"而非"达标"
|
||||
|
||||
### 方案 D:授课量对数归一化(已拒绝)
|
||||
`normalizedCourses = ln(courses + 1) / ln(maxCourses + 1) * 100`
|
||||
- 优点:自然压制极端值
|
||||
- 缺点:解释性弱,非技术人员难以理解评分含义
|
||||
|
||||
### 方案 E:授课量固定目标归一化(已拒绝)
|
||||
`normalizedCourses = min(courses / target * 100, 100)`,target 可配置
|
||||
- 优点:变成"达标制",不受其他教练影响
|
||||
- 缺点:target 值需要根据实际数据校准,设置不当会全员满分或全员不及格
|
||||
|
||||
### 方案 F:违规阶梯扣分(已拒绝)
|
||||
0次=100, 1次=70, 2次=40, 3次=10, >=4次=0
|
||||
- 优点:首次违规惩罚重,有威慑力
|
||||
- 缺点:阶梯粒度太粗,第 1 次和第 2 次违规之间差距 30 分,过于激进
|
||||
|
||||
### 方案 G:违规归一化扣分(已拒绝)
|
||||
`violationScore = (1 - violations / maxViolations) * 100`
|
||||
- 优点:相对于最差教练扣分
|
||||
- 缺点:依赖数据集中的最大值,若所有教练都无违规则无意义
|
||||
|
||||
Reference in New Issue
Block a user