diff --git a/gym-manage-api/docs/datacount-api.md b/gym-manage-api/docs/datacount-api.md
index 1dc7e02..6270458 100644
--- a/gym-manage-api/docs/datacount-api.md
+++ b/gym-manage-api/docs/datacount-api.md
@@ -344,9 +344,134 @@ curl -X GET "http://localhost:8080/api/stats/booking/summary?startDate=2026-06-0
---
-## 3. 数据导出
+## 3. 签到数据统计
-### 3.1 导出会员统计数据
+### 3.1 获取签到统计
+
+获取指定日期范围内的签到统计数据。
+
+**请求**
+
+```http
+GET /api/checkIn/statistics
+```
+
+**查询参数**
+
+| 参数 | 类型 | 必填 | 说明 |
+|-----|------|-----|------|
+| startDate | string | 是 | 开始日期,格式:yyyy-MM-dd |
+| endDate | string | 是 | 结束日期,格式:yyyy-MM-dd |
+
+**请求示例**
+
+```bash
+curl -X GET "http://localhost:8080/api/checkIn/statistics?startDate=2026-06-01&endDate=2026-06-06"
+```
+
+**响应示例**
+
+```json
+{
+ "startDate": "2026-06-01",
+ "endDate": "2026-06-06",
+ "totalSignInCount": 156,
+ "successSignInCount": 148,
+ "failedSignInCount": 8,
+ "successRate": 94.87,
+ "avgDailySignIn": 26,
+ "mostSignInDay": "2026-06-03",
+ "mostSignInCount": 32,
+ "qrCodeSignInCount": 120,
+ "manualSignInCount": 28,
+ "faceSignInCount": 8
+}
+```
+
+**响应字段说明**
+
+| 字段 | 类型 | 说明 |
+|-----|------|------|
+| startDate | string | 统计开始日期 |
+| endDate | string | 统计结束日期 |
+| totalSignInCount | integer | 签到总次数 |
+| successSignInCount | integer | 成功签到次数 |
+| failedSignInCount | integer | 失败签到次数 |
+| successRate | number | 签到成功率(百分比) |
+| avgDailySignIn | integer | 日均签到数 |
+| mostSignInDay | string | 签到最多的日期 |
+| mostSignInCount | integer | 单日最多签到数 |
+| qrCodeSignInCount | integer | 扫码签到次数 |
+| manualSignInCount | integer | 手动签到次数 |
+| faceSignInCount | integer | 人脸识别签到次数 |
+
+---
+
+### 3.2 获取每日签到统计
+
+获取指定日期范围内每天的签到统计列表。
+
+**请求**
+
+```http
+GET /api/checkIn/daily-stats?startDate=2026-06-01&endDate=2026-06-05
+```
+
+**查询参数**
+
+| 参数 | 类型 | 必填 | 说明 |
+|-----|------|-----|------|
+| startDate | string | 是 | 开始日期,格式:yyyy-MM-dd |
+| endDate | string | 是 | 结束日期,格式:yyyy-MM-dd |
+
+**请求示例**
+
+```bash
+curl -X GET "http://localhost:8080/api/checkIn/daily-stats?startDate=2026-06-01&endDate=2026-06-03"
+```
+
+**响应示例**
+
+```json
+[
+ {
+ "date": "2026-06-01",
+ "totalSignInCount": 28,
+ "successSignInCount": 26,
+ "failedSignInCount": 2,
+ "successRate": 92.86,
+ "qrCodeSignInCount": 22,
+ "manualSignInCount": 4,
+ "faceSignInCount": 2
+ },
+ {
+ "date": "2026-06-02",
+ "totalSignInCount": 24,
+ "successSignInCount": 24,
+ "failedSignInCount": 0,
+ "successRate": 100.00,
+ "qrCodeSignInCount": 20,
+ "manualSignInCount": 3,
+ "faceSignInCount": 1
+ },
+ {
+ "date": "2026-06-03",
+ "totalSignInCount": 32,
+ "successSignInCount": 30,
+ "failedSignInCount": 2,
+ "successRate": 93.75,
+ "qrCodeSignInCount": 26,
+ "manualSignInCount": 4,
+ "faceSignInCount": 2
+ }
+]
+```
+
+---
+
+## 4. 数据导出
+
+### 4.1 导出会员统计数据
导出指定日期范围内的会员统计数据为CSV文件。
@@ -387,7 +512,7 @@ curl -X GET "http://localhost:8080/api/stats/export/member?startDate=2026-06-01&
---
-### 3.2 导出预约统计数据
+### 4.2 导出预约统计数据
导出指定日期范围内的预约统计数据为CSV文件。
@@ -428,7 +553,48 @@ curl -X GET "http://localhost:8080/api/stats/export/booking?startDate=2026-06-01
---
-### 3.3 导出全部统计数据
+### 4.3 导出签到记录数据
+
+导出指定日期范围内的签到记录数据为CSV文件。
+
+**请求**
+
+```http
+GET /api/checkIn/records/export?startDate=2026-06-01&endDate=2026-06-30
+```
+
+**查询参数**
+
+| 参数 | 类型 | 必填 | 说明 |
+|-----|------|-----|------|
+| startDate | string | 是 | 开始日期,格式:yyyy-MM-dd |
+| endDate | string | 是 | 结束日期,格式:yyyy-MM-dd |
+
+**请求示例**
+
+```bash
+curl -X GET "http://localhost:8080/api/checkIn/records/export?startDate=2026-06-01&endDate=2026-06-03" \
+ -H "Accept: text/csv" \
+ --output sign_in_records.csv
+```
+
+**响应**
+
+- **Content-Type**: `text/csv; charset=UTF-8`
+- **Content-Disposition**: `attachment; filename=sign_in_records_2026-06-01_2026-06-03.csv`
+
+**CSV格式示例**
+
+```csv
+签到时间,会员ID,会员卡ID,签到方式,签到状态,操作人,来源
+2026-06-01 08:30:00,1,1001,QR_CODE,SUCCESS,张三,MINI_PROGRAM
+2026-06-01 09:15:00,2,1002,MANUAL,SUCCESS,李四,PC_BACKEND
+2026-06-01 10:00:00,3,1003,FACE,SUCCESS,,MINI_PROGRAM
+```
+
+---
+
+### 4.4 导出全部统计数据
导出指定日期范围内的会员和预约统计数据为CSV文件。
@@ -476,7 +642,7 @@ curl -X GET "http://localhost:8080/api/stats/export/all?startDate=2026-06-01&end
---
-## 4. 错误响应
+## 5. 错误响应
所有接口在发生错误时返回统一的错误格式:
@@ -497,7 +663,7 @@ curl -X GET "http://localhost:8080/api/stats/export/all?startDate=2026-06-01&end
---
-## 5. 定时任务
+## 6. 定时任务
模块内置以下定时任务,自动计算统计数据并缓存到Redis:
@@ -515,7 +681,7 @@ curl -X GET "http://localhost:8080/api/stats/export/all?startDate=2026-06-01&end
---
-## 6. 数据来源
+## 7. 数据来源
统计数据来源于以下数据表:
@@ -525,10 +691,11 @@ curl -X GET "http://localhost:8080/api/stats/export/all?startDate=2026-06-01&end
| 会员卡统计 | `member_card_record` | 会员卡记录表 |
| 预约统计 | `group_course_booking` | 团课预约记录表 |
| 课程统计 | `group_course` | 团课课程表 |
+| 签到统计 | `sign_in_record` | 会员到店签到记录表 |
---
-## 7. 技术实现
+## 8. 技术实现
- **框架**: Spring WebFlux(响应式编程)
- **数据库访问**: Spring Data R2DBC
@@ -548,15 +715,18 @@ curl -X GET "http://localhost:8080/api/stats/export/all?startDate=2026-06-01&end
| GET | `/api/stats/booking/date/{date}` | 获取指定日期预约统计 |
| GET | `/api/stats/booking/range` | 获取日期范围预约统计列表 |
| GET | `/api/stats/booking/summary` | 获取预约统计汇总 |
+| GET | `/api/checkIn/statistics` | 获取签到统计 |
+| GET | `/api/checkIn/daily-stats` | 获取每日签到统计 |
| GET | `/api/stats/export/member` | 导出会员统计CSV |
| GET | `/api/stats/export/booking` | 导出预约统计CSV |
| GET | `/api/stats/export/all` | 导出全部统计CSV |
+| GET | `/api/checkIn/records/export` | 导出签到记录CSV |
---
-## 8. 测试指南
+## 9. 测试指南
-### 8.1 测试数据说明
+### 9.1 测试数据说明
数据库已预置测试数据,日期范围为 **2026-06-01 至 2026-06-10**。
@@ -585,9 +755,9 @@ curl -X GET "http://localhost:8080/api/stats/export/all?startDate=2026-06-01&end
---
-### 8.2 测试用例
+### 9.2 测试用例
-#### 8.2.1 会员数据统计
+#### 9.2.1 会员数据统计
**测试1:获取单日会员统计**
@@ -645,7 +815,7 @@ curl -X GET "http://localhost:8080/api/stats/member/summary?startDate=2026-06-01
---
-#### 8.2.2 预约数据统计
+#### 9.2.2 预约数据统计
**测试4:获取单日预约统计**
@@ -703,7 +873,7 @@ curl -X GET "http://localhost:8080/api/stats/booking/summary?startDate=2026-06-0
---
-#### 8.2.3 数据导出
+#### 9.2.3 数据导出
**测试7:导出会员统计数据**
@@ -741,7 +911,7 @@ curl -X GET "http://localhost:8080/api/stats/export/all?startDate=2026-06-01&end
---
-### 8.3 测试参数说明
+### 9.3 测试参数说明
#### 日期参数格式
@@ -765,7 +935,7 @@ curl -X GET "http://localhost:8080/api/stats/export/all?startDate=2026-06-01&end
---
-### 8.4 常见测试场景
+### 9.4 常见测试场景
#### 场景1:测试单日统计
@@ -797,7 +967,7 @@ curl "http://localhost:8080/api/stats/export/all?startDate=2026-06-01&endDate=20
---
-### 8.5 错误测试用例
+### 9.5 错误测试用例
#### 测试10:日期格式错误
@@ -829,7 +999,7 @@ curl -X GET "http://localhost:8080/api/stats/member/date/2025-01-01"
---
-### 8.6 使用 Postman 测试
+### 9.6 使用 Postman 测试
导入以下环境变量到 Postman:
diff --git a/gym-manage-api/gym-dataCount/pom.xml b/gym-manage-api/gym-dataCount/pom.xml
index 792603e..725bacf 100644
--- a/gym-manage-api/gym-dataCount/pom.xml
+++ b/gym-manage-api/gym-dataCount/pom.xml
@@ -87,6 +87,13 @@
gym-groupCourse
${project.version}
+
+
+
+ cn.novalon.gym.manage
+ gym-checkIn
+ ${project.version}
+
diff --git a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/GymDataCountApplication.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/GymDataCountApplication.java
deleted file mode 100644
index b17a83e..0000000
--- a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/GymDataCountApplication.java
+++ /dev/null
@@ -1,29 +0,0 @@
-package cn.novalon.gym.manage.datacount;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-import org.springframework.boot.SpringApplication;
-import org.springframework.boot.autoconfigure.SpringBootApplication;
-import org.springframework.boot.autoconfigure.security.reactive.ReactiveUserDetailsServiceAutoConfiguration;
-import org.springframework.context.annotation.ComponentScan;
-import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
-
-@SpringBootApplication(scanBasePackages = "cn.novalon.gym.manage", exclude = {
- ReactiveUserDetailsServiceAutoConfiguration.class })
-@EnableR2dbcRepositories(basePackages = {
- "cn.novalon.gym.manage.db.dao",
- "cn.novalon.gym.manage.member.repository",
- "cn.novalon.gym.manage.groupcourse.dao",
- "cn.novalon.gym.manage.datacount.dao"
-})
-@ComponentScan(basePackages = "cn.novalon.gym.manage")
-public class GymDataCountApplication {
-
- private static final Logger logger = LoggerFactory.getLogger(GymDataCountApplication.class);
-
- public static void main(String[] args) {
- logger.info("数据统计模块启动中...");
- SpringApplication.run(GymDataCountApplication.class, args);
- logger.info("数据统计模块启动完成");
- }
-}
\ No newline at end of file
diff --git a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/config/DataCountRouter.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/config/DataCountRouter.java
index 2749d6c..4ce4bfb 100644
--- a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/config/DataCountRouter.java
+++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/config/DataCountRouter.java
@@ -24,9 +24,15 @@ public class DataCountRouter {
.GET("/api/stats/booking/range", dataCountHandler::getBookingStatisticsByDateRange)
.GET("/api/stats/booking/summary", dataCountHandler::getBookingStatisticsSummary)
+ // ========== 签到数据统计路由 ==========
+ .GET("/api/stats/signin/date/{date}", dataCountHandler::getSignInStatisticsByDate)
+ .GET("/api/stats/signin/range", dataCountHandler::getSignInStatisticsByDateRange)
+ .GET("/api/stats/signin/summary", dataCountHandler::getSignInStatisticsSummary)
+
// ========== 数据导出路由 ==========
.GET("/api/stats/export/member", dataCountHandler::exportMemberStatistics)
.GET("/api/stats/export/booking", dataCountHandler::exportBookingStatistics)
+ .GET("/api/stats/export/signin", dataCountHandler::exportSignInStatistics)
.GET("/api/stats/export/all", dataCountHandler::exportAllStatistics)
.build();
diff --git a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/dao/StatisticsDao.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/dao/StatisticsDao.java
index d2530ab..c90d94f 100644
--- a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/dao/StatisticsDao.java
+++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/dao/StatisticsDao.java
@@ -151,6 +151,101 @@ public class StatisticsDao {
.one();
}
+ // ========== 签到数据统计 ==========
+
+ public Mono countTotalSignInsByDate(LocalDate date) {
+ String sql = "SELECT COUNT(*) FROM sign_in_record WHERE is_delete = FALSE AND DATE(sign_in_time) = $1";
+ return r2dbcEntityTemplate.getDatabaseClient()
+ .sql(sql)
+ .bind(0, date)
+ .map(row -> row.get(0, Integer.class))
+ .one();
+ }
+
+ public Mono countSuccessSignInsByDate(LocalDate date) {
+ String sql = "SELECT COUNT(*) FROM sign_in_record WHERE is_delete = FALSE AND sign_in_status = 'SUCCESS' AND DATE(sign_in_time) = $1";
+ return r2dbcEntityTemplate.getDatabaseClient()
+ .sql(sql)
+ .bind(0, date)
+ .map(row -> row.get(0, Integer.class))
+ .one();
+ }
+
+ public Mono countFailedSignInsByDate(LocalDate date) {
+ String sql = "SELECT COUNT(*) FROM sign_in_record WHERE is_delete = FALSE AND sign_in_status = 'FAILED' AND DATE(sign_in_time) = $1";
+ return r2dbcEntityTemplate.getDatabaseClient()
+ .sql(sql)
+ .bind(0, date)
+ .map(row -> row.get(0, Integer.class))
+ .one();
+ }
+
+ public Mono countUniqueMembersByDate(LocalDate date) {
+ String sql = "SELECT COUNT(DISTINCT member_id) FROM sign_in_record WHERE is_delete = FALSE AND DATE(sign_in_time) = $1";
+ return r2dbcEntityTemplate.getDatabaseClient()
+ .sql(sql)
+ .bind(0, date)
+ .map(row -> row.get(0, Integer.class))
+ .one();
+ }
+
+ public Mono countQrCodeSignInsByDate(LocalDate date) {
+ String sql = "SELECT COUNT(*) FROM sign_in_record WHERE is_delete = FALSE AND sign_in_type = 'QR_CODE' AND DATE(sign_in_time) = $1";
+ return r2dbcEntityTemplate.getDatabaseClient()
+ .sql(sql)
+ .bind(0, date)
+ .map(row -> row.get(0, Integer.class))
+ .one();
+ }
+
+ public Mono countManualSignInsByDate(LocalDate date) {
+ String sql = "SELECT COUNT(*) FROM sign_in_record WHERE is_delete = FALSE AND sign_in_type = 'MANUAL' AND DATE(sign_in_time) = $1";
+ return r2dbcEntityTemplate.getDatabaseClient()
+ .sql(sql)
+ .bind(0, date)
+ .map(row -> row.get(0, Integer.class))
+ .one();
+ }
+
+ public Mono countFaceSignInsByDate(LocalDate date) {
+ String sql = "SELECT COUNT(*) FROM sign_in_record WHERE is_delete = FALSE AND sign_in_type = 'FACE' AND DATE(sign_in_time) = $1";
+ return r2dbcEntityTemplate.getDatabaseClient()
+ .sql(sql)
+ .bind(0, date)
+ .map(row -> row.get(0, Integer.class))
+ .one();
+ }
+
+ public Flux