Prechádzať zdrojové kódy

feat:文件MD5计算,小说资产同步

pudongliang 4 mesiacov pred
rodič
commit
3bc2774599
43 zmenil súbory, kde vykonal 1075 pridanie a 17 odobranie
  1. 1 1
      portal/dist/assets/accountGroup-DIE3gtyI.js
  2. 1 1
      portal/dist/assets/adPlanConf-DEUTjK5A.js
  3. 1 1
      portal/dist/assets/auth-B8VKdnSI.js
  4. 1 1
      portal/dist/assets/conversion-CWN9QKcb.js
  5. 0 0
      portal/dist/assets/detail-Oegy0HKp.js
  6. 1 1
      portal/dist/assets/drama-CS7KUZnC.js
  7. 0 0
      portal/dist/assets/index-2QdZEOIX.js
  8. 0 0
      portal/dist/assets/index-BA0KF2Mr.js
  9. 0 0
      portal/dist/assets/index-BSVU0cEa.js
  10. 0 0
      portal/dist/assets/index-B_xqWUox.js
  11. 0 0
      portal/dist/assets/index-BfKoB0-G.js
  12. 0 0
      portal/dist/assets/index-BguNr2aC.js
  13. 0 0
      portal/dist/assets/index-CN1hjJ1O.js
  14. 0 0
      portal/dist/assets/index-COoA1FeN.js
  15. 0 0
      portal/dist/assets/index-Co9alpF0.js
  16. 0 0
      portal/dist/assets/index-Cz4WAOx9.js
  17. 0 1
      portal/dist/assets/index-D15WT4Fa.js
  18. 0 0
      portal/dist/assets/index-D59hjBMa.js
  19. 0 0
      portal/dist/assets/index-DTmkUHTd.js
  20. 0 1
      portal/dist/assets/index-NiXg4so_.css
  21. 0 0
      portal/dist/assets/index-khTY6nSF.js
  22. 0 0
      portal/dist/assets/index-p59tveGQ.js
  23. 1 0
      portal/dist/assets/index-vV63W3OI.css
  24. 1 1
      portal/dist/assets/loadPageConf-7bR9J96M.js
  25. 1 1
      portal/dist/assets/ossFile-BNzVPclJ.js
  26. 0 0
      portal/dist/assets/request-bDbUW4Dw.js
  27. 1 1
      portal/dist/assets/user-aknjafug.js
  28. 1 1
      portal/dist/index.html
  29. 48 0
      portal/src/api/fiction.js
  30. 6 0
      portal/src/router/index.js
  31. 5 1
      portal/src/views/ad-plan-conf/index.vue
  32. 485 0
      portal/src/views/fiction/index.vue
  33. 1 0
      portal/src/views/oss-file/index.vue
  34. 1 1
      src/main/java/com/moka/gdtauto/BaseCodeGenerator.java
  35. 10 1
      src/main/java/com/moka/gdtauto/controller/TencentAssetDramaController.java
  36. 173 0
      src/main/java/com/moka/gdtauto/controller/TencentAssetFictionController.java
  37. 84 0
      src/main/java/com/moka/gdtauto/entity/TencentAssetFiction.java
  38. 27 0
      src/main/java/com/moka/gdtauto/mapper/TencentAssetFictionMapper.java
  39. 180 0
      src/main/java/com/moka/gdtauto/service/TencentAssetFictionService.java
  40. 3 1
      src/main/java/com/moka/gdtauto/service/impl/OssFileServiceImpl.java
  41. 0 1
      src/main/java/com/moka/gdtauto/util/TencentFileMd5Util.java
  42. 40 0
      src/main/resources/mapper/TencentAssetFictionMapper.xml
  43. 2 1
      src/test/java/com/moka/gdtauto/service/OssFileMd5.java

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

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

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

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

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

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
portal/dist/assets/detail-Oegy0HKp.js


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

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

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
portal/dist/assets/index-2QdZEOIX.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
portal/dist/assets/index-BA0KF2Mr.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
portal/dist/assets/index-BSVU0cEa.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
portal/dist/assets/index-B_xqWUox.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
portal/dist/assets/index-BfKoB0-G.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
portal/dist/assets/index-BguNr2aC.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
portal/dist/assets/index-CN1hjJ1O.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
portal/dist/assets/index-COoA1FeN.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
portal/dist/assets/index-Co9alpF0.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
portal/dist/assets/index-Cz4WAOx9.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 1
portal/dist/assets/index-D15WT4Fa.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
portal/dist/assets/index-D59hjBMa.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
portal/dist/assets/index-DTmkUHTd.js


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

@@ -1 +0,0 @@
-.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}

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
portal/dist/assets/index-khTY6nSF.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
portal/dist/assets/index-p59tveGQ.js


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

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

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

@@ -1 +1 @@
-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};
+import{r as o}from"./request-bDbUW4Dw.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-DBKIa4n4.js → portal/dist/assets/ossFile-BNzVPclJ.js

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

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
portal/dist/assets/request-bDbUW4Dw.js


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

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

+ 1 - 1
portal/dist/index.html

@@ -5,7 +5,7 @@
     <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-Br1izHrQ.js"></script>
+    <script type="module" crossorigin src="/assets/index-D15WT4Fa.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-BMB56fl-.css">
   </head>
   <body>

+ 48 - 0
portal/src/api/fiction.js

@@ -0,0 +1,48 @@
+import request from '@/utils/request'
+
+/**
+ * 同步小说资产到数据库
+ * @param {Number} accountId 推广账号ID
+ */
+export function syncFiction(accountId) {
+  return request({
+    url: '/api/gdt/asset/fiction/sync',
+    method: 'post',
+    params: { accountId }
+  })
+}
+
+/**
+ * 分页查询小说资产列表
+ * @param {Number} page 页码
+ * @param {Number} pageSize 每页数量
+ * @param {String} fictionName 小说名称
+ */
+export function getFictionList(page = 1, pageSize = 20, fictionName = '') {
+  return request({
+    url: '/api/gdt/asset/fiction/list',
+    method: 'get',
+    params: {fictionName, page, pageSize }
+  })
+}
+
+/**
+ * 查询小说资产详情
+ * @param {Number} id 小说资产ID
+ */
+export function getFictionDetail(id) {
+  return request({
+    url: '/api/gdt/asset/fiction/detail',
+    method: 'get',
+    params: { id }
+  })
+}
+
+
+export function myFictionAssetList(fictionName) {
+  return request({
+    url: '/api/gdt/asset/fiction/myFictionAssets',
+    method: 'get',
+    params: { fictionName }
+  })
+}

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

@@ -86,6 +86,12 @@ const routes = [
         component: () => import('@/views/drama/index.vue'),
         meta: { title: '短剧资产管理', icon: 'VideoPlay' }
       },
+      {
+        path: 'fiction',
+        name: 'Fiction',
+        component: () => import('@/views/fiction/index.vue'),
+        meta: { title: '小说资产管理', icon: 'Reading' }
+      },
       // {
       //   path: 'image',
       //   name: 'Image',

+ 5 - 1
portal/src/views/ad-plan-conf/index.vue

@@ -464,7 +464,7 @@
         :content="plans.length === 0 ? '请先进行预览操作' : ''"
         placement="top-start"
       >
-        <el-button  :disabled="plans.length === 0" @click="handleGeneratePlan" >生成计划</el-button>
+        <el-button :loading="generatePlanBtnLoading" :disabled="plans.length === 0" @click="handleGeneratePlan" >生成计划</el-button>
       </el-tooltip>
         <el-button @click="dialogVisible = false">取消</el-button>
         <el-button type="primary" @click="handleSubmit" :loading="submitting">
@@ -918,6 +918,8 @@ const handleReCreate = async () => {
   }
 }
 
+const generatePlanBtnLoading = ref(false)
+
 const handleGeneratePlan = async () => {
     await formRef.value.validate()
     const { adType, dramaId, dramaAmount } = formData.value
@@ -929,6 +931,7 @@ const handleGeneratePlan = async () => {
   }
     const data = { ...formData.value }
     data.id = editingId.value
+    generatePlanBtnLoading.value = true
     const res = await generatePlan(data)
   if (res.success) {
     ElMessage.success('生成中')
@@ -938,6 +941,7 @@ const handleGeneratePlan = async () => {
   } else {
     ElMessage.error(res.message || '生成失败')
   }
+  generatePlanBtnLoading.value = false
 }
 
 const handleSubmit = async () => {

+ 485 - 0
portal/src/views/fiction/index.vue

@@ -0,0 +1,485 @@
+<template>
+  <div class="space-y-6">
+    <!-- 主卡片 -->
+    <div class="bg-white rounded-xl shadow-sm border border-gray-200 p-6">
+      <div class="border-b border-gray-200 pb-4 mb-6">
+        <div class="flex items-center justify-between">
+          <h2 class="text-xl font-semibold text-gray-800 flex items-center gap-2">
+            <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-indigo-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
+            </svg>
+            小说资产列表
+          </h2>
+          <div class="flex items-center gap-3">
+            <el-button type="primary" @click="showSyncAccountDialog">我的同步账号管理</el-button>
+            <el-select v-model="accountId" placeholder="选择同步账号" clearable filterable style="width: 250px">
+              <el-option v-for="conf in confList" :key="conf.id" :label="`${conf.accountId}`" :value="conf.accountId" />
+            </el-select>
+            <el-button 
+              type="primary" 
+              @click="handleSync"
+              :loading="syncing"
+            >
+              <el-icon><Refresh /></el-icon>
+              同步资产
+            </el-button>
+          </div>
+        </div>
+      </div>
+
+      <!-- 查询条件 -->
+      <div class="flex flex-col gap-3 mb-6">
+        <div class="flex flex-wrap items-center gap-4">
+          <el-input
+            v-model="queryForm.fictionName"
+            placeholder="小说名称筛选"
+            clearable
+            style="width: 220px"
+            @clear="handleSearch"
+            @keyup.enter="handleSearch"
+          />
+          <el-button type="primary" @click="handleSearch">
+            <el-icon><Search /></el-icon>
+            查询
+          </el-button>
+          <el-button @click="handleReset">重置</el-button>
+        </div>
+      </div>
+
+      <!-- 数据统计 -->
+      <div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-6">
+        <div class="bg-gradient-to-br from-indigo-50 to-indigo-100 rounded-lg p-4 border border-indigo-200">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-sm text-indigo-600 mb-1">总资产数</p>
+              <p class="text-2xl font-bold text-indigo-700">{{ total }}</p>
+            </div>
+            <div class="p-3 bg-indigo-200 rounded-lg">
+              <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-indigo-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
+              </svg>
+            </div>
+          </div>
+        </div>
+        <div class="bg-gradient-to-br from-blue-50 to-blue-100 rounded-lg p-4 border border-blue-200">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-sm text-blue-600 mb-1">当前页数量</p>
+              <p class="text-2xl font-bold text-blue-700">{{ fictionList.length }}</p>
+            </div>
+            <div class="p-3 bg-blue-200 rounded-lg">
+              <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
+              </svg>
+            </div>
+          </div>
+        </div>
+        <div class="bg-gradient-to-br from-green-50 to-green-100 rounded-lg p-4 border border-green-200">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-sm text-green-600 mb-1">总页数</p>
+              <p class="text-2xl font-bold text-green-700">{{ totalPages }}</p>
+            </div>
+            <div class="p-3 bg-green-200 rounded-lg">
+              <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
+              </svg>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 小说资产列表表格 -->
+      <el-table
+        :data="fictionList"
+        style="width: 100%"
+        v-loading="loading"
+        stripe
+        border
+      >
+        <el-table-column prop="marketingAssetId" label="资产ID" width="150" />
+        <el-table-column prop="marketingAssetName" label="小说名称" width="300" show-overflow-tooltip />
+        <el-table-column prop="marketingAssetType" label="资产类型" width="200" />
+        <el-table-column label="创建时间" width="180">
+          <template #default="{ row }">
+            {{ formatTimestamp(row.createdTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="更新时间" width="180">
+          <template #default="{ row }">
+            {{ formatDate(row.updateTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="150" fixed="right">
+          <template #default="{ row }">
+            <el-button
+              type="primary"
+              size="small"
+              link
+              @click="handleViewDetail(row)"
+            >
+              <el-icon><View /></el-icon>
+              查看详情
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+      
+      <!-- 分页组件 -->
+      <div class="mt-6 flex justify-end">
+        <el-pagination
+          v-if="total > 0"
+          v-model:current-page="currentPage"
+          v-model:page-size="pageSize"
+          :page-sizes="[10, 20, 50, 100]"
+          :total="total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+          style="justify-content: flex-end"
+        />
+      </div>
+    </div>
+
+    <!-- 同步账号管理对话框 -->
+    <el-dialog
+      v-model="syncAccountDialogVisible"
+      title="我的同步账号管理"
+      width="600px"
+      @close="resetConfForm"
+    >
+      <!-- 新增表单 -->
+      <div class="bg-gray-50 rounded-lg p-4 mb-5 border border-gray-200">
+        <p class="text-sm font-medium text-gray-700 mb-3">新增同步账号</p>
+        <el-form
+          :model="confForm"
+          :rules="confFormRules"
+          ref="confFormRef"
+          label-width="80px"
+          size="default"
+        >
+          <el-form-item label="账号ID" prop="accountId">
+            <el-input
+              v-model="confForm.accountId"
+              placeholder="请输入推广账号ID"
+              clearable
+              style="width: 100%"
+            />
+          </el-form-item>
+          <el-form-item style="margin-bottom: 0">
+            <el-button type="primary" :loading="addingConf" @click="handleAddConf">确认新增</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+
+      <!-- 账号列表 -->
+      <el-table
+        :data="confList"
+        v-loading="confListLoading"
+        stripe
+        border
+        style="width: 100%"
+        empty-text="暂无同步账号配置"
+      >
+        <el-table-column prop="accountId" label="账号ID" />
+        <el-table-column label="创建时间" width="170">
+          <template #default="{ row }">
+            {{ formatDate(row.createTime) }}
+          </template>
+        </el-table-column>
+        <el-table-column label="操作" width="80" fixed="right">
+          <template #default="{ row }">
+            <el-button
+              type="danger"
+              size="small"
+              link
+              @click="handleDelConf(row)"
+            >
+              <el-icon><Delete /></el-icon>
+              删除
+            </el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <template #footer>
+        <el-button @click="syncAccountDialogVisible = false">关闭</el-button>
+      </template>
+    </el-dialog>
+
+    <!-- 小说资产详情对话框 -->
+    <el-dialog
+      v-model="detailDialogVisible"
+      title="小说资产详情"
+      width="700px"
+    >
+      <el-descriptions :column="1" border v-if="currentFiction">
+        <el-descriptions-item label="资产ID">
+          {{ currentFiction.marketingAssetId }}
+        </el-descriptions-item>
+        <el-descriptions-item label="小说名称">
+          {{ currentFiction.marketingAssetName }}
+        </el-descriptions-item>
+        <el-descriptions-item label="资产类型">
+          {{ currentFiction.marketingAssetType }}
+        </el-descriptions-item>
+        <el-descriptions-item label="创建时间">
+          {{ formatTimestamp(currentFiction.createdTime) }}
+        </el-descriptions-item>
+        <el-descriptions-item label="记录创建时间">
+          {{ formatDate(currentFiction.createTime) }}
+        </el-descriptions-item>
+        <el-descriptions-item label="记录更新时间">
+          {{ formatDate(currentFiction.updateTime) }}
+        </el-descriptions-item>
+        <el-descriptions-item label="资产属性">
+          <pre v-if="currentFiction.properties" style="margin: 0; white-space: pre-wrap; word-break: break-all; max-height: 400px; overflow-y: auto;">{{ formatJSON(currentFiction.properties) }}</pre>
+          <span v-else>无</span>
+        </el-descriptions-item>
+      </el-descriptions>
+      <template #footer>
+        <el-button @click="detailDialogVisible = false">关闭</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ref, computed, onMounted } from 'vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { getFictionList, syncFiction } from '@/api/fiction'
+// 同步账号管理共用 drama 的 API
+import { myConfList, addConf, delConf } from '@/api/drama'
+
+import {
+  Refresh,
+  View,
+  Delete,
+  Search
+} from '@element-plus/icons-vue'
+
+// 数据
+const loading = ref(false)
+const syncing = ref(false)
+const fictionList = ref([])
+const detailDialogVisible = ref(false)
+const currentFiction = ref(null)
+
+// 同步账号管理 Dialog
+const syncAccountDialogVisible = ref(false)
+const confListLoading = ref(false)
+const addingConf = ref(false)
+const confFormRef = ref(null)
+const confForm = ref({ accountId: '' })
+const confFormRules = {
+  accountId: [
+    { required: true, message: '请输入账号ID', trigger: 'blur' },
+    { pattern: /^\d+$/, message: '账号ID必须为纯数字', trigger: 'blur' }
+  ]
+}
+
+// 同步参数
+const accountId = ref('')
+const confList = ref([])
+
+const queryForm = ref({ fictionName: '' })
+
+// 分页数据
+const currentPage = ref(1)
+const pageSize = ref(20)
+const total = ref(0)
+
+// 计算属性
+const totalPages = computed(() => {
+  return Math.ceil(total.value / pageSize.value)
+})
+
+const handleSearch = () => {
+  currentPage.value = 1
+  loadFictionList()
+}
+
+const handleReset = () => {
+  queryForm.value = { fictionName: '' }
+  currentPage.value = 1
+  loadFictionList()
+}
+
+// 方法
+const loadFictionList = async () => {
+  loading.value = true
+  try {
+    const { fictionName } = queryForm.value
+    const res = await getFictionList(currentPage.value, pageSize.value, fictionName)
+    if (res.success) {
+      fictionList.value = res.data || []
+      total.value = res.total || 0
+    } else {
+      ElMessage.error(res.message || '加载小说资产列表失败')
+    }
+  } catch (error) {
+    console.error('加载小说资产列表失败:', error)
+    ElMessage.error('加载小说资产列表失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 分页方法
+const handleSizeChange = async (newSize) => {
+  pageSize.value = newSize
+  currentPage.value = 1
+  await loadFictionList()
+}
+
+const handleCurrentChange = async (newPage) => {
+  currentPage.value = newPage
+  await loadFictionList()
+}
+
+const handleSync = async () => {
+  if (!accountId.value) {
+    ElMessage.warning('请填写所有同步参数')
+    return
+  }
+
+  try {
+    await ElMessageBox.confirm(
+      '确定要同步该账户下的小说资产吗?',
+      '提示',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    )
+
+    syncing.value = true
+    const res = await syncFiction(accountId.value)
+    
+    if (res.success) {
+      ElMessage.success(res.message || '同步成功')
+      // 重新加载资产列表
+      currentPage.value = 1 // 重置到第一页
+      await loadFictionList()
+    } else {
+      ElMessage.error(res.message || '同步失败')
+    }
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('同步小说资产失败:', error)
+      ElMessage.error('同步小说资产失败')
+    }
+  } finally {
+    syncing.value = false
+  }
+}
+
+const handleViewDetail = (fiction) => {
+  currentFiction.value = fiction
+  detailDialogVisible.value = true
+}
+
+const formatDate = (dateStr) => {
+  if (!dateStr) return '-'
+  return new Date(dateStr).toLocaleString('zh-CN')
+}
+
+const formatTimestamp = (timestamp) => {
+  if (!timestamp) return '-'
+  return new Date(timestamp * 1000).toLocaleString('zh-CN')
+}
+
+const formatJSON = (jsonStr) => {
+  if (!jsonStr) return ''
+  try {
+    return JSON.stringify(JSON.parse(jsonStr), null, 2)
+  } catch {
+    return jsonStr
+  }
+}
+
+const loadConfList = async () => {
+  confListLoading.value = true
+  try {
+    const response = await myConfList()
+    if (response.success) {
+      confList.value = response.data || []
+    } else {
+      ElMessage.error(response.message || '加载授权列表失败')
+    }
+  } catch (error) {
+    console.error('加载授权列表失败:', error)
+    ElMessage.error('加载授权列表失败')
+  } finally {
+    confListLoading.value = false
+  }
+}
+
+const showSyncAccountDialog = () => {
+  syncAccountDialogVisible.value = true
+}
+
+const resetConfForm = () => {
+  confForm.value = { accountId: '' }
+  confFormRef.value?.clearValidate()
+}
+
+const handleAddConf = async () => {
+  await confFormRef.value?.validate(async (valid) => {
+    if (!valid) return
+    addingConf.value = true
+    try {
+      const res = await addConf({ accountId: confForm.value.accountId })
+      if (res.success) {
+        ElMessage.success('新增成功')
+        resetConfForm()
+        await loadConfList()
+      } else {
+        ElMessage.error(res.message || '新增失败')
+      }
+    } catch (error) {
+      console.error('新增同步账号失败:', error)
+      ElMessage.error('新增失败')
+    } finally {
+      addingConf.value = false
+    }
+  })
+}
+
+const handleDelConf = async (row) => {
+  try {
+    await ElMessageBox.confirm(
+      `确定要删除账号 ${row.accountId} 的同步配置吗?`,
+      '确认删除',
+      {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning'
+      }
+    )
+    const res = await delConf(row.id)
+    if (res.success) {
+      ElMessage.success('删除成功')
+      await loadConfList()
+    } else {
+      ElMessage.error(res.message || '删除失败')
+    }
+  } catch (error) {
+    if (error !== 'cancel') {
+      console.error('删除同步账号失败:', error)
+      ElMessage.error('删除失败')
+    }
+  }
+}
+
+// 生命周期
+onMounted(async () => {
+  await loadConfList()
+  loadFictionList()
+})
+</script>
+
+<style scoped>
+/* 使用Tailwind CSS类替代自定义样式 */
+</style>

+ 1 - 0
portal/src/views/oss-file/index.vue

@@ -164,6 +164,7 @@
             {{ formatFileSize(row.fileSize) }}
           </template>
         </el-table-column>
+        <el-table-column prop="fileMd5" label="MD5" width="150" show-overflow-tooltip></el-table-column>
         
         <el-table-column prop="folder" label="存储路径" width="150" show-overflow-tooltip></el-table-column>
         

+ 1 - 1
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 = "tencent_account_id_brand_image";
+    public final static String TABLE_NAMES = "tencent_asset_fiction";
     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")

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

@@ -13,9 +13,12 @@ import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 
 import org.apache.commons.lang3.StringUtils;
+import org.springframework.data.redis.core.RedisTemplate;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
+import java.util.concurrent.TimeUnit;
+
 import com.moka.gdtauto.entity.SysUser;
 
 
@@ -33,7 +36,7 @@ import com.moka.gdtauto.entity.SysUser;
 public class TencentAssetDramaController {
 
     private final TencentAssetDramaService assetDramaService;
-
+    private final RedisTemplate<String, String> redisTemplate;
     /**
      * 同步短剧资产到数据库
      * 
@@ -51,7 +54,13 @@ public class TencentAssetDramaController {
             @Parameter(description = "推广账号ID", required = true, example = "69781948")
             @RequestParam Long accountId,
         @AuthUser SysUser user) {
+            
         try {
+            String lockKey = "tencent_asset_drama_sync_system_status_lock_" + user.getJobNumber();
+            Boolean isLocked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 1, TimeUnit.MINUTES);
+            if (!isLocked) {
+                return Result.fail("请勿重复操作");
+            }
             int count = assetDramaService.syncMarketingAssets(user.getJobNumber(), Constant.MAIN_ORG_ACCOUNT_ID, accountId);
             return Result.<Void>success("同步成功,共同步" + count + "个短剧资产")
                     .withCount(count);

+ 173 - 0
src/main/java/com/moka/gdtauto/controller/TencentAssetFictionController.java

@@ -0,0 +1,173 @@
+package com.moka.gdtauto.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.moka.gdtauto.annotation.AuthUser;
+import com.moka.gdtauto.common.Constant;
+import com.moka.gdtauto.common.Result;
+import com.moka.gdtauto.entity.TencentAssetFiction;
+import com.moka.gdtauto.service.TencentAssetFictionService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+import com.moka.gdtauto.entity.SysUser;
+
+
+/**
+ * 腾讯广告小说资产控制器
+ *
+ * @author mokamvp
+ * @since 2026-03-11
+ */
+@Tag(name = "小说资产管理", description = "腾讯广告小说资产查询")
+@Slf4j
+@RestController
+@RequestMapping("/api/gdt/asset/fiction")
+@RequiredArgsConstructor
+public class TencentAssetFictionController {
+    private final RedisTemplate<String, String> redisTemplate;
+    private final TencentAssetFictionService assetFictionService;
+
+    /**
+     * 同步小说资产到数据库
+     *
+     * @param accountId 推广账号ID
+     * @return 同步结果
+     */
+    @Operation(
+        summary = "同步小说资产到数据库",
+        description = "从腾讯广告API获取指定账户的小说资产并同步到数据库"
+    )
+    @PostMapping("/sync")
+    public Result<Void> syncAssets(
+            @Parameter(description = "推广账号ID", required = true, example = "69781948")
+            @RequestParam Long accountId,
+        @AuthUser SysUser user) {
+        try {
+            String lockKey = "tencent_asset_fiction_sync_system_status_lock_" + user.getJobNumber();
+            Boolean isLocked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 1, TimeUnit.MINUTES);
+            if (!isLocked) {
+                return Result.fail("请勿重复操作");
+            }
+            int count = assetFictionService.syncMarketingAssets(user.getJobNumber(), Constant.MAIN_ORG_ACCOUNT_ID, accountId);
+            return Result.<Void>success("同步成功,共同步" + count + "个小说资产")
+                    .withCount(count);
+        } catch (Exception e) {
+            log.error("同步小说资产失败,jobNumber={},accountId={}",
+                user.getJobNumber(), accountId, e);
+            return Result.fail("同步失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 分页查询小说资产列表
+     *
+     * @param page 页码
+     * @param pageSize 每页数量
+     * @return 小说资产列表
+     */
+    @Operation(
+        summary = "分页查询小说资产列表",
+        description = "从数据库分页查询小说资产信息"
+    )
+    @GetMapping("/list")
+    public Result<List<TencentAssetFiction>> listAssets(
+            @Parameter(description = "小说资产名称", example = "fictionName")
+            @RequestParam(required = false) String fictionName,
+            @Parameter(description = "页码,从1开始", example = "1")
+            @RequestParam(defaultValue = "1") Long page,
+            @Parameter(description = "每页数量", example = "20")
+            @RequestParam(defaultValue = "20") Long pageSize, @AuthUser SysUser user) {
+        try {
+            // 计算偏移量
+            long offset = (page - 1) * pageSize;
+
+            // 查询总数
+            LambdaQueryWrapper<TencentAssetFiction> countWrapper = new LambdaQueryWrapper<>();
+            countWrapper.eq(TencentAssetFiction::getIsDeleted, false);
+            if (StringUtils.isNotBlank(fictionName)) {
+                countWrapper.like(TencentAssetFiction::getMarketingAssetName, fictionName);
+            }
+            countWrapper.eq(TencentAssetFiction::getJobNumber, user.getJobNumber());
+            long total = assetFictionService.count(countWrapper);
+
+            // 分页查询
+            LambdaQueryWrapper<TencentAssetFiction> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(TencentAssetFiction::getIsDeleted, false)
+                   .eq(TencentAssetFiction::getJobNumber, user.getJobNumber())
+                   .orderByDesc(TencentAssetFiction::getMarketingAssetId)
+                   .last("LIMIT " + offset + ", " + pageSize);
+            if (StringUtils.isNotBlank(fictionName)) {
+                wrapper.like(TencentAssetFiction::getMarketingAssetName, fictionName);
+            }
+
+            List<TencentAssetFiction> assetList = assetFictionService.list(wrapper);
+
+            return Result.success(assetList, "查询成功")
+                    .withPage(page, pageSize, total);
+        } catch (Exception e) {
+            log.error("查询小说资产列表失败", e);
+            return Result.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    @GetMapping("/myFictionAssets")
+    public Result<List<TencentAssetFiction>> getMyFictionAssets(
+            @Parameter(description = "小说资产名称", example = "fictionName")
+            @RequestParam(required = false) String fictionName,
+        @AuthUser SysUser user) {
+        try {
+            LambdaQueryWrapper<TencentAssetFiction> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(TencentAssetFiction::getJobNumber, user.getJobNumber());
+            if (StringUtils.isNotBlank(fictionName)) {
+                wrapper.like(TencentAssetFiction::getMarketingAssetName, fictionName);
+            }
+            wrapper.orderByDesc(TencentAssetFiction::getMarketingAssetId);
+            List<TencentAssetFiction> assetList = assetFictionService.list(wrapper);
+            return Result.success(assetList, "查询成功");
+        } catch (Exception e) {
+            log.error("查询小说资产列表失败", e);
+            return Result.fail("查询失败:" + e.getMessage());
+        }
+    }
+
+    /**
+     * 根据小说资产ID查询详情
+     *
+     * @param id 小说资产ID
+     * @return 小说资产详情
+     */
+    @Operation(
+        summary = "查询小说资产详情",
+        description = "根据ID查询小说资产的详细信息"
+    )
+    @GetMapping("/detail")
+    public Result<TencentAssetFiction> getAssetDetail(
+            @Parameter(description = "小说资产ID", required = true)
+            @RequestParam Long id) {
+        try {
+            LambdaQueryWrapper<TencentAssetFiction> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(TencentAssetFiction::getMarketingAssetId, id)
+                   .eq(TencentAssetFiction::getIsDeleted, false);
+
+            TencentAssetFiction asset = assetFictionService.getOne(wrapper);
+            if (asset != null) {
+                return Result.success(asset, "查询成功");
+            } else {
+                return Result.fail("未找到该小说资产信息");
+            }
+        } catch (Exception e) {
+            log.error("查询小说资产详情失败,id={}", id, e);
+            return Result.fail("查询失败:" + e.getMessage());
+        }
+    }
+}

+ 84 - 0
src/main/java/com/moka/gdtauto/entity/TencentAssetFiction.java

@@ -0,0 +1,84 @@
+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_asset_fiction")
+public class TencentAssetFiction implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 职位编号
+     */
+    private String jobNumber;
+
+    /**
+     * 账号ID
+     */
+    private Long accountId;
+
+    /**
+     * 产品ID
+     */
+    private Long marketingAssetId;
+
+    /**
+     * 产品名称
+     */
+    private String marketingAssetName;
+
+    /**
+     * 产品类型
+     */
+    private String marketingAssetType;
+
+    /**
+     * 创建时间,时间戳
+     */
+    private Long createdTime;
+
+    /**
+     * 是否删除,0:不是,1:是
+     */
+    private Boolean isDeleted;
+
+    /**
+     * 资产属性列表JSON
+     */
+    private String properties;
+
+    /**
+     * 记录创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 记录更新时间
+     */
+    private LocalDateTime updateTime;
+}

+ 27 - 0
src/main/java/com/moka/gdtauto/mapper/TencentAssetFictionMapper.java

@@ -0,0 +1,27 @@
+package com.moka.gdtauto.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.moka.gdtauto.entity.TencentAssetFiction;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 腾讯广告小说资产Mapper接口
+ *
+ * @author mokamvp
+ * @since 2026-03-11
+ */
+@Mapper
+public interface TencentAssetFictionMapper extends BaseMapper<TencentAssetFiction> {
+
+    /**
+     * 批量插入或更新小说资产信息
+     * 使用 INSERT INTO ... ON DUPLICATE KEY UPDATE
+     *
+     * @param fictions 小说资产列表
+     * @return 影响行数
+     */
+    int batchInsertOrUpdate(@Param("fictions") List<TencentAssetFiction> fictions);
+}

+ 180 - 0
src/main/java/com/moka/gdtauto/service/TencentAssetFictionService.java

@@ -0,0 +1,180 @@
+package com.moka.gdtauto.service;
+
+import com.alibaba.fastjson2.JSON;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.moka.gdtauto.client.TencentAdsApiClientFactory;
+import com.moka.gdtauto.common.Constant;
+import com.moka.gdtauto.entity.TencentAssetFiction;
+import com.moka.gdtauto.mapper.TencentAssetFictionMapper;
+import com.tencent.ads.exception.TencentAdsResponseException;
+import com.tencent.ads.exception.TencentAdsSDKException;
+import com.tencent.ads.model.v3.FilteringStruct;
+import com.tencent.ads.model.v3.MarketingTargetAssetsGetResponseData;
+import com.tencent.ads.v3.TencentAds;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 腾讯广告小说资产服务
+ *
+ * @author mokamvp
+ * @since 2026-03-11
+ */
+@Slf4j
+@Service
+@RequiredArgsConstructor
+public class TencentAssetFictionService extends ServiceImpl<TencentAssetFictionMapper, TencentAssetFiction> {
+
+    private final TencentAssetFictionMapper tencentAssetFictionMapper;
+    private final TencentAdsApiClientFactory clientFactory;
+    private final TencentAdsAuthService tencentAdsAuthService;
+
+    /**
+     * 获取小说资产列表
+     * https://developers.e.qq.com/v3.0/docs/api/marketing_target_assets/get
+     *
+     * @param orgAccountId 组织账户ID
+     * @param jobNumber 工号
+     * @param accountId 推广账号ID
+     * @param filtering 过滤条件
+     * @param page 页码
+     * @param pageSize 每页大小
+     * @return 小说资产列表响应数据
+     * @throws TencentAdsResponseException API响应异常
+     * @throws TencentAdsSDKException SDK异常
+     */
+    public MarketingTargetAssetsGetResponseData getMarketingAssets(
+            Long orgAccountId,
+            String jobNumber,
+            Long accountId,
+            List<FilteringStruct> filtering,
+            Long page,
+            Long pageSize) throws TencentAdsResponseException, TencentAdsSDKException, Exception {
+
+        log.info("获取小说资产列表, accountId: {}, page: {}, pageSize: {}", accountId, page, pageSize);
+
+        TencentAds tencentAds = clientFactory.getTencentAds(jobNumber, Constant.MAIN_ORG_ACCOUNT_ID);
+
+        MarketingTargetAssetsGetResponseData response = tencentAds.marketingTargetAssets()
+            .marketingTargetAssetsGet(
+                "MARKETING_TARGET_TYPE_FICTION",  // marketing_target_type
+                accountId,
+                filtering,
+                page,
+                pageSize,
+                null,  // organization_id
+                null   // fields
+            );
+
+        log.info("获取小说资产列表成功, 总数: {}",
+            response.getPageInfo() != null ? response.getPageInfo().getTotalNumber() : 0);
+
+        return response;
+    }
+
+    /**
+     * 同步小说资产数据到数据库
+     * 每次查询后立即保存,最多同步2000条
+     *
+     * @param orgAccountId 组织账户ID
+     * @param jobNumber 工号
+     * @param accountId 推广账号ID
+     * @return 同步的资产数量
+     * @throws Exception 异常
+     */
+    @Transactional(rollbackFor = Exception.class)
+    public int syncMarketingAssets(String jobNumber, Long orgAccountId, Long accountId) throws Exception {
+        log.info("开始同步小说资产, jobNumber: {}, orgAccountId: {}, accountId: {}", jobNumber, orgAccountId, accountId);
+
+        int totalCount = 0; // 总同步数量
+        int maxRecords = 2000; // 最多同步2000条
+        Long page = 1L;
+        Long pageSize = 100L;
+
+        while (totalCount < maxRecords) {
+            MarketingTargetAssetsGetResponseData response = getMarketingAssets(
+                Constant.MAIN_ORG_ACCOUNT_ID, jobNumber, accountId, null, page, pageSize);
+
+            if (response == null || CollectionUtils.isEmpty(response.getList())) {
+                log.info("没有更多数据,结束同步");
+                break;
+            }
+
+            // 转换为实体对象
+            List<TencentAssetFiction> assets = convertToEntities(response);
+            assets.forEach(asset -> {
+                asset.setJobNumber(jobNumber);
+                asset.setAccountId(accountId);
+            });
+
+            // 立即保存当前批次数据
+            if (!CollectionUtils.isEmpty(assets)) {
+                int count = tencentAssetFictionMapper.batchInsertOrUpdate(assets);
+                totalCount += count;
+                log.info("第{}页数据保存成功,本次影响行数: {},累计: {}", page, count, totalCount);
+            }
+
+            // 判断是否达到上限
+            if (totalCount >= maxRecords) {
+                log.info("已达到最大同步数量{},结束同步", maxRecords);
+                break;
+            }
+
+            // 判断是否还有下一页
+            if (response.getPageInfo() == null ||
+                page >= response.getPageInfo().getTotalPage()) {
+                log.info("已是最后一页,结束同步");
+                break;
+            }
+
+            page++;
+        }
+
+        log.info("同步小说资产完成,总影响行数: {}", totalCount);
+        return totalCount;
+    }
+
+    /**
+     * 转换API响应为实体对象列表
+     *
+     * @param response API响应数据
+     * @return 实体对象列表
+     */
+    private List<TencentAssetFiction> convertToEntities(MarketingTargetAssetsGetResponseData response) {
+        List<TencentAssetFiction> result = new ArrayList<>();
+
+        if (response == null || CollectionUtils.isEmpty(response.getList())) {
+            return result;
+        }
+
+        for (var item : response.getList()) {
+            TencentAssetFiction entity = new TencentAssetFiction();
+            entity.setMarketingAssetId(item.getMarketingAssetId());
+            entity.setMarketingAssetName(item.getMarketingAssetName());
+            entity.setMarketingAssetType(item.getMarketingAssetType() != null ?
+                item.getMarketingAssetType().toString() : null);
+            entity.setCreatedTime(item.getCreatedTime());
+            // SDK中MarketingAssetStruct没有isDeleted字段,默认设置为false
+            entity.setIsDeleted(false);
+
+            // 将properties转为JSON字符串
+            if (item.getProperties() != null && !item.getProperties().isEmpty()) {
+                try {
+                    entity.setProperties(JSON.toJSONString(item.getProperties()));
+                } catch (Exception e) {
+                    log.warn("转换properties失败: {}", e.getMessage());
+                }
+            }
+
+            result.add(entity);
+        }
+
+        return result;
+    }
+}

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

@@ -8,6 +8,8 @@ import com.moka.gdtauto.entity.OssFile;
 import com.moka.gdtauto.mapper.OssFileMapper;
 import com.moka.gdtauto.service.OssFileService;
 import com.moka.gdtauto.util.OssUtil;
+import com.moka.gdtauto.util.TencentFileMd5Util;
+
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
@@ -37,7 +39,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()));
+            String md5 = TencentFileMd5Util.calculateMd5(file.getInputStream());
             // 上传到OSS
             String fileUrl = ossUtil.uploadFile(file, folder);
             

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

@@ -10,7 +10,6 @@ public class TencentFileMd5Util {
 
 
     public static String calculateMD5(File file) throws Exception {
-        MessageDigest md = MessageDigest.getInstance("MD5");
         return calculateMd5(new FileInputStream(file));
     }
 

+ 40 - 0
src/main/resources/mapper/TencentAssetFictionMapper.xml

@@ -0,0 +1,40 @@
+<?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.TencentAssetFictionMapper">
+
+    <!-- 批量插入或更新小说资产 -->
+    <insert id="batchInsertOrUpdate" parameterType="java.util.List">
+        INSERT INTO tencent_asset_fiction (
+            job_number,
+            account_id,
+            marketing_asset_id,
+            marketing_asset_name,
+            marketing_asset_type,
+            created_time,
+            is_deleted,
+            properties
+        )
+        VALUES
+        <foreach collection="fictions" item="item" separator=",">
+            (
+                #{item.jobNumber},
+                #{item.accountId},
+                #{item.marketingAssetId},
+                #{item.marketingAssetName},
+                #{item.marketingAssetType},
+                #{item.createdTime},
+                #{item.isDeleted},
+                #{item.properties}
+            )
+        </foreach>
+        ON DUPLICATE KEY UPDATE
+            marketing_asset_name = VALUES(marketing_asset_name),
+            marketing_asset_type = VALUES(marketing_asset_type),
+            created_time = VALUES(created_time),
+            is_deleted = VALUES(is_deleted),
+            properties = VALUES(properties),
+            update_time = CURRENT_TIMESTAMP
+    </insert>
+
+</mapper>

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

@@ -28,7 +28,8 @@ public class OssFileMd5 {
 
     @Test
     public void testOssFileMd5() {
-        List<OssFile> ossFileList = ossFileService.list();
+        List<OssFile> ossFileList = ossFileService.lambdaQuery()
+        .ge(OssFile::getId, 19).list();
         for (OssFile ossFile : ossFileList) {
             try {
                 URL url = new URL(ossFile.getFileUrl());

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov