|
|
@@ -0,0 +1,330 @@
|
|
|
+package com.moka.gdtauto.service;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSON;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
+import com.moka.gdtauto.common.AutoPlanRule;
|
|
|
+import com.moka.gdtauto.entity.AdAutoConf;
|
|
|
+import com.moka.gdtauto.entity.AdAutoExecuteLog;
|
|
|
+import com.moka.gdtauto.entity.AdPlan;
|
|
|
+import com.moka.gdtauto.entity.AdPlanConf;
|
|
|
+import com.moka.gdtauto.entity.AdPlanCreative;
|
|
|
+import com.tencent.ads.model.v3.ConfiguredStatus;
|
|
|
+
|
|
|
+import lombok.AllArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+
|
|
|
+import org.springframework.scheduling.annotation.Async;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+
|
|
|
+import java.time.LocalDateTime;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Optional;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 广告巡检执行器
|
|
|
+ * 根据 AdAutoConf 配置的规则,对广告进行巡检并执行暂停操作
|
|
|
+ *
|
|
|
+ * @author moka
|
|
|
+ * @since 2026-03-12
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+@AllArgsConstructor
|
|
|
+public class AdPlanCreativeAutoExecutor {
|
|
|
+
|
|
|
+ private final AdAutoExecuteLogService adAutoExecuteLogService;
|
|
|
+ private final AdPlanConfService adPlanConfService;
|
|
|
+ private final AdPlanCreativeDailyReportService adPlanCreativeDailyReportService;
|
|
|
+ private final AdPlanCreativeService adPlanCreativeService;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 执行广告创意巡检
|
|
|
+ *
|
|
|
+ * @param conf 巡检配置
|
|
|
+ */
|
|
|
+ @Async
|
|
|
+ public void asyncExecute(AdAutoConf conf) {
|
|
|
+ log.info("[AdPlanCreativeAutoExecutor] 开始执行巡检,confId={}, jobNumber={}, adGroupId={}",
|
|
|
+ conf.getId(), conf.getJobNumber(), conf.getAdGroupId());
|
|
|
+
|
|
|
+ // 0. 创建执行日志
|
|
|
+ AdAutoExecuteLog executeLog = new AdAutoExecuteLog();
|
|
|
+ executeLog.setConfId(conf.getId());
|
|
|
+ executeLog.setParentLogId(0L);
|
|
|
+ executeLog.setAdPlanConfId(0L);
|
|
|
+ executeLog.setExecuteTime(LocalDateTime.now());
|
|
|
+ adAutoExecuteLogService.save(executeLog);
|
|
|
+
|
|
|
+ try {
|
|
|
+ doExecute(conf, executeLog.getId());
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("[AdPlanCreativeAutoExecutor] 巡检执行异常,confId={}", conf.getId(), e);
|
|
|
+ updateLogFailed(executeLog, "巡检异常: " + e.getMessage());
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void doExecute(AdAutoConf autoConf, Long parentExecuteLogId) {
|
|
|
+
|
|
|
+ // 解析规则
|
|
|
+ List<AutoPlanRule> rules = parseRules(autoConf.getRules());
|
|
|
+
|
|
|
+ // 1. 根据 jobNumber 查询 AdPlanConf 列表
|
|
|
+ List<AdPlanConf> adPlanConfs = adPlanConfService.lambdaQuery()
|
|
|
+ .eq(autoConf.getAdGroupId() > 0, AdPlanConf::getAccountGroupId, autoConf.getAdGroupId())
|
|
|
+ .eq(AdPlanConf::getJobNumber, autoConf.getJobNumber())
|
|
|
+ .in(AdPlanConf::getStatus, List.of(4, 5))
|
|
|
+ .eq(AdPlanConf::getDeleted, 0)
|
|
|
+ .list();
|
|
|
+ if (adPlanConfs == null || adPlanConfs.isEmpty()) {
|
|
|
+ log.warn("[AdPlanCreativeAutoExecutor] 未找到 AdPlanConf,jobNumber={}", autoConf.getJobNumber());
|
|
|
+ throw new RuntimeException("未找到关联广告计划配置");
|
|
|
+ }
|
|
|
+ log.info("[AdPlanCreativeAutoExecutor] 找到 {} 个 AdPlanConf,jobNumber={}", adPlanConfs.size(), autoConf.getJobNumber());
|
|
|
+
|
|
|
+ for (AdPlanConf adPlanConf : adPlanConfs) {
|
|
|
+ processOneConf(autoConf, adPlanConf, rules, parentExecuteLogId);
|
|
|
+ }
|
|
|
+ log.info("[AdPlanCreativeAutoExecutor] 巡检执行完成,confId={}", autoConf.getId());
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 处理单个 AdPlanConf:同步日报 → 分页查询 AdPlan → 规则检测 → 并发暂停
|
|
|
+ *
|
|
|
+ * @param adPlanConf 当前广告计划配置
|
|
|
+ * @param rules 巡检规则列表
|
|
|
+ * @param newAdDays 新广告天数阈值
|
|
|
+ * @param allPlansToSuspend 收集所有待暂停广告(跨 conf 汇总用)
|
|
|
+ * @return
|
|
|
+ */
|
|
|
+
|
|
|
+ public void checkRule(Long adPlanConfId, AdAutoConf autoConf,
|
|
|
+ List<AutoPlanRule> rules) {
|
|
|
+ // 1. 根据 jobNumber 查询 AdPlanConf 列表
|
|
|
+ List<AdPlanConf> adPlanConfs = adPlanConfService.lambdaQuery()
|
|
|
+ .eq(autoConf.getAdGroupId() > 0, AdPlanConf::getAccountGroupId, autoConf.getAdGroupId())
|
|
|
+ .eq(AdPlanConf::getId, adPlanConfId)
|
|
|
+ .list();
|
|
|
+ if (adPlanConfs == null || adPlanConfs.isEmpty()) {
|
|
|
+ log.warn("[AdPlanCreativeAutoExecutor] 未找到 AdPlanConf,jobNumber={}", autoConf.getJobNumber());
|
|
|
+ throw new RuntimeException("未找到关联广告计划配置");
|
|
|
+ }
|
|
|
+
|
|
|
+ List<AdPlanCreative> creativesToSuspend = new ArrayList<>();
|
|
|
+ for (AdPlanConf adPlanConf : adPlanConfs) {
|
|
|
+ List<AdPlanCreative> creativeList = adPlanCreativeService.lambdaQuery()
|
|
|
+ .eq(AdPlanCreative::getAdPlanConfId, adPlanConf.getId())
|
|
|
+ .eq(AdPlanCreative::getDeleted, 0)
|
|
|
+ .list();
|
|
|
+ for (AdPlanCreative creative : creativeList) {
|
|
|
+ if (shouldSuspend(creative, rules, autoConf.getNewAdDays())) {
|
|
|
+ creativesToSuspend.add(creative);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+ for (AdPlanCreative creative : creativesToSuspend) {
|
|
|
+ adPlanCreativeService.lambdaUpdate().set(AdPlanCreative::getFailReason, creative.getFailReason()).eq(AdPlanCreative::getId, creative.getId()).update();
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ private void processOneConf(AdAutoConf autoConf,
|
|
|
+ AdPlanConf adPlanConf,
|
|
|
+ List<AutoPlanRule> rules,
|
|
|
+ Long parentExecuteLogId) {
|
|
|
+ long confId = adPlanConf.getId();
|
|
|
+ LocalDateTime now = LocalDateTime.now();
|
|
|
+ AdAutoExecuteLog executeLog = new AdAutoExecuteLog();
|
|
|
+ executeLog.setConfId(autoConf.getId());
|
|
|
+ executeLog.setAdPlanConfId(adPlanConf.getId());
|
|
|
+ executeLog.setParentLogId(parentExecuteLogId);
|
|
|
+ executeLog.setExecuteTime(LocalDateTime.now());
|
|
|
+ adAutoExecuteLogService.save(executeLog);
|
|
|
+ // 同步日报
|
|
|
+ try {
|
|
|
+ log.info("[AdPlanCreativeAutoExecutor] 同步日报,confId={}", confId);
|
|
|
+ adPlanCreativeDailyReportService.syncReqDailyReport(confId, now, now);
|
|
|
+
|
|
|
+ // 分页查询当前 conf 下的 AdPlan,进行规则检测
|
|
|
+ long adCount = 0;
|
|
|
+ List<AdPlanCreative> creativesToSuspend = new ArrayList<>();
|
|
|
+ int pageNum = 1;
|
|
|
+ int pageSize = 100;
|
|
|
+ while (true) {
|
|
|
+ LambdaQueryWrapper<AdPlanCreative> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
+ queryWrapper.eq(AdPlanCreative::getAdPlanConfId, confId)
|
|
|
+ .gt(AdPlanCreative::getAdgroupId, 0)
|
|
|
+ .gt(AdPlanCreative::getCreativeId, 0)
|
|
|
+ .eq(AdPlanCreative::getDeleted, 0)
|
|
|
+ .eq(AdPlanCreative::getStatus, 4)
|
|
|
+ .orderByAsc(AdPlanCreative::getId);
|
|
|
+ Page<AdPlanCreative> page = adPlanCreativeService.page(new Page<>(pageNum, pageSize), queryWrapper);
|
|
|
+ List<AdPlanCreative> creativeList = page.getRecords();
|
|
|
+
|
|
|
+ if (creativeList.isEmpty()) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ creativeList = creativeList.stream()
|
|
|
+ .filter(plan -> {
|
|
|
+ return plan.getConfiguredStatus().equals("") || plan.getConfiguredStatus().equals(ConfiguredStatus.NORMAL.getValue());
|
|
|
+ }).collect(Collectors.toList());
|
|
|
+
|
|
|
+ adCount += creativeList.size();
|
|
|
+
|
|
|
+ for (AdPlanCreative creative : creativeList) {
|
|
|
+ if (shouldSuspend(creative, rules, autoConf.getNewAdDays())) {
|
|
|
+ creativesToSuspend.add(creative);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!page.hasNext()) {
|
|
|
+ break;
|
|
|
+ }
|
|
|
+ pageNum++;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!creativesToSuspend.isEmpty()) {
|
|
|
+ adPlanConfService.gdtSuspendByCreatives(adPlanConf, creativesToSuspend);
|
|
|
+ }
|
|
|
+
|
|
|
+ log.info("[AdPlanCreativeAutoExecutor] confId={} 巡检:广告数={}, 需暂停数={}", confId, adCount, creativesToSuspend.size());
|
|
|
+ executeLog.setAdCount(adCount);
|
|
|
+ executeLog.setSuspendCount(creativesToSuspend.size());
|
|
|
+ executeLog.setExecuteStatus(1);
|
|
|
+ executeLog.setSuspendAdGroupIds(creativesToSuspend.stream().map(x -> String.format("[%d-%d-{%s}]", x.getAdPlanConfId(), x.getCreativeId(), x.getCreativeName())).collect(Collectors.joining("\n")));
|
|
|
+ executeLog.setExecuteResult("成功");
|
|
|
+ executeLog.setSuccessTime(LocalDateTime.now());
|
|
|
+ adAutoExecuteLogService.updateById(executeLog);
|
|
|
+ } catch (Exception e) {
|
|
|
+ executeLog.setExecuteStatus(2);
|
|
|
+ executeLog.setExecuteResult(e.getMessage());
|
|
|
+ adAutoExecuteLogService.updateById(executeLog);
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断广告是否应该被暂停
|
|
|
+ * 遍历所有规则,有任一规则满足条件则暂停
|
|
|
+ */
|
|
|
+ private boolean shouldSuspend(AdPlanCreative plan, List<AutoPlanRule> rules, Integer newAdDays) {
|
|
|
+ for (AutoPlanRule rule : rules) {
|
|
|
+ if (matchRule(plan, rule, newAdDays)) {
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 单条规则匹配
|
|
|
+ * rule.type: 1-ROI规则, 2-粉价规则
|
|
|
+ * rule.newAd: 1-新广告, 2-老广告
|
|
|
+ *
|
|
|
+ */
|
|
|
+ public boolean matchRule(AdPlanCreative plan, AutoPlanRule rule, Integer newAdDays) {
|
|
|
+ // 判断新老广告
|
|
|
+ boolean isNewAd = isNewAd(plan, newAdDays);
|
|
|
+ if (rule.getNewAd() == 1 && !isNewAd) {
|
|
|
+ return false; // 规则针对新广告,但当前是老广告
|
|
|
+ }
|
|
|
+ if (rule.getNewAd() == 2 && isNewAd) {
|
|
|
+ return false; // 规则针对老广告,但当前是新广告
|
|
|
+ }
|
|
|
+
|
|
|
+ Long cost = Optional.ofNullable(plan.getCost()).orElse(0L);
|
|
|
+ Long constYuan = cost / 100;
|
|
|
+ Long scanFollowUserCost = Optional.ofNullable(plan.getScanFollowUserCost()).orElse(0L);
|
|
|
+ Long scanCostYuan = scanFollowUserCost / 100;
|
|
|
+
|
|
|
+ // 1 roi 2 粉价
|
|
|
+ if (rule.getType() == 1) {
|
|
|
+ if (rule.getMinCost() == 0) {
|
|
|
+ if (plan.getReqIncomeRoi1().compareTo(rule.getMinIor()) < 0) {
|
|
|
+ String s = String.format((isNewAd? "[新广告]" : "[老广告]") + " 不考虑消耗 ROI:%.2f < 阈值:%.2f",
|
|
|
+ plan.getReqIncomeRoi1(), rule.getMinIor());
|
|
|
+ plan.setFailReason(s);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // getReqIncomeRoi1 < minIor 关闭
|
|
|
+ if (constYuan > rule.getMinCost() && plan.getReqIncomeRoi1().compareTo(rule.getMinIor()) < 0) {
|
|
|
+ String s = String.format((isNewAd? "[新广告]" : "[老广告]") + " 消耗%d > 阈值%d ROI:%.2f < 阈值:%.2f",
|
|
|
+ constYuan, rule.getMinCost(), plan.getReqIncomeRoi1(), rule.getMinIor());
|
|
|
+ plan.setFailReason(s);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ } else if (rule.getType() == 2) {
|
|
|
+ // minCost == 0 表示不限制消耗门槛
|
|
|
+ if (rule.getMinCost() == 0) {
|
|
|
+ if (scanCostYuan > rule.getScanMaxCost()) {
|
|
|
+ String s = String.format((isNewAd? "[新广告]" : "[老广告]") + " 不考虑消耗 粉价消耗%d > 阈值%d",
|
|
|
+ scanCostYuan, rule.getScanMaxCost());
|
|
|
+ plan.setFailReason(s);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if (scanCostYuan > rule.getScanMaxCost() && constYuan > rule.getMinCost()) {
|
|
|
+ String s = String.format((isNewAd? "[新广告]" : "[老广告]") + " 消耗%d > 阈值%d; 粉价消耗%d > 阈值%d",
|
|
|
+ constYuan, rule.getMinCost(), scanCostYuan, rule.getScanMaxCost());
|
|
|
+ plan.setFailReason(s);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断是否为新广告创意
|
|
|
+ * 新广告创意定义:成功时间 createTime 距今不超过 newAdDays 天
|
|
|
+ */
|
|
|
+ private boolean isNewAd(AdPlanCreative creative, Integer newAdDays) {
|
|
|
+ LocalDateTime threshold = LocalDateTime.now().minusDays(newAdDays);
|
|
|
+ return creative.getCreateTime().isAfter(threshold);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析规则JSON
|
|
|
+ */
|
|
|
+ public List<AutoPlanRule> parseRules(String rulesJson) {
|
|
|
+ try {
|
|
|
+ List<AutoPlanRule> rules = JSON.parseArray(rulesJson, AutoPlanRule.class);
|
|
|
+ // rules排序 新广告在前 ROI在前 粉价在后,minCost大的在前,老广告在后
|
|
|
+ List<AutoPlanRule> newRules = new ArrayList<>();
|
|
|
+ List<AutoPlanRule> oldRules = new ArrayList<>();
|
|
|
+ rules.forEach(r -> {
|
|
|
+ if (r.getNewAd() == 1) {
|
|
|
+ newRules.add(r);
|
|
|
+ } else {
|
|
|
+ oldRules.add(r);
|
|
|
+ }
|
|
|
+ });
|
|
|
+ // ROI在前 规则类型:1 roi 2 粉价
|
|
|
+ newRules.sort((a, b) -> Integer.compare(a.getType(), b.getType()));
|
|
|
+ oldRules.sort((a, b) -> Integer.compare(a.getType(), b.getType()));
|
|
|
+ newRules.sort((a, b) -> Double.compare(b.getMinCost(), a.getMinCost()));
|
|
|
+ oldRules.sort((a, b) -> Double.compare(b.getMinCost(), a.getMinCost()));
|
|
|
+ newRules.sort((a, b) -> Double.compare(b.getScanMaxCost(), a.getScanMaxCost()));
|
|
|
+ oldRules.sort((a, b) -> Double.compare(b.getScanMaxCost(), a.getScanMaxCost()));
|
|
|
+
|
|
|
+ List<AutoPlanRule> allRules = new ArrayList<>();
|
|
|
+ allRules.addAll(newRules);
|
|
|
+ allRules.addAll(oldRules);
|
|
|
+ return allRules;
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("[AdPlanCreativeAutoExecutor] 解析规则JSON失败: {}", rulesJson, e);
|
|
|
+ return List.of();
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private void updateLogFailed(AdAutoExecuteLog executeLog, String reason) {
|
|
|
+ executeLog.setExecuteStatus(2);
|
|
|
+ executeLog.setExecuteResult(reason);
|
|
|
+ adAutoExecuteLogService.updateById(executeLog);
|
|
|
+ }
|
|
|
+}
|