Quellcode durchsuchen

Merge remote-tracking branch 'origin/master'

zhujing vor 3 Wochen
Ursprung
Commit
81dc08e391
19 geänderte Dateien mit 656 neuen und 7 gelöschten Zeilen
  1. 2 2
      Jenkinsfile
  2. 1 1
      java/storlead-api/src/main/resources/application-dev.yml
  3. 1 1
      java/storlead-api/src/main/resources/application-prod.yml
  4. 1 1
      java/storlead-api/src/main/resources/application-test.yml
  5. 10 0
      java/storlead-api/src/main/resources/application.yml
  6. 1 1
      java/storlead-knowledge/storlead-knowledge-biz/src/main/java/com/storlead/knowledge/service/impl/AiDatasetsFileServiceImpl.java
  7. 273 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/controller/MarketingCampaignSopDetailController.java
  8. 8 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/dto/CustomerAnalysisResultDTO.java
  9. 57 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/dto/MarketingCampaignCustomerDTO.java
  10. 170 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/dto/MarketingCampaignSopDetailDTO.java
  11. 8 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/entity/CustomerAnalysisResultEntity.java
  12. 11 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/mapper/CustomerBaseEntityMapper.java
  13. 8 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/CustomerBaseEntityService.java
  14. 15 1
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/CustomerBaseEntityServiceImpl.java
  15. 44 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/vo/MarketingCampaignCustomerVO.java
  16. 37 0
      java/storlead-sasa/storlead-trade/src/main/resources/mapper/CustomerBaseEntityMapper.xml
  17. 3 0
      java/storlead-system/storlead-system-biz/src/main/java/com/storlead/system/service/impl/SysDictDetailServiceImpl.java
  18. 3 0
      java/storlead-system/storlead-system-biz/src/main/java/com/storlead/system/service/impl/SysDictServiceImpl.java
  19. 3 0
      java/storlead-user/storlead-user-biz/src/main/java/com/storlead/user/service/impl/UserServiceImpl.java

+ 2 - 2
Jenkinsfile

@@ -291,7 +291,7 @@ pipeline {
                             sshCommand remote: remote, command: "docker pull ${DOCKER_IMG_NAME}"
                         }
                         sh """
-                          echo '
+                        echo '
   version: "3.7"
   services:
     ${params.module}-${params.profile}:
@@ -332,7 +332,7 @@ pipeline {
       ports:
         - "${API_PORT}:${API_PORT}"
         - "${API_REMOTE_DEBUG_PORT}:${API_REMOTE_DEBUG_PORT}"
-    networks:
+      networks:
         - ${ARTIFACTID}
       extra_hosts:
         - "www.storlead.com:172.18.194.161"

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

@@ -190,7 +190,7 @@ logging:
 
 environment: test
 dify:
-  base-url: http://192.168.1.77/v1/
+  base-url: http://47.112.196.2:11434/v1/
   dataset-api-key: dataset-qfhXMNcjsRSwN5CK6aoWx3hl
   app-keys:
     chat: app-1-qfhXMNcjsRSwN5CK6aoWx3hl  #对话

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

@@ -229,7 +229,7 @@ file:
   avatarMaxSize: 5
 
 dify:
-  base-url: http://192.168.1.77/v1/
+  base-url: http://47.112.196.2:11434/v1/
   dataset-api-key: dataset-qfhXMNcjsRSwN5CK6aoWx3hl
   app-keys:
     chat: app-1-qfhXMNcjsRSwN5CK6aoWx3hl  #对话

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

@@ -175,7 +175,7 @@ logging:
 
 environment: test
 dify:
-  base-url: http://192.168.1.77/v1/
+  base-url: http://47.112.196.2:11434/v1/
   dataset-api-key: dataset-qfhXMNcjsRSwN5CK6aoWx3hl
   app-keys:
     chat: app-1-qfhXMNcjsRSwN5CK6aoWx3hl  #对话

+ 10 - 0
java/storlead-api/src/main/resources/application.yml

@@ -100,6 +100,16 @@ storlead:
           - com.storlead.mail.service
           - com.storlead.mail.mapper
         datasource: mail
+      # 用户相关必须走 master(避免被 trade/sales 外层数据源污染)
+      - packages:
+          - com.storlead.user.service
+          - com.storlead.user.mapper
+        datasource: master
+      # 字典等系统配置必须走 master(DictUtils / SysDict*)
+      - packages:
+          - com.storlead.system.service
+          - com.storlead.system.mapper
+        datasource: master
 
 management:
   health:

+ 1 - 1
java/storlead-knowledge/storlead-knowledge-biz/src/main/java/com/storlead/knowledge/service/impl/AiDatasetsFileServiceImpl.java

@@ -44,7 +44,7 @@ public class AiDatasetsFileServiceImpl extends MyBaseServiceImpl<AiDatasetsFileM
         IPage<AiDatasetsFileEntity> entityPage = this.page(page, wrapper);
 
         // 转换为VO
-        DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
+        //DefaultMapperFactory mapperFactory = new DefaultMapperFactory.Builder().build();
         IPage<AiDatasetsFileEntity> voPage = new Page<>(entityPage.getCurrent(), entityPage.getSize());
         voPage.setTotal(entityPage.getTotal());
         voPage.setRecords(entityPage.getRecords());

+ 273 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/controller/MarketingCampaignSopDetailController.java

@@ -4,20 +4,32 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.storlead.framework.common.result.Result;
+import com.storlead.mail.pojo.entity.EmailsEntity;
+import com.storlead.trade.dto.MarketingCampaignCustomerDTO;
 import com.storlead.trade.dto.MarketingCampaignSopDetailDTO;
+import com.storlead.trade.vo.MarketingCampaignCustomerVO;
+import com.storlead.trade.entity.CustomerBaseEntity;
 import com.storlead.trade.entity.MarketingCampaignSopDetailEntity;
+import com.storlead.trade.service.CustomerBaseEntityService;
 import com.storlead.trade.service.MarketingCampaignSopDetailService;
 import io.swagger.annotations.Api;
 import io.swagger.annotations.ApiOperation;
 import org.springframework.beans.BeanUtils;
+import org.springframework.jdbc.core.JdbcTemplate;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 import org.springframework.util.ObjectUtils;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 /**
  * 营销活动SOP明细 Controller
@@ -30,6 +42,20 @@ public class MarketingCampaignSopDetailController {
     @Resource
     private MarketingCampaignSopDetailService marketingCampaignSopDetailService;
 
+    @Resource
+    private CustomerBaseEntityService customerBaseEntityService;
+
+    @Resource
+    private JdbcTemplate jdbcTemplate;
+
+    /** 智能调度类型常量 */
+    private static final String SCHEDULE_TYPE_NOW = "now";
+    private static final String SCHEDULE_TYPE_SCHEDULED = "scheduled";
+    private static final String SCHEDULE_TYPE_AI = "ai";
+
+    /** 邮件状态:-1待发送 */
+    private static final int EMAIL_STATUS_PENDING = -1;
+
     /**
      * 根据id查询详情
      */
@@ -96,6 +122,227 @@ public class MarketingCampaignSopDetailController {
         return Result.result(entity);
     }
 
+
+    @PostMapping("/add-confirm")
+    @ApiOperation(value = "确认创建/编辑营销活动", notes = "根据智能调度类型生成待发送邮件")
+    @Transactional(rollbackFor = Throwable.class)
+    public Result<?> addConfirm(@RequestBody MarketingCampaignSopDetailDTO dto) {
+        if (ObjectUtils.isEmpty(dto.getMarketingCampaignId())) {
+            return Result.error("营销活动id不能为空");
+        }
+
+        String scheduleType = dto.getSmartSchedulingType();
+        if (scheduleType == null || scheduleType.isEmpty()) {
+            return Result.error("智能调度类型不能为空");
+        }
+
+        // 查询该营销活动下的所有SOP明细(按步骤排序)
+        LambdaQueryWrapper<MarketingCampaignSopDetailEntity> sopWrapper = new LambdaQueryWrapper<>();
+        sopWrapper.eq(MarketingCampaignSopDetailEntity::getMarketingCampaignId, dto.getMarketingCampaignId());
+        sopWrapper.orderByAsc(MarketingCampaignSopDetailEntity::getStep);
+        List<MarketingCampaignSopDetailEntity> sopDetails = marketingCampaignSopDetailService.list(sopWrapper);
+        if (CollectionUtils.isEmpty(sopDetails)) {
+            return Result.error("该营销活动未关联SOP明细");
+        }
+
+        // 查询该营销活动下的所有客户(customer_base表)
+        List<CustomerBaseEntity> customers = customerBaseEntityService.lambdaQuery()
+                .eq(CustomerBaseEntity::getMarketingCampaignId, dto.getMarketingCampaignId())
+                .list();
+        if (CollectionUtils.isEmpty(customers)) {
+            return Result.error("该营销活动未关联客户");
+        }
+
+        // 计算每一步的发送时间
+        LocalDateTime baseTime = resolveBaseSendTime(scheduleType, dto);
+
+        // 生成邮件
+        int generated = 0;
+        for (CustomerBaseEntity customer : customers) {
+            generated += generateEmailsForCustomer(customer, sopDetails, dto, baseTime, scheduleType);
+        }
+
+        java.util.Map<String, Object> result = new java.util.HashMap<>();
+        result.put("scheduleType", scheduleType);
+        result.put("customerCount", customers.size());
+        result.put("sopStepCount", sopDetails.size());
+        result.put("emailsGenerated", generated);
+        return Result.result(result);
+    }
+
+    /**
+     * 计算基础发送时间
+     */
+    private LocalDateTime resolveBaseSendTime(String scheduleType, MarketingCampaignSopDetailDTO dto) {
+        if (SCHEDULE_TYPE_NOW.equalsIgnoreCase(scheduleType)) {
+            // 立即发送
+            return LocalDateTime.now();
+        }
+        if (SCHEDULE_TYPE_SCHEDULED.equalsIgnoreCase(scheduleType)) {
+            // 定时发送:使用指定日期 + 时间
+            LocalDate date = dto.getSmartSchedulingStartDate() != null
+                    ? dto.getSmartSchedulingStartDate() : LocalDate.now();
+            String timeStr = dto.getSmartSchedulingStartTime();
+            LocalTime time = (timeStr != null && !timeStr.isEmpty())
+                    ? LocalTime.parse(timeStr, DateTimeFormatter.ofPattern("HH:mm"))
+                    : LocalTime.of(9, 0);
+            return LocalDateTime.of(date, time);
+        }
+        // AI最优时间:当前日期 + 默认上午9点
+        return LocalDateTime.of(LocalDate.now(), LocalTime.of(9, 0));
+    }
+
+    /**
+     * 为单个客户生成所有SOP步骤的邮件
+     */
+    private int generateEmailsForCustomer(CustomerBaseEntity customer,
+                                          List<MarketingCampaignSopDetailEntity> sopDetails,
+                                          MarketingCampaignSopDetailDTO dto,
+                                          LocalDateTime baseTime,
+                                          String scheduleType) {
+        // 对每个客户去重步骤
+        Set<Integer> stepSet = new HashSet<>();
+        int count = 0;
+        for (MarketingCampaignSopDetailEntity sop : sopDetails) {
+            Integer step = sop.getStep() == null ? 0 : sop.getStep();
+            if (!stepSet.add(step)) {
+                continue;
+            }
+            // 计算发送时间:baseTime + 之前所有步骤累加间隔天数
+            LocalDateTime sendTime = baseTime;
+            for (MarketingCampaignSopDetailEntity prev : sopDetails) {
+                if (prev.getStep() == null || sop.getStep() == null) continue;
+                if (prev.getStep() >= sop.getStep()) break;
+                sendTime = addInterval(sendTime, prev.getDays());
+            }
+            // 立即发送类型则让 sent_date 为 null 由调度发送;delay_send_time 仅对非立即模式生效
+            EmailsEntity email = buildEmailEntity(customer, sop, dto, sendTime, scheduleType);
+            insertEmail(email);
+            count++;
+        }
+        return count;
+    }
+
+    /**
+     * 累加间隔天数
+     */
+    private LocalDateTime addInterval(LocalDateTime base, String days) {
+        if (days == null || days.isEmpty()) {
+            return base;
+        }
+        try {
+            int d = Integer.parseInt(days.trim());
+            return base.plusDays(d);
+        } catch (NumberFormatException e) {
+            return base;
+        }
+    }
+
+    /**
+     * 构造 EmailsEntity(不依赖 mail-biz)
+     */
+    private EmailsEntity buildEmailEntity(CustomerBaseEntity customer,
+                                          MarketingCampaignSopDetailEntity sop,
+                                          MarketingCampaignSopDetailDTO dto,
+                                          LocalDateTime sendTime,
+                                          String scheduleType) {
+        EmailsEntity email = new EmailsEntity();
+        email.setSubject(dto.getEmailSubject() != null ? dto.getEmailSubject() : sop.getName());
+        email.setFrom(dto.getEmailAddress());
+        email.setFromName(dto.getDescription());
+        email.setContent(sop.getEmailContent() != null ? sop.getEmailContent() : "");
+        email.setStatus(EMAIL_STATUS_PENDING);
+        email.setInOutMark(2); // 2 = 出
+        email.setIsRead(0);
+        email.setIsStar(0);
+        email.setIsTrash(0);
+        email.setIsReceipt(0);
+        email.setIsTrack(dto.getTrackConfigOpen() != null ? dto.getTrackConfigOpen() : 0);
+        email.setIsOnlyHead(0);
+        email.setSourceType(0);
+        // 收件人:用 customerAnalysisId 占位(实际邮箱需关联客户表)
+        email.setCustomerId(customer.getCustomerAnalysisId());
+        // 触达渠道作为 folder
+        email.setFolder(sop.getType() != null ? sop.getType() : "email");
+        // 定时发送
+        if (!SCHEDULE_TYPE_NOW.equalsIgnoreCase(scheduleType)) {
+            email.setDelaySendTime(sendTime);
+            email.setSentDate(null);
+        } else {
+            email.setDelaySendTime(null);
+            // 立即发送:sent_date 是 java.util.Date 类型,需要从 LocalDateTime 转换
+            email.setSentDate(java.util.Date.from(sendTime.atZone(java.time.ZoneId.systemDefault()).toInstant()));
+        }
+        // 关联营销活动 id 和 sop 步骤信息到 remark
+        email.setRemark("marketingCampaignId=" + dto.getMarketingCampaignId()
+                + ",sopDetailId=" + sop.getSopDetailId()
+                + ",step=" + sop.getStep()
+                + ",scheduleType=" + scheduleType);
+        return email;
+    }
+
+    /**
+     * 通过 JDBC 插入 emails 表,避免 trade 模块依赖 mail-biz
+     * 采用动态拼列方式,仅写入 EmailsEntity 中非空的字段,最大程度兼容表结构变更
+     */
+    private void insertEmail(EmailsEntity email) {
+        java.util.LinkedHashMap<String, Object> fieldMap = new java.util.LinkedHashMap<>();
+        putIfPresent(fieldMap, "subject", email.getSubject());
+        putIfPresent(fieldMap, "recipient", email.getRecipient());
+        putIfPresent(fieldMap, "recipient_name", email.getRecipientName());
+        putIfPresent(fieldMap, "recipient_cc", email.getRecipientCc());
+        putIfPresent(fieldMap, "recipient_cc_name", email.getRecipientCcName());
+        putIfPresent(fieldMap, "recipient_bcc", email.getRecipientBcc());
+        putIfPresent(fieldMap, "recipient_bcc_name", email.getRecipientBccName());
+        putIfPresent(fieldMap, "`from`", email.getFrom());
+        putIfPresent(fieldMap, "from_name", email.getFromName());
+        putIfPresent(fieldMap, "content", email.getContent());
+        putIfPresent(fieldMap, "folder", email.getFolder());
+        putIfPresent(fieldMap, "sent_date", email.getSentDate() != null
+                ? new java.sql.Timestamp(email.getSentDate().getTime()) : null);
+        putIfPresent(fieldMap, "delay_send_time", email.getDelaySendTime());
+        putIfPresent(fieldMap, "status", email.getStatus());
+        putIfPresent(fieldMap, "remark", email.getRemark());
+        putIfPresent(fieldMap, "in_out_mark", email.getInOutMark());
+        putIfPresent(fieldMap, "is_read", email.getIsRead());
+        putIfPresent(fieldMap, "is_track", email.getIsTrack());
+        putIfPresent(fieldMap, "is_star", email.getIsStar());
+        putIfPresent(fieldMap, "is_trash", email.getIsTrash());
+
+        // 必填字段默认值
+        fieldMap.putIfAbsent("status", -1);
+        fieldMap.putIfAbsent("is_read", 0);
+        fieldMap.putIfAbsent("is_trash", 0);
+        fieldMap.putIfAbsent("is_star", 0);
+        fieldMap.putIfAbsent("in_out_mark", 2);
+        fieldMap.putIfAbsent("is_delete", 0);
+        fieldMap.putIfAbsent("create_time", new java.sql.Timestamp(System.currentTimeMillis()));
+        fieldMap.putIfAbsent("update_time", new java.sql.Timestamp(System.currentTimeMillis()));
+
+        StringBuilder sql = new StringBuilder("INSERT INTO emails (");
+        StringBuilder placeholders = new StringBuilder();
+        List<Object> args = new ArrayList<>();
+        boolean first = true;
+        for (java.util.Map.Entry<String, Object> e : fieldMap.entrySet()) {
+            if (!first) {
+                sql.append(", ");
+                placeholders.append(", ");
+            }
+            sql.append(e.getKey());
+            placeholders.append("?");
+            args.add(e.getValue());
+            first = false;
+        }
+        sql.append(") VALUES (").append(placeholders).append(")");
+        jdbcTemplate.update(sql.toString(), args.toArray());
+    }
+
+    private void putIfPresent(java.util.Map<String, Object> map, String key, Object value) {
+        if (value != null) {
+            map.put(key, value);
+        }
+    }
+
     /**
      * 批量新增
      */
@@ -172,4 +419,30 @@ public class MarketingCampaignSopDetailController {
         List<MarketingCampaignSopDetailEntity> list = marketingCampaignSopDetailService.list(wrapper);
         return Result.result(list);
     }
+
+    /**
+     * 查询营销活动关联的客户列表(打开邮件的客户)
+     */
+    @PostMapping("/customers")
+    @ApiOperation(value = "查询营销活动关联的客户列表", notes = "支持按状态筛选:全部/高价值/已保护/营销暂停")
+    public Result<Object> getCampaignCustomers(@RequestBody MarketingCampaignCustomerDTO dto) {
+        if (ObjectUtils.isEmpty(dto.getMarketingCampaignId())) {
+            return Result.error("营销活动id不能为空");
+        }
+
+        IPage<MarketingCampaignCustomerVO> page = customerBaseEntityService.getCampaignCustomers(dto);
+
+        // 处理状态标签
+        for (MarketingCampaignCustomerVO vo : page.getRecords()) {
+            if (vo.getHasProtect() != null && vo.getHasProtect() == 1) {
+                vo.setStatusTag("已保护");
+            } else if (vo.getHasSuspend() != null && vo.getHasSuspend() == 1) {
+                vo.setStatusTag("营销暂停");
+            } else {
+                vo.setStatusTag("全部");
+            }
+        }
+
+        return Result.result(page);
+    }
 }

+ 8 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/dto/CustomerAnalysisResultDTO.java

@@ -105,4 +105,12 @@ public class CustomerAnalysisResultDTO extends Page {
 
     @ApiModelProperty(value = "idList")
     private List<Long> idList;
+
+    @ApiModelProperty(value = "是否是保护客户(0否,1是)")
+    @TableField("has_protect")
+    private Integer hasProtect;
+
+    @ApiModelProperty(value = "是否是暂停客户(0否,1是)")
+    @TableField("has_suspend")
+    private Integer hasSuspend;
 }

+ 57 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/dto/MarketingCampaignCustomerDTO.java

@@ -0,0 +1,57 @@
+package com.storlead.trade.dto;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 营销活动关联客户信息 DTO
+ */
+@Data
+@ApiModel(value = "MarketingCampaignCustomerDTO", description = "营销活动关联客户信息")
+public class MarketingCampaignCustomerDTO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "营销活动ID")
+    private Long marketingCampaignId;
+
+    @ApiModelProperty(value = "客户ID")
+    private Long customerId;
+
+    @ApiModelProperty(value = "客户名称")
+    private String customerName;
+
+    @ApiModelProperty(value = "客户邮箱")
+    private String email;
+
+    @ApiModelProperty(value = "保护状态:0未保护,1已保护")
+    private Integer hasProtect;
+
+    @ApiModelProperty(value = "暂停状态:0未暂停,1已暂停")
+    private Integer hasSuspend;
+
+    @ApiModelProperty(value = "客户等级")
+    private String customerLevel;
+
+    @ApiModelProperty(value = "客户状态标签:全部/高价值/已保护/营销暂停")
+    private String statusTag;
+
+    @ApiModelProperty(value = "邮件打开时间")
+    private String openTime;
+
+    @ApiModelProperty(value = "点击次数")
+    private Integer clickCount;
+
+    @ApiModelProperty(value = "页码")
+    private Integer pageIndex = 1;
+
+    @ApiModelProperty(value = "每页数量")
+    private Integer pageSize = 10;
+
+    @ApiModelProperty(value = "状态筛选:all全部/highValue高价值/protected已保护/suspended营销暂停")
+    private String filterStatus;
+}

+ 170 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/dto/MarketingCampaignSopDetailDTO.java

@@ -1,10 +1,17 @@
 package com.storlead.trade.dto;
 
+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.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.time.LocalDate;
 import java.util.List;
 
 /**
@@ -48,4 +55,167 @@ public class MarketingCampaignSopDetailDTO implements Serializable {
 
     @ApiModelProperty(value = "每页数量", required = true)
     private Integer pageSize = 10;
+
+
+    @ApiModelProperty(value = "优先级(1紧急、2高优先、3中等、4低优先)")
+    @TableField("priority")
+    private Integer priority;
+
+    @ApiModelProperty(value = "开始日期")
+    @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
+    @DateTimeFormat(pattern="yyyy-MM-dd")
+    @JSONField(format ="yyyy-MM-dd")
+    @TableField("begin_data")
+    private LocalDate beginData;
+
+    @ApiModelProperty(value = "结束日期")
+    @JsonFormat(timezone = "GMT+8",pattern = "yyyy-MM-dd")
+    @DateTimeFormat(pattern="yyyy-MM-dd")
+    @JSONField(format ="yyyy-MM-dd")
+    @TableField("end_date")
+    private LocalDate endDate;
+
+    @ApiModelProperty(value = "负责人id")
+    @TableField("supervisor")
+    private Long supervisor;
+
+    @ApiModelProperty(value = "关联商机")
+    @TableField("link_to_opportunity")
+    private Long linkToOpportunity;
+
+    @ApiModelProperty(value = "活动描述")
+    @TableField("description")
+    private String description;
+
+    @ApiModelProperty(value = "发送量")
+    @TableField("send_volume")
+    private Integer sendVolume;
+
+    @ApiModelProperty(value = "打开率")
+    @TableField("open_rate")
+    private Double openRate;
+
+    @ApiModelProperty(value = "回复率")
+    @TableField("reply_rate")
+    private Double replyRate;
+
+    @ApiModelProperty(value = "商机数")
+    @TableField("opportunity_count")
+    private Integer opportunityCount;
+
+    @ApiModelProperty(value = "渠道配置")
+    @TableField("channel_configuration")
+    private Integer channelConfiguration;
+
+    @ApiModelProperty(value = "发送邮箱")
+    @TableField("email_address")
+    private String emailAddress;
+
+    @ApiModelProperty(value = "发送时间")
+    @TableField("email_sent_time")
+    private String emailSentTime;
+
+    @ApiModelProperty(value = "邮件模板")
+    @TableField("email_template")
+    private String emailTemplate;
+
+    @ApiModelProperty(value = "每批发送量")
+    @TableField("email_batch_size")
+    private Integer emailBatchSize;
+
+    @ApiModelProperty(value = "邮件主题")
+    @TableField("email_subject")
+    private String emailSubject;
+
+    @ApiModelProperty(value = "是否追踪邮件打开(1是0否)")
+    @TableField("track_config_open")
+    private Integer trackConfigOpen;
+
+    @ApiModelProperty(value = "是否追踪链接点击(1是0否)")
+    @TableField("track_config_click")
+    private Integer trackConfigClick;
+
+    @ApiModelProperty(value = "是否追踪回复(1是0否)")
+    @TableField("track_config_reply")
+    private Integer trackConfigReply;
+
+    @ApiModelProperty(value = "SOP序列id")
+    @TableField("sop_id")
+    private Integer sopId;
+
+    @ApiModelProperty(value = "sop详情id(逗号分隔)")
+    @TableField("sop_detail_ids")
+    private String sopDetailIds;
+
+    @ApiModelProperty(value = "未打开邮件跟进规则(1是0否)")
+    @TableField("follow_up_rules_not_opened")
+    private Integer followUpRulesNotOpened;
+
+    @ApiModelProperty(value = "未打开邮件跟进天数")
+    @TableField("follow_up_rules_not_opened_days")
+    private String followUpRulesNotOpenedDays;
+
+    @ApiModelProperty(value = "未回复邮件跟进规则(1是0否)")
+    @TableField("follow_up_rules_not_replied")
+    private Integer followUpRulesNotReplied;
+
+    @ApiModelProperty(value = "未回复邮件跟进天数")
+    @TableField("follow_up_rules_not_replied_days")
+    private String followUpRulesNotRepliedDays;
+
+    @ApiModelProperty(value = "未点击链接跟进规则(1是0否)")
+    @TableField("follow_up_rules_link_not_clicked")
+    private Integer followUpRulesLinkNotClicked;
+
+    @ApiModelProperty(value = "未点击链接跟进天数")
+    @TableField("follow_up_rules_link_not_clicked_days")
+    private String followUpRulesLinkNotClickedDays;
+
+    @ApiModelProperty(value = "客户回复后自动停止(1是0否)")
+    @TableField("follow_up_rules_replied")
+    private Integer followUpRulesReplied;
+
+    @ApiModelProperty(value = "客户预约后停止(1是0否)")
+    @TableField("follow_up_rules_appointment_booked")
+    private Integer followUpRulesAppointmentBooked;
+
+    @ApiModelProperty(value = "A/B测试(1是0否)")
+    @TableField("a_b_testing")
+    private Integer aBTesting;
+
+    @ApiModelProperty(value = "智能调度类型")
+    @TableField("smart_scheduling_type")
+    private String smartSchedulingType;
+
+    @ApiModelProperty(value = "智能调度开始日期")
+    @TableField("smart_scheduling_start_date")
+    private LocalDate smartSchedulingStartDate;
+
+    @ApiModelProperty(value = "智能调度开始时间")
+    @TableField("smart_scheduling_start_time")
+    private String smartSchedulingStartTime;
+
+    @ApiModelProperty(value = "是否已确认内容合规")
+    @TableField("has_content_compliant")
+    private Integer hasContentCompliant;
+
+    @ApiModelProperty(value = "是否已确认发送时间合理")
+    @TableField("has_schedule_approved")
+    private Integer hasScheduleApproved;
+
+    @ApiModelProperty(value = "是否符合GDPR")
+    @TableField("has_gdpr_compliant")
+    private Integer hasGdprCompliant;
+
+    @ApiModelProperty(value = "是否包含退订链接")
+    @TableField("has_unsubscribe_link")
+    private Integer hasUnsubscribeLink;
+
+    @ApiModelProperty(value = "进度百分比")
+    @TableField("progress_percent")
+    private Double progressPercent;
+
+    @ApiModelProperty(value = "状态 0未开始 1运行中 2暂停 3完成")
+    @TableField("status")
+    private Integer status;
 }

+ 8 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/entity/CustomerAnalysisResultEntity.java

@@ -1,6 +1,7 @@
 package com.storlead.trade.entity;
 
 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.storlead.framework.mybatis.entity.TenantBaseField;
@@ -59,4 +60,11 @@ public class CustomerAnalysisResultEntity extends TenantBaseField {
     private Date nextFollowUpAt;
     @ApiModelProperty(value = "分析时间")
     private Date analysisTime;
+
+    @ApiModelProperty(value = "是否是保护客户(0否,1是)")
+    @TableField("has_protect")
+    private Integer hasProtect;
+    @ApiModelProperty(value = "是否是暂停客户(0否,1是)")
+    @TableField("has_suspend")
+    private Integer hasSuspend;
 }

+ 11 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/mapper/CustomerBaseEntityMapper.java

@@ -1,9 +1,20 @@
 package com.storlead.trade.mapper;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.storlead.trade.dto.MarketingCampaignCustomerDTO;
 import com.storlead.trade.entity.CustomerBaseEntity;
 import com.storlead.framework.mybatis.mapper.MyBaseMapper;
+import com.storlead.trade.vo.MarketingCampaignCustomerVO;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 
 @Mapper
 public interface CustomerBaseEntityMapper extends MyBaseMapper<CustomerBaseEntity> {
+
+    /**
+     * 查询营销活动关联的客户列表
+     */
+    IPage<MarketingCampaignCustomerVO> selectCampaignCustomers(Page<MarketingCampaignCustomerVO> page,
+                                                                @Param("dto") MarketingCampaignCustomerDTO dto);
 }

+ 8 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/CustomerBaseEntityService.java

@@ -1,7 +1,15 @@
 package com.storlead.trade.service;
 
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.storlead.trade.dto.MarketingCampaignCustomerDTO;
 import com.storlead.trade.entity.CustomerBaseEntity;
 import com.storlead.framework.mybatis.service.MyBaseService;
+import com.storlead.trade.vo.MarketingCampaignCustomerVO;
 
 public interface CustomerBaseEntityService extends MyBaseService<CustomerBaseEntity> {
+
+    /**
+     * 查询营销活动关联的客户列表
+     */
+    IPage<MarketingCampaignCustomerVO> getCampaignCustomers(MarketingCampaignCustomerDTO dto);
 }

+ 15 - 1
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/CustomerBaseEntityServiceImpl.java

@@ -1,14 +1,28 @@
 package com.storlead.trade.service.impl;
 
-
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.storlead.trade.dto.MarketingCampaignCustomerDTO;
 import com.storlead.trade.entity.CustomerBaseEntity;
 import com.storlead.trade.mapper.CustomerBaseEntityMapper;
 import com.storlead.trade.service.CustomerBaseEntityService;
+import com.storlead.trade.vo.MarketingCampaignCustomerVO;
 import com.storlead.framework.mybatis.service.impl.MyBaseServiceImpl;
 import org.springframework.stereotype.Service;
 
+import javax.annotation.Resource;
+
 @Service
 public class CustomerBaseEntityServiceImpl
         extends MyBaseServiceImpl<CustomerBaseEntityMapper, CustomerBaseEntity>
         implements CustomerBaseEntityService {
+
+    @Resource
+    private CustomerBaseEntityMapper customerBaseEntityMapper;
+
+    @Override
+    public IPage<MarketingCampaignCustomerVO> getCampaignCustomers(MarketingCampaignCustomerDTO dto) {
+        Page<MarketingCampaignCustomerVO> page = new Page<>(dto.getPageIndex(), dto.getPageSize());
+        return customerBaseEntityMapper.selectCampaignCustomers(page, dto);
+    }
 }

+ 44 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/vo/MarketingCampaignCustomerVO.java

@@ -0,0 +1,44 @@
+package com.storlead.trade.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 营销活动关联客户信息 VO
+ */
+@Data
+@ApiModel(value = "MarketingCampaignCustomerVO", description = "营销活动关联客户信息")
+public class MarketingCampaignCustomerVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "客户ID")
+    private Long customerId;
+
+    @ApiModelProperty(value = "客户名称")
+    private String customerName;
+
+    @ApiModelProperty(value = "客户邮箱")
+    private String email;
+
+    @ApiModelProperty(value = "保护状态:0未保护,1已保护")
+    private Integer hasProtect;
+
+    @ApiModelProperty(value = "暂停状态:0未暂停,1已暂停")
+    private Integer hasSuspend;
+
+    @ApiModelProperty(value = "客户等级")
+    private String customerLevel;
+
+    @ApiModelProperty(value = "客户状态标签:全部/高价值/已保护/营销暂停")
+    private String statusTag;
+
+    @ApiModelProperty(value = "邮件打开时间")
+    private String openTime;
+
+    @ApiModelProperty(value = "点击次数")
+    private Integer clickCount;
+}

+ 37 - 0
java/storlead-sasa/storlead-trade/src/main/resources/mapper/CustomerBaseEntityMapper.xml

@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.storlead.trade.mapper.CustomerBaseEntityMapper">
+
+    <!-- 查询营销活动关联的客户列表 -->
+    <select id="selectCampaignCustomers" resultType="com.storlead.trade.vo.MarketingCampaignCustomerVO">
+        SELECT
+            c.id AS customerId,
+            c.customer_name AS customerName,
+            l.email AS email,
+            COALESCE(car.has_protect, 0) AS hasProtect,
+            COALESCE(car.has_suspend, 0) AS hasSuspend,
+            car.customer_level AS customerLevel,
+            '' AS openTime,
+            0 AS clickCount
+        FROM customer_base cb
+        LEFT JOIN customer c ON cb.customer_analysis_id = c.id
+        LEFT JOIN liaison l ON c.id = l.customer_id AND l.has_primary_lialison = 1
+        LEFT JOIN customer_analysis_result car ON c.id = car.customer_id
+        WHERE cb.marketing_campaign_id = #{dto.marketingCampaignId}
+          AND cb.is_delete = 0
+          AND c.is_delete = 0
+        <choose>
+            <when test="dto.filterStatus == 'protected'">
+                AND car.has_protect = 1
+            </when>
+            <when test="dto.filterStatus == 'suspended'">
+                AND car.has_suspend = 1
+            </when>
+            <when test="dto.filterStatus == 'highValue'">
+                AND car.customer_level IN ('A', 'B', '高价值', 'high')
+            </when>
+        </choose>
+        ORDER BY cb.create_time DESC
+    </select>
+
+</mapper>

+ 3 - 0
java/storlead-system/storlead-system-biz/src/main/java/com/storlead/system/service/impl/SysDictDetailServiceImpl.java

@@ -1,7 +1,9 @@
 package com.storlead.system.service.impl;
 
 import cn.hutool.core.util.StrUtil;
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.storlead.framework.common.constant.DSConstants;
 import com.storlead.system.pojo.dto.DictExtQueryParam;
 import com.storlead.system.pojo.entity.SysDictDetailEntity;
 import com.storlead.system.mapper.SysDictDetailMapper;
@@ -25,6 +27,7 @@ import java.util.stream.Collectors;
  * @since 2022-09-09
  */
 @Service
+@DS(DSConstants.DATASOURCE_MASTER)
 public class SysDictDetailServiceImpl extends ServiceImpl<SysDictDetailMapper, SysDictDetailEntity> implements SysDictDetailService {
 
     @Resource

+ 3 - 0
java/storlead-system/storlead-system-biz/src/main/java/com/storlead/system/service/impl/SysDictServiceImpl.java

@@ -1,7 +1,9 @@
 package com.storlead.system.service.impl;
 
 import cn.hutool.core.util.StrUtil;
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.storlead.framework.common.constant.DSConstants;
 import com.storlead.framework.common.result.Result;
 import com.storlead.system.pojo.dto.SysDictDTO;
 import com.storlead.system.pojo.entity.SysDictDetailEntity;
@@ -30,6 +32,7 @@ import java.util.stream.Collectors;
  * @since 2022-09-09
  */
 @Service
+@DS(DSConstants.DATASOURCE_MASTER)
 public class SysDictServiceImpl extends ServiceImpl<SysDictMapper, SysDictEntity> implements SysDictService {
 
     @Resource

+ 3 - 0
java/storlead-user/storlead-user-biz/src/main/java/com/storlead/user/service/impl/UserServiceImpl.java

@@ -6,12 +6,14 @@ import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.storlead.framework.auth.vo.LoginUser;
+import com.storlead.framework.common.constant.DSConstants;
 import com.storlead.framework.common.result.Result;
 import com.storlead.framework.common.util.RsaUtils;
 import com.storlead.framework.mybatis.service.impl.MyBaseServiceImpl;
 import com.storlead.framework.common.ecode.PasswordEncoder;
 import com.storlead.framework.common.thread.ThreadPoolUtil;
 import com.storlead.framework.util.LoginUserUtil;
+import com.baomidou.dynamic.datasource.annotation.DS;
 import com.storlead.user.mapper.UserMapper;
 import com.storlead.user.model.UserInfo;
 import com.storlead.user.model.UserQueryModel;
@@ -46,6 +48,7 @@ import static cn.hutool.core.date.DateTime.now;
 
 @Slf4j
 @Service
+@DS(DSConstants.DATASOURCE_MASTER)
 public class UserServiceImpl extends MyBaseServiceImpl<UserMapper, UserEntity> implements IUserService {
 
 	@Resource