|
|
@@ -35,6 +35,16 @@ import com.tencent.ads.v3.TencentAds;
|
|
|
import com.moka.gdtauto.util.AdgroupsAddReqPool;
|
|
|
import com.moka.gdtauto.common.AdPlanStatus;
|
|
|
|
|
|
+import java.io.File;
|
|
|
+import java.io.FileOutputStream;
|
|
|
+import java.io.InputStream;
|
|
|
+import java.math.BigInteger;
|
|
|
+import java.net.HttpURLConnection;
|
|
|
+import java.net.URL;
|
|
|
+import java.nio.file.Files;
|
|
|
+import java.security.MessageDigest;
|
|
|
+import java.util.Base64;
|
|
|
+
|
|
|
|
|
|
|
|
|
/**
|
|
|
@@ -190,8 +200,12 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
|
|
|
if (plan.getAdgroupId() > 0 || plan.getStatus() == AdPlanStatus.SUCCESS) {
|
|
|
// 创建动态创意
|
|
|
- createDynamicCreatives(plan, jobNumber, 33453856L);
|
|
|
- continue;
|
|
|
+ createDynamicCreatives(plan, jobNumber, 33453856L);
|
|
|
+ plan.setFailReason("");
|
|
|
+ plan.setStatus(AdPlanStatus.SUCCESS);
|
|
|
+ plan.setSuccessTime(LocalDateTime.now());
|
|
|
+ adPlanService.updateById(plan);
|
|
|
+ continue;
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -230,7 +244,9 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
adPlanService.updateById(plan);
|
|
|
|
|
|
// 创建动态创意
|
|
|
- createDynamicCreatives(plan, jobNumber, 33453856L);
|
|
|
+ if (plan.getAdgroupId() > 0) {
|
|
|
+ createDynamicCreatives(plan, jobNumber, 33453856L);
|
|
|
+ }
|
|
|
|
|
|
log.info("广告计划处理成功,planId={}, adgroupId={}", plan.getId(), plan.getAdgroupId());
|
|
|
|
|
|
@@ -301,7 +317,44 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
creative.setStatus(AdPlanStatus.IN_PROGRESS);
|
|
|
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);
|
|
|
|
|
|
@@ -342,22 +395,22 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
CreativeComponents components = new CreativeComponents();
|
|
|
|
|
|
// 标题组件 (固定标题)
|
|
|
- components.setTitle(buildTitleComponents());
|
|
|
+ components.setTitle(buildTitleComponents(creative.getCreativeText()));
|
|
|
|
|
|
// 文案组件 (固定文案)
|
|
|
- components.setDescription(buildDescriptionComponents());
|
|
|
+ components.setDescription(buildDescriptionComponents(creative.getCreativeText()));
|
|
|
|
|
|
// 图片组件 (从 creative.imageId)
|
|
|
components.setImage(buildImageComponents(creative.getImageId()));
|
|
|
|
|
|
// 品牌形象 (固定品牌)
|
|
|
- components.setBrand(buildBrandComponents());
|
|
|
+ components.setBrand(buildBrandComponents(creative.getBrandImageId(), creative.getBrandName()));
|
|
|
|
|
|
// 落地页 (从 creative.pageIds, 逗号分隔)
|
|
|
components.setMainJumpInfo(buildJumpInfoComponents(creative.getPageIds()));
|
|
|
|
|
|
// 行动按钮 (数量与落地页一致, 文案统一)
|
|
|
- components.setActionButton(buildActionButtonComponents(creative.getPageIds()));
|
|
|
+ components.setActionButton(buildActionButtonComponents(creative.getPageIds(), creative.getButtonText()));
|
|
|
|
|
|
request.setCreativeComponents(components);
|
|
|
return request;
|
|
|
@@ -366,12 +419,12 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
/**
|
|
|
* 构建标题组件
|
|
|
*/
|
|
|
- private List<TitleComponent> buildTitleComponents() {
|
|
|
+ private List<TitleComponent> buildTitleComponents(String creativeText) {
|
|
|
List<TitleComponent> titles = new ArrayList<>();
|
|
|
|
|
|
TitleComponent title1 = new TitleComponent();
|
|
|
TitleStruct titleValue1 = new TitleStruct();
|
|
|
- titleValue1.setContent("情感美文点点看");
|
|
|
+ titleValue1.setContent(creativeText);
|
|
|
title1.setValue(titleValue1);
|
|
|
titles.add(title1);
|
|
|
|
|
|
@@ -381,12 +434,12 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
/**
|
|
|
* 构建文案组件
|
|
|
*/
|
|
|
- private List<DescriptionComponent> buildDescriptionComponents() {
|
|
|
+ private List<DescriptionComponent> buildDescriptionComponents(String creativeText) {
|
|
|
List<DescriptionComponent> descriptions = new ArrayList<>();
|
|
|
|
|
|
DescriptionComponent desc1 = new DescriptionComponent();
|
|
|
DescriptionStruct descValue1 = new DescriptionStruct();
|
|
|
- descValue1.setContent("情感美文点点看");
|
|
|
+ descValue1.setContent(creativeText);
|
|
|
desc1.setValue(descValue1);
|
|
|
descriptions.add(desc1);
|
|
|
|
|
|
@@ -398,7 +451,6 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
*/
|
|
|
private List<ImageComponent> buildImageComponents(Long imageId) {
|
|
|
List<ImageComponent> images = new ArrayList<>();
|
|
|
-
|
|
|
ImageComponent image = new ImageComponent();
|
|
|
ImageStruct imageValue = new ImageStruct();
|
|
|
imageValue.setImageId(String.valueOf(imageId));
|
|
|
@@ -411,15 +463,14 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
/**
|
|
|
* 构建品牌组件
|
|
|
*/
|
|
|
- private List<BrandComponent> buildBrandComponents() {
|
|
|
+ private List<BrandComponent> buildBrandComponents(Long brandImageId, String brandName) {
|
|
|
List<BrandComponent> brands = new ArrayList<>();
|
|
|
|
|
|
BrandComponent brand = new BrandComponent();
|
|
|
- brand.setComponentId(44579024220L);
|
|
|
- // BrandStruct brandValue = new BrandStruct();
|
|
|
- // brandValue.setBrandName("情感美文");
|
|
|
- // brandValue.setBrandImageId("30026044695");
|
|
|
- // brand.setValue(brandValue);
|
|
|
+ BrandStruct brandValue = new BrandStruct();
|
|
|
+ brandValue.setBrandName(brandName);
|
|
|
+ brandValue.setBrandImageId(String.valueOf(brandImageId));
|
|
|
+ brand.setValue(brandValue);
|
|
|
brands.add(brand);
|
|
|
|
|
|
return brands;
|
|
|
@@ -457,7 +508,7 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
/**
|
|
|
* 构建行动按钮组件
|
|
|
*/
|
|
|
- private List<ActionButtonComponent> buildActionButtonComponents(String pageIds) {
|
|
|
+ private List<ActionButtonComponent> buildActionButtonComponents(String pageIds, String buttonText) {
|
|
|
List<ActionButtonComponent> actionButtons = new ArrayList<>();
|
|
|
|
|
|
if (pageIds == null || pageIds.isEmpty()) {
|
|
|
@@ -468,7 +519,7 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
for (int i = 0; i < pageIdArray.length; i++) {
|
|
|
ActionButtonComponent actionButton = new ActionButtonComponent();
|
|
|
ActionButtonStruct actionButtonValue = new ActionButtonStruct();
|
|
|
- actionButtonValue.setButtonText("查看详情");
|
|
|
+ actionButtonValue.setButtonText(buttonText);
|
|
|
actionButton.setValue(actionButtonValue);
|
|
|
actionButtons.add(actionButton);
|
|
|
}
|
|
|
@@ -488,14 +539,14 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
// 更新配置状态
|
|
|
AdPlanConf conf = this.getById(confId);
|
|
|
if (conf == null) {
|
|
|
- log.error("未找到广告计划配置,confId={}", confId);
|
|
|
+ log.warn("未找到广告计划配置,confId={}", confId);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (failCount == 0) {
|
|
|
conf.setStatus(AdPlanStatus.SUCCESS); // 全部成功
|
|
|
conf.setSuccessTime(LocalDateTime.now());
|
|
|
- conf.setFailReason(null);
|
|
|
+ conf.setFailReason("");
|
|
|
} else if (successCount == 0) {
|
|
|
conf.setStatus(AdPlanStatus.FAILED); // 全部失败
|
|
|
conf.setFailReason("所有广告创建失败");
|
|
|
@@ -556,4 +607,194 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
return 1;
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从URL下载图片并上传到腾讯广告
|
|
|
+ *
|
|
|
+ * @param tencentAds 腾讯广告SDK实例
|
|
|
+ * @param accountId 账户ID
|
|
|
+ * @param imageUrl 图片URL
|
|
|
+ * @param description 图片描述
|
|
|
+ * @return 图片ID
|
|
|
+ * @throws Exception 上传失败
|
|
|
+ */
|
|
|
+ private Long uploadImageFromUrl(TencentAds tencentAds, Long accountId, String imageUrl, String description) throws Exception {
|
|
|
+ File tempFile = null;
|
|
|
+ try {
|
|
|
+ // 1. 从URL下载图片到临时文件
|
|
|
+ tempFile = downloadFileFromUrl(imageUrl);
|
|
|
+
|
|
|
+ // 2. 读取文件并转Base64
|
|
|
+ byte[] imageBytes = Files.readAllBytes(tempFile.toPath());
|
|
|
+ String base64Image = Base64.getEncoder().encodeToString(imageBytes);
|
|
|
+
|
|
|
+ // 3. 计算MD5签名
|
|
|
+ String signature = calculateMD5(imageBytes);
|
|
|
+
|
|
|
+ // 4. 调用腾讯广告API上传图片
|
|
|
+ ImagesAddResponseData response = tencentAds.images()
|
|
|
+ .imagesAdd(
|
|
|
+ "UPLOAD_TYPE_BYTES", // uploadType
|
|
|
+ signature, // signature
|
|
|
+ accountId, // accountId
|
|
|
+ null, // organizationId
|
|
|
+ null, // file
|
|
|
+ base64Image, // bytes
|
|
|
+ null, // imageUsage
|
|
|
+ description, // description
|
|
|
+ null, // resizeWidth
|
|
|
+ null, // resizeHeight
|
|
|
+ null // resizeFileSize
|
|
|
+ );
|
|
|
+
|
|
|
+ if (response == null || response.getImageId() == null) {
|
|
|
+ throw new Exception("图片上传返回结果为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("图片上传成功,imageId: {}, 宽度: {}px, 高度: {}px",
|
|
|
+ response.getImageId(), response.getImageWidth(), response.getImageHeight());
|
|
|
+
|
|
|
+ return Long.parseLong(response.getImageId());
|
|
|
+
|
|
|
+ } finally {
|
|
|
+ // 5. 删除临时文件
|
|
|
+ if (tempFile != null && tempFile.exists()) {
|
|
|
+ tempFile.delete();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从URL下载品牌形象并上传到腾讯广告
|
|
|
+ *
|
|
|
+ * @param tencentAds 腾讯广告SDK实例
|
|
|
+ * @param accountId 账户ID
|
|
|
+ * @param brandImageUrl 品牌形象图片URL
|
|
|
+ * @param brandName 品牌名称
|
|
|
+ * @return 品牌图片ID
|
|
|
+ * @throws Exception 上传失败
|
|
|
+ */
|
|
|
+ private Long uploadBrandFromUrl(TencentAds tencentAds, Long accountId, String brandImageUrl, 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 Long.parseLong(response.getImageId());
|
|
|
+
|
|
|
+ } finally {
|
|
|
+ // 3. 删除临时文件
|
|
|
+ if (tempFile != null && tempFile.exists()) {
|
|
|
+ tempFile.delete();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从URL下载文件到临时文件
|
|
|
+ *
|
|
|
+ * @param fileUrl 文件URL
|
|
|
+ * @return 临时文件
|
|
|
+ * @throws Exception 下载失败
|
|
|
+ */
|
|
|
+ private File downloadFileFromUrl(String fileUrl) throws Exception {
|
|
|
+ HttpURLConnection connection = null;
|
|
|
+ InputStream inputStream = null;
|
|
|
+ FileOutputStream outputStream = null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 创建临时文件
|
|
|
+ String suffix = getFileSuffix(fileUrl);
|
|
|
+ File tempFile = File.createTempFile("gdt_upload_", suffix);
|
|
|
+
|
|
|
+ // 打开HTTP连接
|
|
|
+ URL url = new URL(fileUrl);
|
|
|
+ connection = (HttpURLConnection) url.openConnection();
|
|
|
+ connection.setConnectTimeout(10000);
|
|
|
+ connection.setReadTimeout(30000);
|
|
|
+ connection.setRequestMethod("GET");
|
|
|
+ connection.connect();
|
|
|
+
|
|
|
+ int responseCode = connection.getResponseCode();
|
|
|
+ if (responseCode != HttpURLConnection.HTTP_OK) {
|
|
|
+ throw new Exception("下载文件失败,HTTP状态码: " + responseCode);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 读取文件内容
|
|
|
+ inputStream = connection.getInputStream();
|
|
|
+ outputStream = new FileOutputStream(tempFile);
|
|
|
+
|
|
|
+ byte[] buffer = new byte[8192];
|
|
|
+ int bytesRead;
|
|
|
+ while ((bytesRead = inputStream.read(buffer)) != -1) {
|
|
|
+ outputStream.write(buffer, 0, bytesRead);
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("文件下载成功,URL: {}, 大小: {} bytes", fileUrl, tempFile.length());
|
|
|
+ return tempFile;
|
|
|
+
|
|
|
+ } finally {
|
|
|
+ if (outputStream != null) {
|
|
|
+ try { outputStream.close(); } catch (Exception e) { }
|
|
|
+ }
|
|
|
+ if (inputStream != null) {
|
|
|
+ try { inputStream.close(); } catch (Exception e) { }
|
|
|
+ }
|
|
|
+ if (connection != null) {
|
|
|
+ connection.disconnect();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取文件后缀名
|
|
|
+ *
|
|
|
+ * @param fileUrl 文件URL
|
|
|
+ * @return 文件后缀名(包含点号,如:.jpg)
|
|
|
+ */
|
|
|
+ private String getFileSuffix(String fileUrl) {
|
|
|
+ int lastDotIndex = fileUrl.lastIndexOf('.');
|
|
|
+ int lastSlashIndex = fileUrl.lastIndexOf('/');
|
|
|
+
|
|
|
+ if (lastDotIndex > lastSlashIndex && lastDotIndex < fileUrl.length() - 1) {
|
|
|
+ return fileUrl.substring(lastDotIndex);
|
|
|
+ }
|
|
|
+
|
|
|
+ return ".jpg"; // 默认后缀
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算字节数组的MD5签名
|
|
|
+ *
|
|
|
+ * @param bytes 字节数组
|
|
|
+ * @return MD5签名(32位小写字符串)
|
|
|
+ * @throws Exception 计算失败
|
|
|
+ */
|
|
|
+ private String calculateMD5(byte[] bytes) throws Exception {
|
|
|
+ MessageDigest md = MessageDigest.getInstance("MD5");
|
|
|
+ md.update(bytes);
|
|
|
+ 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;
|
|
|
+ }
|
|
|
}
|