Compare commits
3 Commits
67cfbeb572
...
2ab02fb574
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2ab02fb574 | ||
|
|
8532628171 | ||
|
|
541c3ff455 |
96
frontend/src/api/__tests__/ragDocuments.spec.ts
Normal file
96
frontend/src/api/__tests__/ragDocuments.spec.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
batchUploadRagDocuments,
|
||||
deleteRagDocument,
|
||||
getRagDocumentById,
|
||||
listRagDocuments,
|
||||
queryRagDocuments,
|
||||
saveRagDocument,
|
||||
SOURCE_TYPE_RAG,
|
||||
} from '../ragDocuments';
|
||||
import { get, post } from '../request';
|
||||
|
||||
vi.mock('../request', () => ({
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('rag documents api', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('lists documents with explicit action url', () => {
|
||||
listRagDocuments();
|
||||
|
||||
expect(post).toHaveBeenCalledWith('/rag/documents/list');
|
||||
});
|
||||
|
||||
it('queries details and saves documents', () => {
|
||||
queryRagDocuments({ storeId: '1001', parseStatus: 'UPLOADED' });
|
||||
getRagDocumentById('2002');
|
||||
saveRagDocument({
|
||||
id: '2002',
|
||||
storeId: '1001',
|
||||
attachmentId: '3003',
|
||||
documentTitle: '知识文档',
|
||||
parseStatus: 'PARSED',
|
||||
indexStatus: 'INDEXED',
|
||||
enabled: true,
|
||||
errorMessage: '无',
|
||||
remark: '备注',
|
||||
});
|
||||
deleteRagDocument('2002');
|
||||
|
||||
expect(post).toHaveBeenCalledWith('/rag/documents/query', {
|
||||
storeId: '1001',
|
||||
parseStatus: 'UPLOADED',
|
||||
});
|
||||
expect(get).toHaveBeenCalledWith('/rag/documents/detail', {
|
||||
params: { id: '2002' },
|
||||
});
|
||||
expect(post).toHaveBeenCalledWith('/rag/documents/save', {
|
||||
id: '2002',
|
||||
storeId: '1001',
|
||||
attachmentId: '3003',
|
||||
documentTitle: '知识文档',
|
||||
parseStatus: 'PARSED',
|
||||
indexStatus: 'INDEXED',
|
||||
enabled: true,
|
||||
errorMessage: '无',
|
||||
remark: '备注',
|
||||
});
|
||||
expect(post).toHaveBeenCalledWith('/rag/documents/delete', undefined, {
|
||||
params: { id: '2002' },
|
||||
});
|
||||
});
|
||||
|
||||
it('batch uploads documents with constant sourceType and storeId sourceId', () => {
|
||||
const formData = new FormData();
|
||||
const file = new File(['hello'], 'knowledge.txt', { type: 'text/plain' });
|
||||
|
||||
batchUploadRagDocuments({
|
||||
storeId: '1001',
|
||||
sourceType: SOURCE_TYPE_RAG,
|
||||
files: [file],
|
||||
documentSummary: '摘要',
|
||||
remark: '备注',
|
||||
});
|
||||
|
||||
expect(post).toHaveBeenCalledTimes(1);
|
||||
const [url, body] = vi.mocked(post).mock.calls[0] ?? [];
|
||||
expect(url).toBe('/rag/documents/batchUpload');
|
||||
expect(body).toBeInstanceOf(FormData);
|
||||
|
||||
formData.append('storeId', '1001');
|
||||
formData.append('sourceType', SOURCE_TYPE_RAG);
|
||||
formData.append('files', file);
|
||||
formData.append('documentSummary', '摘要');
|
||||
formData.append('remark', '备注');
|
||||
|
||||
const actualEntries = Array.from((body as FormData).entries());
|
||||
const expectedEntries = Array.from(formData.entries());
|
||||
expect(actualEntries).toEqual(expectedEntries);
|
||||
});
|
||||
});
|
||||
31
frontend/src/api/__tests__/ragStores.spec.ts
Normal file
31
frontend/src/api/__tests__/ragStores.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import {
|
||||
getRagStoreDocumentOverview,
|
||||
getRagStoreOverview,
|
||||
listRagStores,
|
||||
} from '../ragStores';
|
||||
import { get, post } from '../request';
|
||||
|
||||
vi.mock('../request', () => ({
|
||||
get: vi.fn(),
|
||||
post: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('rag stores api', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('loads overview and document overview endpoints', () => {
|
||||
getRagStoreOverview();
|
||||
getRagStoreDocumentOverview('1001');
|
||||
listRagStores();
|
||||
|
||||
expect(get).toHaveBeenCalledWith('/rag/store/overview');
|
||||
expect(get).toHaveBeenCalledWith('/rag/store/documentOverview', {
|
||||
params: { storeId: '1001' },
|
||||
});
|
||||
expect(post).toHaveBeenCalledWith('/rag/store/list');
|
||||
});
|
||||
});
|
||||
88
frontend/src/api/ragDocuments.ts
Normal file
88
frontend/src/api/ragDocuments.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import { get, post } from './request';
|
||||
|
||||
/** 附件来源类型:标识该附件归属于 RAG 知识库业务。 */
|
||||
export const SOURCE_TYPE_RAG = 'RAG';
|
||||
|
||||
export interface RagDocument {
|
||||
id?: string;
|
||||
storeId: string;
|
||||
attachmentId?: string | null;
|
||||
documentTitle?: string | null;
|
||||
documentSummary?: string | null;
|
||||
parseStatus?: string | null;
|
||||
indexStatus?: string | null;
|
||||
enabled?: boolean | null;
|
||||
errorMessage?: string | null;
|
||||
remark?: string | null;
|
||||
createTime?: string | null;
|
||||
updateTime?: string | null;
|
||||
}
|
||||
|
||||
export interface RagDocumentQueryRequest {
|
||||
storeId?: string;
|
||||
attachmentId?: string;
|
||||
parseStatus?: string;
|
||||
indexStatus?: string;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface RagDocumentSaveRequest {
|
||||
id?: string;
|
||||
storeId: string;
|
||||
attachmentId?: string;
|
||||
documentTitle?: string;
|
||||
documentSummary?: string;
|
||||
parseStatus?: string;
|
||||
indexStatus?: string;
|
||||
enabled?: boolean;
|
||||
errorMessage?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export interface RagDocumentBatchUploadRequest {
|
||||
storeId: string;
|
||||
sourceType?: string;
|
||||
files: File[];
|
||||
documentSummary?: string;
|
||||
remark?: string;
|
||||
}
|
||||
|
||||
export function listRagDocuments() {
|
||||
return post<RagDocument[]>('/rag/documents/list');
|
||||
}
|
||||
|
||||
export function queryRagDocuments(query?: RagDocumentQueryRequest) {
|
||||
return post<RagDocument[], RagDocumentQueryRequest | undefined>('/rag/documents/query', query);
|
||||
}
|
||||
|
||||
export function getRagDocumentById(id: string) {
|
||||
return get<RagDocument>('/rag/documents/detail', {
|
||||
params: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export function saveRagDocument(data: RagDocumentSaveRequest) {
|
||||
return post<boolean, RagDocumentSaveRequest>('/rag/documents/save', data);
|
||||
}
|
||||
|
||||
export function deleteRagDocument(id: string) {
|
||||
return post<boolean>('/rag/documents/delete', undefined, {
|
||||
params: { id },
|
||||
});
|
||||
}
|
||||
|
||||
export function batchUploadRagDocuments(data: RagDocumentBatchUploadRequest) {
|
||||
const formData = new FormData();
|
||||
formData.append('storeId', data.storeId);
|
||||
formData.append('sourceType', data.sourceType || SOURCE_TYPE_RAG);
|
||||
data.files.forEach((file) => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
if (data.documentSummary) {
|
||||
formData.append('documentSummary', data.documentSummary);
|
||||
}
|
||||
if (data.remark) {
|
||||
formData.append('remark', data.remark);
|
||||
}
|
||||
return post<RagDocument[], FormData>('/rag/documents/batchUpload', formData);
|
||||
}
|
||||
@@ -11,6 +11,23 @@ export interface RagStore {
|
||||
updateTime?: string | null;
|
||||
}
|
||||
|
||||
export interface RagStoreOverview {
|
||||
totalStores: number;
|
||||
totalDocuments: number;
|
||||
totalChunks?: number | null;
|
||||
retrievableStores: number;
|
||||
}
|
||||
|
||||
export interface RagStoreDocumentOverview {
|
||||
storeId: string;
|
||||
storeName?: string | null;
|
||||
documentCount: number;
|
||||
enabledDocumentCount: number;
|
||||
parsedDocumentCount: number;
|
||||
indexedDocumentCount: number;
|
||||
lastUploadTime?: string | null;
|
||||
}
|
||||
|
||||
export interface RagStoreQueryRequest {
|
||||
storeCode?: string;
|
||||
storeName?: string;
|
||||
@@ -33,6 +50,16 @@ export function getRagStoreById(id: string) {
|
||||
});
|
||||
}
|
||||
|
||||
export function getRagStoreOverview() {
|
||||
return get<RagStoreOverview>('/rag/store/overview');
|
||||
}
|
||||
|
||||
export function getRagStoreDocumentOverview(storeId: string) {
|
||||
return get<RagStoreDocumentOverview>('/rag/store/documentOverview', {
|
||||
params: { storeId },
|
||||
});
|
||||
}
|
||||
|
||||
export function saveRagStore(data: RagStoreSaveRequest) {
|
||||
return post<boolean, RagStoreSaveRequest>('/rag/store/save', data);
|
||||
}
|
||||
|
||||
@@ -1,8 +1,478 @@
|
||||
<script setup lang="ts">
|
||||
import { Search, UploadFilled } from '@element-plus/icons-vue';
|
||||
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||
import { computed, onMounted, reactive, ref } from 'vue';
|
||||
|
||||
import {
|
||||
batchUploadRagDocuments,
|
||||
deleteRagDocument,
|
||||
getRagDocumentById,
|
||||
listRagDocuments,
|
||||
saveRagDocument,
|
||||
SOURCE_TYPE_RAG,
|
||||
type RagDocument,
|
||||
} from '@/api/ragDocuments';
|
||||
import { queryRagStores, type RagStore } from '@/api/ragStores';
|
||||
|
||||
const loading = ref(false);
|
||||
const submitting = ref(false);
|
||||
|
||||
const storeOptions = ref<RagStore[]>([]);
|
||||
const docRows = ref<RagDocument[]>([]);
|
||||
|
||||
const queryForm = reactive({
|
||||
storeId: '',
|
||||
parseStatus: '',
|
||||
indexStatus: '',
|
||||
enabled: '' as string,
|
||||
keyword: '',
|
||||
});
|
||||
|
||||
const editDialogVisible = ref(false);
|
||||
const uploadDialogVisible = ref(false);
|
||||
|
||||
const uploadStoreId = ref('');
|
||||
const uploadSummary = ref('');
|
||||
const uploadRemark = ref('');
|
||||
|
||||
const editForm = reactive({
|
||||
id: '',
|
||||
storeId: '',
|
||||
attachmentId: '',
|
||||
documentTitle: '',
|
||||
documentSummary: '',
|
||||
enabled: true,
|
||||
remark: '',
|
||||
});
|
||||
|
||||
const filteredRows = computed(() => {
|
||||
const kw = queryForm.keyword.trim().toLowerCase();
|
||||
return docRows.value.filter(
|
||||
(row) => {
|
||||
const matchStore = !queryForm.storeId || row.storeId === queryForm.storeId;
|
||||
const matchParseStatus = !queryForm.parseStatus || row.parseStatus === queryForm.parseStatus;
|
||||
const matchIndexStatus = !queryForm.indexStatus || row.indexStatus === queryForm.indexStatus;
|
||||
const matchEnabled = !queryForm.enabled || String(row.enabled ?? false) === queryForm.enabled;
|
||||
const matchKeyword =
|
||||
!kw ||
|
||||
(row.documentTitle && row.documentTitle.toLowerCase().includes(kw)) ||
|
||||
(row.documentSummary && row.documentSummary.toLowerCase().includes(kw)) ||
|
||||
(row.remark && row.remark.toLowerCase().includes(kw));
|
||||
return matchStore && matchParseStatus && matchIndexStatus && matchEnabled && matchKeyword;
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
async function loadStores() {
|
||||
try {
|
||||
const response = await queryRagStores();
|
||||
storeOptions.value = response.data ?? [];
|
||||
} catch {
|
||||
storeOptions.value = [];
|
||||
}
|
||||
}
|
||||
|
||||
async function loadDocs() {
|
||||
loading.value = true;
|
||||
try {
|
||||
const response = await listRagDocuments();
|
||||
docRows.value = response.data ?? [];
|
||||
} finally {
|
||||
loading.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
function handleSearch() {
|
||||
loadDocs();
|
||||
}
|
||||
|
||||
function handleReset() {
|
||||
queryForm.storeId = '';
|
||||
queryForm.parseStatus = '';
|
||||
queryForm.indexStatus = '';
|
||||
queryForm.enabled = '';
|
||||
queryForm.keyword = '';
|
||||
loadDocs();
|
||||
}
|
||||
|
||||
function openUploadDialog() {
|
||||
if (storeOptions.value.length === 0) {
|
||||
ElMessage.warning('请先创建知识库');
|
||||
return;
|
||||
}
|
||||
const firstStore = storeOptions.value[0];
|
||||
uploadStoreId.value = queryForm.storeId || (firstStore ? String(firstStore.id) : '');
|
||||
uploadSummary.value = '';
|
||||
uploadRemark.value = '';
|
||||
uploadDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitUpload() {
|
||||
if (!uploadStoreId.value) {
|
||||
ElMessage.warning('请选择知识库');
|
||||
return;
|
||||
}
|
||||
const files = (document.getElementById('rag-file-input') as HTMLInputElement)?.files;
|
||||
if (!files || files.length === 0) {
|
||||
ElMessage.warning('请选择要上传的文件');
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
await batchUploadRagDocuments({
|
||||
storeId: uploadStoreId.value,
|
||||
sourceType: SOURCE_TYPE_RAG,
|
||||
files: Array.from(files),
|
||||
documentSummary: uploadSummary.value || undefined,
|
||||
remark: uploadRemark.value || undefined,
|
||||
});
|
||||
uploadDialogVisible.value = false;
|
||||
ElMessage.success('文档已上传');
|
||||
await loadDocs();
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function openEditDialog(row: RagDocument) {
|
||||
const detail = row.id ? (await getRagDocumentById(String(row.id))).data : row;
|
||||
|
||||
editForm.id = String(detail.id ?? '');
|
||||
editForm.storeId = detail.storeId;
|
||||
editForm.attachmentId = detail.attachmentId ?? '';
|
||||
editForm.documentTitle = detail.documentTitle ?? '';
|
||||
editForm.documentSummary = detail.documentSummary ?? '';
|
||||
editForm.enabled = detail.enabled ?? true;
|
||||
editForm.remark = detail.remark ?? '';
|
||||
editDialogVisible.value = true;
|
||||
}
|
||||
|
||||
async function submitEdit() {
|
||||
if (!editForm.id || !editForm.storeId || !editForm.documentTitle) {
|
||||
ElMessage.warning('请填写文档标题');
|
||||
return;
|
||||
}
|
||||
|
||||
submitting.value = true;
|
||||
try {
|
||||
await saveRagDocument({
|
||||
id: editForm.id,
|
||||
storeId: editForm.storeId,
|
||||
attachmentId: editForm.attachmentId || undefined,
|
||||
documentTitle: editForm.documentTitle,
|
||||
documentSummary: editForm.documentSummary || undefined,
|
||||
enabled: editForm.enabled,
|
||||
remark: editForm.remark || undefined,
|
||||
});
|
||||
editDialogVisible.value = false;
|
||||
ElMessage.success('文档信息已更新');
|
||||
await loadDocs();
|
||||
} finally {
|
||||
submitting.value = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function removeDoc(row: RagDocument) {
|
||||
if (!row.id) return;
|
||||
|
||||
await ElMessageBox.confirm(
|
||||
`确认删除文档「${row.documentTitle || '未命名'}」?`,
|
||||
'删除确认',
|
||||
{ type: 'warning', confirmButtonText: '删除', cancelButtonText: '取消' },
|
||||
);
|
||||
|
||||
await deleteRagDocument(String(row.id));
|
||||
ElMessage.success('文档已删除');
|
||||
await loadDocs();
|
||||
}
|
||||
|
||||
function toggleEnabled(row: RagDocument) {
|
||||
if (!row.id) return;
|
||||
const newEnabled = !row.enabled;
|
||||
saveRagDocument({
|
||||
id: String(row.id),
|
||||
storeId: row.storeId,
|
||||
documentTitle: row.documentTitle ?? '',
|
||||
enabled: newEnabled,
|
||||
}).then(() => {
|
||||
row.enabled = newEnabled;
|
||||
ElMessage.success(`已${newEnabled ? '启用' : '停用'}`);
|
||||
});
|
||||
}
|
||||
|
||||
function getStoreName(storeId: string) {
|
||||
const store = storeOptions.value.find((s) => String(s.id) === storeId);
|
||||
return store ? store.storeName : '-';
|
||||
}
|
||||
|
||||
function getStatusLabel(status?: string | null) {
|
||||
const map: Record<string, string> = {
|
||||
UPLOADED: '已上传',
|
||||
PARSING: '解析中',
|
||||
PARSED: '已解析',
|
||||
FAILED: '解析失败',
|
||||
PENDING: '待索引',
|
||||
INDEXING: '索引中',
|
||||
INDEXED: '已索引',
|
||||
};
|
||||
return status ? (map[status] ?? status) : '-';
|
||||
}
|
||||
|
||||
function getParseStatusType(status?: string | null) {
|
||||
const success = ['PARSED'];
|
||||
const warning = ['UPLOADED', 'PARSING'];
|
||||
const danger = ['FAILED'];
|
||||
if (!status) return 'info';
|
||||
if (success.includes(status)) return 'success';
|
||||
if (warning.includes(status)) return 'warning';
|
||||
if (danger.includes(status)) return 'danger';
|
||||
return 'info';
|
||||
}
|
||||
|
||||
function getIndexStatusType(status?: string | null) {
|
||||
const success = ['INDEXED'];
|
||||
const warning = ['PENDING', 'INDEXING'];
|
||||
const danger = ['FAILED'];
|
||||
if (!status) return 'info';
|
||||
if (success.includes(status)) return 'success';
|
||||
if (warning.includes(status)) return 'warning';
|
||||
if (danger.includes(status)) return 'danger';
|
||||
return 'info';
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadStores();
|
||||
loadDocs();
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section class="page-panel">
|
||||
<section class="page-panel rag-doc-page">
|
||||
<div class="page-panel__header">
|
||||
<h2>知识文档</h2>
|
||||
<span>Documents</span>
|
||||
</div>
|
||||
|
||||
<div class="toolbar">
|
||||
<div class="toolbar__filters">
|
||||
<el-select
|
||||
v-model="queryForm.storeId"
|
||||
data-test="doc-store-filter"
|
||||
placeholder="选择知识库"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
>
|
||||
<el-option
|
||||
v-for="store in storeOptions"
|
||||
:key="String(store.id)"
|
||||
:label="store.storeName"
|
||||
:value="String(store.id)"
|
||||
/>
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="queryForm.parseStatus"
|
||||
data-test="doc-parse-filter"
|
||||
placeholder="解析状态"
|
||||
clearable
|
||||
style="width: 130px"
|
||||
>
|
||||
<el-option label="已上传" value="UPLOADED" />
|
||||
<el-option label="解析中" value="PARSING" />
|
||||
<el-option label="已解析" value="PARSED" />
|
||||
<el-option label="解析失败" value="FAILED" />
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="queryForm.indexStatus"
|
||||
data-test="doc-index-filter"
|
||||
placeholder="索引状态"
|
||||
clearable
|
||||
style="width: 130px"
|
||||
>
|
||||
<el-option label="待索引" value="PENDING" />
|
||||
<el-option label="索引中" value="INDEXING" />
|
||||
<el-option label="已索引" value="INDEXED" />
|
||||
<el-option label="索引失败" value="FAILED" />
|
||||
</el-select>
|
||||
<el-select
|
||||
v-model="queryForm.enabled"
|
||||
data-test="doc-enabled-filter"
|
||||
placeholder="启用状态"
|
||||
clearable
|
||||
style="width: 120px"
|
||||
>
|
||||
<el-option label="启用" value="true" />
|
||||
<el-option label="停用" value="false" />
|
||||
</el-select>
|
||||
<el-input
|
||||
v-model="queryForm.keyword"
|
||||
data-test="doc-keyword-input"
|
||||
placeholder="搜索标题/摘要/备注"
|
||||
clearable
|
||||
style="width: 180px"
|
||||
@keyup.enter="handleSearch"
|
||||
/>
|
||||
</div>
|
||||
<div class="toolbar__actions">
|
||||
<el-button type="primary" :icon="UploadFilled" @click="openUploadDialog">批量上传</el-button>
|
||||
<el-button data-test="doc-search" type="primary" :icon="Search" @click="handleSearch">查询</el-button>
|
||||
<el-button @click="handleReset">重置</el-button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<el-table v-loading="loading" :data="filteredRows" border stripe style="width: 100%">
|
||||
<el-table-column prop="documentTitle" label="文档标题" min-width="180" show-overflow-tooltip />
|
||||
<el-table-column label="所属知识库" width="150">
|
||||
<template #default="{ row }">
|
||||
{{ getStoreName(row.storeId) }}
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="解析状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getParseStatusType(row.parseStatus)" size="small">
|
||||
{{ getStatusLabel(row.parseStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="索引状态" width="110">
|
||||
<template #default="{ row }">
|
||||
<el-tag :type="getIndexStatusType(row.indexStatus)" size="small">
|
||||
{{ getStatusLabel(row.indexStatus) }}
|
||||
</el-tag>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column label="启用" width="80" align="center">
|
||||
<template #default="{ row }">
|
||||
<el-switch
|
||||
:model-value="row.enabled ?? false"
|
||||
size="small"
|
||||
@change="toggleEnabled(row)"
|
||||
/>
|
||||
</template>
|
||||
</el-table-column>
|
||||
<el-table-column prop="documentSummary" label="摘要" min-width="160" show-overflow-tooltip />
|
||||
<el-table-column prop="createTime" label="创建时间" width="170" />
|
||||
<el-table-column label="操作" width="160" fixed="right">
|
||||
<template #default="{ row }">
|
||||
<el-button :data-test="`doc-edit-${row.id}`" link type="primary" @click="openEditDialog(row)">编辑</el-button>
|
||||
<el-button link type="danger" @click="removeDoc(row)">删除</el-button>
|
||||
</template>
|
||||
</el-table-column>
|
||||
</el-table>
|
||||
|
||||
<el-empty v-if="!loading && filteredRows.length === 0" description="暂无知识文档" />
|
||||
|
||||
<!-- 上传对话框 -->
|
||||
<el-dialog v-model="uploadDialogVisible" title="批量上传文档" width="560px">
|
||||
<el-form label-width="96px">
|
||||
<el-form-item label="知识库" required>
|
||||
<el-select v-model="uploadStoreId" placeholder="请选择知识库" style="width: 100%">
|
||||
<el-option
|
||||
v-for="store in storeOptions"
|
||||
:key="String(store.id)"
|
||||
:label="store.storeName"
|
||||
:value="String(store.id)"
|
||||
/>
|
||||
</el-select>
|
||||
</el-form-item>
|
||||
<el-form-item label="选择文件" required>
|
||||
<input id="rag-file-input" type="file" multiple accept=".pdf,.doc,.docx,.txt,.md" />
|
||||
<p class="form-hint">支持 PDF、Word、TXT、Markdown 等格式</p>
|
||||
</el-form-item>
|
||||
<el-form-item label="文档摘要">
|
||||
<el-input
|
||||
v-model="uploadSummary"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="可选,将统一设置到所有上传文档"
|
||||
/>
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input
|
||||
v-model="uploadRemark"
|
||||
type="textarea"
|
||||
:rows="2"
|
||||
placeholder="可选"
|
||||
/>
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="uploadDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitUpload">上传</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 编辑对话框 -->
|
||||
<el-dialog v-model="editDialogVisible" title="编辑文档" width="560px">
|
||||
<el-form :model="editForm" label-width="96px">
|
||||
<el-form-item label="文档标题" required>
|
||||
<el-input v-model="editForm.documentTitle" />
|
||||
</el-form-item>
|
||||
<el-form-item label="文档摘要">
|
||||
<el-input v-model="editForm.documentSummary" type="textarea" :rows="3" />
|
||||
</el-form-item>
|
||||
<el-form-item label="启用">
|
||||
<el-switch v-model="editForm.enabled" />
|
||||
</el-form-item>
|
||||
<el-form-item label="备注">
|
||||
<el-input v-model="editForm.remark" type="textarea" :rows="2" />
|
||||
</el-form-item>
|
||||
</el-form>
|
||||
<template #footer>
|
||||
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||
<el-button type="primary" :loading="submitting" @click="submitEdit">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.rag-doc-page {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: flex-start;
|
||||
gap: 16px;
|
||||
padding: 16px 22px;
|
||||
border-bottom: 1px solid #eef2f7;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.toolbar__filters {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toolbar__actions {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.form-hint {
|
||||
margin: 4px 0 0;
|
||||
color: #98a2b3;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.toolbar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.toolbar__filters {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.toolbar__actions {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,8 +6,12 @@ import { computed, onMounted, reactive, ref } from 'vue';
|
||||
import {
|
||||
deleteRagStore,
|
||||
getRagStoreById,
|
||||
getRagStoreDocumentOverview,
|
||||
getRagStoreOverview,
|
||||
queryRagStores,
|
||||
saveRagStore,
|
||||
type RagStoreDocumentOverview,
|
||||
type RagStoreOverview,
|
||||
type RagStore,
|
||||
} from '@/api/ragStores';
|
||||
|
||||
@@ -19,6 +23,8 @@ const submitting = ref(false);
|
||||
const storeRows = ref<RagStore[]>([]);
|
||||
const activeStoreId = ref<string | null>(null);
|
||||
const activeStore = ref<RagStore | null>(null);
|
||||
const pageOverview = ref<RagStoreOverview | null>(null);
|
||||
const activeStoreDocumentOverview = ref<RagStoreDocumentOverview | null>(null);
|
||||
|
||||
const queryForm = reactive({
|
||||
storeName: '',
|
||||
@@ -45,17 +51,30 @@ const editForm = reactive({
|
||||
});
|
||||
|
||||
const overviewCards = computed(() => {
|
||||
const totalStores = storeRows.value.length;
|
||||
const retrievableStores = storeRows.value.filter((row) => row.status === '启用').length;
|
||||
const totalStores = pageOverview.value?.totalStores ?? storeRows.value.length;
|
||||
const totalDocuments = pageOverview.value?.totalDocuments ?? '-';
|
||||
const totalChunks = pageOverview.value?.totalChunks ?? '-';
|
||||
const retrievableStores =
|
||||
pageOverview.value?.retrievableStores
|
||||
?? storeRows.value.filter((row) => row.status === '启用').length;
|
||||
|
||||
return [
|
||||
{ label: '知识库总数', value: totalStores, hint: '当前已登记知识库' },
|
||||
{ label: '文档总数', value: '-', hint: '待文档统计接口补充' },
|
||||
{ label: '切片总数', value: '-', hint: '待切片统计接口补充' },
|
||||
{ label: '可检索知识库数', value: retrievableStores, hint: '当前按启用状态暂代统计' },
|
||||
{ label: '文档总数', value: totalDocuments, hint: '当前知识库已登记文档总量' },
|
||||
{ label: '切片总数', value: totalChunks, hint: '当前未接入切片表,待后续能力补充' },
|
||||
{ label: '可检索知识库数', value: retrievableStores, hint: '当前按启用状态统计' },
|
||||
];
|
||||
});
|
||||
|
||||
async function loadOverview() {
|
||||
try {
|
||||
const response = await getRagStoreOverview();
|
||||
pageOverview.value = response.data ?? null;
|
||||
} catch {
|
||||
pageOverview.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStores(preferredStoreId?: string | null) {
|
||||
loading.value = true;
|
||||
try {
|
||||
@@ -67,6 +86,7 @@ async function loadStores(preferredStoreId?: string | null) {
|
||||
if (storeRows.value.length === 0) {
|
||||
activeStoreId.value = null;
|
||||
activeStore.value = null;
|
||||
activeStoreDocumentOverview.value = null;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -80,6 +100,7 @@ async function loadStores(preferredStoreId?: string | null) {
|
||||
if (!targetId) {
|
||||
activeStoreId.value = null;
|
||||
activeStore.value = null;
|
||||
activeStoreDocumentOverview.value = null;
|
||||
return;
|
||||
}
|
||||
await selectStore(targetId);
|
||||
@@ -92,8 +113,12 @@ async function selectStore(storeId: string) {
|
||||
activeStoreId.value = storeId;
|
||||
detailLoading.value = true;
|
||||
try {
|
||||
const response = await getRagStoreById(storeId);
|
||||
activeStore.value = response.data ?? null;
|
||||
const [storeResponse, documentOverviewResponse] = await Promise.all([
|
||||
getRagStoreById(storeId),
|
||||
getRagStoreDocumentOverview(storeId),
|
||||
]);
|
||||
activeStore.value = storeResponse.data ?? null;
|
||||
activeStoreDocumentOverview.value = documentOverviewResponse.data ?? null;
|
||||
} finally {
|
||||
detailLoading.value = false;
|
||||
}
|
||||
@@ -203,6 +228,7 @@ function getStatusTagType(status?: string | null) {
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadOverview();
|
||||
loadStores();
|
||||
});
|
||||
</script>
|
||||
@@ -327,9 +353,25 @@ onMounted(() => {
|
||||
<article class="detail-card detail-card--placeholder">
|
||||
<div class="detail-card__header">
|
||||
<h4>文档概览</h4>
|
||||
<span>下一批接口补充</span>
|
||||
<span>已对接后端聚合接口</span>
|
||||
</div>
|
||||
<el-empty description="文档数量、切片数量、最近上传时间待后端聚合接口补充" />
|
||||
<el-descriptions :column="2" border>
|
||||
<el-descriptions-item label="文档总数">
|
||||
{{ activeStoreDocumentOverview?.documentCount ?? 0 }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="启用文档数">
|
||||
{{ activeStoreDocumentOverview?.enabledDocumentCount ?? 0 }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="已解析文档数">
|
||||
{{ activeStoreDocumentOverview?.parsedDocumentCount ?? 0 }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="已索引文档数">
|
||||
{{ activeStoreDocumentOverview?.indexedDocumentCount ?? 0 }}
|
||||
</el-descriptions-item>
|
||||
<el-descriptions-item label="最近上传时间" :span="2">
|
||||
{{ activeStoreDocumentOverview?.lastUploadTime || '-' }}
|
||||
</el-descriptions-item>
|
||||
</el-descriptions>
|
||||
</article>
|
||||
|
||||
<article class="detail-card detail-card--placeholder">
|
||||
|
||||
125
frontend/src/pages/__tests__/RagDocumentsPage.spec.ts
Normal file
125
frontend/src/pages/__tests__/RagDocumentsPage.spec.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
import { flushPromises, mount } from '@vue/test-utils';
|
||||
import ElementPlus from 'element-plus';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import RagDocumentsPage from '../RagDocumentsPage.vue';
|
||||
import { getRagDocumentById, listRagDocuments, queryRagDocuments } from '@/api/ragDocuments';
|
||||
import { queryRagStores } from '@/api/ragStores';
|
||||
|
||||
vi.mock('@/api/ragStores', () => ({
|
||||
queryRagStores: vi.fn(() =>
|
||||
Promise.resolve({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: [
|
||||
{ id: '1', storeCode: 'PROD_DOC', storeName: '产品制度库', status: '启用' },
|
||||
{ id: '2', storeCode: 'FAQ', storeName: 'FAQ知识库', status: '停用' },
|
||||
],
|
||||
}),
|
||||
),
|
||||
}));
|
||||
|
||||
vi.mock('@/api/ragDocuments', () => ({
|
||||
SOURCE_TYPE_RAG: 'RAG',
|
||||
listRagDocuments: vi.fn(() =>
|
||||
Promise.resolve({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: [
|
||||
{
|
||||
id: '11',
|
||||
storeId: '1',
|
||||
attachmentId: '101',
|
||||
documentTitle: '产品制度总则',
|
||||
documentSummary: '制度摘要',
|
||||
parseStatus: 'UPLOADED',
|
||||
indexStatus: 'PENDING',
|
||||
enabled: true,
|
||||
remark: '制度文档',
|
||||
createTime: '2026-05-21 10:00:00',
|
||||
},
|
||||
{
|
||||
id: '22',
|
||||
storeId: '2',
|
||||
attachmentId: '202',
|
||||
documentTitle: 'FAQ 手册',
|
||||
documentSummary: 'FAQ 摘要',
|
||||
parseStatus: 'PARSED',
|
||||
indexStatus: 'INDEXED',
|
||||
enabled: false,
|
||||
remark: '常见问题',
|
||||
createTime: '2026-05-21 11:00:00',
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
queryRagDocuments: vi.fn(),
|
||||
getRagDocumentById: vi.fn((id: string) =>
|
||||
Promise.resolve({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: {
|
||||
id,
|
||||
storeId: '2',
|
||||
attachmentId: '202',
|
||||
documentTitle: 'FAQ 手册',
|
||||
documentSummary: 'FAQ 摘要',
|
||||
enabled: false,
|
||||
remark: '常见问题',
|
||||
},
|
||||
}),
|
||||
),
|
||||
saveRagDocument: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
|
||||
deleteRagDocument: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
|
||||
batchUploadRagDocuments: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: [] })),
|
||||
}));
|
||||
|
||||
describe('RagDocumentsPage', () => {
|
||||
it('loads documents from list api instead of broken query api', async () => {
|
||||
const wrapper = mount(RagDocumentsPage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
|
||||
expect(queryRagStores).toHaveBeenCalled();
|
||||
expect(listRagDocuments).toHaveBeenCalled();
|
||||
expect(queryRagDocuments).not.toHaveBeenCalled();
|
||||
expect(wrapper.text()).toContain('产品制度总则');
|
||||
expect(wrapper.text()).toContain('FAQ 手册');
|
||||
});
|
||||
|
||||
it('filters rows locally and still avoids query api on search', async () => {
|
||||
const wrapper = mount(RagDocumentsPage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
await wrapper.get('[data-test="doc-keyword-input"]').setValue('FAQ');
|
||||
await wrapper.get('[data-test="doc-search"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(listRagDocuments).toHaveBeenCalled();
|
||||
expect(queryRagDocuments).not.toHaveBeenCalled();
|
||||
expect(wrapper.text()).toContain('FAQ 手册');
|
||||
expect(wrapper.text()).not.toContain('产品制度总则');
|
||||
});
|
||||
|
||||
it('loads backend detail when editing a row', async () => {
|
||||
const wrapper = mount(RagDocumentsPage, {
|
||||
global: {
|
||||
plugins: [ElementPlus],
|
||||
},
|
||||
});
|
||||
|
||||
await flushPromises();
|
||||
await wrapper.get('[data-test="doc-edit-22"]').trigger('click');
|
||||
await flushPromises();
|
||||
|
||||
expect(getRagDocumentById).toHaveBeenCalledWith('22');
|
||||
});
|
||||
});
|
||||
@@ -3,9 +3,27 @@ import ElementPlus from 'element-plus';
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
|
||||
import RagStoresPage from '../RagStoresPage.vue';
|
||||
import { getRagStoreById, queryRagStores, saveRagStore } from '@/api/ragStores';
|
||||
import {
|
||||
getRagStoreById,
|
||||
getRagStoreDocumentOverview,
|
||||
getRagStoreOverview,
|
||||
queryRagStores,
|
||||
saveRagStore,
|
||||
} from '@/api/ragStores';
|
||||
|
||||
vi.mock('@/api/ragStores', () => ({
|
||||
getRagStoreOverview: vi.fn(() =>
|
||||
Promise.resolve({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data: {
|
||||
totalStores: 2,
|
||||
totalDocuments: 12,
|
||||
totalChunks: null,
|
||||
retrievableStores: 1,
|
||||
},
|
||||
}),
|
||||
),
|
||||
queryRagStores: vi.fn((query?: { storeName?: string }) => {
|
||||
const rows = [
|
||||
{
|
||||
@@ -60,6 +78,32 @@ vi.mock('@/api/ragStores', () => ({
|
||||
},
|
||||
}),
|
||||
),
|
||||
getRagStoreDocumentOverview: vi.fn((storeId: string) =>
|
||||
Promise.resolve({
|
||||
resultcode: '0',
|
||||
message: null,
|
||||
data:
|
||||
storeId === '2'
|
||||
? {
|
||||
storeId: '2',
|
||||
storeName: 'FAQ知识库',
|
||||
documentCount: 3,
|
||||
enabledDocumentCount: 1,
|
||||
parsedDocumentCount: 1,
|
||||
indexedDocumentCount: 1,
|
||||
lastUploadTime: '2026-05-21 11:12:00',
|
||||
}
|
||||
: {
|
||||
storeId: '1',
|
||||
storeName: '产品制度库',
|
||||
documentCount: 9,
|
||||
enabledDocumentCount: 8,
|
||||
parsedDocumentCount: 6,
|
||||
indexedDocumentCount: 5,
|
||||
lastUploadTime: '2026-05-21 16:40:00',
|
||||
},
|
||||
}),
|
||||
),
|
||||
saveRagStore: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
|
||||
deleteRagStore: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
|
||||
}));
|
||||
@@ -75,10 +119,16 @@ describe('RagStoresPage', () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(wrapper.text()).toContain('知识库总数');
|
||||
expect(wrapper.text()).toContain('12');
|
||||
expect(wrapper.text()).toContain('产品制度库');
|
||||
expect(wrapper.text()).toContain('核心制度库');
|
||||
expect(wrapper.text()).toContain('文档总数');
|
||||
expect(wrapper.text()).toContain('最近上传时间');
|
||||
expect(wrapper.text()).toContain('2026-05-21 16:40:00');
|
||||
expect(getRagStoreOverview).toHaveBeenCalled();
|
||||
expect(queryRagStores).toHaveBeenCalled();
|
||||
expect(getRagStoreById).toHaveBeenCalledWith('1');
|
||||
expect(getRagStoreDocumentOverview).toHaveBeenCalledWith('1');
|
||||
});
|
||||
|
||||
it('filters stores by name and updates detail when a store is selected', async () => {
|
||||
@@ -103,7 +153,9 @@ describe('RagStoresPage', () => {
|
||||
await flushPromises();
|
||||
|
||||
expect(getRagStoreById).toHaveBeenLastCalledWith('2');
|
||||
expect(getRagStoreDocumentOverview).toHaveBeenLastCalledWith('2');
|
||||
expect(wrapper.text()).toContain('FAQ 场景知识');
|
||||
expect(wrapper.text()).toContain('3');
|
||||
});
|
||||
|
||||
it('submits create form through backend api', async () => {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.bruce.common.config;
|
||||
|
||||
import com.bruce.common.constant.CommonConsts;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
@@ -11,6 +12,6 @@ import org.springframework.stereotype.Component;
|
||||
@ConfigurationProperties(prefix = "common.attachment")
|
||||
public class AttachmentProperties {
|
||||
|
||||
private String basePath = "data/attachments";
|
||||
private String basePath = CommonConsts.DEFAULT_ATTACHMENT_BASE_PATH;
|
||||
|
||||
}
|
||||
|
||||
21
src/main/java/com/bruce/common/constant/CommonConsts.java
Normal file
21
src/main/java/com/bruce/common/constant/CommonConsts.java
Normal file
@@ -0,0 +1,21 @@
|
||||
package com.bruce.common.constant;
|
||||
|
||||
public final class CommonConsts {
|
||||
|
||||
public static final String DATE_FORMAT_LONG_STR = "yyyy-MM-dd HH:mm:ss";
|
||||
|
||||
public static final String DATE_FORMAT_MILLIS_STR = "yyyy-MM-dd HH:mm:ss.SSS";
|
||||
|
||||
public static final String TIME_ZONE_GMT8 = "GMT+8";
|
||||
|
||||
public static final String DEFAULT_ATTACHMENT_BASE_PATH = "data/attachments";
|
||||
|
||||
public static final String STORAGE_TYPE_LOCAL = "LOCAL";
|
||||
|
||||
public static final String REQUEST_RESULT_SUCCESS_CODE = "0";
|
||||
|
||||
public static final String REQUEST_RESULT_FAIL_CODE = "-1";
|
||||
|
||||
private CommonConsts() {
|
||||
}
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import com.baomidou.mybatisplus.annotation.IdType;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.Version;
|
||||
import com.bruce.common.constant.CommonConsts;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
@@ -24,7 +25,7 @@ public class BaseEntity {
|
||||
private String createBy;
|
||||
|
||||
@Schema(description = "创建时间", example = "2026-05-18 20:00:00")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(pattern = CommonConsts.DATE_FORMAT_LONG_STR, timezone = CommonConsts.TIME_ZONE_GMT8)
|
||||
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
@@ -33,7 +34,7 @@ public class BaseEntity {
|
||||
private String updateBy;
|
||||
|
||||
@Schema(description = "更新时间", example = "2026-05-18 20:00:00")
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
@JsonFormat(pattern = CommonConsts.DATE_FORMAT_LONG_STR, timezone = CommonConsts.TIME_ZONE_GMT8)
|
||||
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.bruce.common.domain.model;
|
||||
|
||||
import com.bruce.common.constant.CommonConsts;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
@@ -10,9 +11,9 @@ import lombok.NoArgsConstructor;
|
||||
@AllArgsConstructor
|
||||
public class RequestResult<T> {
|
||||
|
||||
public static final String FAIL_CODE = "-1";
|
||||
public static final String FAIL_CODE = CommonConsts.REQUEST_RESULT_FAIL_CODE;
|
||||
|
||||
public static final String SUCCESS_CODE = "0";
|
||||
public static final String SUCCESS_CODE = CommonConsts.REQUEST_RESULT_SUCCESS_CODE;
|
||||
|
||||
@Schema(description = "错误消息")
|
||||
private String message;
|
||||
|
||||
@@ -2,6 +2,7 @@ package com.bruce.common.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bruce.common.config.AttachmentProperties;
|
||||
import com.bruce.common.constant.CommonConsts;
|
||||
import com.bruce.common.domain.entity.SysAttachment;
|
||||
import com.bruce.common.dto.request.SysAttachmentUploadRequest;
|
||||
import com.bruce.common.mapper.SysAttachmentMapper;
|
||||
@@ -22,8 +23,6 @@ import java.util.UUID;
|
||||
@Service
|
||||
public class SysAttachmentServiceImpl extends ServiceImpl<SysAttachmentMapper, SysAttachment> implements ISysAttachmentService {
|
||||
|
||||
private static final String STORAGE_TYPE_LOCAL = "LOCAL";
|
||||
|
||||
@Autowired
|
||||
private AttachmentProperties attachmentProperties;
|
||||
|
||||
@@ -64,7 +63,7 @@ public class SysAttachmentServiceImpl extends ServiceImpl<SysAttachmentMapper, S
|
||||
attachment.setFileSuffix(suffix);
|
||||
attachment.setContentType(file.getContentType());
|
||||
attachment.setFileSize(file.getSize());
|
||||
attachment.setStorageType(STORAGE_TYPE_LOCAL);
|
||||
attachment.setStorageType(CommonConsts.STORAGE_TYPE_LOCAL);
|
||||
attachment.setFilePath(dateDirectory + "/" + storedFileName);
|
||||
attachment.setFileUrl(null);
|
||||
attachment.setVersion(1);
|
||||
|
||||
@@ -6,6 +6,11 @@ public final class RagSystemConstants {
|
||||
|
||||
public static final String RAG_DOCUMENT = "RAG_DOCUMENT";
|
||||
|
||||
/**
|
||||
* 用于 sys_attachment.sourceType 标识该附件归属于 RAG 知识库业务。
|
||||
*/
|
||||
public static final String SOURCE_TYPE_RAG = "RAG";
|
||||
|
||||
private RagSystemConstants() {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,21 +1,27 @@
|
||||
package com.bruce.rag.controller;
|
||||
|
||||
import com.bruce.common.domain.model.RequestResult;
|
||||
import com.bruce.rag.dto.request.RagDocumentBatchUploadRequest;
|
||||
import com.bruce.rag.dto.request.RagDocumentQueryRequest;
|
||||
import com.bruce.rag.dto.request.RagDocumentSaveRequest;
|
||||
import com.bruce.rag.dto.response.RagDocumentResponse;
|
||||
import com.bruce.rag.service.IRagDocumentService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Tag(name = "RAG知识库文档管理")
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/rag/documents")
|
||||
public class RagDocumentController {
|
||||
@@ -24,14 +30,59 @@ public class RagDocumentController {
|
||||
private IRagDocumentService ragDocumentService;
|
||||
|
||||
@Operation(summary = "查询全部知识库文档")
|
||||
@GetMapping
|
||||
@PostMapping("/list")
|
||||
public RequestResult<List<RagDocumentResponse>> list() {
|
||||
return RequestResult.success(ragDocumentService.listResponses());
|
||||
log.info("RagDocumentController.list start");
|
||||
List<RagDocumentResponse> responses = ragDocumentService.listResponses();
|
||||
log.info("RagDocumentController.list success, count={}", responses.size());
|
||||
return RequestResult.success(responses);
|
||||
}
|
||||
|
||||
@Operation(summary = "按条件查询知识库文档")
|
||||
@PostMapping("/query")
|
||||
public RequestResult<List<RagDocumentResponse>> query(@RequestBody RagDocumentQueryRequest request) {
|
||||
return RequestResult.success(ragDocumentService.query(request));
|
||||
public RequestResult<List<RagDocumentResponse>> query(@RequestBody(required = false) RagDocumentQueryRequest request) {
|
||||
log.info("RagDocumentController.query start, request={}", request);
|
||||
List<RagDocumentResponse> responses = ragDocumentService.query(request);
|
||||
log.info("RagDocumentController.query success, count={}", responses.size());
|
||||
return RequestResult.success(responses);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询知识库文档详情")
|
||||
@GetMapping("/detail")
|
||||
public RequestResult<RagDocumentResponse> getById(@RequestParam("id") Long id) {
|
||||
log.info("RagDocumentController.getById start, id={}", id);
|
||||
RagDocumentResponse response = ragDocumentService.getResponseById(id);
|
||||
log.info("RagDocumentController.getById success, id={}, found={}", id, response != null);
|
||||
return RequestResult.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "新增或修改知识库文档")
|
||||
@PostMapping("/save")
|
||||
public RequestResult<Boolean> saveOrUpdate(@RequestBody RagDocumentSaveRequest request) {
|
||||
log.info("RagDocumentController.saveOrUpdate start, request={}", request);
|
||||
Boolean result = ragDocumentService.saveOrUpdate(request);
|
||||
log.info("RagDocumentController.saveOrUpdate success, id={}, result={}",
|
||||
request.getId(), result);
|
||||
return RequestResult.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "删除知识库文档")
|
||||
@PostMapping("/delete")
|
||||
public RequestResult<Boolean> deleteById(@RequestParam("id") Long id) {
|
||||
log.info("RagDocumentController.deleteById start, id={}", id);
|
||||
Boolean result = ragDocumentService.removeById(id);
|
||||
log.info("RagDocumentController.deleteById success, id={}, result={}", id, result);
|
||||
return RequestResult.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "批量上传文档到知识库")
|
||||
@PostMapping("/batchUpload")
|
||||
public RequestResult<List<RagDocumentResponse>> batchUpload(@ModelAttribute RagDocumentBatchUploadRequest request) {
|
||||
log.info("RagDocumentController.batchUpload start, storeId={}, fileCount={}",
|
||||
request.getStoreId(), request.getFiles() != null ? request.getFiles().length : 0);
|
||||
List<RagDocumentResponse> responses = ragDocumentService.batchUpload(request);
|
||||
log.info("RagDocumentController.batchUpload success, storeId={}, uploaded={}",
|
||||
request.getStoreId(), responses.size());
|
||||
return RequestResult.success(responses);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.bruce.rag.controller;
|
||||
import com.bruce.common.domain.model.RequestResult;
|
||||
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
||||
import com.bruce.rag.dto.request.RagStoreSaveRequest;
|
||||
import com.bruce.rag.dto.response.RagStoreDocumentOverviewResponse;
|
||||
import com.bruce.rag.dto.response.RagStoreOverviewResponse;
|
||||
import com.bruce.rag.dto.response.RagStoreResponse;
|
||||
import com.bruce.rag.service.IRagStoreService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
@@ -54,6 +56,26 @@ public class RagStoreController {
|
||||
return RequestResult.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询知识库总览")
|
||||
@GetMapping("/overview")
|
||||
public RequestResult<RagStoreOverviewResponse> overview() {
|
||||
log.info("RagStoreController.overview start");
|
||||
RagStoreOverviewResponse response = ragStoreService.getOverview();
|
||||
log.info("RagStoreController.overview success, totalStores={}, totalDocuments={}",
|
||||
response.getTotalStores(), response.getTotalDocuments());
|
||||
return RequestResult.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询知识库文档概览")
|
||||
@GetMapping("/documentOverview")
|
||||
public RequestResult<RagStoreDocumentOverviewResponse> documentOverview(@RequestParam("storeId") Long storeId) {
|
||||
log.info("RagStoreController.documentOverview start, storeId={}", storeId);
|
||||
RagStoreDocumentOverviewResponse response = ragStoreService.getDocumentOverview(storeId);
|
||||
log.info("RagStoreController.documentOverview success, storeId={}, documentCount={}",
|
||||
storeId, response.getDocumentCount());
|
||||
return RequestResult.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "新增或修改知识库")
|
||||
@PostMapping("/save")
|
||||
public RequestResult<Boolean> saveOrUpdate(@RequestBody RagStoreSaveRequest request) {
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.bruce.rag.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@Data
|
||||
@Schema(description = "RAG知识库文档批量上传请求")
|
||||
public class RagDocumentBatchUploadRequest {
|
||||
|
||||
@Schema(description = "知识库ID")
|
||||
private Long storeId;
|
||||
|
||||
@Schema(description = "附件来源类型,RAG 文档上传固定传 RAG")
|
||||
private String sourceType;
|
||||
|
||||
@Schema(description = "上传文件列表")
|
||||
private MultipartFile[] files;
|
||||
|
||||
@Schema(description = "文档摘要(批量设置)")
|
||||
private String documentSummary;
|
||||
|
||||
@Schema(description = "备注(批量设置)")
|
||||
private String remark;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.bruce.rag.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "RAG知识库文档保存请求")
|
||||
public class RagDocumentSaveRequest {
|
||||
|
||||
@Schema(description = "主键ID(更新时必填)")
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "知识库ID")
|
||||
private Long storeId;
|
||||
|
||||
@Schema(description = "附件ID")
|
||||
private Long attachmentId;
|
||||
|
||||
@Schema(description = "文档标题")
|
||||
private String documentTitle;
|
||||
|
||||
@Schema(description = "文档摘要")
|
||||
private String documentSummary;
|
||||
|
||||
@Schema(description = "解析状态")
|
||||
private String parseStatus;
|
||||
|
||||
@Schema(description = "索引状态")
|
||||
private String indexStatus;
|
||||
|
||||
@Schema(description = "是否启用")
|
||||
private Boolean enabled;
|
||||
|
||||
@Schema(description = "失败原因")
|
||||
private String errorMessage;
|
||||
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
}
|
||||
@@ -1,21 +1,30 @@
|
||||
package com.bruce.rag.dto.response;
|
||||
|
||||
import com.bruce.common.constant.CommonConsts;
|
||||
import com.bruce.rag.entity.RagDocument;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Schema(description = "RAG知识库文档响应")
|
||||
public class RagDocumentResponse {
|
||||
|
||||
@Schema(description = "主键ID")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
@Schema(description = "知识库ID")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long storeId;
|
||||
|
||||
@Schema(description = "附件ID")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long attachmentId;
|
||||
|
||||
@Schema(description = "文档标题")
|
||||
@@ -39,6 +48,14 @@ public class RagDocumentResponse {
|
||||
@Schema(description = "备注")
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@JsonFormat(pattern = CommonConsts.DATE_FORMAT_LONG_STR, timezone = CommonConsts.TIME_ZONE_GMT8)
|
||||
private Date createTime;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@JsonFormat(pattern = CommonConsts.DATE_FORMAT_LONG_STR, timezone = CommonConsts.TIME_ZONE_GMT8)
|
||||
private Date updateTime;
|
||||
|
||||
public static RagDocumentResponse fromEntity(RagDocument entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.bruce.rag.dto.response;
|
||||
|
||||
import com.bruce.common.constant.CommonConsts;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
@Data
|
||||
@Schema(description = "RAG知识库文档概览响应")
|
||||
public class RagStoreDocumentOverviewResponse {
|
||||
|
||||
@Schema(description = "知识库ID")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long storeId;
|
||||
|
||||
@Schema(description = "知识库名称")
|
||||
private String storeName;
|
||||
|
||||
@Schema(description = "文档总数")
|
||||
private Integer documentCount;
|
||||
|
||||
@Schema(description = "启用文档数")
|
||||
private Integer enabledDocumentCount;
|
||||
|
||||
@Schema(description = "已解析文档数")
|
||||
private Integer parsedDocumentCount;
|
||||
|
||||
@Schema(description = "已索引文档数")
|
||||
private Integer indexedDocumentCount;
|
||||
|
||||
@Schema(description = "最近上传时间")
|
||||
@JsonFormat(pattern = CommonConsts.DATE_FORMAT_LONG_STR, timezone = CommonConsts.TIME_ZONE_GMT8)
|
||||
private Date lastUploadTime;
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package com.bruce.rag.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
@Data
|
||||
@Schema(description = "RAG知识库总览响应")
|
||||
public class RagStoreOverviewResponse {
|
||||
|
||||
@Schema(description = "知识库总数")
|
||||
private Integer totalStores;
|
||||
|
||||
@Schema(description = "文档总数")
|
||||
private Integer totalDocuments;
|
||||
|
||||
@Schema(description = "切片总数")
|
||||
private Integer totalChunks;
|
||||
|
||||
@Schema(description = "可检索知识库数")
|
||||
private Integer retrievableStores;
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
package com.bruce.rag.dto.response;
|
||||
|
||||
import com.bruce.common.constant.CommonConsts;
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import com.bruce.rag.entity.RagStore;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
import org.springframework.beans.BeanUtils;
|
||||
@@ -33,9 +35,11 @@ public class RagStoreResponse {
|
||||
private String remark;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
@JsonFormat(pattern = CommonConsts.DATE_FORMAT_LONG_STR, timezone = CommonConsts.TIME_ZONE_GMT8)
|
||||
private Date createTime;
|
||||
|
||||
@Schema(description = "更新时间")
|
||||
@JsonFormat(pattern = CommonConsts.DATE_FORMAT_LONG_STR, timezone = CommonConsts.TIME_ZONE_GMT8)
|
||||
private Date updateTime;
|
||||
|
||||
public static RagStoreResponse fromEntity(RagStore entity) {
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.bruce.rag.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bruce.rag.dto.request.RagDocumentBatchUploadRequest;
|
||||
import com.bruce.rag.dto.request.RagDocumentQueryRequest;
|
||||
import com.bruce.rag.dto.request.RagDocumentSaveRequest;
|
||||
import com.bruce.rag.dto.response.RagDocumentResponse;
|
||||
import com.bruce.rag.entity.RagDocument;
|
||||
|
||||
@@ -12,4 +14,12 @@ public interface IRagDocumentService extends IService<RagDocument> {
|
||||
List<RagDocumentResponse> listResponses();
|
||||
|
||||
List<RagDocumentResponse> query(RagDocumentQueryRequest request);
|
||||
|
||||
RagDocumentResponse getResponseById(Long id);
|
||||
|
||||
boolean saveOrUpdate(RagDocumentSaveRequest request);
|
||||
|
||||
boolean removeById(Long id);
|
||||
|
||||
List<RagDocumentResponse> batchUpload(RagDocumentBatchUploadRequest request);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@ package com.bruce.rag.service;
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
||||
import com.bruce.rag.dto.request.RagStoreSaveRequest;
|
||||
import com.bruce.rag.dto.response.RagStoreDocumentOverviewResponse;
|
||||
import com.bruce.rag.dto.response.RagStoreOverviewResponse;
|
||||
import com.bruce.rag.dto.response.RagStoreResponse;
|
||||
import com.bruce.rag.entity.RagStore;
|
||||
|
||||
@@ -16,5 +18,9 @@ public interface IRagStoreService extends IService<RagStore> {
|
||||
|
||||
RagStoreResponse getResponseById(Long id);
|
||||
|
||||
RagStoreOverviewResponse getOverview();
|
||||
|
||||
RagStoreDocumentOverviewResponse getDocumentOverview(Long storeId);
|
||||
|
||||
boolean saveOrUpdate(RagStoreSaveRequest request);
|
||||
}
|
||||
|
||||
@@ -1,36 +1,190 @@
|
||||
package com.bruce.rag.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bruce.common.domain.entity.SysAttachment;
|
||||
import com.bruce.common.dto.request.SysAttachmentUploadRequest;
|
||||
import com.bruce.common.service.ISysAttachmentService;
|
||||
import com.bruce.rag.constant.RagSystemConstants;
|
||||
import com.bruce.rag.dto.request.RagDocumentBatchUploadRequest;
|
||||
import com.bruce.rag.dto.request.RagDocumentQueryRequest;
|
||||
import com.bruce.rag.dto.request.RagDocumentSaveRequest;
|
||||
import com.bruce.rag.dto.response.RagDocumentResponse;
|
||||
import com.bruce.rag.entity.RagDocument;
|
||||
import com.bruce.rag.enums.RagIndexStatusEnum;
|
||||
import com.bruce.rag.enums.RagParseStatusEnum;
|
||||
import com.bruce.rag.mapper.RagDocumentMapper;
|
||||
import com.bruce.rag.service.IRagDocumentService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class RagDocumentServiceImpl extends ServiceImpl<RagDocumentMapper, RagDocument> implements IRagDocumentService {
|
||||
|
||||
@Autowired
|
||||
private ISysAttachmentService sysAttachmentService;
|
||||
|
||||
@Override
|
||||
public List<RagDocumentResponse> listResponses() {
|
||||
return toResponses(list());
|
||||
log.info("RagDocumentServiceImpl.listResponses start");
|
||||
List<RagDocumentResponse> responses = toResponses(list());
|
||||
log.info("RagDocumentServiceImpl.listResponses success, count={}", responses.size());
|
||||
return responses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RagDocumentResponse> query(RagDocumentQueryRequest request) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("查询请求不能为空");
|
||||
}
|
||||
return toResponses(lambdaQuery()
|
||||
.eq(request.getStoreId() != null, RagDocument::getStoreId, request.getStoreId())
|
||||
.eq(request.getAttachmentId() != null, RagDocument::getAttachmentId, request.getAttachmentId())
|
||||
.eq(request.getParseStatus() != null, RagDocument::getParseStatus, request.getParseStatus())
|
||||
.eq(request.getIndexStatus() != null, RagDocument::getIndexStatus, request.getIndexStatus())
|
||||
.eq(request.getEnabled() != null, RagDocument::getEnabled, request.getEnabled())
|
||||
log.info("RagDocumentServiceImpl.query start, request={}", request);
|
||||
RagDocumentQueryRequest queryRequest = request == null ? new RagDocumentQueryRequest() : request;
|
||||
String parseStatus = trimToNull(queryRequest.getParseStatus());
|
||||
String indexStatus = trimToNull(queryRequest.getIndexStatus());
|
||||
List<RagDocumentResponse> responses = toResponses(lambdaQuery()
|
||||
.eq(queryRequest.getStoreId() != null, RagDocument::getStoreId, queryRequest.getStoreId())
|
||||
.eq(queryRequest.getAttachmentId() != null, RagDocument::getAttachmentId, queryRequest.getAttachmentId())
|
||||
.eq(parseStatus != null, RagDocument::getParseStatus, parseStatus)
|
||||
.eq(indexStatus != null, RagDocument::getIndexStatus, indexStatus)
|
||||
.eq(queryRequest.getEnabled() != null, RagDocument::getEnabled, queryRequest.getEnabled())
|
||||
.orderByDesc(RagDocument::getId)
|
||||
.list());
|
||||
log.info("RagDocumentServiceImpl.query success, count={}", responses.size());
|
||||
return responses;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RagDocumentResponse getResponseById(Long id) {
|
||||
log.info("RagDocumentServiceImpl.getResponseById start, id={}", id);
|
||||
RagDocumentResponse response = RagDocumentResponse.fromEntity(getById(id));
|
||||
log.info("RagDocumentServiceImpl.getResponseById success, id={}, found={}", id, response != null);
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean saveOrUpdate(RagDocumentSaveRequest request) {
|
||||
log.info("RagDocumentServiceImpl.saveOrUpdate start, request={}", request);
|
||||
validateSaveRequest(request);
|
||||
|
||||
RagDocument document;
|
||||
if (request.getId() != null) {
|
||||
document = getById(request.getId());
|
||||
if (document == null) {
|
||||
log.warn("RagDocumentServiceImpl.saveOrUpdate document not found, id={}", request.getId());
|
||||
throw new IllegalArgumentException("文档不存在,ID: " + request.getId());
|
||||
}
|
||||
} else {
|
||||
document = new RagDocument();
|
||||
document.setEnabled(true);
|
||||
document.setParseStatus(RagParseStatusEnum.UPLOADED.name());
|
||||
document.setIndexStatus(RagIndexStatusEnum.PENDING.name());
|
||||
}
|
||||
|
||||
document.setStoreId(request.getStoreId());
|
||||
document.setAttachmentId(request.getAttachmentId());
|
||||
document.setDocumentTitle(request.getDocumentTitle().trim());
|
||||
document.setDocumentSummary(trimToNull(request.getDocumentSummary()));
|
||||
if (StringUtils.hasText(request.getParseStatus())) {
|
||||
document.setParseStatus(request.getParseStatus().trim());
|
||||
}
|
||||
if (StringUtils.hasText(request.getIndexStatus())) {
|
||||
document.setIndexStatus(request.getIndexStatus().trim());
|
||||
}
|
||||
if (request.getEnabled() != null) {
|
||||
document.setEnabled(request.getEnabled());
|
||||
}
|
||||
document.setErrorMessage(trimToNull(request.getErrorMessage()));
|
||||
document.setRemark(trimToNull(request.getRemark()));
|
||||
|
||||
boolean result = saveOrUpdate(document);
|
||||
log.info("RagDocumentServiceImpl.saveOrUpdate success, requestId={}, savedId={}, result={}",
|
||||
request.getId(), document.getId(), result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean removeById(Long id) {
|
||||
log.info("RagDocumentServiceImpl.removeById start, id={}", id);
|
||||
if (id == null) {
|
||||
throw new IllegalArgumentException("文档ID不能为空");
|
||||
}
|
||||
RagDocument document = getById(id);
|
||||
if (document == null) {
|
||||
log.warn("RagDocumentServiceImpl.removeById document not found, id={}", id);
|
||||
throw new IllegalArgumentException("文档不存在,ID: " + id);
|
||||
}
|
||||
boolean result = super.removeById(id);
|
||||
log.info("RagDocumentServiceImpl.removeById success, id={}, result={}", id, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<RagDocumentResponse> batchUpload(RagDocumentBatchUploadRequest request) {
|
||||
log.info("RagDocumentServiceImpl.batchUpload start, request={}", request);
|
||||
validateBatchUploadRequest(request);
|
||||
|
||||
List<RagDocumentResponse> results = new ArrayList<>();
|
||||
|
||||
for (var file : request.getFiles()) {
|
||||
if (file == null || file.isEmpty()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
SysAttachmentUploadRequest uploadRequest = new SysAttachmentUploadRequest();
|
||||
uploadRequest.setFile(file);
|
||||
uploadRequest.setSourceType(resolveSourceType(request.getSourceType()));
|
||||
uploadRequest.setSourceId(request.getStoreId());
|
||||
|
||||
SysAttachment attachment = sysAttachmentService.upload(uploadRequest);
|
||||
|
||||
RagDocument document = new RagDocument();
|
||||
document.setStoreId(request.getStoreId());
|
||||
document.setAttachmentId(attachment.getId());
|
||||
document.setDocumentTitle(StringUtils.hasText(file.getOriginalFilename()) ? file.getOriginalFilename().trim() : null);
|
||||
document.setDocumentSummary(trimToNull(request.getDocumentSummary()));
|
||||
document.setParseStatus(RagParseStatusEnum.UPLOADED.name());
|
||||
document.setIndexStatus(RagIndexStatusEnum.PENDING.name());
|
||||
document.setEnabled(true);
|
||||
document.setErrorMessage(null);
|
||||
document.setRemark(trimToNull(request.getRemark()));
|
||||
|
||||
save(document);
|
||||
results.add(RagDocumentResponse.fromEntity(document));
|
||||
}
|
||||
|
||||
log.info("RagDocumentServiceImpl.batchUpload success, storeId={}, uploaded={}",
|
||||
request.getStoreId(), results.size());
|
||||
return results;
|
||||
}
|
||||
|
||||
void validateSaveRequest(RagDocumentSaveRequest request) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("保存请求不能为空");
|
||||
}
|
||||
if (request.getStoreId() == null) {
|
||||
throw new IllegalArgumentException("知识库ID不能为空");
|
||||
}
|
||||
if (!StringUtils.hasText(request.getDocumentTitle())) {
|
||||
throw new IllegalArgumentException("文档标题不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
void validateBatchUploadRequest(RagDocumentBatchUploadRequest request) {
|
||||
if (request == null) {
|
||||
throw new IllegalArgumentException("上传请求不能为空");
|
||||
}
|
||||
if (request.getStoreId() == null) {
|
||||
throw new IllegalArgumentException("知识库ID不能为空");
|
||||
}
|
||||
if (StringUtils.hasText(request.getSourceType())
|
||||
&& !RagSystemConstants.SOURCE_TYPE_RAG.equals(request.getSourceType().trim())) {
|
||||
throw new IllegalArgumentException("sourceType必须为RAG");
|
||||
}
|
||||
if (request.getFiles() == null || request.getFiles().length == 0) {
|
||||
throw new IllegalArgumentException("上传文件不能为空");
|
||||
}
|
||||
}
|
||||
|
||||
private List<RagDocumentResponse> toResponses(List<RagDocument> documents) {
|
||||
@@ -38,4 +192,18 @@ public class RagDocumentServiceImpl extends ServiceImpl<RagDocumentMapper, RagDo
|
||||
.map(RagDocumentResponse::fromEntity)
|
||||
.toList();
|
||||
}
|
||||
|
||||
private String resolveSourceType(String sourceType) {
|
||||
if (!StringUtils.hasText(sourceType)) {
|
||||
return RagSystemConstants.SOURCE_TYPE_RAG;
|
||||
}
|
||||
return RagSystemConstants.SOURCE_TYPE_RAG;
|
||||
}
|
||||
|
||||
private String trimToNull(String value) {
|
||||
if (!StringUtils.hasText(value)) {
|
||||
return null;
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,36 @@
|
||||
package com.bruce.rag.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import com.bruce.common.enums.EnableStatusEnum;
|
||||
import com.bruce.rag.dto.request.RagDocumentQueryRequest;
|
||||
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
||||
import com.bruce.rag.dto.request.RagStoreSaveRequest;
|
||||
import com.bruce.rag.dto.response.RagDocumentResponse;
|
||||
import com.bruce.rag.dto.response.RagStoreDocumentOverviewResponse;
|
||||
import com.bruce.rag.dto.response.RagStoreOverviewResponse;
|
||||
import com.bruce.rag.dto.response.RagStoreResponse;
|
||||
import com.bruce.rag.entity.RagStore;
|
||||
import com.bruce.rag.enums.RagIndexStatusEnum;
|
||||
import com.bruce.rag.enums.RagParseStatusEnum;
|
||||
import com.bruce.rag.mapper.RagStoreMapper;
|
||||
import com.bruce.rag.service.IRagDocumentService;
|
||||
import com.bruce.rag.service.IRagStoreService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> implements IRagStoreService {
|
||||
|
||||
@Autowired
|
||||
private IRagDocumentService ragDocumentService;
|
||||
|
||||
@Override
|
||||
public List<RagStoreResponse> listResponses() {
|
||||
log.info("RagStoreServiceImpl.listResponses start");
|
||||
@@ -47,6 +61,62 @@ public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> i
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RagStoreOverviewResponse getOverview() {
|
||||
log.info("RagStoreServiceImpl.getOverview start");
|
||||
List<RagStore> stores = list();
|
||||
List<RagDocumentResponse> documents = ragDocumentService.listResponses();
|
||||
|
||||
RagStoreOverviewResponse response = new RagStoreOverviewResponse();
|
||||
response.setTotalStores(stores.size());
|
||||
response.setTotalDocuments(documents.size());
|
||||
response.setTotalChunks(null);
|
||||
response.setRetrievableStores((int) stores.stream()
|
||||
.filter(store -> EnableStatusEnum.ENABLED.getLabel().equals(store.getStatus()))
|
||||
.count());
|
||||
log.info("RagStoreServiceImpl.getOverview success, totalStores={}, totalDocuments={}, retrievableStores={}",
|
||||
response.getTotalStores(), response.getTotalDocuments(), response.getRetrievableStores());
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public RagStoreDocumentOverviewResponse getDocumentOverview(Long storeId) {
|
||||
log.info("RagStoreServiceImpl.getDocumentOverview start, storeId={}", storeId);
|
||||
if (storeId == null) {
|
||||
throw new IllegalArgumentException("知识库ID不能为空");
|
||||
}
|
||||
RagStore store = getById(storeId);
|
||||
if (store == null) {
|
||||
throw new IllegalArgumentException("知识库不存在,ID: " + storeId);
|
||||
}
|
||||
|
||||
RagDocumentQueryRequest request = new RagDocumentQueryRequest();
|
||||
request.setStoreId(storeId);
|
||||
List<RagDocumentResponse> documents = ragDocumentService.query(request);
|
||||
|
||||
RagStoreDocumentOverviewResponse response = new RagStoreDocumentOverviewResponse();
|
||||
response.setStoreId(storeId);
|
||||
response.setStoreName(store.getStoreName());
|
||||
response.setDocumentCount(documents.size());
|
||||
response.setEnabledDocumentCount((int) documents.stream()
|
||||
.filter(document -> Boolean.TRUE.equals(document.getEnabled()))
|
||||
.count());
|
||||
response.setParsedDocumentCount((int) documents.stream()
|
||||
.filter(document -> RagParseStatusEnum.PARSED.name().equals(document.getParseStatus()))
|
||||
.count());
|
||||
response.setIndexedDocumentCount((int) documents.stream()
|
||||
.filter(document -> RagIndexStatusEnum.INDEXED.name().equals(document.getIndexStatus()))
|
||||
.count());
|
||||
response.setLastUploadTime(documents.stream()
|
||||
.map(RagDocumentResponse::getCreateTime)
|
||||
.filter(Objects::nonNull)
|
||||
.max(Comparator.naturalOrder())
|
||||
.orElse(null));
|
||||
log.info("RagStoreServiceImpl.getDocumentOverview success, storeId={}, documentCount={}",
|
||||
storeId, response.getDocumentCount());
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean saveOrUpdate(RagStoreSaveRequest request) {
|
||||
log.info("RagStoreServiceImpl.saveOrUpdate start, request={}", request);
|
||||
@@ -62,13 +132,7 @@ public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> i
|
||||
throw new IllegalArgumentException("知识库编码已存在: " + request.getStoreCode().trim());
|
||||
}
|
||||
|
||||
RagStore ragStore = new RagStore();
|
||||
ragStore.setId(request.getId());
|
||||
ragStore.setStoreCode(request.getStoreCode().trim());
|
||||
ragStore.setStoreName(request.getStoreName().trim());
|
||||
ragStore.setDescription(trimToNull(request.getDescription()));
|
||||
ragStore.setStatus(StringUtils.hasText(request.getStatus()) ? request.getStatus().trim() : "启用");
|
||||
ragStore.setRemark(trimToNull(request.getRemark()));
|
||||
RagStore ragStore = buildEntity(request);
|
||||
boolean result = super.saveOrUpdate(ragStore);
|
||||
log.info("RagStoreServiceImpl.saveOrUpdate success, requestId={}, savedId={}, storeCode={}, result={}",
|
||||
request.getId(), ragStore.getId(), ragStore.getStoreCode(), result);
|
||||
@@ -90,6 +154,19 @@ public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> i
|
||||
request.getId(), request.getStoreCode(), request.getStoreName());
|
||||
}
|
||||
|
||||
public RagStore buildEntity(RagStoreSaveRequest request) {
|
||||
RagStore ragStore = new RagStore();
|
||||
ragStore.setId(request.getId());
|
||||
ragStore.setStoreCode(request.getStoreCode().trim());
|
||||
ragStore.setStoreName(request.getStoreName().trim());
|
||||
ragStore.setDescription(trimToNull(request.getDescription()));
|
||||
ragStore.setStatus(StringUtils.hasText(request.getStatus())
|
||||
? request.getStatus().trim()
|
||||
: EnableStatusEnum.ENABLED.getLabel());
|
||||
ragStore.setRemark(trimToNull(request.getRemark()));
|
||||
return ragStore;
|
||||
}
|
||||
|
||||
private List<RagStoreResponse> toResponses(List<RagStore> stores) {
|
||||
return stores.stream()
|
||||
.map(RagStoreResponse::fromEntity)
|
||||
|
||||
@@ -7,3 +7,7 @@ spring:
|
||||
common:
|
||||
attachment:
|
||||
base-path: data/attachments
|
||||
|
||||
logging:
|
||||
pattern:
|
||||
console: "%d{yyyy-MM-dd HH:mm:ss.SSS, GMT+8} %5p ${PID:- } --- [%15.15t] %-40.40logger{39} : %m%n%wEx"
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.bruce.common.constant;
|
||||
|
||||
import com.bruce.common.config.AttachmentProperties;
|
||||
import com.bruce.common.domain.model.RequestResult;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
class CommonConstsStructureTests {
|
||||
|
||||
@Test
|
||||
void commonConstsShouldExposeReusableInfrastructureConstants() {
|
||||
assertEquals("yyyy-MM-dd HH:mm:ss", CommonConsts.DATE_FORMAT_LONG_STR);
|
||||
assertEquals("yyyy-MM-dd HH:mm:ss.SSS", CommonConsts.DATE_FORMAT_MILLIS_STR);
|
||||
assertEquals("GMT+8", CommonConsts.TIME_ZONE_GMT8);
|
||||
assertEquals("data/attachments", CommonConsts.DEFAULT_ATTACHMENT_BASE_PATH);
|
||||
assertEquals("LOCAL", CommonConsts.STORAGE_TYPE_LOCAL);
|
||||
assertEquals("0", CommonConsts.REQUEST_RESULT_SUCCESS_CODE);
|
||||
assertEquals("-1", CommonConsts.REQUEST_RESULT_FAIL_CODE);
|
||||
}
|
||||
|
||||
@Test
|
||||
void sharedDefaultConstantsShouldBeReusedByCommonComponents() {
|
||||
AttachmentProperties properties = new AttachmentProperties();
|
||||
|
||||
assertEquals(CommonConsts.DEFAULT_ATTACHMENT_BASE_PATH, properties.getBasePath());
|
||||
assertEquals(CommonConsts.REQUEST_RESULT_SUCCESS_CODE, RequestResult.SUCCESS_CODE);
|
||||
assertEquals(CommonConsts.REQUEST_RESULT_FAIL_CODE, RequestResult.FAIL_CODE);
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,8 @@ import com.bruce.rag.controller.RagStoreController;
|
||||
import com.bruce.rag.dto.request.RagDocumentQueryRequest;
|
||||
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
||||
import com.bruce.rag.dto.request.RagStoreSaveRequest;
|
||||
import com.bruce.rag.dto.response.RagStoreDocumentOverviewResponse;
|
||||
import com.bruce.rag.dto.response.RagStoreOverviewResponse;
|
||||
import com.bruce.rag.dto.response.RagDocumentResponse;
|
||||
import com.bruce.rag.dto.response.RagStoreResponse;
|
||||
import com.bruce.rag.entity.RagDocument;
|
||||
@@ -21,12 +23,14 @@ import com.bruce.rag.service.IRagStoreService;
|
||||
import com.bruce.rag.service.impl.RagDocumentServiceImpl;
|
||||
import com.bruce.rag.service.impl.RagStoreServiceImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class RagComponentStructureTests {
|
||||
@@ -46,11 +50,15 @@ class RagComponentStructureTests {
|
||||
Method storeListMethod = RagStoreController.class.getMethod("list");
|
||||
Method storeQueryMethod = RagStoreController.class.getMethod("query", RagStoreQueryRequest.class);
|
||||
Method storeDetailMethod = RagStoreController.class.getMethod("getById", Long.class);
|
||||
Method storeOverviewMethod = RagStoreController.class.getMethod("overview");
|
||||
Method storeDocumentOverviewMethod = RagStoreController.class.getMethod("documentOverview", Long.class);
|
||||
Method storeSaveMethod = RagStoreController.class.getMethod("saveOrUpdate", RagStoreSaveRequest.class);
|
||||
Method storeDeleteMethod = RagStoreController.class.getMethod("deleteById", Long.class);
|
||||
Method storeResponseListMethod = IRagStoreService.class.getMethod("listResponses");
|
||||
Method storeServiceQueryMethod = IRagStoreService.class.getMethod("query", RagStoreQueryRequest.class);
|
||||
Method storeServiceDetailMethod = IRagStoreService.class.getMethod("getResponseById", Long.class);
|
||||
Method storeServiceOverviewMethod = IRagStoreService.class.getMethod("getOverview");
|
||||
Method storeServiceDocumentOverviewMethod = IRagStoreService.class.getMethod("getDocumentOverview", Long.class);
|
||||
Method storeServiceSaveMethod = IRagStoreService.class.getMethod("saveOrUpdate", RagStoreSaveRequest.class);
|
||||
|
||||
Method documentListMethod = RagDocumentController.class.getMethod("list");
|
||||
@@ -61,16 +69,22 @@ class RagComponentStructureTests {
|
||||
assertEquals(RequestResult.class, storeListMethod.getReturnType());
|
||||
assertEquals(RequestResult.class, storeQueryMethod.getReturnType());
|
||||
assertEquals(RequestResult.class, storeDetailMethod.getReturnType());
|
||||
assertEquals(RequestResult.class, storeOverviewMethod.getReturnType());
|
||||
assertEquals(RequestResult.class, storeDocumentOverviewMethod.getReturnType());
|
||||
assertEquals(RequestResult.class, storeSaveMethod.getReturnType());
|
||||
assertEquals(RequestResult.class, storeDeleteMethod.getReturnType());
|
||||
assertEquals(List.class, storeServiceQueryMethod.getReturnType());
|
||||
assertEquals(RagStoreResponse.class, storeServiceDetailMethod.getReturnType());
|
||||
assertEquals(RagStoreOverviewResponse.class, storeServiceOverviewMethod.getReturnType());
|
||||
assertEquals(RagStoreDocumentOverviewResponse.class, storeServiceDocumentOverviewMethod.getReturnType());
|
||||
assertEquals(boolean.class, storeServiceSaveMethod.getReturnType());
|
||||
assertTrue(storeResponseListMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
||||
assertTrue(storeServiceQueryMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
||||
assertTrue(storeListMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
||||
assertTrue(storeQueryMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
||||
assertTrue(storeDetailMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
||||
assertTrue(storeOverviewMethod.getGenericReturnType().getTypeName().contains("RagStoreOverviewResponse"));
|
||||
assertTrue(storeDocumentOverviewMethod.getGenericReturnType().getTypeName().contains("RagStoreDocumentOverviewResponse"));
|
||||
assertEquals(RagStoreResponse.class, RagStoreResponse.class.getMethod("fromEntity", RagStore.class).getReturnType());
|
||||
|
||||
assertEquals(RequestResult.class, documentListMethod.getReturnType());
|
||||
@@ -83,6 +97,16 @@ class RagComponentStructureTests {
|
||||
assertEquals(RagDocumentResponse.class, RagDocumentResponse.class.getMethod("fromEntity", RagDocument.class).getReturnType());
|
||||
}
|
||||
|
||||
@Test
|
||||
void ragDocumentListUrlShouldUseExplicitListAction() throws NoSuchMethodException {
|
||||
Method documentListMethod = RagDocumentController.class.getMethod("list");
|
||||
|
||||
PostMapping postMapping = documentListMethod.getAnnotation(PostMapping.class);
|
||||
|
||||
assertNotNull(postMapping);
|
||||
assertEquals("/list", postMapping.value()[0]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void ragSourceTypesAndDocumentRelationShouldExist() throws NoSuchFieldException {
|
||||
Field storeIdField = RagDocument.class.getDeclaredField("storeId");
|
||||
@@ -90,6 +114,7 @@ class RagComponentStructureTests {
|
||||
|
||||
assertEquals("RAG_STORE", RagSystemConstants.RAG_STORE);
|
||||
assertEquals("RAG_DOCUMENT", RagSystemConstants.RAG_DOCUMENT);
|
||||
assertEquals("RAG", RagSystemConstants.SOURCE_TYPE_RAG);
|
||||
assertEquals(Long.class, storeIdField.getType());
|
||||
assertEquals(Long.class, attachmentIdField.getType());
|
||||
assertTrue(RagStore.class.getSimpleName().contains("RagStore"));
|
||||
|
||||
124
src/test/java/com/bruce/rag/RagDocumentServiceImplTests.java
Normal file
124
src/test/java/com/bruce/rag/RagDocumentServiceImplTests.java
Normal file
@@ -0,0 +1,124 @@
|
||||
package com.bruce.rag;
|
||||
|
||||
import com.bruce.common.domain.entity.SysAttachment;
|
||||
import com.bruce.common.dto.request.SysAttachmentUploadRequest;
|
||||
import com.bruce.common.service.ISysAttachmentService;
|
||||
import com.bruce.rag.constant.RagSystemConstants;
|
||||
import com.bruce.rag.dto.request.RagDocumentBatchUploadRequest;
|
||||
import com.bruce.rag.dto.request.RagDocumentSaveRequest;
|
||||
import com.bruce.rag.entity.RagDocument;
|
||||
import com.bruce.rag.enums.RagIndexStatusEnum;
|
||||
import com.bruce.rag.enums.RagParseStatusEnum;
|
||||
import com.bruce.rag.service.impl.RagDocumentServiceImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.doAnswer;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RagDocumentServiceImplTests {
|
||||
|
||||
@Spy
|
||||
@InjectMocks
|
||||
private RagDocumentServiceImpl ragDocumentService;
|
||||
|
||||
@Mock
|
||||
private ISysAttachmentService sysAttachmentService;
|
||||
|
||||
@Test
|
||||
void batchUploadShouldUseRagSourceTypeAndStoreIdAsSourceId() {
|
||||
MockMultipartFile file = new MockMultipartFile(
|
||||
"files",
|
||||
"knowledge.txt",
|
||||
"text/plain",
|
||||
"hello rag".getBytes()
|
||||
);
|
||||
RagDocumentBatchUploadRequest request = new RagDocumentBatchUploadRequest();
|
||||
request.setStoreId(1001L);
|
||||
request.setSourceType(RagSystemConstants.SOURCE_TYPE_RAG);
|
||||
request.setFiles(new MockMultipartFile[]{file});
|
||||
request.setDocumentSummary("批量摘要");
|
||||
request.setRemark("批量备注");
|
||||
|
||||
SysAttachment attachment = new SysAttachment();
|
||||
attachment.setId(2002L);
|
||||
when(sysAttachmentService.upload(any(SysAttachmentUploadRequest.class))).thenReturn(attachment);
|
||||
doAnswer(invocation -> true).when(ragDocumentService).save(any(RagDocument.class));
|
||||
|
||||
var responses = ragDocumentService.batchUpload(request);
|
||||
|
||||
ArgumentCaptor<SysAttachmentUploadRequest> uploadCaptor = ArgumentCaptor.forClass(SysAttachmentUploadRequest.class);
|
||||
verify(sysAttachmentService).upload(uploadCaptor.capture());
|
||||
SysAttachmentUploadRequest uploadRequest = uploadCaptor.getValue();
|
||||
assertEquals(RagSystemConstants.SOURCE_TYPE_RAG, uploadRequest.getSourceType());
|
||||
assertEquals(1001L, uploadRequest.getSourceId());
|
||||
assertEquals(file, uploadRequest.getFile());
|
||||
|
||||
ArgumentCaptor<RagDocument> documentCaptor = ArgumentCaptor.forClass(RagDocument.class);
|
||||
verify(ragDocumentService).save(documentCaptor.capture());
|
||||
RagDocument savedDocument = documentCaptor.getValue();
|
||||
assertEquals(1001L, savedDocument.getStoreId());
|
||||
assertEquals(2002L, savedDocument.getAttachmentId());
|
||||
assertEquals("knowledge.txt", savedDocument.getDocumentTitle());
|
||||
assertEquals("批量摘要", savedDocument.getDocumentSummary());
|
||||
assertEquals(RagParseStatusEnum.UPLOADED.name(), savedDocument.getParseStatus());
|
||||
assertEquals(RagIndexStatusEnum.PENDING.name(), savedDocument.getIndexStatus());
|
||||
assertTrue(savedDocument.getEnabled());
|
||||
assertNull(savedDocument.getErrorMessage());
|
||||
assertEquals("批量备注", savedDocument.getRemark());
|
||||
assertEquals(1, responses.size());
|
||||
assertEquals(RagParseStatusEnum.UPLOADED.name(), responses.getFirst().getParseStatus());
|
||||
assertEquals(RagIndexStatusEnum.PENDING.name(), responses.getFirst().getIndexStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveOrUpdateShouldWriteAllEditableFields() {
|
||||
RagDocument existingDocument = new RagDocument();
|
||||
existingDocument.setId(3003L);
|
||||
|
||||
RagDocumentSaveRequest request = new RagDocumentSaveRequest();
|
||||
request.setId(3003L);
|
||||
request.setStoreId(1001L);
|
||||
request.setAttachmentId(2002L);
|
||||
request.setDocumentTitle(" 新标题 ");
|
||||
request.setDocumentSummary(" 新摘要 ");
|
||||
request.setParseStatus(RagParseStatusEnum.PARSED.name());
|
||||
request.setIndexStatus(RagIndexStatusEnum.INDEXED.name());
|
||||
request.setEnabled(false);
|
||||
request.setErrorMessage(" 已修复 ");
|
||||
request.setRemark(" 备注信息 ");
|
||||
|
||||
doReturn(existingDocument).when(ragDocumentService).getById(3003L);
|
||||
doReturn(true).when(ragDocumentService).saveOrUpdate(any(RagDocument.class));
|
||||
|
||||
boolean result = ragDocumentService.saveOrUpdate(request);
|
||||
|
||||
assertTrue(result);
|
||||
ArgumentCaptor<RagDocument> documentCaptor = ArgumentCaptor.forClass(RagDocument.class);
|
||||
verify(ragDocumentService).saveOrUpdate(documentCaptor.capture());
|
||||
RagDocument savedDocument = documentCaptor.getValue();
|
||||
assertEquals(3003L, savedDocument.getId());
|
||||
assertEquals(1001L, savedDocument.getStoreId());
|
||||
assertEquals(2002L, savedDocument.getAttachmentId());
|
||||
assertEquals("新标题", savedDocument.getDocumentTitle());
|
||||
assertEquals("新摘要", savedDocument.getDocumentSummary());
|
||||
assertEquals(RagParseStatusEnum.PARSED.name(), savedDocument.getParseStatus());
|
||||
assertEquals(RagIndexStatusEnum.INDEXED.name(), savedDocument.getIndexStatus());
|
||||
assertEquals(false, savedDocument.getEnabled());
|
||||
assertEquals("已修复", savedDocument.getErrorMessage());
|
||||
assertEquals("备注信息", savedDocument.getRemark());
|
||||
}
|
||||
}
|
||||
126
src/test/java/com/bruce/rag/RagStoreOverviewServiceTests.java
Normal file
126
src/test/java/com/bruce/rag/RagStoreOverviewServiceTests.java
Normal file
@@ -0,0 +1,126 @@
|
||||
package com.bruce.rag;
|
||||
|
||||
import com.bruce.common.enums.EnableStatusEnum;
|
||||
import com.bruce.rag.dto.response.RagDocumentResponse;
|
||||
import com.bruce.rag.dto.response.RagStoreDocumentOverviewResponse;
|
||||
import com.bruce.rag.dto.response.RagStoreOverviewResponse;
|
||||
import com.bruce.rag.entity.RagStore;
|
||||
import com.bruce.rag.enums.RagIndexStatusEnum;
|
||||
import com.bruce.rag.enums.RagParseStatusEnum;
|
||||
import com.bruce.rag.service.IRagDocumentService;
|
||||
import com.bruce.rag.service.impl.RagStoreServiceImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Spy;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.mockito.Mockito.doReturn;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
class RagStoreOverviewServiceTests {
|
||||
|
||||
@Spy
|
||||
@InjectMocks
|
||||
private RagStoreServiceImpl ragStoreService;
|
||||
|
||||
@Mock
|
||||
private IRagDocumentService ragDocumentService;
|
||||
|
||||
@Test
|
||||
void getOverviewShouldAggregateStoreAndDocumentCounts() {
|
||||
RagStore enabledStore = new RagStore();
|
||||
enabledStore.setId(1L);
|
||||
enabledStore.setStatus(EnableStatusEnum.ENABLED.getLabel());
|
||||
RagStore disabledStore = new RagStore();
|
||||
disabledStore.setId(2L);
|
||||
disabledStore.setStatus("停用");
|
||||
|
||||
when(ragDocumentService.listResponses()).thenReturn(List.of(
|
||||
createDocumentResponse("11", "1", true, RagParseStatusEnum.UPLOADED.name(), RagIndexStatusEnum.PENDING.name(), new Date()),
|
||||
createDocumentResponse("22", "2", false, RagParseStatusEnum.PARSED.name(), RagIndexStatusEnum.INDEXED.name(), new Date())
|
||||
));
|
||||
doReturn(List.of(enabledStore, disabledStore)).when(ragStoreService).list();
|
||||
|
||||
RagStoreOverviewResponse response = ragStoreService.getOverview();
|
||||
|
||||
assertEquals(2, response.getTotalStores());
|
||||
assertEquals(2, response.getTotalDocuments());
|
||||
assertNull(response.getTotalChunks());
|
||||
assertEquals(1, response.getRetrievableStores());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDocumentOverviewShouldAggregateCurrentStoreDocumentMetrics() {
|
||||
RagStore store = new RagStore();
|
||||
store.setId(1L);
|
||||
store.setStoreName("产品制度库");
|
||||
doReturn(store).when(ragStoreService).getById(1L);
|
||||
when(ragDocumentService.query(org.mockito.ArgumentMatchers.any())).thenReturn(List.of(
|
||||
createDocumentResponse("11", "1", true, RagParseStatusEnum.UPLOADED.name(), RagIndexStatusEnum.PENDING.name(), new Date(1747816496000L)),
|
||||
createDocumentResponse("12", "1", true, RagParseStatusEnum.PARSED.name(), RagIndexStatusEnum.INDEXED.name(), new Date(1747820096000L)),
|
||||
createDocumentResponse("13", "1", false, RagParseStatusEnum.FAILED.name(), RagIndexStatusEnum.FAILED.name(), new Date(1747812896000L))
|
||||
));
|
||||
|
||||
RagStoreDocumentOverviewResponse response = ragStoreService.getDocumentOverview(1L);
|
||||
|
||||
assertEquals(1L, response.getStoreId());
|
||||
assertEquals("产品制度库", response.getStoreName());
|
||||
assertEquals(3, response.getDocumentCount());
|
||||
assertEquals(2, response.getEnabledDocumentCount());
|
||||
assertEquals(1, response.getParsedDocumentCount());
|
||||
assertEquals(1, response.getIndexedDocumentCount());
|
||||
assertEquals(new Date(1747820096000L), response.getLastUploadTime());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDocumentOverviewShouldQueryDocumentsByStoreIdOnly() {
|
||||
RagStore store = new RagStore();
|
||||
store.setId(1L);
|
||||
store.setStoreName("产品制度库");
|
||||
doReturn(store).when(ragStoreService).getById(1L);
|
||||
when(ragDocumentService.query(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
|
||||
|
||||
ragStoreService.getDocumentOverview(1L);
|
||||
|
||||
org.mockito.ArgumentCaptor<com.bruce.rag.dto.request.RagDocumentQueryRequest> captor =
|
||||
org.mockito.ArgumentCaptor.forClass(com.bruce.rag.dto.request.RagDocumentQueryRequest.class);
|
||||
org.mockito.Mockito.verify(ragDocumentService).query(captor.capture());
|
||||
assertEquals(1L, captor.getValue().getStoreId());
|
||||
assertNull(captor.getValue().getParseStatus());
|
||||
assertNull(captor.getValue().getIndexStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
void getDocumentOverviewShouldRejectUnknownStore() {
|
||||
doReturn(null).when(ragStoreService).getById(999L);
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> ragStoreService.getDocumentOverview(999L));
|
||||
}
|
||||
|
||||
private RagDocumentResponse createDocumentResponse(
|
||||
String id,
|
||||
String storeId,
|
||||
boolean enabled,
|
||||
String parseStatus,
|
||||
String indexStatus,
|
||||
Date createTime
|
||||
) {
|
||||
RagDocumentResponse response = new RagDocumentResponse();
|
||||
response.setId(Long.valueOf(id));
|
||||
response.setStoreId(Long.valueOf(storeId));
|
||||
response.setEnabled(enabled);
|
||||
response.setParseStatus(parseStatus);
|
||||
response.setIndexStatus(indexStatus);
|
||||
response.setCreateTime(createTime);
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.bruce.rag;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.bruce.rag.dto.response.RagDocumentResponse;
|
||||
import com.bruce.rag.dto.response.RagStoreResponse;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class RagStoreResponseSerializationTests {
|
||||
@@ -19,4 +22,24 @@ class RagStoreResponseSerializationTests {
|
||||
|
||||
assertTrue(json.contains("\"id\":\"2057302206052372481\""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void responseTimeShouldSerializeWithUnifiedFormat() throws Exception {
|
||||
RagStoreResponse storeResponse = new RagStoreResponse();
|
||||
storeResponse.setCreateTime(new Date(1747816496000L));
|
||||
storeResponse.setUpdateTime(new Date(1747816496000L));
|
||||
|
||||
RagDocumentResponse documentResponse = new RagDocumentResponse();
|
||||
documentResponse.setCreateTime(new Date(1747816496000L));
|
||||
documentResponse.setUpdateTime(new Date(1747816496000L));
|
||||
|
||||
ObjectMapper objectMapper = new ObjectMapper();
|
||||
String storeJson = objectMapper.writeValueAsString(storeResponse);
|
||||
String documentJson = objectMapper.writeValueAsString(documentResponse);
|
||||
|
||||
assertTrue(storeJson.contains("\"createTime\":\"2025-05-21 16:34:56\""));
|
||||
assertTrue(storeJson.contains("\"updateTime\":\"2025-05-21 16:34:56\""));
|
||||
assertTrue(documentJson.contains("\"createTime\":\"2025-05-21 16:34:56\""));
|
||||
assertTrue(documentJson.contains("\"updateTime\":\"2025-05-21 16:34:56\""));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.bruce.rag;
|
||||
|
||||
import com.bruce.common.enums.EnableStatusEnum;
|
||||
import com.bruce.rag.dto.request.RagStoreSaveRequest;
|
||||
import com.bruce.rag.entity.RagStore;
|
||||
import com.bruce.rag.service.impl.RagStoreServiceImpl;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
|
||||
class RagStoreSaveValidationTests {
|
||||
@@ -36,4 +39,16 @@ class RagStoreSaveValidationTests {
|
||||
|
||||
assertDoesNotThrow(() -> service.validateSaveRequest(request));
|
||||
}
|
||||
|
||||
@Test
|
||||
void saveShouldDefaultStatusToEnabledEnumLabel() {
|
||||
RagStoreServiceImpl service = new RagStoreServiceImpl();
|
||||
RagStoreSaveRequest request = new RagStoreSaveRequest();
|
||||
request.setStoreCode("PROD_DOC");
|
||||
request.setStoreName("产品制度库");
|
||||
|
||||
RagStore ragStore = service.buildEntity(request);
|
||||
|
||||
assertEquals(EnableStatusEnum.ENABLED.getLabel(), ragStore.getStatus());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user