|
@@ -0,0 +1,201 @@
|
|
|
|
|
+package com.storlead.trade.service.impl;
|
|
|
|
|
+
|
|
|
|
|
+import com.baomidou.dynamic.datasource.annotation.DS;
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
|
|
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
|
|
|
+import com.storlead.framework.common.constant.CommonConstant;
|
|
|
|
|
+import com.storlead.framework.common.constant.DSConstants;
|
|
|
|
|
+import com.storlead.framework.common.result.Result;
|
|
|
|
|
+import com.storlead.mail.webclient.client.MktMailRemoteClient;
|
|
|
|
|
+import com.storlead.mail.webclient.dto.MktMailStatusChangeRemoteDTO;
|
|
|
|
|
+import com.storlead.mail.webclient.dto.MktMailStatusSyncQueryRemoteDTO;
|
|
|
|
|
+import com.storlead.trade.entity.MailStatusSyncCursorEntity;
|
|
|
|
|
+import com.storlead.trade.entity.MarketEmailsEntity;
|
|
|
|
|
+import com.storlead.trade.service.MailStatusSyncCursorService;
|
|
|
|
|
+import com.storlead.trade.service.MarketEmailsService;
|
|
|
|
|
+import com.storlead.trade.service.MarketEmailsStatusSyncService;
|
|
|
|
|
+import lombok.extern.slf4j.Slf4j;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+import org.springframework.util.CollectionUtils;
|
|
|
|
|
+
|
|
|
|
|
+import javax.annotation.Resource;
|
|
|
|
|
+import java.time.LocalDateTime;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * 从邮件服务增量拉取 mkt_mail_status,按 email_id 回写 market_emails。
|
|
|
|
|
+ */
|
|
|
|
|
+@Slf4j
|
|
|
|
|
+@Service
|
|
|
|
|
+@DS(DSConstants.DATASOURCE_TRADE)
|
|
|
|
|
+public class MarketEmailsStatusSyncServiceImpl implements MarketEmailsStatusSyncService {
|
|
|
|
|
+
|
|
|
|
|
+ public static final String SOURCE_MKT_MAIL_STATUS = "mkt_mail_status";
|
|
|
|
|
+
|
|
|
|
|
+ private static final int BATCH_LIMIT = 200;
|
|
|
|
|
+ private static final int MAX_PAGES = 50;
|
|
|
|
|
+
|
|
|
|
|
+ /** trade.status:0草稿/待发,1已发送,2发送失败 */
|
|
|
|
|
+ private static final int TRADE_STATUS_DRAFT = 0;
|
|
|
|
|
+ private static final int TRADE_STATUS_SENT = 1;
|
|
|
|
|
+ private static final int TRADE_STATUS_FAIL = 2;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private MktMailRemoteClient mktMailRemoteClient;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private MailStatusSyncCursorService mailStatusSyncCursorService;
|
|
|
|
|
+
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private MarketEmailsService marketEmailsService;
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public int syncOnce() {
|
|
|
|
|
+ MailStatusSyncCursorEntity cursor = mailStatusSyncCursorService.getOrInit(SOURCE_MKT_MAIL_STATUS);
|
|
|
|
|
+ LocalDateTime since = cursor.getLastStatusUpdatedAt();
|
|
|
|
|
+ Long lastId = cursor.getLastId() == null ? 0L : cursor.getLastId();
|
|
|
|
|
+
|
|
|
|
|
+ int totalUpdated = 0;
|
|
|
|
|
+ for (int page = 0; page < MAX_PAGES; page++) {
|
|
|
|
|
+ MktMailStatusSyncQueryRemoteDTO query = new MktMailStatusSyncQueryRemoteDTO();
|
|
|
|
|
+ query.setSince(since);
|
|
|
|
|
+ query.setLastId(lastId);
|
|
|
|
|
+ query.setLimit(BATCH_LIMIT);
|
|
|
|
|
+
|
|
|
|
|
+ Result<List<MktMailStatusChangeRemoteDTO>> result;
|
|
|
|
|
+ try {
|
|
|
|
|
+ result = mktMailRemoteClient.statusChanges(query);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("拉取营销邮件状态失败 since={} lastId={}", since, lastId, e);
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (result == null || !result.isSuccess()) {
|
|
|
|
|
+ log.warn("拉取营销邮件状态返回失败 message={}", result == null ? null : result.getMessage());
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ List<MktMailStatusChangeRemoteDTO> changes = result.getResult();
|
|
|
|
|
+ if (CollectionUtils.isEmpty(changes)) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ LocalDateTime batchMaxTime = since;
|
|
|
|
|
+ Long batchMaxId = lastId;
|
|
|
|
|
+ int pageUpdated = 0;
|
|
|
|
|
+ for (MktMailStatusChangeRemoteDTO change : changes) {
|
|
|
|
|
+ if (change == null || change.getMailId() == null) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (applyChange(change)) {
|
|
|
|
|
+ pageUpdated++;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (change.getStatusUpdatedAt() != null) {
|
|
|
|
|
+ if (batchMaxTime == null || change.getStatusUpdatedAt().isAfter(batchMaxTime)
|
|
|
|
|
+ || (change.getStatusUpdatedAt().equals(batchMaxTime)
|
|
|
|
|
+ && change.getId() != null && change.getId() > batchMaxId)) {
|
|
|
|
|
+ batchMaxTime = change.getStatusUpdatedAt();
|
|
|
|
|
+ batchMaxId = change.getId() == null ? batchMaxId : change.getId();
|
|
|
|
|
+ }
|
|
|
|
|
+ } else if (change.getId() != null && change.getId() > batchMaxId) {
|
|
|
|
|
+ batchMaxId = change.getId();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 本批处理完再推进游标(失败不推进则下次重拉,更新需幂等)
|
|
|
|
|
+ since = batchMaxTime;
|
|
|
|
|
+ lastId = batchMaxId;
|
|
|
|
|
+ mailStatusSyncCursorService.advance(SOURCE_MKT_MAIL_STATUS, since, lastId);
|
|
|
|
|
+ totalUpdated += pageUpdated;
|
|
|
|
|
+
|
|
|
|
|
+ if (changes.size() < BATCH_LIMIT) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return totalUpdated;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 按 email_id 回写;发送状态不降级;已读/已回复只升不降。
|
|
|
|
|
+ */
|
|
|
|
|
+ private boolean applyChange(MktMailStatusChangeRemoteDTO change) {
|
|
|
|
|
+ MarketEmailsEntity existing = marketEmailsService.getOne(new LambdaQueryWrapper<MarketEmailsEntity>()
|
|
|
|
|
+ .eq(MarketEmailsEntity::getEmailId, change.getMailId())
|
|
|
|
|
+ .eq(MarketEmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
|
|
|
|
|
+ .last("limit 1"));
|
|
|
|
|
+ if (existing == null) {
|
|
|
|
|
+ log.debug("market_emails 无 email_id={} 记录,跳过", change.getMailId());
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Integer mappedStatus = mapSendStatus(change.getSendStatus());
|
|
|
|
|
+ Integer targetReader = resolveReader(change);
|
|
|
|
|
+ Integer targetReply = Integer.valueOf(1).equals(change.getReplied()) ? 1 : null;
|
|
|
|
|
+
|
|
|
|
|
+ LambdaUpdateWrapper<MarketEmailsEntity> update = new LambdaUpdateWrapper<>();
|
|
|
|
|
+ update.eq(MarketEmailsEntity::getId, existing.getId());
|
|
|
|
|
+ boolean changed = false;
|
|
|
|
|
+
|
|
|
|
|
+ if (mappedStatus != null && shouldUpgradeSendStatus(existing.getStatus(), mappedStatus)) {
|
|
|
|
|
+ update.set(MarketEmailsEntity::getStatus, mappedStatus);
|
|
|
|
|
+ changed = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (Integer.valueOf(1).equals(targetReader)
|
|
|
|
|
+ && !Integer.valueOf(1).equals(existing.getHasReader())) {
|
|
|
|
|
+ update.set(MarketEmailsEntity::getHasReader, 1);
|
|
|
|
|
+ changed = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (Integer.valueOf(1).equals(targetReply)
|
|
|
|
|
+ && !Integer.valueOf(1).equals(existing.getHasReply())) {
|
|
|
|
|
+ update.set(MarketEmailsEntity::getHasReply, 1);
|
|
|
|
|
+ changed = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!changed) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return marketEmailsService.update(update);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static Integer mapSendStatus(Integer sendStatus) {
|
|
|
|
|
+ if (sendStatus == null) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sendStatus == 1) {
|
|
|
|
|
+ return TRADE_STATUS_SENT;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sendStatus == 2) {
|
|
|
|
|
+ return TRADE_STATUS_FAIL;
|
|
|
|
|
+ }
|
|
|
|
|
+ // -2草稿 -1定时 0发送中 → trade 0
|
|
|
|
|
+ return TRADE_STATUS_DRAFT;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 已打开或已读都视为 has_reader=1 */
|
|
|
|
|
+ private static Integer resolveReader(MktMailStatusChangeRemoteDTO change) {
|
|
|
|
|
+ if (Integer.valueOf(1).equals(change.getOpened()) || Integer.valueOf(1).equals(change.getIsRead())) {
|
|
|
|
|
+ return 1;
|
|
|
|
|
+ }
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 发送状态:成功不降级;失败可升为成功;草稿可变为成功/失败。
|
|
|
|
|
+ */
|
|
|
|
|
+ private static boolean shouldUpgradeSendStatus(Integer current, Integer mapped) {
|
|
|
|
|
+ if (mapped == null) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (current == null) {
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (current.equals(mapped)) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (TRADE_STATUS_SENT == current) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (TRADE_STATUS_FAIL == current) {
|
|
|
|
|
+ return TRADE_STATUS_SENT == mapped;
|
|
|
|
|
+ }
|
|
|
|
|
+ // 当前为草稿/待发
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|