This commit is contained in:
nguyennt1 2025-11-25 00:07:33 +07:00
parent 969362e4cc
commit 744a2fa40b
24 changed files with 463 additions and 59 deletions

5
.cursor/worktrees.json Normal file
View File

@ -0,0 +1,5 @@
{
"setup-worktree": [
"npm install"
]
}

View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<module version="4">
<component name="AdditionalModuleElements">
<content url="file://$MODULE_DIR$/../../../vega-hrm-report/build/generated/sources/annotationProcessor/java/main">
<sourceFolder url="file://$MODULE_DIR$/../../../vega-hrm-report/build/generated/sources/annotationProcessor/java/main" isTestSource="false" generated="true" />
</content>
</component>
</module>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -22,18 +22,16 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-log4j2:3.4.0' implementation 'org.springframework.boot:spring-boot-starter-log4j2:3.4.0'
implementation 'org.springframework.boot:spring-boot-starter-validation:3.4.0' implementation 'org.springframework.boot:spring-boot-starter-validation:3.4.0'
implementation 'de.mkammerer:argon2-jvm:2.1' 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.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.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' 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.oauth-client:google-oauth-client-jetty:1.34.1'
implementation 'com.google.api-client:google-api-client:1.32.1' implementation "com.google.http-client:google-http-client-jackson2:1.43.3"
// Google Collections implementation 'com.google.guava:guava:32.1.3-jre'
implementation "com.google.collections:google-collections:1.0"
implementation 'com.google.code.gson:gson:2.11.0' implementation 'com.google.code.gson:gson:2.11.0'
annotationProcessor 'org.projectlombok:lombok:1.18.38' annotationProcessor 'org.projectlombok:lombok:1.18.38'
implementation project(":vega-hrm-core") implementation project(":vega-hrm-core")
@ -50,11 +48,14 @@ configurations {
exclude group: 'ch.qos.logback', module: 'logback-classic' exclude group: 'ch.qos.logback', module: 'logback-classic'
exclude group: 'com.google.guava', module: 'guava-jdk5' 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 { resolutionStrategy {
// bắt buộc dùng guava hiện đi // Bắt buộc dùng Guava mới nhất
force "com.google.guava:guava:32.1.3-jre" 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'
} }
} }
} }

View File

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

View File

@ -13,7 +13,7 @@ import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@RestController @RestController
@RequestMapping("api/google/user") @RequestMapping({"api/google/user", "api/auth/google"})
@RequiredArgsConstructor @RequiredArgsConstructor
@Tag(name = "Auth - Google", description = "Luồng OAuth2 với Google phục vụ xác thực người dùng") @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 { public class GoogleController {

View File

@ -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.AuthorizationCodeRequestUrl;
import com.google.api.client.auth.oauth2.BearerToken; import com.google.api.client.auth.oauth2.BearerToken;
import com.google.api.client.auth.oauth2.Credential; 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.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets; import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.auth.oauth2.GoogleTokenResponse; 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.vega.hrm.core.dto.GoogleOAuthConfig;
import com.google.api.services.oauth2.model.Userinfo; import com.google.api.services.oauth2.model.Userinfo;
import com.vega.hrm.core.repositories.UserGoogleTokenRepository; import com.vega.hrm.core.repositories.UserGoogleTokenRepository;
import com.vega.hrm.dto.CustomTokenResponse;
import java.io.IOException; import java.io.IOException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.time.Instant; import java.time.Instant;
import java.util.Objects; import java.util.Objects;
import java.util.UUID; import java.util.UUID;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.Setter;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@Service @Service
@ -100,26 +97,40 @@ public class GoogleService {
String email = userInfo.getEmail(); String email = userInfo.getEmail();
var userGoogleToken = userGoogleTokenRepository.findUserGoogleTokenByEmail(email); var userGoogleToken = userGoogleTokenRepository.findUserGoogleTokenByEmail(email);
if (userGoogleToken == null) { if (userGoogleToken == null) {
userGoogleToken = new UserGoogleToken(); userGoogleToken = new UserGoogleToken();
userGoogleToken.setId(UUID.randomUUID()); userGoogleToken.setId(UUID.randomUUID());
userGoogleToken.setEmail(email); 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()); 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); tokenStore.storeToken(email, tokenResponse);
return BaseResponse.success("00",true); return BaseResponse.success("00",true);
} }

View File

@ -44,17 +44,15 @@ dependencies {
implementation 'org.bouncycastle:bcpkix-jdk18on:1.80' implementation 'org.bouncycastle:bcpkix-jdk18on:1.80'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6' 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.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.apis:google-api-services-youtubeAnalytics:v2-rev272-1.25.0")
implementation "com.google.http-client:google-http-client-jackson2:1.20.0"
// OAuth Client // Google API Client (phiên bản mới)
implementation "com.google.oauth-client:google-oauth-client-jetty:1.20.0" implementation 'com.google.api-client:google-api-client:1.34.1'
implementation 'com.google.oauth-client:google-oauth-client-jetty:1.34.1'
// Google Collections implementation "com.google.http-client:google-http-client-jackson2:1.43.3"
implementation "com.google.collections:google-collections:1.0" implementation 'com.google.guava:guava:32.1.3-jre'
implementation 'com.google.code.gson:gson:2.11.0' implementation 'com.google.code.gson:gson:2.11.0'
// https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-orgjson // https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-orgjson
@ -68,5 +66,17 @@ configurations {
// Loại bỏ logging mặc đnh // Loại bỏ logging mặc đnh
exclude group: 'ch.qos.logback', module: 'logback-classic' exclude group: 'ch.qos.logback', module: 'logback-classic'
// Loại bỏ các thư viện Guava/Google Collections
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'
}
} }
} }

View File

@ -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.Credential;
import com.google.api.client.auth.oauth2.TokenResponse; 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.http.HttpTransport;
import com.google.api.client.json.JsonFactory; 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 java.util.concurrent.ConcurrentHashMap;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
@RequiredArgsConstructor
public class TokenStore { public class TokenStore {
private final ConcurrentHashMap<String, TokenResponse> tokenMap = new ConcurrentHashMap<>(); private final ConcurrentHashMap<String, TokenResponse> tokenMap = new ConcurrentHashMap<>();
private final UserGoogleTokenRepository userGoogleTokenRepository;
public void storeToken(String userId, TokenResponse tokenResponse) { public void storeToken(String userId, TokenResponse tokenResponse) {
tokenMap.put(userId, tokenResponse); tokenMap.put(userId, tokenResponse);
} }
public TokenResponse getTokenResponse(String userId) { 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) { 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) { if (tokenResponse == null) {
return null; return null;
} }
@ -33,4 +45,24 @@ public class TokenStore {
.build() .build()
.setFromTokenResponse(tokenResponse); .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;
}
} }

View File

@ -22,28 +22,23 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-log4j2:3.4.0' implementation 'org.springframework.boot:spring-boot-starter-log4j2:3.4.0'
implementation 'org.springframework.boot:spring-boot-starter-validation:3.4.0' implementation 'org.springframework.boot:spring-boot-starter-validation:3.4.0'
implementation 'de.mkammerer:argon2-jvm:2.1' implementation 'de.mkammerer:argon2-jvm:2.1'
implementation "com.google.apis:google-api-services-youtube:v3-rev182-1.22.0" // Google API Services
implementation("com.google.collections:google-collections:1.0") implementation 'com.google.apis:google-api-services-youtube:v3-rev182-1.22.0'
implementation("com.google.guava:guava:20.0") implementation 'com.google.apis:google-api-services-youtubeAnalytics:v2-rev272-1.25.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.http-client:google-http-client-jackson2:1.20.0"
// OAuth Client
implementation "com.google.oauth-client:google-oauth-client-jetty:1.20.0"
implementation 'com.google.apis:google-api-services-oauth2:v2-rev157-1.25.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 // Google API Client (phiên bản mới đ tương thích với YouTube libs)
implementation "com.google.collections:google-collections:1.0" 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' implementation 'com.google.code.gson:gson:2.11.0'
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'
// YouTube Reporting API
implementation "com.google.apis:google-api-services-youtubereporting:v1-rev10-1.22.0"
} }
configurations { configurations {
@ -53,5 +48,17 @@ configurations {
// Loại bỏ logging mặc đnh // Loại bỏ logging mặc đnh
exclude group: 'ch.qos.logback', module: 'logback-classic' exclude group: 'ch.qos.logback', module: 'logback-classic'
// Loại bỏ các thư viện Guava/Google Collections
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'
}
} }
} }

View File

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

View File

@ -6,15 +6,26 @@ 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.report.request.GetDragRevenueRequest; 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.CreateReportingJobService;
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.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.validation.Valid;
import java.io.IOException; import java.io.IOException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.GetMapping; 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.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@RestController @RestController
@ -24,19 +35,29 @@ import org.springframework.web.bind.annotation.RestController;
public class ReportGoogleController { public class ReportGoogleController {
private final CreateReportingJobService createReportingJob; private final CreateReportingJobService createReportingJob;
private final YouTubeReportService youTubeReportService;
private final GoogleOAuthConfig googleOAuthConfig; private final GoogleOAuthConfig googleOAuthConfig;
private final TokenStore tokenStore;
@GetMapping("/youtube/demo") @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.") @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<BaseResponse<Boolean>> getRevenue(GetDragRevenueRequest getDragRevenueRequest) public ResponseEntity<BaseResponse<Boolean>> getRevenue(@RequestBody GetDragRevenueRequest getDragRevenueRequest)
throws GeneralSecurityException, IOException { 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( createReportingJob.createJobWithStoredCredential(
tokenStore, tokenStore,
googleOAuthConfig, googleOAuthConfig,
GoogleNetHttpTransport.newTrustedTransport(), GoogleNetHttpTransport.newTrustedTransport(),
JacksonFactory.getDefaultInstance(), JacksonFactory.getDefaultInstance(),
"default", email,
"channel_monetized_playback_a1", // report type "channel_monetized_playback_a1", // report type
"vega-monetization-daily" // job name "vega-monetization-daily" // job name
); );
@ -48,4 +69,59 @@ public class ReportGoogleController {
.build() .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<BaseResponse<List<ReportTypeDto>>> 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<ReportTypeDto> reportTypes = youTubeReportService.getReportTypes(
email,
GoogleNetHttpTransport.newTrustedTransport(),
JacksonFactory.getDefaultInstance()
);
return ResponseEntity.ok(
BaseResponse.<List<ReportTypeDto>>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<BaseResponse<List<RevenueDataDto>>> 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<RevenueDataDto> revenueData = youTubeReportService.getRevenue(
request.getEmail(),
request.getStartDate(),
request.getEndDate(),
GoogleNetHttpTransport.newTrustedTransport(),
JacksonFactory.getDefaultInstance()
);
return ResponseEntity.ok(
BaseResponse.<List<RevenueDataDto>>builder()
.code("00")
.message("Lấy dữ liệu doanh thu thành công")
.data(revenueData)
.build()
);
}
} }

View File

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

View File

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

View File

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

View File

@ -15,11 +15,9 @@ import com.google.api.services.youtubereporting.model.ReportType;
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.helpers.LogHelper; import com.vega.hrm.core.helpers.LogHelper;
import java.io.BufferedReader;
import java.io.File; import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException; import java.io.IOException;
import java.io.InputStreamReader;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;

View File

@ -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<ReportTypeDto> 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<ReportType> reportTypeList = reportTypesListResponse.getReportTypes();
if (reportTypeList == null || reportTypeList.isEmpty()) {
LogHelper.info("Không tìm thấy report types nào.");
return new ArrayList<>();
}
List<ReportTypeDto> 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<RevenueDataDto> 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<RevenueDataDto> result = new ArrayList<>();
if (queryResponse.getRows() != null) {
for (List<Object> 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());
}
}
}