Jelajahi Sumber

会话存档对话存贮
会话存档ai分析
scrm的ai分析优化

lk 1 Minggu lalu
induk
melakukan
9b87d54d22
36 mengubah file dengan 2004 tambahan dan 21 penghapusan
  1. 519 0
      fs-admin/src/main/java/com/fs/task/QwExternalAiAnalyzeTask.java
  2. 5 1
      fs-company/src/main/java/com/fs/company/controller/crm/CrmCustomerController.java
  3. 14 4
      fs-company/src/main/java/com/fs/company/controller/crm/chat/CrmCustomerChatSessionController.java
  4. 39 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwCustomerAnalyzeController.java
  5. 45 0
      fs-company/src/main/java/com/fs/company/controller/qw/QwCustomerPropertyController.java
  6. 5 0
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerAnalyzeMapper.java
  7. 4 4
      fs-service/src/main/java/com/fs/crm/mapper/CrmCustomerMapper.java
  8. 19 0
      fs-service/src/main/java/com/fs/crm/service/ICrmCustomerAnalyzeService.java
  9. 353 7
      fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerAnalyzeServiceImpl.java
  10. 1 1
      fs-service/src/main/java/com/fs/crm/utils/CrmCustomerAiTagUtil.java
  11. 11 0
      fs-service/src/main/java/com/fs/crm/vo/CrmCustomerListQueryVO.java
  12. 11 2
      fs-service/src/main/java/com/fs/crm/vo/CrmLineCustomerListQueryVO.java
  13. 25 0
      fs-service/src/main/java/com/fs/crm/vo/QwCustomerAiTagVo.java
  14. 91 0
      fs-service/src/main/java/com/fs/qw/domain/QwCustomerProperty.java
  15. 91 0
      fs-service/src/main/java/com/fs/qw/domain/QwExternalAiAnalyze.java
  16. 38 0
      fs-service/src/main/java/com/fs/qw/domain/QwExternalAiAnalyzeSession.java
  17. 6 0
      fs-service/src/main/java/com/fs/qw/domain/audit/QwMsgAuditMessage.java
  18. 20 0
      fs-service/src/main/java/com/fs/qw/mapper/QwCustomerPropertyMapper.java
  19. 20 0
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalAiAnalyzeMapper.java
  20. 21 0
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalAiAnalyzeSessionMapper.java
  21. 6 2
      fs-service/src/main/java/com/fs/qw/mapper/QwExternalContactMapper.java
  22. 10 0
      fs-service/src/main/java/com/fs/qw/mapper/QwMsgAuditMessageMapper.java
  23. 22 0
      fs-service/src/main/java/com/fs/qw/param/QwAnalyzeAiTagParam.java
  24. 23 0
      fs-service/src/main/java/com/fs/qw/param/audit/QwAiTagGainParam.java
  25. 25 0
      fs-service/src/main/java/com/fs/qw/param/audit/QwAuditMessagebackupParam.java
  26. 20 0
      fs-service/src/main/java/com/fs/qw/service/IQwCustomerPropertyService.java
  27. 15 0
      fs-service/src/main/java/com/fs/qw/service/IQwExternalAiAnalyzeService.java
  28. 12 0
      fs-service/src/main/java/com/fs/qw/service/IQwExternalAiAnalyzeSessionService.java
  29. 170 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwCustomerPropertyServiceImpl.java
  30. 23 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwExternalAiAnalyzeServiceImpl.java
  31. 18 0
      fs-service/src/main/java/com/fs/qw/service/impl/QwExternalAiAnalyzeSessionServiceImpl.java
  32. 15 0
      fs-service/src/main/java/com/fs/qw/vo/QwExternalContactVO.java
  33. 55 0
      fs-service/src/main/resources/mapper/crm/CrmCustomerAnalyzeMapper.xml
  34. 153 0
      fs-service/src/main/resources/mapper/qw/QwCustomerPropertyMapper.xml
  35. 70 0
      fs-service/src/main/resources/mapper/qw/QwExternalAiAnalyzeMapper.xml
  36. 29 0
      fs-service/src/main/resources/mapper/qw/QwMsgAuditMessageMapper.xml

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

+ 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')")
@@ -199,7 +202,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);
@@ -210,6 +213,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()));
 
                 }
             }

+ 14 - 4
fs-company/src/main/java/com/fs/company/controller/crm/chat/CrmCustomerChatSessionController.java

@@ -13,6 +13,7 @@ import com.fs.crm.service.ICrmCustomerChatMessageService;
 import com.fs.crm.service.ICrmCustomerChatSessionService;
 import com.fs.framework.security.LoginUser;
 import com.fs.framework.service.TokenService;
+import com.fs.qw.param.audit.QwAiTagGainParam;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.web.bind.annotation.*;
 
@@ -21,7 +22,7 @@ import java.util.Map;
 
 /**
  * 聊天会话 Controller
- * 
+ *
  * @author ylrz
  * @date 2026-03-30
  */
@@ -91,11 +92,11 @@ public class CrmCustomerChatSessionController extends BaseController {
         try {
             Long sessionId = Long.valueOf(params.get("sessionId").toString());
             String title = (String) params.get("title");
-            
+
             if (title == null) {
                 return error("参数错误");
             }
-            
+
             int result = chatSessionService.updateChatSessionTitle(sessionId, title);
             return result > 0 ? success("更新成功") : error("更新失败");
         } catch (Exception e) {
@@ -111,7 +112,7 @@ public class CrmCustomerChatSessionController extends BaseController {
         try {
             Long sessionId = Long.valueOf(params.get("sessionId").toString());
             Integer isPinned = (Integer) params.get("isPinned");
-            
+
             if (isPinned == null) {
                 return error("置顶参数错误");
             }
@@ -167,4 +168,13 @@ public class CrmCustomerChatSessionController extends BaseController {
     {
         return R.ok().put("data",crmCustomerAnalyzeService.polishingScript(param));
     }
+
+    /**
+     * 企微外部联系人ai打标签
+     */
+    @PostMapping("/qwAiTagGain")
+    public R qwAiTagGain(@RequestBody QwAiTagGainParam param)
+    {
+        return crmCustomerAnalyzeService.qwAiTagGain(param);
+    }
 }

+ 39 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwCustomerAnalyzeController.java

@@ -0,0 +1,39 @@
+package com.fs.company.controller.qw;
+
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.qw.domain.QwExternalAiAnalyze;
+import com.fs.qw.service.IQwExternalAiAnalyzeService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+
+/**
+ * 客户聊天记录分析Controller
+ *
+ * @author fs
+ * @date 2026-03-24
+ */
+@RestController
+@RequestMapping("/qw/analyze")
+public class QwCustomerAnalyzeController extends BaseController
+{
+    @Autowired
+    private IQwExternalAiAnalyzeService qwExternalAiAnalyzeService;
+
+    /**
+     * 查询客户聊天记录分析列表
+     */
+//    @PreAuthorize("@ss.hasPermi('qw:external:analyze:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(QwExternalAiAnalyze crmCustomerAnalyze)
+    {
+        startPage();
+        List<QwExternalAiAnalyze> list = qwExternalAiAnalyzeService.selectQwExternalAiAnalyzeList(crmCustomerAnalyze);
+        return getDataTable(list);
+    }
+
+}

+ 45 - 0
fs-company/src/main/java/com/fs/company/controller/qw/QwCustomerPropertyController.java

@@ -0,0 +1,45 @@
+package com.fs.company.controller.qw;
+
+import cn.hutool.core.util.ObjectUtil;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fs.common.core.controller.BaseController;
+import com.fs.common.core.domain.R;
+import com.fs.common.core.page.TableDataInfo;
+import com.fs.qw.domain.QwCustomerProperty;
+import com.fs.qw.domain.QwExternalAiAnalyze;
+import com.fs.qw.mapper.QwExternalAiAnalyzeMapper;
+import com.fs.qw.param.QwAnalyzeAiTagParam;
+import com.fs.qw.service.IQwCustomerPropertyService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@RestController
+@RequestMapping("/qw/customerProperty")
+@RequiredArgsConstructor
+public class QwCustomerPropertyController extends BaseController {
+
+    private final IQwCustomerPropertyService qwCustomerPropertyService;
+    private final QwExternalAiAnalyzeMapper qwExternalAiAnalyzeMapper;
+
+//    @PreAuthorize("@ss.hasPermi('qw:customerProperty:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(QwCustomerProperty qwCustomerProperty) {
+        startPage();
+        List<QwCustomerProperty> list = qwCustomerPropertyService.selectQwCustomerPropertyList(qwCustomerProperty);
+        return getDataTable(list);
+    }
+
+    @PostMapping("/analyzeAiTagByTrade")
+    public R analyzeAiTagByTrade(@RequestBody QwAnalyzeAiTagParam param){
+        QwExternalAiAnalyze aiAnalyze = qwExternalAiAnalyzeMapper.selectOne(new LambdaQueryWrapper<QwExternalAiAnalyze>().eq(QwExternalAiAnalyze::getExternalUserId, param.getExternalUserId())
+                .eq(QwExternalAiAnalyze::getQwUserId, param.getQwUserId()).eq(QwExternalAiAnalyze::getCorpId, param.getCorpId())
+                .orderByDesc(QwExternalAiAnalyze::getCreateTime).last("limit 1"));
+        if (ObjectUtil.isNull(aiAnalyze)) {
+            return R.error("无AI分析结果");
+        }
+        qwCustomerPropertyService.analyzeAiTagByTrade(param.getTradeType(),aiAnalyze);
+        return R.ok();
+    }
+}

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

@@ -3,6 +3,7 @@ package com.fs.crm.mapper;
 import java.util.List;
 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;
 
 /**
@@ -65,4 +66,8 @@ public interface CrmCustomerAnalyzeMapper extends BaseMapper<CrmCustomerAnalyze>
     int updateCustomerPortrait(CrmCustomerAnalyze crmCustomerAnalyze);
 
     List<CrmCustomerAnalyze> selectCrmCustomerAnalyzeListAll(CrmCustomerAnalyze crmCustomerAnalyze);
+
+    QwExternalAiAnalyze selectLatestQwAnalyze(QwExternalAiAnalyze qwExternalAiAnalyze);
+
+    Integer updateAiAnalyze(QwExternalAiAnalyze qwExternalAiAnalyze);
 }

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

@@ -345,10 +345,10 @@ 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,ca.attrition_level,ca.intention_degree  from  crm_customer c " +
+            "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 FROM crm_customer_analyze WHERE customer_id = c.customer_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 " +
@@ -439,9 +439,9 @@ 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,ca.attrition_level,ca.intention_degree  from  crm_customer c " +
+            "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 FROM crm_customer_analyze WHERE customer_id = c.customer_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 " +

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

@@ -2,9 +2,12 @@ package com.fs.crm.service;
 
 import java.util.List;
 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;
 
 /**
  * 客户聊天记录分析Service接口
@@ -80,4 +83,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);
 }

+ 353 - 7
fs-service/src/main/java/com/fs/crm/service/impl/CrmCustomerAnalyzeServiceImpl.java

@@ -7,6 +7,8 @@ import cn.hutool.json.JSONUtil;
 import com.alibaba.fastjson.JSON;
 import com.alibaba.fastjson.JSONArray;
 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;
@@ -14,21 +16,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.crm.domain.CrmCustomerAnalyze;
-import com.fs.crm.domain.CrmCustomerChatMessage;
+import com.fs.common.utils.DictUtils;
+import com.fs.common.utils.spring.SpringUtils;
+import com.fs.crm.domain.*;
+import com.fs.crm.dto.CrmCustomerAiAutoTagVo;
 import com.fs.crm.mapper.CrmCustomerAnalyzeMapper;
+import com.fs.crm.mapper.CrmCustomerMapper;
 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.crm.vo.CrmCustomerAiTagVo;
+import com.fs.crm.vo.QwCustomerAiTagVo;
+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.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业务层处理
@@ -42,6 +56,13 @@ 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;
+
     /**
      * 查询客户聊天记录分析
      *
@@ -307,7 +328,7 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
     }
 
 
-        private static Long getScore(String level) {
+        private Long getScore(String level) {
             if ("十分满意".equals(level) || "满意".equals(level) || "A".equals(level) || "B".equals(level)) return 1L;
             if ("基本满意".equals(level) || "C".equals(level)) return 2L;
             if ("不满意".equals(level) || "D".equals(level)) return 3L;
@@ -575,13 +596,340 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         return baseMapper.updateCustomerPortrait(crmCustomerAnalyze);
     }
 
+    @Override
+    public String aiGeneratedCustomerPortraitQw(QwExternalAiAnalyze qwExternalAiAnalyze, String dataJson, Long logId) {
+        Map<String, Object> stringObjectMap = buildRequestParamQw(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 = buildRequestParamQw(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 = buildRequestParamQw(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 = buildRequestParamQw(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 = buildRequestParamQw(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 = buildRequestParamQw(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 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("likeRatio", "");
+        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> buildRequestParamAiTag(Long customerId, String tradeType, String communication) {
+        Map<String, Object> requestParam = new HashMap<>();
+        requestParam.put("customerId", customerId);
+        requestParam.put("tradeType", tradeType);
+        requestParam.put("communication", communication);
+        return requestParam;
+    }
+
+    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;
+    }
+
+    private Map<String, Object> buildRequestParamQw(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("likeRatio", 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 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()){
@@ -604,10 +952,8 @@ public class CrmCustomerAnalyzeServiceImpl extends ServiceImpl<CrmCustomerAnalyz
         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(

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

@@ -433,7 +433,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)) {

+ 11 - 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
@@ -120,4 +122,13 @@ public class CrmCustomerListQueryVO implements Serializable
     /** 意向度 */
     @Excel(name = "意向度")
     private Long intentionDegree;
+
+    /**
+     * ai标签
+     */
+    private List<CrmCustomerProperty> properties;
+    /**
+     * 客户关注点
+     */
+    private String customerFocusJson;
 }

+ 11 - 2
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
@@ -112,6 +114,13 @@ public class CrmLineCustomerListQueryVO implements Serializable
     /** 意向度 */
     @Excel(name = "意向度")
     private String intentionDegree;
-
-
+    /**
+     * ai标签
+     */
+    private List<CrmCustomerProperty> properties;
+
+    /**
+     * 客户关注点
+     */
+    private String customerFocusJson;
 }

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

@@ -0,0 +1,25 @@
+package com.fs.crm.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;
+}

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

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

@@ -88,5 +88,11 @@ public class QwMsgAuditMessage {
     @TableField(exist = false)
     private String fromUserName;
 
+    /**
+     * 进行ai分析的开始时间
+     */
+    @TableField(exist = false)
+    private Long analyzeStartTime;
+
 }
 

+ 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.crm.vo.QwCustomerAiTagVo;
+import com.fs.qw.domain.QwCustomerProperty;
+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);
+}

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

@@ -231,12 +231,16 @@ public interface QwExternalContactMapper extends BaseMapper<QwExternalContact> {
     public int deleteQwExternalContactByIds(Long[] ids);
 
     @Select({"<script> " +
-            "select ec.*,qu.qw_user_name,qd.dept_name as departmentName,cw.name way_name,wg.group_name way_group_name from qw_external_contact ec " +
+            "select ec.*,qu.qw_user_name,qd.dept_name as departmentName,cw.name way_name,wg.group_name way_group_name,qwaa.attrition_level,qwaa.intention_degree,qwaa.customer_focus_json from qw_external_contact ec " +
             "left join qw_user qu on ec.user_id=qu.qw_user_id and qu.corp_id=ec.corp_id " +
             "left join qw_dept qd on qd.dept_id=qu.department and qd.corp_id=qu.corp_id " +
             "left join company_user cu on ec.company_user_id=cu.user_id " +
             "left join qw_contact_way cw on cw.id = ec.way_id " +
-            "left join qw_contact_way_group wg on wg.id=cw.group_id" +
+            "left join qw_contact_way_group wg on wg.id=cw.group_id " +
+            "left join qw_external_ai_analyze qwaa on qwaa.id = (" +
+            "select c.id from qw_external_ai_analyze c " +
+            "where c.qw_user_id = ec.user_id and c.external_user_id = ec.external_user_id and c.corp_id = ec.corp_id " +
+            "order by c.create_time desc limit 1) " +
             "<where>  \n" +
             "            <if test=\"id != null  and id != ''\"> and ec.id   like concat( #{id}, '%') </if>\n" +
             "            <if test=\"userId != null  and userId != ''\"> and ec.user_id   like concat( #{userId}, '%') </if>\n" +

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

@@ -30,6 +30,16 @@ public interface QwMsgAuditMessageMapper extends BaseMapper<QwMsgAuditMessage>{
      */
     List<QwMsgAuditMessage> selectQwMsgAuditMessageList(QwMsgAuditMessage qwMsgAuditMessage);
 
+    /**
+     * 按指定分片全量查询企微会话存档结构化消息列表
+     *
+     * @param shard 分片号
+     * @param qwMsgAuditMessage 查询条件
+     * @return 企微会话存档结构化消息集合
+     */
+    List<QwMsgAuditMessage> selectQwMsgAuditMessageListByShard(@Param("shard") int shard,
+                                                               @Param("qwMsgAuditMessage") QwMsgAuditMessage qwMsgAuditMessage);
+
     /**
      * 新增企微会话存档结构化消息
      *

+ 22 - 0
fs-service/src/main/java/com/fs/qw/param/QwAnalyzeAiTagParam.java

@@ -0,0 +1,22 @@
+package com.fs.qw.param;
+
+import com.baidu.dev2.thirdparty.swagger.annotations.ApiModelProperty;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+import lombok.experimental.Accessors;
+
+@Data
+@Accessors(chain = true)
+@AllArgsConstructor
+@NoArgsConstructor
+public class QwAnalyzeAiTagParam {
+    @ApiModelProperty("行业")
+    private String tradeType;
+    /** 客户ID */
+    private String externalUserId;
+    /** 企业id */
+    private String corpId;
+    /** 属于用户id */
+    private String qwUserId;
+}

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

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

@@ -0,0 +1,20 @@
+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 com.fs.qw.param.QwAnalyzeAiTagParam;
+
+import java.util.List;
+
+/**
+ * 客户属性 Service
+ *
+ * @author fs
+ */
+public interface IQwCustomerPropertyService extends IService<QwCustomerProperty> {
+    List<QwCustomerProperty> selectQwCustomerPropertyList(QwCustomerProperty qwCustomerProperty);
+
+    R analyzeAiTagByTrade(String param, QwExternalAiAnalyze aiAnalyze);
+}

+ 15 - 0
fs-service/src/main/java/com/fs/qw/service/IQwExternalAiAnalyzeService.java

@@ -0,0 +1,15 @@
+package com.fs.qw.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.qw.domain.QwExternalAiAnalyze;
+
+import java.util.List;
+
+/**
+ * 外部联系人 AI 分析 Service
+ *
+ * @author fs
+ */
+public interface IQwExternalAiAnalyzeService extends IService<QwExternalAiAnalyze> {
+    List<QwExternalAiAnalyze> selectQwExternalAiAnalyzeList(QwExternalAiAnalyze crmCustomerAnalyze);
+}

+ 12 - 0
fs-service/src/main/java/com/fs/qw/service/IQwExternalAiAnalyzeSessionService.java

@@ -0,0 +1,12 @@
+package com.fs.qw.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.fs.qw.domain.QwExternalAiAnalyzeSession;
+
+/**
+ * 外部联系人 AI 分析会话绑定 Service
+ *
+ * @author fs
+ */
+public interface IQwExternalAiAnalyzeSessionService extends IService<QwExternalAiAnalyzeSession> {
+}

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

@@ -0,0 +1,170 @@
+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.crm.vo.CrmCustomerAiTagVo;
+import com.fs.crm.vo.QwCustomerAiTagVo;
+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.param.QwAnalyzeAiTagParam;
+import com.fs.qw.service.IQwCustomerPropertyService;
+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;
+    }
+}

+ 23 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwExternalAiAnalyzeServiceImpl.java

@@ -0,0 +1,23 @@
+package com.fs.qw.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.qw.domain.QwExternalAiAnalyze;
+import com.fs.qw.mapper.QwExternalAiAnalyzeMapper;
+import com.fs.qw.service.IQwExternalAiAnalyzeService;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * 外部联系人 AI 分析 Service 实现
+ *
+ * @author fs
+ */
+@Service
+public class QwExternalAiAnalyzeServiceImpl extends ServiceImpl<QwExternalAiAnalyzeMapper, QwExternalAiAnalyze>
+        implements IQwExternalAiAnalyzeService {
+    @Override
+    public List<QwExternalAiAnalyze> selectQwExternalAiAnalyzeList(QwExternalAiAnalyze crmCustomerAnalyze) {
+        return baseMapper.selectQwExternalAiAnalyzeList(crmCustomerAnalyze);
+    }
+}

+ 18 - 0
fs-service/src/main/java/com/fs/qw/service/impl/QwExternalAiAnalyzeSessionServiceImpl.java

@@ -0,0 +1,18 @@
+package com.fs.qw.service.impl;
+
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.fs.qw.domain.QwExternalAiAnalyzeSession;
+import com.fs.qw.mapper.QwExternalAiAnalyzeSessionMapper;
+import com.fs.qw.service.IQwExternalAiAnalyzeSessionService;
+import org.springframework.stereotype.Service;
+
+/**
+ * 外部联系人 AI 分析会话绑定 Service 实现
+ *
+ * @author fs
+ */
+@Service
+public class QwExternalAiAnalyzeSessionServiceImpl
+        extends ServiceImpl<QwExternalAiAnalyzeSessionMapper, QwExternalAiAnalyzeSession>
+        implements IQwExternalAiAnalyzeSessionService {
+}

+ 15 - 0
fs-service/src/main/java/com/fs/qw/vo/QwExternalContactVO.java

@@ -153,4 +153,19 @@ public class QwExternalContactVO {
      * 是否下载APP
      */
     private Integer isDownloadApp;
+
+    /**
+     * 流失风险
+     */
+    private Integer attritionLevel;
+
+    /**
+     * 关注点
+     */
+    private String customerFocusJson;
+    /**
+     * 意向度
+     */
+    private String intentionDegree;
+
 }

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

+ 153 - 0
fs-service/src/main/resources/mapper/qw/QwCustomerPropertyMapper.xml

@@ -0,0 +1,153 @@
+<?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.qw.mapper.QwCustomerPropertyMapper">
+
+    <resultMap type="QwCustomerProperty" id="QwCustomerPropertyResult">
+        <result property="id" column="id"/>
+        <result property="externalUserId" column="external_user_id"/>
+        <result property="propertyId" column="property_id"/>
+        <result property="propertyName" column="property_name"/>
+        <result property="propertyValue" column="property_value"/>
+        <result property="propertyValueType" column="property_value_type"/>
+        <result property="tradeType" column="trade_type"/>
+        <result property="aiAnalysis" column="ai_analysis"/>
+        <result property="intention" column="intention"/>
+        <result property="likeRatio" column="like_ratio"/>
+        <result property="createTime" column="create_time"/>
+        <result property="createBy" column="create_by"/>
+        <result property="updateTime" column="update_time"/>
+        <result property="updateBy" column="update_by"/>
+        <result property="remark" column="remark"/>
+        <result property="deleted" column="deleted"/>
+        <result property="deleteBy" column="delete_by"/>
+        <result property="deleteTime" column="delete_time"/>
+        <result property="corpId" column="corp_id"/>
+        <result property="qwUserId" column="qw_user_id"/>
+    </resultMap>
+
+    <sql id="selectQwCustomerPropertyVo">
+        select id, external_user_id, property_id, property_name, property_value, property_value_type,
+               trade_type, ai_analysis, intention, like_ratio, create_time, create_by,
+               update_time, update_by, remark, deleted, delete_by, delete_time, corp_id, qw_user_id
+        from qw_customer_property
+    </sql>
+
+    <select id="selectQwCustomerPropertyList" parameterType="QwCustomerProperty" resultMap="QwCustomerPropertyResult">
+        <include refid="selectQwCustomerPropertyVo"/>
+        <where>
+            <if test="externalUserId != null and externalUserId != ''"> and external_user_id = #{externalUserId}</if>
+            <if test="corpId != null and corpId != ''"> and corp_id = #{corpId}</if>
+            <if test="qwUserId != null and qwUserId != ''"> and qw_user_id = #{qwUserId}</if>
+            <if test="propertyId != null"> and property_id = #{propertyId}</if>
+            <if test="propertyName != null and propertyName != ''"> and property_name = #{propertyName}</if>
+            <if test="intention != null and intention != ''"> and intention = #{intention}</if>
+            <if test="deleted != null"> and deleted = #{deleted}</if>
+        </where>
+        order by id desc
+    </select>
+
+    <select id="selectQwCustomerPropertyById" parameterType="Long" resultMap="QwCustomerPropertyResult">
+        <include refid="selectQwCustomerPropertyVo"/>
+        where id = #{id}
+    </select>
+
+    <insert id="insertQwCustomerProperty" parameterType="QwCustomerProperty" useGeneratedKeys="true" keyProperty="id">
+        insert into qw_customer_property
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="externalUserId != null">external_user_id,</if>
+            <if test="propertyId != null">property_id,</if>
+            <if test="propertyName != null">property_name,</if>
+            <if test="propertyValue != null">property_value,</if>
+            <if test="propertyValueType != null">property_value_type,</if>
+            <if test="tradeType != null">trade_type,</if>
+            <if test="aiAnalysis != null">ai_analysis,</if>
+            <if test="intention != null">intention,</if>
+            <if test="likeRatio != null">like_ratio,</if>
+            <if test="createTime != null">create_time,</if>
+            <if test="createBy != null">create_by,</if>
+            <if test="updateTime != null">update_time,</if>
+            <if test="updateBy != null">update_by,</if>
+            <if test="remark != null">remark,</if>
+            <if test="deleted != null">deleted,</if>
+            <if test="deleteBy != null">delete_by,</if>
+            <if test="deleteTime != null">delete_time,</if>
+            <if test="corpId != null">corp_id,</if>
+            <if test="qwUserId != null">qw_user_id,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="externalUserId != null">#{externalUserId},</if>
+            <if test="propertyId != null">#{propertyId},</if>
+            <if test="propertyName != null">#{propertyName},</if>
+            <if test="propertyValue != null">#{propertyValue},</if>
+            <if test="propertyValueType != null">#{propertyValueType},</if>
+            <if test="tradeType != null">#{tradeType},</if>
+            <if test="aiAnalysis != null">#{aiAnalysis},</if>
+            <if test="intention != null">#{intention},</if>
+            <if test="likeRatio != null">#{likeRatio},</if>
+            <if test="createTime != null">#{createTime},</if>
+            <if test="createBy != null">#{createBy},</if>
+            <if test="updateTime != null">#{updateTime},</if>
+            <if test="updateBy != null">#{updateBy},</if>
+            <if test="remark != null">#{remark},</if>
+            <if test="deleted != null">#{deleted},</if>
+            <if test="deleteBy != null">#{deleteBy},</if>
+            <if test="deleteTime != null">#{deleteTime},</if>
+            <if test="corpId != null">#{corpId},</if>
+            <if test="qwUserId != null">#{qwUserId},</if>
+        </trim>
+    </insert>
+
+    <update id="updateQwCustomerProperty" parameterType="QwCustomerProperty">
+        update qw_customer_property
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="externalUserId != null">external_user_id = #{externalUserId},</if>
+            <if test="propertyId != null">property_id = #{propertyId},</if>
+            <if test="propertyName != null">property_name = #{propertyName},</if>
+            <if test="propertyValue != null">property_value = #{propertyValue},</if>
+            <if test="propertyValueType != null">property_value_type = #{propertyValueType},</if>
+            <if test="tradeType != null">trade_type = #{tradeType},</if>
+            <if test="aiAnalysis != null">ai_analysis = #{aiAnalysis},</if>
+            <if test="intention != null">intention = #{intention},</if>
+            <if test="likeRatio != null">like_ratio = #{likeRatio},</if>
+            <if test="createTime != null">create_time = #{createTime},</if>
+            <if test="createBy != null">create_by = #{createBy},</if>
+            <if test="updateTime != null">update_time = #{updateTime},</if>
+            <if test="updateBy != null">update_by = #{updateBy},</if>
+            <if test="remark != null">remark = #{remark},</if>
+            <if test="deleted != null">deleted = #{deleted},</if>
+            <if test="deleteBy != null">delete_by = #{deleteBy},</if>
+            <if test="deleteTime != null">delete_time = #{deleteTime},</if>
+            <if test="corpId != null">corp_id = #{corpId},</if>
+            <if test="qwUserId != null">qw_user_id = #{qwUserId},</if>
+        </trim>
+        where id = #{id}
+    </update>
+
+    <delete id="deleteQwCustomerPropertyById" parameterType="Long">
+        delete from qw_customer_property where id = #{id}
+    </delete>
+
+    <delete id="deleteQwCustomerPropertyByIds" parameterType="String">
+        delete from qw_customer_property where id in
+        <foreach item="id" collection="array" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <insert id="insertBatch" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
+        insert into qw_customer_property
+        (external_user_id, property_id, property_name, property_value,trade_type,
+        create_time,
+         corp_id, qw_user_id)
+        values
+        <foreach collection="list" item="item" separator=",">
+            (#{item.externalUserId}, #{item.propertyId}, #{item.propertyName}, #{item.propertyValue},
+             #{item.tradeType},  #{item.createTime},
+             #{item.corpId}, #{item.qwUserId})
+        </foreach>
+    </insert>
+
+</mapper>
+

+ 70 - 0
fs-service/src/main/resources/mapper/qw/QwExternalAiAnalyzeMapper.xml

@@ -0,0 +1,70 @@
+<?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.qw.mapper.QwExternalAiAnalyzeMapper">
+
+    <resultMap type="QwExternalAiAnalyze" id="QwExternalAiAnalyzeResult">
+        <result property="id" column="id"/>
+        <result property="externalUserId" column="external_user_id"/>
+        <result property="customerName" column="customer_name"/>
+        <result property="customerPortraitJson" column="customer_portrait_json"/>
+        <result property="communicationAbstract" column="communication_abstract"/>
+        <result property="communicationSummary" column="communication_summary"/>
+        <result property="attritionLevel" column="attrition_level"/>
+        <result property="attritionLevelPrompt" column="attrition_level_prompt"/>
+        <result property="customerFocusJson" column="customer_focus_json"/>
+        <result property="intentionDegree" column="intention_degree"/>
+        <result property="aiChatRecord" column="ai_chat_record"/>
+        <result property="createTime" column="create_time"/>
+        <result property="remark" column="remark"/>
+        <result property="reserveInt" column="reserve_int"/>
+        <result property="reserveStr" column="reserve_str"/>
+        <result property="corpId" column="corp_id"/>
+        <result property="qwUserId" column="qw_user_id"/>
+        <result property="sessionId" column="session_id"/>
+    </resultMap>
+
+    <sql id="selectQwExternalAiAnalyzeVo">
+        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
+    </sql>
+
+    <insert id="insertBatch" parameterType="java.util.List" useGeneratedKeys="true" keyProperty="id" keyColumn="id">
+        insert into qw_external_ai_analyze
+        (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)
+        values
+        <foreach collection="list" item="item" separator=",">
+            (#{item.externalUserId}, #{item.customerName}, #{item.customerPortraitJson}, #{item.communicationAbstract},
+             #{item.communicationSummary}, #{item.attritionLevel}, #{item.attritionLevelPrompt}, #{item.customerFocusJson},
+             #{item.intentionDegree}, #{item.aiChatRecord}, #{item.createTime}, #{item.remark},
+             #{item.reserveInt}, #{item.reserveStr}, #{item.corpId}, #{item.qwUserId}, #{item.sessionId})
+        </foreach>
+    </insert>
+    <select id="selectQwExternalAiAnalyzeList" resultType="com.fs.qw.domain.QwExternalAiAnalyze">
+        <include refid="selectQwExternalAiAnalyzeVo"/>
+        <where>
+            <if test="externalUserId != null and externalUserId != ''"> and external_user_id = #{externalUserId}</if>
+            <if test="customerName != null  and customerName != ''"> and customer_name like concat('%', #{customerName}, '%')</if>
+            <if test="customerPortraitJson != null  and customerPortraitJson != ''"> and customer_portrait_json = #{customerPortraitJson}</if>
+            <if test="communicationAbstract != null  and communicationAbstract != ''"> and communication_abstract = #{communicationAbstract}</if>
+            <if test="communicationSummary != null  and communicationSummary != ''"> and communication_summary = #{communicationSummary}</if>
+            <if test="attritionLevel != null "> and attrition_level = #{attritionLevel}</if>
+            <if test="attritionLevelPrompt != null  and attritionLevelPrompt != ''"> and attrition_level_prompt like concat('%', #{attritionLevelPrompt}, '%')</if>
+            <if test="customerFocusJson != null  and customerFocusJson != ''"> and customer_focus_json = #{customerFocusJson}</if>
+            <if test="intentionDegree != null "> and intention_degree = #{intentionDegree}</if>
+            <if test="aiChatRecord != null  and aiChatRecord != ''"> and ai_chat_record = #{aiChatRecord}</if>
+            <if test="reserveInt != null "> and reserve_int = #{reserveInt}</if>
+            <if test="reserveStr != null  and reserveStr != ''"> and reserve_str = #{reserveStr}</if>
+            <if test="corpId != null  and corpId != ''"> and corp_id = #{corpId}</if>
+            <if test="qwUserId != null  and qwUserId != ''"> and qw_user_id = #{qwUserId}</if>
+        </where>
+        order by create_time desc
+    </select>
+
+</mapper>

+ 29 - 0
fs-service/src/main/resources/mapper/qw/QwMsgAuditMessageMapper.xml

@@ -61,10 +61,39 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="mediaFileName != null  and mediaFileName != ''"> and media_file_name like concat('%', #{mediaFileName}, '%')</if>
             <if test="mediaFileExt != null  and mediaFileExt != ''"> and media_file_ext = #{mediaFileExt}</if>
             <if test="rawId != null "> and raw_id = #{rawId}</if>
+            <if test="analyzeStartTime != null">and msg_time &gt;= #{analyzeStartTime}</if>
         </where>
             order by msg_time desc
     </select>
 
+    <select id="selectQwMsgAuditMessageListByShard" resultMap="QwMsgAuditMessageResult">
+        <include refid="selectQwMsgAuditMessageVo"/>
+        qw_msg_audit_message_${shard}
+        <where>
+            <if test="qwMsgAuditMessage.seq != null "> and seq = #{qwMsgAuditMessage.seq}</if>
+            <if test="qwMsgAuditMessage.msgId != null  and qwMsgAuditMessage.msgId != ''"> and msg_id = #{qwMsgAuditMessage.msgId}</if>
+            <if test="qwMsgAuditMessage.msgTime != null "> and msg_time = #{qwMsgAuditMessage.msgTime}</if>
+            <if test="qwMsgAuditMessage.msgType != null  and qwMsgAuditMessage.msgType != ''"> and msg_type = #{qwMsgAuditMessage.msgType}</if>
+            <if test="qwMsgAuditMessage.fromUser != null  and qwMsgAuditMessage.fromUser != ''"> and from_user = #{qwMsgAuditMessage.fromUser}</if>
+            <if test="qwMsgAuditMessage.toList != null  and qwMsgAuditMessage.toList != ''"> and to_list = #{qwMsgAuditMessage.toList}</if>
+            <if test="qwMsgAuditMessage.roomId != null  and qwMsgAuditMessage.roomId != ''"> and room_id = #{qwMsgAuditMessage.roomId}</if>
+            <if test="qwMsgAuditMessage.conversationKey != null  and qwMsgAuditMessage.conversationKey != ''"> and conversation_key = #{qwMsgAuditMessage.conversationKey}</if>
+            <if test="qwMsgAuditMessage.fromUserRole != null "> and from_user_role = #{qwMsgAuditMessage.fromUserRole}</if>
+            <if test="qwMsgAuditMessage.textContent != null  and qwMsgAuditMessage.textContent != ''"> and text_content = #{qwMsgAuditMessage.textContent}</if>
+            <if test="qwMsgAuditMessage.mediaSdkfileid != null  and qwMsgAuditMessage.mediaSdkfileid != ''"> and media_sdkfileid = #{qwMsgAuditMessage.mediaSdkfileid}</if>
+            <if test="qwMsgAuditMessage.mediaMd5sum != null  and qwMsgAuditMessage.mediaMd5sum != ''"> and media_md5sum = #{qwMsgAuditMessage.mediaMd5sum}</if>
+            <if test="qwMsgAuditMessage.mediaSize != null "> and media_size = #{qwMsgAuditMessage.mediaSize}</if>
+            <if test="qwMsgAuditMessage.mediaPlayLength != null "> and media_play_length = #{qwMsgAuditMessage.mediaPlayLength}</if>
+            <if test="qwMsgAuditMessage.mediaWidth != null "> and media_width = #{qwMsgAuditMessage.mediaWidth}</if>
+            <if test="qwMsgAuditMessage.mediaHeight != null "> and media_height = #{qwMsgAuditMessage.mediaHeight}</if>
+            <if test="qwMsgAuditMessage.mediaFileName != null  and qwMsgAuditMessage.mediaFileName != ''"> and media_file_name like concat('%', #{qwMsgAuditMessage.mediaFileName}, '%')</if>
+            <if test="qwMsgAuditMessage.mediaFileExt != null  and qwMsgAuditMessage.mediaFileExt != ''"> and media_file_ext = #{qwMsgAuditMessage.mediaFileExt}</if>
+            <if test="qwMsgAuditMessage.rawId != null "> and raw_id = #{qwMsgAuditMessage.rawId}</if>
+            <if test="qwMsgAuditMessage.analyzeStartTime != null">and msg_time &gt;= #{qwMsgAuditMessage.analyzeStartTime}</if>
+        </where>
+        order by msg_time desc
+    </select>
+
     <select id="selectQwMsgAuditMessageById" resultMap="QwMsgAuditMessageResult">
         <include refid="selectQwMsgAuditMessageVo"/>
         qw_msg_audit_message_${shard}