瀏覽代碼

feat:计划生成

pudongliang 4 月之前
父節點
當前提交
15cf8b2cb4
共有 40 個文件被更改,包括 2398 次插入224 次删除
  1. 277 0
      .qoder/plans/广告投放腾讯接口对接实现_1c3946fb.md
  2. 43 0
      portal/docs/conf/conf.d/front.conf
  3. 38 0
      portal/docs/conf/conf.d/front_release.conf
  4. 38 0
      portal/docs/conf/nginx.conf
  5. 12 0
      portal/jsconfig.json
  6. 1 1
      portal/package.json
  7. 42 0
      portal/src/api/adPlanConf.js
  8. 3 3
      portal/src/layout/index.vue
  9. 6 0
      portal/src/router/index.js
  10. 454 0
      portal/src/views/ad-plan-conf/detail.vue
  11. 75 8
      portal/src/views/ad-plan-conf/index.vue
  12. 90 87
      portal/src/views/dashboard/index.vue
  13. 1 1
      portal/src/views/image/index.vue
  14. 2 0
      src/main/java/com/moka/gdtauto/GdtAutoApplication.java
  15. 59 0
      src/main/java/com/moka/gdtauto/common/AdPlanStatus.java
  16. 90 1
      src/main/java/com/moka/gdtauto/controller/AdPlanConfController.java
  17. 4 77
      src/main/java/com/moka/gdtauto/controller/AdPlanController.java
  18. 1 1
      src/main/java/com/moka/gdtauto/controller/SysUserController.java
  19. 1 1
      src/main/java/com/moka/gdtauto/controller/TencentImagesController.java
  20. 3 0
      src/main/java/com/moka/gdtauto/controller/vo/UpdateAdPlanConfRequest.java
  21. 2 0
      src/main/java/com/moka/gdtauto/entity/AdPlan.java
  22. 15 0
      src/main/java/com/moka/gdtauto/entity/AdPlanConf.java
  23. 3 1
      src/main/java/com/moka/gdtauto/entity/AdPlanCreative.java
  24. 9 0
      src/main/java/com/moka/gdtauto/service/AdPlanConfService.java
  25. 2 0
      src/main/java/com/moka/gdtauto/service/AdPlanCreativeService.java
  26. 5 1
      src/main/java/com/moka/gdtauto/service/AdPlanService.java
  27. 1 1
      src/main/java/com/moka/gdtauto/service/TencentImagesService.java
  28. 0 15
      src/main/java/com/moka/gdtauto/service/impl/AccountGroupServiceImpl.java
  29. 454 0
      src/main/java/com/moka/gdtauto/service/impl/AdPlanConfServiceImpl.java
  30. 8 0
      src/main/java/com/moka/gdtauto/service/impl/AdPlanCreativeServiceImpl.java
  31. 12 1
      src/main/java/com/moka/gdtauto/service/impl/AdPlanServiceImpl.java
  32. 55 21
      src/main/java/com/moka/gdtauto/service/impl/DashboardServiceImpl.java
  33. 4 2
      src/main/java/com/moka/gdtauto/service/impl/TencentImagesServiceImpl.java
  34. 190 0
      src/main/java/com/moka/gdtauto/util/AdgroupsAddReqPool.java
  35. 22 0
      src/main/java/com/moka/gdtauto/util/SiteConfUtil.java
  36. 95 0
      src/main/java/com/moka/gdtauto/util/TargetConfUtil.java
  37. 231 0
      src/test/java/com/moka/gdtauto/service/AdPlanConfAsyncTest.java
  38. 39 0
      src/test/java/com/moka/gdtauto/service/SysUserTest.java
  39. 0 1
      src/test/java/com/moka/gdtauto/service/TencentAdsAdgroupCreateTest.java
  40. 11 1
      src/test/java/com/moka/gdtauto/util/SampleTest.java

+ 277 - 0
.qoder/plans/广告投放腾讯接口对接实现_1c3946fb.md

@@ -0,0 +1,277 @@
+# 广告投放腾讯接口对接实现
+
+## 目标
+实现 [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 方法及辅助方法

+ 43 - 0
portal/docs/conf/conf.d/front.conf

@@ -0,0 +1,43 @@
+server{
+    listen 80;
+    server_name  localhost;
+    client_max_body_size 10200M;
+    root /usr/share/nginx/html/public;
+    index  index.html index.htm;
+
+    proxy_intercept_errors on;
+    fastcgi_intercept_errors on;
+
+    location /  {
+              proxy_set_header    Host             $host;
+              proxy_set_header    X-Real-IP        $remote_addr;
+              proxy_set_header    X-Forwarded-For  $proxy_add_x_forwarded_for;
+              proxy_set_header    X-Client-Verify  SUCCESS;
+              proxy_set_header    X-Client-DN      $ssl_client_s_dn;
+              proxy_set_header    X-SSL-Subject    $ssl_client_s_dn;
+              proxy_set_header    X-SSL-Issuer     $ssl_client_i_dn;
+              proxy_read_timeout 18000;
+              proxy_connect_timeout 18000;
+              proxy_send_timeout      18000;
+              client_max_body_size 10200M;
+             try_files $uri $uri/ /index.html;
+
+
+                add_header Access-Control-Allow-Origin $http_origin;
+                add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
+                add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
+
+               if ($request_method = 'OPTIONS') {
+                    return 204;
+                 }
+             }
+
+    location /api/ {
+        proxy_pass http://vnovel-admin-h5-in-svc/;
+        proxy_read_timeout 18000;
+        proxy_connect_timeout 18000;
+        proxy_send_timeout      18000;
+        client_max_body_size 10200M;
+    }
+    error_page  405     =200 $uri;
+}

+ 38 - 0
portal/docs/conf/conf.d/front_release.conf

@@ -0,0 +1,38 @@
+server{
+    listen 80;
+    server_name  localhost;
+    client_max_body_size 1020M;
+    root /usr/share/nginx/html/public;
+    index  index.html index.htm;
+
+    proxy_intercept_errors on;
+    fastcgi_intercept_errors on;
+
+    location /  {
+              proxy_set_header    Host             $host;
+              proxy_set_header    X-Real-IP        $remote_addr;
+              proxy_set_header    X-Forwarded-For  $proxy_add_x_forwarded_for;
+              proxy_set_header    X-Client-Verify  SUCCESS;
+              proxy_set_header    X-Client-DN      $ssl_client_s_dn;
+              proxy_set_header    X-SSL-Subject    $ssl_client_s_dn;
+              proxy_set_header    X-SSL-Issuer     $ssl_client_i_dn;
+              proxy_read_timeout 1800;
+              proxy_connect_timeout 1800;
+              client_max_body_size 1020M;
+             try_files $uri $uri/ /index.html;
+
+
+                add_header Access-Control-Allow-Origin $http_origin;
+                add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS';
+                add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization';
+
+               if ($request_method = 'OPTIONS') {
+                    return 204;
+                 }
+             }
+
+    location /api/ {
+        proxy_pass http://gdt-auto-pro-in-svc/;
+    }
+    error_page  405     =200 $uri;
+}

+ 38 - 0
portal/docs/conf/nginx.conf

@@ -0,0 +1,38 @@
+user  nginx;
+worker_processes  auto;
+
+error_log  /var/log/nginx/error.log warn;
+pid        /var/run/nginx.pid;
+
+
+events {
+    worker_connections  20480;
+}
+
+
+http {
+    include       /etc/nginx/mime.types;
+    default_type  application/octet-stream;
+	proxy_intercept_errors on;
+    fastcgi_intercept_errors on;
+    fastcgi_buffer_size 128k;
+    fastcgi_buffers 256 16k;
+    client_body_buffer_size 4096m;
+    client_max_body_size 1024m;
+
+    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
+                      '$status $body_bytes_sent "$http_referer" '
+                      '"$http_user_agent" "$http_x_forwarded_for"';
+
+    access_log  /var/log/nginx/access.log  main;
+
+    sendfile        on;
+    #tcp_nopush     on;
+
+    keepalive_timeout  65;
+
+    gzip  on;
+
+    include /etc/nginx/conf.d/*.conf;
+
+}

+ 12 - 0
portal/jsconfig.json

@@ -0,0 +1,12 @@
+{
+  "compilerOptions": {
+    "target": "ES6",
+    "module": "commonjs",
+    "allowSyntheticDefaultImports": true,
+    "baseUrl": "./",
+    "paths": {
+      "@/*": ["src/*"]
+    }
+  },
+  "exclude": ["node_modules", "dist"]
+}

+ 1 - 1
portal/package.json

@@ -5,7 +5,7 @@
   "type": "module",
   "scripts": {
     "dev": "vite",
-    "build": "vite build",
+    "build": "vite build --mode production", 
     "preview": "vite preview"
   },
   "dependencies": {

+ 42 - 0
portal/src/api/adPlanConf.js

@@ -8,6 +8,21 @@ export function reCreateAdPlanConf(data) {
   })
 }
 
+export function generatePlan(data) {
+  return request({
+    url: '/api/ad-plan-conf/generate-plan',
+    method: 'post',
+    data
+  })
+}
+
+export function reGeneratePlan(confId) {
+  return request({
+    url: `/api/ad-plan-conf/reGeneratePlan/${confId}`,
+    method: 'get'
+  })
+}
+
 
 /**
  * 创建广告计划配置
@@ -67,3 +82,30 @@ export function getAdPlanConfDetail(confId) {
     method: 'get'
   })
 }
+
+/**
+ * 获取广告计划列表(分页)
+ * @param {Number} confId 配置ID
+ * @param {Number} accountId 账户ID(可选)
+ * @param {Number} status 状态(可选)
+ * @param {Number} pageNum 页码
+ * @param {Number} pageSize 每页数量
+ */
+export function getAdPlanList(params) {
+  return request({
+    url: '/api/ad-plan/list',
+    method: 'get',
+    params
+  })
+}
+
+/**
+ * 获取广告计划创意列表
+ * @param {Number} planId 计划ID
+ */
+export function getAdPlanCreativesByPlanId(planId) {
+  return request({
+    url: `/api/ad-plan-creative/plan/${planId}`,
+    method: 'get'
+  })
+}

+ 3 - 3
portal/src/layout/index.vue

@@ -59,7 +59,7 @@
               <el-icon size="20">
                 <User />
               </el-icon>
-              <span class="text-sm font-medium">{{ userJobNumber }}</span>
+              <span class="text-sm font-medium">{{ userName }}</span>
               <el-icon size="14">
                 <ArrowDown />
               </el-icon>
@@ -104,7 +104,7 @@ const route = useRoute()
 const router = useRouter()
 
 // 用户信息
-const userJobNumber = ref('')
+const userName = ref('')
 const userInfo = ref(null)
 
 // 加载用户信息
@@ -112,7 +112,7 @@ const loadUserInfo = () => {
   const storedUserInfo = localStorage.getItem('userInfo')
   if (storedUserInfo) {
     userInfo.value = JSON.parse(storedUserInfo)
-    userJobNumber.value = userInfo.value.jobNumber || '未知'
+    userName.value = userInfo.value.name || '未知'
   }
 }
 

+ 6 - 0
portal/src/router/index.js

@@ -84,6 +84,12 @@ const routes = [
         name: 'AdPlanConf',
         component: () => import('@/views/ad-plan-conf/index.vue'),
         meta: { title: '广告计划配置', icon: 'Setting' }
+      },
+      {
+        path: 'ad-plan-conf/detail',
+        name: 'AdPlanConfDetail',
+        component: () => import('@/views/ad-plan-conf/detail.vue'),
+        meta: { title: '广告计划配置详情', icon: 'Document', hideInMenu: true }
       }
     ]
   }

+ 454 - 0
portal/src/views/ad-plan-conf/detail.vue

@@ -0,0 +1,454 @@
+<template>
+  <div class="space-y-6">
+    <!-- 返回按钮 -->
+    <div>
+      <el-button @click="goBack">
+        <el-icon><ArrowLeft /></el-icon>
+        返回
+      </el-button>
+    </div>
+
+    <!-- 配置信息卡片 -->
+    <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
+      <div class="border-b border-gray-200 pb-4 mb-6">
+        <h2 class="text-xl font-semibold text-gray-800 flex items-center gap-2">
+          <el-icon><Document /></el-icon>
+          配置信息
+        </h2>
+      </div>
+      
+      <div v-if="confDetail">
+        <!-- 主要信息:名称和状态 -->
+        <div class="flex items-center gap-6 mb-4">
+          <div class="flex-1">
+            <div class="text-sm text-gray-500 mb-1">计划名称</div>
+            <div class="text-lg font-semibold">{{ confDetail.name }}</div>
+          </div>
+          <div>
+            <div class="text-sm text-gray-500 mb-1">状态</div>
+            <el-tag :type="getPlanStatusColor(confDetail.status)" size="large">
+              {{ getPlanStatusText(confDetail.status) }}
+            </el-tag>
+          </div>
+        </div>
+
+        <!-- 折叠面板:详细信息 -->
+        <el-collapse v-model="activeCollapse" class="mt-4">
+          <el-collapse-item title="详细配置信息" name="detail">
+            <div class="grid grid-cols-2 gap-6 p-4">
+              <div class="space-y-2">
+                <div class="text-sm text-gray-500">配置ID</div>
+                <div class="text-base font-medium">{{ confDetail.id }}</div>
+              </div>
+              <div class="space-y-2">
+                <div class="text-sm text-gray-500">广告类型</div>
+                <el-tag :type="getAdTypeColor(confDetail.adType)">
+                  {{ getAdTypeText(confDetail.adType) }}
+                </el-tag>
+              </div>
+              <div class="space-y-2">
+                <div class="text-sm text-gray-500">出价金额</div>
+                <div class="text-base font-medium">¥{{ (confDetail.bidAmount / 100).toFixed(2) }}</div>
+              </div>
+              <div class="space-y-2">
+                <div class="text-sm text-gray-500">账号组ID</div>
+                <div class="text-base font-medium">{{ confDetail.accountGroupId }}</div>
+              </div>
+              <div class="space-y-2">
+                <div class="text-sm text-gray-500">转化ID</div>
+                <div class="text-base font-medium">{{ confDetail.conversionId }}</div>
+              </div>
+              <div class="space-y-2">
+                <div class="text-sm text-gray-500">定向模版</div>
+                <div class="text-base font-medium">{{ confDetail.targets || '-' }}</div>
+              </div>
+              <div class="space-y-2">
+                <div class="text-sm text-gray-500">版位模版</div>
+                <div class="text-base font-medium">{{ confDetail.sites || '-' }}</div>
+              </div>
+              <div class="space-y-2" v-if="confDetail.adType === 1">
+                <div class="text-sm text-gray-500">企微主体ID</div>
+                <div class="text-base font-medium">{{ confDetail.wxCorpid || '-' }}</div>
+              </div>
+              <div class="space-y-2" v-if="confDetail.adType === 2">
+                <div class="text-sm text-gray-500">短剧ID</div>
+                <div class="text-base font-medium">{{ confDetail.dramaId || '-' }}</div>
+              </div>
+              <div class="space-y-2">
+                <div class="text-sm text-gray-500">图片IDs</div>
+                <div class="text-base font-medium">{{ confDetail.imageIds || '-' }}</div>
+              </div>
+              <div class="space-y-2">
+                <div class="text-sm text-gray-500">落地页IDs</div>
+                <div class="text-base font-medium">{{ confDetail.pageIds || '-' }}</div>
+              </div>
+              <div class="space-y-2" v-if="confDetail.failReason">
+                <div class="text-sm text-gray-500">失败原因</div>
+                <div class="text-base text-red-500">{{ confDetail.failReason }}</div>
+              </div>
+              <div class="space-y-2">
+                <div class="text-sm text-gray-500">创建时间</div>
+                <div class="text-base font-medium">{{ formatDate(confDetail.createTime) }}</div>
+              </div>
+              <div class="space-y-2">
+                <div class="text-sm text-gray-500">更新时间</div>
+                <div class="text-base font-medium">{{ formatDate(confDetail.updateTime) }}</div>
+              </div>
+            </div>
+          </el-collapse-item>
+        </el-collapse>
+      </div>
+      
+      <div v-else v-loading="confLoading" style="min-height: 200px"></div>
+    </div>
+
+    <!-- 广告计划列表卡片 -->
+    <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
+      <div class="border-b border-gray-200 pb-4 mb-6">
+        <!-- 左右布局 -->
+        <div class="flex items-center justify-between">
+          <h2 class="text-xl font-semibold text-gray-800 flex items-center gap-2">
+          <el-icon><List /></el-icon>
+          广告计划列表
+        </h2>
+        <el-button type="primary" @click="handleReGeneratePlan(confDetail.id)">重新生成计划</el-button>
+        </div>
+        
+      </div>
+
+      <!-- 筛选条件 -->
+      <div class="mb-4 flex gap-4">
+        <el-select 
+          v-model="queryParams.accountId" 
+          placeholder="按账户ID筛选" 
+          clearable
+          style="width: 200px"
+          @change="fetchAdPlans"
+        >
+          <el-option
+            v-for="accountId in accountIds"
+            :key="accountId"
+            :label="`账户 ${accountId}`"
+            :value="accountId"
+          />
+        </el-select>
+        <el-select 
+          v-model="queryParams.status" 
+          placeholder="按状态筛选" 
+          clearable
+          style="width: 200px"
+          @change="fetchAdPlans"
+        >
+          <el-option label="待执行" :value="1" />
+          <el-option label="执行中" :value="2" />
+          <el-option label="失败" :value="3" />
+          <el-option label="已结束" :value="4" />
+        </el-select>
+      </div>
+
+      <!-- 广告计划表格 -->
+      <el-table
+        :data="adPlanList"
+        style="width: 100%"
+        v-loading="planLoading"
+        stripe
+        border
+        row-key="id"
+        :expand-row-keys="expandedRows"
+        @expand-change="handleExpandChange"
+      >
+        <el-table-column type="expand">
+          <template #default="{ row }">
+            <div class="p-4 bg-gray-50">
+              <div class="mb-3 font-semibold text-gray-700">创意列表</div>
+              <el-table
+                :data="row.creatives"
+                v-loading="row.creativesLoading"
+                border
+                size="small"
+              >
+                <el-table-column prop="id" label="创意ID" width="100" />
+                <el-table-column prop="creativeName" label="创意名称" min-width="200" show-overflow-tooltip />
+                <el-table-column prop="creativeId" label="腾讯创意ID" width="150" />
+                <el-table-column prop="imageId" label="图片ID" width="120" />
+                <el-table-column prop="pageIds" label="落地页IDs" min-width="150" show-overflow-tooltip />
+                <el-table-column label="状态" width="100">
+                  <template #default="{ row: creative }">
+                    <el-tag :type="getPlanStatusColor(creative.status)" size="small">
+                      {{ getPlanStatusText(creative.status) }}
+                    </el-tag>
+                  </template>
+                </el-table-column>
+                <el-table-column label="失败原因" min-width="200" show-overflow-tooltip>
+                  <template #default="{ row: creative }">
+                    <span class="text-red-500">{{ creative.failReason || '-' }}</span>
+                  </template>
+                </el-table-column>
+                <el-table-column label="创建时间" width="180">
+                  <template #default="{ row: creative }">
+                    {{ formatDate(creative.createTime) }}
+                  </template>
+                </el-table-column>
+              </el-table>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="id" label="计划ID" width="100" />
+        <el-table-column prop="adgroupName" label="广告组名称" min-width="200" show-overflow-tooltip />
+        <el-table-column prop="accountId" label="账户ID" width="120" />
+        <el-table-column prop="adgroupId" label="广告组ID" width="120" />
+        <el-table-column label="广告类型" width="100">
+          <template #default="{ row }">
+            <el-tag :type="getAdTypeColor(row.adType)" size="small">
+              {{ getAdTypeText(row.adType) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="出价金额" width="120">
+          <template #default="{ row }">
+            ¥{{ (row.bidAmount / 100).toFixed(2) }}
+          </template>
+        </el-table-column>
+        <el-table-column prop="target" label="定向" width="100" show-overflow-tooltip />
+        <el-table-column prop="site" label="版位" width="100" show-overflow-tooltip />
+        <el-table-column label="状态" width="100">
+          <template #default="{ row }">
+            <el-tag :type="getPlanStatusColor(row.status)" size="small">
+              {{ getPlanStatusText(row.status) }}
+            </el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="失败原因" min-width="200" show-overflow-tooltip>
+          <template #default="{ row }">
+            <span class="text-red-500">{{ row.failReason || '-' }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" width="180">
+          <template #default="{ row }">
+            {{ formatDate(row.createTime) }}
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <div class="mt-6 flex justify-end">
+        <el-pagination
+          v-if="total > 0"
+          v-model:current-page="queryParams.pageNum"
+          v-model:page-size="queryParams.pageSize"
+          :page-sizes="[10, 20, 50]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @current-change="fetchAdPlans"
+          @size-change="fetchAdPlans"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, onMounted, computed } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { 
+  getAdPlanConfDetail, 
+  getAdPlanList, 
+  reGeneratePlan,
+  getAdPlanCreativesByPlanId 
+} from '@/api/adPlanConf'
+
+const route = useRoute()
+const router = useRouter()
+
+// 配置详情
+const confDetail = ref(null)
+const confLoading = ref(false)
+const activeCollapse = ref([]) // 折叠面板激活项
+
+// 广告计划列表
+const adPlanList = ref([])
+const planLoading = ref(false)
+const total = ref(0)
+const expandedRows = ref([])
+
+// 查询参数
+const queryParams = ref({
+  confId: route.query.id,
+  accountId: null,
+  status: null,
+  pageNum: 1,
+  pageSize: 10
+})
+const handleReGeneratePlan = async (confId) => {
+  try {
+    const res = await reGeneratePlan(confId)
+    if (res.success) {
+      ElMessage.success('重新生成计划执行中')
+    } else {
+      ElMessage.error(res.msg || '重新生成计划失败')
+    }
+  } catch (error) {
+    console.error('重新生成计划失败:', error)
+    ElMessage.error('重新生成计划失败')
+  }
+}
+
+// 获取唯一的账户ID列表
+const accountIds = computed(() => {
+  const ids = new Set()
+  adPlanList.value.forEach(plan => {
+    if (plan.accountId) {
+      ids.add(plan.accountId)
+    }
+  })
+  return Array.from(ids).sort((a, b) => a - b)
+})
+
+// 获取配置详情
+const fetchConfDetail = async () => {
+  confLoading.value = true
+  try {
+    const res = await getAdPlanConfDetail(queryParams.value.confId)
+    if (res.success) {
+      confDetail.value = res.data
+    } else {
+      ElMessage.error(res.msg || '获取配置详情失败')
+    }
+  } catch (error) {
+    console.error('获取配置详情失败:', error)
+    ElMessage.error('获取配置详情失败')
+  } finally {
+    confLoading.value = false
+  }
+}
+
+// 获取广告计划列表
+const fetchAdPlans = async () => {
+  planLoading.value = true
+  try {
+    const res = await getAdPlanList(queryParams.value)
+    if (res.success) {
+      adPlanList.value = res.data.records || []
+      total.value = res.data.total || 0
+      
+      // 初始化创意列表
+      adPlanList.value.forEach(plan => {
+        plan.creatives = []
+        plan.creativesLoading = false
+      })
+    } else {
+      ElMessage.error(res.msg || '获取广告计划列表失败')
+    }
+  } catch (error) {
+    console.error('获取广告计划列表失败:', error)
+    ElMessage.error('获取广告计划列表失败')
+  } finally {
+    planLoading.value = false
+  }
+}
+
+// 处理展开行变化
+const handleExpandChange = async (row, expandedRowsArray) => {
+  expandedRows.value = expandedRowsArray.map(r => r.id)
+  
+  // 如果展开且还没加载创意
+  if (expandedRowsArray.some(r => r.id === row.id) && row.creatives.length === 0) {
+    await fetchCreatives(row)
+  }
+}
+
+// 获取创意列表
+const fetchCreatives = async (plan) => {
+  plan.creativesLoading = true
+  try {
+    const res = await getAdPlanCreativesByPlanId(plan.id)
+    if (res.success) {
+      plan.creatives = res.data || []
+    } else {
+      ElMessage.error(res.msg || '获取创意列表失败')
+    }
+  } catch (error) {
+    console.error('获取创意列表失败:', error)
+    ElMessage.error('获取创意列表失败')
+  } finally {
+    plan.creativesLoading = false
+  }
+}
+
+// 返回
+const goBack = () => {
+  router.back()
+}
+
+// 广告类型文本
+const getAdTypeText = (type) => {
+  const map = {
+    1: '企微',
+    2: 'ROI'
+  }
+  return map[type] || '未知'
+}
+
+// 广告类型颜色
+const getAdTypeColor = (type) => {
+  const map = {
+    1: 'success',
+    2: 'primary'
+  }
+  return map[type] || ''
+}
+
+// 计划状态文本
+const getPlanStatusText = (status) => {
+  const map = {
+    1: '待执行',
+    2: '执行中',
+    3: '失败',
+    4: '已结束'
+  }
+  return map[status] || '未知'
+}
+
+// 计划状态颜色
+const getPlanStatusColor = (status) => {
+  const map = {
+    1: 'info',
+    2: 'warning',
+    3: 'danger',
+    4: 'success'
+  }
+  return map[status] || ''
+}
+
+// 格式化日期
+const formatDate = (dateStr) => {
+  if (!dateStr) return '-'
+  const date = new Date(dateStr)
+  return date.toLocaleString('zh-CN', {
+    year: 'numeric',
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit',
+    second: '2-digit',
+    hour12: false
+  })
+}
+
+// 初始化
+onMounted(() => {
+  if (!queryParams.value.confId) {
+    ElMessage.error('缺少配置ID参数')
+    goBack()
+    return
+  }
+  
+  fetchConfDetail()
+  fetchAdPlans()
+})
+</script>
+
+<style scoped>
+/* 自定义样式 */
+</style>

+ 75 - 8
portal/src/views/ad-plan-conf/index.vue

@@ -8,10 +8,16 @@
             <el-icon><Setting /></el-icon>
             广告计划配置管理
           </h2>
+          <div>
+            <el-button type="primary" @click="loadConfList">
+            <el-icon><Refresh /></el-icon>
+            刷新
+          </el-button>
           <el-button type="primary" @click="handleCreate">
             <el-icon><Plus /></el-icon>
             创建配置
           </el-button>
+          </div>
         </div>
       </div>
 
@@ -25,6 +31,13 @@
       >
         <el-table-column prop="id" label="ID" width="80" />
         <el-table-column prop="name" label="计划名称" width="200" show-overflow-tooltip />
+        <el-table-column prop="status" label="状态" width="100" >
+          <template #default="{ row }">
+            <el-tag :type="getPlanStatusColor(row.status)">
+              {{ getPlanStatusText(row.status) }}
+            </el-tag>
+          </template>
+          </el-table-column>   
         <el-table-column label="广告类型" width="150">
           <template #default="{ row }">
             <el-tag :type="getAdTypeColor(row.adType)">
@@ -56,6 +69,10 @@
               <el-icon><Delete /></el-icon>
               删除
             </el-button>
+            <el-button size="small" :disabled="row.status  === 1" link @click="goToDetail(row)">
+              <el-icon><View /></el-icon>
+              详情
+            </el-button>
           </template>
         </el-table-column>
       </el-table>
@@ -142,7 +159,7 @@
             />
           </el-select>
         </el-form-item>
-         <el-form-item label="定向IDs" prop="targets">
+         <el-form-item label="定向模版IDs" prop="targets">
           <el-input
             v-model="formData.targets"
             type="textarea"
@@ -150,7 +167,7 @@
             placeholder="多个定向ID用逗号分隔,例如:1,2,3"
           />
         </el-form-item>
-         <el-form-item label="版位IDs" prop="sites">
+         <el-form-item label="版位模版IDs" prop="sites">
           <el-input
             v-model="formData.sites"
             type="textarea"
@@ -193,7 +210,7 @@
           />
         </el-form-item>
       </el-form>
-      <el-button @click="handleReCreate">预览</el-button>
+      <el-button type="primary" @click="handleReCreate">预览</el-button>
       <el-table :data="plans" 
       :preserve-expanded-content="true"
       >
@@ -203,6 +220,7 @@
             <h3>创意</h3>
             <el-table :data="props.row.adCreatives" border>
                 <el-table-column label="图片" prop="imageId" />
+                <el-table-column label="落地页ID" prop="pageIds" />
             </el-table>
             </div>
         </template>
@@ -226,14 +244,20 @@
         <el-table-column prop="conversionId" label="转化ID" width="120" />
         <el-table-column prop="wxCorpid" label="企微主体ID" width="150" show-overflow-tooltip />
         <el-table-column prop="dramaId" label="短剧ID" width="100" />
-    
-        <el-table-column prop="imageId" label="图片IDs" width="150" show-overflow-tooltip />
-        <el-table-column prop="pageId" label="落地页IDs" width="150" show-overflow-tooltip />
       </el-table>
       <template #footer>
+        <!--生成计划按钮tip提示先进行预览操作-->
+        <el-tooltip
+        class="box-item"
+        effect="dark"
+        content="请先进行预览操作"
+        placement="top-start"
+      >
+        <el-button :disabled="plans.length === 0" @click="handleGeneratePlan" >生成计划</el-button>
+      </el-tooltip>
         <el-button @click="dialogVisible = false">取消</el-button>
         <el-button type="primary" @click="handleSubmit" :loading="submitting">
-          确定
+          保存配置
         </el-button>
       </template>
     </el-dialog>
@@ -248,7 +272,8 @@ import {
   createAdPlanConf,
   updateAdPlanConf,
   deleteAdPlanConf,
-  reCreateAdPlanConf
+  reCreateAdPlanConf,
+  generatePlan
 } from '@/api/adPlanConf'
 import { getConversions } from '@/api/conversion'
 import { getAccountGroupListAll } from '@/api/accountGroup'
@@ -259,6 +284,8 @@ import {
   Edit,
   Delete
 } from '@element-plus/icons-vue'
+import { useRouter } from 'vue-router'
+const router = useRouter()
 
 // 数据
 const loading = ref(false)
@@ -359,6 +386,27 @@ const getAdTypeColor = (type) => {
   return colorMap[type] || 'info'
 }
 
+//计划状态:1-待执行,2-执行中,3-失败,4-已结束
+const getPlanStatusText = (status) => {
+  const statusMap = {
+    1: '待执行',
+    2: '执行中',
+    3: '失败',
+    4: '已结束'       
+  }
+  return statusMap[status] || '未知'
+}
+
+const getPlanStatusColor = (status) => {
+  const colorMap = {
+    1: 'info',
+    2: 'success',
+    3: 'danger',
+    4: 'success'
+  }
+  return colorMap[status] || 'info'
+}
+
 const formatDate = (dateStr) => {
   if (!dateStr) return '-'
   return new Date(dateStr).toLocaleString('zh-CN')
@@ -390,6 +438,7 @@ const handleCreate = () => {
     pageIds: '',
     jobNumber: ''
   }
+  plans.value = []
   dialogVisible.value = true
 }
 
@@ -410,9 +459,15 @@ const handleEdit = (row) => {
     pageIds: row.pageIds || '',
     jobNumber: row.jobNumber || ''
   }
+  plans.value = []
   dialogVisible.value = true
 }
 
+const goToDetail = (row) => {
+  // 跳转到详情页面
+  router.push({ path: '/ad-plan-conf/detail', query: { id: row.id } })
+}
+
 const handleDelete = async (row) => {
   try {
     await ElMessageBox.confirm(
@@ -451,6 +506,18 @@ const handleReCreate = async () => {
   }
 }
 
+const handleGeneratePlan = async () => {
+    const data = { ...formData.value }
+    data.id = editingId.value
+    const res = await generatePlan(data)
+  if (res.success) {
+    ElMessage.success('生成中')
+    console.log(res.data)
+  } else {
+    ElMessage.error(res.message || '生成失败')
+  }
+}
+
 const handleSubmit = async () => {
   try {
     await formRef.value.validate()

+ 90 - 87
portal/src/views/dashboard/index.vue

@@ -11,107 +11,44 @@
       </div>
     </div>
     
-    <!-- 统计卡片 -->
-    <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
-      <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow">
-        <div class="flex items-center">
-          <div class="p-3 rounded-lg bg-blue-100 text-blue-600 mr-4">
-            <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
-            </svg>
-          </div>
-          <div>
-            <p class="text-sm text-gray-500">视频素材</p>
-            <p class="text-2xl font-bold text-gray-900">{{ statistics.videoCount }}</p>
-            <p v-if="statistics.todayVideoCount > 0" class="text-xs text-green-600 mt-1">今日新增: {{ statistics.todayVideoCount }}</p>
-          </div>
-        </div>
-      </div>
-      
-      <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow">
-        <div class="flex items-center">
-          <div class="p-3 rounded-lg bg-green-100 text-green-600 mr-4">
-            <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
-            </svg>
-          </div>
-          <div>
-            <p class="text-sm text-gray-500">图片素材</p>
-            <p class="text-2xl font-bold text-gray-900">{{ statistics.imageCount }}</p>
-            <p v-if="statistics.todayImageCount > 0" class="text-xs text-green-600 mt-1">今日新增: {{ statistics.todayImageCount }}</p>
-          </div>
-        </div>
-      </div>
-      
-      <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow">
-        <div class="flex items-center">
-          <div class="p-3 rounded-lg bg-purple-100 text-purple-600 mr-4">
-            <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4v16M17 4v16M3 8h4m10 0h4M3 12h18M3 16h4m10 0h4M4 20h16a1 1 0 001-1V5a1 1 0 00-1-1H4a1 1 0 00-1 1v14a1 1 0 001 1z" />
-            </svg>
-          </div>
-          <div>
-            <p class="text-sm text-gray-500">广告组</p>
-            <p class="text-2xl font-bold text-gray-900">{{ statistics.adgroupCount }}</p>
-            <p v-if="statistics.todayAdgroupCount > 0" class="text-xs text-green-600 mt-1">今日新增: {{ statistics.todayAdgroupCount }}</p>
-          </div>
-        </div>
-      </div>
-      
-      <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6 hover:shadow-md transition-shadow">
-        <div class="flex items-center">
-          <div class="p-3 rounded-lg bg-yellow-100 text-yellow-600 mr-4">
-            <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
-            </svg>
-          </div>
-          <div>
-            <p class="text-sm text-gray-500">转化数据</p>
-            <p class="text-2xl font-bold text-gray-900">{{ statistics.conversionCount }}</p>
-            <p v-if="statistics.todayConversionCount > 0" class="text-xs text-green-600 mt-1">今日新增: {{ statistics.todayConversionCount }}</p>
-          </div>
-        </div>
-      </div>
-    </div>
-    
     <!-- 功能快捷入口 -->
     <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
       <h2 class="text-xl font-semibold text-gray-800 mb-6">快速开始</h2>
       <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
-        <router-link to="/video" class="flex flex-col items-center p-4 rounded-lg border border-gray-200 hover:border-blue-300 hover:bg-blue-50 transition-colors group">
+        <router-link to="/oauth" class="flex flex-col items-center p-4 rounded-lg border border-gray-200 hover:border-blue-300 hover:bg-blue-50 transition-colors group">
           <div class="p-3 rounded-lg bg-blue-100 text-blue-600 mb-3 group-hover:bg-blue-200 transition-colors">
             <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z" />
             </svg>
           </div>
-          <span class="text-gray-700 font-medium">视频管理</span>
+          <span class="text-gray-700 font-medium">OAuth</span>
         </router-link>
         
-        <router-link to="/image" class="flex flex-col items-center p-4 rounded-lg border border-gray-200 hover:border-green-300 hover:bg-green-50 transition-colors group">
+        <router-link to="/user" class="flex flex-col items-center p-4 rounded-lg border border-gray-200 hover:border-green-300 hover:bg-green-50 transition-colors group">
           <div class="p-3 rounded-lg bg-green-100 text-green-600 mb-3 group-hover:bg-green-200 transition-colors">
             <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 6H5a2 2 0 00-2 2v9a2 2 0 002 2h14a2 2 0 002-2V8a2 2 0 00-2-2h-5m-4 0V5a2 2 0 114 0v1m-4 0a2 2 0 104 0m-5 8a2 2 0 100-4 2 2 0 000 4zm0 0c1.306 0 2.417.835 2.83 2M9 14a3.001 3.001 0 00-2.83 2M15 11h3m-3 4h2" />
             </svg>
           </div>
-          <span class="text-gray-700 font-medium">图片管理</span>
+          <span class="text-gray-700 font-medium">实名认证</span>
         </router-link>
         
-        <router-link to="/adgroup" class="flex flex-col items-center p-4 rounded-lg border border-gray-200 hover:border-purple-300 hover:bg-purple-50 transition-colors group">
+        <router-link to="/account-group" class="flex flex-col items-center p-4 rounded-lg border border-gray-200 hover:border-purple-300 hover:bg-purple-50 transition-colors group">
           <div class="p-3 rounded-lg bg-purple-100 text-purple-600 mb-3 group-hover:bg-purple-200 transition-colors">
             <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4v16M17 4v16M3 8h4m10 0h4M3 12h18M3 16h4m10 0h4M4 20h16a1 1 0 001-1V5a1 1 0 00-1-1H4a1 1 0 00-1 1v14a1 1 0 001 1z" />
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" />
             </svg>
           </div>
-          <span class="text-gray-700 font-medium">广告组</span>
+          <span class="text-gray-700 font-medium">账号组</span>
         </router-link>
         
-        <router-link to="/conversion" class="flex flex-col items-center p-4 rounded-lg border border-gray-200 hover:border-yellow-300 hover:bg-yellow-50 transition-colors group">
+        <router-link to="/ad-plan-conf" class="flex flex-col items-center p-4 rounded-lg border border-gray-200 hover:border-yellow-300 hover:bg-yellow-50 transition-colors group">
           <div class="p-3 rounded-lg bg-yellow-100 text-yellow-600 mb-3 group-hover:bg-yellow-200 transition-colors">
             <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
             </svg>
           </div>
-          <span class="text-gray-700 font-medium">转化数据</span>
+          <span class="text-gray-700 font-medium">广告计划</span>
         </router-link>
       </div>
     </div>
@@ -176,7 +113,7 @@
     <!-- 最近活动 -->
     <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
       <div class="flex items-center justify-between mb-6">
-        <h2 class="text-xl font-semibold text-gray-800">最近活动</h2>
+        <h2 class="text-xl font-semibold text-gray-800">广告计划配置执行情况</h2>
         <button 
           @click="loadData" 
           :disabled="loading"
@@ -189,26 +126,85 @@
         <div 
           v-for="activity in recentActivities" 
           :key="activity.id"
-          class="flex items-start p-4 border border-gray-100 rounded-lg hover:bg-gray-50 transition-colors"
+          class="flex items-start p-4 border rounded-lg transition-colors cursor-pointer"
+          :class="{
+            'border-green-200 bg-green-50 hover:bg-green-100': activity.status === 4,
+            'border-red-200 bg-red-50 hover:bg-red-100': activity.status === 3,
+            'border-blue-200 bg-blue-50 hover:bg-blue-100': activity.status === 2,
+            'border-gray-200 bg-gray-50 hover:bg-gray-100': activity.status === 1 || !activity.status
+          }"
+          @click="viewPlanDetail(activity.confId)"
         >
-          <div class="p-2 rounded-lg bg-blue-100 text-blue-600 mr-4 flex-shrink-0">
-            <svg v-if="activity.type === 'video_upload'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
+          <!-- 状态图标 -->
+          <div class="p-2 rounded-lg mr-4 flex-shrink-0" :class="{
+            'bg-green-200 text-green-600': activity.status === 4,
+            'bg-red-200 text-red-600': activity.status === 3,
+            'bg-blue-200 text-blue-600': activity.status === 2,
+            'bg-gray-200 text-gray-600': activity.status === 1 || !activity.status
+          }">
+            <!-- 成功图标 -->
+            <svg v-if="activity.status === 4" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
             </svg>
-            <svg v-else-if="activity.type === 'adgroup_create'" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 4v16M17 4v16M3 8h4m10 0h4M3 12h18M3 16h4m10 0h4M4 20h16a1 1 0 001-1V5a1 1 0 00-1-1H4a1 1 0 00-1 1v14a1 1 0 001 1z" />
+            <!-- 失败图标 -->
+            <svg v-else-if="activity.status === 3" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
             </svg>
+            <!-- 执行中图标 -->
+            <svg v-else-if="activity.status === 2" xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
+            </svg>
+            <!-- 待执行图标 -->
             <svg v-else xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
-              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
             </svg>
           </div>
+          
+          <!-- 活动信息 -->
           <div class="flex-1 min-w-0">
-            <p class="text-gray-900 font-medium">{{ activity.description }}</p>
-            <p class="text-gray-500 text-sm mt-1">{{ activity.user }} • {{ activity.timestamp }}</p>
+            <div class="flex items-start justify-between">
+              <div class="flex-1">
+                <p class="font-medium" :class="{
+                  'text-green-900': activity.status === 4,
+                  'text-red-900': activity.status === 3,
+                  'text-blue-900': activity.status === 2,
+                  'text-gray-900': activity.status === 1 || !activity.status
+                }">{{ activity.description }}</p>
+                <div class="flex items-center gap-3 mt-2 text-sm" :class="{
+                  'text-green-600': activity.status === 4,
+                  'text-red-600': activity.status === 3,
+                  'text-blue-600': activity.status === 2,
+                  'text-gray-600': activity.status === 1 || !activity.status
+                }">
+                  <span class="flex items-center gap-1">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
+                    </svg>
+                    {{ activity.user }}
+                  </span>
+                  <span>•</span>
+                  <span class="flex items-center gap-1">
+                    <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                      <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z" />
+                    </svg>
+                    {{ activity.timestamp }}
+                  </span>
+                </div>
+              </div>
+              <!-- 状态标签 -->
+              <span class="ml-4 px-3 py-1 text-xs font-medium rounded-full flex-shrink-0" :class="{
+                'bg-green-100 text-green-700': activity.status === 4,
+                'bg-red-100 text-red-700': activity.status === 3,
+                'bg-blue-100 text-blue-700': activity.status === 2,
+                'bg-gray-100 text-gray-700': activity.status === 1 || !activity.status
+              }">
+                {{ activity.statusDesc }}
+              </span>
+            </div>
           </div>
         </div>
         <div v-if="recentActivities.length === 0" class="text-center py-8 text-gray-500">
-          暂无活动记录
+          暂无广告计划配置执行记录
         </div>
       </div>
     </div>
@@ -285,7 +281,7 @@ const loadSystemStatus = async () => {
 // 获取最近活动
 const loadRecentActivities = async () => {
   try {
-    const res = await getRecentActivities(5)
+    const res = await getRecentActivities(10)
     if (res.success) {
       recentActivities.value = res.data.activities || []
     }
@@ -295,6 +291,13 @@ const loadRecentActivities = async () => {
   }
 }
 
+// 查看计划详情
+const viewPlanDetail = (confId) => {
+  if (confId) {
+    window.open(`/ad-plan-conf/detail?id=${confId}`, '_blank')
+  }
+}
+
 // 初始化数据
 const loadData = async () => {
   loading.value = true

+ 1 - 1
portal/src/views/image/index.vue

@@ -387,7 +387,7 @@ const loadImageList = async () => {
     }
 
     const res = await getImageList(params)
-    if (res.code === 200) {
+    if (res.success) {
       imageList.value = res.data.records || []
       pagination.total = res.data.total || 0
     }

+ 2 - 0
src/main/java/com/moka/gdtauto/GdtAutoApplication.java

@@ -3,6 +3,7 @@ package com.moka.gdtauto;
 import org.mybatis.spring.annotation.MapperScan;
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.scheduling.annotation.EnableAsync;
 import org.springframework.scheduling.annotation.EnableScheduling;
 
 /**
@@ -13,6 +14,7 @@ import org.springframework.scheduling.annotation.EnableScheduling;
  */
 @SpringBootApplication
 @EnableScheduling
+@EnableAsync
 @MapperScan("com.moka.gdtauto.mapper")
 public class GdtAutoApplication {
 

+ 59 - 0
src/main/java/com/moka/gdtauto/common/AdPlanStatus.java

@@ -0,0 +1,59 @@
+package com.moka.gdtauto.common;
+
+/**
+ * 广告计划状态常量
+ * 适用于 AdPlanConf、AdPlan、AdPlanCreative 三张表
+ * 
+ * @author moka
+ * @since 2026-02-25
+ */
+public class AdPlanStatus {
+    
+    /**
+     * 待执行
+     */
+    public static final Integer PENDING = 1;
+    
+    /**
+     * 执行中
+     */
+    public static final Integer IN_PROGRESS = 2;
+    
+    /**
+     * 失败
+     */
+    public static final Integer FAILED = 3;
+    
+    /**
+     * 已结束(成功)
+     */
+    public static final Integer SUCCESS = 4;
+    
+    private AdPlanStatus() {
+        // 私有构造函数,防止实例化
+    }
+    
+    /**
+     * 获取状态描述
+     * 
+     * @param status 状态值
+     * @return 状态描述
+     */
+    public static String getStatusDesc(Integer status) {
+        if (status == null) {
+            return "未知";
+        }
+        switch (status) {
+            case 1:
+                return "待执行";
+            case 2:
+                return "执行中";
+            case 3:
+                return "失败";
+            case 4:
+                return "已结束(成功)";
+            default:
+                return "未知(" + status + ")";
+        }
+    }
+}

+ 90 - 1
src/main/java/com/moka/gdtauto/controller/AdPlanConfController.java

@@ -26,6 +26,9 @@ import java.util.List;
 
 import org.springframework.beans.BeanUtils;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+
 
 /**
  * 广告计划配置管理控制器
@@ -56,6 +59,7 @@ public class AdPlanConfController {
             String imageIds = request.getImageIds();
             String[] imageIdArray = imageIds.split(",");
             List<AdPlan>  plans = new ArrayList<>();
+             int i = 0;
             for (AccountGroupAccountRelationship relationship : relationships) {
                 Long accountId = relationship.getAccountId();
                 
@@ -64,7 +68,6 @@ public class AdPlanConfController {
 
                 String[] targetArray = targets.split(",");
                 String[] siteArray = sites.split(",");
-                int i = 0;
                 for (String target : targetArray) {
                     for (String site : siteArray) {
                         i++;
@@ -86,6 +89,7 @@ public class AdPlanConfController {
                         for (String imageId : imageIdArray) {
                             AdPlanCreative adPlanCreative = new AdPlanCreative();
                             adPlanCreative.setImageId(Long.valueOf(imageId));
+                            adPlanCreative.setPageIds(request.getPageIds());
                             adPlanCreatives.add(adPlanCreative);
                         }
                     }
@@ -99,6 +103,89 @@ public class AdPlanConfController {
         }
     }
 
+    @PostMapping("/generate-plan")
+    public Result<Void> generatePlan(
+            @RequestBody UpdateAdPlanConfRequest request,
+            @AuthUser SysUser sysUser) {
+        try {
+            Long accountGroupId = request.getAccountGroupId();
+            List<AccountGroupAccountRelationship> relationships = relationshipMapper.selectList(
+                    new LambdaQueryWrapper<AccountGroupAccountRelationship>()
+                            .eq(AccountGroupAccountRelationship::getGroupId, accountGroupId));
+                            Long confId = request.getId();
+            if (confId == null) {
+                AdPlanConf adPlanConf = new AdPlanConf();
+                BeanUtils.copyProperties(request, adPlanConf);
+                adPlanConf.setUserId(sysUser.getId());
+                adPlanConf.setJobNumber(sysUser.getJobNumber());
+                confId = adPlanConfService.createAdPlanConf(adPlanConf);
+            }
+
+            String confName = request.getName();
+            String imageIds = request.getImageIds();
+            String[] imageIdArray = imageIds.split(",");
+            List<AdPlan>  plans = new ArrayList<>();
+             int i = 0;
+            for (AccountGroupAccountRelationship relationship : relationships) {
+                Long accountId = relationship.getAccountId();
+                
+                String targets = request.getTargets();
+                String sites = request.getSites();
+
+                String[] targetArray = targets.split(",");
+                String[] siteArray = sites.split(",");
+                for (String target : targetArray) {
+                    for (String site : siteArray) {
+                        i++;
+                        // 创建广告计划配置
+                        AdPlan adPlan = new AdPlan();
+                        adPlan.setAdPlanConfId(confId);
+                        String planName = confName + "-" + i;
+                        adPlan.setAccountId(accountId);
+                        adPlan.setAdgroupName(planName);
+                        adPlan.setAdType(request.getAdType());
+                        adPlan.setTarget(target);
+                        adPlan.setSite(site);
+                        adPlan.setConversionId(request.getConversionId());
+                        adPlan.setDramaId(request.getDramaId());
+                        adPlan.setBidAmount(request.getBidAmount());
+                        adPlan.setWxCorpid(request.getWxCorpid());
+                        plans.add(adPlan);
+                        List<AdPlanCreative> adPlanCreatives = new ArrayList<>();
+                        adPlan.setAdCreatives(adPlanCreatives);
+                        for (String imageId : imageIdArray) {
+                            AdPlanCreative adPlanCreative = new AdPlanCreative();
+                            String creativeName = "测试创意-" + i;
+                            adPlanCreative.setCreativeName(creativeName);
+                            adPlanCreative.setAdPlanConfId(confId);
+                            adPlanCreative.setImageId(Long.valueOf(imageId));
+                            adPlanCreative.setPageIds(request.getPageIds());
+                            adPlanCreatives.add(adPlanCreative);
+                        }
+                    }
+                }
+            }
+            
+            adPlanConfService.generatePlan(plans);
+
+            //异步对接腾讯接口
+            adPlanConfService.asyncTencentInterface(plans);
+
+            return Result.success("创建广告计划成功;异步执行接口对接中");
+        } catch (Exception e) {
+            log.error("创建广告计划失败", e);
+            return Result.fail("创建广告计划失败: " + e.getMessage());
+        }
+    }
+
+
+    @GetMapping("/reGeneratePlan/{id}")
+    public Result<Void> reGeneratePlan(@PathVariable Long id) {
+        adPlanConfService.asyncReGeneratePlan(id);
+        return Result.success("重新生成广告计划执行中");
+    }
+    
+
     /**
      * 创建广告计划配置
      */
@@ -111,6 +198,7 @@ public class AdPlanConfController {
             AdPlanConf adPlanConf = new AdPlanConf();
             BeanUtils.copyProperties(request, adPlanConf);
             adPlanConf.setUserId(sysUser.getId());
+            adPlanConf.setJobNumber(sysUser.getJobNumber());
             
             Long confId = adPlanConfService.createAdPlanConf(adPlanConf);
             return Result.success(confId, "创建广告计划配置成功");
@@ -132,6 +220,7 @@ public class AdPlanConfController {
             AdPlanConf adPlanConf = new AdPlanConf();
             BeanUtils.copyProperties(request, adPlanConf);
             adPlanConf.setUserId(sysUser.getId());
+            adPlanConf.setJobNumber(sysUser.getJobNumber());
             
             boolean success = adPlanConfService.updateAdPlanConf(adPlanConf);
             if (success) {

+ 4 - 77
src/main/java/com/moka/gdtauto/controller/AdPlanController.java

@@ -26,67 +26,15 @@ public class AdPlanController {
 
     private final AdPlanService adPlanService;
 
-    /**
-     * 创建广告计划
-     */
-    @Operation(summary = "创建广告计划", description = "创建新的广告计划")
-    @PostMapping("/create")
-    public Result<Long> createAdPlan(@RequestBody AdPlan adPlan) {
-        try {
-            Long planId = adPlanService.createAdPlan(adPlan);
-            return Result.success(planId, "创建广告计划成功");
-        } catch (Exception e) {
-            log.error("创建广告计划失败", e);
-            return Result.fail("创建广告计划失败: " + e.getMessage());
-        }
-    }
-
-    /**
-     * 更新广告计划
-     */
-    @Operation(summary = "更新广告计划", description = "更新指定的广告计划")
-    @PostMapping("/update")
-    public Result<Void> updateAdPlan(@RequestBody AdPlan adPlan) {
-        try {
-            boolean success = adPlanService.updateAdPlan(adPlan);
-            if (success) {
-                return Result.success("更新广告计划成功");
-            } else {
-                return Result.fail("广告计划不存在");
-            }
-        } catch (Exception e) {
-            log.error("更新广告计划失败", e);
-            return Result.fail("更新广告计划失败: " + e.getMessage());
-        }
-    }
-
-    /**
-     * 删除广告计划
-     */
-    @Operation(summary = "删除广告计划", description = "删除指定的广告计划")
-    @DeleteMapping("/{planId}")
-    public Result<Void> deleteAdPlan(
-            @Parameter(description = "计划ID", required = true)
-            @PathVariable Long planId) {
-        try {
-            boolean success = adPlanService.deleteAdPlan(planId);
-            if (success) {
-                return Result.success("删除广告计划成功");
-            } else {
-                return Result.fail("广告计划不存在");
-            }
-        } catch (Exception e) {
-            log.error("删除广告计划失败", e);
-            return Result.fail("删除广告计划失败: " + e.getMessage());
-        }
-    }
-
+    
     /**
      * 获取广告计划列表(分页)
      */
     @Operation(summary = "获取广告计划列表", description = "分页获取广告计划列表,可按账户ID和状态筛选")
     @GetMapping("/list")
     public Result<Page<AdPlan>> getAdPlans(
+            @Parameter(description = "配置ID", example = "123456")
+            @RequestParam Long confId,
             @Parameter(description = "账户ID,可选", example = "123456")
             @RequestParam(required = false) Long accountId,
             @Parameter(description = "状态,可选:1-待执行,2-执行中,3-失败,4-已结束", example = "1")
@@ -96,32 +44,11 @@ public class AdPlanController {
             @Parameter(description = "每页大小", example = "10")
             @RequestParam(defaultValue = "10") Integer pageSize) {
         try {
-            Page<AdPlan> page = adPlanService.getAdPlans(accountId, status, pageNum, pageSize);
+            Page<AdPlan> page = adPlanService.getAdPlans(confId,accountId, status, pageNum, pageSize);
             return Result.success(page, "查询成功");
         } catch (Exception e) {
             log.error("查询广告计划列表失败", e);
             return Result.fail("查询广告计划列表失败: " + e.getMessage());
         }
     }
-
-    /**
-     * 获取广告计划详情
-     */
-    @Operation(summary = "获取广告计划详情", description = "根据ID获取广告计划详细信息")
-    @GetMapping("/{planId}")
-    public Result<AdPlan> getAdPlan(
-            @Parameter(description = "计划ID", required = true)
-            @PathVariable Long planId) {
-        try {
-            AdPlan adPlan = adPlanService.getAdPlanById(planId);
-            if (adPlan != null) {
-                return Result.success(adPlan, "查询成功");
-            } else {
-                return Result.fail("广告计划不存在");
-            }
-        } catch (Exception e) {
-            log.error("查询广告计划详情失败", e);
-            return Result.fail("查询广告计划详情失败: " + e.getMessage());
-        }
-    }
 }

+ 1 - 1
src/main/java/com/moka/gdtauto/controller/SysUserController.java

@@ -290,7 +290,7 @@ public class SysUserController {
             @RequestParam(value = "user_token", required = false) String userToken,
             @Parameter(description = "令牌失效时间戳(秒)", example = "1748707200")
             @RequestParam(value = "expire_time", required = false) Long expireTime,
-            @Parameter(description = "状态参数,用于传递手机号", required = true, example = "13800138000")
+            @Parameter(description = "状态参数,用于传递工号", required = true, example = "12458")
             @RequestParam String state) {
         try {
             // state参数就工号

+ 1 - 1
src/main/java/com/moka/gdtauto/controller/TencentImagesController.java

@@ -72,7 +72,7 @@ public class TencentImagesController {
             }
 
             // 上传图片
-            TencentImages result = imagesService.uploadImage(file, accountId, hasWatermark, dramaId, description, user.getJobNumber());
+            TencentImages result = imagesService.uploadImage(file, accountId, hasWatermark, dramaId, description);
             
             if ("SUCCESS".equals(result.getUploadStatus())) {
                 log.info("图片上传成功,ID: {}", result.getId());

+ 3 - 0
src/main/java/com/moka/gdtauto/controller/vo/UpdateAdPlanConfRequest.java

@@ -34,6 +34,9 @@ public class UpdateAdPlanConfRequest {
     @Schema(description = "企微主体ID", example = "wx123456")
     private String wxCorpid;
 
+    private String sites;
+    private String targets;
+
     @Schema(description = "短剧ID", example = "1")
     private Long dramaId;
 

+ 2 - 0
src/main/java/com/moka/gdtauto/entity/AdPlan.java

@@ -27,6 +27,8 @@ public class AdPlan {
     @TableId(type = IdType.AUTO)
     private Long id;
 
+    private Long adPlanConfId;
+
     /**
      * 账户ID
      */

+ 15 - 0
src/main/java/com/moka/gdtauto/entity/AdPlanConf.java

@@ -48,6 +48,15 @@ public class AdPlanConf {
      */
     private Long conversionId;
 
+     /**
+     * 站点IDs
+     */
+    private String sites;
+    /**
+     * 目标IDs
+     */
+    private String targets;
+
     /**
      * 企微主体ID
      */
@@ -58,6 +67,8 @@ public class AdPlanConf {
      */
     private Long dramaId;
 
+   
+
     /**
      * 图片IDs
      */
@@ -68,6 +79,10 @@ public class AdPlanConf {
      */
     private String pageIds;
 
+    private Integer status;
+    private String failReason;
+    private LocalDateTime successTime;
+
     /**
      * 用户ID
      */

+ 3 - 1
src/main/java/com/moka/gdtauto/entity/AdPlanCreative.java

@@ -23,6 +23,8 @@ public class AdPlanCreative {
     @TableId(type = IdType.AUTO)
     private Long id;
 
+    private Long adPlanConfId;
+
     /**
      * 计划ID
      */
@@ -56,7 +58,7 @@ public class AdPlanCreative {
     /**
      * 落地页ID
      */
-    private Long pageId;
+    private String pageIds;
 
     /**
      * 计划状态:1-待执行,2-执行中,3-失败,4-已结束

+ 9 - 0
src/main/java/com/moka/gdtauto/service/AdPlanConfService.java

@@ -1,7 +1,10 @@
 package com.moka.gdtauto.service;
 
+import java.util.List;
+
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.moka.gdtauto.entity.AdPlan;
 import com.moka.gdtauto.entity.AdPlanConf;
 
 /**
@@ -55,4 +58,10 @@ public interface AdPlanConfService extends IService<AdPlanConf> {
      * @return 配置信息
      */
     AdPlanConf getAdPlanConfById(Long confId, Long userId);
+
+    void generatePlan(List<AdPlan> plans);
+
+    void asyncTencentInterface(List<AdPlan> plans);
+
+    void asyncReGeneratePlan(Long id);
 }

+ 2 - 0
src/main/java/com/moka/gdtauto/service/AdPlanCreativeService.java

@@ -72,4 +72,6 @@ public interface AdPlanCreativeService extends IService<AdPlanCreative> {
      * @return 创意信息
      */
     AdPlanCreative getAdPlanCreativeById(Long creativeId);
+
+    List<AdPlanCreative> getCreativesByConfId(Long confId);
 }

+ 5 - 1
src/main/java/com/moka/gdtauto/service/AdPlanService.java

@@ -1,5 +1,7 @@
 package com.moka.gdtauto.service;
 
+import java.util.List;
+
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.moka.gdtauto.entity.AdPlan;
@@ -45,7 +47,7 @@ public interface AdPlanService extends IService<AdPlan> {
      * @param pageSize 每页大小
      * @return 计划分页列表
      */
-    Page<AdPlan> getAdPlans(Long accountId, Integer status, Integer pageNum, Integer pageSize);
+    Page<AdPlan> getAdPlans(Long confId, Long accountId, Integer status, Integer pageNum, Integer pageSize);
 
     /**
      * 获取广告计划详情
@@ -54,4 +56,6 @@ public interface AdPlanService extends IService<AdPlan> {
      * @return 计划信息
      */
     AdPlan getAdPlanById(Long planId);
+
+    List<AdPlan> getPlansByConfId(Long confId);
 }

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

@@ -23,5 +23,5 @@ public interface TencentImagesService extends IService<TencentImages> {
      * @return 图片记录
      */
     TencentImages uploadImage(MultipartFile file, Long accountId,
-                               Boolean hasWatermark, Long dramaId, String description, String jobNumber);
+                               Boolean hasWatermark, Long dramaId, String description);
 }

+ 0 - 15
src/main/java/com/moka/gdtauto/service/impl/AccountGroupServiceImpl.java

@@ -63,14 +63,6 @@ public class AccountGroupServiceImpl extends ServiceImpl<AccountGroupMapper, Acc
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createAccountGroup(Long userId, String groupName, Integer deliveryMode, String remark, List<Long> accountIds) {
-        // 校验账号ID
-        if (accountIds != null && !accountIds.isEmpty()) {
-            List<Long> invalidIds = validateAccountIds(accountIds);
-            if (!invalidIds.isEmpty()) {
-                throw new RuntimeException("以下账号ID不存在:" + invalidIds.toString());
-            }
-        }
-        
         AccountGroup accountGroup = new AccountGroup();
         accountGroup.setUserId(userId);
         accountGroup.setGroupName(groupName);
@@ -116,13 +108,6 @@ public class AccountGroupServiceImpl extends ServiceImpl<AccountGroupMapper, Acc
             return false;
         }
         
-        // 校验账号ID
-        if (accountIds != null && !accountIds.isEmpty()) {
-            List<Long> invalidIds = validateAccountIds(accountIds);
-            if (!invalidIds.isEmpty()) {
-                throw new RuntimeException("以下账号ID不存在:" + invalidIds.toString());
-            }
-        }
         
         existingGroup.setGroupName(groupName);
         existingGroup.setDeliveryMode(deliveryMode);

+ 454 - 0
src/main/java/com/moka/gdtauto/service/impl/AdPlanConfServiceImpl.java

@@ -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;
+        }
+    }
 }

+ 8 - 0
src/main/java/com/moka/gdtauto/service/impl/AdPlanCreativeServiceImpl.java

@@ -131,4 +131,12 @@ public class AdPlanCreativeServiceImpl extends ServiceImpl<AdPlanCreativeMapper,
                 .eq(AdPlanCreative::getId, creativeId)
                 .eq(AdPlanCreative::getDeleted, 0));
     }
+
+    @Override
+    public List<AdPlanCreative> getCreativesByConfId(Long confId) {
+        return this.list(new LambdaQueryWrapper<AdPlanCreative>()
+                .eq(AdPlanCreative::getAdPlanConfId, confId)
+                .eq(AdPlanCreative::getDeleted, 0)
+                .orderByAsc(AdPlanCreative::getId));
+    }
 }

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

@@ -11,6 +11,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.time.LocalDateTime;
+import java.util.List;
 
 /**
  * 广告计划服务实现类
@@ -74,11 +75,14 @@ public class AdPlanServiceImpl extends ServiceImpl<AdPlanMapper, AdPlan> impleme
     }
 
     @Override
-    public Page<AdPlan> getAdPlans(Long accountId, Integer status, Integer pageNum, Integer pageSize) {
+    public Page<AdPlan> getAdPlans(Long confId, Long accountId, Integer status, Integer pageNum, Integer pageSize) {
         Page<AdPlan> page = new Page<>(pageNum, pageSize);
         
         LambdaQueryWrapper<AdPlan> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.eq(AdPlan::getDeleted, 0);
+        if (confId != null) {
+            queryWrapper.eq(AdPlan::getAdPlanConfId, confId);
+        }
         
         if (accountId != null) {
             queryWrapper.eq(AdPlan::getAccountId, accountId);
@@ -98,4 +102,11 @@ public class AdPlanServiceImpl extends ServiceImpl<AdPlanMapper, AdPlan> impleme
                 .eq(AdPlan::getId, planId)
                 .eq(AdPlan::getDeleted, 0));
     }
+
+    @Override
+    public List<AdPlan> getPlansByConfId(Long confId) {
+        return this.list(new LambdaQueryWrapper<AdPlan>()
+                .eq(AdPlan::getAdPlanConfId, confId)
+                .eq(AdPlan::getDeleted, 0).orderByAsc(AdPlan::getId));
+    }
 }

+ 55 - 21
src/main/java/com/moka/gdtauto/service/impl/DashboardServiceImpl.java

@@ -1,5 +1,9 @@
 package com.moka.gdtauto.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.moka.gdtauto.common.AdPlanStatus;
+import com.moka.gdtauto.entity.AdPlanConf;
+import com.moka.gdtauto.mapper.AdPlanConfMapper;
 import com.moka.gdtauto.mapper.TencentAccountMapper;
 import com.moka.gdtauto.mapper.TencentAdsAdgroupMapper;
 import com.moka.gdtauto.mapper.TencentConversionMapper;
@@ -33,6 +37,7 @@ public class DashboardServiceImpl implements DashboardService {
     private final TencentAdsAdgroupMapper adgroupMapper;
     private final TencentConversionMapper conversionMapper;
     private final TencentAccountMapper accountMapper;
+    private final AdPlanConfMapper adPlanConfMapper;
 
     @Value("${spring.application.version:v1.0.0}")
     private String appVersion;
@@ -254,29 +259,58 @@ public class DashboardServiceImpl implements DashboardService {
         Map<String, Object> result = new HashMap<>();
         
         try {
-            // 获取最近的活动记录(这里可以整合各个模块的操作日志)
+            // 查询最近的广告计划配置执行情况
+            LambdaQueryWrapper<AdPlanConf> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.eq(AdPlanConf::getDeleted, 0)
+                    .orderByDesc(AdPlanConf::getUpdateTime)
+                    .last("LIMIT " + Math.min(limit, 100)); // 最多查询100条
+            
+            List<AdPlanConf> adPlanConfs = adPlanConfMapper.selectList(queryWrapper);
+            
+            // 转换为活动记录格式
             List<Map<String, Object>> activities = new java.util.ArrayList<>();
+            DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
+            
+            for (AdPlanConf conf : adPlanConfs) {
+                Map<String, Object> activity = new HashMap<>();
+                activity.put("id", conf.getId());
+                
+                // 根据状态设置活动类型和描述
+                Integer status = conf.getStatus();
+                String statusDesc = AdPlanStatus.getStatusDesc(status);
+                String activityType = "ad_plan_conf";
+                String description;
+                
+                if (AdPlanStatus.SUCCESS.equals(status)) {
+                    activityType = "ad_plan_success";
+                    description = String.format("广告计划配置《%s》执行成功", conf.getName());
+                } else if (AdPlanStatus.FAILED.equals(status)) {
+                    activityType = "ad_plan_failed";
+                    String failReason = conf.getFailReason() != null ? conf.getFailReason() : "未知原因";
+                    description = String.format("广告计划配置《%s》执行失败: %s", conf.getName(), failReason);
+                } else if (AdPlanStatus.IN_PROGRESS.equals(status)) {
+                    activityType = "ad_plan_in_progress";
+                    description = String.format("广告计划配置《%s》执行中", conf.getName());
+                } else {
+                    description = String.format("广告计划配置《%s》状态: %s", conf.getName(), statusDesc);
+                }
+                
+                activity.put("type", activityType);
+                activity.put("description", description);
+                activity.put("status", status);
+                activity.put("statusDesc", statusDesc);
+                activity.put("timestamp", conf.getUpdateTime() != null ? 
+                    conf.getUpdateTime().format(formatter) : 
+                    conf.getCreateTime().format(formatter));
+                activity.put("user", conf.getJobNumber() != null ? conf.getJobNumber() : "系统");
+                activity.put("confId", conf.getId());
+                activity.put("confName", conf.getName());
+                
+                activities.add(activity);
+            }
 
-            // 添加示例活动记录
-            Map<String, Object> activity1 = new HashMap<>();
-            activity1.put("id", 1);
-            activity1.put("type", "video_upload");
-            activity1.put("description", "上传了新的视频素材");
-            activity1.put("timestamp", LocalDateTime.now().minusHours(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
-            activity1.put("user", "admin");
-            activities.add(activity1);
-
-            Map<String, Object> activity2 = new HashMap<>();
-            activity2.put("id", 2);
-            activity2.put("type", "adgroup_create");
-            activity2.put("description", "创建了新的广告组");
-            activity2.put("timestamp", LocalDateTime.now().minusHours(3).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
-            activity2.put("user", "admin");
-            activities.add(activity2);
-
-            result.put("activities", activities.subList(0, Math.min(limit, activities.size())));
-
-            log.info("获取最近活动成功: 数量={}", activities.size());
+            result.put("activities", activities);
+            log.info("获取最近广告计划配置执行情况成功: 数量={}", activities.size());
 
         } catch (Exception e) {
             log.error("获取最近活动失败", e);

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

@@ -1,7 +1,9 @@
 package com.moka.gdtauto.service.impl;
 
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.moka.gdtauto.annotation.AuthUser;
 import com.moka.gdtauto.client.TencentAdsApiClientFactory;
+import com.moka.gdtauto.entity.SysUser;
 import com.moka.gdtauto.entity.TencentAssetDrama;
 import com.moka.gdtauto.entity.TencentImages;
 import com.moka.gdtauto.mapper.TencentImagesMapper;
@@ -44,7 +46,7 @@ public class TencentImagesServiceImpl extends ServiceImpl<TencentImagesMapper, T
     @Override
     @Transactional(rollbackFor = Exception.class)
     public TencentImages uploadImage(MultipartFile file, Long accountId, 
-                                      Boolean hasWatermark, Long dramaId, String description, String jobNumber) {
+                                      Boolean hasWatermark, Long dramaId, String description) {
         TencentImages imageRecord = new TencentImages();
         imageRecord.setAccountId(accountId);
         imageRecord.setOriginalFilename(file.getOriginalFilename());
@@ -93,7 +95,7 @@ public class TencentImagesServiceImpl extends ServiceImpl<TencentImagesMapper, T
             log.info("Base64编码完成,长度: {}", base64Image.length());
 
             // 5. 获取TencentAds客户端
-            TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken(jobNumber, accountId);
+            TencentAds tencentAds = clientFactory.getTencentAdsWithoutUserToken("12458", 33453856L);
             log.info("开始上传图片到腾讯广告,账户ID: {}", accountId);
 
             // 6. 调用腾讯广告API上传图片(使用UPLOAD_TYPE_BYTES方式)

+ 190 - 0
src/main/java/com/moka/gdtauto/util/AdgroupsAddReqPool.java

@@ -0,0 +1,190 @@
+package com.moka.gdtauto.util;
+
+import java.lang.annotation.Target;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.springframework.beans.BeanUtils;
+
+import com.tencent.ads.model.v3.AdgroupsAddRequest;
+
+
+import com.tencent.ads.model.v3.MarketingAssetOuterSpec;
+import com.tencent.ads.model.v3.MarketingCarrierType;
+import com.tencent.ads.model.v3.MarketingGoal;
+import com.tencent.ads.model.v3.MarketingTargetType;
+import com.tencent.ads.model.v3.BidMode;
+
+import com.tencent.ads.model.v3.BidScene;
+
+import com.tencent.ads.model.v3.WriteTargetingSetting;
+import com.tencent.ads.model.v3.OptimizationGoal;
+
+
+
+
+public class AdgroupsAddReqPool {
+    
+    private static final AdgroupsAddRequest leadRequest = buildLEADRequest();
+    private static final AdgroupsAddRequest roiRequest = buildROIRequest();
+
+
+    public static AdgroupsAddRequest getAdgroupsAddRequest(
+        Long accountId ,String groupName, String wxCorpId,
+         String beginDate, String endDate, 
+         Integer targetConfId, Integer siteConfId, 
+        Long bidAmount, Long conversionId) {
+        AdgroupsAddRequest request = new AdgroupsAddRequest();
+        BeanUtils.copyProperties(leadRequest, request);
+        request.setAccountId(accountId);
+        request.setAdgroupName(groupName);
+        request.getMarketingAssetOuterSpec().setMarketingAssetOuterId(wxCorpId);
+        request.setBeginDate(beginDate);
+        request.setEndDate(endDate);
+        request.setTargeting(TargetConfUtil.getTargetingSetting(targetConfId));
+        request.setSiteSet(SiteConfUtil.getSiteConf(siteConfId));
+        request.setBidAmount(bidAmount);
+        request.setConversionId(conversionId);
+        return request;
+    }
+
+    public static AdgroupsAddRequest getROIAdgroupsAddRequest(
+        Long accountId ,String groupName, Long marketingAssetId,
+         String beginDate, String endDate, 
+         Integer targetConfId, Integer siteConfId, 
+        Long bidAmount, Long conversionId) {
+        AdgroupsAddRequest request = new AdgroupsAddRequest();
+        BeanUtils.copyProperties(roiRequest, request);
+        request.setAccountId(accountId);
+        request.setAdgroupName(groupName);
+        request.setMarketingAssetId(marketingAssetId);
+        request.setBeginDate(beginDate);
+        request.setEndDate(endDate);
+        request.setTargeting(TargetConfUtil.getTargetingSetting(targetConfId));
+        request.setSiteSet(SiteConfUtil.getSiteConf(siteConfId));
+        request.setBidAmount(bidAmount);
+        request.setConversionId(conversionId);
+        return request;
+    }
+
+
+    private static  AdgroupsAddRequest buildLEADRequest() {
+        AdgroupsAddRequest request = new AdgroupsAddRequest();
+
+        // 基础字段
+        request.setAdgroupName("");
+        request.setAccountId(0L);
+        //营销目的类型 : 线索留资
+        request.setMarketingGoal(MarketingGoal.LEAD_RETENTION);
+        //营销载体类型 : 页面跳转
+        request.setMarketingCarrierType(MarketingCarrierType.JUMP_PAGE);
+        //营销资产外部规格 : 企业微信
+        MarketingAssetOuterSpec outerSpec = new MarketingAssetOuterSpec();
+        outerSpec.setMarketingTargetType(MarketingTargetType.WECHAT_WORK);
+        
+        // 产品外部 id 数据 : 企业微信ID 
+        outerSpec.setMarketingAssetOuterId("");
+        request.setMarketingAssetOuterSpec(outerSpec);
+        
+        // 投放时间
+        request.setBeginDate("2026-02-05");
+        request.setEndDate("2026-02-06");
+        
+        // 优化目标
+        request.setOptimizationGoal(OptimizationGoal.PAGE_SCAN_CODE);
+        
+        // 时间序列(全部1表示24小时投放,共7天*48个半小时=336位)
+        request.setTimeSeries("111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111");
+        
+        // 投放站点
+
+        request.setSiteSet(SiteConfUtil.getSiteConf(1));
+        
+        // 出价金额(单位:分)
+        request.setBidAmount(100L);
+        // 出价场景 常规投放-稳定投放
+        request.setBidScene(BidScene.NORMAL_AVERAGE);
+        
+        // 日预算(单位:分)设置为 0 表示不设预算
+        // TODO 测试期间设置上限
+        request.setDailyBudget(10000L);
+    
+        // 出价模式
+        request.setBidMode(BidMode.OCPM);
+
+        // 转化 id
+        request.setConversionId(67676141L);
+        
+        // 自动衍生创意
+        request.setAutoDerivedCreativeEnabled(false);
+        
+        // 出价场景
+        request.setBidScene(BidScene.UNKNOWN);
+        
+        // 定向设置
+        WriteTargetingSetting targeting = TargetConfUtil.getTargetingSetting(1);
+        request.setTargeting(targeting);
+
+        // 创意增强 MAX 开关
+        request.setAutoDerivedCreativeEnabled(true);
+
+        return request;
+    }
+
+    /**
+     * 构建ROI请求对象
+     */
+    private static  AdgroupsAddRequest buildROIRequest() {
+        AdgroupsAddRequest request = new AdgroupsAddRequest();
+
+        // 基础字段
+        request.setAdgroupName("");
+        request.setAccountId(0L);
+        request.setMarketingGoal(MarketingGoal.USER_GROWTH);
+        request.setMarketingCarrierType(MarketingCarrierType.JUMP_PAGE);
+        // 营销资产ID 比如短剧ID
+        request.setMarketingAssetId(0L);
+
+        
+        // 投放时间
+        request.setBeginDate("2026-02-06");
+        request.setEndDate("2027-02-06");
+        
+        // 优化目标
+        request.setOptimizationGoal(OptimizationGoal.APP_REGISTER);
+        
+        // 时间序列(全部1表示24小时投放,共7天*48个半小时=336位)
+        request.setTimeSeries("111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111111");
+        
+        // 投放站点
+      
+
+        request.setSiteSet(SiteConfUtil.getSiteConf(1));
+        
+        // 出价金额(单位:分)
+        request.setBidAmount(100L);
+        
+        // 日预算(单位:分)设置为 0 表示不设预算
+        // TODO 测试期间设置上限
+        request.setDailyBudget(10000L);
+        // 期望ROI
+        request.setDeepConversionWorthRate(0.2);
+    
+        // 出价模式
+        request.setBidMode(BidMode.OCPM);
+
+        // 转化 id
+        request.setConversionId(72627668L);
+        
+        // 出价场景
+        request.setBidScene(BidScene.UNKNOWN);
+        
+        // 定向设置
+        request.setTargeting(TargetConfUtil.getTargetingSetting(1));
+
+        // 创意增强 MAX 开关
+        request.setAutoDerivedCreativeEnabled(true);
+
+        return request;
+    }
+}

+ 22 - 0
src/main/java/com/moka/gdtauto/util/SiteConfUtil.java

@@ -0,0 +1,22 @@
+package com.moka.gdtauto.util;
+import java.util.Arrays;
+
+import java.util.List;
+
+import java.util.HashMap;
+import java.util.Map;
+
+
+public class SiteConfUtil {
+    private static final Map<Integer, List<String>> siteConfMap = new HashMap<>();
+
+    static {
+        siteConfMap.put(1, 
+            Arrays.asList( "SITE_SET_KANDIAN", "SITE_SET_QQ_MUSIC_GAME", "SITE_SET_TENCENT_NEWS",  "SITE_SET_TENCENT_VIDEO", "SITE_SET_SEARCH_SCENE", "SITE_SET_MOBILE_UNION","SITE_SET_WECHAT","SITE_SET_WECHAT_PLUGIN")
+        );
+    }
+
+    public static List<String> getSiteConf(Integer id) {
+        return siteConfMap.get(id);
+    }
+}

+ 95 - 0
src/main/java/com/moka/gdtauto/util/TargetConfUtil.java

@@ -0,0 +1,95 @@
+package com.moka.gdtauto.util;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.HashMap;
+import java.util.Map;
+
+
+import com.tencent.ads.model.v3.ExcludedDay;
+import com.tencent.ads.model.v3.WriteTargetingSetting;
+import com.tencent.ads.model.v3.GeoLocations;
+import com.tencent.ads.model.v3.AgeStruct;
+import com.tencent.ads.model.v3.ExcludedConvertedAudience;
+import com.tencent.ads.model.v3.ExcludedDimension;
+
+
+public class TargetConfUtil {
+
+    
+    private static final Map<Integer, WriteTargetingSetting> targetingSettingCache = new HashMap<>();
+
+    static {
+        targetingSettingCache.put(1, build1());
+    }
+
+    public static WriteTargetingSetting getTargetingSetting(Integer id) {
+        return targetingSettingCache.get(id);
+    }
+
+    /**
+     * 广告定向设置:
+    地域:中国(常住地)
+    年龄:大于等于42岁
+    性别:男性
+    排除已转化用户:同集团(自定义转化行为:指定页面曝光,加企业微信客服)(转化时间区间:1个月)
+    排除人群:44106830,44107982,43712084,44241932
+    不限:学历,婚恋育儿状态,用户消费能力,应用安装,设备品牌型号,操作系统版本,联网方式,移动运营商,定向人群,设备价格,微信再营销
+     */
+
+    private static WriteTargetingSetting build1() {
+        WriteTargetingSetting targeting = new WriteTargetingSetting();
+
+        // 地域定向
+        GeoLocations geoLocation = new GeoLocations();
+        List<String> locationTypes = new ArrayList<>();
+        locationTypes.add("LIVE_IN"); // 居住地
+        geoLocation.setLocationTypes(locationTypes);
+        List<Long> regions = new ArrayList<>();
+        regions.add(1156L); // 地域ID:1156(中国)
+        geoLocation.setRegions(regions);
+        targeting.setGeoLocation(geoLocation);
+
+        // 性别定向
+        List<String> gender = new ArrayList<>();
+        gender.add("MALE"); // 男性
+        targeting.setGender(gender);
+
+        // 年龄定向
+        List<AgeStruct> age = new ArrayList<>();
+        AgeStruct ageRange = new AgeStruct();
+        ageRange.setMin(42L);
+        ageRange.setMax(66L);
+        age.add(ageRange);
+        targeting.setAge(age);
+
+        // 指定页面曝光 =? 品牌转化
+        //排除已转化用户:同集团(自定义转化行为:指定页面曝光,加企业微信客服)(转化时间区间:1个月)
+        // 排除已转化人群行为定向,排除已转化用户定向范围
+        ExcludedConvertedAudience excludedConverted = new ExcludedConvertedAudience();
+        // 排除已转化人群行为定向 EXCLUDED_DIMENSION_GROUP 集团
+        excludedConverted.setExcludedDimension(ExcludedDimension.EXCLUDED_DIMENSION_GROUP);
+        
+        //转化行为
+        List<String> conversionBehaviorList = new ArrayList<>();
+        conversionBehaviorList.add("OPTIMIZATIONGOAL_BRAND_CONVERSION"); //品牌转化
+        conversionBehaviorList.add("OPTIMIZATIONGOAL_PAGE_SCAN_CODE"); //加企业微信客服
+        excludedConverted.setConversionBehaviorList(conversionBehaviorList);
+        
+        // 转化时间区间:1个月
+        //排除天数 EXCLUDED_DAY_ONE_MONTH 1 个月
+        excludedConverted.setExcludedDay(ExcludedDay.ONE_MONTH);
+        // 排除已转化人群行为定向,排除已转化用户定向范围
+        targeting.setExcludedConvertedAudience(excludedConverted);
+
+        // 排除自定义人群
+        List<Long> excludedCustomAudience = new ArrayList<>();
+        excludedCustomAudience.add(44106830L);
+        excludedCustomAudience.add(44107982L);
+        excludedCustomAudience.add(43712084L);
+        excludedCustomAudience.add(44241932L);
+        targeting.setExcludedCustomAudience(excludedCustomAudience);
+
+        return targeting;
+    }
+}

+ 231 - 0
src/test/java/com/moka/gdtauto/service/AdPlanConfAsyncTest.java

@@ -0,0 +1,231 @@
+package com.moka.gdtauto.service;
+
+import com.moka.gdtauto.GdtAutoApplication;
+import com.moka.gdtauto.entity.AdPlan;
+import com.moka.gdtauto.entity.AdPlanConf;
+import com.moka.gdtauto.entity.AdPlanCreative;
+import com.moka.gdtauto.common.AdPlanStatus;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 广告计划配置异步接口对接测试
+ * 测试 asyncTencentInterface 方法
+ * 
+ * 
+ * mvn test -Dtest=AdPlanConfAsyncTest#testAsyncTencentInterface -Dspring.profiles.active=test
+ * 
+ * @author moka
+ * @since 2026-02-25
+ */
+@SpringBootTest(classes = GdtAutoApplication.class)
+@ActiveProfiles("test")
+class AdPlanConfAsyncTest {
+
+    @Autowired
+    private AdPlanConfService adPlanConfService;
+
+    @Autowired
+    private AdPlanService adPlanService;
+
+    @Autowired
+    private AdPlanCreativeService adPlanCreativeService;
+
+    private static final Long TEST_CONF_ID = 4L;
+
+    @Test
+    @DisplayName("测试异步对接腾讯接口-从数据库获取confId=4的数据")
+    void testAsyncTencentInterface() throws InterruptedException {
+        System.out.println("\n========================================");
+        System.out.println("开始测试异步对接腾讯接口");
+        System.out.println("配置ID: " + TEST_CONF_ID);
+        System.out.println("========================================\n");
+
+        // 1. 查询广告计划配置
+        AdPlanConf conf = adPlanConfService.getById(TEST_CONF_ID);
+        if (conf == null) {
+            System.err.println("❌ 未找到配置,confId=" + TEST_CONF_ID);
+            return;
+        }
+        
+        System.out.println("📋 广告计划配置信息:");
+        System.out.println("  配置ID: " + conf.getId());
+        System.out.println("  配置名称: " + conf.getName());
+        System.out.println("  工号: " + conf.getJobNumber());
+        System.out.println("  账号组ID: " + conf.getAccountGroupId());
+        System.out.println("  广告类型: " + (conf.getAdType() == 1 ? "企微" : "ROI"));
+        System.out.println("  出价金额: " + conf.getBidAmount() + "分");
+        System.out.println("  转化ID: " + conf.getConversionId());
+        System.out.println();
+
+        // 2. 查询该配置下的所有 AdPlan
+        List<AdPlan> plans = adPlanService.getAdPlans(
+            TEST_CONF_ID, null, null, 1, 100
+        ).getRecords();
+        
+        if (plans == null || plans.isEmpty()) {
+            System.err.println("❌ 未找到广告计划,confId=" + TEST_CONF_ID);
+            return;
+        }
+
+        System.out.println("📊 找到 " + plans.size() + " 个广告计划:");
+        for (AdPlan plan : plans) {
+            System.out.println("  - 计划ID: " + plan.getId() + 
+                ", 广告名称: " + plan.getAdgroupName() + 
+                ", 账户ID: " + plan.getAccountId() +
+                ", 状态: " + plan.getStatus());
+            
+            // 查询该计划的创意
+            List<AdPlanCreative> creatives = adPlanCreativeService.getCreativesByPlanId(plan.getId());
+            if (creatives != null && !creatives.isEmpty()) {
+                System.out.println("    创意数量: " + creatives.size());
+                for (AdPlanCreative creative : creatives) {
+                    System.out.println("      * " + creative.getCreativeName() + 
+                        " (图片ID: " + creative.getImageId() + 
+                        ", 落地页: " + creative.getPageIds() + ")");
+                }
+                // 关联创意到计划对象
+                plan.setAdCreatives(creatives);
+            }
+        }
+        System.out.println();
+
+        // 3. 调用异步接口
+        System.out.println("🚀 开始调用异步对接腾讯接口...");
+        System.out.println("========================================\n");
+        
+        try {
+            adPlanConfService.asyncTencentInterface(plans);
+            
+            // 等待异步任务完成 (因为是异步方法,需要等待)
+            System.out.println("⏳ 等待异步任务执行... (30秒)");
+            TimeUnit.SECONDS.sleep(30);
+            
+            System.out.println("\n========================================");
+            System.out.println("📊 异步任务执行完成,查询结果:");
+            System.out.println("========================================\n");
+            
+            // 4. 查询更新后的状态
+            AdPlanConf updatedConf = adPlanConfService.getById(TEST_CONF_ID);
+            System.out.println("📋 配置状态:");
+            System.out.println("  状态: " + getStatusName(updatedConf.getStatus()));
+            System.out.println("  失败原因: " + updatedConf.getFailReason());
+            System.out.println("  成功时间: " + updatedConf.getSuccessTime());
+            System.out.println();
+            
+            System.out.println("📊 广告计划状态:");
+            for (AdPlan plan : plans) {
+                AdPlan updatedPlan = adPlanService.getById(plan.getId());
+                System.out.println("  - 计划ID: " + updatedPlan.getId());
+                System.out.println("    广告组ID: " + updatedPlan.getAdgroupId());
+                System.out.println("    状态: " + getStatusName(updatedPlan.getStatus()));
+                System.out.println("    失败原因: " + updatedPlan.getFailReason());
+                System.out.println("    成功时间: " + updatedPlan.getSuccessTime());
+                
+                // 查询创意状态
+                List<AdPlanCreative> updatedCreatives = adPlanCreativeService.getCreativesByPlanId(updatedPlan.getId());
+                if (updatedCreatives != null && !updatedCreatives.isEmpty()) {
+                    System.out.println("    创意状态:");
+                    for (AdPlanCreative creative : updatedCreatives) {
+                        System.out.println("      * " + creative.getCreativeName());
+                        System.out.println("        创意ID: " + creative.getCreativeId());
+                        System.out.println("        状态: " + getStatusName(creative.getStatus()));
+                        System.out.println("        失败原因: " + creative.getFailReason());
+                    }
+                }
+                System.out.println();
+            }
+            
+            System.out.println("✅ 测试完成");
+            
+        } catch (Exception e) {
+            System.err.println("\n❌ 测试失败");
+            System.err.println("错误信息: " + e.getMessage());
+            e.printStackTrace();
+            throw new RuntimeException("测试失败", e);
+        }
+    }
+
+    /**
+     * 获取状态名称
+     */
+    private String getStatusName(Integer status) {
+        return AdPlanStatus.getStatusDesc(status);
+    }
+
+    @Test
+    @DisplayName("测试仅查询配置和计划数据")
+    void testQueryOnly() {
+        System.out.println("\n========================================");
+        System.out.println("查询广告计划配置和计划数据");
+        System.out.println("配置ID: " + TEST_CONF_ID);
+        System.out.println("========================================\n");
+
+        // 查询配置
+        AdPlanConf conf = adPlanConfService.getById(TEST_CONF_ID);
+        if (conf == null) {
+            System.err.println("❌ 未找到配置");
+            return;
+        }
+
+        System.out.println("📋 配置信息:");
+        System.out.println("  ID: " + conf.getId());
+        System.out.println("  名称: " + conf.getName());
+        System.out.println("  工号: " + conf.getJobNumber());
+        System.out.println("  账号组ID: " + conf.getAccountGroupId());
+        System.out.println("  广告类型: " + conf.getAdType() + " (" + (conf.getAdType() == 1 ? "企微" : "ROI") + ")");
+        System.out.println("  出价金额: " + conf.getBidAmount() + "分");
+        System.out.println("  转化ID: " + conf.getConversionId());
+        System.out.println("  企微主体ID: " + conf.getWxCorpid());
+        System.out.println("  短剧ID: " + conf.getDramaId());
+        System.out.println("  图片IDs: " + conf.getImageIds());
+        System.out.println("  落地页IDs: " + conf.getPageIds());
+        System.out.println("  站点配置: " + conf.getSites());
+        System.out.println("  定向配置: " + conf.getTargets());
+        System.out.println("  状态: " + getStatusName(conf.getStatus()));
+        System.out.println();
+
+        // 查询计划
+        List<AdPlan> plans = adPlanService.getAdPlans(
+            TEST_CONF_ID, null, null, 1, 100
+        ).getRecords();
+
+        System.out.println("📊 找到 " + plans.size() + " 个广告计划:");
+        for (AdPlan plan : plans) {
+            System.out.println("\n  计划 #" + plan.getId() + ":");
+            System.out.println("    广告名称: " + plan.getAdgroupName());
+            System.out.println("    账户ID: " + plan.getAccountId());
+            System.out.println("    广告组ID: " + plan.getAdgroupId());
+            System.out.println("    广告类型: " + plan.getAdType() + " (" + (plan.getAdType() == 1 ? "企微" : "ROI") + ")");
+            System.out.println("    出价金额: " + plan.getBidAmount() + "分");
+            System.out.println("    转化ID: " + plan.getConversionId());
+            System.out.println("    企微主体ID: " + plan.getWxCorpid());
+            System.out.println("    短剧ID: " + plan.getDramaId());
+            System.out.println("    站点配置: " + plan.getSite());
+            System.out.println("    定向配置: " + plan.getTarget());
+            System.out.println("    状态: " + getStatusName(plan.getStatus()));
+            System.out.println("    失败原因: " + plan.getFailReason());
+
+            // 查询创意
+            List<AdPlanCreative> creatives = adPlanCreativeService.getCreativesByPlanId(plan.getId());
+            if (creatives != null && !creatives.isEmpty()) {
+                System.out.println("    创意列表 (" + creatives.size() + "个):");
+                for (AdPlanCreative creative : creatives) {
+                    System.out.println("      - " + creative.getCreativeName());
+                    System.out.println("        创意ID: " + creative.getCreativeId());
+                    System.out.println("        图片ID: " + creative.getImageId());
+                    System.out.println("        落地页IDs: " + creative.getPageIds());
+                    System.out.println("        状态: " + getStatusName(creative.getStatus()));
+                }
+            }
+        }
+
+        System.out.println("\n✅ 查询完成");
+    }
+}

+ 39 - 0
src/test/java/com/moka/gdtauto/service/SysUserTest.java

@@ -0,0 +1,39 @@
+package com.moka.gdtauto.service;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+import org.springframework.util.DigestUtils;
+
+import com.moka.gdtauto.GdtAutoApplication;
+import com.moka.gdtauto.entity.SysUser;
+
+@SpringBootTest(classes = GdtAutoApplication.class)
+@ActiveProfiles("test")
+public class SysUserTest {
+    
+    //默认密码
+    private static final String DEFAULT_PASSWORD = "123456";
+    //工号
+    private static final String JOB_NUMBER = "12329";
+    private static final String NAME = "陈龙飞";
+
+    @Autowired
+    private SysUserService sysUserService;
+    
+
+    @Test
+    public void createNewUser() {
+        String pwd = DEFAULT_PASSWORD + "#" + JOB_NUMBER;
+        System.out.println(pwd);
+        String password = DigestUtils.md5DigestAsHex(pwd.getBytes());
+        SysUser user = new SysUser();
+        user.setPhone("12345678901");
+        user.setJobNumber(JOB_NUMBER);
+        user.setPassword(password);
+        user.setStatus(1);
+        user.setDefaultPwd(1);
+        user.setName(NAME);
+        sysUserService.save(user);
+    }
+}

+ 0 - 1
src/test/java/com/moka/gdtauto/service/TencentAdsAdgroupCreateTest.java

@@ -105,7 +105,6 @@ class TencentAdsAdgroupCreateTest {
         // 产品外部 id 数据 : 企业微信ID 
         outerSpec.setMarketingAssetOuterId("wp7eXwDAAAC2oIBdHTa0-QV8KNGDd3mA");
         request.setMarketingAssetOuterSpec(outerSpec);
-    
 
         
         // 投放时间

+ 11 - 1
src/test/java/com/moka/gdtauto/util/SampleTest.java

@@ -1,13 +1,23 @@
 package com.moka.gdtauto.util;
 
+import org.junit.jupiter.api.Test;
 import org.springframework.util.DigestUtils;
+import com.alibaba.fastjson.JSON;
+
+import com.tencent.ads.model.v3.WriteTargetingSetting;
 
 public class SampleTest {
     public static void main(String[] args) {
-        String pwd = "123456" + "#" + "12458";
+        String pwd = "123456" + "#" + "11203";
         System.out.println(pwd);
         String password = DigestUtils.md5DigestAsHex(pwd.getBytes());
         System.out.println(password);
     }
+
+    @Test
+    public void Test() {
+        WriteTargetingSetting targetingSetting = TargetConfUtil.getTargetingSetting(1);
+        System.out.println(JSON.toJSONString(targetingSetting, true));
+    }
     
 }