|
|
@@ -0,0 +1,617 @@
|
|
|
+package shop.alien.store.service.impl;
|
|
|
+
|
|
|
+import com.alibaba.fastjson.JSONObject;
|
|
|
+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.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
+import lombok.RequiredArgsConstructor;
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
+import org.apache.commons.lang3.StringUtils;
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
+import shop.alien.entity.result.R;
|
|
|
+import shop.alien.entity.store.OceanEngineClickMonitorLog;
|
|
|
+import shop.alien.mapper.OceanEngineClickMonitorLogMapper;
|
|
|
+import shop.alien.store.dto.OceanEngineClickBindUserDto;
|
|
|
+import shop.alien.store.dto.OceanEngineConversionResultVo;
|
|
|
+import shop.alien.store.service.OceanEngineClickMonitorLogService;
|
|
|
+
|
|
|
+import javax.servlet.http.HttpServletRequest;
|
|
|
+import java.io.BufferedReader;
|
|
|
+import java.io.InputStreamReader;
|
|
|
+import java.io.OutputStream;
|
|
|
+import java.net.HttpURLConnection;
|
|
|
+import java.net.URL;
|
|
|
+import java.nio.charset.StandardCharsets;
|
|
|
+import java.util.Date;
|
|
|
+import java.util.Enumeration;
|
|
|
+import java.util.LinkedHashMap;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 巨量引擎点击监测服务实现
|
|
|
+ */
|
|
|
+@Slf4j
|
|
|
+@Service
|
|
|
+@RequiredArgsConstructor
|
|
|
+public class OceanEngineClickMonitorLogServiceImpl
|
|
|
+ extends ServiceImpl<OceanEngineClickMonitorLogMapper, OceanEngineClickMonitorLog>
|
|
|
+ implements OceanEngineClickMonitorLogService {
|
|
|
+
|
|
|
+ private static final int HTTP_TIMEOUT_MS = 10000;
|
|
|
+ private static final String DEFAULT_EVENT_TYPE = "active_register";
|
|
|
+ private static final int DEFAULT_MATCH_TYPE = 0;
|
|
|
+
|
|
|
+ /** 巨量引擎转化回传 API 地址 */
|
|
|
+ @Value("${oceanengine.conversion.report-url:https://analytics.oceanengine.com/api/v2/conversion}")
|
|
|
+ private String conversionReportUrl;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public R<Long> receiveClickMonitor(HttpServletRequest request) {
|
|
|
+ Map<String, String> paramMap = extractQueryParams(request);
|
|
|
+ log.info("OceanEngineClickMonitorLogServiceImpl.receiveClickMonitor, paramCount={}, queryString={}",
|
|
|
+ paramMap.size(), request.getQueryString());
|
|
|
+
|
|
|
+ String clickId = firstNonBlank(paramMap,
|
|
|
+ "clickid", "click_id", "CLICKID");
|
|
|
+ String callbackParam = firstNonBlank(paramMap,
|
|
|
+ "callback", "callback_param", "CALLBACK_PARAM");
|
|
|
+ String requestId = firstNonBlank(paramMap,
|
|
|
+ "request_id", "req_id", "REQUEST_ID");
|
|
|
+
|
|
|
+ // 幂等:相同 click_id 或 request_id 已存在则直接返回
|
|
|
+ OceanEngineClickMonitorLog existing = findExistingLog(clickId, requestId);
|
|
|
+ if (existing != null) {
|
|
|
+ log.info("OceanEngineClickMonitorLogServiceImpl.receiveClickMonitor, duplicate click, id={}, clickId={}",
|
|
|
+ existing.getId(), clickId);
|
|
|
+ return R.data(existing.getId(), "重复点击,已忽略");
|
|
|
+ }
|
|
|
+
|
|
|
+ OceanEngineClickMonitorLog clickLog = buildClickLog(request, paramMap, clickId, callbackParam, requestId);
|
|
|
+ boolean saved = this.save(clickLog);
|
|
|
+ if (!saved) {
|
|
|
+ log.error("OceanEngineClickMonitorLogServiceImpl.receiveClickMonitor, save failed, clickId={}", clickId);
|
|
|
+ return R.fail("点击监测日志保存失败");
|
|
|
+ }
|
|
|
+ log.info("OceanEngineClickMonitorLogServiceImpl.receiveClickMonitor, saved id={}, clickId={}, advertiserId={}",
|
|
|
+ clickLog.getId(), clickLog.getClickId(), clickLog.getAdvertiserId());
|
|
|
+ return R.data(clickLog.getId(), "接收成功");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public R<String> bindUser(OceanEngineClickBindUserDto bindDto) {
|
|
|
+ log.info("OceanEngineClickMonitorLogServiceImpl.bindUser, param={}", bindDto);
|
|
|
+ if (bindDto == null || bindDto.getUserId() == null) {
|
|
|
+ return R.fail("userId不能为空");
|
|
|
+ }
|
|
|
+ if (StringUtils.isAllBlank(bindDto.getClickId(), bindDto.getCallbackParam())) {
|
|
|
+ return R.fail("clickId与callbackParam至少填一个");
|
|
|
+ }
|
|
|
+
|
|
|
+ OceanEngineClickMonitorLog clickLog = findClickLog(bindDto.getClickId(), bindDto.getCallbackParam());
|
|
|
+ if (clickLog == null) {
|
|
|
+ log.warn("OceanEngineClickMonitorLogServiceImpl.bindUser, click log not found, clickId={}, callbackParam={}",
|
|
|
+ bindDto.getClickId(), bindDto.getCallbackParam());
|
|
|
+ return R.fail("未找到对应的点击监测记录");
|
|
|
+ }
|
|
|
+
|
|
|
+ clickLog.setUserId(bindDto.getUserId());
|
|
|
+ clickLog.setStoreId(bindDto.getStoreId());
|
|
|
+ clickLog.setBindStatus(OceanEngineClickMonitorLog.BIND_STATUS_BOUND);
|
|
|
+ clickLog.setBindTime(new Date());
|
|
|
+ boolean updated = this.updateById(clickLog);
|
|
|
+ return updated ? R.success("绑定成功") : R.fail("绑定失败");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public R<OceanEngineClickMonitorLog> getInfoById(Long id) {
|
|
|
+ log.info("OceanEngineClickMonitorLogServiceImpl.getInfoById, id={}", id);
|
|
|
+ return R.data(this.getById(id));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public R<IPage<OceanEngineClickMonitorLog>> list(Integer pageNum, Integer pageSize,
|
|
|
+ String clickId, String advertiserId,
|
|
|
+ Integer userId, Integer bindStatus) {
|
|
|
+ log.info("OceanEngineClickMonitorLogServiceImpl.list, pageNum={}, pageSize={}, clickId={}, advertiserId={}, userId={}, bindStatus={}",
|
|
|
+ pageNum, pageSize, clickId, advertiserId, userId, bindStatus);
|
|
|
+ Page<OceanEngineClickMonitorLog> page = new Page<>(pageNum, pageSize);
|
|
|
+ LambdaQueryWrapper<OceanEngineClickMonitorLog> wrapper = new LambdaQueryWrapper<>();
|
|
|
+ if (StringUtils.isNotBlank(clickId)) {
|
|
|
+ wrapper.eq(OceanEngineClickMonitorLog::getClickId, clickId);
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(advertiserId)) {
|
|
|
+ wrapper.eq(OceanEngineClickMonitorLog::getAdvertiserId, advertiserId);
|
|
|
+ }
|
|
|
+ if (userId != null) {
|
|
|
+ wrapper.eq(OceanEngineClickMonitorLog::getUserId, userId);
|
|
|
+ }
|
|
|
+ if (bindStatus != null) {
|
|
|
+ wrapper.eq(OceanEngineClickMonitorLog::getBindStatus, bindStatus);
|
|
|
+ }
|
|
|
+ wrapper.orderByDesc(OceanEngineClickMonitorLog::getCreatedTime);
|
|
|
+ return R.data(this.page(page, wrapper));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 转化事件回传巨量引擎
|
|
|
+ * <p>
|
|
|
+ * 官方接口:POST https://analytics.oceanengine.com/api/v2/conversion
|
|
|
+ * </p>
|
|
|
+ * <p>排查日志关键字:{@code [OceanEngineConversion]}</p>
|
|
|
+ * <p>入参支持:id / clickId / userId / callbackParam 查库,或直接传完整点击记录</p>
|
|
|
+ */
|
|
|
+ @Override
|
|
|
+ public R<OceanEngineConversionResultVo> reportConversion(OceanEngineClickMonitorLog clickLogParam) {
|
|
|
+ log.info("[OceanEngineConversion] 开始转化回传, 入参 id={}, clickId={}, userId={}, eventType={}, matchType={}",
|
|
|
+ clickLogParam != null ? clickLogParam.getId() : null,
|
|
|
+ clickLogParam != null ? clickLogParam.getClickId() : null,
|
|
|
+ clickLogParam != null ? clickLogParam.getUserId() : null,
|
|
|
+ clickLogParam != null ? clickLogParam.getEventType() : null,
|
|
|
+ clickLogParam != null ? clickLogParam.getMatchType() : null);
|
|
|
+
|
|
|
+ if (clickLogParam == null) {
|
|
|
+ log.warn("[OceanEngineConversion] 失败-入参为空");
|
|
|
+ return R.fail("点击监测记录不能为空");
|
|
|
+ }
|
|
|
+
|
|
|
+ // 步骤1:解析/合并点击监测记录(库查 + 入参覆盖)
|
|
|
+ OceanEngineClickMonitorLog clickLog = resolveClickLogForReport(clickLogParam);
|
|
|
+ if (clickLog == null) {
|
|
|
+ log.warn("[OceanEngineConversion] 失败-未找到点击记录, id={}, clickId={}, userId={}, callbackParam={}",
|
|
|
+ clickLogParam.getId(), clickLogParam.getClickId(), clickLogParam.getUserId(),
|
|
|
+ maskSensitive(clickLogParam.getCallbackParam()));
|
|
|
+ return R.fail("未找到点击监测记录,请传入 id / clickId / userId 或完整 callbackParam");
|
|
|
+ }
|
|
|
+ log.info("[OceanEngineConversion] 步骤1-点击记录已解析, clickLogId={}, advertiserId={}, promotionId={}, projectId={}",
|
|
|
+ clickLog.getId(), clickLog.getAdvertiserId(), clickLog.getPromotionId(), clickLog.getProjectId());
|
|
|
+
|
|
|
+ // 步骤2:提取归因 callback(巨量匹配核心字段)
|
|
|
+ String eventType = StringUtils.defaultIfBlank(clickLogParam.getEventType(), DEFAULT_EVENT_TYPE);
|
|
|
+ String callback = resolveCallbackFromClickLog(clickLog);
|
|
|
+ if (StringUtils.isBlank(callback)) {
|
|
|
+ log.warn("[OceanEngineConversion] 失败-callback为空, clickLogId={}, callbackParam={}, clickId={}",
|
|
|
+ clickLog.getId(), maskSensitive(clickLog.getCallbackParam()), clickLog.getClickId());
|
|
|
+ return R.fail("callback 不能为空,点击记录需包含 callbackParam 或 clickId");
|
|
|
+ }
|
|
|
+ log.info("[OceanEngineConversion] 步骤2-callback已解析, source={}, callback={}",
|
|
|
+ StringUtils.isNotBlank(clickLog.getCallbackParam()) ? "callbackParam" : "clickId",
|
|
|
+ maskSensitive(callback));
|
|
|
+
|
|
|
+ // 步骤3:组装回传参数
|
|
|
+ int matchType = clickLogParam.getMatchType() != null ? clickLogParam.getMatchType() : DEFAULT_MATCH_TYPE;
|
|
|
+ long timestamp = resolveConversionTimestamp(clickLogParam, clickLog);
|
|
|
+ log.info("[OceanEngineConversion] 步骤3-回传参数, eventType={}, matchType={}, timestamp={}, androidId={}, idfa={}, oaid={}, imei={}",
|
|
|
+ eventType, matchType, timestamp,
|
|
|
+ maskSensitive(clickLog.getAndroidId()), maskSensitive(clickLog.getIdfa()),
|
|
|
+ maskSensitive(clickLog.getOaid()), maskSensitive(clickLog.getImei()));
|
|
|
+
|
|
|
+ JSONObject requestBody = buildConversionRequestBody(eventType, callback, matchType,
|
|
|
+ clickLog.getAndroidId(), clickLog.getIdfa(), clickLog.getOaid(), clickLog.getImei(), timestamp);
|
|
|
+ String requestBodyJson = requestBody.toJSONString();
|
|
|
+
|
|
|
+ OceanEngineConversionResultVo resultVo = new OceanEngineConversionResultVo();
|
|
|
+ resultVo.setCallback(callback);
|
|
|
+ resultVo.setRequestBody(requestBodyJson);
|
|
|
+
|
|
|
+ // 步骤4:HTTP POST 调用巨量转化回传接口
|
|
|
+ HttpURLConnection connection = null;
|
|
|
+ try {
|
|
|
+ log.info("[OceanEngineConversion] 步骤4-发起HTTP请求, url={}, bodyLength={}",
|
|
|
+ conversionReportUrl, requestBodyJson.length());
|
|
|
+ log.debug("[OceanEngineConversion] 请求体详情, body={}", requestBodyJson);
|
|
|
+
|
|
|
+ URL url = new URL(conversionReportUrl);
|
|
|
+ connection = (HttpURLConnection) url.openConnection();
|
|
|
+ connection.setRequestMethod("POST");
|
|
|
+ connection.setConnectTimeout(HTTP_TIMEOUT_MS);
|
|
|
+ connection.setReadTimeout(HTTP_TIMEOUT_MS);
|
|
|
+ connection.setDoOutput(true);
|
|
|
+ connection.setRequestProperty("Content-Type", "application/json; charset=utf-8");
|
|
|
+
|
|
|
+ byte[] bodyBytes = requestBodyJson.getBytes(StandardCharsets.UTF_8);
|
|
|
+ connection.setRequestProperty("Content-Length", String.valueOf(bodyBytes.length));
|
|
|
+ try (OutputStream outputStream = connection.getOutputStream()) {
|
|
|
+ outputStream.write(bodyBytes);
|
|
|
+ outputStream.flush();
|
|
|
+ }
|
|
|
+
|
|
|
+ int httpCode = connection.getResponseCode();
|
|
|
+ String responseBody = readHttpResponse(connection, httpCode);
|
|
|
+ boolean success = httpCode >= 200 && httpCode < 300 && isOceanEngineSuccess(responseBody);
|
|
|
+
|
|
|
+ resultVo.setHttpCode(httpCode);
|
|
|
+ resultVo.setResponseBody(responseBody);
|
|
|
+ resultVo.setSuccess(success);
|
|
|
+ resultVo.setMessage(success ? "转化回传成功" : "转化回传失败");
|
|
|
+
|
|
|
+ if (success) {
|
|
|
+ log.info("[OceanEngineConversion] 步骤5-回传成功, clickLogId={}, userId={}, eventType={}, httpCode={}, response={}",
|
|
|
+ clickLog.getId(), clickLog.getUserId(), eventType, httpCode, responseBody);
|
|
|
+ } else {
|
|
|
+ log.error("[OceanEngineConversion] 步骤5-回传失败, clickLogId={}, userId={}, eventType={}, httpCode={}, response={}, requestBody={}",
|
|
|
+ clickLog.getId(), clickLog.getUserId(), eventType, httpCode, responseBody, requestBodyJson);
|
|
|
+ }
|
|
|
+ return R.data(resultVo, success ? "转化回传成功" : "转化回传失败");
|
|
|
+ } catch (Exception ex) {
|
|
|
+ log.error("[OceanEngineConversion] 步骤5-回传异常, clickLogId={}, eventType={}, callback={}, url={}, requestBody={}",
|
|
|
+ clickLog.getId(), eventType, maskSensitive(callback), conversionReportUrl, requestBodyJson, ex);
|
|
|
+ resultVo.setSuccess(false);
|
|
|
+ resultVo.setMessage(ex.getMessage());
|
|
|
+ return R.data(resultVo, "转化回传异常:" + ex.getMessage());
|
|
|
+ } finally {
|
|
|
+ if (connection != null) {
|
|
|
+ connection.disconnect();
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建巨量转化回传请求体
|
|
|
+ */
|
|
|
+ private JSONObject buildConversionRequestBody(String eventType, String callback, int matchType,
|
|
|
+ String androidId, String idfa, String oaid, String imei,
|
|
|
+ long timestamp) {
|
|
|
+ JSONObject adContext = new JSONObject();
|
|
|
+ adContext.put("callback", callback);
|
|
|
+ adContext.put("match_type", matchType);
|
|
|
+
|
|
|
+ JSONObject deviceContext = new JSONObject();
|
|
|
+ if (StringUtils.isNotBlank(androidId)) {
|
|
|
+ deviceContext.put("android_id", androidId);
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(idfa)) {
|
|
|
+ deviceContext.put("idfa", idfa);
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(oaid)) {
|
|
|
+ deviceContext.put("oaid", oaid);
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(imei)) {
|
|
|
+ deviceContext.put("imei", imei);
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONObject context = new JSONObject();
|
|
|
+ context.put("ad", adContext);
|
|
|
+ if (!deviceContext.isEmpty()) {
|
|
|
+ context.put("device", deviceContext);
|
|
|
+ }
|
|
|
+
|
|
|
+ JSONObject body = new JSONObject();
|
|
|
+ body.put("event_type", eventType);
|
|
|
+ body.put("context", context);
|
|
|
+ body.put("timestamp", timestamp);
|
|
|
+ return body;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析用于回传的点击记录
|
|
|
+ * <p>查找优先级:id → clickId → userId(已绑定) → callbackParam → 入参直传</p>
|
|
|
+ */
|
|
|
+ private OceanEngineClickMonitorLog resolveClickLogForReport(OceanEngineClickMonitorLog clickLogParam) {
|
|
|
+ OceanEngineClickMonitorLog dbLog = null;
|
|
|
+ String resolveBy = null;
|
|
|
+
|
|
|
+ if (clickLogParam.getId() != null) {
|
|
|
+ resolveBy = "id";
|
|
|
+ dbLog = this.getById(clickLogParam.getId());
|
|
|
+ log.info("[OceanEngineConversion] 按id查库, id={}, found={}", clickLogParam.getId(), dbLog != null);
|
|
|
+ } else if (StringUtils.isNotBlank(clickLogParam.getClickId())) {
|
|
|
+ resolveBy = "clickId";
|
|
|
+ dbLog = findClickLog(clickLogParam.getClickId(), clickLogParam.getCallbackParam());
|
|
|
+ log.info("[OceanEngineConversion] 按clickId查库, clickId={}, found={}",
|
|
|
+ clickLogParam.getClickId(), dbLog != null);
|
|
|
+ } else if (clickLogParam.getUserId() != null) {
|
|
|
+ resolveBy = "userId";
|
|
|
+ dbLog = this.getOne(new LambdaQueryWrapper<OceanEngineClickMonitorLog>()
|
|
|
+ .eq(OceanEngineClickMonitorLog::getUserId, clickLogParam.getUserId())
|
|
|
+ .eq(OceanEngineClickMonitorLog::getBindStatus, OceanEngineClickMonitorLog.BIND_STATUS_BOUND)
|
|
|
+ .orderByDesc(OceanEngineClickMonitorLog::getBindTime)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ log.info("[OceanEngineConversion] 按userId查已绑定记录, userId={}, found={}",
|
|
|
+ clickLogParam.getUserId(), dbLog != null);
|
|
|
+ } else if (StringUtils.isNotBlank(clickLogParam.getCallbackParam())) {
|
|
|
+ resolveBy = "callbackParam";
|
|
|
+ dbLog = findClickLog(null, clickLogParam.getCallbackParam());
|
|
|
+ log.info("[OceanEngineConversion] 按callbackParam查库, callback={}, found={}",
|
|
|
+ maskSensitive(clickLogParam.getCallbackParam()), dbLog != null);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (dbLog == null) {
|
|
|
+ // 库中无记录时,入参自带 callback/clickId 则直接使用(联调/补报场景)
|
|
|
+ if (StringUtils.isNotBlank(clickLogParam.getCallbackParam())
|
|
|
+ || StringUtils.isNotBlank(clickLogParam.getClickId())) {
|
|
|
+ log.info("[OceanEngineConversion] 库无记录,使用入参直传, clickId={}, callback={}",
|
|
|
+ clickLogParam.getClickId(), maskSensitive(clickLogParam.getCallbackParam()));
|
|
|
+ return clickLogParam;
|
|
|
+ }
|
|
|
+ log.warn("[OceanEngineConversion] 查库失败且无直传字段, resolveBy={}", resolveBy);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ OceanEngineClickMonitorLog merged = mergeClickLogForReport(dbLog, clickLogParam);
|
|
|
+ log.info("[OceanEngineConversion] 记录合并完成, resolveBy={}, clickLogId={}", resolveBy, merged.getId());
|
|
|
+ return merged;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 库记录与入参合并:入参非空字段覆盖库记录(设备信息、callback 等)
|
|
|
+ */
|
|
|
+ private OceanEngineClickMonitorLog mergeClickLogForReport(OceanEngineClickMonitorLog dbLog,
|
|
|
+ OceanEngineClickMonitorLog param) {
|
|
|
+ if (StringUtils.isNotBlank(param.getCallbackParam())) {
|
|
|
+ log.debug("[OceanEngineConversion] 合并覆盖 callbackParam");
|
|
|
+ dbLog.setCallbackParam(param.getCallbackParam());
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(param.getClickId())) {
|
|
|
+ log.debug("[OceanEngineConversion] 合并覆盖 clickId");
|
|
|
+ dbLog.setClickId(param.getClickId());
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(param.getAndroidId())) {
|
|
|
+ log.debug("[OceanEngineConversion] 合并覆盖 androidId");
|
|
|
+ dbLog.setAndroidId(param.getAndroidId());
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(param.getIdfa())) {
|
|
|
+ log.debug("[OceanEngineConversion] 合并覆盖 idfa");
|
|
|
+ dbLog.setIdfa(param.getIdfa());
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(param.getOaid())) {
|
|
|
+ log.debug("[OceanEngineConversion] 合并覆盖 oaid");
|
|
|
+ dbLog.setOaid(param.getOaid());
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(param.getImei())) {
|
|
|
+ log.debug("[OceanEngineConversion] 合并覆盖 imei");
|
|
|
+ dbLog.setImei(param.getImei());
|
|
|
+ }
|
|
|
+ if (param.getUserId() != null) {
|
|
|
+ dbLog.setUserId(param.getUserId());
|
|
|
+ }
|
|
|
+ return dbLog;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从点击记录解析 callback:优先 callbackParam,其次 clickId
|
|
|
+ */
|
|
|
+ private String resolveCallbackFromClickLog(OceanEngineClickMonitorLog clickLog) {
|
|
|
+ if (StringUtils.isNotBlank(clickLog.getCallbackParam())) {
|
|
|
+ return clickLog.getCallbackParam();
|
|
|
+ }
|
|
|
+ return clickLog.getClickId();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析转化回传时间戳(毫秒)
|
|
|
+ * <p>优先级:conversionTimestamp → clickTsMs → clickTs*1000 → 当前时间</p>
|
|
|
+ */
|
|
|
+ private long resolveConversionTimestamp(OceanEngineClickMonitorLog clickLogParam,
|
|
|
+ OceanEngineClickMonitorLog clickLog) {
|
|
|
+ if (clickLogParam.getConversionTimestamp() != null) {
|
|
|
+ log.debug("[OceanEngineConversion] 时间戳来源=conversionTimestamp, value={}",
|
|
|
+ clickLogParam.getConversionTimestamp());
|
|
|
+ return clickLogParam.getConversionTimestamp();
|
|
|
+ }
|
|
|
+ if (clickLog.getClickTsMs() != null) {
|
|
|
+ log.debug("[OceanEngineConversion] 时间戳来源=clickTsMs, value={}", clickLog.getClickTsMs());
|
|
|
+ return clickLog.getClickTsMs();
|
|
|
+ }
|
|
|
+ if (clickLog.getClickTs() != null) {
|
|
|
+ long tsMs = clickLog.getClickTs() * 1000L;
|
|
|
+ log.debug("[OceanEngineConversion] 时间戳来源=clickTs(秒转毫秒), clickTs={}, tsMs={}",
|
|
|
+ clickLog.getClickTs(), tsMs);
|
|
|
+ return tsMs;
|
|
|
+ }
|
|
|
+ long now = System.currentTimeMillis();
|
|
|
+ log.debug("[OceanEngineConversion] 时间戳来源=当前时间, value={}", now);
|
|
|
+ return now;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 判断巨量引擎业务响应是否成功(HTTP 200 且 body.code 为 0 或空)
|
|
|
+ */
|
|
|
+ private boolean isOceanEngineSuccess(String responseBody) {
|
|
|
+ if (StringUtils.isBlank(responseBody)) {
|
|
|
+ log.debug("[OceanEngineConversion] 响应体为空,视为成功");
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ JSONObject json = JSONObject.parseObject(responseBody);
|
|
|
+ Integer code = json.getInteger("code");
|
|
|
+ String message = json.getString("message");
|
|
|
+ boolean success = code == null || code == 0;
|
|
|
+ if (!success) {
|
|
|
+ log.warn("[OceanEngineConversion] 巨量业务码非0, code={}, message={}, response={}",
|
|
|
+ code, message, responseBody);
|
|
|
+ }
|
|
|
+ return success;
|
|
|
+ } catch (Exception ex) {
|
|
|
+ log.warn("[OceanEngineConversion] 响应体解析失败,视为成功, response={}", responseBody, ex);
|
|
|
+ return true;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 敏感字段脱敏输出(日志排查用,保留前6后4位)
|
|
|
+ */
|
|
|
+ private String maskSensitive(String value) {
|
|
|
+ if (StringUtils.isBlank(value)) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ if (value.length() <= 12) {
|
|
|
+ return value.substring(0, 1) + "***" + "(len=" + value.length() + ")";
|
|
|
+ }
|
|
|
+ return value.substring(0, 6) + "***" + value.substring(value.length() - 4) + "(len=" + value.length() + ")";
|
|
|
+ }
|
|
|
+
|
|
|
+ private String readHttpResponse(HttpURLConnection connection, int httpCode) throws Exception {
|
|
|
+ java.io.InputStream inputStream = httpCode >= 400 ? connection.getErrorStream() : connection.getInputStream();
|
|
|
+ if (inputStream == null) {
|
|
|
+ return "";
|
|
|
+ }
|
|
|
+ StringBuilder builder = new StringBuilder();
|
|
|
+ try (BufferedReader reader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) {
|
|
|
+ String line;
|
|
|
+ while ((line = reader.readLine()) != null) {
|
|
|
+ builder.append(line);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return builder.toString();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 从 HttpServletRequest 提取全部 Query 参数
|
|
|
+ */
|
|
|
+ private Map<String, String> extractQueryParams(HttpServletRequest request) {
|
|
|
+ Map<String, String> paramMap = new LinkedHashMap<>();
|
|
|
+ Enumeration<String> names = request.getParameterNames();
|
|
|
+ while (names.hasMoreElements()) {
|
|
|
+ String name = names.nextElement();
|
|
|
+ paramMap.put(name, request.getParameter(name));
|
|
|
+ }
|
|
|
+ return paramMap;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 构建点击监测日志实体
|
|
|
+ */
|
|
|
+ private OceanEngineClickMonitorLog buildClickLog(HttpServletRequest request,
|
|
|
+ Map<String, String> paramMap,
|
|
|
+ String clickId,
|
|
|
+ String callbackParam,
|
|
|
+ String requestId) {
|
|
|
+ OceanEngineClickMonitorLog clickLog = new OceanEngineClickMonitorLog();
|
|
|
+ clickLog.setClickId(clickId);
|
|
|
+ clickLog.setCallbackParam(callbackParam);
|
|
|
+ clickLog.setRequestId(requestId);
|
|
|
+ clickLog.setAdvertiserId(firstNonBlank(paramMap, "advertiser_id", "ADVERTISER_ID"));
|
|
|
+ clickLog.setCampaignId(firstNonBlank(paramMap, "campaign_id", "CAMPAIGN_ID"));
|
|
|
+ clickLog.setAid(firstNonBlank(paramMap, "aid", "adid", "AID"));
|
|
|
+ clickLog.setCid(firstNonBlank(paramMap, "cid", "creativeid", "CID"));
|
|
|
+ clickLog.setProjectId(firstNonBlank(paramMap, "project_id", "PROJECT_ID"));
|
|
|
+ clickLog.setPromotionId(firstNonBlank(paramMap, "promotion_id", "PROMOTION_ID"));
|
|
|
+ clickLog.setConvertId(firstNonBlank(paramMap, "convert_id", "CONVERT_ID"));
|
|
|
+ clickLog.setCsite(firstNonBlank(paramMap, "csite", "CSITE"));
|
|
|
+ clickLog.setUnionSite(firstNonBlank(paramMap, "union_site", "UNION_SITE"));
|
|
|
+ clickLog.setMid1(firstNonBlank(paramMap, "mid1", "MID1"));
|
|
|
+ clickLog.setMid2(firstNonBlank(paramMap, "mid2", "MID2"));
|
|
|
+ clickLog.setMid3(firstNonBlank(paramMap, "mid3", "MID3"));
|
|
|
+ clickLog.setMid4(firstNonBlank(paramMap, "mid4", "MID4"));
|
|
|
+ clickLog.setMid5(firstNonBlank(paramMap, "mid5", "MID5"));
|
|
|
+ clickLog.setMid6(firstNonBlank(paramMap, "mid6", "MID6"));
|
|
|
+ clickLog.setClickTs(parseLong(firstNonBlank(paramMap, "ts", "TIMESTAMP", "TS")));
|
|
|
+ clickLog.setClickTsMs(parseLong(firstNonBlank(paramMap, "ts_ms", "TS_MS")));
|
|
|
+ clickLog.setClientIp(firstNonBlank(paramMap, "ip", "IP"));
|
|
|
+ clickLog.setClientIpv4(firstNonBlank(paramMap, "ipv4", "IPV4"));
|
|
|
+ clickLog.setClientIpv6(firstNonBlank(paramMap, "ipv6", "IPV6"));
|
|
|
+ clickLog.setUserAgent(firstNonBlank(paramMap, "ua", "UA"));
|
|
|
+ clickLog.setOsType(firstNonBlank(paramMap, "os", "OS"));
|
|
|
+ clickLog.setDeviceModel(firstNonBlank(paramMap, "model", "MODEL"));
|
|
|
+ clickLog.setIdfa(firstNonBlank(paramMap, "idfa", "IDFA"));
|
|
|
+ clickLog.setImei(firstNonBlank(paramMap, "imei", "IMEI"));
|
|
|
+ clickLog.setOaid(firstNonBlank(paramMap, "oaid", "OAID"));
|
|
|
+ clickLog.setOaidMd5(firstNonBlank(paramMap, "oaid_md5", "OAID_MD5"));
|
|
|
+ clickLog.setAndroidId(firstNonBlank(paramMap, "android_id", "ANDROIDID", "ANDROIDID1"));
|
|
|
+ clickLog.setAdVersion(resolveAdVersion(clickLog));
|
|
|
+ clickLog.setBindStatus(OceanEngineClickMonitorLog.BIND_STATUS_UNBOUND);
|
|
|
+ clickLog.setMonitorUrl(buildMonitorUrl(request));
|
|
|
+ clickLog.setRawParams(JSONObject.toJSONString(paramMap));
|
|
|
+ clickLog.setServerIp(resolveClientIp(request));
|
|
|
+ return clickLog;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 推断广告版本:2.0 有 project_id/promotion_id,1.0 有 aid/cid/campaign_id
|
|
|
+ */
|
|
|
+ private Integer resolveAdVersion(OceanEngineClickMonitorLog clickLog) {
|
|
|
+ if (StringUtils.isNotBlank(clickLog.getProjectId())
|
|
|
+ || StringUtils.isNotBlank(clickLog.getPromotionId())) {
|
|
|
+ return OceanEngineClickMonitorLog.AD_VERSION_V2;
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(clickLog.getAid())
|
|
|
+ || StringUtils.isNotBlank(clickLog.getCid())
|
|
|
+ || StringUtils.isNotBlank(clickLog.getCampaignId())) {
|
|
|
+ return OceanEngineClickMonitorLog.AD_VERSION_V1;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private OceanEngineClickMonitorLog findExistingLog(String clickId, String requestId) {
|
|
|
+ if (StringUtils.isNotBlank(clickId)) {
|
|
|
+ OceanEngineClickMonitorLog byClickId = this.getOne(new LambdaQueryWrapper<OceanEngineClickMonitorLog>()
|
|
|
+ .eq(OceanEngineClickMonitorLog::getClickId, clickId)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ if (byClickId != null) {
|
|
|
+ return byClickId;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(requestId)) {
|
|
|
+ return this.getOne(new LambdaQueryWrapper<OceanEngineClickMonitorLog>()
|
|
|
+ .eq(OceanEngineClickMonitorLog::getRequestId, requestId)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private OceanEngineClickMonitorLog findClickLog(String clickId, String callbackParam) {
|
|
|
+ if (StringUtils.isNotBlank(clickId)) {
|
|
|
+ OceanEngineClickMonitorLog log = this.getOne(new LambdaQueryWrapper<OceanEngineClickMonitorLog>()
|
|
|
+ .eq(OceanEngineClickMonitorLog::getClickId, clickId)
|
|
|
+ .orderByDesc(OceanEngineClickMonitorLog::getCreatedTime)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ if (log != null) {
|
|
|
+ return log;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ if (StringUtils.isNotBlank(callbackParam)) {
|
|
|
+ return this.getOne(new LambdaQueryWrapper<OceanEngineClickMonitorLog>()
|
|
|
+ .eq(OceanEngineClickMonitorLog::getCallbackParam, callbackParam)
|
|
|
+ .orderByDesc(OceanEngineClickMonitorLog::getCreatedTime)
|
|
|
+ .last("LIMIT 1"));
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private String buildMonitorUrl(HttpServletRequest request) {
|
|
|
+ String queryString = request.getQueryString();
|
|
|
+ if (StringUtils.isBlank(queryString)) {
|
|
|
+ return request.getRequestURL().toString();
|
|
|
+ }
|
|
|
+ return request.getRequestURL() + "?" + queryString;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 解析客户端真实 IP(兼容反向代理)
|
|
|
+ */
|
|
|
+ private String resolveClientIp(HttpServletRequest request) {
|
|
|
+ String ip = request.getHeader("X-Forwarded-For");
|
|
|
+ if (StringUtils.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
|
|
|
+ return ip.split(",")[0].trim();
|
|
|
+ }
|
|
|
+ ip = request.getHeader("X-Real-IP");
|
|
|
+ if (StringUtils.isNotBlank(ip) && !"unknown".equalsIgnoreCase(ip)) {
|
|
|
+ return ip;
|
|
|
+ }
|
|
|
+ return request.getRemoteAddr();
|
|
|
+ }
|
|
|
+
|
|
|
+ private String firstNonBlank(Map<String, String> paramMap, String... keys) {
|
|
|
+ for (String key : keys) {
|
|
|
+ String value = paramMap.get(key);
|
|
|
+ if (StringUtils.isNotBlank(value)) {
|
|
|
+ return value;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private Long parseLong(String value) {
|
|
|
+ if (StringUtils.isBlank(value)) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return Long.parseLong(value);
|
|
|
+ } catch (NumberFormatException ex) {
|
|
|
+ log.warn("OceanEngineClickMonitorLogServiceImpl.parseLong, invalid number={}", value);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|