Browse Source

feat:同步广告日报

pudongliang 4 months ago
parent
commit
322c164a74
32 changed files with 489 additions and 34 deletions
  1. 1 1
      portal/dist/assets/accountGroup-N5vZ5puV.js
  2. 1 1
      portal/dist/assets/adPlanConf-MEn3Hq5o.js
  3. 1 1
      portal/dist/assets/auth-BI_nGEEP.js
  4. 1 1
      portal/dist/assets/conversion-nBGFaTMc.js
  5. 0 0
      portal/dist/assets/detail-CBxFMQ1J.js
  6. 0 0
      portal/dist/assets/detail-DJd7pifX.js
  7. 1 1
      portal/dist/assets/drama-hiEb51_B.js
  8. 0 0
      portal/dist/assets/index-4gUvJm_w.js
  9. 0 0
      portal/dist/assets/index-B4wMycjF.js
  10. 0 0
      portal/dist/assets/index-BGn61ZZ8.css
  11. 0 0
      portal/dist/assets/index-BIMHmt4H.js
  12. 0 0
      portal/dist/assets/index-BIS1RTxk.js
  13. 1 1
      portal/dist/assets/index-BU9f2rGa.js
  14. 0 0
      portal/dist/assets/index-BxWvmrN-.js
  15. 0 0
      portal/dist/assets/index-C1N-HiIf.js
  16. 0 0
      portal/dist/assets/index-CWDslONk.js
  17. 0 0
      portal/dist/assets/index-DhuxdXdi.js
  18. 0 0
      portal/dist/assets/index-Dx1QTPE6.js
  19. 0 0
      portal/dist/assets/index-HylhreMf.js
  20. 0 0
      portal/dist/assets/index-zXCg8RiH.js
  21. 1 1
      portal/dist/assets/ossFile-CQMV4lyF.js
  22. 0 0
      portal/dist/assets/request-BQ8lBDVA.js
  23. 1 1
      portal/dist/assets/user-INf7jG3f.js
  24. 2 2
      portal/dist/index.html
  25. 1 1
      portal/src/layout/index.vue
  26. 40 23
      portal/src/views/ad-plan-conf/detail.vue
  27. 3 0
      src/main/java/com/moka/gdtauto/controller/AdPlanConfController.java
  28. 14 0
      src/main/java/com/moka/gdtauto/mapper/AdPlanMapper.java
  29. 304 0
      src/main/java/com/moka/gdtauto/service/AdPlanDailyReportService.java
  30. 54 0
      src/main/java/com/moka/gdtauto/task/AdPlanDailyReportSyncTask.java
  31. 27 0
      src/main/resources/mapper/AdPlanMapper.xml
  32. 36 0
      src/test/java/com/moka/gdtauto/service/AdPlanDailyReportSyncTest.java

+ 1 - 1
portal/dist/assets/accountGroup-D5BVVYC-.js → portal/dist/assets/accountGroup-N5vZ5puV.js

@@ -1 +1 @@
-import{r as u}from"./request-DLbisFwp.js";function c(t){return u({url:"/api/account-group/create",method:"post",data:t})}function e(t){return u({url:"/api/account-group/update",method:"post",data:t})}function n(t){return u({url:`/api/account-group/${t}`,method:"delete"})}function a(t=1,o=10){return u({url:"/api/account-group/list",method:"get",params:{pageNum:t,pageSize:o}})}function p(){return u({url:"/api/account-group/all",method:"get"})}function i(t){return u({url:`/api/account-group/${t}/accounts`,method:"get"})}export{i as a,p as b,c,n as d,a as g,e as u};
+import{r as u}from"./request-BQ8lBDVA.js";function c(t){return u({url:"/api/account-group/create",method:"post",data:t})}function e(t){return u({url:"/api/account-group/update",method:"post",data:t})}function n(t){return u({url:`/api/account-group/${t}`,method:"delete"})}function a(t=1,o=10){return u({url:"/api/account-group/list",method:"get",params:{pageNum:t,pageSize:o}})}function p(){return u({url:"/api/account-group/all",method:"get"})}function i(t){return u({url:`/api/account-group/${t}/accounts`,method:"get"})}export{i as a,p as b,c,n as d,a as g,e as u};

+ 1 - 1
portal/dist/assets/adPlanConf-B2AMh7LC.js → portal/dist/assets/adPlanConf-MEn3Hq5o.js

@@ -1 +1 @@
-import{r as a}from"./request-DLbisFwp.js";function r(n){return a({url:"/api/ad-plan-conf/reCreate",method:"post",data:n})}function d(n){return a({url:"/api/ad-plan-conf/generate-plan",method:"post",data:n})}function o(n){return a({url:`/api/ad-plan-conf/reGeneratePlan/${n}`,method:"get"})}function l(n){return a({url:`/api/ad-plan-conf/syncSystemStatus/${n}`,method:"get"})}function u(n){return a({url:"/api/ad-plan-conf/create",method:"post",data:n})}function p(n){return a({url:"/api/ad-plan-conf/update",method:"post",data:n})}function s(n=1,t=10){return a({url:"/api/ad-plan-conf/list",method:"get",params:{pageNum:n,pageSize:t}})}function f(n){return a({url:`/api/ad-plan-conf/${n}`,method:"get"})}function i(n){return a({url:"/api/ad-plan/list",method:"get",params:n})}function c(n){return a({url:`/api/ad-plan-creative/plan/${n}`,method:"get"})}function m(n){return a({url:"/api/ad-plan-conf/gdtSuspendByConfId",params:{confId:n},method:"get"})}function g(n){return a({url:"/api/ad-plan-conf/gdtDeleteByConfId",params:{confId:n},method:"get"})}function h(n,t){return a({url:`/api/ad-plan-conf/gdtSuspendByPlanIds/${n}`,method:"post",data:t})}function P(n,t){return a({url:`/api/ad-plan-conf/gdtDeleteByPlanIds/${n}`,method:"post",data:t})}export{d as a,f as b,u as c,i as d,g as e,h as f,s as g,P as h,l as i,o as j,c as k,r,m as s,p as u};
+import{r as a}from"./request-BQ8lBDVA.js";function r(n){return a({url:"/api/ad-plan-conf/reCreate",method:"post",data:n})}function d(n){return a({url:"/api/ad-plan-conf/generate-plan",method:"post",data:n})}function o(n){return a({url:`/api/ad-plan-conf/reGeneratePlan/${n}`,method:"get"})}function l(n){return a({url:`/api/ad-plan-conf/syncSystemStatus/${n}`,method:"get"})}function u(n){return a({url:"/api/ad-plan-conf/create",method:"post",data:n})}function p(n){return a({url:"/api/ad-plan-conf/update",method:"post",data:n})}function s(n=1,t=10){return a({url:"/api/ad-plan-conf/list",method:"get",params:{pageNum:n,pageSize:t}})}function f(n){return a({url:`/api/ad-plan-conf/${n}`,method:"get"})}function i(n){return a({url:"/api/ad-plan/list",method:"get",params:n})}function c(n){return a({url:`/api/ad-plan-creative/plan/${n}`,method:"get"})}function m(n){return a({url:"/api/ad-plan-conf/gdtSuspendByConfId",params:{confId:n},method:"get"})}function g(n){return a({url:"/api/ad-plan-conf/gdtDeleteByConfId",params:{confId:n},method:"get"})}function h(n,t){return a({url:`/api/ad-plan-conf/gdtSuspendByPlanIds/${n}`,method:"post",data:t})}function P(n,t){return a({url:`/api/ad-plan-conf/gdtDeleteByPlanIds/${n}`,method:"post",data:t})}export{d as a,f as b,u as c,i as d,g as e,h as f,s as g,P as h,l as i,o as j,c as k,r,m as s,p as u};

+ 1 - 1
portal/dist/assets/auth-BG9T5TeO.js → portal/dist/assets/auth-BI_nGEEP.js

@@ -1 +1 @@
-import{r as t}from"./request-DLbisFwp.js";function i(){return t({url:"/api/gdt/auth/authorize-url",method:"get"})}function n(r){return t({url:"/api/gdt/auth/refresh",method:"post",params:r})}function u(){return t({url:"/api/gdt/auth/list",method:"get"})}function a(){return u()}function o(){return t({url:"/api/gdt/auth/expiring",method:"get"})}export{o as a,a as b,i as g,u as l,n as r};
+import{r as t}from"./request-BQ8lBDVA.js";function i(){return t({url:"/api/gdt/auth/authorize-url",method:"get"})}function n(r){return t({url:"/api/gdt/auth/refresh",method:"post",params:r})}function u(){return t({url:"/api/gdt/auth/list",method:"get"})}function a(){return u()}function o(){return t({url:"/api/gdt/auth/expiring",method:"get"})}export{o as a,a as b,i as g,u as l,n as r};

+ 1 - 1
portal/dist/assets/conversion-I_E-Xf3z.js → portal/dist/assets/conversion-nBGFaTMc.js

@@ -1 +1 @@
-import{r as t}from"./request-DLbisFwp.js";function e(n,o){return t({url:"/api/gdt/conversion/sync",method:"post",params:{orgAccountId:n,accountId:o}})}function s(n=1,o=20){return t({url:"/api/gdt/conversion/list",method:"get",params:{page:n,pageSize:o}})}function i(n){return t({url:"/api/gdt/conversion/detail",method:"get",params:{conversionId:n}})}function a(){return t({url:"/api/gdt/conversion/all",method:"get"})}export{s as a,i as b,a as g,e as s};
+import{r as t}from"./request-BQ8lBDVA.js";function e(n,o){return t({url:"/api/gdt/conversion/sync",method:"post",params:{orgAccountId:n,accountId:o}})}function s(n=1,o=20){return t({url:"/api/gdt/conversion/list",method:"get",params:{page:n,pageSize:o}})}function i(n){return t({url:"/api/gdt/conversion/detail",method:"get",params:{conversionId:n}})}function a(){return t({url:"/api/gdt/conversion/all",method:"get"})}export{s as a,i as b,a as g,e as s};

File diff suppressed because it is too large
+ 0 - 0
portal/dist/assets/detail-CBxFMQ1J.js


File diff suppressed because it is too large
+ 0 - 0
portal/dist/assets/detail-DJd7pifX.js


+ 1 - 1
portal/dist/assets/drama-ip2vqI6Z.js → portal/dist/assets/drama-hiEb51_B.js

@@ -1 +1 @@
-import{r as t}from"./request-DLbisFwp.js";function n(a){return t({url:"/api/gdt/asset/drama/sync",method:"post",params:{accountId:a}})}function m(a=1,s=20,r=""){return t({url:"/api/gdt/asset/drama/list",method:"get",params:{dramaName:r,page:a,pageSize:s}})}function d(a){return t({url:"/api/gdt/asset/drama/myDramaAssets",method:"get",params:{dramaName:a}})}function o(){return t({url:"/api/gdt/asset/drama/sync/conf/myConf",method:"get"})}function u(a){return t({url:"/api/gdt/asset/drama/sync/conf/add",method:"post",data:a})}function i(a){return t({url:"/api/gdt/asset/drama/sync/conf/delete",method:"post",params:{id:a}})}export{o as a,u as b,i as d,m as g,d as m,n as s};
+import{r as t}from"./request-BQ8lBDVA.js";function n(a){return t({url:"/api/gdt/asset/drama/sync",method:"post",params:{accountId:a}})}function m(a=1,s=20,r=""){return t({url:"/api/gdt/asset/drama/list",method:"get",params:{dramaName:r,page:a,pageSize:s}})}function d(a){return t({url:"/api/gdt/asset/drama/myDramaAssets",method:"get",params:{dramaName:a}})}function o(){return t({url:"/api/gdt/asset/drama/sync/conf/myConf",method:"get"})}function u(a){return t({url:"/api/gdt/asset/drama/sync/conf/add",method:"post",data:a})}function i(a){return t({url:"/api/gdt/asset/drama/sync/conf/delete",method:"post",params:{id:a}})}export{o as a,u as b,i as d,m as g,d as m,n as s};

File diff suppressed because it is too large
+ 0 - 0
portal/dist/assets/index-4gUvJm_w.js


File diff suppressed because it is too large
+ 0 - 0
portal/dist/assets/index-B4wMycjF.js


File diff suppressed because it is too large
+ 0 - 0
portal/dist/assets/index-BGn61ZZ8.css


File diff suppressed because it is too large
+ 0 - 0
portal/dist/assets/index-BIMHmt4H.js


File diff suppressed because it is too large
+ 0 - 0
portal/dist/assets/index-BIS1RTxk.js


File diff suppressed because it is too large
+ 1 - 1
portal/dist/assets/index-BU9f2rGa.js


File diff suppressed because it is too large
+ 0 - 0
portal/dist/assets/index-BxWvmrN-.js


File diff suppressed because it is too large
+ 0 - 0
portal/dist/assets/index-C1N-HiIf.js


File diff suppressed because it is too large
+ 0 - 0
portal/dist/assets/index-CWDslONk.js


File diff suppressed because it is too large
+ 0 - 0
portal/dist/assets/index-DhuxdXdi.js


File diff suppressed because it is too large
+ 0 - 0
portal/dist/assets/index-Dx1QTPE6.js


File diff suppressed because it is too large
+ 0 - 0
portal/dist/assets/index-HylhreMf.js


File diff suppressed because it is too large
+ 0 - 0
portal/dist/assets/index-zXCg8RiH.js


+ 1 - 1
portal/dist/assets/ossFile-BmNYIa3l.js → portal/dist/assets/ossFile-CQMV4lyF.js

@@ -1 +1 @@
-import{r as t}from"./request-DLbisFwp.js";function i(e){return t({timeout:3e5,url:"/api/oss-file/upload",method:"post",data:e,headers:{"Content-Type":"multipart/form-data"}})}function o(e){return t({url:"/api/oss-file/list",method:"get",params:e})}function r(e){return t({url:`/api/oss-file/${e}`,method:"delete"})}function l(e){return t({url:"/api/oss-file/batch",method:"delete",params:{fileIds:e}})}export{l as b,r as d,o as g,i as u};
+import{r as t}from"./request-BQ8lBDVA.js";function i(e){return t({timeout:3e5,url:"/api/oss-file/upload",method:"post",data:e,headers:{"Content-Type":"multipart/form-data"}})}function o(e){return t({url:"/api/oss-file/list",method:"get",params:e})}function r(e){return t({url:`/api/oss-file/${e}`,method:"delete"})}function l(e){return t({url:"/api/oss-file/batch",method:"delete",params:{fileIds:e}})}export{l as b,r as d,o as g,i as u};

File diff suppressed because it is too large
+ 0 - 0
portal/dist/assets/request-BQ8lBDVA.js


+ 1 - 1
portal/dist/assets/user-CSEJNVe3.js → portal/dist/assets/user-INf7jG3f.js

@@ -1 +1 @@
-import{r as e}from"./request-DLbisFwp.js";function s(){return e({url:"/api/user/list",method:"get"})}function u(r){return e({url:"/api/user/info",method:"get",params:r})}function n(r){return e({url:"/api/user/user-token-url",method:"get",params:r})}function o(r){return e({url:"/api/user/login",method:"post",params:r})}function a(r){return e({url:"/api/user/change-password",method:"post",params:r})}export{u as a,s as b,a as c,n as g,o as l};
+import{r as e}from"./request-BQ8lBDVA.js";function s(){return e({url:"/api/user/list",method:"get"})}function u(r){return e({url:"/api/user/info",method:"get",params:r})}function n(r){return e({url:"/api/user/user-token-url",method:"get",params:r})}function o(r){return e({url:"/api/user/login",method:"post",params:r})}function a(r){return e({url:"/api/user/change-password",method:"post",params:r})}export{u as a,s as b,a as c,n as g,o as l};

+ 2 - 2
portal/dist/index.html

@@ -5,8 +5,8 @@
     <link rel="icon" type="image/svg+xml" href="/vite.svg" />
     <meta name="viewport" content="width=device-width, initial-scale=1.0" />
     <title>腾讯广告自动化管理后台</title>
-    <script type="module" crossorigin src="/assets/index-DuzzreWw.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-CdDwsIxv.css">
+    <script type="module" crossorigin src="/assets/index-BU9f2rGa.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-BGn61ZZ8.css">
   </head>
   <body>
     <div id="app"></div>

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

@@ -86,7 +86,7 @@
       
       <!-- 主内容区 -->
       <div class="flex-1 overflow-y-auto bg-gray-50 p-6">
-        <div class="max-w-7xl">
+        <div class="max-w-9xl">
           <router-view />
         </div>
       </div>

+ 40 - 23
portal/src/views/ad-plan-conf/detail.vue

@@ -179,7 +179,7 @@
             <el-button type="warning" @click="handleDeletePlans(confDetail.id)">
             <el-icon><DeleteFilled /></el-icon>
             删除选中计划</el-button>
-           <el-button type="primary" @click="handleSystemRefresh(confDetail.id)">
+           <el-button type="primary" @click="handleSystemStatusSync(confDetail.id)">
             <el-icon><Refresh /></el-icon>
             同步平台状态</el-button>
            <el-button type="primary" @click="handleRefresh(confDetail.id)">
@@ -322,7 +322,7 @@
         </el-table-column>
         <el-table-column prop="adgroupName" label="广告组名称" min-width="100" show-overflow-tooltip />
         <el-table-column prop="accountId" label="账户ID" width="120" />
-        <el-table-column prop="adgroupId" label="广告ID" width="120" />
+        <el-table-column prop="adgroupId" label="广告ID" width="130" />
         <el-table-column label="广告类型" width="100">
           <template #default="{ row }">
             <el-tag :type="getAdTypeColor(row.adType)" size="small">
@@ -340,13 +340,6 @@
             {{ row.deepConversionWorthRate }}
           </template>
         </el-table-column>
-        <el-table-column label="花费" prop="cost" width="120"></el-table-column>
-        <el-table-column label="播放首日变现" prop="reqIncomeVal1" width="120"></el-table-column>
-        <el-table-column label="播放首日ROI" prop="reqIncomeRoi1" width="120"></el-table-column>
-        <el-table-column label="转化首日变现" prop="repIncomeVal1" width="120"></el-table-column>
-        <el-table-column label="转化首日ROI" prop="repIncomeRoi1" width="120"></el-table-column>
-        <!-- <el-table-column prop="target" label="定向模版ID" width="100" show-overflow-tooltip /> -->
-        <!-- <el-table-column prop="site" label="版位模版ID" width="100" show-overflow-tooltip /> -->
         <el-table-column label="提交状态" width="100">
           <template #default="{ row }">
             <el-tag :type="getPlanStatusColor(row.status)" size="small">
@@ -354,14 +347,25 @@
             </el-tag>
           </template>
         </el-table-column>
-        <el-table-column label="设置状态" prop="configuredStatus" width="100" >
+        <el-table-column label="失败原因" min-width="200" show-overflow-tooltip>
           <template #default="{ row }">
-            {{ getConfiguredStatusText(row.configuredStatus) }}
+            <span class="text-red-500">{{ row.failReason || '-' }}</span>
           </template>
         </el-table-column>
-        <el-table-column label="失败原因" min-width="200" show-overflow-tooltip>
+        <el-table-column label="花费" prop="cost" width="100"></el-table-column>
+        <el-table-column label="转化成本" prop="conversionsCost" width="100"></el-table-column>
+        <el-table-column label="曝光" prop="viewCount" width="100"></el-table-column>
+        <el-table-column label="有效点击" prop="validClickCount" width="100"></el-table-column>
+        <el-table-column label="点击率" prop="ctr" width="100"></el-table-column>
+        <el-table-column label="播放首日变现" prop="reqIncomeVal1" width="120"></el-table-column>
+        <el-table-column label="播放首日ROI" prop="reqIncomeRoi1" width="120"></el-table-column>
+        <el-table-column label="转化首日变现" prop="repIncomeVal1" width="120"></el-table-column>
+        <el-table-column label="转化首日ROI" prop="repIncomeRoi1" width="120"></el-table-column>
+        <!-- <el-table-column prop="target" label="定向模版ID" width="100" show-overflow-tooltip /> -->
+        <!-- <el-table-column prop="site" label="版位模版ID" width="100" show-overflow-tooltip /> -->
+        <el-table-column label="设置状态" prop="configuredStatus" width="100" >
           <template #default="{ row }">
-            <span class="text-red-500">{{ row.failReason || '-' }}</span>
+            {{ getConfiguredStatusText(row.configuredStatus) }}
           </template>
         </el-table-column>
         <el-table-column label="系统状态" prop="systemStatus" width="100" show-overflow-tooltip />
@@ -492,11 +496,11 @@ const fetchAdPlans = async () => {
       })
       
       // 默认展开第一个广告计划并加载其创意
-      if (adPlanList.value.length > 0) {
-        const firstPlan = adPlanList.value[0]
-        expandedRows.value = [firstPlan.id]
-        await fetchCreatives(firstPlan)
-      }
+      // if (adPlanList.value.length > 0) {
+      //   const firstPlan = adPlanList.value[0]
+      //   expandedRows.value = [firstPlan.id]
+      //   await fetchCreatives(firstPlan)
+      // }
     } else {
       ElMessage.error(res.msg || '获取广告计划列表失败')
     }
@@ -648,7 +652,7 @@ const handleRefresh = async () => {
 const handleSuspendAll = async (confId) => {
   try {
     await ElMessageBox.confirm(
-      `确定要暂停配置"${confDetail.value.name}"吗?`,
+      `确定要暂停配置"${confDetail.value.name}"的全部计划吗?`,
       '提示',
       {
         confirmButtonText: '确定',
@@ -658,6 +662,7 @@ const handleSuspendAll = async (confId) => {
     )
     const res = await suspendByConfId(confId)
     if (res.success) {
+      ElMessage.success(res.msg || '暂停中。。。')
       handleRefresh()
     } else {
       ElMessage.error(res.msg || '暂停中。。。')
@@ -671,7 +676,7 @@ const handleSuspendAll = async (confId) => {
 const handleDeleteAll = async (confId) => {
   try {
     await ElMessageBox.confirm(
-      `确定要删除配置"${confDetail.value.name}"吗?`,
+      `确定要删除配置"${confDetail.value.name}"的全部计划吗?`,
       '提示',
       {
         confirmButtonText: '确定',
@@ -681,6 +686,7 @@ const handleDeleteAll = async (confId) => {
     )
     const res = await deleteByConfId(confId)
     if (res.success) {
+      ElMessage.success(res.msg || '删除中。。。')
       handleRefresh()
     } else {
       ElMessage.error(res.msg || '删除中。。。')
@@ -694,7 +700,7 @@ const handleDeleteAll = async (confId) => {
 const handleSuspendPlans = async (confId) => {
   try {
     await ElMessageBox.confirm(
-      `确定要暂停配置"${confDetail.value.name}"吗?`,
+      `确定要暂停配置"${confDetail.value.name}"的选中的计划吗?`,
       '提示',
       {
         confirmButtonText: '确定',
@@ -707,6 +713,7 @@ const handleSuspendPlans = async (confId) => {
     }
     const res = await suspendByPlanIds(confId, selectedRows.value.map(r => r.id))
     if (res.success) {
+      ElMessage.success(res.msg || '暂停中。。。')
       handleRefresh()
     } else {
       ElMessage.error(res.msg || '暂停中。。。')
@@ -720,7 +727,7 @@ const handleSuspendPlans = async (confId) => {
 const handleDeletePlans = async (confId) => {
   try {
     await ElMessageBox.confirm(
-      `确定要删除配置"${confDetail.value.name}"吗?`,
+      `确定要删除配置"${confDetail.value.name}"的选中的计划吗?`,
       '提示',
       {
         confirmButtonText: '确定',
@@ -733,6 +740,7 @@ const handleDeletePlans = async (confId) => {
     }
     const res = await deleteByPlanIds(confId, selectedRows.value.map(r => r.id))
     if (res.success) {
+      ElMessage.success(res.msg || '删除中。。。')
       handleRefresh()
     } else {
       ElMessage.error(res.msg || '删除中。。。')
@@ -743,8 +751,17 @@ const handleDeletePlans = async (confId) => {
   }
 }
 
-const handleSystemRefresh = async (confId) => {
+const handleSystemStatusSync = async (confId) => {
   try {
+    await ElMessageBox.confirm(
+      `确定要同步"${confDetail.value.name}"计划的广告平台状态吗?`,
+      '提示',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    )
     const res = await syncSystemStatus(confId)
     if (res.success) {
       handleRefresh()

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

@@ -20,6 +20,7 @@ import com.moka.gdtauto.mapper.TencentAssetDramaMapper;
 import com.moka.gdtauto.service.AccountGroupService;
 import com.moka.gdtauto.service.AdPlanConfService;
 import com.moka.gdtauto.service.AdPlanCreativeService;
+import com.moka.gdtauto.service.AdPlanDailyReportService;
 import com.moka.gdtauto.service.AdPlanService;
 
 import io.swagger.v3.oas.annotations.Operation;
@@ -55,6 +56,7 @@ public class AdPlanConfController {
 
     private final AdPlanConfService adPlanConfService;
     private final AdPlanService adPlanService;
+    private final AdPlanDailyReportService adPlanDailyReportService;
     private final AdPlanCreativeService adPlanCreativeService;
     private final AccountGroupAccountRelationshipMapper relationshipMapper;
     private final SysUserService sysUserService;
@@ -298,6 +300,7 @@ public class AdPlanConfController {
     public Result<Void> syncSystemStatus(@PathVariable Long id, @AuthUser SysUser sysUser) {
         adPlanService.syncAdPlanStatus(id);
         adPlanCreativeService.syncAdPlanCreativeStatus(id);
+        adPlanDailyReportService.syncDailyReport(id);
         try {TimeUnit.SECONDS.sleep(2L);} catch (InterruptedException e) {}
         return Result.success("同步系统状态执行中");
     }

+ 14 - 0
src/main/java/com/moka/gdtauto/mapper/AdPlanMapper.java

@@ -22,4 +22,18 @@ public interface AdPlanMapper extends BaseMapper<AdPlan> {
      * @param plans 包含 id 和 systemStatus 字段的广告计划列表
      */
     void batchUpdateSystemStatus(@Param("plans") List<AdPlan> plans);
+
+    /**
+     * 批量更新广告计划的 REQUEST_TIME 口径日报数据
+     *
+     * @param plans 包含 adgroupId 和日报字段的广告计划列表
+     */
+    void batchUpdateReqDailyReport(@Param("plans") List<AdPlan> plans);
+
+    /**
+     * 批量更新广告计划的 REPORTING_TIME 口径日报数据
+     *
+     * @param plans 包含 adgroupId 和日报字段的广告计划列表
+     */
+    void batchUpdateRepDailyReport(@Param("plans") List<AdPlan> plans);
 }

+ 304 - 0
src/main/java/com/moka/gdtauto/service/AdPlanDailyReportService.java

@@ -0,0 +1,304 @@
+package com.moka.gdtauto.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.moka.gdtauto.client.TencentAdsApiClientFactory;
+import com.moka.gdtauto.common.Constant;
+import com.moka.gdtauto.entity.AdPlan;
+import com.moka.gdtauto.entity.AdPlanConf;
+import com.moka.gdtauto.mapper.AdPlanConfMapper;
+import com.moka.gdtauto.mapper.AdPlanMapper;
+import com.tencent.ads.model.v3.DailyReportsGetResponseData;
+import com.tencent.ads.model.v3.DailyReportsFilteringStruct;
+import com.tencent.ads.model.v3.FilterOperator;
+import com.tencent.ads.model.v3.ReportDateRange;
+import com.tencent.ads.model.v3.ReportStruct;
+import com.tencent.ads.model.v3.TimeLine;
+import com.tencent.ads.v3.TencentAds;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Collectors;
+
+/**
+ * 广告计划日报表同步服务
+ * 按广告组级别同步日报数据到 ad_plan 表
+ *
+ * @author moka
+ * @since 2026-03-10
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class AdPlanDailyReportService {
+
+    private final TencentAdsApiClientFactory clientFactory;
+    private final AdPlanConfMapper adPlanConfMapper;
+    private final AdPlanMapper adPlanMapper;
+
+    /** REQUEST_TIME 口径同步字段 */
+    private static final List<String> REQ_FIELDS = Arrays.asList(
+            "adgroup_id", "date", "cost", "income_val_1", "income_roi_1",
+            "view_count", "valid_click_count", "ctr", "conversions_cost"
+    );
+
+    /** REPORTING_TIME 口径同步字段 */
+    private static final List<String> REP_FIELDS = Arrays.asList(
+            "adgroup_id", "date", "income_val_1", "income_roi_1"
+    );
+
+    private static final int PAGE_SIZE = 100;
+    private static final int ADGROUP_BATCH_LIMIT = 100;
+
+    @Async
+    public void syncDailyReport(Long confId) {
+        syncReqDailyReport(confId, null, null);
+        syncRepDailyReport(confId, null, null);
+    }
+
+    /**
+     * 同步 REQUEST_TIME 口径日报数据到 ad_plan 表
+     *
+     * @param startTime 开始时间
+     * @param endTime   结束时间
+     */
+    public void syncReqDailyReport(Long confId, LocalDateTime startTime, LocalDateTime endTime) {
+        log.info("[syncReqDailyReport] 开始同步 REQUEST_TIME 口径日报 [{}, {}]", startTime, endTime);
+        doSyncDailyReport(confId, startTime, endTime, TimeLine.REQUEST_TIME.getValue(), true);
+    }
+
+    /**
+     * 同步 REPORTING_TIME 口径日报数据到 ad_plan 表
+     *
+     * @param startTime 开始时间
+     * @param endTime   结束时间
+     */
+    public void syncRepDailyReport(Long confId, LocalDateTime startTime, LocalDateTime endTime) {
+        log.info("[syncRepDailyReport] 开始同步 REPORTING_TIME 口径日报 [{}, {}]", startTime, endTime);
+        doSyncDailyReport(confId, startTime, endTime, TimeLine.REPORTING_TIME.getValue(), false);
+    }
+
+    /**
+     * 执行同步逻辑
+     *
+     * @param startTime   开始时间
+     * @param endTime     结束时间
+     * @param timeLine    时间口径
+     * @param isReqTime   是否为 REQUEST_TIME 口径(决定更新哪些字段)
+     */
+    private void doSyncDailyReport(Long confId, LocalDateTime startTime, LocalDateTime endTime, String timeLine, boolean isReqTime) {
+        // 1. 分页查询 create_time 在范围内的 adPlanConf
+        int confPageNum = 1;
+        int totalUpdated = 0;
+        Map<Long, Integer> confUpdatedCount = new ConcurrentHashMap<>();
+
+        while (true) {
+            LambdaQueryWrapper<AdPlanConf> confWrapper = new LambdaQueryWrapper<>();
+            confWrapper
+                    .eq(confId != null, AdPlanConf::getId, confId)
+                    .between(confId == null, AdPlanConf::getCreateTime, startTime, endTime)
+                    .eq(AdPlanConf::getDeleted, 0)
+                    .orderByDesc(AdPlanConf::getId);
+            Page<AdPlanConf> confPage = adPlanConfMapper.selectPage(new Page<>(confPageNum, PAGE_SIZE), confWrapper);
+            List<AdPlanConf> confList = confPage.getRecords();
+
+            if (confList.isEmpty()) {
+                break;
+            }
+
+            // 2. 每个 conf 启动一个线程处理
+            List<CompletableFuture<Void>> futures = confList.stream()
+                    .map(conf -> CompletableFuture.runAsync(() -> {
+                        try {
+                            int updated = processConf(conf, timeLine, isReqTime);
+                            confUpdatedCount.put(conf.getId(), updated);
+                        } catch (Exception e) {
+                            log.error("[doSyncDailyReport] 处理 confId={} 失败", conf.getId(), e);
+                        }
+                    }))
+                    .collect(Collectors.toList());
+
+            // 等待所有线程完成
+            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+
+            if (!confPage.hasNext()) {
+                break;
+            }
+            confPageNum++;
+        }
+
+        totalUpdated = confUpdatedCount.values().stream().mapToInt(Integer::intValue).sum();
+        log.info("[doSyncDailyReport] 同步完成 timeLine={} totalUpdated={}", timeLine, totalUpdated);
+    }
+
+    /**
+     * 处理单个 AdPlanConf
+     */
+    private int processConf(AdPlanConf conf, String timeLine, boolean isReqTime) throws Exception {
+        String jobNumber = conf.getJobNumber();
+        TencentAds tencentAds = clientFactory.getTencentAds(jobNumber, Constant.MAIN_ORG_ACCOUNT_ID);
+
+        // 日期范围使用 conf.createTime 的日期
+        LocalDateTime confCreateTime = conf.getCreateTime();
+        String dateStr = confCreateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+        // + 1天
+        String endStr = confCreateTime.plusDays(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+        ReportDateRange dateRange = new ReportDateRange();
+        dateRange.setStartDate(dateStr);
+        dateRange.setEndDate(endStr);
+
+        log.info("[广告日报] start={} end={} timeLine={} isReqTime={}", dateStr, endStr, timeLine, isReqTime);
+
+        List<String> groupBy = Arrays.asList("adgroup_id", "date");
+        List<String> fields = isReqTime ? REQ_FIELDS : REP_FIELDS;
+
+        int totalUpdated = 0;
+        int planPageNum = 1;
+
+        // 3. 分页查询该 conf 下的所有 adPlan
+        while (true) {
+            LambdaQueryWrapper<AdPlan> planWrapper = new LambdaQueryWrapper<>();
+            planWrapper.eq(AdPlan::getAdPlanConfId, conf.getId())
+                    .isNotNull(AdPlan::getAdgroupId)
+                    .gt(AdPlan::getAdgroupId, 0)
+                    .eq(AdPlan::getDeleted, 0)
+                    .orderByAsc(AdPlan::getId);
+            Page<AdPlan> planPage = adPlanMapper.selectPage(new Page<>(planPageNum, PAGE_SIZE), planWrapper);
+            List<AdPlan> plans = planPage.getRecords();
+
+            if (plans.isEmpty()) {
+                break;
+            }
+
+            // 4. 按 accountId 分组,每 100 个 adgroup_id 一批调用 API
+            Map<Long, List<AdPlan>> groupByAccount = plans.stream()
+                    .filter(p -> p.getAccountId() != null && p.getAdgroupId() != null)
+                    .collect(Collectors.groupingBy(AdPlan::getAccountId));
+
+            for (Map.Entry<Long, List<AdPlan>> entry : groupByAccount.entrySet()) {
+                Long accountId = entry.getKey();
+                List<AdPlan> plansOfAccount = entry.getValue();
+
+                // 按 100 条分批
+                int batchCount = (int) Math.ceil((double) plansOfAccount.size() / ADGROUP_BATCH_LIMIT);
+                for (int i = 0; i < batchCount; i++) {
+                    int fromIdx = i * ADGROUP_BATCH_LIMIT;
+                    int toIdx = Math.min(fromIdx + ADGROUP_BATCH_LIMIT, plansOfAccount.size());
+                    List<AdPlan> batch = plansOfAccount.subList(fromIdx, toIdx);
+
+                    try {
+                        int updated = syncBatch(tencentAds, accountId, batch, dateRange, groupBy, fields, timeLine, isReqTime);
+                        totalUpdated += updated;
+                    } catch (Exception e) {
+                        log.error("[processConf] 同步失败 accountId={} confId={} batch={}", accountId, conf.getId(), i, e);
+                    }
+                }
+            }
+
+            if (!planPage.hasNext()) {
+                break;
+            }
+            planPageNum++;
+        }
+
+        return totalUpdated;
+    }
+
+    /**
+     * 批量同步单批 adPlan
+     */
+    private int syncBatch(TencentAds tencentAds, Long accountId, List<AdPlan> batch,
+                          ReportDateRange dateRange, List<String> groupBy, List<String> fields,
+                          String timeLine, boolean isReqTime) throws Exception {
+
+        // 构建 filtering: adgroup_id IN [...]
+        List<String> adgroupIdValues = batch.stream()
+                .map(p -> String.valueOf(p.getAdgroupId()))
+                .collect(Collectors.toList());
+
+        DailyReportsFilteringStruct filter = new DailyReportsFilteringStruct();
+        filter.setField("adgroup_id");
+        filter.setOperator(FilterOperator.IN);
+        filter.setValues(adgroupIdValues);
+
+        List<DailyReportsFilteringStruct> filtering = new ArrayList<>();
+        filtering.add(filter);
+
+        // 调用 API
+        DailyReportsGetResponseData response = tencentAds.dailyReports().dailyReportsGet(
+                "REPORT_LEVEL_ADGROUP",
+                dateRange,
+                groupBy,
+                fields,
+                accountId,
+                filtering,
+                null,  // orderBy
+                timeLine,
+                1L,
+                (long) ADGROUP_BATCH_LIMIT,
+                null   // modularDetailMode
+        );
+
+        if (response == null || response.getList() == null || response.getList().isEmpty()) {
+            log.warn("[syncBatch] accountId={} 未返回日报数据", accountId);
+            return 0;
+        }
+
+        // 构建更新数据
+        List<AdPlan> toUpdate = new ArrayList<>();
+        for (ReportStruct rs : response.getList()) {
+            if (rs.getAdgroupId() == null) {
+                continue;
+            }
+            AdPlan update = new AdPlan();
+            update.setAdgroupId(rs.getAdgroupId());
+
+            if (isReqTime) {
+                // REQUEST_TIME 口径:更新 cost, view_count, valid_click_count, ctr, conversions_cost, req_income_val_1, req_income_roi_1
+                update.setCost(rs.getCost());
+                update.setViewCount(rs.getViewCount());
+                update.setValidClickCount(rs.getValidClickCount());
+                update.setCtr(d2b(rs.getCtr()));
+                update.setConversionsCost(rs.getConversionsCost());
+                update.setReqIncomeVal1(rs.getIncomeVal1());
+                update.setReqIncomeRoi1(d2b(rs.getIncomeRoi1()));
+            } else {
+                // REPORTING_TIME 口径:更新 rep_income_val_1, rep_income_roi_1
+                update.setRepIncomeVal1(rs.getIncomeVal1());
+                update.setRepIncomeRoi1(d2b(rs.getIncomeRoi1()));
+            }
+            update.setUpdateTime(LocalDateTime.now());
+            toUpdate.add(update);
+        }
+
+        if (toUpdate.isEmpty()) {
+            return 0;
+        }
+
+        // 批量更新
+        if (isReqTime) {
+            adPlanMapper.batchUpdateReqDailyReport(toUpdate);
+        } else {
+            adPlanMapper.batchUpdateRepDailyReport(toUpdate);
+        }
+
+        log.info("[syncBatch] accountId={} 批量更新 {} 条 timeLine={}", accountId, toUpdate.size(), timeLine);
+        return toUpdate.size();
+    }
+
+    /** Double → BigDecimal 安全转换 */
+    private BigDecimal d2b(Double d) {
+        return d == null ? null : BigDecimal.valueOf(d);
+    }
+}

+ 54 - 0
src/main/java/com/moka/gdtauto/task/AdPlanDailyReportSyncTask.java

@@ -0,0 +1,54 @@
+package com.moka.gdtauto.task;
+
+import java.time.LocalDateTime;
+
+import org.springframework.scheduling.annotation.Scheduled;
+
+import org.springframework.stereotype.Component;
+
+import com.moka.gdtauto.service.AdPlanDailyReportService;
+import com.plumelog.core.TraceId;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
+
+
+/**
+ * 广告日报
+ * 
+ */
+@Component
+@AllArgsConstructor
+@Slf4j
+public class AdPlanDailyReportSyncTask {
+    
+    private final AdPlanDailyReportService adPlanDailyReportService;
+    @Scheduled(cron = "0 0 * * * *")
+    @SchedulerLock(name = "syncReqDailyReport", lockAtMostFor = "4m", lockAtLeastFor = "1m")
+    public void syncReqDailyReport() {
+        try {
+            TraceId.set();
+            log.info("[syncReqDailyReport] 开始同步广告播放口径日报");
+            LocalDateTime start = LocalDateTime.now().minusDays(1).withHour(0).withMinute(0).withSecond(0);
+            LocalDateTime end = LocalDateTime.now().minusDays(1).withHour(23).withMinute(59).withSecond(59);
+            adPlanDailyReportService.syncReqDailyReport(null, start, end);
+        } catch (Exception e) {
+            log.error("[syncReqDailyReport] 同步广告播放口径日报异常", e);
+        }
+    }
+
+    @Scheduled(cron = "0 10 * * * *")
+    @SchedulerLock(name = "syncRepDailyReport", lockAtMostFor = "4m", lockAtLeastFor = "1m")
+    public void syncRepDailyReport() {
+        try {
+            TraceId.set();
+            log.info("[syncRepDailyReport] 开始同步转化回传口径日报");
+            LocalDateTime start = LocalDateTime.now().minusDays(1).withHour(0).withMinute(0).withSecond(0);
+            LocalDateTime end = LocalDateTime.now().minusDays(1).withHour(23).withMinute(59).withSecond(59);
+            adPlanDailyReportService.syncRepDailyReport(null,start, end);
+        } catch (Exception e) {
+            log.error("[syncRepDailyReport] 同步转化回传口径日报异常", e);
+        }
+    }
+}

+ 27 - 0
src/main/resources/mapper/AdPlanMapper.xml

@@ -13,4 +13,31 @@
         </foreach>
     </update>
 
+    <!-- 批量更新广告计划 REQUEST_TIME 口径日报数据 -->
+    <update id="batchUpdateReqDailyReport" parameterType="java.util.List">
+        <foreach collection="plans" item="item" separator=";">
+            UPDATE ad_plan
+            SET cost              = #{item.cost},
+                view_count        = #{item.viewCount},
+                valid_click_count = #{item.validClickCount},
+                ctr               = #{item.ctr},
+                conversions_cost  = #{item.conversionsCost},
+                req_income_val1  = #{item.reqIncomeVal1},
+                req_income_roi1  = #{item.reqIncomeRoi1},
+                update_time       = NOW()
+            WHERE adgroup_id = #{item.adgroupId}
+        </foreach>
+    </update>
+
+    <!-- 批量更新广告计划 REPORTING_TIME 口径日报数据 -->
+    <update id="batchUpdateRepDailyReport" parameterType="java.util.List">
+        <foreach collection="plans" item="item" separator=";">
+            UPDATE ad_plan
+            SET rep_income_val1 = #{item.repIncomeVal1},
+                rep_income_roi1 = #{item.repIncomeRoi1},
+                update_time      = NOW()
+            WHERE adgroup_id = #{item.adgroupId}
+        </foreach>
+    </update>
+
 </mapper>

+ 36 - 0
src/test/java/com/moka/gdtauto/service/AdPlanDailyReportSyncTest.java

@@ -0,0 +1,36 @@
+package com.moka.gdtauto.service;
+
+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 com.moka.gdtauto.GdtAutoApplication;
+import com.moka.gdtauto.task.AdPlanDailyReportSyncTask;
+
+
+
+/**
+ * 广告计划日报同步测试
+ * mvn test -Dtest=AdPlanDailyReportSyncTest -Dspring.profiles.active=test
+ */
+@SpringBootTest(classes = GdtAutoApplication.class)
+@ActiveProfiles("test")
+public class AdPlanDailyReportSyncTest {
+    
+    @Autowired
+    private AdPlanDailyReportSyncTask adPlanDailyReportSyncTask;
+    @Autowired
+    private AdPlanDailyReportService adPlanDailyReportService;
+
+
+    @Test
+    public void testSyncReqDailyReport() {
+        adPlanDailyReportService.syncReqDailyReport(29L, null, null);
+    }
+
+    @Test
+    public void testSyncRepDailyReport() {
+        adPlanDailyReportService.syncRepDailyReport(29L, null, null);
+    }
+}

Some files were not shown because too many files changed in this diff