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/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java
index 0cbbd76..ae20bd4 100644
--- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java
+++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardRecordServiceImpl.java
@@ -3,7 +3,7 @@ package cn.novalon.gym.manage.member.service.impl;
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
import cn.novalon.gym.manage.member.service.IMemberCardRecordService;
-import cn.novalon.gym.manage.member.util.RedisUtil;
+import cn.novalon.gym.manage.common.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java
index 87e345b..8b50822 100644
--- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java
+++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberCardServiceImpl.java
@@ -1,5 +1,6 @@
package cn.novalon.gym.manage.member.service.impl;
+import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.member.entity.MemberCard;
import cn.novalon.gym.manage.member.entity.MemberCardRecord;
import cn.novalon.gym.manage.member.entity.MemberCardTransaction;
@@ -15,7 +16,7 @@ import cn.novalon.gym.manage.member.repository.MemberCardRecordRepository;
import cn.novalon.gym.manage.member.repository.MemberCardRepository;
import cn.novalon.gym.manage.member.service.IMemberCardService;
import cn.novalon.gym.manage.member.service.IMemberCardTransactionService;
-import cn.novalon.gym.manage.member.util.RedisUtil;
+
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java
index cecdaed..86f6110 100644
--- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java
+++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/MemberServiceImpl.java
@@ -17,7 +17,7 @@ import cn.novalon.gym.manage.member.service.MemberService;
import cn.novalon.gym.manage.member.util.AesUtil;
import cn.novalon.gym.manage.member.util.BeanConvertUtil;
import cn.novalon.gym.manage.member.util.EsSyncUtils;
-import cn.novalon.gym.manage.member.util.RedisUtil;
+import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.member.vo.MemberCardInfoVO;
import cn.novalon.gym.manage.member.vo.MemberDetailVO;
import cn.novalon.gym.manage.member.vo.MemberInfoVO;
diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java
index 0e9da72..e505ef7 100644
--- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java
+++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/RefundApplicationServiceImpl.java
@@ -5,7 +5,7 @@ import cn.novalon.gym.manage.member.entity.RefundApplication;
import cn.novalon.gym.manage.member.enums.RefundStatus;
import cn.novalon.gym.manage.member.repository.RefundApplicationRepository;
import cn.novalon.gym.manage.member.service.IRefundApplicationService;
-import cn.novalon.gym.manage.member.util.RedisUtil;
+import cn.novalon.gym.manage.common.util.RedisUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java
index aac7753..99238ec 100644
--- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java
+++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatApiServiceImpl.java
@@ -4,7 +4,7 @@ import cn.novalon.gym.manage.common.exception.ErrorCode;
import cn.novalon.gym.manage.common.exception.SystemException;
import cn.novalon.gym.manage.member.config.WechatProperties;
import cn.novalon.gym.manage.member.service.WechatApiService;
-import cn.novalon.gym.manage.member.util.RedisUtil;
+import cn.novalon.gym.manage.common.util.RedisUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.MediaType;
diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java
index a462a14..b1eec0b 100644
--- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java
+++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatAuthServiceImpl.java
@@ -16,7 +16,7 @@ import cn.novalon.gym.manage.member.service.WechatAuthService;
import cn.novalon.gym.manage.member.util.AesUtil;
import cn.novalon.gym.manage.member.util.EsSyncUtils;
import cn.novalon.gym.manage.member.util.MemberNoGenerator;
-import cn.novalon.gym.manage.member.util.RedisUtil;
+import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.member.util.WechatPhoneUtil;
import cn.novalon.gym.manage.member.vo.WechatLoginVO;
import cn.novalon.gym.manage.sys.security.JwtTokenProvider;
diff --git a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java
index 22a2d56..41aca11 100644
--- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java
+++ b/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/service/impl/WechatOfficialServiceImpl.java
@@ -8,7 +8,7 @@ import cn.novalon.gym.manage.member.es.repository.MemberESRepository;
import cn.novalon.gym.manage.member.repository.IMemberRepository;
import cn.novalon.gym.manage.member.service.WechatOfficialService;
import cn.novalon.gym.manage.member.util.EsSyncUtils;
-import cn.novalon.gym.manage.member.util.RedisUtil;
+import cn.novalon.gym.manage.common.util.RedisUtil;
import cn.novalon.gym.manage.member.vo.WechatUserInfoVO;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml
index 2644308..7d83bcb 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 996c1d1..8a7d9a2 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.member.handler.MemberCardHandler;
import cn.novalon.gym.manage.member.handler.MemberCardRecordHandler;
@@ -62,7 +63,8 @@ public class SystemRouter {
PasswordDiagnosticHandler passwordDiagnosticHandler,
MemberCardHandler memberCardHandler,
MemberCardRecordHandler memberCardRecordHandler,
- MemberCardTransactionHandler memberCardTransactionHandler) {
+ MemberCardTransactionHandler memberCardTransactionHandler,
+ CheckInHandler checkInHandler) {
return route()
// ========== 诊断路由 ==========
@@ -249,7 +251,10 @@ public class SystemRouter {
.GET("/api/member-card-transactions/statistics/deduct/{cardId}", memberCardTransactionHandler::getDeductCountByCardId)
.GET("/api/member-card-transactions/statistics/renew", memberCardTransactionHandler::getRenewAmountByTimeRange)
.GET("/api/member-card-transactions/statistics/purchase/{memberId}", memberCardTransactionHandler::getPurchaseAmountByMember)
-
+
+ // ========= 签到路由 ==========
+ .POST("/api/checkIn", checkInHandler::checkIn )
+ .GET("/api/checkIn/qrcode", checkInHandler::getQRCode)
.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/gym-member/src/main/java/cn/novalon/gym/manage/member/config/RedisConfig.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/config/RedisConfig.java
similarity index 96%
rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/RedisConfig.java
rename to gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/config/RedisConfig.java
index 7321879..6b9f23c 100644
--- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/config/RedisConfig.java
+++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/config/RedisConfig.java
@@ -1,4 +1,4 @@
-package cn.novalon.gym.manage.member.config;
+package cn.novalon.gym.manage.common.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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/gym-member/src/main/java/cn/novalon/gym/manage/member/util/RedisUtil.java b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/RedisUtil.java
similarity index 97%
rename from gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/RedisUtil.java
rename to gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/RedisUtil.java
index ea49f50..0482306 100644
--- a/gym-manage-api/gym-member/src/main/java/cn/novalon/gym/manage/member/util/RedisUtil.java
+++ b/gym-manage-api/manage-common/src/main/java/cn/novalon/gym/manage/common/util/RedisUtil.java
@@ -1,4 +1,4 @@
-package cn.novalon.gym.manage.member.util;
+package cn.novalon.gym.manage.common.util;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.ReactiveRedisTemplate;
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-notify
manage-file
gym-member
+ gym-checkIn