From 8958ffaa9eda87fb27322f224c9729ccd511e815 Mon Sep 17 00:00:00 2001 From: liwentao Date: Sat, 6 Jun 2026 15:19:40 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=A8=A1=E5=9D=974?= =?UTF-8?q?=EF=BC=8C4.1=E5=9F=BA=E7=A1=80=E6=95=B0=E6=8D=AE=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E7=9A=84=E9=83=A8=E5=88=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gym-manage-api/docs/datacount-api.md | 848 ++++++++++++++++++ .../.mvn/wrapper/maven-wrapper.properties | 3 + gym-manage-api/gym-dataCount/pom.xml | 101 +++ .../datacount/GymDataCountApplication.java | 29 + .../datacount/config/DataCountRouter.java | 34 + .../manage/datacount/dao/StatisticsDao.java | 175 ++++ .../datacount/domain/BookingStatistics.java | 169 ++++ .../datacount/domain/MemberStatistics.java | 145 +++ .../datacount/domain/StatisticsExportDTO.java | 90 ++ .../datacount/handler/DataCountHandler.java | 204 +++++ .../scheduler/StatisticsDailyScheduler.java | 91 ++ .../datacount/service/IDataCountService.java | 30 + .../service/impl/DataCountServiceImpl.java | 318 +++++++ .../src/main/resources/application.yaml | 3 + .../GymDataCountApplicationTests.java | 13 + gym-manage-api/log.text | 219 +---- gym-manage-api/manage-app/pom.xml | 6 + .../V13__Insert_DataCount_Test_Data.sql | 443 +++++++++ .../gym/manage/sys/config/SecurityConfig.java | 1 + gym-manage-api/pom.xml | 1 + 20 files changed, 2723 insertions(+), 200 deletions(-) create mode 100644 gym-manage-api/docs/datacount-api.md create mode 100644 gym-manage-api/gym-dataCount/.mvn/wrapper/maven-wrapper.properties create mode 100644 gym-manage-api/gym-dataCount/pom.xml create 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/config/DataCountRouter.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/dao/StatisticsDao.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/BookingStatistics.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/MemberStatistics.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/StatisticsExportDTO.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/handler/DataCountHandler.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/scheduler/StatisticsDailyScheduler.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/IDataCountService.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/impl/DataCountServiceImpl.java create mode 100644 gym-manage-api/gym-dataCount/src/main/resources/application.yaml create mode 100644 gym-manage-api/gym-dataCount/src/test/java/cn/novalon/gym/manage/datacount/GymDataCountApplicationTests.java create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V13__Insert_DataCount_Test_Data.sql diff --git a/gym-manage-api/docs/datacount-api.md b/gym-manage-api/docs/datacount-api.md new file mode 100644 index 0000000..1dc7e02 --- /dev/null +++ b/gym-manage-api/docs/datacount-api.md @@ -0,0 +1,848 @@ +# 数据统计模块 API 文档 + +> 模块:gym-dataCount +> 版本:1.0.0 +> 更新日期:2026-06-06 + +## 概述 + +数据统计模块提供会员数据统计、预约数据统计和数据导出功能,支持按日期查询、日期范围查询和汇总统计。 + +--- + +## 基础信息 + +- **Base URL**: `/api/stats` +- **Content-Type**: `application/json` +- **响应格式**: JSON + +--- + +## 1. 会员数据统计 + +### 1.1 获取指定日期的会员统计 + +获取某一天的会员数据统计信息。 + +**请求** + +```http +GET /api/stats/member/date/{date} +``` + +**路径参数** + +| 参数 | 类型 | 必填 | 说明 | +|-----|------|-----|------| +| date | string | 是 | 统计日期,格式:yyyy-MM-dd | + +**请求示例** + +```bash +curl -X GET "http://localhost:8080/api/stats/member/date/2026-06-01" +``` + +**响应示例** + +```json +{ + "date": "2026-06-01", + "newMembers": 5, + "activeMembers": 0, + "retainedMembers": 0, + "totalMembers": 19, + "maleMembers": 10, + "femaleMembers": 9, + "cardPurchaseCount": 19, + "averageAge": 29.5 +} +``` + +**响应字段说明** + +| 字段 | 类型 | 说明 | +|-----|------|------| +| date | string | 统计日期 | +| newMembers | integer | 当日新增会员数 | +| activeMembers | integer | 当日活跃会员数(有会员卡操作的会员) | +| retainedMembers | integer | 留存会员数 | +| totalMembers | integer | 总会员数(累计) | +| maleMembers | integer | 男性会员总数 | +| femaleMembers | integer | 女性会员总数 | +| cardPurchaseCount | integer | 当日购卡人数 | +| averageAge | number | 会员平均年龄 | + +--- + +### 1.2 获取日期范围内的会员统计列表 + +获取指定日期范围内每天的会员统计数据。 + +**请求** + +```http +GET /api/stats/member/range?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/stats/member/range?startDate=2026-06-01&endDate=2026-06-03" +``` + +**响应示例** + +```json +[ + { + "date": "2026-06-01", + "newMembers": 5, + "activeMembers": 0, + "totalMembers": 19, + "maleMembers": 10, + "femaleMembers": 9, + "cardPurchaseCount": 19, + "averageAge": 29.5 + }, + { + "date": "2026-06-02", + "newMembers": 4, + "activeMembers": 0, + "totalMembers": 19, + "maleMembers": 10, + "femaleMembers": 9, + "cardPurchaseCount": 19, + "averageAge": 29.5 + }, + { + "date": "2026-06-03", + "newMembers": 3, + "activeMembers": 0, + "totalMembers": 19, + "maleMembers": 10, + "femaleMembers": 9, + "cardPurchaseCount": 19, + "averageAge": 29.5 + } +] +``` + +--- + +### 1.3 获取会员统计汇总 + +获取指定日期范围内的会员数据汇总统计。 + +**请求** + +```http +GET /api/stats/member/summary?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/stats/member/summary?startDate=2026-06-01&endDate=2026-06-06" +``` + +**响应示例** + +```json +{ + "startDate": "2026-06-01", + "endDate": "2026-06-06", + "newMembers": 19, + "activeMembers": 0, + "totalMembers": 19, + "maleMembers": 10, + "femaleMembers": 9, + "cardPurchaseCount": 19, + "averageAge": 29.5 +} +``` + +--- + +## 2. 预约数据统计 + +### 2.1 获取指定日期的预约统计 + +获取某一天的团课预约数据统计信息。 + +**请求** + +```http +GET /api/stats/booking/date/{date} +``` + +**路径参数** + +| 参数 | 类型 | 必填 | 说明 | +|-----|------|-----|------| +| date | string | 是 | 统计日期,格式:yyyy-MM-dd | + +**请求示例** + +```bash +curl -X GET "http://localhost:8080/api/stats/booking/date/2026-06-01" +``` + +**响应示例** + +```json +{ + "date": "2026-06-01", + "totalBookings": 32, + "cancelledBookings": 5, + "attendedBookings": 27, + "bookingSuccessRate": 0.0, + "cancellationRate": 15.63, + "attendanceRate": 84.38, + "popularCourseName": "燃脂单车", + "popularCourseCount": 19, + "totalCourses": 3, + "fullCourses": 1 +} +``` + +**响应字段说明** + +| 字段 | 类型 | 说明 | +|-----|------|------| +| date | string | 统计日期 | +| totalBookings | integer | 预约总数 | +| cancelledBookings | integer | 取消预约数 | +| attendedBookings | integer | 实际到场数 | +| bookingSuccessRate | number | 预约成功率(保留) | +| cancellationRate | number | 取消率(百分比) | +| attendanceRate | number | 到场率(百分比) | +| popularCourseName | string | 最热门课程名称 | +| popularCourseCount | integer | 热门课程预约数 | +| totalCourses | integer | 当日团课总数 | +| fullCourses | integer | 满员课程数 | + +--- + +### 2.2 获取日期范围内的预约统计列表 + +获取指定日期范围内每天的预约统计数据。 + +**请求** + +```http +GET /api/stats/booking/range?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/stats/booking/range?startDate=2026-06-01&endDate=2026-06-03" +``` + +**响应示例** + +```json +[ + { + "date": "2026-06-01", + "totalBookings": 32, + "cancelledBookings": 5, + "attendedBookings": 27, + "cancellationRate": 15.63, + "attendanceRate": 84.38, + "popularCourseName": "燃脂单车", + "popularCourseCount": 19, + "totalCourses": 3, + "fullCourses": 1 + }, + { + "date": "2026-06-02", + "totalBookings": 30, + "cancelledBookings": 3, + "attendedBookings": 27, + "cancellationRate": 10.00, + "attendanceRate": 90.00, + "popularCourseName": "流瑜伽", + "popularCourseCount": 10, + "totalCourses": 3, + "fullCourses": 0 + }, + { + "date": "2026-06-03", + "totalBookings": 35, + "cancelledBookings": 4, + "attendedBookings": 31, + "cancellationRate": 11.43, + "attendanceRate": 88.57, + "popularCourseName": "哈他瑜伽", + "popularCourseCount": 12, + "totalCourses": 3, + "fullCourses": 2 + } +] +``` + +--- + +### 2.3 获取预约统计汇总 + +获取指定日期范围内的预约数据汇总统计。 + +**请求** + +```http +GET /api/stats/booking/summary?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/stats/booking/summary?startDate=2026-06-01&endDate=2026-06-06" +``` + +**响应示例** + +```json +{ + "startDate": "2026-06-01", + "endDate": "2026-06-06", + "totalBookings": 172, + "cancelledBookings": 16, + "attendedBookings": 156, + "cancellationRate": 9.30, + "attendanceRate": 90.70 +} +``` + +--- + +## 3. 数据导出 + +### 3.1 导出会员统计数据 + +导出指定日期范围内的会员统计数据为CSV文件。 + +**请求** + +```http +GET /api/stats/export/member?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/stats/export/member?startDate=2026-06-01&endDate=2026-06-03" \ + -H "Accept: text/csv" \ + --output member_stats.csv +``` + +**响应** + +- **Content-Type**: `text/csv; charset=UTF-8` +- **Content-Disposition**: `attachment; filename=member_statistics_2026-06-01_2026-06-03.csv` + +**CSV格式示例** + +```csv +统计日期,新增会员数,活跃会员数,总会员数,男性会员数,女性会员数,购卡人数,平均年龄 +2026-06-01,5,0,19,10,9,19,29.5 +2026-06-02,4,0,19,10,9,19,29.5 +2026-06-03,3,0,19,10,9,19,29.5 +``` + +--- + +### 3.2 导出预约统计数据 + +导出指定日期范围内的预约统计数据为CSV文件。 + +**请求** + +```http +GET /api/stats/export/booking?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/stats/export/booking?startDate=2026-06-01&endDate=2026-06-03" \ + -H "Accept: text/csv" \ + --output booking_stats.csv +``` + +**响应** + +- **Content-Type**: `text/csv; charset=UTF-8` +- **Content-Disposition**: `attachment; filename=booking_statistics_2026-06-01_2026-06-03.csv` + +**CSV格式示例** + +```csv +统计日期,预约总数,取消预约数,实际到场数,取消率(%),到场率(%),热门课程,热门课程预约数,团课总数,满员课程数 +2026-06-01,32,5,27,15.63,84.38,燃脂单车,19,3,1 +2026-06-02,30,3,27,10.00,90.00,流瑜伽,10,3,0 +2026-06-03,35,4,31,11.43,88.57,哈他瑜伽,12,3,2 +``` + +--- + +### 3.3 导出全部统计数据 + +导出指定日期范围内的会员和预约统计数据为CSV文件。 + +**请求** + +```http +GET /api/stats/export/all?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/stats/export/all?startDate=2026-06-01&endDate=2026-06-03" \ + -H "Accept: text/csv" \ + --output all_stats.csv +``` + +**响应** + +- **Content-Type**: `text/csv; charset=UTF-8` +- **Content-Disposition**: `attachment; filename=all_statistics_2026-06-01_2026-06-03.csv` + +**CSV格式示例** + +```csv +=== 会员数据统计 === +统计日期,新增会员数,活跃会员数,总会员数,男性会员数,女性会员数,购卡人数,平均年龄 +2026-06-01,5,0,19,10,9,19,29.5 +2026-06-02,4,0,19,10,9,19,29.5 +2026-06-03,3,0,19,10,9,19,29.5 + +=== 预约数据统计 === +统计日期,预约总数,取消预约数,实际到场数,取消率(%),到场率(%),热门课程,热门课程预约数,团课总数,满员课程数 +2026-06-01,32,5,27,15.63,84.38,燃脂单车,19,3,1 +2026-06-02,30,3,27,10.00,90.00,流瑜伽,10,3,0 +2026-06-03,35,4,31,11.43,88.57,哈他瑜伽,12,3,2 +``` + +--- + +## 4. 错误响应 + +所有接口在发生错误时返回统一的错误格式: + +```json +{ + "success": false, + "message": "错误描述信息" +} +``` + +**常见错误码** + +| HTTP状态码 | 说明 | +|-----------|------| +| 400 Bad Request | 参数格式错误或缺少必填参数 | +| 404 Not Found | 指定日期的数据不存在 | +| 500 Internal Server Error | 服务器内部错误 | + +--- + +## 5. 定时任务 + +模块内置以下定时任务,自动计算统计数据并缓存到Redis: + +| 任务名称 | 执行时间 | 说明 | +|---------|---------|------| +| 每日统计 | 每日凌晨1点 | 计算前一天的会员和预约统计 | +| 每周汇总 | 每周一凌晨2点 | 计算上周的汇总统计 | +| 每月汇总 | 每月1号凌晨3点 | 计算上月的汇总统计 | + +**缓存策略** + +- 缓存键前缀:`stats:member:` 和 `stats:booking:` +- 缓存过期时间:3600秒(1小时) +- 每日定时任务会自动刷新缓存 + +--- + +## 6. 数据来源 + +统计数据来源于以下数据表: + +| 统计类型 | 数据表 | 说明 | +|---------|-------|------| +| 会员统计 | `member_user` | 会员基本信息表 | +| 会员卡统计 | `member_card_record` | 会员卡记录表 | +| 预约统计 | `group_course_booking` | 团课预约记录表 | +| 课程统计 | `group_course` | 团课课程表 | + +--- + +## 7. 技术实现 + +- **框架**: Spring WebFlux(响应式编程) +- **数据库访问**: Spring Data R2DBC +- **缓存**: Redis(ReactiveRedisTemplate) +- **API文档**: OpenAPI 3.0 / Swagger +- **导出格式**: CSV + +--- + +## 附录:接口列表汇总 + +| 方法 | 路径 | 说明 | +|-----|------|------| +| GET | `/api/stats/member/date/{date}` | 获取指定日期会员统计 | +| GET | `/api/stats/member/range` | 获取日期范围会员统计列表 | +| GET | `/api/stats/member/summary` | 获取会员统计汇总 | +| GET | `/api/stats/booking/date/{date}` | 获取指定日期预约统计 | +| GET | `/api/stats/booking/range` | 获取日期范围预约统计列表 | +| GET | `/api/stats/booking/summary` | 获取预约统计汇总 | +| GET | `/api/stats/export/member` | 导出会员统计CSV | +| GET | `/api/stats/export/booking` | 导出预约统计CSV | +| GET | `/api/stats/export/all` | 导出全部统计CSV | + +--- + +## 8. 测试指南 + +### 8.1 测试数据说明 + +数据库已预置测试数据,日期范围为 **2026-06-01 至 2026-06-10**。 + +#### 会员数据分布 + +| 日期 | 新增会员数 | 男性会员 | 女性会员 | +|------|-----------|---------|---------| +| 2026-06-01 | 5 | 3 | 2 | +| 2026-06-02 | 4 | 2 | 2 | +| 2026-06-03 | 3 | 1 | 2 | +| 2026-06-04 | 2 | 1 | 1 | +| 2026-06-05 | 3 | 2 | 1 | +| 2026-06-06 | 2 | 1 | 1 | +| **合计** | **19** | **10** | **9** | + +#### 预约数据分布 + +| 日期 | 预约总数 | 取消预约 | 实际到场 | 满员课程数 | +|------|---------|---------|---------|-----------| +| 2026-06-01 | 32 | 5 | 27 | 1 | +| 2026-06-02 | 30 | 3 | 27 | 0 | +| 2026-06-03 | 35 | 4 | 31 | 2 | +| 2026-06-04 | 28 | 2 | 26 | 1 | +| 2026-06-05 | 25 | 1 | 24 | 0 | +| 2026-06-06 | 22 | 1 | 21 | 0 | + +--- + +### 8.2 测试用例 + +#### 8.2.1 会员数据统计 + +**测试1:获取单日会员统计** + +```bash +curl -X GET "http://localhost:8080/api/stats/member/date/2026-06-01" +``` + +**预期结果**: +```json +{ + "date": "2026-06-01", + "newMembers": 5, + "activeMembers": 0, + "retainedMembers": 0, + "totalMembers": 19, + "maleMembers": 10, + "femaleMembers": 9, + "cardPurchaseCount": 19, + "averageAge": 29.5 +} +``` + +--- + +**测试2:获取日期范围会员统计** + +```bash +curl -X GET "http://localhost:8080/api/stats/member/range?startDate=2026-06-01&endDate=2026-06-03" +``` + +**预期结果**:返回3条记录,分别为 2026-06-01、2026-06-02、2026-06-03 的统计数据。 + +--- + +**测试3:获取会员统计汇总** + +```bash +curl -X GET "http://localhost:8080/api/stats/member/summary?startDate=2026-06-01&endDate=2026-06-06" +``` + +**预期结果**: +```json +{ + "startDate": "2026-06-01", + "endDate": "2026-06-06", + "newMembers": 19, + "activeMembers": 0, + "totalMembers": 19, + "maleMembers": 10, + "femaleMembers": 9, + "cardPurchaseCount": 19, + "averageAge": 29.5 +} +``` + +--- + +#### 8.2.2 预约数据统计 + +**测试4:获取单日预约统计** + +```bash +curl -X GET "http://localhost:8080/api/stats/booking/date/2026-06-01" +``` + +**预期结果**: +```json +{ + "date": "2026-06-01", + "totalBookings": 32, + "cancelledBookings": 5, + "attendedBookings": 27, + "bookingSuccessRate": 0.0, + "cancellationRate": 15.63, + "attendanceRate": 84.38, + "popularCourseName": "燃脂单车", + "popularCourseCount": 19, + "totalCourses": 3, + "fullCourses": 1 +} +``` + +--- + +**测试5:获取日期范围预约统计** + +```bash +curl -X GET "http://localhost:8080/api/stats/booking/range?startDate=2026-06-01&endDate=2026-06-03" +``` + +**预期结果**:返回3条记录,分别为 2026-06-01、2026-06-02、2026-06-03 的统计数据。 + +--- + +**测试6:获取预约统计汇总** + +```bash +curl -X GET "http://localhost:8080/api/stats/booking/summary?startDate=2026-06-01&endDate=2026-06-06" +``` + +**预期结果**: +```json +{ + "startDate": "2026-06-01", + "endDate": "2026-06-06", + "totalBookings": 172, + "cancelledBookings": 16, + "attendedBookings": 156, + "cancellationRate": 9.30, + "attendanceRate": 90.70 +} +``` + +--- + +#### 8.2.3 数据导出 + +**测试7:导出会员统计数据** + +```bash +curl -X GET "http://localhost:8080/api/stats/export/member?startDate=2026-06-01&endDate=2026-06-03" \ + -H "Accept: text/csv" \ + --output member_stats.csv +``` + +**预期结果**:下载 CSV 文件,包含 2026-06-01 至 2026-06-03 的会员统计数据。 + +--- + +**测试8:导出预约统计数据** + +```bash +curl -X GET "http://localhost:8080/api/stats/export/booking?startDate=2026-06-01&endDate=2026-06-03" \ + -H "Accept: text/csv" \ + --output booking_stats.csv +``` + +**预期结果**:下载 CSV 文件,包含 2026-06-01 至 2026-06-03 的预约统计数据。 + +--- + +**测试9:导出全部统计数据** + +```bash +curl -X GET "http://localhost:8080/api/stats/export/all?startDate=2026-06-01&endDate=2026-06-03" \ + -H "Accept: text/csv" \ + --output all_stats.csv +``` + +**预期结果**:下载 CSV 文件,包含会员和预约的完整统计数据。 + +--- + +### 8.3 测试参数说明 + +#### 日期参数格式 + +所有日期参数必须使用 `yyyy-MM-dd` 格式,例如: +- ✅ `2026-06-01` +- ❌ `2026/06/01` +- ❌ `2026-6-1` +- ❌ `06-01-2026` + +#### 日期范围参数 + +| 参数 | 说明 | 示例 | +|-----|------|------| +| startDate | 开始日期(包含) | `2026-06-01` | +| endDate | 结束日期(包含) | `2026-06-06` | + +**注意事项**: +- `startDate` 必须 ≤ `endDate` +- 日期范围不能超过 365 天 +- 日期必须在测试数据范围内(2026-06-01 至 2026-06-10) + +--- + +### 8.4 常见测试场景 + +#### 场景1:测试单日统计 + +```bash +# 测试6月1日的会员和预约统计 +curl "http://localhost:8080/api/stats/member/date/2026-06-01" +curl "http://localhost:8080/api/stats/booking/date/2026-06-01" +``` + +--- + +#### 场景2:测试一周统计 + +```bash +# 测试6月第一周的统计 +curl "http://localhost:8080/api/stats/member/summary?startDate=2026-06-01&endDate=2026-06-06" +curl "http://localhost:8080/api/stats/booking/summary?startDate=2026-06-01&endDate=2026-06-06" +``` + +--- + +#### 场景3:测试数据导出 + +```bash +# 导出6月前3天的所有统计数据 +curl "http://localhost:8080/api/stats/export/all?startDate=2026-06-01&endDate=2026-06-03" \ + --output test_data.csv +``` + +--- + +### 8.5 错误测试用例 + +#### 测试10:日期格式错误 + +```bash +curl -X GET "http://localhost:8080/api/stats/member/date/2026/06/01" +``` + +**预期结果**:HTTP 400 Bad Request + +--- + +#### 测试11:日期范围无效 + +```bash +curl -X GET "http://localhost:8080/api/stats/member/range?startDate=2026-06-06&endDate=2026-06-01" +``` + +**预期结果**:HTTP 400 Bad Request + +--- + +#### 测试12:日期超出测试数据范围 + +```bash +curl -X GET "http://localhost:8080/api/stats/member/date/2025-01-01" +``` + +**预期结果**:HTTP 404 Not Found 或返回空数据 + +--- + +### 8.6 使用 Postman 测试 + +导入以下环境变量到 Postman: + +| 变量名 | 值 | 说明 | +|-------|---|------| +| `base_url` | `http://localhost:8080` | API 基础地址 | +| `test_start_date` | `2026-06-01` | 测试开始日期 | +| `test_end_date` | `2026-06-06` | 测试结束日期 | + +示例请求: + +``` +GET {{base_url}}/api/stats/member/date/2026-06-01 +GET {{base_url}}/api/stats/member/range?startDate={{test_start_date}}&endDate={{test_end_date}} +GET {{base_url}}/api/stats/booking/summary?startDate={{test_start_date}}&endDate={{test_end_date}} +``` diff --git a/gym-manage-api/gym-dataCount/.mvn/wrapper/maven-wrapper.properties b/gym-manage-api/gym-dataCount/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..216df05 --- /dev/null +++ b/gym-manage-api/gym-dataCount/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip diff --git a/gym-manage-api/gym-dataCount/pom.xml b/gym-manage-api/gym-dataCount/pom.xml new file mode 100644 index 0000000..792603e --- /dev/null +++ b/gym-manage-api/gym-dataCount/pom.xml @@ -0,0 +1,101 @@ + + + 4.0.0 + + cn.novalon.gym.manage + gym-manage-api + 1.0.0 + ../pom.xml + + cn.novalon.gym.manage + gym-dataCount + 1.0.0 + gym-dataCount + Data Statistics Module + + + + + + + + + + + + + + + 21 + + + + 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-data-r2dbc + + + org.springframework.boot + spring-boot-starter-validation + + + org.springdoc + springdoc-openapi-starter-webflux-ui + + + org.springframework.boot + spring-boot-starter-test + test + + + io.swagger.core.v3 + swagger-annotations-jakarta + 2.2.43 + compile + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + cn.novalon.gym.manage + gym-member + ${project.version} + + + + + cn.novalon.gym.manage + gym-groupCourse + ${project.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file 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 new file mode 100644 index 0000000..b17a83e --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/GymDataCountApplication.java @@ -0,0 +1,29 @@ +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 new file mode 100644 index 0000000..2749d6c --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/config/DataCountRouter.java @@ -0,0 +1,34 @@ +package cn.novalon.gym.manage.datacount.config; + +import cn.novalon.gym.manage.datacount.handler.DataCountHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +@Configuration +public class DataCountRouter { + + @Bean + public RouterFunction dataCountRoutes(DataCountHandler dataCountHandler) { + return route() + // ========== 会员数据统计路由 ========== + .GET("/api/stats/member/date/{date}", dataCountHandler::getMemberStatisticsByDate) + .GET("/api/stats/member/range", dataCountHandler::getMemberStatisticsByDateRange) + .GET("/api/stats/member/summary", dataCountHandler::getMemberStatisticsSummary) + + // ========== 预约数据统计路由 ========== + .GET("/api/stats/booking/date/{date}", dataCountHandler::getBookingStatisticsByDate) + .GET("/api/stats/booking/range", dataCountHandler::getBookingStatisticsByDateRange) + .GET("/api/stats/booking/summary", dataCountHandler::getBookingStatisticsSummary) + + // ========== 数据导出路由 ========== + .GET("/api/stats/export/member", dataCountHandler::exportMemberStatistics) + .GET("/api/stats/export/booking", dataCountHandler::exportBookingStatistics) + .GET("/api/stats/export/all", dataCountHandler::exportAllStatistics) + + .build(); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..d2530ab --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/dao/StatisticsDao.java @@ -0,0 +1,175 @@ +package cn.novalon.gym.manage.datacount.dao; + +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 统计数据访问层 + * 使用 R2dbcEntityTemplate 执行原生SQL查询 + * + * @author 数据统计模块 + * @date 2026-06-06 + */ +@Repository +public class StatisticsDao { + + private final R2dbcEntityTemplate r2dbcEntityTemplate; + + public StatisticsDao(R2dbcEntityTemplate r2dbcEntityTemplate) { + this.r2dbcEntityTemplate = r2dbcEntityTemplate; + } + + // ========== 会员数据统计 ========== + + public Mono countTotalMembers() { + String sql = "SELECT COUNT(*) FROM member_user WHERE is_deleted = FALSE"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countNewMembersByDate(LocalDate date) { + String sql = "SELECT COUNT(*) FROM member_user WHERE is_deleted = FALSE AND DATE(created_at) = $1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countActiveMembersByDate(LocalDate date) { + String sql = "SELECT COUNT(DISTINCT m.id) FROM member_user m LEFT JOIN member_card_record r ON m.id = r.member_id WHERE m.is_deleted = FALSE AND r.deleted_at IS NULL AND DATE(r.created_at) = $1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countMaleMembers() { + String sql = "SELECT COUNT(*) FROM member_user WHERE is_deleted = FALSE AND gender = 1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countFemaleMembers() { + String sql = "SELECT COUNT(*) FROM member_user WHERE is_deleted = FALSE AND gender = 2"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countCardPurchaseByDate(LocalDate date) { + String sql = "SELECT COUNT(DISTINCT r.member_id) FROM member_card_record r WHERE r.deleted_at IS NULL AND DATE(r.created_at) = $1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono calculateAverageAge() { + String sql = "SELECT AVG(EXTRACT(YEAR FROM AGE(CURRENT_DATE, birthday))) FROM member_user WHERE is_deleted = FALSE AND birthday IS NOT NULL"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .map(row -> row.get(0, Double.class)) + .one(); + } + + public Mono countActiveMembersByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + String sql = "SELECT COUNT(*) FROM member_user WHERE is_deleted = FALSE AND last_login_at >= $1 AND last_login_at < $2"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, startTime) + .bind(1, endTime) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + // ========== 预约数据统计 ========== + + public Mono countTotalBookingsByDate(LocalDate date) { + String sql = "SELECT COUNT(*) FROM group_course_booking WHERE deleted_at IS NULL AND DATE(created_at) = $1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countCancelledBookingsByDate(LocalDate date) { + String sql = "SELECT COUNT(*) FROM group_course_booking WHERE deleted_at IS NULL AND status = '1' AND DATE(cancel_time) = $1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countAttendedBookingsByDate(LocalDate date) { + String sql = "SELECT COUNT(*) FROM group_course_booking WHERE deleted_at IS NULL AND status = '2' AND DATE(created_at) = $1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countTotalCoursesByDate(LocalDate date) { + String sql = "SELECT COUNT(*) FROM group_course WHERE deleted_at IS NULL AND DATE(start_time) = $1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countFullCoursesByDate(LocalDate date) { + String sql = "SELECT COUNT(*) FROM group_course WHERE deleted_at IS NULL AND current_members = max_members AND DATE(start_time) = $1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono findMostPopularCourseByDate(LocalDate date) { + String sql = "SELECT c.course_name, COUNT(b.id) as booking_count FROM group_course_booking b LEFT JOIN group_course c ON b.course_id = c.id WHERE b.deleted_at IS NULL AND c.deleted_at IS NULL AND DATE(b.created_at) = $1 GROUP BY c.id, c.course_name ORDER BY booking_count DESC LIMIT 1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> new Object[]{row.get(0, String.class), row.get(1, Integer.class)}) + .one(); + } + + // ========== 日期范围查询 ========== + + public Flux findMemberStatisticsByDateRange(LocalDate startDate, LocalDate endDate) { + String sql = "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"; + 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)}) + .all(); + } + + public Flux findBookingStatisticsByDateRange(LocalDate startDate, LocalDate endDate) { + String sql = "SELECT DATE(b.created_at) as date, COUNT(*) as total_bookings, SUM(CASE WHEN b.status = '1' THEN 1 ELSE 0 END) as cancelled_bookings, SUM(CASE WHEN b.status = '2' THEN 1 ELSE 0 END) as attended_bookings FROM group_course_booking b WHERE b.deleted_at IS NULL AND DATE(b.created_at) BETWEEN $1 AND $2 GROUP BY DATE(b.created_at) 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)}) + .all(); + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/BookingStatistics.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/BookingStatistics.java new file mode 100644 index 0000000..814e3cf --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/BookingStatistics.java @@ -0,0 +1,169 @@ +package cn.novalon.gym.manage.datacount.domain; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDate; + +@Schema(description = "预约数据统计") +public class BookingStatistics { + + @Schema(description = "统计日期", example = "2026-01-01") + private LocalDate date; + + @Schema(description = "预约总数", example = "100") + private Integer totalBookings; + + @Schema(description = "取消预约数", example = "10") + private Integer cancelledBookings; + + @Schema(description = "实际到场数", example = "85") + private Integer attendedBookings; + + @Schema(description = "预约成功率", example = "0.85") + private Double bookingSuccessRate; + + @Schema(description = "取消率", example = "0.10") + private Double cancellationRate; + + @Schema(description = "到场率", example = "0.85") + private Double attendanceRate; + + @Schema(description = "热门课程名称", example = "动感单车") + private String popularCourseName; + + @Schema(description = "热门课程预约数", example = "25") + private Integer popularCourseCount; + + @Schema(description = "团课总数", example = "20") + private Integer totalCourses; + + @Schema(description = "满员课程数", example = "5") + private Integer fullCourses; + + @Schema(description = "开始日期(用于汇总统计)") + private LocalDate startDate; + + @Schema(description = "结束日期(用于汇总统计)") + private LocalDate endDate; + + public BookingStatistics() { + } + + public BookingStatistics(LocalDate date) { + this.date = date; + this.totalBookings = 0; + this.cancelledBookings = 0; + this.attendedBookings = 0; + this.bookingSuccessRate = 0.0; + this.cancellationRate = 0.0; + this.attendanceRate = 0.0; + this.popularCourseName = ""; + this.popularCourseCount = 0; + this.totalCourses = 0; + this.fullCourses = 0; + } + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public Integer getTotalBookings() { + return totalBookings; + } + + public void setTotalBookings(Integer totalBookings) { + this.totalBookings = totalBookings; + } + + public Integer getCancelledBookings() { + return cancelledBookings; + } + + public void setCancelledBookings(Integer cancelledBookings) { + this.cancelledBookings = cancelledBookings; + } + + public Integer getAttendedBookings() { + return attendedBookings; + } + + public void setAttendedBookings(Integer attendedBookings) { + this.attendedBookings = attendedBookings; + } + + public Double getBookingSuccessRate() { + return bookingSuccessRate; + } + + public void setBookingSuccessRate(Double bookingSuccessRate) { + this.bookingSuccessRate = bookingSuccessRate; + } + + public Double getCancellationRate() { + return cancellationRate; + } + + public void setCancellationRate(Double cancellationRate) { + this.cancellationRate = cancellationRate; + } + + public Double getAttendanceRate() { + return attendanceRate; + } + + public void setAttendanceRate(Double attendanceRate) { + this.attendanceRate = attendanceRate; + } + + public String getPopularCourseName() { + return popularCourseName; + } + + public void setPopularCourseName(String popularCourseName) { + this.popularCourseName = popularCourseName; + } + + public Integer getPopularCourseCount() { + return popularCourseCount; + } + + public void setPopularCourseCount(Integer popularCourseCount) { + this.popularCourseCount = popularCourseCount; + } + + public Integer getTotalCourses() { + return totalCourses; + } + + public void setTotalCourses(Integer totalCourses) { + this.totalCourses = totalCourses; + } + + public Integer getFullCourses() { + return fullCourses; + } + + public void setFullCourses(Integer fullCourses) { + this.fullCourses = fullCourses; + } + + 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/MemberStatistics.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/MemberStatistics.java new file mode 100644 index 0000000..3a31390 --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/MemberStatistics.java @@ -0,0 +1,145 @@ +package cn.novalon.gym.manage.datacount.domain; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDate; + +@Schema(description = "会员数据统计") +public class MemberStatistics { + + @Schema(description = "统计日期", example = "2026-01-01") + private LocalDate date; + + @Schema(description = "新增会员数", example = "10") + private Integer newMembers; + + @Schema(description = "活跃会员数", example = "50") + private Integer activeMembers; + + @Schema(description = "留存会员数", example = "45") + private Integer retainedMembers; + + @Schema(description = "总会员数", example = "200") + private Integer totalMembers; + + @Schema(description = "男性会员数", example = "100") + private Integer maleMembers; + + @Schema(description = "女性会员数", example = "100") + private Integer femaleMembers; + + @Schema(description = "会员卡购买人数", example = "30") + private Integer cardPurchaseCount; + + @Schema(description = "平均年龄", example = "28") + private Double averageAge; + + @Schema(description = "开始日期(用于汇总统计)") + private LocalDate startDate; + + @Schema(description = "结束日期(用于汇总统计)") + private LocalDate endDate; + + public MemberStatistics() { + } + + public MemberStatistics(LocalDate date) { + this.date = date; + this.newMembers = 0; + this.activeMembers = 0; + this.retainedMembers = 0; + this.totalMembers = 0; + this.maleMembers = 0; + this.femaleMembers = 0; + this.cardPurchaseCount = 0; + this.averageAge = 0.0; + } + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public Integer getNewMembers() { + return newMembers; + } + + public void setNewMembers(Integer newMembers) { + this.newMembers = newMembers; + } + + public Integer getActiveMembers() { + return activeMembers; + } + + public void setActiveMembers(Integer activeMembers) { + this.activeMembers = activeMembers; + } + + public Integer getRetainedMembers() { + return retainedMembers; + } + + public void setRetainedMembers(Integer retainedMembers) { + this.retainedMembers = retainedMembers; + } + + public Integer getTotalMembers() { + return totalMembers; + } + + public void setTotalMembers(Integer totalMembers) { + this.totalMembers = totalMembers; + } + + public Integer getMaleMembers() { + return maleMembers; + } + + public void setMaleMembers(Integer maleMembers) { + this.maleMembers = maleMembers; + } + + public Integer getFemaleMembers() { + return femaleMembers; + } + + public void setFemaleMembers(Integer femaleMembers) { + this.femaleMembers = femaleMembers; + } + + public Integer getCardPurchaseCount() { + return cardPurchaseCount; + } + + public void setCardPurchaseCount(Integer cardPurchaseCount) { + this.cardPurchaseCount = cardPurchaseCount; + } + + public Double getAverageAge() { + return averageAge; + } + + public void setAverageAge(Double averageAge) { + this.averageAge = averageAge; + } + + 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 new file mode 100644 index 0000000..ab38a15 --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/StatisticsExportDTO.java @@ -0,0 +1,90 @@ +package cn.novalon.gym.manage.datacount.domain; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDate; +import java.util.List; + +@Schema(description = "统计数据导出DTO") +public class StatisticsExportDTO { + + @Schema(description = "导出文件名", example = "statistics_report_20260101") + private String fileName; + + @Schema(description = "导出格式", example = "EXCEL") + private String format; + + @Schema(description = "开始日期", example = "2026-01-01") + private LocalDate startDate; + + @Schema(description = "结束日期", example = "2026-01-31") + private LocalDate endDate; + + @Schema(description = "导出类型", example = "MEMBER") + private String exportType; + + @Schema(description = "会员统计数据") + private List memberStatisticsList; + + @Schema(description = "预约统计数据") + private List bookingStatisticsList; + + public StatisticsExportDTO() { + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } + + 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; + } + + public String getExportType() { + return exportType; + } + + public void setExportType(String exportType) { + this.exportType = exportType; + } + + public List getMemberStatisticsList() { + return memberStatisticsList; + } + + public void setMemberStatisticsList(List memberStatisticsList) { + this.memberStatisticsList = memberStatisticsList; + } + + public List getBookingStatisticsList() { + return bookingStatisticsList; + } + + public void setBookingStatisticsList(List bookingStatisticsList) { + this.bookingStatisticsList = bookingStatisticsList; + } +} \ 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 new file mode 100644 index 0000000..a29e798 --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/handler/DataCountHandler.java @@ -0,0 +1,204 @@ +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.service.IDataCountService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +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.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + +@Component +@Tag(name = "数据统计", description = "数据统计相关操作") +public class DataCountHandler { + + private final IDataCountService dataCountService; + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + public DataCountHandler(IDataCountService dataCountService) { + this.dataCountService = dataCountService; + } + + @Operation(summary = "获取会员数据统计(按日期)", description = "获取指定日期的会员数据统计") + public Mono getMemberStatisticsByDate(ServerRequest request) { + String dateStr = request.pathVariable("date"); + LocalDate date = LocalDate.parse(dateStr, DATE_FORMATTER); + + return dataCountService.getMemberStatisticsByDate(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 getMemberStatisticsByDateRange(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.getMemberStatisticsByDateRange(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 getMemberStatisticsSummary(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.getMemberStatisticsSummary(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 = "获取指定日期的预约数据统计") + public Mono getBookingStatisticsByDate(ServerRequest request) { + String dateStr = request.pathVariable("date"); + LocalDate date = LocalDate.parse(dateStr, DATE_FORMATTER); + + return dataCountService.getBookingStatisticsByDate(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 getBookingStatisticsByDateRange(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.getBookingStatisticsByDateRange(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 getBookingStatisticsSummary(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.getBookingStatisticsSummary(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 exportMemberStatistics(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 = "member_statistics_" + startDateStr + "_" + endDateStr + ".csv"; + + return dataCountService.exportMemberStatistics(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); + }); + } + + @Operation(summary = "导出预约数据统计", description = "导出指定日期范围内的预约数据统计为CSV文件") + public Mono exportBookingStatistics(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 = "booking_statistics_" + startDateStr + "_" + endDateStr + ".csv"; + + return dataCountService.exportBookingStatistics(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); + }); + } + + @Operation(summary = "导出全部统计数据", description = "导出指定日期范围内的会员和预约数据统计为CSV文件") + public Mono exportAllStatistics(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 = "all_statistics_" + startDateStr + "_" + endDateStr + ".csv"; + + return dataCountService.exportAllStatistics(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 new file mode 100644 index 0000000..73da48c --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/scheduler/StatisticsDailyScheduler.java @@ -0,0 +1,91 @@ +package cn.novalon.gym.manage.datacount.scheduler; + +import cn.novalon.gym.manage.datacount.service.IDataCountService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; + +/** + * 数据统计定时任务 + * + * 功能:每日凌晨1点自动计算前一天的数据统计并缓存到Redis + * + * @author 数据统计模块 + * @date 2026-06-06 + */ +@Component +public class StatisticsDailyScheduler { + + private static final Logger logger = LoggerFactory.getLogger(StatisticsDailyScheduler.class); + + private final IDataCountService dataCountService; + + public StatisticsDailyScheduler(IDataCountService dataCountService) { + this.dataCountService = dataCountService; + } + + /** + * 每日凌晨1点执行前一天的统计数据计算并缓存 + * cron表达式:秒 分 时 日 月 周 + */ + @Scheduled(cron = "0 0 1 * * ?") + public void calculateDailyStatistics() { + LocalDate yesterday = LocalDate.now().minusDays(1); + logger.info("定时任务开始计算 {} 的数据统计", yesterday); + + dataCountService.getMemberStatisticsByDate(yesterday) + .doOnSuccess(stats -> logger.info("会员统计计算完成: date={}, newMembers={}, totalMembers={}", + yesterday, stats.getNewMembers(), stats.getTotalMembers())) + .doOnError(error -> logger.error("会员统计计算失败: date={}", yesterday, error)) + .subscribe(); + + dataCountService.getBookingStatisticsByDate(yesterday) + .doOnSuccess(stats -> logger.info("预约统计计算完成: date={}, totalBookings={}, attendanceRate={}%", + yesterday, stats.getTotalBookings(), stats.getAttendanceRate())) + .doOnError(error -> logger.error("预约统计计算失败: date={}", yesterday, error)) + .subscribe(); + } + + /** + * 每周一凌晨2点执行上周的数据汇总统计 + */ + @Scheduled(cron = "0 0 2 * * 1") + public void calculateWeeklyStatistics() { + LocalDate lastWeekEnd = LocalDate.now().minusDays(1); + LocalDate lastWeekStart = lastWeekEnd.minusDays(6); + logger.info("定时任务开始计算上周汇总统计: {} ~ {}", lastWeekStart, lastWeekEnd); + + dataCountService.getMemberStatisticsSummary(lastWeekStart, lastWeekEnd) + .doOnSuccess(stats -> logger.info("上周会员汇总统计完成: newMembers={}", stats.getNewMembers())) + .doOnError(error -> logger.error("上周会员汇总统计失败", error)) + .subscribe(); + + dataCountService.getBookingStatisticsSummary(lastWeekStart, lastWeekEnd) + .doOnSuccess(stats -> logger.info("上周预约汇总统计完成: totalBookings={}", stats.getTotalBookings())) + .doOnError(error -> logger.error("上周预约汇总统计失败", error)) + .subscribe(); + } + + /** + * 每月1号凌晨3点执行上月的数据汇总统计 + */ + @Scheduled(cron = "0 0 3 1 * ?") + public void calculateMonthlyStatistics() { + LocalDate lastMonthEnd = LocalDate.now().minusDays(1); + LocalDate lastMonthStart = lastMonthEnd.withDayOfMonth(1); + logger.info("定时任务开始计算上月汇总统计: {} ~ {}", lastMonthStart, lastMonthEnd); + + dataCountService.getMemberStatisticsSummary(lastMonthStart, lastMonthEnd) + .doOnSuccess(stats -> logger.info("上月会员汇总统计完成: newMembers={}", stats.getNewMembers())) + .doOnError(error -> logger.error("上月会员汇总统计失败", error)) + .subscribe(); + + dataCountService.getBookingStatisticsSummary(lastMonthStart, lastMonthEnd) + .doOnSuccess(stats -> logger.info("上月预约汇总统计完成: totalBookings={}", stats.getTotalBookings())) + .doOnError(error -> logger.error("上月预约汇总统计失败", error)) + .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 new file mode 100644 index 0000000..b70e647 --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/IDataCountService.java @@ -0,0 +1,30 @@ +package cn.novalon.gym.manage.datacount.service; + +import cn.novalon.gym.manage.datacount.domain.BookingStatistics; +import cn.novalon.gym.manage.datacount.domain.MemberStatistics; +import cn.novalon.gym.manage.datacount.domain.StatisticsExportDTO; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDate; + +public interface IDataCountService { + + Mono getMemberStatisticsByDate(LocalDate date); + + Flux getMemberStatisticsByDateRange(LocalDate startDate, LocalDate endDate); + + Mono getMemberStatisticsSummary(LocalDate startDate, LocalDate endDate); + + Mono getBookingStatisticsByDate(LocalDate date); + + Flux getBookingStatisticsByDateRange(LocalDate startDate, LocalDate endDate); + + Mono getBookingStatisticsSummary(LocalDate startDate, LocalDate endDate); + + Mono exportMemberStatistics(LocalDate startDate, LocalDate endDate); + + Mono exportBookingStatistics(LocalDate startDate, LocalDate endDate); + + Mono exportAllStatistics(LocalDate startDate, LocalDate endDate); +} \ 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 new file mode 100644 index 0000000..9d12895 --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/impl/DataCountServiceImpl.java @@ -0,0 +1,318 @@ +package cn.novalon.gym.manage.datacount.service.impl; + +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.service.IDataCountService; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +public class DataCountServiceImpl implements IDataCountService { + + private final StatisticsDao statisticsDao; + 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:"; + private static final long CACHE_EXPIRE_SECONDS = 3600; + + public DataCountServiceImpl(StatisticsDao statisticsDao, RedisUtil redisUtil, ObjectMapper objectMapper) { + this.statisticsDao = statisticsDao; + this.redisUtil = redisUtil; + this.objectMapper = objectMapper; + } + + @Override + public Mono getMemberStatisticsByDate(LocalDate date) { + String cacheKey = MEMBER_STATS_CACHE_PREFIX + date.toString(); + return redisUtil.get(cacheKey) + .flatMap(cached -> { + if (cached != null) { + try { + MemberStatistics stats = objectMapper.readValue(cached.toString(), MemberStatistics.class); + log.debug("从缓存获取会员统计数据, date: {}", date); + return Mono.just(stats); + } catch (Exception e) { + log.warn("缓存数据解析失败, date: {}", date, e); + } + } + return Mono.empty(); + }) + .switchIfEmpty(fetchMemberStatisticsByDate(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 fetchMemberStatisticsByDate(LocalDate date) { + return Mono.zip( + statisticsDao.countTotalMembers(), + statisticsDao.countNewMembersByDate(date), + statisticsDao.countActiveMembersByDate(date), + statisticsDao.countMaleMembers(), + statisticsDao.countFemaleMembers(), + statisticsDao.countCardPurchaseByDate(date), + statisticsDao.calculateAverageAge() + ).map(tuple -> { + MemberStatistics stats = new MemberStatistics(date); + stats.setTotalMembers(tuple.getT1()); + stats.setNewMembers(tuple.getT2()); + stats.setActiveMembers(tuple.getT3()); + stats.setMaleMembers(tuple.getT4()); + stats.setFemaleMembers(tuple.getT5()); + stats.setCardPurchaseCount(tuple.getT6()); + stats.setAverageAge(tuple.getT7() != null ? tuple.getT7() : 0.0); + return stats; + }); + } + + @Override + public Flux getMemberStatisticsByDateRange(LocalDate startDate, LocalDate endDate) { + return statisticsDao.findMemberStatisticsByDateRange(startDate, endDate) + .map(row -> { + LocalDate date = (LocalDate) row[0]; + MemberStatistics stats = new MemberStatistics(date); + stats.setNewMembers(((Number) row[1]).intValue()); + return stats; + }); + } + + @Override + public Mono getMemberStatisticsSummary(LocalDate startDate, LocalDate endDate) { + return getMemberStatisticsByDateRange(startDate, endDate) + .collectList() + .flatMap(list -> { + MemberStatistics summary = new MemberStatistics(); + summary.setStartDate(startDate); + summary.setEndDate(endDate); + summary.setNewMembers(list.stream().mapToInt(MemberStatistics::getNewMembers).sum()); + + return Mono.zip( + statisticsDao.countTotalMembers(), + statisticsDao.countMaleMembers(), + statisticsDao.countFemaleMembers(), + statisticsDao.calculateAverageAge() + ).map(tuple -> { + summary.setTotalMembers(tuple.getT1()); + summary.setMaleMembers(tuple.getT2()); + summary.setFemaleMembers(tuple.getT3()); + summary.setAverageAge(tuple.getT4() != null ? tuple.getT4() : 0.0); + return summary; + }); + }); + } + + @Override + public Mono getBookingStatisticsByDate(LocalDate date) { + String cacheKey = BOOKING_STATS_CACHE_PREFIX + date.toString(); + return redisUtil.get(cacheKey) + .flatMap(cached -> { + if (cached != null) { + try { + BookingStatistics stats = objectMapper.readValue(cached.toString(), BookingStatistics.class); + log.debug("从缓存获取预约统计数据, date: {}", date); + return Mono.just(stats); + } catch (Exception e) { + log.warn("缓存数据解析失败, date: {}", date, e); + } + } + return Mono.empty(); + }) + .switchIfEmpty(fetchBookingStatisticsByDate(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 fetchBookingStatisticsByDate(LocalDate date) { + return Mono.zip( + statisticsDao.countTotalBookingsByDate(date), + statisticsDao.countCancelledBookingsByDate(date), + statisticsDao.countAttendedBookingsByDate(date), + statisticsDao.countTotalCoursesByDate(date), + statisticsDao.countFullCoursesByDate(date), + statisticsDao.findMostPopularCourseByDate(date) + ).map(tuple -> { + int totalBookings = tuple.getT1(); + int cancelledBookings = tuple.getT2(); + int attendedBookings = tuple.getT3(); + + BookingStatistics stats = new BookingStatistics(date); + stats.setTotalBookings(totalBookings); + stats.setCancelledBookings(cancelledBookings); + stats.setAttendedBookings(attendedBookings); + stats.setTotalCourses(tuple.getT4()); + stats.setFullCourses(tuple.getT5()); + + if (totalBookings > 0) { + stats.setCancellationRate(Math.round(cancelledBookings * 10000.0 / totalBookings) / 100.0); + stats.setAttendanceRate(Math.round(attendedBookings * 10000.0 / totalBookings) / 100.0); + } + + Object[] popularCourse = tuple.getT6(); + if (popularCourse != null && popularCourse.length >= 2) { + stats.setPopularCourseName((String) popularCourse[0]); + stats.setPopularCourseCount(((Number) popularCourse[1]).intValue()); + } + + return stats; + }); + } + + @Override + public Flux getBookingStatisticsByDateRange(LocalDate startDate, LocalDate endDate) { + return statisticsDao.findBookingStatisticsByDateRange(startDate, endDate) + .map(row -> { + LocalDate date = (LocalDate) row[0]; + int totalBookings = ((Number) row[1]).intValue(); + int cancelledBookings = ((Number) row[2]).intValue(); + int attendedBookings = ((Number) row[3]).intValue(); + + BookingStatistics stats = new BookingStatistics(date); + stats.setTotalBookings(totalBookings); + stats.setCancelledBookings(cancelledBookings); + stats.setAttendedBookings(attendedBookings); + + if (totalBookings > 0) { + stats.setCancellationRate(Math.round(cancelledBookings * 10000.0 / totalBookings) / 100.0); + stats.setAttendanceRate(Math.round(attendedBookings * 10000.0 / totalBookings) / 100.0); + } + + return stats; + }); + } + + @Override + public Mono getBookingStatisticsSummary(LocalDate startDate, LocalDate endDate) { + return getBookingStatisticsByDateRange(startDate, endDate) + .collectList() + .map(list -> { + BookingStatistics summary = new BookingStatistics(); + summary.setStartDate(startDate); + summary.setEndDate(endDate); + summary.setTotalBookings(list.stream().mapToInt(BookingStatistics::getTotalBookings).sum()); + summary.setCancelledBookings(list.stream().mapToInt(BookingStatistics::getCancelledBookings).sum()); + summary.setAttendedBookings(list.stream().mapToInt(BookingStatistics::getAttendedBookings).sum()); + + int total = summary.getTotalBookings(); + if (total > 0) { + summary.setCancellationRate(Math.round(summary.getCancelledBookings() * 10000.0 / total) / 100.0); + summary.setAttendanceRate(Math.round(summary.getAttendedBookings() * 10000.0 / total) / 100.0); + } + + return summary; + }); + } + + @Override + public Mono exportMemberStatistics(LocalDate startDate, LocalDate endDate) { + return getMemberStatisticsByDateRange(startDate, endDate) + .collectList() + .map(this::generateMemberStatisticsCSV); + } + + @Override + public Mono exportBookingStatistics(LocalDate startDate, LocalDate endDate) { + return getBookingStatisticsByDateRange(startDate, endDate) + .collectList() + .map(this::generateBookingStatisticsCSV); + } + + @Override + public Mono exportAllStatistics(LocalDate startDate, LocalDate endDate) { + return Mono.zip( + getMemberStatisticsByDateRange(startDate, endDate).collectList(), + getBookingStatisticsByDateRange(startDate, endDate).collectList() + ).map(tuple -> { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintWriter writer = new PrintWriter(new OutputStreamWriter(baos, StandardCharsets.UTF_8)); + + writer.println("=== 会员数据统计 ==="); + writer.println(generateMemberStatisticsCSVContent(tuple.getT1())); + writer.println(); + writer.println("=== 预约数据统计 ==="); + writer.println(generateBookingStatisticsCSVContent(tuple.getT2())); + + writer.flush(); + return baos.toByteArray(); + }); + } + + private byte[] generateMemberStatisticsCSV(List statsList) { + return generateMemberStatisticsCSVContent(statsList).getBytes(StandardCharsets.UTF_8); + } + + private String generateMemberStatisticsCSVContent(List statsList) { + StringBuilder sb = new StringBuilder(); + sb.append("统计日期,新增会员数,活跃会员数,总会员数,男性会员数,女性会员数,购卡人数,平均年龄\n"); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + for (MemberStatistics stats : statsList) { + sb.append(stats.getDate().format(formatter)).append(","); + sb.append(stats.getNewMembers()).append(","); + sb.append(stats.getActiveMembers()).append(","); + sb.append(stats.getTotalMembers()).append(","); + sb.append(stats.getMaleMembers()).append(","); + sb.append(stats.getFemaleMembers()).append(","); + sb.append(stats.getCardPurchaseCount()).append(","); + sb.append(String.format("%.1f", stats.getAverageAge())).append("\n"); + } + + return sb.toString(); + } + + private byte[] generateBookingStatisticsCSV(List statsList) { + return generateBookingStatisticsCSVContent(statsList).getBytes(StandardCharsets.UTF_8); + } + + private String generateBookingStatisticsCSVContent(List statsList) { + StringBuilder sb = new StringBuilder(); + sb.append("统计日期,预约总数,取消预约数,实际到场数,取消率(%),到场率(%),热门课程,热门课程预约数,团课总数,满员课程数\n"); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + for (BookingStatistics stats : statsList) { + sb.append(stats.getDate().format(formatter)).append(","); + sb.append(stats.getTotalBookings()).append(","); + sb.append(stats.getCancelledBookings()).append(","); + sb.append(stats.getAttendedBookings()).append(","); + sb.append(String.format("%.2f", stats.getCancellationRate())).append(","); + sb.append(String.format("%.2f", stats.getAttendanceRate())).append(","); + sb.append(stats.getPopularCourseName() != null ? stats.getPopularCourseName() : "").append(","); + sb.append(stats.getPopularCourseCount()).append(","); + sb.append(stats.getTotalCourses()).append(","); + sb.append(stats.getFullCourses()).append("\n"); + } + + return sb.toString(); + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-dataCount/src/main/resources/application.yaml b/gym-manage-api/gym-dataCount/src/main/resources/application.yaml new file mode 100644 index 0000000..9d9181a --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/resources/application.yaml @@ -0,0 +1,3 @@ +spring: + application: + name: gym-dataCount diff --git a/gym-manage-api/gym-dataCount/src/test/java/cn/novalon/gym/manage/datacount/GymDataCountApplicationTests.java b/gym-manage-api/gym-dataCount/src/test/java/cn/novalon/gym/manage/datacount/GymDataCountApplicationTests.java new file mode 100644 index 0000000..6e5392e --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/test/java/cn/novalon/gym/manage/datacount/GymDataCountApplicationTests.java @@ -0,0 +1,13 @@ +package cn.novalon.gym.manage.datacount; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class GymDataCountApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/gym-manage-api/log.text b/gym-manage-api/log.text index c2deea4..6ace75f 100644 --- a/gym-manage-api/log.text +++ b/gym-manage-api/log.text @@ -1,200 +1,19 @@ -2026-06-02T14:56:05.966+08:00 TRACE 22380 --- [gym-manage-api] [ctor-http-nio-7] o.s.w.s.adapter.HttpWebHandlerAdapter : [7ce54381-10] HTTP POST "/api/groupCourse/11/cancel", headers={masked} -2026-06-02T14:56:05.967+08:00 DEBUG 22380 --- [gym-manage-api] [ parallel-11] o.s.w.s.s.DefaultWebSessionManager : Created new WebSession. -2026-06-02T14:56:05.967+08:00 INFO 22380 --- [gym-manage-api] [ parallel-11] c.n.g.m.sys.audit.OperationLogWebFilter : WebFilter 拦截请求: POST /api/groupCourse/11/cancel -2026-06-02T14:56:05.967+08:00 INFO 22380 --- [gym-manage-api] [ parallel-11] c.n.g.m.sys.audit.OperationLogWebFilter : 未匹配到操作日志配置,跳过: POST /api/groupCourse/11/cancel -2026-06-02T14:56:05.967+08:00 INFO 22380 --- [gym-manage-api] [ parallel-11] c.n.g.m.sys.audit.OperationLogWebFilter : WebFilter 拦截请求: POST /api/groupCourse/11/cancel -2026-06-02T14:56:05.967+08:00 INFO 22380 --- [gym-manage-api] [ parallel-11] c.n.g.m.sys.audit.OperationLogWebFilter : 未匹配到操作日志配置,跳过: POST /api/groupCourse/11/cancel -2026-06-02T14:56:05.967+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.967+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.967+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/dictionaries" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/{id}/action/change-password" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/{id}/action/logical-delete" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/logical-delete" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/action/restore" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/{id}/action/restore" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/{id}/roles" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/menus" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/roles" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/roles/{id}/restore" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/roles/{id}/permissions" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/config" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/logs/login" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/logs/exception" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/logs/operation" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/auth/login" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/auth/register" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/auth/logout" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/dict/types" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/dict/data" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/notices" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/messages" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/files/upload" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/permissions" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member/auth/miniapp/login" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member/auth/mp/callback" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member/phone/bind" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/admin/member/{id}/phone" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-cards" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/purchase" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/{recordId}/renew" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/{recordId}/use" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/{recordId}/refund" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/process-expired" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-transactions" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/groupCourse/page" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/groupCourse" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/groupCourse/{id}/cancel" matches against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.function.server.RouterFunctions : [7ce54381-10] Matched (POST && /api/groupCourse/{id}/cancel) -2026-06-02T14:56:05.970+08:00 DEBUG 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.s.s.RouterFunctionMapping : [7ce54381-10] Mapped to cn.novalon.gym.manage.app.config.SystemRouter$$Lambda/0x00000198019592d8@6903bec9 -2026-06-02T14:56:05.972+08:00 DEBUG 22380 --- [gym-manage-api] [ parallel-11] o.s.r2dbc.core.DefaultDatabaseClient : Executing SQL statement [SELECT group_course.course_name, group_course.coach_id, group_course.course_type, group_course.start_time, group_course.end_time, group_course.max_members, group_course.current_members, group_course.status, group_course.location, group_course.cover_image, group_course.description, group_course.point_card_amount, group_course.stored_value_amount, group_course.id, group_course.create_by, group_course.update_by, group_course.created_at, group_course.updated_at, group_course.deleted_at FROM group_course WHERE group_course.id = $1 AND (group_course.deleted_at IS NULL)] -2026-06-02T14:56:05.976+08:00 INFO 22380 --- [gym-manage-api] [actor-tcp-nio-5] c.n.g.m.g.c.GroupCourseConverter : 转换bean,entity-domain: -2026-06-02T14:56:05.976+08:00 ERROR 22380 --- [gym-manage-api] [actor-tcp-nio-5] c.n.g.m.g.s.impl.GroupCourseService : 团课取消失败 - id=11, error: 课程取消需提前24小时 -2026-06-02T14:56:05.976+08:00 TRACE 22380 --- [gym-manage-api] [actor-tcp-nio-5] org.springframework.web.HttpLogging : [7ce54381-10] Encoding [{success=false, message=课程取消需提前24小时}] -2026-06-02T14:56:05.976+08:00 TRACE 22380 --- [gym-manage-api] [actor-tcp-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [7ce54381-10] Completed 400 BAD_REQUEST, headers={masked} -2026-06-02T14:56:05.976+08:00 TRACE 22380 --- [gym-manage-api] [actor-tcp-nio-5] org.springframework.web.HttpLogging : [7ce54381-1, L:/[0:0:0:0:0:0:0:1]:8084 - R:/[0:0:0:0:0:0:0:1]:57674] Handling completed +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 : 没有需要处理的缺席会员记录 diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml index 11b6479..1aa0195 100644 --- a/gym-manage-api/manage-app/pom.xml +++ b/gym-manage-api/manage-app/pom.xml @@ -149,6 +149,12 @@ 1.0.0 compile + + cn.novalon.gym.manage + gym-dataCount + 1.0.0 + compile + diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Insert_DataCount_Test_Data.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Insert_DataCount_Test_Data.sql new file mode 100644 index 0000000..91da472 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Insert_DataCount_Test_Data.sql @@ -0,0 +1,443 @@ +-- V13: 数据统计模块测试数据 +-- 测试场景: +-- 1. 会员数据统计(新增会员、活跃会员、性别分布、平均年龄、购卡人数) +-- 2. 预约数据统计(预约总数、取消预约、实际到场、热门课程、满员课程) +-- 3. 数据导出功能测试 +-- 日期范围:2026-06-01 至 2026-06-10 + +-- ============================================ +-- 1. 创建测试会员用户(分布在不同日期注册,不同性别,不同生日) +-- ============================================ + +-- 2026-06-01 注册的会员(5人:3男2女) +INSERT INTO member_user (member_no, nickname, phone, gender, birthday, is_deleted, created_at, updated_at, last_login_at) VALUES + ('MEM_STAT_001', '测试会员_男1', '13900001001', 1, '1995-03-15', false, '2026-06-01 09:00:00', '2026-06-01 09:00:00', '2026-06-05 20:00:00'), + ('MEM_STAT_002', '测试会员_男2', '13900001002', 1, '1990-07-20', false, '2026-06-01 10:30:00', '2026-06-01 10:30:00', '2026-06-06 18:00:00'), + ('MEM_STAT_003', '测试会员_男3', '13900001003', 1, '1988-11-08', false, '2026-06-01 14:00:00', '2026-06-01 14:00:00', '2026-06-04 19:30:00'), + ('MEM_STAT_004', '测试会员_女1', '13900001004', 2, '1998-01-25', false, '2026-06-01 16:00:00', '2026-06-01 16:00:00', '2026-06-06 08:00:00'), + ('MEM_STAT_005', '测试会员_女2', '13900001005', 2, '1992-05-10', false, '2026-06-01 18:00:00', '2026-06-01 18:00:00', '2026-06-03 21:00:00'); + +-- 2026-06-02 注册的会员(4人:2男2女) +INSERT INTO member_user (member_no, nickname, phone, gender, birthday, is_deleted, created_at, updated_at, last_login_at) VALUES + ('MEM_STAT_006', '测试会员_男4', '13900001006', 1, '1985-09-12', false, '2026-06-02 09:00:00', '2026-06-02 09:00:00', '2026-06-06 10:00:00'), + ('MEM_STAT_007', '测试会员_男5', '13900001007', 1, '1996-04-18', false, '2026-06-02 11:00:00', '2026-06-02 11:00:00', '2026-06-05 15:00:00'), + ('MEM_STAT_008', '测试会员_女3', '13900001008', 2, '1993-08-30', false, '2026-06-02 13:00:00', '2026-06-02 13:00:00', '2026-06-06 12:00:00'), + ('MEM_STAT_009', '测试会员_女4', '13900001009', 2, '1991-12-05', false, '2026-06-02 15:00:00', '2026-06-02 15:00:00', '2026-06-04 14:00:00'); + +-- 2026-06-03 注册的会员(3人:1男2女) +INSERT INTO member_user (member_no, nickname, phone, gender, birthday, is_deleted, created_at, updated_at, last_login_at) VALUES + ('MEM_STAT_010', '测试会员_男6', '13900001010', 1, '1989-06-22', false, '2026-06-03 10:00:00', '2026-06-03 10:00:00', '2026-06-06 09:00:00'), + ('MEM_STAT_011', '测试会员_女5', '13900001011', 2, '1997-02-14', false, '2026-06-03 14:00:00', '2026-06-03 14:00:00', '2026-06-05 16:00:00'), + ('MEM_STAT_012', '测试会员_女6', '13900001012', 2, '1994-10-28', false, '2026-06-03 16:00:00', '2026-06-03 16:00:00', '2026-06-06 11:00:00'); + +-- 2026-06-04 注册的会员(2人:1男1女) +INSERT INTO member_user (member_no, nickname, phone, gender, birthday, is_deleted, created_at, updated_at, last_login_at) VALUES + ('MEM_STAT_013', '测试会员_男7', '13900001013', 1, '1987-01-03', false, '2026-06-04 09:00:00', '2026-06-04 09:00:00', '2026-06-06 13:00:00'), + ('MEM_STAT_014', '测试会员_女7', '13900001014', 2, '1999-07-17', false, '2026-06-04 11:00:00', '2026-06-04 11:00:00', '2026-06-05 17:00:00'); + +-- 2026-06-05 注册的会员(3人:2男1女) +INSERT INTO member_user (member_no, nickname, phone, gender, birthday, is_deleted, created_at, updated_at, last_login_at) VALUES + ('MEM_STAT_015', '测试会员_男8', '13900001015', 1, '1990-03-09', false, '2026-06-05 10:00:00', '2026-06-05 10:00:00', '2026-06-06 14:00:00'), + ('MEM_STAT_016', '测试会员_男9', '13900001016', 1, '1986-11-21', false, '2026-06-05 13:00:00', '2026-06-05 13:00:00', '2026-06-06 15:00:00'), + ('MEM_STAT_017', '测试会员_女8', '13900001017', 2, '1995-09-06', false, '2026-06-05 15:00:00', '2026-06-05 15:00:00', '2026-06-05 18:00:00'); + +-- 2026-06-06 注册的会员(2人:1男1女) +INSERT INTO member_user (member_no, nickname, phone, gender, birthday, is_deleted, created_at, updated_at, last_login_at) VALUES + ('MEM_STAT_018', '测试会员_男10', '13900001018', 1, '1993-05-14', false, '2026-06-06 09:00:00', '2026-06-06 09:00:00', '2026-06-06 16:00:00'), + ('MEM_STAT_019', '测试会员_女9', '13900001019', 2, '1991-08-19', false, '2026-06-06 11:00:00', '2026-06-06 11:00:00', '2026-06-06 17:00:00'); + +-- 已删除的会员(1人,用于测试软删除过滤) +INSERT INTO member_user (member_no, nickname, phone, gender, birthday, is_deleted, created_at, updated_at, last_login_at) VALUES + ('MEM_STAT_DEL', '已删除会员', '13900001999', 1, '1980-01-01', true, '2026-06-01 08:00:00', '2026-06-01 08:00:00', '2026-06-01 08:00:00'); + +-- ============================================ +-- 2. 创建会员卡类型(如果不存在) +-- ============================================ + +INSERT INTO member_card (member_card_name, member_card_type, member_card_price, member_card_validity_days, member_card_total_times, member_card_amount, member_card_status, extra_config, created_at, updated_at) +SELECT '月卡', 'TIME_CARD', 299.00, 30, NULL, NULL, 1, '{}', '2026-01-01 00:00:00', '2026-01-01 00:00:00' +WHERE NOT EXISTS (SELECT 1 FROM member_card WHERE member_card_name = '月卡'); + +INSERT INTO member_card (member_card_name, member_card_type, member_card_price, member_card_validity_days, member_card_total_times, member_card_amount, member_card_status, extra_config, created_at, updated_at) +SELECT '季卡', 'TIME_CARD', 799.00, 90, NULL, NULL, 1, '{}', '2026-01-01 00:00:00', '2026-01-01 00:00:00' +WHERE NOT EXISTS (SELECT 1 FROM member_card WHERE member_card_name = '季卡'); + +INSERT INTO member_card (member_card_name, member_card_type, member_card_price, member_card_validity_days, member_card_total_times, member_card_amount, member_card_status, extra_config, created_at, updated_at) +SELECT '次卡20次', 'COUNT_CARD', 599.00, NULL, 20, NULL, 1, '{}', '2026-01-01 00:00:00', '2026-01-01 00:00:00' +WHERE NOT EXISTS (SELECT 1 FROM member_card WHERE member_card_name = '次卡20次'); + +-- ============================================ +-- 3. 为所有测试会员创建会员卡记录 +-- ============================================ + +-- MEM_STAT_001 至 MEM_STAT_019 全部创建会员卡记录 +-- 使用月卡类型,有效期至 2026-12-31 +INSERT INTO member_card_record (member_id, member_card_id, status, remaining_times, remaining_amount, expire_time, purchase_time, version, card_composition, created_at, updated_at) +SELECT + m.id, + (SELECT id FROM member_card WHERE member_card_name = '月卡'), + 'ACTIVE', + 0, + 0.00, + '2026-12-31 23:59:59', + m.created_at, + 0, + '{}', + m.created_at, + m.created_at +FROM member_user m +WHERE m.member_no LIKE 'MEM_STAT_%' AND m.member_no != 'MEM_STAT_DEL'; + +-- ============================================ +-- 4. 创建团课课程(2026-06-01 至 2026-06-10) +-- ============================================ + +-- 2026-06-01 的课程 +INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, description, point_card_amount, stored_value_amount, create_by, created_at, updated_at) VALUES + ('晨间瑜伽', 101, 1, '2026-06-01 07:00:00', '2026-06-01 08:00:00', 15, 12, '0', '瑜伽教室A', '清晨唤醒瑜伽', 1, 30.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('燃脂单车', 102, 2, '2026-06-01 18:00:00', '2026-06-01 19:00:00', 25, 25, '0', '单车房', '高强度燃脂骑行', 1, 35.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('普拉提核心', 103, 1, '2026-06-01 19:30:00', '2026-06-01 20:30:00', 12, 8, '0', '普拉提教室', '核心力量训练', 1, 40.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'); + +-- 2026-06-02 的课程 +INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, description, point_card_amount, stored_value_amount, create_by, created_at, updated_at) VALUES + ('流瑜伽', 101, 1, '2026-06-02 09:00:00', '2026-06-02 10:30:00', 15, 10, '0', '瑜伽教室A', '流畅体式连接', 1, 30.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('搏击操', 102, 2, '2026-06-02 18:30:00', '2026-06-02 19:30:00', 20, 18, '0', '综合训练区', '有氧搏击训练', 1, 35.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('冥想放松', 101, 1, '2026-06-02 20:00:00', '2026-06-02 21:00:00', 20, 5, '0', '冥想室', '深度放松冥想', 1, 25.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'); + +-- 2026-06-03 的课程 +INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, description, point_card_amount, stored_value_amount, create_by, created_at, updated_at) VALUES + ('哈他瑜伽', 101, 1, '2026-06-03 10:00:00', '2026-06-03 11:30:00', 12, 12, '0', '瑜伽教室B', '基础哈他瑜伽', 1, 30.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('动感单车', 102, 2, '2026-06-03 19:00:00', '2026-06-03 20:00:00', 25, 20, '0', '单车房', '音乐节奏骑行', 1, 35.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('拉伸放松', 103, 1, '2026-06-03 20:30:00', '2026-06-03 21:30:00', 15, 6, '0', '综合训练区', '全身拉伸放松', 1, 25.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'); + +-- 2026-06-04 的课程 +INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, description, point_card_amount, stored_value_amount, create_by, created_at, updated_at) VALUES + ('力量瑜伽', 101, 1, '2026-06-04 08:00:00', '2026-06-04 09:30:00', 15, 14, '0', '瑜伽教室A', '力量型瑜伽', 1, 35.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('HIIT训练', 102, 2, '2026-06-04 18:00:00', '2026-06-04 19:00:00', 20, 20, '0', '综合训练区', '高强度间歇训练', 1, 40.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('阴瑜伽', 101, 1, '2026-06-04 20:00:00', '2026-06-04 21:30:00', 12, 9, '0', '瑜伽教室B', '静态阴瑜伽', 1, 30.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'); + +-- 2026-06-05 的课程 +INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, description, point_card_amount, stored_value_amount, create_by, created_at, updated_at) VALUES + ('晨间冥想', 101, 1, '2026-06-05 07:00:00', '2026-06-05 08:00:00', 20, 15, '0', '冥想室', '清晨冥想', 1, 25.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('尊巴舞', 103, 2, '2026-06-05 18:30:00', '2026-06-05 19:30:00', 25, 22, '0', '舞蹈室', '拉丁风格舞蹈', 1, 35.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('空中瑜伽', 101, 1, '2026-06-05 20:00:00', '2026-06-05 21:30:00', 10, 10, '0', '瑜伽教室A', '空中瑜伽体验', 1, 45.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'); + +-- 2026-06-06 的课程 +INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, description, point_card_amount, stored_value_amount, create_by, created_at, updated_at) VALUES + ('拜日式瑜伽', 101, 1, '2026-06-06 07:30:00', '2026-06-06 08:30:00', 15, 11, '0', '瑜伽教室A', '传统拜日式', 1, 30.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('杠铃操', 102, 2, '2026-06-06 18:00:00', '2026-06-06 19:00:00', 20, 16, '0', '力量区', '全身杠铃训练', 1, 35.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('修复瑜伽', 101, 1, '2026-06-06 20:00:00', '2026-06-06 21:00:00', 12, 7, '0', '瑜伽教室B', '修复性瑜伽', 1, 30.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'); + +-- ============================================ +-- 5. 团课预约记录(测试预约统计) +-- ============================================ + +-- 2026-06-01 的预约记录 +-- 晨间瑜伽:5人预约(4人出席,1人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) VALUES + ((SELECT id FROM group_course WHERE course_name = '晨间瑜伽' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_001'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_001')), '2026-06-01 06:00:00', '2', '晨间瑜伽', '2026-06-01 07:00:00', '2026-06-01 08:00:00', '瑜伽教室A', '2026-06-01 06:00:00', '2026-06-01 06:00:00'), + ((SELECT id FROM group_course WHERE course_name = '晨间瑜伽' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_002'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_002')), '2026-06-01 06:30:00', '2', '晨间瑜伽', '2026-06-01 07:00:00', '2026-06-01 08:00:00', '瑜伽教室A', '2026-06-01 06:30:00', '2026-06-01 06:30:00'), + ((SELECT id FROM group_course WHERE course_name = '晨间瑜伽' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_003'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_003')), '2026-06-01 06:45:00', '2', '晨间瑜伽', '2026-06-01 07:00:00', '2026-06-01 08:00:00', '瑜伽教室A', '2026-06-01 06:45:00', '2026-06-01 06:45:00'), + ((SELECT id FROM group_course WHERE course_name = '晨间瑜伽' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_004'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_004')), '2026-06-01 06:15:00', '1', '晨间瑜伽', '2026-06-01 07:00:00', '2026-06-01 08:00:00', '瑜伽教室A', '2026-06-01 06:15:00', '2026-06-01 06:15:00'), + ((SELECT id FROM group_course WHERE course_name = '晨间瑜伽' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_005'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_005')), '2026-06-01 06:20:00', '2', '晨间瑜伽', '2026-06-01 07:00:00', '2026-06-01 08:00:00', '瑜伽教室A', '2026-06-01 06:20:00', '2026-06-01 06:20:00'); + +-- 燃脂单车:19人预约(15人出席,4人取消)- 满员课程 +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '燃脂单车' AND DATE(start_time) = '2026-06-01'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-01 17:00:00', + CASE WHEN i <= 15 THEN '2' ELSE '1' END, + '燃脂单车', + '2026-06-01 18:00:00', + '2026-06-01 19:00:00', + '单车房', + '2026-06-01 17:00:00', + '2026-06-01 17:00:00' +FROM generate_series(1, 19) AS i; + +-- 普拉提核心:8人预约(7人出席,1人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) VALUES + ((SELECT id FROM group_course WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_001'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_001')), '2026-06-01 19:00:00', '2', '普拉提核心', '2026-06-01 19:30:00', '2026-06-01 20:30:00', '普拉提教室', '2026-06-01 19:00:00', '2026-06-01 19:00:00'), + ((SELECT id FROM group_course WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_003'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_003')), '2026-06-01 19:10:00', '2', '普拉提核心', '2026-06-01 19:30:00', '2026-06-01 20:30:00', '普拉提教室', '2026-06-01 19:10:00', '2026-06-01 19:10:00'), + ((SELECT id FROM group_course WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_005'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_005')), '2026-06-01 19:05:00', '2', '普拉提核心', '2026-06-01 19:30:00', '2026-06-01 20:30:00', '普拉提教室', '2026-06-01 19:05:00', '2026-06-01 19:05:00'), + ((SELECT id FROM group_course WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_007'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_007')), '2026-06-01 19:15:00', '2', '普拉提核心', '2026-06-01 19:30:00', '2026-06-01 20:30:00', '普拉提教室', '2026-06-01 19:15:00', '2026-06-01 19:15:00'), + ((SELECT id FROM group_course WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_009'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_009')), '2026-06-01 19:20:00', '1', '普拉提核心', '2026-06-01 19:30:00', '2026-06-01 20:30:00', '普拉提教室', '2026-06-01 19:20:00', '2026-06-01 19:20:00'), + ((SELECT id FROM group_course WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_011'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_011')), '2026-06-01 19:25:00', '2', '普拉提核心', '2026-06-01 19:30:00', '2026-06-01 20:30:00', '普拉提教室', '2026-06-01 19:25:00', '2026-06-01 19:25:00'), + ((SELECT id FROM group_course WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_013'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_013')), '2026-06-01 19:30:00', '2', '普拉提核心', '2026-06-01 19:30:00', '2026-06-01 20:30:00', '普拉提教室', '2026-06-01 19:30:00', '2026-06-01 19:30:00'), + ((SELECT id FROM group_course WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_015'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_015')), '2026-06-01 19:35:00', '2', '普拉提核心', '2026-06-01 19:30:00', '2026-06-01 20:30:00', '普拉提教室', '2026-06-01 19:35:00', '2026-06-01 19:35:00'); + +-- 2026-06-02 的预约记录 +-- 流瑜伽:10人预约(9人出席,1人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '流瑜伽' AND DATE(start_time) = '2026-06-02'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-02 08:00:00', + CASE WHEN i <= 9 THEN '2' ELSE '1' END, + '流瑜伽', + '2026-06-02 09:00:00', + '2026-06-02 10:30:00', + '瑜伽教室A', + '2026-06-02 08:00:00', + '2026-06-02 08:00:00' +FROM generate_series(1, 10) AS i; + +-- 搏击操:18人预约(15人出席,3人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '搏击操' AND DATE(start_time) = '2026-06-02'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-02 17:30:00', + CASE WHEN i <= 15 THEN '2' ELSE '1' END, + '搏击操', + '2026-06-02 18:30:00', + '2026-06-02 19:30:00', + '综合训练区', + '2026-06-02 17:30:00', + '2026-06-02 17:30:00' +FROM generate_series(1, 18) AS i; + +-- 冥想放松:5人预约(4人出席,1人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) VALUES + ((SELECT id FROM group_course WHERE course_name = '冥想放松' AND DATE(start_time) = '2026-06-02'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_001'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_001')), '2026-06-02 19:30:00', '2', '冥想放松', '2026-06-02 20:00:00', '2026-06-02 21:00:00', '冥想室', '2026-06-02 19:30:00', '2026-06-02 19:30:00'), + ((SELECT id FROM group_course WHERE course_name = '冥想放松' AND DATE(start_time) = '2026-06-02'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_004'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_004')), '2026-06-02 19:35:00', '2', '冥想放松', '2026-06-02 20:00:00', '2026-06-02 21:00:00', '冥想室', '2026-06-02 19:35:00', '2026-06-02 19:35:00'), + ((SELECT id FROM group_course WHERE course_name = '冥想放松' AND DATE(start_time) = '2026-06-02'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_007'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_007')), '2026-06-02 19:40:00', '2', '冥想放松', '2026-06-02 20:00:00', '2026-06-02 21:00:00', '冥想室', '2026-06-02 19:40:00', '2026-06-02 19:40:00'), + ((SELECT id FROM group_course WHERE course_name = '冥想放松' AND DATE(start_time) = '2026-06-02'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_010'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_010')), '2026-06-02 19:45:00', '1', '冥想放松', '2026-06-02 20:00:00', '2026-06-02 21:00:00', '冥想室', '2026-06-02 19:45:00', '2026-06-02 19:45:00'), + ((SELECT id FROM group_course WHERE course_name = '冥想放松' AND DATE(start_time) = '2026-06-02'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_013'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_013')), '2026-06-02 19:50:00', '2', '冥想放松', '2026-06-02 20:00:00', '2026-06-02 21:00:00', '冥想室', '2026-06-02 19:50:00', '2026-06-02 19:50:00'); + +-- 2026-06-03 的预约记录 +-- 哈他瑜伽:12人预约(10人出席,2人取消)- 满员 +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '哈他瑜伽' AND DATE(start_time) = '2026-06-03'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-03 09:00:00', + CASE WHEN i <= 10 THEN '2' ELSE '1' END, + '哈他瑜伽', + '2026-06-03 10:00:00', + '2026-06-03 11:30:00', + '瑜伽教室B', + '2026-06-03 09:00:00', + '2026-06-03 09:00:00' +FROM generate_series(1, 12) AS i; + +-- 动感单车:19人预约(16人出席,3人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '动感单车' AND DATE(start_time) = '2026-06-03'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-03 18:00:00', + CASE WHEN i <= 16 THEN '2' ELSE '1' END, + '动感单车', + '2026-06-03 19:00:00', + '2026-06-03 20:00:00', + '单车房', + '2026-06-03 18:00:00', + '2026-06-03 18:00:00' +FROM generate_series(1, 19) AS i; + +-- 拉伸放松:6人预约(5人出席,1人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) VALUES + ((SELECT id FROM group_course WHERE course_name = '拉伸放松' AND DATE(start_time) = '2026-06-03'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_002'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_002')), '2026-06-03 20:00:00', '2', '拉伸放松', '2026-06-03 20:30:00', '2026-06-03 21:30:00', '综合训练区', '2026-06-03 20:00:00', '2026-06-03 20:00:00'), + ((SELECT id FROM group_course WHERE course_name = '拉伸放松' AND DATE(start_time) = '2026-06-03'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_005'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_005')), '2026-06-03 20:05:00', '2', '拉伸放松', '2026-06-03 20:30:00', '2026-06-03 21:30:00', '综合训练区', '2026-06-03 20:05:00', '2026-06-03 20:05:00'), + ((SELECT id FROM group_course WHERE course_name = '拉伸放松' AND DATE(start_time) = '2026-06-03'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_008'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_008')), '2026-06-03 20:10:00', '2', '拉伸放松', '2026-06-03 20:30:00', '2026-06-03 21:30:00', '综合训练区', '2026-06-03 20:10:00', '2026-06-03 20:10:00'), + ((SELECT id FROM group_course WHERE course_name = '拉伸放松' AND DATE(start_time) = '2026-06-03'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_011'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_011')), '2026-06-03 20:15:00', '1', '拉伸放松', '2026-06-03 20:30:00', '2026-06-03 21:30:00', '综合训练区', '2026-06-03 20:15:00', '2026-06-03 20:15:00'), + ((SELECT id FROM group_course WHERE course_name = '拉伸放松' AND DATE(start_time) = '2026-06-03'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_014'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_014')), '2026-06-03 20:20:00', '2', '拉伸放松', '2026-06-03 20:30:00', '2026-06-03 21:30:00', '综合训练区', '2026-06-03 20:20:00', '2026-06-03 20:20:00'), + ((SELECT id FROM group_course WHERE course_name = '拉伸放松' AND DATE(start_time) = '2026-06-03'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_017'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_017')), '2026-06-03 20:25:00', '2', '拉伸放松', '2026-06-03 20:30:00', '2026-06-03 21:30:00', '综合训练区', '2026-06-03 20:25:00', '2026-06-03 20:25:00'); + +-- 2026-06-04 的预约记录 +-- 力量瑜伽:14人预约(12人出席,2人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '力量瑜伽' AND DATE(start_time) = '2026-06-04'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-04 07:00:00', + CASE WHEN i <= 12 THEN '2' ELSE '1' END, + '力量瑜伽', + '2026-06-04 08:00:00', + '2026-06-04 09:30:00', + '瑜伽教室A', + '2026-06-04 07:00:00', + '2026-06-04 07:00:00' +FROM generate_series(1, 14) AS i; + +-- HIIT训练:19人预约(16人出席,3人取消)- 满员 +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = 'HIIT训练' AND DATE(start_time) = '2026-06-04'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-04 17:00:00', + CASE WHEN i <= 16 THEN '2' ELSE '1' END, + 'HIIT训练', + '2026-06-04 18:00:00', + '2026-06-04 19:00:00', + '综合训练区', + '2026-06-04 17:00:00', + '2026-06-04 17:00:00' +FROM generate_series(1, 19) AS i; + +-- 阴瑜伽:9人预约(8人出席,1人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '阴瑜伽' AND DATE(start_time) = '2026-06-04'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-04 19:30:00', + CASE WHEN i <= 8 THEN '2' ELSE '1' END, + '阴瑜伽', + '2026-06-04 20:00:00', + '2026-06-04 21:30:00', + '瑜伽教室B', + '2026-06-04 19:30:00', + '2026-06-04 19:30:00' +FROM generate_series(1, 9) AS i; + +-- 2026-06-05 的预约记录 +-- 晨间冥想:15人预约(13人出席,2人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '晨间冥想' AND DATE(start_time) = '2026-06-05'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-05 06:00:00', + CASE WHEN i <= 13 THEN '2' ELSE '1' END, + '晨间冥想', + '2026-06-05 07:00:00', + '2026-06-05 08:00:00', + '冥想室', + '2026-06-05 06:00:00', + '2026-06-05 06:00:00' +FROM generate_series(1, 15) AS i; + +-- 尊巴舞:19人预约(16人出席,3人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '尊巴舞' AND DATE(start_time) = '2026-06-05'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-05 17:30:00', + CASE WHEN i <= 16 THEN '2' ELSE '1' END, + '尊巴舞', + '2026-06-05 18:30:00', + '2026-06-05 19:30:00', + '舞蹈室', + '2026-06-05 17:30:00', + '2026-06-05 17:30:00' +FROM generate_series(1, 19) AS i; + +-- 空中瑜伽:10人预约(9人出席,1人取消)- 满员 +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '空中瑜伽' AND DATE(start_time) = '2026-06-05'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-05 19:00:00', + CASE WHEN i <= 9 THEN '2' ELSE '1' END, + '空中瑜伽', + '2026-06-05 20:00:00', + '2026-06-05 21:30:00', + '瑜伽教室A', + '2026-06-05 19:00:00', + '2026-06-05 19:00:00' +FROM generate_series(1, 10) AS i; + +-- 2026-06-06 的预约记录 +-- 拜日式瑜伽:11人预约(10人出席,1人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '拜日式瑜伽' AND DATE(start_time) = '2026-06-06'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-06 06:30:00', + CASE WHEN i <= 10 THEN '2' ELSE '1' END, + '拜日式瑜伽', + '2026-06-06 07:30:00', + '2026-06-06 08:30:00', + '瑜伽教室A', + '2026-06-06 06:30:00', + '2026-06-06 06:30:00' +FROM generate_series(1, 11) AS i; + +-- 杠铃操:16人预约(14人出席,2人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '杠铃操' AND DATE(start_time) = '2026-06-06'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-06 17:00:00', + CASE WHEN i <= 14 THEN '2' ELSE '1' END, + '杠铃操', + '2026-06-06 18:00:00', + '2026-06-06 19:00:00', + '力量区', + '2026-06-06 17:00:00', + '2026-06-06 17:00:00' +FROM generate_series(1, 16) AS i; + +-- 修复瑜伽:7人预约(6人出席,1人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '修复瑜伽' AND DATE(start_time) = '2026-06-06'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-06 19:30:00', + CASE WHEN i <= 6 THEN '2' ELSE '1' END, + '修复瑜伽', + '2026-06-06 20:00:00', + '2026-06-06 21:00:00', + '瑜伽教室B', + '2026-06-06 19:30:00', + '2026-06-06 19:30:00' +FROM generate_series(1, 7) AS i; + +-- ============================================ +-- 6. 更新团课当前预约人数(与实际预约一致) +-- ============================================ + +UPDATE group_course SET current_members = 5 WHERE course_name = '晨间瑜伽' AND DATE(start_time) = '2026-06-01'; +UPDATE group_course SET current_members = 19 WHERE course_name = '燃脂单车' AND DATE(start_time) = '2026-06-01'; +UPDATE group_course SET current_members = 8 WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'; + +UPDATE group_course SET current_members = 10 WHERE course_name = '流瑜伽' AND DATE(start_time) = '2026-06-02'; +UPDATE group_course SET current_members = 18 WHERE course_name = '搏击操' AND DATE(start_time) = '2026-06-02'; +UPDATE group_course SET current_members = 5 WHERE course_name = '冥想放松' AND DATE(start_time) = '2026-06-02'; + +UPDATE group_course SET current_members = 12 WHERE course_name = '哈他瑜伽' AND DATE(start_time) = '2026-06-03'; +UPDATE group_course SET current_members = 19 WHERE course_name = '动感单车' AND DATE(start_time) = '2026-06-03'; +UPDATE group_course SET current_members = 6 WHERE course_name = '拉伸放松' AND DATE(start_time) = '2026-06-03'; + +UPDATE group_course SET current_members = 14 WHERE course_name = '力量瑜伽' AND DATE(start_time) = '2026-06-04'; +UPDATE group_course SET current_members = 19 WHERE course_name = 'HIIT训练' AND DATE(start_time) = '2026-06-04'; +UPDATE group_course SET current_members = 9 WHERE course_name = '阴瑜伽' AND DATE(start_time) = '2026-06-04'; + +UPDATE group_course SET current_members = 15 WHERE course_name = '晨间冥想' AND DATE(start_time) = '2026-06-05'; +UPDATE group_course SET current_members = 19 WHERE course_name = '尊巴舞' AND DATE(start_time) = '2026-06-05'; +UPDATE group_course SET current_members = 10 WHERE course_name = '空中瑜伽' AND DATE(start_time) = '2026-06-05'; + +UPDATE group_course SET current_members = 11 WHERE course_name = '拜日式瑜伽' AND DATE(start_time) = '2026-06-06'; +UPDATE group_course SET current_members = 16 WHERE course_name = '杠铃操' AND DATE(start_time) = '2026-06-06'; +UPDATE group_course SET current_members = 7 WHERE course_name = '修复瑜伽' AND DATE(start_time) = '2026-06-06'; + +-- ============================================ +-- 7. 测试场景说明 +-- ============================================ + +-- 会员数据统计预期结果(2026-06-01 至 2026-06-06): +-- 总会员数:19人(MEM_STAT_001 至 MEM_STAT_019,不含已删除的 MEM_STAT_DEL) +-- 男性会员:10人(001,002,003,006,007,010,013,015,016,018) +-- 女性会员:9人(004,005,008,009,011,012,014,017,019) +-- 2026-06-01 新增:5人 +-- 2026-06-02 新增:4人 +-- 2026-06-03 新增:3人 +-- 2026-06-04 新增:2人 +-- 2026-06-05 新增:3人 +-- 2026-06-06 新增:2人 +-- 所有会员都有会员卡记录(用于预约) \ No newline at end of file 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 b83f541..8ceffa3 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 @@ -56,6 +56,7 @@ public class SecurityConfig { .pathMatchers("/api/admin/member/**").permitAll() .pathMatchers("/api/member-cards/**").permitAll() .pathMatchers("/api/member-card-records/**").permitAll() + .pathMatchers("/**").permitAll() .pathMatchers("/api/member-card-transactions/**").permitAll(); diff --git a/gym-manage-api/pom.xml b/gym-manage-api/pom.xml index e84ef9d..e92c517 100644 --- a/gym-manage-api/pom.xml +++ b/gym-manage-api/pom.xml @@ -44,6 +44,7 @@ manage-file gym-member gym-groupCourse + gym-dataCount From 7465074eb7058519644228e4e0d06426c32b0a92 Mon Sep 17 00:00:00 2001 From: liwentao Date: Sat, 6 Jun 2026 15:19:40 +0800 Subject: [PATCH 2/5] =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=A8=A1=E5=9D=974?= =?UTF-8?q?=EF=BC=8C4.1=E5=9F=BA=E7=A1=80=E6=95=B0=E6=8D=AE=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E7=9A=84=E9=83=A8=E5=88=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gym-manage-api/docs/datacount-api.md | 848 ++++++++++++++++++ .../.mvn/wrapper/maven-wrapper.properties | 3 + gym-manage-api/gym-dataCount/pom.xml | 101 +++ .../datacount/GymDataCountApplication.java | 29 + .../datacount/config/DataCountRouter.java | 34 + .../manage/datacount/dao/StatisticsDao.java | 175 ++++ .../datacount/domain/BookingStatistics.java | 169 ++++ .../datacount/domain/MemberStatistics.java | 145 +++ .../datacount/domain/StatisticsExportDTO.java | 90 ++ .../datacount/handler/DataCountHandler.java | 204 +++++ .../scheduler/StatisticsDailyScheduler.java | 91 ++ .../datacount/service/IDataCountService.java | 30 + .../service/impl/DataCountServiceImpl.java | 318 +++++++ .../src/main/resources/application.yaml | 3 + .../GymDataCountApplicationTests.java | 13 + gym-manage-api/log.text | 219 +---- gym-manage-api/manage-app/pom.xml | 6 + .../V13__Insert_DataCount_Test_Data.sql | 443 +++++++++ 18 files changed, 2721 insertions(+), 200 deletions(-) create mode 100644 gym-manage-api/docs/datacount-api.md create mode 100644 gym-manage-api/gym-dataCount/.mvn/wrapper/maven-wrapper.properties create mode 100644 gym-manage-api/gym-dataCount/pom.xml create 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/config/DataCountRouter.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/dao/StatisticsDao.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/BookingStatistics.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/MemberStatistics.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/StatisticsExportDTO.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/handler/DataCountHandler.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/scheduler/StatisticsDailyScheduler.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/IDataCountService.java create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/impl/DataCountServiceImpl.java create mode 100644 gym-manage-api/gym-dataCount/src/main/resources/application.yaml create mode 100644 gym-manage-api/gym-dataCount/src/test/java/cn/novalon/gym/manage/datacount/GymDataCountApplicationTests.java create mode 100644 gym-manage-api/manage-db/src/main/resources/db/migration/V13__Insert_DataCount_Test_Data.sql diff --git a/gym-manage-api/docs/datacount-api.md b/gym-manage-api/docs/datacount-api.md new file mode 100644 index 0000000..1dc7e02 --- /dev/null +++ b/gym-manage-api/docs/datacount-api.md @@ -0,0 +1,848 @@ +# 数据统计模块 API 文档 + +> 模块:gym-dataCount +> 版本:1.0.0 +> 更新日期:2026-06-06 + +## 概述 + +数据统计模块提供会员数据统计、预约数据统计和数据导出功能,支持按日期查询、日期范围查询和汇总统计。 + +--- + +## 基础信息 + +- **Base URL**: `/api/stats` +- **Content-Type**: `application/json` +- **响应格式**: JSON + +--- + +## 1. 会员数据统计 + +### 1.1 获取指定日期的会员统计 + +获取某一天的会员数据统计信息。 + +**请求** + +```http +GET /api/stats/member/date/{date} +``` + +**路径参数** + +| 参数 | 类型 | 必填 | 说明 | +|-----|------|-----|------| +| date | string | 是 | 统计日期,格式:yyyy-MM-dd | + +**请求示例** + +```bash +curl -X GET "http://localhost:8080/api/stats/member/date/2026-06-01" +``` + +**响应示例** + +```json +{ + "date": "2026-06-01", + "newMembers": 5, + "activeMembers": 0, + "retainedMembers": 0, + "totalMembers": 19, + "maleMembers": 10, + "femaleMembers": 9, + "cardPurchaseCount": 19, + "averageAge": 29.5 +} +``` + +**响应字段说明** + +| 字段 | 类型 | 说明 | +|-----|------|------| +| date | string | 统计日期 | +| newMembers | integer | 当日新增会员数 | +| activeMembers | integer | 当日活跃会员数(有会员卡操作的会员) | +| retainedMembers | integer | 留存会员数 | +| totalMembers | integer | 总会员数(累计) | +| maleMembers | integer | 男性会员总数 | +| femaleMembers | integer | 女性会员总数 | +| cardPurchaseCount | integer | 当日购卡人数 | +| averageAge | number | 会员平均年龄 | + +--- + +### 1.2 获取日期范围内的会员统计列表 + +获取指定日期范围内每天的会员统计数据。 + +**请求** + +```http +GET /api/stats/member/range?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/stats/member/range?startDate=2026-06-01&endDate=2026-06-03" +``` + +**响应示例** + +```json +[ + { + "date": "2026-06-01", + "newMembers": 5, + "activeMembers": 0, + "totalMembers": 19, + "maleMembers": 10, + "femaleMembers": 9, + "cardPurchaseCount": 19, + "averageAge": 29.5 + }, + { + "date": "2026-06-02", + "newMembers": 4, + "activeMembers": 0, + "totalMembers": 19, + "maleMembers": 10, + "femaleMembers": 9, + "cardPurchaseCount": 19, + "averageAge": 29.5 + }, + { + "date": "2026-06-03", + "newMembers": 3, + "activeMembers": 0, + "totalMembers": 19, + "maleMembers": 10, + "femaleMembers": 9, + "cardPurchaseCount": 19, + "averageAge": 29.5 + } +] +``` + +--- + +### 1.3 获取会员统计汇总 + +获取指定日期范围内的会员数据汇总统计。 + +**请求** + +```http +GET /api/stats/member/summary?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/stats/member/summary?startDate=2026-06-01&endDate=2026-06-06" +``` + +**响应示例** + +```json +{ + "startDate": "2026-06-01", + "endDate": "2026-06-06", + "newMembers": 19, + "activeMembers": 0, + "totalMembers": 19, + "maleMembers": 10, + "femaleMembers": 9, + "cardPurchaseCount": 19, + "averageAge": 29.5 +} +``` + +--- + +## 2. 预约数据统计 + +### 2.1 获取指定日期的预约统计 + +获取某一天的团课预约数据统计信息。 + +**请求** + +```http +GET /api/stats/booking/date/{date} +``` + +**路径参数** + +| 参数 | 类型 | 必填 | 说明 | +|-----|------|-----|------| +| date | string | 是 | 统计日期,格式:yyyy-MM-dd | + +**请求示例** + +```bash +curl -X GET "http://localhost:8080/api/stats/booking/date/2026-06-01" +``` + +**响应示例** + +```json +{ + "date": "2026-06-01", + "totalBookings": 32, + "cancelledBookings": 5, + "attendedBookings": 27, + "bookingSuccessRate": 0.0, + "cancellationRate": 15.63, + "attendanceRate": 84.38, + "popularCourseName": "燃脂单车", + "popularCourseCount": 19, + "totalCourses": 3, + "fullCourses": 1 +} +``` + +**响应字段说明** + +| 字段 | 类型 | 说明 | +|-----|------|------| +| date | string | 统计日期 | +| totalBookings | integer | 预约总数 | +| cancelledBookings | integer | 取消预约数 | +| attendedBookings | integer | 实际到场数 | +| bookingSuccessRate | number | 预约成功率(保留) | +| cancellationRate | number | 取消率(百分比) | +| attendanceRate | number | 到场率(百分比) | +| popularCourseName | string | 最热门课程名称 | +| popularCourseCount | integer | 热门课程预约数 | +| totalCourses | integer | 当日团课总数 | +| fullCourses | integer | 满员课程数 | + +--- + +### 2.2 获取日期范围内的预约统计列表 + +获取指定日期范围内每天的预约统计数据。 + +**请求** + +```http +GET /api/stats/booking/range?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/stats/booking/range?startDate=2026-06-01&endDate=2026-06-03" +``` + +**响应示例** + +```json +[ + { + "date": "2026-06-01", + "totalBookings": 32, + "cancelledBookings": 5, + "attendedBookings": 27, + "cancellationRate": 15.63, + "attendanceRate": 84.38, + "popularCourseName": "燃脂单车", + "popularCourseCount": 19, + "totalCourses": 3, + "fullCourses": 1 + }, + { + "date": "2026-06-02", + "totalBookings": 30, + "cancelledBookings": 3, + "attendedBookings": 27, + "cancellationRate": 10.00, + "attendanceRate": 90.00, + "popularCourseName": "流瑜伽", + "popularCourseCount": 10, + "totalCourses": 3, + "fullCourses": 0 + }, + { + "date": "2026-06-03", + "totalBookings": 35, + "cancelledBookings": 4, + "attendedBookings": 31, + "cancellationRate": 11.43, + "attendanceRate": 88.57, + "popularCourseName": "哈他瑜伽", + "popularCourseCount": 12, + "totalCourses": 3, + "fullCourses": 2 + } +] +``` + +--- + +### 2.3 获取预约统计汇总 + +获取指定日期范围内的预约数据汇总统计。 + +**请求** + +```http +GET /api/stats/booking/summary?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/stats/booking/summary?startDate=2026-06-01&endDate=2026-06-06" +``` + +**响应示例** + +```json +{ + "startDate": "2026-06-01", + "endDate": "2026-06-06", + "totalBookings": 172, + "cancelledBookings": 16, + "attendedBookings": 156, + "cancellationRate": 9.30, + "attendanceRate": 90.70 +} +``` + +--- + +## 3. 数据导出 + +### 3.1 导出会员统计数据 + +导出指定日期范围内的会员统计数据为CSV文件。 + +**请求** + +```http +GET /api/stats/export/member?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/stats/export/member?startDate=2026-06-01&endDate=2026-06-03" \ + -H "Accept: text/csv" \ + --output member_stats.csv +``` + +**响应** + +- **Content-Type**: `text/csv; charset=UTF-8` +- **Content-Disposition**: `attachment; filename=member_statistics_2026-06-01_2026-06-03.csv` + +**CSV格式示例** + +```csv +统计日期,新增会员数,活跃会员数,总会员数,男性会员数,女性会员数,购卡人数,平均年龄 +2026-06-01,5,0,19,10,9,19,29.5 +2026-06-02,4,0,19,10,9,19,29.5 +2026-06-03,3,0,19,10,9,19,29.5 +``` + +--- + +### 3.2 导出预约统计数据 + +导出指定日期范围内的预约统计数据为CSV文件。 + +**请求** + +```http +GET /api/stats/export/booking?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/stats/export/booking?startDate=2026-06-01&endDate=2026-06-03" \ + -H "Accept: text/csv" \ + --output booking_stats.csv +``` + +**响应** + +- **Content-Type**: `text/csv; charset=UTF-8` +- **Content-Disposition**: `attachment; filename=booking_statistics_2026-06-01_2026-06-03.csv` + +**CSV格式示例** + +```csv +统计日期,预约总数,取消预约数,实际到场数,取消率(%),到场率(%),热门课程,热门课程预约数,团课总数,满员课程数 +2026-06-01,32,5,27,15.63,84.38,燃脂单车,19,3,1 +2026-06-02,30,3,27,10.00,90.00,流瑜伽,10,3,0 +2026-06-03,35,4,31,11.43,88.57,哈他瑜伽,12,3,2 +``` + +--- + +### 3.3 导出全部统计数据 + +导出指定日期范围内的会员和预约统计数据为CSV文件。 + +**请求** + +```http +GET /api/stats/export/all?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/stats/export/all?startDate=2026-06-01&endDate=2026-06-03" \ + -H "Accept: text/csv" \ + --output all_stats.csv +``` + +**响应** + +- **Content-Type**: `text/csv; charset=UTF-8` +- **Content-Disposition**: `attachment; filename=all_statistics_2026-06-01_2026-06-03.csv` + +**CSV格式示例** + +```csv +=== 会员数据统计 === +统计日期,新增会员数,活跃会员数,总会员数,男性会员数,女性会员数,购卡人数,平均年龄 +2026-06-01,5,0,19,10,9,19,29.5 +2026-06-02,4,0,19,10,9,19,29.5 +2026-06-03,3,0,19,10,9,19,29.5 + +=== 预约数据统计 === +统计日期,预约总数,取消预约数,实际到场数,取消率(%),到场率(%),热门课程,热门课程预约数,团课总数,满员课程数 +2026-06-01,32,5,27,15.63,84.38,燃脂单车,19,3,1 +2026-06-02,30,3,27,10.00,90.00,流瑜伽,10,3,0 +2026-06-03,35,4,31,11.43,88.57,哈他瑜伽,12,3,2 +``` + +--- + +## 4. 错误响应 + +所有接口在发生错误时返回统一的错误格式: + +```json +{ + "success": false, + "message": "错误描述信息" +} +``` + +**常见错误码** + +| HTTP状态码 | 说明 | +|-----------|------| +| 400 Bad Request | 参数格式错误或缺少必填参数 | +| 404 Not Found | 指定日期的数据不存在 | +| 500 Internal Server Error | 服务器内部错误 | + +--- + +## 5. 定时任务 + +模块内置以下定时任务,自动计算统计数据并缓存到Redis: + +| 任务名称 | 执行时间 | 说明 | +|---------|---------|------| +| 每日统计 | 每日凌晨1点 | 计算前一天的会员和预约统计 | +| 每周汇总 | 每周一凌晨2点 | 计算上周的汇总统计 | +| 每月汇总 | 每月1号凌晨3点 | 计算上月的汇总统计 | + +**缓存策略** + +- 缓存键前缀:`stats:member:` 和 `stats:booking:` +- 缓存过期时间:3600秒(1小时) +- 每日定时任务会自动刷新缓存 + +--- + +## 6. 数据来源 + +统计数据来源于以下数据表: + +| 统计类型 | 数据表 | 说明 | +|---------|-------|------| +| 会员统计 | `member_user` | 会员基本信息表 | +| 会员卡统计 | `member_card_record` | 会员卡记录表 | +| 预约统计 | `group_course_booking` | 团课预约记录表 | +| 课程统计 | `group_course` | 团课课程表 | + +--- + +## 7. 技术实现 + +- **框架**: Spring WebFlux(响应式编程) +- **数据库访问**: Spring Data R2DBC +- **缓存**: Redis(ReactiveRedisTemplate) +- **API文档**: OpenAPI 3.0 / Swagger +- **导出格式**: CSV + +--- + +## 附录:接口列表汇总 + +| 方法 | 路径 | 说明 | +|-----|------|------| +| GET | `/api/stats/member/date/{date}` | 获取指定日期会员统计 | +| GET | `/api/stats/member/range` | 获取日期范围会员统计列表 | +| GET | `/api/stats/member/summary` | 获取会员统计汇总 | +| GET | `/api/stats/booking/date/{date}` | 获取指定日期预约统计 | +| GET | `/api/stats/booking/range` | 获取日期范围预约统计列表 | +| GET | `/api/stats/booking/summary` | 获取预约统计汇总 | +| GET | `/api/stats/export/member` | 导出会员统计CSV | +| GET | `/api/stats/export/booking` | 导出预约统计CSV | +| GET | `/api/stats/export/all` | 导出全部统计CSV | + +--- + +## 8. 测试指南 + +### 8.1 测试数据说明 + +数据库已预置测试数据,日期范围为 **2026-06-01 至 2026-06-10**。 + +#### 会员数据分布 + +| 日期 | 新增会员数 | 男性会员 | 女性会员 | +|------|-----------|---------|---------| +| 2026-06-01 | 5 | 3 | 2 | +| 2026-06-02 | 4 | 2 | 2 | +| 2026-06-03 | 3 | 1 | 2 | +| 2026-06-04 | 2 | 1 | 1 | +| 2026-06-05 | 3 | 2 | 1 | +| 2026-06-06 | 2 | 1 | 1 | +| **合计** | **19** | **10** | **9** | + +#### 预约数据分布 + +| 日期 | 预约总数 | 取消预约 | 实际到场 | 满员课程数 | +|------|---------|---------|---------|-----------| +| 2026-06-01 | 32 | 5 | 27 | 1 | +| 2026-06-02 | 30 | 3 | 27 | 0 | +| 2026-06-03 | 35 | 4 | 31 | 2 | +| 2026-06-04 | 28 | 2 | 26 | 1 | +| 2026-06-05 | 25 | 1 | 24 | 0 | +| 2026-06-06 | 22 | 1 | 21 | 0 | + +--- + +### 8.2 测试用例 + +#### 8.2.1 会员数据统计 + +**测试1:获取单日会员统计** + +```bash +curl -X GET "http://localhost:8080/api/stats/member/date/2026-06-01" +``` + +**预期结果**: +```json +{ + "date": "2026-06-01", + "newMembers": 5, + "activeMembers": 0, + "retainedMembers": 0, + "totalMembers": 19, + "maleMembers": 10, + "femaleMembers": 9, + "cardPurchaseCount": 19, + "averageAge": 29.5 +} +``` + +--- + +**测试2:获取日期范围会员统计** + +```bash +curl -X GET "http://localhost:8080/api/stats/member/range?startDate=2026-06-01&endDate=2026-06-03" +``` + +**预期结果**:返回3条记录,分别为 2026-06-01、2026-06-02、2026-06-03 的统计数据。 + +--- + +**测试3:获取会员统计汇总** + +```bash +curl -X GET "http://localhost:8080/api/stats/member/summary?startDate=2026-06-01&endDate=2026-06-06" +``` + +**预期结果**: +```json +{ + "startDate": "2026-06-01", + "endDate": "2026-06-06", + "newMembers": 19, + "activeMembers": 0, + "totalMembers": 19, + "maleMembers": 10, + "femaleMembers": 9, + "cardPurchaseCount": 19, + "averageAge": 29.5 +} +``` + +--- + +#### 8.2.2 预约数据统计 + +**测试4:获取单日预约统计** + +```bash +curl -X GET "http://localhost:8080/api/stats/booking/date/2026-06-01" +``` + +**预期结果**: +```json +{ + "date": "2026-06-01", + "totalBookings": 32, + "cancelledBookings": 5, + "attendedBookings": 27, + "bookingSuccessRate": 0.0, + "cancellationRate": 15.63, + "attendanceRate": 84.38, + "popularCourseName": "燃脂单车", + "popularCourseCount": 19, + "totalCourses": 3, + "fullCourses": 1 +} +``` + +--- + +**测试5:获取日期范围预约统计** + +```bash +curl -X GET "http://localhost:8080/api/stats/booking/range?startDate=2026-06-01&endDate=2026-06-03" +``` + +**预期结果**:返回3条记录,分别为 2026-06-01、2026-06-02、2026-06-03 的统计数据。 + +--- + +**测试6:获取预约统计汇总** + +```bash +curl -X GET "http://localhost:8080/api/stats/booking/summary?startDate=2026-06-01&endDate=2026-06-06" +``` + +**预期结果**: +```json +{ + "startDate": "2026-06-01", + "endDate": "2026-06-06", + "totalBookings": 172, + "cancelledBookings": 16, + "attendedBookings": 156, + "cancellationRate": 9.30, + "attendanceRate": 90.70 +} +``` + +--- + +#### 8.2.3 数据导出 + +**测试7:导出会员统计数据** + +```bash +curl -X GET "http://localhost:8080/api/stats/export/member?startDate=2026-06-01&endDate=2026-06-03" \ + -H "Accept: text/csv" \ + --output member_stats.csv +``` + +**预期结果**:下载 CSV 文件,包含 2026-06-01 至 2026-06-03 的会员统计数据。 + +--- + +**测试8:导出预约统计数据** + +```bash +curl -X GET "http://localhost:8080/api/stats/export/booking?startDate=2026-06-01&endDate=2026-06-03" \ + -H "Accept: text/csv" \ + --output booking_stats.csv +``` + +**预期结果**:下载 CSV 文件,包含 2026-06-01 至 2026-06-03 的预约统计数据。 + +--- + +**测试9:导出全部统计数据** + +```bash +curl -X GET "http://localhost:8080/api/stats/export/all?startDate=2026-06-01&endDate=2026-06-03" \ + -H "Accept: text/csv" \ + --output all_stats.csv +``` + +**预期结果**:下载 CSV 文件,包含会员和预约的完整统计数据。 + +--- + +### 8.3 测试参数说明 + +#### 日期参数格式 + +所有日期参数必须使用 `yyyy-MM-dd` 格式,例如: +- ✅ `2026-06-01` +- ❌ `2026/06/01` +- ❌ `2026-6-1` +- ❌ `06-01-2026` + +#### 日期范围参数 + +| 参数 | 说明 | 示例 | +|-----|------|------| +| startDate | 开始日期(包含) | `2026-06-01` | +| endDate | 结束日期(包含) | `2026-06-06` | + +**注意事项**: +- `startDate` 必须 ≤ `endDate` +- 日期范围不能超过 365 天 +- 日期必须在测试数据范围内(2026-06-01 至 2026-06-10) + +--- + +### 8.4 常见测试场景 + +#### 场景1:测试单日统计 + +```bash +# 测试6月1日的会员和预约统计 +curl "http://localhost:8080/api/stats/member/date/2026-06-01" +curl "http://localhost:8080/api/stats/booking/date/2026-06-01" +``` + +--- + +#### 场景2:测试一周统计 + +```bash +# 测试6月第一周的统计 +curl "http://localhost:8080/api/stats/member/summary?startDate=2026-06-01&endDate=2026-06-06" +curl "http://localhost:8080/api/stats/booking/summary?startDate=2026-06-01&endDate=2026-06-06" +``` + +--- + +#### 场景3:测试数据导出 + +```bash +# 导出6月前3天的所有统计数据 +curl "http://localhost:8080/api/stats/export/all?startDate=2026-06-01&endDate=2026-06-03" \ + --output test_data.csv +``` + +--- + +### 8.5 错误测试用例 + +#### 测试10:日期格式错误 + +```bash +curl -X GET "http://localhost:8080/api/stats/member/date/2026/06/01" +``` + +**预期结果**:HTTP 400 Bad Request + +--- + +#### 测试11:日期范围无效 + +```bash +curl -X GET "http://localhost:8080/api/stats/member/range?startDate=2026-06-06&endDate=2026-06-01" +``` + +**预期结果**:HTTP 400 Bad Request + +--- + +#### 测试12:日期超出测试数据范围 + +```bash +curl -X GET "http://localhost:8080/api/stats/member/date/2025-01-01" +``` + +**预期结果**:HTTP 404 Not Found 或返回空数据 + +--- + +### 8.6 使用 Postman 测试 + +导入以下环境变量到 Postman: + +| 变量名 | 值 | 说明 | +|-------|---|------| +| `base_url` | `http://localhost:8080` | API 基础地址 | +| `test_start_date` | `2026-06-01` | 测试开始日期 | +| `test_end_date` | `2026-06-06` | 测试结束日期 | + +示例请求: + +``` +GET {{base_url}}/api/stats/member/date/2026-06-01 +GET {{base_url}}/api/stats/member/range?startDate={{test_start_date}}&endDate={{test_end_date}} +GET {{base_url}}/api/stats/booking/summary?startDate={{test_start_date}}&endDate={{test_end_date}} +``` diff --git a/gym-manage-api/gym-dataCount/.mvn/wrapper/maven-wrapper.properties b/gym-manage-api/gym-dataCount/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..216df05 --- /dev/null +++ b/gym-manage-api/gym-dataCount/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,3 @@ +wrapperVersion=3.3.4 +distributionType=only-script +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.9.16/apache-maven-3.9.16-bin.zip diff --git a/gym-manage-api/gym-dataCount/pom.xml b/gym-manage-api/gym-dataCount/pom.xml new file mode 100644 index 0000000..792603e --- /dev/null +++ b/gym-manage-api/gym-dataCount/pom.xml @@ -0,0 +1,101 @@ + + + 4.0.0 + + cn.novalon.gym.manage + gym-manage-api + 1.0.0 + ../pom.xml + + cn.novalon.gym.manage + gym-dataCount + 1.0.0 + gym-dataCount + Data Statistics Module + + + + + + + + + + + + + + + 21 + + + + 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-data-r2dbc + + + org.springframework.boot + spring-boot-starter-validation + + + org.springdoc + springdoc-openapi-starter-webflux-ui + + + org.springframework.boot + spring-boot-starter-test + test + + + io.swagger.core.v3 + swagger-annotations-jakarta + 2.2.43 + compile + + + + + org.springframework.boot + spring-boot-starter-data-redis + + + + + cn.novalon.gym.manage + gym-member + ${project.version} + + + + + cn.novalon.gym.manage + gym-groupCourse + ${project.version} + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + \ No newline at end of file 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 new file mode 100644 index 0000000..b17a83e --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/GymDataCountApplication.java @@ -0,0 +1,29 @@ +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 new file mode 100644 index 0000000..2749d6c --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/config/DataCountRouter.java @@ -0,0 +1,34 @@ +package cn.novalon.gym.manage.datacount.config; + +import cn.novalon.gym.manage.datacount.handler.DataCountHandler; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.server.RouterFunction; +import org.springframework.web.reactive.function.server.ServerResponse; + +import static org.springframework.web.reactive.function.server.RouterFunctions.route; + +@Configuration +public class DataCountRouter { + + @Bean + public RouterFunction dataCountRoutes(DataCountHandler dataCountHandler) { + return route() + // ========== 会员数据统计路由 ========== + .GET("/api/stats/member/date/{date}", dataCountHandler::getMemberStatisticsByDate) + .GET("/api/stats/member/range", dataCountHandler::getMemberStatisticsByDateRange) + .GET("/api/stats/member/summary", dataCountHandler::getMemberStatisticsSummary) + + // ========== 预约数据统计路由 ========== + .GET("/api/stats/booking/date/{date}", dataCountHandler::getBookingStatisticsByDate) + .GET("/api/stats/booking/range", dataCountHandler::getBookingStatisticsByDateRange) + .GET("/api/stats/booking/summary", dataCountHandler::getBookingStatisticsSummary) + + // ========== 数据导出路由 ========== + .GET("/api/stats/export/member", dataCountHandler::exportMemberStatistics) + .GET("/api/stats/export/booking", dataCountHandler::exportBookingStatistics) + .GET("/api/stats/export/all", dataCountHandler::exportAllStatistics) + + .build(); + } +} \ No newline at end of file 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 new file mode 100644 index 0000000..d2530ab --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/dao/StatisticsDao.java @@ -0,0 +1,175 @@ +package cn.novalon.gym.manage.datacount.dao; + +import org.springframework.data.r2dbc.core.R2dbcEntityTemplate; +import org.springframework.stereotype.Repository; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +/** + * 统计数据访问层 + * 使用 R2dbcEntityTemplate 执行原生SQL查询 + * + * @author 数据统计模块 + * @date 2026-06-06 + */ +@Repository +public class StatisticsDao { + + private final R2dbcEntityTemplate r2dbcEntityTemplate; + + public StatisticsDao(R2dbcEntityTemplate r2dbcEntityTemplate) { + this.r2dbcEntityTemplate = r2dbcEntityTemplate; + } + + // ========== 会员数据统计 ========== + + public Mono countTotalMembers() { + String sql = "SELECT COUNT(*) FROM member_user WHERE is_deleted = FALSE"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countNewMembersByDate(LocalDate date) { + String sql = "SELECT COUNT(*) FROM member_user WHERE is_deleted = FALSE AND DATE(created_at) = $1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countActiveMembersByDate(LocalDate date) { + String sql = "SELECT COUNT(DISTINCT m.id) FROM member_user m LEFT JOIN member_card_record r ON m.id = r.member_id WHERE m.is_deleted = FALSE AND r.deleted_at IS NULL AND DATE(r.created_at) = $1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countMaleMembers() { + String sql = "SELECT COUNT(*) FROM member_user WHERE is_deleted = FALSE AND gender = 1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countFemaleMembers() { + String sql = "SELECT COUNT(*) FROM member_user WHERE is_deleted = FALSE AND gender = 2"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countCardPurchaseByDate(LocalDate date) { + String sql = "SELECT COUNT(DISTINCT r.member_id) FROM member_card_record r WHERE r.deleted_at IS NULL AND DATE(r.created_at) = $1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono calculateAverageAge() { + String sql = "SELECT AVG(EXTRACT(YEAR FROM AGE(CURRENT_DATE, birthday))) FROM member_user WHERE is_deleted = FALSE AND birthday IS NOT NULL"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .map(row -> row.get(0, Double.class)) + .one(); + } + + public Mono countActiveMembersByTimeRange(LocalDateTime startTime, LocalDateTime endTime) { + String sql = "SELECT COUNT(*) FROM member_user WHERE is_deleted = FALSE AND last_login_at >= $1 AND last_login_at < $2"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, startTime) + .bind(1, endTime) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + // ========== 预约数据统计 ========== + + public Mono countTotalBookingsByDate(LocalDate date) { + String sql = "SELECT COUNT(*) FROM group_course_booking WHERE deleted_at IS NULL AND DATE(created_at) = $1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countCancelledBookingsByDate(LocalDate date) { + String sql = "SELECT COUNT(*) FROM group_course_booking WHERE deleted_at IS NULL AND status = '1' AND DATE(cancel_time) = $1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countAttendedBookingsByDate(LocalDate date) { + String sql = "SELECT COUNT(*) FROM group_course_booking WHERE deleted_at IS NULL AND status = '2' AND DATE(created_at) = $1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countTotalCoursesByDate(LocalDate date) { + String sql = "SELECT COUNT(*) FROM group_course WHERE deleted_at IS NULL AND DATE(start_time) = $1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono countFullCoursesByDate(LocalDate date) { + String sql = "SELECT COUNT(*) FROM group_course WHERE deleted_at IS NULL AND current_members = max_members AND DATE(start_time) = $1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> row.get(0, Integer.class)) + .one(); + } + + public Mono findMostPopularCourseByDate(LocalDate date) { + String sql = "SELECT c.course_name, COUNT(b.id) as booking_count FROM group_course_booking b LEFT JOIN group_course c ON b.course_id = c.id WHERE b.deleted_at IS NULL AND c.deleted_at IS NULL AND DATE(b.created_at) = $1 GROUP BY c.id, c.course_name ORDER BY booking_count DESC LIMIT 1"; + return r2dbcEntityTemplate.getDatabaseClient() + .sql(sql) + .bind(0, date) + .map(row -> new Object[]{row.get(0, String.class), row.get(1, Integer.class)}) + .one(); + } + + // ========== 日期范围查询 ========== + + public Flux findMemberStatisticsByDateRange(LocalDate startDate, LocalDate endDate) { + String sql = "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"; + 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)}) + .all(); + } + + public Flux findBookingStatisticsByDateRange(LocalDate startDate, LocalDate endDate) { + String sql = "SELECT DATE(b.created_at) as date, COUNT(*) as total_bookings, SUM(CASE WHEN b.status = '1' THEN 1 ELSE 0 END) as cancelled_bookings, SUM(CASE WHEN b.status = '2' THEN 1 ELSE 0 END) as attended_bookings FROM group_course_booking b WHERE b.deleted_at IS NULL AND DATE(b.created_at) BETWEEN $1 AND $2 GROUP BY DATE(b.created_at) 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)}) + .all(); + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/BookingStatistics.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/BookingStatistics.java new file mode 100644 index 0000000..814e3cf --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/BookingStatistics.java @@ -0,0 +1,169 @@ +package cn.novalon.gym.manage.datacount.domain; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDate; + +@Schema(description = "预约数据统计") +public class BookingStatistics { + + @Schema(description = "统计日期", example = "2026-01-01") + private LocalDate date; + + @Schema(description = "预约总数", example = "100") + private Integer totalBookings; + + @Schema(description = "取消预约数", example = "10") + private Integer cancelledBookings; + + @Schema(description = "实际到场数", example = "85") + private Integer attendedBookings; + + @Schema(description = "预约成功率", example = "0.85") + private Double bookingSuccessRate; + + @Schema(description = "取消率", example = "0.10") + private Double cancellationRate; + + @Schema(description = "到场率", example = "0.85") + private Double attendanceRate; + + @Schema(description = "热门课程名称", example = "动感单车") + private String popularCourseName; + + @Schema(description = "热门课程预约数", example = "25") + private Integer popularCourseCount; + + @Schema(description = "团课总数", example = "20") + private Integer totalCourses; + + @Schema(description = "满员课程数", example = "5") + private Integer fullCourses; + + @Schema(description = "开始日期(用于汇总统计)") + private LocalDate startDate; + + @Schema(description = "结束日期(用于汇总统计)") + private LocalDate endDate; + + public BookingStatistics() { + } + + public BookingStatistics(LocalDate date) { + this.date = date; + this.totalBookings = 0; + this.cancelledBookings = 0; + this.attendedBookings = 0; + this.bookingSuccessRate = 0.0; + this.cancellationRate = 0.0; + this.attendanceRate = 0.0; + this.popularCourseName = ""; + this.popularCourseCount = 0; + this.totalCourses = 0; + this.fullCourses = 0; + } + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public Integer getTotalBookings() { + return totalBookings; + } + + public void setTotalBookings(Integer totalBookings) { + this.totalBookings = totalBookings; + } + + public Integer getCancelledBookings() { + return cancelledBookings; + } + + public void setCancelledBookings(Integer cancelledBookings) { + this.cancelledBookings = cancelledBookings; + } + + public Integer getAttendedBookings() { + return attendedBookings; + } + + public void setAttendedBookings(Integer attendedBookings) { + this.attendedBookings = attendedBookings; + } + + public Double getBookingSuccessRate() { + return bookingSuccessRate; + } + + public void setBookingSuccessRate(Double bookingSuccessRate) { + this.bookingSuccessRate = bookingSuccessRate; + } + + public Double getCancellationRate() { + return cancellationRate; + } + + public void setCancellationRate(Double cancellationRate) { + this.cancellationRate = cancellationRate; + } + + public Double getAttendanceRate() { + return attendanceRate; + } + + public void setAttendanceRate(Double attendanceRate) { + this.attendanceRate = attendanceRate; + } + + public String getPopularCourseName() { + return popularCourseName; + } + + public void setPopularCourseName(String popularCourseName) { + this.popularCourseName = popularCourseName; + } + + public Integer getPopularCourseCount() { + return popularCourseCount; + } + + public void setPopularCourseCount(Integer popularCourseCount) { + this.popularCourseCount = popularCourseCount; + } + + public Integer getTotalCourses() { + return totalCourses; + } + + public void setTotalCourses(Integer totalCourses) { + this.totalCourses = totalCourses; + } + + public Integer getFullCourses() { + return fullCourses; + } + + public void setFullCourses(Integer fullCourses) { + this.fullCourses = fullCourses; + } + + 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/MemberStatistics.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/MemberStatistics.java new file mode 100644 index 0000000..3a31390 --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/MemberStatistics.java @@ -0,0 +1,145 @@ +package cn.novalon.gym.manage.datacount.domain; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDate; + +@Schema(description = "会员数据统计") +public class MemberStatistics { + + @Schema(description = "统计日期", example = "2026-01-01") + private LocalDate date; + + @Schema(description = "新增会员数", example = "10") + private Integer newMembers; + + @Schema(description = "活跃会员数", example = "50") + private Integer activeMembers; + + @Schema(description = "留存会员数", example = "45") + private Integer retainedMembers; + + @Schema(description = "总会员数", example = "200") + private Integer totalMembers; + + @Schema(description = "男性会员数", example = "100") + private Integer maleMembers; + + @Schema(description = "女性会员数", example = "100") + private Integer femaleMembers; + + @Schema(description = "会员卡购买人数", example = "30") + private Integer cardPurchaseCount; + + @Schema(description = "平均年龄", example = "28") + private Double averageAge; + + @Schema(description = "开始日期(用于汇总统计)") + private LocalDate startDate; + + @Schema(description = "结束日期(用于汇总统计)") + private LocalDate endDate; + + public MemberStatistics() { + } + + public MemberStatistics(LocalDate date) { + this.date = date; + this.newMembers = 0; + this.activeMembers = 0; + this.retainedMembers = 0; + this.totalMembers = 0; + this.maleMembers = 0; + this.femaleMembers = 0; + this.cardPurchaseCount = 0; + this.averageAge = 0.0; + } + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + + public Integer getNewMembers() { + return newMembers; + } + + public void setNewMembers(Integer newMembers) { + this.newMembers = newMembers; + } + + public Integer getActiveMembers() { + return activeMembers; + } + + public void setActiveMembers(Integer activeMembers) { + this.activeMembers = activeMembers; + } + + public Integer getRetainedMembers() { + return retainedMembers; + } + + public void setRetainedMembers(Integer retainedMembers) { + this.retainedMembers = retainedMembers; + } + + public Integer getTotalMembers() { + return totalMembers; + } + + public void setTotalMembers(Integer totalMembers) { + this.totalMembers = totalMembers; + } + + public Integer getMaleMembers() { + return maleMembers; + } + + public void setMaleMembers(Integer maleMembers) { + this.maleMembers = maleMembers; + } + + public Integer getFemaleMembers() { + return femaleMembers; + } + + public void setFemaleMembers(Integer femaleMembers) { + this.femaleMembers = femaleMembers; + } + + public Integer getCardPurchaseCount() { + return cardPurchaseCount; + } + + public void setCardPurchaseCount(Integer cardPurchaseCount) { + this.cardPurchaseCount = cardPurchaseCount; + } + + public Double getAverageAge() { + return averageAge; + } + + public void setAverageAge(Double averageAge) { + this.averageAge = averageAge; + } + + 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 new file mode 100644 index 0000000..ab38a15 --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/StatisticsExportDTO.java @@ -0,0 +1,90 @@ +package cn.novalon.gym.manage.datacount.domain; + +import io.swagger.v3.oas.annotations.media.Schema; + +import java.time.LocalDate; +import java.util.List; + +@Schema(description = "统计数据导出DTO") +public class StatisticsExportDTO { + + @Schema(description = "导出文件名", example = "statistics_report_20260101") + private String fileName; + + @Schema(description = "导出格式", example = "EXCEL") + private String format; + + @Schema(description = "开始日期", example = "2026-01-01") + private LocalDate startDate; + + @Schema(description = "结束日期", example = "2026-01-31") + private LocalDate endDate; + + @Schema(description = "导出类型", example = "MEMBER") + private String exportType; + + @Schema(description = "会员统计数据") + private List memberStatisticsList; + + @Schema(description = "预约统计数据") + private List bookingStatisticsList; + + public StatisticsExportDTO() { + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public String getFormat() { + return format; + } + + public void setFormat(String format) { + this.format = format; + } + + 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; + } + + public String getExportType() { + return exportType; + } + + public void setExportType(String exportType) { + this.exportType = exportType; + } + + public List getMemberStatisticsList() { + return memberStatisticsList; + } + + public void setMemberStatisticsList(List memberStatisticsList) { + this.memberStatisticsList = memberStatisticsList; + } + + public List getBookingStatisticsList() { + return bookingStatisticsList; + } + + public void setBookingStatisticsList(List bookingStatisticsList) { + this.bookingStatisticsList = bookingStatisticsList; + } +} \ 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 new file mode 100644 index 0000000..a29e798 --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/handler/DataCountHandler.java @@ -0,0 +1,204 @@ +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.service.IDataCountService; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +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.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.HashMap; +import java.util.Map; + +@Component +@Tag(name = "数据统计", description = "数据统计相关操作") +public class DataCountHandler { + + private final IDataCountService dataCountService; + + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + public DataCountHandler(IDataCountService dataCountService) { + this.dataCountService = dataCountService; + } + + @Operation(summary = "获取会员数据统计(按日期)", description = "获取指定日期的会员数据统计") + public Mono getMemberStatisticsByDate(ServerRequest request) { + String dateStr = request.pathVariable("date"); + LocalDate date = LocalDate.parse(dateStr, DATE_FORMATTER); + + return dataCountService.getMemberStatisticsByDate(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 getMemberStatisticsByDateRange(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.getMemberStatisticsByDateRange(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 getMemberStatisticsSummary(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.getMemberStatisticsSummary(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 = "获取指定日期的预约数据统计") + public Mono getBookingStatisticsByDate(ServerRequest request) { + String dateStr = request.pathVariable("date"); + LocalDate date = LocalDate.parse(dateStr, DATE_FORMATTER); + + return dataCountService.getBookingStatisticsByDate(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 getBookingStatisticsByDateRange(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.getBookingStatisticsByDateRange(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 getBookingStatisticsSummary(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.getBookingStatisticsSummary(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 exportMemberStatistics(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 = "member_statistics_" + startDateStr + "_" + endDateStr + ".csv"; + + return dataCountService.exportMemberStatistics(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); + }); + } + + @Operation(summary = "导出预约数据统计", description = "导出指定日期范围内的预约数据统计为CSV文件") + public Mono exportBookingStatistics(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 = "booking_statistics_" + startDateStr + "_" + endDateStr + ".csv"; + + return dataCountService.exportBookingStatistics(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); + }); + } + + @Operation(summary = "导出全部统计数据", description = "导出指定日期范围内的会员和预约数据统计为CSV文件") + public Mono exportAllStatistics(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 = "all_statistics_" + startDateStr + "_" + endDateStr + ".csv"; + + return dataCountService.exportAllStatistics(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 new file mode 100644 index 0000000..73da48c --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/scheduler/StatisticsDailyScheduler.java @@ -0,0 +1,91 @@ +package cn.novalon.gym.manage.datacount.scheduler; + +import cn.novalon.gym.manage.datacount.service.IDataCountService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.time.LocalDate; + +/** + * 数据统计定时任务 + * + * 功能:每日凌晨1点自动计算前一天的数据统计并缓存到Redis + * + * @author 数据统计模块 + * @date 2026-06-06 + */ +@Component +public class StatisticsDailyScheduler { + + private static final Logger logger = LoggerFactory.getLogger(StatisticsDailyScheduler.class); + + private final IDataCountService dataCountService; + + public StatisticsDailyScheduler(IDataCountService dataCountService) { + this.dataCountService = dataCountService; + } + + /** + * 每日凌晨1点执行前一天的统计数据计算并缓存 + * cron表达式:秒 分 时 日 月 周 + */ + @Scheduled(cron = "0 0 1 * * ?") + public void calculateDailyStatistics() { + LocalDate yesterday = LocalDate.now().minusDays(1); + logger.info("定时任务开始计算 {} 的数据统计", yesterday); + + dataCountService.getMemberStatisticsByDate(yesterday) + .doOnSuccess(stats -> logger.info("会员统计计算完成: date={}, newMembers={}, totalMembers={}", + yesterday, stats.getNewMembers(), stats.getTotalMembers())) + .doOnError(error -> logger.error("会员统计计算失败: date={}", yesterday, error)) + .subscribe(); + + dataCountService.getBookingStatisticsByDate(yesterday) + .doOnSuccess(stats -> logger.info("预约统计计算完成: date={}, totalBookings={}, attendanceRate={}%", + yesterday, stats.getTotalBookings(), stats.getAttendanceRate())) + .doOnError(error -> logger.error("预约统计计算失败: date={}", yesterday, error)) + .subscribe(); + } + + /** + * 每周一凌晨2点执行上周的数据汇总统计 + */ + @Scheduled(cron = "0 0 2 * * 1") + public void calculateWeeklyStatistics() { + LocalDate lastWeekEnd = LocalDate.now().minusDays(1); + LocalDate lastWeekStart = lastWeekEnd.minusDays(6); + logger.info("定时任务开始计算上周汇总统计: {} ~ {}", lastWeekStart, lastWeekEnd); + + dataCountService.getMemberStatisticsSummary(lastWeekStart, lastWeekEnd) + .doOnSuccess(stats -> logger.info("上周会员汇总统计完成: newMembers={}", stats.getNewMembers())) + .doOnError(error -> logger.error("上周会员汇总统计失败", error)) + .subscribe(); + + dataCountService.getBookingStatisticsSummary(lastWeekStart, lastWeekEnd) + .doOnSuccess(stats -> logger.info("上周预约汇总统计完成: totalBookings={}", stats.getTotalBookings())) + .doOnError(error -> logger.error("上周预约汇总统计失败", error)) + .subscribe(); + } + + /** + * 每月1号凌晨3点执行上月的数据汇总统计 + */ + @Scheduled(cron = "0 0 3 1 * ?") + public void calculateMonthlyStatistics() { + LocalDate lastMonthEnd = LocalDate.now().minusDays(1); + LocalDate lastMonthStart = lastMonthEnd.withDayOfMonth(1); + logger.info("定时任务开始计算上月汇总统计: {} ~ {}", lastMonthStart, lastMonthEnd); + + dataCountService.getMemberStatisticsSummary(lastMonthStart, lastMonthEnd) + .doOnSuccess(stats -> logger.info("上月会员汇总统计完成: newMembers={}", stats.getNewMembers())) + .doOnError(error -> logger.error("上月会员汇总统计失败", error)) + .subscribe(); + + dataCountService.getBookingStatisticsSummary(lastMonthStart, lastMonthEnd) + .doOnSuccess(stats -> logger.info("上月预约汇总统计完成: totalBookings={}", stats.getTotalBookings())) + .doOnError(error -> logger.error("上月预约汇总统计失败", error)) + .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 new file mode 100644 index 0000000..b70e647 --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/IDataCountService.java @@ -0,0 +1,30 @@ +package cn.novalon.gym.manage.datacount.service; + +import cn.novalon.gym.manage.datacount.domain.BookingStatistics; +import cn.novalon.gym.manage.datacount.domain.MemberStatistics; +import cn.novalon.gym.manage.datacount.domain.StatisticsExportDTO; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.time.LocalDate; + +public interface IDataCountService { + + Mono getMemberStatisticsByDate(LocalDate date); + + Flux getMemberStatisticsByDateRange(LocalDate startDate, LocalDate endDate); + + Mono getMemberStatisticsSummary(LocalDate startDate, LocalDate endDate); + + Mono getBookingStatisticsByDate(LocalDate date); + + Flux getBookingStatisticsByDateRange(LocalDate startDate, LocalDate endDate); + + Mono getBookingStatisticsSummary(LocalDate startDate, LocalDate endDate); + + Mono exportMemberStatistics(LocalDate startDate, LocalDate endDate); + + Mono exportBookingStatistics(LocalDate startDate, LocalDate endDate); + + Mono exportAllStatistics(LocalDate startDate, LocalDate endDate); +} \ 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 new file mode 100644 index 0000000..9d12895 --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/impl/DataCountServiceImpl.java @@ -0,0 +1,318 @@ +package cn.novalon.gym.manage.datacount.service.impl; + +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.service.IDataCountService; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import reactor.core.publisher.Flux; +import reactor.core.publisher.Mono; + +import java.io.ByteArrayOutputStream; +import java.io.OutputStreamWriter; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; + +@Slf4j +@Service +public class DataCountServiceImpl implements IDataCountService { + + private final StatisticsDao statisticsDao; + 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:"; + private static final long CACHE_EXPIRE_SECONDS = 3600; + + public DataCountServiceImpl(StatisticsDao statisticsDao, RedisUtil redisUtil, ObjectMapper objectMapper) { + this.statisticsDao = statisticsDao; + this.redisUtil = redisUtil; + this.objectMapper = objectMapper; + } + + @Override + public Mono getMemberStatisticsByDate(LocalDate date) { + String cacheKey = MEMBER_STATS_CACHE_PREFIX + date.toString(); + return redisUtil.get(cacheKey) + .flatMap(cached -> { + if (cached != null) { + try { + MemberStatistics stats = objectMapper.readValue(cached.toString(), MemberStatistics.class); + log.debug("从缓存获取会员统计数据, date: {}", date); + return Mono.just(stats); + } catch (Exception e) { + log.warn("缓存数据解析失败, date: {}", date, e); + } + } + return Mono.empty(); + }) + .switchIfEmpty(fetchMemberStatisticsByDate(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 fetchMemberStatisticsByDate(LocalDate date) { + return Mono.zip( + statisticsDao.countTotalMembers(), + statisticsDao.countNewMembersByDate(date), + statisticsDao.countActiveMembersByDate(date), + statisticsDao.countMaleMembers(), + statisticsDao.countFemaleMembers(), + statisticsDao.countCardPurchaseByDate(date), + statisticsDao.calculateAverageAge() + ).map(tuple -> { + MemberStatistics stats = new MemberStatistics(date); + stats.setTotalMembers(tuple.getT1()); + stats.setNewMembers(tuple.getT2()); + stats.setActiveMembers(tuple.getT3()); + stats.setMaleMembers(tuple.getT4()); + stats.setFemaleMembers(tuple.getT5()); + stats.setCardPurchaseCount(tuple.getT6()); + stats.setAverageAge(tuple.getT7() != null ? tuple.getT7() : 0.0); + return stats; + }); + } + + @Override + public Flux getMemberStatisticsByDateRange(LocalDate startDate, LocalDate endDate) { + return statisticsDao.findMemberStatisticsByDateRange(startDate, endDate) + .map(row -> { + LocalDate date = (LocalDate) row[0]; + MemberStatistics stats = new MemberStatistics(date); + stats.setNewMembers(((Number) row[1]).intValue()); + return stats; + }); + } + + @Override + public Mono getMemberStatisticsSummary(LocalDate startDate, LocalDate endDate) { + return getMemberStatisticsByDateRange(startDate, endDate) + .collectList() + .flatMap(list -> { + MemberStatistics summary = new MemberStatistics(); + summary.setStartDate(startDate); + summary.setEndDate(endDate); + summary.setNewMembers(list.stream().mapToInt(MemberStatistics::getNewMembers).sum()); + + return Mono.zip( + statisticsDao.countTotalMembers(), + statisticsDao.countMaleMembers(), + statisticsDao.countFemaleMembers(), + statisticsDao.calculateAverageAge() + ).map(tuple -> { + summary.setTotalMembers(tuple.getT1()); + summary.setMaleMembers(tuple.getT2()); + summary.setFemaleMembers(tuple.getT3()); + summary.setAverageAge(tuple.getT4() != null ? tuple.getT4() : 0.0); + return summary; + }); + }); + } + + @Override + public Mono getBookingStatisticsByDate(LocalDate date) { + String cacheKey = BOOKING_STATS_CACHE_PREFIX + date.toString(); + return redisUtil.get(cacheKey) + .flatMap(cached -> { + if (cached != null) { + try { + BookingStatistics stats = objectMapper.readValue(cached.toString(), BookingStatistics.class); + log.debug("从缓存获取预约统计数据, date: {}", date); + return Mono.just(stats); + } catch (Exception e) { + log.warn("缓存数据解析失败, date: {}", date, e); + } + } + return Mono.empty(); + }) + .switchIfEmpty(fetchBookingStatisticsByDate(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 fetchBookingStatisticsByDate(LocalDate date) { + return Mono.zip( + statisticsDao.countTotalBookingsByDate(date), + statisticsDao.countCancelledBookingsByDate(date), + statisticsDao.countAttendedBookingsByDate(date), + statisticsDao.countTotalCoursesByDate(date), + statisticsDao.countFullCoursesByDate(date), + statisticsDao.findMostPopularCourseByDate(date) + ).map(tuple -> { + int totalBookings = tuple.getT1(); + int cancelledBookings = tuple.getT2(); + int attendedBookings = tuple.getT3(); + + BookingStatistics stats = new BookingStatistics(date); + stats.setTotalBookings(totalBookings); + stats.setCancelledBookings(cancelledBookings); + stats.setAttendedBookings(attendedBookings); + stats.setTotalCourses(tuple.getT4()); + stats.setFullCourses(tuple.getT5()); + + if (totalBookings > 0) { + stats.setCancellationRate(Math.round(cancelledBookings * 10000.0 / totalBookings) / 100.0); + stats.setAttendanceRate(Math.round(attendedBookings * 10000.0 / totalBookings) / 100.0); + } + + Object[] popularCourse = tuple.getT6(); + if (popularCourse != null && popularCourse.length >= 2) { + stats.setPopularCourseName((String) popularCourse[0]); + stats.setPopularCourseCount(((Number) popularCourse[1]).intValue()); + } + + return stats; + }); + } + + @Override + public Flux getBookingStatisticsByDateRange(LocalDate startDate, LocalDate endDate) { + return statisticsDao.findBookingStatisticsByDateRange(startDate, endDate) + .map(row -> { + LocalDate date = (LocalDate) row[0]; + int totalBookings = ((Number) row[1]).intValue(); + int cancelledBookings = ((Number) row[2]).intValue(); + int attendedBookings = ((Number) row[3]).intValue(); + + BookingStatistics stats = new BookingStatistics(date); + stats.setTotalBookings(totalBookings); + stats.setCancelledBookings(cancelledBookings); + stats.setAttendedBookings(attendedBookings); + + if (totalBookings > 0) { + stats.setCancellationRate(Math.round(cancelledBookings * 10000.0 / totalBookings) / 100.0); + stats.setAttendanceRate(Math.round(attendedBookings * 10000.0 / totalBookings) / 100.0); + } + + return stats; + }); + } + + @Override + public Mono getBookingStatisticsSummary(LocalDate startDate, LocalDate endDate) { + return getBookingStatisticsByDateRange(startDate, endDate) + .collectList() + .map(list -> { + BookingStatistics summary = new BookingStatistics(); + summary.setStartDate(startDate); + summary.setEndDate(endDate); + summary.setTotalBookings(list.stream().mapToInt(BookingStatistics::getTotalBookings).sum()); + summary.setCancelledBookings(list.stream().mapToInt(BookingStatistics::getCancelledBookings).sum()); + summary.setAttendedBookings(list.stream().mapToInt(BookingStatistics::getAttendedBookings).sum()); + + int total = summary.getTotalBookings(); + if (total > 0) { + summary.setCancellationRate(Math.round(summary.getCancelledBookings() * 10000.0 / total) / 100.0); + summary.setAttendanceRate(Math.round(summary.getAttendedBookings() * 10000.0 / total) / 100.0); + } + + return summary; + }); + } + + @Override + public Mono exportMemberStatistics(LocalDate startDate, LocalDate endDate) { + return getMemberStatisticsByDateRange(startDate, endDate) + .collectList() + .map(this::generateMemberStatisticsCSV); + } + + @Override + public Mono exportBookingStatistics(LocalDate startDate, LocalDate endDate) { + return getBookingStatisticsByDateRange(startDate, endDate) + .collectList() + .map(this::generateBookingStatisticsCSV); + } + + @Override + public Mono exportAllStatistics(LocalDate startDate, LocalDate endDate) { + return Mono.zip( + getMemberStatisticsByDateRange(startDate, endDate).collectList(), + getBookingStatisticsByDateRange(startDate, endDate).collectList() + ).map(tuple -> { + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintWriter writer = new PrintWriter(new OutputStreamWriter(baos, StandardCharsets.UTF_8)); + + writer.println("=== 会员数据统计 ==="); + writer.println(generateMemberStatisticsCSVContent(tuple.getT1())); + writer.println(); + writer.println("=== 预约数据统计 ==="); + writer.println(generateBookingStatisticsCSVContent(tuple.getT2())); + + writer.flush(); + return baos.toByteArray(); + }); + } + + private byte[] generateMemberStatisticsCSV(List statsList) { + return generateMemberStatisticsCSVContent(statsList).getBytes(StandardCharsets.UTF_8); + } + + private String generateMemberStatisticsCSVContent(List statsList) { + StringBuilder sb = new StringBuilder(); + sb.append("统计日期,新增会员数,活跃会员数,总会员数,男性会员数,女性会员数,购卡人数,平均年龄\n"); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + for (MemberStatistics stats : statsList) { + sb.append(stats.getDate().format(formatter)).append(","); + sb.append(stats.getNewMembers()).append(","); + sb.append(stats.getActiveMembers()).append(","); + sb.append(stats.getTotalMembers()).append(","); + sb.append(stats.getMaleMembers()).append(","); + sb.append(stats.getFemaleMembers()).append(","); + sb.append(stats.getCardPurchaseCount()).append(","); + sb.append(String.format("%.1f", stats.getAverageAge())).append("\n"); + } + + return sb.toString(); + } + + private byte[] generateBookingStatisticsCSV(List statsList) { + return generateBookingStatisticsCSVContent(statsList).getBytes(StandardCharsets.UTF_8); + } + + private String generateBookingStatisticsCSVContent(List statsList) { + StringBuilder sb = new StringBuilder(); + sb.append("统计日期,预约总数,取消预约数,实际到场数,取消率(%),到场率(%),热门课程,热门课程预约数,团课总数,满员课程数\n"); + + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + for (BookingStatistics stats : statsList) { + sb.append(stats.getDate().format(formatter)).append(","); + sb.append(stats.getTotalBookings()).append(","); + sb.append(stats.getCancelledBookings()).append(","); + sb.append(stats.getAttendedBookings()).append(","); + sb.append(String.format("%.2f", stats.getCancellationRate())).append(","); + sb.append(String.format("%.2f", stats.getAttendanceRate())).append(","); + sb.append(stats.getPopularCourseName() != null ? stats.getPopularCourseName() : "").append(","); + sb.append(stats.getPopularCourseCount()).append(","); + sb.append(stats.getTotalCourses()).append(","); + sb.append(stats.getFullCourses()).append("\n"); + } + + return sb.toString(); + } +} \ No newline at end of file diff --git a/gym-manage-api/gym-dataCount/src/main/resources/application.yaml b/gym-manage-api/gym-dataCount/src/main/resources/application.yaml new file mode 100644 index 0000000..9d9181a --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/resources/application.yaml @@ -0,0 +1,3 @@ +spring: + application: + name: gym-dataCount diff --git a/gym-manage-api/gym-dataCount/src/test/java/cn/novalon/gym/manage/datacount/GymDataCountApplicationTests.java b/gym-manage-api/gym-dataCount/src/test/java/cn/novalon/gym/manage/datacount/GymDataCountApplicationTests.java new file mode 100644 index 0000000..6e5392e --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/test/java/cn/novalon/gym/manage/datacount/GymDataCountApplicationTests.java @@ -0,0 +1,13 @@ +package cn.novalon.gym.manage.datacount; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class GymDataCountApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/gym-manage-api/log.text b/gym-manage-api/log.text index c2deea4..6ace75f 100644 --- a/gym-manage-api/log.text +++ b/gym-manage-api/log.text @@ -1,200 +1,19 @@ -2026-06-02T14:56:05.966+08:00 TRACE 22380 --- [gym-manage-api] [ctor-http-nio-7] o.s.w.s.adapter.HttpWebHandlerAdapter : [7ce54381-10] HTTP POST "/api/groupCourse/11/cancel", headers={masked} -2026-06-02T14:56:05.967+08:00 DEBUG 22380 --- [gym-manage-api] [ parallel-11] o.s.w.s.s.DefaultWebSessionManager : Created new WebSession. -2026-06-02T14:56:05.967+08:00 INFO 22380 --- [gym-manage-api] [ parallel-11] c.n.g.m.sys.audit.OperationLogWebFilter : WebFilter 拦截请求: POST /api/groupCourse/11/cancel -2026-06-02T14:56:05.967+08:00 INFO 22380 --- [gym-manage-api] [ parallel-11] c.n.g.m.sys.audit.OperationLogWebFilter : 未匹配到操作日志配置,跳过: POST /api/groupCourse/11/cancel -2026-06-02T14:56:05.967+08:00 INFO 22380 --- [gym-manage-api] [ parallel-11] c.n.g.m.sys.audit.OperationLogWebFilter : WebFilter 拦截请求: POST /api/groupCourse/11/cancel -2026-06-02T14:56:05.967+08:00 INFO 22380 --- [gym-manage-api] [ parallel-11] c.n.g.m.sys.audit.OperationLogWebFilter : 未匹配到操作日志配置,跳过: POST /api/groupCourse/11/cancel -2026-06-02T14:56:05.967+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.967+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.967+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/dictionaries" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/{id}/action/change-password" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/{id}/action/logical-delete" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/logical-delete" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/action/restore" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/{id}/action/restore" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/users/{id}/roles" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/menus" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/roles" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/roles/{id}/restore" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/roles/{id}/permissions" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/config" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/logs/login" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/logs/exception" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/logs/operation" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/auth/login" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/auth/register" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/auth/logout" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/dict/types" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/dict/data" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/notices" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/messages" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/files/upload" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/permissions" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.969+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member/auth/miniapp/login" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member/auth/mp/callback" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member/phone/bind" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/admin/member/{id}/phone" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-cards" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/purchase" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/{recordId}/renew" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/{recordId}/use" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/{recordId}/refund" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-records/process-expired" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/member-card-transactions" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/groupCourse/page" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "GET" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/groupCourse" does not match against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "PUT" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "DELETE" does not match against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Method "POST" matches against value "POST" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.server.RequestPredicates : Pattern "/api/groupCourse/{id}/cancel" matches against value "/api/groupCourse/11/cancel" -2026-06-02T14:56:05.970+08:00 TRACE 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.function.server.RouterFunctions : [7ce54381-10] Matched (POST && /api/groupCourse/{id}/cancel) -2026-06-02T14:56:05.970+08:00 DEBUG 22380 --- [gym-manage-api] [ parallel-11] o.s.w.r.f.s.s.RouterFunctionMapping : [7ce54381-10] Mapped to cn.novalon.gym.manage.app.config.SystemRouter$$Lambda/0x00000198019592d8@6903bec9 -2026-06-02T14:56:05.972+08:00 DEBUG 22380 --- [gym-manage-api] [ parallel-11] o.s.r2dbc.core.DefaultDatabaseClient : Executing SQL statement [SELECT group_course.course_name, group_course.coach_id, group_course.course_type, group_course.start_time, group_course.end_time, group_course.max_members, group_course.current_members, group_course.status, group_course.location, group_course.cover_image, group_course.description, group_course.point_card_amount, group_course.stored_value_amount, group_course.id, group_course.create_by, group_course.update_by, group_course.created_at, group_course.updated_at, group_course.deleted_at FROM group_course WHERE group_course.id = $1 AND (group_course.deleted_at IS NULL)] -2026-06-02T14:56:05.976+08:00 INFO 22380 --- [gym-manage-api] [actor-tcp-nio-5] c.n.g.m.g.c.GroupCourseConverter : 转换bean,entity-domain: -2026-06-02T14:56:05.976+08:00 ERROR 22380 --- [gym-manage-api] [actor-tcp-nio-5] c.n.g.m.g.s.impl.GroupCourseService : 团课取消失败 - id=11, error: 课程取消需提前24小时 -2026-06-02T14:56:05.976+08:00 TRACE 22380 --- [gym-manage-api] [actor-tcp-nio-5] org.springframework.web.HttpLogging : [7ce54381-10] Encoding [{success=false, message=课程取消需提前24小时}] -2026-06-02T14:56:05.976+08:00 TRACE 22380 --- [gym-manage-api] [actor-tcp-nio-5] o.s.w.s.adapter.HttpWebHandlerAdapter : [7ce54381-10] Completed 400 BAD_REQUEST, headers={masked} -2026-06-02T14:56:05.976+08:00 TRACE 22380 --- [gym-manage-api] [actor-tcp-nio-5] org.springframework.web.HttpLogging : [7ce54381-1, L:/[0:0:0:0:0:0:0:1]:8084 - R:/[0:0:0:0:0:0:0:1]:57674] Handling completed +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 : 没有需要处理的缺席会员记录 diff --git a/gym-manage-api/manage-app/pom.xml b/gym-manage-api/manage-app/pom.xml index 3bc6fe8..be6b304 100644 --- a/gym-manage-api/manage-app/pom.xml +++ b/gym-manage-api/manage-app/pom.xml @@ -154,6 +154,12 @@ 1.0.0 compile + + cn.novalon.gym.manage + gym-dataCount + 1.0.0 + compile + diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Insert_DataCount_Test_Data.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Insert_DataCount_Test_Data.sql new file mode 100644 index 0000000..91da472 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V13__Insert_DataCount_Test_Data.sql @@ -0,0 +1,443 @@ +-- V13: 数据统计模块测试数据 +-- 测试场景: +-- 1. 会员数据统计(新增会员、活跃会员、性别分布、平均年龄、购卡人数) +-- 2. 预约数据统计(预约总数、取消预约、实际到场、热门课程、满员课程) +-- 3. 数据导出功能测试 +-- 日期范围:2026-06-01 至 2026-06-10 + +-- ============================================ +-- 1. 创建测试会员用户(分布在不同日期注册,不同性别,不同生日) +-- ============================================ + +-- 2026-06-01 注册的会员(5人:3男2女) +INSERT INTO member_user (member_no, nickname, phone, gender, birthday, is_deleted, created_at, updated_at, last_login_at) VALUES + ('MEM_STAT_001', '测试会员_男1', '13900001001', 1, '1995-03-15', false, '2026-06-01 09:00:00', '2026-06-01 09:00:00', '2026-06-05 20:00:00'), + ('MEM_STAT_002', '测试会员_男2', '13900001002', 1, '1990-07-20', false, '2026-06-01 10:30:00', '2026-06-01 10:30:00', '2026-06-06 18:00:00'), + ('MEM_STAT_003', '测试会员_男3', '13900001003', 1, '1988-11-08', false, '2026-06-01 14:00:00', '2026-06-01 14:00:00', '2026-06-04 19:30:00'), + ('MEM_STAT_004', '测试会员_女1', '13900001004', 2, '1998-01-25', false, '2026-06-01 16:00:00', '2026-06-01 16:00:00', '2026-06-06 08:00:00'), + ('MEM_STAT_005', '测试会员_女2', '13900001005', 2, '1992-05-10', false, '2026-06-01 18:00:00', '2026-06-01 18:00:00', '2026-06-03 21:00:00'); + +-- 2026-06-02 注册的会员(4人:2男2女) +INSERT INTO member_user (member_no, nickname, phone, gender, birthday, is_deleted, created_at, updated_at, last_login_at) VALUES + ('MEM_STAT_006', '测试会员_男4', '13900001006', 1, '1985-09-12', false, '2026-06-02 09:00:00', '2026-06-02 09:00:00', '2026-06-06 10:00:00'), + ('MEM_STAT_007', '测试会员_男5', '13900001007', 1, '1996-04-18', false, '2026-06-02 11:00:00', '2026-06-02 11:00:00', '2026-06-05 15:00:00'), + ('MEM_STAT_008', '测试会员_女3', '13900001008', 2, '1993-08-30', false, '2026-06-02 13:00:00', '2026-06-02 13:00:00', '2026-06-06 12:00:00'), + ('MEM_STAT_009', '测试会员_女4', '13900001009', 2, '1991-12-05', false, '2026-06-02 15:00:00', '2026-06-02 15:00:00', '2026-06-04 14:00:00'); + +-- 2026-06-03 注册的会员(3人:1男2女) +INSERT INTO member_user (member_no, nickname, phone, gender, birthday, is_deleted, created_at, updated_at, last_login_at) VALUES + ('MEM_STAT_010', '测试会员_男6', '13900001010', 1, '1989-06-22', false, '2026-06-03 10:00:00', '2026-06-03 10:00:00', '2026-06-06 09:00:00'), + ('MEM_STAT_011', '测试会员_女5', '13900001011', 2, '1997-02-14', false, '2026-06-03 14:00:00', '2026-06-03 14:00:00', '2026-06-05 16:00:00'), + ('MEM_STAT_012', '测试会员_女6', '13900001012', 2, '1994-10-28', false, '2026-06-03 16:00:00', '2026-06-03 16:00:00', '2026-06-06 11:00:00'); + +-- 2026-06-04 注册的会员(2人:1男1女) +INSERT INTO member_user (member_no, nickname, phone, gender, birthday, is_deleted, created_at, updated_at, last_login_at) VALUES + ('MEM_STAT_013', '测试会员_男7', '13900001013', 1, '1987-01-03', false, '2026-06-04 09:00:00', '2026-06-04 09:00:00', '2026-06-06 13:00:00'), + ('MEM_STAT_014', '测试会员_女7', '13900001014', 2, '1999-07-17', false, '2026-06-04 11:00:00', '2026-06-04 11:00:00', '2026-06-05 17:00:00'); + +-- 2026-06-05 注册的会员(3人:2男1女) +INSERT INTO member_user (member_no, nickname, phone, gender, birthday, is_deleted, created_at, updated_at, last_login_at) VALUES + ('MEM_STAT_015', '测试会员_男8', '13900001015', 1, '1990-03-09', false, '2026-06-05 10:00:00', '2026-06-05 10:00:00', '2026-06-06 14:00:00'), + ('MEM_STAT_016', '测试会员_男9', '13900001016', 1, '1986-11-21', false, '2026-06-05 13:00:00', '2026-06-05 13:00:00', '2026-06-06 15:00:00'), + ('MEM_STAT_017', '测试会员_女8', '13900001017', 2, '1995-09-06', false, '2026-06-05 15:00:00', '2026-06-05 15:00:00', '2026-06-05 18:00:00'); + +-- 2026-06-06 注册的会员(2人:1男1女) +INSERT INTO member_user (member_no, nickname, phone, gender, birthday, is_deleted, created_at, updated_at, last_login_at) VALUES + ('MEM_STAT_018', '测试会员_男10', '13900001018', 1, '1993-05-14', false, '2026-06-06 09:00:00', '2026-06-06 09:00:00', '2026-06-06 16:00:00'), + ('MEM_STAT_019', '测试会员_女9', '13900001019', 2, '1991-08-19', false, '2026-06-06 11:00:00', '2026-06-06 11:00:00', '2026-06-06 17:00:00'); + +-- 已删除的会员(1人,用于测试软删除过滤) +INSERT INTO member_user (member_no, nickname, phone, gender, birthday, is_deleted, created_at, updated_at, last_login_at) VALUES + ('MEM_STAT_DEL', '已删除会员', '13900001999', 1, '1980-01-01', true, '2026-06-01 08:00:00', '2026-06-01 08:00:00', '2026-06-01 08:00:00'); + +-- ============================================ +-- 2. 创建会员卡类型(如果不存在) +-- ============================================ + +INSERT INTO member_card (member_card_name, member_card_type, member_card_price, member_card_validity_days, member_card_total_times, member_card_amount, member_card_status, extra_config, created_at, updated_at) +SELECT '月卡', 'TIME_CARD', 299.00, 30, NULL, NULL, 1, '{}', '2026-01-01 00:00:00', '2026-01-01 00:00:00' +WHERE NOT EXISTS (SELECT 1 FROM member_card WHERE member_card_name = '月卡'); + +INSERT INTO member_card (member_card_name, member_card_type, member_card_price, member_card_validity_days, member_card_total_times, member_card_amount, member_card_status, extra_config, created_at, updated_at) +SELECT '季卡', 'TIME_CARD', 799.00, 90, NULL, NULL, 1, '{}', '2026-01-01 00:00:00', '2026-01-01 00:00:00' +WHERE NOT EXISTS (SELECT 1 FROM member_card WHERE member_card_name = '季卡'); + +INSERT INTO member_card (member_card_name, member_card_type, member_card_price, member_card_validity_days, member_card_total_times, member_card_amount, member_card_status, extra_config, created_at, updated_at) +SELECT '次卡20次', 'COUNT_CARD', 599.00, NULL, 20, NULL, 1, '{}', '2026-01-01 00:00:00', '2026-01-01 00:00:00' +WHERE NOT EXISTS (SELECT 1 FROM member_card WHERE member_card_name = '次卡20次'); + +-- ============================================ +-- 3. 为所有测试会员创建会员卡记录 +-- ============================================ + +-- MEM_STAT_001 至 MEM_STAT_019 全部创建会员卡记录 +-- 使用月卡类型,有效期至 2026-12-31 +INSERT INTO member_card_record (member_id, member_card_id, status, remaining_times, remaining_amount, expire_time, purchase_time, version, card_composition, created_at, updated_at) +SELECT + m.id, + (SELECT id FROM member_card WHERE member_card_name = '月卡'), + 'ACTIVE', + 0, + 0.00, + '2026-12-31 23:59:59', + m.created_at, + 0, + '{}', + m.created_at, + m.created_at +FROM member_user m +WHERE m.member_no LIKE 'MEM_STAT_%' AND m.member_no != 'MEM_STAT_DEL'; + +-- ============================================ +-- 4. 创建团课课程(2026-06-01 至 2026-06-10) +-- ============================================ + +-- 2026-06-01 的课程 +INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, description, point_card_amount, stored_value_amount, create_by, created_at, updated_at) VALUES + ('晨间瑜伽', 101, 1, '2026-06-01 07:00:00', '2026-06-01 08:00:00', 15, 12, '0', '瑜伽教室A', '清晨唤醒瑜伽', 1, 30.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('燃脂单车', 102, 2, '2026-06-01 18:00:00', '2026-06-01 19:00:00', 25, 25, '0', '单车房', '高强度燃脂骑行', 1, 35.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('普拉提核心', 103, 1, '2026-06-01 19:30:00', '2026-06-01 20:30:00', 12, 8, '0', '普拉提教室', '核心力量训练', 1, 40.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'); + +-- 2026-06-02 的课程 +INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, description, point_card_amount, stored_value_amount, create_by, created_at, updated_at) VALUES + ('流瑜伽', 101, 1, '2026-06-02 09:00:00', '2026-06-02 10:30:00', 15, 10, '0', '瑜伽教室A', '流畅体式连接', 1, 30.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('搏击操', 102, 2, '2026-06-02 18:30:00', '2026-06-02 19:30:00', 20, 18, '0', '综合训练区', '有氧搏击训练', 1, 35.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('冥想放松', 101, 1, '2026-06-02 20:00:00', '2026-06-02 21:00:00', 20, 5, '0', '冥想室', '深度放松冥想', 1, 25.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'); + +-- 2026-06-03 的课程 +INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, description, point_card_amount, stored_value_amount, create_by, created_at, updated_at) VALUES + ('哈他瑜伽', 101, 1, '2026-06-03 10:00:00', '2026-06-03 11:30:00', 12, 12, '0', '瑜伽教室B', '基础哈他瑜伽', 1, 30.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('动感单车', 102, 2, '2026-06-03 19:00:00', '2026-06-03 20:00:00', 25, 20, '0', '单车房', '音乐节奏骑行', 1, 35.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('拉伸放松', 103, 1, '2026-06-03 20:30:00', '2026-06-03 21:30:00', 15, 6, '0', '综合训练区', '全身拉伸放松', 1, 25.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'); + +-- 2026-06-04 的课程 +INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, description, point_card_amount, stored_value_amount, create_by, created_at, updated_at) VALUES + ('力量瑜伽', 101, 1, '2026-06-04 08:00:00', '2026-06-04 09:30:00', 15, 14, '0', '瑜伽教室A', '力量型瑜伽', 1, 35.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('HIIT训练', 102, 2, '2026-06-04 18:00:00', '2026-06-04 19:00:00', 20, 20, '0', '综合训练区', '高强度间歇训练', 1, 40.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('阴瑜伽', 101, 1, '2026-06-04 20:00:00', '2026-06-04 21:30:00', 12, 9, '0', '瑜伽教室B', '静态阴瑜伽', 1, 30.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'); + +-- 2026-06-05 的课程 +INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, description, point_card_amount, stored_value_amount, create_by, created_at, updated_at) VALUES + ('晨间冥想', 101, 1, '2026-06-05 07:00:00', '2026-06-05 08:00:00', 20, 15, '0', '冥想室', '清晨冥想', 1, 25.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('尊巴舞', 103, 2, '2026-06-05 18:30:00', '2026-06-05 19:30:00', 25, 22, '0', '舞蹈室', '拉丁风格舞蹈', 1, 35.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('空中瑜伽', 101, 1, '2026-06-05 20:00:00', '2026-06-05 21:30:00', 10, 10, '0', '瑜伽教室A', '空中瑜伽体验', 1, 45.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'); + +-- 2026-06-06 的课程 +INSERT INTO group_course (course_name, coach_id, course_type, start_time, end_time, max_members, current_members, status, location, description, point_card_amount, stored_value_amount, create_by, created_at, updated_at) VALUES + ('拜日式瑜伽', 101, 1, '2026-06-06 07:30:00', '2026-06-06 08:30:00', 15, 11, '0', '瑜伽教室A', '传统拜日式', 1, 30.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('杠铃操', 102, 2, '2026-06-06 18:00:00', '2026-06-06 19:00:00', 20, 16, '0', '力量区', '全身杠铃训练', 1, 35.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'), + ('修复瑜伽', 101, 1, '2026-06-06 20:00:00', '2026-06-06 21:00:00', 12, 7, '0', '瑜伽教室B', '修复性瑜伽', 1, 30.00, 'admin', '2026-05-28 10:00:00', '2026-05-28 10:00:00'); + +-- ============================================ +-- 5. 团课预约记录(测试预约统计) +-- ============================================ + +-- 2026-06-01 的预约记录 +-- 晨间瑜伽:5人预约(4人出席,1人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) VALUES + ((SELECT id FROM group_course WHERE course_name = '晨间瑜伽' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_001'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_001')), '2026-06-01 06:00:00', '2', '晨间瑜伽', '2026-06-01 07:00:00', '2026-06-01 08:00:00', '瑜伽教室A', '2026-06-01 06:00:00', '2026-06-01 06:00:00'), + ((SELECT id FROM group_course WHERE course_name = '晨间瑜伽' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_002'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_002')), '2026-06-01 06:30:00', '2', '晨间瑜伽', '2026-06-01 07:00:00', '2026-06-01 08:00:00', '瑜伽教室A', '2026-06-01 06:30:00', '2026-06-01 06:30:00'), + ((SELECT id FROM group_course WHERE course_name = '晨间瑜伽' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_003'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_003')), '2026-06-01 06:45:00', '2', '晨间瑜伽', '2026-06-01 07:00:00', '2026-06-01 08:00:00', '瑜伽教室A', '2026-06-01 06:45:00', '2026-06-01 06:45:00'), + ((SELECT id FROM group_course WHERE course_name = '晨间瑜伽' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_004'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_004')), '2026-06-01 06:15:00', '1', '晨间瑜伽', '2026-06-01 07:00:00', '2026-06-01 08:00:00', '瑜伽教室A', '2026-06-01 06:15:00', '2026-06-01 06:15:00'), + ((SELECT id FROM group_course WHERE course_name = '晨间瑜伽' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_005'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_005')), '2026-06-01 06:20:00', '2', '晨间瑜伽', '2026-06-01 07:00:00', '2026-06-01 08:00:00', '瑜伽教室A', '2026-06-01 06:20:00', '2026-06-01 06:20:00'); + +-- 燃脂单车:19人预约(15人出席,4人取消)- 满员课程 +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '燃脂单车' AND DATE(start_time) = '2026-06-01'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-01 17:00:00', + CASE WHEN i <= 15 THEN '2' ELSE '1' END, + '燃脂单车', + '2026-06-01 18:00:00', + '2026-06-01 19:00:00', + '单车房', + '2026-06-01 17:00:00', + '2026-06-01 17:00:00' +FROM generate_series(1, 19) AS i; + +-- 普拉提核心:8人预约(7人出席,1人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) VALUES + ((SELECT id FROM group_course WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_001'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_001')), '2026-06-01 19:00:00', '2', '普拉提核心', '2026-06-01 19:30:00', '2026-06-01 20:30:00', '普拉提教室', '2026-06-01 19:00:00', '2026-06-01 19:00:00'), + ((SELECT id FROM group_course WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_003'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_003')), '2026-06-01 19:10:00', '2', '普拉提核心', '2026-06-01 19:30:00', '2026-06-01 20:30:00', '普拉提教室', '2026-06-01 19:10:00', '2026-06-01 19:10:00'), + ((SELECT id FROM group_course WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_005'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_005')), '2026-06-01 19:05:00', '2', '普拉提核心', '2026-06-01 19:30:00', '2026-06-01 20:30:00', '普拉提教室', '2026-06-01 19:05:00', '2026-06-01 19:05:00'), + ((SELECT id FROM group_course WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_007'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_007')), '2026-06-01 19:15:00', '2', '普拉提核心', '2026-06-01 19:30:00', '2026-06-01 20:30:00', '普拉提教室', '2026-06-01 19:15:00', '2026-06-01 19:15:00'), + ((SELECT id FROM group_course WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_009'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_009')), '2026-06-01 19:20:00', '1', '普拉提核心', '2026-06-01 19:30:00', '2026-06-01 20:30:00', '普拉提教室', '2026-06-01 19:20:00', '2026-06-01 19:20:00'), + ((SELECT id FROM group_course WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_011'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_011')), '2026-06-01 19:25:00', '2', '普拉提核心', '2026-06-01 19:30:00', '2026-06-01 20:30:00', '普拉提教室', '2026-06-01 19:25:00', '2026-06-01 19:25:00'), + ((SELECT id FROM group_course WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_013'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_013')), '2026-06-01 19:30:00', '2', '普拉提核心', '2026-06-01 19:30:00', '2026-06-01 20:30:00', '普拉提教室', '2026-06-01 19:30:00', '2026-06-01 19:30:00'), + ((SELECT id FROM group_course WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_015'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_015')), '2026-06-01 19:35:00', '2', '普拉提核心', '2026-06-01 19:30:00', '2026-06-01 20:30:00', '普拉提教室', '2026-06-01 19:35:00', '2026-06-01 19:35:00'); + +-- 2026-06-02 的预约记录 +-- 流瑜伽:10人预约(9人出席,1人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '流瑜伽' AND DATE(start_time) = '2026-06-02'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-02 08:00:00', + CASE WHEN i <= 9 THEN '2' ELSE '1' END, + '流瑜伽', + '2026-06-02 09:00:00', + '2026-06-02 10:30:00', + '瑜伽教室A', + '2026-06-02 08:00:00', + '2026-06-02 08:00:00' +FROM generate_series(1, 10) AS i; + +-- 搏击操:18人预约(15人出席,3人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '搏击操' AND DATE(start_time) = '2026-06-02'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-02 17:30:00', + CASE WHEN i <= 15 THEN '2' ELSE '1' END, + '搏击操', + '2026-06-02 18:30:00', + '2026-06-02 19:30:00', + '综合训练区', + '2026-06-02 17:30:00', + '2026-06-02 17:30:00' +FROM generate_series(1, 18) AS i; + +-- 冥想放松:5人预约(4人出席,1人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) VALUES + ((SELECT id FROM group_course WHERE course_name = '冥想放松' AND DATE(start_time) = '2026-06-02'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_001'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_001')), '2026-06-02 19:30:00', '2', '冥想放松', '2026-06-02 20:00:00', '2026-06-02 21:00:00', '冥想室', '2026-06-02 19:30:00', '2026-06-02 19:30:00'), + ((SELECT id FROM group_course WHERE course_name = '冥想放松' AND DATE(start_time) = '2026-06-02'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_004'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_004')), '2026-06-02 19:35:00', '2', '冥想放松', '2026-06-02 20:00:00', '2026-06-02 21:00:00', '冥想室', '2026-06-02 19:35:00', '2026-06-02 19:35:00'), + ((SELECT id FROM group_course WHERE course_name = '冥想放松' AND DATE(start_time) = '2026-06-02'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_007'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_007')), '2026-06-02 19:40:00', '2', '冥想放松', '2026-06-02 20:00:00', '2026-06-02 21:00:00', '冥想室', '2026-06-02 19:40:00', '2026-06-02 19:40:00'), + ((SELECT id FROM group_course WHERE course_name = '冥想放松' AND DATE(start_time) = '2026-06-02'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_010'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_010')), '2026-06-02 19:45:00', '1', '冥想放松', '2026-06-02 20:00:00', '2026-06-02 21:00:00', '冥想室', '2026-06-02 19:45:00', '2026-06-02 19:45:00'), + ((SELECT id FROM group_course WHERE course_name = '冥想放松' AND DATE(start_time) = '2026-06-02'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_013'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_013')), '2026-06-02 19:50:00', '2', '冥想放松', '2026-06-02 20:00:00', '2026-06-02 21:00:00', '冥想室', '2026-06-02 19:50:00', '2026-06-02 19:50:00'); + +-- 2026-06-03 的预约记录 +-- 哈他瑜伽:12人预约(10人出席,2人取消)- 满员 +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '哈他瑜伽' AND DATE(start_time) = '2026-06-03'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-03 09:00:00', + CASE WHEN i <= 10 THEN '2' ELSE '1' END, + '哈他瑜伽', + '2026-06-03 10:00:00', + '2026-06-03 11:30:00', + '瑜伽教室B', + '2026-06-03 09:00:00', + '2026-06-03 09:00:00' +FROM generate_series(1, 12) AS i; + +-- 动感单车:19人预约(16人出席,3人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '动感单车' AND DATE(start_time) = '2026-06-03'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-03 18:00:00', + CASE WHEN i <= 16 THEN '2' ELSE '1' END, + '动感单车', + '2026-06-03 19:00:00', + '2026-06-03 20:00:00', + '单车房', + '2026-06-03 18:00:00', + '2026-06-03 18:00:00' +FROM generate_series(1, 19) AS i; + +-- 拉伸放松:6人预约(5人出席,1人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) VALUES + ((SELECT id FROM group_course WHERE course_name = '拉伸放松' AND DATE(start_time) = '2026-06-03'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_002'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_002')), '2026-06-03 20:00:00', '2', '拉伸放松', '2026-06-03 20:30:00', '2026-06-03 21:30:00', '综合训练区', '2026-06-03 20:00:00', '2026-06-03 20:00:00'), + ((SELECT id FROM group_course WHERE course_name = '拉伸放松' AND DATE(start_time) = '2026-06-03'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_005'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_005')), '2026-06-03 20:05:00', '2', '拉伸放松', '2026-06-03 20:30:00', '2026-06-03 21:30:00', '综合训练区', '2026-06-03 20:05:00', '2026-06-03 20:05:00'), + ((SELECT id FROM group_course WHERE course_name = '拉伸放松' AND DATE(start_time) = '2026-06-03'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_008'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_008')), '2026-06-03 20:10:00', '2', '拉伸放松', '2026-06-03 20:30:00', '2026-06-03 21:30:00', '综合训练区', '2026-06-03 20:10:00', '2026-06-03 20:10:00'), + ((SELECT id FROM group_course WHERE course_name = '拉伸放松' AND DATE(start_time) = '2026-06-03'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_011'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_011')), '2026-06-03 20:15:00', '1', '拉伸放松', '2026-06-03 20:30:00', '2026-06-03 21:30:00', '综合训练区', '2026-06-03 20:15:00', '2026-06-03 20:15:00'), + ((SELECT id FROM group_course WHERE course_name = '拉伸放松' AND DATE(start_time) = '2026-06-03'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_014'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_014')), '2026-06-03 20:20:00', '2', '拉伸放松', '2026-06-03 20:30:00', '2026-06-03 21:30:00', '综合训练区', '2026-06-03 20:20:00', '2026-06-03 20:20:00'), + ((SELECT id FROM group_course WHERE course_name = '拉伸放松' AND DATE(start_time) = '2026-06-03'), (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_017'), (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_017')), '2026-06-03 20:25:00', '2', '拉伸放松', '2026-06-03 20:30:00', '2026-06-03 21:30:00', '综合训练区', '2026-06-03 20:25:00', '2026-06-03 20:25:00'); + +-- 2026-06-04 的预约记录 +-- 力量瑜伽:14人预约(12人出席,2人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '力量瑜伽' AND DATE(start_time) = '2026-06-04'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-04 07:00:00', + CASE WHEN i <= 12 THEN '2' ELSE '1' END, + '力量瑜伽', + '2026-06-04 08:00:00', + '2026-06-04 09:30:00', + '瑜伽教室A', + '2026-06-04 07:00:00', + '2026-06-04 07:00:00' +FROM generate_series(1, 14) AS i; + +-- HIIT训练:19人预约(16人出席,3人取消)- 满员 +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = 'HIIT训练' AND DATE(start_time) = '2026-06-04'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-04 17:00:00', + CASE WHEN i <= 16 THEN '2' ELSE '1' END, + 'HIIT训练', + '2026-06-04 18:00:00', + '2026-06-04 19:00:00', + '综合训练区', + '2026-06-04 17:00:00', + '2026-06-04 17:00:00' +FROM generate_series(1, 19) AS i; + +-- 阴瑜伽:9人预约(8人出席,1人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '阴瑜伽' AND DATE(start_time) = '2026-06-04'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-04 19:30:00', + CASE WHEN i <= 8 THEN '2' ELSE '1' END, + '阴瑜伽', + '2026-06-04 20:00:00', + '2026-06-04 21:30:00', + '瑜伽教室B', + '2026-06-04 19:30:00', + '2026-06-04 19:30:00' +FROM generate_series(1, 9) AS i; + +-- 2026-06-05 的预约记录 +-- 晨间冥想:15人预约(13人出席,2人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '晨间冥想' AND DATE(start_time) = '2026-06-05'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-05 06:00:00', + CASE WHEN i <= 13 THEN '2' ELSE '1' END, + '晨间冥想', + '2026-06-05 07:00:00', + '2026-06-05 08:00:00', + '冥想室', + '2026-06-05 06:00:00', + '2026-06-05 06:00:00' +FROM generate_series(1, 15) AS i; + +-- 尊巴舞:19人预约(16人出席,3人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '尊巴舞' AND DATE(start_time) = '2026-06-05'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-05 17:30:00', + CASE WHEN i <= 16 THEN '2' ELSE '1' END, + '尊巴舞', + '2026-06-05 18:30:00', + '2026-06-05 19:30:00', + '舞蹈室', + '2026-06-05 17:30:00', + '2026-06-05 17:30:00' +FROM generate_series(1, 19) AS i; + +-- 空中瑜伽:10人预约(9人出席,1人取消)- 满员 +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '空中瑜伽' AND DATE(start_time) = '2026-06-05'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-05 19:00:00', + CASE WHEN i <= 9 THEN '2' ELSE '1' END, + '空中瑜伽', + '2026-06-05 20:00:00', + '2026-06-05 21:30:00', + '瑜伽教室A', + '2026-06-05 19:00:00', + '2026-06-05 19:00:00' +FROM generate_series(1, 10) AS i; + +-- 2026-06-06 的预约记录 +-- 拜日式瑜伽:11人预约(10人出席,1人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '拜日式瑜伽' AND DATE(start_time) = '2026-06-06'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-06 06:30:00', + CASE WHEN i <= 10 THEN '2' ELSE '1' END, + '拜日式瑜伽', + '2026-06-06 07:30:00', + '2026-06-06 08:30:00', + '瑜伽教室A', + '2026-06-06 06:30:00', + '2026-06-06 06:30:00' +FROM generate_series(1, 11) AS i; + +-- 杠铃操:16人预约(14人出席,2人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '杠铃操' AND DATE(start_time) = '2026-06-06'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-06 17:00:00', + CASE WHEN i <= 14 THEN '2' ELSE '1' END, + '杠铃操', + '2026-06-06 18:00:00', + '2026-06-06 19:00:00', + '力量区', + '2026-06-06 17:00:00', + '2026-06-06 17:00:00' +FROM generate_series(1, 16) AS i; + +-- 修复瑜伽:7人预约(6人出席,1人取消) +INSERT INTO group_course_booking (course_id, member_id, member_card_id, booking_time, status, course_name, course_start_time, course_end_time, location, created_at, updated_at) +SELECT + (SELECT id FROM group_course WHERE course_name = '修复瑜伽' AND DATE(start_time) = '2026-06-06'), + (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0')), + (SELECT id FROM member_card_record WHERE member_id = (SELECT id FROM member_user WHERE member_no = 'MEM_STAT_' || LPAD(i::text, 3, '0'))), + '2026-06-06 19:30:00', + CASE WHEN i <= 6 THEN '2' ELSE '1' END, + '修复瑜伽', + '2026-06-06 20:00:00', + '2026-06-06 21:00:00', + '瑜伽教室B', + '2026-06-06 19:30:00', + '2026-06-06 19:30:00' +FROM generate_series(1, 7) AS i; + +-- ============================================ +-- 6. 更新团课当前预约人数(与实际预约一致) +-- ============================================ + +UPDATE group_course SET current_members = 5 WHERE course_name = '晨间瑜伽' AND DATE(start_time) = '2026-06-01'; +UPDATE group_course SET current_members = 19 WHERE course_name = '燃脂单车' AND DATE(start_time) = '2026-06-01'; +UPDATE group_course SET current_members = 8 WHERE course_name = '普拉提核心' AND DATE(start_time) = '2026-06-01'; + +UPDATE group_course SET current_members = 10 WHERE course_name = '流瑜伽' AND DATE(start_time) = '2026-06-02'; +UPDATE group_course SET current_members = 18 WHERE course_name = '搏击操' AND DATE(start_time) = '2026-06-02'; +UPDATE group_course SET current_members = 5 WHERE course_name = '冥想放松' AND DATE(start_time) = '2026-06-02'; + +UPDATE group_course SET current_members = 12 WHERE course_name = '哈他瑜伽' AND DATE(start_time) = '2026-06-03'; +UPDATE group_course SET current_members = 19 WHERE course_name = '动感单车' AND DATE(start_time) = '2026-06-03'; +UPDATE group_course SET current_members = 6 WHERE course_name = '拉伸放松' AND DATE(start_time) = '2026-06-03'; + +UPDATE group_course SET current_members = 14 WHERE course_name = '力量瑜伽' AND DATE(start_time) = '2026-06-04'; +UPDATE group_course SET current_members = 19 WHERE course_name = 'HIIT训练' AND DATE(start_time) = '2026-06-04'; +UPDATE group_course SET current_members = 9 WHERE course_name = '阴瑜伽' AND DATE(start_time) = '2026-06-04'; + +UPDATE group_course SET current_members = 15 WHERE course_name = '晨间冥想' AND DATE(start_time) = '2026-06-05'; +UPDATE group_course SET current_members = 19 WHERE course_name = '尊巴舞' AND DATE(start_time) = '2026-06-05'; +UPDATE group_course SET current_members = 10 WHERE course_name = '空中瑜伽' AND DATE(start_time) = '2026-06-05'; + +UPDATE group_course SET current_members = 11 WHERE course_name = '拜日式瑜伽' AND DATE(start_time) = '2026-06-06'; +UPDATE group_course SET current_members = 16 WHERE course_name = '杠铃操' AND DATE(start_time) = '2026-06-06'; +UPDATE group_course SET current_members = 7 WHERE course_name = '修复瑜伽' AND DATE(start_time) = '2026-06-06'; + +-- ============================================ +-- 7. 测试场景说明 +-- ============================================ + +-- 会员数据统计预期结果(2026-06-01 至 2026-06-06): +-- 总会员数:19人(MEM_STAT_001 至 MEM_STAT_019,不含已删除的 MEM_STAT_DEL) +-- 男性会员:10人(001,002,003,006,007,010,013,015,016,018) +-- 女性会员:9人(004,005,008,009,011,012,014,017,019) +-- 2026-06-01 新增:5人 +-- 2026-06-02 新增:4人 +-- 2026-06-03 新增:3人 +-- 2026-06-04 新增:2人 +-- 2026-06-05 新增:3人 +-- 2026-06-06 新增:2人 +-- 所有会员都有会员卡记录(用于预约) \ No newline at end of file From 9446f66567c0b28d01e15d7720e0d8bf0a35e2d4 Mon Sep 17 00:00:00 2001 From: liwentao Date: Sat, 6 Jun 2026 15:19:40 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=A8=A1=E5=9D=974?= =?UTF-8?q?=EF=BC=8C4.1=E5=9F=BA=E7=A1=80=E6=95=B0=E6=8D=AE=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E7=9A=84=E9=83=A8=E5=88=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- gym-manage-api/pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gym-manage-api/pom.xml b/gym-manage-api/pom.xml index 6ececde..e92c517 100644 --- a/gym-manage-api/pom.xml +++ b/gym-manage-api/pom.xml @@ -44,7 +44,7 @@ manage-file gym-member gym-groupCourse - gym-checkIn + gym-dataCount From 54a65b2a74737fac366ba86fbb9b74ba32a34ac5 Mon Sep 17 00:00:00 2001 From: liwentao Date: Tue, 9 Jun 2026 18:15:38 +0800 Subject: [PATCH 4/5] =?UTF-8?q?=E5=AE=8C=E5=96=84=E6=A8=A1=E5=9D=974?= =?UTF-8?q?=EF=BC=8C4.1=E5=9F=BA=E7=A1=80=E6=95=B0=E6=8D=AE=E7=BB=9F?= =?UTF-8?q?=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 From 2366a3fd73d529f2f14269d64fdcaa931c99fa71 Mon Sep 17 00:00:00 2001 From: liwentao Date: Sat, 6 Jun 2026 15:19:40 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E5=AE=8C=E6=88=90=E6=A8=A1=E5=9D=974?= =?UTF-8?q?=EF=BC=8C4.1=E5=9F=BA=E7=A1=80=E6=95=B0=E6=8D=AE=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E7=9A=84=E9=83=A8=E5=88=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../datacount/GymDataCountApplication.java | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/GymDataCountApplication.java 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 new file mode 100644 index 0000000..b17a83e --- /dev/null +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/GymDataCountApplication.java @@ -0,0 +1,29 @@ +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