新增到课签到时间窗口与迟到签到时间窗口配置,优化教练评分机制(未测试)
This commit is contained in:
@@ -37,6 +37,11 @@
|
||||
<artifactId>gym-groupCourse</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>cn.novalon.gym.manage</groupId>
|
||||
<artifactId>gym-coach-config</artifactId>
|
||||
<version>${project.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
|
||||
+25
-24
@@ -1,6 +1,7 @@
|
||||
package cn.novalon.gym.manage.coach.scheduler;
|
||||
|
||||
import cn.novalon.gym.manage.coach.enums.ViolationReason;
|
||||
import cn.novalon.gym.manage.coachconfig.service.CoachTimeRuleService;
|
||||
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseBookingDao;
|
||||
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseDao;
|
||||
@@ -30,22 +31,23 @@ import java.time.LocalDateTime;
|
||||
public class CoachCourseScheduler {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CoachCourseScheduler.class);
|
||||
private static final long ONE_HOUR_MINUTES = 60;
|
||||
private static final long END_GRACE_MINUTES = 10;
|
||||
|
||||
private final GroupCourseDao groupCourseDao;
|
||||
private final GroupCourseBookingDao groupCourseBookingDao;
|
||||
private final DatabaseClient databaseClient;
|
||||
private final RedisUtil redisUtil;
|
||||
private final CoachTimeRuleService timeRuleService;
|
||||
|
||||
public CoachCourseScheduler(GroupCourseDao groupCourseDao,
|
||||
GroupCourseBookingDao groupCourseBookingDao,
|
||||
DatabaseClient databaseClient,
|
||||
RedisUtil redisUtil) {
|
||||
RedisUtil redisUtil,
|
||||
CoachTimeRuleService timeRuleService) {
|
||||
this.groupCourseDao = groupCourseDao;
|
||||
this.groupCourseBookingDao = groupCourseBookingDao;
|
||||
this.databaseClient = databaseClient;
|
||||
this.redisUtil = redisUtil;
|
||||
this.timeRuleService = timeRuleService;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -89,39 +91,38 @@ public class CoachCourseScheduler {
|
||||
*/
|
||||
private Mono<Long> processAbsentCourses(LocalDateTime now) {
|
||||
return groupCourseDao.findByStatusAndStartTimeBefore(databaseClient, "0", now)
|
||||
.filter(course -> isAbsentThresholdExceeded(course, now))
|
||||
.flatMap(course -> markAsCoachAbsent(course, now))
|
||||
.flatMap(course -> {
|
||||
long courseDurationMinutes = Duration.between(course.getStartTime(), course.getEndTime()).toMinutes();
|
||||
return timeRuleService.matchRule(courseDurationMinutes)
|
||||
.filter(rule -> {
|
||||
long minutesSinceStart = Duration.between(course.getStartTime(), now).toMinutes();
|
||||
return minutesSinceStart > rule.getLateWindow();
|
||||
})
|
||||
.flatMap(rule -> markAsCoachAbsent(course, now));
|
||||
})
|
||||
.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理自动结课:status IN ('3','7') 且 end_time + 10分钟 已过
|
||||
* 处理自动结课:status IN ('3','7') 且 end_time + endGrace 已过
|
||||
*/
|
||||
private Mono<Long> processAutoEndCourses(LocalDateTime now) {
|
||||
LocalDateTime endThreshold = now.minusMinutes(END_GRACE_MINUTES);
|
||||
return groupCourseDao.findByStatusInAndEndTimeBefore(databaseClient,
|
||||
new String[]{String.valueOf(CourseStatus.IN_PROGRESS.getValue()),
|
||||
String.valueOf(CourseStatus.COACH_LATE.getValue())},
|
||||
endThreshold)
|
||||
.flatMap(course -> markAsAutoEnded(course, now))
|
||||
now)
|
||||
.flatMap(course -> {
|
||||
long courseDurationMinutes = Duration.between(course.getStartTime(), course.getEndTime()).toMinutes();
|
||||
return timeRuleService.matchRule(courseDurationMinutes)
|
||||
.filter(rule -> {
|
||||
long minutesAfterEnd = Duration.between(course.getEndTime(), now).toMinutes();
|
||||
return minutesAfterEnd > rule.getEndGrace();
|
||||
})
|
||||
.flatMap(rule -> markAsAutoEnded(course, now));
|
||||
})
|
||||
.count();
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断课程是否已过缺席阈值
|
||||
*/
|
||||
private boolean isAbsentThresholdExceeded(GroupCourseEntity course, LocalDateTime now) {
|
||||
long courseDurationMinutes = Duration.between(course.getStartTime(), course.getEndTime()).toMinutes();
|
||||
long minutesSinceStart = Duration.between(course.getStartTime(), now).toMinutes();
|
||||
|
||||
if (courseDurationMinutes >= ONE_HOUR_MINUTES) {
|
||||
return minutesSinceStart > 30;
|
||||
} else {
|
||||
long thresholdB = Math.max(1, (long) (courseDurationMinutes * 0.25));
|
||||
return minutesSinceStart > thresholdB;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 标记课程为教练缺席(5),更新预约记录为教练缺席(4),记录违规
|
||||
*/
|
||||
|
||||
+33
-48
@@ -3,6 +3,8 @@ package cn.novalon.gym.manage.coach.service;
|
||||
import cn.novalon.gym.manage.coach.dao.CoachViolationDao;
|
||||
import cn.novalon.gym.manage.coach.entity.CoachViolationEntity;
|
||||
import cn.novalon.gym.manage.coach.enums.ViolationReason;
|
||||
import cn.novalon.gym.manage.coachconfig.domain.CoachTimeRule;
|
||||
import cn.novalon.gym.manage.coachconfig.service.CoachTimeRuleService;
|
||||
import cn.novalon.gym.manage.common.util.RedisUtil;
|
||||
import cn.novalon.gym.manage.common.util.StatusConstants;
|
||||
import cn.novalon.gym.manage.groupcourse.dao.GroupCourseBookingDao;
|
||||
@@ -43,7 +45,6 @@ public class CoachCourseService {
|
||||
|
||||
private static final Logger logger = LoggerFactory.getLogger(CoachCourseService.class);
|
||||
private static final String COACH_ROLE_NAME = "教练";
|
||||
private static final long ONE_HOUR_MINUTES = 60;
|
||||
|
||||
private final ISysUserRepository userRepository;
|
||||
private final ISysRoleRepository roleRepository;
|
||||
@@ -56,6 +57,7 @@ public class CoachCourseService {
|
||||
private final DatabaseClient databaseClient;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
private final RedisUtil redisUtil;
|
||||
private final CoachTimeRuleService timeRuleService;
|
||||
|
||||
public CoachCourseService(ISysUserRepository userRepository,
|
||||
ISysRoleRepository roleRepository,
|
||||
@@ -67,7 +69,8 @@ public class CoachCourseService {
|
||||
CoachViolationDao violationDao,
|
||||
DatabaseClient databaseClient,
|
||||
PasswordEncoder passwordEncoder,
|
||||
RedisUtil redisUtil) {
|
||||
RedisUtil redisUtil,
|
||||
CoachTimeRuleService timeRuleService) {
|
||||
this.userRepository = userRepository;
|
||||
this.roleRepository = roleRepository;
|
||||
this.userRoleRepository = userRoleRepository;
|
||||
@@ -79,6 +82,7 @@ public class CoachCourseService {
|
||||
this.databaseClient = databaseClient;
|
||||
this.passwordEncoder = passwordEncoder;
|
||||
this.redisUtil = redisUtil;
|
||||
this.timeRuleService = timeRuleService;
|
||||
}
|
||||
|
||||
// ==================== 教练管理(从原 CoachService 迁移) ====================
|
||||
@@ -176,9 +180,7 @@ public class CoachCourseService {
|
||||
|
||||
/**
|
||||
* 教练手动开课
|
||||
* 判定逻辑:
|
||||
* - 长课时(>=1h): 10分钟内正常,10~30分钟迟到,>30分钟拒绝
|
||||
* - 短课时(<1h): 10%时长内正常,10%~25%迟到,>25%拒绝
|
||||
* 通过 CoachTimeRuleService 匹配规则获取时间阈值,替代原硬编码逻辑
|
||||
*/
|
||||
public Mono<GroupCourseEntity> startCourse(Long courseId, Long coachId) {
|
||||
return groupCourseDao.findByIdIsAndDeletedAtIsNull(courseId)
|
||||
@@ -196,53 +198,29 @@ public class CoachCourseService {
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long courseDurationMinutes = Duration.between(course.getStartTime(), course.getEndTime()).toMinutes();
|
||||
|
||||
if (courseDurationMinutes >= ONE_HOUR_MINUTES) {
|
||||
return handleLongCourseStart(course, now);
|
||||
} else {
|
||||
return handleShortCourseStart(course, now, courseDurationMinutes);
|
||||
}
|
||||
return timeRuleService.matchRule(courseDurationMinutes)
|
||||
.flatMap(rule -> doStartCourseWithRule(course, now, rule));
|
||||
});
|
||||
}
|
||||
|
||||
private Mono<GroupCourseEntity> handleLongCourseStart(GroupCourseEntity course, LocalDateTime now) {
|
||||
private Mono<GroupCourseEntity> doStartCourseWithRule(GroupCourseEntity course, LocalDateTime now,
|
||||
CoachTimeRule rule) {
|
||||
long minutesSinceStart = Duration.between(course.getStartTime(), now).toMinutes();
|
||||
|
||||
if (minutesSinceStart < 0) {
|
||||
return Mono.error(new RuntimeException("课程尚未到开课时间"));
|
||||
}
|
||||
if (minutesSinceStart <= 10) {
|
||||
if (minutesSinceStart <= rule.getNormalWindow()) {
|
||||
// 正常开课
|
||||
return doStartCourse(course, now, CourseStatus.IN_PROGRESS, null);
|
||||
}
|
||||
if (minutesSinceStart <= 30) {
|
||||
if (minutesSinceStart <= rule.getLateWindow()) {
|
||||
// 教练迟到
|
||||
return recordViolation(course.getCoachId(), course.getId(), now, ViolationReason.COACH_LATE)
|
||||
.then(doStartCourse(course, now, CourseStatus.COACH_LATE, ViolationReason.COACH_LATE));
|
||||
}
|
||||
// >30分钟,拒绝(调度器应已标记为缺席)
|
||||
return Mono.error(new RuntimeException("已超过开课时间30分钟,无法开课"));
|
||||
}
|
||||
|
||||
private Mono<GroupCourseEntity> handleShortCourseStart(GroupCourseEntity course, LocalDateTime now,
|
||||
long courseDurationMinutes) {
|
||||
long minutesSinceStart = Duration.between(course.getStartTime(), now).toMinutes();
|
||||
long thresholdA = Math.max(1, (long) (courseDurationMinutes * 0.10));
|
||||
long thresholdB = Math.max(1, (long) (courseDurationMinutes * 0.25));
|
||||
|
||||
if (minutesSinceStart < 0) {
|
||||
return Mono.error(new RuntimeException("课程尚未到开课时间"));
|
||||
}
|
||||
if (minutesSinceStart <= thresholdA) {
|
||||
// 正常开课
|
||||
return doStartCourse(course, now, CourseStatus.IN_PROGRESS, null);
|
||||
}
|
||||
if (minutesSinceStart <= thresholdB) {
|
||||
// 教练迟到
|
||||
return recordViolation(course.getCoachId(), course.getId(), now, ViolationReason.COACH_LATE)
|
||||
.then(doStartCourse(course, now, CourseStatus.COACH_LATE, ViolationReason.COACH_LATE));
|
||||
}
|
||||
// >thresholdB,拒绝
|
||||
return Mono.error(new RuntimeException("已超过开课时间,无法开课"));
|
||||
// 超过 lateWindow,拒绝
|
||||
return Mono.error(new RuntimeException("已超过开课时间" + rule.getLateWindow() + "分钟,无法开课"));
|
||||
}
|
||||
|
||||
private Mono<GroupCourseEntity> doStartCourse(GroupCourseEntity course, LocalDateTime now,
|
||||
@@ -260,7 +238,7 @@ public class CoachCourseService {
|
||||
/**
|
||||
* 教练手动结课
|
||||
* 可在 IN_PROGRESS(3) 或 COACH_LATE(7) 状态下结课
|
||||
* 必须在标注结课时间 + 10分钟内
|
||||
* 必须在标注结课时间 + endGrace 分钟内
|
||||
*/
|
||||
public Mono<GroupCourseEntity> endCourse(Long courseId, Long coachId) {
|
||||
return groupCourseDao.findByIdIsAndDeletedAtIsNull(courseId)
|
||||
@@ -278,18 +256,25 @@ public class CoachCourseService {
|
||||
}
|
||||
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
long minutesAfterEnd = Duration.between(course.getEndTime(), now).toMinutes();
|
||||
long courseDurationMinutes = Duration.between(course.getStartTime(), course.getEndTime()).toMinutes();
|
||||
|
||||
if (minutesAfterEnd > 10) {
|
||||
return Mono.error(new RuntimeException("已超过结课时间10分钟,请等待系统自动结课"));
|
||||
}
|
||||
return timeRuleService.matchRule(courseDurationMinutes)
|
||||
.flatMap(rule -> {
|
||||
long minutesAfterEnd = Duration.between(course.getEndTime(), now).toMinutes();
|
||||
|
||||
course.setStatus(CourseStatus.ENDED.getValue());
|
||||
course.setActualEndTime(now);
|
||||
course.setUpdatedAt(now);
|
||||
return groupCourseDao.updateEndInfo(course.getId(), String.valueOf(CourseStatus.ENDED.getValue()), now, now)
|
||||
.then(groupCourseDao.findByIdIsAndDeletedAtIsNull(course.getId()))
|
||||
.flatMap(entity -> invalidateStatisticsCache().thenReturn(entity));
|
||||
if (minutesAfterEnd > rule.getEndGrace()) {
|
||||
return Mono.error(new RuntimeException(
|
||||
"已超过结课时间" + rule.getEndGrace() + "分钟,请等待系统自动结课"));
|
||||
}
|
||||
|
||||
course.setStatus(CourseStatus.ENDED.getValue());
|
||||
course.setActualEndTime(now);
|
||||
course.setUpdatedAt(now);
|
||||
return groupCourseDao.updateEndInfo(course.getId(),
|
||||
String.valueOf(CourseStatus.ENDED.getValue()), now, now)
|
||||
.then(groupCourseDao.findByIdIsAndDeletedAtIsNull(course.getId()))
|
||||
.flatMap(entity -> invalidateStatisticsCache().thenReturn(entity));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user