|
|
@@ -3,20 +3,26 @@ package com.fs.app.facade;
|
|
|
import cn.hutool.core.map.MapUtil;
|
|
|
import cn.hutool.core.util.StrUtil;
|
|
|
import cn.hutool.json.JSONUtil;
|
|
|
-import com.fs.app.dto.req.LeadSubmitRequest;
|
|
|
+import com.fs.app.enums.AdvertiserTypeEnum;
|
|
|
import com.fs.app.event.ConversionEventPublisher;
|
|
|
import com.fs.app.integration.adapter.IAdvertiserAdapter;
|
|
|
import com.fs.app.integration.client.BaiduApiClient;
|
|
|
import com.fs.app.integration.factory.AdvertiserHandlerFactory;
|
|
|
import com.fs.app.integration.strategy.ICallbackStrategy;
|
|
|
+import com.fs.app.mq.message.ClickMessage;
|
|
|
import com.fs.common.exception.base.BusinessException;
|
|
|
-import com.fs.common.result.Result;
|
|
|
import com.fs.common.utils.SnowflakeUtil;
|
|
|
import com.fs.newAdv.domain.ClickTrace;
|
|
|
+import com.fs.newAdv.domain.LandingPageTemplate;
|
|
|
import com.fs.newAdv.domain.Lead;
|
|
|
-import com.fs.newAdv.mapper.SiteStatisticsMapper;
|
|
|
+import com.fs.newAdv.domain.Site;
|
|
|
+import com.fs.newAdv.dto.req.LeadSubmitRequest;
|
|
|
+import com.fs.newAdv.dto.res.LandingIndexRes;
|
|
|
+import com.fs.app.enums.ConversionTypeEnum;
|
|
|
import com.fs.newAdv.service.IClickTraceService;
|
|
|
+import com.fs.newAdv.service.ILandingPageTemplateService;
|
|
|
import com.fs.newAdv.service.ILeadService;
|
|
|
+import com.fs.newAdv.service.ISiteService;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
|
import org.springframework.stereotype.Service;
|
|
|
@@ -25,6 +31,7 @@ import org.springframework.transaction.annotation.Transactional;
|
|
|
import javax.servlet.http.Cookie;
|
|
|
import javax.servlet.http.HttpServletRequest;
|
|
|
import javax.servlet.http.HttpServletResponse;
|
|
|
+import java.time.LocalDateTime;
|
|
|
import java.util.HashMap;
|
|
|
import java.util.Map;
|
|
|
|
|
|
@@ -39,264 +46,150 @@ public class CallbackProcessingFacadeServiceImpl implements CallbackProcessingFa
|
|
|
|
|
|
@Autowired
|
|
|
private AdvertiserHandlerFactory handlerFactory;
|
|
|
+
|
|
|
@Autowired
|
|
|
private ILeadService leadService;
|
|
|
|
|
|
- @Autowired
|
|
|
- private SiteStatisticsMapper statisticsMapper;
|
|
|
+ private ISiteService siteService;
|
|
|
|
|
|
@Autowired
|
|
|
private ConversionEventPublisher conversionEventPublisher;
|
|
|
|
|
|
- @Override
|
|
|
- public Result<Map<String, Object>> saveClickTrace(Map<String, String> allParams, Long siteId, HttpServletRequest request, HttpServletResponse response) {
|
|
|
- try {
|
|
|
- // 1. 验证站点ID
|
|
|
- if (siteId == null) {
|
|
|
- String siteIdStr = allParams.get("site_id");
|
|
|
- if (StrUtil.isNotBlank(siteIdStr)) {
|
|
|
- siteId = Long.parseLong(siteIdStr);
|
|
|
- } else {
|
|
|
- return Result.error(400, "缺少站点ID参数");
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 2. 获取或创建访客ID
|
|
|
- String visitorId = getOrCreateVisitorId(request, response);
|
|
|
-
|
|
|
- // 3. 保存点击追踪记录
|
|
|
- ClickTrace trace = clickTraceService.saveClickTrace(
|
|
|
- siteId,
|
|
|
- allParams,
|
|
|
- visitorId,
|
|
|
- getClientIp(request),
|
|
|
- request.getHeader("User-Agent")
|
|
|
- );
|
|
|
+ @Autowired
|
|
|
+ private IClickTraceService iClickTraceService;
|
|
|
+ @Autowired
|
|
|
+ private ILandingPageTemplateService landingPageTemplateService;
|
|
|
|
|
|
- // 4. 返回追踪信息
|
|
|
- Map<String, Object> result = new HashMap<>();
|
|
|
- result.put("traceId", trace.getTraceId());
|
|
|
- result.put("visitorId", visitorId);
|
|
|
- result.put("message", "追踪成功");
|
|
|
- log.info("落地页追踪成功:traceId={}", trace.getTraceId());
|
|
|
- return Result.success(result);
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("落地页追踪失败", e);
|
|
|
- return Result.error(500, "追踪失败:" + e.getMessage());
|
|
|
- }
|
|
|
+ @Override
|
|
|
+ public void saveClickTrace(AdvertiserTypeEnum trackType, Map<String, String> allParams) {
|
|
|
+ ClickTrace trace = new ClickTrace();
|
|
|
+ // 提取不同平台的参数
|
|
|
+ extractPlatformParams(trace, trackType, allParams);
|
|
|
+ // 保存原始参数
|
|
|
+ trace.setRawParams(JSONUtil.toJsonStr(allParams));
|
|
|
+ // 状态初始化
|
|
|
+ trace.setIsConverted(0);
|
|
|
+ trace.setSource(trackType.getName());
|
|
|
+ // 时间
|
|
|
+ trace.setCreateTime(LocalDateTime.now());
|
|
|
+ trace.setUpdateTime(LocalDateTime.now());
|
|
|
+
|
|
|
+ iClickTraceService.save(trace);
|
|
|
+ log.info("点击追踪记录保存成功:traceId={}", trace.getTraceId());
|
|
|
}
|
|
|
|
|
|
@Override
|
|
|
- @Transactional(rollbackFor = Exception.class)
|
|
|
- public Result<Map<String, Object>> submitForm(LeadSubmitRequest request, HttpServletRequest httpRequest) {
|
|
|
- try {
|
|
|
- // 2. 构建Lead对象
|
|
|
- Lead lead = new Lead();
|
|
|
- lead.setName(request.getName());
|
|
|
- lead.setPhone(request.getPhone());
|
|
|
- lead.setCompany(request.getCompany());
|
|
|
- lead.setEmail(request.getEmail());
|
|
|
- lead.setSiteId(request.getSiteId());
|
|
|
- lead.setClickId(request.getClickId());
|
|
|
- lead.setSource(request.getSource());
|
|
|
- lead.setCampaignId(request.getCampaignId());
|
|
|
- lead.setKeyword(request.getKeyword());
|
|
|
- lead.setCreativeId(request.getCreativeId());
|
|
|
- lead.setStatus(0); // 新线索
|
|
|
-
|
|
|
- // 3. 保存原始参数JSON(优先使用前端传递的完整参数)
|
|
|
- Map<String, Object> rawParams = new HashMap<>();
|
|
|
- if (request.getRawParams() != null && !request.getRawParams().isEmpty()) {
|
|
|
- // 使用前端传递的所有URL参数(包括bd_vid等平台特定参数)
|
|
|
- rawParams.putAll(request.getRawParams());
|
|
|
- } else {
|
|
|
- // 向后兼容:如果没有rawParams,使用单独字段构建
|
|
|
- rawParams.put("click_id", request.getClickId());
|
|
|
- rawParams.put("source", request.getSource());
|
|
|
- rawParams.put("site_id", request.getSiteId());
|
|
|
- rawParams.put("campaign_id", request.getCampaignId());
|
|
|
- rawParams.put("keyword", request.getKeyword());
|
|
|
- rawParams.put("creative_id", request.getCreativeId());
|
|
|
- }
|
|
|
- lead.setRawParams(JSONUtil.toJsonStr(rawParams));
|
|
|
-
|
|
|
- // 4. 获取客户端信息
|
|
|
- lead.setClientIp(getClientIp(httpRequest));
|
|
|
- lead.setUserAgent(httpRequest.getHeader("User-Agent"));
|
|
|
-
|
|
|
- // 5. 保存线索并触发转化事件
|
|
|
- Lead savedLead = saveLeadAndTriggerConversion(lead);
|
|
|
-
|
|
|
- // 6. 返回结果
|
|
|
- Map<String, Object> result = new HashMap<>();
|
|
|
- result.put("leadId", savedLead.getId());
|
|
|
- result.put("message", "提交成功,我们会尽快与您联系");
|
|
|
-
|
|
|
- log.info("表单提交成功 | leadId={}, name={}, phone={}",
|
|
|
- savedLead.getId(), savedLead.getName(), savedLead.getPhone());
|
|
|
-
|
|
|
- return Result.success(result);
|
|
|
-
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("表单提交失败 | name={}, phone={}", request.getName(), request.getPhone(), e);
|
|
|
- return Result.error(500, "提交失败,请稍后重试");
|
|
|
- }
|
|
|
+ public void updateClickTrace(ClickMessage clickMessage) {
|
|
|
+ Map<String, String> params = getTraceIdByPlatformParams(clickMessage.getAllParams());
|
|
|
+ Site site = siteService.getById(clickMessage.getSiteId());
|
|
|
+ ClickTrace byTraceId = iClickTraceService.getByTraceId(params.get("traceId"));
|
|
|
+ byTraceId.setSiteId(site.getId());
|
|
|
+ byTraceId.setAdvertiserId(site.getAdvertiserId());
|
|
|
+ byTraceId.setAdvertiserName(site.getAdvertiserName());
|
|
|
+ byTraceId.setViewUrl(clickMessage.getViewUrl());
|
|
|
+ siteService.updateById(site);
|
|
|
}
|
|
|
|
|
|
- public Lead saveLeadAndTriggerConversion(Lead lead) {
|
|
|
- // 1. 保存线索到数据库
|
|
|
- boolean saved = leadService.save(lead);
|
|
|
-
|
|
|
- if (!saved) {
|
|
|
- log.error("线索保存失败 | phone={}", lead.getPhone());
|
|
|
- throw new RuntimeException("线索保存失败");
|
|
|
- }
|
|
|
-
|
|
|
- log.info("线索保存成功 | leadId={}, name={}, phone={}, clickId={}",
|
|
|
- lead.getId(), lead.getName(), lead.getPhone(), lead.getClickId());
|
|
|
-
|
|
|
- // 2. 如果有点击ID和来源,触发转化事件
|
|
|
- if (StrUtil.isNotBlank(lead.getClickId()) && StrUtil.isNotBlank(lead.getSource())) {
|
|
|
- try {
|
|
|
- // 发布转化事件(会自动发送到MQ)
|
|
|
- conversionEventPublisher.publishConversionEvent(
|
|
|
- lead.getSiteId(), // 站点ID
|
|
|
- lead.getClickId(), // 点击ID
|
|
|
- lead.getSource().toUpperCase(), // 广告商(BAIDU/OCEANENGINE/SINA/GDT)
|
|
|
- "SUBMIT_FORM", // 事件类型:提交表单
|
|
|
- null, // 转化价值(可选)
|
|
|
- lead.getId() // 线索ID
|
|
|
- );
|
|
|
-
|
|
|
- log.info("转化事件已发布 | leadId={}, clickId={}, source={}",
|
|
|
- lead.getId(), lead.getClickId(), lead.getSource());
|
|
|
-
|
|
|
- } catch (Exception e) {
|
|
|
- log.error("转化事件发布失败 | leadId={}", lead.getId(), e);
|
|
|
- // 不影响线索保存,只记录日志
|
|
|
- }
|
|
|
+ /**
|
|
|
+ * 获取traceId和source平台信息
|
|
|
+ *
|
|
|
+ * @param allParams
|
|
|
+ * @return traceId 线索id
|
|
|
+ */
|
|
|
+ private Map<String, String> getTraceIdByPlatformParams(Map<String, String> allParams) {
|
|
|
+ Map<String, String> traceId = new HashMap<>();
|
|
|
+ if (StrUtil.isNotEmpty(allParams.get("bd_vid"))) {
|
|
|
+ traceId.put("traceId", allParams.get("bd_vid"));
|
|
|
+ traceId.put("source", AdvertiserTypeEnum.BAIDU.getName());
|
|
|
+ return traceId;
|
|
|
} else {
|
|
|
- log.warn("缺少点击ID或来源信息,跳过转化事件 | leadId={}, clickId={}, source={}",
|
|
|
- lead.getId(), lead.getClickId(), lead.getSource());
|
|
|
+ throw new BusinessException("回传参数错误");
|
|
|
}
|
|
|
|
|
|
- return lead;
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * 获取或创建访客ID(使用Cookie)
|
|
|
+ * 提取不同平台的参数
|
|
|
*/
|
|
|
- private String getOrCreateVisitorId(HttpServletRequest request,
|
|
|
- HttpServletResponse response) {
|
|
|
- // 从Cookie获取
|
|
|
- Cookie[] cookies = request.getCookies();
|
|
|
- if (cookies != null) {
|
|
|
- for (Cookie cookie : cookies) {
|
|
|
- if ("visitor_id".equals(cookie.getName())) {
|
|
|
- String visitorId = cookie.getValue();
|
|
|
- if (StrUtil.isNotBlank(visitorId)) {
|
|
|
- return visitorId;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+ private void extractPlatformParams(ClickTrace trace,
|
|
|
+ AdvertiserTypeEnum trackType,
|
|
|
+ Map<String, String> allParams) {
|
|
|
+ switch (trackType) {
|
|
|
+ case BAIDU:
|
|
|
+ trace.setTraceId(allParams.get("bd_vid"));
|
|
|
+ trace.setClickId(allParams.get("click_id"));
|
|
|
+ trace.setCreativeId(allParams.get("aid"));
|
|
|
+ trace.setCampaignId(allParams.get("pid"));
|
|
|
+ trace.setIp(allParams.get("ip"));
|
|
|
+ break;
|
|
|
+ case SINA:
|
|
|
+ break;
|
|
|
}
|
|
|
|
|
|
- // 生成新的访客ID
|
|
|
- String visitorId = SnowflakeUtil.nextIdStr();
|
|
|
- Cookie cookie = new Cookie("visitor_id", visitorId);
|
|
|
- cookie.setMaxAge(30 * 24 * 60 * 60); // 30天
|
|
|
- cookie.setPath("/");
|
|
|
- cookie.setHttpOnly(false); // 允许JavaScript访问
|
|
|
- response.addCookie(cookie);
|
|
|
-
|
|
|
- log.info("生成新的访客ID:{}", visitorId);
|
|
|
- return visitorId;
|
|
|
}
|
|
|
|
|
|
- /**
|
|
|
- * 获取客户端真实IP
|
|
|
- */
|
|
|
- private String getClientIp(HttpServletRequest request) {
|
|
|
- String ip = request.getHeader("X-Forwarded-For");
|
|
|
- if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
|
|
|
- ip = request.getHeader("X-Real-IP");
|
|
|
- }
|
|
|
- if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
|
|
|
- ip = request.getHeader("Proxy-Client-IP");
|
|
|
- }
|
|
|
- if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
|
|
|
- ip = request.getHeader("WL-Proxy-Client-IP");
|
|
|
+ @Override
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
+ public void submitForm(LeadSubmitRequest request) {
|
|
|
+ Map<String, String> params = getTraceIdByPlatformParams(request.getRawParams());
|
|
|
+ String traceId = params.get("traceId");
|
|
|
+ String source = params.get("source");
|
|
|
+ if (StrUtil.isEmpty(traceId)) {
|
|
|
+ throw new BusinessException("缺少traceId");
|
|
|
}
|
|
|
- if (StrUtil.isBlank(ip) || "unknown".equalsIgnoreCase(ip)) {
|
|
|
- ip = request.getRemoteAddr();
|
|
|
+ // 2. 构建Lead对象
|
|
|
+ Lead lead = new Lead();
|
|
|
+ lead.setSiteId(request.getSiteId());
|
|
|
+ lead.setViewUrl(request.getViewUrl());
|
|
|
+ lead.setSource(source);
|
|
|
+ lead.setTraceId(traceId);
|
|
|
+ lead.setStatus(0); // 新线索
|
|
|
+
|
|
|
+ // 3. 保存原始参数JSON(优先使用前端传递的完整参数)
|
|
|
+ Map<String, Object> rawParams = new HashMap<>();
|
|
|
+ if (request.getRawParams() != null && !request.getRawParams().isEmpty()) {
|
|
|
+ // 使用前端传递的所有URL参数(包括bd_vid等平台特定参数)
|
|
|
+ rawParams.putAll(request.getRawParams());
|
|
|
}
|
|
|
+ lead.setRawParams(JSONUtil.toJsonStr(rawParams));
|
|
|
|
|
|
- // 处理多个IP的情况,取第一个
|
|
|
- if (StrUtil.isNotBlank(ip) && ip.contains(",")) {
|
|
|
- ip = ip.split(",")[0].trim();
|
|
|
- }
|
|
|
+ // 4. 获取客户端信息
|
|
|
|
|
|
- return ip;
|
|
|
+ // 5. 保存线索并触发转化事件
|
|
|
+ saveLeadAndTriggerConversion(lead);
|
|
|
}
|
|
|
|
|
|
+ @Override
|
|
|
+ public LandingIndexRes getLandingIndexBySiteId(Long siteId) {
|
|
|
+ Site byId = siteService.getById(siteId);
|
|
|
+ LandingPageTemplate byId1 = landingPageTemplateService.getById(byId.getLaunchPageId());
|
|
|
+ LandingIndexRes res = new LandingIndexRes();
|
|
|
+ res.setTemplateData(byId1.getTemplateData());
|
|
|
+ return res;
|
|
|
+ }
|
|
|
|
|
|
- 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());
|
|
|
+ public void saveLeadAndTriggerConversion(Lead lead) {
|
|
|
+ // 1. 保存线索到数据库
|
|
|
+ boolean saved = leadService.save(lead);
|
|
|
+ if (!saved) {
|
|
|
+ log.error("线索保存失败 | phone={}", lead.getPhone());
|
|
|
+ throw new RuntimeException("线索保存失败");
|
|
|
}
|
|
|
+ // 2. 如果有点击ID和来源,触发转化事件
|
|
|
+ // 发布转化事件(会自动发送到MQ)
|
|
|
+ conversionEventPublisher.publishConversionEvent(
|
|
|
+ lead.getSiteId(), // 站点ID
|
|
|
+ lead.getTraceId(), // 点击ID
|
|
|
+ lead.getSource().toUpperCase(), // 广告商(BAIDU/OCEANENGINE/SINA/GDT)
|
|
|
+ ConversionTypeEnum.FORM_SUBMIT, // 事件类型:提交表单
|
|
|
+ null, // 转化价值(可选)
|
|
|
+ lead.getId() // 线索ID
|
|
|
+ );
|
|
|
}
|
|
|
|
|
|
|
|
|
- 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
|