feat: 添加签到模块和数据库表结构

- 新增gym-checkIn签到模块
- 创建sign_in_record签到记录表(V13迁移脚本)
- 添加签到相关路由配置
- 更新应用配置以支持签到模块
- 添加Redis密钥常量
This commit is contained in:
future
2026-06-24 00:46:47 +08:00
committed by fujia
parent a7f31083cf
commit 79eb07599e
8 changed files with 298 additions and 1 deletions
@@ -206,4 +206,8 @@ public class SignInRecord {
public void restore() {
this.isDelete = false;
}
<<<<<<< HEAD
}
=======
}
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
@@ -1,7 +1,12 @@
package cn.novalon.gym.manage.checkIn.handler;
import cn.novalon.gym.manage.checkIn.service.impl.CheckServiceImpl;
<<<<<<< HEAD
import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler;
=======
import cn.novalon.gym.manage.checkIn.vo.SignInRecordVO;
import cn.novalon.gym.manage.checkIn.vo.SignInStatsVO;
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
import cn.novalon.gym.manage.sys.util.AuthUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@@ -12,6 +17,7 @@ import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import reactor.core.publisher.Mono;
import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@@ -82,7 +88,12 @@ public class CheckInHandler {
* @return
*/
public Mono<ServerResponse> getSignInRecords(ServerRequest request) {
<<<<<<< HEAD
Long memberId = authUtil.getMemberIdOrThrow(request);
=======
Long memberId = 1L;
// authUtil.getMemberIdOrThrow(request);
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
String startDateStr = request.queryParam("startDate").orElse(null);
String endDateStr = request.queryParam("endDate").orElse(null);
@@ -128,7 +139,12 @@ public class CheckInHandler {
* @return
*/
public Mono<ServerResponse> getSignInStatistics(ServerRequest request) {
<<<<<<< HEAD
Long memberId = authUtil.getMemberIdOrThrow(request);
=======
Long memberId = 1L;
// authUtil.getMemberIdOrThrow(request);
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
String startDateStr = request.queryParam("startDate").orElse(null);
String endDateStr = request.queryParam("endDate").orElse(null);
@@ -153,7 +169,12 @@ public class CheckInHandler {
* @return
*/
public Mono<ServerResponse> exportSignInRecords(ServerRequest request) {
<<<<<<< HEAD
Long memberId = authUtil.getMemberIdOrThrow(request);
=======
Long memberId = 1L;
// authUtil.getMemberIdOrThrow(request);
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
String startDateStr = request.queryParam("startDate").orElse(null);
String endDateStr = request.queryParam("endDate").orElse(null);
@@ -191,4 +212,8 @@ public class CheckInHandler {
.contentType(MediaType.APPLICATION_JSON)
.bodyValue(Map.of("code", 200, "message", "success", "data", stats)));
}
<<<<<<< HEAD
}
=======
}
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
@@ -78,4 +78,8 @@ public interface ICheckInService {
* @return 签到统计VO
*/
Mono<SignInStatsVO> getDailySignInStats(LocalDate date);
<<<<<<< HEAD
}
=======
}
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
@@ -2,8 +2,11 @@ package cn.novalon.gym.manage.checkIn.service.impl;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.json.JSONUtil;
<<<<<<< HEAD
import cn.hutool.extra.qrcode.QrCodeUtil;
import cn.hutool.extra.qrcode.QrConfig;
=======
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
import cn.novalon.gym.manage.checkIn.config.QRCodeConfig;
import cn.novalon.gym.manage.checkIn.constant.QRRedisKey;
import cn.novalon.gym.manage.checkIn.entity.SignInRecord;
@@ -21,6 +24,11 @@ import cn.novalon.gym.manage.member.entity.MemberCardRecord;
import cn.novalon.gym.manage.member.enums.MemberCardType;
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
<<<<<<< HEAD
=======
import cn.hutool.extra.qrcode.QrCodeUtil;
import cn.hutool.extra.qrcode.QrConfig;
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
@@ -84,6 +92,10 @@ public class CheckServiceImpl implements ICheckInService {
public Mono<String> checkIn(Long memberId, String qrContent) {
String key = RedisKeyConstants.QRCODE_USER_DAILY + memberId + LocalDate.now();
<<<<<<< HEAD
=======
// 先检查当天是否已经签到(从数据库查询,作为重复签到的额外保障)
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
return checkTodayAlreadySignedIn(memberId)
.flatMap(existingRecord -> {
String checkInTime = existingRecord.getSignInTime().format(DATE_FORMATTER);
@@ -125,6 +137,14 @@ public class CheckServiceImpl implements ICheckInService {
});
}
<<<<<<< HEAD
=======
/**
* 检查会员当天是否已经签到
* @param memberId 会员ID
* @return 如果已签到返回签到记录,否则返回空
*/
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
private Mono<SignInRecord> checkTodayAlreadySignedIn(Long memberId) {
LocalDateTime startOfDay = LocalDate.now().atStartOfDay();
LocalDateTime endOfDay = LocalDate.now().atTime(LocalTime.MAX);
@@ -132,9 +152,19 @@ public class CheckServiceImpl implements ICheckInService {
return signInRecordRepository.findByMemberIdAndDate(memberId, startOfDay, endOfDay);
}
<<<<<<< HEAD
private Mono<String> processCheckIn(Long memberId, Long memberCardRecordId, Map<String, Object> redisMap, String qrContent) {
LocalDateTime now = LocalDateTime.now();
=======
/**
* 处理签到逻辑
*/
private Mono<String> processCheckIn(Long memberId, Long memberCardRecordId, Map<String, Object> redisMap, String qrContent) {
LocalDateTime now = LocalDateTime.now();
// 发送实时进度通知
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_CARD", "正在验证会员卡...");
return memberCardRecordRepository.findById(memberCardRecordId)
@@ -153,8 +183,15 @@ public class CheckServiceImpl implements ICheckInService {
return Mono.error(new RuntimeException("会员卡已过期"));
}
<<<<<<< HEAD
MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_BOOKING", "会员卡验证通过,正在检查预约信息...");
=======
// 发送实时进度通知
MyWebSocketHandler.sendProgress(qrContent, "VALIDATE_BOOKING", "会员卡验证通过,正在检查预约信息...");
// 检查是否有需要签到的团课预约
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
return validateBooking(memberId, now)
.then(memberCardRepository.findByMemberCardIdAndDeletedAtIsNull(cardRecord.getMemberCardId())
.switchIfEmpty(Mono.defer(() -> {
@@ -162,6 +199,10 @@ public class CheckServiceImpl implements ICheckInService {
return Mono.error(new RuntimeException("会员卡类型不存在"));
}))
.flatMap(card -> {
<<<<<<< HEAD
=======
// 发送实时进度通知
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
MyWebSocketHandler.sendProgress(qrContent, "DEDUCT_USAGE", "正在扣减会员卡次数...");
return deductCardUsage(cardRecord, card)
@@ -182,6 +223,12 @@ public class CheckServiceImpl implements ICheckInService {
});
}
<<<<<<< HEAD
=======
/**
* 验证预约信息
*/
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
private Mono<Void> validateBooking(Long memberId, LocalDateTime now) {
return groupCourseBookingService.getBookingsByMemberId(memberId)
.filter(booking -> {
@@ -213,6 +260,12 @@ public class CheckServiceImpl implements ICheckInService {
});
}
<<<<<<< HEAD
=======
/**
* 扣减会员卡使用次数/金额
*/
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
private Mono<MemberCardRecord> deductCardUsage(MemberCardRecord record, MemberCard card) {
MemberCardType cardType = MemberCardType.valueOf(card.getMemberCardType());
LocalDateTime now = LocalDateTime.now();
@@ -248,6 +301,12 @@ public class CheckServiceImpl implements ICheckInService {
}
}
<<<<<<< HEAD
=======
/**
* 保存签到记录
*/
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
private Mono<Void> saveSignInRecord(Long memberId, Long memberCardRecordId, Long memberCardId) {
SignInRecord record = SignInRecord.builder()
.memberId(memberId)
@@ -262,6 +321,12 @@ public class CheckServiceImpl implements ICheckInService {
return signInRecordRepository.save(record).then();
}
<<<<<<< HEAD
=======
/**
* 构建成功响应
*/
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
private String buildSuccessResponse(LocalDateTime dateTime) {
Map<String, Object> res = new HashMap<>();
res.put("message", "签到成功");
@@ -269,6 +334,12 @@ public class CheckServiceImpl implements ICheckInService {
return JSONUtil.toJsonStr(res);
}
<<<<<<< HEAD
=======
/**
* 查找会员的有效会员卡(优先选择有效期最早到期的)
*/
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
private Mono<MemberCardRecord> findValidMemberCard(Long memberId) {
return memberCardRecordRepository.findActiveCardsByMemberId(memberId)
.filter(record -> {
@@ -286,6 +357,11 @@ public class CheckServiceImpl implements ICheckInService {
.next();
}
<<<<<<< HEAD
=======
// ==================== 签到记录管理功能 ====================
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
@Override
public Flux<SignInRecordVO> getSignInRecords(Long memberId, LocalDate startTime, LocalDate endTime) {
LocalDateTime start = startTime.atStartOfDay();
@@ -383,6 +459,12 @@ public class CheckServiceImpl implements ICheckInService {
);
}
<<<<<<< HEAD
=======
/**
* 转换实体到VO
*/
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
private SignInRecordVO convertToVO(SignInRecord record) {
SignInRecordVO vo = new SignInRecordVO();
vo.setId(record.getId());
@@ -403,4 +485,4 @@ public class CheckServiceImpl implements ICheckInService {
if (now.isAfter(endOfDay)) return 1;
return ChronoUnit.SECONDS.between(now, endOfDay);
}
}
}
@@ -77,7 +77,11 @@ public class MyWebSocketHandler implements WebSocketHandler {
.doOnError(e -> {
log.error("接收消息出错,sessionId={}", sessionId, e);
})
<<<<<<< HEAD
.subscribe();
=======
.subscribe(); // 必须订阅,否则不会执行
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
// 发送流给客户端
return session.send(sink.asFlux().map(session::textMessage))
@@ -100,6 +104,10 @@ public class MyWebSocketHandler implements WebSocketHandler {
* @return 是否发送成功
*/
public static boolean sendMessageToClient(String qrContent, String message) {
<<<<<<< HEAD
=======
// 先清理超时连接
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
cleanupTimeoutConnections();
Sinks.Many<String> sink = qrContentToSink.get(qrContent);
@@ -129,6 +137,77 @@ public class MyWebSocketHandler implements WebSocketHandler {
String progressMessage = buildMessage("PROGRESS", message);
sendMessageToClient(qrContent, progressMessage);
log.debug("发送进度消息: qrContent={}, step={}, message={}", qrContent, step, message);
<<<<<<< HEAD
=======
}
/**
* 发送签到成功消息
*
* @param qrContent 二维码内容
* @param memberId 会员ID
* @param signInTime 签到时间
*/
public static void sendSuccess(String qrContent, Long memberId, String signInTime) {
String successMessage = buildMessage("SUCCESS", "签到成功!欢迎光临\n会员ID: " + memberId + "\n签到时间: " + signInTime);
sendMessageToClient(qrContent, successMessage);
log.info("发送成功消息: qrContent={}, memberId={}", qrContent, memberId);
}
/**
* 发送签到失败消息
*
* @param qrContent 二维码内容
* @param reason 失败原因
*/
public static void sendFailure(String qrContent, String reason) {
String failureMessage = buildMessage("FAILURE", "签到失败:" + reason);
sendMessageToClient(qrContent, failureMessage);
log.warn("发送失败消息: qrContent={}, reason={}", qrContent, reason);
}
/**
* 构建标准消息格式
*
* @param type 消息类型
* @param content 消息内容
* @return 格式化后的消息字符串
*/
private static String buildMessage(String type, String content) {
return JSONUtil.toJsonStr(Map.of(
"type", type,
"content", content,
"timestamp", System.currentTimeMillis()
));
}
/**
* 清理超时连接
*/
private static void cleanupTimeoutConnections() {
LocalDateTime now = LocalDateTime.now();
qrContentToCreateTime.entrySet().removeIf(entry -> {
LocalDateTime createTime = entry.getValue();
long secondsDiff = java.time.Duration.between(createTime, now).getSeconds();
if (secondsDiff > TIMEOUT_SECONDS) {
String qrContent = entry.getKey();
qrContentToSink.remove(qrContent);
log.debug("清理超时连接: qrContent={}, 超时时间={}秒", qrContent, secondsDiff);
return true;
}
return false;
});
}
/**
* 获取当前连接数
*
* @return 连接数
*/
public static int getConnectionCount() {
cleanupTimeoutConnections();
return qrContentToSink.size();
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
}
/**
@@ -25,6 +25,10 @@ import reactor.test.StepVerifier;
import java.time.LocalDate;
import java.time.LocalDateTime;
<<<<<<< HEAD
=======
import java.time.LocalTime;
>>>>>>> f52451d (feat: 添加签到模块和数据库表结构)
import java.util.HashMap;
import java.util.Map;