Compare commits
4 Commits
387681a6ab
...
67cfbeb572
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
67cfbeb572 | ||
|
|
088853b098 | ||
|
|
1ada88c02a | ||
|
|
91e6d5bdd3 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -31,5 +31,4 @@ build/
|
|||||||
|
|
||||||
### VS Code ###
|
### VS Code ###
|
||||||
.vscode/
|
.vscode/
|
||||||
/docs/
|
|
||||||
src/main/resources/application-dev.yaml
|
src/main/resources/application-dev.yaml
|
||||||
|
|||||||
121
AGENT.md
121
AGENT.md
@@ -11,22 +11,22 @@
|
|||||||
- 文件上传与附件管理
|
- 文件上传与附件管理
|
||||||
- 前后端统一的管理控制台
|
- 前后端统一的管理控制台
|
||||||
|
|
||||||
当前阶段以“先搭平台骨架,再逐步补智能能力”为主,优先保证工程结构、接口规范、知识库链路和可扩展性。
|
当前阶段以"先搭平台骨架,再逐步补智能能力"为主,优先保证工程结构、接口规范、知识库链路和可扩展性。
|
||||||
|
|
||||||
## 2. 总体设计思路
|
## 2. 总体设计思路
|
||||||
|
|
||||||
平台整体按“接入层 - 应用层 - 领域层 - 基础设施层”拆分:
|
平台整体按"接入层 - 应用层 - 领域层 - 基础设施层"拆分:
|
||||||
|
|
||||||
- 接入层
|
- **接入层**
|
||||||
提供 REST API、后续可扩展 WebSocket / SSE,用于前端控制台和外部系统接入。
|
提供 REST API,后续可扩展 WebSocket / SSE,用于前端控制台和外部系统接入。
|
||||||
|
|
||||||
- 应用层
|
- **应用层**
|
||||||
负责请求编排、DTO 转换、统一返回体、会话协调和 Agent 调度入口。
|
负责请求编排、DTO 转换、统一返回体、会话协调和 Agent 调度入口。
|
||||||
|
|
||||||
- 领域层
|
- **领域层**
|
||||||
承载核心业务对象,如系统枚举、附件、知识库、知识文档、Agent 配置、任务执行记录等。
|
承载核心业务对象,如系统枚举、附件、知识库、知识文档、Agent 配置、任务执行记录等。
|
||||||
|
|
||||||
- 基础设施层
|
- **基础设施层**
|
||||||
负责数据库访问、文件存储、模型调用、向量检索、日志、缓存和第三方工具适配。
|
负责数据库访问、文件存储、模型调用、向量检索、日志、缓存和第三方工具适配。
|
||||||
|
|
||||||
## 3. 核心模块规划
|
## 3. 核心模块规划
|
||||||
@@ -35,18 +35,22 @@
|
|||||||
|
|
||||||
用于支撑整个平台的通用能力:
|
用于支撑整个平台的通用能力:
|
||||||
|
|
||||||
- `sys_enum`:系统枚举配置
|
- `sys_enum`:系统枚举配置(已完成 CRUD、批量新增、管理端查询)
|
||||||
- `sys_attachment`:附件与文件上传
|
- `sys_attachment`:附件与文件上传(已完成本地上传、元数据持久化)
|
||||||
- 统一 DTO / `RequestResult`
|
- 统一 DTO / `RequestResult`(已完成)
|
||||||
- 通用状态枚举、启用禁用枚举
|
- 通用状态枚举、启用禁用枚举(已完成)
|
||||||
|
- 全局异常处理 `GlobalExceptionHandler`(已完成)
|
||||||
|
- 公共审计字段自动填充 `EntityAuditMetaObjectHandler`(已完成)
|
||||||
- 后续可补用户、权限、审计等基础能力
|
- 后续可补用户、权限、审计等基础能力
|
||||||
|
|
||||||
### 3.2 RAG 知识库模块
|
### 3.2 RAG 知识库模块
|
||||||
|
|
||||||
当前已经有初步表设计与 Java 骨架:
|
当前已有完整的元数据管理层:
|
||||||
|
|
||||||
- `rag_store`:知识库主表
|
- `rag_store`:知识库主表(已完成 CRUD、编码唯一性校验)
|
||||||
- `rag_document`:知识库文档表
|
- `rag_document`:知识库文档表(已完成实体、Mapper、Service、条件查询)
|
||||||
|
- RAG 解析状态枚举 `RagParseStatusEnum`(已完成)
|
||||||
|
- RAG 索引状态枚举 `RagIndexStatusEnum`(已完成)
|
||||||
|
|
||||||
后续计划继续扩展:
|
后续计划继续扩展:
|
||||||
|
|
||||||
@@ -58,7 +62,7 @@
|
|||||||
当前设计原则:
|
当前设计原则:
|
||||||
|
|
||||||
- 文件物理信息放在 `sys_attachment`
|
- 文件物理信息放在 `sys_attachment`
|
||||||
- 业务归属关系通过 `source_type`、`source_id` 或文档关联字段承接
|
- 业务归属关系通过 `storeId`、`attachmentId` 关联
|
||||||
- RAG 领域代码独立放在 `com.bruce.rag`
|
- RAG 领域代码独立放在 `com.bruce.rag`
|
||||||
|
|
||||||
### 3.3 Agent 运行模块
|
### 3.3 Agent 运行模块
|
||||||
@@ -82,52 +86,61 @@
|
|||||||
|
|
||||||
### 3.4 管理控制台模块
|
### 3.4 管理控制台模块
|
||||||
|
|
||||||
当前已经建立基于 Vue 3、Vite、Element Plus 的前端控制台基础骨架。
|
当前已建立基于 Vue 3、Vite、TypeScript、Element Plus 的前端控制台。
|
||||||
|
|
||||||
已具备的页面与布局:
|
已具备的页面与布局:
|
||||||
|
|
||||||
- 左侧管理菜单与品牌区
|
- 左侧管理菜单与品牌区(232px 侧边栏)
|
||||||
- 工作台入口
|
- 工作台(占位)
|
||||||
- 系统枚举管理页
|
- 系统枚举管理页(完整 CRUD + 批量新增)
|
||||||
- 附件管理入口
|
- 附件管理入口(占位)
|
||||||
- 知识库入口
|
- 知识库管理页(完整 CRUD + 双栏详情)
|
||||||
- 知识文档入口
|
- 知识文档入口(占位)
|
||||||
|
|
||||||
|
前端技术要点:
|
||||||
|
|
||||||
|
- Composition API + `<script setup lang="ts">`
|
||||||
|
- Axios 封装,统一 `/api` 前缀,`RequestResult<T>` 信封解包
|
||||||
|
- Long 类型安全解析(`json-bigint` 防止 JS 精度丢失)
|
||||||
|
- 全局错误拦截与 Element Plus 弹窗提示
|
||||||
|
- Vitest + @vue/test-utils 单元测试
|
||||||
|
|
||||||
当前样式约定:
|
当前样式约定:
|
||||||
|
|
||||||
- 管理控制台定位为后台工具界面,优先保证信息密度、可扫描性和稳定布局。
|
- 管理控制台定位为后台工具界面,优先保证信息密度、可扫描性和稳定布局。
|
||||||
- 主内容区域只保留页面自身标题,避免外层布局和页面面板重复展示标题。
|
- 主内容区域只保留页面自身标题,避免外层布局和页面面板重复展示标题。
|
||||||
- 页面统一使用 `page-panel` 作为内容容器,侧边栏、页面面板、工具栏和表格使用统一边框、背景和主色变量。
|
- 页面统一使用 `page-panel` 作为内容容器,侧边栏、页面面板、工具栏和表格使用统一边框、背景和主色变量。
|
||||||
- 系统枚举页工具栏采用响应式布局,桌面端保持查询项和操作按钮分区,窄屏时纵向排列。
|
- 全局样式集中在 `frontend/src/styles/global.css`,页面专属样式在对应 `.vue` 文件的 scoped style 中。
|
||||||
|
|
||||||
后续控制台至少继续覆盖:
|
后续控制台至少继续覆盖:
|
||||||
|
|
||||||
- 枚举管理
|
- 附件管理页面前端联调
|
||||||
- 附件管理
|
- 知识文档管理页面前端联调
|
||||||
- 知识库管理
|
|
||||||
- 文档上传与状态查看
|
|
||||||
- Agent 调试页
|
- Agent 调试页
|
||||||
- 执行日志查看
|
- 执行日志查看
|
||||||
|
|
||||||
## 4. 当前接口设计原则
|
## 4. 当前接口设计原则
|
||||||
|
|
||||||
项目后续统一遵循这些规则:
|
项目统一遵循这些规则:
|
||||||
|
|
||||||
1. `controller` 不直接暴露实体类
|
1. `controller` 不直接暴露实体类
|
||||||
所有请求和响应优先走 DTO。
|
所有请求和响应走 DTO。
|
||||||
|
|
||||||
2. `service` 尽量以 DTO 为边界
|
2. `service` 以 DTO 为边界
|
||||||
持久化实体只在内部流转,不直接穿透到外层接口。
|
持久化实体只在内部流转,不直接穿透到外层接口。
|
||||||
|
|
||||||
3. 查询条件不直接使用多个裸参数
|
3. 查询条件使用请求 DTO
|
||||||
尽量改成 `QueryRequest` / `SaveRequest` / `Response` 形式。
|
统一使用 `QueryRequest` / `SaveRequest` / `Response` 形式。
|
||||||
|
|
||||||
4. 统一返回体
|
4. 统一返回体
|
||||||
使用 `RequestResult<T>` 作为标准响应包装。
|
使用 `RequestResult<T>` 作为标准响应包装。
|
||||||
|
|
||||||
5. 基础枚举统一化
|
5. 基础枚举统一化
|
||||||
通用状态、启用禁用、RAG 解析/索引状态等统一管理。
|
通用状态、启用禁用、RAG 解析/索引状态等统一管理。
|
||||||
|
|
||||||
|
6. OpenAPI 注解覆盖
|
||||||
|
所有 Controller、DTO 使用 `@Tag`、`@Operation`、`@Schema` 注解。
|
||||||
|
|
||||||
## 5. 数据与存储设计
|
## 5. 数据与存储设计
|
||||||
|
|
||||||
### 5.1 关系型数据库
|
### 5.1 关系型数据库
|
||||||
@@ -138,7 +151,8 @@
|
|||||||
|
|
||||||
- 业务表采用 PostgreSQL 规范 SQL
|
- 业务表采用 PostgreSQL 规范 SQL
|
||||||
- 主键使用 MyBatis-Plus `ASSIGN_ID`
|
- 主键使用 MyBatis-Plus `ASSIGN_ID`
|
||||||
- 通用字段沉淀到 `BaseEntity`
|
- 通用字段沉淀到 `BaseEntity`(含审计字段 + 乐观锁 `version`)
|
||||||
|
- 审计字段通过 `EntityAuditMetaObjectHandler` 自动填充
|
||||||
|
|
||||||
### 5.2 向量能力
|
### 5.2 向量能力
|
||||||
|
|
||||||
@@ -152,7 +166,7 @@
|
|||||||
|
|
||||||
### 5.3 文件存储
|
### 5.3 文件存储
|
||||||
|
|
||||||
当前先走本地文件存储,后续可抽象成:
|
当前使用本地文件存储(`AttachmentProperties.basePath` 默认 `data/attachments`),后续可抽象成:
|
||||||
|
|
||||||
- 本地文件系统
|
- 本地文件系统
|
||||||
- MinIO / S3
|
- MinIO / S3
|
||||||
@@ -160,35 +174,28 @@
|
|||||||
|
|
||||||
## 6. 当前阶段开发优先级
|
## 6. 当前阶段开发优先级
|
||||||
|
|
||||||
建议优先顺序如下:
|
1. ~~统一接口层规范~~ DTO、返回体、基础校验、通用异常处理(已完成)
|
||||||
|
2. ~~收紧基础模块~~ `sys_enum`、`sys_attachment`(已完成)
|
||||||
1. 统一接口层规范
|
3. ~~补全 RAG 基础元数据管理~~ `rag_store`、`rag_document`(已完成)
|
||||||
DTO、返回体、基础校验、通用异常处理
|
|
||||||
|
|
||||||
2. 收紧基础模块
|
|
||||||
`sys_enum`、`sys_attachment`
|
|
||||||
|
|
||||||
3. 补全 RAG 基础业务闭环
|
|
||||||
`rag_store`、`rag_document`、文件归属、文档状态
|
|
||||||
|
|
||||||
4. 接入 Spring AI
|
4. 接入 Spring AI
|
||||||
|
|
||||||
5. 建立 Agent 运行时骨架
|
5. 建立 Agent 运行时骨架
|
||||||
|
6. ~~补前端控制台基础骨架~~(已完成,部分页面待联调)
|
||||||
|
|
||||||
6. 补前端控制台
|
剩余重点:
|
||||||
|
|
||||||
当前前端控制台已经完成基础骨架和系统枚举页面样式优化,后续重点应转向附件、知识库、知识文档页面的业务闭环。
|
- 完善 RAG 文档上传、解析、索引的业务闭环
|
||||||
|
- 补齐前端附件管理、知识文档页面的表单与接口联调
|
||||||
|
- 接入 Spring AI 并实现模型调用链路
|
||||||
|
|
||||||
## 7. 下一步建议
|
## 7. 下一步建议
|
||||||
|
|
||||||
结合当前代码状态,接下来建议重点做:
|
结合当前代码状态,接下来建议重点做:
|
||||||
|
|
||||||
- 完成现有三块接口 DTO 化改造
|
- 实现知识库文档上传并自动创建 `rag_document` 记录
|
||||||
- 建立统一异常处理和错误码规范
|
- 建立文档解析任务入口与状态流转
|
||||||
- 完善 `rag_store` / `rag_document` 的增删改查
|
|
||||||
- 增加知识库文档上传并自动关联附件
|
|
||||||
- 为后续切片与向量化预留任务入口
|
- 为后续切片与向量化预留任务入口
|
||||||
- 补齐前端附件、知识库、知识文档页面的表单、列表和接口联调
|
- 补齐前端附件管理、知识文档页面的联调
|
||||||
|
- 接入 Spring AI,实现最小模型调用链路
|
||||||
|
|
||||||
## 8. 文档用途说明
|
## 8. 文档用途说明
|
||||||
|
|
||||||
@@ -203,4 +210,4 @@
|
|||||||
- `agent-runtime.md`
|
- `agent-runtime.md`
|
||||||
- `rag-design.md`
|
- `rag-design.md`
|
||||||
- `api-style.md`
|
- `api-style.md`
|
||||||
- `frontend-console.md`
|
- `frontend-console.md`
|
||||||
116
README.md
116
README.md
@@ -3,8 +3,8 @@
|
|||||||
Common Agent 是一个规划中的通用 Agent 平台,技术路线基于 Java、Spring Boot 和 Spring AI。
|
Common Agent 是一个规划中的通用 Agent 平台,技术路线基于 Java、Spring Boot 和 Spring AI。
|
||||||
项目目标是建设一套完整的前后端系统,支持 Agent 编排、工具调用、会话管理、RAG 知识库和平台管理能力。
|
项目目标是建设一套完整的前后端系统,支持 Agent 编排、工具调用、会话管理、RAG 知识库和平台管理能力。
|
||||||
|
|
||||||
当前项目处于基础工程阶段。后端骨架、PostgreSQL 配置、MyBatis-Plus、Lombok、多环境配置文件和前端控制台基础页面已经完成;
|
当前项目处于基础工程阶段。后端骨架(含 DTO、统一返回体、全局异常处理、审计自动填充)、PostgreSQL 配置、MyBatis-Plus、Lombok、多环境配置文件和前端控制台基础页面已经完成;
|
||||||
Agent 运行时、RAG 索引和更多管理功能会在后续阶段逐步实现。
|
Agent 运行时、RAG 文档解析与向量化和更多管理功能会在后续阶段逐步实现。
|
||||||
|
|
||||||
## 项目愿景
|
## 项目愿景
|
||||||
|
|
||||||
@@ -18,33 +18,63 @@ Common Agent 希望成为一个可复用的企业级 AI 应用基础平台:
|
|||||||
|
|
||||||
## 当前技术栈
|
## 当前技术栈
|
||||||
|
|
||||||
- Java 21
|
| 类别 | 技术 |
|
||||||
- Spring Boot 4.0.6
|
|------|------|
|
||||||
- Spring AI,规划接入
|
| 后端 | Java 21, Spring Boot 4.0.6, MyBatis-Plus 3.5.16 |
|
||||||
- MyBatis-Plus 3.5.16
|
| 数据库 | PostgreSQL |
|
||||||
- PostgreSQL JDBC Driver
|
| 工具库 | Lombok, Springdoc OpenAPI 2.8.13, Jackson |
|
||||||
- Lombok
|
| 构建 | Maven Wrapper |
|
||||||
- Maven Wrapper
|
| 前端 | Vue 3, TypeScript 5.9, Vite, Element Plus, Pinia, Vue Router 4 |
|
||||||
- PostgreSQL 数据库:`common_agent`
|
| 前端测试 | Vitest, @vue/test-utils |
|
||||||
|
| 后端测试 | Spring Boot Test |
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```text
|
```text
|
||||||
common_agent
|
common_agent
|
||||||
├── frontend
|
├── frontend/ # Vue 3 前端控制台
|
||||||
│ ├── src/layouts
|
│ ├── src/
|
||||||
│ ├── src/pages
|
│ │ ├── api/ # Axios 封装与各模块 API
|
||||||
│ ├── src/router
|
│ │ ├── layouts/ # AdminLayout 管理后台布局
|
||||||
│ └── src/styles
|
│ │ ├── pages/ # 业务页面(工作台、枚举、附件、知识库、文档)
|
||||||
├── src/main/java/com/bruce
|
│ │ ├── router/ # Vue Router 配置
|
||||||
│ └── CommonAgentApplication.java
|
│ │ ├── stores/ # Pinia 状态管理
|
||||||
├── src/main/resources
|
│ │ ├── styles/ # 全局样式
|
||||||
│ ├── application.yaml
|
│ │ └── types/ # TypeScript 类型声明
|
||||||
│ ├── application-dev.yaml
|
│ └── package.json
|
||||||
│ └── application-template.yaml
|
├── src/
|
||||||
├── docs
|
│ ├── main/java/com/bruce/
|
||||||
│ ├── ARCHITECTURE.md
|
│ │ ├── CommonAgentApplication.java
|
||||||
│ └── ROADMAP.md
|
│ │ ├── common/ # 公共模块
|
||||||
|
│ │ │ ├── config/ # AttachmentProperties, EntityAuditMetaObjectHandler
|
||||||
|
│ │ │ ├── controller/ # SysAttachmentController, SysEnumController
|
||||||
|
│ │ │ ├── domain/
|
||||||
|
│ │ │ │ ├── entity/ # SysAttachment, SysEnum
|
||||||
|
│ │ │ │ └── model/ # BaseEntity, RequestResult
|
||||||
|
│ │ │ ├── dto/
|
||||||
|
│ │ │ │ ├── request/ # 各模块请求 DTO
|
||||||
|
│ │ │ │ └── response/ # 各模块响应 DTO
|
||||||
|
│ │ │ ├── enums/ # CommonStatusEnum, EnableStatusEnum
|
||||||
|
│ │ │ ├── handler/ # GlobalExceptionHandler
|
||||||
|
│ │ │ ├── mapper/ # SysAttachmentMapper, SysEnumMapper
|
||||||
|
│ │ │ └── service/ # 接口与实现
|
||||||
|
│ │ └── rag/ # RAG 知识库模块
|
||||||
|
│ │ ├── constant/ # RagSystemConstants
|
||||||
|
│ │ ├── controller/ # RagStoreController, RagDocumentController
|
||||||
|
│ │ ├── dto/ # 请求/响应 DTO
|
||||||
|
│ │ ├── entity/ # RagStore, RagDocument
|
||||||
|
│ │ ├── enums/ # RagParseStatusEnum, RagIndexStatusEnum
|
||||||
|
│ │ ├── mapper/ # RagDocumentMapper, RagStoreMapper
|
||||||
|
│ │ └── service/ # 接口与实现
|
||||||
|
│ ├── main/resources/
|
||||||
|
│ │ ├── application.yaml # 环境选择
|
||||||
|
│ │ ├── application-dev.yaml # 开发环境配置
|
||||||
|
│ │ └── application-template.yaml # 配置模板
|
||||||
|
│ └── test/java/ # 单元测试(结构稳定性测试 + 前端 API 测试)
|
||||||
|
├── docs/
|
||||||
|
│ ├── ARCHITECTURE.md # 架构说明
|
||||||
|
│ └── ROADMAP.md # 开发路线图
|
||||||
|
├── AGENT.md # 平台设计草案
|
||||||
├── pom.xml
|
├── pom.xml
|
||||||
└── README.md
|
└── README.md
|
||||||
```
|
```
|
||||||
@@ -80,21 +110,19 @@ spring:
|
|||||||
|
|
||||||
运行测试:
|
运行测试:
|
||||||
|
|
||||||
```powershell
|
```bash
|
||||||
.\mvnw.cmd test
|
./mvnw test
|
||||||
```
|
```
|
||||||
|
|
||||||
启动应用:
|
启动应用:
|
||||||
|
|
||||||
```powershell
|
```bash
|
||||||
.\mvnw.cmd spring-boot:run
|
./mvnw spring-boot:run
|
||||||
```
|
```
|
||||||
|
|
||||||
当前阶段还没有加入 Web 服务依赖或常驻任务,所以应用可能启动成功后立即退出。
|
|
||||||
|
|
||||||
启动前端:
|
启动前端:
|
||||||
|
|
||||||
```powershell
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
npm install
|
npm install
|
||||||
npm run dev
|
npm run dev
|
||||||
@@ -102,7 +130,7 @@ npm run dev
|
|||||||
|
|
||||||
前端检查:
|
前端检查:
|
||||||
|
|
||||||
```powershell
|
```bash
|
||||||
cd frontend
|
cd frontend
|
||||||
npm run test:unit
|
npm run test:unit
|
||||||
npm run type-check
|
npm run type-check
|
||||||
@@ -115,11 +143,13 @@ npm run build
|
|||||||
|
|
||||||
当前已有页面:
|
当前已有页面:
|
||||||
|
|
||||||
- 工作台
|
| 页面 | 状态 |
|
||||||
- 系统枚举
|
|------|------|
|
||||||
- 附件管理
|
| 工作台 | 占位 |
|
||||||
- 知识库
|
| 系统枚举 | 完整 CRUD + 批量新增 |
|
||||||
- 知识文档
|
| 附件管理 | 占位 |
|
||||||
|
| 知识库 | 完整 CRUD + 双栏详情 |
|
||||||
|
| 知识文档 | 占位 |
|
||||||
|
|
||||||
当前 UI 规范:
|
当前 UI 规范:
|
||||||
|
|
||||||
@@ -128,6 +158,15 @@ npm run build
|
|||||||
- 全局样式集中在 `frontend/src/styles/global.css`,页面专属样式优先放在对应 `.vue` 文件的 scoped style 中。
|
- 全局样式集中在 `frontend/src/styles/global.css`,页面专属样式优先放在对应 `.vue` 文件的 scoped style 中。
|
||||||
- 后台页面以清晰、克制、便于扫描为优先目标,避免营销式大面积装饰。
|
- 后台页面以清晰、克制、便于扫描为优先目标,避免营销式大面积装饰。
|
||||||
|
|
||||||
|
## 接口规范
|
||||||
|
|
||||||
|
- 统一返回体:`RequestResult<T>`(`resultcode`, `message`, `data`)
|
||||||
|
- 所有接口通过 DTO 交互,不直接暴露实体
|
||||||
|
- 查询条件封装为 `XxxQueryRequest`
|
||||||
|
- 响应 DTO 提供 `fromEntity()` 静态转换
|
||||||
|
- 大整数 ID 通过 `@JsonSerialize(ToStringSerializer.class)` 防止前端精度丢失
|
||||||
|
- 全局异常处理返回 HTTP 400/500 状态码
|
||||||
|
|
||||||
## 规划模块
|
## 规划模块
|
||||||
|
|
||||||
- `agent-core`:Agent 执行模型、工具注册、记忆和编排能力。
|
- `agent-core`:Agent 执行模型、工具注册、记忆和编排能力。
|
||||||
@@ -141,9 +180,10 @@ npm run build
|
|||||||
|
|
||||||
- [架构说明](docs/ARCHITECTURE.md)
|
- [架构说明](docs/ARCHITECTURE.md)
|
||||||
- [开发路线图](docs/ROADMAP.md)
|
- [开发路线图](docs/ROADMAP.md)
|
||||||
|
- [平台设计草案](AGENT.md)
|
||||||
|
|
||||||
## 参考资料
|
## 参考资料
|
||||||
|
|
||||||
- [Spring AI Reference](https://docs.spring.io/spring-ai/reference/)
|
- [Spring AI Reference](https://docs.spring.io/spring-ai/reference/)
|
||||||
- [Spring AI RAG Reference](https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html)
|
- [Spring AI RAG Reference](https://docs.spring.io/spring-ai/reference/api/retrieval-augmented-generation.html)
|
||||||
- [MyBatis-Plus](https://baomidou.com/)
|
- [MyBatis-Plus](https://baomidou.com/)
|
||||||
155
docs/ARCHITECTURE.md
Normal file
155
docs/ARCHITECTURE.md
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
# Common Agent 当前代码架构说明
|
||||||
|
|
||||||
|
## 1. 文档目的
|
||||||
|
|
||||||
|
本文档描述 `common_agent` 当前已经落地的前后端架构,用于帮助快速理解代码边界、模块职责和扩展点。
|
||||||
|
|
||||||
|
## 2. 总体分层
|
||||||
|
|
||||||
|
### 后端分层
|
||||||
|
|
||||||
|
采用标准 Spring Boot + MyBatis-Plus 分层:
|
||||||
|
|
||||||
|
- **controller**:对外暴露 REST API,统一使用 DTO 交互,返回 `RequestResult<T>`。
|
||||||
|
- **service**:接口 + 实现,继承 MyBatis-Plus `IService` / `ServiceImpl`。
|
||||||
|
- **mapper**:继承 `BaseMapper<T>`,无 XML,全部使用 `lambdaQuery()` 类型安全查询。
|
||||||
|
- **entity**:数据库实体模型,继承 `BaseEntity`。
|
||||||
|
- **dto/request|response**:请求/响应 DTO,响应 DTO 提供 `fromEntity()` 静态转换。
|
||||||
|
- **config / constant / enums / handler**:模块级配置、常量、枚举和全局异常处理。
|
||||||
|
|
||||||
|
### 前端架构
|
||||||
|
|
||||||
|
- **入口**:`main.ts` 挂载 Vue App,注册 Pinia、Vue Router、Element Plus。
|
||||||
|
- **布局**:`AdminLayout.vue` 管理后台壳布局(侧边栏菜单 + 主内容区)。
|
||||||
|
- **页面**:各业务页面位于 `pages/`,使用 Composition API + `<script setup lang="ts">`。
|
||||||
|
- **API 层**:`api/request.ts` 封装 Axios,统一 `/api` 前缀,`RequestResult<T>` 信封解包,大整数安全解析。
|
||||||
|
- **路由**:`router/index.ts` 使用 HTML5 history 模式,所有页面作为 AdminLayout 子路由。
|
||||||
|
- **状态**:`stores/app.ts` Pinia 状态管理(当前为环境名占位)。
|
||||||
|
- **样式**:`styles/global.css` 定义全局 CSS 变量和布局,各页面使用 scoped style。
|
||||||
|
|
||||||
|
## 3. 已实现模块
|
||||||
|
|
||||||
|
### 3.1 公共基础模块
|
||||||
|
|
||||||
|
包路径:`com.bruce.common`
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 提供实体基类 `BaseEntity`(主键、审计字段、乐观锁)。
|
||||||
|
- 统一 API 返回体 `RequestResult<T>`。
|
||||||
|
- 全局异常处理 `GlobalExceptionHandler`。
|
||||||
|
- MyBatis-Plus 审计自动填充 `EntityAuditMetaObjectHandler`。
|
||||||
|
- 附件本地存储配置 `AttachmentProperties`。
|
||||||
|
- 系统枚举管理能力(CRUD + 批量新增 + 管理端查询)。
|
||||||
|
- 附件上传能力(本地磁盘 + 元数据持久化)。
|
||||||
|
|
||||||
|
关键类:
|
||||||
|
|
||||||
|
| 类 | 路径 |
|
||||||
|
|----|------|
|
||||||
|
| BaseEntity | `common/domain/model/BaseEntity.java` |
|
||||||
|
| RequestResult | `common/domain/model/RequestResult.java` |
|
||||||
|
| GlobalExceptionHandler | `common/handler/GlobalExceptionHandler.java` |
|
||||||
|
| EntityAuditMetaObjectHandler | `common/config/EntityAuditMetaObjectHandler.java` |
|
||||||
|
| AttachmentProperties | `common/config/AttachmentProperties.java` |
|
||||||
|
| SysEnum | `common/domain/entity/SysEnum.java` |
|
||||||
|
| SysAttachment | `common/domain/entity/SysAttachment.java` |
|
||||||
|
| SysEnumController | `common/controller/SysEnumController.java` |
|
||||||
|
| SysAttachmentController | `common/controller/SysAttachmentController.java` |
|
||||||
|
| SysEnumServiceImpl | `common/service/impl/SysEnumServiceImpl.java` |
|
||||||
|
| SysAttachmentServiceImpl | `common/service/impl/SysAttachmentServiceImpl.java` |
|
||||||
|
| CommonStatusEnum | `common/enums/CommonStatusEnum.java` |
|
||||||
|
| EnableStatusEnum | `common/enums/EnableStatusEnum.java` |
|
||||||
|
|
||||||
|
接口列表:
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| POST | `/api/sys-enum/queryForManagement` | 管理端枚举查询(支持关键词搜索) |
|
||||||
|
| GET | `/api/sys-enum/detail` | 获取单个枚举 |
|
||||||
|
| POST | `/api/sys-enum/save` | 新增/更新枚举 |
|
||||||
|
| POST | `/api/sys-enum/batchSave` | 批量新增枚举 |
|
||||||
|
| POST | `/api/sys-enum/delete` | 删除枚举 |
|
||||||
|
| POST | `/api/attachments/upload` | 上传附件 |
|
||||||
|
|
||||||
|
### 3.2 RAG 知识库模块
|
||||||
|
|
||||||
|
包路径:`com.bruce.rag`
|
||||||
|
|
||||||
|
职责:
|
||||||
|
|
||||||
|
- 维护 RAG 知识库主数据(CRUD + 编码唯一性校验)。
|
||||||
|
- 维护知识库文档与附件的关联关系。
|
||||||
|
- 定义解析状态、索引状态和 RAG 相关来源常量。
|
||||||
|
|
||||||
|
关键类:
|
||||||
|
|
||||||
|
| 类 | 路径 |
|
||||||
|
|----|------|
|
||||||
|
| RagStore | `rag/entity/RagStore.java` |
|
||||||
|
| RagDocument | `rag/entity/RagDocument.java` |
|
||||||
|
| RagStoreController | `rag/controller/RagStoreController.java` |
|
||||||
|
| RagDocumentController | `rag/controller/RagDocumentController.java` |
|
||||||
|
| RagStoreServiceImpl | `rag/service/impl/RagStoreServiceImpl.java` |
|
||||||
|
| RagDocumentServiceImpl | `rag/service/impl/RagDocumentServiceImpl.java` |
|
||||||
|
| RagParseStatusEnum | `rag/enums/RagParseStatusEnum.java` |
|
||||||
|
| RagIndexStatusEnum | `rag/enums/RagIndexStatusEnum.java` |
|
||||||
|
| RagSystemConstants | `rag/constant/RagSystemConstants.java` |
|
||||||
|
|
||||||
|
接口列表:
|
||||||
|
|
||||||
|
| 方法 | 路径 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| POST | `/api/rag/store/query` | 知识库条件查询 |
|
||||||
|
| GET | `/api/rag/store/detail` | 获取知识库详情 |
|
||||||
|
| POST | `/api/rag/store/save` | 新增/更新知识库 |
|
||||||
|
| POST | `/api/rag/store/delete` | 删除知识库 |
|
||||||
|
| POST | `/api/rag/documents/query` | 知识文档条件查询 |
|
||||||
|
|
||||||
|
当前边界:
|
||||||
|
|
||||||
|
- 元数据管理层已完成(知识库 CRUD、文档查询)。
|
||||||
|
- 尚未实现"文档上传后自动建档"、"解析入库"、"切片向量化"、"检索问答"等业务流程。
|
||||||
|
|
||||||
|
## 4. 数据模型关系
|
||||||
|
|
||||||
|
当前核心表关系如下:
|
||||||
|
|
||||||
|
| 表名 | 说明 | 关联 |
|
||||||
|
|------|------|------|
|
||||||
|
| `sys_enum` | 系统枚举配置 | 独立 |
|
||||||
|
| `sys_attachment` | 附件元数据 | 独立,被 rag_document 引用 |
|
||||||
|
| `rag_store` | 知识库主表 | 独立 |
|
||||||
|
| `rag_document` | 知识库文档表 | 关联 `rag_store.id` 和 `sys_attachment.id` |
|
||||||
|
|
||||||
|
`rag_document` 是 RAG 模块与附件模块的连接点。
|
||||||
|
|
||||||
|
## 5. 配置与运行
|
||||||
|
|
||||||
|
关键配置文件:
|
||||||
|
|
||||||
|
| 文件 | 说明 |
|
||||||
|
|------|------|
|
||||||
|
| `application.yaml` | 环境选择(当前 `active: dev`) |
|
||||||
|
| `application-dev.yaml` | 开发环境配置(PostgreSQL 数据源、MyBatis-Plus、附件目录) |
|
||||||
|
| `application-template.yaml` | 配置模板 |
|
||||||
|
|
||||||
|
## 6. 测试策略
|
||||||
|
|
||||||
|
- **后端测试**:围绕结构约束的单元测试(Mapper/Service/Controller 继承体系、实体字段注解、方法签名验证)。
|
||||||
|
- **前端测试**:Vitest + @vue/test-utils,覆盖路由定义、布局组件、页面渲染、API 调用和 Long 类型解析。
|
||||||
|
|
||||||
|
## 7. 当前不足
|
||||||
|
|
||||||
|
- RAG 尚未进入"可用链路",只有元数据管理层。
|
||||||
|
- Agent 运行时相关模型与服务尚未开始建设。
|
||||||
|
- 前端部分页面(工作台、附件管理、知识文档)为占位状态。
|
||||||
|
- 缺少鉴权、租户、操作日志。
|
||||||
|
|
||||||
|
## 8. 建议演进方向
|
||||||
|
|
||||||
|
1. 补 RAG 最小闭环:上传附件 → 建立文档 → 状态流转 → 解析占位。
|
||||||
|
2. 接入 Spring AI,实现最小模型调用链路。
|
||||||
|
3. 建设 Agent 域模型:Agent、Session、Message、Tool、Task。
|
||||||
|
4. 补齐前端占位页面的表单与联调。
|
||||||
|
5. 衔接模型供应商、工作流编排和前端管理台。
|
||||||
122
docs/ROADMAP.md
Normal file
122
docs/ROADMAP.md
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
# Common Agent 开发路线图
|
||||||
|
|
||||||
|
本文档基于 2026-05-21 当前分支代码整理,用来区分"已经完成""建议优先做""中期建设项"。
|
||||||
|
|
||||||
|
## 已完成
|
||||||
|
|
||||||
|
### 基础工程
|
||||||
|
|
||||||
|
- Spring Boot 4.0.6 后端工程初始化。
|
||||||
|
- PostgreSQL 数据源与多环境配置文件(dev / template)。
|
||||||
|
- MyBatis-Plus 3.5.16、Lombok、Springdoc OpenAPI 2.8.13 已接入。
|
||||||
|
- Maven Wrapper。
|
||||||
|
|
||||||
|
### 公共能力
|
||||||
|
|
||||||
|
- `BaseEntity` 公共字段模型(主键、审计字段、乐观锁)。
|
||||||
|
- `EntityAuditMetaObjectHandler` 审计字段自动填充。
|
||||||
|
- `RequestResult<T>` 统一 API 返回体。
|
||||||
|
- `GlobalExceptionHandler` 全局异常处理。
|
||||||
|
- `AttachmentProperties` 附件本地存储配置。
|
||||||
|
- `sys_enum` 完整能力:实体、Mapper、Service、Controller、DTO 层。
|
||||||
|
- 支持单条增删改查、批量新增、管理端条件查询(含关键词搜索)。
|
||||||
|
- 批量新增内含重复值校验。
|
||||||
|
- `sys_attachment` 完整能力:实体、Mapper、Service、Controller、DTO 层。
|
||||||
|
- 支持本地文件上传、UUID 文件名、日期目录分层、元数据持久化。
|
||||||
|
|
||||||
|
### RAG 基础能力
|
||||||
|
|
||||||
|
- `rag_store`、`rag_document` 表结构与实体定义。
|
||||||
|
- RAG 知识库完整 CRUD(含编码唯一性校验)。
|
||||||
|
- 知识文档条件查询服务。
|
||||||
|
- RAG 解析状态枚举 `RagParseStatusEnum`(UPLOADED / PARSING / PARSED / FAILED)。
|
||||||
|
- RAG 索引状态枚举 `RagIndexStatusEnum`(PENDING / INDEXING / INDEXED / FAILED)。
|
||||||
|
- RAG 来源常量 `RagSystemConstants`。
|
||||||
|
|
||||||
|
### 前端控制台
|
||||||
|
|
||||||
|
- Vue 3 + TypeScript + Vite + Element Plus + Pinia + Vue Router 工程。
|
||||||
|
- `AdminLayout.vue` 管理后台布局(侧边栏菜单 + 主内容区)。
|
||||||
|
- 系统枚举管理页:完整 CRUD + 批量新增对话框 + 关键词搜索 + 响应式布局。
|
||||||
|
- 知识库管理页:完整 CRUD + 概览卡片 + 双栏详情 + 编辑对话框。
|
||||||
|
- API 层:Axios 封装 + Long 类型安全解析 + 统一错误拦截。
|
||||||
|
- 单元测试:Vitest + @vue/test-utils,覆盖路由、布局、页面和 API。
|
||||||
|
|
||||||
|
### 质量保障
|
||||||
|
|
||||||
|
- 后端结构稳定性单元测试。
|
||||||
|
- 前端组件与 API 单元测试。
|
||||||
|
|
||||||
|
## 短期优先级
|
||||||
|
|
||||||
|
建议优先完成下面几项,把 RAG 元数据管理层升级为可用的业务闭环:
|
||||||
|
|
||||||
|
1. 知识库文档上传接口:上传文件后自动创建 `rag_document` 记录。
|
||||||
|
2. 文档解析任务入口与状态流转。
|
||||||
|
3. 向量化任务入口与状态流转。
|
||||||
|
4. 知识库文档新增、详情、启停用、重试等管理接口。
|
||||||
|
5. 前端附件管理页面联调。
|
||||||
|
6. 前端知识文档页面联调。
|
||||||
|
|
||||||
|
## RAG 最小闭环
|
||||||
|
|
||||||
|
在基础规范层补齐后,当前 RAG 元数据层已完成,下一步建设业务闭环:
|
||||||
|
|
||||||
|
1. 附件上传后自动创建 `rag_document` 记录。
|
||||||
|
2. 建立文档解析任务入口(占位解析器)。
|
||||||
|
3. 解析状态、索引状态按流程流转。
|
||||||
|
4. 接入占位向量化接口。
|
||||||
|
5. 提供知识库文档管理完整接口(新增、详情、启停用、重试、删除)。
|
||||||
|
|
||||||
|
## Agent 核心能力
|
||||||
|
|
||||||
|
RAG 数据链路稳定后,再进入 Agent 主线:
|
||||||
|
|
||||||
|
1. Agent 定义管理。
|
||||||
|
2. 会话与消息模型。
|
||||||
|
3. 工具注册与工具调用协议。
|
||||||
|
4. Prompt 模板管理。
|
||||||
|
5. 任务执行与简单编排能力。
|
||||||
|
6. 运行日志与调用追踪。
|
||||||
|
|
||||||
|
## 平台化能力
|
||||||
|
|
||||||
|
中期建议补齐的平台能力:
|
||||||
|
|
||||||
|
- 用户与权限体系。
|
||||||
|
- 知识库管理后台完善(检索配置、索引任务查看)。
|
||||||
|
- Agent 管理后台。
|
||||||
|
- 文件管理与文档预览。
|
||||||
|
- 系统配置中心。
|
||||||
|
- 审计日志与监控告警。
|
||||||
|
|
||||||
|
## 前端协同建议
|
||||||
|
|
||||||
|
当前前端工程已在仓库中落地,后端约定已经冻结:
|
||||||
|
|
||||||
|
- 统一响应体格式:`RequestResult<T>`(`resultcode`, `message`, `data`)。
|
||||||
|
- 上传接口返回模型:`SysAttachmentResponse`。
|
||||||
|
- 枚举查询接口规范:POST `/api/sys-enum/queryForManagement`。
|
||||||
|
- RAG 文档状态字段:`parseStatus` + `indexStatus` + `enabled`。
|
||||||
|
- 大整数 ID 通过 `@JsonSerialize(ToStringSerializer.class)` 输出为字符串。
|
||||||
|
|
||||||
|
## 里程碑
|
||||||
|
|
||||||
|
### 里程碑 1:后端规范化 ~~已完成~~
|
||||||
|
|
||||||
|
- 补齐 DTO、响应体、异常处理、校验。
|
||||||
|
- 接口具备稳定对接能力。
|
||||||
|
|
||||||
|
### 里程碑 2:RAG 可演示
|
||||||
|
|
||||||
|
- 实现知识库文档上传、建档、状态流转。
|
||||||
|
- 预留解析和索引任务接口。
|
||||||
|
- 前端知识库页面完整联调。
|
||||||
|
|
||||||
|
### 里程碑 3:Agent 最小运行时
|
||||||
|
|
||||||
|
- 支持一个可配置 Agent、一个会话、一次模型调用、一次工具调用。
|
||||||
|
|
||||||
|
### 里程碑 4:平台管理化
|
||||||
|
|
||||||
|
- 补齐前端占位页面联调与后台配置能力,形成完整平台雏形。
|
||||||
@@ -1,414 +0,0 @@
|
|||||||
# DTO And RequestResult Refactor Implementation Plan
|
|
||||||
|
|
||||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
||||||
|
|
||||||
**Goal:** 将现有枚举、附件、RAG 三块接口统一改造成 DTO 入参与 DTO 返回,并引入 `RequestResult` 作为统一响应包装。
|
|
||||||
|
|
||||||
**Architecture:** `controller`、`service`、`mapper` 三层都尽量以 DTO 作为边界对象,实体类仅用于 MyBatis-Plus 持久化与表映射。控制层统一返回 `RequestResult<T>`,查询条件走 request/query DTO,列表和详情都返回 response DTO,避免继续直接暴露实体和零散参数。
|
|
||||||
|
|
||||||
**Tech Stack:** Java 21、Spring Boot 4、MyBatis-Plus、Spring MVC、JUnit 5
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Task 1: 建立统一响应体和 DTO 包结构
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `src/main/java/com/bruce/common/dto/RequestResult.java`
|
|
||||||
- Create: `src/main/java/com/bruce/common/dto/request/`
|
|
||||||
- Create: `src/main/java/com/bruce/common/dto/response/`
|
|
||||||
- Create: `src/main/java/com/bruce/rag/dto/request/`
|
|
||||||
- Create: `src/main/java/com/bruce/rag/dto/response/`
|
|
||||||
- Modify: `src/test/java/com/bruce/common/enumconfig/SysEnumComponentStructureTests.java`
|
|
||||||
- Modify: `src/test/java/com/bruce/rag/RagComponentStructureTests.java`
|
|
||||||
|
|
||||||
- [ ] **Step 1: 写失败测试,固定统一返回体存在且控制层不再暴露裸实体**
|
|
||||||
|
|
||||||
在结构测试中增加如下断言思路:
|
|
||||||
|
|
||||||
```java
|
|
||||||
Method saveOrUpdateMethod = SysEnumController.class.getMethod("saveOrUpdate", SysEnumSaveRequest.class);
|
|
||||||
assertEquals(RequestResult.class, saveOrUpdateMethod.getReturnType());
|
|
||||||
```
|
|
||||||
|
|
||||||
```java
|
|
||||||
Method listMethod = RagStoreController.class.getMethod("list", RagStoreQueryRequest.class);
|
|
||||||
assertEquals(RequestResult.class, listMethod.getReturnType());
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 运行测试并确认失败**
|
|
||||||
|
|
||||||
Run: `.\mvnw.cmd "-Dtest=SysEnumComponentStructureTests,RagComponentStructureTests" test`
|
|
||||||
|
|
||||||
Expected: FAIL,提示 DTO 或 `RequestResult` 类型不存在,或控制器方法签名不匹配。
|
|
||||||
|
|
||||||
- [ ] **Step 3: 最小化实现统一响应体和基础 DTO 目录**
|
|
||||||
|
|
||||||
`RequestResult.java` 采用如下结构:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Data
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class RequestResult<T> {
|
|
||||||
private boolean success;
|
|
||||||
private String code;
|
|
||||||
private String message;
|
|
||||||
private T data;
|
|
||||||
|
|
||||||
public static <T> RequestResult<T> success(T data) {
|
|
||||||
return new RequestResult<>(true, "SUCCESS", "操作成功", data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> RequestResult<T> success(String message, T data) {
|
|
||||||
return new RequestResult<>(true, "SUCCESS", message, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
public static <T> RequestResult<T> failure(String code, String message) {
|
|
||||||
return new RequestResult<>(false, code, message, null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: 运行测试并确认通过**
|
|
||||||
|
|
||||||
Run: `.\mvnw.cmd "-Dtest=SysEnumComponentStructureTests,RagComponentStructureTests" test`
|
|
||||||
|
|
||||||
Expected: PASS 或只剩下后续控制器签名相关失败。
|
|
||||||
|
|
||||||
- [ ] **Step 5: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add src/main/java/com/bruce/common/dto src/main/java/com/bruce/rag/dto src/test/java/com/bruce/common/enumconfig/SysEnumComponentStructureTests.java src/test/java/com/bruce/rag/RagComponentStructureTests.java
|
|
||||||
git commit -m "refactor: 增加统一响应体与DTO结构"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task 2: 重构 sys_enum 模块为 DTO 入参与 DTO 返回
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `src/main/java/com/bruce/common/dto/request/SysEnumQueryRequest.java`
|
|
||||||
- Create: `src/main/java/com/bruce/common/dto/request/SysEnumSaveRequest.java`
|
|
||||||
- Create: `src/main/java/com/bruce/common/dto/response/SysEnumResponse.java`
|
|
||||||
- Modify: `src/main/java/com/bruce/common/controller/SysEnumController.java`
|
|
||||||
- Modify: `src/main/java/com/bruce/common/service/ISysEnumService.java`
|
|
||||||
- Modify: `src/main/java/com/bruce/common/service/impl/SysEnumServiceImpl.java`
|
|
||||||
- Modify: `src/main/java/com/bruce/common/mapper/SysEnumMapper.java`
|
|
||||||
- Modify: `src/test/java/com/bruce/common/enumconfig/SysEnumComponentStructureTests.java`
|
|
||||||
|
|
||||||
- [ ] **Step 1: 写失败测试,固定 sys_enum 控制器和服务都使用 DTO**
|
|
||||||
|
|
||||||
在 `SysEnumComponentStructureTests` 中增加断言:
|
|
||||||
|
|
||||||
```java
|
|
||||||
Method queryMethod = SysEnumController.class.getMethod("queryByCatalogAndType", SysEnumQueryRequest.class);
|
|
||||||
Method saveMethod = SysEnumController.class.getMethod("saveOrUpdate", SysEnumSaveRequest.class);
|
|
||||||
Method serviceMethod = ISysEnumService.class.getMethod("listByCatalogAndType", SysEnumQueryRequest.class);
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 运行测试并确认失败**
|
|
||||||
|
|
||||||
Run: `.\mvnw.cmd "-Dtest=SysEnumComponentStructureTests" test`
|
|
||||||
|
|
||||||
Expected: FAIL,提示方法签名仍然是 `String` 或 `SysEnum`。
|
|
||||||
|
|
||||||
- [ ] **Step 3: 最小化实现 request/response DTO**
|
|
||||||
|
|
||||||
`SysEnumQueryRequest.java`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Data
|
|
||||||
public class SysEnumQueryRequest {
|
|
||||||
private String catalog;
|
|
||||||
private String type;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`SysEnumSaveRequest.java`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Data
|
|
||||||
public class SysEnumSaveRequest {
|
|
||||||
private Long id;
|
|
||||||
private String catalog;
|
|
||||||
private String type;
|
|
||||||
private String name;
|
|
||||||
private Integer value;
|
|
||||||
private String strvalue;
|
|
||||||
private Integer sort;
|
|
||||||
private String remark;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`SysEnumResponse.java`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Data
|
|
||||||
public class SysEnumResponse {
|
|
||||||
private Long id;
|
|
||||||
private String catalog;
|
|
||||||
private String type;
|
|
||||||
private String name;
|
|
||||||
private Integer value;
|
|
||||||
private String strvalue;
|
|
||||||
private Integer sort;
|
|
||||||
private String remark;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: 修改 mapper/service/controller**
|
|
||||||
|
|
||||||
- `ISysEnumService` 返回 `List<SysEnumResponse>`,保存返回 `SysEnumResponse`
|
|
||||||
- `SysEnumServiceImpl` 新增 DTO 与实体互转私有方法
|
|
||||||
- `SysEnumMapper` 保留 MP 基础能力;如需自定义查询,新增 DTO 查询方法签名
|
|
||||||
- `SysEnumController` 所有接口返回 `RequestResult<?>`
|
|
||||||
|
|
||||||
控制器目标形态:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@PostMapping("/query")
|
|
||||||
public RequestResult<List<SysEnumResponse>> queryByCatalogAndType(@RequestBody SysEnumQueryRequest request) {
|
|
||||||
return RequestResult.success(sysEnumService.listByCatalogAndType(request));
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 5: 运行测试并确认通过**
|
|
||||||
|
|
||||||
Run: `.\mvnw.cmd "-Dtest=SysEnumComponentStructureTests" test`
|
|
||||||
|
|
||||||
Expected: PASS
|
|
||||||
|
|
||||||
- [ ] **Step 6: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add src/main/java/com/bruce/common/controller/SysEnumController.java src/main/java/com/bruce/common/service/ISysEnumService.java src/main/java/com/bruce/common/service/impl/SysEnumServiceImpl.java src/main/java/com/bruce/common/dto/request/SysEnumQueryRequest.java src/main/java/com/bruce/common/dto/request/SysEnumSaveRequest.java src/main/java/com/bruce/common/dto/response/SysEnumResponse.java src/test/java/com/bruce/common/enumconfig/SysEnumComponentStructureTests.java
|
|
||||||
git commit -m "refactor: 调整sys_enum接口为DTO模式"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task 3: 重构 sys_attachment 模块为 DTO 入参与 DTO 返回
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `src/main/java/com/bruce/common/dto/request/SysAttachmentUploadRequest.java`
|
|
||||||
- Create: `src/main/java/com/bruce/common/dto/request/SysAttachmentQueryRequest.java`
|
|
||||||
- Create: `src/main/java/com/bruce/common/dto/response/SysAttachmentResponse.java`
|
|
||||||
- Modify: `src/main/java/com/bruce/common/controller/SysAttachmentController.java`
|
|
||||||
- Modify: `src/main/java/com/bruce/common/service/ISysAttachmentService.java`
|
|
||||||
- Modify: `src/main/java/com/bruce/common/service/impl/SysAttachmentServiceImpl.java`
|
|
||||||
- Modify: `src/test/java/com/bruce/common/attachment/SysAttachmentComponentStructureTests.java`
|
|
||||||
|
|
||||||
- [ ] **Step 1: 写失败测试,固定附件接口返回 `RequestResult` 且 service 返回 DTO**
|
|
||||||
|
|
||||||
示例断言:
|
|
||||||
|
|
||||||
```java
|
|
||||||
Method uploadMethod = SysAttachmentController.class.getMethod("upload", MultipartFile.class, SysAttachmentUploadRequest.class);
|
|
||||||
assertEquals(RequestResult.class, uploadMethod.getReturnType());
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 运行测试并确认失败**
|
|
||||||
|
|
||||||
Run: `.\mvnw.cmd "-Dtest=SysAttachmentComponentStructureTests" test`
|
|
||||||
|
|
||||||
Expected: FAIL,提示控制器或服务方法签名不匹配。
|
|
||||||
|
|
||||||
- [ ] **Step 3: 新增附件 DTO**
|
|
||||||
|
|
||||||
`SysAttachmentUploadRequest.java`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Data
|
|
||||||
public class SysAttachmentUploadRequest {
|
|
||||||
private String sourceType;
|
|
||||||
private Long sourceId;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`SysAttachmentQueryRequest.java`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Data
|
|
||||||
public class SysAttachmentQueryRequest {
|
|
||||||
private String sourceType;
|
|
||||||
private Long sourceId;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`SysAttachmentResponse.java`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Data
|
|
||||||
public class SysAttachmentResponse {
|
|
||||||
private Long id;
|
|
||||||
private String sourceType;
|
|
||||||
private Long sourceId;
|
|
||||||
private String originalName;
|
|
||||||
private String fileName;
|
|
||||||
private String fileSuffix;
|
|
||||||
private String contentType;
|
|
||||||
private Long fileSize;
|
|
||||||
private String storageType;
|
|
||||||
private String filePath;
|
|
||||||
private String fileUrl;
|
|
||||||
private String remark;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: 修改附件控制器和服务**
|
|
||||||
|
|
||||||
- `ISysAttachmentService.upload` 返回 `SysAttachmentResponse`
|
|
||||||
- 控制器上传接口返回 `RequestResult<SysAttachmentResponse>`
|
|
||||||
- 如补充列表查询,也走 `SysAttachmentQueryRequest`
|
|
||||||
|
|
||||||
- [ ] **Step 5: 运行测试并确认通过**
|
|
||||||
|
|
||||||
Run: `.\mvnw.cmd "-Dtest=SysAttachmentComponentStructureTests" test`
|
|
||||||
|
|
||||||
Expected: PASS
|
|
||||||
|
|
||||||
- [ ] **Step 6: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add src/main/java/com/bruce/common/controller/SysAttachmentController.java src/main/java/com/bruce/common/service/ISysAttachmentService.java src/main/java/com/bruce/common/service/impl/SysAttachmentServiceImpl.java src/main/java/com/bruce/common/dto/request/SysAttachmentUploadRequest.java src/main/java/com/bruce/common/dto/request/SysAttachmentQueryRequest.java src/main/java/com/bruce/common/dto/response/SysAttachmentResponse.java src/test/java/com/bruce/common/attachment/SysAttachmentComponentStructureTests.java
|
|
||||||
git commit -m "refactor: 调整附件接口为DTO模式"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task 4: 重构 rag_store 与 rag_document 模块为 DTO 入参与 DTO 返回
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Create: `src/main/java/com/bruce/rag/dto/request/RagStoreQueryRequest.java`
|
|
||||||
- Create: `src/main/java/com/bruce/rag/dto/request/RagStoreSaveRequest.java`
|
|
||||||
- Create: `src/main/java/com/bruce/rag/dto/request/RagDocumentQueryRequest.java`
|
|
||||||
- Create: `src/main/java/com/bruce/rag/dto/request/RagDocumentSaveRequest.java`
|
|
||||||
- Create: `src/main/java/com/bruce/rag/dto/response/RagStoreResponse.java`
|
|
||||||
- Create: `src/main/java/com/bruce/rag/dto/response/RagDocumentResponse.java`
|
|
||||||
- Modify: `src/main/java/com/bruce/rag/controller/RagStoreController.java`
|
|
||||||
- Modify: `src/main/java/com/bruce/rag/controller/RagDocumentController.java`
|
|
||||||
- Modify: `src/main/java/com/bruce/rag/service/IRagStoreService.java`
|
|
||||||
- Modify: `src/main/java/com/bruce/rag/service/IRagDocumentService.java`
|
|
||||||
- Modify: `src/main/java/com/bruce/rag/service/impl/RagStoreServiceImpl.java`
|
|
||||||
- Modify: `src/main/java/com/bruce/rag/service/impl/RagDocumentServiceImpl.java`
|
|
||||||
- Modify: `src/test/java/com/bruce/rag/RagComponentStructureTests.java`
|
|
||||||
|
|
||||||
- [ ] **Step 1: 写失败测试,固定 RAG 控制器和服务都使用 DTO**
|
|
||||||
|
|
||||||
示例断言:
|
|
||||||
|
|
||||||
```java
|
|
||||||
Method storeListMethod = RagStoreController.class.getMethod("list", RagStoreQueryRequest.class);
|
|
||||||
Method documentListMethod = RagDocumentController.class.getMethod("list", RagDocumentQueryRequest.class);
|
|
||||||
assertEquals(RequestResult.class, storeListMethod.getReturnType());
|
|
||||||
assertEquals(RequestResult.class, documentListMethod.getReturnType());
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 2: 运行测试并确认失败**
|
|
||||||
|
|
||||||
Run: `.\mvnw.cmd "-Dtest=RagComponentStructureTests" test`
|
|
||||||
|
|
||||||
Expected: FAIL,提示控制器和 service 仍然暴露实体或无 DTO。
|
|
||||||
|
|
||||||
- [ ] **Step 3: 新增 RAG DTO**
|
|
||||||
|
|
||||||
`RagStoreQueryRequest.java`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Data
|
|
||||||
public class RagStoreQueryRequest {
|
|
||||||
private String storeCode;
|
|
||||||
private String storeName;
|
|
||||||
private String status;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`RagStoreResponse.java`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Data
|
|
||||||
public class RagStoreResponse {
|
|
||||||
private Long id;
|
|
||||||
private String storeCode;
|
|
||||||
private String storeName;
|
|
||||||
private String description;
|
|
||||||
private String status;
|
|
||||||
private String remark;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`RagDocumentQueryRequest.java`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Data
|
|
||||||
public class RagDocumentQueryRequest {
|
|
||||||
private Long storeId;
|
|
||||||
private Long attachmentId;
|
|
||||||
private String parseStatus;
|
|
||||||
private String indexStatus;
|
|
||||||
private Boolean enabled;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
`RagDocumentResponse.java`:
|
|
||||||
|
|
||||||
```java
|
|
||||||
@Data
|
|
||||||
public class RagDocumentResponse {
|
|
||||||
private Long id;
|
|
||||||
private Long storeId;
|
|
||||||
private Long attachmentId;
|
|
||||||
private String documentTitle;
|
|
||||||
private String documentSummary;
|
|
||||||
private String parseStatus;
|
|
||||||
private String indexStatus;
|
|
||||||
private Boolean enabled;
|
|
||||||
private String errorMessage;
|
|
||||||
private String remark;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
- [ ] **Step 4: 修改 RAG service/controller**
|
|
||||||
|
|
||||||
- `IRagStoreService`、`IRagDocumentService` 返回 DTO
|
|
||||||
- `RagStoreController` 和 `RagDocumentController` 返回 `RequestResult`
|
|
||||||
- 查询接口改为 `@PostMapping("/query")` + `@RequestBody QueryRequest`
|
|
||||||
|
|
||||||
- [ ] **Step 5: 运行测试并确认通过**
|
|
||||||
|
|
||||||
Run: `.\mvnw.cmd "-Dtest=RagComponentStructureTests" test`
|
|
||||||
|
|
||||||
Expected: PASS
|
|
||||||
|
|
||||||
- [ ] **Step 6: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add src/main/java/com/bruce/rag/controller/RagStoreController.java src/main/java/com/bruce/rag/controller/RagDocumentController.java src/main/java/com/bruce/rag/service/IRagStoreService.java src/main/java/com/bruce/rag/service/IRagDocumentService.java src/main/java/com/bruce/rag/service/impl/RagStoreServiceImpl.java src/main/java/com/bruce/rag/service/impl/RagDocumentServiceImpl.java src/main/java/com/bruce/rag/dto/request src/main/java/com/bruce/rag/dto/response src/test/java/com/bruce/rag/RagComponentStructureTests.java
|
|
||||||
git commit -m "refactor: 调整RAG接口为DTO模式"
|
|
||||||
```
|
|
||||||
|
|
||||||
### Task 5: 全量验证与整理
|
|
||||||
|
|
||||||
**Files:**
|
|
||||||
- Modify: `src/test/java/com/bruce/common/attachment/SysAttachmentComponentStructureTests.java`
|
|
||||||
- Modify: `src/test/java/com/bruce/common/enumconfig/SysEnumComponentStructureTests.java`
|
|
||||||
- Modify: `src/test/java/com/bruce/rag/RagComponentStructureTests.java`
|
|
||||||
|
|
||||||
- [ ] **Step 1: 运行定向测试,确认三块 DTO 改造都覆盖到**
|
|
||||||
|
|
||||||
Run: `.\mvnw.cmd "-Dtest=SysAttachmentComponentStructureTests,SysEnumComponentStructureTests,RagComponentStructureTests" test`
|
|
||||||
|
|
||||||
Expected: PASS
|
|
||||||
|
|
||||||
- [ ] **Step 2: 运行全量测试**
|
|
||||||
|
|
||||||
Run: `.\mvnw.cmd test`
|
|
||||||
|
|
||||||
Expected: `BUILD SUCCESS`
|
|
||||||
|
|
||||||
- [ ] **Step 3: 检查工作区**
|
|
||||||
|
|
||||||
Run: `git status --short`
|
|
||||||
|
|
||||||
Expected: 仅显示本次预期文件,或为空(若已提交)。
|
|
||||||
|
|
||||||
- [ ] **Step 4: Commit**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
git add src/test/java/com/bruce/common/attachment/SysAttachmentComponentStructureTests.java src/test/java/com/bruce/common/enumconfig/SysEnumComponentStructureTests.java src/test/java/com/bruce/rag/RagComponentStructureTests.java
|
|
||||||
git commit -m "test: 校验DTO接口改造结构"
|
|
||||||
```
|
|
||||||
92
docs/superpowers/plans/2026-05-19-vue3-frontend-framework.md
Normal file
92
docs/superpowers/plans/2026-05-19-vue3-frontend-framework.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# Vue3 Frontend Framework Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** 在当前仓库根目录下新增 `frontend/` Vue3 前端工程,形成可独立运行的前后端分离管理台骨架。
|
||||||
|
|
||||||
|
**Architecture:** 前端采用 `Vite + Vue 3 + TypeScript + Vue Router + Pinia + Axios`,以 `frontend/` 作为独立工程目录。页面层按“管理台主布局 + 模块页面”组织,接口请求按模块拆到 `api/`,统一解析后端 `RequestResult<T>` 返回体,并通过开发代理转发到 Spring Boot 后端。
|
||||||
|
|
||||||
|
**Tech Stack:** Vue 3、TypeScript、Vite、Vue Router、Pinia、Axios、CSS Variables
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Task 1: 初始化前端工程目录和基础依赖
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `frontend/package.json`
|
||||||
|
- Create: `frontend/tsconfig.json`
|
||||||
|
- Create: `frontend/tsconfig.node.json`
|
||||||
|
- Create: `frontend/vite.config.ts`
|
||||||
|
- Create: `frontend/index.html`
|
||||||
|
- Create: `frontend/.gitignore`
|
||||||
|
|
||||||
|
- [ ] 建立前端工程元数据、脚本和 Vite 配置
|
||||||
|
- [ ] 配置开发服务器代理到后端 `/api`
|
||||||
|
- [ ] 固定 TypeScript 和构建入口
|
||||||
|
|
||||||
|
### Task 2: 建立应用入口与全局框架
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `frontend/src/main.ts`
|
||||||
|
- Create: `frontend/src/App.vue`
|
||||||
|
- Create: `frontend/src/styles/reset.css`
|
||||||
|
- Create: `frontend/src/styles/theme.css`
|
||||||
|
- Create: `frontend/src/styles/global.css`
|
||||||
|
|
||||||
|
- [ ] 注册 Router 与 Pinia
|
||||||
|
- [ ] 建立全局样式、主题变量和基础页面容器
|
||||||
|
- [ ] 保证桌面与移动端都能正常加载
|
||||||
|
|
||||||
|
### Task 3: 建立路由、布局和导航骨架
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `frontend/src/router/index.ts`
|
||||||
|
- Create: `frontend/src/layouts/AdminLayout.vue`
|
||||||
|
- Create: `frontend/src/components/app/AppSidebar.vue`
|
||||||
|
- Create: `frontend/src/components/app/AppHeader.vue`
|
||||||
|
- Create: `frontend/src/components/app/AppShellCard.vue`
|
||||||
|
|
||||||
|
- [ ] 建立管理台布局
|
||||||
|
- [ ] 配置仪表盘、系统枚举、附件管理、RAG知识库、RAG文档路由
|
||||||
|
- [ ] 建立侧边导航和顶部栏
|
||||||
|
|
||||||
|
### Task 4: 建立接口层与通用类型
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `frontend/src/types/http.ts`
|
||||||
|
- Create: `frontend/src/types/sys-enum.ts`
|
||||||
|
- Create: `frontend/src/types/attachment.ts`
|
||||||
|
- Create: `frontend/src/types/rag.ts`
|
||||||
|
- Create: `frontend/src/api/http.ts`
|
||||||
|
- Create: `frontend/src/api/sys-enum.ts`
|
||||||
|
- Create: `frontend/src/api/attachment.ts`
|
||||||
|
- Create: `frontend/src/api/rag.ts`
|
||||||
|
|
||||||
|
- [ ] 建立 `RequestResult<T>` 前端类型
|
||||||
|
- [ ] 建立 Axios 实例和统一响应解包
|
||||||
|
- [ ] 按模块拆分 API 请求方法
|
||||||
|
|
||||||
|
### Task 5: 建立页面骨架与基础交互
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `frontend/src/views/dashboard/DashboardView.vue`
|
||||||
|
- Create: `frontend/src/views/sys-enum/SysEnumView.vue`
|
||||||
|
- Create: `frontend/src/views/attachment/AttachmentView.vue`
|
||||||
|
- Create: `frontend/src/views/rag-store/RagStoreView.vue`
|
||||||
|
- Create: `frontend/src/views/rag-document/RagDocumentView.vue`
|
||||||
|
- Create: `frontend/src/components/common/PageSection.vue`
|
||||||
|
- Create: `frontend/src/components/common/EmptyState.vue`
|
||||||
|
|
||||||
|
- [ ] 建立四个业务页面的查询区、状态卡片和表格占位
|
||||||
|
- [ ] 系统枚举、RAG 页面接入真实查询请求
|
||||||
|
- [ ] 附件页面预留上传表单和结果展示
|
||||||
|
|
||||||
|
### Task 6: 补充工程说明与验证
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `README.md`
|
||||||
|
- Create: `frontend/README.md`
|
||||||
|
|
||||||
|
- [ ] 追加前端启动说明
|
||||||
|
- [ ] 记录 Node 版本、运行命令和代理约定
|
||||||
|
- [ ] 执行 `npm install`、`npm run build` 做基础验证
|
||||||
1
frontend/.npmrc
Normal file
1
frontend/.npmrc
Normal file
@@ -0,0 +1 @@
|
|||||||
|
registry=https://registry.npmmirror.com/
|
||||||
@@ -14,6 +14,7 @@
|
|||||||
"@element-plus/icons-vue": "^2.3.2",
|
"@element-plus/icons-vue": "^2.3.2",
|
||||||
"axios": "^1.13.2",
|
"axios": "^1.13.2",
|
||||||
"element-plus": "^2.11.8",
|
"element-plus": "^2.11.8",
|
||||||
|
"json-bigint": "^1.0.0",
|
||||||
"pinia": "^3.0.4",
|
"pinia": "^3.0.4",
|
||||||
"vue": "^3.5.24",
|
"vue": "^3.5.24",
|
||||||
"vue-router": "^4.6.3"
|
"vue-router": "^4.6.3"
|
||||||
|
|||||||
2478
frontend/pnpm-lock.yaml
generated
Normal file
2478
frontend/pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
2
frontend/pnpm-workspace.yaml
Normal file
2
frontend/pnpm-workspace.yaml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
allowBuilds:
|
||||||
|
esbuild: true
|
||||||
30
frontend/src/api/__tests__/json.spec.ts
Normal file
30
frontend/src/api/__tests__/json.spec.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import { parseJsonPreservingLong } from '../json';
|
||||||
|
|
||||||
|
describe('parseJsonPreservingLong', () => {
|
||||||
|
it('should preserve unsafe long ids as strings', () => {
|
||||||
|
const result = parseJsonPreservingLong<{ id: string; storeCode: string }>(
|
||||||
|
'{"id":2057302206052372481,"storeCode":"TEXT-1"}',
|
||||||
|
'application/json',
|
||||||
|
) as { id: string; storeCode: string };
|
||||||
|
|
||||||
|
expect(result.id).toBe('2057302206052372481');
|
||||||
|
expect(result.storeCode).toBe('TEXT-1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep safe integers as numbers', () => {
|
||||||
|
const result = parseJsonPreservingLong<{ total: number }>(
|
||||||
|
'{"total":12}',
|
||||||
|
'application/json',
|
||||||
|
) as { total: number };
|
||||||
|
|
||||||
|
expect(result.total).toBe(12);
|
||||||
|
expect(typeof result.total).toBe('number');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should skip non json payloads', () => {
|
||||||
|
const csv = 'id,name\n1,test';
|
||||||
|
|
||||||
|
expect(parseJsonPreservingLong(csv, 'text/csv')).toBe(csv);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,9 +1,32 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
|
|
||||||
import { request } from '../request';
|
import { buildErrorDetail, request } from '../request';
|
||||||
|
|
||||||
describe('request client', () => {
|
describe('request client', () => {
|
||||||
it('uses the backend api prefix', () => {
|
it('uses the backend api prefix', () => {
|
||||||
expect(request.defaults.baseURL).toBe('/api');
|
expect(request.defaults.baseURL).toBe('/api');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('parses oversized numeric ids as strings to avoid precision loss', () => {
|
||||||
|
const transformResponse = request.defaults.transformResponse;
|
||||||
|
const transform = Array.isArray(transformResponse) ? transformResponse[0] : transformResponse;
|
||||||
|
const payload = '{"resultcode":"0","message":null,"data":{"id":2057302206052372481,"storeCode":"TEXT-1"}}';
|
||||||
|
|
||||||
|
const parsed = transform
|
||||||
|
? transform.call({} as never, payload, { 'content-type': 'application/json' } as never)
|
||||||
|
: JSON.parse(payload);
|
||||||
|
|
||||||
|
expect(parsed.data.id).toBe('2057302206052372481');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('formats structured backend errors for dialog display', () => {
|
||||||
|
const detail = buildErrorDetail({
|
||||||
|
resultcode: '400',
|
||||||
|
message: '知识库编码已存在: TEST-1',
|
||||||
|
data: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(detail).toContain('失败原因:知识库编码已存在: TEST-1');
|
||||||
|
expect(detail).toContain('错误编码:400');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { describe, expect, it, vi } from 'vitest';
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
import { batchSave, deleteById, listForManagement, saveOrUpdate } from '../sysEnums';
|
import { batchSave, deleteById, listForManagement, saveOrUpdate } from '../sysEnums';
|
||||||
import { del, post } from '../request';
|
import { post } from '../request';
|
||||||
|
|
||||||
vi.mock('../request', () => ({
|
vi.mock('../request', () => ({
|
||||||
del: vi.fn(),
|
get: vi.fn(),
|
||||||
post: vi.fn(),
|
post: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ describe('sys enum api', () => {
|
|||||||
it('queries system enums with management filters', () => {
|
it('queries system enums with management filters', () => {
|
||||||
listForManagement({ catalog: 'common', keyword: '启用' });
|
listForManagement({ catalog: 'common', keyword: '启用' });
|
||||||
|
|
||||||
expect(post).toHaveBeenCalledWith('/sys-enums/manage/query', {
|
expect(post).toHaveBeenCalledWith('/sys-enum/queryForManagement', {
|
||||||
catalog: 'common',
|
catalog: 'common',
|
||||||
keyword: '启用',
|
keyword: '启用',
|
||||||
});
|
});
|
||||||
@@ -22,13 +22,15 @@ describe('sys enum api', () => {
|
|||||||
saveOrUpdate({ catalog: 'common', type: 'status', name: '启用', value: 1 });
|
saveOrUpdate({ catalog: 'common', type: 'status', name: '启用', value: 1 });
|
||||||
deleteById('123');
|
deleteById('123');
|
||||||
|
|
||||||
expect(post).toHaveBeenCalledWith('/sys-enums', {
|
expect(post).toHaveBeenCalledWith('/sys-enum/save', {
|
||||||
catalog: 'common',
|
catalog: 'common',
|
||||||
type: 'status',
|
type: 'status',
|
||||||
name: '启用',
|
name: '启用',
|
||||||
value: 1,
|
value: 1,
|
||||||
});
|
});
|
||||||
expect(del).toHaveBeenCalledWith('/sys-enums/123');
|
expect(post).toHaveBeenCalledWith('/sys-enum/delete', undefined, {
|
||||||
|
params: { id: '123' },
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('batch saves enum groups', () => {
|
it('batch saves enum groups', () => {
|
||||||
@@ -46,7 +48,7 @@ describe('sys enum api', () => {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(post).toHaveBeenCalledWith('/sys-enums/batch', {
|
expect(post).toHaveBeenCalledWith('/sys-enum/batchSave', {
|
||||||
catalog: 'common',
|
catalog: 'common',
|
||||||
type: 'enable_status',
|
type: 'enable_status',
|
||||||
items: [
|
items: [
|
||||||
|
|||||||
21
frontend/src/api/json.ts
Normal file
21
frontend/src/api/json.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import JSONBig from 'json-bigint';
|
||||||
|
|
||||||
|
const jsonParser = JSONBig({
|
||||||
|
storeAsString: true,
|
||||||
|
useNativeBigInt: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
function looksLikeJsonPayload(data: string) {
|
||||||
|
const trimmed = data.trim();
|
||||||
|
return trimmed.startsWith('{') || trimmed.startsWith('[');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseJsonPreservingLong<T>(data: unknown, contentType?: string): T | unknown {
|
||||||
|
if (typeof data !== 'string' || !looksLikeJsonPayload(data)) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
if (contentType && !contentType.toLowerCase().includes('application/json')) {
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
return jsonParser.parse(data) as T;
|
||||||
|
}
|
||||||
44
frontend/src/api/ragStores.ts
Normal file
44
frontend/src/api/ragStores.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { get, post } from './request';
|
||||||
|
|
||||||
|
export interface RagStore {
|
||||||
|
id?: string;
|
||||||
|
storeCode: string;
|
||||||
|
storeName: string;
|
||||||
|
description?: string | null;
|
||||||
|
status?: string | null;
|
||||||
|
remark?: string | null;
|
||||||
|
createTime?: string | null;
|
||||||
|
updateTime?: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RagStoreQueryRequest {
|
||||||
|
storeCode?: string;
|
||||||
|
storeName?: string;
|
||||||
|
status?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RagStoreSaveRequest = RagStore;
|
||||||
|
|
||||||
|
export function listRagStores() {
|
||||||
|
return post<RagStore[]>('/rag/store/list');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function queryRagStores(query?: RagStoreQueryRequest) {
|
||||||
|
return post<RagStore[], RagStoreQueryRequest | undefined>('/rag/store/query', query);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getRagStoreById(id: string) {
|
||||||
|
return get<RagStore>('/rag/store/detail', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function saveRagStore(data: RagStoreSaveRequest) {
|
||||||
|
return post<boolean, RagStoreSaveRequest>('/rag/store/save', data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deleteRagStore(id: string) {
|
||||||
|
return post<boolean>('/rag/store/delete', undefined, {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import axios from 'axios';
|
import axios from 'axios';
|
||||||
import type { AxiosRequestConfig } from 'axios';
|
import type { AxiosRequestConfig } from 'axios';
|
||||||
|
import { ElMessageBox } from 'element-plus';
|
||||||
|
import { parseJsonPreservingLong } from './json';
|
||||||
|
|
||||||
export interface RequestResult<T> {
|
export interface RequestResult<T> {
|
||||||
resultcode: string;
|
resultcode: string;
|
||||||
@@ -10,8 +12,37 @@ export interface RequestResult<T> {
|
|||||||
export const request = axios.create({
|
export const request = axios.create({
|
||||||
baseURL: '/api',
|
baseURL: '/api',
|
||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
|
transformResponse: [
|
||||||
|
(data, headers) =>
|
||||||
|
parseJsonPreservingLong(
|
||||||
|
data,
|
||||||
|
typeof headers?.['content-type'] === 'string' ? headers['content-type'] : undefined,
|
||||||
|
),
|
||||||
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export function buildErrorDetail(errorData: RequestResult<unknown>) {
|
||||||
|
const lines = [
|
||||||
|
`失败原因:${errorData.message || '未知错误'}`,
|
||||||
|
`错误编码:${errorData.resultcode || '-'}`,
|
||||||
|
];
|
||||||
|
return lines.join('\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
request.interceptors.response.use(
|
||||||
|
(response) => response,
|
||||||
|
async (error) => {
|
||||||
|
const errorData = error?.response?.data as RequestResult<unknown> | undefined;
|
||||||
|
if (errorData?.resultcode && errorData?.message) {
|
||||||
|
await ElMessageBox.alert(buildErrorDetail(errorData), '请求失败', {
|
||||||
|
type: 'error',
|
||||||
|
confirmButtonText: '知道了',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return Promise.reject(error);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
export function get<T>(url: string, config?: AxiosRequestConfig) {
|
export function get<T>(url: string, config?: AxiosRequestConfig) {
|
||||||
return request.get<RequestResult<T>>(url, config).then((response) => response.data);
|
return request.get<RequestResult<T>>(url, config).then((response) => response.data);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { del, get, post } from './request';
|
import { get, post } from './request';
|
||||||
|
|
||||||
export interface SysEnum {
|
export interface SysEnum {
|
||||||
id?: string;
|
id?: string;
|
||||||
@@ -34,21 +34,25 @@ export interface SysEnumBatchSaveRequest {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function listForManagement(query: SysEnumManageQuery) {
|
export function listForManagement(query: SysEnumManageQuery) {
|
||||||
return post<SysEnum[], SysEnumManageQuery>('/sys-enums/manage/query', query);
|
return post<SysEnum[], SysEnumManageQuery>('/sys-enum/queryForManagement', query);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getById(id: string) {
|
export function getById(id: string) {
|
||||||
return get<SysEnum>(`/sys-enums/${id}`);
|
return get<SysEnum>('/sys-enum/detail', {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function saveOrUpdate(data: SysEnumSaveRequest) {
|
export function saveOrUpdate(data: SysEnumSaveRequest) {
|
||||||
return post<boolean, SysEnumSaveRequest>('/sys-enums', data);
|
return post<boolean, SysEnumSaveRequest>('/sys-enum/save', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function batchSave(data: SysEnumBatchSaveRequest) {
|
export function batchSave(data: SysEnumBatchSaveRequest) {
|
||||||
return post<boolean, SysEnumBatchSaveRequest>('/sys-enums/batch', data);
|
return post<boolean, SysEnumBatchSaveRequest>('/sys-enum/batchSave', data);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteById(id: string) {
|
export function deleteById(id: string) {
|
||||||
return del<boolean>(`/sys-enums/${id}`);
|
return post<boolean>('/sys-enum/delete', undefined, {
|
||||||
|
params: { id },
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,668 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { CirclePlus, Delete, Edit, FolderAdd, Refresh, Search, UploadFilled } from '@element-plus/icons-vue';
|
||||||
|
import { ElMessage, ElMessageBox } from 'element-plus';
|
||||||
|
import { computed, onMounted, reactive, ref } from 'vue';
|
||||||
|
|
||||||
|
import {
|
||||||
|
deleteRagStore,
|
||||||
|
getRagStoreById,
|
||||||
|
queryRagStores,
|
||||||
|
saveRagStore,
|
||||||
|
type RagStore,
|
||||||
|
} from '@/api/ragStores';
|
||||||
|
|
||||||
|
type StoreStatus = '启用' | '停用';
|
||||||
|
|
||||||
|
const loading = ref(false);
|
||||||
|
const detailLoading = ref(false);
|
||||||
|
const submitting = ref(false);
|
||||||
|
const storeRows = ref<RagStore[]>([]);
|
||||||
|
const activeStoreId = ref<string | null>(null);
|
||||||
|
const activeStore = ref<RagStore | null>(null);
|
||||||
|
|
||||||
|
const queryForm = reactive({
|
||||||
|
storeName: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const createDialogVisible = ref(false);
|
||||||
|
const editDialogVisible = ref(false);
|
||||||
|
|
||||||
|
const createForm = reactive({
|
||||||
|
storeCode: '',
|
||||||
|
storeName: '',
|
||||||
|
description: '',
|
||||||
|
status: '启用' as StoreStatus,
|
||||||
|
remark: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const editForm = reactive({
|
||||||
|
id: '',
|
||||||
|
storeCode: '',
|
||||||
|
storeName: '',
|
||||||
|
description: '',
|
||||||
|
status: '启用' as StoreStatus,
|
||||||
|
remark: '',
|
||||||
|
});
|
||||||
|
|
||||||
|
const overviewCards = computed(() => {
|
||||||
|
const totalStores = storeRows.value.length;
|
||||||
|
const retrievableStores = storeRows.value.filter((row) => row.status === '启用').length;
|
||||||
|
|
||||||
|
return [
|
||||||
|
{ label: '知识库总数', value: totalStores, hint: '当前已登记知识库' },
|
||||||
|
{ label: '文档总数', value: '-', hint: '待文档统计接口补充' },
|
||||||
|
{ label: '切片总数', value: '-', hint: '待切片统计接口补充' },
|
||||||
|
{ label: '可检索知识库数', value: retrievableStores, hint: '当前按启用状态暂代统计' },
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
|
async function loadStores(preferredStoreId?: string | null) {
|
||||||
|
loading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await queryRagStores({
|
||||||
|
storeName: queryForm.storeName.trim() || undefined,
|
||||||
|
});
|
||||||
|
storeRows.value = response.data ?? [];
|
||||||
|
|
||||||
|
if (storeRows.value.length === 0) {
|
||||||
|
activeStoreId.value = null;
|
||||||
|
activeStore.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const firstStore = storeRows.value[0];
|
||||||
|
const targetId =
|
||||||
|
preferredStoreId && storeRows.value.some((row) => String(row.id) === preferredStoreId)
|
||||||
|
? preferredStoreId
|
||||||
|
: firstStore
|
||||||
|
? String(firstStore.id)
|
||||||
|
: null;
|
||||||
|
if (!targetId) {
|
||||||
|
activeStoreId.value = null;
|
||||||
|
activeStore.value = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await selectStore(targetId);
|
||||||
|
} finally {
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function selectStore(storeId: string) {
|
||||||
|
activeStoreId.value = storeId;
|
||||||
|
detailLoading.value = true;
|
||||||
|
try {
|
||||||
|
const response = await getRagStoreById(storeId);
|
||||||
|
activeStore.value = response.data ?? null;
|
||||||
|
} finally {
|
||||||
|
detailLoading.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleSearch() {
|
||||||
|
loadStores(activeStoreId.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleReset() {
|
||||||
|
queryForm.storeName = '';
|
||||||
|
loadStores();
|
||||||
|
}
|
||||||
|
|
||||||
|
function openCreateDialog() {
|
||||||
|
createForm.storeCode = '';
|
||||||
|
createForm.storeName = '';
|
||||||
|
createForm.description = '';
|
||||||
|
createForm.status = '启用';
|
||||||
|
createForm.remark = '';
|
||||||
|
createDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function openEditDialog() {
|
||||||
|
if (!activeStore.value) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
editForm.id = String(activeStore.value.id ?? '');
|
||||||
|
editForm.storeCode = activeStore.value.storeCode;
|
||||||
|
editForm.storeName = activeStore.value.storeName;
|
||||||
|
editForm.description = activeStore.value.description ?? '';
|
||||||
|
editForm.status = (activeStore.value.status as StoreStatus) || '启用';
|
||||||
|
editForm.remark = activeStore.value.remark ?? '';
|
||||||
|
editDialogVisible.value = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitCreateStore() {
|
||||||
|
if (!createForm.storeCode || !createForm.storeName) {
|
||||||
|
ElMessage.warning('请填写知识库编码和知识库名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
await saveRagStore({
|
||||||
|
storeCode: createForm.storeCode,
|
||||||
|
storeName: createForm.storeName,
|
||||||
|
description: createForm.description,
|
||||||
|
status: createForm.status,
|
||||||
|
remark: createForm.remark,
|
||||||
|
});
|
||||||
|
createDialogVisible.value = false;
|
||||||
|
ElMessage.success('知识库已创建');
|
||||||
|
await loadStores();
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitEditStore() {
|
||||||
|
if (!editForm.id || !editForm.storeCode || !editForm.storeName) {
|
||||||
|
ElMessage.warning('请填写知识库编码和知识库名称');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
submitting.value = true;
|
||||||
|
try {
|
||||||
|
await saveRagStore({
|
||||||
|
id: editForm.id,
|
||||||
|
storeCode: editForm.storeCode,
|
||||||
|
storeName: editForm.storeName,
|
||||||
|
description: editForm.description,
|
||||||
|
status: editForm.status,
|
||||||
|
remark: editForm.remark,
|
||||||
|
});
|
||||||
|
editDialogVisible.value = false;
|
||||||
|
ElMessage.success('知识库信息已更新');
|
||||||
|
await loadStores(editForm.id);
|
||||||
|
} finally {
|
||||||
|
submitting.value = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function removeStore() {
|
||||||
|
if (!activeStore.value?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await ElMessageBox.confirm(`确认删除知识库「${activeStore.value.storeName}」?`, '删除确认', {
|
||||||
|
type: 'warning',
|
||||||
|
confirmButtonText: '删除',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
});
|
||||||
|
|
||||||
|
await deleteRagStore(String(activeStore.value.id));
|
||||||
|
ElMessage.success('知识库已删除');
|
||||||
|
await loadStores();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showFutureMessage(actionName: string) {
|
||||||
|
ElMessage.info(`${actionName} 会在下一批接口里补齐`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getStatusTagType(status?: string | null) {
|
||||||
|
return status === '启用' ? 'success' : 'info';
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadStores();
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<section class="page-panel">
|
<section class="page-panel rag-store-page">
|
||||||
<div class="page-panel__header">
|
<div class="page-panel__header rag-store-page__header">
|
||||||
<h2>知识库</h2>
|
<div>
|
||||||
<span>RAG</span>
|
<h2>知识库</h2>
|
||||||
|
<p>统一管理知识库及其文档、索引状态</p>
|
||||||
|
</div>
|
||||||
|
<span>RAG Stores</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="overview-grid">
|
||||||
|
<article v-for="card in overviewCards" :key="card.label" class="overview-card">
|
||||||
|
<span class="overview-card__label">{{ card.label }}</span>
|
||||||
|
<strong class="overview-card__value">{{ card.value }}</strong>
|
||||||
|
<small class="overview-card__hint">{{ card.hint }}</small>
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="rag-store-page__content">
|
||||||
|
<section class="store-list-panel">
|
||||||
|
<div class="section-heading">
|
||||||
|
<div>
|
||||||
|
<h3>知识库列表</h3>
|
||||||
|
<p>按知识库名称检索并切换当前查看对象</p>
|
||||||
|
</div>
|
||||||
|
<el-button data-test="create-store" type="primary" :icon="CirclePlus" @click="openCreateDialog">
|
||||||
|
新建知识库
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="store-search-bar">
|
||||||
|
<el-input
|
||||||
|
v-model="queryForm.storeName"
|
||||||
|
data-test="store-name-input"
|
||||||
|
clearable
|
||||||
|
placeholder="请输入知识库名称"
|
||||||
|
@keyup.enter="handleSearch"
|
||||||
|
/>
|
||||||
|
<el-button data-test="store-search" type="primary" :icon="Search" @click="handleSearch">
|
||||||
|
查询
|
||||||
|
</el-button>
|
||||||
|
<el-button :icon="Refresh" @click="handleReset">重置</el-button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-loading="loading" class="store-list">
|
||||||
|
<button
|
||||||
|
v-for="store in storeRows"
|
||||||
|
:key="store.id"
|
||||||
|
:data-test="`store-card-${String(store.storeCode).toLowerCase()}`"
|
||||||
|
class="store-card"
|
||||||
|
:class="{ 'store-card--active': String(store.id) === activeStoreId }"
|
||||||
|
type="button"
|
||||||
|
@click="selectStore(String(store.id))"
|
||||||
|
>
|
||||||
|
<div class="store-card__title-row">
|
||||||
|
<strong>{{ store.storeName }}</strong>
|
||||||
|
<el-tag size="small" :type="getStatusTagType(store.status)">{{ store.status || '未设置' }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<p>编码:{{ store.storeCode }}</p>
|
||||||
|
<div class="store-card__metrics">
|
||||||
|
<span>描述:{{ store.description || '暂无描述' }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="store-card__metrics">
|
||||||
|
<span>更新时间:{{ store.updateTime || '-' }}</span>
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<el-empty v-if="!loading && storeRows.length === 0" description="未找到匹配的知识库" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="store-detail-panel">
|
||||||
|
<div v-loading="detailLoading" class="store-detail-panel__body">
|
||||||
|
<template v-if="activeStore">
|
||||||
|
<div class="section-heading section-heading--detail">
|
||||||
|
<div>
|
||||||
|
<h3>{{ activeStore.storeName }}</h3>
|
||||||
|
<p>编码:{{ activeStore.storeCode }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="detail-actions">
|
||||||
|
<el-button :icon="Edit" @click="openEditDialog">编辑</el-button>
|
||||||
|
<el-button type="primary" :icon="UploadFilled" @click="showFutureMessage('批量导入文件')">
|
||||||
|
批量导入文件
|
||||||
|
</el-button>
|
||||||
|
<el-button :icon="FolderAdd" @click="showFutureMessage('重建索引')">重建索引</el-button>
|
||||||
|
<el-button type="danger" :icon="Delete" @click="removeStore">删除</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="detail-grid">
|
||||||
|
<article class="detail-card">
|
||||||
|
<div class="detail-card__header">
|
||||||
|
<h4>基本信息</h4>
|
||||||
|
<el-tag :type="getStatusTagType(activeStore.status)">{{ activeStore.status || '未设置' }}</el-tag>
|
||||||
|
</div>
|
||||||
|
<el-descriptions :column="2" border>
|
||||||
|
<el-descriptions-item label="知识库名称">
|
||||||
|
{{ activeStore.storeName }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="知识库编码">
|
||||||
|
{{ activeStore.storeCode }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="描述" :span="2">
|
||||||
|
{{ activeStore.description || '暂无描述' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="备注" :span="2">
|
||||||
|
{{ activeStore.remark || '暂无备注' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="创建时间">
|
||||||
|
{{ activeStore.createTime || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="更新时间">
|
||||||
|
{{ activeStore.updateTime || '-' }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="detail-card detail-card--placeholder">
|
||||||
|
<div class="detail-card__header">
|
||||||
|
<h4>文档概览</h4>
|
||||||
|
<span>下一批接口补充</span>
|
||||||
|
</div>
|
||||||
|
<el-empty description="文档数量、切片数量、最近上传时间待后端聚合接口补充" />
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="detail-card detail-card--placeholder">
|
||||||
|
<div class="detail-card__header">
|
||||||
|
<h4>检索配置</h4>
|
||||||
|
<span>下一批接口补充</span>
|
||||||
|
</div>
|
||||||
|
<el-empty description="检索模式、Embedding 模型、Chunk 参数待后端补充" />
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article class="detail-card detail-card--placeholder">
|
||||||
|
<div class="detail-card__header">
|
||||||
|
<h4>最近任务</h4>
|
||||||
|
<span>下一批接口补充</span>
|
||||||
|
</div>
|
||||||
|
<el-empty description="导入任务、索引任务与状态流转待后端补充" />
|
||||||
|
</article>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<el-empty v-else description="请选择左侧一个知识库查看详情" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<el-dialog v-model="createDialogVisible" title="新建知识库" width="560px">
|
||||||
|
<el-form :model="createForm" label-width="96px">
|
||||||
|
<el-form-item label="知识库编码" required>
|
||||||
|
<el-input v-model="createForm.storeCode" data-test="create-store-code" placeholder="如 PROD_DOC" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="知识库名称" required>
|
||||||
|
<el-input
|
||||||
|
v-model="createForm.storeName"
|
||||||
|
data-test="create-store-name"
|
||||||
|
placeholder="请输入知识库名称"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-radio-group v-model="createForm.status">
|
||||||
|
<el-radio-button label="启用" />
|
||||||
|
<el-radio-button label="停用" />
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="描述">
|
||||||
|
<el-input v-model="createForm.description" type="textarea" :rows="3" placeholder="请输入知识库描述" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="createForm.remark" type="textarea" :rows="2" placeholder="可选" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="createDialogVisible = false">取消</el-button>
|
||||||
|
<el-button data-test="create-store-submit" type="primary" :loading="submitting" @click="submitCreateStore">
|
||||||
|
保存
|
||||||
|
</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="editDialogVisible" title="编辑知识库" width="560px">
|
||||||
|
<el-form :model="editForm" label-width="96px">
|
||||||
|
<el-form-item label="知识库编码" required>
|
||||||
|
<el-input v-model="editForm.storeCode" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="知识库名称" required>
|
||||||
|
<el-input v-model="editForm.storeName" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="状态">
|
||||||
|
<el-radio-group v-model="editForm.status">
|
||||||
|
<el-radio-button label="启用" />
|
||||||
|
<el-radio-button label="停用" />
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="描述">
|
||||||
|
<el-input v-model="editForm.description" type="textarea" :rows="3" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="备注">
|
||||||
|
<el-input v-model="editForm.remark" type="textarea" :rows="2" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="editDialogVisible = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submitting" @click="submitEditStore">保存</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.rag-store-page {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rag-store-page__header {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rag-store-page__header p {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
color: #667085;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||||
|
gap: 16px;
|
||||||
|
padding: 20px 22px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card {
|
||||||
|
border: 1px solid #e6ebf3;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 18px 18px 16px;
|
||||||
|
background: linear-gradient(180deg, #ffffff, #f9fbff);
|
||||||
|
box-shadow: 0 6px 20px rgba(15, 23, 42, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card__label {
|
||||||
|
display: block;
|
||||||
|
color: #667085;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card__value {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #172033;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.overview-card__hint {
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
color: #98a2b3;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rag-store-page__content {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(320px, 0.95fr) minmax(0, 1.45fr);
|
||||||
|
gap: 18px;
|
||||||
|
padding: 18px 22px 22px;
|
||||||
|
min-height: 0;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-list-panel,
|
||||||
|
.store-detail-panel {
|
||||||
|
border: 1px solid #e6ebf3;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #fcfdff;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-detail-panel__body {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 18px 18px 14px;
|
||||||
|
border-bottom: 1px solid #eef2f7;
|
||||||
|
background: linear-gradient(180deg, #ffffff, #fbfcff);
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading h3 {
|
||||||
|
margin: 0;
|
||||||
|
color: #172033;
|
||||||
|
font-size: 17px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading p {
|
||||||
|
margin: 6px 0 0;
|
||||||
|
color: #667085;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-search-bar {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(0, 1fr) auto auto;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 16px 18px;
|
||||||
|
border-bottom: 1px solid #eef2f7;
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 18px;
|
||||||
|
max-height: 780px;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card {
|
||||||
|
border: 1px solid #e3e8f0;
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 16px;
|
||||||
|
text-align: left;
|
||||||
|
background: #ffffff;
|
||||||
|
cursor: pointer;
|
||||||
|
transition:
|
||||||
|
border-color 0.2s ease,
|
||||||
|
box-shadow 0.2s ease,
|
||||||
|
transform 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card:hover {
|
||||||
|
border-color: #bfdbfe;
|
||||||
|
box-shadow: 0 8px 22px rgba(22, 119, 255, 0.08);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card--active {
|
||||||
|
border-color: #1677ff;
|
||||||
|
box-shadow: 0 10px 24px rgba(22, 119, 255, 0.12);
|
||||||
|
background: #f7fbff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card__title-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card strong {
|
||||||
|
color: #172033;
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card p {
|
||||||
|
margin: 10px 0 0;
|
||||||
|
color: #475467;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-card__metrics {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
margin-top: 10px;
|
||||||
|
color: #667085;
|
||||||
|
font-size: 13px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading--detail {
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 8px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 16px;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card {
|
||||||
|
border: 1px solid #e7edf5;
|
||||||
|
border-radius: 12px;
|
||||||
|
background: #ffffff;
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card__header h4 {
|
||||||
|
margin: 0;
|
||||||
|
color: #172033;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card__header span {
|
||||||
|
color: #667085;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-card--placeholder :deep(.el-empty) {
|
||||||
|
padding: 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1280px) {
|
||||||
|
.overview-grid {
|
||||||
|
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.rag-store-page__content {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.overview-grid {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
padding: 16px 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rag-store-page__content {
|
||||||
|
padding: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-heading,
|
||||||
|
.section-heading--detail {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.store-search-bar {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-actions {
|
||||||
|
justify-content: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
132
frontend/src/pages/__tests__/RagStoresPage.spec.ts
Normal file
132
frontend/src/pages/__tests__/RagStoresPage.spec.ts
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
import { flushPromises, mount } from '@vue/test-utils';
|
||||||
|
import ElementPlus from 'element-plus';
|
||||||
|
import { describe, expect, it, vi } from 'vitest';
|
||||||
|
|
||||||
|
import RagStoresPage from '../RagStoresPage.vue';
|
||||||
|
import { getRagStoreById, queryRagStores, saveRagStore } from '@/api/ragStores';
|
||||||
|
|
||||||
|
vi.mock('@/api/ragStores', () => ({
|
||||||
|
queryRagStores: vi.fn((query?: { storeName?: string }) => {
|
||||||
|
const rows = [
|
||||||
|
{
|
||||||
|
id: '1',
|
||||||
|
storeCode: 'PROD_DOC',
|
||||||
|
storeName: '产品制度库',
|
||||||
|
description: '产品制度、业务规范、流程材料',
|
||||||
|
status: '启用',
|
||||||
|
createTime: '2026-05-03 10:20:00',
|
||||||
|
updateTime: '2026-05-21 16:40:00',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: '2',
|
||||||
|
storeCode: 'FAQ',
|
||||||
|
storeName: 'FAQ知识库',
|
||||||
|
description: '常见问题知识沉淀',
|
||||||
|
status: '停用',
|
||||||
|
createTime: '2026-05-06 09:10:00',
|
||||||
|
updateTime: '2026-05-21 11:12:00',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
const keyword = query?.storeName?.trim();
|
||||||
|
const data = keyword ? rows.filter((row) => row.storeName.includes(keyword)) : rows;
|
||||||
|
return Promise.resolve({ resultcode: '0', message: null, data });
|
||||||
|
}),
|
||||||
|
getRagStoreById: vi.fn((id: string) =>
|
||||||
|
Promise.resolve({
|
||||||
|
resultcode: '0',
|
||||||
|
message: null,
|
||||||
|
data:
|
||||||
|
id === '2'
|
||||||
|
? {
|
||||||
|
id: '2',
|
||||||
|
storeCode: 'FAQ',
|
||||||
|
storeName: 'FAQ知识库',
|
||||||
|
description: '常见问题知识沉淀',
|
||||||
|
status: '停用',
|
||||||
|
remark: 'FAQ 场景知识',
|
||||||
|
createTime: '2026-05-06 09:10:00',
|
||||||
|
updateTime: '2026-05-21 11:12:00',
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
id: '1',
|
||||||
|
storeCode: 'PROD_DOC',
|
||||||
|
storeName: '产品制度库',
|
||||||
|
description: '产品制度、业务规范、流程材料',
|
||||||
|
status: '启用',
|
||||||
|
remark: '核心制度库',
|
||||||
|
createTime: '2026-05-03 10:20:00',
|
||||||
|
updateTime: '2026-05-21 16:40:00',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
saveRagStore: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
|
||||||
|
deleteRagStore: vi.fn(() => Promise.resolve({ resultcode: '0', message: null, data: true })),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('RagStoresPage', () => {
|
||||||
|
it('renders overview cards and loads default store detail from backend data', async () => {
|
||||||
|
const wrapper = mount(RagStoresPage, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(wrapper.text()).toContain('知识库总数');
|
||||||
|
expect(wrapper.text()).toContain('产品制度库');
|
||||||
|
expect(wrapper.text()).toContain('核心制度库');
|
||||||
|
expect(queryRagStores).toHaveBeenCalled();
|
||||||
|
expect(getRagStoreById).toHaveBeenCalledWith('1');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('filters stores by name and updates detail when a store is selected', async () => {
|
||||||
|
const wrapper = mount(RagStoresPage, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
await wrapper.get('[data-test="store-name-input"]').setValue('FAQ');
|
||||||
|
await wrapper.get('[data-test="store-search"]').trigger('click');
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(queryRagStores).toHaveBeenLastCalledWith({
|
||||||
|
storeName: 'FAQ',
|
||||||
|
});
|
||||||
|
expect(wrapper.text()).toContain('FAQ知识库');
|
||||||
|
expect(wrapper.text()).not.toContain('核心制度库');
|
||||||
|
|
||||||
|
await wrapper.get('[data-test="store-card-faq"]').trigger('click');
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(getRagStoreById).toHaveBeenLastCalledWith('2');
|
||||||
|
expect(wrapper.text()).toContain('FAQ 场景知识');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('submits create form through backend api', async () => {
|
||||||
|
const wrapper = mount(RagStoresPage, {
|
||||||
|
global: {
|
||||||
|
plugins: [ElementPlus],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await flushPromises();
|
||||||
|
await wrapper.get('[data-test="create-store"]').trigger('click');
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
await wrapper.get('[data-test="create-store-code"]').setValue('NEW_STORE');
|
||||||
|
await wrapper.get('[data-test="create-store-name"]').setValue('新建知识库');
|
||||||
|
await wrapper.get('[data-test="create-store-submit"]').trigger('click');
|
||||||
|
await flushPromises();
|
||||||
|
|
||||||
|
expect(saveRagStore).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
storeCode: 'NEW_STORE',
|
||||||
|
storeName: '新建知识库',
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
14
frontend/src/types/json-bigint.d.ts
vendored
Normal file
14
frontend/src/types/json-bigint.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
declare module 'json-bigint' {
|
||||||
|
interface JsonBigOptions {
|
||||||
|
storeAsString?: boolean;
|
||||||
|
useNativeBigInt?: boolean;
|
||||||
|
alwaysParseAsBig?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface JsonBigInstance {
|
||||||
|
parse<T = unknown>(text: string): T;
|
||||||
|
stringify(value: unknown): string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function JSONBig(options?: JsonBigOptions): JsonBigInstance;
|
||||||
|
}
|
||||||
434
rag-store-page-apis.md
Normal file
434
rag-store-page-apis.md
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
# 知识库页面后端接口清单
|
||||||
|
|
||||||
|
本文对应前端页面:[RagStoresPage.vue](/D:/Code/common_agent/frontend/src/pages/RagStoresPage.vue)
|
||||||
|
|
||||||
|
## 1. 页面目标
|
||||||
|
|
||||||
|
知识库页面采用:
|
||||||
|
|
||||||
|
- 顶部 4 张全局统计卡片
|
||||||
|
- 左侧知识库名称搜索与列表
|
||||||
|
- 右侧当前知识库详情
|
||||||
|
- 当前知识库级别操作:编辑、批量导入文件、重建索引
|
||||||
|
|
||||||
|
因此接口建议拆成 `全局概览`、`知识库列表/详情`、`单库动作` 三组。
|
||||||
|
|
||||||
|
## 2. 本批已实现并已用于前端联调的接口
|
||||||
|
|
||||||
|
### 2.1 查询全部知识库
|
||||||
|
|
||||||
|
- `GET /api/rag/stores`
|
||||||
|
|
||||||
|
当前返回类型:
|
||||||
|
|
||||||
|
- `RequestResult<List<RagStoreResponse>>`
|
||||||
|
|
||||||
|
当前字段:
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `storeCode`
|
||||||
|
- `storeName`
|
||||||
|
- `description`
|
||||||
|
- `status`
|
||||||
|
- `remark`
|
||||||
|
|
||||||
|
对应代码:
|
||||||
|
|
||||||
|
- [RagStoreController.java](/D:/Code/common_agent/src/main/java/com/bruce/rag/controller/RagStoreController.java)
|
||||||
|
- [RagStoreResponse.java](/D:/Code/common_agent/src/main/java/com/bruce/rag/dto/response/RagStoreResponse.java)
|
||||||
|
|
||||||
|
### 2.2 按条件查询知识库
|
||||||
|
|
||||||
|
- `POST /api/rag/stores/query`
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"storeCode": "PROD_DOC",
|
||||||
|
"storeName": "产品制度",
|
||||||
|
"status": "ENABLED"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
当前支持字段:
|
||||||
|
|
||||||
|
- `storeCode`
|
||||||
|
- `storeName`
|
||||||
|
- `status`
|
||||||
|
|
||||||
|
对应代码:
|
||||||
|
|
||||||
|
- [RagStoreQueryRequest.java](/D:/Code/common_agent/src/main/java/com/bruce/rag/dto/request/RagStoreQueryRequest.java)
|
||||||
|
|
||||||
|
### 2.3 查询知识库详情
|
||||||
|
|
||||||
|
- `GET /api/rag/stores/{id}`
|
||||||
|
|
||||||
|
返回类型:
|
||||||
|
|
||||||
|
- `RequestResult<RagStoreResponse>`
|
||||||
|
|
||||||
|
### 2.4 新增或修改知识库
|
||||||
|
|
||||||
|
- `POST /api/rag/stores`
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"storeCode": "PROD_DOC",
|
||||||
|
"storeName": "产品制度库",
|
||||||
|
"description": "产品制度、业务规范、流程材料",
|
||||||
|
"status": "启用",
|
||||||
|
"remark": "核心制度库"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回类型:
|
||||||
|
|
||||||
|
- `RequestResult<Boolean>`
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `id` 为空时新增
|
||||||
|
- `id` 不为空时修改
|
||||||
|
|
||||||
|
### 2.5 删除知识库
|
||||||
|
|
||||||
|
- `DELETE /api/rag/stores/{id}`
|
||||||
|
|
||||||
|
返回类型:
|
||||||
|
|
||||||
|
- `RequestResult<Boolean>`
|
||||||
|
|
||||||
|
## 3. 当前项目里已有但本批前端未联调的接口
|
||||||
|
|
||||||
|
### 3.1 查询全部知识文档
|
||||||
|
|
||||||
|
- `GET /api/rag/documents`
|
||||||
|
|
||||||
|
### 3.2 按条件查询知识文档
|
||||||
|
|
||||||
|
- `POST /api/rag/documents/query`
|
||||||
|
|
||||||
|
当前可用于按 `storeId` 查询当前知识库下文档。
|
||||||
|
|
||||||
|
对应代码:
|
||||||
|
|
||||||
|
- [RagDocumentController.java](/D:/Code/common_agent/src/main/java/com/bruce/rag/controller/RagDocumentController.java)
|
||||||
|
- [RagDocumentQueryRequest.java](/D:/Code/common_agent/src/main/java/com/bruce/rag/dto/request/RagDocumentQueryRequest.java)
|
||||||
|
|
||||||
|
## 4. 下一批建议补充的接口
|
||||||
|
|
||||||
|
当前已有接口能支撑最基础的列表查询,但还不足以支撑统计卡片、右侧详情聚合和单库动作。建议补下面几个接口。
|
||||||
|
|
||||||
|
### 4.1 知识库总览统计
|
||||||
|
|
||||||
|
- `GET /api/rag/stores/overview`
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 顶部 4 张卡片数据
|
||||||
|
|
||||||
|
返回建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"resultcode": "0",
|
||||||
|
"message": null,
|
||||||
|
"data": {
|
||||||
|
"storeCount": 12,
|
||||||
|
"documentCount": 1286,
|
||||||
|
"chunkCount": 24390,
|
||||||
|
"retrievableStoreCount": 9
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议响应 DTO:
|
||||||
|
|
||||||
|
- `RagStoreOverviewResponse`
|
||||||
|
|
||||||
|
字段建议:
|
||||||
|
|
||||||
|
- `storeCount`
|
||||||
|
- `documentCount`
|
||||||
|
- `chunkCount`
|
||||||
|
- `retrievableStoreCount`
|
||||||
|
|
||||||
|
### 4.2 知识库列表查询增强版
|
||||||
|
|
||||||
|
- `POST /api/rag/stores/manage/query`
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 左侧知识库列表
|
||||||
|
|
||||||
|
相比当前 `/query`,建议直接返回列表页需要的摘要字段,避免前端再额外聚合文档数据。
|
||||||
|
|
||||||
|
请求体建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"storeName": "FAQ"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"resultcode": "0",
|
||||||
|
"message": null,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"storeCode": "FAQ",
|
||||||
|
"storeName": "FAQ知识库",
|
||||||
|
"description": "常见问题知识沉淀",
|
||||||
|
"status": "ENABLED",
|
||||||
|
"documentCount": 58,
|
||||||
|
"chunkCount": 920,
|
||||||
|
"indexStatus": "PROCESSING",
|
||||||
|
"retrievable": true,
|
||||||
|
"updateTime": "2026-05-21 11:12:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议响应 DTO:
|
||||||
|
|
||||||
|
- `RagStoreManageListResponse`
|
||||||
|
|
||||||
|
字段建议:
|
||||||
|
|
||||||
|
- `id`
|
||||||
|
- `storeCode`
|
||||||
|
- `storeName`
|
||||||
|
- `description`
|
||||||
|
- `status`
|
||||||
|
- `documentCount`
|
||||||
|
- `chunkCount`
|
||||||
|
- `indexStatus`
|
||||||
|
- `retrievable`
|
||||||
|
- `updateTime`
|
||||||
|
|
||||||
|
### 4.3 查询单个知识库详情增强版
|
||||||
|
|
||||||
|
- `GET /api/rag/stores/{id}/detail`
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 右侧详情区
|
||||||
|
|
||||||
|
返回建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"resultcode": "0",
|
||||||
|
"message": null,
|
||||||
|
"data": {
|
||||||
|
"id": 1,
|
||||||
|
"storeCode": "PROD_DOC",
|
||||||
|
"storeName": "产品制度库",
|
||||||
|
"description": "产品制度、业务规范、流程材料",
|
||||||
|
"status": "ENABLED",
|
||||||
|
"createTime": "2026-05-03 10:20:00",
|
||||||
|
"updateTime": "2026-05-21 16:40:00",
|
||||||
|
"documentCount": 126,
|
||||||
|
"parseSuccessCount": 120,
|
||||||
|
"parseFailedCount": 6,
|
||||||
|
"chunkCount": 3800,
|
||||||
|
"lastUploadTime": "2026-05-21 15:32:00",
|
||||||
|
"lastIndexTime": "2026-05-21 15:48:00",
|
||||||
|
"retrievalMode": "HYBRID",
|
||||||
|
"embeddingModel": "bge-large-zh",
|
||||||
|
"chunkSize": 500,
|
||||||
|
"chunkOverlap": 100,
|
||||||
|
"topK": 5,
|
||||||
|
"retrievable": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议响应 DTO:
|
||||||
|
|
||||||
|
- `RagStoreDetailResponse`
|
||||||
|
|
||||||
|
### 4.4 新建知识库独立接口
|
||||||
|
|
||||||
|
- `POST /api/rag/stores`
|
||||||
|
|
||||||
|
请求体建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"storeCode": "PROD_DOC",
|
||||||
|
"storeName": "产品制度库",
|
||||||
|
"description": "产品制度、业务规范、流程材料"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回建议:
|
||||||
|
|
||||||
|
- 返回新建后的 `id` 或完整 `RagStoreDetailResponse`
|
||||||
|
|
||||||
|
建议请求 DTO:
|
||||||
|
|
||||||
|
- `RagStoreSaveRequest`
|
||||||
|
|
||||||
|
### 4.5 编辑知识库独立接口
|
||||||
|
|
||||||
|
- `PUT /api/rag/stores/{id}`
|
||||||
|
|
||||||
|
请求体建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"storeCode": "PROD_DOC",
|
||||||
|
"storeName": "产品制度库",
|
||||||
|
"description": "产品制度、业务规范、流程材料",
|
||||||
|
"status": "ENABLED"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 右侧“编辑”按钮
|
||||||
|
|
||||||
|
### 4.6 当前知识库批量导入文件
|
||||||
|
|
||||||
|
- `POST /api/rag/stores/{id}/documents/import`
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 右侧“批量导入文件”按钮
|
||||||
|
|
||||||
|
建议请求类型:
|
||||||
|
|
||||||
|
- `multipart/form-data`
|
||||||
|
|
||||||
|
表单字段建议:
|
||||||
|
|
||||||
|
- `files`: 文件数组
|
||||||
|
- `remark`: 批次备注,可选
|
||||||
|
|
||||||
|
返回建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"resultcode": "0",
|
||||||
|
"message": null,
|
||||||
|
"data": {
|
||||||
|
"taskId": 1001,
|
||||||
|
"storeId": 1,
|
||||||
|
"fileCount": 12,
|
||||||
|
"status": "PROCESSING"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议响应 DTO:
|
||||||
|
|
||||||
|
- `RagImportTaskResponse`
|
||||||
|
|
||||||
|
### 4.7 发起当前知识库重建索引
|
||||||
|
|
||||||
|
- `POST /api/rag/stores/{id}/reindex`
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 右侧“重建索引”按钮
|
||||||
|
|
||||||
|
请求体建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"force": true
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"resultcode": "0",
|
||||||
|
"message": null,
|
||||||
|
"data": {
|
||||||
|
"taskId": 1002,
|
||||||
|
"storeId": 1,
|
||||||
|
"status": "PROCESSING"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4.8 查询当前知识库最近任务
|
||||||
|
|
||||||
|
- `GET /api/rag/stores/{id}/tasks?limit=10`
|
||||||
|
|
||||||
|
用途:
|
||||||
|
|
||||||
|
- 右侧“最近任务”区
|
||||||
|
|
||||||
|
返回建议:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"resultcode": "0",
|
||||||
|
"message": null,
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1002,
|
||||||
|
"taskType": "REINDEX",
|
||||||
|
"summary": "全库索引刷新",
|
||||||
|
"status": "PROCESSING",
|
||||||
|
"startedAt": "2026-05-21 16:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 1001,
|
||||||
|
"taskType": "IMPORT",
|
||||||
|
"summary": "12 个文件,制度文档增量导入",
|
||||||
|
"status": "SUCCESS",
|
||||||
|
"startedAt": "2026-05-21 15:20:00"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
建议响应 DTO:
|
||||||
|
|
||||||
|
- `RagStoreTaskResponse`
|
||||||
|
|
||||||
|
## 5. 这页前后端最小联调顺序
|
||||||
|
|
||||||
|
如果想尽快把这页从演示版切到真实联调版,建议按下面顺序接:
|
||||||
|
|
||||||
|
1. 先复用已有:
|
||||||
|
- `POST /api/rag/stores/query`
|
||||||
|
|
||||||
|
2. 然后新增:
|
||||||
|
- `GET /api/rag/stores/overview`
|
||||||
|
- `GET /api/rag/stores/{id}/detail`
|
||||||
|
|
||||||
|
3. 再补动作接口:
|
||||||
|
- `POST /api/rag/stores`
|
||||||
|
- `PUT /api/rag/stores/{id}`
|
||||||
|
- `POST /api/rag/stores/{id}/documents/import`
|
||||||
|
- `POST /api/rag/stores/{id}/reindex`
|
||||||
|
- `GET /api/rag/stores/{id}/tasks`
|
||||||
|
|
||||||
|
## 6. 当前前端实现说明
|
||||||
|
|
||||||
|
当前前端页已经按上述页面结构实现,但由于后端尚未提供完整聚合接口,页面中的统计、详情和任务区先以演示数据承载。
|
||||||
|
|
||||||
|
后端接口齐备后,前端建议按下面方式替换:
|
||||||
|
|
||||||
|
- 统计卡片:改调 `/api/rag/stores/overview`
|
||||||
|
- 左侧列表:改调 `/api/rag/stores/manage/query`
|
||||||
|
- 右侧详情:改调 `/api/rag/stores/{id}/detail`
|
||||||
|
- 批量导入:改调 `/api/rag/stores/{id}/documents/import`
|
||||||
|
- 重建索引:改调 `/api/rag/stores/{id}/reindex`
|
||||||
|
- 最近任务:改调 `/api/rag/stores/{id}/tasks`
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.bruce.common.config;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
|
||||||
|
import org.apache.ibatis.reflection.MetaObject;
|
||||||
|
import org.springframework.stereotype.Component;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class EntityAuditMetaObjectHandler implements MetaObjectHandler {
|
||||||
|
|
||||||
|
private static final String SYSTEM_USER = "system";
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void insertFill(MetaObject metaObject) {
|
||||||
|
Date now = new Date();
|
||||||
|
strictInsertFill(metaObject, "createTime", Date.class, now);
|
||||||
|
strictInsertFill(metaObject, "updateTime", Date.class, now);
|
||||||
|
strictInsertFill(metaObject, "createBy", String.class, SYSTEM_USER);
|
||||||
|
strictInsertFill(metaObject, "updateBy", String.class, SYSTEM_USER);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateFill(MetaObject metaObject) {
|
||||||
|
strictUpdateFill(metaObject, "updateTime", Date.class, new Date());
|
||||||
|
strictUpdateFill(metaObject, "updateBy", String.class, SYSTEM_USER);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,64 +9,88 @@ import com.bruce.common.dto.response.SysEnumResponse;
|
|||||||
import com.bruce.common.service.ISysEnumService;
|
import com.bruce.common.service.ISysEnumService;
|
||||||
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.DeleteMapping;
|
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PathVariable;
|
|
||||||
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 = "系统枚举管理")
|
@Tag(name = "系统枚举管理")
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/sys-enums")
|
@RequestMapping("/api/sys-enum")
|
||||||
public class SysEnumController {
|
public class SysEnumController {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private ISysEnumService sysEnumService;
|
private ISysEnumService sysEnumService;
|
||||||
|
|
||||||
@Operation(summary = "查询全部系统枚举")
|
@Operation(summary = "查询全部系统枚举")
|
||||||
@GetMapping
|
@PostMapping("/list")
|
||||||
public RequestResult<List<SysEnumResponse>> list() {
|
public RequestResult<List<SysEnumResponse>> list() {
|
||||||
return RequestResult.success(sysEnumService.listResponses());
|
log.info("SysEnumController.list start");
|
||||||
|
List<SysEnumResponse> responses = sysEnumService.listResponses();
|
||||||
|
log.info("SysEnumController.list success, count={}", responses.size());
|
||||||
|
return RequestResult.success(responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "根据模块和类型查询系统枚举")
|
@Operation(summary = "根据模块和类型查询系统枚举")
|
||||||
@PostMapping("/query")
|
@PostMapping("/query")
|
||||||
public RequestResult<List<SysEnumResponse>> queryByCatalogAndType(@RequestBody SysEnumQueryRequest request) {
|
public RequestResult<List<SysEnumResponse>> queryByCatalogAndType(@RequestBody SysEnumQueryRequest request) {
|
||||||
return RequestResult.success(sysEnumService.listByCatalogAndTypeResponses(request));
|
log.info("SysEnumController.queryByCatalogAndType start, request={}", request);
|
||||||
|
List<SysEnumResponse> responses = sysEnumService.listByCatalogAndTypeResponses(request);
|
||||||
|
log.info("SysEnumController.queryByCatalogAndType success, count={}", responses.size());
|
||||||
|
return RequestResult.success(responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "管理端查询系统枚举")
|
@Operation(summary = "管理端查询系统枚举")
|
||||||
@PostMapping("/manage/query")
|
@PostMapping("/queryForManagement")
|
||||||
public RequestResult<List<SysEnumResponse>> queryForManagement(@RequestBody(required = false) SysEnumManageQueryRequest request) {
|
public RequestResult<List<SysEnumResponse>> queryForManagement(@RequestBody(required = false) SysEnumManageQueryRequest request) {
|
||||||
return RequestResult.success(sysEnumService.listForManagement(request));
|
log.info("SysEnumController.queryForManagement start, request={}", request);
|
||||||
|
List<SysEnumResponse> responses = sysEnumService.listForManagement(request);
|
||||||
|
log.info("SysEnumController.queryForManagement success, count={}", responses.size());
|
||||||
|
return RequestResult.success(responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "查询系统枚举详情")
|
@Operation(summary = "查询系统枚举详情")
|
||||||
@GetMapping("/{id}")
|
@GetMapping("/detail")
|
||||||
public RequestResult<SysEnumResponse> getById(@PathVariable Long id) {
|
public RequestResult<SysEnumResponse> getById(@RequestParam("id") Long id) {
|
||||||
return RequestResult.success(sysEnumService.getResponseById(id));
|
log.info("SysEnumController.getById start, id={}", id);
|
||||||
|
SysEnumResponse response = sysEnumService.getResponseById(id);
|
||||||
|
log.info("SysEnumController.getById success, id={}, found={}", id, response != null);
|
||||||
|
return RequestResult.success(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "新增或修改系统枚举")
|
@Operation(summary = "新增或修改系统枚举")
|
||||||
@PostMapping
|
@PostMapping("/save")
|
||||||
public RequestResult<Boolean> saveOrUpdate(@RequestBody SysEnumSaveRequest request) {
|
public RequestResult<Boolean> saveOrUpdate(@RequestBody SysEnumSaveRequest request) {
|
||||||
return RequestResult.success(sysEnumService.saveOrUpdate(request));
|
log.info("SysEnumController.saveOrUpdate start, request={}", request);
|
||||||
|
Boolean result = sysEnumService.saveOrUpdate(request);
|
||||||
|
log.info("SysEnumController.saveOrUpdate success, id={}, catalog={}, type={}, value={}, result={}",
|
||||||
|
request.getId(), request.getCatalog(), request.getType(), request.getValue(), result);
|
||||||
|
return RequestResult.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "批量新增系统枚举")
|
@Operation(summary = "批量新增系统枚举")
|
||||||
@PostMapping("/batch")
|
@PostMapping("/batchSave")
|
||||||
public RequestResult<Boolean> batchSave(@RequestBody SysEnumBatchSaveRequest request) {
|
public RequestResult<Boolean> batchSave(@RequestBody SysEnumBatchSaveRequest request) {
|
||||||
return RequestResult.success(sysEnumService.batchSave(request));
|
log.info("SysEnumController.batchSave start, request={}", request);
|
||||||
|
Boolean result = sysEnumService.batchSave(request);
|
||||||
|
log.info("SysEnumController.batchSave success, catalog={}, type={}, itemCount={}, result={}",
|
||||||
|
request.getCatalog(), request.getType(), request.getItems() == null ? 0 : request.getItems().size(), result);
|
||||||
|
return RequestResult.success(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "删除系统枚举")
|
@Operation(summary = "删除系统枚举")
|
||||||
@DeleteMapping("/{id}")
|
@PostMapping("/delete")
|
||||||
public RequestResult<Boolean> deleteById(@PathVariable Long id) {
|
public RequestResult<Boolean> deleteById(@RequestParam("id") Long id) {
|
||||||
return RequestResult.success(sysEnumService.removeById(id));
|
log.info("SysEnumController.deleteById start, id={}", id);
|
||||||
|
Boolean result = sysEnumService.removeById(id);
|
||||||
|
log.info("SysEnumController.deleteById success, id={}, result={}", id, result);
|
||||||
|
return RequestResult.success(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package com.bruce.common.domain.model;
|
package com.bruce.common.domain.model;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
import com.baomidou.mybatisplus.annotation.TableField;
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
@@ -19,21 +20,21 @@ public class BaseEntity {
|
|||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@Schema(description = "创建者")
|
@Schema(description = "创建者")
|
||||||
@TableField(value = "create_by")
|
@TableField(value = "create_by", fill = FieldFill.INSERT)
|
||||||
private String createBy;
|
private String createBy;
|
||||||
|
|
||||||
@Schema(description = "创建时间", example = "2026-05-18 20:00:00")
|
@Schema(description = "创建时间", example = "2026-05-18 20:00:00")
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
@TableField(value = "create_time")
|
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
||||||
private Date createTime;
|
private Date createTime;
|
||||||
|
|
||||||
@Schema(description = "更新者")
|
@Schema(description = "更新者")
|
||||||
@TableField(value = "update_by")
|
@TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
|
||||||
private String updateBy;
|
private String updateBy;
|
||||||
|
|
||||||
@Schema(description = "更新时间", example = "2026-05-18 20:00:00")
|
@Schema(description = "更新时间", example = "2026-05-18 20:00:00")
|
||||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||||
@TableField(value = "update_time")
|
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
|
||||||
private Date updateTime;
|
private Date updateTime;
|
||||||
|
|
||||||
@Schema(description = "版本")
|
@Schema(description = "版本")
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.bruce.common.handler;
|
||||||
|
|
||||||
|
import com.bruce.common.domain.model.RequestResult;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.ExceptionHandler;
|
||||||
|
import org.springframework.web.bind.annotation.RestControllerAdvice;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestControllerAdvice
|
||||||
|
public class GlobalExceptionHandler {
|
||||||
|
|
||||||
|
@ExceptionHandler(IllegalArgumentException.class)
|
||||||
|
public ResponseEntity<RequestResult<Void>> handleIllegalArgumentException(IllegalArgumentException exception) {
|
||||||
|
log.warn("GlobalExceptionHandler.handleIllegalArgumentException, message={}", exception.getMessage(), exception);
|
||||||
|
return buildResponse(HttpStatus.BAD_REQUEST, exception.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
@ExceptionHandler(Exception.class)
|
||||||
|
public ResponseEntity<RequestResult<Void>> handleException(Exception exception) {
|
||||||
|
log.error("GlobalExceptionHandler.handleException", exception);
|
||||||
|
return buildResponse(HttpStatus.INTERNAL_SERVER_ERROR, "系统内部错误,请稍后重试");
|
||||||
|
}
|
||||||
|
|
||||||
|
private ResponseEntity<RequestResult<Void>> buildResponse(HttpStatus status, String message) {
|
||||||
|
return ResponseEntity.status(status)
|
||||||
|
.body(RequestResult.fail(String.valueOf(status.value()), message));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import com.bruce.common.dto.request.SysEnumSaveRequest;
|
|||||||
import com.bruce.common.dto.response.SysEnumResponse;
|
import com.bruce.common.dto.response.SysEnumResponse;
|
||||||
import com.bruce.common.mapper.SysEnumMapper;
|
import com.bruce.common.mapper.SysEnumMapper;
|
||||||
import com.bruce.common.service.ISysEnumService;
|
import com.bruce.common.service.ISysEnumService;
|
||||||
|
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;
|
||||||
|
|
||||||
@@ -16,36 +17,47 @@ import java.util.HashSet;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class SysEnumServiceImpl extends ServiceImpl<SysEnumMapper, SysEnum> implements ISysEnumService {
|
public class SysEnumServiceImpl extends ServiceImpl<SysEnumMapper, SysEnum> implements ISysEnumService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<SysEnum> listByCatalogAndType(SysEnumQueryRequest request) {
|
public List<SysEnum> listByCatalogAndType(SysEnumQueryRequest request) {
|
||||||
|
log.info("SysEnumServiceImpl.listByCatalogAndType start, request={}", request);
|
||||||
if (request == null) {
|
if (request == null) {
|
||||||
throw new IllegalArgumentException("查询请求不能为空");
|
throw new IllegalArgumentException("查询请求不能为空");
|
||||||
}
|
}
|
||||||
return lambdaQuery()
|
List<SysEnum> result = lambdaQuery()
|
||||||
.eq(StringUtils.hasText(request.getCatalog()), SysEnum::getCatalog, request.getCatalog())
|
.eq(StringUtils.hasText(request.getCatalog()), SysEnum::getCatalog, request.getCatalog())
|
||||||
.eq(StringUtils.hasText(request.getType()), SysEnum::getType, request.getType())
|
.eq(StringUtils.hasText(request.getType()), SysEnum::getType, request.getType())
|
||||||
.orderByAsc(SysEnum::getSort)
|
.orderByAsc(SysEnum::getSort)
|
||||||
.list();
|
.list();
|
||||||
|
log.info("SysEnumServiceImpl.listByCatalogAndType success, count={}", result.size());
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<SysEnumResponse> listResponses() {
|
public List<SysEnumResponse> listResponses() {
|
||||||
return toResponses(list());
|
log.info("SysEnumServiceImpl.listResponses start");
|
||||||
|
List<SysEnumResponse> responses = toResponses(list());
|
||||||
|
log.info("SysEnumServiceImpl.listResponses success, count={}", responses.size());
|
||||||
|
return responses;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<SysEnumResponse> listByCatalogAndTypeResponses(SysEnumQueryRequest request) {
|
public List<SysEnumResponse> listByCatalogAndTypeResponses(SysEnumQueryRequest request) {
|
||||||
return toResponses(listByCatalogAndType(request));
|
log.info("SysEnumServiceImpl.listByCatalogAndTypeResponses start");
|
||||||
|
List<SysEnumResponse> responses = toResponses(listByCatalogAndType(request));
|
||||||
|
log.info("SysEnumServiceImpl.listByCatalogAndTypeResponses success, count={}", responses.size());
|
||||||
|
return responses;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<SysEnumResponse> listForManagement(SysEnumManageQueryRequest request) {
|
public List<SysEnumResponse> listForManagement(SysEnumManageQueryRequest request) {
|
||||||
|
log.info("SysEnumServiceImpl.listForManagement start, request={}", request);
|
||||||
SysEnumManageQueryRequest queryRequest = request == null ? new SysEnumManageQueryRequest() : request;
|
SysEnumManageQueryRequest queryRequest = request == null ? new SysEnumManageQueryRequest() : request;
|
||||||
String keyword = queryRequest.getKeyword();
|
String keyword = queryRequest.getKeyword();
|
||||||
return toResponses(lambdaQuery()
|
List<SysEnumResponse> responses = toResponses(lambdaQuery()
|
||||||
.eq(StringUtils.hasText(queryRequest.getCatalog()), SysEnum::getCatalog, queryRequest.getCatalog())
|
.eq(StringUtils.hasText(queryRequest.getCatalog()), SysEnum::getCatalog, queryRequest.getCatalog())
|
||||||
.eq(StringUtils.hasText(queryRequest.getType()), SysEnum::getType, queryRequest.getType())
|
.eq(StringUtils.hasText(queryRequest.getType()), SysEnum::getType, queryRequest.getType())
|
||||||
.and(StringUtils.hasText(keyword), wrapper -> wrapper
|
.and(StringUtils.hasText(keyword), wrapper -> wrapper
|
||||||
@@ -63,15 +75,21 @@ public class SysEnumServiceImpl extends ServiceImpl<SysEnumMapper, SysEnum> impl
|
|||||||
.orderByAsc(SysEnum::getSort)
|
.orderByAsc(SysEnum::getSort)
|
||||||
.orderByAsc(SysEnum::getId)
|
.orderByAsc(SysEnum::getId)
|
||||||
.list());
|
.list());
|
||||||
|
log.info("SysEnumServiceImpl.listForManagement success, count={}", responses.size());
|
||||||
|
return responses;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public SysEnumResponse getResponseById(Long id) {
|
public SysEnumResponse getResponseById(Long id) {
|
||||||
return SysEnumResponse.fromEntity(getById(id));
|
log.info("SysEnumServiceImpl.getResponseById start, id={}", id);
|
||||||
|
SysEnumResponse response = SysEnumResponse.fromEntity(getById(id));
|
||||||
|
log.info("SysEnumServiceImpl.getResponseById success, id={}, found={}", id, response != null);
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean saveOrUpdate(SysEnumSaveRequest request) {
|
public boolean saveOrUpdate(SysEnumSaveRequest request) {
|
||||||
|
log.info("SysEnumServiceImpl.saveOrUpdate start, request={}", request);
|
||||||
if (request == null) {
|
if (request == null) {
|
||||||
throw new IllegalArgumentException("保存请求不能为空");
|
throw new IllegalArgumentException("保存请求不能为空");
|
||||||
}
|
}
|
||||||
@@ -85,11 +103,15 @@ public class SysEnumServiceImpl extends ServiceImpl<SysEnumMapper, SysEnum> impl
|
|||||||
sysEnum.setStrvalue(request.getStrvalue());
|
sysEnum.setStrvalue(request.getStrvalue());
|
||||||
sysEnum.setSort(request.getSort());
|
sysEnum.setSort(request.getSort());
|
||||||
sysEnum.setRemark(request.getRemark());
|
sysEnum.setRemark(request.getRemark());
|
||||||
return super.saveOrUpdate(sysEnum);
|
boolean result = super.saveOrUpdate(sysEnum);
|
||||||
|
log.info("SysEnumServiceImpl.saveOrUpdate success, id={}, catalog={}, type={}, value={}, result={}",
|
||||||
|
request.getId(), request.getCatalog(), request.getType(), request.getValue(), result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean batchSave(SysEnumBatchSaveRequest request) {
|
public boolean batchSave(SysEnumBatchSaveRequest request) {
|
||||||
|
log.info("SysEnumServiceImpl.batchSave start, request={}", request);
|
||||||
List<SysEnum> existingEnums = lambdaQuery()
|
List<SysEnum> existingEnums = lambdaQuery()
|
||||||
.eq(request != null && StringUtils.hasText(request.getCatalog()), SysEnum::getCatalog, request == null ? null : request.getCatalog())
|
.eq(request != null && StringUtils.hasText(request.getCatalog()), SysEnum::getCatalog, request == null ? null : request.getCatalog())
|
||||||
.eq(request != null && StringUtils.hasText(request.getType()), SysEnum::getType, request == null ? null : request.getType())
|
.eq(request != null && StringUtils.hasText(request.getType()), SysEnum::getType, request == null ? null : request.getType())
|
||||||
@@ -109,10 +131,14 @@ public class SysEnumServiceImpl extends ServiceImpl<SysEnumMapper, SysEnum> impl
|
|||||||
return sysEnum;
|
return sysEnum;
|
||||||
})
|
})
|
||||||
.toList();
|
.toList();
|
||||||
return saveBatch(enums);
|
boolean result = saveBatch(enums);
|
||||||
|
log.info("SysEnumServiceImpl.batchSave success, catalog={}, type={}, itemCount={}, result={}",
|
||||||
|
request.getCatalog(), request.getType(), enums.size(), result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void validateBatchSaveRequest(SysEnumBatchSaveRequest request, List<SysEnum> existingEnums) {
|
public void validateBatchSaveRequest(SysEnumBatchSaveRequest request, List<SysEnum> existingEnums) {
|
||||||
|
log.info("SysEnumServiceImpl.validateBatchSaveRequest start");
|
||||||
if (request == null) {
|
if (request == null) {
|
||||||
throw new IllegalArgumentException("批量保存请求不能为空");
|
throw new IllegalArgumentException("批量保存请求不能为空");
|
||||||
}
|
}
|
||||||
@@ -153,6 +179,8 @@ public class SysEnumServiceImpl extends ServiceImpl<SysEnumMapper, SysEnum> impl
|
|||||||
throw new IllegalArgumentException("枚举值已存在: " + value);
|
throw new IllegalArgumentException("枚举值已存在: " + value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
log.info("SysEnumServiceImpl.validateBatchSaveRequest success, catalog={}, type={}, requestValueCount={}, existingValueCount={}",
|
||||||
|
request.getCatalog(), request.getType(), requestValues.size(), existingValues.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<SysEnumResponse> toResponses(List<SysEnum> enums) {
|
private List<SysEnumResponse> toResponses(List<SysEnum> enums) {
|
||||||
|
|||||||
@@ -2,36 +2,74 @@ package com.bruce.rag.controller;
|
|||||||
|
|
||||||
import com.bruce.common.domain.model.RequestResult;
|
import com.bruce.common.domain.model.RequestResult;
|
||||||
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
||||||
|
import com.bruce.rag.dto.request.RagStoreSaveRequest;
|
||||||
import com.bruce.rag.dto.response.RagStoreResponse;
|
import com.bruce.rag.dto.response.RagStoreResponse;
|
||||||
import com.bruce.rag.service.IRagStoreService;
|
import com.bruce.rag.service.IRagStoreService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
import org.springframework.web.bind.annotation.PostMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestBody;
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.RequestMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@Tag(name = "RAG知识库管理")
|
@Tag(name = "RAG知识库管理")
|
||||||
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/rag/stores")
|
@RequestMapping("/api/rag/store")
|
||||||
public class RagStoreController {
|
public class RagStoreController {
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
private IRagStoreService ragStoreService;
|
private IRagStoreService ragStoreService;
|
||||||
|
|
||||||
@Operation(summary = "查询全部知识库")
|
@Operation(summary = "查询全部知识库")
|
||||||
@GetMapping
|
@PostMapping("/list")
|
||||||
public RequestResult<List<RagStoreResponse>> list() {
|
public RequestResult<List<RagStoreResponse>> list() {
|
||||||
return RequestResult.success(ragStoreService.listResponses());
|
log.info("RagStoreController.list start");
|
||||||
|
List<RagStoreResponse> responses = ragStoreService.listResponses();
|
||||||
|
log.info("RagStoreController.list success, count={}", responses.size());
|
||||||
|
return RequestResult.success(responses);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Operation(summary = "按条件查询知识库")
|
@Operation(summary = "按条件查询知识库")
|
||||||
@PostMapping("/query")
|
@PostMapping("/query")
|
||||||
public RequestResult<List<RagStoreResponse>> query(@RequestBody RagStoreQueryRequest request) {
|
public RequestResult<List<RagStoreResponse>> query(@RequestBody(required = false) RagStoreQueryRequest request) {
|
||||||
return RequestResult.success(ragStoreService.query(request));
|
log.info("RagStoreController.query start, request={}", request);
|
||||||
|
List<RagStoreResponse> responses = ragStoreService.query(request);
|
||||||
|
log.info("RagStoreController.query success, count={}", responses.size());
|
||||||
|
return RequestResult.success(responses);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "查询知识库详情")
|
||||||
|
@GetMapping("/detail")
|
||||||
|
public RequestResult<RagStoreResponse> getById(@RequestParam("id") Long id) {
|
||||||
|
log.info("RagStoreController.getById start, id={}", id);
|
||||||
|
RagStoreResponse response = ragStoreService.getResponseById(id);
|
||||||
|
log.info("RagStoreController.getById success, id={}, found={}", id, response != null);
|
||||||
|
return RequestResult.success(response);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "新增或修改知识库")
|
||||||
|
@PostMapping("/save")
|
||||||
|
public RequestResult<Boolean> saveOrUpdate(@RequestBody RagStoreSaveRequest request) {
|
||||||
|
log.info("RagStoreController.saveOrUpdate start, request={}", request);
|
||||||
|
Boolean result = ragStoreService.saveOrUpdate(request);
|
||||||
|
log.info("RagStoreController.saveOrUpdate success, id={}, storeCode={}, result={}",
|
||||||
|
request.getId(), request.getStoreCode(), result);
|
||||||
|
return RequestResult.success(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Operation(summary = "删除知识库")
|
||||||
|
@PostMapping("/delete")
|
||||||
|
public RequestResult<Boolean> deleteById(@RequestParam("id") Long id) {
|
||||||
|
log.info("RagStoreController.deleteById start, id={}", id);
|
||||||
|
Boolean result = ragStoreService.removeById(id);
|
||||||
|
log.info("RagStoreController.deleteById success, id={}, result={}", id, result);
|
||||||
|
return RequestResult.success(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.bruce.rag.dto.request;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Schema(description = "RAG知识库保存请求")
|
||||||
|
public class RagStoreSaveRequest {
|
||||||
|
|
||||||
|
@Schema(description = "主键ID")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Schema(description = "知识库编码")
|
||||||
|
private String storeCode;
|
||||||
|
|
||||||
|
@Schema(description = "知识库名称")
|
||||||
|
private String storeName;
|
||||||
|
|
||||||
|
@Schema(description = "知识库描述")
|
||||||
|
private String description;
|
||||||
|
|
||||||
|
@Schema(description = "状态")
|
||||||
|
private String status;
|
||||||
|
|
||||||
|
@Schema(description = "备注")
|
||||||
|
private String remark;
|
||||||
|
}
|
||||||
@@ -1,14 +1,19 @@
|
|||||||
package com.bruce.rag.dto.response;
|
package com.bruce.rag.dto.response;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||||
|
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||||
import com.bruce.rag.entity.RagStore;
|
import com.bruce.rag.entity.RagStore;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@Schema(description = "RAG知识库响应")
|
@Schema(description = "RAG知识库响应")
|
||||||
public class RagStoreResponse {
|
public class RagStoreResponse {
|
||||||
|
|
||||||
|
@JsonSerialize(using = ToStringSerializer.class)
|
||||||
@Schema(description = "主键ID")
|
@Schema(description = "主键ID")
|
||||||
private Long id;
|
private Long id;
|
||||||
|
|
||||||
@@ -27,6 +32,12 @@ public class RagStoreResponse {
|
|||||||
@Schema(description = "备注")
|
@Schema(description = "备注")
|
||||||
private String remark;
|
private String remark;
|
||||||
|
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private Date createTime;
|
||||||
|
|
||||||
|
@Schema(description = "更新时间")
|
||||||
|
private Date updateTime;
|
||||||
|
|
||||||
public static RagStoreResponse fromEntity(RagStore entity) {
|
public static RagStoreResponse fromEntity(RagStore entity) {
|
||||||
if (entity == null) {
|
if (entity == null) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.bruce.rag.service;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.IService;
|
import com.baomidou.mybatisplus.extension.service.IService;
|
||||||
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
||||||
|
import com.bruce.rag.dto.request.RagStoreSaveRequest;
|
||||||
import com.bruce.rag.dto.response.RagStoreResponse;
|
import com.bruce.rag.dto.response.RagStoreResponse;
|
||||||
import com.bruce.rag.entity.RagStore;
|
import com.bruce.rag.entity.RagStore;
|
||||||
|
|
||||||
@@ -12,4 +13,8 @@ public interface IRagStoreService extends IService<RagStore> {
|
|||||||
List<RagStoreResponse> listResponses();
|
List<RagStoreResponse> listResponses();
|
||||||
|
|
||||||
List<RagStoreResponse> query(RagStoreQueryRequest request);
|
List<RagStoreResponse> query(RagStoreQueryRequest request);
|
||||||
|
|
||||||
|
RagStoreResponse getResponseById(Long id);
|
||||||
|
|
||||||
|
boolean saveOrUpdate(RagStoreSaveRequest request);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,34 +2,92 @@ package com.bruce.rag.service.impl;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||||
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
||||||
|
import com.bruce.rag.dto.request.RagStoreSaveRequest;
|
||||||
import com.bruce.rag.dto.response.RagStoreResponse;
|
import com.bruce.rag.dto.response.RagStoreResponse;
|
||||||
import com.bruce.rag.entity.RagStore;
|
import com.bruce.rag.entity.RagStore;
|
||||||
import com.bruce.rag.mapper.RagStoreMapper;
|
import com.bruce.rag.mapper.RagStoreMapper;
|
||||||
import com.bruce.rag.service.IRagStoreService;
|
import com.bruce.rag.service.IRagStoreService;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Service;
|
import org.springframework.stereotype.Service;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@Service
|
@Service
|
||||||
public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> implements IRagStoreService {
|
public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> implements IRagStoreService {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<RagStoreResponse> listResponses() {
|
public List<RagStoreResponse> listResponses() {
|
||||||
return toResponses(list());
|
log.info("RagStoreServiceImpl.listResponses start");
|
||||||
|
List<RagStoreResponse> responses = toResponses(list());
|
||||||
|
log.info("RagStoreServiceImpl.listResponses success, count={}", responses.size());
|
||||||
|
return responses;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<RagStoreResponse> query(RagStoreQueryRequest request) {
|
public List<RagStoreResponse> query(RagStoreQueryRequest request) {
|
||||||
if (request == null) {
|
log.info("RagStoreServiceImpl.query start, request={}", request);
|
||||||
throw new IllegalArgumentException("查询请求不能为空");
|
RagStoreQueryRequest queryRequest = request == null ? new RagStoreQueryRequest() : request;
|
||||||
}
|
List<RagStoreResponse> responses = toResponses(lambdaQuery()
|
||||||
return toResponses(lambdaQuery()
|
.eq(StringUtils.hasText(queryRequest.getStoreCode()), RagStore::getStoreCode, queryRequest.getStoreCode())
|
||||||
.eq(StringUtils.hasText(request.getStoreCode()), RagStore::getStoreCode, request.getStoreCode())
|
.like(StringUtils.hasText(queryRequest.getStoreName()), RagStore::getStoreName, queryRequest.getStoreName())
|
||||||
.like(StringUtils.hasText(request.getStoreName()), RagStore::getStoreName, request.getStoreName())
|
.eq(StringUtils.hasText(queryRequest.getStatus()), RagStore::getStatus, queryRequest.getStatus())
|
||||||
.eq(StringUtils.hasText(request.getStatus()), RagStore::getStatus, request.getStatus())
|
|
||||||
.orderByAsc(RagStore::getStoreCode)
|
.orderByAsc(RagStore::getStoreCode)
|
||||||
.list());
|
.list());
|
||||||
|
log.info("RagStoreServiceImpl.query success, count={}", responses.size());
|
||||||
|
return responses;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RagStoreResponse getResponseById(Long id) {
|
||||||
|
log.info("RagStoreServiceImpl.getResponseById start, id={}", id);
|
||||||
|
RagStoreResponse response = RagStoreResponse.fromEntity(getById(id));
|
||||||
|
log.info("RagStoreServiceImpl.getResponseById success, id={}, found={}", id, response != null);
|
||||||
|
return response;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean saveOrUpdate(RagStoreSaveRequest request) {
|
||||||
|
log.info("RagStoreServiceImpl.saveOrUpdate start, request={}", request);
|
||||||
|
validateSaveRequest(request);
|
||||||
|
|
||||||
|
RagStore existingStore = lambdaQuery()
|
||||||
|
.eq(RagStore::getStoreCode, request.getStoreCode().trim())
|
||||||
|
.ne(request.getId() != null, RagStore::getId, request.getId())
|
||||||
|
.one();
|
||||||
|
if (existingStore != null) {
|
||||||
|
log.warn("RagStoreServiceImpl.saveOrUpdate duplicate storeCode detected, requestId={}, existingId={}, storeCode={}",
|
||||||
|
request.getId(), existingStore.getId(), request.getStoreCode().trim());
|
||||||
|
throw new IllegalArgumentException("知识库编码已存在: " + request.getStoreCode().trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
RagStore ragStore = new RagStore();
|
||||||
|
ragStore.setId(request.getId());
|
||||||
|
ragStore.setStoreCode(request.getStoreCode().trim());
|
||||||
|
ragStore.setStoreName(request.getStoreName().trim());
|
||||||
|
ragStore.setDescription(trimToNull(request.getDescription()));
|
||||||
|
ragStore.setStatus(StringUtils.hasText(request.getStatus()) ? request.getStatus().trim() : "启用");
|
||||||
|
ragStore.setRemark(trimToNull(request.getRemark()));
|
||||||
|
boolean result = super.saveOrUpdate(ragStore);
|
||||||
|
log.info("RagStoreServiceImpl.saveOrUpdate success, requestId={}, savedId={}, storeCode={}, result={}",
|
||||||
|
request.getId(), ragStore.getId(), ragStore.getStoreCode(), result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void validateSaveRequest(RagStoreSaveRequest request) {
|
||||||
|
log.info("RagStoreServiceImpl.validateSaveRequest start");
|
||||||
|
if (request == null) {
|
||||||
|
throw new IllegalArgumentException("保存请求不能为空");
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasText(request.getStoreCode())) {
|
||||||
|
throw new IllegalArgumentException("知识库编码不能为空");
|
||||||
|
}
|
||||||
|
if (!StringUtils.hasText(request.getStoreName())) {
|
||||||
|
throw new IllegalArgumentException("知识库名称不能为空");
|
||||||
|
}
|
||||||
|
log.info("RagStoreServiceImpl.validateSaveRequest success, id={}, storeCode={}, storeName={}",
|
||||||
|
request.getId(), request.getStoreCode(), request.getStoreName());
|
||||||
}
|
}
|
||||||
|
|
||||||
private List<RagStoreResponse> toResponses(List<RagStore> stores) {
|
private List<RagStoreResponse> toResponses(List<RagStore> stores) {
|
||||||
@@ -37,4 +95,11 @@ public class RagStoreServiceImpl extends ServiceImpl<RagStoreMapper, RagStore> i
|
|||||||
.map(RagStoreResponse::fromEntity)
|
.map(RagStoreResponse::fromEntity)
|
||||||
.toList();
|
.toList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String trimToNull(String value) {
|
||||||
|
if (!StringUtils.hasText(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package com.bruce.common.config;
|
||||||
|
|
||||||
|
import com.bruce.rag.entity.RagStore;
|
||||||
|
import org.apache.ibatis.reflection.SystemMetaObject;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
|
class EntityAuditMetaObjectHandlerTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void insertFillShouldPopulateAuditFields() {
|
||||||
|
EntityAuditMetaObjectHandler handler = new EntityAuditMetaObjectHandler();
|
||||||
|
RagStore entity = new RagStore();
|
||||||
|
|
||||||
|
handler.insertFill(SystemMetaObject.forObject(entity));
|
||||||
|
|
||||||
|
assertNotNull(entity.getCreateTime());
|
||||||
|
assertNotNull(entity.getUpdateTime());
|
||||||
|
assertEquals("system", entity.getCreateBy());
|
||||||
|
assertEquals("system", entity.getUpdateBy());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void updateFillShouldRefreshUpdateAuditFields() {
|
||||||
|
EntityAuditMetaObjectHandler handler = new EntityAuditMetaObjectHandler();
|
||||||
|
RagStore entity = new RagStore();
|
||||||
|
entity.setUpdateBy("oldUser");
|
||||||
|
|
||||||
|
handler.updateFill(SystemMetaObject.forObject(entity));
|
||||||
|
|
||||||
|
assertNotNull(entity.getUpdateTime());
|
||||||
|
assertEquals("system", entity.getUpdateBy());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package com.bruce.common.entity;
|
package com.bruce.common.entity;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||||
import com.baomidou.mybatisplus.annotation.IdType;
|
import com.baomidou.mybatisplus.annotation.IdType;
|
||||||
|
import com.baomidou.mybatisplus.annotation.TableField;
|
||||||
import com.baomidou.mybatisplus.annotation.TableId;
|
import com.baomidou.mybatisplus.annotation.TableId;
|
||||||
import com.baomidou.mybatisplus.annotation.Version;
|
import com.baomidou.mybatisplus.annotation.Version;
|
||||||
import com.bruce.common.domain.model.BaseEntity;
|
import com.bruce.common.domain.model.BaseEntity;
|
||||||
@@ -65,4 +67,21 @@ class EntityStructureTests {
|
|||||||
assertTrue(Arrays.stream(SysEnum.class.getDeclaredFields()).noneMatch(field -> "version".equals(field.getName())));
|
assertTrue(Arrays.stream(SysEnum.class.getDeclaredFields()).noneMatch(field -> "version".equals(field.getName())));
|
||||||
assertTrue(Arrays.stream(SysAttachment.class.getDeclaredFields()).noneMatch(field -> "version".equals(field.getName())));
|
assertTrue(Arrays.stream(SysAttachment.class.getDeclaredFields()).noneMatch(field -> "version".equals(field.getName())));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void auditFieldsShouldUseMybatisPlusAutoFill() throws NoSuchFieldException {
|
||||||
|
TableField createBy = BaseEntity.class.getDeclaredField("createBy").getAnnotation(TableField.class);
|
||||||
|
TableField createTime = BaseEntity.class.getDeclaredField("createTime").getAnnotation(TableField.class);
|
||||||
|
TableField updateBy = BaseEntity.class.getDeclaredField("updateBy").getAnnotation(TableField.class);
|
||||||
|
TableField updateTime = BaseEntity.class.getDeclaredField("updateTime").getAnnotation(TableField.class);
|
||||||
|
|
||||||
|
assertNotNull(createBy);
|
||||||
|
assertNotNull(createTime);
|
||||||
|
assertNotNull(updateBy);
|
||||||
|
assertNotNull(updateTime);
|
||||||
|
assertEquals(FieldFill.INSERT, createBy.fill());
|
||||||
|
assertEquals(FieldFill.INSERT, createTime.fill());
|
||||||
|
assertEquals(FieldFill.INSERT_UPDATE, updateBy.fill());
|
||||||
|
assertEquals(FieldFill.INSERT_UPDATE, updateTime.fill());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package com.bruce.common.handler;
|
||||||
|
|
||||||
|
import com.bruce.common.domain.model.RequestResult;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
|
||||||
|
class GlobalExceptionHandlerTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnStructuredBadRequestForIllegalArgumentException() {
|
||||||
|
GlobalExceptionHandler handler = new GlobalExceptionHandler();
|
||||||
|
ResponseEntity<RequestResult<Void>> response = handler.handleIllegalArgumentException(
|
||||||
|
new IllegalArgumentException("知识库编码已存在: TEST-1"));
|
||||||
|
|
||||||
|
assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode());
|
||||||
|
assertNotNull(response.getBody());
|
||||||
|
assertEquals("400", response.getBody().getResultcode());
|
||||||
|
assertEquals("知识库编码已存在: TEST-1", response.getBody().getMessage());
|
||||||
|
assertEquals(null, response.getBody().getData());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import com.bruce.rag.controller.RagDocumentController;
|
|||||||
import com.bruce.rag.controller.RagStoreController;
|
import com.bruce.rag.controller.RagStoreController;
|
||||||
import com.bruce.rag.dto.request.RagDocumentQueryRequest;
|
import com.bruce.rag.dto.request.RagDocumentQueryRequest;
|
||||||
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
import com.bruce.rag.dto.request.RagStoreQueryRequest;
|
||||||
|
import com.bruce.rag.dto.request.RagStoreSaveRequest;
|
||||||
import com.bruce.rag.dto.response.RagDocumentResponse;
|
import com.bruce.rag.dto.response.RagDocumentResponse;
|
||||||
import com.bruce.rag.dto.response.RagStoreResponse;
|
import com.bruce.rag.dto.response.RagStoreResponse;
|
||||||
import com.bruce.rag.entity.RagDocument;
|
import com.bruce.rag.entity.RagDocument;
|
||||||
@@ -44,8 +45,13 @@ class RagComponentStructureTests {
|
|||||||
void ragControllersShouldExposeRequestResultAndQueryDtoMethods() throws NoSuchMethodException {
|
void ragControllersShouldExposeRequestResultAndQueryDtoMethods() throws NoSuchMethodException {
|
||||||
Method storeListMethod = RagStoreController.class.getMethod("list");
|
Method storeListMethod = RagStoreController.class.getMethod("list");
|
||||||
Method storeQueryMethod = RagStoreController.class.getMethod("query", RagStoreQueryRequest.class);
|
Method storeQueryMethod = RagStoreController.class.getMethod("query", RagStoreQueryRequest.class);
|
||||||
|
Method storeDetailMethod = RagStoreController.class.getMethod("getById", Long.class);
|
||||||
|
Method storeSaveMethod = RagStoreController.class.getMethod("saveOrUpdate", RagStoreSaveRequest.class);
|
||||||
|
Method storeDeleteMethod = RagStoreController.class.getMethod("deleteById", Long.class);
|
||||||
Method storeResponseListMethod = IRagStoreService.class.getMethod("listResponses");
|
Method storeResponseListMethod = IRagStoreService.class.getMethod("listResponses");
|
||||||
Method storeServiceQueryMethod = IRagStoreService.class.getMethod("query", RagStoreQueryRequest.class);
|
Method storeServiceQueryMethod = IRagStoreService.class.getMethod("query", RagStoreQueryRequest.class);
|
||||||
|
Method storeServiceDetailMethod = IRagStoreService.class.getMethod("getResponseById", Long.class);
|
||||||
|
Method storeServiceSaveMethod = IRagStoreService.class.getMethod("saveOrUpdate", RagStoreSaveRequest.class);
|
||||||
|
|
||||||
Method documentListMethod = RagDocumentController.class.getMethod("list");
|
Method documentListMethod = RagDocumentController.class.getMethod("list");
|
||||||
Method documentQueryMethod = RagDocumentController.class.getMethod("query", RagDocumentQueryRequest.class);
|
Method documentQueryMethod = RagDocumentController.class.getMethod("query", RagDocumentQueryRequest.class);
|
||||||
@@ -54,11 +60,17 @@ class RagComponentStructureTests {
|
|||||||
|
|
||||||
assertEquals(RequestResult.class, storeListMethod.getReturnType());
|
assertEquals(RequestResult.class, storeListMethod.getReturnType());
|
||||||
assertEquals(RequestResult.class, storeQueryMethod.getReturnType());
|
assertEquals(RequestResult.class, storeQueryMethod.getReturnType());
|
||||||
|
assertEquals(RequestResult.class, storeDetailMethod.getReturnType());
|
||||||
|
assertEquals(RequestResult.class, storeSaveMethod.getReturnType());
|
||||||
|
assertEquals(RequestResult.class, storeDeleteMethod.getReturnType());
|
||||||
assertEquals(List.class, storeServiceQueryMethod.getReturnType());
|
assertEquals(List.class, storeServiceQueryMethod.getReturnType());
|
||||||
|
assertEquals(RagStoreResponse.class, storeServiceDetailMethod.getReturnType());
|
||||||
|
assertEquals(boolean.class, storeServiceSaveMethod.getReturnType());
|
||||||
assertTrue(storeResponseListMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
assertTrue(storeResponseListMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
||||||
assertTrue(storeServiceQueryMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
assertTrue(storeServiceQueryMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
||||||
assertTrue(storeListMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
assertTrue(storeListMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
||||||
assertTrue(storeQueryMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
assertTrue(storeQueryMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
||||||
|
assertTrue(storeDetailMethod.getGenericReturnType().getTypeName().contains("RagStoreResponse"));
|
||||||
assertEquals(RagStoreResponse.class, RagStoreResponse.class.getMethod("fromEntity", RagStore.class).getReturnType());
|
assertEquals(RagStoreResponse.class, RagStoreResponse.class.getMethod("fromEntity", RagStore.class).getReturnType());
|
||||||
|
|
||||||
assertEquals(RequestResult.class, documentListMethod.getReturnType());
|
assertEquals(RequestResult.class, documentListMethod.getReturnType());
|
||||||
|
|||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package com.bruce.rag;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
import com.bruce.rag.dto.response.RagStoreResponse;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
|
||||||
|
class RagStoreResponseSerializationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void idShouldSerializeAsStringForFrontendPrecisionSafety() throws Exception {
|
||||||
|
RagStoreResponse response = new RagStoreResponse();
|
||||||
|
response.setId(2057302206052372481L);
|
||||||
|
response.setStoreCode("TEXT-1");
|
||||||
|
response.setStoreName("测试库1");
|
||||||
|
|
||||||
|
String json = new ObjectMapper().writeValueAsString(response);
|
||||||
|
|
||||||
|
assertTrue(json.contains("\"id\":\"2057302206052372481\""));
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/test/java/com/bruce/rag/RagStoreSaveValidationTests.java
Normal file
39
src/test/java/com/bruce/rag/RagStoreSaveValidationTests.java
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
package com.bruce.rag;
|
||||||
|
|
||||||
|
import com.bruce.rag.dto.request.RagStoreSaveRequest;
|
||||||
|
import com.bruce.rag.service.impl.RagStoreServiceImpl;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||||
|
|
||||||
|
class RagStoreSaveValidationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveShouldRejectBlankStoreCode() {
|
||||||
|
RagStoreServiceImpl service = new RagStoreServiceImpl();
|
||||||
|
RagStoreSaveRequest request = new RagStoreSaveRequest();
|
||||||
|
request.setStoreName("产品制度库");
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> service.validateSaveRequest(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveShouldRejectBlankStoreName() {
|
||||||
|
RagStoreServiceImpl service = new RagStoreServiceImpl();
|
||||||
|
RagStoreSaveRequest request = new RagStoreSaveRequest();
|
||||||
|
request.setStoreCode("PROD_DOC");
|
||||||
|
|
||||||
|
assertThrows(IllegalArgumentException.class, () -> service.validateSaveRequest(request));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void saveShouldAcceptMinimalValidRequest() {
|
||||||
|
RagStoreServiceImpl service = new RagStoreServiceImpl();
|
||||||
|
RagStoreSaveRequest request = new RagStoreSaveRequest();
|
||||||
|
request.setStoreCode("PROD_DOC");
|
||||||
|
request.setStoreName("产品制度库");
|
||||||
|
|
||||||
|
assertDoesNotThrow(() -> service.validateSaveRequest(request));
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user