first commit
This commit is contained in:
@@ -0,0 +1,747 @@
|
||||
# RuoYi Chat 模块
|
||||
|
||||
## 模块简介
|
||||
|
||||
`ruoyi-chat` 是 PoJie 项目的智能聊天系统模块,提供完整的聊天会话管理、消息处理、知识库集成和实时通信功能。该模块通过集成 `ruoyi-ai` 和 `ruoyi-knowledge` 模块,实现了基于知识库的智能问答系统(RAG),支持多种通信方式(REST API、SSE 流式、WebSocket)。
|
||||
|
||||
## 功能特性
|
||||
|
||||
### 1. 聊天会话管理 ✅
|
||||
|
||||
- ✅ 创建和管理聊天会话
|
||||
- ✅ 支持多用户并发聊天
|
||||
- ✅ 会话历史记录和导出
|
||||
- ✅ 会话状态管理
|
||||
- ✅ 会话标题自动生成和手动编辑
|
||||
|
||||
### 2. 消息处理 ✅
|
||||
|
||||
- ✅ 实时消息发送和接收
|
||||
- ✅ 消息类型支持(文本、图片等)
|
||||
- ✅ 消息历史查询(支持分页和条件筛选)
|
||||
- ✅ 消息状态跟踪
|
||||
- ✅ Token 使用量和成本统计
|
||||
|
||||
### 3. 知识库集成(RAG)✅
|
||||
|
||||
- ✅ 基于知识库的智能问答
|
||||
- ✅ 上下文理解和构建
|
||||
- ✅ 向量相似度搜索
|
||||
- ✅ 智能回复生成
|
||||
- ✅ 知识库会话管理
|
||||
- ✅ 知识库内容搜索
|
||||
- **内部对接模块**:
|
||||
- AI 模块:`AiChatService`(统一聊天接口)
|
||||
- 知识库模块:`KnowledgeVectorService`(向量检索)
|
||||
|
||||
### 4. 实时通信 ✅
|
||||
|
||||
- ✅ **REST API**:标准 HTTP 接口
|
||||
- ✅ **SSE 流式**:服务器推送事件,支持流式对话(`ChatOpenApiController`)
|
||||
- ✅ **WebSocket**:STOMP 协议,实时双向通信(`ChatStompController`)
|
||||
|
||||
### 5. 使用统计 ✅
|
||||
|
||||
- ✅ Token 使用量统计
|
||||
- ✅ 聊天成本计算
|
||||
- ✅ 用户活跃度分析
|
||||
- ✅ 模型使用情况
|
||||
- ✅ 使用记录导出
|
||||
|
||||
### 6. 配置管理 ✅
|
||||
|
||||
- ✅ 聊天配置管理(键值对配置)
|
||||
- ✅ 模型配置管理(模型信息、价格、API 配置)
|
||||
- ✅ 配置分类管理
|
||||
- ✅ 配置导入导出
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
ruoyi-chat/
|
||||
├─ src/main/java/org/dromara/chat/
|
||||
│ ├─ controller/ # REST 控制器层
|
||||
│ │ ├─ AIKnowledgeChatController.java # 知识库聊天控制器
|
||||
│ │ ├─ ChatSessionController.java # 会话管理控制器
|
||||
│ │ ├─ ChatMessageController.java # 消息管理控制器
|
||||
│ │ ├─ ChatOpenApiController.java # 开放 API 控制器(SSE 流式)
|
||||
│ │ ├─ ChatStompController.java # WebSocket 控制器(STOMP)
|
||||
│ │ ├─ ChatConfigController.java # 配置管理控制器
|
||||
│ │ ├─ ChatModelController.java # 模型管理控制器
|
||||
│ │ └─ ChatUsageTokenController.java # Token 使用记录控制器
|
||||
│ ├─ service/ # 核心业务服务层
|
||||
│ │ ├─ AiKnowledgeChatService.java # 知识库聊天服务
|
||||
│ │ ├─ ChatSessionService.java # 会话管理服务
|
||||
│ │ ├─ ChatMessageService.java # 消息管理服务
|
||||
│ │ ├─ ChatConfigService.java # 配置管理服务
|
||||
│ │ ├─ ChatModelService.java # 模型管理服务
|
||||
│ │ ├─ ChatUsageTokenService.java # Token 使用记录服务
|
||||
│ │ └─ impl/ # 服务实现类
|
||||
│ ├─ domain/ # 领域模型
|
||||
│ │ ├─ bo/ # 业务对象(Business Object)
|
||||
│ │ ├─ vo/ # 视图对象(View Object)
|
||||
│ │ ├─ request/ # 请求对象
|
||||
│ │ └─ *.java # 实体类(Entity)
|
||||
│ ├─ mapper/ # MyBatis Mapper 接口
|
||||
│ ├─ config/ # 配置类(WebSocket 配置等)
|
||||
│ └─ client/ # Java SDK(统一网关封装)
|
||||
│ └─ ChatSdk.java
|
||||
└─ docs/ # API 使用说明文档
|
||||
```
|
||||
|
||||
## 模块结构
|
||||
|
||||
```
|
||||
org.dromara.chat
|
||||
├── controller/ # 控制器层
|
||||
│ ├── AIKnowledgeChatController.java # 知识库聊天
|
||||
│ ├── ChatSessionController.java # 会话管理
|
||||
│ ├── ChatMessageController.java # 消息管理
|
||||
│ ├── ChatOpenApiController.java # 开放 API(SSE)
|
||||
│ ├── ChatStompController.java # WebSocket(STOMP)
|
||||
│ ├── ChatConfigController.java # 配置管理
|
||||
│ ├── ChatModelController.java # 模型管理
|
||||
│ └── ChatUsageTokenController.java # Token 统计
|
||||
├── service/ # 服务层
|
||||
│ ├── AiKnowledgeChatService.java
|
||||
│ ├── ChatSessionService.java
|
||||
│ ├── ChatMessageService.java
|
||||
│ ├── ChatConfigService.java
|
||||
│ ├── ChatModelService.java
|
||||
│ ├── ChatUsageTokenService.java
|
||||
│ └── impl/ # 服务实现
|
||||
├── domain/ # 领域模型
|
||||
│ ├── bo/ # 业务对象
|
||||
│ ├── vo/ # 视图对象
|
||||
│ ├── request/ # 请求对象
|
||||
│ ├── ChatSession.java # 会话实体
|
||||
│ ├── ChatMessage.java # 消息实体
|
||||
│ ├── ChatConfig.java # 配置实体
|
||||
│ ├── ChatModel.java # 模型实体
|
||||
│ └── ChatUsageToken.java # Token 使用实体
|
||||
├── mapper/ # MyBatis Mapper
|
||||
├── config/ # 配置类
|
||||
│ └── StompWebSocketConfig.java
|
||||
└── client/ # Java SDK
|
||||
└── ChatSdk.java
|
||||
```
|
||||
|
||||
## 架构设计
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Chat Controller Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ AIKnowledgeChatController │ ChatSessionController │
|
||||
│ ChatMessageController │ ChatConfigController │
|
||||
│ ChatModelController │ ChatUsageTokenController │
|
||||
│ ChatOpenApiController │ ChatStompController │
|
||||
│ (SSE 流式) │ (WebSocket) │
|
||||
└─────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────▼───────────────────────────────────────┐
|
||||
│ Service Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ AiKnowledgeChatService │ ChatSessionService │
|
||||
│ ChatMessageService │ ChatConfigService │
|
||||
│ ChatModelService │ ChatUsageTokenService │
|
||||
└─────────────────────┬───────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────────────▼───────────────────────────────────────┐
|
||||
│ Integration Layer │
|
||||
├─────────────────────────────────────────────────────────────┤
|
||||
│ RuoYi AI Module │
|
||||
│ └─ AiChatService │
|
||||
│ └─ AiChatHistoryService │
|
||||
│ └─ AiProviderManager │
|
||||
│ │
|
||||
│ RuoYi Knowledge Module │
|
||||
│ └─ KnowledgeVectorService │
|
||||
│ └─ KnowledgeInfoService │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 数据库表结构
|
||||
|
||||
### 1. 聊天会话表 (chat_session)
|
||||
|
||||
存储用户聊天会话信息。
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| id | BIGINT | 主键ID |
|
||||
| user_id | BIGINT | 用户ID |
|
||||
| session_title | VARCHAR(255) | 会话标题 |
|
||||
| session_content | TEXT | 会话内容 |
|
||||
| create_by | BIGINT | 创建者 |
|
||||
| create_time | DATETIME | 创建时间 |
|
||||
| update_by | BIGINT | 更新者 |
|
||||
| update_time | DATETIME | 更新时间 |
|
||||
| remark | VARCHAR(500) | 备注 |
|
||||
|
||||
**索引**:`PRIMARY KEY (id)`
|
||||
|
||||
### 2. 聊天消息表 (chat_message)
|
||||
|
||||
存储聊天消息记录。
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| id | BIGINT | 主键ID |
|
||||
| session_id | BIGINT | 会话ID |
|
||||
| user_id | BIGINT | 用户ID |
|
||||
| content | LONGTEXT | 消息内容 |
|
||||
| role | VARCHAR(255) | 对话角色(user/assistant/system) |
|
||||
| deduct_cost | DOUBLE(20,2) | 扣除金额(默认 0.00) |
|
||||
| total_tokens | INT(20) | 累计 Tokens(默认 0) |
|
||||
| model_name | VARCHAR(255) | 模型名称 |
|
||||
| create_by | BIGINT | 创建者 |
|
||||
| create_time | DATETIME | 创建时间 |
|
||||
| update_by | BIGINT | 更新者 |
|
||||
| update_time | DATETIME | 更新时间 |
|
||||
| remark | VARCHAR(500) | 备注 |
|
||||
|
||||
**索引**:`PRIMARY KEY (id)`
|
||||
|
||||
### 3. 聊天配置表 (chat_config)
|
||||
|
||||
存储聊天系统配置(键值对形式)。
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| id | BIGINT | 主键ID |
|
||||
| category | VARCHAR(255) | 配置类型 |
|
||||
| config_name | VARCHAR(255) | 配置名称 |
|
||||
| config_value | TEXT | 配置值 |
|
||||
| config_dict | VARCHAR(255) | 说明 |
|
||||
| create_by | BIGINT | 创建者 |
|
||||
| create_time | DATETIME | 创建时间 |
|
||||
| update_by | BIGINT | 更新者 |
|
||||
| update_time | DATETIME | 更新时间 |
|
||||
| remark | VARCHAR(500) | 备注 |
|
||||
| version | INT | 版本 |
|
||||
| del_flag | CHAR(1) | 删除标志(0存在 1删除) |
|
||||
| update_ip | VARCHAR(128) | 更新IP |
|
||||
|
||||
**索引**:
|
||||
- `PRIMARY KEY (id)`
|
||||
- `UNIQUE KEY unique_category_key (category, config_name)`
|
||||
|
||||
### 4. 聊天模型表 (chat_model)
|
||||
|
||||
存储聊天模型配置信息。
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| id | BIGINT | 主键ID |
|
||||
| category | VARCHAR(20) | 模型分类 |
|
||||
| model_name | VARCHAR(50) | 模型名称 |
|
||||
| model_describe | VARCHAR(255) | 模型描述 |
|
||||
| model_price | DOUBLE | 模型价格 |
|
||||
| model_type | CHAR(1) | 计费类型 |
|
||||
| model_show | CHAR(1) | 是否显示 |
|
||||
| system_prompt | VARCHAR(255) | 系统提示词 |
|
||||
| api_host | VARCHAR(255) | 请求地址 |
|
||||
| api_key | VARCHAR(255) | 密钥 |
|
||||
| api_url | VARCHAR(50) | 请求后缀 |
|
||||
| create_by | BIGINT | 创建者 |
|
||||
| create_time | DATETIME | 创建时间 |
|
||||
| update_by | BIGINT | 更新者 |
|
||||
| update_time | DATETIME | 更新时间 |
|
||||
| remark | VARCHAR(500) | 备注 |
|
||||
|
||||
**索引**:`PRIMARY KEY (id)`
|
||||
|
||||
### 5. Token 使用记录表 (chat_usage_token)
|
||||
|
||||
存储用户 Token 使用统计。
|
||||
|
||||
| 字段名 | 类型 | 说明 |
|
||||
|--------|------|------|
|
||||
| id | BIGINT | 主键ID |
|
||||
| user_id | BIGINT | 用户ID |
|
||||
| token | INT(10) | 待结算 token |
|
||||
| model_name | VARCHAR(64) | 模型名称 |
|
||||
| total_token | VARCHAR(255) | 累计使用 token |
|
||||
|
||||
**索引**:`PRIMARY KEY (id)`
|
||||
|
||||
**数据库脚本位置**:`script/sql/ry_chat.sql`
|
||||
|
||||
## API 接口
|
||||
|
||||
### 知识库聊天接口 (`/chat/knowledge`)
|
||||
|
||||
#### 知识库问答
|
||||
- **接口**:`POST /chat/knowledge/chat`
|
||||
- **说明**:基于知识库进行智能问答
|
||||
- **权限**:`chat:knowledge:chat`
|
||||
- **参数**:
|
||||
- `sessionId` (Long, 必填) - 会话ID
|
||||
- `userId` (Long, 必填) - 用户ID
|
||||
- `question` (String, 必填) - 问题
|
||||
- `knowledgeId` (Long, 必填) - 知识库ID
|
||||
- `modelName` (String, 必填) - 模型名称
|
||||
|
||||
#### 创建知识库会话
|
||||
- **接口**:`POST /chat/knowledge/session`
|
||||
- **说明**:创建知识库聊天会话
|
||||
- **权限**:`chat:knowledge:session`
|
||||
- **参数**:
|
||||
- `userId` (Long, 必填) - 用户ID
|
||||
- `knowledgeId` (Long, 必填) - 知识库ID
|
||||
- `sessionTitle` (String, 可选) - 会话标题
|
||||
|
||||
#### 搜索知识内容
|
||||
- **接口**:`GET /chat/knowledge/search`
|
||||
- **说明**:搜索知识库内容
|
||||
- **权限**:`chat:knowledge:search`
|
||||
- **参数**:
|
||||
- `knowledgeId` (Long, 必填) - 知识库ID
|
||||
- `query` (String, 必填) - 查询内容
|
||||
- `limit` (Integer, 可选, 默认5) - 返回数量
|
||||
|
||||
#### 构建知识上下文
|
||||
- **接口**:`GET /chat/knowledge/context`
|
||||
- **说明**:构建知识库上下文
|
||||
- **权限**:`chat:knowledge:context`
|
||||
- **参数**:
|
||||
- `knowledgeId` (Long, 必填) - 知识库ID
|
||||
- `question` (String, 必填) - 问题
|
||||
|
||||
#### 获取聊天历史
|
||||
- **接口**:`GET /chat/knowledge/history/{sessionId}`
|
||||
- **说明**:获取知识库聊天历史
|
||||
- **权限**:`chat:knowledge:history`
|
||||
- **参数**:
|
||||
- `sessionId` (Long, 路径参数) - 会话ID
|
||||
- `limit` (Integer, 可选, 默认20) - 返回数量
|
||||
|
||||
### 会话管理接口 (`/chat/session`)
|
||||
|
||||
- `GET /chat/session/list` - 查询会话列表(分页)
|
||||
- `GET /chat/session/{id}` - 获取会话详情
|
||||
- `POST /chat/session` - 创建新会话
|
||||
- `PUT /chat/session` - 更新会话
|
||||
- `DELETE /chat/session/{ids}` - 删除会话(批量)
|
||||
- `POST /chat/session/export` - 导出会话数据
|
||||
|
||||
**权限前缀**:`chat:session:*`
|
||||
|
||||
### 消息管理接口 (`/chat/message`)
|
||||
|
||||
- `GET /chat/message/list` - 查询消息列表(分页)
|
||||
- `GET /chat/message/{id}` - 获取消息详情
|
||||
- `POST /chat/message` - 新增消息
|
||||
- `PUT /chat/message` - 更新消息
|
||||
- `DELETE /chat/message/{ids}` - 删除消息(批量)
|
||||
- `POST /chat/message/export` - 导出消息数据
|
||||
|
||||
**权限前缀**:`chat:message:*`
|
||||
|
||||
### 开放 API 接口 (`/chat`)
|
||||
|
||||
#### SSE 流式聊天
|
||||
- **接口**:`POST /chat/send`
|
||||
- **说明**:SSE 流式聊天接口,支持 OpenAI 兼容格式
|
||||
- **认证**:需要登录(`@SaCheckLogin`)
|
||||
- **响应类型**:`text/event-stream`
|
||||
- **请求体**:JSON 格式
|
||||
```json
|
||||
{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{"role": "user", "content": "你好"}
|
||||
],
|
||||
"sessionId": "1",
|
||||
"temperature": 0.7,
|
||||
"max_tokens": 2000
|
||||
}
|
||||
```
|
||||
|
||||
### WebSocket 接口
|
||||
|
||||
#### STOMP 消息映射
|
||||
- **映射路径**:`/chat/send`
|
||||
- **说明**:STOMP 协议消息发送
|
||||
- **订阅主题**:`/topic/session/{sessionId}`
|
||||
- **配置类**:`StompWebSocketConfig`
|
||||
|
||||
### 配置管理接口 (`/chat/config`)
|
||||
|
||||
- `GET /chat/config/list` - 查询配置列表(分页)
|
||||
- `GET /chat/config/{id}` - 获取配置详情
|
||||
- `POST /chat/config` - 新增配置
|
||||
- `PUT /chat/config` - 更新配置
|
||||
- `DELETE /chat/config/{ids}` - 删除配置(批量)
|
||||
- `POST /chat/config/export` - 导出配置数据
|
||||
|
||||
**权限前缀**:`chat:config:*`
|
||||
|
||||
### 模型管理接口 (`/chat/model`)
|
||||
|
||||
- `GET /chat/model/list` - 查询模型列表(分页)
|
||||
- `GET /chat/model/{id}` - 获取模型详情
|
||||
- `POST /chat/model` - 新增模型
|
||||
- `PUT /chat/model` - 更新模型
|
||||
- `DELETE /chat/model/{ids}` - 删除模型(批量)
|
||||
- `POST /chat/model/export` - 导出模型数据
|
||||
|
||||
**权限前缀**:`chat:model:*`
|
||||
|
||||
### Token 使用记录接口 (`/chat/token`)
|
||||
|
||||
- `GET /chat/token/list` - 查询 Token 使用记录列表(分页)
|
||||
- `GET /chat/token/{id}` - 获取 Token 使用记录详情
|
||||
- `POST /chat/token` - 新增 Token 使用记录
|
||||
- `PUT /chat/token` - 更新 Token 使用记录
|
||||
- `DELETE /chat/token/{ids}` - 删除 Token 使用记录(批量)
|
||||
- `POST /chat/token/export` - 导出 Token 使用记录数据
|
||||
|
||||
**权限前缀**:`chat:token:*`
|
||||
|
||||
## 统一网关与协同
|
||||
|
||||
### 后端统一网关
|
||||
|
||||
Chat 模块可通过 `ruoyi-ai` 模块的统一网关进行对话:
|
||||
|
||||
- `POST /api/ai/chat` - 同步聊天
|
||||
- `POST /api/ai/chat/async` - 异步聊天
|
||||
- `POST /api/ai/chat/stream` - 流式聊天(SSE)
|
||||
- `WS /ws/ai/chat` - WebSocket 实时通道
|
||||
|
||||
### Chat 模块与 AI/Knowledge 的协同
|
||||
|
||||
1. **知识库增强聊天**:
|
||||
- 在 `AiKnowledgeChatServiceImpl` 中构建 `AiChatRequest`
|
||||
- 调用 `AiChatService.chat`,开启知识库增强(`enableKnowledge=true`)
|
||||
- 通过 `KnowledgeVectorService.vectorSimilaritySearch` 进行相关性检索
|
||||
- 构建上下文并注入到消息中
|
||||
|
||||
2. **聊天历史管理**:
|
||||
- 使用 `AiChatHistoryService` 保存和管理聊天记录
|
||||
- 支持 Redis 缓存和历史查询
|
||||
|
||||
3. **模型管理**:
|
||||
- 通过 `AiProviderManager` 获取可用模型列表
|
||||
- 支持模型切换和负载均衡
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 1. 创建知识库会话
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/chat/knowledge/session" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-d '{
|
||||
"userId": 1,
|
||||
"knowledgeId": 1,
|
||||
"sessionTitle": "技术咨询会话"
|
||||
}'
|
||||
```
|
||||
|
||||
### 2. 知识库问答
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/chat/knowledge/chat" \
|
||||
-H "Content-Type: application/x-www-form-urlencoded" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-d "sessionId=1&userId=1&question=什么是Spring Boot?&knowledgeId=1&modelName=gpt-3.5-turbo"
|
||||
```
|
||||
|
||||
### 3. SSE 流式聊天
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/chat/send" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-H "Accept: text/event-stream" \
|
||||
-d '{
|
||||
"model": "gpt-3.5-turbo",
|
||||
"messages": [
|
||||
{"role": "user", "content": "你好,请介绍一下你自己"}
|
||||
],
|
||||
"sessionId": "1",
|
||||
"temperature": 0.7
|
||||
}'
|
||||
```
|
||||
|
||||
### 4. 通过统一网关进行对话
|
||||
|
||||
```bash
|
||||
curl -X POST "http://localhost:8080/api/ai/chat" \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer <token>" \
|
||||
-d '{
|
||||
"userId": "1",
|
||||
"sessionId": "1",
|
||||
"model": "gpt-4o-mini",
|
||||
"enableKnowledge": true,
|
||||
"knowledgeId": "1",
|
||||
"knowledgeLimit": 5,
|
||||
"knowledgeThreshold": 0.7,
|
||||
"messages": [
|
||||
{"role": "user", "content": "什么是Spring Boot?"}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### 5. 获取聊天历史
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8080/chat/knowledge/history/1?limit=10" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
### 6. 查询会话列表
|
||||
|
||||
```bash
|
||||
curl -X GET "http://localhost:8080/chat/session/list?pageNum=1&pageSize=10" \
|
||||
-H "Authorization: Bearer <token>"
|
||||
```
|
||||
|
||||
## Java SDK
|
||||
|
||||
### ChatSdk(统一网关封装)
|
||||
|
||||
封装类:`org.dromara.chat.client.ChatSdk`
|
||||
|
||||
```java
|
||||
// 初始化 SDK
|
||||
ChatSdk sdk = new ChatSdk("http://localhost:8080")
|
||||
.withToken("Bearer xxx");
|
||||
|
||||
// 同步聊天
|
||||
String result = sdk.chat("{\"model\":\"gpt-4o\",\"messages\":[{\"role\":\"user\",\"content\":\"hello\"}]}");
|
||||
|
||||
// 流式聊天(SSE)
|
||||
sdk.chatStream("{\"model\":\"gpt-4o\",\"messages\":[{\"role\":\"user\",\"content\":\"hello\"}]}",
|
||||
chunk -> System.out.println(chunk),
|
||||
response -> System.out.println("Complete: " + response),
|
||||
error -> System.err.println("Error: " + error));
|
||||
```
|
||||
|
||||
## 配置说明
|
||||
|
||||
### 应用配置
|
||||
|
||||
Chat 模块依赖 `ruoyi-ai` 模块的配置,主要配置在 `application.yml` 中:
|
||||
|
||||
```yaml
|
||||
ruoyi:
|
||||
ai:
|
||||
# 是否启用 AI 模块
|
||||
enabled: true
|
||||
|
||||
# 知识库配置
|
||||
knowledge:
|
||||
enabled: true
|
||||
default-search-count: 5
|
||||
similarity-threshold: 0.7
|
||||
max-context-length: 4000
|
||||
|
||||
# 流式响应配置
|
||||
stream:
|
||||
enabled: true
|
||||
buffer-size: 1024
|
||||
timeout: 60
|
||||
```
|
||||
|
||||
### WebSocket 配置
|
||||
|
||||
WebSocket 配置在 `StompWebSocketConfig` 中:
|
||||
|
||||
- **端点路径**:`/ws`
|
||||
- **消息映射**:`/chat/send`
|
||||
- **订阅主题**:`/topic/session/{sessionId}`
|
||||
|
||||
### 数据库配置
|
||||
|
||||
确保已执行 `script/sql/ry_chat.sql` 初始化脚本。
|
||||
|
||||
## 依赖模块
|
||||
|
||||
### 核心依赖
|
||||
|
||||
- **ruoyi-ai**: AI 服务集成
|
||||
- `AiChatService` - 统一聊天服务
|
||||
- `AiChatHistoryService` - 聊天历史服务
|
||||
- `AiProviderManager` - Provider 管理器
|
||||
- **ruoyi-knowledge**: 知识库服务
|
||||
- `KnowledgeVectorService` - 向量检索服务
|
||||
- `KnowledgeInfoService` - 知识库信息服务
|
||||
- **ruoyi-common-core**: 核心工具类
|
||||
- **ruoyi-common-mybatis**: 数据库操作
|
||||
- **ruoyi-common-web**: Web 框架支持
|
||||
- **ruoyi-common-security**: 安全认证(Sa-Token)
|
||||
- **ruoyi-common-log**: 日志记录
|
||||
- **ruoyi-common-excel**: Excel 导出
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 添加新的聊天类型
|
||||
|
||||
1. 创建新的 Controller 类,继承 `BaseController`
|
||||
2. 实现对应的 Service 接口
|
||||
3. 添加相关的 BO 和 VO 类
|
||||
4. 配置路由映射和权限注解
|
||||
|
||||
### 扩展消息类型
|
||||
|
||||
1. 在 `ChatMessage` 实体中添加新字段
|
||||
2. 更新数据库表结构(修改 `ry_chat.sql`)
|
||||
3. 修改消息处理逻辑
|
||||
4. 添加相应的验证规则
|
||||
|
||||
### 集成新的 AI 模型
|
||||
|
||||
1. 在 `ruoyi-ai` 模块中添加新的 Provider
|
||||
2. 在 `chat_model` 表中添加模型配置
|
||||
3. 测试模型兼容性
|
||||
4. 更新文档
|
||||
|
||||
### WebSocket 扩展
|
||||
|
||||
1. 在 `StompWebSocketConfig` 中添加新的消息映射
|
||||
2. 创建对应的 Controller 处理消息
|
||||
3. 配置订阅主题
|
||||
4. 更新客户端连接代码
|
||||
|
||||
## 认证与权限
|
||||
|
||||
### 认证方式
|
||||
|
||||
- **登录认证**:基于 Sa-Token,所有写操作需要登录态(`@SaCheckLogin`)
|
||||
- **权限控制**:管理接口需要相应权限(`@SaCheckPermission`)
|
||||
- **令牌格式**:`Authorization: Bearer <token>`
|
||||
|
||||
### 权限列表
|
||||
|
||||
- `chat:knowledge:*` - 知识库聊天权限
|
||||
- `chat:session:*` - 会话管理权限
|
||||
- `chat:message:*` - 消息管理权限
|
||||
- `chat:config:*` - 配置管理权限
|
||||
- `chat:model:*` - 模型管理权限
|
||||
- `chat:token:*` - Token 统计权限
|
||||
|
||||
### 统一网关权限
|
||||
|
||||
统一网关(`/api/ai/*`)需要相应的 AI 模块权限,如 `ai:chat:sync`、`ai:chat:stream` 等。
|
||||
|
||||
## 监控和日志
|
||||
|
||||
### 关键指标
|
||||
|
||||
- 会话创建数量
|
||||
- 消息发送成功率
|
||||
- 平均响应时间
|
||||
- Token 使用量
|
||||
- 错误率统计
|
||||
- 知识库检索命中率
|
||||
|
||||
### 日志配置
|
||||
|
||||
```yaml
|
||||
logging:
|
||||
level:
|
||||
org.dromara.chat: DEBUG
|
||||
org.dromara.ai: INFO
|
||||
org.dromara.knowledge: INFO
|
||||
```
|
||||
|
||||
### 日志记录
|
||||
|
||||
- 所有接口调用记录(通过 `@Log` 注解)
|
||||
- 异常错误日志
|
||||
- 性能监控日志
|
||||
- Token 使用日志
|
||||
|
||||
## 故障排除
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **AI 服务不可用**
|
||||
- 检查 `ruoyi-ai` 模块状态
|
||||
- 验证 API 密钥配置
|
||||
- 查看网络连接
|
||||
- 检查 Provider 配置
|
||||
|
||||
2. **知识库搜索失败**
|
||||
- 确认知识库 ID 有效
|
||||
- 检查知识库内容是否已索引
|
||||
- 验证搜索权限
|
||||
- 查看向量服务状态
|
||||
|
||||
3. **会话创建失败**
|
||||
- 检查用户权限
|
||||
- 验证数据库连接
|
||||
- 查看参数格式
|
||||
- 检查会话表结构
|
||||
|
||||
4. **SSE 流式响应中断**
|
||||
- 检查网络连接稳定性
|
||||
- 查看超时配置
|
||||
- 验证模型响应时间
|
||||
- 检查服务器资源
|
||||
|
||||
5. **WebSocket 连接失败**
|
||||
- 检查 WebSocket 配置
|
||||
- 验证 STOMP 端点路径
|
||||
- 查看防火墙设置
|
||||
- 检查客户端连接代码
|
||||
|
||||
### 性能优化
|
||||
|
||||
1. **数据库优化**
|
||||
- 添加适当索引(session_id, user_id, create_time 等)
|
||||
- 定期清理历史数据
|
||||
- 使用连接池
|
||||
- 考虑分表策略(按时间或用户)
|
||||
|
||||
2. **缓存策略**
|
||||
- 缓存热门会话
|
||||
- 缓存知识库搜索结果
|
||||
- 使用 Redis 缓存会话信息
|
||||
- 缓存模型配置信息
|
||||
|
||||
3. **异步处理**
|
||||
- 异步保存消息
|
||||
- 后台统计计算
|
||||
- 队列处理长任务
|
||||
- 异步日志记录
|
||||
|
||||
4. **流式响应优化**
|
||||
- 调整缓冲区大小
|
||||
- 优化模型响应时间
|
||||
- 使用连接池管理 SSE 连接
|
||||
- 实现背压控制
|
||||
|
||||
## 版本历史
|
||||
|
||||
- **v1.0.0**: 初始版本,基础聊天功能
|
||||
- **v1.1.0**: 添加知识库集成(RAG)
|
||||
- **v1.2.0**: 支持多模型切换
|
||||
- **v1.3.0**: 增加使用统计功能
|
||||
- **v1.4.0**: 对接统一 AI 网关与 RAG 能力,完善文档与 SDK
|
||||
- **v1.5.0**: 添加 SSE 流式支持和 WebSocket 实时通信
|
||||
|
||||
## 贡献指南
|
||||
|
||||
1. Fork 项目
|
||||
2. 创建功能分支(`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交代码变更(`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支(`git push origin feature/AmazingFeature`)
|
||||
5. 创建 Pull Request
|
||||
6. 等待代码审查
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 MIT 许可证,详见 LICENSE 文件。
|
||||
|
||||
## 相关文档
|
||||
|
||||
- [RuoYi AI 模块文档](../ruoyi-ai/README.md)
|
||||
- [RuoYi Knowledge 模块文档](../ruoyi-knowledge/README.md)
|
||||
- [API 使用文档](./docs/API.md)
|
||||
@@ -0,0 +1,80 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<parent>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-modules</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<artifactId>ruoyi-face</artifactId>
|
||||
<description>chat聊天模块</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-core</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-doc</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-mybatis</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-translation</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-log</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-excel</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-idempotent</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-sensitive</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-common-encrypt</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.fasterxml.jackson.core</groupId>
|
||||
<artifactId>jackson-databind</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.dromara</groupId>
|
||||
<artifactId>ruoyi-ai</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
||||
@@ -0,0 +1,38 @@
|
||||
package org.dromara.chat.client;
|
||||
|
||||
import java.net.URI;
|
||||
import java.net.http.HttpClient;
|
||||
import java.net.http.HttpRequest;
|
||||
import java.net.http.HttpResponse;
|
||||
|
||||
public class ChatSdk {
|
||||
private final HttpClient client = HttpClient.newHttpClient();
|
||||
private final String baseUrl;
|
||||
private String token;
|
||||
|
||||
public ChatSdk(String baseUrl) {
|
||||
this.baseUrl = baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl;
|
||||
}
|
||||
|
||||
public ChatSdk withToken(String token) {
|
||||
this.token = token;
|
||||
return this;
|
||||
}
|
||||
|
||||
public String chat(String jsonBody) {
|
||||
HttpRequest.Builder b = HttpRequest.newBuilder()
|
||||
.uri(URI.create(baseUrl + "/api/ai/chat"))
|
||||
.header("Content-Type", "application/json");
|
||||
if (token != null && !token.isEmpty()) {
|
||||
b.header("Authorization", token);
|
||||
}
|
||||
HttpRequest req = b.POST(HttpRequest.BodyPublishers.ofString(jsonBody)).build();
|
||||
try {
|
||||
HttpResponse<String> resp = client.send(req, HttpResponse.BodyHandlers.ofString());
|
||||
return resp.body();
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package org.dromara.chat.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class StompWebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
registry.addEndpoint("/ws/chat").setAllowedOriginPatterns("*");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry registry) {
|
||||
registry.setApplicationDestinationPrefixes("/app");
|
||||
ThreadPoolTaskScheduler scheduler = taskScheduler();
|
||||
registry.enableSimpleBroker("/topic")
|
||||
.setHeartbeatValue(new long[]{30000, 30000})
|
||||
.setTaskScheduler(scheduler);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public ThreadPoolTaskScheduler taskScheduler() {
|
||||
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
|
||||
scheduler.setPoolSize(1);
|
||||
scheduler.setThreadNamePrefix("stomp-heartbeat-");
|
||||
scheduler.initialize();
|
||||
return scheduler;
|
||||
}
|
||||
}
|
||||
|
||||
+97
@@ -0,0 +1,97 @@
|
||||
package org.dromara.chat.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.chat.domain.vo.ChatMessageVo;
|
||||
import org.dromara.chat.domain.vo.ChatSessionVo;
|
||||
import org.dromara.chat.service.AiKnowledgeChatService;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.log.annotation.Log;
|
||||
import org.dromara.common.log.enums.BusinessType;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 知识库聊天控制器
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/chat/knowledge")
|
||||
public class AIKnowledgeChatController extends BaseController {
|
||||
|
||||
private final AiKnowledgeChatService knowledgeChatService;
|
||||
|
||||
/**
|
||||
* 基于知识库进行智能问答
|
||||
*/
|
||||
@SaCheckPermission("chat:knowledge:chat")
|
||||
@Log(title = "知识库问答", businessType = BusinessType.OTHER)
|
||||
@PostMapping("/chat")
|
||||
public R<ChatMessageVo> chatWithKnowledge(
|
||||
@RequestParam Long sessionId,
|
||||
@RequestParam Long userId,
|
||||
@RequestParam String question,
|
||||
@RequestParam Long knowledgeId,
|
||||
@RequestParam String modelName) {
|
||||
ChatMessageVo response = knowledgeChatService.chatWithKnowledge(sessionId, userId, question, knowledgeId, modelName);
|
||||
return R.ok(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建知识库聊天会话
|
||||
*/
|
||||
@SaCheckPermission("chat:knowledge:session")
|
||||
@Log(title = "创建知识库会话", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/session")
|
||||
public R<ChatSessionVo> createKnowledgeSession(
|
||||
@RequestParam Long userId,
|
||||
@RequestParam Long knowledgeId,
|
||||
@RequestParam(required = false) String sessionTitle) {
|
||||
ChatSessionVo session = knowledgeChatService.createKnowledgeSession(userId, knowledgeId, sessionTitle);
|
||||
return R.ok(session);
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索知识库内容
|
||||
*/
|
||||
@SaCheckPermission("chat:knowledge:search")
|
||||
@GetMapping("/search")
|
||||
public R<List<String>> searchKnowledgeContent(
|
||||
@RequestParam Long knowledgeId,
|
||||
@RequestParam String query,
|
||||
@RequestParam(defaultValue = "5") Integer limit) {
|
||||
List<String> results = knowledgeChatService.searchKnowledgeContent(knowledgeId, query, limit);
|
||||
return R.ok(results);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建知识库上下文
|
||||
*/
|
||||
@SaCheckPermission("chat:knowledge:context")
|
||||
@GetMapping("/context")
|
||||
public R<String> buildKnowledgeContext(
|
||||
@RequestParam Long knowledgeId,
|
||||
@RequestParam String question) {
|
||||
String context = knowledgeChatService.buildKnowledgeContext(knowledgeId, question);
|
||||
return R.ok(context);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库聊天历史
|
||||
*/
|
||||
@SaCheckPermission("chat:knowledge:history")
|
||||
@GetMapping("/history/{sessionId}")
|
||||
public R<List<ChatMessageVo>> getKnowledgeChatHistory(
|
||||
@PathVariable Long sessionId,
|
||||
@RequestParam(defaultValue = "20") Integer limit) {
|
||||
List<ChatMessageVo> history = knowledgeChatService.getKnowledgeChatHistory(sessionId, limit);
|
||||
return R.ok(history);
|
||||
}
|
||||
|
||||
}
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
package org.dromara.chat.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.chat.domain.bo.ChatConfigBo;
|
||||
import org.dromara.chat.domain.vo.ChatConfigVo;
|
||||
import org.dromara.chat.service.ChatConfigService;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.excel.utils.ExcelUtil;
|
||||
import org.dromara.common.log.annotation.Log;
|
||||
import org.dromara.common.log.enums.BusinessType;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天配置控制器
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/chat/config")
|
||||
public class ChatConfigController extends BaseController {
|
||||
|
||||
private final ChatConfigService chatConfigService;
|
||||
|
||||
/**
|
||||
* 查询聊天配置列表
|
||||
*/
|
||||
@SaCheckPermission("chat:config:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<ChatConfigVo> list(ChatConfigBo bo, PageQuery pageQuery) {
|
||||
return chatConfigService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出聊天配置列表
|
||||
*/
|
||||
@SaCheckPermission("chat:config:export")
|
||||
@Log(title = "聊天配置", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(ChatConfigBo bo, HttpServletResponse response) {
|
||||
List<ChatConfigVo> list = chatConfigService.queryList(bo);
|
||||
ExcelUtil.exportExcel(list, "聊天配置", ChatConfigVo.class, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天配置详细信息
|
||||
*/
|
||||
@SaCheckPermission("chat:config:query")
|
||||
@GetMapping("/{id}")
|
||||
public R<ChatConfigVo> getInfo(@PathVariable Long id) {
|
||||
return R.ok(chatConfigService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增聊天配置
|
||||
*/
|
||||
@SaCheckPermission("chat:config:add")
|
||||
@Log(title = "聊天配置", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public R<Void> add(@Validated @RequestBody ChatConfigBo bo) {
|
||||
return toAjax(chatConfigService.insertByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改聊天配置
|
||||
*/
|
||||
@SaCheckPermission("chat:config:edit")
|
||||
@Log(title = "聊天配置", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public R<Void> edit(@Validated @RequestBody ChatConfigBo bo) {
|
||||
return toAjax(chatConfigService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除聊天配置
|
||||
*/
|
||||
@SaCheckPermission("chat:config:remove")
|
||||
@Log(title = "聊天配置", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public R<Void> remove(@PathVariable Long[] ids) {
|
||||
return toAjax(chatConfigService.deleteWithValidByIds(List.of(ids), true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置分类查询配置列表
|
||||
*/
|
||||
@SaCheckPermission("chat:config:query")
|
||||
@GetMapping("/category/{category}")
|
||||
public R<List<ChatConfigVo>> getByCategory(@PathVariable String category) {
|
||||
return R.ok(chatConfigService.queryListByCategory(category));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置名称查询配置
|
||||
*/
|
||||
@SaCheckPermission("chat:config:query")
|
||||
@GetMapping("/name/{configName}")
|
||||
public R<ChatConfigVo> getByConfigName(@PathVariable String configName) {
|
||||
return R.ok(chatConfigService.queryByConfigName(configName));
|
||||
}
|
||||
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
package org.dromara.chat.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.chat.domain.bo.ChatMessageBo;
|
||||
import org.dromara.chat.domain.vo.ChatMessageVo;
|
||||
import org.dromara.chat.service.ChatMessageService;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.excel.utils.ExcelUtil;
|
||||
import org.dromara.common.log.annotation.Log;
|
||||
import org.dromara.common.log.enums.BusinessType;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天消息控制器
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/chat/message")
|
||||
public class ChatMessageController extends BaseController {
|
||||
|
||||
private final ChatMessageService chatMessageService;
|
||||
|
||||
/**
|
||||
* 查询聊天消息列表
|
||||
*/
|
||||
@SaCheckPermission("chat:message:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<ChatMessageVo> list(ChatMessageBo bo, PageQuery pageQuery) {
|
||||
return chatMessageService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出聊天消息列表
|
||||
*/
|
||||
@SaCheckPermission("chat:message:export")
|
||||
@Log(title = "聊天消息", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(ChatMessageBo bo, HttpServletResponse response) {
|
||||
List<ChatMessageVo> list = chatMessageService.queryList(bo);
|
||||
ExcelUtil.exportExcel(list, "聊天消息", ChatMessageVo.class, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天消息详细信息
|
||||
*/
|
||||
@SaCheckPermission("chat:message:query")
|
||||
@GetMapping("/{id}")
|
||||
public R<ChatMessageVo> getInfo(@PathVariable Long id) {
|
||||
return R.ok(chatMessageService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增聊天消息
|
||||
*/
|
||||
@SaCheckPermission("chat:message:add")
|
||||
@Log(title = "聊天消息", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public R<Void> add(@Validated @RequestBody ChatMessageBo bo) {
|
||||
return toAjax(chatMessageService.insertByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改聊天消息
|
||||
*/
|
||||
@SaCheckPermission("chat:message:edit")
|
||||
@Log(title = "聊天消息", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public R<Void> edit(@Validated @RequestBody ChatMessageBo bo) {
|
||||
return toAjax(chatMessageService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除聊天消息
|
||||
*/
|
||||
@SaCheckPermission("chat:message:remove")
|
||||
@Log(title = "聊天消息", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public R<Void> remove(@PathVariable Long[] ids) {
|
||||
return toAjax(chatMessageService.deleteWithValidByIds(List.of(ids), true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID查询消息列表
|
||||
*/
|
||||
@SaCheckPermission("chat:message:query")
|
||||
@GetMapping("/session/{sessionId}")
|
||||
public R<List<ChatMessageVo>> getBySessionId(@PathVariable Long sessionId) {
|
||||
return R.ok(chatMessageService.queryListBySessionId(sessionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID查询消息列表
|
||||
*/
|
||||
@SaCheckPermission("chat:message:query")
|
||||
@GetMapping("/user/{userId}")
|
||||
public R<List<ChatMessageVo>> getByUserId(@PathVariable Long userId) {
|
||||
return R.ok(chatMessageService.queryListByUserId(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID和用户ID查询消息列表
|
||||
*/
|
||||
@SaCheckPermission("chat:message:query")
|
||||
@GetMapping("/session/{sessionId}/user/{userId}")
|
||||
public R<List<ChatMessageVo>> getBySessionIdAndUserId(@PathVariable Long sessionId, @PathVariable Long userId) {
|
||||
return R.ok(chatMessageService.queryListBySessionIdAndUserId(sessionId, userId));
|
||||
}
|
||||
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package org.dromara.chat.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.chat.domain.bo.ChatModelBo;
|
||||
import org.dromara.chat.domain.vo.ChatModelVo;
|
||||
import org.dromara.chat.service.ChatModelService;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.excel.utils.ExcelUtil;
|
||||
import org.dromara.common.log.annotation.Log;
|
||||
import org.dromara.common.log.enums.BusinessType;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天模型控制器
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/chat/model")
|
||||
public class ChatModelController extends BaseController {
|
||||
|
||||
private final ChatModelService chatModelService;
|
||||
|
||||
/**
|
||||
* 查询聊天模型列表
|
||||
*/
|
||||
@SaCheckPermission("chat:model:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<ChatModelVo> list(ChatModelBo bo, PageQuery pageQuery) {
|
||||
return chatModelService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出聊天模型列表
|
||||
*/
|
||||
@SaCheckPermission("chat:model:export")
|
||||
@Log(title = "聊天模型", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(ChatModelBo bo, HttpServletResponse response) {
|
||||
List<ChatModelVo> list = chatModelService.queryList(bo);
|
||||
ExcelUtil.exportExcel(list, "聊天模型", ChatModelVo.class, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天模型详细信息
|
||||
*/
|
||||
@SaCheckPermission("chat:model:query")
|
||||
@GetMapping("/{id}")
|
||||
public R<ChatModelVo> getInfo(@PathVariable Long id) {
|
||||
return R.ok(chatModelService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增聊天模型
|
||||
*/
|
||||
@SaCheckPermission("chat:model:add")
|
||||
@Log(title = "聊天模型", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public R<Void> add(@Validated @RequestBody ChatModelBo bo) {
|
||||
return toAjax(chatModelService.insertByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改聊天模型
|
||||
*/
|
||||
@SaCheckPermission("chat:model:edit")
|
||||
@Log(title = "聊天模型", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public R<Void> edit(@Validated @RequestBody ChatModelBo bo) {
|
||||
return toAjax(chatModelService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除聊天模型
|
||||
*/
|
||||
@SaCheckPermission("chat:model:remove")
|
||||
@Log(title = "聊天模型", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public R<Void> remove(@PathVariable Long[] ids) {
|
||||
return toAjax(chatModelService.deleteWithValidByIds(List.of(ids), true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型分类查询模型列表
|
||||
*/
|
||||
@SaCheckPermission("chat:model:query")
|
||||
@GetMapping("/category/{category}")
|
||||
public R<List<ChatModelVo>> getByCategory(@PathVariable String category) {
|
||||
return R.ok(chatModelService.queryListByCategory(category));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型名称查询模型
|
||||
*/
|
||||
@SaCheckPermission("chat:model:query")
|
||||
@GetMapping("/name/{modelName}")
|
||||
public R<ChatModelVo> getByModelName(@PathVariable String modelName) {
|
||||
return R.ok(chatModelService.queryByModelName(modelName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询显示的模型列表
|
||||
*/
|
||||
@SaCheckPermission("chat:model:query")
|
||||
@GetMapping("/show")
|
||||
public R<List<ChatModelVo>> getShowModelList() {
|
||||
return R.ok(chatModelService.queryShowModelList());
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型类型查询模型列表
|
||||
*/
|
||||
@SaCheckPermission("chat:model:query")
|
||||
@GetMapping("/type/{modelType}")
|
||||
public R<List<ChatModelVo>> getByModelType(@PathVariable String modelType) {
|
||||
return R.ok(chatModelService.queryListByModelType(modelType));
|
||||
}
|
||||
|
||||
}
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
package org.dromara.chat.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckLogin;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.ai.domain.dto.AiChatRequest;
|
||||
import org.dromara.ai.domain.dto.AiChatResponse;
|
||||
import org.dromara.ai.service.AiChatService;
|
||||
import org.dromara.chat.domain.bo.ChatMessageBo;
|
||||
import org.dromara.chat.domain.vo.ChatSessionVo;
|
||||
import org.dromara.chat.service.ChatMessageService;
|
||||
import org.dromara.chat.service.ChatSessionService;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.json.utils.JsonUtils;
|
||||
import org.dromara.common.satoken.utils.LoginHelper;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
/**
|
||||
* 开放聊天接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@SaCheckLogin
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/chat")
|
||||
public class ChatOpenApiController {
|
||||
|
||||
private final AiChatService aiChatService;
|
||||
private final ChatSessionService chatSessionService;
|
||||
private final ChatMessageService chatMessageService;
|
||||
|
||||
@PostMapping(value = "/send", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter send(@RequestBody Map<String, Object> body) {
|
||||
SseEmitter emitter = new SseEmitter(TimeUnit.MINUTES.toMillis(10));
|
||||
Long userId;
|
||||
try {
|
||||
userId = LoginHelper.getUserId();
|
||||
} catch (Exception e) {
|
||||
emitter.completeWithError(new RuntimeException("未登录"));
|
||||
return emitter;
|
||||
}
|
||||
|
||||
try {
|
||||
AiChatRequest req = toRequest(body);
|
||||
req.setUserId(String.valueOf(userId));
|
||||
|
||||
// 获取或创建会话
|
||||
String firstMsgContent = "";
|
||||
if (req.getMessages() != null && !req.getMessages().isEmpty()) {
|
||||
firstMsgContent = req.getMessages().get(req.getMessages().size() - 1).getContent();
|
||||
}
|
||||
Long sessionId = getOrCreateSession(userId, req.getSessionId(), firstMsgContent);
|
||||
req.setSessionId(String.valueOf(sessionId));
|
||||
|
||||
// 如果是新会话,尝试通知前端sessionId
|
||||
if (StrUtil.isBlank(req.getSessionId())) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event().name("session").data("{\"sessionId\":\"" + sessionId + "\"}"));
|
||||
} catch (IOException e) {
|
||||
log.error("发送会话ID失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户消息
|
||||
saveUserMessage(sessionId, userId, firstMsgContent, req.getModel());
|
||||
|
||||
final StringBuilder fullText = new StringBuilder();
|
||||
final Long finalSessionId = sessionId;
|
||||
final String modelName = req.getModel();
|
||||
|
||||
aiChatService.chatStream(req, new AiChatService.StreamCallback() {
|
||||
@Override
|
||||
public void onChunk(String chunk) {
|
||||
String c = normalizeChunk(chunk);
|
||||
if ("[DONE]".equals(c)) {
|
||||
// ignore
|
||||
} else {
|
||||
fullText.append(c);
|
||||
try {
|
||||
emitter.send(toOpenAiDelta(c, false));
|
||||
} catch (IOException e) {
|
||||
log.debug("发送SSE失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(AiChatResponse response) {
|
||||
try {
|
||||
emitter.send(toOpenAiDelta("", true));
|
||||
emitter.send("[DONE]");
|
||||
emitter.complete();
|
||||
|
||||
// 保存AI响应消息
|
||||
saveAiMessage(finalSessionId, userId, fullText.toString(), modelName);
|
||||
} catch (IOException e) {
|
||||
log.error("发送SSE完成标记失败", e);
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onError(Throwable error) {
|
||||
log.error("AI聊天出错", error);
|
||||
try {
|
||||
emitter.send("{\"error\":\"" + error.getMessage().replace("\"", "\\\"") + "\"}");
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
emitter.completeWithError(error);
|
||||
}
|
||||
});
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理聊天请求失败", e);
|
||||
try {
|
||||
emitter.send("{\"error\":\"" + e.getMessage().replace("\"", "\\\"") + "\"}");
|
||||
} catch (IOException ignored) {
|
||||
}
|
||||
emitter.completeWithError(e);
|
||||
}
|
||||
return emitter;
|
||||
}
|
||||
|
||||
private Long getOrCreateSession(Long userId, String sessionIdStr, String firstMsgContent) {
|
||||
if (StrUtil.isNotBlank(sessionIdStr)) {
|
||||
Long sessionId = Long.parseLong(sessionIdStr);
|
||||
ChatSessionVo session = chatSessionService.queryById(sessionId);
|
||||
if (session == null) {
|
||||
throw new RuntimeException("会话不存在");
|
||||
}
|
||||
if (!session.getUserId().equals(userId)) {
|
||||
throw new RuntimeException("无权访问该会话");
|
||||
}
|
||||
return sessionId;
|
||||
} else {
|
||||
String title = "新对话";
|
||||
if (StrUtil.isNotBlank(firstMsgContent)) {
|
||||
title = StringUtils.substring(firstMsgContent, 0, 20);
|
||||
}
|
||||
ChatSessionVo session = chatSessionService.createNewSession(userId, title);
|
||||
return session.getId();
|
||||
}
|
||||
}
|
||||
|
||||
private void saveUserMessage(Long sessionId, Long userId, String content, String modelName) {
|
||||
if (StrUtil.isNotBlank(content)) {
|
||||
ChatMessageBo userMsg = new ChatMessageBo();
|
||||
userMsg.setSessionId(sessionId);
|
||||
userMsg.setUserId(userId);
|
||||
userMsg.setRole("user");
|
||||
userMsg.setContent(content);
|
||||
userMsg.setModelName(modelName);
|
||||
userMsg.setCreateTime(new Date());
|
||||
chatMessageService.insertByBo(userMsg);
|
||||
}
|
||||
}
|
||||
|
||||
private void saveAiMessage(Long sessionId, Long userId, String content, String modelName) {
|
||||
if (StrUtil.isNotBlank(content)) {
|
||||
ChatMessageBo aiMsg = new ChatMessageBo();
|
||||
aiMsg.setSessionId(sessionId);
|
||||
aiMsg.setUserId(userId);
|
||||
aiMsg.setRole("assistant");
|
||||
aiMsg.setContent(content);
|
||||
aiMsg.setModelName(modelName);
|
||||
aiMsg.setCreateTime(new Date());
|
||||
aiMsg.setTotalTokens(0L);
|
||||
aiMsg.setDeductCost(BigDecimal.ZERO);
|
||||
chatMessageService.insertByBo(aiMsg);
|
||||
}
|
||||
}
|
||||
|
||||
private AiChatRequest toRequest(Map<String, Object> body) {
|
||||
AiChatRequest req = new AiChatRequest();
|
||||
req.setModel(asString(body.get("model")));
|
||||
req.setTemperature(asDouble(body.get("temperature")));
|
||||
req.setTopP(asDouble(body.get("top_p")));
|
||||
req.setPresencePenalty(asDouble(body.get("presence_penalty")));
|
||||
req.setFrequencyPenalty(asDouble(body.get("frequency_penalty")));
|
||||
req.setMaxTokens(asInt(body.get("max_tokens")));
|
||||
req.setStream(Boolean.TRUE);
|
||||
req.setSessionId(asString(body.get("sessionId")));
|
||||
Object msgs = body.get("messages");
|
||||
if (msgs instanceof List<?> list) {
|
||||
java.util.ArrayList<AiChatRequest.AiMessage> arr = new java.util.ArrayList<>();
|
||||
for (Object o : list) {
|
||||
if (o instanceof Map<?, ?> m) {
|
||||
AiChatRequest.AiMessage am = new AiChatRequest.AiMessage();
|
||||
am.setRole(asString(m.get("role")));
|
||||
am.setContent(asString(m.get("content")));
|
||||
arr.add(am);
|
||||
}
|
||||
}
|
||||
req.setMessages(arr);
|
||||
}
|
||||
return req;
|
||||
}
|
||||
|
||||
private String asString(Object o) {
|
||||
return o == null ? null : String.valueOf(o);
|
||||
}
|
||||
|
||||
private Double asDouble(Object o) {
|
||||
try {
|
||||
return o == null ? null : Double.valueOf(String.valueOf(o));
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private Integer asInt(Object o) {
|
||||
try {
|
||||
return o == null ? null : Integer.valueOf(String.valueOf(o));
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String toOpenAiDelta(String content, boolean finish) {
|
||||
String c = content == null ? "" : content;
|
||||
String esc = c.replace("\\", "\\\\").replace("\"", "\\\"").replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t");
|
||||
String finishReason = finish ? "\"stop\"" : "null";
|
||||
return "{\"choices\":[{\"delta\":{\"content\":\"" + esc + "\"},\"finish_reason\":" + finishReason + "}]}";
|
||||
}
|
||||
|
||||
private String normalizeChunk(String chunk) {
|
||||
if (chunk == null) return "";
|
||||
String s = chunk.trim();
|
||||
if ("[DONE]".equals(s)) return "[DONE]";
|
||||
try {
|
||||
Map<?, ?> m = JsonUtils.parseObject(s, Map.class);
|
||||
if (m != null) {
|
||||
Object choices = m.get("choices");
|
||||
if (choices instanceof List<?> list && !list.isEmpty()) {
|
||||
Object first = list.get(0);
|
||||
if (first instanceof Map<?, ?> fm) {
|
||||
Object delta = fm.get("delta");
|
||||
if (delta instanceof Map<?, ?> dm) {
|
||||
Object c = dm.get("content");
|
||||
if (c == null) c = dm.get("reasoning_content");
|
||||
if (c != null) return String.valueOf(c);
|
||||
}
|
||||
Object text = fm.get("text");
|
||||
if (text != null) return String.valueOf(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
+122
@@ -0,0 +1,122 @@
|
||||
package org.dromara.chat.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.chat.domain.bo.ChatSessionBo;
|
||||
import org.dromara.chat.domain.vo.ChatSessionVo;
|
||||
import org.dromara.chat.service.ChatSessionService;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.excel.utils.ExcelUtil;
|
||||
import org.dromara.common.log.annotation.Log;
|
||||
import org.dromara.common.log.enums.BusinessType;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天会话控制器
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/chat/session")
|
||||
public class ChatSessionController extends BaseController {
|
||||
|
||||
private final ChatSessionService chatSessionService;
|
||||
|
||||
/**
|
||||
* 查询聊天会话列表
|
||||
*/
|
||||
@SaCheckPermission("chat:session:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<ChatSessionVo> list(ChatSessionBo bo, PageQuery pageQuery) {
|
||||
return chatSessionService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出聊天会话列表
|
||||
*/
|
||||
@SaCheckPermission("chat:session:export")
|
||||
@Log(title = "聊天会话", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(ChatSessionBo bo, HttpServletResponse response) {
|
||||
List<ChatSessionVo> list = chatSessionService.queryList(bo);
|
||||
ExcelUtil.exportExcel(list, "聊天会话", ChatSessionVo.class, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天会话详细信息
|
||||
*/
|
||||
@SaCheckPermission("chat:session:query")
|
||||
@GetMapping("/{id}")
|
||||
public R<ChatSessionVo> getInfo(@PathVariable Long id) {
|
||||
return R.ok(chatSessionService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增聊天会话
|
||||
*/
|
||||
@SaCheckPermission("chat:session:add")
|
||||
@Log(title = "聊天会话", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public R<Void> add(@Validated @RequestBody ChatSessionBo bo) {
|
||||
return toAjax(chatSessionService.insertByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改聊天会话
|
||||
*/
|
||||
@SaCheckPermission("chat:session:edit")
|
||||
@Log(title = "聊天会话", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public R<Void> edit(@Validated @RequestBody ChatSessionBo bo) {
|
||||
return toAjax(chatSessionService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除聊天会话
|
||||
*/
|
||||
@SaCheckPermission("chat:session:remove")
|
||||
@Log(title = "聊天会话", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public R<Void> remove(@PathVariable Long[] ids) {
|
||||
return toAjax(chatSessionService.deleteWithValidByIds(List.of(ids), true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID查询会话列表
|
||||
*/
|
||||
@SaCheckPermission("chat:session:query")
|
||||
@GetMapping("/user/{userId}")
|
||||
public R<List<ChatSessionVo>> getByUserId(@PathVariable Long userId) {
|
||||
return R.ok(chatSessionService.queryListByUserId(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
*/
|
||||
@SaCheckPermission("chat:session:add")
|
||||
@Log(title = "聊天会话", businessType = BusinessType.INSERT)
|
||||
@PostMapping("/create")
|
||||
public R<ChatSessionVo> createNewSession(@RequestParam Long userId, @RequestParam(required = false) String sessionTitle) {
|
||||
return R.ok(chatSessionService.createNewSession(userId, sessionTitle));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话标题
|
||||
*/
|
||||
@SaCheckPermission("chat:session:edit")
|
||||
@Log(title = "聊天会话", businessType = BusinessType.UPDATE)
|
||||
@PutMapping("/{sessionId}/title")
|
||||
public R<Void> updateSessionTitle(@PathVariable Long sessionId, @RequestParam String sessionTitle) {
|
||||
return toAjax(chatSessionService.updateSessionTitle(sessionId, sessionTitle));
|
||||
}
|
||||
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
package org.dromara.chat.controller;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.chat.domain.bo.ChatMessageBo;
|
||||
import org.dromara.chat.service.ChatMessageService;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.handler.annotation.Payload;
|
||||
import org.springframework.messaging.simp.SimpMessagingTemplate;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@Controller
|
||||
@RequiredArgsConstructor
|
||||
public class ChatStompController {
|
||||
private final SimpMessagingTemplate messagingTemplate;
|
||||
private final ChatMessageService chatMessageService;
|
||||
|
||||
@MessageMapping("/chat/send")
|
||||
public void send(@Payload Map<String, Object> payload) {
|
||||
Long sessionId = toLong(payload.get("sessionId"));
|
||||
Long userId = toLong(payload.get("userId"));
|
||||
String content = String.valueOf(payload.get("content"));
|
||||
ChatMessageBo bo = new ChatMessageBo();
|
||||
bo.setSessionId(sessionId);
|
||||
bo.setUserId(userId);
|
||||
bo.setContent(content);
|
||||
bo.setRole("user");
|
||||
chatMessageService.insertByBo(bo);
|
||||
messagingTemplate.convertAndSend("/topic/session/" + sessionId, Map.of(
|
||||
"sessionId", sessionId, "userId", userId, "content", content));
|
||||
}
|
||||
|
||||
private Long toLong(Object o) {
|
||||
if (o == null) return null;
|
||||
if (o instanceof Number) return ((Number) o).longValue();
|
||||
try {
|
||||
return Long.parseLong(String.valueOf(o));
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package org.dromara.chat.controller;
|
||||
|
||||
import cn.dev33.satoken.annotation.SaCheckPermission;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.chat.domain.bo.ChatUsageTokenBo;
|
||||
import org.dromara.chat.domain.vo.ChatUsageTokenVo;
|
||||
import org.dromara.chat.service.ChatUsageTokenService;
|
||||
import org.dromara.common.core.domain.R;
|
||||
import org.dromara.common.excel.utils.ExcelUtil;
|
||||
import org.dromara.common.log.annotation.Log;
|
||||
import org.dromara.common.log.enums.BusinessType;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.dromara.common.web.core.BaseController;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天Token使用记录控制器
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Validated
|
||||
@RequiredArgsConstructor
|
||||
@RestController
|
||||
@RequestMapping("/chat/token")
|
||||
public class ChatUsageTokenController extends BaseController {
|
||||
|
||||
private final ChatUsageTokenService chatUsageTokenService;
|
||||
|
||||
/**
|
||||
* 查询聊天Token使用记录列表
|
||||
*/
|
||||
@SaCheckPermission("chat:token:list")
|
||||
@GetMapping("/list")
|
||||
public TableDataInfo<ChatUsageTokenVo> list(ChatUsageTokenBo bo, PageQuery pageQuery) {
|
||||
return chatUsageTokenService.queryPageList(bo, pageQuery);
|
||||
}
|
||||
|
||||
/**
|
||||
* 导出聊天Token使用记录列表
|
||||
*/
|
||||
@SaCheckPermission("chat:token:export")
|
||||
@Log(title = "聊天Token使用记录", businessType = BusinessType.EXPORT)
|
||||
@PostMapping("/export")
|
||||
public void export(ChatUsageTokenBo bo, HttpServletResponse response) {
|
||||
List<ChatUsageTokenVo> list = chatUsageTokenService.queryList(bo);
|
||||
ExcelUtil.exportExcel(list, "聊天Token使用记录", ChatUsageTokenVo.class, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天Token使用记录详细信息
|
||||
*/
|
||||
@SaCheckPermission("chat:token:query")
|
||||
@GetMapping("/{id}")
|
||||
public R<ChatUsageTokenVo> getInfo(@PathVariable Long id) {
|
||||
return R.ok(chatUsageTokenService.queryById(id));
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增聊天Token使用记录
|
||||
*/
|
||||
@SaCheckPermission("chat:token:add")
|
||||
@Log(title = "聊天Token使用记录", businessType = BusinessType.INSERT)
|
||||
@PostMapping
|
||||
public R<Void> add(@Validated @RequestBody ChatUsageTokenBo bo) {
|
||||
return toAjax(chatUsageTokenService.insertByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改聊天Token使用记录
|
||||
*/
|
||||
@SaCheckPermission("chat:token:edit")
|
||||
@Log(title = "聊天Token使用记录", businessType = BusinessType.UPDATE)
|
||||
@PutMapping
|
||||
public R<Void> edit(@Validated @RequestBody ChatUsageTokenBo bo) {
|
||||
return toAjax(chatUsageTokenService.updateByBo(bo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除聊天Token使用记录
|
||||
*/
|
||||
@SaCheckPermission("chat:token:remove")
|
||||
@Log(title = "聊天Token使用记录", businessType = BusinessType.DELETE)
|
||||
@DeleteMapping("/{ids}")
|
||||
public R<Void> remove(@PathVariable Long[] ids) {
|
||||
return toAjax(chatUsageTokenService.deleteWithValidByIds(List.of(ids), true));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID查询Token使用记录列表
|
||||
*/
|
||||
@SaCheckPermission("chat:token:query")
|
||||
@GetMapping("/user/{userId}")
|
||||
public R<List<ChatUsageTokenVo>> getByUserId(@PathVariable Long userId) {
|
||||
return R.ok(chatUsageTokenService.queryListByUserId(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型名称查询Token使用记录列表
|
||||
*/
|
||||
@SaCheckPermission("chat:token:query")
|
||||
@GetMapping("/model/{modelName}")
|
||||
public R<List<ChatUsageTokenVo>> getByModelName(@PathVariable String modelName) {
|
||||
return R.ok(chatUsageTokenService.queryListByModelName(modelName));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户总Token使用量
|
||||
*/
|
||||
@SaCheckPermission("chat:token:query")
|
||||
@GetMapping("/total/user/{userId}")
|
||||
public R<Long> getTotalTokenByUserId(@PathVariable Long userId) {
|
||||
return R.ok(chatUsageTokenService.getTotalTokenByUserId(userId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取模型总Token使用量
|
||||
*/
|
||||
@SaCheckPermission("chat:token:query")
|
||||
@GetMapping("/total/model/{modelName}")
|
||||
public R<Long> getTotalTokenByModelName(@PathVariable String modelName) {
|
||||
return R.ok(chatUsageTokenService.getTotalTokenByModelName(modelName));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package org.dromara.chat.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 聊天配置对象 chat_config
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@TableName("chat_config")
|
||||
public class ChatConfig implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 配置分类
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 配置名称
|
||||
*/
|
||||
private String configName;
|
||||
|
||||
/**
|
||||
* 配置值
|
||||
*/
|
||||
private String configValue;
|
||||
|
||||
/**
|
||||
* 配置字典
|
||||
*/
|
||||
private String configDict;
|
||||
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
private String version;
|
||||
|
||||
/**
|
||||
* 删除标志(0代表存在 2代表删除)
|
||||
*/
|
||||
@TableLogic
|
||||
private String delFlag;
|
||||
|
||||
/**
|
||||
* 更新IP
|
||||
*/
|
||||
private String updateIp;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Long updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package org.dromara.chat.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 聊天消息对象 chat_message
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@TableName("chat_message")
|
||||
public class ChatMessage implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private Long sessionId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 角色(user/assistant/system)
|
||||
*/
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* 扣费成本
|
||||
*/
|
||||
private BigDecimal deductCost;
|
||||
|
||||
/**
|
||||
* 总token数
|
||||
*/
|
||||
private Long totalTokens;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Long updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
package org.dromara.chat.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 聊天模型对象 chat_model
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@TableName("chat_model")
|
||||
public class ChatModel implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模型分类
|
||||
*/
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 模型描述
|
||||
*/
|
||||
private String modelDescribe;
|
||||
|
||||
/**
|
||||
* 模型价格
|
||||
*/
|
||||
private BigDecimal modelPrice;
|
||||
|
||||
/**
|
||||
* 模型类型
|
||||
*/
|
||||
private String modelType;
|
||||
|
||||
/**
|
||||
* 是否显示(0隐藏 1显示)
|
||||
*/
|
||||
private String modelShow;
|
||||
|
||||
/**
|
||||
* 系统提示词
|
||||
*/
|
||||
private String systemPrompt;
|
||||
|
||||
/**
|
||||
* API主机地址
|
||||
*/
|
||||
private String apiHost;
|
||||
|
||||
/**
|
||||
* API密钥
|
||||
*/
|
||||
private String apiKey;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Long updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.dromara.chat.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.FieldFill;
|
||||
import com.baomidou.mybatisplus.annotation.TableField;
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 聊天会话对象 chat_session
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@TableName("chat_session")
|
||||
public class ChatSession implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
private String sessionTitle;
|
||||
|
||||
/**
|
||||
* 会话内容
|
||||
*/
|
||||
private String sessionContent;
|
||||
|
||||
/**
|
||||
* 创建者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Long createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT)
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新者
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Long updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(fill = FieldFill.INSERT_UPDATE)
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.dromara.chat.domain;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.TableId;
|
||||
import com.baomidou.mybatisplus.annotation.TableName;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 聊天使用令牌对象 chat_usage_token
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@TableName("chat_usage_token")
|
||||
public class ChatUsageToken implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 令牌数量
|
||||
*/
|
||||
private Long token;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 总令牌数
|
||||
*/
|
||||
private Long totalToken;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.dromara.chat.domain.bo;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.dromara.chat.domain.ChatConfig;
|
||||
import org.dromara.common.core.validate.AddGroup;
|
||||
import org.dromara.common.core.validate.EditGroup;
|
||||
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 聊天配置业务对象 chat_config
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@AutoMapper(target = ChatConfig.class, reverseConvertGenerate = false)
|
||||
public class ChatConfigBo extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@NotNull(message = "主键ID不能为空", groups = {EditGroup.class})
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 配置分类
|
||||
*/
|
||||
@NotBlank(message = "配置分类不能为空", groups = {AddGroup.class, EditGroup.class})
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 配置名称
|
||||
*/
|
||||
@NotBlank(message = "配置名称不能为空", groups = {AddGroup.class, EditGroup.class})
|
||||
private String configName;
|
||||
|
||||
/**
|
||||
* 配置值
|
||||
*/
|
||||
@NotBlank(message = "配置值不能为空", groups = {AddGroup.class, EditGroup.class})
|
||||
private String configValue;
|
||||
|
||||
/**
|
||||
* 配置字典
|
||||
*/
|
||||
private String configDict;
|
||||
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
private String version;
|
||||
|
||||
/**
|
||||
* 更新IP
|
||||
*/
|
||||
private String updateIp;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
package org.dromara.chat.domain.bo;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.dromara.chat.domain.ChatMessage;
|
||||
import org.dromara.common.core.validate.AddGroup;
|
||||
import org.dromara.common.core.validate.EditGroup;
|
||||
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 聊天消息业务对象 chat_message
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@AutoMapper(target = ChatMessage.class, reverseConvertGenerate = false)
|
||||
public class ChatMessageBo extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@NotNull(message = "主键ID不能为空", groups = {EditGroup.class})
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
@NotNull(message = "会话ID不能为空", groups = {AddGroup.class, EditGroup.class})
|
||||
private Long sessionId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@NotNull(message = "用户ID不能为空", groups = {AddGroup.class, EditGroup.class})
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 消息类型(text/image/file)
|
||||
*/
|
||||
@NotBlank(message = "消息类型不能为空", groups = {AddGroup.class, EditGroup.class})
|
||||
private String messageType;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
@NotBlank(message = "消息内容不能为空", groups = {AddGroup.class, EditGroup.class})
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 角色(user/assistant/system)
|
||||
*/
|
||||
@NotBlank(message = "角色不能为空", groups = {AddGroup.class, EditGroup.class})
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* 扣费成本
|
||||
*/
|
||||
private BigDecimal deductCost;
|
||||
|
||||
/**
|
||||
* 总token数
|
||||
*/
|
||||
private Long totalTokens;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
private String modelName;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package org.dromara.chat.domain.bo;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.dromara.chat.domain.ChatModel;
|
||||
import org.dromara.common.core.validate.AddGroup;
|
||||
import org.dromara.common.core.validate.EditGroup;
|
||||
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* 聊天模型业务对象 chat_model
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@AutoMapper(target = ChatModel.class, reverseConvertGenerate = false)
|
||||
public class ChatModelBo extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@NotNull(message = "主键ID不能为空", groups = {EditGroup.class})
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模型分类
|
||||
*/
|
||||
@NotBlank(message = "模型分类不能为空", groups = {AddGroup.class, EditGroup.class})
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
@NotBlank(message = "模型名称不能为空", groups = {AddGroup.class, EditGroup.class})
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 模型描述
|
||||
*/
|
||||
private String modelDescribe;
|
||||
|
||||
/**
|
||||
* 模型价格
|
||||
*/
|
||||
private BigDecimal modelPrice;
|
||||
|
||||
/**
|
||||
* 模型类型
|
||||
*/
|
||||
private String modelType;
|
||||
|
||||
/**
|
||||
* 是否显示(0隐藏 1显示)
|
||||
*/
|
||||
private String modelShow;
|
||||
|
||||
/**
|
||||
* 系统提示词
|
||||
*/
|
||||
private String systemPrompt;
|
||||
|
||||
/**
|
||||
* API主机地址
|
||||
*/
|
||||
private String apiHost;
|
||||
|
||||
/**
|
||||
* API密钥
|
||||
*/
|
||||
private String apiKey;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package org.dromara.chat.domain.bo;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import org.dromara.chat.domain.ChatSession;
|
||||
import org.dromara.common.core.validate.AddGroup;
|
||||
import org.dromara.common.core.validate.EditGroup;
|
||||
import org.dromara.common.mybatis.core.domain.BaseEntity;
|
||||
|
||||
/**
|
||||
* 聊天会话业务对象 chat_session
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@AutoMapper(target = ChatSession.class, reverseConvertGenerate = false)
|
||||
public class ChatSessionBo extends BaseEntity {
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@NotNull(message = "主键ID不能为空", groups = {EditGroup.class})
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@NotNull(message = "用户ID不能为空", groups = {AddGroup.class, EditGroup.class})
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
@NotBlank(message = "会话标题不能为空", groups = {AddGroup.class, EditGroup.class})
|
||||
private String sessionTitle;
|
||||
|
||||
/**
|
||||
* 会话内容
|
||||
*/
|
||||
private String sessionContent;
|
||||
|
||||
}
|
||||
+53
@@ -0,0 +1,53 @@
|
||||
package org.dromara.chat.domain.bo;
|
||||
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import org.dromara.chat.domain.ChatUsageToken;
|
||||
import org.dromara.common.core.validate.AddGroup;
|
||||
import org.dromara.common.core.validate.EditGroup;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 聊天使用令牌业务对象 chat_usage_token
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@AutoMapper(target = ChatUsageToken.class, reverseConvertGenerate = false)
|
||||
public class ChatUsageTokenBo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@NotNull(message = "主键ID不能为空", groups = {EditGroup.class})
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@NotNull(message = "用户ID不能为空", groups = {AddGroup.class, EditGroup.class})
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 令牌数量
|
||||
*/
|
||||
@NotNull(message = "令牌数量不能为空", groups = {AddGroup.class, EditGroup.class})
|
||||
private Long token;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 总令牌数
|
||||
*/
|
||||
private Long totalToken;
|
||||
|
||||
}
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
package org.dromara.chat.domain.request;
|
||||
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 聊天请求基础类
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
public class ChatRequest {
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
@NotBlank(message = "会话ID不能为空")
|
||||
private String sessionId;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
@NotBlank(message = "消息内容不能为空")
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
private String modelName;
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package org.dromara.chat.domain.request;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 多Provider聊天请求
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class MultiProviderChatRequest extends ChatRequest {
|
||||
|
||||
/**
|
||||
* Provider名称列表
|
||||
*/
|
||||
@NotNull(message = "Provider列表不能为空")
|
||||
@NotEmpty(message = "Provider列表不能为空")
|
||||
private List<String> providerNames;
|
||||
|
||||
/**
|
||||
* 是否合并结果
|
||||
*/
|
||||
private Boolean mergeResults = true;
|
||||
|
||||
/**
|
||||
* 最大并发数
|
||||
*/
|
||||
private Integer maxConcurrency = 3;
|
||||
|
||||
/**
|
||||
* 超时时间(秒)
|
||||
*/
|
||||
private Integer timeoutSeconds = 30;
|
||||
}
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
package org.dromara.chat.domain.request;
|
||||
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 回答质量评估请求
|
||||
*
|
||||
* @author pengles
|
||||
* @date 2024-01-01
|
||||
*/
|
||||
@Data
|
||||
public class RateRequest {
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
@NotBlank(message = "消息ID不能为空")
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 评分(1-5分)
|
||||
*/
|
||||
@NotNull(message = "评分不能为空")
|
||||
@Min(value = 1, message = "评分不能小于1")
|
||||
@Max(value = 5, message = "评分不能大于5")
|
||||
private Integer rating;
|
||||
|
||||
/**
|
||||
* 反馈内容
|
||||
*/
|
||||
private String feedback;
|
||||
|
||||
/**
|
||||
* 评估维度
|
||||
*/
|
||||
private String dimension;
|
||||
|
||||
/**
|
||||
* 是否匿名
|
||||
*/
|
||||
private Boolean anonymous = false;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package org.dromara.chat.domain.vo;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import org.dromara.chat.domain.ChatConfig;
|
||||
import org.dromara.common.excel.annotation.ExcelDictFormat;
|
||||
import org.dromara.common.excel.convert.ExcelDictConvert;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 聊天配置视图对象 chat_config
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@AutoMapper(target = ChatConfig.class)
|
||||
public class ChatConfigVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@ExcelProperty(value = "主键ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 配置分类
|
||||
*/
|
||||
@ExcelProperty(value = "配置分类")
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 配置名称
|
||||
*/
|
||||
@ExcelProperty(value = "配置名称")
|
||||
private String configName;
|
||||
|
||||
/**
|
||||
* 配置值
|
||||
*/
|
||||
@ExcelProperty(value = "配置值")
|
||||
private String configValue;
|
||||
|
||||
/**
|
||||
* 配置字典
|
||||
*/
|
||||
@ExcelProperty(value = "配置字典")
|
||||
private String configDict;
|
||||
|
||||
/**
|
||||
* 版本号
|
||||
*/
|
||||
@ExcelProperty(value = "版本号")
|
||||
private String version;
|
||||
|
||||
/**
|
||||
* 删除标志
|
||||
*/
|
||||
@ExcelProperty(value = "删除标志", converter = ExcelDictConvert.class)
|
||||
@ExcelDictFormat(readConverterExp = "0=正常,2=删除")
|
||||
private String delFlag;
|
||||
|
||||
/**
|
||||
* 更新IP
|
||||
*/
|
||||
@ExcelProperty(value = "更新IP")
|
||||
private String updateIp;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@ExcelProperty(value = "创建时间")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@ExcelProperty(value = "更新时间")
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package org.dromara.chat.domain.vo;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import org.dromara.chat.domain.ChatMessage;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 聊天消息视图对象 chat_message
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@AutoMapper(target = ChatMessage.class)
|
||||
public class ChatMessageVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@ExcelProperty(value = "主键ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
@ExcelProperty(value = "会话ID")
|
||||
private Long sessionId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@ExcelProperty(value = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
@ExcelProperty(value = "消息内容")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 角色(user/assistant/system)
|
||||
*/
|
||||
@ExcelProperty(value = "角色")
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* 扣费成本
|
||||
*/
|
||||
@ExcelProperty(value = "扣费成本")
|
||||
private BigDecimal deductCost;
|
||||
|
||||
/**
|
||||
* 总token数
|
||||
*/
|
||||
@ExcelProperty(value = "总token数")
|
||||
private Long totalTokens;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
@ExcelProperty(value = "模型名称")
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@ExcelProperty(value = "创建时间")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@ExcelProperty(value = "更新时间")
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
package org.dromara.chat.domain.vo;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import org.dromara.chat.domain.ChatModel;
|
||||
import org.dromara.common.excel.annotation.ExcelDictFormat;
|
||||
import org.dromara.common.excel.convert.ExcelDictConvert;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 聊天模型视图对象 chat_model
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@AutoMapper(target = ChatModel.class)
|
||||
public class ChatModelVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@ExcelProperty(value = "主键ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 模型分类
|
||||
*/
|
||||
@ExcelProperty(value = "模型分类")
|
||||
private String category;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
@ExcelProperty(value = "模型名称")
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 模型描述
|
||||
*/
|
||||
@ExcelProperty(value = "模型描述")
|
||||
private String modelDescribe;
|
||||
|
||||
/**
|
||||
* 模型价格
|
||||
*/
|
||||
@ExcelProperty(value = "模型价格")
|
||||
private BigDecimal modelPrice;
|
||||
|
||||
/**
|
||||
* 模型类型
|
||||
*/
|
||||
@ExcelProperty(value = "模型类型")
|
||||
private String modelType;
|
||||
|
||||
/**
|
||||
* 是否显示(0隐藏 1显示)
|
||||
*/
|
||||
@ExcelProperty(value = "是否显示", converter = ExcelDictConvert.class)
|
||||
@ExcelDictFormat(readConverterExp = "0=隐藏,1=显示")
|
||||
private String modelShow;
|
||||
|
||||
/**
|
||||
* 系统提示词
|
||||
*/
|
||||
@ExcelProperty(value = "系统提示词")
|
||||
private String systemPrompt;
|
||||
|
||||
/**
|
||||
* API主机地址
|
||||
*/
|
||||
@ExcelProperty(value = "API主机地址")
|
||||
private String apiHost;
|
||||
|
||||
/**
|
||||
* API密钥
|
||||
*/
|
||||
@ExcelProperty(value = "API密钥")
|
||||
private String apiKey;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@ExcelProperty(value = "创建时间")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@ExcelProperty(value = "更新时间")
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package org.dromara.chat.domain.vo;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import org.dromara.chat.domain.ChatSession;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 聊天会话视图对象 chat_session
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@AutoMapper(target = ChatSession.class)
|
||||
public class ChatSessionVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@ExcelProperty(value = "主键ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@ExcelProperty(value = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 会话标题
|
||||
*/
|
||||
@ExcelProperty(value = "会话标题")
|
||||
private String sessionTitle;
|
||||
|
||||
/**
|
||||
* 会话内容
|
||||
*/
|
||||
@ExcelProperty(value = "会话内容")
|
||||
private String sessionContent;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@ExcelProperty(value = "创建时间")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@ExcelProperty(value = "更新时间")
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package org.dromara.chat.domain.vo;
|
||||
|
||||
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
|
||||
import cn.idev.excel.annotation.ExcelProperty;
|
||||
import io.github.linpeilie.annotations.AutoMapper;
|
||||
import lombok.Data;
|
||||
import org.dromara.chat.domain.ChatUsageToken;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 聊天Token使用记录视图对象 chat_usage_token
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Data
|
||||
@ExcelIgnoreUnannotated
|
||||
@AutoMapper(target = ChatUsageToken.class)
|
||||
public class ChatUsageTokenVo implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@ExcelProperty(value = "主键ID")
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@ExcelProperty(value = "用户ID")
|
||||
private Long userId;
|
||||
|
||||
/**
|
||||
* 使用token数
|
||||
*/
|
||||
@ExcelProperty(value = "使用token数")
|
||||
private Long token;
|
||||
|
||||
/**
|
||||
* 模型名称
|
||||
*/
|
||||
@ExcelProperty(value = "模型名称")
|
||||
private String modelName;
|
||||
|
||||
/**
|
||||
* 总token数
|
||||
*/
|
||||
@ExcelProperty(value = "总token数")
|
||||
private Long totalToken;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@ExcelProperty(value = "创建时间")
|
||||
private Date createTime;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@ExcelProperty(value = "更新时间")
|
||||
private Date updateTime;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.dromara.chat.mapper;
|
||||
|
||||
import org.dromara.chat.domain.ChatConfig;
|
||||
import org.dromara.chat.domain.vo.ChatConfigVo;
|
||||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
|
||||
/**
|
||||
* 聊天配置Mapper接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface ChatConfigMapper extends BaseMapperPlus<ChatConfig, ChatConfigVo> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.dromara.chat.mapper;
|
||||
|
||||
import org.dromara.chat.domain.ChatMessage;
|
||||
import org.dromara.chat.domain.vo.ChatMessageVo;
|
||||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
|
||||
/**
|
||||
* 聊天消息Mapper接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface ChatMessageMapper extends BaseMapperPlus<ChatMessage, ChatMessageVo> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.dromara.chat.mapper;
|
||||
|
||||
import org.dromara.chat.domain.ChatModel;
|
||||
import org.dromara.chat.domain.vo.ChatModelVo;
|
||||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
|
||||
/**
|
||||
* 聊天模型Mapper接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface ChatModelMapper extends BaseMapperPlus<ChatModel, ChatModelVo> {
|
||||
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.dromara.chat.mapper;
|
||||
|
||||
import org.dromara.chat.domain.ChatSession;
|
||||
import org.dromara.chat.domain.vo.ChatSessionVo;
|
||||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
|
||||
/**
|
||||
* 聊天会话Mapper接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface ChatSessionMapper extends BaseMapperPlus<ChatSession, ChatSessionVo> {
|
||||
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package org.dromara.chat.mapper;
|
||||
|
||||
import org.dromara.chat.domain.ChatUsageToken;
|
||||
import org.dromara.chat.domain.vo.ChatUsageTokenVo;
|
||||
import org.dromara.common.mybatis.core.mapper.BaseMapperPlus;
|
||||
|
||||
/**
|
||||
* 聊天Token使用记录Mapper接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface ChatUsageTokenMapper extends BaseMapperPlus<ChatUsageToken, ChatUsageTokenVo> {
|
||||
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package org.dromara.chat.service;
|
||||
|
||||
import org.dromara.chat.domain.vo.ChatMessageVo;
|
||||
import org.dromara.chat.domain.vo.ChatSessionVo;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 知识库聊天服务接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface AiKnowledgeChatService {
|
||||
|
||||
/**
|
||||
* 基于知识库进行智能问答
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @param userId 用户ID
|
||||
* @param question 用户问题
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param modelName 使用的模型名称
|
||||
* @return 聊天回复消息
|
||||
*/
|
||||
ChatMessageVo chatWithKnowledge(Long sessionId, Long userId, String question, Long knowledgeId, String modelName);
|
||||
|
||||
/**
|
||||
* 创建知识库聊天会话
|
||||
*
|
||||
* @param userId 用户ID
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param sessionTitle 会话标题
|
||||
* @return 聊天会话
|
||||
*/
|
||||
ChatSessionVo createKnowledgeSession(Long userId, Long knowledgeId, String sessionTitle);
|
||||
|
||||
/**
|
||||
* 搜索相关知识库内容
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param query 查询内容
|
||||
* @param limit 返回数量限制
|
||||
* @return 相关知识库内容列表
|
||||
*/
|
||||
List<String> searchKnowledgeContent(Long knowledgeId, String query, Integer limit);
|
||||
|
||||
/**
|
||||
* 构建知识库上下文
|
||||
*
|
||||
* @param knowledgeId 知识库ID
|
||||
* @param question 用户问题
|
||||
* @return 构建的上下文内容
|
||||
*/
|
||||
String buildKnowledgeContext(Long knowledgeId, String question);
|
||||
|
||||
/**
|
||||
* 获取知识库聊天历史
|
||||
*
|
||||
* @param sessionId 会话ID
|
||||
* @param limit 返回数量限制
|
||||
* @return 聊天历史消息列表
|
||||
*/
|
||||
List<ChatMessageVo> getKnowledgeChatHistory(Long sessionId, Integer limit);
|
||||
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
package org.dromara.chat.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.dromara.chat.domain.ChatConfig;
|
||||
import org.dromara.chat.domain.bo.ChatConfigBo;
|
||||
import org.dromara.chat.domain.vo.ChatConfigVo;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天配置Service接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface ChatConfigService extends IService<ChatConfig> {
|
||||
|
||||
/**
|
||||
* 查询聊天配置
|
||||
*/
|
||||
ChatConfigVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 查询聊天配置列表
|
||||
*/
|
||||
TableDataInfo<ChatConfigVo> queryPageList(ChatConfigBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询聊天配置列表
|
||||
*/
|
||||
List<ChatConfigVo> queryList(ChatConfigBo bo);
|
||||
|
||||
/**
|
||||
* 新增聊天配置
|
||||
*/
|
||||
Boolean insertByBo(ChatConfigBo bo);
|
||||
|
||||
/**
|
||||
* 修改聊天配置
|
||||
*/
|
||||
Boolean updateByBo(ChatConfigBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除聊天配置
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
||||
/**
|
||||
* 根据配置分类查询配置列表
|
||||
*/
|
||||
List<ChatConfigVo> queryListByCategory(String category);
|
||||
|
||||
/**
|
||||
* 根据配置名称查询配置
|
||||
*/
|
||||
ChatConfigVo queryByConfigName(String configName);
|
||||
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package org.dromara.chat.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.dromara.chat.domain.ChatMessage;
|
||||
import org.dromara.chat.domain.bo.ChatMessageBo;
|
||||
import org.dromara.chat.domain.vo.ChatMessageVo;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天消息Service接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface ChatMessageService extends IService<ChatMessage> {
|
||||
|
||||
/**
|
||||
* 查询聊天消息
|
||||
*/
|
||||
ChatMessageVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 查询聊天消息列表
|
||||
*/
|
||||
TableDataInfo<ChatMessageVo> queryPageList(ChatMessageBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询聊天消息列表
|
||||
*/
|
||||
List<ChatMessageVo> queryList(ChatMessageBo bo);
|
||||
|
||||
/**
|
||||
* 新增聊天消息
|
||||
*/
|
||||
Boolean insertByBo(ChatMessageBo bo);
|
||||
|
||||
/**
|
||||
* 修改聊天消息
|
||||
*/
|
||||
Boolean updateByBo(ChatMessageBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除聊天消息
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
||||
/**
|
||||
* 根据会话ID查询消息列表
|
||||
*/
|
||||
List<ChatMessageVo> queryListBySessionId(Long sessionId);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询消息列表
|
||||
*/
|
||||
List<ChatMessageVo> queryListByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 根据会话ID和用户ID查询消息列表
|
||||
*/
|
||||
List<ChatMessageVo> queryListBySessionIdAndUserId(Long sessionId, Long userId);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package org.dromara.chat.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.dromara.chat.domain.ChatModel;
|
||||
import org.dromara.chat.domain.bo.ChatModelBo;
|
||||
import org.dromara.chat.domain.vo.ChatModelVo;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天模型Service接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface ChatModelService extends IService<ChatModel> {
|
||||
|
||||
/**
|
||||
* 查询聊天模型
|
||||
*/
|
||||
ChatModelVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 查询聊天模型列表
|
||||
*/
|
||||
TableDataInfo<ChatModelVo> queryPageList(ChatModelBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询聊天模型列表
|
||||
*/
|
||||
List<ChatModelVo> queryList(ChatModelBo bo);
|
||||
|
||||
/**
|
||||
* 新增聊天模型
|
||||
*/
|
||||
Boolean insertByBo(ChatModelBo bo);
|
||||
|
||||
/**
|
||||
* 修改聊天模型
|
||||
*/
|
||||
Boolean updateByBo(ChatModelBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除聊天模型
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
||||
/**
|
||||
* 根据模型分类查询模型列表
|
||||
*/
|
||||
List<ChatModelVo> queryListByCategory(String category);
|
||||
|
||||
/**
|
||||
* 根据模型名称查询模型
|
||||
*/
|
||||
ChatModelVo queryByModelName(String modelName);
|
||||
|
||||
/**
|
||||
* 查询显示的模型列表
|
||||
*/
|
||||
List<ChatModelVo> queryShowModelList();
|
||||
|
||||
/**
|
||||
* 根据模型类型查询模型列表
|
||||
*/
|
||||
List<ChatModelVo> queryListByModelType(String modelType);
|
||||
|
||||
}
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
package org.dromara.chat.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.dromara.chat.domain.ChatSession;
|
||||
import org.dromara.chat.domain.bo.ChatSessionBo;
|
||||
import org.dromara.chat.domain.vo.ChatSessionVo;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天会话Service接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface ChatSessionService extends IService<ChatSession> {
|
||||
|
||||
/**
|
||||
* 查询聊天会话
|
||||
*/
|
||||
ChatSessionVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 查询聊天会话列表
|
||||
*/
|
||||
TableDataInfo<ChatSessionVo> queryPageList(ChatSessionBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询聊天会话列表
|
||||
*/
|
||||
List<ChatSessionVo> queryList(ChatSessionBo bo);
|
||||
|
||||
/**
|
||||
* 新增聊天会话
|
||||
*/
|
||||
Boolean insertByBo(ChatSessionBo bo);
|
||||
|
||||
/**
|
||||
* 修改聊天会话
|
||||
*/
|
||||
Boolean updateByBo(ChatSessionBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除聊天会话
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询会话列表
|
||||
*/
|
||||
List<ChatSessionVo> queryListByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
*/
|
||||
ChatSessionVo createNewSession(Long userId, String sessionTitle);
|
||||
|
||||
/**
|
||||
* 更新会话标题
|
||||
*/
|
||||
Boolean updateSessionTitle(Long sessionId, String sessionTitle);
|
||||
|
||||
}
|
||||
+70
@@ -0,0 +1,70 @@
|
||||
package org.dromara.chat.service;
|
||||
|
||||
import com.baomidou.mybatisplus.extension.service.IService;
|
||||
import org.dromara.chat.domain.ChatUsageToken;
|
||||
import org.dromara.chat.domain.bo.ChatUsageTokenBo;
|
||||
import org.dromara.chat.domain.vo.ChatUsageTokenVo;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天Token使用记录Service接口
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
public interface ChatUsageTokenService extends IService<ChatUsageToken> {
|
||||
|
||||
/**
|
||||
* 查询聊天Token使用记录
|
||||
*/
|
||||
ChatUsageTokenVo queryById(Long id);
|
||||
|
||||
/**
|
||||
* 查询聊天Token使用记录列表
|
||||
*/
|
||||
TableDataInfo<ChatUsageTokenVo> queryPageList(ChatUsageTokenBo bo, PageQuery pageQuery);
|
||||
|
||||
/**
|
||||
* 查询聊天Token使用记录列表
|
||||
*/
|
||||
List<ChatUsageTokenVo> queryList(ChatUsageTokenBo bo);
|
||||
|
||||
/**
|
||||
* 新增聊天Token使用记录
|
||||
*/
|
||||
Boolean insertByBo(ChatUsageTokenBo bo);
|
||||
|
||||
/**
|
||||
* 修改聊天Token使用记录
|
||||
*/
|
||||
Boolean updateByBo(ChatUsageTokenBo bo);
|
||||
|
||||
/**
|
||||
* 校验并批量删除聊天Token使用记录
|
||||
*/
|
||||
Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid);
|
||||
|
||||
/**
|
||||
* 根据用户ID查询Token使用记录列表
|
||||
*/
|
||||
List<ChatUsageTokenVo> queryListByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 根据模型名称查询Token使用记录列表
|
||||
*/
|
||||
List<ChatUsageTokenVo> queryListByModelName(String modelName);
|
||||
|
||||
/**
|
||||
* 统计用户总Token使用量
|
||||
*/
|
||||
Long getTotalTokenByUserId(Long userId);
|
||||
|
||||
/**
|
||||
* 统计模型总Token使用量
|
||||
*/
|
||||
Long getTotalTokenByModelName(String modelName);
|
||||
|
||||
}
|
||||
+198
@@ -0,0 +1,198 @@
|
||||
package org.dromara.chat.service.impl;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.dromara.ai.domain.dto.AiChatRequest;
|
||||
import org.dromara.ai.domain.dto.AiChatResponse;
|
||||
import org.dromara.ai.service.AiChatService;
|
||||
import org.dromara.chat.domain.bo.ChatMessageBo;
|
||||
import org.dromara.chat.domain.bo.ChatSessionBo;
|
||||
import org.dromara.chat.domain.vo.ChatMessageVo;
|
||||
import org.dromara.chat.domain.vo.ChatSessionVo;
|
||||
import org.dromara.chat.service.AiKnowledgeChatService;
|
||||
import org.dromara.chat.service.ChatMessageService;
|
||||
import org.dromara.chat.service.ChatSessionService;
|
||||
import org.dromara.knowledge.domain.vo.KnowledgeInfoVo;
|
||||
import org.dromara.knowledge.domain.vo.KnowledgeSearchResultVo;
|
||||
import org.dromara.knowledge.service.KnowledgeInfoService;
|
||||
import org.dromara.knowledge.service.KnowledgeVectorService;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 知识库聊天服务实现
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class AiKnowledgeChatServiceImpl implements AiKnowledgeChatService {
|
||||
|
||||
private final ChatSessionService chatSessionService;
|
||||
private final ChatMessageService chatMessageService;
|
||||
private final KnowledgeInfoService knowledgeInfoService;
|
||||
private final KnowledgeVectorService knowledgeVectorService;
|
||||
private final AiChatService aiChatService;
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ChatMessageVo chatWithKnowledge(Long sessionId, Long userId, String question, Long knowledgeId, String modelName) {
|
||||
// 验证会话是否存在
|
||||
ChatSessionVo session = chatSessionService.queryById(sessionId);
|
||||
if (ObjectUtil.isNull(session)) {
|
||||
throw new RuntimeException("会话不存在");
|
||||
}
|
||||
|
||||
// 验证知识库是否存在
|
||||
KnowledgeInfoVo knowledge = knowledgeInfoService.queryById(knowledgeId);
|
||||
if (ObjectUtil.isNull(knowledge)) {
|
||||
throw new RuntimeException("知识库不存在");
|
||||
}
|
||||
|
||||
// 保存用户问题
|
||||
ChatMessageBo userMessageBo = new ChatMessageBo();
|
||||
userMessageBo.setSessionId(sessionId);
|
||||
userMessageBo.setUserId(userId);
|
||||
userMessageBo.setMessageType("text");
|
||||
userMessageBo.setContent(question);
|
||||
userMessageBo.setRole("user");
|
||||
userMessageBo.setModelName(modelName);
|
||||
userMessageBo.setCreateTime(new Date());
|
||||
chatMessageService.insertByBo(userMessageBo);
|
||||
|
||||
try {
|
||||
AiChatRequest req = new AiChatRequest()
|
||||
.setUserId(String.valueOf(userId))
|
||||
.setSessionId(String.valueOf(sessionId))
|
||||
.setModel(modelName)
|
||||
.setEnableKnowledge(true)
|
||||
.setKnowledgeId(String.valueOf(knowledgeId))
|
||||
.setKnowledgeLimit(5)
|
||||
.setKnowledgeThreshold(0.7)
|
||||
.setMessages(java.util.List.of(
|
||||
new AiChatRequest.AiMessage().setRole("user").setContent(question)
|
||||
));
|
||||
|
||||
AiChatResponse resp = aiChatService.chat(req);
|
||||
String aiAnswer = "";
|
||||
if (resp != null && resp.getChoices() != null && !resp.getChoices().isEmpty()) {
|
||||
AiChatRequest.AiMessage msg = resp.getChoices().get(0).getMessage();
|
||||
if (msg != null && StrUtil.isNotBlank(msg.getContent())) {
|
||||
aiAnswer = msg.getContent();
|
||||
}
|
||||
}
|
||||
if (StrUtil.isBlank(aiAnswer)) {
|
||||
throw new RuntimeException("AI服务返回空回答");
|
||||
}
|
||||
|
||||
// 保存AI回复
|
||||
ChatMessageBo aiMessageBo = new ChatMessageBo();
|
||||
aiMessageBo.setSessionId(sessionId);
|
||||
aiMessageBo.setUserId(userId);
|
||||
aiMessageBo.setMessageType("text");
|
||||
aiMessageBo.setContent(aiAnswer);
|
||||
aiMessageBo.setRole("assistant");
|
||||
aiMessageBo.setModelName(modelName);
|
||||
aiMessageBo.setTotalTokens(0L); // 简化版本不统计token
|
||||
aiMessageBo.setDeductCost(BigDecimal.ZERO); // 简化版本不计费
|
||||
aiMessageBo.setCreateTime(new Date());
|
||||
chatMessageService.insertByBo(aiMessageBo);
|
||||
|
||||
return chatMessageService.queryById(aiMessageBo.getId());
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("知识库聊天失败 - 用户: {}, 会话: {}, 知识库: {}", userId, sessionId, knowledgeId, e);
|
||||
|
||||
// 保存错误回复
|
||||
ChatMessageBo errorMessageBo = new ChatMessageBo();
|
||||
errorMessageBo.setSessionId(sessionId);
|
||||
errorMessageBo.setUserId(userId);
|
||||
errorMessageBo.setMessageType("text");
|
||||
errorMessageBo.setContent("抱歉,AI服务暂时不可用,请稍后重试。");
|
||||
errorMessageBo.setRole("assistant");
|
||||
errorMessageBo.setModelName(modelName);
|
||||
errorMessageBo.setCreateTime(new Date());
|
||||
chatMessageService.insertByBo(errorMessageBo);
|
||||
|
||||
return chatMessageService.queryById(errorMessageBo.getId());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public ChatSessionVo createKnowledgeSession(Long userId, Long knowledgeId, String sessionTitle) {
|
||||
// 验证知识库是否存在
|
||||
KnowledgeInfoVo knowledge = knowledgeInfoService.queryById(knowledgeId);
|
||||
if (ObjectUtil.isNull(knowledge)) {
|
||||
throw new RuntimeException("知识库不存在");
|
||||
}
|
||||
|
||||
// 如果没有提供标题,使用知识库名称作为默认标题
|
||||
if (StrUtil.isBlank(sessionTitle)) {
|
||||
sessionTitle = "基于" + knowledge.getKnowledgeName() + "的对话";
|
||||
}
|
||||
|
||||
// 创建会话
|
||||
ChatSessionBo sessionBo = new ChatSessionBo();
|
||||
sessionBo.setUserId(userId);
|
||||
sessionBo.setSessionTitle(sessionTitle);
|
||||
sessionBo.setSessionContent("知识库ID: " + knowledgeId);
|
||||
sessionBo.setCreateTime(new Date());
|
||||
chatSessionService.insertByBo(sessionBo);
|
||||
|
||||
return chatSessionService.queryById(sessionBo.getId());
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> searchKnowledgeContent(Long knowledgeId, String query, Integer limit) {
|
||||
try {
|
||||
log.info("搜索知识库内容 - 知识库: {}, 查询: {}, 限制: {}", knowledgeId, query, limit);
|
||||
List<KnowledgeSearchResultVo> searchResults = knowledgeVectorService.vectorSimilaritySearch(
|
||||
String.valueOf(knowledgeId), query, "default", limit != null ? limit : 5, 0.7
|
||||
);
|
||||
return searchResults.stream().map(KnowledgeSearchResultVo::getContent).toList();
|
||||
} catch (Exception e) {
|
||||
log.error("搜索知识库内容失败 - 知识库: {}, 查询: {}", knowledgeId, query, e);
|
||||
return Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String buildKnowledgeContext(Long knowledgeId, String question) {
|
||||
// 搜索相关知识库内容
|
||||
List<String> relevantContent = searchKnowledgeContent(knowledgeId, question, 3);
|
||||
|
||||
StringBuilder context = new StringBuilder();
|
||||
context.append("以下是相关的知识库内容:\n\n");
|
||||
|
||||
for (int i = 0; i < relevantContent.size(); i++) {
|
||||
context.append("参考资料 ").append(i + 1).append(": ");
|
||||
context.append(relevantContent.get(i)).append("\n\n");
|
||||
}
|
||||
|
||||
return context.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<ChatMessageVo> getKnowledgeChatHistory(Long sessionId, Integer limit) {
|
||||
// 获取会话的聊天历史
|
||||
List<ChatMessageVo> messages = chatMessageService.queryListBySessionId(sessionId);
|
||||
|
||||
// 如果指定了限制数量,返回最近的消息
|
||||
if (limit != null && limit > 0 && messages.size() > limit) {
|
||||
return messages.subList(Math.max(0, messages.size() - limit), messages.size());
|
||||
}
|
||||
|
||||
return messages;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package org.dromara.chat.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.chat.domain.ChatConfig;
|
||||
import org.dromara.chat.domain.bo.ChatConfigBo;
|
||||
import org.dromara.chat.domain.vo.ChatConfigVo;
|
||||
import org.dromara.chat.mapper.ChatConfigMapper;
|
||||
import org.dromara.chat.service.ChatConfigService;
|
||||
import org.dromara.common.core.utils.MapstructUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天配置Service业务层处理
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ChatConfigServiceImpl extends ServiceImpl<ChatConfigMapper, ChatConfig> implements ChatConfigService {
|
||||
|
||||
/**
|
||||
* 查询聊天配置
|
||||
*/
|
||||
@Override
|
||||
public ChatConfigVo queryById(Long id) {
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天配置列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<ChatConfigVo> queryPageList(ChatConfigBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<ChatConfig> lqw = buildQueryWrapper(bo);
|
||||
Page<ChatConfigVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天配置列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatConfigVo> queryList(ChatConfigBo bo) {
|
||||
LambdaQueryWrapper<ChatConfig> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ChatConfig> buildQueryWrapper(ChatConfigBo bo) {
|
||||
LambdaQueryWrapper<ChatConfig> lqw = Wrappers.lambdaQuery();
|
||||
lqw.like(StringUtils.isNotBlank(bo.getCategory()), ChatConfig::getCategory, bo.getCategory());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getConfigName()), ChatConfig::getConfigName, bo.getConfigName());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getConfigValue()), ChatConfig::getConfigValue, bo.getConfigValue());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getVersion()), ChatConfig::getVersion, bo.getVersion());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增聊天配置
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(ChatConfigBo bo) {
|
||||
ChatConfig add = MapstructUtils.convert(bo, ChatConfig.class);
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改聊天配置
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(ChatConfigBo bo) {
|
||||
ChatConfig update = MapstructUtils.convert(bo, ChatConfig.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(ChatConfig entity) {
|
||||
// TODO 做一些数据校验,如唯一约束
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并批量删除聊天配置
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if (isValid) {
|
||||
// TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteBatchIds(ids) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置分类查询配置列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatConfigVo> queryListByCategory(String category) {
|
||||
LambdaQueryWrapper<ChatConfig> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ChatConfig::getCategory, category);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置名称查询配置
|
||||
*/
|
||||
@Override
|
||||
public ChatConfigVo queryByConfigName(String configName) {
|
||||
LambdaQueryWrapper<ChatConfig> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ChatConfig::getConfigName, configName);
|
||||
return baseMapper.selectVoOne(lqw);
|
||||
}
|
||||
|
||||
}
|
||||
+151
@@ -0,0 +1,151 @@
|
||||
package org.dromara.chat.service.impl;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.chat.domain.ChatMessage;
|
||||
import org.dromara.chat.domain.bo.ChatMessageBo;
|
||||
import org.dromara.chat.domain.vo.ChatMessageVo;
|
||||
import org.dromara.chat.mapper.ChatMessageMapper;
|
||||
import org.dromara.chat.service.ChatMessageService;
|
||||
import org.dromara.common.core.utils.MapstructUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天消息Service业务层处理
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ChatMessageServiceImpl extends ServiceImpl<ChatMessageMapper, ChatMessage> implements ChatMessageService {
|
||||
|
||||
/**
|
||||
* 查询聊天消息
|
||||
*/
|
||||
@Override
|
||||
public ChatMessageVo queryById(Long id) {
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天消息列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<ChatMessageVo> queryPageList(ChatMessageBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<ChatMessage> lqw = buildQueryWrapper(bo);
|
||||
Page<ChatMessageVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天消息列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatMessageVo> queryList(ChatMessageBo bo) {
|
||||
LambdaQueryWrapper<ChatMessage> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ChatMessage> buildQueryWrapper(ChatMessageBo bo) {
|
||||
LambdaQueryWrapper<ChatMessage> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ObjectUtil.isNotNull(bo.getSessionId()), ChatMessage::getSessionId, bo.getSessionId());
|
||||
lqw.eq(ObjectUtil.isNotNull(bo.getUserId()), ChatMessage::getUserId, bo.getUserId());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getContent()), ChatMessage::getContent, bo.getContent());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getRole()), ChatMessage::getRole, bo.getRole());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getModelName()), ChatMessage::getModelName, bo.getModelName());
|
||||
lqw.orderByDesc(ChatMessage::getCreateTime);
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增聊天消息
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(ChatMessageBo bo) {
|
||||
ChatMessage add = MapstructUtils.convert(bo, ChatMessage.class);
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改聊天消息
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(ChatMessageBo bo) {
|
||||
ChatMessage update = MapstructUtils.convert(bo, ChatMessage.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(ChatMessage entity) {
|
||||
if (StringUtils.isBlank(entity.getContent())) {
|
||||
throw new IllegalArgumentException("消息内容不能为空");
|
||||
}
|
||||
if (StringUtils.isBlank(entity.getRole())) {
|
||||
entity.setRole("user");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并批量删除聊天消息
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if (isValid) {
|
||||
// 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteBatchIds(ids) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID查询消息列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatMessageVo> queryListBySessionId(Long sessionId) {
|
||||
LambdaQueryWrapper<ChatMessage> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ChatMessage::getSessionId, sessionId);
|
||||
lqw.orderByAsc(ChatMessage::getCreateTime);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID查询消息列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatMessageVo> queryListByUserId(Long userId) {
|
||||
LambdaQueryWrapper<ChatMessage> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ChatMessage::getUserId, userId);
|
||||
lqw.orderByDesc(ChatMessage::getCreateTime);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据会话ID和用户ID查询消息列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatMessageVo> queryListBySessionIdAndUserId(Long sessionId, Long userId) {
|
||||
LambdaQueryWrapper<ChatMessage> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ChatMessage::getSessionId, sessionId);
|
||||
lqw.eq(ChatMessage::getUserId, userId);
|
||||
lqw.orderByAsc(ChatMessage::getCreateTime);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
package org.dromara.chat.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.chat.domain.ChatModel;
|
||||
import org.dromara.chat.domain.bo.ChatModelBo;
|
||||
import org.dromara.chat.domain.vo.ChatModelVo;
|
||||
import org.dromara.chat.mapper.ChatModelMapper;
|
||||
import org.dromara.chat.service.ChatModelService;
|
||||
import org.dromara.common.core.utils.MapstructUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天模型Service业务层处理
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ChatModelServiceImpl extends ServiceImpl<ChatModelMapper, ChatModel> implements ChatModelService {
|
||||
|
||||
/**
|
||||
* 查询聊天模型
|
||||
*/
|
||||
@Override
|
||||
public ChatModelVo queryById(Long id) {
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天模型列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<ChatModelVo> queryPageList(ChatModelBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<ChatModel> lqw = buildQueryWrapper(bo);
|
||||
Page<ChatModelVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天模型列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatModelVo> queryList(ChatModelBo bo) {
|
||||
LambdaQueryWrapper<ChatModel> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ChatModel> buildQueryWrapper(ChatModelBo bo) {
|
||||
LambdaQueryWrapper<ChatModel> lqw = Wrappers.lambdaQuery();
|
||||
lqw.like(StringUtils.isNotBlank(bo.getCategory()), ChatModel::getCategory, bo.getCategory());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getModelName()), ChatModel::getModelName, bo.getModelName());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getModelDescribe()), ChatModel::getModelDescribe, bo.getModelDescribe());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getModelType()), ChatModel::getModelType, bo.getModelType());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getModelShow()), ChatModel::getModelShow, bo.getModelShow());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增聊天模型
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(ChatModelBo bo) {
|
||||
ChatModel add = MapstructUtils.convert(bo, ChatModel.class);
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改聊天模型
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(ChatModelBo bo) {
|
||||
ChatModel update = MapstructUtils.convert(bo, ChatModel.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(ChatModel entity) {
|
||||
// TODO 做一些数据校验,如唯一约束
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并批量删除聊天模型
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if (isValid) {
|
||||
// TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteBatchIds(ids) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型分类查询模型列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatModelVo> queryListByCategory(String category) {
|
||||
LambdaQueryWrapper<ChatModel> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ChatModel::getCategory, category);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型名称查询模型
|
||||
*/
|
||||
@Override
|
||||
public ChatModelVo queryByModelName(String modelName) {
|
||||
LambdaQueryWrapper<ChatModel> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ChatModel::getModelName, modelName);
|
||||
return baseMapper.selectVoOne(lqw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询显示的模型列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatModelVo> queryShowModelList() {
|
||||
LambdaQueryWrapper<ChatModel> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ChatModel::getModelShow, "1");
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型类型查询模型列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatModelVo> queryListByModelType(String modelType) {
|
||||
LambdaQueryWrapper<ChatModel> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ChatModel::getModelType, modelType);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
}
|
||||
+157
@@ -0,0 +1,157 @@
|
||||
package org.dromara.chat.service.impl;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.chat.domain.ChatMessage;
|
||||
import org.dromara.chat.domain.ChatSession;
|
||||
import org.dromara.chat.domain.bo.ChatSessionBo;
|
||||
import org.dromara.chat.domain.vo.ChatSessionVo;
|
||||
import org.dromara.chat.mapper.ChatMessageMapper;
|
||||
import org.dromara.chat.mapper.ChatSessionMapper;
|
||||
import org.dromara.chat.service.ChatSessionService;
|
||||
import org.dromara.common.core.utils.MapstructUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天会话Service业务层处理
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ChatSessionServiceImpl extends ServiceImpl<ChatSessionMapper, ChatSession> implements ChatSessionService {
|
||||
|
||||
private final ChatMessageMapper chatMessageMapper;
|
||||
|
||||
/**
|
||||
* 查询聊天会话
|
||||
*/
|
||||
@Override
|
||||
public ChatSessionVo queryById(Long id) {
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天会话列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<ChatSessionVo> queryPageList(ChatSessionBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<ChatSession> lqw = buildQueryWrapper(bo);
|
||||
Page<ChatSessionVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天会话列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatSessionVo> queryList(ChatSessionBo bo) {
|
||||
LambdaQueryWrapper<ChatSession> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ChatSession> buildQueryWrapper(ChatSessionBo bo) {
|
||||
LambdaQueryWrapper<ChatSession> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ObjectUtil.isNotNull(bo.getUserId()), ChatSession::getUserId, bo.getUserId());
|
||||
lqw.like(StringUtils.isNotBlank(bo.getSessionTitle()), ChatSession::getSessionTitle, bo.getSessionTitle());
|
||||
lqw.orderByDesc(ChatSession::getUpdateTime);
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增聊天会话
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(ChatSessionBo bo) {
|
||||
ChatSession add = MapstructUtils.convert(bo, ChatSession.class);
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改聊天会话
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(ChatSessionBo bo) {
|
||||
ChatSession update = MapstructUtils.convert(bo, ChatSession.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(ChatSession entity) {
|
||||
if (StringUtils.isBlank(entity.getSessionTitle())) {
|
||||
entity.setSessionTitle("新会话");
|
||||
}
|
||||
if (entity.getSessionTitle().length() > 50) {
|
||||
entity.setSessionTitle(entity.getSessionTitle().substring(0, 50));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并批量删除聊天会话
|
||||
*/
|
||||
@Override
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if (isValid) {
|
||||
// 删除关联的消息
|
||||
LambdaQueryWrapper<ChatMessage> lqw = Wrappers.lambdaQuery();
|
||||
lqw.in(ChatMessage::getSessionId, ids);
|
||||
chatMessageMapper.delete(lqw);
|
||||
}
|
||||
return baseMapper.deleteBatchIds(ids) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID查询会话列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatSessionVo> queryListByUserId(Long userId) {
|
||||
LambdaQueryWrapper<ChatSession> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ChatSession::getUserId, userId);
|
||||
lqw.orderByDesc(ChatSession::getUpdateTime);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新会话
|
||||
*/
|
||||
@Override
|
||||
public ChatSessionVo createNewSession(Long userId, String sessionTitle) {
|
||||
ChatSession session = new ChatSession();
|
||||
session.setUserId(userId);
|
||||
session.setSessionTitle(sessionTitle);
|
||||
baseMapper.insert(session);
|
||||
return baseMapper.selectVoById(session.getId());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新会话标题
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateSessionTitle(Long sessionId, String sessionTitle) {
|
||||
ChatSession session = new ChatSession();
|
||||
session.setId(sessionId);
|
||||
session.setSessionTitle(sessionTitle);
|
||||
return baseMapper.updateById(session) > 0;
|
||||
}
|
||||
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package org.dromara.chat.service.impl;
|
||||
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.dromara.chat.domain.ChatUsageToken;
|
||||
import org.dromara.chat.domain.bo.ChatUsageTokenBo;
|
||||
import org.dromara.chat.domain.vo.ChatUsageTokenVo;
|
||||
import org.dromara.chat.mapper.ChatUsageTokenMapper;
|
||||
import org.dromara.chat.service.ChatUsageTokenService;
|
||||
import org.dromara.common.core.utils.MapstructUtils;
|
||||
import org.dromara.common.core.utils.StringUtils;
|
||||
import org.dromara.common.mybatis.core.page.PageQuery;
|
||||
import org.dromara.common.mybatis.core.page.TableDataInfo;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天Token使用记录Service业务层处理
|
||||
*
|
||||
* @author pengles
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@Service
|
||||
public class ChatUsageTokenServiceImpl extends ServiceImpl<ChatUsageTokenMapper, ChatUsageToken> implements ChatUsageTokenService {
|
||||
|
||||
/**
|
||||
* 查询聊天Token使用记录
|
||||
*/
|
||||
@Override
|
||||
public ChatUsageTokenVo queryById(Long id) {
|
||||
return baseMapper.selectVoById(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天Token使用记录列表
|
||||
*/
|
||||
@Override
|
||||
public TableDataInfo<ChatUsageTokenVo> queryPageList(ChatUsageTokenBo bo, PageQuery pageQuery) {
|
||||
LambdaQueryWrapper<ChatUsageToken> lqw = buildQueryWrapper(bo);
|
||||
Page<ChatUsageTokenVo> result = baseMapper.selectVoPage(pageQuery.build(), lqw);
|
||||
return TableDataInfo.build(result);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天Token使用记录列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatUsageTokenVo> queryList(ChatUsageTokenBo bo) {
|
||||
LambdaQueryWrapper<ChatUsageToken> lqw = buildQueryWrapper(bo);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
private LambdaQueryWrapper<ChatUsageToken> buildQueryWrapper(ChatUsageTokenBo bo) {
|
||||
LambdaQueryWrapper<ChatUsageToken> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ObjectUtil.isNotNull(bo.getUserId()), ChatUsageToken::getUserId, bo.getUserId());
|
||||
lqw.eq(StringUtils.isNotBlank(bo.getModelName()), ChatUsageToken::getModelName, bo.getModelName());
|
||||
return lqw;
|
||||
}
|
||||
|
||||
/**
|
||||
* 新增聊天Token使用记录
|
||||
*/
|
||||
@Override
|
||||
public Boolean insertByBo(ChatUsageTokenBo bo) {
|
||||
ChatUsageToken add = MapstructUtils.convert(bo, ChatUsageToken.class);
|
||||
validEntityBeforeSave(add);
|
||||
boolean flag = baseMapper.insert(add) > 0;
|
||||
if (flag) {
|
||||
bo.setId(add.getId());
|
||||
}
|
||||
return flag;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改聊天Token使用记录
|
||||
*/
|
||||
@Override
|
||||
public Boolean updateByBo(ChatUsageTokenBo bo) {
|
||||
ChatUsageToken update = MapstructUtils.convert(bo, ChatUsageToken.class);
|
||||
validEntityBeforeSave(update);
|
||||
return baseMapper.updateById(update) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存前的数据校验
|
||||
*/
|
||||
private void validEntityBeforeSave(ChatUsageToken entity) {
|
||||
// TODO 做一些数据校验,如唯一约束
|
||||
}
|
||||
|
||||
/**
|
||||
* 校验并批量删除聊天Token使用记录
|
||||
*/
|
||||
@Override
|
||||
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
|
||||
if (isValid) {
|
||||
// TODO 做一些业务上的校验,判断是否需要校验
|
||||
}
|
||||
return baseMapper.deleteBatchIds(ids) > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据用户ID查询Token使用记录列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatUsageTokenVo> queryListByUserId(Long userId) {
|
||||
LambdaQueryWrapper<ChatUsageToken> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ChatUsageToken::getUserId, userId);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据模型名称查询Token使用记录列表
|
||||
*/
|
||||
@Override
|
||||
public List<ChatUsageTokenVo> queryListByModelName(String modelName) {
|
||||
LambdaQueryWrapper<ChatUsageToken> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ChatUsageToken::getModelName, modelName);
|
||||
return baseMapper.selectVoList(lqw);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计用户总Token使用量
|
||||
*/
|
||||
@Override
|
||||
public Long getTotalTokenByUserId(Long userId) {
|
||||
LambdaQueryWrapper<ChatUsageToken> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ChatUsageToken::getUserId, userId);
|
||||
lqw.select(ChatUsageToken::getToken);
|
||||
List<ChatUsageToken> list = baseMapper.selectList(lqw);
|
||||
return list.stream().mapToLong(ChatUsageToken::getToken).sum();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计模型总Token使用量
|
||||
*/
|
||||
@Override
|
||||
public Long getTotalTokenByModelName(String modelName) {
|
||||
LambdaQueryWrapper<ChatUsageToken> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(ChatUsageToken::getModelName, modelName);
|
||||
lqw.select(ChatUsageToken::getToken);
|
||||
List<ChatUsageToken> list = baseMapper.selectList(lqw);
|
||||
return list.stream().mapToLong(ChatUsageToken::getToken).sum();
|
||||
}
|
||||
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
package org.dromara.chat.service;
|
||||
|
||||
import org.dromara.chat.domain.ChatSession;
|
||||
import org.dromara.chat.domain.bo.ChatSessionBo;
|
||||
import org.dromara.chat.mapper.ChatSessionMapper;
|
||||
import org.dromara.chat.service.impl.ChatSessionServiceImpl;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.InjectMocks;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.Mockito;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
/**
|
||||
* ChatSessionService单元测试
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
public class ChatSessionServiceTest {
|
||||
|
||||
@Mock
|
||||
private ChatSessionMapper chatSessionMapper;
|
||||
|
||||
@InjectMocks
|
||||
private ChatSessionServiceImpl chatSessionService;
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testInsertByBo() {
|
||||
ChatSessionBo bo = new ChatSessionBo();
|
||||
bo.setUserId(1L);
|
||||
bo.setSessionTitle("Test Session");
|
||||
|
||||
Mockito.when(chatSessionMapper.insert(Mockito.any(ChatSession.class))).thenReturn(1);
|
||||
|
||||
Boolean result = chatSessionService.insertByBo(bo);
|
||||
Assertions.assertTrue(result);
|
||||
Mockito.verify(chatSessionMapper).insert(Mockito.any(ChatSession.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUpdateSessionTitle() {
|
||||
Long sessionId = 1L;
|
||||
String newTitle = "Updated Title";
|
||||
|
||||
Mockito.when(chatSessionMapper.updateById(Mockito.any(ChatSession.class))).thenReturn(1);
|
||||
|
||||
Boolean result = chatSessionService.updateSessionTitle(sessionId, newTitle);
|
||||
Assertions.assertTrue(result);
|
||||
Mockito.verify(chatSessionMapper).updateById(Mockito.any(ChatSession.class));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user