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 row : queryResponse.getRows()) { + if (row.size() >= 2) { + String date = row.get(0).toString(); + Double revenue = row.get(1) instanceof Number + ? ((Number) row.get(1)).doubleValue() + : Double.parseDouble(row.get(1).toString()); + + result.add(RevenueDataDto.builder() + .date(date) + .estimatedRevenue(revenue) + .build()); + } + } + } + + LogHelper.info("Đã lấy được " + result.size() + " dòng dữ liệu doanh thu."); + return result; + + } catch (GoogleJsonResponseException e) { + LogHelper.error("Lỗi khi lấy dữ liệu revenue: " + e.getDetails().getMessage()); + throw new IOException("Không thể lấy dữ liệu doanh thu: " + e.getDetails().getMessage()); + } + } +} +