浏览代码

回复记录

chenkq 4 天之前
父节点
当前提交
0f5fe0fa23

+ 30 - 0
storlead-mail/storlead-mail-common/src/main/resources/sql/mkt_mail_reply_record.sql

@@ -0,0 +1,30 @@
+-- 营销邮件回复沟通记录(每次客户回复原信落一行)
+CREATE TABLE IF NOT EXISTS mkt_mail_reply_record (
+    id               bigint       NOT NULL AUTO_INCREMENT COMMENT '主键',
+    mail_id          bigint       NOT NULL COMMENT '原信(发出信)ID,对应 mkt_mail_message.id',
+    reply_mail_id    bigint       NOT NULL COMMENT '回复邮件ID,对应 mkt_mail_message.id',
+    account_id       bigint       NULL COMMENT '发信账户ID',
+    org_id           bigint       NULL COMMENT '组织ID',
+    biz_ref          varchar(64)  NULL COMMENT '业务单号',
+    biz_batch_no     varchar(64)  NULL COMMENT '批次号',
+    original_subject varchar(512) NULL COMMENT '原信主题',
+    reply_subject    varchar(512) NULL COMMENT '回复邮件主题',
+    reply_from       varchar(255) NULL COMMENT '回复人邮箱',
+    reply_from_name  varchar(255) NULL COMMENT '回复人显示名',
+    reply_time       datetime     NULL COMMENT '回复时间(收信/落库时间)',
+    is_first_reply   tinyint      NOT NULL DEFAULT 0 COMMENT '是否该原信的首次回复:0否 1是',
+    owner_by         bigint       NULL COMMENT '数据归属人',
+    create_by        bigint       NULL COMMENT '创建人',
+    update_by        bigint       NULL COMMENT '最后更新人',
+    create_time      datetime     NULL COMMENT '创建时间',
+    update_time      datetime     NULL COMMENT '更新时间',
+    is_delete        tinyint      NOT NULL DEFAULT 0 COMMENT '逻辑删除:0未删除;1已删除',
+    enabled          tinyint      NOT NULL DEFAULT 1 COMMENT '是否启用:1启用;0禁用',
+    sort             int          NULL COMMENT '排序号',
+    PRIMARY KEY (id),
+    UNIQUE KEY uk_mkt_reply_mail_pair (mail_id, reply_mail_id),
+    KEY idx_mkt_reply_mail_id (mail_id, reply_time),
+    KEY idx_mkt_reply_biz_ref (biz_ref),
+    KEY idx_mkt_reply_from (reply_from),
+    KEY idx_mkt_reply_account (account_id, reply_time)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='营销邮件回复沟通记录';

+ 84 - 0
storlead-mail/storlead-mail-mkt/src/main/java/com/storlead/sales/mail/mkt/entity/MktMailReplyRecordEntity.java

@@ -0,0 +1,84 @@
+package com.storlead.sales.mail.mkt.entity;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.storlead.framework.mybatis.entity.SysBaseField;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.experimental.Accessors;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.util.Date;
+
+/**
+ * 营销邮件回复沟通记录:每次客户回复原信落一行。
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+@Accessors(chain = true)
+@TableName("mkt_mail_reply_record")
+@ApiModel(value = "MktMailReplyRecordEntity", description = "营销邮件回复沟通记录")
+public class MktMailReplyRecordEntity extends SysBaseField {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(value = "id", type = IdType.AUTO)
+    private Long id;
+
+    @ApiModelProperty("原信(发出信)ID")
+    @TableField("mail_id")
+    private Long mailId;
+
+    @ApiModelProperty("回复邮件ID")
+    @TableField("reply_mail_id")
+    private Long replyMailId;
+
+    @ApiModelProperty("发信账户ID")
+    @TableField("account_id")
+    private Long accountId;
+
+    @ApiModelProperty("组织ID")
+    @TableField("org_id")
+    private Long orgId;
+
+    @ApiModelProperty("业务单号")
+    @TableField("biz_ref")
+    private String bizRef;
+
+    @ApiModelProperty("批次号")
+    @TableField("biz_batch_no")
+    private String bizBatchNo;
+
+    @ApiModelProperty("原信主题")
+    @TableField("original_subject")
+    private String originalSubject;
+
+    @ApiModelProperty("回复邮件主题")
+    @TableField("reply_subject")
+    private String replySubject;
+
+    @ApiModelProperty("回复人邮箱")
+    @TableField("reply_from")
+    private String replyFrom;
+
+    @ApiModelProperty("回复人显示名")
+    @TableField("reply_from_name")
+    private String replyFromName;
+
+    @ApiModelProperty("回复时间")
+    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @JSONField(format = "yyyy-MM-dd HH:mm:ss")
+    @TableField("reply_time")
+    private Date replyTime;
+
+    @ApiModelProperty("是否该原信的首次回复:0否 1是")
+    @TableField("is_first_reply")
+    private Integer isFirstReply;
+}

+ 10 - 0
storlead-mail/storlead-mail-mkt/src/main/java/com/storlead/sales/mail/mkt/mapper/MktMailReplyRecordMapper.java

@@ -0,0 +1,10 @@
+package com.storlead.sales.mail.mkt.mapper;
+
+import com.storlead.framework.mybatis.mapper.MyBaseMapper;
+import com.storlead.sales.mail.mkt.entity.MktMailReplyRecordEntity;
+
+/**
+ * 营销邮件回复沟通记录 Mapper。
+ */
+public interface MktMailReplyRecordMapper extends MyBaseMapper<MktMailReplyRecordEntity> {
+}

+ 28 - 0
storlead-mail/storlead-mail-mkt/src/main/java/com/storlead/sales/mail/mkt/service/MktMailReplyRecordService.java

@@ -0,0 +1,28 @@
+package com.storlead.sales.mail.mkt.service;
+
+import com.storlead.framework.mybatis.service.MyBaseService;
+import com.storlead.sales.mail.mkt.entity.MktMailMessageEntity;
+import com.storlead.sales.mail.mkt.entity.MktMailReplyRecordEntity;
+
+import java.util.List;
+
+/**
+ * 营销邮件回复沟通记录。
+ */
+public interface MktMailReplyRecordService extends MyBaseService<MktMailReplyRecordEntity> {
+
+    /**
+     * 记录一次回复沟通(同一原信+回复邮件幂等,不重复插入)。
+     *
+     * @param original     原信(发出信)
+     * @param replyMailId  回复邮件 ID
+     * @param replyTheme   回复主题(可空,优先用回复邮件实体主题)
+     * @param firstReply   是否该原信首次回复
+     */
+    void recordReply(MktMailMessageEntity original, Long replyMailId, String replyTheme, boolean firstReply);
+
+    /**
+     * 按原信查询回复沟通记录(时间正序)。
+     */
+    List<MktMailReplyRecordEntity> listByMailId(Long mailId);
+}

+ 14 - 2
storlead-mail/storlead-mail-mkt/src/main/java/com/storlead/sales/mail/mkt/service/impl/MktMailMessageServiceImpl.java

@@ -31,6 +31,7 @@ import com.storlead.sales.mail.mkt.pojo.MktSendMailResult;
 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.MktMailReplyRecordService;
 import com.storlead.sales.mail.mkt.service.MktMailStatusService;
 import com.storlead.sales.mail.mkt.support.MktMailHeaders;
 import com.storlead.sales.mail.mkt.util.MktMailSender;
@@ -97,6 +98,9 @@ public class MktMailMessageServiceImpl extends MyBaseServiceImpl<MktMailMessageM
     @Resource
     private MktMailStatusService mktMailStatusService;
 
+    @Resource
+    private MktMailReplyRecordService mktMailReplyRecordService;
+
     @Resource
     private MktMailBlacklistService mktMailBlacklistService;
 
@@ -424,8 +428,10 @@ public class MktMailMessageServiceImpl extends MyBaseServiceImpl<MktMailMessageM
         }
         MktMailMessageEntity before = this.getOne(new LambdaQueryWrapper<MktMailMessageEntity>()
                 .select(MktMailMessageEntity::getId, MktMailMessageEntity::getReplied,
-                        MktMailMessageEntity::getBizRef, MktMailMessageEntity::getRecipient,
-                        MktMailMessageEntity::getSubject)
+                        MktMailMessageEntity::getBizRef, MktMailMessageEntity::getBizBatchNo,
+                        MktMailMessageEntity::getRecipient, MktMailMessageEntity::getSubject,
+                        MktMailMessageEntity::getAccountId, MktMailMessageEntity::getOrgId,
+                        MktMailMessageEntity::getOwnerBy)
                 .eq(MktMailMessageEntity::getId, mailId)
                 .eq(MktMailMessageEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
                 .last("limit 1"));
@@ -448,6 +454,12 @@ public class MktMailMessageServiceImpl extends MyBaseServiceImpl<MktMailMessageM
         } catch (Exception e) {
             log.error("mkt_mail_status markReplied error, mailId={}", before.getId(), e);
         }
+        try {
+            mktMailReplyRecordService.recordReply(before, replyEmailId, replyTheme, firstReply);
+        } catch (Exception e) {
+            log.error("mkt_mail_reply_record save error, mailId={}, replyEmailId={}",
+                    before.getId(), replyEmailId, e);
+        }
         return true;
     }
 

+ 125 - 0
storlead-mail/storlead-mail-mkt/src/main/java/com/storlead/sales/mail/mkt/service/impl/MktMailReplyRecordServiceImpl.java

@@ -0,0 +1,125 @@
+package com.storlead.sales.mail.mkt.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.storlead.framework.common.constant.CommonConstant;
+import com.storlead.framework.mybatis.service.impl.MyBaseServiceImpl;
+import com.storlead.sales.mail.mkt.entity.MktMailMessageEntity;
+import com.storlead.sales.mail.mkt.entity.MktMailReplyRecordEntity;
+import com.storlead.sales.mail.mkt.mapper.MktMailMessageMapper;
+import com.storlead.sales.mail.mkt.mapper.MktMailReplyRecordMapper;
+import com.storlead.sales.mail.mkt.service.MktMailReplyRecordService;
+import lombok.extern.log4j.Log4j2;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.stereotype.Service;
+import org.springframework.util.StringUtils;
+
+import javax.annotation.Resource;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+
+@Log4j2
+@Service
+public class MktMailReplyRecordServiceImpl
+        extends MyBaseServiceImpl<MktMailReplyRecordMapper, MktMailReplyRecordEntity>
+        implements MktMailReplyRecordService {
+
+    @Resource
+    private MktMailMessageMapper mktMailMessageMapper;
+
+    @Override
+    public void recordReply(MktMailMessageEntity original, Long replyMailId, String replyTheme, boolean firstReply) {
+        if (original == null || original.getId() == null || replyMailId == null) {
+            return;
+        }
+        // 已存在则跳过(幂等)
+        Integer exist = this.count(new LambdaQueryWrapper<MktMailReplyRecordEntity>()
+                .eq(MktMailReplyRecordEntity::getMailId, original.getId())
+                .eq(MktMailReplyRecordEntity::getReplyMailId, replyMailId)
+                .eq(MktMailReplyRecordEntity::getIsDelete, CommonConstant.DEL_FLAG_0));
+        if (exist != null && exist > 0) {
+            return;
+        }
+
+        MktMailMessageEntity reply = mktMailMessageMapper.selectOne(new LambdaQueryWrapper<MktMailMessageEntity>()
+                .select(MktMailMessageEntity::getId,
+                        MktMailMessageEntity::getSubject,
+                        MktMailMessageEntity::getFromAddr,
+                        MktMailMessageEntity::getFromName,
+                        MktMailMessageEntity::getRecipientDate,
+                        MktMailMessageEntity::getSentDate,
+                        MktMailMessageEntity::getCreateTime)
+                .eq(MktMailMessageEntity::getId, replyMailId)
+                .eq(MktMailMessageEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
+                .last("limit 1"));
+
+        Date now = new Date();
+        MktMailReplyRecordEntity row = new MktMailReplyRecordEntity();
+        row.setMailId(original.getId());
+        row.setReplyMailId(replyMailId);
+        row.setAccountId(original.getAccountId());
+        row.setOrgId(original.getOrgId());
+        row.setBizRef(original.getBizRef());
+        row.setBizBatchNo(original.getBizBatchNo());
+        row.setOriginalSubject(original.getSubject());
+        if (reply != null && StringUtils.hasText(reply.getSubject())) {
+            row.setReplySubject(truncate(reply.getSubject(), 512));
+        } else if (StringUtils.hasText(replyTheme)) {
+            row.setReplySubject(truncate(replyTheme, 512));
+        }
+        if (reply != null) {
+            row.setReplyFrom(reply.getFromAddr());
+            row.setReplyFromName(reply.getFromName());
+            if (reply.getRecipientDate() != null) {
+                row.setReplyTime(reply.getRecipientDate());
+            } else if (reply.getSentDate() != null) {
+                row.setReplyTime(reply.getSentDate());
+            } else if (reply.getCreateTime() != null) {
+                row.setReplyTime(reply.getCreateTime());
+            } else {
+                row.setReplyTime(now);
+            }
+        } else {
+            row.setReplySubject(truncate(replyTheme, 512));
+            row.setReplyTime(now);
+        }
+        row.setIsFirstReply(firstReply ? 1 : 0);
+        row.setOwnerBy(original.getOwnerBy());
+        row.setCreateBy(original.getOwnerBy());
+        row.setCreateTime(now);
+        row.setUpdateTime(now);
+        row.setIsDelete(CommonConstant.DEL_FLAG_0);
+        row.setEnabled(Boolean.TRUE);
+        try {
+            this.save(row);
+            log.info("mkt reply record saved, mailId={}, replyMailId={}, firstReply={}",
+                    original.getId(), replyMailId, firstReply);
+        } catch (DuplicateKeyException e) {
+            log.debug("mkt reply record already exists, mailId={}, replyMailId={}",
+                    original.getId(), replyMailId);
+        } catch (Exception e) {
+            log.error("mkt reply record save error, mailId={}, replyMailId={}",
+                    original.getId(), replyMailId, e);
+        }
+    }
+
+    @Override
+    public List<MktMailReplyRecordEntity> listByMailId(Long mailId) {
+        if (mailId == null) {
+            return Collections.emptyList();
+        }
+        return this.list(new LambdaQueryWrapper<MktMailReplyRecordEntity>()
+                .eq(MktMailReplyRecordEntity::getMailId, mailId)
+                .eq(MktMailReplyRecordEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
+                .orderByAsc(MktMailReplyRecordEntity::getReplyTime)
+                .orderByAsc(MktMailReplyRecordEntity::getId));
+    }
+
+    private static String truncate(String text, int max) {
+        if (!StringUtils.hasText(text)) {
+            return text;
+        }
+        String s = text.trim();
+        return s.length() <= max ? s : s.substring(0, max);
+    }
+}