# 广告投放腾讯接口对接实现

## 目标
实现 [AdPlanConfServiceImpl.asyncTencentInterface](file:///Users/pudongliang/moka_workspace/gdt-auto/src/main/java/com/moka/gdtauto/service/impl/AdPlanConfServiceImpl.java#L130-L135) 方法,完成广告投放的腾讯接口对接。

## 业务逻辑

### 核心流程
遍历 `List<AdPlan> plans`,针对每个 AdPlan:
1. 根据 `adType` 字段区分处理:
   - `adType=1`: 企微广告,使用 `AdgroupsAddReqPool.getAdgroupsAddRequest`
   - `adType=2`: ROI广告,使用 `AdgroupsAddReqPool.getROIAdgroupsAddRequest`
2. 调用 `TencentAdsAdgroupService.createAdgroup` 创建广告组
3. 遍历 `AdPlan.getAdCreatives()`,为每个 `AdPlanCreative` 创建一个动态创意
4. 回写状态:
   - 成功: 更新 `AdPlan.adgroupId`、`AdPlanCreative.creativeId` 和 `status=4`(已结束)
   - 失败: 记录 `status=3`(失败) 和简短错误信息到 `failReason`
5. 单个计划失败不影响其他计划,继续处理

### 状态定义
- `status=1`: 待执行
- `status=2`: 执行中
- `status=3`: 失败
- `status=4`: 已结束(成功)

## 实现步骤

### 步骤1: 注入依赖服务
在 [AdPlanConfServiceImpl](file:///Users/pudongliang/moka_workspace/gdt-auto/src/main/java/com/moka/gdtauto/service/impl/AdPlanConfServiceImpl.java) 中添加依赖:
```java
@Autowired
private TencentAdsAdgroupService tencentAdsAdgroupService;
@Autowired
private TencentAdsApiClientFactory clientFactory;
@Autowired
private TencentAccountService tencentAccountService;
```

### 步骤2: 实现 asyncTencentInterface 方法

#### 2.1 获取 AdPlanConf 信息
- 通过 `plans.get(0).getAdPlanConfId()` 获取配置ID
- 查询 `AdPlanConf` 获取 `jobNumber` 用于API鉴权

#### 2.2 遍历处理每个 AdPlan
```java
for (AdPlan plan : plans) {
    try {
        // 更新状态为执行中
        plan.setStatus(2);
        adPlanService.updateById(plan);
        
        // 根据 adType 构建请求
        AdgroupsAddRequest request = buildAdgroupRequest(plan);
        
        // 获取授权账号的 orgAccountId
        TencentAccount account = tencentAccountService.getOne(
            new LambdaQueryWrapper<TencentAccount>()
                .eq(TencentAccount::getAccountId, plan.getAccountId())
        );
        
        // 创建广告组
        AdgroupsAddResponseData response = tencentAdsAdgroupService.createAdgroup(
            jobNumber, account.getOrgAccountId(), request
        );
        
        // 更新 adgroupId
        plan.setAdgroupId(response.getAdgroupId());
        
        // 创建动态创意
        createDynamicCreatives(plan, jobNumber, account.getOrgAccountId());
        
        // 更新状态为成功
        plan.setStatus(4);
        plan.setSuccessTime(LocalDateTime.now());
        adPlanService.updateById(plan);
        
    } catch (Exception e) {
        // 记录失败
        plan.setStatus(3);
        plan.setFailReason(truncateError(e.getMessage()));
        adPlanService.updateById(plan);
        log.error("创建广告失败, planId={}", plan.getId(), e);
    }
}
```

#### 2.3 构建广告组请求 (buildAdgroupRequest)
参考 [AdgroupsAddReqPool](file:///Users/pudongliang/moka_workspace/gdt-auto/src/main/java/com/moka/gdtauto/util/AdgroupsAddReqPool.java):
```java
private AdgroupsAddRequest buildAdgroupRequest(AdPlan plan) {
    if (plan.getAdType() == 1) {
        // 企微广告
        return AdgroupsAddReqPool.getAdgroupsAddRequest(
            plan.getAccountId(),
            plan.getAdgroupName(),
            plan.getWxCorpid(),
            calculateBeginDate(),  // 明天
            calculateEndDate(),    // 一年后
            parseTargetConfId(plan.getTarget()),
            parseSiteConfId(plan.getSite()),
            plan.getBidAmount(),
            plan.getConversionId()
        );
    } else {
        // ROI广告
        return AdgroupsAddReqPool.getROIAdgroupsAddRequest(
            plan.getAccountId(),
            plan.getAdgroupName(),
            plan.getDramaId(),
            calculateBeginDate(),
            calculateEndDate(),
            parseTargetConfId(plan.getTarget()),
            parseSiteConfId(plan.getSite()),
            plan.getBidAmount(),
            plan.getConversionId()
        );
    }
}
```

#### 2.4 创建动态创意 (createDynamicCreatives)
参考测试用例 [TencentAdsAdgroupCreateTest](file:///Users/pudongliang/moka_workspace/gdt-auto/src/test/java/com/moka/gdtauto/service/TencentAdsAdgroupCreateTest.java#L220-L267):
```java
private void createDynamicCreatives(AdPlan plan, String jobNumber, Long orgAccountId) 
    throws Exception {
    
    TencentAds tencentAds = clientFactory.getTencentAds(jobNumber, orgAccountId);
    
    for (AdPlanCreative creative : plan.getAdCreatives()) {
        try {
            // 更新状态为执行中
            creative.setStatus(2);
            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(4);
            creative.setSuccessTime(LocalDateTime.now());
            adPlanCreativeService.updateById(creative);
            
        } catch (Exception e) {
            creative.setStatus(3);
            creative.setFailReason(truncateError(e.getMessage()));
            adPlanCreativeService.updateById(creative);
            throw e; // 创意失败则整个广告组失败
        }
    }
}
```

#### 2.5 构建动态创意请求 (buildDynamicCreativeRequest)
参考 [TencentAdsAdgroupCreateTest.buildCreativeComponents](file:///Users/pudongliang/moka_workspace/gdt-auto/src/test/java/com/moka/gdtauto/service/TencentAdsAdgroupCreateTest.java#L292-L409):
```java
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;
}
```

#### 2.6 辅助方法
```java
// 截断错误信息到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().plusDays(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}

// 计算结束日期 (一年后)
private String calculateEndDate() {
    return LocalDate.now().plusYears(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}

// 解析定向配置ID (从 AdPlan.target 字段)
private Integer parseTargetConfId(String target) {
    // target 可能是 "1" 或其他格式, 需根据实际情况解析
    return Integer.parseInt(target);
}

// 解析站点配置ID (从 AdPlan.site 字段)
private Integer parseSiteConfId(String site) {
    return Integer.parseInt(site);
}
```

### 步骤3: 更新 AdPlanConf 状态
在所有 AdPlan 处理完成后:
```java
// 统计结果
int totalCount = plans.size();
int successCount = (int) plans.stream().filter(p -> p.getStatus() == 4).count();
int failCount = totalCount - successCount;

// 更新配置状态
AdPlanConf conf = this.getById(confId);
if (failCount == 0) {
    conf.setStatus(4); // 全部成功
    conf.setSuccessTime(LocalDateTime.now());
} else if (successCount == 0) {
    conf.setStatus(3); // 全部失败
    conf.setFailReason("所有广告创建失败");
} else {
    conf.setStatus(3); // 部分失败
    conf.setFailReason(String.format("成功%d个,失败%d个", successCount, failCount));
}
this.updateById(conf);
```

## 关键技术点

### 1. 参数获取
- `AdPlanConf.jobNumber`: 用于API鉴权
- `TencentAccount.orgAccountId`: 通过 `plan.accountId` 查询获取

### 2. 创意组件硬编码
参考测试用例中的固定值:
- 标题: "点击看更多情感美文"、"高品质产品,值得信赖"
- 文案: "点击看更多情感美文"、"点击看更多"  
- 品牌名称: "情感美女", 品牌图片ID: "24319699712"
- 行动按钮文案: 统一使用 "查看详情"

### 3. 落地页处理
- `AdPlanCreative.pageIds` 为逗号分隔的ID字符串
- 需要解析为多个 `JumpinfoComponent`
- 行动按钮数量必须与落地页数量一致

### 4. 异常处理
- 单个 AdPlan 失败继续处理下一个
- 单个 AdPlanCreative 失败则整个 AdPlan 标记为失败
- 记录简短错误信息(最多200字符)

## 文件修改清单
1. [AdPlanConfServiceImpl.java](file:///Users/pudongliang/moka_workspace/gdt-auto/src/main/java/com/moka/gdtauto/service/impl/AdPlanConfServiceImpl.java) - 实现 asyncTencentInterface 方法及辅助方法
