diff --git a/.cursor/worktrees.json b/.cursor/worktrees.json
new file mode 100644
index 0000000..77e9744
--- /dev/null
+++ b/.cursor/worktrees.json
@@ -0,0 +1,5 @@
+{
+ "setup-worktree": [
+ "npm install"
+ ]
+}
diff --git a/.idea/modules/vega-hrm-report/VegaHRM.Backend.vega-hrm-report.main.iml b/.idea/modules/vega-hrm-report/VegaHRM.Backend.vega-hrm-report.main.iml
new file mode 100644
index 0000000..835666a
--- /dev/null
+++ b/.idea/modules/vega-hrm-report/VegaHRM.Backend.vega-hrm-report.main.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/logs/vega-hrm-auth/error/app-error-2025-11-15.log.gz b/logs/vega-hrm-auth/error/app-error-2025-11-15.log.gz
new file mode 100644
index 0000000..d992220
Binary files /dev/null and b/logs/vega-hrm-auth/error/app-error-2025-11-15.log.gz differ
diff --git a/logs/vega-hrm-auth/error/app-error-2025-11-17.log.gz b/logs/vega-hrm-auth/error/app-error-2025-11-17.log.gz
new file mode 100644
index 0000000..6d461b2
Binary files /dev/null and b/logs/vega-hrm-auth/error/app-error-2025-11-17.log.gz differ
diff --git a/logs/vega-hrm-auth/info/app-info-2025-11-15.log.gz b/logs/vega-hrm-auth/info/app-info-2025-11-15.log.gz
new file mode 100644
index 0000000..d977c5a
Binary files /dev/null and b/logs/vega-hrm-auth/info/app-info-2025-11-15.log.gz differ
diff --git a/logs/vega-hrm-auth/info/app-info-2025-11-17.log.gz b/logs/vega-hrm-auth/info/app-info-2025-11-17.log.gz
new file mode 100644
index 0000000..c9ba689
Binary files /dev/null and b/logs/vega-hrm-auth/info/app-info-2025-11-17.log.gz differ
diff --git a/logs/vega-hrm-report/error/app-error-2025-11-24.log.gz b/logs/vega-hrm-report/error/app-error-2025-11-24.log.gz
new file mode 100644
index 0000000..594910c
Binary files /dev/null and b/logs/vega-hrm-report/error/app-error-2025-11-24.log.gz differ
diff --git a/logs/vega-hrm-report/info/app-info-2025-11-15.log.gz b/logs/vega-hrm-report/info/app-info-2025-11-15.log.gz
new file mode 100644
index 0000000..a4f7150
Binary files /dev/null and b/logs/vega-hrm-report/info/app-info-2025-11-15.log.gz differ
diff --git a/logs/vega-hrm-report/info/app-info-2025-11-17.log.gz b/logs/vega-hrm-report/info/app-info-2025-11-17.log.gz
new file mode 100644
index 0000000..07a57e5
Binary files /dev/null and b/logs/vega-hrm-report/info/app-info-2025-11-17.log.gz differ
diff --git a/logs/vega-hrm-report/info/app-info-2025-11-24.log.gz b/logs/vega-hrm-report/info/app-info-2025-11-24.log.gz
new file mode 100644
index 0000000..cf44133
Binary files /dev/null and b/logs/vega-hrm-report/info/app-info-2025-11-24.log.gz differ
diff --git a/vega-hrm-auth/build.gradle b/vega-hrm-auth/build.gradle
index ffef6d8..91dd460 100644
--- a/vega-hrm-auth/build.gradle
+++ b/vega-hrm-auth/build.gradle
@@ -22,18 +22,16 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-log4j2:3.4.0'
implementation 'org.springframework.boot:spring-boot-starter-validation:3.4.0'
implementation 'de.mkammerer:argon2-jvm:2.1'
+ // Google API Services
implementation "com.google.apis:google-api-services-youtube:v3-rev182-1.22.0"
- implementation("com.google.collections:google-collections:1.0")
- implementation("com.google.guava:guava:31.1-jre")
implementation("com.google.apis:google-api-services-youtubeAnalytics:v2-rev272-1.25.0")
- implementation "com.google.http-client:google-http-client-jackson2:1.20.0"
-
- // OAuth Client
implementation 'com.google.apis:google-api-services-oauth2:v2-rev157-1.25.0'
+
+ // Google API Client (phiên bản mới)
+ implementation 'com.google.api-client:google-api-client:1.34.1'
implementation 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
- implementation 'com.google.api-client:google-api-client:1.32.1'
- // Google Collections
- implementation "com.google.collections:google-collections:1.0"
+ implementation "com.google.http-client:google-http-client-jackson2:1.43.3"
+ implementation 'com.google.guava:guava:32.1.3-jre'
implementation 'com.google.code.gson:gson:2.11.0'
annotationProcessor 'org.projectlombok:lombok:1.18.38'
implementation project(":vega-hrm-core")
@@ -50,11 +48,14 @@ configurations {
exclude group: 'ch.qos.logback', module: 'logback-classic'
exclude group: 'com.google.guava', module: 'guava-jdk5'
- exclude group: 'com.google.collections', module: 'google-collections' // very old
+ exclude group: 'com.google.collections', module: 'google-collections'
resolutionStrategy {
- // bắt buộc dùng guava hiện đại
- force "com.google.guava:guava:32.1.3-jre"
+ // Bắt buộc dùng Guava mới nhất
+ force 'com.google.guava:guava:32.1.3-jre'
+ force 'com.google.api-client:google-api-client:1.34.1'
+ force 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
+ force 'com.google.http-client:google-http-client-jackson2:1.43.3'
}
}
}
diff --git a/vega-hrm-auth/src/main/java/com/vega/hrm/config/OpenApiConfig.java b/vega-hrm-auth/src/main/java/com/vega/hrm/config/OpenApiConfig.java
new file mode 100644
index 0000000..a814df2
--- /dev/null
+++ b/vega-hrm-auth/src/main/java/com/vega/hrm/config/OpenApiConfig.java
@@ -0,0 +1,36 @@
+package com.vega.hrm.config;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.info.License;
+import org.springdoc.core.models.GroupedOpenApi;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class OpenApiConfig {
+
+ @Bean
+ public OpenAPI authOpenAPI() {
+ return new OpenAPI()
+ .info(new Info()
+ .title("Vega HRM Auth API")
+ .version("v1")
+ .description("Tài liệu mô tả các API xác thực, phân quyền và tích hợp Google cho Vega HRM.")
+ .contact(new Contact()
+ .name("Vega HRM Backend Team")
+ .email("backend@vegahrm.local"))
+ .license(new License().name("Proprietary")));
+ }
+
+ @Bean
+ public GroupedOpenApi authGroupedOpenApi() {
+ return GroupedOpenApi.builder()
+ .group("auth")
+ .packagesToScan("com.vega.hrm.controller")
+ .pathsToMatch("/api/**")
+ .build();
+ }
+}
+
diff --git a/vega-hrm-auth/src/main/java/com/vega/hrm/controller/GoogleController.java b/vega-hrm-auth/src/main/java/com/vega/hrm/controller/GoogleController.java
index 36ed050..b8d574d 100644
--- a/vega-hrm-auth/src/main/java/com/vega/hrm/controller/GoogleController.java
+++ b/vega-hrm-auth/src/main/java/com/vega/hrm/controller/GoogleController.java
@@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
-@RequestMapping("api/google/user")
+@RequestMapping({"api/google/user", "api/auth/google"})
@RequiredArgsConstructor
@Tag(name = "Auth - Google", description = "Luồng OAuth2 với Google phục vụ xác thực người dùng")
public class GoogleController {
diff --git a/vega-hrm-auth/src/main/java/com/vega/hrm/service/GoogleService.java b/vega-hrm-auth/src/main/java/com/vega/hrm/service/GoogleService.java
index edd313d..e56a08f 100644
--- a/vega-hrm-auth/src/main/java/com/vega/hrm/service/GoogleService.java
+++ b/vega-hrm-auth/src/main/java/com/vega/hrm/service/GoogleService.java
@@ -6,7 +6,6 @@ import com.google.api.client.auth.oauth2.AuthorizationCodeFlow;
import com.google.api.client.auth.oauth2.AuthorizationCodeRequestUrl;
import com.google.api.client.auth.oauth2.BearerToken;
import com.google.api.client.auth.oauth2.Credential;
-import com.google.api.client.auth.oauth2.TokenResponse;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse;
@@ -21,14 +20,12 @@ import com.vega.hrm.core.models.responses.BaseResponse;
import com.vega.hrm.core.dto.GoogleOAuthConfig;
import com.google.api.services.oauth2.model.Userinfo;
import com.vega.hrm.core.repositories.UserGoogleTokenRepository;
-import com.vega.hrm.dto.CustomTokenResponse;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.time.Instant;
import java.util.Objects;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
-import lombok.Setter;
import org.springframework.stereotype.Service;
@Service
@@ -100,26 +97,40 @@ public class GoogleService {
String email = userInfo.getEmail();
var userGoogleToken = userGoogleTokenRepository.findUserGoogleTokenByEmail(email);
-
if (userGoogleToken == null) {
userGoogleToken = new UserGoogleToken();
userGoogleToken.setId(UUID.randomUUID());
userGoogleToken.setEmail(email);
- userGoogleToken.setAccessToken(tokenResponse.getAccessToken());
- userGoogleToken.setRefreshToken(tokenResponse.getRefreshToken());
- userGoogleToken.setScope(tokenResponse.getScope());
- userGoogleToken.setExpiresIn(tokenResponse.getExpiresInSeconds());
- userGoogleToken.setRefreshTokenExpiresIn(tokenResponse.getExpiresInSeconds());
- userGoogleToken.setExpiresAt(Instant.now().plusSeconds(tokenResponse.getExpiresInSeconds()));
- userGoogleToken.setTokenType(tokenResponse.getTokenType());
-
- userGoogleToken.setRefreshTokenExpiresAt(Instant.now().plusSeconds(tokenResponse.get("refresh_token_expires_in") != null
- ? Long.valueOf(tokenResponse.get("refresh_token_expires_in").toString())
- : null));
userGoogleToken.setCreatedAt(Instant.now());
- userGoogleTokenRepository.save(userGoogleToken);
}
+ Instant now = Instant.now();
+ Long expiresInSeconds = tokenResponse.getExpiresInSeconds();
+ Object refreshTokenExpiresInObj = tokenResponse.get("refresh_token_expires_in");
+ Long refreshTokenExpiresInSeconds = refreshTokenExpiresInObj != null
+ ? Long.valueOf(refreshTokenExpiresInObj.toString())
+ : null;
+
+ userGoogleToken.setAccessToken(tokenResponse.getAccessToken());
+ userGoogleToken.setRefreshToken(tokenResponse.getRefreshToken());
+ userGoogleToken.setScope(tokenResponse.getScope());
+ userGoogleToken.setExpiresIn(expiresInSeconds);
+ userGoogleToken.setRefreshTokenExpiresIn(refreshTokenExpiresInSeconds);
+ userGoogleToken.setTokenType(tokenResponse.getTokenType());
+ userGoogleToken.setUpdatedAt(now);
+
+ if (expiresInSeconds != null) {
+ userGoogleToken.setExpiresAt(now.plusSeconds(expiresInSeconds));
+ } else if (userGoogleToken.getExpiresAt() == null) {
+ userGoogleToken.setExpiresAt(now);
+ }
+ if (refreshTokenExpiresInSeconds != null) {
+ userGoogleToken.setRefreshTokenExpiresAt(now.plusSeconds(refreshTokenExpiresInSeconds));
+ } else if (userGoogleToken.getRefreshTokenExpiresAt() == null) {
+ userGoogleToken.setRefreshTokenExpiresAt(now);
+ }
+
+ userGoogleTokenRepository.save(userGoogleToken);
tokenStore.storeToken(email, tokenResponse);
return BaseResponse.success("00",true);
}
diff --git a/vega-hrm-core/build.gradle b/vega-hrm-core/build.gradle
index ebd3673..795f8ec 100644
--- a/vega-hrm-core/build.gradle
+++ b/vega-hrm-core/build.gradle
@@ -44,17 +44,15 @@ dependencies {
implementation 'org.bouncycastle:bcpkix-jdk18on:1.80'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
+ // Google API Services
implementation "com.google.apis:google-api-services-youtube:v3-rev182-1.22.0"
- implementation("com.google.collections:google-collections:1.0")
- implementation("com.google.guava:guava:20.0")
implementation("com.google.apis:google-api-services-youtubeAnalytics:v2-rev272-1.25.0")
- implementation "com.google.http-client:google-http-client-jackson2:1.20.0"
- // OAuth Client
- implementation "com.google.oauth-client:google-oauth-client-jetty:1.20.0"
-
- // Google Collections
- implementation "com.google.collections:google-collections:1.0"
+ // Google API Client (phiên bản mới)
+ implementation 'com.google.api-client:google-api-client:1.34.1'
+ implementation 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
+ implementation "com.google.http-client:google-http-client-jackson2:1.43.3"
+ implementation 'com.google.guava:guava:32.1.3-jre'
implementation 'com.google.code.gson:gson:2.11.0'
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-orgjson
@@ -68,5 +66,17 @@ configurations {
// Loại bỏ logging mặc định
exclude group: 'ch.qos.logback', module: 'logback-classic'
+
+ // Loại bỏ các thư viện Guava/Google Collections cũ
+ exclude group: 'com.google.guava', module: 'guava-jdk5'
+ exclude group: 'com.google.collections', module: 'google-collections'
+
+ resolutionStrategy {
+ // Bắt buộc dùng Guava mới nhất
+ force 'com.google.guava:guava:32.1.3-jre'
+ force 'com.google.api-client:google-api-client:1.34.1'
+ force 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
+ force 'com.google.http-client:google-http-client-jackson2:1.43.3'
+ }
}
}
diff --git a/vega-hrm-core/src/main/java/com/vega/hrm/core/component/TokenStore.java b/vega-hrm-core/src/main/java/com/vega/hrm/core/component/TokenStore.java
index cd2a423..5218640 100644
--- a/vega-hrm-core/src/main/java/com/vega/hrm/core/component/TokenStore.java
+++ b/vega-hrm-core/src/main/java/com/vega/hrm/core/component/TokenStore.java
@@ -2,26 +2,38 @@ package com.vega.hrm.core.component;
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.auth.oauth2.TokenResponse;
+import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
+import com.vega.hrm.core.entities.UserGoogleToken;
+import com.vega.hrm.core.repositories.UserGoogleTokenRepository;
import java.util.concurrent.ConcurrentHashMap;
-import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
+import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component;
@Component
+@RequiredArgsConstructor
public class TokenStore {
private final ConcurrentHashMap tokenMap = new ConcurrentHashMap<>();
+ private final UserGoogleTokenRepository userGoogleTokenRepository;
public void storeToken(String userId, TokenResponse tokenResponse) {
tokenMap.put(userId, tokenResponse);
}
public TokenResponse getTokenResponse(String userId) {
- return tokenMap.get(userId);
+ TokenResponse tokenResponse = tokenMap.get(userId);
+ if (tokenResponse == null) {
+ tokenResponse = loadTokenFromDatabase(userId);
+ if (tokenResponse != null) {
+ tokenMap.put(userId, tokenResponse);
+ }
+ }
+ return tokenResponse;
}
public Credential buildCredential(HttpTransport transport, JsonFactory jsonFactory, String userId, String clientId, String clientSecret) {
- TokenResponse tokenResponse = tokenMap.get(userId);
+ TokenResponse tokenResponse = getTokenResponse(userId);
if (tokenResponse == null) {
return null;
}
@@ -33,4 +45,24 @@ public class TokenStore {
.build()
.setFromTokenResponse(tokenResponse);
}
+
+ private TokenResponse loadTokenFromDatabase(String userId) {
+ UserGoogleToken userGoogleToken = userGoogleTokenRepository.findUserGoogleTokenByEmail(userId);
+ if (userGoogleToken == null) {
+ return null;
+ }
+
+ TokenResponse tokenResponse = new TokenResponse()
+ .setAccessToken(userGoogleToken.getAccessToken())
+ .setRefreshToken(userGoogleToken.getRefreshToken())
+ .setScope(userGoogleToken.getScope())
+ .setTokenType(userGoogleToken.getTokenType())
+ .setExpiresInSeconds(userGoogleToken.getExpiresIn());
+
+ if (userGoogleToken.getRefreshTokenExpiresIn() != null) {
+ tokenResponse.set("refresh_token_expires_in", userGoogleToken.getRefreshTokenExpiresIn());
+ }
+
+ return tokenResponse;
+ }
}
diff --git a/vega-hrm-report/build.gradle b/vega-hrm-report/build.gradle
index ddb3451..0a39c4e 100644
--- a/vega-hrm-report/build.gradle
+++ b/vega-hrm-report/build.gradle
@@ -22,28 +22,23 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-log4j2:3.4.0'
implementation 'org.springframework.boot:spring-boot-starter-validation:3.4.0'
implementation 'de.mkammerer:argon2-jvm:2.1'
- implementation "com.google.apis:google-api-services-youtube:v3-rev182-1.22.0"
- implementation("com.google.collections:google-collections:1.0")
- implementation("com.google.guava:guava:20.0")
- implementation("com.google.apis:google-api-services-youtubeAnalytics:v2-rev272-1.25.0")
- implementation "com.google.http-client:google-http-client-jackson2:1.20.0"
-
- // OAuth Client
- implementation "com.google.oauth-client:google-oauth-client-jetty:1.20.0"
+ // Google API Services
+ implementation 'com.google.apis:google-api-services-youtube:v3-rev182-1.22.0'
+ implementation 'com.google.apis:google-api-services-youtubeAnalytics:v2-rev272-1.25.0'
+ implementation 'com.google.apis:google-api-services-youtubereporting:v1-rev10-1.22.0'
implementation 'com.google.apis:google-api-services-oauth2:v2-rev157-1.25.0'
- implementation 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
- implementation 'com.google.api-client:google-api-client:2.3.0'
- // Google Collections
- implementation "com.google.collections:google-collections:1.0"
+ // Google API Client (phiên bản mới để tương thích với YouTube libs)
+ implementation 'com.google.api-client:google-api-client:1.34.1'
+ implementation 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
+ implementation 'com.google.http-client:google-http-client-jackson2:1.43.3'
+ implementation 'com.google.guava:guava:32.1.3-jre'
implementation 'com.google.code.gson:gson:2.11.0'
+
annotationProcessor 'org.projectlombok:lombok:1.18.38'
implementation project(":vega-hrm-core")
implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1'
- // YouTube Reporting API
- implementation "com.google.apis:google-api-services-youtubereporting:v1-rev10-1.22.0"
-
}
configurations {
@@ -53,5 +48,17 @@ configurations {
// Loại bỏ logging mặc định
exclude group: 'ch.qos.logback', module: 'logback-classic'
+
+ // Loại bỏ các thư viện Guava/Google Collections cũ
+ exclude group: 'com.google.guava', module: 'guava-jdk5'
+ exclude group: 'com.google.collections', module: 'google-collections'
+
+ resolutionStrategy {
+ // Bắt buộc dùng Guava mới nhất
+ force 'com.google.guava:guava:32.1.3-jre'
+ force 'com.google.api-client:google-api-client:1.34.1'
+ force 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
+ force 'com.google.http-client:google-http-client-jackson2:1.43.3'
+ }
}
}
diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/config/OpenApiConfig.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/config/OpenApiConfig.java
new file mode 100644
index 0000000..71a8bb0
--- /dev/null
+++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/config/OpenApiConfig.java
@@ -0,0 +1,36 @@
+package com.vega.hrm.report.config;
+
+import io.swagger.v3.oas.models.OpenAPI;
+import io.swagger.v3.oas.models.info.Contact;
+import io.swagger.v3.oas.models.info.Info;
+import io.swagger.v3.oas.models.info.License;
+import org.springdoc.core.models.GroupedOpenApi;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class OpenApiConfig {
+
+ @Bean
+ public OpenAPI reportOpenAPI() {
+ return new OpenAPI()
+ .info(new Info()
+ .title("Vega HRM Report API")
+ .version("v1")
+ .description("Danh mục API phục vụ trích xuất báo cáo và đồng bộ dữ liệu YouTube/Google cho Vega HRM.")
+ .contact(new Contact()
+ .name("Vega HRM Backend Team")
+ .email("backend@vegahrm.local"))
+ .license(new License().name("Proprietary")));
+ }
+
+ @Bean
+ public GroupedOpenApi reportGroupedOpenApi() {
+ return GroupedOpenApi.builder()
+ .group("report")
+ .packagesToScan("com.vega.hrm.report.controller")
+ .pathsToMatch("/api/**")
+ .build();
+ }
+}
+
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 db219cc..7dee775 100644
--- a/vega-hrm-report/src/main/java/com/vega/hrm/report/controller/ReportGoogleController.java
+++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/controller/ReportGoogleController.java
@@ -6,15 +6,26 @@ 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.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.YouTubeReportService;
import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.validation.Valid;
import java.io.IOException;
import java.security.GeneralSecurityException;
+import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
+import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@@ -24,19 +35,29 @@ import org.springframework.web.bind.annotation.RestController;
public class ReportGoogleController {
private final CreateReportingJobService createReportingJob;
+ private final YouTubeReportService youTubeReportService;
private final GoogleOAuthConfig googleOAuthConfig;
+ private final TokenStore tokenStore;
@GetMapping("/youtube/demo")
@Operation(summary = "Khởi tạo job demo YouTube", description = "Thực thi job báo cáo mẫu với cấu hình được lưu trữ trong hệ thống.")
- public ResponseEntity> getRevenue(GetDragRevenueRequest getDragRevenueRequest)
+ public ResponseEntity> getRevenue(@RequestBody GetDragRevenueRequest getDragRevenueRequest)
throws GeneralSecurityException, IOException {
- var tokenStore = new TokenStore();
+ String email = getDragRevenueRequest != null ? getDragRevenueRequest.getEmail() : null;
+ if (!StringUtils.hasText(email)) {
+ return ResponseEntity.ok(BaseResponse.invalid("Email không được để trống"));
+ }
+
+ if (tokenStore.getTokenResponse(email) == null) {
+ return ResponseEntity.ok(BaseResponse.invalid("Không tìm thấy token Google cho email đã cung cấp"));
+ }
+
createReportingJob.createJobWithStoredCredential(
tokenStore,
googleOAuthConfig,
GoogleNetHttpTransport.newTrustedTransport(),
JacksonFactory.getDefaultInstance(),
- "default",
+ email,
"channel_monetized_playback_a1", // report type
"vega-monetization-daily" // job name
);
@@ -48,4 +69,59 @@ public class ReportGoogleController {
.build()
);
}
+
+ @GetMapping("/report-types")
+ @Operation(summary = "Lấy danh sách Report Types", description = "Lấy tất cả các loại báo cáo có sẵn từ YouTube Reporting API.")
+ public ResponseEntity>> getReportTypes(
+ @Parameter(description = "Email của người dùng đã xác thực với Google")
+ @RequestParam String email
+ ) throws GeneralSecurityException, IOException {
+ if (!StringUtils.hasText(email)) {
+ return ResponseEntity.ok(BaseResponse.invalid("Email không được để trống"));
+ }
+
+ if (tokenStore.getTokenResponse(email) == null) {
+ return ResponseEntity.ok(BaseResponse.invalid("Không tìm thấy token Google cho email đã cung cấp"));
+ }
+
+ List reportTypes = youTubeReportService.getReportTypes(
+ email,
+ GoogleNetHttpTransport.newTrustedTransport(),
+ JacksonFactory.getDefaultInstance()
+ );
+
+ return ResponseEntity.ok(
+ BaseResponse.>builder()
+ .code("00")
+ .message("Lấy danh sách report types thành công")
+ .data(reportTypes)
+ .build()
+ );
+ }
+
+ @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(
+ @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"));
+ }
+
+ List revenueData = youTubeReportService.getRevenue(
+ request.getEmail(),
+ request.getStartDate(),
+ request.getEndDate(),
+ GoogleNetHttpTransport.newTrustedTransport(),
+ JacksonFactory.getDefaultInstance()
+ );
+
+ return ResponseEntity.ok(
+ BaseResponse.>builder()
+ .code("00")
+ .message("Lấy dữ liệu doanh thu thành công")
+ .data(revenueData)
+ .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
new file mode 100644
index 0000000..34b5aff
--- /dev/null
+++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/request/GetRevenueRequest.java
@@ -0,0 +1,19 @@
+package com.vega.hrm.report.request;
+
+import jakarta.validation.constraints.NotBlank;
+import lombok.Getter;
+import lombok.Setter;
+
+@Getter
+@Setter
+public class GetRevenueRequest {
+ @NotBlank(message = "Email không được để trống")
+ private String email;
+
+ @NotBlank(message = "Ngày bắt đầu không được để trống")
+ private String startDate; // format: YYYY-MM-DD
+
+ @NotBlank(message = "Ngày kết thúc không được để trống")
+ private String endDate; // format: YYYY-MM-DD
+}
+
diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/response/ReportTypeDto.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/response/ReportTypeDto.java
new file mode 100644
index 0000000..6463c21
--- /dev/null
+++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/response/ReportTypeDto.java
@@ -0,0 +1,16 @@
+package com.vega.hrm.report.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class ReportTypeDto {
+ private String id;
+ private String name;
+}
+
diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/response/RevenueDataDto.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/response/RevenueDataDto.java
new file mode 100644
index 0000000..047bb98
--- /dev/null
+++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/response/RevenueDataDto.java
@@ -0,0 +1,16 @@
+package com.vega.hrm.report.response;
+
+import lombok.AllArgsConstructor;
+import lombok.Builder;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class RevenueDataDto {
+ private String date;
+ private Double estimatedRevenue;
+}
+
diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/CreateReportingJobService.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/CreateReportingJobService.java
index aefd108..068a8a8 100644
--- a/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/CreateReportingJobService.java
+++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/CreateReportingJobService.java
@@ -15,11 +15,9 @@ import com.google.api.services.youtubereporting.model.ReportType;
import com.vega.hrm.core.component.TokenStore;
import com.vega.hrm.core.dto.GoogleOAuthConfig;
import com.vega.hrm.core.helpers.LogHelper;
-import java.io.BufferedReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
-import java.io.InputStreamReader;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
diff --git a/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/YouTubeReportService.java b/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/YouTubeReportService.java
new file mode 100644
index 0000000..fd63ed2
--- /dev/null
+++ b/vega-hrm-report/src/main/java/com/vega/hrm/report/serivce/YouTubeReportService.java
@@ -0,0 +1,133 @@
+package com.vega.hrm.report.serivce;
+
+import com.google.api.client.auth.oauth2.Credential;
+import com.google.api.client.googleapis.json.GoogleJsonResponseException;
+import com.google.api.client.http.HttpTransport;
+import com.google.api.client.json.JsonFactory;
+import com.google.api.services.youtubeAnalytics.v2.YouTubeAnalytics;
+import com.google.api.services.youtubeAnalytics.v2.model.QueryResponse;
+import com.google.api.services.youtubereporting.YouTubeReporting;
+import com.google.api.services.youtubereporting.model.ListReportTypesResponse;
+import com.google.api.services.youtubereporting.model.ReportType;
+import com.vega.hrm.core.component.TokenStore;
+import com.vega.hrm.core.dto.GoogleOAuthConfig;
+import com.vega.hrm.core.helpers.LogHelper;
+import com.vega.hrm.report.response.ReportTypeDto;
+import com.vega.hrm.report.response.RevenueDataDto;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+@Service
+@RequiredArgsConstructor
+public class YouTubeReportService {
+ private static final String APPLICATION_NAME = "vega-report";
+ private final TokenStore tokenStore;
+ private final GoogleOAuthConfig googleOAuthConfig;
+
+ /**
+ * Lấy danh sách tất cả report types từ YouTube Reporting API
+ */
+ public List getReportTypes(String email, HttpTransport httpTransport, JsonFactory jsonFactory)
+ throws IOException {
+ Credential credential = tokenStore.buildCredential(
+ httpTransport,
+ jsonFactory,
+ email,
+ googleOAuthConfig.clientId,
+ googleOAuthConfig.clientSecret
+ );
+
+ if (credential == null) {
+ throw new IllegalStateException("Không tìm thấy token Google cho email: " + email);
+ }
+
+ YouTubeReporting youtubeReporting = new YouTubeReporting.Builder(httpTransport, jsonFactory, credential)
+ .setApplicationName(APPLICATION_NAME)
+ .build();
+
+ try {
+ ListReportTypesResponse reportTypesListResponse = youtubeReporting.reportTypes().list().execute();
+ List reportTypeList = reportTypesListResponse.getReportTypes();
+
+ if (reportTypeList == null || reportTypeList.isEmpty()) {
+ LogHelper.info("Không tìm thấy report types nào.");
+ return new ArrayList<>();
+ }
+
+ List result = new ArrayList<>();
+ for (ReportType reportType : reportTypeList) {
+ result.add(ReportTypeDto.builder()
+ .id(reportType.getId())
+ .name(reportType.getName())
+ .build());
+ }
+
+ return result;
+ } catch (GoogleJsonResponseException e) {
+ LogHelper.error("Lỗi khi lấy report types: " + e.getDetails().getMessage());
+ throw new IOException("Không thể lấy danh sách report types: " + e.getDetails().getMessage());
+ }
+ }
+
+ /**
+ * Lấy dữ liệu doanh thu từ YouTube Analytics API
+ */
+ public List getRevenue(String email, String startDate, String endDate,
+ HttpTransport httpTransport, JsonFactory jsonFactory)
+ throws IOException {
+ Credential credential = tokenStore.buildCredential(
+ httpTransport,
+ jsonFactory,
+ email,
+ googleOAuthConfig.clientId,
+ googleOAuthConfig.clientSecret
+ );
+
+ if (credential == null) {
+ throw new IllegalStateException("Không tìm thấy token Google cho email: " + email);
+ }
+
+ YouTubeAnalytics youtubeAnalytics = new YouTubeAnalytics.Builder(httpTransport, jsonFactory, credential)
+ .setApplicationName(APPLICATION_NAME)
+ .build();
+
+ try {
+ QueryResponse queryResponse = youtubeAnalytics.reports().query()
+ .setIds("channel==MINE")
+ .setStartDate(startDate)
+ .setEndDate(endDate)
+ .setMetrics("estimatedRevenue")
+ .setDimensions("day")
+ .execute();
+
+ List result = new ArrayList<>();
+
+ if (queryResponse.getRows() != null) {
+ for (List