package com.nuojing.media.auth.service.impl; import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; 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; import com.nuojing.media.auth.domain.entity.MediaAuthConfig; import com.nuojing.media.auth.domain.vo.AuthUrlVO; import com.nuojing.media.auth.domain.vo.MediaAuthConfigVO; import com.nuojing.media.auth.mapper.MediaAuthMapper; import com.nuojing.media.auth.service.MediaAuthService; import com.nuojing.media.callback.domain.vo.CallbackResultVO; import com.nuojing.media.common.enums.MediaType; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import org.springframework.web.util.UriComponentsBuilder; import java.util.ArrayList; import java.util.List; import java.util.Map; @Slf4j @Service @RequiredArgsConstructor 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 public List configs() { return mediaAuthMapper.selectActiveConfigs(null).stream().map(config -> { MediaType media = MediaType.ofMediaId(config.getMediaType()); MediaAuthConfigVO vo = new MediaAuthConfigVO(); vo.setMediaId(media.getMediaId()); vo.setMediaCode(media.getCode()); vo.setMediaName(media.getName()); vo.setAppId(config.getAppId()); vo.setRedirectUri(config.getRedirectUri()); vo.setCallbackReturnUrl(config.getCallbackReturnUrl()); vo.setConfigured(StringUtils.hasText(config.getAuthUrl()) && (StringUtils.hasText(config.getAppSecret()) || StringUtils.hasText(config.getClientSecret()))); return vo; }).toList(); } @Override public AuthUrlVO getAuthUrl(AuthUrlRequest request) { if (request.getMediaId() == null) { throw new BusinessException(400, "请输入媒体类型"); } MediaType media = MediaType.ofMediaId(request.getMediaId()); MediaAuthConfig config = findConfig(media); if (config == null || !StringUtils.hasText(config.getAuthUrl())) { throw new BusinessException(400, media.getName() + "授权配置未完成"); } Long userId = currentUserId(); String state = buildState(userId, config.getAppId()); AuthUrlVO vo = new AuthUrlVO(); vo.setMediaId(media.getMediaId()); vo.setMediaCode(media.getCode()); vo.setMediaName(media.getName()); vo.setAppId(config.getAppId()); vo.setUserId(userId); vo.setState(state); vo.setAuthUrl(appendState(config.getAuthUrl(), state)); return vo; } @Override @Transactional(rollbackFor = Exception.class) public CallbackResultVO handleCallback(Integer mediaId, Map params) { MediaType media = MediaType.ofMediaId(mediaId); CallbackResultVO result = new CallbackResultVO(); result.setMediaType(media.getMediaId()); result.setBindStatus(2); result.setState(params.get("state")); result.setTime(System.currentTimeMillis()); result.setCallbackReturnUrl(callbackReturnUrl(media)); try { validateCallbackParams(media, params); Long adminAdvertiserId = handleCallback(media, params); result.setCode(200); result.setAdminAdvertiserId(adminAdvertiserId); result.setMessage(media.getName() + "授权回调已接收"); log.info("{} authorization callback received, state={}, params={}", media.getName(), params.get("state"), params); } catch (Exception ex) { result.setCode(500); result.setMessage(ex.getMessage()); log.error("{} authorization callback failed, params={}", media.getName(), params, ex); } return result; } @Override public String buildReturnUrl(CallbackResultVO result) { String callbackReturnUrl = result.getCallbackReturnUrl(); if (!StringUtils.hasText(callbackReturnUrl)) { callbackReturnUrl = callbackReturnUrl(MediaType.ofMediaId(result.getMediaType())); } UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(callbackReturnUrl) .queryParam("code", result.getCode()) .queryParam("mediaType", result.getMediaType()) .queryParam("bindStatus", result.getBindStatus()) .queryParam("time", result.getTime()); if (result.getAdminAdvertiserId() != null) { builder.queryParam("adminAdvertiserId", result.getAdminAdvertiserId()); } if (StringUtils.hasText(result.getState())) { builder.queryParam("state", result.getState()); } if (StringUtils.hasText(result.getMessage())) { builder.queryParam(result.getCode() == 200 ? "message" : "msg", result.getMessage()); } return builder.build().encode().toUriString(); } private MediaAuthConfig findConfig(MediaType media) { return mediaAuthMapper.selectActiveConfigs(media.getMediaId()).stream().findFirst().orElse(null); } private String callbackReturnUrl(MediaType media) { return mediaAuthMapper.selectActiveConfigs(media.getMediaId()).stream() .map(MediaAuthConfig::getCallbackReturnUrl) .filter(StringUtils::hasText) .findFirst() .orElseThrow(() -> new BusinessException(400, media.getName() + "前端回跳地址未配置")); } private Long currentUserId() { CurrentUser currentUser = CurrentUserContext.get(); return currentUser == null ? null : currentUser.getUserId(); } private String buildState(Long userId, Long appId) { return safe(userId) + "_" + safe(appId); } private String safe(Long value) { return value == null ? "" : String.valueOf(value); } private String appendState(String authUrl, String state) { UriComponentsBuilder builder = UriComponentsBuilder.fromUriString(authUrl); builder.replaceQueryParam("state", state); return builder.build().encode().toUriString(); } private void validateCallbackParams(MediaType media, Map params) { if (!StringUtils.hasText(params.get("state"))) { throw new BusinessException(400, "state不能为空"); } if (media == MediaType.BYTEDANCE && !StringUtils.hasText(params.get("app_id"))) { throw new BusinessException(400, "app_id不能为空"); } if (media == MediaType.BYTEDANCE && !StringUtils.hasText(params.get("auth_code"))) { throw new BusinessException(400, "auth_code不能为空"); } if (media == MediaType.KUAISHOU && !StringUtils.hasText(params.get("auth_code"))) { throw new BusinessException(400, "auth_code不能为空"); } if (media == MediaType.TENCENT && !StringUtils.hasText(params.get("authorization_code"))) { throw new BusinessException(400, "authorization_code不能为空"); } } private Long handleBytedanceCallback(Map params) { Long appId = parseLong(params.get("app_id"), "app_id"); MediaAuthConfig config = mediaAuthMapper.selectActiveConfigByAppId(MediaType.BYTEDANCE.getMediaId(), appId); if (config == null) { throw new BusinessException(400, "通过此appId未获取到相关配置信息,请联系技术"); } JsonNode tokenResult = bytedanceMediaClient.exchangeToken(config, params.get("auth_code")); JsonNode tokenData = bytedanceMediaClient.requireSuccess(tokenResult, "获取字节token"); String accessToken = tokenData.path("access_token").asText(); String refreshToken = tokenData.path("refresh_token").asText(); if (!StringUtils.hasText(accessToken)) { throw new BusinessException(500, "获取字节token失败:access_token为空"); } JsonNode advertiserResult = bytedanceMediaClient.advertiserList(accessToken); JsonNode advertiserData = bytedanceMediaClient.requireSuccess(advertiserResult, "获取字节授权账户"); JsonNode advertisers = advertiserData.path("list"); if (!advertisers.isArray()) { throw new BusinessException(500, "获取字节授权账户list信息为空"); } Long userId = parseStateUserId(params.get("state")); JsonNode adminAdvertiser = null; List customerAccounts = new ArrayList<>(); for (JsonNode advertiser : advertisers) { String accountRole = advertiser.path("account_role").asText(); if ("CUSTOMER_ADMIN".equals(accountRole)) { adminAdvertiser = advertiser; } if ("CUSTOMER_ADMIN".equals(accountRole) || "CUSTOMER_OPERATOR".equals(accountRole)) { customerAccounts.add(advertiser); } } if (adminAdvertiser == null) { throw new BusinessException(500, "未获取到字节管家账户"); } Long adminAdvertiserId = adminAdvertiser.path("advertiser_id").asLong(); MediaAuthAccount authAccount = toAuthAccount(config, adminAdvertiser, accessToken, refreshToken, userId); applyUserInfo(authAccount, accessToken); mediaAuthMapper.replaceAuthAccount(authAccount); // /oauth2/advertiser/get 返回的是工作台(纵横组织)账号,需进一步展开组织下的真实投放账户 syncBytedanceDeliveryAccounts(accessToken, customerAccounts, adminAdvertiserId); return adminAdvertiserId; } private void syncBytedanceDeliveryAccounts(String accessToken, List customerAccounts, Long adminAdvertiserId) { for (JsonNode org : customerAccounts) { long ccAccountId = org.path("advertiser_id").asLong(); try { int page = 1; int totalPage = 1; do { JsonNode data = bytedanceMediaClient.requireSuccess( bytedanceMediaClient.ccAdvertiserList(accessToken, ccAccountId, page), "获取字节纵横组织投放账户"); JsonNode list = data.path("list"); if (list.isArray()) { for (JsonNode item : list) { long advertiserId = item.path("advertiser_id").asLong(0); if (advertiserId > 0) { mediaAuthMapper.replaceAdvertiserAccount(toDeliveryAccount(MediaType.BYTEDANCE, adminAdvertiserId, item)); } } } totalPage = Math.max(data.path("page_info").path("total_page").asInt(page), page); page++; } while (page <= totalPage); } catch (Exception ex) { log.warn("获取字节纵横组织投放账户失败,跳过该组织:ccAccountId={}", ccAccountId, ex); } } } private MediaAdvertiserAccount toDeliveryAccount(MediaType media, Long adminAdvertiserId, JsonNode item) { MediaAdvertiserAccount account = new MediaAdvertiserAccount(); account.setMediaType(media.getMediaId()); account.setAdminAdvertiserId(adminAdvertiserId); account.setAdvertiserId(item.path("advertiser_id").asLong()); account.setAdvertiserName(item.path("advertiser_name").asText(null)); account.setAccountRole(item.path("advertiser_type").asText(null)); account.setIsValid(1); account.setRawResponse(toJson(item)); return account; } private Long handleCallback(MediaType media, Map 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 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为空"); } // 广告账户来源:token响应的advertiser_ids(广告账户授权场景);为空则用approval/list拉取(用户授权/服务商场景) List advertiserIds = new ArrayList<>(); JsonNode tokenAdvertiserIds = tokenData.path("advertiser_ids"); if (tokenAdvertiserIds.isArray() && !tokenAdvertiserIds.isEmpty()) { tokenAdvertiserIds.forEach(node -> advertiserIds.add(node.asLong())); } else { advertiserIds.addAll(syncKuaishouApprovalList(config, accessToken)); } if (advertiserIds.isEmpty()) { throw new BusinessException(500, "未获取到快手授权广告账户"); } 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 (Long advertiserId : advertiserIds) { mediaAuthMapper.replaceAdvertiserAccount(toKuaishouAdvertiserAccount(adminAdvertiserId, advertiserId)); } return adminAdvertiserId; } private List syncKuaishouApprovalList(MediaAuthConfig config, String accessToken) { List advertiserIds = new ArrayList<>(); int pageNo = 1; boolean isEnd; do { JsonNode data = kuaishouMediaClient.requireSuccess( kuaishouMediaClient.approvalList(config, accessToken, pageNo), "获取快手授权广告账户列表"); JsonNode details = data.path("details"); if (details.isArray()) { for (JsonNode detail : details) { long advertiserId = detail.asLong(0); if (advertiserId > 0) { advertiserIds.add(advertiserId); } } } isEnd = data.path("isEnd").asBoolean(true); pageNo++; } while (!isEnd); return advertiserIds; } private Long handleTencentCallback(Map 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, List 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); } 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()); account.setAppId(config.getAppId()); account.setUserId(userId); account.setAdminAdvertiserId(advertiser.path("advertiser_id").asLong()); account.setAdminAdvertiserName(advertiser.path("advertiser_name").asText(null)); account.setAccountRole(advertiser.path("account_role").asText(null)); account.setAdvertiserRole(advertiser.path("advertiser_role").isMissingNode() ? null : advertiser.path("advertiser_role").asInt()); account.setIsValid(advertiser.path("is_valid").asBoolean(false) ? 1 : 0); account.setAccessToken(accessToken); account.setRefreshToken(refreshToken); account.setRawResponse(toJson(advertiser)); return account; } private void applyUserInfo(MediaAuthAccount account, String accessToken) { try { JsonNode userInfoResult = bytedanceMediaClient.userInfo(accessToken); JsonNode data = bytedanceMediaClient.requireSuccess(userInfoResult, "获取字节用户信息"); String email = data.path("email").asText(null); String displayName = data.path("display_name").asText(null); if (StringUtils.hasText(email)) { account.setEmail(email); } if (StringUtils.hasText(displayName)) { account.setAdminAdvertiserName(displayName); } } catch (Exception ex) { log.warn("获取字节用户信息失败,不影响授权入库:adminAdvertiserId={}", account.getAdminAdvertiserId(), ex); } } private Long parseStateUserId(String state) { if (!StringUtils.hasText(state)) { return null; } String[] parts = state.split("_"); return parts.length == 0 ? null : parseNullableLong(parts[0]); } private Long parseLong(String value, String fieldName) { Long parsed = parseNullableLong(value); if (parsed == null) { throw new BusinessException(400, fieldName + "格式错误"); } return parsed; } private Long parseNullableLong(String value) { if (!StringUtils.hasText(value)) { return null; } try { return Long.valueOf(value); } catch (NumberFormatException ex) { return null; } } private String toJson(JsonNode node) { try { return objectMapper.writeValueAsString(node); } catch (Exception ex) { return "{}"; } } }