Explorar o código

Merge remote-tracking branch 'origin/master'

yuhongqi hai 2 días
pai
achega
15d6ee1edb
Modificáronse 40 ficheiros con 1628 adicións e 112 borrados
  1. 2 2
      fs-cid-workflow/src/main/java/com/fs/app/controller/CommonController.java
  2. 12 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwSopController.java
  3. 2 2
      fs-company/src/main/java/com/fs/company/controller/qw/SopUserLogsInfoController.java
  4. 96 0
      fs-company/src/main/java/com/fs/company/controller/qw/WxSopUserLogsController.java
  5. 26 0
      fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopLogsController.java
  6. 3 4
      fs-qwhook-sop/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java
  7. 16 1
      fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java
  8. 41 2
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerServiceImpl.java
  9. 2 1
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java
  10. 2 1
      fs-service/src/main/java/com/fs/qw/service/IQwExternalContactService.java
  11. 3 2
      fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java
  12. 40 0
      fs-service/src/main/java/com/fs/sop/params/SopUserLogsWxParam.java
  13. 33 0
      fs-service/src/main/java/com/fs/sop/params/WxSopTagsParam.java
  14. 1 1
      fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java
  15. 63 0
      fs-service/src/main/java/com/fs/sop/vo/WxFilterSopCustomersResult.java
  16. 10 7
      fs-service/src/main/java/com/fs/system/service/ISysDictDataService.java
  17. 7 0
      fs-service/src/main/java/com/fs/wx/sop/domain/WxSop.java
  18. 6 0
      fs-service/src/main/java/com/fs/wx/sop/domain/WxSopUser.java
  19. 9 0
      fs-service/src/main/java/com/fs/wx/sop/domain/WxSopUserInfo.java
  20. 10 0
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopLogsMapper.java
  21. 36 7
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopMapper.java
  22. 11 0
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserInfoMapper.java
  23. 19 8
      fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserMapper.java
  24. 57 0
      fs-service/src/main/java/com/fs/wx/sop/params/WxSopLogsParam.java
  25. 42 0
      fs-service/src/main/java/com/fs/wx/sop/service/IWxSopExecuteService.java
  26. 10 0
      fs-service/src/main/java/com/fs/wx/sop/service/IWxSopLogsService.java
  27. 9 0
      fs-service/src/main/java/com/fs/wx/sop/service/IWxSopService.java
  28. 559 0
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopExecuteServiceImpl.java
  29. 15 0
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopLogsServiceImpl.java
  30. 93 14
      fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopServiceImpl.java
  31. 102 0
      fs-service/src/main/java/com/fs/wx/sop/vo/WxSopLogsListVO.java
  32. 34 0
      fs-service/src/main/resources/db/20260226-个微SOP表结构.sql
  33. 1 1
      fs-service/src/main/resources/mapper/crm/CrmCustomerMapper.xml
  34. 2 1
      fs-service/src/main/resources/mapper/qw/QwExternalContactMapper.xml
  35. 17 17
      fs-service/src/main/resources/mapper/sopUserLogsWx/SopUserLogsWxMapper.xml
  36. 74 0
      fs-service/src/main/resources/mapper/wx/WxSopLogsMapper.xml
  37. 72 8
      fs-service/src/main/resources/mapper/wx/WxSopMapper.xml
  38. 44 18
      fs-service/src/main/resources/mapper/wx/WxSopUserInfoMapper.xml
  39. 24 7
      fs-service/src/main/resources/mapper/wx/WxSopUserMapper.xml
  40. 23 8
      fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java

+ 2 - 2
fs-cid-workflow/src/main/java/com/fs/app/controller/CommonController.java

@@ -22,12 +22,12 @@ public class CommonController {
 
     private final CidWorkflowTaskService taskService;
 
-    @GetMapping("runCidWorkflow")
+    @GetMapping("/runCidWorkflow")
     public void runCidWorkflow() {
         taskService.runCidWorkflow();
     }
 
-    @GetMapping("activateTimeAvailableTask")
+    @GetMapping("/activateTimeAvailableTask")
     public void activateTimeAvailableTask() {
         taskService.activateTimeAvailableTask();
     }

+ 12 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwSopController.java

@@ -30,6 +30,7 @@ import com.fs.sop.service.IQwSopService;
 import com.fs.sop.service.IQwSopTempContentService;
 import com.fs.sop.service.IQwSopTempVoiceService;
 import com.fs.sop.vo.SopVoiceListVo;
+import com.fs.wx.sop.service.IWxSopService;
 import org.apache.commons.beanutils.ConvertUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.security.access.prepost.PreAuthorize;
@@ -81,6 +82,8 @@ public class QwSopController extends BaseController
     @Autowired
     private IQwSopTempVoiceService voiceService;
 
+    @Autowired
+    private IWxSopService  wxSopService;
     /**
      * 查询企微sop列表
      */
@@ -381,6 +384,15 @@ public class QwSopController extends BaseController
         return qwSopService.updateStatusQwSopByIds(ids);
     }
 
+    /**
+     * 批量执行个微SOP
+     */
+    @GetMapping(value = "/updateWxStatus/{ids}")
+    public R batchDoWxSop(@PathVariable Long[] ids)
+    {
+        return wxSopService.updateStatusWxSopByIds(ids);
+    }
+
     /**
      * 修改sop员工
      */

+ 2 - 2
fs-company/src/main/java/com/fs/company/controller/qw/SopUserLogsInfoController.java

@@ -516,7 +516,7 @@ public class SopUserLogsInfoController extends BaseController
     }
 
     /**
-     * 一键群发sopUserLogsInfo
+     * 营期一键群发sopUserLogsInfo
      */
     @PreAuthorize("@ss.hasPermi('qw:sopUserLogsInfo:msgSchedule')")
     @Log(title = "sendUserLogsInfoMsgType", businessType = BusinessType.INSERT,isSaveRequestData=false)
@@ -528,7 +528,7 @@ public class SopUserLogsInfoController extends BaseController
     }
 
     /**
-     * 一键群发sopUserLogsInfo
+     * sop一键群发sopUserLogsInfo
      */
     @PreAuthorize("@ss.hasPermi('qw:sopUserLogsInfo:msgSop')")
     @Log(title = "sendUserLogsInfoMsgSop", businessType = BusinessType.INSERT,isSaveRequestData=false)

+ 96 - 0
fs-company/src/main/java/com/fs/company/controller/qw/WxSopUserLogsController.java

@@ -0,0 +1,96 @@
+package com.fs.company.controller.qw;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.company.domain.CompanyUser;
+import com.fs.company.service.ICompanyUserService;
+import com.fs.wx.sop.domain.WxSopUser;
+import com.fs.wx.sop.domain.WxSopUserInfo;
+import com.fs.wx.sop.service.IWxSopUserService;
+import com.fs.wx.sop.service.IWxSopUserInfoService;
+import com.fs.wx.sop.service.IWxSopService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+/**
+ * 个微sop营期Controller
+ *
+ * @author fs
+ * @date 2025-03-12
+ */
+@RestController
+@RequestMapping("/wxSop/sopUserLogsWx")
+public class WxSopUserLogsController extends BaseController
+{
+    @Autowired
+    private IWxSopUserService wxSopUserService;
+
+    @Autowired
+    private IWxSopUserInfoService wxSopUserInfoService;
+
+    @Autowired
+    private IWxSopService wxSopService;
+
+    @Autowired
+    private ICompanyUserService companyUserService;
+
+
+    /**
+     * 查询个微sop营期列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:sop:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(WxSopUser wxSopUser)
+    {
+        startPage();
+        List<WxSopUser> list = wxSopUserService.selectWxSopUserList(wxSopUser);
+
+        return getDataTable(list);
+    }
+
+    /**
+     * 获取个微sop营期详细信息
+     */
+    @PreAuthorize("@ss.hasPermi('qw:sop:list')")
+    @GetMapping(value = "/{id}")
+    public AjaxResult getInfo(@PathVariable("id") Long id)
+    {
+        WxSopUser wxSopUser = wxSopUserService.selectWxSopUserById(id);
+        return AjaxResult.success(wxSopUser);
+    }
+
+    /**
+     * 根据SOP ID查询营期列表
+     */
+    @PreAuthorize("@ss.hasPermi('qw:sop:list')")
+    @GetMapping("/listBySopId/{sopId}")
+    public TableDataInfo listBySopId(@PathVariable("sopId") Long sopId)
+    {
+        startPage();
+        WxSopUser queryParam = new WxSopUser();
+        queryParam.setSopId(sopId);
+        List<WxSopUser> list = wxSopUserService.selectWxSopUserList(queryParam);
+
+        return getDataTable(list);
+    }
+
+    /**
+     * 查询营期详情(客户列表)
+     * @param sopUserId 营期ID
+     */
+    @PreAuthorize("@ss.hasPermi('qw:sop:list')")
+    @GetMapping("/detail/{sopUserId}")
+    public TableDataInfo detail(@PathVariable("sopUserId") Long sopUserId)
+    {
+        startPage();
+        WxSopUserInfo queryParam = new WxSopUserInfo();
+        queryParam.setSopUserId(sopUserId);
+        List<WxSopUserInfo> list = wxSopUserInfoService.selectWxSopUserInfoList(queryParam);
+
+        return getDataTable(list);
+    }
+}

+ 26 - 0
fs-company/src/main/java/com/fs/company/controller/wx/controller/WxSopLogsController.java

@@ -1,6 +1,7 @@
 package com.fs.company.controller.wx.controller;
 
 import java.util.List;
+import javax.servlet.http.HttpServletResponse;
 import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.GetMapping;
@@ -16,6 +17,8 @@ import com.fs.common.core.controller.BaseController;
 import com.fs.common.core.domain.AjaxResult;
 import com.fs.common.enums.BusinessType;
 import com.fs.wx.sop.domain.WxSopLogs;
+import com.fs.wx.sop.params.WxSopLogsParam;
+import com.fs.wx.sop.vo.WxSopLogsListVO;
 import com.fs.wx.sop.service.IWxSopLogsService;
 import com.fs.common.utils.poi.ExcelUtil;
 import com.fs.common.core.page.TableDataInfo;
@@ -94,4 +97,27 @@ public class WxSopLogsController extends BaseController
     {
         return toAjax(wxSopLogsService.deleteWxSopLogsByIds(ids));
     }
+
+    /**
+     * 查询个微SOP执行记录列表(带关联信息)
+     */
+    @GetMapping("/listCVO")
+    public TableDataInfo listCVO(WxSopLogsParam param)
+    {
+        startPage();
+        List<WxSopLogsListVO> list = wxSopLogsService.selectWxSopLogsListBySopId(param);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出个微SOP执行记录列表
+     */
+    @Log(title = "个微SOP执行记录", businessType = BusinessType.EXPORT)
+    @PostMapping("/exportCVO")
+    public void exportCVO(HttpServletResponse response, WxSopLogsParam param) throws Exception
+    {
+        List<WxSopLogsListVO> list = wxSopLogsService.selectWxSopLogsListBySopId(param);
+        ExcelUtil<WxSopLogsListVO> util = new ExcelUtil<WxSopLogsListVO>(WxSopLogsListVO.class);
+        util.exportExcel(response, list, "个微SOP执行记录数据");
+    }
 }

+ 3 - 4
fs-qwhook-sop/src/main/java/com/fs/app/controller/FsUserCourseVideoController.java

@@ -124,8 +124,8 @@ public class FsUserCourseVideoController {
         if (qwUser == null || qwUser.getCompanyId() == null) {
             return R.error("员工未绑定 销售公司 或 未获取到员工信息,请重试!");
         }
-        String externalUserId = param.getExternalUserId();
-        QwExternalContact qwExternalContact = qwExternalContactService.selectQwExternalContactByExternalUserIdSidebar(externalUserId, param.getCorpId());
+        param.setCompanyUserId(qwUser.getCompanyUserId());
+        QwExternalContact qwExternalContact = qwExternalContactService.selectQwExternalContactByExternalUserIdSidebar(param);
 
         if(qwExternalContact == null || qwExternalContact.getFsUserId() == null){
             return R.error("用户未绑定,暂时无法下单");
@@ -293,8 +293,7 @@ public class FsUserCourseVideoController {
     @GetMapping("/getStoreOrderListBySidebar")
     public R getStoreOrderListBySidebar(FsStoreOrderScrmSidebarVO param){
 
-        String externalUserId = param.getExternalUserId();
-        QwExternalContact qwExternalContact = qwExternalContactService.selectQwExternalContactByExternalUserIdSidebar(externalUserId, param.getCorpId());
+        QwExternalContact qwExternalContact = qwExternalContactService.selectQwExternalContactByExternalUserIdSidebar(param);
 
         if(qwExternalContact == null || qwExternalContact.getFsUserId() == null){
             return R.error("用户未绑定,无法查询订单");

+ 16 - 1
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java

@@ -30,6 +30,7 @@ import com.fs.crm.param.SmsSendBatchParam;
 import com.fs.crm.service.impl.CrmCustomerServiceImpl;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.mapper.QwUserMapper;
+import com.fs.qw.service.impl.QwExternalContactServiceImpl;
 import com.fs.system.mapper.SysDictDataMapper;
 import com.fs.system.service.ISysConfigService;
 import lombok.RequiredArgsConstructor;
@@ -106,6 +107,8 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
     private final CompanyAiWorkflowServerMapper companyAiWorkflowServerMapper;
     private final QwUserMapper qwUserMapper;
     private final EasyCallMapper easyCallMapper;
+    private final QwExternalContactServiceImpl qwExternalContactService;
+
     /**
      * 查询机器人外呼任务
      *
@@ -430,8 +433,20 @@ public class CompanyVoiceRoboticServiceImpl extends ServiceImpl<CompanyVoiceRobo
             //构建短信参数
             CompanyWxClient wxClient = companyWxClientMapper.selectOneByRoboticIdAndUserId(roboticId, callees.getUserId());
             CompanyVoiceRoboticWx wx = companyVoiceRoboticWxService.getById(wxClient.getRoboticWxId());
-            CompanyWxAccount wxAccount = companyWxAccountService.selectCompanyWxAccountById(wxClient.getAccountId());
+
+            CompanyWxAccount wxAccount=new CompanyWxAccount();
+            if (wxClient.getIsWeCom()==2){
+                QwUser  qwUserByRedis = qwExternalContactService.getQwUserByRedisForId(String.valueOf(wxClient.getAccountId()));
+                wxAccount.setCompanyId(qwUserByRedis.getCompanyId());
+                wxAccount.setCompanyUserId(qwUserByRedis.getCompanyUserId());
+                wxAccount.setWxNickName(qwUserByRedis.getQwUserName());
+            }else {
+                 wxAccount = companyWxAccountService.selectCompanyWxAccountById(wxClient.getAccountId());
+            }
+
+
             CompanySmsTemp temp = smsTempService.selectCompanySmsTempById(smsTempId);
+
             if (temp != null && temp.getStatus().equals(1) && temp.getIsAudit().equals(1)) {
                 CompanySms sms=companySmsService.selectCompanySmsByCompanyId(wxAccount.getCompanyId());
                 if(sms!=null){

+ 41 - 2
fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerServiceImpl.java

@@ -33,7 +33,9 @@ import com.fs.jpush.service.JpushService;
 import com.fs.qwApi.param.QwCustomerDetailParam;
 import com.fs.system.service.ISysDictDataService;
 import com.fs.system.service.ISysDictTypeService;
+import com.fs.wx.sop.service.IWxSopExecuteService;
 import lombok.Synchronized;
+import lombok.extern.slf4j.Slf4j;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
@@ -52,6 +54,7 @@ import java.util.stream.Collectors;
  * @date 2022-12-21
  */
 @Service
+@Slf4j
 public class CrmCustomerServiceImpl extends ServiceImpl<CrmCustomerMapper, CrmCustomer> implements ICrmCustomerService
 {
     @Autowired
@@ -77,6 +80,8 @@ public class CrmCustomerServiceImpl extends ServiceImpl<CrmCustomerMapper, CrmCu
     @Autowired
     private CrmCustomerAssignMapper assignMapper;
     @Autowired
+    private IWxSopExecuteService wxSopExecuteService;
+    @Autowired
     private JpushService jpushService;
     @Autowired
     private CrmIndexDataMapper crmIndexDataMapper;
@@ -153,8 +158,31 @@ public class CrmCustomerServiceImpl extends ServiceImpl<CrmCustomerMapper, CrmCu
     @Override
     public int updateCrmCustomer(CrmCustomer crmCustomer)
     {
+        // 检查是否修改了标签
+        boolean isTagsChanged = false;
+        if (crmCustomer.getTags() != null && crmCustomer.getCustomerId() != null) {
+            CrmCustomer oldCustomer = crmCustomerMapper.selectCrmCustomerById(crmCustomer.getCustomerId());
+            if (oldCustomer != null) {
+                String oldTags = oldCustomer.getTags() == null ? "" : oldCustomer.getTags();
+                String newTags = crmCustomer.getTags();
+                isTagsChanged = !oldTags.equals(newTags);
+            }
+        }
+
         crmCustomer.setUpdateTime(DateUtils.getNowDate());
-        return crmCustomerMapper.updateCrmCustomer(crmCustomer);
+        int result = crmCustomerMapper.updateCrmCustomer(crmCustomer);
+
+        // 如果标签发生变更,触发SOP营期动态管理
+        if (result > 0 && isTagsChanged) {
+            try {
+                wxSopExecuteService.processCustomerTagsChange(crmCustomer.getCustomerId());
+            } catch (Exception e) {
+                log.error("处理客户标签变更后的SOP营期管理失败", e);
+                // 不影响主流程,只记录错误
+            }
+        }
+
+        return result;
     }
 
 
@@ -494,7 +522,18 @@ public class CrmCustomerServiceImpl extends ServiceImpl<CrmCustomerMapper, CrmCu
         CrmCustomer map=new CrmCustomer();
         map.setCustomerId(param.getCustomerId());
         map.setTags(param.getTags());
-        return crmCustomerMapper.updateCrmCustomer(map)>0? R.ok("操作成功"): R.error("操作失败");
+        int result = crmCustomerMapper.updateCrmCustomer(map);
+        if (result > 0) {
+            // 标签修改成功后,触发SOP营期动态管理
+            try {
+                wxSopExecuteService.processCustomerTagsChange(param.getCustomerId());
+            } catch (Exception e) {
+                log.error("处理客户标签变更后的SOP营期管理失败", e);
+                // 不影响主流程,只记录错误
+            }
+            return R.ok("操作成功");
+        }
+        return R.error("操作失败");
     }
 
     @Override

+ 2 - 1
fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java

@@ -4,6 +4,7 @@ import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
 import com.fs.fastGpt.domain.FastgptChatArtificialWords;
+import com.fs.hisStore.vo.FsStoreOrderScrmSidebarVO;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwUserDelLossLog;
 import com.fs.qw.param.*;
@@ -649,7 +650,7 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
             "</script>")
     public int batchUpdateQwExternalContactMandatoryRegistration(@Param("map") List<QwMandatoryRegistrParam> batchList);
 
-    QwExternalContact selectQwExternalContactByExternalUserIdSidebar(@Param("externalUserId") String externalUserId, @Param("corpId") String corpId);
+    QwExternalContact selectQwExternalContactByExternalUserIdSidebar(@Param("param") FsStoreOrderScrmSidebarVO param);
 
     @Select("SELECT " +
             "id " +

+ 2 - 1
fs-service/src/main/java/com/fs/qw/service/IQwExternalContactService.java

@@ -4,6 +4,7 @@ package com.fs.qw.service;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.common.core.domain.R;
 import com.fs.course.param.FsCourseListBySidebarParam;
+import com.fs.hisStore.vo.FsStoreOrderScrmSidebarVO;
 import com.fs.qw.domain.QwExternalContact;
 import com.fs.qw.domain.QwUser;
 import com.fs.qw.param.*;
@@ -269,5 +270,5 @@ public interface IQwExternalContactService extends IService<QwExternalContact> {
     /**
      * 企微用户-查询外部联系人信息
      */
-    QwExternalContact selectQwExternalContactByExternalUserIdSidebar(String externalUserId, String corpId);
+    QwExternalContact selectQwExternalContactByExternalUserIdSidebar(FsStoreOrderScrmSidebarVO param);
 }

+ 3 - 2
fs-service/src/main/java/com/fs/qw/service/impl/QwExternalContactServiceImpl.java

@@ -34,6 +34,7 @@ import com.fs.crm.mapper.CrmCustomerMapper;
 import com.fs.his.domain.FsUser;
 import com.fs.his.mapper.FsUserMapper;
 import com.fs.his.utils.PhoneUtil;
+import com.fs.hisStore.vo.FsStoreOrderScrmSidebarVO;
 import com.fs.qw.domain.*;
 import com.fs.qw.mapper.*;
 import com.fs.qw.param.*;
@@ -6011,8 +6012,8 @@ public class QwExternalContactServiceImpl extends ServiceImpl<QwExternalContactM
     }
 
     @Override
-    public QwExternalContact selectQwExternalContactByExternalUserIdSidebar(String externalUserId, String corpId) {
-        return qwExternalContactMapper.selectQwExternalContactByExternalUserIdSidebar(externalUserId,corpId);
+    public QwExternalContact selectQwExternalContactByExternalUserIdSidebar(FsStoreOrderScrmSidebarVO param) {
+        return qwExternalContactMapper.selectQwExternalContactByExternalUserIdSidebar(param);
     }
 
     @Override

+ 40 - 0
fs-service/src/main/java/com/fs/sop/params/SopUserLogsWxParam.java

@@ -0,0 +1,40 @@
+package com.fs.sop.params;
+
+import com.fs.sop.domain.SopUserLogsWx;
+import lombok.Data;
+
+/**
+ * 个微sop营期参数对象 SopUserLogsWx
+ *
+ * @author fs
+ * @date 2025-03-12
+ */
+@Data
+public class SopUserLogsWxParam extends SopUserLogsWx
+{
+    /**
+     * 每页显示记录数
+     */
+    private Integer pageSize = 10;
+
+    /**
+     * 页码
+     */
+    private Integer pageNum = 1;
+
+    /**
+     * 排序列
+     */
+    private String orderByColumn;
+
+    /**
+     * 排序的方向(desc, asc)
+     */
+    private String isAsc = "desc";
+
+    /**
+     * 是否需要分页
+     */
+    private Boolean needPage = true;
+
+}

+ 33 - 0
fs-service/src/main/java/com/fs/sop/params/WxSopTagsParam.java

@@ -0,0 +1,33 @@
+package com.fs.sop.params;
+
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 个微SOP标签筛选参数类
+ *
+ * @author fs
+ * @date 2025-03-12
+ */
+@Data
+public class WxSopTagsParam {
+
+    /**
+     * 执行账号ID集合
+     */
+    private List<String> accountIdsSelectList;
+
+    /**
+     * 标签过滤类型(排除标签默认 只要有其一的就排除)
+     */
+    private Integer filterType;
+
+    /** 按标签筛选客户 */
+    private List<String> tagsIdsSelectList;
+
+    /** 按排除标签筛选客户 */
+    private List<String> outTagsIdsSelectList;
+
+    private String corpId;
+}

+ 1 - 1
fs-service/src/main/java/com/fs/sop/service/impl/SopUserLogsInfoServiceImpl.java

@@ -2651,7 +2651,7 @@ public class SopUserLogsInfoServiceImpl implements ISopUserLogsInfoService {
         FsCourseRealLink courseMap = new FsCourseRealLink();
         BeanUtils.copyProperties(link, courseMap);
 
-        String realLinkFull = registeredRealLink + JSON.toJSONString(courseMap);
+        String realLinkFull = REAL_LINK_PREFIX + JSON.toJSONString(courseMap);
         link.setRealLink(realLinkFull);
         fsCourseLinkMapper.insertFsCourseLink(link);
         if(StringUtils.isEmpty(config.getSmsDomainName())){

+ 63 - 0
fs-service/src/main/java/com/fs/sop/vo/WxFilterSopCustomersResult.java

@@ -0,0 +1,63 @@
+package com.fs.sop.vo;
+
+import lombok.Data;
+
+/**
+ * 个微SOP筛选客户结果对象
+ *
+ * @author fs
+ * @date 2025-03-12
+ */
+@Data
+public class WxFilterSopCustomersResult {
+
+    /**
+     * 微信号ID
+     */
+    private String weixinId;
+
+    /**
+     * 客户ID
+     */
+    private String id;
+
+    /**
+     * CRM客户ID
+     */
+    private String customerId;
+
+    /**
+     * 客户名称
+     */
+    private String name;
+
+    /**
+     * 小程序用户ID
+     */
+    private String fsUserId;
+
+    /**
+     * 公司用户ID
+     */
+    private Long cuCompanyUserId;
+
+    /**
+     * 公司ID
+     */
+    private Long cuCompanyId;
+
+    /**
+     * 执行账号ID
+     */
+    private String accountId;
+
+    /**
+     * 企微用户ID
+     */
+    private String qwUserId;
+
+    /**
+     * 企业ID
+     */
+    private String corpId;
+}

+ 10 - 7
fs-service/src/main/java/com/fs/system/service/ISysDictDataService.java

@@ -1,19 +1,22 @@
 package com.fs.system.service;
 
 import java.util.List;
+
+import com.fs.common.annotation.DataSource;
 import com.fs.common.core.domain.entity.SysDictData;
+import com.fs.common.enums.DataSourceType;
 import com.fs.system.vo.DictVO;
 
 /**
  * 字典 业务层
- * 
+ *
 
  */
 public interface ISysDictDataService
 {
     /**
      * 根据条件分页查询字典数据
-     * 
+     *
      * @param dictData 字典数据信息
      * @return 字典数据集合信息
      */
@@ -21,7 +24,7 @@ public interface ISysDictDataService
 
     /**
      * 根据字典类型和字典键值查询字典数据信息
-     * 
+     *
      * @param dictType 字典类型
      * @param dictValue 字典键值
      * @return 字典标签
@@ -30,7 +33,7 @@ public interface ISysDictDataService
 
     /**
      * 根据字典数据ID查询信息
-     * 
+     *
      * @param dictCode 字典数据ID
      * @return 字典数据
      */
@@ -38,7 +41,7 @@ public interface ISysDictDataService
 
     /**
      * 批量删除字典数据信息
-     * 
+     *
      * @param dictCodes 需要删除的字典数据ID
      * @return 结果
      */
@@ -46,7 +49,7 @@ public interface ISysDictDataService
 
     /**
      * 新增保存字典数据信息
-     * 
+     *
      * @param dictData 字典数据信息
      * @return 结果
      */
@@ -54,7 +57,7 @@ public interface ISysDictDataService
 
     /**
      * 修改保存字典数据信息
-     * 
+     *
      * @param dictData 字典数据信息
      * @return 结果
      */

+ 7 - 0
fs-service/src/main/java/com/fs/wx/sop/domain/WxSop.java

@@ -68,5 +68,12 @@ public class WxSop extends BaseEntityTow {
     /** 执行账号列表(不入库,用于编辑回显) */
     @TableField(exist = false)
     private List<Map<String, Object>> selectedQwUsers;
+    
+    /** 状态(0停止 1启用 2执行中) */
+    @Excel(name = "状态(0停止 1启用 2执行中)")
+    private Long status;
 
+    /** 备注 */
+    @Excel(name = "备注")
+    private String remark;
 }

+ 6 - 0
fs-service/src/main/java/com/fs/wx/sop/domain/WxSopUser.java

@@ -4,6 +4,7 @@ import java.time.LocalDate;
 import java.util.Date;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntityTow;
 import lombok.Data;
@@ -18,6 +19,7 @@ import lombok.EqualsAndHashCode;
  */
 @Data
 @EqualsAndHashCode(callSuper = true)
+@TableName("wx_sop_user")
 public class WxSopUser extends BaseEntityTow {
 
     /** 类型(0个人1群聊) */
@@ -32,6 +34,10 @@ public class WxSopUser extends BaseEntityTow {
     @Excel(name = "个微账号ID")
     private Long accountId;
 
+    /** 个微账号名称 */
+    @Excel(name = "个微账号名称")
+    private String accountName;
+
     /** 营期时间 */
     @JsonFormat(pattern = "yyyy-MM-dd")
     @Excel(name = "营期时间", width = 30, dateFormat = "yyyy-MM-dd")

+ 9 - 0
fs-service/src/main/java/com/fs/wx/sop/domain/WxSopUserInfo.java

@@ -4,6 +4,7 @@ import java.time.LocalDateTime;
 import java.util.Date;
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
 import com.fs.common.annotation.Excel;
 import com.fs.common.core.domain.BaseEntityTow;
 import lombok.Data;
@@ -18,6 +19,7 @@ import lombok.EqualsAndHashCode;
  */
 @Data
 @EqualsAndHashCode(callSuper = true)
+@TableName("wx_sop_user_info")
 public class WxSopUserInfo extends BaseEntityTow {
 
     /** 任务ID */
@@ -32,6 +34,10 @@ public class WxSopUserInfo extends BaseEntityTow {
     @Excel(name = "联系人ID")
     private Long wxContactId;
 
+    /** 客户ID */
+    @Excel(name = "客户ID")
+    private Long customerId;
+
     /** 小程序ID */
     @Excel(name = "小程序ID")
     private Long fsUserId;
@@ -61,5 +67,8 @@ public class WxSopUserInfo extends BaseEntityTow {
     @Excel(name = "禁用状态 0 正常 1禁用")
     private Integer status;
 
+    /** 客户标签名称 */
+    private String tagNames;
+
 
 }

+ 10 - 0
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopLogsMapper.java

@@ -3,6 +3,8 @@ package com.fs.wx.sop.mapper;
 import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.wx.sop.domain.WxSopLogs;
+import com.fs.wx.sop.params.WxSopLogsParam;
+import com.fs.wx.sop.vo.WxSopLogsListVO;
 
 /**
  * 个微发送记录Mapper接口
@@ -27,6 +29,14 @@ public interface WxSopLogsMapper extends BaseMapper<WxSopLogs>{
      */
     List<WxSopLogs> selectWxSopLogsList(WxSopLogs wxSopLogs);
 
+    /**
+     * 查询个微SOP执行记录列表(带关联信息)
+     *
+     * @param param 查询参数
+     * @return 执行记录集合
+     */
+    List<WxSopLogsListVO> selectWxSopLogsListBySopId(WxSopLogsParam param);
+
     /**
      * 新增个微发送记录
      * 

+ 36 - 7
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopMapper.java

@@ -4,18 +4,20 @@ import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
+import com.fs.sop.params.WxSopTagsParam;
+import com.fs.sop.vo.WxFilterSopCustomersResult;
 import com.fs.wx.sop.domain.WxSop;
 
 /**
  * 个微SOPMapper接口
- * 
+ *
  * @author 吴树波
  * @date 2026-02-24
  */
 public interface WxSopMapper extends BaseMapper<WxSop>{
     /**
      * 查询个微SOP
-     * 
+     *
      * @param id 个微SOP主键
      * @return 个微SOP
      */
@@ -24,15 +26,16 @@ public interface WxSopMapper extends BaseMapper<WxSop>{
 
     /**
      * 查询个微SOP列表
-     * 
+     *
      * @param wxSop 个微SOP
      * @return 个微SOP集合
      */
+    @DataSource(DataSourceType.SOP)
     List<WxSop> selectWxSopList(WxSop wxSop);
 
     /**
      * 新增个微SOP
-     * 
+     *
      * @param wxSop 个微SOP
      * @return 结果
      */
@@ -40,7 +43,7 @@ public interface WxSopMapper extends BaseMapper<WxSop>{
 
     /**
      * 修改个微SOP
-     * 
+     *
      * @param wxSop 个微SOP
      * @return 结果
      */
@@ -48,7 +51,7 @@ public interface WxSopMapper extends BaseMapper<WxSop>{
 
     /**
      * 删除个微SOP
-     * 
+     *
      * @param id 个微SOP主键
      * @return 结果
      */
@@ -56,9 +59,35 @@ public interface WxSopMapper extends BaseMapper<WxSop>{
 
     /**
      * 批量删除个微SOP
-     * 
+     *
      * @param ids 需要删除的数据主键集合
      * @return 结果
      */
     int deleteWxSopByIds(Long[] ids);
+
+    /**
+     * 根据ID数组查询个微SOP列表
+     *
+     * @param ids 个微SOP主键数组
+     * @return 个微SOP集合
+     */
+    List<WxSop> selectWxSopByIds(Long[] ids);
+
+    /**
+     * 批量更新个微SOP状态
+     *
+     * @param ids 个微SOP主键数组
+     * @param status 状态
+     * @return 结果
+     */
+    int updateStatusWxSopByIds(Long[] ids, Long status);
+
+    /**
+     * 根据筛选条件查询符合条件的客户
+     *
+     * @param param 筛选参数
+     * @return 客户结果列表
+     */
+    @DataSource(DataSourceType.MASTER)
+    List<WxFilterSopCustomersResult> selectFilterWxSopCustomers(WxSopTagsParam param);
 }

+ 11 - 0
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserInfoMapper.java

@@ -2,6 +2,8 @@ package com.fs.wx.sop.mapper;
 
 import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
 import com.fs.wx.sop.domain.WxSopUserInfo;
 
 /**
@@ -10,6 +12,7 @@ import com.fs.wx.sop.domain.WxSopUserInfo;
  * @author 吴树波
  * @date 2026-02-24
  */
+@DataSource(DataSourceType.SOP)
 public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
     /**
      * 查询个微营期详情
@@ -58,4 +61,12 @@ public interface WxSopUserInfoMapper extends BaseMapper<WxSopUserInfo>{
      * @return 结果
      */
     int deleteWxSopUserInfoByIds(Long[] ids);
+
+    /**
+     * 根据条件查询单个营期成员记录
+     * 
+     * @param wxSopUserInfo 个微营期详情
+     * @return 个微营期详情
+     */
+    WxSopUserInfo selectWxSopUserInfoByCondition(WxSopUserInfo wxSopUserInfo);
 }

+ 19 - 8
fs-service/src/main/java/com/fs/wx/sop/mapper/WxSopUserMapper.java

@@ -2,18 +2,21 @@ package com.fs.wx.sop.mapper;
 
 import java.util.List;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.common.annotation.DataSource;
+import com.fs.common.enums.DataSourceType;
 import com.fs.wx.sop.domain.WxSopUser;
 
 /**
  * 个微营期Mapper接口
- * 
+ *
  * @author 吴树波
  * @date 2026-02-24
  */
+@DataSource(DataSourceType.SOP)
 public interface WxSopUserMapper extends BaseMapper<WxSopUser>{
     /**
      * 查询个微营期
-     * 
+     *
      * @param id 个微营期主键
      * @return 个微营期
      */
@@ -21,7 +24,7 @@ public interface WxSopUserMapper extends BaseMapper<WxSopUser>{
 
     /**
      * 查询个微营期列表
-     * 
+     *
      * @param wxSopUser 个微营期
      * @return 个微营期集合
      */
@@ -29,7 +32,7 @@ public interface WxSopUserMapper extends BaseMapper<WxSopUser>{
 
     /**
      * 新增个微营期
-     * 
+     *
      * @param wxSopUser 个微营期
      * @return 结果
      */
@@ -37,7 +40,7 @@ public interface WxSopUserMapper extends BaseMapper<WxSopUser>{
 
     /**
      * 修改个微营期
-     * 
+     *
      * @param wxSopUser 个微营期
      * @return 结果
      */
@@ -45,7 +48,7 @@ public interface WxSopUserMapper extends BaseMapper<WxSopUser>{
 
     /**
      * 删除个微营期
-     * 
+     *
      * @param id 个微营期主键
      * @return 结果
      */
@@ -53,7 +56,7 @@ public interface WxSopUserMapper extends BaseMapper<WxSopUser>{
 
     /**
      * 批量删除个微营期
-     * 
+     *
      * @param ids 需要删除的数据主键集合
      * @return 结果
      */
@@ -61,9 +64,17 @@ public interface WxSopUserMapper extends BaseMapper<WxSopUser>{
 
     /**
      * 根据SOP ID删除执行账号
-     * 
+     *
      * @param sopId SOP主键
      * @return 结果
      */
     int deleteBySopId(Long sopId);
+
+    /**
+     * 查询营期记录(根据条件)
+     *
+     * @param wxSopUser 查询条件
+     * @return 营期记录
+     */
+    WxSopUser selectwxSopUser(WxSopUser wxSopUser);
 }

+ 57 - 0
fs-service/src/main/java/com/fs/wx/sop/params/WxSopLogsParam.java

@@ -0,0 +1,57 @@
+package com.fs.wx.sop.params;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 个微SOP执行记录查询参数
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+public class WxSopLogsParam {
+    
+    /** SOP ID */
+    private Long sopId;
+
+    /** 个微账号昵称(用于模糊搜索) */
+    private String accountName;
+
+    /** 个微账号ID */
+    private Long accountId;
+
+    /** 个微账号ID列表 */
+    private List<Long> accountIdList;
+
+    /** 客户昵称(用于模糊搜索) */
+    private String wxContactName;
+
+    /** 客户ID */
+    private Long wxContactId;
+
+    /** 公司ID */
+    private Long companyId;
+
+    /** 发送状态 0待发送1发送成功2发送失败3消息作废 */
+    private Integer sendStatus;
+
+    /** 发送类型 */
+    private Integer sendType;
+
+    /** 消息类型 0个人1群 */
+    private Integer type;
+
+    /** 营期ID */
+    private Long sopUserId;
+
+    /** 预计发送开始时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private String scheduleStartTime;
+
+    /** 预计发送结束时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private String scheduleEndTime;
+}

+ 42 - 0
fs-service/src/main/java/com/fs/wx/sop/service/IWxSopExecuteService.java

@@ -0,0 +1,42 @@
+package com.fs.wx.sop.service;
+
+import com.fs.common.core.domain.R;
+import com.fs.wx.sop.domain.WxSop;
+
+/**
+ * 个微SOP执行服务接口
+ *
+ * @author fs
+ * @date 2025-03-12
+ */
+public interface IWxSopExecuteService {
+    /**
+     * 根据标签筛选客户并创建营期
+     *
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    R processTagFilterWxSop(WxSop wxSop);
+
+    /**
+     * 根据群聊筛选客户并创建营期
+     *
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    R processGroupFilterWxSop(WxSop wxSop);
+
+    /**
+     * 为客户创建营期记录
+     *
+     * @param wxSop 个微SOP
+     * @return 结果
+     */
+    R createSopUserLogsWx(WxSop wxSop);
+
+    /**
+     * 处理客户标签变更
+     *
+     */
+    void processCustomerTagsChange(Long customerId);
+}

+ 10 - 0
fs-service/src/main/java/com/fs/wx/sop/service/IWxSopLogsService.java

@@ -3,6 +3,8 @@ package com.fs.wx.sop.service;
 import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.wx.sop.domain.WxSopLogs;
+import com.fs.wx.sop.params.WxSopLogsParam;
+import com.fs.wx.sop.vo.WxSopLogsListVO;
 
 /**
  * 个微发送记录Service接口
@@ -27,6 +29,14 @@ public interface IWxSopLogsService extends IService<WxSopLogs>{
      */
     List<WxSopLogs> selectWxSopLogsList(WxSopLogs wxSopLogs);
 
+    /**
+     * 查询个微SOP执行记录列表(带关联信息)
+     *
+     * @param param 查询参数
+     * @return 执行记录集合
+     */
+    List<WxSopLogsListVO> selectWxSopLogsListBySopId(WxSopLogsParam param);
+
     /**
      * 新增个微发送记录
      * 

+ 9 - 0
fs-service/src/main/java/com/fs/wx/sop/service/IWxSopService.java

@@ -3,6 +3,7 @@ package com.fs.wx.sop.service;
 import java.util.List;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.fs.wx.sop.domain.WxSop;
+import com.fs.common.core.domain.R;
 
 /**
  * 个微SOPService接口
@@ -66,4 +67,12 @@ public interface IWxSopService extends IService<WxSop>{
      * @return 结果
      */
     int deleteWxSopById(Long id);
+    
+    /**
+     * 批量执行个微SOP
+     * 
+     * @param ids 个微SOP主键数组
+     * @return 结果
+     */
+    R updateStatusWxSopByIds(Long[] ids);
 }

+ 559 - 0
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopExecuteServiceImpl.java

@@ -0,0 +1,559 @@
+package com.fs.wx.sop.service.impl;
+
+
+import com.fs.common.annotation.DataSource;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.entity.SysDictData;
+import com.fs.common.enums.DataSourceType;
+import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.mapper.CrmCustomerMapper;
+import com.fs.sop.params.WxSopTagsParam;
+import com.fs.sop.vo.WxFilterSopCustomersResult;
+import com.fs.system.service.ISysDictDataService;
+import com.fs.wx.sop.domain.WxSop;
+import com.fs.wx.sop.domain.WxSopUser;
+import com.fs.wx.sop.domain.WxSopUserInfo;
+import com.fs.wx.sop.mapper.WxSopMapper;
+import com.fs.wx.sop.mapper.WxSopUserInfoMapper;
+import com.fs.wx.sop.mapper.WxSopUserMapper;
+import com.fs.wx.sop.service.IWxSopExecuteService;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 个微SOP执行服务实现类
+ *
+ * @author fs
+ * @date 2025-03-12
+ */
+@Service
+@Slf4j
+public class WxSopExecuteServiceImpl implements IWxSopExecuteService {
+
+    @Autowired
+    private WxSopMapper wxSopMapper;
+
+    @Autowired
+    private WxSopUserMapper wxSopUserMapper;
+
+    @Autowired
+    private WxSopUserInfoMapper wxSopUserInfoMapper;
+
+    @Autowired
+    private CrmCustomerMapper crmCustomerMapper;
+
+    @Autowired
+    private ISysDictDataService sysDictDataService;
+
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public R processTagFilterWxSop(WxSop wxSop) {
+        try {
+            log.info("====== 开始执行标签筛选SOP ======");
+            log.info("SOP ID: {}, 名称: {}, 筛选标签: {}, 执行账号: {}",
+                    wxSop.getId(), wxSop.getName(), wxSop.getSelectTags(), wxSop.getAccountIds());
+
+            // 构建标签筛选参数
+            WxSopTagsParam wxSopTagsParam = buildWxSopTagsParam(wxSop);
+            log.info("筛选参数: accountIds={}, tags={}, excludeTags={}",
+                    wxSopTagsParam.getAccountIdsSelectList(),
+                    wxSopTagsParam.getTagsIdsSelectList(),
+                    wxSopTagsParam.getOutTagsIdsSelectList());
+
+            // 查询符合条件的客户
+            List<WxFilterSopCustomersResult> customerResults = selectFilterWxSopCustomers(wxSopTagsParam);
+            log.info("查询到的客户数量: {}", customerResults != null ? customerResults.size() : 0);
+
+            if (customerResults != null && !customerResults.isEmpty()) {
+                for (WxFilterSopCustomersResult customer : customerResults) {
+                    log.info("符合条件的客户: ID={}, 名称={}, 账号ID={}",
+                            customer.getId(), customer.getName(), customer.getAccountId());
+                }
+            }
+
+            if (customerResults == null || customerResults.isEmpty()) {
+                log.warn("未找到符合条件的客户,SOP ID: {}", wxSop.getId());
+                return R.error("未找到符合条件的客户");
+            }
+
+            // 为客户创建营期记录
+            createSopUserLogsWxForCustomers(wxSop, customerResults);
+
+            log.info("标签筛选SOP处理完成,SOP ID: {},客户数量: {}", wxSop.getId(), customerResults.size());
+            return R.ok("标签筛选SOP处理完成");
+        } catch (Exception e) {
+            log.error("处理标签筛选SOP时发生异常,SOP ID: {}", wxSop.getId(), e);
+            return R.error("处理过程中发生异常: " + e.getMessage());
+        }
+    }
+
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public R processGroupFilterWxSop(WxSop wxSop) {
+        try {
+            // 群聊筛选逻辑
+            // 这里需要根据群聊ID或其他群聊相关信息筛选客户
+            // 暂时留空实现,需要根据具体业务需求完善
+
+            log.info("群聊筛选SOP处理完成,SOP ID: {}", wxSop.getId());
+            return R.ok("群聊筛选SOP处理完成");
+        } catch (Exception e) {
+            log.error("处理群聊筛选SOP时发生异常,SOP ID: {}", wxSop.getId(), e);
+            return R.error("处理过程中发生异常: " + e.getMessage());
+        }
+    }
+
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public R createSopUserLogsWx(WxSop wxSop) {
+        try {
+            // 根据SOP的筛选方式进行不同的客户筛选
+            if (wxSop.getFilterType() != null && wxSop.getFilterType() == 0) { // 0: 标签筛选
+                return processTagFilterWxSop(wxSop);
+            } else if (wxSop.getFilterType() != null && wxSop.getFilterType() == 1) { // 1: 群聊筛选
+                return processGroupFilterWxSop(wxSop);
+            } else {
+                return R.error("未知的筛选方式");
+            }
+        } catch (Exception e) {
+            log.error("创建SOP营期记录时发生异常,SOP ID: {}", wxSop.getId(), e);
+            return R.error("创建营期记录时发生异常: " + e.getMessage());
+        }
+    }
+
+    /**
+     * 构建标签筛选参数
+     *
+     * @param wxSop 个微SOP
+     * @return 标签筛选参数
+     */
+    private WxSopTagsParam buildWxSopTagsParam(WxSop wxSop) {
+        WxSopTagsParam param = new WxSopTagsParam();
+
+        // 设置执行账号ID列表
+        if (wxSop.getAccountIds() != null && !wxSop.getAccountIds().isEmpty()) {
+            String[] accountIds = wxSop.getAccountIds().split(",");
+            List<String> accountIdsList = new ArrayList<>();
+            for (String accountId : accountIds) {
+                if (accountId != null && !accountId.trim().isEmpty()) {
+                    accountIdsList.add(accountId.trim());
+                }
+            }
+            param.setAccountIdsSelectList(accountIdsList);
+        }
+
+        // 设置筛选标签
+        if (wxSop.getSelectTags() != null && !wxSop.getSelectTags().isEmpty()) {
+            String[] tags = wxSop.getSelectTags().split(",");
+            List<String> tagsList = new ArrayList<>();
+            for (String tag : tags) {
+                if (tag != null && !tag.trim().isEmpty()) {
+                    tagsList.add(tag.trim());
+                }
+            }
+            param.setTagsIdsSelectList(tagsList);
+        }
+
+        // 设置排除标签
+        if (wxSop.getExcludeTags() != null && !wxSop.getExcludeTags().isEmpty()) {
+            String[] excludeTags = wxSop.getExcludeTags().split(",");
+            List<String> excludeTagsList = new ArrayList<>();
+            for (String excludeTag : excludeTags) {
+                if (excludeTag != null && !excludeTag.trim().isEmpty()) {
+                    excludeTagsList.add(excludeTag.trim());
+                }
+            }
+            param.setOutTagsIdsSelectList(excludeTagsList);
+        }
+
+        // 设置筛选类型
+        param.setFilterType(wxSop.getFilterType());
+
+        return param;
+    }
+
+    /**
+     * 查询符合条件的客户
+     *
+     * @param param 标签筛选参数
+     * @return 客户结果列表
+     */
+    private List<WxFilterSopCustomersResult> selectFilterWxSopCustomers(WxSopTagsParam param) {
+        log.info("开始查询符合条件的客户...");
+        List<WxFilterSopCustomersResult> results = wxSopMapper.selectFilterWxSopCustomers(param);
+        log.info("客户查询完成,结果数: {}", results != null ? results.size() : 0);
+        return results;
+    }
+
+    /**
+     * 为客户创建营期记录
+     *
+     * @param wxSop 个微SOP
+     * @param customerResults 客户结果列表
+     */
+    @Transactional(rollbackFor = Exception.class)
+    @DataSource(DataSourceType.SOP)
+    public void createSopUserLogsWxForCustomers(WxSop wxSop, List<WxFilterSopCustomersResult> customerResults) {
+        log.info("====== 开始为 {} 个客户创建营期记录 ======", customerResults.size());
+        // 按执行账号分组创建营期
+        for (WxFilterSopCustomersResult customer : customerResults) {
+            try {
+                log.info("处理客户: ID={}, 名称={}, 账号ID={}",
+                        customer.getId(), customer.getName(), customer.getAccountId());
+                Long sopUserId = getOrCreateWxSopUser(wxSop, customer);
+
+                if (sopUserId == null) {
+                    log.warn("创建营期失败,跳过客户:{}", customer.getId());
+                    continue;
+                }
+
+                log.info("营期ID: {}", sopUserId);
+
+                // 2. 检查客户是否已在该营期中
+                WxSopUserInfo queryParam = new WxSopUserInfo();
+                queryParam.setSopId(wxSop.getId());
+                queryParam.setSopUserId(sopUserId);
+                queryParam.setWxContactId(Long.parseLong(customer.getId()));
+
+                WxSopUserInfo existingInfo = wxSopUserInfoMapper.selectWxSopUserInfoByCondition(queryParam);
+                if (existingInfo != null) {
+                    log.info("客户已在营期中,跳过:客户ID={}, 营期ID={}", customer.getId(), sopUserId);
+                    continue;
+                }
+
+                // 3. 创建营期成员记录(wx_sop_user_info)
+                WxSopUserInfo wxSopUserInfo = new WxSopUserInfo();
+                wxSopUserInfo.setSopId(wxSop.getId());
+                wxSopUserInfo.setSopUserId(sopUserId);
+                wxSopUserInfo.setWxContactId(Long.parseLong(customer.getId()));
+                if (customer.getCustomerId() != null && !customer.getCustomerId().isEmpty()) {
+                    try {
+                        wxSopUserInfo.setCustomerId(Long.parseLong(customer.getCustomerId()));
+                    } catch (NumberFormatException e) {
+                        log.warn("客户ID格式错误:{}", customer.getCustomerId());
+                    }
+                }
+
+                // 设置小程序ID(如果有)
+                if (customer.getFsUserId() != null && !customer.getFsUserId().isEmpty()) {
+                    try {
+                        wxSopUserInfo.setFsUserId(Long.parseLong(customer.getFsUserId()));
+                    } catch (NumberFormatException e) {
+                        log.warn("小程序ID格式错误:{}", customer.getFsUserId());
+                    }
+                }
+
+                wxSopUserInfo.setStatus(0); // 正常状态
+
+                int result = wxSopUserInfoMapper.insertWxSopUserInfo(wxSopUserInfo);
+                log.info("成功添加客户到营期:客户ID={}, 营期ID={}, SOP ID={}, 插入结果={}",
+                        customer.getId(), sopUserId, wxSop.getId(), result);
+
+            } catch (Exception e) {
+                log.error("为客户创建营期记录失败,客户ID: {}", customer.getId(), e);
+                // 继续处理其他客户
+            }
+        }
+        log.info("====== 营期记录创建完成 ======");
+    }
+
+    /**
+     * 获取或创建营期主表记录
+     *
+     * @param wxSop 个微SOP
+     * @param customer 客户信息
+     * @return 营期ID
+     */
+    private Long getOrCreateWxSopUser(WxSop wxSop, WxFilterSopCustomersResult customer) {
+        try {
+            // 解析账号ID
+            Long accountId = null;
+            if (customer.getAccountId() != null && !customer.getAccountId().isEmpty()) {
+                accountId = Long.parseLong(customer.getAccountId());
+            } else {
+                log.warn("客户账号ID为空,客户ID: {}", customer.getId());
+                return null;
+            }
+
+            // 确定营期时间
+            LocalDate startTime;
+            if (wxSop.getIsFixed() != null && wxSop.getIsFixed() == 1) {
+                // 固定营期:使用SOP的开始时间
+                startTime = wxSop.getStartTime();
+            } else {
+                // 非固定营期:使用今天
+                startTime = LocalDate.now();
+            }
+
+            // 查询是否已存在营期
+            WxSopUser queryParam = new WxSopUser();
+            queryParam.setSopId(wxSop.getId());
+            queryParam.setType(0); // 个人类型
+            queryParam.setAccountId(accountId);
+            queryParam.setStartTime(startTime);
+
+            WxSopUser existingUser = wxSopUserMapper.selectwxSopUser(queryParam);
+            if (existingUser != null) {
+                return existingUser.getId();
+            }
+
+            // 创建新的营期记录
+            WxSopUser wxSopUser = new WxSopUser();
+            wxSopUser.setType(0); // 个人类型
+            wxSopUser.setSopId(wxSop.getId());
+            wxSopUser.setAccountId(accountId);
+            wxSopUser.setStartTime(startTime);
+            wxSopUser.setStatus(0); // 正常状态
+
+            int result = wxSopUserMapper.insertWxSopUser(wxSopUser);
+            if (result > 0) {
+                log.info("创建营期成功:SOP ID={}, 账号ID={}, 营期时间={}, 营期ID={}",
+                        wxSop.getId(), accountId, startTime, wxSopUser.getId());
+                return wxSopUser.getId();
+            } else {
+                log.error("创建营期失败:SOP ID={}, 账号ID={}", wxSop.getId(), accountId);
+                return null;
+            }
+        } catch (Exception e) {
+            log.error("获取或创建营期失败", e);
+            return null;
+        }
+    }
+
+    /**
+     * 处理客户标签变更后的SOP营期动态管理
+     * 根据客户最新标签,自动加入或移出符合条件的营期
+     *
+     * @param customerId 客户ID
+     */
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    @DataSource(DataSourceType.SOP)
+    public void processCustomerTagsChange(Long customerId) {
+        try {
+            log.info("====== 开始处理客户标签变更,客户ID: {} ======", customerId);
+
+            CrmCustomer customer = crmCustomerMapper.selectCrmCustomerById(customerId);
+            if (customer == null) {
+                log.warn("客户不存在,客户ID: {}", customerId);
+                return;
+            }
+            log.info("客户信息: ID={}, 名称={}, 标签={}", customerId, customer.getCustomerName(), customer.getTags());
+
+            WxSop sopQuery = new WxSop();
+            sopQuery.setStatus(2L);
+            sopQuery.setFilterType(0);
+            List<WxSop> enabledSops = wxSopMapper.selectWxSopList(sopQuery);
+            log.info("查询到 {} 个启用的标签筛选SOP", enabledSops != null ? enabledSops.size() : 0);
+
+            if (enabledSops == null || enabledSops.isEmpty()) {
+                log.info("没有启用的标签筛选SOP,无需处理");
+                return;
+            }
+
+            for (WxSop sop : enabledSops) {
+                processCustomerForSop(customer, sop);
+            }
+
+            log.info("====== 客户标签变更处理完成,客户ID: {} ======", customerId);
+        } catch (Exception e) {
+            log.error("处理客户标签变更时发生异常,客户ID: {}", customerId, e);
+            throw e;
+        }
+    }
+
+
+
+
+    /**
+     * 处理单个客户在单个SOP中的营期管理
+     *
+     * @param customer 客户信息
+     * @param sop SOP信息
+     */
+    public void processCustomerForSop(CrmCustomer customer, WxSop sop) {
+        try {
+            log.info("检查客户是否符合SOP条件 - 客户ID: {}, SOP ID: {}, SOP名称: {}",
+                    customer.getCustomerId(), sop.getId(), sop.getName());
+
+            boolean isMatch = checkCustomerMatchSopTags(customer, sop);
+            log.info("客户是否符合SOP条件: {}", isMatch);
+
+
+            List<WxSopUserInfo> existingInfos = querySopUserInfosByCustomer(sop.getId(), customer.getCustomerId());
+
+            if (isMatch) {
+                // 符合条件:如果不在营期中,则加入
+                if (existingInfos == null || existingInfos.isEmpty()) {
+                    log.info("客户符合条件且不在营期中,准备加入营期");
+                    addCustomerToSop(customer, sop);
+                } else {
+                    log.info("客户已在营期中,无需重复加入");
+                }
+            } else {
+                // 不符合条件:如果在营期中,则移出
+                if (existingInfos != null && !existingInfos.isEmpty()) {
+                    log.info("客户不符合条件但在营期中,准备移出营期,记录数: {}", existingInfos.size());
+                    removeCustomerFromSop(customer, sop, existingInfos);
+                } else {
+                    log.info("客户不符合条件且不在营期中,无需处理");
+                }
+            }
+        } catch (Exception e) {
+            log.error("处理客户在SOP中的营期管理时发生异常,客户ID: {}, SOP ID: {}",
+                    customer.getCustomerId(), sop.getId(), e);
+        }
+    }
+
+    /**
+     * 查询客户在指定SOP下的所有营期成员记录
+     *
+     * @param sopId SOP ID
+     * @param customerId 客户ID
+     * @return 营期成员记录列表
+     */
+    @DataSource(DataSourceType.SOP)
+    public List<WxSopUserInfo> querySopUserInfosByCustomer(Long sopId, Long customerId) {
+        WxSopUserInfo queryParam = new WxSopUserInfo();
+        queryParam.setSopId(sopId);
+        queryParam.setCustomerId(customerId);
+        return wxSopUserInfoMapper.selectWxSopUserInfoList(queryParam);
+    }
+
+    /**
+     * 检查客户是否符合SOP的标签筛选条件
+     * 注意:客户tags字段与SOP的selectTags/excludeTags统一使用标签值(label)进行比对
+     *
+     * @param customer 客户信息
+     * @param sop SOP信息
+     * @return true-符合条件,false-不符合条件
+     */
+    private boolean checkCustomerMatchSopTags(CrmCustomer customer, WxSop sop) {
+        String customerTags = customer.getTags();
+        if (customerTags == null || customerTags.trim().isEmpty()) {
+            log.info("客户标签为空");
+            // 如果客户没有标签,但SOP要求有筛选标签,则不符合
+            return sop.getSelectTags() == null || sop.getSelectTags().trim().isEmpty();
+        }
+
+        // 客户的标签列表(逗号分隔的标签值)- 需要将dict_label转换为dict_value
+        String[] customerTagArray = customerTags.split(",");
+        List<String> customerTagIdList = new ArrayList<>();
+        for (String tagLabel : customerTagArray) {
+            if (tagLabel != null && !tagLabel.trim().isEmpty()) {
+                // 将标签名(dict_label)转换为标签ID(dict_value)
+                String tagId = com.fs.common.utils.DictUtils.getDictValue("crm_customer_tag", tagLabel.trim());
+                if (tagId != null && !tagId.isEmpty()) {
+                    customerTagIdList.add(tagId);
+                }
+            }
+        }
+        log.info("客户标签ID列表: {}", customerTagIdList);
+
+        // 检查必须包含的标签(selectTags)- 直接使用标签ID比对
+        if (sop.getSelectTags() != null && !sop.getSelectTags().trim().isEmpty()) {
+            String[] selectTags = sop.getSelectTags().split(",");
+            for (String tag : selectTags) {
+                if (tag != null && !tag.trim().isEmpty()) {
+                    String trimmedTag = tag.trim();
+                    if (!customerTagIdList.contains(trimmedTag)) {
+                        log.info("客户缺少必需标签ID: {}", trimmedTag);
+                        return false;
+                    }
+                }
+            }
+            log.info("客户包含所有必需标签");
+        }
+
+        // 检查排除的标签(excludeTags)- 直接使用标签ID比对
+        if (sop.getExcludeTags() != null && !sop.getExcludeTags().trim().isEmpty()) {
+            String[] excludeTags = sop.getExcludeTags().split(",");
+            for (String tag : excludeTags) {
+                if (tag != null && !tag.trim().isEmpty()) {
+                    String trimmedTag = tag.trim();
+                    if (customerTagIdList.contains(trimmedTag)) {
+                        log.info("客户包含排除标签ID: {}", trimmedTag);
+                        return false;
+                    }
+                }
+            }
+            log.info("客户不包含排除标签");
+        }
+
+        log.info("客户符合SOP标签筛选条件");
+        return true;
+    }
+
+    /**
+     * 将客户加入SOP营期
+     *
+     * @param customer 客户信息
+     * @param sop SOP信息
+     */
+    @DataSource(DataSourceType.SOP)
+    public void addCustomerToSop(CrmCustomer customer, WxSop sop) {
+        try {
+            WxFilterSopCustomersResult customerResult = new WxFilterSopCustomersResult();
+            customerResult.setId(customer.getCustomerId().toString());
+            customerResult.setName(customer.getCustomerName());
+            if (sop.getAccountIds() != null && !sop.getAccountIds().isEmpty()) {
+                String[] accountIds = sop.getAccountIds().split(",");
+                if (accountIds.length > 0) {
+                    customerResult.setAccountId(accountIds[0].trim());
+                }
+            }
+
+            if (customerResult.getAccountId() == null) {
+                log.warn("无法获取执行账号,跳过加入营期");
+                return;
+            }
+
+            // 获取或创建营期
+            Long sopUserId = getOrCreateWxSopUser(sop, customerResult);
+            if (sopUserId == null) {
+                log.warn("创建营期失败,跳过");
+                return;
+            }
+
+            // 创建营期成员记录
+            WxSopUserInfo wxSopUserInfo = new WxSopUserInfo();
+            wxSopUserInfo.setSopId(sop.getId());
+            wxSopUserInfo.setSopUserId(sopUserId);
+            wxSopUserInfo.setCustomerId(customer.getCustomerId());
+            wxSopUserInfo.setStatus(0); // 正常状态
+            wxSopUserInfo.setTagNames(customer.getTags());
+
+            int result = wxSopUserInfoMapper.insertWxSopUserInfo(wxSopUserInfo);
+            log.info("成功将客户加入营期:客户ID={}, 营期ID={}, SOP ID={}, 插入结果={}",
+                    customer.getCustomerId(), sopUserId, sop.getId(), result);
+        } catch (Exception e) {
+            log.error("将客户加入SOP营期时发生异常", e);
+        }
+    }
+
+    /**
+     * 将客户从SOP营期中移出
+     *
+     * @param customer 客户信息
+     * @param sop SOP信息
+     * @param existingInfos 已存在的营期成员记录
+     */
+    @DataSource(DataSourceType.SOP)
+    public void removeCustomerFromSop(CrmCustomer customer, WxSop sop, List<WxSopUserInfo> existingInfos) {
+        try {
+            for (WxSopUserInfo info : existingInfos) {
+                int result = wxSopUserInfoMapper.deleteWxSopUserInfoById(info.getId());
+                log.info("成功将客户从营期中移出:客户ID={}, 营期成员记录ID={}, SOP ID={}, 删除结果={}",
+                        customer.getCustomerId(), info.getId(), sop.getId(), result);
+            }
+        } catch (Exception e) {
+            log.error("将客户从SOP营期中移出时发生异常", e);
+        }
+    }
+}

+ 15 - 0
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopLogsServiceImpl.java

@@ -7,6 +7,8 @@ import com.fs.common.annotation.DataSource;
 import com.fs.common.enums.DataSourceType;
 import com.fs.common.utils.DateUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.wx.sop.params.WxSopLogsParam;
+import com.fs.wx.sop.vo.WxSopLogsListVO;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import com.fs.wx.sop.mapper.WxSopLogsMapper;
@@ -101,4 +103,17 @@ public class WxSopLogsServiceImpl extends ServiceImpl<WxSopLogsMapper, WxSopLogs
     {
         return baseMapper.deleteWxSopLogsById(id);
     }
+
+    /**
+     * 查询个微SOP执行记录列表(带关联信息)
+     *
+     * @param param 查询参数
+     * @return 执行记录集合
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public List<WxSopLogsListVO> selectWxSopLogsListBySopId(WxSopLogsParam param)
+    {
+        return baseMapper.selectWxSopLogsListBySopId(param);
+    }
 }

+ 93 - 14
fs-service/src/main/java/com/fs/wx/sop/service/impl/WxSopServiceImpl.java

@@ -1,15 +1,12 @@
 package com.fs.wx.sop.service.impl;
 
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
+import java.util.*;
 import java.util.stream.Collectors;
 
 import com.fs.company.domain.CompanyWxAccount;
 import com.fs.company.service.ICompanyWxAccountService;
 import com.fs.common.annotation.DataSource;
+import com.fs.common.core.domain.R;
 import com.fs.common.enums.DataSourceType;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
@@ -18,11 +15,12 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import com.fs.wx.sop.mapper.WxSopMapper;
 import com.fs.wx.sop.domain.WxSop;
+import com.fs.wx.sop.service.IWxSopExecuteService;
 import com.fs.wx.sop.service.IWxSopService;
 
 /**
  * 个微SOPService业务层处理
- * 
+ *
  * @author 吴树波
  * @date 2026-02-24
  */
@@ -32,9 +30,15 @@ public class WxSopServiceImpl extends ServiceImpl<WxSopMapper, WxSop> implements
     @Autowired
     private ICompanyWxAccountService companyWxAccountService;
 
+    @Autowired
+    private WxSopMapper wxSopMapper;
+
+    @Autowired
+    private IWxSopExecuteService wxSopExecuteService;
+
     /**
      * 查询个微SOP
-     * 
+     *
      * @param id 个微SOP主键
      * @return 个微SOP
      */
@@ -56,7 +60,7 @@ public class WxSopServiceImpl extends ServiceImpl<WxSopMapper, WxSop> implements
     }
 
     /**
-     * 填充执行账号 selectedQwUsers(从 accountIds 解析并查询账号详情)
+     * 填充执行账号 selectedQwUsers(从 accountIds 解析并查询并查询账号详情)
      */
     private void fillSelectedQwUsers(WxSop wxSop) {
         String accountIdsStr = wxSop.getAccountIds();
@@ -88,7 +92,7 @@ public class WxSopServiceImpl extends ServiceImpl<WxSopMapper, WxSop> implements
 
     /**
      * 查询个微SOP列表
-     * 
+     *
      * @param wxSop 个微SOP
      * @return 个微SOP
      */
@@ -101,13 +105,13 @@ public class WxSopServiceImpl extends ServiceImpl<WxSopMapper, WxSop> implements
 
     /**
      * 新增个微SOP(含执行账号保存到 wx_sop_user)
-     * 
+     *
      * @param wxSop 个微SOP
      * @return 结果
      */
     /**
      * 新增个微SOP(accountIds 直接入库到 wx_sop.account_ids)
-     * 
+     *
      * @param wxSop 个微SOP
      * @return 结果
      */
@@ -115,13 +119,16 @@ public class WxSopServiceImpl extends ServiceImpl<WxSopMapper, WxSop> implements
     @DataSource(DataSourceType.SOP)
     public int insertWxSop(WxSop wxSop)
     {
+        if (wxSop.getStatus() == null) {
+            wxSop.setStatus(1L);
+        }
         wxSop.setCreateTime(DateUtils.getNowDate());
         return baseMapper.insertWxSop(wxSop);
     }
 
     /**
      * 修改个微SOP(accountIds 直接入库到 wx_sop.account_ids)
-     * 
+     *
      * @param wxSop 个微SOP
      * @return 结果
      */
@@ -135,7 +142,7 @@ public class WxSopServiceImpl extends ServiceImpl<WxSopMapper, WxSop> implements
 
     /**
      * 批量删除个微SOP
-     * 
+     *
      * @param ids 需要删除的个微SOP主键
      * @return 结果
      */
@@ -148,7 +155,7 @@ public class WxSopServiceImpl extends ServiceImpl<WxSopMapper, WxSop> implements
 
     /**
      * 删除个微SOP信息
-     * 
+     *
      * @param id 个微SOP主键
      * @return 结果
      */
@@ -158,4 +165,76 @@ public class WxSopServiceImpl extends ServiceImpl<WxSopMapper, WxSop> implements
     {
         return baseMapper.deleteWxSopById(id);
     }
+
+    /**
+     * 批量执行个微SOP
+     *
+     * @param ids 个微SOP主键数组
+     * @return 结果
+     */
+    @Override
+    @DataSource(DataSourceType.SOP)
+    public R updateStatusWxSopByIds(Long[] ids) {
+        if (ids == null || ids.length == 0) {
+            return R.error("参数不能为空");
+        }
+
+        // 获取要操作的个微SOP列表
+        List<WxSop> wxSops = baseMapper.selectWxSopByIds(ids);
+        if (wxSops.isEmpty()) {
+            return R.ok().put("suc", new ArrayList<>()).put("err", ids);
+        }
+
+        // 筛选出 status == 1 的 IDs (可执行的)
+        List<Long> toBeSent = wxSops.stream()
+                .filter(wxSop -> wxSop.getStatus() != null && wxSop.getStatus() == 1)
+                .map(WxSop::getId)
+                .collect(Collectors.toList());
+
+        // 筛选出 status != 1 的 IDs (不可执行的)
+        List<Long> areadyList = wxSops.stream()
+                .filter(wxSop -> wxSop.getStatus() == null || wxSop.getStatus() != 1)
+                .map(WxSop::getId)
+                .collect(Collectors.toList());
+
+        // 如果有待执行的SOP,则更新其状态
+        if (!toBeSent.isEmpty()) {
+            int updateCount = baseMapper.updateStatusWxSopByIds(toBeSent.toArray(new Long[0]), 2L); // 设置为执行中状态
+            if (updateCount > 0) {
+                // 对于每个待执行的SOP,根据其筛选方式进行客户筛选和营期创建
+                for (Long sopId : toBeSent) {
+                    WxSop wxSop = baseMapper.selectWxSopById(sopId);
+                    if (wxSop != null) {
+                        // 根据筛选方式进行客户筛选和营期创建
+                        processSopCustomerSelection(wxSop);
+                    }
+                }
+
+                return R.ok().put("suc", toBeSent.toArray(new Long[0])).put("err", areadyList.toArray(new Long[0]));
+            } else {
+                // 即使更新失败,也要返回哪些成功哪些失败
+                return R.ok().put("suc", new ArrayList<>()).put("err", ids);
+            }
+        } else {
+            return R.ok().put("suc", new ArrayList<>()).put("err", areadyList.toArray(new Long[0]));
+        }
+    }
+
+
+
+    /**
+     * 处理SOP的客户筛选和营期创建
+     *
+     * @param wxSop 个微SOP
+     */
+    private void processSopCustomerSelection(WxSop wxSop) {
+        // 根据SOP的筛选方式筛选客户
+        if (wxSop.getFilterType() != null && wxSop.getFilterType() == 0) { // 0: 标签筛选
+            // 执行标签筛选逻辑
+            wxSopExecuteService.processTagFilterWxSop(wxSop);
+        } else if (wxSop.getFilterType() != null && wxSop.getFilterType() == 1) { // 1: 群聊筛选
+            // 执行群聊筛选逻辑
+            wxSopExecuteService.processGroupFilterWxSop(wxSop);
+        }
+    }
 }

+ 102 - 0
fs-service/src/main/java/com/fs/wx/sop/vo/WxSopLogsListVO.java

@@ -0,0 +1,102 @@
+package com.fs.wx.sop.vo;
+
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 个微SOP执行记录列表VO
+ *
+ * @author fs
+ * @date 2026-03-06
+ */
+@Data
+public class WxSopLogsListVO implements Serializable {
+    
+    private static final long serialVersionUID = 1L;
+
+    /** 主键ID */
+    private Long id;
+
+    /** SOP ID */
+    private Long sopId;
+
+    /** 营期ID */
+    private Long sopUserId;
+
+    /** 个微账号ID */
+    @Excel(name = "个微账号ID")
+    private Long accountId;
+
+    /** 个微账号昵称 */
+    @Excel(name = "个微账号昵称")
+    private String accountName;
+
+    /** 客户ID */
+    private Long wxContactId;
+
+    /** 客户昵称 */
+    @Excel(name = "客户昵称")
+    private String wxContactName;
+
+    /** 客户标签 */
+    @Excel(name = "客户标签")
+    private String tagNames;
+
+    /** 群聊ID */
+    private Long wxRoomId;
+
+    /** 群聊名称 */
+    @Excel(name = "群聊名称")
+    private String wxRoomName;
+
+    /** 消息类型 0个人1群 */
+    @Excel(name = "消息类型")
+    private Integer type;
+
+    /** 发送类型 */
+    @Excel(name = "发送类型")
+    private Integer sendType;
+
+    /** 生成类型 0自动1手动 */
+    @Excel(name = "生成类型")
+    private Integer generateType;
+
+    /** 发送状态 0待发送1发送成功2发送失败3消息作废 */
+    @Excel(name = "发送状态")
+    private Integer sendStatus;
+
+    /** 发送备注 */
+    @Excel(name = "发送备注")
+    private String sendRemark;
+
+    /** 发送排序 */
+    private Integer sendSort;
+
+    /** 消息过期时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "消息过期时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date expirationTime;
+
+    /** 生成时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "生成时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date createTime;
+
+    /** 实际发送时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "实际发送时间", width = 30, dateFormat = "yyyy-MM-dd HH:mm:ss")
+    private Date realSendTime;
+
+    /** 备注 */
+    private String remark;
+
+    /** 小程序ID */
+    private Long fsUserId;
+
+    /** 公司ID */
+    private Long companyId;
+}

+ 34 - 0
fs-service/src/main/resources/db/20260226-个微SOP表结构.sql

@@ -36,6 +36,7 @@ CREATE TABLE IF NOT EXISTS `wx_sop` (
   `create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
   `update_time` datetime DEFAULT NULL COMMENT '更新时间',
   `update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
+  `status` int(11) DEFAULT NULL COMMENT '状态(0停止 1启用 2执行中)',
   `remark` varchar(500) DEFAULT NULL COMMENT '备注',
   PRIMARY KEY (`id`),
   KEY `idx_company_id` (`company_id`)
@@ -43,3 +44,36 @@ CREATE TABLE IF NOT EXISTS `wx_sop` (
 
 -- 若 wx_sop 表已存在且无 account_ids 字段,可执行:
 -- ALTER TABLE wx_sop ADD COLUMN account_ids varchar(500) DEFAULT NULL COMMENT '执行账号ID,逗号分隔' AFTER start_time;
+
+-- 若 wx_sop 表已存在且无 status 字段,可执行:
+ALTER TABLE wx_sop ADD COLUMN status int(11) DEFAULT NULL COMMENT '状态(0停止 1启用 2执行中)' AFTER update_by;
+
+-- 个微营期详情表
+CREATE TABLE IF NOT EXISTS `wx_sop_user_info` (
+  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
+  `sop_id` bigint(20) DEFAULT NULL COMMENT '任务ID',
+  `sop_user_id` bigint(20) DEFAULT NULL COMMENT '营期ID',
+  `wx_contact_id` bigint(20) DEFAULT NULL COMMENT '联系人ID',
+  `customer_id` bigint(20) DEFAULT NULL COMMENT '客户ID',
+  `fs_user_id` bigint(20) DEFAULT NULL COMMENT '小程序ID',
+  `is_days_not_study` int(11) DEFAULT NULL COMMENT '是否7天都没有看课 0否 1是',
+  `finish_cout` int(11) DEFAULT NULL COMMENT '总完课天数',
+  `finish_time` datetime DEFAULT NULL COMMENT '最近完课时间',
+  `finish_course_days` int(11) DEFAULT NULL COMMENT '连续完课天数',
+  `grade` int(11) DEFAULT NULL COMMENT '客户评级的等级',
+  `status` int(11) DEFAULT NULL COMMENT '禁用状态 0 正常 1禁用',
+  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
+  `create_by` varchar(64) DEFAULT NULL COMMENT '创建者',
+  `update_time` datetime DEFAULT NULL COMMENT '更新时间',
+  `update_by` varchar(64) DEFAULT NULL COMMENT '更新者',
+  `remark` varchar(500) DEFAULT NULL COMMENT '备注',
+  PRIMARY KEY (`id`),
+  KEY `idx_sop_id` (`sop_id`),
+  KEY `idx_sop_user_id` (`sop_user_id`),
+  KEY `idx_wx_contact_id` (`wx_contact_id`),
+  KEY `idx_customer_id` (`customer_id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='个微营期详情表';
+
+-- 若 wx_sop_user_info 表已存在但没有 customer_id 字段,可执行:
+ALTER TABLE wx_sop_user_info ADD COLUMN customer_id bigint(20) DEFAULT NULL COMMENT '客户ID' AFTER wx_contact_id;
+ALTER TABLE wx_sop_user_info ADD INDEX idx_customer_id (customer_id);

+ 1 - 1
fs-service/src/main/resources/mapper/crm/CrmCustomerMapper.xml

@@ -61,7 +61,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectCrmCustomerVo">
-        select * from crm_customer
+        select * from ylrz_his_scrm.crm_customer
     </sql>
 
     <select id="selectCrmCustomerList" parameterType="CrmCustomer" resultMap="CrmCustomerResult">

+ 2 - 1
fs-service/src/main/resources/mapper/qw/QwExternalContactMapper.xml

@@ -819,7 +819,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="selectQwExternalContactByExternalUserIdSidebar"
             resultMap="QwExternalContactResult">
         <include refid="selectQwExternalContactVo"/>
-        where corp_id = #{corpId} and external_user_id = #{externalUserId}
+        where corp_id = #{param.corpId} and external_user_id = #{param.externalUserId}
+        and company_user_id = #{param.companyUserId}
         and user_id is not null and company_id is not null
         limit 1
     </select>

+ 17 - 17
fs-service/src/main/resources/mapper/sopUserLogsWx/SopUserLogsWxMapper.xml

@@ -3,13 +3,13 @@
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.fs.sop.mapper.SopUserLogsWxMapper">
-    
+
     <resultMap type="SopUserLogsWx" id="SopUserLogsWxResult">
         <result property="id"    column="id"    />
         <result property="sopId"    column="sop_id"    />
         <result property="sopTempId"    column="sop_temp_id"    />
-        <result property="qwUserNo"    column="qw_user_no"    />
-        <result property="qwUserId"    column="qw_user_id"    />
+        <result property="userWxName"    column="user_wx_name"    />
+        <result property="accountId"    column="account_id"    />
         <result property="companyUserId"    column="company_user_id"    />
         <result property="companyId"    column="company_id"    />
         <result property="startTime"    column="start_time"    />
@@ -17,35 +17,35 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectSopUserLogsWxVo">
-        select id, sop_id, sop_temp_id, qw_user_no, qw_user_id, company_user_id, company_id, start_time, status from sop_user_logs_wx
+        select id, sop_id, sop_temp_id, user_wx_name, account_id, company_user_id, company_id, start_time, status from sop_user_logs_wx
     </sql>
 
     <select id="selectSopUserLogsWxList" parameterType="SopUserLogsWx" resultMap="SopUserLogsWxResult">
         <include refid="selectSopUserLogsWxVo"/>
-        <where>  
+        <where>
             <if test="sopId != null  and sopId != ''"> and sop_id = #{sopId}</if>
             <if test="sopTempId != null  and sopTempId != ''"> and sop_temp_id = #{sopTempId}</if>
-            <if test="qwUserNo != null  and qwUserNo != ''"> and qw_user_no = #{qwUserNo}</if>
-            <if test="qwUserId != null "> and qw_user_id = #{qwUserId}</if>
+            <if test="userWxName != null  and userWxName != ''"> and user_wx_name = #{userWxName}</if>
+            <if test="accountId != null "> and account_id = #{accountId}</if>
             <if test="companyUserId != null "> and company_user_id = #{companyUserId}</if>
             <if test="companyId != null "> and company_id = #{companyId}</if>
             <if test="startTime != null "> and start_time = #{startTime}</if>
             <if test="status != null "> and status = #{status}</if>
         </where>
     </select>
-    
+
     <select id="selectSopUserLogsWxById" parameterType="Long" resultMap="SopUserLogsWxResult">
         <include refid="selectSopUserLogsWxVo"/>
         where id = #{id}
     </select>
-        
+
     <insert id="insertSopUserLogsWx" parameterType="SopUserLogsWx" useGeneratedKeys="true" keyProperty="id">
         insert into sop_user_logs_wx
         <trim prefix="(" suffix=")" suffixOverrides=",">
             <if test="sopId != null">sop_id,</if>
             <if test="sopTempId != null">sop_temp_id,</if>
-            <if test="qwUserNo != null">qw_user_no,</if>
-            <if test="qwUserId != null">qw_user_id,</if>
+            <if test="userWxName != null">user_wx_name,</if>
+            <if test="accountId != null">account_id,</if>
             <if test="companyUserId != null">company_user_id,</if>
             <if test="companyId != null">company_id,</if>
             <if test="startTime != null">start_time,</if>
@@ -54,8 +54,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="sopId != null">#{sopId},</if>
             <if test="sopTempId != null">#{sopTempId},</if>
-            <if test="qwUserNo != null">#{qwUserNo},</if>
-            <if test="qwUserId != null">#{qwUserId},</if>
+            <if test="userWxName != null">#{userWxName},</if>
+            <if test="accountId != null">#{accountId},</if>
             <if test="companyUserId != null">#{companyUserId},</if>
             <if test="companyId != null">#{companyId},</if>
             <if test="startTime != null">#{startTime},</if>
@@ -68,8 +68,8 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <trim prefix="SET" suffixOverrides=",">
             <if test="sopId != null">sop_id = #{sopId},</if>
             <if test="sopTempId != null">sop_temp_id = #{sopTempId},</if>
-            <if test="qwUserNo != null">qw_user_no = #{qwUserNo},</if>
-            <if test="qwUserId != null">qw_user_id = #{qwUserId},</if>
+            <if test="userWxName != null">user_wx_name = #{userWxName},</if>
+            <if test="accountId != null">account_id = #{accountId},</if>
             <if test="companyUserId != null">company_user_id = #{companyUserId},</if>
             <if test="companyId != null">company_id = #{companyId},</if>
             <if test="startTime != null">start_time = #{startTime},</if>
@@ -83,9 +83,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </delete>
 
     <delete id="deleteSopUserLogsWxByIds" parameterType="String">
-        delete from sop_user_logs_wx where id in 
+        delete from sop_user_logs_wx where id in
         <foreach item="id" collection="array" open="(" separator="," close=")">
             #{id}
         </foreach>
     </delete>
-</mapper>
+</mapper>

+ 74 - 0
fs-service/src/main/resources/mapper/wx/WxSopLogsMapper.xml

@@ -143,4 +143,78 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             #{id}
         </foreach>
     </delete>
+
+    <!-- 查询个微SOP执行记录列表(带关联信息) -->
+    <resultMap type="com.fs.wx.sop.vo.WxSopLogsListVO" id="WxSopLogsListVOResult">
+        <result property="id"    column="id"    />
+        <result property="sopId"    column="sop_id"    />
+        <result property="sopUserId"    column="sop_user_id"    />
+        <result property="accountId"    column="account_id"    />
+        <result property="accountName"    column="account_name"    />
+        <result property="wxContactId"    column="wx_contact_id"    />
+        <result property="wxContactName"    column="wx_contact_name"    />
+        <result property="tagNames"    column="tag_names"    />
+        <result property="wxRoomId"    column="wx_room_id"    />
+        <result property="wxRoomName"    column="wx_room_name"    />
+        <result property="type"    column="type"    />
+        <result property="sendType"    column="send_type"    />
+        <result property="generateType"    column="generate_type"    />
+        <result property="sendStatus"    column="send_status"    />
+        <result property="sendRemark"    column="send_remark"    />
+        <result property="sendSort"    column="send_sort"    />
+        <result property="expirationTime"    column="expiration_time"    />
+        <result property="createTime"    column="create_time"    />
+        <result property="realSendTime"    column="real_send_time"    />
+        <result property="remark"    column="remark"    />
+        <result property="fsUserId"    column="fs_user_id"    />
+        <result property="companyId"    column="company_id"    />
+    </resultMap>
+
+    <select id="selectWxSopLogsListBySopId" parameterType="com.fs.wx.sop.params.WxSopLogsParam" resultMap="WxSopLogsListVOResult">
+        SELECT 
+            wsl.id,
+            wsl.sop_id,
+            wsl.sop_user_id,
+            wsl.account_id,
+            cwa.wx_nick_name as account_name,
+            wsl.wx_contact_id,
+            wsl.wx_contact_name,
+            wsui.tag_names,
+            wsl.wx_room_id,
+            wsl.wx_room_name,
+            wsl.type,
+            wsl.send_type,
+            wsl.generate_type,
+            wsl.send_status,
+            wsl.send_remark,
+            wsl.send_sort,
+            wsl.expiration_time,
+            wsl.create_time,
+            wsl.update_time as real_send_time,
+            wsl.remark,
+            wsl.fs_user_id,
+            cwa.company_id
+        FROM wx_sop_logs wsl
+        LEFT JOIN wx_sop_user_info wsui ON wsl.sop_id = wsui.sop_id AND wsl.wx_contact_id = wsui.wx_contact_id
+        LEFT JOIN ylrz_his_scrm.company_wx_account cwa ON wsl.account_id = cwa.id
+        <where>
+            <if test="sopId != null">AND wsl.sop_id = #{sopId}</if>
+            <if test="sopUserId != null">AND wsl.sop_user_id = #{sopUserId}</if>
+            <if test="accountId != null">AND wsl.account_id = #{accountId}</if>
+            <if test="accountIdList != null and accountIdList.size() > 0">
+                AND wsl.account_id IN
+                <foreach collection="accountIdList" item="accId" open="(" separator="," close=")">
+                    #{accId}
+                </foreach>
+            </if>
+            <if test="wxContactName != null and wxContactName != ''">AND wsl.wx_contact_name LIKE CONCAT('%', #{wxContactName}, '%')</if>
+            <if test="wxContactId != null">AND wsl.wx_contact_id = #{wxContactId}</if>
+            <if test="sendStatus != null">AND wsl.send_status = #{sendStatus}</if>
+            <if test="sendType != null">AND wsl.send_type = #{sendType}</if>
+            <if test="type != null">AND wsl.type = #{type}</if>
+            <if test="scheduleStartTime != null and scheduleStartTime != ''">AND wsl.create_time >= #{scheduleStartTime}</if>
+            <if test="scheduleEndTime != null and scheduleEndTime != ''">AND wsl.create_time &lt;= #{scheduleEndTime}</if>
+        </where>
+        ORDER BY wsl.create_time DESC
+    </select>
 </mapper>

+ 72 - 8
fs-service/src/main/resources/mapper/wx/WxSopMapper.xml

@@ -3,7 +3,7 @@
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.fs.wx.sop.mapper.WxSopMapper">
-    
+
     <resultMap type="WxSop" id="WxSopResult">
         <result property="id"    column="id"    />
         <result property="name"    column="name"    />
@@ -20,16 +20,17 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="createBy"    column="create_by"    />
         <result property="updateTime"    column="update_time"    />
         <result property="updateBy"    column="update_by"    />
+        <result property="status"    column="status"    />
         <result property="remark"    column="remark"    />
     </resultMap>
 
     <sql id="selectWxSopVo">
-        select id, name, filter_type, select_tags, exclude_tags, temp_id, company_id, is_fixed, expiry_time, start_time, account_ids, create_time, create_by, update_time, update_by, remark from wx_sop
+        select id, name, filter_type, select_tags, exclude_tags, temp_id, company_id, is_fixed, expiry_time, start_time, account_ids, create_time, create_by, update_time, update_by, status, remark from wx_sop
     </sql>
 
     <select id="selectWxSopList" parameterType="WxSop" resultMap="WxSopResult">
         <include refid="selectWxSopVo"/>
-        <where>  
+        <where>
             <if test="name != null  and name != ''"> and name like concat('%', #{name}, '%')</if>
             <if test="filterType != null "> and filter_type = #{filterType}</if>
             <if test="selectTags != null  and selectTags != ''"> and select_tags = #{selectTags}</if>
@@ -40,14 +41,23 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="expiryTime != null "> and expiry_time = #{expiryTime}</if>
             <if test="startTime != null "> and start_time = #{startTime}</if>
             <if test="accountIds != null  and accountIds != ''"> and account_ids = #{accountIds}</if>
+            <if test="status != null "> and status = #{status}</if>
         </where>
     </select>
-    
+
     <select id="selectWxSopById" parameterType="Long" resultMap="WxSopResult">
         <include refid="selectWxSopVo"/>
         where id = #{id}
     </select>
-        
+
+    <select id="selectWxSopByIds" resultMap="WxSopResult">
+        <include refid="selectWxSopVo"/>
+        where id in
+        <foreach collection="array" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </select>
+
     <insert id="insertWxSop" parameterType="WxSop" useGeneratedKeys="true" keyProperty="id">
         insert into wx_sop
         <trim prefix="(" suffix=")" suffixOverrides=",">
@@ -65,6 +75,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="createBy != null">create_by,</if>
             <if test="updateTime != null">update_time,</if>
             <if test="updateBy != null">update_by,</if>
+            <if test="status != null">status,</if>
             <if test="remark != null">remark,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
@@ -82,6 +93,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="createBy != null">#{createBy},</if>
             <if test="updateTime != null">#{updateTime},</if>
             <if test="updateBy != null">#{updateBy},</if>
+            <if test="status != null">#{status},</if>
             <if test="remark != null">#{remark},</if>
          </trim>
     </insert>
@@ -103,19 +115,71 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="createBy != null">create_by = #{createBy},</if>
             <if test="updateTime != null">update_time = #{updateTime},</if>
             <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="status != null">status = #{status},</if>
             <if test="remark != null">remark = #{remark},</if>
         </trim>
         where id = #{id}
     </update>
 
+    <update id="updateStatusWxSopByIds" parameterType="map">
+        update wx_sop
+        SET status = #{arg1}
+        where id in
+        <foreach collection="arg0" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </update>
+
     <delete id="deleteWxSopById" parameterType="Long">
         delete from wx_sop where id = #{id}
     </delete>
 
-    <delete id="deleteWxSopByIds" parameterType="String">
-        delete from wx_sop where id in 
+    <delete id="deleteWxSopByIds">
+        delete from wx_sop where id in
         <foreach item="id" collection="array" open="(" separator="," close=")">
             #{id}
         </foreach>
     </delete>
-</mapper>
+
+    <!-- 根据标签筛选符合条件的客户 -->
+        <!-- 根据标签筛选符合条件的客户 -->
+    <select id="selectFilterWxSopCustomers" parameterType="com.fs.sop.params.WxSopTagsParam" resultType="com.fs.sop.vo.WxFilterSopCustomersResult">
+        SELECT DISTINCT
+            wc.id,
+            wc.nick_name AS name,
+            wc.account_id AS accountId,
+            wc.company_id AS cuCompanyId,
+            wc.company_user_id AS cuCompanyUserId,
+            cc.customer_id AS customerId
+        FROM
+            wx_contact wc
+        INNER JOIN crm_customer cc ON wc.customer_id = cc.customer_id
+        WHERE
+            1 = 1
+            <!-- 执行账号筛选 -->
+            <if test="accountIdsSelectList != null and accountIdsSelectList.size() > 0">
+                AND wc.account_id IN
+                <foreach collection="accountIdsSelectList" item="accountId" open="(" separator="," close=")">
+                    #{accountId}
+                </foreach>
+            </if>
+            <!-- 必须包含的标签(选择标签) - 通过字典表转换标签ID为标签名 -->
+            <if test="tagsIdsSelectList != null and tagsIdsSelectList.size() > 0">
+                <foreach collection="tagsIdsSelectList" item="tagId">
+                    AND FIND_IN_SET(
+                        (SELECT dict_label FROM sys_dict_data WHERE dict_type = 'crm_customer_tag' AND dict_value = #{tagId} LIMIT 1),
+                        cc.tags
+                    )
+                </foreach>
+            </if>
+            <!-- 排除的标签 - 通过字典表转换标签ID为标签名 -->
+            <if test="outTagsIdsSelectList != null and outTagsIdsSelectList.size() > 0">
+                <foreach collection="outTagsIdsSelectList" item="excludeTagId">
+                    AND NOT FIND_IN_SET(
+                        (SELECT dict_label FROM sys_dict_data WHERE dict_type = 'crm_customer_tag' AND dict_value = #{excludeTagId} LIMIT 1),
+                        cc.tags
+                    )
+                </foreach>
+            </if>
+    </select>
+</mapper>

+ 44 - 18
fs-service/src/main/resources/mapper/wx/WxSopUserInfoMapper.xml

@@ -3,12 +3,13 @@
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.fs.wx.sop.mapper.WxSopUserInfoMapper">
-    
+
     <resultMap type="WxSopUserInfo" id="WxSopUserInfoResult">
         <result property="id"    column="id"    />
         <result property="sopId"    column="sop_id"    />
         <result property="sopUserId"    column="sop_user_id"    />
         <result property="wxContactId"    column="wx_contact_id"    />
+        <result property="customerId"    column="customer_id"    />
         <result property="fsUserId"    column="fs_user_id"    />
         <result property="isDaysNotStudy"    column="is_days_not_study"    />
         <result property="finishCout"    column="finish_cout"    />
@@ -21,39 +22,48 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         <result property="updateTime"    column="update_time"    />
         <result property="updateBy"    column="update_by"    />
         <result property="remark"    column="remark"    />
+        <result property="tagNames"    column="tag_names"    />
     </resultMap>
 
     <sql id="selectWxSopUserInfoVo">
-        select id, sop_id, sop_user_id, wx_contact_id, fs_user_id, is_days_not_study, finish_cout, finish_time, finish_course_days, grade, status, create_time, create_by, update_time, update_by, remark from wx_sop_user_info
+        select wsui.id, wsui.sop_id, wsui.sop_user_id, wsui.wx_contact_id, wsui.customer_id, wsui.fs_user_id, wsui.is_days_not_study,
+               wsui.finish_cout, wsui.finish_time, wsui.finish_course_days, wsui.grade, wsui.status,
+               wsui.create_time, wsui.create_by, wsui.update_time, wsui.update_by, wsui.remark,
+               COALESCE(wsui.tag_names, cc.tags) as tag_names
+        from wx_sop_user_info wsui
+        left join ylrz_his_scrm.wx_contact wc on wsui.wx_contact_id = wc.id
+        left join ylrz_his_scrm.crm_customer cc on COALESCE(wc.customer_id, wsui.customer_id) = cc.customer_id
     </sql>
 
     <select id="selectWxSopUserInfoList" parameterType="WxSopUserInfo" resultMap="WxSopUserInfoResult">
         <include refid="selectWxSopUserInfoVo"/>
-        <where>  
-            <if test="sopId != null "> and sop_id = #{sopId}</if>
-            <if test="sopUserId != null "> and sop_user_id = #{sopUserId}</if>
-            <if test="wxContactId != null "> and wx_contact_id = #{wxContactId}</if>
-            <if test="fsUserId != null "> and fs_user_id = #{fsUserId}</if>
-            <if test="isDaysNotStudy != null "> and is_days_not_study = #{isDaysNotStudy}</if>
-            <if test="finishCout != null "> and finish_cout = #{finishCout}</if>
-            <if test="finishTime != null "> and finish_time = #{finishTime}</if>
-            <if test="finishCourseDays != null "> and finish_course_days = #{finishCourseDays}</if>
-            <if test="grade != null "> and grade = #{grade}</if>
-            <if test="status != null "> and status = #{status}</if>
+        <where>
+            <if test="sopId != null "> and wsui.sop_id = #{sopId}</if>
+            <if test="sopUserId != null "> and wsui.sop_user_id = #{sopUserId}</if>
+            <if test="wxContactId != null "> and wsui.wx_contact_id = #{wxContactId}</if>
+            <if test="customerId != null "> and wsui.customer_id = #{customerId}</if>
+            <if test="fsUserId != null "> and wsui.fs_user_id = #{fsUserId}</if>
+            <if test="isDaysNotStudy != null "> and wsui.is_days_not_study = #{isDaysNotStudy}</if>
+            <if test="finishCout != null "> and wsui.finish_cout = #{finishCout}</if>
+            <if test="finishTime != null "> and wsui.finish_time = #{finishTime}</if>
+            <if test="finishCourseDays != null "> and wsui.finish_course_days = #{finishCourseDays}</if>
+            <if test="grade != null "> and wsui.grade = #{grade}</if>
+            <if test="status != null "> and wsui.status = #{status}</if>
         </where>
     </select>
-    
+
     <select id="selectWxSopUserInfoById" parameterType="Long" resultMap="WxSopUserInfoResult">
         <include refid="selectWxSopUserInfoVo"/>
-        where id = #{id}
+        where wsui.id = #{id}
     </select>
-        
+
     <insert id="insertWxSopUserInfo" parameterType="WxSopUserInfo" useGeneratedKeys="true" keyProperty="id">
         insert into wx_sop_user_info
         <trim prefix="(" suffix=")" suffixOverrides=",">
             <if test="sopId != null">sop_id,</if>
             <if test="sopUserId != null">sop_user_id,</if>
             <if test="wxContactId != null">wx_contact_id,</if>
+            <if test="customerId != null">customer_id,</if>
             <if test="fsUserId != null">fs_user_id,</if>
             <if test="isDaysNotStudy != null">is_days_not_study,</if>
             <if test="finishCout != null">finish_cout,</if>
@@ -66,11 +76,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="updateTime != null">update_time,</if>
             <if test="updateBy != null">update_by,</if>
             <if test="remark != null">remark,</if>
+            <if test="tagNames != null">tag_names,</if>
          </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="sopId != null">#{sopId},</if>
             <if test="sopUserId != null">#{sopUserId},</if>
             <if test="wxContactId != null">#{wxContactId},</if>
+            <if test="customerId != null">#{customerId},</if>
             <if test="fsUserId != null">#{fsUserId},</if>
             <if test="isDaysNotStudy != null">#{isDaysNotStudy},</if>
             <if test="finishCout != null">#{finishCout},</if>
@@ -83,6 +95,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="updateTime != null">#{updateTime},</if>
             <if test="updateBy != null">#{updateBy},</if>
             <if test="remark != null">#{remark},</if>
+            <if test="tagNames != null">#{tagNames},</if>
          </trim>
     </insert>
 
@@ -92,6 +105,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="sopId != null">sop_id = #{sopId},</if>
             <if test="sopUserId != null">sop_user_id = #{sopUserId},</if>
             <if test="wxContactId != null">wx_contact_id = #{wxContactId},</if>
+            <if test="customerId != null">customer_id = #{customerId},</if>
             <if test="fsUserId != null">fs_user_id = #{fsUserId},</if>
             <if test="isDaysNotStudy != null">is_days_not_study = #{isDaysNotStudy},</if>
             <if test="finishCout != null">finish_cout = #{finishCout},</if>
@@ -104,6 +118,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="updateTime != null">update_time = #{updateTime},</if>
             <if test="updateBy != null">update_by = #{updateBy},</if>
             <if test="remark != null">remark = #{remark},</if>
+            <if test="tagNames != null">tag_names = #{tagNames},</if>
         </trim>
         where id = #{id}
     </update>
@@ -113,9 +128,20 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </delete>
 
     <delete id="deleteWxSopUserInfoByIds" parameterType="String">
-        delete from wx_sop_user_info where id in 
+        delete from wx_sop_user_info where id in
         <foreach item="id" collection="array" open="(" separator="," close=")">
             #{id}
         </foreach>
     </delete>
-</mapper>
+
+    <select id="selectWxSopUserInfoByCondition" parameterType="WxSopUserInfo" resultMap="WxSopUserInfoResult">
+        <include refid="selectWxSopUserInfoVo"/>
+        <where>
+            <if test="sopId != null">and wsui.sop_id = #{sopId}</if>
+            <if test="sopUserId != null">and wsui.sop_user_id = #{sopUserId}</if>
+            <if test="wxContactId != null">and wsui.wx_contact_id = #{wxContactId}</if>
+            <if test="customerId != null">and wsui.customer_id = #{customerId}</if>
+        </where>
+        limit 1
+    </select>
+</mapper>

+ 24 - 7
fs-service/src/main/resources/mapper/wx/WxSopUserMapper.xml

@@ -3,7 +3,7 @@
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.fs.wx.sop.mapper.WxSopUserMapper">
-    
+
     <resultMap type="WxSopUser" id="WxSopUserResult">
         <result property="id"    column="id"    />
         <result property="type"    column="type"    />
@@ -24,22 +24,28 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </sql>
 
     <select id="selectWxSopUserList" parameterType="WxSopUser" resultMap="WxSopUserResult">
-        <include refid="selectWxSopUserVo"/>
-        <where>  
+        SELECT
+        w.id,w.type, w.sop_id, w.account_id, w.start_time, w.chat_id, w.status,
+        w.create_time, w.create_by, w.update_time, w.update_by, w.remark,
+        c.wx_nick_name AS account_name
+        FROM wx_sop_user w
+        LEFT JOIN ylrz_his_scrm.company_wx_account c ON w.account_id = c.id
+        <where>
             <if test="type != null "> and type = #{type}</if>
             <if test="sopId != null "> and sop_id = #{sopId}</if>
             <if test="accountId != null "> and account_id = #{accountId}</if>
+            <if test="accountName != null "> and account_name = #{accountName}</if>
             <if test="startTime != null "> and start_time = #{startTime}</if>
             <if test="chatId != null  and chatId != ''"> and chat_id = #{chatId}</if>
             <if test="status != null "> and status = #{status}</if>
         </where>
     </select>
-    
+
     <select id="selectWxSopUserById" parameterType="Long" resultMap="WxSopUserResult">
         <include refid="selectWxSopUserVo"/>
         where id = #{id}
     </select>
-        
+
     <insert id="insertWxSopUser" parameterType="WxSopUser" useGeneratedKeys="true" keyProperty="id">
         insert into wx_sop_user
         <trim prefix="(" suffix=")" suffixOverrides=",">
@@ -93,7 +99,7 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </delete>
 
     <delete id="deleteWxSopUserByIds" parameterType="String">
-        delete from wx_sop_user where id in 
+        delete from wx_sop_user where id in
         <foreach item="id" collection="array" open="(" separator="," close=")">
             #{id}
         </foreach>
@@ -102,4 +108,15 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <delete id="deleteBySopId" parameterType="Long">
         delete from wx_sop_user where sop_id = #{sopId}
     </delete>
-</mapper>
+
+    <select id="selectwxSopUser" parameterType="WxSopUser" resultMap="WxSopUserResult">
+        <include refid="selectWxSopUserVo"/>
+        <where>
+            <if test="sopId != null">and sop_id = #{sopId}</if>
+            <if test="type != null">and type = #{type}</if>
+            <if test="accountId != null">and account_id = #{accountId}</if>
+            <if test="startTime != null">and start_time = #{startTime}</if>
+        </where>
+        LIMIT 1
+    </select>
+</mapper>

+ 23 - 8
fs-wx-task/src/main/java/com/fs/app/service/WxTaskService.java

@@ -36,6 +36,8 @@ import com.fs.wxcid.dto.friend.AddContactParam;
 import com.fs.wxcid.service.FriendService;
 import com.fs.wxcid.vo.AddContactVo;
 import com.fs.wxwork.dto.WxAddSearchDTO;
+import com.fs.wxwork.dto.WxSearchContactDTO;
+import com.fs.wxwork.dto.WxSearchContactResp;
 import com.fs.wxwork.dto.WxWorkResponseDTO;
 import com.fs.wxwork.service.WxWorkService;
 import lombok.AllArgsConstructor;
@@ -877,7 +879,7 @@ public class WxTaskService {
                 if (client != null) {
                     CrmCustomer crmCustomer = crmCustomerService.selectCrmCustomerById(client.getCustomerId());
                     // 开始申请加微
-                    WxWorkResponseDTO<String> resp = qwAddWxInvokeIpad(crmCustomer.getMobile(), qwUser.getUid(), qwUser.getServerId());
+                    WxWorkResponseDTO<String> resp = qwAddWxInvokeIpad(crmCustomer.getMobile(), qwUser.getUid(),qwUser.getServerId(),qwUser.getVid(),qwUser.getQwUserName());
                     JSONObject runParam = new JSONObject();
                     runParam.put("qwId", qwUser.getId());
                     runParam.put("mobile", crmCustomer.getMobile());
@@ -935,7 +937,7 @@ public class WxTaskService {
         try {
             //is_add = 2,状态为加微中且是企微类型
             List<CompanyWxClient> clients = companyWxClientService.getQwAddWxList(accountIdList, 2);
-            log.info("企微申请加微结果查询任务需要查询的数量:{}", clients.size());
+           log.info("企微申请加微结果查询任务需要查询的数量:{}", clients.size());
 
             if (clients.isEmpty()) return;
             // 处理每个客户的加微结果
@@ -1010,22 +1012,35 @@ public class WxTaskService {
      * @param serverId   服务器id
      * @return String 结果
      */
-    private WxWorkResponseDTO<String> qwAddWxInvokeIpad(String mobile, String qwUid, Long serverId) {
+    private WxWorkResponseDTO<String> qwAddWxInvokeIpad(String mobile, String qwUid, Long serverId,String vid,String qwUserName) {
         if (StringUtils.isBlank(mobile) || StringUtils.isBlank(qwUid) || serverId == null) {
             log.warn("企微申请加好友任务参数校验失败: mobile={}, qwUid={}, serverId={}", mobile, qwUid, serverId);
             return null;
         }
+
         try {
             WxAddSearchDTO wxAddSearchDTO = new WxAddSearchDTO();
             wxAddSearchDTO.setUuid(qwUid);
+            wxAddSearchDTO.setVid(Long.valueOf(vid));
             wxAddSearchDTO.setPhone(mobile);
-            wxAddSearchDTO.setVid(null);
-            wxAddSearchDTO.setOptionid(null);
-            wxAddSearchDTO.setContent(null);
-            wxAddSearchDTO.setTicket(null);
+
+            WxSearchContactDTO contactDTO=new WxSearchContactDTO();
+            contactDTO.setUuid(qwUid);
+            contactDTO.setPhoneNumber(mobile);
+
+
+            WxWorkResponseDTO<WxSearchContactResp> respWxWorkResponseDTO = wxWorkService.searchContact(contactDTO, serverId);
+            WxSearchContactResp.UserList user = respWxWorkResponseDTO.getData().getUserList().stream()
+                    .filter(u -> u.getState().equals("2"))
+                    .findFirst()
+                    .orElse(null); // 或者 .orElseThrow(() -> new RuntimeException("未找到指定用户"))
+
+            wxAddSearchDTO.setOptionid(user.getOpenid());
+            wxAddSearchDTO.setTicket(user.getTicket());
+            wxAddSearchDTO.setContent("你好,我是你的专属助手:"+qwUserName+",有什么问题都可以问我哦~");
 
             WxWorkResponseDTO<String> response = wxWorkService.addSearch(wxAddSearchDTO, serverId);
-            log.debug("企微申请加好友任务调用结果: errcode={}, errmsg={}",
+            log.debug("企微加微接口调用结果: errcode={}, errmsg={}",
                     response != null ? response.getErrcode() : "null",
                     response != null ? response.getErrmsg() : "null");