From f9626aa7014a596d99eceff2a3435a44042477e9 Mon Sep 17 00:00:00 2001 From: nguyennt1 Date: Wed, 3 Dec 2025 23:10:40 +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 --- .../com/vega/hrm/VegaHrmAuthApplication.java | 2 + .../hrm/service/TokenRefreshScheduledJob.java | 102 +++++++++++ .../vega/hrm/service/TokenRefreshService.java | 113 +++++++++++++ .../vega/hrm/core/entities/RevenueData.java | 46 +++++ .../repositories/RevenueDataRepository.java | 27 +++ .../UserGoogleTokenRepository.java | 19 +++ vega-hrm-report/build.gradle | 1 + .../hrm/report/VegaHrmReportApplication.java | 2 + .../controller/ReportGoogleController.java | 34 +++- .../hrm/report/request/GetRevenueRequest.java | 8 + .../serivce/RevenueDataScheduledJob.java | 160 ++++++++++++++++++ .../report/serivce/RevenueDataService.java | 104 ++++++++++++ 12 files changed, 609 insertions(+), 9 deletions(-) create mode 100644 vega-hrm-auth/src/main/java/com/vega/hrm/service/TokenRefreshScheduledJob.java create mode 100644 vega-hrm-auth/src/main/java/com/vega/hrm/service/TokenRefreshService.java create mode 100644 vega-hrm-core/src/main/java/com/vega/hrm/core/entities/RevenueData.java create mode 100644 vega-hrm-core/src/main/java/com/vega/hrm/core/repositories/RevenueDataRepository.java create mode 100644 vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/RevenueDataScheduledJob.java create mode 100644 vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/RevenueDataService.java diff --git a/vega-hrm-auth/src/main/java/com/vega/hrm/VegaHrmAuthApplication.java b/vega-hrm-auth/src/main/java/com/vega/hrm/VegaHrmAuthApplication.java index 0dffce7..3eae0a8 100644 --- a/vega-hrm-auth/src/main/java/com/vega/hrm/VegaHrmAuthApplication.java +++ b/vega-hrm-auth/src/main/java/com/vega/hrm/VegaHrmAuthApplication.java @@ -4,10 +4,12 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication(scanBasePackages = "com.vega.hrm") @EnableTransactionManagement +@EnableScheduling public class VegaHrmAuthApplication { public static void main(String[] args) { SpringApplication.run(VegaHrmAuthApplication.class, args); diff --git a/vega-hrm-auth/src/main/java/com/vega/hrm/service/TokenRefreshScheduledJob.java b/vega-hrm-auth/src/main/java/com/vega/hrm/service/TokenRefreshScheduledJob.java new file mode 100644 index 0000000..e64d0fc --- /dev/null +++ b/vega-hrm-auth/src/main/java/com/vega/hrm/service/TokenRefreshScheduledJob.java @@ -0,0 +1,102 @@ +package com.vega.hrm.service; + +import com.vega.hrm.core.entities.UserGoogleToken; +import com.vega.hrm.core.helpers.LogHelper; +import com.vega.hrm.core.repositories.UserGoogleTokenRepository; +import java.time.Instant; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class TokenRefreshScheduledJob { + + private final UserGoogleTokenRepository userGoogleTokenRepository; + private final TokenRefreshService tokenRefreshService; + + + @Scheduled(cron = "0 */30 * * * ?") + public void refreshExpiredTokens() { + LogHelper.info("Bắt đầu job refresh access token hết hạn"); + + try { + Instant now = Instant.now(); + + // Tìm các token đã hết hạn nhưng refresh token vẫn còn hiệu lực + List expiredTokens = userGoogleTokenRepository.findExpiredTokensWithValidRefreshToken(now); + + if (expiredTokens == null || expiredTokens.isEmpty()) { + LogHelper.info("Không có token nào cần refresh"); + return; + } + + LogHelper.info("Tìm thấy " + expiredTokens.size() + " token cần refresh"); + + int successCount = 0; + int failCount = 0; + + for (UserGoogleToken token : expiredTokens) { + try { + boolean success = tokenRefreshService.refreshAccessToken(token); + if (success) { + successCount++; + } else { + failCount++; + } + } catch (Exception e) { + LogHelper.error("Lỗi không mong đợi khi refresh token cho email: " + token.getEmail() + " - " + e.getMessage()); + failCount++; + } + } + + LogHelper.info("Hoàn thành job refresh token. Thành công: " + successCount + ", Thất bại: " + failCount); + + } catch (Exception e) { + LogHelper.error("Lỗi nghiêm trọng trong job refresh token: " + e.getMessage()); + } + } + + @Scheduled(cron = "0 */10 * * * ?") + public void refreshTokensExpiringSoon() { + LogHelper.info("Bắt đầu job refresh token sắp hết hạn"); + + try { + Instant now = Instant.now(); + // Refresh các token sẽ hết hạn trong vòng 15 phút tới + Instant expiryTime = now.plusSeconds(15 * 60); + + List expiringSoonTokens = userGoogleTokenRepository.findTokensExpiringSoon(expiryTime, now); + + if (expiringSoonTokens == null || expiringSoonTokens.isEmpty()) { + LogHelper.info("Không có token nào sắp hết hạn"); + return; + } + + LogHelper.info("Tìm thấy " + expiringSoonTokens.size() + " token sắp hết hạn"); + + int successCount = 0; + int failCount = 0; + + for (UserGoogleToken token : expiringSoonTokens) { + try { + boolean success = tokenRefreshService.refreshAccessToken(token); + if (success) { + successCount++; + } else { + failCount++; + } + } catch (Exception e) { + LogHelper.error("Lỗi không mong đợi khi refresh token sắp hết hạn cho email: " + token.getEmail() + " - " + e.getMessage()); + failCount++; + } + } + + LogHelper.info("Hoàn thành job refresh token sắp hết hạn. Thành công: " + successCount + ", Thất bại: " + failCount); + + } catch (Exception e) { + LogHelper.error("Lỗi nghiêm trọng trong job refresh token sắp hết hạn: " + e.getMessage()); + } + } +} diff --git a/vega-hrm-auth/src/main/java/com/vega/hrm/service/TokenRefreshService.java b/vega-hrm-auth/src/main/java/com/vega/hrm/service/TokenRefreshService.java new file mode 100644 index 0000000..643c532 --- /dev/null +++ b/vega-hrm-auth/src/main/java/com/vega/hrm/service/TokenRefreshService.java @@ -0,0 +1,113 @@ +package com.vega.hrm.service; + +import com.google.api.client.auth.oauth2.TokenResponse; +import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.http.javanet.NetHttpTransport; +import com.google.api.client.json.JsonFactory; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.vega.hrm.core.component.TokenStore; +import com.vega.hrm.core.dto.GoogleOAuthConfig; +import com.vega.hrm.core.entities.UserGoogleToken; +import com.vega.hrm.core.helpers.LogHelper; +import com.vega.hrm.core.repositories.UserGoogleTokenRepository; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.time.Instant; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class TokenRefreshService { + + private static final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance(); + private final UserGoogleTokenRepository userGoogleTokenRepository; + private final GoogleOAuthConfig googleOAuthConfig; + private final TokenStore tokenStore; + + /** + * Refresh access token bằng refresh token + * @param userGoogleToken Token cần refresh + * @return true nếu refresh thành công, false nếu thất bại + */ + @Transactional + public boolean refreshAccessToken(UserGoogleToken userGoogleToken) { + if (userGoogleToken == null || userGoogleToken.getRefreshToken() == null) { + LogHelper.error("Không thể refresh token: token hoặc refresh token null"); + return false; + } + + // Kiểm tra refresh token có còn hiệu lực không + if (userGoogleToken.getRefreshTokenExpiresAt() != null + && userGoogleToken.getRefreshTokenExpiresAt().isBefore(Instant.now())) { + LogHelper.error("Refresh token đã hết hạn cho email: " + userGoogleToken.getEmail()); + return false; + } + + NetHttpTransport httpTransport = null; + try { + httpTransport = GoogleNetHttpTransport.newTrustedTransport(); + } catch (GeneralSecurityException | IOException e) { + LogHelper.error("Lỗi khi tạo HTTP transport: " + e.getMessage()); + return false; + } + + try { + // Tạo GoogleCredential với refresh token để tự động refresh + GoogleCredential credential = new GoogleCredential.Builder() + .setTransport(httpTransport) + .setJsonFactory(JSON_FACTORY) + .setClientSecrets(googleOAuthConfig.clientId, googleOAuthConfig.clientSecret) + .build() + .setRefreshToken(userGoogleToken.getRefreshToken()); + + // Thử refresh token - GoogleCredential sẽ tự động refresh nếu cần + if (!credential.refreshToken()) { + LogHelper.error("Không thể refresh token cho email: " + userGoogleToken.getEmail()); + return false; + } + + // Lấy token response từ credential đã refresh + String newAccessToken = credential.getAccessToken(); + Long expiresInSeconds = credential.getExpiresInSeconds(); + + // Cập nhật token mới vào database + Instant now = Instant.now(); + userGoogleToken.setAccessToken(newAccessToken); + userGoogleToken.setExpiresIn(expiresInSeconds); + userGoogleToken.setUpdatedAt(now); + + if (expiresInSeconds != null) { + userGoogleToken.setExpiresAt(now.plusSeconds(expiresInSeconds)); + } + + userGoogleTokenRepository.save(userGoogleToken); + + // Tạo TokenResponse để cập nhật vào TokenStore cache + TokenResponse tokenResponse = new TokenResponse() + .setAccessToken(newAccessToken) + .setRefreshToken(userGoogleToken.getRefreshToken()) + .setExpiresInSeconds(expiresInSeconds) + .setTokenType(userGoogleToken.getTokenType()) + .setScope(userGoogleToken.getScope()); + + if (userGoogleToken.getRefreshTokenExpiresIn() != null) { + tokenResponse.set("refresh_token_expires_in", userGoogleToken.getRefreshTokenExpiresIn()); + } + + tokenStore.storeToken(userGoogleToken.getEmail(), tokenResponse); + + LogHelper.info("Đã refresh thành công access token cho email: " + userGoogleToken.getEmail()); + return true; + + } catch (IOException e) { + LogHelper.error("Lỗi khi refresh token cho email: " + userGoogleToken.getEmail() + " - " + e.getMessage()); + return false; + } catch (Exception e) { + LogHelper.error("Lỗi không mong đợi khi refresh token cho email: " + userGoogleToken.getEmail() + " - " + e.getMessage()); + return false; + } + } +} diff --git a/vega-hrm-core/src/main/java/com/vega/hrm/core/entities/RevenueData.java b/vega-hrm-core/src/main/java/com/vega/hrm/core/entities/RevenueData.java new file mode 100644 index 0000000..fad041f --- /dev/null +++ b/vega-hrm-core/src/main/java/com/vega/hrm/core/entities/RevenueData.java @@ -0,0 +1,46 @@ +package com.vega.hrm.core.entities; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import java.time.Instant; +import java.time.LocalDate; +import java.util.UUID; +import lombok.Getter; +import lombok.Setter; +import org.hibernate.annotations.ColumnDefault; + +@Getter +@Setter +@Entity +@Table(name = "revenue_data") +public class RevenueData { + + @Id + @ColumnDefault("gen_random_uuid()") + @Column(name = "id", nullable = false) + private UUID id; + + @Size(max = 255) + @NotNull + @Column(name = "email", nullable = false, length = 255) + private String email; + + @NotNull + @Column(name = "revenue_date", nullable = false) + private LocalDate revenueDate; + + @Column(name = "estimated_revenue") + private Double estimatedRevenue; + + @ColumnDefault("CURRENT_TIMESTAMP") + @Column(name = "created_at", nullable = false) + private Instant createdAt; + + @ColumnDefault("CURRENT_TIMESTAMP") + @Column(name = "updated_at") + private Instant updatedAt; +} 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 new file mode 100644 index 0000000..8dde13c --- /dev/null +++ b/vega-hrm-core/src/main/java/com/vega/hrm/core/repositories/RevenueDataRepository.java @@ -0,0 +1,27 @@ +package com.vega.hrm.core.repositories; + +import com.vega.hrm.core.entities.RevenueData; +import java.time.LocalDate; +import java.util.UUID; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface RevenueDataRepository extends JpaRepository { + + @Query("SELECT r FROM RevenueData r WHERE r.email = :email " + + "AND r.revenueDate >= :startDate AND r.revenueDate <= :endDate " + + "ORDER BY r.revenueDate DESC") + Page findByEmailAndDateRange( + @Param("email") String email, + @Param("startDate") LocalDate startDate, + @Param("endDate") LocalDate endDate, + Pageable pageable + ); + + RevenueData findByEmailAndRevenueDate(String email, LocalDate revenueDate); +} diff --git a/vega-hrm-core/src/main/java/com/vega/hrm/core/repositories/UserGoogleTokenRepository.java b/vega-hrm-core/src/main/java/com/vega/hrm/core/repositories/UserGoogleTokenRepository.java index bfd0f83..4927f81 100644 --- a/vega-hrm-core/src/main/java/com/vega/hrm/core/repositories/UserGoogleTokenRepository.java +++ b/vega-hrm-core/src/main/java/com/vega/hrm/core/repositories/UserGoogleTokenRepository.java @@ -2,10 +2,29 @@ package com.vega.hrm.core.repositories; import com.vega.hrm.core.entities.BoUser; import com.vega.hrm.core.entities.UserGoogleToken; +import java.time.Instant; +import java.util.List; import java.util.UUID; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; @Repository public interface UserGoogleTokenRepository extends JpaRepository { UserGoogleToken findUserGoogleTokenByEmail(String user); + + @Query("SELECT DISTINCT u.email FROM UserGoogleToken u WHERE u.email IS NOT NULL") + List findAllDistinctEmails(); + + /** + * Tìm các token đã hết hạn (expires_at < now) nhưng refresh token vẫn còn hiệu lực (refresh_token_expires_at > now) + */ + @Query("SELECT u FROM UserGoogleToken u WHERE u.expiresAt < :now AND u.refreshTokenExpiresAt > :now AND u.refreshToken IS NOT NULL") + List findExpiredTokensWithValidRefreshToken(@Param("now") Instant now); + + /** + * Tìm các token sắp hết hạn (trong vòng X phút) + */ + @Query("SELECT u FROM UserGoogleToken u WHERE u.expiresAt < :expiryTime AND u.refreshTokenExpiresAt > :now AND u.refreshToken IS NOT NULL") + List findTokensExpiringSoon(@Param("expiryTime") Instant expiryTime, @Param("now") Instant now); } diff --git a/vega-hrm-report/build.gradle b/vega-hrm-report/build.gradle index 0a39c4e..7809975 100644 --- a/vega-hrm-report/build.gradle +++ b/vega-hrm-report/build.gradle @@ -38,6 +38,7 @@ dependencies { annotationProcessor 'org.projectlombok:lombok:1.18.38' implementation project(":vega-hrm-core") implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1' + implementation(platform("org.springframework.boot:spring-boot-dependencies:3.4.0")) } diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/VegaHrmReportApplication.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/VegaHrmReportApplication.java index 9aabfa7..26eb8e6 100644 --- a/vega-hrm-report/src/main/java/com/vega/hrm/report/VegaHrmReportApplication.java +++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/VegaHrmReportApplication.java @@ -3,10 +3,12 @@ package com.vega.hrm.report; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.scheduling.annotation.EnableScheduling; import org.springframework.transaction.annotation.EnableTransactionManagement; @SpringBootApplication(scanBasePackages = "com.vega.hrm") @EnableTransactionManagement +@EnableScheduling public class VegaHrmReportApplication { public static void main(String[] args) { 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 7dee775..5be2773 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 @@ -5,11 +5,13 @@ import com.google.api.client.json.jackson2.JacksonFactory; 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.GetDragRevenueRequest; import com.vega.hrm.report.request.GetRevenueRequest; import com.vega.hrm.report.response.ReportTypeDto; import com.vega.hrm.report.response.RevenueDataDto; import com.vega.hrm.report.serivce.CreateReportingJobService; +import com.vega.hrm.report.serivce.RevenueDataService; import com.vega.hrm.report.serivce.YouTubeReportService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; @@ -19,6 +21,9 @@ import java.io.IOException; import java.security.GeneralSecurityException; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; import org.springframework.http.ResponseEntity; import org.springframework.util.StringUtils; import org.springframework.web.bind.annotation.GetMapping; @@ -36,6 +41,7 @@ public class ReportGoogleController { private final CreateReportingJobService createReportingJob; private final YouTubeReportService youTubeReportService; + private final RevenueDataService revenueDataService; private final GoogleOAuthConfig googleOAuthConfig; private final TokenStore tokenStore; @@ -100,27 +106,37 @@ public class ReportGoogleController { } @PostMapping("/revenue") - @Operation(summary = "Lấy dữ liệu doanh thu", description = "Lấy dữ liệu doanh thu ước tính từ YouTube Analytics API theo khoảng thời gian.") - public ResponseEntity>> getRevenueData( + @Operation(summary = "Lấy dữ liệu doanh thu", description = "Lấy dữ liệu doanh thu ước tính từ database theo khoảng thời gian với phân trang.") + public ResponseEntity> getRevenueData( @Valid @RequestBody GetRevenueRequest request - ) throws GeneralSecurityException, IOException { + ) { if (tokenStore.getTokenResponse(request.getEmail()) == null) { - return ResponseEntity.ok(BaseResponse.invalid("Không tìm thấy token Google cho email đã cung cấp")); + return ResponseEntity.ok( + PagedListResponse.builder() + .code("01") + .message("Không tìm thấy token Google cho email đã cung cấp") + .build() + ); } - List revenueData = youTubeReportService.getRevenue( + // Tạo Pageable từ request + int pageIndex = request.getPageIndex() != null ? request.getPageIndex() : 1; + int pageSize = request.getPageSize() != null ? request.getPageSize() : 20; + Pageable pageable = PageRequest.of(pageIndex - 1, pageSize, Sort.by(Sort.Direction.DESC, "revenueDate")); + + // Lấy dữ liệu từ database với phân trang + var pagedList = revenueDataService.getRevenueData( request.getEmail(), request.getStartDate(), request.getEndDate(), - GoogleNetHttpTransport.newTrustedTransport(), - JacksonFactory.getDefaultInstance() + pageable ); return ResponseEntity.ok( - BaseResponse.>builder() + PagedListResponse.builder() .code("00") .message("Lấy dữ liệu doanh thu thành công") - .data(revenueData) + .data(pagedList) .build() ); } diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/request/GetRevenueRequest.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/request/GetRevenueRequest.java index 34b5aff..a5bc72e 100644 --- a/vega-hrm-report/src/main/java/com/vega/hrm/report/request/GetRevenueRequest.java +++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/request/GetRevenueRequest.java @@ -1,6 +1,8 @@ package com.vega.hrm.report.request; +import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.Positive; import lombok.Getter; import lombok.Setter; @@ -15,5 +17,11 @@ public class GetRevenueRequest { @NotBlank(message = "Ngày kết thúc không được để trống") private String endDate; // format: YYYY-MM-DD + + @Min(value = 1, message = "Số trang phải lớn hơn 0") + private Integer pageIndex = 1; + + @Positive(message = "Kích thước trang phải lớn hơn 0") + private Integer pageSize = 20; } diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/RevenueDataScheduledJob.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/RevenueDataScheduledJob.java new file mode 100644 index 0000000..1dd8101 --- /dev/null +++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/RevenueDataScheduledJob.java @@ -0,0 +1,160 @@ +package com.vega.hrm.report.serivce; + +import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport; +import com.google.api.client.json.jackson2.JacksonFactory; +import com.vega.hrm.core.component.TokenStore; +import com.vega.hrm.core.helpers.LogHelper; +import com.vega.hrm.core.repositories.UserGoogleTokenRepository; +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class RevenueDataScheduledJob { + + private final UserGoogleTokenRepository userGoogleTokenRepository; + private final TokenStore tokenStore; + private final YouTubeReportService youTubeReportService; + private final RevenueDataService revenueDataService; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + /** + * Job chạy hàng ngày lúc 2:00 AM để lấy và lưu dữ liệu doanh thu của ngày hôm qua + * Cron: 0 0 2 * * ? - Chạy lúc 2:00 AM mỗi ngày + */ + @Scheduled(cron = "0 0 2 * * ?") + public void syncRevenueDataDaily() { + LogHelper.info("Bắt đầu job đồng bộ dữ liệu doanh thu hàng ngày"); + + try { + // Lấy tất cả email có token Google + List emails = userGoogleTokenRepository.findAllDistinctEmails(); + + if (emails == null || emails.isEmpty()) { + LogHelper.info("Không tìm thấy email nào có token Google để đồng bộ"); + return; + } + + // Lấy dữ liệu của ngày hôm qua + LocalDate yesterday = LocalDate.now().minusDays(1); + String dateStr = yesterday.format(DATE_FORMATTER); + + LogHelper.info("Đang đồng bộ dữ liệu doanh thu cho ngày: " + dateStr); + + int successCount = 0; + int failCount = 0; + + for (String email : emails) { + try { + if (tokenStore.getTokenResponse(email) == null) { + LogHelper.info("Không tìm thấy token cho email: " + email + ", bỏ qua"); + failCount++; + continue; + } + + // Lấy dữ liệu từ YouTube API + List revenueData = youTubeReportService.getRevenue( + email, + dateStr, + dateStr, + GoogleNetHttpTransport.newTrustedTransport(), + JacksonFactory.getDefaultInstance() + ); + + if (revenueData != null && !revenueData.isEmpty()) { + // Lưu vào database + revenueDataService.saveRevenueData(email, revenueData); + LogHelper.info("Đã đồng bộ thành công dữ liệu doanh thu cho email: " + email); + successCount++; + } else { + LogHelper.info("Không có dữ liệu doanh thu cho email: " + email + " vào ngày " + dateStr); + successCount++; // Vẫn tính là success vì không có lỗi + } + + } catch (GeneralSecurityException | IOException e) { + LogHelper.error("Lỗi khi đồng bộ dữ liệu doanh thu cho email: " + email + " - " + e.getMessage()); + failCount++; + } catch (Exception e) { + LogHelper.error("Lỗi không mong đợi khi đồng bộ dữ liệu doanh thu cho email: " + email + " - " + e.getMessage()); + failCount++; + } + } + + LogHelper.info("Hoàn thành job đồng bộ dữ liệu doanh thu. Thành công: " + successCount + ", Thất bại: " + failCount); + + } catch (Exception e) { + LogHelper.error("Lỗi nghiêm trọng trong job đồng bộ dữ liệu doanh thu: " + e.getMessage()); + } + } + + /** + * Job để đồng bộ lại dữ liệu của các ngày trước (chạy mỗi tuần một lần vào Chủ nhật lúc 3:00 AM) + * Có thể dùng để đồng bộ lại dữ liệu của 7 ngày gần nhất nếu có thiếu sót + */ + @Scheduled(cron = "0 0 3 * * SUN") + public void syncRevenueDataWeekly() { + LogHelper.info("Bắt đầu job đồng bộ lại dữ liệu doanh thu hàng tuần"); + + try { + List emails = userGoogleTokenRepository.findAllDistinctEmails(); + + if (emails == null || emails.isEmpty()) { + LogHelper.info("Không tìm thấy email nào có token Google để đồng bộ"); + return; + } + + // Đồng bộ lại dữ liệu của 7 ngày gần nhất + LocalDate endDate = LocalDate.now().minusDays(1); + LocalDate startDate = endDate.minusDays(6); + String startDateStr = startDate.format(DATE_FORMATTER); + String endDateStr = endDate.format(DATE_FORMATTER); + + LogHelper.info("Đang đồng bộ lại dữ liệu doanh thu từ " + startDateStr + " đến " + endDateStr); + + int successCount = 0; + int failCount = 0; + + for (String email : emails) { + try { + if (tokenStore.getTokenResponse(email) == null) { + LogHelper.info("Không tìm thấy token cho email: " + email + ", bỏ qua"); + failCount++; + continue; + } + + List revenueData = youTubeReportService.getRevenue( + email, + startDateStr, + endDateStr, + GoogleNetHttpTransport.newTrustedTransport(), + JacksonFactory.getDefaultInstance() + ); + + if (revenueData != null && !revenueData.isEmpty()) { + revenueDataService.saveRevenueData(email, revenueData); + LogHelper.info("Đã đồng bộ lại thành công dữ liệu doanh thu cho email: " + email); + successCount++; + } + + } catch (GeneralSecurityException | IOException e) { + LogHelper.error("Lỗi khi đồng bộ lại dữ liệu doanh thu cho email: " + email + " - " + e.getMessage()); + failCount++; + } catch (Exception e) { + LogHelper.error("Lỗi không mong đợi khi đồng bộ lại dữ liệu doanh thu cho email: " + email + " - " + e.getMessage()); + failCount++; + } + } + + LogHelper.info("Hoàn thành job đồng bộ lại dữ liệu doanh thu. Thành công: " + successCount + ", Thất bại: " + failCount); + + } catch (Exception e) { + LogHelper.error("Lỗi nghiêm trọng trong job đồng bộ lại dữ liệu doanh thu: " + e.getMessage()); + } + } +} diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/RevenueDataService.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/RevenueDataService.java new file mode 100644 index 0000000..353c513 --- /dev/null +++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/RevenueDataService.java @@ -0,0 +1,104 @@ +package com.vega.hrm.report.serivce; + +import com.vega.hrm.core.entities.RevenueData; +import com.vega.hrm.core.helpers.LogHelper; +import com.vega.hrm.core.models.responses.PagedList; +import com.vega.hrm.core.repositories.RevenueDataRepository; +import com.vega.hrm.report.response.RevenueDataDto; +import java.time.Instant; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.util.List; +import java.util.UUID; +import java.util.stream.Collectors; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class RevenueDataService { + + private final RevenueDataRepository revenueDataRepository; + private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + /** + * Lưu danh sách dữ liệu doanh thu vào database + */ + @Transactional + public void saveRevenueData(String email, List revenueDataList) { + if (revenueDataList == null || revenueDataList.isEmpty()) { + LogHelper.info("Không có dữ liệu doanh thu nào để lưu cho email: " + email); + return; + } + + int savedCount = 0; + int updatedCount = 0; + + for (RevenueDataDto dto : revenueDataList) { + try { + LocalDate revenueDate = LocalDate.parse(dto.getDate(), DATE_FORMATTER); + + RevenueData existingData = revenueDataRepository.findByEmailAndRevenueDate(email, revenueDate); + + if (existingData != null) { + // Cập nhật dữ liệu đã tồn tại + existingData.setEstimatedRevenue(dto.getEstimatedRevenue()); + existingData.setUpdatedAt(Instant.now()); + revenueDataRepository.save(existingData); + updatedCount++; + } else { + // Tạo mới dữ liệu + RevenueData revenueData = new RevenueData(); + revenueData.setId(UUID.randomUUID()); + revenueData.setEmail(email); + revenueData.setRevenueDate(revenueDate); + revenueData.setEstimatedRevenue(dto.getEstimatedRevenue()); + revenueData.setCreatedAt(Instant.now()); + revenueData.setUpdatedAt(Instant.now()); + revenueDataRepository.save(revenueData); + savedCount++; + } + } catch (Exception e) { + LogHelper.error("Lỗi khi lưu dữ liệu doanh thu cho ngày " + dto.getDate() + ": " + e.getMessage()); + } + } + + LogHelper.info("Đã lưu " + savedCount + " bản ghi mới và cập nhật " + updatedCount + " bản ghi cho email: " + email); + } + + /** + * Lấy dữ liệu doanh thu từ database với phân trang + */ + public PagedList getRevenueData(String email, String startDate, String endDate, Pageable pageable) { + LocalDate start = LocalDate.parse(startDate, DATE_FORMATTER); + LocalDate end = LocalDate.parse(endDate, DATE_FORMATTER); + + Page page = revenueDataRepository.findByEmailAndDateRange(email, start, end, pageable); + + List items = page.getContent().stream() + .map(this::convertToDto) + .collect(Collectors.toList()); + + int pageCount = page.getTotalPages(); + long totalItemCount = page.getTotalElements(); + + return PagedList.builder() + .pageCount(pageCount) + .totalItemCount(totalItemCount) + .items(items) + .build(); + } + + /** + * Convert entity sang DTO + */ + private RevenueDataDto convertToDto(RevenueData entity) { + return RevenueDataDto.builder() + .date(entity.getRevenueDate().format(DATE_FORMATTER)) + .estimatedRevenue(entity.getEstimatedRevenue()) + .build(); + } +}