Przeglądaj źródła

迁移scrm的ai客户分析及自动任务
ai销冠系统外部接口管理

lk 2 tygodni temu
rodzic
commit
21f7c435da
23 zmienionych plików z 796 dodań i 13 usunięć
  1. 145 0
      fs-admin/src/main/java/com/fs/company/controller/ExternalApiConfigController.java
  2. 161 0
      fs-admin/src/main/java/com/fs/task/CrmCustomerAiProcessingTask.java
  3. 5 1
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  4. 1 0
      fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java
  5. 43 0
      fs-service/src/main/java/com/fs/companyWorkflow/domain/ExternalApiCallLog.java
  6. 85 0
      fs-service/src/main/java/com/fs/companyWorkflow/domain/ExternalApiConfig.java
  7. 8 0
      fs-service/src/main/java/com/fs/companyWorkflow/mapper/ExternalApiCallLogMapper.java
  8. 8 0
      fs-service/src/main/java/com/fs/companyWorkflow/mapper/ExternalApiConfigMapper.java
  9. 8 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/IExternalApiCallLogService.java
  10. 11 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/IExternalApiConfigService.java
  11. 12 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiCallLogPageReq.java
  12. 15 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiConfigPageReq.java
  13. 24 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiConfigSaveReq.java
  14. 11 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiTestReq.java
  15. 25 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiTestRequest.java
  16. 16 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiTestResult.java
  17. 12 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/impl/ExternalApiCallLogServiceImpl.java
  18. 135 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/impl/ExternalApiConfigServiceImpl.java
  19. 25 2
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java
  20. 4 8
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerAnalyzeServiceImpl.java
  21. 8 1
      fs-service/src/main/java/com/fs/crm/utils/CrmCustomerAiTagUtil.java
  22. 18 0
      fs-service/src/main/java/com/fs/crm/vo/CrmCustomerListQueryVO.java
  23. 16 1
      fs-service/src/main/java/com/fs/crm/vo/CrmLineCustomerListQueryVO.java

+ 145 - 0
fs-admin/src/main/java/com/fs/company/controller/ExternalApiConfigController.java

@@ -0,0 +1,145 @@
+package com.fs.company.controller;
+
+import cn.hutool.core.util.StrUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.metadata.IPage;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.model.LoginUser;
+import com.fs.common.result.Result;
+import com.fs.companyWorkflow.service.dto.ExternalApiCallLogPageReq;
+import com.fs.companyWorkflow.service.dto.ExternalApiConfigPageReq;
+import com.fs.companyWorkflow.service.dto.ExternalApiConfigSaveReq;
+import com.fs.companyWorkflow.service.dto.ExternalApiTestReq;
+import com.fs.companyWorkflow.domain.ExternalApiCallLog;
+import com.fs.companyWorkflow.domain.ExternalApiConfig;
+import com.fs.companyWorkflow.service.IExternalApiCallLogService;
+import com.fs.companyWorkflow.service.IExternalApiConfigService;
+import com.fs.companyWorkflow.service.dto.ExternalApiTestRequest;
+import com.fs.companyWorkflow.service.dto.ExternalApiTestResult;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Date;
+
+/**
+ * 外部接口管理配置
+ */
+@RestController
+@RequestMapping("/companyWorkflow/externalApi")
+public class ExternalApiConfigController extends BaseController {
+
+    private final IExternalApiConfigService configService;
+    private final IExternalApiCallLogService logService;
+
+    public ExternalApiConfigController(IExternalApiConfigService configService, IExternalApiCallLogService logService) {
+        this.configService = configService;
+        this.logService = logService;
+    }
+
+    /**
+     * 列表分页
+     */
+    @GetMapping("/page")
+    public Result<IPage<ExternalApiConfig>> page(ExternalApiConfigPageReq req) {
+        Page<ExternalApiConfig> page = new Page<>(req.getPageNum(), req.getPageSize());
+        LambdaQueryWrapper<ExternalApiConfig> qw = new LambdaQueryWrapper<>();
+        qw.eq(req.getStatus() != null, ExternalApiConfig::getStatus, req.getStatus());
+        qw.like(StrUtil.isNotBlank(req.getApiType()), ExternalApiConfig::getApiType, req.getApiType());
+        qw.like(StrUtil.isNotBlank(req.getApiName()), ExternalApiConfig::getApiName, req.getApiName());
+        qw.like(StrUtil.isNotBlank(req.getApiCode()), ExternalApiConfig::getApiCode, req.getApiCode());
+        qw.orderByDesc(ExternalApiConfig::getPriority).orderByDesc(ExternalApiConfig::getCreateTime);
+        return Result.success(configService.page(page, qw));
+    }
+
+    /**
+     * 新增/编辑
+     */
+    @PostMapping("/saveOrUpdate")
+    public Result<Void> saveOrUpdate(@RequestBody ExternalApiConfigSaveReq req) {
+        LoginUser loginUser = getLoginUser();
+        ExternalApiConfig entity = new ExternalApiConfig();
+        entity.setId(req.getId());
+        entity.setApiType(req.getApiType());
+        entity.setApiName(req.getApiName());
+        entity.setApiCode(req.getApiCode());
+        entity.setApiUrl(req.getApiUrl());
+        entity.setHttpMethod(req.getHttpMethod());
+        entity.setRateWindowSeconds(req.getRateWindowSeconds());
+        entity.setRateMaxCount(req.getRateMaxCount());
+        entity.setPriority(req.getPriority());
+        entity.setStatus(req.getStatus());
+        entity.setDefaultHeadersJson(req.getDefaultHeadersJson());
+        entity.setDefaultBodyJson(req.getDefaultBodyJson());
+        entity.setTimeoutMs(req.getTimeoutMs());
+        boolean ok;
+        if (req.getId() != null){
+            entity.setUpdateTime(new Date());
+            entity.setUpdateBy(loginUser.getUserId().toString());
+            ok = configService.updateById(entity);
+        }else {
+            entity.setCreateTime(new Date());
+            entity.setCreateBy(loginUser.getUserId().toString());
+            ok = configService.save(entity);
+        }
+        return ok ? Result.success() : Result.error("保存失败");
+    }
+
+    @GetMapping("/{id}")
+    public Result<ExternalApiConfig> detail(@PathVariable Long id) {
+        return Result.success(configService.getById(id));
+    }
+
+    /**
+     * 启停
+     */
+    @PostMapping("/{id}/status")
+    public Result<Void> changeStatus(@PathVariable Long id, @RequestParam Integer status) {
+        ExternalApiConfig entity = new ExternalApiConfig();
+        entity.setId(id);
+        entity.setStatus(status);
+        boolean ok = configService.updateById(entity);
+        return ok ? Result.success() : Result.error("更新状态失败");
+    }
+
+    /**
+     * 删除(逻辑删)
+     */
+    @DeleteMapping("/{id}")
+    public Result<Void> delete(@PathVariable Long id) {
+        ExternalApiConfig entity = new ExternalApiConfig();
+        entity.setId(id);
+        entity.setIsDel(Integer.valueOf(1));
+        boolean ok = configService.updateById(entity);
+        return ok ? Result.success() : Result.error("删除失败");
+    }
+
+    /**
+     * 测试接口
+     */
+    @PostMapping("/{id}/test")
+    public Result<ExternalApiTestResult> test(@PathVariable Long id, @RequestBody(required = false) ExternalApiTestReq req) {
+        ExternalApiTestRequest r = new ExternalApiTestRequest();
+        if (req != null) {
+            r.setHeadersJson(req.getHeadersJson());
+            r.setBodyJson(req.getBodyJson());
+            r.setTimeoutMs(req.getTimeoutMs());
+        }
+        return Result.success(configService.testCall(id, r));
+    }
+
+    /**
+     * 调用日志分页
+     */
+    @GetMapping("/logs/page")
+    public Result<IPage<ExternalApiCallLog>> logs(ExternalApiCallLogPageReq req) {
+        if (req.getConfigId() == null) {
+            return Result.error("configId不能为空");
+        }
+        Page<ExternalApiCallLog> page = new Page<>(req.getPageNum(), req.getPageSize());
+        LambdaQueryWrapper<ExternalApiCallLog> qw = new LambdaQueryWrapper<>();
+        qw.eq(ExternalApiCallLog::getConfigId, req.getConfigId());
+        qw.orderByDesc(ExternalApiCallLog::getCreateTime);
+        return Result.success(logService.page(page, qw));
+    }
+}
+

+ 161 - 0
fs-admin/src/main/java/com/fs/task/CrmCustomerAiProcessingTask.java

@@ -0,0 +1,161 @@
+package com.fs.task;
+
+import com.fs.crm.domain.CrmCustomerAnalyze;
+import com.fs.crm.service.ICrmCustomerAnalyzeService;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.data.redis.core.RedisTemplate;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Component("CrmCustomerAiProcessingTask")
+@RequiredArgsConstructor
+@Slf4j
+public class CrmCustomerAiProcessingTask {
+
+    private final RedisTemplate redisTemplate;
+
+    private static final String CRM_AI_REDIS_KEY = "crm:AI:data:processing";
+
+    private final ICrmCustomerAnalyzeService crmCustomerAnalyzeService;
+
+    // 自定义线程池
+    private final ExecutorService executorService = new ThreadPoolExecutor(
+            5,  // 核心线程数
+            10, // 最大线程数
+            60, TimeUnit.SECONDS,
+            new LinkedBlockingQueue<>(200),
+            r -> {
+                Thread thread = new Thread(r);
+                thread.setName("crm-ai-processor-" + thread.getId());
+                return thread;
+            },
+            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用线程处理
+    );
+
+    @SuppressWarnings("unchecked")
+    public void process() {
+//        一次只处理5条,AI响应慢避免阻塞
+        List<Map<String, Object>> range =
+                (List<Map<String, Object>>) redisTemplate.opsForList().range(CRM_AI_REDIS_KEY, 0, 4);
+        if (range == null || range.isEmpty()) {
+            log.info("CrmCustomerAiProcessingTask没有待处理的数据");
+            return;
+        }
+        final int total = range.size();
+        log.info("CrmCustomerAiProcessingTask开始处理数据, 条数: {}", total);
+
+
+        AtomicInteger successCount = new AtomicInteger(0);
+        AtomicInteger failCount = new AtomicInteger(0);
+        long startTime = System.currentTimeMillis();
+        CompletableFuture<Void> futures = CompletableFuture.runAsync(
+                () -> processBatch(range, successCount, failCount), executorService);
+        try {
+            CompletableFuture.allOf(futures).join();
+        } catch (CompletionException e) {
+            Throwable cause = e.getCause() != null ? e.getCause() : e;
+            log.error("多线程处理异常", cause);
+            return;
+        }
+
+        long costTime = System.currentTimeMillis() - startTime;
+        log.info("CrmCustomerAiProcessingTask处理完成, 总条数: {}, 成功: {}, 失败: {}, 耗时: {}ms",
+                total, successCount.get(), failCount.get(), costTime);
+
+        // 当前 processBatch 内任一条失败会抛异常导致 join 失败;能走到此处说明整批成功
+        if (failCount.get() == 0 && successCount.get() == total) {
+            redisTemplate.delete(CRM_AI_REDIS_KEY);
+            log.info("全部处理成功,已删除Redis数据");
+            return;
+        }
+        log.warn("计数与预期不一致: 总条数={}, 成功={}, 失败={}, 未删除 Redis 队列", total,
+                successCount.get(), failCount.get());
+    }
+    /**
+     * 处理单个批次
+     */
+    private void processBatch(List<Map<String, Object>> batch,
+                              AtomicInteger successCount,
+                              AtomicInteger failCount) {
+        String threadName = Thread.currentThread().getName();
+        long batchStartTime = System.currentTimeMillis();
+
+        try {
+            log.info("线程 {} 开始处理批次, 数据量: {}", threadName, batch.size());
+
+                for (Map<String, Object> data : batch) {
+                processSingleCustomer(data, successCount, failCount);
+            }
+
+            long costTime = System.currentTimeMillis() - batchStartTime;
+            log.info("线程 {} 批次处理完成, 数据量: {}, 耗时: {}ms",
+                    threadName, batch.size(), costTime);
+        } catch (Exception e) {
+            failCount.addAndGet(batch.size());
+            log.error("线程 {} 批次处理失败, 数据量: {}", threadName, batch.size(), e);
+            throw new RuntimeException("批次处理失败", e);
+        }
+    }
+    /**
+     * 处理单个客户的AI分析(6个接口并行)
+     */
+    private void processSingleCustomer(Map<String, Object> data,
+                                       AtomicInteger successCount,
+                                       AtomicInteger failCount)  {
+        try {
+            Long customerId = (Long)data.get("customerId");
+            String dataJson = (String)data.get("data");
+            Long logId = (Long)data.get("logId");
+
+            long startTime = System.currentTimeMillis();
+
+            // 6 个 AI 接口并行;使用 commonPool,避免与批次线程池 executorService 嵌套导致死锁
+            Executor asyncPool = ForkJoinPool.commonPool();
+            // 使用 supplyAsync 获取返回值,定义具体返回类型
+            CompletableFuture<String> portraitFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiGeneratedCustomerPortrait(customerId, dataJson, logId), asyncPool);
+
+            CompletableFuture<String> summaryFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiCommunicationSummary(customerId, dataJson, logId), asyncPool);
+
+            CompletableFuture<String> abstractFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiCommunicationAbstract(customerId, dataJson, logId), asyncPool);
+
+            CompletableFuture<Long> attritionFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiAttritionLevel(customerId, dataJson, logId), asyncPool);
+
+            CompletableFuture<String> focusFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiCustomerFocus(customerId, dataJson, logId), asyncPool);
+
+            CompletableFuture<String> intentionFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiIntentionDegree(customerId, dataJson, logId), asyncPool);
+
+// 等待所有异步任务完成
+            CompletableFuture.allOf(portraitFuture, summaryFuture, abstractFuture,
+                    attritionFuture, focusFuture, intentionFuture).join();
+        //      allAiFutures.get(60, TimeUnit.SECONDS);
+            CrmCustomerAnalyze crmCustomerAnalyze = new CrmCustomerAnalyze();
+            crmCustomerAnalyze.setCustomerId(customerId);
+            crmCustomerAnalyze.setCustomerPortraitJson(portraitFuture.get());
+            crmCustomerAnalyze.setCommunicationSummary(summaryFuture.get());
+            crmCustomerAnalyze.setCommunicationAbstract(abstractFuture.get());
+            crmCustomerAnalyze.setAttritionLevel(attritionFuture.get());
+            crmCustomerAnalyze.setCustomerFocusJson(focusFuture.get());
+            crmCustomerAnalyze.setIntentionDegree(intentionFuture.get());
+            Integer i = crmCustomerAnalyzeService.updateCrmCustomerAnalyzeByCustomerId(crmCustomerAnalyze);
+            long costTime = System.currentTimeMillis() - startTime;
+            successCount.incrementAndGet();
+            log.info("客户 {} 的AI分析完成, 耗时: {}ms,更新{}条", customerId, costTime,i);
+
+        } catch (Exception e) {
+            failCount.incrementAndGet();
+            log.error("处理客户数据失败, customerId: {}, logId: {}",
+                    data.get("customerId"), data.get("logId"), e);
+        }
+    }
+}

+ 5 - 1
fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java

@@ -15,6 +15,7 @@ import com.fs.company.service.ICompanyUserService;
 import com.fs.company.util.OrderUtils;
 import com.fs.crm.domain.CrmCustomer;
 import com.fs.crm.param.*;
+import com.fs.crm.service.ICrmCustomerPropertyService;
 import com.fs.crm.service.ICrmCustomerService;
 import com.fs.crm.service.ICrmCustomerUserService;
 import com.fs.crm.vo.*;
@@ -51,6 +52,8 @@ public class CrmCustomerController extends BaseController
     private TokenService tokenService;
     @Autowired
     ICrmCustomerUserService crmCustomerUserService;
+    @Autowired
+    private ICrmCustomerPropertyService crmCustomerPropertyService;
 
     @ApiOperation("获取线索客户")
     @PreAuthorize("@ss.hasPermi('crm:customer:lineList')")
@@ -177,7 +180,7 @@ public class CrmCustomerController extends BaseController
                     if(vo.getMobile()!=null){
                         vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
                     }
-
+                    vo.setProperties( crmCustomerPropertyService.selectCrmCustomerPropertyByCustomerId(vo.getCustomerId()));
                 }
             }
             return getDataTable(list1);
@@ -188,6 +191,7 @@ public class CrmCustomerController extends BaseController
                     if (vo.getMobile() != null) {
                         vo.setMobile(vo.getMobile().replaceAll("(\\d{3})\\d*(\\d{4})", "$1****$2"));
                     }
+                    vo.setProperties( crmCustomerPropertyService.selectCrmCustomerPropertyByCustomerId(vo.getCustomerId()));
 
                 }
             }

+ 1 - 0
fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java

@@ -135,6 +135,7 @@ public class SecurityConfig extends WebSecurityConfigurerAdapter
                 .antMatchers("/system/config/getConfigByKey/his.adminUi.config").permitAll()
                 .antMatchers("/live/LiveMixLiuTestOpen/**").anonymous()
                 .antMatchers("/company/companyVoiceRobotic/callerResult4EasyCall").anonymous()
+                .antMatchers("/companyWorkflow/externalApi/page").permitAll()
                 // 除上面外的所有请求全部需要鉴权认证
                 .anyRequest().authenticated()
                 .and()

+ 43 - 0
fs-service/src/main/java/com/fs/companyWorkflow/domain/ExternalApiCallLog.java

@@ -0,0 +1,43 @@
+package com.fs.companyWorkflow.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+/**
+ * 外部接口调用日志
+ */
+@Data
+@TableName("company_external_api_call_log")
+public class ExternalApiCallLog extends BaseEntity {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long configId;
+
+    private String apiCode;
+
+    private String httpMethod;
+
+    private String requestUrl;
+
+    private String requestHeadersJson;
+
+    private String requestBody;
+
+    private Integer responseStatus;
+
+    private String responseBody;
+
+    /**
+     * 1-成功 0-失败
+     */
+    private Integer success;
+
+    private String errorMessage;
+
+    private Long durationMs;
+}
+

+ 85 - 0
fs-service/src/main/java/com/fs/companyWorkflow/domain/ExternalApiConfig.java

@@ -0,0 +1,85 @@
+package com.fs.companyWorkflow.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableLogic;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+
+/**
+ * 外部接口配置
+ */
+@Data
+@TableName("company_external_api_config")
+public class ExternalApiConfig extends BaseEntity {
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /**
+     * 接口类型(例如:WEATHER/LOCATION/FESTIVAL...)
+     */
+    private String apiType;
+
+    /**
+     * 接口名称
+     */
+    private String apiName;
+
+    /**
+     * 接口编码(唯一)
+     */
+    private String apiCode;
+
+    /**
+     * 请求地址
+     */
+    private String apiUrl;
+
+    /**
+     * 请求方式(GET/POST/PUT/DELETE...)
+     */
+    private String httpMethod;
+
+    /**
+     * 限频:窗口秒数(例如 60 表示“每分钟”)
+     */
+    private Integer rateWindowSeconds;
+
+    /**
+     * 限频:窗口内最大次数
+     */
+    private Integer rateMaxCount;
+
+    /**
+     * 优先级(数字越大优先级越高)
+     */
+    private Integer priority;
+
+    /**
+     * 状态:1-启用,0-停用
+     */
+    private Integer status;
+
+    /**
+     * 默认请求头(JSON字符串)
+     */
+    private String defaultHeadersJson;
+
+    /**
+     * 默认请求参数(JSON字符串,GET 可作为 query 参数模板;POST 可作为 body 模板)
+     */
+    private String defaultBodyJson;
+
+    /**
+     * 超时时间(ms)
+     */
+    private Integer timeoutMs;
+
+    /**
+     * 逻辑删除:0-未删除,1-已删除
+     */
+    @TableLogic(value = "0", delval = "1")
+    private Integer isDel;
+}
+

+ 8 - 0
fs-service/src/main/java/com/fs/companyWorkflow/mapper/ExternalApiCallLogMapper.java

@@ -0,0 +1,8 @@
+package com.fs.companyWorkflow.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.companyWorkflow.domain.ExternalApiCallLog;
+
+public interface ExternalApiCallLogMapper extends BaseMapper<ExternalApiCallLog> {
+}
+

+ 8 - 0
fs-service/src/main/java/com/fs/companyWorkflow/mapper/ExternalApiConfigMapper.java

@@ -0,0 +1,8 @@
+package com.fs.companyWorkflow.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.companyWorkflow.domain.ExternalApiConfig;
+
+public interface ExternalApiConfigMapper extends BaseMapper<ExternalApiConfig> {
+}
+

+ 8 - 0
fs-service/src/main/java/com/fs/companyWorkflow/service/IExternalApiCallLogService.java

@@ -0,0 +1,8 @@
+package com.fs.companyWorkflow.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.companyWorkflow.domain.ExternalApiCallLog;
+
+public interface IExternalApiCallLogService extends IService<ExternalApiCallLog> {
+}
+

+ 11 - 0
fs-service/src/main/java/com/fs/companyWorkflow/service/IExternalApiConfigService.java

@@ -0,0 +1,11 @@
+package com.fs.companyWorkflow.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.companyWorkflow.domain.ExternalApiConfig;
+import com.fs.companyWorkflow.service.dto.ExternalApiTestRequest;
+import com.fs.companyWorkflow.service.dto.ExternalApiTestResult;
+
+public interface IExternalApiConfigService extends IService<ExternalApiConfig> {
+    ExternalApiTestResult testCall(Long configId, ExternalApiTestRequest request);
+}
+

+ 12 - 0
fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiCallLogPageReq.java

@@ -0,0 +1,12 @@
+package com.fs.companyWorkflow.service.dto;
+
+import lombok.Data;
+
+@Data
+public class ExternalApiCallLogPageReq {
+    private Long pageNum = 1L;
+    private Long pageSize = 10L;
+
+    private Long configId;
+}
+

+ 15 - 0
fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiConfigPageReq.java

@@ -0,0 +1,15 @@
+package com.fs.companyWorkflow.service.dto;
+
+import lombok.Data;
+
+@Data
+public class ExternalApiConfigPageReq {
+    private Long pageNum = 1L;
+    private Long pageSize = 10L;
+
+    private String apiType;
+    private String apiName;
+    private String apiCode;
+    private Integer status;
+}
+

+ 24 - 0
fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiConfigSaveReq.java

@@ -0,0 +1,24 @@
+package com.fs.companyWorkflow.service.dto;
+
+import lombok.Data;
+
+@Data
+public class ExternalApiConfigSaveReq {
+    private Long id;
+
+    private String apiType;
+    private String apiName;
+    private String apiCode;
+    private String apiUrl;
+    private String httpMethod;
+
+    private Integer rateWindowSeconds;
+    private Integer rateMaxCount;
+    private Integer priority;
+    private Integer status;
+
+    private String defaultHeadersJson;
+    private String defaultBodyJson;
+    private Integer timeoutMs;
+}
+

+ 11 - 0
fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiTestReq.java

@@ -0,0 +1,11 @@
+package com.fs.companyWorkflow.service.dto;
+
+import lombok.Data;
+
+@Data
+public class ExternalApiTestReq {
+    private String headersJson;
+    private String bodyJson;
+    private Integer timeoutMs;
+}
+

+ 25 - 0
fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiTestRequest.java

@@ -0,0 +1,25 @@
+package com.fs.companyWorkflow.service.dto;
+
+import lombok.Data;
+
+/**
+ * 测试调用入参(允许覆盖配置里的默认 header/body/timeout)
+ */
+@Data
+public class ExternalApiTestRequest {
+    /**
+     * 覆盖请求头(JSON字符串)
+     */
+    private String headersJson;
+
+    /**
+     * 覆盖请求体(JSON字符串)
+     */
+    private String bodyJson;
+
+    /**
+     * 覆盖超时(ms)
+     */
+    private Integer timeoutMs;
+}
+

+ 16 - 0
fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiTestResult.java

@@ -0,0 +1,16 @@
+package com.fs.companyWorkflow.service.dto;
+
+import lombok.Data;
+
+/**
+ * 测试调用结果
+ */
+@Data
+public class ExternalApiTestResult {
+    private Integer responseStatus;
+    private String responseBody;
+    private Long durationMs;
+    private Boolean success;
+    private String errorMessage;
+}
+

+ 12 - 0
fs-service/src/main/java/com/fs/companyWorkflow/service/impl/ExternalApiCallLogServiceImpl.java

@@ -0,0 +1,12 @@
+package com.fs.companyWorkflow.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.companyWorkflow.domain.ExternalApiCallLog;
+import com.fs.companyWorkflow.mapper.ExternalApiCallLogMapper;
+import com.fs.companyWorkflow.service.IExternalApiCallLogService;
+import org.springframework.stereotype.Service;
+
+@Service
+public class ExternalApiCallLogServiceImpl extends ServiceImpl<ExternalApiCallLogMapper, ExternalApiCallLog> implements IExternalApiCallLogService {
+}
+

+ 135 - 0
fs-service/src/main/java/com/fs/companyWorkflow/service/impl/ExternalApiConfigServiceImpl.java

@@ -0,0 +1,135 @@
+package com.fs.companyWorkflow.service.impl;
+
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.http.HttpRequest;
+import cn.hutool.http.HttpResponse;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.companyWorkflow.domain.ExternalApiCallLog;
+import com.fs.companyWorkflow.domain.ExternalApiConfig;
+import com.fs.companyWorkflow.mapper.ExternalApiCallLogMapper;
+import com.fs.companyWorkflow.mapper.ExternalApiConfigMapper;
+import com.fs.companyWorkflow.service.IExternalApiConfigService;
+import com.fs.companyWorkflow.service.dto.ExternalApiTestRequest;
+import com.fs.companyWorkflow.service.dto.ExternalApiTestResult;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.Map;
+
+@Service
+public class ExternalApiConfigServiceImpl extends ServiceImpl<ExternalApiConfigMapper, ExternalApiConfig> implements IExternalApiConfigService {
+
+    private final ExternalApiCallLogMapper callLogMapper;
+
+    public ExternalApiConfigServiceImpl(ExternalApiCallLogMapper callLogMapper) {
+        this.callLogMapper = callLogMapper;
+    }
+
+    @Override
+    public ExternalApiTestResult testCall(Long configId, ExternalApiTestRequest request) {
+        ExternalApiConfig config = getById(configId);
+        ExternalApiTestResult result = new ExternalApiTestResult();
+        if (config == null || (config.getIsDel() != null && config.getIsDel() == 1)) {
+            result.setSuccess(Boolean.valueOf(false));
+            result.setErrorMessage("接口配置不存在");
+            return result;
+        }
+
+        String headersJson = request != null && StrUtil.isNotBlank(request.getHeadersJson())
+                ? request.getHeadersJson()
+                : config.getDefaultHeadersJson();
+        String bodyJson = request != null && StrUtil.isNotBlank(request.getBodyJson())
+                ? request.getBodyJson()
+                : config.getDefaultBodyJson();
+        int timeoutMs = request != null && request.getTimeoutMs() != null
+                ? request.getTimeoutMs()
+                : (config.getTimeoutMs() != null ? config.getTimeoutMs() : 10000);
+
+        Map<String, String> headers = parseHeaders(headersJson);
+        long start = System.currentTimeMillis();
+        Integer statusCode = null;
+        String respBody = null;
+        boolean success = false;
+        String errorMessage = null;
+
+        String method = StrUtil.blankToDefault(config.getHttpMethod(), "GET").toUpperCase();
+        String url = config.getApiUrl();
+
+        try {
+            HttpRequest httpRequest;
+            switch (method) {
+                case "POST":
+                    httpRequest = HttpRequest.post(url);
+                    break;
+                case "PUT":
+                    httpRequest = HttpRequest.put(url);
+                    break;
+                case "DELETE":
+                    httpRequest = HttpRequest.delete(url);
+                    break;
+                case "GET":
+                default:
+                    httpRequest = HttpRequest.get(url);
+                    break;
+            }
+
+            if (headers != null && !headers.isEmpty()) {
+                httpRequest.addHeaders(headers);
+            }
+            httpRequest.timeout(timeoutMs);
+
+            if (!"GET".equals(method) && StrUtil.isNotBlank(bodyJson)) {
+                httpRequest.body(bodyJson);
+            }
+
+            try (HttpResponse response = httpRequest.execute()) {
+                statusCode = (Integer) response.getStatus();
+                respBody = response.body();
+            }
+            success = statusCode != null && statusCode >= 200 && statusCode < 300;
+        } catch (Exception e) {
+            errorMessage = e.getMessage();
+            success = false;
+        }
+
+        Long duration = (Long) (System.currentTimeMillis() - start);
+        result.setResponseStatus(statusCode);
+        result.setResponseBody(respBody);
+        result.setDurationMs(duration);
+        result.setSuccess(Boolean.valueOf(success));
+        result.setErrorMessage(errorMessage);
+
+        ExternalApiCallLog log = new ExternalApiCallLog();
+        log.setConfigId(config.getId());
+        log.setApiCode(config.getApiCode());
+        log.setHttpMethod(method);
+        log.setRequestUrl(url);
+        log.setRequestHeadersJson(headersJson);
+        log.setRequestBody(bodyJson);
+        log.setResponseStatus(statusCode);
+        log.setResponseBody(respBody);
+        log.setSuccess(Integer.valueOf(success ? 1 : 0));
+        log.setErrorMessage(errorMessage);
+        log.setDurationMs(duration);
+        log.setCreateTime(new Date(start));
+        callLogMapper.insert(log);
+
+        return result;
+    }
+
+    private Map<String, String> parseHeaders(String headersJson) {
+        if (StrUtil.isBlank(headersJson)) {
+            return Collections.emptyMap();
+        }
+        try {
+            JSONObject obj = JSON.parseObject(headersJson);
+            return obj.toJavaObject(Map.class);
+        } catch (Exception e) {
+            return Collections.emptyMap();
+        }
+    }
+}
+

+ 25 - 2
fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java

@@ -345,9 +345,21 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
             "</script>"})
     List<CrmMyCustomerListQueryVO> selectCrmMyCustomerListQuery(@Param("maps") CrmMyCustomerListQueryParam param);
     @Select({"<script> " +
-            "select c.*,u.nick_name as company_user_nick_name,ccu.start_time as startTime  from  crm_customer c left join company_user u on u.user_id=c.receive_user_id left join crm_customer_user ccu on c.customer_user_id = ccu.customer_user_id " +
+            "select c.*,u.nick_name as company_user_nick_name,ccu.start_time as startTime,ca.attrition_level,ca.intention_degree,ca.customer_focus_json  from  crm_customer c " +
+            "left join company_user u on u.user_id=c.receive_user_id " +
+            "left join crm_customer_user ccu on c.customer_user_id = ccu.customer_user_id " +
+            "LEFT JOIN LATERAL ( SELECT attrition_level, intention_degree,customer_focus_json FROM crm_customer_analyze WHERE customer_id = c.customer_id " +
+            "   ORDER BY create_time DESC " +
+            "   LIMIT 1 " +
+            ") ca ON TRUE " +
 //            "where is_line=0 " +
             "where 1=1 " +
+            "<if test = 'maps.attritionLevel != null'> " +
+            "and ca.attrition_level = #{maps.attritionLevel} " +
+            "</if>" +
+            "<if test = 'maps.intentionDegree != null and maps.intentionDegree != \"\" ' > " +
+            "and ca.intention_degree = #{maps.intentionDegree} " +
+            "</if>" +
             "<if test = 'maps.companyId != null     '> " +
             "and c.company_id =#{maps.companyId} " +
             "</if>" +
@@ -427,7 +439,12 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
             "</script>"})
     List<CrmCustomerListQueryVO> selectCrmCustomerListQuery(@Param("maps") CrmCustomerListQueryParam param);
     @Select({"<script> " +
-            "select c.*,u.nick_name as company_user_nick_name  from  crm_customer c left join company_user u on u.user_id=c.receive_user_id   " +
+            "select c.*,u.nick_name as company_user_nick_name,ca.attrition_level,ca.intention_degree,ca.customer_focus_json  from  crm_customer c " +
+            "left join company_user u on u.user_id=c.receive_user_id " +
+            "LEFT JOIN LATERAL ( SELECT attrition_level, intention_degree,customer_focus_json FROM crm_customer_analyze WHERE customer_id = c.customer_id " +
+            "   ORDER BY create_time DESC " +
+            "   LIMIT 1 " +
+            ") ca ON TRUE " +
             "where is_line=1 " +
 //            " select  c.*  " +
 ////            "<if test = 'maps.isDuplicate != null '> " +
@@ -480,6 +497,12 @@ public interface CrmCustomerMapper extends BaseMapper<CrmCustomer> {
             "<if test = 'maps.endTime != null and maps.endTime != \"\" '> " +
             "and date_format(c.create_time,'%y%m%d') &lt;= date_format(#{maps.endTime},'%y%m%d') " +
             "</if>" +
+            "<if test = 'maps.attritionLevel != null'> " +
+            "and ca.attrition_level = #{maps.attritionLevel} " +
+            "</if>" +
+            "<if test = 'maps.intentionDegree != null and maps.intentionDegree != \"\" ' > " +
+            "and ca.intention_degree = #{maps.intentionDegree} " +
+            "</if>" +
             " order by c.customer_id desc "+
             "</script>"})
     List<CrmLineCustomerListQueryVO> selectCrmLineCustomerListQuery(@Param("maps") CrmLineCustomerListQueryParam param);

+ 4 - 8
fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerAnalyzeServiceImpl.java

@@ -388,7 +388,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         requestParam.put("likeRatio", "");
         long startTime = System.currentTimeMillis();
         R aiResponse = CrmCustomerAiTagUtil.callAiService(requestParam, Long.valueOf(param.getChatId()),OTHER_KEY);
-        System.out.println(aiResponse);
+//        System.out.println(aiResponse);
         String result = "";
         CrmCustomerChatMessage crmCustomerChatMessage = new CrmCustomerChatMessage();
         crmCustomerChatMessage.setContentType(1);
@@ -531,12 +531,12 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         requestParam.put("isRepository", "");
         requestParam.put("userContent", "");
         requestParam.put("aiContent", "");
-        requestParam.put("likeRatio", "");
+        requestParam.put("userIntent", "");
         requestParam.put("modelType","客户意向度");
 //        log.info("请求参数:{}", requestParam);
 
         R aiResponse = CrmCustomerAiTagUtil.callAiService(requestParam, chatId,OTHER_KEY);
-        JSONObject root = JSON.parseObject(JSONUtil.toJsonStr(aiResponse));
+//        JSONObject root = JSON.parseObject(JSONUtil.toJsonStr(aiResponse));
 //        System.out.println(aiResponse);
 // 获取 data.responseData
         String result = "";
@@ -587,8 +587,6 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         Map<String, Object> requestParam = new HashMap<>();
 
         // 获取各类数据
-        HashMap<String, Object> history = new HashMap<>();
-        history.put("history", communication);
         Map<String, Object> userInfo = getUserInfo(customerId);
         String likeRatio ="";
         if (!userInfo.isEmpty()){
@@ -603,7 +601,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         requestParam.put("isRepository", "");
         requestParam.put("userContent", "");
         requestParam.put("aiContent", "");
-        requestParam.put("likeRatio", likeRatio);
+        requestParam.put("userIntent", likeRatio);
 
         return requestParam;
     }
@@ -613,10 +611,8 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
             throw new RuntimeException("客户信息不存在");
         }
         HashMap<String, String> userInfo = new HashMap<String, String>();
-//        userInfo.put("name", crmCustomerAnalyze.getCustomerName()==null?"" : crmCustomerAnalyze.getCustomerName());
         List<SysDictData> portraits = sysDictDataMapper.selectDictDataByType(AI_PORTRAIT);
         List<String> dictValue = portraits.stream().map(SysDictData::getDictValue).collect(Collectors.toList());
-//        Map<String, String> portraitMap = portraits.stream().collect(Collectors.toMap(SysDictData::getDictValue, SysDictData::getDictLabel));
 
         if (crmCustomerAnalyze.getCustomerPortraitJson()!= null && !crmCustomerAnalyze.getCustomerPortraitJson().isEmpty()){
             Map<String, String> portraitList = JSON.parseObject(

+ 8 - 1
fs-service/src/main/java/com/fs/crm/utils/CrmCustomerAiTagUtil.java

@@ -80,7 +80,7 @@ public class CrmCustomerAiTagUtil {
 
         // 3. 调用AI服务
         R aiResponse = callAiService(requestParam, logId,APP_KEY);
-        System.out.println(aiResponse);
+//        System.out.println(aiResponse);
         // 4. 解析响应并保存
         List<CrmCustomerAiTagVo> results = parseAiResponse(aiResponse, customerId);
 
@@ -407,6 +407,13 @@ public class CrmCustomerAiTagUtil {
         if (!maps.isEmpty()){
             CrmCustomerAnalyzeMapper bean = SpringUtils.getBean(CrmCustomerAnalyzeMapper.class);
             CrmCustomerAnalyze crmCustomerAnalyze = bean.selectLatestOne(Long.valueOf(customerId));
+            if (crmCustomerAnalyze == null){
+                CrmCustomerMapper customerMapper = SpringUtils.getBean(CrmCustomerMapper.class);
+                CrmCustomer crmCustomer = customerMapper.selectCrmCustomerById(Long.valueOf(customerId));
+                crmCustomerAnalyze = new CrmCustomerAnalyze();
+                crmCustomerAnalyze.setCustomerId(Long.valueOf(customerId));
+                crmCustomerAnalyze.setCustomerName(crmCustomer.getCustomerName());
+            }
             crmCustomerAnalyze.setAiChatRecord(JSONUtil.toJsonStr(maps));
             crmCustomerAnalyze.setCreateTime(new Date());
             bean.insertCrmCustomerAnalyze(crmCustomerAnalyze);

+ 18 - 0
fs-service/src/main/java/com/fs/crm/vo/CrmCustomerListQueryVO.java

@@ -2,11 +2,13 @@ package com.fs.crm.vo;
 
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
+import com.fs.crm.domain.CrmCustomerProperty;
 import lombok.Data;
 
 import java.io.Serializable;
 import java.math.BigDecimal;
 import java.util.Date;
+import java.util.List;
 
 @Data
 public class CrmCustomerListQueryVO implements Serializable
@@ -113,4 +115,20 @@ public class CrmCustomerListQueryVO implements Serializable
     /** 认领开始时间 */
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
     private Date startTime;
+
+    /** 流失风险等级 0:无风险;1:低风险;2:中风险;3:高风险 */
+    @Excel(name = "流失风险等级 0:无风险;1:低风险;2:中风险;3:高风险")
+    private Long attritionLevel;
+    /** 意向度 */
+    @Excel(name = "意向度")
+    private Long intentionDegree;
+
+    /**
+     * ai标签
+     */
+    private List<CrmCustomerProperty> properties;
+    /**
+     * 客户关注点
+     */
+    private String customerFocusJson;
 }

+ 16 - 1
fs-service/src/main/java/com/fs/crm/vo/CrmLineCustomerListQueryVO.java

@@ -2,10 +2,12 @@ package com.fs.crm.vo;
 
 import com.fasterxml.jackson.annotation.JsonFormat;
 import com.fs.common.annotation.Excel;
+import com.fs.crm.domain.CrmCustomerProperty;
 import lombok.Data;
 
 import java.io.Serializable;
 import java.util.Date;
+import java.util.List;
 
 @Data
 public class CrmLineCustomerListQueryVO implements Serializable
@@ -106,5 +108,18 @@ public class CrmLineCustomerListQueryVO implements Serializable
     /** 非重客户Id(重客户最早录入手机号码的客户id) */
     private Long dCustomerId;
 
-
+    /** 流失风险等级 0:未知;1:无风险;2:低风险;3:中风险;4:高风险 */
+    @Excel(name = "流失风险等级 0:未知;1:无风险;2:低风险;3:中风险;4:高风险")
+    private Long attritionLevel;
+    /** 意向度 */
+    @Excel(name = "意向度")
+    private String intentionDegree;
+    /**
+     * ai标签
+     */
+    private List<CrmCustomerProperty> properties;
+    /**
+     * 客户关注点
+     */
+    private String customerFocusJson;
 }