|
@@ -6,19 +6,29 @@ import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
|
|
import com.etotem.cfc.entity.DanKnowledgeBase;
|
|
import com.etotem.cfc.entity.DanKnowledgeBase;
|
|
|
import com.etotem.cfc.entity.DanKnowledgeBaseDimension;
|
|
import com.etotem.cfc.entity.DanKnowledgeBaseDimension;
|
|
|
import com.etotem.cfc.entity.DanKnowledgeBaseTag;
|
|
import com.etotem.cfc.entity.DanKnowledgeBaseTag;
|
|
|
|
|
+import com.etotem.cfc.entity.DanKnowledgeTag;
|
|
|
import com.etotem.cfc.entity.ProductDimensionConfig;
|
|
import com.etotem.cfc.entity.ProductDimensionConfig;
|
|
|
import com.etotem.cfc.mapper.DanKnowledgeBaseDimensionMapper;
|
|
import com.etotem.cfc.mapper.DanKnowledgeBaseDimensionMapper;
|
|
|
import com.etotem.cfc.mapper.DanKnowledgeBaseMapper;
|
|
import com.etotem.cfc.mapper.DanKnowledgeBaseMapper;
|
|
|
import com.etotem.cfc.mapper.DanKnowledgeBaseTagMapper;
|
|
import com.etotem.cfc.mapper.DanKnowledgeBaseTagMapper;
|
|
|
|
|
+import com.etotem.cfc.mapper.DanKnowledgeTagMapper;
|
|
|
import com.etotem.cfc.mapper.ProductDimensionConfigMapper;
|
|
import com.etotem.cfc.mapper.ProductDimensionConfigMapper;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import org.springframework.stereotype.Service;
|
|
import org.springframework.stereotype.Service;
|
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
import org.springframework.transaction.annotation.Transactional;
|
|
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
import javax.annotation.Resource;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.Arrays;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.Comparator;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
|
|
|
+import java.util.LinkedHashMap;
|
|
|
|
|
+import java.util.LinkedHashSet;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
|
|
+import java.util.Objects;
|
|
|
|
|
+import java.util.Set;
|
|
|
import java.util.stream.Collectors;
|
|
import java.util.stream.Collectors;
|
|
|
import com.etotem.cfc.util.SortUtil;
|
|
import com.etotem.cfc.util.SortUtil;
|
|
|
|
|
|
|
@@ -32,6 +42,9 @@ public class DanKnowledgeBaseService extends ServiceImpl<DanKnowledgeBaseMapper,
|
|
|
@Resource
|
|
@Resource
|
|
|
private DanKnowledgeBaseTagMapper danKnowledgeBaseTagMapper;
|
|
private DanKnowledgeBaseTagMapper danKnowledgeBaseTagMapper;
|
|
|
|
|
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private DanKnowledgeTagMapper danKnowledgeTagMapper;
|
|
|
|
|
+
|
|
|
@Resource
|
|
@Resource
|
|
|
private ProductDimensionConfigMapper productDimensionConfigMapper;
|
|
private ProductDimensionConfigMapper productDimensionConfigMapper;
|
|
|
|
|
|
|
@@ -39,6 +52,11 @@ public class DanKnowledgeBaseService extends ServiceImpl<DanKnowledgeBaseMapper,
|
|
|
private LanggraphSyncService langgraphSyncService;
|
|
private LanggraphSyncService langgraphSyncService;
|
|
|
|
|
|
|
|
public Map<String, Object> list(String keyword, Integer status, int page, int size) {
|
|
public Map<String, Object> list(String keyword, Integer status, int page, int size) {
|
|
|
|
|
+ return list(keyword, status, page, size, null, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ public Map<String, Object> list(String keyword, Integer status, int page, int size,
|
|
|
|
|
+ Long tagId, Long dimensionId) {
|
|
|
Page<DanKnowledgeBase> p = new Page<>(page, size);
|
|
Page<DanKnowledgeBase> p = new Page<>(page, size);
|
|
|
LambdaQueryWrapper<DanKnowledgeBase> wrapper = new LambdaQueryWrapper<>();
|
|
LambdaQueryWrapper<DanKnowledgeBase> wrapper = new LambdaQueryWrapper<>();
|
|
|
if (keyword != null && !keyword.isEmpty()) {
|
|
if (keyword != null && !keyword.isEmpty()) {
|
|
@@ -48,6 +66,17 @@ public class DanKnowledgeBaseService extends ServiceImpl<DanKnowledgeBaseMapper,
|
|
|
if (status != null) {
|
|
if (status != null) {
|
|
|
wrapper.eq(DanKnowledgeBase::getStatus, status);
|
|
wrapper.eq(DanKnowledgeBase::getStatus, status);
|
|
|
}
|
|
}
|
|
|
|
|
+ // 标签过滤:经关联表反查 knowledge_id;无命中用 -1 常量避免 in 空集导致 SQL 语法错误
|
|
|
|
|
+ if (tagId != null) {
|
|
|
|
|
+ List<Long> ids = knowledgeIdsByTag(tagId);
|
|
|
|
|
+ wrapper.in(DanKnowledgeBase::getId,
|
|
|
|
|
+ ids.isEmpty() ? Collections.singletonList(-1L) : ids);
|
|
|
|
|
+ }
|
|
|
|
|
+ if (dimensionId != null) {
|
|
|
|
|
+ List<Long> ids = knowledgeIdsByDimension(dimensionId);
|
|
|
|
|
+ wrapper.in(DanKnowledgeBase::getId,
|
|
|
|
|
+ ids.isEmpty() ? Collections.singletonList(-1L) : ids);
|
|
|
|
|
+ }
|
|
|
wrapper.orderByAsc(DanKnowledgeBase::getSort);
|
|
wrapper.orderByAsc(DanKnowledgeBase::getSort);
|
|
|
this.page(p, wrapper);
|
|
this.page(p, wrapper);
|
|
|
|
|
|
|
@@ -60,6 +89,22 @@ public class DanKnowledgeBaseService extends ServiceImpl<DanKnowledgeBaseMapper,
|
|
|
return data;
|
|
return data;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ private List<Long> knowledgeIdsByTag(Long tagId) {
|
|
|
|
|
+ List<DanKnowledgeBaseTag> rels = danKnowledgeBaseTagMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<DanKnowledgeBaseTag>()
|
|
|
|
|
+ .eq(DanKnowledgeBaseTag::getTagId, tagId));
|
|
|
|
|
+ return rels.stream().map(DanKnowledgeBaseTag::getKnowledgeId)
|
|
|
|
|
+ .distinct().collect(Collectors.toList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private List<Long> knowledgeIdsByDimension(Long dimensionId) {
|
|
|
|
|
+ List<DanKnowledgeBaseDimension> rels = danKnowledgeBaseDimensionMapper.selectList(
|
|
|
|
|
+ new LambdaQueryWrapper<DanKnowledgeBaseDimension>()
|
|
|
|
|
+ .eq(DanKnowledgeBaseDimension::getDimensionId, dimensionId));
|
|
|
|
|
+ return rels.stream().map(DanKnowledgeBaseDimension::getKnowledgeId)
|
|
|
|
|
+ .distinct().collect(Collectors.toList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
@Transactional(rollbackFor = Exception.class)
|
|
@Transactional(rollbackFor = Exception.class)
|
|
|
public DanKnowledgeBase saveKnowledge(DanKnowledgeBase knowledge) {
|
|
public DanKnowledgeBase saveKnowledge(DanKnowledgeBase knowledge) {
|
|
|
if (knowledge.getId() != null) {
|
|
if (knowledge.getId() != null) {
|
|
@@ -178,4 +223,125 @@ public class DanKnowledgeBaseService extends ServiceImpl<DanKnowledgeBaseMapper,
|
|
|
.filter(code -> code != null)
|
|
.filter(code -> code != null)
|
|
|
.collect(Collectors.toList());
|
|
.collect(Collectors.toList());
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // ==================== 标签/维度自动回填 ====================
|
|
|
|
|
+
|
|
|
|
|
+ private static final int MAX_TAGS_PER_KB = 12;
|
|
|
|
|
+
|
|
|
|
|
+ private static final Map<String, List<String>> DIMENSION_KEYWORDS;
|
|
|
|
|
+
|
|
|
|
|
+ static {
|
|
|
|
|
+ Map<String, List<String>> m = new LinkedHashMap<>();
|
|
|
|
|
+ m.put("body", Arrays.asList(
|
|
|
|
|
+ "饮食", "营养", "膳食", "肠道", "菌群", "微生物", "代谢", "血糖", "血脂", "血压",
|
|
|
|
|
+ "尿酸", "痛风", "心脑血管", "血管", "心脏", "肝脏", "肾脏", "免疫", "炎症",
|
|
|
|
|
+ "抗炎", "抗氧化", "自由基", "脂肪", "体重", "减脂", "骨骼", "关节", "骨密度",
|
|
|
|
|
+ "甲状腺", "内分泌", "消化", "胃", "肠", "过敏", "湿疹", "皮肤", "口腔", "眼睛",
|
|
|
|
|
+ "视力", "呼吸", "肺", "饮水", "饮用水", "水质", "矿物质", "钙", "镁", "维生素",
|
|
|
|
|
+ "蛋白质", "电解", "杀菌", "消毒", "致病菌", "体质", "代谢综合征"
|
|
|
|
|
+ ));
|
|
|
|
|
+ m.put("wisdom", Arrays.asList(
|
|
|
|
|
+ "认知", "大脑", "记忆", "专注", "注意力", "学习", "儿童", "教育", "测评",
|
|
|
|
|
+ "评估", "检测", "智商", "智力", "思维", "多动", "自闭", "菌群检测", "儿童成长"
|
|
|
|
|
+ ));
|
|
|
|
|
+ m.put("wealth", Arrays.asList(
|
|
|
|
|
+ "财富", "理财", "投资", "经济", "消费", "收益", "成本", "价值", "省钱", "创富"
|
|
|
|
|
+ ));
|
|
|
|
|
+ m.put("action", Arrays.asList(
|
|
|
|
|
+ "习惯", "日常", "管理", "计划", "坚持", "打卡", "预防", "干预", "方案", "调理",
|
|
|
|
|
+ "改善", "生活方式", "作息", "行动", "自测"
|
|
|
|
|
+ ));
|
|
|
|
|
+ m.put("mind", Arrays.asList(
|
|
|
|
|
+ "情绪", "心理", "压力", "焦虑", "抑郁", "幸福", "亲子", "家庭", "关系", "陪伴",
|
|
|
|
|
+ "疗愈", "正念", "冥想", "心态", "大爱", "睡眠"
|
|
|
|
|
+ ));
|
|
|
|
|
+ DIMENSION_KEYWORDS = Collections.unmodifiableMap(m);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 标签关键词匹配:2 字词仅标题命中;>=3 字词标题/正文均可;组合标签按分隔符拆分 */
|
|
|
|
|
+ public static Set<Long> matchTagIds(String title, String content, List<DanKnowledgeTag> tags) {
|
|
|
|
|
+ Set<Long> hit = new LinkedHashSet<>();
|
|
|
|
|
+ String t = title == null ? "" : title;
|
|
|
|
|
+ String c = content == null ? "" : content;
|
|
|
|
|
+ List<DanKnowledgeTag> sorted = new ArrayList<>(tags);
|
|
|
|
|
+ sorted.sort(Comparator
|
|
|
|
|
+ .comparingInt((DanKnowledgeTag tag) -> tag.getTagName() == null ? 0 : tag.getTagName().length())
|
|
|
|
|
+ .reversed());
|
|
|
|
|
+ for (DanKnowledgeTag tag : sorted) {
|
|
|
|
|
+ if (hit.size() >= MAX_TAGS_PER_KB) {
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ String name = tag.getTagName();
|
|
|
|
|
+ if (name == null || name.isEmpty()) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ for (String kw : name.split("[,\uFF0C]")) {
|
|
|
|
|
+ kw = kw.trim();
|
|
|
|
|
+ if (kw.isEmpty()) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ boolean inTitle = t.contains(kw);
|
|
|
|
|
+ boolean inContent = c.contains(kw);
|
|
|
|
|
+ if (inTitle || (kw.length() >= 3 && inContent)) {
|
|
|
|
|
+ hit.add(tag.getId());
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return hit;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 维度关键词匹配:命中任一关键词即归入该维度 */
|
|
|
|
|
+ public static Set<String> matchDimensionCodes(String title, String content) {
|
|
|
|
|
+ String text = (title == null ? "" : title) + "\n" + (content == null ? "" : content);
|
|
|
|
|
+ Set<String> hit = new LinkedHashSet<>();
|
|
|
|
|
+ for (Map.Entry<String, List<String>> e : DIMENSION_KEYWORDS.entrySet()) {
|
|
|
|
|
+ for (String kw : e.getValue()) {
|
|
|
|
|
+ if (text.contains(kw)) {
|
|
|
|
|
+ hit.add(e.getKey());
|
|
|
|
|
+ break;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return hit;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 为全部启用知识回填标签与维度绑定,返回统计 */
|
|
|
|
|
+ @Transactional(rollbackFor = Exception.class)
|
|
|
|
|
+ public Map<String, Object> backfillTags() {
|
|
|
|
|
+ List<DanKnowledgeTag> tags = danKnowledgeTagMapper.selectList(null);
|
|
|
|
|
+ List<DanKnowledgeBase> list = this.list(new LambdaQueryWrapper<DanKnowledgeBase>()
|
|
|
|
|
+ .eq(DanKnowledgeBase::getStatus, 1));
|
|
|
|
|
+ int tagBound = 0;
|
|
|
|
|
+ int dimBound = 0;
|
|
|
|
|
+ for (DanKnowledgeBase kb : list) {
|
|
|
|
|
+ Set<Long> tagIds = matchTagIds(kb.getTitle(), kb.getContent(), tags);
|
|
|
|
|
+ if (!tagIds.isEmpty()) {
|
|
|
|
|
+ bindTags(kb.getId(), new ArrayList<>(tagIds));
|
|
|
|
|
+ tagBound++;
|
|
|
|
|
+ }
|
|
|
|
|
+ Set<String> codes = matchDimensionCodes(kb.getTitle(), kb.getContent());
|
|
|
|
|
+ List<Long> dimIds = codes.stream()
|
|
|
|
|
+ .map(this::dimensionIdByCode)
|
|
|
|
|
+ .filter(Objects::nonNull)
|
|
|
|
|
+ .distinct()
|
|
|
|
|
+ .collect(Collectors.toList());
|
|
|
|
|
+ if (!dimIds.isEmpty()) {
|
|
|
|
|
+ bindDimensions(kb.getId(), dimIds);
|
|
|
|
|
+ dimBound++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ Map<String, Object> r = new HashMap<>();
|
|
|
|
|
+ r.put("knowledgeCount", list.size());
|
|
|
|
|
+ r.put("tagBoundCount", tagBound);
|
|
|
|
|
+ r.put("dimensionBoundCount", dimBound);
|
|
|
|
|
+ return r;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Long dimensionIdByCode(String code) {
|
|
|
|
|
+ ProductDimensionConfig cfg = productDimensionConfigMapper.selectOne(
|
|
|
|
|
+ new LambdaQueryWrapper<ProductDimensionConfig>()
|
|
|
|
|
+ .eq(ProductDimensionConfig::getDimensionCode, code));
|
|
|
|
|
+ return cfg != null ? cfg.getId() : null;
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|