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:
2026-02-25 16:53:31 +08:00
parent 3b6ed8a49a
commit 9600d0855e
19 changed files with 2224 additions and 99 deletions

View File

@@ -0,0 +1,65 @@
package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.FaqCategory;
import tech.easyflow.ai.service.FaqCategoryService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.List;
@RestController
@RequestMapping("/api/v1/faqCategory")
@UsePermission(moduleName = "/api/v1/documentCollection")
public class FaqCategoryController extends BaseCurdController<FaqCategoryService, FaqCategory> {
public FaqCategoryController(FaqCategoryService service) {
super(service);
}
@Override
@GetMapping("list")
@SaCheckPermission("/api/v1/documentCollection/query")
public Result<List<FaqCategory>> list(FaqCategory entity, Boolean asTree, String sortKey, String sortType) {
BigInteger collectionId = entity == null ? null : entity.getCollectionId();
if (collectionId == null) {
throw new BusinessException("知识库ID不能为空");
}
return Result.ok(service.listByCollection(collectionId, asTree));
}
@Override
@PostMapping("save")
@SaCheckPermission("/api/v1/documentCollection/save")
public Result<?> save(@JsonBody FaqCategory entity) {
return Result.ok(service.saveCategory(entity));
}
@Override
@PostMapping("update")
@SaCheckPermission("/api/v1/documentCollection/save")
public Result<?> update(@JsonBody FaqCategory entity) {
return Result.ok(service.updateCategory(entity));
}
@Override
@PostMapping("remove")
@SaCheckPermission("/api/v1/documentCollection/remove")
public Result<?> remove(@JsonBody(value = "id", required = true) Serializable id) {
return Result.ok(service.removeCategory(new BigInteger(String.valueOf(id))));
}
@Override
protected String getDefaultOrderBy() {
return "sort_no asc";
}
}

View File

@@ -2,27 +2,36 @@ package tech.easyflow.admin.controller.ai;
import cn.dev33.satoken.annotation.SaCheckPermission;
import com.mybatisflex.core.paginate.Page;
import com.mybatisflex.core.query.QueryWrapper;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import tech.easyflow.ai.entity.FaqItem;
import tech.easyflow.ai.service.FaqCategoryService;
import tech.easyflow.ai.service.FaqItemService;
import tech.easyflow.common.annotation.UsePermission;
import tech.easyflow.common.domain.Result;
import tech.easyflow.common.web.controller.BaseCurdController;
import tech.easyflow.common.web.exceptions.BusinessException;
import tech.easyflow.common.web.jsonbody.JsonBody;
import java.io.Serializable;
import java.math.BigInteger;
import java.util.List;
import java.util.Map;
@RestController
@RequestMapping("/api/v1/faqItem")
@UsePermission(moduleName = "/api/v1/documentCollection")
public class FaqItemController extends BaseCurdController<FaqItemService, FaqItem> {
public FaqItemController(FaqItemService service) {
private final FaqCategoryService faqCategoryService;
public FaqItemController(FaqItemService service, FaqCategoryService faqCategoryService) {
super(service);
this.faqCategoryService = faqCategoryService;
}
@Override
@@ -36,7 +45,49 @@ public class FaqItemController extends BaseCurdController<FaqItemService, FaqIte
@GetMapping("page")
@SaCheckPermission("/api/v1/documentCollection/query")
public Result<Page<FaqItem>> page(HttpServletRequest request, String sortKey, String sortType, Long pageNumber, Long pageSize) {
return super.page(request, sortKey, sortType, pageNumber, pageSize);
if (pageNumber == null || pageNumber < 1) {
pageNumber = 1L;
}
if (pageSize == null || pageSize < 1) {
pageSize = 10L;
}
String collectionIdText = request.getParameter("collectionId");
if (collectionIdText == null || collectionIdText.trim().isEmpty()) {
throw new BusinessException("知识库ID不能为空");
}
BigInteger collectionId = new BigInteger(collectionIdText);
faqCategoryService.ensureDefaultCategory(collectionId);
QueryWrapper queryWrapper = QueryWrapper.create()
.eq(FaqItem::getCollectionId, collectionId);
String question = request.getParameter("question");
if (question != null && !question.trim().isEmpty()) {
queryWrapper.like(FaqItem::getQuestion, question.trim());
}
String categoryIdText = request.getParameter("categoryId");
if (categoryIdText != null && !categoryIdText.trim().isEmpty()) {
BigInteger categoryId = new BigInteger(categoryIdText);
List<BigInteger> descendantIds = faqCategoryService.findDescendantIds(collectionId, categoryId);
if (descendantIds.isEmpty()) {
queryWrapper.eq(FaqItem::getId, BigInteger.ZERO);
} else {
queryWrapper.in(FaqItem::getCategoryId, descendantIds);
}
}
queryWrapper.orderBy(buildOrderBy(sortKey, sortType, getDefaultOrderBy()));
Page<FaqItem> page = service.page(new Page<>(pageNumber, pageSize), queryWrapper);
Map<BigInteger, String> pathMap = faqCategoryService.buildPathMap(collectionId);
if (page.getRecords() != null) {
for (FaqItem record : page.getRecords()) {
record.setCategoryPath(pathMap.get(record.getCategoryId()));
}
}
return Result.ok(page);
}
@Override