Просмотр исходного кода

feat: add knowledge base service with Dify sync support

Ultraworked with Sisyphus

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
Sisyphus 3 месяцев назад
Родитель
Сommit
3278054113

+ 139 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DanKnowledgeBaseService.java

@@ -0,0 +1,139 @@
+package com.etotem.cfc.service;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.etotem.cfc.entity.DanKnowledgeBase;
+import com.etotem.cfc.entity.DanKnowledgeBaseDimension;
+import com.etotem.cfc.entity.DanKnowledgeBaseTag;
+import com.etotem.cfc.mapper.DanKnowledgeBaseDimensionMapper;
+import com.etotem.cfc.mapper.DanKnowledgeBaseMapper;
+import com.etotem.cfc.mapper.DanKnowledgeBaseTagMapper;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import javax.annotation.Resource;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Service
+public class DanKnowledgeBaseService extends ServiceImpl<DanKnowledgeBaseMapper, DanKnowledgeBase> {
+
+    @Resource
+    private DanKnowledgeBaseDimensionMapper danKnowledgeBaseDimensionMapper;
+
+    @Resource
+    private DanKnowledgeBaseTagMapper danKnowledgeBaseTagMapper;
+
+    @Resource
+    private DifySyncService difySyncService;
+
+    public Map<String, Object> list(String keyword, Integer status, int page, int size) {
+        Page<DanKnowledgeBase> p = new Page<>(page, size);
+        LambdaQueryWrapper<DanKnowledgeBase> wrapper = new LambdaQueryWrapper<>();
+        if (keyword != null && !keyword.isEmpty()) {
+            wrapper.and(w -> w.like(DanKnowledgeBase::getTitle, keyword)
+                .or().like(DanKnowledgeBase::getContent, keyword));
+        }
+        if (status != null) {
+            wrapper.eq(DanKnowledgeBase::getStatus, status);
+        }
+        wrapper.orderByAsc(DanKnowledgeBase::getSort);
+        this.page(p, wrapper);
+        Map<String, Object> data = new HashMap<>();
+        data.put("records", p.getRecords());
+        data.put("total", p.getTotal());
+        data.put("page", p.getCurrent());
+        data.put("size", p.getSize());
+        return data;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public DanKnowledgeBase saveKnowledge(DanKnowledgeBase knowledge) {
+        if (knowledge.getId() != null) {
+            this.updateById(knowledge);
+        } else {
+            this.save(knowledge);
+        }
+        return knowledge;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public DanKnowledgeBase saveWithAssociations(DanKnowledgeBase knowledge,
+                                                  List<Long> dimensionIds,
+                                                  List<Long> tagIds) {
+        if (knowledge.getId() != null) {
+            this.updateById(knowledge);
+        } else {
+            this.save(knowledge);
+        }
+        if (dimensionIds != null) {
+            bindDimensions(knowledge.getId(), dimensionIds);
+        }
+        if (tagIds != null) {
+            bindTags(knowledge.getId(), tagIds);
+        }
+        return knowledge;
+    }
+
+    public Map<String, Object> getWithAssociations(Long id) {
+        DanKnowledgeBase knowledge = this.getById(id);
+        if (knowledge == null) {
+            return null;
+        }
+        List<Long> dimensionIds = danKnowledgeBaseDimensionMapper.selectList(
+            new LambdaQueryWrapper<DanKnowledgeBaseDimension>()
+                .eq(DanKnowledgeBaseDimension::getKnowledgeId, id))
+            .stream().map(DanKnowledgeBaseDimension::getDimensionId)
+            .collect(Collectors.toList());
+        List<Long> tagIds = danKnowledgeBaseTagMapper.selectList(
+            new LambdaQueryWrapper<DanKnowledgeBaseTag>()
+                .eq(DanKnowledgeBaseTag::getKnowledgeId, id))
+            .stream().map(DanKnowledgeBaseTag::getTagId)
+            .collect(Collectors.toList());
+        Map<String, Object> result = new HashMap<>();
+        result.put("knowledge", knowledge);
+        result.put("dimensionIds", dimensionIds);
+        result.put("tagIds", tagIds);
+        return result;
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public boolean deleteKnowledge(Long id) {
+        danKnowledgeBaseDimensionMapper.delete(
+            new LambdaQueryWrapper<DanKnowledgeBaseDimension>()
+                .eq(DanKnowledgeBaseDimension::getKnowledgeId, id));
+        danKnowledgeBaseTagMapper.delete(
+            new LambdaQueryWrapper<DanKnowledgeBaseTag>()
+                .eq(DanKnowledgeBaseTag::getKnowledgeId, id));
+        return this.removeById(id);
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public void bindDimensions(Long knowledgeId, List<Long> dimensionIds) {
+        danKnowledgeBaseDimensionMapper.delete(
+            new LambdaQueryWrapper<DanKnowledgeBaseDimension>()
+                .eq(DanKnowledgeBaseDimension::getKnowledgeId, knowledgeId));
+        for (Long dimensionId : dimensionIds) {
+            DanKnowledgeBaseDimension rel = new DanKnowledgeBaseDimension();
+            rel.setKnowledgeId(knowledgeId);
+            rel.setDimensionId(dimensionId);
+            danKnowledgeBaseDimensionMapper.insert(rel);
+        }
+    }
+
+    @Transactional(rollbackFor = Exception.class)
+    public void bindTags(Long knowledgeId, List<Long> tagIds) {
+        danKnowledgeBaseTagMapper.delete(
+            new LambdaQueryWrapper<DanKnowledgeBaseTag>()
+                .eq(DanKnowledgeBaseTag::getKnowledgeId, knowledgeId));
+        for (Long tagId : tagIds) {
+            DanKnowledgeBaseTag rel = new DanKnowledgeBaseTag();
+            rel.setKnowledgeId(knowledgeId);
+            rel.setTagId(tagId);
+            danKnowledgeBaseTagMapper.insert(rel);
+        }
+    }
+}

+ 68 - 0
cfc-backend/src/main/java/com/etotem/cfc/service/DifySyncService.java

@@ -0,0 +1,68 @@
+package com.etotem.cfc.service;
+
+import com.etotem.cfc.entity.DanKnowledgeBase;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.scheduling.annotation.Async;
+import org.springframework.stereotype.Service;
+
+import java.util.List;
+
+/**
+ * Dify 知识库同步服务
+ *
+ * 在知识点新增/编辑/删除时异步推送到 Dify Dataset。
+ * Dify 调用失败不影响主流程,仅记录 error log。
+ */
+@Service
+public class DifySyncService {
+
+    private static final Logger log = LoggerFactory.getLogger(DifySyncService.class);
+
+    @Value("${dify.base-url}")
+    private String difyBaseUrl;
+
+    @Value("${dify.api-key}")
+    private String difyApiKey;
+
+    /**
+     * 推送知识点到 Dify(异步,不阻塞主流程)
+     *
+     * @param knowledge      知识点
+     * @param dimensionCodes 关联维度代码列表(每个维度对应一个 Dify dataset)
+     */
+    @Async
+    public void syncToDify(DanKnowledgeBase knowledge, List<String> dimensionCodes) {
+        if (dimensionCodes == null || dimensionCodes.isEmpty()) {
+            return;
+        }
+        for (String code : dimensionCodes) {
+            try {
+                // TODO: 根据 dimensionCode 查找 product_dimension_config 中的 dify_dataset_id
+                // 如果 dify_dataset_id 为空则跳过
+                // 调用 Dify API: POST /v1/datasets/{dataset_id}/documents
+                // 文档标题: knowledge.title, 内容: knowledge.content
+                log.info("准备同步知识点[{}]到维度[{}]的 Dify dataset", knowledge.getTitle(), code);
+            } catch (Exception e) {
+                log.error("同步知识点[{}]到维度[{}]的 Dify dataset 失败", knowledge.getTitle(), code, e);
+            }
+        }
+    }
+
+    /**
+     * 从 Dify 删除知识点文档(异步)
+     *
+     * @param knowledgeId   知识点ID
+     * @param difyDatasetId Dify Dataset ID
+     */
+    @Async
+    public void removeFromDify(Long knowledgeId, String difyDatasetId) {
+        try {
+            // TODO: 调用 Dify API: DELETE /v1/datasets/{dataset_id}/documents/{doc_id}
+            log.info("准备从 Dify dataset[{}]删除知识点[{}]", difyDatasetId, knowledgeId);
+        } catch (Exception e) {
+            log.error("从 Dify 删除知识点[{}]失败", knowledgeId, e);
+        }
+    }
+}