feat : thêm bao cao
This commit is contained in:
parent
abd31bf956
commit
f9626aa701
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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"))
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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<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 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<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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user