Jelajahi Sumber

增加快手和腾讯的授权配置

yumeng 10 jam lalu
induk
melakukan
5d4a7c3a41

+ 67 - 0
src/main/java/com/nuojing/media/auth/client/KuaishouMediaClient.java

@@ -0,0 +1,67 @@
+package com.nuojing.media.auth.client;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nuojing.admin.common.BusinessException;
+import com.nuojing.media.auth.domain.entity.MediaAuthConfig;
+import lombok.RequiredArgsConstructor;
+import org.springframework.http.HttpEntity;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.MediaType;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestTemplate;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@Component
+@RequiredArgsConstructor
+public class KuaishouMediaClient {
+
+    private static final String TOKEN_URL = "https://open.e.kuaishou.com/rest/openapi/oauth2/authorize/access_token";
+
+    private final ObjectMapper objectMapper;
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    public JsonNode exchangeToken(MediaAuthConfig config, String authCode) {
+        Map<String, Object> body = new HashMap<>();
+        body.put("app_id", config.getAppId());
+        body.put("app_secret", config.getAppSecret());
+        body.put("auth_code", authCode);
+        return postJson(TOKEN_URL, body);
+    }
+
+    public JsonNode requireSuccess(JsonNode response, String action) {
+        if (response == null || response.isMissingNode() || response.isNull()) {
+            throw new BusinessException(500, action + "返回信息为空");
+        }
+        int code = response.path("code").asInt(-1);
+        if (code != 0) {
+            String message = response.path("message").asText(action + "失败");
+            throw new BusinessException(500, message);
+        }
+        JsonNode data = response.path("data");
+        if (data.isMissingNode() || data.isNull()) {
+            throw new BusinessException(500, action + "data信息为空");
+        }
+        return data;
+    }
+
+    private JsonNode postJson(String url, Map<String, Object> body) {
+        HttpHeaders headers = new HttpHeaders();
+        headers.setContentType(MediaType.APPLICATION_JSON);
+        return parse(restTemplate.postForObject(url, new HttpEntity<>(body, headers), String.class));
+    }
+
+    private JsonNode parse(String response) {
+        if (!StringUtils.hasText(response)) {
+            return objectMapper.missingNode();
+        }
+        try {
+            return objectMapper.readTree(response);
+        } catch (Exception ex) {
+            throw new BusinessException(500, "媒体响应解析失败");
+        }
+    }
+}

+ 86 - 0
src/main/java/com/nuojing/media/auth/client/TencentMediaClient.java

@@ -0,0 +1,86 @@
+package com.nuojing.media.auth.client;
+
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.nuojing.admin.common.BusinessException;
+import com.nuojing.media.auth.domain.entity.MediaAuthConfig;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+import org.springframework.web.client.RestTemplate;
+import org.springframework.web.util.UriComponentsBuilder;
+
+import java.util.UUID;
+
+@Component
+@RequiredArgsConstructor
+public class TencentMediaClient {
+
+    private static final String TOKEN_URL = "https://api.e.qq.com/oauth/token";
+    private static final String BM_RELATIONS_URL = "https://api.e.qq.com/v1.1/business_manager_relations/get";
+    private static final int PAGE_SIZE = 100;
+
+    private final ObjectMapper objectMapper;
+    private final RestTemplate restTemplate = new RestTemplate();
+
+    public JsonNode exchangeToken(MediaAuthConfig config, String authorizationCode) {
+        UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(TOKEN_URL)
+                .queryParam("client_id", config.getAppId())
+                .queryParam("client_secret", tencentSecret(config))
+                .queryParam("grant_type", "authorization_code")
+                .queryParam("authorization_code", authorizationCode);
+        if (StringUtils.hasText(config.getRedirectUri())) {
+            builder.queryParam("redirect_uri", config.getRedirectUri());
+        }
+        String url = builder.build().encode().toUriString();
+        return parse(restTemplate.getForObject(url, String.class));
+    }
+
+    public JsonNode businessManagerRelations(String accessToken, int page) {
+        String url = UriComponentsBuilder.fromUriString(BM_RELATIONS_URL)
+                .queryParam("access_token", accessToken)
+                .queryParam("timestamp", System.currentTimeMillis() / 1000)
+                .queryParam("nonce", UUID.randomUUID().toString().replace("-", ""))
+                .queryParam("page", page)
+                .queryParam("page_size", PAGE_SIZE)
+                .build()
+                .encode()
+                .toUriString();
+        return parse(restTemplate.getForObject(url, String.class));
+    }
+
+    public JsonNode requireSuccess(JsonNode response, String action) {
+        if (response == null || response.isMissingNode() || response.isNull()) {
+            throw new BusinessException(500, action + "返回信息为空");
+        }
+        int code = response.path("code").asInt(-1);
+        if (code != 0) {
+            String message = response.path("message").asText(action + "失败");
+            throw new BusinessException(500, message);
+        }
+        JsonNode data = response.path("data");
+        if (data.isMissingNode() || data.isNull()) {
+            throw new BusinessException(500, action + "data信息为空");
+        }
+        return data;
+    }
+
+    private String tencentSecret(MediaAuthConfig config) {
+        // 腾讯应用密钥放在 client_secret 字段,兼容误填到 app_secret 的情况
+        if (StringUtils.hasText(config.getClientSecret())) {
+            return config.getClientSecret();
+        }
+        return config.getAppSecret();
+    }
+
+    private JsonNode parse(String response) {
+        if (!StringUtils.hasText(response)) {
+            return objectMapper.missingNode();
+        }
+        try {
+            return objectMapper.readTree(response);
+        } catch (Exception ex) {
+            throw new BusinessException(500, "媒体响应解析失败");
+        }
+    }
+}

+ 153 - 8
src/main/java/com/nuojing/media/auth/service/impl/MediaAuthServiceImpl.java

@@ -6,6 +6,8 @@ import com.nuojing.admin.common.BusinessException;
 import com.nuojing.admin.context.CurrentUser;
 import com.nuojing.admin.context.CurrentUserContext;
 import com.nuojing.media.auth.client.BytedanceMediaClient;
+import com.nuojing.media.auth.client.KuaishouMediaClient;
+import com.nuojing.media.auth.client.TencentMediaClient;
 import com.nuojing.media.auth.domain.dto.AuthUrlRequest;
 import com.nuojing.media.auth.domain.entity.MediaAdvertiserAccount;
 import com.nuojing.media.auth.domain.entity.MediaAuthAccount;
@@ -33,6 +35,8 @@ public class MediaAuthServiceImpl implements MediaAuthService {
 
     private final MediaAuthMapper mediaAuthMapper;
     private final BytedanceMediaClient bytedanceMediaClient;
+    private final KuaishouMediaClient kuaishouMediaClient;
+    private final TencentMediaClient tencentMediaClient;
     private final ObjectMapper objectMapper;
 
     @Override
@@ -46,7 +50,8 @@ public class MediaAuthServiceImpl implements MediaAuthService {
             vo.setAppId(config.getAppId());
             vo.setRedirectUri(config.getRedirectUri());
             vo.setCallbackReturnUrl(config.getCallbackReturnUrl());
-            vo.setConfigured(StringUtils.hasText(config.getAuthUrl()) && StringUtils.hasText(config.getAppSecret()));
+            vo.setConfigured(StringUtils.hasText(config.getAuthUrl())
+                    && (StringUtils.hasText(config.getAppSecret()) || StringUtils.hasText(config.getClientSecret())));
             return vo;
         }).toList();
     }
@@ -57,9 +62,6 @@ public class MediaAuthServiceImpl implements MediaAuthService {
             throw new BusinessException(400, "请输入媒体类型");
         }
         MediaType media = MediaType.ofMediaId(request.getMediaId());
-        if (media != MediaType.BYTEDANCE) {
-            throw new BusinessException(400, media.getName() + "授权暂未接入");
-        }
         MediaAuthConfig config = findConfig(media);
         if (config == null || !StringUtils.hasText(config.getAuthUrl())) {
             throw new BusinessException(400, media.getName() + "授权配置未完成");
@@ -89,11 +91,8 @@ public class MediaAuthServiceImpl implements MediaAuthService {
         result.setTime(System.currentTimeMillis());
         result.setCallbackReturnUrl(callbackReturnUrl(media));
         try {
-            if (media != MediaType.BYTEDANCE) {
-                throw new BusinessException(400, media.getName() + "回调暂未接入");
-            }
             validateCallbackParams(media, params);
-            Long adminAdvertiserId = handleBytedanceCallback(params);
+            Long adminAdvertiserId = handleCallback(media, params);
             result.setCode(200);
             result.setAdminAdvertiserId(adminAdvertiserId);
             result.setMessage(media.getName() + "授权回调已接收");
@@ -220,6 +219,152 @@ public class MediaAuthServiceImpl implements MediaAuthService {
         return adminAdvertiserId;
     }
 
+    private Long handleCallback(MediaType media, Map<String, String> params) {
+        if (media == MediaType.BYTEDANCE) {
+            return handleBytedanceCallback(params);
+        }
+        if (media == MediaType.KUAISHOU) {
+            return handleKuaishouCallback(params);
+        }
+        if (media == MediaType.TENCENT) {
+            return handleTencentCallback(params);
+        }
+        throw new BusinessException(400, media.getName() + "回调暂未接入");
+    }
+
+    private Long handleKuaishouCallback(Map<String, String> params) {
+        MediaAuthConfig config = requireConfig(MediaType.KUAISHOU);
+        JsonNode tokenResult = kuaishouMediaClient.exchangeToken(config, params.get("auth_code"));
+        JsonNode tokenData = kuaishouMediaClient.requireSuccess(tokenResult, "获取快手token");
+        String accessToken = tokenData.path("access_token").asText();
+        String refreshToken = tokenData.path("refresh_token").asText(null);
+        if (!StringUtils.hasText(accessToken)) {
+            throw new BusinessException(500, "获取快手token失败:access_token为空");
+        }
+
+        JsonNode advertiserIds = tokenData.path("advertiser_ids");
+        if (!advertiserIds.isArray() || advertiserIds.isEmpty()) {
+            throw new BusinessException(500, "未获取到快手授权广告账户:advertiser_ids为空");
+        }
+
+        Long userId = parseStateUserId(params.get("state"));
+        // 磁力引擎token响应未单独返回管家账户ID,优先取响应内的账户字段,否则以首个广告账户作为管家标识
+        Long adminAdvertiserId = resolveKuaishouAdminId(tokenData, advertiserIds);
+        MediaAuthAccount authAccount = baseAuthAccount(config, MediaType.KUAISHOU, userId, adminAdvertiserId, accessToken, refreshToken);
+        authAccount.setRawResponse(toJson(tokenData));
+        mediaAuthMapper.replaceAuthAccount(authAccount);
+
+        for (JsonNode advertiserId : advertiserIds) {
+            mediaAuthMapper.replaceAdvertiserAccount(toKuaishouAdvertiserAccount(adminAdvertiserId, advertiserId.asLong()));
+        }
+        return adminAdvertiserId;
+    }
+
+    private Long handleTencentCallback(Map<String, String> params) {
+        MediaAuthConfig config = requireConfig(MediaType.TENCENT);
+        JsonNode tokenResult = tencentMediaClient.exchangeToken(config, params.get("authorization_code"));
+        JsonNode tokenData = tencentMediaClient.requireSuccess(tokenResult, "获取腾讯token");
+        String accessToken = tokenData.path("access_token").asText();
+        String refreshToken = tokenData.path("refresh_token").asText(null);
+        if (!StringUtils.hasText(accessToken)) {
+            throw new BusinessException(500, "获取腾讯token失败:access_token为空");
+        }
+
+        JsonNode authorizer = tokenData.path("authorizer_info");
+        long accountId = authorizer.path("account_id").asLong(0);
+        if (accountId <= 0) {
+            throw new BusinessException(500, "未获取到腾讯授权账号ID");
+        }
+        Long adminAdvertiserId = accountId;
+
+        Long userId = parseStateUserId(params.get("state"));
+        MediaAuthAccount authAccount = baseAuthAccount(config, MediaType.TENCENT, userId, adminAdvertiserId, accessToken, refreshToken);
+        authAccount.setAccountRole(authorizer.path("account_role_type").asText(null));
+        authAccount.setRawResponse(toJson(authorizer));
+        mediaAuthMapper.replaceAuthAccount(authAccount);
+
+        syncTencentAdvertisers(accessToken, adminAdvertiserId);
+        return adminAdvertiserId;
+    }
+
+    private void syncTencentAdvertisers(String accessToken, Long adminAdvertiserId) {
+        int page = 1;
+        int totalPage = 1;
+        do {
+            JsonNode relations;
+            try {
+                relations = tencentMediaClient.requireSuccess(
+                        tencentMediaClient.businessManagerRelations(accessToken, page), "获取腾讯商务管家广告主");
+            } catch (Exception ex) {
+                log.warn("获取腾讯商务管家广告主列表失败,仅保留管家授权信息:adminAdvertiserId={}", adminAdvertiserId, ex);
+                return;
+            }
+            JsonNode list = relations.path("list");
+            if (list.isArray()) {
+                for (JsonNode item : list) {
+                    long advertiserId = item.path("account_id").asLong(0);
+                    if (advertiserId > 0) {
+                        mediaAuthMapper.replaceAdvertiserAccount(toTencentAdvertiserAccount(adminAdvertiserId, item));
+                    }
+                }
+            }
+            totalPage = Math.max(relations.path("page_info").path("total_page").asInt(page), page);
+            page++;
+        } while (page <= totalPage);
+    }
+
+    private MediaAuthConfig requireConfig(MediaType media) {
+        MediaAuthConfig config = findConfig(media);
+        if (config == null) {
+            throw new BusinessException(400, media.getName() + "授权配置不存在");
+        }
+        return config;
+    }
+
+    private Long resolveKuaishouAdminId(JsonNode tokenData, JsonNode advertiserIds) {
+        for (String field : new String[]{"user_id", "account_id", "corporation_id"}) {
+            long value = tokenData.path(field).asLong(0);
+            if (value > 0) {
+                return value;
+            }
+        }
+        return advertiserIds.get(0).asLong();
+    }
+
+    private MediaAuthAccount baseAuthAccount(MediaAuthConfig config, MediaType media, Long userId, Long adminAdvertiserId, String accessToken, String refreshToken) {
+        MediaAuthAccount account = new MediaAuthAccount();
+        account.setMediaType(media.getMediaId());
+        account.setAppId(config.getAppId());
+        account.setUserId(userId);
+        account.setAdminAdvertiserId(adminAdvertiserId);
+        account.setIsValid(1);
+        account.setAccessToken(accessToken);
+        account.setRefreshToken(refreshToken);
+        return account;
+    }
+
+    private MediaAdvertiserAccount toKuaishouAdvertiserAccount(Long adminAdvertiserId, Long advertiserId) {
+        MediaAdvertiserAccount account = new MediaAdvertiserAccount();
+        account.setMediaType(MediaType.KUAISHOU.getMediaId());
+        account.setAdminAdvertiserId(adminAdvertiserId);
+        account.setAdvertiserId(advertiserId);
+        account.setIsValid(1);
+        account.setRawResponse("{\"advertiser_id\":" + advertiserId + "}");
+        return account;
+    }
+
+    private MediaAdvertiserAccount toTencentAdvertiserAccount(Long adminAdvertiserId, JsonNode item) {
+        MediaAdvertiserAccount account = new MediaAdvertiserAccount();
+        account.setMediaType(MediaType.TENCENT.getMediaId());
+        account.setAdminAdvertiserId(adminAdvertiserId);
+        account.setAdvertiserId(item.path("account_id").asLong());
+        account.setAdvertiserName(item.path("corporation_name").asText(null));
+        account.setAccountRole(item.path("account_type").asText(null));
+        account.setIsValid(1);
+        account.setRawResponse(toJson(item));
+        return account;
+    }
+
     private MediaAuthAccount toAuthAccount(MediaAuthConfig config, JsonNode advertiser, String accessToken, String refreshToken, Long userId) {
         MediaAuthAccount account = new MediaAuthAccount();
         account.setMediaType(MediaType.BYTEDANCE.getMediaId());