Преглед изворни кода

Merge remote-tracking branch 'origin/master'

zhujing пре 4 дана
родитељ
комит
9b0ad5541c

+ 1 - 1
java/storlead-api/src/main/resources/application-dev.yml

@@ -201,7 +201,7 @@ dify:
     mail: app-4-qfhXMNcjsRSwN5CK6aoWx3hl #邮件
 
 # 站内消息、邮件模板中的前端站点根地址(覆盖 application.yml 默认值)
-domainname: https://test1.storlead.com
+domainname: https://trade.test.storlead.com
 
 storlead:
   # 独立邮件服务(B)远程调用:按环境配置 IP/域名 + context-path,请求自动透传 token

+ 1 - 1
java/storlead-api/src/main/resources/application-test.yml

@@ -184,7 +184,7 @@ dify:
     mail-temp: app-3-qfhXMNcjsRSwN5CK6aoWx3hl #邮件模板
     mail: app-4-qfhXMNcjsRSwN5CK6aoWx3hl #邮件
 
-domainname: https://test1.storlead.com
+domainname: https://trade.test.storlead.com
 
 
 storlead:

+ 2 - 1
java/storlead-mail/storlead-mail-biz/src/main/java/com/storlead/mail/service/impl/MktMailMessageServiceImpl.java

@@ -29,11 +29,12 @@ public class MktMailMessageServiceImpl extends MailDataSourceServiceImpl<MktMail
         wrapper.select(
                         MktMailMessageEntity::getId,
                         MktMailMessageEntity::getFromAddr,
+                        MktMailMessageEntity::getSubject,
+                        MktMailMessageEntity::getReplyMsgId,
                         MktMailMessageEntity::getOwnerBy,
                         MktMailMessageEntity::getCreateBy)
                 .eq(MktMailMessageEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
                 .eq(MktMailMessageEntity::getFolder, EmailBoxEnum.INBOX.code)
-                .and(w -> w.eq(MktMailMessageEntity::getIsRead, 0).or().isNull(MktMailMessageEntity::getIsRead))
                 .gt(lastId != null, MktMailMessageEntity::getId, lastId)
                 .orderByAsc(MktMailMessageEntity::getId)
                 .last("limit " + limit);

+ 11 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/MarketEmailsService.java

@@ -123,4 +123,15 @@ public interface MarketEmailsService extends MyBaseService<MarketEmailsEntity> {
       * @Author: YPZ
       * @Date: 2026/9/17 15:38
       */
+
+    /**
+     * 兜底清理:预计发送时间已超过 30 分钟、状态仍为 -1(待发送)的邮件,统一置为 -3(已取消)。
+     * <p>
+     * send() 只在 expect_send_time 落进「当前这一分钟」时投递,一旦漏过窗口(服务停机、
+     * 该分钟任务未抢到锁、客户当时处于保护/暂停等),邮件就会长期卡在待发送。
+     * 这里给 30 分钟宽限期,超时即视为作废。
+     *
+     * @return 被置为已取消的邮件条数
+     */
+    int cancelOverduePending();
 }

+ 53 - 1
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MarketEmailsServiceImpl.java

@@ -60,6 +60,9 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
     /** 重新发送邮件时,若预计发送时间已过期,顺延的分钟数 */
     private static final int RESEND_DELAY_MINUTES = 5;
 
+    /** 待发送邮件超时未发出的清理阈值(分钟):超过该时长仍为 -1 则置为 -3 */
+    private static final int PENDING_OVERDUE_MINUTES = 30;
+
     @Resource
     private CustomerBaseService customerBaseService;
 
@@ -504,6 +507,29 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
         return Result.ok();
     }
 
+    /**
+     * 兜底清理:预计发送时间已超过 {@link #PENDING_OVERDUE_MINUTES} 分钟、状态仍为 -1(待发送)的邮件,
+     * 统一置为 -3(已取消)。
+     * <p>
+     * send() 只在 expect_send_time 落进「当前这一分钟」时投递,一旦漏过窗口(服务停机、
+     * 该分钟任务没抢到锁、客户当时处于保护/暂停等),邮件就会长期卡在「待发送」。
+     * 这里给 30 分钟宽限期,超时即作废。一条 SQL 批量更新,不做逐条处理。
+     */
+    @Override
+    public int cancelOverduePending() {
+        Date deadline = DateUtils.addMinutes(new Date(), -PENDING_OVERDUE_MINUTES);
+        LambdaUpdateWrapper<MarketEmailsEntity> wrapper = new LambdaUpdateWrapper<>();
+        wrapper.eq(MarketEmailsEntity::getStatus, -1)
+                .eq(MarketEmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
+                .lt(MarketEmailsEntity::getExpectSendTime, deadline)
+                .set(MarketEmailsEntity::getStatus, -3);
+        int rows = this.baseMapper.update(null, wrapper);
+        if (rows > 0) {
+            log.info("[cancelOverduePending] 超时未发出的待发送邮件已置为已取消,共{}条,deadline={}", rows, deadline);
+        }
+        return rows;
+    }
+
     /**
      * 回复邮件的客户列表(按 marketingCampaignId 过滤 + hasReply=1 + recipient 聚合)
      * 不分页:全部/已保护/营销暂停 三个 Tab 由前端在结果中过滤计数。
@@ -1167,6 +1193,7 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
         queryStopWrapper.in(MarketEmailsEntity::getMarketingCampaignId, marketingCampaignIdListWithReplyStop);
         queryStopWrapper.eq(MarketEmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
         queryStopWrapper.eq(MarketEmailsEntity::getHasReply, 1);
+        queryStopWrapper.eq(MarketEmailsEntity::getHasReader, 1);
         List<MarketEmailsEntity> marketEmailsStopList = this.list(queryStopWrapper);
         List<Long> customerIdList = new ArrayList<>();
         if (!CollectionUtils.isEmpty(marketEmailsStopList)){
@@ -1261,7 +1288,32 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
         marketingCampaignQueryWrapper.eq(MarketingCampaignEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
         marketingCampaignQueryWrapper.eq(MarketingCampaignEntity::getStatus, 1);
         marketingCampaignQueryWrapper.eq(MarketingCampaignEntity::getFollowUpRulesNotOpened, 1);
-        List<MarketingCampaignEntity> marketingCampaignList = marketingCampaignService.list(marketingCampaignQueryWrapper);
+        List<MarketingCampaignEntity> marketingCampaignFirstList = marketingCampaignService.list(marketingCampaignQueryWrapper);
+        if (CollectionUtils.isEmpty(marketingCampaignFirstList)){
+            return;
+        }
+        List<MarketingCampaignEntity> marketingCampaignList = new ArrayList<>();
+        //提取未开启客户回复后停止自动化的营销活动
+        List<MarketingCampaignEntity> marketingCampaignListA = marketingCampaignFirstList.stream().filter(
+                campaign -> campaign.getFollowUpRulesReplied() == null || campaign.getFollowUpRulesReplied() == 0
+        ).collect(Collectors.toList());
+        if (!CollectionUtils.isEmpty(marketingCampaignListA)){
+            marketingCampaignList.addAll(marketingCampaignListA);
+        }
+        List<MarketingCampaignEntity> marketingCampaignListB = marketingCampaignFirstList.stream().filter(
+                campaign -> campaign.getFollowUpRulesReplied() != null && campaign.getFollowUpRulesReplied() == 1
+        ).collect(Collectors.toList());
+        //排除有客户回复的营销活动
+        marketingCampaignListB = marketingCampaignListB.stream().filter(campaign -> {
+            LambdaQueryWrapper<MarketEmailsEntity> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.eq(MarketEmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
+            queryWrapper.eq(MarketEmailsEntity::getMarketingCampaignId, campaign.getId());
+            queryWrapper.eq(MarketEmailsEntity::getHasReply, 1);
+            return this.count(queryWrapper) == 0;
+        }).collect(Collectors.toList());
+        if (!CollectionUtils.isEmpty(marketingCampaignListB)){
+            marketingCampaignList.addAll(marketingCampaignListB);
+        }
         if (CollectionUtils.isEmpty(marketingCampaignList)){
             return;
         }

+ 3 - 93
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MarketEmailsStatusSyncServiceImpl.java

@@ -8,25 +8,16 @@ import com.storlead.framework.common.constant.DSConstants;
 import com.storlead.mail.pojo.entity.MktMailStatusEntity;
 import com.storlead.mail.service.MktMailStatusService;
 import com.storlead.trade.entity.MarketEmailsEntity;
-import com.storlead.trade.entity.MarketingCampaignEntity;
-import com.storlead.trade.enums.TradeMessageTypeEnum;
 import com.storlead.trade.service.MarketEmailsService;
 import com.storlead.trade.service.MarketEmailsStatusSyncService;
-import com.storlead.trade.service.MarketingCampaignService;
-import com.storlead.trade.service.TradeMessageAssembleService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
-import org.springframework.util.StringUtils;
 
 import javax.annotation.Resource;
 import java.util.ArrayList;
 import java.util.Date;
-import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 /**
  * 跨库同步:邮件库 mkt_mail_status → 业务库 market_emails,再回写 synced_at。
@@ -51,12 +42,6 @@ public class MarketEmailsStatusSyncServiceImpl implements MarketEmailsStatusSync
     @Resource
     private MarketEmailsService marketEmailsService;
 
-    @Resource
-    private MarketingCampaignService marketingCampaignService;
-
-    @Resource
-    private TradeMessageAssembleService tradeMessageAssembleService;
-
     @Override
     public int syncOnce() {
         int totalUpdated = 0;
@@ -91,6 +76,8 @@ public class MarketEmailsStatusSyncServiceImpl implements MarketEmailsStatusSync
                     syncedMailIds.add(change.getMailId());
                 } else if (applyResult == ApplyResult.MATCHED_NO_CHANGE) {
                     syncedMailIds.add(change.getMailId());
+                } else {
+                    syncedMailIds.add(change.getMailId());
                 }
                 // UNMATCHED:尚无 email_id 关联,不 ack,下次继续等
             }
@@ -165,11 +152,9 @@ public class MarketEmailsStatusSyncServiceImpl implements MarketEmailsStatusSync
             changed = true;
         }
         //补充首次回复邮件id
-        boolean firstReplyMailFilled = false;
         if (existing.getFirstReplyEmailId() == null && change.getFirstReplyEmailId() != null) {
             update.set(MarketEmailsEntity::getFirstReplyEmailId, change.getFirstReplyEmailId());
             changed = true;
-            firstReplyMailFilled = true;
         }
         //补充回复邮件主题
         if (existing.getFirstReplyTheme() == null && change.getFirstReplyTheme() != null) {
@@ -179,82 +164,7 @@ public class MarketEmailsStatusSyncServiceImpl implements MarketEmailsStatusSync
         if (!changed) {
             return ApplyResult.MATCHED_NO_CHANGE;
         }
-        boolean updated = marketEmailsService.update(update);
-        if (updated && firstReplyMailFilled) {
-            notifyCampaignSupervisor(existing, change);
-        }
-        return updated ? ApplyResult.UPDATED : ApplyResult.MATCHED_NO_CHANGE;
-    }
-
-    /**
-     * 首次同步到回复邮件时,提醒营销活动负责人。
-     */
-    private void notifyCampaignSupervisor(MarketEmailsEntity email, MktMailStatusEntity change) {
-        if (email.getMarketingCampaignId() == null) {
-            log.warn("营销回复提醒跳过:无营销活动 id, marketEmailId={}", email.getId());
-            return;
-        }
-        MarketingCampaignEntity campaign = marketingCampaignService.getById(email.getMarketingCampaignId());
-        if (campaign == null) {
-            log.warn("营销回复提醒跳过:活动不存在 campaignId={}", email.getMarketingCampaignId());
-            return;
-        }
-        Set<Long> toUserIds = resolveCampaignSupervisors(campaign);
-        if (toUserIds.isEmpty()) {
-            log.warn("营销回复提醒无接收人 campaignId={}", campaign.getId());
-            return;
-        }
-        Map<String, Object> payload = new HashMap<>();
-        payload.put("messageContent", buildFirstReplyContent(email, change, campaign));
-        payload.put("id", email.getEmailId());
-        try {
-            tradeMessageAssembleService.sendEventMessage(
-                    TradeMessageTypeEnum.MKT_REPLY_NEW_MAIL.getCode(),
-                    "MKT_REPLY_NEW_MAIL",
-                    payload,
-                    toUserIds);
-        } catch (Exception e) {
-            log.error("发送营销回复提醒失败 marketEmailId={} campaignId={}", email.getId(), campaign.getId(), e);
-        }
-    }
-
-    private static Set<Long> resolveCampaignSupervisors(MarketingCampaignEntity campaign) {
-        Set<Long> toUserIds = new HashSet<>();
-        if (StringUtils.hasText(campaign.getSupervisor())) {
-            for (String part : campaign.getSupervisor().split(",")) {
-                if (!StringUtils.hasText(part)) {
-                    continue;
-                }
-                try {
-                    Long userId = Long.valueOf(part.trim());
-                    if (userId > 0) {
-                        toUserIds.add(userId);
-                    }
-                } catch (NumberFormatException ignored) {
-                    // skip
-                }
-            }
-        }
-        if (campaign.getOwnerBy() != null && campaign.getOwnerBy() > 0) {
-            toUserIds.add(campaign.getOwnerBy());
-        }
-        return toUserIds;
-    }
-
-    private static String buildFirstReplyContent(MarketEmailsEntity email, MktMailStatusEntity change,
-                                                 MarketingCampaignEntity campaign) {
-        String from = StringUtils.hasText(email.getRecipient()) ? email.getRecipient() : "";
-        String subject = StringUtils.hasText(change.getFirstReplyTheme())
-                ? change.getFirstReplyTheme()
-                : (StringUtils.hasText(email.getSubject()) ? email.getSubject() : "");
-        String campaignName = StringUtils.hasText(campaign.getName()) ? campaign.getName() : "";
-        if (StringUtils.hasText(from) && StringUtils.hasText(subject)) {
-            return "客户【" + from + "】回复了营销邮件《" + subject + "》,请前往查看!";
-        }
-        if (StringUtils.hasText(from)) {
-            return "客户【" + from + "】回复了营销活动【" + campaignName + "】的邮件,请前往查看!";
-        }
-        return "营销活动【" + campaignName + "】有新的客户回复邮件,请前往查看!";
+        return marketEmailsService.update(update) ? ApplyResult.UPDATED : ApplyResult.MATCHED_NO_CHANGE;
     }
 
     private static Integer mapSendStatus(Integer sendStatus) {

+ 1 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MarketingCampaignServiceImpl.java

@@ -263,6 +263,7 @@ public class MarketingCampaignServiceImpl
         Map<Long, List<MarketEmailsEntity>> emailsBySopDetail = new HashMap<>();
         List<MarketEmailsEntity> campaignEmails = marketEmailsService.lambdaQuery()
                 .eq(MarketEmailsEntity::getMarketingCampaignId, id)
+                .eq(MarketEmailsEntity::getHasFirst, 1)
                 .eq(MarketEmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
                 .list();
         for (MarketEmailsEntity email : campaignEmails) {

+ 126 - 39
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MktMailNewNotifyServiceImpl.java

@@ -7,20 +7,26 @@ import com.storlead.framework.common.constant.CommonConstant;
 import com.storlead.framework.common.constant.DSConstants;
 import com.storlead.mail.pojo.entity.MktMailMessageEntity;
 import com.storlead.mail.service.MktMailMessageService;
+import com.storlead.trade.entity.MarketEmailsEntity;
+import com.storlead.trade.entity.MarketingCampaignEntity;
 import com.storlead.trade.entity.MktMailInboxCursorEntity;
 import com.storlead.trade.enums.TradeMessageTypeEnum;
+import com.storlead.trade.service.MarketEmailsService;
+import com.storlead.trade.service.MarketingCampaignService;
 import com.storlead.trade.service.MktMailInboxCursorService;
 import com.storlead.trade.service.MktMailNewNotifyService;
 import com.storlead.trade.service.TradeMessageAssembleService;
 import lombok.extern.slf4j.Slf4j;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
 
 import javax.annotation.Resource;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.Set;
 import java.util.stream.Collectors;
 
@@ -44,6 +50,12 @@ public class MktMailNewNotifyServiceImpl implements MktMailNewNotifyService {
     @Resource
     private TradeMessageAssembleService tradeMessageAssembleService;
 
+    @Resource
+    private MarketEmailsService marketEmailsService;
+
+    @Resource
+    private MarketingCampaignService marketingCampaignService;
+
     @Override
     public synchronized int syncOnce() {
         MktMailInboxCursorEntity cursor = currentCursor();
@@ -66,7 +78,7 @@ public class MktMailNewNotifyServiceImpl implements MktMailNewNotifyService {
                 break;
             }
             Long batchLastId = newMails.get(newMails.size() - 1).getId();
-            notified += notifyByOwner(newMails);
+            notified += notifyByCampaignSupervisor(newMails);
             if (batchLastId != null) {
                 lastId = batchLastId;
                 maxId = batchLastId;
@@ -110,22 +122,36 @@ public class MktMailNewNotifyServiceImpl implements MktMailNewNotifyService {
         log.info("营销收件箱新邮件游标首次初始化 lastMailId={},本次不提醒", maxId);
     }
 
-    private int notifyByOwner(List<MktMailMessageEntity> newMails) {
-        Map<Long, List<MktMailMessageEntity>> byOwner = newMails.stream()
-                .filter(mail -> resolveOwner(mail) != null)
-                .collect(Collectors.groupingBy(MktMailNewNotifyServiceImpl::resolveOwner));
-        if (byOwner.isEmpty()) {
+    /**
+     * 每一封回复单独发一条消息,接收人为对应营销活动负责人。
+     */
+    private int notifyByCampaignSupervisor(List<MktMailMessageEntity> newMails) {
+        Map<Long, Long> mailToCampaign = resolveMailCampaignIds(newMails);
+        if (mailToCampaign.isEmpty()) {
+            log.warn("营销新邮件未匹配到活动,跳过提醒 count={}", newMails.size());
             return 0;
         }
+        Map<Long, MarketingCampaignEntity> campaignMap = loadCampaigns(new HashSet<>(mailToCampaign.values()));
         int sent = 0;
-        for (Map.Entry<Long, List<MktMailMessageEntity>> entry : byOwner.entrySet()) {
-            List<MktMailMessageEntity> ownerMails = entry.getValue();
+        for (MktMailMessageEntity mail : newMails) {
+            Long campaignId = mailToCampaign.get(mail.getId());
+            if (campaignId == null) {
+                log.warn("营销回复提醒跳过:未匹配活动 mailId={}", mail.getId());
+                continue;
+            }
+            MarketingCampaignEntity campaign = campaignMap.get(campaignId);
+            if (campaign == null) {
+                log.warn("营销回复提醒跳过:活动不存在 campaignId={} mailId={}", campaignId, mail.getId());
+                continue;
+            }
+            Set<Long> toUserIds = resolveCampaignSupervisors(campaign);
+            if (toUserIds.isEmpty()) {
+                log.warn("营销回复提醒无接收人 campaignId={} mailId={}", campaign.getId(), mail.getId());
+                continue;
+            }
             Map<String, Object> payload = new HashMap<>();
-            payload.put("messageContent", buildContent(ownerMails));
-            payload.put("callbackUrl", "/marketing");
-            payload.put("id", "/marketing");
-            Set<Long> toUserIds = new HashSet<>();
-            toUserIds.add(entry.getKey());
+            payload.put("messageContent", buildContent(mail, campaign));
+            payload.put("id",mail.getId());
             try {
                 tradeMessageAssembleService.sendEventMessage(
                         TradeMessageTypeEnum.MKT_REPLY_NEW_MAIL.getCode(),
@@ -134,44 +160,105 @@ public class MktMailNewNotifyServiceImpl implements MktMailNewNotifyService {
                         toUserIds);
                 sent++;
             } catch (Exception e) {
-                log.error("发送营销新邮件提醒失败 ownerBy={} count={}", entry.getKey(), ownerMails.size(), e);
+                log.error("发送营销回复提醒失败 campaignId={} mailId={}", campaign.getId(), mail.getId(), e);
             }
         }
         return sent;
     }
 
-    private static String buildContent(List<MktMailMessageEntity> ownerMails) {
-        Set<String> froms = ownerMails.stream()
-                .map(MktMailMessageEntity::getFromAddr)
-                .filter(StrUtil::isNotBlank)
-                .collect(Collectors.toCollection(HashSet::new));
-        int count = ownerMails.size();
-        if (froms.isEmpty()) {
-            return "您有" + count + "封新邮件,请前往查看!";
+    /**
+     * 优先用回复原邮件 id(replyMsgId = market_emails.email_id),否则用 first_reply_email_id。
+     */
+    private Map<Long, Long> resolveMailCampaignIds(List<MktMailMessageEntity> newMails) {
+        Map<Long, Long> mailToCampaign = new HashMap<>();
+        Set<Long> replyMsgIds = newMails.stream()
+                .map(MktMailMessageEntity::getReplyMsgId)
+                .filter(id -> id != null && id > 0)
+                .collect(Collectors.toSet());
+        Set<Long> inboxIds = newMails.stream()
+                .map(MktMailMessageEntity::getId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toSet());
+        if (!replyMsgIds.isEmpty()) {
+            List<MarketEmailsEntity> bySent = marketEmailsService.list(new LambdaQueryWrapper<MarketEmailsEntity>()
+                    .select(MarketEmailsEntity::getEmailId, MarketEmailsEntity::getMarketingCampaignId)
+                    .in(MarketEmailsEntity::getEmailId, replyMsgIds)
+                    .eq(MarketEmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
+                    .isNotNull(MarketEmailsEntity::getMarketingCampaignId));
+            Map<Long, Long> sentToCampaign = bySent.stream()
+                    .filter(e -> e.getEmailId() != null && e.getMarketingCampaignId() != null)
+                    .collect(Collectors.toMap(MarketEmailsEntity::getEmailId,
+                            MarketEmailsEntity::getMarketingCampaignId, (a, b) -> a));
+            for (MktMailMessageEntity mail : newMails) {
+                Long campaignId = sentToCampaign.get(mail.getReplyMsgId());
+                if (campaignId != null) {
+                    mailToCampaign.put(mail.getId(), campaignId);
+                }
+            }
         }
-        StringBuilder sb = new StringBuilder();
-        int i = 0;
-        for (String from : froms) {
-            if (i >= 3) {
-                break;
+        Set<Long> unmatchedIds = inboxIds.stream()
+                .filter(id -> !mailToCampaign.containsKey(id))
+                .collect(Collectors.toSet());
+        if (!unmatchedIds.isEmpty()) {
+            List<MarketEmailsEntity> byReply = marketEmailsService.list(new LambdaQueryWrapper<MarketEmailsEntity>()
+                    .select(MarketEmailsEntity::getFirstReplyEmailId, MarketEmailsEntity::getMarketingCampaignId)
+                    .in(MarketEmailsEntity::getFirstReplyEmailId, unmatchedIds)
+                    .eq(MarketEmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
+                    .isNotNull(MarketEmailsEntity::getMarketingCampaignId));
+            for (MarketEmailsEntity email : byReply) {
+                if (email.getFirstReplyEmailId() != null && email.getMarketingCampaignId() != null) {
+                    mailToCampaign.putIfAbsent(email.getFirstReplyEmailId(), email.getMarketingCampaignId());
+                }
             }
-            if (sb.length() > 0) {
-                sb.append("、");
+        }
+        return mailToCampaign;
+    }
+
+    private Map<Long, MarketingCampaignEntity> loadCampaigns(Set<Long> campaignIds) {
+        if (CollectionUtils.isEmpty(campaignIds)) {
+            return new HashMap<>();
+        }
+        return marketingCampaignService.listByIds(campaignIds).stream()
+                .filter(c -> c.getId() != null)
+                .collect(Collectors.toMap(MarketingCampaignEntity::getId, c -> c, (a, b) -> a));
+    }
+
+    private static Set<Long> resolveCampaignSupervisors(MarketingCampaignEntity campaign) {
+        Set<Long> toUserIds = new HashSet<>();
+        if (StringUtils.hasText(campaign.getSupervisor())) {
+            for (String part : campaign.getSupervisor().split(",")) {
+                if (!StringUtils.hasText(part)) {
+                    continue;
+                }
+                try {
+                    Long userId = Long.valueOf(part.trim());
+                    if (userId > 0) {
+                        toUserIds.add(userId);
+                    }
+                } catch (NumberFormatException ignored) {
+                    // skip
+                }
             }
-            sb.append("【").append(from).append("】");
-            i++;
         }
-        String more = froms.size() > 3 ? "等" : "";
-        return "您有来自" + sb + more + "的" + count + "封新邮件,请前往查看!";
+        if (toUserIds.isEmpty() && campaign.getOwnerBy() != null && campaign.getOwnerBy() > 0) {
+            toUserIds.add(campaign.getOwnerBy());
+        }
+        return toUserIds;
     }
 
-    private static Long resolveOwner(MktMailMessageEntity mail) {
-        if (mail.getOwnerBy() != null && mail.getOwnerBy() > 0) {
-            return mail.getOwnerBy();
+    private static String buildContent(MktMailMessageEntity mail, MarketingCampaignEntity campaign) {
+        String from = StrUtil.blankToDefault(mail.getFromAddr(), "");
+        String subject = StrUtil.blankToDefault(mail.getSubject(), "");
+        String campaignName = StringUtils.hasText(campaign.getName()) ? campaign.getName() : "";
+        if (StringUtils.hasText(from) && StringUtils.hasText(subject)) {
+            return "客户【" + from + "】回复了营销活动【" + campaignName + "】的邮件《" + subject + "》,请前往查看!";
+        }
+        if (StringUtils.hasText(from)) {
+            return "客户【" + from + "】回复了营销活动【" + campaignName + "】的邮件,请前往查看!";
         }
-        if (mail.getCreateBy() != null && mail.getCreateBy() > 0) {
-            return mail.getCreateBy();
+        if (StringUtils.hasText(campaignName)) {
+            return "营销活动【" + campaignName + "】有一封新的客户回复邮件,请前往查看!";
         }
-        return null;
+        return "您有一封新的客户回复邮件,请前往查看!";
     }
 }

+ 3 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/task/MarketEmailsSendSyncTask.java

@@ -28,6 +28,9 @@ public class MarketEmailsSendSyncTask {
             //客户开启自动化后,到时间没有发送未打开邮件,再次发送
             marketEmailsService.sendAgain();
 
+            //如果超过发送时间半小时还是状态还是-1,将状态改为-3
+            marketEmailsService.cancelOverduePending();
+
 
         } catch (Exception e) {
             log.error("mkt_mail_status → market_emails 同步失败", e);

+ 16 - 16
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/task/MktMailNewNotifyTask.java

@@ -14,20 +14,20 @@ import javax.annotation.Resource;
 @Component
 public class MktMailNewNotifyTask {
 
-//    @Resource
-//    private MktMailNewNotifyService mktMailNewNotifyService;
-//
-//    @Scheduled(cron = "0 */2 * * * ?")
-//    public void sync() {
-//        long start = System.currentTimeMillis();
-//        try {
-//            int notified = mktMailNewNotifyService.syncOnce();
-//            if (notified > 0) {
-//                log.info("营销收件箱新邮件提醒完成 notified={} cost={}ms",
-//                        notified, System.currentTimeMillis() - start);
-//            }
-//        } catch (Exception e) {
-//            log.error("营销收件箱新邮件提醒失败", e);
-//        }
-//    }
+    @Resource
+    private MktMailNewNotifyService mktMailNewNotifyService;
+
+    @Scheduled(cron = "0 */2 * * * ?")
+    public void sync() {
+        long start = System.currentTimeMillis();
+        try {
+            int notified = mktMailNewNotifyService.syncOnce();
+            if (notified > 0) {
+                log.info("营销收件箱新邮件提醒完成 notified={} cost={}ms",
+                        notified, System.currentTimeMillis() - start);
+            }
+        } catch (Exception e) {
+            log.error("营销收件箱新邮件提醒失败", e);
+        }
+    }
 }

+ 2 - 2
ui/smarttrade-platform/src/components/addMarketing.vue

@@ -161,7 +161,7 @@
                     <h4><i class="fas fa-users"></i> 目标客户群</h4>
 
                     <!-- AI智能推荐 -->
-                    <div class="ai-recommend-section">
+                    <!-- <div class="ai-recommend-section">
                         <div class="ai-recommend-header">
                             <i class="fas fa-robot"></i>
                             <span>AI 智能推荐客户</span>
@@ -198,7 +198,7 @@
                                 <i class="fas fa-check-circle"></i>
                             </div>
                         </div>
-                    </div>
+                    </div> -->
 
                     <div class="advanced-filters">
                         <div class="filter-header">

+ 4 - 2
ui/smarttrade-platform/src/components/layout/TopBar.vue

@@ -632,12 +632,13 @@
 
 <script setup>
 import { ref, computed, onMounted } from 'vue'
-import { useRoute } from 'vue-router'
+import { useRoute, useRouter } from 'vue-router'
 import AddMarketing from '../addMarketing.vue'
 import { getNoReadCount, getMessageList, updateRead, updateReadAll, listPageBuUserSet } from '@/api/layout'
 import moment from 'moment'
 
 const route = useRoute()
+const router = useRouter()
 const showQuickActions = ref(false)
 const showNotifications = ref(false)
 const showHelp = ref(false)
@@ -855,7 +856,8 @@ const getIconColor = (type) => {
 
 const handleNotificationClick = (notification) => {
   setRead(notification)
-  selectedNotification.value = notification
+  // selectedNotification.value = notification
+  window.open(notification.callbackUrl, '_blank')
   // if(notification.callbackUrl){
   //     window.open(item.callbackUrl, '_blank')
   // }

+ 1 - 0
ui/smarttrade-platform/src/components/marketing/customerDetail.vue

@@ -213,6 +213,7 @@
             customers.value = []
             total.value = 0
         }else{
+            filterData.value = allData.value
             customers.value = allData.value.slice(0, 10)
             total.value = allData.value.length
         }