pudongliang 3 місяців тому
батько
коміт
fcc2f154f4
19 змінених файлів з 519 додано та 503 видалено
  1. 2 0
      .vscode/settings.json
  2. 7 11
      portal/src/views/ad-plan-conf/index.vue
  3. 30 6
      portal/src/views/oss-file/index.vue
  4. 1 1
      src/main/java/com/moka/gdtauto/client/TencentAdsApiClientFactory.java
  5. 4 2
      src/main/java/com/moka/gdtauto/controller/OssFileStatisticsController.java
  6. 168 201
      src/main/java/com/moka/gdtauto/service/AdPlanCreativeDailyReportService.java
  7. 184 221
      src/main/java/com/moka/gdtauto/service/AdPlanDailyReportService.java
  8. 3 1
      src/main/java/com/moka/gdtauto/service/OssFileStatisticsService.java
  9. 9 7
      src/main/java/com/moka/gdtauto/service/TencentAdsAccountService.java
  10. 10 8
      src/main/java/com/moka/gdtauto/service/impl/AccountGroupServiceImpl.java
  11. 8 6
      src/main/java/com/moka/gdtauto/service/impl/AdPlanConfServiceImpl.java
  12. 28 3
      src/main/java/com/moka/gdtauto/service/impl/OssFileStatisticsServiceImp.java
  13. 9 7
      src/main/java/com/moka/gdtauto/service/impl/TencentAccountServiceImpl.java
  14. 12 7
      src/main/java/com/moka/gdtauto/service/impl/TencentConversionServiceImpl.java
  15. 1 1
      src/main/java/com/moka/gdtauto/service/impl/TencentImagesServiceImpl.java
  16. 15 13
      src/main/java/com/moka/gdtauto/service/impl/TencentVideoServiceImpl.java
  17. 5 4
      src/main/java/com/moka/gdtauto/task/AdAutoTask.java
  18. 2 2
      src/main/resources/mapper/AdPlanCreativeMapper.xml
  19. 21 2
      src/test/java/com/moka/gdtauto/service/AdPlanDailyReportSyncTest.java

+ 2 - 0
.vscode/settings.json

@@ -2,6 +2,8 @@
     "java.configuration.updateBuildConfiguration": "automatic",
     "java.compile.nullAnalysis.mode": "automatic",
     "cSpell.enabled": false,
+     "java.referencesCodeLens.enabled": true,
+    "references.preferredLocation": "peek", 
     // 保存时自动组织导入(删除未使用的import + 排序)
     "java.saveActions.organizeImports": true,
     // 在变量视图中显示对象的toString()结果

+ 7 - 11
portal/src/views/ad-plan-conf/index.vue

@@ -259,13 +259,11 @@
             />
           </el-select>
         </el-form-item>
-         <el-form-item label="定向模版IDs" prop="targets">
+         <el-form-item label="定向模版ID" prop="targets">
           <el-input
           :disabled="valueDisabled"
             v-model="formData.targets"
-            type="textarea"
-            :rows="2"
-            placeholder="多个定向ID用逗号分隔,例如:1,2,3"
+            placeholder="定向模版ID"
           />
           <el-collapse class="mt-1 text-xs text-gray-500" >
             <el-collapse-item title="定向模版ID配置说明(新模版【2】)">
@@ -290,13 +288,11 @@
             </el-collapse-item>
           </el-collapse>
         </el-form-item>
-         <el-form-item label="版位模版IDs" prop="sites">
+         <el-form-item label="版位模版ID" prop="sites">
           <el-input
           :disabled="valueDisabled"
             v-model="formData.sites"
-            type="textarea"
-            :rows="2"
-            placeholder="多个版位ID用逗号分隔,例如:1,2,3"
+            placeholder="版位模版ID"
           />
           <el-collapse class="mt-1 text-xs text-gray-500" >
             <el-collapse-item title="版位模版ID配置说明(新模版【11】)">
@@ -1092,7 +1088,7 @@ const handleCreate = async () => {
   let accountGroupId = toRaw(accountGroupList.value)[0].id
   
 
-  const site = Math.floor(Math.random() * 11) + 1
+  // const site = Math.floor(Math.random() * 11) + 1
   dialogTitle.value = '创建配置'
   editingId.value = null
   formData.value = {
@@ -1102,8 +1098,8 @@ const handleCreate = async () => {
     bidAmount: 150,
     deepConversionWorthRate: 50.00,
     conversionId: null,
-    targets: '1',
-    sites: site + '',
+    targets: '2',
+    sites: '11',
     wxCorpid: '',
     dramaId: null,
     dramaAmount: 0,

+ 30 - 6
portal/src/views/oss-file/index.vue

@@ -244,29 +244,31 @@
         <el-table
           :data="statisticsList"
           v-loading="statisticsLoading"
+          @sort-change="handleStatisticsSortChange"
+          :default-sort="{ prop: 'cost', order: 'descending' }"
           style="width: 100%"
           :header-cell-style="{ background: '#f9fafb', color: '#374151', fontWeight: '600' }">
-          <el-table-column prop="date" label="日期" width="120">
+          <el-table-column prop="date" label="日期" width="120" sortable="custom">
             <template #default="{ row }">
               {{ row.date.substring(0, 10) }}
             </template>
           </el-table-column>
-          <el-table-column prop="viewCount" label="曝光次数" width="120">
+          <el-table-column prop="viewCount" label="曝光次数" width="120" sortable="custom">
             <template #default="{ row }">
               {{ formatNumber(row.viewCount) }}
             </template>
           </el-table-column>
-          <el-table-column prop="validClickCount" label="点击次数" width="120">
+          <el-table-column prop="validClickCount" label="点击次数" width="120" sortable="custom">
             <template #default="{ row }">
               {{ formatNumber(row.validClickCount) }}
             </template>
           </el-table-column>
-          <el-table-column prop="ctr" label="点击率" width="120">
+          <el-table-column prop="ctr" label="点击率" width="120" sortable="custom">
             <template #default="{ row }">
               <el-tag :type="getCtrTagType(row.ctr)" size="small">{{ formatCtr(row.ctr) }}</el-tag>
             </template>
           </el-table-column>
-          <el-table-column prop="cost" label="花费(元)" min-width="120">
+          <el-table-column prop="cost" label="花费(元)" min-width="120" sortable="custom">
             <template #default="{ row }">
               {{ formatCost(row.cost) }}
             </template>
@@ -436,6 +438,12 @@ const statisticsPagination = reactive({
   total: 0
 })
 
+// 统计排序参数
+const statisticsSortParams = reactive({
+  sortField: 'cost',
+  sortOrder: 'descending'
+})
+
 // 验证普通图片
 const validateNormalImage = (file) => {
   return new Promise((resolve, reject) => {
@@ -762,7 +770,9 @@ const loadStatisticsList = async () => {
       size: statisticsPagination.size,
       fileMd5: currentFileMd5.value,
       startDate: statisticsQuery.dateRange && statisticsQuery.dateRange.length > 0 ? statisticsQuery.dateRange[0] : '',
-      endDate: statisticsQuery.dateRange && statisticsQuery.dateRange.length > 0 ? statisticsQuery.dateRange[1] : ''
+      endDate: statisticsQuery.dateRange && statisticsQuery.dateRange.length > 0 ? statisticsQuery.dateRange[1] : '',
+      sortField: statisticsSortParams.sortField,
+      sortOrder: statisticsSortParams.sortOrder
     })
     if (res.success) {
       statisticsList.value = res.data.records
@@ -787,9 +797,23 @@ const handleStatisticsQuery = () => {
 // 统计重置
 const handleStatisticsReset = () => {
   statisticsQuery.dateRange = []
+  statisticsSortParams.sortField = 'cost'
+  statisticsSortParams.sortOrder = 'descending'
   handleStatisticsQuery()
 }
 
+// 统计排序变化
+const handleStatisticsSortChange = ({ prop, order }) => {
+  if (order) {
+    statisticsSortParams.sortField = prop
+    statisticsSortParams.sortOrder = order
+  } else {
+    statisticsSortParams.sortField = 'cost'
+    statisticsSortParams.sortOrder = 'descending'
+  }
+  loadStatisticsList()
+}
+
 // 统计分页变化
 const handleStatisticsSizeChange = (size) => {
   statisticsPagination.size = size

+ 1 - 1
src/main/java/com/moka/gdtauto/client/TencentAdsApiClientFactory.java

@@ -76,7 +76,7 @@ public class TencentAdsApiClientFactory {
      * @return TencentAds实例
      * @throws Exception 当获取token失败时抛出
      */
-    public TencentAds getTencentAdsWithoutUserToken(String jobNumber, Long orgAccountId) throws Exception {
+    public TencentAds getTencentAdsWithoutUserToken(Long orgAccountId) throws Exception {
         // 获取账户的访问令牌
         TencentAdsAuth auth = authService.getByAccountId(orgAccountId);
         if (auth == null) {

+ 4 - 2
src/main/java/com/moka/gdtauto/controller/OssFileStatisticsController.java

@@ -43,9 +43,11 @@ public class OssFileStatisticsController {
             @Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Long size,
             @Parameter(description = "文件MD5", required = true) @RequestParam String fileMd5,
             @Parameter(description = "开始日期,格式:yyyy-MM-dd") @RequestParam(required = false) String startDate,
-            @Parameter(description = "结束日期,格式:yyyy-MM-dd") @RequestParam(required = false) String endDate) {
+            @Parameter(description = "结束日期,格式:yyyy-MM-dd") @RequestParam(required = false) String endDate,
+            @Parameter(description = "排序字段(如: date, viewCount, validClickCount, ctr, cost)") @RequestParam(required = false) String sortField,
+            @Parameter(description = "排序方式(ascending/descending)") @RequestParam(required = false) String sortOrder) {
         try {
-            Page<OssFileStatistics> result = ossFileStatisticsService.listStatisticsByMd5AndDate(page, size, fileMd5, startDate, endDate);
+            Page<OssFileStatistics> result = ossFileStatisticsService.listStatisticsByMd5AndDate(page, size, fileMd5, startDate, endDate, sortField, sortOrder);
             return Result.success(result);
         } catch (Exception e) {
             log.error("查询文件统计失败", e);

+ 168 - 201
src/main/java/com/moka/gdtauto/service/AdPlanCreativeDailyReportService.java

@@ -6,32 +6,27 @@ import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentHashMap;
+import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.stream.Collectors;
 
 import org.springframework.scheduling.annotation.Async;
 import org.springframework.stereotype.Service;
 
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.google.common.collect.Lists;
 import com.moka.gdtauto.client.TencentAdsApiClientFactory;
 import com.moka.gdtauto.common.Constant;
-import com.moka.gdtauto.entity.AdPlanConf;
 import com.moka.gdtauto.entity.AdPlanCreative;
 import com.moka.gdtauto.mapper.AdPlanConfMapper;
 import com.moka.gdtauto.mapper.AdPlanCreativeMapper;
 import com.tencent.ads.exception.TencentAdsResponseException;
-import com.tencent.ads.model.v3.DailyReportsFilteringStruct;
 import com.tencent.ads.model.v3.DailyReportsGetResponseData;
-import com.tencent.ads.model.v3.FilterOperator;
 import com.tencent.ads.model.v3.ReportDateRange;
 import com.tencent.ads.model.v3.ReportStruct;
 import com.tencent.ads.model.v3.TimeLine;
 import com.tencent.ads.v3.TencentAds;
 
+import jakarta.validation.constraints.NotNull;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 
@@ -50,6 +45,7 @@ public class AdPlanCreativeDailyReportService {
     private final TencentAdsApiClientFactory clientFactory;
     private final AdPlanConfMapper adPlanConfMapper;
     private final AdPlanCreativeMapper adPlanCreativeMapper;
+    private final AdPlanCreativeService adplanCreativeService;
 
     /** REQUEST_TIME 口径同步字段 */
     private static final List<String> REQ_FIELDS = Arrays.asList(
@@ -62,9 +58,6 @@ public class AdPlanCreativeDailyReportService {
             "adgroup_id",  "dynamic_creative_id", "date", "income_val_1", "income_roi_1"
     );
 
-    private static final int PAGE_SIZE = 100;
-    private static final int ADGROUP_BATCH_LIMIT = 100;
-
     @Async
     public void syncDailyReport(Long confId) {
         LocalDateTime start = LocalDateTime.now();
@@ -74,7 +67,7 @@ public class AdPlanCreativeDailyReportService {
     }
 
     /**
-     * 同步 REQUEST_TIME 口径日报数据到 ad_plan 表
+     * 同步 REQUEST_TIME 口径日报数据到 ad_plan_creative
      *
      * @param startTime 开始时间
      * @param endTime   结束时间
@@ -85,7 +78,7 @@ public class AdPlanCreativeDailyReportService {
     }
 
     /**
-     * 同步 REPORTING_TIME 口径日报数据到 ad_plan 表
+     * 同步 REPORTING_TIME 口径日报数据到 ad_plan_creative
      *
      * @param startTime 开始时间
      * @param endTime   结束时间
@@ -103,219 +96,137 @@ public class AdPlanCreativeDailyReportService {
      * @param timeLine    时间口径
      * @param isReqTime   是否为 REQUEST_TIME 口径(决定更新哪些字段)
      */
-    private void doSyncDailyReport(Long confId, LocalDateTime startTime, LocalDateTime endTime, String timeLine, boolean isReqTime) {
-        // 1. 分页查询 create_time 在范围内的 adPlanConf
-        int confPageNum = 1;
-        int totalUpdated = 0;
-        Map<Long, Integer> confUpdatedCount = new ConcurrentHashMap<>();
-
-        while (true) {
-            LambdaQueryWrapper<AdPlanConf> confWrapper = new LambdaQueryWrapper<>();
-            confWrapper
-                    .eq(confId != null, AdPlanConf::getId, confId)
-                    .eq(AdPlanConf::getDeleted, 0)
-                    .orderByDesc(AdPlanConf::getId);
-            Page<AdPlanConf> confPage = adPlanConfMapper.selectPage(new Page<>(confPageNum, PAGE_SIZE), confWrapper);
-            List<AdPlanConf> confList = confPage.getRecords();
-
-            if (confList.isEmpty()) {
-                break;
-            }
-
-            // 2. 每个 conf 启动一个线程处理
-            List<CompletableFuture<Void>> futures = confList.stream()
-                    .map(conf -> CompletableFuture.runAsync(() -> {
-                        try {
-                            int updated = processConf(conf, startTime, endTime, timeLine, isReqTime);
-                            confUpdatedCount.put(conf.getId(), updated);
-                        } catch (Exception e) {
-                            log.error("[doSyncDailyReport] 处理 confId={} 失败", conf.getId(), e);
-                        }
-                    }))
-                    .collect(Collectors.toList());
-
-            // 等待所有线程完成
-            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
-
-            if (!confPage.hasNext()) {
-                break;
-            }
-            confPageNum++;
+    private void doSyncDailyReport(@NotNull Long confId, LocalDateTime startTime, LocalDateTime endTime, String timeLine, boolean isReqTime) {
+        List<AdPlanCreative> adPlanCreatives = adplanCreativeService.lambdaQuery()
+        .select(AdPlanCreative::getAccountId, AdPlanCreative::getCreativeId)
+        .gt(AdPlanCreative::getCreativeId, 0)
+        .eq(AdPlanCreative::getAdPlanConfId, confId)
+        .list();
+        Set<Long> creativeIdSet = adPlanCreatives.stream().map(AdPlanCreative::getCreativeId).collect(Collectors.toSet());
+        Set<Long> accountIdSet = adPlanCreatives.stream().map(AdPlanCreative::getAccountId).collect(Collectors.toSet());
+        if (creativeIdSet.isEmpty()) {
+            log.warn("[doSyncDailyReport] 没有创意数据,不进行同步");
+            return;
         }
-
-        totalUpdated = confUpdatedCount.values().stream().mapToInt(Integer::intValue).sum();
-        log.info("[doSyncDailyReport] 同步完成 timeLine={} totalUpdated={}", timeLine, totalUpdated);
-    }
-
-    /**
-     * 处理单个 AdPlanConf
-     */
-    private int processConf(AdPlanConf conf, LocalDateTime startTime, LocalDateTime endTime, String timeLine, boolean isReqTime) throws Exception {
-        String jobNumber = conf.getJobNumber();
-        TencentAds tencentAds = clientFactory.getTencentAds(jobNumber, Constant.MAIN_ORG_ACCOUNT_ID);
-
         String startStr = startTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
         String endStr = endTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
         ReportDateRange dateRange = new ReportDateRange();
         dateRange.setStartDate(startStr);
         dateRange.setEndDate(endStr);
 
-        log.info("[广告日报] start={} end={} timeLine={} isReqTime={}", startStr, endStr, timeLine, isReqTime);
-
         List<String> groupBy = Arrays.asList("dynamic_creative_id", "date");
         List<String> fields = isReqTime ? REQ_FIELDS : REP_FIELDS;
 
-        int totalUpdated = 0;
-        int planPageNum = 1;
-
-        // 3. 分页查询该 conf 下的所有 adPlan
-        while (true) {
-            LambdaQueryWrapper<AdPlanCreative> planWrapper = new LambdaQueryWrapper<>();
-            planWrapper.eq(AdPlanCreative::getAdPlanConfId, conf.getId())
-                    .isNotNull(AdPlanCreative::getCreativeId)
-                    .gt(AdPlanCreative::getCreativeId, 0)
-                    .eq(AdPlanCreative::getDeleted, 0)
-                    .orderByAsc(AdPlanCreative::getId);
-            Page<AdPlanCreative> planPage = adPlanCreativeMapper.selectPage(new Page<>(planPageNum, PAGE_SIZE), planWrapper);
-            List<AdPlanCreative> plans = planPage.getRecords();
-
-            if (plans.isEmpty()) {
-                break;
-            }
-
-            // 4. 按 accountId 分组,每 100 个 adgroup_id 一批调用 API
-            Map<Long, List<AdPlanCreative>> groupByAccount = plans.stream()
-                    .filter(p -> p.getAccountId() != null && p.getAdgroupId() != null)
-                    .collect(Collectors.groupingBy(AdPlanCreative::getAccountId));
-
-            for (Map.Entry<Long, List<AdPlanCreative>> entry : groupByAccount.entrySet()) {
-                Long accountId = entry.getKey();
-                List<AdPlanCreative> plansOfAccount = entry.getValue();
-
-                // 按 100 条分批
-                int batchCount = (int) Math.ceil((double) plansOfAccount.size() / ADGROUP_BATCH_LIMIT);
-                for (int i = 0; i < batchCount; i++) {
-                    int fromIdx = i * ADGROUP_BATCH_LIMIT;
-                    int toIdx = Math.min(fromIdx + ADGROUP_BATCH_LIMIT, plansOfAccount.size());
-                    List<AdPlanCreative> batch = plansOfAccount.subList(fromIdx, toIdx);
-
-                    try {
-                        int updated = syncBatch(tencentAds, accountId, batch, dateRange, groupBy, fields, timeLine, isReqTime);
-                        totalUpdated += updated;
-                    } catch (Exception e) {
-                        log.error("[processConf] 同步失败 accountId={} confId={} batch={}", accountId, conf.getId(), i, e);
-                    }
-                }
-            }
-
-            if (!planPage.hasNext()) {
-                break;
+        try {
+            TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken(Constant.MAIN_ORG_ACCOUNT_ID);
+            for (Long accountId : accountIdSet) {
+                syncBatch(tencentAds, accountId, creativeIdSet, dateRange, groupBy, fields, timeLine, isReqTime);
             }
-            planPageNum++;
+        } catch (Exception e) {
+            log.error("[doSyncDailyReport] 同步失败 confId={} startTime={} endTime={} timeLine={} isReqTime={}", confId, startTime, endTime, timeLine, isReqTime, e);
         }
-
-        return totalUpdated;
     }
 
     /**
+     * 处理单个 AdPlanConf
+     */
+
+
+    /**
      * 批量同步单批 adPlan
+     * 支持分页循环获取所有数据,每页数据立即处理并更新
      */
-    private int syncBatch(TencentAds tencentAds, Long accountId, List<AdPlanCreative> batch,
+    private int syncBatch(TencentAds tencentAds, Long accountId, Set<Long> creativeIdSet,
                           ReportDateRange dateRange, List<String> groupBy, List<String> fields,
                           String timeLine, boolean isReqTime) throws Exception {
+        // 用于跟踪已处理的creativeId,避免重复处理
+        Set<Long> processedCreativeIds = new java.util.HashSet<>();
+        Long page = 1L;
+        Long pageSize = 2000L;
+        boolean hasMore = true;
+        int totalUpdated = 0;
 
-        // 构建 filtering: adgroup_id IN [...]
-        List<String> creativeIdValues = batch.stream()
-                .map(p -> String.valueOf(p.getCreativeId()))
-                .collect(Collectors.toList());
+        // 分页循环获取所有数据,每页立即处理
+        while (hasMore) {
+            DailyReportsGetResponseData response = null;
+            final Long currentPage = page;
+            try {
+                response = tencentAds.dailyReports().dailyReportsGet(
+                        "REPORT_LEVEL_DYNAMIC_CREATIVE",
+                        dateRange,
+                        groupBy,
+                        fields,
+                        accountId,
+                        null,
+                        null,  // orderBy
+                        timeLine,
+                        currentPage,
+                        pageSize,
+                        null   // modularDetailMode
+                );
+            } catch (TencentAdsResponseException e) {
+                // 11016 Application has reached the request limit per second.
+                // 应用程序已达到每秒的请求限制,请稍后再试
+                // 11017 Application has reached the request limit per minute.
+                // 应用请求已达分钟频次上限,请登陆开发者后台查看当前接口对应分配的频次,并在要求范围内请求接口。
+                if (e.getCode() == 11016) {
+                    log.warn("请求达到每秒限制,请稍后再试");
+                    TimeUnit.SECONDS.sleep(1);
+                } else if (e.getCode() == 11017) {
+                    log.warn("请求达到每分钟限制,请稍后再试");
+                    // 计算出还剩多少时间
+                    LocalDateTime now = LocalDateTime.now();
+                    // the second-of-minute, from 0 to 59
+                    int second = now.getSecond();
+                    int sleepSeconds = (59 - second);
+                    log.warn("请求达到每分钟限制,sleep:{} s", sleepSeconds);
+                    TimeUnit.SECONDS.sleep(sleepSeconds);
+                }
+                // 重试当前页
+                continue;
+            }
 
-        DailyReportsFilteringStruct filter = new DailyReportsFilteringStruct();
-        filter.setField("dynamic_creative_id");
-        filter.setOperator(FilterOperator.IN);
-        filter.setValues(creativeIdValues);
+            if (response == null || response.getList() == null) {
+                log.warn("[syncBatch] accountId={} page={} 未返回日报数据", accountId, currentPage);
+                break;
+            }
 
-        List<DailyReportsFilteringStruct> filtering = new ArrayList<>();
-        filtering.add(filter);
+            List<ReportStruct> pageList = response.getList();
+            log.info("[syncBatch] accountId={} page={} 获取 {} 条数据", accountId, currentPage, pageList.size());
 
-        // 调用 API
-        DailyReportsGetResponseData response = null;
-        try {
-           response = tencentAds.dailyReports().dailyReportsGet(
-                "REPORT_LEVEL_DYNAMIC_CREATIVE",
-                dateRange,
-                groupBy,
-                fields,
-                accountId,
-                filtering,
-                null,  // orderBy
-                timeLine,
-                1L,
-                (long) ADGROUP_BATCH_LIMIT,
-                null   // modularDetailMode
-        );
-        } catch (TencentAdsResponseException e) {
-            // 11016 Application has reached the request limit per second.
-            // 应用程序已达到每秒的请求限制,请稍后再试
-            // 11017 Application has reached the request limit per minute.
-            // 应用请求已达分钟频次上限,请登陆开发者后台查看当前接口对应分配的频次,并在要求范围内请求接口。
-            if (e.getCode() == 11016) {
-                log.warn("请求达到每秒限制,请稍后再试");
-                TimeUnit.SECONDS.sleep(1);
-            } else if (e.getCode() == 11017) {
-                log.warn("请求达到每分钟限制,请稍后再试");
-                // 计算出还剩多少时间
-                LocalDateTime now = LocalDateTime.now();
-                // the second-of-minute, from 0 to 59
-                int second = now.getSecond();
-                int sleepSeconds = (59 - second);
-                log.warn("请求达到每分钟限制,sleep:{} s", sleepSeconds);
-                TimeUnit.SECONDS.sleep(sleepSeconds);
+            // 立即处理当前页数据
+            if (!pageList.isEmpty()) {
+                int updated = processAndUpdatePage(pageList, accountId, creativeIdSet, processedCreativeIds, isReqTime);
+                totalUpdated += updated;
             }
-            syncBatch(tencentAds, accountId, batch, dateRange, groupBy, fields, timeLine, isReqTime);
-        }
 
-        if (response == null) {
-            log.warn("[syncBatch] accountId={} 未返回日报数据", accountId);
-            return 0;
+            // 判断是否有更多数据:当前页数据量小于pageSize表示已到最后一页
+            if (pageList.size() < pageSize) {
+                hasMore = false;
+            } else {
+                page++;
+            }
         }
 
-        // 构建更新数据
-        List<AdPlanCreative> toUpdate = new ArrayList<>();
-        if (response.getList() != null) {
-            for (ReportStruct rs : response.getList()) {
-                if (rs.getAdgroupId() == null) {
-                    continue;
-                }
-                AdPlanCreative update = new AdPlanCreative();
-                update.setCreativeId(rs.getDynamicCreativeId());
-                if (isReqTime) {
-                    // REQUEST_TIME 口径:更新 cost, view_count, valid_click_count, ctr, conversions_cost, req_income_val_1, req_income_roi_1
-                    update.setCost(rs.getCost());
-                    update.setViewCount(rs.getViewCount());
-                    update.setValidClickCount(rs.getValidClickCount());
-                    update.setCtr(d2b(rs.getCtr()));
-                    update.setConversionsCost(rs.getConversionsCost());
-                    update.setReqIncomeVal1(rs.getIncomeVal1());
-                    update.setReqIncomeRoi1(d2b(rs.getIncomeRoi1()));
-                    update.setScanFollowUserCost(rs.getScanFollowUserCost());
-                } else {
-                    // REPORTING_TIME 口径:更新 rep_income_val_1, rep_income_roi_1
-                    update.setRepIncomeVal1(rs.getIncomeVal1());
-                    update.setRepIncomeRoi1(d2b(rs.getIncomeRoi1()));
-                }
-                update.setUpdateTime(LocalDateTime.now());
-                toUpdate.add(update);
-            }   
-        }
+        // 所有分页完成后,对无报表数据的creativeId填充0值
+        int zeroFilledCount = fillZeroForMissingCreatives(accountId, creativeIdSet, processedCreativeIds, isReqTime);
+        totalUpdated += zeroFilledCount;
 
-        //无报表数据的,填充0值
-        Map<Long, AdPlanCreative> updateMap = toUpdate.stream().collect(Collectors.toMap(AdPlanCreative::getCreativeId, p -> p));
-        batch.forEach(p -> {
-            if (!updateMap.containsKey(p.getCreativeId())) {
-                AdPlanCreative update = new AdPlanCreative();
-                update.setAdgroupId(p.getAdgroupId());
+        log.info("[syncBatch] accountId={} 总共更新 {} 条 timeLine={}", accountId, totalUpdated, timeLine);
+        return totalUpdated;
+    }
 
-                if (isReqTime) { 
+    /**
+     * 对无报表数据的creativeId填充0值
+     */
+    private int fillZeroForMissingCreatives(Long accountId, Set<Long> creativeIdSet,
+                                            Set<Long> processedCreativeIds, boolean isReqTime) {
+        List<AdPlanCreative> zeroUpdates = new ArrayList<>();
+        creativeIdSet.forEach(creativeId -> {
+            if (!processedCreativeIds.contains(creativeId)) {
+                AdPlanCreative update = new AdPlanCreative();
+                update.setCreativeId(creativeId);
+                if (isReqTime) {
                     update.setCost(0L);
                     update.setViewCount(0L);
                     update.setValidClickCount(0L);
@@ -329,22 +240,78 @@ public class AdPlanCreativeDailyReportService {
                     update.setRepIncomeRoi1(BigDecimal.ZERO);
                 }
                 update.setUpdateTime(LocalDateTime.now());
-                toUpdate.add(update);
+                zeroUpdates.add(update);
             }
         });
 
+        if (zeroUpdates.isEmpty()) {
+            return 0;
+        }
+
+        // 批量更新0值
+        if (isReqTime) {
+            Lists.partition(zeroUpdates, 200).forEach(batch -> adPlanCreativeMapper.batchUpdateReqDailyReport(batch));
+        } else {
+            Lists.partition(zeroUpdates, 200).forEach(batch -> adPlanCreativeMapper.batchUpdateRepDailyReport(batch));
+        }
+
+        log.info("[fillZeroForMissingCreatives] accountId={} 填充0值 {} 条", accountId, zeroUpdates.size());
+        return zeroUpdates.size();
+    }
+
+    /**
+     * 处理单页数据并更新数据库
+     */
+    private int processAndUpdatePage(List<ReportStruct> pageList, Long accountId, Set<Long> creativeIdSet,
+                                     Set<Long> processedCreativeIds, boolean isReqTime) {
+        // 构建更新数据
+        List<AdPlanCreative> toUpdate = new ArrayList<>();
+        for (ReportStruct rs : pageList) {
+            if (rs.getAdgroupId() == null) {
+                continue;
+            }
+            Long creativeId = rs.getDynamicCreativeId();
+            // 跳过已处理的creativeId
+            if (processedCreativeIds.contains(creativeId)) {
+                continue;
+            }
+            processedCreativeIds.add(creativeId);
+
+            AdPlanCreative update = new AdPlanCreative();
+            update.setCreativeId(creativeId);
+            if (isReqTime) {
+                // REQUEST_TIME 口径:更新 cost, view_count, valid_click_count, ctr, conversions_cost, req_income_val_1, req_income_roi_1
+                update.setCost(rs.getCost());
+                update.setViewCount(rs.getViewCount());
+                update.setValidClickCount(rs.getValidClickCount());
+                update.setCtr(d2b(rs.getCtr()));
+                update.setConversionsCost(rs.getConversionsCost());
+                update.setReqIncomeVal1(rs.getIncomeVal1());
+                update.setReqIncomeRoi1(d2b(rs.getIncomeRoi1()));
+                update.setScanFollowUserCost(rs.getScanFollowUserCost());
+            } else {
+                // REPORTING_TIME 口径:更新 rep_income_val_1, rep_income_roi_1
+                update.setRepIncomeVal1(rs.getIncomeVal1());
+                update.setRepIncomeRoi1(d2b(rs.getIncomeRoi1()));
+            }
+            update.setUpdateTime(LocalDateTime.now());
+            toUpdate.add(update);
+        }
+
+        // 无报表数据的creativeId,填充0值(只在最后一页后处理,这里不处理)
+        // 注意:0值填充逻辑移到所有分页完成后统一处理,避免中间页误填0值
+
         if (toUpdate.isEmpty()) {
             return 0;
         }
 
         // 批量更新
         if (isReqTime) {
-            adPlanCreativeMapper.batchUpdateReqDailyReport(toUpdate);
+            Lists.partition(toUpdate, 200).forEach(batch -> adPlanCreativeMapper.batchUpdateReqDailyReport(batch));
         } else {
-            adPlanCreativeMapper.batchUpdateRepDailyReport(toUpdate);
+            Lists.partition(toUpdate, 200).forEach(batch -> adPlanCreativeMapper.batchUpdateRepDailyReport(batch));
         }
 
-        log.info("[syncBatch] accountId={} 批量更新 {} 条 timeLine={}", accountId, toUpdate.size(), timeLine);
         return toUpdate.size();
     }
 

+ 184 - 221
src/main/java/com/moka/gdtauto/service/AdPlanDailyReportService.java

@@ -1,41 +1,39 @@
 package com.moka.gdtauto.service;
 
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.TimeUnit;
+import java.util.stream.Collectors;
+
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import com.google.common.collect.Lists;
 import com.moka.gdtauto.client.TencentAdsApiClientFactory;
 import com.moka.gdtauto.common.Constant;
 import com.moka.gdtauto.entity.AdPlan;
-import com.moka.gdtauto.entity.AdPlanConf;
 import com.moka.gdtauto.mapper.AdPlanConfMapper;
 import com.moka.gdtauto.mapper.AdPlanMapper;
-import com.tencent.ads.model.v3.DailyReportsGetResponseData;
 import com.tencent.ads.exception.TencentAdsResponseException;
-import com.tencent.ads.model.v3.DailyReportsFilteringStruct;
-import com.tencent.ads.model.v3.FilterOperator;
+import com.tencent.ads.model.v3.DailyReportsGetResponseData;
 import com.tencent.ads.model.v3.ReportDateRange;
 import com.tencent.ads.model.v3.ReportStruct;
 import com.tencent.ads.model.v3.TimeLine;
 import com.tencent.ads.v3.TencentAds;
+
+import jakarta.validation.constraints.NotNull;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.scheduling.annotation.Async;
-import org.springframework.stereotype.Service;
-
-import java.math.BigDecimal;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.CompletableFuture;
-import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.TimeUnit;
-import java.util.stream.Collectors;
 
 /**
  * 广告计划日报表同步服务
  * 按广告组级别同步日报数据到 ad_plan 表
+ * 改造为按 accountId 直接查询日报,无需 adgroup_id 过滤
  *
  * @author moka
  * @since 2026-03-10
@@ -48,6 +46,7 @@ public class AdPlanDailyReportService {
     private final TencentAdsApiClientFactory clientFactory;
     private final AdPlanConfMapper adPlanConfMapper;
     private final AdPlanMapper adPlanMapper;
+    private final AdPlanService adPlanService;
 
     /** REQUEST_TIME 口径同步字段 */
     private static final List<String> REQ_FIELDS = Arrays.asList(
@@ -58,9 +57,6 @@ public class AdPlanDailyReportService {
     private static final List<String> REP_FIELDS = Arrays.asList(
             "adgroup_id", "date", "income_val_1", "income_roi_1");
 
-    private static final int PAGE_SIZE = 100;
-    private static final int ADGROUP_BATCH_LIMIT = 100;
-
     @Async
     public void syncDailyReport(Long confId) {
         LocalDateTime start = LocalDateTime.now();
@@ -93,229 +89,139 @@ public class AdPlanDailyReportService {
 
     /**
      * 执行同步逻辑
+     * 直接按 accountId 查询日报,无需 adgroup_id 过滤
      *
-     * @param startTime 开始时间
-     * @param endTime   结束时间
-     * @param timeLine  时间口径
-     * @param isReqTime 是否为 REQUEST_TIME 口径(决定更新哪些字段)
+     * @param startTime   开始时间
+     * @param endTime     结束时间
+     * @param timeLine    时间口径
+     * @param isReqTime   是否为 REQUEST_TIME 口径(决定更新哪些字段)
      */
-    private void doSyncDailyReport(Long confId, LocalDateTime startTime, LocalDateTime endTime, String timeLine,
-            boolean isReqTime) {
-        // 1. 分页查询 create_time 在范围内的 adPlanConf
-        int confPageNum = 1;
-        int totalUpdated = 0;
-        Map<Long, Integer> confUpdatedCount = new ConcurrentHashMap<>();
-
-        while (true) {
-            LambdaQueryWrapper<AdPlanConf> confWrapper = new LambdaQueryWrapper<>();
-            confWrapper
-                    .eq(confId != null, AdPlanConf::getId, confId)
-                    .eq(AdPlanConf::getDeleted, 0)
-                    .orderByDesc(AdPlanConf::getId);
-            Page<AdPlanConf> confPage = adPlanConfMapper.selectPage(new Page<>(confPageNum, PAGE_SIZE), confWrapper);
-            List<AdPlanConf> confList = confPage.getRecords();
-
-            if (confList.isEmpty()) {
-                break;
-            }
-
-            // 2. 每个 conf 启动一个线程处理
-            List<CompletableFuture<Void>> futures = confList.stream()
-                    .map(conf -> CompletableFuture.runAsync(() -> {
-                        try {
-                            int updated = processConf(conf, startTime, endTime, timeLine, isReqTime);
-                            confUpdatedCount.put(conf.getId(), updated);
-                        } catch (Exception e) {
-                            log.error("[doSyncDailyReport] 处理 confId={} 失败", conf.getId(), e);
-                        }
-                    }))
-                    .collect(Collectors.toList());
-
-            // 等待所有线程完成
-            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
-
-            if (!confPage.hasNext()) {
-                break;
-            }
-            confPageNum++;
+    private void doSyncDailyReport(@NotNull Long confId, LocalDateTime startTime, LocalDateTime endTime, String timeLine, boolean isReqTime) {
+        // 查询该配置下的所有 adgroup_id 和 account_id
+        List<AdPlan> adPlans = adPlanService.lambdaQuery()
+                .select(AdPlan::getAccountId, AdPlan::getAdgroupId)
+                .gt(AdPlan::getAdgroupId, 0)
+                .eq(AdPlan::getAdPlanConfId, confId)
+                .list();
+        Set<Long> adgroupIdSet = adPlans.stream().map(AdPlan::getAdgroupId).collect(Collectors.toSet());
+        Set<Long> accountIdSet = adPlans.stream().map(AdPlan::getAccountId).collect(Collectors.toSet());
+        if (adgroupIdSet.isEmpty()) {
+            log.warn("[doSyncDailyReport] 没有广告组数据,不进行同步");
+            return;
         }
-
-        totalUpdated = confUpdatedCount.values().stream().mapToInt(Integer::intValue).sum();
-        log.info("[doSyncDailyReport] 同步完成 timeLine={} totalUpdated={}", timeLine, totalUpdated);
-    }
-
-    /**
-     * 处理单个 AdPlanConf
-     */
-    private int processConf(AdPlanConf conf, LocalDateTime startTime, LocalDateTime endTime, String timeLine,
-            boolean isReqTime) throws Exception {
-        String jobNumber = conf.getJobNumber();
-        TencentAds tencentAds = clientFactory.getTencentAds(jobNumber, Constant.MAIN_ORG_ACCOUNT_ID);
-
         String startStr = startTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
         String endStr = endTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
         ReportDateRange dateRange = new ReportDateRange();
         dateRange.setStartDate(startStr);
         dateRange.setEndDate(endStr);
 
-        log.info("[广告日报] start={} end={} timeLine={} isReqTime={}", startStr, endStr, timeLine, isReqTime);
-
         List<String> groupBy = Arrays.asList("adgroup_id", "date");
         List<String> fields = isReqTime ? REQ_FIELDS : REP_FIELDS;
 
-        int totalUpdated = 0;
-        int planPageNum = 1;
-
-        // 3. 分页查询该 conf 下的所有 adPlan
-        while (true) {
-            LambdaQueryWrapper<AdPlan> planWrapper = new LambdaQueryWrapper<>();
-            planWrapper.eq(AdPlan::getAdPlanConfId, conf.getId())
-                    .isNotNull(AdPlan::getAdgroupId)
-                    .gt(AdPlan::getAdgroupId, 0)
-                    .eq(AdPlan::getDeleted, 0)
-                    .orderByAsc(AdPlan::getId);
-            Page<AdPlan> planPage = adPlanMapper.selectPage(new Page<>(planPageNum, PAGE_SIZE), planWrapper);
-            List<AdPlan> plans = planPage.getRecords();
-
-            if (plans.isEmpty()) {
-                break;
-            }
-
-            // 4. 按 accountId 分组,每 100 个 adgroup_id 一批调用 API
-            Map<Long, List<AdPlan>> groupByAccount = plans.stream()
-                    .filter(p -> p.getAccountId() != null && p.getAdgroupId() != null)
-                    .collect(Collectors.groupingBy(AdPlan::getAccountId));
-
-            for (Map.Entry<Long, List<AdPlan>> entry : groupByAccount.entrySet()) {
-                Long accountId = entry.getKey();
-                List<AdPlan> plansOfAccount = entry.getValue();
-
-                // 按 100 条分批
-                int batchCount = (int) Math.ceil((double) plansOfAccount.size() / ADGROUP_BATCH_LIMIT);
-                for (int i = 0; i < batchCount; i++) {
-                    int fromIdx = i * ADGROUP_BATCH_LIMIT;
-                    int toIdx = Math.min(fromIdx + ADGROUP_BATCH_LIMIT, plansOfAccount.size());
-                    List<AdPlan> batch = plansOfAccount.subList(fromIdx, toIdx);
-
-                    try {
-                        int updated = syncBatch(tencentAds, accountId, batch, dateRange, groupBy, fields, timeLine,
-                                isReqTime);
-                        totalUpdated += updated;
-                    } catch (Exception e) {
-                        log.error("[processConf] 同步失败 accountId={} confId={} batch={}", accountId, conf.getId(), i, e);
-                    }
-                }
-            }
-
-            if (!planPage.hasNext()) {
-                break;
+        try {
+            TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken(Constant.MAIN_ORG_ACCOUNT_ID);
+            for (Long accountId : accountIdSet) {
+                syncBatch(tencentAds, accountId, adgroupIdSet, dateRange, groupBy, fields, timeLine, isReqTime);
             }
-            planPageNum++;
+        } catch (Exception e) {
+            log.error("[doSyncDailyReport] 同步失败 confId={} startTime={} endTime={} timeLine={} isReqTime={}", confId, startTime, endTime, timeLine, isReqTime, e);
         }
-
-        return totalUpdated;
     }
 
     /**
-     * 批量同步单批 adPlan
+     * 批量同步单 accountId 下的日报数据
+     * 支持分页循环获取所有数据,每页数据立即处理并更新
      */
-    private int syncBatch(TencentAds tencentAds, Long accountId, List<AdPlan> batch,
-            ReportDateRange dateRange, List<String> groupBy, List<String> fields,
-            String timeLine, boolean isReqTime) throws Exception {
+    private int syncBatch(TencentAds tencentAds, Long accountId, Set<Long> adgroupIdSet,
+                          ReportDateRange dateRange, List<String> groupBy, List<String> fields,
+                          String timeLine, boolean isReqTime) throws Exception {
+        // 用于跟踪已处理的 adgroupId,避免重复处理
+        Set<Long> processedAdgroupIds = new java.util.HashSet<>();
+        Long page = 1L;
+        Long pageSize = 2000L;
+        boolean hasMore = true;
+        int totalUpdated = 0;
 
-        // 构建 filtering: adgroup_id IN [...]
-        List<String> adgroupIdValues = batch.stream()
-                .map(p -> String.valueOf(p.getAdgroupId()))
-                .collect(Collectors.toList());
+        // 分页循环获取所有数据,每页立即处理
+        while (hasMore) {
+            DailyReportsGetResponseData response = null;
+            final Long currentPage = page;
+            try {
+                response = tencentAds.dailyReports().dailyReportsGet(
+                        "REPORT_LEVEL_ADGROUP",
+                        dateRange,
+                        groupBy,
+                        fields,
+                        accountId,
+                        null,  // filtering 不需要过滤,直接查询该账号下所有广告组
+                        null,  // orderBy
+                        timeLine,
+                        currentPage,
+                        pageSize,
+                        null   // modularDetailMode
+                );
+            } catch (TencentAdsResponseException e) {
+                // 11016 Application has reached the request limit per second.
+                // 应用程序已达到每秒的请求限制,请稍后再试
+                // 11017 Application has reached the request limit per minute.
+                // 应用请求已达分钟频次上限,请登陆开发者后台查看当前接口对应分配的频次,并在要求范围内请求接口。
+                if (e.getCode() == 11016) {
+                    log.warn("请求达到每秒限制,请稍后再试");
+                    TimeUnit.SECONDS.sleep(1);
+                } else if (e.getCode() == 11017) {
+                    log.warn("请求达到每分钟限制,请稍后再试");
+                    // 计算出还剩多少时间
+                    LocalDateTime now = LocalDateTime.now();
+                    // the second-of-minute, from 0 to 59
+                    int second = now.getSecond();
+                    int sleepSeconds = (59 - second);
+                    log.warn("请求达到每分钟限制,sleep:{} s", sleepSeconds);
+                    TimeUnit.SECONDS.sleep(sleepSeconds);
+                }
+                // 重试当前页
+                continue;
+            }
 
-        DailyReportsFilteringStruct filter = new DailyReportsFilteringStruct();
-        filter.setField("adgroup_id");
-        filter.setOperator(FilterOperator.IN);
-        filter.setValues(adgroupIdValues);
+            if (response == null || response.getList() == null) {
+                log.warn("[syncBatch] accountId={} page={} 未返回日报数据", accountId, currentPage);
+                break;
+            }
 
-        List<DailyReportsFilteringStruct> filtering = new ArrayList<>();
-        filtering.add(filter);
+            List<ReportStruct> pageList = response.getList();
+            log.info("[syncBatch] accountId={} page={} 获取 {} 条数据", accountId, currentPage, pageList.size());
 
-        // 调用 API
-        DailyReportsGetResponseData response = null;
-        try {
-            response = tencentAds.dailyReports().dailyReportsGet(
-                    "REPORT_LEVEL_ADGROUP",
-                    dateRange,
-                    groupBy,
-                    fields,
-                    accountId,
-                    filtering,
-                    null, // orderBy
-                    timeLine,
-                    1L,
-                    (long) ADGROUP_BATCH_LIMIT,
-                    null // modularDetailMode
-            );
-        } catch (TencentAdsResponseException e) {
-            // 11016 Application has reached the request limit per second.
-            // 应用程序已达到每秒的请求限制,请稍后再试
-            // 11017 Application has reached the request limit per minute.
-            // 应用请求已达分钟频次上限,请登陆开发者后台查看当前接口对应分配的频次,并在要求范围内请求接口。
-            if (e.getCode() == 11016) {
-                log.warn("请求达到每秒限制,请稍后再试");
-                TimeUnit.SECONDS.sleep(1);
-            } else if (e.getCode() == 11017) {
-                log.warn("请求达到每分钟限制,请稍后再试");
-                // 计算出还剩多少时间
-                LocalDateTime now = LocalDateTime.now();
-                // the second-of-minute, from 0 to 59
-                int second = now.getSecond();
-                int sleepSeconds = (59 - second);
-                log.warn("请求达到每分钟限制,sleep:{} s", sleepSeconds);
-                TimeUnit.SECONDS.sleep(sleepSeconds);
+            // 立即处理当前页数据
+            if (!pageList.isEmpty()) {
+                int updated = processAndUpdatePage(pageList, accountId, adgroupIdSet, processedAdgroupIds, isReqTime);
+                totalUpdated += updated;
             }
-            syncBatch(tencentAds, accountId, batch, dateRange, groupBy, fields, timeLine, isReqTime);
-        }
 
-        if (response == null) {
-            log.warn("[syncBatch] accountId={} 未返回日报数据", accountId);
-            return 0;
+            // 判断是否有更多数据:当前页数据量小于pageSize表示已到最后一页
+            if (pageList.size() < pageSize) {
+                hasMore = false;
+            } else {
+                page++;
+            }
         }
 
-        // 构建更新数据
-        List<AdPlan> toUpdate = new ArrayList<>();
-        if (response.getList() != null) {
-            for (ReportStruct rs : response.getList()) {
-                if (rs.getAdgroupId() == null) {
-                    continue;
-                }
-                AdPlan update = new AdPlan();
-                update.setAdgroupId(rs.getAdgroupId());
+        // 所有分页完成后,对无报表数据的 adgroupId 填充0值
+        int zeroFilledCount = fillZeroForMissingAdgroups(accountId, adgroupIdSet, processedAdgroupIds, isReqTime);
+        totalUpdated += zeroFilledCount;
 
-                if (isReqTime) {
-                    // REQUEST_TIME 口径:更新 cost, view_count, valid_click_count, ctr,
-                    // conversions_cost, req_income_val_1, req_income_roi_1
-                    update.setCost(rs.getCost());
-                    update.setViewCount(rs.getViewCount());
-                    update.setValidClickCount(rs.getValidClickCount());
-                    update.setCtr(d2b(rs.getCtr()));
-                    update.setConversionsCost(rs.getConversionsCost());
-                    update.setReqIncomeVal1(rs.getIncomeVal1());
-                    update.setReqIncomeRoi1(d2b(rs.getIncomeRoi1()));
-                    update.setScanFollowUserCost(rs.getScanFollowUserCost());
-                } else {
-                    // REPORTING_TIME 口径:更新 rep_income_val_1, rep_income_roi_1
-                    update.setRepIncomeVal1(rs.getIncomeVal1());
-                    update.setRepIncomeRoi1(d2b(rs.getIncomeRoi1()));
-                }
-                update.setUpdateTime(LocalDateTime.now());
-                toUpdate.add(update);
-            }
-        }
+        log.info("[syncBatch] accountId={} 总共更新 {} 条 timeLine={}", accountId, totalUpdated, timeLine);
+        return totalUpdated;
+    }
 
-        // 无报表数据的,填充0值
-        Map<Long, AdPlan> updateMap = toUpdate.stream().collect(Collectors.toMap(AdPlan::getAdgroupId, p -> p));
-        batch.forEach(p -> {
-            if (!updateMap.containsKey(p.getAdgroupId())) {
+    /**
+     * 对无报表数据的 adgroupId 填充0值
+     */
+    private int fillZeroForMissingAdgroups(Long accountId, Set<Long> adgroupIdSet,
+                                           Set<Long> processedAdgroupIds, boolean isReqTime) {
+        List<AdPlan> zeroUpdates = new ArrayList<>();
+        adgroupIdSet.forEach(adgroupId -> {
+            if (!processedAdgroupIds.contains(adgroupId)) {
                 AdPlan update = new AdPlan();
-                update.setAdgroupId(p.getAdgroupId());
-
+                update.setAdgroupId(adgroupId);
                 if (isReqTime) {
                     update.setCost(0L);
                     update.setViewCount(0L);
@@ -330,22 +236,79 @@ public class AdPlanDailyReportService {
                     update.setRepIncomeRoi1(BigDecimal.ZERO);
                 }
                 update.setUpdateTime(LocalDateTime.now());
-                toUpdate.add(update);
+                zeroUpdates.add(update);
             }
         });
 
+        if (zeroUpdates.isEmpty()) {
+            return 0;
+        }
+
+        // 批量更新0值
+        if (isReqTime) {
+            Lists.partition(zeroUpdates, 200).forEach(batch -> adPlanMapper.batchUpdateReqDailyReport(batch));
+        } else {
+            Lists.partition(zeroUpdates, 200).forEach(batch -> adPlanMapper.batchUpdateRepDailyReport(batch));
+        }
+
+        log.info("[fillZeroForMissingAdgroups] accountId={} 填充0值 {} 条", accountId, zeroUpdates.size());
+        return zeroUpdates.size();
+    }
+
+    /**
+     * 处理单页数据并更新数据库
+     */
+    private int processAndUpdatePage(List<ReportStruct> pageList, Long accountId, Set<Long> adgroupIdSet,
+                                     Set<Long> processedAdgroupIds, boolean isReqTime) {
+        // 构建更新数据
+        List<AdPlan> toUpdate = new ArrayList<>();
+        for (ReportStruct rs : pageList) {
+            if (rs.getAdgroupId() == null) {
+                continue;
+            }
+            Long adgroupId = rs.getAdgroupId();
+            // 跳过不在目标 adgroupIdSet 中的广告组
+            if (!adgroupIdSet.contains(adgroupId)) {
+                continue;
+            }
+            // 跳过已处理的 adgroupId
+            if (processedAdgroupIds.contains(adgroupId)) {
+                continue;
+            }
+            processedAdgroupIds.add(adgroupId);
+
+            AdPlan update = new AdPlan();
+            update.setAdgroupId(adgroupId);
+            if (isReqTime) {
+                // REQUEST_TIME 口径:更新 cost, view_count, valid_click_count, ctr, conversions_cost, req_income_val_1, req_income_roi_1
+                update.setCost(rs.getCost());
+                update.setViewCount(rs.getViewCount());
+                update.setValidClickCount(rs.getValidClickCount());
+                update.setCtr(d2b(rs.getCtr()));
+                update.setConversionsCost(rs.getConversionsCost());
+                update.setReqIncomeVal1(rs.getIncomeVal1());
+                update.setReqIncomeRoi1(d2b(rs.getIncomeRoi1()));
+                update.setScanFollowUserCost(rs.getScanFollowUserCost());
+            } else {
+                // REPORTING_TIME 口径:更新 rep_income_val_1, rep_income_roi_1
+                update.setRepIncomeVal1(rs.getIncomeVal1());
+                update.setRepIncomeRoi1(d2b(rs.getIncomeRoi1()));
+            }
+            update.setUpdateTime(LocalDateTime.now());
+            toUpdate.add(update);
+        }
+
         if (toUpdate.isEmpty()) {
             return 0;
         }
 
         // 批量更新
         if (isReqTime) {
-            adPlanMapper.batchUpdateReqDailyReport(toUpdate);
+            Lists.partition(toUpdate, 200).forEach(batch -> adPlanMapper.batchUpdateReqDailyReport(batch));
         } else {
-            adPlanMapper.batchUpdateRepDailyReport(toUpdate);
+            Lists.partition(toUpdate, 200).forEach(batch -> adPlanMapper.batchUpdateRepDailyReport(batch));
         }
 
-        log.info("[syncBatch] accountId={} 批量更新 {} 条 timeLine={}", accountId, toUpdate.size(), timeLine);
         return toUpdate.size();
     }
 

+ 3 - 1
src/main/java/com/moka/gdtauto/service/OssFileStatisticsService.java

@@ -22,7 +22,9 @@ public interface OssFileStatisticsService extends IService<OssFileStatistics> {
      * @param fileMd5   文件MD5
      * @param startDate 开始日期
      * @param endDate   结束日期
+     * @param sortField 排序字段
+     * @param sortOrder 排序方式
      * @return 分页结果
      */
-    Page<OssFileStatistics> listStatisticsByMd5AndDate(Long page, Long size, String fileMd5, String startDate, String endDate);
+    Page<OssFileStatistics> listStatisticsByMd5AndDate(Long page, Long size, String fileMd5, String startDate, String endDate, String sortField, String sortOrder);
 }

+ 9 - 7
src/main/java/com/moka/gdtauto/service/TencentAdsAccountService.java

@@ -1,16 +1,18 @@
 package com.moka.gdtauto.service;
 
+import java.util.List;
+
+import org.springframework.stereotype.Service;
+
 import com.moka.gdtauto.client.TencentAdsApiClientFactory;
 import com.tencent.ads.exception.TencentAdsResponseException;
 import com.tencent.ads.exception.TencentAdsSDKException;
-import com.tencent.ads.model.v3.FilteringStruct;
 import com.tencent.ads.model.v3.AdgroupsGetResponseData;
+import com.tencent.ads.model.v3.FilteringStruct;
 import com.tencent.ads.v3.TencentAds;
+
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-
-import java.util.List;
 
 /**
  * 腾讯广告账户API服务
@@ -34,7 +36,7 @@ public class TencentAdsAccountService {
      * @throws Exception API异常
      */
     public TencentAds getTencentAdsInstance(String jobNumber, Long accountId) throws Exception {
-        return clientFactory.getTencentAdsWithoutUserToken(jobNumber, accountId);
+        return clientFactory.getTencentAdsWithoutUserToken(accountId);
     }
 
     /**
@@ -51,7 +53,7 @@ public class TencentAdsAccountService {
         log.info("开始获取广告组列表,accountId={}, page={}, pageSize={}", orgAccountId, page, pageSize);
             
         // 获取TencentAds实例
-        TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken(jobNumber,orgAccountId);
+        TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken(orgAccountId);
             
         // 调用SDK的adgroups().adgroupsGet()方法
         List<FilteringStruct> filtering = null; // 不传过滤条件,获取全部
@@ -114,7 +116,7 @@ public class TencentAdsAccountService {
             accountId, page, pageSize, filtering);
         
         // 获取TencentAds实例
-        TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken(jobNumber, orgAccountId);
+        TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken(orgAccountId);
         
         // 调用SDK的adgroups().adgroupsGet()方法
         Boolean isDeleted = false;

+ 10 - 8
src/main/java/com/moka/gdtauto/service/impl/AccountGroupServiceImpl.java

@@ -1,5 +1,14 @@
 package com.moka.gdtauto.service.impl;
 
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -10,15 +19,8 @@ import com.moka.gdtauto.mapper.AccountGroupAccountRelationshipMapper;
 import com.moka.gdtauto.mapper.AccountGroupMapper;
 import com.moka.gdtauto.service.AccountGroupService;
 import com.moka.gdtauto.service.TencentAccountService;
-import lombok.extern.slf4j.Slf4j;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
 
-import java.time.LocalDateTime;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.stream.Collectors;
+import lombok.extern.slf4j.Slf4j;
 
 /**
  * 账号组服务实现类

+ 8 - 6
src/main/java/com/moka/gdtauto/service/impl/AdPlanConfServiceImpl.java

@@ -1112,11 +1112,13 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
                     .filter(plan -> plan.getAdgroupId() != null)
                     .collect(Collectors.toMap(AdPlan::getAdgroupId, plan -> plan, (left, right) -> left));
 
-            // 连续5天消费为0
+            // 自动删除计划,从创建日期往后连续3天花费为0的计划自动暂停,
+            // 连续5天花费为0的自动删除(包含之前被暂停的广告)
             LocalDate startDate = conf.getCreateTime().toLocalDate();
             LocalDate endDate = startDate.plusDays(5);
 
             List<ReqDailyAdgroupReport> reports = reqDailyAdgroupReportService.lambdaQuery()
+                    .select(ReqDailyAdgroupReport::getAdgroupId, ReqDailyAdgroupReport::getCost)
                     .in(ReqDailyAdgroupReport::getAdgroupId, planMap.keySet())
                     .between(ReqDailyAdgroupReport::getDate, startDate, endDate)
                     .list();
@@ -1126,23 +1128,23 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
                             ReqDailyAdgroupReport::getAdgroupId,
                             Collectors.summingLong(report -> report.getCost() == null ? 0L : report.getCost())));
 
-            List<AdPlan> deletePlans = planMap.entrySet().stream()
+            List<AdPlan> suspendPlans = planMap.entrySet().stream()
                     .filter(entry -> validCostSumMap.getOrDefault(entry.getKey(), 0L) == 0L)
                     .map(Map.Entry::getValue)
                     .toList();
 
-            if (deletePlans.isEmpty()) {
+            if (suspendPlans.isEmpty()) {
                 log.info("gdtAdgroupPatrolDelete 无需暂停广告组 confId={} [{}, {}]", conf.getId(), startDate, endDate);
                 return;
             }
 
             TencentAds tencentAds = clientFactory.getTencentAds(Constant.JOB_NUMBER, Constant.MAIN_ORG_ACCOUNT_ID);
-            doSuspendPlans(tencentAds, deletePlans);
+            doSuspendPlans(tencentAds, suspendPlans);
             lambdaUpdate().eq(AdPlanConf::getId, conf.getId())
-                    .set(AdPlanConf::getFailReason, "广告巡检暂停" + deletePlans.size() + "个广告")
+                    .set(AdPlanConf::getFailReason, "广告巡检暂停" + suspendPlans.size() + "个广告")
                     .update();
             log.info("gdtAdgroupPatrolDelete 暂停广告组完成 confId={} deleteCount={} [{}, {}]",
-                    conf.getId(), deletePlans.size(), startDate, endDate);
+                    conf.getId(), suspendPlans.size(), startDate, endDate);
         } catch (Exception e) {
             log.error("gdtAdgroupPatrolDelete error confId={}", conf.getId(), e);
             lambdaUpdate()

+ 28 - 3
src/main/java/com/moka/gdtauto/service/impl/OssFileStatisticsServiceImp.java

@@ -24,7 +24,7 @@ import com.moka.gdtauto.service.OssFileStatisticsService;
 public class OssFileStatisticsServiceImp extends ServiceImpl<OssFileStatisticsMapper, OssFileStatistics> implements OssFileStatisticsService {
 
     @Override
-    public Page<OssFileStatistics> listStatisticsByMd5AndDate(Long page, Long size, String fileMd5, String startDate, String endDate) {
+    public Page<OssFileStatistics> listStatisticsByMd5AndDate(Long page, Long size, String fileMd5, String startDate, String endDate, String sortField, String sortOrder) {
         Page<OssFileStatistics> pageParam = new Page<>(page, size);
         LambdaQueryWrapper<OssFileStatistics> wrapper = new LambdaQueryWrapper<>();
 
@@ -39,8 +39,33 @@ public class OssFileStatisticsServiceImp extends ServiceImpl<OssFileStatisticsMa
             wrapper.le(OssFileStatistics::getDate, LocalDate.parse(endDate));
         }
 
-        // 按日期倒序排列
-        wrapper.orderByDesc(OssFileStatistics::getCost);
+        // 处理排序
+        boolean isAscending = "ascending".equalsIgnoreCase(sortOrder);
+        if (StringUtils.isNotBlank(sortField)) {
+            switch (sortField) {
+                case "date":
+                    wrapper.orderBy(true, isAscending, OssFileStatistics::getDate);
+                    break;
+                case "viewCount":
+                    wrapper.orderBy(true, isAscending, OssFileStatistics::getViewCount);
+                    break;
+                case "validClickCount":
+                    wrapper.orderBy(true, isAscending, OssFileStatistics::getValidClickCount);
+                    break;
+                case "ctr":
+                    wrapper.orderBy(true, isAscending, OssFileStatistics::getCtr);
+                    break;
+                case "cost":
+                    wrapper.orderBy(true, isAscending, OssFileStatistics::getCost);
+                    break;
+                default:
+                    // 默认按cost降序
+                    wrapper.orderByDesc(OssFileStatistics::getCost);
+            }
+        } else {
+            // 默认按cost降序
+            wrapper.orderByDesc(OssFileStatistics::getCost);
+        }
 
         return page(pageParam, wrapper);
     }

+ 9 - 7
src/main/java/com/moka/gdtauto/service/impl/TencentAccountServiceImpl.java

@@ -1,5 +1,12 @@
 package com.moka.gdtauto.service.impl;
 
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+
 import com.alibaba.fastjson2.JSON;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -9,14 +16,9 @@ import com.moka.gdtauto.mapper.TencentAccountMapper;
 import com.moka.gdtauto.service.TencentAccountService;
 import com.tencent.ads.model.v3.OrganizationAccountRelationGetResponseData;
 import com.tencent.ads.v3.TencentAds;
+
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-import org.springframework.util.CollectionUtils;
-
-import java.util.ArrayList;
-import java.util.List;
 
 /**
  * 腾讯广告账户服务实现类
@@ -39,7 +41,7 @@ public class TencentAccountServiceImpl extends ServiceImpl<TencentAccountMapper,
         
         try {
             // 获取TencentAds实例
-            TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken(jobNumber, orgAccountId);
+            TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken(orgAccountId);
             tencentAds.setDebug(true);
             // 调用API获取组织账户下的广告账户列表
             List<TencentAccount> accounts = new ArrayList<>();

+ 12 - 7
src/main/java/com/moka/gdtauto/service/impl/TencentConversionServiceImpl.java

@@ -1,19 +1,24 @@
 package com.moka.gdtauto.service.impl;
 
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.moka.gdtauto.client.TencentAdsApiClientFactory;
 import com.moka.gdtauto.entity.TencentConversion;
 import com.moka.gdtauto.mapper.TencentConversionMapper;
 import com.moka.gdtauto.service.TencentConversionService;
-import com.tencent.ads.model.v3.*;
+import com.tencent.ads.model.v3.ConversionsGetListStruct;
+import com.tencent.ads.model.v3.ConversionsGetResponseData;
+import com.tencent.ads.model.v3.FilterOperator;
+import com.tencent.ads.model.v3.FilteringStruct;
 import com.tencent.ads.v3.TencentAds;
+
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-
-import java.util.ArrayList;
-import java.util.List;
 
 /**
  * 腾讯广告转化归因服务实现类
@@ -35,7 +40,7 @@ public class TencentConversionServiceImpl extends ServiceImpl<TencentConversionM
         log.info("开始同步转化归因数据,jobNumber={}, orgAccountId={}, accountId={}", jobNumber, orgAccountId, accountId);
         
         try {
-            TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken(jobNumber, orgAccountId);
+            TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken(orgAccountId);
             tencentAds.setDebug(true);
             
             // 构建过滤条件:短剧用户增长注册变现

+ 1 - 1
src/main/java/com/moka/gdtauto/service/impl/TencentImagesServiceImpl.java

@@ -93,7 +93,7 @@ public class TencentImagesServiceImpl extends ServiceImpl<TencentImagesMapper, T
             log.info("Base64编码完成,长度: {}", base64Image.length());
 
             // 5. 获取TencentAds客户端
-            TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken("12458", 33453856L);
+            TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken(33453856L);
             log.info("开始上传图片到腾讯广告,账户ID: {}", accountId);
 
             // 6. 调用腾讯广告API上传图片(使用UPLOAD_TYPE_BYTES方式)

+ 15 - 13
src/main/java/com/moka/gdtauto/service/impl/TencentVideoServiceImpl.java

@@ -1,5 +1,18 @@
 package com.moka.gdtauto.service.impl;
 
+import java.io.File;
+import java.io.FileInputStream;
+import java.math.BigInteger;
+import java.nio.file.Files;
+import java.security.MessageDigest;
+import java.time.LocalDateTime;
+
+import org.bytedeco.javacv.FFmpegFrameGrabber;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.multipart.MultipartFile;
+
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.moka.gdtauto.client.TencentAdsApiClientFactory;
 import com.moka.gdtauto.entity.TencentAssetDrama;
@@ -10,19 +23,8 @@ import com.moka.gdtauto.service.TencentVideoService;
 import com.moka.gdtauto.util.VideoWatermarkUtil;
 import com.tencent.ads.model.v3.VideosAddResponseData;
 import com.tencent.ads.v3.TencentAds;
-import lombok.extern.slf4j.Slf4j;
-import org.bytedeco.javacv.FFmpegFrameGrabber;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-import org.springframework.web.multipart.MultipartFile;
 
-import java.io.File;
-import java.io.FileInputStream;
-import java.math.BigInteger;
-import java.nio.file.Files;
-import java.security.MessageDigest;
-import java.time.LocalDateTime;
+import lombok.extern.slf4j.Slf4j;
 
 /**
  * 腾讯广告视频Service实现类
@@ -106,7 +108,7 @@ public class TencentVideoServiceImpl extends ServiceImpl<TencentVideoMapper, Ten
             log.info("视频MD5签名: {}", signature);
 
             // 4. 获取TencentAds客户端
-            TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken(jobNumber, accountId);
+            TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken(accountId);
             log.info("开始上传视频到腾讯广告,账户ID: {}", accountId);
 
             // 5. 调用腾讯广告API上传视频

+ 5 - 4
src/main/java/com/moka/gdtauto/task/AdAutoTask.java

@@ -8,6 +8,7 @@ import java.util.concurrent.Executors;
 import java.util.concurrent.Semaphore;
 
 import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
 
 import com.moka.gdtauto.entity.AdAutoConf;
 import com.moka.gdtauto.entity.AdPlanConf;
@@ -20,7 +21,7 @@ import lombok.AllArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
 
-//@Component
+@Component
 @AllArgsConstructor
 @Slf4j
 public class AdAutoTask {
@@ -51,12 +52,12 @@ public class AdAutoTask {
     @SchedulerLock(name = "ad-auto-suspend-task", lockAtMostFor = "9m", lockAtLeastFor = "2m")
     public void adAutoSuspendTask() {
         log.info("adAutoSuspendTask execute");
-        // 8天前
-        LocalDateTime beginTime = LocalDateTime.now().minusDays(10);
+        // 5天前创建的广告
+        LocalDateTime fiveDaysAgo = LocalDateTime.now().minusDays(5);
         List<AdPlanConf> confList = 
             adPlanConfService.lambdaQuery()
             //.ge(AdPlanConf::getId, 57L)
-            .lt(AdPlanConf::getCreateTime, beginTime)
+            .lt(AdPlanConf::getCreateTime, fiveDaysAgo)
             .orderByDesc(AdPlanConf::getId)
             .list();
         

+ 2 - 2
src/main/resources/mapper/AdPlanCreativeMapper.xml

@@ -28,7 +28,7 @@
                 req_income_roi1  = #{item.reqIncomeRoi1},
                 scan_follow_user_cost = #{item.scanFollowUserCost},
                 update_time       = NOW()
-            WHERE adgroup_id = #{item.adgroupId}
+            WHERE creative_id = #{item.creativeId}
         </foreach>
     </update>
 
@@ -39,7 +39,7 @@
             SET rep_income_val1 = #{item.repIncomeVal1},
                 rep_income_roi1 = #{item.repIncomeRoi1},
                 update_time      = NOW()
-            WHERE adgroup_id = #{item.adgroupId}
+            WHERE creative_id = #{item.creativeId}
         </foreach>
     </update>
 

+ 21 - 2
src/test/java/com/moka/gdtauto/service/AdPlanDailyReportSyncTest.java

@@ -9,7 +9,6 @@ import org.springframework.boot.test.context.SpringBootTest;
 import org.springframework.test.context.ActiveProfiles;
 
 import com.moka.gdtauto.GdtAutoApplication;
-import com.moka.gdtauto.task.AdPlanDailyReportSyncTask;
 import com.tencent.ads.model.v3.TimeLine;
 
 
@@ -23,13 +22,33 @@ import com.tencent.ads.model.v3.TimeLine;
 public class AdPlanDailyReportSyncTest {
     
     @Autowired
-    private AdPlanDailyReportSyncTask adPlanDailyReportSyncTask;
+    private AdPlanCreativeDailyReportService adPlanCreativeDailyReportService;
+    @Autowired
+    private AdPlanDailyReportService adPlanDailyReportService;
     @Autowired
     private DailyCreativeReportService dailyCreativeReportService;
     @Autowired
     private DailyAdgroupReportService dailyAdgroupReportService;
 
     @Test
+    public void testSyncDailyReport() {
+        try {
+            adPlanDailyReportService.syncDailyReport(67L);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test
+    public void testSyncCreativeReport() {
+        try {
+            adPlanCreativeDailyReportService.syncDailyReport(67L);
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    @Test
     public void testSyncDailyCreativeReport() {
         DateTimeFormatter FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
         String today = LocalDate.now().format(FMT);