From 54a65b2a74737fac366ba86fbb9b74ba32a34ac5 Mon Sep 17 00:00:00 2001 From: liwentao Date: Tue, 9 Jun 2026 18:15:38 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=A8=A1=E5=9D=974=EF=BC=8C4?= =?UTF-8?q?.1=E5=9F=BA=E7=A1=80=E6=95=B0=E6=8D=AE=E7=BB=9F=E8=AE=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gym-manage-api/docs/datacount-api.md | 206 ++++++++++++++++-- gym-manage-api/gym-dataCount/pom.xml | 7 + .../datacount/GymDataCountApplication.java | 29 --- .../datacount/config/DataCountRouter.java | 6 + .../manage/datacount/dao/StatisticsDao.java | 95 ++++++++ .../datacount/domain/SignInStatistics.java | 157 +++++++++++++ .../datacount/domain/StatisticsExportDTO.java | 11 + .../datacount/handler/DataCountHandler.java | 79 +++++++ .../scheduler/StatisticsDailyScheduler.java | 47 ++++ .../datacount/service/IDataCountService.java | 14 ++ .../service/impl/DataCountServiceImpl.java | 163 +++++++++++++- gym-manage-api/log.text | 99 +++++++-- ...l => V14__Create_sign_in_record_table.sql} | 0 .../gym/manage/sys/config/SecurityConfig.java | 4 +- gym-manage-api/pom.xml | 1 + 15 files changed, 848 insertions(+), 70 deletions(-) delete mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/GymDataCountApplication.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/SignInStatistics.java rename gym-manage-api/manage-db/src/main/resources/db/migration/{V13__Create_sign_in_record_table.sql => V14__Create_sign_in_record_table.sql} (100%) 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 findSignInStatisticsByDateRange(LocalDate startDate, LocalDate endDate) { + String sql = "SELECT DATE(s.sign_in_time) as date, " + + "COUNT(*) as total_sign_ins, " + + "SUM(CASE WHEN s.sign_in_status = 'SUCCESS' THEN 1 ELSE 0 END) as success_sign_ins, " + + "SUM(CASE WHEN s.sign_in_status = 'FAILED' THEN 1 ELSE 0 END) as failed_sign_ins, " + + "COUNT(DISTINCT s.member_id) as unique_members, " + + "SUM(CASE WHEN s.sign_in_type = 'QR_CODE' THEN 1 ELSE 0 END) as qr_code_sign_ins, " + + "SUM(CASE WHEN s.sign_in_type = 'MANUAL' THEN 1 ELSE 0 END) as manual_sign_ins, " + + "SUM(CASE WHEN s.sign_in_type = 'FACE' THEN 1 ELSE 0 END) as face_sign_ins " + + "FROM sign_in_record s " + + "WHERE s.is_delete = FALSE AND DATE(s.sign_in_time) BETWEEN $1 AND $2 " + + "GROUP BY DATE(s.sign_in_time) " + + "ORDER BY date"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, startDate) + .bind(1, endDate) + .map(row -> new Object[]{ + row.get(0, LocalDate.class), + row.get(1, Integer.class), + row.get(2, Integer.class), + row.get(3, Integer.class), + row.get(4, Integer.class), + row.get(5, Integer.class), + row.get(6, Integer.class), + row.get(7, Integer.class) + }) + .all(); + } + // ========== 日期范围查询 ========== public Flux findMemberStatisticsByDateRange(LocalDate startDate, LocalDate endDate) { diff --git a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/SignInStatistics.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/SignInStatistics.java new file mode 100644 index 0000000..cb4a47c --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/SignInStatistics.java @@ -0,0 +1,157 @@ +package cn.novalon.gym.manage.datacount.domain; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDate; + +@Schema(description = "签到数据统计") +public class SignInStatistics { + + @Schema(description = "统计日期", example = "2026-01-01") + private LocalDate date; + + @Schema(description = "签到总次数", example = "100") + private Integer totalSignIns; + + @Schema(description = "成功签到次数", example = "95") + private Integer successSignIns; + + @Schema(description = "失败签到次数", example = "5") + private Integer failedSignIns; + + @Schema(description = "签到成功率", example = "0.95") + private Double successRate; + + @Schema(description = "签到会员数(去重)", example = "80") + private Integer uniqueMembers; + + @Schema(description = "平均到店频次(月均)", example = "4.5") + private Double averageFrequency; + + @Schema(description = "扫码签到次数", example = "80") + private Integer qrCodeSignIns; + + @Schema(description = "手动签到次数", example = "15") + private Integer manualSignIns; + + @Schema(description = "人脸识别签到次数", example = "5") + private Integer faceSignIns; + + @Schema(description = "开始日期(用于汇总统计)") + private LocalDate startDate; + + @Schema(description = "结束日期(用于汇总统计)") + private LocalDate endDate; + + public SignInStatistics() { + } + + public SignInStatistics(LocalDate date) { + this.date = date; + this.totalSignIns = 0; + this.successSignIns = 0; + this.failedSignIns = 0; + this.successRate = 0.0; + this.uniqueMembers = 0; + this.averageFrequency = 0.0; + this.qrCodeSignIns = 0; + this.manualSignIns = 0; + this.faceSignIns = 0; + } + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public Integer getTotalSignIns() { + return totalSignIns; + } + + public void setTotalSignIns(Integer totalSignIns) { + this.totalSignIns = totalSignIns; + } + + public Integer getSuccessSignIns() { + return successSignIns; + } + + public void setSuccessSignIns(Integer successSignIns) { + this.successSignIns = successSignIns; + } + + public Integer getFailedSignIns() { + return failedSignIns; + } + + public void setFailedSignIns(Integer failedSignIns) { + this.failedSignIns = failedSignIns; + } + + public Double getSuccessRate() { + return successRate; + } + + public void setSuccessRate(Double successRate) { + this.successRate = successRate; + } + + public Integer getUniqueMembers() { + return uniqueMembers; + } + + public void setUniqueMembers(Integer uniqueMembers) { + this.uniqueMembers = uniqueMembers; + } + + public Double getAverageFrequency() { + return averageFrequency; + } + + public void setAverageFrequency(Double averageFrequency) { + this.averageFrequency = averageFrequency; + } + + public Integer getQrCodeSignIns() { + return qrCodeSignIns; + } + + public void setQrCodeSignIns(Integer qrCodeSignIns) { + this.qrCodeSignIns = qrCodeSignIns; + } + + public Integer getManualSignIns() { + return manualSignIns; + } + + public void setManualSignIns(Integer manualSignIns) { + this.manualSignIns = manualSignIns; + } + + public Integer getFaceSignIns() { + return faceSignIns; + } + + public void setFaceSignIns(Integer faceSignIns) { + this.faceSignIns = faceSignIns; + } + + public LocalDate getStartDate() { + return startDate; + } + + public void setStartDate(LocalDate startDate) { + this.startDate = startDate; + } + + public LocalDate getEndDate() { + return endDate; + } + + public void setEndDate(LocalDate endDate) { + this.endDate = endDate; + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/StatisticsExportDTO.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/StatisticsExportDTO.java index ab38a15..76d7c79 100644 --- a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/StatisticsExportDTO.java +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/StatisticsExportDTO.java @@ -29,6 +29,9 @@ public class StatisticsExportDTO { @Schema(description = "预约统计数据") private List bookingStatisticsList; + @Schema(description = "签到统计数据") + private List signInStatisticsList; + public StatisticsExportDTO() { } @@ -87,4 +90,12 @@ public class StatisticsExportDTO { public void setBookingStatisticsList(List bookingStatisticsList) { this.bookingStatisticsList = bookingStatisticsList; } + + public List getSignInStatisticsList() { + return signInStatisticsList; + } + + public void setSignInStatisticsList(List signInStatisticsList) { + this.signInStatisticsList = signInStatisticsList; + } } \ No newline at end of file diff --git a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/handler/DataCountHandler.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/handler/DataCountHandler.java index a29e798..d2ecd8c 100644 --- a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/handler/DataCountHandler.java +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/handler/DataCountHandler.java @@ -2,6 +2,7 @@ package cn.novalon.gym.manage.datacount.handler; import cn.novalon.gym.manage.datacount.domain.BookingStatistics; import cn.novalon.gym.manage.datacount.domain.MemberStatistics; +import cn.novalon.gym.manage.datacount.domain.SignInStatistics; import cn.novalon.gym.manage.datacount.service.IDataCountService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -201,4 +202,82 @@ public class DataCountHandler { return ServerResponse.badRequest().bodyValue(response); }); } + + // ========== 签到数据统计 ========== + + @Operation(summary = "获取签到数据统计(按日期)", description = "获取指定日期的签到数据统计") + public Mono getSignInStatisticsByDate(ServerRequest request) { + String dateStr = request.pathVariable("date"); + LocalDate date = LocalDate.parse(dateStr, DATE_FORMATTER); + + return dataCountService.getSignInStatisticsByDate(date) + .flatMap(stats -> ServerResponse.ok().bodyValue(stats)) + .switchIfEmpty(ServerResponse.notFound().build()) + .onErrorResume(error -> { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", error.getMessage()); + return ServerResponse.badRequest().bodyValue(response); + }); + } + + @Operation(summary = "获取签到数据统计(按日期范围)", description = "获取指定日期范围内的签到数据统计") + public Mono getSignInStatisticsByDateRange(ServerRequest request) { + String startDateStr = request.queryParam("startDate").orElseThrow(() -> new IllegalArgumentException("startDate必填")); + String endDateStr = request.queryParam("endDate").orElseThrow(() -> new IllegalArgumentException("endDate必填")); + + LocalDate startDate = LocalDate.parse(startDateStr, DATE_FORMATTER); + LocalDate endDate = LocalDate.parse(endDateStr, DATE_FORMATTER); + + return dataCountService.getSignInStatisticsByDateRange(startDate, endDate) + .collectList() + .flatMap(list -> ServerResponse.ok().bodyValue(list)) + .onErrorResume(error -> { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", error.getMessage()); + return ServerResponse.badRequest().bodyValue(response); + }); + } + + @Operation(summary = "获取签到数据统计汇总", description = "获取指定日期范围内的签到数据统计汇总") + public Mono getSignInStatisticsSummary(ServerRequest request) { + String startDateStr = request.queryParam("startDate").orElseThrow(() -> new IllegalArgumentException("startDate必填")); + String endDateStr = request.queryParam("endDate").orElseThrow(() -> new IllegalArgumentException("endDate必填")); + + LocalDate startDate = LocalDate.parse(startDateStr, DATE_FORMATTER); + LocalDate endDate = LocalDate.parse(endDateStr, DATE_FORMATTER); + + return dataCountService.getSignInStatisticsSummary(startDate, endDate) + .flatMap(summary -> ServerResponse.ok().bodyValue(summary)) + .onErrorResume(error -> { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", error.getMessage()); + return ServerResponse.badRequest().bodyValue(response); + }); + } + + @Operation(summary = "导出签到数据统计", description = "导出指定日期范围内的签到数据统计为CSV文件") + public Mono exportSignInStatistics(ServerRequest request) { + String startDateStr = request.queryParam("startDate").orElseThrow(() -> new IllegalArgumentException("startDate必填")); + String endDateStr = request.queryParam("endDate").orElseThrow(() -> new IllegalArgumentException("endDate必填")); + + LocalDate startDate = LocalDate.parse(startDateStr, DATE_FORMATTER); + LocalDate endDate = LocalDate.parse(endDateStr, DATE_FORMATTER); + + String fileName = "signin_statistics_" + startDateStr + "_" + endDateStr + ".csv"; + + return dataCountService.exportSignInStatistics(startDate, endDate) + .flatMap(data -> ServerResponse.ok() + .header("Content-Disposition", "attachment; filename=" + fileName) + .header("Content-Type", "text/csv; charset=UTF-8") + .bodyValue(data)) + .onErrorResume(error -> { + Map response = new HashMap<>(); + response.put("success", false); + response.put("message", error.getMessage()); + return ServerResponse.badRequest().bodyValue(response); + }); + } } \ No newline at end of file diff --git a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/scheduler/StatisticsDailyScheduler.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/scheduler/StatisticsDailyScheduler.java index 73da48c..a1927ae 100644 --- a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/scheduler/StatisticsDailyScheduler.java +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/scheduler/StatisticsDailyScheduler.java @@ -1,6 +1,7 @@ package cn.novalon.gym.manage.datacount.scheduler; import cn.novalon.gym.manage.datacount.service.IDataCountService; +import cn.novalon.gym.manage.datacount.service.impl.DataCountServiceImpl; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.scheduling.annotation.Scheduled; @@ -47,6 +48,12 @@ public class StatisticsDailyScheduler { yesterday, stats.getTotalBookings(), stats.getAttendanceRate())) .doOnError(error -> logger.error("预约统计计算失败: date={}", yesterday, error)) .subscribe(); + + dataCountService.getSignInStatisticsByDate(yesterday) + .doOnSuccess(stats -> logger.info("签到统计计算完成: date={}, totalSignIns={}, successRate={}%", + yesterday, stats.getTotalSignIns(), stats.getSuccessRate())) + .doOnError(error -> logger.error("签到统计计算失败: date={}", yesterday, error)) + .subscribe(); } /** @@ -67,6 +74,11 @@ public class StatisticsDailyScheduler { .doOnSuccess(stats -> logger.info("上周预约汇总统计完成: totalBookings={}", stats.getTotalBookings())) .doOnError(error -> logger.error("上周预约汇总统计失败", error)) .subscribe(); + + dataCountService.getSignInStatisticsSummary(lastWeekStart, lastWeekEnd) + .doOnSuccess(stats -> logger.info("上周签到汇总统计完成: totalSignIns={}", stats.getTotalSignIns())) + .doOnError(error -> logger.error("上周签到汇总统计失败", error)) + .subscribe(); } /** @@ -87,5 +99,40 @@ public class StatisticsDailyScheduler { .doOnSuccess(stats -> logger.info("上月预约汇总统计完成: totalBookings={}", stats.getTotalBookings())) .doOnError(error -> logger.error("上月预约汇总统计失败", error)) .subscribe(); + + dataCountService.getSignInStatisticsSummary(lastMonthStart, lastMonthEnd) + .doOnSuccess(stats -> logger.info("上月签到汇总统计完成: totalSignIns={}", stats.getTotalSignIns())) + .doOnError(error -> logger.error("上月签到汇总统计失败", error)) + .subscribe(); + } + + /** + * 每日凌晨4点清理30天前的统计缓存数据 + * 业务规则:统计数据保留30天(用于趋势分析),业务记录永久保存 + */ + @Scheduled(cron = "0 0 4 * * ?") + public void cleanOldStatisticsCache() { + LocalDate thirtyDaysAgo = LocalDate.now().minusDays(30); + logger.info("定时任务开始清理30天前的统计缓存数据: before {}", thirtyDaysAgo); + + String thirtyDaysAgoStr = thirtyDaysAgo.toString(); + + // 清理会员统计缓存 + String memberCacheKey = DataCountServiceImpl.MEMBER_STATS_CACHE_PREFIX + thirtyDaysAgoStr; + dataCountService.getRedisUtil().delete(memberCacheKey) + .doOnSuccess(deleted -> logger.info("清理会员统计缓存: key={}, deleted={}", memberCacheKey, deleted)) + .subscribe(); + + // 清理预约统计缓存 + String bookingCacheKey = DataCountServiceImpl.BOOKING_STATS_CACHE_PREFIX + thirtyDaysAgoStr; + dataCountService.getRedisUtil().delete(bookingCacheKey) + .doOnSuccess(deleted -> logger.info("清理预约统计缓存: key={}, deleted={}", bookingCacheKey, deleted)) + .subscribe(); + + // 清理签到统计缓存 + String signInCacheKey = DataCountServiceImpl.SIGN_IN_STATS_CACHE_PREFIX + thirtyDaysAgoStr; + dataCountService.getRedisUtil().delete(signInCacheKey) + .doOnSuccess(deleted -> logger.info("清理签到统计缓存: key={}, deleted={}", signInCacheKey, deleted)) + .subscribe(); } } \ No newline at end of file diff --git a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/IDataCountService.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/IDataCountService.java index b70e647..491e59d 100644 --- a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/IDataCountService.java +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/IDataCountService.java @@ -1,7 +1,9 @@ package cn.novalon.gym.manage.datacount.service; +import cn.novalon.gym.manage.common.util.RedisUtil; import cn.novalon.gym.manage.datacount.domain.BookingStatistics; import cn.novalon.gym.manage.datacount.domain.MemberStatistics; +import cn.novalon.gym.manage.datacount.domain.SignInStatistics; import cn.novalon.gym.manage.datacount.domain.StatisticsExportDTO; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -22,9 +24,21 @@ public interface IDataCountService { Mono getBookingStatisticsSummary(LocalDate startDate, LocalDate endDate); + // ========== 签到数据统计 ========== + + Mono getSignInStatisticsByDate(LocalDate date); + + Flux getSignInStatisticsByDateRange(LocalDate startDate, LocalDate endDate); + + Mono getSignInStatisticsSummary(LocalDate startDate, LocalDate endDate); + + Mono exportSignInStatistics(LocalDate startDate, LocalDate endDate); + Mono exportMemberStatistics(LocalDate startDate, LocalDate endDate); Mono exportBookingStatistics(LocalDate startDate, LocalDate endDate); Mono exportAllStatistics(LocalDate startDate, LocalDate endDate); + + RedisUtil getRedisUtil(); } \ No newline at end of file diff --git a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/impl/DataCountServiceImpl.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/impl/DataCountServiceImpl.java index 9d12895..b7367ba 100644 --- a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/impl/DataCountServiceImpl.java +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/impl/DataCountServiceImpl.java @@ -4,6 +4,7 @@ import cn.novalon.gym.manage.common.util.RedisUtil; import cn.novalon.gym.manage.datacount.dao.StatisticsDao; import cn.novalon.gym.manage.datacount.domain.BookingStatistics; import cn.novalon.gym.manage.datacount.domain.MemberStatistics; +import cn.novalon.gym.manage.datacount.domain.SignInStatistics; import cn.novalon.gym.manage.datacount.service.IDataCountService; import com.fasterxml.jackson.databind.ObjectMapper; import lombok.extern.slf4j.Slf4j; @@ -28,8 +29,9 @@ public class DataCountServiceImpl implements IDataCountService { private final RedisUtil redisUtil; private final ObjectMapper objectMapper; - private static final String MEMBER_STATS_CACHE_PREFIX = "stats:member:"; - private static final String BOOKING_STATS_CACHE_PREFIX = "stats:booking:"; + public static final String MEMBER_STATS_CACHE_PREFIX = "stats:member:"; + public static final String BOOKING_STATS_CACHE_PREFIX = "stats:booking:"; + public static final String SIGN_IN_STATS_CACHE_PREFIX = "stats:signin:"; private static final long CACHE_EXPIRE_SECONDS = 3600; public DataCountServiceImpl(StatisticsDao statisticsDao, RedisUtil redisUtil, ObjectMapper objectMapper) { @@ -234,6 +236,152 @@ public class DataCountServiceImpl implements IDataCountService { }); } + @Override + public Mono getSignInStatisticsByDate(LocalDate date) { + String cacheKey = SIGN_IN_STATS_CACHE_PREFIX + date.toString(); + return redisUtil.get(cacheKey) + .flatMap(cached -> { + if (cached != null) { + try { + SignInStatistics stats = objectMapper.readValue(cached.toString(), SignInStatistics.class); + log.debug("从缓存获取签到统计数据, date: {}", date); + return Mono.just(stats); + } catch (Exception e) { + log.warn("缓存数据解析失败, date: {}", date, e); + } + } + return Mono.empty(); + }) + .switchIfEmpty(fetchSignInStatisticsByDate(date) + .flatMap(stats -> { + try { + String json = objectMapper.writeValueAsString(stats); + return redisUtil.setWithExpire(cacheKey, json, CACHE_EXPIRE_SECONDS) + .then(Mono.just(stats)); + } catch (Exception e) { + log.warn("缓存签到统计数据失败, date: {}", date, e); + return Mono.just(stats); + } + })); + } + + private Mono fetchSignInStatisticsByDate(LocalDate date) { + return Mono.zip( + statisticsDao.countTotalSignInsByDate(date), + statisticsDao.countSuccessSignInsByDate(date), + statisticsDao.countFailedSignInsByDate(date), + statisticsDao.countUniqueMembersByDate(date), + statisticsDao.countQrCodeSignInsByDate(date), + statisticsDao.countManualSignInsByDate(date), + statisticsDao.countFaceSignInsByDate(date) + ).map(tuple -> { + int totalSignIns = tuple.getT1(); + int successSignIns = tuple.getT2(); + int failedSignIns = tuple.getT3(); + + SignInStatistics stats = new SignInStatistics(date); + stats.setTotalSignIns(totalSignIns); + stats.setSuccessSignIns(successSignIns); + stats.setFailedSignIns(failedSignIns); + stats.setUniqueMembers(tuple.getT4()); + stats.setQrCodeSignIns(tuple.getT5()); + stats.setManualSignIns(tuple.getT6()); + stats.setFaceSignIns(tuple.getT7()); + + if (totalSignIns > 0) { + stats.setSuccessRate(Math.round(successSignIns * 10000.0 / totalSignIns) / 100.0); + } + + return stats; + }); + } + + @Override + public Flux getSignInStatisticsByDateRange(LocalDate startDate, LocalDate endDate) { + return statisticsDao.findSignInStatisticsByDateRange(startDate, endDate) + .map(row -> { + LocalDate date = (LocalDate) row[0]; + int totalSignIns = ((Number) row[1]).intValue(); + int successSignIns = ((Number) row[2]).intValue(); + int failedSignIns = ((Number) row[3]).intValue(); + int uniqueMembers = ((Number) row[4]).intValue(); + int qrCodeSignIns = ((Number) row[5]).intValue(); + int manualSignIns = ((Number) row[6]).intValue(); + int faceSignIns = ((Number) row[7]).intValue(); + + SignInStatistics stats = new SignInStatistics(date); + stats.setTotalSignIns(totalSignIns); + stats.setSuccessSignIns(successSignIns); + stats.setFailedSignIns(failedSignIns); + stats.setUniqueMembers(uniqueMembers); + stats.setQrCodeSignIns(qrCodeSignIns); + stats.setManualSignIns(manualSignIns); + stats.setFaceSignIns(faceSignIns); + + if (totalSignIns > 0) { + stats.setSuccessRate(Math.round(successSignIns * 10000.0 / totalSignIns) / 100.0); + } + + return stats; + }); + } + + @Override + public Mono getSignInStatisticsSummary(LocalDate startDate, LocalDate endDate) { + return getSignInStatisticsByDateRange(startDate, endDate) + .collectList() + .map(list -> { + SignInStatistics summary = new SignInStatistics(); + summary.setStartDate(startDate); + summary.setEndDate(endDate); + summary.setTotalSignIns(list.stream().mapToInt(SignInStatistics::getTotalSignIns).sum()); + summary.setSuccessSignIns(list.stream().mapToInt(SignInStatistics::getSuccessSignIns).sum()); + summary.setFailedSignIns(list.stream().mapToInt(SignInStatistics::getFailedSignIns).sum()); + summary.setUniqueMembers(list.stream().mapToInt(SignInStatistics::getUniqueMembers).sum()); + summary.setQrCodeSignIns(list.stream().mapToInt(SignInStatistics::getQrCodeSignIns).sum()); + summary.setManualSignIns(list.stream().mapToInt(SignInStatistics::getManualSignIns).sum()); + summary.setFaceSignIns(list.stream().mapToInt(SignInStatistics::getFaceSignIns).sum()); + + int total = summary.getTotalSignIns(); + if (total > 0) { + summary.setSuccessRate(Math.round(summary.getSuccessSignIns() * 10000.0 / total) / 100.0); + } + + return summary; + }); + } + + @Override + public Mono exportSignInStatistics(LocalDate startDate, LocalDate endDate) { + return getSignInStatisticsByDateRange(startDate, endDate) + .collectList() + .map(this::generateSignInStatisticsCSV); + } + + private byte[] generateSignInStatisticsCSV(List statsList) { + return generateSignInStatisticsCSVContent(statsList).getBytes(StandardCharsets.UTF_8); + } + + private String generateSignInStatisticsCSVContent(List statsList) { + StringBuilder sb = new StringBuilder(); + sb.append("统计日期,签到总次数,成功签到次数,失败签到次数,签到成功率(%),签到会员数,扫码签到次数,手动签到次数,人脸识别签到次数\n"); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + for (SignInStatistics stats : statsList) { + sb.append(stats.getDate().format(formatter)).append(","); + sb.append(stats.getTotalSignIns()).append(","); + sb.append(stats.getSuccessSignIns()).append(","); + sb.append(stats.getFailedSignIns()).append(","); + sb.append(String.format("%.2f", stats.getSuccessRate())).append(","); + sb.append(stats.getUniqueMembers()).append(","); + sb.append(stats.getQrCodeSignIns()).append(","); + sb.append(stats.getManualSignIns()).append(","); + sb.append(stats.getFaceSignIns()).append("\n"); + } + + return sb.toString(); + } + @Override public Mono exportMemberStatistics(LocalDate startDate, LocalDate endDate) { return getMemberStatisticsByDateRange(startDate, endDate) @@ -252,7 +400,8 @@ public class DataCountServiceImpl implements IDataCountService { public Mono exportAllStatistics(LocalDate startDate, LocalDate endDate) { return Mono.zip( getMemberStatisticsByDateRange(startDate, endDate).collectList(), - getBookingStatisticsByDateRange(startDate, endDate).collectList() + getBookingStatisticsByDateRange(startDate, endDate).collectList(), + getSignInStatisticsByDateRange(startDate, endDate).collectList() ).map(tuple -> { ByteArrayOutputStream baos = new ByteArrayOutputStream(); PrintWriter writer = new PrintWriter(new OutputStreamWriter(baos, StandardCharsets.UTF_8)); @@ -262,6 +411,9 @@ public class DataCountServiceImpl implements IDataCountService { writer.println(); writer.println("=== 预约数据统计 ==="); writer.println(generateBookingStatisticsCSVContent(tuple.getT2())); + writer.println(); + writer.println("=== 签到数据统计 ==="); + writer.println(generateSignInStatisticsCSVContent(tuple.getT3())); writer.flush(); return baos.toByteArray(); @@ -315,4 +467,9 @@ public class DataCountServiceImpl implements IDataCountService { return sb.toString(); } + + @Override + public RedisUtil getRedisUtil() { + return redisUtil; + } } \ No newline at end of file diff --git a/gym-manage-api/log.text b/gym-manage-api/log.text index 6ace75f..77209a2 100644 --- a/gym-manage-api/log.text +++ b/gym-manage-api/log.text @@ -1,19 +1,80 @@ -2026-06-06T15:01:42.450+08:00 TRACE 20572 --- [gym-manage-api] [ctor-http-nio-3] o.s.w.s.adapter.HttpWebHandlerAdapter : [a300293b-3] HTTP GET "/api/stats/member/range?startDate=2026-06-01&endDate=2026-06-05", headers={masked} -2026-06-06T15:01:42.453+08:00 DEBUG 20572 --- [gym-manage-api] [ parallel-5] o.s.w.s.s.DefaultWebSessionManager : Created new WebSession. -2026-06-06T15:01:42.454+08:00 INFO 20572 --- [gym-manage-api] [ parallel-5] c.n.g.m.sys.audit.OperationLogWebFilter : WebFilter 拦截请求: GET /api/stats/member/range -2026-06-06T15:01:42.454+08:00 INFO 20572 --- [gym-manage-api] [ parallel-5] c.n.g.m.sys.audit.OperationLogWebFilter : 未匹配到操作日志配置,跳过: GET /api/stats/member/range -2026-06-06T15:01:42.454+08:00 INFO 20572 --- [gym-manage-api] [ parallel-5] c.n.g.m.sys.audit.OperationLogWebFilter : WebFilter 拦截请求: GET /api/stats/member/range -2026-06-06T15:01:42.455+08:00 INFO 20572 --- [gym-manage-api] [ parallel-5] c.n.g.m.sys.audit.OperationLogWebFilter : 未匹配到操作日志配置,跳过: GET /api/stats/member/range -2026-06-06T15:01:42.455+08:00 TRACE 20572 --- [gym-manage-api] [ parallel-5] o.s.w.r.f.server.RequestPredicates : Method "GET" matches against value "GET" -2026-06-06T15:01:42.455+08:00 TRACE 20572 --- [gym-manage-api] [ parallel-5] o.s.w.r.f.server.RequestPredicates : Pattern "/api/stats/member/date/{date}" does not match against value "/api/stats/member/range" -2026-06-06T15:01:42.455+08:00 TRACE 20572 --- [gym-manage-api] [ parallel-5] o.s.w.r.f.server.RequestPredicates : Method "GET" matches against value "GET" -2026-06-06T15:01:42.455+08:00 TRACE 20572 --- [gym-manage-api] [ parallel-5] o.s.w.r.f.server.RequestPredicates : Pattern "/api/stats/member/range" matches against value "/api/stats/member/range" -2026-06-06T15:01:42.455+08:00 TRACE 20572 --- [gym-manage-api] [ parallel-5] o.s.w.r.function.server.RouterFunctions : [a300293b-3] Matched (GET && /api/stats/member/range) -2026-06-06T15:01:42.455+08:00 DEBUG 20572 --- [gym-manage-api] [ parallel-5] o.s.w.r.f.s.s.RouterFunctionMapping : [a300293b-3] Mapped to cn.novalon.gym.manage.datacount.config.DataCountRouter$$Lambda/0x00000230b4931ec0@4746b319 -2026-06-06T15:01:42.455+08:00 DEBUG 20572 --- [gym-manage-api] [ parallel-5] o.s.r2dbc.core.DefaultDatabaseClient : Executing SQL statement [SELECT DATE(m.created_at) as date, COUNT(*) as new_members FROM member_user m WHERE m.is_deleted = FALSE AND DATE(m.created_at) BETWEEN $1 AND $2 GROUP BY DATE(m.created_at) ORDER BY date] -2026-06-06T15:01:42.460+08:00 TRACE 20572 --- [gym-manage-api] [actor-tcp-nio-2] org.springframework.web.HttpLogging : [a300293b-3] Encoding [{success=false, message=Cannot decode value of type java.sql.Date with OID 1082}] -2026-06-06T15:01:42.461+08:00 TRACE 20572 --- [gym-manage-api] [actor-tcp-nio-2] o.s.w.s.adapter.HttpWebHandlerAdapter : [a300293b-3] Completed 400 BAD_REQUEST, headers={masked} -2026-06-06T15:01:42.461+08:00 TRACE 20572 --- [gym-manage-api] [actor-tcp-nio-2] org.springframework.web.HttpLogging : [f8ae8809, L:/[0:0:0:0:0:0:0:1]:8084 ! R:/[0:0:0:0:0:0:0:1]:63191] Handling completed -2026-06-06T15:02:07.299+08:00 INFO 20572 --- [gym-manage-api] [ scheduling-1] c.n.g.m.g.s.i.GroupCourseBookingService : 开始处理已开始课程但未到场会员的预约记录 -2026-06-06T15:02:07.301+08:00 DEBUG 20572 --- [gym-manage-api] [ scheduling-1] o.s.r2dbc.core.DefaultDatabaseClient : Executing SQL statement [SELECT b.* FROM group_course_booking b JOIN group_course c ON b.course_id = c.id WHERE b.status = '0' AND b.deleted_at IS NULL AND c.deleted_at IS NULL AND c.status = '0' AND c.start_time < CURRENT_TIMESTAMP] -2026-06-06T15:02:07.305+08:00 INFO 20572 --- [gym-manage-api] [actor-tcp-nio-2] c.n.g.m.g.s.i.GroupCourseBookingService : 没有需要处理的缺席会员记录 +D:\JAVA\jdk-21\bin\java.exe -XX:TieredStopAtLevel=1 -Dspring.output.ansi.enabled=always -Dcom.sun.management.jmxremote -Dspring.jmx.enabled=true -Dspring.liveBeansView.mbeanDomain -Dspring.application.admin.enabled=true "-Dmanagement.endpoints.jmx.exposure.include=*" "-javaagent:D:\IDEA2025\IntelliJ IDEA 2025.3.3\lib\idea_rt.jar=64328" -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath D:\Work\BIG_project\week2\base6-dataCount\gym-manage\gym-manage-api\manage-app\target\classes;D:\Work\BIG_project\week2\base6-dataCount\gym-manage\gym-manage-api\manage-sys\target\classes;D:\Work\BIG_project\week2\base6-dataCount\gym-manage\gym-manage-api\manage-common\target\classes;D:\Maven\JARs\io\jsonwebtoken\jjwt-api\0.11.5\jjwt-api-0.11.5.jar;D:\Maven\JARs\io\jsonwebtoken\jjwt-impl\0.11.5\jjwt-impl-0.11.5.jar;D:\Maven\JARs\io\jsonwebtoken\jjwt-jackson\0.11.5\jjwt-jackson-0.11.5.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-starter-security\3.5.13\spring-boot-starter-security-3.5.13.jar;D:\Maven\JARs\org\springframework\security\spring-security-config\6.2.4\spring-security-config-6.2.4.jar;D:\Maven\JARs\org\springframework\data\spring-data-commons\3.5.10\spring-data-commons-3.5.10.jar;D:\Maven\JARs\org\springframework\spring-beans\6.2.17\spring-beans-6.2.17.jar;D:\Maven\JARs\org\apache\poi\poi\5.2.5\poi-5.2.5.jar;D:\Maven\JARs\commons-codec\commons-codec\1.18.0\commons-codec-1.18.0.jar;D:\Maven\JARs\org\apache\commons\commons-math3\3.6.1\commons-math3-3.6.1.jar;D:\Maven\JARs\commons-io\commons-io\2.15.0\commons-io-2.15.0.jar;D:\Maven\JARs\com\zaxxer\SparseBitSet\1.3\SparseBitSet-1.3.jar;D:\Maven\JARs\org\apache\logging\log4j\log4j-api\2.24.3\log4j-api-2.24.3.jar;D:\Maven\JARs\org\apache\poi\poi-ooxml\5.2.5\poi-ooxml-5.2.5.jar;D:\Maven\JARs\org\apache\poi\poi-ooxml-lite\5.2.5\poi-ooxml-lite-5.2.5.jar;D:\Maven\JARs\org\apache\xmlbeans\xmlbeans\5.2.0\xmlbeans-5.2.0.jar;D:\Maven\JARs\com\github\virtuald\curvesapi\1.08\curvesapi-1.08.jar;D:\Work\BIG_project\week2\base6-dataCount\gym-manage\gym-manage-api\manage-notify\target\classes;D:\Maven\JARs\com\fasterxml\jackson\core\jackson-databind\2.21.2\jackson-databind-2.21.2.jar;D:\Maven\JARs\com\fasterxml\jackson\core\jackson-annotations\2.21\jackson-annotations-2.21.jar;D:\Maven\JARs\com\fasterxml\jackson\core\jackson-core\2.21.2\jackson-core-2.21.2.jar;D:\Work\BIG_project\week2\base6-dataCount\gym-manage\gym-manage-api\manage-file\target\classes;D:\Work\BIG_project\week2\base6-dataCount\gym-manage\gym-manage-api\manage-db\target\classes;D:\Maven\JARs\org\springframework\boot\spring-boot-starter-data-r2dbc\3.5.13\spring-boot-starter-data-r2dbc-3.5.13.jar;D:\Maven\JARs\io\r2dbc\r2dbc-pool\1.0.2.RELEASE\r2dbc-pool-1.0.2.RELEASE.jar;D:\Maven\JARs\io\projectreactor\addons\reactor-pool\1.1.8\reactor-pool-1.1.8.jar;D:\Maven\JARs\org\springframework\data\spring-data-r2dbc\3.5.10\spring-data-r2dbc-3.5.10.jar;D:\Maven\JARs\org\springframework\data\spring-data-relational\3.5.10\spring-data-relational-3.5.10.jar;D:\Maven\JARs\org\springframework\spring-tx\6.2.17\spring-tx-6.2.17.jar;D:\Maven\JARs\org\springframework\spring-context\6.2.17\spring-context-6.2.17.jar;D:\Maven\JARs\org\springframework\spring-r2dbc\6.2.17\spring-r2dbc-6.2.17.jar;D:\Maven\JARs\org\mapstruct\mapstruct\1.5.5.Final\mapstruct-1.5.5.Final.jar;D:\Maven\JARs\org\apache\commons\commons-collections4\4.4\commons-collections4-4.4.jar;D:\Maven\JARs\org\apache\commons\commons-lang3\3.17.0\commons-lang3-3.17.0.jar;D:\Work\BIG_project\week2\base6-dataCount\gym-manage\gym-manage-api\gym-member\target\classes;D:\Maven\JARs\com\github\binarywang\weixin-java-miniapp\4.6.0\weixin-java-miniapp-4.6.0.jar;D:\Maven\JARs\com\github\binarywang\weixin-java-common\4.6.0\weixin-java-common-4.6.0.jar;D:\Maven\JARs\com\thoughtworks\xstream\xstream\1.4.20\xstream-1.4.20.jar;D:\Maven\JARs\io\github\x-stream\mxparser\1.2.2\mxparser-1.2.2.jar;D:\Maven\JARs\xmlpull\xmlpull\1.1.3.1\xmlpull-1.1.3.1.jar;D:\Maven\JARs\org\apache\httpcomponents\httpclient\4.5.13\httpclient-4.5.13.jar;D:\Maven\JARs\org\apache\httpcomponents\httpcore\4.4.16\httpcore-4.4.16.jar;D:\Maven\JARs\org\apache\httpcomponents\httpmime\4.5.13\httpmime-4.5.13.jar;D:\Maven\JARs\org\slf4j\jcl-over-slf4j\2.0.17\jcl-over-slf4j-2.0.17.jar;D:\Maven\JARs\com\google\code\gson\gson\2.13.2\gson-2.13.2.jar;D:\Maven\JARs\com\google\errorprone\error_prone_annotations\2.41.0\error_prone_annotations-2.41.0.jar;D:\Maven\JARs\com\google\guava\guava\33.3.1-jre\guava-33.3.1-jre.jar;D:\Maven\JARs\com\google\guava\failureaccess\1.0.2\failureaccess-1.0.2.jar;D:\Maven\JARs\com\google\guava\listenablefuture\9999.0-empty-to-avoid-conflict-with-guava\listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar;D:\Maven\JARs\com\google\code\findbugs\jsr305\3.0.2\jsr305-3.0.2.jar;D:\Maven\JARs\com\google\j2objc\j2objc-annotations\3.0.0\j2objc-annotations-3.0.0.jar;D:\Maven\JARs\org\dom4j\dom4j\2.1.3\dom4j-2.1.3.jar;D:\Maven\JARs\org\bouncycastle\bcpkix-jdk15on\1.70\bcpkix-jdk15on-1.70.jar;D:\Maven\JARs\org\bouncycastle\bcprov-jdk15on\1.70\bcprov-jdk15on-1.70.jar;D:\Maven\JARs\org\bouncycastle\bcutil-jdk15on\1.70\bcutil-jdk15on-1.70.jar;D:\Maven\JARs\com\github\binarywang\weixin-java-mp\4.6.0\weixin-java-mp-4.6.0.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-starter-data-elasticsearch\3.5.13\spring-boot-starter-data-elasticsearch-3.5.13.jar;D:\Maven\JARs\org\springframework\data\spring-data-elasticsearch\5.5.10\spring-data-elasticsearch-5.5.10.jar;D:\Maven\JARs\co\elastic\clients\elasticsearch-java\8.18.8\elasticsearch-java-8.18.8.jar;D:\Maven\JARs\jakarta\json\jakarta.json-api\2.1.3\jakarta.json-api-2.1.3.jar;D:\Maven\JARs\org\eclipse\parsson\parsson\1.0.5\parsson-1.0.5.jar;D:\Maven\JARs\io\opentelemetry\opentelemetry-api\1.49.0\opentelemetry-api-1.49.0.jar;D:\Maven\JARs\io\opentelemetry\opentelemetry-context\1.49.0\opentelemetry-context-1.49.0.jar;D:\Maven\JARs\org\elasticsearch\client\elasticsearch-rest-client\8.18.8\elasticsearch-rest-client-8.18.8.jar;D:\Maven\JARs\org\apache\httpcomponents\httpasyncclient\4.1.5\httpasyncclient-4.1.5.jar;D:\Maven\JARs\org\apache\httpcomponents\httpcore-nio\4.4.16\httpcore-nio-4.4.16.jar;D:\Work\BIG_project\week2\base6-dataCount\gym-manage\gym-manage-api\gym-checkIn\target\classes;D:\Maven\JARs\com\google\zxing\core\3.5.1\core-3.5.1.jar;D:\Maven\JARs\com\google\zxing\javase\3.5.1\javase-3.5.1.jar;D:\Maven\JARs\com\beust\jcommander\1.82\jcommander-1.82.jar;D:\Maven\JARs\com\github\jai-imageio\jai-imageio-core\1.4.0\jai-imageio-core-1.4.0.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-starter-websocket\3.5.13\spring-boot-starter-websocket-3.5.13.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-starter-web\3.5.13\spring-boot-starter-web-3.5.13.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-starter-tomcat\3.5.13\spring-boot-starter-tomcat-3.5.13.jar;D:\Maven\JARs\org\apache\tomcat\embed\tomcat-embed-core\10.1.53\tomcat-embed-core-10.1.53.jar;D:\Maven\JARs\org\apache\tomcat\embed\tomcat-embed-websocket\10.1.53\tomcat-embed-websocket-10.1.53.jar;D:\Maven\JARs\org\springframework\spring-webmvc\6.2.17\spring-webmvc-6.2.17.jar;D:\Maven\JARs\org\springframework\spring-messaging\6.2.17\spring-messaging-6.2.17.jar;D:\Maven\JARs\org\springframework\spring-websocket\6.2.17\spring-websocket-6.2.17.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-starter-webflux\3.5.13\spring-boot-starter-webflux-3.5.13.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-starter\3.5.13\spring-boot-starter-3.5.13.jar;D:\Maven\JARs\org\springframework\boot\spring-boot\3.5.13\spring-boot-3.5.13.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-autoconfigure\3.5.13\spring-boot-autoconfigure-3.5.13.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-starter-logging\3.5.13\spring-boot-starter-logging-3.5.13.jar;D:\Maven\JARs\ch\qos\logback\logback-classic\1.5.32\logback-classic-1.5.32.jar;D:\Maven\JARs\ch\qos\logback\logback-core\1.5.32\logback-core-1.5.32.jar;D:\Maven\JARs\org\apache\logging\log4j\log4j-to-slf4j\2.24.3\log4j-to-slf4j-2.24.3.jar;D:\Maven\JARs\org\slf4j\jul-to-slf4j\2.0.17\jul-to-slf4j-2.0.17.jar;D:\Maven\JARs\jakarta\annotation\jakarta.annotation-api\2.1.1\jakarta.annotation-api-2.1.1.jar;D:\Maven\JARs\org\yaml\snakeyaml\2.4\snakeyaml-2.4.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-starter-json\3.5.13\spring-boot-starter-json-3.5.13.jar;D:\Maven\JARs\com\fasterxml\jackson\datatype\jackson-datatype-jdk8\2.21.2\jackson-datatype-jdk8-2.21.2.jar;D:\Maven\JARs\com\fasterxml\jackson\module\jackson-module-parameter-names\2.21.2\jackson-module-parameter-names-2.21.2.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-starter-reactor-netty\3.5.13\spring-boot-starter-reactor-netty-3.5.13.jar;D:\Maven\JARs\io\projectreactor\netty\reactor-netty-http\1.2.16\reactor-netty-http-1.2.16.jar;D:\Maven\JARs\io\netty\netty-codec-http\4.1.132.Final\netty-codec-http-4.1.132.Final.jar;D:\Maven\JARs\io\netty\netty-codec-http2\4.1.132.Final\netty-codec-http2-4.1.132.Final.jar;D:\Maven\JARs\org\springframework\spring-web\6.2.17\spring-web-6.2.17.jar;D:\Maven\JARs\org\springframework\spring-webflux\6.2.17\spring-webflux-6.2.17.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-starter-aop\3.5.13\spring-boot-starter-aop-3.5.13.jar;D:\Maven\JARs\org\springframework\spring-aop\6.2.17\spring-aop-6.2.17.jar;D:\Maven\JARs\org\aspectj\aspectjweaver\1.9.25.1\aspectjweaver-1.9.25.1.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-starter-actuator\3.5.13\spring-boot-starter-actuator-3.5.13.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-actuator-autoconfigure\3.5.13\spring-boot-actuator-autoconfigure-3.5.13.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-actuator\3.5.13\spring-boot-actuator-3.5.13.jar;D:\Maven\JARs\io\micrometer\micrometer-observation\1.15.10\micrometer-observation-1.15.10.jar;D:\Maven\JARs\io\micrometer\micrometer-commons\1.15.10\micrometer-commons-1.15.10.jar;D:\Maven\JARs\io\micrometer\micrometer-jakarta9\1.15.10\micrometer-jakarta9-1.15.10.jar;D:\Maven\JARs\io\github\resilience4j\resilience4j-spring-boot3\2.4.0\resilience4j-spring-boot3-2.4.0.jar;D:\Maven\JARs\io\github\resilience4j\resilience4j-spring6\2.4.0\resilience4j-spring6-2.4.0.jar;D:\Maven\JARs\io\github\resilience4j\resilience4j-annotations\2.2.0\resilience4j-annotations-2.2.0.jar;D:\Maven\JARs\io\github\resilience4j\resilience4j-consumer\2.2.0\resilience4j-consumer-2.2.0.jar;D:\Maven\JARs\io\github\resilience4j\resilience4j-circularbuffer\2.2.0\resilience4j-circularbuffer-2.2.0.jar;D:\Maven\JARs\io\github\resilience4j\resilience4j-framework-common\2.4.0\resilience4j-framework-common-2.4.0.jar;D:\Maven\JARs\org\slf4j\slf4j-api\2.0.17\slf4j-api-2.0.17.jar;D:\Maven\JARs\io\github\resilience4j\resilience4j-micrometer\2.2.0\resilience4j-micrometer-2.2.0.jar;D:\Maven\JARs\io\github\resilience4j\resilience4j-reactor\2.4.0\resilience4j-reactor-2.4.0.jar;D:\Maven\JARs\io\projectreactor\reactor-core\3.7.17\reactor-core-3.7.17.jar;D:\Maven\JARs\io\github\resilience4j\resilience4j-circuitbreaker\2.2.0\resilience4j-circuitbreaker-2.2.0.jar;D:\Maven\JARs\io\github\resilience4j\resilience4j-core\2.2.0\resilience4j-core-2.2.0.jar;D:\Maven\JARs\io\github\resilience4j\resilience4j-ratelimiter\2.2.0\resilience4j-ratelimiter-2.2.0.jar;D:\Maven\JARs\io\github\resilience4j\resilience4j-timelimiter\2.2.0\resilience4j-timelimiter-2.2.0.jar;D:\Maven\JARs\io\github\resilience4j\resilience4j-bulkhead\2.2.0\resilience4j-bulkhead-2.2.0.jar;D:\Maven\JARs\io\github\resilience4j\resilience4j-retry\2.2.0\resilience4j-retry-2.2.0.jar;D:\Maven\JARs\io\reactivex\rxjava3\rxjava\3.1.9\rxjava-3.1.9.jar;D:\Maven\JARs\org\reactivestreams\reactive-streams\1.0.4\reactive-streams-1.0.4.jar;D:\Maven\JARs\io\micrometer\micrometer-registry-prometheus\1.13.4\micrometer-registry-prometheus-1.13.4.jar;D:\Maven\JARs\io\micrometer\micrometer-core\1.15.10\micrometer-core-1.15.10.jar;D:\Maven\JARs\org\hdrhistogram\HdrHistogram\2.2.2\HdrHistogram-2.2.2.jar;D:\Maven\JARs\org\latencyutils\LatencyUtils\2.0.3\LatencyUtils-2.0.3.jar;D:\Maven\JARs\io\prometheus\prometheus-metrics-core\1.3.10\prometheus-metrics-core-1.3.10.jar;D:\Maven\JARs\io\prometheus\prometheus-metrics-model\1.3.10\prometheus-metrics-model-1.3.10.jar;D:\Maven\JARs\io\prometheus\prometheus-metrics-config\1.3.10\prometheus-metrics-config-1.3.10.jar;D:\Maven\JARs\io\prometheus\prometheus-metrics-tracer-common\1.3.10\prometheus-metrics-tracer-common-1.3.10.jar;D:\Maven\JARs\io\prometheus\prometheus-metrics-exposition-formats\1.3.10\prometheus-metrics-exposition-formats-1.3.10.jar;D:\Maven\JARs\io\prometheus\prometheus-metrics-exposition-textformats\1.3.10\prometheus-metrics-exposition-textformats-1.3.10.jar;D:\Maven\JARs\org\postgresql\r2dbc-postgresql\1.0.0.RELEASE\r2dbc-postgresql-1.0.0.RELEASE.jar;D:\Maven\JARs\io\r2dbc\r2dbc-spi\1.0.0.RELEASE\r2dbc-spi-1.0.0.RELEASE.jar;D:\Maven\JARs\com\ongres\scram\client\2.1\client-2.1.jar;D:\Maven\JARs\com\ongres\scram\common\2.1\common-2.1.jar;D:\Maven\JARs\com\ongres\stringprep\saslprep\1.1\saslprep-1.1.jar;D:\Maven\JARs\com\ongres\stringprep\stringprep\1.1\stringprep-1.1.jar;D:\Maven\JARs\io\projectreactor\netty\reactor-netty-core\1.2.16\reactor-netty-core-1.2.16.jar;D:\Maven\JARs\io\netty\netty-handler\4.1.132.Final\netty-handler-4.1.132.Final.jar;D:\Maven\JARs\io\netty\netty-resolver\4.1.132.Final\netty-resolver-4.1.132.Final.jar;D:\Maven\JARs\io\netty\netty-buffer\4.1.132.Final\netty-buffer-4.1.132.Final.jar;D:\Maven\JARs\io\netty\netty-transport-native-unix-common\4.1.132.Final\netty-transport-native-unix-common-4.1.132.Final.jar;D:\Maven\JARs\io\netty\netty-codec\4.1.132.Final\netty-codec-4.1.132.Final.jar;D:\Maven\JARs\io\netty\netty-handler-proxy\4.1.132.Final\netty-handler-proxy-4.1.132.Final.jar;D:\Maven\JARs\io\netty\netty-codec-socks\4.1.132.Final\netty-codec-socks-4.1.132.Final.jar;D:\Maven\JARs\io\netty\netty-resolver-dns\4.1.132.Final\netty-resolver-dns-4.1.132.Final.jar;D:\Maven\JARs\io\netty\netty-codec-dns\4.1.132.Final\netty-codec-dns-4.1.132.Final.jar;D:\Maven\JARs\io\netty\netty-resolver-dns-native-macos\4.1.132.Final\netty-resolver-dns-native-macos-4.1.132.Final-osx-x86_64.jar;D:\Maven\JARs\io\netty\netty-resolver-dns-classes-macos\4.1.132.Final\netty-resolver-dns-classes-macos-4.1.132.Final.jar;D:\Maven\JARs\io\netty\netty-transport-native-epoll\4.1.132.Final\netty-transport-native-epoll-4.1.132.Final-linux-x86_64.jar;D:\Maven\JARs\io\netty\netty-transport-classes-epoll\4.1.132.Final\netty-transport-classes-epoll-4.1.132.Final.jar;D:\Maven\JARs\org\postgresql\postgresql\42.7.4\postgresql-42.7.4.jar;D:\Maven\JARs\org\checkerframework\checker-qual\3.42.0\checker-qual-3.42.0.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-starter-jdbc\3.5.13\spring-boot-starter-jdbc-3.5.13.jar;D:\Maven\JARs\com\zaxxer\HikariCP\6.3.3\HikariCP-6.3.3.jar;D:\Maven\JARs\org\springframework\spring-jdbc\6.2.17\spring-jdbc-6.2.17.jar;D:\Maven\JARs\com\h2database\h2\2.3.232\h2-2.3.232.jar;D:\Maven\JARs\io\r2dbc\r2dbc-h2\1.0.1.RELEASE\r2dbc-h2-1.0.1.RELEASE.jar;D:\Maven\JARs\org\flywaydb\flyway-core\11.0.1\flyway-core-11.0.1.jar;D:\Maven\JARs\com\fasterxml\jackson\dataformat\jackson-dataformat-toml\2.21.2\jackson-dataformat-toml-2.21.2.jar;D:\Maven\JARs\com\fasterxml\jackson\datatype\jackson-datatype-jsr310\2.21.2\jackson-datatype-jsr310-2.21.2.jar;D:\Maven\JARs\org\flywaydb\flyway-database-postgresql\11.0.1\flyway-database-postgresql-11.0.1.jar;D:\Maven\JARs\jakarta\xml\bind\jakarta.xml.bind-api\4.0.4\jakarta.xml.bind-api-4.0.4.jar;D:\Maven\JARs\jakarta\activation\jakarta.activation-api\2.1.4\jakarta.activation-api-2.1.4.jar;D:\Maven\JARs\org\springframework\spring-core\6.2.17\spring-core-6.2.17.jar;D:\Maven\JARs\org\springframework\spring-jcl\6.2.17\spring-jcl-6.2.17.jar;D:\Maven\JARs\org\springframework\security\spring-security-core\6.5.9\spring-security-core-6.5.9.jar;D:\Maven\JARs\org\springframework\security\spring-security-crypto\6.2.4\spring-security-crypto-6.2.4.jar;D:\Maven\JARs\org\springframework\spring-expression\6.2.17\spring-expression-6.2.17.jar;D:\Maven\JARs\org\springframework\security\spring-security-web\6.5.9\spring-security-web-6.5.9.jar;D:\Maven\JARs\org\apache\commons\commons-compress\1.24.0\commons-compress-1.24.0.jar;D:\Maven\JARs\org\springdoc\springdoc-openapi-starter-webflux-ui\2.8.16\springdoc-openapi-starter-webflux-ui-2.8.16.jar;D:\Maven\JARs\org\springdoc\springdoc-openapi-starter-webflux-api\2.8.16\springdoc-openapi-starter-webflux-api-2.8.16.jar;D:\Maven\JARs\org\springdoc\springdoc-openapi-starter-common\2.8.16\springdoc-openapi-starter-common-2.8.16.jar;D:\Maven\JARs\io\swagger\core\v3\swagger-core-jakarta\2.2.43\swagger-core-jakarta-2.2.43.jar;D:\Maven\JARs\io\swagger\core\v3\swagger-models-jakarta\2.2.43\swagger-models-jakarta-2.2.43.jar;D:\Maven\JARs\com\fasterxml\jackson\dataformat\jackson-dataformat-yaml\2.21.2\jackson-dataformat-yaml-2.21.2.jar;D:\Maven\JARs\org\webjars\swagger-ui\5.32.0\swagger-ui-5.32.0.jar;D:\Maven\JARs\org\webjars\webjars-locator-lite\1.1.3\webjars-locator-lite-1.1.3.jar;D:\Maven\JARs\org\jspecify\jspecify\1.0.0\jspecify-1.0.0.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-starter-data-redis\3.5.13\spring-boot-starter-data-redis-3.5.13.jar;D:\Maven\JARs\io\lettuce\lettuce-core\6.6.0.RELEASE\lettuce-core-6.6.0.RELEASE.jar;D:\Maven\JARs\redis\clients\authentication\redis-authx-core\0.1.1-beta2\redis-authx-core-0.1.1-beta2.jar;D:\Maven\JARs\io\netty\netty-common\4.1.132.Final\netty-common-4.1.132.Final.jar;D:\Maven\JARs\io\netty\netty-transport\4.1.132.Final\netty-transport-4.1.132.Final.jar;D:\Maven\JARs\org\springframework\data\spring-data-redis\3.5.10\spring-data-redis-3.5.10.jar;D:\Maven\JARs\org\springframework\data\spring-data-keyvalue\3.5.10\spring-data-keyvalue-3.5.10.jar;D:\Maven\JARs\org\springframework\spring-oxm\6.2.17\spring-oxm-6.2.17.jar;D:\Maven\JARs\org\springframework\spring-context-support\6.2.17\spring-context-support-6.2.17.jar;D:\Work\BIG_project\week2\base6-dataCount\gym-manage\gym-manage-api\gym-groupCourse\target\classes;D:\Maven\JARs\org\springframework\boot\spring-boot-starter-validation\3.5.13\spring-boot-starter-validation-3.5.13.jar;D:\Maven\JARs\org\apache\tomcat\embed\tomcat-embed-el\10.1.53\tomcat-embed-el-10.1.53.jar;D:\Maven\JARs\org\hibernate\validator\hibernate-validator\8.0.3.Final\hibernate-validator-8.0.3.Final.jar;D:\Maven\JARs\jakarta\validation\jakarta.validation-api\3.0.2\jakarta.validation-api-3.0.2.jar;D:\Maven\JARs\org\jboss\logging\jboss-logging\3.6.3.Final\jboss-logging-3.6.3.Final.jar;D:\Maven\JARs\com\fasterxml\classmate\1.7.3\classmate-1.7.3.jar;D:\Maven\JARs\io\swagger\core\v3\swagger-annotations-jakarta\2.2.43\swagger-annotations-jakarta-2.2.43.jar;D:\Work\BIG_project\week2\base6-dataCount\gym-manage\gym-manage-api\gym-dataCount\target\classes;D:\Maven\JARs\org\projectlombok\lombok\1.18.30\lombok-1.18.30.jar;D:\Maven\JARs\cn\hutool\hutool-all\5.8.38\hutool-all-5.8.38.jar;D:\Maven\JARs\org\springframework\boot\spring-boot-starter-data-redis-reactive\3.5.13\spring-boot-starter-data-redis-reactive-3.5.13.jar cn.novalon.gym.manage.app.SimpleManageApplication +17:44:30.489 [main] INFO cn.novalon.gym.manage.app.SimpleManageApplication -- 简化版应用程序启动中... +17:44:30.490 [main] INFO cn.novalon.gym.manage.app.SimpleManageApplication -- 包扫描路径: cn.novalon.gym.manage.app + + . ____ _ __ _ _ + /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ +( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ + \\/ ___)| |_)| | | | | || (_| | ) ) ) ) + ' |____| .__|_| |_|_| |_\__, | / / / / + =========|_|==============|___/=/_/_/_/ + + :: Spring Boot :: (v3.5.13) + +2026-06-09T17:44:31.084+08:00 INFO 30368 --- [gym-dataCount] [ main] c.n.g.m.app.SimpleManageApplication : Starting SimpleManageApplication using Java 21.0.9 with PID 30368 (D:\Work\BIG_project\week2\base6-dataCount\gym-manage\gym-manage-api\manage-app\target\classes started by 29827 in D:\Work\BIG_project\week2\base6-dataCount\gym-manage\gym-manage-api) +2026-06-09T17:44:31.086+08:00 INFO 30368 --- [gym-dataCount] [ main] c.n.g.m.app.SimpleManageApplication : No active profile set, falling back to 1 default profile: "default" +2026-06-09T17:44:32.184+08:00 INFO 30368 --- [gym-dataCount] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode +2026-06-09T17:44:32.185+08:00 INFO 30368 --- [gym-dataCount] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data R2DBC repositories in DEFAULT mode. +2026-06-09T17:44:32.532+08:00 INFO 30368 --- [gym-dataCount] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 341 ms. Found 27 R2DBC repository interfaces. +2026-06-09T17:44:32.534+08:00 INFO 30368 --- [gym-dataCount] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Multiple Spring Data modules found, entering strict repository configuration mode +2026-06-09T17:44:32.534+08:00 INFO 30368 --- [gym-dataCount] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data Reactive Elasticsearch repositories in DEFAULT mode. +2026-06-09T17:44:32.542+08:00 INFO 30368 --- [gym-dataCount] [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 5 ms. Found 1 Reactive Elasticsearch repository interface. +2026-06-09T17:44:32.557+08:00 WARN 30368 --- [gym-dataCount] [ main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: java.lang.IllegalStateException: Error processing condition on org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration.propertySourcesPlaceholderConfigurer +2026-06-09T17:44:32.564+08:00 INFO 30368 --- [gym-dataCount] [ main] .s.b.a.l.ConditionEvaluationReportLogger : + +Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled. +2026-06-09T17:44:32.584+08:00 ERROR 30368 --- [gym-dataCount] [ main] o.s.boot.SpringApplication : Application run failed + +java.lang.IllegalStateException: Error processing condition on org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration.propertySourcesPlaceholderConfigurer + at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:60) ~[spring-boot-autoconfigure-3.5.13.jar:3.5.13] + at org.springframework.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:99) ~[spring-context-6.2.17.jar:6.2.17] + at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(ConfigurationClassBeanDefinitionReader.java:191) ~[spring-context-6.2.17.jar:6.2.17] + at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:144) ~[spring-context-6.2.17.jar:6.2.17] + at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitions(ConfigurationClassBeanDefinitionReader.java:120) ~[spring-context-6.2.17.jar:6.2.17] + at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:430) ~[spring-context-6.2.17.jar:6.2.17] + at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:290) ~[spring-context-6.2.17.jar:6.2.17] + at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:349) ~[spring-context-6.2.17.jar:6.2.17] + at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanFactoryPostProcessors(PostProcessorRegistrationDelegate.java:118) ~[spring-context-6.2.17.jar:6.2.17] + at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:792) ~[spring-context-6.2.17.jar:6.2.17] + at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:610) ~[spring-context-6.2.17.jar:6.2.17] + at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.5.13.jar:3.5.13] + at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:752) ~[spring-boot-3.5.13.jar:3.5.13] + at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:439) ~[spring-boot-3.5.13.jar:3.5.13] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:318) ~[spring-boot-3.5.13.jar:3.5.13] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1361) ~[spring-boot-3.5.13.jar:3.5.13] + at org.springframework.boot.SpringApplication.run(SpringApplication.java:1350) ~[spring-boot-3.5.13.jar:3.5.13] + at cn.novalon.gym.manage.app.SimpleManageApplication.main(SimpleManageApplication.java:29) ~[classes/:na] +Caused by: java.lang.IllegalStateException: Failed to introspect Class [cn.novalon.gym.manage.app.config.SystemRouter] from ClassLoader [jdk.internal.loader.ClassLoaders$AppClassLoader@36baf30c] + at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:483) ~[spring-core-6.2.17.jar:6.2.17] + at org.springframework.util.ReflectionUtils.doWithMethods(ReflectionUtils.java:360) ~[spring-core-6.2.17.jar:6.2.17] + at org.springframework.util.ReflectionUtils.getUniqueDeclaredMethods(ReflectionUtils.java:417) ~[spring-core-6.2.17.jar:6.2.17] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.lambda$getTypeForFactoryMethod$1(AbstractAutowireCapableBeanFactory.java:757) ~[spring-beans-6.2.17.jar:6.2.17] + at java.base/java.util.concurrent.ConcurrentHashMap.computeIfAbsent(ConcurrentHashMap.java:1708) ~[na:na] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.getTypeForFactoryMethod(AbstractAutowireCapableBeanFactory.java:756) ~[spring-beans-6.2.17.jar:6.2.17] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.determineTargetType(AbstractAutowireCapableBeanFactory.java:689) ~[spring-beans-6.2.17.jar:6.2.17] + at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.predictBeanType(AbstractAutowireCapableBeanFactory.java:660) ~[spring-beans-6.2.17.jar:6.2.17] + at org.springframework.beans.factory.support.AbstractBeanFactory.isFactoryBean(AbstractBeanFactory.java:1715) ~[spring-beans-6.2.17.jar:6.2.17] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.doGetBeanNamesForType(DefaultListableBeanFactory.java:640) ~[spring-beans-6.2.17.jar:6.2.17] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForType(DefaultListableBeanFactory.java:612) ~[spring-beans-6.2.17.jar:6.2.17] + at org.springframework.beans.factory.support.DefaultListableBeanFactory.getBeanNamesForType(DefaultListableBeanFactory.java:597) ~[spring-beans-6.2.17.jar:6.2.17] + at org.springframework.boot.autoconfigure.condition.OnBeanCondition.collectBeanDefinitionsForType(OnBeanCondition.java:326) ~[spring-boot-autoconfigure-3.5.13.jar:3.5.13] + at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getBeanDefinitionsForType(OnBeanCondition.java:315) ~[spring-boot-autoconfigure-3.5.13.jar:3.5.13] + at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getMatchingBeans(OnBeanCondition.java:214) ~[spring-boot-autoconfigure-3.5.13.jar:3.5.13] + at org.springframework.boot.autoconfigure.condition.OnBeanCondition.evaluateConditionalOnMissingBean(OnBeanCondition.java:197) ~[spring-boot-autoconfigure-3.5.13.jar:3.5.13] + at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getMatchOutcome(OnBeanCondition.java:144) ~[spring-boot-autoconfigure-3.5.13.jar:3.5.13] + at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:47) ~[spring-boot-autoconfigure-3.5.13.jar:3.5.13] + ... 17 common frames omitted +Caused by: java.lang.NoClassDefFoundError: DictionaryHandler + at java.base/java.lang.Class.getDeclaredMethods0(Native Method) ~[na:na] + at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3580) ~[na:na] + at java.base/java.lang.Class.getDeclaredMethods(Class.java:2678) ~[na:na] + at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:465) ~[spring-core-6.2.17.jar:6.2.17] + ... 34 common frames omitted +Caused by: java.lang.ClassNotFoundException: DictionaryHandler + at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:641) ~[na:na] + at java.base/jdk.internal.loader.ClassLoaders$AppClassLoader.loadClass(ClassLoaders.java:188) ~[na:na] + at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:526) ~[na:na] + ... 38 common frames omitted + + +进程已结束,退出代码为 1 diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Create_sign_in_record_table.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V14__Create_sign_in_record_table.sql similarity index 100% rename from gym-manage-api/manage-db/src/main/resources/db/migration/V13__Create_sign_in_record_table.sql rename to gym-manage-api/manage-db/src/main/resources/db/migration/V14__Create_sign_in_record_table.sql diff --git a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java index 2dd184a..5fa3e7e 100644 --- a/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java +++ b/gym-manage-api/manage-sys/src/main/java/cn/novalon/gym/manage/sys/config/SecurityConfig.java @@ -57,7 +57,9 @@ public class SecurityConfig { .pathMatchers("/api/member-cards/**").permitAll() .pathMatchers("/api/member-card-records/**").permitAll() .pathMatchers("/api/member-card-transactions/**").permitAll() - .pathMatchers("/api/checkIn/**").permitAll(); + .pathMatchers("/api/checkIn/**").permitAll() + .pathMatchers("/**").permitAll() + .pathMatchers("/api/member-card-transactions/**").permitAll(); if (isDevOrTest) { diff --git a/gym-manage-api/pom.xml b/gym-manage-api/pom.xml index e92c517..91a2e5a 100644 --- a/gym-manage-api/pom.xml +++ b/gym-manage-api/pom.xml @@ -44,6 +44,7 @@ manage-file gym-member gym-groupCourse + gym-checkIn gym-dataCount