feat(ai): add three-level FAQ category management
- add FAQ category table/sql migration and initialize ddl updates - add category service/controller with validation, default category rules, and sorting - support faq item category binding and category-based filtering (include descendants) - redesign FAQ page with category tree actions and UI polish
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
package tech.easyflow.ai.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import tech.easyflow.ai.entity.base.FaqCategoryBase;
|
||||
|
||||
@Table("tb_faq_category")
|
||||
public class FaqCategory extends FaqCategoryBase {
|
||||
}
|
||||
@@ -1,8 +1,31 @@
|
||||
package tech.easyflow.ai.entity;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Table;
|
||||
import tech.easyflow.ai.entity.base.FaqItemBase;
|
||||
|
||||
@Table("tb_faq_item")
|
||||
public class FaqItem extends FaqItemBase {
|
||||
|
||||
@Column(ignore = true)
|
||||
private String categoryName;
|
||||
|
||||
@Column(ignore = true)
|
||||
private String categoryPath;
|
||||
|
||||
public String getCategoryName() {
|
||||
return categoryName;
|
||||
}
|
||||
|
||||
public void setCategoryName(String categoryName) {
|
||||
this.categoryName = categoryName;
|
||||
}
|
||||
|
||||
public String getCategoryPath() {
|
||||
return categoryPath;
|
||||
}
|
||||
|
||||
public void setCategoryPath(String categoryPath) {
|
||||
this.categoryPath = categoryPath;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
package tech.easyflow.ai.entity.base;
|
||||
|
||||
import com.mybatisflex.annotation.Column;
|
||||
import com.mybatisflex.annotation.Id;
|
||||
import com.mybatisflex.annotation.KeyType;
|
||||
import tech.easyflow.common.entity.DateTreeEntity;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.math.BigInteger;
|
||||
import java.util.Date;
|
||||
|
||||
public class FaqCategoryBase extends DateTreeEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Id(keyType = KeyType.Generator, value = "snowFlakeId", comment = "主键")
|
||||
private BigInteger id;
|
||||
|
||||
@Column(comment = "知识库ID")
|
||||
private BigInteger collectionId;
|
||||
|
||||
@Column(comment = "父分类ID,0表示根")
|
||||
private BigInteger parentId;
|
||||
|
||||
@Column(comment = "祖先路径(逗号分隔)")
|
||||
private String ancestors;
|
||||
|
||||
@Column(comment = "层级(1-3)")
|
||||
private Integer levelNo;
|
||||
|
||||
@Column(comment = "分类名称")
|
||||
private String categoryName;
|
||||
|
||||
@Column(comment = "排序")
|
||||
private Integer sortNo;
|
||||
|
||||
@Column(comment = "是否默认分类")
|
||||
private Boolean isDefault;
|
||||
|
||||
@Column(comment = "数据状态")
|
||||
private Integer status;
|
||||
|
||||
@Column(comment = "创建时间")
|
||||
private Date created;
|
||||
|
||||
@Column(comment = "创建人")
|
||||
private BigInteger createdBy;
|
||||
|
||||
@Column(comment = "更新时间")
|
||||
private Date modified;
|
||||
|
||||
@Column(comment = "更新人")
|
||||
private BigInteger modifiedBy;
|
||||
|
||||
public BigInteger getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(BigInteger id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public BigInteger getCollectionId() {
|
||||
return collectionId;
|
||||
}
|
||||
|
||||
public void setCollectionId(BigInteger collectionId) {
|
||||
this.collectionId = collectionId;
|
||||
}
|
||||
|
||||
public BigInteger getParentId() {
|
||||
return parentId;
|
||||
}
|
||||
|
||||
public void setParentId(BigInteger parentId) {
|
||||
this.parentId = parentId;
|
||||
}
|
||||
|
||||
public String getAncestors() {
|
||||
return ancestors;
|
||||
}
|
||||
|
||||
public void setAncestors(String ancestors) {
|
||||
this.ancestors = ancestors;
|
||||
}
|
||||
|
||||
public Integer getLevelNo() {
|
||||
return levelNo;
|
||||
}
|
||||
|
||||
public void setLevelNo(Integer levelNo) {
|
||||
this.levelNo = levelNo;
|
||||
}
|
||||
|
||||
public String getCategoryName() {
|
||||
return categoryName;
|
||||
}
|
||||
|
||||
public void setCategoryName(String categoryName) {
|
||||
this.categoryName = categoryName;
|
||||
}
|
||||
|
||||
public Integer getSortNo() {
|
||||
return sortNo;
|
||||
}
|
||||
|
||||
public void setSortNo(Integer sortNo) {
|
||||
this.sortNo = sortNo;
|
||||
}
|
||||
|
||||
public Boolean getIsDefault() {
|
||||
return isDefault;
|
||||
}
|
||||
|
||||
public void setIsDefault(Boolean isDefault) {
|
||||
this.isDefault = isDefault;
|
||||
}
|
||||
|
||||
public Integer getStatus() {
|
||||
return status;
|
||||
}
|
||||
|
||||
public void setStatus(Integer status) {
|
||||
this.status = status;
|
||||
}
|
||||
|
||||
public Date getCreated() {
|
||||
return created;
|
||||
}
|
||||
|
||||
public void setCreated(Date created) {
|
||||
this.created = created;
|
||||
}
|
||||
|
||||
public BigInteger getCreatedBy() {
|
||||
return createdBy;
|
||||
}
|
||||
|
||||
public void setCreatedBy(BigInteger createdBy) {
|
||||
this.createdBy = createdBy;
|
||||
}
|
||||
|
||||
public Date getModified() {
|
||||
return modified;
|
||||
}
|
||||
|
||||
public void setModified(Date modified) {
|
||||
this.modified = modified;
|
||||
}
|
||||
|
||||
public BigInteger getModifiedBy() {
|
||||
return modifiedBy;
|
||||
}
|
||||
|
||||
public void setModifiedBy(BigInteger modifiedBy) {
|
||||
this.modifiedBy = modifiedBy;
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,9 @@ public class FaqItemBase implements Serializable {
|
||||
@Column(comment = "知识库ID")
|
||||
private BigInteger collectionId;
|
||||
|
||||
@Column(comment = "FAQ分类ID")
|
||||
private BigInteger categoryId;
|
||||
|
||||
@Column(comment = "问题")
|
||||
private String question;
|
||||
|
||||
@@ -63,6 +66,14 @@ public class FaqItemBase implements Serializable {
|
||||
this.collectionId = collectionId;
|
||||
}
|
||||
|
||||
public BigInteger getCategoryId() {
|
||||
return categoryId;
|
||||
}
|
||||
|
||||
public void setCategoryId(BigInteger categoryId) {
|
||||
this.categoryId = categoryId;
|
||||
}
|
||||
|
||||
public String getQuestion() {
|
||||
return question;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
package tech.easyflow.ai.mapper;
|
||||
|
||||
import com.mybatisflex.core.BaseMapper;
|
||||
import tech.easyflow.ai.entity.FaqCategory;
|
||||
|
||||
public interface FaqCategoryMapper extends BaseMapper<FaqCategory> {
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package tech.easyflow.ai.service;
|
||||
|
||||
import com.mybatisflex.core.service.IService;
|
||||
import tech.easyflow.ai.entity.FaqCategory;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public interface FaqCategoryService extends IService<FaqCategory> {
|
||||
|
||||
BigInteger ensureDefaultCategory(BigInteger collectionId);
|
||||
|
||||
BigInteger ensureFaqItemCategory(BigInteger collectionId, BigInteger categoryId);
|
||||
|
||||
List<FaqCategory> listByCollection(BigInteger collectionId, Boolean asTree);
|
||||
|
||||
boolean saveCategory(FaqCategory entity);
|
||||
|
||||
boolean updateCategory(FaqCategory entity);
|
||||
|
||||
boolean removeCategory(BigInteger id);
|
||||
|
||||
List<BigInteger> findDescendantIds(BigInteger collectionId, BigInteger categoryId);
|
||||
|
||||
Map<BigInteger, String> buildPathMap(BigInteger collectionId);
|
||||
}
|
||||
@@ -0,0 +1,555 @@
|
||||
package tech.easyflow.ai.service.impl;
|
||||
|
||||
import cn.dev33.satoken.stp.StpUtil;
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import com.mybatisflex.spring.service.impl.ServiceImpl;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.FaqCategory;
|
||||
import tech.easyflow.ai.entity.FaqItem;
|
||||
import tech.easyflow.ai.mapper.FaqCategoryMapper;
|
||||
import tech.easyflow.ai.mapper.FaqItemMapper;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.FaqCategoryService;
|
||||
import tech.easyflow.common.tree.Tree;
|
||||
import tech.easyflow.common.util.StringUtil;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
|
||||
import javax.annotation.Resource;
|
||||
import java.math.BigInteger;
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
@Service
|
||||
public class FaqCategoryServiceImpl extends ServiceImpl<FaqCategoryMapper, FaqCategory> implements FaqCategoryService {
|
||||
|
||||
private static final BigInteger ROOT_PARENT_ID = BigInteger.ZERO;
|
||||
private static final ConcurrentHashMap<BigInteger, Object> DEFAULT_CATEGORY_LOCKS = new ConcurrentHashMap<>();
|
||||
|
||||
@Resource
|
||||
private DocumentCollectionService documentCollectionService;
|
||||
|
||||
@Resource
|
||||
private FaqItemMapper faqItemMapper;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public BigInteger ensureDefaultCategory(BigInteger collectionId) {
|
||||
Object lock = DEFAULT_CATEGORY_LOCKS.computeIfAbsent(collectionId, key -> new Object());
|
||||
synchronized (lock) {
|
||||
try {
|
||||
return ensureDefaultCategoryWithLock(collectionId);
|
||||
} finally {
|
||||
DEFAULT_CATEGORY_LOCKS.remove(collectionId, lock);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private BigInteger ensureDefaultCategoryWithLock(BigInteger collectionId) {
|
||||
DocumentCollection collection = checkFaqCollection(collectionId);
|
||||
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.eq(FaqCategory::getCollectionId, collection.getId())
|
||||
.eq(FaqCategory::getIsDefault, true)
|
||||
.orderBy("id asc");
|
||||
List<FaqCategory> defaultCategories = list(queryWrapper);
|
||||
FaqCategory defaultCategory;
|
||||
if (defaultCategories == null || defaultCategories.isEmpty()) {
|
||||
Date now = new Date();
|
||||
BigInteger userId = getCurrentUserId();
|
||||
FaqCategory entity = new FaqCategory();
|
||||
entity.setCollectionId(collectionId);
|
||||
entity.setParentId(ROOT_PARENT_ID);
|
||||
entity.setAncestors("0");
|
||||
entity.setLevelNo(1);
|
||||
entity.setCategoryName("默认分类");
|
||||
entity.setSortNo(0);
|
||||
entity.setIsDefault(true);
|
||||
entity.setStatus(0);
|
||||
entity.setCreated(now);
|
||||
entity.setCreatedBy(userId);
|
||||
entity.setModified(now);
|
||||
entity.setModifiedBy(userId);
|
||||
save(entity);
|
||||
defaultCategory = entity;
|
||||
} else {
|
||||
defaultCategory = defaultCategories.get(0);
|
||||
if (defaultCategories.size() > 1) {
|
||||
normalizeDuplicateDefaultCategories(collectionId, defaultCategory, defaultCategories);
|
||||
}
|
||||
}
|
||||
|
||||
backfillNullFaqItemCategory(collectionId, defaultCategory.getId());
|
||||
return defaultCategory.getId();
|
||||
}
|
||||
|
||||
private void normalizeDuplicateDefaultCategories(BigInteger collectionId,
|
||||
FaqCategory keepCategory,
|
||||
List<FaqCategory> defaultCategories) {
|
||||
if (keepCategory == null || defaultCategories == null || defaultCategories.size() <= 1) {
|
||||
return;
|
||||
}
|
||||
Date now = new Date();
|
||||
BigInteger userId = getCurrentUserId();
|
||||
|
||||
for (FaqCategory duplicate : defaultCategories) {
|
||||
if (duplicate == null || duplicate.getId() == null || keepCategory.getId().equals(duplicate.getId())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
QueryWrapper faqQuery = QueryWrapper.create()
|
||||
.eq(FaqItem::getCollectionId, collectionId)
|
||||
.eq(FaqItem::getCategoryId, duplicate.getId());
|
||||
FaqItem faqItem = new FaqItem();
|
||||
faqItem.setCategoryId(keepCategory.getId());
|
||||
faqItem.setModified(now);
|
||||
faqItem.setModifiedBy(userId);
|
||||
faqItemMapper.updateByQuery(faqItem, faqQuery);
|
||||
|
||||
QueryWrapper childQuery = QueryWrapper.create()
|
||||
.eq(FaqCategory::getCollectionId, collectionId)
|
||||
.eq(FaqCategory::getParentId, duplicate.getId());
|
||||
long childCount = mapper.selectCountByQuery(childQuery);
|
||||
if (childCount <= 0) {
|
||||
removeById(duplicate.getId());
|
||||
continue;
|
||||
}
|
||||
|
||||
duplicate.setIsDefault(false);
|
||||
if ("默认分类".equals(duplicate.getCategoryName())) {
|
||||
duplicate.setCategoryName("默认分类副本-" + duplicate.getId());
|
||||
}
|
||||
duplicate.setModified(now);
|
||||
duplicate.setModifiedBy(userId);
|
||||
updateById(duplicate);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public BigInteger ensureFaqItemCategory(BigInteger collectionId, BigInteger categoryId) {
|
||||
checkFaqCollection(collectionId);
|
||||
BigInteger defaultCategoryId = ensureDefaultCategory(collectionId);
|
||||
if (categoryId == null) {
|
||||
return defaultCategoryId;
|
||||
}
|
||||
|
||||
FaqCategory category = getById(categoryId);
|
||||
if (category == null || !collectionId.equals(category.getCollectionId())) {
|
||||
throw new BusinessException("FAQ分类不存在或不属于当前知识库");
|
||||
}
|
||||
if (category.getStatus() != null && category.getStatus() != 0) {
|
||||
throw new BusinessException("FAQ分类不可用");
|
||||
}
|
||||
return categoryId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<FaqCategory> listByCollection(BigInteger collectionId, Boolean asTree) {
|
||||
checkFaqCollection(collectionId);
|
||||
ensureDefaultCategory(collectionId);
|
||||
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.eq(FaqCategory::getCollectionId, collectionId)
|
||||
.orderBy("is_default desc, sort_no asc, id asc");
|
||||
List<FaqCategory> categories = list(queryWrapper);
|
||||
if (asTree == null || asTree) {
|
||||
return Tree.tryToTree(categories, "id", "parentId");
|
||||
}
|
||||
return categories;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean saveCategory(FaqCategory entity) {
|
||||
if (entity == null) {
|
||||
throw new BusinessException("FAQ分类不能为空");
|
||||
}
|
||||
if (entity.getCollectionId() == null) {
|
||||
throw new BusinessException("知识库ID不能为空");
|
||||
}
|
||||
if (StringUtil.noText(entity.getCategoryName())) {
|
||||
throw new BusinessException("分类名称不能为空");
|
||||
}
|
||||
|
||||
checkFaqCollection(entity.getCollectionId());
|
||||
BigInteger parentId = normalizeParentId(entity.getParentId());
|
||||
validateParentNotDefault(entity.getCollectionId(), parentId);
|
||||
ParentMeta parentMeta = getParentMeta(entity.getCollectionId(), parentId, null);
|
||||
|
||||
Date now = new Date();
|
||||
BigInteger userId = getCurrentUserId();
|
||||
entity.setParentId(parentId);
|
||||
entity.setAncestors(parentMeta.ancestors);
|
||||
entity.setLevelNo(parentMeta.levelNo);
|
||||
entity.setCategoryName(entity.getCategoryName().trim());
|
||||
if (entity.getSortNo() == null) {
|
||||
entity.setSortNo(nextSortNo(entity.getCollectionId(), parentId));
|
||||
}
|
||||
entity.setIsDefault(false);
|
||||
if (entity.getStatus() == null) {
|
||||
entity.setStatus(0);
|
||||
}
|
||||
entity.setCreated(now);
|
||||
entity.setCreatedBy(userId);
|
||||
entity.setModified(now);
|
||||
entity.setModifiedBy(userId);
|
||||
|
||||
return save(entity);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean updateCategory(FaqCategory entity) {
|
||||
if (entity == null || entity.getId() == null) {
|
||||
throw new BusinessException("FAQ分类ID不能为空");
|
||||
}
|
||||
if (StringUtil.noText(entity.getCategoryName())) {
|
||||
throw new BusinessException("分类名称不能为空");
|
||||
}
|
||||
|
||||
FaqCategory old = getById(entity.getId());
|
||||
if (old == null) {
|
||||
throw new BusinessException("FAQ分类不存在");
|
||||
}
|
||||
|
||||
checkFaqCollection(old.getCollectionId());
|
||||
|
||||
BigInteger parentId = normalizeParentId(entity.getParentId() == null ? old.getParentId() : entity.getParentId());
|
||||
if (Boolean.TRUE.equals(old.getIsDefault()) && !ROOT_PARENT_ID.equals(parentId)) {
|
||||
throw new BusinessException("默认分类不能设置父级");
|
||||
}
|
||||
if (!parentId.equals(normalizeParentId(old.getParentId()))) {
|
||||
validateParentNotDefault(old.getCollectionId(), parentId);
|
||||
}
|
||||
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.eq(FaqCategory::getCollectionId, old.getCollectionId())
|
||||
.orderBy("sort_no asc, id asc");
|
||||
List<FaqCategory> allCategories = list(queryWrapper);
|
||||
Map<BigInteger, FaqCategory> categoryMap = new HashMap<>();
|
||||
for (FaqCategory category : allCategories) {
|
||||
categoryMap.put(category.getId(), category);
|
||||
}
|
||||
|
||||
if (!categoryMap.containsKey(old.getId())) {
|
||||
throw new BusinessException("FAQ分类不存在");
|
||||
}
|
||||
|
||||
if (parentId.equals(old.getId())) {
|
||||
throw new BusinessException("父分类不能是自己");
|
||||
}
|
||||
|
||||
Set<BigInteger> descendants = findDescendantSet(categoryMap, old.getId());
|
||||
if (descendants.contains(parentId)) {
|
||||
throw new BusinessException("父分类不能是当前分类的子分类");
|
||||
}
|
||||
|
||||
ParentMeta parentMeta = getParentMeta(old.getCollectionId(), parentId, old.getId());
|
||||
|
||||
Date now = new Date();
|
||||
BigInteger userId = getCurrentUserId();
|
||||
|
||||
old.setParentId(parentId);
|
||||
old.setAncestors(parentMeta.ancestors);
|
||||
old.setLevelNo(parentMeta.levelNo);
|
||||
old.setCategoryName(entity.getCategoryName().trim());
|
||||
if (entity.getSortNo() != null) {
|
||||
old.setSortNo(entity.getSortNo());
|
||||
}
|
||||
if (entity.getStatus() != null) {
|
||||
old.setStatus(entity.getStatus());
|
||||
}
|
||||
old.setModified(now);
|
||||
old.setModifiedBy(userId);
|
||||
|
||||
categoryMap.put(old.getId(), old);
|
||||
Map<BigInteger, List<FaqCategory>> childrenMap = buildChildrenMap(categoryMap.values());
|
||||
List<FaqCategory> changedChildren = new ArrayList<>();
|
||||
refreshChildrenLevelAndAncestors(old, childrenMap, changedChildren, now, userId);
|
||||
|
||||
boolean updated = updateById(old);
|
||||
if (!updated) {
|
||||
return false;
|
||||
}
|
||||
|
||||
for (FaqCategory child : changedChildren) {
|
||||
updateById(child);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public boolean removeCategory(BigInteger id) {
|
||||
if (id == null) {
|
||||
throw new BusinessException("FAQ分类ID不能为空");
|
||||
}
|
||||
|
||||
FaqCategory old = getById(id);
|
||||
if (old == null) {
|
||||
throw new BusinessException("FAQ分类不存在");
|
||||
}
|
||||
|
||||
checkFaqCollection(old.getCollectionId());
|
||||
|
||||
if (Boolean.TRUE.equals(old.getIsDefault())) {
|
||||
throw new BusinessException("默认分类不允许删除");
|
||||
}
|
||||
|
||||
QueryWrapper childQuery = QueryWrapper.create()
|
||||
.eq(FaqCategory::getCollectionId, old.getCollectionId())
|
||||
.eq(FaqCategory::getParentId, old.getId());
|
||||
long childrenCount = mapper.selectCountByQuery(childQuery);
|
||||
if (childrenCount > 0) {
|
||||
throw new BusinessException("该分类下存在子分类,请先处理子分类");
|
||||
}
|
||||
|
||||
QueryWrapper faqQuery = QueryWrapper.create()
|
||||
.eq(FaqItem::getCollectionId, old.getCollectionId())
|
||||
.eq(FaqItem::getCategoryId, old.getId());
|
||||
long faqCount = faqItemMapper.selectCountByQuery(faqQuery);
|
||||
if (faqCount > 0) {
|
||||
throw new BusinessException("该分类下存在FAQ条目,请先处理FAQ条目");
|
||||
}
|
||||
|
||||
return removeById(id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<BigInteger> findDescendantIds(BigInteger collectionId, BigInteger categoryId) {
|
||||
if (collectionId == null || categoryId == null) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.eq(FaqCategory::getCollectionId, collectionId)
|
||||
.orderBy("sort_no asc, id asc");
|
||||
List<FaqCategory> categories = list(queryWrapper);
|
||||
Map<BigInteger, FaqCategory> categoryMap = new HashMap<>();
|
||||
for (FaqCategory category : categories) {
|
||||
categoryMap.put(category.getId(), category);
|
||||
}
|
||||
|
||||
if (!categoryMap.containsKey(categoryId)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
Set<BigInteger> descendantSet = findDescendantSet(categoryMap, categoryId);
|
||||
return new ArrayList<>(descendantSet);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<BigInteger, String> buildPathMap(BigInteger collectionId) {
|
||||
if (collectionId == null) {
|
||||
return Collections.emptyMap();
|
||||
}
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.eq(FaqCategory::getCollectionId, collectionId)
|
||||
.orderBy("sort_no asc, id asc");
|
||||
List<FaqCategory> categories = list(queryWrapper);
|
||||
Map<BigInteger, FaqCategory> categoryMap = new HashMap<>();
|
||||
for (FaqCategory category : categories) {
|
||||
categoryMap.put(category.getId(), category);
|
||||
}
|
||||
|
||||
Map<BigInteger, String> pathMap = new HashMap<>();
|
||||
for (FaqCategory category : categories) {
|
||||
buildPath(category.getId(), categoryMap, pathMap, new HashSet<>());
|
||||
}
|
||||
return pathMap;
|
||||
}
|
||||
|
||||
private void backfillNullFaqItemCategory(BigInteger collectionId, BigInteger defaultCategoryId) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.eq(FaqItem::getCollectionId, collectionId)
|
||||
.isNull(FaqItem::getCategoryId);
|
||||
long emptyCategoryCount = faqItemMapper.selectCountByQuery(queryWrapper);
|
||||
if (emptyCategoryCount <= 0) {
|
||||
return;
|
||||
}
|
||||
FaqItem faqItem = new FaqItem();
|
||||
faqItem.setCategoryId(defaultCategoryId);
|
||||
faqItem.setModified(new Date());
|
||||
faqItem.setModifiedBy(getCurrentUserId());
|
||||
faqItemMapper.updateByQuery(faqItem, queryWrapper);
|
||||
}
|
||||
|
||||
private String buildPath(BigInteger categoryId,
|
||||
Map<BigInteger, FaqCategory> categoryMap,
|
||||
Map<BigInteger, String> pathMap,
|
||||
Set<BigInteger> visiting) {
|
||||
if (categoryId == null) {
|
||||
return "";
|
||||
}
|
||||
if (pathMap.containsKey(categoryId)) {
|
||||
return pathMap.get(categoryId);
|
||||
}
|
||||
|
||||
FaqCategory category = categoryMap.get(categoryId);
|
||||
if (category == null) {
|
||||
return "";
|
||||
}
|
||||
|
||||
if (!visiting.add(categoryId)) {
|
||||
throw new BusinessException("FAQ分类存在循环引用");
|
||||
}
|
||||
|
||||
String path;
|
||||
BigInteger parentId = normalizeParentId(category.getParentId());
|
||||
if (ROOT_PARENT_ID.equals(parentId)) {
|
||||
path = category.getCategoryName();
|
||||
} else {
|
||||
String parentPath = buildPath(parentId, categoryMap, pathMap, visiting);
|
||||
if (StringUtil.noText(parentPath)) {
|
||||
path = category.getCategoryName();
|
||||
} else {
|
||||
path = parentPath + " / " + category.getCategoryName();
|
||||
}
|
||||
}
|
||||
|
||||
visiting.remove(categoryId);
|
||||
pathMap.put(categoryId, path);
|
||||
return path;
|
||||
}
|
||||
|
||||
private Set<BigInteger> findDescendantSet(Map<BigInteger, FaqCategory> categoryMap, BigInteger categoryId) {
|
||||
Map<BigInteger, List<FaqCategory>> childrenMap = buildChildrenMap(categoryMap.values());
|
||||
Set<BigInteger> result = new HashSet<>();
|
||||
ArrayDeque<BigInteger> queue = new ArrayDeque<>();
|
||||
queue.add(categoryId);
|
||||
|
||||
while (!queue.isEmpty()) {
|
||||
BigInteger currentId = queue.poll();
|
||||
if (!result.add(currentId)) {
|
||||
continue;
|
||||
}
|
||||
List<FaqCategory> children = childrenMap.getOrDefault(currentId, Collections.emptyList());
|
||||
for (FaqCategory child : children) {
|
||||
queue.offer(child.getId());
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private Map<BigInteger, List<FaqCategory>> buildChildrenMap(Iterable<FaqCategory> categories) {
|
||||
Map<BigInteger, List<FaqCategory>> childrenMap = new HashMap<>();
|
||||
for (FaqCategory category : categories) {
|
||||
BigInteger parentId = normalizeParentId(category.getParentId());
|
||||
childrenMap.computeIfAbsent(parentId, key -> new ArrayList<>()).add(category);
|
||||
}
|
||||
return childrenMap;
|
||||
}
|
||||
|
||||
private void refreshChildrenLevelAndAncestors(FaqCategory parent,
|
||||
Map<BigInteger, List<FaqCategory>> childrenMap,
|
||||
List<FaqCategory> changedChildren,
|
||||
Date modified,
|
||||
BigInteger modifiedBy) {
|
||||
List<FaqCategory> children = childrenMap.getOrDefault(parent.getId(), Collections.emptyList());
|
||||
for (FaqCategory child : children) {
|
||||
int nextLevel = parent.getLevelNo() + 1;
|
||||
if (nextLevel > 3) {
|
||||
throw new BusinessException("最多支持三级分类");
|
||||
}
|
||||
child.setLevelNo(nextLevel);
|
||||
child.setAncestors(parent.getAncestors() + "," + parent.getId());
|
||||
child.setModified(modified);
|
||||
child.setModifiedBy(modifiedBy);
|
||||
changedChildren.add(child);
|
||||
refreshChildrenLevelAndAncestors(child, childrenMap, changedChildren, modified, modifiedBy);
|
||||
}
|
||||
}
|
||||
|
||||
private ParentMeta getParentMeta(BigInteger collectionId, BigInteger parentId, BigInteger currentId) {
|
||||
BigInteger normalizedParentId = normalizeParentId(parentId);
|
||||
if (ROOT_PARENT_ID.equals(normalizedParentId)) {
|
||||
return new ParentMeta("0", 1);
|
||||
}
|
||||
|
||||
FaqCategory parent = getById(normalizedParentId);
|
||||
if (parent == null || !collectionId.equals(parent.getCollectionId())) {
|
||||
throw new BusinessException("父分类不存在");
|
||||
}
|
||||
if (currentId != null && currentId.equals(parent.getId())) {
|
||||
throw new BusinessException("父分类不能是自己");
|
||||
}
|
||||
|
||||
int level = (parent.getLevelNo() == null ? 0 : parent.getLevelNo()) + 1;
|
||||
if (level > 3) {
|
||||
throw new BusinessException("最多支持三级分类");
|
||||
}
|
||||
return new ParentMeta(parent.getAncestors() + "," + parent.getId(), level);
|
||||
}
|
||||
|
||||
private BigInteger normalizeParentId(BigInteger parentId) {
|
||||
return parentId == null ? ROOT_PARENT_ID : parentId;
|
||||
}
|
||||
|
||||
private Integer nextSortNo(BigInteger collectionId, BigInteger parentId) {
|
||||
QueryWrapper queryWrapper = QueryWrapper.create()
|
||||
.eq(FaqCategory::getCollectionId, collectionId)
|
||||
.eq(FaqCategory::getParentId, parentId)
|
||||
.orderBy("sort_no desc, id desc");
|
||||
List<FaqCategory> siblings = list(queryWrapper);
|
||||
if (siblings == null || siblings.isEmpty() || siblings.get(0).getSortNo() == null) {
|
||||
return 0;
|
||||
}
|
||||
return siblings.get(0).getSortNo() + 1;
|
||||
}
|
||||
|
||||
private void validateParentNotDefault(BigInteger collectionId, BigInteger parentId) {
|
||||
if (parentId == null || ROOT_PARENT_ID.equals(parentId)) {
|
||||
return;
|
||||
}
|
||||
FaqCategory parent = getById(parentId);
|
||||
if (parent == null || !collectionId.equals(parent.getCollectionId())) {
|
||||
return;
|
||||
}
|
||||
if (Boolean.TRUE.equals(parent.getIsDefault())) {
|
||||
throw new BusinessException("默认分类不允许创建子分类");
|
||||
}
|
||||
}
|
||||
|
||||
private DocumentCollection checkFaqCollection(BigInteger collectionId) {
|
||||
if (collectionId == null) {
|
||||
throw new BusinessException("知识库ID不能为空");
|
||||
}
|
||||
DocumentCollection collection = documentCollectionService.getById(collectionId);
|
||||
if (collection == null) {
|
||||
throw new BusinessException("知识库不存在");
|
||||
}
|
||||
if (!collection.isFaqCollection()) {
|
||||
throw new BusinessException("当前知识库不是FAQ类型");
|
||||
}
|
||||
return collection;
|
||||
}
|
||||
|
||||
private BigInteger getCurrentUserId() {
|
||||
if (!StpUtil.isLogin()) {
|
||||
return null;
|
||||
}
|
||||
return BigInteger.valueOf(StpUtil.getLoginIdAsLong());
|
||||
}
|
||||
|
||||
private static class ParentMeta {
|
||||
private final String ancestors;
|
||||
private final int levelNo;
|
||||
|
||||
private ParentMeta(String ancestors, int levelNo) {
|
||||
this.ancestors = ancestors;
|
||||
this.levelNo = levelNo;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import tech.easyflow.ai.entity.DocumentCollection;
|
||||
import tech.easyflow.ai.entity.FaqItem;
|
||||
import tech.easyflow.ai.entity.Model;
|
||||
import tech.easyflow.ai.mapper.FaqItemMapper;
|
||||
import tech.easyflow.ai.service.FaqCategoryService;
|
||||
import tech.easyflow.ai.service.DocumentCollectionService;
|
||||
import tech.easyflow.ai.service.FaqItemService;
|
||||
import tech.easyflow.ai.service.ModelService;
|
||||
@@ -47,6 +48,9 @@ public class FaqItemServiceImpl extends ServiceImpl<FaqItemMapper, FaqItem> impl
|
||||
@Resource
|
||||
private ModelService modelService;
|
||||
|
||||
@Resource
|
||||
private FaqCategoryService faqCategoryService;
|
||||
|
||||
@Autowired
|
||||
private SearcherFactory searcherFactory;
|
||||
|
||||
@@ -54,6 +58,9 @@ public class FaqItemServiceImpl extends ServiceImpl<FaqItemMapper, FaqItem> impl
|
||||
@Transactional
|
||||
public boolean saveFaqItem(FaqItem entity) {
|
||||
checkAndNormalize(entity, true);
|
||||
DocumentCollection collection = getFaqCollection(entity.getCollectionId());
|
||||
entity.setCategoryId(faqCategoryService.ensureFaqItemCategory(entity.getCollectionId(), entity.getCategoryId()));
|
||||
|
||||
Date now = new Date();
|
||||
BigInteger userId = getCurrentUserId();
|
||||
entity.setCreated(now);
|
||||
@@ -69,7 +76,6 @@ public class FaqItemServiceImpl extends ServiceImpl<FaqItemMapper, FaqItem> impl
|
||||
return false;
|
||||
}
|
||||
|
||||
DocumentCollection collection = getFaqCollection(entity.getCollectionId());
|
||||
storeToVector(collection, entity, false);
|
||||
return true;
|
||||
}
|
||||
@@ -89,6 +95,8 @@ public class FaqItemServiceImpl extends ServiceImpl<FaqItemMapper, FaqItem> impl
|
||||
}
|
||||
checkAndNormalize(entity, false);
|
||||
|
||||
DocumentCollection collection = getFaqCollection(old.getCollectionId());
|
||||
old.setCategoryId(faqCategoryService.ensureFaqItemCategory(old.getCollectionId(), entity.getCategoryId()));
|
||||
old.setQuestion(entity.getQuestion());
|
||||
old.setAnswerHtml(entity.getAnswerHtml());
|
||||
old.setAnswerText(entity.getAnswerText());
|
||||
@@ -104,7 +112,6 @@ public class FaqItemServiceImpl extends ServiceImpl<FaqItemMapper, FaqItem> impl
|
||||
return false;
|
||||
}
|
||||
|
||||
DocumentCollection collection = getFaqCollection(old.getCollectionId());
|
||||
storeToVector(collection, old, true);
|
||||
return true;
|
||||
}
|
||||
@@ -226,6 +233,7 @@ public class FaqItemServiceImpl extends ServiceImpl<FaqItemMapper, FaqItem> impl
|
||||
Map<String, Object> metadata = new HashMap<>();
|
||||
metadata.put("question", entity.getQuestion());
|
||||
metadata.put("answerText", entity.getAnswerText());
|
||||
metadata.put("categoryId", entity.getCategoryId());
|
||||
doc.setMetadataMap(metadata);
|
||||
return doc;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user