Przeglądaj źródła

coding:投流需求整体框架提交--销售端

zhangqin 3 miesięcy temu
rodzic
commit
44ed76d3f0
100 zmienionych plików z 7793 dodań i 1 usunięć
  1. 71 0
      fs-common/src/main/java/com/fs/common/constant/RedisKeyConstant.java
  2. 57 0
      fs-common/src/main/java/com/fs/common/constant/RocketMqConstant.java
  3. 51 0
      fs-common/src/main/java/com/fs/common/constant/SystemConstant.java
  4. 51 0
      fs-common/src/main/java/com/fs/common/exception/ThirdPartyException.java
  5. 52 0
      fs-common/src/main/java/com/fs/common/exception/base/BusinessException.java
  6. 102 0
      fs-common/src/main/java/com/fs/common/result/Result.java
  7. 72 0
      fs-common/src/main/java/com/fs/common/result/ResultCode.java
  8. 120 0
      fs-common/src/main/java/com/fs/common/utils/RedisUtil.java
  9. 102 0
      fs-common/src/main/java/com/fs/common/utils/SignatureUtil.java
  10. 65 0
      fs-common/src/main/java/com/fs/common/utils/SnowflakeUtil.java
  11. 97 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/AdvertiserController.java
  12. 93 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/CallbackAccountController.java
  13. 291 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/DomainController.java
  14. 340 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/LandingPageTemplateController.java
  15. 94 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/LeadSourceController.java
  16. 110 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/PromotionAccountController.java
  17. 86 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/SiteController.java
  18. 98 0
      fs-company/src/main/java/com/fs/company/controller/newAdv/TrackingLinkController.java
  19. 2 1
      fs-company/src/main/resources/application.yml
  20. 84 0
      fs-service/src/main/java/com/fs/newAdv/entity/Advertiser.java
  21. 78 0
      fs-service/src/main/java/com/fs/newAdv/entity/AlertLog.java
  22. 88 0
      fs-service/src/main/java/com/fs/newAdv/entity/ApiCallLog.java
  23. 69 0
      fs-service/src/main/java/com/fs/newAdv/entity/CallbackAccount.java
  24. 103 0
      fs-service/src/main/java/com/fs/newAdv/entity/CallbackLog.java
  25. 124 0
      fs-service/src/main/java/com/fs/newAdv/entity/ClickTrace.java
  26. 104 0
      fs-service/src/main/java/com/fs/newAdv/entity/ConversionLog.java
  27. 60 0
      fs-service/src/main/java/com/fs/newAdv/entity/ConversionTarget.java
  28. 71 0
      fs-service/src/main/java/com/fs/newAdv/entity/Domain.java
  29. 81 0
      fs-service/src/main/java/com/fs/newAdv/entity/LandingPageTemplate.java
  30. 119 0
      fs-service/src/main/java/com/fs/newAdv/entity/Lead.java
  31. 94 0
      fs-service/src/main/java/com/fs/newAdv/entity/LeadSource.java
  32. 93 0
      fs-service/src/main/java/com/fs/newAdv/entity/OperationLog.java
  33. 119 0
      fs-service/src/main/java/com/fs/newAdv/entity/PromotionAccount.java
  34. 89 0
      fs-service/src/main/java/com/fs/newAdv/entity/ScheduleTaskLog.java
  35. 159 0
      fs-service/src/main/java/com/fs/newAdv/entity/Site.java
  36. 145 0
      fs-service/src/main/java/com/fs/newAdv/entity/SiteStatistics.java
  37. 145 0
      fs-service/src/main/java/com/fs/newAdv/entity/SiteStatisticsDaily.java
  38. 144 0
      fs-service/src/main/java/com/fs/newAdv/entity/SiteStatisticsMonthly.java
  39. 155 0
      fs-service/src/main/java/com/fs/newAdv/entity/SiteStatisticsWeekly.java
  40. 78 0
      fs-service/src/main/java/com/fs/newAdv/entity/SyncLog.java
  41. 79 0
      fs-service/src/main/java/com/fs/newAdv/entity/TrackingLink.java
  42. 39 0
      fs-service/src/main/java/com/fs/newAdv/enums/AdvertiserTypeEnum.java
  43. 35 0
      fs-service/src/main/java/com/fs/newAdv/enums/CallbackStatusEnum.java
  44. 36 0
      fs-service/src/main/java/com/fs/newAdv/enums/TaskStatusEnum.java
  45. 39 0
      fs-service/src/main/java/com/fs/newAdv/enums/TaskTypeEnum.java
  46. 59 0
      fs-service/src/main/java/com/fs/newAdv/event/ConversionEvent.java
  47. 72 0
      fs-service/src/main/java/com/fs/newAdv/event/ConversionEventListener.java
  48. 54 0
      fs-service/src/main/java/com/fs/newAdv/event/ConversionEventPublisher.java
  49. 59 0
      fs-service/src/main/java/com/fs/newAdv/integration/adapter/BaiduAdapter.java
  50. 36 0
      fs-service/src/main/java/com/fs/newAdv/integration/adapter/IAdvertiserAdapter.java
  51. 57 0
      fs-service/src/main/java/com/fs/newAdv/integration/adapter/OceanEngineAdapter.java
  52. 280 0
      fs-service/src/main/java/com/fs/newAdv/integration/client/BaiduApiClient.java
  53. 234 0
      fs-service/src/main/java/com/fs/newAdv/integration/client/OceanEngineApiClient.java
  54. 89 0
      fs-service/src/main/java/com/fs/newAdv/integration/factory/AdvertiserHandlerFactory.java
  55. 103 0
      fs-service/src/main/java/com/fs/newAdv/integration/handler/AbstractCallbackHandler.java
  56. 197 0
      fs-service/src/main/java/com/fs/newAdv/integration/handler/BaiduCallbackHandler.java
  57. 191 0
      fs-service/src/main/java/com/fs/newAdv/integration/handler/OceanEngineCallbackHandler.java
  58. 50 0
      fs-service/src/main/java/com/fs/newAdv/integration/strategy/BaiduCallbackStrategy.java
  59. 35 0
      fs-service/src/main/java/com/fs/newAdv/integration/strategy/ICallbackStrategy.java
  60. 47 0
      fs-service/src/main/java/com/fs/newAdv/integration/strategy/OceanEngineCallbackStrategy.java
  61. 17 0
      fs-service/src/main/java/com/fs/newAdv/mapper/AdvertiserMapper.java
  62. 17 0
      fs-service/src/main/java/com/fs/newAdv/mapper/AlertLogMapper.java
  63. 17 0
      fs-service/src/main/java/com/fs/newAdv/mapper/ApiCallLogMapper.java
  64. 17 0
      fs-service/src/main/java/com/fs/newAdv/mapper/CallbackAccountMapper.java
  65. 17 0
      fs-service/src/main/java/com/fs/newAdv/mapper/CallbackLogMapper.java
  66. 16 0
      fs-service/src/main/java/com/fs/newAdv/mapper/ClickTraceMapper.java
  67. 25 0
      fs-service/src/main/java/com/fs/newAdv/mapper/ConversionLogMapper.java
  68. 17 0
      fs-service/src/main/java/com/fs/newAdv/mapper/ConversionTargetMapper.java
  69. 17 0
      fs-service/src/main/java/com/fs/newAdv/mapper/DomainMapper.java
  70. 17 0
      fs-service/src/main/java/com/fs/newAdv/mapper/LandingPageTemplateMapper.java
  71. 16 0
      fs-service/src/main/java/com/fs/newAdv/mapper/LeadMapper.java
  72. 17 0
      fs-service/src/main/java/com/fs/newAdv/mapper/LeadSourceMapper.java
  73. 17 0
      fs-service/src/main/java/com/fs/newAdv/mapper/OperationLogMapper.java
  74. 25 0
      fs-service/src/main/java/com/fs/newAdv/mapper/PromotionAccountMapper.java
  75. 26 0
      fs-service/src/main/java/com/fs/newAdv/mapper/ScheduleTaskLogMapper.java
  76. 25 0
      fs-service/src/main/java/com/fs/newAdv/mapper/SiteMapper.java
  77. 17 0
      fs-service/src/main/java/com/fs/newAdv/mapper/SiteStatisticsDailyMapper.java
  78. 30 0
      fs-service/src/main/java/com/fs/newAdv/mapper/SiteStatisticsMapper.java
  79. 17 0
      fs-service/src/main/java/com/fs/newAdv/mapper/SiteStatisticsMonthlyMapper.java
  80. 17 0
      fs-service/src/main/java/com/fs/newAdv/mapper/SiteStatisticsWeeklyMapper.java
  81. 17 0
      fs-service/src/main/java/com/fs/newAdv/mapper/SyncLogMapper.java
  82. 17 0
      fs-service/src/main/java/com/fs/newAdv/mapper/TrackingLinkMapper.java
  83. 116 0
      fs-service/src/main/java/com/fs/newAdv/mq/consumer/ConversionDLQConsumer.java
  84. 204 0
      fs-service/src/main/java/com/fs/newAdv/mq/consumer/ConversionMessageConsumer.java
  85. 65 0
      fs-service/src/main/java/com/fs/newAdv/mq/message/ConversionMessage.java
  86. 148 0
      fs-service/src/main/java/com/fs/newAdv/mq/producer/ConversionMessageProducer.java
  87. 46 0
      fs-service/src/main/java/com/fs/newAdv/service/IAdvertiserService.java
  88. 46 0
      fs-service/src/main/java/com/fs/newAdv/service/ICallbackAccountService.java
  89. 33 0
      fs-service/src/main/java/com/fs/newAdv/service/ICallbackService.java
  90. 68 0
      fs-service/src/main/java/com/fs/newAdv/service/IClickTraceService.java
  91. 34 0
      fs-service/src/main/java/com/fs/newAdv/service/IConversionService.java
  92. 16 0
      fs-service/src/main/java/com/fs/newAdv/service/IDomainService.java
  93. 16 0
      fs-service/src/main/java/com/fs/newAdv/service/ILandingPageTemplateService.java
  94. 23 0
      fs-service/src/main/java/com/fs/newAdv/service/ILeadService.java
  95. 46 0
      fs-service/src/main/java/com/fs/newAdv/service/ILeadSourceService.java
  96. 66 0
      fs-service/src/main/java/com/fs/newAdv/service/IPromotionAccountService.java
  97. 102 0
      fs-service/src/main/java/com/fs/newAdv/service/impl/AdvertiserServiceImpl.java
  98. 100 0
      fs-service/src/main/java/com/fs/newAdv/service/impl/CallbackAccountServiceImpl.java
  99. 107 0
      fs-service/src/main/java/com/fs/newAdv/service/impl/CallbackServiceImpl.java
  100. 192 0
      fs-service/src/main/java/com/fs/newAdv/service/impl/ClickTraceServiceImpl.java

+ 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";
+}
+

+ 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;
+    }
+}
+

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

@@ -0,0 +1,120 @@
+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);
+    }
+}
+

+ 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();
+    }
+}
+

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

@@ -0,0 +1,97 @@
+package com.fs.company.controller.newAdv;
+
+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.entity.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.*;
+
+
+import javax.validation.constraints.NotNull;
+
+/**
+ * 广告商管理控制器
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Validated
+@RestController
+@RequestMapping("/advertiser")
+public class AdvertiserController {
+
+    @Autowired
+    private IAdvertiserService advertiserService;
+
+    /**
+     * 分页查询广告商列表
+     */
+    @GetMapping("/page")
+    public Result<IPage<Advertiser>> page(
+            @RequestParam(defaultValue = "1") Long current,
+            @RequestParam(defaultValue = "10") Long size,
+            @RequestParam(required = false) String advertiserName,
+            @RequestParam(required = false) Integer type,
+            @RequestParam(required = false) Integer enabled) {
+
+        Page<Advertiser> page = new Page<>(current, size);
+        IPage<Advertiser> result = advertiserService.page(page, advertiserName, type, enabled);
+        return Result.success(result);
+    }
+
+    /**
+     * 根据ID查询广告商详情
+     */
+    @GetMapping("/{id}")
+    public Result<Advertiser> getById(@PathVariable @NotNull(message = "广告商ID不能为空") Long id) {
+        Advertiser advertiser = advertiserService.getById(id);
+        return Result.success(advertiser);
+    }
+
+    /**
+     * 创建广告商
+     */
+    @PostMapping
+    public Result<Void> create(@RequestBody @Validated Advertiser advertiser) {
+        boolean success = advertiserService.create(advertiser);
+        return success ? Result.success() : Result.error("创建失败");
+    }
+
+    /**
+     * 更新广告商
+     */
+    @PutMapping("/{id}")
+    public Result<Void> update(
+            @PathVariable @NotNull(message = "广告商ID不能为空") Long id,
+            @RequestBody @Validated Advertiser advertiser) {
+
+        advertiser.setId(id);
+        boolean success = advertiserService.update(advertiser);
+        return success ? Result.success() : Result.error("更新失败");
+    }
+
+    /**
+     * 删除广告商
+     */
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable @NotNull(message = "广告商ID不能为空") Long id) {
+        boolean success = advertiserService.delete(id);
+        return success ? Result.success() : Result.error("删除失败");
+    }
+
+    /**
+     * 批量删除广告商
+     */
+    @DeleteMapping("/batch")
+    public Result<Void> batchDelete(@RequestBody Long[] ids) {
+        boolean success = advertiserService.batchDelete(ids);
+        return success ? Result.success() : Result.error("批量删除失败");
+    }
+}
+

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

@@ -0,0 +1,93 @@
+package com.fs.company.controller.newAdv;
+
+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.entity.CallbackAccount;
+import com.fs.newAdv.service.ICallbackAccountService;
+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("/callback-account")
+public class CallbackAccountController {
+
+    @Autowired
+    private ICallbackAccountService callbackAccountService;
+
+    /**
+     * 分页查询回传账号列表
+     */
+    @GetMapping("/page")
+    public Result<IPage<CallbackAccount>> page(
+            @RequestParam(defaultValue = "1") Long current,
+            @RequestParam(defaultValue = "10") Long size,
+            @RequestParam(required = false) String accountName) {
+
+        Page<CallbackAccount> page = new Page<>(current, size);
+        IPage<CallbackAccount> result = callbackAccountService.page(page, accountName);
+        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.create(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.update(callbackAccount);
+        return success ? Result.success() : Result.error("更新失败");
+    }
+
+    /**
+     * 删除回传账号
+     */
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable @NotNull(message = "账号ID不能为空") Long id) {
+        boolean success = callbackAccountService.delete(id);
+        return success ? Result.success() : Result.error("删除失败");
+    }
+
+    /**
+     * 批量删除回传账号
+     */
+    @DeleteMapping("/batch")
+    public Result<Void> batchDelete(@RequestBody Long[] ids) {
+        boolean success = callbackAccountService.batchDelete(ids);
+        return success ? Result.success() : Result.error("批量删除失败");
+    }
+}
+

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

@@ -0,0 +1,291 @@
+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.entity.Domain;
+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("/domains")
+@Validated
+public class DomainController {
+
+    @Autowired
+    private IDomainService domainService;
+
+    /**
+     * 分页查询域名列表
+     *
+     * @param page    当前页
+     * @param size    每页大小
+     * @param name    域名名称(模糊查询)
+     * @param domain  域名地址(模糊查询)
+     * @param status  状态
+     * @return 域名列表
+     */
+    @GetMapping("/page")
+    public Result<IPage<Domain>> page(
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "10") Integer size,
+            @RequestParam(required = false) String name,
+            @RequestParam(required = false) String domain,
+            @RequestParam(required = false) Integer status) {
+
+        log.info("分页查询域名列表 | page={}, size={}, name={}, domain={}, status={}",
+                 page, size, name, domain, status);
+
+        Page<Domain> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<Domain> queryWrapper = new LambdaQueryWrapper<>();
+
+        if (StrUtil.isNotBlank(name)) {
+            queryWrapper.like(Domain::getName, name);
+        }
+        if (StrUtil.isNotBlank(domain)) {
+            queryWrapper.like(Domain::getDomain, domain);
+        }
+        if (status != null) {
+            queryWrapper.eq(Domain::getStatus, status);
+        }
+
+        queryWrapper.orderByDesc(Domain::getCreateTime);
+
+        IPage<Domain> result = domainService.page(pageParam, queryWrapper);
+        return Result.success(result);
+    }
+
+    /**
+     * 查询所有启用的域名
+     *
+     * @return 域名列表
+     */
+    @GetMapping("/list/enabled")
+    public Result<List<Domain>> listEnabled() {
+        log.info("查询所有启用的域名");
+
+        LambdaQueryWrapper<Domain> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(Domain::getStatus, 1)
+                   .orderByDesc(Domain::getCreateTime);
+
+        List<Domain> list = domainService.list(queryWrapper);
+        return Result.success(list);
+    }
+
+    /**
+     * 根据ID查询域名详情
+     *
+     * @param id 域名ID
+     * @return 域名详情
+     */
+    @GetMapping("/{id}")
+    public Result<Domain> getById(@PathVariable Long id) {
+        log.info("查询域名详情 | id={}", id);
+
+        Domain domain = domainService.getById(id);
+        if (domain == null) {
+            return Result.error("域名不存在");
+        }
+
+        return Result.success(domain);
+    }
+
+    /**
+     * 新增域名
+     *
+     * @param request 域名信息
+     * @return 操作结果
+     */
+    @PostMapping
+    public Result<Domain> create(@RequestBody @Validated DomainCreateRequest request) {
+        log.info("新增域名 | name={}, domain={}", request.getName(), request.getDomain());
+
+        // 检查域名是否已存在
+        LambdaQueryWrapper<Domain> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(Domain::getDomain, request.getDomain());
+        long count = domainService.count(queryWrapper);
+        if (count > 0) {
+            return Result.error("域名已存在");
+        }
+
+        Domain domain = new Domain();
+        domain.setName(request.getName());
+        domain.setDomain(request.getDomain());
+        domain.setStatus(request.getStatus() != null ? request.getStatus() : 1);
+        domain.setRemark(request.getRemark());
+
+        boolean success = domainService.save(domain);
+        if (!success) {
+            return Result.error("新增域名失败");
+        }
+
+        log.info("新增域名成功 | id={}", domain.getId());
+        return Result.success(domain);
+    }
+
+    /**
+     * 更新域名
+     *
+     * @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());
+
+        Domain domain = domainService.getById(id);
+        if (domain == null) {
+            return Result.error("域名不存在");
+        }
+
+        // 如果修改了域名地址,检查新域名是否已被其他记录使用
+        if (StrUtil.isNotBlank(request.getDomain()) && !request.getDomain().equals(domain.getDomain())) {
+            LambdaQueryWrapper<Domain> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.eq(Domain::getDomain, request.getDomain())
+                       .ne(Domain::getId, id);
+            long count = domainService.count(queryWrapper);
+            if (count > 0) {
+                return Result.error("域名已存在");
+            }
+        }
+
+        domain.setName(request.getName());
+        domain.setDomain(request.getDomain());
+        domain.setStatus(request.getStatus());
+        domain.setRemark(request.getRemark());
+
+        boolean success = domainService.updateById(domain);
+        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);
+
+        Domain domain = domainService.getById(id);
+        if (domain == 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);
+
+        Domain domain = domainService.getById(id);
+        if (domain == null) {
+            return Result.error("域名不存在");
+        }
+
+        domain.setStatus(status);
+        boolean success = domainService.updateById(domain);
+        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;
+    }
+}
+

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

@@ -0,0 +1,340 @@
+package com.fs.company.controller.newAdv;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+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.entity.LandingPageTemplate;
+import com.fs.newAdv.service.ILandingPageTemplateService;
+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("/landing-page-templates")
+@Validated
+public class LandingPageTemplateController {
+
+    @Autowired
+    private ILandingPageTemplateService templateService;
+
+    /**
+     * 分页查询模板列表
+     *
+     * @param page         当前页
+     * @param size         每页大小
+     * @param templateName 模板名称(模糊查询)
+     * @param templateType 模板类型
+     * @param domainId     关联域名ID
+     * @param status       状态
+     * @return 模板列表
+     */
+    @GetMapping("/page")
+    public Result<IPage<LandingPageTemplate>> page(
+            @RequestParam(defaultValue = "1") Integer page,
+            @RequestParam(defaultValue = "10") Integer size,
+            @RequestParam(required = false) String templateName,
+            @RequestParam(required = false) String templateType,
+            @RequestParam(required = false) Long domainId,
+            @RequestParam(required = false) Integer status) {
+
+        log.info("分页查询模板列表 | page={}, size={}, templateName={}, templateType={}, domainId={}, status={}",
+                 page, size, templateName, templateType, domainId, status);
+
+        Page<LandingPageTemplate> pageParam = new Page<>(page, size);
+        LambdaQueryWrapper<LandingPageTemplate> queryWrapper = new LambdaQueryWrapper<>();
+
+        if (StrUtil.isNotBlank(templateName)) {
+            queryWrapper.like(LandingPageTemplate::getTemplateName, templateName);
+        }
+        if (StrUtil.isNotBlank(templateType)) {
+            queryWrapper.eq(LandingPageTemplate::getTemplateType, templateType);
+        }
+        if (domainId != null) {
+            queryWrapper.eq(LandingPageTemplate::getDomainId, domainId);
+        }
+        if (status != null) {
+            queryWrapper.eq(LandingPageTemplate::getStatus, status);
+        }
+
+        queryWrapper.orderByDesc(LandingPageTemplate::getCreateTime);
+
+        IPage<LandingPageTemplate> result = templateService.page(pageParam, queryWrapper);
+        return Result.success(result);
+    }
+
+    /**
+     * 查询所有启用的模板
+     *
+     * @return 模板列表
+     */
+    @GetMapping("/list/enabled")
+    public Result<List<LandingPageTemplate>> listEnabled() {
+        log.info("查询所有启用的模板");
+
+        LambdaQueryWrapper<LandingPageTemplate> queryWrapper = new LambdaQueryWrapper<>();
+        queryWrapper.eq(LandingPageTemplate::getStatus, 1)
+                   .orderByDesc(LandingPageTemplate::getCreateTime);
+
+        List<LandingPageTemplate> list = templateService.list(queryWrapper);
+        return Result.success(list);
+    }
+
+    /**
+     * 根据ID查询模板详情
+     *
+     * @param id 模板ID
+     * @return 模板详情
+     */
+    @GetMapping("/{id}")
+    public Result<LandingPageTemplate> getById(@PathVariable String id) {
+        log.info("查询模板详情 | id={}", id);
+
+        LandingPageTemplate template = templateService.getById(id);
+        if (template == null) {
+            return Result.error("模板不存在");
+        }
+
+        return Result.success(template);
+    }
+
+    /**
+     * 新增模板
+     *
+     * @param request 模板信息
+     * @return 操作结果
+     */
+    @PostMapping
+    public Result<LandingPageTemplate> create(@RequestBody @Validated TemplateCreateRequest request) {
+        log.info("新增模板 | templateName={}, templateType={}",
+                 request.getTemplateName(), request.getTemplateType());
+
+        // 验证JSON格式
+        if (StrUtil.isNotBlank(request.getTemplateData())) {
+            try {
+                JSONUtil.parseObj(request.getTemplateData());
+            } catch (Exception e) {
+                return Result.error("模板数据格式不正确,必须是有效的JSON");
+            }
+        }
+
+        LandingPageTemplate template = new LandingPageTemplate();
+        template.setTemplateName(request.getTemplateName());
+        template.setTemplateData(request.getTemplateData());
+        template.setTemplateType(request.getTemplateType() != null ? request.getTemplateType() : "DEFAULT");
+        template.setDomainId(request.getDomainId());
+        template.setStatus(request.getStatus() != null ? request.getStatus() : 1);
+        template.setRemark(request.getRemark());
+
+        boolean success = templateService.save(template);
+        if (!success) {
+            return Result.error("新增模板失败");
+        }
+
+        log.info("新增模板成功 | id={}", template.getId());
+        return Result.success(template);
+    }
+
+    /**
+     * 更新模板
+     *
+     * @param id      模板ID
+     * @param request 模板信息
+     * @return 操作结果
+     */
+    @PutMapping("/{id}")
+    public Result<String> update(@PathVariable String id,
+                                  @RequestBody @Validated TemplateUpdateRequest request) {
+        log.info("更新模板 | id={}, templateName={}", id, request.getTemplateName());
+
+        LandingPageTemplate template = templateService.getById(id);
+        if (template == null) {
+            return Result.error("模板不存在");
+        }
+
+        // 验证JSON格式
+        if (StrUtil.isNotBlank(request.getTemplateData())) {
+            try {
+                JSONUtil.parseObj(request.getTemplateData());
+            } catch (Exception e) {
+                return Result.error("模板数据格式不正确,必须是有效的JSON");
+            }
+        }
+
+        template.setTemplateName(request.getTemplateName());
+        template.setTemplateData(request.getTemplateData());
+        template.setTemplateType(request.getTemplateType());
+        template.setDomainId(request.getDomainId());
+        template.setStatus(request.getStatus());
+        template.setRemark(request.getRemark());
+
+        boolean success = templateService.updateById(template);
+        if (!success) {
+            return Result.error("更新模板失败");
+        }
+
+        log.info("更新模板成功 | id={}", id);
+        return Result.success("更新成功");
+    }
+
+    /**
+     * 删除模板
+     *
+     * @param id 模板ID
+     * @return 操作结果
+     */
+    @DeleteMapping("/{id}")
+    public Result<String> delete(@PathVariable String id) {
+        log.info("删除模板 | id={}", id);
+
+        LandingPageTemplate template = templateService.getById(id);
+        if (template == null) {
+            return Result.error("模板不存在");
+        }
+
+        boolean success = templateService.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<String> ids) {
+        log.info("批量删除模板 | ids={}", ids);
+
+        if (ids == null || ids.isEmpty()) {
+            return Result.error("请选择要删除的模板");
+        }
+
+        boolean success = templateService.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 String id, @RequestParam Integer status) {
+        log.info("修改模板状态 | id={}, status={}", id, status);
+
+        LandingPageTemplate template = templateService.getById(id);
+        if (template == null) {
+            return Result.error("模板不存在");
+        }
+
+        template.setStatus(status);
+        boolean success = templateService.updateById(template);
+        if (!success) {
+            return Result.error("修改状态失败");
+        }
+
+        log.info("修改模板状态成功 | id={}, status={}", id, status);
+        return Result.success("修改成功");
+    }
+
+    /**
+     * 复制模板
+     *
+     * @param id 源模板ID
+     * @return 新模板
+     */
+    @PostMapping("/{id}/copy")
+    public Result<LandingPageTemplate> copyTemplate(@PathVariable String id) {
+        log.info("复制模板 | id={}", id);
+
+        LandingPageTemplate sourceTemplate = templateService.getById(id);
+        if (sourceTemplate == null) {
+            return Result.error("源模板不存在");
+        }
+
+        LandingPageTemplate newTemplate = new LandingPageTemplate();
+        newTemplate.setTemplateName(sourceTemplate.getTemplateName() + "_副本");
+        newTemplate.setTemplateData(sourceTemplate.getTemplateData());
+        newTemplate.setTemplateType(sourceTemplate.getTemplateType());
+        newTemplate.setDomainId(sourceTemplate.getDomainId());
+        newTemplate.setStatus(0); // 默认禁用
+        newTemplate.setRemark(sourceTemplate.getRemark());
+
+        boolean success = templateService.save(newTemplate);
+        if (!success) {
+            return Result.error("复制模板失败");
+        }
+
+        log.info("复制模板成功 | sourceId={}, newId={}", id, newTemplate.getId());
+        return Result.success(newTemplate);
+    }
+
+    /**
+     * 模板创建请求DTO
+     */
+    @Data
+    public static class TemplateCreateRequest {
+        @NotBlank(message = "模板名称不能为空")
+        private String templateName;
+
+        private String templateData;
+
+        private String templateType;
+
+        private Long domainId;
+
+        private Integer status;
+
+        private String remark;
+    }
+
+    /**
+     * 模板更新请求DTO
+     */
+    @Data
+    public static class TemplateUpdateRequest {
+        @NotBlank(message = "模板名称不能为空")
+        private String templateName;
+
+        private String templateData;
+
+        @NotBlank(message = "模板类型不能为空")
+        private String templateType;
+
+        private Long domainId;
+
+        @NotNull(message = "状态不能为空")
+        private Integer status;
+
+        private String remark;
+    }
+}
+

+ 94 - 0
fs-company/src/main/java/com/fs/company/controller/newAdv/LeadSourceController.java

@@ -0,0 +1,94 @@
+package com.fs.company.controller.newAdv;
+
+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.entity.LeadSource;
+import com.fs.newAdv.service.ILeadSourceService;
+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("/lead-source")
+public class LeadSourceController {
+
+    @Autowired
+    private ILeadSourceService leadSourceService;
+
+    /**
+     * 分页查询线索来源列表
+     */
+    @GetMapping("/page")
+    public Result<IPage<LeadSource>> page(
+            @RequestParam(defaultValue = "1") Long current,
+            @RequestParam(defaultValue = "10") Long size,
+            @RequestParam(required = false) String sourceName,
+            @RequestParam(required = false) Integer distributeType) {
+
+        Page<LeadSource> page = new Page<>(current, size);
+        IPage<LeadSource> result = leadSourceService.page(page, sourceName, distributeType);
+        return Result.success(result);
+    }
+
+    /**
+     * 根据ID查询线索来源详情
+     */
+    @GetMapping("/{id}")
+    public Result<LeadSource> getById(@PathVariable @NotNull(message = "来源ID不能为空") Long id) {
+        LeadSource leadSource = leadSourceService.getById(id);
+        return Result.success(leadSource);
+    }
+
+    /**
+     * 创建线索来源
+     */
+    @PostMapping
+    public Result<Void> create(@RequestBody @Validated LeadSource leadSource) {
+        boolean success = leadSourceService.create(leadSource);
+        return success ? Result.success() : Result.error("创建失败");
+    }
+
+    /**
+     * 更新线索来源
+     */
+    @PutMapping("/{id}")
+    public Result<Void> update(
+            @PathVariable @NotNull(message = "来源ID不能为空") Long id,
+            @RequestBody @Validated LeadSource leadSource) {
+
+        leadSource.setId(id);
+        boolean success = leadSourceService.update(leadSource);
+        return success ? Result.success() : Result.error("更新失败");
+    }
+
+    /**
+     * 删除线索来源
+     */
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable @NotNull(message = "来源ID不能为空") Long id) {
+        boolean success = leadSourceService.delete(id);
+        return success ? Result.success() : Result.error("删除失败");
+    }
+
+    /**
+     * 批量删除线索来源
+     */
+    @DeleteMapping("/batch")
+    public Result<Void> batchDelete(@RequestBody Long[] ids) {
+        boolean success = leadSourceService.batchDelete(ids);
+        return success ? Result.success() : Result.error("批量删除失败");
+    }
+}
+

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

@@ -0,0 +1,110 @@
+package com.fs.company.controller.newAdv;
+
+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.entity.PromotionAccount;
+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("/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) Long advertiserId) {
+
+        Page<PromotionAccount> page = new Page<>(current, size);
+        IPage<PromotionAccount> result = promotionAccountService.page(page, accountName, advertiserId);
+        return Result.success(result);
+    }
+
+    /**
+     * 根据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.create(account);
+        return success ? Result.success() : Result.error("创建失败");
+    }
+
+    /**
+     * 更新推广账号
+     *
+     * @param id 账号ID
+     * @param account 推广账号信息
+     */
+    @PutMapping("/{id}")
+    public Result<Void> update(
+            @PathVariable @NotNull(message = "账号ID不能为空") Long id,
+            @RequestBody @Validated PromotionAccount account) {
+
+        account.setId(id);
+        boolean success = promotionAccountService.update(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.delete(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("批量删除失败");
+    }
+}
+

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

@@ -0,0 +1,86 @@
+package com.fs.company.controller.newAdv;
+
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.fs.common.result.Result;
+import com.fs.newAdv.entity.Site;
+import com.fs.newAdv.entity.SiteStatistics;
+import com.fs.newAdv.mapper.SiteMapper;
+import com.fs.newAdv.mapper.SiteStatisticsMapper;
+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("/site")
+public class SiteController {
+
+    @Autowired
+    private SiteMapper siteMapper;
+
+    @Autowired
+    private SiteStatisticsMapper statisticsMapper;
+
+    @GetMapping("/list")
+    public Result<List<Site>> list() {
+        List<Site> list = siteMapper.selectList(new QueryWrapper<>());
+        return Result.success(list);
+    }
+
+    /**
+     * 查询站点详情
+     */
+    @GetMapping("/{id}")
+    public Result<Site> getById(@PathVariable Long id) {
+        Site site = siteMapper.selectById(id);
+        return Result.success(site);
+    }
+
+    /**
+     * 创建站点
+     */
+    @PostMapping
+    public Result<Void> create(@RequestBody Site site) {
+        siteMapper.insert(site);
+        return Result.success();
+    }
+
+    /**
+     * 更新站点
+     */
+    @PutMapping("/{id}")
+    public Result<Void> update(@PathVariable Long id, @RequestBody Site site) {
+        site.setId(id);
+        siteMapper.updateById(site);
+        return Result.success();
+    }
+
+    /**
+     * 删除站点
+     */
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable Long id) {
+        siteMapper.deleteById(id);
+        return Result.success();
+    }
+
+    /**
+     * 查询站点统计数据
+     */
+    @GetMapping("/{id}/statistics")
+    public Result<SiteStatistics> getStatistics(@PathVariable Long id) {
+        QueryWrapper<SiteStatistics> wrapper = new QueryWrapper<>();
+        wrapper.eq("site_id", id);
+        SiteStatistics statistics = statisticsMapper.selectOne(wrapper);
+        return Result.success(statistics);
+    }
+}
+

+ 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.entity.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("/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);
+    }
+}
+

+ 2 - 1
fs-company/src/main/resources/application.yml

@@ -3,7 +3,7 @@ server:
 # Spring配置
 spring:
   profiles:
-    active: druid-jnsyj-test
+#    active: druid-jnsyj-test
 #    active: druid-jnmy-test
 #    active: druid-jzzx-test
 #    active: druid-hdt
@@ -13,3 +13,4 @@ spring:
 #    active: druid-sft
 #    active: dev-jnlzjk
 #    active: dev-yjb
+    active: dev

+ 84 - 0
fs-service/src/main/java/com/fs/newAdv/entity/Advertiser.java

@@ -0,0 +1,84 @@
+package com.fs.newAdv.entity;
+
+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_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;
+
+    /**
+     * 类型(1百度 2巨量引擎 3新浪 4广点通)
+     */
+    private Integer type;
+
+    /**
+     * 是否支持API(0否 1是)
+     */
+    private Integer supportApi;
+
+    /**
+     * 是否支持回传(0否 1是)
+     */
+    private Integer supportCallback;
+
+    /**
+     * 是否启用(0否 1是)
+     */
+    private Integer enabled;
+
+    /**
+     * 是否自定义(0否 1是)
+     */
+    private Integer custom;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建人
+     */
+    private String creator;
+
+    /**
+     * 更新人
+     */
+    private String updater;
+}
+

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

@@ -0,0 +1,78 @@
+package com.fs.newAdv.entity;
+
+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;
+}
+

+ 88 - 0
fs-service/src/main/java/com/fs/newAdv/entity/ApiCallLog.java

@@ -0,0 +1,88 @@
+package com.fs.newAdv.entity;
+
+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;
+
+    /**
+     * 广告商名称
+     */
+    private String advertiserName;
+
+    /**
+     * API名称
+     */
+    private String apiName;
+
+    /**
+     * API地址
+     */
+    private String apiUrl;
+
+    /**
+     * 请求方法
+     */
+    private String requestMethod;
+
+    /**
+     * 请求参数
+     */
+    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;
+}
+

+ 69 - 0
fs-service/src/main/java/com/fs/newAdv/entity/CallbackAccount.java

@@ -0,0 +1,69 @@
+package com.fs.newAdv.entity;
+
+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_account")
+public class CallbackAccount implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 回传账号ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 回传账号名称
+     */
+    private String callbackAccountName;
+
+    /**
+     * 广告商转化类型
+     */
+    private String advertiserConversionType;
+
+    /**
+     * 回传数据类型
+     */
+    private String callbackDataType;
+
+    /**
+     * 着陆页URL
+     */
+    private String landingPageUrl;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建人
+     */
+    private String creator;
+
+    /**
+     * 更新人
+     */
+    private String updater;
+}
+

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

@@ -0,0 +1,103 @@
+package com.fs.newAdv.entity;
+
+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;
+}
+

+ 124 - 0
fs-service/src/main/java/com/fs/newAdv/entity/ClickTrace.java

@@ -0,0 +1,124 @@
+package com.fs.newAdv.entity;
+
+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;
+import java.time.LocalDateTime;
+
+/**
+ * 点击追踪表实体类
+ * 用于记录用户通过广告平台访问落地页时携带的参数
+ *
+ * @author zhangqin
+ * @date 2025-11-04
+ */
+@Data
+@TableName("adv_click_trace")
+public class ClickTrace implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(type = IdType.INPUT)
+    private Long id;
+
+    /**
+     * 追踪ID(唯一标识)
+     */
+    private String traceId;
+
+    /**
+     * 站点ID
+     */
+    private Long siteId;
+
+    /**
+     * 广告商ID
+     */
+    private Long advertiserId;
+
+    /**
+     * 广告商名称
+     */
+    private String advertiserName;
+
+    /**
+     * 点击ID(百度:clk_id/logid,巨量:callback,广点通:gdt_vid)
+     */
+    private String clickId;
+
+    /**
+     * 广告计划ID
+     */
+    private String campaignId;
+
+    /**
+     * 创意ID
+     */
+    private String creativeId;
+
+    /**
+     * 关键词
+     */
+    private String keyword;
+
+    /**
+     * 来源平台(baidu/oceanengine/sina/gdt)
+     */
+    private String source;
+
+    /**
+     * 原始参数JSON
+     */
+    private String rawParams;
+
+    /**
+     * 访客ID(Cookie/设备ID)
+     */
+    private String visitorId;
+
+    /**
+     * 用户ID(注册后关联)
+     */
+    private Long userId;
+
+    /**
+     * IP地址
+     */
+    private String ip;
+
+    /**
+     * User-Agent
+     */
+    private String userAgent;
+
+    /**
+     * 是否已转化:0=未转化,1=已转化
+     */
+    private Integer isConverted;
+
+    /**
+     * 转化时间
+     */
+    private LocalDateTime conversionTime;
+
+    /**
+     * 访问时间
+     */
+    private LocalDateTime visitTime;
+
+    /**
+     * 创建时间
+     */
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    private LocalDateTime updateTime;
+}

+ 104 - 0
fs-service/src/main/java/com/fs/newAdv/entity/ConversionLog.java

@@ -0,0 +1,104 @@
+package com.fs.newAdv.entity;
+
+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_conversion_log")
+public class ConversionLog 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 conversionType;
+
+    /**
+     * 转化事件
+     */
+    private String conversionEvent;
+
+    /**
+     * 回传URL
+     */
+    private String callbackUrl;
+
+    /**
+     * 回传参数
+     */
+    private String callbackParams;
+
+    /**
+     * 回传状态(0待回传 1成功 2失败)
+     */
+    private Integer callbackStatus;
+
+    /**
+     * 重试次数
+     */
+    private Integer retryCount;
+
+    /**
+     * 成功时间
+     */
+    private LocalDateTime successTime;
+
+    /**
+     * 错误信息
+     */
+    private String errorMsg;
+
+    /**
+     * 用户ID
+     */
+    private Long userId;
+
+    /**
+     * 点击ID(广告平台提供)
+     */
+    private String clickId;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+}
+

+ 60 - 0
fs-service/src/main/java/com/fs/newAdv/entity/ConversionTarget.java

@@ -0,0 +1,60 @@
+package com.fs.newAdv.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 转化目标表
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Data
+@TableName("adv_conversion_target")
+public class ConversionTarget implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 转化目标ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 回传事件
+     */
+    private String callbackEvent;
+
+    /**
+     * 转化类型
+     */
+    private String conversionType;
+
+    /**
+     * 回传比例(%)
+     */
+    private BigDecimal callbackRatio;
+
+    /**
+     * 站点ID
+     */
+    private Long siteId;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+}
+

+ 71 - 0
fs-service/src/main/java/com/fs/newAdv/entity/Domain.java

@@ -0,0 +1,71 @@
+package com.fs.newAdv.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 域名管理实体类
+ *
+ * @author zhangqin
+ * @date 2025-11-06
+ */
+@Data
+@TableName("adv_domain")
+public class Domain implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 域名名称
+     */
+    private String name;
+
+    /**
+     * 域名地址
+     */
+    private String domain;
+
+    /**
+     * 状态(0禁用 1启用)
+     */
+    private Integer status;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建人
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private String creator;
+
+    /**
+     * 更新人
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private String updater;
+}
+

+ 81 - 0
fs-service/src/main/java/com/fs/newAdv/entity/LandingPageTemplate.java

@@ -0,0 +1,81 @@
+package com.fs.newAdv.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 投放页面模板实体类
+ *
+ * @author zhangqin
+ * @date 2025-11-06
+ */
+@Data
+@TableName("adv_landing_page_template")
+public class LandingPageTemplate implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键ID(UUID)
+     */
+    @TableId(value = "id", type = IdType.UUID)
+    private String id;
+
+    /**
+     * 模板名称
+     */
+    private String templateName;
+
+    /**
+     * 模板数据(JSON格式)
+     */
+    private String templateData;
+
+    /**
+     * 模板类型
+     */
+    private String templateType;
+
+    /**
+     * 关联域名ID
+     */
+    private Long domainId;
+
+    /**
+     * 状态(0禁用 1启用)
+     */
+    private Integer status;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建人
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private String creator;
+
+    /**
+     * 更新人
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private String updater;
+}
+

+ 119 - 0
fs-service/src/main/java/com/fs/newAdv/entity/Lead.java

@@ -0,0 +1,119 @@
+package com.fs.newAdv.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.time.LocalDateTime;
+
+/**
+ * 线索表
+ *
+ * @author zhangqin
+ * @date 2025-11-05
+ */
+@Data
+@TableName("adv_lead")
+public class Lead implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 姓名
+     */
+    private String name;
+
+    /**
+     * 手机号
+     */
+    private String phone;
+
+    /**
+     * 公司名称
+     */
+    private String company;
+
+    /**
+     * 邮箱
+     */
+    private String email;
+
+    /**
+     * 站点ID
+     */
+    private Long siteId;
+
+    /**
+     * 点击ID(广告平台提供)
+     */
+    private String clickId;
+
+    /**
+     * 来源平台(BAIDU, OCEANENGINE, SINA, GDT)
+     */
+    private String source;
+
+    /**
+     * 广告计划ID
+     */
+    private String campaignId;
+
+    /**
+     * 关键词
+     */
+    private String keyword;
+
+    /**
+     * 创意ID
+     */
+    private String creativeId;
+
+    /**
+     * 原始参数JSON
+     */
+    private String rawParams;
+
+    /**
+     * 访客ID
+     */
+    private String visitorId;
+
+    /**
+     * 客户端IP
+     */
+    private String clientIp;
+
+    /**
+     * User-Agent
+     */
+    private String userAgent;
+
+    /**
+     * 状态:0=新线索,1=已联系,2=已转化,3=无效
+     */
+    private Integer status;
+
+    /**
+     * 备注
+     */
+    private String remark;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+}
+

+ 94 - 0
fs-service/src/main/java/com/fs/newAdv/entity/LeadSource.java

@@ -0,0 +1,94 @@
+package com.fs.newAdv.entity;
+
+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_lead_source")
+public class LeadSource implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 来源ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 来源名称
+     */
+    private String sourceName;
+
+    /**
+     * 负责机构
+     */
+    private String responsibleOrg;
+
+    /**
+     * 负责机构ID
+     */
+    private Long responsibleOrgId;
+
+    /**
+     * 负责人
+     */
+    private String responsiblePerson;
+
+    /**
+     * 负责人ID
+     */
+    private Long responsiblePersonId;
+
+    /**
+     * 分配方式(1自动 2手动)
+     */
+    private Integer distributeType;
+
+    /**
+     * 名片创建开始时间
+     */
+    private LocalDateTime cardCreateStartTime;
+
+    /**
+     * 名片创建结束时间
+     */
+    private LocalDateTime cardCreateEndTime;
+
+    /**
+     * 来源选项(1显示 2隐藏)
+     */
+    private Integer sourceOption;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建人
+     */
+    private String creator;
+
+    /**
+     * 更新人
+     */
+    private String updater;
+}
+

+ 93 - 0
fs-service/src/main/java/com/fs/newAdv/entity/OperationLog.java

@@ -0,0 +1,93 @@
+package com.fs.newAdv.entity;
+
+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_operation_log")
+public class OperationLog implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 模块名称
+     */
+    private String moduleName;
+
+    /**
+     * 操作类型(增/删/改/查)
+     */
+    private String operationType;
+
+    /**
+     * 操作描述
+     */
+    private String operationDesc;
+
+    /**
+     * 请求方法
+     */
+    private String requestMethod;
+
+    /**
+     * 请求URL
+     */
+    private String requestUrl;
+
+    /**
+     * 请求参数
+     */
+    private String requestParams;
+
+    /**
+     * 响应结果
+     */
+    private String responseResult;
+
+    /**
+     * 操作人ID
+     */
+    private Long operatorId;
+
+    /**
+     * 操作人姓名
+     */
+    private String operatorName;
+
+    /**
+     * IP地址
+     */
+    private String ipAddress;
+
+    /**
+     * 用户代理
+     */
+    private String userAgent;
+
+    /**
+     * 耗时(ms)
+     */
+    private Long costTime;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+}
+

+ 119 - 0
fs-service/src/main/java/com/fs/newAdv/entity/PromotionAccount.java

@@ -0,0 +1,119 @@
+package com.fs.newAdv.entity;
+
+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_promotion_account")
+public class PromotionAccount implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 推广账户ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 推广账户名称
+     */
+    private String accountName;
+
+    /**
+     * 推广方式
+     */
+    private String promotionType;
+
+    /**
+     * 账户简称
+     */
+    private String accountShortName;
+
+    /**
+     * 广告商名称
+     */
+    private String advertiserName;
+
+    /**
+     * 广告商ID
+     */
+    private Long advertiserId;
+
+    /**
+     * 配置方式(1服务模式 2广告主模式)
+     */
+    private Integer configMode;
+
+    /**
+     * 负责人名称
+     */
+    private String managerName;
+
+    /**
+     * 负责人ID
+     */
+    private Long managerId;
+
+    /**
+     * API读取账号开关(1开 2关)
+     */
+    private Integer apiSwitch;
+
+    /**
+     * 推广账户密码(加密存储)
+     */
+    private String accountPassword;
+
+    /**
+     * 广告主ID
+     */
+    private String adAccountId;
+
+    /**
+     * 应用程序ID
+     */
+    private String appId;
+
+    /**
+     * 应用程序Secret(加密存储)
+     */
+    private String appSecret;
+
+    /**
+     * 回调地址
+     */
+    private String callbackUrl;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建人
+     */
+    private String creator;
+
+    /**
+     * 更新人
+     */
+    private String updater;
+}
+

+ 89 - 0
fs-service/src/main/java/com/fs/newAdv/entity/ScheduleTaskLog.java

@@ -0,0 +1,89 @@
+package com.fs.newAdv.entity;
+
+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_schedule_task_log")
+public class ScheduleTaskLog implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 任务执行ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 任务类型(DATA_SYNC/DAILY_ARCHIVE/WEEKLY_ARCHIVE等)
+     */
+    private String taskType;
+
+    /**
+     * 执行批次号(如:2025-11-03-daily)
+     */
+    private String batchNo;
+
+    /**
+     * 执行状态(0待执行 1执行中 2执行成功 3执行失败)
+     */
+    private Integer executeStatus;
+
+    /**
+     * 执行开始时间
+     */
+    private LocalDateTime startTime;
+
+    /**
+     * 执行结束时间
+     */
+    private LocalDateTime endTime;
+
+    /**
+     * 执行实例IP
+     */
+    private String executeInstanceIp;
+
+    /**
+     * 处理数据量
+     */
+    private Long processDataCount;
+
+    /**
+     * 失败原因
+     */
+    private String errorMsg;
+
+    /**
+     * 重试次数
+     */
+    private Integer retryCount;
+
+    /**
+     * 执行进度(JSON格式)
+     */
+    private String executeProgress;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+}
+

+ 159 - 0
fs-service/src/main/java/com/fs/newAdv/entity/Site.java

@@ -0,0 +1,159 @@
+package com.fs.newAdv.entity;
+
+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_site")
+public class Site implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 站点ID
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 站点名称
+     */
+    private String siteName;
+
+    /**
+     * 投放类型
+     */
+    private String launchType;
+
+    /**
+     * 运营部门
+     */
+    private String operationDept;
+
+    /**
+     * 负责人
+     */
+    private String manager;
+
+    /**
+     * 广告类型
+     */
+    private String adType;
+
+    /**
+     * 广告商名称
+     */
+    private String advertiserName;
+
+    /**
+     * 广告商ID
+     */
+    private Long advertiserId;
+
+    /**
+     * 推广账户名称
+     */
+    private String promotionAccountName;
+
+    /**
+     * 推广账户ID
+     */
+    private Long promotionAccountId;
+
+    /**
+     * 投放页面名称
+     */
+    private String launchPageName;
+
+    /**
+     * 投放页面ID
+     */
+    private Long launchPageId;
+
+    /**
+     * 分配方式
+     */
+    private Integer distributeType;
+
+    /**
+     * 分配规则
+     */
+    private String distributeRule;
+
+    /**
+     * 来源名称
+     */
+    private String sourceName;
+
+    /**
+     * 来源ID
+     */
+    private Long sourceId;
+
+    /**
+     * 项目名称
+     */
+    private String projectName;
+
+    /**
+     * 项目ID
+     */
+    private Long projectId;
+
+    /**
+     * 投放域名
+     */
+    private String launchDomain;
+
+    /**
+     * 是否配置回传(0否 1是)
+     */
+    private Integer configCallback;
+
+    /**
+     * 回传账号ID
+     */
+    private Long callbackAccountId;
+
+    /**
+     * 回传账号名称
+     */
+    private String callbackAccountName;
+
+    /**
+     * 转化目标ID
+     */
+    private Long conversionTargetId;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建人
+     */
+    private String creator;
+
+    /**
+     * 更新人
+     */
+    private String updater;
+}
+

+ 145 - 0
fs-service/src/main/java/com/fs/newAdv/entity/SiteStatistics.java

@@ -0,0 +1,145 @@
+package com.fs.newAdv.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 站点统计表
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Data
+@TableName("adv_site_statistics")
+public class SiteStatistics implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 站点ID
+     */
+    private Long siteId;
+
+    /**
+     * ROI
+     */
+    private BigDecimal roi;
+
+    /**
+     * 销售额
+     */
+    private BigDecimal salesAmount;
+
+    /**
+     * PV
+     */
+    private Long pv;
+
+    /**
+     * UV
+     */
+    private Long uv;
+
+    /**
+     * 展示数
+     */
+    private Long impressionCount;
+
+    /**
+     * 点击数
+     */
+    private Long clickCount;
+
+    /**
+     * 点击率(%)
+     */
+    private BigDecimal clickRate;
+
+    /**
+     * 平均点击单价
+     */
+    private BigDecimal avgClickPrice;
+
+    /**
+     * 账面花费
+     */
+    private BigDecimal accountCost;
+
+    /**
+     * 实际花费
+     */
+    private BigDecimal actualCost;
+
+    /**
+     * 名片数
+     */
+    private Long cardCount;
+
+    /**
+     * 名片获取率(%)
+     */
+    private BigDecimal cardAcquireRate;
+
+    /**
+     * 名片获取成本
+     */
+    private BigDecimal cardAcquireCost;
+
+    /**
+     * 企微添加人数
+     */
+    private Long wechatAddCount;
+
+    /**
+     * 企微添加率(%)
+     */
+    private BigDecimal wechatAddRate;
+
+    /**
+     * 报名成功名片人数
+     */
+    private Long registerSuccessCount;
+
+    /**
+     * 报名成功加微率(%)
+     */
+    private BigDecimal registerWechatRate;
+
+    /**
+     * 企微添加成本
+     */
+    private BigDecimal wechatAddCost;
+
+    /**
+     * 企微加群人数
+     */
+    private Long wechatGroupCount;
+
+    /**
+     * 企微加群率(%)
+     */
+    private BigDecimal wechatGroupRate;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+}
+

+ 145 - 0
fs-service/src/main/java/com/fs/newAdv/entity/SiteStatisticsDaily.java

@@ -0,0 +1,145 @@
+package com.fs.newAdv.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 站点日统计归档表
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Data
+@TableName("adv_site_statistics_daily")
+public class SiteStatisticsDaily implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 站点ID
+     */
+    private Long siteId;
+
+    /**
+     * 统计日期
+     */
+    private LocalDate statDate;
+
+    /**
+     * ROI
+     */
+    private BigDecimal roi;
+
+    /**
+     * 销售额
+     */
+    private BigDecimal salesAmount;
+
+    /**
+     * PV
+     */
+    private Long pv;
+
+    /**
+     * UV
+     */
+    private Long uv;
+
+    /**
+     * 展示数
+     */
+    private Long impressionCount;
+
+    /**
+     * 点击数
+     */
+    private Long clickCount;
+
+    /**
+     * 点击率(%)
+     */
+    private BigDecimal clickRate;
+
+    /**
+     * 平均点击单价
+     */
+    private BigDecimal avgClickPrice;
+
+    /**
+     * 账面花费
+     */
+    private BigDecimal accountCost;
+
+    /**
+     * 实际花费
+     */
+    private BigDecimal actualCost;
+
+    /**
+     * 名片数
+     */
+    private Long cardCount;
+
+    /**
+     * 名片获取率(%)
+     */
+    private BigDecimal cardAcquireRate;
+
+    /**
+     * 名片获取成本
+     */
+    private BigDecimal cardAcquireCost;
+
+    /**
+     * 企微添加人数
+     */
+    private Long wechatAddCount;
+
+    /**
+     * 企微添加率(%)
+     */
+    private BigDecimal wechatAddRate;
+
+    /**
+     * 报名成功名片人数
+     */
+    private Long registerSuccessCount;
+
+    /**
+     * 报名成功加微率(%)
+     */
+    private BigDecimal registerWechatRate;
+
+    /**
+     * 企微添加成本
+     */
+    private BigDecimal wechatAddCost;
+
+    /**
+     * 企微加群人数
+     */
+    private Long wechatGroupCount;
+
+    /**
+     * 企微加群率(%)
+     */
+    private BigDecimal wechatGroupRate;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+}
+

+ 144 - 0
fs-service/src/main/java/com/fs/newAdv/entity/SiteStatisticsMonthly.java

@@ -0,0 +1,144 @@
+package com.fs.newAdv.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDateTime;
+
+/**
+ * 站点月统计归档表
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Data
+@TableName("adv_site_statistics_monthly")
+public class SiteStatisticsMonthly implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 站点ID
+     */
+    private Long siteId;
+
+    /**
+     * 统计月份(如:2025-11)
+     */
+    private String statMonth;
+
+    /**
+     * ROI
+     */
+    private BigDecimal roi;
+
+    /**
+     * 销售额
+     */
+    private BigDecimal salesAmount;
+
+    /**
+     * PV
+     */
+    private Long pv;
+
+    /**
+     * UV
+     */
+    private Long uv;
+
+    /**
+     * 展示数
+     */
+    private Long impressionCount;
+
+    /**
+     * 点击数
+     */
+    private Long clickCount;
+
+    /**
+     * 点击率(%)
+     */
+    private BigDecimal clickRate;
+
+    /**
+     * 平均点击单价
+     */
+    private BigDecimal avgClickPrice;
+
+    /**
+     * 账面花费
+     */
+    private BigDecimal accountCost;
+
+    /**
+     * 实际花费
+     */
+    private BigDecimal actualCost;
+
+    /**
+     * 名片数
+     */
+    private Long cardCount;
+
+    /**
+     * 名片获取率(%)
+     */
+    private BigDecimal cardAcquireRate;
+
+    /**
+     * 名片获取成本
+     */
+    private BigDecimal cardAcquireCost;
+
+    /**
+     * 企微添加人数
+     */
+    private Long wechatAddCount;
+
+    /**
+     * 企微添加率(%)
+     */
+    private BigDecimal wechatAddRate;
+
+    /**
+     * 报名成功名片人数
+     */
+    private Long registerSuccessCount;
+
+    /**
+     * 报名成功加微率(%)
+     */
+    private BigDecimal registerWechatRate;
+
+    /**
+     * 企微添加成本
+     */
+    private BigDecimal wechatAddCost;
+
+    /**
+     * 企微加群人数
+     */
+    private Long wechatGroupCount;
+
+    /**
+     * 企微加群率(%)
+     */
+    private BigDecimal wechatGroupRate;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+}
+

+ 155 - 0
fs-service/src/main/java/com/fs/newAdv/entity/SiteStatisticsWeekly.java

@@ -0,0 +1,155 @@
+package com.fs.newAdv.entity;
+
+import com.baomidou.mybatisplus.annotation.*;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+/**
+ * 站点周统计归档表
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Data
+@TableName("adv_site_statistics_weekly")
+public class SiteStatisticsWeekly implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 站点ID
+     */
+    private Long siteId;
+
+    /**
+     * 统计周(如:2025-W44)
+     */
+    private String statWeek;
+
+    /**
+     * 周开始日期
+     */
+    private LocalDate weekStartDate;
+
+    /**
+     * 周结束日期
+     */
+    private LocalDate weekEndDate;
+
+    /**
+     * ROI
+     */
+    private BigDecimal roi;
+
+    /**
+     * 销售额
+     */
+    private BigDecimal salesAmount;
+
+    /**
+     * PV
+     */
+    private Long pv;
+
+    /**
+     * UV
+     */
+    private Long uv;
+
+    /**
+     * 展示数
+     */
+    private Long impressionCount;
+
+    /**
+     * 点击数
+     */
+    private Long clickCount;
+
+    /**
+     * 点击率(%)
+     */
+    private BigDecimal clickRate;
+
+    /**
+     * 平均点击单价
+     */
+    private BigDecimal avgClickPrice;
+
+    /**
+     * 账面花费
+     */
+    private BigDecimal accountCost;
+
+    /**
+     * 实际花费
+     */
+    private BigDecimal actualCost;
+
+    /**
+     * 名片数
+     */
+    private Long cardCount;
+
+    /**
+     * 名片获取率(%)
+     */
+    private BigDecimal cardAcquireRate;
+
+    /**
+     * 名片获取成本
+     */
+    private BigDecimal cardAcquireCost;
+
+    /**
+     * 企微添加人数
+     */
+    private Long wechatAddCount;
+
+    /**
+     * 企微添加率(%)
+     */
+    private BigDecimal wechatAddRate;
+
+    /**
+     * 报名成功名片人数
+     */
+    private Long registerSuccessCount;
+
+    /**
+     * 报名成功加微率(%)
+     */
+    private BigDecimal registerWechatRate;
+
+    /**
+     * 企微添加成本
+     */
+    private BigDecimal wechatAddCost;
+
+    /**
+     * 企微加群人数
+     */
+    private Long wechatGroupCount;
+
+    /**
+     * 企微加群率(%)
+     */
+    private BigDecimal wechatGroupRate;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+}
+

+ 78 - 0
fs-service/src/main/java/com/fs/newAdv/entity/SyncLog.java

@@ -0,0 +1,78 @@
+package com.fs.newAdv.entity;
+
+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_sync_log")
+public class SyncLog implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 同步类型(账户数据/统计数据)
+     */
+    private String syncType;
+
+    /**
+     * 广告商ID
+     */
+    private Long advertiserId;
+
+    /**
+     * 推广账户ID
+     */
+    private Long promotionAccountId;
+
+    /**
+     * 同步状态(1成功 2失败)
+     */
+    private Integer syncStatus;
+
+    /**
+     * 同步数据量
+     */
+    private Integer syncDataCount;
+
+    /**
+     * 开始时间
+     */
+    private LocalDateTime startTime;
+
+    /**
+     * 结束时间
+     */
+    private LocalDateTime endTime;
+
+    /**
+     * 耗时(ms)
+     */
+    private Long costTime;
+
+    /**
+     * 错误信息
+     */
+    private String errorMsg;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+}
+

+ 79 - 0
fs-service/src/main/java/com/fs/newAdv/entity/TrackingLink.java

@@ -0,0 +1,79 @@
+package com.fs.newAdv.entity;
+
+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_tracking_link")
+public class TrackingLink implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 主键
+     */
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 监测名称
+     */
+    private String trackingName;
+
+    /**
+     * 广告商
+     */
+    private String advertiserName;
+
+    /**
+     * 广告商ID
+     */
+    private Long advertiserId;
+
+    /**
+     * 推广方式
+     */
+    private String promotionType;
+
+    /**
+     * 类型
+     */
+    private String type;
+
+    /**
+     * 监测链接URL
+     */
+    private String trackingUrl;
+
+    /**
+     * 创建时间
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private LocalDateTime createTime;
+
+    /**
+     * 更新时间
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private LocalDateTime updateTime;
+
+    /**
+     * 创建人
+     */
+    private String creator;
+
+    /**
+     * 更新人
+     */
+    private String updater;
+}
+

+ 39 - 0
fs-service/src/main/java/com/fs/newAdv/enums/AdvertiserTypeEnum.java

@@ -0,0 +1,39 @@
+package com.fs.newAdv.enums;
+
+import lombok.Getter;
+
+/**
+ * 广告商类型枚举
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Getter
+public enum AdvertiserTypeEnum {
+
+    BAIDU(1, "百度"),
+    OCEAN_ENGINE(2, "巨量引擎"),
+    SINA(3, "新浪"),
+    GDT(4, "广点通");
+
+    private final Integer code;
+    private final String name;
+
+    AdvertiserTypeEnum(Integer code, String name) {
+        this.code = code;
+        this.name = name;
+    }
+
+    /**
+     * 根据code获取枚举
+     */
+    public static AdvertiserTypeEnum getByCode(Integer code) {
+        for (AdvertiserTypeEnum typeEnum : values()) {
+            if (typeEnum.getCode().equals(code)) {
+                return typeEnum;
+            }
+        }
+        return null;
+    }
+}
+

+ 35 - 0
fs-service/src/main/java/com/fs/newAdv/enums/CallbackStatusEnum.java

@@ -0,0 +1,35 @@
+package com.fs.newAdv.enums;
+
+import lombok.Getter;
+
+/**
+ * 回传状态枚举
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Getter
+public enum CallbackStatusEnum {
+
+    PENDING(0, "待回传"),
+    SUCCESS(1, "回传成功"),
+    FAILED(2, "回传失败");
+
+    private final Integer code;
+    private final String name;
+
+    CallbackStatusEnum(Integer code, String name) {
+        this.code = code;
+        this.name = name;
+    }
+
+    public static CallbackStatusEnum getByCode(Integer code) {
+        for (CallbackStatusEnum statusEnum : values()) {
+            if (statusEnum.getCode().equals(code)) {
+                return statusEnum;
+            }
+        }
+        return null;
+    }
+}
+

+ 36 - 0
fs-service/src/main/java/com/fs/newAdv/enums/TaskStatusEnum.java

@@ -0,0 +1,36 @@
+package com.fs.newAdv.enums;
+
+import lombok.Getter;
+
+/**
+ * 任务状态枚举
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Getter
+public enum TaskStatusEnum {
+
+    PENDING(0, "待执行"),
+    RUNNING(1, "执行中"),
+    SUCCESS(2, "执行成功"),
+    FAILED(3, "执行失败");
+
+    private final Integer code;
+    private final String name;
+
+    TaskStatusEnum(Integer code, String name) {
+        this.code = code;
+        this.name = name;
+    }
+
+    public static TaskStatusEnum getByCode(Integer code) {
+        for (TaskStatusEnum statusEnum : values()) {
+            if (statusEnum.getCode().equals(code)) {
+                return statusEnum;
+            }
+        }
+        return null;
+    }
+}
+

+ 39 - 0
fs-service/src/main/java/com/fs/newAdv/enums/TaskTypeEnum.java

@@ -0,0 +1,39 @@
+package com.fs.newAdv.enums;
+
+import lombok.Getter;
+
+/**
+ * 任务类型枚举
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Getter
+public enum TaskTypeEnum {
+
+    DATA_SYNC("DATA_SYNC", "数据同步"),
+    DAILY_ARCHIVE("DAILY_ARCHIVE", "日归档"),
+    WEEKLY_ARCHIVE("WEEKLY_ARCHIVE", "周归档"),
+    MONTHLY_ARCHIVE("MONTHLY_ARCHIVE", "月归档"),
+    REDIS_PERSIST("REDIS_PERSIST", "Redis持久化"),
+    CONVERSION_RETRY("CONVERSION_RETRY", "回传重试"),
+    TASK_COMPENSATION("TASK_COMPENSATION", "任务补偿");
+
+    private final String code;
+    private final String name;
+
+    TaskTypeEnum(String code, String name) {
+        this.code = code;
+        this.name = name;
+    }
+
+    public static TaskTypeEnum getByCode(String code) {
+        for (TaskTypeEnum typeEnum : values()) {
+            if (typeEnum.getCode().equals(code)) {
+                return typeEnum;
+            }
+        }
+        return null;
+    }
+}
+

+ 59 - 0
fs-service/src/main/java/com/fs/newAdv/event/ConversionEvent.java

@@ -0,0 +1,59 @@
+package com.fs.newAdv.event;
+
+import lombok.Getter;
+import org.springframework.context.ApplicationEvent;
+
+/**
+ * 转化事件
+ * 当用户发生转化行为时发布此事件(观察者模式)
+ * 事件监听器会将其转换为MQ消息发送
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ * @updated 2025-11-05 添加clickId参数,直接传递点击ID
+ */
+@Getter
+public class ConversionEvent extends ApplicationEvent {
+
+    /**
+     * 站点ID
+     */
+    private final Long siteId;
+
+    /**
+     * 点击ID(广告平台提供)
+     */
+    private final String clickId;
+
+    /**
+     * 广告商名称(BAIDU, OCEANENGINE, SINA, GDT)
+     */
+    private final String advertiser;
+
+    /**
+     * 转化事件类型(如:register、form_submit、add_wechat)
+     */
+    private final String eventType;
+
+    /**
+     * 转化价值(元)
+     */
+    private final Double value;
+
+    /**
+     * 线索ID(可选)
+     */
+    private final Long leadId;
+
+    public ConversionEvent(Object source, Long siteId, String clickId,
+                          String advertiser, String eventType, Double value, Long leadId) {
+        super(source);
+        this.siteId = siteId;
+        this.clickId = clickId;
+        this.advertiser = advertiser;
+        this.eventType = eventType;
+        this.value = value;
+        this.leadId = leadId;
+    }
+}
+

+ 72 - 0
fs-service/src/main/java/com/fs/newAdv/event/ConversionEventListener.java

@@ -0,0 +1,72 @@
+package com.fs.newAdv.event;
+
+import cn.hutool.core.util.StrUtil;
+import com.fs.newAdv.mq.message.ConversionMessage;
+import com.fs.newAdv.mq.producer.ConversionMessageProducer;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.event.EventListener;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Component;
+
+/**
+ * 转化事件监听器
+ * 监听转化事件并发送到MQ进行异步处理
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ * @updated 2025-11-05 重构为发送MQ消息
+ */
+@Slf4j
+@Component
+public class ConversionEventListener {
+
+    @Autowired
+    private ConversionMessageProducer messageProducer;
+
+    /**
+     * 处理转化事件
+     * 将转化事件转换为MQ消息并发送
+     *
+     * @param event 转化事件
+     */
+    @Async("asyncExecutor")
+    @EventListener
+    public void handleConversionEvent(ConversionEvent event) {
+        Long siteId = event.getSiteId();
+        String clickId = event.getClickId();
+        String advertiser = event.getAdvertiser();
+        String eventType = event.getEventType();
+        Double value = event.getValue();
+        Long leadId = event.getLeadId();
+
+        log.info("接收到转化事件 | siteId={}, clickId={}, advertiser={}, eventType={}",
+                 siteId, clickId, advertiser, eventType);
+
+        try {
+            // 1. 校验参数
+            if (siteId == null || StrUtil.isBlank(clickId) || StrUtil.isBlank(advertiser)) {
+                log.error("转化事件参数不完整 | siteId={}, clickId={}, advertiser={}",
+                          siteId, clickId, advertiser);
+                return;
+            }
+
+            // 2. 构建MQ消息
+            ConversionMessage message = new ConversionMessage();
+            message.setSiteId(siteId);
+            message.setClickId(clickId);
+            message.setAdvertiser(advertiser);
+            message.setEventType(eventType);
+            message.setValue(value);
+            message.setLeadId(leadId);
+
+            // 3. 发送到MQ
+            messageProducer.sendConversionMessage(message);
+
+            log.info("转化事件已发送到MQ | clickId={}, eventType={}", clickId, eventType);
+
+        } catch (Exception e) {
+            log.error("处理转化事件失败 | clickId={}, eventType={}", clickId, eventType, e);
+        }
+    }
+}

+ 54 - 0
fs-service/src/main/java/com/fs/newAdv/event/ConversionEventPublisher.java

@@ -0,0 +1,54 @@
+package com.fs.newAdv.event;
+
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.context.ApplicationEventPublisher;
+import org.springframework.stereotype.Component;
+
+/**
+ * 转化事件发布器
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ * @updated 2025-11-05 添加clickId和advertiser参数
+ */
+@Slf4j
+@Component
+public class ConversionEventPublisher {
+
+    @Autowired
+    private ApplicationEventPublisher applicationEventPublisher;
+
+    /**
+     * 发布转化事件
+     *
+     * @param siteId 站点ID
+     * @param clickId 点击ID(广告平台提供)
+     * @param advertiser 广告商名称(BAIDU, OCEANENGINE, SINA, GDT)
+     * @param eventType 转化事件类型(如:SUBMIT_FORM、REGISTER、PAY)
+     * @param value 转化价值(元)
+     * @param leadId 线索ID(可选)
+     */
+    public void publishConversionEvent(Long siteId, String clickId, String advertiser,
+                                      String eventType, Double value, Long leadId) {
+        log.info("发布转化事件 | siteId={}, clickId={}, advertiser={}, eventType={}, value={}",
+                siteId, clickId, advertiser, eventType, value);
+
+        ConversionEvent event = new ConversionEvent(this, siteId, clickId,
+                                                     advertiser, eventType, value, leadId);
+
+        applicationEventPublisher.publishEvent(event);
+    }
+
+    /**
+     * 发布转化事件(无价值,无线索ID)
+     *
+     * @param siteId 站点ID
+     * @param clickId 点击ID
+     * @param advertiser 广告商名称
+     * @param eventType 转化事件类型
+     */
+    public void publishConversionEvent(Long siteId, String clickId, String advertiser, String eventType) {
+        publishConversionEvent(siteId, clickId, advertiser, eventType, null, null);
+    }
+}

+ 59 - 0
fs-service/src/main/java/com/fs/newAdv/integration/adapter/BaiduAdapter.java

@@ -0,0 +1,59 @@
+package com.fs.newAdv.integration.adapter;
+
+import cn.hutool.core.map.MapUtil;
+
+import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 百度广告适配器
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Component
+public class BaiduAdapter implements IAdvertiserAdapter {
+
+    @Override
+    public Map<String, Object> adaptCallbackData(Map<String, Object> rawData) {
+        log.info("百度数据适配:{}", rawData);
+
+        Map<String, Object> result = new HashMap<>();
+
+        // 百度字段映射
+        result.put("advertiserType", getAdvertiserType());
+        result.put("clickId", MapUtil.getStr(rawData, "clk_id"));
+        result.put("siteId", MapUtil.getLong(rawData, "site_id"));
+        result.put("impressionCount", MapUtil.getLong(rawData, "show"));
+        result.put("clickCount", MapUtil.getLong(rawData, "click"));
+        result.put("cost", MapUtil.getDouble(rawData, "cost"));
+
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> adaptApiResponse(Map<String, Object> rawData) {
+        log.info("百度API响应适配:{}", rawData);
+
+        Map<String, Object> result = new HashMap<>();
+
+        // 适配API返回的统计数据
+        result.put("impressionCount", MapUtil.getLong(rawData, "impression"));
+        result.put("clickCount", MapUtil.getLong(rawData, "click"));
+        result.put("cost", MapUtil.getDouble(rawData, "cost"));
+        result.put("avgClickPrice", MapUtil.getDouble(rawData, "acp"));
+
+        return result;
+    }
+
+    @Override
+    public Integer getAdvertiserType() {
+        return AdvertiserTypeEnum.BAIDU.getCode();
+    }
+}
+

+ 36 - 0
fs-service/src/main/java/com/fs/newAdv/integration/adapter/IAdvertiserAdapter.java

@@ -0,0 +1,36 @@
+package com.fs.newAdv.integration.adapter;
+
+import java.util.Map;
+
+/**
+ * 广告商适配器接口(适配器模式)
+ * 统一不同广告平台的数据格式
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+public interface IAdvertiserAdapter {
+
+    /**
+     * 适配回调数据
+     * 将第三方平台的回调数据转换为统一格式
+     *
+     * @param rawData 原始数据
+     * @return 统一格式数据
+     */
+    Map<String, Object> adaptCallbackData(Map<String, Object> rawData);
+
+    /**
+     * 适配API响应数据
+     *
+     * @param rawData 原始数据
+     * @return 统一格式数据
+     */
+    Map<String, Object> adaptApiResponse(Map<String, Object> rawData);
+
+    /**
+     * 获取广告商类型
+     */
+    Integer getAdvertiserType();
+}
+

+ 57 - 0
fs-service/src/main/java/com/fs/newAdv/integration/adapter/OceanEngineAdapter.java

@@ -0,0 +1,57 @@
+package com.fs.newAdv.integration.adapter;
+
+import cn.hutool.core.map.MapUtil;
+import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 巨量引擎广告适配器
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Component
+public class OceanEngineAdapter implements IAdvertiserAdapter {
+
+    @Override
+    public Map<String, Object> adaptCallbackData(Map<String, Object> rawData) {
+        log.info("巨量引擎数据适配:{}", rawData);
+
+        Map<String, Object> result = new HashMap<>();
+
+        // 巨量引擎字段映射
+        result.put("advertiserType", getAdvertiserType());
+        result.put("clickId", MapUtil.getStr(rawData, "clickid"));
+        result.put("siteId", MapUtil.getLong(rawData, "site_id"));
+        result.put("impressionCount", MapUtil.getLong(rawData, "show"));
+        result.put("clickCount", MapUtil.getLong(rawData, "click"));
+        result.put("cost", MapUtil.getDouble(rawData, "cost"));
+
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> adaptApiResponse(Map<String, Object> rawData) {
+        log.info("巨量引擎API响应适配:{}", rawData);
+
+        Map<String, Object> result = new HashMap<>();
+
+        result.put("impressionCount", MapUtil.getLong(rawData, "stat_cost"));
+        result.put("clickCount", MapUtil.getLong(rawData, "click"));
+        result.put("cost", MapUtil.getDouble(rawData, "cost"));
+        result.put("avgClickPrice", MapUtil.getDouble(rawData, "avg_show_cost"));
+
+        return result;
+    }
+
+    @Override
+    public Integer getAdvertiserType() {
+        return AdvertiserTypeEnum.OCEAN_ENGINE.getCode();
+    }
+}
+

+ 280 - 0
fs-service/src/main/java/com/fs/newAdv/integration/client/BaiduApiClient.java

@@ -0,0 +1,280 @@
+package com.fs.newAdv.integration.client;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.fs.common.constant.SystemConstant;
+import com.fs.common.exception.ThirdPartyException;
+import com.fs.common.utils.SnowflakeUtil;
+import com.fs.newAdv.entity.ApiCallLog;
+import com.fs.newAdv.mapper.ApiCallLogMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 百度营销API客户端
+ * 用于调用百度营销的API接口(转化回传等)
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ * @see <a href="https://dev2.baidu.com/content?sceneType=0&pageId=101211&nodeId=658">百度营销API文档</a>
+ */
+@Slf4j
+@Component
+public class BaiduApiClient {
+
+    /**
+     * 百度OCPC转化回传API地址
+     */
+    private static final String CONVERSION_API_URL = "https://ocpc.baidu.com/ocpcapi/api/uploadConvertData";
+
+    /**
+     * 百度OAuth Token地址
+     */
+    private static final String TOKEN_API_URL = "https://openapi.baidu.com/oauth/2.0/token";
+
+    @Autowired
+    private ApiCallLogMapper apiCallLogMapper;
+
+    /**
+     * 回传转化数据到百度
+     *
+     * @param accessToken    访问令牌
+     * @param conversionData 转化数据
+     * @return 是否成功
+     */
+    public boolean reportConversion(String accessToken, Map<String, Object> conversionData) {
+        String apiUrl = CONVERSION_API_URL;
+        long startTime = System.currentTimeMillis();
+
+        log.info("开始回传转化数据到百度,URL:{}", apiUrl);
+
+        try {
+            // 构建请求参数
+            Map<String, Object> requestParams = buildConversionParams(conversionData);
+            String requestBody = JSONUtil.toJsonStr(requestParams);
+
+            // 发送HTTP请求
+            HttpResponse response = HttpRequest.post(apiUrl)
+                    .header("Content-Type", "application/json")
+                    .body(requestBody)
+                    .timeout(SystemConstant.API_TIMEOUT)
+                    .execute();
+
+            String responseBody = response.body();
+            int statusCode = response.getStatus();
+
+            log.info("百度API响应:状态码={},响应={}", statusCode, responseBody);
+
+            // 记录API调用日志
+            saveApiCallLog(apiUrl, "POST", requestBody, statusCode, responseBody,
+                    statusCode == 200 ? 1 : 2, null, startTime);
+
+            // 解析响应
+            if (statusCode == 200) {
+                JSONObject result = JSONUtil.parseObj(responseBody);
+
+                // 百度API成功的标识
+                if (result.containsKey("success") && result.getBool("success")) {
+                    log.info("百度转化回传成功");
+                    return true;
+                } else {
+                    String message = result.getStr("message", "未知错误");
+                    log.error("百度转化回传失败:{}", message);
+                    throw new ThirdPartyException("百度回传失败:" + message);
+                }
+            } else {
+                log.error("百度API调用失败,状态码:{}", statusCode);
+                throw new ThirdPartyException("百度API调用失败,状态码:" + statusCode);
+            }
+
+        } catch (Exception e) {
+            log.error("调用百度API异常", e);
+            saveApiCallLog(apiUrl, "POST", JSONUtil.toJsonStr(conversionData),
+                    0, null, 2, e.getMessage(), startTime);
+            throw new ThirdPartyException("调用百度API异常:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 构建转化回传参数(百度OCPC格式)
+     *
+     * @param conversionData 转化数据
+     * @return 请求参数
+     */
+    private Map<String, Object> buildConversionParams(Map<String, Object> conversionData) {
+        Map<String, Object> params = new HashMap<>();
+
+        // 必填参数
+        String token = (String) conversionData.get("accessToken");
+        if (StrUtil.isBlank(token)) {
+            throw new ThirdPartyException("AccessToken不能为空");
+        }
+        params.put("token", token);
+
+        // 转化数据列表
+        List<Map<String, Object>> conversionList = new ArrayList<>();
+        Map<String, Object> conversion = new HashMap<>();
+
+        // 点击ID(必填)
+        String logId = (String) conversionData.get("logId");
+        if (StrUtil.isBlank(logId)) {
+            throw new ThirdPartyException("点击ID不能为空");
+        }
+        conversion.put("logidUrl", logId);
+
+        // 转化类型(必填)
+        Integer conversionType = (Integer) conversionData.get("conversionType");
+        if (conversionType == null) {
+            conversionType = 1; // 默认为1(激活)
+        }
+        conversion.put("newType", conversionType);
+
+        // 转化时间(毫秒时间戳)
+        Long conversionTime = (Long) conversionData.get("conversionTime");
+        if (conversionTime == null) {
+            conversionTime = System.currentTimeMillis() / 1000;
+        }
+        conversion.put("convertTime", conversionTime);
+
+        // 转化价值(可选)
+        if (conversionData.containsKey("value")) {
+            conversion.put("value", conversionData.get("value"));
+        }
+
+        conversionList.add(conversion);
+        params.put("conversionTypes", conversionList);
+
+        return params;
+    }
+
+    /**
+     * 获取访问令牌(百度OAuth2.0)
+     *
+     * @param apiKey    API Key
+     * @param secretKey Secret Key
+     * @return 访问令牌
+     */
+    public String getAccessToken(String apiKey, String secretKey) {
+        log.info("开始获取百度AccessToken");
+
+        try {
+            String url = TOKEN_API_URL +
+                    "?grant_type=client_credentials" +
+                    "&client_id=" + apiKey +
+                    "&client_secret=" + secretKey;
+
+            String response = HttpRequest.get(url)
+                    .timeout(SystemConstant.API_TIMEOUT)
+                    .execute()
+                    .body();
+
+            JSONObject result = JSONUtil.parseObj(response);
+
+            if (result.containsKey("access_token")) {
+                String accessToken = result.getStr("access_token");
+                log.info("获取百度AccessToken成功");
+                return accessToken;
+            } else {
+                String error = result.getStr("error_description", "未知错误");
+                throw new ThirdPartyException("获取百度AccessToken失败:" + error);
+            }
+
+        } catch (Exception e) {
+            log.error("获取百度AccessToken异常", e);
+            throw new ThirdPartyException("获取AccessToken异常:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 查询账户报告数据
+     *
+     * @param accessToken 访问令牌
+     * @param startDate   开始日期(yyyyMMdd)
+     * @param endDate     结束日期(yyyyMMdd)
+     * @return 报告数据
+     */
+    public Map<String, Object> getAccountReport(String accessToken, String startDate, String endDate) {
+        log.info("查询百度账户报告:{}到{}", startDate, endDate);
+
+        String apiUrl = "https://api.baidu.com/json/sms/service/KR2Service/getReportId";
+
+        try {
+            Map<String, Object> params = new HashMap<>();
+            params.put("performanceData", new String[]{"impression", "click", "cost"});
+            params.put("startDate", startDate);
+            params.put("endDate", endDate);
+            params.put("levelOfDetails", "account");
+
+            Map<String, Object> header = new HashMap<>();
+            header.put("token", accessToken);
+
+            Map<String, Object> body = new HashMap<>();
+            body.put("header", header);
+            body.put("body", params);
+
+            String response = HttpRequest.post(apiUrl)
+                    .header("Content-Type", "application/json")
+                    .body(JSONUtil.toJsonStr(body))
+                    .timeout(SystemConstant.API_TIMEOUT)
+                    .execute()
+                    .body();
+
+            JSONObject result = JSONUtil.parseObj(response);
+
+            if (result.containsKey("header")) {
+                JSONObject resHeader = result.getJSONObject("header");
+                Integer status = resHeader.getInt("status");
+
+                if (status != null && status == 0) {
+                    return result.getJSONObject("body");
+                }
+            }
+
+            log.error("查询百度报告失败:{}", response);
+            throw new ThirdPartyException("查询百度报告失败");
+
+        } catch (Exception e) {
+            log.error("查询百度账户报告异常", e);
+            throw new ThirdPartyException("查询报告异常:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 保存API调用日志
+     */
+    private void saveApiCallLog(String apiUrl, String method, String requestParams,
+                                Integer responseStatus, String responseBody,
+                                Integer callStatus, String errorMsg, long startTime) {
+        try {
+            ApiCallLog log = new ApiCallLog();
+            log.setId(SnowflakeUtil.nextId());
+            log.setAdvertiserId(1L); // 百度
+            log.setAdvertiserName("百度");
+            log.setApiName("转化回传API");
+            log.setApiUrl(apiUrl);
+            log.setRequestMethod(method);
+            log.setRequestParams(requestParams);
+            log.setResponseStatus(responseStatus);
+            log.setResponseBody(responseBody);
+            log.setCallStatus(callStatus);
+            log.setErrorMsg(errorMsg);
+            log.setCostTime(System.currentTimeMillis() - startTime);
+            log.setCreateTime(LocalDateTime.now());
+
+            apiCallLogMapper.insert(log);
+        } catch (Exception e) {
+            log.error("保存API调用日志失败", e);
+        }
+    }
+}
+

+ 234 - 0
fs-service/src/main/java/com/fs/newAdv/integration/client/OceanEngineApiClient.java

@@ -0,0 +1,234 @@
+package com.fs.newAdv.integration.client;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.fs.common.constant.SystemConstant;
+import com.fs.common.exception.ThirdPartyException;
+import com.fs.common.utils.SnowflakeUtil;
+import com.fs.newAdv.entity.ApiCallLog;
+import com.fs.newAdv.mapper.ApiCallLogMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 巨量引擎API客户端
+ * 用于调用巨量引擎的API接口(转化回传等)
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ * @see <a href="https://open.oceanengine.com/labels/7/docs/1696710497745920">巨量引擎API文档</a>
+ */
+@Slf4j
+@Component
+public class OceanEngineApiClient {
+
+    /**
+     * 巨量引擎转化回传API地址
+     */
+    private static final String CONVERSION_API_URL = "https://ad.oceanengine.com/open_api/2/conversion/";
+
+    @Autowired
+    private ApiCallLogMapper apiCallLogMapper;
+
+    /**
+     * 回传转化数据到巨量引擎
+     *
+     * @param accessToken 访问令牌
+     * @param conversionData 转化数据
+     * @return 是否成功
+     */
+    public boolean reportConversion(String accessToken, Map<String, Object> conversionData) {
+        String apiUrl = CONVERSION_API_URL;
+        long startTime = System.currentTimeMillis();
+
+        log.info("开始回传转化数据到巨量引擎,URL:{}", apiUrl);
+
+        try {
+            // 构建请求参数
+            Map<String, Object> requestParams = buildConversionParams(conversionData);
+            String requestBody = JSONUtil.toJsonStr(requestParams);
+
+            // 发送HTTP请求
+            HttpResponse response = HttpRequest.post(apiUrl)
+                    .header("Access-Token", accessToken)
+                    .header("Content-Type", "application/json")
+                    .body(requestBody)
+                    .timeout(SystemConstant.API_TIMEOUT)
+                    .execute();
+
+            String responseBody = response.body();
+            int statusCode = response.getStatus();
+
+            log.info("巨量引擎API响应:状态码={},响应={}", statusCode, responseBody);
+
+            // 记录API调用日志
+            saveApiCallLog(apiUrl, "POST", requestBody, statusCode, responseBody,
+                    statusCode == 200 ? 1 : 2, null, startTime);
+
+            // 解析响应
+            if (statusCode == 200) {
+                JSONObject result = JSONUtil.parseObj(responseBody);
+                Integer code = result.getInt("code");
+
+                if (code != null && code == 0) {
+                    log.info("巨量引擎转化回传成功");
+                    return true;
+                } else {
+                    String message = result.getStr("message", "未知错误");
+                    log.error("巨量引擎转化回传失败:code={},message={}", code, message);
+                    throw new ThirdPartyException("巨量引擎回传失败:" + message);
+                }
+            } else {
+                log.error("巨量引擎API调用失败,状态码:{}", statusCode);
+                throw new ThirdPartyException("巨量引擎API调用失败,状态码:" + statusCode);
+            }
+
+        } catch (Exception e) {
+            log.error("调用巨量引擎API异常", e);
+            saveApiCallLog(apiUrl, "POST", JSONUtil.toJsonStr(conversionData),
+                    0, null, 2, e.getMessage(), startTime);
+            throw new ThirdPartyException("调用巨量引擎API异常:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 构建转化回传参数
+     *
+     * @param conversionData 转化数据
+     * @return 请求参数
+     */
+    private Map<String, Object> buildConversionParams(Map<String, Object> conversionData) {
+        Map<String, Object> params = new HashMap<>();
+
+        // 必填参数
+        params.put("event_type", conversionData.get("eventType")); // 转化类型
+        params.put("context", buildContext(conversionData)); // 上下文信息
+
+        // 可选参数
+        if (conversionData.containsKey("value")) {
+            params.put("value", conversionData.get("value")); // 转化价值
+        }
+
+        if (conversionData.containsKey("description")) {
+            params.put("description", conversionData.get("description")); // 描述
+        }
+
+        return params;
+    }
+
+    /**
+     * 构建上下文信息
+     *
+     * @param conversionData 转化数据
+     * @return 上下文Map
+     */
+    private Map<String, Object> buildContext(Map<String, Object> conversionData) {
+        Map<String, Object> context = new HashMap<>();
+
+        // 必填:点击ID
+        context.put("ad", buildAdContext(conversionData));
+
+        // 用户信息
+        if (conversionData.containsKey("userId")) {
+            Map<String, Object> user = new HashMap<>();
+            user.put("user_id", conversionData.get("userId"));
+            context.put("user", user);
+        }
+
+        return context;
+    }
+
+    /**
+     * 构建广告上下文
+     *
+     * @param conversionData 转化数据
+     * @return 广告上下文
+     */
+    private Map<String, Object> buildAdContext(Map<String, Object> conversionData) {
+        Map<String, Object> ad = new HashMap<>();
+
+        // 点击ID(必填)
+        String clickId = (String) conversionData.get("clickId");
+        if (StrUtil.isBlank(clickId)) {
+            throw new ThirdPartyException("点击ID不能为空");
+        }
+        ad.put("callback", clickId);
+
+        return ad;
+    }
+
+    /**
+     * 获取访问令牌
+     *
+     * @param appId 应用ID
+     * @param appSecret 应用密钥
+     * @return 访问令牌
+     */
+    public String getAccessToken(String appId, String appSecret) {
+        String apiUrl = "https://ad.oceanengine.com/open_api/oauth2/access_token/";
+
+        try {
+            Map<String, Object> params = new HashMap<>();
+            params.put("app_id", appId);
+            params.put("secret", appSecret);
+            params.put("grant_type", "auth_code");
+
+            String response = HttpRequest.post(apiUrl)
+                    .header("Content-Type", "application/json")
+                    .body(JSONUtil.toJsonStr(params))
+                    .timeout(SystemConstant.API_TIMEOUT)
+                    .execute()
+                    .body();
+
+            JSONObject result = JSONUtil.parseObj(response);
+
+            if (result.getInt("code") == 0) {
+                JSONObject data = result.getJSONObject("data");
+                return data.getStr("access_token");
+            } else {
+                throw new ThirdPartyException("获取巨量引擎AccessToken失败:" + result.getStr("message"));
+            }
+
+        } catch (Exception e) {
+            log.error("获取巨量引擎AccessToken异常", e);
+            throw new ThirdPartyException("获取AccessToken异常:" + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 保存API调用日志
+     */
+    private void saveApiCallLog(String apiUrl, String method, String requestParams,
+                                Integer responseStatus, String responseBody,
+                                Integer callStatus, String errorMsg, long startTime) {
+        try {
+            ApiCallLog log = new ApiCallLog();
+            log.setId(SnowflakeUtil.nextId());
+            log.setAdvertiserId(2L); // 巨量引擎
+            log.setAdvertiserName("巨量引擎");
+            log.setApiName("转化回传API");
+            log.setApiUrl(apiUrl);
+            log.setRequestMethod(method);
+            log.setRequestParams(requestParams);
+            log.setResponseStatus(responseStatus);
+            log.setResponseBody(responseBody);
+            log.setCallStatus(callStatus);
+            log.setErrorMsg(errorMsg);
+            log.setCostTime(System.currentTimeMillis() - startTime);
+            log.setCreateTime(LocalDateTime.now());
+
+            apiCallLogMapper.insert(log);
+        } catch (Exception e) {
+            log.error("保存API调用日志失败", e);
+        }
+    }
+}
+

+ 89 - 0
fs-service/src/main/java/com/fs/newAdv/integration/factory/AdvertiserHandlerFactory.java

@@ -0,0 +1,89 @@
+package com.fs.newAdv.integration.factory;
+
+import com.fs.common.exception.base.BusinessException;
+import com.fs.newAdv.integration.adapter.IAdvertiserAdapter;
+import com.fs.newAdv.integration.strategy.ICallbackStrategy;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * 广告商处理器工厂(工厂模式)
+ * 根据广告商类型获取对应的适配器和策略
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Component
+public class AdvertiserHandlerFactory {
+
+    @Autowired
+    private List<IAdvertiserAdapter> advertiserAdapters;
+
+    @Autowired
+    private List<ICallbackStrategy> callbackStrategies;
+
+    /**
+     * 适配器缓存
+     */
+    private final Map<Integer, IAdvertiserAdapter> adapterMap = new ConcurrentHashMap<>();
+
+    /**
+     * 策略缓存
+     */
+    private final Map<Integer, ICallbackStrategy> strategyMap = new ConcurrentHashMap<>();
+
+    /**
+     * 初始化工厂
+     */
+    @PostConstruct
+    public void init() {
+        // 初始化适配器Map
+        for (IAdvertiserAdapter adapter : advertiserAdapters) {
+            adapterMap.put(adapter.getAdvertiserType(), adapter);
+        }
+
+        // 初始化策略Map
+        for (ICallbackStrategy strategy : callbackStrategies) {
+            strategyMap.put(strategy.getAdvertiserType(), strategy);
+        }
+
+        log.info("广告商处理器工厂初始化完成,适配器数量:{},策略数量:{}",
+                adapterMap.size(), strategyMap.size());
+    }
+
+    /**
+     * 获取适配器
+     *
+     * @param advertiserType 广告商类型
+     * @return 适配器
+     */
+    public IAdvertiserAdapter getAdapter(Integer advertiserType) {
+        IAdvertiserAdapter adapter = adapterMap.get(advertiserType);
+        if (adapter == null) {
+            throw new BusinessException("不支持的广告商类型:" + advertiserType);
+        }
+        return adapter;
+    }
+
+    /**
+     * 获取回调策略
+     *
+     * @param advertiserType 广告商类型
+     * @return 回调策略
+     */
+    public ICallbackStrategy getStrategy(Integer advertiserType) {
+        ICallbackStrategy strategy = strategyMap.get(advertiserType);
+        if (strategy == null) {
+            throw new BusinessException("不支持的广告商类型:" + advertiserType);
+        }
+        return strategy;
+    }
+}
+

+ 103 - 0
fs-service/src/main/java/com/fs/newAdv/integration/handler/AbstractCallbackHandler.java

@@ -0,0 +1,103 @@
+package com.fs.newAdv.integration.handler;
+
+import cn.hutool.json.JSONUtil;
+import com.fs.common.utils.SnowflakeUtil;
+import com.fs.newAdv.entity.CallbackLog;
+import com.fs.newAdv.mapper.CallbackLogMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import java.time.LocalDateTime;
+import java.util.Map;
+
+/**
+ * 回调处理抽象类(模板方法模式)
+ * 定义回调处理的统一流程
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+public abstract class AbstractCallbackHandler {
+
+    @Autowired
+    private CallbackLogMapper callbackLogMapper;
+
+    /**
+     * 模板方法:处理回调的统一流程
+     *
+     * @param rawData 原始数据
+     * @return 是否处理成功
+     */
+    public boolean process(Map<String, Object> rawData) {
+        long startTime = System.currentTimeMillis();
+        CallbackLog callbackLog = new CallbackLog();
+
+        try {
+            // 1. 数据验证
+            log.info("步骤1:数据验证");
+            boolean valid = validate(rawData);
+            if (!valid) {
+                log.error("数据验证失败:{}", rawData);
+                saveLog(callbackLog, rawData, 2, "数据验证失败", startTime);
+                return false;
+            }
+
+            // 2. 数据转换
+            log.info("步骤2:数据转换");
+            Map<String, Object> convertedData = convert(rawData);
+
+            // 3. 业务处理
+            log.info("步骤3:业务处理");
+            handle(convertedData);
+
+            // 4. 保存日志
+            log.info("步骤4:保存成功日志");
+            saveLog(callbackLog, rawData, 1, null, startTime);
+
+            return true;
+
+        } catch (Exception e) {
+            log.error("回调处理异常:{}", e.getMessage(), e);
+            saveLog(callbackLog, rawData, 2, e.getMessage(), startTime);
+            return false;
+        }
+    }
+
+    /**
+     * 数据验证(子类实现)
+     */
+    protected abstract boolean validate(Map<String, Object> rawData);
+
+    /**
+     * 数据转换(子类实现)
+     */
+    protected abstract Map<String, Object> convert(Map<String, Object> rawData);
+
+    /**
+     * 业务处理(子类实现)
+     */
+    protected abstract void handle(Map<String, Object> convertedData);
+
+    /**
+     * 获取广告商类型(子类实现)
+     */
+    protected abstract Integer getAdvertiserType();
+
+    /**
+     * 保存回调日志
+     */
+    private void saveLog(CallbackLog log, Map<String, Object> rawData, Integer status, String errorMsg, long startTime) {
+        log.setId(SnowflakeUtil.nextId());
+        log.setAdvertiserId(getAdvertiserType().longValue());
+        log.setCallbackType("数据回调");
+        log.setRequestParams(JSONUtil.toJsonStr(rawData));
+        log.setProcessStatus(status);
+        log.setErrorMsg(errorMsg);
+        log.setProcessTime(System.currentTimeMillis() - startTime);
+        log.setCreateTime(LocalDateTime.now());
+
+        callbackLogMapper.insert(log);
+    }
+}
+

+ 197 - 0
fs-service/src/main/java/com/fs/newAdv/integration/handler/BaiduCallbackHandler.java

@@ -0,0 +1,197 @@
+//package com.fs.newAdv.integration.handler;
+//
+//import cn.hutool.core.map.MapUtil;
+//import cn.hutool.core.util.StrUtil;
+//import com.fs.newAdv.enums.AdvertiserTypeEnum;
+//import com.fs.newAdv.integration.adapter.BaiduAdapter;
+//import lombok.extern.slf4j.Slf4j;
+//import org.springframework.beans.factory.annotation.Autowired;
+//import org.springframework.stereotype.Component;
+//
+//import java.util.Map;
+//
+///**
+// * 百度回调处理器
+// * 基于模板方法模式,实现百度特定的回调处理逻辑
+// *
+// * @author zhangqin
+// * @date 2025-11-03
+// * @deprecated 该Handler已废弃!
+// *
+// * 原设计:处理广告平台的回调数据
+// * 新设计:广告平台通过URL参数传递数据,由LandingPageController接收并保存到adv_click_trace表
+// *
+// * 请使用ClickTraceService代替!
+// * @see com.crm.adv.service.IClickTraceService
+// */
+//@Deprecated
+//@Slf4j
+//@Component
+//public class BaiduCallbackHandler extends AbstractCallbackHandler {
+//
+//    @Autowired
+//    private BaiduAdapter baiduAdapter;
+//
+//    @Override
+//    protected boolean validate(Map<String, Object> rawData) {
+//        log.info("百度回调数据验证");
+//
+//        // 验证必填字段
+//        if (MapUtil.isEmpty(rawData)) {
+//            log.error("回调数据为空");
+//            return false;
+//        }
+//
+//        // 验证点击ID(百度使用clk_id或logid字段)
+//        String clkId = MapUtil.getStr(rawData, "clk_id");
+//        String logId = MapUtil.getStr(rawData, "logid");
+//
+//        if (StrUtil.isBlank(clkId) && StrUtil.isBlank(logId)) {
+//            log.error("缺少点击ID(clk_id或logid)");
+//            return false;
+//        }
+//
+//        // 验证站点ID
+//        if (!rawData.containsKey("site_id")) {
+//            log.error("缺少站点ID");
+//            return false;
+//        }
+//
+//        return true;
+//    }
+//
+//    @Override
+//    protected Map<String, Object> convert(Map<String, Object> rawData) {
+//        log.info("百度回调数据转换");
+//
+//        // 使用适配器转换数据格式
+//        Map<String, Object> convertedData = baiduAdapter.adaptCallbackData(rawData);
+//
+//        // 补充百度特有字段
+//        convertedData.put("platform", "baidu");
+//        convertedData.put("platformName", "百度");
+//
+//        // 处理点击ID(百度可能使用clk_id或logid)
+//        String clkId = MapUtil.getStr(rawData, "clk_id");
+//        String logId = MapUtil.getStr(rawData, "logid");
+//        String clickId = StrUtil.isNotBlank(clkId) ? clkId : logId;
+//
+//        convertedData.put("clickId", clickId);
+//        convertedData.put("originalClickId", clickId);
+//        convertedData.put("logId", clickId); // 百度回传需要logId
+//
+//        // 处理回调类型
+//        String callbackType = MapUtil.getStr(rawData, "callback_type", "click");
+//        convertedData.put("callbackType", callbackType);
+//
+//        // 处理时间戳
+//        Long timestamp = MapUtil.getLong(rawData, "timestamp");
+//        if (timestamp != null) {
+//            convertedData.put("timestamp", timestamp);
+//        }
+//
+//        // 处理计划ID
+//        String planId = MapUtil.getStr(rawData, "plan_id");
+//        if (StrUtil.isNotBlank(planId)) {
+//            convertedData.put("planId", planId);
+//        }
+//
+//        // 处理单元ID
+//        String unitId = MapUtil.getStr(rawData, "unit_id");
+//        if (StrUtil.isNotBlank(unitId)) {
+//            convertedData.put("unitId", unitId);
+//        }
+//
+//        log.info("百度数据转换完成:{}", convertedData);
+//        return convertedData;
+//    }
+//
+//    @Override
+//    protected void handle(Map<String, Object> convertedData) {
+//        log.info("百度回调业务处理");
+//
+//        Long siteId = MapUtil.getLong(convertedData, "siteId");
+//        String callbackType = MapUtil.getStr(convertedData, "callbackType");
+//
+//        // 根据回调类型处理不同的业务逻辑
+//        switch (callbackType) {
+//            case "impression":
+//            case "show":
+//                // 处理展示回调
+//                handleShowCallback(convertedData);
+//                break;
+//            case "click":
+//                // 处理点击回调
+//                handleClickCallback(convertedData);
+//                break;
+//            case "convert":
+//            case "conversion":
+//                // 处理转化回调
+//                handleConversionCallback(convertedData);
+//                break;
+//            default:
+//                // 默认处理
+//                handleDefaultCallback(convertedData);
+//                break;
+//        }
+//
+//        log.info("百度回调处理完成,站点ID:{}", siteId);
+//    }
+//
+//    /**
+//     * 处理展示回调
+//     */
+//    private void handleShowCallback(Map<String, Object> data) {
+//        log.info("处理百度展示回调");
+//        Long siteId = MapUtil.getLong(data, "siteId");
+//        Long impressionCount = MapUtil.getLong(data, "impressionCount", 1L);
+//
+//        // TODO: 更新站点展示数统计
+//        log.info("站点 {} 展示数 +{}", siteId, impressionCount);
+//    }
+//
+//    /**
+//     * 处理点击回调
+//     */
+//    private void handleClickCallback(Map<String, Object> data) {
+//        log.info("处理百度点击回调");
+//        Long siteId = MapUtil.getLong(data, "siteId");
+//        Long clickCount = MapUtil.getLong(data, "clickCount", 1L);
+//        Double cost = MapUtil.getDouble(data, "cost", 0.0);
+//
+//        // TODO: 更新站点点击数和花费统计
+//        log.info("站点 {} 点击数 +{},花费 +{}", siteId, clickCount, cost);
+//    }
+//
+//    /**
+//     * 处理转化回调
+//     */
+//    private void handleConversionCallback(Map<String, Object> data) {
+//        log.info("处理百度转化回调");
+//        Long siteId = MapUtil.getLong(data, "siteId");
+//        String logId = MapUtil.getStr(data, "logId");
+//
+//        // TODO: 记录转化数据
+//        log.info("站点 {} 转化,logId:{}", siteId, logId);
+//    }
+//
+//    /**
+//     * 处理默认回调
+//     */
+//    private void handleDefaultCallback(Map<String, Object> data) {
+//        log.info("处理百度默认回调");
+//        Long siteId = MapUtil.getLong(data, "siteId");
+//        Long impressionCount = MapUtil.getLong(data, "impressionCount");
+//        Long clickCount = MapUtil.getLong(data, "clickCount");
+//        Double cost = MapUtil.getDouble(data, "cost");
+//
+//        // 统一更新统计数据
+//        log.info("站点 {} 数据更新:展示={},点击={},花费={}",
+//                siteId, impressionCount, clickCount, cost);
+//    }
+//
+//    @Override
+//    protected Integer getAdvertiserType() {
+//        return AdvertiserTypeEnum.BAIDU.getCode();
+//    }
+//}

+ 191 - 0
fs-service/src/main/java/com/fs/newAdv/integration/handler/OceanEngineCallbackHandler.java

@@ -0,0 +1,191 @@
+//package com.fs.newAdv.integration.handler;
+//
+//import cn.hutool.core.map.MapUtil;
+//import cn.hutool.core.util.StrUtil;
+//import com.crm.adv.common.enums.AdvertiserTypeEnum;
+//import com.crm.adv.integration.adapter.OceanEngineAdapter;
+//import lombok.extern.slf4j.Slf4j;
+//import org.springframework.beans.factory.annotation.Autowired;
+//import org.springframework.stereotype.Component;
+//
+//import java.util.Map;
+//
+///**
+// * 巨量引擎回调处理器
+// * 基于模板方法模式,实现巨量引擎特定的回调处理逻辑
+// *
+// * @author zhangqin
+// * @date 2025-11-03
+// * @deprecated 该Handler已废弃!
+// *
+// * 原设计:处理广告平台的回调数据
+// * 新设计:广告平台通过URL参数传递数据,由LandingPageController接收并保存到adv_click_trace表
+// *
+// * 请使用ClickTraceService代替!
+// * @see com.crm.adv.service.IClickTraceService
+// */
+//@Deprecated
+//@Slf4j
+//@Component
+//public class OceanEngineCallbackHandler extends AbstractCallbackHandler {
+//
+//    @Autowired
+//    private OceanEngineAdapter oceanEngineAdapter;
+//
+//    @Override
+//    protected boolean validate(Map<String, Object> rawData) {
+//        log.info("巨量引擎回调数据验证");
+//
+//        // 验证必填字段
+//        if (MapUtil.isEmpty(rawData)) {
+//            log.error("回调数据为空");
+//            return false;
+//        }
+//
+//        // 验证点击ID(巨量引擎使用clickid字段)
+//        String clickId = MapUtil.getStr(rawData, "clickid");
+//        if (StrUtil.isBlank(clickId)) {
+//            log.error("缺少点击ID(clickid)");
+//            return false;
+//        }
+//
+//        // 验证站点ID
+//        if (!rawData.containsKey("site_id")) {
+//            log.error("缺少站点ID");
+//            return false;
+//        }
+//
+//        // 验证回调类型
+//        String callbackType = MapUtil.getStr(rawData, "callback_type");
+//        if (StrUtil.isBlank(callbackType)) {
+//            log.warn("未指定回调类型,使用默认类型");
+//        }
+//
+//        return true;
+//    }
+//
+//    @Override
+//    protected Map<String, Object> convert(Map<String, Object> rawData) {
+//        log.info("巨量引擎回调数据转换");
+//
+//        // 使用适配器转换数据格式
+//        Map<String, Object> convertedData = oceanEngineAdapter.adaptCallbackData(rawData);
+//
+//        // 补充巨量引擎特有字段
+//        convertedData.put("platform", "oceanengine");
+//        convertedData.put("platformName", "巨量引擎");
+//
+//        // 处理点击ID
+//        String clickId = MapUtil.getStr(rawData, "clickid");
+//        convertedData.put("clickId", clickId);
+//        convertedData.put("originalClickId", clickId);
+//
+//        // 处理回调类型
+//        String callbackType = MapUtil.getStr(rawData, "callback_type", "data");
+//        convertedData.put("callbackType", callbackType);
+//
+//        // 处理时间戳
+//        Long timestamp = MapUtil.getLong(rawData, "timestamp");
+//        if (timestamp != null) {
+//            convertedData.put("timestamp", timestamp);
+//        }
+//
+//        // 处理转化类型
+//        String convertType = MapUtil.getStr(rawData, "convert_type");
+//        if (StrUtil.isNotBlank(convertType)) {
+//            convertedData.put("convertType", convertType);
+//        }
+//
+//        log.info("巨量引擎数据转换完成:{}", convertedData);
+//        return convertedData;
+//    }
+//
+//    @Override
+//    protected void handle(Map<String, Object> convertedData) {
+//        log.info("巨量引擎回调业务处理");
+//
+//        Long siteId = MapUtil.getLong(convertedData, "siteId");
+//        String callbackType = MapUtil.getStr(convertedData, "callbackType");
+//
+//        // 根据回调类型处理不同的业务逻辑
+//        switch (callbackType) {
+//            case "show":
+//                // 处理展示回调
+//                handleShowCallback(convertedData);
+//                break;
+//            case "click":
+//                // 处理点击回调
+//                handleClickCallback(convertedData);
+//                break;
+//            case "convert":
+//                // 处理转化回调
+//                handleConvertCallback(convertedData);
+//                break;
+//            case "data":
+//            default:
+//                // 处理数据回调(默认)
+//                handleDataCallback(convertedData);
+//                break;
+//        }
+//
+//        log.info("巨量引擎回调处理完成,站点ID:{}", siteId);
+//    }
+//
+//    /**
+//     * 处理展示回调
+//     */
+//    private void handleShowCallback(Map<String, Object> data) {
+//        log.info("处理巨量引擎展示回调");
+//        Long siteId = MapUtil.getLong(data, "siteId");
+//        Long impressionCount = MapUtil.getLong(data, "impressionCount", 1L);
+//
+//        // TODO: 更新站点展示数统计
+//        log.info("站点 {} 展示数 +{}", siteId, impressionCount);
+//    }
+//
+//    /**
+//     * 处理点击回调
+//     */
+//    private void handleClickCallback(Map<String, Object> data) {
+//        log.info("处理巨量引擎点击回调");
+//        Long siteId = MapUtil.getLong(data, "siteId");
+//        Long clickCount = MapUtil.getLong(data, "clickCount", 1L);
+//        Double cost = MapUtil.getDouble(data, "cost", 0.0);
+//
+//        // TODO: 更新站点点击数和花费统计
+//        log.info("站点 {} 点击数 +{},花费 +{}", siteId, clickCount, cost);
+//    }
+//
+//    /**
+//     * 处理转化回调
+//     */
+//    private void handleConvertCallback(Map<String, Object> data) {
+//        log.info("处理巨量引擎转化回调");
+//        Long siteId = MapUtil.getLong(data, "siteId");
+//        String convertType = MapUtil.getStr(data, "convertType");
+//
+//        // TODO: 记录转化数据
+//        log.info("站点 {} 转化,类型:{}", siteId, convertType);
+//    }
+//
+//    /**
+//     * 处理数据回调(默认)
+//     */
+//    private void handleDataCallback(Map<String, Object> data) {
+//        log.info("处理巨量引擎数据回调");
+//        Long siteId = MapUtil.getLong(data, "siteId");
+//        Long impressionCount = MapUtil.getLong(data, "impressionCount");
+//        Long clickCount = MapUtil.getLong(data, "clickCount");
+//        Double cost = MapUtil.getDouble(data, "cost");
+//
+//        // 统一更新统计数据
+//        log.info("站点 {} 数据更新:展示={},点击={},花费={}",
+//                siteId, impressionCount, clickCount, cost);
+//    }
+//
+//    @Override
+//    protected Integer getAdvertiserType() {
+//        return AdvertiserTypeEnum.OCEAN_ENGINE.getCode();
+//    }
+//}
+//

+ 50 - 0
fs-service/src/main/java/com/fs/newAdv/integration/strategy/BaiduCallbackStrategy.java

@@ -0,0 +1,50 @@
+package com.fs.newAdv.integration.strategy;
+
+import cn.hutool.core.map.MapUtil;
+import com.fs.common.utils.SignatureUtil;
+import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 百度回调处理策略
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Component
+public class BaiduCallbackStrategy implements ICallbackStrategy {
+
+    /**
+     * 百度密钥(实际应从配置或数据库读取)
+     */
+    private static final String BAIDU_SECRET = "baidu_secret_key";
+
+    @Override
+    public void handleCallback(Map<String, Object> callbackData) {
+        log.info("百度回调数据处理:{}", callbackData);
+
+        Long siteId = MapUtil.getLong(callbackData, "siteId");
+        Long impressionCount = MapUtil.getLong(callbackData, "impressionCount");
+        Long clickCount = MapUtil.getLong(callbackData, "clickCount");
+
+        // 更新站点统计数据
+        log.info("更新站点 {} 统计数据:展示数 {},点击数 {}", siteId, impressionCount, clickCount);
+
+        // 具体业务逻辑在Service层实现
+    }
+
+    @Override
+    public boolean verifySign(Map<String, String> params, String sign) {
+        return SignatureUtil.verifySign(params, sign, BAIDU_SECRET);
+    }
+
+    @Override
+    public Integer getAdvertiserType() {
+        return AdvertiserTypeEnum.BAIDU.getCode();
+    }
+}
+

+ 35 - 0
fs-service/src/main/java/com/fs/newAdv/integration/strategy/ICallbackStrategy.java

@@ -0,0 +1,35 @@
+package com.fs.newAdv.integration.strategy;
+
+import java.util.Map;
+
+/**
+ * 回调处理策略接口(策略模式)
+ * 定义不同广告平台的回调处理策略
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+public interface ICallbackStrategy {
+
+    /**
+     * 处理回调数据
+     *
+     * @param callbackData 回调数据
+     */
+    void handleCallback(Map<String, Object> callbackData);
+
+    /**
+     * 验证签名
+     *
+     * @param params 参数
+     * @param sign 签名
+     * @return 是否验证通过
+     */
+    boolean verifySign(Map<String, String> params, String sign);
+
+    /**
+     * 获取广告商类型
+     */
+    Integer getAdvertiserType();
+}
+

+ 47 - 0
fs-service/src/main/java/com/fs/newAdv/integration/strategy/OceanEngineCallbackStrategy.java

@@ -0,0 +1,47 @@
+package com.fs.newAdv.integration.strategy;
+
+import cn.hutool.core.map.MapUtil;
+import com.fs.common.utils.SignatureUtil;
+import com.fs.newAdv.enums.AdvertiserTypeEnum;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import java.util.Map;
+
+/**
+ * 巨量引擎回调处理策略
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Component
+public class OceanEngineCallbackStrategy implements ICallbackStrategy {
+
+    /**
+     * 巨量引擎密钥(实际应从配置或数据库读取)
+     */
+    private static final String OCEAN_ENGINE_SECRET = "oceanengine_secret_key";
+
+    @Override
+    public void handleCallback(Map<String, Object> callbackData) {
+        log.info("巨量引擎回调数据处理:{}", callbackData);
+
+        Long siteId = MapUtil.getLong(callbackData, "siteId");
+        Long impressionCount = MapUtil.getLong(callbackData, "impressionCount");
+        Long clickCount = MapUtil.getLong(callbackData, "clickCount");
+
+        log.info("更新站点 {} 统计数据:展示数 {},点击数 {}", siteId, impressionCount, clickCount);
+    }
+
+    @Override
+    public boolean verifySign(Map<String, String> params, String sign) {
+        return SignatureUtil.verifySign(params, sign, OCEAN_ENGINE_SECRET);
+    }
+
+    @Override
+    public Integer getAdvertiserType() {
+        return AdvertiserTypeEnum.OCEAN_ENGINE.getCode();
+    }
+}
+

+ 17 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/AdvertiserMapper.java

@@ -0,0 +1,17 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.Advertiser;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 广告商表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface AdvertiserMapper extends BaseMapper<Advertiser> {
+
+}
+

+ 17 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/AlertLogMapper.java

@@ -0,0 +1,17 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.AlertLog;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 告警记录表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface AlertLogMapper extends BaseMapper<AlertLog> {
+
+}
+

+ 17 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/ApiCallLogMapper.java

@@ -0,0 +1,17 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.ApiCallLog;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * API调用日志表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface ApiCallLogMapper extends BaseMapper<ApiCallLog> {
+
+}
+

+ 17 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/CallbackAccountMapper.java

@@ -0,0 +1,17 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.CallbackAccount;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 回传账号表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface CallbackAccountMapper extends BaseMapper<CallbackAccount> {
+
+}
+

+ 17 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/CallbackLogMapper.java

@@ -0,0 +1,17 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.CallbackLog;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 第三方回调记录表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface CallbackLogMapper extends BaseMapper<CallbackLog> {
+
+}
+

+ 16 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/ClickTraceMapper.java

@@ -0,0 +1,16 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.ClickTrace;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 点击追踪表Mapper
+ *
+ * @author zhangqin
+ * @date 2025-11-04
+ */
+@Mapper
+public interface ClickTraceMapper extends BaseMapper<ClickTrace> {
+
+}

+ 25 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/ConversionLogMapper.java

@@ -0,0 +1,25 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.ConversionLog;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 转化回传记录表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface ConversionLogMapper extends BaseMapper<ConversionLog> {
+
+    /**
+     * 查询待回传的转化记录(失败且重试次数小于3)
+     */
+    @Select("SELECT * FROM adv_conversion_log WHERE callback_status IN (0, 2) AND retry_count < 3 LIMIT 100")
+    List<ConversionLog> selectPendingConversions();
+}
+

+ 17 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/ConversionTargetMapper.java

@@ -0,0 +1,17 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.ConversionTarget;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 转化目标表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface ConversionTargetMapper extends BaseMapper<ConversionTarget> {
+
+}
+

+ 17 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/DomainMapper.java

@@ -0,0 +1,17 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.Domain;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 域名管理Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-06
+ */
+@Mapper
+public interface DomainMapper extends BaseMapper<Domain> {
+
+}
+

+ 17 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/LandingPageTemplateMapper.java

@@ -0,0 +1,17 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.LandingPageTemplate;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 投放页面模板Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-06
+ */
+@Mapper
+public interface LandingPageTemplateMapper extends BaseMapper<LandingPageTemplate> {
+
+}
+

+ 16 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/LeadMapper.java

@@ -0,0 +1,16 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.Lead;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 线索Mapper
+ *
+ * @author zhangqin
+ * @date 2025-11-05
+ */
+@Mapper
+public interface LeadMapper extends BaseMapper<Lead> {
+}
+

+ 17 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/LeadSourceMapper.java

@@ -0,0 +1,17 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.LeadSource;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 线索来源表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface LeadSourceMapper extends BaseMapper<LeadSource> {
+
+}
+

+ 17 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/OperationLogMapper.java

@@ -0,0 +1,17 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.OperationLog;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 系统操作日志表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface OperationLogMapper extends BaseMapper<OperationLog> {
+
+}
+

+ 25 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/PromotionAccountMapper.java

@@ -0,0 +1,25 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.PromotionAccount;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 推广账号表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface PromotionAccountMapper extends BaseMapper<PromotionAccount> {
+
+    /**
+     * 查询开启API读取的推广账号
+     */
+    @Select("SELECT * FROM adv_promotion_account WHERE api_switch = 1")
+    List<PromotionAccount> selectApiEnabledAccounts();
+}
+

+ 26 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/ScheduleTaskLogMapper.java

@@ -0,0 +1,26 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.ScheduleTaskLog;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 定时任务执行记录表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface ScheduleTaskLogMapper extends BaseMapper<ScheduleTaskLog> {
+
+    /**
+     * 查询需要补偿的任务(执行失败或超时)
+     */
+    @Select("SELECT * FROM adv_schedule_task_log WHERE (execute_status = 3 AND retry_count < 3) " +
+            "OR (execute_status = 1 AND TIMESTAMPDIFF(HOUR, start_time, NOW()) > 2)")
+    List<ScheduleTaskLog> selectTasksNeedCompensation();
+}
+

+ 25 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/SiteMapper.java

@@ -0,0 +1,25 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.Site;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Select;
+
+import java.util.List;
+
+/**
+ * 站点管理表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface SiteMapper extends BaseMapper<Site> {
+
+    /**
+     * 查询配置了回传的站点列表
+     */
+    @Select("SELECT * FROM adv_site WHERE config_callback = 1")
+    List<Site> selectCallbackEnabledSites();
+}
+

+ 17 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/SiteStatisticsDailyMapper.java

@@ -0,0 +1,17 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.SiteStatisticsDaily;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 站点日统计归档表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface SiteStatisticsDailyMapper extends BaseMapper<SiteStatisticsDaily> {
+
+}
+

+ 30 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/SiteStatisticsMapper.java

@@ -0,0 +1,30 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.SiteStatistics;
+import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Update;
+
+/**
+ * 站点统计表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface SiteStatisticsMapper extends BaseMapper<SiteStatistics> {
+
+    /**
+     * 增加展示数
+     */
+    @Update("UPDATE adv_site_statistics SET impression_count = impression_count + #{count} WHERE site_id = #{siteId}")
+    int incrementImpressionCount(@Param("siteId") Long siteId, @Param("count") Long count);
+
+    /**
+     * 增加点击数
+     */
+    @Update("UPDATE adv_site_statistics SET click_count = click_count + #{count} WHERE site_id = #{siteId}")
+    int incrementClickCount(@Param("siteId") Long siteId, @Param("count") Long count);
+}
+

+ 17 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/SiteStatisticsMonthlyMapper.java

@@ -0,0 +1,17 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.SiteStatisticsMonthly;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 站点月统计归档表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface SiteStatisticsMonthlyMapper extends BaseMapper<SiteStatisticsMonthly> {
+
+}
+

+ 17 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/SiteStatisticsWeeklyMapper.java

@@ -0,0 +1,17 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.SiteStatisticsWeekly;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 站点周统计归档表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface SiteStatisticsWeeklyMapper extends BaseMapper<SiteStatisticsWeekly> {
+
+}
+

+ 17 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/SyncLogMapper.java

@@ -0,0 +1,17 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.SyncLog;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 数据同步日志表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface SyncLogMapper extends BaseMapper<SyncLog> {
+
+}
+

+ 17 - 0
fs-service/src/main/java/com/fs/newAdv/mapper/TrackingLinkMapper.java

@@ -0,0 +1,17 @@
+package com.fs.newAdv.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.newAdv.entity.TrackingLink;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 监测链接表 Mapper接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Mapper
+public interface TrackingLinkMapper extends BaseMapper<TrackingLink> {
+
+}
+

+ 116 - 0
fs-service/src/main/java/com/fs/newAdv/mq/consumer/ConversionDLQConsumer.java

@@ -0,0 +1,116 @@
+package com.fs.newAdv.mq.consumer;
+
+import com.fs.newAdv.entity.ConversionLog;
+import com.fs.newAdv.mapper.ConversionLogMapper;
+import com.fs.newAdv.mq.message.ConversionMessage;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.spring.annotation.RocketMQMessageListener;
+import org.apache.rocketmq.spring.core.RocketMQListener;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+/**
+ * 转化消息死信队列消费者
+ * 处理超过最大重试次数的失败消息
+ *
+ * @author zhangqin
+ * @date 2025-11-05
+ */
+@Slf4j
+@Component
+@RocketMQMessageListener(
+    // 死信队列Topic格式:%DLQ%消费组名称
+    topic = "%DLQ%event-feedback-consumer-group",
+    consumerGroup = "dlq-consumer-group"
+)
+public class ConversionDLQConsumer implements RocketMQListener<ConversionMessage> {
+
+    @Autowired
+    private ConversionLogMapper conversionLogMapper;
+
+    /**
+     * 处理死信队列消息
+     *
+     * @param message 转化消息
+     */
+    @Override
+    public void onMessage(ConversionMessage message) {
+        String clickId = message.getClickId();
+        String eventType = message.getEventType();
+
+        log.error("========== 转化消息进入死信队列 ==========");
+        log.error("clickId={}, eventType={}, advertiser={}, messageId={}",
+                  clickId, eventType, message.getAdvertiser(), message.getMessageId());
+        log.error("详细信息: siteId={}, value={}, leadId={}, conversionLogId={}",
+                  message.getSiteId(), message.getValue(),
+                  message.getLeadId(), message.getConversionLogId());
+        log.error("==========================================");
+
+        try {
+            // 1. 记录到数据库,标记为最终失败
+            recordFailedConversion(message);
+
+            // 2. 发送告警通知(预留接口,后期接入企微)
+            sendAlert(message);
+
+        } catch (Exception e) {
+            log.error("处理死信队列消息异常 | clickId={}, eventType={}", clickId, eventType, e);
+        }
+    }
+
+    /**
+     * 记录失败的转化到数据库
+     *
+     * @param message 转化消息
+     */
+    private void recordFailedConversion(ConversionMessage message) {
+        try {
+            if (message.getConversionLogId() != null) {
+                // 更新现有记录
+                ConversionLog conversionLog = new ConversionLog();
+                conversionLog.setId(message.getConversionLogId());
+                conversionLog.setCallbackStatus(2); // 2=失败
+                conversionLog.setSuccessTime(null);
+                conversionLog.setErrorMsg("超过最大重试次数,进入死信队列,需人工介入");
+                conversionLogMapper.updateById(conversionLog);
+
+                log.info("更新转化日志状态为死信 | conversionLogId={}", message.getConversionLogId());
+            }
+        } catch (Exception e) {
+            log.error("记录失败转化异常 | clickId={}", message.getClickId(), e);
+        }
+    }
+
+    /**
+     * 发送告警通知
+     * 预留接口,后期接入企业微信/钉钉等
+     *
+     * @param message 转化消息
+     */
+    private void sendAlert(ConversionMessage message) {
+        // TODO: 后期接入企业微信告警
+        log.warn("【告警】转化回传失败 | clickId={}, eventType={}, advertiser={}",
+                 message.getClickId(), message.getEventType(), message.getAdvertiser());
+
+        // 告警内容示例:
+        // String alertMessage = String.format(
+        //     "【CRM广告系统告警】\n" +
+        //     "类型:转化回传失败\n" +
+        //     "广告商:%s\n" +
+        //     "点击ID:%s\n" +
+        //     "事件类型:%s\n" +
+        //     "站点ID:%s\n" +
+        //     "时间:%s\n" +
+        //     "备注:超过最大重试次数,需人工介入处理",
+        //     message.getAdvertiser(),
+        //     message.getClickId(),
+        //     message.getEventType(),
+        //     message.getSiteId(),
+        //     LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
+        // );
+        //
+        // // 发送到企业微信
+        // wechatAlertService.send(alertMessage);
+    }
+}
+

+ 204 - 0
fs-service/src/main/java/com/fs/newAdv/mq/consumer/ConversionMessageConsumer.java

@@ -0,0 +1,204 @@
+package com.fs.newAdv.mq.consumer;
+
+import cn.hutool.core.util.StrUtil;
+import com.fs.newAdv.entity.ConversionLog;
+import com.fs.newAdv.mapper.ConversionLogMapper;
+import com.fs.newAdv.mq.message.ConversionMessage;
+import com.fs.newAdv.service.IConversionService;
+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.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.time.LocalDateTime;
+import java.util.concurrent.TimeUnit;
+
+/**
+ * 转化消息消费者
+ * 多线程并发消费,支持自动重试
+ *
+ * @author zhangqin
+ * @date 2025-11-05
+ */
+@Slf4j
+@Component
+@RocketMQMessageListener(
+    topic = "event-feedback",
+    consumerGroup = "event-feedback-consumer-group",
+    // 并发消费模式(多线程并发消费,线程数由RocketMQ自动管理)
+    consumeMode = ConsumeMode.CONCURRENTLY,
+    // 最大重试次数(RocketMQ默认16次)
+    maxReconsumeTimes = 16
+)
+public class ConversionMessageConsumer implements RocketMQListener<ConversionMessage> {
+
+    @Autowired
+    private IConversionService conversionService;
+
+    @Autowired
+    private StringRedisTemplate redisTemplate;
+
+    @Autowired
+    private ConversionLogMapper conversionLogMapper;
+
+    /**
+     * 初始化检查
+     */
+    @PostConstruct
+    public void init() {
+        log.info("========================================");
+        log.info("RocketMQ Consumer 初始化成功");
+        log.info("Topic: event-feedback");
+        log.info("Consumer Group: event-feedback-consumer-group");
+        log.info("Consume Mode: CONCURRENTLY");
+        log.info("Max Reconsume Times: 16");
+        log.info("========================================");
+    }
+
+    /**
+     * 消费转化消息
+     *
+     * @param message 转化消息
+     */
+    @Override
+    public void onMessage(ConversionMessage message) {
+        String messageId = message.getMessageId();
+        String clickId = message.getClickId();
+        String eventType = message.getEventType();
+
+        log.info("接收到转化消息 | clickId={}, eventType={}, messageId={}",
+                 clickId, eventType, messageId);
+
+        try {
+            // 1. 幂等性校验(防止重复消费)
+            if (!checkIdempotent(messageId)) {
+                log.warn("消息已处理,跳过 | messageId={}", messageId);
+                return;
+            }
+
+            // 2. 调用广告平台API回传
+            boolean success = callAdvertiserApi(message);
+
+            if (success) {
+                // 3. 标记为已处理
+                markAsProcessed(messageId);
+
+                // 4. 更新转化日志状态
+                updateConversionLogStatus(message, true);
+
+                log.info("转化回传成功 | clickId={}, eventType={}", clickId, eventType);
+            } else {
+                // 抛出异常触发RocketMQ重试
+                throw new RuntimeException("回传失败,触发重试");
+            }
+
+        } catch (Exception e) {
+            log.error("处理转化消息失败 | clickId={}, eventType={}, error={}",
+                      clickId, eventType, e.getMessage(), e);
+
+            // 更新转化日志状态为失败
+            updateConversionLogStatus(message, false);
+
+            // 抛出异常,RocketMQ会自动重试
+            throw new RuntimeException("处理转化消息失败: " + e.getMessage(), e);
+        }
+    }
+
+    /**
+     * 幂等性校验(Redis)
+     *
+     * @param messageId 消息ID
+     * @return true=未处理,false=已处理
+     */
+    private boolean checkIdempotent(String messageId) {
+        String key = "conversion:processed:" + messageId;
+        Boolean success = redisTemplate.opsForValue()
+            .setIfAbsent(key, "1", 7, TimeUnit.DAYS);
+        return Boolean.TRUE.equals(success);
+    }
+
+    /**
+     * 标记消息已处理
+     *
+     * @param messageId 消息ID
+     */
+    private void markAsProcessed(String messageId) {
+        String key = "conversion:processed:" + messageId;
+        redisTemplate.opsForValue().set(key, "1", 7, TimeUnit.DAYS);
+    }
+
+    /**
+     * 调用广告平台API
+     *
+     * @param message 转化消息
+     * @return 是否成功
+     */
+    private boolean callAdvertiserApi(ConversionMessage message) {
+        String advertiser = message.getAdvertiser();
+
+        if (StrUtil.isBlank(advertiser)) {
+            log.error("广告商名称为空 | clickId={}", message.getClickId());
+            return false;
+        }
+
+        switch (advertiser.toUpperCase()) {
+            case "BAIDU":
+                return conversionService.reportToBaidu(
+                    message.getSiteId(),
+                    message.getEventType(),
+                    message.getClickId(),
+                    message.getValue()
+                );
+
+            case "OCEANENGINE":
+                return conversionService.reportToOceanEngine(
+                    message.getSiteId(),
+                    message.getEventType(),
+                    message.getClickId(),
+                    message.getValue()
+                );
+
+            case "SINA":
+                // TODO: 实现新浪API调用
+                log.warn("新浪平台回传暂未实现 | clickId={}", message.getClickId());
+                return false;
+
+            case "GDT":
+                // TODO: 实现广点通API调用
+                log.warn("广点通平台回传暂未实现 | clickId={}", message.getClickId());
+                return false;
+
+            default:
+                log.error("未知广告商 | advertiser={}, clickId={}",
+                          advertiser, message.getClickId());
+                return false;
+        }
+    }
+
+    /**
+     * 更新转化日志状态
+     *
+     * @param message 转化消息
+     * @param success 是否成功
+     */
+    private void updateConversionLogStatus(ConversionMessage message, boolean success) {
+        try {
+            if (message.getConversionLogId() != null) {
+                ConversionLog conversionLog = new ConversionLog();
+                conversionLog.setId(message.getConversionLogId());
+                conversionLog.setCallbackStatus(success ? 1 : 2); // 1=成功,2=失败
+                conversionLog.setSuccessTime(success ? LocalDateTime.now() : null);
+                conversionLog.setErrorMsg(success ? null : "回传失败");
+                conversionLogMapper.updateById(conversionLog);
+            }
+        } catch (Exception e) {
+            log.error("更新转化日志状态失败 | conversionLogId={}",
+                      message.getConversionLogId(), e);
+        }
+    }
+}
+

+ 65 - 0
fs-service/src/main/java/com/fs/newAdv/mq/message/ConversionMessage.java

@@ -0,0 +1,65 @@
+package com.fs.newAdv.mq.message;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 转化回传消息实体
+ *
+ * @author zhangqin
+ * @date 2025-11-05
+ */
+@Data
+public class ConversionMessage implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 站点ID
+     */
+    private Long siteId;
+
+    /**
+     * 点击ID(广告平台提供)
+     */
+    private String clickId;
+
+    /**
+     * 广告商名称:BAIDU, OCEANENGINE, SINA, GDT
+     */
+    private String advertiser;
+
+    /**
+     * 事件类型:SUBMIT_FORM(提交表单), REGISTER(注册), PAY(支付)等
+     */
+    private String eventType;
+
+    /**
+     * 转化价值(可选)
+     */
+    private Double value;
+
+    /**
+     * 消息时间戳
+     */
+    private Long timestamp;
+
+    /**
+     * 消息ID(用于幂等性校验)
+     * 格式:clickId_eventType_timestamp
+     */
+    private String messageId;
+
+    /**
+     * 线索ID(可选,用于关联业务数据)
+     */
+    private Long leadId;
+
+    /**
+     * 转化日志ID(可选,用于更新转化记录状态)
+     */
+    private Long conversionLogId;
+}
+
+

+ 148 - 0
fs-service/src/main/java/com/fs/newAdv/mq/producer/ConversionMessageProducer.java

@@ -0,0 +1,148 @@
+package com.fs.newAdv.mq.producer;
+
+import cn.hutool.core.util.StrUtil;
+
+import com.fs.newAdv.mq.message.ConversionMessage;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.rocketmq.client.producer.SendCallback;
+import org.apache.rocketmq.client.producer.SendResult;
+import org.apache.rocketmq.spring.core.RocketMQTemplate;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+
+/**
+ * 转化消息生产者
+ *
+ * @author zhangqin
+ * @date 2025-11-05
+ */
+@Slf4j
+@Component
+public class ConversionMessageProducer {
+
+    @Autowired
+    private RocketMQTemplate rocketMQTemplate;
+
+    /**
+     * 初始化检查
+     */
+    @PostConstruct
+    public void init() {
+        try {
+            log.info("========================================");
+            log.info("RocketMQ Producer 初始化成功");
+            log.info("Producer Group: {}", rocketMQTemplate.getProducer().getProducerGroup());
+            log.info("NameServer: {}", rocketMQTemplate.getProducer().getNamesrvAddr());
+            log.info("========================================");
+        } catch (Exception e) {
+            log.error("RocketMQ Producer 初始化失败", e);
+        }
+    }
+
+    /**
+     * 发送转化回传消息(异步)
+     *
+     * @param message 转化消息
+     */
+    public void sendConversionMessage(ConversionMessage message) {
+        try {
+            // 1. 设置消息ID(用于幂等性校验)
+            if (StrUtil.isBlank(message.getMessageId())) {
+                message.setMessageId(buildMessageId(message));
+            }
+
+            // 2. 设置时间戳
+            if (message.getTimestamp() == null) {
+                message.setTimestamp(System.currentTimeMillis());
+            }
+
+            // 3. 异步发送消息
+            rocketMQTemplate.asyncSend(
+                "event-feedback",
+                message,
+                new SendCallback() {
+                    @Override
+                    public void onSuccess(SendResult sendResult) {
+                        log.info("转化消息发送成功 | clickId={}, eventType={}, msgId={}, queueId={}",
+                                 message.getClickId(),
+                                 message.getEventType(),
+                                 sendResult.getMsgId(),
+                                 sendResult.getMessageQueue().getQueueId());
+                    }
+
+                    @Override
+                    public void onException(Throwable e) {
+                        log.error("转化消息发送失败 | clickId={}, eventType={}, error={}",
+                                  message.getClickId(),
+                                  message.getEventType(),
+                                  e.getMessage(),
+                                  e);
+                        // TODO: 记录到数据库,后续补偿
+                    }
+                }
+            );
+
+        } catch (Exception e) {
+            log.error("发送转化消息异常 | clickId={}, eventType={}",
+                      message.getClickId(),
+                      message.getEventType(),
+                      e);
+        }
+    }
+
+    /**
+     * 发送转化回传消息(同步,用于测试或关键场景)
+     *
+     * @param message 转化消息
+     * @return 是否发送成功
+     */
+    public boolean sendConversionMessageSync(ConversionMessage message) {
+        try {
+            // 1. 设置消息ID和时间戳
+            if (StrUtil.isBlank(message.getMessageId())) {
+                message.setMessageId(buildMessageId(message));
+            }
+            if (message.getTimestamp() == null) {
+                message.setTimestamp(System.currentTimeMillis());
+            }
+
+            // 2. 同步发送消息
+            SendResult sendResult = rocketMQTemplate.syncSend(
+                "event-feedback",
+                message
+            );
+
+            log.info("转化消息同步发送成功 | clickId={}, eventType={}, msgId={}",
+                     message.getClickId(),
+                     message.getEventType(),
+                     sendResult.getMsgId());
+
+            return true;
+
+        } catch (Exception e) {
+            log.error("转化消息同步发送失败 | clickId={}, eventType={}",
+                      message.getClickId(),
+                      message.getEventType(),
+                      e);
+            return false;
+        }
+    }
+
+    /**
+     * 构建消息ID(用于幂等性)
+     * 格式:clickId_eventType_timestamp
+     *
+     * @param message 转化消息
+     * @return 消息ID
+     */
+    private String buildMessageId(ConversionMessage message) {
+        return String.format("%s_%s_%d",
+                             message.getClickId(),
+                             message.getEventType(),
+                             System.currentTimeMillis());
+    }
+}
+
+

+ 46 - 0
fs-service/src/main/java/com/fs/newAdv/service/IAdvertiserService.java

@@ -0,0 +1,46 @@
+package com.fs.newAdv.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.newAdv.entity.Advertiser;
+
+
+/**
+ * 广告商服务接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+public interface IAdvertiserService {
+
+    /**
+     * 分页查询广告商列表
+     */
+    IPage<Advertiser> page(Page<Advertiser> page, String advertiserName, Integer type, Integer enabled);
+
+    /**
+     * 根据ID查询广告商
+     */
+    Advertiser getById(Long id);
+
+    /**
+     * 创建广告商
+     */
+    boolean create(Advertiser advertiser);
+
+    /**
+     * 更新广告商
+     */
+    boolean update(Advertiser advertiser);
+
+    /**
+     * 删除广告商
+     */
+    boolean delete(Long id);
+
+    /**
+     * 批量删除广告商
+     */
+    boolean batchDelete(Long[] ids);
+}
+

+ 46 - 0
fs-service/src/main/java/com/fs/newAdv/service/ICallbackAccountService.java

@@ -0,0 +1,46 @@
+package com.fs.newAdv.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.newAdv.entity.CallbackAccount;
+
+
+/**
+ * 回传账号服务接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+public interface ICallbackAccountService {
+
+    /**
+     * 分页查询回传账号列表
+     */
+    IPage<CallbackAccount> page(Page<CallbackAccount> page, String accountName);
+
+    /**
+     * 根据ID查询回传账号
+     */
+    CallbackAccount getById(Long id);
+
+    /**
+     * 创建回传账号
+     */
+    boolean create(CallbackAccount callbackAccount);
+
+    /**
+     * 更新回传账号
+     */
+    boolean update(CallbackAccount callbackAccount);
+
+    /**
+     * 删除回传账号
+     */
+    boolean delete(Long id);
+
+    /**
+     * 批量删除回传账号
+     */
+    boolean batchDelete(Long[] ids);
+}
+

+ 33 - 0
fs-service/src/main/java/com/fs/newAdv/service/ICallbackService.java

@@ -0,0 +1,33 @@
+package com.fs.newAdv.service;
+
+import java.util.Map;
+
+/**
+ * 回调服务接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+public interface ICallbackService {
+
+    /**
+     * 处理第三方平台回调
+     *
+     * @param advertiserType 广告商类型
+     * @param callbackData 回调数据
+     * @param sign 签名
+     * @return 处理结果
+     */
+    boolean handleCallback(Integer advertiserType, Map<String, Object> callbackData, String sign);
+
+    /**
+     * 更新站点统计数据
+     *
+     * @param siteId 站点ID
+     * @param impressionCount 展示数
+     * @param clickCount 点击数
+     * @param cost 花费
+     */
+    void updateSiteStatistics(Long siteId, Long impressionCount, Long clickCount, Double cost);
+}
+

+ 68 - 0
fs-service/src/main/java/com/fs/newAdv/service/IClickTraceService.java

@@ -0,0 +1,68 @@
+package com.fs.newAdv.service;
+
+
+import com.fs.newAdv.entity.ClickTrace;
+
+import java.util.Map;
+
+/**
+ * 点击追踪服务接口
+ *
+ * @author zhangqin
+ * @date 2025-11-04
+ */
+public interface IClickTraceService {
+
+    /**
+     * 保存点击追踪记录
+     *
+     * @param siteId    站点ID
+     * @param params    URL参数
+     * @param visitorId 访客ID
+     * @param ip        IP地址
+     * @param userAgent User-Agent
+     * @return 追踪记录
+     */
+    ClickTrace saveClickTrace(Long siteId, Map<String, String> params,
+                              String visitorId, String ip, String userAgent);
+
+    /**
+     * 关联用户ID
+     *
+     * @param traceId 追踪ID
+     * @param userId  用户ID
+     */
+    void bindUser(String traceId, Long userId);
+
+    /**
+     * 根据访客ID查询最近的追踪记录
+     *
+     * @param visitorId 访客ID
+     * @return 追踪记录
+     */
+    ClickTrace getLatestByVisitorId(String visitorId);
+
+    /**
+     * 根据用户ID查询最近的追踪记录
+     *
+     * @param userId 用户ID
+     * @return 追踪记录
+     */
+    ClickTrace getLatestByUserId(Long userId);
+
+    /**
+     * 标记为已转化
+     *
+     * @param traceId 追踪记录ID
+     */
+    void markAsConverted(Long traceId);
+
+    /**
+     * 根据traceId查询
+     *
+     * @param traceId 追踪ID
+     * @return 追踪记录
+     */
+    ClickTrace getByTraceId(String traceId);
+}
+

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

@@ -0,0 +1,34 @@
+package com.fs.newAdv.service;
+
+/**
+ * 转化回传服务接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ * @updated 2025-11-05 使用clickId参数直接回传
+ */
+public interface IConversionService {
+
+    /**
+     * 回传转化数据到巨量引擎
+     *
+     * @param siteId 站点ID
+     * @param eventType 事件类型
+     * @param clickId 点击ID(广告平台提供)
+     * @param value 转化价值
+     * @return 是否成功
+     */
+    boolean reportToOceanEngine(Long siteId, String eventType, String clickId, Double value);
+
+    /**
+     * 回传转化数据到百度
+     *
+     * @param siteId 站点ID
+     * @param eventType 事件类型
+     * @param clickId 点击ID(广告平台提供)
+     * @param value 转化价值
+     * @return 是否成功
+     */
+    boolean reportToBaidu(Long siteId, String eventType, String clickId, Double value);
+}
+

+ 16 - 0
fs-service/src/main/java/com/fs/newAdv/service/IDomainService.java

@@ -0,0 +1,16 @@
+package com.fs.newAdv.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.newAdv.entity.Domain;
+
+
+/**
+ * 域名管理Service接口
+ *
+ * @author zhangqin
+ * @date 2025-11-06
+ */
+public interface IDomainService extends IService<Domain> {
+
+}
+

+ 16 - 0
fs-service/src/main/java/com/fs/newAdv/service/ILandingPageTemplateService.java

@@ -0,0 +1,16 @@
+package com.fs.newAdv.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.newAdv.entity.LandingPageTemplate;
+
+
+/**
+ * 投放页面模板Service接口
+ *
+ * @author zhangqin
+ * @date 2025-11-06
+ */
+public interface ILandingPageTemplateService extends IService<LandingPageTemplate> {
+
+}
+

+ 23 - 0
fs-service/src/main/java/com/fs/newAdv/service/ILeadService.java

@@ -0,0 +1,23 @@
+package com.fs.newAdv.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.newAdv.entity.Lead;
+
+
+/**
+ * 线索服务接口
+ *
+ * @author zhangqin
+ * @date 2025-11-05
+ */
+public interface ILeadService extends IService<Lead> {
+
+    /**
+     * 保存线索并触发转化事件
+     *
+     * @param lead 线索信息
+     * @return 保存后的线索
+     */
+    Lead saveLeadAndTriggerConversion(Lead lead);
+}
+

+ 46 - 0
fs-service/src/main/java/com/fs/newAdv/service/ILeadSourceService.java

@@ -0,0 +1,46 @@
+package com.fs.newAdv.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.newAdv.entity.LeadSource;
+
+
+/**
+ * 线索来源服务接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+public interface ILeadSourceService {
+
+    /**
+     * 分页查询线索来源列表
+     */
+    IPage<LeadSource> page(Page<LeadSource> page, String sourceName, Integer distributeType);
+
+    /**
+     * 根据ID查询线索来源
+     */
+    LeadSource getById(Long id);
+
+    /**
+     * 创建线索来源
+     */
+    boolean create(LeadSource leadSource);
+
+    /**
+     * 更新线索来源
+     */
+    boolean update(LeadSource leadSource);
+
+    /**
+     * 删除线索来源
+     */
+    boolean delete(Long id);
+
+    /**
+     * 批量删除线索来源
+     */
+    boolean batchDelete(Long[] ids);
+}
+

+ 66 - 0
fs-service/src/main/java/com/fs/newAdv/service/IPromotionAccountService.java

@@ -0,0 +1,66 @@
+package com.fs.newAdv.service;
+
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.newAdv.entity.PromotionAccount;
+
+
+/**
+ * 推广账号服务接口
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+public interface IPromotionAccountService {
+
+    /**
+     * 分页查询推广账号列表
+     *
+     * @param page 分页参数
+     * @param accountName 账号名称(模糊查询)
+     * @param advertiserId 广告商ID
+     * @return 分页结果
+     */
+    IPage<PromotionAccount> page(Page<PromotionAccount> page, String accountName, Long advertiserId);
+
+    /**
+     * 根据ID查询推广账号
+     *
+     * @param id 账号ID
+     * @return 推广账号
+     */
+    PromotionAccount getById(Long id);
+
+    /**
+     * 创建推广账号
+     *
+     * @param account 推广账号
+     * @return 是否成功
+     */
+    boolean create(PromotionAccount account);
+
+    /**
+     * 更新推广账号
+     *
+     * @param account 推广账号
+     * @return 是否成功
+     */
+    boolean update(PromotionAccount account);
+
+    /**
+     * 删除推广账号
+     *
+     * @param id 账号ID
+     * @return 是否成功
+     */
+    boolean delete(Long id);
+
+    /**
+     * 批量删除推广账号
+     *
+     * @param ids 账号ID列表
+     * @return 是否成功
+     */
+    boolean batchDelete(Long[] ids);
+}
+

+ 102 - 0
fs-service/src/main/java/com/fs/newAdv/service/impl/AdvertiserServiceImpl.java

@@ -0,0 +1,102 @@
+package com.fs.newAdv.service.impl;
+
+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.exception.base.BusinessException;
+import com.fs.newAdv.entity.Advertiser;
+import com.fs.newAdv.mapper.AdvertiserMapper;
+import com.fs.newAdv.service.IAdvertiserService;
+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.util.Arrays;
+
+/**
+ * 广告商服务实现类
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Service
+public class AdvertiserServiceImpl implements IAdvertiserService {
+
+    @Autowired
+    private AdvertiserMapper advertiserMapper;
+
+    @Override
+    public IPage<Advertiser> page(Page<Advertiser> page, String advertiserName, Integer type, Integer enabled) {
+        LambdaQueryWrapper<Advertiser> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StrUtil.isNotBlank(advertiserName), Advertiser::getAdvertiserName, advertiserName);
+        wrapper.eq(type != null, Advertiser::getType, type);
+        wrapper.eq(enabled != null, Advertiser::getEnabled, enabled);
+        wrapper.orderByDesc(Advertiser::getCreateTime);
+
+        return advertiserMapper.selectPage(page, wrapper);
+    }
+
+    @Override
+    public Advertiser getById(Long id) {
+        Advertiser advertiser = advertiserMapper.selectById(id);
+        if (advertiser == null) {
+            throw new BusinessException("广告商不存在");
+        }
+        return advertiser;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean create(Advertiser advertiser) {
+        // 校验广告商名称是否重复
+        LambdaQueryWrapper<Advertiser> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Advertiser::getAdvertiserName, advertiser.getAdvertiserName());
+        Integer count = advertiserMapper.selectCount(wrapper);
+        if (count > 0) {
+            throw new BusinessException("广告商名称已存在");
+        }
+
+        return advertiserMapper.insert(advertiser) > 0;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean update(Advertiser advertiser) {
+        Advertiser existAdvertiser = advertiserMapper.selectById(advertiser.getId());
+        if (existAdvertiser == null) {
+            throw new BusinessException("广告商不存在");
+        }
+
+        // 校验名称是否重复
+        LambdaQueryWrapper<Advertiser> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(Advertiser::getAdvertiserName, advertiser.getAdvertiserName());
+        wrapper.ne(Advertiser::getId, advertiser.getId());
+        Integer count = advertiserMapper.selectCount(wrapper);
+        if (count > 0) {
+            throw new BusinessException("广告商名称已存在");
+        }
+
+        return advertiserMapper.updateById(advertiser) > 0;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean delete(Long id) {
+        Advertiser advertiser = advertiserMapper.selectById(id);
+        if (advertiser == null) {
+            throw new BusinessException("广告商不存在");
+        }
+
+        return advertiserMapper.deleteById(id) > 0;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean batchDelete(Long[] ids) {
+        return advertiserMapper.deleteBatchIds(Arrays.asList(ids)) > 0;
+    }
+}
+

+ 100 - 0
fs-service/src/main/java/com/fs/newAdv/service/impl/CallbackAccountServiceImpl.java

@@ -0,0 +1,100 @@
+package com.fs.newAdv.service.impl;
+
+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.exception.base.BusinessException;
+import com.fs.newAdv.entity.CallbackAccount;
+import com.fs.newAdv.mapper.CallbackAccountMapper;
+import com.fs.newAdv.service.ICallbackAccountService;
+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.util.Arrays;
+
+/**
+ * 回传账号服务实现类
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Service
+public class CallbackAccountServiceImpl implements ICallbackAccountService {
+
+    @Autowired
+    private CallbackAccountMapper callbackAccountMapper;
+
+    @Override
+    public IPage<CallbackAccount> page(Page<CallbackAccount> page, String accountName) {
+        LambdaQueryWrapper<CallbackAccount> wrapper = new LambdaQueryWrapper<>();
+        wrapper.like(StrUtil.isNotBlank(accountName), CallbackAccount::getCallbackAccountName, accountName);
+        wrapper.orderByDesc(CallbackAccount::getCreateTime);
+
+        return callbackAccountMapper.selectPage(page, wrapper);
+    }
+
+    @Override
+    public CallbackAccount getById(Long id) {
+        CallbackAccount callbackAccount = callbackAccountMapper.selectById(id);
+        if (callbackAccount == null) {
+            throw new BusinessException("回传账号不存在");
+        }
+        return callbackAccount;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean create(CallbackAccount callbackAccount) {
+        // 校验账号名称是否重复
+        LambdaQueryWrapper<CallbackAccount> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(CallbackAccount::getCallbackAccountName, callbackAccount.getCallbackAccountName());
+        Integer count = callbackAccountMapper.selectCount(wrapper);
+        if (count > 0) {
+            throw new BusinessException("回传账号名称已存在");
+        }
+
+        return callbackAccountMapper.insert(callbackAccount) > 0;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean update(CallbackAccount callbackAccount) {
+        CallbackAccount existAccount = callbackAccountMapper.selectById(callbackAccount.getId());
+        if (existAccount == null) {
+            throw new BusinessException("回传账号不存在");
+        }
+
+        // 校验名称是否重复
+        LambdaQueryWrapper<CallbackAccount> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(CallbackAccount::getCallbackAccountName, callbackAccount.getCallbackAccountName());
+        wrapper.ne(CallbackAccount::getId, callbackAccount.getId());
+        Integer count = callbackAccountMapper.selectCount(wrapper);
+        if (count > 0) {
+            throw new BusinessException("回传账号名称已存在");
+        }
+
+        return callbackAccountMapper.updateById(callbackAccount) > 0;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean delete(Long id) {
+        CallbackAccount callbackAccount = callbackAccountMapper.selectById(id);
+        if (callbackAccount == null) {
+            throw new BusinessException("回传账号不存在");
+        }
+
+        return callbackAccountMapper.deleteById(id) > 0;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public boolean batchDelete(Long[] ids) {
+        return callbackAccountMapper.deleteBatchIds(Arrays.asList(ids)) > 0;
+    }
+}
+

+ 107 - 0
fs-service/src/main/java/com/fs/newAdv/service/impl/CallbackServiceImpl.java

@@ -0,0 +1,107 @@
+package com.fs.newAdv.service.impl;
+
+import cn.hutool.core.map.MapUtil;
+import com.fs.common.exception.base.BusinessException;
+import com.fs.newAdv.integration.adapter.IAdvertiserAdapter;
+import com.fs.newAdv.integration.factory.AdvertiserHandlerFactory;
+import com.fs.newAdv.integration.strategy.ICallbackStrategy;
+import com.fs.newAdv.mapper.SiteStatisticsMapper;
+import com.fs.newAdv.service.ICallbackService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * 回调服务实现类
+ *
+ * @author zhangqin
+ * @date 2025-11-03
+ */
+@Slf4j
+@Service
+public class CallbackServiceImpl implements ICallbackService {
+
+    @Autowired
+    private AdvertiserHandlerFactory handlerFactory;
+
+    @Autowired
+    private SiteStatisticsMapper statisticsMapper;
+
+    @Override
+    public boolean handleCallback(Integer advertiserType, Map<String, Object> callbackData, String sign) {
+        log.info("处理广告商 {} 的回调数据", advertiserType);
+
+        try {
+            // 1. 获取对应的适配器和策略
+            IAdvertiserAdapter adapter = handlerFactory.getAdapter(advertiserType);
+            ICallbackStrategy strategy = handlerFactory.getStrategy(advertiserType);
+
+            // 2. 验证签名
+            Map<String, String> signParams = convertToStringMap(callbackData);
+            if (!strategy.verifySign(signParams, sign)) {
+                log.error("签名验证失败");
+                return false;
+            }
+
+            // 3. 适配数据格式
+            Map<String, Object> adaptedData = adapter.adaptCallbackData(callbackData);
+
+            // 4. 执行策略处理
+            strategy.handleCallback(adaptedData);
+
+            // 5. 更新统计数据
+            updateSiteStatistics(
+                    MapUtil.getLong(adaptedData, "siteId"),
+                    MapUtil.getLong(adaptedData, "impressionCount"),
+                    MapUtil.getLong(adaptedData, "clickCount"),
+                    MapUtil.getDouble(adaptedData, "cost")
+            );
+
+            return true;
+
+        } catch (Exception e) {
+            log.error("处理回调数据失败", e);
+            throw new BusinessException("处理回调数据失败:" + e.getMessage());
+        }
+    }
+
+    @Override
+    @Async("asyncExecutor")
+    public void updateSiteStatistics(Long siteId, Long impressionCount, Long clickCount, Double cost) {
+        log.info("更新站点 {} 统计数据:展示={}, 点击={}, 花费={}", siteId, impressionCount, clickCount, cost);
+
+        if (siteId == null) {
+            return;
+        }
+
+        // 更新展示数
+        if (impressionCount != null && impressionCount > 0) {
+            statisticsMapper.incrementImpressionCount(siteId, impressionCount);
+        }
+
+        // 更新点击数
+        if (clickCount != null && clickCount > 0) {
+            statisticsMapper.incrementClickCount(siteId, clickCount);
+        }
+
+        // TODO: 更新花费等其他指标
+    }
+
+    /**
+     * 转换为String Map
+     */
+    private Map<String, String> convertToStringMap(Map<String, Object> map) {
+        Map<String, String> result = new HashMap<>();
+        map.forEach((key, value) -> {
+            if (value != null) {
+                result.put(key, value.toString());
+            }
+        });
+        return result;
+    }
+}
+

+ 192 - 0
fs-service/src/main/java/com/fs/newAdv/service/impl/ClickTraceServiceImpl.java

@@ -0,0 +1,192 @@
+package com.fs.newAdv.service.impl;
+
+import cn.hutool.json.JSONUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
+
+import com.fs.common.exception.base.BusinessException;
+import com.fs.common.utils.SnowflakeUtil;
+import com.fs.newAdv.entity.ClickTrace;
+import com.fs.newAdv.entity.Site;
+import com.fs.newAdv.mapper.ClickTraceMapper;
+import com.fs.newAdv.mapper.SiteMapper;
+import com.fs.newAdv.service.IClickTraceService;
+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.Map;
+
+/**
+ * 点击追踪服务实现类
+ *
+ * @author zhangqin
+ * @date 2025-11-04
+ */
+@Slf4j
+@Service
+public class ClickTraceServiceImpl implements IClickTraceService {
+
+    @Autowired
+    private ClickTraceMapper clickTraceMapper;
+
+    @Autowired
+    private SiteMapper siteMapper;
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public ClickTrace saveClickTrace(Long siteId, Map<String, String> params,
+                                     String visitorId, String ip, String userAgent) {
+
+        log.info("保存点击追踪记录:站点ID={},访客ID={}", siteId, visitorId);
+
+        // 解析站点信息
+        Site site = siteMapper.selectById(siteId);
+        if (site == null) {
+            throw new BusinessException("站点不存在:" + siteId);
+        }
+
+        ClickTrace trace = new ClickTrace();
+        trace.setId(SnowflakeUtil.nextId());
+        trace.setTraceId(SnowflakeUtil.nextIdStr());
+        trace.setSiteId(siteId);
+        trace.setAdvertiserId(site.getAdvertiserId());
+        trace.setAdvertiserName(site.getAdvertiserName());
+
+        // 提取不同平台的参数
+        extractPlatformParams(trace, params, site.getAdvertiserId());
+
+        // 访客信息
+        trace.setVisitorId(visitorId);
+        trace.setIp(ip);
+        trace.setUserAgent(userAgent);
+
+        // 保存原始参数
+        trace.setRawParams(JSONUtil.toJsonStr(params));
+
+        // 状态初始化
+        trace.setIsConverted(0);
+
+        // 时间
+        trace.setVisitTime(LocalDateTime.now());
+        trace.setCreateTime(LocalDateTime.now());
+        trace.setUpdateTime(LocalDateTime.now());
+
+        clickTraceMapper.insert(trace);
+
+        log.info("点击追踪记录保存成功:traceId={}", trace.getTraceId());
+        return trace;
+    }
+
+    /**
+     * 提取不同平台的参数
+     */
+    private void extractPlatformParams(ClickTrace trace,
+                                      Map<String, String> params,
+                                      Long advertiserId) {
+
+        // 根据广告商ID判断平台
+        // 1=百度,2=巨量引擎,3=新浪,4=广点通
+
+        if (advertiserId == 1L) {
+            // 百度
+            String clkId = params.get("clk_id");
+            String logId = params.get("logid");
+            trace.setClickId(clkId != null ? clkId : logId);
+            trace.setCampaignId(params.get("plan_id"));
+            trace.setCreativeId(params.get("unit_id"));
+            trace.setKeyword(params.get("keyword"));
+            trace.setSource("baidu");
+
+        } else if (advertiserId == 2L) {
+            // 巨量引擎
+            trace.setClickId(params.get("callback"));
+            trace.setCampaignId(params.get("campaign_id"));
+            trace.setCreativeId(params.get("creative_id"));
+            trace.setSource("oceanengine");
+
+        } else if (advertiserId == 3L) {
+            // 新浪
+            trace.setClickId(params.get("click_id"));
+            trace.setCampaignId(params.get("cid"));
+            trace.setSource("sina");
+
+        } else if (advertiserId == 4L) {
+            // 广点通
+            trace.setClickId(params.get("gdt_vid"));
+            trace.setCampaignId(params.get("campaign_id"));
+            trace.setSource("gdt");
+
+        } else {
+            // 通用处理
+            trace.setClickId(params.get("click_id"));
+            trace.setCampaignId(params.get("campaign_id"));
+            trace.setCreativeId(params.get("creative_id"));
+            trace.setSource(params.get("source"));
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void bindUser(String traceId, Long userId) {
+        log.info("关联用户ID:traceId={},userId={}", traceId, userId);
+
+        LambdaUpdateWrapper<ClickTrace> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(ClickTrace::getTraceId, traceId);
+        wrapper.set(ClickTrace::getUserId, userId);
+        wrapper.set(ClickTrace::getUpdateTime, LocalDateTime.now());
+
+        int count = clickTraceMapper.update(null, wrapper);
+
+        if (count > 0) {
+            log.info("用户ID关联成功");
+        } else {
+            log.warn("未找到对应的追踪记录:traceId={}", traceId);
+        }
+    }
+
+    @Override
+    public ClickTrace getLatestByVisitorId(String visitorId) {
+        LambdaQueryWrapper<ClickTrace> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ClickTrace::getVisitorId, visitorId);
+        wrapper.orderByDesc(ClickTrace::getVisitTime);
+        wrapper.last("LIMIT 1");
+
+        return clickTraceMapper.selectOne(wrapper);
+    }
+
+    @Override
+    public ClickTrace getLatestByUserId(Long userId) {
+        LambdaQueryWrapper<ClickTrace> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ClickTrace::getUserId, userId);
+        wrapper.orderByDesc(ClickTrace::getVisitTime);
+        wrapper.last("LIMIT 1");
+
+        return clickTraceMapper.selectOne(wrapper);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public void markAsConverted(Long traceId) {
+        log.info("标记为已转化:traceId={}", traceId);
+
+        LambdaUpdateWrapper<ClickTrace> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(ClickTrace::getId, traceId);
+        wrapper.set(ClickTrace::getIsConverted, 1);
+        wrapper.set(ClickTrace::getConversionTime, LocalDateTime.now());
+        wrapper.set(ClickTrace::getUpdateTime, LocalDateTime.now());
+
+        clickTraceMapper.update(null, wrapper);
+    }
+
+    @Override
+    public ClickTrace getByTraceId(String traceId) {
+        LambdaQueryWrapper<ClickTrace> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(ClickTrace::getTraceId, traceId);
+
+        return clickTraceMapper.selectOne(wrapper);
+    }
+}
+

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików