Prechádzať zdrojové kódy

Merge remote-tracking branch 'origin/master'

18118724055@163.com 3 týždňov pred
rodič
commit
7a261a24a4
26 zmenil súbory, kde vykonal 480 pridanie a 264 odobranie
  1. 2 1
      java/storlead-api/src/main/resources/application-dev.yml
  2. 1 1
      java/storlead-api/src/main/resources/application-prod.yml
  3. 1 1
      java/storlead-api/src/main/resources/application-test.yml
  4. 23 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/controller/MarketEmailsController.java
  5. 8 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/dto/MarketEmailsDTO.java
  6. 24 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/MarketEmailsService.java
  7. 22 16
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/CustomerTradeServiceImpl.java
  8. 87 21
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MarketEmailsServiceImpl.java
  9. 33 57
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MarketingCampaignServiceImpl.java
  10. 56 48
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MarketingCampaignSopDetailServiceImpl.java
  11. 1 1
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/task/MarketStartOrEndTask.java
  12. 35 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/vo/MarketEmailsStatsVO.java
  13. 4 1
      ui/smarttrade-platform/src/api/knowledge/index.js
  14. 5 4
      ui/smarttrade-platform/src/api/marketing/index.js
  15. 4 0
      ui/smarttrade-platform/src/assets/styles/marketing.css
  16. 50 14
      ui/smarttrade-platform/src/components/ckeditor/index.vue
  17. 1 1
      ui/smarttrade-platform/src/components/layout/Sidebar.vue
  18. 7 3
      ui/smarttrade-platform/src/components/layout/TopBar.vue
  19. 2 2
      ui/smarttrade-platform/src/components/marketing/aiGenerator.vue
  20. 41 43
      ui/smarttrade-platform/src/components/marketing/emailList.vue
  21. 1 1
      ui/smarttrade-platform/src/components/marketing/marketingDetail.vue
  22. 2 2
      ui/smarttrade-platform/src/utils/jsencrypt.js
  23. 45 34
      ui/smarttrade-platform/src/views/CustomersView.vue
  24. 9 8
      ui/smarttrade-platform/src/views/KnowledgeView.vue
  25. 10 1
      ui/smarttrade-platform/src/views/MarketingView.vue
  26. 6 4
      ui/smarttrade-platform/src/views/emails/edit.vue

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

@@ -191,7 +191,8 @@ logging:
 environment: test
 dify:
   base-url: http://47.112.196.2:11434/v1/
-  download-url: http://47.112.196.2:11435
+  download-url: https://trade.test.storlead.com
+  #http://47.112.196.2:11435
   dataset-api-key: dataset-qfhXMNcjsRSwN5CK6aoWx3hl
   app-keys:
     chat: app-1-qfhXMNcjsRSwN5CK6aoWx3hl  #对话

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

@@ -241,7 +241,7 @@ file:
 
 dify:
   base-url: http://47.112.196.2:11434/v1/
-  download-url: http://47.112.196.2:11435
+  download-url: https://trade.test.storlead.com
   dataset-api-key: dataset-qfhXMNcjsRSwN5CK6aoWx3hl
   app-keys:
     chat: app-1-qfhXMNcjsRSwN5CK6aoWx3hl  #对话

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

@@ -176,7 +176,7 @@ logging:
 environment: test
 dify:
   base-url: http://47.112.196.2:11434/v1/
-  download-url: http://47.112.196.2:11435
+  download-url: https://trade.test.storlead.com
   dataset-api-key: dataset-qfhXMNcjsRSwN5CK6aoWx3hl
   app-keys:
     chat: app-1-qfhXMNcjsRSwN5CK6aoWx3hl  #对话

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

@@ -1,6 +1,7 @@
 package com.storlead.trade.controller;
 
 import com.storlead.trade.dto.MarketEmailsDTO;
+import com.storlead.trade.entity.MarketEmailsEntity;
 import com.storlead.trade.service.MarketEmailsService;
 import com.storlead.framework.common.result.Result;
 import com.storlead.trade.vo.MarketEmailsManagementVO;
@@ -114,9 +115,31 @@ public class MarketEmailsController {
         return Result.result(marketEmailsService.getById(id));
     }
 
+    @PostMapping("/editContent")
+    @ApiOperation("营销: 营销邮件修改内容和主题")
+    public Result<?> editContent(@RequestBody MarketEmailsEntity marketEmailsEntity) {
+        boolean isOk = marketEmailsService.updateById(marketEmailsEntity);
+        if (!isOk) {
+            return Result.error("修改失败");
+        }
+        return Result.ok();
+    }
+
     @GetMapping("/management-detail")
     @ApiOperation("营销: 邮件管理弹窗(按 marketingCampaignId 查询所有邮件卡片)")
     public Result<Object> managementDetail(Long id) {
         return marketEmailsService.getManagementDetail(id);
     }
+
+    @GetMapping("/management-stats")
+    @ApiOperation("营销: 邮件管理弹窗-按状态统计(4 个状态计数:-3 已取消 / -2 待插入内容 / -1 待发送 / 1 已发送)")
+    public Result<Object> managementStats(Long id) {
+        return marketEmailsService.getManagementStats(id);
+    }
+
+    @PostMapping("/management-page")
+    @ApiOperation("营销: 邮件管理弹窗-按状态分页查询(按 marketingCampaignId + status)")
+    public Result<Object> managementPage(@RequestBody MarketEmailsDTO marketEmailsDTO) {
+        return marketEmailsService.getManagementPage(marketEmailsDTO);
+    }
 }

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

@@ -75,4 +75,12 @@ public class MarketEmailsDTO extends Page {
     @JSONField(format ="yyyy-MM-dd HH:mm:ss")
     @TableField("first_open_time")
     private Date firstOpenTime;
+
+    @ApiModelProperty("主题")
+    @TableField("subject")
+    private String subject;
+
+    @ApiModelProperty(value = "邮件内容")
+    @TableField("content")
+    private String content;
 }

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

@@ -5,8 +5,10 @@ import com.storlead.trade.entity.MarketEmailsEntity;
 import com.storlead.trade.entity.MarketingCampaignEntity;
 import com.storlead.trade.vo.MarketEmailsCustomerVO;
 import com.storlead.trade.vo.MarketEmailsManagementVO;
+import com.storlead.trade.vo.MarketEmailsStatsVO;
 import com.storlead.framework.common.result.Result;
 import com.storlead.framework.mybatis.service.MyBaseService;
+import com.baomidou.mybatisplus.core.metadata.IPage;
 import org.springframework.scheduling.annotation.Async;
 
 import java.util.List;
@@ -65,6 +67,28 @@ public interface MarketEmailsService extends MyBaseService<MarketEmailsEntity> {
      */
     Result<Object> getManagementDetail(Long marketingCampaignId);
 
+    /**
+     * 邮件管理弹窗-按状态统计(按营销活动 id)
+     * <p>
+     * 仅统计四个核心状态:-3 已取消 / -2 待插入内容 / -1 待发送 / 1 已发送
+     * </p>
+     *
+     * @param marketingCampaignId 营销活动 id
+     * @return 四个状态的邮件数量
+     */
+    Result<Object> getManagementStats(Long marketingCampaignId);
+
+    /**
+     * 邮件管理弹窗-按状态分页查询(按营销活动 id + status)
+     * <p>
+     * 复用 getManagementDetail 的 enrichment(customerName / campaignName / stageName)。
+     * </p>
+     *
+     * @param dto 需含 marketingCampaignId、status;分页参数走 MarketEmailsDTO 继承的 Page
+     * @return 分页结果
+     */
+    Result<Object> getManagementPage(MarketEmailsDTO dto);
+
     /***
       * @Description: 通过id/marketingCampaignId/客户id关联的邮件状态
       * @Param:

+ 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;
+            });
         }
 
         // 精确筛选:联系人手机/邮箱

+ 87 - 21
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MarketEmailsServiceImpl.java

@@ -372,6 +372,7 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
         if (!ok) {
             return Result.error("操作失败");
         }
+        //如果是重发,判断时间如果小于当前时间,直接发送
         return Result.ok();
     }
 
@@ -633,29 +634,22 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
     }
 
 
-    @Override
-    public Result<Object> getManagementDetail(Long marketingCampaignId) {
-        if (ObjectUtils.isEmpty(marketingCampaignId)) {
-            return Result.error("marketingCampaignId不能为空");
+    /**
+     * enrichment 私有方法:把 MarketEmailsEntity 列表填充 customerName / campaignName / stageName / customerContact。
+     * 供 getManagementDetail 与 getManagementPage 共用。
+     */
+    private List<MarketEmailsManagementVO> enrichManagementList(List<MarketEmailsEntity> entityList, Long marketingCampaignId) {
+        if (CollectionUtils.isEmpty(entityList)) {
+            return new ArrayList<>();
         }
 
-        // 营销活动名(用于拼接在卡片标题位置或返回顶部)
+        // 营销活动名
         String campaignName = null;
         MarketingCampaignEntity campaign = marketingCampaignService.getById(marketingCampaignId);
         if (campaign != null) {
             campaignName = campaign.getName();
         }
 
-        // 查该营销活动下所有未软删的邮件
-        LambdaQueryWrapper<MarketEmailsEntity> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(MarketEmailsEntity::getMarketingCampaignId, marketingCampaignId);
-        wrapper.eq(MarketEmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
-        wrapper.orderByAsc(MarketEmailsEntity::getExpectSendTime);
-        List<MarketEmailsEntity> entityList = this.list(wrapper);
-        if (CollectionUtils.isEmpty(entityList)) {
-            return Result.ok(new ArrayList<MarketEmailsManagementVO>());
-        }
-
         // 批量收 ID,减少 N+1
         Set<Long> customerIdSet = new HashSet<>();
         Set<Long> sopDetailIdSet = new HashSet<>();
@@ -670,10 +664,9 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
                     .collect(Collectors.toMap(CustomerEntity::getId, c -> c, (a, c) -> a));
         Map<Long, MarketingCampaignSopDetailEntity> sopDetailMap = sopDetailIdSet.isEmpty()
                 ? Collections.emptyMap()
-                : marketingCampaignSopDetailEntityService.listByIds(new java.util.ArrayList<>(sopDetailIdSet)).stream()
+                : marketingCampaignSopDetailEntityService.listByIds(new ArrayList<>(sopDetailIdSet)).stream()
                     .collect(Collectors.toMap(MarketingCampaignSopDetailEntity::getId, s -> s, (a, c) -> a));
 
-        // 组装 VO
         List<MarketEmailsManagementVO> voList = new ArrayList<>(entityList.size());
         for (MarketEmailsEntity entity : entityList) {
             MarketEmailsManagementVO vo = new MarketEmailsManagementVO();
@@ -697,7 +690,67 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
 
             voList.add(vo);
         }
-        return Result.ok(voList);
+        return voList;
+    }
+
+    @Override
+    public Result<Object> getManagementDetail(Long marketingCampaignId) {
+        if (ObjectUtils.isEmpty(marketingCampaignId)) {
+            return Result.error("marketingCampaignId不能为空");
+        }
+
+        // 查该营销活动下所有未软删的邮件
+        LambdaQueryWrapper<MarketEmailsEntity> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(MarketEmailsEntity::getMarketingCampaignId, marketingCampaignId);
+        wrapper.eq(MarketEmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
+        wrapper.orderByAsc(MarketEmailsEntity::getExpectSendTime);
+        List<MarketEmailsEntity> entityList = this.list(wrapper);
+        return Result.ok(enrichManagementList(entityList, marketingCampaignId));
+    }
+
+    @Override
+    public Result<Object> getManagementStats(Long marketingCampaignId) {
+        if (ObjectUtils.isEmpty(marketingCampaignId)) {
+            return Result.error("marketingCampaignId不能为空");
+        }
+        MarketEmailsStatsVO vo = new MarketEmailsStatsVO();
+        LambdaQueryWrapper<MarketEmailsEntity> base = new LambdaQueryWrapper<>();
+        base.eq(MarketEmailsEntity::getMarketingCampaignId, marketingCampaignId);
+        base.eq(MarketEmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
+        vo.setCancelledCount(this.count(base.clone().eq(MarketEmailsEntity::getStatus, -3)));
+        vo.setPendingInsertCount(this.count(base.clone().eq(MarketEmailsEntity::getStatus, -2)));
+        vo.setPendingSendCount(this.count(base.clone().eq(MarketEmailsEntity::getStatus, -1)));
+        vo.setSentCount(this.count(base.clone().eq(MarketEmailsEntity::getStatus, 1)));
+        return Result.result(vo);
+    }
+
+    @Override
+    public Result<Object> getManagementPage(MarketEmailsDTO dto) {
+        if (dto == null || ObjectUtils.isEmpty(dto.getMarketingCampaignId())) {
+            return Result.error("marketingCampaignId不能为空");
+        }
+        if (dto.getStatus() == null) {
+            return Result.error("status不能为空");
+        }
+
+        LambdaQueryWrapper<MarketEmailsEntity> wrapper = new LambdaQueryWrapper<>();
+        wrapper.eq(MarketEmailsEntity::getMarketingCampaignId, dto.getMarketingCampaignId());
+        wrapper.eq(MarketEmailsEntity::getStatus, dto.getStatus());
+        wrapper.eq(MarketEmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
+        wrapper.orderByAsc(MarketEmailsEntity::getExpectSendTime);
+
+        Page<MarketEmailsEntity> page = new Page<>(dto.getPageIndex(), dto.getPageSize());
+        IPage<MarketEmailsEntity> entityPage = this.page(page, wrapper);
+
+        // enrichment 仅作用于当前页 records
+        List<MarketEmailsManagementVO> voRecords = enrichManagementList(entityPage.getRecords(), dto.getMarketingCampaignId());
+
+        Page<MarketEmailsManagementVO> voPage = new Page<>(entityPage.getCurrent(), entityPage.getSize(), entityPage.getTotal());
+        voPage.setRecords(voRecords);
+
+
+
+        return Result.result(voPage);
     }
 
     @Override
@@ -817,7 +870,7 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
 
             Map sendMap = new HashMap();
             sendMap.put("country", country);
-            sendMap.put("beginDate", marketingCampaignEntity.getBeginData().toString());
+            sendMap.put("beginDate", marketingCampaignEntity.getBeginData() != null ? marketingCampaignEntity.getBeginData().toString() : "");
             sendMap.put("steps", sopDetailEntityString);
 
             ChatDTO chatDTO = new ChatDTO();
@@ -897,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());
@@ -908,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)){
@@ -922,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.setIsTrack(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());

+ 33 - 57
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MarketingCampaignServiceImpl.java

@@ -233,6 +233,9 @@ public class MarketingCampaignServiceImpl
     public Result<Object> addMarket(MarketingCampaignDTO marketingCampaignDTO) {
         MarketingCampaignEntity marketingCampaignEntity = new MarketingCampaignEntity();
         BeanUtils.copyProperties(marketingCampaignDTO, marketingCampaignEntity);
+        if (ObjectUtils.isEmpty(marketingCampaignEntity.getBeginData())){
+            marketingCampaignEntity.setBeginData(LocalDate.now());
+        }
         // supervisorList 转为逗号分隔字符串
         marketingCampaignEntity.setSupervisor(joinSupervisorList(marketingCampaignDTO));
         boolean isOk = this.save(marketingCampaignEntity);
@@ -272,6 +275,7 @@ public class MarketingCampaignServiceImpl
                     marketingCampaignSopDetailEntity.setName(sopDetailEntity.getName());
                     marketingCampaignSopDetailEntity.setStep(sopDetailEntity.getStep());
                     marketingCampaignSopDetailEntity.setType(sopDetailEntity.getType());
+                    marketingCampaignSopDetailEntity.setDays(sopDetailEntity.getDays());
                     marketingCampaignSopDetailEntity.setEmailContent(sopDetailEntity.getEmailContent());
                     //预留补充ai生成的邮件内容模板
 //                    marketingCampaignSopDetailEntity.setEmailContent();
@@ -293,6 +297,10 @@ public class MarketingCampaignServiceImpl
             return Result.error("id不能为空");
         }
         MarketingCampaignEntity marketingCampaignOld = this.getById(marketingCampaignDTO.getId());
+        //只有未开始的营销活动才能修改
+        if (marketingCampaignOld.getStatus() != null && marketingCampaignOld.getStatus() != 0) {
+            return Result.error("该营销活动不能修改");
+        }
         MarketingCampaignEntity marketingCampaignEntity = new MarketingCampaignEntity();
         BeanUtils.copyProperties(marketingCampaignDTO, marketingCampaignEntity);
         // supervisorList 转为逗号分隔字符串
@@ -344,6 +352,7 @@ public class MarketingCampaignServiceImpl
                     marketingCampaignSopDetailEntity.setName(sopDetailEntity.getName());
                     marketingCampaignSopDetailEntity.setStep(sopDetailEntity.getStep());
                     marketingCampaignSopDetailEntity.setType(sopDetailEntity.getType());
+                    marketingCampaignSopDetailEntity.setDays(sopDetailEntity.getDays());
                     marketingCampaignSopDetailEntity.setEmailContent(sopDetailEntity.getEmailContent());
                     //预留补充ai生成的邮件内容模板
 //                    marketingCampaignSopDetailEntity.setEmailContent();
@@ -397,67 +406,34 @@ public class MarketingCampaignServiceImpl
     @Override
     public void syncStartEndStatus() {
         LocalDate today = LocalDate.now(ZoneId.of("Asia/Shanghai"));
-        List<MarketingCampaignEntity> all = this.lambdaQuery()
+        // 查询所有未开始的活动
+        List<MarketingCampaignEntity> nonAll = this.lambdaQuery()
                 .eq(MarketingCampaignEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
+                .eq(MarketingCampaignEntity::getStatus, 0)
+                .ge(MarketingCampaignEntity::getBeginData, today)
                 .list();
-        if (CollectionUtils.isEmpty(all)) {
-            log.info("营销活动开始/结束状态同步:无未删除活动,跳过");
-            return;
-        }
-        // 按目标状态分组,只收集需要变更的 id(跳过暂停 / 日期缺失 / 状态已一致)
-        // 后续每种状态仅执行一次批量 UPDATE,而非逐条 UPDATE
-        Map<Integer, List<Long>> idsByStatus = new HashMap<>(4);
-        // 变更的实体列表(status 已回填为目标状态,用于区分改为开始/结束)
-        List<MarketingCampaignEntity> changed = new ArrayList<>();
-        for (MarketingCampaignEntity c : all) {
-            Integer expected = computeStartEndStatus(c, today);
-            if (expected == null) {
-                continue;
-            }
-            Integer current = c.getStatus();
-            // 尊重手动暂停,不自动覆盖
-            if (current != null && current == STATUS_PAUSED) {
-                continue;
-            }
-            if (expected.equals(current)) {
-                continue;
-            }
-            idsByStatus.computeIfAbsent(expected, k -> new ArrayList<>()).add(c.getId());
-            c.setStatus(expected);
-            changed.add(c);
+        if (!CollectionUtils.isEmpty(nonAll)) {
+            List<Long> startIdList = nonAll.stream().map(MarketingCampaignEntity::getId).collect(Collectors.toList());
+            LambdaUpdateWrapper<MarketEmailsEntity> marketEmailsUpdateWrapper = new LambdaUpdateWrapper<>();
+            marketEmailsUpdateWrapper.in(MarketEmailsEntity::getMarketingCampaignId, startIdList);
+            marketEmailsUpdateWrapper.ne(MarketEmailsEntity::getStatus, -3);
+            marketEmailsUpdateWrapper.set(MarketEmailsEntity::getStatus, -1);
+            marketEmailsService.update(marketEmailsUpdateWrapper);
         }
-        // 每种目标状态执行一次批量 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<MarketingCampaignEntity> stopAll = this.lambdaQuery()
+                .eq(MarketingCampaignEntity::getIsDelete, CommonConstant.DEL_FLAG_0)
+                .eq(MarketingCampaignEntity::getStatus, 1)
+                .le(MarketingCampaignEntity::getEndDate, today)
+                .list();
+        if (!CollectionUtils.isEmpty(stopAll)) {
+            List<Long> stopIdList = stopAll.stream().map(MarketingCampaignEntity::getId).collect(Collectors.toList());
+            LambdaUpdateWrapper<MarketEmailsEntity> marketEmailsUpdateWrapper = new LambdaUpdateWrapper<>();
+            marketEmailsUpdateWrapper.in(MarketEmailsEntity::getMarketingCampaignId, stopIdList);
+            marketEmailsUpdateWrapper.eq(MarketEmailsEntity::getStatus, -1);
+            marketEmailsUpdateWrapper.set(MarketEmailsEntity::getStatus, -3);
+            marketEmailsService.update(marketEmailsUpdateWrapper);
         }
-        // 按目标状态拆分列表并打印:哪些改为开始、哪些改为结束
-        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);
     }
 
     /**

+ 56 - 48
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MarketingCampaignSopDetailServiceImpl.java

@@ -11,10 +11,7 @@ import com.storlead.mail.service.MktMailMessageService;
 import com.storlead.trade.dto.CustomerDTO;
 import com.storlead.trade.dto.MarketingCampaignSopDetailDTO;
 import com.storlead.trade.dto.SysDictDetailResponseDTO;
-import com.storlead.trade.entity.CustomerEntity;
-import com.storlead.trade.entity.MarketEmailsEntity;
-import com.storlead.trade.entity.MarketingCampaignSopDetailEntity;
-import com.storlead.trade.entity.SysDictDetailEntity;
+import com.storlead.trade.entity.*;
 import com.storlead.trade.mapper.MarketingCampaignSopDetailMapper;
 import com.storlead.trade.service.*;
 import com.storlead.trade.service.impl.support.TradeDataSourceServiceImpl;
@@ -87,59 +84,70 @@ public class MarketingCampaignSopDetailServiceImpl
 
 
 //        List<CustomerEntity> customerEntityList = customerService.listByIds(customerIdList);
-        //查询本地信息
-        String yourName = "";
-        String yourTitle = "";
-        String yourCompany = "";
-        String yourPhone = "";
-        String yourEmail = "";
-        String fullCompanyAddress = "";
-        List<SysDictDetailResponseDTO> sysDictDetailResponseDTOList = sysDictService.getListByCode("company_base");
-        if (!CollectionUtils.isEmpty(sysDictDetailResponseDTOList)){
-            //取label为本地信息的value值
-            for (SysDictDetailResponseDTO sysDictDetailResponseDTO : sysDictDetailResponseDTOList) {
-                if ("yourName".equals(sysDictDetailResponseDTO.getLabel())) {
-                    yourName = sysDictDetailResponseDTO.getValue();
-                }
-                if ("yourTitle".equals(sysDictDetailResponseDTO.getLabel())) {
-                    yourTitle = sysDictDetailResponseDTO.getValue();
-                }
-                if ("yourCompany".equals(sysDictDetailResponseDTO.getLabel())) {
-                    yourTitle = sysDictDetailResponseDTO.getValue();
-                }
-                if ("yourPhone".equals(sysDictDetailResponseDTO.getLabel())) {
-                    yourPhone = sysDictDetailResponseDTO.getValue();
-                }
-                if ("yourEmail".equals(sysDictDetailResponseDTO.getLabel())) {
-                    yourEmail = sysDictDetailResponseDTO.getValue();
-                }
-                if ("fullCompanyAddress".equals(sysDictDetailResponseDTO.getLabel())) {
-                    fullCompanyAddress = sysDictDetailResponseDTO.getValue();
-                }
-            }
 
-        }
 
 
         Map<Long,CustomerVO> customerEntityMap = customerVOList.stream().collect(Collectors.toMap(CustomerVO::getId, customerVO -> customerVO));
         for (MarketEmailsEntity marketEmailsEntity : marketEmailsEntityList) {
-            marketEmailsEntity.setSubject(emailSubject);
+            String emailContentNew = emailContent;
+            String emailSubjectNew = emailSubject;
+            //查询客户信息
+//            客户名称:${customerName}$
+//            区域名称:${areaName}$
+//            企业名称:${companyName}$
+//            企业网址:${companyUrl}$
+//            联系人名称:${liaisonName}$
+//            联系人邮箱:${email}$
+            String customerName = "";
+            String areaName = "";
+            String companyName = "";
+            String companyUrl = "";
+            String liaisonName = "";
+            String email = "";
+//            marketEmailsEntity.setSubject(emailSubject);
             marketEmailsEntity.setStatus(-1);
             CustomerVO customerEntity = customerEntityMap.get(marketEmailsEntity.getCustomerId());
             if (customerEntity != null) {
+                if (ObjectUtils.isEmpty(customerEntity.getCustomerCompany())) {
+                    customerEntity.setCustomerCompany(new CustomerCompanyEntity());
+                }
+                if (ObjectUtils.isEmpty(customerEntity.getLiaison())) {
+                    customerEntity.setLiaison(new LiaisonEntity());
+                }
+                if (!ObjectUtils.isEmpty(customerEntity.getCustomerName())) {
+                    customerName = customerEntity.getCustomerName();
+                }
+                if (!ObjectUtils.isEmpty(customerEntity.getCountry())) {
+                    areaName = customerEntity.getCountry();
+                }
+                if (!ObjectUtils.isEmpty(customerEntity.getCustomerCompany().getName())){
+                    companyName = customerEntity.getCustomerCompany().getName();
+                }
+                if (!ObjectUtils.isEmpty(customerEntity.getCustomerCompany().getWebsite())){
+                    companyUrl = customerEntity.getCustomerCompany().getWebsite();
+                }
+                if (!ObjectUtils.isEmpty(customerEntity.getLiaison().getName())){
+                    liaisonName = customerEntity.getLiaison().getName();
+                }
+                if (!ObjectUtils.isEmpty(customerEntity.getLiaison().getEmail())){
+                    email = customerEntity.getLiaison().getEmail();
+                }
+                emailContentNew = emailContentNew.replace("${customerName}$", customerName)
+                        .replace("${areaName}$", areaName)
+                        .replace("${companyName}$", companyName)
+                        .replace("${companyUrl}$", companyUrl)
+                        .replace("${liaisonName}$", liaisonName)
+                        .replace("${email}$", email);
+
+                emailSubjectNew = emailSubjectNew.replace("${customerName}$", customerName)
+                        .replace("${areaName}$", areaName)
+                        .replace("${companyName}$", companyName)
+                        .replace("${companyUrl}$", companyUrl)
+                        .replace("${liaisonName}$", liaisonName)
+                        .replace("${email}$", email);
+                marketEmailsEntity.setContent(emailContentNew);
+                marketEmailsEntity.setSubject(emailSubjectNew);
 
-//                ${areaName}$、${customerIndustry}$
-                //修改emailContent中全部的${customerName}$为customerEntity.getCustomerName()
-                emailContent = emailContent.replace("${customerName}$", customerEntity.getCustomerName())
-                        .replace("${areaName}$", customerEntity.getCountry())
-                        .replace("${customerIndustry}$", customerEntity.getCustomerCompany().getIndustry())
-                        .replace("${yourName}$", yourName)
-                        .replace("${yourTitle}$", yourTitle)
-                        .replace("${yourCompany}$", yourCompany)
-                        .replace("${yourPhone}$", yourPhone)
-                        .replace("${yourEmail}$", yourEmail)
-                        .replace("${fullCompanyAddress}$", fullCompanyAddress);
-                marketEmailsEntity.setContent(emailContent);
             }
             if (!ObjectUtils.isEmpty(marketEmailsEntity.getCustomerId()) && !ObjectUtils.isEmpty(customerEntityMap.get(marketEmailsEntity.getCustomerId()).getCustomerCompany()) && !ObjectUtils.isEmpty(customerEntityMap.get(marketEmailsEntity.getCustomerId()).getCustomerCompany().getEmail())){
                 marketEmailsEntity.setRecipient(customerEntityMap.get(marketEmailsEntity.getCustomerId()).getCustomerCompany().getEmail());

+ 1 - 1
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/task/MarketStartOrEndTask.java

@@ -18,7 +18,7 @@ public class MarketStartOrEndTask {
     @Resource
     private MarketingCampaignService marketingCampaignService;
 
-    @Scheduled(cron = "0 0 8 * * ?")
+    @Scheduled(cron = "0 50 10 * * ?")
     public void dispatch() {
         long start = System.currentTimeMillis();
         log.info("营销活动开始/结束状态同步任务开始");

+ 35 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/vo/MarketEmailsStatsVO.java

@@ -0,0 +1,35 @@
+package com.storlead.trade.vo;
+
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 邮件管理弹窗-按状态统计 VO
+ * <p>
+ * 按 marketingCampaignId 统计四个核心状态下的邮件数量。
+ * </p>
+ *
+ * @author QClaw Generated
+ * @since 2026-09-15
+ */
+@Data
+@ApiModel(value = "MarketEmailsStatsVO", description = "邮件管理弹窗-按状态统计")
+public class MarketEmailsStatsVO implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    @ApiModelProperty(value = "已取消数量(status = -3)")
+    private Integer cancelledCount;
+
+    @ApiModelProperty(value = "待插入内容数量(status = -2)")
+    private Integer pendingInsertCount;
+
+    @ApiModelProperty(value = "待发送数量(status = -1)")
+    private Integer pendingSendCount;
+
+    @ApiModelProperty(value = "已发送数量(status = 1)")
+    private Integer sentCount;
+}

+ 4 - 1
ui/smarttrade-platform/src/api/knowledge/index.js

@@ -59,7 +59,10 @@ export const delChunk = (params = {}) => {
 }
 
 export const editChunk = (data = {}) => {
-    const { dataset_id,document_id,segment_id, ...query } = data
+    const { dataset_id,document_id,segment_id, ...query } = data.segment
+    if ([undefined, null, ''].includes(data.segment.answer)) {
+        delete data.segment.answer
+    }
     return post(`/router/rest/knowledge/chunk/${dataset_id}/${document_id}/${segment_id}`, data)
 }
 

+ 5 - 4
ui/smarttrade-platform/src/api/marketing/index.js

@@ -5,7 +5,7 @@ export const getMarketingById = (id) => get(`/router/rest/marketing/detail/${id}
 export const getSopList = (data = {}) => post('/router/rest/api/sop/list', data)
 export const addSop = (data = {}) => post('/router/rest/api/sop/create', data)
 export const editSop = (id, data = {}) => put(`/router/rest/api/sop/${id}`, data)
-export const getSopById = (id) => get(`/router/rest/api/sop/${id}`)
+export const getSopById = (id) => get(`/router/rest/marketing/campaign/sop-detail/campaign/${id}`)
 export const addMarketing = (data = {}) => post('/router/rest/marketing/add', data)
 export const editMarketing = (data = {}) => post('/router/rest/marketing/edit', data)
 export const marketingAnalysis = () => get('/router/rest/marketing/dashboard/stats')
@@ -14,10 +14,11 @@ 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)
 export const saveGenerateEmail = (data = {}) => post('/router/rest/marketing/campaign/sop-detail/edit', data)
-export const getEmailDetail = (data = {}) => post('/router/rest/mkt/email/get_mail_detial', data)
-export const editEmail = (data = {}) => post('/router/rest/mkt/email/edit_content', data)
+export const getEmailDetail = (data = {}) => get('/router/rest/marketEmails/getById', data)
+export const editEmail = (data = {}) => post('/router/rest/marketEmails/editContent', data)

+ 4 - 0
ui/smarttrade-platform/src/assets/styles/marketing.css

@@ -923,6 +923,10 @@
 
 .no-data{ min-height: 100px; display: flex; align-items: center; justify-content: center; color: var(--text-muted); font-size: 14px; }
 :deep(.el-loading-mask){ background: transparent; }
+.btn:disabled{
+  opacity: 0.5;
+  cursor: not-allowed;
+}
 
 
 @media (max-width: 1400px) {

+ 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();

+ 1 - 1
ui/smarttrade-platform/src/components/layout/Sidebar.vue

@@ -330,7 +330,7 @@ import { ref, onMounted } from 'vue'
 import { useRoute, useRouter } from 'vue-router'
 import { getUserInfo, updateUserPwd, getLoginData, updateRemark } from '@/api/layout'
 import { ElMessageBox } from 'element-plus'
-import { encrypt } from "@/utils/jsencrypt";
+import { encrypt } from "@/utils/jsencrypt.js";
 
 const route = useRoute()
 const router = useRouter()

+ 7 - 3
ui/smarttrade-platform/src/components/layout/TopBar.vue

@@ -49,9 +49,9 @@
           </div>
           <div class="panel-actions">
             <button class="action-text" @click="markAllAsRead">全部已读</button>
-            <button class="action-text" @click="openSettings">
+            <!-- <button class="action-text" @click="openSettings">
               <i class="fas fa-cog"></i>
-            </button>
+            </button> -->
           </div>
         </div>
 
@@ -567,7 +567,7 @@
     </transition>
   </header>
 
-  <AddMarketing ref="addMarketingRef" />
+  <AddMarketing ref="addMarketingRef" @refresh="handleMarketingRefresh" />
 
   <div v-if="showSettings" class="modal-overlay" @click.self="showSettings = false">
     <div class="modal create-modal" style="max-width: 800px;">
@@ -709,6 +709,10 @@ const pageTitle = computed(() => {
 })
 
 const addMarketingRef = ref()
+const handleMarketingRefresh = () => {
+  window.dispatchEvent(new CustomEvent('marketing:refresh'))
+}
+
 const openModal = (type) => {
   showQuickActions.value = false
   addMarketingRef.value.open()

+ 2 - 2
ui/smarttrade-platform/src/components/marketing/aiGenerator.vue

@@ -183,8 +183,8 @@
     const emailSubject = ref('')
     const open = (data) => {
         marketingData.value = data
-        getSopById(data.sopId).then(res => {
-            typeData.value = res.result.details || []
+        getSopById(data.id).then(res => {
+            typeData.value = res.result || []
         })
         aiGenerator.value.inputs = {
             scene: '',

+ 41 - 43
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>
                 
@@ -60,7 +45,7 @@
               <button v-if="activeFilter !== 'sent'" class="btn btn-secondary" @click="cancelSend(item)">
                 {{  activeFilter === 'pending' ? '取消' : '重新' }}发送
               </button>
-              <button class="btn btn-secondary" @click="viewDetail(item)">
+              <button class="btn btn-secondary" @click="viewDetail(item)" v-if="activeFilter == 'sent'">
                 邮件详情
               </button>
               <button v-if="activeFilter !== 'sent'" class="btn btn-primary" @click="editEmail(item)">
@@ -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 = () => {
   // 这里可以接入真实数据刷新逻辑
 }
@@ -141,9 +138,10 @@ const cancelSend = (item) => {
       type: 'warning',
     }
   ).then(() => {
-    cancelEmail({ id: item.id, status: (item.status == -2 ? -3 : -2) }).then(() => {
+    cancelEmail({ id: item.id, status: (item.status == -1 ? -3 : -1) }).then(() => {
       ElMessage.success('操作成功')
       getList()
+      getStatusCounts()
     }).catch((err) => {
       ElMessage.error(err.message || '操作失败')
     })
@@ -163,7 +161,7 @@ const editEmail = (item) => {
   // 这里可以接入编辑邮件逻辑
   const { href } = router.resolve({
     path: '/emails/edit',
-    query: { id: item.emailId }
+    query: { id: item.id }
   })
   window.open(href, '_blank')
 }

+ 1 - 1
ui/smarttrade-platform/src/components/marketing/marketingDetail.vue

@@ -208,7 +208,7 @@
             <i class="fas fa-download"></i> 导出报告
           </button> -->
           <button class="btn btn-secondary" @click="showCampaignDetailModal = false">关闭</button>
-          <button class="btn btn-primary" @click="editCampaign(selectedCampaign)">
+          <button class="btn btn-primary" :disabled="selectedCampaign.status != 0" @click="editCampaign(selectedCampaign)">
             <i class="fas fa-edit"></i> 编辑活动
           </button>
         </div>

+ 2 - 2
ui/smarttrade-platform/src/utils/jsencrypt.ts → ui/smarttrade-platform/src/utils/jsencrypt.js

@@ -7,7 +7,7 @@ const publicKey = import.meta.env.VITE_APP_RSA_PUBLIC_KEY;
 const privateKey = import.meta.env.VITE_APP_RSA_PRIVATE_KEY;
 
 // 加密
-export const encrypt = (txt: string | undefined) => {
+export const encrypt = (txt) => {
     if(!!!txt) {
         throw new Error('加密数据不能为空')
     }
@@ -17,7 +17,7 @@ export const encrypt = (txt: string | undefined) => {
 };
 
 // 解密
-export const decrypt = (txt: string) => {
+export const decrypt = (txt) => {
     const encryptor = new JSEncrypt();
     encryptor.setPrivateKey(privateKey); // 设置私钥
     return encryptor.decrypt(txt); // 对数据进行解密

+ 45 - 34
ui/smarttrade-platform/src/views/CustomersView.vue

@@ -36,10 +36,10 @@
     <div style="display: flex; gap: 16px; margin-bottom: 20px;">
       <div class="search-box" style="flex: 1;">
         <i class="fas fa-search search-icon"></i>
-        <input type="text" v-model="searchQuery" class="search-input" placeholder="搜索公司名称、联系人、邮箱..." @input="getCustomer">
+        <input type="text" v-model="searchQuery" class="search-input" placeholder="搜索公司名称、联系人、邮箱..." @change="getCustomerData">
       </div>
-      <div style="display: flex; gap: 8px;">
-        <el-cascader class="form-select" style="width: auto; padding: 0;" placeholder="区域名称" :options="region"
+      <div style="display: flex; gap: 8px;" class="form-select">
+        <el-cascader placeholder="区域名称" :options="region"
           v-model="regionValue" clearable filterable @change="getRegion" />
         <!-- <select v-model="industryFilter" class="form-select" style="width: auto; padding: 8px 16px;">
           <option value="">全部行业</option>
@@ -50,7 +50,7 @@
         <button class="btn btn-secondary"><i class="fas fa-sliders-h"></i> 高级筛选</button> -->
       </div>
     </div>
-
+    
     <!-- Tabs -->
     <div style="display: flex; gap: 4px; margin-bottom: 20px; border-bottom: 1px solid var(--border); padding-bottom: 12px;">
       <div v-for="tab in tabs" :key="tab.key" 
@@ -70,39 +70,41 @@
 
     <!-- Customer List -->
     <!-- <div style="flex: 1; overflow-y: auto;"> -->
+    <div v-loading="dataLoading" element-loading-background="rgba(0, 0, 0, 0.8)">
       <div v-for="customer in customers" :key="customer.id" 
            class="customer-card"
            @click="showCustomerDetail(customer)">
-        <div class="customer-flag">{{ customer.flag }}</div>
-        <div class="customer-info">
-          <div class="customer-header">
-            <span class="customer-name">{{ customer.customerName }}</span>
-            <!-- 客户类型标识 -->
-            <!-- <span class="customer-status" :style="{ background: getStatusBg(customer.status), color: getStatusColor(customer.status) }">
-              {{ getStatusLabel(customer.status) }}
-            </span> -->
-          </div>
-          <div class="customer-contact">
-            <i class="fas fa-user"></i> {{ customer.liaison?.name }}<span v-if="customer.liaison?.departmentPosition"> · </span>{{ customer.liaison?.departmentPosition }}
+          <div class="customer-flag">{{ customer.flag }}</div>
+          <div class="customer-info">
+            <div class="customer-header">
+              <span class="customer-name">{{ customer.customerName }}</span>
+              <!-- 客户类型标识 -->
+              <!-- <span class="customer-status" :style="{ background: getStatusBg(customer.status), color: getStatusColor(customer.status) }">
+                {{ getStatusLabel(customer.status) }}
+              </span> -->
+            </div>
+            <div class="customer-contact">
+              <i class="fas fa-user"></i> {{ customer.liaison?.name }}<span v-if="customer.liaison?.departmentPosition"> · </span>{{ customer.liaison?.departmentPosition }}
+            </div>
+            <div class="customer-meta">
+              <span><i class="fas fa-map-marker-alt"></i> {{ customer.continent }} · {{ customer.country }}</span>
+              <span><i class="fas fa-industry"></i> {{ customer.customerCompany?.industry }}</span>
+              <!-- <span><i class="fas fa-dollar-sign"></i> {{ customer.annualPurchase }}</span> -->
+            </div>
           </div>
-          <div class="customer-meta">
-            <span><i class="fas fa-map-marker-alt"></i> {{ customer.continent }} · {{ customer.country }}</span>
-            <span><i class="fas fa-industry"></i> {{ customer.customerCompany?.industry }}</span>
-            <!-- <span><i class="fas fa-dollar-sign"></i> {{ customer.annualPurchase }}</span> -->
+          <!-- <div class="customer-score">
+            <div class="score-circle" :class="getScoreClass(customer.score)">{{ customer.score }}</div>
+            <div class="score-label">意向分</div>
+          </div> -->
+          <div class="customer-actions" @click.stop>
+            <button class="btn btn-secondary btn-sm" @click="showCustomerDetail(customer)"><i class="fas fa-eye"></i>更多详情</button>
           </div>
         </div>
-        <!-- <div class="customer-score">
-          <div class="score-circle" :class="getScoreClass(customer.score)">{{ customer.score }}</div>
-          <div class="score-label">意向分</div>
-        </div> -->
-        <div class="customer-actions" @click.stop>
-          <button class="btn btn-secondary btn-sm" @click="showCustomerDetail(customer)"><i class="fas fa-eye"></i>更多详情</button>
-        </div>
       </div>
       <div class="pagination-area">
         <el-pagination
           background
-          :total="customerStats.total"
+          :total="customerTotal"
           style="text-align: right;"
           :current-page="params.pageIndex"
           :page-size="params.pageSize"
@@ -110,6 +112,7 @@
           @current-change="pageCurrentChange">
         </el-pagination>
       </div>
+    
     <!-- </div> -->
 
     <!-- 引入添加客户弹窗组件 -->
@@ -273,6 +276,7 @@ const industryFilter = ref('')
 const showToast = ref(false)
 const toastMessage = ref('')
 const toastType = ref('success')
+const dataLoading = ref(false)
 
 const customerStats = ref({
   total: 0,
@@ -297,19 +301,26 @@ const params = ref({
   blurry:null,
   country:null,
 })
-const getCustomer = () => {
+const getCustomer = (type) => {
+    dataLoading.value= true
     params.value.blurry = searchQuery.value
     getCustomerList(params.value).then(res => {
-    customerStats.value.total = tabs.value[0].count = res.result.total
+    dataLoading.value= false
+    if(type=='init'){ //页面初始记录客户总数量
+      customerStats.value.total = res.result.total
+    }
+    customerTotal.value = tabs.value[0].count = res.result.total
     customers.value = res.result.records
   })
 }
-getCustomer()
+getCustomer('init')
 const pageCurrentChange = (page) => {
     params.value.pageIndex = page
     getCustomer()
 }
-
+const getCustomerData = ()=>{
+  getCustomer()
+}
 const regionValue = ref([])
 const getRegion = (value) => {
     params.value.country = value?value[1]:''
@@ -530,8 +541,8 @@ const handleAddCustomer = (customerData) => {
 .search-icon { position: absolute; left: 14px; top: 50%; transform: translateY(-50%); color: var(--text-muted); }
 .search-input { width: 100%; padding: 10px 14px 10px 40px; background: var(--bg-card); border: 1px solid var(--border); border-radius: 10px; color: var(--text-primary); font-size: 13px; }
 .search-input:focus { outline: none; border-color: var(--primary); }
-.form-select { background: var(--bg-card); border: 1px solid var(--border); color: var(--text-primary); }
-.form-select :deep(.el-input__wrapper){ background: var(--bg-card);}
+.form-select { background: var(--bg-card); border: none; color: var(--text-primary);width: auto;padding: 0; }
+.form-select :deep(.el-input__wrapper){ background: var(--bg-card);box-shadow:none;border: 1px solid var(--border);border-radius: 8px;padding: 3px 10px;}
 .customer-card { display: flex; align-items: center; gap: 16px; padding: 16px; background: var(--bg-card); border-radius: 12px; margin-bottom: 12px; border: 1px solid var(--border); cursor: pointer; transition: all 0.2s; }
 .customer-card:hover { border-color: var(--primary); }
 .customer-flag { font-size: 32px; }
@@ -553,7 +564,7 @@ const handleAddCustomer = (customerData) => {
 .customer-detail-modal { max-width: 800px; }
 .modal-header { display: flex; justify-content: space-between; align-items: center; padding: 20px; border-bottom: 1px solid var(--border); }
 .modal-title { display: flex; align-items: center; gap: 12px; font-size: 16px; font-weight: 600; }
-.modal-title h3 { font-size: 16px; font-weight: 600; margin: 0; }
+.modal-title h3 { font-size: 16px; font-weight: 600; margin: 0;word-break: break-all;}
 .modal-title p { font-size: 12px; color: var(--text-secondary); margin: 4px 0 0 0; }
 .detail-flag { font-size: 32px; }
 .modal-close { width: 32px; height: 32px; background: var(--bg-hover); border: none; border-radius: 8px; color: var(--text-secondary); cursor: pointer; display: flex; align-items: center; justify-content: center; }

+ 9 - 8
ui/smarttrade-platform/src/views/KnowledgeView.vue

@@ -548,14 +548,14 @@
             </div>
           </div> -->
           <div class="detail-stats-row">
-            <div class="detail-stat">
+            <!-- <div class="detail-stat">
               <i class="fas fa-eye"></i>
-              <span>{{ selectedKnowledge.views }} 次查看</span>
-            </div>
-            <div class="detail-stat">
+              <span>{{ selectedKnowledge.hit_count }} 次查看</span>
+            </div> -->
+            <!-- <div class="detail-stat">
               <i class="fas fa-thumbs-up"></i>
               <span>{{ selectedKnowledge.likes }} 次好评</span>
-            </div>
+            </div> -->
             <div class="detail-stat">
               <i class="fas fa-clock"></i>
               <span>更新于 {{ selectedKnowledge.timeip }}</span>
@@ -1107,7 +1107,8 @@ const addKnowledgeDocument = ()=> {
   documentList.value = []
 }
 const addKnowledge = () => {
-  if(!newKnowledge.value.answer||!newKnowledge.value.content||!newKnowledge.value.dataset_id||!newKnowledge.value.document_id){
+   
+  if(((chunkType.value=='add'||doc_form.value=='qa_model')&&!newKnowledge.value.answer)||!newKnowledge.value.content||!newKnowledge.value.dataset_id||!newKnowledge.value.document_id){
     ElMessage.error('请填入必填项')
     return false
   }
@@ -1119,7 +1120,7 @@ const addKnowledge = () => {
       }
     })
   }else{
-    editChunk(newKnowledge.value).then(res => {
+    editChunk({segment:newKnowledge.value}).then(res => {
       if(res.success){
         showToastMessage('知识编辑成功,AI 已学习', 'success')
         showAddModal.value = false
@@ -1383,7 +1384,7 @@ const showToastMessage = (message, type = 'success') => {
 .import-modal { max-width: 500px; }
 .modal-header { display: flex; justify-content: space-between; align-items: center; padding: 20px; border-bottom: 1px solid var(--border); }
 .modal-title { display: flex; align-items: center; gap: 12px; font-size: 16px; font-weight: 600; }
-.modal-title h3 { font-size: 16px; font-weight: 600; margin: 0; }
+.modal-title h3 { font-size: 16px; font-weight: 600; margin: 0; word-break: break-all;}
 .modal-title p { font-size: 12px; color: var(--text-secondary); margin: 4px 0 0 0; }
 .detail-icon { width: 40px; height: 40px; border-radius: 10px; display: flex; align-items: center; justify-content: center; font-size: 18px; }
 .modal-close { width: 32px; height: 32px; background: var(--bg-hover); border: none; border-radius: 8px; color: var(--text-secondary); cursor: pointer; display: flex; align-items: center; justify-content: center; }

+ 10 - 1
ui/smarttrade-platform/src/views/MarketingView.vue

@@ -731,7 +731,7 @@
 </template>
 
 <script setup>
-import { ref, computed, onMounted } from 'vue'
+import { ref, computed, onMounted, onBeforeUnmount } from 'vue'
 import { ElMessageBox } from 'element-plus'
 import AddMarketing from '../components/addMarketing.vue'
 import { getMarketingList, setMarketingStatus, marketingAnalysis } from '../api/marketing'
@@ -773,9 +773,18 @@ const openCreateModal = () => {
 }
 
 const marketingList = ref([])
+const handleExternalMarketingRefresh = () => {
+  getData()
+}
+
 onMounted(() => {
   getData()
   getStatsData()
+  window.addEventListener('marketing:refresh', handleExternalMarketingRefresh)
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('marketing:refresh', handleExternalMarketingRefresh)
 })
 
 const loading = ref(false)

+ 6 - 4
ui/smarttrade-platform/src/views/emails/edit.vue

@@ -21,7 +21,7 @@
                 </div>
                 <div class="send-input">
                     <span class="send-label">主题</span>
-                    <el-input v-model="form.subject" disabled></el-input>
+                    <el-input v-model="form.subject"></el-input>
                 </div>
                 <div class="msg-item" style="display: flex;flex: 1">
                     <div style="display: flex;flex-direction:column; flex: 1">
@@ -107,8 +107,10 @@
     const router = useRouter();
     let queryData = router.currentRoute.value.query
     const getData = () => {
-        getEmailDetail({ mailId: queryData.id }).then(res => {
-            Object.assign(form.value, res.result)
+        getEmailDetail({ id: queryData.id }).then(res => {
+            form.value.recipient = res.result.recipient
+            form.value.subject = res.result.subject
+            form.value.content = res.result.content
         })
     }
 
@@ -301,7 +303,7 @@
     const saveLoading = ref(false)
     const saveEmail = () => {
         saveLoading.value = true
-        editEmail({ mailId: form.value.id, content: form.value.content }).then(res => {
+        editEmail({ id: queryData.id, content: form.value.content, subject: form.value.subject }).then(res => {
             ElMessage.success('保存成功')
         }).catch((err) => {
             ElMessage.error(err.message || '保存失败')