feat(knowledge-base): 添加知识库统计功能

- 后端服务新增 KnowledgeBaseStatisticsResp 和 RagFileStatistics 数据传输对象
- 在 KnowledgeBaseService 中实现 getStatistics 方法提供统计信息查询
- 为 RagFileRepository 添加 getStatistics 接口及其实现
- 通过 MyBatis Mapper 实现数据库层面的统计查询功能
- 在 KnowledgeBaseController 中暴露 /statistics 接口供前端调用
- 前端页面集成统计卡片组件展示知识库、文件数量及总大小信息
- 实现前后端数据同步机制确保统计数据实时更新
This commit is contained in:
2026-01-30 23:17:40 +08:00
parent 76f70a6847
commit fd209c3083
10 changed files with 155 additions and 6 deletions

View File

@@ -140,6 +140,18 @@ public class KnowledgeBaseService {
return PagedResponse.of(respList, page.getCurrent(), page.getTotal(), page.getPages()); return PagedResponse.of(respList, page.getCurrent(), page.getTotal(), page.getPages());
} }
public KnowledgeBaseStatisticsResp getStatistics() {
KnowledgeBaseStatisticsResp resp = new KnowledgeBaseStatisticsResp();
resp.setTotalKnowledgeBases(knowledgeBaseRepository.count());
RagFileStatistics fileStatistics = ragFileRepository.getStatistics();
if (fileStatistics != null) {
resp.setTotalFiles(fileStatistics.getTotalFiles() != null ? fileStatistics.getTotalFiles() : 0L);
resp.setTotalSize(fileStatistics.getTotalSize() != null ? fileStatistics.getTotalSize() : 0L);
}
return resp;
}
@Transactional(rollbackFor = Exception.class) @Transactional(rollbackFor = Exception.class)
public void addFiles(AddFilesReq request) { public void addFiles(AddFilesReq request) {
KnowledgeBase knowledgeBase = Optional.ofNullable(knowledgeBaseRepository.getById(request.getKnowledgeBaseId())) KnowledgeBase knowledgeBase = Optional.ofNullable(knowledgeBaseRepository.getById(request.getKnowledgeBaseId()))

View File

@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.extension.repository.IRepository;
import com.datamate.rag.indexer.domain.model.RagFile; import com.datamate.rag.indexer.domain.model.RagFile;
import com.datamate.rag.indexer.interfaces.dto.KnowledgeBaseFileSearchReq; import com.datamate.rag.indexer.interfaces.dto.KnowledgeBaseFileSearchReq;
import com.datamate.rag.indexer.interfaces.dto.RagFileReq; import com.datamate.rag.indexer.interfaces.dto.RagFileReq;
import com.datamate.rag.indexer.interfaces.dto.RagFileStatistics;
import java.util.List; import java.util.List;
@@ -24,4 +25,6 @@ public interface RagFileRepository extends IRepository<RagFile> {
IPage<RagFile> page(IPage<RagFile> page, RagFileReq request); IPage<RagFile> page(IPage<RagFile> page, RagFileReq request);
IPage<RagFile> searchPage(IPage<RagFile> page, KnowledgeBaseFileSearchReq request); IPage<RagFile> searchPage(IPage<RagFile> page, KnowledgeBaseFileSearchReq request);
RagFileStatistics getStatistics();
} }

View File

@@ -8,6 +8,7 @@ import com.datamate.rag.indexer.domain.repository.RagFileRepository;
import com.datamate.rag.indexer.infrastructure.persistence.mapper.RagFileMapper; import com.datamate.rag.indexer.infrastructure.persistence.mapper.RagFileMapper;
import com.datamate.rag.indexer.interfaces.dto.KnowledgeBaseFileSearchReq; import com.datamate.rag.indexer.interfaces.dto.KnowledgeBaseFileSearchReq;
import com.datamate.rag.indexer.interfaces.dto.RagFileReq; import com.datamate.rag.indexer.interfaces.dto.RagFileReq;
import com.datamate.rag.indexer.interfaces.dto.RagFileStatistics;
import org.springframework.stereotype.Repository; import org.springframework.stereotype.Repository;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
@@ -60,6 +61,11 @@ public class RagFileRepositoryImpl extends CrudRepository<RagFileMapper, RagFile
.page(page); .page(page);
} }
@Override
public RagFileStatistics getStatistics() {
return baseMapper.getStatistics();
}
private String normalizeRelativePath(String relativePath) { private String normalizeRelativePath(String relativePath) {
if (!StringUtils.hasText(relativePath)) { if (!StringUtils.hasText(relativePath)) {
return ""; return "";

View File

@@ -3,7 +3,9 @@ package com.datamate.rag.indexer.infrastructure.persistence.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper; import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.datamate.rag.indexer.domain.model.RagFile; import com.datamate.rag.indexer.domain.model.RagFile;
import com.datamate.rag.indexer.interfaces.dto.RagFileStatistics;
import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
/** /**
* RAG文件映射器接口 * RAG文件映射器接口
@@ -13,4 +15,9 @@ import org.apache.ibatis.annotations.Mapper;
*/ */
@Mapper @Mapper
public interface RagFileMapper extends BaseMapper<RagFile> { public interface RagFileMapper extends BaseMapper<RagFile> {
@Select("SELECT COUNT(*) AS totalFiles, " +
"COALESCE(SUM(df.file_size), 0) AS totalSize " +
"FROM t_rag_file rf " +
"LEFT JOIN t_dm_dataset_files df ON rf.file_id = df.id")
RagFileStatistics getStatistics();
} }

View File

@@ -80,6 +80,16 @@ public class KnowledgeBaseController {
return knowledgeBaseService.list(request); return knowledgeBaseService.list(request);
} }
/**
* 获取知识库统计信息
*
* @return 知识库统计信息
*/
@GetMapping("/statistics")
public KnowledgeBaseStatisticsResp statistics() {
return knowledgeBaseService.getStatistics();
}
/** /**
* 添加文件到知识库 * 添加文件到知识库
* *

View File

@@ -0,0 +1,15 @@
package com.datamate.rag.indexer.interfaces.dto;
import lombok.Getter;
import lombok.Setter;
/**
* 知识库统计响应
*/
@Getter
@Setter
public class KnowledgeBaseStatisticsResp {
private Long totalKnowledgeBases = 0L;
private Long totalFiles = 0L;
private Long totalSize = 0L;
}

View File

@@ -0,0 +1,14 @@
package com.datamate.rag.indexer.interfaces.dto;
import lombok.Getter;
import lombok.Setter;
/**
* 知识库文件统计
*/
@Getter
@Setter
public class RagFileStatistics {
private Long totalFiles = 0L;
private Long totalSize = 0L;
}

View File

@@ -1,23 +1,48 @@
import { useState } from "react"; import { useCallback, useEffect, useState } from "react";
import { Card, Button, Table, Tooltip, message } from "antd"; import { Card, Button, Table, Tooltip, message, Statistic } from "antd";
import { DeleteOutlined, EditOutlined } from "@ant-design/icons"; import { DeleteOutlined, EditOutlined } from "@ant-design/icons";
import { SearchControls } from "@/components/SearchControls"; import { SearchControls } from "@/components/SearchControls";
import { useNavigate } from "react-router"; import { useNavigate } from "react-router";
import CardView from "@/components/CardView"; import CardView from "@/components/CardView";
import { import {
deleteKnowledgeBaseByIdUsingDelete, deleteKnowledgeBaseByIdUsingDelete,
getKnowledgeBaseStatisticsUsingGet,
queryKnowledgeBasesUsingPost, queryKnowledgeBasesUsingPost,
} from "../knowledge-base.api"; } from "../knowledge-base.api";
import useFetchData from "@/hooks/useFetchData"; import useFetchData from "@/hooks/useFetchData";
import { KnowledgeBaseItem } from "../knowledge-base.model"; import { KnowledgeBaseItem, KnowledgeBaseStatistics } from "../knowledge-base.model";
import CreateKnowledgeBase from "../components/CreateKnowledgeBase"; import CreateKnowledgeBase from "../components/CreateKnowledgeBase";
import { mapKnowledgeBase } from "../knowledge-base.const"; import { mapKnowledgeBase } from "../knowledge-base.const";
import { formatBytes } from "@/utils/unit";
type StatisticsItem = {
title: string;
value: number | string;
};
const DEFAULT_STATISTICS: StatisticsItem[] = [
{
title: "知识库总数",
value: 0,
},
{
title: "文件总数",
value: 0,
},
{
title: "总大小",
value: "0 B",
},
];
export default function KnowledgeBasePage() { export default function KnowledgeBasePage() {
const navigate = useNavigate(); const navigate = useNavigate();
const [viewMode, setViewMode] = useState<"card" | "list">("card"); const [viewMode, setViewMode] = useState<"card" | "list">("card");
const [isEdit, setIsEdit] = useState(false); const [isEdit, setIsEdit] = useState(false);
const [currentKB, setCurrentKB] = useState<KnowledgeBaseItem | null>(null); const [currentKB, setCurrentKB] = useState<KnowledgeBaseItem | null>(null);
const [statisticsData, setStatisticsData] = useState<StatisticsItem[]>(
DEFAULT_STATISTICS
);
const { const {
loading, loading,
tableData, tableData,
@@ -32,11 +57,43 @@ export default function KnowledgeBasePage() {
(kb) => mapKnowledgeBase(kb, false) // 在首页不显示索引模型和文本理解模型字段 (kb) => mapKnowledgeBase(kb, false) // 在首页不显示索引模型和文本理解模型字段
); );
const fetchStatistics = useCallback(async () => {
try {
const { data } = await getKnowledgeBaseStatisticsUsingGet();
const stats = data as KnowledgeBaseStatistics | undefined;
setStatisticsData([
{
title: "知识库总数",
value: stats?.totalKnowledgeBases ?? 0,
},
{
title: "文件总数",
value: stats?.totalFiles ?? 0,
},
{
title: "总大小",
value: formatBytes(stats?.totalSize ?? 0),
},
]);
} catch {
message.error("统计数据加载失败");
setStatisticsData(DEFAULT_STATISTICS);
}
}, []);
const refreshAll = useCallback(async () => {
await Promise.all([fetchData(), fetchStatistics()]);
}, [fetchData, fetchStatistics]);
useEffect(() => {
fetchStatistics();
}, [fetchStatistics]);
const handleDeleteKB = async (kb: KnowledgeBaseItem) => { const handleDeleteKB = async (kb: KnowledgeBaseItem) => {
try { try {
await deleteKnowledgeBaseByIdUsingDelete(kb.id); await deleteKnowledgeBaseByIdUsingDelete(kb.id);
message.success("知识库删除成功"); message.success("知识库删除成功");
fetchData(); await refreshAll();
} catch { } catch {
message.error("知识库删除失败"); message.error("知识库删除失败");
} }
@@ -140,7 +197,7 @@ export default function KnowledgeBasePage() {
isEdit={isEdit} isEdit={isEdit}
data={currentKB} data={currentKB}
onUpdate={() => { onUpdate={() => {
fetchData(); refreshAll();
}} }}
onClose={() => { onClose={() => {
setIsEdit(false); setIsEdit(false);
@@ -150,6 +207,20 @@ export default function KnowledgeBasePage() {
</div> </div>
</div> </div>
<div className="grid grid-cols-1 gap-4">
<Card>
<div className="grid grid-cols-3">
{statisticsData.map((item) => (
<Statistic
title={item.title}
key={item.title}
value={`${item.value}`}
/>
))}
</div>
</Card>
</div>
<SearchControls <SearchControls
searchTerm={searchParams.keyword} searchTerm={searchParams.keyword}
onSearchChange={handleKeywordChange} onSearchChange={handleKeywordChange}
@@ -160,7 +231,7 @@ export default function KnowledgeBasePage() {
viewMode={viewMode} viewMode={viewMode}
onViewModeChange={setViewMode} onViewModeChange={setViewMode}
showViewToggle showViewToggle
onReload={fetchData} onReload={refreshAll}
/> />
{viewMode === "card" ? ( {viewMode === "card" ? (
<CardView <CardView

View File

@@ -38,6 +38,11 @@ export function queryKnowledgeBaseFilesSearchUsingGet(params: RequestParams) {
return get("/api/knowledge-base/files/search", params); return get("/api/knowledge-base/files/search", params);
} }
// 获取知识库统计
export function getKnowledgeBaseStatisticsUsingGet() {
return get("/api/knowledge-base/statistics");
}
// 添加文件到知识库 // 添加文件到知识库
export function addKnowledgeBaseFilesUsingPost(baseId: string, data: RequestPayload) { export function addKnowledgeBaseFilesUsingPost(baseId: string, data: RequestPayload) {
return post(`/api/knowledge-base/${baseId}/files`, data); return post(`/api/knowledge-base/${baseId}/files`, data);

View File

@@ -25,6 +25,12 @@ export interface KnowledgeBaseItem {
chat: never; chat: never;
} }
export interface KnowledgeBaseStatistics {
totalKnowledgeBases: number;
totalFiles: number;
totalSize: number;
}
export interface KBFile { export interface KBFile {
id: string; id: string;
fileName: string; fileName: string;