Jelajahi Sumber

feat: 图片上传优化

pudongliang 4 bulan lalu
induk
melakukan
e6ef8025c6

+ 11 - 0
src/main/java/com/moka/gdtauto/service/TencentAccountIdBrandImageService.java

@@ -3,6 +3,8 @@ package com.moka.gdtauto.service;
 import com.moka.gdtauto.entity.TencentAccountIdBrandImage;
 import com.moka.gdtauto.entity.TencentAccountIdBrandImage;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.baomidou.mybatisplus.extension.service.IService;
 
 
+import java.util.List;
+
 /**
 /**
  * <p>
  * <p>
  * 腾讯账户上传品牌形象记录 服务类
  * 腾讯账户上传品牌形象记录 服务类
@@ -33,4 +35,13 @@ public interface TencentAccountIdBrandImageService extends IService<TencentAccou
      * @param height    图片高度
      * @param height    图片高度
      */
      */
     void saveBrandImageId(Long accountId, String fileMd5, Long imageId, String imageUrl, Long width, Long height);
     void saveBrandImageId(Long accountId, String fileMd5, Long imageId, String imageUrl, Long width, Long height);
+
+    /**
+     * 批量查询品牌形象图片ID - 根据多个accountId和fileMd5列表
+     *
+     * @param accountIds 账户ID列表
+     * @param fileMd5s   文件MD5列表
+     * @return 匹配的品牌形象图片记录列表
+     */
+    List<TencentAccountIdBrandImage> batchGetByAccountIdsAndMd5s(List<Long> accountIds, List<String> fileMd5s);
 }
 }

+ 11 - 0
src/main/java/com/moka/gdtauto/service/TencentAccountIdImageService.java

@@ -3,6 +3,8 @@ package com.moka.gdtauto.service;
 import com.moka.gdtauto.entity.TencentAccountIdImage;
 import com.moka.gdtauto.entity.TencentAccountIdImage;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.baomidou.mybatisplus.extension.service.IService;
 
 
+import java.util.List;
+
 /**
 /**
  * <p>
  * <p>
  * 腾讯账户上传图片记录 服务类
  * 腾讯账户上传图片记录 服务类
@@ -31,4 +33,13 @@ public interface TencentAccountIdImageService extends IService<TencentAccountIdI
      * @param imageUrl  图片URL
      * @param imageUrl  图片URL
      */
      */
     void saveImageId(Long accountId, String fileMd5, Long imageId, String imageUrl);
     void saveImageId(Long accountId, String fileMd5, Long imageId, String imageUrl);
+
+    /**
+     * 批量查询图片ID - 根据多个accountId和fileMd5列表
+     *
+     * @param accountIds 账户ID列表
+     * @param fileMd5s   文件MD5列表
+     * @return 匹配的图片记录列表
+     */
+    List<TencentAccountIdImage> batchGetByAccountIdsAndMd5s(List<Long> accountIds, List<String> fileMd5s);
 }
 }

+ 11 - 0
src/main/java/com/moka/gdtauto/service/TencentAccountIdVideoIdService.java

@@ -3,6 +3,8 @@ package com.moka.gdtauto.service;
 import com.moka.gdtauto.entity.TencentAccountIdVideoId;
 import com.moka.gdtauto.entity.TencentAccountIdVideoId;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.baomidou.mybatisplus.extension.service.IService;
 
 
+import java.util.List;
+
 /**
 /**
  * <p>
  * <p>
  * 腾讯账户上传视频记录 服务类
  * 腾讯账户上传视频记录 服务类
@@ -32,4 +34,13 @@ public interface TencentAccountIdVideoIdService extends IService<TencentAccountI
      * @param coverImageId  封面图片ID
      * @param coverImageId  封面图片ID
      */
      */
     void saveVideoId(Long accountId, String fileMd5, Long videoId, String videoUrl, Long coverImageId);
     void saveVideoId(Long accountId, String fileMd5, Long videoId, String videoUrl, Long coverImageId);
+
+    /**
+     * 批量查询视频ID - 根据多个accountId和fileMd5列表
+     *
+     * @param accountIds 账户ID列表
+     * @param fileMd5s   文件MD5列表
+     * @return 匹配的视频记录列表
+     */
+    List<TencentAccountIdVideoId> batchGetByAccountIdsAndMd5s(List<Long> accountIds, List<String> fileMd5s);
 }
 }

+ 241 - 153
src/main/java/com/moka/gdtauto/service/impl/AdPlanConfServiceImpl.java

@@ -25,8 +25,16 @@ import java.util.Map;
 import java.util.concurrent.*;
 import java.util.concurrent.*;
 import java.util.stream.Collectors;
 import java.util.stream.Collectors;
 import java.util.ArrayList;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Objects;
+import java.util.Set;
 import com.moka.gdtauto.entity.AdPlan;
 import com.moka.gdtauto.entity.AdPlan;
 import com.moka.gdtauto.entity.AdPlanCreative;
 import com.moka.gdtauto.entity.AdPlanCreative;
+import com.moka.gdtauto.entity.OssFile;
+import com.moka.gdtauto.entity.TencentAccountIdImage;
+import com.moka.gdtauto.entity.TencentAccountIdBrandImage;
+import com.moka.gdtauto.entity.TencentAccountIdVideoId;
 import com.moka.gdtauto.service.AdPlanService;
 import com.moka.gdtauto.service.AdPlanService;
 import com.moka.gdtauto.service.TencentAccountIdBrandImageService;
 import com.moka.gdtauto.service.TencentAccountIdBrandImageService;
 import com.moka.gdtauto.service.TencentAccountIdImageService;
 import com.moka.gdtauto.service.TencentAccountIdImageService;
@@ -511,7 +519,7 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
             return;
             return;
         }
         }
 
 
-        
+
         int planCount = plans.size();
         int planCount = plans.size();
         conf.setPlanCount(planCount);
         conf.setPlanCount(planCount);
         int creativeCount = plans.stream().mapToInt(p -> p.getAdCreatives() == null ? 0 : p.getAdCreatives().size()).sum();
         int creativeCount = plans.stream().mapToInt(p -> p.getAdCreatives() == null ? 0 : p.getAdCreatives().size()).sum();
@@ -536,7 +544,7 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
 
 
         // 第一步:准备阶段 - 填充创意的 imageId/videoId
         // 第一步:准备阶段 - 填充创意的 imageId/videoId
         try {
         try {
-            prepareCreativeAssets(tencentAds, plans);
+            prepareCreativeAssets2(conf, tencentAds, plans);
         } catch (Exception e) {
         } catch (Exception e) {
             log.error("准备创意资产失败,confId={}", confId, e);
             log.error("准备创意资产失败,confId={}", confId, e);
             // 更新 AdPlanConf 状态为失败
             // 更新 AdPlanConf 状态为失败
@@ -614,187 +622,258 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
         log.info("异步对接腾讯接口完成,confId={}", confId);
         log.info("异步对接腾讯接口完成,confId={}", confId);
     }
     }
 
 
-    /**
-     * 准备创意资产 - 填充创意的 imageId/videoId
-     * 按 accountId 分组,先查询缓存/数据库,不存在则上传并保存
-     */
-    private void prepareCreativeAssets(TencentAds tencentAds, List<AdPlan> plans) throws Exception {
+    private void prepareCreativeAssets2(AdPlanConf conf, TencentAds tencentAds, List<AdPlan> plans) throws Exception {
         log.info("准备创意资产开始,计划数量: {}", plans.size());
         log.info("准备创意资产开始,计划数量: {}", plans.size());
+        String creativeFiles = conf.getCreativeFiles();
+        String brandImageFile = conf.getBrandImageFile();
+        List<OssFile> creativeOssFile = JSON.parseArray(creativeFiles, OssFile.class);
+        OssFile brandFile = JSON.parseObject(brandImageFile, OssFile.class);
+
+        // 第一步:收集所有 accountId
+        Set<Long> accountIds = plans.stream()
+                .map(AdPlan::getAccountId)
+                .collect(Collectors.toSet());
+        log.info("收集到 {} 个账户ID", accountIds.size());
+
+        // 收集创意文件的 fileMd5(区分图片和视频)
+        List<String> imageFileMd5List = creativeOssFile.stream()
+                .filter(f -> "image".equalsIgnoreCase(f.getFileType()))
+                .map(OssFile::getFileMd5)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+        
+        List<String> videoFileMd5List = creativeOssFile.stream()
+                .filter(f -> "video".equalsIgnoreCase(f.getFileType()))
+                .map(OssFile::getFileMd5)
+                .filter(Objects::nonNull)
+                .distinct()
+                .collect(Collectors.toList());
+
+        // 批量查询已上传的图片记录,构建双 key map: accountId -> fileMd5 -> imageId
+        Map<Long, Map<String, Long>> imageIdMap = new HashMap<>();
+        if (!imageFileMd5List.isEmpty()) {
+            List<TencentAccountIdImage> existingImages = tencentAccountIdImageService
+                    .batchGetByAccountIdsAndMd5s(new ArrayList<>(accountIds), imageFileMd5List);
+            for (TencentAccountIdImage image : existingImages) {
+                imageIdMap.computeIfAbsent(image.getAccountId(), k -> new HashMap<>())
+                        .put(image.getFileMd5(), image.getImageId());
+            }
+            log.info("从数据库查询到 {} 条已上传图片记录", existingImages.size());
+        }
 
 
-        // 按 accountId 分组
-        Map<Long, List<AdPlan>> plansByAccountId = plans.stream()
-                .collect(Collectors.groupingBy(AdPlan::getAccountId));
+        // 批量查询已上传的视频记录
+        Map<Long, Map<String, Long>> videoIdMap = new HashMap<>();
+        if (!videoFileMd5List.isEmpty()) {
+            List<TencentAccountIdVideoId> existingVideos = tencentAccountIdVideoIdService
+                    .batchGetByAccountIdsAndMd5s(new ArrayList<>(accountIds), videoFileMd5List);
+            for (TencentAccountIdVideoId video : existingVideos) {
+                videoIdMap.computeIfAbsent(video.getAccountId(), k -> new HashMap<>())
+                        .put(video.getFileMd5(), video.getVideoId());
+            }
+            log.info("从数据库查询到 {} 条已上传视频记录", existingVideos.size());
+        }
 
 
-        for (Map.Entry<Long, List<AdPlan>> entry : plansByAccountId.entrySet()) {
-            Long accountId = entry.getKey();
-            List<AdPlan> accountPlans = entry.getValue();
+        // 第二步:处理未上传的创意文件
+        for (OssFile ossFile : creativeOssFile) {
+            String fileMd5 = ossFile.getFileMd5();
+            String fileUrl = ossFile.getFileUrl();
+            String fileType = ossFile.getFileType();
+            
+            if (fileMd5 == null || fileUrl == null || fileType == null) {
+                log.warn("跳过无效的创意文件: {}", ossFile.getFileName());
+                continue;
+            }
 
 
-            log.info("处理账户 {} 的创意资产,计划数量: {}", accountId, accountPlans.size());
+            if ("image".equalsIgnoreCase(fileType)) {
+                // 处理图片类型
+                List<Long> needUploadAccountIds = accountIds.stream()
+                        .filter(accId -> !imageIdMap.getOrDefault(accId, Collections.emptyMap()).containsKey(fileMd5))
+                        .toList();
+                
+                if (!needUploadAccountIds.isEmpty()) {
+                    log.info("图片 {} 需要上传到 {} 个账户", ossFile.getFileName(), needUploadAccountIds.size());
+                    // 下载图片一次
+                    byte[] fileBytes = downloadFileToBytes(fileUrl);
+                    for (Long accountId : needUploadAccountIds) {
+                        Long imageId = uploadImageFromBytes(tencentAds, accountId, fileBytes, fileMd5, "创意图片-" + ossFile.getFileName());
+                        tencentAccountIdImageService.saveImageId(accountId, fileMd5, imageId, fileUrl);
+                        imageIdMap.computeIfAbsent(accountId, k -> new HashMap<>()).put(fileMd5, imageId);
+                        log.info("图片上传成功,accountId={}, fileMd5={}, imageId={}", accountId, fileMd5, imageId);
+                    }
+                }
+            } else if ("video".equalsIgnoreCase(fileType)) {
+                // 处理视频类型
+                List<Long> needUploadAccountIds = accountIds.stream()
+                        .filter(accId -> !videoIdMap.getOrDefault(accId, Collections.emptyMap()).containsKey(fileMd5))
+                        .toList();
+                
+                if (!needUploadAccountIds.isEmpty()) {
+                    log.info("视频 {} 需要上传到 {} 个账户", ossFile.getFileName(), needUploadAccountIds.size());
+                    for (Long accountId : needUploadAccountIds) {
+                        VideoUploadResult videoResult = uploadVideoFromUrl(tencentAds, accountId, fileUrl, fileMd5, "创意视频-" + ossFile.getFileName());
+                        tencentAccountIdVideoIdService.saveVideoId(accountId, fileMd5, videoResult.videoId(), fileUrl, videoResult.coverImageId());
+                        videoIdMap.computeIfAbsent(accountId, k -> new HashMap<>()).put(fileMd5, videoResult.videoId());
+                        log.info("视频上传成功,accountId={}, fileMd5={}, videoId={}", accountId, fileMd5, videoResult.videoId());
+                    }
+                }
+            }
+        }
 
 
-            // 收集该账户下所有创意
-            List<AdPlanCreative> allCreatives = accountPlans.stream()
-                    .filter (plan -> plan.getAdCreatives() != null)
-                    .flatMap(plan -> plan.getAdCreatives().stream())
-                    .collect(Collectors.toList());
+        // 第三步:处理 brandFile(品牌形象图片)
+        Map<Long, Map<String, Long>> brandImageIdMap = new HashMap<>();
+        if (brandFile != null && brandFile.getFileMd5() != null && brandFile.getFileUrl() != null) {
+            String brandFileMd5 = brandFile.getFileMd5();
+            String brandFileUrl = brandFile.getFileUrl();
+            
+            // 批量查询已上传的品牌形象记录
+            List<TencentAccountIdBrandImage> existingBrandImages = tencentAccountIdBrandImageService
+                    .batchGetByAccountIdsAndMd5s(new ArrayList<>(accountIds), List.of(brandFileMd5));
+            for (TencentAccountIdBrandImage image : existingBrandImages) {
+                brandImageIdMap.computeIfAbsent(image.getAccountId(), k -> new HashMap<>())
+                        .put(image.getFileMd5(), image.getImageId());
+            }
+            log.info("从数据库查询到 {} 条已上传品牌形象记录", existingBrandImages.size());
 
 
-            // 使用虚拟线程并发处理创意资产,Semaphore限制最大并发数为100
-            // TODO tencentAds不支持跨进程
-            //Semaphore semaphore = new Semaphore(20);
-            //CountDownLatch latch = new CountDownLatch(allCreatives.size());
+            // 处理未上传的品牌形象
+            List<Long> needUploadAccountIds = accountIds.stream()
+                    .filter(accId -> !brandImageIdMap.getOrDefault(accId, Collections.emptyMap()).containsKey(brandFileMd5))
+                    .toList();
             
             
-            // try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
-            //     for (AdPlanCreative creative : allCreatives) {
-            //         executor.submit(() -> {
-            //             try {
-            //                 semaphore.acquire();
-            //                 prepareSingleCreativeAsset(tencentAds, accountId, creative);
-            //             } catch (Exception e) {
-            //                 log.error("准备创意资产失败, accountId={}, creativeId={}, creativeName={}", 
-            //                         accountId, creative.getId(), creative.getCreativeName(), e);
-            //                 adPlanCreativeService.lambdaUpdate()
-            //                     .eq(AdPlanCreative::getId, creative.getId())
-            //                     .set(AdPlanCreative::getFailReason, truncateError(e.getMessage()))
-            //                     .set(AdPlanCreative::getStatus, AdPlanStatus.FAILED)
-            //                     .update();
-            //             } finally {
-            //                 semaphore.release();
-            //                 latch.countDown();
-            //             }
-            //         });
-            //     }
+            if (!needUploadAccountIds.isEmpty()) {
+                log.info("品牌形象需要上传到 {} 个账户", needUploadAccountIds.size());
+                // 获取品牌名称(从 conf 或创意中获取)
+                String brandName = getBrandNameFromPlans(plans);
+                for (Long accountId : needUploadAccountIds) {
+                    BrandUploadResult brandResult = uploadBrandImageFromUrl(tencentAds, accountId, brandFileUrl, brandFileMd5, brandName);
+                    tencentAccountIdBrandImageService.saveBrandImageId(accountId, brandFileMd5, brandResult.imageId(), brandFileUrl, brandResult.width(), brandResult.height());
+                    brandImageIdMap.computeIfAbsent(accountId, k -> new HashMap<>()).put(brandFileMd5, brandResult.imageId());
+                    log.info("品牌形象上传成功,accountId={}, fileMd5={}, brandImageId={}", accountId, brandFileMd5, brandResult.imageId());
+                }
+            }
+        }
+
+        // 第四步:给 plan.getAdCreatives() 下的创意填充 imageId/videoId/brandImageId
+        for (AdPlan plan : plans) {
+            Long accountId = plan.getAccountId();
+            List<AdPlanCreative> creatives = plan.getAdCreatives();
+            if (creatives == null) continue;
+
+            for (AdPlanCreative creative : creatives) {
+                String creativeFileMd5 = creative.getCreativeFileMd5();
+                String creativeFileType = creative.getCreativeFileType();
                 
                 
-            //     // 等待所有任务完成
-            //     latch.await();
-            // } catch (InterruptedException e) {
-            //     Thread.currentThread().interrupt();
-            //     log.error("等待创意资产准备完成被中断, accountId={}", accountId, e);
-            // }
-            for (AdPlanCreative creative : allCreatives) {
-                    try {
-                            prepareSingleCreativeAsset(tencentAds, accountId, creative);
-                        } catch (Exception e) {
-                            log.error("准备创意资产失败, accountId={}, creativeId={}, creativeName={}", 
-                                    accountId, creative.getId(), creative.getCreativeName(), e);
-                            adPlanCreativeService.lambdaUpdate()
-                                .eq(AdPlanCreative::getId, creative.getId())
-                                .set(AdPlanCreative::getFailReason, truncateError(e.getMessage()))
-                                .set(AdPlanCreative::getStatus, AdPlanStatus.FAILED)
-                                .update();
-                        } 
+                // 填充创意文件的 imageId 或 videoId
+                if (creativeFileMd5 != null && creativeFileType != null) {
+                    if ("image".equalsIgnoreCase(creativeFileType)) {
+                        Long imageId = imageIdMap.getOrDefault(accountId, Collections.emptyMap()).get(creativeFileMd5);
+                        if (imageId != null) {
+                            creative.setImageId(imageId);
+                        }
+                    } else if ("video".equalsIgnoreCase(creativeFileType)) {
+                        Long videoId = videoIdMap.getOrDefault(accountId, Collections.emptyMap()).get(creativeFileMd5);
+                        if (videoId != null) {
+                            creative.setVideoId(videoId);
+                        }
+                    }
+                }
+
+                // 填充品牌形象的 brandImageId
+                if (brandFile != null && brandFile.getFileMd5() != null) {
+                    Long brandImageId = brandImageIdMap.getOrDefault(accountId, Collections.emptyMap()).get(brandFile.getFileMd5());
+                    if (brandImageId != null) {
+                        creative.setBrandImageId(brandImageId);
+                    }
                 }
                 }
+            }
         }
         }
 
 
         log.info("准备创意资产完成");
         log.info("准备创意资产完成");
     }
     }
 
 
     /**
     /**
-     * 准备单个创意的资产
+     * 从 plans 中获取品牌名称
      */
      */
-    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;
+    private String getBrandNameFromPlans(List<AdPlan> plans) {
+        for (AdPlan plan : plans) {
+            if (plan.getAdCreatives() != null) {
+                for (AdPlanCreative creative : plan.getAdCreatives()) {
+                    if (creative.getBrandName() != null && !creative.getBrandName().isEmpty()) {
+                        return creative.getBrandName();
+                    }
+                }
+            }
         }
         }
+        return "默认品牌";
+    }
 
 
-        if ("image".equalsIgnoreCase(creativeFileType) && creativeImageId != null && creativeImageId > 0) {
-            return;
-        } else if ("video".equalsIgnoreCase(creativeFileType) && creativeVideoId != null && creativeVideoId > 0) {
-            return;
-        }
+    /**
+     * 下载文件并返回字节数组
+     */
+    private byte[] downloadFileToBytes(String fileUrl) throws Exception {
+        HttpURLConnection connection = null;
+        InputStream inputStream = null;
+        try {
+            URL url = new URL(fileUrl);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setConnectTimeout(10000);
+            connection.setReadTimeout(30000);
+            connection.setRequestMethod("GET");
+            connection.connect();
 
 
-        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);
+            int responseCode = connection.getResponseCode();
+            if (responseCode != HttpURLConnection.HTTP_OK) {
+                throw new Exception("下载文件失败,HTTP状态码: " + responseCode);
             }
             }
-            adPlanCreativeService.updateById(creative);
 
 
-        } else if ("video".equalsIgnoreCase(creativeFileType)) {
-            // 视频类型:查询或上传视频
-            if (creative.getVideoId() != null && creative.getVideoId() > 0) {
-                return;
+            inputStream = connection.getInputStream();
+            byte[] fileBytes = inputStream.readAllBytes();
+            log.info("文件下载成功,URL: {}, 大小: {} bytes", fileUrl, fileBytes.length);
+            return fileBytes;
+        } finally {
+            if (inputStream != null) {
+                try {
+                    inputStream.close();
+                } catch (IOException e) {
+                    log.warn("关闭输入流失败", e);
+                }
             }
             }
-            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());
+            if (connection != null) {
+                connection.disconnect();
             }
             }
-            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;
+    private Long uploadImageFromBytes(TencentAds tencentAds, Long accountId, byte[] imageBytes, String fileMd5, String description)
+            throws Exception {
+        String base64Image = Base64.getEncoder().encodeToString(imageBytes);
+
+        ImagesAddResponseData response = tencentAds.images()
+                .imagesAdd(
+                        "UPLOAD_TYPE_BYTES",
+                        fileMd5,
+                        accountId,
+                        null,
+                        null,
+                        base64Image,
+                        null,
+                        description,
+                        null,
+                        null,
+                        null
+                );
+
+        if (response == null || response.getImageId() == null) {
+            throw new Exception("图片上传返回结果为空");
         }
         }
 
 
-        // 已经有 brandImageId 则跳过
-        if (creative.getBrandImageId() != null && creative.getBrandImageId() > 0) {
-            return;
-        }
+        log.info("图片上传成功,imageId: {}, 宽度: {}px, 高度: {}px",
+                response.getImageId(), response.getImageWidth(), response.getImageHeight());
 
 
-        // 查询缓存/数据库
-        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);
+        return Long.parseLong(response.getImageId());
     }
     }
 
 
     /**
     /**
@@ -842,6 +921,15 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
                 if (creative.getCreativeId() > 0 || creative.getStatus() == AdPlanStatus.SUCCESS)
                 if (creative.getCreativeId() > 0 || creative.getStatus() == AdPlanStatus.SUCCESS)
                     continue;
                     continue;
 
 
+                // 判定creative的imageId或者videoId没有值或者=0,直接判定为失败
+                if (creative.getImageId() == 0 || creative.getVideoId() == 0) {
+                    creative.setStatus(AdPlanStatus.FAILED);
+                    creative.setFailReason("创意图片或视频未上传成功");
+                    adPlanCreativeService.updateById(creative);
+                    continue;
+                }
+                
+
                 // 更新状态为执行中
                 // 更新状态为执行中
                 creative.setStatus(AdPlanStatus.IN_PROGRESS);
                 creative.setStatus(AdPlanStatus.IN_PROGRESS);
                 creative.setAdgroupId(plan.getAdgroupId());
                 creative.setAdgroupId(plan.getAdgroupId());
@@ -899,7 +987,7 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
         if (creative.getVideoId() > 0) {
         if (creative.getVideoId() > 0) {
              components.setVideo(buildVideoComponents(creative.getVideoId()));
              components.setVideo(buildVideoComponents(creative.getVideoId()));
         }
         }
-       
+
 
 
         // 品牌形象 (固定品牌)
         // 品牌形象 (固定品牌)
         components.setBrand(buildBrandComponents(creative.getBrandImageId(), creative.getBrandName()));
         components.setBrand(buildBrandComponents(creative.getBrandImageId(), creative.getBrandName()));

+ 13 - 0
src/main/java/com/moka/gdtauto/service/impl/TencentAccountIdBrandImageServiceImp.java

@@ -10,6 +10,8 @@ import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 
 
 import java.time.LocalDateTime;
 import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
 
 
 /**
 /**
  * <p>
  * <p>
@@ -47,4 +49,15 @@ public class TencentAccountIdBrandImageServiceImp extends ServiceImpl<TencentAcc
         record.setCreateTime(LocalDateTime.now());
         record.setCreateTime(LocalDateTime.now());
         this.baseMapper.insertIgnore(record);
         this.baseMapper.insertIgnore(record);
     }
     }
+
+    @Override
+    public List<TencentAccountIdBrandImage> batchGetByAccountIdsAndMd5s(List<Long> accountIds, List<String> fileMd5s) {
+        if (accountIds == null || accountIds.isEmpty() || fileMd5s == null || fileMd5s.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return this.lambdaQuery()
+                .in(TencentAccountIdBrandImage::getAccountId, accountIds)
+                .in(TencentAccountIdBrandImage::getFileMd5, fileMd5s)
+                .list();
+    }
 }
 }

+ 13 - 0
src/main/java/com/moka/gdtauto/service/impl/TencentAccountIdImageServiceImp.java

@@ -10,6 +10,8 @@ import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 
 
 import java.time.LocalDateTime;
 import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
 
 
 /**
 /**
  * <p>
  * <p>
@@ -45,4 +47,15 @@ public class TencentAccountIdImageServiceImp extends ServiceImpl<TencentAccountI
         record.setCreateTime(LocalDateTime.now());
         record.setCreateTime(LocalDateTime.now());
         this.baseMapper.insertIgnore(record);
         this.baseMapper.insertIgnore(record);
     }
     }
+
+    @Override
+    public List<TencentAccountIdImage> batchGetByAccountIdsAndMd5s(List<Long> accountIds, List<String> fileMd5s) {
+        if (accountIds == null || accountIds.isEmpty() || fileMd5s == null || fileMd5s.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return this.lambdaQuery()
+                .in(TencentAccountIdImage::getAccountId, accountIds)
+                .in(TencentAccountIdImage::getFileMd5, fileMd5s)
+                .list();
+    }
 }
 }

+ 13 - 0
src/main/java/com/moka/gdtauto/service/impl/TencentAccountIdVideoIdServiceImp.java

@@ -10,6 +10,8 @@ import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 import org.springframework.stereotype.Service;
 
 
 import java.time.LocalDateTime;
 import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
 
 
 /**
 /**
  * <p>
  * <p>
@@ -46,4 +48,15 @@ public class TencentAccountIdVideoIdServiceImp extends ServiceImpl<TencentAccoun
         record.setCreateTime(LocalDateTime.now());
         record.setCreateTime(LocalDateTime.now());
         this.baseMapper.insertIgnore(record);
         this.baseMapper.insertIgnore(record);
     }
     }
+
+    @Override
+    public List<TencentAccountIdVideoId> batchGetByAccountIdsAndMd5s(List<Long> accountIds, List<String> fileMd5s) {
+        if (accountIds == null || accountIds.isEmpty() || fileMd5s == null || fileMd5s.isEmpty()) {
+            return Collections.emptyList();
+        }
+        return this.lambdaQuery()
+                .in(TencentAccountIdVideoId::getAccountId, accountIds)
+                .in(TencentAccountIdVideoId::getFileMd5, fileMd5s)
+                .list();
+    }
 }
 }