فهرست منبع

Merge remote-tracking branch 'origin/master'

zhujing 4 هفته پیش
والد
کامیت
d72cd41b04
25فایلهای تغییر یافته به همراه331 افزوده شده و 71 حذف شده
  1. 1 1
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/MktMailRemoteClient.java
  2. 3 0
      java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/constant/MailRemoteApiConstants.java
  3. 11 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/controller/MarketEmailsController.java
  4. 8 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/dto/MarketEmailsDTO.java
  5. 6 2
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/entity/MarketEmailsEntity.java
  6. 8 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/MarketEmailsService.java
  7. 87 10
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MarketEmailsServiceImpl.java
  8. 37 29
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/service/impl/MarketingCampaignSopDetailServiceImpl.java
  9. 30 0
      java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/task/MarketEmailsSendSyncTask.java
  10. 5 0
      ui/smarttrade-platform/.env.development
  11. 5 0
      ui/smarttrade-platform/.env.production
  12. 6 1
      ui/smarttrade-platform/.env.test
  13. 6 0
      ui/smarttrade-platform/package-lock.json
  14. 2 1
      ui/smarttrade-platform/package.json
  15. 4 4
      ui/smarttrade-platform/src/api/marketing/index.js
  16. 1 0
      ui/smarttrade-platform/src/assets/styles/marketing.css
  17. 7 2
      ui/smarttrade-platform/src/components/addMarketing.vue
  18. 7 1
      ui/smarttrade-platform/src/components/layout/Sidebar.vue
  19. 2 2
      ui/smarttrade-platform/src/components/layout/TopBar.vue
  20. 57 8
      ui/smarttrade-platform/src/components/marketing/aiGenerator.vue
  21. 1 1
      ui/smarttrade-platform/src/components/marketing/emailList.vue
  22. 3 1
      ui/smarttrade-platform/src/components/marketing/marketingDetail.vue
  23. 24 0
      ui/smarttrade-platform/src/utils/jsencrypt.js
  24. 4 4
      ui/smarttrade-platform/src/views/MarketingView.vue
  25. 6 4
      ui/smarttrade-platform/src/views/emails/edit.vue

+ 1 - 1
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/client/MktMailRemoteClient.java

@@ -52,7 +52,7 @@ public class MktMailRemoteClient {
     }
 
     public Result<?> send(MktSendMailRemoteDTO dto) {
-        return http.postJson(MailRemoteApiConstants.MKT_SEND, dto);
+        return http.postJson(MailRemoteApiConstants.MKT_DISPATCH_SENT, dto);
     }
 
     /**

+ 3 - 0
java/storlead-mail/storlead-mail-api/src/main/java/com/storlead/mail/webclient/constant/MailRemoteApiConstants.java

@@ -34,6 +34,9 @@ public final class MailRemoteApiConstants {
     /** 发送营销邮件(默认同步;async=true 异步),JSON:MktSendMailRemoteDTO */
     public static final String MKT_SEND = "/mkt/send";
 
+    /** 发送营销邮件不需要token(默认同步;async=true 异步),JSON:MktSendMailRemoteDTO */
+    public static final String MKT_DISPATCH_SENT = "/mkt/dispatch_sent";
+
     /** 编辑营销邮件正文(仅更新内容,不发送、不改收件人/附件),JSON:MktEditMailContentRemoteDTO */
     public static final String MKT_EDIT_CONTENT = "/mkt/edit_content";
 

+ 11 - 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,6 +115,16 @@ 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) {

+ 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;
 }

+ 6 - 2
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/entity/MarketEmailsEntity.java

@@ -35,7 +35,7 @@ public class MarketEmailsEntity extends SysBaseField {
     @TableField("email_id")
     private Long emailId;
 
-    @ApiModelProperty(value = "发送状态:-3取消 -2草稿 -1待定时 0发送中 1成功 2失败;收信记录一般为1")
+    @ApiModelProperty(value = "发送状态:-3取消 -2草稿 -1定时 0发送中 1成功 2失败;收信记录一般为1")
     @TableField("status")
     private Integer status;
 
@@ -43,6 +43,10 @@ public class MarketEmailsEntity extends SysBaseField {
     @TableField("has_reader")
     private Integer hasReader;
 
+    @ApiModelProperty("主题")
+    @TableField("subject")
+    private String subject;
+
     @ApiModelProperty(value = "是否已回复:0否,1是")
     @TableField("has_reply")
     private Integer hasReply;
@@ -66,7 +70,7 @@ public class MarketEmailsEntity extends SysBaseField {
     @TableField("has_suspend")
     private Integer hasSuspend;
 
-    @ApiModelProperty(value = "发件人邮箱")
+    @ApiModelProperty(value = "收件人邮箱")
     @TableField("recipient")
     private String recipient;
 

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

@@ -77,4 +77,12 @@ public interface MarketEmailsService extends MyBaseService<MarketEmailsEntity> {
     @Async
     void addSendTime(MarketingCampaignEntity marketingCampaignEntity);
 
+    /***
+      * @Description: 定时发送邮件
+      * @Param:
+      * @return:
+      * @Author: YPZ
+      * @Date: 2026/9/11 16:46
+      */
+    void send();
 }

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

@@ -18,6 +18,8 @@ import com.storlead.knowledge.service.ChatService;
 import com.storlead.knowledge.service.WorkflowsService;
 import com.storlead.mail.pojo.entity.MktMailStatusEntity;
 import com.storlead.mail.service.MktMailStatusService;
+import com.storlead.mail.webclient.client.MktMailRemoteClient;
+import com.storlead.mail.webclient.dto.MktSendMailRemoteDTO;
 import com.storlead.trade.dto.MarketEmailsDTO;
 import com.storlead.trade.entity.*;
 import com.storlead.trade.mapper.MarketEmailsMapper;
@@ -32,6 +34,8 @@ import org.springframework.util.CollectionUtils;
 import org.springframework.util.ObjectUtils;
 
 import javax.annotation.Resource;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
 import java.util.*;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -72,6 +76,9 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
     @Resource
     private MktMailStatusService mktMailStatusService;
 
+    @Resource
+    private MktMailRemoteClient mktMailRemoteClient;
+
 
     @Override
     public Result<Object> getList(MarketEmailsDTO dto) {
@@ -365,6 +372,7 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
         if (!ok) {
             return Result.error("操作失败");
         }
+        //如果是重发,判断时间如果小于当前时间,直接发送
         return Result.ok();
     }
 
@@ -720,12 +728,12 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
         markerEmailUpdateWrapper.set(MarketEmailsEntity::getStatus, marketEmailsCustomerVO.getStatus());
         this.update(null, markerEmailUpdateWrapper);
 
-        if (!CollectionUtils.isEmpty(emailIdList) && !ObjectUtils.isEmpty(marketEmailsCustomerVO.getStatus())){
-            LambdaUpdateWrapper<MktMailStatusEntity> updateWrapper = new LambdaUpdateWrapper<>();
-            updateWrapper.in(MktMailStatusEntity::getId, emailIdList);
-            updateWrapper.set(MktMailStatusEntity::getSendStatus, marketEmailsCustomerVO.getStatus());
-            mktMailStatusService.update(null, updateWrapper);
-        }
+//        if (!CollectionUtils.isEmpty(emailIdList) && !ObjectUtils.isEmpty(marketEmailsCustomerVO.getStatus())){
+//            LambdaUpdateWrapper<MktMailStatusEntity> updateWrapper = new LambdaUpdateWrapper<>();
+//            updateWrapper.in(MktMailStatusEntity::getId, emailIdList);
+//            updateWrapper.set(MktMailStatusEntity::getSendStatus, marketEmailsCustomerVO.getStatus());
+//            mktMailStatusService.update(null, updateWrapper);
+//        }
 
 
     }
@@ -810,6 +818,7 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
 
             Map sendMap = new HashMap();
             sendMap.put("country", country);
+            sendMap.put("beginDate", marketingCampaignEntity.getBeginData().toString());
             sendMap.put("steps", sopDetailEntityString);
 
             ChatDTO chatDTO = new ChatDTO();
@@ -846,6 +855,7 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
                     MarketEmailsEntity marketEmail = new MarketEmailsEntity();
                     marketEmail.setMarketingCampaignId(marketingCampaignEntity.getId());
                     marketEmail.setCustomerId(customerVO.getCustomerId());
+                    marketEmail.setRecipient(customerVO.getEmail());
                     marketEmail.setMarketingCampaignSopDetailId(marketingCampaignSopDetail.getId());
                     marketEmail.setExpectSendTime(scheduleStage.getBeijingDatetime());
                     marketEmailList.add(marketEmail);
@@ -858,16 +868,83 @@ public class MarketEmailsServiceImpl extends MyBaseServiceImpl<MarketEmailsMappe
         }
         if (!marketEmailList.isEmpty()){
             //删除原有营销关联的邮件信息
-            LambdaUpdateWrapper<MarketEmailsEntity> updateWrapper = new LambdaUpdateWrapper<>();
-            updateWrapper.eq(MarketEmailsEntity::getMarketingCampaignId, marketingCampaignEntity.getId());
-            updateWrapper.set(MarketEmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_1);
-            this.update(updateWrapper);
+            LambdaQueryWrapper<MarketEmailsEntity> queryWrapper = new LambdaQueryWrapper<>();
+            queryWrapper.eq(MarketEmailsEntity::getMarketingCampaignId, marketingCampaignEntity.getId());
+            queryWrapper.eq(MarketEmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
+            List<MarketEmailsEntity> marketEmailsOldList = this.list(queryWrapper);
+            if (!CollectionUtils.isEmpty(marketEmailsOldList)) {
+                marketEmailsOldList.forEach(marketEmail -> marketEmail.setIsDelete(CommonConstant.DEL_FLAG_1));
+                this.updateBatchById(marketEmailsOldList);
+            }
+
+            //删除原有关联邮件记录
+//            List<Long> emailIdList = marketEmailsOldList.stream().map(MarketEmailsEntity::getEmailId).collect(Collectors.toList());
+//            LambdaUpdateWrapper<MktMailStatusEntity> updateWrapper = new LambdaUpdateWrapper<>();
+//            updateWrapper.in(MktMailStatusEntity::getId, emailIdList);
+//            updateWrapper.set(MktMailStatusEntity::getIsDelete, CommonConstant.DEL_FLAG_1);
+//            mktMailStatusService.update(updateWrapper);
+
+            marketEmailList.forEach(
+                    marketEmail -> {
+                        marketEmail.setIsDelete(CommonConstant.DEL_FLAG_0);
+                    }
+            );
             this.saveBatch(marketEmailList);
+
         }
 
 
     }
 
+    @Override
+    public void send() {
+        LambdaQueryWrapper<MarketEmailsEntity> queryWrapper = new LambdaQueryWrapper<>();
+
+//        queryWrapper.eq(MarketEmailsEntity::getExpectSendTime, new Date());
+        // get start and end of current minute
+        LocalDateTime now = LocalDateTime.now();
+        LocalDateTime start = now.withSecond(0).withNano(0);
+        LocalDateTime end = now.withSecond(59).withNano(999_999_999);
+        queryWrapper.between(MarketEmailsEntity::getExpectSendTime, start, end);
+
+        queryWrapper.eq(MarketEmailsEntity::getStatus, -1);
+        queryWrapper.eq(MarketEmailsEntity::getIsDelete, CommonConstant.DEL_FLAG_0);
+        List<MarketEmailsEntity> marketEmailList = this.list(queryWrapper);
+
+        if (CollectionUtils.isEmpty(marketEmailList)){
+            return;
+        }
+        log.info("queryWrapper: {}", queryWrapper);
+        log.info("marketEmailList: {}", marketEmailList);
+        for (MarketEmailsEntity marketEmail : marketEmailList){
+            MktSendMailRemoteDTO dto = new MktSendMailRemoteDTO();
+            List<String> recipientls = new ArrayList<>();
+            recipientls.add(marketEmail.getRecipient());
+            dto.setRecipientls(recipientls);
+            dto.setSubject(marketEmail.getSubject());
+            dto.setContent(marketEmail.getContent());
+            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);
+                log.info("id为:{}", sentMail.get("id"));
+                log.info("status为:{}", sentMail.get("status"));
+                log.info("邮件发送成功,邮件ID:{}", marketEmail.getId());
+            } else {
+                log.error("邮件发送失败,邮件ID:{},错误信息:{}", marketEmail.getId(), result.getMessage());
+            }
+
+//            123
+        }
+        if (!CollectionUtils.isEmpty(marketEmailList)){
+            this.updateBatchById(marketEmailList);
+        }
+    }
+
     /**
      * 调用 AI workflow 并解析调度阶段,带重试。
      * 入参 chatDTO 经 workflowsService.AiWorkflows 返回 Result<Object>,

+ 37 - 29
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;
@@ -61,12 +58,12 @@ public class MarketingCampaignSopDetailServiceImpl
         }
         if (!ObjectUtils.isEmpty(dto.getEmailContent())){
             //写入每个客户的邮件内容
-            this.updateEmailContent(dto.getId(), dto.getEmailContent());
+            this.updateEmailContent(dto.getId(),dto.getEmailSubject(), dto.getEmailContent());
         }
         return Result.result(entity);
     }
 
-    private void updateEmailContent(Long id, String emailContent) {
+    private void updateEmailContent(Long id, String emailSubject, String emailContent) {
         //查询营销活动关联的所有客户
 //        MarketingCampaignSopDetailEntity entity = this.getById(id);
         LambdaQueryWrapper<MarketEmailsEntity> marketEmailsEntityLambdaQueryWrapper = new LambdaQueryWrapper<>();
@@ -123,13 +120,23 @@ public class MarketingCampaignSopDetailServiceImpl
 
         Map<Long,CustomerVO> customerEntityMap = customerVOList.stream().collect(Collectors.toMap(CustomerVO::getId, customerVO -> customerVO));
         for (MarketEmailsEntity marketEmailsEntity : marketEmailsEntityList) {
+            marketEmailsEntity.setSubject(emailSubject);
+            marketEmailsEntity.setStatus(-1);
             CustomerVO customerEntity = customerEntityMap.get(marketEmailsEntity.getCustomerId());
             if (customerEntity != null) {
+                if (ObjectUtils.isEmpty(customerEntity.getCustomerCompany())) {
+                    customerEntity.setCustomerCompany(new CustomerCompanyEntity());
+                }
+
 //                ${areaName}$、${customerIndustry}$
                 //修改emailContent中全部的${customerName}$为customerEntity.getCustomerName()
+                String industry = "";
+                if (!ObjectUtils.isEmpty(customerEntity.getCustomerCompany().getIndustry())){
+                    industry = customerEntity.getCustomerCompany().getIndustry();
+                }
                 emailContent = emailContent.replace("${customerName}$", customerEntity.getCustomerName())
                         .replace("${areaName}$", customerEntity.getCountry())
-                        .replace("${customerIndustry}$", customerEntity.getCustomerCompany().getIndustry())
+                        .replace("${customerIndustry}$", industry)
                         .replace("${yourName}$", yourName)
                         .replace("${yourTitle}$", yourTitle)
                         .replace("${yourCompany}$", yourCompany)
@@ -149,33 +156,34 @@ public class MarketingCampaignSopDetailServiceImpl
         }
 
         //异步写入邮件发送时间及内容
-        this.insertMailMessage(marketEmailsEntityList,customerEntityMap);
+//        this.insertMailMessage(marketEmailsEntityList,customerEntityMap);
     }
 
     @Async
     public void insertMailMessage(List<MarketEmailsEntity> marketEmailsEntityList, Map<Long, CustomerVO> customerEntityMap) {
         for (MarketEmailsEntity marketEmailsEntity : marketEmailsEntityList) {
-            if (ObjectUtils.isEmpty(marketEmailsEntity.getCustomerId()) || ObjectUtils.isEmpty(customerEntityMap.get(marketEmailsEntity.getCustomerId()).getCustomerCompany()) || ObjectUtils.isEmpty(customerEntityMap.get(marketEmailsEntity.getCustomerId()).getCustomerCompany().getEmail())){
-                continue;
-            }
-            if (!ObjectUtils.isEmpty(marketEmailsEntity.getEmailId())){
-                LambdaUpdateWrapper<MktMailMessageEntity> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
-                lambdaUpdateWrapper.eq(MktMailMessageEntity::getId, marketEmailsEntity.getEmailId());
-//                lambdaUpdateWrapper.set(MktMailMessageEntity::getStatus, -1);
-                lambdaUpdateWrapper.set(MktMailMessageEntity::getContent, marketEmailsEntity.getContent());
-                mktMailMessageService.update(lambdaUpdateWrapper);
-            }else {
-                MktMailMessageEntity mktMailMessageEntity = new MktMailMessageEntity();
-                mktMailMessageEntity.setStatus(-1);
-                mktMailMessageEntity.setDelaySendTime(marketEmailsEntity.getExpectSendTime());
-                mktMailMessageEntity.setContent(marketEmailsEntity.getContent());
-                mktMailMessageEntity.setRecipient(customerEntityMap.get(marketEmailsEntity.getCustomerId()).getCustomerCompany().getEmail());
-                mktMailMessageEntity.setIsTrack(1);
-                mktMailMessageService.save(mktMailMessageEntity);
-                if (mktMailMessageEntity.getId() != null) {
-                    marketEmailsEntity.setEmailId(mktMailMessageEntity.getId());
-                }
-            }
+
+//            if (ObjectUtils.isEmpty(marketEmailsEntity.getCustomerId()) || ObjectUtils.isEmpty(customerEntityMap.get(marketEmailsEntity.getCustomerId()).getCustomerCompany()) || ObjectUtils.isEmpty(customerEntityMap.get(marketEmailsEntity.getCustomerId()).getCustomerCompany().getEmail())){
+//                continue;
+//            }
+//            if (!ObjectUtils.isEmpty(marketEmailsEntity.getEmailId())){
+//                LambdaUpdateWrapper<MktMailMessageEntity> lambdaUpdateWrapper = new LambdaUpdateWrapper<>();
+//                lambdaUpdateWrapper.eq(MktMailMessageEntity::getId, marketEmailsEntity.getEmailId());
+////                lambdaUpdateWrapper.set(MktMailMessageEntity::getStatus, -1);
+//                lambdaUpdateWrapper.set(MktMailMessageEntity::getContent, marketEmailsEntity.getContent());
+//                mktMailMessageService.update(lambdaUpdateWrapper);
+//            }else {
+//                MktMailMessageEntity mktMailMessageEntity = new MktMailMessageEntity();
+//                mktMailMessageEntity.setStatus(-1);
+//                mktMailMessageEntity.setDelaySendTime(marketEmailsEntity.getExpectSendTime());
+//                mktMailMessageEntity.setContent(marketEmailsEntity.getContent());
+//                mktMailMessageEntity.setRecipient(customerEntityMap.get(marketEmailsEntity.getCustomerId()).getCustomerCompany().getEmail());
+//                mktMailMessageEntity.setIsTrack(1);
+//                mktMailMessageService.save(mktMailMessageEntity);
+//                if (mktMailMessageEntity.getId() != null) {
+//                    marketEmailsEntity.setEmailId(mktMailMessageEntity.getId());
+//                }
+//            }
         }
         boolean isOk = marketEmailsService.updateBatchById(marketEmailsEntityList);
     }

+ 30 - 0
java/storlead-sasa/storlead-trade/src/main/java/com/storlead/trade/task/MarketEmailsSendSyncTask.java

@@ -0,0 +1,30 @@
+package com.storlead.trade.task;
+
+import com.storlead.trade.service.MarketEmailsService;
+import com.storlead.trade.service.MarketEmailsStatusSyncService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.Resource;
+
+/**
+ * 定时发送邮件
+ */
+@Slf4j
+@Component
+public class MarketEmailsSendSyncTask {
+
+    @Resource
+    private MarketEmailsService marketEmailsService;
+
+    @Scheduled(cron = "0 */1 * * * ?")
+    public void sync() {
+        long start = System.currentTimeMillis();
+        try {
+            marketEmailsService.send();
+        } catch (Exception e) {
+            log.error("mkt_mail_status → market_emails 同步失败", e);
+        }
+    }
+}

+ 5 - 0
ui/smarttrade-platform/.env.development

@@ -4,3 +4,8 @@ VITE_API_BASE_URL=http://localhost:10010/
 VITE_APP_BASE_URL = http://localhost:3000/
 
 VITE_API_APP_ID=7
+
+# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
+VITE_APP_RSA_PUBLIC_KEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCOlrKBglhJkU/bfgEEv7zuo4y5RiScg3XeDTBrHHePoixDOvyvBsiAc8b33bjin9j77EzRusJT2jjJV1hnHIbu5HkMd3T5JK00gMviCiSbfCkS4/tf3CnFMWLn31YcBgdCOFoiKh/JpGEvTx192LWSKNupDkR6bgpcRvWr5Yw2swIDAQAB'
+# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
+VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='

+ 5 - 0
ui/smarttrade-platform/.env.production

@@ -4,3 +4,8 @@ VITE_API_BASE_URL=/
 VITE_APP_BASE_URL = https://trade.storlead.com/
 
 VITE_API_APP_ID=7
+
+# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
+VITE_APP_RSA_PUBLIC_KEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCOlrKBglhJkU/bfgEEv7zuo4y5RiScg3XeDTBrHHePoixDOvyvBsiAc8b33bjin9j77EzRusJT2jjJV1hnHIbu5HkMd3T5JK00gMviCiSbfCkS4/tf3CnFMWLn31YcBgdCOFoiKh/JpGEvTx192LWSKNupDkR6bgpcRvWr5Yw2swIDAQAB'
+# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
+VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='

+ 6 - 1
ui/smarttrade-platform/.env.test

@@ -3,4 +3,9 @@ VITE_API_BASE_URL=/
 
 VITE_APP_BASE_URL = https://trade.test.storlead.com/
 
-VITE_API_APP_ID=7
+VITE_API_APP_ID=7
+
+# 接口加密传输 RSA 公钥与后端解密私钥对应 如更换需前后端一同更换
+VITE_APP_RSA_PUBLIC_KEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCOlrKBglhJkU/bfgEEv7zuo4y5RiScg3XeDTBrHHePoixDOvyvBsiAc8b33bjin9j77EzRusJT2jjJV1hnHIbu5HkMd3T5JK00gMviCiSbfCkS4/tf3CnFMWLn31YcBgdCOFoiKh/JpGEvTx192LWSKNupDkR6bgpcRvWr5Yw2swIDAQAB'
+# 接口响应解密 RSA 私钥与后端加密公钥对应 如更换需前后端一同更换
+VITE_APP_RSA_PRIVATE_KEY = 'MIIBVAIBADANBgkqhkiG9w0BAQEFAASCAT4wggE6AgEAAkEAmc3CuPiGL/LcIIm7zryCEIbl1SPzBkr75E2VMtxegyZ1lYRD+7TZGAPkvIsBcaMs6Nsy0L78n2qh+lIZMpLH8wIDAQABAkEAk82Mhz0tlv6IVCyIcw/s3f0E+WLmtPFyR9/WtV3Y5aaejUkU60JpX4m5xNR2VaqOLTZAYjW8Wy0aXr3zYIhhQQIhAMfqR9oFdYw1J9SsNc+CrhugAvKTi0+BF6VoL6psWhvbAiEAxPPNTmrkmrXwdm/pQQu3UOQmc2vCZ5tiKpW10CgJi8kCIFGkL6utxw93Ncj4exE/gPLvKcT+1Emnoox+O9kRXss5AiAMtYLJDaLEzPrAWcZeeSgSIzbL+ecokmFKSDDcRske6QIgSMkHedwND1olF8vlKsJUGK3BcdtM8w4Xq7BpSBwsloE='

+ 6 - 0
ui/smarttrade-platform/package-lock.json

@@ -11,6 +11,7 @@
         "@fortawesome/fontawesome-free": "^6.5.1",
         "axios": "^1.6.7",
         "element-plus": "^2.14.2",
+        "jsencrypt": "3.3.1",
         "moment": "^2.30.1",
         "pinia": "^2.1.7",
         "vue": "^3.4.21",
@@ -1450,6 +1451,11 @@
         "node": ">= 6"
       }
     },
+    "node_modules/jsencrypt": {
+      "version": "3.3.1",
+      "resolved": "https://registry.npmjs.org/jsencrypt/-/jsencrypt-3.3.1.tgz",
+      "integrity": "sha512-dVvV54GdFuJgmEKn+oBiaifDMen4p6o6j/lJh0OVMcouME8sST0bJ7bldIgKBQk4za0zyGn0/pm4vOznR25mLw=="
+    },
     "node_modules/lodash": {
       "version": "4.18.1",
       "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",

+ 2 - 1
ui/smarttrade-platform/package.json

@@ -17,7 +17,8 @@
     "moment": "^2.30.1",
     "pinia": "^2.1.7",
     "vue": "^3.4.21",
-    "vue-router": "^4.3.0"
+    "vue-router": "^4.3.0",
+    "jsencrypt": "3.3.1"
   },
   "devDependencies": {
     "@vitejs/plugin-vue": "^5.0.4",

+ 4 - 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')
@@ -18,6 +18,6 @@ export const getEmailList = (data = {}) => get('/router/rest/marketEmails/manage
 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/sopDetail/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 saveGenerateEmail = (data = {}) => post('/router/rest/marketing/campaign/sop-detail/edit', data)
+export const getEmailDetail = (data = {}) => get('/router/rest/marketEmails/getById', data)
+export const editEmail = (data = {}) => post('/router/rest/marketEmails/editContent', data)

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

@@ -328,6 +328,7 @@
 .status-badge.info { background: rgba(10, 10, 10, 0.2); color: white; }
 .status-badge.active { background: rgba(16, 185, 129, 0.2); color: var(--success); }
 .status-badge.paused { background: rgba(245, 158, 11, 0.2); color: var(--warning); }
+.status-badge.finished { background: rgba(99, 102, 241, 0.2); color: var(--primary); }
 .card-title { font-size: 15px; font-weight: 600; margin-bottom: 6px; }
 .card-desc { font-size: 12px; color: var(--text-secondary); margin-bottom: 12px; }
 .card-channels { display: flex; gap: 6px; margin-bottom: 12px; }

+ 7 - 2
ui/smarttrade-platform/src/components/addMarketing.vue

@@ -522,10 +522,10 @@
                             </div>
                             <div class="preview-card">
                                 <div class="preview-icon"><i class="fas fa-user"></i></div>
-                                <div class="preview-info">
+                                <div class="preview-info" style="flex: 1">
                                     <div class="preview-label">负责人</div>
                                     <div class="preview-value">
-                                        {{ getOwnerName() }}
+                                        <el-text style="color: #FFF" line-clamp="2">{{ getOwnerName() }}</el-text>
                                     </div>
                                 </div>
                             </div>
@@ -898,6 +898,11 @@
             ElMessage.error('请完成确认事项后创建活动')
             return
         }
+        if (customerList.value.length == 0) {
+            currentStep.value = 2
+            ElMessage.error('请选择目标客户')
+            return
+        }
         try {
             const typeMap = { prospect: 1, nurture: 2, reactivate: 3, promote: 4, seasonal: 5, exhibition: 6, other: 7 }
             const priorityMap = { urgent: 1, high: 2, medium: 3, low: 4 }

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

@@ -330,6 +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.js";
 
 const route = useRoute()
 const router = useRouter()
@@ -450,7 +451,12 @@ const rules = ref({
 const submitForm = () => {
     resetPwdformRef.value?.validate((valid) => {
         if (valid) {
-            updateUserPwd(pwdForm.value).then(res => {
+            let params = {
+              password: encrypt(pwdForm.value.password),
+              newPass: encrypt(pwdForm.value.newPass),
+              newPassAgain: encrypt(pwdForm.value.newPassAgain)
+            }
+            updateUserPwd(params).then(res => {
               localStorage.clear()
               ElMessage.success("修改成功");
               router.push({ path: '/login' })

+ 2 - 2
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>
 

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

@@ -82,6 +82,10 @@
                     <span>${email}$</span>
                 </div>
               </div>
+              <div class="subject-div">
+                <span>邮件主题:</span>
+                <el-input v-model="emailSubject" style="flex: 1" />
+              </div>
               <div class="msg-item">
                     <div class="msg-item-left">
                         <ckeditor
@@ -176,10 +180,11 @@
 
     const marketingData = ref()
     const typeData = ref([])
+    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: '',
@@ -190,6 +195,7 @@
             extra: ''
         }
         emailContent.value = ''
+        emailSubject.value = ''
         showAIModal.value = true
     }
 
@@ -298,6 +304,9 @@
             // 3. 循环读取流式数据
             let buffer = ''; // 缓冲区
             let doneData = '<p>'
+            let subject = ''
+            let waitingForSubject = false
+            let readingSubject = false
             while (true) {
                 const { done, value } = await reader.read();
                 if (done) break;
@@ -315,16 +324,47 @@
                     if (!trimmedLine || trimmedLine.startsWith(':')) continue;
 
                     if (trimmedLine === 'data: [DONE]') {
-                    console.log('流传输结束');
-                    doneData += '</p>'
-                    return;
+                        console.log('流传输结束');
+                        doneData += '</p>'
+                        return;
                     }
 
                     if (trimmedLine.startsWith('data:')) {
                         const jsonStr = trimmedLine.replace('data:', '');
                         try {
                             const data = JSON.parse(jsonStr);
-                            if(data.trim()){
+                            const trimmedData = data.trim()
+                            if (trimmedData === 'Subject' || trimmedData === 'Subject:') {
+                                waitingForSubject = true
+                                continue
+                            }
+
+                            if (waitingForSubject) {
+                                waitingForSubject = false
+                                readingSubject = true
+                            }
+
+                            if (readingSubject) {
+                                if (!data) {
+                                    readingSubject = false
+                                    continue
+                                }
+
+                                const newlineIndex = data.indexOf('\n')
+                                if (newlineIndex === -1) {
+                                    subject += data
+                                    continue
+                                }
+
+                                subject += data.slice(0, newlineIndex)
+                                readingSubject = false
+                                const contentAfterSubject = data.slice(newlineIndex + 1)
+                                if (contentAfterSubject.trim()) {
+                                    doneData += contentAfterSubject
+                                } else {
+                                    doneData += '</p><p>'
+                                }
+                            } else if(trimmedData){
                                 doneData += data
                             }else{
                                 doneData += '</p><p>'
@@ -335,7 +375,8 @@
                     }
                 }
             }
-            emailContent.value = doneData
+            emailContent.value = doneData.replace('<p></p>', '')
+            emailSubject.value = subject.replace(': ', '')
             isGenerating.value = false
             ElMessage.success('生成成功')
         } catch (error) {
@@ -345,7 +386,8 @@
     const useEmail = () => {
         let params = {
             id: typeData.value.filter(item => item.name == aiGenerator.value.inputs.scene)[0].id,
-            emailContent: emailContent.value
+            emailContent: emailContent.value,
+            emailSubject: emailSubject.value,
         }
         saveGenerateEmail(params).then(res => {
             ElMessage.success('保存成功')
@@ -361,6 +403,7 @@
 <style scoped>
 @import '../../assets/styles/marketing.css';
 
+.ai-modal :deep(.el-input__wrapper),
 .ai-modal :deep(.el-textarea__inner),
 .ai-modal :deep(.el-select .el-select__wrapper) {
     background: var(--bg-dark);
@@ -379,6 +422,7 @@
     background: var(--primary);
     border-color: var(--primary);
 }
+.ai-modal :deep(.el-input__inner),
 .ai-modal :deep(.el-checkbox__label) {
     color: var(--text-primary);
 }
@@ -498,4 +542,9 @@
 .msg-item .ai-button :deep(.el-button):hover{
     background: #3441569a;
 }
+.subject-div{
+    display: flex;
+    align-items: center;
+    margin-top: 16px;
+}
 </style>

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

@@ -163,7 +163,7 @@ const editEmail = (item) => {
   // 这里可以接入编辑邮件逻辑
   const { href } = router.resolve({
     path: '/emails/edit',
-    query: { id: item.emailId }
+    query: { id: item.id }
   })
   window.open(href, '_blank')
 }

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

@@ -29,7 +29,9 @@
               </div>
               <div class="detail-item">
                 <label>负责人</label>
-                <span>{{ supervisorUser }}</span>
+                <span>
+                  <el-text style="color: #FFF" line-clamp="2">{{ supervisorUser }}</el-text>
+                </span>
               </div>
               <div class="detail-item">
                 <label>创建时间</label>

+ 24 - 0
ui/smarttrade-platform/src/utils/jsencrypt.js

@@ -0,0 +1,24 @@
+import JSEncrypt from 'jsencrypt';
+// 密钥对生成 http://web.chacuo.net/netrsakeypair
+
+const publicKey = import.meta.env.VITE_APP_RSA_PUBLIC_KEY;
+
+// 前端不建议存放私钥 不建议解密数据 因为都是透明的意义不大
+const privateKey = import.meta.env.VITE_APP_RSA_PRIVATE_KEY;
+
+// 加密
+export const encrypt = (txt) => {
+    if(!!!txt) {
+        throw new Error('加密数据不能为空')
+    }
+    const encryptor = new JSEncrypt();
+    encryptor.setPublicKey(publicKey); // 设置公钥
+    return encryptor.encrypt(txt); // 对数据进行加密
+};
+
+// 解密
+export const decrypt = (txt) => {
+    const encryptor = new JSEncrypt();
+    encryptor.setPrivateKey(privateKey); // 设置私钥
+    return encryptor.decrypt(txt); // 对数据进行解密
+};

+ 4 - 4
ui/smarttrade-platform/src/views/MarketingView.vue

@@ -84,7 +84,7 @@
           <div class="card-icon" :style="{ background: 'rgba(99, 102, 241, 0.2)' }">
             <i class="fas fa-globe" :style="{ color: 'var(--primary)' }"></i>
           </div>
-          <span class="status-badge" :class="campaign.status == 0 ? 'info' : campaign.status == 1 ? 'active' : 'paused'">{{ getStatusLabel(campaign.status || 0) }}</span>
+          <span class="status-badge" :class="campaign.status == 0 ? 'info' : campaign.status == 1 ? 'active' : campaign.status == 2 ? 'paused' : 'finished'">{{ getStatusLabel(campaign.status || 0) }}</span>
         </div>
         <div class="card-title">{{ campaign.name }}</div>
         <div class="card-desc">{{ campaign.description }}</div>
@@ -95,15 +95,15 @@
         </div>
         <div class="card-stats">
           <div class="stat-item">
-            <span class="stat-value">{{ formatNumber(campaign.sendVolume) }}</span>
+            <span class="stat-value">{{ campaign.sendVolume || 0 }}</span>
             <span class="stat-label">已发送</span>
           </div>
           <div class="stat-item">
-            <span class="stat-value success">{{ campaign.openRate }}%</span>
+            <span class="stat-value success">{{ campaign.openRate || 0 }}%</span>
             <span class="stat-label">打开率</span>
           </div>
           <div class="stat-item">
-            <span class="stat-value primary">{{ campaign.replyRate }}%</span>
+            <span class="stat-value primary">{{ campaign.replyRate || 0 }}%</span>
             <span class="stat-label">回复率</span>
           </div>
         </div>

+ 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 || '保存失败')