Selaa lähdekoodia

Merge remote-tracking branch 'origin/master'

yys 1 kuukausi sitten
vanhempi
commit
85fbe60b1f
100 muutettua tiedostoa jossa 6268 lisäystä ja 18 poistoa
  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. 519 0
      fs-admin/src/main/java/com/fs/task/QwExternalAiAnalyzeTask.java
  4. 199 0
      fs-admin/src/main/java/com/fs/tenant/task/TenantBillTask.java
  5. 128 0
      fs-company/src/main/java/com/fs/company/controller/companyWorkflow/CompanyWorkflowLobsterController.java
  6. 147 0
      fs-company/src/main/java/com/fs/company/controller/companyWorkflow/ExternalApiConfigController.java
  7. 5 1
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  8. 111 0
      fs-company/src/main/java/com/fs/company/controller/knowledge/CompanyKnowledgeAuditController.java
  9. 132 0
      fs-company/src/main/java/com/fs/company/controller/knowledge/CompanyKnowledgeBaseController.java
  10. 106 0
      fs-company/src/main/java/com/fs/company/controller/tag/CompanyTagTemplateBindingController.java
  11. 1 0
      fs-company/src/main/java/com/fs/framework/config/SecurityConfig.java
  12. 52 0
      fs-service/src/main/java/com/fs/company/domain/CompanyKnowledgeAudit.java
  13. 64 0
      fs-service/src/main/java/com/fs/company/domain/CompanyKnowledgeBase.java
  14. 43 0
      fs-service/src/main/java/com/fs/company/domain/CompanyKnowledgeSuggestion.java
  15. 46 0
      fs-service/src/main/java/com/fs/company/domain/CompanyTagTemplateBinding.java
  16. 40 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowLobster.java
  17. 40 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowLobsterEdge.java
  18. 40 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowLobsterNode.java
  19. 37 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowLobsterRecord.java
  20. 36 0
      fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowLobsterVariable.java
  21. 25 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyKnowledgeAuditMapper.java
  22. 25 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyKnowledgeBaseMapper.java
  23. 24 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyKnowledgeSuggestionMapper.java
  24. 29 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyTagTemplateBindingMapper.java
  25. 15 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowLobsterEdgeMapper.java
  26. 19 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowLobsterMapper.java
  27. 15 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowLobsterNodeMapper.java
  28. 13 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowLobsterRecordMapper.java
  29. 15 0
      fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowLobsterVariableMapper.java
  30. 29 0
      fs-service/src/main/java/com/fs/company/param/CompanyWorkflowLobsterCanvasParam.java
  31. 18 0
      fs-service/src/main/java/com/fs/company/param/CompanyWorkflowLobsterConfirmParam.java
  32. 40 0
      fs-service/src/main/java/com/fs/company/param/CompanyWorkflowLobsterEdgeParam.java
  33. 15 0
      fs-service/src/main/java/com/fs/company/param/CompanyWorkflowLobsterGenerateParam.java
  34. 20 0
      fs-service/src/main/java/com/fs/company/param/CompanyWorkflowLobsterNodeParam.java
  35. 18 0
      fs-service/src/main/java/com/fs/company/param/CompanyWorkflowLobsterVariableParam.java
  36. 50 0
      fs-service/src/main/java/com/fs/company/service/ICompanyKnowledgeAuditService.java
  37. 60 0
      fs-service/src/main/java/com/fs/company/service/ICompanyKnowledgeBaseService.java
  38. 50 0
      fs-service/src/main/java/com/fs/company/service/ICompanyTagTemplateBindingService.java
  39. 62 0
      fs-service/src/main/java/com/fs/company/service/ICompanyWorkflowLobsterService.java
  40. 126 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyKnowledgeAuditServiceImpl.java
  41. 165 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyKnowledgeBaseServiceImpl.java
  42. 148 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyTagTemplateBindingServiceImpl.java
  43. 598 0
      fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowLobsterServiceImpl.java
  44. 18 0
      fs-service/src/main/java/com/fs/company/vo/CompanyWorkflowLobsterDetailVO.java
  45. 43 0
      fs-service/src/main/java/com/fs/companyWorkflow/domain/ExternalApiCallLog.java
  46. 85 0
      fs-service/src/main/java/com/fs/companyWorkflow/domain/ExternalApiConfig.java
  47. 8 0
      fs-service/src/main/java/com/fs/companyWorkflow/mapper/ExternalApiCallLogMapper.java
  48. 8 0
      fs-service/src/main/java/com/fs/companyWorkflow/mapper/ExternalApiConfigMapper.java
  49. 8 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/IExternalApiCallLogService.java
  50. 11 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/IExternalApiConfigService.java
  51. 12 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiCallLogPageReq.java
  52. 15 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiConfigPageReq.java
  53. 24 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiConfigSaveReq.java
  54. 11 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiTestReq.java
  55. 25 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiTestRequest.java
  56. 16 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/dto/ExternalApiTestResult.java
  57. 12 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/impl/ExternalApiCallLogServiceImpl.java
  58. 135 0
      fs-service/src/main/java/com/fs/companyWorkflow/service/impl/ExternalApiConfigServiceImpl.java
  59. 6 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerAnalyzeMapper.java
  60. 25 2
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java
  61. 19 0
      fs-service/src/main/java/com/fs/crm/service/ICrmCustomerAnalyzeService.java
  62. 339 8
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerAnalyzeServiceImpl.java
  63. 9 2
      fs-service/src/main/java/com/fs/crm/utils/CrmCustomerAiTagUtil.java
  64. 18 0
      fs-service/src/main/java/com/fs/crm/vo/CrmCustomerListQueryVO.java
  65. 16 1
      fs-service/src/main/java/com/fs/crm/vo/CrmLineCustomerListQueryVO.java
  66. 4 4
      fs-service/src/main/java/com/fs/fastGpt/mapper/FastGptChatMsgMapper.java
  67. 91 0
      fs-service/src/main/java/com/fs/qw/domain/QwCustomerProperty.java
  68. 91 0
      fs-service/src/main/java/com/fs/qw/domain/QwExternalAiAnalyze.java
  69. 38 0
      fs-service/src/main/java/com/fs/qw/domain/QwExternalAiAnalyzeSession.java
  70. 98 0
      fs-service/src/main/java/com/fs/qw/domain/audit/QwMsgAuditMessage.java
  71. 31 0
      fs-service/src/main/java/com/fs/qw/domain/audit/QwMsgAuditRaw.java
  72. 21 0
      fs-service/src/main/java/com/fs/qw/domain/audit/QwMsgAuditSeq.java
  73. 14 0
      fs-service/src/main/java/com/fs/qw/dto/QwMsgAuditConversationDTO.java
  74. 20 0
      fs-service/src/main/java/com/fs/qw/mapper/QwCustomerPropertyMapper.java
  75. 20 0
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalAiAnalyzeMapper.java
  76. 21 0
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalAiAnalyzeSessionMapper.java
  77. 90 0
      fs-service/src/main/java/com/fs/qw/mapper/QwMsgAuditMessageMapper.java
  78. 23 0
      fs-service/src/main/java/com/fs/qw/param/audit/QwAiTagGainParam.java
  79. 25 0
      fs-service/src/main/java/com/fs/qw/param/audit/QwAuditMessagebackupParam.java
  80. 19 0
      fs-service/src/main/java/com/fs/qw/service/IQwCustomerPropertyService.java
  81. 168 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwCustomerPropertyServiceImpl.java
  82. 63 0
      fs-service/src/main/java/com/fs/qw/shardingConfig/QwMsgAuditMessageSharding.java
  83. 25 0
      fs-service/src/main/java/com/fs/qw/vo/QwCustomerAiTagVo.java
  84. 5 0
      fs-service/src/main/java/com/fs/tenant/domain/TenantInfo.java
  85. 9 0
      fs-service/src/main/java/com/fs/tenant/dto/SopTokenDto.java
  86. 14 0
      fs-service/src/main/java/com/fs/tenant/dto/TenantBillDto.java
  87. 29 0
      fs-service/src/main/java/com/fs/tenant/enums/FeeItemEnum.java
  88. 17 0
      fs-service/src/main/java/com/fs/tenant/mapper/TenantInfoMapper.java
  89. 16 0
      fs-service/src/main/java/com/fs/tenant/service/TenantInfoService.java
  90. 143 0
      fs-service/src/main/java/com/fs/tenant/service/impl/TenantInfoServiceImpl.java
  91. 93 0
      fs-service/src/main/resources/mapper/company/CompanyKnowledgeAuditMapper.xml
  92. 105 0
      fs-service/src/main/resources/mapper/company/CompanyKnowledgeBaseMapper.xml
  93. 88 0
      fs-service/src/main/resources/mapper/company/CompanyKnowledgeSuggestionMapper.xml
  94. 107 0
      fs-service/src/main/resources/mapper/company/CompanyTagTemplateBindingMapper.xml
  95. 50 0
      fs-service/src/main/resources/mapper/company/CompanyWorkflowLobsterEdgeMapper.xml
  96. 71 0
      fs-service/src/main/resources/mapper/company/CompanyWorkflowLobsterMapper.xml
  97. 30 0
      fs-service/src/main/resources/mapper/company/CompanyWorkflowLobsterNodeMapper.xml
  98. 50 0
      fs-service/src/main/resources/mapper/company/CompanyWorkflowLobsterRecordMapper.xml
  99. 30 0
      fs-service/src/main/resources/mapper/company/CompanyWorkflowLobsterVariableMapper.xml
  100. 55 0
      fs-service/src/main/resources/mapper/crm/CrmCustomerAnalyzeMapper.xml

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

+ 519 - 0
fs-admin/src/main/java/com/fs/task/QwExternalAiAnalyzeTask.java

@@ -0,0 +1,519 @@
+package com.fs.task;
+
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.fs.crm.service.ICrmCustomerAnalyzeService;
+import com.fs.qw.domain.QwExternalAiAnalyze;
+import com.fs.qw.domain.QwExternalAiAnalyzeSession;
+import com.fs.qw.domain.audit.QwMsgAuditMessage;
+import com.fs.qw.mapper.QwExternalAiAnalyzeMapper;
+import com.fs.qw.mapper.QwExternalAiAnalyzeSessionMapper;
+import com.fs.qw.mapper.QwMsgAuditMessageMapper;
+import com.fs.qw.param.audit.QwAuditMessagebackupParam;
+import com.fs.qw.service.IQwCustomerPropertyService;
+import com.fs.qw.shardingConfig.QwMsgAuditMessageSharding;
+import com.fs.system.domain.SysConfig;
+import com.fs.system.mapper.SysConfigMapper;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.dao.DuplicateKeyException;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.util.*;
+import java.util.concurrent.*;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
+
+@Component("QwExternalAiAnalyzeTask")
+@RequiredArgsConstructor
+@Slf4j
+public class QwExternalAiAnalyzeTask {
+    private final QwMsgAuditMessageMapper qwMsgAuditMessageMapper;
+    private final QwExternalAiAnalyzeMapper qwExternalAiAnalyzeMapper;
+    private final QwExternalAiAnalyzeSessionMapper qwExternalAiAnalyzeSessionMapper;
+    private final ICrmCustomerAnalyzeService crmCustomerAnalyzeService;
+    private final IQwCustomerPropertyService qwCustomerPropertyService;
+    private final SysConfigMapper sysConfigMapper;
+    private final static String CHAT_BACKUP_MSG_TYPE = "text";
+
+    //调用时间间隔min
+    @Value("${qw.external.ai.interval:5}")
+    private Integer interval;
+
+//    //表分片数量
+//    @Value("${qw.external.ai.devide:12}")
+//    private Integer divideNum;
+
+    // 自定义线程池
+    private final ExecutorService executorService = new ThreadPoolExecutor(
+            5,  // 核心线程数
+            10, // 最大线程数
+            60, TimeUnit.SECONDS,
+            new LinkedBlockingQueue<>(200),
+            r -> {
+                Thread thread = new Thread(r);
+                thread.setName("qw-external-ai-processor-" + thread.getId());
+                return thread;
+            },
+            new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用线程处理
+    );
+    //根据会话存档频率拉取
+    public void processQwChatBackup(){
+        log.info("AI开始处理-会话存档");
+        LocalDateTime now = LocalDateTime.now();
+        QwMsgAuditMessage qwMsgAuditMessage = new QwMsgAuditMessage();
+        qwMsgAuditMessage.setMsgType(CHAT_BACKUP_MSG_TYPE);
+        qwMsgAuditMessage.setRoomId("");
+        Long timestamp = now.minusMinutes(interval).atZone(ZoneId.systemDefault()).toInstant().getEpochSecond();//根据配置的分钟间隔获取时间戳
+        qwMsgAuditMessage.setAnalyzeStartTime(timestamp);
+        List<QwMsgAuditMessage> qwMsgAuditMessages = new ArrayList<>();
+        for (int shard = 0; shard < QwMsgAuditMessageSharding.SHARD_COUNT; shard++) {
+            List<QwMsgAuditMessage> shardMessages =
+                    qwMsgAuditMessageMapper.selectQwMsgAuditMessageListByShard(shard, qwMsgAuditMessage);
+            if (shardMessages != null && !shardMessages.isEmpty()) {
+                qwMsgAuditMessages.addAll(shardMessages);
+            }
+        }
+//                .selectList(
+//                new LambdaQueryWrapper<QwMsgAuditMessage>()
+//                        .gt(QwMsgAuditMessage::getMsgTime, now.minusMinutes(interval))
+//                        .eq(QwMsgAuditMessage::getMsgType, CHAT_BACKUP_MSG_TYPE)
+//                        .eq(QwMsgAuditMessage::getRoomId, "").or().isNull(QwMsgAuditMessage::getRoomId)
+//        );
+        if (qwMsgAuditMessages.isEmpty()) {
+            log.info("会话存档qw_msg_audit_message无新数据");
+            return;
+        }
+
+        // 1) 按 msgTime 升序,确保输出组内顺序正确
+        qwMsgAuditMessages.sort(Comparator.comparing(
+                QwMsgAuditMessage::getMsgTime,
+                Comparator.nullsLast(Long::compareTo)
+        ));
+
+        // 2) 解析每条消息的 from_user / to_list(toList 是 JSON 字符串)
+//        List<ParsedMsg> parsedMsgs = new ArrayList<>(qwMsgAuditMessages.size());
+//        for (QwMsgAuditMessage msg : qwMsgAuditMessages) {
+//            Set<String> toUsers = parseToUserSet(msg.getToList());
+//            parsedMsgs.add(new ParsedMsg(msg, msg.getFromUser(), toUsers));
+//        }
+//
+//        // 3) 按会话参与双方分组:
+//        //    只要 A.from_user 出现在 B.to_list,或 B.from_user 出现在 A.to_list,就视为同一会话链,归为同一组;
+//        //    使用并查集把所有满足条件的消息聚成连通分量。
+//        UnionFind uf = new UnionFind(parsedMsgs.size());
+//
+//        // 反向索引:fromUser -> 消息下标列表
+//        Map<String, List<Integer>> fromUserIndex = new HashMap<>();
+//        for (int i = 0; i < parsedMsgs.size(); i++) {
+//            ParsedMsg pm = parsedMsgs.get(i);
+//            if (pm.fromUser == null) {
+//                continue;
+//            }
+//            fromUserIndex.computeIfAbsent(pm.fromUser, k -> new ArrayList<>()).add(i);
+//        }
+//
+//        for (int i = 0; i < parsedMsgs.size(); i++) {
+//            ParsedMsg a = parsedMsgs.get(i);
+//            if (a.fromUser == null || a.toUsers == null || a.toUsers.isEmpty()) {
+//                continue;
+//            }
+//            for (String toUser : a.toUsers) {
+//                List<Integer> candidates = fromUserIndex.get(toUser);
+//                if (candidates == null || candidates.isEmpty()) {
+//                    continue;
+//                }
+//                for (Integer j : candidates) {
+//                    ParsedMsg b = parsedMsgs.get(j);
+//                    if (b == null || b.fromUser == null) {
+//                        continue;
+//                    }
+//                    // 条件1:A.from 在 B.to_list 中
+//                    boolean aInB = b.toUsers != null && b.toUsers.contains(a.fromUser);
+//                    // 条件2:B.from 在 A.to_list 中
+//                    boolean bInA = a.toUsers.contains(b.fromUser);
+//
+//                    if (aInB || bInA) {
+//                        uf.union(i, j);
+//                    }
+//                }
+//            }
+//        }
+
+        // 4) 转成分组结构并按 msgTime 排序
+//        Map<Integer, List<ParsedMsg>> groupedMap = new HashMap<>();
+//        for (int i = 0; i < parsedMsgs.size(); i++) {
+//            groupedMap.computeIfAbsent(uf.find(i), k -> new ArrayList<>()).add(parsedMsgs.get(i));
+//        }
+        Map<String, List<QwMsgAuditMessage>> collect = qwMsgAuditMessages.stream().collect(Collectors.groupingBy(QwMsgAuditMessage::getConversationKey));
+        List<List<QwMsgAuditMessage>> groupedParsed = new ArrayList<>(
+                collect.values()
+//                groupedMap.values()
+        );
+
+//        for (List<ParsedMsg> group : groupedParsed) {
+//            group.sort(Comparator.comparing(ParsedMsg::getMsgTime, Comparator.nullsLast(Long::compareTo)));
+//        }
+
+        log.info("会话存档分组完成: 分组数={}", groupedParsed.size());
+
+        // 5) 生成入参:每组拼 history + 统一更新 param 外部联系人信息
+        ArrayList<QwAuditMessagebackupParam> historys = new ArrayList<>(groupedParsed.size());
+        for (List<QwMsgAuditMessage> group : groupedParsed) {
+            if (group == null || group.isEmpty()) {
+                continue;
+            }
+
+            QwMsgAuditMessage first = group.get(0);
+            QwAuditMessagebackupParam param = new QwAuditMessagebackupParam();
+            param.setCorpId(first.getCorpId());
+
+            // 用“第一条消息”决定 user/external/qwUserId(与旧逻辑一致,但避免重复 parseToUserSet)
+            Integer role = first.getFromUserRole();
+            if (role != null && role == 2) {
+                param.setExternalUserId(first.getFromUser());
+                if (!first.getFromUser().isEmpty()) {
+                    Object o = JSONArray.parseArray(first.getToList()).get(0);
+                    param.setQwUserId(o.toString());
+                }
+            } else {
+                if (!first.getToList().isEmpty()) {
+                    Object o = JSONArray.parseArray(first.getToList()).get(0);
+                    param.setExternalUserId(o.toString());
+                }
+                param.setQwUserId(first.getFromUser());
+            }
+
+            ArrayList<Map<String, String>> maps = new ArrayList<>();
+//            StringBuilder historyArr = new StringBuilder("{");
+            for (QwMsgAuditMessage pm : group) {
+                String roleTag = (pm.getFromUserRole() != null && pm.getFromUserRole() == 2) ? "user" : "ai";
+                String text = pm.getTextContent();
+                if (text == null) {
+                    text = "";
+                }
+                Map<String, String> map = new HashMap<>();
+                map.put(roleTag, text);
+//                if (historyArr.length()>1)historyArr.append(",");
+//                historyArr.append("\"").append(roleTag).append("\":\"").append(text).append("\"");
+                maps.add(map);
+            }
+//            historyArr.append("}");
+            param.setHistory(JSONUtil.toJsonStr(maps));
+            historys.add(param);
+        }
+
+        log.info("会话存档处理完成: 分组数={}", historys.size());
+        //入库
+        List<QwExternalAiAnalyze> qwExternalAiAnalyzes = new ArrayList<>();
+        historys.forEach(o -> {
+            QwExternalAiAnalyze qwExternalAiAnalyze = new QwExternalAiAnalyze();
+            QwExternalAiAnalyzeSession session = new QwExternalAiAnalyzeSession();
+            session.setCorpId(o.getCorpId());
+            session.setQwUserId(o.getQwUserId());
+            session.setExternalUserId(o.getExternalUserId());
+            Long sessionId;
+            //获取唯一sessionId,调用ai时绑定为同一对话
+            try {
+                qwExternalAiAnalyzeSessionMapper.insert(session);
+                sessionId = session.getSessionId();
+            } catch (DuplicateKeyException e) {
+                QwExternalAiAnalyzeSession exist = qwExternalAiAnalyzeSessionMapper.selectByUniqueKey(
+                        o.getExternalUserId(), o.getCorpId(), o.getQwUserId());
+                if (exist == null || exist.getSessionId() == null) {
+                    throw e;
+                }
+                sessionId = exist.getSessionId();
+            }
+
+            qwExternalAiAnalyze.setAiChatRecord(o.getHistory());
+            qwExternalAiAnalyze.setCorpId(o.getCorpId());
+            qwExternalAiAnalyze.setQwUserId(o.getQwUserId());
+            qwExternalAiAnalyze.setExternalUserId(o.getExternalUserId());
+            qwExternalAiAnalyze.setSessionId(sessionId);
+            qwExternalAiAnalyze.setCreateTime(new Date());
+            qwExternalAiAnalyzes.add(qwExternalAiAnalyze);
+        });
+        int affected = qwExternalAiAnalyzeMapper.insertBatch(qwExternalAiAnalyzes);
+        if (qwExternalAiAnalyzes == null || qwExternalAiAnalyzes.isEmpty()) {
+            log.info("会话分析数据为空");
+            return;
+        }
+//        List<Long> insertedIds = qwExternalAiAnalyzes.stream()
+//                .map(QwExternalAiAnalyze::getId)
+//                .filter(id -> id != null)
+//                .collect(Collectors.toList());
+        log.info("会话分析批量入库完成: 影响行数={}", affected);
+        List<List<QwExternalAiAnalyze>> batches = new ArrayList<>();
+        for (int i = 0; i < qwExternalAiAnalyzes.size(); i += 5) {
+            batches.add(qwExternalAiAnalyzes.subList(i, Math.min(i + 5, qwExternalAiAnalyzes.size())));
+        }
+        AtomicInteger successCount = new AtomicInteger(0);
+        AtomicInteger failCount = new AtomicInteger(0);
+        List<CompletableFuture<Void>> futures = new ArrayList<>();
+        for (List<QwExternalAiAnalyze> batch : batches) {
+            futures.add(CompletableFuture.runAsync(() -> processSingleCustomer(batch,successCount,failCount), executorService));
+        }
+        CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
+
+    }
+
+    private void processSingleCustomer(List<QwExternalAiAnalyze> qwExternalAiAnalyzes, AtomicInteger successCount,
+                                       AtomicInteger failCount) {
+        String threadName = Thread.currentThread().getName();
+        long batchStartTime = System.currentTimeMillis();
+
+        try {
+            log.info("线程 {} 开始处理批次, 数据量: {}", threadName, qwExternalAiAnalyzes.size());
+
+            for (QwExternalAiAnalyze data : qwExternalAiAnalyzes) {
+                processSingleAiAnalyze(data, successCount, failCount);
+            }
+
+            long costTime = System.currentTimeMillis() - batchStartTime;
+            log.info("线程 {} 批次处理完成, 数据量: {}, 耗时: {}ms",
+                    threadName, qwExternalAiAnalyzes.size(), costTime);
+        } catch (Exception e) {
+            failCount.addAndGet(qwExternalAiAnalyzes.size());
+            log.error("线程 {} 批次处理失败, 数据量: {}", threadName, qwExternalAiAnalyzes.size(), e);
+            throw new RuntimeException("批次处理失败", e);
+        }
+    }
+
+    private void processSingleAiAnalyze(QwExternalAiAnalyze qwExternalAiAnalyze, AtomicInteger successCount,
+                                       AtomicInteger failCount) {
+        log.info("开始处理单条会话分析: {}", qwExternalAiAnalyze.getId());
+        try {
+            //TODO 调用AI分析 分析结果
+
+            String dataJson =
+//                    qwExternalAiAnalyze.getAiChatRecord();
+                    parseAiChat2String(qwExternalAiAnalyze.getAiChatRecord());
+            Long logId = qwExternalAiAnalyze.getSessionId();
+            SysConfig sysConfig = sysConfigMapper.selectConfigByConfigKey("aiTagTradeType.config");
+
+            long startTime = System.currentTimeMillis();
+
+            Executor asyncPool = ForkJoinPool.commonPool();
+            // 6 个 AI 接口并行;使用 commonPool,避免与批次线程池 executorService 嵌套导致死锁
+// 使用 supplyAsync 获取返回值,定义具体返回类型
+            CompletableFuture<String> portraitFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiGeneratedCustomerPortraitQw(qwExternalAiAnalyze, dataJson, logId)
+                            , asyncPool)
+            ;
+
+            CompletableFuture<String> summaryFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiCommunicationSummaryQw(qwExternalAiAnalyze, dataJson, logId)
+                            , asyncPool)
+            ;
+
+            CompletableFuture<String> abstractFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiCommunicationAbstractQw(qwExternalAiAnalyze, dataJson, logId)
+                            , asyncPool)
+            ;
+
+            CompletableFuture<Long> attritionFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiAttritionLevelQw(qwExternalAiAnalyze, dataJson, logId)
+                            , asyncPool)
+            ;
+
+            CompletableFuture<String> focusFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiCustomerFocusQw(qwExternalAiAnalyze, dataJson, logId)
+                            , asyncPool)
+            ;
+
+            CompletableFuture<String> intentionFuture = CompletableFuture.supplyAsync(() ->
+                    crmCustomerAnalyzeService.aiIntentionDegreeQw(qwExternalAiAnalyze, dataJson, logId)
+                            , asyncPool)
+            ;
+            if (sysConfig != null){
+                JSONObject jsonObject = JSONObject.parseObject(sysConfig.getConfigValue());
+                CompletableFuture.runAsync(() ->
+                                qwCustomerPropertyService.analyzeAiTagByTrade(jsonObject.get("tradeType").toString(),qwExternalAiAnalyze)
+                        , asyncPool)
+                ;
+            }else {
+                log.error("ai标签-行业未配置");
+            }
+
+
+// 等待所有异步任务完成
+            CompletableFuture.allOf(portraitFuture, summaryFuture, abstractFuture,
+                    attritionFuture, focusFuture, intentionFuture).join();
+
+            qwExternalAiAnalyze.setCustomerPortraitJson(portraitFuture.get());
+            qwExternalAiAnalyze.setCommunicationSummary(summaryFuture.get());
+            qwExternalAiAnalyze.setCommunicationAbstract(abstractFuture.get());
+            qwExternalAiAnalyze.setAttritionLevel(attritionFuture.get());
+            qwExternalAiAnalyze.setCustomerFocusJson(focusFuture.get());
+            qwExternalAiAnalyze.setIntentionDegree(intentionFuture.get());
+            Integer i = crmCustomerAnalyzeService.updateQwAnalyzeByCustomerId(qwExternalAiAnalyze);
+            long costTime = System.currentTimeMillis() - startTime;
+            successCount.incrementAndGet();
+            log.info("客户 {} 的AI分析完成, 耗时: {}ms,更新{}条", qwExternalAiAnalyze.getExternalUserId(), costTime, i);
+
+
+        }
+         catch (Exception e) {
+            failCount.incrementAndGet();
+            log.error("单条会话分析失败: {}", qwExternalAiAnalyze.getId(), e);
+        }
+    }
+
+
+    private static String parseAiChat2String(String aiChatRecord){
+        JSONArray objects = JSONArray.parseArray(aiChatRecord);
+        StringBuilder result = new StringBuilder("{");
+        if (objects != null) {
+            objects.stream().iterator().forEachRemaining(item -> {
+                if (result.length() > 1)result.append(",");
+                result.append(item.toString());
+            });
+//            for (int i = 0; i < objects.size(); i++) {
+//                JSONObject item = objects.getJSONObject(i);
+//                if (item == null) {
+//                    continue;
+//                }
+//                String role = item.getString("role");
+//                String content = item.getString("content");
+//                String roleTag = "user".equals(role) ? "user" : "ai";
+//
+//                if (result.length() > 1) {
+//                    result.append(",");
+//                }
+//                result.append("\"").append(roleTag).append("\":\"")
+//                        .append(JSON.toJSONString(content == null ? "" : content)).append("\"");
+//            }
+        }
+        result.append("}");
+        return result.toString();
+    }
+
+    private static boolean isMutualContained(ParsedMsg a, ParsedMsg b) {
+        if (a == null || b == null) {
+            return false;
+        }
+        if (a.fromUser == null || b.fromUser == null) {
+            return false;
+        }
+        return a.toUsers.contains(b.fromUser) && b.toUsers.contains(a.fromUser);
+    }
+
+    private static Long groupMinTime(List<ParsedMsg> group) {
+        if (group == null || group.isEmpty()) {
+            return null;
+        }
+        Long min = null;
+        for (ParsedMsg pm : group) {
+            if (pm == null) {
+                continue;
+            }
+            Long t = pm.getMsgTime();
+            if (t == null) {
+                continue;
+            }
+            if (min == null || t < min) {
+                min = t;
+            }
+        }
+        return min;
+    }
+
+    private static class UnionFind {
+        private final int[] parent;
+        private final int[] rank;
+
+        private UnionFind(int n) {
+            this.parent = new int[n];
+            this.rank = new int[n];
+            for (int i = 0; i < n; i++) {
+                parent[i] = i;
+            }
+        }
+
+        private int find(int x) {
+            if (parent[x] != x) {
+                parent[x] = find(parent[x]);
+            }
+            return parent[x];
+        }
+
+        private void union(int a, int b) {
+            int ra = find(a);
+            int rb = find(b);
+            if (ra == rb) {
+                return;
+            }
+            if (rank[ra] < rank[rb]) {
+                parent[ra] = rb;
+            } else if (rank[ra] > rank[rb]) {
+                parent[rb] = ra;
+            } else {
+                parent[rb] = ra;
+                rank[ra]++;
+            }
+        }
+    }
+
+    private static Set<String> parseToUserSet(String toListJson) {
+        if (toListJson == null || toListJson.trim().isEmpty()) {
+            return Collections.emptySet();
+        }
+
+        // 优先按 JSON 数组解析:["1","2"]
+        try {
+            JSONArray arr = JSON.parseArray(toListJson);
+            if (arr != null) {
+                Set<String> set = new HashSet<>();
+                for (int i = 0; i < arr.size(); i++) {
+                    Object v = arr.get(i);
+                    if (v != null) {
+                        set.add(String.valueOf(v));
+                    }
+                }
+                return set;
+            }
+        } catch (Exception ignore) {
+            // fallthrough
+        }
+
+        // 兜底:toListJson 可能是单值或形如 ["1","2"] 的字符串(被转义等情况)
+        String s = toListJson.trim();
+        if (s.startsWith("[") && s.endsWith("]")) {
+            s = s.substring(1, s.length() - 1);
+        }
+        s = s.replace("\"", "");
+
+        Set<String> set = new HashSet<>();
+        if (!s.trim().isEmpty()) {
+            String[] parts = s.split(",");
+            for (String p : parts) {
+                if (p != null && !p.trim().isEmpty()) {
+                    set.add(p.trim());
+                }
+            }
+        }
+        return set;
+    }
+
+    private static class ParsedMsg {
+        private final QwMsgAuditMessage msg;
+        private final String fromUser;
+        private final Set<String> toUsers;
+
+        private ParsedMsg(QwMsgAuditMessage msg, String fromUser, Set<String> toUsers) {
+            this.msg = msg;
+            this.fromUser = fromUser;
+            this.toUsers = toUsers == null ? Collections.<String>emptySet() : toUsers;
+        }
+
+        private Long getMsgTime() {
+            return msg == null ? null : msg.getMsgTime();
+        }
+    }
+}

+ 199 - 0
fs-admin/src/main/java/com/fs/tenant/task/TenantBillTask.java

@@ -0,0 +1,199 @@
+package com.fs.tenant.task;
+
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fs.billing.domain.BillingDetail;
+import com.fs.billing.domain.FeePlanItem;
+import com.fs.billing.domain.TenantWallet;
+import com.fs.billing.domain.UsageEvent;
+import com.fs.billing.mapper.BillingDetailMapper;
+import com.fs.billing.mapper.TenantWalletMapper;
+import com.fs.billing.mapper.UsageEventMapper;
+import com.fs.common.enums.DataSourceType;
+import com.fs.common.utils.DateUtils;
+import com.fs.framework.datasource.DynamicDataSourceContextHolder;
+import com.fs.framework.datasource.TenantDataSourceManager;
+import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.enums.FeeItemEnum;
+import com.fs.tenant.service.TenantInfoService;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.commons.lang3.StringUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.math.BigDecimal;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+@Slf4j
+@Component("tenantBillTask")
+public class TenantBillTask {
+
+    @Autowired
+    private TenantInfoService tenantInfoService;
+
+    @Autowired
+    private TenantDataSourceManager tenantDataSourceManager;
+
+    @Autowired
+    private TenantWalletMapper tenantWalletMapper;
+
+    @Autowired
+    private UsageEventMapper usageEventMapper;
+    @Autowired
+    private BillingDetailMapper billingDetailMapper;
+
+    /**
+     * 租户费用结算 (每日01:00执行)
+     */
+    public void tenantFeeSettlementTask() {
+        List<TenantInfo> tenants = getValidTenants();
+
+        for (TenantInfo tenant : tenants) {
+            try {
+                processTenant(tenant);
+            } catch (Exception e) {
+                log.error("租户结算失败 tenantId={}", tenant.getId(), e);
+            } finally {
+                tenantDataSourceManager.clear();
+            }
+        }
+    }
+
+    /**
+     * 查询有效租户
+     */
+    private List<TenantInfo> getValidTenants() {
+        LambdaQueryWrapper<TenantInfo> query = new LambdaQueryWrapper<>();
+        query.eq(TenantInfo::getStatus, 1)
+                .gt(TenantInfo::getExpireTime, DateUtils.getNowDate());
+
+        return tenantInfoService.list(query);
+    }
+
+    /**
+     * 处理单个租户
+     */
+    private void processTenant(TenantInfo tenant) {
+        if (StringUtils.isBlank(tenant.getFeePlanCode())) {
+            return;
+        }
+
+        List<FeePlanItem> items = tenantInfoService.selectFeeItem(tenant.getFeePlanCode());
+        if (items.isEmpty()) {
+            return;
+        }
+
+        DateTime start = DateUtil.beginOfDay(DateUtil.yesterday());
+        DateTime end = DateUtil.endOfDay(DateUtil.yesterday());
+
+        BigDecimal aiCallFee = getAiCallFee(items);
+        BigDecimal totalFee = BigDecimal.ZERO;
+
+        for (FeePlanItem item : items) {
+            // 切租户库
+            tenantDataSourceManager.switchTenant(tenant);
+            BigDecimal fee = handleFeeItem(item, start, end, aiCallFee,tenant);
+            totalFee = totalFee.add(fee);
+        }
+
+        // 回主库
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        if (totalFee.compareTo(BigDecimal.ZERO) <= 0) {
+            return;
+        }
+        // 费用扣减
+        tenantWalletMapper.deduct(tenant.getId(), totalFee);
+        // 查询租户钱包
+        TenantWallet tenantWallet = tenantWalletMapper.selectByTenantId(tenant.getId());
+        log.info("租户={} 总费用={} 余额={}", tenant.getTenantName(), totalFee, tenantWallet.getBalanceAmount());
+    }
+
+    /**
+     * 费用项处理分发
+     */
+    private BigDecimal handleFeeItem(FeePlanItem item, DateTime start, DateTime end, BigDecimal aiCallFee,TenantInfo tenant) {
+        String code = item.getItemCode();
+
+        switch (FeeItemEnum.valueOf(code)) {
+            case FLOW_POSTPAID:
+                Map<String, Object> traffic = tenantInfoService.getYesterDayTraffic(start, end, item, tenant);
+                return processResult(traffic, item, tenant);
+
+            case CALL_OUT:
+                Map<String, Object> callOut = tenantInfoService.getYesterCallOut(start, end, item, aiCallFee, tenant);
+                return processResult(callOut, item, tenant);
+
+            case ADD_WECHAT:
+                Map<String, Object> addWechat = tenantInfoService.getYesterAddWechat(start, end, item, tenant);
+                return processResult(addWechat, item, tenant);
+
+            case AI_REPLY_TOKEN:
+                Map<String, Object> aiReply = tenantInfoService.getYesterAiReplyToken(start, end, item, tenant);
+                return processResult(aiReply, item, tenant);
+
+            case SOP_TOKEN:
+                Map<String, Object> sopToken = tenantInfoService.getYesterSopToken(start, end, item, tenant);
+                return processResult(sopToken, item, tenant);
+
+            default:
+                return BigDecimal.ZERO;
+        }
+    }
+
+    private BigDecimal processResult(Map<String,Object> map,FeePlanItem item,TenantInfo tenant){
+        BigDecimal fee = new BigDecimal(map.get("price").toString());
+        String usage = map.get("usage").toString();
+        boolean isLeZero = new BigDecimal(usage).compareTo(BigDecimal.ZERO) <= 0;
+        // 没有产生费用 直接返回
+        if (fee.compareTo(BigDecimal.ZERO) <= 0 && isLeZero) {
+            return fee;
+        }
+        saveUsageEventAndBill(usage, item, tenant,fee);
+        return fee;
+    }
+
+    private void saveUsageEventAndBill(String value,FeePlanItem item,TenantInfo tenant,BigDecimal fee){
+        // 切换到主库
+        DynamicDataSourceContextHolder.setDataSourceType(DataSourceType.MASTER.name());
+        log.info("【当前已切换到数据源】:{}", DynamicDataSourceContextHolder.getDataSourceType());
+        String eventID = "EVT_" + DateUtil.current();
+        UsageEvent event = new UsageEvent();
+        event.setTenantId(tenant.getId());
+        event.setEventId(eventID);
+        event.setEventType(item.getItemCode());
+        event.setUsageValue(new BigDecimal(value));
+        event.setUsageUnit(item.getUnit());
+        event.setCreateTime(DateUtils.getNowDate());
+        event.setOccurredAt(DateUtils.getNowDate());
+        usageEventMapper.insert(event);
+
+        BillingDetail billingDetail = new BillingDetail();
+        billingDetail.setTenantId(tenant.getId());
+        billingDetail.setEventId(eventID);
+        billingDetail.setEventType(item.getItemCode());
+        billingDetail.setPlanCode(item.getPlanCode());
+        billingDetail.setPlanVersion(item.getVersion());
+        billingDetail.setUnitPrice(item.getUnitPrice());
+        billingDetail.setUsageValue(new BigDecimal(value));
+        billingDetail.setChargeValue(new BigDecimal(value));
+        billingDetail.setAmount(fee);
+        billingDetail.setBillingMode(tenant.getBillingMode());
+        billingDetail.setCreateTime(DateUtils.getNowDate());
+        billingDetail.setOccurredAt(DateUtils.getNowDate());
+        billingDetailMapper.insert(billingDetail);
+    }
+
+    /**
+     * 获取 AI 外呼费用
+     */
+    private BigDecimal getAiCallFee(List<FeePlanItem> items) {
+        return items.stream()
+                .filter(e -> Objects.equals(e.getItemCode(), FeeItemEnum.AI_CALL.name()))
+                .map(FeePlanItem::getUnitPrice)
+                .findFirst()
+                .orElse(BigDecimal.ZERO);
+    }
+}

+ 128 - 0
fs-company/src/main/java/com/fs/company/controller/companyWorkflow/CompanyWorkflowLobsterController.java

@@ -0,0 +1,128 @@
+package com.fs.company.controller.companyWorkflow;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.ServletUtils;
+import com.fs.company.param.CompanyWorkflowLobsterCanvasParam;
+import com.fs.company.param.CompanyWorkflowLobsterConfirmParam;
+import com.fs.company.param.CompanyWorkflowLobsterGenerateParam;
+import com.fs.company.service.ICompanyWorkflowLobsterService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Collections;
+
+/**
+ * AI工作流龙虾Controller
+ * 合并了模板管理、AI生成、画布编辑等所有工作流相关接口
+ */
+@RestController
+@RequestMapping("/workflow")
+public class CompanyWorkflowLobsterController extends BaseController {
+
+    @Autowired
+    private ICompanyWorkflowLobsterService lobsterService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    // ==================== 模板管理接口 ====================
+
+    /**
+     * 分页查询模板列表
+     */
+    @GetMapping("/template/list")
+    public AjaxResult list(@RequestParam(defaultValue = "1") Integer page,
+                           @RequestParam(defaultValue = "10") Integer size) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return AjaxResult.success(lobsterService.listTemplate(loginUser.getCompany().getCompanyId(), page, size));
+    }
+
+    /**
+     * 获取模板详情(用于画布编辑)
+     */
+    @GetMapping("/template/{templateId}")
+    public AjaxResult getTemplate(@PathVariable Long templateId) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return AjaxResult.success(lobsterService.getTemplate(loginUser.getCompany().getCompanyId(), templateId));
+    }
+
+    /**
+     * 预览模板
+     */
+    @GetMapping("/template/{templateId}/preview")
+    public AjaxResult preview(@PathVariable Long templateId) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return AjaxResult.success(lobsterService.previewTemplate(loginUser.getCompany().getCompanyId(), templateId));
+    }
+
+    /**
+     * 更新模板基本信息
+     */
+    @PutMapping("/template/{templateId}")
+    public AjaxResult updateTemplate(@PathVariable Long templateId, @RequestBody CompanyWorkflowLobsterConfirmParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return lobsterService.updateTemplate(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), templateId, param);
+    }
+
+    /**
+     * 删除模板
+     */
+    @DeleteMapping("/template/{templateId}")
+    public AjaxResult deleteTemplate(@PathVariable Long templateId) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return lobsterService.deleteTemplate(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), templateId);
+    }
+
+    // ==================== AI生成接口 ====================
+
+    /**
+     * AI生成工作流
+     */
+    @PostMapping("/ai-generator/generate")
+    public AjaxResult generate(@RequestBody CompanyWorkflowLobsterGenerateParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        String recordId = lobsterService.generate(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), param);
+        return AjaxResult.success(Collections.singletonMap("recordId", recordId));
+    }
+
+    /**
+     * 获取AI生成结果详情
+     */
+    @GetMapping("/ai-generator/result/{recordId}/detail")
+    public AjaxResult resultDetail(@PathVariable String recordId) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return AjaxResult.success(lobsterService.getResultDetail(loginUser.getCompany().getCompanyId(), recordId));
+    }
+
+    /**
+     * 确认保存原始AI生成结果
+     */
+    @PostMapping("/ai-generator/confirm/{recordId}")
+    public AjaxResult confirm(@PathVariable String recordId) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return lobsterService.confirmRaw(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), recordId);
+    }
+
+    /**
+     * 确认保存编辑后的AI生成结果
+     */
+    @PostMapping("/ai-generator/confirm/{recordId}/edited")
+    public AjaxResult confirmEdited(@PathVariable String recordId, @RequestBody CompanyWorkflowLobsterConfirmParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return lobsterService.confirmEdited(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), recordId, param);
+    }
+
+    // ==================== 画布编辑接口 ====================
+
+    /**
+     * 保存画布数据(包含节点位置、连线等可视化信息)
+     */
+    @PutMapping("/canvas/{templateId}")
+    public AjaxResult saveCanvas(@PathVariable Long templateId, @RequestBody CompanyWorkflowLobsterCanvasParam param) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return lobsterService.saveCanvas(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), templateId, param);
+    }
+}

+ 147 - 0
fs-company/src/main/java/com/fs/company/controller/companyWorkflow/ExternalApiConfigController.java

@@ -0,0 +1,147 @@
+package com.fs.company.controller.companyWorkflow;
+
+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.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.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.service.dto.ExternalApiTestRequest;
+import com.fs.companyWorkflow.service.dto.ExternalApiTestResult;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.Date;
+import java.util.List;
+
+@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));
+    }
+
+    /**
+     * 模板预览(保存后可直接查看)
+     */
+    @GetMapping("/{id}/preview")
+    public Result<ExternalApiConfig> preview(@PathVariable Long id) {
+        ExternalApiConfig config = configService.getById(id);
+        if (config == null) {
+            return Result.error("模板不存在或已删除");
+        }
+        return Result.success(config);
+    }
+
+    @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(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));
+    }
+
+    //获取所有可用接口
+    @GetMapping("/activeList")
+    public Result<List<ExternalApiConfig>> activeList() {
+        LambdaQueryWrapper<ExternalApiConfig> qw = new LambdaQueryWrapper<>();
+        qw.eq(ExternalApiConfig::getStatus, 1);
+        qw.orderByDesc(ExternalApiConfig::getPriority).orderByDesc(ExternalApiConfig::getCreateTime);
+        return Result.success(configService.list(qw));
+    }
+}
+

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

+ 111 - 0
fs-company/src/main/java/com/fs/company/controller/knowledge/CompanyKnowledgeAuditController.java

@@ -0,0 +1,111 @@
+package com.fs.company.controller.knowledge;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.ServletUtils;
+import com.fs.company.domain.CompanyKnowledgeAudit;
+import com.fs.company.domain.CompanyKnowledgeSuggestion;
+import com.fs.company.service.ICompanyKnowledgeAuditService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 知识审核Controller
+ */
+@RestController
+@RequestMapping("/knowledge")
+public class CompanyKnowledgeAuditController extends BaseController {
+
+    @Autowired
+    private ICompanyKnowledgeAuditService knowledgeAuditService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询待审核知识列表
+     */
+    @GetMapping("/audit/pending")
+    public AjaxResult listPending() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<CompanyKnowledgeAudit> list = knowledgeAuditService.listPendingAudit(loginUser.getCompany().getCompanyId());
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 查询已审核记录
+     */
+    @GetMapping("/audit/audited")
+    public AjaxResult listAudited() {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<CompanyKnowledgeAudit> list = knowledgeAuditService.listAudited(loginUser.getCompany().getCompanyId());
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 查询优化建议
+     */
+    @GetMapping("/suggestion/list")
+    public AjaxResult listSuggestions(@RequestParam(required = false) Integer status,
+                                      @RequestParam(required = false) String type) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<CompanyKnowledgeSuggestion> list = knowledgeAuditService.listSuggestions(
+            loginUser.getCompany().getCompanyId(), status, type);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 通过审核
+     */
+    @PostMapping("/audit/{id}/approve")
+    public AjaxResult approve(@PathVariable Long id,
+                             @RequestParam(required = false) String comment) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return knowledgeAuditService.approveKnowledge(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), id, comment);
+    }
+
+    /**
+     * 驳回审核
+     */
+    @PostMapping("/audit/{id}/reject")
+    public AjaxResult reject(@PathVariable Long id,
+                            @RequestParam(required = false) String comment) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return knowledgeAuditService.rejectKnowledge(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), id, comment);
+    }
+
+    /**
+     * 批量审核
+     */
+    @PostMapping("/audit/batch")
+    public AjaxResult batchAudit(@RequestBody Map<String, Object> params) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        @SuppressWarnings("unchecked")
+        List<Long> ids = (List<Long>) params.get("ids");
+        String auditResult = (String) params.get("auditResult");
+        return knowledgeAuditService.batchAudit(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), ids, auditResult);
+    }
+
+    /**
+     * 应用优化建议
+     */
+    @PostMapping("/suggestion/{id}/apply")
+    public AjaxResult applySuggestion(@PathVariable Long id) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return knowledgeAuditService.applySuggestion(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), id);
+    }
+
+    /**
+     * 忽略优化建议
+     */
+    @PostMapping("/suggestion/{id}/ignore")
+    public AjaxResult ignoreSuggestion(@PathVariable Long id) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return knowledgeAuditService.ignoreSuggestion(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), id);
+    }
+}

+ 132 - 0
fs-company/src/main/java/com/fs/company/controller/knowledge/CompanyKnowledgeBaseController.java

@@ -0,0 +1,132 @@
+package com.fs.company.controller.knowledge;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.ServletUtils;
+import com.fs.company.domain.CompanyKnowledgeBase;
+import com.fs.company.service.ICompanyKnowledgeBaseService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 知识库管理Controller
+ */
+@RestController
+@RequestMapping("/knowledge")
+public class CompanyKnowledgeBaseController extends BaseController {
+
+    @Autowired
+    private ICompanyKnowledgeBaseService knowledgeBaseService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询知识库列表
+     */
+    @GetMapping("/base/list")
+    public AjaxResult list(@RequestParam(required = false) String keyword,
+                           @RequestParam(required = false) String industryType,
+                           @RequestParam(required = false) Integer auditStatus) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<CompanyKnowledgeBase> list = knowledgeBaseService.listKnowledge(
+            loginUser.getCompany().getCompanyId(), keyword, industryType, auditStatus);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取知识详情
+     */
+    @GetMapping("/base/{id}")
+    public AjaxResult getById(@PathVariable Long id) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return AjaxResult.success(knowledgeBaseService.getKnowledgeById(loginUser.getCompany().getCompanyId(), id));
+    }
+
+    /**
+     * 新增知识
+     */
+    @PostMapping("/base")
+    public AjaxResult add(@RequestBody CompanyKnowledgeBase knowledge) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return knowledgeBaseService.addKnowledge(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), knowledge);
+    }
+
+    /**
+     * 修改知识
+     */
+    @PutMapping("/base/{id}")
+    public AjaxResult update(@PathVariable Long id, @RequestBody CompanyKnowledgeBase knowledge) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        knowledge.setId(id);
+        return knowledgeBaseService.updateKnowledge(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), knowledge);
+    }
+
+    /**
+     * 删除知识
+     */
+    @DeleteMapping("/base/{id}")
+    public AjaxResult delete(@PathVariable Long id) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return knowledgeBaseService.deleteKnowledge(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), id);
+    }
+
+    /**
+     * 审核知识
+     */
+    @PostMapping("/base/{id}/audit")
+    public AjaxResult audit(@PathVariable Long id,
+                           @RequestParam Integer auditStatus,
+                           @RequestParam(required = false) String comment) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return knowledgeBaseService.auditKnowledge(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), id, auditStatus, comment);
+    }
+
+    /**
+     * 同步到FastGPT
+     */
+    @PostMapping("/base/{id}/sync-fastgpt")
+    public AjaxResult syncToFastGpt(@PathVariable Long id) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return knowledgeBaseService.syncToFastGpt(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), id);
+    }
+
+    /**
+     * 从聊天记录提取知识
+     */
+    @PostMapping("/base/extract-from-chat")
+    public AjaxResult extractFromChat(@RequestParam Long chatRecordId,
+                                      @RequestParam String question,
+                                      @RequestParam String answer) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return knowledgeBaseService.extractFromChat(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), chatRecordId, question, answer);
+    }
+
+    /**
+     * 双知识库校验
+     */
+    @PostMapping("/base/dual-validation")
+    public AjaxResult dualValidation(@RequestParam String query,
+                                     @RequestParam String fastgptResult) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Map<String, Object> result = knowledgeBaseService.dualValidation(loginUser.getCompany().getCompanyId(), query, fastgptResult);
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 搜索知识
+     */
+    @GetMapping("/base/search")
+    public AjaxResult search(@RequestParam String keyword,
+                            @RequestParam(required = false) String industryType) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<CompanyKnowledgeBase> list = knowledgeBaseService.searchKnowledge(
+            loginUser.getCompany().getCompanyId(), keyword, industryType);
+        return AjaxResult.success(list);
+    }
+}

+ 106 - 0
fs-company/src/main/java/com/fs/company/controller/tag/CompanyTagTemplateBindingController.java

@@ -0,0 +1,106 @@
+package com.fs.company.controller.tag;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.ServletUtils;
+import com.fs.company.domain.CompanyTagTemplateBinding;
+import com.fs.company.service.ICompanyTagTemplateBindingService;
+import com.fs.framework.security.LoginUser;
+import com.fs.framework.service.TokenService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 标签模板绑定Controller
+ */
+@RestController
+@RequestMapping("/workflow")
+public class CompanyTagTemplateBindingController extends BaseController {
+
+    @Autowired
+    private ICompanyTagTemplateBindingService tagTemplateBindingService;
+
+    @Autowired
+    private TokenService tokenService;
+
+    /**
+     * 查询标签绑定列表
+     */
+    @GetMapping("/tag-binding/list")
+    public AjaxResult list(@RequestParam(required = false) String tagCode,
+                          @RequestParam(required = false) Long templateId) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        List<CompanyTagTemplateBinding> list = tagTemplateBindingService.listBinding(
+            loginUser.getCompany().getCompanyId(), tagCode, templateId);
+        return AjaxResult.success(list);
+    }
+
+    /**
+     * 获取绑定详情
+     */
+    @GetMapping("/tag-binding/{id}")
+    public AjaxResult getById(@PathVariable Long id) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return AjaxResult.success(tagTemplateBindingService.getBindingById(loginUser.getCompany().getCompanyId(), id));
+    }
+
+    /**
+     * 新增绑定
+     */
+    @PostMapping("/tag-binding")
+    public AjaxResult add(@RequestBody CompanyTagTemplateBinding binding) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return tagTemplateBindingService.addBinding(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), binding);
+    }
+
+    /**
+     * 修改绑定
+     */
+    @PutMapping("/tag-binding/{id}")
+    public AjaxResult update(@PathVariable Long id, @RequestBody CompanyTagTemplateBinding binding) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        binding.setId(id);
+        return tagTemplateBindingService.updateBinding(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), binding);
+    }
+
+    /**
+     * 删除绑定
+     */
+    @DeleteMapping("/tag-binding/{id}")
+    public AjaxResult delete(@PathVariable Long id) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return tagTemplateBindingService.deleteBinding(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), id);
+    }
+
+    /**
+     * 批量绑定
+     */
+    @PostMapping("/tag-binding/batch-bind/{templateId}")
+    public AjaxResult batchBind(@PathVariable Long templateId, @RequestBody List<String> tagCodes) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        return tagTemplateBindingService.batchBind(loginUser.getCompany().getCompanyId(), loginUser.getUsername(), templateId, tagCodes);
+    }
+
+    /**
+     * 根据用户标签匹配模板
+     */
+    @PostMapping("/tag-binding/match-template")
+    public AjaxResult matchTemplate(@RequestBody List<String> userTags) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Map<String, Object> result = tagTemplateBindingService.matchTemplate(loginUser.getCompany().getCompanyId(), userTags);
+        return AjaxResult.success(result);
+    }
+
+    /**
+     * 测试标签匹配
+     */
+    @PostMapping("/tag-binding/{id}/test-match")
+    public AjaxResult testMatch(@PathVariable Long id, @RequestBody Map<String, Object> testTags) {
+        LoginUser loginUser = tokenService.getLoginUser(ServletUtils.getRequest());
+        Map<String, Object> result = tagTemplateBindingService.testMatch(loginUser.getCompany().getCompanyId(), id, testTags);
+        return AjaxResult.success(result);
+    }
+}

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

+ 52 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyKnowledgeAudit.java

@@ -0,0 +1,52 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 知识审核记录表
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanyKnowledgeAudit extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long companyId;
+
+    /** 关联知识ID */
+    private Long knowledgeId;
+
+    /** 来源类型:chat-聊天记录, ai-AI生成 */
+    private String sourceType;
+
+    /** 来源记录ID */
+    private String sourceId;
+
+    /** 内容摘要 */
+    private String content;
+
+    /** AI优化建议 */
+    private String suggestion;
+
+    /** 审核结果:approve-通过, reject-驳回 */
+    private String auditResult;
+
+    /** 审核意见 */
+    private String auditComment;
+
+    /** 审核人 */
+    private String auditor;
+
+    /** 审核时间 */
+    private String auditTime;
+
+    /** 删除标志 0正常 1删除 */
+    private Integer delFlag;
+}

+ 64 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyKnowledgeBase.java

@@ -0,0 +1,64 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 企业知识库基础表
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanyKnowledgeBase extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long companyId;
+
+    /** 知识标题 */
+    private String title;
+
+    /** 问题 */
+    private String question;
+
+    /** 答案/解答 */
+    private String answer;
+
+    /** 行业类型 */
+    private String industryType;
+
+    /** 来源:manual-手动录入, chat-聊天记录, ai-AI生成 */
+    private String source;
+
+    /** 审核状态:0-待审核, 1-已通过, 2-已驳回 */
+    private Integer auditStatus;
+
+    /** 审核意见 */
+    private String auditComment;
+
+    /** 审核人 */
+    private String auditor;
+
+    /** 审核时间 */
+    private String auditTime;
+
+    /** 使用次数 */
+    private Integer useCount;
+
+    /** FastGPT中的知识ID */
+    private String fastgptId;
+
+    /** 同步状态:0-未同步, 1-已同步, 2-同步失败 */
+    private Integer syncStatus;
+
+    /** 同步时间 */
+    private String syncTime;
+
+    /** 删除标志 0正常 1删除 */
+    private Integer delFlag;
+}

+ 43 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyKnowledgeSuggestion.java

@@ -0,0 +1,43 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 知识优化建议表
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanyKnowledgeSuggestion extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long companyId;
+
+    /** 优化类型:accuracy-精准度, completeness-完整性, clarity-清晰度 */
+    private String type;
+
+    /** 问题描述 */
+    private String description;
+
+    /** 优化建议 */
+    private String suggestion;
+
+    /** 影响范围:low-低, medium-中, high-高 */
+    private String impact;
+
+    /** 状态:0-待处理, 1-已应用, 2-已忽略 */
+    private Integer status;
+
+    /** 应用时间 */
+    private String applyTime;
+
+    /** 删除标志 0正常 1删除 */
+    private Integer delFlag;
+}

+ 46 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyTagTemplateBinding.java

@@ -0,0 +1,46 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 标签-模板绑定表
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanyTagTemplateBinding extends BaseEntity {
+
+    private static final long serialVersionUID = 1L;
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long companyId;
+
+    /** 标签编码 */
+    private String tagCode;
+
+    /** 标签名称 */
+    private String tagName;
+
+    /** 绑定的工作流模板ID */
+    private Long templateId;
+
+    /** 模板名称(冗余) */
+    private String templateName;
+
+    /** 优先级(数值越大优先级越高) */
+    private Integer priority;
+
+    /** 匹配条件(JSON格式) */
+    private String matchCondition;
+
+    /** 状态:0-禁用, 1-启用 */
+    private Integer status;
+
+    /** 删除标志 0正常 1删除 */
+    private Integer delFlag;
+}

+ 40 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowLobster.java

@@ -0,0 +1,40 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 工作流龙虾模板
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanyWorkflowLobster extends BaseEntity {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long companyId;
+
+    private String templateCode;
+
+    private String templateName;
+
+    private String industryType;
+
+    private String description;
+
+    /**
+     * 0草稿 1发布 2停用
+     */
+    private Integer status;
+
+    private Integer version;
+
+    private Integer delFlag;
+
+    private String canvasData;
+
+}

+ 40 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowLobsterEdge.java

@@ -0,0 +1,40 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 工作流龙虾连线
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanyWorkflowLobsterEdge extends BaseEntity {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long workflowId;
+
+    private String edgeKey;
+
+    private String sourceNodeCode;
+
+    private String targetNodeCode;
+
+    private String sourcePort;
+
+    private String targetPort;
+
+    private String edgeLabel;
+
+    private String edgeColor;
+
+    private String conditionExpr;
+
+    private Integer sortNo;
+
+    private Integer delFlag;
+}

+ 40 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowLobsterNode.java

@@ -0,0 +1,40 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 工作流龙虾节点
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanyWorkflowLobsterNode extends BaseEntity {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long workflowId;
+
+    private String nodeCode;
+
+    private String nodeName;
+
+    private Integer nodeType;
+
+    private Integer sortNo;
+
+    private String nextNodeCode;
+
+    private String messageTemplate;
+
+    private String conditionExpr;
+
+    private String nodeConfig;
+
+    private String greetingConfig;
+
+    private Integer delFlag;
+}

+ 37 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowLobsterRecord.java

@@ -0,0 +1,37 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 工作流龙虾AI生成记录
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanyWorkflowLobsterRecord extends BaseEntity {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long companyId;
+
+    private String recordNo;
+
+    private String requirement;
+
+    private String selectedApiIds;
+
+    /**
+     * 0生成中 1成功 2失败 3已确认
+     */
+    private Integer status;
+
+    private String errorMsg;
+
+    private String resultJson;
+
+    private Integer delFlag;
+}

+ 36 - 0
fs-service/src/main/java/com/fs/company/domain/CompanyWorkflowLobsterVariable.java

@@ -0,0 +1,36 @@
+package com.fs.company.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fs.common.core.domain.BaseEntity;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+/**
+ * 工作流龙虾变量
+ */
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanyWorkflowLobsterVariable extends BaseEntity {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private Long workflowId;
+
+    private String varCode;
+
+    private String varName;
+
+    private String varType;
+
+    private String sourceType;
+
+    private Integer required;
+
+    private String defaultValue;
+
+    private String description;
+
+    private Integer delFlag;
+}

+ 25 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyKnowledgeAuditMapper.java

@@ -0,0 +1,25 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanyKnowledgeAudit;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface CompanyKnowledgeAuditMapper extends BaseMapper<CompanyKnowledgeAudit> {
+    
+    List<CompanyKnowledgeAudit> selectAuditList(@Param("companyId") Long companyId,
+                                                 @Param("auditResult") String auditResult);
+    
+    int insertAudit(CompanyKnowledgeAudit entity);
+    
+    CompanyKnowledgeAudit selectAuditByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId);
+    
+    int updateAuditById(CompanyKnowledgeAudit entity);
+    
+    int logicalDeleteById(@Param("id") Long id, @Param("companyId") Long companyId);
+    
+    int batchAudit(@Param("ids") List<Long> ids, 
+                   @Param("auditResult") String auditResult,
+                   @Param("auditor") String auditor);
+}

+ 25 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyKnowledgeBaseMapper.java

@@ -0,0 +1,25 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanyKnowledgeBase;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface CompanyKnowledgeBaseMapper extends BaseMapper<CompanyKnowledgeBase> {
+    
+    List<CompanyKnowledgeBase> selectKnowledgeList(@Param("companyId") Long companyId, 
+                                                    @Param("keyword") String keyword,
+                                                    @Param("industryType") String industryType,
+                                                    @Param("auditStatus") Integer auditStatus);
+    
+    int insertKnowledge(CompanyKnowledgeBase entity);
+    
+    CompanyKnowledgeBase selectKnowledgeByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId);
+    
+    int updateKnowledgeById(CompanyKnowledgeBase entity);
+    
+    int logicalDeleteById(@Param("id") Long id, @Param("companyId") Long companyId);
+    
+    int incrementUseCount(@Param("id") Long id);
+}

+ 24 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyKnowledgeSuggestionMapper.java

@@ -0,0 +1,24 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanyKnowledgeSuggestion;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface CompanyKnowledgeSuggestionMapper extends BaseMapper<CompanyKnowledgeSuggestion> {
+    
+    List<CompanyKnowledgeSuggestion> selectSuggestionList(@Param("companyId") Long companyId,
+                                                           @Param("status") Integer status,
+                                                           @Param("type") String type);
+    
+    int insertSuggestion(CompanyKnowledgeSuggestion entity);
+    
+    CompanyKnowledgeSuggestion selectSuggestionByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId);
+    
+    int updateSuggestionById(CompanyKnowledgeSuggestion entity);
+    
+    int logicalDeleteById(@Param("id") Long id, @Param("companyId") Long companyId);
+    
+    int batchApply(@Param("ids") List<Long> ids);
+}

+ 29 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyTagTemplateBindingMapper.java

@@ -0,0 +1,29 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanyTagTemplateBinding;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface CompanyTagTemplateBindingMapper extends BaseMapper<CompanyTagTemplateBinding> {
+    
+    List<CompanyTagTemplateBinding> selectBindingList(@Param("companyId") Long companyId,
+                                                       @Param("tagCode") String tagCode,
+                                                       @Param("templateId") Long templateId);
+    
+    int insertBinding(CompanyTagTemplateBinding entity);
+    
+    CompanyTagTemplateBinding selectBindingByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId);
+    
+    int updateBindingById(CompanyTagTemplateBinding entity);
+    
+    int logicalDeleteById(@Param("id") Long id, @Param("companyId") Long companyId);
+    
+    List<CompanyTagTemplateBinding> selectMatchedTemplates(@Param("companyId") Long companyId,
+                                                            @Param("tagCodes") List<String> tagCodes);
+    
+    int batchBind(@Param("companyId") Long companyId,
+                  @Param("templateId") Long templateId,
+                  @Param("tagCodes") List<String> tagCodes);
+}

+ 15 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowLobsterEdgeMapper.java

@@ -0,0 +1,15 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanyWorkflowLobsterEdge;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface CompanyWorkflowLobsterEdgeMapper extends BaseMapper<CompanyWorkflowLobsterEdge> {
+    int batchInsert(@Param("list") List<CompanyWorkflowLobsterEdge> list);
+
+    int deleteByWorkflowId(@Param("workflowId") Long workflowId);
+
+    List<CompanyWorkflowLobsterEdge> selectByWorkflowId(@Param("workflowId") Long workflowId);
+}

+ 19 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowLobsterMapper.java

@@ -0,0 +1,19 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanyWorkflowLobster;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface CompanyWorkflowLobsterMapper extends BaseMapper<CompanyWorkflowLobster> {
+    List<CompanyWorkflowLobster> selectTemplateList(@Param("companyId") Long companyId);
+
+    int insertTemplate(CompanyWorkflowLobster entity);
+
+    CompanyWorkflowLobster selectTemplateByIdAndCompanyId(@Param("id") Long id, @Param("companyId") Long companyId);
+
+    int updateTemplateById(CompanyWorkflowLobster entity);
+
+    int logicalDeleteById(@Param("id") Long id, @Param("companyId") Long companyId);
+}

+ 15 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowLobsterNodeMapper.java

@@ -0,0 +1,15 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanyWorkflowLobsterNode;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface CompanyWorkflowLobsterNodeMapper extends BaseMapper<CompanyWorkflowLobsterNode> {
+    int batchInsert(@Param("list") List<CompanyWorkflowLobsterNode> list);
+
+    int deleteByWorkflowId(@Param("workflowId") Long workflowId);
+
+    List<CompanyWorkflowLobsterNode> selectByWorkflowId(@Param("workflowId") Long workflowId);
+}

+ 13 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowLobsterRecordMapper.java

@@ -0,0 +1,13 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanyWorkflowLobsterRecord;
+import org.apache.ibatis.annotations.Param;
+
+public interface CompanyWorkflowLobsterRecordMapper extends BaseMapper<CompanyWorkflowLobsterRecord> {
+    int insertRecord(CompanyWorkflowLobsterRecord record);
+
+    CompanyWorkflowLobsterRecord selectByRecordNo(@Param("recordNo") String recordNo, @Param("companyId") Long companyId);
+
+    int updateRecord(CompanyWorkflowLobsterRecord record);
+}

+ 15 - 0
fs-service/src/main/java/com/fs/company/mapper/CompanyWorkflowLobsterVariableMapper.java

@@ -0,0 +1,15 @@
+package com.fs.company.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.company.domain.CompanyWorkflowLobsterVariable;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+public interface CompanyWorkflowLobsterVariableMapper extends BaseMapper<CompanyWorkflowLobsterVariable> {
+    int batchInsert(@Param("list") List<CompanyWorkflowLobsterVariable> list);
+
+    int deleteByWorkflowId(@Param("workflowId") Long workflowId);
+
+    List<CompanyWorkflowLobsterVariable> selectByWorkflowId(@Param("workflowId") Long workflowId);
+}

+ 29 - 0
fs-service/src/main/java/com/fs/company/param/CompanyWorkflowLobsterCanvasParam.java

@@ -0,0 +1,29 @@
+package com.fs.company.param;
+
+import com.fs.company.domain.CompanyWorkflowLobsterNode;
+import com.fs.company.domain.CompanyWorkflowLobsterEdge;
+import com.fs.company.domain.CompanyWorkflowLobsterVariable;
+import lombok.Data;
+
+import java.util.List;
+
+/**
+ * 工作流画布保存参数
+ */
+@Data
+public class CompanyWorkflowLobsterCanvasParam {
+
+    private String templateName;
+
+    private String industryType;
+
+    private String description;
+
+    private String canvasData;
+
+    private List<CompanyWorkflowLobsterVariable> variables;
+
+    private List<CompanyWorkflowLobsterNode> nodes;
+
+    private List<CompanyWorkflowLobsterEdge> edges;
+}

+ 18 - 0
fs-service/src/main/java/com/fs/company/param/CompanyWorkflowLobsterConfirmParam.java

@@ -0,0 +1,18 @@
+package com.fs.company.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class CompanyWorkflowLobsterConfirmParam implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String templateName;
+    private String industryType;
+    private String description;
+    private List<CompanyWorkflowLobsterVariableParam> variables;
+    private List<CompanyWorkflowLobsterNodeParam> nodes;
+    private List<CompanyWorkflowLobsterEdgeParam> edges; // 添加连线数据
+}

+ 40 - 0
fs-service/src/main/java/com/fs/company/param/CompanyWorkflowLobsterEdgeParam.java

@@ -0,0 +1,40 @@
+package com.fs.company.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 工作流连线参数
+ */
+@Data
+public class CompanyWorkflowLobsterEdgeParam implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    /** 连线唯一标识 */
+    private String edgeKey;
+    
+    /** 源节点编码 */
+    private String sourceNodeCode;
+    
+    /** 目标节点编码 */
+    private String targetNodeCode;
+    
+    /** 源连接点位置(top/right/bottom/left) */
+    private String sourcePort;
+    
+    /** 目标连接点位置(top/right/bottom/left) */
+    private String targetPort;
+    
+    /** 连线标签 */
+    private String edgeLabel;
+    
+    /** 连线颜色 */
+    private String edgeColor;
+    
+    /** 条件表达式 */
+    private String conditionExpr;
+    
+    /** 排序号 */
+    private Integer sortNo;
+}

+ 15 - 0
fs-service/src/main/java/com/fs/company/param/CompanyWorkflowLobsterGenerateParam.java

@@ -0,0 +1,15 @@
+package com.fs.company.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.List;
+
+@Data
+public class CompanyWorkflowLobsterGenerateParam implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String requirement;
+
+    private List<Long> selectedApiIds;
+}

+ 20 - 0
fs-service/src/main/java/com/fs/company/param/CompanyWorkflowLobsterNodeParam.java

@@ -0,0 +1,20 @@
+package com.fs.company.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class CompanyWorkflowLobsterNodeParam implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String nodeCode;
+    private String nodeName;
+    private Integer nodeType;
+    private Integer sortNo;
+    private String nextNodeCode;
+    private String messageTemplate;
+    private String conditionExpr;
+    private String nodeConfig;
+    private String greetingConfig;
+}

+ 18 - 0
fs-service/src/main/java/com/fs/company/param/CompanyWorkflowLobsterVariableParam.java

@@ -0,0 +1,18 @@
+package com.fs.company.param;
+
+import lombok.Data;
+
+import java.io.Serializable;
+
+@Data
+public class CompanyWorkflowLobsterVariableParam implements Serializable {
+    private static final long serialVersionUID = 1L;
+
+    private String varCode;
+    private String varName;
+    private String varType;
+    private String sourceType;
+    private Integer required;
+    private String defaultValue;
+    private String description;
+}

+ 50 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyKnowledgeAuditService.java

@@ -0,0 +1,50 @@
+package com.fs.company.service;
+
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.company.domain.CompanyKnowledgeAudit;
+import com.fs.company.domain.CompanyKnowledgeSuggestion;
+
+import java.util.List;
+
+public interface ICompanyKnowledgeAuditService {
+    
+    /**
+     * 查询待审核知识列表
+     */
+    List<CompanyKnowledgeAudit> listPendingAudit(Long companyId);
+    
+    /**
+     * 查询已审核记录
+     */
+    List<CompanyKnowledgeAudit> listAudited(Long companyId);
+    
+    /**
+     * 查询优化建议
+     */
+    List<CompanyKnowledgeSuggestion> listSuggestions(Long companyId, Integer status, String type);
+    
+    /**
+     * 审核知识
+     */
+    AjaxResult approveKnowledge(Long companyId, String userName, Long id, String comment);
+    
+    /**
+     * 驳回知识
+     */
+    AjaxResult rejectKnowledge(Long companyId, String userName, Long id, String comment);
+    
+    /**
+     * 批量审核
+     */
+    AjaxResult batchAudit(Long companyId, String userName, List<Long> ids, String auditResult);
+    
+    /**
+     * 应用优化建议
+     */
+    AjaxResult applySuggestion(Long companyId, String userName, Long id);
+    
+    /**
+     * 忽略优化建议
+     */
+    AjaxResult ignoreSuggestion(Long companyId, String userName, Long id);
+}

+ 60 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyKnowledgeBaseService.java

@@ -0,0 +1,60 @@
+package com.fs.company.service;
+
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.company.domain.CompanyKnowledgeBase;
+
+import java.util.List;
+import java.util.Map;
+
+public interface ICompanyKnowledgeBaseService {
+    
+    /**
+     * 查询知识库列表
+     */
+    List<CompanyKnowledgeBase> listKnowledge(Long companyId, String keyword, String industryType, Integer auditStatus);
+    
+    /**
+     * 根据ID查询知识库
+     */
+    CompanyKnowledgeBase getKnowledgeById(Long companyId, Long id);
+    
+    /**
+     * 新增知识
+     */
+    AjaxResult addKnowledge(Long companyId, String userName, CompanyKnowledgeBase knowledge);
+    
+    /**
+     * 修改知识
+     */
+    AjaxResult updateKnowledge(Long companyId, String userName, CompanyKnowledgeBase knowledge);
+    
+    /**
+     * 删除知识
+     */
+    AjaxResult deleteKnowledge(Long companyId, String userName, Long id);
+    
+    /**
+     * 审核知识
+     */
+    AjaxResult auditKnowledge(Long companyId, String userName, Long id, Integer auditStatus, String comment);
+    
+    /**
+     * 同步到FastGPT
+     */
+    AjaxResult syncToFastGpt(Long companyId, String userName, Long id);
+    
+    /**
+     * 从聊天记录提取知识
+     */
+    AjaxResult extractFromChat(Long companyId, String userName, Long chatRecordId, String question, String answer);
+    
+    /**
+     * 双知识库校验
+     */
+    Map<String, Object> dualValidation(Long companyId, String query, String fastgptResult);
+    
+    /**
+     * 搜索知识
+     */
+    List<CompanyKnowledgeBase> searchKnowledge(Long companyId, String keyword, String industryType);
+}

+ 50 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyTagTemplateBindingService.java

@@ -0,0 +1,50 @@
+package com.fs.company.service;
+
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.company.domain.CompanyTagTemplateBinding;
+
+import java.util.List;
+import java.util.Map;
+
+public interface ICompanyTagTemplateBindingService {
+    
+    /**
+     * 查询标签绑定列表
+     */
+    List<CompanyTagTemplateBinding> listBinding(Long companyId, String tagCode, Long templateId);
+    
+    /**
+     * 根据ID查询绑定
+     */
+    CompanyTagTemplateBinding getBindingById(Long companyId, Long id);
+    
+    /**
+     * 新增绑定
+     */
+    AjaxResult addBinding(Long companyId, String userName, CompanyTagTemplateBinding binding);
+    
+    /**
+     * 修改绑定
+     */
+    AjaxResult updateBinding(Long companyId, String userName, CompanyTagTemplateBinding binding);
+    
+    /**
+     * 删除绑定
+     */
+    AjaxResult deleteBinding(Long companyId, String userName, Long id);
+    
+    /**
+     * 批量绑定
+     */
+    AjaxResult batchBind(Long companyId, String userName, Long templateId, List<String> tagCodes);
+    
+    /**
+     * 根据用户标签匹配模板
+     */
+    Map<String, Object> matchTemplate(Long companyId, List<String> userTags);
+    
+    /**
+     * 测试标签匹配
+     */
+    Map<String, Object> testMatch(Long companyId, Long id, Map<String, Object> testTags);
+}

+ 62 - 0
fs-service/src/main/java/com/fs/company/service/ICompanyWorkflowLobsterService.java

@@ -0,0 +1,62 @@
+package com.fs.company.service;
+
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.company.domain.CompanyWorkflowLobster;
+import com.fs.company.param.CompanyWorkflowLobsterCanvasParam;
+import com.fs.company.param.CompanyWorkflowLobsterConfirmParam;
+import com.fs.company.param.CompanyWorkflowLobsterGenerateParam;
+
+import java.util.List;
+import java.util.Map;
+
+public interface ICompanyWorkflowLobsterService {
+    /**
+     * 分页查询模板列表
+     */
+    List<CompanyWorkflowLobster> listTemplate(Long companyId, Integer page, Integer size);
+
+    /**
+     * AI生成工作流
+     */
+    String generate(Long companyId, String userName, CompanyWorkflowLobsterGenerateParam param);
+
+    /**
+     * 获取生成结果详情
+     */
+    Map<String, Object> getResultDetail(Long companyId, String recordId);
+
+    /**
+     * 确认保存原始生成结果
+     */
+    AjaxResult confirmRaw(Long companyId, String userName, String recordId);
+
+    /**
+     * 确认保存编辑后的结果
+     */
+    AjaxResult confirmEdited(Long companyId, String userName, String recordId, CompanyWorkflowLobsterConfirmParam param);
+
+    /**
+     * 预览模板
+     */
+    Map<String, Object> previewTemplate(Long companyId, Long templateId);
+
+    /**
+     * 删除模板
+     */
+    AjaxResult deleteTemplate(Long companyId, String userName, Long templateId);
+
+    /**
+     * 获取模板详情(用于画布编辑)
+     */
+    Map<String, Object> getTemplate(Long companyId, Long templateId);
+
+    /**
+     * 更新模板基本信息
+     */
+    AjaxResult updateTemplate(Long companyId, String userName, Long templateId, CompanyWorkflowLobsterConfirmParam param);
+
+    /**
+     * 保存画布数据(包含节点位置、连线等可视化信息)
+     */
+    AjaxResult saveCanvas(Long companyId, String userName, Long templateId, CompanyWorkflowLobsterCanvasParam param);
+}

+ 126 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyKnowledgeAuditServiceImpl.java

@@ -0,0 +1,126 @@
+package com.fs.company.service.impl;
+
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.DateUtils;
+import com.fs.company.domain.CompanyKnowledgeAudit;
+import com.fs.company.domain.CompanyKnowledgeSuggestion;
+import com.fs.company.mapper.CompanyKnowledgeAuditMapper;
+import com.fs.company.mapper.CompanyKnowledgeSuggestionMapper;
+import com.fs.company.service.ICompanyKnowledgeAuditService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+
+/**
+ * 知识审核Service实现
+ */
+@Service
+public class CompanyKnowledgeAuditServiceImpl implements ICompanyKnowledgeAuditService {
+
+    @Autowired
+    private CompanyKnowledgeAuditMapper knowledgeAuditMapper;
+
+    @Autowired
+    private CompanyKnowledgeSuggestionMapper knowledgeSuggestionMapper;
+
+    @Override
+    public List<CompanyKnowledgeAudit> listPendingAudit(Long companyId) {
+        return knowledgeAuditMapper.selectAuditList(companyId, null);
+    }
+
+    @Override
+    public List<CompanyKnowledgeAudit> listAudited(Long companyId) {
+        return knowledgeAuditMapper.selectAuditList(companyId, "approve");
+    }
+
+    @Override
+    public List<CompanyKnowledgeSuggestion> listSuggestions(Long companyId, Integer status, String type) {
+        return knowledgeSuggestionMapper.selectSuggestionList(companyId, status, type);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult approveKnowledge(Long companyId, String userName, Long id, String comment) {
+        CompanyKnowledgeAudit audit = knowledgeAuditMapper.selectAuditByIdAndCompanyId(id, companyId);
+        if (audit == null) {
+            return AjaxResult.error("审核记录不存在");
+        }
+
+        audit.setAuditResult("approve");
+        audit.setAuditComment(comment);
+        audit.setAuditor(userName);
+        audit.setAuditTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+        audit.setUpdateBy(userName);
+        audit.setUpdateTime(DateUtils.getNowDate());
+
+        int result = knowledgeAuditMapper.updateAuditById(audit);
+        return result > 0 ? AjaxResult.success("审核通过") : AjaxResult.error("审核失败");
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult rejectKnowledge(Long companyId, String userName, Long id, String comment) {
+        CompanyKnowledgeAudit audit = knowledgeAuditMapper.selectAuditByIdAndCompanyId(id, companyId);
+        if (audit == null) {
+            return AjaxResult.error("审核记录不存在");
+        }
+
+        audit.setAuditResult("reject");
+        audit.setAuditComment(comment);
+        audit.setAuditor(userName);
+        audit.setAuditTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+        audit.setUpdateBy(userName);
+        audit.setUpdateTime(DateUtils.getNowDate());
+
+        int result = knowledgeAuditMapper.updateAuditById(audit);
+        return result > 0 ? AjaxResult.success("已驳回") : AjaxResult.error("操作失败");
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult batchAudit(Long companyId, String userName, List<Long> ids, String auditResult) {
+        if (ids == null || ids.isEmpty()) {
+            return AjaxResult.error("请选择要审核的数据");
+        }
+
+        int result = knowledgeAuditMapper.batchAudit(ids, auditResult, userName);
+        return result > 0 ? AjaxResult.success("批量审核成功") : AjaxResult.error("批量审核失败");
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult applySuggestion(Long companyId, String userName, Long id) {
+        CompanyKnowledgeSuggestion suggestion = knowledgeSuggestionMapper.selectSuggestionByIdAndCompanyId(id, companyId);
+        if (suggestion == null) {
+            return AjaxResult.error("优化建议不存在");
+        }
+
+        suggestion.setStatus(1);
+        suggestion.setApplyTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+        suggestion.setUpdateBy(userName);
+        suggestion.setUpdateTime(DateUtils.getNowDate());
+
+        int result = knowledgeSuggestionMapper.updateSuggestionById(suggestion);
+        return result > 0 ? AjaxResult.success("应用成功") : AjaxResult.error("应用失败");
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult ignoreSuggestion(Long companyId, String userName, Long id) {
+        CompanyKnowledgeSuggestion suggestion = knowledgeSuggestionMapper.selectSuggestionByIdAndCompanyId(id, companyId);
+        if (suggestion == null) {
+            return AjaxResult.error("优化建议不存在");
+        }
+
+        suggestion.setStatus(2);
+        suggestion.setUpdateBy(userName);
+        suggestion.setUpdateTime(DateUtils.getNowDate());
+
+        int result = knowledgeSuggestionMapper.updateSuggestionById(suggestion);
+        return result > 0 ? AjaxResult.success("已忽略") : AjaxResult.error("操作失败");
+    }
+}

+ 165 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyKnowledgeBaseServiceImpl.java

@@ -0,0 +1,165 @@
+package com.fs.company.service.impl;
+
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.DateUtils;
+import com.fs.company.domain.CompanyKnowledgeBase;
+import com.fs.company.mapper.CompanyKnowledgeBaseMapper;
+import com.fs.company.service.ICompanyKnowledgeBaseService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.time.format.DateTimeFormatter;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * 知识库基础Service实现
+ */
+@Service
+public class CompanyKnowledgeBaseServiceImpl implements ICompanyKnowledgeBaseService {
+
+    @Autowired
+    private CompanyKnowledgeBaseMapper knowledgeBaseMapper;
+
+    @Override
+    public List<CompanyKnowledgeBase> listKnowledge(Long companyId, String keyword, String industryType, Integer auditStatus) {
+        return knowledgeBaseMapper.selectKnowledgeList(companyId, keyword, industryType, auditStatus);
+    }
+
+    @Override
+    public CompanyKnowledgeBase getKnowledgeById(Long companyId, Long id) {
+        return knowledgeBaseMapper.selectKnowledgeByIdAndCompanyId(id, companyId);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult addKnowledge(Long companyId, String userName, CompanyKnowledgeBase knowledge) {
+        knowledge.setCompanyId(companyId);
+        knowledge.setAuditStatus(0); // 默认待审核
+        knowledge.setSource("manual"); // 手动录入
+        knowledge.setUseCount(0);
+        knowledge.setSyncStatus(0);
+        knowledge.setDelFlag(0);
+        knowledge.setCreateBy(userName);
+        knowledge.setCreateTime(DateUtils.getNowDate());
+        knowledge.setUpdateBy(userName);
+        knowledge.setUpdateTime(DateUtils.getNowDate());
+
+        int result = knowledgeBaseMapper.insertKnowledge(knowledge);
+        return result > 0 ? AjaxResult.success("新增成功") : AjaxResult.error("新增失败");
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult updateKnowledge(Long companyId, String userName, CompanyKnowledgeBase knowledge) {
+        CompanyKnowledgeBase exist = knowledgeBaseMapper.selectKnowledgeByIdAndCompanyId(knowledge.getId(), companyId);
+        if (exist == null) {
+            return AjaxResult.error("知识不存在");
+        }
+
+        knowledge.setUpdateBy(userName);
+        knowledge.setUpdateTime(DateUtils.getNowDate());
+
+        int result = knowledgeBaseMapper.updateKnowledgeById(knowledge);
+        return result > 0 ? AjaxResult.success("修改成功") : AjaxResult.error("修改失败");
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult deleteKnowledge(Long companyId, String userName, Long id) {
+        CompanyKnowledgeBase exist = knowledgeBaseMapper.selectKnowledgeByIdAndCompanyId(id, companyId);
+        if (exist == null) {
+            return AjaxResult.error("知识不存在");
+        }
+
+        int result = knowledgeBaseMapper.logicalDeleteById(id, companyId);
+        return result > 0 ? AjaxResult.success("删除成功") : AjaxResult.error("删除失败");
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult auditKnowledge(Long companyId, String userName, Long id, Integer auditStatus, String comment) {
+        CompanyKnowledgeBase knowledge = knowledgeBaseMapper.selectKnowledgeByIdAndCompanyId(id, companyId);
+        if (knowledge == null) {
+            return AjaxResult.error("知识不存在");
+        }
+
+        knowledge.setAuditStatus(auditStatus);
+        knowledge.setAuditComment(comment);
+        knowledge.setAuditor(userName);
+        knowledge.setAuditTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+        knowledge.setUpdateBy(userName);
+        knowledge.setUpdateTime(DateUtils.getNowDate());
+
+        int result = knowledgeBaseMapper.updateKnowledgeById(knowledge);
+        return result > 0 ? AjaxResult.success("审核成功") : AjaxResult.error("审核失败");
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult syncToFastGpt(Long companyId, String userName, Long id) {
+        CompanyKnowledgeBase knowledge = knowledgeBaseMapper.selectKnowledgeByIdAndCompanyId(id, companyId);
+        if (knowledge == null) {
+            return AjaxResult.error("知识不存在");
+        }
+
+        // TODO: 实现同步到FastGPT的逻辑
+        // 1. 调用FastGPT API创建知识库
+        // 2. 更新fastgpt_id和sync_status
+
+        knowledge.setSyncStatus(1);
+        knowledge.setSyncTime(LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
+        knowledge.setUpdateBy(userName);
+        knowledge.setUpdateTime(DateUtils.getNowDate());
+
+        int result = knowledgeBaseMapper.updateKnowledgeById(knowledge);
+        return result > 0 ? AjaxResult.success("同步成功") : AjaxResult.error("同步失败");
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult extractFromChat(Long companyId, String userName, Long chatRecordId, String question, String answer) {
+        CompanyKnowledgeBase knowledge = new CompanyKnowledgeBase();
+        knowledge.setCompanyId(companyId);
+        knowledge.setTitle(question.length() > 50 ? question.substring(0, 50) + "..." : question);
+        knowledge.setQuestion(question);
+        knowledge.setAnswer(answer);
+        knowledge.setSource("chat");
+        knowledge.setAuditStatus(0);
+        knowledge.setUseCount(0);
+        knowledge.setSyncStatus(0);
+        knowledge.setDelFlag(0);
+        knowledge.setCreateBy(userName);
+        knowledge.setCreateTime(DateUtils.getNowDate());
+        knowledge.setUpdateBy(userName);
+        knowledge.setUpdateTime(DateUtils.getNowDate());
+
+        int result = knowledgeBaseMapper.insertKnowledge(knowledge);
+        return result > 0 ? AjaxResult.success("提取成功", knowledge) : AjaxResult.error("提取失败");
+    }
+
+    @Override
+    public Map<String, Object> dualValidation(Long companyId, String query, String fastgptResult) {
+        // TODO: 实现双知识库校验逻辑
+        // 1. 在本地知识库中搜索相似问题
+        // 2. 对比FastGPT返回的结果
+        // 3. 返回校验结果
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("query", query);
+        result.put("fastgptResult", fastgptResult);
+        result.put("localResult", "待实现");
+        result.put("match", true);
+
+        return result;
+    }
+
+    @Override
+    public List<CompanyKnowledgeBase> searchKnowledge(Long companyId, String keyword, String industryType) {
+        return knowledgeBaseMapper.selectKnowledgeList(companyId, keyword, industryType, null);
+    }
+}
+

+ 148 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyTagTemplateBindingServiceImpl.java

@@ -0,0 +1,148 @@
+package com.fs.company.service.impl;
+
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.utils.DateUtils;
+import com.fs.company.domain.CompanyTagTemplateBinding;
+import com.fs.company.mapper.CompanyTagTemplateBindingMapper;
+import com.fs.company.service.ICompanyTagTemplateBindingService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.*;
+
+/**
+ * 标签模板绑定Service实现
+ */
+@Service
+public class CompanyTagTemplateBindingServiceImpl implements ICompanyTagTemplateBindingService {
+
+    @Autowired
+    private CompanyTagTemplateBindingMapper tagTemplateBindingMapper;
+
+    @Override
+    public List<CompanyTagTemplateBinding> listBinding(Long companyId, String tagCode, Long templateId) {
+        return tagTemplateBindingMapper.selectBindingList(companyId, tagCode, templateId);
+    }
+
+    @Override
+    public CompanyTagTemplateBinding getBindingById(Long companyId, Long id) {
+        return tagTemplateBindingMapper.selectBindingByIdAndCompanyId(id, companyId);
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult addBinding(Long companyId, String userName, CompanyTagTemplateBinding binding) {
+        binding.setCompanyId(companyId);
+        binding.setStatus(1);
+        binding.setDelFlag(0);
+        binding.setCreateBy(userName);
+        binding.setCreateTime(DateUtils.getNowDate());
+        binding.setUpdateBy(userName);
+        binding.setUpdateTime(DateUtils.getNowDate());
+
+        int result = tagTemplateBindingMapper.insertBinding(binding);
+        return result > 0 ? AjaxResult.success("新增成功") : AjaxResult.error("新增失败");
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult updateBinding(Long companyId, String userName, CompanyTagTemplateBinding binding) {
+        CompanyTagTemplateBinding exist = tagTemplateBindingMapper.selectBindingByIdAndCompanyId(binding.getId(), companyId);
+        if (exist == null) {
+            return AjaxResult.error("绑定关系不存在");
+        }
+
+        binding.setUpdateBy(userName);
+        binding.setUpdateTime(DateUtils.getNowDate());
+
+        int result = tagTemplateBindingMapper.updateBindingById(binding);
+        return result > 0 ? AjaxResult.success("修改成功") : AjaxResult.error("修改失败");
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult deleteBinding(Long companyId, String userName, Long id) {
+        CompanyTagTemplateBinding exist = tagTemplateBindingMapper.selectBindingByIdAndCompanyId(id, companyId);
+        if (exist == null) {
+            return AjaxResult.error("绑定关系不存在");
+        }
+
+        int result = tagTemplateBindingMapper.logicalDeleteById(id, companyId);
+        return result > 0 ? AjaxResult.success("删除成功") : AjaxResult.error("删除失败");
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult batchBind(Long companyId, String userName, Long templateId, List<String> tagCodes) {
+        if (tagCodes == null || tagCodes.isEmpty()) {
+            return AjaxResult.error("请选择标签");
+        }
+
+        int result = tagTemplateBindingMapper.batchBind(companyId, templateId, tagCodes);
+        return result > 0 ? AjaxResult.success("批量绑定成功") : AjaxResult.error("批量绑定失败");
+    }
+
+    @Override
+    public Map<String, Object> matchTemplate(Long companyId, List<String> userTags) {
+        if (userTags == null || userTags.isEmpty()) {
+            Map<String, Object> result = new HashMap<>();
+            result.put("matched", false);
+            result.put("message", "用户没有标签");
+            return result;
+        }
+
+        // 查询匹配的模板
+        List<CompanyTagTemplateBinding> matchedBindings = tagTemplateBindingMapper.selectMatchedTemplates(companyId, userTags);
+
+        Map<String, Object> result = new HashMap<>();
+        if (matchedBindings.isEmpty()) {
+            result.put("matched", false);
+            result.put("message", "没有匹配的模板");
+            result.put("templateId", null);
+        } else {
+            // 返回优先级最高的模板
+            CompanyTagTemplateBinding bestMatch = matchedBindings.get(0);
+            result.put("matched", true);
+            result.put("message", "匹配成功");
+            result.put("templateId", bestMatch.getTemplateId());
+            result.put("templateName", bestMatch.getTemplateName());
+            result.put("priority", bestMatch.getPriority());
+            result.put("matchedTags", matchedBindings.size());
+        }
+
+        return result;
+    }
+
+    @Override
+    public Map<String, Object> testMatch(Long companyId, Long id, Map<String, Object> testTags) {
+        CompanyTagTemplateBinding binding = tagTemplateBindingMapper.selectBindingByIdAndCompanyId(id, companyId);
+        if (binding == null) {
+            Map<String, Object> result = new HashMap<>();
+            result.put("matched", false);
+            result.put("message", "绑定关系不存在");
+            return result;
+        }
+
+        // 解析匹配条件
+        String matchCondition = binding.getMatchCondition();
+        boolean matched = true;
+
+        if (matchCondition != null && !matchCondition.isEmpty()) {
+            // TODO: 实现匹配条件逻辑
+            // 这里简化处理,实际应该解析JSON条件并验证
+            matched = true;
+        }
+
+        Map<String, Object> result = new HashMap<>();
+        result.put("matched", matched);
+        result.put("tagCode", binding.getTagCode());
+        result.put("tagName", binding.getTagName());
+        result.put("templateId", binding.getTemplateId());
+        result.put("templateName", binding.getTemplateName());
+        result.put("priority", binding.getPriority());
+        result.put("matchCondition", matchCondition);
+
+        return result;
+    }
+}

+ 598 - 0
fs-service/src/main/java/com/fs/company/service/impl/CompanyWorkflowLobsterServiceImpl.java

@@ -0,0 +1,598 @@
+package com.fs.company.service.impl;
+
+import cn.hutool.json.JSONObject;
+import cn.hutool.json.JSONUtil;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fs.common.core.domain.AjaxResult;
+import com.fs.common.core.domain.R;
+import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.company.domain.CompanyWorkflowLobster;
+import com.fs.company.domain.CompanyWorkflowLobsterEdge;
+import com.fs.company.domain.CompanyWorkflowLobsterNode;
+import com.fs.company.domain.CompanyWorkflowLobsterRecord;
+import com.fs.company.domain.CompanyWorkflowLobsterVariable;
+import com.fs.company.mapper.CompanyWorkflowLobsterEdgeMapper;
+import com.fs.company.mapper.CompanyWorkflowLobsterMapper;
+import com.fs.company.mapper.CompanyWorkflowLobsterNodeMapper;
+import com.fs.company.mapper.CompanyWorkflowLobsterRecordMapper;
+import com.fs.company.mapper.CompanyWorkflowLobsterVariableMapper;
+import com.fs.company.param.CompanyWorkflowLobsterCanvasParam;
+import com.fs.company.param.CompanyWorkflowLobsterConfirmParam;
+import com.fs.company.param.CompanyWorkflowLobsterEdgeParam;
+import com.fs.company.param.CompanyWorkflowLobsterGenerateParam;
+import com.fs.company.param.CompanyWorkflowLobsterNodeParam;
+import com.fs.company.param.CompanyWorkflowLobsterVariableParam;
+import com.fs.company.service.ICompanyWorkflowLobsterService;
+import com.fs.fastgptApi.param.ChatParam;
+import com.fs.fastgptApi.service.ChatService;
+import org.springframework.beans.BeanUtils;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+
+import java.util.*;
+import java.util.stream.Collectors;
+
+@Service
+public class CompanyWorkflowLobsterServiceImpl implements ICompanyWorkflowLobsterService {
+
+    @Autowired
+    private CompanyWorkflowLobsterMapper lobsterMapper;
+    @Autowired
+    private CompanyWorkflowLobsterNodeMapper nodeMapper;
+    @Autowired
+    private CompanyWorkflowLobsterVariableMapper variableMapper;
+    @Autowired
+    private CompanyWorkflowLobsterRecordMapper recordMapper;
+    @Autowired
+    private CompanyWorkflowLobsterEdgeMapper edgeMapper;
+
+    @Override
+    public List<CompanyWorkflowLobster> listTemplate(Long companyId, Integer page, Integer size) {
+        List<CompanyWorkflowLobster> all = lobsterMapper.selectTemplateList(companyId);
+        int start = Math.max((page - 1) * size, 0);
+        int end = Math.min(start + size, all.size());
+        if (start >= all.size()) {
+            return Collections.emptyList();
+        }
+        return all.subList(start, end);
+    }
+
+    private static final String LOBSTER_KEY = "fastgpt-lDP6kVelHf2p8j80vfz2Kl7g9PjacwJoTmCplEBGWBaGMCRtv7SueW5mZ4iXe";
+    private static final ObjectMapper mapper = new ObjectMapper();
+    private static final String AI_API = "http://129.28.170.206:3000/api";
+
+    @Override
+    public String generate(Long companyId, String userName, CompanyWorkflowLobsterGenerateParam param) {
+        Long l = System.currentTimeMillis();
+        String recordNo = "LOB_" + l;
+        CompanyWorkflowLobsterRecord record = new CompanyWorkflowLobsterRecord();
+        record.setCompanyId(companyId);
+        record.setRecordNo(recordNo);
+        record.setRequirement(param.getRequirement());
+        record.setSelectedApiIds(param.getSelectedApiIds() == null ? null : param.getSelectedApiIds().stream().map(String::valueOf).collect(Collectors.joining(",")));
+        record.setStatus(1);
+        record.setCreateBy(userName);
+        record.setCreateTime(DateUtils.getNowDate());
+        record.setUpdateBy(userName);
+        record.setUpdateTime(DateUtils.getNowDate());
+        record.setDelFlag(0);
+        String requestStr = "{ \"userContent\": \""+param.getRequirement()+"\", \"aiContent\": null }";
+        R r = callAiService(requestStr, l, LOBSTER_KEY);
+//        System.out.println(r);
+        record.setResultJson(buildResultJsonFromAi(r, param.getRequirement()));
+        recordMapper.insertRecord(record);
+        return recordNo;
+    }
+    private static R callAiService(String requestParam, Long logId, String appKey) {
+        try {
+            ChatParam param = new ChatParam();
+            param.setChatId(logId.toString());
+            param.setStream(false);
+            param.setDetail(true);
+            ChatParam.Message message = new ChatParam.Message();
+            List<ChatParam.Message> messageList = new ArrayList<ChatParam.Message>();
+            message.setContent(requestParam);
+            message.setRole("user");
+            messageList.add(message);
+            param.setMessages(messageList);
+            ChatService chatService = SpringUtils.getBean(ChatService.class);
+
+            return chatService.initiatingTakeChat(param, AI_API, appKey);
+        } catch (Exception e) {
+            throw new RuntimeException("AI服务调用失败", e);
+        }
+    }
+
+    @Override
+    public Map<String, Object> getResultDetail(Long companyId, String recordId) {
+        CompanyWorkflowLobsterRecord record = recordMapper.selectByRecordNo(recordId, companyId);
+        if (record == null) {
+            return new HashMap<String, Object>() {{
+                put("status", 2);
+                put("errorMsg", "record不存在");
+            }};
+        }
+        Map<String, Object> map = new HashMap<>();
+        map.put("status", record.getStatus());
+        map.put("errorMsg", record.getErrorMsg());
+        if (record.getResultJson() != null) {
+            JSONObject resultJson = JSONUtil.parseObj(record.getResultJson());
+            map.putAll(resultJson);
+        }
+        return map;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult confirmRaw(Long companyId, String userName, String recordId) {
+        CompanyWorkflowLobsterRecord record = recordMapper.selectByRecordNo(recordId, companyId);
+        if (record == null) {
+            return AjaxResult.error("record不存在");
+        }
+        if (record.getResultJson() == null) {
+            return AjaxResult.error("生成结果为空");
+        }
+        CompanyWorkflowLobsterConfirmParam param = JSONUtil.toBean(record.getResultJson(), CompanyWorkflowLobsterConfirmParam.class);
+        saveWorkflow(companyId, userName, param);
+        record.setStatus(3);
+        record.setUpdateBy(userName);
+        record.setUpdateTime(DateUtils.getNowDate());
+        recordMapper.updateRecord(record);
+        return AjaxResult.success("保存成功");
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult confirmEdited(Long companyId, String userName, String recordId, CompanyWorkflowLobsterConfirmParam param) {
+        CompanyWorkflowLobsterRecord record = recordMapper.selectByRecordNo(recordId, companyId);
+        if (record == null) {
+            return AjaxResult.error("record不存在");
+        }
+        saveWorkflow(companyId, userName, param);
+        record.setStatus(3);
+        record.setUpdateBy(userName);
+        record.setUpdateTime(DateUtils.getNowDate());
+        recordMapper.updateRecord(record);
+        return AjaxResult.success("保存成功");
+    }
+
+    @Override
+    public Map<String, Object> previewTemplate(Long companyId, Long templateId) {
+        CompanyWorkflowLobster template = lobsterMapper.selectById(templateId);
+        if (template == null || !Objects.equals(template.getCompanyId(), companyId) || Objects.equals(template.getDelFlag(), 1)) {
+            return null;
+        }
+        List<CompanyWorkflowLobsterVariable> variables = variableMapper.selectList(
+                new LambdaQueryWrapper<CompanyWorkflowLobsterVariable>()
+                        .eq(CompanyWorkflowLobsterVariable::getWorkflowId, templateId)
+                        .eq(CompanyWorkflowLobsterVariable::getDelFlag, 0)
+                        .orderByAsc(CompanyWorkflowLobsterVariable::getId)
+        );
+        List<CompanyWorkflowLobsterNode> nodes = nodeMapper.selectList(
+                new LambdaQueryWrapper<CompanyWorkflowLobsterNode>()
+                        .eq(CompanyWorkflowLobsterNode::getWorkflowId, templateId)
+                        .eq(CompanyWorkflowLobsterNode::getDelFlag, 0)
+                        .orderByAsc(CompanyWorkflowLobsterNode::getSortNo)
+                        .orderByAsc(CompanyWorkflowLobsterNode::getId)
+        );
+        Map<String, Object> result = new HashMap<>();
+        result.put("template", template);
+        result.put("variables", variables);
+        result.put("nodes", nodes);
+        return result;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult deleteTemplate(Long companyId, String userName, Long templateId) {
+        CompanyWorkflowLobster template = lobsterMapper.selectById(templateId);
+        if (template == null || !Objects.equals(template.getCompanyId(), companyId) || Objects.equals(template.getDelFlag(), 1)) {
+            return AjaxResult.error("模板不存在");
+        }
+        template.setDelFlag(1);
+        template.setUpdateBy(userName);
+        template.setUpdateTime(DateUtils.getNowDate());
+        lobsterMapper.updateById(template);
+        variableMapper.deleteByWorkflowId(templateId);
+        nodeMapper.deleteByWorkflowId(templateId);
+        edgeMapper.deleteByWorkflowId(templateId); // 删除关联的连线数据
+        return AjaxResult.success("删除成功");
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult updateTemplate(Long companyId, String userName, Long templateId, CompanyWorkflowLobsterConfirmParam param) {
+        CompanyWorkflowLobster template = lobsterMapper.selectById(templateId);
+        if (template == null || !Objects.equals(template.getCompanyId(), companyId) || Objects.equals(template.getDelFlag(), 1)) {
+            return AjaxResult.error("模板不存在");
+        }
+        template.setTemplateName(param.getTemplateName());
+        template.setIndustryType(param.getIndustryType());
+        template.setDescription(param.getDescription());
+        template.setVersion(template.getVersion() == null ? 1 : template.getVersion() + 1);
+        template.setUpdateBy(userName);
+        template.setUpdateTime(DateUtils.getNowDate());
+        lobsterMapper.updateById(template);
+
+        // 先逻辑删除旧节点与旧变量,再写入新内容
+        variableMapper.deleteByWorkflowId(templateId);
+        nodeMapper.deleteByWorkflowId(templateId);
+        saveWorkflowDetails(templateId, userName, param);
+        return AjaxResult.success("修改成功");
+    }
+
+    @Override
+    public Map<String, Object> getTemplate(Long companyId, Long templateId) {
+        CompanyWorkflowLobster template = lobsterMapper.selectById(templateId);
+        if (template == null || !Objects.equals(template.getCompanyId(), companyId) || Objects.equals(template.getDelFlag(), 1)) {
+            return new HashMap<String, Object>() {{
+                put("status", 2);
+                put("errorMsg", "模板不存在");
+            }};
+        }
+
+        List<CompanyWorkflowLobsterVariable> variables = variableMapper.selectList(
+                new LambdaQueryWrapper<CompanyWorkflowLobsterVariable>()
+                        .eq(CompanyWorkflowLobsterVariable::getWorkflowId, templateId)
+                        .eq(CompanyWorkflowLobsterVariable::getDelFlag, 0)
+                        .orderByAsc(CompanyWorkflowLobsterVariable::getId)
+        );
+        List<CompanyWorkflowLobsterNode> nodes = nodeMapper.selectList(
+                new LambdaQueryWrapper<CompanyWorkflowLobsterNode>()
+                        .eq(CompanyWorkflowLobsterNode::getWorkflowId, templateId)
+                        .eq(CompanyWorkflowLobsterNode::getDelFlag, 0)
+                        .orderByAsc(CompanyWorkflowLobsterNode::getSortNo)
+                        .orderByAsc(CompanyWorkflowLobsterNode::getId)
+        );
+        // 查询连线数据
+        List<CompanyWorkflowLobsterEdge> edges = edgeMapper.selectList(
+                new LambdaQueryWrapper<CompanyWorkflowLobsterEdge>()
+                        .eq(CompanyWorkflowLobsterEdge::getWorkflowId, templateId)
+                        .eq(CompanyWorkflowLobsterEdge::getDelFlag, 0)
+                        .orderByAsc(CompanyWorkflowLobsterEdge::getSortNo)
+                        .orderByAsc(CompanyWorkflowLobsterEdge::getId)
+        );
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("status", 3);
+        map.put("errorMsg", null);
+        map.put("templateId", template.getId());
+        map.put("templateCode", template.getTemplateCode());
+        map.put("templateName", template.getTemplateName());
+        map.put("industryType", template.getIndustryType());
+        map.put("description", template.getDescription());
+        map.put("canvasData", template.getCanvasData()); // 添加画布数据
+        map.put("variables", variables);
+        map.put("nodes", nodes);
+        map.put("edges", edges); // 添加连线数据
+        return map;
+    }
+
+    private String buildDefaultResultJson(String requirement) {
+        Map<String, Object> result = new HashMap<>();
+        result.put("templateName", "AI生成工作流方案");
+        result.put("industryType", "general");
+        result.put("description", requirement == null ? "" : requirement);
+        result.put("variables", Arrays.asList(
+                variableMap("customer_name", "客户姓名", "string", "user_profile", 1, "客户名称"),
+                variableMap("follow_date", "跟进日期", "date", "manual", 0, "计划跟进时间")
+        ));
+        result.put("nodes", Arrays.asList(
+                nodeMap("START", "开始节点", 1, 1, "MSG_1", "", "", "{}"),
+                nodeMap("MSG_1", "消息节点", 2, 2, "END", "生成失败,请重新编写提示词", "", "{}"),
+                nodeMap("END", "结束节点", 5, 3, "", "", "", "{}")
+        ));
+        // 默认连线:START -> MSG_1 -> END
+        result.put("edges", Arrays.asList(
+                edgeMap("EDGE_0", "START", "MSG_1", "right", "left", "", "#999", ""),
+                edgeMap("EDGE_1", "MSG_1", "END", "right", "left", "", "#999", "")
+        ));
+        return JSONUtil.toJsonStr(result);
+    }
+
+    /**
+     * 按 r.data.choices[0].message.content 结构提取 aiContent 并生成工作流模板JSON
+     */
+    private String buildResultJsonFromAi(R aiResponse, String requirement) {
+        try {
+            Object dataObj = aiResponse.get("data");
+            if (dataObj == null) {
+                return buildDefaultResultJson(requirement);
+            }
+
+            Map<?, ?> dataMap = mapper.convertValue(dataObj, Map.class);
+            Object choicesObj = dataMap.get("choices");
+            if (!(choicesObj instanceof List) || ((List<?>) choicesObj).isEmpty()) {
+                return buildDefaultResultJson(requirement);
+            }
+
+            Map<?, ?> firstChoice = mapper.convertValue(((List<?>) choicesObj).get(0), Map.class);
+            Map<?, ?> message = mapper.convertValue(firstChoice.get("message"), Map.class);
+            String content = message.get("content") == null ? null : String.valueOf(message.get("content"));
+            if (content == null || content.trim().isEmpty()) {
+                return buildDefaultResultJson(requirement);
+            }
+
+            JSONObject contentJson = JSONUtil.parseObj(content);
+            String userContent = contentJson.getStr("userContent");
+            String industryType = (userContent == null || userContent.trim().isEmpty()) ? "general" : userContent;
+            List<Object> aiContentList = contentJson.getJSONArray("aiContent");
+            if (aiContentList == null || aiContentList.isEmpty()) {
+                return buildDefaultResultJson(requirement);
+            }
+
+            List<Map<String, Object>> nodes = new ArrayList<>();
+            nodes.add(nodeMap("START", "开始节点", 1, 1, "", "", "", "{}"));
+
+            int sortNo = 2;
+            List<String> dayCodes = new ArrayList<>();
+            for (Object item : aiContentList) {
+                Map<?, ?> itemMap = mapper.convertValue(item, Map.class);
+                Object addDayObj = itemMap.get("addDay");
+                if (addDayObj == null) {
+                    continue;
+                }
+                String day = String.valueOf(addDayObj).trim();
+                if (day.isEmpty()) {
+                    continue;
+                }
+
+                String nodeCode = "DAY_" + day;
+                dayCodes.add(nodeCode);
+                String template = itemMap.get("content") == null ? "" : String.valueOf(itemMap.get("content"));
+                nodes.add(nodeMap(nodeCode, "第" + day + "天", 2, sortNo++, "", template, "", "{}"));
+            }
+
+            if (dayCodes.isEmpty()) {
+                return buildDefaultResultJson(requirement);
+            }
+
+            // 生成节点之间的连线
+            List<Map<String, Object>> edges = new ArrayList<>();
+
+            // START -> 第一个DAY节点
+            if (!dayCodes.isEmpty()) {
+                edges.add(edgeMap("EDGE_START_0", "START", dayCodes.get(0), "right", "left", "", "#999", ""));
+            }
+
+            // DAY节点之间的连接
+            for (int i = 0; i < dayCodes.size(); i++) {
+                String currentCode = dayCodes.get(i);
+                String nextCode = (i == dayCodes.size() - 1) ? "END" : dayCodes.get(i + 1);
+
+                // 更新节点的nextNodeCode
+                nodes.get(i + 1).put("nextNodeCode", nextCode);
+
+                // 添加连线
+                String edgeKey = "EDGE_" + i;
+                edges.add(edgeMap(edgeKey, currentCode, nextCode, "right", "left", "", "#999", ""));
+            }
+
+            nodes.add(nodeMap("END", "结束节点", 5, sortNo, "", "", "", "{}"));
+
+            Map<String, Object> result = new HashMap<>();
+            result.put("templateName", "AI生成工作流方案");
+            result.put("industryType", industryType);
+            result.put("description", requirement == null ? "" : requirement);
+            result.put("variables", Arrays.asList(
+                    variableMap("customer_name", "客户姓名", "string", "user_profile", 1, "客户名称"),
+                    variableMap("follow_date", "跟进日期", "date", "manual", 0, "计划跟进时间")
+            ));
+            result.put("nodes", nodes);
+            result.put("edges", edges); // 添加连线数据
+            return JSONUtil.toJsonStr(result);
+        } catch (Exception ignore) {
+            return buildDefaultResultJson(requirement);
+        }
+    }
+
+    private Map<String, Object> variableMap(String varCode, String varName, String varType, String sourceType, Integer required, String description) {
+        Map<String, Object> m = new HashMap<>();
+        m.put("varCode", varCode);
+        m.put("varName", varName);
+        m.put("varType", varType);
+        m.put("sourceType", sourceType);
+        m.put("required", required);
+        m.put("description", description);
+        return m;
+    }
+
+    private Map<String, Object> nodeMap(String nodeCode, String nodeName, Integer nodeType, Integer sortNo, String nextNodeCode, String messageTemplate, String conditionExpr, String nodeConfig) {
+        Map<String, Object> m = new HashMap<>();
+        m.put("nodeCode", nodeCode);
+        m.put("nodeName", nodeName);
+        m.put("nodeType", nodeType);
+        m.put("sortNo", sortNo);
+        m.put("nextNodeCode", nextNodeCode);
+        m.put("messageTemplate", messageTemplate);
+        m.put("conditionExpr", conditionExpr);
+        m.put("nodeConfig", nodeConfig);
+        return m;
+    }
+
+    private Map<String, Object> edgeMap(String edgeKey, String sourceNodeCode, String targetNodeCode,
+                                        String sourcePort, String targetPort,
+                                        String edgeLabel, String edgeColor, String conditionExpr) {
+        Map<String, Object> m = new HashMap<>();
+        m.put("edgeKey", edgeKey);
+        m.put("sourceNodeCode", sourceNodeCode);
+        m.put("targetNodeCode", targetNodeCode);
+        m.put("sourcePort", sourcePort);
+        m.put("targetPort", targetPort);
+        m.put("edgeLabel", edgeLabel);
+        m.put("edgeColor", edgeColor);
+        m.put("conditionExpr", conditionExpr);
+        m.put("sortNo", 0);
+        m.put("delFlag", 0);
+        return m;
+    }
+
+    private void saveWorkflow(Long companyId, String userName, CompanyWorkflowLobsterConfirmParam param) {
+        CompanyWorkflowLobster entity = new CompanyWorkflowLobster();
+        entity.setCompanyId(companyId);
+        entity.setTemplateCode("LOB_" + System.currentTimeMillis());
+        entity.setTemplateName(param.getTemplateName());
+        entity.setIndustryType(param.getIndustryType());
+        entity.setDescription(param.getDescription());
+        entity.setStatus(0);
+        entity.setVersion(1);
+        entity.setDelFlag(0);
+        entity.setCreateBy(userName);
+        entity.setCreateTime(DateUtils.getNowDate());
+        entity.setUpdateBy(userName);
+        entity.setUpdateTime(DateUtils.getNowDate());
+        lobsterMapper.insertTemplate(entity);
+
+        Long workflowId = entity.getId();
+        if (workflowId == null) {
+            throw new RuntimeException("保存模板失败");
+        }
+
+        saveWorkflowDetails(workflowId, userName, param);
+    }
+
+    private void saveWorkflowDetails(Long workflowId, String userName, CompanyWorkflowLobsterConfirmParam param) {
+        // 保存变量
+        List<CompanyWorkflowLobsterVariableParam> vars = param.getVariables();
+        if (vars != null && !vars.isEmpty()) {
+            List<CompanyWorkflowLobsterVariable> varEntities = new ArrayList<>();
+            for (CompanyWorkflowLobsterVariableParam it : vars) {
+                CompanyWorkflowLobsterVariable v = new CompanyWorkflowLobsterVariable();
+                BeanUtils.copyProperties(it, v);
+                v.setWorkflowId(workflowId);
+                v.setDelFlag(0);
+                v.setCreateBy(userName);
+                v.setCreateTime(DateUtils.getNowDate());
+                v.setUpdateBy(userName);
+                v.setUpdateTime(DateUtils.getNowDate());
+                varEntities.add(v);
+            }
+            variableMapper.batchInsert(varEntities);
+        }
+
+        // 保存节点
+        List<CompanyWorkflowLobsterNodeParam> nodes = param.getNodes();
+        if (nodes != null && !nodes.isEmpty()) {
+            List<CompanyWorkflowLobsterNode> nodeEntities = new ArrayList<>();
+            int idx = 1;
+            for (CompanyWorkflowLobsterNodeParam it : nodes) {
+                CompanyWorkflowLobsterNode n = new CompanyWorkflowLobsterNode();
+                BeanUtils.copyProperties(it, n);
+                n.setWorkflowId(workflowId);
+                if (n.getSortNo() == null) {
+                    n.setSortNo(idx++);
+                }
+                n.setDelFlag(0);
+                n.setCreateBy(userName);
+                n.setCreateTime(DateUtils.getNowDate());
+                n.setUpdateBy(userName);
+                n.setUpdateTime(DateUtils.getNowDate());
+                nodeEntities.add(n);
+            }
+            nodeMapper.batchInsert(nodeEntities);
+        }
+
+        // 保存连线
+        List<CompanyWorkflowLobsterEdgeParam> edges = param.getEdges();
+        if (edges != null && !edges.isEmpty()) {
+            List<CompanyWorkflowLobsterEdge> edgeEntities = new ArrayList<>();
+            int idx = 0;
+            for (CompanyWorkflowLobsterEdgeParam it : edges) {
+                CompanyWorkflowLobsterEdge e = new CompanyWorkflowLobsterEdge();
+                BeanUtils.copyProperties(it, e);
+                e.setWorkflowId(workflowId);
+                if (e.getSortNo() == null) {
+                    e.setSortNo(idx++);
+                }
+                e.setDelFlag(0);
+                e.setCreateBy(userName);
+                e.setCreateTime(DateUtils.getNowDate());
+                e.setUpdateBy(userName);
+                e.setUpdateTime(DateUtils.getNowDate());
+                edgeEntities.add(e);
+            }
+            edgeMapper.batchInsert(edgeEntities);
+        }
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult saveCanvas(Long companyId, String userName, Long templateId, CompanyWorkflowLobsterCanvasParam param) {
+        // 验证模板是否存在
+        CompanyWorkflowLobster template = lobsterMapper.selectById(templateId);
+        if (template == null || !Objects.equals(template.getCompanyId(), companyId) || Objects.equals(template.getDelFlag(), 1)) {
+            return AjaxResult.error("模板不存在");
+        }
+
+        // 更新模板基本信息和画布数据
+        template.setTemplateName(param.getTemplateName());
+        template.setIndustryType(param.getIndustryType());
+        template.setDescription(param.getDescription());
+        template.setCanvasData(param.getCanvasData()); // 保存画布JSON数据
+        template.setVersion(template.getVersion() == null ? 1 : template.getVersion() + 1);
+        template.setUpdateBy(userName);
+        template.setUpdateTime(DateUtils.getNowDate());
+        lobsterMapper.updateById(template);
+
+        // 先逻辑删除旧数据
+        variableMapper.deleteByWorkflowId(templateId);
+        nodeMapper.deleteByWorkflowId(templateId);
+        edgeMapper.deleteByWorkflowId(templateId);
+
+        // 保存变量
+        if (param.getVariables() != null && !param.getVariables().isEmpty()) {
+            variableMapper.batchInsert(param.getVariables().stream().map(v -> {
+                CompanyWorkflowLobsterVariable entity = new CompanyWorkflowLobsterVariable();
+                BeanUtils.copyProperties(v, entity);
+                entity.setWorkflowId(templateId);
+                entity.setDelFlag(0);
+                entity.setCreateBy(userName);
+                entity.setCreateTime(DateUtils.getNowDate());
+                entity.setUpdateBy(userName);
+                entity.setUpdateTime(DateUtils.getNowDate());
+                return entity;
+            }).collect(Collectors.toList()));
+        }
+
+        // 保存节点(包含位置信息)
+        if (param.getNodes() != null && !param.getNodes().isEmpty()) {
+            nodeMapper.batchInsert(param.getNodes().stream().map(n -> {
+                CompanyWorkflowLobsterNode entity = new CompanyWorkflowLobsterNode();
+                BeanUtils.copyProperties(n, entity);
+                entity.setWorkflowId(templateId);
+                if (entity.getSortNo() == null) {
+                    entity.setSortNo(0);
+                }
+                entity.setDelFlag(0);
+                entity.setCreateBy(userName);
+                entity.setCreateTime(DateUtils.getNowDate());
+                entity.setUpdateBy(userName);
+                entity.setUpdateTime(DateUtils.getNowDate());
+                return entity;
+            }).collect(Collectors.toList()));
+        }
+
+        // 保存连线
+        if (param.getEdges() != null && !param.getEdges().isEmpty()) {
+            edgeMapper.batchInsert(param.getEdges().stream().map(e -> {
+                CompanyWorkflowLobsterEdge entity = new CompanyWorkflowLobsterEdge();
+                BeanUtils.copyProperties(e, entity);
+                entity.setWorkflowId(templateId);
+                if (entity.getSortNo() == null) {
+                    entity.setSortNo(0);
+                }
+                entity.setDelFlag(0);
+                entity.setCreateBy(userName);
+                entity.setCreateTime(DateUtils.getNowDate());
+                entity.setUpdateBy(userName);
+                entity.setUpdateTime(DateUtils.getNowDate());
+                return entity;
+            }).collect(Collectors.toList()));
+        }
+
+        return AjaxResult.success("画布保存成功");
+    }
+}

+ 18 - 0
fs-service/src/main/java/com/fs/company/vo/CompanyWorkflowLobsterDetailVO.java

@@ -0,0 +1,18 @@
+package com.fs.company.vo;
+
+import com.fs.company.domain.CompanyWorkflowLobster;
+import com.fs.company.domain.CompanyWorkflowLobsterEdge;
+import com.fs.company.domain.CompanyWorkflowLobsterNode;
+import com.fs.company.domain.CompanyWorkflowLobsterVariable;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+
+import java.util.List;
+
+@Data
+@EqualsAndHashCode(callSuper = true)
+public class CompanyWorkflowLobsterDetailVO extends CompanyWorkflowLobster {
+    private List<CompanyWorkflowLobsterVariable> variables;
+    private List<CompanyWorkflowLobsterNode> nodes;
+    private List<CompanyWorkflowLobsterEdge> edges;
+}

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

+ 6 - 0
fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerAnalyzeMapper.java

@@ -2,6 +2,7 @@ package com.fs.crm.mapper;
 
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.fs.crm.domain.CrmCustomerAnalyze;
+import com.fs.qw.domain.QwExternalAiAnalyze;
 import org.apache.ibatis.annotations.Param;
 
 import java.util.List;
@@ -66,4 +67,9 @@ public interface CrmCustomerAnalyzeMapper extends BaseMapper<CrmCustomerAnalyze>
     int updateCustomerPortrait(CrmCustomerAnalyze crmCustomerAnalyze);
 
     List<CrmCustomerAnalyze> selectCrmCustomerAnalyzeListAll(CrmCustomerAnalyze crmCustomerAnalyze);
+
+    QwExternalAiAnalyze selectLatestQwAnalyze(QwExternalAiAnalyze qwExternalAiAnalyze);
+
+    Integer updateAiAnalyze(QwExternalAiAnalyze qwExternalAiAnalyze);
+
 }

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

+ 19 - 0
fs-service/src/main/java/com/fs/crm/service/ICrmCustomerAnalyzeService.java

@@ -1,9 +1,12 @@
 package com.fs.crm.service;
 
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.common.core.domain.R;
 import com.fs.crm.domain.CrmCustomerAnalyze;
 import com.fs.crm.domain.CrmCustomerChatMessage;
 import com.fs.crm.param.PolishingScriptParam;
+import com.fs.qw.domain.QwExternalAiAnalyze;
+import com.fs.qw.param.audit.QwAiTagGainParam;
 
 import java.util.List;
 
@@ -81,4 +84,20 @@ public interface ICrmCustomerAnalyzeService extends IService<CrmCustomerAnalyze>
     String aiIntentionDegree(String content , Long chatId) ;
 
     int updateCrmCustomerAnalyzeByCustomerId(CrmCustomerAnalyze crmCustomerAnalyze);
+
+    String aiGeneratedCustomerPortraitQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId);
+
+    Integer updateQwAnalyzeByCustomerId(QwExternalAiAnalyze qwExternalAiAnalyze);
+
+    String aiCommunicationSummaryQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId);
+
+    String aiCommunicationAbstractQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId);
+
+    Long aiAttritionLevelQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId);
+
+    String aiCustomerFocusQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId);
+
+    String aiIntentionDegreeQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId);
+
+    R qwAiTagGain(QwAiTagGainParam param);
 }

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

@@ -6,6 +6,8 @@ import cn.hutool.core.util.ObjectUtil;
 import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.JsonNode;
@@ -13,21 +15,33 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.entity.SysDictData;
 import com.fs.common.utils.DateUtils;
+import com.fs.common.utils.DictUtils;
+import com.fs.common.utils.spring.SpringUtils;
 import com.fs.crm.domain.CrmCustomerAnalyze;
 import com.fs.crm.domain.CrmCustomerChatMessage;
+import com.fs.crm.domain.CrmCustomerPropertyTemplate;
+import com.fs.crm.dto.CrmCustomerAiAutoTagVo;
 import com.fs.crm.mapper.CrmCustomerAnalyzeMapper;
 import com.fs.crm.param.PolishingScriptParam;
 import com.fs.crm.service.ICrmCustomerAnalyzeService;
+import com.fs.crm.service.ICrmCustomerPropertyTemplateService;
 import com.fs.crm.utils.CrmCustomerAiTagUtil;
+import com.fs.qw.domain.QwExternalAiAnalyze;
+import com.fs.qw.mapper.QwCustomerPropertyMapper;
+import com.fs.qw.mapper.QwExternalAiAnalyzeMapper;
+import com.fs.qw.param.audit.QwAiTagGainParam;
+import com.fs.qw.vo.QwCustomerAiTagVo;
 import com.fs.system.mapper.SysDictDataMapper;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
 import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
 import java.util.*;
 import java.util.stream.Collectors;
+import java.util.stream.Stream;
 
 /**
  * 客户聊天记录分析Service业务层处理
@@ -41,6 +55,11 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
     private static final Logger log = LoggerFactory.getLogger(CrmCustomerAnalyzeServiceImpl.class);
     @Autowired
     private SysDictDataMapper sysDictDataMapper;
+    @Autowired
+    private QwExternalAiAnalyzeMapper qwExternalAiAnalyzeMapper;
+    @Qualifier("qwCustomerPropertyMapper")
+    @Autowired
+    private QwCustomerPropertyMapper qwCustomerPropertyMapper;
     /**
      * 查询客户聊天记录分析
      *
@@ -388,7 +407,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 +550,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 = "";
@@ -582,13 +601,175 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         return baseMapper.updateCustomerPortrait(crmCustomerAnalyze);
     }
 
+    @Override
+    public String aiGeneratedCustomerPortraitQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId) {
+        Map<String, Object> stringObjectMap = buildRequestParam(qwExternalAiAnalyze, dataJson);
+        stringObjectMap.put("modelType", "客户画像");
+//        log.info("请求参数:{}", stringObjectMap);
+        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId, OTHER_KEY);
+//        System.out.println(aiResponse);
+        String result = "";
+        try {
+            result = Objects.requireNonNull(responseAnalyze(aiResponse, "userInfo")).toString();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return result;
+
+    }
+
+    @Override
+    public Integer updateQwAnalyzeByCustomerId(QwExternalAiAnalyze qwExternalAiAnalyze) {
+        return baseMapper.updateAiAnalyze(qwExternalAiAnalyze);
+    }
+
+    @Override
+    public String aiCommunicationSummaryQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId) {
+        Map<String, Object> stringObjectMap = buildRequestParam(qwExternalAiAnalyze, dataJson);
+        stringObjectMap.put("modelType", "沟通总结");
+//        log.info("请求参数:{}", stringObjectMap);
+        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId, OTHER_KEY);
+//        System.out.println(aiResponse);
+        String result = "";
+        try {
+            JsonNode userInfo = responseAnalyze(aiResponse, "userInfo");
+            assert userInfo != null;
+            result = userInfo.get("沟通总结").asText();
+        } catch (Exception e) {
+            log.error("获取沟通总结失败", e);
+            e.printStackTrace();
+        }
+        return result;
+    }
+
+    @Override
+    public String aiCommunicationAbstractQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId) {
+        Map<String, Object> stringObjectMap = buildRequestParam(qwExternalAiAnalyze, dataJson);
+        stringObjectMap.put("modelType", "沟通摘要");
+        stringObjectMap.remove("userInfo");
+        HashMap<String, String> map = MapUtil.of("沟通摘要", "");
+        stringObjectMap.put("userInfo", map);
+//        log.info("请求参数:{}", stringObjectMap);
+        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId, OTHER_KEY);
+//        System.out.println(aiResponse);
+        String result = "";
+        try {
+            JsonNode userInfo = responseAnalyze(aiResponse, "userInfo");
+            assert userInfo != null;
+            result = userInfo.get("沟通摘要").asText();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return result;
+    }
+
+    @Override
+    public Long aiAttritionLevelQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId) {
+        Map<String, Object> stringObjectMap = buildRequestParam(qwExternalAiAnalyze, dataJson);
+        stringObjectMap.put("modelType","流失风险等级");
+        stringObjectMap.remove("userInfo");
+        HashMap<String, String> map = MapUtil.of("流失风险等级", "");
+        stringObjectMap.put("userInfo", map);
+//        log.info("请求参数:{}", stringObjectMap);
+        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId, OTHER_KEY);
+//        System.out.println(aiResponse);
+        Long result = 0L;
+        try {
+            JsonNode userInfo = responseAnalyze(aiResponse, "userInfo");
+            assert userInfo != null;
+            result = getScore(userInfo.path("流失风险等级").asText());
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return result;
+    }
+
+    @Override
+    public String aiCustomerFocusQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId) {
+        Map<String, Object> stringObjectMap = buildRequestParam(qwExternalAiAnalyze, dataJson);
+        stringObjectMap.put("modelType","客户关注点");
+        stringObjectMap.remove("userInfo");
+        HashMap<String, String> map = MapUtil.of("客户关注点", "");
+        stringObjectMap.put("userInfo", map);
+//        log.info("请求参数:{}", stringObjectMap);
+        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId, OTHER_KEY);
+//        System.out.println(aiResponse);
+        String result = "";
+        try {
+            JsonNode userInfo = responseAnalyze(aiResponse, "userInfo");
+            assert userInfo != null;
+            result = userInfo.path("客户关注点").asText();
+
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return result;
+
+    }
+
+    @Override
+    public String aiIntentionDegreeQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId) {
+        Map<String, Object> stringObjectMap = buildRequestParam(qwExternalAiAnalyze, dataJson);
+        stringObjectMap.put("modelType","客户意向度");
+        stringObjectMap.remove("userInfo");
+        HashMap<String, String> map = MapUtil.of("客户意向度", "");
+        stringObjectMap.put("userInfo", map);
+//        log.info("请求参数:{}", stringObjectMap);
+        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, logId, OTHER_KEY);
+//        System.out.println(aiResponse);
+        String result = "";
+        try {
+            JsonNode userInfo = responseAnalyze(aiResponse, "userInfo");
+            assert userInfo != null;
+            result = userInfo.get("客户意向度").asText();
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+        return result;
+
+    }
+
+    @Override
+    public R qwAiTagGain(QwAiTagGainParam param) {
+        QwExternalAiAnalyze qwExternalAiAnalyze = qwExternalAiAnalyzeMapper.selectOne(new LambdaQueryWrapper<QwExternalAiAnalyze>()
+                .eq(QwExternalAiAnalyze::getExternalUserId, param.getExternalUserId()).eq(QwExternalAiAnalyze::getCorpId, param.getCorpId())
+                .eq(QwExternalAiAnalyze::getQwUserId, param.getQwUserId()).orderByDesc(QwExternalAiAnalyze::getCreateTime).last("limit 1"));
+        if (ObjectUtil.isEmpty(qwExternalAiAnalyze))return R.error("客户ai分析信息不存在");
+        Map<String, Object> stringObjectMap = buildRequestParam(param.getTradeType(), qwExternalAiAnalyze);
+        R aiResponse = CrmCustomerAiTagUtil.callAiService(stringObjectMap, qwExternalAiAnalyze.getSessionId(),OTHER_KEY);
+        List<QwCustomerAiTagVo> results = parseAiResponse(aiResponse, qwExternalAiAnalyze);
+        if (!results.isEmpty()){
+            qwCustomerPropertyMapper.insertBatch(results);
+        }
+        return R.ok();
+    }
+    private static List<QwCustomerAiTagVo> parseAiResponse(R aiResponse, QwExternalAiAnalyze analyze) {
+        if (aiResponse == null || !Integer.valueOf(200).equals(aiResponse.get("code"))) {
+            throw new RuntimeException("AI响应异常: " +
+                    (aiResponse != null ? aiResponse.get("msg") : "响应为空"));
+        }
+
+        List<Map<String, String>> tagInfos = CrmCustomerAiTagUtil.extractTagInfos(JSONUtil.toJsonStr(aiResponse));
+        if (CollectionUtils.isEmpty(tagInfos)) {
+            return Collections.emptyList();
+        }
+
+        return tagInfos.stream()
+                .map(tag -> buildTagVo(tag, analyze))
+                .collect(Collectors.toList());
+    }
+    private static QwCustomerAiTagVo buildTagVo(Map<String, String> tag, QwExternalAiAnalyze analyze) {
+        QwCustomerAiTagVo vo = new QwCustomerAiTagVo();
+        vo.setExternalUserId(analyze.getExternalUserId()).setQwUserId(analyze.getQwUserId()).setCorpId(analyze.getCorpId())
+                .setPropertyId(tag.get("id")).setPropertyName(tag.get("name")).setPropertyValue(tag.get("value"));
+        return vo;
+    }
     private Map<String, Object> buildRequestParam(Long customerId,
                                                          String communication) {
         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,20 +784,94 @@ 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;
+    }
+    private static Map<String, Object> buildRequestParam(String tradeType, QwExternalAiAnalyze analyze) {
+        Map<String, Object> requestParam = new HashMap<>();
+        // 获取各类数据
+        String tradeName = getDictLabel(tradeType);
+        Map<String, Object> tags = getTags(tradeType);
+        requestParam.put("history",analyze.getAiChatRecord());
+        Map<String, Object> userInfo = getUserInfo(analyze);
+        // 合并数据
+        Stream.of(tags, userInfo)
+                .filter(Objects::nonNull)
+                .forEach(requestParam::putAll);
+        // 设置其他参数
+        requestParam.put("tradeName", tradeName);
+        requestParam.put("tradeType", tradeType);
+        requestParam.put("tagInfos", Collections.emptyList());
+        requestParam.put("isRepository", "");
+        requestParam.put("userContent", "");
+        requestParam.put("aiContent", "");
+        requestParam.put("userIntent", "");
+        requestParam.put("modelType","ai标签");
 
         return requestParam;
     }
+    private static Map<String, Object> getUserInfo(QwExternalAiAnalyze analyze) {
+
+        if (ObjectUtil.isEmpty(analyze))throw new RuntimeException("客户信息不存在");
+        HashMap<String, String> userInfo = new HashMap<String, String>();
+        List<SysDictData> portraits = SpringUtils.getBean(SysDictDataMapper.class).selectDictDataByType(AI_PORTRAIT);
+        List<String> dictValue = portraits.stream().map(SysDictData::getDictValue).collect(Collectors.toList());
+        if (analyze.getCustomerPortraitJson()!= null && !analyze.getCustomerPortraitJson().isEmpty()){
+            Map<String, String> portraitList = JSON.parseObject(
+                    analyze.getCustomerPortraitJson(),
+                    new cn.hutool.core.lang.TypeReference<Map<String, String>>() {}
+            );
+            portraitList.keySet().removeIf(k -> k.matches(".*[a-zA-Z].*"));
+            userInfo.putAll(portraitList);
+        }else {
+            dictValue.forEach(o->{
+                userInfo.put(o, "");
+            });
+        }
+        HashMap<String, Object> result = new HashMap<>();
+        result.put("userInfo", userInfo);
+        return result;
+    }
+    private static Map<String, Object> getTags(String tradeType) {
+        List<CrmCustomerPropertyTemplate> templates = SpringUtils.getBean(ICrmCustomerPropertyTemplateService.class).getBaseMapper().selectList(new LambdaQueryWrapper<CrmCustomerPropertyTemplate>().eq(
+                CrmCustomerPropertyTemplate::getTradeType, tradeType
+        ));
+        if (ObjectUtil.isEmpty(templates)) throw new RuntimeException("该行业无标签模板");
+        ArrayList<Map<String, String>> tags = new ArrayList<>();//标签及提示词
+        templates.forEach(o -> {
+            Map<String, String> tag = com.fs.hisapi.util.MapUtil.convertToMap(new CrmCustomerAiAutoTagVo(String.valueOf(o.getId()), o.getName(), o.getAiHint()));
+            tags.add(tag);
+        });
+        HashMap<String, Object> resultMap = new HashMap<>();
+        resultMap.put("tags", tags);
+        return resultMap;
+    }
+    private static final String TRADE_TYPE = "trade_type";
+
+    private static String getDictLabel(String tradeType) {
+        List<SysDictData> tradeTypeDict = DictUtils.getDictCache(TRADE_TYPE);
+        String dictLabel;
+        if (ObjectUtil.isEmpty(tradeTypeDict)) {
+            dictLabel = DictUtils.getDictLabel(TRADE_TYPE, tradeType);
+        } else {
+            Map<String, String> collect = tradeTypeDict.stream().collect(Collectors.toMap(SysDictData::getDictValue,
+                    SysDictData::getDictLabel, (v1, v2) -> v1)
+            );
+            dictLabel = collect.get(tradeType);
+        }
+        if (ObjectUtil.isEmpty(dictLabel)) {
+            throw new RuntimeException("字典中不存在该行业");
+        } else return dictLabel;
+    }
     private Map<String, Object> getUserInfo(Long customerId) {
         CrmCustomerAnalyze crmCustomerAnalyze = baseMapper.selectLatestOne(customerId);
         if (ObjectUtil.isEmpty(crmCustomerAnalyze)) {
             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(
@@ -634,4 +889,80 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         result.put("userInfo", userInfo);
         return result;
     }
+    private Map<String, Object> buildRequestParam(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson) {
+        Map<String, Object> requestParam = new HashMap<>();
+
+        // 获取各类数据
+        Map<String, Object> userInfo = getUserInfoQw(qwExternalAiAnalyze);
+        String likeRatio ="";
+        if (!userInfo.isEmpty()){
+            likeRatio = (String) userInfo.remove("likeRatio");
+        }
+        // 合并数据
+        requestParam.put("history",dataJson);
+        requestParam.putAll(userInfo);
+
+        // 设置其他参数
+        requestParam.put("tagInfos", Collections.emptyList());
+        requestParam.put("isRepository", "");
+        requestParam.put("userContent", "");
+        requestParam.put("aiContent", "");
+        requestParam.put("userIntent", likeRatio);
+
+        return requestParam;
+    }
+    private Map<String, Object> getUserInfoQw(QwExternalAiAnalyze qwExternalAiAnalyze) {
+        QwExternalAiAnalyze crmCustomerAnalyze = baseMapper.selectLatestQwAnalyze(qwExternalAiAnalyze);
+        if (ObjectUtil.isEmpty(crmCustomerAnalyze))throw new RuntimeException("客户信息不存在");
+        HashMap<String, String> userInfo = new HashMap<String, String>();
+        List<SysDictData> portraits = sysDictDataMapper.selectDictDataByType(AI_PORTRAIT);
+        List<String> dictValue = portraits.stream().map(SysDictData::getDictValue).collect(Collectors.toList());
+
+        if (crmCustomerAnalyze.getCustomerPortraitJson()!= null && !crmCustomerAnalyze.getCustomerPortraitJson().isEmpty()){
+            Map<String, String> portraitList = JSON.parseObject(
+                    crmCustomerAnalyze.getCustomerPortraitJson(),
+                    new TypeReference<Map<String, String>>() {}
+            );
+            userInfo.putAll(portraitList);
+
+        }else {
+            dictValue.forEach(o->{
+                userInfo.put(o, "");
+            });
+        }
+        HashMap<String, Object> result = new HashMap<>();
+        result.put("userInfo", userInfo);
+        return result;
+    }
+    private JsonNode responseAnalyze(R aiResponse,String analyseKey) throws JsonProcessingException {
+        JsonNode rootS = mapper.readTree(JSONUtil.toJsonStr(aiResponse));
+        JsonNode choices = rootS.path("data").path("choices");
+
+        if (choices.isArray() && choices.size() > 0) {
+            JsonNode contentNode = choices.get(0).path("message").path("content");
+
+            if (contentNode.isTextual()) {
+                String contentStr = contentNode.asText();
+                // 将content字符串解析为JsonNode
+                JsonNode contentArray = mapper.readTree(contentStr);
+
+                if (contentArray.isArray() && contentArray.size() > 1) {
+                    JsonNode secondElement = contentArray.get(1);
+                    JsonNode textNode = secondElement.path("text");
+
+                    if (!textNode.isMissingNode()) {
+                        JsonNode contentInnerNode = textNode.path("content");
+
+                        if (contentInnerNode.isTextual()) {
+                            String innerJsonStr = contentInnerNode.asText();
+                            JsonNode innerJson = mapper.readTree(innerJsonStr);
+                            JsonNode userInfo = innerJson.path(analyseKey);
+                            return userInfo;
+                        }
+                    }
+                }
+            }
+        }
+        return null;
+    }
 }

+ 9 - 2
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);
@@ -432,7 +439,7 @@ public class CrmCustomerAiTagUtil {
         return resultMap;
     }
 
-    private static String getDictLabel(String tradeType) {
+    public static String getDictLabel(String tradeType) {
         List<SysDictData> tradeTypeDict = DictUtils.getDictCache(TRADE_TYPE);
         String dictLabel;
         if (ObjectUtil.isEmpty(tradeTypeDict)) {

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

+ 4 - 4
fs-service/src/main/java/com/fs/fastGpt/mapper/FastGptChatMsgMapper.java

@@ -95,11 +95,11 @@ public interface FastGptChatMsgMapper
             "<if test = ' maps.status!=null  '> " +
             "and m.status = #{maps.status} " +
             "</if>" +
-            "<if test=' maps.params.beginTime != null and maps.params.beginTime != \"\" '>" +
-            "AND date_format(m.create_time,'%y%m%d') &gt;= date_format(#{maps.params.beginTime},'%y%m%d')  " +
+            "<if test=' maps.beginTime != null and maps.beginTime != \"\" '>" +
+            "AND date_format(m.create_time,'%y%m%d') &gt;= date_format(#{maps.beginTime},'%y%m%d')  " +
             "</if>" +
-            "<if test='maps.params.endTime != null and maps.params.endTime != \"\" '>" +
-            "AND date_format(m.create_time,'%y%m%d') &lt;= date_format(#{maps.params.endTime},'%y%m%d')  " +
+            "<if test='maps.endTime != null and maps.endTime != \"\" '>" +
+            "AND date_format(m.create_time,'%y%m%d') &lt;= date_format(#{maps.endTime},'%y%m%d')  " +
             "</if>" +
             " order by m.create_time desc "+
             "</script>"})

+ 91 - 0
fs-service/src/main/java/com/fs/qw/domain/QwCustomerProperty.java

@@ -0,0 +1,91 @@
+package com.fs.qw.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 客户属性对象 qw_customer_property
+ *
+ * @author fs
+ */
+@Data
+@TableName("qw_customer_property")
+public class QwCustomerProperty implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** id */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 客户ID */
+    @Excel(name = "客户ID")
+    private String externalUserId;
+
+    /** 字段ID */
+    @Excel(name = "字段ID")
+    private Long propertyId;
+
+    /** 字段名称 */
+    @Excel(name = "字段名称")
+    private String propertyName;
+
+    /** 字段内容 */
+    @Excel(name = "字段内容")
+    private String propertyValue;
+
+    /** 字段类型 */
+    @Excel(name = "字段类型")
+    private String propertyValueType;
+
+    /** 行业类型 */
+    @Excel(name = "行业类型")
+    private String tradeType;
+
+    /** 内容解析 */
+    private String aiAnalysis;
+
+    /** 意向等级: high/medium/low/none */
+    @Excel(name = "意向等级")
+    private String intention;
+
+    /** 喜欢占比:0-100 */
+    @Excel(name = "喜欢占比")
+    private Integer likeRatio;
+
+    /** 创建时间 */
+    private Date createTime;
+
+    /** 创建人 */
+    private String createBy;
+
+    /** 修改时间 */
+    private Date updateTime;
+
+    /** 修改人 */
+    private String updateBy;
+
+    /** 备注 */
+    private String remark;
+
+    /** 是否删除 0否 1是 */
+    private Integer deleted;
+
+    /** 删除人 */
+    private String deleteBy;
+
+    /** 删除时间 */
+    private Date deleteTime;
+
+    /** 企业id */
+    private String corpId;
+
+    /** 属于用户id */
+    private String qwUserId;
+}

+ 91 - 0
fs-service/src/main/java/com/fs/qw/domain/QwExternalAiAnalyze.java

@@ -0,0 +1,91 @@
+package com.fs.qw.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+import java.util.Date;
+
+/**
+ * 外部联系人 AI 分析 qw_external_ai_analyze
+ *
+ * @author fs
+ */
+@Data
+@TableName("qw_external_ai_analyze")
+public class QwExternalAiAnalyze implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 主键 */
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    /** 外部联系人 id */
+    @Excel(name = "外部联系人id")
+    private String externalUserId;
+
+    /** 客户姓名 */
+    @Excel(name = "客户姓名")
+    private String customerName;
+
+    /** 客户画像 */
+    @Excel(name = "客户画像")
+    private String customerPortraitJson;
+
+    /** 沟通摘要 */
+    @Excel(name = "沟通摘要")
+    private String communicationAbstract;
+
+    /** 沟通总结 */
+    @Excel(name = "沟通总结")
+    private String communicationSummary;
+
+    /**
+     * 流失风险等级 0:未知;1:无风险;2:低风险;3:中风险;4:高风险
+     */
+    @Excel(name = "流失风险等级")
+    private Long attritionLevel;
+
+    /** 流失风险等级提示 */
+    @Excel(name = "流失风险等级提示")
+    private String attritionLevelPrompt;
+
+    /** 客户关注点 */
+    @Excel(name = "客户关注点")
+    private String customerFocusJson;
+
+    /** 意向度 */
+    @Excel(name = "意向度")
+    private String intentionDegree;
+
+    /** AI 通话 / 聊天记录 */
+    @Excel(name = "AI聊天记录")
+    private String aiChatRecord;
+
+    /** 创建时间 */
+    @Excel(name = "创建时间")
+    private Date createTime;
+
+    /** 备注 */
+    @Excel(name = "备注")
+    private String remark;
+
+    /** 预留数字型字段 */
+    private Long reserveInt;
+
+    /** 预留字符串型字段 */
+    private String reserveStr;
+
+    /** 企业 id */
+    private String corpId;
+
+    /** 企微用户 id(内部员工) */
+    private String qwUserId;
+
+    @Excel(name = "会话id")
+    private Long sessionId;
+}

+ 38 - 0
fs-service/src/main/java/com/fs/qw/domain/QwExternalAiAnalyzeSession.java

@@ -0,0 +1,38 @@
+package com.fs.qw.domain;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fs.common.annotation.Excel;
+import lombok.Data;
+
+import java.io.Serializable;
+
+/**
+ * 外部联系人 AI 分析会话绑定表 qw_external_ai_analyze_session
+ *
+ * @author fs
+ */
+@Data
+@TableName("qw_external_ai_analyze_session")
+public class QwExternalAiAnalyzeSession implements Serializable {
+
+    private static final long serialVersionUID = 1L;
+
+    /** 会话主键 */
+    @TableId(value = "session_id", type = IdType.AUTO)
+    @Excel(name = "会话id")
+    private Long sessionId;
+
+    /** 外部联系人 id */
+    @Excel(name = "外部联系人id")
+    private String externalUserId;
+
+    /** 企业 id */
+    @Excel(name = "企业id")
+    private String corpId;
+
+    /** 属于用户 id */
+    @Excel(name = "属于用户id")
+    private String qwUserId;
+}

+ 98 - 0
fs-service/src/main/java/com/fs/qw/domain/audit/QwMsgAuditMessage.java

@@ -0,0 +1,98 @@
+package com.fs.qw.domain.audit;
+
+import com.baomidou.mybatisplus.annotation.TableField;
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 企业微信会话存档结构化消息 qw_msg_audit_message
+ */
+@Data
+public class QwMsgAuditMessage {
+    private Long id;
+    private String corpId;
+    private Long seq;
+    private String msgId;
+    private Long msgTime;
+    private String msgType;
+
+    /**
+     * 发送方 from,冗余,对应明文 JSON 字段 from
+     */
+    private String fromUser;
+
+    /**
+     * 接收方列表,JSON,冗余,对应 tolist
+     */
+    private String toList;
+
+    /**
+     * 群聊 roomid,冗余,单聊可为空
+     */
+    private String roomId;
+
+    /**
+     * 会话维度键:群为 corpId:room:roomId;单聊为 corpId:p2p:参与方id字典序拼接,便于按会话查询
+     */
+    private String conversationKey;
+
+    /**
+     * 发送人身份:1 企业内部(销售) 2 企业外部
+     */
+    private Integer fromUserRole;
+
+    /**
+     * text.content
+     */
+    private String textContent;
+
+    /**
+     * 媒体通用字段(语音/图片/视频/表情等),字段名与表 media_sdkfileid / media_md5sum 及 Mapper 一致
+     */
+    private String mediaSdkfileid;
+    private String mediaMd5sum;
+    private Integer mediaSize;
+    private Integer mediaPlayLength;
+    private Integer mediaWidth;
+    private Integer mediaHeight;
+    private String mediaFileName;
+    private String mediaFileExt;
+
+    /**
+     * 媒体文件上传至 OSS 后的访问 URL(语音/视频)
+     */
+    private String mediaOssUrl;
+
+    /**
+     * 关联原始表
+     */
+    private Long rawId;
+
+    private Date createTime;
+
+    /**
+     * 销售id
+     */
+    @TableField(exist = false)
+    private String qwUserId;
+    /**
+     * 聊天区域,single-个人;group-群聊
+     */
+    @TableField(exist = false)
+    private String chatScope;
+
+    /**
+     * 发消息人名称
+     */
+    @TableField(exist = false)
+    private String fromUserName;
+
+    /**
+     * 进行ai分析的开始时间
+     */
+    @TableField(exist = false)
+    private Long analyzeStartTime;
+
+}
+

+ 31 - 0
fs-service/src/main/java/com/fs/qw/domain/audit/QwMsgAuditRaw.java

@@ -0,0 +1,31 @@
+package com.fs.qw.domain.audit;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 企业微信会话存档原始明文(解密后) qw_msg_audit_raw
+ */
+@Data
+public class QwMsgAuditRaw {
+    private Long id;
+    private String corpId;
+    private Long seq;
+    private String msgId;
+    private String action;
+    private String fromUser;
+    /**
+     * 逗号分隔或 JSON 字符串(按现有项目风格存 String)
+     */
+    private String toList;
+    private String roomId;
+    private Long msgTime;
+    private String msgType;
+    /**
+     * 解密后的完整 JSON
+     */
+    private String rawJson;
+    private Date createTime;
+}
+

+ 21 - 0
fs-service/src/main/java/com/fs/qw/domain/audit/QwMsgAuditSeq.java

@@ -0,0 +1,21 @@
+package com.fs.qw.domain.audit;
+
+import lombok.Data;
+
+import java.util.Date;
+
+/**
+ * 企业微信会话存档拉取进度(seq) qw_msg_audit_seq
+ */
+@Data
+public class QwMsgAuditSeq {
+    private Long id;
+    private String corpId;
+    /**
+     * 已成功处理到的最大 seq
+     */
+    private Long seq;
+    private Date createTime;
+    private Date updateTime;
+}
+

+ 14 - 0
fs-service/src/main/java/com/fs/qw/dto/QwMsgAuditConversationDTO.java

@@ -0,0 +1,14 @@
+package com.fs.qw.dto;
+
+import lombok.Data;
+
+/**
+ * 单聊会话列表查询行
+ */
+@Data
+public class QwMsgAuditConversationDTO {
+
+    private String conversationKey;
+
+    private Long lastMsgTime;
+}

+ 20 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwCustomerPropertyMapper.java

@@ -0,0 +1,20 @@
+package com.fs.qw.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.qw.domain.QwCustomerProperty;
+import com.fs.qw.vo.QwCustomerAiTagVo;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * 客户属性 Mapper
+ *
+ * @author fs
+ */
+@Repository
+public interface QwCustomerPropertyMapper extends BaseMapper<QwCustomerProperty> {
+    void insertBatch(List<QwCustomerAiTagVo> results);
+
+    List<QwCustomerProperty> selectQwCustomerPropertyList(QwCustomerProperty qwCustomerProperty);
+}

+ 20 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwExternalAiAnalyzeMapper.java

@@ -0,0 +1,20 @@
+package com.fs.qw.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.qw.domain.QwExternalAiAnalyze;
+import org.springframework.stereotype.Repository;
+
+import java.util.List;
+
+/**
+ * 外部联系人 AI 分析 Mapper
+ *
+ * @author fs
+ */
+@Repository
+public interface QwExternalAiAnalyzeMapper extends BaseMapper<QwExternalAiAnalyze> {
+    int insertBatch(List<QwExternalAiAnalyze> qwExternalAiAnalyzes);
+
+    List<QwExternalAiAnalyze> selectQwExternalAiAnalyzeList(QwExternalAiAnalyze crmCustomerAnalyze);
+
+}

+ 21 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwExternalAiAnalyzeSessionMapper.java

@@ -0,0 +1,21 @@
+package com.fs.qw.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.qw.domain.QwExternalAiAnalyzeSession;
+import org.apache.ibatis.annotations.Param;
+import org.apache.ibatis.annotations.Select;
+import org.springframework.stereotype.Repository;
+
+/**
+ * 外部联系人 AI 分析会话绑定 Mapper
+ *
+ * @author fs
+ */
+@Repository
+public interface QwExternalAiAnalyzeSessionMapper extends BaseMapper<QwExternalAiAnalyzeSession> {
+    @Select("select * from qw_external_ai_analyze_session " +
+            "where external_user_id = #{externalUserId} and corp_id = #{corpId} and qw_user_id = #{qwUserId} limit 1")
+    QwExternalAiAnalyzeSession selectByUniqueKey(@Param("externalUserId") String externalUserId,
+                                                 @Param("corpId") String corpId,
+                                                 @Param("qwUserId") String qwUserId);
+}

+ 90 - 0
fs-service/src/main/java/com/fs/qw/mapper/QwMsgAuditMessageMapper.java

@@ -0,0 +1,90 @@
+package com.fs.qw.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.qw.domain.audit.QwMsgAuditMessage;
+import com.fs.qw.dto.QwMsgAuditConversationDTO;
+import org.apache.ibatis.annotations.Param;
+
+import java.util.List;
+
+/**
+ * 企微会话存档结构化消息Mapper接口
+ *
+ * @author fs
+ * @date 2026-03-19
+ */
+public interface QwMsgAuditMessageMapper extends BaseMapper<QwMsgAuditMessage>{
+    /**
+     * 查询企微会话存档结构化消息
+     *
+     * @param id 企微会话存档结构化消息主键
+     * @param shard
+     * @return 企微会话存档结构化消息
+     */
+    QwMsgAuditMessage selectQwMsgAuditMessageById(@Param("id") Long id, @Param("shard") int shard);
+
+    /**
+     * 查询企微会话存档结构化消息列表
+     *
+     * @param qwMsgAuditMessage 企微会话存档结构化消息
+     * @return 企微会话存档结构化消息集合
+     */
+    List<QwMsgAuditMessage> selectQwMsgAuditMessageList(QwMsgAuditMessage qwMsgAuditMessage);
+
+    /**
+     * 按指定分片全量查询企微会话存档结构化消息列表
+     *
+     * @param shard 分片号
+     * @param qwMsgAuditMessage 查询条件
+     * @return 企微会话存档结构化消息集合
+     */
+    List<QwMsgAuditMessage> selectQwMsgAuditMessageListByShard(@Param("shard") int shard,
+                                                               @Param("qwMsgAuditMessage") QwMsgAuditMessage qwMsgAuditMessage);
+
+    /**
+     * 新增企微会话存档结构化消息
+     *
+     * @param qwMsgAuditMessage 企微会话存档结构化消息
+     * @return 结果
+     */
+    int insertQwMsgAuditMessage(QwMsgAuditMessage qwMsgAuditMessage);
+
+    /**
+     * 修改企微会话存档结构化消息
+     *
+     * @param qwMsgAuditMessage 企微会话存档结构化消息
+     * @return 结果
+     */
+    int updateQwMsgAuditMessage(QwMsgAuditMessage qwMsgAuditMessage);
+
+    /**
+     * 删除企微会话存档结构化消息
+     *
+     * @param id 企微会话存档结构化消息主键
+     * @param shard
+     * @return 结果
+     */
+    int deleteQwMsgAuditMessageById(@Param("id") Long id, @Param("shard") int shard);
+
+    /**
+     * 批量删除企微会话存档结构化消息
+     *
+     * @param ids 需要删除的数据主键集合
+     * @param shard
+     * @return 结果
+     */
+    int deleteQwMsgAuditMessageByIdsInShard(@Param("ids") Long[] ids, @Param("shard") int shard);
+
+    QwMsgAuditMessage selectByCorpIdAndSeq(@Param("corpId") String corpId, @Param("seq") Long seq);
+
+    /**
+     * 单聊会话列表:按 conversation_key 分组,取最后消息时间(分页会话列表)
+     */
+    List<QwMsgAuditConversationDTO> selectSingleConversationList(@Param("corpId") String corpId, @Param("qwUserId") String qwUserId);
+
+    /**
+     * 查询指定企业下指定消息类型中 media_oss_url 为空的记录(待下载上传)
+     */
+    List<QwMsgAuditMessage> selectMediaPendingOssUpload(@Param("corpId") String corpId, @Param("msgType") String msgType, @Param("limit") int limit);
+
+}

+ 23 - 0
fs-service/src/main/java/com/fs/qw/param/audit/QwAiTagGainParam.java

@@ -0,0 +1,23 @@
+package com.fs.qw.param.audit;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Accessors(chain = true)
+public class QwAiTagGainParam {
+    @ApiModelProperty(value = "外部联系人id")
+    private String externalUserId;
+    @ApiModelProperty(value = "企业id")
+    private String corpId;
+    @ApiModelProperty(value = "属于用户企微id")
+    private String qwUserId;
+    //crm_customer_property_template的 trade_type
+    @ApiModelProperty("行业")
+    private String tradeType;
+}

+ 25 - 0
fs-service/src/main/java/com/fs/qw/param/audit/QwAuditMessagebackupParam.java

@@ -0,0 +1,25 @@
+package com.fs.qw.param.audit;
+
+import io.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+@Data
+@AllArgsConstructor
+@NoArgsConstructor
+@Accessors(chain = true)
+public class QwAuditMessagebackupParam {
+    @ApiModelProperty(value = "聊天记录")
+    private String history;
+
+    @ApiModelProperty(value = "外部联系人userid")
+    private String externalUserId;
+
+    @ApiModelProperty(value = "企业id")
+    private String corpId;
+
+    @ApiModelProperty(value = "企业微信用户id")
+    private String qwUserId;
+}

+ 19 - 0
fs-service/src/main/java/com/fs/qw/service/IQwCustomerPropertyService.java

@@ -0,0 +1,19 @@
+package com.fs.qw.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.common.core.domain.R;
+import com.fs.qw.domain.QwCustomerProperty;
+import com.fs.qw.domain.QwExternalAiAnalyze;
+
+import java.util.List;
+
+/**
+ * 客户属性 Service
+ *
+ * @author fs
+ */
+public interface IQwCustomerPropertyService extends IService<QwCustomerProperty> {
+    List<QwCustomerProperty> selectQwCustomerPropertyList(QwCustomerProperty qwCustomerProperty);
+
+    R analyzeAiTagByTrade(String param, QwExternalAiAnalyze aiAnalyze);
+}

+ 168 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwCustomerPropertyServiceImpl.java

@@ -0,0 +1,168 @@
+package com.fs.qw.service.impl;
+
+import cn.hutool.core.util.ObjectUtil;
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONArray;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.domain.entity.SysDictData;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.crm.domain.CrmCustomerPropertyTemplate;
+import com.fs.crm.dto.CrmCustomerAiAutoTagVo;
+import com.fs.crm.service.ICrmCustomerPropertyTemplateService;
+import com.fs.crm.utils.CrmCustomerAiTagUtil;
+import com.fs.hisapi.util.MapUtil;
+import com.fs.qw.domain.QwCustomerProperty;
+import com.fs.qw.domain.QwExternalAiAnalyze;
+import com.fs.qw.mapper.QwCustomerPropertyMapper;
+import com.fs.qw.mapper.QwExternalAiAnalyzeMapper;
+import com.fs.qw.service.IQwCustomerPropertyService;
+import com.fs.qw.vo.QwCustomerAiTagVo;
+import com.fs.system.mapper.SysDictDataMapper;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import static com.fs.crm.utils.CrmCustomerAiTagUtil.getDictLabel;
+
+/**
+ * 客户属性 Service 实现
+ *
+ * @author fs
+ */
+@Service
+public class QwCustomerPropertyServiceImpl
+        extends ServiceImpl<QwCustomerPropertyMapper, QwCustomerProperty>
+        implements IQwCustomerPropertyService {
+    @Autowired
+    private QwExternalAiAnalyzeMapper qwExternalAiAnalyzeMapper;
+
+    @Autowired
+    private QwCustomerPropertyMapper qwCustomerPropertyMapper;
+
+
+    @Override
+    public List<QwCustomerProperty> selectQwCustomerPropertyList(QwCustomerProperty qwCustomerProperty) {
+        return baseMapper.selectQwCustomerPropertyList(qwCustomerProperty);
+    }
+
+    @Value("${crm.customer.ai.key:mygpt-iTUua2CHVd4WGrBbQQGl1HHjyyBAD1KuXARsxHj5eHpLYv5CfnOh8iwVU}")
+    private String appKey;
+    @Override
+    public R analyzeAiTagByTrade(String param, QwExternalAiAnalyze aiAnalyze) {
+
+        QwCustomerProperty property = new QwCustomerProperty();
+        property.setCorpId(aiAnalyze.getCorpId());
+        property.setQwUserId(aiAnalyze.getQwUserId());
+        property.setExternalUserId(aiAnalyze.getExternalUserId());
+//        List<QwCustomerProperty> list = baseMapper.selectQwCustomerPropertyList(property);
+//        if (CollectionUtil.isEmpty(list)){
+        List<QwCustomerAiTagVo> aiTagList = getAiTagList(aiAnalyze, param);
+        if (CollectionUtils.isEmpty(aiTagList))return R.ok("未分析出AI标签");
+        qwCustomerPropertyMapper.insertBatch(aiTagList);
+//        }else{
+//            List<QwCustomerAiTagVo> aiTagList = getAiTagList(aiAnalyze, param.getTradeType());
+//        }
+//        CrmCustomerAiTagUtil
+
+        return R.ok();
+    }
+
+    private List<QwCustomerAiTagVo> getAiTagList(QwExternalAiAnalyze aiAnalyze, String tradeType) {
+        Map<String, Object> requestParam = new HashMap<>();
+        String tradeName = getDictLabel(tradeType);
+
+        List<CrmCustomerPropertyTemplate> templates = SpringUtils.getBean(ICrmCustomerPropertyTemplateService.class).getBaseMapper().selectList(new LambdaQueryWrapper<CrmCustomerPropertyTemplate>().eq(
+                CrmCustomerPropertyTemplate::getTradeType, tradeType
+        ));
+        if (ObjectUtil.isEmpty(templates)) throw new RuntimeException("该行业无标签模板");
+        ArrayList<Map<String, String>> tags = new ArrayList<>();//标签及提示词
+        templates.forEach(o -> {
+            Map<String, String> tag = MapUtil.convertToMap(new CrmCustomerAiAutoTagVo(String.valueOf(o.getId()), o.getName(), o.getAiHint()));
+            tags.add(tag);
+        });
+        requestParam.put("tags", tags);
+
+        JSONArray objects = JSONArray.parseArray(aiAnalyze.getAiChatRecord());
+        StringBuilder result = new StringBuilder("{");
+        if (objects != null) {
+            objects.stream().iterator().forEachRemaining(item -> {
+                if (result.length() > 1) result.append(",");
+                result.append(item.toString());
+            });
+            result.append("}");
+            requestParam.put("history", result.toString());
+        }
+
+        HashMap<String, String> userInfo = new HashMap<String, String>();
+        List<SysDictData> portraits = SpringUtils.getBean(SysDictDataMapper.class).selectDictDataByType("crm_ai_portrait");
+        List<String> dictValue = portraits.stream().map(SysDictData::getDictValue).collect(Collectors.toList());
+        if (aiAnalyze.getCustomerPortraitJson()!= null && !aiAnalyze.getCustomerPortraitJson().isEmpty()){
+            Map<String, String> portraitList = JSON.parseObject(
+                    aiAnalyze.getCustomerPortraitJson(),
+                    new cn.hutool.core.lang.TypeReference<Map<String, String>>() {}
+            );
+            portraitList.keySet().removeIf(k -> k.matches(".*[a-zA-Z].*"));
+            userInfo.putAll(portraitList);
+
+        }else {
+            dictValue.forEach(o->{
+                userInfo.put(o, "");
+            });
+        }
+        requestParam.put("userInfo", userInfo);
+
+        HashMap<String, String> aiInfo = new HashMap<>();
+        aiInfo.put("name", "");
+        aiInfo.put("sex", "");
+        aiInfo.put("age", "");
+        aiInfo.put("city", "");
+        aiInfo.put("habits", "");
+        aiInfo.put("describe", "");
+        requestParam.put("aiInfo", aiInfo);
+
+        requestParam.put("tradeName", tradeName);
+        requestParam.put("tradeType", tradeType);
+        requestParam.put("tagInfos", Collections.emptyList());
+        requestParam.put("isRepository", "");
+        requestParam.put("userContent", "");
+        requestParam.put("aiContent", "");
+        requestParam.put("likeRatio", "");
+        requestParam.put("modelType","ai标签");
+
+        R aiResponse = CrmCustomerAiTagUtil.callAiService(requestParam, aiAnalyze.getSessionId(), appKey);
+
+        if (aiResponse == null || !Integer.valueOf(200).equals(aiResponse.get("code"))) {
+            throw new RuntimeException("AI响应异常: " +
+                    (aiResponse != null ? aiResponse.get("msg") : "响应为空"));
+        }
+
+        List<Map<String, String>> tagInfos = CrmCustomerAiTagUtil.extractTagInfos(JSONUtil.toJsonStr(aiResponse));
+        if (CollectionUtils.isEmpty(tagInfos)) {
+            return Collections.emptyList();
+        }
+
+        return tagInfos.stream()
+                .map(tag -> buildTagVo(tag, aiAnalyze,tradeType))
+                .collect(Collectors.toList());
+    }
+    private static QwCustomerAiTagVo buildTagVo(Map<String, String> tag, QwExternalAiAnalyze aiAnalyze, String tradeType) {
+        QwCustomerAiTagVo vo = new QwCustomerAiTagVo();
+        vo.setCorpId(aiAnalyze.getCorpId());
+        vo.setQwUserId(aiAnalyze.getQwUserId());
+        vo.setExternalUserId(aiAnalyze.getExternalUserId());
+        vo.setPropertyId(tag.get("id"));
+        vo.setPropertyName(tag.get("name"));
+        vo.setPropertyValue(tag.get("value"));
+        vo.setTradeType(tradeType);
+        vo.setCreateTime(LocalDateTime.now());
+        return vo;
+    }
+}

+ 63 - 0
fs-service/src/main/java/com/fs/qw/shardingConfig/QwMsgAuditMessageSharding.java

@@ -0,0 +1,63 @@
+package com.fs.qw.shardingConfig;
+
+import org.springframework.util.StringUtils;
+
+import java.nio.charset.StandardCharsets;
+import java.util.zip.CRC32;
+
+/**
+ * 会话存档表分片:qw_msg_audit_message_0.._11、qw_msg_audit_raw_0.._11;
+ */
+
+public final class QwMsgAuditMessageSharding {
+
+    public static final int SHARD_COUNT = 12;
+
+    public static final String TABLE_PREFIX = "qw_msg_audit_message_";
+
+    public static final String RAW_TABLE_PREFIX = "qw_msg_audit_raw_";
+
+    private QwMsgAuditMessageSharding() {
+    }
+
+    /**
+
+     * @return 0 .. SHARD_COUNT-1,corpId 为空时返回 0(避免 NPE,调用方应校验 corpId)
+
+     */
+    public static int shardIndex(String corpId) {
+
+        if (!StringUtils.hasText(corpId)) {
+            return 0;
+        }
+        CRC32 crc = new CRC32();
+        crc.update(corpId.getBytes(StandardCharsets.UTF_8));
+        long v = crc.getValue();
+        return (int) (v % SHARD_COUNT);
+    }
+
+    public static String physicalTableName(int shard) {
+
+        if (shard < 0 || shard >= SHARD_COUNT) {
+            throw new IllegalArgumentException("shard out of range: " + shard);
+        }
+        return TABLE_PREFIX + shard;
+    }
+
+    public static String physicalTableName(String corpId) {
+        return physicalTableName(shardIndex(corpId));
+    }
+
+    public static String rawPhysicalTableName(int shard) {
+        if (shard < 0 || shard >= SHARD_COUNT) {
+            throw new IllegalArgumentException("shard out of range: " + shard);
+        }
+        return RAW_TABLE_PREFIX + shard;
+    }
+
+    public static boolean isValidShard(int shard) {
+        return shard >= 0 && shard < SHARD_COUNT;
+    }
+
+}
+

+ 25 - 0
fs-service/src/main/java/com/fs/qw/vo/QwCustomerAiTagVo.java

@@ -0,0 +1,25 @@
+package com.fs.qw.vo;
+
+import lombok.Data;
+import lombok.experimental.Accessors;
+
+import java.time.LocalDateTime;
+
+@Data
+@Accessors(chain = true)
+public class QwCustomerAiTagVo {
+    private String propertyId; // 标签id
+    private String propertyName; // 标签名称
+    private String propertyValue; // 标签值
+    private String externalUserId;
+    /** 企业 id */
+    private String corpId;
+
+    /** 企微用户 id(内部员工) */
+    private String qwUserId;
+    /**
+     * 行业
+     */
+    private String tradeType;
+    private LocalDateTime createTime;
+}

+ 5 - 0
fs-service/src/main/java/com/fs/tenant/domain/TenantInfo.java

@@ -96,4 +96,9 @@ public class TenantInfo {
     private Integer companyNum;
 
     private Integer accountNum;
+
+    private String feePlanCode;
+
+
+    private String billingMode;
 }

+ 9 - 0
fs-service/src/main/java/com/fs/tenant/dto/SopTokenDto.java

@@ -0,0 +1,9 @@
+package com.fs.tenant.dto;
+
+import lombok.Data;
+
+@Data
+public class SopTokenDto {
+    private int type;
+    private int count;
+}

+ 14 - 0
fs-service/src/main/java/com/fs/tenant/dto/TenantBillDto.java

@@ -0,0 +1,14 @@
+package com.fs.tenant.dto;
+
+import com.fs.billing.domain.FeePlanItem;
+import com.fs.tenant.domain.TenantInfo;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+public class TenantBillDto {
+    private TenantInfo tenantInfo;
+
+    private List<FeePlanItem> feePlanItems;
+}

+ 29 - 0
fs-service/src/main/java/com/fs/tenant/enums/FeeItemEnum.java

@@ -0,0 +1,29 @@
+package com.fs.tenant.enums;
+
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+@Getter
+@AllArgsConstructor
+public enum FeeItemEnum {
+    // 流量类
+    FLOW_POSTPAID("流量后付费"),
+
+    // 通话类
+    CALL_OUT("外呼通话"),
+    CALL_IN("呼入通话"),
+    AI_CALL("AI外呼附加费"),
+
+    // Token类
+    SOP_TOKEN("SOP Token"),
+    AI_REPLY_TOKEN("AI回复Token"),
+
+    // 开户类
+    OPEN_ACCOUNT_NON_AI("开户费-非AI"),
+    OPEN_ACCOUNT_AI("开户费-AI"),
+
+    // 其他
+    ADD_WECHAT("加微数量");
+
+    private final String desc;
+}

+ 17 - 0
fs-service/src/main/java/com/fs/tenant/mapper/TenantInfoMapper.java

@@ -1,10 +1,15 @@
 package com.fs.tenant.mapper;
 
+import cn.hutool.core.date.DateTime;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.fs.billing.domain.FeePlanItem;
 import com.fs.common.core.domain.entity.SysMenu;
 
 import com.fs.common.core.domain.entity.TenantCompanyMenu;
+import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.qw.domain.QwRestrictionPushRecord;
 import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.dto.SopTokenDto;
 import com.fs.tenant.vo.TenantInfoShowVo;
 import org.apache.ibatis.annotations.Param;
 
@@ -126,6 +131,18 @@ public interface TenantInfoMapper extends BaseMapper<TenantInfo> {
     int addComMenu(List<TenantCompanyMenu> addCompanyMenu);
 
     TenantInfo getTenByCode(String code);
+
+    List<FeePlanItem> selectFeeItem(String feePlanCode);
+
+    String getYesterDayTraffic(@Param("yesterDayBegin") DateTime yesterDayBegin,@Param("yesterdayEnd") DateTime yesterdayEnd);
+
+    List<CompanyVoiceRoboticCallLogCallphone> getYesterCallOut(@Param("yesterDayBegin") DateTime yesterDayBegin,@Param("yesterdayEnd") DateTime yesterdayEnd);
+
+    int getYesterAddWechat(@Param("yesterDayBegin") DateTime yesterDayBegin,@Param("yesterdayEnd") DateTime yesterdayEnd);
+
+    long getYesterAiReplyToken(@Param("yesterDayBegin") DateTime yesterDayBegin,@Param("yesterdayEnd") DateTime yesterdayEnd);
+
+    List<SopTokenDto> getYesterSopToken(@Param("yesterDayBegin") DateTime yesterDayBegin, @Param("yesterdayEnd") DateTime yesterdayEnd);
 }
 
 

+ 16 - 0
fs-service/src/main/java/com/fs/tenant/service/TenantInfoService.java

@@ -1,6 +1,8 @@
 package com.fs.tenant.service;
 
+import cn.hutool.core.date.DateTime;
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.billing.domain.FeePlanItem;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.entity.SysMenu;
 ;
@@ -8,7 +10,9 @@ import com.fs.common.core.domain.entity.TenantCompanyMenu;
 import com.fs.tenant.domain.TenantInfo;
 import com.fs.tenant.vo.TenantInfoShowVo;
 
+import java.math.BigDecimal;
 import java.util.List;
+import java.util.Map;
 
 /**
 * @author Administrator
@@ -101,4 +105,16 @@ public interface TenantInfoService extends IService<TenantInfo> {
     boolean hasChildByComMenuId(Long menuId);
 
     int deleteComMenuById(Long menuId);
+
+    List<FeePlanItem> selectFeeItem(String feePlanCode);
+
+    Map<String, Object>  getYesterDayTraffic(DateTime yesterDayBegin, DateTime yesterdayEnd, FeePlanItem item,TenantInfo tenant);
+
+    Map<String, Object> getYesterCallOut(DateTime yesterDayBegin, DateTime yesterdayEnd, FeePlanItem item,BigDecimal aiCallFee,TenantInfo tenant);
+
+    Map<String, Object> getYesterAddWechat(DateTime yesterDayBegin, DateTime yesterdayEnd, FeePlanItem item,TenantInfo tenant);
+
+    Map<String, Object> getYesterAiReplyToken(DateTime yesterDayBegin, DateTime yesterdayEnd, FeePlanItem item, TenantInfo tenant);
+
+    Map<String, Object> getYesterSopToken(DateTime yesterDayBegin, DateTime yesterdayEnd, FeePlanItem item,TenantInfo tenant);
 }

+ 143 - 0
fs-service/src/main/java/com/fs/tenant/service/impl/TenantInfoServiceImpl.java

@@ -1,18 +1,30 @@
 package com.fs.tenant.service.impl;
 
+import cn.hutool.core.bean.BeanUtil;
+import cn.hutool.core.date.DateTime;
+import cn.hutool.core.date.DateUtil;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.billing.domain.BillingDetail;
+import com.fs.billing.domain.FeePlanItem;
+import com.fs.billing.domain.UsageEvent;
+import com.fs.billing.mapper.BillingDetailMapper;
+import com.fs.billing.mapper.UsageEventMapper;
 import com.fs.common.constant.UserConstants;
 import com.fs.common.core.domain.R;
 import com.fs.common.core.domain.TreeSelect;
 import com.fs.common.core.domain.entity.SysMenu;
 import com.fs.common.core.domain.entity.TenantCompanyMenu;
+import com.fs.common.enums.DataSourceType;
 import com.fs.common.exception.CustomException;
 import com.fs.common.utils.DateUtils;
 import com.fs.common.utils.StringUtils;
 
+import com.fs.company.domain.CompanyVoiceRoboticCallLogCallphone;
+import com.fs.qw.domain.QwRestrictionPushRecord;
 import com.fs.system.service.ISysMenuService;
 import com.fs.tenant.domain.TenantInfo;
+import com.fs.tenant.dto.SopTokenDto;
 import com.fs.tenant.mapper.TenantInfoMapper;
 import com.fs.tenant.service.TenantAsyncService;
 import com.fs.tenant.service.TenantInfoService;
@@ -23,8 +35,10 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 import org.springframework.util.CollectionUtils;
 
+import java.math.BigDecimal;
 import java.sql.Connection;
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import java.util.stream.Collectors;
@@ -41,6 +55,10 @@ public class TenantInfoServiceImpl extends ServiceImpl<TenantInfoMapper, TenantI
     private TenantAsyncService tenantAsyncService;
     @Autowired
     private ISysMenuService menuService;
+    @Autowired
+    private UsageEventMapper usageEventMapper;
+    @Autowired
+    private BillingDetailMapper billingDetailMapper;
 
     /**
      * 查询租户基础信息
@@ -389,6 +407,131 @@ public class TenantInfoServiceImpl extends ServiceImpl<TenantInfoMapper, TenantI
     public int deleteComMenuById(Long menuId) {
         return baseMapper.deleteComMenuById(menuId);
     }
+
+    @Override
+    public List<FeePlanItem> selectFeeItem(String feePlanCode) {
+        return baseMapper.selectFeeItem(feePlanCode);
+    }
+
+    @Override
+    public Map<String, Object> getYesterDayTraffic(DateTime yesterDayBegin, DateTime yesterdayEnd, FeePlanItem item, TenantInfo tenant) {
+        // 查询流量
+        String traffic = baseMapper.getYesterDayTraffic(yesterDayBegin, yesterdayEnd);
+
+        BigDecimal trafficB = traffic == null ? BigDecimal.ZERO : new BigDecimal(traffic);
+
+        // 根据计费方案单位,转换成对应单位
+        String unit = item.getUnit();
+        BigDecimal usage = BigDecimal.ZERO;
+
+        switch (unit) {
+            case "MB":
+                usage = trafficB.divide(new BigDecimal("1024"), 6, BigDecimal.ROUND_HALF_UP)
+                        .divide(new BigDecimal("1024"), 6, BigDecimal.ROUND_HALF_UP);
+                break;
+            case "GB":
+                usage = trafficB.divide(new BigDecimal("1024"), 6, BigDecimal.ROUND_HALF_UP)
+                        .divide(new BigDecimal("1024"), 6, BigDecimal.ROUND_HALF_UP)
+                        .divide(new BigDecimal("1024"), 6, BigDecimal.ROUND_HALF_UP);
+                break;
+            default:
+                usage = trafficB.divide(new BigDecimal("1024"), 6, BigDecimal.ROUND_HALF_UP);
+                break;
+        }
+
+        // 计算费用
+        BigDecimal totalPrice = usage.multiply(item.getUnitPrice()).setScale(6, BigDecimal.ROUND_HALF_UP);
+
+        // 返回结果
+        Map<String, Object> map = new HashMap<>();
+        map.put("usage", usage);
+        map.put("price", totalPrice);
+        return map;
+    }
+
+    @Override
+    public Map<String, Object> getYesterCallOut(DateTime yesterDayBegin, DateTime yesterdayEnd, FeePlanItem item,BigDecimal aiCallFee,TenantInfo tenant) {
+        List<CompanyVoiceRoboticCallLogCallphone> list = baseMapper.getYesterCallOut(yesterDayBegin, yesterdayEnd);
+
+        // 初始金额 0
+        BigDecimal totalPrice = BigDecimal.ZERO;
+        long minutes = 0;
+        for (CompanyVoiceRoboticCallLogCallphone c : list) {
+            // 通话时长(不足1分钟按1分钟算)
+            Long callTime = c.getCallTime();
+            if (callTime == null || callTime <= 0) {
+                continue; // 空或0秒跳过
+            }
+            minutes = (callTime + 60 - 1) / 60;
+
+            // 计算单价
+            BigDecimal itemPrice = 2 == c.getCallType() ?
+                    item.getUnitPrice().add(aiCallFee).multiply(new BigDecimal(minutes))
+                    : item.getUnitPrice().multiply(new BigDecimal(minutes));
+
+            totalPrice = totalPrice.add(itemPrice);
+        }
+
+        BigDecimal totalFee = totalPrice.setScale(6, BigDecimal.ROUND_HALF_UP);
+        Map<String, Object> map = new HashMap<>();
+        map.put("usage", minutes);
+        map.put("price", totalFee);
+        return map;
+    }
+
+    @Override
+    public Map<String, Object> getYesterAddWechat(DateTime yesterDayBegin, DateTime yesterdayEnd, FeePlanItem item,TenantInfo tenant) {
+        int count = 0;
+        count = baseMapper.getYesterAddWechat(yesterDayBegin, yesterdayEnd);
+        BigDecimal totalFee = BigDecimal.ZERO;
+        if (count > 0) {
+            totalFee = item.getUnitPrice().multiply(new BigDecimal(count)).setScale(6, BigDecimal.ROUND_HALF_UP);
+        }
+
+        Map<String, Object> map = new HashMap<>();
+        map.put("usage", count);
+        map.put("price", totalFee);
+        return map;
+    }
+
+    @Override
+    public Map<String, Object> getYesterAiReplyToken(DateTime yesterDayBegin, DateTime yesterdayEnd, FeePlanItem item,TenantInfo tenant) {
+        Map<String, Object> map = new HashMap<>();
+        long token = 0L;
+        token = baseMapper.getYesterAiReplyToken(yesterDayBegin, yesterdayEnd);
+        if (BeanUtil.isEmpty(token)) {
+            map.put("usage", token);
+            map.put("price", BigDecimal.ZERO);
+            return map;
+        }
+
+        BigDecimal tokenCount = new BigDecimal(token).divide(new BigDecimal(item.getTokenUnit()), 6, BigDecimal.ROUND_HALF_UP);
+        BigDecimal totalFee = item.getUnitPrice().multiply(tokenCount).setScale(6, BigDecimal.ROUND_HALF_UP);
+        map.put("usage", token);
+        map.put("price", totalFee);
+        return map;
+    }
+
+    @Override
+    public Map<String, Object> getYesterSopToken(DateTime yesterDayBegin, DateTime yesterdayEnd, FeePlanItem item,TenantInfo tenant) {
+        List<SopTokenDto> list = baseMapper.getYesterSopToken(yesterDayBegin, yesterdayEnd);
+        BigDecimal totalToken = BigDecimal.ZERO;
+        for (SopTokenDto sopToken : list) {
+            int type = sopToken.getType();
+            if (type == 7) {
+                totalToken = totalToken.add(new BigDecimal(sopToken.getCount() * 450));
+            } else {
+                totalToken = totalToken.add(new BigDecimal(sopToken.getCount() * 150));
+            }
+        }
+
+        BigDecimal tokenCount = totalToken.divide(new BigDecimal(item.getTokenUnit()), 6, BigDecimal.ROUND_HALF_UP);
+        BigDecimal totalFee = item.getUnitPrice().multiply(tokenCount).setScale(6, BigDecimal.ROUND_HALF_UP);
+        Map<String, Object> map = new HashMap<>();
+        map.put("usage", totalToken);
+        map.put("price", totalFee);
+        return map;
+    }
 }
 
 

+ 93 - 0
fs-service/src/main/resources/mapper/company/CompanyKnowledgeAuditMapper.xml

@@ -0,0 +1,93 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanyKnowledgeAuditMapper">
+
+    <resultMap id="CompanyKnowledgeAuditResult" type="CompanyKnowledgeAudit">
+        <result property="id" column="id"/>
+        <result property="companyId" column="company_id"/>
+        <result property="knowledgeId" column="knowledge_id"/>
+        <result property="sourceType" column="source_type"/>
+        <result property="sourceId" column="source_id"/>
+        <result property="content" column="content"/>
+        <result property="suggestion" column="suggestion"/>
+        <result property="auditResult" column="audit_result"/>
+        <result property="auditComment" column="audit_comment"/>
+        <result property="auditor" column="auditor"/>
+        <result property="auditTime" column="audit_time"/>
+        <result property="delFlag" column="del_flag"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <select id="selectAuditList" resultMap="CompanyKnowledgeAuditResult">
+        select id, company_id, knowledge_id, source_type, source_id, content, suggestion,
+               audit_result, audit_comment, auditor, audit_time, del_flag,
+               create_by, create_time, update_by, update_time
+        from company_knowledge_audit
+        where del_flag = 0
+          and company_id = #{companyId}
+          <if test="auditResult != null and auditResult != ''">
+              and audit_result = #{auditResult}
+          </if>
+        order by create_time desc
+    </select>
+
+    <insert id="insertAudit" parameterType="CompanyKnowledgeAudit" useGeneratedKeys="true" keyProperty="id">
+        insert into company_knowledge_audit
+        (company_id, knowledge_id, source_type, source_id, content, suggestion, audit_result,
+         audit_comment, auditor, audit_time, del_flag, create_by, create_time, update_by, update_time)
+        values
+        (#{companyId}, #{knowledgeId}, #{sourceType}, #{sourceId}, #{content}, #{suggestion}, #{auditResult},
+         #{auditComment}, #{auditor}, #{auditTime}, #{delFlag}, #{createBy}, #{createTime}, #{updateBy}, #{updateTime})
+    </insert>
+
+    <select id="selectAuditByIdAndCompanyId" resultMap="CompanyKnowledgeAuditResult">
+        select id, company_id, knowledge_id, source_type, source_id, content, suggestion,
+               audit_result, audit_comment, auditor, audit_time, del_flag,
+               create_by, create_time, update_by, update_time
+        from company_knowledge_audit
+        where id = #{id}
+          and company_id = #{companyId}
+          and del_flag = 0
+        limit 1
+    </select>
+
+    <update id="updateAuditById" parameterType="CompanyKnowledgeAudit">
+        update company_knowledge_audit
+        <trim prefix="set" suffixOverrides=",">
+            <if test="auditResult != null">audit_result = #{auditResult},</if>
+            <if test="auditComment != null">audit_comment = #{auditComment},</if>
+            <if test="auditor != null">auditor = #{auditor},</if>
+            <if test="auditTime != null">audit_time = #{auditTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where id = #{id}
+          and company_id = #{companyId}
+          and del_flag = 0
+    </update>
+
+    <update id="logicalDeleteById">
+        update company_knowledge_audit
+        set del_flag = 1
+        where id = #{id}
+          and company_id = #{companyId}
+          and del_flag = 0
+    </update>
+
+    <update id="batchAudit">
+        update company_knowledge_audit
+        set audit_result = #{auditResult},
+            auditor = #{auditor},
+            audit_time = now(),
+            update_time = now()
+        where id in
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+        and del_flag = 0
+    </update>
+
+</mapper>

+ 105 - 0
fs-service/src/main/resources/mapper/company/CompanyKnowledgeBaseMapper.xml

@@ -0,0 +1,105 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanyKnowledgeBaseMapper">
+
+    <resultMap id="CompanyKnowledgeBaseResult" type="CompanyKnowledgeBase">
+        <result property="id" column="id"/>
+        <result property="companyId" column="company_id"/>
+        <result property="title" column="title"/>
+        <result property="question" column="question"/>
+        <result property="answer" column="answer"/>
+        <result property="industryType" column="industry_type"/>
+        <result property="source" column="source"/>
+        <result property="auditStatus" column="audit_status"/>
+        <result property="auditComment" column="audit_comment"/>
+        <result property="auditor" column="auditor"/>
+        <result property="auditTime" column="audit_time"/>
+        <result property="useCount" column="use_count"/>
+        <result property="fastgptId" column="fastgpt_id"/>
+        <result property="syncStatus" column="sync_status"/>
+        <result property="syncTime" column="sync_time"/>
+        <result property="delFlag" column="del_flag"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <select id="selectKnowledgeList" resultMap="CompanyKnowledgeBaseResult">
+        select id, company_id, title, question, answer, industry_type, source, audit_status,
+               audit_comment, auditor, audit_time, use_count, fastgpt_id, sync_status, sync_time,
+               del_flag, create_by, create_time, update_by, update_time
+        from company_knowledge_base
+        where del_flag = 0
+          and company_id = #{companyId}
+          <if test="keyword != null and keyword != ''">
+              and (title like concat('%', #{keyword}, '%') or question like concat('%', #{keyword}, '%'))
+          </if>
+          <if test="industryType != null and industryType != ''">
+              and industry_type = #{industryType}
+          </if>
+          <if test="auditStatus != null">
+              and audit_status = #{auditStatus}
+          </if>
+        order by create_time desc
+    </select>
+
+    <insert id="insertKnowledge" parameterType="CompanyKnowledgeBase" useGeneratedKeys="true" keyProperty="id">
+        insert into company_knowledge_base
+        (company_id, title, question, answer, industry_type, source, audit_status, use_count, 
+         sync_status, del_flag, create_by, create_time, update_by, update_time)
+        values
+        (#{companyId}, #{title}, #{question}, #{answer}, #{industryType}, #{source}, #{auditStatus}, #{useCount},
+         #{syncStatus}, #{delFlag}, #{createBy}, #{createTime}, #{updateBy}, #{updateTime})
+    </insert>
+
+    <select id="selectKnowledgeByIdAndCompanyId" resultMap="CompanyKnowledgeBaseResult">
+        select id, company_id, title, question, answer, industry_type, source, audit_status,
+               audit_comment, auditor, audit_time, use_count, fastgpt_id, sync_status, sync_time,
+               del_flag, create_by, create_time, update_by, update_time
+        from company_knowledge_base
+        where id = #{id}
+          and company_id = #{companyId}
+          and del_flag = 0
+        limit 1
+    </select>
+
+    <update id="updateKnowledgeById" parameterType="CompanyKnowledgeBase">
+        update company_knowledge_base
+        <trim prefix="set" suffixOverrides=",">
+            <if test="title != null">title = #{title},</if>
+            <if test="question != null">question = #{question},</if>
+            <if test="answer != null">answer = #{answer},</if>
+            <if test="industryType != null">industry_type = #{industryType},</if>
+            <if test="source != null">source = #{source},</if>
+            <if test="auditStatus != null">audit_status = #{auditStatus},</if>
+            <if test="auditComment != null">audit_comment = #{auditComment},</if>
+            <if test="auditor != null">auditor = #{auditor},</if>
+            <if test="auditTime != null">audit_time = #{auditTime},</if>
+            <if test="fastgptId != null">fastgpt_id = #{fastgptId},</if>
+            <if test="syncStatus != null">sync_status = #{syncStatus},</if>
+            <if test="syncTime != null">sync_time = #{syncTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where id = #{id}
+          and company_id = #{companyId}
+          and del_flag = 0
+    </update>
+
+    <update id="logicalDeleteById">
+        update company_knowledge_base
+        set del_flag = 1
+        where id = #{id}
+          and company_id = #{companyId}
+          and del_flag = 0
+    </update>
+
+    <update id="incrementUseCount">
+        update company_knowledge_base
+        set use_count = use_count + 1
+        where id = #{id}
+          and del_flag = 0
+    </update>
+
+</mapper>

+ 88 - 0
fs-service/src/main/resources/mapper/company/CompanyKnowledgeSuggestionMapper.xml

@@ -0,0 +1,88 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanyKnowledgeSuggestionMapper">
+
+    <resultMap id="CompanyKnowledgeSuggestionResult" type="CompanyKnowledgeSuggestion">
+        <result property="id" column="id"/>
+        <result property="companyId" column="company_id"/>
+        <result property="type" column="type"/>
+        <result property="description" column="description"/>
+        <result property="suggestion" column="suggestion"/>
+        <result property="impact" column="impact"/>
+        <result property="status" column="status"/>
+        <result property="applyTime" column="apply_time"/>
+        <result property="delFlag" column="del_flag"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <select id="selectSuggestionList" resultMap="CompanyKnowledgeSuggestionResult">
+        select id, company_id, type, description, suggestion, impact, status, apply_time,
+               del_flag, create_by, create_time, update_by, update_time
+        from company_knowledge_suggestion
+        where del_flag = 0
+          and company_id = #{companyId}
+          <if test="status != null">
+              and status = #{status}
+          </if>
+          <if test="type != null and type != ''">
+              and type = #{type}
+          </if>
+        order by create_time desc
+    </select>
+
+    <insert id="insertSuggestion" parameterType="CompanyKnowledgeSuggestion" useGeneratedKeys="true" keyProperty="id">
+        insert into company_knowledge_suggestion
+        (company_id, type, description, suggestion, impact, status, del_flag, 
+         create_by, create_time, update_by, update_time)
+        values
+        (#{companyId}, #{type}, #{description}, #{suggestion}, #{impact}, #{status}, #{delFlag},
+         #{createBy}, #{createTime}, #{updateBy}, #{updateTime})
+    </insert>
+
+    <select id="selectSuggestionByIdAndCompanyId" resultMap="CompanyKnowledgeSuggestionResult">
+        select id, company_id, type, description, suggestion, impact, status, apply_time,
+               del_flag, create_by, create_time, update_by, update_time
+        from company_knowledge_suggestion
+        where id = #{id}
+          and company_id = #{companyId}
+          and del_flag = 0
+        limit 1
+    </select>
+
+    <update id="updateSuggestionById" parameterType="CompanyKnowledgeSuggestion">
+        update company_knowledge_suggestion
+        <trim prefix="set" suffixOverrides=",">
+            <if test="status != null">status = #{status},</if>
+            <if test="applyTime != null">apply_time = #{applyTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where id = #{id}
+          and company_id = #{companyId}
+          and del_flag = 0
+    </update>
+
+    <update id="logicalDeleteById">
+        update company_knowledge_suggestion
+        set del_flag = 1
+        where id = #{id}
+          and company_id = #{companyId}
+          and del_flag = 0
+    </update>
+
+    <update id="batchApply">
+        update company_knowledge_suggestion
+        set status = 1,
+            apply_time = now(),
+            update_time = now()
+        where id in
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+        and del_flag = 0
+    </update>
+
+</mapper>

+ 107 - 0
fs-service/src/main/resources/mapper/company/CompanyTagTemplateBindingMapper.xml

@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanyTagTemplateBindingMapper">
+
+    <resultMap id="CompanyTagTemplateBindingResult" type="CompanyTagTemplateBinding">
+        <result property="id" column="id"/>
+        <result property="companyId" column="company_id"/>
+        <result property="tagCode" column="tag_code"/>
+        <result property="tagName" column="tag_name"/>
+        <result property="templateId" column="template_id"/>
+        <result property="templateName" column="template_name"/>
+        <result property="priority" column="priority"/>
+        <result property="matchCondition" column="match_condition"/>
+        <result property="status" column="status"/>
+        <result property="delFlag" column="del_flag"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <select id="selectBindingList" resultMap="CompanyTagTemplateBindingResult">
+        select id, company_id, tag_code, tag_name, template_id, template_name, priority,
+               match_condition, status, del_flag, create_by, create_time, update_by, update_time
+        from company_tag_template_binding
+        where del_flag = 0
+          and company_id = #{companyId}
+          <if test="tagCode != null and tagCode != ''">
+              and tag_code like concat('%', #{tagCode}, '%')
+          </if>
+          <if test="templateId != null">
+              and template_id = #{templateId}
+          </if>
+        order by priority desc, create_time desc
+    </select>
+
+    <insert id="insertBinding" parameterType="CompanyTagTemplateBinding" useGeneratedKeys="true" keyProperty="id">
+        insert into company_tag_template_binding
+        (company_id, tag_code, tag_name, template_id, template_name, priority, match_condition, 
+         status, del_flag, create_by, create_time, update_by, update_time)
+        values
+        (#{companyId}, #{tagCode}, #{tagName}, #{templateId}, #{templateName}, #{priority}, #{matchCondition},
+         #{status}, #{delFlag}, #{createBy}, #{createTime}, #{updateBy}, #{updateTime})
+    </insert>
+
+    <select id="selectBindingByIdAndCompanyId" resultMap="CompanyTagTemplateBindingResult">
+        select id, company_id, tag_code, tag_name, template_id, template_name, priority,
+               match_condition, status, del_flag, create_by, create_time, update_by, update_time
+        from company_tag_template_binding
+        where id = #{id}
+          and company_id = #{companyId}
+          and del_flag = 0
+        limit 1
+    </select>
+
+    <update id="updateBindingById" parameterType="CompanyTagTemplateBinding">
+        update company_tag_template_binding
+        <trim prefix="set" suffixOverrides=",">
+            <if test="tagCode != null">tag_code = #{tagCode},</if>
+            <if test="tagName != null">tag_name = #{tagName},</if>
+            <if test="templateId != null">template_id = #{templateId},</if>
+            <if test="templateName != null">template_name = #{templateName},</if>
+            <if test="priority != null">priority = #{priority},</if>
+            <if test="matchCondition != null">match_condition = #{matchCondition},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where id = #{id}
+          and company_id = #{companyId}
+          and del_flag = 0
+    </update>
+
+    <update id="logicalDeleteById">
+        update company_tag_template_binding
+        set del_flag = 1
+        where id = #{id}
+          and company_id = #{companyId}
+          and del_flag = 0
+    </update>
+
+    <select id="selectMatchedTemplates" resultMap="CompanyTagTemplateBindingResult">
+        select id, company_id, tag_code, tag_name, template_id, template_name, priority,
+               match_condition, status, del_flag
+        from company_tag_template_binding
+        where del_flag = 0
+          and company_id = #{companyId}
+          and status = 1
+          and tag_code in
+        <foreach collection="tagCodes" item="tagCode" open="(" separator="," close=")">
+            #{tagCode}
+        </foreach>
+        order by priority desc
+    </select>
+
+    <insert id="batchBind">
+        insert into company_tag_template_binding
+        (company_id, tag_code, tag_name, template_id, template_name, priority, status, 
+         del_flag, create_by, create_time, update_by, update_time)
+        values
+        <foreach collection="tagCodes" item="tagCode" separator=",">
+            (#{companyId}, #{tagCode}, #{tagCode}, #{templateId}, '', 0, 1, 0, 
+             #{createBy}, #{createTime}, #{updateBy}, #{updateTime})
+        </foreach>
+    </insert>
+
+</mapper>

+ 50 - 0
fs-service/src/main/resources/mapper/company/CompanyWorkflowLobsterEdgeMapper.xml

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanyWorkflowLobsterEdgeMapper">
+
+    <resultMap id="CompanyWorkflowLobsterEdgeResult" type="CompanyWorkflowLobsterEdge">
+        <result property="id" column="id"/>
+        <result property="workflowId" column="workflow_id"/>
+        <result property="edgeKey" column="edge_key"/>
+        <result property="sourceNodeCode" column="source_node_code"/>
+        <result property="targetNodeCode" column="target_node_code"/>
+        <result property="sourcePort" column="source_port"/>
+        <result property="targetPort" column="target_port"/>
+        <result property="edgeLabel" column="edge_label"/>
+        <result property="edgeColor" column="edge_color"/>
+        <result property="conditionExpr" column="condition_expr"/>
+        <result property="sortNo" column="sort_no"/>
+        <result property="delFlag" column="del_flag"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+    </resultMap>
+
+    <insert id="batchInsert" parameterType="java.util.List">
+        insert into company_workflow_lobster_edge
+        (workflow_id, edge_key, source_node_code, target_node_code, source_port, target_port,
+         edge_label, edge_color, condition_expr, sort_no, del_flag, create_by, create_time)
+        values
+        <foreach collection="list" item="item" separator=",">
+            (#{item.workflowId}, #{item.edgeKey}, #{item.sourceNodeCode}, #{item.targetNodeCode},
+             #{item.sourcePort}, #{item.targetPort}, #{item.edgeLabel}, #{item.edgeColor},
+             #{item.conditionExpr}, #{item.sortNo}, #{item.delFlag}, #{item.createBy}, #{item.createTime})
+        </foreach>
+    </insert>
+
+    <update id="deleteByWorkflowId">
+        update company_workflow_lobster_edge
+        set del_flag = 1
+        where workflow_id = #{workflowId} and del_flag = 0
+    </update>
+
+    <select id="selectByWorkflowId" resultMap="CompanyWorkflowLobsterEdgeResult">
+        select id, workflow_id, edge_key, source_node_code, target_node_code, source_port, target_port,
+               edge_label, edge_color, condition_expr, sort_no, del_flag, create_by, create_time
+        from company_workflow_lobster_edge
+        where workflow_id = #{workflowId} and del_flag = 0
+        order by sort_no asc
+    </select>
+
+</mapper>

+ 71 - 0
fs-service/src/main/resources/mapper/company/CompanyWorkflowLobsterMapper.xml

@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanyWorkflowLobsterMapper">
+
+    <resultMap id="CompanyWorkflowLobsterResult" type="CompanyWorkflowLobster">
+        <result property="id" column="id"/>
+        <result property="companyId" column="company_id"/>
+        <result property="templateCode" column="template_code"/>
+        <result property="templateName" column="template_name"/>
+        <result property="industryType" column="industry_type"/>
+        <result property="description" column="description"/>
+        <result property="status" column="status"/>
+        <result property="version" column="version"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="delFlag" column="del_flag"/>
+    </resultMap>
+
+    <select id="selectTemplateList" resultMap="CompanyWorkflowLobsterResult">
+        select id, company_id, template_code, template_name, industry_type, description, status, version,
+               create_by, create_time, update_by, update_time, del_flag
+        from company_workflow_lobster
+        where del_flag = 0
+          and company_id = #{companyId}
+        order by create_time desc
+    </select>
+
+    <insert id="insertTemplate" parameterType="CompanyWorkflowLobster" useGeneratedKeys="true" keyProperty="id">
+        insert into company_workflow_lobster
+        (company_id, template_code, template_name, industry_type, description, status, version, create_by, create_time, update_by, update_time, del_flag)
+        values
+        (#{companyId}, #{templateCode}, #{templateName}, #{industryType}, #{description}, #{status}, #{version}, #{createBy}, #{createTime}, #{updateBy}, #{updateTime}, #{delFlag})
+    </insert>
+
+    <select id="selectTemplateByIdAndCompanyId" resultMap="CompanyWorkflowLobsterResult">
+        select id, company_id, template_code, template_name, industry_type, description, status, version,
+               create_by, create_time, update_by, update_time, del_flag
+        from company_workflow_lobster
+        where id = #{id}
+          and company_id = #{companyId}
+          and del_flag = 0
+        limit 1
+    </select>
+
+    <update id="updateTemplateById" parameterType="CompanyWorkflowLobster">
+        update company_workflow_lobster
+        <trim prefix="set" suffixOverrides=",">
+            <if test="templateName != null">template_name = #{templateName},</if>
+            <if test="industryType != null">industry_type = #{industryType},</if>
+            <if test="description != null">description = #{description},</if>
+            <if test="status != null">status = #{status},</if>
+            <if test="version != null">version = #{version},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where id = #{id}
+          and company_id = #{companyId}
+          and del_flag = 0
+    </update>
+
+    <update id="logicalDeleteById">
+        update company_workflow_lobster
+        set del_flag = 1
+        where id = #{id}
+          and company_id = #{companyId}
+          and del_flag = 0
+    </update>
+
+</mapper>

+ 30 - 0
fs-service/src/main/resources/mapper/company/CompanyWorkflowLobsterNodeMapper.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanyWorkflowLobsterNodeMapper">
+
+    <insert id="batchInsert">
+        insert into company_workflow_lobster_node
+        (workflow_id, node_code, node_name, node_type, sort_no, next_node_code, message_template, condition_expr, node_config, greeting_config, create_by, create_time, update_by, update_time, del_flag)
+        values
+        <foreach collection="list" item="item" separator=",">
+            (#{item.workflowId}, #{item.nodeCode}, #{item.nodeName}, #{item.nodeType}, #{item.sortNo}, #{item.nextNodeCode},
+            #{item.messageTemplate}, #{item.conditionExpr}, #{item.nodeConfig}, #{item.greetingConfig}, #{item.createBy}, #{item.createTime}, #{item.updateBy}, #{item.updateTime}, #{item.delFlag})
+        </foreach>
+    </insert>
+
+    <update id="deleteByWorkflowId">
+        update company_workflow_lobster_node
+        set del_flag = 1
+        where workflow_id = #{workflowId}
+    </update>
+
+    <select id="selectByWorkflowId" resultType="CompanyWorkflowLobsterNode">
+        select id, workflow_id, node_code, node_name, node_type, sort_no, next_node_code, message_template,
+               condition_expr, node_config, greeting_config, create_by, create_time, update_by, update_time, del_flag
+        from company_workflow_lobster_node
+        where workflow_id = #{workflowId}
+          and del_flag = 0
+        order by sort_no asc, id asc
+    </select>
+
+</mapper>

+ 50 - 0
fs-service/src/main/resources/mapper/company/CompanyWorkflowLobsterRecordMapper.xml

@@ -0,0 +1,50 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanyWorkflowLobsterRecordMapper">
+
+    <resultMap id="CompanyWorkflowLobsterRecordResult" type="CompanyWorkflowLobsterRecord">
+        <result property="id" column="id"/>
+        <result property="companyId" column="company_id"/>
+        <result property="recordNo" column="record_no"/>
+        <result property="requirement" column="requirement"/>
+        <result property="selectedApiIds" column="selected_api_ids"/>
+        <result property="status" column="status"/>
+        <result property="errorMsg" column="error_msg"/>
+        <result property="resultJson" column="result_json"/>
+        <result property="createBy" column="create_by"/>
+        <result property="createTime" column="create_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="delFlag" column="del_flag"/>
+    </resultMap>
+
+    <insert id="insertRecord" parameterType="CompanyWorkflowLobsterRecord" useGeneratedKeys="true" keyProperty="id">
+        insert into company_workflow_lobster_record
+        (company_id, record_no, requirement, selected_api_ids, status, error_msg, result_json, create_by, create_time, update_by, update_time, del_flag)
+        values
+        (#{companyId}, #{recordNo}, #{requirement}, #{selectedApiIds}, #{status}, #{errorMsg}, #{resultJson}, #{createBy}, #{createTime}, #{updateBy}, #{updateTime}, #{delFlag})
+    </insert>
+
+    <select id="selectByRecordNo" resultMap="CompanyWorkflowLobsterRecordResult">
+        select id, company_id, record_no, requirement, selected_api_ids, status, error_msg, result_json,
+               create_by, create_time, update_by, update_time, del_flag
+        from company_workflow_lobster_record
+        where record_no = #{recordNo}
+          and company_id = #{companyId}
+          and del_flag = 0
+        limit 1
+    </select>
+
+    <update id="updateRecord" parameterType="CompanyWorkflowLobsterRecord">
+        update company_workflow_lobster_record
+        <trim prefix="set" suffixOverrides=",">
+            <if test="status != null">status = #{status},</if>
+            <if test="errorMsg != null">error_msg = #{errorMsg},</if>
+            <if test="resultJson != null">result_json = #{resultJson},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+</mapper>

+ 30 - 0
fs-service/src/main/resources/mapper/company/CompanyWorkflowLobsterVariableMapper.xml

@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.fs.company.mapper.CompanyWorkflowLobsterVariableMapper">
+
+    <insert id="batchInsert">
+        insert into company_workflow_lobster_variable
+        (workflow_id, var_code, var_name, var_type, source_type, required, default_value, description, create_by, create_time, update_by, update_time, del_flag)
+        values
+        <foreach collection="list" item="item" separator=",">
+            (#{item.workflowId}, #{item.varCode}, #{item.varName}, #{item.varType}, #{item.sourceType}, #{item.required},
+            #{item.defaultValue}, #{item.description}, #{item.createBy}, #{item.createTime}, #{item.updateBy}, #{item.updateTime}, #{item.delFlag})
+        </foreach>
+    </insert>
+
+    <update id="deleteByWorkflowId">
+        update company_workflow_lobster_variable
+        set del_flag = 1
+        where workflow_id = #{workflowId}
+    </update>
+
+    <select id="selectByWorkflowId" resultType="CompanyWorkflowLobsterVariable">
+        select id, workflow_id, var_code, var_name, var_type, source_type, required, default_value, description,
+               create_by, create_time, update_by, update_time, del_flag
+        from company_workflow_lobster_variable
+        where workflow_id = #{workflowId}
+          and del_flag = 0
+        order by id asc
+    </select>
+
+</mapper>

+ 55 - 0
fs-service/src/main/resources/mapper/crm/CrmCustomerAnalyzeMapper.xml

@@ -139,6 +139,35 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         ORDER BY create_time DESC
         LIMIT 1
     </update>
+    <update id="updateAiAnalyze">
+        update qw_external_ai_analyze
+        <set>
+            <if test="customerPortraitJson != null and customerPortraitJson != ''">
+                customer_portrait_json = #{customerPortraitJson},
+            </if>
+            <if test="communicationSummary != null and communicationSummary != ''">
+                communication_summary = #{communicationSummary},
+            </if>
+            <if test="communicationAbstract != null and communicationAbstract != ''">
+                communication_abstract = #{communicationAbstract},
+            </if>
+            <if test="customerFocusJson != null and customerFocusJson != ''">
+                customer_focus_json = #{customerFocusJson},
+            </if>
+            <if test="intentionDegree != null and intentionDegree != ''">
+                intention_degree = #{intentionDegree},
+            </if>
+            <if test="attritionLevel != null and attritionLevel != ''">
+                attrition_level = #{attritionLevel},
+            </if>
+
+        </set>
+        where external_user_id = #{externalUserId}
+        and corp_id = #{corpId}
+        and qw_user_id = #{qwUserId}
+        ORDER BY create_time DESC
+        LIMIT 1
+    </update>
 
     <delete id="deleteCrmCustomerAnalyzeById" parameterType="Long">
         delete from crm_customer_analyze where id = #{id}
@@ -187,4 +216,30 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
         </where>
         order by a.create_time desc
     </select>
+    <select id="selectLatestQwAnalyze" resultType="com.fs.qw.domain.QwExternalAiAnalyze">
+        SELECT
+            `id`,
+            `external_user_id`,
+            `customer_name`,
+            `customer_portrait_json`,
+            `communication_abstract`,
+            `communication_summary`,
+            `attrition_level`,
+            `attrition_level_prompt`,
+            `customer_focus_json`,
+            `intention_degree`,
+            `ai_chat_record`,
+            `create_time`,
+            `remark`,
+            `reserve_int`,
+            `reserve_str`,
+            `corp_id`,
+            `qw_user_id`,
+            `session_id`
+        FROM `qw_external_ai_analyze`
+        WHERE `external_user_id` = #{externalUserId}
+          AND `corp_id` = #{corpId}
+          AND `qw_user_id` = #{qwUserId}
+        order by create_time desc limit 1;
+    </select>
 </mapper>

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä