2 Komitmen fde84feaaa ... 84009acfaf

Pembuat SHA1 Pesan Tanggal
  YPZ 84009acfaf Merge remote-tracking branch 'origin/master' 2 hari lalu
  YPZ 66333897b3 添加sop 2 hari lalu

+ 6 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/controller/MarketEmailsController.java

@@ -57,6 +57,12 @@ public class MarketEmailsController {
         return marketEmailsService.cancel(marketEmailsDTO);
     }
 
+    @GetMapping("/reply-list")
+    @ApiOperation("营销: 回复邮件的客户列表(按 marketingCampaignId)")
+    public Result<Object> replyList(Long id) {
+        return marketEmailsService.getReplyList(id);
+    }
+
     @PostMapping("/add")
     @ApiOperation("营销: 营销邮件添加")
     public Result<?> add(@RequestBody MarketEmailsDTO marketEmailsDTO) {

+ 15 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/entity/MarketEmailsEntity.java

@@ -77,4 +77,19 @@ public class MarketEmailsEntity extends SysBaseField {
     @TableField("first_open_time")
     private Date firstOpenTime;
 
+    @ApiModelProperty(value = "首次回复时间")
+    @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("first_reply_time")
+    private Date firstReplyTime;
+
+    @ApiModelProperty(value = "首次回复邮件id")
+    @TableField("first_reply_email_id")
+    private Long firstReplyEmailId;
+
+    @ApiModelProperty(value = "首次回复邮件主题")
+    @TableField("first_reply_theme")
+    private String firstReplyTheme;
+
 }

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

@@ -40,6 +40,12 @@ public interface MarketEmailsService extends MyBaseService<MarketEmailsEntity> {
      */
     Result<Object> cancel(MarketEmailsDTO marketEmailsDTO);
 
+    /**
+     * 回复邮件的客户列表(按 marketingCampaignId 过滤 + hasReply=1 + recipient 聚合)
+     * 不分页(页面 Tab 全部/已保护/营销暂停 展示)
+     */
+    Result<Object> getReplyList(Long marketingCampaignId);
+
     /**
      * 邮件管理弹窗(按营销活动 id 查询,拼装一个营销活动下所有邮件卡片)
      *

+ 132 - 2
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MarketEmailsServiceImpl.java

@@ -233,11 +233,11 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
             }
             if (!customerIdSet.isEmpty()) {
                 try {
-                    java.util.List<com.storlead.trade.entity.CustomerEntity> customers =
+                    java.util.List<CustomerEntity> customers =
                             customerTradeService.listByIds(new java.util.ArrayList<>(customerIdSet));
                     if (!CollectionUtils.isEmpty(customers)) {
                         java.util.Map<Long, String> idToName = new java.util.HashMap<>(customers.size());
-                        for (com.storlead.trade.entity.CustomerEntity c : customers) {
+                        for (CustomerEntity c : customers) {
                             if (c.getId() != null) {
                                 idToName.put(c.getId(), c.getCustomerName());
                             }
@@ -353,6 +353,136 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
         return Result.ok();
     }
 
+    /**
+     * 回复邮件的客户列表(按 marketingCampaignId 过滤 + hasReply=1 + recipient 聚合)
+     * 不分页:全部/已保护/营销暂停 三个 Tab 由前端在结果中过滤计数。
+     */
+    @Override
+    public Result<Object> getReplyList(Long marketingCampaignId) {
+        if (ObjectUtils.isEmpty(marketingCampaignId)) {
+            return Result.error("marketingCampaignId不能为空");
+        }
+        // 查该营销活动下 has_reply=1 且未软删的邮件
+        LambdaQueryWrapper<MarketEmailsEntity> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(MarketEmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
+        wrapper.eq(MarketEmailsEntity::getHasReply, 1);
+        wrapper.eq(MarketEmailsEntity::getMarketingCampaignId, marketingCampaignId);
+        wrapper.isNotNull(MarketEmailsEntity::getRecipient);
+        wrapper.ne(MarketEmailsEntity::getRecipient, "");
+        wrapper.select(MarketEmailsEntity::getRecipient,
+                MarketEmailsEntity::getCustomerId,
+                MarketEmailsEntity::getHasProtect,
+                MarketEmailsEntity::getHasSuspend,
+                MarketEmailsEntity::getFirstReplyTime,
+                MarketEmailsEntity::getFirstReplyEmailId,
+                MarketEmailsEntity::getFirstReplyTheme,
+                MarketEmailsEntity::getCreateTime);
+        List<MarketEmailsEntity> all = this.list(wrapper);
+
+        // 按 recipient 聚合
+        Map<String, com.storlead.trade.vo.MarketEmailsReplyVO> aggMap = new HashMap<>();
+        if (!CollectionUtils.isEmpty(all)) {
+            for (MarketEmailsEntity e : all) {
+                String r = e.getRecipient();
+                if (r == null || r.isEmpty()) {
+                    continue;
+                }
+                com.storlead.trade.vo.MarketEmailsReplyVO vo = aggMap.computeIfAbsent(r, k -> {
+                    com.storlead.trade.vo.MarketEmailsReplyVO v = new com.storlead.trade.vo.MarketEmailsReplyVO();
+                    v.setRecipient(k);
+                    v.setReplyCount(0L);
+                    return v;
+                });
+                vo.setReplyCount(vo.getReplyCount() + 1);
+                // 记下最早一条所属的 customerId(用于回填客户名)
+                if (vo.getCustomerId() == null && e.getCustomerId() != null) {
+                    vo.setCustomerId(e.getCustomerId());
+                }
+                // 取最早的 firstReplyTime + 同时记录主题/emailId
+                if (e.getFirstReplyTime() != null) {
+                    if (vo.getFirstReplyTime() == null || e.getFirstReplyTime().before(vo.getFirstReplyTime())) {
+                        vo.setFirstReplyTime(e.getFirstReplyTime());
+                        vo.setReplyContent(e.getFirstReplyTheme());
+                    }
+                }
+                // MAX(has_protect) / MAX(has_suspend) 语义
+                if (e.getHasProtect() != null && e.getHasProtect() == 1) {
+                    vo.setHasProtect(1);
+                }
+                if (e.getHasSuspend() != null && e.getHasSuspend() == 1) {
+                    vo.setHasSuspend(1);
+                }
+            }
+        }
+        // 默认未保护/未暂停为 0
+        for (com.storlead.trade.vo.MarketEmailsReplyVO vo : aggMap.values()) {
+            if (vo.getHasProtect() == null) vo.setHasProtect(0);
+            if (vo.getHasSuspend() == null) vo.setHasSuspend(0);
+        }
+        // 按首次回复时间倒序(最近回复的排前)
+        List<com.storlead.trade.vo.MarketEmailsReplyVO> voList = new java.util.ArrayList<>(aggMap.values());
+        voList.sort((a, b) -> {
+            if (a.getFirstReplyTime() == null && b.getFirstReplyTime() == null) return 0;
+            if (a.getFirstReplyTime() == null) return 1;
+            if (b.getFirstReplyTime() == null) return -1;
+            return b.getFirstReplyTime().compareTo(a.getFirstReplyTime());
+        });
+
+        // 批量回填:客户名 / 客户等级 / 核心标签
+        if (!voList.isEmpty()) {
+            java.util.Set<Long> customerIdSet = new java.util.LinkedHashSet<>();
+            for (com.storlead.trade.vo.MarketEmailsReplyVO v : voList) {
+                if (v.getCustomerId() != null) customerIdSet.add(v.getCustomerId());
+            }
+            if (!customerIdSet.isEmpty()) {
+                try {
+                    // 回填客户名
+                    java.util.List<CustomerEntity> customers =
+                            customerTradeService.listByIds(new java.util.ArrayList<>(customerIdSet));
+                    java.util.Map<Long, CustomerEntity> customerMap = new java.util.HashMap<>();
+                    if (!CollectionUtils.isEmpty(customers)) {
+                        for (CustomerEntity c : customers) {
+                            if (c.getId() != null) customerMap.put(c.getId(), c);
+                        }
+                    }
+                    // 回填客户等级 / 核心标签(从 CustomerAnalysisResultEntity)
+                    // 一个 customer 可能有多个分析结果,取最新一条(id 最大)
+                    java.util.List<CustomerAnalysisResultEntity> analysisList =
+                            customerAnalysisResultEntityService.list(
+                                    new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<CustomerAnalysisResultEntity>()
+                                            .in(CustomerAnalysisResultEntity::getCustomerId, customerIdSet)
+                                            .eq(CustomerAnalysisResultEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
+                                            .orderByDesc(CustomerAnalysisResultEntity::getId));
+                    java.util.Map<Long, CustomerAnalysisResultEntity> analysisMap = new java.util.HashMap<>();
+                    if (!CollectionUtils.isEmpty(analysisList)) {
+                        for (CustomerAnalysisResultEntity a : analysisList) {
+                            if (a.getCustomerId() != null && !analysisMap.containsKey(a.getCustomerId())) {
+                                analysisMap.put(a.getCustomerId(), a);
+                            }
+                        }
+                    }
+                    for (com.storlead.trade.vo.MarketEmailsReplyVO v : voList) {
+                        CustomerEntity c = customerMap.get(v.getCustomerId());
+                        if (c != null) {
+                            v.setCustomerName(c.getCustomerName());
+                        }
+                        CustomerAnalysisResultEntity a = analysisMap.get(v.getCustomerId());
+                        if (a != null) {
+                            v.setCustomerLevel(a.getCustomerLevel());
+                            v.setCoreTags(a.getCoreTags());
+                            if (a.getCustomerLevel() != null) {
+                                v.setCustomerLevelLabel(a.getCustomerLevel());
+                            }
+                        }
+                    }
+                } catch (Exception ex) {
+                    log.warn("回填客户名/标签失败,customerIds={} err={}", customerIdSet, ex.getMessage());
+                }
+            }
+        }
+        return Result.ok(voList);
+    }
+
 
     @Override
     public Result<Object> getManagementDetail(Long marketingCampaignId) {

+ 62 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/vo/MarketEmailsReplyVO.java

@@ -0,0 +1,62 @@
+package com.storlead.trade.vo;
+
+import com.alibaba.fastjson.annotation.JSONField;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 回复邮件的客户列表响应 VO(按 recipient 聚合)
+ * <p>
+ * 数据源:market_emails.has_reply = 1 的记录
+ * 排序:第一封回复时间 ASC(最早的回复排前面)
+ * 聚合:同一 recipient 多条回复合并为一张卡片,replyCount 累加
+ * </p>
+ *
+ * @author Generated
+ */
+@Data
+@ApiModel(value = "MarketEmailsReplyVO", description = "回复邮件的客户(按发件人邮箱聚合)")
+public class MarketEmailsReplyVO implements Serializable {
+
+    @ApiModelProperty(value = "发件人邮箱(客户邮箱)")
+    private String recipient;
+
+    @ApiModelProperty(value = "客户id(同一 recipient 取最早回复那一条所属的 customerId)")
+    private Long customerId;
+
+    @ApiModelProperty(value = "客户名称")
+    private String customerName;
+
+    @ApiModelProperty(value = "客户优先级标签(如:高优先级/高价值)")
+    private String customerLevel;
+
+    @ApiModelProperty(value = "客户等级标签")
+    private String customerLevelLabel;
+
+    @ApiModelProperty(value = "核心标签(AI 给客户打的标签,逗号分隔)")
+    private String coreTags;
+
+    @ApiModelProperty(value = "回复次数(同 recipient 下的邮件回复记录数)")
+    private Long replyCount;
+
+    @ApiModelProperty(value = "首次回复时间")
+    @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")
+    private Date firstReplyTime;
+
+    @ApiModelProperty(value = "回复内容(复用 first_reply_theme 字段,DB 未单独建 reply_content)")
+    private String replyContent;
+
+    @ApiModelProperty(value = "是否保护(0未保护、1保护)")
+    private Integer hasProtect;
+
+    @ApiModelProperty(value = "是否暂停(0未暂停、1暂停)")
+    private Integer hasSuspend;
+}