|
|
@@ -6,11 +6,36 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
import com.moka.gdtauto.entity.AdPlanConf;
|
|
|
import com.moka.gdtauto.mapper.AdPlanConfMapper;
|
|
|
import com.moka.gdtauto.service.AdPlanConfService;
|
|
|
+import com.moka.gdtauto.service.AdPlanCreativeService;
|
|
|
+import com.moka.gdtauto.service.TencentAdsAdgroupService;
|
|
|
+import com.moka.gdtauto.service.TencentAccountService;
|
|
|
+import com.moka.gdtauto.client.TencentAdsApiClientFactory;
|
|
|
+import com.moka.gdtauto.entity.TencentAccount;
|
|
|
+
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
import java.time.LocalDateTime;
|
|
|
+import java.time.LocalDate;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+import java.util.stream.Collectors;
|
|
|
+import java.util.ArrayList;
|
|
|
+import com.moka.gdtauto.entity.AdPlan;
|
|
|
+import com.moka.gdtauto.entity.AdPlanCreative;
|
|
|
+import com.moka.gdtauto.service.AdPlanService;
|
|
|
+
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
+import org.springframework.scheduling.annotation.Async;
|
|
|
+
|
|
|
+import com.tencent.ads.model.v3.*;
|
|
|
+import com.tencent.ads.v3.TencentAds;
|
|
|
+import com.moka.gdtauto.util.AdgroupsAddReqPool;
|
|
|
+import com.moka.gdtauto.common.AdPlanStatus;
|
|
|
+
|
|
|
+
|
|
|
|
|
|
/**
|
|
|
* 广告计划配置服务实现类
|
|
|
@@ -22,6 +47,17 @@ import java.time.LocalDateTime;
|
|
|
@Service
|
|
|
public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanConf> implements AdPlanConfService {
|
|
|
|
|
|
+ @Autowired
|
|
|
+ private AdPlanService adPlanService;
|
|
|
+ @Autowired
|
|
|
+ private AdPlanCreativeService adPlanCreativeService;
|
|
|
+ @Autowired
|
|
|
+ private TencentAdsAdgroupService tencentAdsAdgroupService;
|
|
|
+ @Autowired
|
|
|
+ private TencentAdsApiClientFactory clientFactory;
|
|
|
+ @Autowired
|
|
|
+ private TencentAccountService tencentAccountService;
|
|
|
+
|
|
|
@Override
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
public Long createAdPlanConf(AdPlanConf adPlanConf) {
|
|
|
@@ -95,4 +131,422 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
|
|
|
.eq(AdPlanConf::getUserId, userId)
|
|
|
.eq(AdPlanConf::getDeleted, 0));
|
|
|
}
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public void generatePlan(List<AdPlan> plans) {
|
|
|
+ // 实现生成计划的逻辑
|
|
|
+ log.info("生成计划,计划列表: {}", plans);
|
|
|
+ for (AdPlan plan : plans) {
|
|
|
+ adPlanService.save(plan);
|
|
|
+ Long planId = plan.getId();
|
|
|
+
|
|
|
+ plan.getAdCreatives().forEach(adPlanCreative -> {
|
|
|
+ adPlanCreative.setAdPlanId(planId);
|
|
|
+ adPlanCreative.setAccountId(plan.getAccountId());
|
|
|
+ adPlanCreativeService.save(adPlanCreative);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Async
|
|
|
+ @Override
|
|
|
+ public void asyncReGeneratePlan(Long confId) {
|
|
|
+ log.info("异步重新生成计划开始,配置ID: {}", confId);
|
|
|
+ // 根据配置ID获取计划列表
|
|
|
+ List<AdPlan> plans = adPlanService.getPlansByConfId(confId);
|
|
|
+ List<AdPlanCreative> creatives = adPlanCreativeService.getCreativesByConfId(confId);
|
|
|
+ //group by planId
|
|
|
+ Map<Long, List<AdPlanCreative>> creativeMap = creatives.stream().collect(Collectors.groupingBy(AdPlanCreative::getAdPlanId));
|
|
|
+ for (AdPlan plan : plans) {
|
|
|
+ plan.setAdCreatives(creativeMap.getOrDefault(plan.getId(), new ArrayList<AdPlanCreative>()));
|
|
|
+ }
|
|
|
+ asyncTencentInterface(plans);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Async
|
|
|
+ @Override
|
|
|
+ public void asyncTencentInterface(List<AdPlan> plans) {
|
|
|
+ log.info("异步对接腾讯接口开始,计划数量: {}", plans.size());
|
|
|
+
|
|
|
+ if (plans == null || plans.isEmpty()) {
|
|
|
+ log.warn("计划列表为空,无需处理");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取配置信息
|
|
|
+ Long confId = plans.get(0).getAdPlanConfId();
|
|
|
+ AdPlanConf conf = this.getById(confId);
|
|
|
+ if (conf == null) {
|
|
|
+ log.error("未找到广告计划配置,confId={}", confId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ String jobNumber = conf.getJobNumber();
|
|
|
+
|
|
|
+ // 遍历处理每个 AdPlan
|
|
|
+ for (AdPlan plan : plans) {
|
|
|
+ try {
|
|
|
+ log.info("开始处理广告计划,planId={}, adgroupName={}", plan.getId(), plan.getAdgroupName());
|
|
|
+
|
|
|
+ if (plan.getAdgroupId() > 0 || plan.getStatus() == AdPlanStatus.SUCCESS)
|
|
|
+ continue;
|
|
|
+
|
|
|
+ // 更新状态为执行中
|
|
|
+ plan.setStatus(AdPlanStatus.IN_PROGRESS);
|
|
|
+ adPlanService.updateById(plan);
|
|
|
+
|
|
|
+ // 根据 adType 构建请求
|
|
|
+ AdgroupsAddRequest request = buildAdgroupRequest(plan);
|
|
|
+
|
|
|
+ // 获取授权账号的 orgAccountId
|
|
|
+ // TencentAccount account = tencentAccountService.getOne(
|
|
|
+ // new LambdaQueryWrapper<TencentAccount>()
|
|
|
+ // .eq(TencentAccount::getAccountId, plan.getAccountId())
|
|
|
+ // .eq(TencentAccount::getDeleted, 0)
|
|
|
+ // );
|
|
|
+
|
|
|
+ // if (account == null) {
|
|
|
+ // throw new RuntimeException("未找到账户信息,accountId=" + plan.getAccountId());
|
|
|
+ // }
|
|
|
+
|
|
|
+ // 创建广告组
|
|
|
+ log.info("调用腾讯API创建广告组,accountId={}, orgAccountId={}", plan.getAccountId(), 33453856L);
|
|
|
+ AdgroupsAddResponseData response = tencentAdsAdgroupService.createAdgroup(
|
|
|
+ jobNumber, 33453856L, request
|
|
|
+ );
|
|
|
+
|
|
|
+ // 更新 adgroupId
|
|
|
+ plan.setAdgroupId(response.getAdgroupId());
|
|
|
+ log.info("广告组创建成功,adgroupId={}", response.getAdgroupId());
|
|
|
+
|
|
|
+ // 更新状态为成功
|
|
|
+ plan.setStatus(AdPlanStatus.SUCCESS);
|
|
|
+ plan.setSuccessTime(LocalDateTime.now());
|
|
|
+ adPlanService.updateById(plan);
|
|
|
+
|
|
|
+ // 创建动态创意
|
|
|
+ createDynamicCreatives(plan, jobNumber, 33453856L);
|
|
|
+
|
|
|
+ log.info("广告计划处理成功,planId={}, adgroupId={}", plan.getId(), plan.getAdgroupId());
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ // 记录失败
|
|
|
+ log.error("创建广告失败, planId={}", plan.getId(), e);
|
|
|
+ plan.setStatus(AdPlanStatus.FAILED);
|
|
|
+ plan.setFailReason(truncateError(e.getMessage()));
|
|
|
+ adPlanService.updateById(plan);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 更新 AdPlanConf 状态
|
|
|
+ updateAdPlanConfStatus(confId, plans);
|
|
|
+
|
|
|
+ log.info("异步对接腾讯接口完成,confId={}", confId);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建广告组请求
|
|
|
+ */
|
|
|
+ private AdgroupsAddRequest buildAdgroupRequest(AdPlan plan) {
|
|
|
+ if (plan.getAdType() == 1) {
|
|
|
+ // 企微广告
|
|
|
+ return AdgroupsAddReqPool.getAdgroupsAddRequest(
|
|
|
+ plan.getAccountId(),
|
|
|
+ plan.getAdgroupName(),
|
|
|
+ plan.getWxCorpid(),
|
|
|
+ calculateBeginDate(),
|
|
|
+ "",
|
|
|
+ parseTargetConfId(plan.getTarget()),
|
|
|
+ parseSiteConfId(plan.getSite()),
|
|
|
+ plan.getBidAmount(),
|
|
|
+ plan.getConversionId()
|
|
|
+ );
|
|
|
+ } else {
|
|
|
+ // ROI广告
|
|
|
+ return AdgroupsAddReqPool.getROIAdgroupsAddRequest(
|
|
|
+ plan.getAccountId(),
|
|
|
+ plan.getAdgroupName(),
|
|
|
+ plan.getDramaId(),
|
|
|
+ calculateBeginDate(),
|
|
|
+ "",
|
|
|
+ parseTargetConfId(plan.getTarget()),
|
|
|
+ parseSiteConfId(plan.getSite()),
|
|
|
+ plan.getBidAmount(),
|
|
|
+ plan.getConversionId()
|
|
|
+ );
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 创建动态创意
|
|
|
+ */
|
|
|
+ private void createDynamicCreatives(AdPlan plan, String jobNumber, Long orgAccountId)
|
|
|
+ throws Exception {
|
|
|
+
|
|
|
+ TencentAds tencentAds = clientFactory.getTencentAds(jobNumber, orgAccountId);
|
|
|
+
|
|
|
+ for (AdPlanCreative creative : plan.getAdCreatives()) {
|
|
|
+ try {
|
|
|
+ log.info("开始创建动态创意,planId={}, creativeName={}", plan.getId(), creative.getCreativeName());
|
|
|
+
|
|
|
+ if (creative.getCreativeId() > 0 || creative.getStatus() == AdPlanStatus.SUCCESS)
|
|
|
+ continue;
|
|
|
+
|
|
|
+ // 更新状态为执行中
|
|
|
+ creative.setStatus(AdPlanStatus.IN_PROGRESS);
|
|
|
+ creative.setAdgroupId(plan.getAdgroupId());
|
|
|
+ adPlanCreativeService.updateById(creative);
|
|
|
+
|
|
|
+ // 构建动态创意请求
|
|
|
+ DynamicCreativesAddRequest request = buildDynamicCreativeRequest(plan, creative);
|
|
|
+
|
|
|
+ // 调用API创建
|
|
|
+ DynamicCreativesAddResponseData response =
|
|
|
+ tencentAds.dynamicCreatives().dynamicCreativesAdd(request);
|
|
|
+
|
|
|
+ // 更新创意ID和状态
|
|
|
+ creative.setCreativeId(response.getDynamicCreativeId());
|
|
|
+ creative.setStatus(AdPlanStatus.SUCCESS);
|
|
|
+ creative.setSuccessTime(LocalDateTime.now());
|
|
|
+ adPlanCreativeService.updateById(creative);
|
|
|
+
|
|
|
+ log.info("动态创意创建成功,creativeId={}", response.getDynamicCreativeId());
|
|
|
+
|
|
|
+ } catch (Exception e) {
|
|
|
+ log.error("创建动态创意失败, creativeId={}", creative.getId(), e);
|
|
|
+ creative.setStatus(AdPlanStatus.FAILED);
|
|
|
+ creative.setFailReason(truncateError(e.getMessage()));
|
|
|
+ adPlanCreativeService.updateById(creative);
|
|
|
+ throw e; // 创意失败则整个广告组失败
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建动态创意请求
|
|
|
+ */
|
|
|
+ private DynamicCreativesAddRequest buildDynamicCreativeRequest(AdPlan plan, AdPlanCreative creative) {
|
|
|
+ DynamicCreativesAddRequest request = new DynamicCreativesAddRequest();
|
|
|
+ request.setAccountId(plan.getAccountId());
|
|
|
+ request.setAdgroupId(plan.getAdgroupId());
|
|
|
+ request.setDynamicCreativeName(creative.getCreativeName());
|
|
|
+ request.setDeliveryMode(DeliveryMode.COMPONENT);
|
|
|
+ request.setDynamicCreativeType(DynamicCreativeType.PROGRAM);
|
|
|
+
|
|
|
+ CreativeComponents components = new CreativeComponents();
|
|
|
+
|
|
|
+ // 标题组件 (固定标题)
|
|
|
+ components.setTitle(buildTitleComponents());
|
|
|
+
|
|
|
+ // 文案组件 (固定文案)
|
|
|
+ components.setDescription(buildDescriptionComponents());
|
|
|
+
|
|
|
+ // 图片组件 (从 creative.imageId)
|
|
|
+ components.setImage(buildImageComponents(creative.getImageId()));
|
|
|
+
|
|
|
+ // 品牌形象 (固定品牌)
|
|
|
+ components.setBrand(buildBrandComponents());
|
|
|
+
|
|
|
+ // 落地页 (从 creative.pageIds, 逗号分隔)
|
|
|
+ components.setMainJumpInfo(buildJumpInfoComponents(creative.getPageIds()));
|
|
|
+
|
|
|
+ // 行动按钮 (数量与落地页一致, 文案统一)
|
|
|
+ components.setActionButton(buildActionButtonComponents(creative.getPageIds()));
|
|
|
+
|
|
|
+ request.setCreativeComponents(components);
|
|
|
+ return request;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建标题组件
|
|
|
+ */
|
|
|
+ private List<TitleComponent> buildTitleComponents() {
|
|
|
+ List<TitleComponent> titles = new ArrayList<>();
|
|
|
+
|
|
|
+ TitleComponent title1 = new TitleComponent();
|
|
|
+ TitleStruct titleValue1 = new TitleStruct();
|
|
|
+ titleValue1.setContent("情感美文点点看");
|
|
|
+ title1.setValue(titleValue1);
|
|
|
+ titles.add(title1);
|
|
|
+
|
|
|
+ return titles;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建文案组件
|
|
|
+ */
|
|
|
+ private List<DescriptionComponent> buildDescriptionComponents() {
|
|
|
+ List<DescriptionComponent> descriptions = new ArrayList<>();
|
|
|
+
|
|
|
+ DescriptionComponent desc1 = new DescriptionComponent();
|
|
|
+ DescriptionStruct descValue1 = new DescriptionStruct();
|
|
|
+ descValue1.setContent("情感美文点点看");
|
|
|
+ desc1.setValue(descValue1);
|
|
|
+ descriptions.add(desc1);
|
|
|
+
|
|
|
+ return descriptions;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建图片组件
|
|
|
+ */
|
|
|
+ private List<ImageComponent> buildImageComponents(Long imageId) {
|
|
|
+ List<ImageComponent> images = new ArrayList<>();
|
|
|
+
|
|
|
+ ImageComponent image = new ImageComponent();
|
|
|
+ ImageStruct imageValue = new ImageStruct();
|
|
|
+ imageValue.setImageId(String.valueOf(imageId));
|
|
|
+ image.setValue(imageValue);
|
|
|
+ images.add(image);
|
|
|
+
|
|
|
+ return images;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建品牌组件
|
|
|
+ */
|
|
|
+ private List<BrandComponent> buildBrandComponents() {
|
|
|
+ List<BrandComponent> brands = new ArrayList<>();
|
|
|
+
|
|
|
+ BrandComponent brand = new BrandComponent();
|
|
|
+ BrandStruct brandValue = new BrandStruct();
|
|
|
+ brandValue.setBrandName("情感美文");
|
|
|
+ brandValue.setBrandImageId("30026044695");
|
|
|
+ brand.setValue(brandValue);
|
|
|
+ brands.add(brand);
|
|
|
+
|
|
|
+ return brands;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建落地页组件
|
|
|
+ */
|
|
|
+ private List<JumpinfoComponent> buildJumpInfoComponents(String pageIds) {
|
|
|
+ List<JumpinfoComponent> jumpInfoList = new ArrayList<>();
|
|
|
+
|
|
|
+ if (pageIds == null || pageIds.isEmpty()) {
|
|
|
+ return jumpInfoList;
|
|
|
+ }
|
|
|
+
|
|
|
+ String[] pageIdArray = pageIds.split(",");
|
|
|
+ for (String pageIdStr : pageIdArray) {
|
|
|
+ Long pageId = Long.parseLong(pageIdStr.trim());
|
|
|
+
|
|
|
+ JumpinfoComponent jumpinfo = new JumpinfoComponent();
|
|
|
+ JumpinfoStruct jumpinfoValue = new JumpinfoStruct();
|
|
|
+ PageSpec pageSpec = new PageSpec();
|
|
|
+ XjPageSpec xjPageSpec = new XjPageSpec();
|
|
|
+ xjPageSpec.setPageId(pageId);
|
|
|
+ pageSpec.setOfficialSpec(xjPageSpec);
|
|
|
+ jumpinfoValue.setPageSpec(pageSpec);
|
|
|
+ jumpinfoValue.setPageType(PageType.OFFICIAL);
|
|
|
+ jumpinfo.setValue(jumpinfoValue);
|
|
|
+ jumpInfoList.add(jumpinfo);
|
|
|
+ }
|
|
|
+
|
|
|
+ return jumpInfoList;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建行动按钮组件
|
|
|
+ */
|
|
|
+ private List<ActionButtonComponent> buildActionButtonComponents(String pageIds) {
|
|
|
+ List<ActionButtonComponent> actionButtons = new ArrayList<>();
|
|
|
+
|
|
|
+ if (pageIds == null || pageIds.isEmpty()) {
|
|
|
+ return actionButtons;
|
|
|
+ }
|
|
|
+
|
|
|
+ String[] pageIdArray = pageIds.split(",");
|
|
|
+ for (int i = 0; i < pageIdArray.length; i++) {
|
|
|
+ ActionButtonComponent actionButton = new ActionButtonComponent();
|
|
|
+ ActionButtonStruct actionButtonValue = new ActionButtonStruct();
|
|
|
+ actionButtonValue.setButtonText("查看详情");
|
|
|
+ actionButton.setValue(actionButtonValue);
|
|
|
+ actionButtons.add(actionButton);
|
|
|
+ }
|
|
|
+
|
|
|
+ return actionButtons;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 更新 AdPlanConf 状态
|
|
|
+ */
|
|
|
+ private void updateAdPlanConfStatus(Long confId, List<AdPlan> plans) {
|
|
|
+ // 统计结果
|
|
|
+ int totalCount = plans.size();
|
|
|
+ int successCount = (int) plans.stream().filter(p -> AdPlanStatus.SUCCESS.equals(p.getStatus())).count();
|
|
|
+ int failCount = totalCount - successCount;
|
|
|
+
|
|
|
+ // 更新配置状态
|
|
|
+ AdPlanConf conf = this.getById(confId);
|
|
|
+ if (conf == null) {
|
|
|
+ log.error("未找到广告计划配置,confId={}", confId);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (failCount == 0) {
|
|
|
+ conf.setStatus(AdPlanStatus.SUCCESS); // 全部成功
|
|
|
+ conf.setSuccessTime(LocalDateTime.now());
|
|
|
+ conf.setFailReason(null);
|
|
|
+ } else if (successCount == 0) {
|
|
|
+ conf.setStatus(AdPlanStatus.FAILED); // 全部失败
|
|
|
+ conf.setFailReason("所有广告创建失败");
|
|
|
+ } else {
|
|
|
+ conf.setStatus(AdPlanStatus.FAILED); // 部分失败
|
|
|
+ conf.setFailReason(String.format("成功%d个,失败%d个", successCount, failCount));
|
|
|
+ }
|
|
|
+
|
|
|
+ this.updateById(conf);
|
|
|
+ log.info("更新广告计划配置状态,confId={}, status={}, 成功:{}, 失败:{}",
|
|
|
+ confId, conf.getStatus(), successCount, failCount);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 截断错误信息到200字符
|
|
|
+ */
|
|
|
+ private String truncateError(String error) {
|
|
|
+ if (error == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ return error.length() > 200 ? error.substring(0, 200) + "..." : error;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 计算开始日期 (明天)
|
|
|
+ */
|
|
|
+ private String calculateBeginDate() {
|
|
|
+ return LocalDate.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析定向配置ID
|
|
|
+ */
|
|
|
+ private Integer parseTargetConfId(String target) {
|
|
|
+ if (target == null || target.isEmpty()) {
|
|
|
+ return 1; // 默认值
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return Integer.parseInt(target.trim());
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ log.warn("解析定向配置ID失败,使用默认值1,target={}", target);
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析站点配置ID
|
|
|
+ */
|
|
|
+ private Integer parseSiteConfId(String site) {
|
|
|
+ if (site == null || site.isEmpty()) {
|
|
|
+ return 1; // 默认值
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return Integer.parseInt(site.trim());
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ log.warn("解析站点配置ID失败,使用默认值1,site={}", site);
|
|
|
+ return 1;
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|