feat:新增知识库管理页面并联调知识库接口
This commit is contained in:
44
frontend/src/api/ragStores.ts
Normal file
44
frontend/src/api/ragStores.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { get, post } from './request';
|
||||||
|
|
||||||
|
export interface RagStore {
|
||||||
|
id?: string;
|
||||||
|
storeCode: string;
|
||||||
|
storeName: string;
|
||||||
|
description?: string | null;
|
||||||
|
status?: string | null;
|
||||||
|
remark?: string | null;
|
||||||
|
createTime?: string | null;
|
||||||
|
updateTime?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RagStoreQueryRequest {
|
||||||
|
storeCode?: string;
|
||||||
|
storeName?: string;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RagStoreSaveRequest = RagStore;
|
||||||
|
|
||||||
|
export function listRagStores() {
|
||||||
|
return post<RagStore[]>('/rag/store/list');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryRagStores(query?: RagStoreQueryRequest) {
|
||||||
|
return post<RagStore[], RagStoreQueryRequest | undefined>('/rag/store/query', query);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRagStoreById(id: string) {
|
||||||
|
return get<RagStore>('/rag/store/detail', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveRagStore(data: RagStoreSaveRequest) {
|
||||||
|
return post<boolean, RagStoreSaveRequest>('/rag/store/save', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteRagStore(id: string) {
|
||||||
|
return post<boolean>('/rag/store/delete', undefined, {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,8 +1,668 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { CirclePlus, Delete, Edit, FolderAdd, Refresh, Search, UploadFilled } from '@element-plus/icons-vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
deleteRagStore,
|
||||||
|
getRagStoreById,
|
||||||
|
queryRagStores,
|
||||||
|
saveRagStore,
|
||||||
|
type RagStore,
|
||||||
|
} from '@/api/ragStores';
|
||||||
|
|
||||||
|
type StoreStatus = '启用' | '停用';
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const detailLoading = ref(false);
|
||||||
|
const submitting = ref(false);
|
||||||
|
const storeRows = ref<RagStore[]>([]);
|
||||||
|
const activeStoreId = ref<string | null>(null);
|
||||||
|
const activeStore = ref<RagStore | null>(null);
|
||||||
|
|
||||||
|
const queryForm = reactive({
|
||||||
|
storeName: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const createDialogVisible = ref(false);
|
||||||
|
const editDialogVisible = ref(false);
|
||||||
|
|
||||||
|
const createForm = reactive({
|
||||||
|
storeCode: '',
|
||||||
|
storeName: '',
|
||||||
|
description: '',
|
||||||
|
status: '启用' as StoreStatus,
|
||||||
|
remark: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const editForm = reactive({
|
||||||
|
id: '',
|
||||||
|
storeCode: '',
|
||||||
|
storeName: '',
|
||||||
|
description: '',
|
||||||
|
status: '启用' as StoreStatus,
|
||||||
|
remark: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const overviewCards = computed(() => {
|
||||||
|
const totalStores = storeRows.value.length;
|
||||||
|
const retrievableStores = storeRows.value.filter((row) => row.status === '启用').length;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ label: '知识库总数', value: totalStores, hint: '当前已登记知识库' },
|
||||||
|
{ label: '文档总数', value: '-', hint: '待文档统计接口补充' },
|
||||||
|
{ label: '切片总数', value: '-', hint: '待切片统计接口补充' },
|
||||||
|
{ label: '可检索知识库数', value: retrievableStores, hint: '当前按启用状态暂代统计' },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadStores(preferredStoreId?: string | null) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await queryRagStores({
|
||||||
|
storeName: queryForm.storeName.trim() || undefined,
|
||||||
|
});
|
||||||
|
storeRows.value = response.data ?? [];
|
||||||
|
|
||||||
|
if (storeRows.value.length === 0) {
|
||||||
|
activeStoreId.value = null;
|
||||||
|
activeStore.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstStore = storeRows.value[0];
|
||||||
|
const targetId =
|
||||||
|
preferredStoreId && storeRows.value.some((row) => String(row.id) === preferredStoreId)
|
||||||
|
? preferredStoreId
|
||||||
|
: firstStore
|
||||||
|
? String(firstStore.id)
|
||||||
|
: null;
|
||||||
|
if (!targetId) {
|
||||||
|
activeStoreId.value = null;
|
||||||
|
activeStore.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await selectStore(targetId);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectStore(storeId: string) {
|
||||||
|
activeStoreId.value = storeId;
|
||||||
|
detailLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await getRagStoreById(storeId);
|
||||||
|
activeStore.value = response.data ?? null;
|
||||||
|
} finally {
|
||||||
|
detailLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
loadStores(activeStoreId.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
queryForm.storeName = '';
|
||||||
|
loadStores();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateDialog() {
|
||||||
|
createForm.storeCode = '';
|
||||||
|
createForm.storeName = '';
|
||||||
|
createForm.description = '';
|
||||||
|
createForm.status = '启用';
|
||||||
|
createForm.remark = '';
|
||||||
|
createDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditDialog() {
|
||||||
|
if (!activeStore.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editForm.id = String(activeStore.value.id ?? '');
|
||||||
|
editForm.storeCode = activeStore.value.storeCode;
|
||||||
|
editForm.storeName = activeStore.value.storeName;
|
||||||
|
editForm.description = activeStore.value.description ?? '';
|
||||||
|
editForm.status = (activeStore.value.status as StoreStatus) || '启用';
|
||||||
|
editForm.remark = activeStore.value.remark ?? '';
|
||||||
|
editDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitCreateStore() {
|
||||||
|
if (!createForm.storeCode || !createForm.storeName) {
|
||||||
|
ElMessage.warning('请填写知识库编码和知识库名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
await saveRagStore({
|
||||||
|
storeCode: createForm.storeCode,
|
||||||
|
storeName: createForm.storeName,
|
||||||
|
description: createForm.description,
|
||||||
|
status: createForm.status,
|
||||||
|
remark: createForm.remark,
|
||||||
|
});
|
||||||
|
createDialogVisible.value = false;
|
||||||
|
ElMessage.success('知识库已创建');
|
||||||
|
await loadStores();
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitEditStore() {
|
||||||
|
if (!editForm.id || !editForm.storeCode || !editForm.storeName) {
|
||||||
|
ElMessage.warning('请填写知识库编码和知识库名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
await saveRagStore({
|
||||||
|
id: editForm.id,
|
||||||
|
storeCode: editForm.storeCode,
|
||||||
|
storeName: editForm.storeName,
|
||||||
|
description: editForm.description,
|
||||||
|
status: editForm.status,
|
||||||
|
remark: editForm.remark,
|
||||||
|
});
|
||||||
|
editDialogVisible.value = false;
|
||||||
|
ElMessage.success('知识库信息已更新');
|
||||||
|
await loadStores(editForm.id);
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeStore() {
|
||||||
|
if (!activeStore.value?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ElMessageBox.confirm(`确认删除知识库「${activeStore.value.storeName}」?`, '删除确认', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '删除',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
});
|
||||||
|
|
||||||
|
await deleteRagStore(String(activeStore.value.id));
|
||||||
|
ElMessage.success('知识库已删除');
|
||||||
|
await loadStores();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFutureMessage(actionName: string) {
|
||||||
|
ElMessage.info(`${actionName} 会在下一批接口里补齐`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusTagType(status?: string | null) {
|
||||||
|
return status === '启用' ? 'success' : 'info';
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadStores();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page-panel">
|
<section class="page-panel rag-store-page">
|
||||||
<div class="page-panel__header">
|
<div class="page-panel__header rag-store-page__header">
|
||||||
<h2>知识库</h2>
|
<div>
|
||||||
<span>RAG</span>
|
<h2>知识库</h2>
|
||||||
|
<p>统一管理知识库及其文档、索引状态</p>
|
||||||
|
</div>
|
||||||
|
<span>RAG Stores</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="overview-grid">
|
||||||
|
<article v-for="card in overviewCards" :key="card.label" class="overview-card">
|
||||||
|
<span class="overview-card__label">{{ card.label }}</span>
|
||||||
|
<strong class="overview-card__value">{{ card.value }}</strong>
|
||||||
|
<small class="overview-card__hint">{{ card.hint }}</small>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rag-store-page__content">
|
||||||
|
<section class="store-list-panel">
|
||||||
|
<div class="section-heading">
|
||||||
|
<div>
|
||||||
|
<h3>知识库列表</h3>
|
||||||
|
<p>按知识库名称检索并切换当前查看对象</p>
|
||||||
|
</div>
|
||||||
|
<el-button data-test="create-store" type="primary" :icon="CirclePlus" @click="openCreateDialog">
|
||||||
|
新建知识库
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="store-search-bar">
|
||||||
|
<el-input
|
||||||
|
v-model="queryForm.storeName"
|
||||||
|
data-test="store-name-input"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入知识库名称"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
<el-button data-test="store-search" type="primary" :icon="Search" @click="handleSearch">
|
||||||
|
查询
|
||||||
|
</el-button>
|
||||||
|
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-loading="loading" class="store-list">
|
||||||
|
<button
|
||||||
|
v-for="store in storeRows"
|
||||||
|
:key="store.id"
|
||||||
|
:data-test="`store-card-${String(store.storeCode).toLowerCase()}`"
|
||||||
|
class="store-card"
|
||||||
|
:class="{ 'store-card--active': String(store.id) === activeStoreId }"
|
||||||
|
type="button"
|
||||||
|
@click="selectStore(String(store.id))"
|
||||||
|
>
|
||||||
|
<div class="store-card__title-row">
|
||||||
|
<strong>{{ store.storeName }}</strong>
|
||||||
|
<el-tag size="small" :type="getStatusTagType(store.status)">{{ store.status || '未设置' }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<p>编码:{{ store.storeCode }}</p>
|
||||||
|
<div class="store-card__metrics">
|
||||||
|
<span>描述:{{ store.description || '暂无描述' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="store-card__metrics">
|
||||||
|
<span>更新时间:{{ store.updateTime || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<el-empty v-if="!loading && storeRows.length === 0" description="未找到匹配的知识库" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="store-detail-panel">
|
||||||
|
<div v-loading="detailLoading" class="store-detail-panel__body">
|
||||||
|
<template v-if="activeStore">
|
||||||
|
<div class="section-heading section-heading--detail">
|
||||||
|
<div>
|
||||||
|
<h3>{{ activeStore.storeName }}</h3>
|
||||||
|
<p>编码:{{ activeStore.storeCode }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="detail-actions">
|
||||||
|
<el-button :icon="Edit" @click="openEditDialog">编辑</el-button>
|
||||||
|
<el-button type="primary" :icon="UploadFilled" @click="showFutureMessage('批量导入文件')">
|
||||||
|
批量导入文件
|
||||||
|
</el-button>
|
||||||
|
<el-button :icon="FolderAdd" @click="showFutureMessage('重建索引')">重建索引</el-button>
|
||||||
|
<el-button type="danger" :icon="Delete" @click="removeStore">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-grid">
|
||||||
|
<article class="detail-card">
|
||||||
|
<div class="detail-card__header">
|
||||||
|
<h4>基本信息</h4>
|
||||||
|
<el-tag :type="getStatusTagType(activeStore.status)">{{ activeStore.status || '未设置' }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="知识库名称">
|
||||||
|
{{ activeStore.storeName }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="知识库编码">
|
||||||
|
{{ activeStore.storeCode }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="描述" :span="2">
|
||||||
|
{{ activeStore.description || '暂无描述' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备注" :span="2">
|
||||||
|
{{ activeStore.remark || '暂无备注' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">
|
||||||
|
{{ activeStore.createTime || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间">
|
||||||
|
{{ activeStore.updateTime || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="detail-card detail-card--placeholder">
|
||||||
|
<div class="detail-card__header">
|
||||||
|
<h4>文档概览</h4>
|
||||||
|
<span>下一批接口补充</span>
|
||||||
|
</div>
|
||||||
|
<el-empty description="文档数量、切片数量、最近上传时间待后端聚合接口补充" />
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="detail-card detail-card--placeholder">
|
||||||
|
<div class="detail-card__header">
|
||||||
|
<h4>检索配置</h4>
|
||||||
|
<span>下一批接口补充</span>
|
||||||
|
</div>
|
||||||
|
<el-empty description="检索模式、Embedding 模型、Chunk 参数待后端补充" />
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="detail-card detail-card--placeholder">
|
||||||
|
<div class="detail-card__header">
|
||||||
|
<h4>最近任务</h4>
|
||||||
|
<span>下一批接口补充</span>
|
||||||
|
</div>
|
||||||
|
<el-empty description="导入任务、索引任务与状态流转待后端补充" />
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-empty v-else description="请选择左侧一个知识库查看详情" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-dialog v-model="createDialogVisible" title="新建知识库" width="560px">
|
||||||
|
<el-form :model="createForm" label-width="96px">
|
||||||
|
<el-form-item label="知识库编码" required>
|
||||||
|
<el-input v-model="createForm.storeCode" data-test="create-store-code" placeholder="如 PROD_DOC" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="知识库名称" required>
|
||||||
|
<el-input
|
||||||
|
v-model="createForm.storeName"
|
||||||
|
data-test="create-store-name"
|
||||||
|
placeholder="请输入知识库名称"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-radio-group v-model="createForm.status">
|
||||||
|
<el-radio-button label="启用" />
|
||||||
|
<el-radio-button label="停用" />
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="描述">
|
||||||
|
<el-input v-model="createForm.description" type="textarea" :rows="3" placeholder="请输入知识库描述" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="createForm.remark" type="textarea" :rows="2" placeholder="可选" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||||
|
<el-button data-test="create-store-submit" type="primary" :loading="submitting" @click="submitCreateStore">
|
||||||
|
保存
|
||||||
|
</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.storeCode" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="知识库名称" required>
|
||||||
|
<el-input v-model="editForm.storeName" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-radio-group v-model="editForm.status">
|
||||||
|
<el-radio-button label="启用" />
|
||||||
|
<el-radio-button label="停用" />
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="描述">
|
||||||
|
<el-input v-model="editForm.description" type="textarea" :rows="3" />
|
||||||
|
</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="submitEditStore">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rag-store-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rag-store-page__header {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rag-store-page__header p {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
color: #667085;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px 22px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card {
|
||||||
|
border: 1px solid #e6ebf3;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 18px 18px 16px;
|
||||||
|
background: linear-gradient(180deg, #ffffff, #f9fbff);
|
||||||
|
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card__label {
|
||||||
|
display: block;
|
||||||
|
color: #667085;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card__value {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #172033;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card__hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #98a2b3;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rag-store-page__content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(320px, 0.95fr) minmax(0, 1.45fr);
|
||||||
|
gap: 18px;
|
||||||
|
padding: 18px 22px 22px;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-list-panel,
|
||||||
|
.store-detail-panel {
|
||||||
|
border: 1px solid #e6ebf3;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fcfdff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-detail-panel__body {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 18px 18px 14px;
|
||||||
|
border-bottom: 1px solid #eef2f7;
|
||||||
|
background: linear-gradient(180deg, #ffffff, #fbfcff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #172033;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading p {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
color: #667085;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-search-bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
border-bottom: 1px solid #eef2f7;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 18px;
|
||||||
|
max-height: 780px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card {
|
||||||
|
border: 1px solid #e3e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: left;
|
||||||
|
background: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 0.2s ease,
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card:hover {
|
||||||
|
border-color: #bfdbfe;
|
||||||
|
box-shadow: 0 8px 22px rgba(22, 119, 255, 0.08);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card--active {
|
||||||
|
border-color: #1677ff;
|
||||||
|
box-shadow: 0 10px 24px rgba(22, 119, 255, 0.12);
|
||||||
|
background: #f7fbff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card__title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card strong {
|
||||||
|
color: #172033;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card p {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: #475467;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card__metrics {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #667085;
|
||||||
|
font-size: 13px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading--detail {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card {
|
||||||
|
border: 1px solid #e7edf5;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card__header h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: #172033;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card__header span {
|
||||||
|
color: #667085;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card--placeholder :deep(.el-empty) {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
.overview-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.rag-store-page__content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.overview-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding: 16px 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rag-store-page__content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading,
|
||||||
|
.section-heading--detail {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-search-bar {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
132
frontend/src/pages/__tests__/RagStoresPage.spec.ts
Normal file
132
frontend/src/pages/__tests__/RagStoresPage.spec.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { flushPromises, mount } from '@vue/test-utils';
|
||||||
|
import ElementPlus from 'element-plus';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import RagStoresPage from '../RagStoresPage.vue';
|
||||||
|
import { getRagStoreById, queryRagStores, saveRagStore } from '@/api/ragStores';
|
||||||
|
|
||||||
|
vi.mock('@/api/ragStores', () => ({
|
||||||
|
queryRagStores: vi.fn((query?: { storeName?: string }) => {
|
||||||
|
const rows = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
storeCode: 'PROD_DOC',
|
||||||
|
storeName: '产品制度库',
|
||||||
|
description: '产品制度、业务规范、流程材料',
|
||||||
|
status: '启用',
|
||||||
|
createTime: '2026-05-03 10:20:00',
|
||||||
|
updateTime: '2026-05-21 16:40:00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
storeCode: 'FAQ',
|
||||||
|
storeName: 'FAQ知识库',
|
||||||
|
description: '常见问题知识沉淀',
|
||||||
|
status: '停用',
|
||||||
|
createTime: '2026-05-06 09:10:00',
|
||||||
|
updateTime: '2026-05-21 11:12:00',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const keyword = query?.storeName?.trim();
|
||||||
|
const data = keyword ? rows.filter((row) => row.storeName.includes(keyword)) : rows;
|
||||||
|
return Promise.resolve({ resultcode: '0', message: null, data });
|
||||||
|
}),
|
||||||
|
getRagStoreById: vi.fn((id: string) =>
|
||||||
|
Promise.resolve({
|
||||||
|
resultcode: '0',
|
||||||
|
message: null,
|
||||||
|
data:
|
||||||
|
id === '2'
|
||||||
|
? {
|
||||||
|
id: '2',
|
||||||
|
storeCode: 'FAQ',
|
||||||
|
storeName: 'FAQ知识库',
|
||||||
|
description: '常见问题知识沉淀',
|
||||||
|
status: '停用',
|
||||||
|
remark: 'FAQ 场景知识',
|
||||||
|
createTime: '2026-05-06 09:10:00',
|
||||||
|
updateTime: '2026-05-21 11:12:00',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
id: '1',
|
||||||
|
storeCode: 'PROD_DOC',
|
||||||
|
storeName: '产品制度库',
|
||||||
|
description: '产品制度、业务规范、流程材料',
|
||||||
|
status: '启用',
|
||||||
|
remark: '核心制度库',
|
||||||
|
createTime: '2026-05-03 10:20:00',
|
||||||
|
updateTime: '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 })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('RagStoresPage', () => {
|
||||||
|
it('renders overview cards and loads default store detail from backend data', async () => {
|
||||||
|
const wrapper = mount(RagStoresPage, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('知识库总数');
|
||||||
|
expect(wrapper.text()).toContain('产品制度库');
|
||||||
|
expect(wrapper.text()).toContain('核心制度库');
|
||||||
|
expect(queryRagStores).toHaveBeenCalled();
|
||||||
|
expect(getRagStoreById).toHaveBeenCalledWith('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters stores by name and updates detail when a store is selected', async () => {
|
||||||
|
const wrapper = mount(RagStoresPage, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
await wrapper.get('[data-test="store-name-input"]').setValue('FAQ');
|
||||||
|
await wrapper.get('[data-test="store-search"]').trigger('click');
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(queryRagStores).toHaveBeenLastCalledWith({
|
||||||
|
storeName: 'FAQ',
|
||||||
|
});
|
||||||
|
expect(wrapper.text()).toContain('FAQ知识库');
|
||||||
|
expect(wrapper.text()).not.toContain('核心制度库');
|
||||||
|
|
||||||
|
await wrapper.get('[data-test="store-card-faq"]').trigger('click');
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(getRagStoreById).toHaveBeenLastCalledWith('2');
|
||||||
|
expect(wrapper.text()).toContain('FAQ 场景知识');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submits create form through backend api', async () => {
|
||||||
|
const wrapper = mount(RagStoresPage, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
await wrapper.get('[data-test="create-store"]').trigger('click');
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
await wrapper.get('[data-test="create-store-code"]').setValue('NEW_STORE');
|
||||||
|
await wrapper.get('[data-test="create-store-name"]').setValue('新建知识库');
|
||||||
|
await wrapper.get('[data-test="create-store-submit"]').trigger('click');
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(saveRagStore).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
storeCode: 'NEW_STORE',
|
||||||
|
storeName: '新建知识库',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
434
rag-store-page-apis.md
Normal file
434
rag-store-page-apis.md
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
# 知识库页面后端接口清单
|
||||||
|
|
||||||
|
本文对应前端页面:[RagStoresPage.vue](/D:/Code/common_agent/frontend/src/pages/RagStoresPage.vue)
|
||||||
|
|
||||||
|
## 1. 页面目标
|
||||||
|
|
||||||
|
知识库页面采用:
|
||||||
|
|
||||||
|
- 顶部 4 张全局统计卡片
|
||||||
|
- 左侧知识库名称搜索与列表
|
||||||
|
- 右侧当前知识库详情
|
||||||
|
- 当前知识库级别操作:编辑、批量导入文件、重建索引
|
||||||
|
|
||||||
|
因此接口建议拆成 `全局概览`、`知识库列表/详情`、`单库动作` 三组。
|
||||||
|
|
||||||
|
## 2. 本批已实现并已用于前端联调的接口
|
||||||
|
|
||||||
|
### 2.1 查询全部知识库
|
||||||
|
|
||||||
|
- `GET /api/rag/stores`
|
||||||
|
|
||||||
|
当前返回类型:
|
||||||
|
|
||||||
|
- `RequestResult<List<RagStoreResponse>>`
|
||||||
|
|
||||||
|
当前字段:
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `storeCode`
|
||||||
|
- `storeName`
|
||||||
|
- `description`
|
||||||
|
- `status`
|
||||||
|
- `remark`
|
||||||
|
|
||||||
|
对应代码:
|
||||||
|
|
||||||
|
- [RagStoreController.java](/D:/Code/common_agent/src/main/java/com/bruce/rag/controller/RagStoreController.java)
|
||||||
|
- [RagStoreResponse.java](/D:/Code/common_agent/src/main/java/com/bruce/rag/dto/response/RagStoreResponse.java)
|
||||||
|
|
||||||
|
### 2.2 按条件查询知识库
|
||||||
|
|
||||||
|
- `POST /api/rag/stores/query`
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"storeCode": "PROD_DOC",
|
||||||
|
"storeName": "产品制度",
|
||||||
|
"status": "ENABLED"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
当前支持字段:
|
||||||
|
|
||||||
|
- `storeCode`
|
||||||
|
- `storeName`
|
||||||
|
- `status`
|
||||||
|
|
||||||
|
对应代码:
|
||||||
|
|
||||||
|
- [RagStoreQueryRequest.java](/D:/Code/common_agent/src/main/java/com/bruce/rag/dto/request/RagStoreQueryRequest.java)
|
||||||
|
|
||||||
|
### 2.3 查询知识库详情
|
||||||
|
|
||||||
|
- `GET /api/rag/stores/{id}`
|
||||||
|
|
||||||
|
返回类型:
|
||||||
|
|
||||||
|
- `RequestResult<RagStoreResponse>`
|
||||||
|
|
||||||
|
### 2.4 新增或修改知识库
|
||||||
|
|
||||||
|
- `POST /api/rag/stores`
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"storeCode": "PROD_DOC",
|
||||||
|
"storeName": "产品制度库",
|
||||||
|
"description": "产品制度、业务规范、流程材料",
|
||||||
|
"status": "启用",
|
||||||
|
"remark": "核心制度库"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回类型:
|
||||||
|
|
||||||
|
- `RequestResult<Boolean>`
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `id` 为空时新增
|
||||||
|
- `id` 不为空时修改
|
||||||
|
|
||||||
|
### 2.5 删除知识库
|
||||||
|
|
||||||
|
- `DELETE /api/rag/stores/{id}`
|
||||||
|
|
||||||
|
返回类型:
|
||||||
|
|
||||||
|
- `RequestResult<Boolean>`
|
||||||
|
|
||||||
|
## 3. 当前项目里已有但本批前端未联调的接口
|
||||||
|
|
||||||
|
### 3.1 查询全部知识文档
|
||||||
|
|
||||||
|
- `GET /api/rag/documents`
|
||||||
|
|
||||||
|
### 3.2 按条件查询知识文档
|
||||||
|
|
||||||
|
- `POST /api/rag/documents/query`
|
||||||
|
|
||||||
|
当前可用于按 `storeId` 查询当前知识库下文档。
|
||||||
|
|
||||||
|
对应代码:
|
||||||
|
|
||||||
|
- [RagDocumentController.java](/D:/Code/common_agent/src/main/java/com/bruce/rag/controller/RagDocumentController.java)
|
||||||
|
- [RagDocumentQueryRequest.java](/D:/Code/common_agent/src/main/java/com/bruce/rag/dto/request/RagDocumentQueryRequest.java)
|
||||||
|
|
||||||
|
## 4. 下一批建议补充的接口
|
||||||
|
|
||||||
|
当前已有接口能支撑最基础的列表查询,但还不足以支撑统计卡片、右侧详情聚合和单库动作。建议补下面几个接口。
|
||||||
|
|
||||||
|
### 4.1 知识库总览统计
|
||||||
|
|
||||||
|
- `GET /api/rag/stores/overview`
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 顶部 4 张卡片数据
|
||||||
|
|
||||||
|
返回建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"resultcode": "0",
|
||||||
|
"message": null,
|
||||||
|
"data": {
|
||||||
|
"storeCount": 12,
|
||||||
|
"documentCount": 1286,
|
||||||
|
"chunkCount": 24390,
|
||||||
|
"retrievableStoreCount": 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议响应 DTO:
|
||||||
|
|
||||||
|
- `RagStoreOverviewResponse`
|
||||||
|
|
||||||
|
字段建议:
|
||||||
|
|
||||||
|
- `storeCount`
|
||||||
|
- `documentCount`
|
||||||
|
- `chunkCount`
|
||||||
|
- `retrievableStoreCount`
|
||||||
|
|
||||||
|
### 4.2 知识库列表查询增强版
|
||||||
|
|
||||||
|
- `POST /api/rag/stores/manage/query`
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 左侧知识库列表
|
||||||
|
|
||||||
|
相比当前 `/query`,建议直接返回列表页需要的摘要字段,避免前端再额外聚合文档数据。
|
||||||
|
|
||||||
|
请求体建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"storeName": "FAQ"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"resultcode": "0",
|
||||||
|
"message": null,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"storeCode": "FAQ",
|
||||||
|
"storeName": "FAQ知识库",
|
||||||
|
"description": "常见问题知识沉淀",
|
||||||
|
"status": "ENABLED",
|
||||||
|
"documentCount": 58,
|
||||||
|
"chunkCount": 920,
|
||||||
|
"indexStatus": "PROCESSING",
|
||||||
|
"retrievable": true,
|
||||||
|
"updateTime": "2026-05-21 11:12:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议响应 DTO:
|
||||||
|
|
||||||
|
- `RagStoreManageListResponse`
|
||||||
|
|
||||||
|
字段建议:
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `storeCode`
|
||||||
|
- `storeName`
|
||||||
|
- `description`
|
||||||
|
- `status`
|
||||||
|
- `documentCount`
|
||||||
|
- `chunkCount`
|
||||||
|
- `indexStatus`
|
||||||
|
- `retrievable`
|
||||||
|
- `updateTime`
|
||||||
|
|
||||||
|
### 4.3 查询单个知识库详情增强版
|
||||||
|
|
||||||
|
- `GET /api/rag/stores/{id}/detail`
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 右侧详情区
|
||||||
|
|
||||||
|
返回建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"resultcode": "0",
|
||||||
|
"message": null,
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"storeCode": "PROD_DOC",
|
||||||
|
"storeName": "产品制度库",
|
||||||
|
"description": "产品制度、业务规范、流程材料",
|
||||||
|
"status": "ENABLED",
|
||||||
|
"createTime": "2026-05-03 10:20:00",
|
||||||
|
"updateTime": "2026-05-21 16:40:00",
|
||||||
|
"documentCount": 126,
|
||||||
|
"parseSuccessCount": 120,
|
||||||
|
"parseFailedCount": 6,
|
||||||
|
"chunkCount": 3800,
|
||||||
|
"lastUploadTime": "2026-05-21 15:32:00",
|
||||||
|
"lastIndexTime": "2026-05-21 15:48:00",
|
||||||
|
"retrievalMode": "HYBRID",
|
||||||
|
"embeddingModel": "bge-large-zh",
|
||||||
|
"chunkSize": 500,
|
||||||
|
"chunkOverlap": 100,
|
||||||
|
"topK": 5,
|
||||||
|
"retrievable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议响应 DTO:
|
||||||
|
|
||||||
|
- `RagStoreDetailResponse`
|
||||||
|
|
||||||
|
### 4.4 新建知识库独立接口
|
||||||
|
|
||||||
|
- `POST /api/rag/stores`
|
||||||
|
|
||||||
|
请求体建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"storeCode": "PROD_DOC",
|
||||||
|
"storeName": "产品制度库",
|
||||||
|
"description": "产品制度、业务规范、流程材料"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回建议:
|
||||||
|
|
||||||
|
- 返回新建后的 `id` 或完整 `RagStoreDetailResponse`
|
||||||
|
|
||||||
|
建议请求 DTO:
|
||||||
|
|
||||||
|
- `RagStoreSaveRequest`
|
||||||
|
|
||||||
|
### 4.5 编辑知识库独立接口
|
||||||
|
|
||||||
|
- `PUT /api/rag/stores/{id}`
|
||||||
|
|
||||||
|
请求体建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"storeCode": "PROD_DOC",
|
||||||
|
"storeName": "产品制度库",
|
||||||
|
"description": "产品制度、业务规范、流程材料",
|
||||||
|
"status": "ENABLED"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 右侧“编辑”按钮
|
||||||
|
|
||||||
|
### 4.6 当前知识库批量导入文件
|
||||||
|
|
||||||
|
- `POST /api/rag/stores/{id}/documents/import`
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 右侧“批量导入文件”按钮
|
||||||
|
|
||||||
|
建议请求类型:
|
||||||
|
|
||||||
|
- `multipart/form-data`
|
||||||
|
|
||||||
|
表单字段建议:
|
||||||
|
|
||||||
|
- `files`: 文件数组
|
||||||
|
- `remark`: 批次备注,可选
|
||||||
|
|
||||||
|
返回建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"resultcode": "0",
|
||||||
|
"message": null,
|
||||||
|
"data": {
|
||||||
|
"taskId": 1001,
|
||||||
|
"storeId": 1,
|
||||||
|
"fileCount": 12,
|
||||||
|
"status": "PROCESSING"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议响应 DTO:
|
||||||
|
|
||||||
|
- `RagImportTaskResponse`
|
||||||
|
|
||||||
|
### 4.7 发起当前知识库重建索引
|
||||||
|
|
||||||
|
- `POST /api/rag/stores/{id}/reindex`
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 右侧“重建索引”按钮
|
||||||
|
|
||||||
|
请求体建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"force": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"resultcode": "0",
|
||||||
|
"message": null,
|
||||||
|
"data": {
|
||||||
|
"taskId": 1002,
|
||||||
|
"storeId": 1,
|
||||||
|
"status": "PROCESSING"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.8 查询当前知识库最近任务
|
||||||
|
|
||||||
|
- `GET /api/rag/stores/{id}/tasks?limit=10`
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 右侧“最近任务”区
|
||||||
|
|
||||||
|
返回建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"resultcode": "0",
|
||||||
|
"message": null,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1002,
|
||||||
|
"taskType": "REINDEX",
|
||||||
|
"summary": "全库索引刷新",
|
||||||
|
"status": "PROCESSING",
|
||||||
|
"startedAt": "2026-05-21 16:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1001,
|
||||||
|
"taskType": "IMPORT",
|
||||||
|
"summary": "12 个文件,制度文档增量导入",
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"startedAt": "2026-05-21 15:20:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议响应 DTO:
|
||||||
|
|
||||||
|
- `RagStoreTaskResponse`
|
||||||
|
|
||||||
|
## 5. 这页前后端最小联调顺序
|
||||||
|
|
||||||
|
如果想尽快把这页从演示版切到真实联调版,建议按下面顺序接:
|
||||||
|
|
||||||
|
1. 先复用已有:
|
||||||
|
- `POST /api/rag/stores/query`
|
||||||
|
|
||||||
|
2. 然后新增:
|
||||||
|
- `GET /api/rag/stores/overview`
|
||||||
|
- `GET /api/rag/stores/{id}/detail`
|
||||||
|
|
||||||
|
3. 再补动作接口:
|
||||||
|
- `POST /api/rag/stores`
|
||||||
|
- `PUT /api/rag/stores/{id}`
|
||||||
|
- `POST /api/rag/stores/{id}/documents/import`
|
||||||
|
- `POST /api/rag/stores/{id}/reindex`
|
||||||
|
- `GET /api/rag/stores/{id}/tasks`
|
||||||
|
|
||||||
|
## 6. 当前前端实现说明
|
||||||
|
|
||||||
|
当前前端页已经按上述页面结构实现,但由于后端尚未提供完整聚合接口,页面中的统计、详情和任务区先以演示数据承载。
|
||||||
|
|
||||||
|
后端接口齐备后,前端建议按下面方式替换:
|
||||||
|
|
||||||
|
- 统计卡片:改调 `/api/rag/stores/overview`
|
||||||
|
- 左侧列表:改调 `/api/rag/stores/manage/query`
|
||||||
|
- 右侧详情:改调 `/api/rag/stores/{id}/detail`
|
||||||
|
- 批量导入:改调 `/api/rag/stores/{id}/documents/import`
|
||||||
|
- 重建索引:改调 `/api/rag/stores/{id}/reindex`
|
||||||
|
- 最近任务:改调 `/api/rag/stores/{id}/tasks`
|
||||||
@@ -2,36 +2,74 @@ package com.bruce.rag.controller;
|
|||||||
|
|
||||||
import com.bruce.common.domain.model.RequestResult;
|
import com.bruce.common.domain.model.RequestResult;
|
||||||
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
||||||
|
import com.bruce.rag.dto.request.RagStoreSaveRequest;
|
||||||
import com.bruce.rag.dto.response.RagStoreResponse;
|
import com.bruce.rag.dto.response.RagStoreResponse;
|
||||||
import com.bruce.rag.service.IRagStoreService;
|
import com.bruce.rag.service.IRagStoreService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Tag(name = "RAG知识库管理")
|
@Tag(name = "RAG知识库管理")
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/rag/stores")
|
@RequestMapping("/api/rag/store")
|
||||||
public class RagStoreController {
|
public class RagStoreController {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IRagStoreService ragStoreService;
|
private IRagStoreService ragStoreService;
|
||||||
|
|
||||||
@Operation(summary = "查询全部知识库")
|
@Operation(summary = "查询全部知识库")
|
||||||
@GetMapping
|
@PostMapping("/list")
|
||||||
public RequestResult<List<RagStoreResponse>> list() {
|
public RequestResult<List<RagStoreResponse>> list() {
|
||||||
return RequestResult.success(ragStoreService.listResponses());
|
log.info("RagStoreController.list start");
|
||||||
|
List<RagStoreResponse> responses = ragStoreService.listResponses();
|
||||||
|
log.info("RagStoreController.list success, count={}", responses.size());
|
||||||
|
return RequestResult.success(responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "按条件查询知识库")
|
@Operation(summary = "按条件查询知识库")
|
||||||
@PostMapping("/query")
|
@PostMapping("/query")
|
||||||
public RequestResult<List<RagStoreResponse>> query(@RequestBody RagStoreQueryRequest request) {
|
public RequestResult<List<RagStoreResponse>> query(@RequestBody(required = false) RagStoreQueryRequest request) {
|
||||||
return RequestResult.success(ragStoreService.query(request));
|
log.info("RagStoreController.query start, request={}", request);
|
||||||
|
List<RagStoreResponse> responses = ragStoreService.query(request);
|
||||||
|
log.info("RagStoreController.query success, count={}", responses.size());
|
||||||
|
return RequestResult.success(responses);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "查询知识库详情")
|
||||||
|
@GetMapping("/detail")
|
||||||
|
public RequestResult<RagStoreResponse> getById(@RequestParam("id") Long id) {
|
||||||
|
log.info("RagStoreController.getById start, id={}", id);
|
||||||
|
RagStoreResponse response = ragStoreService.getResponseById(id);
|
||||||
|
log.info("RagStoreController.getById success, id={}, found={}", id, response != null);
|
||||||
|
return RequestResult.success(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "新增或修改知识库")
|
||||||
|
@PostMapping("/save")
|
||||||
|
public RequestResult<Boolean> saveOrUpdate(@RequestBody RagStoreSaveRequest request) {
|
||||||
|
log.info("RagStoreController.saveOrUpdate start, request={}", request);
|
||||||
|
Boolean result = ragStoreService.saveOrUpdate(request);
|
||||||
|
log.info("RagStoreController.saveOrUpdate success, id={}, storeCode={}, result={}",
|
||||||
|
request.getId(), request.getStoreCode(), result);
|
||||||
|
return RequestResult.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "删除知识库")
|
||||||
|
@PostMapping("/delete")
|
||||||
|
public RequestResult<Boolean> deleteById(@RequestParam("id") Long id) {
|
||||||
|
log.info("RagStoreController.deleteById start, id={}", id);
|
||||||
|
Boolean result = ragStoreService.removeById(id);
|
||||||
|
log.info("RagStoreController.deleteById success, id={}, result={}", id, result);
|
||||||
|
return RequestResult.success(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.bruce.rag.dto.request;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "RAG知识库保存请求")
|
||||||
|
public class RagStoreSaveRequest {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "知识库编码")
|
||||||
|
private String storeCode;
|
||||||
|
|
||||||
|
@Schema(description = "知识库名称")
|
||||||
|
private String storeName;
|
||||||
|
|
||||||
|
@Schema(description = "知识库描述")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "状态")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
package com.bruce.rag.dto.response;
|
package com.bruce.rag.dto.response;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
import com.bruce.rag.entity.RagStore;
|
import com.bruce.rag.entity.RagStore;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "RAG知识库响应")
|
@Schema(description = "RAG知识库响应")
|
||||||
public class RagStoreResponse {
|
public class RagStoreResponse {
|
||||||
|
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
@Schema(description = "主键ID")
|
@Schema(description = "主键ID")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@@ -27,6 +32,12 @@ public class RagStoreResponse {
|
|||||||
@Schema(description = "备注")
|
@Schema(description = "备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private Date updateTime;
|
||||||
|
|
||||||
public static RagStoreResponse fromEntity(RagStore entity) {
|
public static RagStoreResponse fromEntity(RagStore entity) {
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.bruce.rag.service;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
||||||
|
import com.bruce.rag.dto.request.RagStoreSaveRequest;
|
||||||
import com.bruce.rag.dto.response.RagStoreResponse;
|
import com.bruce.rag.dto.response.RagStoreResponse;
|
||||||
import com.bruce.rag.entity.RagStore;
|
import com.bruce.rag.entity.RagStore;
|
||||||
|
|
||||||
@@ -12,4 +13,8 @@ public interface IRagStoreService extends IService<RagStore> {
|
|||||||
List<RagStoreResponse> listResponses();
|
List<RagStoreResponse> listResponses();
|
||||||
|
|
||||||
List<RagStoreResponse> query(RagStoreQueryRequest request);
|
List<RagStoreResponse> query(RagStoreQueryRequest request);
|
||||||
|
|
||||||
|
RagStoreResponse getResponseById(Long id);
|
||||||
|
|
||||||
|
boolean saveOrUpdate(RagStoreSaveRequest request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,34 +2,92 @@ package com.bruce.rag.service.impl;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
||||||
|
import com.bruce.rag.dto.request.RagStoreSaveRequest;
|
||||||
import com.bruce.rag.dto.response.RagStoreResponse;
|
import com.bruce.rag.dto.response.RagStoreResponse;
|
||||||
import com.bruce.rag.entity.RagStore;
|
import com.bruce.rag.entity.RagStore;
|
||||||
import com.bruce.rag.mapper.RagStoreMapper;
|
import com.bruce.rag.mapper.RagStoreMapper;
|
||||||
import com.bruce.rag.service.IRagStoreService;
|
import com.bruce.rag.service.IRagStoreService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> implements IRagStoreService {
|
public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> implements IRagStoreService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<RagStoreResponse> listResponses() {
|
public List<RagStoreResponse> listResponses() {
|
||||||
return toResponses(list());
|
log.info("RagStoreServiceImpl.listResponses start");
|
||||||
|
List<RagStoreResponse> responses = toResponses(list());
|
||||||
|
log.info("RagStoreServiceImpl.listResponses success, count={}", responses.size());
|
||||||
|
return responses;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<RagStoreResponse> query(RagStoreQueryRequest request) {
|
public List<RagStoreResponse> query(RagStoreQueryRequest request) {
|
||||||
if (request == null) {
|
log.info("RagStoreServiceImpl.query start, request={}", request);
|
||||||
throw new IllegalArgumentException("查询请求不能为空");
|
RagStoreQueryRequest queryRequest = request == null ? new RagStoreQueryRequest() : request;
|
||||||
}
|
List<RagStoreResponse> responses = toResponses(lambdaQuery()
|
||||||
return toResponses(lambdaQuery()
|
.eq(StringUtils.hasText(queryRequest.getStoreCode()), RagStore::getStoreCode, queryRequest.getStoreCode())
|
||||||
.eq(StringUtils.hasText(request.getStoreCode()), RagStore::getStoreCode, request.getStoreCode())
|
.like(StringUtils.hasText(queryRequest.getStoreName()), RagStore::getStoreName, queryRequest.getStoreName())
|
||||||
.like(StringUtils.hasText(request.getStoreName()), RagStore::getStoreName, request.getStoreName())
|
.eq(StringUtils.hasText(queryRequest.getStatus()), RagStore::getStatus, queryRequest.getStatus())
|
||||||
.eq(StringUtils.hasText(request.getStatus()), RagStore::getStatus, request.getStatus())
|
|
||||||
.orderByAsc(RagStore::getStoreCode)
|
.orderByAsc(RagStore::getStoreCode)
|
||||||
.list());
|
.list());
|
||||||
|
log.info("RagStoreServiceImpl.query success, count={}", responses.size());
|
||||||
|
return responses;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RagStoreResponse getResponseById(Long id) {
|
||||||
|
log.info("RagStoreServiceImpl.getResponseById start, id={}", id);
|
||||||
|
RagStoreResponse response = RagStoreResponse.fromEntity(getById(id));
|
||||||
|
log.info("RagStoreServiceImpl.getResponseById success, id={}, found={}", id, response != null);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean saveOrUpdate(RagStoreSaveRequest request) {
|
||||||
|
log.info("RagStoreServiceImpl.saveOrUpdate start, request={}", request);
|
||||||
|
validateSaveRequest(request);
|
||||||
|
|
||||||
|
RagStore existingStore = lambdaQuery()
|
||||||
|
.eq(RagStore::getStoreCode, request.getStoreCode().trim())
|
||||||
|
.ne(request.getId() != null, RagStore::getId, request.getId())
|
||||||
|
.one();
|
||||||
|
if (existingStore != null) {
|
||||||
|
log.warn("RagStoreServiceImpl.saveOrUpdate duplicate storeCode detected, requestId={}, existingId={}, storeCode={}",
|
||||||
|
request.getId(), existingStore.getId(), request.getStoreCode().trim());
|
||||||
|
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()));
|
||||||
|
boolean result = super.saveOrUpdate(ragStore);
|
||||||
|
log.info("RagStoreServiceImpl.saveOrUpdate success, requestId={}, savedId={}, storeCode={}, result={}",
|
||||||
|
request.getId(), ragStore.getId(), ragStore.getStoreCode(), result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateSaveRequest(RagStoreSaveRequest request) {
|
||||||
|
log.info("RagStoreServiceImpl.validateSaveRequest start");
|
||||||
|
if (request == null) {
|
||||||
|
throw new IllegalArgumentException("保存请求不能为空");
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasText(request.getStoreCode())) {
|
||||||
|
throw new IllegalArgumentException("知识库编码不能为空");
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasText(request.getStoreName())) {
|
||||||
|
throw new IllegalArgumentException("知识库名称不能为空");
|
||||||
|
}
|
||||||
|
log.info("RagStoreServiceImpl.validateSaveRequest success, id={}, storeCode={}, storeName={}",
|
||||||
|
request.getId(), request.getStoreCode(), request.getStoreName());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<RagStoreResponse> toResponses(List<RagStore> stores) {
|
private List<RagStoreResponse> toResponses(List<RagStore> stores) {
|
||||||
@@ -37,4 +95,11 @@ public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> i
|
|||||||
.map(RagStoreResponse::fromEntity)
|
.map(RagStoreResponse::fromEntity)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String trimToNull(String value) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import com.bruce.rag.controller.RagDocumentController;
|
|||||||
import com.bruce.rag.controller.RagStoreController;
|
import com.bruce.rag.controller.RagStoreController;
|
||||||
import com.bruce.rag.dto.request.RagDocumentQueryRequest;
|
import com.bruce.rag.dto.request.RagDocumentQueryRequest;
|
||||||
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
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.RagDocumentResponse;
|
||||||
import com.bruce.rag.dto.response.RagStoreResponse;
|
import com.bruce.rag.dto.response.RagStoreResponse;
|
||||||
import com.bruce.rag.entity.RagDocument;
|
import com.bruce.rag.entity.RagDocument;
|
||||||
@@ -44,8 +45,13 @@ class RagComponentStructureTests {
|
|||||||
void ragControllersShouldExposeRequestResultAndQueryDtoMethods() throws NoSuchMethodException {
|
void ragControllersShouldExposeRequestResultAndQueryDtoMethods() throws NoSuchMethodException {
|
||||||
Method storeListMethod = RagStoreController.class.getMethod("list");
|
Method storeListMethod = RagStoreController.class.getMethod("list");
|
||||||
Method storeQueryMethod = RagStoreController.class.getMethod("query", RagStoreQueryRequest.class);
|
Method storeQueryMethod = RagStoreController.class.getMethod("query", RagStoreQueryRequest.class);
|
||||||
|
Method storeDetailMethod = RagStoreController.class.getMethod("getById", 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 storeResponseListMethod = IRagStoreService.class.getMethod("listResponses");
|
||||||
Method storeServiceQueryMethod = IRagStoreService.class.getMethod("query", RagStoreQueryRequest.class);
|
Method storeServiceQueryMethod = IRagStoreService.class.getMethod("query", RagStoreQueryRequest.class);
|
||||||
|
Method storeServiceDetailMethod = IRagStoreService.class.getMethod("getResponseById", Long.class);
|
||||||
|
Method storeServiceSaveMethod = IRagStoreService.class.getMethod("saveOrUpdate", RagStoreSaveRequest.class);
|
||||||
|
|
||||||
Method documentListMethod = RagDocumentController.class.getMethod("list");
|
Method documentListMethod = RagDocumentController.class.getMethod("list");
|
||||||
Method documentQueryMethod = RagDocumentController.class.getMethod("query", RagDocumentQueryRequest.class);
|
Method documentQueryMethod = RagDocumentController.class.getMethod("query", RagDocumentQueryRequest.class);
|
||||||
@@ -54,11 +60,17 @@ class RagComponentStructureTests {
|
|||||||
|
|
||||||
assertEquals(RequestResult.class, storeListMethod.getReturnType());
|
assertEquals(RequestResult.class, storeListMethod.getReturnType());
|
||||||
assertEquals(RequestResult.class, storeQueryMethod.getReturnType());
|
assertEquals(RequestResult.class, storeQueryMethod.getReturnType());
|
||||||
|
assertEquals(RequestResult.class, storeDetailMethod.getReturnType());
|
||||||
|
assertEquals(RequestResult.class, storeSaveMethod.getReturnType());
|
||||||
|
assertEquals(RequestResult.class, storeDeleteMethod.getReturnType());
|
||||||
assertEquals(List.class, storeServiceQueryMethod.getReturnType());
|
assertEquals(List.class, storeServiceQueryMethod.getReturnType());
|
||||||
|
assertEquals(RagStoreResponse.class, storeServiceDetailMethod.getReturnType());
|
||||||
|
assertEquals(boolean.class, storeServiceSaveMethod.getReturnType());
|
||||||
assertTrue(storeResponseListMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
assertTrue(storeResponseListMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
||||||
assertTrue(storeServiceQueryMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
assertTrue(storeServiceQueryMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
||||||
assertTrue(storeListMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
assertTrue(storeListMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
||||||
assertTrue(storeQueryMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
assertTrue(storeQueryMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
||||||
|
assertTrue(storeDetailMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
||||||
assertEquals(RagStoreResponse.class, RagStoreResponse.class.getMethod("fromEntity", RagStore.class).getReturnType());
|
assertEquals(RagStoreResponse.class, RagStoreResponse.class.getMethod("fromEntity", RagStore.class).getReturnType());
|
||||||
|
|
||||||
assertEquals(RequestResult.class, documentListMethod.getReturnType());
|
assertEquals(RequestResult.class, documentListMethod.getReturnType());
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.bruce.rag;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.bruce.rag.dto.response.RagStoreResponse;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class RagStoreResponseSerializationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void idShouldSerializeAsStringForFrontendPrecisionSafety() throws Exception {
|
||||||
|
RagStoreResponse response = new RagStoreResponse();
|
||||||
|
response.setId(2057302206052372481L);
|
||||||
|
response.setStoreCode("TEXT-1");
|
||||||
|
response.setStoreName("测试库1");
|
||||||
|
|
||||||
|
String json = new ObjectMapper().writeValueAsString(response);
|
||||||
|
|
||||||
|
assertTrue(json.contains("\"id\":\"2057302206052372481\""));
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/test/java/com/bruce/rag/RagStoreSaveValidationTests.java
Normal file
39
src/test/java/com/bruce/rag/RagStoreSaveValidationTests.java
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package com.bruce.rag;
|
||||||
|
|
||||||
|
import com.bruce.rag.dto.request.RagStoreSaveRequest;
|
||||||
|
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.assertThrows;
|
||||||
|
|
||||||
|
class RagStoreSaveValidationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveShouldRejectBlankStoreCode() {
|
||||||
|
RagStoreServiceImpl service = new RagStoreServiceImpl();
|
||||||
|
RagStoreSaveRequest request = new RagStoreSaveRequest();
|
||||||
|
request.setStoreName("产品制度库");
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> service.validateSaveRequest(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveShouldRejectBlankStoreName() {
|
||||||
|
RagStoreServiceImpl service = new RagStoreServiceImpl();
|
||||||
|
RagStoreSaveRequest request = new RagStoreSaveRequest();
|
||||||
|
request.setStoreCode("PROD_DOC");
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> service.validateSaveRequest(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveShouldAcceptMinimalValidRequest() {
|
||||||
|
RagStoreServiceImpl service = new RagStoreServiceImpl();
|
||||||
|
RagStoreSaveRequest request = new RagStoreSaveRequest();
|
||||||
|
request.setStoreCode("PROD_DOC");
|
||||||
|
request.setStoreName("产品制度库");
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> service.validateSaveRequest(request));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user