From 3c055be676867bb8739572957baba4e97bd50dc0 Mon Sep 17 00:00:00 2001 From: nguyennt1 Date: Wed, 3 Dec 2025 23:27:42 +0700 Subject: [PATCH] =?UTF-8?q?feat=20:=20th=C3=AAm=20bao=20cao?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../repositories/RevenueDataRepository.java | 10 + .../controller/ReportGoogleController.java | 71 +++++ .../report/request/ChannelReportRequest.java | 23 ++ .../hrm/report/response/ActivityDataDto.java | 21 ++ .../report/response/ActivitySummaryDto.java | 21 ++ .../response/ChannelActivityReportDto.java | 40 +++ .../response/ChannelRevenueReportDto.java | 35 +++ .../report/response/RevenueSummaryDto.java | 18 ++ .../serivce/ChannelActivityReportService.java | 245 ++++++++++++++++++ .../serivce/ChannelRevenueReportService.java | 180 +++++++++++++ 10 files changed, 664 insertions(+) create mode 100644 vega-hrm-report/src/main/java/com/vega/hrm/report/request/ChannelReportRequest.java create mode 100644 vega-hrm-report/src/main/java/com/vega/hrm/report/response/ActivityDataDto.java create mode 100644 vega-hrm-report/src/main/java/com/vega/hrm/report/response/ActivitySummaryDto.java create mode 100644 vega-hrm-report/src/main/java/com/vega/hrm/report/response/ChannelActivityReportDto.java create mode 100644 vega-hrm-report/src/main/java/com/vega/hrm/report/response/ChannelRevenueReportDto.java create mode 100644 vega-hrm-report/src/main/java/com/vega/hrm/report/response/RevenueSummaryDto.java create mode 100644 vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/ChannelActivityReportService.java create mode 100644 vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/ChannelRevenueReportService.java diff --git a/vega-hrm-core/src/main/java/com/vega/hrm/core/repositories/RevenueDataRepository.java b/vega-hrm-core/src/main/java/com/vega/hrm/core/repositories/RevenueDataRepository.java index 8dde13c..679904e 100644 --- a/vega-hrm-core/src/main/java/com/vega/hrm/core/repositories/RevenueDataRepository.java +++ b/vega-hrm-core/src/main/java/com/vega/hrm/core/repositories/RevenueDataRepository.java @@ -2,6 +2,7 @@ package com.vega.hrm.core.repositories; import com.vega.hrm.core.entities.RevenueData; import java.time.LocalDate; +import java.util.List; import java.util.UUID; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -24,4 +25,13 @@ public interface RevenueDataRepository extends JpaRepository ); RevenueData findByEmailAndRevenueDate(String email, LocalDate revenueDate); + + @Query("SELECT r FROM RevenueData r WHERE r.email = :email " + + "AND r.revenueDate >= :startDate AND r.revenueDate <= :endDate " + + "ORDER BY r.revenueDate ASC") + List findAllByEmailAndDateRange( + @Param("email") String email, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate + ); } diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/controller/ReportGoogleController.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/controller/ReportGoogleController.java index 5be2773..7f91eb2 100644 --- a/vega-hrm-report/src/main/java/com/vega/hrm/report/controller/ReportGoogleController.java +++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/controller/ReportGoogleController.java @@ -6,10 +6,15 @@ import com.vega.hrm.core.component.TokenStore; import com.vega.hrm.core.dto.GoogleOAuthConfig; import com.vega.hrm.core.models.responses.BaseResponse; import com.vega.hrm.core.models.responses.PagedListResponse; +import com.vega.hrm.report.request.ChannelReportRequest; import com.vega.hrm.report.request.GetDragRevenueRequest; import com.vega.hrm.report.request.GetRevenueRequest; +import com.vega.hrm.report.response.ChannelActivityReportDto; +import com.vega.hrm.report.response.ChannelRevenueReportDto; import com.vega.hrm.report.response.ReportTypeDto; import com.vega.hrm.report.response.RevenueDataDto; +import com.vega.hrm.report.serivce.ChannelActivityReportService; +import com.vega.hrm.report.serivce.ChannelRevenueReportService; import com.vega.hrm.report.serivce.CreateReportingJobService; import com.vega.hrm.report.serivce.RevenueDataService; import com.vega.hrm.report.serivce.YouTubeReportService; @@ -42,6 +47,8 @@ public class ReportGoogleController { private final CreateReportingJobService createReportingJob; private final YouTubeReportService youTubeReportService; private final RevenueDataService revenueDataService; + private final ChannelRevenueReportService channelRevenueReportService; + private final ChannelActivityReportService channelActivityReportService; private final GoogleOAuthConfig googleOAuthConfig; private final TokenStore tokenStore; @@ -140,4 +147,68 @@ public class ReportGoogleController { .build() ); } + + @PostMapping("/channel/revenue") + @Operation(summary = "Báo cáo doanh thu theo kênh", description = "Tổng hợp báo cáo doanh thu theo kênh từ database với các thống kê và tổng hợp theo tuần/tháng.") + public ResponseEntity> getChannelRevenueReport( + @Valid @RequestBody ChannelReportRequest request + ) { + if (tokenStore.getTokenResponse(request.getEmail()) == null) { + return ResponseEntity.ok( + BaseResponse.builder() + .code("01") + .message("Không tìm thấy token Google cho email đã cung cấp") + .build() + ); + } + + ChannelRevenueReportDto report = channelRevenueReportService.generateRevenueReport( + request.getEmail(), + request.getStartDate(), + request.getEndDate(), + request.getIncludeWeeklySummary() != null && request.getIncludeWeeklySummary(), + request.getIncludeMonthlySummary() != null && request.getIncludeMonthlySummary() + ); + + return ResponseEntity.ok( + BaseResponse.builder() + .code("00") + .message("Lấy báo cáo doanh thu theo kênh thành công") + .data(report) + .build() + ); + } + + @PostMapping("/channel/activity") + @Operation(summary = "Báo cáo hoạt động theo kênh", description = "Lấy báo cáo hoạt động theo kênh từ YouTube Analytics API bao gồm views, watch time, subscribers, likes, comments.") + public ResponseEntity> getChannelActivityReport( + @Valid @RequestBody ChannelReportRequest request + ) throws GeneralSecurityException, IOException { + if (tokenStore.getTokenResponse(request.getEmail()) == null) { + return ResponseEntity.ok( + BaseResponse.builder() + .code("01") + .message("Không tìm thấy token Google cho email đã cung cấp") + .build() + ); + } + + ChannelActivityReportDto report = channelActivityReportService.getChannelActivityReport( + request.getEmail(), + request.getStartDate(), + request.getEndDate(), + GoogleNetHttpTransport.newTrustedTransport(), + JacksonFactory.getDefaultInstance(), + request.getIncludeWeeklySummary() != null && request.getIncludeWeeklySummary(), + request.getIncludeMonthlySummary() != null && request.getIncludeMonthlySummary() + ); + + return ResponseEntity.ok( + BaseResponse.builder() + .code("00") + .message("Lấy báo cáo hoạt động theo kênh thành công") + .data(report) + .build() + ); + } } diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/request/ChannelReportRequest.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/request/ChannelReportRequest.java new file mode 100644 index 0000000..38212fa --- /dev/null +++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/request/ChannelReportRequest.java @@ -0,0 +1,23 @@ +package com.vega.hrm.report.request; + +import jakarta.validation.constraints.NotBlank; +import lombok.Getter; +import lombok.Setter; + +@Getter +@Setter +public class ChannelReportRequest { + @NotBlank(message = "Email không được để trống") + private String email; + + @NotBlank(message = "Ngày bắt đầu không được để trống") + private String startDate; // format: YYYY-MM-DD + + @NotBlank(message = "Ngày kết thúc không được để trống") + private String endDate; // format: YYYY-MM-DD + + // Optional: Group by period + private Boolean includeWeeklySummary = false; + private Boolean includeMonthlySummary = false; +} + diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/response/ActivityDataDto.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/response/ActivityDataDto.java new file mode 100644 index 0000000..6ad589e --- /dev/null +++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/response/ActivityDataDto.java @@ -0,0 +1,21 @@ +package com.vega.hrm.report.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ActivityDataDto { + private String date; + private Long views; + private Long watchTime; // seconds + private Long subscribers; + private Long likes; + private Long comments; + private Long shares; +} + diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/response/ActivitySummaryDto.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/response/ActivitySummaryDto.java new file mode 100644 index 0000000..8d6d914 --- /dev/null +++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/response/ActivitySummaryDto.java @@ -0,0 +1,21 @@ +package com.vega.hrm.report.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ActivitySummaryDto { + private String period; // "2025-W01" cho tuần, "2025-01" cho tháng + private Long totalViews; + private Long totalWatchTime; + private Long totalSubscribers; + private Long totalLikes; + private Long totalComments; + private Integer dayCount; +} + diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/response/ChannelActivityReportDto.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/response/ChannelActivityReportDto.java new file mode 100644 index 0000000..e79b146 --- /dev/null +++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/response/ChannelActivityReportDto.java @@ -0,0 +1,40 @@ +package com.vega.hrm.report.response; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelActivityReportDto { + private String email; + private String startDate; + private String endDate; + + // Tổng hợp metrics + private Long totalViews; + private Long totalWatchTime; // seconds + private Long totalSubscribers; + private Long totalLikes; + private Long totalComments; + + // Trung bình + private Double averageViews; + private Double averageWatchTime; + private Double averageSubscribers; + + // Thống kê + private Integer totalDays; + + // Dữ liệu chi tiết theo ngày + private List dailyActivity; + + // Tổng hợp theo tuần/tháng + private List weeklySummary; + private List monthlySummary; +} + diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/response/ChannelRevenueReportDto.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/response/ChannelRevenueReportDto.java new file mode 100644 index 0000000..44b1dd6 --- /dev/null +++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/response/ChannelRevenueReportDto.java @@ -0,0 +1,35 @@ +package com.vega.hrm.report.response; + +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class ChannelRevenueReportDto { + private String email; + private String startDate; + private String endDate; + + // Tổng hợp doanh thu + private Double totalRevenue; + private Double averageDailyRevenue; + private Double maxDailyRevenue; + private Double minDailyRevenue; + + // Thống kê + private Integer totalDays; + private Integer daysWithRevenue; + + // Dữ liệu chi tiết theo ngày + private List dailyRevenue; + + // Dữ liệu tổng hợp theo tuần/tháng (nếu cần) + private List weeklySummary; + private List monthlySummary; +} + diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/response/RevenueSummaryDto.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/response/RevenueSummaryDto.java new file mode 100644 index 0000000..e0706ca --- /dev/null +++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/response/RevenueSummaryDto.java @@ -0,0 +1,18 @@ +package com.vega.hrm.report.response; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class RevenueSummaryDto { + private String period; // "2025-W01" cho tuần, "2025-01" cho tháng + private Double totalRevenue; + private Double averageRevenue; + private Integer dayCount; +} + diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/ChannelActivityReportService.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/ChannelActivityReportService.java new file mode 100644 index 0000000..1fd25e1 --- /dev/null +++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/ChannelActivityReportService.java @@ -0,0 +1,245 @@ +package com.vega.hrm.report.serivce; + +import com.google.api.client.auth.oauth2.Credential; +import com.google.api.client.googleapis.json.GoogleJsonResponseException; +import com.google.api.client.http.HttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.services.youtubeAnalytics.v2.YouTubeAnalytics; +import com.google.api.services.youtubeAnalytics.v2.model.QueryResponse; +import com.vega.hrm.core.component.TokenStore; +import com.vega.hrm.core.dto.GoogleOAuthConfig; +import com.vega.hrm.core.helpers.LogHelper; +import com.vega.hrm.report.response.ActivityDataDto; +import com.vega.hrm.report.response.ActivitySummaryDto; +import com.vega.hrm.report.response.ChannelActivityReportDto; +import java.io.IOException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ChannelActivityReportService { + + private static final String APPLICATION_NAME = "vega-report"; + private final TokenStore tokenStore; + private final GoogleOAuthConfig googleOAuthConfig; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + /** + * Lấy báo cáo hoạt động theo kênh từ YouTube Analytics API + */ + public ChannelActivityReportDto getChannelActivityReport(String email, String startDate, String endDate, + HttpTransport httpTransport, JsonFactory jsonFactory, + boolean includeWeekly, boolean includeMonthly) + throws IOException { + + Credential credential = tokenStore.buildCredential( + httpTransport, + jsonFactory, + email, + googleOAuthConfig.clientId, + googleOAuthConfig.clientSecret + ); + + if (credential == null) { + throw new IllegalStateException("Không tìm thấy token Google cho email: " + email); + } + + YouTubeAnalytics youtubeAnalytics = new YouTubeAnalytics.Builder(httpTransport, jsonFactory, credential) + .setApplicationName(APPLICATION_NAME) + .build(); + + try { + // Lấy các metrics: views, watchTime, subscribers, likes, comments + QueryResponse queryResponse = youtubeAnalytics.reports().query() + .setIds("channel==MINE") + .setStartDate(startDate) + .setEndDate(endDate) + .setMetrics("views,estimatedMinutesWatched,subscribersGained,likes,comments") + .setDimensions("day") + .execute(); + + List dailyActivity = parseActivityData(queryResponse); + + // Tính tổng hợp + long totalViews = dailyActivity.stream().mapToLong(a -> a.getViews() != null ? a.getViews() : 0L).sum(); + long totalWatchTime = dailyActivity.stream().mapToLong(a -> a.getWatchTime() != null ? a.getWatchTime() : 0L).sum(); + long totalSubscribers = dailyActivity.stream().mapToLong(a -> a.getSubscribers() != null ? a.getSubscribers() : 0L).sum(); + long totalLikes = dailyActivity.stream().mapToLong(a -> a.getLikes() != null ? a.getLikes() : 0L).sum(); + long totalComments = dailyActivity.stream().mapToLong(a -> a.getComments() != null ? a.getComments() : 0L).sum(); + + int totalDays = dailyActivity.size(); + double avgViews = totalDays > 0 ? (double) totalViews / totalDays : 0.0; + double avgWatchTime = totalDays > 0 ? (double) totalWatchTime / totalDays : 0.0; + double avgSubscribers = totalDays > 0 ? (double) totalSubscribers / totalDays : 0.0; + + ChannelActivityReportDto.ChannelActivityReportDtoBuilder reportBuilder = ChannelActivityReportDto.builder() + .email(email) + .startDate(startDate) + .endDate(endDate) + .totalViews(totalViews) + .totalWatchTime(totalWatchTime) + .totalSubscribers(totalSubscribers) + .totalLikes(totalLikes) + .totalComments(totalComments) + .averageViews(avgViews) + .averageWatchTime(avgWatchTime) + .averageSubscribers(avgSubscribers) + .totalDays(totalDays) + .dailyActivity(dailyActivity); + + // Tổng hợp theo tuần + if (includeWeekly) { + reportBuilder.weeklySummary(generateWeeklySummary(dailyActivity)); + } else { + reportBuilder.weeklySummary(new ArrayList<>()); + } + + // Tổng hợp theo tháng + if (includeMonthly) { + reportBuilder.monthlySummary(generateMonthlySummary(dailyActivity)); + } else { + reportBuilder.monthlySummary(new ArrayList<>()); + } + + LogHelper.info("Đã lấy được " + dailyActivity.size() + " dòng dữ liệu hoạt động."); + return reportBuilder.build(); + + } catch (GoogleJsonResponseException e) { + LogHelper.error("Lỗi khi lấy dữ liệu hoạt động: " + e.getDetails().getMessage()); + throw new IOException("Không thể lấy dữ liệu hoạt động: " + e.getDetails().getMessage()); + } + } + + /** + * Parse dữ liệu hoạt động từ QueryResponse + */ + private List parseActivityData(QueryResponse queryResponse) { + List result = new ArrayList<>(); + + if (queryResponse.getRows() != null) { + for (List row : queryResponse.getRows()) { + if (row.size() >= 6) { + String date = row.get(0).toString(); + Long views = parseLong(row.get(1)); + Long watchTime = parseLong(row.get(2)); // estimatedMinutesWatched (phút) -> chuyển sang giây + Long subscribers = parseLong(row.get(3)); + Long likes = parseLong(row.get(4)); + Long comments = parseLong(row.get(5)); + + // Chuyển watchTime từ phút sang giây + if (watchTime != null) { + watchTime = watchTime * 60; + } + + result.add(ActivityDataDto.builder() + .date(date) + .views(views) + .watchTime(watchTime) + .subscribers(subscribers) + .likes(likes) + .comments(comments) + .build()); + } + } + } + + return result; + } + + /** + * Parse giá trị Long từ Object + */ + private Long parseLong(Object obj) { + if (obj == null) { + return null; + } + if (obj instanceof Number) { + return ((Number) obj).longValue(); + } + try { + return Long.parseLong(obj.toString()); + } catch (NumberFormatException e) { + return null; + } + } + + /** + * Tổng hợp dữ liệu theo tuần + */ + private List generateWeeklySummary(List data) { + Map> weeklyGrouped = data.stream() + .collect(Collectors.groupingBy(ad -> { + LocalDate date = LocalDate.parse(ad.getDate(), DATE_FORMATTER); + int year = date.getYear(); + int week = date.get(java.time.temporal.WeekFields.ISO.weekOfWeekBasedYear()); + return String.format("%d-W%02d", year, week); + })); + + return weeklyGrouped.entrySet().stream() + .map(entry -> { + String period = entry.getKey(); + List weekData = entry.getValue(); + + long totalViews = weekData.stream().mapToLong(a -> a.getViews() != null ? a.getViews() : 0L).sum(); + long totalWatchTime = weekData.stream().mapToLong(a -> a.getWatchTime() != null ? a.getWatchTime() : 0L).sum(); + long totalSubscribers = weekData.stream().mapToLong(a -> a.getSubscribers() != null ? a.getSubscribers() : 0L).sum(); + long totalLikes = weekData.stream().mapToLong(a -> a.getLikes() != null ? a.getLikes() : 0L).sum(); + long totalComments = weekData.stream().mapToLong(a -> a.getComments() != null ? a.getComments() : 0L).sum(); + + return ActivitySummaryDto.builder() + .period(period) + .totalViews(totalViews) + .totalWatchTime(totalWatchTime) + .totalSubscribers(totalSubscribers) + .totalLikes(totalLikes) + .totalComments(totalComments) + .dayCount(weekData.size()) + .build(); + }) + .sorted((a, b) -> a.getPeriod().compareTo(b.getPeriod())) + .collect(Collectors.toList()); + } + + /** + * Tổng hợp dữ liệu theo tháng + */ + private List generateMonthlySummary(List data) { + Map> monthlyGrouped = data.stream() + .collect(Collectors.groupingBy(ad -> { + LocalDate date = LocalDate.parse(ad.getDate(), DATE_FORMATTER); + return String.format("%d-%02d", date.getYear(), date.getMonthValue()); + })); + + return monthlyGrouped.entrySet().stream() + .map(entry -> { + String period = entry.getKey(); + List monthData = entry.getValue(); + + long totalViews = monthData.stream().mapToLong(a -> a.getViews() != null ? a.getViews() : 0L).sum(); + long totalWatchTime = monthData.stream().mapToLong(a -> a.getWatchTime() != null ? a.getWatchTime() : 0L).sum(); + long totalSubscribers = monthData.stream().mapToLong(a -> a.getSubscribers() != null ? a.getSubscribers() : 0L).sum(); + long totalLikes = monthData.stream().mapToLong(a -> a.getLikes() != null ? a.getLikes() : 0L).sum(); + long totalComments = monthData.stream().mapToLong(a -> a.getComments() != null ? a.getComments() : 0L).sum(); + + return ActivitySummaryDto.builder() + .period(period) + .totalViews(totalViews) + .totalWatchTime(totalWatchTime) + .totalSubscribers(totalSubscribers) + .totalLikes(totalLikes) + .totalComments(totalComments) + .dayCount(monthData.size()) + .build(); + }) + .sorted((a, b) -> a.getPeriod().compareTo(b.getPeriod())) + .collect(Collectors.toList()); + } +} + diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/ChannelRevenueReportService.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/ChannelRevenueReportService.java new file mode 100644 index 0000000..a0fc750 --- /dev/null +++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/ChannelRevenueReportService.java @@ -0,0 +1,180 @@ +package com.vega.hrm.report.serivce; + +import com.vega.hrm.core.entities.RevenueData; +import com.vega.hrm.core.repositories.RevenueDataRepository; +import com.vega.hrm.report.response.ChannelRevenueReportDto; +import com.vega.hrm.report.response.RevenueDataDto; +import com.vega.hrm.report.response.RevenueSummaryDto; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +@Service +@RequiredArgsConstructor +public class ChannelRevenueReportService { + + private final RevenueDataRepository revenueDataRepository; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + /** + * Tạo báo cáo doanh thu theo kênh + */ + public ChannelRevenueReportDto generateRevenueReport(String email, String startDate, String endDate, + boolean includeWeekly, boolean includeMonthly) { + LocalDate start = LocalDate.parse(startDate, DATE_FORMATTER); + LocalDate end = LocalDate.parse(endDate, DATE_FORMATTER); + + // Lấy tất cả dữ liệu trong khoảng thời gian + List allData = revenueDataRepository.findAllByEmailAndDateRange(email, start, end); + + if (allData == null || allData.isEmpty()) { + return ChannelRevenueReportDto.builder() + .email(email) + .startDate(startDate) + .endDate(endDate) + .totalRevenue(0.0) + .averageDailyRevenue(0.0) + .maxDailyRevenue(0.0) + .minDailyRevenue(0.0) + .totalDays(0) + .daysWithRevenue(0) + .dailyRevenue(new ArrayList<>()) + .weeklySummary(new ArrayList<>()) + .monthlySummary(new ArrayList<>()) + .build(); + } + + // Chuyển đổi sang DTO + List dailyRevenue = allData.stream() + .map(this::convertToDto) + .collect(Collectors.toList()); + + // Tính tổng hợp + List revenues = allData.stream() + .filter(r -> r.getEstimatedRevenue() != null) + .map(RevenueData::getEstimatedRevenue) + .collect(Collectors.toList()); + + double totalRevenue = revenues.stream().mapToDouble(Double::doubleValue).sum(); + double avgRevenue = revenues.isEmpty() ? 0.0 : totalRevenue / revenues.size(); + double maxRevenue = revenues.stream().mapToDouble(Double::doubleValue).max().orElse(0.0); + double minRevenue = revenues.stream().mapToDouble(Double::doubleValue).min().orElse(0.0); + + int totalDays = (int) start.datesUntil(end.plusDays(1)).count(); + int daysWithRevenue = revenues.size(); + + ChannelRevenueReportDto.ChannelRevenueReportDtoBuilder reportBuilder = ChannelRevenueReportDto.builder() + .email(email) + .startDate(startDate) + .endDate(endDate) + .totalRevenue(totalRevenue) + .averageDailyRevenue(avgRevenue) + .maxDailyRevenue(maxRevenue) + .minDailyRevenue(minRevenue) + .totalDays(totalDays) + .daysWithRevenue(daysWithRevenue) + .dailyRevenue(dailyRevenue); + + // Tổng hợp theo tuần + if (includeWeekly) { + reportBuilder.weeklySummary(generateWeeklySummary(allData)); + } else { + reportBuilder.weeklySummary(new ArrayList<>()); + } + + // Tổng hợp theo tháng + if (includeMonthly) { + reportBuilder.monthlySummary(generateMonthlySummary(allData)); + } else { + reportBuilder.monthlySummary(new ArrayList<>()); + } + + return reportBuilder.build(); + } + + /** + * Tổng hợp dữ liệu theo tuần + */ + private List generateWeeklySummary(List data) { + Map> weeklyGrouped = data.stream() + .collect(Collectors.groupingBy(rd -> { + LocalDate date = rd.getRevenueDate(); + int year = date.getYear(); + int week = date.get(java.time.temporal.WeekFields.ISO.weekOfWeekBasedYear()); + return String.format("%d-W%02d", year, week); + })); + + return weeklyGrouped.entrySet().stream() + .map(entry -> { + String period = entry.getKey(); + List weekData = entry.getValue(); + + List revenues = weekData.stream() + .filter(r -> r.getEstimatedRevenue() != null) + .map(RevenueData::getEstimatedRevenue) + .collect(Collectors.toList()); + + double total = revenues.stream().mapToDouble(Double::doubleValue).sum(); + double avg = revenues.isEmpty() ? 0.0 : total / revenues.size(); + + return RevenueSummaryDto.builder() + .period(period) + .totalRevenue(total) + .averageRevenue(avg) + .dayCount(revenues.size()) + .build(); + }) + .sorted((a, b) -> a.getPeriod().compareTo(b.getPeriod())) + .collect(Collectors.toList()); + } + + /** + * Tổng hợp dữ liệu theo tháng + */ + private List generateMonthlySummary(List data) { + Map> monthlyGrouped = data.stream() + .collect(Collectors.groupingBy(rd -> { + LocalDate date = rd.getRevenueDate(); + return String.format("%d-%02d", date.getYear(), date.getMonthValue()); + })); + + return monthlyGrouped.entrySet().stream() + .map(entry -> { + String period = entry.getKey(); + List monthData = entry.getValue(); + + List revenues = monthData.stream() + .filter(r -> r.getEstimatedRevenue() != null) + .map(RevenueData::getEstimatedRevenue) + .collect(Collectors.toList()); + + double total = revenues.stream().mapToDouble(Double::doubleValue).sum(); + double avg = revenues.isEmpty() ? 0.0 : total / revenues.size(); + + return RevenueSummaryDto.builder() + .period(period) + .totalRevenue(total) + .averageRevenue(avg) + .dayCount(revenues.size()) + .build(); + }) + .sorted((a, b) -> a.getPeriod().compareTo(b.getPeriod())) + .collect(Collectors.toList()); + } + + /** + * Convert entity sang DTO + */ + private RevenueDataDto convertToDto(RevenueData entity) { + return RevenueDataDto.builder() + .date(entity.getRevenueDate().format(DATE_FORMATTER)) + .estimatedRevenue(entity.getEstimatedRevenue()) + .build(); + } +} +