Kaynağa Gözat

feat:支持视频创意,创建速度优化

pudongliang 4 ay önce
ebeveyn
işleme
be66baf2fb
66 değiştirilmiş dosya ile 1015 ekleme ve 99 silme
  1. 1 1
      .gitignore
  2. 50 0
      .vscode/launch.json
  3. 5 0
      .vscode/settings.json
  4. 3 0
      portal/.vscode/extensions.json
  5. 1 1
      portal/dist/assets/accountGroup-BKL9yVFs.js
  6. 1 1
      portal/dist/assets/adPlanConf-ChJS0TbS.js
  7. 1 1
      portal/dist/assets/auth-B2nX56rw.js
  8. 1 1
      portal/dist/assets/conversion-sn6DCzLa.js
  9. 0 0
      portal/dist/assets/detail-DDb6Dgxn.js
  10. 0 0
      portal/dist/assets/detail-DE5KJoqu.js
  11. 1 1
      portal/dist/assets/drama-BU3tW7W-.js
  12. 0 0
      portal/dist/assets/index-AOCA_H1_.js
  13. 0 0
      portal/dist/assets/index-B4bwFZBN.js
  14. 0 0
      portal/dist/assets/index-BAtgzrkK.css
  15. 0 0
      portal/dist/assets/index-BCHUF70o.js
  16. 0 0
      portal/dist/assets/index-BLu7DkFI.js
  17. 0 0
      portal/dist/assets/index-BMB56fl-.css
  18. 0 0
      portal/dist/assets/index-BSVU0cEa.js
  19. 0 0
      portal/dist/assets/index-BXgiuV5d.js
  20. 0 0
      portal/dist/assets/index-BgrQmebK.js
  21. 1 1
      portal/dist/assets/index-Br1izHrQ.js
  22. 0 0
      portal/dist/assets/index-CCr6YYR4.js
  23. 0 1
      portal/dist/assets/index-C_7xiWjB.css
  24. 0 0
      portal/dist/assets/index-Ce6T8bQF.js
  25. 0 0
      portal/dist/assets/index-D26C5WOn.js
  26. 0 0
      portal/dist/assets/index-DL0c3Li9.js
  27. 0 0
      portal/dist/assets/index-Drk8g2yZ.js
  28. 0 0
      portal/dist/assets/index-E3motGVF.js
  29. 1 0
      portal/dist/assets/index-NiXg4so_.css
  30. 0 0
      portal/dist/assets/index-RShexnyK.js
  31. 1 1
      portal/dist/assets/loadPageConf-wc7AXuZw.js
  32. 1 1
      portal/dist/assets/ossFile-DBKIa4n4.js
  33. 0 0
      portal/dist/assets/request-DPAaulnq.js
  34. 1 1
      portal/dist/assets/user-CNlTyVuD.js
  35. 2 2
      portal/dist/index.html
  36. 4 0
      portal/src/style.css
  37. 12 9
      portal/src/views/ad-plan-conf/detail.vue
  38. 31 6
      portal/src/views/ad-plan-conf/index.vue
  39. 92 3
      portal/src/views/oss-file/index.vue
  40. 2 2
      src/main/java/com/moka/gdtauto/BaseCodeGenerator.java
  41. 21 10
      src/main/java/com/moka/gdtauto/controller/AdPlanConfController.java
  42. 18 0
      src/main/java/com/moka/gdtauto/controller/TencentAccountIdBrandImageController.java
  43. 8 0
      src/main/java/com/moka/gdtauto/controller/vo/CreateAdPlanConfRequest.java
  44. 8 0
      src/main/java/com/moka/gdtauto/controller/vo/UpdateAdPlanConfRequest.java
  45. 4 1
      src/main/java/com/moka/gdtauto/entity/AdPlanConf.java
  46. 15 0
      src/main/java/com/moka/gdtauto/entity/AdPlanCreative.java
  47. 69 0
      src/main/java/com/moka/gdtauto/entity/TencentAccountIdBrandImage.java
  48. 11 0
      src/main/java/com/moka/gdtauto/entity/TencentAccountIdImage.java
  49. 10 0
      src/main/java/com/moka/gdtauto/entity/TencentAccountIdVideoId.java
  50. 21 0
      src/main/java/com/moka/gdtauto/mapper/TencentAccountIdBrandImageMapper.java
  51. 5 0
      src/main/java/com/moka/gdtauto/mapper/TencentAccountIdImageMapper.java
  52. 5 0
      src/main/java/com/moka/gdtauto/mapper/TencentAccountIdVideoIdMapper.java
  53. 36 0
      src/main/java/com/moka/gdtauto/service/TencentAccountIdBrandImageService.java
  54. 18 0
      src/main/java/com/moka/gdtauto/service/TencentAccountIdImageService.java
  55. 19 0
      src/main/java/com/moka/gdtauto/service/TencentAccountIdVideoIdService.java
  56. 17 0
      src/main/java/com/moka/gdtauto/service/TencentAdsAdgroupService.java
  57. 352 53
      src/main/java/com/moka/gdtauto/service/impl/AdPlanConfServiceImpl.java
  58. 3 1
      src/main/java/com/moka/gdtauto/service/impl/OssFileServiceImpl.java
  59. 50 0
      src/main/java/com/moka/gdtauto/service/impl/TencentAccountIdBrandImageServiceImp.java
  60. 28 0
      src/main/java/com/moka/gdtauto/service/impl/TencentAccountIdImageServiceImp.java
  61. 29 0
      src/main/java/com/moka/gdtauto/service/impl/TencentAccountIdVideoIdServiceImp.java
  62. 34 0
      src/main/java/com/moka/gdtauto/util/TencentFileMd5Util.java
  63. 10 0
      src/main/resources/mapper/TencentAccountIdBrandImageMapper.xml
  64. 5 0
      src/main/resources/mapper/TencentAccountIdImageMapper.xml
  65. 5 0
      src/main/resources/mapper/TencentAccountIdVideoIdMapper.xml
  66. 2 1
      src/test/java/com/moka/gdtauto/service/OssFileMd5.java

+ 1 - 1
.gitignore

@@ -29,7 +29,7 @@ build/
 !**/src/test/**/build/
 
 ### VS Code ###
-.vscode/
+
 
 ### Logs ###
 logs/

+ 50 - 0
.vscode/launch.json

@@ -0,0 +1,50 @@
+{
+    // 使用 IntelliSense 了解相关属性。 
+    // 悬停以查看现有属性的描述。
+    // 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
+    "version": "0.2.0",
+    "configurations": [
+        {
+            "type": "java",
+            "name": "Current File",
+            "request": "launch",
+            "mainClass": "${file}"
+        },
+        {
+            "type": "java",
+            "name": "BaseCodeGenerator",
+            "request": "launch",
+            "mainClass": "com.moka.gdtauto.BaseCodeGenerator",
+            "projectName": "gdt-auto"
+        },
+        {
+            "type": "java",
+            "name": "GdtAutoApplication",
+            "request": "launch",
+            "mainClass": "com.moka.gdtauto.GdtAutoApplication",
+            "projectName": "gdt-auto",
+            "vmArgs": "--add-opens java.base/java.lang=ALL-UNNAMED --add-opens java.base/java.util=ALL-UNNAMED"
+        },
+        {
+            "type": "java",
+            "name": "ImageWatermarkSimpleTest",
+            "request": "launch",
+            "mainClass": "com.moka.gdtauto.util.ImageWatermarkSimpleTest",
+            "projectName": "gdt-auto"
+        },
+        {
+            "type": "java",
+            "name": "SampleTest",
+            "request": "launch",
+            "mainClass": "com.moka.gdtauto.util.SampleTest",
+            "projectName": "gdt-auto"
+        },
+        {
+            "type": "java",
+            "name": "VideoWatermarkSimpleTest",
+            "request": "launch",
+            "mainClass": "com.moka.gdtauto.util.VideoWatermarkSimpleTest",
+            "projectName": "gdt-auto"
+        }
+    ]
+}

+ 5 - 0
.vscode/settings.json

@@ -0,0 +1,5 @@
+{
+    "java.configuration.updateBuildConfiguration": "automatic",
+    "java.compile.nullAnalysis.mode": "automatic",
+    "cSpell.enabled": false
+}

+ 3 - 0
portal/.vscode/extensions.json

@@ -0,0 +1,3 @@
+{
+  "recommendations": ["Vue.volar"]
+}

+ 1 - 1
portal/dist/assets/accountGroup-kLwTKZmR.js → portal/dist/assets/accountGroup-BKL9yVFs.js

@@ -1 +1 @@
-import{r as u}from"./request-GYOOhKo9.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-DPAaulnq.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-BheiTRbJ.js → portal/dist/assets/adPlanConf-ChJS0TbS.js

@@ -1 +1 @@
-import{r as a}from"./request-GYOOhKo9.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-DPAaulnq.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-CzhhlL42.js → portal/dist/assets/auth-B2nX56rw.js

@@ -1 +1 @@
-import{r as t}from"./request-GYOOhKo9.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-DPAaulnq.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-CmLtY_Pa.js → portal/dist/assets/conversion-sn6DCzLa.js

@@ -1 +1 @@
-import{r as t}from"./request-GYOOhKo9.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-DPAaulnq.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};

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/detail-DDb6Dgxn.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/detail-DE5KJoqu.js


+ 1 - 1
portal/dist/assets/drama-C7EQZzd7.js → portal/dist/assets/drama-BU3tW7W-.js

@@ -1 +1 @@
-import{r as t}from"./request-GYOOhKo9.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-DPAaulnq.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};

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/index-AOCA_H1_.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/index-B4bwFZBN.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/index-BAtgzrkK.css


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/index-BCHUF70o.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/index-BLu7DkFI.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/index-BMB56fl-.css


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/index-BSVU0cEa.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/index-BXgiuV5d.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/index-BgrQmebK.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 1 - 1
portal/dist/assets/index-Br1izHrQ.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/index-CCr6YYR4.js


+ 0 - 1
portal/dist/assets/index-C_7xiWjB.css

@@ -1 +0,0 @@
-.btn-primary[data-v-9955c2ec]{background:linear-gradient(135deg,#667eea,#764ba2);border:none;color:#fff}.btn-primary[data-v-9955c2ec]:hover{background:linear-gradient(135deg,#764ba2,#667eea)}.btn-secondary[data-v-9955c2ec]{border-color:#e5e7eb;color:#6b7280}.btn-secondary[data-v-9955c2ec]:hover{background-color:#f9fafb}[data-v-9955c2ec] .el-upload-dragger{border:2px dashed #d1d5db;border-radius:8px;background-color:#f9fafb;transition:all .3s}[data-v-9955c2ec] .el-upload-dragger:hover{border-color:#667eea;background-color:#f3f4f6}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/index-Ce6T8bQF.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/index-D26C5WOn.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/index-DL0c3Li9.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/index-Drk8g2yZ.js


Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/index-E3motGVF.js


+ 1 - 0
portal/dist/assets/index-NiXg4so_.css

@@ -0,0 +1 @@
+.btn-primary[data-v-8929ff85]{background:linear-gradient(135deg,#667eea,#764ba2);border:none;color:#fff}.btn-primary[data-v-8929ff85]:hover{background:linear-gradient(135deg,#764ba2,#667eea)}.btn-secondary[data-v-8929ff85]{border-color:#e5e7eb;color:#6b7280}.btn-secondary[data-v-8929ff85]:hover{background-color:#f9fafb}[data-v-8929ff85] .el-upload-dragger{border:2px dashed #d1d5db;border-radius:8px;background-color:#f9fafb;transition:all .3s}[data-v-8929ff85] .el-upload-dragger:hover{border-color:#667eea;background-color:#f3f4f6}

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/index-RShexnyK.js


+ 1 - 1
portal/dist/assets/loadPageConf-DfwdfAiV.js → portal/dist/assets/loadPageConf-wc7AXuZw.js

@@ -1 +1 @@
-import{r as o}from"./request-GYOOhKo9.js";function t(a){return o({url:"/api/load-page-conf/list",method:"get",params:a})}function n(a){return o({url:`/api/load-page-conf/${a}`,method:"get"})}function r(a){return o({url:"/api/load-page-conf",method:"post",data:a})}function u(a){return o({url:"/api/load-page-conf",method:"put",data:a})}function d(){return o({url:"/api/load-page-conf/all",method:"get"})}export{t as a,n as b,r as c,d as g,u};
+import{r as o}from"./request-DPAaulnq.js";function t(a){return o({url:"/api/load-page-conf/list",method:"get",params:a})}function n(a){return o({url:`/api/load-page-conf/${a}`,method:"get"})}function r(a){return o({url:"/api/load-page-conf",method:"post",data:a})}function u(a){return o({url:"/api/load-page-conf",method:"put",data:a})}function d(){return o({url:"/api/load-page-conf/all",method:"get"})}export{t as a,n as b,r as c,d as g,u};

+ 1 - 1
portal/dist/assets/ossFile-MqFfyJ4q.js → portal/dist/assets/ossFile-DBKIa4n4.js

@@ -1 +1 @@
-import{r as t}from"./request-GYOOhKo9.js";function i(e){return t({timeout:12e5,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-DPAaulnq.js";function i(e){return t({timeout:12e5,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};

Dosya farkı çok büyük olduğundan ihmal edildi
+ 0 - 0
portal/dist/assets/request-DPAaulnq.js


+ 1 - 1
portal/dist/assets/user-umYKzI0W.js → portal/dist/assets/user-CNlTyVuD.js

@@ -1 +1 @@
-import{r as e}from"./request-GYOOhKo9.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-DPAaulnq.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-D2Gwvn-S.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-BAtgzrkK.css">
+    <script type="module" crossorigin src="/assets/index-Br1izHrQ.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-BMB56fl-.css">
   </head>
   <body>
     <div id="app"></div>

+ 4 - 0
portal/src/style.css

@@ -10,3 +10,7 @@ body {
 #app {
   min-height: 100vh;
 }
+
+.el-image-viewer__wrapper {
+  z-index: 9999 !important;
+}

+ 12 - 9
portal/src/views/ad-plan-conf/detail.vue

@@ -261,14 +261,16 @@
                 <el-table-column label="创意图片/视频" width="120">
                   <template #default="{ row: creative }">
                     <el-image
-                      v-if="creative.creativeFileUrl"
+                      v-if="creative.creativeFileType === 'image' && creative.creativeFileUrl"
                       :src="creative.creativeFileUrl"
                       :preview-src-list="[creative.creativeFileUrl]"
-                      fit="cover"
+                      :preview-teleported="true"
                       style="width: 60px; height: 60px"
-                      class="cursor-pointer rounded border"
+                      class="cursor-pointer rounded"
                     />
-                    <span v-else>-</span>
+                    <video v-else-if="creative.creativeFileType === 'video' && creative.creativeFileUrl" controls>
+                      <source :src="creative.creativeFileUrl" type="video/mp4">
+                    </video>
                   </template>
                 </el-table-column>
                 <el-table-column prop="creativeText" label="创意文案" min-width="150" show-overflow-tooltip />
@@ -279,6 +281,7 @@
                       v-if="creative.brandImageUrl"
                       :src="creative.brandImageUrl"
                       :preview-src-list="[creative.brandImageUrl]"
+                      :preview-teleported="true"
                       fit="cover"
                       style="width: 60px; height: 60px"
                       class="cursor-pointer rounded border"
@@ -496,11 +499,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 || '获取广告计划列表失败')
     }

+ 31 - 6
portal/src/views/ad-plan-conf/index.vue

@@ -260,6 +260,7 @@
             style="width: 100%"
             placeholder="请选择短剧"
             filterable
+            clearable
           >
             <el-option
               v-for="drama in sortedDramaList"
@@ -282,7 +283,7 @@
           />
         </el-form-item>
 
-        <el-form-item label="落地页IDs" prop="pageIds">
+        <el-form-item label="落地页" prop="pageIdArr">
           <el-select
             :disabled="valueDisabled"
             v-model="formData.pageIdArr"
@@ -300,7 +301,7 @@
           </el-select>
         </el-form-item>
 
-        <el-form-item label="创意图片/视频">
+        <el-form-item label="创意图片/视频" prop="creativeFiles">
           <div class="flex flex-col gap-2">
             <el-button :disabled="valueDisabled" size="small" @click="openFileSelector('creative')" style="width: fit-content">选择文件</el-button>
             <div v-if="formData.creativeFileUrls" class="flex flex-wrap gap-2">
@@ -313,6 +314,7 @@
                   :src="url"
                   :preview-src-list="formData.creativeFileUrls.split(',')"
                   :initial-index="index"
+                  :preview-teleported="true"
                   fit="cover"
                   style="width: 80px; height: 80px; display: block"
                   class="cursor-pointer rounded border"
@@ -332,7 +334,7 @@
           </div>
         </el-form-item>
 
-        <el-form-item label="品牌形象图片">
+        <el-form-item label="品牌形象图片" prop="brandImageUrl">
           <div class="flex flex-col gap-2">
             <el-button :disabled="valueDisabled" size="small" @click="openFileSelector('brand')" style="width: fit-content">选择图片</el-button>
             <div v-if="formData.brandImageUrl">
@@ -497,11 +499,16 @@
         <el-table-column type="selection" width="55" />
         <el-table-column prop="fileUrl" label="展示" max-width="100" show-overflow-tooltip >
           <template #default="{ row }">
-            <el-image
+            <el-image v-if="row.fileType === 'image'"
               :src="row.fileUrl"
               style="width: 100px; height: 100px"
               fit="cover"
             />
+            <video v-else-if="row.fileType === 'video'"
+              :src="row.fileUrl"
+              style="width: 100px; height: 100px"
+              controls
+            />
           </template>
         </el-table-column>
         <el-table-column prop="fileType" label="类型" width="100" />
@@ -641,8 +648,10 @@ const formData = ref({
   imageIds: '',
   pageIds: '',
   pageIdArr: [],
+  creativeFileArr: [],
   creativeFileUrls: '',
   creativeFileNames: '',
+  brandImageFile: null,
   brandImageUrl: '',
   creativeText: '',
   brandName: '',
@@ -694,12 +703,18 @@ const formRules = {
   dramaAmount: [
     { required: true, message: '请输入短剧每户几部剧', trigger: 'blur' }
   ],
-  pageIds: [
-    { required: true, message: '请输入页面ID', trigger: 'blur' }
+  pageIdArr: [
+    { required: true, message: '请选择落地页', trigger: 'change' }
+  ],
+  creativeFileUrls: [
+    { required: true, message: '请选择创意文件', trigger: 'blur' }
   ],
   creativeText: [
     { required: true, message: '请输入创意文案', trigger: 'blur' }
   ],
+  brandImageUrl: [
+    { required: true, message: '请选择品牌图片', trigger: 'blur' }
+  ],
   brandName: [
     { required: true, message: '请输入品牌名称', trigger: 'blur' }
   ],
@@ -803,6 +818,7 @@ const handleCreate = () => {
     pageIdArr: [],
     creativeFileUrls: '',
     creativeFileNames: '',
+    brandImageFile: null,
     brandImageUrl: '',
     creativeText: '情感美文点点看',
     brandName: '情感美文',
@@ -834,6 +850,8 @@ const handleEdit = (row) => {
     pageIdArr: row.pageIds ? row.pageIds.split(',').map(Number) : [],
     creativeFileUrls: row.creativeFileUrls || '',
     creativeFileNames: row.creativeFileNames || '',
+    creativeFileArr: row.creativeFiles ? JSON.parse(row.creativeFiles) : [],
+    brandImageFile: row.brandImageFile ? JSON.parse(row.brandImageFile) : null,
     brandImageUrl: row.brandImageUrl || '',
     creativeText: row.creativeText || '',
     brandName: row.brandName || '',
@@ -1042,8 +1060,10 @@ const confirmFileSelection = () => {
   if (currentFileType.value === 'creative') {
     formData.value.creativeFileUrls = urls
     formData.value.creativeFileNames = fileNames
+    formData.value.creativeFileArr = selectedFiles.value
   } else if (currentFileType.value === 'brand') {
     formData.value.brandImageUrl = selectedFiles.value[0].fileUrl
+    formData.value.brandImageFile = selectedFiles.value[0]
   }
   
   fileSelectorVisible.value = false
@@ -1056,8 +1076,13 @@ const removeFile = (type, index) => {
     const urls = formData.value.creativeFileUrls.split(',')
     urls.splice(index, 1)
     formData.value.creativeFileUrls = urls.filter(url => url).join(',')
+    // 同步更新 creativeFileArr
+    if (formData.value.creativeFileArr && formData.value.creativeFileArr.length > index) {
+      formData.value.creativeFileArr.splice(index, 1)
+    }
   } else if (type === 'brand') {
     formData.value.brandImageUrl = ''
+    formData.value.brandImageFile = null
   }
 }
 

+ 92 - 3
portal/src/views/oss-file/index.vue

@@ -45,7 +45,15 @@
             </div>
             <template #tip>
               <div class="el-upload__tip">
-                {{ uploadForm.fileType === 'image' ? '支持 jpg/png/gif/webp 格式图片' : '支持 mp4/mov/avi 格式视频' }},最多20个文件,单个文件建议不超过100MB
+                <template v-if="uploadForm.businessType === 2">
+                  品牌形象图片要求:尺寸 512x512,大小不超过 400KB,格式:jpg/jpeg/png,最多20个文件
+                </template>
+                <template v-else-if="uploadForm.fileType === 'image'">
+                  支持 jpg/png/gif 格式图片,最多20个文件,单个文件不超过 10MB
+                </template>
+                <template v-else>
+                  支持 mp4/mov/avi 格式视频,最多20个文件,单个文件建议不超过100MB
+                </template>
               </div>
             </template>
           </el-upload>
@@ -257,13 +265,44 @@ const uploading = ref(false)
 // 根据文件类型计算接受的文件格式
 const fileAccept = computed(() => {
   if (uploadForm.fileType === 'image') {
-    return 'image/jpeg,image/png,image/gif,image/webp'
+    // 品牌形象只接受 jpg/jpeg/png
+    if (uploadForm.businessType === 2) {
+      return 'image/jpeg,image/png'
+    }
+    // 非品牌形象图片:jpg、png、gif
+    return 'image/jpeg,image/png,image/gif'
   } else if (uploadForm.fileType === 'video') {
     return 'video/mp4,video/quicktime,video/x-msvideo'
   }
   return ''
 })
 
+// 验证品牌形象图片
+const validateBrandImage = (file) => {
+  return new Promise((resolve, reject) => {
+    // 验证文件大小(400KB = 400 * 1024 bytes)
+    const maxSize = 400 * 1024
+    if (file.size > maxSize) {
+      reject(`图片大小不能超过 400KB,当前大小:${formatFileSize(file.size)}`)
+      return
+    }
+
+    // 验证图片尺寸
+    const img = new Image()
+    img.onload = () => {
+      if (img.width !== 512 || img.height !== 512) {
+        reject(`图片尺寸必须为 512x512,当前尺寸:${img.width}x${img.height}`)
+      } else {
+        resolve(true)
+      }
+    }
+    img.onerror = () => {
+      reject('图片加载失败,请检查文件格式')
+    }
+    img.src = URL.createObjectURL(file)
+  })
+}
+
 // 查询表单
 const queryForm = reactive({
   businessType: null,
@@ -287,8 +326,58 @@ const pagination = reactive({
 const previewVisible = ref(false)
 const previewUrl = ref('')
 
+// 验证普通图片
+const validateNormalImage = (file) => {
+  return new Promise((resolve, reject) => {
+    // 验证文件大小(10MB = 10 * 1024 * 1024 bytes)
+    const maxSize = 10 * 1024 * 1024
+    if (file.size > maxSize) {
+      reject(`图片大小不能超过 10MB,当前大小:${formatFileSize(file.size)}`)
+      return
+    }
+    resolve(true)
+  })
+}
+
 // 文件选择
-const handleFileChange = (file, fileList) => {
+const handleFileChange = async (file, fileList) => {
+  const rawFile = file.raw
+  
+  // 如果是品牌形象图片,进行额外验证
+  if (uploadForm.businessType === 2) {
+    // 验证文件格式
+    const validTypes = ['image/jpeg', 'image/png', 'image/jpg']
+    if (!validTypes.includes(rawFile.type)) {
+      ElMessage.error('品牌形象图片仅支持 jpg、jpeg、png 格式')
+      uploadRef.value?.handleRemove(file)
+      return
+    }
+    
+    try {
+      await validateBrandImage(rawFile)
+    } catch (error) {
+      ElMessage.error(error)
+      uploadRef.value?.handleRemove(file)
+      return
+    }
+  } else if (uploadForm.fileType === 'image') {
+    // 非品牌形象图片验证
+    const validTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/jpg']
+    if (!validTypes.includes(rawFile.type)) {
+      ElMessage.error('图片仅支持 jpg、jpeg、png、gif 格式')
+      uploadRef.value?.handleRemove(file)
+      return
+    }
+    
+    try {
+      await validateNormalImage(rawFile)
+    } catch (error) {
+      ElMessage.error(error)
+      uploadRef.value?.handleRemove(file)
+      return
+    }
+  }
+  
   uploadForm.files = fileList.map(f => f.raw)
 }
 

+ 2 - 2
src/main/java/com/moka/gdtauto/BaseCodeGenerator.java

@@ -9,7 +9,7 @@ import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
 
 public class BaseCodeGenerator {
 
-    public final static String TABLE_NAMES = "load_page_conf";
+    public final static String TABLE_NAMES = "tencent_account_id_brand_image";
     public static void main(String[] args) {
         String url = "jdbc:mysql://rm-uf65d6xsxrp743w3aio.mysql.rds.aliyuncs.com:3306/gdt_auto?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true";
         FastAutoGenerator.create(url, "gdt_auto", "QA_GDT_AUTO%$PDL")
@@ -28,7 +28,7 @@ public class BaseCodeGenerator {
         )
         .strategyConfig(builder -> builder
                 .addInclude(TABLE_NAMES)
-                .entityBuilder().enableFileOverride()
+                .entityBuilder().enableLombok().enableFileOverride()
                 .mapperBuilder().enableFileOverride()
                 .serviceBuilder().formatServiceFileName("%sService")
                 .formatServiceImplFileName("%sServiceImp").enableFileOverride()

+ 21 - 10
src/main/java/com/moka/gdtauto/controller/AdPlanConfController.java

@@ -1,5 +1,6 @@
 package com.moka.gdtauto.controller;
 
+import com.alibaba.fastjson2.JSON;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.moka.gdtauto.annotation.AuthUser;
@@ -13,6 +14,7 @@ import com.moka.gdtauto.entity.SysUser;
 import com.moka.gdtauto.entity.TencentAssetDrama;
 import com.moka.gdtauto.entity.AdPlan;
 import com.moka.gdtauto.entity.AdPlanCreative;
+import com.moka.gdtauto.entity.OssFile;
 import com.moka.gdtauto.service.SysUserService;
 
 import com.moka.gdtauto.mapper.AccountGroupAccountRelationshipMapper;
@@ -172,10 +174,7 @@ public class AdPlanConfController {
             List<AccountGroupAccountRelationship> relationships) {
         String dateTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
         String accountGroupName = accountGroup.getGroupName();
-        String creativeFileUrls = request.getCreativeFileUrls();
-        String creativeFileNames = request.getCreativeFileNames();
-        String[] creativeUrlArray = creativeFileUrls.split(",");
-        String[] creativeNameArray = creativeFileNames != null ? creativeFileNames.split(",") : new String[0];
+        List<OssFile> creativeFileArr = request.getCreativeFileArr();
         List<AdPlan> plans = new ArrayList<>();
         Integer dramaAmount = request.getDramaAmount();
         Long dramaId = request.getDramaId();
@@ -233,22 +232,28 @@ public class AdPlanConfController {
                         List<AdPlanCreative> adPlanCreatives = new ArrayList<>();
                         adPlan.setAdCreatives(adPlanCreatives);
                         int j = 0;
-                        for (String creativeUrl : creativeUrlArray) {
+                        for (OssFile creativeFile : creativeFileArr) {
                             j++;
-                            String fileName = (j < creativeNameArray.length && creativeNameArray[j] != null
-                                    && !creativeNameArray[j].isEmpty())
-                                            ? creativeNameArray[j]
-                                            : creativeUrl.substring(creativeUrl.lastIndexOf("/") + 1);
+                            String creativeUrl = creativeFile.getFileUrl();
+                            String originalFileName = creativeFile.getOriginalFileName();
+                            // 去除文件后缀
+                            String fileName = originalFileName != null
+                                    ? originalFileName.replaceAll("\\.[^.]+$", "")
+                                    : creativeUrl.substring(creativeUrl.lastIndexOf("/") + 1);
                             String creativeName = accountGroupName.trim() + "-" + dateTime + "-" + j + "-" + fileName;
                             AdPlanCreative adPlanCreative = new AdPlanCreative();
                             adPlanCreative.setCreativeFileUrl(creativeUrl);
+                            adPlanCreative.setCreativeFileMd5(creativeFile.getFileMd5());
+                            adPlanCreative.setCreativeFileType(creativeFile.getFileType());
                             adPlanCreative.setCreativeId(0L);
                             adPlanCreative.setCreativeName(creativeName);
                             adPlanCreative.setCreativeText(request.getCreativeText());
                             adPlanCreative.setImageId(0L);
+                            adPlanCreative.setVideoId(0L);
                             adPlanCreative.setBrandImageId(0L);
                             adPlanCreative.setBrandName(request.getBrandName());
-                            adPlanCreative.setBrandImageUrl(request.getBrandImageUrl());
+                            adPlanCreative.setBrandImageUrl(request.getBrandImageFile().getFileUrl());
+                            adPlanCreative.setBrandImageFileMd5(request.getBrandImageFile().getFileMd5());
                             adPlanCreative.setButtonText(request.getButtonText());
                             adPlanCreative.setPageIds(request.getPageIds());
                             adPlanCreatives.add(adPlanCreative);
@@ -282,6 +287,8 @@ public class AdPlanConfController {
             AdPlanConf adPlanConf = new AdPlanConf();
             if (confId == null) {
                 BeanUtils.copyProperties(request, adPlanConf);
+                adPlanConf.setCreativeFiles(JSON.toJSONString(request.getCreativeFileArr()));
+                adPlanConf.setBrandImageFile(JSON.toJSONString(request.getBrandImageFile()));
                 adPlanConf.setUserId(sysUser.getId());
                 adPlanConf.setJobNumber(sysUser.getJobNumber());
                 adPlanConf.setCreator(sysUser.getName());
@@ -344,6 +351,8 @@ public class AdPlanConfController {
         try {
             AdPlanConf adPlanConf = new AdPlanConf();
             BeanUtils.copyProperties(request, adPlanConf);
+            adPlanConf.setCreativeFiles(JSON.toJSONString(request.getCreativeFileArr()));
+            adPlanConf.setBrandImageFile(JSON.toJSONString(request.getBrandImageFile()));
             adPlanConf.setUserId(sysUser.getId());
             adPlanConf.setJobNumber(sysUser.getJobNumber());
             adPlanConf.setCreator(sysUser.getName());
@@ -367,6 +376,8 @@ public class AdPlanConfController {
         try {
             AdPlanConf adPlanConf = new AdPlanConf();
             BeanUtils.copyProperties(request, adPlanConf);
+            adPlanConf.setCreativeFiles(JSON.toJSONString(request.getCreativeFileArr()));
+            adPlanConf.setBrandImageFile(JSON.toJSONString(request.getBrandImageFile()));
             boolean success = adPlanConfService.updateAdPlanConf(adPlanConf);
             if (success) {
                 return Result.success("更新广告计划配置成功");

+ 18 - 0
src/main/java/com/moka/gdtauto/controller/TencentAccountIdBrandImageController.java

@@ -0,0 +1,18 @@
+package com.moka.gdtauto.controller;
+
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <p>
+ * 腾讯账户上传品牌形象记录 前端控制器
+ * </p>
+ *
+ * @author mokamvp
+ * @since 2026-03-11
+ */
+@RestController
+@RequestMapping("/tencentAccountIdBrandImage")
+public class TencentAccountIdBrandImageController {
+
+}

+ 8 - 0
src/main/java/com/moka/gdtauto/controller/vo/CreateAdPlanConfRequest.java

@@ -1,6 +1,9 @@
 package com.moka.gdtauto.controller.vo;
 
 import java.math.BigDecimal;
+import java.util.List;
+
+import com.moka.gdtauto.entity.OssFile;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
@@ -53,12 +56,17 @@ public class CreateAdPlanConfRequest {
     @Schema(description = "落地页IDs,多个用逗号分隔", example = "1,2,3")
     private String pageIds;
 
+    private List<OssFile> creativeFileArr;
+
     @Schema(description = "创意图片/视频文件URL,多个用逗号分隔")
     private String creativeFileUrls;
 
     @Schema(description = "创意图片/视频文件名,多个用逗号分隔")
     private String creativeFileNames;
 
+    @Schema(description = "品牌形象图片文件,单张")
+    private OssFile brandImageFile;
+
     @Schema(description = "品牌形象图片文件URL,单张")
     private String brandImageUrl;
 

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

@@ -1,6 +1,9 @@
 package com.moka.gdtauto.controller.vo;
 
 import java.math.BigDecimal;
+import java.util.List;
+
+import com.moka.gdtauto.entity.OssFile;
 
 import io.swagger.v3.oas.annotations.media.Schema;
 import lombok.Data;
@@ -53,12 +56,17 @@ public class UpdateAdPlanConfRequest {
     @Schema(description = "落地页IDs,多个用逗号分隔", example = "1,2,3")
     private String pageIds;
 
+    private List<OssFile> creativeFileArr;
+
     @Schema(description = "创意图片/视频文件URL,多个用逗号分隔")
     private String creativeFileUrls;
 
     @Schema(description = "创意图片/视频文件名,多个用逗号分隔")
     private String creativeFileNames;
 
+    @Schema(description = "品牌形象图片文件,单张")
+    private OssFile brandImageFile;
+    
     @Schema(description = "品牌形象图片文件URL,单张")
     private String brandImageUrl;
 

+ 4 - 1
src/main/java/com/moka/gdtauto/entity/AdPlanConf.java

@@ -72,8 +72,10 @@ public class AdPlanConf {
      * 短剧ID
      */
     private Long dramaId;
-    
+
     private Integer dramaAmount;
+
+    private String creativeFiles;
     /**
      * 创意图片/视频文件URL(从oss_file表获取,多个用逗号分隔)
      */
@@ -84,6 +86,7 @@ public class AdPlanConf {
      */
     private String creativeFileNames;
 
+    private String brandImageFile;
     /**
      * 品牌形象图片文件URL(从oss_file表获取,单张)
      */

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

@@ -49,15 +49,30 @@ public class AdPlanCreative {
      * 创意名称60个中文字
      */
     private String creativeName;
+    
 
     /**
      * 图片ID
      */
     private Long imageId;
+    private Long videoId;
+    /**
+     * 创意文件
+     */
+    private String creativeFileType;
     private String creativeFileUrl;
+    private String creativeFileMd5;
+    /**
+     * 创意文本
+     */
     private String creativeText;
+
+    /**
+     * 品牌相关
+     */
     private String brandName;
     private String brandImageUrl;
+    private String brandImageFileMd5;
     private Long brandImageId;
     private String buttonText;
 

+ 69 - 0
src/main/java/com/moka/gdtauto/entity/TencentAccountIdBrandImage.java

@@ -0,0 +1,69 @@
+package com.moka.gdtauto.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 腾讯账户上传品牌形象记录
+ * </p>
+ *
+ * @author mokamvp
+ * @since 2026-03-11
+ */
+@Getter
+@Setter
+@ToString
+@TableName("tencent_account_id_brand_image")
+public class TencentAccountIdBrandImage implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 账户ID
+     */
+    private Long accountId;
+
+    /**
+     * 图片URL
+     */
+    private String imageUrl;
+
+    /**
+     * 文件md5
+     */
+    private String fileMd5;
+
+    /**
+     * 图片ID
+     */
+    private Long imageId;
+
+    /**
+     * 图片宽度
+     */
+    private Integer width;
+
+    /**
+     * 图片高度
+     */
+    private Integer height;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+}

+ 11 - 0
src/main/java/com/moka/gdtauto/entity/TencentAccountIdImage.java

@@ -41,6 +41,9 @@ public class TencentAccountIdImage implements Serializable {
      */
     private Long imageId;
 
+    // 文件MD5签名
+    private String fileMd5;
+
     /**
      * 创建时间
      */
@@ -78,6 +81,14 @@ public class TencentAccountIdImage implements Serializable {
         this.imageId = imageId;
     }
 
+    public String getFileMd5() {
+        return fileMd5;
+    }
+
+    public void setFileMd5(String fileMd5) {
+        this.fileMd5 = fileMd5;
+    }
+
     public LocalDateTime getCreateTime() {
         return createTime;
     }

+ 10 - 0
src/main/java/com/moka/gdtauto/entity/TencentAccountIdVideoId.java

@@ -46,6 +46,16 @@ public class TencentAccountIdVideoId implements Serializable {
      */
     private Long coverImageId;
 
+    // 文件MD5签名
+    private String fileMd5;
+    
+    public String getFileMd5() {
+        return fileMd5;
+    }
+    public void setFileMd5(String fileMd5) {
+        this.fileMd5 = fileMd5;
+    }
+
     /**
      * 创建时间
      */

+ 21 - 0
src/main/java/com/moka/gdtauto/mapper/TencentAccountIdBrandImageMapper.java

@@ -0,0 +1,21 @@
+package com.moka.gdtauto.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.moka.gdtauto.entity.TencentAccountIdBrandImage;
+import org.apache.ibatis.annotations.Param;
+
+/**
+ * <p>
+ * 腾讯账户上传品牌形象记录 Mapper 接口
+ * </p>
+ *
+ * @author mokamvp
+ * @since 2026-03-11
+ */
+public interface TencentAccountIdBrandImageMapper extends BaseMapper<TencentAccountIdBrandImage> {
+
+    /**
+     * 插入记录(忽略重复)
+     */
+    void insertIgnore(@Param("entity") TencentAccountIdBrandImage entity);
+}

+ 5 - 0
src/main/java/com/moka/gdtauto/mapper/TencentAccountIdImageMapper.java

@@ -2,6 +2,7 @@ package com.moka.gdtauto.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.moka.gdtauto.entity.TencentAccountIdImage;
+import org.apache.ibatis.annotations.Param;
 
 /**
  * <p>
@@ -13,4 +14,8 @@ import com.moka.gdtauto.entity.TencentAccountIdImage;
  */
 public interface TencentAccountIdImageMapper extends BaseMapper<TencentAccountIdImage> {
 
+    /**
+     * 插入记录(忽略重复)
+     */
+    void insertIgnore(@Param("entity") TencentAccountIdImage entity);
 }

+ 5 - 0
src/main/java/com/moka/gdtauto/mapper/TencentAccountIdVideoIdMapper.java

@@ -2,6 +2,7 @@ package com.moka.gdtauto.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.moka.gdtauto.entity.TencentAccountIdVideoId;
+import org.apache.ibatis.annotations.Param;
 
 /**
  * <p>
@@ -13,4 +14,8 @@ import com.moka.gdtauto.entity.TencentAccountIdVideoId;
  */
 public interface TencentAccountIdVideoIdMapper extends BaseMapper<TencentAccountIdVideoId> {
 
+    /**
+     * 插入记录(忽略重复)
+     */
+    void insertIgnore(@Param("entity") TencentAccountIdVideoId entity);
 }

+ 36 - 0
src/main/java/com/moka/gdtauto/service/TencentAccountIdBrandImageService.java

@@ -0,0 +1,36 @@
+package com.moka.gdtauto.service;
+
+import com.moka.gdtauto.entity.TencentAccountIdBrandImage;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * 腾讯账户上传品牌形象记录 服务类
+ * </p>
+ *
+ * @author mokamvp
+ * @since 2026-03-11
+ */
+public interface TencentAccountIdBrandImageService extends IService<TencentAccountIdBrandImage> {
+
+    /**
+     * 根据账户ID和文件MD5查询品牌形象图片ID
+     *
+     * @param accountId 账户ID
+     * @param fileMd5   文件MD5
+     * @return 品牌形象图片ID,不存在返回null
+     */
+    Long getBrandImageIdByAccountIdAndMd5(Long accountId, String fileMd5);
+
+    /**
+     * 保存品牌形象图片ID记录
+     *
+     * @param accountId 账户ID
+     * @param fileMd5   文件MD5
+     * @param imageId   图片ID
+     * @param imageUrl  图片URL
+     * @param width     图片宽度
+     * @param height    图片高度
+     */
+    void saveBrandImageId(Long accountId, String fileMd5, Long imageId, String imageUrl, Long width, Long height);
+}

+ 18 - 0
src/main/java/com/moka/gdtauto/service/TencentAccountIdImageService.java

@@ -13,4 +13,22 @@ import com.baomidou.mybatisplus.extension.service.IService;
  */
 public interface TencentAccountIdImageService extends IService<TencentAccountIdImage> {
 
+    /**
+     * 根据账户ID和文件MD5查询图片ID
+     *
+     * @param accountId 账户ID
+     * @param fileMd5   文件MD5
+     * @return 图片ID,不存在返回null
+     */
+    Long getImageIdByAccountIdAndMd5(Long accountId, String fileMd5);
+
+    /**
+     * 保存图片ID记录
+     *
+     * @param accountId 账户ID
+     * @param fileMd5   文件MD5
+     * @param imageId   图片ID
+     * @param imageUrl  图片URL
+     */
+    void saveImageId(Long accountId, String fileMd5, Long imageId, String imageUrl);
 }

+ 19 - 0
src/main/java/com/moka/gdtauto/service/TencentAccountIdVideoIdService.java

@@ -13,4 +13,23 @@ import com.baomidou.mybatisplus.extension.service.IService;
  */
 public interface TencentAccountIdVideoIdService extends IService<TencentAccountIdVideoId> {
 
+    /**
+     * 根据账户ID和文件MD5查询视频ID
+     *
+     * @param accountId 账户ID
+     * @param fileMd5   文件MD5
+     * @return 视频ID,不存在返回null
+     */
+    Long getVideoIdByAccountIdAndMd5(Long accountId, String fileMd5);
+
+    /**
+     * 保存视频ID记录
+     *
+     * @param accountId     账户ID
+     * @param fileMd5       文件MD5
+     * @param videoId       视频ID
+     * @param videoUrl      视频URL
+     * @param coverImageId  封面图片ID
+     */
+    void saveVideoId(Long accountId, String fileMd5, Long videoId, String videoUrl, Long coverImageId);
 }

+ 17 - 0
src/main/java/com/moka/gdtauto/service/TencentAdsAdgroupService.java

@@ -260,6 +260,23 @@ public class TencentAdsAdgroupService extends ServiceImpl<TencentAdsAdgroupMappe
         
         // 获取TencentAds实例
         TencentAds tencentAds = clientFactory.getTencentAds(jobNumber, orgAccountId);
+        return createAdgroup(tencentAds, request);
+    }
+
+    /**
+     * 创建广告(复用TencentAds实例)
+     * 
+     * @param tencentAds TencentAds实例
+     * @param request 创建广告请求参数
+     * @return 创建成功的广告信息
+     * @throws TencentAdsResponseException API响应异常
+     * @throws TencentAdsSDKException SDK异常
+     * @throws Exception 其他异常
+     */
+    public AdgroupsAddResponseData createAdgroup(TencentAds tencentAds, AdgroupsAddRequest request) 
+            throws TencentAdsResponseException, TencentAdsSDKException, Exception {
+        log.info("开始创建广告,adgroupName={}", request.getAdgroupName());
+        
         tencentAds.setDebug(true);
         // 调用SDK创建广告
         AdgroupsAddResponseData response = tencentAds.adgroups()

+ 352 - 53
src/main/java/com/moka/gdtauto/service/impl/AdPlanConfServiceImpl.java

@@ -27,6 +27,9 @@ import java.util.ArrayList;
 import com.moka.gdtauto.entity.AdPlan;
 import com.moka.gdtauto.entity.AdPlanCreative;
 import com.moka.gdtauto.service.AdPlanService;
+import com.moka.gdtauto.service.TencentAccountIdBrandImageService;
+import com.moka.gdtauto.service.TencentAccountIdImageService;
+import com.moka.gdtauto.service.TencentAccountIdVideoIdService;
 
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.annotation.Async;
@@ -39,6 +42,7 @@ import com.moka.gdtauto.common.AdPlanStatus;
 
 import java.io.File;
 import java.io.FileOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
 import java.math.BigInteger;
 import java.net.HttpURLConnection;
@@ -68,6 +72,12 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
     private TencentAdsApiClientFactory clientFactory;
     @Autowired
     private TencentAccountService tencentAccountService;
+    @Autowired
+    private TencentAccountIdImageService tencentAccountIdImageService;
+    @Autowired
+    private TencentAccountIdVideoIdService tencentAccountIdVideoIdService;
+    @Autowired
+    private TencentAccountIdBrandImageService tencentAccountIdBrandImageService;
 
     @Async
     @Override
@@ -487,6 +497,31 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
 
         String jobNumber = conf.getJobNumber();
 
+        // 创建 TencentAds 实例,整个流程复用同一个实例
+        TencentAds tencentAds;
+        try {
+            tencentAds = clientFactory.getTencentAds(jobNumber, Constant.MAIN_ORG_ACCOUNT_ID);
+        } catch (Exception e) {
+            log.error("创建TencentAds实例失败,confId={}", confId, e);
+            conf.setStatus(AdPlanStatus.FAILED);
+            conf.setFailReason("创建TencentAds实例失败: " + truncateError(e.getMessage()));
+            this.updateById(conf);
+            return;
+        }
+
+        // 第一步:准备阶段 - 填充创意的 imageId/videoId
+        try {
+            prepareCreativeAssets(tencentAds, plans);
+        } catch (Exception e) {
+            log.error("准备创意资产失败,confId={}", confId, e);
+            // 更新 AdPlanConf 状态为失败
+            conf.setStatus(AdPlanStatus.FAILED);
+            conf.setFailReason("准备创意资产失败: " + truncateError(e.getMessage()));
+            this.updateById(conf);
+            return;
+        }
+
+        // 第二步:创建广告组与创意
         // 遍历处理每个 AdPlan
         for (AdPlan plan : plans) {
             try {
@@ -494,7 +529,7 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
 
                 if (plan.getAdgroupId() > 0 || plan.getStatus() == AdPlanStatus.SUCCESS) {
                     // 创建动态创意
-                    createDynamicCreatives(plan, jobNumber, Constant.MAIN_ORG_ACCOUNT_ID);
+                    createDynamicCreatives(tencentAds, plan);
                     plan.setFailReason("");
                     plan.setStatus(AdPlanStatus.SUCCESS);
                     plan.setSuccessTime(LocalDateTime.now());
@@ -513,7 +548,7 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
                 log.info("调用腾讯API创建广告组,accountId={}, orgAccountId={}", plan.getAccountId(),
                         Constant.MAIN_ORG_ACCOUNT_ID);
                 AdgroupsAddResponseData response = tencentAdsAdgroupService.createAdgroup(
-                        jobNumber, Constant.MAIN_ORG_ACCOUNT_ID, request);
+                        tencentAds, request);
 
                 // 更新 adgroupId
                 plan.setAdgroupId(response.getAdgroupId());
@@ -527,7 +562,7 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
 
                 // 创建动态创意
                 if (plan.getAdgroupId() > 0) {
-                    createDynamicCreatives(plan, jobNumber, Constant.MAIN_ORG_ACCOUNT_ID);
+                    createDynamicCreatives(tencentAds, plan);
                 }
 
                 log.info("广告计划处理成功,planId={}, adgroupId={}", plan.getId(), plan.getAdgroupId());
@@ -554,6 +589,146 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
     }
 
     /**
+     * 准备创意资产 - 填充创意的 imageId/videoId
+     * 按 accountId 分组,先查询缓存/数据库,不存在则上传并保存
+     */
+    private void prepareCreativeAssets(TencentAds tencentAds, List<AdPlan> plans) throws Exception {
+        log.info("准备创意资产开始,计划数量: {}", plans.size());
+
+        // 按 accountId 分组
+        Map<Long, List<AdPlan>> plansByAccountId = plans.stream()
+                .collect(Collectors.groupingBy(AdPlan::getAccountId));
+
+        for (Map.Entry<Long, List<AdPlan>> entry : plansByAccountId.entrySet()) {
+            Long accountId = entry.getKey();
+            List<AdPlan> accountPlans = entry.getValue();
+
+            log.info("处理账户 {} 的创意资产,计划数量: {}", accountId, accountPlans.size());
+
+            // 收集该账户下所有创意
+            List<AdPlanCreative> allCreatives = accountPlans.stream()
+                    .flatMap(plan -> plan.getAdCreatives().stream())
+                    .collect(Collectors.toList());
+
+            for (AdPlanCreative creative : allCreatives) {
+                prepareSingleCreativeAsset(tencentAds, accountId, creative);
+            }
+        }
+
+        log.info("准备创意资产完成");
+    }
+
+    /**
+     * 准备单个创意的资产
+     */
+    private void prepareSingleCreativeAsset(TencentAds tencentAds, Long accountId, AdPlanCreative creative) throws Exception {
+        String creativeFileType = creative.getCreativeFileType();
+        String creativeFileUrl = creative.getCreativeFileUrl();
+        String creativeFileMd5 = creative.getCreativeFileMd5();
+        Long creativeImageId = creative.getImageId();
+        Long creativeVideoId = creative.getVideoId();
+        
+
+        // 跳过无效数据
+        if (creativeFileType == null || creativeFileType.isEmpty()
+                || creativeFileUrl == null || creativeFileUrl.isEmpty()
+                || creativeFileMd5 == null || creativeFileMd5.isEmpty()) {
+            return;
+        }
+
+        if ("image".equalsIgnoreCase(creativeFileType) && creativeImageId != null && creativeImageId > 0) {
+            return;
+        } else if ("video".equalsIgnoreCase(creativeFileType) && creativeVideoId != null && creativeVideoId > 0) {
+            return;
+        }
+
+        if ("image".equalsIgnoreCase(creativeFileType)) {
+            // 图片类型:查询或上传图片
+            if (creative.getImageId() != null && creative.getImageId() > 0) {
+                return;
+            }
+            Long imageId = tencentAccountIdImageService.getImageIdByAccountIdAndMd5(accountId, creativeFileMd5);
+            if (imageId != null) {
+                log.info("从缓存/数据库获取图片ID,accountId={}, md5={}, imageId={}", accountId, creativeFileMd5, imageId);
+                creative.setImageId(imageId);
+            } else {
+                // 上传图片
+                log.info("上传创意图片,accountId={}, URL={}", accountId, creativeFileUrl);
+                imageId = uploadImageFromUrl(tencentAds, accountId, creativeFileUrl, creativeFileMd5,
+                        "创意图片-" + creative.getCreativeName());
+                creative.setImageId(imageId);
+                // 保存到数据库
+                tencentAccountIdImageService.saveImageId(accountId, creativeFileMd5, imageId, creativeFileUrl);
+                log.info("图片上传并保存成功,accountId={}, md5={}, imageId={}", accountId, creativeFileMd5, imageId);
+            }
+            adPlanCreativeService.updateById(creative);
+
+        } else if ("video".equalsIgnoreCase(creativeFileType)) {
+            // 视频类型:查询或上传视频
+            if (creative.getVideoId() != null && creative.getVideoId() > 0) {
+                return;
+            }
+            Long videoId = tencentAccountIdVideoIdService.getVideoIdByAccountIdAndMd5(accountId, creativeFileMd5);
+            if (videoId != null) {
+                log.info("从缓存/数据库获取视频ID,accountId={}, md5={}, videoId={}", accountId, creativeFileMd5, videoId);
+                creative.setVideoId(videoId);
+            } else {
+                // 上传视频
+                log.info("上传创意视频,accountId={}, URL={}", accountId, creativeFileUrl);
+                VideoUploadResult videoResult = uploadVideoFromUrl(tencentAds, accountId, creativeFileUrl, creativeFileMd5,
+                        "创意视频-" + creative.getCreativeName());
+                creative.setVideoId(videoResult.videoId());
+                // 保存到数据库
+                tencentAccountIdVideoIdService.saveVideoId(accountId, creativeFileMd5, videoResult.videoId(),
+                        creativeFileUrl, videoResult.coverImageId());
+                log.info("视频上传并保存成功,accountId={}, md5={}, videoId={}", accountId, creativeFileMd5, videoResult.videoId());
+            }
+            adPlanCreativeService.updateById(creative);
+        }
+
+        // 处理品牌形象图片
+        prepareBrandImageAsset(tencentAds, accountId, creative);
+    }
+
+    /**
+     * 准备品牌形象图片资产
+     */
+    private void prepareBrandImageAsset(TencentAds tencentAds, Long accountId, AdPlanCreative creative) throws Exception {
+        String brandImageUrl = creative.getBrandImageUrl();
+        String brandImageFileMd5 = creative.getBrandImageFileMd5();
+        String brandName = creative.getBrandName();
+
+        // 跳过无效数据
+        if (brandImageUrl == null || brandImageUrl.isEmpty()
+                || brandImageFileMd5 == null || brandImageFileMd5.isEmpty()
+                || brandName == null || brandName.isEmpty()) {
+            return;
+        }
+
+        // 已经有 brandImageId 则跳过
+        if (creative.getBrandImageId() != null && creative.getBrandImageId() > 0) {
+            return;
+        }
+
+        // 查询缓存/数据库
+        Long brandImageId = tencentAccountIdBrandImageService.getBrandImageIdByAccountIdAndMd5(accountId, brandImageFileMd5);
+        if (brandImageId != null) {
+            log.info("从缓存/数据库获取品牌形象ID,accountId={}, md5={}, brandImageId={}", accountId, brandImageFileMd5, brandImageId);
+            creative.setBrandImageId(brandImageId);
+        } else {
+            // 上传品牌形象
+            log.info("上传品牌形象,accountId={}, URL={}, brandName={}", accountId, brandImageUrl, brandName);
+            BrandUploadResult brandResult = uploadBrandImageFromUrl(tencentAds, accountId, brandImageUrl, brandImageFileMd5, brandName);
+            creative.setBrandImageId(brandResult.imageId());
+            // 保存到数据库
+            tencentAccountIdBrandImageService.saveBrandImageId(accountId, brandImageFileMd5, brandResult.imageId(),
+                    brandImageUrl, brandResult.width(), brandResult.height());
+            log.info("品牌形象上传并保存成功,accountId={}, md5={}, brandImageId={}", accountId, brandImageFileMd5, brandResult.imageId());
+        }
+        adPlanCreativeService.updateById(creative);
+    }
+
+    /**
      * 构建广告组请求
      */
     private AdgroupsAddRequest buildAdgroupRequest(AdPlan plan) {
@@ -588,11 +763,9 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
     /**
      * 创建动态创意
      */
-    private void createDynamicCreatives(AdPlan plan, String jobNumber, Long orgAccountId)
+    private void createDynamicCreatives(TencentAds tencentAds, AdPlan plan)
             throws TencentAdsResponseException, Exception {
 
-        TencentAds tencentAds = clientFactory.getTencentAds(jobNumber, orgAccountId);
-
         for (AdPlanCreative creative : plan.getAdCreatives()) {
             try {
                 log.info("开始创建动态创意,planId={}, creativeName={}", plan.getId(), creative.getCreativeName());
@@ -605,44 +778,6 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
                 creative.setAdgroupId(plan.getAdgroupId());
                 adPlanCreativeService.updateById(creative);
 
-                // 上传创意文件的到imageId
-                if (creative.getImageId() == 0) {
-                    String creativeFileUrl = creative.getCreativeFileUrl();
-                    if (creativeFileUrl != null && !creativeFileUrl.isEmpty()) {
-                        try {
-                            log.info("开始上传创意图片,URL: {}", creativeFileUrl);
-                            Long imageId = uploadImageFromUrl(tencentAds, plan.getAccountId(), creativeFileUrl,
-                                    "创意图片-" + creative.getCreativeName());
-                            creative.setImageId(imageId);
-                            adPlanCreativeService.updateById(creative);
-                            log.info("创意图片上传成功,imageId: {}", imageId);
-                        } catch (Exception e) {
-                            log.error("创意图片上传失败", e);
-                            throw new RuntimeException("创意图片上传失败: " + e.getMessage(), e);
-                        }
-                    }
-                }
-
-                if (creative.getBrandImageId() == 0) {
-                    // 上传品牌形象
-                    String brandImageUrl = creative.getBrandImageUrl();
-                    String brandName = creative.getBrandName();
-                    if (brandImageUrl != null && !brandImageUrl.isEmpty()
-                            && brandName != null && !brandName.isEmpty()) {
-                        try {
-                            log.info("开始上传品牌形象,URL: {}, 品牌名称: {}", brandImageUrl, brandName);
-                            Long brandImageId = uploadBrandFromUrl(tencentAds, plan.getAccountId(), brandImageUrl,
-                                    brandName);
-                            creative.setBrandImageId(brandImageId);
-                            adPlanCreativeService.updateById(creative);
-                            log.info("品牌形象上传成功,brandImageId: {}", brandImageId);
-                        } catch (Exception e) {
-                            log.error("品牌形象上传失败", e);
-                            throw new RuntimeException("品牌形象上传失败: " + e.getMessage(), e);
-                        }
-                    }
-                }
-
                 // 构建动态创意请求
                 DynamicCreativesAddRequest request = buildDynamicCreativeRequest(plan, creative);
 
@@ -663,7 +798,6 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
                 creative.setStatus(AdPlanStatus.FAILED);
                 creative.setFailReason(truncateError(e.getMessage()));
                 adPlanCreativeService.updateById(creative);
-                throw e; // 创意失败则整个广告组失败
             }
         }
     }
@@ -688,7 +822,14 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
         components.setDescription(buildDescriptionComponents(creative.getCreativeText()));
 
         // 图片组件 (从 creative.imageId)
-        components.setImage(buildImageComponents(creative.getImageId()));
+        if (creative.getImageId() > 0) {
+             components.setImage(buildImageComponents(creative.getImageId()));
+        }
+
+        if (creative.getVideoId() > 0) {
+             components.setVideo(buildVideoComponents(creative.getVideoId()));
+        }
+       
 
         // 品牌形象 (固定品牌)
         components.setBrand(buildBrandComponents(creative.getBrandImageId(), creative.getBrandName()));
@@ -703,6 +844,16 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
         return request;
     }
 
+    private List<VideoComponent> buildVideoComponents(Long videoId) {
+        List<VideoComponent> videos = new ArrayList<>();
+        VideoComponent video = new VideoComponent();
+        VideoStruct videoValue = new VideoStruct();
+        videoValue.setVideoId(String.valueOf(videoId));
+        video.setValue(videoValue);
+        videos.add(video);
+        return videos;
+    }
+
     /**
      * 构建标题组件
      */
@@ -904,19 +1055,33 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
      * @return 图片ID
      * @throws Exception 上传失败
      */
-    private Long uploadImageFromUrl(TencentAds tencentAds, Long accountId, String imageUrl, String description)
+    private Long uploadImageFromUrl(TencentAds tencentAds, Long accountId, String imageUrl, String fileMd5, String description)
             throws Exception {
-        File tempFile = null;
+        HttpURLConnection connection = null;
+        InputStream inputStream = null;
         try {
-            // 1. 从URL下载图片到临时文件
-            tempFile = downloadFileFromUrl(imageUrl);
+            // 1. 从URL直接读取图片字节流
+            URL url = new URL(imageUrl);
+            connection = (HttpURLConnection) url.openConnection();
+            connection.setConnectTimeout(10000);
+            connection.setReadTimeout(30000);
+            connection.setRequestMethod("GET");
+            connection.connect();
 
-            // 2. 读取文件并转Base64
-            byte[] imageBytes = Files.readAllBytes(tempFile.toPath());
+            int responseCode = connection.getResponseCode();
+            if (responseCode != HttpURLConnection.HTTP_OK) {
+                throw new Exception("下载图片失败,HTTP状态码: " + responseCode);
+            }
+
+            // 2. 读取输入流并转Base64
+            inputStream = connection.getInputStream();
+            byte[] imageBytes = inputStream.readAllBytes();
             String base64Image = Base64.getEncoder().encodeToString(imageBytes);
 
+            log.info("图片下载成功,URL: {}, 大小: {} bytes", imageUrl, imageBytes.length);
+
             // 3. 计算MD5签名
-            String signature = calculateMD5(imageBytes);
+            String signature = fileMd5;
 
             // 4. 调用腾讯广告API上传图片
             ImagesAddResponseData response = tencentAds.images()
@@ -944,6 +1109,67 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
             return Long.parseLong(response.getImageId());
 
         } finally {
+            if (inputStream != null) {
+                try {
+                    inputStream.close();
+                } catch (IOException e) {
+                    log.warn("关闭输入流失败", e);
+                }
+            }
+            if (connection != null) {
+                connection.disconnect();
+            }
+        }
+    }
+
+    /**
+     * 视频上传结果记录
+     */
+    private record VideoUploadResult(Long videoId, Long coverImageId) {}
+
+    /**
+     * 品牌形象上传结果记录
+     */
+    private record BrandUploadResult(Long imageId, Long width, Long height) {}
+
+    private VideoUploadResult uploadVideoFromUrl(TencentAds tencentAds, Long accountId, String videoUrl, String fileMd5, String description)
+            throws Exception {
+        File tempFile = null;
+        try {
+            // 1. 从URL下载视频到临时文件
+            tempFile = downloadFileFromUrl(videoUrl);
+
+            // 2. 检查文件大小(最大100MB)
+            long fileSizeInMB = tempFile.length() / (1024 * 1024);
+            if (fileSizeInMB > 100) {
+                throw new Exception("视频文件过大: " + fileSizeInMB + "MB,超过 100MB 限制");
+            }
+
+            // 3. 计算视频MD5签名
+            String signature = fileMd5;
+            log.info("视频MD5签名: {}, 文件大小: {}MB", signature, fileSizeInMB);
+
+            // 4. 调用腾讯广告API上传视频
+            VideosAddResponseData response = tencentAds.videos()
+                    .videosAdd(
+                            tempFile,           // videoFile
+                            signature,          // signature
+                            accountId,          // accountId
+                            null,               // organizationId
+                            description,        // description
+                            null                // adcreativeTemplateId
+                    );
+
+            if (response == null || response.getVideoId() == null) {
+                throw new Exception("视频上传返回结果为空");
+            }
+
+            log.info("视频上传成功,videoId: {}, 封面图片ID: {}",
+                    response.getVideoId(), response.getCoverImageId());
+
+            return new VideoUploadResult(response.getVideoId(), response.getCoverImageId());
+
+        } finally {
             // 5. 删除临时文件
             if (tempFile != null && tempFile.exists()) {
                 tempFile.delete();
@@ -952,6 +1178,32 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
     }
 
     /**
+     * 计算文件的MD5签名
+     *
+     * @param file 文件
+     * @return MD5签名(32位小写字符串)
+     * @throws Exception 计算失败
+     */
+    private String calculateFileMD5(File file) throws Exception {
+        MessageDigest md = MessageDigest.getInstance("MD5");
+        try (java.io.FileInputStream fis = new java.io.FileInputStream(file)) {
+            byte[] buffer = new byte[8192];
+            int bytesRead;
+            while ((bytesRead = fis.read(buffer)) != -1) {
+                md.update(buffer, 0, bytesRead);
+            }
+        }
+        byte[] digest = md.digest();
+        BigInteger bigInt = new BigInteger(1, digest);
+        String hashText = bigInt.toString(16);
+        // 补齐前导0
+        while (hashText.length() < 32) {
+            hashText = "0" + hashText;
+        }
+        return hashText;
+    }
+
+    /**
      * 从URL下载品牌形象并上传到腾讯广告
      *
      * @param tencentAds    腾讯广告SDK实例
@@ -994,6 +1246,53 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
     }
 
     /**
+     * 从URL下载品牌形象并上传到腾讯广告(返回完整信息)
+     *
+     * @param tencentAds    腾讯广告SDK实例
+     * @param accountId     账户ID
+     * @param brandImageUrl 品牌形象图片URL
+     * @param fileMd5       文件MD5
+     * @param brandName     品牌名称
+     * @return 品牌形象上传结果
+     * @throws Exception 上传失败
+     */
+    private BrandUploadResult uploadBrandImageFromUrl(TencentAds tencentAds, Long accountId, String brandImageUrl, String fileMd5, String brandName)
+            throws Exception {
+        File tempFile = null;
+        try {
+            // 1. 从URL下载图片到临时文件
+            tempFile = downloadFileFromUrl(brandImageUrl);
+
+            // 2. 调用腾讯广告API创建品牌形象
+            BrandAddResponseData response = tencentAds.brand()
+                    .brandAdd(
+                            accountId, // accountId
+                            brandName, // name
+                            tempFile // brandImageFile
+                    );
+
+            if (response == null || response.getImageId() == null) {
+                throw new Exception("品牌形象上传返回结果为空");
+            }
+
+            log.info("品牌形象上传成功,imageId: {}, 品牌名称: {}, 宽度: {}px, 高度: {}px",
+                    response.getImageId(), response.getName(), response.getWidth(), response.getHeight());
+
+            return new BrandUploadResult(
+                    Long.parseLong(response.getImageId()),
+                    response.getWidth(),
+                    response.getHeight()
+            );
+
+        } finally {
+            // 3. 删除临时文件
+            if (tempFile != null && tempFile.exists()) {
+                tempFile.delete();
+            }
+        }
+    }
+
+    /**
      * 从URL下载文件到临时文件
      *
      * @param fileUrl 文件URL

+ 3 - 1
src/main/java/com/moka/gdtauto/service/impl/OssFileServiceImpl.java

@@ -1,5 +1,6 @@
 package com.moka.gdtauto.service.impl;
 
+import com.aliyun.oss.common.utils.BinaryUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
@@ -36,7 +37,7 @@ public class OssFileServiceImpl extends ServiceImpl<OssFileMapper, OssFile> impl
             if (businessType != null && businessType == 2 && !"image".equals(fileType)) {
                 throw new RuntimeException("品牌形象只能上传图片文件");
             }
-            
+            String md5 = BinaryUtil.toBase64String(BinaryUtil.calculateMd5(file.getBytes()));
             // 上传到OSS
             String fileUrl = ossUtil.uploadFile(file, folder);
             
@@ -58,6 +59,7 @@ public class OssFileServiceImpl extends ServiceImpl<OssFileMapper, OssFile> impl
             ossFile.setBusinessType(businessType);
             ossFile.setFileSize(file.getSize());
             ossFile.setFileUrl(fileUrl);
+            ossFile.setFileMd5(md5);
             ossFile.setObjectKey(objectKey);
             ossFile.setFolder(folder);
             ossFile.setFileExtension(fileExtension);

+ 50 - 0
src/main/java/com/moka/gdtauto/service/impl/TencentAccountIdBrandImageServiceImp.java

@@ -0,0 +1,50 @@
+package com.moka.gdtauto.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.moka.gdtauto.entity.TencentAccountIdBrandImage;
+import com.moka.gdtauto.mapper.TencentAccountIdBrandImageMapper;
+import com.moka.gdtauto.service.TencentAccountIdBrandImageService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 腾讯账户上传品牌形象记录 服务实现类
+ * </p>
+ *
+ * @author mokamvp
+ * @since 2026-03-11
+ */
+@Service
+public class TencentAccountIdBrandImageServiceImp extends ServiceImpl<TencentAccountIdBrandImageMapper, TencentAccountIdBrandImage> implements TencentAccountIdBrandImageService {
+
+    private static final String CACHE_NAME = "tencentBrandImageId";
+
+    @Override
+    @Cacheable(value = CACHE_NAME, key = "#accountId + ':' + #fileMd5")
+    public Long getBrandImageIdByAccountIdAndMd5(Long accountId, String fileMd5) {
+        LambdaQueryWrapper<TencentAccountIdBrandImage> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(TencentAccountIdBrandImage::getAccountId, accountId)
+                .eq(TencentAccountIdBrandImage::getFileMd5, fileMd5);
+        TencentAccountIdBrandImage record = this.getOne(queryWrapper);
+        return record != null ? record.getImageId() : null;
+    }
+
+    @Override
+    @CacheEvict(value = CACHE_NAME, key = "#accountId + ':' + #fileMd5")
+    public void saveBrandImageId(Long accountId, String fileMd5, Long imageId, String imageUrl, Long width, Long height) {
+        TencentAccountIdBrandImage record = new TencentAccountIdBrandImage();
+        record.setAccountId(accountId);
+        record.setFileMd5(fileMd5);
+        record.setImageId(imageId);
+        record.setImageUrl(imageUrl);
+        record.setWidth(width != null ? width.intValue() : null);
+        record.setHeight(height != null ? height.intValue() : null);
+        record.setCreateTime(LocalDateTime.now());
+        this.baseMapper.insertIgnore(record);
+    }
+}

+ 28 - 0
src/main/java/com/moka/gdtauto/service/impl/TencentAccountIdImageServiceImp.java

@@ -1,11 +1,16 @@
 package com.moka.gdtauto.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.moka.gdtauto.entity.TencentAccountIdImage;
 import com.moka.gdtauto.mapper.TencentAccountIdImageMapper;
 import com.moka.gdtauto.service.TencentAccountIdImageService;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 
+import java.time.LocalDateTime;
+
 /**
  * <p>
  * 腾讯账户上传图片记录 服务实现类
@@ -17,4 +22,27 @@ import org.springframework.stereotype.Service;
 @Service
 public class TencentAccountIdImageServiceImp extends ServiceImpl<TencentAccountIdImageMapper, TencentAccountIdImage> implements TencentAccountIdImageService {
 
+    private static final String CACHE_NAME = "tencentImageId";
+
+    @Override
+    @Cacheable(value = CACHE_NAME, key = "#accountId + ':' + #fileMd5")
+    public Long getImageIdByAccountIdAndMd5(Long accountId, String fileMd5) {
+        LambdaQueryWrapper<TencentAccountIdImage> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(TencentAccountIdImage::getAccountId, accountId)
+                .eq(TencentAccountIdImage::getFileMd5, fileMd5);
+        TencentAccountIdImage record = this.getOne(queryWrapper);
+        return record != null ? record.getImageId() : null;
+    }
+
+    @Override
+    @CacheEvict(value = CACHE_NAME, key = "#accountId + ':' + #fileMd5")
+    public void saveImageId(Long accountId, String fileMd5, Long imageId, String imageUrl) {
+        TencentAccountIdImage record = new TencentAccountIdImage();
+        record.setAccountId(accountId);
+        record.setFileMd5(fileMd5);
+        record.setImageId(imageId);
+        record.setImageUrl(imageUrl);
+        record.setCreateTime(LocalDateTime.now());
+        this.baseMapper.insertIgnore(record);
+    }
 }

+ 29 - 0
src/main/java/com/moka/gdtauto/service/impl/TencentAccountIdVideoIdServiceImp.java

@@ -1,11 +1,16 @@
 package com.moka.gdtauto.service.impl;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.moka.gdtauto.entity.TencentAccountIdVideoId;
 import com.moka.gdtauto.mapper.TencentAccountIdVideoIdMapper;
 import com.moka.gdtauto.service.TencentAccountIdVideoIdService;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.cache.annotation.CacheEvict;
+import org.springframework.cache.annotation.Cacheable;
 import org.springframework.stereotype.Service;
 
+import java.time.LocalDateTime;
+
 /**
  * <p>
  * 腾讯账户上传视频记录 服务实现类
@@ -17,4 +22,28 @@ import org.springframework.stereotype.Service;
 @Service
 public class TencentAccountIdVideoIdServiceImp extends ServiceImpl<TencentAccountIdVideoIdMapper, TencentAccountIdVideoId> implements TencentAccountIdVideoIdService {
 
+    private static final String CACHE_NAME = "tencentVideoId";
+
+    @Override
+    @Cacheable(value = CACHE_NAME, key = "#accountId + ':' + #fileMd5")
+    public Long getVideoIdByAccountIdAndMd5(Long accountId, String fileMd5) {
+        LambdaQueryWrapper<TencentAccountIdVideoId> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(TencentAccountIdVideoId::getAccountId, accountId)
+                .eq(TencentAccountIdVideoId::getFileMd5, fileMd5);
+        TencentAccountIdVideoId record = this.getOne(queryWrapper);
+        return record != null ? record.getVideoId() : null;
+    }
+
+    @Override
+    @CacheEvict(value = CACHE_NAME, key = "#accountId + ':' + #fileMd5")
+    public void saveVideoId(Long accountId, String fileMd5, Long videoId, String videoUrl, Long coverImageId) {
+        TencentAccountIdVideoId record = new TencentAccountIdVideoId();
+        record.setAccountId(accountId);
+        record.setFileMd5(fileMd5);
+        record.setVideoId(videoId);
+        record.setVideoUrl(videoUrl);
+        record.setCoverImageId(coverImageId);
+        record.setCreateTime(LocalDateTime.now());
+        this.baseMapper.insertIgnore(record);
+    }
 }

+ 34 - 0
src/main/java/com/moka/gdtauto/util/TencentFileMd5Util.java

@@ -0,0 +1,34 @@
+package com.moka.gdtauto.util;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.InputStream;
+import java.math.BigInteger;
+import java.security.MessageDigest;
+
+public class TencentFileMd5Util {
+
+
+    public static String calculateMD5(File file) throws Exception {
+        MessageDigest md = MessageDigest.getInstance("MD5");
+        return calculateMd5(new FileInputStream(file));
+    }
+
+    public static String calculateMd5(InputStream in) throws Exception {
+        MessageDigest md = MessageDigest.getInstance("MD5");
+        byte[] buffer = new byte[8192];
+        int bytesRead;
+        while ((bytesRead = in.read(buffer)) != -1) {
+            md.update(buffer, 0, bytesRead);
+        }
+        byte[] digest = md.digest();
+        BigInteger bigInt = new BigInteger(1, digest);
+        String hashtext = bigInt.toString(16);
+        // 补齐前导0
+        while (hashtext.length() < 32) {
+            hashtext = "0" + hashtext;
+        }
+        return hashtext;
+    }
+
+}

+ 10 - 0
src/main/resources/mapper/TencentAccountIdBrandImageMapper.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.moka.gdtauto.mapper.TencentAccountIdBrandImageMapper">
+
+    <insert id="insertIgnore">
+        INSERT IGNORE INTO tencent_account_id_brand_image (account_id, image_url, file_md5, image_id, width, height)
+        VALUES (#{entity.accountId}, #{entity.imageUrl}, #{entity.fileMd5}, #{entity.imageId}, #{entity.width}, #{entity.height})
+    </insert>
+
+</mapper>

+ 5 - 0
src/main/resources/mapper/TencentAccountIdImageMapper.xml

@@ -2,4 +2,9 @@
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.moka.gdtauto.mapper.TencentAccountIdImageMapper">
 
+    <insert id="insertIgnore">
+        INSERT IGNORE INTO tencent_account_id_image (account_id, image_url, image_id, file_md5)
+        VALUES (#{entity.accountId}, #{entity.imageUrl}, #{entity.imageId}, #{entity.fileMd5})
+    </insert>
+
 </mapper>

+ 5 - 0
src/main/resources/mapper/TencentAccountIdVideoIdMapper.xml

@@ -2,4 +2,9 @@
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.moka.gdtauto.mapper.TencentAccountIdVideoIdMapper">
 
+    <insert id="insertIgnore">
+        INSERT IGNORE INTO tencent_account_id_video_id (account_id, video_url, video_id, cover_image_id, file_md5)
+        VALUES (#{entity.accountId}, #{entity.videoUrl}, #{entity.videoId}, #{entity.coverImageId}, #{entity.fileMd5})
+    </insert>
+
 </mapper>

+ 2 - 1
src/test/java/com/moka/gdtauto/service/OssFileMd5.java

@@ -3,6 +3,7 @@ package com.moka.gdtauto.service;
 import com.aliyun.oss.common.utils.BinaryUtil;
 import com.moka.gdtauto.GdtAutoApplication;
 import com.moka.gdtauto.entity.OssFile;
+import com.moka.gdtauto.util.TencentFileMd5Util;
 
 import org.springframework.beans.factory.annotation.Autowired;
 
@@ -43,7 +44,7 @@ public class OssFileMd5 {
                 }
 
                 try (java.io.InputStream inputStream = connection.getInputStream()) {
-                    String md5 = BinaryUtil.toBase64String(BinaryUtil.calculateMd5(inputStream.readAllBytes()));
+                    String md5 = TencentFileMd5Util.calculateMd5(inputStream);
                     ossFileService.lambdaUpdate()
                         .eq(OssFile::getId, ossFile.getId())
                         .set(OssFile::getFileMd5, md5).update();

Bu fark içinde çok fazla dosya değişikliği olduğu için bazı dosyalar gösterilmiyor