diff --git a/gym-manage-api/gym-dataCount/pom.xml b/gym-manage-api/gym-dataCount/pom.xml index 32e1c3f..9b17795 100644 --- a/gym-manage-api/gym-dataCount/pom.xml +++ b/gym-manage-api/gym-dataCount/pom.xml @@ -67,7 +67,12 @@ org.apache.poi poi-ooxml - 5.2.5 + + + + + org.apache.commons + commons-compress diff --git a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/DataStatistics.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/DataStatistics.java index 26e116d..5817fe5 100644 --- a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/DataStatistics.java +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/domain/DataStatistics.java @@ -59,6 +59,12 @@ public class DataStatistics { public static final String WEEK = "WEEK"; /** 月统计 */ public static final String MONTH = "MONTH"; + /** 近30天 */ + public static final String LAST_30_DAYS = "LAST_30_DAYS"; + /** 近90天 */ + public static final String LAST_90_DAYS = "LAST_90_DAYS"; + /** 今年 */ + public static final String YEAR = "YEAR"; private PeriodType() {} } diff --git a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/impl/DataStatisticsServiceImpl.java b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/impl/DataStatisticsServiceImpl.java index d2185e4..074d29f 100644 --- a/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/impl/DataStatisticsServiceImpl.java +++ b/gym-manage-api/gym-dataCount/src/main/java/cn/novalon/gym/manage/datacount/service/impl/DataStatisticsServiceImpl.java @@ -458,13 +458,16 @@ public class DataStatisticsServiceImpl implements IDataStatisticsService { String periodType = query.getPeriodType(); if (DataStatistics.PeriodType.WEEK.equals(periodType)) { - // 周统计:本周一 return today.with(TemporalAdjusters.previousOrSame(DayOfWeek.MONDAY)).atStartOfDay(); } else if (DataStatistics.PeriodType.MONTH.equals(periodType)) { - // 月统计:本月第一天 return today.withDayOfMonth(1).atStartOfDay(); + } else if (DataStatistics.PeriodType.LAST_30_DAYS.equals(periodType)) { + return today.minusDays(29).atStartOfDay(); + } else if (DataStatistics.PeriodType.LAST_90_DAYS.equals(periodType)) { + return today.minusDays(89).atStartOfDay(); + } else if (DataStatistics.PeriodType.YEAR.equals(periodType)) { + return today.withDayOfYear(1).atStartOfDay(); } else { - // 日统计:当天零点 return today.atStartOfDay(); } } @@ -478,13 +481,14 @@ public class DataStatisticsServiceImpl implements IDataStatisticsService { String periodType = query.getPeriodType(); if (DataStatistics.PeriodType.WEEK.equals(periodType)) { - // 周统计:本周日 23:59:59 return today.with(TemporalAdjusters.nextOrSame(DayOfWeek.SUNDAY)).atTime(23, 59, 59); } else if (DataStatistics.PeriodType.MONTH.equals(periodType)) { - // 月统计:本月最后一天 23:59:59 return today.with(TemporalAdjusters.lastDayOfMonth()).atTime(23, 59, 59); + } else if (DataStatistics.PeriodType.LAST_30_DAYS.equals(periodType) + || DataStatistics.PeriodType.LAST_90_DAYS.equals(periodType) + || DataStatistics.PeriodType.YEAR.equals(periodType)) { + return LocalDateTime.now(); } else { - // 日统计:当前时间 return LocalDateTime.now(); } } @@ -503,6 +507,15 @@ public class DataStatisticsServiceImpl implements IDataStatisticsService { } else if (DataStatistics.PeriodType.MONTH.equals(periodType)) { startTime = date.withDayOfMonth(1).atStartOfDay(); endTime = date.with(TemporalAdjusters.lastDayOfMonth()).atTime(23, 59, 59); + } else if (DataStatistics.PeriodType.LAST_30_DAYS.equals(periodType)) { + startTime = date.minusDays(29).atStartOfDay(); + endTime = date.atTime(23, 59, 59); + } else if (DataStatistics.PeriodType.LAST_90_DAYS.equals(periodType)) { + startTime = date.minusDays(89).atStartOfDay(); + endTime = date.atTime(23, 59, 59); + } else if (DataStatistics.PeriodType.YEAR.equals(periodType)) { + startTime = date.withDayOfYear(1).atStartOfDay(); + endTime = date.atTime(23, 59, 59); } else { startTime = date.atStartOfDay(); endTime = date.plusDays(1).atStartOfDay(); diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V31__Add_Statistics_Menu.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V31__Add_Statistics_Menu.sql new file mode 100644 index 0000000..60a94e1 --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V31__Add_Statistics_Menu.sql @@ -0,0 +1,19 @@ +-- ============================================ +-- V31: 新增数据统计菜单 +-- 描述: 为数据统计看板功能创建菜单项 +-- ============================================ + +-- 数据统计一级菜单(目录) +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(504, '数据统计', 0, 7, 'M', NULL, NULL, 1, NOW(), NOW()); + +-- 数据统计子菜单(页面) +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(505, '数据统计看板', 504, 1, 'C', 'business:statistics:view', 'statistics/dashboard/index', 1, NOW(), NOW()); + +-- 数据统计按钮权限 +INSERT INTO sys_menu (id, menu_name, parent_id, order_num, menu_type, perms, component, status, created_at, updated_at) VALUES +(5051, '数据统计导出', 505, 1, 'F', 'business:statistics:export', NULL, 1, NOW(), NOW()); + +-- 重置菜单序列 +SELECT setval('sys_menu_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sys_menu)); diff --git a/gym-manage-api/manage-db/src/main/resources/db/migration/V32__Add_Statistics_Test_Data_Today.sql b/gym-manage-api/manage-db/src/main/resources/db/migration/V32__Add_Statistics_Test_Data_Today.sql new file mode 100644 index 0000000..c1a3b8f --- /dev/null +++ b/gym-manage-api/manage-db/src/main/resources/db/migration/V32__Add_Statistics_Test_Data_Today.sql @@ -0,0 +1,66 @@ +-- ============================================================ +-- V32: 新增测试数据(用于数据统计看板展示) +-- 描述: 基于当天日期,创建跨多个时间区间的会员、签到、预约数据 +-- 覆盖时间区间: 今日、本周、本月、近30天、近90天、今年 +-- ============================================================ + +-- ============================================================ +-- 1. 新增会员(100人,ID 2001-2100) +-- 按时间分布:今日10人 / 本周20人 / 本月30人 / 近30天60人 / 近90天85人 / 今年100人 +-- 创建日期从 180 天前均匀分布到今天 +-- ============================================================ +INSERT INTO member_user (id, member_no, nickname, phone, gender, subscribed, is_deleted, created_at, updated_at) +SELECT + 2000 + g, + 'M' || TO_CHAR(CURRENT_DATE - (g % 180), 'YYYYMMDD') || LPAD(g::text, 3, '0'), + '测试会员' || g, + '138' || LPAD(g::text, 8, '0'), + (g % 3)::int, + false, + false, + (CURRENT_DATE - (g % 180) + TIME '10:00:00')::timestamp, + (CURRENT_DATE - (g % 180) + TIME '10:00:00')::timestamp +FROM generate_series(1, 100) AS g +ON CONFLICT (id) DO NOTHING; + +-- ============================================================ +-- 2. 签到记录(80条,使用已有会员 ID 1001-1010) +-- 按时间分布:最近90天,每天约1条 +-- 签到类型:QR_CODE/MANUAL/FACE 均匀分布 +-- 签到状态:SUCCESS(90%) / FAILED(10%) +-- ============================================================ +INSERT INTO sign_in_record (member_id, sign_in_time, sign_in_type, sign_in_status, source, is_delete, created_at, updated_at) +SELECT + 1001 + (g % 10), + (CURRENT_DATE - (g % 90) + TIME '08:00:00' + (INTERVAL '1 hour' * (g % 12)))::timestamp, + CASE (g % 3) WHEN 0 THEN 'QR_CODE' WHEN 1 THEN 'MANUAL' ELSE 'FACE' END, + CASE WHEN (g % 10) = 0 THEN 'FAILED' ELSE 'SUCCESS' END, + CASE (g % 2) WHEN 0 THEN 'MINI_PROGRAM' ELSE 'PC_BACKEND' END, + false, + (CURRENT_DATE - (g % 90) + TIME '08:00:00')::timestamp, + (CURRENT_DATE - (g % 90) + TIME '08:00:00')::timestamp +FROM generate_series(1, 80) AS g; + +-- ============================================================ +-- 3. 团课预约(40条,使用已有会员 ID 1001-1010) +-- 按时间分布:最近90天 +-- 状态分布:出勤(2) 60% / 取消(1) 20% / 缺席(3) 20% +-- ============================================================ +INSERT INTO group_course_booking (id, course_id, member_id, member_card_id, booking_time, status, created_at, updated_at) +SELECT + 5100 + g, + CASE (g % 3) WHEN 0 THEN 3001 WHEN 1 THEN 3002 ELSE 3003 END, + 1001 + (g % 10), + (SELECT id FROM member_card_record ORDER BY id LIMIT 1), + (CURRENT_DATE - (g % 90) + TIME '10:00:00')::timestamp, + CASE WHEN (g % 5) = 0 THEN '1' WHEN (g % 5) = 1 THEN '3' ELSE '2' END, + (CURRENT_DATE - (g % 90) + TIME '10:00:00')::timestamp, + (CURRENT_DATE - (g % 90) + TIME '10:00:00')::timestamp +FROM generate_series(1, 40) AS g; + +-- ============================================================ +-- 4. 重置序列(防止后续自动插入 ID 冲突) +-- ============================================================ +SELECT setval('member_user_id_seq', (SELECT COALESCE(MAX(id), 1) FROM member_user)); +SELECT setval('group_course_booking_id_seq', (SELECT COALESCE(MAX(id), 1) FROM group_course_booking)); +SELECT setval('sign_in_record_id_seq', (SELECT COALESCE(MAX(id), 1) FROM sign_in_record)); diff --git a/gym-manage-api/manage-sys/pom.xml b/gym-manage-api/manage-sys/pom.xml index 72f044d..7f48bb7 100644 --- a/gym-manage-api/manage-sys/pom.xml +++ b/gym-manage-api/manage-sys/pom.xml @@ -106,6 +106,12 @@ org.apache.poi poi-ooxml + + + + org.apache.commons + commons-compress + diff --git a/gym-manage-api/pom.xml b/gym-manage-api/pom.xml index 796ce9b..385258e 100644 --- a/gym-manage-api/pom.xml +++ b/gym-manage-api/pom.xml @@ -213,6 +213,11 @@ poi-ooxml ${poi.version} + + org.apache.commons + commons-compress + 1.21 + diff --git a/gym-manage-web/src/api/statistics.api.ts b/gym-manage-web/src/api/statistics.api.ts new file mode 100644 index 0000000..53f36e4 --- /dev/null +++ b/gym-manage-web/src/api/statistics.api.ts @@ -0,0 +1,100 @@ +import request from '@/utils/request' + +// ========== 接口类型定义 ========== + +export interface MemberStatistics { + statDate: string + newMembers: number + activeMembers: number + totalMembers: number + signInMembers: number + bookingMembers: number + cancelBookingMembers: number +} + +export interface BookingStatistics { + statDate: string + newBookings: number + cancelBookings: number + attendBookings: number + absentBookings: number + attendanceRate: number + cancelRate: number + bookingMembers: number + cancelMembers: number +} + +export interface SignInStatistics { + statDate: string + totalSignIns: number + successSignIns: number + failedSignIns: number + successRate: number + signInMembers: number + qrCodeSignIns: number + manualSignIns: number + faceSignIns: number +} + +export interface StatisticsSummary { + statDate: string + memberStatistics: MemberStatistics + bookingStatistics: BookingStatistics + signInStatistics: SignInStatistics + generatedAt: string +} + +export interface HistoryRecord { + statType: string + periodType: string + statDate: string + count: number + relatedId: number + extraData: string +} + +export interface StatisticsQuery { + statType?: string + periodType?: string + startTime?: string + endTime?: string + page?: number + size?: number +} + +// ========== API ========== + +export const statisticsApi = { + /** 获取综合统计摘要 */ + getSummary(params?: StatisticsQuery) { + return request.get('/datacount/summary', { params }) + }, + + /** 获取会员统计 */ + getMember(params?: StatisticsQuery) { + return request.get('/datacount/member', { params }) + }, + + /** 获取预约统计 */ + getBooking(params?: StatisticsQuery) { + return request.get('/datacount/booking', { params }) + }, + + /** 获取签到统计 */ + getSignIn(params?: StatisticsQuery) { + return request.get('/datacount/signin', { params }) + }, + + /** 查询历史统计数据 */ + getHistory(params?: StatisticsQuery) { + return request.get('/datacount/history', { params }) + }, + + /** 导出统计报表 (Excel) */ + exportStatistics(params?: StatisticsQuery) { + return request.get('/datacount/export', { + params, + responseType: 'blob', + }) + }, +} diff --git a/gym-manage-web/src/router/index.ts b/gym-manage-web/src/router/index.ts index b638465..303e2ea 100644 --- a/gym-manage-web/src/router/index.ts +++ b/gym-manage-web/src/router/index.ts @@ -129,6 +129,12 @@ const routes: RouteRecordRaw[] = [ name: 'MemberCardManagement', component: () => import('@/views/member/MemberCardManagement.vue'), meta: { title: '会员卡管理' } + }, + { + path: 'statistics', + name: 'StatisticsDashboard', + component: () => import('@/views/statistics/StatisticsDashboard.vue'), + meta: { title: '数据统计看板' } } ] } diff --git a/gym-manage-web/src/stores/permission.ts b/gym-manage-web/src/stores/permission.ts index 9fd43d0..513427e 100644 --- a/gym-manage-web/src/stores/permission.ts +++ b/gym-manage-web/src/stores/permission.ts @@ -44,6 +44,7 @@ function transformMenuData(backendMenus: BackendMenuItem[]): MenuItem[] { 'gymCourse/recommend/index': '/courses/recommend', 'member/member/index': '/members', 'member/card/index': '/member-cards', + 'statistics/dashboard/index': '/statistics', } const filteredMenus = backendMenus.filter(menu => menu.menuType !== 'F') @@ -105,6 +106,7 @@ function getMenuIcon(menuName: string): string { '团课推荐': 'Star', '会员管理': 'Avatar', '会员卡管理': 'CreditCard', + '数据统计': 'DataAnalysis', } return iconMap[menuName] || 'Document' } diff --git a/gym-manage-web/src/views/statistics/StatisticsDashboard.vue b/gym-manage-web/src/views/statistics/StatisticsDashboard.vue new file mode 100644 index 0000000..42efa28 --- /dev/null +++ b/gym-manage-web/src/views/statistics/StatisticsDashboard.vue @@ -0,0 +1,402 @@ + + + + +