pudongliang преди 4 месеца
родител
ревизия
9c268f2770
променени са 77 файла, в които са добавени 2205 реда и са изтрити 68 реда
  1. 9 0
      .codex/README.md
  2. 8 0
      .codex/config.toml
  3. 105 0
      .codex/skills/git-commit-push/SKILL.md
  4. 30 0
      AGENTS.md
  5. 39 0
      docs/巡检.md
  6. 1 1
      portal/dist/assets/accountGroup-CIc_28_6.js
  7. 1 0
      portal/dist/assets/adAutoConf-i84Oyuvo.js
  8. 1 1
      portal/dist/assets/adPlanConf-B6YC8P7P.js
  9. 1 1
      portal/dist/assets/auth-Ca4P_5tW.js
  10. 1 1
      portal/dist/assets/conversion-iMsROi_Y.js
  11. 0 0
      portal/dist/assets/detail-DJtk5TjH.js
  12. 1 1
      portal/dist/assets/drama-B3_hnp6Z.js
  13. 0 0
      portal/dist/assets/edit-BhjGObfr.js
  14. 1 0
      portal/dist/assets/edit-DL6-jY31.css
  15. 0 0
      portal/dist/assets/index-BMB56fl-.css
  16. 0 0
      portal/dist/assets/index-BcuL_ZyG.js
  17. 0 0
      portal/dist/assets/index-BgaAXfx_.js
  18. 0 0
      portal/dist/assets/index-BiD6Oz0B.js
  19. 0 0
      portal/dist/assets/index-BvOZO_NA.js
  20. 0 0
      portal/dist/assets/index-C1G4zBEp.js
  21. 0 0
      portal/dist/assets/index-C9kFEVBd.js
  22. 0 0
      portal/dist/assets/index-CQb2O2a5.js
  23. 0 0
      portal/dist/assets/index-CUDIoSEu.js
  24. 0 0
      portal/dist/assets/index-Cb0Z9rAk.css
  25. 0 0
      portal/dist/assets/index-Cf8QgW4o.js
  26. 0 0
      portal/dist/assets/index-D7oYgj79.js
  27. 0 0
      portal/dist/assets/index-DYJz7ny4.js
  28. 0 0
      portal/dist/assets/index-EZafHA9G.js
  29. 0 0
      portal/dist/assets/index-IKjf_osf.js
  30. 0 0
      portal/dist/assets/index-VGTUrogb.js
  31. 0 1
      portal/dist/assets/index-lknx8aJB.js
  32. 0 0
      portal/dist/assets/index-pxO60Enl.js
  33. 1 1
      portal/dist/assets/loadPageConf-DfC1RoK1.js
  34. 1 1
      portal/dist/assets/ossFile-Bmoq_1dU.js
  35. 0 0
      portal/dist/assets/request-Bzgc6No_.js
  36. 1 1
      portal/dist/assets/user-Cp3LOilH.js
  37. 2 2
      portal/dist/index.html
  38. 86 0
      portal/src/api/adAutoConf.js
  39. 12 0
      portal/src/router/index.js
  40. 286 0
      portal/src/views/ad-auto-conf/edit.vue
  41. 532 0
      portal/src/views/ad-auto-conf/index.vue
  42. 26 0
      portal/src/views/ad-plan-conf/index.vue
  43. 1 1
      src/main/java/com/moka/gdtauto/BaseCodeGenerator.java
  44. 26 0
      src/main/java/com/moka/gdtauto/common/AutoPlanRule.java
  45. 179 0
      src/main/java/com/moka/gdtauto/controller/AdAutoConfController.java
  46. 71 0
      src/main/java/com/moka/gdtauto/controller/AdAutoExecuteLogController.java
  47. 30 8
      src/main/java/com/moka/gdtauto/controller/AdPlanConfController.java
  48. 2 0
      src/main/java/com/moka/gdtauto/controller/vo/CreateAdPlanConfRequest.java
  49. 2 0
      src/main/java/com/moka/gdtauto/controller/vo/UpdateAdPlanConfRequest.java
  50. 72 0
      src/main/java/com/moka/gdtauto/entity/AdAutoConf.java
  51. 78 0
      src/main/java/com/moka/gdtauto/entity/AdAutoExecuteLog.java
  52. 5 0
      src/main/java/com/moka/gdtauto/entity/AdPlan.java
  53. 8 0
      src/main/java/com/moka/gdtauto/entity/AdPlanConf.java
  54. 22 0
      src/main/java/com/moka/gdtauto/mapper/AdAutoConfMapper.java
  55. 16 0
      src/main/java/com/moka/gdtauto/mapper/AdAutoExecuteLogMapper.java
  56. 2 0
      src/main/java/com/moka/gdtauto/mapper/TencentAssetDramaMapper.java
  57. 2 0
      src/main/java/com/moka/gdtauto/mapper/TencentAssetFictionMapper.java
  58. 16 0
      src/main/java/com/moka/gdtauto/service/AdAutoConfService.java
  59. 16 0
      src/main/java/com/moka/gdtauto/service/AdAutoExecuteLogService.java
  60. 281 0
      src/main/java/com/moka/gdtauto/service/AdAutoExecutor.java
  61. 3 4
      src/main/java/com/moka/gdtauto/service/AdPlanConfService.java
  62. 12 12
      src/main/java/com/moka/gdtauto/service/AdPlanDailyReportService.java
  63. 25 0
      src/main/java/com/moka/gdtauto/service/impl/AdAutoConfServiceImp.java
  64. 20 0
      src/main/java/com/moka/gdtauto/service/impl/AdAutoExecuteLogServiceImp.java
  65. 43 22
      src/main/java/com/moka/gdtauto/service/impl/AdPlanConfServiceImpl.java
  66. 34 0
      src/main/java/com/moka/gdtauto/task/AdAutoTask.java
  67. 4 4
      src/main/java/com/moka/gdtauto/task/AdPlanDailyReportSyncTask.java
  68. 0 1
      src/main/java/com/moka/gdtauto/util/SiteConfUtil.java
  69. 13 0
      src/main/resources/mapper/AdAutoConfMapper.xml
  70. 5 0
      src/main/resources/mapper/AdAutoExecuteLogMapper.xml
  71. 1 0
      src/main/resources/mapper/AdPlanMapper.xml
  72. 11 0
      src/main/resources/mapper/TencentAssetDramaMapper.xml
  73. 12 0
      src/main/resources/mapper/TencentAssetFictionMapper.xml
  74. 1 1
      src/test/java/com/moka/gdtauto/service/AdPlanConfAsyncTest.java
  75. 6 2
      src/test/java/com/moka/gdtauto/service/AdPlanDailyReportSyncTest.java
  76. 5 1
      src/test/java/com/moka/gdtauto/service/CreateAdGroupTest.java
  77. 36 1
      src/test/java/com/moka/gdtauto/util/SampleTest.java

+ 9 - 0
.codex/README.md

@@ -0,0 +1,9 @@
+# Codex Project Metadata
+
+This directory stores Codex project-local configuration and skills for `gdt-auto`.
+
+## Source Mapping
+- `.qoder/rules/always_on_rule.md` was normalized into the project-level `AGENTS.md`.
+- `.qoder/skills/git-commit-push/SKILL.md` was migrated to `.codex/skills/git-commit-push/SKILL.md`.
+
+Keep `AGENTS.md` as the primary always-on project instruction file. Keep reusable task guides under `.codex/skills/`.

+ 8 - 0
.codex/config.toml

@@ -0,0 +1,8 @@
+# Project-local Codex bootstrap config for gdt-auto.
+# Keep this file minimal and stick to keys already used by the local Codex install.
+
+model = "gpt-5.4"
+model_reasoning_effort = "medium"
+
+[projects."/Users/pudongliang/moka_workspace/gdt-auto"]
+trust_level = "trusted"

+ 105 - 0
.codex/skills/git-commit-push/SKILL.md

@@ -0,0 +1,105 @@
+# git-commit-push
+
+Git commit and push quick reference for this repository.
+
+Use this skill when the task is about staging changes, creating commits, pushing branches, amending commits, stashing work, or inspecting git history.
+
+## Basic Flow
+
+### Inspect changes
+```bash
+git status
+git diff
+git diff --cached
+```
+
+### Stage changes
+```bash
+git add .
+git add <path>
+git add -p
+```
+
+### Commit
+```bash
+git commit -m "feat: short summary"
+git commit
+git commit -am "fix: short summary"
+```
+
+## Push
+```bash
+git push
+git push origin main
+git push -u origin <branch>
+git push --force-with-lease
+```
+
+## Amend
+```bash
+git commit --amend -m "new message"
+git add <missed-file>
+git commit --amend --no-edit
+```
+
+If the amended commit has already been pushed:
+
+```bash
+git push --force-with-lease
+```
+
+## Undo and Restore
+Prefer non-destructive commands unless the user explicitly asks otherwise.
+
+### Unstage
+```bash
+git restore --staged <path>
+git reset HEAD <path>
+```
+
+### Discard worktree changes
+```bash
+git restore <path>
+```
+
+### Undo commits while keeping changes
+```bash
+git reset --soft HEAD~1
+git reset HEAD~1
+```
+
+Avoid `git reset --hard` unless the user explicitly requests destructive history/worktree reset.
+
+## Stash
+```bash
+git stash
+git stash -u
+git stash list
+git stash pop
+git stash apply stash@{0}
+git stash drop stash@{0}
+```
+
+## History
+```bash
+git log
+git log --oneline
+git log --graph --oneline --all
+git log -n 5
+git log <path>
+```
+
+## Conflict Handling
+```bash
+git status
+git checkout --ours <path>
+git checkout --theirs <path>
+git add <path>
+git commit -m "resolve merge conflict"
+git merge --abort
+```
+
+## Commit Message Guidance
+- Prefer conventional prefixes such as `feat:`, `fix:`, `refactor:`, `docs:`, `test:`, `chore:`.
+- Keep the first line concise and user-impact oriented.
+- Do not amend or force-push unless the user asked for it or the workflow clearly requires it.

+ 30 - 0
AGENTS.md

@@ -0,0 +1,30 @@
+# gdt-auto Agent Guide
+
+## Project Scope
+- This project integrates with Tencent Ads APIs to batch-create and manage ad plans and ad creatives.
+- Backend is a standard Java web application built with Spring Boot and MyBatis-Plus.
+- Backend layering follows `controller`, `service`, `service.impl`, `mapper`, and `entity`.
+- Frontend lives under `portal/` and is a Vue 3 + Vite application using Element Plus and Tailwind CSS.
+
+## Working Rules
+- Prefer existing code patterns in this repository over introducing new abstractions.
+- When the user provides a concrete method context and asks for a change, limit the code change to that method's behavior unless broader refactoring is explicitly confirmed.
+- For backend API responses used by the frontend, prefer `com.moka.gdtauto.common.Result`; frontend business success is determined by the `success` field.
+- Do not read `src/main/resources` configuration files unless the task requires it and the user has confirmed.
+- Do not inspect generated or dependency directories unless strictly necessary, especially `target/`, `node_modules/`, and `dist/`.
+- Avoid reading files and directories already ignored by `.gitignore` unless they are directly relevant to the task.
+
+## Project References
+- Tencent Ads SDK usage reference: check tests under `src/test` when present, especially Tencent Ads create-flow examples such as `TencentAdsAdgroupCreateTest`.
+- CRUD backend reference: [src/main/java/com/moka/gdtauto/controller/OssFileController.java](/Users/pudongliang/moka_workspace/gdt-auto/src/main/java/com/moka/gdtauto/controller/OssFileController.java)
+- Frontend page reference: [portal/src/views/oss-file/index.vue](/Users/pudongliang/moka_workspace/gdt-auto/portal/src/views/oss-file/index.vue)
+- Frontend API reference: [portal/src/api/ossFile.js](/Users/pudongliang/moka_workspace/gdt-auto/portal/src/api/ossFile.js)
+- Router reference: [portal/src/router/index.js](/Users/pudongliang/moka_workspace/gdt-auto/portal/src/router/index.js)
+- UI style reference: [portal/src/views/conversion/index.vue](/Users/pudongliang/moka_workspace/gdt-auto/portal/src/views/conversion/index.vue)
+
+## CRUD Generation
+- If the task is to add CRUD for a new table, update `TABLE_NAMES` in [src/main/java/com/moka/gdtauto/BaseCodeGenerator.java](/Users/pudongliang/moka_workspace/gdt-auto/src/main/java/com/moka/gdtauto/BaseCodeGenerator.java) and then run its `main` method.
+
+## Codex Skills
+- Project-local skills for Codex are stored under `.codex/skills/`.
+- Migrated from `.qoder`: `git-commit-push`

+ 39 - 0
docs/巡检.md

@@ -0,0 +1,39 @@
+```json
+if (ad.is_new) {
+    // 新广告规则
+    const rules = config.patrol.rules_new;
+
+    // 规则1: 消耗>100元 & ROI<rules.roi_min_100 -> 暂停
+    if (costYuan > 100 && roi < rules.roi_min_100) {
+      return `新广告消耗${costYuan}元>100元 且 ROI=${roi.toFixed(3)}<${rules.roi_min_100}`;
+    }
+
+    // 规则2: 消耗>50元 & ROI<rules.roi_min_50 -> 暂停
+    if (costYuan > 50 && roi < rules.roi_min_50) {
+      return `新广告消耗${costYuan}元>50元 且 ROI=${roi.toFixed(3)}<${rules.roi_min_50}`;
+    }
+
+    // 规则3: 消耗>30元 & 粉价>rules.scan_cost_max -> 暂停
+    if (costYuan > 30 && scanCostFen > rules.scan_cost_max) {
+      return `新广告消耗${costYuan}元>30元 且 粉价=${fenToYuan(scanCostFen)}元>${fenToYuan(rules.scan_cost_max)}元`;
+    }
+  } else {
+    // 老广告规则
+    const rules = config.patrol.rules_old;
+
+    // 规则1: 消耗>50元 & ROI<rules.roi_min_50 -> 暂停
+    if (costYuan > 50 && roi < rules.roi_min_50) {
+      return `老广告消耗${costYuan}元>50元 且 ROI=${roi.toFixed(3)}<${rules.roi_min_50}`;
+    }
+
+    // 规则2: 消耗>30元 & ROI<rules.roi_min_30 -> 暂停
+    if (costYuan > 30 && roi < rules.roi_min_30) {
+      return `老广告消耗${costYuan}元>30元 且 ROI=${roi.toFixed(3)}<${rules.roi_min_30}`;
+    }
+
+    // 规则3: 粉价>rules.scan_cost_max -> 暂停(不限消耗门槛)
+    if (scanCostFen > rules.scan_cost_max) {
+      return `老广告粉价=${fenToYuan(scanCostFen)}元>${fenToYuan(rules.scan_cost_max)}元`;
+    }
+  }
+```

+ 1 - 1
portal/dist/assets/accountGroup-9R0Ff6NU.js → portal/dist/assets/accountGroup-CIc_28_6.js

@@ -1 +1 @@
-import{r as u}from"./request-DJN4cqfM.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-Bzgc6No_.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 - 0
portal/dist/assets/adAutoConf-i84Oyuvo.js

@@ -0,0 +1 @@
+import{r as e}from"./request-Bzgc6No_.js";function u(t){return e({url:"/api/ad-auto-conf/list",method:"get",params:t})}function n(t){return e({url:`/api/ad-auto-conf/${t}`,method:"get"})}function a(t){return e({url:"/api/ad-auto-conf",method:"post",data:t})}function r(t){return e({url:`/api/ad-auto-conf/execute?id=${t}`,method:"post"})}function i(t){return e({url:"/api/execute-log/list",method:"get",params:t})}function d(t){return e({url:"/api/execute-log/getChildLog",method:"get",params:t})}export{d as a,i as b,n as c,r as e,u as g,a as s};

+ 1 - 1
portal/dist/assets/adPlanConf-LY0heeNc.js → portal/dist/assets/adPlanConf-B6YC8P7P.js

@@ -1 +1 @@
-import{r as a}from"./request-DJN4cqfM.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-Bzgc6No_.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-DdfmB9Bv.js → portal/dist/assets/auth-Ca4P_5tW.js

@@ -1 +1 @@
-import{r as t}from"./request-DJN4cqfM.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-Bzgc6No_.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-u9xFNIGd.js → portal/dist/assets/conversion-iMsROi_Y.js

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

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/detail-DJtk5TjH.js


+ 1 - 1
portal/dist/assets/drama-DZ8gangp.js → portal/dist/assets/drama-B3_hnp6Z.js

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

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/edit-BhjGObfr.js


+ 1 - 0
portal/dist/assets/edit-DL6-jY31.css

@@ -0,0 +1 @@
+.rules-container[data-v-724bfc0b]{max-height:500px;overflow-y:auto}.rule-item[data-v-724bfc0b]{border:1px solid #e5e7eb}

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/index-BMB56fl-.css


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/index-BcuL_ZyG.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/index-BgaAXfx_.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/index-BiD6Oz0B.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/index-BvOZO_NA.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/index-C1G4zBEp.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/index-C9kFEVBd.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/index-CQb2O2a5.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/index-CUDIoSEu.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/index-Cb0Z9rAk.css


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/index-Cf8QgW4o.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/index-D7oYgj79.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/index-DYJz7ny4.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/index-EZafHA9G.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/index-IKjf_osf.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/index-VGTUrogb.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 1
portal/dist/assets/index-lknx8aJB.js


Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/index-pxO60Enl.js


+ 1 - 1
portal/dist/assets/loadPageConf-C84rijr5.js → portal/dist/assets/loadPageConf-DfC1RoK1.js

@@ -1 +1 @@
-import{r as o}from"./request-DJN4cqfM.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-Bzgc6No_.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-Dhy5SzHS.js → portal/dist/assets/ossFile-Bmoq_1dU.js

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

Файловите разлики са ограничени, защото са твърде много
+ 0 - 0
portal/dist/assets/request-Bzgc6No_.js


+ 1 - 1
portal/dist/assets/user-CS-nnh4R.js → portal/dist/assets/user-Cp3LOilH.js

@@ -1 +1 @@
-import{r as e}from"./request-DJN4cqfM.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-Bzgc6No_.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-m3V4qQyI.js"></script>
-    <link rel="stylesheet" crossorigin href="/assets/index-BMB56fl-.css">
+    <script type="module" crossorigin src="/assets/index-lknx8aJB.js"></script>
+    <link rel="stylesheet" crossorigin href="/assets/index-Cb0Z9rAk.css">
   </head>
   <body>
     <div id="app"></div>

+ 86 - 0
portal/src/api/adAutoConf.js

@@ -0,0 +1,86 @@
+import request from '../utils/request'
+
+/**
+ * 分页查询广告自动划配置列表
+ */
+export function getAdAutoConfList(params) {
+  return request({
+    url: '/api/ad-auto-conf/list',
+    method: 'get',
+    params
+  })
+}
+
+/**
+ * 获取广告自动划配置详情
+ */
+export function getAdAutoConfDetail(id) {
+  return request({
+    url: `/api/ad-auto-conf/${id}`,
+    method: 'get'
+  })
+}
+
+/**
+ * 保存广告自动划配置(新增或更新)
+ */
+export function saveAdAutoConf(data) {
+  return request({
+    url: '/api/ad-auto-conf',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 删除广告自动划配置
+ */
+export function deleteAdAutoConf(id) {
+  return request({
+    url: `/api/ad-auto-conf/${id}`,
+    method: 'delete'
+  })
+}
+
+/**
+ * 批量删除广告自动划配置
+ */
+export function batchDeleteAdAutoConf(ids) {
+  return request({
+    url: '/api/ad-auto-conf/batch',
+    method: 'delete',
+    data: ids
+  })
+}
+
+/**
+ * 执行广告自动划配置
+ */
+export function executeAdAutoConf(id) {
+  return request({
+    url: `/api/ad-auto-conf/execute?id=${id}`,
+    method: 'post'
+  })
+}
+
+/**
+ * 分页查询执行日志
+ */
+export function getExecuteLogList(params) {
+  return request({
+    url: '/api/execute-log/list',
+    method: 'get',
+    params
+  })
+}
+
+/**
+ * 查询子日志
+ */
+export function getChildLog(params) {
+  return request({
+    url: '/api/execute-log/getChildLog',
+    method: 'get',
+    params
+  })
+}

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

@@ -115,6 +115,18 @@ const routes = [
         name: 'LoadPageConf',
         component: () => import('@/views/load-page-conf/index.vue'),
         meta: { title: '落地页配置', icon: 'Document' }
+      },
+      {
+        path: 'ad-auto-conf',
+        name: 'AdAutoConf',
+        component: () => import('@/views/ad-auto-conf/index.vue'),
+        meta: { title: '广告自动划配置', icon: 'Setting' }
+      },
+      {
+        path: 'ad-auto-conf/edit',
+        name: 'AdAutoConfEdit',
+        component: () => import('@/views/ad-auto-conf/edit.vue'),
+        meta: { title: '编辑广告自动划配置', icon: 'Setting', hideInMenu: true }
       }
     ]
   }

+ 286 - 0
portal/src/views/ad-auto-conf/edit.vue

@@ -0,0 +1,286 @@
+<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 ">
+          <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-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
+            </svg>
+            {{ isEdit ? '编辑配置' : '新增配置' }}
+          </h2>
+          <el-button @click="handleBack">
+            <el-icon><ArrowLeft /></el-icon>
+            返回列表
+          </el-button>
+        </div>
+      </div>
+
+      <el-form ref="formRef" :model="form" :rules="rules" label-width="120px">
+        <el-form-item label="广告组" prop="adGroupId">
+          <el-select v-model="form.adGroupId" placeholder="请选择广告组" style="width: 50%"  filterable>
+            <el-option label="全部广告组" :value="0" />
+            <el-option
+              v-for="group in accountGroupList"
+              :key="group.id"
+              :label="`${group.groupName} (${group.id})`"
+              :value="group.id"
+            />
+          </el-select>
+          <p class="text-xs text-gray-500 mt-1">选择"全部广告组"表示不考虑分组情况</p>
+        </el-form-item>
+        <el-form-item label="新广告天数" prop="newAdDays">
+          <el-input-number v-model="form.newAdDays" :min="1" placeholder="请输入新广告天数定义" style="width: 50%" />
+        </el-form-item>
+
+        <!-- 规则配置 -->
+        <el-divider content-position="left">规则配置</el-divider>
+        <p class="text-sm text-red-500">规则执行将按照最小成本从高到低排序,当最小成本为0时候,将不考虑成本,直接计算ROI或粉价</p>
+         <div class="rules-container">
+          <div class="grid grid-cols-2 gap-3">
+            <div v-for="(rule, index) in form.rules" :key="index" class="rule-item p-3 bg-gray-50 rounded-lg">
+              <div class="flex items-center justify-between mb-1">
+                <span class="font-medium text-gray-700">【规则 {{ index + 1 }}】</span>
+                <div class="bg-blue-50 rounded text-xs text-blue-800">
+                    <template v-if="rule.type === 1">
+                    <span class="font-medium">{{ rule.newAd === 1 ? '新广告' : '老广告' }}:</span>消耗 > {{ rule.minCost }}元 且 ROI &lt; {{ rule.minIor }} 时暂停
+                    </template>
+                    <template v-else>
+                    <span class="font-medium">{{ rule.newAd === 1 ? '新广告' : '老广告' }}:</span>{{ rule.minCost > 0 ? `消耗 > ${rule.minCost}元 且 ` : '' }}粉价 > {{ rule.scanMaxCost }}元 时暂停
+                    </template>
+                 </div>
+                 <el-button type="danger" size="small" link @click="removeRule(index)">
+                  <el-icon><Delete /></el-icon>
+                  删除
+                </el-button>
+              </div>
+              
+              <!-- 广告类型 radio -->
+              <el-form-item style="margin-bottom:4px" label="广告" :prop="`rules.${index}.newAd`" :rules="{ required: true, message: '请选择', trigger: 'change' }" label-width="70px">
+                <el-radio-group v-model="rule.newAd" size="small">
+                  <el-radio :label="1">新广告</el-radio>
+                  <el-radio :label="2">老广告</el-radio>
+                </el-radio-group>
+              </el-form-item>
+              <!-- 规则类型 radio -->
+              <el-form-item style="margin-bottom:4px" label="规则" :prop="`rules.${index}.type`" :rules="{ required: true, message: '请选择', trigger: 'change' }" label-width="70px">
+                <el-radio-group v-model="rule.type" size="small">
+                  <el-radio :label="1">ROI规则</el-radio>
+                  <el-radio :label="2">粉价规则</el-radio>
+                </el-radio-group>
+              </el-form-item>
+              <!-- 最小成本 -->
+              <el-form-item style="margin-bottom:4px" label="最小成本" :prop="`rules.${index}.minCost`" :rules="{ required: true, message: '请输入', trigger: 'blur' }" label-width="80px">
+                <el-input-number v-model="rule.minCost" :min="0" placeholder="最小成本" style="width: 80%" size="small" />
+              </el-form-item>
+              <!-- ROI规则显示最小ROI -->
+              <el-form-item v-if="rule.type === 1" style="margin-bottom:4px" label="最小ROI" :prop="`rules.${index}.minIor`" :rules="{ required: true, message: '请输入', trigger: 'blur' }" label-width="80px">
+                <el-input-number v-model="rule.minIor" :min="0" placeholder="最小ROI" style="width: 80%" size="small" />
+              </el-form-item>
+              <!-- 粉价规则显示最大粉价 -->
+              <el-form-item v-if="rule.type === 2" style="margin-bottom:4px" label="最大粉价" :prop="`rules.${index}.scanMaxCost`" :rules="{ required: true, message: '请输入', trigger: 'blur' }" label-width="80px">
+                <el-input-number v-model="rule.scanMaxCost" :min="0" placeholder="最大粉价" style="width: 80%" size="small" />
+              </el-form-item>
+            </div>
+          </div>
+          <el-button type="primary" link @click="addRule" class="w-full">
+            <el-icon><Plus /></el-icon>
+            添加规则
+          </el-button>
+        </div>
+      </el-form>
+
+      <div class="flex justify-center gap-4 mt-8 pt-6 border-t border-gray-200">
+        <el-button @click="handleBack">取消</el-button>
+        <el-button type="primary" @click="handleSubmit" :loading="submitting">保存</el-button>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { ref, reactive, onMounted } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
+import { ElMessage } from 'element-plus'
+import { Plus, Delete, ArrowLeft } from '@element-plus/icons-vue'
+import {
+  getAdAutoConfDetail,
+  saveAdAutoConf
+} from '@/api/adAutoConf'
+import { getAccountGroupListAll } from '@/api/accountGroup'
+
+// 响应式数据
+const submitting = ref(false)
+const formRef = ref(null)
+const accountGroupList = ref([])
+const router = useRouter()
+const route = useRoute()
+
+// 判断是新增还是编辑
+const isEdit = ref(false)
+const editId = ref(null)
+
+// 表单数据
+const form = reactive({
+  id: null,
+  jobNumber: null,
+  adGroupId: 0,
+  newAdDays: 3,
+  rules: []
+})
+
+// 表单校验规则
+const rules = {
+  adGroupId: [
+    { required: true, message: '请选择广告组', trigger: 'change' }
+  ],
+  newAdDays: [
+    { required: true, message: '请输入新广告天数', trigger: 'blur' }
+  ]
+}
+
+// 创建默认规则
+const createDefaultRule = () => ({
+  type: 1,
+  newAd: 1,
+  minCost: 30,
+  minIor: 0.15,
+  scanMaxCost: 10
+})
+
+const createFirstDefaultRule = () => ([
+  { type: 1, newAd: 1, minCost: 100, minIor: 0.15, scanMaxCost: 10 },
+  { type: 1, newAd: 1, minCost: 30,  minIor: 0.10, scanMaxCost: 10 },
+  { type: 2, newAd: 1, minCost: 0,   minIor: 0,    scanMaxCost: 10  },
+  { type: 2, newAd: 2, minCost: 0,   minIor: 0,    scanMaxCost: 100 }
+])
+
+// 添加规则
+const addRule = () => {
+  form.rules.push(createDefaultRule())
+}
+
+// 删除规则
+const removeRule = (index) => {
+  form.rules.splice(index, 1)
+}
+
+// 返回列表
+const handleBack = () => {
+  router.push('/ad-auto-conf')
+}
+
+// 加载广告组列表
+const loadAccountGroupList = async () => {
+  try {
+    const response = await getAccountGroupListAll()
+    if (response.success) {
+      accountGroupList.value = response.data || []
+    }
+  } catch (error) {
+    console.error('加载广告组列表失败:', error)
+  }
+}
+
+// 加载配置详情
+const loadDetail = async (id) => {
+  try {
+    const response = await getAdAutoConfDetail(id)
+    if (response.success) {
+      const data = response.data
+      form.id = data.id
+      form.jobNumber = data.jobNumber
+      form.adGroupId = data.adGroupId
+      form.newAdDays = data.newAdDays
+      // 解析 rules JSON
+      if (data.rules) {
+        try {
+          form.rules = JSON.parse(data.rules)
+          if (!Array.isArray(form.rules) || form.rules.length === 0) {
+            form.rules = [createDefaultRule()]
+          }
+        } catch (e) {
+          form.rules = [createDefaultRule()]
+        }
+      } else {
+        form.rules = [createDefaultRule()]
+      }
+    } else {
+      ElMessage.error(response.message || '获取详情失败')
+      handleBack()
+    }
+  } catch (error) {
+    console.error('获取详情失败:', error)
+    ElMessage.error('获取详情失败')
+    handleBack()
+  }
+}
+
+// 提交表单
+const handleSubmit = async () => {
+  if (!formRef.value) return
+
+  await formRef.value.validate(async (valid) => {
+    if (!valid) return
+
+    if (form.rules.length === 0) {
+      ElMessage.warning('请至少添加一条规则')
+      return
+    }
+
+    submitting.value = true
+    try {
+      const data = {
+        jobNumber: form.jobNumber,
+        adGroupId: form.adGroupId,
+        newAdDays: form.newAdDays,
+        rules: JSON.stringify(form.rules)
+      }
+
+      const response = await saveAdAutoConf(data)
+
+      if (response.success) {
+        ElMessage.success(isEdit.value ? '更新成功' : '新增成功')
+        handleBack()
+      } else {
+        ElMessage.error(response.message || '操作失败')
+      }
+    } catch (error) {
+      console.error('操作失败:', error)
+      ElMessage.error('操作失败')
+    } finally {
+      submitting.value = false
+    }
+  })
+}
+
+// 页面加载
+onMounted(() => {
+  loadAccountGroupList()
+  
+  // 检查是否有id参数,有则是编辑模式
+  const id = route.query.id
+  if (id) {
+    isEdit.value = true
+    editId.value = id
+    loadDetail(id)
+  } else {
+    // 新增模式,初始化默认规则
+    form.rules = createFirstDefaultRule()
+  }
+})
+</script>
+
+<style scoped>
+.rules-container {
+  max-height: 500px;
+  overflow-y: auto;
+}
+
+
+
+.rule-item {
+  border: 1px solid #e5e7eb;
+}
+</style>

+ 532 - 0
portal/src/views/ad-auto-conf/index.vue

@@ -0,0 +1,532 @@
+<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-blue-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
+              <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
+            </svg>
+            广告自动划配置管理
+          </h2>
+          <el-button type="primary" @click="handleAdd">
+            <el-icon><Plus /></el-icon>
+            新增配置
+          </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-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">{{ pagination.total }}</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">{{ confList.length }}</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="M5 13l4 4L19 7" />
+              </svg>
+            </div>
+          </div>
+        </div>
+        <div class="bg-gradient-to-br from-purple-50 to-purple-100 rounded-lg p-4 border border-purple-200">
+          <div class="flex items-center justify-between">
+            <div>
+              <p class="text-sm text-purple-600 mb-1">总页数</p>
+              <p class="text-2xl font-bold text-purple-700">{{ totalPages }}</p>
+            </div>
+            <div class="p-3 bg-purple-200 rounded-lg">
+              <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 text-purple-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
+                <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
+              </svg>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <!-- 搜索筛选 -->
+      <div class="mb-6 p-4 bg-gray-50 rounded-lg">
+        <el-form :inline="true" :model="queryForm" class="flex flex-wrap gap-4">
+          <el-form-item label="工号">
+            <el-input v-model="queryForm.jobNumber" placeholder="请输入工号" clearable style="width: 200px" />
+          </el-form-item>
+          <el-form-item label="广告组ID">
+            <el-input v-model.number="queryForm.adGroupId" placeholder="请输入广告组ID" clearable style="width: 200px" />
+          </el-form-item>
+          <el-form-item>
+            <el-button type="primary" @click="handleQuery">查询</el-button>
+            <el-button @click="handleReset">重置</el-button>
+          </el-form-item>
+        </el-form>
+      </div>
+
+      <!-- 数据表格 -->
+      <el-table
+        :data="confList"
+        style="width: 100%"
+        v-loading="loading"
+        stripe
+      >
+        <el-table-column type="expand">
+          <template #default="{ row }">
+            <div class="px-6 py-3">
+              <el-table :data="parseRules(row.rules)" border size="small">
+                <el-table-column label="广告" width="90">
+                  <template #default="{ row: r }">
+                    <el-tag size="small" :type="r.newAd === 1 ? 'success' : 'warning'">{{ r.newAd === 1 ? '新广告' : '老广告' }}</el-tag>
+                  </template>
+                </el-table-column>
+                <el-table-column label="规则类型" width="110">
+                  <template #default="{ row: r }">
+                    <el-tag size="small" :type="r.type === 1 ? 'primary' : 'info'">{{ r.type === 1 ? 'ROI规则' : '粉价规则' }}</el-tag>
+                  </template>
+                </el-table-column>
+                <el-table-column label="最小成本" prop="minCost" width="100" />
+                <el-table-column label="最小ROI" width="100">
+                  <template #default="{ row: r }">
+                    {{ r.type === 1 ? r.minIor : '-' }}
+                  </template>
+                </el-table-column>
+                <el-table-column label="最大粉价" width="100">
+                  <template #default="{ row: r }">
+                    {{ r.type === 2 ? r.scanMaxCost : '-' }}
+                  </template>
+                </el-table-column>
+              </el-table>
+            </div>
+          </template>
+        </el-table-column>
+        <el-table-column prop="id" label="ID" width="80" />
+        <el-table-column prop="ruleName" label="规则名称" width="120" />
+        <el-table-column prop="adGroupId" label="广告组ID" width="120">
+          <template #default="{ row }">
+            <span>{{ row.adGroupId === 0 ? '全部' : row.adGroupId }}</span>
+          </template>
+        </el-table-column>
+        <el-table-column prop="newAdDays" label="新广告天数" width="120" />
+        <el-table-column label="规则数量" width="100">
+          <template #default="{ row }">
+            <el-tag @click="handleEdit(row)" size="small" type="info">{{ getRulesCount(row.rules) }} 条</el-tag>
+          </template>
+        </el-table-column>
+        <el-table-column label="创建时间" width="180">
+          <template #default="{ row }">
+            {{ formatDate(row.createTime) }}
+          </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="260" fixed="right">
+          <template #default="{ row }">
+            <el-button type="primary" size="small" link @click="handleEdit(row)">编辑</el-button>
+            <el-button type="warning" size="small" link @click="handleExecute(row)">执行规则</el-button>
+            <el-button type="primary" size="small" link @click="handleShowLog(row)">执行日志</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <div class="mt-6 flex justify-end">
+        <el-pagination
+          v-model:current-page="pagination.page"
+          v-model:page-size="pagination.pageSize"
+          :page-sizes="[10, 20, 50, 100]"
+          :total="pagination.total"
+          layout="total, sizes, prev, pager, next, jumper"
+          @size-change="handleSizeChange"
+          @current-change="handleCurrentChange"
+        />
+      </div>
+    </div>
+
+  </div>
+
+  <!-- 执行日志弹窗 -->
+  <el-dialog
+    v-model="logDialog.visible"
+    title="执行日志"
+    width="860px"
+    destroy-on-close
+  >
+    <el-table
+      :data="logList"
+      v-loading="logLoading"
+      border
+      size="small"
+      style="width: 100%"
+      row-key="id"
+      lazy
+      :load="loadChildLog"
+      :tree-props="{ children: 'children', hasChildren: 'hasChildren' }"
+    >
+      <el-table-column prop="id" label="ID" width="80" />
+      <el-table-column prop="parentLogId" label="父日志ID" width="80" />
+      <el-table-column prop="adPlanConfId" label="广告计划ID" width="80" >
+          <template #default="{ row }">
+            <el-button v-if="row.adPlanConfId > 0" type="primary" size="small" link @click="handleShowDetail(row)">
+              {{ row.adPlanConfId }}
+            </el-button>
+            <span v-else>-</span>
+          </template>
+        </el-table-column>
+      <el-table-column label="执行时间" width="170">
+        <template #default="{ row }">
+          {{ formatDate(row.executeTime) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="成功时间" width="170">
+        <template #default="{ row }">
+          {{ formatDate(row.successTime) }}
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" width="90">
+        <template #default="{ row }">
+          <el-tag :type="getStatusTagType(row.executeStatus)" size="small">
+            {{ getStatusText(row.executeStatus) }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="总广告数" prop="adCount" width="90" />
+      <el-table-column label="暂停数" prop="suspendCount" width="80" />
+      <el-table-column label="执行结果" prop="executeResult" min-width="160" show-overflow-tooltip />
+      <el-table-column label="暂停的计划" prop="suspendAdGroupIds" width="120" show-overflow-tooltip/>
+    </el-table>
+    <div class="mt-4 flex justify-end">
+      <el-pagination
+        v-model:current-page="logPagination.page"
+        v-model:page-size="logPagination.pageSize"
+        :page-sizes="[10, 20, 50]"
+        :total="logPagination.total"
+        layout="total, sizes, prev, pager, next"
+        @size-change="handleLogSizeChange"
+        @current-change="handleLogPageChange"
+      />
+    </div>
+  </el-dialog>
+</template>
+
+<script setup>
+import { ref, reactive, computed, onMounted } from 'vue'
+import { useRouter } from 'vue-router'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import { Plus } from '@element-plus/icons-vue'
+import {
+  getAdAutoConfList,
+  deleteAdAutoConf,
+  batchDeleteAdAutoConf,
+  executeAdAutoConf,
+  getExecuteLogList,
+  getChildLog
+} from '@/api/adAutoConf'
+
+// 响应式数据
+const loading = ref(false)
+const confList = ref([])
+const selectedRows = ref([])
+const router = useRouter()
+
+// 查询表单
+const queryForm = reactive({
+  jobNumber: '',
+  adGroupId: null
+})
+
+// 分页数据
+const pagination = reactive({
+  page: 1,
+  pageSize: 10,
+  total: 0
+})
+
+// 计算属性
+const totalPages = computed(() => {
+  return Math.ceil(pagination.total / pagination.pageSize)
+})
+
+// 获取规则数量
+const parseRules = (rulesJson) => {
+  if (!rulesJson) return []
+  try {
+    const rules = JSON.parse(rulesJson)
+    return Array.isArray(rules) ? rules : []
+  } catch (e) {
+    return []
+  }
+}
+
+const getRulesCount = (rulesJson) => {
+  if (!rulesJson) return 0
+  try {
+    const rules = JSON.parse(rulesJson)
+    return Array.isArray(rules) ? rules.length : 0
+  } catch (e) {
+    return 0
+  }
+}
+
+const getStatusText = (status) => {
+  switch (status) {
+    case 1:
+      return '成功'
+    case 2:
+      return '失败'
+    default:
+      return '-'
+  }
+}
+
+const getStatusTagType = (status) => {
+  switch (status) {
+    case 1:
+      return 'success'
+    case 2:
+      return 'danger'
+    default:
+      return ''
+  }
+}
+
+// 格式化日期
+const formatDate = (dateStr) => {
+  if (!dateStr) return '-'
+  const date = new Date(dateStr)
+  return date.toLocaleString('zh-CN', {
+    year: 'numeric',
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit',
+    second: '2-digit'
+  })
+}
+
+const handleShowDetail = (row) => {
+  router.push(`/ad-plan-conf/detail?id=${row.adPlanConfId}`)
+}
+
+// 加载列表
+const loadList = async () => {
+  loading.value = true
+  try {
+    const params = {
+      page: pagination.page,
+      size: pagination.pageSize
+    }
+    if (queryForm.jobNumber) {
+      params.jobNumber = queryForm.jobNumber
+    }
+    if (queryForm.adGroupId !== null && queryForm.adGroupId !== '') {
+      params.adGroupId = queryForm.adGroupId
+    }
+    const response = await getAdAutoConfList(params)
+    if (response.success) {
+      confList.value = response.data.records || []
+      pagination.total = response.data.total || 0
+    } else {
+      ElMessage.error(response.message || '加载列表失败')
+    }
+  } catch (error) {
+    console.error('加载列表失败:', error)
+    ElMessage.error('加载列表失败')
+  } finally {
+    loading.value = false
+  }
+}
+
+// 查询
+const handleQuery = () => {
+  pagination.page = 1
+  loadList()
+}
+
+// 重置
+const handleReset = () => {
+  queryForm.jobNumber = ''
+  queryForm.adGroupId = null
+  handleQuery()
+}
+
+// 选择变化
+const handleSelectionChange = (selection) => {
+  selectedRows.value = selection
+}
+
+// 新增 - 跳转到新增页面
+const handleAdd = () => {
+  router.push('/ad-auto-conf/edit')
+}
+
+// 编辑 - 跳转到编辑页面
+const handleEdit = (row) => {
+  router.push(`/ad-auto-conf/edit?id=${row.id}`)
+}
+
+// 执行规则
+const handleExecute = (row) => {
+  ElMessageBox.confirm('确定要执行该规则吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(async () => {
+    executeAdAutoConf(row.id).then(response => {
+      if (response.success) {
+        ElMessage.success(response.message || '执行成功')
+      } else {
+        ElMessage.error(response.message || '执行失败')
+      }
+    })
+  }).catch(() => {})
+}
+
+// 删除
+const handleDelete = (row) => {
+  ElMessageBox.confirm('确定要删除该配置吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(async () => {
+    try {
+      const response = await deleteAdAutoConf(row.id)
+      if (response.success) {
+        ElMessage.success('删除成功')
+        loadList()
+      } else {
+        ElMessage.error(response.message || '删除失败')
+      }
+    } catch (error) {
+      console.error('删除失败:', error)
+      ElMessage.error('删除失败')
+    }
+  }).catch(() => {})
+}
+
+// 批量删除
+const handleBatchDelete = () => {
+  ElMessageBox.confirm(`确定要删除选中的 ${selectedRows.value.length} 个配置吗?`, '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(async () => {
+    try {
+      const ids = selectedRows.value.map(item => item.id)
+      const response = await batchDeleteAdAutoConf(ids)
+      if (response.success) {
+        ElMessage.success('批量删除成功')
+        loadList()
+      } else {
+        ElMessage.error(response.message || '批量删除失败')
+      }
+    } catch (error) {
+      console.error('批量删除失败:', error)
+      ElMessage.error('批量删除失败')
+    }
+  }).catch(() => {})
+}
+
+// 分页大小改变
+const handleSizeChange = (newSize) => {
+  pagination.pageSize = newSize
+  pagination.page = 1
+  loadList()
+}
+
+// 当前页改变
+const handleCurrentChange = (newPage) => {
+  pagination.page = newPage
+  loadList()
+}
+
+// 执行日志弹窗
+const logDialog = reactive({ visible: false, confId: null })
+const logLoading = ref(false)
+const logList = ref([])
+const logPagination = reactive({ page: 1, pageSize: 10, total: 0 })
+
+// 懒加载子日志
+const loadChildLog = async (row, treeNode, resolve) => {
+  try {
+    const response = await getChildLog({
+      confId: logDialog.confId,
+      parentId: row.id
+    })
+    if (response.success) {
+      resolve(response.data || [])
+    } else {
+      resolve([])
+    }
+  } catch (e) {
+    resolve([])
+  }
+}
+
+// 加载执行日志
+const loadLogList = async () => {
+  logLoading.value = true
+  try {
+    const response = await getExecuteLogList({
+      page: logPagination.page,
+      size: logPagination.pageSize,
+      confId: logDialog.confId
+    })
+    if (response.success) {
+      logList.value = (response.data.records || []).map(item => ({ ...item, hasChildren: true }))
+      logPagination.total = response.data.total || 0
+    } else {
+      ElMessage.error(response.message || '加载日志失败')
+    }
+  } catch (error) {
+    console.error('加载日志失败:', error)
+    ElMessage.error('加载日志失败')
+  } finally {
+    logLoading.value = false
+  }
+}
+
+// 显示执行日志
+const handleShowLog = (row) => {
+  logDialog.confId = row.id
+  logDialog.visible = true
+  logPagination.page = 1
+  loadLogList()
+}
+
+// 日志分页大小改变
+const handleLogSizeChange = (newSize) => {
+  logPagination.pageSize = newSize
+  logPagination.page = 1
+  loadLogList()
+}
+
+// 日志当前页改变
+const handleLogPageChange = (newPage) => {
+  logPagination.page = newPage
+  loadLogList()
+}
+
+// 页面加载时获取列表
+onMounted(() => {
+  loadList()
+})
+</script>
+
+<style scoped>
+</style>

+ 26 - 0
portal/src/views/ad-plan-conf/index.vue

@@ -66,6 +66,13 @@
             {{ getAssetTypeText(row.assetType) }}
           </template>
            </el-table-column>
+        <el-table-column prop="assetModel" align="center" label="资产模型" width="100" >
+          <template #default="{ row }">
+            <el-tag :type="row.assetModel === 1 ? 'success' : 'primary'">
+              {{ getAssetModelText(row.assetModel) }}
+            </el-tag>
+          </template>
+        </el-table-column>
         <el-table-column prop="dramaAmount" align="center" label="资产数量" width="120" >
           <template #default="{ row }">
             {{ `${row.dramaAmount}  |  ${row.fictionAmount}` }}
@@ -280,6 +287,12 @@
             />
           </el-select>
         </el-form-item> -->
+        <el-form-item label="资产模型" prop="assetModel" v-if="formData.adType === 2">
+          <el-radio-group v-model="formData.assetModel">
+            <el-radio label="随机" :value="1" />
+            <el-radio label="最新" :value="2" />
+          </el-radio-group>
+        </el-form-item>
         <el-form-item label="资产类型" prop="assetType" v-if="formData.adType === 2">
           <el-radio-group v-model="formData.assetType">
             <el-radio label="短剧" :value="1" />
@@ -709,6 +722,7 @@ const formData = ref({
   dramaId: null,
   imageIds: '',
   assetType: 1,
+  assetModel: 1,
   dramaAmount: 0,
   fictionAmount: 0,
   pageIds: '',
@@ -768,6 +782,9 @@ const formRules = {
   assetType: [
     { required: true, message: '请选择资产类型', trigger: 'change' }
   ],
+  assetModel: [
+    { required: true, message: '请选择资产模型', trigger: 'change' }
+  ],
   dramaAmount: [
     { required: true, message: '请输入短剧每户几部剧', trigger: 'blur' }
   ],
@@ -830,6 +847,13 @@ const getAssetTypeText = (type) => {
   return typeMap[type] || '未知'
 }
 
+const getAssetModelText = (type) => {
+  const typeMap = {
+    1: '随机',
+    2: '最新'
+  }
+  return typeMap[type] || '未知'
+}
 const getAdTypeColor = (type) => {
   const colorMap = {
     1: 'success',
@@ -894,6 +918,7 @@ const handleCreate = () => {
     dramaId: null,
     dramaAmount: 0,
     assetType: 1,
+    assetModel: 1,
     fictionAmount: 0,
     imageIds: '',
     pageIds: '',
@@ -929,6 +954,7 @@ const handleEdit = (row) => {
     dramaId: row.dramaId,
     dramaAmount: row.dramaAmount || 0,
     assetType: row.assetType || 1,
+    assetModel: row.assetModel || 1,
     fictionAmount: row.fictionAmount || 0,
     imageIds: row.imageIds || '',
     pageIds: row.pageIds || '',

+ 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_asset_fiction";
+    public final static String TABLE_NAMES = "ad_auto_execute_log";
     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")

+ 26 - 0
src/main/java/com/moka/gdtauto/common/AutoPlanRule.java

@@ -0,0 +1,26 @@
+package com.moka.gdtauto.common;
+
+import java.math.BigDecimal;
+
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+
+@Data
+public class AutoPlanRule {
+    // 规则类型:1 roi 2 粉价
+    @NotNull
+    private Integer type;
+    // 广告类型:1 新广告 2 老广告
+    @NotNull
+    private Integer newAd;
+    // 最小成本(元)
+    @NotNull
+    private Integer minCost;
+    // ROI规则:最小ROI
+    @NotNull
+    private BigDecimal minIor;
+    // 粉价规则:最大粉价(元)
+    @NotNull
+    private Integer scanMaxCost;
+}

+ 179 - 0
src/main/java/com/moka/gdtauto/controller/AdAutoConfController.java

@@ -0,0 +1,179 @@
+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;
+import com.moka.gdtauto.common.AutoPlanRule;
+import com.moka.gdtauto.common.Result;
+import com.moka.gdtauto.entity.AdAutoConf;
+import com.moka.gdtauto.entity.SysUser;
+import com.moka.gdtauto.mapper.AdAutoConfMapper;
+import com.moka.gdtauto.service.AdAutoConfService;
+import com.moka.gdtauto.service.AdAutoExecutor;
+
+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.springframework.util.StringUtils;
+import org.springframework.web.bind.annotation.*;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * <p>
+ * 广告自动划配置表 前端控制器
+ * </p>
+ *
+ * @author mokamvp
+ * @since 2026-03-12
+ */
+@Tag(name = "广告自动划配置管理", description = "广告自动划配置的增删改查")
+@Slf4j
+@RestController
+@RequestMapping("/api/ad-auto-conf")
+@RequiredArgsConstructor
+public class AdAutoConfController {
+
+    private final AdAutoConfService adAutoConfService;
+    private final AdAutoConfMapper adAutoConfMapper;
+    private final AdAutoExecutor adAutoExecutor;
+
+    /**
+     * 分页查询广告自动划配置列表
+     */
+    @Operation(summary = "分页查询", description = "根据条件分页查询广告自动划配置列表")
+    @GetMapping("/list")
+    public Result<Page<AdAutoConf>> list(
+            @Parameter(description = "页码") @RequestParam(defaultValue = "1") Long page,
+            @Parameter(description = "每页数量") @RequestParam(defaultValue = "10") Long size,
+            @Parameter(description = "工号") @RequestParam(required = false) String jobNumber,
+            @Parameter(description = "广告组ID") @RequestParam(required = false) Long adGroupId) {
+        try {
+            Page<AdAutoConf> pageParam = new Page<>(page, size);
+            LambdaQueryWrapper<AdAutoConf> wrapper = new LambdaQueryWrapper<>();
+            wrapper.like(StringUtils.hasText(jobNumber), AdAutoConf::getJobNumber, jobNumber)
+                    .eq(adGroupId != null, AdAutoConf::getAdGroupId, adGroupId)
+                    .orderByDesc(AdAutoConf::getCreateTime);
+            Page<AdAutoConf> result = adAutoConfService.page(pageParam, wrapper);
+            return Result.success(result);
+        } catch (Exception e) {
+            log.error("查询广告自动划配置列表失败", e);
+            return Result.fail("查询广告自动划配置列表失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 根据ID查询广告自动划配置详情
+     */
+    @Operation(summary = "查询详情", description = "根据ID查询广告自动划配置详情")
+    @GetMapping("/{id}")
+    public Result<AdAutoConf> getById(
+            @Parameter(description = "配置ID", required = true)
+            @PathVariable Long id) {
+        try {
+            AdAutoConf conf = adAutoConfService.getById(id);
+            if (conf == null) {
+                return Result.fail("广告自动划配置不存在");
+            }
+            return Result.success(conf);
+        } catch (Exception e) {
+            log.error("查询广告自动划配置详情失败,ID: {}", id, e);
+            return Result.fail("查询广告自动划配置详情失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 新增或更新广告自动划配置
+     * 使用 INSERT INTO ... ON DUPLICATE KEY UPDATE 实现
+     */
+    @Operation(summary = "保存配置", description = "新增或更新广告自动划配置,jobNumber+adGroupId唯一")
+    @PostMapping
+    public Result<AdAutoConf> save(
+            @RequestBody AdAutoConf adAutoConf,
+            @AuthUser SysUser sysUser) {
+        try {
+            adAutoConf.setJobNumber(sysUser.getJobNumber());
+            adAutoConf.setCreateTime(LocalDateTime.now());
+            adAutoConf.setUpdateTime(LocalDateTime.now());
+            List<AutoPlanRule> rules = adAutoExecutor.parseRules(adAutoConf.getRules());
+            adAutoConf.setRules(JSON.toJSONString(rules));
+            boolean success = adAutoConfMapper.saveOrUpdate(adAutoConf);
+            if (success) {
+                return Result.success(adAutoConf, "保存成功");
+            } else {
+                return Result.fail("保存失败");
+            }
+        } catch (Exception e) {
+            log.error("保存广告自动划配置失败", e);
+            return Result.fail("保存广告自动划配置失败: " + e.getMessage());
+        }
+    }
+
+    //编辑
+    @Operation(summary = "编辑配置", description = "根据ID编辑广告自动划配置")
+    @PostMapping("/update")
+    public Result<AdAutoConf> update(
+            @RequestBody AdAutoConf adAutoConf) {
+        try {
+            adAutoConf.setUpdateTime(LocalDateTime.now());
+             List<AutoPlanRule> rules = adAutoExecutor.parseRules(adAutoConf.getRules());
+            adAutoConf.setRules(JSON.toJSONString(rules));
+            boolean success = adAutoConfService.updateById(adAutoConf);
+            if (success) {
+                return Result.success(adAutoConf, "编辑成功");
+            } else {
+                return Result.fail("编辑失败");
+            }
+        } catch (Exception e) {
+            log.error("编辑广告自动划配置失败", e);
+            return Result.fail("编辑广告自动划配置失败: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 删除广告自动划配置
+     */
+    @Operation(summary = "删除配置", description = "根据ID删除广告自动划配置")
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(
+            @Parameter(description = "配置ID", required = true)
+            @PathVariable Long id) {
+        try {
+            AdAutoConf conf = adAutoConfService.getById(id);
+            if (conf == null) {
+                return Result.fail("广告自动划配置不存在");
+            }
+            boolean success = adAutoConfService.removeById(id);
+            if (success) {
+                return Result.success("删除成功");
+            } else {
+                return Result.fail("删除失败");
+            }
+        } catch (Exception e) {
+            log.error("删除广告自动划配置失败,ID: {}", id, e);
+            return Result.fail("删除广告自动划配置失败: " + e.getMessage());
+        }
+    }
+
+    @Operation(summary = "执行配置", description = "根据ID执行广告自动划配置")
+    @PostMapping("/execute")
+    public Result<Void> execute(
+            @Parameter(description = "配置ID", required = true)
+            @RequestParam Long id) {
+        try {
+            AdAutoConf conf = adAutoConfService.getById(id);
+            if (conf == null) {
+                return Result.fail("广告自动划配置不存在");
+            }
+            adAutoExecutor.asyncExecute(conf);
+            return Result.success("执行中,请稍候");
+        } catch (Exception e) {
+            log.error("执行广告自动划配置失败,ID: {}", id, e);
+            return Result.fail("执行广告自动划配置失败: " + e.getMessage());
+        }
+    }
+}

+ 71 - 0
src/main/java/com/moka/gdtauto/controller/AdAutoExecuteLogController.java

@@ -0,0 +1,71 @@
+package com.moka.gdtauto.controller;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.moka.gdtauto.common.Result;
+import com.moka.gdtauto.entity.AdAutoExecuteLog;
+import com.moka.gdtauto.service.AdAutoExecuteLogService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import java.util.List;
+
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * <p>
+ * 广告规则执行日志表 前端控制器
+ * </p>
+ *
+ * @author mokamvp
+ * @since 2026-03-12
+ */
+@Slf4j
+@RequiredArgsConstructor
+@RestController
+@RequestMapping("/api/execute-log")
+public class AdAutoExecuteLogController {
+
+    private final AdAutoExecuteLogService adAutoExecuteLogService;
+
+    /**
+     * 分页查询执行日志
+     */
+    @GetMapping("/list")
+    public Result<Page<AdAutoExecuteLog>> list(
+            @RequestParam(defaultValue = "1") Long page,
+            @RequestParam(defaultValue = "10") Long size,
+            @RequestParam(required = true) Long confId) {
+        try {
+            LambdaQueryWrapper<AdAutoExecuteLog> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(AdAutoExecuteLog::getConfId, confId);
+            wrapper.eq(AdAutoExecuteLog::getParentLogId, 0);
+            wrapper.orderByDesc(AdAutoExecuteLog::getId);
+            Page<AdAutoExecuteLog> result = adAutoExecuteLogService.page(new Page<>(page, size), wrapper);
+            return Result.success(result);
+        } catch (Exception e) {
+            log.error("查询执行日志失败", e);
+            return Result.fail("查询执行日志失败: " + e.getMessage());
+        }
+    }
+
+    @GetMapping("/getChildLog")
+    public Result<List<AdAutoExecuteLog>> getChildLog(
+            @RequestParam(required = true) Long confId,
+            @RequestParam(required = true) Long parentId) {
+        try {
+            LambdaQueryWrapper<AdAutoExecuteLog> wrapper = new LambdaQueryWrapper<>();
+            wrapper.eq(AdAutoExecuteLog::getConfId, confId);
+            wrapper.eq(AdAutoExecuteLog::getParentLogId, parentId);
+            wrapper.orderByDesc(AdAutoExecuteLog::getId);
+            return Result.success(adAutoExecuteLogService.list(wrapper));
+        } catch (Exception e) {
+            log.error("查询执行日志失败", e);
+            return Result.fail("查询执行日志失败: " + e.getMessage());
+        }
+    }
+
+}

+ 30 - 8
src/main/java/com/moka/gdtauto/controller/AdPlanConfController.java

@@ -185,31 +185,53 @@ public class AdPlanConfController {
         Integer fictionAmount = request.getFictionAmount();
         //获取短剧 relationships.size * dramaAmount
         Integer assetType = request.getAssetType();
+        Integer assetModel = request.getAssetModel();
         List<TencentAsset> assets = new ArrayList<>();
         if (assetType == 1 && dramaAmount > 0) {
-            List<TencentAssetDrama> dramas = tencentAssetDramaMapper.selectList(new LambdaQueryWrapper<TencentAssetDrama>().eq(TencentAssetDrama::getJobNumber, jobNumber).last("order by rand() limit " + dramaAmount * relationships.size()));
+            List<TencentAssetDrama> dramas = new ArrayList<>();
+            if (assetModel == 1) {
+                dramas = tencentAssetDramaMapper.selectList(new LambdaQueryWrapper<TencentAssetDrama>().eq(TencentAssetDrama::getJobNumber, jobNumber).last("order by rand() limit " + dramaAmount * relationships.size()));
+            } else {
+                dramas =  tencentAssetDramaMapper.randomNewAsset(jobNumber, dramaAmount * relationships.size() * 2, dramaAmount * relationships.size());
+            }
+
             if (dramas.size() < dramaAmount * relationships.size()) {
                 throw new IllegalArgumentException("短剧数量不足,请在短剧资产管理同步");
             }
             assets = dramas.stream().map(drama -> new TencentAsset(1, drama.getMarketingAssetId(), drama.getMarketingAssetName())).toList();
         }
         if (assetType == 2 && fictionAmount > 0) {
-            List<TencentAssetFiction> fictions = tencentAssetFictionMapper.selectList(new LambdaQueryWrapper<TencentAssetFiction>().eq(TencentAssetFiction::getJobNumber, jobNumber).last("order by rand() limit " + dramaAmount * relationships.size()));
-            if (fictions.size() < dramaAmount * relationships.size()) {
+            List<TencentAssetFiction> fictions = new ArrayList<>();
+            if (assetModel == 1) {
+                fictions = tencentAssetFictionMapper.selectList(new LambdaQueryWrapper<TencentAssetFiction>().eq(TencentAssetFiction::getJobNumber, jobNumber).last("order by rand() limit " + fictionAmount * relationships.size()));
+            } else {
+                fictions = tencentAssetFictionMapper.randomNewAsset(jobNumber, fictionAmount * relationships.size() * 2, fictionAmount * relationships.size());
+            }
+            if (fictions.size() < fictionAmount * relationships.size()) {
                 throw new IllegalArgumentException("小说数量不足,请在小说资产管理同步");
             }
             assets = fictions.stream().map(fiction -> new TencentAsset(2, fiction.getMarketingAssetId(), fiction.getMarketingAssetName())).toList();
         }
         if (assetType == 3) {
             if (dramaAmount > 0) {
-                List<TencentAssetDrama> dramas = tencentAssetDramaMapper.selectList(new LambdaQueryWrapper<TencentAssetDrama>().eq(TencentAssetDrama::getJobNumber, jobNumber).last("order by rand() limit " + dramaAmount * relationships.size()));
+                List<TencentAssetDrama> dramas = new ArrayList<>();
+                if (assetModel == 1) {
+                    dramas = tencentAssetDramaMapper.selectList(new LambdaQueryWrapper<TencentAssetDrama>().eq(TencentAssetDrama::getJobNumber, jobNumber).last("order by rand() limit " + dramaAmount * relationships.size()));
+                } else {
+                    dramas = tencentAssetDramaMapper.randomNewAsset(jobNumber, dramaAmount * relationships.size() * 2, dramaAmount * relationships.size());
+                }
                 if (dramas.size() < dramaAmount * relationships.size()) {
                     throw new IllegalArgumentException("短剧数量不足,请在短剧资产管理同步");
                 }
                 assets.addAll(dramas.stream().map(drama -> new TencentAsset(1, drama.getMarketingAssetId(), drama.getMarketingAssetName())).toList());
             }
             if (fictionAmount > 0) {
-                List<TencentAssetFiction> fictions = tencentAssetFictionMapper.selectList(new LambdaQueryWrapper<TencentAssetFiction>().eq(TencentAssetFiction::getJobNumber, jobNumber).last("order by rand() limit " + fictionAmount * relationships.size()));
+                List<TencentAssetFiction> fictions = new ArrayList<>();
+                if (assetModel == 1) {
+                    fictions = tencentAssetFictionMapper.selectList(new LambdaQueryWrapper<TencentAssetFiction>().eq(TencentAssetFiction::getJobNumber, jobNumber).last("order by rand() limit " + fictionAmount * relationships.size()));
+                } else {
+                    fictions = tencentAssetFictionMapper.randomNewAsset(jobNumber, fictionAmount * relationships.size() * 2, fictionAmount * relationships.size());
+                }
                 if (fictions.size() < fictionAmount * relationships.size()) {
                     throw new IllegalArgumentException("小说数量不足,请在小说资产管理同步");
                 }
@@ -333,10 +355,10 @@ public class AdPlanConfController {
             for (AdPlan plan : plans) {
                 plan.setAdPlanConfId(confId);
             }
-            adPlanConfService.generatePlan(plans);
-
+            adPlanConfService.savePlanAndCreative(plans);
             // 异步对接腾讯接口
-            adPlanConfService.asyncTencentInterface(plans);
+            AdPlanConf savedConf = adPlanConfService.getById(confId);
+            adPlanConfService.asyncTencentInterface(savedConf, plans);
 
             return Result.success(adPlanConf, "创建广告计划成功;异步执行接口对接中");
         } catch (Exception e) {

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

@@ -46,6 +46,8 @@ public class CreateAdPlanConfRequest {
     private String wxCorpid;
     @Schema(description = "资产类型", example = "1")
     private Integer assetType;
+    @Schema(description = "资产模型", example = "1")
+    private Integer assetModel;
     @Schema(description = "短剧ID", example = "1")
     private Long dramaId;
     @Schema(description = "短剧每户几部剧", example = "1")

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

@@ -47,6 +47,8 @@ public class UpdateAdPlanConfRequest {
 
     @Schema(description = "资产类型", example = "1")
     private Integer assetType;
+    @Schema(description = "资产模型", example = "1")
+    private Integer assetModel;
     @Schema(description = "短剧ID", example = "1")
     private Long dramaId;
     @Schema(description = "短剧每户几部剧", example = "1")

+ 72 - 0
src/main/java/com/moka/gdtauto/entity/AdAutoConf.java

@@ -0,0 +1,72 @@
+package com.moka.gdtauto.entity;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Getter;
+import lombok.Setter;
+import lombok.ToString;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 广告自动划配置表
+ * </p>
+ *
+ * @author mokamvp
+ * @since 2026-03-12
+ */
+@Getter
+@Setter
+@ToString
+@TableName("ad_auto_conf")
+public class AdAutoConf implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 工号
+     */
+    private String jobNumber;
+
+    /**
+     * 规则名称
+     */
+    private String ruleName;
+
+    /**
+     * 新广告天数定义
+     */
+    private Integer newAdDays;
+
+    /**
+     * 广告组ID
+     */
+    private Long adGroupId;
+
+    /**
+     * com.moka.gdtauto.common.AutoPlanRule数组
+     * 规则json
+     */
+    @NotEmpty
+    @NotNull
+    private String rules;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

+ 78 - 0
src/main/java/com/moka/gdtauto/entity/AdAutoExecuteLog.java

@@ -0,0 +1,78 @@
+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-12
+ */
+@Getter
+@Setter
+@ToString
+@TableName("ad_auto_execute_log")
+public class AdAutoExecuteLog implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+    private Long parentLogId;
+
+    /**
+     * 配置ID
+     */
+    private Long confId;
+
+
+    private Long adPlanConfId;
+
+    /**
+     * 执行时间
+     */
+    private LocalDateTime executeTime;
+
+    /**
+     * 成功时间
+     */
+    private LocalDateTime successTime;
+
+    /**
+     * 执行结果
+     */
+    private String executeResult;
+
+    /**
+     * 执行状态:1-成功,2-失败
+     */
+    private Integer executeStatus;
+
+    /**
+     * 总广告数
+     */
+    private Long adCount;
+
+    /**
+     * 暂停数
+     */
+    private Integer suspendCount;
+
+    /**
+     * 暂停的广告组ID列表 ID-NAME;
+     */
+    private String suspendAdGroupIds;
+}

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

@@ -131,6 +131,7 @@ public class AdPlan {
 
    /**
     * 花费
+    * 单位:分
     */
    private Long cost;
    /**
@@ -167,6 +168,10 @@ public class AdPlan {
     */
    private BigDecimal repIncomeRoi1;
 
+   //加企业微信客服成本。计算公式:花费/加企业微信客服人数
+   // 单位:分
+   private Long scanFollowUserCost;
+
     /**
      * 创建时间
      */

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

@@ -33,6 +33,7 @@ public class AdPlanConf {
      * 账号组ID
      */
     private Long accountGroupId;
+    private Integer accountCount;
 
     /**
      * 广告类型:1-企微,2-ROI
@@ -68,10 +69,17 @@ public class AdPlanConf {
      */
     private String wxCorpid;
 
+    private Integer planCount;
+    private Integer planSuccessCount;
+    private Integer creativeCount;
+    private Integer creativeSuccessCount;
+
     /** 
      * 资产类型:1-短剧,2-小说 3 混合
      */
     private Integer assetType;
+    // 资产模型:1-随机 2 最新
+    private Integer assetModel;
 
     /**
      * 短剧ID

+ 22 - 0
src/main/java/com/moka/gdtauto/mapper/AdAutoConfMapper.java

@@ -0,0 +1,22 @@
+package com.moka.gdtauto.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.moka.gdtauto.entity.AdAutoConf;
+
+/**
+ * <p>
+ * 广告自动划配置表 Mapper 接口
+ * </p>
+ *
+ * @author mokamvp
+ * @since 2026-03-12
+ */
+public interface AdAutoConfMapper extends BaseMapper<AdAutoConf> {
+
+    /**
+     * 插入或更新配置
+     * 使用 INSERT INTO ... ON DUPLICATE KEY UPDATE
+     * 唯一键: job_number + ad_group_id
+     */
+    boolean saveOrUpdate(AdAutoConf adAutoConf);
+}

+ 16 - 0
src/main/java/com/moka/gdtauto/mapper/AdAutoExecuteLogMapper.java

@@ -0,0 +1,16 @@
+package com.moka.gdtauto.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.moka.gdtauto.entity.AdAutoExecuteLog;
+
+/**
+ * <p>
+ * 广告规则执行日志表 Mapper 接口
+ * </p>
+ *
+ * @author mokamvp
+ * @since 2026-03-12
+ */
+public interface AdAutoExecuteLogMapper extends BaseMapper<AdAutoExecuteLog> {
+
+}

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

@@ -24,4 +24,6 @@ public interface TencentAssetDramaMapper extends BaseMapper<TencentAssetDrama> {
      * @return 影响行数
      */
     int batchInsertOrUpdate(@Param("dramas") List<TencentAssetDrama> dramas);
+
+    List<TencentAssetDrama> randomNewAsset(@Param("jobNumber") String jobNumber, @Param("best") int best, @Param("rand") int rand);
 }

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

@@ -24,4 +24,6 @@ public interface TencentAssetFictionMapper extends BaseMapper<TencentAssetFictio
      * @return 影响行数
      */
     int batchInsertOrUpdate(@Param("fictions") List<TencentAssetFiction> fictions);
+
+    List<TencentAssetFiction> randomNewAsset(@Param("jobNumber") String jobNumber, @Param("best") int best, @Param("rand") int rand);
 }

+ 16 - 0
src/main/java/com/moka/gdtauto/service/AdAutoConfService.java

@@ -0,0 +1,16 @@
+package com.moka.gdtauto.service;
+
+import com.moka.gdtauto.entity.AdAutoConf;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * 广告自动划配置表 服务类
+ * </p>
+ *
+ * @author mokamvp
+ * @since 2026-03-12
+ */
+public interface AdAutoConfService extends IService<AdAutoConf> {
+
+}

+ 16 - 0
src/main/java/com/moka/gdtauto/service/AdAutoExecuteLogService.java

@@ -0,0 +1,16 @@
+package com.moka.gdtauto.service;
+
+import com.moka.gdtauto.entity.AdAutoExecuteLog;
+import com.baomidou.mybatisplus.extension.service.IService;
+
+/**
+ * <p>
+ * 广告规则执行日志表 服务类
+ * </p>
+ *
+ * @author mokamvp
+ * @since 2026-03-12
+ */
+public interface AdAutoExecuteLogService extends IService<AdAutoExecuteLog> {
+
+}

+ 281 - 0
src/main/java/com/moka/gdtauto/service/AdAutoExecutor.java

@@ -0,0 +1,281 @@
+package com.moka.gdtauto.service;
+
+import com.alibaba.fastjson.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.moka.gdtauto.common.AutoPlanRule;
+import com.moka.gdtauto.config.RedisConfig;
+import com.moka.gdtauto.entity.AdAutoConf;
+import com.moka.gdtauto.entity.AdAutoExecuteLog;
+import com.moka.gdtauto.entity.AdPlan;
+import com.moka.gdtauto.entity.AdPlanConf;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.stream.Collectors;
+
+/**
+ * 广告巡检执行器
+ * 根据 AdAutoConf 配置的规则,对广告进行巡检并执行暂停操作
+ *
+ * @author moka
+ * @since 2026-03-12
+ */
+@Slf4j
+@Service
+@AllArgsConstructor
+public class AdAutoExecutor {
+
+    private final AdAutoExecuteLogService adAutoExecuteLogService;
+    private final AdPlanConfService adPlanConfService;
+    private final AdPlanDailyReportService adPlanDailyReportService;
+    private final AdPlanService adPlanService;
+
+    /**
+     * 执行广告巡检
+     *
+     * @param conf 巡检配置
+     */
+    @Async
+    public void asyncExecute(AdAutoConf conf) {
+        log.info("[AdAutoExecutor] 开始执行巡检,confId={}, jobNumber={}, adGroupId={}",
+                conf.getId(), conf.getJobNumber(), conf.getAdGroupId());
+
+        // 0. 创建执行日志
+        AdAutoExecuteLog executeLog = new AdAutoExecuteLog();
+        executeLog.setConfId(conf.getId());
+        executeLog.setParentLogId(0L);
+        executeLog.setAdPlanConfId(0L);
+        executeLog.setExecuteTime(LocalDateTime.now());
+        adAutoExecuteLogService.save(executeLog);
+
+        try {
+            doExecute(conf, executeLog.getId());
+        } catch (Exception e) {
+            log.error("[AdAutoExecutor] 巡检执行异常,confId={}", conf.getId(), e);
+            updateLogFailed(executeLog, "巡检异常: " + e.getMessage());
+        }
+    }
+
+    private void doExecute(AdAutoConf autoConf, Long parentExecuteLogId) {
+
+        // 解析规则
+        List<AutoPlanRule> rules = parseRules(autoConf.getRules());
+
+        // 1. 根据 jobNumber 查询 AdPlanConf 列表
+        List<AdPlanConf> adPlanConfs = adPlanConfService.lambdaQuery()
+                .eq(autoConf.getAdGroupId() > 0, AdPlanConf::getAccountGroupId, autoConf.getAdGroupId())
+                .eq(AdPlanConf::getJobNumber, autoConf.getJobNumber())
+                .in(AdPlanConf::getStatus, List.of(4, 5))
+                .eq(AdPlanConf::getDeleted, 0)
+                .eq(AdPlanConf::getSystemSuspendProcessStatus, 0)
+                .list();
+        if (adPlanConfs == null || adPlanConfs.isEmpty()) {
+            log.warn("[AdAutoExecutor] 未找到 AdPlanConf,jobNumber={}", autoConf.getJobNumber());
+            throw new RuntimeException("未找到关联广告计划配置");
+        }
+        log.info("[AdAutoExecutor] 找到 {} 个 AdPlanConf,jobNumber={}", adPlanConfs.size(), autoConf.getJobNumber());
+
+        for (AdPlanConf adPlanConf : adPlanConfs) {
+            processOneConf(autoConf, adPlanConf, rules, parentExecuteLogId);
+        }
+        log.info("[AdAutoExecutor] 巡检执行完成,confId={}", autoConf.getId());
+    }
+
+    /**
+     * 处理单个 AdPlanConf:同步日报 → 分页查询 AdPlan → 规则检测 → 并发暂停
+     *
+     * @param adPlanConf        当前广告计划配置
+     * @param rules             巡检规则列表
+     * @param newAdDays         新广告天数阈值
+     * @param allPlansToSuspend 收集所有待暂停广告(跨 conf 汇总用)
+     * @return
+     */
+    private void processOneConf(AdAutoConf autoConf,
+            AdPlanConf adPlanConf,
+            List<AutoPlanRule> rules,
+            Long parentExecuteLogId) {
+        long confId = adPlanConf.getId();
+        LocalDateTime now = LocalDateTime.now();
+        AdAutoExecuteLog executeLog = new AdAutoExecuteLog();
+        executeLog.setConfId(autoConf.getId());
+        executeLog.setAdPlanConfId(adPlanConf.getId());
+        executeLog.setParentLogId(parentExecuteLogId);
+        executeLog.setExecuteTime(LocalDateTime.now());
+        adAutoExecuteLogService.save(executeLog);
+        // 同步日报
+        try {
+            log.info("[AdAutoExecutor] 同步日报,confId={}", confId);
+            adPlanDailyReportService.syncReqDailyReport(confId, now, now);
+
+            // 分页查询当前 conf 下的 AdPlan,进行规则检测
+            long adCount = 0;
+            List<AdPlan> plansToSuspend = new ArrayList<>();
+            int pageNum = 1;
+            int pageSize = 100;
+            while (true) {
+                LambdaQueryWrapper<AdPlan> queryWrapper = new LambdaQueryWrapper<>();
+                queryWrapper.eq(AdPlan::getAdPlanConfId, confId)
+                        .isNotNull(AdPlan::getAdgroupId)
+                        .gt(AdPlan::getAdgroupId, 0)
+                        .eq(AdPlan::getDeleted, 0)
+                        .eq(AdPlan::getStatus, 4) // status=4 已成功的广告
+                        .orderByAsc(AdPlan::getId);
+                Page<AdPlan> page = adPlanService.page(new Page<>(pageNum, pageSize), queryWrapper);
+                List<AdPlan> plans = page.getRecords();
+
+                if (plans.isEmpty()) {
+                    break;
+                }
+                adCount += plans.size();
+
+                for (AdPlan plan : plans) {
+                    if (shouldSuspend(plan, rules, autoConf.getNewAdDays())) {
+                        plansToSuspend.add(plan);
+                    }
+                }
+
+                if (!page.hasNext()) {
+                    break;
+                }
+                pageNum++;
+            }
+
+            if (!plansToSuspend.isEmpty()) {
+                adPlanConfService.gdtSuspendByPlans(adPlanConf, plansToSuspend);
+            }
+
+            log.info("[AdAutoExecutor] confId={} 巡检:广告数={}, 需暂停数={}", confId, adCount, plansToSuspend.size());
+            executeLog.setAdCount(adCount);
+            executeLog.setSuspendCount(plansToSuspend.size());
+            executeLog.setExecuteStatus(1);
+            executeLog.setSuspendAdGroupIds(plansToSuspend.stream().map(x -> String.format("[%d-{%s}]", x.getAdgroupId(), x.getAdgroupName())).collect(Collectors.joining("\n")));
+            executeLog.setExecuteResult("成功");
+            executeLog.setSuccessTime(LocalDateTime.now());
+            adAutoExecuteLogService.updateById(executeLog);
+        } catch (Exception e) {
+            executeLog.setExecuteStatus(2);
+            executeLog.setExecuteResult(e.getMessage());
+            adAutoExecuteLogService.updateById(executeLog);
+        }
+
+    }
+
+    /**
+     * 判断广告是否应该被暂停
+     * 遍历所有规则,有任一规则满足条件则暂停
+     */
+    private boolean shouldSuspend(AdPlan plan, List<AutoPlanRule> rules, Integer newAdDays) {
+        for (AutoPlanRule rule : rules) {
+            if (matchRule(plan, rule, newAdDays)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    /**
+     * 单条规则匹配
+     * rule.type: 1-ROI规则, 2-粉价规则
+     * rule.newAd: 1-新广告, 2-老广告
+     * 
+     */
+    private boolean matchRule(AdPlan plan, AutoPlanRule rule, Integer newAdDays) {
+        // 判断新老广告
+        boolean isNewAd = isNewAd(plan, newAdDays);
+        if (rule.getNewAd() == 1 && !isNewAd) {
+            return false; // 规则针对新广告,但当前是老广告
+        }
+        if (rule.getNewAd() == 2 && isNewAd) {
+            return false; // 规则针对老广告,但当前是新广告
+        }
+
+        Long cost = Optional.ofNullable(plan.getCost()).orElse(0L);
+        Long constYuan = cost / 100;
+        Long scanFollowUserCost = Optional.ofNullable(plan.getScanFollowUserCost()).orElse(0L);
+        Long scanCostYuan = scanFollowUserCost / 100;
+
+        // 1 roi 2 粉价
+        if (rule.getType() == 1) {
+            if (rule.getMinCost() == 0) {
+                if (plan.getReqIncomeRoi1().compareTo(rule.getMinIor()) < 0) {
+                    return true;
+                }
+            } else {
+                if (constYuan > rule.getMinCost() && plan.getReqIncomeRoi1().compareTo(rule.getMinIor()) < 0) {
+                    return true;
+                }
+            }
+        } else if (rule.getType() == 2) {
+            // minCost == 0 表示不限制消耗门槛
+            if (rule.getMinCost() == 0) {
+                if (scanCostYuan > rule.getScanMaxCost()) {
+                    return true;
+                }
+            } else {
+                if (scanCostYuan > rule.getScanMaxCost() && constYuan > rule.getMinCost()) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+
+    /**
+     * 判断是否为新广告
+     * 新广告定义:成功时间 createTime 距今不超过 newAdDays 天
+     */
+    private boolean isNewAd(AdPlan plan, Integer newAdDays) {
+        LocalDateTime threshold = LocalDateTime.now().minusDays(newAdDays);
+        return plan.getCreateTime().isAfter(threshold);
+    }
+
+    /**
+     * 解析规则JSON
+     */
+    public List<AutoPlanRule> parseRules(String rulesJson) {
+        try {
+            List<AutoPlanRule> rules = JSON.parseArray(rulesJson, AutoPlanRule.class);
+            // rules排序 新广告在前 ROI在前 粉价在后,minCost大的在前,老广告在后
+            List<AutoPlanRule> newRules = new ArrayList<>();
+            List<AutoPlanRule> oldRules = new ArrayList<>();
+            rules.forEach(r -> {
+                if (r.getNewAd() == 1) {
+                    newRules.add(r);
+                } else {
+                    oldRules.add(r);
+                }
+            });
+            // ROI在前 规则类型:1 roi 2 粉价
+            newRules.sort((a, b) -> Integer.compare(a.getType(), b.getType()));
+            oldRules.sort((a, b) -> Integer.compare(a.getType(), b.getType()));
+            newRules.sort((a, b) -> Double.compare(b.getMinCost(), a.getMinCost()));
+            oldRules.sort((a, b) -> Double.compare(b.getMinCost(), a.getMinCost()));
+            newRules.sort((a, b) -> Double.compare(b.getScanMaxCost(), a.getScanMaxCost()));
+            oldRules.sort((a, b) -> Double.compare(b.getScanMaxCost(), a.getScanMaxCost()));
+
+            List<AutoPlanRule> allRules = new ArrayList<>();
+            allRules.addAll(newRules);
+            allRules.addAll(oldRules);
+            return allRules;
+        } catch (Exception e) {
+            log.error("[AdAutoExecutor] 解析规则JSON失败: {}", rulesJson, e);
+            return List.of();
+        }
+    }
+
+    private void updateLogFailed(AdAutoExecuteLog executeLog, String reason) {
+        executeLog.setExecuteStatus(2);
+        executeLog.setExecuteResult(reason);
+        adAutoExecuteLogService.updateById(executeLog);
+    }
+}

+ 3 - 4
src/main/java/com/moka/gdtauto/service/AdPlanConfService.java

@@ -60,14 +60,13 @@ public interface AdPlanConfService extends IService<AdPlanConf> {
      */
     AdPlanConf getAdPlanConfById(Long confId, Long userId);
 
-    void generatePlan(List<AdPlan> plans);
-
-    void asyncTencentInterface(List<AdPlan> plans);
+    void savePlanAndCreative(List<AdPlan> plans);
 
+    void asyncTencentInterface(AdPlanConf conf, List<AdPlan> plans);
     void asyncReGeneratePlan(Long id);
-
     void gdtDeleteByConf(AdPlanConf conf);
     void gdtSuspendByConf(AdPlanConf conf);
     void gdtDeleteByPlanIds(AdPlanConf conf, Long planIds);
     void gdtSuspendByPlanIds(AdPlanConf conf, Long planIds);
+    void gdtSuspendByPlans(AdPlanConf conf, List<AdPlan> plans);
 }

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

@@ -50,7 +50,7 @@ public class AdPlanDailyReportService {
     /** REQUEST_TIME 口径同步字段 */
     private static final List<String> REQ_FIELDS = Arrays.asList(
             "adgroup_id", "date", "cost", "income_val_1", "income_roi_1",
-            "view_count", "valid_click_count", "ctr", "conversions_cost"
+            "view_count", "valid_click_count", "ctr", "conversions_cost" ,"scan_follow_user_cost"
     );
 
     /** REPORTING_TIME 口径同步字段 */
@@ -63,8 +63,10 @@ public class AdPlanDailyReportService {
 
     @Async
     public void syncDailyReport(Long confId) {
-        syncReqDailyReport(confId, null, null);
-        syncRepDailyReport(confId, null, null);
+        LocalDateTime start = LocalDateTime.now();
+        LocalDateTime end = LocalDateTime.now();
+        syncReqDailyReport(confId, start, end);
+        syncRepDailyReport(confId, start, end);
     }
 
     /**
@@ -107,7 +109,6 @@ public class AdPlanDailyReportService {
             LambdaQueryWrapper<AdPlanConf> confWrapper = new LambdaQueryWrapper<>();
             confWrapper
                     .eq(confId != null, AdPlanConf::getId, confId)
-                    .between(confId == null, AdPlanConf::getCreateTime, startTime, endTime)
                     .eq(AdPlanConf::getDeleted, 0)
                     .orderByDesc(AdPlanConf::getId);
             Page<AdPlanConf> confPage = adPlanConfMapper.selectPage(new Page<>(confPageNum, PAGE_SIZE), confWrapper);
@@ -121,7 +122,7 @@ public class AdPlanDailyReportService {
             List<CompletableFuture<Void>> futures = confList.stream()
                     .map(conf -> CompletableFuture.runAsync(() -> {
                         try {
-                            int updated = processConf(conf, timeLine, isReqTime);
+                            int updated = processConf(conf, startTime, endTime, timeLine, isReqTime);
                             confUpdatedCount.put(conf.getId(), updated);
                         } catch (Exception e) {
                             log.error("[doSyncDailyReport] 处理 confId={} 失败", conf.getId(), e);
@@ -145,20 +146,18 @@ public class AdPlanDailyReportService {
     /**
      * 处理单个 AdPlanConf
      */
-    private int processConf(AdPlanConf conf, String timeLine, boolean isReqTime) throws Exception {
+    private int processConf(AdPlanConf conf, LocalDateTime startTime, LocalDateTime endTime, String timeLine, boolean isReqTime) throws Exception {
         String jobNumber = conf.getJobNumber();
         TencentAds tencentAds = clientFactory.getTencentAds(jobNumber, Constant.MAIN_ORG_ACCOUNT_ID);
 
         // 日期范围使用 conf.createTime 的日期
-        LocalDateTime confCreateTime = conf.getCreateTime();
-        String dateStr = confCreateTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
-        // + 1天
-        String endStr = confCreateTime.plusDays(1).format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+        String startStr = startTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
+        String endStr = endTime.format(DateTimeFormatter.ofPattern("yyyy-MM-dd"));
         ReportDateRange dateRange = new ReportDateRange();
-        dateRange.setStartDate(dateStr);
+        dateRange.setStartDate(startStr);
         dateRange.setEndDate(endStr);
 
-        log.info("[广告日报] start={} end={} timeLine={} isReqTime={}", dateStr, endStr, timeLine, isReqTime);
+        log.info("[广告日报] start={} end={} timeLine={} isReqTime={}", startStr, endStr, timeLine, isReqTime);
 
         List<String> groupBy = Arrays.asList("adgroup_id", "date");
         List<String> fields = isReqTime ? REQ_FIELDS : REP_FIELDS;
@@ -273,6 +272,7 @@ public class AdPlanDailyReportService {
                 update.setConversionsCost(rs.getConversionsCost());
                 update.setReqIncomeVal1(rs.getIncomeVal1());
                 update.setReqIncomeRoi1(d2b(rs.getIncomeRoi1()));
+                update.setScanFollowUserCost(rs.getScanFollowUserCost());
             } else {
                 // REPORTING_TIME 口径:更新 rep_income_val_1, rep_income_roi_1
                 update.setRepIncomeVal1(rs.getIncomeVal1());

+ 25 - 0
src/main/java/com/moka/gdtauto/service/impl/AdAutoConfServiceImp.java

@@ -0,0 +1,25 @@
+package com.moka.gdtauto.service.impl;
+
+import com.moka.gdtauto.entity.AdAutoConf;
+import com.moka.gdtauto.mapper.AdAutoConfMapper;
+import com.moka.gdtauto.service.AdAutoConfService;
+import com.moka.gdtauto.service.AdAutoExecutor;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 广告自动化配置表 服务实现类
+ * </p>
+ *
+ * @author mokamvp
+ * @since 2026-03-12
+ */
+@Service
+@RequiredArgsConstructor
+public class AdAutoConfServiceImp extends ServiceImpl<AdAutoConfMapper, AdAutoConf> implements AdAutoConfService {
+}
+

+ 20 - 0
src/main/java/com/moka/gdtauto/service/impl/AdAutoExecuteLogServiceImp.java

@@ -0,0 +1,20 @@
+package com.moka.gdtauto.service.impl;
+
+import com.moka.gdtauto.entity.AdAutoExecuteLog;
+import com.moka.gdtauto.mapper.AdAutoExecuteLogMapper;
+import com.moka.gdtauto.service.AdAutoExecuteLogService;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import org.springframework.stereotype.Service;
+
+/**
+ * <p>
+ * 广告规则执行日志表 服务实现类
+ * </p>
+ *
+ * @author mokamvp
+ * @since 2026-03-12
+ */
+@Service
+public class AdAutoExecuteLogServiceImp extends ServiceImpl<AdAutoExecuteLogMapper, AdAutoExecuteLog> implements AdAutoExecuteLogService {
+
+}

+ 43 - 22
src/main/java/com/moka/gdtauto/service/impl/AdPlanConfServiceImpl.java

@@ -269,6 +269,7 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
                 req.setConfiguredStatus(ConfiguredStatus.SUSPEND);
                 tencentAds.adgroups().adgroupsUpdate(req);
                 plan.setConfiguredStatus(ConfiguredStatus.SUSPEND.getValue());
+                plan.setFailReason("");
             } catch (Exception e) {
                 plan.setFailReason(truncateError(e.getMessage()));
             }
@@ -361,6 +362,24 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
     }
 
     @Override
+    public void gdtSuspendByPlans(AdPlanConf conf, List<AdPlan> plans) {
+        try {
+            TencentAds tencentAds = clientFactory.getTencentAds(conf.getJobNumber(), Constant.MAIN_ORG_ACCOUNT_ID);
+            doSuspendPlans(tencentAds, plans);
+            this.lambdaUpdate()
+                .eq(AdPlanConf::getId, conf.getId())
+                .set(AdPlanConf::getSystemSuspendProcessStatus, 2)
+                .update();
+        } catch (Exception e) {
+            log.error("gdtSuspendByPlanIds error", e);
+            this.lambdaUpdate()
+                .eq(AdPlanConf::getId, conf.getId())
+                .set(AdPlanConf::getSystemSuspendProcessStatus, 3)
+                .set(AdPlanConf::getFailReason, truncateError(e.getMessage())).update();
+        }
+    }
+
+    @Override
     @Transactional(rollbackFor = Exception.class)
     public Long createAdPlanConf(AdPlanConf adPlanConf) {
         adPlanConf.setCreateTime(LocalDateTime.now());
@@ -432,19 +451,18 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
     }
 
     @Override
-    public void generatePlan(List<AdPlan> plans) {
+    public void savePlanAndCreative(List<AdPlan> plans) {
         // 实现生成计划的逻辑
         log.info("生成计划,计划列表: {}", plans);
         for (AdPlan plan : plans) {
             adPlanService.save(plan);
             Long planId = plan.getId();
-
             plan.getAdCreatives().forEach(adPlanCreative -> {
                 adPlanCreative.setAdPlanId(planId);
                 adPlanCreative.setAdPlanConfId(plan.getAdPlanConfId());
                 adPlanCreative.setAccountId(plan.getAccountId());
-                adPlanCreativeService.save(adPlanCreative);
             });
+            adPlanCreativeService.saveBatch(plan.getAdCreatives());
         }
     }
 
@@ -455,6 +473,7 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
         // 根据配置ID获取计划列表
         AdPlanConf conf = this.getById(confId);
         List<AdPlan> plans = adPlanService.getPlansByConfId(confId);
+        // 替换最新更改
         plans.forEach(plan -> {
             plan.setConversionId(conf.getConversionId());
             plan.setWxCorpid(conf.getWxCorpid());
@@ -474,27 +493,26 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
         for (AdPlan plan : plans) {
             plan.setAdCreatives(creativeMap.getOrDefault(plan.getId(), new ArrayList<AdPlanCreative>()));
         }
-        asyncTencentInterface(plans);
+        asyncTencentInterface(conf, plans);
     }
 
     @Async
     @Override
-    public void asyncTencentInterface(List<AdPlan> plans) {
-        log.info("异步对接腾讯接口开始,计划数量: {}", plans.size());
-
+    public void asyncTencentInterface(AdPlanConf conf, List<AdPlan> plans) {
         if (plans == null || plans.isEmpty()) {
             log.warn("计划列表为空,无需处理");
             return;
         }
 
-        // 获取配置信息
-        Long confId = plans.get(0).getAdPlanConfId();
-        AdPlanConf conf = this.getById(confId);
-        if (conf == null) {
-            log.error("未找到广告计划配置,confId={}", confId);
-            return;
-        }
+        
+        int planCount = plans.size();
+        conf.setPlanCount(planCount);
+        int creativeCount = plans.stream().mapToInt(p -> p.getAdCreatives() == null ? 0 : p.getAdCreatives().size()).sum();
+        conf.setCreativeCount(creativeCount);
 
+        log.info("异步对接腾讯接口开始,计划数量: {} 创意数: {}", planCount, creativeCount);
+
+        Long confId = conf.getId();
         String jobNumber = conf.getJobNumber();
 
         // 创建 TencentAds 实例,整个流程复用同一个实例
@@ -583,7 +601,7 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
         }
 
         // 更新 AdPlanConf 状态
-        updateAdPlanConfStatus(confId, plans);
+        updateAdPlanConfStatus(conf, plans);
 
         log.info("异步对接腾讯接口完成,confId={}", confId);
     }
@@ -607,6 +625,7 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
 
             // 收集该账户下所有创意
             List<AdPlanCreative> allCreatives = accountPlans.stream()
+                    .filter (plan -> plan.getAdCreatives() != null)
                     .flatMap(plan -> plan.getAdCreatives().stream())
                     .collect(Collectors.toList());
 
@@ -968,28 +987,30 @@ public class AdPlanConfServiceImpl extends ServiceImpl<AdPlanConfMapper, AdPlanC
     /**
      * 更新 AdPlanConf 状态
      */
-    private void updateAdPlanConfStatus(Long confId, List<AdPlan> plans) {
+    private void updateAdPlanConfStatus(AdPlanConf conf, List<AdPlan> plans) {
         // 统计结果
         int totalCount = plans.size();
         int successCount = (int) plans.stream().filter(p -> AdPlanStatus.SUCCESS.equals(p.getStatus())).count();
         int failCount = totalCount - successCount;
+        int creativeSuccessCount = (int) plans.stream().filter(p -> p.getAdCreatives() != null).flatMap(p -> p.getAdCreatives().stream()).filter(c -> c.getStatus().equals(AdPlanStatus.SUCCESS)).count();
 
-        // 更新配置状态
-        AdPlanConf conf = this.getById(confId);
-        if (conf == null) {
-            log.warn("未找到广告计划配置,confId={}", confId);
-            return;
-        }
+        Long confId = conf.getId();
+        conf.setPlanSuccessCount(successCount);
+        conf.setCreativeSuccessCount(creativeSuccessCount);
 
         if (failCount == 0) {
             conf.setStatus(AdPlanStatus.SUCCESS); // 全部成功
             conf.setSuccessTime(LocalDateTime.now());
+            conf.setUpdateTime(LocalDateTime.now());
             conf.setFailReason("");
         } else if (successCount == 0) {
             conf.setStatus(AdPlanStatus.FAILED); // 全部失败
+            conf.setUpdateTime(LocalDateTime.now());
             conf.setFailReason("所有广告创建失败");
         } else {
             conf.setStatus(AdPlanStatus.PART_FAILED); // 部分失败
+            conf.setSuccessTime(LocalDateTime.now());
+            conf.setUpdateTime(LocalDateTime.now());
             conf.setFailReason(String.format("成功%d个,失败%d个", successCount, failCount));
         }
 

+ 34 - 0
src/main/java/com/moka/gdtauto/task/AdAutoTask.java

@@ -0,0 +1,34 @@
+package com.moka.gdtauto.task;
+
+import java.util.List;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import com.moka.gdtauto.entity.AdAutoConf;
+import com.moka.gdtauto.service.AdAutoConfService;
+import com.moka.gdtauto.service.AdAutoExecutor;
+
+import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import net.javacrumbs.shedlock.spring.annotation.SchedulerLock;
+
+@Component
+@AllArgsConstructor
+@Slf4j
+public class AdAutoTask {
+
+    private final AdAutoConfService adAutoConfService;
+    private final AdAutoExecutor adAutoExecutor;
+    // private final ExecutorService executorService = Executors.newFixedThreadPool(10);
+
+    @Scheduled(cron = "30 30 * * * ?")
+    @SchedulerLock(name = "ad-auto-task", lockAtMostFor = "9m", lockAtLeastFor = "2m")
+    public void execute() {
+        log.info("AdAutoTask execute");
+        List<AdAutoConf> confList = adAutoConfService.lambdaQuery().list();
+        confList.forEach(conf -> adAutoExecutor.asyncExecute(conf));
+    }
+}

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

@@ -30,8 +30,8 @@ public class AdPlanDailyReportSyncTask {
         try {
             TraceId.set();
             log.info("[syncReqDailyReport] 开始同步广告播放口径日报");
-            LocalDateTime start = LocalDateTime.now().minusDays(1).withHour(0).withMinute(0).withSecond(0);
-            LocalDateTime end = LocalDateTime.now().minusDays(1).withHour(23).withMinute(59).withSecond(59);
+            LocalDateTime start = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0);
+            LocalDateTime end = LocalDateTime.now().withHour(23).withMinute(59).withSecond(59);
             adPlanDailyReportService.syncReqDailyReport(null, start, end);
         } catch (Exception e) {
             log.error("[syncReqDailyReport] 同步广告播放口径日报异常", e);
@@ -44,8 +44,8 @@ public class AdPlanDailyReportSyncTask {
         try {
             TraceId.set();
             log.info("[syncRepDailyReport] 开始同步转化回传口径日报");
-            LocalDateTime start = LocalDateTime.now().minusDays(1).withHour(0).withMinute(0).withSecond(0);
-            LocalDateTime end = LocalDateTime.now().minusDays(1).withHour(23).withMinute(59).withSecond(59);
+            LocalDateTime start = LocalDateTime.now().withHour(0).withMinute(0).withSecond(0);
+            LocalDateTime end = LocalDateTime.now().withHour(23).withMinute(59).withSecond(59);
             adPlanDailyReportService.syncRepDailyReport(null,start, end);
         } catch (Exception e) {
             log.error("[syncRepDailyReport] 同步转化回传口径日报异常", e);

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

@@ -122,7 +122,6 @@ public class SiteConfUtil {
         siteConfMap.put(11,
             Arrays.asList(
                 "SITE_SET_WECHAT",
-                "SITE_SET_WECHAT_PLUGIN",
                 "SITE_SET_SEARCH_SCENE" 
             )
         );

+ 13 - 0
src/main/resources/mapper/AdAutoConfMapper.xml

@@ -0,0 +1,13 @@
+<?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.AdAutoConfMapper">
+
+    <insert id="saveOrUpdate">
+        INSERT INTO ad_auto_conf (job_number, ad_group_id, new_ad_days, rules, create_time, update_time)
+        VALUES (#{jobNumber}, #{adGroupId}, #{newAdDays}, #{rules}, #{createTime}, #{updateTime})
+        ON DUPLICATE KEY UPDATE
+        new_ad_days = VALUES(new_ad_days),
+        rules = VALUES(rules),
+        update_time = VALUES(update_time)
+    </insert>
+</mapper>

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

@@ -0,0 +1,5 @@
+<?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.AdAutoExecuteLogMapper">
+
+</mapper>

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

@@ -24,6 +24,7 @@
                 conversions_cost  = #{item.conversionsCost},
                 req_income_val1  = #{item.reqIncomeVal1},
                 req_income_roi1  = #{item.reqIncomeRoi1},
+                scan_follow_user_cost = #{item.scanFollowUserCost},
                 update_time       = NOW()
             WHERE adgroup_id = #{item.adgroupId}
         </foreach>

+ 11 - 0
src/main/resources/mapper/TencentAssetDramaMapper.xml

@@ -37,4 +37,15 @@
             update_time = CURRENT_TIMESTAMP
     </insert>
 
+    <!-- randomNewAsset --> 
+    <select id="randomNewAsset">
+        SELECT * FROM (
+            SELECT * FROM tencent_asset_drama
+            WHERE job_number = #{jobNumber}
+            ORDER BY marketing_asset_id DESC
+            LIMIT #{best}
+        ) AS temp_table
+        ORDER BY RAND()
+        LIMIT #{rand}
+    </select>
 </mapper>

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

@@ -37,4 +37,16 @@
             update_time = CURRENT_TIMESTAMP
     </insert>
 
+    <!-- randomNewAsset --> 
+
+    <select id="randomNewAsset">
+        SELECT * FROM (
+            SELECT * FROM tencent_asset_fiction
+            WHERE job_number = #{jobNumber}
+            ORDER BY marketing_asset_id DESC
+            LIMIT #{best}
+        ) AS temp_table
+        ORDER BY RAND()
+        LIMIT #{rand}
+    </select>
 </mapper>

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

@@ -104,7 +104,7 @@ class AdPlanConfAsyncTest {
         System.out.println("========================================\n");
         
         try {
-            adPlanConfService.asyncTencentInterface(plans);
+            adPlanConfService.asyncTencentInterface(conf, plans);
             
             // 等待异步任务完成 (因为是异步方法,需要等待)
             System.out.println("⏳ 等待异步任务执行... (30秒)");

+ 6 - 2
src/test/java/com/moka/gdtauto/service/AdPlanDailyReportSyncTest.java

@@ -1,5 +1,7 @@
 package com.moka.gdtauto.service;
 
+import java.time.LocalDateTime;
+
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
@@ -26,11 +28,13 @@ public class AdPlanDailyReportSyncTest {
 
     @Test
     public void testSyncReqDailyReport() {
-        adPlanDailyReportService.syncReqDailyReport(29L, null, null);
+        LocalDateTime now = LocalDateTime.now();
+        adPlanDailyReportService.syncReqDailyReport(29L, now, now);
     }
 
     @Test
     public void testSyncRepDailyReport() {
-        adPlanDailyReportService.syncRepDailyReport(29L, null, null);
+        LocalDateTime now = LocalDateTime.now();
+        adPlanDailyReportService.syncRepDailyReport(29L, now, now);
     }
 }

+ 5 - 1
src/test/java/com/moka/gdtauto/service/CreateAdGroupTest.java

@@ -2,6 +2,8 @@ package com.moka.gdtauto.service;
 
 import com.moka.gdtauto.GdtAutoApplication;
 import com.moka.gdtauto.entity.AdPlan;
+import com.moka.gdtauto.entity.AdPlanConf;
+
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
@@ -24,6 +26,8 @@ public class CreateAdGroupTest {
     @Test
     public void createAdGroup() {
         AdPlan  adPlan = adPlanService.getById(7580);
-        adPlanConfService.asyncTencentInterface(List.of(adPlan));
+        Long adPlanConfId = adPlan.getAdPlanConfId();
+        AdPlanConf adPlanConf = adPlanConfService.getById(adPlanConfId);
+        adPlanConfService.asyncTencentInterface(adPlanConf, List.of(adPlan));
     }
 }

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

@@ -2,11 +2,13 @@ package com.moka.gdtauto.util;
 
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.util.ArrayList;
+import java.util.List;
 
 import org.junit.jupiter.api.Test;
 import org.springframework.util.DigestUtils;
 import com.alibaba.fastjson.JSON;
-
+import com.moka.gdtauto.common.AutoPlanRule;
 import com.tencent.ads.model.v3.WriteTargetingSetting;
 
 public class SampleTest {
@@ -30,5 +32,38 @@ public class SampleTest {
         WriteTargetingSetting targetingSetting = TargetConfUtil.getTargetingSetting(1);
         System.out.println(JSON.toJSONString(targetingSetting, true));
     }
+
+    @Test
+    public void testRuleSort() {
+        String str = """
+            [{"type":1,"newAd":1,"minCost":100,"minIor":0.15,"scanMaxCost":10},
+            {"type":1,"newAd":1,"minCost":30,"minIor":0.1,"scanMaxCost":10},
+            {"type":2,"newAd":1,"minCost":0,"minIor":0.1,"scanMaxCost":10},
+            {"type":2,"newAd":2,"minCost":0,"minIor":0,"scanMaxCost":100}]
+        """;
+        List<AutoPlanRule> rules = JSON.parseArray(str, AutoPlanRule.class);
+        List<AutoPlanRule> newRules = new ArrayList<>();
+        List<AutoPlanRule> oldRules = new ArrayList<>();
+        rules.forEach(r -> {
+            if (r.getNewAd() == 1) {
+                newRules.add(r);
+            } else {
+                oldRules.add(r);
+            }
+        });
+        //ROI在前 规则类型:1 roi 2 粉价
+        newRules.sort((a, b) -> Integer.compare(a.getType(), b.getType()));
+        oldRules.sort((a, b) -> Integer.compare(a.getType(), b.getType()));
+        newRules.sort((a, b) -> Double.compare(b.getMinCost(), a.getMinCost()));
+        oldRules.sort((a, b) -> Double.compare(b.getMinCost(), a.getMinCost()));
+        newRules.sort((a, b) -> Double.compare(b.getScanMaxCost(), a.getScanMaxCost()));
+        oldRules.sort((a, b) -> Double.compare(b.getScanMaxCost(), a.getScanMaxCost()));
+
+        List<AutoPlanRule> allRules = new ArrayList<>();
+        allRules.addAll(newRules);
+        allRules.addAll(oldRules);
+
+        System.out.println(JSON.toJSONString(allRules, true));
+    }
     
 }

Някои файлове не бяха показани, защото твърде много файлове са промени