|
@@ -4,20 +4,32 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import com.storlead.framework.common.result.Result;
|
|
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.dto.MarketingCampaignSopDetailDTO;
|
|
|
|
|
+import com.storlead.trade.vo.MarketingCampaignCustomerVO;
|
|
|
|
|
+import com.storlead.trade.entity.CustomerBaseEntity;
|
|
|
import com.storlead.trade.entity.MarketingCampaignSopDetailEntity;
|
|
import com.storlead.trade.entity.MarketingCampaignSopDetailEntity;
|
|
|
|
|
+import com.storlead.trade.service.CustomerBaseEntityService;
|
|
|
import com.storlead.trade.service.MarketingCampaignSopDetailService;
|
|
import com.storlead.trade.service.MarketingCampaignSopDetailService;
|
|
|
import io.swagger.annotations.Api;
|
|
import io.swagger.annotations.Api;
|
|
|
import io.swagger.annotations.ApiOperation;
|
|
import io.swagger.annotations.ApiOperation;
|
|
|
import org.springframework.beans.BeanUtils;
|
|
import org.springframework.beans.BeanUtils;
|
|
|
|
|
+import org.springframework.jdbc.core.JdbcTemplate;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
import org.springframework.util.CollectionUtils;
|
|
import org.springframework.util.CollectionUtils;
|
|
|
import org.springframework.util.ObjectUtils;
|
|
import org.springframework.util.ObjectUtils;
|
|
|
import org.springframework.web.bind.annotation.*;
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
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.ArrayList;
|
|
|
|
|
+import java.util.HashSet;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
|
|
+import java.util.Set;
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* 营销活动SOP明细 Controller
|
|
* 营销活动SOP明细 Controller
|
|
@@ -30,6 +42,20 @@ public class MarketingCampaignSopDetailController {
|
|
|
@Resource
|
|
@Resource
|
|
|
private MarketingCampaignSopDetailService marketingCampaignSopDetailService;
|
|
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查询详情
|
|
* 根据id查询详情
|
|
|
*/
|
|
*/
|
|
@@ -96,6 +122,227 @@ public class MarketingCampaignSopDetailController {
|
|
|
return Result.result(entity);
|
|
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);
|
|
List<MarketingCampaignSopDetailEntity> list = marketingCampaignSopDetailService.list(wrapper);
|
|
|
return Result.result(list);
|
|
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);
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|