|
@@ -17,17 +17,20 @@ import com.storlead.sales.mail.common.service.MailAccountBizService;
|
|
|
import com.storlead.sales.mail.common.service.MailTempAttachmentService;
|
|
import com.storlead.sales.mail.common.service.MailTempAttachmentService;
|
|
|
import com.storlead.sales.mail.common.transport.MailTransportSupport;
|
|
import com.storlead.sales.mail.common.transport.MailTransportSupport;
|
|
|
import com.storlead.sales.mail.common.util.EmailHelper;
|
|
import com.storlead.sales.mail.common.util.EmailHelper;
|
|
|
|
|
+import com.storlead.sales.mail.mkt.callback.MktMailOpenCallback;
|
|
|
import com.storlead.sales.mail.mkt.entity.MktMailAttachmentEntity;
|
|
import com.storlead.sales.mail.mkt.entity.MktMailAttachmentEntity;
|
|
|
import com.storlead.sales.mail.mkt.entity.MktMailMessageEntity;
|
|
import com.storlead.sales.mail.mkt.entity.MktMailMessageEntity;
|
|
|
|
|
+import com.storlead.sales.mail.mkt.enums.MktMailFolderEnum;
|
|
|
import com.storlead.sales.mail.mkt.mapper.MktMailMessageMapper;
|
|
import com.storlead.sales.mail.mkt.mapper.MktMailMessageMapper;
|
|
|
import com.storlead.sales.mail.mkt.pojo.MktMailQueryDTO;
|
|
import com.storlead.sales.mail.mkt.pojo.MktMailQueryDTO;
|
|
|
|
|
+import com.storlead.sales.mail.mkt.pojo.MktOpenStatsVO;
|
|
|
import com.storlead.sales.mail.mkt.pojo.MktSendMailDTO;
|
|
import com.storlead.sales.mail.mkt.pojo.MktSendMailDTO;
|
|
|
import com.storlead.sales.mail.mkt.service.MktMailAttachmentService;
|
|
import com.storlead.sales.mail.mkt.service.MktMailAttachmentService;
|
|
|
import com.storlead.sales.mail.mkt.service.MktMailMessageService;
|
|
import com.storlead.sales.mail.mkt.service.MktMailMessageService;
|
|
|
-import com.storlead.sales.mail.mkt.enums.MktMailFolderEnum;
|
|
|
|
|
import com.storlead.sales.mail.mkt.support.MktMailHeaders;
|
|
import com.storlead.sales.mail.mkt.support.MktMailHeaders;
|
|
|
import com.storlead.sales.mail.mkt.util.MktMailSender;
|
|
import com.storlead.sales.mail.mkt.util.MktMailSender;
|
|
|
import lombok.extern.log4j.Log4j2;
|
|
import lombok.extern.log4j.Log4j2;
|
|
|
|
|
+import org.springframework.beans.factory.ObjectProvider;
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
@@ -91,12 +94,19 @@ public class MktMailMessageServiceImpl extends MyBaseServiceImpl<MktMailMessageM
|
|
|
@Resource
|
|
@Resource
|
|
|
private MktMailSender mktMailSender;
|
|
private MktMailSender mktMailSender;
|
|
|
|
|
|
|
|
|
|
+ private final List<MktMailOpenCallback> openCallbacks;
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
- * 打开追踪像素根地址,需可被收件方公网访问,例如 https://host/router/mail
|
|
|
|
|
|
|
+ * 打开追踪像素根地址,需可被收件方公网访问,且含 context-path。
|
|
|
|
|
+ * 例:https://host/router/mail (像素最终为 {base}/mail/mkt/track/{token})
|
|
|
*/
|
|
*/
|
|
|
@Value("${storlead.mail.track-base-url:${domainname:}}")
|
|
@Value("${storlead.mail.track-base-url:${domainname:}}")
|
|
|
private String trackBaseUrl;
|
|
private String trackBaseUrl;
|
|
|
|
|
|
|
|
|
|
+ public MktMailMessageServiceImpl(ObjectProvider<MktMailOpenCallback> openCallbackProvider) {
|
|
|
|
|
+ this.openCallbacks = openCallbackProvider.orderedStream().collect(Collectors.toList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@Override
|
|
@Override
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
public MktMailMessageEntity saveDraft(MktSendMailDTO dto, Long operatorId) {
|
|
public MktMailMessageEntity saveDraft(MktSendMailDTO dto, Long operatorId) {
|
|
@@ -134,6 +144,7 @@ public class MktMailMessageServiceImpl extends MyBaseServiceImpl<MktMailMessageM
|
|
|
MktMailMessageEntity entity = resolveWritableEntity(dto.getId(), operatorId);
|
|
MktMailMessageEntity entity = resolveWritableEntity(dto.getId(), operatorId);
|
|
|
fillMessageFields(entity, dto, account, operatorId);
|
|
fillMessageFields(entity, dto, account, operatorId);
|
|
|
ensureTrackToken(entity, dto.getIsTrack());
|
|
ensureTrackToken(entity, dto.getIsTrack());
|
|
|
|
|
+ assertTrackReady(entity);
|
|
|
|
|
|
|
|
boolean delayed = dto.getDelaySendTime() != null && dto.getDelaySendTime().after(new Date());
|
|
boolean delayed = dto.getDelaySendTime() != null && dto.getDelaySendTime().after(new Date());
|
|
|
entity.setFolder(MktMailFolderEnum.SENTING.code);
|
|
entity.setFolder(MktMailFolderEnum.SENTING.code);
|
|
@@ -236,6 +247,15 @@ public class MktMailMessageServiceImpl extends MyBaseServiceImpl<MktMailMessageM
|
|
|
if (StringUtils.hasText(query.getFolder())) {
|
|
if (StringUtils.hasText(query.getFolder())) {
|
|
|
wrapper.eq(MktMailMessageEntity::getFolder, query.getFolder());
|
|
wrapper.eq(MktMailMessageEntity::getFolder, query.getFolder());
|
|
|
}
|
|
}
|
|
|
|
|
+ if (query.getOpened() != null) {
|
|
|
|
|
+ wrapper.eq(MktMailMessageEntity::getOpened, query.getOpened());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (query.getIsTrack() != null) {
|
|
|
|
|
+ wrapper.eq(MktMailMessageEntity::getIsTrack, query.getIsTrack());
|
|
|
|
|
+ }
|
|
|
|
|
+ if (StringUtils.hasText(query.getBizBatchNo())) {
|
|
|
|
|
+ wrapper.eq(MktMailMessageEntity::getBizBatchNo, query.getBizBatchNo());
|
|
|
|
|
+ }
|
|
|
wrapper.orderByDesc(MktMailMessageEntity::getId);
|
|
wrapper.orderByDesc(MktMailMessageEntity::getId);
|
|
|
return this.page(page, wrapper);
|
|
return this.page(page, wrapper);
|
|
|
}
|
|
}
|
|
@@ -271,20 +291,75 @@ public class MktMailMessageServiceImpl extends MyBaseServiceImpl<MktMailMessageM
|
|
|
if (!StringUtils.hasText(trackToken)) {
|
|
if (!StringUtils.hasText(trackToken)) {
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
- MktMailMessageEntity entity = this.getOne(new LambdaQueryWrapper<MktMailMessageEntity>()
|
|
|
|
|
|
|
+ MktMailMessageEntity before = this.getOne(new LambdaQueryWrapper<MktMailMessageEntity>()
|
|
|
|
|
+ .select(MktMailMessageEntity::getId, MktMailMessageEntity::getOpened,
|
|
|
|
|
+ MktMailMessageEntity::getTrackToken, MktMailMessageEntity::getBizRef,
|
|
|
|
|
+ MktMailMessageEntity::getRecipient, MktMailMessageEntity::getIsTrack)
|
|
|
.eq(MktMailMessageEntity::getTrackToken, trackToken)
|
|
.eq(MktMailMessageEntity::getTrackToken, trackToken)
|
|
|
.eq(MktMailMessageEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
|
|
.eq(MktMailMessageEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
|
|
|
|
|
+ .eq(MktMailMessageEntity::getIsTrack, 1)
|
|
|
.last("limit 1"));
|
|
.last("limit 1"));
|
|
|
- if (entity == null) {
|
|
|
|
|
|
|
+ if (before == null) {
|
|
|
return false;
|
|
return false;
|
|
|
}
|
|
}
|
|
|
- Date now = new Date();
|
|
|
|
|
- if (!Integer.valueOf(1).equals(entity.getOpened())) {
|
|
|
|
|
- entity.setOpened(1);
|
|
|
|
|
- entity.setFirstOpenTime(now);
|
|
|
|
|
|
|
+ boolean firstOpen = !Integer.valueOf(1).equals(before.getOpened());
|
|
|
|
|
+ LambdaUpdateWrapper<MktMailMessageEntity> update = new LambdaUpdateWrapper<>();
|
|
|
|
|
+ update.eq(MktMailMessageEntity::getId, before.getId());
|
|
|
|
|
+ update.set(MktMailMessageEntity::getOpened, 1);
|
|
|
|
|
+ update.setSql("open_count = IFNULL(open_count, 0) + 1");
|
|
|
|
|
+ update.setSql("first_open_time = IFNULL(first_open_time, NOW())");
|
|
|
|
|
+ boolean ok = this.update(update);
|
|
|
|
|
+ if (!ok) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ MktMailMessageEntity latest = this.getById(before.getId());
|
|
|
|
|
+ fireOpenCallbacks(latest == null ? before : latest, firstOpen);
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public MktOpenStatsVO openStatsByBizRef(String bizRef) {
|
|
|
|
|
+ if (!StringUtils.hasText(bizRef)) {
|
|
|
|
|
+ throw new WebException("业务单号不能为空");
|
|
|
|
|
+ }
|
|
|
|
|
+ List<MktMailMessageEntity> list = this.list(new LambdaQueryWrapper<MktMailMessageEntity>()
|
|
|
|
|
+ .select(MktMailMessageEntity::getId, MktMailMessageEntity::getIsTrack,
|
|
|
|
|
+ MktMailMessageEntity::getOpened, MktMailMessageEntity::getOpenCount)
|
|
|
|
|
+ .eq(MktMailMessageEntity::getBizRef, bizRef)
|
|
|
|
|
+ .eq(MktMailMessageEntity::getIsDelete, CommonConstant.DEL_FLAG_0));
|
|
|
|
|
+ MktOpenStatsVO vo = new MktOpenStatsVO();
|
|
|
|
|
+ vo.setBizRef(bizRef);
|
|
|
|
|
+ vo.setTotal(list.size());
|
|
|
|
|
+ int trackEnabled = 0;
|
|
|
|
|
+ int opened = 0;
|
|
|
|
|
+ int openCountSum = 0;
|
|
|
|
|
+ for (MktMailMessageEntity item : list) {
|
|
|
|
|
+ if (Integer.valueOf(1).equals(item.getIsTrack())) {
|
|
|
|
|
+ trackEnabled++;
|
|
|
|
|
+ if (Integer.valueOf(1).equals(item.getOpened())) {
|
|
|
|
|
+ opened++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ openCountSum += item.getOpenCount() == null ? 0 : item.getOpenCount();
|
|
|
|
|
+ }
|
|
|
|
|
+ vo.setTrackEnabled(trackEnabled);
|
|
|
|
|
+ vo.setOpened(opened);
|
|
|
|
|
+ vo.setUnopened(Math.max(trackEnabled - opened, 0));
|
|
|
|
|
+ vo.setOpenCountSum(openCountSum);
|
|
|
|
|
+ return vo;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void fireOpenCallbacks(MktMailMessageEntity entity, boolean firstOpen) {
|
|
|
|
|
+ if (entity == null || CollectionUtils.isEmpty(openCallbacks)) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (MktMailOpenCallback callback : openCallbacks) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ callback.onOpened(entity, firstOpen);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ log.error("mkt open callback error, mailId={}", entity.getId(), e);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
- entity.setOpenCount((entity.getOpenCount() == null ? 0 : entity.getOpenCount()) + 1);
|
|
|
|
|
- return this.updateById(entity);
|
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
@@ -421,8 +496,25 @@ public class MktMailMessageServiceImpl extends MyBaseServiceImpl<MktMailMessageM
|
|
|
}
|
|
}
|
|
|
String base = trackBaseUrl.endsWith("/") ? trackBaseUrl.substring(0, trackBaseUrl.length() - 1) : trackBaseUrl;
|
|
String base = trackBaseUrl.endsWith("/") ? trackBaseUrl.substring(0, trackBaseUrl.length() - 1) : trackBaseUrl;
|
|
|
String pixel = "<img src=\"" + base + "/mail/mkt/track/" + entity.getTrackToken()
|
|
String pixel = "<img src=\"" + base + "/mail/mkt/track/" + entity.getTrackToken()
|
|
|
- + "\" width=\"1\" height=\"1\" style=\"display:none\" alt=\"\"/>";
|
|
|
|
|
- return html + pixel;
|
|
|
|
|
|
|
+ + "\" width=\"1\" height=\"1\" style=\"display:none!important;width:1px;height:1px;border:0;\" alt=\"\"/>";
|
|
|
|
|
+ String body = html == null ? "" : html;
|
|
|
|
|
+ int idx = body.toLowerCase().lastIndexOf("</body>");
|
|
|
|
|
+ if (idx >= 0) {
|
|
|
|
|
+ return body.substring(0, idx) + pixel + body.substring(idx);
|
|
|
|
|
+ }
|
|
|
|
|
+ return body + pixel;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private void assertTrackReady(MktMailMessageEntity entity) {
|
|
|
|
|
+ if (!Integer.valueOf(1).equals(entity.getIsTrack())) {
|
|
|
|
|
+ return;
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!StringUtils.hasText(trackBaseUrl)) {
|
|
|
|
|
+ throw new WebException("已开启打开追踪,请配置 storlead.mail.track-base-url(公网可访问,含 context-path)");
|
|
|
|
|
+ }
|
|
|
|
|
+ if (!StringUtils.hasText(entity.getTrackToken())) {
|
|
|
|
|
+ throw new WebException("打开追踪 token 生成失败");
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
private void bindTempAttachments(Long mailId, List<Long> fileIds, Long operatorId) {
|
|
private void bindTempAttachments(Long mailId, List<Long> fileIds, Long operatorId) {
|