feat : thêm bao cao

This commit is contained in:
nguyennt1 2025-12-03 23:10:40 +07:00
parent abd31bf956
commit f9626aa701
12 changed files with 609 additions and 9 deletions

View File

@ -4,10 +4,12 @@ import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan; import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories; import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication(scanBasePackages = "com.vega.hrm") @SpringBootApplication(scanBasePackages = "com.vega.hrm")
@EnableTransactionManagement @EnableTransactionManagement
@EnableScheduling
public class VegaHrmAuthApplication { public class VegaHrmAuthApplication {
public static void main(String[] args) { public static void main(String[] args) {
SpringApplication.run(VegaHrmAuthApplication.class, args); SpringApplication.run(VegaHrmAuthApplication.class, args);

View File

@ -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<UserGoogleToken> 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<UserGoogleToken> 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());
}
}
}

View File

@ -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ò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;
}
}
}

View File

@ -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;
}

View File

@ -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<RevenueData, UUID> {
@Query("SELECT r FROM RevenueData r WHERE r.email = :email " +
"AND r.revenueDate >= :startDate AND r.revenueDate <= :endDate " +
"ORDER BY r.revenueDate DESC")
Page<RevenueData> findByEmailAndDateRange(
@Param("email") String email,
@Param("startDate") LocalDate startDate,
@Param("endDate") LocalDate endDate,
Pageable pageable
);
RevenueData findByEmailAndRevenueDate(String email, LocalDate revenueDate);
}

View File

@ -2,10 +2,29 @@ package com.vega.hrm.core.repositories;
import com.vega.hrm.core.entities.BoUser; import com.vega.hrm.core.entities.BoUser;
import com.vega.hrm.core.entities.UserGoogleToken; import com.vega.hrm.core.entities.UserGoogleToken;
import java.time.Instant;
import java.util.List;
import java.util.UUID; import java.util.UUID;
import org.springframework.data.jpa.repository.JpaRepository; 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; import org.springframework.stereotype.Repository;
@Repository @Repository
public interface UserGoogleTokenRepository extends JpaRepository<UserGoogleToken, UUID> { public interface UserGoogleTokenRepository extends JpaRepository<UserGoogleToken, UUID> {
UserGoogleToken findUserGoogleTokenByEmail(String user); UserGoogleToken findUserGoogleTokenByEmail(String user);
@Query("SELECT DISTINCT u.email FROM UserGoogleToken u WHERE u.email IS NOT NULL")
List<String> 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<UserGoogleToken> 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<UserGoogleToken> findTokensExpiringSoon(@Param("expiryTime") Instant expiryTime, @Param("now") Instant now);
} }

View File

@ -38,6 +38,7 @@ dependencies {
annotationProcessor 'org.projectlombok:lombok:1.18.38' annotationProcessor 'org.projectlombok:lombok:1.18.38'
implementation project(":vega-hrm-core") implementation project(":vega-hrm-core")
implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1' implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1'
implementation(platform("org.springframework.boot:spring-boot-dependencies:3.4.0"))
} }

View File

@ -3,10 +3,12 @@ package com.vega.hrm.report;
import org.springframework.boot.SpringApplication; import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement; import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication(scanBasePackages = "com.vega.hrm") @SpringBootApplication(scanBasePackages = "com.vega.hrm")
@EnableTransactionManagement @EnableTransactionManagement
@EnableScheduling
public class VegaHrmReportApplication { public class VegaHrmReportApplication {
public static void main(String[] args) { public static void main(String[] args) {

View File

@ -5,11 +5,13 @@ import com.google.api.client.json.jackson2.JacksonFactory;
import com.vega.hrm.core.component.TokenStore; import com.vega.hrm.core.component.TokenStore;
import com.vega.hrm.core.dto.GoogleOAuthConfig; import com.vega.hrm.core.dto.GoogleOAuthConfig;
import com.vega.hrm.core.models.responses.BaseResponse; 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.GetDragRevenueRequest;
import com.vega.hrm.report.request.GetRevenueRequest; import com.vega.hrm.report.request.GetRevenueRequest;
import com.vega.hrm.report.response.ReportTypeDto; import com.vega.hrm.report.response.ReportTypeDto;
import com.vega.hrm.report.response.RevenueDataDto; import com.vega.hrm.report.response.RevenueDataDto;
import com.vega.hrm.report.serivce.CreateReportingJobService; import com.vega.hrm.report.serivce.CreateReportingJobService;
import com.vega.hrm.report.serivce.RevenueDataService;
import com.vega.hrm.report.serivce.YouTubeReportService; import com.vega.hrm.report.serivce.YouTubeReportService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.Parameter;
@ -19,6 +21,9 @@ import java.io.IOException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; 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.http.ResponseEntity;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
@ -36,6 +41,7 @@ public class ReportGoogleController {
private final CreateReportingJobService createReportingJob; private final CreateReportingJobService createReportingJob;
private final YouTubeReportService youTubeReportService; private final YouTubeReportService youTubeReportService;
private final RevenueDataService revenueDataService;
private final GoogleOAuthConfig googleOAuthConfig; private final GoogleOAuthConfig googleOAuthConfig;
private final TokenStore tokenStore; private final TokenStore tokenStore;
@ -100,27 +106,37 @@ public class ReportGoogleController {
} }
@PostMapping("/revenue") @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.") @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<BaseResponse<List<RevenueDataDto>>> getRevenueData( public ResponseEntity<PagedListResponse<RevenueDataDto>> getRevenueData(
@Valid @RequestBody GetRevenueRequest request @Valid @RequestBody GetRevenueRequest request
) throws GeneralSecurityException, IOException { ) {
if (tokenStore.getTokenResponse(request.getEmail()) == null) { 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.<RevenueDataDto>builder()
.code("01")
.message("Không tìm thấy token Google cho email đã cung cấp")
.build()
);
} }
List<RevenueDataDto> 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.getEmail(),
request.getStartDate(), request.getStartDate(),
request.getEndDate(), request.getEndDate(),
GoogleNetHttpTransport.newTrustedTransport(), pageable
JacksonFactory.getDefaultInstance()
); );
return ResponseEntity.ok( return ResponseEntity.ok(
BaseResponse.<List<RevenueDataDto>>builder() PagedListResponse.<RevenueDataDto>builder()
.code("00") .code("00")
.message("Lấy dữ liệu doanh thu thành công") .message("Lấy dữ liệu doanh thu thành công")
.data(revenueData) .data(pagedList)
.build() .build()
); );
} }

View File

@ -1,6 +1,8 @@
package com.vega.hrm.report.request; package com.vega.hrm.report.request;
import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Positive;
import lombok.Getter; import lombok.Getter;
import lombok.Setter; import lombok.Setter;
@ -15,5 +17,11 @@ public class GetRevenueRequest {
@NotBlank(message = "Ngày kết thúc không được để trống") @NotBlank(message = "Ngày kết thúc không được để trống")
private String endDate; // format: YYYY-MM-DD 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;
} }

View File

@ -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 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 token Google
List<String> 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<com.vega.hrm.report.response.RevenueDataDto> 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 success không 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)
* thể dùng để đồng bộ lại dữ liệu của 7 ngày gần nhất nếu 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<String> 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<com.vega.hrm.report.response.RevenueDataDto> 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());
}
}
}

View File

@ -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<RevenueDataDto> 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<RevenueDataDto> getRevenueData(String email, String startDate, String endDate, Pageable pageable) {
LocalDate start = LocalDate.parse(startDate, DATE_FORMATTER);
LocalDate end = LocalDate.parse(endDate, DATE_FORMATTER);
Page<RevenueData> page = revenueDataRepository.findByEmailAndDateRange(email, start, end, pageable);
List<RevenueDataDto> items = page.getContent().stream()
.map(this::convertToDto)
.collect(Collectors.toList());
int pageCount = page.getTotalPages();
long totalItemCount = page.getTotalElements();
return PagedList.<RevenueDataDto>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();
}
}