新增功能:团课签到前验证是否到店签到

This commit is contained in:
2026-06-18 10:34:37 +08:00
parent 7cc9a68144
commit c374a3ccae
2 changed files with 111 additions and 23 deletions
@@ -27,11 +27,15 @@ import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.r2dbc.core.DatabaseClient;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;
@Service
public class GroupCourseService implements IGroupCourseService {
@@ -46,6 +50,7 @@ public class GroupCourseService implements IGroupCourseService {
private final RedisUtil redisUtil;
private final ObjectMapper objectMapper;
private final GroupCourseStateMachine stateMachine;
private final DatabaseClient databaseClient;
private static final String CACHE_KEY_PREFIX = "group_course:page:";
private static final String CACHE_KEY_ID_PREFIX = "group_course:id:";
@@ -62,7 +67,8 @@ public class GroupCourseService implements IGroupCourseService {
MemberCardRepository memberCardRepository,
RedisUtil redisUtil,
ObjectMapper objectMapper,
GroupCourseStateMachine stateMachine){
GroupCourseStateMachine stateMachine,
DatabaseClient databaseClient){
this.groupCourseRepository = groupCourseRepository;
this.bookingRepository = bookingRepository;
this.groupCourseTypeRepository = groupCourseTypeRepository;
@@ -72,6 +78,7 @@ public class GroupCourseService implements IGroupCourseService {
this.redisUtil = redisUtil;
this.objectMapper = objectMapper;
this.stateMachine = stateMachine;
this.databaseClient = databaseClient;
}
@Override
@@ -441,26 +448,51 @@ public class GroupCourseService implements IGroupCourseService {
@Override
public Mono<GroupCourse> signIn(Long courseId, Long memberId) {
return groupCourseRepository.findByIdAndDeletedAtIsNull(courseId)
.switchIfEmpty(Mono.error(new RuntimeException("团课不存在")))
.switchIfEmpty(Mono.error(new RuntimeException("团课不存在或已删除")))
.flatMap(course -> {
if (course.getStatus() != 0L) {
return Mono.error(new RuntimeException("课程状态不允许签到"));
// 校验1:团课已取消
if (course.getStatus().equals(CourseStatus.CANCELLED.getValue())) {
return Mono.error(new RuntimeException("团课已取消,无法签到"));
}
if (course.getCurrentMembers() >= course.getMaxMembers()) {
return Mono.error(new RuntimeException("课程已满员"));
// 校验2:团课已结束
if (course.getStatus().equals(CourseStatus.ENDED.getValue())) {
return Mono.error(new RuntimeException("团课已结束,无法签到"));
}
// 检查会员是否已预约此课程
return bookingRepository.findValidBooking(courseId, memberId)
.switchIfEmpty(Mono.error(new RuntimeException("会员未预约此课程")))
.flatMap(booking -> {
// 更新课程当前人数
return groupCourseRepository.updateCurrentMembers(courseId, 1)
.flatMap(updatedCourse -> {
// 更新预约状态为已出席
return bookingRepository.updateStatus(booking.getId(), "2")
.thenReturn(updatedCourse);
// 校验3:签到时间校验(开课前2小时内才能签到)
LocalDateTime now = LocalDateTime.now();
LocalDateTime signInStart = course.getStartTime().minusHours(2);
if (now.isBefore(signInStart)) {
return Mono.error(new RuntimeException("未到签到时间,请在开课前2小时内签到"));
}
if (now.isAfter(course.getEndTime())) {
return Mono.error(new RuntimeException("团课已结束,无法签到"));
}
// 校验4:用户是否已到店签到(今日有成功签到记录)
return checkStoreCheckIn(memberId)
.flatMap(storeCheckedIn -> {
if (!storeCheckedIn) {
return Mono.error(new RuntimeException("请先完成到店签到"));
}
// 校验5:课程已满员
if (course.getCurrentMembers() >= course.getMaxMembers()) {
return Mono.error(new RuntimeException("课程已满员,无法签到"));
}
// 校验6:用户已预约此课程(有效预约,状态为0-已预约)
return bookingRepository.findValidBooking(courseId, memberId)
.switchIfEmpty(Mono.error(new RuntimeException("您未预约此课程,无法签到")))
.flatMap(booking -> {
// 更新课程当前人数
return groupCourseRepository.updateCurrentMembers(courseId, 1)
.flatMap(updatedCourse -> {
// 更新预约状态为已出席
return bookingRepository.updateStatus(booking.getId(), "2")
.thenReturn(updatedCourse);
});
});
});
})
@@ -468,6 +500,31 @@ public class GroupCourseService implements IGroupCourseService {
.flatMap(course -> clearCache().thenReturn(course))
.doOnError(error -> logger.error("团课签到失败 - courseId={}, memberId={}, error: {}", courseId, memberId, error.getMessage()));
}
/**
* 检查用户今日是否有成功到店签到记录
* 基于 sign_in_record 表查询,与 gym-dataCount 模块使用相同数据源
*/
private Mono<Boolean> checkStoreCheckIn(Long memberId) {
LocalDate today = LocalDate.now();
LocalDateTime startOfDay = today.atStartOfDay();
LocalDateTime endOfDay = today.plusDays(1).atStartOfDay();
return databaseClient.sql(
"SELECT COUNT(*) FROM sign_in_record " +
"WHERE member_id = :memberId " +
"AND sign_in_time >= :startTime " +
"AND sign_in_time < :endTime " +
"AND sign_in_status = 'SUCCESS' " +
"AND is_delete = false")
.bind("memberId", memberId)
.bind("startTime", startOfDay)
.bind("endTime", endOfDay)
.map(row -> row.get(0, Long.class))
.one()
.map(count -> count > 0)
.defaultIfEmpty(false);
}
@Override
public Mono<Void> delete(Long id) {