|
|
@@ -27,6 +27,9 @@ import java.util.ArrayList;
|
|
|
import com.moka.gdtauto.entity.AdPlan;
|
|
|
import com.moka.gdtauto.entity.AdPlanCreative;
|
|
|
import com.moka.gdtauto.service.AdPlanService;
|
|
|
+import com.moka.gdtauto.service.TencentAccountIdBrandImageService;
|
|
|
+import com.moka.gdtauto.service.TencentAccountIdImageService;
|
|
|
+import com.moka.gdtauto.service.TencentAccountIdVideoIdService;
|
|
|
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.scheduling.annotation.Async;
|
|
|
@@ -39,6 +42,7 @@ import com.moka.gdtauto.common.AdPlanStatus;
|
|
|
|
|
|
import java.io.File;
|
|
|
import java.io.FileOutputStream;
|
|
|
+import java.io.IOException;
|
|
|
import java.io.InputStream;
|
|
|
import java.math.BigInteger;
|
|
|
import java.net.HttpURLConnection;
|
|
|
@@ -68,6 +72,12 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
private TencentAdsApiClientFactory clientFactory;
|
|
|
@Autowired
|
|
|
private TencentAccountService tencentAccountService;
|
|
|
+ @Autowired
|
|
|
+ private TencentAccountIdImageService tencentAccountIdImageService;
|
|
|
+ @Autowired
|
|
|
+ private TencentAccountIdVideoIdService tencentAccountIdVideoIdService;
|
|
|
+ @Autowired
|
|
|
+ private TencentAccountIdBrandImageService tencentAccountIdBrandImageService;
|
|
|
|
|
|
@Async
|
|
|
@Override
|
|
|
@@ -487,6 +497,31 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
|
|
|
String jobNumber = conf.getJobNumber();
|
|
|
|
|
|
+ // 创建 TencentAds 实例,整个流程复用同一个实例
|
|
|
+ TencentAds tencentAds;
|
|
|
+ try {
|
|
|
+ tencentAds = clientFactory.getTencentAds(jobNumber, Constant.MAIN_ORG_ACCOUNT_ID);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("创建TencentAds实例失败,confId={}", confId, e);
|
|
|
+ conf.setStatus(AdPlanStatus.FAILED);
|
|
|
+ conf.setFailReason("创建TencentAds实例失败: " + truncateError(e.getMessage()));
|
|
|
+ this.updateById(conf);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 第一步:准备阶段 - 填充创意的 imageId/videoId
|
|
|
+ try {
|
|
|
+ prepareCreativeAssets(tencentAds, plans);
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("准备创意资产失败,confId={}", confId, e);
|
|
|
+ // 更新 AdPlanConf 状态为失败
|
|
|
+ conf.setStatus(AdPlanStatus.FAILED);
|
|
|
+ conf.setFailReason("准备创意资产失败: " + truncateError(e.getMessage()));
|
|
|
+ this.updateById(conf);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 第二步:创建广告组与创意
|
|
|
// 遍历处理每个 AdPlan
|
|
|
for (AdPlan plan : plans) {
|
|
|
try {
|
|
|
@@ -494,7 +529,7 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
|
|
|
if (plan.getAdgroupId() > 0 || plan.getStatus() == AdPlanStatus.SUCCESS) {
|
|
|
// 创建动态创意
|
|
|
- createDynamicCreatives(plan, jobNumber, Constant.MAIN_ORG_ACCOUNT_ID);
|
|
|
+ createDynamicCreatives(tencentAds, plan);
|
|
|
plan.setFailReason("");
|
|
|
plan.setStatus(AdPlanStatus.SUCCESS);
|
|
|
plan.setSuccessTime(LocalDateTime.now());
|
|
|
@@ -513,7 +548,7 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
log.info("调用腾讯API创建广告组,accountId={}, orgAccountId={}", plan.getAccountId(),
|
|
|
Constant.MAIN_ORG_ACCOUNT_ID);
|
|
|
AdgroupsAddResponseData response = tencentAdsAdgroupService.createAdgroup(
|
|
|
- jobNumber, Constant.MAIN_ORG_ACCOUNT_ID, request);
|
|
|
+ tencentAds, request);
|
|
|
|
|
|
// 更新 adgroupId
|
|
|
plan.setAdgroupId(response.getAdgroupId());
|
|
|
@@ -527,7 +562,7 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
|
|
|
// 创建动态创意
|
|
|
if (plan.getAdgroupId() > 0) {
|
|
|
- createDynamicCreatives(plan, jobNumber, Constant.MAIN_ORG_ACCOUNT_ID);
|
|
|
+ createDynamicCreatives(tencentAds, plan);
|
|
|
}
|
|
|
|
|
|
log.info("广告计划处理成功,planId={}, adgroupId={}", plan.getId(), plan.getAdgroupId());
|
|
|
@@ -554,6 +589,146 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 准备创意资产 - 填充创意的 imageId/videoId
|
|
|
+ * 按 accountId 分组,先查询缓存/数据库,不存在则上传并保存
|
|
|
+ */
|
|
|
+ private void prepareCreativeAssets(TencentAds tencentAds, List<AdPlan> plans) throws Exception {
|
|
|
+ log.info("准备创意资产开始,计划数量: {}", plans.size());
|
|
|
+
|
|
|
+ // 按 accountId 分组
|
|
|
+ Map<Long, List<AdPlan>> plansByAccountId = plans.stream()
|
|
|
+ .collect(Collectors.groupingBy(AdPlan::getAccountId));
|
|
|
+
|
|
|
+ for (Map.Entry<Long, List<AdPlan>> entry : plansByAccountId.entrySet()) {
|
|
|
+ Long accountId = entry.getKey();
|
|
|
+ List<AdPlan> accountPlans = entry.getValue();
|
|
|
+
|
|
|
+ log.info("处理账户 {} 的创意资产,计划数量: {}", accountId, accountPlans.size());
|
|
|
+
|
|
|
+ // 收集该账户下所有创意
|
|
|
+ List<AdPlanCreative> allCreatives = accountPlans.stream()
|
|
|
+ .flatMap(plan -> plan.getAdCreatives().stream())
|
|
|
+ .collect(Collectors.toList());
|
|
|
+
|
|
|
+ for (AdPlanCreative creative : allCreatives) {
|
|
|
+ prepareSingleCreativeAsset(tencentAds, accountId, creative);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("准备创意资产完成");
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 准备单个创意的资产
|
|
|
+ */
|
|
|
+ private void prepareSingleCreativeAsset(TencentAds tencentAds, Long accountId, AdPlanCreative creative) throws Exception {
|
|
|
+ String creativeFileType = creative.getCreativeFileType();
|
|
|
+ String creativeFileUrl = creative.getCreativeFileUrl();
|
|
|
+ String creativeFileMd5 = creative.getCreativeFileMd5();
|
|
|
+ Long creativeImageId = creative.getImageId();
|
|
|
+ Long creativeVideoId = creative.getVideoId();
|
|
|
+
|
|
|
+
|
|
|
+ // 跳过无效数据
|
|
|
+ if (creativeFileType == null || creativeFileType.isEmpty()
|
|
|
+ || creativeFileUrl == null || creativeFileUrl.isEmpty()
|
|
|
+ || creativeFileMd5 == null || creativeFileMd5.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ("image".equalsIgnoreCase(creativeFileType) && creativeImageId != null && creativeImageId > 0) {
|
|
|
+ return;
|
|
|
+ } else if ("video".equalsIgnoreCase(creativeFileType) && creativeVideoId != null && creativeVideoId > 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if ("image".equalsIgnoreCase(creativeFileType)) {
|
|
|
+ // 图片类型:查询或上传图片
|
|
|
+ if (creative.getImageId() != null && creative.getImageId() > 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Long imageId = tencentAccountIdImageService.getImageIdByAccountIdAndMd5(accountId, creativeFileMd5);
|
|
|
+ if (imageId != null) {
|
|
|
+ log.info("从缓存/数据库获取图片ID,accountId={}, md5={}, imageId={}", accountId, creativeFileMd5, imageId);
|
|
|
+ creative.setImageId(imageId);
|
|
|
+ } else {
|
|
|
+ // 上传图片
|
|
|
+ log.info("上传创意图片,accountId={}, URL={}", accountId, creativeFileUrl);
|
|
|
+ imageId = uploadImageFromUrl(tencentAds, accountId, creativeFileUrl, creativeFileMd5,
|
|
|
+ "创意图片-" + creative.getCreativeName());
|
|
|
+ creative.setImageId(imageId);
|
|
|
+ // 保存到数据库
|
|
|
+ tencentAccountIdImageService.saveImageId(accountId, creativeFileMd5, imageId, creativeFileUrl);
|
|
|
+ log.info("图片上传并保存成功,accountId={}, md5={}, imageId={}", accountId, creativeFileMd5, imageId);
|
|
|
+ }
|
|
|
+ adPlanCreativeService.updateById(creative);
|
|
|
+
|
|
|
+ } else if ("video".equalsIgnoreCase(creativeFileType)) {
|
|
|
+ // 视频类型:查询或上传视频
|
|
|
+ if (creative.getVideoId() != null && creative.getVideoId() > 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ Long videoId = tencentAccountIdVideoIdService.getVideoIdByAccountIdAndMd5(accountId, creativeFileMd5);
|
|
|
+ if (videoId != null) {
|
|
|
+ log.info("从缓存/数据库获取视频ID,accountId={}, md5={}, videoId={}", accountId, creativeFileMd5, videoId);
|
|
|
+ creative.setVideoId(videoId);
|
|
|
+ } else {
|
|
|
+ // 上传视频
|
|
|
+ log.info("上传创意视频,accountId={}, URL={}", accountId, creativeFileUrl);
|
|
|
+ VideoUploadResult videoResult = uploadVideoFromUrl(tencentAds, accountId, creativeFileUrl, creativeFileMd5,
|
|
|
+ "创意视频-" + creative.getCreativeName());
|
|
|
+ creative.setVideoId(videoResult.videoId());
|
|
|
+ // 保存到数据库
|
|
|
+ tencentAccountIdVideoIdService.saveVideoId(accountId, creativeFileMd5, videoResult.videoId(),
|
|
|
+ creativeFileUrl, videoResult.coverImageId());
|
|
|
+ log.info("视频上传并保存成功,accountId={}, md5={}, videoId={}", accountId, creativeFileMd5, videoResult.videoId());
|
|
|
+ }
|
|
|
+ adPlanCreativeService.updateById(creative);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理品牌形象图片
|
|
|
+ prepareBrandImageAsset(tencentAds, accountId, creative);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 准备品牌形象图片资产
|
|
|
+ */
|
|
|
+ private void prepareBrandImageAsset(TencentAds tencentAds, Long accountId, AdPlanCreative creative) throws Exception {
|
|
|
+ String brandImageUrl = creative.getBrandImageUrl();
|
|
|
+ String brandImageFileMd5 = creative.getBrandImageFileMd5();
|
|
|
+ String brandName = creative.getBrandName();
|
|
|
+
|
|
|
+ // 跳过无效数据
|
|
|
+ if (brandImageUrl == null || brandImageUrl.isEmpty()
|
|
|
+ || brandImageFileMd5 == null || brandImageFileMd5.isEmpty()
|
|
|
+ || brandName == null || brandName.isEmpty()) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 已经有 brandImageId 则跳过
|
|
|
+ if (creative.getBrandImageId() != null && creative.getBrandImageId() > 0) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 查询缓存/数据库
|
|
|
+ Long brandImageId = tencentAccountIdBrandImageService.getBrandImageIdByAccountIdAndMd5(accountId, brandImageFileMd5);
|
|
|
+ if (brandImageId != null) {
|
|
|
+ log.info("从缓存/数据库获取品牌形象ID,accountId={}, md5={}, brandImageId={}", accountId, brandImageFileMd5, brandImageId);
|
|
|
+ creative.setBrandImageId(brandImageId);
|
|
|
+ } else {
|
|
|
+ // 上传品牌形象
|
|
|
+ log.info("上传品牌形象,accountId={}, URL={}, brandName={}", accountId, brandImageUrl, brandName);
|
|
|
+ BrandUploadResult brandResult = uploadBrandImageFromUrl(tencentAds, accountId, brandImageUrl, brandImageFileMd5, brandName);
|
|
|
+ creative.setBrandImageId(brandResult.imageId());
|
|
|
+ // 保存到数据库
|
|
|
+ tencentAccountIdBrandImageService.saveBrandImageId(accountId, brandImageFileMd5, brandResult.imageId(),
|
|
|
+ brandImageUrl, brandResult.width(), brandResult.height());
|
|
|
+ log.info("品牌形象上传并保存成功,accountId={}, md5={}, brandImageId={}", accountId, brandImageFileMd5, brandResult.imageId());
|
|
|
+ }
|
|
|
+ adPlanCreativeService.updateById(creative);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 构建广告组请求
|
|
|
*/
|
|
|
private AdgroupsAddRequest buildAdgroupRequest(AdPlan plan) {
|
|
|
@@ -588,11 +763,9 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
/**
|
|
|
* 创建动态创意
|
|
|
*/
|
|
|
- private void createDynamicCreatives(AdPlan plan, String jobNumber, Long orgAccountId)
|
|
|
+ private void createDynamicCreatives(TencentAds tencentAds, AdPlan plan)
|
|
|
throws TencentAdsResponseException, Exception {
|
|
|
|
|
|
- TencentAds tencentAds = clientFactory.getTencentAds(jobNumber, orgAccountId);
|
|
|
-
|
|
|
for (AdPlanCreative creative : plan.getAdCreatives()) {
|
|
|
try {
|
|
|
log.info("开始创建动态创意,planId={}, creativeName={}", plan.getId(), creative.getCreativeName());
|
|
|
@@ -605,44 +778,6 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
creative.setAdgroupId(plan.getAdgroupId());
|
|
|
adPlanCreativeService.updateById(creative);
|
|
|
|
|
|
- // 上传创意文件的到imageId
|
|
|
- if (creative.getImageId() == 0) {
|
|
|
- String creativeFileUrl = creative.getCreativeFileUrl();
|
|
|
- if (creativeFileUrl != null && !creativeFileUrl.isEmpty()) {
|
|
|
- try {
|
|
|
- log.info("开始上传创意图片,URL: {}", creativeFileUrl);
|
|
|
- Long imageId = uploadImageFromUrl(tencentAds, plan.getAccountId(), creativeFileUrl,
|
|
|
- "创意图片-" + creative.getCreativeName());
|
|
|
- creative.setImageId(imageId);
|
|
|
- adPlanCreativeService.updateById(creative);
|
|
|
- log.info("创意图片上传成功,imageId: {}", imageId);
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("创意图片上传失败", e);
|
|
|
- throw new RuntimeException("创意图片上传失败: " + e.getMessage(), e);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- if (creative.getBrandImageId() == 0) {
|
|
|
- // 上传品牌形象
|
|
|
- String brandImageUrl = creative.getBrandImageUrl();
|
|
|
- String brandName = creative.getBrandName();
|
|
|
- if (brandImageUrl != null && !brandImageUrl.isEmpty()
|
|
|
- && brandName != null && !brandName.isEmpty()) {
|
|
|
- try {
|
|
|
- log.info("开始上传品牌形象,URL: {}, 品牌名称: {}", brandImageUrl, brandName);
|
|
|
- Long brandImageId = uploadBrandFromUrl(tencentAds, plan.getAccountId(), brandImageUrl,
|
|
|
- brandName);
|
|
|
- creative.setBrandImageId(brandImageId);
|
|
|
- adPlanCreativeService.updateById(creative);
|
|
|
- log.info("品牌形象上传成功,brandImageId: {}", brandImageId);
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("品牌形象上传失败", e);
|
|
|
- throw new RuntimeException("品牌形象上传失败: " + e.getMessage(), e);
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
// 构建动态创意请求
|
|
|
DynamicCreativesAddRequest request = buildDynamicCreativeRequest(plan, creative);
|
|
|
|
|
|
@@ -663,7 +798,6 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
creative.setStatus(AdPlanStatus.FAILED);
|
|
|
creative.setFailReason(truncateError(e.getMessage()));
|
|
|
adPlanCreativeService.updateById(creative);
|
|
|
- throw e; // 创意失败则整个广告组失败
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
@@ -688,7 +822,14 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
components.setDescription(buildDescriptionComponents(creative.getCreativeText()));
|
|
|
|
|
|
// 图片组件 (从 creative.imageId)
|
|
|
- components.setImage(buildImageComponents(creative.getImageId()));
|
|
|
+ if (creative.getImageId() > 0) {
|
|
|
+ components.setImage(buildImageComponents(creative.getImageId()));
|
|
|
+ }
|
|
|
+
|
|
|
+ if (creative.getVideoId() > 0) {
|
|
|
+ components.setVideo(buildVideoComponents(creative.getVideoId()));
|
|
|
+ }
|
|
|
+
|
|
|
|
|
|
// 品牌形象 (固定品牌)
|
|
|
components.setBrand(buildBrandComponents(creative.getBrandImageId(), creative.getBrandName()));
|
|
|
@@ -703,6 +844,16 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
return request;
|
|
|
}
|
|
|
|
|
|
+ private List<VideoComponent> buildVideoComponents(Long videoId) {
|
|
|
+ List<VideoComponent> videos = new ArrayList<>();
|
|
|
+ VideoComponent video = new VideoComponent();
|
|
|
+ VideoStruct videoValue = new VideoStruct();
|
|
|
+ videoValue.setVideoId(String.valueOf(videoId));
|
|
|
+ video.setValue(videoValue);
|
|
|
+ videos.add(video);
|
|
|
+ return videos;
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 构建标题组件
|
|
|
*/
|
|
|
@@ -904,19 +1055,33 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
* @return 图片ID
|
|
|
* @throws Exception 上传失败
|
|
|
*/
|
|
|
- private Long uploadImageFromUrl(TencentAds tencentAds, Long accountId, String imageUrl, String description)
|
|
|
+ private Long uploadImageFromUrl(TencentAds tencentAds, Long accountId, String imageUrl, String fileMd5, String description)
|
|
|
throws Exception {
|
|
|
- File tempFile = null;
|
|
|
+ HttpURLConnection connection = null;
|
|
|
+ InputStream inputStream = null;
|
|
|
try {
|
|
|
- // 1. 从URL下载图片到临时文件
|
|
|
- tempFile = downloadFileFromUrl(imageUrl);
|
|
|
+ // 1. 从URL直接读取图片字节流
|
|
|
+ URL url = new URL(imageUrl);
|
|
|
+ connection = (HttpURLConnection) url.openConnection();
|
|
|
+ connection.setConnectTimeout(10000);
|
|
|
+ connection.setReadTimeout(30000);
|
|
|
+ connection.setRequestMethod("GET");
|
|
|
+ connection.connect();
|
|
|
|
|
|
- // 2. 读取文件并转Base64
|
|
|
- byte[] imageBytes = Files.readAllBytes(tempFile.toPath());
|
|
|
+ int responseCode = connection.getResponseCode();
|
|
|
+ if (responseCode != HttpURLConnection.HTTP_OK) {
|
|
|
+ throw new Exception("下载图片失败,HTTP状态码: " + responseCode);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 读取输入流并转Base64
|
|
|
+ inputStream = connection.getInputStream();
|
|
|
+ byte[] imageBytes = inputStream.readAllBytes();
|
|
|
String base64Image = Base64.getEncoder().encodeToString(imageBytes);
|
|
|
|
|
|
+ log.info("图片下载成功,URL: {}, 大小: {} bytes", imageUrl, imageBytes.length);
|
|
|
+
|
|
|
// 3. 计算MD5签名
|
|
|
- String signature = calculateMD5(imageBytes);
|
|
|
+ String signature = fileMd5;
|
|
|
|
|
|
// 4. 调用腾讯广告API上传图片
|
|
|
ImagesAddResponseData response = tencentAds.images()
|
|
|
@@ -944,6 +1109,67 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
return Long.parseLong(response.getImageId());
|
|
|
|
|
|
} finally {
|
|
|
+ if (inputStream != null) {
|
|
|
+ try {
|
|
|
+ inputStream.close();
|
|
|
+ } catch (IOException e) {
|
|
|
+ log.warn("关闭输入流失败", e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (connection != null) {
|
|
|
+ connection.disconnect();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 视频上传结果记录
|
|
|
+ */
|
|
|
+ private record VideoUploadResult(Long videoId, Long coverImageId) {}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 品牌形象上传结果记录
|
|
|
+ */
|
|
|
+ private record BrandUploadResult(Long imageId, Long width, Long height) {}
|
|
|
+
|
|
|
+ private VideoUploadResult uploadVideoFromUrl(TencentAds tencentAds, Long accountId, String videoUrl, String fileMd5, String description)
|
|
|
+ throws Exception {
|
|
|
+ File tempFile = null;
|
|
|
+ try {
|
|
|
+ // 1. 从URL下载视频到临时文件
|
|
|
+ tempFile = downloadFileFromUrl(videoUrl);
|
|
|
+
|
|
|
+ // 2. 检查文件大小(最大100MB)
|
|
|
+ long fileSizeInMB = tempFile.length() / (1024 * 1024);
|
|
|
+ if (fileSizeInMB > 100) {
|
|
|
+ throw new Exception("视频文件过大: " + fileSizeInMB + "MB,超过 100MB 限制");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 3. 计算视频MD5签名
|
|
|
+ String signature = fileMd5;
|
|
|
+ log.info("视频MD5签名: {}, 文件大小: {}MB", signature, fileSizeInMB);
|
|
|
+
|
|
|
+ // 4. 调用腾讯广告API上传视频
|
|
|
+ VideosAddResponseData response = tencentAds.videos()
|
|
|
+ .videosAdd(
|
|
|
+ tempFile, // videoFile
|
|
|
+ signature, // signature
|
|
|
+ accountId, // accountId
|
|
|
+ null, // organizationId
|
|
|
+ description, // description
|
|
|
+ null // adcreativeTemplateId
|
|
|
+ );
|
|
|
+
|
|
|
+ if (response == null || response.getVideoId() == null) {
|
|
|
+ throw new Exception("视频上传返回结果为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("视频上传成功,videoId: {}, 封面图片ID: {}",
|
|
|
+ response.getVideoId(), response.getCoverImageId());
|
|
|
+
|
|
|
+ return new VideoUploadResult(response.getVideoId(), response.getCoverImageId());
|
|
|
+
|
|
|
+ } finally {
|
|
|
// 5. 删除临时文件
|
|
|
if (tempFile != null && tempFile.exists()) {
|
|
|
tempFile.delete();
|
|
|
@@ -952,6 +1178,32 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 计算文件的MD5签名
|
|
|
+ *
|
|
|
+ * @param file 文件
|
|
|
+ * @return MD5签名(32位小写字符串)
|
|
|
+ * @throws Exception 计算失败
|
|
|
+ */
|
|
|
+ private String calculateFileMD5(File file) throws Exception {
|
|
|
+ MessageDigest md = MessageDigest.getInstance("MD5");
|
|
|
+ try (java.io.FileInputStream fis = new java.io.FileInputStream(file)) {
|
|
|
+ byte[] buffer = new byte[8192];
|
|
|
+ int bytesRead;
|
|
|
+ while ((bytesRead = fis.read(buffer)) != -1) {
|
|
|
+ md.update(buffer, 0, bytesRead);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ byte[] digest = md.digest();
|
|
|
+ BigInteger bigInt = new BigInteger(1, digest);
|
|
|
+ String hashText = bigInt.toString(16);
|
|
|
+ // 补齐前导0
|
|
|
+ while (hashText.length() < 32) {
|
|
|
+ hashText = "0" + hashText;
|
|
|
+ }
|
|
|
+ return hashText;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 从URL下载品牌形象并上传到腾讯广告
|
|
|
*
|
|
|
* @param tencentAds 腾讯广告SDK实例
|
|
|
@@ -994,6 +1246,53 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
+ * 从URL下载品牌形象并上传到腾讯广告(返回完整信息)
|
|
|
+ *
|
|
|
+ * @param tencentAds 腾讯广告SDK实例
|
|
|
+ * @param accountId 账户ID
|
|
|
+ * @param brandImageUrl 品牌形象图片URL
|
|
|
+ * @param fileMd5 文件MD5
|
|
|
+ * @param brandName 品牌名称
|
|
|
+ * @return 品牌形象上传结果
|
|
|
+ * @throws Exception 上传失败
|
|
|
+ */
|
|
|
+ private BrandUploadResult uploadBrandImageFromUrl(TencentAds tencentAds, Long accountId, String brandImageUrl, String fileMd5, String brandName)
|
|
|
+ throws Exception {
|
|
|
+ File tempFile = null;
|
|
|
+ try {
|
|
|
+ // 1. 从URL下载图片到临时文件
|
|
|
+ tempFile = downloadFileFromUrl(brandImageUrl);
|
|
|
+
|
|
|
+ // 2. 调用腾讯广告API创建品牌形象
|
|
|
+ BrandAddResponseData response = tencentAds.brand()
|
|
|
+ .brandAdd(
|
|
|
+ accountId, // accountId
|
|
|
+ brandName, // name
|
|
|
+ tempFile // brandImageFile
|
|
|
+ );
|
|
|
+
|
|
|
+ if (response == null || response.getImageId() == null) {
|
|
|
+ throw new Exception("品牌形象上传返回结果为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("品牌形象上传成功,imageId: {}, 品牌名称: {}, 宽度: {}px, 高度: {}px",
|
|
|
+ response.getImageId(), response.getName(), response.getWidth(), response.getHeight());
|
|
|
+
|
|
|
+ return new BrandUploadResult(
|
|
|
+ Long.parseLong(response.getImageId()),
|
|
|
+ response.getWidth(),
|
|
|
+ response.getHeight()
|
|
|
+ );
|
|
|
+
|
|
|
+ } finally {
|
|
|
+ // 3. 删除临时文件
|
|
|
+ if (tempFile != null && tempFile.exists()) {
|
|
|
+ tempFile.delete();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
* 从URL下载文件到临时文件
|
|
|
*
|
|
|
* @param fileUrl 文件URL
|