Explorar el Código

Merge remote-tracking branch 'origin/master'

zhujing hace 4 semanas
padre
commit
fa4481c177

+ 3 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/dto/MktSendMailRemoteDTO.java

@@ -60,4 +60,7 @@ public class MktSendMailRemoteDTO {
 
     @ApiModelProperty("回复原信 id")
     private Long replyMsgId;
+
+    @ApiModelProperty("是否已打开(像素)")
+    private Integer opened;
 }

+ 22 - 16
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/CustomerTradeServiceImpl.java

@@ -25,8 +25,11 @@ import org.springframework.util.ObjectUtils;
 import org.springframework.util.StringUtils;
 
 import java.util.ArrayList;
+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;
 
 @Service
@@ -107,8 +110,9 @@ public class CustomerTradeServiceImpl
             wrapper.like(CustomerEntity::getCustomerName, dto.getCustomerName());
         }
 
-        // 模糊筛选:blurry(覆盖公司名称 + 联系人姓名)
-        // 思路:分别查 customer_company / liaison 两张表取 customerId,做并集后再 IN customer 表
+        // 模糊筛选:blurry(覆盖公司名称 + 联系人姓名 + 客户名称)
+        // 思路:查 customer_company / liaison 取 customerId 并集,与「customerName LIKE」组成 OR 组,
+        //       整个 OR 组与其他条件走 AND:SQL = <其他> AND (customerName LIKE ? OR customer.id IN (B∪C))
         if (!StringUtils.isEmpty(dto.getBlurry())) {
             String blurry = dto.getBlurry().trim();
 
@@ -116,28 +120,30 @@ public class CustomerTradeServiceImpl
                     new LambdaQueryWrapper<CustomerCompanyEntity>()
                             .eq(CustomerCompanyEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
                             .like(CustomerCompanyEntity::getName, blurry)
-            ).stream().map(CustomerCompanyEntity::getCustomerId).collect(Collectors.toList());
+            ).stream().map(CustomerCompanyEntity::getCustomerId).filter(Objects::nonNull).collect(Collectors.toList());
 
             List<Long> liaisonCustomerIds = liaisonTradeService.list(
                     new LambdaQueryWrapper<LiaisonEntity>()
                             .eq(LiaisonEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
                             .eq(LiaisonEntity::getHasPrimaryLialison, 1)
                             .like(LiaisonEntity::getName, blurry)
-            ).stream().map(LiaisonEntity::getCustomerId).collect(Collectors.toList());
+            ).stream().map(LiaisonEntity::getCustomerId).filter(Objects::nonNull).collect(Collectors.toList());
 
-            // 合并两个集合,去重
-            List<Long> blurCustomerIds = new ArrayList<>();
-            blurCustomerIds.addAll(companyCustomerIds);
-            blurCustomerIds.addAll(liaisonCustomerIds);
-            blurCustomerIds = blurCustomerIds.stream().distinct().collect(Collectors.toList());
+            // 合并两个集合去重
+            Set<Long> blurCustomerIdSet = new HashSet<>();
+            blurCustomerIdSet.addAll(companyCustomerIds);
+            blurCustomerIdSet.addAll(liaisonCustomerIds);
 
-            if (CollectionUtils.isEmpty(blurCustomerIds)) {
-                // 无匹配公司也无匹配联系人,直接返回空
-                Page<CustomerVO> emptyPage = new Page<>(dto.getPageIndex(), dto.getPageSize());
-                emptyPage.setRecords(new ArrayList<>());
-                return emptyPage;
-            }
-            wrapper.in(CustomerEntity::getId, blurCustomerIds);
+            // OR 组:(customerName LIKE blurry) OR (id IN 并集)
+            // 当 blurCustomerIdSet 为空时跳过 .in(),仅保留 .like() 作为 OR 组里唯一条件
+            // 注意:本项目 MP 版本 < 3.5.0,and() 只有 Function 重载,lambda 必须 return
+            wrapper.and(w -> {
+                w.like(CustomerEntity::getCustomerName, blurry);
+                if (!blurCustomerIdSet.isEmpty()) {
+                    w.or().in(CustomerEntity::getId, blurCustomerIdSet);
+                }
+                return w;
+            });
         }
 
         // 精确筛选:联系人手机/邮箱

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

@@ -950,6 +950,15 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
 
     @Override
     public void send() {
+        //获取需要发送的营销活动
+        LambdaQueryWrapper<MarketingCampaignEntity> marketingCampaignQueryWrapper = new LambdaQueryWrapper<>();
+        marketingCampaignQueryWrapper.eq(MarketingCampaignEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
+        marketingCampaignQueryWrapper.eq(MarketingCampaignEntity::getStatus, 1);
+        List<MarketingCampaignEntity> marketingCampaignList = marketingCampaignService.list(marketingCampaignQueryWrapper);
+        if (CollectionUtils.isEmpty(marketingCampaignList)){
+            return;
+        }
+        List<Long> marketingCampaignIdList = marketingCampaignList.stream().map(MarketingCampaignEntity::getId).collect(Collectors.toList());
         LambdaQueryWrapper<MarketEmailsEntity> queryWrapper = new LambdaQueryWrapper<>();
 
 //        queryWrapper.eq(MarketEmailsEntity::getExpectSendTime, new Date());
@@ -961,6 +970,9 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
 
         queryWrapper.eq(MarketEmailsEntity::getStatus, -1);
         queryWrapper.eq(MarketEmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
+        queryWrapper.eq(MarketEmailsEntity::getHasProtect,0);
+        queryWrapper.eq(MarketEmailsEntity::getHasSuspend,0);
+        queryWrapper.in(MarketEmailsEntity::getMarketingCampaignId, marketingCampaignIdList);
         List<MarketEmailsEntity> marketEmailList = this.list(queryWrapper);
 
         if (CollectionUtils.isEmpty(marketEmailList)){
@@ -975,14 +987,15 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
             dto.setRecipientls(recipientls);
             dto.setSubject(marketEmail.getSubject());
             dto.setContent(marketEmail.getContent());
-            dto.setDelaySendTime(marketEmail.getExpectSendTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
+            dto.setOpened(1);
+//            dto.setDelaySendTime(marketEmail.getExpectSendTime().toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime());
             Result result = mktMailRemoteClient.send(dto);
             if (result.isSuccess()) {
                 // 远程返回 result 是 LinkedHashMap(Jackson 默认反序列化),按 Map 取字段
                 Map<String, Object> sentMail = (Map<String, Object>) result.getResult();
                 //将邮件id写入MarketEmailsEntity
                 marketEmail.setEmailId(((Number) sentMail.get("id")).longValue());
-                marketEmail.setStatus(1);
+                marketEmail.setStatus((Integer) sentMail.get("status"));
                 log.info("id为:{}", sentMail.get("id"));
                 log.info("status为:{}", sentMail.get("status"));
                 log.info("邮件发送成功,邮件ID:{}", marketEmail.getId());

+ 34 - 32
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MarketingCampaignServiceImpl.java

@@ -400,8 +400,10 @@ public class MarketingCampaignServiceImpl
     @Override
     public void syncStartEndStatus() {
         LocalDate today = LocalDate.now(ZoneId.of("Asia/Shanghai"));
+        // 查询所有未开始的活动
         List<MarketingCampaignEntity> all = this.lambdaQuery()
                 .eq(MarketingCampaignEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
+                .eq(MarketingCampaignEntity::getStatus, 0)
                 .list();
         if (CollectionUtils.isEmpty(all)) {
             log.info("营销活动开始/结束状态同步:无未删除活动,跳过");
@@ -429,38 +431,38 @@ public class MarketingCampaignServiceImpl
             c.setStatus(expected);
             changed.add(c);
         }
-        // 每种目标状态执行一次批量 UPDATE
-        for (Map.Entry<Integer, List<Long>> entry : idsByStatus.entrySet()) {
-            List<Long> ids = entry.getValue();
-            if (CollectionUtils.isEmpty(ids)) {
-                continue;
-            }
-            this.lambdaUpdate()
-                    .in(MarketingCampaignEntity::getId, ids)
-                    .set(MarketingCampaignEntity::getStatus, entry.getKey())
-                    .update();
-        }
-        // 按目标状态拆分列表并打印:哪些改为开始、哪些改为结束
-        List<Long> started = changed.stream()
-                .filter(e -> STATUS_RUNNING == e.getStatus())
-                .map(MarketingCampaignEntity::getId)
-                .collect(Collectors.toList());
-        List<Long> ended = changed.stream()
-                .filter(e -> STATUS_COMPLETED == e.getStatus())
-                .map(MarketingCampaignEntity::getId)
-                .collect(Collectors.toList());
-        log.info("营销活动开始/结束状态同步完成:改为开始(运行中)={}, 改为结束(完成)={}", started, ended);
-        MarketEmailsCustomerVO startVO = new MarketEmailsCustomerVO();
-        startVO.setMarketingCampaignIdList(started);
-        //修改状态为待发送
-        startVO.setStatus(-1);
-        marketEmailsService.updateCustomerEmailStatus(startVO);
-
-        MarketEmailsCustomerVO endVO = new MarketEmailsCustomerVO();
-        endVO.setMarketingCampaignIdList(ended);
-        //修改状态为待发送
-        endVO.setStatus(-3);
-        marketEmailsService.updateCustomerEmailStatus(endVO);
+//        // 每种目标状态执行一次批量 UPDATE
+//        for (Map.Entry<Integer, List<Long>> entry : idsByStatus.entrySet()) {
+//            List<Long> ids = entry.getValue();
+//            if (CollectionUtils.isEmpty(ids)) {
+//                continue;
+//            }
+//            this.lambdaUpdate()
+//                    .in(MarketingCampaignEntity::getId, ids)
+//                    .set(MarketingCampaignEntity::getStatus, entry.getKey())
+//                    .update();
+//        }
+//        // 按目标状态拆分列表并打印:哪些改为开始、哪些改为结束
+//        List<Long> started = changed.stream()
+//                .filter(e -> STATUS_RUNNING == e.getStatus())
+//                .map(MarketingCampaignEntity::getId)
+//                .collect(Collectors.toList());
+//        List<Long> ended = changed.stream()
+//                .filter(e -> STATUS_COMPLETED == e.getStatus())
+//                .map(MarketingCampaignEntity::getId)
+//                .collect(Collectors.toList());
+//        log.info("营销活动开始/结束状态同步完成:改为开始(运行中)={}, 改为结束(完成)={}", started, ended);
+//        MarketEmailsCustomerVO startVO = new MarketEmailsCustomerVO();
+//        startVO.setMarketingCampaignIdList(started);
+//        //修改状态为待发送
+//        startVO.setStatus(-1);
+//        marketEmailsService.updateCustomerEmailStatus(startVO);
+//
+//        MarketEmailsCustomerVO endVO = new MarketEmailsCustomerVO();
+//        endVO.setMarketingCampaignIdList(ended);
+//        //修改状态为待发送
+//        endVO.setStatus(-3);
+//        marketEmailsService.updateCustomerEmailStatus(endVO);
     }
 
     /**

+ 2 - 1
ui/smarttrade-platform/src/api/marketing/index.js

@@ -14,7 +14,8 @@ export const setProtectCustomer = (data = {}) => post('/router/rest/marketEmails
 export const setSuspendCustomer = (data = {}) => post('/router/rest/marketEmails/suspend', data)
 export const getReplyCustomer = (data = {}) => get('/router/rest/marketEmails/reply-list', data)
 export const getInterventionCustomer = (data = {}) => get('/router/rest/marketEmails/intervention-list', data)
-export const getEmailList = (data = {}) => get('/router/rest/marketEmails/management-detail', data)
+export const getEmailList = (data = {}) => post('/router/rest/marketEmails/management-page', data)
+export const getEmailCount = (data = {}) => get('/router/rest/marketEmails/management-stats', data)
 export const cancelEmail = (data = {}) => post('/router/rest/marketEmails/cancel', data)
 export const setMarketingStatus = (data = {}) => post('/router/rest/marketing/status', data)
 export const getTradeCustomer = (data = {}) => post('/router/rest/customerTrade/list', data)

+ 50 - 14
ui/smarttrade-platform/src/components/ckeditor/index.vue

@@ -4,27 +4,54 @@
 	</div>
 </template>
 <script>
+const waitForCKEditor = (timeout = 10000) => new Promise((resolve, reject) => {
+	const startTime = Date.now()
+	const check = () => {
+		if (window.CKEDITOR && typeof window.CKEDITOR.replace === 'function') {
+			resolve(window.CKEDITOR)
+			return
+		}
+
+		if (Date.now() - startTime >= timeout) {
+			reject(new Error('CKEditor script did not load'))
+			return
+		}
+
+		setTimeout(check, 50)
+	}
+	check()
+})
+
 export default {
 	props: ["content", "placeholder", 'type', 'isDark'],//从父组件转递的内容
-	mounted: function () {
-		const self = this;
-		// 渲染编辑器
-		self.ckeditor = window.CKEDITOR.replace('content', self.editorConfig);//定义编辑器的高度
-		
-		// 设置初始内容
-		self.ckeditor.setData(self.content);
+	mounted: async function () {
+		try {
+			const CKEDITOR = await waitForCKEditor()
+			if (this.destroyed) return
 
-		// 监听内容变更事件
-		self.ckeditor.on("change", function () {
-			self.$emit("sendContnet", self.ckeditor.getData());
-		});
-		self.loading = false
+			this.ckeditor = CKEDITOR.replace(this.id, this.editorConfig)
+			this.ckeditor.once('instanceReady', () => {
+				if (this.destroyed) return
+				this.editorReady = true
+				this.syncContent()
+			})
+			this.ckeditor.on("change", () => {
+				if (this.ckeditor) {
+					this.$emit("sendContnet", this.ckeditor.getData())
+				}
+			})
+		} catch (error) {
+			console.error('CKEditor initialization failed:', error)
+		} finally {
+			this.loading = false
+		}
 	},
 	data: function () {
 		const self = this;
 		return {
 			id: parseInt(Math.random() * 10000).toString(),
 			ckeditor: null,
+			editorReady: false,
 			editorConfig: {
 				versionCheck: false,
 				dragDrop: false,
@@ -77,12 +104,21 @@ export default {
 	watch: {
 		// 监听prop的变化,更新ckeditor中的值
 		content: function () {
-			if (this.content !== this.ckeditor.getData()) {
-				this.ckeditor.setData(this.content);
+			this.syncContent()
+		}
+	},
+	methods: {
+		syncContent() {
+			if (!this.ckeditor || !this.editorReady) return
+
+			const content = this.content || ''
+			if (content !== this.ckeditor.getData()) {
+				this.ckeditor.setData(content)
 			}
 		}
 	},
 	beforeDestroy() {
+		this.destroyed = true
 		// 组件销毁前释放CKEditor资源
 		if (this.ckeditor) {
 			this.ckeditor.destroy();

+ 37 - 40
ui/smarttrade-platform/src/components/marketing/emailList.vue

@@ -11,37 +11,22 @@
       <div class="modal-body email-manage-body">
         <div class="email-tabs">
           <div
+            v-for="item in tabData"
             class="email-tab"
-            :class="{ active: activeFilter === 'pending' }"
-            @click="activeFilter = 'pending'"
+            :class="{ active: activeFilter === item.value }"
+            @click="changeTab(item.value)"
           >
-            <div class="tab-title">待发送</div>
-            <div class="tab-count">{{ statusCounts.pending }}</div>
-          </div>
-          <div
-            class="email-tab"
-            :class="{ active: activeFilter === 'sent' }"
-            @click="activeFilter = 'sent'"
-          >
-            <div class="tab-title">已发送</div>
-            <div class="tab-count">{{ statusCounts.sent }}</div>
-          </div>
-          <div
-            class="email-tab"
-            :class="{ active: activeFilter === 'failed' }"
-            @click="activeFilter = 'failed'"
-          >
-            <div class="tab-title">已取消</div>
-            <div class="tab-count">{{ statusCounts.failed }}</div>
+            <div class="tab-title">{{ item.title }}</div>
+            <div class="tab-count">{{ item.count }}</div>
           </div>
         </div>
 
         <div class="email-card-list" v-loading="loading">
           <div
-            v-for="item in filterEmailList"
+            v-for="item in emailList"
             :key="item.id"
             class="email-card"
-            v-if="filterEmailList.length > 0"
+            v-if="emailList.length > 0"
           >
             <div class="email-card-left">
               <div class="email-card-top">
@@ -50,7 +35,7 @@
                     {{ item.customerName }}
                     <span class="status-pill">{{ item.stageName }}</span>
                   </div>
-                  <div class="email-card-subtitle">{{ item.email }}</div>
+                  <div class="email-card-subtitle">{{ item.recipient }}</div>
                   <div class="email-card-subtitle">预计发送时间:{{ item.expectSendTime }}</div>
                 </div>
                 
@@ -79,9 +64,9 @@
 </template>
 
 <script setup>
-import { computed, ref } from 'vue'
+import { ref } from 'vue'
 import { useRouter } from 'vue-router'
-import { getEmailList, cancelEmail } from '@/api/marketing'
+import { getEmailList, getEmailCount, cancelEmail } from '@/api/marketing'
 import { ElMessageBox } from 'element-plus'
 
 const selectedCampaign = ref(null)
@@ -90,15 +75,40 @@ const emailList = ref([])
 const open = (campaign) => {
   selectedCampaign.value = campaign || null
   emailList.value = []
+  getStatusCounts()
   getList()
   showEmailListModal.value = true
 }
 
+const tabData = ref([])
+
+const changeTab = (value) => {
+  activeFilter.value = value
+  getStatusCounts()
+  getList()
+}
+
+const getStatusCounts = () => {
+  getEmailCount({ id: selectedCampaign.value.id }).then((res) => {
+    tabData.value = [
+      { value: 'pending', title: '待发送', count: res.result.pendingSendCount || 0 },
+      { value: 'sent', title: '已发送', count: res.result.sentCount || 0 },
+      { value: 'failed', title: '已取消', count: res.result.cancelledCount || 0 }
+    ]
+  })
+}
+
 const loading = ref(false)
 const getList = () => {
   loading.value = true
-  getEmailList({ id: selectedCampaign.value.id }).then((res) => {
-    emailList.value = res.result || []
+  let params = {
+    marketingCampaignId: selectedCampaign.value.id,
+    pageIndex: 1,
+    pageSize: 10000,
+    status: activeFilter.value === 'pending' ? -1 : activeFilter.value === 'sent' ? 1 : -3
+  }
+  getEmailList(params).then((res) => {
+    emailList.value = res.result.records || []
   }).finally(() => {
     loading.value = false
   })
@@ -109,19 +119,6 @@ const showEmailListModal = ref(false)
 
 const activeFilter = ref('pending')
 
-const filterEmailList = computed(() => {
-  if (activeFilter.value === 'all') {
-    return emailList.value
-  }
-  return emailList.value.filter((item) => item.status === (activeFilter.value == 'pending' ? -2 : activeFilter.value == 'sent' ? 1 : -3))
-})
-
-const statusCounts = computed(() => ({
-  pending: emailList.value.filter((item) => item.status === -2).length,
-  sent: emailList.value.filter((item) => item.status === 1).length,
-  failed: emailList.value.filter((item) => item.status === -3).length
-}))
-
 const refreshList = () => {
   // 这里可以接入真实数据刷新逻辑
 }