Ver código fonte

Merge remote-tracking branch 'origin/master'

xw 3 dias atrás
pai
commit
ce14fe3a06
100 arquivos alterados com 7032 adições e 61 exclusões
  1. 6 0
      fs-ad-new-api/Dockerfile
  2. 98 0
      fs-ad-new-api/pom.xml
  3. 14 0
      fs-ad-new-api/src/main/java/com/fs/FSServletInitializer.java
  4. 22 0
      fs-ad-new-api/src/main/java/com/fs/FsAdNewApiApplication.java
  5. 126 0
      fs-ad-new-api/src/main/java/com/fs/app/controller/CallbackController.java
  6. 51 0
      fs-ad-new-api/src/main/java/com/fs/app/controller/LandingPageController.java
  7. 42 0
      fs-ad-new-api/src/main/java/com/fs/app/controller/TestController.java
  8. 48 0
      fs-ad-new-api/src/main/java/com/fs/app/controller/TrackingController.java
  9. 67 0
      fs-ad-new-api/src/main/java/com/fs/app/controller/WeChatController.java
  10. 29 0
      fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeService.java
  11. 276 0
      fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeServiceImpl.java
  12. 172 0
      fs-ad-new-api/src/main/java/com/fs/app/facade/ConversionServiceImpl.java
  13. 23 0
      fs-ad-new-api/src/main/java/com/fs/app/facade/IConversionService.java
  14. 71 0
      fs-ad-new-api/src/main/java/com/fs/app/mq/consumer/ConversionTrackingMessageConsumer.java
  15. 91 0
      fs-ad-new-api/src/main/java/com/fs/app/task/ConversionRetryTask.java
  16. 224 0
      fs-ad-new-api/src/main/java/com/fs/app/task/DataSyncTask.java
  17. 182 0
      fs-ad-new-api/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java
  18. 73 0
      fs-ad-new-api/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java
  19. 117 0
      fs-ad-new-api/src/main/java/com/fs/framework/aspectj/DistributeLockAspect.java
  20. 244 0
      fs-ad-new-api/src/main/java/com/fs/framework/aspectj/LogAspect.java
  21. 58 0
      fs-ad-new-api/src/main/java/com/fs/framework/aspectj/ScheduledTaskAspect.java
  22. 29 0
      fs-ad-new-api/src/main/java/com/fs/framework/aspectj/ScheduledTraceIdAspect.java
  23. 31 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/ApplicationConfig.java
  24. 61 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/AsyncConfig.java
  25. 19 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/CorsConfig.java
  26. 92 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/DataSourceConfig.java
  27. 72 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java
  28. 59 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/FilterConfig.java
  29. 30 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/MdcTaskDecorator.java
  30. 148 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/MyBatisConfig.java
  31. 158 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/RedisConfig.java
  32. 50 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/SecurityConfig.java
  33. 33 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/ServerConfig.java
  34. 63 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/ThreadPoolConfig.java
  35. 36 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/TrackIdFilter.java
  36. 77 0
      fs-ad-new-api/src/main/java/com/fs/framework/config/properties/DruidProperties.java
  37. 27 0
      fs-ad-new-api/src/main/java/com/fs/framework/datasource/DynamicDataSource.java
  38. 45 0
      fs-ad-new-api/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java
  39. 56 0
      fs-ad-new-api/src/main/java/com/fs/framework/manager/AsyncManager.java
  40. 40 0
      fs-ad-new-api/src/main/java/com/fs/framework/manager/ShutdownManager.java
  41. 103 0
      fs-ad-new-api/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java
  42. 1 0
      fs-ad-new-api/src/main/resources/META-INF/spring-devtools.properties
  43. 9 0
      fs-ad-new-api/src/main/resources/application.yml
  44. 2 0
      fs-ad-new-api/src/main/resources/banner.txt
  45. 37 0
      fs-ad-new-api/src/main/resources/i18n/messages.properties
  46. 93 0
      fs-ad-new-api/src/main/resources/logback.xml
  47. 15 0
      fs-ad-new-api/src/main/resources/mybatis/mybatis-config.xml
  48. 29 2
      fs-admin/src/main/java/com/fs/course/controller/FsVideoResourceController.java
  49. 33 2
      fs-admin/src/main/java/com/fs/his/controller/FsUserAddressController.java
  50. 2 2
      fs-admin/src/main/resources/application.yml
  51. 60 0
      fs-common/src/main/java/com/fs/common/annotation/DistributeLock.java
  52. 13 0
      fs-common/src/main/java/com/fs/common/constant/DistributeLockConstant.java
  53. 71 0
      fs-common/src/main/java/com/fs/common/constant/RedisKeyConstant.java
  54. 57 0
      fs-common/src/main/java/com/fs/common/constant/RocketMqConstant.java
  55. 51 0
      fs-common/src/main/java/com/fs/common/constant/SystemConstant.java
  56. 24 0
      fs-common/src/main/java/com/fs/common/exception/DistributeLockException.java
  57. 51 0
      fs-common/src/main/java/com/fs/common/exception/ThirdPartyException.java
  58. 52 0
      fs-common/src/main/java/com/fs/common/exception/base/BusinessException.java
  59. 102 0
      fs-common/src/main/java/com/fs/common/result/Result.java
  60. 72 0
      fs-common/src/main/java/com/fs/common/result/ResultCode.java
  61. 124 0
      fs-common/src/main/java/com/fs/common/utils/RedisUtil.java
  62. 102 0
      fs-common/src/main/java/com/fs/common/utils/SignatureUtil.java
  63. 65 0
      fs-common/src/main/java/com/fs/common/utils/SnowflakeUtil.java
  64. 26 0
      fs-common/src/main/java/com/fs/common/utils/TraceIdUtil.java
  65. 103 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/AdvChannelController.java
  66. 46 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/AdvConfigController.java
  67. 58 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/AdvProjectController.java
  68. 73 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/AdvertiserController.java
  69. 112 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/CallbackAccountController.java
  70. 48 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/ConversionLogController.java
  71. 269 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/DomainController.java
  72. 202 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/LandingPageTemplateController.java
  73. 135 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/PromotionAccountController.java
  74. 104 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/SiteController.java
  75. 121 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/StatisticsController.java
  76. 98 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/TrackingLinkController.java
  77. 104 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwAssignRuleController.java
  78. 212 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwCustomerLinkController.java
  79. 75 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwGroupActualController.java
  80. 66 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwGroupLiveCodeController.java
  81. 4 0
      fs-company/src/main/java/com/fs/framework/config/MyBatisConfig.java
  82. 2 2
      fs-company/src/main/java/com/fs/framework/security/handle/AuthenticationEntryPointImpl.java
  83. 29 1
      fs-qw-api/src/main/java/com/fs/app/service/QwDataCallbackService.java
  84. 61 0
      fs-qw-api/src/main/java/com/fs/framework/config/AsyncConfig.java
  85. 2 0
      fs-service/src/main/java/com/fs/course/domain/FsVideoResource.java
  86. 8 6
      fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java
  87. 2 0
      fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java
  88. 78 46
      fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java
  89. 2 0
      fs-service/src/main/java/com/fs/hisStore/param/LoginMpWxParam.java
  90. 8 0
      fs-service/src/main/java/com/fs/newAdv/constant/AdvConfigConstant.java
  91. 34 0
      fs-service/src/main/java/com/fs/newAdv/constant/ConversionTrackingMessage.java
  92. 27 0
      fs-service/src/main/java/com/fs/newAdv/constant/MqTopicConstant.java
  93. 54 0
      fs-service/src/main/java/com/fs/newAdv/domain/AdvChannelEntity.java
  94. 44 0
      fs-service/src/main/java/com/fs/newAdv/domain/AdvEventType.java
  95. 45 0
      fs-service/src/main/java/com/fs/newAdv/domain/AdvProjectEntity.java
  96. 82 0
      fs-service/src/main/java/com/fs/newAdv/domain/Advertiser.java
  97. 78 0
      fs-service/src/main/java/com/fs/newAdv/domain/AlertLog.java
  98. 80 0
      fs-service/src/main/java/com/fs/newAdv/domain/ApiCallLog.java
  99. 109 0
      fs-service/src/main/java/com/fs/newAdv/domain/CallbackAccount.java
  100. 103 0
      fs-service/src/main/java/com/fs/newAdv/domain/CallbackLog.java

+ 6 - 0
fs-ad-new-api/Dockerfile

@@ -0,0 +1,6 @@
+FROM openjdk:8-jre
+# java版本,最好使用openjdk,而不是类似于Java:1.8
+COPY ./target/fs-ad-new-api.jar fs-ad-new-api.jar
+# 向外暴露的接口,最好与项目yml文件中的端口一致
+ENTRYPOINT ["java","-jar","fs-ad-new-api.jar"]
+# 执行启动命令java -jar

+ 98 - 0
fs-ad-new-api/pom.xml

@@ -0,0 +1,98 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <parent>
+        <artifactId>fs</artifactId>
+        <groupId>com.fs</groupId>
+        <version>1.1.0</version>
+    </parent>
+    <modelVersion>4.0.0</modelVersion>
+    <artifactId>fs-ad-new-api</artifactId>
+    <description>
+       新投流接口
+    </description>
+    <dependencies>
+
+        <!-- Mysql驱动包 -->
+        <dependency>
+            <groupId>mysql</groupId>
+            <artifactId>mysql-connector-java</artifactId>
+        </dependency>
+
+        <!-- SpringBoot Web容器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-web</artifactId>
+        </dependency>
+
+        <!-- SpringBoot 拦截器 -->
+        <dependency>
+            <groupId>org.springframework.boot</groupId>
+            <artifactId>spring-boot-starter-aop</artifactId>
+        </dependency>
+
+        <!-- 阿里数据库连接池 -->
+        <dependency>
+            <groupId>com.alibaba</groupId>
+            <artifactId>druid-spring-boot-starter</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.fs</groupId>
+            <artifactId>fs-service</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.apache.rocketmq</groupId>
+            <artifactId>rocketmq-spring-boot-starter</artifactId>
+            <version>2.2.3</version>
+        </dependency>
+    </dependencies>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.springframework.boot</groupId>
+                <artifactId>spring-boot-maven-plugin</artifactId>
+                <version>2.1.1.RELEASE</version>
+                <configuration>
+                    <fork>true</fork> <!-- 如果没有该配置,devtools不会生效 -->
+                </configuration>
+                <executions>
+                    <execution>
+                        <goals>
+                            <goal>repackage</goal>
+                        </goals>
+                    </execution>
+                </executions>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-war-plugin</artifactId>
+                <version>3.1.0</version>
+                <configuration>
+                    <failOnMissingWebXml>false</failOnMissingWebXml>
+                    <warName>${project.artifactId}</warName>
+                </configuration>
+            </plugin>
+
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-compiler-plugin</artifactId>
+                <configuration>
+                    <compilerArgs>
+                        <arg>-parameters</arg>
+                    </compilerArgs>
+                </configuration>
+            </plugin>
+
+        </plugins>
+        <finalName>${project.artifactId}</finalName>
+    </build>
+
+</project>

+ 14 - 0
fs-ad-new-api/src/main/java/com/fs/FSServletInitializer.java

@@ -0,0 +1,14 @@
+package com.fs;
+
+import org.springframework.boot.builder.SpringApplicationBuilder;
+import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;
+
+
+public class FSServletInitializer extends SpringBootServletInitializer
+{
+    @Override
+    protected SpringApplicationBuilder configure(SpringApplicationBuilder application)
+    {
+        return application.sources(FsAdNewApiApplication.class);
+    }
+}

+ 22 - 0
fs-ad-new-api/src/main/java/com/fs/FsAdNewApiApplication.java

@@ -0,0 +1,22 @@
+package com.fs;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.annotation.EnableScheduling;
+
+/**
+ * 启动程序
+ */
+@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
+@EnableAsync
+@EnableScheduling
+public class FsAdNewApiApplication {
+    public static void main(String[] args) {
+        SpringApplication.run(FsAdNewApiApplication.class, args);
+        System.out.println("========================================");
+        System.out.println("ad-new-api启动成功");
+        System.out.println("========================================");
+    }
+}

+ 126 - 0
fs-ad-new-api/src/main/java/com/fs/app/controller/CallbackController.java

@@ -0,0 +1,126 @@
+package com.fs.app.controller;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.fs.newAdv.integration.client.IAccessTokenClient;
+import com.fs.newAdv.integration.client.IApiClient;
+import com.fs.newAdv.integration.factory.AdvertiserHandlerFactory;
+import com.fs.common.result.Result;
+import com.fs.newAdv.domain.PromotionAccount;
+import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.service.IPromotionAccountService;
+import com.fs.newAdv.vo.AccessTokenByAuthCodeVo;
+import com.fs.newAdv.vo.AccessTokenVo;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 广告商回调接口
+ */
+@Slf4j
+@RestController
+@RequestMapping("/callBack")
+public class CallbackController {
+
+    @Autowired
+    private AdvertiserHandlerFactory advertiserHandlerFactory;
+    @Autowired
+    private IPromotionAccountService promotionAccountService;
+
+    /**
+     * 百度回调接口
+     *
+     * @param request
+     * @return
+     */
+    @GetMapping("/baidu/getAuthCode")
+    public Result<String> getBaiduAuthCode(HttpServletRequest request) {
+        getAccessToken(AdvertiserTypeEnum.BAIDU, request.getParameter("state"), request.getParameter("authCode"));
+        return Result.success("");
+    }
+
+    /**
+     * 巨量回调接口
+     *
+     * @param request
+     * @return
+     */
+    @GetMapping("/oceanEngine/getAuthCode")
+    public Result<String> getOceanEngineAuthCode(HttpServletRequest request) {
+        getAccessToken(AdvertiserTypeEnum.OCEANENGINE, request.getParameter("state"), request.getParameter("auth_code"));
+        return Result.success("");
+    }
+
+    /**
+     * 腾讯回调接口
+     *
+     * @param request
+     * @return
+     */
+    @GetMapping("/tencent/getAuthCode")
+    public Result<String> getTencentTAuthCode(HttpServletRequest request) {
+        getAccessToken(AdvertiserTypeEnum.TENCENT, request.getParameter("state"), request.getParameter("authorization_code"));
+        return Result.success("");
+    }
+
+    /**
+     * OPPO回调接口
+     *
+     * @param request
+     * @return
+     */
+    @GetMapping("/oppo/getAuthCode")
+    public Result<String> getOppoAuthCode(HttpServletRequest request) {
+        getAccessToken(AdvertiserTypeEnum.OCEANENGINE, request.getParameter("state"), request.getParameter("auth_code"));
+        return Result.success("");
+    }
+
+    /**
+     * vivo回调接口
+     *
+     * @param request
+     * @return
+     */
+    @GetMapping("/vivo/getAuthCode")
+    public Result<String> getViVoAuthCode(HttpServletRequest request) {
+        getAccessToken(AdvertiserTypeEnum.OCEANENGINE, request.getParameter("state"), request.getParameter("auth_code"));
+        return Result.success("");
+    }
+
+    /**
+     * iqiyi回调接口
+     *
+     * @param request
+     * @return
+     */
+    @GetMapping("/iqiyi/getAuthCode")
+    public Result<String> getiqiyiAuthCode(HttpServletRequest request) {
+        getAccessToken(AdvertiserTypeEnum.OCEANENGINE, request.getParameter("state"), request.getParameter("auth_code"));
+        return Result.success("");
+    }
+
+    private void getAccessToken(AdvertiserTypeEnum advertiserType, String state, String authCode) {
+        PromotionAccount byId = promotionAccountService.getById(state);
+        // 获取请求参数
+        IApiClient apiClient = advertiserHandlerFactory.getApiClient(advertiserType);
+        IAccessTokenClient tokenClient = (IAccessTokenClient) apiClient;
+        AccessTokenVo accessToken = tokenClient.getAccessTokenByAuthCode(AccessTokenByAuthCodeVo
+                .builder()
+                .userId(byId.getAdAccountId())
+                .appId(byId.getAppId())
+                .authCode(authCode)
+                .appSecret(byId.getAppSecret())
+                .build());
+        if (ObjectUtil.isNotEmpty(accessToken)) {
+            byId.setAccessToken(accessToken.getAccessToken());
+            byId.setRefreshToken(accessToken.getRefreshToken());
+            promotionAccountService.updateById(byId);
+        } else {
+            log.error("获取accessToken失败:{}", byId.getId());
+        }
+    }
+}

+ 51 - 0
fs-ad-new-api/src/main/java/com/fs/app/controller/LandingPageController.java

@@ -0,0 +1,51 @@
+package com.fs.app.controller;
+
+import com.fs.app.facade.CallbackProcessingFacadeService;
+import com.fs.common.result.Result;
+import com.fs.newAdv.dto.req.LandingIndexReq;
+import com.fs.newAdv.dto.req.WeChatLandingIndexReq;
+import com.fs.newAdv.dto.res.LandingIndexRes;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 落地页控制器
+ * 用于接收广告平台跳转时携带的参数
+ *
+ * @author zhangqin
+ * @date 2025-11-04
+ */
+@Slf4j
+@RestController
+@RequestMapping("/landing")
+public class LandingPageController {
+
+    @Autowired
+    private CallbackProcessingFacadeService facadeService;
+
+    /**
+     * 落地页访问
+     */
+    @PostMapping("/h5/home")
+    public Result<LandingIndexRes> track(
+            @RequestBody LandingIndexReq req) {
+        log.info("落地页访问追踪:req={}", req);
+        // 查询落地页模板
+        return Result.success(facadeService.getLandingIndexBySiteId(req.getAllParams()));
+    }
+
+    /**
+     * 小程序访问
+     */
+    @PostMapping("/mini/home")
+    public Result<LandingIndexRes> track(@RequestBody WeChatLandingIndexReq req) {
+        return Result.success(facadeService.getWxLandingIndexBySiteId(req));
+    }
+
+
+}
+

+ 42 - 0
fs-ad-new-api/src/main/java/com/fs/app/controller/TestController.java

@@ -0,0 +1,42 @@
+package com.fs.app.controller;
+
+import com.fs.newAdv.integration.client.advertiser.BaiduApiClient;
+import com.fs.newAdv.enums.SystemEventTypeEnum;
+import com.fs.newAdv.event.ConversionEventPublisher;
+import com.fs.newAdv.service.IPromotionAccountService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * 广告商监测链接
+ *
+ * @author zhangqin
+ */
+@Slf4j
+@RestController
+@RequestMapping("/test")
+public class TestController {
+
+    @Autowired
+    private ConversionEventPublisher conversionEventPublisher;
+    @Autowired
+    private BaiduApiClient baiduApiClient;
+    @Autowired
+    private IPromotionAccountService promotionAccountService;
+
+    @GetMapping("/test1/{traceId}")
+    public void test1(@PathVariable("traceId") String traceId) {
+        log.info("模拟 当日加群 事件完成");
+        conversionEventPublisher.publishConversionEvent(traceId, SystemEventTypeEnum.GROUP_TODAY);
+    }
+
+    @GetMapping("/test2/{traceId}")
+    public void test2(@PathVariable("traceId") String traceId) {
+        log.info("模拟 当日加微 事件完成");
+        conversionEventPublisher.publishConversionEvent(traceId, SystemEventTypeEnum.WEI_CHAT_TODAY);
+    }
+}

+ 48 - 0
fs-ad-new-api/src/main/java/com/fs/app/controller/TrackingController.java

@@ -0,0 +1,48 @@
+package com.fs.app.controller;
+
+import com.fs.app.facade.CallbackProcessingFacadeService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import javax.servlet.http.HttpServletResponse;
+import java.util.Map;
+
+/**
+ * 广告商监测链接
+ *
+ * @author zhangqin
+ */
+@Slf4j
+@RestController
+@RequestMapping("/track")
+public class TrackingController {
+
+    @Autowired
+    private CallbackProcessingFacadeService facadeService;
+
+
+    /**
+     * 监测链接端口
+     *
+     * @param allParams 所有URL参数
+     * @param response  HTTP响应
+     */
+    @GetMapping("/click/{advertiserCode}")
+    public void trackBaidu(
+            @RequestParam Map<String, String> allParams,
+            @PathVariable("advertiserCode") Long advertiserCode,
+            HttpServletResponse response) {
+        log.info("接收监测请求 | params={}", allParams);
+        try {
+            // 2. 保存点击追踪记录
+            facadeService.saveClickTrace(advertiserCode,allParams);
+            // 3. 返回 200 OK
+            response.setStatus(HttpServletResponse.SC_OK);
+        } catch (Exception e) {
+            log.error("监测请求处理失败 | params={}", allParams, e);
+            response.setStatus(HttpServletResponse.SC_OK);
+        }
+    }
+
+}

+ 67 - 0
fs-ad-new-api/src/main/java/com/fs/app/controller/WeChatController.java

@@ -0,0 +1,67 @@
+package com.fs.app.controller;
+
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.app.facade.CallbackProcessingFacadeService;
+import com.fs.common.constant.SystemConstant;
+import com.fs.common.result.Result;
+import com.fs.wx.miniapp.config.WxMaProperties;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+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;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 广告商监测链接
+ *
+ * @author zhangqin
+ */
+@Slf4j
+@RestController
+@RequestMapping("/scheme")
+public class WeChatController {
+
+    @Autowired
+    private CallbackProcessingFacadeService facadeService;
+
+    @Autowired
+    private WxMaProperties properties;
+
+    @GetMapping("/getSchemeUrl")
+    public Result<String> getSchemeUrl(@RequestParam(value = "traceId") String traceId) {
+        String appId = "wx0447a16ef6199f03";
+        String secret = "f063fcd818e31d4c89013a67f5123990";
+        HttpResponse execute2 = HttpRequest.get("https://api.weixin.qq.com/cgi-bin/token")
+                .form("grant_type", "client_credential")
+                .form("appid", appId)
+                .form("secret", secret)
+                .timeout(SystemConstant.API_TIMEOUT)
+                .execute();
+        JSONObject obj = JSONObject.parseObject(execute2.body());
+        String access_token = obj.getString("access_token");
+
+        Map<String, Object> map = new HashMap<>();
+        Map<String, Object> map2 = new HashMap<>();
+        map2.put("path", "/pages/shopping/productDetails");
+        map2.put("query", "traceId=" + traceId);
+        //map2.put("env_version", "trial");
+        map.put("jump_wxa", map2);
+        map.put("is_expire", false);
+        HttpResponse execute = HttpRequest.post("https://api.weixin.qq.com/wxa/generatescheme?access_token=" + access_token)
+                .header("Content-Type", "application/json")
+                .body(JSONUtil.toJsonStr(map))
+                .timeout(SystemConstant.API_TIMEOUT)
+                .execute();
+        log.info("getSchemeUrl:{}", execute.body());
+        obj = JSONObject.parseObject(execute.body());
+        //response.addHeader("Access-Control-Allow-Origin", "*");
+        return Result.success(obj.getString("openlink"));
+    }
+}

+ 29 - 0
fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeService.java

@@ -0,0 +1,29 @@
+package com.fs.app.facade;
+
+import com.fs.newAdv.dto.req.WeChatLandingIndexReq;
+import com.fs.newAdv.dto.res.LandingIndexRes;
+
+import java.util.Map;
+
+public interface CallbackProcessingFacadeService {
+
+
+    /**
+     * 链化追踪点击
+     * @param allParams
+     */
+    void saveClickTrace(Long advertiserCode,Map<String,String> allParams);
+    /**
+     * 根据站点ID获取落地页信息
+     *
+     * @param siteId
+     * @return
+     */
+    LandingIndexRes getLandingIndexBySiteId(Map<String, String> allParams);
+
+    //----------------------code回调---------------------------------
+    void gdtGetAuthCode(Integer code, Long state);
+
+
+    LandingIndexRes getWxLandingIndexBySiteId(WeChatLandingIndexReq req);
+}

+ 276 - 0
fs-ad-new-api/src/main/java/com/fs/app/facade/CallbackProcessingFacadeServiceImpl.java

@@ -0,0 +1,276 @@
+package com.fs.app.facade;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONArray;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.fs.newAdv.integration.adapter.IAdvertiserAdapter;
+import com.fs.newAdv.integration.factory.AdvertiserHandlerFactory;
+import com.fs.common.exception.base.BusinessException;
+import com.fs.common.utils.RedisUtil;
+import com.fs.newAdv.domain.LandingPageTemplate;
+import com.fs.newAdv.domain.Lead;
+import com.fs.newAdv.domain.Site;
+import com.fs.newAdv.dto.req.WeChatLandingIndexReq;
+import com.fs.newAdv.dto.res.LandingIndexRes;
+import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.service.ILandingPageTemplateService;
+import com.fs.newAdv.service.ILeadService;
+import com.fs.newAdv.service.ISiteService;
+import com.fs.qw.domain.QwAssignRule;
+import com.fs.qw.domain.QwAssignRuleUser;
+import com.fs.qw.domain.QwContactWay;
+import com.fs.qw.domain.QwGroupActual;
+import com.fs.qw.service.IQwAssignRuleService;
+import com.fs.qw.service.IQwAssignRuleUserService;
+import com.fs.qw.service.IQwContactWayService;
+import com.fs.qw.service.IQwGroupActualService;
+import com.fs.qwApi.Result.QwAddContactWayResult;
+import com.fs.qwApi.param.QwAddContactWayParam;
+import com.fs.qwApi.service.QwApiService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+@Service
+@Slf4j
+public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFacadeService {
+    @Autowired
+    private AdvertiserHandlerFactory handlerFactory;
+    @Autowired
+    private ILeadService leadService;
+    @Autowired
+    private ISiteService siteService;
+    @Autowired
+    private IQwAssignRuleUserService assignRuleUserService;
+    @Autowired
+    private IQwAssignRuleService assignRuleService;
+    @Autowired
+    private ILandingPageTemplateService landingPageTemplateService;
+    @Autowired
+    private IQwGroupActualService actualService;
+    @Autowired
+    private QwApiService qwApiService;
+    @Autowired
+    private IQwContactWayService contactWayService;
+
+    @Autowired
+    private RedisUtil redisUtil;
+
+    private static final String TEMPLATE_DATA = "new-adv:template-data:";
+
+    @Override
+    public void saveClickTrace(Long advertiserCode, Map<String, String> allParams) {
+        IAdvertiserAdapter advertiserAdapter = handlerFactory.getAdapter(AdvertiserTypeEnum.getByCode(advertiserCode));
+        Lead lead = advertiserAdapter.adaptCallbackData(allParams);
+        Lead byTraceId = leadService.getByTraceId(lead.getTraceId());
+        if (ObjectUtil.isNotEmpty(byTraceId)) {
+            throw new BusinessException("监测信息已存在: " + lead.getTraceId());
+        }
+        lead.setStatus(0);
+        lead.setClickTrigger(1);
+        lead.setTraceRawParams(JSONUtil.toJsonStr(allParams));
+        boolean saved = leadService.save(lead);
+        if (!saved) {
+            log.error("线索保存失败:{}", lead);
+            throw new RuntimeException("线索保存失败");
+        }
+    }
+
+    private String getTraceIdByAdvertiser(AdvertiserTypeEnum byCode, Map<String, String> allParams) {
+        String traceId;
+        switch (byCode) {
+            case OCEANENGINE:
+            case TENCENT:
+            case OPPO:
+                traceId = allParams.get("click_id");
+                break;
+            case BAIDU:
+                traceId = allParams.get("bd_vid");
+                break;
+            case VIVO:
+                traceId = allParams.get("requestId");
+                break;
+            case IQIYI:
+                traceId = allParams.get("traceId");
+                break;
+            default:
+                traceId = "ylrz_test";
+        }
+        if (StrUtil.isEmpty(traceId)) {
+            traceId = "ylrz_test";
+        }
+
+        return traceId;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public LandingIndexRes getLandingIndexBySiteId(Map<String, String> allParams) {
+        // 站点信息
+        String paramsSiteId = allParams.get("siteId");
+        if (ObjectUtil.isEmpty(paramsSiteId)) {
+            log.info("站点id不存在:{}", paramsSiteId);
+            return null;
+        }
+        Long siteId = Long.valueOf(paramsSiteId);
+        Site byId = siteService.getById(siteId);
+        if (ObjectUtil.isEmpty(byId)) {
+            log.info("站点信息不存在:{}", siteId);
+            return null;
+        }
+        // 广告商信息
+        Long advertiserId = byId.getAdvertiserId();
+        // 访问链路id
+        String traceId = getTraceIdByAdvertiser(Objects.requireNonNull(AdvertiserTypeEnum.getByCode(advertiserId)), allParams);
+        // 线索信息
+        Lead byTraceId = leadService.getByTraceId(traceId);
+        boolean isNewLead = ObjectUtil.isEmpty(byTraceId);
+        if (isNewLead) {
+            byTraceId = new Lead();
+            byTraceId.setAdvertiserId(advertiserId);
+            byTraceId.setSiteId(siteId);
+            byTraceId.setTraceId(traceId);
+        } else {
+            // 检查站点和广告商信息是否异常
+            if (!Objects.equals(byTraceId.getSiteId(), siteId)) {
+                log.info("落地页站点信息异常:{}---{}", byTraceId.getSiteId(), siteId);
+            }
+            if (!Objects.equals(byTraceId.getAdvertiserId(), advertiserId)) {
+                log.info("落地页广告商信息异常:{}---{}", byTraceId.getAdvertiserId(), advertiserId);
+            }
+        }
+        // 模板缓存
+        Object ca = redisUtil.get(TEMPLATE_DATA + traceId);
+        String templateData;
+        if (ca != null) {
+            templateData = String.valueOf(ca);
+        } else {
+            // 查询模板数据
+            LandingPageTemplate landingPageTemplate = landingPageTemplateService.getById(byId.getLaunchPageId());
+            JSONObject jsonObject = JSONUtil.parseObj(landingPageTemplate.getTemplateData());
+            // 替换二维码链接
+            updateQrCodeInTemplate(jsonObject, traceId, byId, byTraceId);
+            templateData = JSONUtil.toJsonStr(jsonObject);
+        }
+
+
+        // 保存或更新 线索信息
+        LocalDateTime now = LocalDateTime.now();
+        byTraceId.setLandingPageRawParams(JSONUtil.toJsonStr(allParams));
+        byTraceId.setLandingPageTrigger(1);
+        byTraceId.setLandingPageTs(now);
+        byTraceId.setUpdateTime(now);
+        if (isNewLead) {
+            leadService.save(byTraceId);
+        } else {
+            leadService.updateById(byTraceId);
+        }
+
+        // 封装返回结果
+        LandingIndexRes res = new LandingIndexRes();
+        redisUtil.set(TEMPLATE_DATA + traceId, templateData, 24, TimeUnit.HOURS);
+        res.setTemplateData(templateData);
+        res.setTraceId(byTraceId.getTraceId());
+        return res;
+    }
+
+    /**
+     * 更新模板中的二维码信息
+     */
+    private void updateQrCodeInTemplate(JSONObject templateData, String traceId, Site site, Lead lead) {
+        JSONArray configList = templateData.getJSONArray("configList");
+        if (configList == null || configList.isEmpty()) {
+            return;
+        }
+        AtomicReference<String> qrCode = new AtomicReference<>("");
+        configList.stream()
+                .map(config -> (JSONObject) config)
+                .map(config -> config.getJSONArray("moduleList"))
+                .filter(ObjectUtil::isNotEmpty)
+                .flatMap(Collection::stream)
+                .map(module -> (JSONObject) module)
+                .filter(module -> "h5-qrcode".equals(module.getStr("type")))
+                .forEach(module -> {
+                    if (StrUtil.isEmpty(qrCode.get())) {
+                        qrCode.set(getQrCodeByAllocationRuleId(site.getLaunchType(), site.getAllocationRule(), site.getAllocationRuleId(), lead));
+                    }
+                    module.set("workUrl", qrCode.get());
+                });
+    }
+
+    private String getQrCodeByAllocationRuleId(Integer launchType,
+                                               Integer allocationRule,
+                                               Long allocationRuleId,
+                                               Lead byTraceId) {
+        // 二维码
+        String qrCode = "";
+        if (allocationRule == 1) {
+            // 个人分配规则
+            QwAssignRule byId1 = assignRuleService.getById(allocationRuleId);
+            QwAssignRuleUser qwAssignRuleUser = assignRuleUserService.getUserByAllocationRuleId(allocationRuleId, byId1.getAssignType());
+            byTraceId.setQwAssignRuleUserId(qwAssignRuleUser.getId());
+            if (byId1.getAssignType().equals(3)) {
+                List<String> users = Collections.singletonList(qwAssignRuleUser.getQwUserId());
+                QwAddContactWayParam qwAddContactWayParam = new QwAddContactWayParam();
+                qwAddContactWayParam.setType(1);
+                qwAddContactWayParam.setScene(2);
+                qwAddContactWayParam.setUser(users);
+                qwAddContactWayParam.setSkip_verify(true);
+                QwAddContactWayResult qwAddContactWayResult = qwApiService.addContactWay(qwAddContactWayParam, qwAssignRuleUser.getCorpId());
+                qrCode = qwAddContactWayResult.getQr_code();
+            }
+        } else if (allocationRule == 2) {
+            // 活码分配规则
+            if (launchType.equals(1)) {
+                // 线上规则--群活码
+                QwGroupActual qwGroupActual = actualService.getGroupActualByAllocationRuleId(allocationRuleId);
+                byTraceId.setQwGroupActualId(qwGroupActual.getId());
+                byTraceId.setQwGroupLiveCodeId(qwGroupActual.getLiveCodeId());
+                qrCode = qwGroupActual.getGroupUrl();
+            } else if (launchType.equals(2)) {
+                // 线下规则--个人活码
+                QwContactWay qwContactWay = contactWayService.selectQwContactWayById(allocationRuleId);
+                byTraceId.setQwContactWayId(qwContactWay.getId());
+                qrCode = qwContactWay.getQrCode();
+            }
+        }
+        log.info("落地页广告获取二维码: {}", qrCode);
+        return qrCode;
+    }
+
+    @Override
+    public void gdtGetAuthCode(Integer code, Long state) {
+   /*     if (code == null || state == null) {
+            return;
+        }
+        PromotionAccount byId = promotionAccountService.getById(state);
+        if (byId == null) {
+            return;
+        }
+        IApiClient apiClient = handlerFactory.getApiClient(AdvertiserTypeEnum.TENCENT);
+        if (apiClient instanceof IAccessTokenClient) {
+            IAccessTokenClient tokenClient = (IAccessTokenClient) apiClient;
+            Map<String, String> accessToken = tokenClient.getAccessToken(code, byId.getAppId(), byId.getAppSecret(), byId.getCallbackUrl());
+            byId.setAccessToken(accessToken.get("access_token"));
+            byId.setRefreshToken(accessToken.get("refresh_token"));
+        }*/
+    }
+
+    @Override
+    public LandingIndexRes getWxLandingIndexBySiteId(WeChatLandingIndexReq req) {
+        // 更新授权页访问信息记录
+        leadService.updateAuthIndex(req.getTraceId(),req.getType());
+        String templateData = String.valueOf(redisUtil.get(TEMPLATE_DATA + req.getTraceId()));
+        LandingIndexRes landingIndexRes = new LandingIndexRes();
+        landingIndexRes.setTemplateData(templateData);
+        return landingIndexRes;
+    }
+}

+ 172 - 0
fs-ad-new-api/src/main/java/com/fs/app/facade/ConversionServiceImpl.java

@@ -0,0 +1,172 @@
+package com.fs.app.facade;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
+import com.fs.newAdv.integration.adapter.IAdvertiserAdapter;
+import com.fs.newAdv.integration.client.IApiClient;
+import com.fs.newAdv.integration.factory.AdvertiserHandlerFactory;
+import com.fs.common.utils.RedisUtil;
+import com.fs.common.utils.SnowflakeUtil;
+import com.fs.newAdv.domain.*;
+import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.enums.SystemEventTypeEnum;
+import com.fs.newAdv.mapper.ConversionLogMapper;
+import com.fs.newAdv.mapper.ConversionTargetMapper;
+import com.fs.newAdv.service.ICallbackAccountService;
+import com.fs.newAdv.service.ILeadService;
+import com.fs.newAdv.service.IPromotionAccountService;
+import com.fs.newAdv.service.ISiteService;
+import com.fs.newAdv.vo.ConversionEventVo;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 转化回传服务实现类
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Service
+public class ConversionServiceImpl implements IConversionService {
+
+    @Autowired
+    private ISiteService siteService;
+    @Autowired
+    private ICallbackAccountService callbackAccountService;
+    @Autowired
+    private IPromotionAccountService promotionAccountService;
+
+
+    @Autowired
+    private ConversionLogMapper conversionLogMapper;
+
+    @Autowired
+    private ConversionTargetMapper conversionTargetMapper;
+
+    @Autowired
+    private AdvertiserHandlerFactory advertiserHandlerFactory;
+
+    @Autowired
+    private RedisUtil redisUtil;
+
+    @Autowired
+    private ILeadService leadService;
+
+    @Override
+    public boolean reportTrackingToAdvertiser(SystemEventTypeEnum systemEventTypeEnum, String traceId) {
+        // 线索信息
+        Lead lead = leadService.getByTraceId(traceId);
+        // 站点信息
+        Site site = siteService.getById(lead.getSiteId());
+        if (site == null) {
+            log.error("站点不存在:{}", site);
+            return false;
+        }
+        // 查询回传信息
+        CallbackAccount callbackAccount = callbackAccountService.getById(site.getCallbackAccountId());
+        // 回传事件
+        List<ConversionEventVo> conversionEventVo = callbackAccount.getConversionEventVo();
+        if (CollUtil.isEmpty(conversionEventVo)) {
+            log.error("回传事件不存在: {}", traceId);
+            return false;
+        }
+
+        ConversionEventVo advertiserEventType = conversionEventVo.stream()
+                .filter(item -> item.getSystemEventType().equals(systemEventTypeEnum.getCode()))
+                .findFirst()
+                .orElse(null);
+
+        if (ObjectUtil.isEmpty(advertiserEventType)) {
+            log.error("回传事件未匹配: {}", traceId);
+            return false;
+        }
+
+        // 查询广告商账号
+        PromotionAccount promotionAccount = promotionAccountService.getById(site.getPromotionAccountId());
+        if (promotionAccount == null) {
+            log.error("回传账号不存在:{}", site.getPromotionAccountId());
+            return false;
+        }
+
+        Map<String, Object> params = JSONUtil.toBean(lead.getTraceRawParams(), Map.class);
+        // 构建回传参数
+        Map<String, Object> conversionData = new HashMap<>();
+        // ------------------------------通用参数----------
+        conversionData.put("adAccountId", callbackAccount.getAdAccountId());
+        conversionData.put("appId", callbackAccount.getAppId());
+        conversionData.put("appSecret", callbackAccount.getAppSecret());
+        conversionData.put("accessToken", callbackAccount.getAccessToken());
+        conversionData.put("ip", lead.getIp());
+        conversionData.put("scr_id", callbackAccount.getScrId());
+        conversionData.put("traceId", traceId);
+        conversionData.put("eventType", advertiserEventType.getAdvertiserEventType());
+        conversionData.put("timestamp", System.currentTimeMillis() / 1000);
+        // 添加平台适应参数
+        IAdvertiserAdapter adapter = advertiserHandlerFactory.getAdapter(AdvertiserTypeEnum.getByCode(callbackAccount.getAdvertiserId()));
+        adapter.uploadConversionData(conversionData, params);
+
+
+        // 调用API回传
+        IApiClient apiClient = advertiserHandlerFactory.getApiClient(AdvertiserTypeEnum.getByCode(site.getAdvertiserId()));
+        boolean b = apiClient.reportConversion(conversionData);
+
+        // 保存转化日志(待回传状态)
+        ConversionLog conversionLog = saveConversionLog(
+                site.getCallbackAccountId(),
+                site.getId(), site.getAdvertiserId(), site.getAdvertiserName(),
+                advertiserEventType, traceId, conversionData, b);
+        return b;
+    }
+
+
+    /**
+     * 构建巨量引擎转化数据
+     */
+    private Map<String, Object> buildOceanEngineConversionData(
+            String clickId, String eventType, Double value) {
+
+  /*      Map<String, Object> data = new HashMap<>();
+        data.put("clickId", clickId);
+        data.put("eventType", mapEventType(eventType));
+
+        if (value != null && value > 0) {
+            data.put("value", value);
+        }*/
+
+        return null;
+    }
+
+    /**
+     * 保存转化日志
+     */
+    private ConversionLog saveConversionLog(Long callbackAccountId,Long siteId, Long advertiserId,
+                                            String advertiserName, ConversionEventVo eventType,
+                                            String clickId,
+                                            Map<String, Object> conversionData,
+                                            boolean status) {
+        ConversionLog log = new ConversionLog();
+        log.setId(SnowflakeUtil.nextId());
+        log.setCallbackAccountId(callbackAccountId);
+        log.setSiteId(siteId);
+        log.setAdvertiserId(advertiserId);
+        log.setAdvertiserName(advertiserName);
+        log.setSysConversionEvent(eventType.getSystemEventTypeName());
+        log.setAdvConversionEvent(eventType.getAdvertiserEventName());
+        log.setCallbackParams(JSONUtil.toJsonStr(conversionData));
+        log.setCallbackStatus(status ? 1 : 2);
+        log.setRetryCount(0);
+        log.setTraceId(clickId);
+        log.setCreateTime(LocalDateTime.now());
+        conversionLogMapper.insert(log);
+        return log;
+    }
+}
+

+ 23 - 0
fs-ad-new-api/src/main/java/com/fs/app/facade/IConversionService.java

@@ -0,0 +1,23 @@
+package com.fs.app.facade;
+
+
+import com.fs.newAdv.enums.SystemEventTypeEnum;
+
+/**
+ * 转化回传服务接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ * @updated 2025-11-05 使用clickId参数直接回传
+ */
+public interface IConversionService {
+    /**
+     * 链化追踪回传转化数据到广告商
+     *
+     * @param eventType 事件类型
+     * @param traceId   点击ID(广告平台提供)
+     * @return 是否成功
+     */
+    boolean reportTrackingToAdvertiser(SystemEventTypeEnum eventType, String traceId);
+}
+

+ 71 - 0
fs-ad-new-api/src/main/java/com/fs/app/mq/consumer/ConversionTrackingMessageConsumer.java

@@ -0,0 +1,71 @@
+package com.fs.app.mq.consumer;
+
+import com.fs.app.facade.IConversionService;
+import com.fs.common.annotation.DistributeLock;
+import com.fs.common.utils.RedisUtil;
+import com.fs.common.utils.TraceIdUtil;
+import com.fs.newAdv.constant.ConversionTrackingMessage;
+import com.fs.newAdv.constant.MqTopicConstant;
+import com.fs.newAdv.enums.SystemEventTypeEnum;
+import com.fs.newAdv.mapper.ConversionLogMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.annotation.ConsumeMode;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+import org.jboss.logging.MDC;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 转化追踪
+ *
+ * @author zhangqin
+ */
+@Slf4j
+@Component
+@RocketMQMessageListener(
+        topic = MqTopicConstant.CONVERSION_TRACKING_TOPIC,
+        consumerGroup = MqTopicConstant.CONVERSION_TRACKING_TOPIC_CONSUMER_GROUP,
+        // 并发消费模式(多线程并发消费,线程数由RocketMQ自动管理)
+        consumeMode = ConsumeMode.CONCURRENTLY,
+        // 最大重试次数(RocketMQ默认16次)
+        maxReconsumeTimes = 16
+)
+public class ConversionTrackingMessageConsumer implements RocketMQListener<ConversionTrackingMessage> {
+
+    @Autowired
+    private IConversionService conversionService;
+
+    @Autowired
+    private RedisUtil redisUtil;
+
+    @Autowired
+    private ConversionLogMapper conversionLogMapper;
+
+    /**
+     * 消费转化消息
+     *
+     * @param message 转化消息
+     */
+    @Override
+    @DistributeLock(scene = "mq", keyExpression = "#message.traceId", waitTime = 0, errorMsg = "重复消费")
+    public void onMessage(ConversionTrackingMessage message) {
+        TraceIdUtil.put(message.getTrackId());
+
+        String traceId = message.getTraceId();
+        SystemEventTypeEnum eventType = message.getEventType();
+
+        log.info("转化追踪消息开始消费 | traceId={}, eventType={}",
+                traceId, eventType.getDescription());
+
+        //  调用广告平台API回传
+        boolean success = conversionService.reportTrackingToAdvertiser(eventType, traceId);
+
+        if (success) {
+            log.info("转化回传成功 | traceId={}, eventType={}", traceId, eventType);
+        } else {
+            log.info("转化回传失败 | traceId={}, eventType={}", traceId, eventType);
+        }
+    }
+}
+

+ 91 - 0
fs-ad-new-api/src/main/java/com/fs/app/task/ConversionRetryTask.java

@@ -0,0 +1,91 @@
+package com.fs.app.task;
+
+
+import cn.hutool.json.JSONUtil;
+import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.integration.client.IApiClient;
+import com.fs.newAdv.integration.factory.AdvertiserHandlerFactory;
+import com.fs.common.annotation.DistributeLock;
+import com.fs.common.constant.SystemConstant;
+import com.fs.newAdv.domain.ConversionLog;
+import com.fs.newAdv.mapper.ConversionLogMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 转化回传重试定时任务
+ * 每10分钟执行一次,重试失败的转化回传
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Component
+public class ConversionRetryTask {
+
+    @Autowired
+    private ConversionLogMapper conversionLogMapper;
+    @Autowired
+    private AdvertiserHandlerFactory advertiserHandlerFactory;
+
+    /**
+     * 转化回传重试任务
+     * cron: 每10分钟执行
+     */
+    // @Scheduled(cron = "0 */1 * * * ?")
+    @DistributeLock(scene = "task", key = "conversion_retry", waitTime = 0, errorMsg = "任务已执行")
+    public void execute() {
+        // 查询待重试的转化记录
+        List<ConversionLog> pendingList = conversionLogMapper.selectPendingConversions();
+        log.info("查询到 {} 条待重试的转化记录", pendingList.size());
+
+        int successCount = 0;
+        int failCount = 0;
+
+        for (ConversionLog conversionLog : pendingList) {
+            try {
+                // 重试回传
+                boolean success = retryConversion(conversionLog);
+                if (success) {
+                    successCount++;
+                } else {
+                    failCount++;
+                }
+            } catch (Exception e) {
+                log.error("重试转化回传失败,ID:{}", conversionLog.getId(), e);
+                failCount++;
+            }
+        }
+        log.info("========== 转化回传重试任务完成,成功:{},失败:{} ==========", successCount, failCount);
+    }
+
+    /**
+     * 重试转化回传
+     */
+    private boolean retryConversion(ConversionLog conversionLog) {
+        log.info("重试转化回传,ID:{},重试次数:{}", conversionLog.getId(), conversionLog.getRetryCount());
+        IApiClient apiClient = advertiserHandlerFactory.getApiClient(AdvertiserTypeEnum.getByCode(conversionLog.getAdvertiserId()));
+        boolean b = apiClient.reportConversion(JSONUtil.parseObj(conversionLog.getCallbackParams()));
+        if (b) {
+            conversionLog.setCallbackStatus(1);
+            conversionLog.setSuccessTime(LocalDateTime.now());
+        }
+        // 更新重试次数
+        conversionLog.setRetryCount(conversionLog.getRetryCount() + 1);
+
+        // 如果重试次数超过最大值,标记为失败
+        if (conversionLog.getRetryCount() >= SystemConstant.MAX_RETRY_COUNT) {
+            conversionLog.setCallbackStatus(2); // 失败
+            log.warn("转化回传重试次数已达上限,ID:{}", conversionLog.getId());
+        }
+        conversionLog.setUpdateTime(LocalDateTime.now());
+        conversionLogMapper.updateById(conversionLog);
+        return b;
+    }
+}
+

+ 224 - 0
fs-ad-new-api/src/main/java/com/fs/app/task/DataSyncTask.java

@@ -0,0 +1,224 @@
+package com.fs.app.task;
+
+import cn.hutool.core.date.DateUtil;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.fs.newAdv.domain.Lead;
+import com.fs.newAdv.service.ILeadService;
+import com.fs.newAdv.service.ISiteStatisticsService;
+import com.fs.qw.domain.QwAssignRuleUser;
+import com.fs.qw.domain.QwGroupLiveCode;
+import com.fs.qw.service.IQwAssignRuleUserService;
+import com.fs.qw.service.IQwGroupLiveCodeService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * 数据同步定时任务
+ * 每2小时执行一次,拉取各平台账户数据
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Component
+public class DataSyncTask {
+
+
+    @Autowired
+    private ISiteStatisticsService statisticsService;
+    @Autowired
+    private ILeadService leadService;
+    @Autowired
+    private IQwGroupLiveCodeService qwGroupLiveCodeService;
+    @Autowired
+    private IQwAssignRuleUserService qwAssignRuleUserService;
+
+
+    /**
+     * 数据同步任务->同步昨日数据
+     * cron: 每天00:10
+     */
+    @Scheduled(cron = "0 10 0 * * ?")
+    public void syncYesterdayData() {
+        String batchNo = DateUtil.format(LocalDateTime.now().minusDays(1), "yyyy-MM-dd");
+        statisticsService.syncData(batchNo, 1);
+    }
+
+    /**
+     * 数据同步任务->当日数据
+     * cron: 每1小时统计站点数据
+     */
+    @Scheduled(cron = "0 0 0/1 * * ?")
+    public void syncTodayData() {
+        String batchNo = DateUtil.format(LocalDateTime.now(), "yyyy-MM-dd");
+        statisticsService.syncData(batchNo, 1);
+    }
+
+
+    /**
+     * 今日加群数据
+     * cron: 每1小时统计站点数据
+     */
+    // @Scheduled(cron = "0 0 0/1 * * ?")
+    @Scheduled(cron = "0 0/1 * * * ?")
+
+    public void weiChatGroupToDayData() {
+        // 统计今日加群数量
+        Optional.ofNullable(leadService.getToDayGroupNum())
+                .orElse(Collections.emptyList())
+                .stream()
+                .map(Lead::getQwGroupLiveCodeId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.groupingBy(
+                        Function.identity(),
+                        Collectors.counting()
+                ))
+                .forEach((k, v) -> {
+                    if (v > 0) {
+                        qwGroupLiveCodeService.update(
+                                new LambdaUpdateWrapper<QwGroupLiveCode>()
+                                        .eq(QwGroupLiveCode::getId, k)
+                                        .set(QwGroupLiveCode::getToDayNum, v)
+                        );
+                    }
+                });
+    }
+
+    /**
+     * 累计加群数据
+     * cron: 每天00:20
+     */
+    // @Scheduled(cron = "0 20 0 * * ?")
+    @Scheduled(cron = "0 0/1 * * * ?")
+
+    public void weiChatGroupCountData() {
+        // 统计累积加群数量
+        Optional.ofNullable(leadService.getYesterdayGroupNum())
+                .orElse(Collections.emptyList())
+                .stream()
+                .map(Lead::getQwGroupLiveCodeId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.groupingBy(
+                        Function.identity(),
+                        Collectors.counting()
+                ))
+                .forEach((k, v) -> {
+                    if (v > 0) {
+                        QwGroupLiveCode byId = qwGroupLiveCodeService.getById(k);
+                        byId.setCountNum(byId.getCountNum() + v.intValue());
+                        qwGroupLiveCodeService.updateById(byId);
+                    }
+                });
+    }
+
+
+    /**
+     * 微信当天数据
+     * cron: 一小时执行一次
+     */
+    // @Scheduled(cron = "0 0 0/1 * * ?")
+    @Scheduled(cron = "0 0/1 * * * ?")
+    public void weiChatToDayData() {
+        // 统计累积加微数量
+        Optional.ofNullable(leadService.getToDayWeiChatNum())
+                .ifPresent(item -> {
+                            // 统计分配数
+                            item.stream()
+                                    .map(Lead::getQwAssignRuleUserId)
+                                    .filter(Objects::nonNull)
+                                    .collect(Collectors.groupingBy(
+                                            Function.identity(),
+                                            Collectors.counting()
+                                    ))
+                                    .forEach((k, v) -> {
+                                        if (v > 0) {
+                                            qwAssignRuleUserService.update(
+                                                    new LambdaUpdateWrapper<QwAssignRuleUser>()
+                                                            .eq(QwAssignRuleUser::getId, k)
+                                                            .set(QwAssignRuleUser::getAssignNumToDay, v)
+                                            );
+                                        }
+                                    });
+                            // 统计添加数
+                            item.stream()
+                                    .filter(data -> data.getAddContactQw().equals(1))
+                                    .map(Lead::getQwAssignRuleUserId)
+                                    .filter(Objects::nonNull)
+                                    .collect(Collectors.groupingBy(
+                                            Function.identity(),
+                                            Collectors.counting()
+                                    ))
+                                    .forEach((k, v) -> {
+                                        if (v > 0) {
+                                            qwAssignRuleUserService.update(
+                                                    new LambdaUpdateWrapper<QwAssignRuleUser>()
+                                                            .eq(QwAssignRuleUser::getId, k)
+                                                            .set(QwAssignRuleUser::getAddNumToDay, v)
+                                            );
+                                        }
+                                    });
+                        }
+                );
+    }
+
+    /**
+     * 微信累计数据
+     * cron: 每天00:30
+     */
+    // @Scheduled(cron = "0 30 0 * * ?")
+    @Scheduled(cron = "0 0/1 * * * ?")
+    public void weiChatCountData() {
+        // 统计累积加微数量
+        Optional.ofNullable(leadService.getYesterdayWeiChatNum())
+                .ifPresent(item -> {
+                            // 统计分配数
+                            item.stream()
+                                    .map(Lead::getQwAssignRuleUserId)
+                                    .filter(Objects::nonNull)
+                                    .collect(Collectors.groupingBy(
+                                            Function.identity(),
+                                            Collectors.counting()
+                                    ))
+                                    .forEach((k, v) -> {
+                                        if (v > 0) {
+                                            QwAssignRuleUser byId = qwAssignRuleUserService.getById(k);
+                                            qwAssignRuleUserService.update(
+                                                    new LambdaUpdateWrapper<QwAssignRuleUser>()
+                                                            .eq(QwAssignRuleUser::getId, k)
+                                                            .set(QwAssignRuleUser::getAssignNumCount, v+byId.getAssignNumCount())
+                                            );
+                                        }
+                                    });
+                            // 统计添加数
+                            item.stream()
+                                    .filter(data -> data.getAddContactQw().equals(1))
+                                    .map(Lead::getQwAssignRuleUserId)
+                                    .filter(Objects::nonNull)
+                                    .collect(Collectors.groupingBy(
+                                            Function.identity(),
+                                            Collectors.counting()
+                                    ))
+                                    .forEach((k, v) -> {
+                                        if (v > 0) {
+                                            QwAssignRuleUser byId = qwAssignRuleUserService.getById(k);
+                                            qwAssignRuleUserService.update(
+                                                    new LambdaUpdateWrapper<QwAssignRuleUser>()
+                                                            .eq(QwAssignRuleUser::getId, k)
+                                                            .set(QwAssignRuleUser::getAddNumCount, v+byId.getAddNumCount())
+                                            );
+                                        }
+                                    });
+                        }
+                );
+    }
+}
+

+ 182 - 0
fs-ad-new-api/src/main/java/com/fs/framework/aspectj/DataScopeAspect.java

@@ -0,0 +1,182 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.DataScope;
+import com.fs.common.core.domain.BaseEntity;
+import com.fs.common.core.domain.entity.SysRole;
+import com.fs.common.core.domain.entity.SysUser;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.common.utils.StringUtils;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Before;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+
+/**
+ * 数据过滤处理
+ *
+
+ */
+@Aspect
+@Component
+public class DataScopeAspect
+{
+    /**
+     * 全部数据权限
+     */
+    public static final String DATA_SCOPE_ALL = "1";
+
+    /**
+     * 自定数据权限
+     */
+    public static final String DATA_SCOPE_CUSTOM = "2";
+
+    /**
+     * 部门数据权限
+     */
+    public static final String DATA_SCOPE_DEPT = "3";
+
+    /**
+     * 部门及以下数据权限
+     */
+    public static final String DATA_SCOPE_DEPT_AND_CHILD = "4";
+
+    /**
+     * 仅本人数据权限
+     */
+    public static final String DATA_SCOPE_SELF = "5";
+
+    /**
+     * 数据权限过滤关键字
+     */
+    public static final String DATA_SCOPE = "dataScope";
+
+    // 配置织入点
+    @Pointcut("@annotation(com.fs.common.annotation.DataScope)")
+    public void dataScopePointCut()
+    {
+    }
+
+    @Before("dataScopePointCut()")
+    public void doBefore(JoinPoint point) throws Throwable
+    {
+        clearDataScope(point);
+        handleDataScope(point);
+    }
+
+    protected void handleDataScope(final JoinPoint joinPoint)
+    {
+        // 获得注解
+        DataScope controllerDataScope = getAnnotationLog(joinPoint);
+        if (controllerDataScope == null)
+        {
+            return;
+        }
+        // 获取当前的用户
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        if (StringUtils.isNotNull(loginUser))
+        {
+            SysUser currentUser = loginUser.getUser();
+            // 如果是超级管理员,则不过滤数据
+            if (StringUtils.isNotNull(currentUser) && !currentUser.isAdmin())
+            {
+                dataScopeFilter(joinPoint, currentUser, controllerDataScope.deptAlias(),
+                        controllerDataScope.userAlias());
+            }
+        }
+    }
+
+    /**
+     * 数据范围过滤
+     *
+     * @param joinPoint 切点
+     * @param user 用户
+     * @param userAlias 别名
+     */
+    public static void dataScopeFilter(JoinPoint joinPoint, SysUser user, String deptAlias, String userAlias)
+    {
+        StringBuilder sqlString = new StringBuilder();
+
+        for (SysRole role : user.getRoles())
+        {
+            String dataScope = role.getDataScope();
+            if (DATA_SCOPE_ALL.equals(dataScope))
+            {
+                sqlString = new StringBuilder();
+                break;
+            }
+            else if (DATA_SCOPE_CUSTOM.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(
+                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_role_dept WHERE role_id = {} ) ", deptAlias,
+                        role.getRoleId()));
+            }
+            else if (DATA_SCOPE_DEPT.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(" OR {}.dept_id = {} ", deptAlias, user.getDeptId()));
+            }
+            else if (DATA_SCOPE_DEPT_AND_CHILD.equals(dataScope))
+            {
+                sqlString.append(StringUtils.format(
+                        " OR {}.dept_id IN ( SELECT dept_id FROM sys_dept WHERE dept_id = {} or find_in_set( {} , ancestors ) )",
+                        deptAlias, user.getDeptId(), user.getDeptId()));
+            }
+            else if (DATA_SCOPE_SELF.equals(dataScope))
+            {
+                if (StringUtils.isNotBlank(userAlias))
+                {
+                    sqlString.append(StringUtils.format(" OR {}.user_id = {} ", userAlias, user.getUserId()));
+                }
+                else
+                {
+                    // 数据权限为仅本人且没有userAlias别名不查询任何数据
+                    sqlString.append(" OR 1=0 ");
+                }
+            }
+        }
+
+        if (StringUtils.isNotBlank(sqlString.toString()))
+        {
+            Object params = joinPoint.getArgs()[0];
+            if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+            {
+                BaseEntity baseEntity = (BaseEntity) params;
+                baseEntity.getParams().put(DATA_SCOPE, " AND (" + sqlString.substring(4) + ")");
+            }
+        }
+    }
+
+    /**
+     * 是否存在注解,如果存在就获取
+     */
+    private DataScope getAnnotationLog(JoinPoint joinPoint)
+    {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+
+        if (method != null)
+        {
+            return method.getAnnotation(DataScope.class);
+        }
+        return null;
+    }
+
+    /**
+     * 拼接权限sql前先清空params.dataScope参数防止注入
+     */
+    private void clearDataScope(final JoinPoint joinPoint)
+    {
+        Object params = joinPoint.getArgs()[0];
+        if (StringUtils.isNotNull(params) && params instanceof BaseEntity)
+        {
+            BaseEntity baseEntity = (BaseEntity) params;
+            baseEntity.getParams().put(DATA_SCOPE, "");
+        }
+    }
+}

+ 73 - 0
fs-ad-new-api/src/main/java/com/fs/framework/aspectj/DataSourceAspect.java

@@ -0,0 +1,73 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.utils.StringUtils;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.util.Objects;
+
+/**
+ * 多数据源处理
+ * 
+
+ */
+@Aspect
+@Order(1)
+@Component
+public class DataSourceAspect
+{
+    protected Logger logger = LoggerFactory.getLogger(getClass());
+
+    @Pointcut("@annotation(com.fs.common.annotation.DataSource)"
+            + "|| @within(com.fs.common.annotation.DataSource)")
+    public void dsPointCut()
+    {
+
+    }
+
+    @Around("dsPointCut()")
+    public Object around(ProceedingJoinPoint point) throws Throwable
+    {
+        DataSource dataSource = getDataSource(point);
+
+        if (StringUtils.isNotNull(dataSource))
+        {
+            DynamicDataSourceContextHolder.setDataSourceType(dataSource.value().name());
+        }
+
+        try
+        {
+            return point.proceed();
+        }
+        finally
+        {
+            // 销毁数据源 在执行方法之后
+            DynamicDataSourceContextHolder.clearDataSourceType();
+        }
+    }
+
+    /**
+     * 获取需要切换的数据源
+     */
+    public DataSource getDataSource(ProceedingJoinPoint point)
+    {
+        MethodSignature signature = (MethodSignature) point.getSignature();
+        DataSource dataSource = AnnotationUtils.findAnnotation(signature.getMethod(), DataSource.class);
+        if (Objects.nonNull(dataSource))
+        {
+            return dataSource;
+        }
+
+        return AnnotationUtils.findAnnotation(signature.getDeclaringType(), DataSource.class);
+    }
+}

+ 117 - 0
fs-ad-new-api/src/main/java/com/fs/framework/aspectj/DistributeLockAspect.java

@@ -0,0 +1,117 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.annotation.DistributeLock;
+import com.fs.common.constant.DistributeLockConstant;
+import com.fs.common.exception.DistributeLockException;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.redisson.api.RLock;
+import org.redisson.api.RedissonClient;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.core.StandardReflectionParameterNameDiscoverer;
+import org.springframework.core.annotation.Order;
+import org.springframework.expression.EvaluationContext;
+import org.springframework.expression.Expression;
+import org.springframework.expression.spel.standard.SpelExpressionParser;
+import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.stereotype.Component;
+
+import java.lang.reflect.Method;
+import java.util.concurrent.TimeUnit;
+
+@Aspect
+@Component
+@Order(Integer.MIN_VALUE + 1)
+public class DistributeLockAspect {
+
+    private RedissonClient redissonClient;
+
+    public DistributeLockAspect(RedissonClient redissonClient) {
+        this.redissonClient = redissonClient;
+    }
+
+    private static final Logger LOG = LoggerFactory.getLogger(DistributeLockAspect.class);
+
+    @Around("@annotation(com.fs.common.annotation.DistributeLock)")
+    public Object process(ProceedingJoinPoint pjp) throws Throwable {
+        Object response = null;
+        Method method = ((MethodSignature) pjp.getSignature()).getMethod();
+        DistributeLock distributeLock = method.getAnnotation(DistributeLock.class);
+
+        String key = distributeLock.key();
+        if (DistributeLockConstant.NONE_KEY.equals(key)) {
+            if (DistributeLockConstant.NONE_KEY.equals(distributeLock.keyExpression())) {
+                throw new DistributeLockException("no lock key found...");
+            }
+            SpelExpressionParser parser = new SpelExpressionParser();
+            Expression expression = parser.parseExpression(distributeLock.keyExpression());
+
+            EvaluationContext context = new StandardEvaluationContext();
+            // 获取参数值
+            Object[] args = pjp.getArgs();
+
+            // 获取运行时参数的名称
+            StandardReflectionParameterNameDiscoverer discoverer
+                    = new StandardReflectionParameterNameDiscoverer();
+            String[] parameterNames = discoverer.getParameterNames(method);
+
+            // 将参数绑定到context中
+            if (parameterNames != null) {
+                for (int i = 0; i < parameterNames.length; i++) {
+                    context.setVariable(parameterNames[i], args[i]);
+                }
+            }
+
+            // 解析表达式,获取结果
+            key = String.valueOf(expression.getValue(context));
+        }
+
+        String scene = distributeLock.scene();
+
+        String lockKey = scene + "#" + key;
+
+        int expireTime = distributeLock.expireTime();
+        int waitTime = distributeLock.waitTime();
+        RLock rLock= redissonClient.getLock(lockKey);
+        try {
+            boolean lockResult = false;
+            if (waitTime == DistributeLockConstant.DEFAULT_WAIT_TIME) {
+                if (expireTime == DistributeLockConstant.DEFAULT_EXPIRE_TIME) {
+                    LOG.info(String.format("lock for key : %s", lockKey));
+                    rLock.lock();
+                } else {
+                    LOG.info(String.format("lock for key : %s , expire : %s", lockKey, expireTime));
+                    rLock.lock(expireTime, TimeUnit.MILLISECONDS);
+                }
+                lockResult = true;
+            } else {
+                if (expireTime == DistributeLockConstant.DEFAULT_EXPIRE_TIME) {
+                    LOG.info(String.format("try lock for key : %s , wait : %s", lockKey, waitTime));
+                    lockResult = rLock.tryLock(waitTime, TimeUnit.MILLISECONDS);
+                } else {
+                    LOG.info(String.format("try lock for key : %s , expire : %s , wait : %s", lockKey, expireTime, waitTime));
+                    lockResult = rLock.tryLock(waitTime, expireTime, TimeUnit.MILLISECONDS);
+                }
+            }
+
+            if (!lockResult) {
+                LOG.warn(String.format("lock failed for key : %s , expire : %s", lockKey, expireTime));
+                throw new DistributeLockException(distributeLock.errorMsg());
+            }
+
+
+            LOG.info(String.format("lock success for key : %s , expire : %s", lockKey, expireTime));
+            response = pjp.proceed();
+        }  finally {
+            if (rLock.isHeldByCurrentThread()) {
+                rLock.unlock();
+                LOG.info(String.format("unlock for key : %s , expire : %s", lockKey, expireTime));
+            }
+        }
+        return response;
+    }
+}

+ 244 - 0
fs-ad-new-api/src/main/java/com/fs/framework/aspectj/LogAspect.java

@@ -0,0 +1,244 @@
+package com.fs.framework.aspectj;
+
+import com.alibaba.fastjson.JSON;
+import com.fs.common.annotation.Log;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.enums.BusinessStatus;
+import com.fs.common.enums.HttpMethod;
+import com.fs.common.utils.SecurityUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.framework.manager.AsyncManager;
+import com.fs.framework.manager.factory.AsyncFactory;
+import com.fs.system.domain.SysOperLog;
+import org.aspectj.lang.JoinPoint;
+import org.aspectj.lang.Signature;
+import org.aspectj.lang.annotation.AfterReturning;
+import org.aspectj.lang.annotation.AfterThrowing;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.aspectj.lang.reflect.MethodSignature;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+import org.springframework.validation.BindingResult;
+import org.springframework.web.multipart.MultipartFile;
+import org.springframework.web.servlet.HandlerMapping;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import java.lang.reflect.Method;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.Map;
+
+/**
+ * 操作日志记录处理
+ *
+
+ */
+@Aspect
+@Component
+public class LogAspect
+{
+    private static final Logger log = LoggerFactory.getLogger(LogAspect.class);
+
+    // 配置织入点
+    @Pointcut("@annotation(com.fs.common.annotation.Log)")
+    public void logPointCut()
+    {
+    }
+
+    /**
+     * 处理完请求后执行
+     *
+     * @param joinPoint 切点
+     */
+    @AfterReturning(pointcut = "logPointCut()", returning = "jsonResult")
+    public void doAfterReturning(JoinPoint joinPoint, Object jsonResult)
+    {
+        handleLog(joinPoint, null, jsonResult);
+    }
+
+    /**
+     * 拦截异常操作
+     *
+     * @param joinPoint 切点
+     * @param e 异常
+     */
+    @AfterThrowing(value = "logPointCut()", throwing = "e")
+    public void doAfterThrowing(JoinPoint joinPoint, Exception e)
+    {
+        handleLog(joinPoint, e, null);
+    }
+
+    protected void handleLog(final JoinPoint joinPoint, final Exception e, Object jsonResult)
+    {
+        try
+        {
+            // 获得注解
+            Log controllerLog = getAnnotationLog(joinPoint);
+            if (controllerLog == null)
+            {
+                return;
+            }
+
+            // 获取当前的用户
+            LoginUser loginUser = SecurityUtils.getLoginUser();
+
+            // *========数据库日志=========*//
+            SysOperLog operLog = new SysOperLog();
+            operLog.setStatus(BusinessStatus.SUCCESS.ordinal());
+            // 请求的地址
+            String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
+            operLog.setOperIp(ip);
+            // 返回参数
+            operLog.setJsonResult(JSON.toJSONString(jsonResult));
+
+            operLog.setOperUrl(ServletUtils.getRequest().getRequestURI());
+            if (loginUser != null)
+            {
+                operLog.setOperName(loginUser.getUsername());
+            }
+
+            if (e != null)
+            {
+                operLog.setStatus(BusinessStatus.FAIL.ordinal());
+                operLog.setErrorMsg(StringUtils.substring(e.getMessage(), 0, 2000));
+            }
+            // 设置方法名称
+            String className = joinPoint.getTarget().getClass().getName();
+            String methodName = joinPoint.getSignature().getName();
+            operLog.setMethod(className + "." + methodName + "()");
+            // 设置请求方式
+            operLog.setRequestMethod(ServletUtils.getRequest().getMethod());
+            // 处理设置注解上的参数
+            getControllerMethodDescription(joinPoint, controllerLog, operLog);
+            // 保存数据库
+            AsyncManager.me().execute(AsyncFactory.recordOper(operLog));
+        }
+        catch (Exception exp)
+        {
+            // 记录本地异常日志
+            log.error("==前置通知异常==");
+            log.error("异常信息:{}", exp.getMessage());
+            exp.printStackTrace();
+        }
+    }
+
+    /**
+     * 获取注解中对方法的描述信息 用于Controller层注解
+     *
+     * @param log 日志
+     * @param operLog 操作日志
+     * @throws Exception
+     */
+    public void getControllerMethodDescription(JoinPoint joinPoint, Log log, SysOperLog operLog) throws Exception
+    {
+        // 设置action动作
+        operLog.setBusinessType(log.businessType().ordinal());
+        // 设置标题
+        operLog.setTitle(log.title());
+        // 设置操作人类别
+        operLog.setOperatorType(log.operatorType().ordinal());
+        // 是否需要保存request,参数和值
+        if (log.isSaveRequestData())
+        {
+            // 获取参数的信息,传入到数据库中。
+            setRequestValue(joinPoint, operLog);
+        }
+    }
+
+    /**
+     * 获取请求的参数,放到log中
+     *
+     * @param operLog 操作日志
+     * @throws Exception 异常
+     */
+    private void setRequestValue(JoinPoint joinPoint, SysOperLog operLog) throws Exception
+    {
+        String requestMethod = operLog.getRequestMethod();
+        if (HttpMethod.PUT.name().equals(requestMethod) || HttpMethod.POST.name().equals(requestMethod))
+        {
+            String params = argsArrayToString(joinPoint.getArgs());
+            operLog.setOperParam(StringUtils.substring(params, 0, 2000));
+        }
+        else
+        {
+            Map<?, ?> paramsMap = (Map<?, ?>) ServletUtils.getRequest().getAttribute(HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE);
+            operLog.setOperParam(StringUtils.substring(paramsMap.toString(), 0, 2000));
+        }
+    }
+
+    /**
+     * 是否存在注解,如果存在就获取
+     */
+    private Log getAnnotationLog(JoinPoint joinPoint) throws Exception
+    {
+        Signature signature = joinPoint.getSignature();
+        MethodSignature methodSignature = (MethodSignature) signature;
+        Method method = methodSignature.getMethod();
+
+        if (method != null)
+        {
+            return method.getAnnotation(Log.class);
+        }
+        return null;
+    }
+
+    /**
+     * 参数拼装
+     */
+    private String argsArrayToString(Object[] paramsArray)
+    {
+        String params = "";
+        if (paramsArray != null && paramsArray.length > 0)
+        {
+            for (int i = 0; i < paramsArray.length; i++)
+            {
+                if (StringUtils.isNotNull(paramsArray[i]) && !isFilterObject(paramsArray[i]))
+                {
+                    Object jsonObj = JSON.toJSON(paramsArray[i]);
+                    params += jsonObj.toString() + " ";
+                }
+            }
+        }
+        return params.trim();
+    }
+
+    /**
+     * 判断是否需要过滤的对象。
+     *
+     * @param o 对象信息。
+     * @return 如果是需要过滤的对象,则返回true;否则返回false。
+     */
+    @SuppressWarnings("rawtypes")
+    public boolean isFilterObject(final Object o)
+    {
+        Class<?> clazz = o.getClass();
+        if (clazz.isArray())
+        {
+            return clazz.getComponentType().isAssignableFrom(MultipartFile.class);
+        }
+        else if (Collection.class.isAssignableFrom(clazz))
+        {
+            Collection collection = (Collection) o;
+            for (Iterator iter = collection.iterator(); iter.hasNext();)
+            {
+                return iter.next() instanceof MultipartFile;
+            }
+        }
+        else if (Map.class.isAssignableFrom(clazz))
+        {
+            Map map = (Map) o;
+            for (Iterator iter = map.entrySet().iterator(); iter.hasNext();)
+            {
+                Map.Entry entry = (Map.Entry) iter.next();
+                return entry.getValue() instanceof MultipartFile;
+            }
+        }
+        return o instanceof MultipartFile || o instanceof HttpServletRequest || o instanceof HttpServletResponse
+                || o instanceof BindingResult;
+    }
+}

+ 58 - 0
fs-ad-new-api/src/main/java/com/fs/framework/aspectj/ScheduledTaskAspect.java

@@ -0,0 +1,58 @@
+package com.fs.framework.aspectj;
+
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StopWatch;
+
+/**
+ * 定时任务执行时间切面
+ * 记录所有定时任务的执行时间和状态
+ *
+ * @author zhangqin
+ * @date 2025-12-15
+ */
+@Slf4j
+@Aspect
+@Component
+public class ScheduledTaskAspect {
+
+    @Pointcut("@annotation(org.springframework.scheduling.annotation.Scheduled) && execution(* com.fs.app.task..*(..))")
+    public void scheduledTaskPointcut() {
+    }
+
+    @Around("scheduledTaskPointcut()")
+    public Object aroundScheduledTask(ProceedingJoinPoint joinPoint) throws Throwable {
+        String taskName = getTaskName(joinPoint);
+        StopWatch stopWatch = new StopWatch(taskName);
+        stopWatch.start();
+
+        try {
+            Object result = joinPoint.proceed();
+            stopWatch.stop();
+            long executionTime = stopWatch.getTotalTimeMillis();
+            log.info("任务执行耗时: {} {}", taskName, executionTime);
+            return result;
+        } catch (Exception e) {
+            stopWatch.stop();
+            long executionTime = stopWatch.getTotalTimeMillis();
+            log.error("==================== 定时任务执行失败 ====================");
+            log.error("任务名称: [{}]", taskName);
+            log.error("执行耗时: [{}ms]", executionTime);
+            log.error("执行状态: [失败]");
+            log.error("错误信息: [{}]", e.getMessage(), e);
+            throw e;
+        }
+    }
+
+    /**
+     * 获取任务名称
+     */
+    private String getTaskName(ProceedingJoinPoint joinPoint) {
+        return joinPoint.getSignature().getName();
+    }
+}
+

+ 29 - 0
fs-ad-new-api/src/main/java/com/fs/framework/aspectj/ScheduledTraceIdAspect.java

@@ -0,0 +1,29 @@
+package com.fs.framework.aspectj;
+
+import com.fs.common.utils.TraceIdUtil;
+import lombok.extern.slf4j.Slf4j;
+import org.aspectj.lang.ProceedingJoinPoint;
+import org.aspectj.lang.annotation.Around;
+import org.aspectj.lang.annotation.Aspect;
+import org.aspectj.lang.annotation.Pointcut;
+import org.springframework.stereotype.Component;
+
+@Aspect
+@Component
+@Slf4j
+public class ScheduledTraceIdAspect {
+
+    @Pointcut("@annotation(org.springframework.scheduling.annotation.Scheduled)")
+    public void scheduledPointcut() {
+    }
+
+    @Around("scheduledPointcut()")
+    public Object around(ProceedingJoinPoint pjp) throws Throwable {
+        try {
+            TraceIdUtil.init();
+            return pjp.proceed();
+        } finally {
+            TraceIdUtil.clear();
+        }
+    }
+}

+ 31 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/ApplicationConfig.java

@@ -0,0 +1,31 @@
+package com.fs.framework.config;
+
+import org.mybatis.spring.annotation.MapperScan;
+import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.EnableAspectJAutoProxy;
+
+import java.util.TimeZone;
+
+/**
+ * 程序注解配置
+ *
+
+ */
+@Configuration
+// 表示通过aop框架暴露该代理对象,AopContext能够访问
+@EnableAspectJAutoProxy(exposeProxy = true)
+// 指定要扫描的Mapper类的包的路径
+@MapperScan("com.fs.**.mapper")
+public class ApplicationConfig
+{
+    /**
+     * 时区配置
+     */
+    @Bean
+    public Jackson2ObjectMapperBuilderCustomizer jacksonObjectMapperCustomization()
+    {
+        return jacksonObjectMapperBuilder -> jacksonObjectMapperBuilder.timeZone(TimeZone.getDefault());
+    }
+}

+ 61 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/AsyncConfig.java

@@ -0,0 +1,61 @@
+package com.fs.framework.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.AsyncConfigurer;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 异步任务配置类
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Configuration
+public class AsyncConfig implements AsyncConfigurer {
+
+    @Bean(name = "asyncExecutor")
+    @Override
+    public Executor getAsyncExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+
+        // 核心线程数
+        executor.setCorePoolSize(10);
+
+        // 最大线程数
+        executor.setMaxPoolSize(50);
+
+        // 队列容量
+        executor.setQueueCapacity(1000);
+
+        // 线程存活时间(秒)
+        executor.setKeepAliveSeconds(60);
+
+        // 线程名称前缀
+        executor.setThreadNamePrefix("async-task-");
+
+        // 拒绝策略:调用者运行
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+
+        // 等待所有任务完成后再关闭线程池
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+
+        // 等待时间
+        executor.setAwaitTerminationSeconds(60);
+
+
+        // ⭐ 关键:传递 MDC
+        executor.setTaskDecorator(new MdcTaskDecorator());
+
+        executor.initialize();
+
+        log.info("异步线程池初始化完成");
+        return executor;
+    }
+}
+

+ 19 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/CorsConfig.java

@@ -0,0 +1,19 @@
+package com.fs.framework.config;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class CorsConfig implements WebMvcConfigurer {
+
+    @Override
+    public void addCorsMappings(CorsRegistry registry) {
+        registry.addMapping("/**")
+                .allowedOrigins("*") // ★旧版本用这个
+                .allowedMethods("*")
+                .allowedHeaders("*")
+                .allowCredentials(false) // ★注意:allowedOrigins("*") 不能和 true 一起用
+                .maxAge(3600);
+    }
+}

+ 92 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/DataSourceConfig.java

@@ -0,0 +1,92 @@
+package com.fs.framework.config;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import com.alibaba.druid.spring.boot.autoconfigure.properties.DruidStatProperties;
+import com.alibaba.druid.util.Utils;
+import com.fs.common.enums.DataSourceType;
+import com.fs.framework.datasource.DynamicDataSource;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Primary;
+
+import javax.servlet.*;
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@Configuration
+public class DataSourceConfig {
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.sop.druid.master")
+    public DataSource sopDataSource() {
+        return new DruidDataSource();
+    }
+
+    @Bean
+    @ConfigurationProperties(prefix = "spring.datasource.mysql.druid.master")
+    public DataSource masterDataSource() {
+        return new DruidDataSource();
+    }
+
+
+
+    @Bean
+    @Primary
+    public DynamicDataSource dataSource(@Qualifier("masterDataSource") DataSource masterDataSource, @Qualifier("sopDataSource") DataSource sopDataSource) {
+        Map<Object, Object> targetDataSources = new HashMap<>();
+        targetDataSources.put(DataSourceType.SOP.name(), sopDataSource);
+        return new DynamicDataSource(masterDataSource, targetDataSources);
+    }
+
+    /**
+     * 去除监控页面底部的广告
+     */
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    @ConditionalOnProperty(name = "spring.datasource.mysql.druid.statViewServlet.enabled", havingValue = "true")
+    public FilterRegistrationBean removeDruidFilterRegistrationBean(DruidStatProperties properties)
+    {
+        // 获取web监控页面的参数
+        DruidStatProperties.StatViewServlet config = properties.getStatViewServlet();
+        // 提取common.js的配置路径
+        String pattern = config.getUrlPattern() != null ? config.getUrlPattern() : "/druid/*";
+        String commonJsPattern = pattern.replaceAll("\\*", "js/common.js");
+        final String filePath = "support/http/resources/js/common.js";
+        // 创建filter进行过滤
+        Filter filter = new Filter()
+        {
+            @Override
+            public void init(javax.servlet.FilterConfig filterConfig) throws ServletException
+            {
+            }
+            @Override
+            public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+                    throws IOException, ServletException
+            {
+                chain.doFilter(request, response);
+                // 重置缓冲区,响应头不会被重置
+                response.resetBuffer();
+                // 获取common.js
+                String text = Utils.readFromResource(filePath);
+                // 正则替换banner, 除去底部的广告信息
+                text = text.replaceAll("<a.*?banner\"></a><br/>", "");
+                text = text.replaceAll("powered.*?shrek.wang</a>", "");
+                response.getWriter().write(text);
+            }
+            @Override
+            public void destroy()
+            {
+            }
+        };
+        FilterRegistrationBean registrationBean = new FilterRegistrationBean();
+        registrationBean.setFilter(filter);
+        registrationBean.addUrlPatterns(commonJsPattern);
+        return registrationBean;
+    }
+}

+ 72 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/FastJson2JsonRedisSerializer.java

@@ -0,0 +1,72 @@
+package com.fs.framework.config;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.parser.ParserConfig;
+import com.alibaba.fastjson.serializer.SerializerFeature;
+import com.fasterxml.jackson.databind.JavaType;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.type.TypeFactory;
+import org.springframework.data.redis.serializer.RedisSerializer;
+import org.springframework.data.redis.serializer.SerializationException;
+import org.springframework.util.Assert;
+
+import java.nio.charset.Charset;
+
+/**
+ * Redis使用FastJson序列化
+ * 
+
+ */
+public class FastJson2JsonRedisSerializer<T> implements RedisSerializer<T>
+{
+    @SuppressWarnings("unused")
+    private ObjectMapper objectMapper = new ObjectMapper();
+
+    public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");
+
+    private Class<T> clazz;
+
+    static
+    {
+        ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
+    }
+
+    public FastJson2JsonRedisSerializer(Class<T> clazz)
+    {
+        super();
+        this.clazz = clazz;
+    }
+
+    @Override
+    public byte[] serialize(T t) throws SerializationException
+    {
+        if (t == null)
+        {
+            return new byte[0];
+        }
+        return JSON.toJSONString(t, SerializerFeature.WriteClassName).getBytes(DEFAULT_CHARSET);
+    }
+
+    @Override
+    public T deserialize(byte[] bytes) throws SerializationException
+    {
+        if (bytes == null || bytes.length <= 0)
+        {
+            return null;
+        }
+        String str = new String(bytes, DEFAULT_CHARSET);
+
+        return JSON.parseObject(str, clazz);
+    }
+
+    public void setObjectMapper(ObjectMapper objectMapper)
+    {
+        Assert.notNull(objectMapper, "'objectMapper' must not be null");
+        this.objectMapper = objectMapper;
+    }
+
+    protected JavaType getJavaType(Class<?> clazz)
+    {
+        return TypeFactory.defaultInstance().constructType(clazz);
+    }
+}

+ 59 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/FilterConfig.java

@@ -0,0 +1,59 @@
+package com.fs.framework.config;
+
+import com.fs.common.filter.RepeatableFilter;
+import com.fs.common.filter.XssFilter;
+import com.fs.common.utils.StringUtils;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.web.servlet.FilterRegistrationBean;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+import javax.servlet.DispatcherType;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Filter配置
+ *
+
+ */
+@Configuration
+@ConditionalOnProperty(value = "xss.enabled", havingValue = "true")
+public class FilterConfig
+{
+    @Value("${xss.excludes}")
+    private String excludes;
+
+    @Value("${xss.urlPatterns}")
+    private String urlPatterns;
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    public FilterRegistrationBean xssFilterRegistration()
+    {
+        FilterRegistrationBean registration = new FilterRegistrationBean();
+        registration.setDispatcherTypes(DispatcherType.REQUEST);
+        registration.setFilter(new XssFilter());
+        registration.addUrlPatterns(StringUtils.split(urlPatterns, ","));
+        registration.setName("xssFilter");
+        registration.setOrder(FilterRegistrationBean.HIGHEST_PRECEDENCE);
+        Map<String, String> initParameters = new HashMap<String, String>();
+        initParameters.put("excludes", excludes);
+        registration.setInitParameters(initParameters);
+        return registration;
+    }
+
+    @SuppressWarnings({ "rawtypes", "unchecked" })
+    @Bean
+    public FilterRegistrationBean someFilterRegistration()
+    {
+        FilterRegistrationBean registration = new FilterRegistrationBean();
+        registration.setFilter(new RepeatableFilter());
+        registration.addUrlPatterns("/*");
+        registration.setName("repeatableFilter");
+        registration.setOrder(FilterRegistrationBean.LOWEST_PRECEDENCE);
+        return registration;
+    }
+
+}

+ 30 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/MdcTaskDecorator.java

@@ -0,0 +1,30 @@
+package com.fs.framework.config;
+
+import org.slf4j.MDC;
+import org.springframework.core.task.TaskDecorator;
+
+import java.util.Map;
+
+public class MdcTaskDecorator implements TaskDecorator {
+
+    @Override
+    public Runnable decorate(Runnable runnable) {
+
+        // 1. 获取提交任务时的 MDC 内容
+        Map<String, String> contextMap = MDC.getCopyOfContextMap();
+
+        return () -> {
+            try {
+                // 2. 任务执行前恢复 MDC
+                if (contextMap != null) {
+                    MDC.setContextMap(contextMap);
+                }
+
+                runnable.run();
+            } finally {
+                // 3. 清理,避免线程复用污染
+                MDC.clear();
+            }
+        };
+    }
+}

+ 148 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/MyBatisConfig.java

@@ -0,0 +1,148 @@
+package com.fs.framework.config;
+
+import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
+import org.apache.ibatis.io.VFS;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.mybatis.spring.boot.autoconfigure.SpringBootVFS;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.env.Environment;
+import org.springframework.core.io.DefaultResourceLoader;
+import org.springframework.core.io.Resource;
+import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
+import org.springframework.core.io.support.ResourcePatternResolver;
+import org.springframework.core.type.classreading.CachingMetadataReaderFactory;
+import org.springframework.core.type.classreading.MetadataReader;
+import org.springframework.core.type.classreading.MetadataReaderFactory;
+import org.springframework.util.ClassUtils;
+
+import javax.sql.DataSource;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Mybatis支持*匹配扫描包
+ *
+
+ */
+@Configuration
+public class MyBatisConfig
+{
+    @Autowired
+    private Environment env;
+
+    static final String DEFAULT_RESOURCE_PATTERN = "**/*.class";
+
+    public static String setTypeAliasesPackage(String typeAliasesPackage)
+    {
+        ResourcePatternResolver resolver = (ResourcePatternResolver) new PathMatchingResourcePatternResolver();
+        MetadataReaderFactory metadataReaderFactory = new CachingMetadataReaderFactory(resolver);
+        List<String> allResult = new ArrayList<String>();
+        try
+        {
+            for (String aliasesPackage : typeAliasesPackage.split(","))
+            {
+                List<String> result = new ArrayList<String>();
+                aliasesPackage = ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX
+                        + ClassUtils.convertClassNameToResourcePath(aliasesPackage.trim()) + "/" + DEFAULT_RESOURCE_PATTERN;
+                Resource[] resources = resolver.getResources(aliasesPackage);
+                if (resources != null && resources.length > 0)
+                {
+                    MetadataReader metadataReader = null;
+                    for (Resource resource : resources)
+                    {
+                        if (resource.isReadable())
+                        {
+                            metadataReader = metadataReaderFactory.getMetadataReader(resource);
+                            try
+                            {
+                                result.add(Class.forName(metadataReader.getClassMetadata().getClassName()).getPackage().getName());
+                            }
+                            catch (ClassNotFoundException e)
+                            {
+                                e.printStackTrace();
+                            }
+                        }
+                    }
+                }
+                if (result.size() > 0)
+                {
+                    HashSet<String> hashResult = new HashSet<String>(result);
+                    allResult.addAll(hashResult);
+                }
+            }
+            if (allResult.size() > 0)
+            {
+                typeAliasesPackage = String.join(",", (String[]) allResult.toArray(new String[0]));
+            }
+            else
+            {
+                throw new RuntimeException("mybatis typeAliasesPackage 路径扫描错误,参数typeAliasesPackage:" + typeAliasesPackage + "未找到任何包");
+            }
+        }
+        catch (IOException e)
+        {
+            e.printStackTrace();
+        }
+        return typeAliasesPackage;
+    }
+
+    public Resource[] resolveMapperLocations(String[] mapperLocations)
+    {
+        ResourcePatternResolver resourceResolver = new PathMatchingResourcePatternResolver();
+        List<Resource> resources = new ArrayList<Resource>();
+        if (mapperLocations != null)
+        {
+            for (String mapperLocation : mapperLocations)
+            {
+                try
+                {
+                    Resource[] mappers = resourceResolver.getResources(mapperLocation);
+                    resources.addAll(Arrays.asList(mappers));
+                }
+                catch (IOException e)
+                {
+                    // ignore
+                }
+            }
+        }
+        return resources.toArray(new Resource[resources.size()]);
+    }
+
+//    @Bean
+//    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception
+//    {
+//        String typeAliasesPackage = env.getProperty("mybatis.typeAliasesPackage");
+//        String mapperLocations = env.getProperty("mybatis.mapperLocations");
+//        String configLocation = env.getProperty("mybatis.configLocation");
+//        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
+//        VFS.addImplClass(SpringBootVFS.class);
+//
+//        final SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
+//        sessionFactory.setDataSource(dataSource);
+//        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
+//        sessionFactory.setMapperLocations(resolveMapperLocations(StringUtils.split(mapperLocations, ",")));
+//        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+//        return sessionFactory.getObject();
+//    }
+    @Bean
+    public SqlSessionFactory sqlSessionFactorys(DataSource dataSource) throws Exception
+    {
+        String typeAliasesPackage = env.getProperty("mybatis-plus.typeAliasesPackage");
+        String mapperLocations = env.getProperty("mybatis-plus.mapperLocations");
+        String configLocation = env.getProperty("mybatis-plus.configLocation");
+        typeAliasesPackage = setTypeAliasesPackage(typeAliasesPackage);
+        VFS.addImplClass(SpringBootVFS.class);
+
+        final MybatisSqlSessionFactoryBean sessionFactory = new MybatisSqlSessionFactoryBean();
+        sessionFactory.setDataSource(dataSource);
+        sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
+        sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
+        sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+        return sessionFactory.getObject();
+    }
+}

+ 158 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/RedisConfig.java

@@ -0,0 +1,158 @@
+package com.fs.framework.config;
+
+import com.fasterxml.jackson.annotation.JsonAutoDetect;
+import com.fasterxml.jackson.annotation.JsonTypeInfo;
+import com.fasterxml.jackson.annotation.PropertyAccessor;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
+import org.springframework.cache.annotation.CachingConfigurerSupport;
+import org.springframework.cache.annotation.EnableCaching;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.data.redis.connection.RedisConnectionFactory;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.data.redis.core.script.DefaultRedisScript;
+import org.springframework.data.redis.serializer.GenericToStringSerializer;
+import org.springframework.data.redis.serializer.StringRedisSerializer;
+
+import java.math.BigDecimal;
+
+/**
+ * redis配置
+ *
+
+ */
+@Configuration
+@EnableCaching
+public class RedisConfig extends CachingConfigurerSupport
+{
+    @Bean
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory)
+    {
+        RedisTemplate<Object, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+        serializer.setObjectMapper(mapper);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(serializer);
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(serializer);
+
+        template.afterPropertiesSet();
+        return template;
+    }
+    @Bean
+    public RedisTemplate<String, Boolean> redisTemplateForBoolean(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Boolean> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Boolean.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+    @Bean
+    public RedisTemplate<String, Integer> redisTemplateForInteger(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Integer> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(Integer.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+
+    @Bean
+    @SuppressWarnings(value = { "unchecked", "rawtypes" })
+    public RedisTemplate<String, Object> redisTemplateForObject(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, Object> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        FastJson2JsonRedisSerializer serializer = new FastJson2JsonRedisSerializer(Object.class);
+
+        ObjectMapper mapper = new ObjectMapper();
+        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
+        mapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
+        serializer.setObjectMapper(mapper);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+        template.setValueSerializer(serializer);
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(serializer);
+
+        template.afterPropertiesSet();
+        return template;
+    }
+    @Bean
+    public RedisTemplate<String, BigDecimal> redisTemplateForBigDecimal(RedisConnectionFactory connectionFactory) {
+        RedisTemplate<String, BigDecimal> template = new RedisTemplate<>();
+        template.setConnectionFactory(connectionFactory);
+
+        // 使用StringRedisSerializer来序列化和反序列化redis的key值
+        template.setKeySerializer(new StringRedisSerializer());
+
+        // 使用GenericToStringSerializer保证BigDecimal精度不丢失
+        template.setValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
+
+        // Hash的key也采用StringRedisSerializer的序列化方式
+        template.setHashKeySerializer(new StringRedisSerializer());
+        template.setHashValueSerializer(new GenericToStringSerializer<>(BigDecimal.class));
+
+        template.afterPropertiesSet();
+        return template;
+    }
+    @Bean
+    public DefaultRedisScript<Long> limitScript()
+    {
+        DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
+        redisScript.setScriptText(limitScriptText());
+        redisScript.setResultType(Long.class);
+        return redisScript;
+    }
+
+    /**
+     * 限流脚本
+     */
+    private String limitScriptText()
+    {
+        return "local key = KEYS[1]\n" +
+                "local count = tonumber(ARGV[1])\n" +
+                "local time = tonumber(ARGV[2])\n" +
+                "local current = redis.call('get', key);\n" +
+                "if current and tonumber(current) > count then\n" +
+                "    return current;\n" +
+                "end\n" +
+                "current = redis.call('incr', key)\n" +
+                "if tonumber(current) == 1 then\n" +
+                "    redis.call('expire', key, time)\n" +
+                "end\n" +
+                "return current;";
+    }
+}

+ 50 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -0,0 +1,50 @@
+package com.fs.framework.config;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.security.authentication.AuthenticationManager;
+import org.springframework.security.config.BeanIds;
+import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
+import org.springframework.security.config.annotation.web.builders.HttpSecurity;
+import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
+
+/**
+ * spring security配置
+ * 
+
+ */
+@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
+public class SecurityConfig extends WebSecurityConfigurerAdapter
+{
+
+    /**
+     * anyRequest          |   匹配所有请求路径
+     * access              |   SpringEl表达式结果为true时可以访问
+     * anonymous           |   匿名可以访问
+     * denyAll             |   用户不能访问
+     * fullyAuthenticated  |   用户完全认证可以访问(非remember-me下自动登录)
+     * hasAnyAuthority     |   如果有参数,参数表示权限,则其中任何一个权限可以访问
+     * hasAnyRole          |   如果有参数,参数表示角色,则其中任何一个角色可以访问
+     * hasAuthority        |   如果有参数,参数表示权限,则其权限可以访问
+     * hasIpAddress        |   如果有参数,参数表示IP地址,如果用户IP和参数匹配,则可以访问
+     * hasRole             |   如果有参数,参数表示角色,则其角色可以访问
+     * permitAll           |   用户可以任意访问
+     * rememberMe          |   允许通过remember-me登录的用户访问
+     * authenticated       |   用户登录后可访问
+     */
+    @Override
+    protected void configure(HttpSecurity http) throws Exception
+    {
+        http.authorizeRequests()
+                .antMatchers("/**").permitAll()
+                .anyRequest().authenticated()
+                .and().csrf().disable();
+    }
+
+    @Bean(name = BeanIds.AUTHENTICATION_MANAGER)
+    @Override
+    public AuthenticationManager authenticationManagerBean() throws Exception {
+        return super.authenticationManagerBean();
+    }
+
+
+}

+ 33 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/ServerConfig.java

@@ -0,0 +1,33 @@
+package com.fs.framework.config;
+
+import com.fs.common.utils.ServletUtils;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * 服务相关配置
+ * 
+
+ */
+@Component
+public class ServerConfig
+{
+    /**
+     * 获取完整的请求路径,包括:域名,端口,上下文访问路径
+     * 
+     * @return 服务地址
+     */
+    public String getUrl()
+    {
+        HttpServletRequest request = ServletUtils.getRequest();
+        return getDomain(request);
+    }
+
+    public static String getDomain(HttpServletRequest request)
+    {
+        StringBuffer url = request.getRequestURL();
+        String contextPath = request.getServletContext().getContextPath();
+        return url.delete(url.length() - request.getRequestURI().length(), url.length()).append(contextPath).toString();
+    }
+}

+ 63 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/ThreadPoolConfig.java

@@ -0,0 +1,63 @@
+package com.fs.framework.config;
+
+import com.fs.common.utils.Threads;
+import org.apache.commons.lang3.concurrent.BasicThreadFactory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 线程池配置
+ *
+
+ **/
+@Configuration
+public class ThreadPoolConfig
+{
+    // 核心线程池大小
+    private int corePoolSize = 50;
+
+    // 最大可创建的线程数
+    private int maxPoolSize = 200;
+
+    // 队列最大长度
+    private int queueCapacity = 1000;
+
+    // 线程池维护线程所允许的空闲时间
+    private int keepAliveSeconds = 300;
+
+    @Bean(name = "threadPoolTaskExecutor")
+    public ThreadPoolTaskExecutor threadPoolTaskExecutor()
+    {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setMaxPoolSize(maxPoolSize);
+        executor.setCorePoolSize(corePoolSize);
+        executor.setQueueCapacity(queueCapacity);
+        executor.setKeepAliveSeconds(keepAliveSeconds);
+        // 线程池对拒绝任务(无线程可用)的处理策略
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        return executor;
+    }
+
+    /**
+     * 执行周期性或定时任务
+     */
+    @Bean(name = "scheduledExecutorService")
+    protected ScheduledExecutorService scheduledExecutorService()
+    {
+        return new ScheduledThreadPoolExecutor(corePoolSize,
+                new BasicThreadFactory.Builder().namingPattern("schedule-pool-%d").daemon(true).build())
+        {
+            @Override
+            protected void afterExecute(Runnable r, Throwable t)
+            {
+                super.afterExecute(r, t);
+                Threads.printException(r, t);
+            }
+        };
+    }
+}

+ 36 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/TrackIdFilter.java

@@ -0,0 +1,36 @@
+package com.fs.framework.config;
+
+import cn.hutool.core.lang.UUID;
+import org.jboss.logging.MDC;
+import org.springframework.stereotype.Component;
+
+import javax.servlet.*;
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+
+@Component
+public class TrackIdFilter implements Filter {
+
+    public static final String TRACK_ID = "X-Track-Id";
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
+            throws IOException, ServletException {
+
+        // 1. 生成唯一 trackId
+        String trackId = UUID.randomUUID().toString().replace("-", "");
+
+        // 2. 放入 MDC
+        MDC.put(TRACK_ID, trackId);
+
+        // 3. 响应头返回 traceId
+        HttpServletResponse httpResponse = (HttpServletResponse) response;
+        httpResponse.setHeader(TRACK_ID, trackId);
+
+        try {
+            chain.doFilter(request, response);
+        } finally {
+            MDC.remove(TRACK_ID);
+        }
+    }
+}

+ 77 - 0
fs-ad-new-api/src/main/java/com/fs/framework/config/properties/DruidProperties.java

@@ -0,0 +1,77 @@
+package com.fs.framework.config.properties;
+
+import com.alibaba.druid.pool.DruidDataSource;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.context.annotation.Configuration;
+
+/**
+ * druid 配置属性
+ *
+
+ */
+@Configuration
+public class DruidProperties
+{
+    @Value("${spring.datasource.mysql.druid.initialSize}")
+    private int initialSize;
+
+    @Value("${spring.datasource.mysql.druid.minIdle}")
+    private int minIdle;
+
+    @Value("${spring.datasource.mysql.druid.maxActive}")
+    private int maxActive;
+
+    @Value("${spring.datasource.mysql.druid.maxWait}")
+    private int maxWait;
+
+    @Value("${spring.datasource.mysql.druid.timeBetweenEvictionRunsMillis}")
+    private int timeBetweenEvictionRunsMillis;
+
+    @Value("${spring.datasource.mysql.druid.minEvictableIdleTimeMillis}")
+    private int minEvictableIdleTimeMillis;
+
+    @Value("${spring.datasource.mysql.druid.maxEvictableIdleTimeMillis}")
+    private int maxEvictableIdleTimeMillis;
+
+    @Value("${spring.datasource.mysql.druid.validationQuery}")
+    private String validationQuery;
+
+    @Value("${spring.datasource.mysql.druid.testWhileIdle}")
+    private boolean testWhileIdle;
+
+    @Value("${spring.datasource.mysql.druid.testOnBorrow}")
+    private boolean testOnBorrow;
+
+    @Value("${spring.datasource.mysql.druid.testOnReturn}")
+    private boolean testOnReturn;
+
+    public DruidDataSource dataSource(DruidDataSource datasource)
+    {
+        /** 配置初始化大小、最小、最大 */
+        datasource.setInitialSize(initialSize);
+        datasource.setMaxActive(maxActive);
+        datasource.setMinIdle(minIdle);
+
+        /** 配置获取连接等待超时的时间 */
+        datasource.setMaxWait(maxWait);
+
+        /** 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒 */
+        datasource.setTimeBetweenEvictionRunsMillis(timeBetweenEvictionRunsMillis);
+
+        /** 配置一个连接在池中最小、最大生存的时间,单位是毫秒 */
+        datasource.setMinEvictableIdleTimeMillis(minEvictableIdleTimeMillis);
+        datasource.setMaxEvictableIdleTimeMillis(maxEvictableIdleTimeMillis);
+
+        /**
+         * 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
+         */
+        datasource.setValidationQuery(validationQuery);
+        /** 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。 */
+        datasource.setTestWhileIdle(testWhileIdle);
+        /** 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
+        datasource.setTestOnBorrow(testOnBorrow);
+        /** 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。 */
+        datasource.setTestOnReturn(testOnReturn);
+        return datasource;
+    }
+}

+ 27 - 0
fs-ad-new-api/src/main/java/com/fs/framework/datasource/DynamicDataSource.java

@@ -0,0 +1,27 @@
+package com.fs.framework.datasource;
+
+import org.springframework.jdbc.datasource.lookup.AbstractRoutingDataSource;
+
+import javax.sql.DataSource;
+import java.util.Map;
+
+/**
+ * 动态数据源
+ * 
+
+ */
+public class DynamicDataSource extends AbstractRoutingDataSource
+{
+    public DynamicDataSource(DataSource defaultTargetDataSource, Map<Object, Object> targetDataSources)
+    {
+        super.setDefaultTargetDataSource(defaultTargetDataSource);
+        super.setTargetDataSources(targetDataSources);
+        super.afterPropertiesSet();
+    }
+
+    @Override
+    protected Object determineCurrentLookupKey()
+    {
+        return DynamicDataSourceContextHolder.getDataSourceType();
+    }
+}

+ 45 - 0
fs-ad-new-api/src/main/java/com/fs/framework/datasource/DynamicDataSourceContextHolder.java

@@ -0,0 +1,45 @@
+package com.fs.framework.datasource;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * 数据源切换处理
+ * 
+
+ */
+public class DynamicDataSourceContextHolder
+{
+    public static final Logger log = LoggerFactory.getLogger(DynamicDataSourceContextHolder.class);
+
+    /**
+     * 使用ThreadLocal维护变量,ThreadLocal为每个使用该变量的线程提供独立的变量副本,
+     *  所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
+     */
+    private static final ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>();
+
+    /**
+     * 设置数据源的变量
+     */
+    public static void setDataSourceType(String dsType)
+    {
+//        log.info("切换到{}数据源", dsType);
+        CONTEXT_HOLDER.set(dsType);
+    }
+
+    /**
+     * 获得数据源的变量
+     */
+    public static String getDataSourceType()
+    {
+        return CONTEXT_HOLDER.get();
+    }
+
+    /**
+     * 清空数据源变量
+     */
+    public static void clearDataSourceType()
+    {
+        CONTEXT_HOLDER.remove();
+    }
+}

+ 56 - 0
fs-ad-new-api/src/main/java/com/fs/framework/manager/AsyncManager.java

@@ -0,0 +1,56 @@
+package com.fs.framework.manager;
+
+import com.fs.common.utils.Threads;
+import com.fs.common.utils.spring.SpringUtils;
+
+import java.util.TimerTask;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 异步任务管理器
+ * 
+
+ */
+public class AsyncManager
+{
+    /**
+     * 操作延迟10毫秒
+     */
+    private final int OPERATE_DELAY_TIME = 10;
+
+    /**
+     * 异步操作任务调度线程池
+     */
+    private ScheduledExecutorService executor = SpringUtils.getBean("scheduledExecutorService");
+
+    /**
+     * 单例模式
+     */
+    private AsyncManager(){}
+
+    private static AsyncManager me = new AsyncManager();
+
+    public static AsyncManager me()
+    {
+        return me;
+    }
+
+    /**
+     * 执行任务
+     * 
+     * @param task 任务
+     */
+    public void execute(TimerTask task)
+    {
+        executor.schedule(task, OPERATE_DELAY_TIME, TimeUnit.MILLISECONDS);
+    }
+
+    /**
+     * 停止任务线程池
+     */
+    public void shutdown()
+    {
+        Threads.shutdownAndAwaitTermination(executor);
+    }
+}

+ 40 - 0
fs-ad-new-api/src/main/java/com/fs/framework/manager/ShutdownManager.java

@@ -0,0 +1,40 @@
+package com.fs.framework.manager;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PreDestroy;
+
+/**
+ * 确保应用退出时能关闭后台线程
+ *
+
+ */
+@Component
+public class ShutdownManager
+{
+    private static final Logger logger = LoggerFactory.getLogger("sys-user");
+
+    @PreDestroy
+    public void destroy()
+    {
+        shutdownAsyncManager();
+    }
+
+    /**
+     * 停止异步执行任务
+     */
+    private void shutdownAsyncManager()
+    {
+        try
+        {
+            logger.info("====关闭后台任务任务线程池====");
+            AsyncManager.me().shutdown();
+        }
+        catch (Exception e)
+        {
+            logger.error(e.getMessage(), e);
+        }
+    }
+}

+ 103 - 0
fs-ad-new-api/src/main/java/com/fs/framework/manager/factory/AsyncFactory.java

@@ -0,0 +1,103 @@
+package com.fs.framework.manager.factory;
+
+import com.fs.common.constant.Constants;
+import com.fs.common.utils.LogUtils;
+import com.fs.common.utils.ServletUtils;
+import com.fs.common.utils.StringUtils;
+import com.fs.common.utils.ip.AddressUtils;
+import com.fs.common.utils.ip.IpUtils;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.system.domain.SysLogininfor;
+import com.fs.system.domain.SysOperLog;
+import com.fs.system.service.ISysLogininforService;
+import com.fs.system.service.ISysOperLogService;
+import eu.bitwalker.useragentutils.UserAgent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.TimerTask;
+
+/**
+ * 异步工厂(产生任务用)
+ * 
+
+ */
+public class AsyncFactory
+{
+    private static final Logger sys_user_logger = LoggerFactory.getLogger("sys-user");
+
+    /**
+     * 记录登录信息
+     * 
+     * @param username 用户名
+     * @param status 状态
+     * @param message 消息
+     * @param args 列表
+     * @return 任务task
+     */
+    public static TimerTask recordLogininfor(final String username, final String status, final String message,
+            final Object... args)
+    {
+        final UserAgent userAgent = UserAgent.parseUserAgentString(ServletUtils.getRequest().getHeader("User-Agent"));
+        final String ip = IpUtils.getIpAddr(ServletUtils.getRequest());
+        return new TimerTask()
+        {
+            @Override
+            public void run()
+            {
+                String address = AddressUtils.getRealAddressByIP(ip);
+                StringBuilder s = new StringBuilder();
+                s.append(LogUtils.getBlock(ip));
+                s.append(address);
+                s.append(LogUtils.getBlock(username));
+                s.append(LogUtils.getBlock(status));
+                s.append(LogUtils.getBlock(message));
+                // 打印信息到日志
+                sys_user_logger.info(s.toString(), args);
+                // 获取客户端操作系统
+                String os = userAgent.getOperatingSystem().getName();
+                // 获取客户端浏览器
+                String browser = userAgent.getBrowser().getName();
+                // 封装对象
+                SysLogininfor logininfor = new SysLogininfor();
+                logininfor.setUserName(username);
+                logininfor.setIpaddr(ip);
+                logininfor.setLoginLocation(address);
+                logininfor.setBrowser(browser);
+                logininfor.setOs(os);
+                logininfor.setMsg(message);
+                // 日志状态
+                if (StringUtils.equalsAny(status, Constants.LOGIN_SUCCESS, Constants.LOGOUT, Constants.REGISTER))
+                {
+                    logininfor.setStatus(Constants.SUCCESS);
+                }
+                else if (Constants.LOGIN_FAIL.equals(status))
+                {
+                    logininfor.setStatus(Constants.FAIL);
+                }
+                // 插入数据
+                SpringUtils.getBean(ISysLogininforService.class).insertLogininfor(logininfor);
+            }
+        };
+    }
+
+    /**
+     * 操作日志记录
+     * 
+     * @param operLog 操作日志信息
+     * @return 任务task
+     */
+    public static TimerTask recordOper(final SysOperLog operLog)
+    {
+        return new TimerTask()
+        {
+            @Override
+            public void run()
+            {
+                // 远程查询操作地点
+                operLog.setOperLocation(AddressUtils.getRealAddressByIP(operLog.getOperIp()));
+                SpringUtils.getBean(ISysOperLogService.class).insertOperlog(operLog);
+            }
+        };
+    }
+}

+ 1 - 0
fs-ad-new-api/src/main/resources/META-INF/spring-devtools.properties

@@ -0,0 +1 @@
+restart.include.json=/com.alibaba.fastjson.*.jar

+ 9 - 0
fs-ad-new-api/src/main/resources/application.yml

@@ -0,0 +1,9 @@
+# 开发环境配置
+server:
+  port: 7775
+# Spring配置
+spring:
+  profiles:
+#    active: dev
+    active: druid-ylrz
+

+ 2 - 0
fs-ad-new-api/src/main/resources/banner.txt

@@ -0,0 +1,2 @@
+Application Version: ${fs.version}
+Spring Boot Version: ${spring-boot.version}

+ 37 - 0
fs-ad-new-api/src/main/resources/i18n/messages.properties

@@ -0,0 +1,37 @@
+#错误消息
+not.null=* 必须填写
+user.jcaptcha.error=验证码错误
+user.jcaptcha.expire=验证码已失效
+user.not.exists=用户不存在/密码错误
+user.password.not.match=用户不存在/密码错误
+user.password.retry.limit.count=密码输入错误{0}次
+user.password.retry.limit.exceed=密码输入错误{0}次,帐户锁定10分钟
+user.password.delete=对不起,您的账号已被删除
+user.blocked=用户已封禁,请联系管理员
+role.blocked=角色已封禁,请联系管理员
+user.logout.success=退出成功
+
+length.not.valid=长度必须在{min}到{max}个字符之间
+
+user.username.not.valid=* 2到20个汉字、字母、数字或下划线组成,且必须以非数字开头
+user.password.not.valid=* 5-50个字符
+ 
+user.email.not.valid=邮箱格式错误
+user.mobile.phone.number.not.valid=手机号格式错误
+user.login.success=登录成功
+user.register.success=注册成功
+user.notfound=请重新登录
+user.forcelogout=管理员强制退出,请重新登录
+user.unknown.error=未知错误,请重新登录
+
+##文件上传消息
+upload.exceed.maxSize=上传的文件大小超出限制的文件大小!<br/>允许的文件最大大小是:{0}MB!
+upload.filename.exceed.length=上传的文件名最长{0}个字符
+
+##权限
+no.permission=您没有数据的权限,请联系管理员添加权限 [{0}]
+no.create.permission=您没有创建数据的权限,请联系管理员添加权限 [{0}]
+no.update.permission=您没有修改数据的权限,请联系管理员添加权限 [{0}]
+no.delete.permission=您没有删除数据的权限,请联系管理员添加权限 [{0}]
+no.export.permission=您没有导出数据的权限,请联系管理员添加权限 [{0}]
+no.view.permission=您没有查看数据的权限,请联系管理员添加权限 [{0}]

+ 93 - 0
fs-ad-new-api/src/main/resources/logback.xml

@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<configuration>
+    <!-- 日志存放路径 -->
+	<property name="log.path" value="/home/fs-ad-new-api/logs" />
+    <!-- 日志输出格式 -->
+	<property name="log.pattern" value="%d{HH:mm:ss.SSS} [%thread] [%X{X-Track-Id}] %-5level %logger{20} - [%method,%line] - %msg%n" />
+
+	<!-- 控制台输出 -->
+	<appender name="console" class="ch.qos.logback.core.ConsoleAppender">
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+	</appender>
+
+	<!-- 系统日志输出 -->
+	<appender name="file_info" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-info.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+		<rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+			<fileNamePattern>${log.path}/sys-info.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 30 -->
+			<maxHistory>30</maxHistory>
+		</rollingPolicy>
+		<encoder>
+			<pattern>${log.pattern}</pattern>
+		</encoder>
+		<filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>INFO</level>
+            <!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+            <!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+	</appender>
+
+	<appender name="file_error" class="ch.qos.logback.core.rolling.RollingFileAppender">
+	    <file>${log.path}/sys-error.log</file>
+        <!-- 循环政策:基于时间创建日志文件 -->
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 日志文件名格式 -->
+            <fileNamePattern>${log.path}/sys-error.%d{yyyy-MM-dd}.log</fileNamePattern>
+			<!-- 日志最大的历史 60天 -->
+			<maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+        <filter class="ch.qos.logback.classic.filter.LevelFilter">
+            <!-- 过滤的级别 -->
+            <level>ERROR</level>
+			<!-- 匹配时的操作:接收(记录) -->
+            <onMatch>ACCEPT</onMatch>
+			<!-- 不匹配时的操作:拒绝(不记录) -->
+            <onMismatch>DENY</onMismatch>
+        </filter>
+    </appender>
+
+	<!-- 用户访问日志输出  -->
+    <appender name="sys-user" class="ch.qos.logback.core.rolling.RollingFileAppender">
+		<file>${log.path}/sys-user.log</file>
+        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
+            <!-- 按天回滚 daily -->
+            <fileNamePattern>${log.path}/sys-user.%d{yyyy-MM-dd}.log</fileNamePattern>
+            <!-- 日志最大的历史 60天 -->
+            <maxHistory>30</maxHistory>
+        </rollingPolicy>
+        <encoder>
+            <pattern>${log.pattern}</pattern>
+        </encoder>
+    </appender>
+
+	<!-- 系统模块日志级别控制  -->
+	<logger name="com.fs" level="debug" />
+	<!-- Spring日志级别控制  -->
+	<logger name="org.springframework" level="warn" />
+
+	<root level="info">
+		<appender-ref ref="console" />
+	</root>
+
+	<!--系统操作日志-->
+    <root level="info">
+        <appender-ref ref="file_info" />
+        <appender-ref ref="file_error" />
+    </root>
+
+	<!--系统用户操作日志-->
+    <logger name="sys-user" level="info">
+        <appender-ref ref="sys-user"/>
+    </logger>
+</configuration>

+ 15 - 0
fs-ad-new-api/src/main/resources/mybatis/mybatis-config.xml

@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE configuration
+PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
+"http://mybatis.org/dtd/mybatis-3-config.dtd">
+<configuration>
+	
+	<settings>
+		<setting name="cacheEnabled"             value="true" />  <!-- 全局映射器启用缓存 -->
+		<setting name="useGeneratedKeys"         value="true" />  <!-- 允许 JDBC 支持自动生成主键 -->
+		<setting name="defaultExecutorType"      value="REUSE" /> <!-- 配置默认的执行器 -->
+		<setting name="logImpl"                  value="SLF4J" /> <!-- 指定 MyBatis 所用日志的具体实现 -->
+		 <setting name="mapUnderscoreToCamelCase" value="true"/>
+	</settings>
+	
+</configuration>

+ 29 - 2
fs-admin/src/main/java/com/fs/course/controller/FsVideoResourceController.java

@@ -16,6 +16,7 @@ import com.fs.config.cloud.CloudHostProper;
 import com.fs.course.business.FsVideoResourceBusinessService;
 import com.fs.course.config.CourseConfig;
 import com.fs.course.domain.FsVideoResource;
+import com.fs.course.service.IFsUserCourseVideoService;
 import com.fs.course.service.IFsUserVideoService;
 import com.fs.course.service.IFsVideoResourceService;
 import com.fs.course.vo.FsVideoResourceVO;
@@ -23,6 +24,7 @@ import com.fs.framework.web.service.TokenService;
 import com.fs.system.service.ISysConfigService;
 import com.github.pagehelper.PageHelper;
 import lombok.AllArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
@@ -34,6 +36,7 @@ import java.util.stream.Collectors;
 /**
  * 资源库管理
  */
+@Slf4j
 @RestController
 @RequestMapping("/course/videoResource")
 @AllArgsConstructor
@@ -50,6 +53,8 @@ public class FsVideoResourceController extends BaseController {
     private final FsVideoResourceBusinessService videoResourceBusinessService;
     @Autowired
     private IFsUserVideoService fsUserVideoService;
+    @Autowired
+    private IFsUserCourseVideoService fsUserCourseVideoService;
 
     /**
      * 查询视频素材库列表
@@ -102,8 +107,17 @@ public class FsVideoResourceController extends BaseController {
         if (ObjectUtil.isNotEmpty(config.getIsBound()) && config.getIsBound()) {
             fsVideoResource.setUserId(userId);
         }
+
         fsVideoResource.setCreateTime(LocalDateTime.now());
-        fsVideoResourceService.save(fsVideoResource);
+        boolean save = fsVideoResourceService.save(fsVideoResource);
+        if (save&&StringUtils.isNotEmpty(fsVideoResource.getHsyVid())){
+            try {
+                fsUserCourseVideoService.updateMediaPublishStatus(fsVideoResource.getHsyVid());
+                log.info("更新视频发布状态成功,hsyVid: {}", fsVideoResource.getHsyVid());
+            } catch (Exception e) {
+                log.error("更新视频发布状态失败,hsyVid: {}, 错误: {}", fsVideoResource.getHsyVid(), e.getMessage());
+            }
+        }
         return AjaxResult.success();
     }
 
@@ -193,7 +207,20 @@ public class FsVideoResourceController extends BaseController {
                 v.setUserId(userId);
             }
         });
-        fsVideoResourceService.saveBatch(list);
+        boolean saveStatus = fsVideoResourceService.saveBatch(list);
+        if (saveStatus) {
+            list.forEach(fsVideoResource -> {
+                // 检查hsyVid是否存在且不为空
+                if (ObjectUtil.isNotEmpty(fsVideoResource.getHsyVid())) {
+                    try {
+                        fsUserCourseVideoService.updateMediaPublishStatus(fsVideoResource.getHsyVid());
+                        log.info("更新视频发布状态成功,hsyVid: {}", fsVideoResource.getHsyVid());
+                    } catch (Exception e) {
+                        log.error("更新视频发布状态失败,hsyVid: {}, 错误: {}", fsVideoResource.getHsyVid(), e.getMessage());
+                    }
+                }
+            });
+        }
         return AjaxResult.success();
     }
 }

+ 33 - 2
fs-admin/src/main/java/com/fs/his/controller/FsUserAddressController.java

@@ -4,9 +4,12 @@ import java.util.List;
 
 import com.alibaba.fastjson.JSON;
 import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.entity.SysRole;
+import com.fs.common.core.domain.entity.SysUser;
 import com.fs.common.utils.ParseUtils;
 import com.fs.common.utils.SecurityUtils;
 import com.fs.his.dto.AddressInfoDTO;
+import com.fs.system.service.ISysRoleService;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -50,13 +53,41 @@ public class FsUserAddressController extends BaseController
     {
         startPage();
         List<FsUserAddress> list = fsUserAddressService.selectFsUserAddressList(fsUserAddress);
+        SysRole sysRole = null;
+        try {
+            sysRole = isCheckPermission();
+        } catch (Exception e) {
+        }
         for (FsUserAddress userAddress : list) {
-            userAddress.setPhone(decryptAutoPhoneMk(userAddress.getPhone()));
-            userAddress.setDetail(ParseUtils.parseAddress(userAddress.getDetail()));
+            if (sysRole == null || sysRole.getIsCheckPhone() != 1 ) {
+                userAddress.setPhone(decryptAutoPhoneMk(userAddress.getPhone()));
+            }
+            if (sysRole == null || sysRole.getIsCheckAddress() != 1 ) {
+                userAddress.setDetail(ParseUtils.parseAddress(userAddress.getDetail()));
+            }
         }
         return getDataTable(list);
     }
 
+    @Autowired
+    private ISysRoleService sysRoleService;
+    private SysRole isCheckPermission() {
+        SysRole sysRole = new SysRole();
+        SysUser user = getLoginUser().getUser();
+        boolean flag = user.isAdmin();
+        if (flag) {
+            sysRole.setIsCheckPhone(1);
+            sysRole.setIsCheckAddress(1);
+        } else {
+            List<SysRole> roles = user.getRoles();
+            if (roles != null && !roles.isEmpty()) {
+                Long[] roleIds = roles.stream().map(SysRole::getRoleId).toArray(Long[]::new);
+                return sysRoleService.getIsCheckPermission(roleIds);
+            }
+        }
+        return sysRole;
+    }
+
     /**
      * 导出用户地址列表
      */

+ 2 - 2
fs-admin/src/main/resources/application.yml

@@ -4,11 +4,11 @@ server:
 # Spring配置
 spring:
   profiles:
-    active: druid-ylrz
+#    active: druid-ylrz
 #    active: druid-hdt
 #    active: druid-yzt
 #    active: druid-sxjz-test
 #    active: druid-sft
 #    active: druid-fby
-#    active: dev
+    active: dev
 

+ 60 - 0
fs-common/src/main/java/com/fs/common/annotation/DistributeLock.java

@@ -0,0 +1,60 @@
+package com.fs.common.annotation;
+
+import com.fs.common.constant.DistributeLockConstant;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * 分布式锁注解
+ *
+ * @author Hollis
+ */
+@Target(ElementType.METHOD)
+@Retention(RetentionPolicy.RUNTIME)
+public @interface DistributeLock {
+
+    /**
+     * 锁的场景
+     *
+     * @return
+     */
+    public String scene();
+
+    /**
+     * 加锁的key,优先取key(),如果没有,则取keyExpression()
+     *
+     * @return
+     */
+    public String key() default DistributeLockConstant.NONE_KEY;
+
+    /**
+     * SPEL表达式:
+     * <pre>
+     *     #id
+     *     #insertResult.id
+     * </pre>
+     *
+     * @return
+     */
+    public String keyExpression() default DistributeLockConstant.NONE_KEY;
+
+    /**
+     * 超时时间,毫秒
+     * 默认情况下不设置超时时间,会自动续期
+     *
+     * @return
+     */
+    public int expireTime() default DistributeLockConstant.DEFAULT_EXPIRE_TIME;
+
+    public String errorMsg() default DistributeLockConstant.ERROR_MSG;
+
+    /**
+     * 加锁等待时长,毫秒
+     * 默认情况下不设置等待时长,会一直等待直到获取到锁
+     * @return
+     */
+    public int waitTime() default DistributeLockConstant.DEFAULT_WAIT_TIME;
+}

+ 13 - 0
fs-common/src/main/java/com/fs/common/constant/DistributeLockConstant.java

@@ -0,0 +1,13 @@
+package com.fs.common.constant;
+
+public class DistributeLockConstant {
+
+    public static final String NONE_KEY = "NONE";
+
+    public static final String DEFAULT_OWNER = "DEFAULT";
+
+    public static final int DEFAULT_EXPIRE_TIME = -1;
+
+    public static final int DEFAULT_WAIT_TIME = Integer.MAX_VALUE;
+    public static final String ERROR_MSG  = "请勿重复操作";
+}

+ 71 - 0
fs-common/src/main/java/com/fs/common/constant/RedisKeyConstant.java

@@ -0,0 +1,71 @@
+package com.fs.common.constant;
+
+/**
+ * Redis Key常量
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+public class RedisKeyConstant {
+
+    /**
+     * 分布式锁前缀
+     */
+    public static final String LOCK_PREFIX = "crm:adv:lock:";
+
+    /**
+     * 站点统计缓存前缀
+     */
+    public static final String SITE_STATS_PREFIX = "crm:adv:site:stats:";
+
+    /**
+     * 展示数计数器前缀
+     */
+    public static final String IMPRESSION_COUNT_PREFIX = "crm:adv:impression:";
+
+    /**
+     * 点击数计数器前缀
+     */
+    public static final String CLICK_COUNT_PREFIX = "crm:adv:click:";
+
+    /**
+     * 转化数计数器前缀
+     */
+    public static final String CONVERSION_COUNT_PREFIX = "crm:adv:conversion:";
+
+    /**
+     * 任务执行锁前缀
+     */
+    public static final String TASK_LOCK_PREFIX = "crm:adv:task:lock:";
+
+    /**
+     * 广告商配置缓存前缀
+     */
+    public static final String ADVERTISER_CONFIG_PREFIX = "crm:adv:advertiser:config:";
+
+    /**
+     * 站点配置缓存前缀
+     */
+    public static final String SITE_CONFIG_PREFIX = "crm:adv:site:config:";
+
+    /**
+     * 百度AccessToken缓存前缀
+     */
+    public static final String BAIDU_ACCESS_TOKEN_PREFIX = "crm:adv:baidu:token:";
+
+    /**
+     * 巨量引擎AccessToken缓存前缀
+     */
+    public static final String OCEANENGINE_ACCESS_TOKEN_PREFIX = "crm:adv:oceanengine:token:";
+
+    /**
+     * 缓存过期时间(秒)
+     */
+    public static final long CACHE_EXPIRE_TIME = 3600;
+
+    /**
+     * 锁过期时间(秒)
+     */
+    public static final long LOCK_EXPIRE_TIME = 300;
+}
+

+ 57 - 0
fs-common/src/main/java/com/fs/common/constant/RocketMqConstant.java

@@ -0,0 +1,57 @@
+package com.fs.common.constant;
+
+/**
+ * RocketMQ常量
+ *
+ * @author zhangqin
+ * @date 2025-11-05
+ */
+public class RocketMqConstant {
+
+    /**
+     * Topic: 转化回传
+     */
+    public static final String TOPIC_CONVERSION_CALLBACK = "CONVERSION_CALLBACK_TOPIC";
+
+    /**
+     * Topic: 事件反馈(表单提交等转化事件)
+     */
+    public static final String TOPIC_EVENT_FEEDBACK = "event-feedback";
+
+    /**
+     * Topic: 点击追踪
+     */
+    public static final String TOPIC_CLICK_TRACE = "CLICK_TRACE_TOPIC";
+
+    /**
+     * Topic: 数据同步
+     */
+    public static final String TOPIC_DATA_SYNC = "DATA_SYNC_TOPIC";
+
+    /**
+     * Consumer Group: 转化回传消费组
+     */
+    public static final String CONSUMER_GROUP_CONVERSION = "conversion-consumer-group";
+
+    /**
+     * Consumer Group: 事件反馈消费组
+     */
+    public static final String CONSUMER_GROUP_EVENT_FEEDBACK = "event-feedback-consumer-group";
+
+    /**
+     * Consumer Group: 点击追踪消费组
+     */
+    public static final String CONSUMER_GROUP_CLICK_TRACE = "click-trace-consumer-group";
+
+    /**
+     * Consumer Group: 数据同步消费组
+     */
+    public static final String CONSUMER_GROUP_DATA_SYNC = "data-sync-consumer-group";
+
+    /**
+     * Consumer Group: 死信队列消费组
+     */
+    public static final String CONSUMER_GROUP_DLQ = "dlq-consumer-group";
+}
+
+

+ 51 - 0
fs-common/src/main/java/com/fs/common/constant/SystemConstant.java

@@ -0,0 +1,51 @@
+package com.fs.common.constant;
+
+/**
+ * 系统常量
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+public class SystemConstant {
+
+    /**
+     * 系统操作人
+     */
+    public static final String SYSTEM_OPERATOR = "system";
+
+    /**
+     * 最大重试次数
+     */
+    public static final int MAX_RETRY_COUNT = 3;
+
+    /**
+     * 分页默认页码
+     */
+    public static final int DEFAULT_PAGE_NUM = 1;
+
+    /**
+     * 分页默认每页数量
+     */
+    public static final int DEFAULT_PAGE_SIZE = 10;
+
+    /**
+     * 批量处理数量
+     */
+    public static final int BATCH_SIZE = 1000;
+
+    /**
+     * API调用超时时间(毫秒)
+     */
+    public static final int API_TIMEOUT = 20000;
+
+    /**
+     * 日期格式
+     */
+    public static final String DATE_FORMAT = "yyyy-MM-dd";
+
+    /**
+     * 日期时间格式
+     */
+    public static final String DATETIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
+}
+

+ 24 - 0
fs-common/src/main/java/com/fs/common/exception/DistributeLockException.java

@@ -0,0 +1,24 @@
+package com.fs.common.exception;
+
+
+public class DistributeLockException extends RuntimeException {
+
+    public DistributeLockException() {
+    }
+
+    public DistributeLockException(String message) {
+        super(message);
+    }
+
+    public DistributeLockException(String message, Throwable cause) {
+        super(message, cause);
+    }
+
+    public DistributeLockException(Throwable cause) {
+        super(cause);
+    }
+
+    public DistributeLockException(String message, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
+        super(message, cause, enableSuppression, writableStackTrace);
+    }
+}

+ 51 - 0
fs-common/src/main/java/com/fs/common/exception/ThirdPartyException.java

@@ -0,0 +1,51 @@
+package com.fs.common.exception;
+
+import com.fs.common.result.ResultCode;
+import lombok.Getter;
+
+/**
+ * 第三方接口异常
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Getter
+public class ThirdPartyException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 错误码
+     */
+    private Integer code;
+
+    /**
+     * 错误信息
+     */
+    private String message;
+
+    /**
+     * 广告商类型
+     */
+    private Integer advertiserType;
+
+    public ThirdPartyException(String message) {
+        super(message);
+        this.code = ResultCode.THIRD_PARTY_ERROR.getCode();
+        this.message = message;
+    }
+
+    public ThirdPartyException(Integer advertiserType, String message) {
+        super(message);
+        this.code = ResultCode.THIRD_PARTY_ERROR.getCode();
+        this.message = message;
+        this.advertiserType = advertiserType;
+    }
+
+    public ThirdPartyException(String message, Throwable cause) {
+        super(message, cause);
+        this.code = ResultCode.THIRD_PARTY_ERROR.getCode();
+        this.message = message;
+    }
+}
+

+ 52 - 0
fs-common/src/main/java/com/fs/common/exception/base/BusinessException.java

@@ -0,0 +1,52 @@
+package com.fs.common.exception.base;
+
+
+import com.fs.common.result.ResultCode;
+import lombok.Getter;
+
+/**
+ * 业务异常
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Getter
+public class BusinessException extends RuntimeException {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 错误码
+     */
+    private Integer code;
+
+    /**
+     * 错误信息
+     */
+    private String message;
+
+    public BusinessException(String message) {
+        super(message);
+        this.code = ResultCode.BUSINESS_ERROR.getCode();
+        this.message = message;
+    }
+
+    public BusinessException(Integer code, String message) {
+        super(message);
+        this.code = code;
+        this.message = message;
+    }
+
+    public BusinessException(ResultCode resultCode) {
+        super(resultCode.getMessage());
+        this.code = resultCode.getCode();
+        this.message = resultCode.getMessage();
+    }
+
+    public BusinessException(String message, Throwable cause) {
+        super(message, cause);
+        this.code = ResultCode.BUSINESS_ERROR.getCode();
+        this.message = message;
+    }
+}
+

+ 102 - 0
fs-common/src/main/java/com/fs/common/result/Result.java

@@ -0,0 +1,102 @@
+package com.fs.common.result;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 统一返回结果
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Data
+public class Result<T> implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 状态码
+     */
+    private Integer code;
+
+    /**
+     * 返回消息
+     */
+    private String message;
+
+    /**
+     * 返回数据
+     */
+    private T data;
+
+    /**
+     * 时间戳
+     */
+    private Long timestamp;
+
+    public Result() {
+        this.timestamp = System.currentTimeMillis();
+    }
+
+    public Result(Integer code, String message) {
+        this();
+        this.code = code;
+        this.message = message;
+    }
+
+    public Result(Integer code, String message, T data) {
+        this(code, message);
+        this.data = data;
+    }
+
+    /**
+     * 成功返回(无数据)
+     */
+    public static <T> Result<T> success() {
+        return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage());
+    }
+
+    /**
+     * 成功返回(有数据)
+     */
+    public static <T> Result<T> success(T data) {
+        return new Result<>(ResultCode.SUCCESS.getCode(), ResultCode.SUCCESS.getMessage(), data);
+    }
+
+    /**
+     * 成功返回(自定义消息)
+     */
+    public static <T> Result<T> success(String message, T data) {
+        return new Result<>(ResultCode.SUCCESS.getCode(), message, data);
+    }
+
+    /**
+     * 失败返回
+     */
+    public static <T> Result<T> error() {
+        return new Result<>(ResultCode.ERROR.getCode(), ResultCode.ERROR.getMessage());
+    }
+
+    /**
+     * 失败返回(自定义消息)
+     */
+    public static <T> Result<T> error(String message) {
+        return new Result<>(ResultCode.ERROR.getCode(), message);
+    }
+
+    /**
+     * 失败返回(自定义状态码和消息)
+     */
+    public static <T> Result<T> error(Integer code, String message) {
+        return new Result<>(code, message);
+    }
+
+    /**
+     * 失败返回(使用ResultCode)
+     */
+    public static <T> Result<T> error(ResultCode resultCode) {
+        return new Result<>(resultCode.getCode(), resultCode.getMessage());
+    }
+}
+

+ 72 - 0
fs-common/src/main/java/com/fs/common/result/ResultCode.java

@@ -0,0 +1,72 @@
+package com.fs.common.result;
+
+import lombok.Getter;
+
+/**
+ * 返回状态码枚举
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Getter
+public enum ResultCode {
+
+    /**
+     * 成功
+     */
+    SUCCESS(200, "操作成功"),
+
+    /**
+     * 失败
+     */
+    ERROR(500, "操作失败"),
+
+    /**
+     * 参数错误
+     */
+    PARAM_ERROR(400, "参数错误"),
+
+    /**
+     * 未授权
+     */
+    UNAUTHORIZED(401, "未授权"),
+
+    /**
+     * 禁止访问
+     */
+    FORBIDDEN(403, "禁止访问"),
+
+    /**
+     * 资源不存在
+     */
+    NOT_FOUND(404, "资源不存在"),
+
+    /**
+     * 请求超时
+     */
+    REQUEST_TIMEOUT(408, "请求超时"),
+
+    /**
+     * 业务异常
+     */
+    BUSINESS_ERROR(600, "业务异常"),
+
+    /**
+     * 第三方接口调用失败
+     */
+    THIRD_PARTY_ERROR(700, "第三方接口调用失败"),
+
+    /**
+     * 数据库操作失败
+     */
+    DATABASE_ERROR(800, "数据库操作失败");
+
+    private final Integer code;
+    private final String message;
+
+    ResultCode(Integer code, String message) {
+        this.code = code;
+        this.message = message;
+    }
+}
+

+ 124 - 0
fs-common/src/main/java/com/fs/common/utils/RedisUtil.java

@@ -0,0 +1,124 @@
+package com.fs.common.utils;
+
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.util.concurrent.TimeUnit;
+
+/**
+ * Redis工具类
+ * 基于RedisTemplate封装常用操作
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Component
+public class RedisUtil {
+
+    @Autowired
+    private RedisTemplate<String, Object> redisTemplate;
+
+    /**
+     * 设置缓存
+     */
+    public void set(String key, Object value) {
+        redisTemplate.opsForValue().set(key, value);
+    }
+
+    /**
+     * 设置缓存(带过期时间)
+     */
+    public void set(String key, Object value, long timeout, TimeUnit unit) {
+        redisTemplate.opsForValue().set(key, value, timeout, unit);
+    }
+
+    /**
+     * 获取缓存
+     */
+    public Object get(String key) {
+        return redisTemplate.opsForValue().get(key);
+    }
+
+    /**
+     * 删除缓存
+     */
+    public Boolean delete(String key) {
+        return redisTemplate.delete(key);
+    }
+
+    /**
+     * 判断key是否存在
+     */
+    public Boolean hasKey(String key) {
+        return redisTemplate.hasKey(key);
+    }
+
+    /**
+     * 设置过期时间
+     */
+    public Boolean expire(String key, long timeout, TimeUnit unit) {
+        return redisTemplate.expire(key, timeout, unit);
+    }
+
+    /**
+     * 原子递增
+     */
+    public Long increment(String key) {
+        return redisTemplate.opsForValue().increment(key);
+    }
+
+    /**
+     * 原子递增(指定步长)
+     */
+    public Long increment(String key, long delta) {
+        return redisTemplate.opsForValue().increment(key, delta);
+    }
+
+    /**
+     * 原子递减
+     */
+    public Long decrement(String key) {
+        return redisTemplate.opsForValue().decrement(key);
+    }
+
+    /**
+     * 原子递减(指定步长)
+     */
+    public Long decrement(String key, long delta) {
+        return redisTemplate.opsForValue().decrement(key, delta);
+    }
+
+    /**
+     * Hash设置
+     */
+    public void hSet(String key, String field, Object value) {
+        redisTemplate.opsForHash().put(key, field, value);
+    }
+
+    /**
+     * Hash获取
+     */
+    public Object hGet(String key, String field) {
+        return redisTemplate.opsForHash().get(key, field);
+    }
+
+    /**
+     * Hash删除
+     */
+    public Long hDelete(String key, Object... fields) {
+        return redisTemplate.opsForHash().delete(key, fields);
+    }
+
+    /**
+     * Hash判断是否存在
+     */
+    public Boolean hHasKey(String key, String field) {
+        return redisTemplate.opsForHash().hasKey(key, field);
+    }
+
+    public Boolean setIfAbsent(String key, String number, int i, TimeUnit timeUnit) {
+        return redisTemplate.opsForValue().setIfAbsent(key, number, i, timeUnit);
+    }
+}
+

+ 102 - 0
fs-common/src/main/java/com/fs/common/utils/SignatureUtil.java

@@ -0,0 +1,102 @@
+package com.fs.common.utils;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.crypto.SecureUtil;
+
+import java.util.Comparator;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 签名工具类
+ * 用于第三方回调签名验证
+ * 基于Hutool SecureUtil
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+public class SignatureUtil {
+
+    /**
+     * 生成签名
+     * 1. 参数按key排序
+     * 2. 拼接为key=value&key=value格式
+     * 3. 追加secret
+     * 4. MD5加密
+     *
+     * @param params 参数Map
+     * @param secret 密钥
+     * @return 签名字符串
+     */
+    public static String generateSign(Map<String, String> params, String secret) {
+        if (CollUtil.isEmpty(params)) {
+            return "";
+        }
+
+        // 1. 参数排序
+        Map<String, String> sortedMap = params.entrySet().stream()
+                .sorted(Comparator.comparing(Map.Entry::getKey))
+                .collect(Collectors.toMap(
+                        Map.Entry::getKey,
+                        Map.Entry::getValue,
+                        (v1, v2) -> v1,
+                        java.util.LinkedHashMap::new
+                ));
+
+        // 2. 拼接字符串
+        String content = sortedMap.entrySet().stream()
+                .filter(entry -> StrUtil.isNotBlank(entry.getValue()))
+                .map(entry -> entry.getKey() + "=" + entry.getValue())
+                .collect(Collectors.joining("&"));
+
+        // 3. 追加secret并MD5加密
+        return SecureUtil.md5(content + secret);
+    }
+
+    /**
+     * 验证签名
+     *
+     * @param params 参数Map
+     * @param sign 待验证的签名
+     * @param secret 密钥
+     * @return 是否验证通过
+     */
+    public static boolean verifySign(Map<String, String> params, String sign, String secret) {
+        if (StrUtil.isBlank(sign)) {
+            return false;
+        }
+        String calculatedSign = generateSign(params, secret);
+        return StrUtil.equals(sign, calculatedSign, true);
+    }
+
+    /**
+     * 生成SHA256签名
+     *
+     * @param params 参数Map
+     * @param secret 密钥
+     * @return 签名字符串
+     */
+    public static String generateSha256Sign(Map<String, String> params, String secret) {
+        if (CollUtil.isEmpty(params)) {
+            return "";
+        }
+
+        Map<String, String> sortedMap = params.entrySet().stream()
+                .sorted(Comparator.comparing(Map.Entry::getKey))
+                .collect(Collectors.toMap(
+                        Map.Entry::getKey,
+                        Map.Entry::getValue,
+                        (v1, v2) -> v1,
+                        java.util.LinkedHashMap::new
+                ));
+
+        String content = sortedMap.entrySet().stream()
+                .filter(entry -> StrUtil.isNotBlank(entry.getValue()))
+                .map(entry -> entry.getKey() + "=" + entry.getValue())
+                .collect(Collectors.joining("&"));
+
+        return SecureUtil.sha256(content + secret);
+    }
+}
+

+ 65 - 0
fs-common/src/main/java/com/fs/common/utils/SnowflakeUtil.java

@@ -0,0 +1,65 @@
+package com.fs.common.utils;
+
+import cn.hutool.core.lang.Snowflake;
+import cn.hutool.core.util.IdUtil;
+
+/**
+ * 雪花算法ID生成器
+ * 基于Hutool IdUtil(单例模式)
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+public class SnowflakeUtil {
+
+    /**
+     * 工作机器ID(0-31)
+     */
+    private static final long WORKER_ID = 1;
+
+    /**
+     * 数据中心ID(0-31)
+     */
+    private static final long DATACENTER_ID = 1;
+
+    /**
+     * 雪花算法实例(单例)
+     */
+    private static final Snowflake SNOWFLAKE = IdUtil.getSnowflake(WORKER_ID, DATACENTER_ID);
+
+    /**
+     * 生成Long型ID
+     */
+    public static Long nextId() {
+        return SNOWFLAKE.nextId();
+    }
+
+    /**
+     * 生成String型ID
+     */
+    public static String nextIdStr() {
+        return SNOWFLAKE.nextIdStr();
+    }
+
+    /**
+     * 生成UUID(无横线)
+     */
+    public static String simpleUUID() {
+        return IdUtil.simpleUUID();
+    }
+
+    /**
+     * 生成UUID(带横线)
+     */
+    public static String randomUUID() {
+        return IdUtil.randomUUID();
+    }
+
+    /**
+     * 生成ObjectId(MongoDB风格)
+     */
+    public static String objectId() {
+        return IdUtil.objectId();
+    }
+}
+

+ 26 - 0
fs-common/src/main/java/com/fs/common/utils/TraceIdUtil.java

@@ -0,0 +1,26 @@
+package com.fs.common.utils;
+
+import cn.hutool.core.lang.UUID;
+import org.slf4j.MDC;
+
+public class TraceIdUtil {
+
+    private static final String TRACK_ID_KEY = "X-Track-Id";
+
+    public static void init() {
+        String trackId = UUID.randomUUID().toString().replace("-", "");
+        MDC.put(TRACK_ID_KEY, trackId);
+    }
+
+    public static void clear() {
+        MDC.remove(TRACK_ID_KEY);
+    }
+
+    public static String get() {
+        return MDC.get(TRACK_ID_KEY);
+    }
+
+    public static void put(String key) {
+        MDC.put(TRACK_ID_KEY, key);
+    }
+}

+ 103 - 0
fs-company/src/main/java/com/fs/company/controller/newAdv/AdvChannelController.java

@@ -0,0 +1,103 @@
+package com.fs.company.controller.newAdv;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.common.result.Result;
+import com.fs.newAdv.domain.AdvChannelEntity;
+import com.fs.newAdv.dto.req.ChannelSaveBatchReq;
+import com.fs.newAdv.service.IAdvChannelService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * <p>
+ * 广告渠道表 前端控制器
+ * </p>
+ *
+ * @author zhangqin
+ * @since 2025-11-27
+ */
+@RestController
+@RequestMapping("/adv/channel")
+public class AdvChannelController {
+
+    @Autowired
+    private IAdvChannelService advChannelService;
+
+    /**
+     * 分页查询渠道
+     */
+    @GetMapping("/page")
+    public Result<IPage<AdvChannelEntity>> page(
+            @RequestParam(defaultValue = "1") Long pageNum,
+            @RequestParam(defaultValue = "10") Long pageSize,
+            @RequestParam(required = false) String channelName,
+            @RequestParam(required = false) Long parentId) {
+
+        Page<AdvChannelEntity> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<AdvChannelEntity> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StrUtil.isNotBlank(channelName), AdvChannelEntity::getChannelName, channelName);
+        wrapper.eq(ObjectUtil.isNotEmpty(parentId), AdvChannelEntity::getParentId, parentId);
+        wrapper.ne(ObjectUtil.isEmpty(parentId), AdvChannelEntity::getParentId, 0);
+        wrapper.orderByDesc(AdvChannelEntity::getCreateTime);
+        IPage<AdvChannelEntity> result = advChannelService.page(page, wrapper);
+        return Result.success(result);
+    }
+
+    /**
+     * 创建渠道/修改渠道
+     */
+    @PostMapping("/addOrUpdate")
+    public Result<Void> create(@RequestBody AdvChannelEntity advertiser) {
+        boolean success = false;
+        if (advertiser.getId() != null) {
+            success = advChannelService.updateById(advertiser);
+        } else {
+            success = advChannelService.save(advertiser);
+        }
+        return success ? Result.success() : Result.error("创建失败");
+    }
+
+    /**
+     * 批量新增
+     */
+    @PostMapping("/saveBatch")
+    public Result<Void> saveBatch(@RequestBody ChannelSaveBatchReq saveBatch) {
+        // 参数验证
+        int num = saveBatch.getNum();
+        if (num <= 0 || num > 10000) {
+            return Result.error("批量数量必须在1-10000之间");
+        }
+
+        String channelName = saveBatch.getChannelName();
+        if (StrUtil.isBlank(channelName)) {
+            return Result.error("渠道名称不能为空");
+        }
+
+        List<AdvChannelEntity> list = new ArrayList<>(num);
+        Integer start = saveBatch.getStart();
+        Long parentId = saveBatch.getParentId();
+
+        for (int i = 0; i < num; i++) {
+            AdvChannelEntity entity = new AdvChannelEntity();
+            entity.setChannelName(new StringBuilder()
+                    .append(channelName)
+                    .append("-")
+                    .append(start++)
+                    .toString());
+            entity.setParentId(parentId);
+            list.add(entity);
+        }
+        try {
+            return advChannelService.saveBatch(list) ? Result.success() : Result.error("创建失败");
+        } catch (Exception e) {
+            return Result.error("批量创建失败:" + e.getMessage());
+        }
+    }
+}

+ 46 - 0
fs-company/src/main/java/com/fs/company/controller/newAdv/AdvConfigController.java

@@ -0,0 +1,46 @@
+package com.fs.company.controller.newAdv;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
+import com.fs.common.result.Result;
+import com.fs.newAdv.constant.AdvConfigConstant;
+import com.fs.newAdv.dto.res.AdvConfigRes;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.service.ISysConfigService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+@RestController
+@RequestMapping("/adv/config")
+public class AdvConfigController {
+
+    @Autowired
+    private ISysConfigService sysConfigService;
+
+    /**
+     * 删除站点
+     */
+    @GetMapping("/detail")
+    public Result<AdvConfigRes> detail() {
+        return Result.success(JSONUtil.toBean(sysConfigService.selectConfigByKey(AdvConfigConstant.ADV_CONFIG), AdvConfigRes.class));
+    }
+
+    /**
+     * 新增更新配置
+     */
+    @PostMapping("/addOrUpdate")
+    public Result<Integer> addOrUpdate(@RequestBody AdvConfigRes advConfigRes) {
+        SysConfig sysConfig = sysConfigService.selectConfigByConfigKey(AdvConfigConstant.ADV_CONFIG);
+        int flag = 0;
+        if (ObjectUtil.isEmpty(sysConfig)) {
+            sysConfig = new SysConfig();
+            sysConfig.setConfigKey(AdvConfigConstant.ADV_CONFIG);
+            sysConfig.setConfigValue(JSONUtil.toJsonStr(advConfigRes));
+            flag = sysConfigService.insertConfig(sysConfig);
+        } else {
+            sysConfig.setConfigValue(JSONUtil.toJsonStr(advConfigRes));
+            flag = sysConfigService.updateConfig(sysConfig);
+        }
+        return Result.success(flag);
+    }
+}

+ 58 - 0
fs-company/src/main/java/com/fs/company/controller/newAdv/AdvProjectController.java

@@ -0,0 +1,58 @@
+package com.fs.company.controller.newAdv;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.common.result.Result;
+import com.fs.newAdv.domain.AdvProjectEntity;
+import com.fs.newAdv.service.IAdvProjectService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * <p>
+ * 前端控制器
+ * </p>
+ *
+ * @author zhangqin
+ * @since 2025-11-27
+ */
+@RestController
+@RequestMapping("/adv/project")
+public class AdvProjectController {
+
+    @Autowired
+    private IAdvProjectService advProjectService;
+
+    /**
+     * 分页查询渠道
+     */
+    @GetMapping("/page")
+    public Result<IPage<AdvProjectEntity>> page(
+            @RequestParam(defaultValue = "1") Long pageNum,
+            @RequestParam(defaultValue = "10") Long pageSize,
+            @RequestParam(required = false) String projectName) {
+        Page<AdvProjectEntity> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<AdvProjectEntity> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StrUtil.isNotBlank(projectName), AdvProjectEntity::getProjectName, projectName);
+        wrapper.orderByDesc(AdvProjectEntity::getCreateTime);
+        IPage<AdvProjectEntity> result = advProjectService.page(page, wrapper);
+        return Result.success(result);
+    }
+
+    /**
+     * 创建渠道/修改渠道
+     */
+    @PostMapping("/add")
+    public Result<Void> create(@RequestBody @Validated AdvProjectEntity advertiser) {
+        boolean success = false;
+        if (advertiser.getId() != null) {
+            success = advProjectService.updateById(advertiser);
+        } else {
+            success = advProjectService.save(advertiser);
+        }
+        return success ? Result.success() : Result.error("创建失败");
+    }
+}

+ 73 - 0
fs-company/src/main/java/com/fs/company/controller/newAdv/AdvertiserController.java

@@ -0,0 +1,73 @@
+package com.fs.company.controller.newAdv;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.common.result.Result;
+import com.fs.newAdv.domain.Advertiser;
+import com.fs.newAdv.service.IAdvertiserService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * 广告商管理控制器
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Validated
+@RestController
+@RequestMapping("/adv/advertiser")
+public class AdvertiserController {
+
+    @Autowired
+    private IAdvertiserService advertiserService;
+
+    /**
+     * 分页查询广告商列表
+     */
+    @GetMapping("/page")
+    public Result<IPage<Advertiser>> page(
+            @RequestParam(defaultValue = "1") Long pageNum,
+            @RequestParam(defaultValue = "10") Long pageSize,
+            @RequestParam(required = false) String advertiserName,
+            @RequestParam(required = false) Integer custom,
+            @RequestParam(required = false) Integer enabled) {
+
+        Page<Advertiser> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<Advertiser> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StrUtil.isNotBlank(advertiserName), Advertiser::getAdvertiserName, advertiserName);
+        wrapper.eq(custom != null, Advertiser::getCustom, custom);
+        wrapper.eq(enabled != null, Advertiser::getEnabled, enabled);
+        wrapper.orderByDesc(Advertiser::getCreateTime);
+        IPage<Advertiser> result = advertiserService.page(page, wrapper);
+        return Result.success(result);
+    }
+
+    /**
+     * 创建广告商
+     */
+    @PostMapping
+    public Result<Void> create(@RequestBody @Validated Advertiser advertiser) {
+        boolean success = advertiserService.save(advertiser);
+        return success ? Result.success() : Result.error("创建失败");
+    }
+
+    /**
+     * 启用停用
+     */
+    @PostMapping("/enable/{id}")
+    public Result<Void> enable(@PathVariable Long id) {
+        Advertiser byId = advertiserService.getById(id);
+        byId.setEnabled(byId.getEnabled() == 1 ? 0 : 1);
+        advertiserService.updateById(byId);
+        return Result.success();
+    }
+
+
+}
+

+ 112 - 0
fs-company/src/main/java/com/fs/company/controller/newAdv/CallbackAccountController.java

@@ -0,0 +1,112 @@
+package com.fs.company.controller.newAdv;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.common.result.Result;
+import com.fs.newAdv.domain.AdvEventType;
+import com.fs.newAdv.domain.CallbackAccount;
+import com.fs.newAdv.service.ICallbackAccountService;
+import com.fs.newAdv.vo.ConversionEventVo;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * 回传账号管理控制器
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Validated
+@RestController
+@RequestMapping("/adv/callback-account")
+public class CallbackAccountController {
+
+    @Autowired
+    private ICallbackAccountService callbackAccountService;
+
+    /**
+     * 分页查询回传账号列表
+     */
+    @GetMapping("/page")
+    public Result<IPage<CallbackAccount>> page(
+            @RequestParam(defaultValue = "1") Long pageNum,
+            @RequestParam(defaultValue = "10") Long pageSize,
+            @RequestParam(required = false) String advertiserId,
+            @RequestParam(required = false) String accountName) {
+
+        Page<CallbackAccount> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<CallbackAccount> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StrUtil.isNotBlank(accountName), CallbackAccount::getAccountName, accountName);
+        wrapper.eq(StrUtil.isNotBlank(advertiserId), CallbackAccount::getAdvertiserId, advertiserId);
+        wrapper.orderByDesc(CallbackAccount::getCreateTime);
+        IPage<CallbackAccount> result = callbackAccountService.page(page, wrapper);
+
+        return Result.success(result);
+    }
+
+    /**
+     * 根据ID查询回传账号详情
+     */
+    @GetMapping("/{id}")
+    public Result<CallbackAccount> getById(@PathVariable @NotNull(message = "账号ID不能为空") Long id) {
+        CallbackAccount callbackAccount = callbackAccountService.getById(id);
+        return Result.success(callbackAccount);
+    }
+
+    /**
+     * 创建回传账号
+     */
+    @PostMapping
+    public Result<Void> create(@RequestBody @Validated CallbackAccount callbackAccount) {
+        boolean success = callbackAccountService.save(callbackAccount);
+        return success ? Result.success() : Result.error("创建失败");
+    }
+
+    /**
+     * 更新回传账号
+     */
+    @PutMapping("/{id}")
+    public Result<Void> update(
+            @PathVariable @NotNull(message = "账号ID不能为空") Long id,
+            @RequestBody @Validated CallbackAccount callbackAccount) {
+
+        callbackAccount.setId(id);
+        boolean success = callbackAccountService.updateById(callbackAccount);
+        return success ? Result.success() : Result.error("更新失败");
+    }
+
+    /**
+     * 删除回传账号
+     */
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable @NotNull(message = "账号ID不能为空") Long id) {
+        boolean success = callbackAccountService.removeById(id);
+        return success ? Result.success() : Result.error("删除失败");
+    }
+
+    /**
+     * 新增回传事件
+     */
+    @PostMapping("/saveEventType/{id}")
+    public Result<Void> saveEventType(@RequestBody List<ConversionEventVo> vo, @PathVariable("id") Long id) {
+        boolean success = callbackAccountService.saveEventType(vo, id);
+        return success ? Result.success() : Result.error("新增回传事件失败");
+    }
+
+    /**
+     * 查询回传事件
+     */
+    @PostMapping("/queryEventType/{advertiserId}")
+    public Result<List<AdvEventType>> queryEventType(@PathVariable("advertiserId") Long advertiserId) {
+        return Result.success(callbackAccountService.queryEventType(advertiserId));
+    }
+}
+

+ 48 - 0
fs-company/src/main/java/com/fs/company/controller/newAdv/ConversionLogController.java

@@ -0,0 +1,48 @@
+package com.fs.company.controller.newAdv;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.common.result.Result;
+import com.fs.newAdv.domain.ConversionLog;
+import com.fs.newAdv.service.IConversionLogService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+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;
+
+@Slf4j
+@RestController
+@RequestMapping("/adv/conversion-log")
+@Validated
+public class ConversionLogController {
+
+    @Autowired
+    private IConversionLogService conversionLogService;
+
+    @GetMapping("/page")
+    public Result<IPage<ConversionLog>> page(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam(required = false) String conversionType,
+            @RequestParam(required = false) String advertiserId,
+            @RequestParam(required = false) String callbackStatus,
+            @RequestParam(required = false) String callbackAccountId,
+            @RequestParam(required = false) String traceId) {
+        Page<ConversionLog> pageParam = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<ConversionLog> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(ObjectUtil.isNotEmpty(conversionType), ConversionLog::getSysConversionEvent, conversionType);
+        queryWrapper.eq(ObjectUtil.isNotEmpty(advertiserId), ConversionLog::getAdvertiserId, advertiserId);
+        queryWrapper.eq(ObjectUtil.isNotEmpty(callbackStatus), ConversionLog::getCallbackStatus, callbackStatus);
+        queryWrapper.eq(ObjectUtil.isNotEmpty(traceId), ConversionLog::getTraceId, traceId);
+        queryWrapper.eq(ObjectUtil.isNotEmpty(callbackAccountId), ConversionLog::getCallbackAccountId, callbackAccountId);
+        queryWrapper.orderByDesc(ConversionLog::getCreateTime);
+
+        IPage<ConversionLog> result = conversionLogService.page(pageParam, queryWrapper);
+        return Result.success(result);
+    }
+}

+ 269 - 0
fs-company/src/main/java/com/fs/company/controller/newAdv/DomainController.java

@@ -0,0 +1,269 @@
+package com.fs.company.controller.newAdv;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.common.result.Result;
+import com.fs.newAdv.domain.DomainUrl;
+import com.fs.newAdv.service.IDomainService;
+import lombok.Data;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.constraints.NotBlank;
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * 域名管理Controller
+ *
+ * @author zhangqin
+ * @date 2025-11-06
+ */
+@Slf4j
+@RestController
+@RequestMapping("/adv/domains")
+@Validated
+public class DomainController {
+
+    @Autowired
+    private IDomainService domainService;
+
+    /**
+     * 分页查询域名列表
+     *
+     * @param name    域名名称(模糊查询)
+     * @param domain  域名地址(模糊查询)
+     * @param status  状态
+     * @return 域名列表
+     */
+    @GetMapping("/page")
+    public Result<IPage<DomainUrl>> page(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam(required = false) String name,
+            @RequestParam(required = false) String domain,
+            @RequestParam(required = false) Integer status) {
+        Page<DomainUrl> pageParam = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<DomainUrl> queryWrapper = new LambdaQueryWrapper<>();
+
+        if (StrUtil.isNotBlank(name)) {
+            queryWrapper.like(DomainUrl::getName, name);
+        }
+        if (StrUtil.isNotBlank(domain)) {
+            queryWrapper.like(DomainUrl::getDomain, domain);
+        }
+        if (status != null) {
+            queryWrapper.eq(DomainUrl::getStatus, status);
+        }
+
+        queryWrapper.orderByDesc(DomainUrl::getCreateTime);
+
+        IPage<DomainUrl> result = domainService.page(pageParam, queryWrapper);
+        return Result.success(result);
+    }
+
+
+    /**
+     * 根据ID查询域名详情
+     *
+     * @param id 域名ID
+     * @return 域名详情
+     */
+    @GetMapping("/{id}")
+    public Result<DomainUrl> getById(@PathVariable Long id) {
+        log.info("查询域名详情 | id={}", id);
+
+        DomainUrl domainUrl = domainService.getById(id);
+        if (domainUrl == null) {
+            return Result.error("域名不存在");
+        }
+
+        return Result.success(domainUrl);
+    }
+
+    /**
+     * 新增域名
+     *
+     * @param request 域名信息
+     * @return 操作结果
+     */
+    @PostMapping
+    public Result<DomainUrl> create(@RequestBody @Validated DomainCreateRequest request) {
+        log.info("新增域名 | name={}, domain={}", request.getName(), request.getDomain());
+
+        // 检查域名是否已存在
+        LambdaQueryWrapper<DomainUrl> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(DomainUrl::getDomain, request.getDomain());
+        long count = domainService.count(queryWrapper);
+        if (count > 0) {
+            return Result.error("域名已存在");
+        }
+
+        DomainUrl domainUrl = new DomainUrl();
+        domainUrl.setName(request.getName());
+        domainUrl.setDomain(request.getDomain());
+        domainUrl.setStatus(request.getStatus() != null ? request.getStatus() : 1);
+        domainUrl.setRemark(request.getRemark());
+
+        boolean success = domainService.save(domainUrl);
+        if (!success) {
+            return Result.error("新增域名失败");
+        }
+
+        log.info("新增域名成功 | id={}", domainUrl.getId());
+        return Result.success(domainUrl);
+    }
+
+    /**
+     * 更新域名
+     *
+     * @param id      域名ID
+     * @param request 域名信息
+     * @return 操作结果
+     */
+    @PutMapping("/{id}")
+    public Result<String> update(@PathVariable Long id,
+                                  @RequestBody @Validated DomainUpdateRequest request) {
+        log.info("更新域名 | id={}, name={}, domain={}", id, request.getName(), request.getDomain());
+
+        DomainUrl domainUrl = domainService.getById(id);
+        if (domainUrl == null) {
+            return Result.error("域名不存在");
+        }
+
+        // 如果修改了域名地址,检查新域名是否已被其他记录使用
+        if (StrUtil.isNotBlank(request.getDomain()) && !request.getDomain().equals(domainUrl.getDomain())) {
+            LambdaQueryWrapper<DomainUrl> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.eq(DomainUrl::getDomain, request.getDomain())
+                       .ne(DomainUrl::getId, id);
+            long count = domainService.count(queryWrapper);
+            if (count > 0) {
+                return Result.error("域名已存在");
+            }
+        }
+
+        domainUrl.setName(request.getName());
+        domainUrl.setDomain(request.getDomain());
+        domainUrl.setStatus(request.getStatus());
+        domainUrl.setRemark(request.getRemark());
+
+        boolean success = domainService.updateById(domainUrl);
+        if (!success) {
+            return Result.error("更新域名失败");
+        }
+
+        log.info("更新域名成功 | id={}", id);
+        return Result.success("更新成功");
+    }
+
+    /**
+     * 删除域名
+     *
+     * @param id 域名ID
+     * @return 操作结果
+     */
+    @DeleteMapping("/{id}")
+    public Result<String> delete(@PathVariable Long id) {
+        log.info("删除域名 | id={}", id);
+
+        DomainUrl domainUrl = domainService.getById(id);
+        if (domainUrl == null) {
+            return Result.error("域名不存在");
+        }
+
+        boolean success = domainService.removeById(id);
+        if (!success) {
+            return Result.error("删除域名失败");
+        }
+
+        log.info("删除域名成功 | id={}", id);
+        return Result.success("删除成功");
+    }
+
+    /**
+     * 批量删除域名
+     *
+     * @param ids 域名ID列表
+     * @return 操作结果
+     */
+    @DeleteMapping("/batch")
+    public Result<String> batchDelete(@RequestBody List<Long> ids) {
+        log.info("批量删除域名 | ids={}", ids);
+
+        if (ids == null || ids.isEmpty()) {
+            return Result.error("请选择要删除的域名");
+        }
+
+        boolean success = domainService.removeByIds(ids);
+        if (!success) {
+            return Result.error("批量删除域名失败");
+        }
+
+        log.info("批量删除域名成功 | count={}", ids.size());
+        return Result.success("删除成功");
+    }
+
+    /**
+     * 启用/禁用域名
+     *
+     * @param id     域名ID
+     * @param status 状态(0禁用 1启用)
+     * @return 操作结果
+     */
+    @PutMapping("/{id}/status")
+    public Result<String> updateStatus(@PathVariable Long id, @RequestParam Integer status) {
+        log.info("修改域名状态 | id={}, status={}", id, status);
+
+        DomainUrl domainUrl = domainService.getById(id);
+        if (domainUrl == null) {
+            return Result.error("域名不存在");
+        }
+
+        domainUrl.setStatus(status);
+        boolean success = domainService.updateById(domainUrl);
+        if (!success) {
+            return Result.error("修改状态失败");
+        }
+
+        log.info("修改域名状态成功 | id={}, status={}", id, status);
+        return Result.success("修改成功");
+    }
+
+    /**
+     * 域名创建请求DTO
+     */
+    @Data
+    public static class DomainCreateRequest {
+        @NotBlank(message = "域名名称不能为空")
+        private String name;
+
+        @NotBlank(message = "域名地址不能为空")
+        private String domain;
+
+        private Integer status;
+
+        private String remark;
+    }
+
+    /**
+     * 域名更新请求DTO
+     */
+    @Data
+    public static class DomainUpdateRequest {
+        @NotBlank(message = "域名名称不能为空")
+        private String name;
+
+        @NotBlank(message = "域名地址不能为空")
+        private String domain;
+
+        @NotNull(message = "状态不能为空")
+        private Integer status;
+
+        private String remark;
+    }
+}
+

+ 202 - 0
fs-company/src/main/java/com/fs/company/controller/newAdv/LandingPageTemplateController.java

@@ -0,0 +1,202 @@
+package com.fs.company.controller.newAdv;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.common.result.Result;
+import com.fs.newAdv.domain.LandingPageTemplate;
+import com.fs.newAdv.domain.Lead;
+import com.fs.newAdv.domain.Site;
+import com.fs.newAdv.dto.req.LandingIndexReq;
+import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.service.ILandingPageTemplateService;
+import com.fs.newAdv.service.ILeadService;
+import com.fs.newAdv.service.ISiteService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.Valid;
+import java.time.LocalDateTime;
+import java.util.Map;
+import java.util.Objects;
+
+/**
+ * 投放页面模板Controller
+ *
+ * @author zhangqin
+ * @date 2025-11-06
+ */
+@Slf4j
+@RestController
+@RequestMapping("/adv/landing-page-templates")
+@Validated
+public class LandingPageTemplateController {
+
+    @Autowired
+    private ILandingPageTemplateService templateService;
+    @Autowired
+    private ISiteService siteService;
+    @Autowired
+    private ILeadService leadService;
+
+    /**
+     * 分页查询模板列表
+     *
+     * @param templateName 模板名称(模糊查询)
+     * @param templateType 模板类型
+     * @param status       状态
+     * @return 模板列表
+     */
+    @GetMapping("/page")
+    public Result<IPage<LandingPageTemplate>> page(
+            @RequestParam(defaultValue = "1") Integer pageNum,
+            @RequestParam(defaultValue = "10") Integer pageSize,
+            @RequestParam(required = false) String templateName,
+            @RequestParam(required = false) String templateType,
+            @RequestParam(required = false) Integer status) {
+
+
+        Page<LandingPageTemplate> pageParam = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<LandingPageTemplate> queryWrapper = new LambdaQueryWrapper<>();
+
+        if (StrUtil.isNotBlank(templateName)) {
+            queryWrapper.like(LandingPageTemplate::getTemplateName, templateName);
+        }
+        if (StrUtil.isNotBlank(templateType)) {
+            queryWrapper.eq(LandingPageTemplate::getTemplateType, templateType);
+        }
+        if (status != null) {
+            queryWrapper.eq(LandingPageTemplate::getStatus, status);
+        }
+        queryWrapper.ne(LandingPageTemplate::getStatus, 2);
+        queryWrapper.orderByDesc(LandingPageTemplate::getCreateTime);
+
+        IPage<LandingPageTemplate> result = templateService.page(pageParam, queryWrapper);
+        return Result.success(result);
+    }
+
+    /**
+     * 根据ID查询模板详情
+     *
+     * @param id 模板ID
+     * @return 模板详情
+     */
+    @GetMapping("/{id}")
+    public Result<LandingPageTemplate> getById(@PathVariable String id) {
+        LandingPageTemplate template = templateService.getById(id);
+        if (template == null) {
+            return Result.error("模板不存在");
+        }
+
+        return Result.success(template);
+    }
+
+    /**
+     * 新增模板
+     *
+     * @param request 模板信息
+     * @return 操作结果
+     */
+    @PostMapping
+    public Result<Void> create(@RequestBody @Validated LandingPageTemplate request) {
+        log.info("新增模板 | templateName={}, templateType={}",
+                request.getTemplateName(), request.getTemplateType());
+        boolean success = templateService.save(request);
+        if (!success) {
+            return Result.error("新增模板失败");
+        }
+        return Result.success();
+    }
+
+    /**
+     * 启停模板
+     *
+     * @return 操作结果
+     */
+    @PutMapping("/{id}")
+    public Result<Void> update(@PathVariable String id,@RequestBody LandingPageTemplate request) {
+        return templateService.updateById(request)?Result.success():Result.error("更新模板失败");
+    }
+
+    /**
+     * 启停模板
+     *
+     * @return 操作结果
+     */
+    @PostMapping("/enable/{id}")
+    public Result<Void> create(@PathVariable String id, @RequestParam(required = false) Integer status) {
+        if (status == 0 && siteService.findByTemplateId(id)) {
+            return Result.error("该模板正在被使用中,请先停用");
+        }
+        templateService.update(new LambdaUpdateWrapper<LandingPageTemplate>()
+                .eq(LandingPageTemplate::getId, id)
+                .set(LandingPageTemplate::getStatus, status));
+        return Result.success();
+    }
+
+    /**
+     * 落地页访问
+     */
+    @PostMapping("/index")
+    public Result<LandingPageTemplate> track(
+            @Valid @RequestBody LandingIndexReq req) {
+        log.info("落地页访问追踪:req={},params={}", req, req.getAllParams());
+        Long siteId = Long.valueOf(req.getAllParams().get("siteId"));
+        Site byId = siteService.getById(siteId);
+        // 保存落地页访问记录
+        saveLandingIndexTrace(siteId, req.getAllParams());
+        // 查询落地页模板
+        return Result.success(templateService.getById(byId.getLaunchPageId()));
+    }
+
+    public void saveLandingIndexTrace(Long siteId, Map<String, String> allParams) {
+        Site byId = siteService.getById(siteId);
+        Long advertiserId = byId.getAdvertiserId();
+        String traceId = getTraceIdByAdvertiser(Objects.requireNonNull(AdvertiserTypeEnum.getByCode(advertiserId)), allParams);
+        Lead byTraceId = leadService.getByTraceId(traceId);
+        if (ObjectUtil.isEmpty(byTraceId)) {
+            byTraceId = new Lead();
+            byTraceId.setAdvertiserId(advertiserId);
+            byTraceId.setSiteId(siteId);
+            leadService.save(byTraceId);
+        }
+        if (!Objects.equals(byTraceId.getSiteId(), siteId)) {
+            log.info("落地页站点信息异常:{}---{}", byTraceId.getSiteId(), siteId);
+        }
+        if (!Objects.equals(byTraceId.getAdvertiserId(), advertiserId)) {
+            log.info("落地页站点信息异常:{}---{}", byTraceId.getAdvertiserId(), advertiserId);
+        }
+        byTraceId.setLandingPageRawParams(JSONUtil.toJsonStr(allParams));
+        byTraceId.setLandingPageTrigger(1);
+        byTraceId.setLandingPageTs(LocalDateTime.now());
+        byTraceId.setUpdateTime(LocalDateTime.now());
+        leadService.updateById(byTraceId);
+    }
+
+
+    private String getTraceIdByAdvertiser(AdvertiserTypeEnum byCode, Map<String, String> allParams) {
+        switch (byCode) {
+            case OCEANENGINE:
+                return allParams.get("click_id");
+            case TENCENT:
+                return allParams.get("click_id");
+            case OPPO:
+                return allParams.get("traceId");
+            case BAIDU:
+                return allParams.get("bdVid");
+            case VIVO:
+                return allParams.get("requestId");
+            case IQIYI:
+                return allParams.get("traceId");
+            default:
+                return null;
+        }
+    }
+}
+

+ 135 - 0
fs-company/src/main/java/com/fs/company/controller/newAdv/PromotionAccountController.java

@@ -0,0 +1,135 @@
+package com.fs.company.controller.newAdv;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.common.result.Result;
+import com.fs.newAdv.domain.PromotionAccount;
+import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import com.fs.newAdv.service.IPromotionAccountService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 推广账号管理控制器
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Validated
+@RestController
+@RequestMapping("/adv/promotion-account")
+public class PromotionAccountController {
+
+    @Autowired
+    private IPromotionAccountService promotionAccountService;
+
+    /**
+     * 分页查询推广账号列表
+     *
+     * @param current 当前页
+     * @param size 每页数量
+     * @param accountName 账号名称(模糊查询)
+     * @param advertiserId 广告商ID
+     */
+    @GetMapping("/page")
+    public Result<IPage<PromotionAccount>> page(
+            @RequestParam(defaultValue = "1") Long current,
+            @RequestParam(defaultValue = "10") Long size,
+            @RequestParam(required = false) String accountName,
+            @RequestParam(required = false) String custom,
+            @RequestParam(required = false) Long advertiserId) {
+
+        Page<PromotionAccount> page = new Page<>(current, size);
+        LambdaQueryWrapper<PromotionAccount> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StrUtil.isNotBlank(accountName), PromotionAccount::getAccountName, accountName);
+        wrapper.eq(advertiserId != null, PromotionAccount::getAdvertiserId, advertiserId);
+        wrapper.eq(custom != null, PromotionAccount::getCustom, custom);
+        wrapper.orderByDesc(PromotionAccount::getCreateTime);
+        IPage<PromotionAccount> page1 = promotionAccountService.page(page, wrapper);
+        return Result.success(page1);
+    }
+
+    /**
+     * 根据ID查询推广账号详情
+     *
+     * @param id 账号ID
+     */
+    @GetMapping("/{id}")
+    public Result<PromotionAccount> getById(@PathVariable @NotNull(message = "账号ID不能为空") Long id) {
+        PromotionAccount account = promotionAccountService.getById(id);
+        return Result.success(account);
+    }
+
+    /**
+     * 创建推广账号
+     *
+     * @param account 推广账号信息
+     */
+    @PostMapping
+    public Result<Void> create(@RequestBody @Validated PromotionAccount account) {
+
+        boolean success = promotionAccountService.save(account);
+        checkAuthUrl(account);
+        promotionAccountService.updateById(account);
+        return success ? Result.success() : Result.error("创建失败");
+    }
+
+    private void checkAuthUrl(PromotionAccount account) {
+        if (account.getAdvertiserId().equals(AdvertiserTypeEnum.OCEANENGINE.getCode())){
+            // 巨量
+            account.setAuthUrl("https://open.oceanengine.com/audit/oauth.html?app_id="+account.getAppId()+"&state="+account.getId()+"&redirect_uri=https://track.mynatapp.cc/callback/oceanEngine/getAuthCode");
+        }
+        if (account.getAdvertiserId().equals(AdvertiserTypeEnum.BAIDU.getCode())){
+            // 百度
+            account.setAuthUrl(account.getAuthUrl().replaceAll("state=[^&]*", "state=" + account.getId()));
+        }
+
+    }
+
+    /**
+     * 更新推广账号
+     *
+     * @param id 账号ID
+     * @param account 推广账号信息
+     */
+    @PutMapping("/{id}")
+    public Result<Void> update(
+            @PathVariable @NotNull(message = "账号ID不能为空") Long id,
+            @RequestBody @Validated PromotionAccount account) {
+
+        account.setId(id);
+        checkAuthUrl(account);
+        boolean success = promotionAccountService.updateById(account);
+        return success ? Result.success() : Result.error("更新失败");
+    }
+
+    /**
+     * 删除推广账号
+     *
+     * @param id 账号ID
+     */
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable @NotNull(message = "账号ID不能为空") Long id) {
+        boolean success = promotionAccountService.removeById(id);
+        return success ? Result.success() : Result.error("删除失败");
+    }
+
+    /**
+     * 批量删除推广账号
+     *
+     * @param ids 账号ID数组
+     */
+    @DeleteMapping("/batch")
+    public Result<Void> batchDelete(@RequestBody Long[] ids) {
+        boolean success = promotionAccountService.batchDelete(ids);
+        return success ? Result.success() : Result.error("批量删除失败");
+    }
+}
+

+ 104 - 0
fs-company/src/main/java/com/fs/company/controller/newAdv/SiteController.java

@@ -0,0 +1,104 @@
+package com.fs.company.controller.newAdv;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.common.result.Result;
+import com.fs.newAdv.domain.Site;
+import com.fs.newAdv.service.ISiteService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 站点管理控制器
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@RestController
+@RequestMapping("/adv/site")
+public class SiteController {
+
+    @Autowired
+    private ISiteService siteService;
+
+    @GetMapping("/page")
+    public Result<IPage<Site>> pageSiteStatistics(
+            @RequestParam(defaultValue = "1") Long pageNum,
+            @RequestParam(defaultValue = "10") Long pageSize,
+            @RequestParam(required = false) Long siteName,
+            @RequestParam(required = false) Long launchType,
+            @RequestParam(required = false) Long advertiserId
+    ) {
+        Page<Site> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<Site> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(siteName != null, Site::getSiteName, siteName);
+        wrapper.eq(launchType != null, Site::getLaunchType, launchType);
+        wrapper.eq(advertiserId != null, Site::getAdvertiserId, advertiserId);
+        wrapper.orderByAsc(Site::getCreateTime);
+        IPage<Site> result = siteService.page(page, wrapper);
+        return Result.success(result);
+    }
+
+
+    @GetMapping("/list")
+    public Result<List<Site>> list() {
+        List<Site> list = siteService.list(new QueryWrapper<>());
+        return Result.success(list);
+    }
+
+    /**
+     * 查询站点详情
+     */
+    @GetMapping("/{id}")
+    public Result<Site> getById(@PathVariable Long id) {
+        Site site = siteService.getById(id);
+        return Result.success(site);
+    }
+
+    /**
+     * 创建站点
+     */
+    @PostMapping
+    public Result<Void> create(@RequestBody Site site) {
+        siteService.createSite(site);
+        return Result.success();
+    }
+
+    /**
+     * 更新站点
+     */
+    @PutMapping("/{id}")
+    public Result<Void> update(@PathVariable Long id, @RequestBody Site site) {
+        site.setId(id);
+        siteService.updateById(site);
+        return Result.success();
+    }
+
+    /**
+     * 删除站点
+     */
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable Long id) {
+        siteService.removeById(id);
+        return Result.success();
+    }
+
+
+    /**
+     * 启用停用站点
+     */
+    @PostMapping("enable/{id}")
+    public Result<Void> enable(@PathVariable Long id) {
+        Site byId = siteService.getById(id);
+        byId.setStatus(byId.getStatus() == 1 ? 0 : 1);
+        siteService.updateById(byId);
+        return Result.success();
+    }
+}
+

+ 121 - 0
fs-company/src/main/java/com/fs/company/controller/newAdv/StatisticsController.java

@@ -0,0 +1,121 @@
+package com.fs.company.controller.newAdv;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.common.result.Result;
+import com.fs.newAdv.domain.Site;
+import com.fs.newAdv.domain.SiteStatistics;
+import com.fs.newAdv.service.ILeadService;
+import com.fs.newAdv.service.ISiteService;
+import com.fs.newAdv.service.ISiteStatisticsService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+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;
+
+import java.math.BigDecimal;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 站点统计控制器
+ * 提供查询接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Validated
+@RestController
+@RequestMapping("/adv/site-statistics")
+public class StatisticsController {
+
+    @Autowired
+    private ISiteStatisticsService statisticsService;
+
+    @Autowired
+    private ISiteService siteService;
+    private ILeadService lead;
+
+    /**
+     * 分页查询所有站点统计数据
+     */
+    @GetMapping("/page")
+    public Result<IPage<SiteStatistics>> pageSiteStatistics(
+            @RequestParam(defaultValue = "1") Long pageNum,
+            @RequestParam(defaultValue = "10") Long pageSize,
+            @RequestParam(required = false) Long advertiserId,
+            @RequestParam(required = true) String startDate,
+            @RequestParam(required = true) String endDate
+    ) {
+        Page<Site> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<Site> siteLambdaQueryWrapper = new LambdaQueryWrapper<>();
+        siteLambdaQueryWrapper.eq(advertiserId != null, Site::getAdvertiserId, advertiserId);
+        siteLambdaQueryWrapper.orderByDesc(Site::getCreateTime);
+        IPage<Site> result = siteService.page(page, siteLambdaQueryWrapper);
+        List<Site> siteList = result.getRecords();
+        List<Long> siteIds = siteList.stream().map(Site::getId).collect(Collectors.toList());
+
+        IPage<SiteStatistics> siteStatisticsIPage = new Page<>(pageNum, pageSize);
+        if (!siteIds.isEmpty()) {
+            LambdaQueryWrapper<SiteStatistics> statisticsLambdaQueryWrapper = new LambdaQueryWrapper<>();
+            statisticsLambdaQueryWrapper.in(SiteStatistics::getSiteId, siteIds);
+            statisticsLambdaQueryWrapper.orderByDesc(SiteStatistics::getId);
+            statisticsLambdaQueryWrapper.between(SiteStatistics::getStatDate, startDate, endDate);
+            List<SiteStatistics> list = statisticsService.list(statisticsLambdaQueryWrapper);
+
+            // 按照siteId分组并求和
+            Map<Long, SiteStatistics> groupedMap = list.stream()
+                    .collect(Collectors.groupingBy(
+                            SiteStatistics::getSiteId,
+                            Collectors.collectingAndThen(
+                                    Collectors.toList(),
+                                    siteStatsList -> {
+                                        SiteStatistics aggregated = new SiteStatistics();
+                                        if (!siteStatsList.isEmpty()) {
+                                            SiteStatistics first = siteStatsList.get(0);
+                                            aggregated.setSiteId(first.getSiteId());
+                                            aggregated.setSiteName(first.getSiteName());
+
+                                            // 对所有数值字段求和
+                                            aggregated.setPv(siteStatsList.stream().mapToInt(s -> s.getPv() != null ? s.getPv() : 0).sum());
+                                            aggregated.setUv(siteStatsList.stream().mapToInt(s -> s.getUv() != null ? s.getUv() : 0).sum());
+                                            aggregated.setImpressionCount(siteStatsList.stream().mapToInt(s -> s.getImpressionCount() != null ? s.getImpressionCount() : 0).sum());
+                                            aggregated.setSysClickCount(siteStatsList.stream().mapToInt(s -> s.getSysClickCount() != null ? s.getSysClickCount() : 0).sum());
+                                            aggregated.setClickCount(siteStatsList.stream().mapToInt(s -> s.getClickCount() != null ? s.getClickCount() : 0).sum());
+                                            aggregated.setAccountCost(siteStatsList.stream().map(s -> s.getAccountCost() != null ? s.getAccountCost() : BigDecimal.ZERO).reduce(BigDecimal.ZERO, BigDecimal::add));
+                                            aggregated.setActualCost(siteStatsList.stream().map(s -> s.getActualCost() != null ? s.getActualCost() : BigDecimal.ZERO).reduce(BigDecimal.ZERO, BigDecimal::add));
+                                            aggregated.setCardCount(siteStatsList.stream().mapToInt(s -> s.getCardCount() != null ? s.getCardCount() : 0).sum());
+                                            aggregated.setWechatAddCount(siteStatsList.stream().mapToInt(s -> s.getWechatAddCount() != null ? s.getWechatAddCount() : 0).sum());
+                                            aggregated.setRegisterSuccessCount(siteStatsList.stream().mapToInt(s -> s.getRegisterSuccessCount() != null ? s.getRegisterSuccessCount() : 0).sum());
+                                            aggregated.setWechatGroupCount(siteStatsList.stream().mapToInt(s -> s.getWechatGroupCount() != null ? s.getWechatGroupCount() : 0).sum());
+                                            aggregated.setWechatDeleteCount(siteStatsList.stream().mapToInt(s -> s.getWechatDeleteCount() != null ? s.getWechatDeleteCount() : 0).sum());
+                                            aggregated.setMiniLaunchIndexCount(siteStatsList.stream().mapToInt(s -> s.getMiniLaunchIndexCount() != null ? s.getMiniLaunchIndexCount() : 0).sum());
+                                            aggregated.setMiniAuthIndexCount(siteStatsList.stream().mapToInt(s -> s.getMiniAuthIndexCount() != null ? s.getMiniAuthIndexCount() : 0).sum());
+                                            aggregated.setMiniAuthCount(siteStatsList.stream().mapToInt(s -> s.getMiniAuthCount() != null ? s.getMiniAuthCount() : 0).sum());
+                                            aggregated.setMiniQrCodeIndexCount(siteStatsList.stream().mapToInt(s -> s.getMiniQrCodeIndexCount() != null ? s.getMiniQrCodeIndexCount() : 0).sum());
+
+                                            // 计算所有比率
+                                            aggregated.calculateRates();
+                                        }
+                                        return aggregated;
+                                    }
+                            )
+                    ));
+
+            // 转换为列表
+            List<SiteStatistics> aggregatedList = new ArrayList<>(groupedMap.values());
+            siteStatisticsIPage.setRecords(aggregatedList);
+        }
+        siteStatisticsIPage.setTotal(result.getTotal());
+        siteStatisticsIPage.setPages(result.getPages());
+        return Result.success(siteStatisticsIPage);
+    }
+}
+

+ 98 - 0
fs-company/src/main/java/com/fs/company/controller/newAdv/TrackingLinkController.java

@@ -0,0 +1,98 @@
+package com.fs.company.controller.newAdv;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.common.result.Result;
+import com.fs.newAdv.domain.TrackingLink;
+import com.fs.newAdv.mapper.TrackingLinkMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import javax.validation.constraints.NotNull;
+import java.util.List;
+
+/**
+ * 监测链接管理控制器
+ * 提供查询接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Validated
+@RestController
+@RequestMapping("/adv/tracking-link")
+public class TrackingLinkController {
+
+    @Autowired
+    private TrackingLinkMapper trackingLinkMapper;
+
+    /**
+     * 分页查询监测链接列表
+     */
+    @GetMapping("/page")
+    public Result<IPage<TrackingLink>> page(
+            @RequestParam(defaultValue = "1") Long current,
+            @RequestParam(defaultValue = "10") Long size,
+            @RequestParam(required = false) String trackingName,
+            @RequestParam(required = false) Long advertiserId,
+            @RequestParam(required = false) String promotionType) {
+
+        Page<TrackingLink> page = new Page<>(current, size);
+
+        LambdaQueryWrapper<TrackingLink> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StrUtil.isNotBlank(trackingName), TrackingLink::getTrackingName, trackingName);
+        wrapper.eq(advertiserId != null, TrackingLink::getAdvertiserId, advertiserId);
+        wrapper.eq(StrUtil.isNotBlank(promotionType), TrackingLink::getPromotionType, promotionType);
+        wrapper.orderByDesc(TrackingLink::getCreateTime);
+
+        IPage<TrackingLink> result = trackingLinkMapper.selectPage(page, wrapper);
+        return Result.success(result);
+    }
+
+    /**
+     * 查询所有监测链接列表(不分页)
+     */
+    @GetMapping("/list")
+    public Result<List<TrackingLink>> list(
+            @RequestParam(required = false) String trackingName,
+            @RequestParam(required = false) Long advertiserId) {
+
+        LambdaQueryWrapper<TrackingLink> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StrUtil.isNotBlank(trackingName), TrackingLink::getTrackingName, trackingName);
+        wrapper.eq(advertiserId != null, TrackingLink::getAdvertiserId, advertiserId);
+        wrapper.orderByDesc(TrackingLink::getCreateTime);
+
+        List<TrackingLink> list = trackingLinkMapper.selectList(wrapper);
+        return Result.success(list);
+    }
+
+    /**
+     * 根据ID查询监测链接详情
+     */
+    @GetMapping("/{id}")
+    public Result<TrackingLink> getById(@PathVariable @NotNull(message = "链接ID不能为空") Long id) {
+        TrackingLink trackingLink = trackingLinkMapper.selectById(id);
+        return Result.success(trackingLink);
+    }
+
+    /**
+     * 根据广告商ID查询监测链接列表
+     */
+    @GetMapping("/advertiser/{advertiserId}")
+    public Result<List<TrackingLink>> getByAdvertiserId(
+            @PathVariable @NotNull(message = "广告商ID不能为空") Long advertiserId) {
+
+        LambdaQueryWrapper<TrackingLink> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(TrackingLink::getAdvertiserId, advertiserId);
+        wrapper.orderByDesc(TrackingLink::getCreateTime);
+
+        List<TrackingLink> list = trackingLinkMapper.selectList(wrapper);
+        return Result.success(list);
+    }
+}
+

+ 104 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwAssignRuleController.java

@@ -0,0 +1,104 @@
+package com.fs.company.controller.qw;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.common.result.Result;
+import com.fs.qw.domain.QwAssignRule;
+import com.fs.qw.domain.QwAssignRuleUser;
+import com.fs.qw.service.IQwAssignRuleService;
+import com.fs.qw.service.IQwAssignRuleUserService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * <p>
+ * 前端控制器
+ * </p>
+ *
+ * @author zhangqin
+ * @since 2025-11-28
+ */
+@RestController
+@RequestMapping("/qwAssignRule")
+public class QwAssignRuleController {
+    @Autowired
+    private IQwAssignRuleService qwAssignRuleService;
+    @Autowired
+    private IQwAssignRuleUserService qwAssignRuleUserService;
+
+    /**
+     * 分页查询微信分配规则
+     */
+    @GetMapping("/page")
+    public Result<IPage<QwAssignRule>> page(
+            @RequestParam(defaultValue = "1") Long pageNum,
+            @RequestParam(defaultValue = "10") Long pageSize,
+            @RequestParam(required = false) String ruleName
+    ) {
+        Page<QwAssignRule> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<QwAssignRule> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StrUtil.isNotBlank(ruleName), QwAssignRule::getRuleName, ruleName);
+        wrapper.orderByDesc(QwAssignRule::getCreateTime);
+        IPage<QwAssignRule> result = qwAssignRuleService.page(page, wrapper);
+        result.getRecords().forEach(item -> {
+            List<QwAssignRuleUser> list = qwAssignRuleUserService.list(new LambdaQueryWrapper<QwAssignRuleUser>()
+                    .eq(QwAssignRuleUser::getAssignId, item.getId()));
+            for (QwAssignRuleUser qwAssignRuleUser : list) {
+                qwAssignRuleUser.setAssignNumCount(qwAssignRuleUser.getAssignNumCount()+qwAssignRuleUser.getAssignNumToDay());
+                qwAssignRuleUser.setAddNumCount(qwAssignRuleUser.getAddNumCount()+qwAssignRuleUser.getAddNumToDay());
+            }
+            item.setQwAssignRuleUsers(list);
+        });
+        return Result.success(result);
+    }
+
+    /**
+     * 创建修改分配规则
+     */
+    @PostMapping("/addOrUpdate")
+    @Transactional(rollbackFor = Exception.class)
+    public Result<QwAssignRule> create(@RequestBody QwAssignRule qwGroupLiveCode) {
+        boolean success = false;
+        if (qwGroupLiveCode.getId() != null) {
+            success = qwAssignRuleService.updateById(qwGroupLiveCode);
+        } else {
+            success = qwAssignRuleService.save(qwGroupLiveCode);
+        }
+        if (success) {
+            qwAssignRuleUserService.remove(new LambdaQueryWrapper<QwAssignRuleUser>()
+                    .eq(QwAssignRuleUser::getAssignId, qwGroupLiveCode.getId()));
+            for (QwAssignRuleUser qwAssignRuleUser : qwGroupLiveCode.getQwAssignRuleUsers()) {
+                qwAssignRuleUser.setAssignId(qwGroupLiveCode.getId());
+            }
+            qwAssignRuleUserService.saveBatch(qwGroupLiveCode.getQwAssignRuleUsers());
+        }
+
+        return success ? Result.success() : Result.error("创建失败");
+    }
+
+    /**
+     * 启用状态
+     */
+    @PostMapping("/enable/{id}/{status}")
+    @Transactional(rollbackFor = Exception.class)
+    public Result<QwAssignRule> create(@PathVariable Long id, @PathVariable Integer status) {
+        boolean update = qwAssignRuleService.update(new LambdaUpdateWrapper<QwAssignRule>()
+                .eq(QwAssignRule::getId, id)
+                .set(QwAssignRule::getStatus, status));
+        return update ? Result.success() : Result.error("修改失败");
+    }
+
+    @GetMapping("/{id}")
+    public Result<QwAssignRule> create(@PathVariable Long id) {
+        QwAssignRule byId = qwAssignRuleService.getById(id);
+        byId.setQwAssignRuleUsers(qwAssignRuleUserService.list(new LambdaQueryWrapper<QwAssignRuleUser>()
+                .eq(QwAssignRuleUser::getAssignId, id)));
+        return Result.success(byId);
+    }
+}

+ 212 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwCustomerLinkController.java

@@ -0,0 +1,212 @@
+package com.fs.company.controller.qw;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.util.IdUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.common.result.Result;
+import com.fs.qw.domain.QwCustomerLink;
+import com.fs.qw.domain.QwCustomerLinkChannel;
+import com.fs.qw.domain.QwCustomerLinkUser;
+import com.fs.qw.dto.QwCustomerLinkChannelReq;
+import com.fs.qw.dto.QwCustomerLinkReq;
+import com.fs.qw.dto.QwCustomerLinkUserDto;
+import com.fs.qw.service.IQwCustomerLinkChannelService;
+import com.fs.qw.service.IQwCustomerLinkService;
+import com.fs.qw.service.IQwCustomerLinkUserService;
+import com.fs.qwApi.param.QwLinkCreateParam;
+import com.fs.qwApi.service.QwApiService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+/**
+ * <p>
+ * 企微获客链接 前端控制器
+ * </p>
+ *
+ * @author zhangqin
+ * @since 2025-12-03
+ */
+@RestController
+@RequestMapping("/qwCustomerLink")
+public class QwCustomerLinkController {
+
+    @Autowired
+    private IQwCustomerLinkService qwCustomerLinkService;
+    @Autowired
+    private IQwCustomerLinkUserService qwCustomerLinkUserService;
+    @Autowired
+    private IQwCustomerLinkChannelService qwCustomerLinkChannelService;
+    @Autowired
+    private QwApiService qwApiService;
+
+    /**
+     * 分页查询主链
+     */
+    @GetMapping("/page")
+    public Result<IPage<QwCustomerLink>> page(
+            @RequestParam(defaultValue = "1") Long pageNum,
+            @RequestParam(defaultValue = "10") Long pageSize,
+            @RequestParam(required = false) String corpName,
+            @RequestParam(required = false) String linkName
+    ) {
+        Page<QwCustomerLink> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<QwCustomerLink> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StrUtil.isNotBlank(corpName), QwCustomerLink::getCorpName, corpName);
+        wrapper.like(StrUtil.isNotBlank(linkName), QwCustomerLink::getLinkName, linkName);
+        wrapper.eq(QwCustomerLink::getStatus, 0);
+        wrapper.orderByDesc(QwCustomerLink::getCreateTime);
+        IPage<QwCustomerLink> result = qwCustomerLinkService.page(page, wrapper);
+        return Result.success(result);
+    }
+
+    /**
+     * 分页查询渠道链
+     */
+    @GetMapping("/channel/page")
+    public Result<IPage<QwCustomerLinkChannel>> pageChannel(
+            @RequestParam(defaultValue = "1") Long pageNum,
+            @RequestParam(defaultValue = "10") Long pageSize,
+            @RequestParam(required = false) String linkName,
+            @RequestParam(required = false) String sysLinkId
+    ) {
+        Page<QwCustomerLinkChannel> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<QwCustomerLinkChannel> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StrUtil.isNotBlank(linkName), QwCustomerLinkChannel::getLinkName, linkName);
+        wrapper.eq(QwCustomerLinkChannel::getStatus, 0);
+        wrapper.eq(QwCustomerLinkChannel::getSysLinkId, sysLinkId);
+        wrapper.orderByDesc(QwCustomerLinkChannel::getCreateTime);
+        IPage<QwCustomerLinkChannel> result = qwCustomerLinkChannelService.page(page, wrapper);
+        return Result.success(result);
+    }
+
+    /**
+     * 新增/编辑 主链
+     */
+    @PostMapping("/createOrUpdate")
+    @Transactional(rollbackFor = Exception.class)
+    public Result<Void> create(@RequestBody QwCustomerLinkReq qwGroupLiveCode) {
+        QwCustomerLink bean = BeanUtil.toBean(qwGroupLiveCode, QwCustomerLink.class);
+
+        QwLinkCreateParam qwLinkCreateParam = new QwLinkCreateParam();
+        qwLinkCreateParam.setLink_name(qwGroupLiveCode.getLinkName());
+        qwLinkCreateParam.setSkip_verify(qwGroupLiveCode.getSkipVerify());
+        QwLinkCreateParam.Range range = new QwLinkCreateParam.Range();
+
+        range.setUser_list(qwGroupLiveCode.getLinkUser().stream().map(QwCustomerLinkUserDto::getQwUserId).collect(Collectors.toList()));
+        qwLinkCreateParam.setRange(range);
+
+        boolean success = false;
+        if (qwGroupLiveCode.getId() != null) {
+            QwCustomerLink byId = qwCustomerLinkService.getById(qwGroupLiveCode.getId());
+            qwLinkCreateParam.setLink_id(byId.getLinkId());
+            // qwApiService.linkUpdate(qwLinkCreateParam, qwGroupLiveCode.getCorpId());
+            success = qwCustomerLinkService.updateById(bean);
+        } else {
+/*            QwLinkCreateResult qwLinkCreateResult = qwApiService.linkCreate(qwLinkCreateParam, qwGroupLiveCode.getCorpId());
+            bean.setLinkId(qwLinkCreateResult.getLinkId());
+            bean.setUrl(qwLinkCreateResult.getUrl());*/
+            bean.setLinkId(IdUtil.randomUUID());
+            bean.setUrl("https://work.weixin.qq.com/ca/" + IdUtil.randomUUID());
+            success = qwCustomerLinkService.save(bean);
+        }
+
+        if (success) {
+            qwCustomerLinkUserService.remove(new LambdaQueryWrapper<QwCustomerLinkUser>().eq(QwCustomerLinkUser::getSysLinkId, bean.getId()));
+            qwCustomerLinkUserService.saveBatch(qwGroupLiveCode.getLinkUser().stream()
+                    .map(e -> {
+                        QwCustomerLinkUser user = new QwCustomerLinkUser();
+                        user.setSysLinkId(bean.getId());
+                        user.setQwUserId(e.getQwUserId());
+                        user.setSysQwUserId(e.getSysQwUserId());
+                        user.setQwUserName(e.getQwUserName());
+                        return user;
+                    })
+                    .collect(Collectors.toList()));
+        }
+        return success ? Result.success() : Result.error("创建失败");
+    }
+
+    /**
+     * 新增渠道链接
+     */
+    @PostMapping("/channel/create")
+    public Result<Void> createChannel(@RequestBody QwCustomerLinkChannelReq linkChannelReq) {
+
+        // 主链
+        QwCustomerLink qwCustomerLink = qwCustomerLinkService.getById(linkChannelReq.getSysLinkId());
+        // 主链关联的企微用户
+        List<QwCustomerLinkUser> linkUsers = qwCustomerLinkUserService.list(new LambdaQueryWrapper<QwCustomerLinkUser>()
+                .eq(QwCustomerLinkUser::getSysLinkId, qwCustomerLink.getId()));
+        List<String> userIdList = linkUsers.stream()
+                .map(QwCustomerLinkUser::getQwUserId)
+                .collect(Collectors.toList());
+
+        List<QwCustomerLinkChannel> channels = linkChannelReq.getChannelList().stream()
+                .map(channelDto -> {
+                    QwLinkCreateParam param = new QwLinkCreateParam();
+                    param.setLink_name(channelDto.getChannelName());
+                    param.setSkip_verify(qwCustomerLink.getSkipVerify().equals(1));
+                    QwLinkCreateParam.Range range = new QwLinkCreateParam.Range();
+                    range.setUser_list(userIdList);
+                    param.setRange(range);
+
+                    // QwLinkCreateResult linkResult = qwApiService.linkCreate(param, qwCustomerLink.getCorpId());
+
+                    QwCustomerLinkChannel channel = new QwCustomerLinkChannel();
+                    channel.setChannelId(channelDto.getChannelId());
+                    channel.setChannelName(channelDto.getChannelName());
+                    channel.setSysLinkId(linkChannelReq.getSysLinkId());
+                    channel.setLinkName(channelDto.getChannelName());
+                    // channel.setLinkId(linkResult.getLinkId());
+                    // channel.setUrl(linkResult.getUrl());
+
+                    channel.setLinkId(IdUtil.randomUUID());
+                    channel.setUrl("https://work.weixin.qq.com/ca/" + IdUtil.randomUUID());
+                    return channel;
+                })
+                .collect(Collectors.toList());
+
+        return qwCustomerLinkChannelService.saveBatch(channels) ? Result.success() : Result.error("创建失败");
+    }
+
+
+    /**
+     * 删除 主链
+     */
+    @PostMapping("/delete/{id}")
+    public Result<Void> create(@PathVariable Long id) {
+        return qwCustomerLinkService.update(new LambdaUpdateWrapper<QwCustomerLink>()
+                .eq(QwCustomerLink::getId, id)
+                .set(QwCustomerLink::getStatus, 1)) ? Result.success() : Result.error("删除失败");
+    }
+
+    /**
+     * 删除 渠道链
+     */
+    @PostMapping("/channel/delete/{id}")
+    public Result<Void> createChannel(@PathVariable Long id) {
+        return qwCustomerLinkChannelService.removeById(id) ? Result.success() : Result.error("删除失败");
+    }
+
+    /**
+     * 查询主链信息
+     *
+     * @param id
+     * @return
+     */
+    @GetMapping("/{id}")
+    public Result<QwCustomerLink> getById(@PathVariable Long id) {
+        QwCustomerLink byId = qwCustomerLinkService.getById(id);
+        byId.setQwCustomerLinkUsers(qwCustomerLinkUserService.list(new LambdaQueryWrapper<QwCustomerLinkUser>()
+                .eq(QwCustomerLinkUser::getSysLinkId, id)));
+        return Result.success(byId);
+    }
+}

+ 75 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwGroupActualController.java

@@ -0,0 +1,75 @@
+package com.fs.company.controller.qw;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.common.result.Result;
+import com.fs.qw.domain.QwGroupActual;
+import com.fs.qw.domain.QwGroupLiveCode;
+import com.fs.qw.service.IQwGroupActualService;
+import com.fs.qw.service.IQwGroupLiveCodeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * <p>
+ * 群活码实际二维码表 前端控制器
+ * </p>
+ *
+ * @author zhangqin
+ * @since 2025-11-28
+ */
+@RestController
+@RequestMapping("/qwGroupActual")
+public class QwGroupActualController {
+    @Autowired
+    private IQwGroupLiveCodeService qwGroupLiveCodeService;
+
+    @Autowired
+    private IQwGroupActualService qwGroupActualService;
+
+    /**
+     * 分页查询群活码实际二维码
+     */
+    @GetMapping("/page")
+    public Result<IPage<QwGroupActual>> page(
+            @RequestParam(defaultValue = "1") Long pageNum,
+            @RequestParam(defaultValue = "10") Long pageSize,
+            @RequestParam(required = false) String status,
+            @RequestParam(required = false) String groupName,
+            @RequestParam(required = false) Long liveCodeId
+    ) {
+        Page<QwGroupActual> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<QwGroupActual> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StrUtil.isNotBlank(groupName), QwGroupActual::getGroupName, groupName);
+        wrapper.eq(StrUtil.isNotBlank(status), QwGroupActual::getStatus, status);
+        wrapper.eq(ObjectUtil.isNotEmpty(liveCodeId), QwGroupActual::getLiveCodeId, liveCodeId);
+        wrapper.orderByDesc(QwGroupActual::getCreateTime);
+        IPage<QwGroupActual> result = qwGroupActualService.page(page, wrapper);
+        return Result.success(result);
+    }
+
+    /**
+     * 创建群活码/修改群活码
+     */
+    @PostMapping("/addOrUpdate")
+    public Result<Void> create(@RequestBody QwGroupActual qwGroupLiveCode) {
+        boolean success = false;
+        if (qwGroupLiveCode.getId() != null) {
+            success = qwGroupActualService.updateById(qwGroupLiveCode);
+        } else {
+            QwGroupLiveCode byId = qwGroupLiveCodeService.getById(qwGroupLiveCode.getLiveCodeId());
+            byId.setQrcodeNum(byId.getQrcodeNum() + 1);
+            qwGroupLiveCodeService.updateById(byId);
+            success = qwGroupActualService.save(qwGroupLiveCode);
+        }
+        return success ? Result.success() : Result.error("创建失败");
+    }
+
+    @GetMapping("/{id}")
+    public Result<QwGroupActual> create(@PathVariable Long id) {
+        return Result.success(qwGroupActualService.getById(id));
+    }
+}

+ 66 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwGroupLiveCodeController.java

@@ -0,0 +1,66 @@
+package com.fs.company.controller.qw;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.common.result.Result;
+import com.fs.qw.domain.QwGroupLiveCode;
+import com.fs.qw.service.IQwGroupLiveCodeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+/**
+ * <p>
+ * 群活码 前端控制器
+ * </p>
+ *
+ * @author zhangqin
+ * @since 2025-11-28
+ */
+@RestController
+@RequestMapping("/qwGroupLiveCode")
+public class QwGroupLiveCodeController {
+    @Autowired
+    private IQwGroupLiveCodeService qwGroupLiveCodeService;
+
+    /**
+     * 分页查询群活码
+     */
+    @GetMapping("/page")
+    public Result<IPage<QwGroupLiveCode>> page(
+            @RequestParam(defaultValue = "1") Long pageNum,
+            @RequestParam(defaultValue = "10") Long pageSize,
+            @RequestParam(required = false) String status,
+            @RequestParam(required = false) String groupName
+    ) {
+        Page<QwGroupLiveCode> page = new Page<>(pageNum, pageSize);
+        LambdaQueryWrapper<QwGroupLiveCode> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StrUtil.isNotBlank(groupName), QwGroupLiveCode::getGroupName, groupName);
+        wrapper.like(StrUtil.isNotBlank(status), QwGroupLiveCode::getStatus, status);
+        wrapper.orderByDesc(QwGroupLiveCode::getCreateTime);
+        IPage<QwGroupLiveCode> result = qwGroupLiveCodeService.page(page, wrapper);
+        return Result.success(result);
+    }
+
+    /**
+     * 创建群活码/修改群活码
+     */
+    @PostMapping("/addOrUpdate")
+    public Result<Void> create(@RequestBody @Validated QwGroupLiveCode qwGroupLiveCode) {
+        boolean success = false;
+        if (qwGroupLiveCode.getId() != null) {
+            success = qwGroupLiveCodeService.updateById(qwGroupLiveCode);
+        } else {
+            success = qwGroupLiveCodeService.save(qwGroupLiveCode);
+        }
+        return success ? Result.success() : Result.error("创建失败");
+    }
+
+
+    @GetMapping("/{id}")
+    public Result<QwGroupLiveCode> getById(@PathVariable Long id) {
+        return Result.success(qwGroupLiveCodeService.getById(id));
+    }
+}

+ 4 - 0
fs-company/src/main/java/com/fs/framework/config/MyBatisConfig.java

@@ -1,5 +1,6 @@
 package com.fs.framework.config;
 
+import com.baomidou.mybatisplus.extension.plugins.PaginationInterceptor;
 import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean;
 import com.fs.common.utils.StringUtils;
 import org.apache.ibatis.io.VFS;
@@ -145,6 +146,9 @@ public class MyBatisConfig
         sessionFactory.setTypeAliasesPackage(typeAliasesPackage);
         sessionFactory.setMapperLocations(new PathMatchingResourcePatternResolver().getResources(mapperLocations));
         sessionFactory.setConfigLocation(new DefaultResourceLoader().getResource(configLocation));
+        // 添加MyBatis-Plus分页插件
+        PaginationInterceptor paginationInterceptor = new PaginationInterceptor();
+        sessionFactory.setPlugins(new PaginationInterceptor[]{paginationInterceptor});
         return sessionFactory.getObject();
     }
 }

+ 2 - 2
fs-company/src/main/java/com/fs/framework/security/handle/AuthenticationEntryPointImpl.java

@@ -16,8 +16,8 @@ import java.io.Serializable;
 
 /**
  * 认证失败处理类 返回未授权
- * 
- 
+ *
+
  */
 @Component
 public class AuthenticationEntryPointImpl implements AuthenticationEntryPoint, Serializable

+ 29 - 1
fs-qw-api/src/main/java/com/fs/app/service/QwDataCallbackService.java

@@ -1,6 +1,7 @@
 package com.fs.app.service;
 
 import cn.hutool.core.util.RandomUtil;
+import cn.hutool.core.util.StrUtil;
 import com.alibaba.fastjson.JSON;
 import com.fs.app.util.WXBizMsgCrypt;
 import com.fs.common.core.domain.AjaxResult;
@@ -8,6 +9,7 @@ import com.fs.common.core.redis.RedisCache;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.PubFun;
 import com.fs.company.service.ICompanyConfigService;
+import com.fs.newAdv.service.ILeadService;
 import com.fs.qw.domain.*;
 import com.fs.qw.mapper.QwCompanyMapper;
 import com.fs.qw.mapper.QwExternalContactMapper;
@@ -92,6 +94,8 @@ public class QwDataCallbackService {
     private RedisCache redisCache;
     @Autowired
     private QwCompanyMapper qwCompanyMapper;
+    @Autowired
+    private ILeadService leadService;
 
     @Async
     public void dataCallback( Document document,String corpId,QwCompany qwCompany) throws Exception {
@@ -224,10 +228,32 @@ public class QwDataCallbackService {
                                     String qwApiExternal = redisCache.getCacheObject(cacheKey);
                                     if (StringUtil.strIsNullOrEmpty(qwApiExternal)) {
                                         try {
+                                            String externalUserID = root.getElementsByTagName("ExternalUserID").item(0).getTextContent();
+                                            String userID = root.getElementsByTagName("UserID").item(0).getTextContent();
                                             // 5. 新增用户
-                                            qwExternalContactService.insertQwExternalContactByExternalUserId(root.getElementsByTagName("ExternalUserID").item(0).getTextContent(),root.getElementsByTagName("UserID").item(0).getTextContent(),null,corpId,State,WelcomeCode);
+                                            qwExternalContactService.insertQwExternalContactByExternalUserId(externalUserID,userID,null,corpId,State,WelcomeCode);
                                             // 6. 业务逻辑执行成功后,写入 Redis 缓存(有效期 10 分钟)
                                             redisCache.setCacheObject(cacheKey, "1", 10, TimeUnit.MINUTES);
+
+                                            // 广告线索处理
+                                            leadService.updateAddMemberLead(externalUserID,userID,corpId,State)
+                                                    .thenAccept(result -> {
+                                                        if (StrUtil.isNotEmpty(result)) {
+                                                            QwExternalContact qwExternalContact = qwExternalContactMapper.selectQwExternalByExternalIdAndCompanyIdToIdAndFs(externalUserID, userID, corpId);
+                                                            if (qwExternalContact == null){
+                                                                try {
+                                                                    Thread.sleep(2000);
+                                                                } catch (InterruptedException e) {
+                                                                    throw new RuntimeException(e);
+                                                                }
+                                                                qwExternalContact = qwExternalContactMapper.selectQwExternalByExternalIdAndCompanyIdToIdAndFs(externalUserID, userID, corpId);
+                                                            }
+                                                            QwExternalContact temp = new QwExternalContact();
+                                                            temp.setId(qwExternalContact.getId());
+                                                            temp.setTraceId(result);
+                                                            qwExternalContactMapper.updateById(temp);
+                                                        }
+                                                    });
                                         } catch (Exception e) {
                                             // 7. 业务逻辑失败时,删除缓存
                                             redisCache.deleteObject(cacheKey);
@@ -404,6 +430,8 @@ public class QwDataCallbackService {
 
                                     if (qwGroupChatUserOld==null) {
                                         qwGroupChatUserService.insertQwGroupChatUser(qwGroupChatUser);
+                                        // 群员入群 广告判断记录
+                                        leadService.updateGroupAddMemberLead(qwGroupChatUser.getUserId(),qwGroupChatUser.getChatId(),qwGroupChatUser.getCorpId(),qwGroupChatUser.getUnionid());
                                     }else {
                                         qwGroupChatUserOld.setIsOut(1L);
                                         qwGroupChatUserOld.setCorpId(corpId);

+ 61 - 0
fs-qw-api/src/main/java/com/fs/framework/config/AsyncConfig.java

@@ -0,0 +1,61 @@
+package com.fs.framework.config;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.AsyncConfigurer;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+/**
+ * 异步任务配置类
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Configuration
+public class AsyncConfig implements AsyncConfigurer {
+
+    @Bean(name = "asyncExecutor")
+    @Override
+    public Executor getAsyncExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+
+        // 核心线程数
+        executor.setCorePoolSize(10);
+
+        // 最大线程数
+        executor.setMaxPoolSize(50);
+
+        // 队列容量
+        executor.setQueueCapacity(1000);
+
+        // 线程存活时间(秒)
+        executor.setKeepAliveSeconds(60);
+
+        // 线程名称前缀
+        executor.setThreadNamePrefix("async-task-");
+
+        // 拒绝策略:调用者运行
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+
+        // 等待所有任务完成后再关闭线程池
+        executor.setWaitForTasksToCompleteOnShutdown(true);
+
+        // 等待时间
+        executor.setAwaitTerminationSeconds(60);
+
+
+/*        // 关键:传递 MDC
+        executor.setTaskDecorator(new MdcTaskDecorator());*/
+
+        executor.initialize();
+
+        log.info("异步线程池初始化完成");
+        return executor;
+    }
+}
+

+ 2 - 0
fs-service/src/main/java/com/fs/course/domain/FsVideoResource.java

@@ -102,4 +102,6 @@ public class FsVideoResource {
 
     //火山云vid
     private String hsyVid;
+
+    private String jobId;
 }

+ 8 - 6
fs-service/src/main/java/com/fs/course/mapper/FsUserCourseVideoMapper.java

@@ -2,6 +2,7 @@ package com.fs.course.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.course.domain.FsUserCourseVideo;
+import com.fs.course.domain.FsVideoResource;
 import com.fs.course.param.CourseVideoUpdates;
 import com.fs.course.param.FsCourseListBySidebarParam;
 import com.fs.course.param.FsUserCourseVideoListUParam;
@@ -266,12 +267,13 @@ public interface FsUserCourseVideoMapper extends BaseMapper<FsUserCourseVideo> {
     @MapKey("videoId")
     Map<Long, FsUserCourseVideo> selectAllMap();
 
-    @Select("select * from fs_user_course_video where line_two is not null and job_id is null")
-    List<FsUserCourseVideo> selectVideoByHuaWei();
+    @Select("select * from fs_video_resource where line2 is not null and job_id is null")
+    List<FsVideoResource> selectVideoByHuaWei();
 
-    @Select("select * from fs_user_course_video where job_id is not null and  (vid is null or vid='')")
-    List<FsUserCourseVideo> selectVideoByJobId();
+    @Select("select * from fs_video_resource where job_id is not null and  (hsy_vid is null or hsy_vid='')")
+    List<FsVideoResource> selectVideoByJobId();
 
-    @Select("select * from fs_user_course_video where vid is not null")
-    List<FsUserCourseVideo> selectVideoByVid();
+
+    @Select("select * from fs_video_resource where hsy_vid is not null")
+    List<FsVideoResource> selectVideoByVid();
 }

+ 2 - 0
fs-service/src/main/java/com/fs/course/service/IFsUserCourseVideoService.java

@@ -243,4 +243,6 @@ public interface IFsUserCourseVideoService extends IService<FsUserCourseVideo> {
     R getVideoInfoByVid();
 
     R createRoomMiniLinkByCourse(FsCourseLinkRoomNewParam param);
+
+    void updateMediaPublishStatus(String vid);
 }

+ 78 - 46
fs-service/src/main/java/com/fs/course/service/impl/FsUserCourseVideoServiceImpl.java

@@ -87,14 +87,8 @@ import com.volcengine.helper.VodUploadProgressListener;
 import com.volcengine.model.beans.Functions;
 import com.volcengine.service.vod.IVodService;
 import com.volcengine.service.vod.model.business.VodUrlUploadURLSet;
-import com.volcengine.service.vod.model.request.VodGetMediaInfosRequest;
-import com.volcengine.service.vod.model.request.VodQueryUploadTaskInfoRequest;
-import com.volcengine.service.vod.model.request.VodUploadMediaRequest;
-import com.volcengine.service.vod.model.request.VodUrlUploadRequest;
-import com.volcengine.service.vod.model.response.VodCommitUploadInfoResponse;
-import com.volcengine.service.vod.model.response.VodGetMediaInfosResponse;
-import com.volcengine.service.vod.model.response.VodQueryUploadTaskInfoResponse;
-import com.volcengine.service.vod.model.response.VodUrlUploadResponse;
+import com.volcengine.service.vod.model.request.*;
+import com.volcengine.service.vod.model.response.*;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.CollectionUtils;
 import org.redisson.api.RLock;
@@ -4082,8 +4076,8 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 
     @Override
     public R uploadVideoToHuoShanByUrl() {
-        List <FsUserCourseVideo> videos = fsUserCourseVideoMapper.selectVideoByHuaWei();
-        for (FsUserCourseVideo video : videos){
+        List <FsVideoResource> videos = fsUserCourseVideoMapper.selectVideoByHuaWei();
+        for (FsVideoResource video : videos){
             uploadVideoByUrl(video);
         }
         return R.ok();
@@ -4093,7 +4087,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     private IVodService vodService;
 
     //通过Url上传视频
-    public void uploadVideoByUrl(FsUserCourseVideo courseVideo) {
+    public void uploadVideoByUrl(FsVideoResource videoResource) {
 
         try {
             VodUrlUploadRequest.Builder reqBuilder = VodUrlUploadRequest.newBuilder();
@@ -4101,7 +4095,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
             reqBuilder.setSpaceName(cloudHostProper.getSpaceName());
             VodUrlUploadURLSet.Builder uRLSetsBuilder = VodUrlUploadURLSet.newBuilder();
             //源文件 URL
-            uRLSetsBuilder.setSourceUrl(courseVideo.getLineTwo());//华为云
+            uRLSetsBuilder.setSourceUrl(videoResource.getLine2());//华为云
             //存储类型。默认为 1。取值如下:
             //1:标准存储。
             //2:归档存储。
@@ -4116,7 +4110,7 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
 //            String fileName = System.currentTimeMillis() + ".mp4";
 //            String remoteFileName = "fs/" + datePath + "/" + fileName;
 
-            uRLSetsBuilder.setFileName(courseVideo.getFileKey());
+            uRLSetsBuilder.setFileName(videoResource.getFileKey());
             reqBuilder.addURLSets(uRLSetsBuilder);
 
             VodUrlUploadResponse resp = vodService.uploadMediaByUrl(reqBuilder.build());
@@ -4125,11 +4119,11 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                 log.info("上传返回异常:{}",resp.getResponseMetadata().getError());
                 System.exit(-1);
             }else {
-                FsUserCourseVideo video = new FsUserCourseVideo();
-                video.setVideoId(courseVideo.getVideoId());
+                FsVideoResource video = new FsVideoResource();
+                video.setId(videoResource.getId());
                 video.setJobId(resp.getResult().getData(0).getJobId());
                 //更新JobId
-                fsUserCourseVideoMapper.updateFsUserCourseVideo(video);
+                fsVideoResourceMapper.updateById(video);
             }
             log.info("上传返回参数:{}",resp);
         } catch (Exception e) {
@@ -4140,25 +4134,25 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     @Override
     public R getVidByJob() {
         // 查询有JobId的视频
-        List<FsUserCourseVideo> list = fsUserCourseVideoMapper.selectVideoByJobId();
+        List<FsVideoResource> list = fsUserCourseVideoMapper.selectVideoByJobId();
         if (list.isEmpty()) {
             log.info("没有待上传的视频任务");
             return R.error();
         }
         // 按五百一批切割
-        List<List<FsUserCourseVideo>> batches = splitList(list, 500);
+        List<List<FsVideoResource>> batches = splitList(list, 500);
         log.info("总任务 {} 条,分成 {} 批", list.size(), batches.size());
 
         int batchIndex = 1;
 
         // 批次顺序执行,每批内部多线程并发执行
-        for (List<FsUserCourseVideo> batch : batches) {
+        for (List<FsVideoResource> batch : batches) {
 
             log.info("开始执行批次 {}/{},本批任务 {} 条", batchIndex, batches.size(), batch.size());
 
             CountDownLatch latch = new CountDownLatch(batch.size());
 
-            for (FsUserCourseVideo video : batch) {
+            for (FsVideoResource video : batch) {
                 uploadExecutor.submit(() -> {
                     try {
                         uploadSingleTaskWithRetry(video,1);
@@ -4186,25 +4180,25 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     @Override
     public R getVideoInfoByVid() {
         // 查询有JobId的视频
-        List<FsUserCourseVideo> list = fsUserCourseVideoMapper.selectVideoByVid();
+        List<FsVideoResource> list = fsUserCourseVideoMapper.selectVideoByVid();
         if (list.isEmpty()) {
             log.info("没有包含vid的视频");
             return R.error();
         }
         // 按五百一批切割
-        List<List<FsUserCourseVideo>> batches = splitList(list, 500);
+        List<List<FsVideoResource>> batches = splitList(list, 500);
         log.info("总任务 {} 条,分成 {} 批", list.size(), batches.size());
 
         int batchIndex = 1;
 
         // 批次顺序执行,每批内部多线程并发执行
-        for (List<FsUserCourseVideo> batch : batches) {
+        for (List<FsVideoResource> batch : batches) {
 
             log.info("开始执行批次 {}/{},本批任务 {} 条", batchIndex, batches.size(), batch.size());
 
             CountDownLatch latch = new CountDownLatch(batch.size());
 
-            for (FsUserCourseVideo video : batch) {
+            for (FsVideoResource video : batch) {
                 uploadExecutor.submit(() -> {
                     try {
                         uploadSingleTaskWithRetry(video,2);
@@ -4286,10 +4280,10 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     }
 
     //根据jobid查询上传视频的vid
-    public void getVidByJobId(FsUserCourseVideo courseVideo){
+    public void getVidByJobId(FsVideoResource videoResource){
         try {
             VodQueryUploadTaskInfoRequest.Builder reqBuilder = VodQueryUploadTaskInfoRequest.newBuilder();
-            reqBuilder.setJobIds(courseVideo.getJobId());
+            reqBuilder.setJobIds(videoResource.getJobId());
 
             VodQueryUploadTaskInfoResponse resp = vodService.queryUploadTaskInfo(reqBuilder.build());
             if (resp.getResponseMetadata().hasError()) {
@@ -4299,10 +4293,15 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
                 if (StringUtils.isEmpty(resp.getResult().getData().getMediaInfoList(0).getVid())){
                     return;
                 }
-                FsUserCourseVideo video = new FsUserCourseVideo();
-                video.setVideoId(courseVideo.getVideoId());
-                video.setVid(resp.getResult().getData().getMediaInfoList(0).getVid());
-                fsUserCourseVideoMapper.updateFsUserCourseVideo(video);
+                FsVideoResource video = new FsVideoResource();
+                video.setId(videoResource.getId());
+                //视频上传失败,清空jobid
+                if (StringUtils.isEmpty(resp.getResult().getData().getMediaInfoList(0).getVid())){
+                    video.setJobId("");
+                }else {
+                    video.setHsyVid(resp.getResult().getData().getMediaInfoList(0).getVid());
+                }
+                fsVideoResourceMapper.updateById(video);
             }
             System.out.println(resp);
         } catch (Exception e) {
@@ -4311,26 +4310,39 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
     }
 
 
-    public void getVideoInfoByVid(FsUserCourseVideo courseVideo) {
+    //根据vid获取视频详情
+    public void getVideoInfoByVid(FsVideoResource videoResource) {
         try {
             VodGetMediaInfosRequest.Builder reqBuilder = VodGetMediaInfosRequest.newBuilder();
-            reqBuilder.setVids(courseVideo.getVid());
+            reqBuilder.setVids(videoResource.getHsyVid());
 
             VodGetMediaInfosResponse resp = vodService.getMediaInfos20230701(reqBuilder.build());
             if (resp.getResponseMetadata().hasError()) {
                 System.out.println(resp.getResponseMetadata().getError());
                 System.exit(-1);
             }else {
-                //更新小节
-                FsUserCourseVideo video = new FsUserCourseVideo();
-                video.setVideoId(courseVideo.getVideoId());
-                String url = cloudHostProper.volcengineUrl+"/"+resp.getResult().getMediaInfoList(0).getSourceInfo().getStoreUri();
-                video.setLineTwo(url);
-                fsUserCourseVideoMapper.updateFsUserCourseVideo(video);
+                //如果路径是空的直接返回
+                if (StringUtils.isEmpty(resp.getResult().getMediaInfoList(0).getSourceInfo().getStoreUri())){
+                    return;
+                }
+                //如果是未发布状态修改发布状态
+                if (resp.getResult().getMediaInfoList(0).getBasicInfo().getPublishStatus().equals("Unpublished")){
+                    updateMediaPublishStatus(videoResource.getHsyVid());
+                }
                 //更新视频资源
-                FsVideoResource videoResource = fsVideoResourceMapper.selectByFileKey(courseVideo.getFileKey());
-                videoResource.setLine2(url);
-                fsVideoResourceMapper.updateById(videoResource);
+                String url = cloudHostProper.volcengineUrl+"/"+resp.getResult().getMediaInfoList(0).getSourceInfo().getStoreUri();
+
+                FsVideoResource fsVideoResource = new FsVideoResource();
+                fsVideoResource.setId(videoResource.getId());
+                fsVideoResource.setLine2(url);
+                fsVideoResourceMapper.updateById(fsVideoResource);
+
+                //更新小节
+                FsUserCourseVideo courseVideo = new FsUserCourseVideo();
+                courseVideo.setFileKey(videoResource.getFileKey());
+                courseVideo.setLineTwo(url);
+                fsUserCourseVideoMapper.updateFsUserCourseVideoByFileKey(courseVideo);
+
             }
             System.out.println(resp);
         } catch (Exception e) {
@@ -4338,24 +4350,44 @@ public class FsUserCourseVideoServiceImpl extends ServiceImpl<FsUserCourseVideoM
         }
     }
 
+    //修改发布状态
+    public void updateMediaPublishStatus(String vid){
+        String statusPublished = "Published";
+
+        try {
+            // publish
+            VodUpdateMediaPublishStatusRequest.Builder req = VodUpdateMediaPublishStatusRequest.newBuilder();
+            req.setVid(vid);
+            req.setStatus(statusPublished);
+
+            VodUpdateMediaPublishStatusResponse resp = vodService.updateMediaPublishStatus(req.build());
+            System.out.println(resp);
+
+            Thread.sleep(1000);
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
 
-    public void uploadSingleTaskWithRetry(FsUserCourseVideo courseVideo,Integer type) {
+    public void uploadSingleTaskWithRetry(FsVideoResource videoResource,Integer type) {
         int maxRetry = 3;
         for (int i = 1; i <= maxRetry; i++) {
             try {
                 if (type == 1){
                     //获取上传成功的视频vid,同步到数据库
-                    getVidByJobId(courseVideo);
+                    getVidByJobId(videoResource);
                 }else if (type == 2){
                     //获取视频地址同步到线路二
-                    getVideoInfoByVid(courseVideo);
+                    getVideoInfoByVid(videoResource);
                 }
                 return;
             } catch (Exception e) {
                 log.error("视频 {} 上传失败,第 {} 次重试,原因:{}",
-                        courseVideo.getVideoId(), i, e.getMessage());
+                        videoResource.getId(), i, e.getMessage());
                 if (i == maxRetry) {
-                    log.error("视频 {} 上传最终失败!", courseVideo.getVideoId());
+                    log.error("视频 {} 上传最终失败!", videoResource.getId());
                 }
             }
         }

+ 2 - 0
fs-service/src/main/java/com/fs/hisStore/param/LoginMpWxParam.java

@@ -25,4 +25,6 @@ public class LoginMpWxParam implements Serializable {
     private String userCode;
     //小程序APPID
     private String appId;
+    // 广告链路id
+    private String traceId;
 }

+ 8 - 0
fs-service/src/main/java/com/fs/newAdv/constant/AdvConfigConstant.java

@@ -0,0 +1,8 @@
+package com.fs.newAdv.constant;
+
+public class AdvConfigConstant {
+    /**
+     * 落地页回传topic
+     */
+    public static final String ADV_CONFIG = "adv-config";
+}

+ 34 - 0
fs-service/src/main/java/com/fs/newAdv/constant/ConversionTrackingMessage.java

@@ -0,0 +1,34 @@
+package com.fs.newAdv.constant;
+
+
+import com.fs.newAdv.enums.SystemEventTypeEnum;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 链化追踪回传消息实体
+ *
+ * @author zhangqin
+ */
+@Data
+public class ConversionTrackingMessage implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+    /**
+     * 链路id
+     */
+    private String traceId;
+
+    /**
+     * 事件类型
+     */
+    private SystemEventTypeEnum eventType;
+    /*
+     * value
+     */
+    private Double value;
+    private String trackId;
+}
+
+

+ 27 - 0
fs-service/src/main/java/com/fs/newAdv/constant/MqTopicConstant.java

@@ -0,0 +1,27 @@
+package com.fs.newAdv.constant;
+
+public class MqTopicConstant {
+    /**
+     * 落地页回传topic
+     */
+    public static final String CONVERSION_TOPIC = "conversion-topic";
+
+    /**
+     * 转化追踪topic
+     */
+    public static final String CONVERSION_TRACKING_TOPIC = "conversion-tracking-topic";
+
+
+
+    //---------------------consumerGroup
+
+    /**
+     * 落地页回传consumer
+     */
+    public static final String CONVERSION_TOPIC_CONSUMER_GROUP = "conversion-topic-consumer-group";
+
+    /**
+     * 转化追踪consumer
+     */
+    public static final String CONVERSION_TRACKING_TOPIC_CONSUMER_GROUP = "conversion-topic-tracking-consumer-group";
+}

+ 54 - 0
fs-service/src/main/java/com/fs/newAdv/domain/AdvChannelEntity.java

@@ -0,0 +1,54 @@
+package com.fs.newAdv.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ * 广告渠道表
+ * </p>
+ *
+ * @author zhangqin
+ * @since 2025-11-27
+ */
+@Getter
+@Setter
+@TableName("adv_channel")
+@ApiModel(value = "AdvChannelEntity对象", description = "广告渠道表")
+public class AdvChannelEntity extends Model<AdvChannelEntity> {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty("渠道名称/分组名称")
+    @TableField("channel_name")
+    private String channelName;
+
+    @ApiModelProperty("父id")
+    @TableField("parent_id")
+    private Long parentId;
+
+    @TableField(value = "create_time", strategy = FieldStrategy.NOT_NULL)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    @TableField(value = "update_time", strategy = FieldStrategy.NOT_NULL)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime updateTime;
+
+
+    @Override
+    public Serializable pkVal() {
+        return this.id;
+    }
+}

+ 44 - 0
fs-service/src/main/java/com/fs/newAdv/domain/AdvEventType.java

@@ -0,0 +1,44 @@
+package com.fs.newAdv.domain;
+
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+@TableName("adv_event_type")
+public class AdvEventType implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 事件类型
+     */
+    private String eventType;
+
+    /**
+     * 事件名称
+     */
+    private String eventName;
+
+    /**
+     * 广告商id
+     */
+    private Long advertiserId;
+    /**
+     * 广告商名称
+     */
+    private String advertiserName;
+
+    /**
+     * 是否系统内置(1是0否)
+     */
+    private String systemBuiltin;
+
+}

+ 45 - 0
fs-service/src/main/java/com/fs/newAdv/domain/AdvProjectEntity.java

@@ -0,0 +1,45 @@
+package com.fs.newAdv.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.baomidou.mybatisplus.extension.activerecord.Model;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Getter;
+import lombok.Setter;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * <p>
+ *
+ * </p>
+ *
+ * @author zhangqin
+ * @since 2025-11-27
+ */
+@Getter
+@Setter
+@TableName("adv_project")
+@ApiModel(value = "AdvProjectEntity对象", description = "")
+public class AdvProjectEntity extends Model<AdvProjectEntity> {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty("项目名称")
+    @TableField("project_name")
+    private String projectName;
+
+    @TableField(value = "create_time", strategy = FieldStrategy.NOT_NULL)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    @Override
+    public Serializable pkVal() {
+        return this.id;
+    }
+}

+ 82 - 0
fs-service/src/main/java/com/fs/newAdv/domain/Advertiser.java

@@ -0,0 +1,82 @@
+package com.fs.newAdv.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 广告商表
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Data
+@TableName("adv_advertiser")
+public class Advertiser implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 广告商ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 广告商名称
+     */
+    private String advertiserName;
+
+    /**
+     * 广告商图片
+     */
+    private String advertiserImage;
+
+    /**
+     * 是否支持API(0否 1是)
+     */
+    private Integer supportApi;
+
+    /**
+     * 是否支持回传(0否 1是)
+     */
+    private Integer supportCallback;
+
+    /**
+     * 是否启用(0否 1是)
+     */
+    private Integer enabled;
+
+    /**
+     * 1线上广告商 2自定义广告商
+     */
+    private Integer custom;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "create_time", strategy = FieldStrategy.NOT_NULL)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "update_time", strategy = FieldStrategy.NOT_NULL)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建人
+     */
+    private String creator;
+
+    /**
+     * 更新人
+     */
+    private String updater;
+}
+

+ 78 - 0
fs-service/src/main/java/com/fs/newAdv/domain/AlertLog.java

@@ -0,0 +1,78 @@
+package com.fs.newAdv.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 告警记录表
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Data
+@TableName("adv_alert_log")
+public class AlertLog implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 告警ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 告警类型(任务失败/任务超时)
+     */
+    private String alertType;
+
+    /**
+     * 告警级别(1低 2中 3高)
+     */
+    private Integer alertLevel;
+
+    /**
+     * 告警标题
+     */
+    private String alertTitle;
+
+    /**
+     * 告警内容
+     */
+    private String alertContent;
+
+    /**
+     * 关联任务ID
+     */
+    private Long relatedTaskId;
+
+    /**
+     * 是否已处理(0否 1是)
+     */
+    private Integer isHandled;
+
+    /**
+     * 处理人
+     */
+    private String handler;
+
+    /**
+     * 处理时间
+     */
+    private LocalDateTime handleTime;
+
+    /**
+     * 处理备注
+     */
+    private String handleRemark;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+}
+

+ 80 - 0
fs-service/src/main/java/com/fs/newAdv/domain/ApiCallLog.java

@@ -0,0 +1,80 @@
+package com.fs.newAdv.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * API调用日志表
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Data
+@TableName("adv_api_call_log")
+public class ApiCallLog implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 广告商ID
+     */
+    private Long advertiserId;
+
+    /**
+     * API名称
+     */
+    private String apiName;
+
+    /**
+     * API地址
+     */
+    private String apiUrl;
+
+
+
+    /**
+     * 请求参数
+     */
+    private String requestParams;
+
+    /**
+     * 响应状态码
+     */
+    private Integer responseStatus;
+
+    /**
+     * 响应内容
+     */
+    private String responseBody;
+
+    /**
+     * 调用状态(1成功 2失败)
+     */
+    private Integer callStatus;
+
+    /**
+     * 错误信息
+     */
+    private String errorMsg;
+
+    /**
+     * 耗时(ms)
+     */
+    private Long costTime;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+}
+

+ 109 - 0
fs-service/src/main/java/com/fs/newAdv/domain/CallbackAccount.java

@@ -0,0 +1,109 @@
+package com.fs.newAdv.domain;
+
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.annotation.*;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.newAdv.vo.ConversionEventVo;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * 回传账号表
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Data
+@TableName("adv_callback_account")
+public class CallbackAccount implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 回传账号ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 回传账号名称
+     */
+    private String accountName;
+    /**
+     * 广告商Id
+     */
+    private Long advertiserId;
+
+    /**
+     * 广告商名称
+     */
+    private String advertiserName;
+    /**
+     * 广告主id
+     */
+    private String adAccountId;
+    /**
+     * 回传token
+     */
+    private String accessToken;
+
+    /**
+     * 数据源id
+     */
+    private String scrId;
+
+    /**
+     * 应用程序ID
+     */
+    private String appId;
+
+    /**
+     * 应用程序Secret(加密存储)
+     */
+    private String appSecret;
+
+    /**
+     * 转换事件
+     */
+    private String conversionEvent;
+    @TableField(exist = false)
+    private List<ConversionEventVo> conversionEventVo;
+
+    /**
+     * 创建时间
+     */
+    @TableField(value = "create_time", strategy = FieldStrategy.NOT_NULL)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(value = "update_time", strategy = FieldStrategy.NOT_NULL)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建人
+     */
+    private String creator;
+
+    /**
+     * 更新人
+     */
+    private String updater;
+
+    public List<ConversionEventVo> getConversionEventVo() {
+        if (this.conversionEvent == null || this.conversionEvent.isEmpty()) {
+            return Collections.emptyList();
+        }
+        // 根据实际的数据格式进行相应的转换处理
+        // 这里假设 conversionEvent 存储的是JSON格式的数组字符串
+        return JSONUtil.toList(JSONUtil.parseArray(this.conversionEvent), ConversionEventVo.class);    }
+}
+

+ 103 - 0
fs-service/src/main/java/com/fs/newAdv/domain/CallbackLog.java

@@ -0,0 +1,103 @@
+package com.fs.newAdv.domain;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 第三方回调记录表
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Data
+@TableName("adv_callback_log")
+public class CallbackLog implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 站点ID
+     */
+    private Long siteId;
+
+    /**
+     * 广告商ID
+     */
+    private Long advertiserId;
+
+    /**
+     * 广告商名称
+     */
+    private String advertiserName;
+
+    /**
+     * 回调类型(展示/点击/转化)
+     */
+    private String callbackType;
+
+    /**
+     * 请求URL
+     */
+    private String requestUrl;
+
+    /**
+     * 请求方法
+     */
+    private String requestMethod;
+
+    /**
+     * 请求参数
+     */
+    private String requestParams;
+
+    /**
+     * 请求头
+     */
+    private String requestHeaders;
+
+    /**
+     * 响应状态码
+     */
+    private Integer responseStatus;
+
+    /**
+     * 响应内容
+     */
+    private String responseBody;
+
+    /**
+     * 处理状态(1成功 2失败)
+     */
+    private Integer processStatus;
+
+    /**
+     * 错误信息
+     */
+    private String errorMsg;
+
+    /**
+     * IP地址
+     */
+    private String ipAddress;
+
+    /**
+     * 处理耗时(ms)
+     */
+    private Long processTime;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+}
+

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff