diff --git a/gym-manage-api/gym-checkIn/.gitignore b/gym-manage-api/gym-checkIn/.gitignore new file mode 100644 index 0000000..8220137 --- /dev/null +++ b/gym-manage-api/gym-checkIn/.gitignore @@ -0,0 +1,48 @@ +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# Virtual machine crash logs +hs_err_pid* +replay_pid* + +# Maven +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +.mvn/wrapper/maven-wrapper.jar + +# IDE +.idea/ +*.iml +.vscode/ +.settings/ +.classpath +.project + +# OS +.DS_Store +Thumbs.db diff --git a/gym-manage-api/gym-checkIn/pom.xml b/gym-manage-api/gym-checkIn/pom.xml new file mode 100644 index 0000000..8e29b7e --- /dev/null +++ b/gym-manage-api/gym-checkIn/pom.xml @@ -0,0 +1,242 @@ + + + 4.0.0 + + + cn.novalon.gym.manage + gym-manage-api + 1.0.0 + + + gym-checkIn + jar + + Gym CheckIn + Check-In Management Module - Member Attendance Services + + + + cn.novalon.gym.manage + manage-common + ${project.version} + + + cn.novalon.gym.manage + manage-db + ${project.version} + + + org.springframework.boot + spring-boot-starter-webflux + + + org.springframework.boot + spring-boot-starter-aop + + + org.springdoc + springdoc-openapi-starter-webflux-ui + + + org.springframework.boot + spring-boot-starter-security + + + org.springframework.data + spring-data-commons + + + org.springframework.boot + spring-boot-starter-test + test + + + org.springframework.security + spring-security-test + test + + + io.projectreactor + reactor-test + test + + + io.github.resilience4j + resilience4j-spring-boot3 + + + io.github.resilience4j + resilience4j-reactor + + + org.testcontainers + testcontainers + 1.21.4 + test + + + org.testcontainers + postgresql + 1.21.4 + test + + + org.testcontainers + junit-jupiter + 1.21.4 + test + + + com.h2database + h2 + test + + + io.r2dbc + r2dbc-h2 + test + + + org.postgresql + r2dbc-postgresql + test + + + cn.hutool + hutool-all + 5.8.25 + + + com.google.zxing + core + 3.5.1 + + + + + com.google.zxing + javase + 3.5.1 + + + + org.springframework.boot + spring-boot-starter-websocket + + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.4.2 + + + default-jar + package + + jar + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + 21 + 21 + + + org.mapstruct + mapstruct-processor + 1.5.5.Final + + + org.projectlombok + lombok + ${lombok.version} + + + org.projectlombok + lombok-mapstruct-binding + 0.2.0 + + + + + + org.jacoco + jacoco-maven-plugin + 0.8.12 + + + prepare-agent + + prepare-agent + + + + report + verify + + report + + + + check + verify + + check + + + + + BUNDLE + + + INSTRUCTION + COVEREDRATIO + 0.60 + + + + + + + + + + com.github.spotbugs + spotbugs-maven-plugin + 4.8.6.0 + + + com.github.spotbugs + spotbugs + 4.8.6 + + + + + spotbugs-check + verify + + check + + + + + Max + High + true + spotbugs-exclude.xml + + + + + diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/config/QRCodeConfig.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/config/QRCodeConfig.java new file mode 100644 index 0000000..dd44c33 --- /dev/null +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/config/QRCodeConfig.java @@ -0,0 +1,46 @@ +package cn.novalon.gym.manage.checkIn.config; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +@Data +@Configuration +@ConfigurationProperties(prefix = "qr.config") +public class QRCodeConfig { + + /** + * 二维码宽度(像素) + */ + private Integer width = 300; + + /** + * 二维码高度(像素) + */ + private Integer height = 300; + + /** + * 白边宽度 + */ + private Integer margin = 1; + + /** + * 容错率:L, M, Q, H + */ + private String errorCorrection = "M"; + + /** + * 图片格式:png, jpg + */ + private String format = "png"; + + /** + * 是否启用Logo + */ + private Boolean logoEnabled = false; + + /** + * Logo路径 + */ + private String logoPath = ""; +} \ No newline at end of file diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/config/WebSocketConfig.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/config/WebSocketConfig.java new file mode 100644 index 0000000..e4d6ea1 --- /dev/null +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/config/WebSocketConfig.java @@ -0,0 +1,43 @@ +package cn.novalon.gym.manage.checkIn.config; + +import cn.novalon.gym.manage.checkIn.websocket.MyWebSocketHandler; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.HandlerMapping; +import org.springframework.web.reactive.handler.SimpleUrlHandlerMapping; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.server.support.WebSocketHandlerAdapter; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class WebSocketConfig { + + @Autowired + private MyWebSocketHandler myWebSocketHandler; + + /** + * 注册 WebSocket 路由映射 + * 路径对应前端连接的 ws://xxx/webSocket/checkIn + */ + @Bean + public HandlerMapping webSocketMapping() { + Map map = new HashMap<>(); + map.put("/webSocket/checkIn", myWebSocketHandler); + + SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); + mapping.setUrlMap(map); + mapping.setOrder(10); // 设置优先级 + return mapping; + } + + /** + * 注册 WebSocket 处理器适配器(必须) + */ + @Bean + public WebSocketHandlerAdapter handlerAdapter() { + return new WebSocketHandlerAdapter(); + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/constant/QRRedisKey.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/constant/QRRedisKey.java new file mode 100644 index 0000000..8baa890 --- /dev/null +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/constant/QRRedisKey.java @@ -0,0 +1,43 @@ +package cn.novalon.gym.manage.checkIn.constant; + +import java.time.LocalDate; +import java.util.UUID; + +/** + * 打卡模块 Redis 键常量 + * + * @author 付嘉 + * @date 2026-05-30 + */ +public final class QRRedisKey { + + private static final String SEPARATOR = ":"; + private static final String QRCODE_USER_DAILY = "qrcode:user:daily"; + private static final String QRCODE_CONTENT = "QR_"; + private QRRedisKey() { + // 私有构造,防止实例化 + } + + /** + * 用户当日二维码 + * 格式:qrcode:user:daily:{userId}:{date} + * 示例:qrcode:user:daily:1001:2026-05-30 + */ + public static String qrcodeUserDaily(Long userId, LocalDate date) { + return QRCODE_USER_DAILY + SEPARATOR + userId + SEPARATOR + date; + } + + /** + * 用户当日二维码(今天) + */ + public static String qrcodeUserToday(Long userId) { + return qrcodeUserDaily(userId, LocalDate.now()); + } + + /** + * 生成二维码内容(每个用户每次调用都不同) + */ + public static String generateQrcodeContent() { + return QRCODE_CONTENT + UUID.randomUUID().toString().replace("-", ""); + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/dto/QRCodeDto.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/dto/QRCodeDto.java new file mode 100644 index 0000000..4ae9b80 --- /dev/null +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/dto/QRCodeDto.java @@ -0,0 +1,13 @@ +package cn.novalon.gym.manage.checkIn.dto; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class QRCodeDto { + + private String qrContent; + + private boolean isUsed; +} diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/entity/SignInRecord.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/entity/SignInRecord.java new file mode 100644 index 0000000..db15196 --- /dev/null +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/entity/SignInRecord.java @@ -0,0 +1,41 @@ +package cn.novalon.gym.manage.checkIn.entity; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Column; +import org.springframework.data.relational.core.mapping.Table; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +@Table("sign_in_record") +public class SignInRecord { + + @Id + private Long id; + + // 会员ID + @Column("member_id") + private Long memberId; + + // 签到日期 + @Column("sign_in_date") + private LocalDate signInDate; + + // 签到时间 + @Column("sign_in_time") + private LocalDateTime signInTime; + + // 创建时间 + @CreatedDate + @Column("created_at") + private LocalDateTime createdAt; +} diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/handler/CheckInHandler.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/handler/CheckInHandler.java new file mode 100644 index 0000000..324a9b9 --- /dev/null +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/handler/CheckInHandler.java @@ -0,0 +1,69 @@ +package cn.novalon.gym.manage.checkIn.handler; + +import cn.novalon.gym.manage.checkIn.service.impl.CheckServiceImpl; +import cn.novalon.gym.manage.sys.util.AuthUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +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 java.util.Map; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CheckInHandler { + + private final AuthUtil authUtil; + private final CheckServiceImpl checkService; + + /** + * 签到 + * + * POST /api/checkIn + * + */ + public Mono checkIn(ServerRequest request) { + + Long memberId = 1L; +// authUtil.getMemberIdOrThrow(request); + return request.bodyToMono(Map.class) + .flatMap(body -> { + String qrContent = (String) body.get("qrContent"); + log.info("收到签到请求, memberId: {}, qrContent: {}", memberId, qrContent); + + return checkService.checkIn(memberId, qrContent) + .flatMap(result -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(Map.of("code", 200, "message", "签到成功"))); + }) + .onErrorResume(e -> { + log.error("签到失败", e); + return ServerResponse.status(HttpStatus.BAD_REQUEST) + .bodyValue(Map.of("code", 400, "message", e.getMessage())); + }); + } + + /** + * 获取二维码 + * + * GET /api/checkin/qrcode + * + */ + public Mono getQRCode(ServerRequest request) { + + Long memberId = 1L; +// authUtil.getMemberIdOrThrow(request); + + log.info("收到用户{}获取二维码请求", memberId); + + return checkService.getQRCode(memberId) + .flatMap(qrCodeVo -> ServerResponse.ok() + .contentType(MediaType.APPLICATION_JSON) + .bodyValue(qrCodeVo)); + } +} diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/ICheckInService.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/ICheckInService.java new file mode 100644 index 0000000..9b37980 --- /dev/null +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/ICheckInService.java @@ -0,0 +1,11 @@ +package cn.novalon.gym.manage.checkIn.service; + +import cn.novalon.gym.manage.checkIn.vo.QRCodeVo; +import reactor.core.publisher.Mono; + +public interface ICheckInService { + + Mono getQRCode(Long memberId); + + Mono checkIn(Long memberId, String qrContent); +} diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java new file mode 100644 index 0000000..fbdd4f0 --- /dev/null +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/service/impl/CheckServiceImpl.java @@ -0,0 +1,97 @@ +package cn.novalon.gym.manage.checkIn.service.impl; + +import cn.hutool.core.bean.BeanUtil; +import cn.hutool.extra.qrcode.QrCodeUtil; +import cn.hutool.extra.qrcode.QrConfig; +import cn.hutool.json.JSONUtil; +import cn.novalon.gym.manage.checkIn.config.QRCodeConfig; +import cn.novalon.gym.manage.checkIn.constant.QRRedisKey; +import cn.novalon.gym.manage.checkIn.service.ICheckInService; +import cn.novalon.gym.manage.checkIn.vo.QRCodeVo; +import cn.novalon.gym.manage.common.constant.RedisKeyConstants; +import cn.novalon.gym.manage.common.util.RedisUtil; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Mono; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.HashMap; +import java.util.Map; + +@Slf4j +@Service +@RequiredArgsConstructor +public class CheckServiceImpl implements ICheckInService { + + @Autowired + private final QRCodeConfig qrCodeConfig; + + private final RedisUtil redisUtil; + + @Override + public Mono getQRCode(Long memberId) { + log.info("开始查询会员信息"); + // TODO: 获取会员信息 - 查会员卡有效期/剩余次数,过期返回,先查缓存,缓存不存在则查数据库 + // if (member有效期过了) throw new RuntimeException("会员有效期已过,拒绝生成二维码"); + log.info("会员信息查询完成"); + + log.info("开始生成二维码"); + String qrContent = QRRedisKey.generateQrcodeContent(); + Map redisMap = new HashMap<>(); + redisMap.put("qrContent", qrContent); + redisMap.put("isUsed", false); + + return redisUtil.setWithExpire( + RedisKeyConstants.QRCODE_USER_DAILY+memberId+LocalDate.now(), + redisMap, + getSecondsUntilEndOfDay() + ) + .then(Mono.fromSupplier(() -> { + String qrCodeBase64 = QrCodeUtil.generateAsBase64(qrContent, + BeanUtil.copyProperties(qrCodeConfig, QrConfig.class), "png"); + return new QRCodeVo(qrCodeBase64,false,qrCodeConfig.getWidth(),qrCodeConfig.getHeight()); + })); + } + + @Override + public Mono checkIn(Long memberId, String qrContent) { + String key = RedisKeyConstants.QRCODE_USER_DAILY+memberId+LocalDate.now(); + + return redisUtil.get(key) + .flatMap(cachedQrContent -> { + if (cachedQrContent != null) { + // 匹配成功,执行签到逻辑 + Map map = JSONUtil.parseObj(cachedQrContent); + if(map.get("qrContent").equals(qrContent)){ + if((boolean)map.get("isUsed")){ + log.error("重复签到"); + throw new RuntimeException("您已经在"+map.get("checkInTime")+"完成签到,请勿重复签到"); + } + log.info("二维码匹配成功,memberId: {}", memberId); + // TODO查会员卡缓存,按照卡有效期进行扣减次数,没有缓存查数据库 + map.put("isUsed", true); + map.put("checkInTime", LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))); + + return redisUtil.set(key,map). + then(Mono.just("签到成功")); + } + } + throw new RuntimeException("二维码无效"); + }) + .switchIfEmpty(Mono.error(new RuntimeException("二维码已过期或不存在"))); + } + + private long getSecondsUntilEndOfDay() { + LocalDateTime now = LocalDateTime.now(); + LocalDateTime endOfDay = now.toLocalDate().atTime(23, 59, 59); + + if (now.isAfter(endOfDay)) return 1; + + return ChronoUnit.SECONDS.between(now, endOfDay); + } +} diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/vo/QRCodeVo.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/vo/QRCodeVo.java new file mode 100644 index 0000000..13e329e --- /dev/null +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/vo/QRCodeVo.java @@ -0,0 +1,17 @@ +package cn.novalon.gym.manage.checkIn.vo; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class QRCodeVo { + + private String qrCodeBase64; + + private boolean isUsed; + + private Integer width; + + private Integer height; +} diff --git a/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/websocket/MyWebSocketHandler.java b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/websocket/MyWebSocketHandler.java new file mode 100644 index 0000000..c786050 --- /dev/null +++ b/gym-manage-api/gym-checkIn/src/main/java/cn/novalon/gym/manage/checkIn/websocket/MyWebSocketHandler.java @@ -0,0 +1,98 @@ +package cn.novalon.gym.manage.checkIn.websocket; + +import cn.hutool.json.JSONUtil; +import cn.novalon.gym.manage.checkIn.dto.QRCodeDto; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; +import org.springframework.web.reactive.socket.WebSocketHandler; +import org.springframework.web.reactive.socket.WebSocketMessage; +import org.springframework.web.reactive.socket.WebSocketSession; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +@Slf4j +@Component +public class MyWebSocketHandler implements WebSocketHandler { + + // 存储所有连接 + private static final Map sessions = new ConcurrentHashMap<>(); + + @Override + public Mono handle(WebSocketSession session) { + String sessionId = session.getId(); + + // 连接建立 + sessions.put(sessionId, session); + log.info("WebSocket 连接建立,sessionId:{},当前连接数:{}", sessionId, sessions.size()); + + // 处理接收到的消息 + Flux output = session.receive() + .doOnNext(message -> { + String payload = message.getPayloadAsText(); + log.info("收到消息:{}", payload); + }) + .map(message -> { + String payload = message.getPayloadAsText(); + String response = processMessage(payload, sessionId); + return session.textMessage(response); + }); + + // 连接关闭时清理 + return session.send(output) + .doFinally(signalType -> { + sessions.remove(sessionId); + log.info("WebSocket 连接关闭,sessionId:{},剩余连接数:{}", sessionId, sessions.size()); + }); + } + + /** + * 处理消息逻辑 + */ + private String processMessage(String message, String sessionId) { + try { + // 解析 QRCodeDto + QRCodeDto qrCodeDto = JSONUtil.toBean(message, QRCodeDto.class); + + String response; + + // 判断二维码是否有效 + if (qrCodeDto.getQrContent() != null + && !qrCodeDto.getQrContent().isEmpty() + && !qrCodeDto.isUsed()) { + // 有效:qrContent 有值且 isUsed 为 false + response = "正在进行签到"; + + // 可选:将二维码标记为已使用(需要调用后端服务) + // checkInService.handleCheckIn(qrCodeDto.getQrContent()); + + log.info("二维码有效,sessionId:{},qrContent:{}", sessionId, qrCodeDto.getQrContent()); + } else { + // 无效:qrContent 为空 或 isUsed 为 true + String reason = ""; + if (qrCodeDto.getQrContent() == null || qrCodeDto.getQrContent().isEmpty()) { + reason = "二维码内容为空"; + } else if (qrCodeDto.isUsed()) { + reason = "二维码已被使用"; + } + response = "二维码无效:" + reason; + log.warn("二维码无效,sessionId:{},原因:{}", sessionId, reason); + } + + return response; + + } catch (Exception e) { + log.error("解析消息失败,sessionId:{}", sessionId, e); + return "消息格式错误"; + } + } + + /** + * 获取当前在线连接数 + */ + public static int getOnlineCount() { + return sessions.size(); + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-checkIn/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/gym-manage-api/gym-checkIn/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..e69de29 diff --git a/gym-manage-api/gym-checkIn/src/main/resources/checkIn-config.yml b/gym-manage-api/gym-checkIn/src/main/resources/checkIn-config.yml new file mode 100644 index 0000000..b1eb6d9 --- /dev/null +++ b/gym-manage-api/gym-checkIn/src/main/resources/checkIn-config.yml @@ -0,0 +1,10 @@ +# 二维码配置 +qr: + config: + width: 300 # 二维码宽度(像素) + height: 300 # 二维码高度(像素) + margin: 1 # 白边宽度(像素) + format: png # 图片格式:png / jpg + error-correction: L #容错率:L, M, Q, H,如果启用Logo(logo-enabled: true),必须设置为 H + logo-enabled: false # 是否启用Logo(启用时error-correction必须为H) +# logo-path: static/logo.png # Logo图片路径(支持相对路径或绝对路径) \ No newline at end of file diff --git a/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/CheckInModuleTest.java b/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/CheckInModuleTest.java new file mode 100644 index 0000000..bed8bb0 --- /dev/null +++ b/gym-manage-api/gym-checkIn/src/test/java/cn/novalon/gym/manage/checkin/CheckInModuleTest.java @@ -0,0 +1,12 @@ +package cn.novalon.gym.manage.checkin; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +public class CheckInModuleTest { + + @Test + public void contextLoads() { + } +} diff --git a/gym-manage-api/gym-checkIn/src/test/resources/application-test.yml b/gym-manage-api/gym-checkIn/src/test/resources/application-test.yml new file mode 100644 index 0000000..ffee846 --- /dev/null +++ b/gym-manage-api/gym-checkIn/src/test/resources/application-test.yml @@ -0,0 +1 @@ +# Test Configuration diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml index 11b6479..3bc6fe8 100644 --- a/gym-manage-api/manage-app/pom.xml +++ b/gym-manage-api/manage-app/pom.xml @@ -43,6 +43,11 @@ gym-member ${project.version} + + cn.novalon.gym.manage + gym-checkIn + ${project.version} + org.springframework.boot diff --git a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java index 76e0b56..ad7b8d4 100644 --- a/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java +++ b/gym-manage-api/manage-app/src/main/java/cn/novalon/gym/manage/app/config/SystemRouter.java @@ -1,6 +1,7 @@ package cn.novalon.gym.manage.app.config; +import cn.novalon.gym.manage.checkIn.handler.CheckInHandler; import cn.novalon.gym.manage.file.handler.SysFileHandler; import cn.novalon.gym.manage.groupcourse.handler.GroupCourseBookingHandler; import cn.novalon.gym.manage.groupcourse.handler.GroupCourseHandler; @@ -66,7 +67,8 @@ public class SystemRouter { MemberCardRecordHandler memberCardRecordHandler, MemberCardTransactionHandler memberCardTransactionHandler, GroupCourseHandler groupCourseHandler, - GroupCourseBookingHandler groupCourseBookingHandler) { + GroupCourseBookingHandler groupCourseBookingHandler, + CheckInHandler checkInHandler) { return route() // ========== 诊断路由 ========== @@ -275,6 +277,21 @@ public class SystemRouter { .GET("/api/groupCourse/bookings/{bookingId}", groupCourseBookingHandler::getBookingById) .GET("/api/groupCourse/bookings/course/{courseId}", groupCourseBookingHandler::getBookingsByCourseId) + // ========= 签到模块路由 ========== + // ===== 签到核心功能 ===== + .POST("/api/checkIn", checkInHandler::checkIn) + .GET("/api/checkIn/qrcode", checkInHandler::getQRCode) + + // ===== 签到记录管理 ===== + .GET("/api/checkIn/records", checkInHandler::getSignInRecords) + .GET("/api/checkIn/records/{id}", checkInHandler::getSignInRecordById) + + // ===== 签到统计 ===== + .GET("/api/checkIn/statistics", checkInHandler::getSignInStatistics) + .GET("/api/checkIn/daily-stats", checkInHandler::getDailySignInStats) + + // ===== 签到数据导出 ===== + .GET("/api/checkIn/records/export", checkInHandler::exportSignInRecords) .build(); } } diff --git a/gym-manage-api/manage-common/pom.xml b/gym-manage-api/manage-common/pom.xml index 7f51635..e5a81c2 100644 --- a/gym-manage-api/manage-common/pom.xml +++ b/gym-manage-api/manage-common/pom.xml @@ -56,6 +56,10 @@ spring-boot-starter-test test + + org.springframework.data + spring-data-redis + diff --git a/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/constant/RedisKeyConstants.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/constant/RedisKeyConstants.java new file mode 100644 index 0000000..053180d --- /dev/null +++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/constant/RedisKeyConstants.java @@ -0,0 +1,64 @@ +package cn.novalon.gym.manage.common.constant; + +/** + * Redis 缓存 Key 常量类 + * 统一管理项目中所有 Redis 缓存的 key 前缀 + * + * @author auto-generated + * @date 2026-05-30 + */ +public final class RedisKeyConstants { + + private RedisKeyConstants() { + } + + // ==================== 会员模块 ==================== + + /** + * 会员信息缓存 + * 格式:member:info:{memberId} + */ + public static final String MEMBER_INFO = "member:info:"; + + /** + * 会员详情缓存 + * 格式:member:detail:{memberId} + */ + public static final String MEMBER_DETAIL = "member:detail:"; + + /** + * 会员卡类型缓存 + * 格式:member:card:{memberCardId} + */ + public static final String MEMBER_CARD = "member:card:"; + + /** + * 会员卡记录缓存(包含剩余次数/金额) + * 格式:member:card:record:{recordId} + */ + public static final String MEMBER_CARD_RECORD = "member:card:record:"; + + /** + * 会员退款申请缓存 + * 格式:member:refund:{recordId} + */ + public static final String MEMBER_REFUND = "member:refund:"; + + // ==================== 签到模块 ==================== + + /** + * 用户当日二维码缓存 + * 格式:qrcode:user:daily:{userId}:{date} + * 示例:qrcode:user:daily:1:2026-05-30 + */ + public static final String QRCODE_USER_DAILY = "qrcode:user:daily:"; + + // ==================== 微信模块 ==================== + + /** + * 微信 access_token 缓存 + * 格式:wechat:access_token:{appType} + * appType: miniapp(小程序), mp(公众号) + */ + public static final String WECHAT_ACCESS_TOKEN = "wechat:access_token:"; +} \ No newline at end of file diff --git a/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java b/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java index ea4b245..54c4a4e 100644 --- a/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java +++ b/gym-manage-api/manage-gateway/src/main/java/cn/novalon/gym/manage/gateway/filter/JwtAuthenticationFilter.java @@ -60,7 +60,8 @@ public class JwtAuthenticationFilter extends AbstractGatewayFilterFactorymanage-file gym-member gym-groupCourse + gym-checkIn