Prechádzať zdrojové kódy

Merge branch 'refs/heads/master' into dev-zenas-20241018

zq940222 5 mesiacov pred
rodič
commit
9c2f3734eb
29 zmenil súbory, kde vykonal 520 pridanie a 94 odobranie
  1. 20 0
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/common/util/CommonUtil.java
  2. 50 0
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/controller/GTMController.java
  3. 2 0
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/dto/google/analytics/CreatePropertyRequestDTO.java
  4. 5 1
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/dto/google/analytics/GAAccountDTO.java
  5. 5 1
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/dto/google/analytics/GAPropertyDTO.java
  6. 2 0
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/dto/google/gtm/CreateContainerRequestDTO.java
  7. 5 1
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/dto/google/gtm/GTMAccountDTO.java
  8. 9 2
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/dto/google/gtm/GTMContainerDTO.java
  9. 40 0
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/dto/google/gtm/GTMTagDTO.java
  10. 4 4
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/entity/GACountryReport.java
  11. 4 4
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/entity/GAPagePathReport.java
  12. 4 4
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/entity/GASourceMediumReport.java
  13. 4 4
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/entity/GoogleGTM.java
  14. 35 20
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/service/google/GAReportService.java
  15. 224 0
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/service/google/GTMAdminService.java
  16. 0 7
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/enquiry/controller/EnquiryController.java
  17. 5 13
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/seo/service/dataforseo/DataForSEOService.java
  18. 0 12
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/site/entity/AdwebSite.java
  19. 5 0
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/site/service/IAdwebSiteService.java
  20. 12 0
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/site/service/impl/AdwebSiteServiceImpl.java
  21. 26 17
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/xxl/DataForSEOJob.java
  22. 9 2
      jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/xxl/GAReportJob.java
  23. 4 0
      jeecg-module-system/jeecg-system-biz/src/main/resources/google/gtm/body-snippet.tmpl
  24. 7 0
      jeecg-module-system/jeecg-system-biz/src/main/resources/google/gtm/head-snippet.tmpl
  25. 4 0
      jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml
  26. 4 0
      jeecg-module-system/jeecg-system-start/src/main/resources/application-prod.yml
  27. 4 0
      jeecg-module-system/jeecg-system-start/src/main/resources/application-test.yml
  28. 25 0
      jeecg-module-system/jeecg-system-start/src/test/java/org/jeecg/modules/adweb/dmp/service/google/GoogleServiceTest.java
  29. 2 2
      jeecg-module-system/jeecg-system-start/src/test/java/org/jeecg/modules/adweb/seo/service/DataForSEOTest.java

+ 20 - 0
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/common/util/CommonUtil.java

@@ -1,11 +1,16 @@
 package org.jeecg.modules.adweb.common.util;
 
+import static java.util.function.Predicate.*;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
 import com.google.common.net.InternetDomainName;
 
 import lombok.extern.slf4j.Slf4j;
 
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.util.List;
 
 /**
  * 常用工具类
@@ -31,4 +36,19 @@ public class CommonUtil {
             return InternetDomainName.from(host).topPrivateDomain().toString();
         }
     }
+
+    /**
+     * 对一个字符串进行split and trim,过滤空串,返回{@link List}
+     *
+     * @param str
+     * @param separator
+     * @return
+     */
+    public static List<String> splitAndTrim(String str, final String separator) {
+        return Splitter.on(separator)
+                .splitToStream(str)
+                .map(String::trim)
+                .filter(not(Strings::isNullOrEmpty))
+                .toList();
+    }
 }

+ 50 - 0
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/controller/GTMController.java

@@ -0,0 +1,50 @@
+package org.jeecg.modules.adweb.dmp.controller;
+
+import io.swagger.v3.oas.annotations.tags.Tag;
+
+import lombok.extern.slf4j.Slf4j;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.jeecg.common.api.vo.Result;
+import org.jeecg.modules.adweb.dmp.entity.GoogleGTM;
+import org.jeecg.modules.adweb.dmp.service.google.GTMAdminService;
+import org.jeecg.modules.adweb.site.entity.AdwebSite;
+import org.jeecg.modules.adweb.site.service.IAdwebSiteService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestMethod;
+import org.springframework.web.bind.annotation.ResponseBody;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.Objects;
+
+/**
+ * GTM容器和标签管理
+ *
+ * @author wfansh
+ */
+@Tag(name = "Google Tag Manager")
+@RestController
+@RequestMapping("/gtm")
+@Slf4j
+public class GTMController {
+
+    @Autowired private IAdwebSiteService adwebSiteService;
+
+    @Autowired private GTMAdminService gtmAdminService;
+
+    @RequestMapping(value = "/add", method = RequestMethod.POST)
+    @ResponseBody
+    public Result<Pair<String, String>> addContainer(String siteId) {
+        AdwebSite adwebSite = adwebSiteService.getById(siteId);
+        if (Objects.isNull(adwebSite)) {
+            return Result.error("站点未找到" + siteId);
+        }
+
+        // 如果GoogleGTM表中已存在,不再创建,返回现有记录
+        GoogleGTM googleGTM =
+                gtmAdminService.createContainer(
+                        adwebSite.getCode(), adwebSite.getDomain(), adwebSite.getName());
+        return Result.ok(gtmAdminService.getSnippets(googleGTM.getGtmTagId()));
+    }
+}

+ 2 - 0
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/dto/google/analytics/CreatePropertyRequestDTO.java

@@ -1,11 +1,13 @@
 package org.jeecg.modules.adweb.dmp.dto.google.analytics;
 
+import lombok.Builder;
 import lombok.Data;
 
 /**
  * @author wfansh
  */
 @Data
+@Builder
 public class CreatePropertyRequestDTO {
 
     private String accountResourceName;

+ 5 - 1
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/dto/google/analytics/GAAccountDTO.java

@@ -1,5 +1,7 @@
 package org.jeecg.modules.adweb.dmp.dto.google.analytics;
 
+import com.google.common.base.Joiner;
+
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
@@ -24,6 +26,8 @@ public class GAAccountDTO extends ResourceDTO {
     private long updateTime;
 
     public static String toResourceName(String id) {
-        return StringUtils.isNumeric(id) ? "accounts" + RESOURCE_NAME_SPLITTER + id : id;
+        return StringUtils.isNumeric(id)
+                ? Joiner.on(RESOURCE_NAME_SPLITTER).join("accounts", id)
+                : id;
     }
 }

+ 5 - 1
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/dto/google/analytics/GAPropertyDTO.java

@@ -1,5 +1,7 @@
 package org.jeecg.modules.adweb.dmp.dto.google.analytics;
 
+import com.google.common.base.Joiner;
+
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
@@ -42,6 +44,8 @@ public class GAPropertyDTO extends ResourceDTO {
     private List<GADataStreamDTO> dataStreams;
 
     public static String toResourceName(String id) {
-        return StringUtils.isNumeric(id) ? "properties" + RESOURCE_NAME_SPLITTER + id : id;
+        return StringUtils.isNumeric(id)
+                ? Joiner.on(RESOURCE_NAME_SPLITTER).join("properties", id)
+                : id;
     }
 }

+ 2 - 0
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/dto/google/gtm/CreateContainerRequestDTO.java

@@ -1,11 +1,13 @@
 package org.jeecg.modules.adweb.dmp.dto.google.gtm;
 
+import lombok.Builder;
 import lombok.Data;
 
 /**
  * @author wfansh
  */
 @Data
+@Builder
 public class CreateContainerRequestDTO {
 
     private String accountResourceName;

+ 5 - 1
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/dto/google/gtm/GTMAccountDTO.java

@@ -1,5 +1,7 @@
 package org.jeecg.modules.adweb.dmp.dto.google.gtm;
 
+import com.google.common.base.Joiner;
+
 import lombok.*;
 import lombok.experimental.SuperBuilder;
 
@@ -21,6 +23,8 @@ public class GTMAccountDTO extends ResourceDTO {
     private boolean supportMultipleContainers;
 
     public static String toResourceName(String id) {
-        return StringUtils.isNumeric(id) ? "accounts" + RESOURCE_NAME_SPLITTER + id : id;
+        return StringUtils.isNumeric(id)
+                ? Joiner.on(RESOURCE_NAME_SPLITTER).join("accounts", id)
+                : id;
     }
 }

+ 9 - 2
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/dto/google/gtm/GTMContainerDTO.java

@@ -1,5 +1,7 @@
 package org.jeecg.modules.adweb.dmp.dto.google.gtm;
 
+import com.google.common.base.Joiner;
+
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 import lombok.NoArgsConstructor;
@@ -39,7 +41,12 @@ public class GTMContainerDTO extends ResourceDTO {
 
     private String fingerprint;
 
-    public static String toResourceName(String id) {
-        return StringUtils.isNumeric(id) ? "accounts" + RESOURCE_NAME_SPLITTER + id : id;
+    private List<GTMTagDTO> tags;
+
+    public static String toResourceName(String accountId, String containerId) {
+        return StringUtils.isNumeric(accountId) && StringUtils.isNumeric(containerId)
+                ? Joiner.on(RESOURCE_NAME_SPLITTER)
+                        .join("accounts", accountId, "containers", containerId)
+                : containerId;
     }
 }

+ 40 - 0
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/dto/google/gtm/GTMTagDTO.java

@@ -0,0 +1,40 @@
+package org.jeecg.modules.adweb.dmp.dto.google.gtm;
+
+import lombok.*;
+import lombok.experimental.SuperBuilder;
+
+import org.jeecg.modules.adweb.dmp.dto.google.ResourceDTO;
+
+import java.util.List;
+
+/**
+ * @author wfansh
+ */
+@Data
+@SuperBuilder
+@NoArgsConstructor
+@ToString(callSuper = true)
+@EqualsAndHashCode(callSuper = true)
+public class GTMTagDTO extends ResourceDTO {
+
+    private String accountId;
+
+    private String containerId;
+
+    private String workspaceId;
+
+    private String type;
+
+    private List<TagParameter> parameters;
+
+    private List<String> firingTriggerIds;
+
+    @Data
+    public static class TagParameter {
+        private String type;
+
+        private String key;
+
+        private String value;
+    }
+}

+ 4 - 4
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/entity/GACountryReport.java

@@ -35,10 +35,10 @@ public class GACountryReport implements Serializable {
 	@TableId(type = IdType.AUTO)
     @Schema(description = "id")
     private Long id;
-	/**站点id*/
-	@Excel(name = "站点id", width = 15)
-    @Schema(description = "站点id")
-    private Integer siteId;
+    /**站点code*/
+    @Excel(name = "站点code", width = 15)
+    @Schema(description = "站点code")
+    private String siteCode;
 	/**统计时间*/
 	@Excel(name = "统计时间", width = 15, format = "yyyy-MM-dd")
 	@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")

+ 4 - 4
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/entity/GAPagePathReport.java

@@ -35,10 +35,10 @@ public class GAPagePathReport implements Serializable {
 	@TableId(type = IdType.AUTO)
     @Schema(description = "id")
     private Long id;
-	/**站点id*/
-	@Excel(name = "站点id", width = 15)
-    @Schema(description = "站点id")
-    private Integer siteId;
+    /**站点code*/
+    @Excel(name = "站点code", width = 15)
+    @Schema(description = "站点code")
+    private String siteCode;
 	/**统计时间*/
 	@Excel(name = "统计时间", width = 15, format = "yyyy-MM-dd")
 	@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")

+ 4 - 4
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/entity/GASourceMediumReport.java

@@ -35,10 +35,10 @@ public class GASourceMediumReport implements Serializable {
 	@TableId(type = IdType.AUTO)
     @Schema(description = "id")
     private Long id;
-	/**站点id*/
-	@Excel(name = "站点id", width = 15)
-    @Schema(description = "站点id")
-    private Integer siteId;
+    /**站点code*/
+    @Excel(name = "站点code", width = 15)
+    @Schema(description = "站点code")
+    private String siteCode;
 	/**统计时间*/
 	@Excel(name = "统计时间", width = 15, format = "yyyy-MM-dd")
 	@JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")

+ 4 - 4
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/entity/GoogleGTM.java

@@ -35,10 +35,10 @@ public class GoogleGTM implements Serializable {
 	@TableId(type = IdType.AUTO)
     @Schema(description = "id")
     private Integer id;
-	/**siteId*/
-	@Excel(name = "siteId", width = 15)
-    @Schema(description = "siteId")
-    private Integer siteId;
+	/**站点code*/
+	@Excel(name = "站点code", width = 15)
+    @Schema(description = "站点code")
+    private String siteCode;
 	/**uid*/
 	@Excel(name = "uid", width = 15)
     @Schema(description = "uid")

+ 35 - 20
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/service/google/GAReportService.java

@@ -3,6 +3,7 @@ package org.jeecg.modules.adweb.dmp.service.google;
 import static org.jeecg.modules.adweb.dmp.dto.google.analytics.report.ReportConstant.*;
 
 import com.baomidou.mybatisplus.annotation.TableName;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.google.common.annotations.VisibleForTesting;
 
@@ -16,6 +17,7 @@ import org.jeecg.common.util.DateUtils;
 import org.jeecg.common.util.FastJsonUtil;
 import org.jeecg.modules.adweb.common.mapper.CommonMapper;
 import org.jeecg.modules.adweb.common.util.DateUtil;
+import org.jeecg.modules.adweb.common.util.ListUtil;
 import org.jeecg.modules.adweb.common.util.RestTemplateUtil;
 import org.jeecg.modules.adweb.dmp.dto.OpenAPIRequest;
 import org.jeecg.modules.adweb.dmp.dto.OpenAPIResponse;
@@ -34,6 +36,7 @@ import org.jeecg.modules.adweb.dmp.service.IGACountryReportService;
 import org.jeecg.modules.adweb.dmp.service.IGAPagePathReportService;
 import org.jeecg.modules.adweb.dmp.service.IGASourceMediumReportService;
 import org.jeecg.modules.adweb.dmp.service.IGoogleGTMService;
+import org.jeecg.modules.adweb.site.service.IAdwebSiteService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.core.ParameterizedTypeReference;
@@ -70,8 +73,8 @@ public class GAReportService {
     @Value("${data-bridge.api.token}")
     private String dataBridgeApiToken;
 
+    @Autowired private IAdwebSiteService adwebSiteService;
     @Autowired private IGoogleGTMService googleGTMService;
-
     @Autowired private IGACountryReportService gaCountryReportService;
     @Autowired private IGASourceMediumReportService gaSourceMediumReportService;
     @Autowired private IGAPagePathReportService gaPagePathReportService;
@@ -85,19 +88,28 @@ public class GAReportService {
         this.restTemplate = RestTemplateUtil.getRestTemplate(60, 60, dataBridgeApiToken);
     }
 
-    /** 拉取并同步Google Analytics报表 */
-    public void syncGAReport() {
-        // TODO: 判断网站状态
-        List<GoogleGTM> googleGTMS = googleGTMService.list();
+    /**
+     * 拉取并同步Google Analytics报表
+     *
+     * @param siteCodes 待执行的网站codes,为空时执行所有网站
+     */
+    public void syncGAReport(List<String> siteCodes) {
+        if (ListUtil.isEmpty(siteCodes)) {
+            siteCodes = adwebSiteService.getAllActiveSiteCodes();
+        }
+
+        List<GoogleGTM> googleGTMs =
+                googleGTMService.list(
+                        new LambdaQueryWrapper<GoogleGTM>().in(GoogleGTM::getSiteCode, siteCodes));
 
-        for (GoogleGTM googleGTM : googleGTMS) {
+        for (GoogleGTM googleGTM : googleGTMs) {
             // 每个帐号同步更新三张报表
             try {
                 this.syncGACountryReport(googleGTM);
                 this.syncGASourceMediumReport(googleGTM);
                 this.syncGAPagePathReport(googleGTM);
             } catch (RuntimeException e) {
-                log.warn("同步GA报表异常, siteId = {], error = {}", googleGTM.getSiteId(), e);
+                log.warn("同步GA报表异常, siteCode = {], error = {}", googleGTM.getSiteCode(), e);
             }
         }
     }
@@ -109,7 +121,7 @@ public class GAReportService {
      */
     private void syncGACountryReport(GoogleGTM googleGTM) {
         // 1. 报表时间区间
-        Date startDate = this.getReportStartDate(TABLE_GA_COUNTRY_REPORT, googleGTM.getSiteId());
+        Date startDate = this.getReportStartDate(TABLE_GA_COUNTRY_REPORT, googleGTM.getSiteCode());
         Date endDate = new Date();
 
         // 2. 构建GA报表请求参数
@@ -132,7 +144,7 @@ public class GAReportService {
         List<GACountryReport> countryReport = Lists.newArrayList();
         for (CustomReportData reportData : reportDataList) {
             GACountryReport reportRow = new GACountryReport();
-            reportRow.setSiteId(googleGTM.getSiteId());
+            reportRow.setSiteCode(googleGTM.getSiteCode());
             reportRow.setDate(
                     DateUtils.str2Date(
                             reportData.get(ReportConstant.DIMENSION_DATE),
@@ -147,7 +159,7 @@ public class GAReportService {
         // 3. 更新数据库 - 删除旧数据,插入新数据
         gaCountryReportService.remove(
                 this.getRemoveQueryWrapper(
-                        GACountryReport.class, googleGTM.getSiteId(), startDate, endDate));
+                        GACountryReport.class, googleGTM.getSiteCode(), startDate, endDate));
 
         gaCountryReportService.saveBatch(countryReport, countryReport.size());
     }
@@ -160,7 +172,7 @@ public class GAReportService {
     private void syncGASourceMediumReport(GoogleGTM googleGTM) {
         // 1. 报表时间区间
         Date startDate =
-                this.getReportStartDate(TABLE_GA_SOURCE_MEDIUM_REPORT, googleGTM.getSiteId());
+                this.getReportStartDate(TABLE_GA_SOURCE_MEDIUM_REPORT, googleGTM.getSiteCode());
         Date endDate = new Date();
 
         // 2. 构建GA报表请求参数
@@ -192,7 +204,7 @@ public class GAReportService {
         List<GASourceMediumReport> sourceMediumReport = Lists.newArrayList();
         for (CustomReportData reportData : reportDataList) {
             GASourceMediumReport reportRow = new GASourceMediumReport();
-            reportRow.setSiteId(googleGTM.getSiteId());
+            reportRow.setSiteCode(googleGTM.getSiteCode());
             reportRow.setDate(
                     DateUtils.str2Date(
                             reportData.get(ReportConstant.DIMENSION_DATE),
@@ -217,7 +229,7 @@ public class GAReportService {
         // 3. 更新数据库 - 删除旧数据,插入新数据
         gaSourceMediumReportService.remove(
                 this.getRemoveQueryWrapper(
-                        GASourceMediumReport.class, googleGTM.getSiteId(), startDate, endDate));
+                        GASourceMediumReport.class, googleGTM.getSiteCode(), startDate, endDate));
 
         gaSourceMediumReportService.saveBatch(sourceMediumReport, sourceMediumReport.size());
     }
@@ -229,7 +241,8 @@ public class GAReportService {
      */
     private void syncGAPagePathReport(GoogleGTM googleGTM) {
         // 1. 报表时间区间
-        Date startDate = this.getReportStartDate(TABLE_GA_PAGE_PATH_REPORT, googleGTM.getSiteId());
+        Date startDate =
+                this.getReportStartDate(TABLE_GA_PAGE_PATH_REPORT, googleGTM.getSiteCode());
         Date endDate = new Date();
 
         // 2. 构建GA报表请求参数
@@ -256,7 +269,7 @@ public class GAReportService {
         List<GAPagePathReport> pagePathReport = Lists.newArrayList();
         for (CustomReportData reportData : reportDataList) {
             GAPagePathReport reportRow = new GAPagePathReport();
-            reportRow.setSiteId(googleGTM.getSiteId());
+            reportRow.setSiteCode(googleGTM.getSiteCode());
             reportRow.setDate(
                     DateUtils.str2Date(
                             reportData.get(ReportConstant.DIMENSION_DATE),
@@ -276,7 +289,7 @@ public class GAReportService {
         // 3. 更新数据库 - 删除旧数据,插入新数据
         gaPagePathReportService.remove(
                 this.getRemoveQueryWrapper(
-                        GAPagePathReport.class, googleGTM.getSiteId(), startDate, endDate));
+                        GAPagePathReport.class, googleGTM.getSiteCode(), startDate, endDate));
 
         gaPagePathReportService.saveBatch(pagePathReport, pagePathReport.size());
     }
@@ -325,8 +338,10 @@ public class GAReportService {
      *
      * <p>2. 表中最大时间减一天 - 如10月10号凌晨2点执行,最大时间可能是10号;同时9号数据GA侧有可能更新
      */
-    private Date getReportStartDate(String tableName, int siteId) {
-        Date maxDate = commonMapper.getMaxDate(tableName, "date", "site_id = " + siteId);
+    private Date getReportStartDate(String tableName, String siteCode) {
+        Date maxDate =
+                commonMapper.getMaxDate(
+                        tableName, "date", String.format("site_code = '%s'", siteCode));
         if (Objects.isNull(maxDate)) {
             // 1. 一年前
             return DateUtil.addDays(new Date(), -365);
@@ -337,9 +352,9 @@ public class GAReportService {
 
     /** 生成{@link QueryWrapper}, 删除GA报表历史数据 */
     private <T> QueryWrapper<T> getRemoveQueryWrapper(
-            Class<T> entityClass, int siteId, Date startDate, Date endDate) {
+            Class<T> entityClass, String siteCode, Date startDate, Date endDate) {
         QueryWrapper<T> queryWrapper = new QueryWrapper<>(entityClass);
-        queryWrapper.eq("site_id", siteId);
+        queryWrapper.eq("site_code", siteCode);
         queryWrapper.ge("date", startDate);
         queryWrapper.le("date", endDate);
         return queryWrapper;

+ 224 - 0
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/dmp/service/google/GTMAdminService.java

@@ -0,0 +1,224 @@
+package org.jeecg.modules.adweb.dmp.service.google;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.io.Resources;
+
+import jakarta.annotation.PostConstruct;
+
+import lombok.extern.slf4j.Slf4j;
+
+import org.apache.commons.lang3.tuple.Pair;
+import org.jeecg.common.util.FastJsonUtil;
+import org.jeecg.modules.adweb.common.util.ListUtil;
+import org.jeecg.modules.adweb.common.util.RestTemplateUtil;
+import org.jeecg.modules.adweb.dmp.dto.OpenAPIRequest;
+import org.jeecg.modules.adweb.dmp.dto.OpenAPIResponse;
+import org.jeecg.modules.adweb.dmp.dto.google.analytics.CreatePropertyRequestDTO;
+import org.jeecg.modules.adweb.dmp.dto.google.analytics.GAAccountDTO;
+import org.jeecg.modules.adweb.dmp.dto.google.analytics.GAPropertyDTO;
+import org.jeecg.modules.adweb.dmp.dto.google.gtm.CreateContainerRequestDTO;
+import org.jeecg.modules.adweb.dmp.dto.google.gtm.GTMAccountDTO;
+import org.jeecg.modules.adweb.dmp.dto.google.gtm.GTMContainerDTO;
+import org.jeecg.modules.adweb.dmp.entity.GoogleGTM;
+import org.jeecg.modules.adweb.dmp.service.IGoogleGTMService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.stereotype.Service;
+import org.springframework.web.client.RestTemplate;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+
+/**
+ * Google Tag Manager + Google Analytics帐号及标签管理
+ *
+ * <p>http://data-bridge.v3.adwebcloud.com:9002/swagger-ui/index.html
+ *
+ * @author wfansh
+ */
+@Slf4j
+@Service
+public class GTMAdminService {
+
+    private static final String GA_CREATE_PROPERTY_API_PATH = "/api/google/ga/properties/create";
+    private static final String GA_DELETE_PROPERTY_API_PATH = "/api/google/ga/properties/delete";
+    private static final String GTM_CREATE_CONTAINER_API_PATH = "/api/google/gtm/containers/create";
+    private static final String GTM_DELETE_CONTAINER_API_PATH = "/api/google/gtm/containers/delete";
+
+    @Value("${data-bridge.api.host}")
+    private String dataBridgeApiHost;
+
+    @Value("${data-bridge.api.token}")
+    private String dataBridgeApiToken;
+
+    @Value("${data-bridge.gtm.account-id}")
+    private String gtmAccountId;
+
+    @Value("${data-bridge.ga.account-id}")
+    private String gaAccountId;
+
+    @Autowired private IGoogleGTMService googleGTMService;
+
+    private RestTemplate restTemplate;
+
+    private String headSnippetTemplate;
+
+    private String bodySnippetTemplate;
+
+    @PostConstruct
+    private void init() throws IOException {
+        this.restTemplate = RestTemplateUtil.getRestTemplate(60, 60, dataBridgeApiToken);
+
+        headSnippetTemplate =
+                Resources.toString(
+                        this.getClass()
+                                .getClassLoader()
+                                .getResource("google/gtm/head-snippet.tmpl"),
+                        StandardCharsets.UTF_8);
+        bodySnippetTemplate =
+                Resources.toString(
+                        this.getClass()
+                                .getClassLoader()
+                                .getResource("google/gtm/body-snippet.tmpl"),
+                        StandardCharsets.UTF_8);
+    }
+
+    /**
+     *
+     * <li>1. 创建GTM container + GA property
+     * <li>2. 更新{@link GoogleGTM}表
+     */
+    public GoogleGTM createContainer(String siteCode, String siteUrl, String siteName) {
+        List<GoogleGTM> googleGTMs =
+                googleGTMService.list(
+                        new LambdaQueryWrapper<GoogleGTM>().eq(GoogleGTM::getSiteCode, siteCode));
+        if (ListUtil.notEmpty(googleGTMs)) {
+            log.info("站点 {} 对应的GoogleGTM已存在,ID = {}", siteCode, googleGTMs.get(0).getId());
+            return googleGTMs.get(0);
+        }
+
+        // 1. 创建GA property - 通过data-bridge API
+        OpenAPIRequest<CreatePropertyRequestDTO> createGAPropertyRequest = new OpenAPIRequest<>();
+        createGAPropertyRequest.setRequestServer(this.getClass().getSimpleName());
+        createGAPropertyRequest.setRequestTime(System.currentTimeMillis());
+        createGAPropertyRequest.setData(
+                CreatePropertyRequestDTO.builder()
+                        .accountResourceName(GAAccountDTO.toResourceName(gaAccountId))
+                        .displayName(siteName)
+                        .url(siteUrl)
+                        .build());
+        GAPropertyDTO gaProperty =
+                RestTemplateUtil.postForObject(
+                                restTemplate,
+                                dataBridgeApiHost + GA_CREATE_PROPERTY_API_PATH,
+                                createGAPropertyRequest,
+                                new ParameterizedTypeReference<OpenAPIResponse<GAPropertyDTO>>() {})
+                        .getData();
+        log.info("为站点 {} 创建GA property: {}", siteCode, FastJsonUtil.toJSONString(gaProperty));
+
+        // 2. 创建GTM container - 通过data-bridge API
+        OpenAPIRequest<CreateContainerRequestDTO> createGTMContainerRequest =
+                new OpenAPIRequest<>();
+        createGTMContainerRequest.setRequestServer(this.getClass().getSimpleName());
+        createGTMContainerRequest.setRequestTime(System.currentTimeMillis());
+        createGTMContainerRequest.setData(
+                CreateContainerRequestDTO.builder()
+                        .accountResourceName(GTMAccountDTO.toResourceName(gtmAccountId))
+                        .displayName(siteName)
+                        .googleTagId(gaProperty.getDataStreams().get(0).getStreamMeasurementId())
+                        .build());
+        GTMContainerDTO gtmContainer =
+                RestTemplateUtil.postForObject(
+                                restTemplate,
+                                dataBridgeApiHost + GTM_CREATE_CONTAINER_API_PATH,
+                                createGTMContainerRequest,
+                                new ParameterizedTypeReference<
+                                        OpenAPIResponse<GTMContainerDTO>>() {})
+                        .getData();
+        log.info("为站点 {} 创建GTM container: {}", siteCode, FastJsonUtil.toJSONString(gtmContainer));
+
+        // 3. 更新数据库
+        GoogleGTM googleGTM = new GoogleGTM();
+        googleGTM.setSiteCode(siteCode);
+        googleGTM.setUid(null); // TODO
+        googleGTM.setGtmAccountId(gtmAccountId);
+        googleGTM.setGtmContainerId(gtmContainer.getId());
+        googleGTM.setGtmTagId(gtmContainer.getPublicId());
+        googleGTM.setGaAccountId(gaAccountId);
+        googleGTM.setGaPropertyId(gaProperty.getId());
+        googleGTM.setGaVersion("V4"); // GA4 https://support.google.com/analytics/answer/10089681
+        googleGTM.setGaSiteUrl(siteUrl);
+        googleGTM.setGaTagId(gaProperty.getDataStreams().get(0).getStreamMeasurementId());
+        googleGTMService.save(googleGTM);
+
+        return googleGTM;
+    }
+
+    /**
+     * 返回GTM container的head + body snippet代码
+     *
+     * @param gtmTagId
+     * @return
+     */
+    public Pair<String, String> getSnippets(String gtmTagId) {
+        return Pair.of(
+                String.format(headSnippetTemplate, gtmTagId),
+                String.format(bodySnippetTemplate, gtmTagId));
+    }
+
+    /**
+     * 测试专用 - 删除相关资源
+     * <li>1. 删除GTM container + GA property
+     * <li>2. 删除{@link GoogleGTM}表
+     */
+    @VisibleForTesting
+    boolean deleteContainer(String siteCode) {
+        List<GoogleGTM> googleGTMs =
+                googleGTMService.list(
+                        new LambdaQueryWrapper<GoogleGTM>().eq(GoogleGTM::getSiteCode, siteCode));
+        if (ListUtil.isEmpty(googleGTMs)) {
+            log.info("站点 {} 对应的GoogleGTM不存在", siteCode);
+            return false;
+        }
+
+        GoogleGTM googleGTM = googleGTMs.get(0);
+
+        // 1. 删除GA property - 通过data-bridge API
+        OpenAPIRequest<String> deleteGAPropertyRequest = new OpenAPIRequest<>();
+        deleteGAPropertyRequest.setRequestServer(this.getClass().getSimpleName());
+        deleteGAPropertyRequest.setRequestTime(System.currentTimeMillis());
+        deleteGAPropertyRequest.setData(GAPropertyDTO.toResourceName(googleGTM.getGaPropertyId()));
+        String gaPropertyResourceName =
+                RestTemplateUtil.postForObject(
+                                restTemplate,
+                                dataBridgeApiHost + GA_DELETE_PROPERTY_API_PATH,
+                                deleteGAPropertyRequest,
+                                new ParameterizedTypeReference<OpenAPIResponse<String>>() {})
+                        .getData();
+        log.info("为站点 {} 删除GA property: {}", siteCode, gaPropertyResourceName);
+
+        // 2. 删除GTM container - 通过data-bridge API
+        OpenAPIRequest<String> deleteGTMContainerRequest = new OpenAPIRequest<>();
+        deleteGTMContainerRequest.setRequestServer(this.getClass().getSimpleName());
+        deleteGTMContainerRequest.setRequestTime(System.currentTimeMillis());
+        deleteGTMContainerRequest.setData(
+                GTMContainerDTO.toResourceName(
+                        googleGTM.getGtmAccountId(), googleGTM.getGtmContainerId()));
+        String gtmContainerResourceName =
+                RestTemplateUtil.postForObject(
+                                restTemplate,
+                                dataBridgeApiHost + GTM_DELETE_CONTAINER_API_PATH,
+                                deleteGTMContainerRequest,
+                                new ParameterizedTypeReference<OpenAPIResponse<String>>() {})
+                        .getData();
+        log.info("为站点 {} 删除GTM container: {}", siteCode, gtmContainerResourceName);
+
+        // 3. 更新数据库
+        googleGTMService.removeById(googleGTM.getId());
+
+        return true;
+    }
+}

+ 0 - 7
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/enquiry/controller/EnquiryController.java

@@ -1,7 +0,0 @@
-package org.jeecg.modules.adweb.enquiry.controller;
-
-/**
- * @author wfansh
- */
-public class EnquiryController {
-}

+ 5 - 13
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/seo/service/dataforseo/DataForSEOService.java

@@ -1,7 +1,6 @@
 package org.jeecg.modules.adweb.seo.service.dataforseo;
 
 import cn.hutool.core.collection.CollectionUtil;
-import cn.hutool.json.JSONUtil;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
@@ -18,6 +17,7 @@ import jakarta.annotation.PostConstruct;
 import lombok.extern.slf4j.Slf4j;
 
 import org.apache.commons.lang3.StringUtils;
+import org.jeecg.common.util.FastJsonUtil;
 import org.jeecg.modules.adweb.common.util.AdwebRedisUtil;
 import org.jeecg.modules.adweb.common.util.CommonUtil;
 import org.jeecg.modules.adweb.common.util.DateUtil;
@@ -27,7 +27,6 @@ import org.jeecg.modules.adweb.seo.entity.SeoKeywordsSerp;
 import org.jeecg.modules.adweb.seo.mapper.SeoKeywordsMapper;
 import org.jeecg.modules.adweb.seo.service.ISeoKeywordsSerpService;
 import org.jeecg.modules.adweb.seo.service.ISeoKeywordsService;
-import org.jeecg.modules.adweb.site.entity.AdwebSite;
 import org.jeecg.modules.adweb.site.service.IAdwebSiteService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.beans.factory.annotation.Value;
@@ -95,20 +94,13 @@ public class DataForSEOService {
     /**
      * 全局查询更新DataForSEO keywords Serp数据 - 启动Serp任务并保存到Redis
      *
+     * @param siteCodes 待执行的网站codes,为空时执行所有网站
      * @param keywordType 1 - 指定词; 2 - 长尾词
      * @param limit 最大查询条数
      */
     public void runKeywordsSerpTasks(List<String> siteCodes, int keywordType, int limit) {
         if (ListUtil.isEmpty(siteCodes)) {
-            siteCodes =
-                    adwebSiteService
-                            .list(
-                                    new LambdaQueryWrapper<AdwebSite>()
-                                            .eq(AdwebSite::getStatus, 1)
-                                            .eq(AdwebSite::getRunStatus, 1))
-                            .stream()
-                            .map(AdwebSite::getCode)
-                            .toList();
+            siteCodes = adwebSiteService.getAllActiveSiteCodes();
         }
 
         List<SeoKeywords> seoKeywords =
@@ -167,7 +159,7 @@ public class DataForSEOService {
                     serpApi.googleOrganicTaskPost(serpTaskRequestInfoList);
             log.info(
                     "创建DataForSEO Serp任务,response = {}",
-                    JSONUtil.toJsonStr(serpTaskPostResponseInfo));
+                    FastJsonUtil.toJSONString(serpTaskPostResponseInfo));
             if (serpTaskPostResponseInfo.getStatusCode() != SERP_STATUS_CODE_SUCCESS) {
                 throw new ApiException(serpTaskPostResponseInfo.getStatusMessage());
             }
@@ -223,7 +215,7 @@ public class DataForSEOService {
             String taskId = redisUtil.getString(serpTaskRedisKey);
             SerpGoogleOrganicTaskGetRegularTaskInfo serpTask =
                     serpApi.googleOrganicTaskGetRegular(taskId).getTasks().get(0);
-            log.info("获取DataForSEO Serp任务,response = {}", JSONUtil.toJsonStr(serpTask));
+            log.info("获取DataForSEO Serp任务,response = {}", FastJsonUtil.toJSONString(serpTask));
             if (serpTask.getStatusCode() != SERP_STATUS_CODE_SUCCESS) {
                 log.info(
                         "DataForSEO Serp任务 {} 状态为 {} {}",

+ 0 - 12
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/site/entity/AdwebSite.java

@@ -107,18 +107,6 @@ public class AdwebSite implements Serializable {
     @DateTimeFormat(pattern="yyyy-MM-dd HH:mm:ss")
     @Schema(description = "到期时间,默认为ctime+1年")
     private java.util.Date etime;
-	/**Google GTM Head埋点代码*/
-	@Excel(name = "Google GTM Head埋点代码", width = 15)
-    @Schema(description = "Google GTM Head埋点代码")
-    private java.lang.String gtmHead;
-	/**Google GTM Body埋点代码*/
-	@Excel(name = "Google GTM Body埋点代码", width = 15)
-    @Schema(description = "Google GTM Body埋点代码")
-    private java.lang.String gtmBody;
-	/**gtmType*/
-	@Excel(name = "gtmType", width = 15)
-    @Schema(description = "gtmType")
-    private java.lang.String gtmType;
 	/**站点大小*/
 	@Excel(name = "站点大小", width = 15)
     @Schema(description = "站点大小")

+ 5 - 0
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/site/service/IAdwebSiteService.java

@@ -46,4 +46,9 @@ public interface IAdwebSiteService extends IService<AdwebSite> {
     List<Integer> getAllSiteIdBySiteId(Integer siteId);
 
     public String getSiteNameByCode(String siteCode);
+
+    /**
+     * 查询全部有效的站点code
+     */
+    List<String> getAllActiveSiteCodes();
 }

+ 12 - 0
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/adweb/site/service/impl/AdwebSiteServiceImpl.java

@@ -195,4 +195,16 @@ public class AdwebSiteServiceImpl extends ServiceImpl<AdwebSiteMapper, AdwebSite
         }
         return "";
     }
+
+    /** 查询全部有效的站点code */
+    @Override
+    public List<String> getAllActiveSiteCodes() {
+        return this.list(
+                        new LambdaQueryWrapper<AdwebSite>()
+                                .eq(AdwebSite::getStatus, 1)
+                                .eq(AdwebSite::getRunStatus, 1))
+                .stream()
+                .map(AdwebSite::getCode)
+                .toList();
+    }
 }

+ 26 - 17
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/xxl/DataForSEOJob.java

@@ -6,15 +6,11 @@ import com.xxl.job.core.handler.annotation.XxlJob;
 import lombok.extern.slf4j.Slf4j;
 
 import org.jeecg.modules.adweb.common.constant.AdwebConstant;
+import org.jeecg.modules.adweb.common.util.CommonUtil;
 import org.jeecg.modules.adweb.seo.service.dataforseo.DataForSEOService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
 
-import java.util.Arrays;
-import java.util.Collections;
-import java.util.List;
-import java.util.Objects;
-
 /**
  * DataForSEO Serp查询及同步任务,{@link DataForSEOService}
  *
@@ -26,11 +22,17 @@ public class DataForSEOJob {
 
     @Autowired private DataForSEOService dataForSEOService;
 
+    /**
+     * 查询长尾关键词,Serp更新频率较低,建议三天左右执行一次
+     *
+     * @param siteCodes
+     * @return
+     */
     @XxlJob("runLongTailKeywordsSerpTasksHandler")
-    public ReturnT<String> runLongTailKeywordsSerpTasksHandler(String param) {
-        log.info("执行长尾词Serp查询..., param = {}", param);
+    public ReturnT<String> runLongTailKeywordsSerpTasksHandler(String siteCodes) {
+        log.info("执行长尾词Serp查询..., site codes = {}", siteCodes);
         dataForSEOService.runKeywordsSerpTasks(
-                this.parseSiteCodes(param),
+                CommonUtil.splitAndTrim(siteCodes, ","),
                 AdwebConstant.KEYWORD_TYPE_LONG_TAIL,
                 Integer.MAX_VALUE);
         log.info("执行长尾词Serp查询结束");
@@ -38,16 +40,29 @@ public class DataForSEOJob {
         return ReturnT.SUCCESS;
     }
 
+    /**
+     * 查询指定关键词,Serp更新频率较低,建议三天左右执行一次
+     *
+     * @param siteCodes
+     * @return
+     */
     @XxlJob("runAppointKeywordsSerpTasksHandler")
-    public ReturnT<String> runAppointKeywordsSerpTasksHandler(String param) {
-        log.info("执行指定词Serp查询..., param = {}", param);
+    public ReturnT<String> runAppointKeywordsSerpTasksHandler(String siteCodes) {
+        log.info("执行指定词Serp查询..., site codes = {}", siteCodes);
         dataForSEOService.runKeywordsSerpTasks(
-                this.parseSiteCodes(param), AdwebConstant.KEYWORD_TYPE_APPOINT, Integer.MAX_VALUE);
+                CommonUtil.splitAndTrim(siteCodes, ","),
+                AdwebConstant.KEYWORD_TYPE_APPOINT,
+                Integer.MAX_VALUE);
         log.info("执行指定词Serp查询结束");
 
         return ReturnT.SUCCESS;
     }
 
+    /**
+     * 同步Serp查询结果,基于Redis中的剩余任务 - 负载较低,建议一天执行多次
+     *
+     * @return
+     */
     @XxlJob("syncKeywordsSerpResultsHandler")
     public ReturnT<String> syncKeywordsSerpResultsHandler(String param) {
         log.info("同步关键词Serp查询结果...");
@@ -56,10 +71,4 @@ public class DataForSEOJob {
 
         return ReturnT.SUCCESS;
     }
-
-    private List<String> parseSiteCodes(String param) {
-        return Objects.nonNull(param)
-                ? Arrays.stream(param.split(",")).map(siteCode -> siteCode.trim()).toList()
-                : Collections.EMPTY_LIST;
-    }
 }

+ 9 - 2
jeecg-module-system/jeecg-system-biz/src/main/java/org/jeecg/modules/xxl/GAReportJob.java

@@ -5,6 +5,7 @@ import com.xxl.job.core.handler.annotation.XxlJob;
 
 import lombok.extern.slf4j.Slf4j;
 
+import org.jeecg.modules.adweb.common.util.CommonUtil;
 import org.jeecg.modules.adweb.dmp.service.google.GAReportService;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Component;
@@ -20,10 +21,16 @@ public class GAReportJob {
 
     @Autowired private GAReportService gaReportService;
 
+    /**
+     * 查询GA数据 - 报表以为天为单位,且覆盖旧数据,建议每天执行一至三次
+     *
+     * @param siteCodes
+     * @return
+     */
     @XxlJob("syncGAReportHandler")
-    public ReturnT<String> syncGAReportHandler(String param) {
+    public ReturnT<String> syncGAReportHandler(String siteCodes) {
         log.info("同步GA报表数据...");
-        gaReportService.syncGAReport();
+        gaReportService.syncGAReport(CommonUtil.splitAndTrim(siteCodes, ","));
         log.info("同步GA报表数据结束");
 
         return ReturnT.SUCCESS;

+ 4 - 0
jeecg-module-system/jeecg-system-biz/src/main/resources/google/gtm/body-snippet.tmpl

@@ -0,0 +1,4 @@
+<!-- Google Tag Manager (noscript) -->
+<noscript><iframe src="https://www.googletagmanager.com/ns.html?id=%s"
+height="0" width="0" style="display:none;visibility:hidden"></iframe></noscript>
+<!-- End Google Tag Manager (noscript) -->

+ 7 - 0
jeecg-module-system/jeecg-system-biz/src/main/resources/google/gtm/head-snippet.tmpl

@@ -0,0 +1,7 @@
+<!-- Google Tag Manager -->
+<script>(function(w,d,s,l,i){w[l]=w[l]||[];w[l].push({'gtm.start':
+new Date().getTime(),event:'gtm.js'});var f=d.getElementsByTagName(s)[0],
+j=d.createElement(s),dl=l!='dataLayer'?'&l='+l:'';j.async=true;j.src=
+'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
+})(window,document,'script','dataLayer','%s');</script>
+<!-- End Google Tag Manager -->

+ 4 - 0
jeecg-module-system/jeecg-system-start/src/main/resources/application-dev.yml

@@ -355,6 +355,10 @@ data-bridge:
   api:
     host: http://data-bridge.v3.adwebcloud.com:9002
     token: lgoXX9APqgPLGMPECiNoxaPx
+  gtm:
+    account-id: 6000226571
+  ga:
+    account-id: 191734056
 
 ##GEOIP MMDB 静态数据库文件
 geoip:

+ 4 - 0
jeecg-module-system/jeecg-system-start/src/main/resources/application-prod.yml

@@ -343,6 +343,10 @@ data-bridge:
   api:
     host: http://data-bridge.v3.adwebcloud.com:9002
     token: lgoXX9APqgPLGMPECiNoxaPx
+  gtm:
+    account-id: 6000226571
+  ga:
+    account-id: 191734056
 
 ##GEOIP MMDB 静态数据库文件
 geoip:

+ 4 - 0
jeecg-module-system/jeecg-system-start/src/main/resources/application-test.yml

@@ -340,6 +340,10 @@ data-bridge:
   api:
     host: http://data-bridge.v3.adwebcloud.com:9002
     token: lgoXX9APqgPLGMPECiNoxaPx
+  gtm:
+    account-id: 6000226571
+  ga:
+    account-id: 191734056
 
 ### dataforseo
 dataforseo:

+ 25 - 0
jeecg-module-system/jeecg-system-start/src/test/java/org/jeecg/modules/adweb/dmp/service/google/GoogleServiceTest.java

@@ -1,9 +1,12 @@
 package org.jeecg.modules.adweb.dmp.service.google;
 
+import org.apache.commons.lang3.tuple.Pair;
 import org.jeecg.common.util.FastJsonUtil;
 import org.jeecg.modules.adweb.dmp.dto.google.analytics.report.GAReportRequestDTO;
 import org.jeecg.modules.adweb.dmp.dto.google.analytics.report.ReportType;
 import org.jeecg.modules.adweb.dmp.dto.google.analytics.report.data.CountryChartData;
+import org.jeecg.modules.adweb.dmp.entity.GoogleGTM;
+import org.junit.jupiter.api.Disabled;
 import org.junit.jupiter.api.Test;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.test.context.SpringBootTest;
@@ -17,6 +20,8 @@ public class GoogleServiceTest {
 
     @Autowired GAReportService gaReportService;
 
+    @Autowired GTMAdminService gtmAdminService;
+
     @Test
     public void testGAReport() {
         GAReportRequestDTO gaReportRequest = new GAReportRequestDTO();
@@ -30,4 +35,24 @@ public class GoogleServiceTest {
 
         System.out.println(FastJsonUtil.toJSONString(countryReport));
     }
+
+    @Test
+    @Disabled("GTM + GA帐户资源限制")
+    public void testCreateAndDeleteGTMContainer() {
+        GoogleGTM googleGTM =
+                gtmAdminService.createContainer(
+                        "230206anit40",
+                        "https://www.sourcingstone.com",
+                        "泉州索兴石业有限公司#" + this.getClass().getSimpleName());
+        System.out.println(FastJsonUtil.toJSONString(googleGTM));
+
+        gtmAdminService.deleteContainer(googleGTM.getSiteCode());
+    }
+
+    @Test
+    public void testGetGTMSnippets() {
+        Pair<String, String> snippets = gtmAdminService.getSnippets("GTM-T87NNQ4F");
+        System.out.println(snippets.getLeft());
+        System.out.println(snippets.getRight());
+    }
 }

+ 2 - 2
jeecg-module-system/jeecg-system-start/src/test/java/org/jeecg/modules/adweb/seo/service/DataForSEOTest.java

@@ -17,14 +17,14 @@ public class DataForSEOTest {
     @Autowired private DataForSEOService dataForSEOService;
 
     @Test
-    @Disabled
+    @Disabled("DataForSEO创建Serp任务收费")
     public void testRunKeywordsSerpTasks() {
         dataForSEOService.runKeywordsSerpTasks(
                 Collections.EMPTY_LIST, AdwebConstant.KEYWORD_TYPE_APPOINT, 10);
     }
 
     @Test
-    @Disabled
+    @Disabled("DataForSEO读取Serp结果免费 - 但用例影响生产数据")
     public void testSyncKeywordsSerpResults() {
         dataForSEOService.syncKeywordsSerpResults();
     }