|
|
@@ -5,6 +5,7 @@ import com.alibaba.fastjson.JSON;
|
|
|
import com.alibaba.fastjson.TypeReference;
|
|
|
import com.alibaba.fastjson.parser.Feature;
|
|
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
+import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
|
|
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
|
|
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
|
|
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
@@ -38,8 +39,12 @@ import org.springframework.util.CollectionUtils;
|
|
|
import org.springframework.util.ObjectUtils;
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
|
+import java.math.BigDecimal;
|
|
|
+import java.math.RoundingMode;
|
|
|
+import java.time.LocalDate;
|
|
|
import java.time.LocalDateTime;
|
|
|
import java.time.ZoneId;
|
|
|
+import java.time.format.DateTimeFormatter;
|
|
|
import java.util.*;
|
|
|
import java.util.regex.Matcher;
|
|
|
import java.util.regex.Pattern;
|
|
|
@@ -94,10 +99,13 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
|
|
|
@Resource
|
|
|
private LiaisonTradeService liaisonTradeService;
|
|
|
|
|
|
- @Autowired
|
|
|
+ @Resource
|
|
|
@Lazy
|
|
|
private RedissonClient redissonClient;
|
|
|
|
|
|
+ @Resource
|
|
|
+ private CustomerAnalysisResultEntityService customerAnalysisResultService;
|
|
|
+
|
|
|
|
|
|
@Override
|
|
|
public Result<Object> getList(MarketEmailsDTO dto) {
|
|
|
@@ -530,6 +538,44 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
|
|
|
return rows;
|
|
|
}
|
|
|
|
|
|
+ @Override
|
|
|
+ public void updateEmailContent(MarketEmailsEntity marketEmailsEntity) {
|
|
|
+ //通过客户id获取客户分析结果
|
|
|
+ CustomerAnalysisResultEntity customerAnalysisResult = new CustomerAnalysisResultEntity();
|
|
|
+ LambdaQueryWrapper<CustomerAnalysisResultEntity> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
+ queryWrapper.eq(CustomerAnalysisResultEntity::getCustomerId, marketEmailsEntity.getCustomerId());
|
|
|
+ queryWrapper.last("limit 1");
|
|
|
+ customerAnalysisResult = customerAnalysisResultService.getOne(queryWrapper);
|
|
|
+ String reasonSummary = "";
|
|
|
+ if (customerAnalysisResult != null){
|
|
|
+ reasonSummary = customerAnalysisResult.getReasonSummary();
|
|
|
+ }
|
|
|
+ Map sendMap = new HashMap();
|
|
|
+ sendMap.put("email", marketEmailsEntity.getContent());
|
|
|
+ sendMap.put("reason_summary", reasonSummary);
|
|
|
+
|
|
|
+ ChatDTO chatDTO = new ChatDTO();
|
|
|
+ chatDTO.setInputs(sendMap);
|
|
|
+// chatDTO.setQuery("send_time");
|
|
|
+ chatDTO.setResponseMode("blocking");
|
|
|
+ chatDTO.setAppId("app-CVPtLUPO0zd2SeGPvZv3Q4Ev");
|
|
|
+ chatDTO.setUser("1");
|
|
|
+
|
|
|
+ // 调取 AI 获取新的content
|
|
|
+ Result<Object> r = workflowsService.AiWorkflows(chatDTO);
|
|
|
+ if (r == null) {
|
|
|
+ log.warn("无内容生成");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 从结构化结果里取出 AI 真正输出的 text 字段(多个结束节点拼接)。
|
|
|
+ // 不再做 inner.toString() 兜底——整串 Result 里有 files=[]/inputs=[] 等空数组
|
|
|
+ // 会被误当成 AI 输出的 JSON 数组(如 [models]),污染解析。
|
|
|
+ String text = extractCleanText(r.getResult());
|
|
|
+ marketEmailsEntity.setContent(text);
|
|
|
+
|
|
|
+ this.updateById(marketEmailsEntity);
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* 回复邮件的客户列表(按 marketingCampaignId 过滤 + hasReply=1 + recipient 聚合)
|
|
|
* 不分页:全部/已保护/营销暂停 三个 Tab 由前端在结果中过滤计数。
|
|
|
@@ -931,6 +977,207 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
|
|
|
return Result.result(voPage);
|
|
|
}
|
|
|
|
|
|
+ @Override
|
|
|
+ public Result<Object> getTodayStats() {
|
|
|
+ // 统计窗口:今天 00:00:00 ~ 明天 00:00:00(左闭右开),按服务器时区
|
|
|
+ DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
|
|
|
+ LocalDateTime startDay = LocalDate.now().atStartOfDay();
|
|
|
+ String start = startDay.format(formatter);
|
|
|
+ String end = startDay.plusDays(1).format(formatter);
|
|
|
+
|
|
|
+ // 单条 SQL 条件聚合(start/end 由服务端按当天生成,非用户输入,无注入风险):
|
|
|
+ // 今日发送:expect_send_time 在今天且已发送成功(status=1,已取消/待发送/发送中不算)
|
|
|
+ // 今日打开:first_open_time 在今天
|
|
|
+ // 今日回复:first_reply_time 在今天
|
|
|
+ // 今日拒收:expect_send_time 在今天且已拒收(has_refuse=1)
|
|
|
+ QueryWrapper<MarketEmailsEntity> qw = new QueryWrapper<>();
|
|
|
+ qw.eq("is_delete", CommonConstant.DEL_FLAG_0);
|
|
|
+ qw.select(
|
|
|
+ "COUNT(CASE WHEN status = 1 AND expect_send_time >= '" + start + "' AND expect_send_time < '" + end + "' THEN 1 END) AS send_count",
|
|
|
+ "COUNT(CASE WHEN first_open_time >= '" + start + "' AND first_open_time < '" + end + "' THEN 1 END) AS open_count",
|
|
|
+ "COUNT(CASE WHEN first_reply_time >= '" + start + "' AND first_reply_time < '" + end + "' THEN 1 END) AS reply_count",
|
|
|
+ "COUNT(CASE WHEN IFNULL(has_refuse, 0) = 1 AND expect_send_time >= '" + start + "' AND expect_send_time < '" + end + "' THEN 1 END) AS refuse_count");
|
|
|
+ List<Map<String, Object>> rows = this.listMaps(qw);
|
|
|
+
|
|
|
+ MarketEmailsTodayStatsVO vo = new MarketEmailsTodayStatsVO();
|
|
|
+ if (!CollectionUtils.isEmpty(rows) && rows.get(0) != null) {
|
|
|
+ Map<String, Object> row = rows.get(0);
|
|
|
+ vo.setTodaySendCount(toInteger(row.get("send_count")));
|
|
|
+ vo.setTodayOpenCount(toInteger(row.get("open_count")));
|
|
|
+ vo.setTodayReplyCount(toInteger(row.get("reply_count")));
|
|
|
+ vo.setTodayRefuseCount(toInteger(row.get("refuse_count")));
|
|
|
+ }
|
|
|
+ return Result.result(vo);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Result<Object> getLast7DaysSendTrend() {
|
|
|
+ LocalDate today = LocalDate.now();
|
|
|
+ // 窗口:今天往前推 6 天 的 00:00 ~ 明天 00:00,共 7 天(含今天)
|
|
|
+ LocalDateTime startDay = today.minusDays(6).atStartOfDay();
|
|
|
+ LocalDateTime endDay = today.plusDays(1).atStartOfDay();
|
|
|
+ Date startDate = Date.from(startDay.atZone(ZoneId.systemDefault()).toInstant());
|
|
|
+ Date endDate = Date.from(endDay.atZone(ZoneId.systemDefault()).toInstant());
|
|
|
+
|
|
|
+ // 按天聚合;只统计发送成功(status=1),与「今日发送」口径一致
|
|
|
+ QueryWrapper<MarketEmailsEntity> qw = new QueryWrapper<>();
|
|
|
+ qw.select("DATE(expect_send_time) AS day", "COUNT(*) AS cnt");
|
|
|
+ qw.eq("is_delete", CommonConstant.DEL_FLAG_0);
|
|
|
+ qw.eq("status", 1);
|
|
|
+ qw.ge("expect_send_time", startDate);
|
|
|
+ qw.lt("expect_send_time", endDate);
|
|
|
+ qw.groupBy("DATE(expect_send_time)");
|
|
|
+ List<Map<String, Object>> rows = this.listMaps(qw);
|
|
|
+
|
|
|
+ Map<String, Integer> countMap = new HashMap<>();
|
|
|
+ if (!CollectionUtils.isEmpty(rows)) {
|
|
|
+ for (Map<String, Object> row : rows) {
|
|
|
+ if (row == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ String day = toDayKey(row.get("day"));
|
|
|
+ if (day != null) {
|
|
|
+ countMap.put(day, toInteger(row.get("cnt")));
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 补齐 7 天:没有数据的天补 0,按日期升序(最旧 → 今天)
|
|
|
+ List<MarketEmailsTrendVO> list = new ArrayList<>();
|
|
|
+ for (int i = 6; i >= 0; i--) {
|
|
|
+ String day = today.minusDays(i).toString();
|
|
|
+ MarketEmailsTrendVO vo = new MarketEmailsTrendVO();
|
|
|
+ vo.setDate(day);
|
|
|
+ vo.setSendCount(countMap.getOrDefault(day, 0));
|
|
|
+ list.add(vo);
|
|
|
+ }
|
|
|
+ return Result.result(list);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Result<Map<Long, Object>> getAccountStats(List<Long> outboxIds) {
|
|
|
+ Map<Long, Object> result = new LinkedHashMap<>();
|
|
|
+ if (CollectionUtils.isEmpty(outboxIds)) {
|
|
|
+ return Result.result(result);
|
|
|
+ }
|
|
|
+ List<Long> ids = outboxIds.stream().filter(Objects::nonNull).distinct().collect(Collectors.toList());
|
|
|
+ if (ids.isEmpty()) {
|
|
|
+ return Result.result(result);
|
|
|
+ }
|
|
|
+ for (Long id : ids) {
|
|
|
+ result.put(id, emptyAccountStats(id));
|
|
|
+ }
|
|
|
+
|
|
|
+ QueryWrapper<MarketEmailsEntity> qw = new QueryWrapper<>();
|
|
|
+ qw.select(
|
|
|
+ "outbox_id",
|
|
|
+ "COUNT(*) AS send_count",
|
|
|
+ "COUNT(CASE WHEN IFNULL(has_reply, 0) = 1 THEN 1 END) AS reply_count",
|
|
|
+ "MAX(expect_send_time) AS last_send_time");
|
|
|
+ qw.eq("is_delete", CommonConstant.DEL_FLAG_0);
|
|
|
+ qw.eq("status", 1);
|
|
|
+ qw.in("outbox_id", ids);
|
|
|
+ qw.groupBy("outbox_id");
|
|
|
+ List<Map<String, Object>> rows = this.listMaps(qw);
|
|
|
+ if (CollectionUtils.isEmpty(rows)) {
|
|
|
+ return Result.result(result);
|
|
|
+ }
|
|
|
+ for (Map<String, Object> row : rows) {
|
|
|
+ if (row == null) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ Long id = toLongOrNull(row.get("outbox_id"));
|
|
|
+ if (id == null || !result.containsKey(id)) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ MarketEmailsAccountStatsVO vo = new MarketEmailsAccountStatsVO();
|
|
|
+ int sendCount = toInteger(row.get("send_count"));
|
|
|
+ int replyCount = toInteger(row.get("reply_count"));
|
|
|
+ vo.setOutboxId(id);
|
|
|
+ vo.setSendCount(sendCount);
|
|
|
+ vo.setReplyCount(replyCount);
|
|
|
+ vo.setReplyRate(toReplyRate(sendCount, replyCount));
|
|
|
+ vo.setLastSendTime(toDate(row.get("last_send_time")));
|
|
|
+ result.put(id, vo);
|
|
|
+ }
|
|
|
+ return Result.result(result);
|
|
|
+ }
|
|
|
+
|
|
|
+ private MarketEmailsAccountStatsVO emptyAccountStats(Long outboxId) {
|
|
|
+ MarketEmailsAccountStatsVO vo = new MarketEmailsAccountStatsVO();
|
|
|
+ vo.setOutboxId(outboxId);
|
|
|
+ vo.setSendCount(0);
|
|
|
+ vo.setReplyCount(0);
|
|
|
+ vo.setReplyRate(BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP));
|
|
|
+ return vo;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 分组键转 yyyy-MM-dd:JDBC 可能返回 java.sql.Date / java.sql.Timestamp / 字符串。
|
|
|
+ * java.sql.Date#toInstant 会抛 UnsupportedOperationException,必须优先判断。
|
|
|
+ */
|
|
|
+ private String toDayKey(Object value) {
|
|
|
+ if (value == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (value instanceof java.sql.Date) {
|
|
|
+ return ((java.sql.Date) value).toLocalDate().toString();
|
|
|
+ }
|
|
|
+ if (value instanceof Date) {
|
|
|
+ return ((Date) value).toInstant().atZone(ZoneId.systemDefault()).toLocalDate().toString();
|
|
|
+ }
|
|
|
+ String text = value.toString().trim();
|
|
|
+ return text.length() > 10 ? text.substring(0, 10) : text;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 聚合结果转 Integer:MySQL 的 COUNT/SUM 经 JDBC 可能是 Long / BigInteger / BigDecimal,null 记 0。
|
|
|
+ */
|
|
|
+ private Integer toInteger(Object value) {
|
|
|
+ if (value == null) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ if (value instanceof Number) {
|
|
|
+ return ((Number) value).intValue();
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return Integer.valueOf(value.toString().trim());
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 聚合字段转 Long,空值保持 null。 */
|
|
|
+ private Long toLongOrNull(Object value) {
|
|
|
+ if (value == null) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ if (value instanceof Number) {
|
|
|
+ return ((Number) value).longValue();
|
|
|
+ }
|
|
|
+ try {
|
|
|
+ return Long.valueOf(value.toString().trim());
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private Date toDate(Object value) {
|
|
|
+ if (value instanceof Date) {
|
|
|
+ return (Date) value;
|
|
|
+ }
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ private BigDecimal toReplyRate(int sendCount, int replyCount) {
|
|
|
+ if (sendCount <= 0) {
|
|
|
+ return BigDecimal.ZERO.setScale(2, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+ return BigDecimal.valueOf(replyCount)
|
|
|
+ .multiply(BigDecimal.valueOf(100))
|
|
|
+ .divide(BigDecimal.valueOf(sendCount), 2, RoundingMode.HALF_UP);
|
|
|
+ }
|
|
|
+
|
|
|
@Override
|
|
|
public void updateCustomerEmailStatus(MarketEmailsCustomerVO marketEmailsCustomerVO) {
|
|
|
LambdaQueryWrapper<MarketEmailsEntity> queryWrapper = new LambdaQueryWrapper<>();
|
|
|
@@ -1195,9 +1442,11 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
|
|
|
queryStopWrapper.eq(MarketEmailsEntity::getHasReply, 1);
|
|
|
queryStopWrapper.eq(MarketEmailsEntity::getHasReader, 1);
|
|
|
List<MarketEmailsEntity> marketEmailsStopList = this.list(queryStopWrapper);
|
|
|
- List<Long> customerIdList = new ArrayList<>();
|
|
|
+ List<Long> customerIdList;
|
|
|
if (!CollectionUtils.isEmpty(marketEmailsStopList)){
|
|
|
customerIdList = marketEmailsStopList.stream().map(MarketEmailsEntity::getCustomerId).collect(Collectors.toList());
|
|
|
+ } else {
|
|
|
+ customerIdList = new ArrayList<>();
|
|
|
}
|
|
|
|
|
|
|
|
|
@@ -1211,16 +1460,34 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
|
|
|
queryWrapper.between(MarketEmailsEntity::getExpectSendTime, start, end);
|
|
|
|
|
|
queryWrapper.eq(MarketEmailsEntity::getStatus, -1);
|
|
|
- queryWrapper.notIn(!CollectionUtils.isEmpty(customerIdList), MarketEmailsEntity::getCustomerId, customerIdList);
|
|
|
+// queryWrapper.notIn(!CollectionUtils.isEmpty(customerIdList), MarketEmailsEntity::getCustomerId, customerIdList);
|
|
|
queryWrapper.eq(MarketEmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
|
|
|
- queryWrapper.eq(MarketEmailsEntity::getHasProtect,0);
|
|
|
- queryWrapper.eq(MarketEmailsEntity::getHasSuspend,0);
|
|
|
+// queryWrapper.eq(MarketEmailsEntity::getHasProtect,0);
|
|
|
+// queryWrapper.eq(MarketEmailsEntity::getHasSuspend,0);
|
|
|
queryWrapper.in(MarketEmailsEntity::getMarketingCampaignId, marketingCampaignIdList);
|
|
|
- List<MarketEmailsEntity> marketEmailList = this.list(queryWrapper);
|
|
|
+ List<MarketEmailsEntity> marketEmailFirstList = this.list(queryWrapper);
|
|
|
|
|
|
+ if (CollectionUtils.isEmpty(marketEmailFirstList)){
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ List<MarketEmailsEntity> marketEmailList = marketEmailFirstList;
|
|
|
+ //获取客户id在customerIdList或者已开启保护或者已暂停的marketEmailList
|
|
|
+ List<MarketEmailsEntity> marketEmailNotList = marketEmailFirstList.stream()
|
|
|
+ .filter(marketEmail -> customerIdList.contains(marketEmail.getCustomerId()) ||
|
|
|
+ marketEmail.getHasProtect() == 1 ||
|
|
|
+ marketEmail.getHasSuspend() == 1)
|
|
|
+ .collect(Collectors.toList());
|
|
|
+ if (!CollectionUtils.isEmpty(marketEmailNotList)) {
|
|
|
+ for (MarketEmailsEntity marketEmail : marketEmailNotList) {
|
|
|
+ marketEmail.setStatus(-3);
|
|
|
+ }
|
|
|
+ this.updateBatchById(marketEmailNotList);
|
|
|
+ marketEmailList.removeAll(marketEmailNotList);
|
|
|
+ }
|
|
|
if (CollectionUtils.isEmpty(marketEmailList)){
|
|
|
return;
|
|
|
}
|
|
|
+
|
|
|
log.info("queryWrapper: {}", queryWrapper);
|
|
|
log.info("marketEmailList: {}", marketEmailList);
|
|
|
for (MarketEmailsEntity marketEmail : marketEmailList){
|