feat:新增知识库管理页面并联调知识库接口

This commit is contained in:
zhiye.sun
2026-05-21 13:09:33 +08:00
parent 387681a6ab
commit 91e6d5bdd3
12 changed files with 1506 additions and 17 deletions

View 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 },
});
}

View File

@@ -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">
<div>
<h2>知识库</h2> <h2>知识库</h2>
<span>RAG</span> <p>统一管理知识库及其文档索引状态</p>
</div>
<span>RAG Stores</span>
</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> </div>
</section> </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>
</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>

View 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
View 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`

View File

@@ -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);
} }
} }

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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);
} }

View File

@@ -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();
}
} }

View File

@@ -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());

View File

@@ -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\""));
}
}

View 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));
}
}