Эх сурвалжийг харах

营销计划关联发件箱逻辑和是否拒收逻辑

chenkq 1 өдөр өмнө
parent
commit
cc636dcc2c

+ 1 - 1
storlead-mail-api/src/main/java/com/storlead/mail/mkt/MktMailApiController.java

@@ -246,7 +246,7 @@ public class MktMailApiController {
 
     @PostMapping("/list_by_ids")
     @ApiOperation("按邮件ID列表查询(不含正文 content)")
-    public Result<?> listByIds(@RequestBody List<Long> ids) {
+    public Result<?> listByIds(@RequestParam("ids") List<Long> ids) {
         Long userId = LoginUserUtil.getCurrentUserId();
         if (Objects.isNull(userId)) {
             return Result.error("未登录");

+ 3 - 0
storlead-mail/storlead-mail-common/src/main/resources/sql/mkt_mail_recreate.sql

@@ -88,6 +88,9 @@ CREATE TABLE mkt_mail_attachment (
 CREATE TABLE mkt_mail_status (
     id                bigint       NOT NULL AUTO_INCREMENT COMMENT '主键',
     mail_id           bigint       NOT NULL COMMENT '邮件ID,对应 mkt_mail_message.id',
+    outbox_id         bigint       NULL COMMENT '发送邮箱账户ID(smtp_pop_settings.id)',
+    outbox            varchar(255) NULL COMMENT '发送邮箱地址',
+    has_refuse        tinyint      NOT NULL DEFAULT 0 COMMENT '是否拒收:0否 1是',
     send_status       tinyint      NOT NULL DEFAULT 0 COMMENT '发送状态:-2草稿 -1定时 0发送中 1成功 2失败',
     opened            tinyint      NOT NULL DEFAULT 0 COMMENT '是否已打开(像素):0否 1是',
     open_count        int          NOT NULL DEFAULT 0 COMMENT '打开次数(每次像素回调+1)',

+ 3 - 0
storlead-mail/storlead-mail-common/src/main/resources/sql/mkt_mail_status.sql

@@ -2,6 +2,9 @@
 CREATE TABLE IF NOT EXISTS mkt_mail_status (
     id                bigint       NOT NULL AUTO_INCREMENT COMMENT '主键',
     mail_id           bigint       NOT NULL COMMENT '邮件ID,对应 mkt_mail_message.id',
+    outbox_id         bigint       NULL COMMENT '发送邮箱账户ID(smtp_pop_settings.id)',
+    outbox            varchar(255) NULL COMMENT '发送邮箱地址',
+    has_refuse        tinyint      NOT NULL DEFAULT 0 COMMENT '是否拒收:0否 1是',
 
     send_status       tinyint      NOT NULL DEFAULT 0 COMMENT '发送状态:-2草稿 -1定时 0发送中 1成功 2失败',
     opened            tinyint      NOT NULL DEFAULT 0 COMMENT '是否已打开(像素):0否 1是',

+ 5 - 0
storlead-mail/storlead-mail-common/src/main/resources/sql/mkt_mail_status_add_outbox_refuse.sql

@@ -0,0 +1,5 @@
+-- mkt_mail_status:发送邮箱 / 是否拒收
+ALTER TABLE mkt_mail_status
+    ADD COLUMN outbox_id bigint NULL COMMENT '发送邮箱账户ID(smtp_pop_settings.id)' AFTER mail_id,
+    ADD COLUMN outbox varchar(255) NULL COMMENT '发送邮箱地址' AFTER outbox_id,
+    ADD COLUMN has_refuse tinyint NOT NULL DEFAULT 0 COMMENT '是否拒收:0否 1是' AFTER outbox;

+ 12 - 0
storlead-mail/storlead-mail-mkt/src/main/java/com/storlead/sales/mail/mkt/entity/MktMailStatusEntity.java

@@ -37,6 +37,18 @@ public class MktMailStatusEntity extends SysBaseField {
     @TableField("mail_id")
     private Long mailId;
 
+    @ApiModelProperty("发送邮箱账户ID(smtp_pop_settings.id)")
+    @TableField("outbox_id")
+    private Long outboxId;
+
+    @ApiModelProperty("发送邮箱地址")
+    @TableField("outbox")
+    private String outbox;
+
+    @ApiModelProperty("是否拒收:0否 1是")
+    @TableField("has_refuse")
+    private Integer hasRefuse;
+
     @ApiModelProperty("发送状态:-2草稿 -1定时 0发送中 1成功 2失败")
     @TableField("send_status")
     private Integer sendStatus;

+ 41 - 5
storlead-mail/storlead-mail-mkt/src/main/java/com/storlead/sales/mail/mkt/receive/MktMailReceivePersist.java

@@ -23,6 +23,7 @@ import com.storlead.sales.mail.mkt.enums.MktMailFolderEnum;
 import com.storlead.sales.mail.mkt.service.MktMailAttachmentService;
 import com.storlead.sales.mail.mkt.service.MktMailBlacklistService;
 import com.storlead.sales.mail.mkt.service.MktMailMessageService;
+import com.storlead.sales.mail.mkt.service.MktMailStatusService;
 import com.storlead.sales.mail.mkt.support.MktBounceDetector;
 import com.storlead.sales.mail.mkt.support.MktMailHeaders;
 import lombok.extern.log4j.Log4j2;
@@ -68,6 +69,9 @@ public class MktMailReceivePersist implements MailReceivePersistSpi {
     @Resource
     private MktMailBlacklistService mktMailBlacklistService;
 
+    @Resource
+    private MktMailStatusService mktMailStatusService;
+
     @Resource
     private MailFileProperties mailFileProperties;
 
@@ -171,11 +175,16 @@ public class MktMailReceivePersist implements MailReceivePersistSpi {
             entity.setOpened(0);
             entity.setOpenCount(0);
 
-            bindReply(account.getId(), message, draft, entity);
+            String bounceProbe = joinText(entity.getSubject(), body);
+            boolean bounceMail = MktBounceDetector.looksLikeBounceContent(bounceProbe);
+            // 退信不当客户回复绑定
+            if (!bounceMail) {
+                bindReply(account.getId(), message, draft, entity);
+            }
 
             if (mktMailMessageService.save(entity)) {
                 // 落库后才有回复邮件 ID,再标记原信已回复并写入首次回复邮件/主题
-                if (entity.getReplyMsgId() != null) {
+                if (!bounceMail && entity.getReplyMsgId() != null) {
                     try {
                         mktMailMessageService.markReplied(
                                 entity.getReplyMsgId(), entity.getId(), entity.getSubject());
@@ -388,7 +397,7 @@ public class MktMailReceivePersist implements MailReceivePersistSpi {
     }
 
     /**
-     * 用已有正文做退信入黑(不读 Message;由 updateBody / 拉头已带正文 触发)。
+     * 退信检测:入黑名单,并按失败收件人回写原发出信 has_refuse。
      */
     private void handleBounceIfNeeded(MktMailMessageEntity mail, String body) {
         try {
@@ -396,23 +405,28 @@ public class MktMailReceivePersist implements MailReceivePersistSpi {
             if (!MktBounceDetector.looksLikeBounceContent(content)) {
                 return;
             }
-            List<String> failed = MktBounceDetector.extractFailedRecipients(content, null);
+            // 排除本账户邮箱,避免把退信接收邮箱当成失败地址
+            String excludeAddr = mail == null ? null : mail.getRecipient();
+            List<String> failed = MktBounceDetector.extractFailedRecipients(content, excludeAddr);
             if (CollectionUtils.isEmpty(failed)) {
                 log.info("mkt bounce content detected but no recipient extracted, mailId={}",
                         mail == null ? null : mail.getId());
                 return;
             }
             String reason = MktBounceDetector.buildBounceReason(content);
+            Long accountId = mail == null ? null : mail.getAccountId();
             for (String email : failed) {
                 mktMailBlacklistService.addOrRefresh(
                         email,
                         reason,
                         MktBlacklistSource.BOUNCE,
                         mail == null ? null : mail.getOwnerBy(),
-                        mail == null ? null : mail.getAccountId(),
+                        accountId,
                         mail == null ? null : mail.getId());
                 log.info("mkt bounce auto blacklist, email={}, mailId={}",
                         email, mail == null ? null : mail.getId());
+                // 回写对应发出信拒收标记
+                markOutboundRefused(accountId, email);
             }
         } catch (Exception e) {
             log.error("mkt handleBounceIfNeeded error, mailId={}",
@@ -420,6 +434,28 @@ public class MktMailReceivePersist implements MailReceivePersistSpi {
         }
     }
 
+    /**
+     * 按失败收件人找本账户最新一封发出信,标记 has_refuse。
+     */
+    private void markOutboundRefused(Long accountId, String failedEmail) {
+        if (accountId == null || !StringUtils.hasText(failedEmail)) {
+            return;
+        }
+        List<MktMailMessageEntity> outbounds = listOutboundToRecipient(accountId, failedEmail.trim().toLowerCase());
+        if (CollectionUtils.isEmpty(outbounds)) {
+            log.info("mkt bounce no outbound matched, accountId={}, email={}", accountId, failedEmail);
+            return;
+        }
+        MktMailMessageEntity outbound = outbounds.get(0);
+        try {
+            mktMailStatusService.markRefused(outbound.getId());
+            log.info("mkt bounce markRefused, outboundMailId={}, email={}", outbound.getId(), failedEmail);
+        } catch (Exception e) {
+            log.error("mkt bounce markRefused error, outboundMailId={}, email={}",
+                    outbound.getId(), failedEmail, e);
+        }
+    }
+
     private static String joinText(String subject, String body) {
         StringBuilder sb = new StringBuilder();
         if (StringUtils.hasText(subject)) {

+ 5 - 0
storlead-mail/storlead-mail-mkt/src/main/java/com/storlead/sales/mail/mkt/service/MktMailStatusService.java

@@ -36,6 +36,11 @@ public interface MktMailStatusService extends MyBaseService<MktMailStatusEntity>
      */
     void markReplied(Long mailId, Long firstReplyEmailId, String firstReplyTheme);
 
+    /**
+     * 退信 / SMTP 拒收:标记 has_refuse=1,并刷新 status_updated_at。
+     */
+    void markRefused(Long mailId);
+
     /**
      * 业务侧同步完成后回写同步时间(synced_at)。
      */

+ 84 - 2
storlead-mail/storlead-mail-mkt/src/main/java/com/storlead/sales/mail/mkt/service/impl/MktMailStatusServiceImpl.java

@@ -4,8 +4,11 @@ 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.mybatis.service.impl.MyBaseServiceImpl;
+import com.storlead.sales.mail.common.entity.SmtpPopSettingsEntity;
+import com.storlead.sales.mail.common.service.SmtpPopSettingsService;
 import com.storlead.sales.mail.mkt.entity.MktMailMessageEntity;
 import com.storlead.sales.mail.mkt.entity.MktMailStatusEntity;
+import com.storlead.sales.mail.mkt.mapper.MktMailMessageMapper;
 import com.storlead.sales.mail.mkt.mapper.MktMailStatusMapper;
 import com.storlead.sales.mail.mkt.service.MktMailStatusService;
 import lombok.extern.log4j.Log4j2;
@@ -14,6 +17,7 @@ import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.StringUtils;
 
+import javax.annotation.Resource;
 import java.util.Collection;
 import java.util.Date;
 
@@ -25,15 +29,43 @@ import java.util.Date;
 public class MktMailStatusServiceImpl extends MyBaseServiceImpl<MktMailStatusMapper, MktMailStatusEntity>
         implements MktMailStatusService {
 
+    @Resource
+    private MktMailMessageMapper mktMailMessageMapper;
+
+    @Resource
+    private SmtpPopSettingsService smtpPopSettingsService;
+
     @Override
     public void ensureStatus(MktMailMessageEntity mail) {
         if (mail == null || mail.getId() == null) {
             return;
         }
+        Long outboxId = mail.getAccountId();
+        String outbox = resolveOutboxAddr(mail.getFromAddr(), outboxId);
+
         MktMailStatusEntity existing = getByMailId(mail.getId());
         if (existing != null) {
+            LambdaUpdateWrapper<MktMailStatusEntity> patch = new LambdaUpdateWrapper<>();
+            patch.eq(MktMailStatusEntity::getMailId, mail.getId());
+            patch.eq(MktMailStatusEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
+            boolean needPatch = false;
             if (mail.getStatus() != null && !mail.getStatus().equals(existing.getSendStatus())) {
-                markSendStatus(mail.getId(), mail.getStatus());
+                patch.set(MktMailStatusEntity::getSendStatus, mail.getStatus());
+                needPatch = true;
+            }
+            if (existing.getOutboxId() == null && outboxId != null) {
+                patch.set(MktMailStatusEntity::getOutboxId, outboxId);
+                needPatch = true;
+            }
+            if (!StringUtils.hasText(existing.getOutbox()) && StringUtils.hasText(outbox)) {
+                patch.set(MktMailStatusEntity::getOutbox, outbox);
+                needPatch = true;
+            }
+            if (needPatch) {
+                Date now = new Date();
+                patch.set(MktMailStatusEntity::getStatusUpdatedAt, now);
+                patch.set(MktMailStatusEntity::getUpdateTime, now);
+                this.update(patch);
             }
             return;
         }
@@ -41,6 +73,9 @@ public class MktMailStatusServiceImpl extends MyBaseServiceImpl<MktMailStatusMap
         Date now = new Date();
         MktMailStatusEntity row = new MktMailStatusEntity();
         row.setMailId(mail.getId());
+        row.setOutboxId(outboxId);
+        row.setOutbox(outbox);
+        row.setHasRefuse(0);
         row.setSendStatus(mail.getStatus() == null ? 0 : mail.getStatus());
         row.setOpened(0);
         row.setOpenCount(0);
@@ -141,6 +176,23 @@ public class MktMailStatusServiceImpl extends MyBaseServiceImpl<MktMailStatusMap
         this.update(update);
     }
 
+    @Override
+    public void markRefused(Long mailId) {
+        if (mailId == null) {
+            return;
+        }
+        ensureMinimalRow(mailId);
+        Date now = new Date();
+        LambdaUpdateWrapper<MktMailStatusEntity> update = new LambdaUpdateWrapper<>();
+        update.eq(MktMailStatusEntity::getMailId, mailId);
+        update.eq(MktMailStatusEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
+        update.set(MktMailStatusEntity::getHasRefuse, 1);
+        update.set(MktMailStatusEntity::getStatusUpdatedAt, now);
+        update.set(MktMailStatusEntity::getUpdateTime, now);
+        this.update(update);
+        log.info("mkt_mail_status markRefused, mailId={}", mailId);
+    }
+
     /**
      * 将回复邮件 ID 追加到逗号串;已存在则原样返回;超过 200 字符则不再追加。
      */
@@ -200,18 +252,31 @@ public class MktMailStatusServiceImpl extends MyBaseServiceImpl<MktMailStatusMap
                 .last("limit 1"));
     }
 
+    /**
+     * 兜底建行:从原信补齐 outbox_id / outbox。
+     */
     private void ensureMinimalRow(Long mailId) {
         if (getByMailId(mailId) != null) {
             return;
         }
+        MktMailMessageEntity mail = mktMailMessageMapper.selectById(mailId);
         Date now = new Date();
         MktMailStatusEntity row = new MktMailStatusEntity();
         row.setMailId(mailId);
-        row.setSendStatus(0);
+        if (mail != null) {
+            row.setOutboxId(mail.getAccountId());
+            row.setOutbox(resolveOutboxAddr(mail.getFromAddr(), mail.getAccountId()));
+            row.setOwnerBy(mail.getOwnerBy());
+            row.setCreateBy(mail.getCreateBy() == null ? mail.getOwnerBy() : mail.getCreateBy());
+            row.setSendStatus(mail.getStatus() == null ? 0 : mail.getStatus());
+        } else {
+            row.setSendStatus(0);
+        }
         row.setOpened(0);
         row.setOpenCount(0);
         row.setIsRead(0);
         row.setReplied(0);
+        row.setHasRefuse(0);
         row.setStatusUpdatedAt(now);
         row.setSyncedAt(null);
         row.setCreateTime(now);
@@ -226,4 +291,21 @@ public class MktMailStatusServiceImpl extends MyBaseServiceImpl<MktMailStatusMap
             log.error("ensureMinimalRow error, mailId={}", mailId, e);
         }
     }
+
+    /** 优先用邮件 from_addr;为空则按账户 ID 查邮箱地址。 */
+    private String resolveOutboxAddr(String fromAddr, Long accountId) {
+        if (StringUtils.hasText(fromAddr)) {
+            return fromAddr.trim();
+        }
+        if (accountId == null) {
+            return null;
+        }
+        try {
+            SmtpPopSettingsEntity account = smtpPopSettingsService.getById(accountId);
+            return account == null ? null : account.getEmailAddress();
+        } catch (Exception e) {
+            log.warn("resolve outbox by accountId failed, accountId={}", accountId, e);
+            return null;
+        }
+    }
 }

+ 9 - 2
storlead-mail/storlead-mail-mkt/src/main/java/com/storlead/sales/mail/mkt/util/MktMailSender.java

@@ -243,8 +243,7 @@ public class MktMailSender {
     }
 
     /**
-     * 仅 {@link MailSendStatus#SERVER_EXCEPTION_550}:把拒收地址写入黑名单。
-     * 优先用异常里的无效地址,没有则回落本封的主收件人。
+     * {@link MailSendStatus#SERVER_EXCEPTION_550} / SendFailed:拒收地址入黑名单,并标记本封 has_refuse。
      */
     private void blacklistOn550(MktMailMessageEntity mail, SendFailedException exception) {
         if (mail == null) {
@@ -275,6 +274,14 @@ public class MktMailSender {
                 log.error("mkt add smtp 550 blacklist error, mailId={}, email={}", mail.getId(), email, e);
             }
         }
+        // 当场拒收:标记本封发出信
+        try {
+            if (mail.getId() != null) {
+                mktMailStatusService.markRefused(mail.getId());
+            }
+        } catch (Exception e) {
+            log.error("mkt 550 markRefused error, mailId={}", mail.getId(), e);
+        }
     }
 
     private static List<String> addressList(Address[] addresses) {