feat(rag-store): 补充知识库文档概览接口

This commit is contained in:
zhiye.sun
2026-05-21 15:35:45 +08:00
parent 541c3ff455
commit 8532628171
11 changed files with 484 additions and 17 deletions

View File

@@ -0,0 +1,31 @@
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
getRagStoreDocumentOverview,
getRagStoreOverview,
listRagStores,
} from '../ragStores';
import { get, post } from '../request';
vi.mock('../request', () => ({
get: vi.fn(),
post: vi.fn(),
}));
describe('rag stores api', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('loads overview and document overview endpoints', () => {
getRagStoreOverview();
getRagStoreDocumentOverview('1001');
listRagStores();
expect(get).toHaveBeenCalledWith('/rag/store/overview');
expect(get).toHaveBeenCalledWith('/rag/store/documentOverview', {
params: { storeId: '1001' },
});
expect(post).toHaveBeenCalledWith('/rag/store/list');
});
});

View File

@@ -11,6 +11,23 @@ export interface RagStore {
updateTime?: string | null; updateTime?: string | null;
} }
export interface RagStoreOverview {
totalStores: number;
totalDocuments: number;
totalChunks?: number | null;
retrievableStores: number;
}
export interface RagStoreDocumentOverview {
storeId: string;
storeName?: string | null;
documentCount: number;
enabledDocumentCount: number;
parsedDocumentCount: number;
indexedDocumentCount: number;
lastUploadTime?: string | null;
}
export interface RagStoreQueryRequest { export interface RagStoreQueryRequest {
storeCode?: string; storeCode?: string;
storeName?: string; storeName?: string;
@@ -33,6 +50,16 @@ export function getRagStoreById(id: string) {
}); });
} }
export function getRagStoreOverview() {
return get<RagStoreOverview>('/rag/store/overview');
}
export function getRagStoreDocumentOverview(storeId: string) {
return get<RagStoreDocumentOverview>('/rag/store/documentOverview', {
params: { storeId },
});
}
export function saveRagStore(data: RagStoreSaveRequest) { export function saveRagStore(data: RagStoreSaveRequest) {
return post<boolean, RagStoreSaveRequest>('/rag/store/save', data); return post<boolean, RagStoreSaveRequest>('/rag/store/save', data);
} }

View File

@@ -6,8 +6,12 @@ import { computed, onMounted, reactive, ref } from 'vue';
import { import {
deleteRagStore, deleteRagStore,
getRagStoreById, getRagStoreById,
getRagStoreDocumentOverview,
getRagStoreOverview,
queryRagStores, queryRagStores,
saveRagStore, saveRagStore,
type RagStoreDocumentOverview,
type RagStoreOverview,
type RagStore, type RagStore,
} from '@/api/ragStores'; } from '@/api/ragStores';
@@ -19,6 +23,8 @@ const submitting = ref(false);
const storeRows = ref<RagStore[]>([]); const storeRows = ref<RagStore[]>([]);
const activeStoreId = ref<string | null>(null); const activeStoreId = ref<string | null>(null);
const activeStore = ref<RagStore | null>(null); const activeStore = ref<RagStore | null>(null);
const pageOverview = ref<RagStoreOverview | null>(null);
const activeStoreDocumentOverview = ref<RagStoreDocumentOverview | null>(null);
const queryForm = reactive({ const queryForm = reactive({
storeName: '', storeName: '',
@@ -45,17 +51,30 @@ const editForm = reactive({
}); });
const overviewCards = computed(() => { const overviewCards = computed(() => {
const totalStores = storeRows.value.length; const totalStores = pageOverview.value?.totalStores ?? storeRows.value.length;
const retrievableStores = storeRows.value.filter((row) => row.status === '启用').length; const totalDocuments = pageOverview.value?.totalDocuments ?? '-';
const totalChunks = pageOverview.value?.totalChunks ?? '-';
const retrievableStores =
pageOverview.value?.retrievableStores
?? storeRows.value.filter((row) => row.status === '启用').length;
return [ return [
{ label: '知识库总数', value: totalStores, hint: '当前已登记知识库' }, { label: '知识库总数', value: totalStores, hint: '当前已登记知识库' },
{ label: '文档总数', value: '-', hint: '待文档统计接口补充' }, { label: '文档总数', value: totalDocuments, hint: '当前知识库已登记文档总量' },
{ label: '切片总数', value: '-', hint: '待切片统计接口补充' }, { label: '切片总数', value: totalChunks, hint: '当前未接入切片表,待后续能力补充' },
{ label: '可检索知识库数', value: retrievableStores, hint: '当前按启用状态暂代统计' }, { label: '可检索知识库数', value: retrievableStores, hint: '当前按启用状态统计' },
]; ];
}); });
async function loadOverview() {
try {
const response = await getRagStoreOverview();
pageOverview.value = response.data ?? null;
} catch {
pageOverview.value = null;
}
}
async function loadStores(preferredStoreId?: string | null) { async function loadStores(preferredStoreId?: string | null) {
loading.value = true; loading.value = true;
try { try {
@@ -67,6 +86,7 @@ async function loadStores(preferredStoreId?: string | null) {
if (storeRows.value.length === 0) { if (storeRows.value.length === 0) {
activeStoreId.value = null; activeStoreId.value = null;
activeStore.value = null; activeStore.value = null;
activeStoreDocumentOverview.value = null;
return; return;
} }
@@ -80,6 +100,7 @@ async function loadStores(preferredStoreId?: string | null) {
if (!targetId) { if (!targetId) {
activeStoreId.value = null; activeStoreId.value = null;
activeStore.value = null; activeStore.value = null;
activeStoreDocumentOverview.value = null;
return; return;
} }
await selectStore(targetId); await selectStore(targetId);
@@ -92,8 +113,12 @@ async function selectStore(storeId: string) {
activeStoreId.value = storeId; activeStoreId.value = storeId;
detailLoading.value = true; detailLoading.value = true;
try { try {
const response = await getRagStoreById(storeId); const [storeResponse, documentOverviewResponse] = await Promise.all([
activeStore.value = response.data ?? null; getRagStoreById(storeId),
getRagStoreDocumentOverview(storeId),
]);
activeStore.value = storeResponse.data ?? null;
activeStoreDocumentOverview.value = documentOverviewResponse.data ?? null;
} finally { } finally {
detailLoading.value = false; detailLoading.value = false;
} }
@@ -203,6 +228,7 @@ function getStatusTagType(status?: string | null) {
} }
onMounted(() => { onMounted(() => {
loadOverview();
loadStores(); loadStores();
}); });
</script> </script>
@@ -327,9 +353,25 @@ onMounted(() => {
<article class="detail-card detail-card--placeholder"> <article class="detail-card detail-card--placeholder">
<div class="detail-card__header"> <div class="detail-card__header">
<h4>文档概览</h4> <h4>文档概览</h4>
<span>下一批接口补充</span> <span>已对接后端聚合接口</span>
</div> </div>
<el-empty description="文档数量、切片数量、最近上传时间待后端聚合接口补充" /> <el-descriptions :column="2" border>
<el-descriptions-item label="文档总数">
{{ activeStoreDocumentOverview?.documentCount ?? 0 }}
</el-descriptions-item>
<el-descriptions-item label="启用文档数">
{{ activeStoreDocumentOverview?.enabledDocumentCount ?? 0 }}
</el-descriptions-item>
<el-descriptions-item label="已解析文档数">
{{ activeStoreDocumentOverview?.parsedDocumentCount ?? 0 }}
</el-descriptions-item>
<el-descriptions-item label="已索引文档数">
{{ activeStoreDocumentOverview?.indexedDocumentCount ?? 0 }}
</el-descriptions-item>
<el-descriptions-item label="最近上传时间" :span="2">
{{ activeStoreDocumentOverview?.lastUploadTime || '-' }}
</el-descriptions-item>
</el-descriptions>
</article> </article>
<article class="detail-card detail-card--placeholder"> <article class="detail-card detail-card--placeholder">

View File

@@ -3,9 +3,27 @@ import ElementPlus from 'element-plus';
import { describe, expect, it, vi } from 'vitest'; import { describe, expect, it, vi } from 'vitest';
import RagStoresPage from '../RagStoresPage.vue'; import RagStoresPage from '../RagStoresPage.vue';
import { getRagStoreById, queryRagStores, saveRagStore } from '@/api/ragStores'; import {
getRagStoreById,
getRagStoreDocumentOverview,
getRagStoreOverview,
queryRagStores,
saveRagStore,
} from '@/api/ragStores';
vi.mock('@/api/ragStores', () => ({ vi.mock('@/api/ragStores', () => ({
getRagStoreOverview: vi.fn(() =>
Promise.resolve({
resultcode: '0',
message: null,
data: {
totalStores: 2,
totalDocuments: 12,
totalChunks: null,
retrievableStores: 1,
},
}),
),
queryRagStores: vi.fn((query?: { storeName?: string }) => { queryRagStores: vi.fn((query?: { storeName?: string }) => {
const rows = [ const rows = [
{ {
@@ -60,6 +78,32 @@ vi.mock('@/api/ragStores', () => ({
}, },
}), }),
), ),
getRagStoreDocumentOverview: vi.fn((storeId: string) =>
Promise.resolve({
resultcode: '0',
message: null,
data:
storeId === '2'
? {
storeId: '2',
storeName: 'FAQ知识库',
documentCount: 3,
enabledDocumentCount: 1,
parsedDocumentCount: 1,
indexedDocumentCount: 1,
lastUploadTime: '2026-05-21 11:12:00',
}
: {
storeId: '1',
storeName: '产品制度库',
documentCount: 9,
enabledDocumentCount: 8,
parsedDocumentCount: 6,
indexedDocumentCount: 5,
lastUploadTime: '2026-05-21 16:40:00',
},
}),
),
saveRagStore: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })), saveRagStore: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
deleteRagStore: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })), deleteRagStore: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
})); }));
@@ -75,10 +119,16 @@ describe('RagStoresPage', () => {
await flushPromises(); await flushPromises();
expect(wrapper.text()).toContain('知识库总数'); expect(wrapper.text()).toContain('知识库总数');
expect(wrapper.text()).toContain('12');
expect(wrapper.text()).toContain('产品制度库'); expect(wrapper.text()).toContain('产品制度库');
expect(wrapper.text()).toContain('核心制度库'); expect(wrapper.text()).toContain('核心制度库');
expect(wrapper.text()).toContain('文档总数');
expect(wrapper.text()).toContain('最近上传时间');
expect(wrapper.text()).toContain('2026-05-21 16:40:00');
expect(getRagStoreOverview).toHaveBeenCalled();
expect(queryRagStores).toHaveBeenCalled(); expect(queryRagStores).toHaveBeenCalled();
expect(getRagStoreById).toHaveBeenCalledWith('1'); expect(getRagStoreById).toHaveBeenCalledWith('1');
expect(getRagStoreDocumentOverview).toHaveBeenCalledWith('1');
}); });
it('filters stores by name and updates detail when a store is selected', async () => { it('filters stores by name and updates detail when a store is selected', async () => {
@@ -103,7 +153,9 @@ describe('RagStoresPage', () => {
await flushPromises(); await flushPromises();
expect(getRagStoreById).toHaveBeenLastCalledWith('2'); expect(getRagStoreById).toHaveBeenLastCalledWith('2');
expect(getRagStoreDocumentOverview).toHaveBeenLastCalledWith('2');
expect(wrapper.text()).toContain('FAQ 场景知识'); expect(wrapper.text()).toContain('FAQ 场景知识');
expect(wrapper.text()).toContain('3');
}); });
it('submits create form through backend api', async () => { it('submits create form through backend api', async () => {

View File

@@ -3,6 +3,8 @@ 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.request.RagStoreSaveRequest;
import com.bruce.rag.dto.response.RagStoreDocumentOverviewResponse;
import com.bruce.rag.dto.response.RagStoreOverviewResponse;
import com.bruce.rag.dto.response.RagStoreResponse; import com.bruce.rag.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;
@@ -54,6 +56,26 @@ public class RagStoreController {
return RequestResult.success(response); return RequestResult.success(response);
} }
@Operation(summary = "查询知识库总览")
@GetMapping("/overview")
public RequestResult<RagStoreOverviewResponse> overview() {
log.info("RagStoreController.overview start");
RagStoreOverviewResponse response = ragStoreService.getOverview();
log.info("RagStoreController.overview success, totalStores={}, totalDocuments={}",
response.getTotalStores(), response.getTotalDocuments());
return RequestResult.success(response);
}
@Operation(summary = "查询知识库文档概览")
@GetMapping("/documentOverview")
public RequestResult<RagStoreDocumentOverviewResponse> documentOverview(@RequestParam("storeId") Long storeId) {
log.info("RagStoreController.documentOverview start, storeId={}", storeId);
RagStoreDocumentOverviewResponse response = ragStoreService.getDocumentOverview(storeId);
log.info("RagStoreController.documentOverview success, storeId={}, documentCount={}",
storeId, response.getDocumentCount());
return RequestResult.success(response);
}
@Operation(summary = "新增或修改知识库") @Operation(summary = "新增或修改知识库")
@PostMapping("/save") @PostMapping("/save")
public RequestResult<Boolean> saveOrUpdate(@RequestBody RagStoreSaveRequest request) { public RequestResult<Boolean> saveOrUpdate(@RequestBody RagStoreSaveRequest request) {

View File

@@ -0,0 +1,38 @@
package com.bruce.rag.dto.response;
import com.bruce.common.constant.CommonConsts;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.util.Date;
@Data
@Schema(description = "RAG知识库文档概览响应")
public class RagStoreDocumentOverviewResponse {
@Schema(description = "知识库ID")
@JsonSerialize(using = ToStringSerializer.class)
private Long storeId;
@Schema(description = "知识库名称")
private String storeName;
@Schema(description = "文档总数")
private Integer documentCount;
@Schema(description = "启用文档数")
private Integer enabledDocumentCount;
@Schema(description = "已解析文档数")
private Integer parsedDocumentCount;
@Schema(description = "已索引文档数")
private Integer indexedDocumentCount;
@Schema(description = "最近上传时间")
@JsonFormat(pattern = CommonConsts.DATE_FORMAT_LONG_STR, timezone = CommonConsts.TIME_ZONE_GMT8)
private Date lastUploadTime;
}

View File

@@ -0,0 +1,21 @@
package com.bruce.rag.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
@Data
@Schema(description = "RAG知识库总览响应")
public class RagStoreOverviewResponse {
@Schema(description = "知识库总数")
private Integer totalStores;
@Schema(description = "文档总数")
private Integer totalDocuments;
@Schema(description = "切片总数")
private Integer totalChunks;
@Schema(description = "可检索知识库数")
private Integer retrievableStores;
}

View File

@@ -3,6 +3,8 @@ 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.request.RagStoreSaveRequest;
import com.bruce.rag.dto.response.RagStoreDocumentOverviewResponse;
import com.bruce.rag.dto.response.RagStoreOverviewResponse;
import com.bruce.rag.dto.response.RagStoreResponse; import com.bruce.rag.dto.response.RagStoreResponse;
import com.bruce.rag.entity.RagStore; import com.bruce.rag.entity.RagStore;
@@ -16,5 +18,9 @@ public interface IRagStoreService extends IService<RagStore> {
RagStoreResponse getResponseById(Long id); RagStoreResponse getResponseById(Long id);
RagStoreOverviewResponse getOverview();
RagStoreDocumentOverviewResponse getDocumentOverview(Long storeId);
boolean saveOrUpdate(RagStoreSaveRequest request); boolean saveOrUpdate(RagStoreSaveRequest request);
} }

View File

@@ -1,22 +1,36 @@
package com.bruce.rag.service.impl; 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.common.enums.EnableStatusEnum;
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.request.RagStoreSaveRequest;
import com.bruce.rag.dto.response.RagDocumentResponse;
import com.bruce.rag.dto.response.RagStoreDocumentOverviewResponse;
import com.bruce.rag.dto.response.RagStoreOverviewResponse;
import com.bruce.rag.dto.response.RagStoreResponse; import com.bruce.rag.dto.response.RagStoreResponse;
import com.bruce.rag.entity.RagStore; import com.bruce.rag.entity.RagStore;
import com.bruce.rag.enums.RagIndexStatusEnum;
import com.bruce.rag.enums.RagParseStatusEnum;
import com.bruce.rag.mapper.RagStoreMapper; import com.bruce.rag.mapper.RagStoreMapper;
import com.bruce.rag.service.IRagDocumentService;
import com.bruce.rag.service.IRagStoreService; import com.bruce.rag.service.IRagStoreService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils; import org.springframework.util.StringUtils;
import java.util.Comparator;
import java.util.List; import java.util.List;
import java.util.Objects;
@Slf4j @Slf4j
@Service @Service
public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> implements IRagStoreService { public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> implements IRagStoreService {
@Autowired
private IRagDocumentService ragDocumentService;
@Override @Override
public List<RagStoreResponse> listResponses() { public List<RagStoreResponse> listResponses() {
log.info("RagStoreServiceImpl.listResponses start"); log.info("RagStoreServiceImpl.listResponses start");
@@ -47,6 +61,62 @@ public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> i
return response; return response;
} }
@Override
public RagStoreOverviewResponse getOverview() {
log.info("RagStoreServiceImpl.getOverview start");
List<RagStore> stores = list();
List<RagDocumentResponse> documents = ragDocumentService.listResponses();
RagStoreOverviewResponse response = new RagStoreOverviewResponse();
response.setTotalStores(stores.size());
response.setTotalDocuments(documents.size());
response.setTotalChunks(null);
response.setRetrievableStores((int) stores.stream()
.filter(store -> EnableStatusEnum.ENABLED.getLabel().equals(store.getStatus()))
.count());
log.info("RagStoreServiceImpl.getOverview success, totalStores={}, totalDocuments={}, retrievableStores={}",
response.getTotalStores(), response.getTotalDocuments(), response.getRetrievableStores());
return response;
}
@Override
public RagStoreDocumentOverviewResponse getDocumentOverview(Long storeId) {
log.info("RagStoreServiceImpl.getDocumentOverview start, storeId={}", storeId);
if (storeId == null) {
throw new IllegalArgumentException("知识库ID不能为空");
}
RagStore store = getById(storeId);
if (store == null) {
throw new IllegalArgumentException("知识库不存在ID: " + storeId);
}
RagDocumentQueryRequest request = new RagDocumentQueryRequest();
request.setStoreId(storeId);
List<RagDocumentResponse> documents = ragDocumentService.query(request);
RagStoreDocumentOverviewResponse response = new RagStoreDocumentOverviewResponse();
response.setStoreId(storeId);
response.setStoreName(store.getStoreName());
response.setDocumentCount(documents.size());
response.setEnabledDocumentCount((int) documents.stream()
.filter(document -> Boolean.TRUE.equals(document.getEnabled()))
.count());
response.setParsedDocumentCount((int) documents.stream()
.filter(document -> RagParseStatusEnum.PARSED.name().equals(document.getParseStatus()))
.count());
response.setIndexedDocumentCount((int) documents.stream()
.filter(document -> RagIndexStatusEnum.INDEXED.name().equals(document.getIndexStatus()))
.count());
response.setLastUploadTime(documents.stream()
.map(RagDocumentResponse::getCreateTime)
.filter(Objects::nonNull)
.max(Comparator.naturalOrder())
.orElse(null));
log.info("RagStoreServiceImpl.getDocumentOverview success, storeId={}, documentCount={}",
storeId, response.getDocumentCount());
return response;
}
@Override @Override
public boolean saveOrUpdate(RagStoreSaveRequest request) { public boolean saveOrUpdate(RagStoreSaveRequest request) {
log.info("RagStoreServiceImpl.saveOrUpdate start, request={}", request); log.info("RagStoreServiceImpl.saveOrUpdate start, request={}", request);
@@ -62,13 +132,7 @@ public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> i
throw new IllegalArgumentException("知识库编码已存在: " + request.getStoreCode().trim()); throw new IllegalArgumentException("知识库编码已存在: " + request.getStoreCode().trim());
} }
RagStore ragStore = new RagStore(); RagStore ragStore = buildEntity(request);
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); boolean result = super.saveOrUpdate(ragStore);
log.info("RagStoreServiceImpl.saveOrUpdate success, requestId={}, savedId={}, storeCode={}, result={}", log.info("RagStoreServiceImpl.saveOrUpdate success, requestId={}, savedId={}, storeCode={}, result={}",
request.getId(), ragStore.getId(), ragStore.getStoreCode(), result); request.getId(), ragStore.getId(), ragStore.getStoreCode(), result);
@@ -90,6 +154,19 @@ public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> i
request.getId(), request.getStoreCode(), request.getStoreName()); request.getId(), request.getStoreCode(), request.getStoreName());
} }
public RagStore buildEntity(RagStoreSaveRequest request) {
RagStore ragStore = new RagStore();
ragStore.setId(request.getId());
ragStore.setStoreCode(request.getStoreCode().trim());
ragStore.setStoreName(request.getStoreName().trim());
ragStore.setDescription(trimToNull(request.getDescription()));
ragStore.setStatus(StringUtils.hasText(request.getStatus())
? request.getStatus().trim()
: EnableStatusEnum.ENABLED.getLabel());
ragStore.setRemark(trimToNull(request.getRemark()));
return ragStore;
}
private List<RagStoreResponse> toResponses(List<RagStore> stores) { private List<RagStoreResponse> toResponses(List<RagStore> stores) {
return stores.stream() return stores.stream()
.map(RagStoreResponse::fromEntity) .map(RagStoreResponse::fromEntity)

View File

@@ -10,6 +10,8 @@ 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.request.RagStoreSaveRequest;
import com.bruce.rag.dto.response.RagStoreDocumentOverviewResponse;
import com.bruce.rag.dto.response.RagStoreOverviewResponse;
import com.bruce.rag.dto.response.RagDocumentResponse; import com.bruce.rag.dto.response.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;
@@ -21,12 +23,14 @@ import com.bruce.rag.service.IRagStoreService;
import com.bruce.rag.service.impl.RagDocumentServiceImpl; import com.bruce.rag.service.impl.RagDocumentServiceImpl;
import com.bruce.rag.service.impl.RagStoreServiceImpl; import com.bruce.rag.service.impl.RagStoreServiceImpl;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.web.bind.annotation.PostMapping;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.List; import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
class RagComponentStructureTests { class RagComponentStructureTests {
@@ -46,11 +50,15 @@ class RagComponentStructureTests {
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 storeDetailMethod = RagStoreController.class.getMethod("getById", Long.class);
Method storeOverviewMethod = RagStoreController.class.getMethod("overview");
Method storeDocumentOverviewMethod = RagStoreController.class.getMethod("documentOverview", Long.class);
Method storeSaveMethod = RagStoreController.class.getMethod("saveOrUpdate", RagStoreSaveRequest.class); Method storeSaveMethod = RagStoreController.class.getMethod("saveOrUpdate", RagStoreSaveRequest.class);
Method storeDeleteMethod = RagStoreController.class.getMethod("deleteById", Long.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 storeServiceDetailMethod = IRagStoreService.class.getMethod("getResponseById", Long.class);
Method storeServiceOverviewMethod = IRagStoreService.class.getMethod("getOverview");
Method storeServiceDocumentOverviewMethod = IRagStoreService.class.getMethod("getDocumentOverview", Long.class);
Method storeServiceSaveMethod = IRagStoreService.class.getMethod("saveOrUpdate", RagStoreSaveRequest.class); Method storeServiceSaveMethod = IRagStoreService.class.getMethod("saveOrUpdate", RagStoreSaveRequest.class);
Method documentListMethod = RagDocumentController.class.getMethod("list"); Method documentListMethod = RagDocumentController.class.getMethod("list");
@@ -61,16 +69,22 @@ 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, storeDetailMethod.getReturnType());
assertEquals(RequestResult.class, storeOverviewMethod.getReturnType());
assertEquals(RequestResult.class, storeDocumentOverviewMethod.getReturnType());
assertEquals(RequestResult.class, storeSaveMethod.getReturnType()); assertEquals(RequestResult.class, storeSaveMethod.getReturnType());
assertEquals(RequestResult.class, storeDeleteMethod.getReturnType()); assertEquals(RequestResult.class, storeDeleteMethod.getReturnType());
assertEquals(List.class, storeServiceQueryMethod.getReturnType()); assertEquals(List.class, storeServiceQueryMethod.getReturnType());
assertEquals(RagStoreResponse.class, storeServiceDetailMethod.getReturnType()); assertEquals(RagStoreResponse.class, storeServiceDetailMethod.getReturnType());
assertEquals(RagStoreOverviewResponse.class, storeServiceOverviewMethod.getReturnType());
assertEquals(RagStoreDocumentOverviewResponse.class, storeServiceDocumentOverviewMethod.getReturnType());
assertEquals(boolean.class, storeServiceSaveMethod.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")); assertTrue(storeDetailMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
assertTrue(storeOverviewMethod.getGenericReturnType().getTypeName().contains("RagStoreOverviewResponse"));
assertTrue(storeDocumentOverviewMethod.getGenericReturnType().getTypeName().contains("RagStoreDocumentOverviewResponse"));
assertEquals(RagStoreResponse.class, RagStoreResponse.class.getMethod("fromEntity", RagStore.class).getReturnType()); assertEquals(RagStoreResponse.class, RagStoreResponse.class.getMethod("fromEntity", RagStore.class).getReturnType());
assertEquals(RequestResult.class, documentListMethod.getReturnType()); assertEquals(RequestResult.class, documentListMethod.getReturnType());
@@ -83,6 +97,16 @@ class RagComponentStructureTests {
assertEquals(RagDocumentResponse.class, RagDocumentResponse.class.getMethod("fromEntity", RagDocument.class).getReturnType()); assertEquals(RagDocumentResponse.class, RagDocumentResponse.class.getMethod("fromEntity", RagDocument.class).getReturnType());
} }
@Test
void ragDocumentListUrlShouldUseExplicitListAction() throws NoSuchMethodException {
Method documentListMethod = RagDocumentController.class.getMethod("list");
PostMapping postMapping = documentListMethod.getAnnotation(PostMapping.class);
assertNotNull(postMapping);
assertEquals("/list", postMapping.value()[0]);
}
@Test @Test
void ragSourceTypesAndDocumentRelationShouldExist() throws NoSuchFieldException { void ragSourceTypesAndDocumentRelationShouldExist() throws NoSuchFieldException {
Field storeIdField = RagDocument.class.getDeclaredField("storeId"); Field storeIdField = RagDocument.class.getDeclaredField("storeId");
@@ -90,6 +114,7 @@ class RagComponentStructureTests {
assertEquals("RAG_STORE", RagSystemConstants.RAG_STORE); assertEquals("RAG_STORE", RagSystemConstants.RAG_STORE);
assertEquals("RAG_DOCUMENT", RagSystemConstants.RAG_DOCUMENT); assertEquals("RAG_DOCUMENT", RagSystemConstants.RAG_DOCUMENT);
assertEquals("RAG", RagSystemConstants.SOURCE_TYPE_RAG);
assertEquals(Long.class, storeIdField.getType()); assertEquals(Long.class, storeIdField.getType());
assertEquals(Long.class, attachmentIdField.getType()); assertEquals(Long.class, attachmentIdField.getType());
assertTrue(RagStore.class.getSimpleName().contains("RagStore")); assertTrue(RagStore.class.getSimpleName().contains("RagStore"));

View File

@@ -0,0 +1,126 @@
package com.bruce.rag;
import com.bruce.common.enums.EnableStatusEnum;
import com.bruce.rag.dto.response.RagDocumentResponse;
import com.bruce.rag.dto.response.RagStoreDocumentOverviewResponse;
import com.bruce.rag.dto.response.RagStoreOverviewResponse;
import com.bruce.rag.entity.RagStore;
import com.bruce.rag.enums.RagIndexStatusEnum;
import com.bruce.rag.enums.RagParseStatusEnum;
import com.bruce.rag.service.IRagDocumentService;
import com.bruce.rag.service.impl.RagStoreServiceImpl;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Spy;
import org.mockito.junit.jupiter.MockitoExtension;
import java.util.Date;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.when;
@ExtendWith(MockitoExtension.class)
class RagStoreOverviewServiceTests {
@Spy
@InjectMocks
private RagStoreServiceImpl ragStoreService;
@Mock
private IRagDocumentService ragDocumentService;
@Test
void getOverviewShouldAggregateStoreAndDocumentCounts() {
RagStore enabledStore = new RagStore();
enabledStore.setId(1L);
enabledStore.setStatus(EnableStatusEnum.ENABLED.getLabel());
RagStore disabledStore = new RagStore();
disabledStore.setId(2L);
disabledStore.setStatus("停用");
when(ragDocumentService.listResponses()).thenReturn(List.of(
createDocumentResponse("11", "1", true, RagParseStatusEnum.UPLOADED.name(), RagIndexStatusEnum.PENDING.name(), new Date()),
createDocumentResponse("22", "2", false, RagParseStatusEnum.PARSED.name(), RagIndexStatusEnum.INDEXED.name(), new Date())
));
doReturn(List.of(enabledStore, disabledStore)).when(ragStoreService).list();
RagStoreOverviewResponse response = ragStoreService.getOverview();
assertEquals(2, response.getTotalStores());
assertEquals(2, response.getTotalDocuments());
assertNull(response.getTotalChunks());
assertEquals(1, response.getRetrievableStores());
}
@Test
void getDocumentOverviewShouldAggregateCurrentStoreDocumentMetrics() {
RagStore store = new RagStore();
store.setId(1L);
store.setStoreName("产品制度库");
doReturn(store).when(ragStoreService).getById(1L);
when(ragDocumentService.query(org.mockito.ArgumentMatchers.any())).thenReturn(List.of(
createDocumentResponse("11", "1", true, RagParseStatusEnum.UPLOADED.name(), RagIndexStatusEnum.PENDING.name(), new Date(1747816496000L)),
createDocumentResponse("12", "1", true, RagParseStatusEnum.PARSED.name(), RagIndexStatusEnum.INDEXED.name(), new Date(1747820096000L)),
createDocumentResponse("13", "1", false, RagParseStatusEnum.FAILED.name(), RagIndexStatusEnum.FAILED.name(), new Date(1747812896000L))
));
RagStoreDocumentOverviewResponse response = ragStoreService.getDocumentOverview(1L);
assertEquals(1L, response.getStoreId());
assertEquals("产品制度库", response.getStoreName());
assertEquals(3, response.getDocumentCount());
assertEquals(2, response.getEnabledDocumentCount());
assertEquals(1, response.getParsedDocumentCount());
assertEquals(1, response.getIndexedDocumentCount());
assertEquals(new Date(1747820096000L), response.getLastUploadTime());
}
@Test
void getDocumentOverviewShouldQueryDocumentsByStoreIdOnly() {
RagStore store = new RagStore();
store.setId(1L);
store.setStoreName("产品制度库");
doReturn(store).when(ragStoreService).getById(1L);
when(ragDocumentService.query(org.mockito.ArgumentMatchers.any())).thenReturn(List.of());
ragStoreService.getDocumentOverview(1L);
org.mockito.ArgumentCaptor<com.bruce.rag.dto.request.RagDocumentQueryRequest> captor =
org.mockito.ArgumentCaptor.forClass(com.bruce.rag.dto.request.RagDocumentQueryRequest.class);
org.mockito.Mockito.verify(ragDocumentService).query(captor.capture());
assertEquals(1L, captor.getValue().getStoreId());
assertNull(captor.getValue().getParseStatus());
assertNull(captor.getValue().getIndexStatus());
}
@Test
void getDocumentOverviewShouldRejectUnknownStore() {
doReturn(null).when(ragStoreService).getById(999L);
assertThrows(IllegalArgumentException.class, () -> ragStoreService.getDocumentOverview(999L));
}
private RagDocumentResponse createDocumentResponse(
String id,
String storeId,
boolean enabled,
String parseStatus,
String indexStatus,
Date createTime
) {
RagDocumentResponse response = new RagDocumentResponse();
response.setId(Long.valueOf(id));
response.setStoreId(Long.valueOf(storeId));
response.setEnabled(enabled);
response.setParseStatus(parseStatus);
response.setIndexStatus(indexStatus);
response.setCreateTime(createTime);
return response;
}
}