From 91e6d5bdd356770e990e2bce09945eaa7ccf5b52 Mon Sep 17 00:00:00 2001 From: "zhiye.sun" Date: Thu, 21 May 2026 13:09:33 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E5=BA=93=E7=AE=A1=E7=90=86=E9=A1=B5=E9=9D=A2=E5=B9=B6=E8=81=94?= =?UTF-8?q?=E8=B0=83=E7=9F=A5=E8=AF=86=E5=BA=93=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/api/ragStores.ts | 44 ++ frontend/src/pages/RagStoresPage.vue | 668 +++++++++++++++++- .../src/pages/__tests__/RagStoresPage.spec.ts | 132 ++++ rag-store-page-apis.md | 434 ++++++++++++ .../rag/controller/RagStoreController.java | 48 +- .../rag/dto/request/RagStoreSaveRequest.java | 27 + .../rag/dto/response/RagStoreResponse.java | 11 + .../bruce/rag/service/IRagStoreService.java | 5 + .../rag/service/impl/RagStoreServiceImpl.java | 81 ++- .../bruce/rag/RagComponentStructureTests.java | 12 + .../RagStoreResponseSerializationTests.java | 22 + .../rag/RagStoreSaveValidationTests.java | 39 + 12 files changed, 1506 insertions(+), 17 deletions(-) create mode 100644 frontend/src/api/ragStores.ts create mode 100644 frontend/src/pages/__tests__/RagStoresPage.spec.ts create mode 100644 rag-store-page-apis.md create mode 100644 src/main/java/com/bruce/rag/dto/request/RagStoreSaveRequest.java create mode 100644 src/test/java/com/bruce/rag/RagStoreResponseSerializationTests.java create mode 100644 src/test/java/com/bruce/rag/RagStoreSaveValidationTests.java diff --git a/frontend/src/api/ragStores.ts b/frontend/src/api/ragStores.ts new file mode 100644 index 0000000..aa3f3db --- /dev/null +++ b/frontend/src/api/ragStores.ts @@ -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('/rag/store/list'); +} + +export function queryRagStores(query?: RagStoreQueryRequest) { + return post('/rag/store/query', query); +} + +export function getRagStoreById(id: string) { + return get('/rag/store/detail', { + params: { id }, + }); +} + +export function saveRagStore(data: RagStoreSaveRequest) { + return post('/rag/store/save', data); +} + +export function deleteRagStore(id: string) { + return post('/rag/store/delete', undefined, { + params: { id }, + }); +} diff --git a/frontend/src/pages/RagStoresPage.vue b/frontend/src/pages/RagStoresPage.vue index 9ef6124..8cefdb0 100644 --- a/frontend/src/pages/RagStoresPage.vue +++ b/frontend/src/pages/RagStoresPage.vue @@ -1,8 +1,668 @@ + + + + diff --git a/frontend/src/pages/__tests__/RagStoresPage.spec.ts b/frontend/src/pages/__tests__/RagStoresPage.spec.ts new file mode 100644 index 0000000..9ab54e7 --- /dev/null +++ b/frontend/src/pages/__tests__/RagStoresPage.spec.ts @@ -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: '新建知识库', + }), + ); + }); +}); diff --git a/rag-store-page-apis.md b/rag-store-page-apis.md new file mode 100644 index 0000000..81b7ef2 --- /dev/null +++ b/rag-store-page-apis.md @@ -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>` + +当前字段: + +- `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` + +### 2.4 新增或修改知识库 + +- `POST /api/rag/stores` + +请求体: + +```json +{ + "id": 1, + "storeCode": "PROD_DOC", + "storeName": "产品制度库", + "description": "产品制度、业务规范、流程材料", + "status": "启用", + "remark": "核心制度库" +} +``` + +返回类型: + +- `RequestResult` + +说明: + +- `id` 为空时新增 +- `id` 不为空时修改 + +### 2.5 删除知识库 + +- `DELETE /api/rag/stores/{id}` + +返回类型: + +- `RequestResult` + +## 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` diff --git a/src/main/java/com/bruce/rag/controller/RagStoreController.java b/src/main/java/com/bruce/rag/controller/RagStoreController.java index a8797dc..eb50853 100644 --- a/src/main/java/com/bruce/rag/controller/RagStoreController.java +++ b/src/main/java/com/bruce/rag/controller/RagStoreController.java @@ -2,36 +2,74 @@ package com.bruce.rag.controller; import com.bruce.common.domain.model.RequestResult; import com.bruce.rag.dto.request.RagStoreQueryRequest; +import com.bruce.rag.dto.request.RagStoreSaveRequest; import com.bruce.rag.dto.response.RagStoreResponse; import com.bruce.rag.service.IRagStoreService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; @Tag(name = "RAG知识库管理") +@Slf4j @RestController -@RequestMapping("/api/rag/stores") +@RequestMapping("/api/rag/store") public class RagStoreController { @Autowired private IRagStoreService ragStoreService; @Operation(summary = "查询全部知识库") - @GetMapping + @PostMapping("/list") public RequestResult> list() { - return RequestResult.success(ragStoreService.listResponses()); + log.info("RagStoreController.list start"); + List responses = ragStoreService.listResponses(); + log.info("RagStoreController.list success, count={}", responses.size()); + return RequestResult.success(responses); } @Operation(summary = "按条件查询知识库") @PostMapping("/query") - public RequestResult> query(@RequestBody RagStoreQueryRequest request) { - return RequestResult.success(ragStoreService.query(request)); + public RequestResult> query(@RequestBody(required = false) RagStoreQueryRequest request) { + log.info("RagStoreController.query start, request={}", request); + List responses = ragStoreService.query(request); + log.info("RagStoreController.query success, count={}", responses.size()); + return RequestResult.success(responses); + } + + @Operation(summary = "查询知识库详情") + @GetMapping("/detail") + public RequestResult 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 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 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); } } diff --git a/src/main/java/com/bruce/rag/dto/request/RagStoreSaveRequest.java b/src/main/java/com/bruce/rag/dto/request/RagStoreSaveRequest.java new file mode 100644 index 0000000..5753381 --- /dev/null +++ b/src/main/java/com/bruce/rag/dto/request/RagStoreSaveRequest.java @@ -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; +} diff --git a/src/main/java/com/bruce/rag/dto/response/RagStoreResponse.java b/src/main/java/com/bruce/rag/dto/response/RagStoreResponse.java index 95877e8..7d0e776 100644 --- a/src/main/java/com/bruce/rag/dto/response/RagStoreResponse.java +++ b/src/main/java/com/bruce/rag/dto/response/RagStoreResponse.java @@ -1,14 +1,19 @@ 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 io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import org.springframework.beans.BeanUtils; +import java.util.Date; + @Data @Schema(description = "RAG知识库响应") public class RagStoreResponse { + @JsonSerialize(using = ToStringSerializer.class) @Schema(description = "主键ID") private Long id; @@ -27,6 +32,12 @@ public class RagStoreResponse { @Schema(description = "备注") private String remark; + @Schema(description = "创建时间") + private Date createTime; + + @Schema(description = "更新时间") + private Date updateTime; + public static RagStoreResponse fromEntity(RagStore entity) { if (entity == null) { return null; diff --git a/src/main/java/com/bruce/rag/service/IRagStoreService.java b/src/main/java/com/bruce/rag/service/IRagStoreService.java index 8ec3a4e..617d43c 100644 --- a/src/main/java/com/bruce/rag/service/IRagStoreService.java +++ b/src/main/java/com/bruce/rag/service/IRagStoreService.java @@ -2,6 +2,7 @@ package com.bruce.rag.service; import com.baomidou.mybatisplus.extension.service.IService; import com.bruce.rag.dto.request.RagStoreQueryRequest; +import com.bruce.rag.dto.request.RagStoreSaveRequest; import com.bruce.rag.dto.response.RagStoreResponse; import com.bruce.rag.entity.RagStore; @@ -12,4 +13,8 @@ public interface IRagStoreService extends IService { List listResponses(); List query(RagStoreQueryRequest request); + + RagStoreResponse getResponseById(Long id); + + boolean saveOrUpdate(RagStoreSaveRequest request); } diff --git a/src/main/java/com/bruce/rag/service/impl/RagStoreServiceImpl.java b/src/main/java/com/bruce/rag/service/impl/RagStoreServiceImpl.java index 986b9c0..69f3ce0 100644 --- a/src/main/java/com/bruce/rag/service/impl/RagStoreServiceImpl.java +++ b/src/main/java/com/bruce/rag/service/impl/RagStoreServiceImpl.java @@ -2,34 +2,92 @@ package com.bruce.rag.service.impl; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; 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.entity.RagStore; import com.bruce.rag.mapper.RagStoreMapper; import com.bruce.rag.service.IRagStoreService; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import java.util.List; +@Slf4j @Service public class RagStoreServiceImpl extends ServiceImpl implements IRagStoreService { @Override public List listResponses() { - return toResponses(list()); + log.info("RagStoreServiceImpl.listResponses start"); + List responses = toResponses(list()); + log.info("RagStoreServiceImpl.listResponses success, count={}", responses.size()); + return responses; } @Override public List query(RagStoreQueryRequest request) { - if (request == null) { - throw new IllegalArgumentException("查询请求不能为空"); - } - return toResponses(lambdaQuery() - .eq(StringUtils.hasText(request.getStoreCode()), RagStore::getStoreCode, request.getStoreCode()) - .like(StringUtils.hasText(request.getStoreName()), RagStore::getStoreName, request.getStoreName()) - .eq(StringUtils.hasText(request.getStatus()), RagStore::getStatus, request.getStatus()) + log.info("RagStoreServiceImpl.query start, request={}", request); + RagStoreQueryRequest queryRequest = request == null ? new RagStoreQueryRequest() : request; + List responses = toResponses(lambdaQuery() + .eq(StringUtils.hasText(queryRequest.getStoreCode()), RagStore::getStoreCode, queryRequest.getStoreCode()) + .like(StringUtils.hasText(queryRequest.getStoreName()), RagStore::getStoreName, queryRequest.getStoreName()) + .eq(StringUtils.hasText(queryRequest.getStatus()), RagStore::getStatus, queryRequest.getStatus()) .orderByAsc(RagStore::getStoreCode) .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 toResponses(List stores) { @@ -37,4 +95,11 @@ public class RagStoreServiceImpl extends ServiceImpl i .map(RagStoreResponse::fromEntity) .toList(); } + + private String trimToNull(String value) { + if (!StringUtils.hasText(value)) { + return null; + } + return value.trim(); + } } diff --git a/src/test/java/com/bruce/rag/RagComponentStructureTests.java b/src/test/java/com/bruce/rag/RagComponentStructureTests.java index 6e31d57..9926ae7 100644 --- a/src/test/java/com/bruce/rag/RagComponentStructureTests.java +++ b/src/test/java/com/bruce/rag/RagComponentStructureTests.java @@ -9,6 +9,7 @@ import com.bruce.rag.controller.RagDocumentController; import com.bruce.rag.controller.RagStoreController; import com.bruce.rag.dto.request.RagDocumentQueryRequest; import com.bruce.rag.dto.request.RagStoreQueryRequest; +import com.bruce.rag.dto.request.RagStoreSaveRequest; import com.bruce.rag.dto.response.RagDocumentResponse; import com.bruce.rag.dto.response.RagStoreResponse; import com.bruce.rag.entity.RagDocument; @@ -44,8 +45,13 @@ class RagComponentStructureTests { void ragControllersShouldExposeRequestResultAndQueryDtoMethods() throws NoSuchMethodException { Method storeListMethod = RagStoreController.class.getMethod("list"); 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 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 documentQueryMethod = RagDocumentController.class.getMethod("query", RagDocumentQueryRequest.class); @@ -54,11 +60,17 @@ class RagComponentStructureTests { assertEquals(RequestResult.class, storeListMethod.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(RagStoreResponse.class, storeServiceDetailMethod.getReturnType()); + assertEquals(boolean.class, storeServiceSaveMethod.getReturnType()); assertTrue(storeResponseListMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse")); assertTrue(storeServiceQueryMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse")); assertTrue(storeListMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse")); assertTrue(storeQueryMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse")); + assertTrue(storeDetailMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse")); assertEquals(RagStoreResponse.class, RagStoreResponse.class.getMethod("fromEntity", RagStore.class).getReturnType()); assertEquals(RequestResult.class, documentListMethod.getReturnType()); diff --git a/src/test/java/com/bruce/rag/RagStoreResponseSerializationTests.java b/src/test/java/com/bruce/rag/RagStoreResponseSerializationTests.java new file mode 100644 index 0000000..0091fe6 --- /dev/null +++ b/src/test/java/com/bruce/rag/RagStoreResponseSerializationTests.java @@ -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\"")); + } +} diff --git a/src/test/java/com/bruce/rag/RagStoreSaveValidationTests.java b/src/test/java/com/bruce/rag/RagStoreSaveValidationTests.java new file mode 100644 index 0000000..d661dbc --- /dev/null +++ b/src/test/java/com/bruce/rag/RagStoreSaveValidationTests.java @@ -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)); + } +}