新增功能:团课签到前验证是否到店签到
This commit is contained in:
@@ -604,26 +604,26 @@
|
|||||||
| 属性 | 值 |
|
| 属性 | 值 |
|
||||||
|------|-----|
|
|------|-----|
|
||||||
| **HTTP方法** | POST |
|
| **HTTP方法** | POST |
|
||||||
| **接口路径** | `/api/groupCourse/{courseId}/signin` |
|
| **接口路径** | `/api/groupCourse/signin/{memberId}` |
|
||||||
| **所属文件** | `GroupCourseHandler.java` |
|
| **所属文件** | `GroupCourseHandler.java` |
|
||||||
|
|
||||||
**路径参数**:
|
**路径参数**:
|
||||||
|
|
||||||
| 参数名 | 类型 | 必填 | 说明 |
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|--------|------|------|------|
|
|--------|------|------|------|
|
||||||
| courseId | Long | 是 | 团课ID |
|
| memberId | Long | 是 | 会员ID |
|
||||||
|
|
||||||
**请求体**:
|
**请求体**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"memberId": 1001
|
"courseId": 1
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
| 参数名 | 类型 | 必填 | 说明 |
|
| 参数名 | 类型 | 必填 | 说明 |
|
||||||
|--------|------|------|------|
|
|--------|------|------|------|
|
||||||
| memberId | Long | **是** | 会员ID |
|
| courseId | Long | **是** | 团课ID |
|
||||||
|
|
||||||
**成功响应** (200 OK):
|
**成功响应** (200 OK):
|
||||||
|
|
||||||
@@ -638,12 +638,36 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
**失败响应** (400 Bad Request):
|
**失败响应** (400 Bad Request) —— 校验规则及对应错误信息:
|
||||||
|
|
||||||
|
| 序号 | 校验规则 | 错误信息 |
|
||||||
|
|------|----------|----------|
|
||||||
|
| 1 | 团课不存在或已被删除 | `团课不存在或已删除` |
|
||||||
|
| 2 | 团课状态为"已取消" | `团课已取消,无法签到` |
|
||||||
|
| 3 | 团课状态为"已结束" | `团课已结束,无法签到` |
|
||||||
|
| 4 | 当前时间早于开课前2小时 | `未到签到时间,请在开课前2小时内签到` |
|
||||||
|
| 5 | 当前时间晚于团课结束时间 | `团课已结束,无法签到` |
|
||||||
|
| 6 | 用户今日未到店签到 | `请先完成到店签到` |
|
||||||
|
| 7 | 课程当前人数已达上限 | `课程已满员,无法签到` |
|
||||||
|
| 8 | 用户未预约此课程 | `您未预约此课程,无法签到` |
|
||||||
|
| - | 请求体为空 | `请求体不能为空` |
|
||||||
|
| - | 请求体缺少courseId | `courseId不能为空` |
|
||||||
|
|
||||||
|
> **说明**: 校验按上表顺序依次执行,命中第一个失败条件即返回对应错误信息。其中第6项「到店签到」基于 `sign_in_record` 表验证用户今日是否存在 `sign_in_status = 'SUCCESS'` 的记录,与 `gym-dataCount` 模块共用同一数据源。
|
||||||
|
|
||||||
|
**失败响应示例**:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"success": false,
|
"success": false,
|
||||||
"message": "课程已满员"
|
"message": "请先完成到店签到"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"success": false,
|
||||||
|
"message": "您未预约此课程,无法签到"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -1637,7 +1661,14 @@
|
|||||||
### 团课管理
|
### 团课管理
|
||||||
1. **创建团课**:课程名称为必填项
|
1. **创建团课**:课程名称为必填项
|
||||||
2. **取消团课**:需提前24小时通知,否则拒绝操作
|
2. **取消团课**:需提前24小时通知,否则拒绝操作
|
||||||
3. **团课签到**:验证课程状态必须为正常,且未达最大人数限制
|
3. **团课签到**:严格按以下顺序校验,任一不通过即拒绝签到:
|
||||||
|
- 团课是否存在且未被删除
|
||||||
|
- 团课状态不为"已取消"
|
||||||
|
- 团课状态不为"已结束"(含已过结束时间)
|
||||||
|
- 当前时间在开课前2小时内(签到时间窗口)
|
||||||
|
- 用户今日已完成到店签到(基于 `sign_in_record` 表验证)
|
||||||
|
- 课程当前人数未达到最大人数上限
|
||||||
|
- 用户已预约该课程(有效预约)
|
||||||
4. **删除团课**:采用软删除机制,数据保留可恢复
|
4. **删除团课**:采用软删除机制,数据保留可恢复
|
||||||
|
|
||||||
### 团课预约
|
### 团课预约
|
||||||
|
|||||||
+73
-16
@@ -27,11 +27,15 @@ import com.fasterxml.jackson.core.JsonProcessingException;
|
|||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import org.slf4j.Logger;
|
import org.slf4j.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
|
import org.springframework.r2dbc.core.DatabaseClient;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import reactor.core.publisher.Flux;
|
import reactor.core.publisher.Flux;
|
||||||
import reactor.core.publisher.Mono;
|
import reactor.core.publisher.Mono;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Service
|
@Service
|
||||||
public class GroupCourseService implements IGroupCourseService {
|
public class GroupCourseService implements IGroupCourseService {
|
||||||
@@ -46,6 +50,7 @@ public class GroupCourseService implements IGroupCourseService {
|
|||||||
private final RedisUtil redisUtil;
|
private final RedisUtil redisUtil;
|
||||||
private final ObjectMapper objectMapper;
|
private final ObjectMapper objectMapper;
|
||||||
private final GroupCourseStateMachine stateMachine;
|
private final GroupCourseStateMachine stateMachine;
|
||||||
|
private final DatabaseClient databaseClient;
|
||||||
|
|
||||||
private static final String CACHE_KEY_PREFIX = "group_course:page:";
|
private static final String CACHE_KEY_PREFIX = "group_course:page:";
|
||||||
private static final String CACHE_KEY_ID_PREFIX = "group_course:id:";
|
private static final String CACHE_KEY_ID_PREFIX = "group_course:id:";
|
||||||
@@ -62,7 +67,8 @@ public class GroupCourseService implements IGroupCourseService {
|
|||||||
MemberCardRepository memberCardRepository,
|
MemberCardRepository memberCardRepository,
|
||||||
RedisUtil redisUtil,
|
RedisUtil redisUtil,
|
||||||
ObjectMapper objectMapper,
|
ObjectMapper objectMapper,
|
||||||
GroupCourseStateMachine stateMachine){
|
GroupCourseStateMachine stateMachine,
|
||||||
|
DatabaseClient databaseClient){
|
||||||
this.groupCourseRepository = groupCourseRepository;
|
this.groupCourseRepository = groupCourseRepository;
|
||||||
this.bookingRepository = bookingRepository;
|
this.bookingRepository = bookingRepository;
|
||||||
this.groupCourseTypeRepository = groupCourseTypeRepository;
|
this.groupCourseTypeRepository = groupCourseTypeRepository;
|
||||||
@@ -72,6 +78,7 @@ public class GroupCourseService implements IGroupCourseService {
|
|||||||
this.redisUtil = redisUtil;
|
this.redisUtil = redisUtil;
|
||||||
this.objectMapper = objectMapper;
|
this.objectMapper = objectMapper;
|
||||||
this.stateMachine = stateMachine;
|
this.stateMachine = stateMachine;
|
||||||
|
this.databaseClient = databaseClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -441,26 +448,51 @@ public class GroupCourseService implements IGroupCourseService {
|
|||||||
@Override
|
@Override
|
||||||
public Mono<GroupCourse> signIn(Long courseId, Long memberId) {
|
public Mono<GroupCourse> signIn(Long courseId, Long memberId) {
|
||||||
return groupCourseRepository.findByIdAndDeletedAtIsNull(courseId)
|
return groupCourseRepository.findByIdAndDeletedAtIsNull(courseId)
|
||||||
.switchIfEmpty(Mono.error(new RuntimeException("团课不存在")))
|
.switchIfEmpty(Mono.error(new RuntimeException("团课不存在或已删除")))
|
||||||
.flatMap(course -> {
|
.flatMap(course -> {
|
||||||
if (course.getStatus() != 0L) {
|
// 校验1:团课已取消
|
||||||
return Mono.error(new RuntimeException("课程状态不允许签到"));
|
if (course.getStatus().equals(CourseStatus.CANCELLED.getValue())) {
|
||||||
|
return Mono.error(new RuntimeException("团课已取消,无法签到"));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (course.getCurrentMembers() >= course.getMaxMembers()) {
|
// 校验2:团课已结束
|
||||||
return Mono.error(new RuntimeException("课程已满员"));
|
if (course.getStatus().equals(CourseStatus.ENDED.getValue())) {
|
||||||
|
return Mono.error(new RuntimeException("团课已结束,无法签到"));
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查会员是否已预约此课程
|
// 校验3:签到时间校验(开课前2小时内才能签到)
|
||||||
return bookingRepository.findValidBooking(courseId, memberId)
|
LocalDateTime now = LocalDateTime.now();
|
||||||
.switchIfEmpty(Mono.error(new RuntimeException("会员未预约此课程")))
|
LocalDateTime signInStart = course.getStartTime().minusHours(2);
|
||||||
.flatMap(booking -> {
|
if (now.isBefore(signInStart)) {
|
||||||
// 更新课程当前人数
|
return Mono.error(new RuntimeException("未到签到时间,请在开课前2小时内签到"));
|
||||||
return groupCourseRepository.updateCurrentMembers(courseId, 1)
|
}
|
||||||
.flatMap(updatedCourse -> {
|
if (now.isAfter(course.getEndTime())) {
|
||||||
// 更新预约状态为已出席
|
return Mono.error(new RuntimeException("团课已结束,无法签到"));
|
||||||
return bookingRepository.updateStatus(booking.getId(), "2")
|
}
|
||||||
.thenReturn(updatedCourse);
|
|
||||||
|
// 校验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))
|
.flatMap(course -> clearCache().thenReturn(course))
|
||||||
.doOnError(error -> logger.error("团课签到失败 - courseId={}, memberId={}, error: {}", courseId, memberId, error.getMessage()));
|
.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
|
@Override
|
||||||
public Mono<Void> delete(Long id) {
|
public Mono<Void> delete(Long id) {
|
||||||
|
|||||||
Reference in New Issue
Block a user