MediaAuthServiceImpl.java 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481
  1. package com.nuojing.media.auth.service.impl;
  2. import com.fasterxml.jackson.databind.JsonNode;
  3. import com.fasterxml.jackson.databind.ObjectMapper;
  4. import com.nuojing.admin.common.BusinessException;
  5. import com.nuojing.admin.context.CurrentUser;
  6. import com.nuojing.admin.context.CurrentUserContext;
  7. import com.nuojing.media.auth.client.BytedanceMediaClient;
  8. import com.nuojing.media.auth.client.KuaishouMediaClient;
  9. import com.nuojing.media.auth.client.TencentMediaClient;
  10. import com.nuojing.media.auth.domain.dto.AuthUrlRequest;
  11. import com.nuojing.media.auth.domain.entity.MediaAdvertiserAccount;
  12. import com.nuojing.media.auth.domain.entity.MediaAuthAccount;
  13. import com.nuojing.media.auth.domain.entity.MediaAuthConfig;
  14. import com.nuojing.media.auth.domain.vo.AuthUrlVO;
  15. import com.nuojing.media.auth.domain.vo.MediaAuthConfigVO;
  16. import com.nuojing.media.auth.mapper.MediaAuthMapper;
  17. import com.nuojing.media.auth.service.MediaAuthService;
  18. import com.nuojing.media.callback.domain.vo.CallbackResultVO;
  19. import com.nuojing.media.common.enums.MediaType;
  20. import lombok.RequiredArgsConstructor;
  21. import lombok.extern.slf4j.Slf4j;
  22. import org.springframework.stereotype.Service;
  23. import org.springframework.transaction.annotation.Transactional;
  24. import org.springframework.util.StringUtils;
  25. import org.springframework.web.util.UriComponentsBuilder;
  26. import java.util.ArrayList;
  27. import java.util.List;
  28. import java.util.Map;
  29. @Slf4j
  30. @Service
  31. @RequiredArgsConstructor
  32. public class MediaAuthServiceImpl implements MediaAuthService {
  33. private final MediaAuthMapper mediaAuthMapper;
  34. private final BytedanceMediaClient bytedanceMediaClient;
  35. private final KuaishouMediaClient kuaishouMediaClient;
  36. private final TencentMediaClient tencentMediaClient;
  37. private final ObjectMapper objectMapper;
  38. @Override
  39. public List<MediaAuthConfigVO> configs() {
  40. return mediaAuthMapper.selectActiveConfigs(null).stream().map(config -> {
  41. MediaType media = MediaType.ofMediaId(config.getMediaType());
  42. MediaAuthConfigVO vo = new MediaAuthConfigVO();
  43. vo.setMediaId(media.getMediaId());
  44. vo.setMediaCode(media.getCode());
  45. vo.setMediaName(media.getName());
  46. vo.setAppId(config.getAppId());
  47. vo.setRedirectUri(config.getRedirectUri());
  48. vo.setCallbackReturnUrl(config.getCallbackReturnUrl());
  49. vo.setConfigured(StringUtils.hasText(config.getAuthUrl())
  50. && (StringUtils.hasText(config.getAppSecret()) || StringUtils.hasText(config.getClientSecret())));
  51. return vo;
  52. }).toList();
  53. }
  54. @Override
  55. public AuthUrlVO getAuthUrl(AuthUrlRequest request) {
  56. if (request.getMediaId() == null) {
  57. throw new BusinessException(400, "请输入媒体类型");
  58. }
  59. MediaType media = MediaType.ofMediaId(request.getMediaId());
  60. MediaAuthConfig config = findConfig(media);
  61. if (config == null || !StringUtils.hasText(config.getAuthUrl())) {
  62. throw new BusinessException(400, media.getName() + "授权配置未完成");
  63. }
  64. Long userId = currentUserId();
  65. String state = buildState(userId, config.getAppId());
  66. AuthUrlVO vo = new AuthUrlVO();
  67. vo.setMediaId(media.getMediaId());
  68. vo.setMediaCode(media.getCode());
  69. vo.setMediaName(media.getName());
  70. vo.setAppId(config.getAppId());
  71. vo.setUserId(userId);
  72. vo.setState(state);
  73. vo.setAuthUrl(appendState(config.getAuthUrl(), state));
  74. return vo;
  75. }
  76. @Override
  77. @Transactional(rollbackFor = Exception.class)
  78. public CallbackResultVO handleCallback(Integer mediaId, Map<String, String> params) {
  79. MediaType media = MediaType.ofMediaId(mediaId);
  80. CallbackResultVO result = new CallbackResultVO();
  81. result.setMediaType(media.getMediaId());
  82. result.setBindStatus(2);
  83. result.setState(params.get("state"));
  84. result.setTime(System.currentTimeMillis());
  85. result.setCallbackReturnUrl(callbackReturnUrl(media));
  86. try {
  87. validateCallbackParams(media, params);
  88. Long adminAdvertiserId = handleCallback(media, params);
  89. result.setCode(200);
  90. result.setAdminAdvertiserId(adminAdvertiserId);
  91. result.setMessage(media.getName() + "授权回调已接收");
  92. log.info("{} authorization callback received, state={}, params={}", media.getName(), params.get("state"), params);
  93. } catch (Exception ex) {
  94. result.setCode(500);
  95. result.setMessage(ex.getMessage());
  96. log.error("{} authorization callback failed, params={}", media.getName(), params, ex);
  97. }
  98. return result;
  99. }
  100. @Override
  101. public String buildReturnUrl(CallbackResultVO result) {
  102. String callbackReturnUrl = result.getCallbackReturnUrl();
  103. if (!StringUtils.hasText(callbackReturnUrl)) {
  104. callbackReturnUrl = callbackReturnUrl(MediaType.ofMediaId(result.getMediaType()));
  105. }
  106. UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(callbackReturnUrl)
  107. .queryParam("code", result.getCode())
  108. .queryParam("mediaType", result.getMediaType())
  109. .queryParam("bindStatus", result.getBindStatus())
  110. .queryParam("time", result.getTime());
  111. if (result.getAdminAdvertiserId() != null) {
  112. builder.queryParam("adminAdvertiserId", result.getAdminAdvertiserId());
  113. }
  114. if (StringUtils.hasText(result.getState())) {
  115. builder.queryParam("state", result.getState());
  116. }
  117. if (StringUtils.hasText(result.getMessage())) {
  118. builder.queryParam(result.getCode() == 200 ? "message" : "msg", result.getMessage());
  119. }
  120. return builder.build().encode().toUriString();
  121. }
  122. private MediaAuthConfig findConfig(MediaType media) {
  123. return mediaAuthMapper.selectActiveConfigs(media.getMediaId()).stream().findFirst().orElse(null);
  124. }
  125. private String callbackReturnUrl(MediaType media) {
  126. return mediaAuthMapper.selectActiveConfigs(media.getMediaId()).stream()
  127. .map(MediaAuthConfig::getCallbackReturnUrl)
  128. .filter(StringUtils::hasText)
  129. .findFirst()
  130. .orElseThrow(() -> new BusinessException(400, media.getName() + "前端回跳地址未配置"));
  131. }
  132. private Long currentUserId() {
  133. CurrentUser currentUser = CurrentUserContext.get();
  134. return currentUser == null ? null : currentUser.getUserId();
  135. }
  136. private String buildState(Long userId, Long appId) {
  137. return safe(userId) + "_" + safe(appId);
  138. }
  139. private String safe(Long value) {
  140. return value == null ? "" : String.valueOf(value);
  141. }
  142. private String appendState(String authUrl, String state) {
  143. UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(authUrl);
  144. builder.replaceQueryParam("state", state);
  145. return builder.build().encode().toUriString();
  146. }
  147. private void validateCallbackParams(MediaType media, Map<String, String> params) {
  148. if (!StringUtils.hasText(params.get("state"))) {
  149. throw new BusinessException(400, "state不能为空");
  150. }
  151. if (media == MediaType.BYTEDANCE && !StringUtils.hasText(params.get("app_id"))) {
  152. throw new BusinessException(400, "app_id不能为空");
  153. }
  154. if (media == MediaType.BYTEDANCE && !StringUtils.hasText(params.get("auth_code"))) {
  155. throw new BusinessException(400, "auth_code不能为空");
  156. }
  157. if (media == MediaType.KUAISHOU && !StringUtils.hasText(params.get("auth_code"))) {
  158. throw new BusinessException(400, "auth_code不能为空");
  159. }
  160. if (media == MediaType.TENCENT && !StringUtils.hasText(params.get("authorization_code"))) {
  161. throw new BusinessException(400, "authorization_code不能为空");
  162. }
  163. }
  164. private Long handleBytedanceCallback(Map<String, String> params) {
  165. Long appId = parseLong(params.get("app_id"), "app_id");
  166. MediaAuthConfig config = mediaAuthMapper.selectActiveConfigByAppId(MediaType.BYTEDANCE.getMediaId(), appId);
  167. if (config == null) {
  168. throw new BusinessException(400, "通过此appId未获取到相关配置信息,请联系技术");
  169. }
  170. JsonNode tokenResult = bytedanceMediaClient.exchangeToken(config, params.get("auth_code"));
  171. JsonNode tokenData = bytedanceMediaClient.requireSuccess(tokenResult, "获取字节token");
  172. String accessToken = tokenData.path("access_token").asText();
  173. String refreshToken = tokenData.path("refresh_token").asText();
  174. if (!StringUtils.hasText(accessToken)) {
  175. throw new BusinessException(500, "获取字节token失败:access_token为空");
  176. }
  177. JsonNode advertiserResult = bytedanceMediaClient.advertiserList(accessToken);
  178. JsonNode advertiserData = bytedanceMediaClient.requireSuccess(advertiserResult, "获取字节授权账户");
  179. JsonNode advertisers = advertiserData.path("list");
  180. if (!advertisers.isArray()) {
  181. throw new BusinessException(500, "获取字节授权账户list信息为空");
  182. }
  183. Long userId = parseStateUserId(params.get("state"));
  184. JsonNode adminAdvertiser = null;
  185. List<JsonNode> customerAccounts = new ArrayList<>();
  186. for (JsonNode advertiser : advertisers) {
  187. String accountRole = advertiser.path("account_role").asText();
  188. if ("CUSTOMER_ADMIN".equals(accountRole)) {
  189. adminAdvertiser = advertiser;
  190. }
  191. if ("CUSTOMER_ADMIN".equals(accountRole) || "CUSTOMER_OPERATOR".equals(accountRole)) {
  192. customerAccounts.add(advertiser);
  193. }
  194. }
  195. if (adminAdvertiser == null) {
  196. throw new BusinessException(500, "未获取到字节管家账户");
  197. }
  198. Long adminAdvertiserId = adminAdvertiser.path("advertiser_id").asLong();
  199. MediaAuthAccount authAccount = toAuthAccount(config, adminAdvertiser, accessToken, refreshToken, userId);
  200. applyUserInfo(authAccount, accessToken);
  201. mediaAuthMapper.replaceAuthAccount(authAccount);
  202. // /oauth2/advertiser/get 返回的是工作台(纵横组织)账号,需进一步展开组织下的真实投放账户
  203. syncBytedanceDeliveryAccounts(accessToken, customerAccounts, adminAdvertiserId);
  204. return adminAdvertiserId;
  205. }
  206. private void syncBytedanceDeliveryAccounts(String accessToken, List<JsonNode> customerAccounts, Long adminAdvertiserId) {
  207. for (JsonNode org : customerAccounts) {
  208. long ccAccountId = org.path("advertiser_id").asLong();
  209. try {
  210. int page = 1;
  211. int totalPage = 1;
  212. do {
  213. JsonNode data = bytedanceMediaClient.requireSuccess(
  214. bytedanceMediaClient.ccAdvertiserList(accessToken, ccAccountId, page),
  215. "获取字节纵横组织投放账户");
  216. JsonNode list = data.path("list");
  217. if (list.isArray()) {
  218. for (JsonNode item : list) {
  219. long advertiserId = item.path("advertiser_id").asLong(0);
  220. if (advertiserId > 0) {
  221. mediaAuthMapper.replaceAdvertiserAccount(toDeliveryAccount(MediaType.BYTEDANCE, adminAdvertiserId, item));
  222. }
  223. }
  224. }
  225. totalPage = Math.max(data.path("page_info").path("total_page").asInt(page), page);
  226. page++;
  227. } while (page <= totalPage);
  228. } catch (Exception ex) {
  229. log.warn("获取字节纵横组织投放账户失败,跳过该组织:ccAccountId={}", ccAccountId, ex);
  230. }
  231. }
  232. }
  233. private MediaAdvertiserAccount toDeliveryAccount(MediaType media, Long adminAdvertiserId, JsonNode item) {
  234. MediaAdvertiserAccount account = new MediaAdvertiserAccount();
  235. account.setMediaType(media.getMediaId());
  236. account.setAdminAdvertiserId(adminAdvertiserId);
  237. account.setAdvertiserId(item.path("advertiser_id").asLong());
  238. account.setAdvertiserName(item.path("advertiser_name").asText(null));
  239. account.setAccountRole(item.path("advertiser_type").asText(null));
  240. account.setIsValid(1);
  241. account.setRawResponse(toJson(item));
  242. return account;
  243. }
  244. private Long handleCallback(MediaType media, Map<String, String> params) {
  245. if (media == MediaType.BYTEDANCE) {
  246. return handleBytedanceCallback(params);
  247. }
  248. if (media == MediaType.KUAISHOU) {
  249. return handleKuaishouCallback(params);
  250. }
  251. if (media == MediaType.TENCENT) {
  252. return handleTencentCallback(params);
  253. }
  254. throw new BusinessException(400, media.getName() + "回调暂未接入");
  255. }
  256. private Long handleKuaishouCallback(Map<String, String> params) {
  257. MediaAuthConfig config = requireConfig(MediaType.KUAISHOU);
  258. JsonNode tokenResult = kuaishouMediaClient.exchangeToken(config, params.get("auth_code"));
  259. JsonNode tokenData = kuaishouMediaClient.requireSuccess(tokenResult, "获取快手token");
  260. String accessToken = tokenData.path("access_token").asText();
  261. String refreshToken = tokenData.path("refresh_token").asText(null);
  262. if (!StringUtils.hasText(accessToken)) {
  263. throw new BusinessException(500, "获取快手token失败:access_token为空");
  264. }
  265. JsonNode advertiserIds = tokenData.path("advertiser_ids");
  266. if (!advertiserIds.isArray() || advertiserIds.isEmpty()) {
  267. throw new BusinessException(500, "未获取到快手授权广告账户:advertiser_ids为空");
  268. }
  269. Long userId = parseStateUserId(params.get("state"));
  270. // 磁力引擎token响应未单独返回管家账户ID,优先取响应内的账户字段,否则以首个广告账户作为管家标识
  271. Long adminAdvertiserId = resolveKuaishouAdminId(tokenData, advertiserIds);
  272. MediaAuthAccount authAccount = baseAuthAccount(config, MediaType.KUAISHOU, userId, adminAdvertiserId, accessToken, refreshToken);
  273. authAccount.setRawResponse(toJson(tokenData));
  274. mediaAuthMapper.replaceAuthAccount(authAccount);
  275. for (JsonNode advertiserId : advertiserIds) {
  276. mediaAuthMapper.replaceAdvertiserAccount(toKuaishouAdvertiserAccount(adminAdvertiserId, advertiserId.asLong()));
  277. }
  278. return adminAdvertiserId;
  279. }
  280. private Long handleTencentCallback(Map<String, String> params) {
  281. MediaAuthConfig config = requireConfig(MediaType.TENCENT);
  282. JsonNode tokenResult = tencentMediaClient.exchangeToken(config, params.get("authorization_code"));
  283. JsonNode tokenData = tencentMediaClient.requireSuccess(tokenResult, "获取腾讯token");
  284. String accessToken = tokenData.path("access_token").asText();
  285. String refreshToken = tokenData.path("refresh_token").asText(null);
  286. if (!StringUtils.hasText(accessToken)) {
  287. throw new BusinessException(500, "获取腾讯token失败:access_token为空");
  288. }
  289. JsonNode authorizer = tokenData.path("authorizer_info");
  290. long accountId = authorizer.path("account_id").asLong(0);
  291. if (accountId <= 0) {
  292. throw new BusinessException(500, "未获取到腾讯授权账号ID");
  293. }
  294. Long adminAdvertiserId = accountId;
  295. Long userId = parseStateUserId(params.get("state"));
  296. MediaAuthAccount authAccount = baseAuthAccount(config, MediaType.TENCENT, userId, adminAdvertiserId, accessToken, refreshToken);
  297. authAccount.setAccountRole(authorizer.path("account_role_type").asText(null));
  298. authAccount.setRawResponse(toJson(authorizer));
  299. mediaAuthMapper.replaceAuthAccount(authAccount);
  300. syncTencentAdvertisers(accessToken, adminAdvertiserId);
  301. return adminAdvertiserId;
  302. }
  303. private void syncTencentAdvertisers(String accessToken, Long adminAdvertiserId) {
  304. int page = 1;
  305. int totalPage = 1;
  306. do {
  307. JsonNode relations;
  308. try {
  309. relations = tencentMediaClient.requireSuccess(
  310. tencentMediaClient.businessManagerRelations(accessToken, page), "获取腾讯商务管家广告主");
  311. } catch (Exception ex) {
  312. log.warn("获取腾讯商务管家广告主列表失败,仅保留管家授权信息:adminAdvertiserId={}", adminAdvertiserId, ex);
  313. return;
  314. }
  315. JsonNode list = relations.path("list");
  316. if (list.isArray()) {
  317. for (JsonNode item : list) {
  318. long advertiserId = item.path("account_id").asLong(0);
  319. if (advertiserId > 0) {
  320. mediaAuthMapper.replaceAdvertiserAccount(toTencentAdvertiserAccount(adminAdvertiserId, item));
  321. }
  322. }
  323. }
  324. totalPage = Math.max(relations.path("page_info").path("total_page").asInt(page), page);
  325. page++;
  326. } while (page <= totalPage);
  327. }
  328. private MediaAuthConfig requireConfig(MediaType media) {
  329. MediaAuthConfig config = findConfig(media);
  330. if (config == null) {
  331. throw new BusinessException(400, media.getName() + "授权配置不存在");
  332. }
  333. return config;
  334. }
  335. private Long resolveKuaishouAdminId(JsonNode tokenData, JsonNode advertiserIds) {
  336. for (String field : new String[]{"user_id", "account_id", "corporation_id"}) {
  337. long value = tokenData.path(field).asLong(0);
  338. if (value > 0) {
  339. return value;
  340. }
  341. }
  342. return advertiserIds.get(0).asLong();
  343. }
  344. private MediaAuthAccount baseAuthAccount(MediaAuthConfig config, MediaType media, Long userId, Long adminAdvertiserId, String accessToken, String refreshToken) {
  345. MediaAuthAccount account = new MediaAuthAccount();
  346. account.setMediaType(media.getMediaId());
  347. account.setAppId(config.getAppId());
  348. account.setUserId(userId);
  349. account.setAdminAdvertiserId(adminAdvertiserId);
  350. account.setIsValid(1);
  351. account.setAccessToken(accessToken);
  352. account.setRefreshToken(refreshToken);
  353. return account;
  354. }
  355. private MediaAdvertiserAccount toKuaishouAdvertiserAccount(Long adminAdvertiserId, Long advertiserId) {
  356. MediaAdvertiserAccount account = new MediaAdvertiserAccount();
  357. account.setMediaType(MediaType.KUAISHOU.getMediaId());
  358. account.setAdminAdvertiserId(adminAdvertiserId);
  359. account.setAdvertiserId(advertiserId);
  360. account.setIsValid(1);
  361. account.setRawResponse("{\"advertiser_id\":" + advertiserId + "}");
  362. return account;
  363. }
  364. private MediaAdvertiserAccount toTencentAdvertiserAccount(Long adminAdvertiserId, JsonNode item) {
  365. MediaAdvertiserAccount account = new MediaAdvertiserAccount();
  366. account.setMediaType(MediaType.TENCENT.getMediaId());
  367. account.setAdminAdvertiserId(adminAdvertiserId);
  368. account.setAdvertiserId(item.path("account_id").asLong());
  369. account.setAdvertiserName(item.path("corporation_name").asText(null));
  370. account.setAccountRole(item.path("account_type").asText(null));
  371. account.setIsValid(1);
  372. account.setRawResponse(toJson(item));
  373. return account;
  374. }
  375. private MediaAuthAccount toAuthAccount(MediaAuthConfig config, JsonNode advertiser, String accessToken, String refreshToken, Long userId) {
  376. MediaAuthAccount account = new MediaAuthAccount();
  377. account.setMediaType(MediaType.BYTEDANCE.getMediaId());
  378. account.setAppId(config.getAppId());
  379. account.setUserId(userId);
  380. account.setAdminAdvertiserId(advertiser.path("advertiser_id").asLong());
  381. account.setAdminAdvertiserName(advertiser.path("advertiser_name").asText(null));
  382. account.setAccountRole(advertiser.path("account_role").asText(null));
  383. account.setAdvertiserRole(advertiser.path("advertiser_role").isMissingNode() ? null : advertiser.path("advertiser_role").asInt());
  384. account.setIsValid(advertiser.path("is_valid").asBoolean(false) ? 1 : 0);
  385. account.setAccessToken(accessToken);
  386. account.setRefreshToken(refreshToken);
  387. account.setRawResponse(toJson(advertiser));
  388. return account;
  389. }
  390. private void applyUserInfo(MediaAuthAccount account, String accessToken) {
  391. try {
  392. JsonNode userInfoResult = bytedanceMediaClient.userInfo(accessToken);
  393. JsonNode data = bytedanceMediaClient.requireSuccess(userInfoResult, "获取字节用户信息");
  394. String email = data.path("email").asText(null);
  395. String displayName = data.path("display_name").asText(null);
  396. if (StringUtils.hasText(email)) {
  397. account.setEmail(email);
  398. }
  399. if (StringUtils.hasText(displayName)) {
  400. account.setAdminAdvertiserName(displayName);
  401. }
  402. } catch (Exception ex) {
  403. log.warn("获取字节用户信息失败,不影响授权入库:adminAdvertiserId={}", account.getAdminAdvertiserId(), ex);
  404. }
  405. }
  406. private Long parseStateUserId(String state) {
  407. if (!StringUtils.hasText(state)) {
  408. return null;
  409. }
  410. String[] parts = state.split("_");
  411. return parts.length == 0 ? null : parseNullableLong(parts[0]);
  412. }
  413. private Long parseLong(String value, String fieldName) {
  414. Long parsed = parseNullableLong(value);
  415. if (parsed == null) {
  416. throw new BusinessException(400, fieldName + "格式错误");
  417. }
  418. return parsed;
  419. }
  420. private Long parseNullableLong(String value) {
  421. if (!StringUtils.hasText(value)) {
  422. return null;
  423. }
  424. try {
  425. return Long.valueOf(value);
  426. } catch (NumberFormatException ex) {
  427. return null;
  428. }
  429. }
  430. private String toJson(JsonNode node) {
  431. try {
  432. return objectMapper.writeValueAsString(node);
  433. } catch (Exception ex) {
  434. return "{}";
  435. }
  436. }
  437. }