lmx преди 3 дни
родител
ревизия
e03bfad3ee

+ 27 - 0
fs-company/src/main/java/com/fs/company/controller/company/GeneralCustomerEntryController.java

@@ -0,0 +1,27 @@
+package com.fs.company.controller.company;
+
+import com.fs.common.core.domain.R;
+import com.fs.company.service.IGeneralCustomerEntryService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+/**
+ * @author MixLiu
+ * @date 2026/3/17 15:01
+ * @description
+ */
+@RestController
+@RequestMapping("/company/general/customer")
+public class GeneralCustomerEntryController {
+
+    @Autowired
+    IGeneralCustomerEntryService iGeneralCustomerEntryService;
+
+    @PostMapping("/entryCustomer")
+    public R entryCustomer(String param){
+       return iGeneralCustomerEntryService.entryCustomer(param);
+    }
+
+}

+ 2 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticMapper.java

@@ -72,4 +72,6 @@ public interface CompanyVoiceRoboticMapper extends BaseMapper<CompanyVoiceRoboti
     int finishAddWxRobotic(@Param("collect") List<Long> collect);
 
     List<DictVO> getDictDataList(@Param("dictType") String dictType);
+
+    List<CompanyVoiceRobotic> selectSceneTaskByCompanyIdAndType(@Param("companyId") Long companyId, @Param("sceneType") Integer sceneType);
 }

+ 2 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyVoiceRoboticWxMapper.java

@@ -68,4 +68,6 @@ public interface CompanyVoiceRoboticWxMapper extends BaseMapper<CompanyVoiceRobo
     List<CompanyVoiceRoboticWx> selectByRoboticIdWithGroupBy(@Param("id") Long id);
 
     List<CompanyVoiceRoboticWx> selectByRoboticIdQw(@Param("id") Long id, @Param("intention") String intention);
+
+    CompanyVoiceRoboticWx selectAllocationTargetByTaskId(@Param("roboticId") Long roboticId);
 }

+ 166 - 0
fs-service/src/main/java/com/fs/company/param/EntryCustomerParam.java

@@ -0,0 +1,166 @@
+package com.fs.company.param;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import javax.validation.constraints.NotBlank;
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * @author MixLiu
+ * @date 2026/3/17 15:42
+ * @description
+ */
+@Data
+public class EntryCustomerParam {
+
+    /**主键*/
+    private Long customerId;
+
+    /** 组织机构代码 */
+    private String customerCode;
+
+    /** 客户名称 */
+    private String customerName;
+
+    /**
+     * 客户的企微名称
+     */
+    private String qwName;
+
+    /** 手机 */
+    //必传
+    private String mobile;
+
+    /** 流量来源 */
+    private String trafficSource;
+
+    /** 性别 */
+    private Integer sex;
+
+    /** $column.columnComment */
+    private String weixin;
+
+    /** 关联用户ID */
+    private Long userId;
+
+    /** 创建人ID */
+    private Long createUserId;
+
+    /** 当前认领用户 */
+    private Long receiveUserId;
+
+    /** 认领ID */
+    private Long customerUserId;
+
+    /** 省市区 */
+    private String address;
+
+    private String cityIds;
+
+    /** 定位信息 */
+    private String location;
+
+    /** 详细地址 */
+    private String detailAddress;
+
+    /** 地理位置经度 */
+    private String lng;
+
+    /** 地理位置维度 */
+    private String lat;
+
+    /** 客户状态  0锁定 1 正常 */
+    private Integer status;
+
+    /** 1 已认领 0未认领 */
+    private Integer isReceive;
+
+    /** 0 手动导入 1 自动导入 */
+    private Integer importType;
+
+
+
+    /** 所属部门ID */
+    private Long deptId;
+
+    /** 是否删除 */
+    private Integer isDel;
+
+    /** 客户类型 */
+    private Integer customerType;
+
+    /** 最后一次跟进时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date receiveTime;
+
+    /** 入公海时间 */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date poolTime;
+
+    /** $column.columnComment */
+    //必传
+    private Long companyId;
+
+    /** 是否为线索客户 */
+    private Integer isLine;
+
+    /** 客户来源 */
+    private String source;
+
+    /** 标签 */
+    private String tags;
+
+    private String extJson;
+    //跟进阶段
+    private Integer visitStatus;
+    //进线日期
+    private String registerDate;
+    //进线客户连接
+    private String registerLinkUrl;
+    //进线客户详情
+    private String registerDesc;
+    //进线客户填写时间
+    private String registerSubmitTime;
+
+    /** 流量平台线索归属账号 */
+    private String thirdAccount;
+
+    /** 流量平台线索Id */
+    private String clueId;
+    //是否在公海
+    private Integer isPool;
+    //进线方式
+    private String registerType;
+    //消费金额
+    private BigDecimal payMoney;
+    //购买次数
+    private Integer buyCount;
+    //来源渠道编码
+    private String sourceCode;
+    //推线时间
+    private String pushTime;
+    //推线编码
+    private String pushCode;
+    private String intention;
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private Date visitTime;
+    // 购买商品
+    private String goodsName;
+    // 购买规格
+    private String goodsSpecification;
+    // 店铺名称
+    private String shopName;
+    // 平台名称
+    private String platformName;
+
+    //场景类型,字典task_scene_type
+    private Integer sceneType;
+    //对话图
+    private String dialogue;
+
+}

+ 7 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyVoiceRoboticService.java

@@ -95,4 +95,11 @@ public interface ICompanyVoiceRoboticService extends IService<CompanyVoiceRoboti
     Map<String, Object> getExecRecords(Long roboticId, Integer pageNum, Integer pageSize,String customerName,String customerPhone, Boolean onlyCallNode);
 
     void finishAddWxByCallees(Set<Long> roboticIds);
+
+    /**
+     *
+     * @param taskId
+     * @param crmCustomerId
+     */
+    void addNewExec4Task(Long taskId,Long crmCustomerId);
 }

+ 16 - 0
fs-service/src/main/java/com/fs/company/service/IGeneralCustomerEntryService.java

@@ -0,0 +1,16 @@
+package com.fs.company.service;
+
+import com.fs.common.core.domain.R;
+import com.fs.company.param.EntryCustomerParam;
+
+/**
+ * @author MixLiu
+ * @date 2026/3/17 15:49
+ * @description
+ */
+public interface IGeneralCustomerEntryService {
+
+    R entryCustomer(String param);
+
+    String entryCustomer(EntryCustomerParam param);
+}

Файловите разлики са ограничени, защото са твърде много
+ 292 - 173
fs-service/src/main/java/com/fs/company/service/impl/CompanyVoiceRoboticServiceImpl.java


+ 4 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowEngineImpl.java

@@ -540,4 +540,8 @@ public class CompanyWorkflowEngineImpl implements CompanyWorkflowEngine {
         }
     }
 
+    public void workFlowAddExec(){
+
+    }
+
 }

+ 197 - 0
fs-service/src/main/java/com/fs/company/service/impl/GeneralCustomerEntryServiceImpl.java

@@ -0,0 +1,197 @@
+package com.fs.company.service.impl;
+
+import com.alibaba.fastjson.JSONObject;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.StringUtils;
+import com.fs.company.domain.CompanyVoiceRobotic;
+import com.fs.company.mapper.CompanyVoiceRoboticMapper;
+import com.fs.company.param.EntryCustomerParam;
+import com.fs.company.service.ICompanyVoiceRoboticService;
+import com.fs.company.service.IGeneralCustomerEntryService;
+import com.fs.company.util.CryptoUtil;
+import com.fs.company.util.PhoneNumberUtil;
+import com.fs.crm.domain.CrmCustomer;
+import com.fs.crm.mapper.CrmCustomerMapper;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalTime;
+import java.util.HashMap;
+import java.util.List;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+/**
+ * @author MixLiu
+ * @date 2026/3/17 15:49
+ * @description
+ */
+@Service
+@Slf4j
+public class GeneralCustomerEntryServiceImpl implements IGeneralCustomerEntryService {
+
+    @Autowired
+    @Qualifier("crmCustomerExecutor")
+    Executor customerExecutor;
+    @Autowired
+    CrmCustomerMapper crmCustomerMapper;
+    @Autowired
+    CompanyVoiceRoboticMapper companyVoiceRoboticMapper;
+    @Autowired
+    ICompanyVoiceRoboticService companyVoiceRoboticService;
+
+    /**
+     * 录入客户
+     *
+     * @param param
+     * @return
+     */
+    @Override
+    public R entryCustomer(String param) {
+        try {
+            String decryptParam = CryptoUtil.decrypt(param);
+            if (StringUtils.isBlank(decryptParam)) {
+                return R.error("参数错误");
+            }
+            List<EntryCustomerParam> list = JSONObject.parseArray(decryptParam, EntryCustomerParam.class);
+            CompletableFuture.runAsync(() -> handleList(list), customerExecutor);
+        } catch (Exception ex) {
+            log.error("录入客户异常", ex);
+        }
+        return R.ok().put("result", "录入成功");
+    }
+
+    @Override
+    @Async("crmCustomerExecutor")
+    public String entryCustomer(EntryCustomerParam param) {
+        handleData(param);
+        return "success";
+    }
+
+    /**
+     * 处理外部来源客户数据
+     *
+     * @param list
+     */
+    public void handleList(List<EntryCustomerParam> list) {
+        list.forEach(a -> {
+            handleData(a);
+        });
+    }
+
+    /**
+     * 处理单条数据
+     * @param data
+     */
+    public void handleData(EntryCustomerParam data) {
+        //客户数据校验
+        Boolean b = validateCustomerData(data);
+        if (!b) {
+            log.error("客户数据校验失败,{}", data);
+            return;
+        }
+        //客户数据解析,是否包含对话图 对话图解析标签&意向度标识 todo 庄旭组在研发此功能
+        if (StringUtils.isNotBlank(data.getDialogue())) {
+            JSONObject jsonObject = analysisDialogue(data.getDialogue());
+            if (jsonObject != null) {
+                data.setIntention(jsonObject.getString("intention"));
+                data.setTags(jsonObject.getString("tags"));
+            }
+        }
+        //客户数据插入
+        insertCrmCustomer(data);
+        if(null != data.getCompanyId() && null != data.getSceneType()){
+            //公司场景任务读取
+            CompanyVoiceRobotic companySceneTasks = getCompanySceneTask(data);
+            if(null != companySceneTasks){
+                //场景任务存在 加入场景任务队列
+                companyVoiceRoboticService.addNewExec4Task(companySceneTasks.getId(),data.getCustomerId());
+            }
+        }
+    }
+
+    /**
+     * 校验客户数据
+     * @param param
+     * @return
+     */
+    public Boolean validateCustomerData(EntryCustomerParam param) {
+        boolean valid = PhoneNumberUtil.isValid(param.getMobile());
+        if(!valid){
+            log.error("手机号格式错误,{}", param.getMobile());
+            return false;
+        }
+        Long  customerId = crmCustomerMapper.selectCrmCustomerByCrmMobile(param.getMobile());
+        // todo 添加配置是否允许重复客户导入
+        if( null != customerId && true){
+            return false;
+        }
+        return true;
+    }
+
+    /**
+     * 解析对话图 得到意向度以及标签
+     *
+     * @param dialogue
+     * @return
+     */
+    public JSONObject analysisDialogue(String dialogue) {
+        return null;
+    }
+
+    /**
+     * 插入crmcustomer
+     * @param data
+     * @return
+     */
+    public void insertCrmCustomer(EntryCustomerParam data){
+        CrmCustomer insertObj = new CrmCustomer();
+        BeanUtils.copyProperties(data,insertObj);
+        crmCustomerMapper.insertCrmCustomer(insertObj);
+        data.setCustomerId(insertObj.getCustomerId());
+    }
+
+    /**
+     * 获取公司场景任务
+     * @param data
+     * @return
+     */
+    public CompanyVoiceRobotic getCompanySceneTask(EntryCustomerParam data){
+        List<CompanyVoiceRobotic> companyVoiceRobotics = companyVoiceRoboticMapper.selectSceneTaskByCompanyIdAndType(data.getCompanyId(), data.getSceneType());
+        if(null != companyVoiceRobotics && !companyVoiceRobotics.isEmpty()){
+            List<CompanyVoiceRobotic> resList = companyVoiceRobotics.stream()
+                    .filter(item -> verificationTime(item.getAvailableStartTime(), item.getAvailableEndTime()))
+                    .collect(Collectors.toList());
+            if(null !=  resList && !resList.isEmpty()){
+                return resList.get(0);
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 校验当前时间是否在指定时间区间内
+     * 支持跨天凌晨区间(如22:00-06:00)
+     *
+     * @param start 开始时间
+     * @param end   结束时间
+     * @return true-在区间内,false-不在区间内
+     */
+    public Boolean verificationTime(LocalTime start, LocalTime end) {
+        if (start == null || end == null) {
+            return false;
+        }
+        LocalTime now = LocalTime.now();
+        if (!start.isAfter(end)) {
+            return !now.isBefore(start) && !now.isAfter(end);
+        }
+        return !now.isBefore(start) || !now.isAfter(end);
+    }
+
+}

+ 323 - 0
fs-service/src/main/java/com/fs/company/util/CryptoUtil.java

@@ -0,0 +1,323 @@
+package com.fs.company.util;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * 加密解密工具类
+ * 用于接口入参解析、返回结果加解密处理
+ *
+ */
+public class CryptoUtil {
+
+    private static final Logger log = LoggerFactory.getLogger(CryptoUtil.class);
+
+    public static void main(String[] args) {
+        JSONObject jsonObject = new JSONObject();
+        jsonObject.put("name", "MixLiu");
+        jsonObject.put("age", 18);
+        jsonObject.put("sex", "男");
+        jsonObject.put("occupation","engineer");
+        String encrypt = encrypt(jsonObject.toJSONString());
+        System.out.println(encrypt);
+        String decrypt = decrypt(encrypt);
+        System.out.println(decrypt);
+        System.out.println(JSONObject.parse(decrypt));
+    }
+    // ==================== 秘钥配置 ====================
+    /**
+     * AES加密秘钥
+     * 【重要】请根据实际业务需求修改此秘钥
+     * 秘钥长度必须为16位(AES-128)、24位(AES-192)或32位(AES-256)
+     * 当前使用AES-128,秘钥长度为16位
+     */
+    private static final String AES_SECRET_KEY = "FsCmp@nyK3y!2026";
+
+    /**
+     * 备用秘钥(用于秘钥轮换场景)
+     * 【重要】生产环境请修改为不同的秘钥
+     */
+    private static final String AES_BACKUP_KEY = "BkFC0mp@nyK3y!26";
+
+    /**
+     * AES加密算法/模式/填充方式
+     * AES/ECB/PKCS5Padding:AES加密,ECB模式,PKCS5填充
+     */
+    private static final String AES_ALGORITHM = "AES/ECB/PKCS5Padding";
+
+    /**
+     * AES算法名称
+     */
+    private static final String AES_KEY_ALGORITHM = "AES";
+
+    // ==================== 加密方法 ====================
+
+    /**
+     * AES加密
+     * 用于加密入参或返回结果
+     *
+     * @param plainText 明文
+     * @return Base64编码的密文,加密失败返回null
+     */
+    public static String encrypt(String plainText) {
+        return encrypt(plainText, AES_SECRET_KEY);
+    }
+
+    /**
+     * AES加密(使用指定秘钥)
+     *
+     * @param plainText 明文
+     * @param secretKey 秘钥
+     * @return Base64编码的密文,加密失败返回null
+     */
+    public static String encrypt(String plainText, String secretKey) {
+        if (plainText == null || secretKey == null) {
+            log.warn("加密参数不能为空");
+            return null;
+        }
+        try {
+            SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), AES_KEY_ALGORITHM);
+            Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
+            cipher.init(Cipher.ENCRYPT_MODE, keySpec);
+            byte[] encryptedBytes = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
+            return Base64.getEncoder().encodeToString(encryptedBytes);
+        } catch (Exception e) {
+            log.error("AES加密失败: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 加密入参
+     * 将原始入参字符串加密后返回
+     *
+     * @param inputParam 原始入参
+     * @return 加密后的入参字符串
+     */
+    public static String encryptInput(String inputParam) {
+        log.debug("加密入参");
+        return encrypt(inputParam);
+    }
+
+    /**
+     * 加密返回结果
+     * 将返回结果对象转换为JSON字符串后加密
+     *
+     * @param result 返回结果对象
+     * @return 加密后的结果字符串
+     */
+    public static String encryptResult(Object result) {
+        if (result == null) {
+            return null;
+        }
+        String jsonStr;
+        if (result instanceof String) {
+            jsonStr = (String) result;
+        } else {
+            jsonStr = JSON.toJSONString(result);
+        }
+        log.debug("加密返回结果");
+        return encrypt(jsonStr);
+    }
+
+    // ==================== 解密方法 ====================
+
+    /**
+     * AES解密
+     * 用于解密入参或返回结果
+     *
+     * @param encryptedText Base64编码的密文
+     * @return 解密后的明文,解密失败返回null
+     */
+    public static String decrypt(String encryptedText) {
+        return decrypt(encryptedText, AES_SECRET_KEY);
+    }
+
+    /**
+     * AES解密(使用指定秘钥)
+     *
+     * @param encryptedText Base64编码的密文
+     * @param secretKey     秘钥
+     * @return 解密后的明文,解密失败返回null
+     */
+    public static String decrypt(String encryptedText, String secretKey) {
+        if (encryptedText == null || secretKey == null) {
+            log.warn("解密参数不能为空");
+            return null;
+        }
+        try {
+            SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(StandardCharsets.UTF_8), AES_KEY_ALGORITHM);
+            Cipher cipher = Cipher.getInstance(AES_ALGORITHM);
+            cipher.init(Cipher.DECRYPT_MODE, keySpec);
+            byte[] decryptedBytes = cipher.doFinal(Base64.getDecoder().decode(encryptedText));
+            return new String(decryptedBytes, StandardCharsets.UTF_8);
+        } catch (Exception e) {
+            log.error("AES解密失败: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 解密入参
+     * 将加密的入参字符串解密
+     *
+     * @param encryptedInput 加密的入参
+     * @return 解密后的入参字符串
+     */
+    public static String decryptInput(String encryptedInput) {
+        log.debug("解密入参");
+        return decrypt(encryptedInput);
+    }
+
+    /**
+     * 解密返回结果
+     * 将加密的返回结果解密
+     *
+     * @param encryptedResult 加密的返回结果
+     * @return 解密后的结果字符串
+     */
+    public static String decryptResult(String encryptedResult) {
+        log.debug("解密返回结果");
+        return decrypt(encryptedResult);
+    }
+
+    // ==================== 解析方法 ====================
+
+    /**
+     * 解析入参(解密后解析为JSON对象)
+     * 先解密入参,再将解密后的字符串解析为JSONObject
+     *
+     * @param encryptedInput 加密的入参
+     * @return 解析后的JSONObject,解析失败返回null
+     */
+    public static JSONObject parseInput(String encryptedInput) {
+        String decryptedStr = decryptInput(encryptedInput);
+        if (decryptedStr == null) {
+            log.warn("入参解密失败,无法解析");
+            return null;
+        }
+        try {
+            return JSON.parseObject(decryptedStr);
+        } catch (Exception e) {
+            log.error("入参JSON解析失败: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 解析入参(解密后解析为指定类型对象)
+     * 先解密入参,再将解密后的字符串解析为指定类型的对象
+     *
+     * @param encryptedInput 加密的入参
+     * @param clazz          目标类型
+     * @param <T>            泛型类型
+     * @return 解析后的对象,解析失败返回null
+     */
+    public static <T> T parseInput(String encryptedInput, Class<T> clazz) {
+        String decryptedStr = decryptInput(encryptedInput);
+        if (decryptedStr == null) {
+            log.warn("入参解密失败,无法解析");
+            return null;
+        }
+        try {
+            return JSON.parseObject(decryptedStr, clazz);
+        } catch (Exception e) {
+            log.error("入参解析为{}失败: {}", clazz.getSimpleName(), e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 解析返回结果(解密后解析为JSON对象)
+     * 先解密返回结果,再将解密后的字符串解析为JSONObject
+     *
+     * @param encryptedResult 加密的返回结果
+     * @return 解析后的JSONObject,解析失败返回null
+     */
+    public static JSONObject parseResult(String encryptedResult) {
+        String decryptedStr = decryptResult(encryptedResult);
+        if (decryptedStr == null) {
+            log.warn("返回结果解密失败,无法解析");
+            return null;
+        }
+        try {
+            return JSON.parseObject(decryptedStr);
+        } catch (Exception e) {
+            log.error("返回结果JSON解析失败: {}", e.getMessage(), e);
+            return null;
+        }
+    }
+
+    /**
+     * 解析返回结果(解密后解析为指定类型对象)
+     * 先解密返回结果,再将解密后的字符串解析为指定类型的对象
+     *
+     * @param encryptedResult 加密的返回结果
+     * @param clazz           目标类型
+     * @param <T>             泛型类型
+     * @return 解析后的对象,解析失败返回null
+     */
+    public static <T> T parseResult(String encryptedResult, Class<T> clazz) {
+        String decryptedStr = decryptResult(encryptedResult);
+        if (decryptedStr == null) {
+            log.warn("返回结果解密失败,无法解析");
+            return null;
+        }
+        try {
+            return JSON.parseObject(decryptedStr, clazz);
+        } catch (Exception e) {
+            log.error("返回结果解析为{}失败: {}", clazz.getSimpleName(), e.getMessage(), e);
+            return null;
+        }
+    }
+
+    // ==================== 秘钥轮换相关 ====================
+
+    /**
+     * 使用备用秘钥解密(用于秘钥轮换场景)
+     * 先尝试主秘钥解密,失败后尝试备用秘钥
+     *
+     * @param encryptedText 加密文本
+     * @return 解密后的明文
+     */
+    public static String decryptWithFallback(String encryptedText) {
+        String result = decrypt(encryptedText, AES_SECRET_KEY);
+        if (result == null) {
+            log.info("主秘钥解密失败,尝试使用备用秘钥");
+            result = decrypt(encryptedText, AES_BACKUP_KEY);
+        }
+        return result;
+    }
+
+    // ==================== 工具方法 ====================
+
+    /**
+     * 验证秘钥长度是否有效
+     *
+     * @param key 秘钥
+     * @return 是否有效
+     */
+    public static boolean isValidKeyLength(String key) {
+        if (key == null) {
+            return false;
+        }
+        int len = key.getBytes(StandardCharsets.UTF_8).length;
+        return len == 16 || len == 24 || len == 32;
+    }
+
+    /**
+     * 获取当前使用的秘钥长度
+     *
+     * @return 秘钥长度
+     */
+    public static int getKeyLength() {
+        return AES_SECRET_KEY.getBytes(StandardCharsets.UTF_8).length;
+    }
+}

+ 163 - 0
fs-service/src/main/java/com/fs/company/util/PhoneNumberUtil.java

@@ -0,0 +1,163 @@
+package com.fs.company.util;
+
+import com.fs.common.utils.StringUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.regex.Pattern;
+
+/**
+ * 手机号工具类
+ * 提供手机号格式校验、脱敏等功能
+ *
+ * @author MixLiu
+ * @date 2026/3/17 18:57
+ */
+public class PhoneNumberUtil {
+
+    private static final Logger log = LoggerFactory.getLogger(PhoneNumberUtil.class);
+
+    // ==================== 常量定义 ====================
+
+    /**
+     * 中国大陆手机号正则表达式
+     * 规则说明:
+     * 1. 以1开头
+     * 2. 第二位为3-9之间的数字(目前运营商号段:13x、14x、15x、16x、17x、18x、19x)
+     * 3. 后面跟着9位数字
+     * 4. 总长度为11位
+     */
+    private static final String CHINA_MOBILE_REGEX = "^1[3-9]\\d{9}$";
+
+    /**
+     * 手机号正则编译对象(预编译提高性能)
+     */
+    private static final Pattern MOBILE_PATTERN = Pattern.compile(CHINA_MOBILE_REGEX);
+
+    /**
+     * 手机号长度
+     */
+    private static final int MOBILE_LENGTH = 11;
+
+    // ==================== 校验方法 ====================
+
+    /**
+     * 校验手机号是否合法
+     * <p>
+     * 校验规则:
+     * 1. 手机号不能为空
+     * 2. 手机号长度必须为11位
+     * 3. 手机号必须符合中国大陆手机号格式(以1开头,第二位为3-9,后跟9位数字)
+     * </p>
+     *
+     * @param mobile 待校验的手机号
+     * @return true-手机号合法;false-手机号不合法
+     */
+    public static boolean isValid(String mobile) {
+        // 空值校验
+        if (StringUtils.isBlank(mobile)) {
+            log.warn("手机号校验失败:手机号为空");
+            return false;
+        }
+        // 去除首尾空格
+        String trimmedMobile = mobile.trim();
+        // 长度校验(手机号必须为11位)
+        if (trimmedMobile.length() != MOBILE_LENGTH) {
+            log.warn("手机号校验失败:手机号长度不正确,当前长度={}", trimmedMobile.length());
+            return false;
+        }
+        // 格式校验(使用正则表达式)
+        boolean isValid = MOBILE_PATTERN.matcher(trimmedMobile).matches();
+        if (!isValid) {
+            log.warn("手机号校验失败:手机号格式不正确,mobile={}", trimmedMobile);
+        }
+        return isValid;
+    }
+
+    /**
+     * 校验手机号是否合法(带详细返回信息)
+     * <p>
+     * 校验规则同 {@link #isValid(String)},但返回详细的校验结果信息
+     * </p>
+     *
+     * @param mobile 待校验的手机号
+     * @return 校验结果对象,包含是否合法及错误信息
+     */
+    public static ValidateResult validate(String mobile) {
+        // 空值校验
+        if (StringUtils.isBlank(mobile)) {
+            return new ValidateResult(false, "手机号不能为空");
+        }
+        // 去除首尾空格
+        String trimmedMobile = mobile.trim();
+        // 长度校验
+        if (trimmedMobile.length() != MOBILE_LENGTH) {
+            return new ValidateResult(false, "手机号长度必须为11位");
+        }
+        // 格式校验
+        if (!MOBILE_PATTERN.matcher(trimmedMobile).matches()) {
+            return new ValidateResult(false, "手机号格式不正确,请输入有效的中国大陆手机号");
+        }
+        return new ValidateResult(true, "手机号校验通过");
+    }
+
+    // ==================== 脱敏方法 ====================
+
+    /**
+     * 手机号脱敏处理
+     * <p>
+     * 将手机号中间4位替换为*号,例如:13812345678 -> 138****5678
+     * </p>
+     *
+     * @param mobile 原始手机号
+     * @return 脱敏后的手机号,如果手机号为空或格式不正确则返回原始值
+     */
+    public static String mask(String mobile) {
+        if (StringUtils.isBlank(mobile)) {
+            return mobile;
+        }
+        String trimmedMobile = mobile.trim();
+        if (trimmedMobile.length() != MOBILE_LENGTH) {
+            return mobile;
+        }
+        return trimmedMobile.substring(0, 3) + "****" + trimmedMobile.substring(7);
+    }
+
+    // ==================== 内部类 ====================
+
+    /**
+     * 校验结果类
+     * 用于返回详细的校验结果信息
+     */
+    public static class ValidateResult {
+        /**
+         * 是否校验通过
+         */
+        private boolean valid;
+        /**
+         * 校验结果消息
+         */
+        private String message;
+
+        public ValidateResult(boolean valid, String message) {
+            this.valid = valid;
+            this.message = message;
+        }
+
+        public boolean isValid() {
+            return valid;
+        }
+
+        public String getMessage() {
+            return message;
+        }
+
+        @Override
+        public String toString() {
+            return "ValidateResult{" +
+                    "valid=" + valid +
+                    ", message='" + message + '\'' +
+                    '}';
+        }
+    }
+}

+ 27 - 0
fs-service/src/main/java/com/fs/wxcid/threadExecutor/generalCustomerExecutor.java

@@ -0,0 +1,27 @@
+package com.fs.wxcid.threadExecutor;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.scheduling.annotation.EnableAsync;
+import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ThreadPoolExecutor;
+
+@Configuration
+@EnableAsync
+public class generalCustomerExecutor {
+
+    @Bean("crmCustomerExecutor")
+    public Executor crmCustomerExecutor() {
+        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
+        executor.setCorePoolSize(16);
+        executor.setMaxPoolSize(32);
+        executor.setQueueCapacity(10000);
+        executor.setThreadNamePrefix("CustomerExec-");
+        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
+        executor.setKeepAliveSeconds(600);
+        executor.initialize();
+        return executor;
+    }
+}

+ 7 - 0
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticMapper.xml

@@ -214,4 +214,11 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     <select id="getDictDataList" resultType="com.fs.company.vo.DictVO">
         SELECT dict_type,dict_label,dict_value FROM `sys_dict_data` where  dict_type = #{dictType}
     </select>
+
+    <select id="selectSceneTaskByCompanyIdAndType" resultType="CompanyVoiceRobotic">
+        select * from company_voice_robotic where company_id = #{companyId}
+                                              and scene_type = #{sceneType}
+                                              and task_status = 1
+                                              order by create_time desc
+    </select>
 </mapper>

+ 8 - 0
fs-service/src/main/resources/mapper/company/CompanyVoiceRoboticWxMapper.xml

@@ -124,5 +124,13 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         where a.robotic_id = #{id}
         group by a.account_id
     </select>
+
+    <select id="selectAllocationTargetByTaskId" resultType="com.fs.company.domain.CompanyVoiceRoboticWx">
+        select a.*
+        from company_voice_robotic_wx a
+        where a.robotic_id = #{roboticId}
+        order by a.num asc
+        limit 1
+    </select>
     
 </mapper>

Някои файлове не бяха показани, защото твърде много файлове са промени