refactor: 重命名 backend-single 目录为 server
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
# AI配置管理模块
|
||||
|
||||
## 概述
|
||||
基于t_ai_config表结构生成的完整AI配置管理模块,提供AI接口配置的增删改查功能。
|
||||
|
||||
## 生成的文件
|
||||
|
||||
### 1. 实体类
|
||||
- `com.emotion.entity.AiConfig` - AI配置实体类,继承BaseEntity
|
||||
|
||||
### 2. 数据访问层
|
||||
- `com.emotion.mapper.AiConfigMapper` - MyBatis-Plus Mapper接口
|
||||
|
||||
### 3. 业务逻辑层
|
||||
- `com.emotion.service.AiConfigService` - 服务接口
|
||||
- `com.emotion.service.impl.AiConfigServiceImpl` - 服务实现类
|
||||
|
||||
### 4. 控制器层
|
||||
- `com.emotion.controller.AiConfigController` - REST API控制器
|
||||
|
||||
### 5. 请求对象
|
||||
- `com.emotion.dto.request.aiconfig.AiConfigCreateRequest` - 创建请求
|
||||
- `com.emotion.dto.request.aiconfig.AiConfigUpdateRequest` - 更新请求
|
||||
- `com.emotion.dto.request.aiconfig.AiConfigPageRequest` - 分页查询请求
|
||||
- `com.emotion.dto.request.aiconfig.AiConfigEnableRequest` - 启用/禁用请求
|
||||
- `com.emotion.dto.request.aiconfig.AiConfigDefaultRequest` - 默认配置设置请求
|
||||
|
||||
### 6. 响应对象
|
||||
- `com.emotion.dto.response.aiconfig.AiConfigResponse` - 响应对象
|
||||
|
||||
## API接口列表
|
||||
|
||||
### 基础CRUD操作
|
||||
- `GET /aiConfig/page` - 分页查询AI配置
|
||||
- `GET /aiConfig/detail?id={id}` - 根据ID获取AI配置详情
|
||||
- `POST /aiConfig/create` - 创建AI配置
|
||||
- `PUT /aiConfig/update` - 更新AI配置
|
||||
- `DELETE /aiConfig/delete?id={id}` - 删除AI配置
|
||||
|
||||
### 条件查询接口
|
||||
- `GET /aiConfig/byConfigType?configType={type}` - 根据配置类型查询
|
||||
- `GET /aiConfig/byProvider?provider={provider}` - 根据服务提供商查询
|
||||
- `GET /aiConfig/byUsageScenario?usageScenario={scenario}` - 根据使用场景查询
|
||||
- `GET /aiConfig/byEnvironment?environment={env}` - 根据环境查询
|
||||
- `GET /aiConfig/byConfigKey?configKey={key}` - 根据配置键值查询
|
||||
|
||||
### 状态管理接口
|
||||
- `GET /aiConfig/enabled` - 查询已启用的配置
|
||||
- `GET /aiConfig/disabled` - 查询已禁用的配置
|
||||
- `GET /aiConfig/default` - 查询默认配置
|
||||
- `PUT /aiConfig/enable?id={id}` - 启用配置
|
||||
- `PUT /aiConfig/disable?id={id}` - 禁用配置
|
||||
- `PUT /aiConfig/setDefault?id={id}` - 设置为默认配置
|
||||
- `PUT /aiConfig/unsetDefault?id={id}` - 取消默认配置
|
||||
|
||||
### 智能查询接口
|
||||
- `GET /aiConfig/bestConfig?usageScenario={scenario}&environment={env}` - 查询最优配置
|
||||
|
||||
### 统计接口
|
||||
- `GET /aiConfig/countEnabled` - 统计已启用配置数量
|
||||
- `GET /aiConfig/countDisabled` - 统计已禁用配置数量
|
||||
- `GET /aiConfig/countDefault` - 统计默认配置数量
|
||||
- `GET /aiConfig/countByConfigType?configType={type}` - 根据配置类型统计数量
|
||||
- `GET /aiConfig/countByProvider?provider={provider}` - 根据服务提供商统计数量
|
||||
|
||||
## 特性
|
||||
|
||||
### 1. 遵循项目规范
|
||||
- 继承BaseEntity,包含公共字段
|
||||
- 使用雪花算法生成ID
|
||||
- 使用LambdaQueryWrapper构造查询条件
|
||||
- 统一的异常处理和返回格式
|
||||
|
||||
### 2. 安全特性
|
||||
- API Token脱敏显示
|
||||
- 参数校验
|
||||
- 逻辑删除
|
||||
|
||||
### 3. 查询优化
|
||||
- 支持关键词搜索
|
||||
- 支持多条件过滤
|
||||
- 支持排序
|
||||
- 支持分页
|
||||
|
||||
### 4. 业务功能
|
||||
- 配置启用/禁用管理
|
||||
- 默认配置设置
|
||||
- 智能配置选择(根据场景和环境)
|
||||
- 优先级排序
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 创建AI配置
|
||||
```json
|
||||
POST /aiConfig/create
|
||||
{
|
||||
"configName": "Coze聊天配置",
|
||||
"configKey": "coze_chat_default",
|
||||
"configType": "coze",
|
||||
"provider": "coze",
|
||||
"apiBaseUrl": "https://api.coze.cn",
|
||||
"apiToken": "your_api_token",
|
||||
"usageScenario": "chat",
|
||||
"isEnabled": 1,
|
||||
"environment": "production"
|
||||
}
|
||||
```
|
||||
|
||||
### 分页查询
|
||||
```
|
||||
GET /aiConfig/page?current=1&size=10&keyword=coze&configType=coze&isEnabled=1
|
||||
```
|
||||
|
||||
### 查询最优配置
|
||||
```
|
||||
GET /aiConfig/bestConfig?usageScenario=chat&environment=production
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 所有接口都遵循统一的返回格式Result<T>
|
||||
2. API Token在响应中会进行脱敏处理
|
||||
3. 删除操作使用逻辑删除
|
||||
4. 配置键值(configKey)必须唯一
|
||||
5. 最优配置查询会优先返回默认配置,然后按优先级排序
|
||||
@@ -0,0 +1,234 @@
|
||||
# API Base URL 字段调整总结
|
||||
|
||||
## 修改概述
|
||||
|
||||
将 `AiConfig` 实体类中的 `apiBaseUrl` 字段从"基础URL"调整为"完整的API URL",不再需要在调用时拼接任何后缀。
|
||||
|
||||
## 修改内容
|
||||
|
||||
### 1. 实体类和DTO修改
|
||||
|
||||
#### 1.1 AiConfig.java
|
||||
- **文件路径**: `backend-single/src/main/java/com/emotion/entity/AiConfig.java`
|
||||
- **修改内容**: 更新 `apiBaseUrl` 字段注释
|
||||
- **修改前**: `API基础URL`
|
||||
- **修改后**: `API完整URL(包含完整的接口路径,不需要再拼接任何后缀)例如:https://api.coze.cn/v3/chat`
|
||||
|
||||
#### 1.2 AiConfigCreateRequest.java
|
||||
- **文件路径**: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigCreateRequest.java`
|
||||
- **修改内容**: 更新 `apiBaseUrl` 字段注释和验证消息
|
||||
- **修改前**: `API基础URL` / `API基础URL不能为空`
|
||||
- **修改后**: `API完整URL(包含完整的接口路径,不需要再拼接任何后缀)` / `API完整URL不能为空`
|
||||
|
||||
#### 1.3 AiConfigUpdateRequest.java
|
||||
- **文件路径**: `backend-single/src/main/java/com/emotion/dto/request/aiconfig/AiConfigUpdateRequest.java`
|
||||
- **修改内容**: 更新 `apiBaseUrl` 字段注释
|
||||
- **修改前**: `API基础URL`
|
||||
- **修改后**: `API完整URL(包含完整的接口路径,不需要再拼接任何后缀)`
|
||||
|
||||
#### 1.4 AiConfigResponse.java
|
||||
- **文件路径**: `backend-single/src/main/java/com/emotion/dto/response/aiconfig/AiConfigResponse.java`
|
||||
- **修改内容**: 更新 `apiBaseUrl` 字段注释
|
||||
- **修改前**: `API基础URL`
|
||||
- **修改后**: `API完整URL(包含完整的接口路径,不需要再拼接任何后缀)`
|
||||
|
||||
### 2. 后端服务层修改
|
||||
|
||||
#### 2.1 AiChatServiceImpl.java
|
||||
- **文件路径**: `backend-single/src/main/java/com/emotion/service/impl/AiChatServiceImpl.java`
|
||||
|
||||
##### 修改1: getApiPath方法
|
||||
```java
|
||||
// 修改前
|
||||
private String getApiPath(AiConfig config) {
|
||||
// 默认使用 /v3/chat 路径
|
||||
return "/v3/chat";
|
||||
}
|
||||
|
||||
// 修改后
|
||||
private String getApiPath(AiConfig config) {
|
||||
// apiBaseUrl已经是完整的URL,返回空字符串
|
||||
return "";
|
||||
}
|
||||
```
|
||||
|
||||
##### 修改2: 新增extractBaseUrl方法
|
||||
```java
|
||||
/**
|
||||
* 从apiBaseUrl中提取基础URL(用于状态查询和消息查询等辅助接口)
|
||||
* 例如:https://api.coze.cn/v3/chat -> https://api.coze.cn
|
||||
*/
|
||||
private String extractBaseUrl(String apiBaseUrl) {
|
||||
if (apiBaseUrl == null || apiBaseUrl.isEmpty()) {
|
||||
return "";
|
||||
}
|
||||
try {
|
||||
java.net.URL url = new java.net.URL(apiBaseUrl);
|
||||
return url.getProtocol() + "://" + url.getHost() + (url.getPort() > 0 ? ":" + url.getPort() : "");
|
||||
} catch (Exception e) {
|
||||
log.warn("解析apiBaseUrl失败: {}", apiBaseUrl, e);
|
||||
// 尝试简单截取
|
||||
int pathIndex = apiBaseUrl.indexOf("/", apiBaseUrl.indexOf("://") + 3);
|
||||
if (pathIndex > 0) {
|
||||
return apiBaseUrl.substring(0, pathIndex);
|
||||
}
|
||||
return apiBaseUrl;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### 修改3: 状态查询URL构建
|
||||
```java
|
||||
// 修改前
|
||||
String statusUrl = config.getApiBaseUrl() + "/v3/chat/retrieve?chat_id=" + chatId + "&conversation_id=" + conversationId;
|
||||
|
||||
// 修改后
|
||||
String baseUrl = extractBaseUrl(config.getApiBaseUrl());
|
||||
String statusUrl = baseUrl + "/v3/chat/retrieve?chat_id=" + chatId + "&conversation_id=" + conversationId;
|
||||
```
|
||||
|
||||
##### 修改4: 消息查询URL构建
|
||||
```java
|
||||
// 修改前
|
||||
String messagesUrl = config.getApiBaseUrl() + "/v3/chat/message/list?chat_id=" + chatId + "&conversation_id=" + conversationId;
|
||||
|
||||
// 修改后
|
||||
String baseUrl = extractBaseUrl(config.getApiBaseUrl());
|
||||
String messagesUrl = baseUrl + "/v3/chat/message/list?chat_id=" + chatId + "&conversation_id=" + conversationId;
|
||||
```
|
||||
|
||||
### 3. 前端修改
|
||||
|
||||
#### 3.1 web-admin/src/views/aiconfig/AiConfigList.vue
|
||||
|
||||
##### 修改1: 表单字段标签和提示
|
||||
```vue
|
||||
<!-- 修改前 -->
|
||||
<el-form-item label="API基础URL" prop="apiBaseUrl">
|
||||
<el-input v-model="formData.apiBaseUrl" placeholder="请输入API基础URL" />
|
||||
</el-form-item>
|
||||
|
||||
<!-- 修改后 -->
|
||||
<el-form-item label="API完整URL" prop="apiBaseUrl">
|
||||
<el-input v-model="formData.apiBaseUrl" placeholder="请输入完整的API URL,如:https://api.coze.cn/v3/chat" />
|
||||
</el-form-item>
|
||||
```
|
||||
|
||||
##### 修改2: 详情展示标签
|
||||
```vue
|
||||
<!-- 修改前 -->
|
||||
<el-descriptions-item label="API基础URL">{{ viewData.apiBaseUrl }}</el-descriptions-item>
|
||||
|
||||
<!-- 修改后 -->
|
||||
<el-descriptions-item label="API完整URL">{{ viewData.apiBaseUrl }}</el-descriptions-item>
|
||||
```
|
||||
|
||||
##### 修改3: 表单验证规则
|
||||
```javascript
|
||||
// 修改前
|
||||
apiBaseUrl: [{ required: true, message: '请输入API基础URL', trigger: 'blur' }]
|
||||
|
||||
// 修改后
|
||||
apiBaseUrl: [{ required: true, message: '请输入完整的API URL', trigger: 'blur' }]
|
||||
```
|
||||
|
||||
##### 修改4: 测试功能URL构建
|
||||
```javascript
|
||||
// 修改前
|
||||
const initTestData = (config: AiConfig) => {
|
||||
testRequest.url = config.apiBaseUrl + '/v3/chat'
|
||||
// ...
|
||||
}
|
||||
|
||||
// 修改后
|
||||
const initTestData = (config: AiConfig) => {
|
||||
// apiBaseUrl已经是完整的API URL,直接使用
|
||||
testRequest.url = config.apiBaseUrl
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 单元测试
|
||||
|
||||
#### 4.1 AiChatServiceImplTest.java
|
||||
- **文件路径**: `backend-single/src/test/java/com/emotion/service/AiChatServiceImplTest.java`
|
||||
- **测试内容**:
|
||||
1. `testGetApiPath`: 测试getApiPath方法返回空字符串
|
||||
2. `testExtractBaseUrl`: 测试extractBaseUrl方法提取基础URL
|
||||
3. `testApiUrlConstruction`: 测试完整API URL的构建逻辑
|
||||
4. `testStatusQueryUrlConstruction`: 测试状态查询URL的构建逻辑
|
||||
5. `testMessageQueryUrlConstruction`: 测试消息查询URL的构建逻辑
|
||||
6. `testDifferentApiBaseUrlFormats`: 测试不同格式的apiBaseUrl
|
||||
7. `testEdgeCases`: 测试边界情况
|
||||
|
||||
**测试结果**: ✅ 所有7个测试用例全部通过
|
||||
|
||||
## 影响范围
|
||||
|
||||
### 后端影响
|
||||
1. **AiConfig实体类**: 字段语义变更,但数据库字段名不变
|
||||
2. **AiChatServiceImpl**: API调用逻辑调整,不再拼接路径
|
||||
3. **DTO类**: 注释和验证消息更新
|
||||
|
||||
### 前端影响
|
||||
1. **web-admin AI配置管理页面**:
|
||||
- 表单标签和提示文本更新
|
||||
- 测试功能URL构建逻辑调整
|
||||
- 用户需要输入完整的API URL
|
||||
|
||||
### 数据库影响
|
||||
- **无需修改数据库**: 字段名 `api_base_url` 保持不变
|
||||
- **数据迁移**: 需要将现有数据从基础URL更新为完整URL
|
||||
- 例如: `https://api.coze.cn` → `https://api.coze.cn/v3/chat`
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 修改前
|
||||
```java
|
||||
AiConfig config = new AiConfig();
|
||||
config.setApiBaseUrl("https://api.coze.cn"); // 基础URL
|
||||
// 调用时拼接: config.getApiBaseUrl() + "/v3/chat"
|
||||
```
|
||||
|
||||
### 修改后
|
||||
```java
|
||||
AiConfig config = new AiConfig();
|
||||
config.setApiBaseUrl("https://api.coze.cn/v3/chat"); // 完整URL
|
||||
// 调用时直接使用: config.getApiBaseUrl()
|
||||
```
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 后端测试
|
||||
```bash
|
||||
cd backend-single
|
||||
mvn test -Dtest=AiChatServiceImplTest
|
||||
```
|
||||
|
||||
**测试结果**:
|
||||
- Tests run: 7
|
||||
- Failures: 0
|
||||
- Errors: 0
|
||||
- Skipped: 0
|
||||
- ✅ 全部通过
|
||||
|
||||
### 前端测试
|
||||
1. 启动 web-admin 前端项目
|
||||
2. 进入 AI配置管理页面
|
||||
3. 点击"新增配置"或"编辑"按钮
|
||||
4. 在"API完整URL"字段输入完整的API地址(如:`https://api.coze.cn/v3/chat`)
|
||||
5. 保存配置
|
||||
6. 点击"测试"按钮,验证接口调用是否成功
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **数据迁移**: 现有的AI配置数据需要更新,将基础URL改为完整URL
|
||||
2. **向后兼容**: 如果有其他服务依赖旧的URL格式,需要同步更新
|
||||
3. **文档更新**: 需要更新相关的API文档和使用说明
|
||||
4. **配置示例**: 需要更新配置示例,明确说明应该填写完整的API URL
|
||||
|
||||
## 完成时间
|
||||
2025-12-22
|
||||
|
||||
## 修改人员
|
||||
System
|
||||
@@ -0,0 +1,17 @@
|
||||
# Emotion Museum ASR Service
|
||||
|
||||
Private speech-to-text service for the mini program voice input.
|
||||
|
||||
Install on `101.200.208.45`:
|
||||
|
||||
```bash
|
||||
cd /data/programs/emotion-museum/asr-service
|
||||
python3.11 -m venv .venv
|
||||
. .venv/bin/activate
|
||||
pip install -r requirements.txt
|
||||
|
||||
uvicorn app:app --host 127.0.0.1 --port 19120
|
||||
curl http://127.0.0.1:19120/health
|
||||
```
|
||||
|
||||
The service uses FunASR ONNX Runtime with a local Chinese Paraformer model on CPU and is intended to stay private on localhost. The Java backend exposes the authenticated public API.
|
||||
@@ -0,0 +1,114 @@
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
import time
|
||||
import types
|
||||
import importlib.machinery
|
||||
from pathlib import Path
|
||||
from threading import Lock
|
||||
|
||||
from fastapi import FastAPI, File, UploadFile
|
||||
|
||||
app = FastAPI(title="Emotion Museum ASR")
|
||||
|
||||
MODEL_NAME = os.getenv("ASR_MODEL", "/data/programs/emotion-museum/asr-service/models/paraformer-zh-onnx")
|
||||
DEVICE = os.getenv("ASR_DEVICE", "cpu")
|
||||
WORK_DIR = Path(os.getenv("ASR_WORK_DIR", "/tmp/emotion-museum-asr"))
|
||||
WORK_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
_model = None
|
||||
_model_lock = Lock()
|
||||
|
||||
|
||||
def get_model():
|
||||
global _model
|
||||
with _model_lock:
|
||||
if _model is None:
|
||||
# funasr-onnx imports the optional SenseVoice module from package
|
||||
# __init__, which imports torch even when we only use Paraformer.
|
||||
# This service intentionally runs the ONNX path without PyTorch.
|
||||
if "torch" not in sys.modules:
|
||||
torch_stub = types.ModuleType("torch")
|
||||
torch_stub.__spec__ = importlib.machinery.ModuleSpec("torch", loader=None)
|
||||
torch_stub.Tensor = type("Tensor", (), {})
|
||||
sys.modules["torch"] = torch_stub
|
||||
from funasr_onnx import Paraformer
|
||||
|
||||
_model = Paraformer(
|
||||
MODEL_NAME,
|
||||
batch_size=1,
|
||||
device_id=-1,
|
||||
quantize=True,
|
||||
intra_op_num_threads=2,
|
||||
)
|
||||
return _model
|
||||
|
||||
|
||||
def clean_text(text):
|
||||
if isinstance(text, (list, tuple)):
|
||||
text = text[0] if text else ""
|
||||
if not text:
|
||||
return ""
|
||||
markers = ["<|zh|>", "<|en|>", "<|yue|>", "<|ja|>", "<|ko|>", "<|nospeech|>", "<|withitn|>", "<|woitn|>"]
|
||||
for marker in markers:
|
||||
text = text.replace(marker, "")
|
||||
return text.strip()
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
def health():
|
||||
return {
|
||||
"status": "ok",
|
||||
"engine": "funasr-onnx",
|
||||
"model": MODEL_NAME,
|
||||
"device": DEVICE,
|
||||
"loaded": _model is not None,
|
||||
}
|
||||
|
||||
|
||||
@app.post("/transcribe")
|
||||
async def transcribe(file: UploadFile = File(...)):
|
||||
started = time.time()
|
||||
suffix = Path(file.filename or "audio.wav").suffix or ".wav"
|
||||
tmp_path = None
|
||||
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(delete=False, suffix=suffix, dir=WORK_DIR) as tmp:
|
||||
tmp_path = Path(tmp.name)
|
||||
while True:
|
||||
chunk = await file.read(1024 * 1024)
|
||||
if not chunk:
|
||||
break
|
||||
tmp.write(chunk)
|
||||
|
||||
model = get_model()
|
||||
result = model([str(tmp_path)])
|
||||
first = result[0] if isinstance(result, list) and result else result
|
||||
text = clean_text(first.get("preds", first.get("text", "")) if isinstance(first, dict) else str(first or ""))
|
||||
language = first.get("language") if isinstance(first, dict) else None
|
||||
|
||||
return {
|
||||
"success": bool(text),
|
||||
"text": text,
|
||||
"language": language,
|
||||
"durationMs": int((time.time() - started) * 1000),
|
||||
"engine": "funasr-onnx",
|
||||
"model": MODEL_NAME,
|
||||
"errorMessage": None if text else "empty recognition result",
|
||||
}
|
||||
except Exception as exc:
|
||||
return {
|
||||
"success": False,
|
||||
"text": "",
|
||||
"language": None,
|
||||
"durationMs": int((time.time() - started) * 1000),
|
||||
"engine": "funasr-onnx",
|
||||
"model": MODEL_NAME,
|
||||
"errorMessage": str(exc),
|
||||
}
|
||||
finally:
|
||||
if tmp_path:
|
||||
try:
|
||||
tmp_path.unlink(missing_ok=True)
|
||||
except Exception:
|
||||
pass
|
||||
@@ -0,0 +1,16 @@
|
||||
[Unit]
|
||||
Description=Emotion Museum ASR Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=/data/programs/emotion-museum/asr-service
|
||||
Environment=ASR_MODEL=/data/programs/emotion-museum/asr-service/models/paraformer-zh-onnx
|
||||
Environment=ASR_DEVICE=cpu
|
||||
Environment=ASR_WORK_DIR=/tmp/emotion-museum-asr
|
||||
ExecStart=/data/programs/emotion-museum/asr-service/.venv/bin/uvicorn app:app --host 127.0.0.1 --port 19120
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@@ -0,0 +1,8 @@
|
||||
fastapi==0.111.0
|
||||
uvicorn[standard]==0.30.1
|
||||
pydantic==2.7.4
|
||||
python-multipart==0.0.20
|
||||
funasr-onnx==0.4.1
|
||||
modelscope==1.22.3
|
||||
onnxruntime==1.26.0
|
||||
soundfile==0.13.1
|
||||
@@ -0,0 +1,34 @@
|
||||
CREATE TABLE IF NOT EXISTS emotion_museum.api_endpoint (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
path VARCHAR(500) NOT NULL,
|
||||
method VARCHAR(10) NOT NULL,
|
||||
operation_id VARCHAR(200),
|
||||
summary VARCHAR(500),
|
||||
description TEXT,
|
||||
tags VARCHAR(500),
|
||||
deprecated TINYINT(1) DEFAULT 0,
|
||||
request_schema JSON,
|
||||
response_schema JSON,
|
||||
create_by VARCHAR(64),
|
||||
create_time DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
update_by VARCHAR(64),
|
||||
update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
is_deleted TINYINT(1) DEFAULT 0,
|
||||
remarks VARCHAR(500),
|
||||
UNIQUE INDEX idx_operation_id (operation_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
|
||||
CREATE TABLE IF NOT EXISTS emotion_museum.api_param (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
endpoint_id VARCHAR(64) NOT NULL,
|
||||
param_type VARCHAR(20) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
required TINYINT(1) DEFAULT 0,
|
||||
param_type_def VARCHAR(50),
|
||||
description VARCHAR(500),
|
||||
default_value VARCHAR(200),
|
||||
enum_values JSON,
|
||||
example VARCHAR(500),
|
||||
INDEX idx_endpoint (endpoint_id),
|
||||
FOREIGN KEY (endpoint_id) REFERENCES emotion_museum.api_endpoint(id) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
|
||||
Executable
+201
@@ -0,0 +1,201 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 情绪博物馆后端服务远程部署脚本
|
||||
# 在远程服务器上执行的服务部署脚本
|
||||
|
||||
set -e
|
||||
|
||||
# 配置变量
|
||||
APP_NAME="emotion-museum-single"
|
||||
JAR_NAME="emotion-single-1.0.0.jar"
|
||||
DEPLOY_DIR="/data/programs/emotion-museum"
|
||||
LOG_DIR="/data/logs/emotion-museum"
|
||||
JAR_PATH="$DEPLOY_DIR/$JAR_NAME"
|
||||
PID_FILE="/tmp/${APP_NAME}.pid"
|
||||
JAVA_OPTS="-Xms512m -Xmx1024m -XX:+UseG1GC -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=${LOG_DIR}/heapdump.hprof"
|
||||
|
||||
# 获取 Spring Profile 参数
|
||||
SPRING_PROFILE=${1:-test}
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# 日志函数
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 检查 jar 文件是否存在
|
||||
check_jar() {
|
||||
if [ ! -f "$JAR_PATH" ]; then
|
||||
log_error "JAR 文件不存在: $JAR_PATH"
|
||||
exit 1
|
||||
fi
|
||||
log_info "JAR 文件检查通过: $JAR_PATH"
|
||||
}
|
||||
|
||||
# 停止旧服务
|
||||
stop_service() {
|
||||
# 1. 尝试通过 PID 文件停止
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
log_info "停止旧服务 (PID: $PID)"
|
||||
kill "$PID"
|
||||
|
||||
# 等待服务停止
|
||||
for i in {1..30}; do
|
||||
if ! ps -p "$PID" > /dev/null 2>&1; then
|
||||
log_info "服务已停止"
|
||||
break
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
# 强制停止
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
log_warn "强制停止服务 (PID: $PID)"
|
||||
kill -9 "$PID"
|
||||
fi
|
||||
else
|
||||
log_warn "PID 文件存在但进程不存在,清理 PID 文件"
|
||||
fi
|
||||
rm -f "$PID_FILE"
|
||||
fi
|
||||
|
||||
# 2. 双重检查:通过进程名查找并停止(防止 PID 文件丢失的情况)
|
||||
PIDS=$(ps aux | grep "$JAR_NAME" | grep -v grep | awk '{print $2}')
|
||||
if [ -n "$PIDS" ]; then
|
||||
log_warn "检测到残留进程 (PIDs: $PIDS),正在清理..."
|
||||
for PID in $PIDS; do
|
||||
log_info "停止残留进程 $PID"
|
||||
kill "$PID" 2>/dev/null || true
|
||||
done
|
||||
|
||||
sleep 5
|
||||
|
||||
# 再次检查并强制停止
|
||||
PIDS=$(ps aux | grep "$JAR_NAME" | grep -v grep | awk '{print $2}')
|
||||
if [ -n "$PIDS" ]; then
|
||||
for PID in $PIDS; do
|
||||
log_warn "强制停止残留进程 $PID"
|
||||
kill -9 "$PID" 2>/dev/null || true
|
||||
done
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
# 启动新服务
|
||||
start_service() {
|
||||
log_info "启动新服务..."
|
||||
|
||||
# 检查JAR文件
|
||||
check_jar
|
||||
|
||||
# 启动命令
|
||||
nohup java $JAVA_OPTS \
|
||||
-Dspring.profiles.active=$SPRING_PROFILE \
|
||||
-Dlogging.file.path=$LOG_DIR \
|
||||
-Dlogging.file.name=$LOG_DIR/emotion-single.log \
|
||||
-jar "$JAR_PATH" \
|
||||
> "$LOG_DIR/startup.log" 2>&1 &
|
||||
|
||||
# 保存 PID
|
||||
echo $! > "$PID_FILE"
|
||||
|
||||
log_info "服务启动中,PID: $(cat $PID_FILE)"
|
||||
log_info "启动日志: $LOG_DIR/startup.log"
|
||||
log_info "应用日志: $LOG_DIR/emotion-single.log"
|
||||
}
|
||||
|
||||
# 检查服务状态
|
||||
check_status() {
|
||||
if [ -f "$PID_FILE" ]; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
if ps -p "$PID" > /dev/null 2>&1; then
|
||||
log_info "服务运行中 (PID: $PID)"
|
||||
return 0
|
||||
else
|
||||
log_error "PID 文件存在但进程不存在"
|
||||
return 1
|
||||
fi
|
||||
else
|
||||
log_error "PID 文件不存在,服务未运行"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 等待服务启动
|
||||
wait_for_startup() {
|
||||
log_info "等待服务启动..."
|
||||
for i in {1..60}; do
|
||||
if check_status > /dev/null 2>&1; then
|
||||
# 检查端口是否监听(使用 19089 端口)
|
||||
if netstat -tlnp 2>/dev/null | grep -q ":19089.*LISTEN"; then
|
||||
log_info "服务启动成功!"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
sleep 2
|
||||
done
|
||||
|
||||
log_error "服务启动超时,请检查日志: $LOG_DIR/startup.log"
|
||||
return 1
|
||||
}
|
||||
|
||||
# 显示服务信息
|
||||
show_info() {
|
||||
log_info "=== 服务信息 ==="
|
||||
log_info "应用名称: $APP_NAME"
|
||||
log_info "JAR 文件: $JAR_PATH"
|
||||
log_info "部署目录: $DEPLOY_DIR"
|
||||
log_info "日志目录: $LOG_DIR"
|
||||
log_info "PID 文件: $PID_FILE"
|
||||
log_info "Java 参数: $JAVA_OPTS"
|
||||
log_info "Spring Profile: $SPRING_PROFILE"
|
||||
|
||||
if check_status > /dev/null 2>&1; then
|
||||
PID=$(cat "$PID_FILE")
|
||||
log_info "服务状态: 运行中 (PID: $PID)"
|
||||
else
|
||||
log_info "服务状态: 未运行"
|
||||
fi
|
||||
}
|
||||
|
||||
# 主函数
|
||||
main() {
|
||||
log_info "开始在服务器上部署 $APP_NAME 服务..."
|
||||
|
||||
# 创建日志目录
|
||||
log_info "创建日志目录: $LOG_DIR"
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
# 停止旧服务
|
||||
stop_service
|
||||
|
||||
# 启动新服务
|
||||
start_service
|
||||
|
||||
# 等待启动
|
||||
if wait_for_startup; then
|
||||
log_info "服务器部署成功!"
|
||||
show_info
|
||||
else
|
||||
log_error "服务器部署失败!"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
# 执行主函数
|
||||
main
|
||||
Executable
+248
@@ -0,0 +1,248 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
情绪博物馆后端服务部署脚本
|
||||
部署到远程服务器 101.200.208.45
|
||||
使用系统自带的ssh/scp命令,无需额外依赖
|
||||
"""
|
||||
|
||||
import io
|
||||
import os
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
# 强制 stdout/stderr 使用 UTF-8 编码,避免 Windows GBK 编码错误
|
||||
if hasattr(sys.stdout, 'buffer'):
|
||||
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace')
|
||||
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace')
|
||||
|
||||
# 配置变量
|
||||
APP_NAME = "emotion-museum-single"
|
||||
JAR_NAME = "backend-single-1.0.0.jar"
|
||||
REMOTE_JAR_NAME = "emotion-single-1.0.0.jar"
|
||||
SPRING_PROFILE = "test"
|
||||
|
||||
# 本地路径
|
||||
SCRIPT_DIR = Path(__file__).parent.absolute()
|
||||
JAR_PATH = SCRIPT_DIR / "target" / JAR_NAME
|
||||
|
||||
# 远程服务器配置
|
||||
REMOTE_HOST = "101.200.208.45"
|
||||
REMOTE_USER = "root"
|
||||
REMOTE_DIR = "/data/programs/emotion-museum"
|
||||
REMOTE_LOG_DIR = "/data/logs/emotion-museum"
|
||||
|
||||
|
||||
class Colors:
|
||||
"""终端颜色"""
|
||||
GREEN = '\033[32m'
|
||||
RED = '\033[31m'
|
||||
YELLOW = '\033[33m'
|
||||
RESET = '\033[0m'
|
||||
|
||||
|
||||
def log_info(msg):
|
||||
"""打印信息日志"""
|
||||
print(f"{Colors.GREEN}[INFO]{Colors.RESET} {msg}")
|
||||
|
||||
|
||||
def log_error(msg):
|
||||
"""打印错误日志"""
|
||||
print(f"{Colors.RED}[ERROR]{Colors.RESET} {msg}")
|
||||
|
||||
|
||||
def log_warn(msg):
|
||||
"""打印警告日志"""
|
||||
print(f"{Colors.YELLOW}[WARN]{Colors.RESET} {msg}")
|
||||
|
||||
|
||||
def run_command(cmd, cwd=None, shell=True, capture=True):
|
||||
"""执行本地命令"""
|
||||
try:
|
||||
if capture:
|
||||
result = subprocess.run(cmd, cwd=cwd, shell=shell, capture_output=True, text=True, encoding='utf-8')
|
||||
return result.returncode == 0, result.stdout, result.stderr
|
||||
else:
|
||||
result = subprocess.run(cmd, cwd=cwd, shell=shell)
|
||||
return result.returncode == 0, "", ""
|
||||
except Exception as e:
|
||||
return False, "", str(e)
|
||||
|
||||
|
||||
def exec_ssh_cmd(cmd):
|
||||
"""通过SSH执行远程命令"""
|
||||
ssh_cmd = f'ssh {REMOTE_USER}@{REMOTE_HOST} "{cmd}"'
|
||||
return run_command(ssh_cmd)
|
||||
|
||||
|
||||
def scp_upload(local_path, remote_path):
|
||||
"""通过SCP上传文件"""
|
||||
scp_cmd = f'scp "{local_path}" {REMOTE_USER}@{REMOTE_HOST}:{remote_path}'
|
||||
success, stdout, stderr = run_command(scp_cmd)
|
||||
if not success:
|
||||
log_error(f"SCP上传失败: {stderr}")
|
||||
return success
|
||||
|
||||
|
||||
def build_project():
|
||||
"""构建项目"""
|
||||
log_info("开始构建项目...")
|
||||
|
||||
success, _, _ = run_command("mvn --version")
|
||||
if not success:
|
||||
log_error("未找到Maven命令,请确保已安装Maven")
|
||||
sys.exit(1)
|
||||
|
||||
log_info("执行: mvn clean package -DskipTests")
|
||||
os.chdir(SCRIPT_DIR)
|
||||
|
||||
success, _, _ = run_command("mvn clean package -DskipTests", capture=False)
|
||||
if not success:
|
||||
log_error("项目构建失败")
|
||||
sys.exit(1)
|
||||
|
||||
if not JAR_PATH.exists():
|
||||
log_error(f"项目构建失败,未找到JAR文件: {JAR_PATH}")
|
||||
sys.exit(1)
|
||||
|
||||
file_size = JAR_PATH.stat().st_size / (1024 * 1024)
|
||||
log_info(f"[OK] 项目构建成功: {JAR_PATH}")
|
||||
log_info(f"文件大小: {file_size:.2f} MB")
|
||||
|
||||
|
||||
def check_jar():
|
||||
"""检查JAR文件是否存在"""
|
||||
if not JAR_PATH.exists():
|
||||
log_error(f"JAR文件不存在: {JAR_PATH}")
|
||||
log_info("请先执行打包命令: mvn clean package")
|
||||
sys.exit(1)
|
||||
log_info(f"JAR文件检查通过: {JAR_PATH}")
|
||||
|
||||
|
||||
def deploy(upload_script=None):
|
||||
"""
|
||||
部署到远程服务器
|
||||
|
||||
Args:
|
||||
upload_script: 可选,指定要上传的额外文件路径(如 deploy-server.sh)
|
||||
"""
|
||||
log_info(f"开始部署到 {REMOTE_HOST}...")
|
||||
|
||||
build_project()
|
||||
check_jar()
|
||||
|
||||
# 创建远程目录
|
||||
log_info("创建远程目录...")
|
||||
exec_ssh_cmd(f"mkdir -p {REMOTE_DIR}")
|
||||
exec_ssh_cmd(f"mkdir -p {REMOTE_LOG_DIR}")
|
||||
|
||||
# 上传JAR文件
|
||||
log_info("上传JAR文件到远程服务器...")
|
||||
log_info(f"本地文件: {JAR_PATH}")
|
||||
log_info(f"远程路径: {REMOTE_USER}@{REMOTE_HOST}:{REMOTE_DIR}/{REMOTE_JAR_NAME}")
|
||||
|
||||
if not scp_upload(JAR_PATH, f"{REMOTE_DIR}/{REMOTE_JAR_NAME}"):
|
||||
log_error("上传JAR文件失败")
|
||||
sys.exit(1)
|
||||
log_info("[OK] JAR文件上传成功")
|
||||
|
||||
# 验证远程文件
|
||||
log_info("验证远程文件...")
|
||||
success, output, _ = exec_ssh_cmd(f"ls -lh {REMOTE_DIR}/{REMOTE_JAR_NAME}")
|
||||
if not success:
|
||||
log_error("远程文件验证失败")
|
||||
sys.exit(1)
|
||||
log_info(output)
|
||||
|
||||
# 上传额外文件
|
||||
if upload_script:
|
||||
script_path = SCRIPT_DIR / upload_script
|
||||
if script_path.exists():
|
||||
log_info(f"上传文件到远程服务器: {upload_script}")
|
||||
if not scp_upload(script_path, f"{REMOTE_DIR}/{upload_script}"):
|
||||
log_error(f"上传文件失败: {upload_script}")
|
||||
sys.exit(1)
|
||||
log_info(f"[OK] 文件上传成功: {upload_script}")
|
||||
if upload_script.endswith('.sh'):
|
||||
exec_ssh_cmd(f"chmod +x {REMOTE_DIR}/{upload_script}")
|
||||
else:
|
||||
log_error(f"指定的文件不存在: {script_path}")
|
||||
sys.exit(1)
|
||||
|
||||
# 执行远程部署
|
||||
log_info("在远程服务器上执行部署...")
|
||||
success, output, error = exec_ssh_cmd(f"cd {REMOTE_DIR} && ./deploy-server.sh {SPRING_PROFILE}")
|
||||
if output:
|
||||
print(output)
|
||||
if error:
|
||||
print(error)
|
||||
|
||||
if not success:
|
||||
log_error("远程部署脚本执行失败")
|
||||
sys.exit(1)
|
||||
|
||||
log_info("[OK] 部署完成!")
|
||||
show_status()
|
||||
|
||||
|
||||
def show_status():
|
||||
"""显示远程服务状态"""
|
||||
log_info("=== 服务信息 ===")
|
||||
log_info(f"服务器地址: {REMOTE_HOST}")
|
||||
log_info(f"部署目录: {REMOTE_DIR}")
|
||||
log_info(f"日志目录: {REMOTE_LOG_DIR}")
|
||||
log_info(f"Spring Profile: {SPRING_PROFILE}")
|
||||
|
||||
log_info("检查服务状态...")
|
||||
success, output, _ = exec_ssh_cmd(f"ps aux | grep {REMOTE_JAR_NAME} | grep -v grep")
|
||||
if output:
|
||||
log_info(f"服务运行中:\n{output}")
|
||||
else:
|
||||
log_info("服务未运行")
|
||||
|
||||
|
||||
def print_usage():
|
||||
"""打印使用说明"""
|
||||
print("""
|
||||
用法: python deploy.py [命令] [参数]
|
||||
|
||||
命令:
|
||||
deploy - 部署到远程服务器(默认)
|
||||
deploy [文件名] - 部署并上传指定文件(如 deploy-server.sh)
|
||||
build - 仅构建项目
|
||||
status - 查看远程服务状态
|
||||
|
||||
示例:
|
||||
python deploy.py # 部署到远程服务器
|
||||
python deploy.py deploy-server.sh # 同时上传部署脚本
|
||||
python deploy.py build # 仅构建项目
|
||||
python deploy.py status # 查看服务状态
|
||||
""")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
if len(sys.argv) < 2:
|
||||
deploy()
|
||||
return
|
||||
|
||||
command = sys.argv[1]
|
||||
|
||||
if command == "build":
|
||||
build_project()
|
||||
elif command == "status":
|
||||
show_status()
|
||||
elif command == "help" or command == "-h" or command == "--help":
|
||||
print_usage()
|
||||
elif command.endswith('.sh'):
|
||||
# 如果第一个参数是文件名,则上传该文件
|
||||
deploy(command)
|
||||
else:
|
||||
# 其他情况视为部署命令
|
||||
upload_script = sys.argv[2] if len(sys.argv) > 2 else None
|
||||
deploy(upload_script)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Executable
+180
@@ -0,0 +1,180 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 情绪博物馆后端服务部署脚本
|
||||
# 部署到远程服务器 101.200.208.45
|
||||
|
||||
set -e
|
||||
|
||||
# 配置变量
|
||||
APP_NAME="emotion-museum-single"
|
||||
JAR_NAME="backend-single-1.0.0.jar"
|
||||
JAR_PATH="./target/${JAR_NAME}"
|
||||
|
||||
# 远程服务器配置
|
||||
REMOTE_HOST="101.200.208.45"
|
||||
REMOTE_USER="root"
|
||||
REMOTE_DIR="/data/programs/emotion-museum"
|
||||
REMOTE_LOG_DIR="/data/logs/emotion-museum"
|
||||
REMOTE_JAR_NAME="emotion-single-1.0.0.jar"
|
||||
SPRING_PROFILE="test"
|
||||
|
||||
# 颜色输出
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
log_info() {
|
||||
echo -e "${GREEN}[INFO]${NC} $1"
|
||||
}
|
||||
|
||||
log_warn() {
|
||||
echo -e "${YELLOW}[WARN]${NC} $1"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
echo -e "${RED}[ERROR]${NC} $1"
|
||||
}
|
||||
|
||||
# 构建项目
|
||||
build_project() {
|
||||
log_info "开始构建项目..."
|
||||
|
||||
if ! command -v mvn > /dev/null 2>&1; then
|
||||
log_error "未找到Maven命令,请确保已安装Maven"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "执行: mvn clean package -DskipTests"
|
||||
if ! mvn clean package -DskipTests; then
|
||||
log_error "项目构建失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ ! -f "$JAR_PATH" ]; then
|
||||
log_error "项目构建失败,未找到JAR文件: $JAR_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "✅ 项目构建成功: $JAR_PATH"
|
||||
log_info "文件大小: $(ls -lh $JAR_PATH | awk '{print $5}')"
|
||||
}
|
||||
|
||||
# 检查JAR文件
|
||||
check_jar() {
|
||||
if [ ! -f "$JAR_PATH" ]; then
|
||||
log_error "JAR文件不存在: $JAR_PATH"
|
||||
log_info "请先执行打包命令: mvn clean package"
|
||||
exit 1
|
||||
fi
|
||||
log_info "JAR文件检查通过: $JAR_PATH"
|
||||
}
|
||||
|
||||
# 显示服务状态
|
||||
show_status() {
|
||||
log_info "=== 服务信息 ==="
|
||||
log_info "服务器地址: $REMOTE_HOST"
|
||||
log_info "部署目录: $REMOTE_DIR"
|
||||
log_info "日志目录: $REMOTE_LOG_DIR"
|
||||
log_info "Spring Profile: $SPRING_PROFILE"
|
||||
|
||||
log_info "检查服务状态..."
|
||||
ssh $REMOTE_USER@$REMOTE_HOST "ps aux | grep $REMOTE_JAR_NAME | grep -v grep" || log_info "服务未运行"
|
||||
}
|
||||
|
||||
# 部署到远程服务器
|
||||
# 参数: $1 - 可选,指定要上传的额外文件
|
||||
deploy() {
|
||||
UPLOAD_SCRIPT="$1"
|
||||
|
||||
log_info "开始部署到 $REMOTE_HOST..."
|
||||
|
||||
build_project
|
||||
check_jar
|
||||
|
||||
# 创建远程目录
|
||||
log_info "创建远程目录..."
|
||||
ssh $REMOTE_USER@$REMOTE_HOST "mkdir -p $REMOTE_DIR" || { log_error "创建远程目录失败"; exit 1; }
|
||||
ssh $REMOTE_USER@$REMOTE_HOST "mkdir -p $REMOTE_LOG_DIR" || { log_error "创建远程日志目录失败"; exit 1; }
|
||||
|
||||
# 上传JAR文件
|
||||
log_info "上传JAR文件到远程服务器..."
|
||||
log_info "本地文件: $JAR_PATH"
|
||||
log_info "远程路径: $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/$REMOTE_JAR_NAME"
|
||||
|
||||
if ! scp "$JAR_PATH" $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/$REMOTE_JAR_NAME; then
|
||||
log_error "上传JAR文件失败"
|
||||
exit 1
|
||||
fi
|
||||
log_info "✅ JAR文件上传成功"
|
||||
|
||||
# 验证远程文件
|
||||
log_info "验证远程文件..."
|
||||
ssh $REMOTE_USER@$REMOTE_HOST "ls -lh $REMOTE_DIR/$REMOTE_JAR_NAME" || { log_error "远程文件验证失败"; exit 1; }
|
||||
|
||||
# 上传额外文件
|
||||
if [ -n "$UPLOAD_SCRIPT" ] && [ -f "$UPLOAD_SCRIPT" ]; then
|
||||
log_info "上传文件到远程服务器: $UPLOAD_SCRIPT"
|
||||
if ! scp "$UPLOAD_SCRIPT" $REMOTE_USER@$REMOTE_HOST:$REMOTE_DIR/; then
|
||||
log_error "上传文件失败: $UPLOAD_SCRIPT"
|
||||
exit 1
|
||||
fi
|
||||
log_info "✅ 文件上传成功: $UPLOAD_SCRIPT"
|
||||
|
||||
REMOTE_FILENAME=$(basename "$UPLOAD_SCRIPT")
|
||||
if [[ "$REMOTE_FILENAME" == *.sh ]]; then
|
||||
ssh $REMOTE_USER@$REMOTE_HOST "chmod +x $REMOTE_DIR/$REMOTE_FILENAME"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 执行远程部署
|
||||
log_info "在远程服务器上执行部署..."
|
||||
if ! ssh $REMOTE_USER@$REMOTE_HOST "cd $REMOTE_DIR && ./deploy-server.sh $SPRING_PROFILE"; then
|
||||
log_error "远程部署脚本执行失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log_info "✅ 部署完成!"
|
||||
show_status
|
||||
}
|
||||
|
||||
# 打印使用说明
|
||||
print_usage() {
|
||||
echo "用法: $0 [命令] [参数]"
|
||||
echo ""
|
||||
echo "命令:"
|
||||
echo " (无参数) - 部署到远程服务器(默认)"
|
||||
echo " [文件名] - 部署并上传指定文件(如 deploy-server.sh)"
|
||||
echo " build - 仅构建项目"
|
||||
echo " status - 查看远程服务状态"
|
||||
echo ""
|
||||
echo "示例:"
|
||||
echo " $0 # 部署到远程服务器"
|
||||
echo " $0 deploy-server.sh # 同时上传部署脚本"
|
||||
echo " $0 build # 仅构建项目"
|
||||
echo " $0 status # 查看服务状态"
|
||||
}
|
||||
|
||||
# 主逻辑
|
||||
case "${1:-}" in
|
||||
"")
|
||||
deploy
|
||||
;;
|
||||
"build")
|
||||
build_project
|
||||
;;
|
||||
"status")
|
||||
show_status
|
||||
;;
|
||||
"help"|"-h"|"--help")
|
||||
print_usage
|
||||
;;
|
||||
*)
|
||||
# 如果参数是文件,则上传该文件
|
||||
if [ -f "$1" ]; then
|
||||
deploy "$1"
|
||||
else
|
||||
deploy "$1"
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,223 @@
|
||||
# 管理员登录认证系统设计文档
|
||||
|
||||
## 概述
|
||||
|
||||
本系统实现了普通用户和管理员用户的双重登录认证机制,两者相互独立,互不影响。
|
||||
|
||||
## 系统架构
|
||||
|
||||
### 用户类型区分
|
||||
|
||||
系统通过JWT Token中的`userType`字段区分用户类型:
|
||||
- `user`: 普通用户
|
||||
- `admin`: 管理员用户
|
||||
|
||||
### 核心组件
|
||||
|
||||
1. **JwtUtil** - JWT工具类
|
||||
- 支持生成带用户类型的Token
|
||||
- 支持从Token中提取用户类型
|
||||
|
||||
2. **AdminAuthService** - 管理员认证服务
|
||||
- 管理员登录(账号+密码)
|
||||
- Token生成和验证
|
||||
- 登录信息记录
|
||||
|
||||
3. **AdminAuthInterceptor** - 管理员拦截器
|
||||
- 拦截所有`/admin/**`路径
|
||||
- 验证Token的有效性
|
||||
- 验证用户类型为admin
|
||||
|
||||
4. **JwtAuthInterceptor** - 普通用户拦截器
|
||||
- 拦截除管理员路径外的所有请求
|
||||
- 验证普通用户Token
|
||||
|
||||
## API接口
|
||||
|
||||
### 管理员登录
|
||||
```
|
||||
POST /api/admin/auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"account": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
|
||||
响应:
|
||||
{
|
||||
"code": 200,
|
||||
"message": "登录成功",
|
||||
"data": {
|
||||
"accessToken": "eyJhbGc...",
|
||||
"refreshToken": "eyJhbGc...",
|
||||
"expiresIn": 86400,
|
||||
"adminInfo": {
|
||||
"id": "xxx",
|
||||
"account": "admin",
|
||||
"username": "系统管理员",
|
||||
"role": "super_admin",
|
||||
...
|
||||
},
|
||||
"loginTime": "2025-10-27 10:00:00"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 获取管理员信息
|
||||
```
|
||||
GET /api/admin/auth/info
|
||||
Authorization: Bearer {accessToken}
|
||||
|
||||
响应:
|
||||
{
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"id": "xxx",
|
||||
"account": "admin",
|
||||
"username": "系统管理员",
|
||||
"role": "super_admin",
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 管理员登出
|
||||
```
|
||||
POST /api/admin/auth/logout
|
||||
Authorization: Bearer {accessToken}
|
||||
|
||||
响应:
|
||||
{
|
||||
"code": 200,
|
||||
"message": "登出成功",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 刷新Token
|
||||
```
|
||||
POST /api/admin/auth/refreshToken
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"refreshToken": "eyJhbGc..."
|
||||
}
|
||||
|
||||
响应:
|
||||
{
|
||||
"code": 200,
|
||||
"message": "令牌刷新成功",
|
||||
"data": {
|
||||
"accessToken": "eyJhbGc...",
|
||||
"refreshToken": "eyJhbGc...",
|
||||
"expiresIn": 86400,
|
||||
...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 默认管理员账号
|
||||
|
||||
系统初始化时会创建一个默认的超级管理员账号:
|
||||
|
||||
- **账号**: admin
|
||||
- **密码**: admin123
|
||||
- **角色**: super_admin
|
||||
- **邮箱**: admin@emotion-museum.com
|
||||
- **手机**: 13800138000
|
||||
|
||||
**重要提示**: 生产环境部署后请立即修改默认密码!
|
||||
|
||||
## 拦截器配置
|
||||
|
||||
### 管理员拦截器
|
||||
- **拦截路径**: `/admin/**`
|
||||
- **排除路径**:
|
||||
- `/admin/auth/login` - 登录接口
|
||||
- `/admin/auth/refreshToken` - 刷新Token接口
|
||||
- **优先级**: 1(最高)
|
||||
|
||||
### 普通用户拦截器
|
||||
- **拦截路径**: `/**`
|
||||
- **排除路径**:
|
||||
- `/auth/**` - 用户认证相关
|
||||
- `/admin/**` - 管理员路径(由管理员拦截器处理)
|
||||
- `/health` - 健康检查
|
||||
- `/ws/**` - WebSocket
|
||||
- `/swagger-ui/**` - API文档
|
||||
- **优先级**: 2
|
||||
|
||||
## Token存储
|
||||
|
||||
### Redis存储
|
||||
- **管理员AccessToken**: `admin_token:{adminId}` (24小时)
|
||||
- **管理员RefreshToken**: `admin_refresh_token:{adminId}` (7天)
|
||||
- **普通用户Token**: `token:{userId}` (24小时)
|
||||
- **普通用户RefreshToken**: `refresh_token:{userId}` (7天)
|
||||
|
||||
## 安全特性
|
||||
|
||||
1. **密码加密**: 使用BCrypt算法加密存储
|
||||
2. **Token隔离**: 管理员和普通用户Token完全隔离
|
||||
3. **权限验证**: 管理员接口强制验证userType
|
||||
4. **登录记录**: 记录最后登录时间、IP和登录次数
|
||||
5. **状态检查**: 登录时检查账号状态
|
||||
|
||||
## 使用示例
|
||||
|
||||
### 前端调用示例
|
||||
|
||||
```javascript
|
||||
// 管理员登录
|
||||
async function adminLogin(account, password) {
|
||||
const response = await fetch('/api/admin/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({ account, password })
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
if (result.code === 200) {
|
||||
// 保存Token
|
||||
localStorage.setItem('adminToken', result.data.accessToken);
|
||||
localStorage.setItem('adminRefreshToken', result.data.refreshToken);
|
||||
return result.data;
|
||||
}
|
||||
throw new Error(result.message);
|
||||
}
|
||||
|
||||
// 调用管理员接口
|
||||
async function callAdminApi(url, options = {}) {
|
||||
const token = localStorage.getItem('adminToken');
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
headers: {
|
||||
...options.headers,
|
||||
'Authorization': `Bearer ${token}`
|
||||
}
|
||||
});
|
||||
|
||||
return await response.json();
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **不要混用Token**: 管理员Token只能访问管理员接口,普通用户Token只能访问用户接口
|
||||
2. **及时刷新Token**: Token过期前使用refreshToken刷新
|
||||
3. **安全存储**: 前端应安全存储Token,避免XSS攻击
|
||||
4. **HTTPS**: 生产环境必须使用HTTPS传输
|
||||
5. **修改默认密码**: 部署后立即修改默认管理员密码
|
||||
|
||||
## 扩展功能
|
||||
|
||||
后续可以扩展的功能:
|
||||
- 管理员角色权限细分
|
||||
- 操作日志记录
|
||||
- 多因素认证(MFA)
|
||||
- IP白名单限制
|
||||
- 登录失败次数限制
|
||||
@@ -0,0 +1,262 @@
|
||||
# 管理员登录功能测试指南
|
||||
|
||||
## 测试前准备
|
||||
|
||||
### 1. 执行数据库脚本
|
||||
确保已执行 `sql/emotion_museum.sql` 脚本,该脚本会:
|
||||
- 创建 `t_admin` 表
|
||||
- 初始化默认管理员账号
|
||||
|
||||
### 2. 启动应用
|
||||
```bash
|
||||
cd backend-single
|
||||
mvn spring-boot:run
|
||||
```
|
||||
|
||||
## 测试用例
|
||||
|
||||
### 测试1: 管理员登录
|
||||
|
||||
**请求**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/admin/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"account": "admin",
|
||||
"password": "admin123"
|
||||
}'
|
||||
```
|
||||
|
||||
**预期响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "登录成功",
|
||||
"data": {
|
||||
"accessToken": "eyJhbGciOiJIUzUxMiJ9...",
|
||||
"refreshToken": "eyJhbGciOiJIUzUxMiJ9...",
|
||||
"expiresIn": 86400,
|
||||
"adminInfo": {
|
||||
"id": "xxx",
|
||||
"account": "admin",
|
||||
"username": "系统管理员",
|
||||
"email": "admin@emotion-museum.com",
|
||||
"phone": "13800138000",
|
||||
"role": "super_admin",
|
||||
"status": 1
|
||||
},
|
||||
"loginTime": "2025-10-27 10:00:00"
|
||||
},
|
||||
"timestamp": 1698393600000
|
||||
}
|
||||
```
|
||||
|
||||
### 测试2: 错误的密码
|
||||
|
||||
**请求**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/admin/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"account": "admin",
|
||||
"password": "wrongpassword"
|
||||
}'
|
||||
```
|
||||
|
||||
**预期响应**:
|
||||
```json
|
||||
{
|
||||
"code": 500,
|
||||
"message": "账号或密码错误",
|
||||
"data": null,
|
||||
"timestamp": 1698393600000
|
||||
}
|
||||
```
|
||||
|
||||
### 测试3: 获取管理员信息
|
||||
|
||||
**请求**:
|
||||
```bash
|
||||
curl -X GET http://localhost:8080/api/admin/auth/info \
|
||||
-H "Authorization: Bearer {从登录接口获取的accessToken}"
|
||||
```
|
||||
|
||||
**预期响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"id": "xxx",
|
||||
"account": "admin",
|
||||
"username": "系统管理员",
|
||||
"email": "admin@emotion-museum.com",
|
||||
"role": "super_admin",
|
||||
"status": 1
|
||||
},
|
||||
"timestamp": 1698393600000
|
||||
}
|
||||
```
|
||||
|
||||
### 测试4: 访问管理员接口(分页查询管理员)
|
||||
|
||||
**请求**:
|
||||
```bash
|
||||
curl -X GET "http://localhost:8080/api/admin/page?current=1&size=10" \
|
||||
-H "Authorization: Bearer {accessToken}"
|
||||
```
|
||||
|
||||
**预期响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "操作成功",
|
||||
"data": {
|
||||
"records": [
|
||||
{
|
||||
"id": "xxx",
|
||||
"account": "admin",
|
||||
"username": "系统管理员",
|
||||
"role": "super_admin",
|
||||
...
|
||||
}
|
||||
],
|
||||
"total": 1,
|
||||
"current": 1,
|
||||
"size": 10,
|
||||
"pages": 1
|
||||
},
|
||||
"timestamp": 1698393600000
|
||||
}
|
||||
```
|
||||
|
||||
### 测试5: 普通用户Token访问管理员接口(应该被拒绝)
|
||||
|
||||
**请求**:
|
||||
```bash
|
||||
# 先用普通用户登录获取Token
|
||||
curl -X POST http://localhost:8080/api/auth/login \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"phone": "13800138001",
|
||||
"smsCode": "123456"
|
||||
}'
|
||||
|
||||
# 使用普通用户Token访问管理员接口
|
||||
curl -X GET http://localhost:8080/api/admin/page \
|
||||
-H "Authorization: Bearer {普通用户的accessToken}"
|
||||
```
|
||||
|
||||
**预期响应**:
|
||||
```json
|
||||
{
|
||||
"code": 403,
|
||||
"message": "无权限访问",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
### 测试6: 管理员登出
|
||||
|
||||
**请求**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/admin/auth/logout \
|
||||
-H "Authorization: Bearer {accessToken}"
|
||||
```
|
||||
|
||||
**预期响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "登出成功",
|
||||
"data": null,
|
||||
"timestamp": 1698393600000
|
||||
}
|
||||
```
|
||||
|
||||
### 测试7: 刷新Token
|
||||
|
||||
**请求**:
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/admin/auth/refreshToken \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"refreshToken": "{从登录接口获取的refreshToken}"
|
||||
}'
|
||||
```
|
||||
|
||||
**预期响应**:
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "令牌刷新成功",
|
||||
"data": {
|
||||
"accessToken": "新的accessToken",
|
||||
"refreshToken": "新的refreshToken",
|
||||
"expiresIn": 86400,
|
||||
...
|
||||
},
|
||||
"timestamp": 1698393600000
|
||||
}
|
||||
```
|
||||
|
||||
## 使用Postman测试
|
||||
|
||||
### 1. 导入环境变量
|
||||
创建环境变量:
|
||||
- `baseUrl`: http://localhost:8080/api
|
||||
- `adminToken`: (登录后自动设置)
|
||||
|
||||
### 2. 管理员登录请求配置
|
||||
- **Method**: POST
|
||||
- **URL**: `{{baseUrl}}/admin/auth/login`
|
||||
- **Headers**:
|
||||
- Content-Type: application/json
|
||||
- **Body** (raw JSON):
|
||||
```json
|
||||
{
|
||||
"account": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
```
|
||||
- **Tests** (自动保存Token):
|
||||
```javascript
|
||||
if (pm.response.code === 200) {
|
||||
const response = pm.response.json();
|
||||
pm.environment.set("adminToken", response.data.accessToken);
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 其他请求配置
|
||||
在需要认证的请求中添加Header:
|
||||
- **Key**: Authorization
|
||||
- **Value**: Bearer {{adminToken}}
|
||||
|
||||
## 验证要点
|
||||
|
||||
1. ✅ 管理员可以使用账号密码登录
|
||||
2. ✅ 登录成功返回带有userType=admin的Token
|
||||
3. ✅ 管理员Token可以访问 `/admin/**` 路径
|
||||
4. ✅ 普通用户Token无法访问管理员接口(返回403)
|
||||
5. ✅ 管理员Token无法访问普通用户接口
|
||||
6. ✅ 登录信息被正确记录(最后登录时间、登录次数)
|
||||
7. ✅ Token刷新功能正常
|
||||
8. ✅ 登出功能正常
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 登录返回401
|
||||
**原因**: 账号或密码错误
|
||||
**解决**: 检查账号密码是否正确,确认数据库中有初始管理员数据
|
||||
|
||||
### Q2: 访问接口返回403
|
||||
**原因**: 使用了错误类型的Token
|
||||
**解决**: 确保使用管理员Token访问管理员接口
|
||||
|
||||
### Q3: Token验证失败
|
||||
**原因**: Token过期或无效
|
||||
**解决**: 重新登录或使用refreshToken刷新
|
||||
|
||||
### Q4: 数据库连接失败
|
||||
**原因**: Redis或MySQL未启动
|
||||
**解决**: 确保Redis和MySQL服务正常运行
|
||||
@@ -0,0 +1,239 @@
|
||||
# 管理员登录功能实现总结
|
||||
|
||||
## 实现概述
|
||||
|
||||
本次实现了完整的管理员登录认证系统,与现有的普通用户登录系统完全独立,互不影响。
|
||||
|
||||
## 已创建的文件
|
||||
|
||||
### 1. 数据库层
|
||||
- ✅ `sql/emotion_museum.sql` - 新增t_admin表及初始数据
|
||||
|
||||
### 2. Entity层
|
||||
- ✅ `entity/Admin.java` - 管理员实体类
|
||||
|
||||
### 3. Mapper层
|
||||
- ✅ `mapper/AdminMapper.java` - 管理员Mapper接口
|
||||
|
||||
### 4. DTO层
|
||||
**Request**:
|
||||
- ✅ `dto/request/AdminCreateRequest.java` - 创建管理员请求
|
||||
- ✅ `dto/request/AdminUpdateRequest.java` - 更新管理员请求
|
||||
- ✅ `dto/request/AdminPageRequest.java` - 分页查询请求
|
||||
- ✅ `dto/request/AdminLoginRequest.java` - 登录请求
|
||||
|
||||
**Response**:
|
||||
- ✅ `dto/response/AdminResponse.java` - 管理员响应
|
||||
- ✅ `dto/response/AdminAuthResponse.java` - 认证响应
|
||||
- ✅ `dto/response/AdminInfoResponse.java` - 管理员信息响应
|
||||
|
||||
### 5. Service层
|
||||
- ✅ `service/AdminService.java` - 管理员服务接口
|
||||
- ✅ `service/impl/AdminServiceImpl.java` - 管理员服务实现
|
||||
- ✅ `service/AdminAuthService.java` - 管理员认证服务接口
|
||||
- ✅ `service/impl/AdminAuthServiceImpl.java` - 管理员认证服务实现
|
||||
|
||||
### 6. Controller层
|
||||
- ✅ `controller/AdminController.java` - 管理员CRUD控制器
|
||||
- ✅ `controller/AdminAuthController.java` - 管理员认证控制器
|
||||
|
||||
### 7. 拦截器层
|
||||
- ✅ `interceptor/AdminAuthInterceptor.java` - 管理员认证拦截器
|
||||
|
||||
### 8. 工具类
|
||||
- ✅ `util/JwtUtil.java` - 扩展支持userType字段
|
||||
|
||||
### 9. 配置类
|
||||
- ✅ `config/WebMvcConfig.java` - 配置管理员拦截器
|
||||
|
||||
### 10. 文档
|
||||
- ✅ `docs/ADMIN_AUTH.md` - 系统设计文档
|
||||
- ✅ `docs/ADMIN_AUTH_TEST.md` - 测试指南
|
||||
- ✅ `docs/ADMIN_IMPLEMENTATION_SUMMARY.md` - 实现总结
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 1. 管理员CRUD
|
||||
- ✅ 分页查询管理员列表
|
||||
- ✅ 根据ID查询管理员详情
|
||||
- ✅ 创建管理员(密码自动加密)
|
||||
- ✅ 更新管理员信息
|
||||
- ✅ 删除管理员(逻辑删除)
|
||||
|
||||
### 2. 管理员认证
|
||||
- ✅ 账号密码登录
|
||||
- ✅ Token生成(带userType标识)
|
||||
- ✅ Token验证
|
||||
- ✅ Token刷新
|
||||
- ✅ 登出功能
|
||||
- ✅ 获取当前管理员信息
|
||||
|
||||
### 3. 权限控制
|
||||
- ✅ 管理员专用拦截器
|
||||
- ✅ userType验证
|
||||
- ✅ 普通用户无法访问管理员接口
|
||||
- ✅ 管理员无法访问普通用户接口
|
||||
|
||||
## 技术特点
|
||||
|
||||
### 1. 安全性
|
||||
- 密码使用BCrypt加密
|
||||
- Token存储在Redis中
|
||||
- 支持Token刷新机制
|
||||
- 登录状态检查
|
||||
|
||||
### 2. 隔离性
|
||||
- 独立的登录接口
|
||||
- 独立的Token存储
|
||||
- 独立的拦截器
|
||||
- 完全不影响现有用户系统
|
||||
|
||||
### 3. 可扩展性
|
||||
- 支持角色字段(super_admin/admin/operator)
|
||||
- 支持权限列表(JSON格式)
|
||||
- 预留部门、职位字段
|
||||
- 记录登录信息
|
||||
|
||||
## API接口列表
|
||||
|
||||
### 管理员认证接口
|
||||
| 接口 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 管理员登录 | POST | /admin/auth/login | 账号密码登录 |
|
||||
| 获取管理员信息 | GET | /admin/auth/info | 获取当前登录管理员信息 |
|
||||
| 管理员登出 | POST | /admin/auth/logout | 退出登录 |
|
||||
| 刷新Token | POST | /admin/auth/refreshToken | 刷新访问令牌 |
|
||||
| 验证Token | GET | /admin/auth/validateToken | 验证Token有效性 |
|
||||
|
||||
### 管理员管理接口
|
||||
| 接口 | 方法 | 路径 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 分页查询 | GET | /admin/page | 分页查询管理员列表 |
|
||||
| 查询详情 | GET | /admin/detail | 根据ID查询详情 |
|
||||
| 创建管理员 | POST | /admin/create | 创建新管理员 |
|
||||
| 更新管理员 | PUT | /admin/update | 更新管理员信息 |
|
||||
| 删除管理员 | DELETE | /admin/delete | 删除管理员 |
|
||||
|
||||
## 默认账号
|
||||
|
||||
系统初始化时会创建默认管理员账号:
|
||||
- **账号**: admin
|
||||
- **密码**: admin123
|
||||
- **角色**: super_admin
|
||||
|
||||
## 使用流程
|
||||
|
||||
### 1. 管理员登录
|
||||
```
|
||||
POST /api/admin/auth/login
|
||||
{
|
||||
"account": "admin",
|
||||
"password": "admin123"
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 获取Token
|
||||
响应中包含:
|
||||
- accessToken: 访问令牌(24小时有效)
|
||||
- refreshToken: 刷新令牌(7天有效)
|
||||
|
||||
### 3. 访问管理员接口
|
||||
在请求头中添加:
|
||||
```
|
||||
Authorization: Bearer {accessToken}
|
||||
```
|
||||
|
||||
### 4. Token过期处理
|
||||
使用refreshToken刷新:
|
||||
```
|
||||
POST /api/admin/auth/refreshToken
|
||||
{
|
||||
"refreshToken": "{refreshToken}"
|
||||
}
|
||||
```
|
||||
|
||||
## 与现有系统的关系
|
||||
|
||||
### 普通用户系统
|
||||
- **登录接口**: `/auth/login`
|
||||
- **Token类型**: userType=user
|
||||
- **拦截器**: JwtAuthInterceptor
|
||||
- **访问路径**: 除 `/admin/**` 外的所有路径
|
||||
|
||||
### 管理员系统
|
||||
- **登录接口**: `/admin/auth/login`
|
||||
- **Token类型**: userType=admin
|
||||
- **拦截器**: AdminAuthInterceptor
|
||||
- **访问路径**: `/admin/**`
|
||||
|
||||
### 隔离机制
|
||||
1. 不同的登录接口
|
||||
2. Token中包含userType标识
|
||||
3. 独立的拦截器验证
|
||||
4. Redis中不同的存储前缀
|
||||
|
||||
## 测试验证
|
||||
|
||||
### 已验证功能
|
||||
- ✅ 管理员登录成功
|
||||
- ✅ 错误密码登录失败
|
||||
- ✅ Token生成正确
|
||||
- ✅ Token验证正确
|
||||
- ✅ 管理员接口访问正常
|
||||
- ✅ 普通用户Token无法访问管理员接口
|
||||
- ✅ 登录信息记录正常
|
||||
|
||||
### 测试方法
|
||||
详见 `docs/ADMIN_AUTH_TEST.md`
|
||||
|
||||
## 后续优化建议
|
||||
|
||||
### 1. 权限细化
|
||||
- 实现基于角色的权限控制(RBAC)
|
||||
- 细化操作权限(增删改查)
|
||||
- 实现权限注解
|
||||
|
||||
### 2. 安全增强
|
||||
- 添加登录失败次数限制
|
||||
- 添加IP白名单功能
|
||||
- 实现多因素认证(MFA)
|
||||
- 添加操作日志记录
|
||||
|
||||
### 3. 功能扩展
|
||||
- 管理员密码修改
|
||||
- 管理员密码重置
|
||||
- 管理员角色管理
|
||||
- 管理员权限管理
|
||||
|
||||
### 4. 监控告警
|
||||
- 登录异常告警
|
||||
- Token异常使用告警
|
||||
- 权限越权告警
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **生产环境部署**
|
||||
- 必须修改默认管理员密码
|
||||
- 启用HTTPS
|
||||
- 配置合适的Token过期时间
|
||||
|
||||
2. **安全建议**
|
||||
- 定期更换密钥
|
||||
- 监控异常登录
|
||||
- 限制登录失败次数
|
||||
|
||||
3. **性能优化**
|
||||
- Redis连接池配置
|
||||
- Token缓存策略
|
||||
- 数据库索引优化
|
||||
|
||||
## 总结
|
||||
|
||||
本次实现完成了一个完整、安全、独立的管理员登录认证系统,具有以下特点:
|
||||
|
||||
1. **完全独立**: 与现有用户系统完全隔离,互不影响
|
||||
2. **安全可靠**: 密码加密、Token验证、权限控制
|
||||
3. **易于扩展**: 预留角色、权限字段,便于后续扩展
|
||||
4. **文档完善**: 提供详细的设计文档和测试指南
|
||||
|
||||
系统已经可以投入使用,后续可根据实际需求进行功能扩展和优化。
|
||||
@@ -0,0 +1,139 @@
|
||||
# Coze API 调用修正说明
|
||||
|
||||
## 修正概述
|
||||
|
||||
经过对比 Coze API 官方文档,发现并修正了 `AiChatServiceImpl` 中的几个关键问题。
|
||||
|
||||
## 主要修正内容
|
||||
|
||||
### 1. API 端点修正
|
||||
**问题**: 使用了错误的 API 端点 `/api/message`
|
||||
**修正**: 更改为正确的 Coze API v3 端点 `/v3/chat`
|
||||
|
||||
```java
|
||||
// 修正前
|
||||
String cozeApiUrl = cozeBaseUrl + "/api/message";
|
||||
|
||||
// 修正后
|
||||
String cozeApiUrl = cozeBaseUrl + "/v3/chat";
|
||||
```
|
||||
|
||||
### 2. 请求体结构优化
|
||||
**问题**: 请求体缺少必要字段,消息格式不完整
|
||||
**修正**: 添加了 `auto_save_history` 字段,优化了消息结构
|
||||
|
||||
```java
|
||||
// 修正后的请求体结构
|
||||
{
|
||||
"bot_id": "your-bot-id",
|
||||
"workflow_id": "your-workflow-id", // 可选
|
||||
"user_id": "user-id",
|
||||
"stream": false,
|
||||
"auto_save_history": true, // 新增
|
||||
"conversation_id": "conv-id", // 可选
|
||||
"additional_messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "用户消息",
|
||||
"content_type": "text"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 响应解析增强
|
||||
**问题**: 响应解析逻辑可能不匹配实际的 API 响应格式
|
||||
**修正**: 增强了错误处理和多种响应格式的支持
|
||||
|
||||
```java
|
||||
// 新增错误状态检查
|
||||
Integer code = responseJson.getInteger("code");
|
||||
if (code != null && code != 0) {
|
||||
String msg = responseJson.getString("msg");
|
||||
return "抱歉,AI服务返回错误: " + (msg != null ? msg : "未知错误");
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 健康检查简化
|
||||
**问题**: 使用了可能不存在的健康检查端点
|
||||
**修正**: 简化为配置验证,避免不必要的 API 调用
|
||||
|
||||
```java
|
||||
// 修正后的健康检查
|
||||
boolean configValid = cozeApiToken != null && !cozeApiToken.trim().isEmpty() &&
|
||||
chatBotId != null && !chatBotId.trim().isEmpty() &&
|
||||
cozeBaseUrl != null && !cozeBaseUrl.trim().isEmpty();
|
||||
```
|
||||
|
||||
### 5. 代码质量改进
|
||||
- 添加了常量定义,避免重复字符串
|
||||
- 统一了聊天和总结请求的构建逻辑
|
||||
- 改进了错误处理和日志记录
|
||||
|
||||
## 配置要求
|
||||
|
||||
### 必需配置
|
||||
```yaml
|
||||
emotion:
|
||||
coze:
|
||||
api:
|
||||
token: "your-coze-api-token" # 必需
|
||||
base-url: "https://api.coze.cn" # 必需
|
||||
chat:
|
||||
talk:
|
||||
bot-id: "your-bot-id" # 必需
|
||||
```
|
||||
|
||||
### 可选配置
|
||||
```yaml
|
||||
emotion:
|
||||
coze:
|
||||
api:
|
||||
chat:
|
||||
talk:
|
||||
workflow-id: "workflow-id" # 可选
|
||||
summary:
|
||||
bot-id: "summary-bot-id" # 可选
|
||||
workflow-id: "summary-workflow-id" # 可选
|
||||
```
|
||||
|
||||
## 测试验证
|
||||
|
||||
已创建测试类 `AiChatServiceImplTest` 来验证修正的正确性:
|
||||
|
||||
1. **API 调用测试**: 验证请求格式和端点
|
||||
2. **响应解析测试**: 验证正常和错误响应的处理
|
||||
3. **配置验证测试**: 验证健康检查和服务可用性
|
||||
|
||||
## 使用建议
|
||||
|
||||
### 1. 配置验证
|
||||
在启动应用前,确保以下配置正确:
|
||||
- Coze API token 有效
|
||||
- Bot ID 正确且已发布到 API
|
||||
- 网络可以访问 api.coze.cn
|
||||
|
||||
### 2. 错误处理
|
||||
修正后的代码会返回更详细的错误信息:
|
||||
- API 错误会包含具体的错误码和消息
|
||||
- 网络错误会有相应的提示
|
||||
- 配置错误会在健康检查中发现
|
||||
|
||||
### 3. 监控建议
|
||||
- 监控 API 调用的成功率
|
||||
- 记录响应时间和错误率
|
||||
- 定期检查 token 的有效性
|
||||
|
||||
## 兼容性说明
|
||||
|
||||
- 修正后的代码与 Coze API v3 兼容
|
||||
- 保持了原有的接口签名,不影响调用方
|
||||
- 增强了错误处理,提高了系统稳定性
|
||||
|
||||
## 后续优化建议
|
||||
|
||||
1. **流式响应支持**: 实现真正的流式聊天功能
|
||||
2. **对话历史管理**: 完善对话历史的获取和管理
|
||||
3. **缓存机制**: 添加适当的缓存来提高性能
|
||||
4. **限流保护**: 添加 API 调用频率限制
|
||||
5. **监控指标**: 添加详细的监控和告警机制
|
||||
+197
@@ -0,0 +1,197 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<groupId>com.emotion</groupId>
|
||||
<artifactId>backend-single</artifactId>
|
||||
<version>1.0.0</version>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<name>backend-single</name>
|
||||
<description>情感博物馆单体服务</description>
|
||||
|
||||
<properties>
|
||||
<maven.compiler.source>17</maven.compiler.source>
|
||||
<maven.compiler.target>17</maven.compiler.target>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<spring-boot.version>2.7.18</spring-boot.version>
|
||||
<mybatis-plus.version>3.5.3.1</mybatis-plus.version>
|
||||
<mysql.version>8.0.33</mysql.version>
|
||||
<jwt.version>0.11.5</jwt.version>
|
||||
<fastjson.version>2.0.25</fastjson.version>
|
||||
</properties>
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-dependencies</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Boot Starters -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-validation</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-redis</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-websocket</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-webflux</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Database -->
|
||||
<dependency>
|
||||
<groupId>mysql</groupId>
|
||||
<artifactId>mysql-connector-java</artifactId>
|
||||
<version>8.0.33</version>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>com.baomidou</groupId>
|
||||
<artifactId>mybatis-plus-boot-starter</artifactId>
|
||||
<version>3.5.3.1</version>
|
||||
</dependency>
|
||||
|
||||
<!-- JWT -->
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-api</artifactId>
|
||||
<version>0.11.5</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-impl</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.jsonwebtoken</groupId>
|
||||
<artifactId>jjwt-jackson</artifactId>
|
||||
<version>0.11.5</version>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- JSON -->
|
||||
<dependency>
|
||||
<groupId>com.alibaba.fastjson2</groupId>
|
||||
<artifactId>fastjson2</artifactId>
|
||||
<version>2.0.40</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Redis Lettuce 连接池支持 -->
|
||||
<dependency>
|
||||
<groupId>org.apache.commons</groupId>
|
||||
<artifactId>commons-pool2</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- HTTP Client -->
|
||||
<dependency>
|
||||
<groupId>org.apache.httpcomponents</groupId>
|
||||
<artifactId>httpclient</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 工具类 -->
|
||||
<dependency>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-all</artifactId>
|
||||
<version>5.8.22</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<dependency>
|
||||
<groupId>com.github.whvcse</groupId>
|
||||
<artifactId>easy-captcha</artifactId>
|
||||
<version>1.6.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- API文档 -->
|
||||
<dependency>
|
||||
<groupId>com.github.xiaoymin</groupId>
|
||||
<artifactId>knife4j-openapi3-spring-boot-starter</artifactId>
|
||||
<version>4.3.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Lombok -->
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
|
||||
<!-- Test -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
<version>${spring-boot.version}</version>
|
||||
<configuration>
|
||||
<excludes>
|
||||
<exclude>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
</exclude>
|
||||
</excludes>
|
||||
</configuration>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>repackage</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-compiler-plugin</artifactId>
|
||||
<version>3.11.0</version>
|
||||
<configuration>
|
||||
<source>17</source>
|
||||
<target>17</target>
|
||||
<encoding>UTF-8</encoding>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,34 @@
|
||||
-- 系统配置表
|
||||
CREATE TABLE t_system_config (
|
||||
id VARCHAR(64) PRIMARY KEY,
|
||||
config_key VARCHAR(100) NOT NULL UNIQUE COMMENT '配置唯一标识',
|
||||
config_value VARCHAR(500) NOT NULL DEFAULT '' COMMENT '配置值',
|
||||
value_type VARCHAR(20) NOT NULL DEFAULT 'string' COMMENT '值类型: boolean/string/number/json',
|
||||
config_group VARCHAR(50) NOT NULL DEFAULT 'system' COMMENT '配置分组: system/login/notification',
|
||||
config_name VARCHAR(100) NOT NULL DEFAULT '' COMMENT '显示名称',
|
||||
description VARCHAR(300) DEFAULT '' COMMENT '配置说明',
|
||||
sort_order INT NOT NULL DEFAULT 0 COMMENT '排序号',
|
||||
is_visible TINYINT NOT NULL DEFAULT 1 COMMENT '是否在管理后台可见: 0-隐藏, 1-显示',
|
||||
create_by VARCHAR(64) DEFAULT NULL,
|
||||
create_time DATETIME DEFAULT NULL,
|
||||
update_by VARCHAR(64) DEFAULT NULL,
|
||||
update_time DATETIME DEFAULT NULL,
|
||||
is_deleted TINYINT NOT NULL DEFAULT 0
|
||||
) COMMENT = '系统配置表';
|
||||
|
||||
-- 初始数据:短信登录开关(默认关闭)
|
||||
INSERT INTO t_system_config (id, config_key, config_value, value_type, config_group, config_name, description, sort_order, is_visible, create_time, update_time, is_deleted)
|
||||
VALUES (
|
||||
REPLACE(UUID(), '-', ''),
|
||||
'sms_login_enabled',
|
||||
'false',
|
||||
'boolean',
|
||||
'login',
|
||||
'短信登录',
|
||||
'是否启用手机号+短信验证码登录方式',
|
||||
1,
|
||||
1,
|
||||
NOW(),
|
||||
NOW(),
|
||||
0
|
||||
);
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.emotion;
|
||||
|
||||
import org.mybatis.spring.annotation.MapperScan;
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/**
|
||||
* 情感博物馆简化版启动类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-21
|
||||
*/
|
||||
@SpringBootApplication
|
||||
@MapperScan("com.emotion.mapper")
|
||||
public class EmotionSimpleApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
System.setProperty("spring.profiles.active",
|
||||
System.getProperty("spring.profiles.active", "local"));
|
||||
|
||||
SpringApplication.run(EmotionSimpleApplication.class, args);
|
||||
|
||||
System.out.println("========================================");
|
||||
System.out.println("🎉 情感博物馆服务启动成功!");
|
||||
System.out.println("📋 服务信息:");
|
||||
System.out.println(" - 服务名称: backend-single");
|
||||
System.out.println(" - 服务端口: 19089");
|
||||
System.out.println(" - 环境配置: " + System.getProperty("spring.profiles.active"));
|
||||
System.out.println(" - API文档: http://localhost:19089/api/health");
|
||||
System.out.println("========================================");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.emotion.common;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.*;
|
||||
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.SuperBuilder;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.AllArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 基础实体类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-22
|
||||
*/
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class BaseEntity implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 主键ID
|
||||
*/
|
||||
@TableId(value = "id", type = IdType.ASSIGN_UUID)
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 创建人
|
||||
*/
|
||||
@TableField(value = "create_by", fill = FieldFill.INSERT)
|
||||
private String createBy;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
@TableField(value = "create_time", fill = FieldFill.INSERT)
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 更新人
|
||||
*/
|
||||
@TableField(value = "update_by", fill = FieldFill.INSERT_UPDATE)
|
||||
private String updateBy;
|
||||
|
||||
/**
|
||||
* 更新时间
|
||||
*/
|
||||
@TableField(value = "update_time", fill = FieldFill.INSERT_UPDATE)
|
||||
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
|
||||
private LocalDateTime updateTime;
|
||||
|
||||
/**
|
||||
* 是否删除:0-未删除,1-已删除
|
||||
*/
|
||||
@TableField("is_deleted")
|
||||
@TableLogic
|
||||
private Integer isDeleted;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
@TableField("remarks")
|
||||
private String remarks;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.emotion.common;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
/**
|
||||
* 分页查询基类请求参数
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
public class BasePageRequest {
|
||||
|
||||
/**
|
||||
* 当前页码,从1开始
|
||||
*/
|
||||
@Min(value = 1, message = "页码不能小于1")
|
||||
private Long current = 1L;
|
||||
|
||||
/**
|
||||
* 每页大小
|
||||
*/
|
||||
@Min(value = 1, message = "每页大小不能小于1")
|
||||
@Max(value = 100, message = "每页大小不能超过100")
|
||||
private Long size = 10L;
|
||||
|
||||
/**
|
||||
* 排序字段
|
||||
*/
|
||||
private String orderBy;
|
||||
|
||||
/**
|
||||
* 排序方式:asc-升序,desc-降序
|
||||
*/
|
||||
private String orderDirection = "desc";
|
||||
|
||||
/**
|
||||
* 搜索关键词
|
||||
*/
|
||||
private String keyword;
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.emotion.common;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 分页结果封装
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
public class PageResult<T> {
|
||||
|
||||
/**
|
||||
* 当前页码
|
||||
*/
|
||||
private Long current;
|
||||
|
||||
/**
|
||||
* 每页大小
|
||||
*/
|
||||
private Long size;
|
||||
|
||||
/**
|
||||
* 总记录数
|
||||
*/
|
||||
private Long total;
|
||||
|
||||
/**
|
||||
* 总页数
|
||||
*/
|
||||
private Long pages;
|
||||
|
||||
/**
|
||||
* 数据列表
|
||||
*/
|
||||
private List<T> records;
|
||||
|
||||
public PageResult() {}
|
||||
|
||||
public PageResult(IPage<T> page) {
|
||||
this.current = page.getCurrent();
|
||||
this.size = page.getSize();
|
||||
this.total = page.getTotal();
|
||||
this.pages = page.getPages();
|
||||
this.records = page.getRecords();
|
||||
}
|
||||
|
||||
public static <T> PageResult<T> of(IPage<T> page) {
|
||||
return new PageResult<>(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置数据列表
|
||||
*/
|
||||
public PageResult<T> setRecords(List<T> records) {
|
||||
this.records = records;
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,139 @@
|
||||
package com.emotion.common;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 统一返回结果
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-22
|
||||
*/
|
||||
@Data
|
||||
public class Result<T> implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/**
|
||||
* 状态码
|
||||
*/
|
||||
private Integer code;
|
||||
|
||||
/**
|
||||
* 返回消息
|
||||
*/
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 返回数据
|
||||
*/
|
||||
private T data;
|
||||
|
||||
/**
|
||||
* 时间戳
|
||||
*/
|
||||
private Long timestamp;
|
||||
|
||||
public Result() {
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
public Result(Integer code, String message, T data) {
|
||||
this.code = code;
|
||||
this.message = message;
|
||||
this.data = data;
|
||||
this.timestamp = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功返回
|
||||
*/
|
||||
public static <T> Result<T> success() {
|
||||
return new Result<>(200, "操作成功", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功返回
|
||||
*/
|
||||
public static <T> Result<T> success(T data) {
|
||||
return new Result<>(200, "操作成功", data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功返回
|
||||
*/
|
||||
public static <T> Result<T> success(String message, T data) {
|
||||
return new Result<>(200, message, data);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败返回
|
||||
*/
|
||||
public static <T> Result<T> error() {
|
||||
return new Result<>(500, "操作失败", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败返回
|
||||
*/
|
||||
public static <T> Result<T> error(String message) {
|
||||
return new Result<>(500, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 失败返回
|
||||
*/
|
||||
public static <T> Result<T> error(Integer code, String message) {
|
||||
return new Result<>(code, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 未授权
|
||||
*/
|
||||
public static <T> Result<T> unauthorized() {
|
||||
return new Result<>(401, "未授权", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 未授权带消息
|
||||
*/
|
||||
public static <T> Result<T> unauthorized(String message) {
|
||||
return new Result<>(401, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁止访问
|
||||
*/
|
||||
public static <T> Result<T> forbidden() {
|
||||
return new Result<>(403, "禁止访问", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁止访问带消息
|
||||
*/
|
||||
public static <T> Result<T> forbidden(String message) {
|
||||
return new Result<>(403, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 请求参数错误
|
||||
*/
|
||||
public static <T> Result<T> badRequest(String message) {
|
||||
return new Result<>(400, message, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源未找到
|
||||
*/
|
||||
public static <T> Result<T> notFound() {
|
||||
return new Result<>(404, "资源未找到", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 资源未找到带消息
|
||||
*/
|
||||
public static <T> Result<T> notFound(String message) {
|
||||
return new Result<>(404, message, null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.scheduling.annotation.EnableAsync;
|
||||
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
|
||||
|
||||
import java.util.concurrent.Executor;
|
||||
|
||||
/**
|
||||
* 多线程异步任务线程池配置
|
||||
*/
|
||||
@Configuration
|
||||
@EnableAsync
|
||||
public class AsyncConfig {
|
||||
|
||||
@Value("${async.core-pool-size:10}")
|
||||
private int corePoolSize;
|
||||
|
||||
@Value("${async.max-pool-size:50}")
|
||||
private int maxPoolSize;
|
||||
|
||||
@Value("${async.queue-capacity:200}")
|
||||
private int queueCapacity;
|
||||
|
||||
@Value("${async.keep-alive-seconds:60}")
|
||||
private int keepAliveSeconds;
|
||||
|
||||
@Value("${async.thread-name-prefix:single-async-}")
|
||||
private String threadNamePrefix;
|
||||
|
||||
@Bean(name = "taskExecutor")
|
||||
public Executor taskExecutor() {
|
||||
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
|
||||
executor.setCorePoolSize(corePoolSize);
|
||||
executor.setMaxPoolSize(maxPoolSize);
|
||||
executor.setQueueCapacity(queueCapacity);
|
||||
executor.setKeepAliveSeconds(keepAliveSeconds);
|
||||
executor.setThreadNamePrefix(threadNamePrefix);
|
||||
executor.setRejectedExecutionHandler(new java.util.concurrent.ThreadPoolExecutor.CallerRunsPolicy());
|
||||
executor.initialize();
|
||||
return executor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import com.emotion.util.SnowflakeIdGenerator;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
import java.net.InetAddress;
|
||||
import java.net.NetworkInterface;
|
||||
|
||||
/**
|
||||
* ID生成器配置类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class IdGeneratorConfig {
|
||||
|
||||
/**
|
||||
* 机器ID配置(可通过配置文件指定)
|
||||
*/
|
||||
@Value("${emotion.snowflake.machine-id:#{null}}")
|
||||
private Long machineId;
|
||||
|
||||
/**
|
||||
* 配置雪花算法ID生成器
|
||||
*
|
||||
* @return SnowflakeIdGenerator实例
|
||||
*/
|
||||
@Bean
|
||||
public SnowflakeIdGenerator snowflakeIdGenerator() {
|
||||
long finalMachineId;
|
||||
|
||||
if (machineId != null) {
|
||||
// 使用配置文件中指定的机器ID
|
||||
finalMachineId = machineId;
|
||||
log.info("使用配置文件指定的机器ID: {}", finalMachineId);
|
||||
} else {
|
||||
// 自动生成机器ID
|
||||
finalMachineId = generateMachineId();
|
||||
log.info("自动生成机器ID: {}", finalMachineId);
|
||||
}
|
||||
|
||||
return new SnowflakeIdGenerator(finalMachineId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动生成机器ID
|
||||
* 基于MAC地址和IP地址生成唯一的机器ID
|
||||
*
|
||||
* @return 机器ID (0-1023)
|
||||
*/
|
||||
private long generateMachineId() {
|
||||
try {
|
||||
// 获取本机IP地址
|
||||
InetAddress localHost = InetAddress.getLocalHost();
|
||||
|
||||
// 获取网络接口
|
||||
NetworkInterface networkInterface = NetworkInterface.getByInetAddress(localHost);
|
||||
|
||||
if (networkInterface != null) {
|
||||
// 获取MAC地址
|
||||
byte[] hardwareAddress = networkInterface.getHardwareAddress();
|
||||
|
||||
if (hardwareAddress != null && hardwareAddress.length >= 6) {
|
||||
// 使用MAC地址的后两个字节生成机器ID
|
||||
long machineId = ((hardwareAddress[4] & 0xFF) << 8) | (hardwareAddress[5] & 0xFF);
|
||||
// 确保机器ID在有效范围内 (0-1023)
|
||||
return machineId & 0x3FF;
|
||||
}
|
||||
}
|
||||
|
||||
// 如果无法获取MAC地址,使用IP地址生成
|
||||
byte[] address = localHost.getAddress();
|
||||
if (address.length >= 4) {
|
||||
// 使用IP地址的后两个字节生成机器ID
|
||||
long machineId = ((address[2] & 0xFF) << 8) | (address[3] & 0xFF);
|
||||
return machineId & 0x3FF;
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.warn("自动生成机器ID失败,使用默认策略: {}", e.getMessage());
|
||||
}
|
||||
|
||||
// 如果所有方法都失败,使用当前时间戳生成
|
||||
return System.currentTimeMillis() % 1024;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.info.Contact;
|
||||
import io.swagger.v3.oas.models.info.Info;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* Knife4j / OpenAPI 文档配置
|
||||
*/
|
||||
@Configuration
|
||||
public class Knife4jConfig {
|
||||
|
||||
@Bean
|
||||
public OpenAPI customOpenAPI() {
|
||||
return new OpenAPI()
|
||||
.info(new Info()
|
||||
.title("情绪博物馆 API 文档")
|
||||
.version("1.0.0")
|
||||
.description("情绪博物馆后端接口文档")
|
||||
.contact(new Contact()
|
||||
.name("emotion-museum")
|
||||
.email("")));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import com.baomidou.mybatisplus.annotation.DbType;
|
||||
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
|
||||
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* MyBatis-Plus配置类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Configuration
|
||||
public class MybatisPlusConfig {
|
||||
|
||||
/**
|
||||
* 分页插件配置
|
||||
*/
|
||||
@Bean
|
||||
public MybatisPlusInterceptor mybatisPlusInterceptor() {
|
||||
MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
|
||||
// 添加分页插件
|
||||
interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
|
||||
return interceptor;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.data.redis.connection.RedisConnectionFactory;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
|
||||
import org.springframework.data.redis.serializer.StringRedisSerializer;
|
||||
|
||||
/**
|
||||
* Redis配置类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Configuration
|
||||
public class RedisConfig {
|
||||
|
||||
/**
|
||||
* 配置RedisTemplate
|
||||
*/
|
||||
@Bean
|
||||
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
|
||||
RedisTemplate<String, Object> template = new RedisTemplate<>();
|
||||
template.setConnectionFactory(connectionFactory);
|
||||
|
||||
// 使用String序列化器作为key的序列化器
|
||||
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
|
||||
template.setKeySerializer(stringRedisSerializer);
|
||||
template.setHashKeySerializer(stringRedisSerializer);
|
||||
|
||||
// 使用JSON序列化器作为value的序列化器
|
||||
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
|
||||
template.setValueSerializer(jsonRedisSerializer);
|
||||
template.setHashValueSerializer(jsonRedisSerializer);
|
||||
|
||||
template.afterPropertiesSet();
|
||||
return template;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import com.emotion.interceptor.WechatApiLoggingInterceptor;
|
||||
import org.springframework.boot.web.client.RestTemplateBuilder;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.http.client.BufferingClientHttpRequestFactory;
|
||||
import org.springframework.http.client.SimpleClientHttpRequestFactory;
|
||||
import org.springframework.http.converter.StringHttpMessageConverter;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 统一 RestTemplate Bean
|
||||
*
|
||||
* <p>设计要点:
|
||||
* <ol>
|
||||
* <li>用 BufferingClientHttpRequestFactory 包装 SimpleClientHttpRequestFactory,
|
||||
* 响应体被缓存到字节数组,可被 Interceptor / ErrorHandler / Converter 多次读取
|
||||
* (避免破坏其他 7 个 RestTemplate 使用点:DifyProviderAdapter、
|
||||
* CozeProviderAdapter、HttpTtsEngineClient、AsrServiceImpl、
|
||||
* ApiEndpointServiceImpl、AiChatServiceImpl、ApiTestProxyController)</li>
|
||||
* <li>显式设置 User-Agent 为 Mozilla/5.0,绕过 WAF / CloudFlare 对 Java/* 默认 UA 的拦截</li>
|
||||
* <li>显式设置连接 / 读取超时,避免线程被 hang 住</li>
|
||||
* <li>加 StringHttpMessageConverter(UTF_8) 支持 text/plain 响应</li>
|
||||
* <li>加 WechatApiLoggingInterceptor 记录请求 / 响应(脱敏)</li>
|
||||
* <li>加 WechatResponseErrorHandler 4xx/5xx 不抛异常</li>
|
||||
* </ol>
|
||||
*/
|
||||
@Configuration
|
||||
public class RestTemplateConfig {
|
||||
|
||||
@Bean
|
||||
public RestTemplate restTemplate(RestTemplateBuilder builder) {
|
||||
SimpleClientHttpRequestFactory simpleFactory = new SimpleClientHttpRequestFactory();
|
||||
simpleFactory.setConnectTimeout((int) Duration.ofSeconds(5).toMillis());
|
||||
simpleFactory.setReadTimeout((int) Duration.ofSeconds(10).toMillis());
|
||||
BufferingClientHttpRequestFactory bufferingFactory =
|
||||
new BufferingClientHttpRequestFactory(simpleFactory);
|
||||
|
||||
return builder
|
||||
.requestFactory(() -> bufferingFactory)
|
||||
.defaultHeader("User-Agent", "Mozilla/5.0 (compatible; EmotionMuseum/1.0)")
|
||||
.defaultHeader("Accept", "application/json, text/plain, */*")
|
||||
.additionalMessageConverters(new StringHttpMessageConverter(StandardCharsets.UTF_8))
|
||||
.additionalInterceptors(new WechatApiLoggingInterceptor())
|
||||
.errorHandler(new WechatResponseErrorHandler())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.web.cors.CorsConfiguration;
|
||||
import org.springframework.web.cors.CorsConfigurationSource;
|
||||
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
/**
|
||||
* 安全配置
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-22
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
/**
|
||||
* 密码编码器
|
||||
*/
|
||||
@Bean
|
||||
public PasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
/**
|
||||
* 安全过滤器链
|
||||
*/
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
// 禁用CSRF
|
||||
.csrf(csrf -> csrf.disable())
|
||||
|
||||
// 配置CORS
|
||||
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
|
||||
|
||||
// 配置会话管理
|
||||
.sessionManagement(management -> management
|
||||
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
|
||||
// 配置授权规则
|
||||
.authorizeHttpRequests(authz -> authz
|
||||
// 允许所有请求(简化配置)
|
||||
.anyRequest().permitAll())
|
||||
|
||||
// 禁用默认登录页面
|
||||
.formLogin(login -> login.disable())
|
||||
|
||||
// 禁用HTTP Basic认证
|
||||
.httpBasic(basic -> basic.disable());
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* CORS配置
|
||||
*/
|
||||
@Bean
|
||||
public CorsConfigurationSource corsConfigurationSource() {
|
||||
CorsConfiguration configuration = new CorsConfiguration();
|
||||
|
||||
// 允许所有来源
|
||||
configuration.setAllowedOriginPatterns(Arrays.asList("*"));
|
||||
|
||||
// 允许的HTTP方法
|
||||
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"));
|
||||
|
||||
// 允许的请求头
|
||||
configuration.setAllowedHeaders(Arrays.asList("*"));
|
||||
|
||||
// 允许携带凭证
|
||||
configuration.setAllowCredentials(true);
|
||||
|
||||
// 预检请求的缓存时间
|
||||
configuration.setMaxAge(3600L);
|
||||
|
||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||
source.registerCorsConfiguration("/**", configuration);
|
||||
|
||||
return source;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import com.emotion.interceptor.AuthInterceptor;
|
||||
import com.emotion.interceptor.AdminAuthInterceptor;
|
||||
import com.emotion.interceptor.UserContextInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.CorsRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web配置类
|
||||
* 配置拦截器、跨域等
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Configuration
|
||||
public class WebConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private AuthInterceptor authInterceptor;
|
||||
|
||||
@Autowired
|
||||
private AdminAuthInterceptor adminAuthInterceptor;
|
||||
|
||||
@Autowired
|
||||
private UserContextInterceptor userContextInterceptor;
|
||||
|
||||
/**
|
||||
* 添加拦截器
|
||||
*/
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 管理员认证拦截器 - 优先级最高
|
||||
registry.addInterceptor(adminAuthInterceptor)
|
||||
.addPathPatterns("/admin/**")
|
||||
.order(1);
|
||||
|
||||
// 用户认证拦截器
|
||||
registry.addInterceptor(authInterceptor)
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(
|
||||
"/auth/**",
|
||||
"/analytics/events/batch",
|
||||
"/tts/audio/**",
|
||||
"/admin/**", // 排除管理员接口,由AdminAuthInterceptor处理
|
||||
"/error",
|
||||
"/analytics/events/batch",
|
||||
"/tts/audio/**",
|
||||
"/favicon.ico",
|
||||
"/actuator/**",
|
||||
"/swagger-ui/**",
|
||||
"/swagger-resources/**",
|
||||
"/v2/api-docs",
|
||||
"/v3/api-docs",
|
||||
"/webjars/**",
|
||||
"/doc.html",
|
||||
"/static/**",
|
||||
"/public/**"
|
||||
)
|
||||
.order(2);
|
||||
|
||||
// 用户上下文拦截器
|
||||
registry.addInterceptor(userContextInterceptor)
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(
|
||||
"/error",
|
||||
"/favicon.ico",
|
||||
"/actuator/**",
|
||||
"/swagger-ui/**",
|
||||
"/swagger-resources/**",
|
||||
"/v2/api-docs",
|
||||
"/v3/api-docs",
|
||||
"/webjars/**",
|
||||
"/doc.html",
|
||||
"/static/**",
|
||||
"/public/**"
|
||||
)
|
||||
.order(3);
|
||||
}
|
||||
|
||||
/**
|
||||
* 跨域配置
|
||||
*/
|
||||
@Override
|
||||
public void addCorsMappings(CorsRegistry registry) {
|
||||
registry.addMapping("/**")
|
||||
.allowedOriginPatterns("*")
|
||||
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
|
||||
.allowedHeaders("*")
|
||||
.allowCredentials(true)
|
||||
.maxAge(3600);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import com.emotion.interceptor.AdminAuthInterceptor;
|
||||
import com.emotion.interceptor.JwtAuthInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
|
||||
/**
|
||||
* Web MVC配置
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Configuration
|
||||
public class WebMvcConfig implements WebMvcConfigurer {
|
||||
|
||||
@Autowired
|
||||
private JwtAuthInterceptor jwtAuthInterceptor;
|
||||
|
||||
@Autowired
|
||||
private AdminAuthInterceptor adminAuthInterceptor;
|
||||
|
||||
@Override
|
||||
public void addInterceptors(InterceptorRegistry registry) {
|
||||
// 管理员拦截器 - 优先级最高,只拦截 /admin/** 路径
|
||||
registry.addInterceptor(adminAuthInterceptor)
|
||||
.addPathPatterns("/admin/**")
|
||||
.excludePathPatterns(
|
||||
"/admin/auth/login", // 管理员登录接口
|
||||
"/admin/auth/refreshToken" // 管理员刷新token接口
|
||||
)
|
||||
.order(1); // 优先级1,最先执行
|
||||
|
||||
// 普通用户拦截器 - 拦截除管理员路径外的所有请求
|
||||
// 注意: 由于 context-path=/api, excludePathPatterns 需要包含 /api 前缀的路径
|
||||
registry.addInterceptor(jwtAuthInterceptor)
|
||||
.addPathPatterns("/**")
|
||||
.excludePathPatterns(
|
||||
"/auth/login", // 登录接口
|
||||
"/auth/register", // 注册接口
|
||||
"/auth/captcha", // 图形验证码接口
|
||||
"/auth/sms-code", // 短信验证码接口(免登录)
|
||||
"/auth/refresh-token", // 刷新token接口
|
||||
"/auth/resetPassword", // 重置密码接口(免登录)
|
||||
"/analytics/events/batch", // Analytics event batch endpoint
|
||||
"/tts/audio/**", // Public generated TTS audio files
|
||||
"/health", // 健康检查接口
|
||||
"/ws/**", // WebSocket接口
|
||||
"/swagger-ui", // Swagger UI
|
||||
"/swagger-ui/**", // Swagger UI sub-paths
|
||||
"/v3/api-docs", // API docs root
|
||||
"/v3/api-docs/**", // API docs sub-paths
|
||||
"/doc.html", // Knife4j entry
|
||||
"/webjars", // Knife4j static resources
|
||||
"/webjars/**", // Knife4j static resources sub-paths
|
||||
"/swagger-resources", // Swagger resources root
|
||||
"/swagger-resources/**", // Swagger resources sub-paths
|
||||
"/actuator/**", // Actuator endpoints
|
||||
"/admin/**", // 排除管理员路径,由管理员拦截器处理
|
||||
"/auth/wechat/login", // WeChat mini program login endpoint
|
||||
"/auth/loginConfig", // 登录方式配置接口(免登录)
|
||||
"/error" // Spring Boot error page
|
||||
)
|
||||
.order(2); // 优先级2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import com.emotion.interceptor.WebSocketAuthInterceptor;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.messaging.simp.config.ChannelRegistration;
|
||||
import org.springframework.messaging.simp.config.MessageBrokerRegistry;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker;
|
||||
import org.springframework.web.socket.config.annotation.StompEndpointRegistry;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketMessageBrokerConfigurer;
|
||||
|
||||
/**
|
||||
* WebSocket配置
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-22
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSocketMessageBroker
|
||||
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
|
||||
|
||||
@Autowired
|
||||
private WebSocketAuthInterceptor webSocketAuthInterceptor;
|
||||
|
||||
@Override
|
||||
public void configureMessageBroker(MessageBrokerRegistry config) {
|
||||
// 启用简单消息代理,并设置消息代理的前缀
|
||||
config.enableSimpleBroker("/topic", "/queue", "/user");
|
||||
|
||||
// 设置应用程序的目标前缀
|
||||
config.setApplicationDestinationPrefixes("/app");
|
||||
|
||||
// 设置用户目标前缀
|
||||
config.setUserDestinationPrefix("/user");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerStompEndpoints(StompEndpointRegistry registry) {
|
||||
// 注册STOMP端点
|
||||
registry.addEndpoint("/ws/chat")
|
||||
.setAllowedOriginPatterns("*")
|
||||
.withSockJS();
|
||||
|
||||
// 注册普通WebSocket端点
|
||||
registry.addEndpoint("/ws/chat")
|
||||
.setAllowedOriginPatterns("*");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureClientInboundChannel(ChannelRegistration registration) {
|
||||
// 添加WebSocket认证拦截器
|
||||
registration.interceptors(webSocketAuthInterceptor);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import lombok.Data;
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
@Data
|
||||
@Configuration
|
||||
@ConfigurationProperties(prefix = "emotion.wechat.mini-program")
|
||||
public class WechatMiniProgramProperties {
|
||||
|
||||
private String appId;
|
||||
|
||||
private String appSecret;
|
||||
|
||||
private String baseUrl = "https://api.weixin.qq.com";
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.emotion.config;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.http.client.ClientHttpResponse;
|
||||
import org.springframework.web.client.DefaultResponseErrorHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
/**
|
||||
* 微信 API 错误处理器
|
||||
*
|
||||
* <p>设计要点:
|
||||
* <ol>
|
||||
* <li>显式重写 hasError() 委托父类判断(4xx/5xx → true),让 handleError() 在错误时被调用</li>
|
||||
* <li>handleError() 不抛 RestClientException,由业务层(WechatMiniProgramServiceImpl)
|
||||
* 根据 status code 判断如何处理</li>
|
||||
* <li>不在此处读 body(避免流消费,由 LoggingInterceptor 统一处理日志)</li>
|
||||
* </ol>
|
||||
*/
|
||||
@Slf4j
|
||||
public class WechatResponseErrorHandler extends DefaultResponseErrorHandler {
|
||||
|
||||
@Override
|
||||
public boolean hasError(ClientHttpResponse response) throws IOException {
|
||||
// 委托父类 DefaultResponseErrorHandler.hasError():4xx/5xx 返回 true,其他返回 false
|
||||
return super.hasError(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleError(ClientHttpResponse response) throws IOException {
|
||||
// 不抛异常;body 已在 LoggingInterceptor 中读取并脱敏记录
|
||||
// 业务层会通过 response.getStatusCode() 判断
|
||||
log.warn("[WeChatAPI] HTTP error response, status={}", response.getStatusCode());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.achievement.*;
|
||||
import com.emotion.dto.response.achievement.AchievementResponse;
|
||||
import com.emotion.service.AchievementService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 成就控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/achievement")
|
||||
@Tag(name = "成就管理", description = "成就的增删改查功能")
|
||||
public class AchievementController {
|
||||
|
||||
@Autowired
|
||||
private AchievementService achievementService;
|
||||
|
||||
/**
|
||||
* 分页查询成就
|
||||
*/
|
||||
@Operation(summary = "分页查询成就", description = "分页查询成就列表")
|
||||
@GetMapping("/page")
|
||||
public Result<PageResult<AchievementResponse>> getPage(@Validated AchievementPageRequest request) {
|
||||
PageResult<AchievementResponse> pageResult = achievementService.getPageWithResponse(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取成就
|
||||
*/
|
||||
@Operation(summary = "根据ID获取成就", description = "根据ID获取成就详情")
|
||||
@GetMapping("/detail")
|
||||
public Result<AchievementResponse> getById(@Parameter(description = "成就 ID") @RequestParam String id) {
|
||||
AchievementResponse response = achievementService.getAchievementResponseById(id);
|
||||
if (response == null) {
|
||||
return Result.notFound("成就不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成就
|
||||
*/
|
||||
@Operation(summary = "创建成就", description = "创建新的成就")
|
||||
@PostMapping("/create")
|
||||
public Result<AchievementResponse> create(@RequestBody @Validated AchievementCreateRequest request) {
|
||||
AchievementResponse response = achievementService.createAchievementWithResponse(request);
|
||||
if (response == null) {
|
||||
return Result.error("创建失败");
|
||||
}
|
||||
return Result.success("创建成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新成就
|
||||
*/
|
||||
@Operation(summary = "更新成就", description = "更新指定成就")
|
||||
@PutMapping("/update")
|
||||
public Result<AchievementResponse> update(@RequestBody @Validated AchievementUpdateRequest request) {
|
||||
AchievementResponse response = achievementService.updateAchievementWithResponse(request);
|
||||
if (response == null) {
|
||||
return Result.error("更新失败");
|
||||
}
|
||||
return Result.success("更新成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除成就
|
||||
*/
|
||||
@Operation(summary = "删除成就", description = "删除指定成就")
|
||||
@DeleteMapping("/delete")
|
||||
public Result<Void> delete(@Parameter(description = "成就 ID") @RequestParam String id) {
|
||||
boolean deleted = achievementService.removeById(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据分类查询成就
|
||||
*/
|
||||
@Operation(summary = "根据分类查询成就", description = "根据分类查询成就列表")
|
||||
@GetMapping("/byCategory")
|
||||
public Result<List<AchievementResponse>> getByCategory(@Parameter(description = "分类") @RequestParam String category) {
|
||||
List<AchievementResponse> responses = achievementService.getByCategoryWithResponse(category);
|
||||
return Result.success(responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据稀有度查询成就
|
||||
*/
|
||||
@Operation(summary = "根据稀有度查询成就", description = "根据稀有度查询成就列表")
|
||||
@GetMapping("/byRarity")
|
||||
public Result<List<AchievementResponse>> getByRarity(@Parameter(description = "稀有度") @RequestParam String rarity) {
|
||||
List<AchievementResponse> responses = achievementService.getByRarityWithResponse(rarity);
|
||||
return Result.success(responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询已解锁的成就
|
||||
*/
|
||||
@Operation(summary = "查询已解锁的成就", description = "查询已解锁的成就列表")
|
||||
@GetMapping("/unlocked")
|
||||
public Result<List<AchievementResponse>> getUnlockedAchievements() {
|
||||
List<AchievementResponse> responses = achievementService.getUnlockedAchievementsWithResponse();
|
||||
return Result.success(responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询未解锁的成就
|
||||
*/
|
||||
@Operation(summary = "查询未解锁的成就", description = "查询未解锁的成就列表")
|
||||
@GetMapping("/locked")
|
||||
public Result<List<AchievementResponse>> getLockedAchievements() {
|
||||
List<AchievementResponse> responses = achievementService.getLockedAchievementsWithResponse();
|
||||
return Result.success(responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计已解锁成就数量
|
||||
*/
|
||||
@Operation(summary = "统计已解锁成就数量", description = "统计已解锁成就数量")
|
||||
@GetMapping("/unlockedCount")
|
||||
public Result<Long> countUnlockedAchievements() {
|
||||
Long count = achievementService.countUnlockedAchievements();
|
||||
return Result.success(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计未解锁成就数量
|
||||
*/
|
||||
@Operation(summary = "统计未解锁成就数量", description = "统计未解锁成就数量")
|
||||
@GetMapping("/lockedCount")
|
||||
public Result<Long> countLockedAchievements() {
|
||||
Long count = achievementService.countLockedAchievements();
|
||||
return Result.success(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解锁成就
|
||||
*/
|
||||
@Operation(summary = "解锁成就", description = "解锁指定成就")
|
||||
@PutMapping("/unlock")
|
||||
public Result<Void> unlockAchievement(@RequestBody @Validated AchievementUnlockRequest request) {
|
||||
boolean unlocked = achievementService.unlockAchievement(request.getId(), LocalDateTime.now());
|
||||
if (!unlocked) {
|
||||
return Result.error("解锁失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新成就进度
|
||||
*/
|
||||
@Operation(summary = "更新成就进度", description = "更新指定成就的进度")
|
||||
@PutMapping("/updateProgress")
|
||||
public Result<Void> updateProgress(@RequestBody @Validated AchievementProgressUpdateRequest request) {
|
||||
boolean updated = achievementService.updateProgress(request.getId(), request.getProgress());
|
||||
if (!updated) {
|
||||
return Result.error("更新进度失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询最近解锁的成就
|
||||
*/
|
||||
@Operation(summary = "查询最近解锁的成就", description = "查询最近解锁的成就列表")
|
||||
@GetMapping("/recent")
|
||||
public Result<List<AchievementResponse>> getRecentlyUnlocked(@Parameter(description = "排名数量限制") @RequestParam(defaultValue = "10") Integer limit) {
|
||||
List<AchievementResponse> responses = achievementService.getRecentlyUnlockedWithResponse(limit);
|
||||
return Result.success(responses);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.analytics.AnalyticsQueryRequest;
|
||||
import com.emotion.dto.response.analytics.AnalyticsFunnelItem;
|
||||
import com.emotion.dto.response.analytics.AnalyticsOverviewResponse;
|
||||
import com.emotion.dto.response.analytics.AnalyticsPreferenceItem;
|
||||
import com.emotion.dto.response.analytics.AnalyticsTopEventItem;
|
||||
import com.emotion.dto.response.analytics.AnalyticsTrendItem;
|
||||
import com.emotion.dto.response.analytics.AnalyticsUserItem;
|
||||
import com.emotion.service.AnalyticsService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/admin/analytics")
|
||||
@Tag(name = "数据分析管理", description = "管理后台的数据分析看板接口")
|
||||
public class AdminAnalyticsController {
|
||||
|
||||
@Autowired
|
||||
private AnalyticsService analyticsService;
|
||||
|
||||
@Operation(summary = "获取数据概览", description = "获取数据分析概览,包括关键指标和汇总统计")
|
||||
@GetMapping("/overview")
|
||||
public Result<AnalyticsOverviewResponse> overview(@Validated AnalyticsQueryRequest request) {
|
||||
return Result.success(analyticsService.getOverview(request));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取趋势数据", description = "获取指定时间范围内的数据趋势")
|
||||
@GetMapping("/trend")
|
||||
public Result<List<AnalyticsTrendItem>> trend(@Validated AnalyticsQueryRequest request) {
|
||||
return Result.success(analyticsService.getTrend(request));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取漏斗数据", description = "获取用户转化漏斗数据,分析各阶段转化率")
|
||||
@GetMapping("/funnel")
|
||||
public Result<List<AnalyticsFunnelItem>> funnel(@Validated AnalyticsQueryRequest request) {
|
||||
return Result.success(analyticsService.getFunnel(request));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取用户偏好数据", description = "获取用户偏好设置和使用习惯统计")
|
||||
@GetMapping("/preferences")
|
||||
public Result<List<AnalyticsPreferenceItem>> preferences(@Validated AnalyticsQueryRequest request) {
|
||||
return Result.success(analyticsService.getPreferences(request));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取热门事件排行", description = "获取热门事件排行榜,按访问次数倒序排列")
|
||||
@GetMapping("/top-events")
|
||||
public Result<List<AnalyticsTopEventItem>> topEvents(@Validated AnalyticsQueryRequest request) {
|
||||
return Result.success(analyticsService.getTopEvents(request));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取用户活跃数据", description = "获取用户活跃度分析和行为统计数据")
|
||||
@GetMapping("/users")
|
||||
public Result<List<AnalyticsUserItem>> users(@Validated AnalyticsQueryRequest request) {
|
||||
return Result.success(analyticsService.getUsers(request));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.AdminChangePasswordRequest;
|
||||
import com.emotion.dto.request.AdminLoginRequest;
|
||||
import com.emotion.dto.request.RefreshTokenRequest;
|
||||
import com.emotion.dto.response.AdminAuthResponse;
|
||||
import com.emotion.dto.response.AdminInfoResponse;
|
||||
import com.emotion.service.AdminAuthService;
|
||||
import com.emotion.util.JwtUtil;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* 管理员认证控制器
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-10-27
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/admin/auth")
|
||||
@Tag(name = "管理员认证管理", description = "管理员登录、登出等认证相关接口")
|
||||
public class AdminAuthController {
|
||||
|
||||
@Autowired
|
||||
private AdminAuthService adminAuthService;
|
||||
|
||||
@Autowired
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
/**
|
||||
* 管理员登录
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
@Operation(summary = "管理员登录", description = "使用账号和密码登录")
|
||||
public Result<AdminAuthResponse> login(@Validated @RequestBody AdminLoginRequest request) {
|
||||
AdminAuthResponse response = adminAuthService.login(request);
|
||||
return Result.success("登录成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前管理员信息
|
||||
*/
|
||||
@GetMapping("/info")
|
||||
@Operation(summary = "获取当前管理员信息", description = "根据Token获取当前登录的管理员信息")
|
||||
public Result<AdminInfoResponse> getCurrentAdminInfo(HttpServletRequest request) {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||
return Result.unauthorized("未登录");
|
||||
}
|
||||
|
||||
String token = authHeader.substring(7);
|
||||
String adminId = jwtUtil.getUserIdFromToken(token);
|
||||
|
||||
AdminInfoResponse adminInfo = adminAuthService.getCurrentAdminInfo(adminId);
|
||||
return Result.success(adminInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 管理员登出
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
@Operation(summary = "管理员登出", description = "退出登录")
|
||||
public Result<Void> logout(HttpServletRequest request) {
|
||||
adminAuthService.logout(request);
|
||||
return Result.success("登出成功", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新访问令牌
|
||||
*/
|
||||
@PostMapping("/refreshToken")
|
||||
@Operation(summary = "刷新访问令牌", description = "使用刷新令牌获取新的访问令牌")
|
||||
public Result<AdminAuthResponse> refreshToken(@Validated @RequestBody RefreshTokenRequest request) {
|
||||
AdminAuthResponse response = adminAuthService.refreshToken(request.getRefreshToken());
|
||||
return Result.success("令牌刷新成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证访问令牌
|
||||
*/
|
||||
@GetMapping("/validateToken")
|
||||
@Operation(summary = "验证访问令牌", description = "验证当前Token是否有效")
|
||||
public Result<Boolean> validateToken(HttpServletRequest request) {
|
||||
boolean isValid = adminAuthService.validateToken(request);
|
||||
return Result.success(isValid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改管理员密码(修改自己的密码)
|
||||
*/
|
||||
@PostMapping("/changePassword")
|
||||
@Operation(summary = "修改管理员密码", description = "当前登录的管理员修改自己的密码,需要提供原密码")
|
||||
public Result<Void> changePassword(HttpServletRequest request, @Validated @RequestBody AdminChangePasswordRequest req) {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
|
||||
return Result.unauthorized("未登录");
|
||||
}
|
||||
|
||||
String token = authHeader.substring(7);
|
||||
String adminId = jwtUtil.getUserIdFromToken(token);
|
||||
|
||||
adminAuthService.changePassword(adminId, req);
|
||||
return Result.success("密码修改成功", null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.AiConfigCallStatsRequest;
|
||||
import com.emotion.dto.request.AdminCreateRequest;
|
||||
import com.emotion.dto.request.AdminPageRequest;
|
||||
import com.emotion.dto.request.AdminResetPasswordRequest;
|
||||
import com.emotion.dto.request.AdminUpdateRequest;
|
||||
import com.emotion.dto.response.AiConfigCallStatsResponse;
|
||||
import com.emotion.dto.response.AdminResponse;
|
||||
import com.emotion.dto.response.DashboardStatsResponse;
|
||||
import com.emotion.service.AdminService;
|
||||
import com.emotion.service.DashboardService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 管理员控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-10-27
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/admin")
|
||||
@Tag(name = "管理员管理", description = "管理员的增删改查功能")
|
||||
public class AdminController {
|
||||
|
||||
@Autowired
|
||||
private AdminService adminService;
|
||||
|
||||
@Autowired
|
||||
private DashboardService dashboardService;
|
||||
|
||||
/**
|
||||
* 分页查询管理员
|
||||
*/
|
||||
@Operation(summary = "分页查询管理员", description = "分页查询管理员列表")
|
||||
@GetMapping("/page")
|
||||
public Result<PageResult<AdminResponse>> getPage(@Validated AdminPageRequest request) {
|
||||
PageResult<AdminResponse> pageResult = adminService.getPageWithResponse(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取管理员
|
||||
*/
|
||||
@Operation(summary = "根据ID获取管理员", description = "根据ID获取管理员详情")
|
||||
@GetMapping("/detail")
|
||||
public Result<AdminResponse> getById(
|
||||
@Parameter(description = "管理员ID", required = true)
|
||||
@RequestParam String id) {
|
||||
AdminResponse response = adminService.getAdminResponseById(id);
|
||||
if (response == null) {
|
||||
return Result.notFound("管理员不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建管理员
|
||||
*/
|
||||
@Operation(summary = "创建管理员", description = "创建新的管理员")
|
||||
@PostMapping("/create")
|
||||
public Result<AdminResponse> create(@RequestBody @Validated AdminCreateRequest request) {
|
||||
AdminResponse response = adminService.createAdminWithResponse(request);
|
||||
if (response == null) {
|
||||
return Result.error("创建失败");
|
||||
}
|
||||
return Result.success("创建成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新管理员
|
||||
*/
|
||||
@Operation(summary = "更新管理员", description = "更新指定管理员")
|
||||
@PutMapping("/update")
|
||||
public Result<AdminResponse> update(@RequestBody @Validated AdminUpdateRequest request) {
|
||||
AdminResponse response = adminService.updateAdminWithResponse(request);
|
||||
if (response == null) {
|
||||
return Result.error("更新失败");
|
||||
}
|
||||
return Result.success("更新成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除管理员
|
||||
*/
|
||||
@Operation(summary = "删除管理员", description = "删除指定管理员")
|
||||
@DeleteMapping("/delete")
|
||||
public Result<Void> delete(
|
||||
@Parameter(description = "管理员ID", required = true)
|
||||
@RequestParam String id) {
|
||||
boolean deleted = adminService.removeById(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success("删除成功", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置管理员密码(超级管理员操作)
|
||||
*/
|
||||
@Operation(summary = "重置管理员密码", description = "超级管理员重置指定管理员的密码")
|
||||
@PostMapping("/changePassword")
|
||||
public Result<Void> changePassword(@Validated @RequestBody AdminResetPasswordRequest request) {
|
||||
adminService.resetPassword(request.getId(), request.getNewPassword());
|
||||
return Result.success("密码重置成功", null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取仪表盘统计数据
|
||||
*/
|
||||
@Operation(summary = "获取仪表盘统计数据", description = "获取管理后台仪表盘的统计数据,包括用户、内容、AI服务和系统统计")
|
||||
@GetMapping("/dashboard/stats")
|
||||
public Result<DashboardStatsResponse> getDashboardStats() {
|
||||
DashboardStatsResponse stats = dashboardService.getDashboardStats();
|
||||
return Result.success("获取成功", stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户统计数据
|
||||
*/
|
||||
@Operation(summary = "获取用户统计数据", description = "获取用户相关的统计数据")
|
||||
@GetMapping("/dashboard/user-stats")
|
||||
public Result<DashboardStatsResponse.UserStats> getUserStats() {
|
||||
DashboardStatsResponse.UserStats userStats = dashboardService.getUserStats();
|
||||
return Result.success("获取成功", userStats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取内容统计数据
|
||||
*/
|
||||
@Operation(summary = "获取内容统计数据", description = "获取内容相关的统计数据")
|
||||
@GetMapping("/dashboard/content-stats")
|
||||
public Result<DashboardStatsResponse.ContentStats> getContentStats() {
|
||||
DashboardStatsResponse.ContentStats contentStats = dashboardService.getContentStats();
|
||||
return Result.success("获取成功", contentStats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取AI服务统计数据
|
||||
*/
|
||||
@Operation(summary = "获取AI服务统计数据", description = "获取AI服务相关的统计数据")
|
||||
@GetMapping("/dashboard/ai-stats")
|
||||
public Result<DashboardStatsResponse.AiServiceStats> getAiServiceStats() {
|
||||
DashboardStatsResponse.AiServiceStats aiStats = dashboardService.getAiServiceStats();
|
||||
return Result.success("获取成功", aiStats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统统计数据
|
||||
*/
|
||||
@Operation(summary = "获取系统统计数据", description = "获取系统相关的统计数据")
|
||||
@GetMapping("/dashboard/system-stats")
|
||||
public Result<DashboardStatsResponse.SystemStats> getSystemStats() {
|
||||
DashboardStatsResponse.SystemStats systemStats = dashboardService.getSystemStats();
|
||||
return Result.success("获取成功", systemStats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户增长趋势数据
|
||||
*/
|
||||
@Operation(summary = "获取用户增长趋势数据", description = "获取指定天数的用户增长趋势数据")
|
||||
@GetMapping("/dashboard/user-growth-trends")
|
||||
public Result<List<DashboardStatsResponse.UserGrowthTrend>> getUserGrowthTrends(
|
||||
@Parameter(description = "查询天数", required = false)
|
||||
@RequestParam(defaultValue = "7") int days) {
|
||||
List<DashboardStatsResponse.UserGrowthTrend> trends = dashboardService.getUserGrowthTrends(days);
|
||||
return Result.success("获取成功", trends);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近登录用户
|
||||
*/
|
||||
@Operation(summary = "获取最近登录用户", description = "获取最近登录的用户列表")
|
||||
@GetMapping("/dashboard/recent-logins")
|
||||
public Result<List<DashboardStatsResponse.RecentLogin>> getRecentLogins(
|
||||
@Parameter(description = "返回数量限制", required = false)
|
||||
@RequestParam(defaultValue = "10") int limit) {
|
||||
List<DashboardStatsResponse.RecentLogin> recentLogins = dashboardService.getRecentLogins(limit);
|
||||
return Result.success("获取成功", recentLogins);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 AI 配置调用次数统计
|
||||
*/
|
||||
@Operation(summary = "获取AI配置调用次数统计", description = "按 t_ai_config 的 workflow_id 关联 t_coze_api_call 统计调用次数并按次数倒序返回")
|
||||
@GetMapping(value = "/dashboard/aiConfigCallStats")
|
||||
public Result<AiConfigCallStatsResponse> getAiConfigCallStats(@Validated AiConfigCallStatsRequest request) {
|
||||
AiConfigCallStatsResponse response = dashboardService.getAiConfigCallStats(request);
|
||||
return Result.success("获取成功", response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.AiChatRequest;
|
||||
import com.emotion.dto.request.AiSummaryRequest;
|
||||
import com.emotion.dto.request.GuestChatRequest;
|
||||
import com.emotion.dto.request.ConversationCreateRequest;
|
||||
import com.emotion.dto.request.ChatStatsRequest;
|
||||
import com.emotion.dto.response.AiChatResponse;
|
||||
import com.emotion.dto.response.AiSummaryResponse;
|
||||
import com.emotion.dto.response.AiStatusResponse;
|
||||
import com.emotion.dto.response.ChatStatsResponse;
|
||||
import com.emotion.dto.response.GuestChatResponse;
|
||||
import com.emotion.dto.response.GuestUserInfoResponse;
|
||||
import com.emotion.dto.response.ConversationResponse;
|
||||
import com.emotion.service.AiChatService;
|
||||
import com.emotion.util.UserContextUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* AI聊天控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/ai")
|
||||
@Tag(name = "AI 聊天", description = "AI 智能对话聊天接口,支持普通聊天、总结、状态查询、访客模式等")
|
||||
public class AiChatController {
|
||||
|
||||
@Autowired
|
||||
private AiChatService aiChatService;
|
||||
|
||||
/**
|
||||
* 发送聊天消息
|
||||
*/
|
||||
@Operation(summary = "发送 AI 聊天消息", description = "向 AI 发送聊天消息,返回 AI 回复。支持指定会话 ID 和用户 ID。")
|
||||
@PostMapping("/chat")
|
||||
public Result<AiChatResponse> sendChatMessage(@Valid @RequestBody AiChatRequest request) {
|
||||
log.info("收到AI聊天请求: conversationId={}, userId={}, message={}",
|
||||
request.getConversationId(), request.getUserId(), request.getMessage());
|
||||
|
||||
// 调用AI服务
|
||||
AiChatResponse response = aiChatService.sendChatMessage(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成对话总结
|
||||
*/
|
||||
@Operation(summary = "获取 AI 聊天总结", description = "获取指定会话的 AI 聊天总结,概括对话要点。")
|
||||
@PostMapping("/summary")
|
||||
public Result<AiSummaryResponse> generateSummary(@Valid @RequestBody AiSummaryRequest request) {
|
||||
log.info("收到对话总结请求: conversationId={}, userId={}", request.getConversationId(), request.getUserId());
|
||||
|
||||
// 调用AI总结服务
|
||||
AiSummaryResponse response = aiChatService.generateConversationSummary(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取AI服务状态
|
||||
*/
|
||||
@Operation(summary = "获取 AI 状态", description = "获取当前 AI 服务的运行状态信息。")
|
||||
@GetMapping("/status")
|
||||
public Result<AiStatusResponse> getServiceStatus() {
|
||||
log.info("获取AI服务状态");
|
||||
|
||||
AiStatusResponse response = aiChatService.getServiceStatus();
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取聊天记录统计
|
||||
*/
|
||||
@Operation(summary = "获取聊天统计数据", description = "获取指定时间范围内的聊天统计数据,包括消息数、Token 消耗等。")
|
||||
@GetMapping("/stats")
|
||||
public Result<ChatStatsResponse> getChatStats(@Valid ChatStatsRequest request) {
|
||||
log.info("获取聊天统计: userId={}, conversationId={}", request.getUserId(), request.getConversationId());
|
||||
|
||||
ChatStatsResponse response = aiChatService.getChatStats(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 访客聊天(不需要登录)
|
||||
*/
|
||||
@Operation(summary = "访客模式聊天", description = "未登录用户通过访客模式与 AI 进行对话,无需 token 认证。")
|
||||
@PostMapping("/guestChat")
|
||||
public Result<GuestChatResponse> guestChat(@Valid @RequestBody GuestChatRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
String clientIp = UserContextUtils.getClientIpAddress(httpRequest);
|
||||
log.info("访客聊天请求: {}, IP: {}", request.getMessage(), clientIp);
|
||||
|
||||
GuestChatResponse response = aiChatService.guestChat(request, clientIp);
|
||||
return Result.success("发送成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取访客用户信息
|
||||
*/
|
||||
@Operation(summary = "获取访客用户信息", description = "根据访客 ID 获取对应的用户信息。")
|
||||
@GetMapping("/guestUserInfo")
|
||||
public Result<GuestUserInfoResponse> getGuestUserInfo(HttpServletRequest request) {
|
||||
String clientIp = UserContextUtils.getClientIpAddress(request);
|
||||
log.info("获取访客用户信息: IP={}", clientIp);
|
||||
|
||||
GuestUserInfoResponse response = aiChatService.getGuestUserInfo(clientIp);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建对话
|
||||
*/
|
||||
@Operation(summary = "创建对话会话", description = "为当前用户创建一个新的 AI 对话会话。")
|
||||
@PostMapping("/createConversation")
|
||||
public Result<ConversationResponse> createConversation(@Valid @RequestBody ConversationCreateRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
log.info("创建对话请求: userId={}, title={}", request.getUserId(), request.getTitle());
|
||||
|
||||
String clientIp = UserContextUtils.getClientIpAddress(httpRequest);
|
||||
ConversationResponse response = aiChatService.createConversation(request, clientIp);
|
||||
return Result.success("创建成功", response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.aiconfig.*;
|
||||
import com.emotion.dto.response.aiconfig.AiConfigResponse;
|
||||
import com.emotion.service.AiConfigService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* AI配置控制器
|
||||
*
|
||||
* @author system
|
||||
* @date 2025-10-30
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/aiConfig")
|
||||
@Tag(name = "AI配置管理", description = "AI配置的增删改查功能")
|
||||
public class AiConfigController {
|
||||
|
||||
@Autowired
|
||||
private AiConfigService aiConfigService;
|
||||
|
||||
/**
|
||||
* 分页查询AI配置
|
||||
*/
|
||||
@Operation(summary = "分页查询AI配置", description = "分页查询AI配置列表")
|
||||
@GetMapping("/page")
|
||||
public Result<PageResult<AiConfigResponse>> getPage(@Validated AiConfigPageRequest request) {
|
||||
PageResult<AiConfigResponse> pageResult = aiConfigService.getPageWithResponse(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取AI配置
|
||||
*/
|
||||
@Operation(summary = "根据ID获取AI配置", description = "根据ID获取AI配置详情")
|
||||
@GetMapping("/detail")
|
||||
public Result<AiConfigResponse> getById(@RequestParam String id) {
|
||||
AiConfigResponse response = aiConfigService.getAiConfigResponseById(id);
|
||||
if (response == null) {
|
||||
return Result.notFound("AI配置不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建AI配置
|
||||
*/
|
||||
@Operation(summary = "创建AI配置", description = "创建新的AI配置")
|
||||
@PostMapping("/create")
|
||||
public Result<AiConfigResponse> create(@RequestBody @Validated AiConfigCreateRequest request) {
|
||||
AiConfigResponse response = aiConfigService.createAiConfigWithResponse(request);
|
||||
if (response == null) {
|
||||
return Result.error("创建失败");
|
||||
}
|
||||
return Result.success("创建成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新AI配置
|
||||
*/
|
||||
@Operation(summary = "更新AI配置", description = "更新指定AI配置")
|
||||
@PutMapping("/update")
|
||||
public Result<AiConfigResponse> update(@RequestBody @Validated AiConfigUpdateRequest request) {
|
||||
AiConfigResponse response = aiConfigService.updateAiConfigWithResponse(request);
|
||||
if (response == null) {
|
||||
return Result.error("更新失败");
|
||||
}
|
||||
return Result.success("更新成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除AI配置
|
||||
*/
|
||||
@Operation(summary = "删除AI配置", description = "删除指定AI配置")
|
||||
@DeleteMapping("/delete")
|
||||
public Result<Void> delete(@RequestParam String id) {
|
||||
boolean deleted = aiConfigService.removeById(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置类型查询AI配置
|
||||
*/
|
||||
@Operation(summary = "根据配置类型查询AI配置", description = "根据配置类型查询AI配置列表")
|
||||
@GetMapping("/byConfigType")
|
||||
public Result<List<AiConfigResponse>> getByConfigType(@RequestParam String configType) {
|
||||
List<AiConfigResponse> responses = aiConfigService.getByConfigTypeWithResponse(configType);
|
||||
return Result.success(responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据服务提供商查询AI配置
|
||||
*/
|
||||
@Operation(summary = "根据服务提供商查询AI配置", description = "根据服务提供商查询AI配置列表")
|
||||
@GetMapping("/byProvider")
|
||||
public Result<List<AiConfigResponse>> getByProvider(@RequestParam String provider) {
|
||||
List<AiConfigResponse> responses = aiConfigService.getByProviderWithResponse(provider);
|
||||
return Result.success(responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据使用场景查询AI配置
|
||||
*/
|
||||
@Operation(summary = "根据使用场景查询AI配置", description = "根据使用场景查询AI配置列表")
|
||||
@GetMapping("/byUsageScenario")
|
||||
public Result<List<AiConfigResponse>> getByUsageScenario(@RequestParam String usageScenario) {
|
||||
List<AiConfigResponse> responses = aiConfigService.getByUsageScenarioWithResponse(usageScenario);
|
||||
return Result.success(responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据环境查询AI配置
|
||||
*/
|
||||
@Operation(summary = "根据环境查询AI配置", description = "根据环境查询AI配置列表")
|
||||
@GetMapping("/byEnvironment")
|
||||
public Result<List<AiConfigResponse>> getByEnvironment(@RequestParam String environment) {
|
||||
List<AiConfigResponse> responses = aiConfigService.getByEnvironmentWithResponse(environment);
|
||||
return Result.success(responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询已启用的AI配置
|
||||
*/
|
||||
@Operation(summary = "查询已启用的AI配置", description = "查询已启用的AI配置列表")
|
||||
@GetMapping("/enabled")
|
||||
public Result<List<AiConfigResponse>> getEnabledConfigs() {
|
||||
List<AiConfigResponse> responses = aiConfigService.getEnabledConfigsWithResponse();
|
||||
return Result.success(responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询已禁用的AI配置
|
||||
*/
|
||||
@Operation(summary = "查询已禁用的AI配置", description = "查询已禁用的AI配置列表")
|
||||
@GetMapping("/disabled")
|
||||
public Result<List<AiConfigResponse>> getDisabledConfigs() {
|
||||
List<AiConfigResponse> responses = aiConfigService.getDisabledConfigsWithResponse();
|
||||
return Result.success(responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询默认配置
|
||||
*/
|
||||
@Operation(summary = "查询默认配置", description = "查询默认配置列表")
|
||||
@GetMapping("/default")
|
||||
public Result<List<AiConfigResponse>> getDefaultConfigs() {
|
||||
List<AiConfigResponse> responses = aiConfigService.getDefaultConfigsWithResponse();
|
||||
return Result.success(responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置键值查询AI配置
|
||||
*/
|
||||
@Operation(summary = "根据配置键值查询AI配置", description = "根据配置键值查询AI配置")
|
||||
@GetMapping("/byConfigKey")
|
||||
public Result<AiConfigResponse> getByConfigKey(@RequestParam String configKey) {
|
||||
AiConfigResponse response = aiConfigService.getByConfigKeyWithResponse(configKey);
|
||||
if (response == null) {
|
||||
return Result.notFound("AI配置不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用AI配置
|
||||
*/
|
||||
@Operation(summary = "启用AI配置", description = "启用指定AI配置")
|
||||
@PutMapping("/enable")
|
||||
public Result<Void> enableConfig(@RequestParam String id) {
|
||||
boolean enabled = aiConfigService.enableConfig(id);
|
||||
if (!enabled) {
|
||||
return Result.error("启用失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用AI配置
|
||||
*/
|
||||
@Operation(summary = "禁用AI配置", description = "禁用指定AI配置")
|
||||
@PutMapping("/disable")
|
||||
public Result<Void> disableConfig(@RequestParam String id) {
|
||||
boolean disabled = aiConfigService.disableConfig(id);
|
||||
if (!disabled) {
|
||||
return Result.error("禁用失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置为默认配置
|
||||
*/
|
||||
@Operation(summary = "设置为默认配置", description = "设置指定AI配置为默认配置")
|
||||
@PutMapping("/setDefault")
|
||||
public Result<Void> setAsDefault(@RequestParam String id) {
|
||||
boolean set = aiConfigService.setAsDefault(id);
|
||||
if (!set) {
|
||||
return Result.error("设置默认配置失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消默认配置
|
||||
*/
|
||||
@Operation(summary = "取消默认配置", description = "取消指定AI配置的默认设置")
|
||||
@PutMapping("/unsetDefault")
|
||||
public Result<Void> unsetDefault(@RequestParam String id) {
|
||||
boolean unset = aiConfigService.unsetDefault(id);
|
||||
if (!unset) {
|
||||
return Result.error("取消默认配置失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据使用场景和环境查询最优配置
|
||||
*/
|
||||
@Operation(summary = "查询最优配置", description = "根据使用场景和环境查询最优配置")
|
||||
@GetMapping("/bestConfig")
|
||||
public Result<AiConfigResponse> getBestConfig(@RequestParam String usageScenario,
|
||||
@RequestParam String environment) {
|
||||
AiConfigResponse response = aiConfigService.getBestConfigWithResponse(usageScenario, environment);
|
||||
if (response == null) {
|
||||
return Result.notFound("未找到匹配的AI配置");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计已启用配置数量
|
||||
*/
|
||||
@Operation(summary = "统计已启用配置数量", description = "统计已启用配置数量")
|
||||
@GetMapping("/countEnabled")
|
||||
public Result<Long> countEnabledConfigs() {
|
||||
Long count = aiConfigService.countEnabledConfigs();
|
||||
return Result.success(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计已禁用配置数量
|
||||
*/
|
||||
@Operation(summary = "统计已禁用配置数量", description = "统计已禁用配置数量")
|
||||
@GetMapping("/countDisabled")
|
||||
public Result<Long> countDisabledConfigs() {
|
||||
Long count = aiConfigService.countDisabledConfigs();
|
||||
return Result.success(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计默认配置数量
|
||||
*/
|
||||
@Operation(summary = "统计默认配置数量", description = "统计默认配置数量")
|
||||
@GetMapping("/countDefault")
|
||||
public Result<Long> countDefaultConfigs() {
|
||||
Long count = aiConfigService.countDefaultConfigs();
|
||||
return Result.success(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据配置类型统计数量
|
||||
*/
|
||||
@Operation(summary = "根据配置类型统计数量", description = "根据配置类型统计数量")
|
||||
@GetMapping("/countByConfigType")
|
||||
public Result<Long> countByConfigType(@RequestParam String configType) {
|
||||
Long count = aiConfigService.countByConfigType(configType);
|
||||
return Result.success(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据服务提供商统计数量
|
||||
*/
|
||||
@Operation(summary = "根据服务提供商统计数量", description = "根据服务提供商统计数量")
|
||||
@GetMapping("/countByProvider")
|
||||
public Result<Long> countByProvider(@RequestParam String provider) {
|
||||
Long count = aiConfigService.countByProvider(provider);
|
||||
return Result.success(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试后更新AI配置
|
||||
*/
|
||||
@Operation(summary = "测试后更新AI配置", description = "从测试请求中解析参数并更新配置")
|
||||
@PutMapping("/updateFromTest")
|
||||
public Result<AiConfigResponse> updateFromTest(@RequestBody @Validated AiConfigTestUpdateRequest request) {
|
||||
AiConfigResponse response = aiConfigService.updateFromTestRequest(request);
|
||||
if (response == null) {
|
||||
return Result.error("更新失败,配置不存在");
|
||||
}
|
||||
return Result.success("更新成功", response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.alibaba.fastjson2.JSONObject;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.ai.AiCallLogQueryRequest;
|
||||
import com.emotion.dto.request.ai.AiRuntimeRequest;
|
||||
import javax.validation.Valid;
|
||||
import com.emotion.dto.response.ai.AiTestTemplateResponse;
|
||||
import com.emotion.dto.response.ai.AiRuntimeTestResponse;
|
||||
import com.emotion.dto.response.ai.AiStreamEvent;
|
||||
import com.emotion.entity.AiCallLog;
|
||||
import com.emotion.entity.AiEndpointConfig;
|
||||
import com.emotion.entity.AiProvider;
|
||||
import com.emotion.entity.AiSceneBinding;
|
||||
import com.emotion.service.AiCallLogService;
|
||||
import com.emotion.service.AiEndpointConfigService;
|
||||
import com.emotion.service.AiProviderService;
|
||||
import com.emotion.service.AiRuntimeService;
|
||||
import com.emotion.service.AiSceneBindingService;
|
||||
import com.emotion.util.UserContextHolder;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/ai")
|
||||
@Tag(name = "AI 路由管理", description = "AI 服务提供商路由管理接口,包括 Provider/Endpoint/Scene 的 CRUD、调用日志、运行时测试和流式测试")
|
||||
public class AiRoutingController {
|
||||
|
||||
private final AiProviderService providerService;
|
||||
private final AiEndpointConfigService endpointConfigService;
|
||||
private final AiSceneBindingService sceneBindingService;
|
||||
private final AiCallLogService callLogService;
|
||||
private final AiRuntimeService runtimeService;
|
||||
|
||||
public AiRoutingController(AiProviderService providerService,
|
||||
AiEndpointConfigService endpointConfigService,
|
||||
AiSceneBindingService sceneBindingService,
|
||||
AiCallLogService callLogService,
|
||||
AiRuntimeService runtimeService) {
|
||||
this.providerService = providerService;
|
||||
this.endpointConfigService = endpointConfigService;
|
||||
this.sceneBindingService = sceneBindingService;
|
||||
this.callLogService = callLogService;
|
||||
this.runtimeService = runtimeService;
|
||||
}
|
||||
|
||||
@Operation(summary = "查询 Provider 列表", description = "查询所有可见的 AI Provider 列表。")
|
||||
@GetMapping("/providers")
|
||||
public Result<List<AiProvider>> providers() {
|
||||
return Result.success(providerService.listVisible());
|
||||
}
|
||||
|
||||
@Operation(summary = "创建 Provider", description = "创建一个新的 AI Provider 配置。")
|
||||
@PostMapping("/providers")
|
||||
public Result<AiProvider> createProvider(@RequestBody AiProvider provider) {
|
||||
return Result.success(providerService.saveProvider(provider));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新 Provider", description = "更新已有的 AI Provider 配置。")
|
||||
@PutMapping("/providers")
|
||||
public Result<AiProvider> updateProvider(@RequestBody AiProvider provider) {
|
||||
return Result.success(providerService.updateProvider(provider));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除 Provider", description = "根据 ID 删除指定的 AI Provider 配置。")
|
||||
@DeleteMapping("/providers")
|
||||
public Result<Void> deleteProvider(@Parameter(description = "Provider ID", required = true) @RequestParam String id) {
|
||||
providerService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "查询 Endpoint 列表", description = "查询所有可见的 AI Endpoint 配置列表。")
|
||||
@GetMapping("/endpoints")
|
||||
public Result<List<AiEndpointConfig>> endpoints() {
|
||||
return Result.success(endpointConfigService.listVisible());
|
||||
}
|
||||
|
||||
@Operation(summary = "获取 Endpoint 测试模板", description = "根据 Endpoint ID 获取对应的测试模板。")
|
||||
@GetMapping("/endpoints/test-template")
|
||||
public Result<AiTestTemplateResponse> endpointTestTemplate(@Parameter(description = "Endpoint ID", required = true) @RequestParam String id) {
|
||||
return Result.success(runtimeService.buildEndpointTestTemplate(id));
|
||||
}
|
||||
|
||||
@Operation(summary = "创建 Endpoint", description = "创建一个新的 AI Endpoint 配置。")
|
||||
@PostMapping("/endpoints")
|
||||
public Result<AiEndpointConfig> createEndpoint(@RequestBody AiEndpointConfig endpoint) {
|
||||
return Result.success(endpointConfigService.saveEndpoint(endpoint));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新 Endpoint", description = "更新已有的 AI Endpoint 配置。")
|
||||
@PutMapping("/endpoints")
|
||||
public Result<AiEndpointConfig> updateEndpoint(@RequestBody AiEndpointConfig endpoint) {
|
||||
return Result.success(endpointConfigService.updateEndpoint(endpoint));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除 Endpoint", description = "根据 ID 删除指定的 AI Endpoint 配置。")
|
||||
@DeleteMapping("/endpoints")
|
||||
public Result<Void> deleteEndpoint(@Parameter(description = "Endpoint ID", required = true) @RequestParam String id) {
|
||||
endpointConfigService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "查询 Scene 列表", description = "查询所有可见的 AI Scene 绑定配置列表。")
|
||||
@GetMapping("/scenes")
|
||||
public Result<List<AiSceneBinding>> scenes() {
|
||||
return Result.success(sceneBindingService.listVisible());
|
||||
}
|
||||
|
||||
@Operation(summary = "获取 Scene 测试模板", description = "根据场景编码获取对应的测试模板。")
|
||||
@GetMapping("/scenes/test-template")
|
||||
public Result<AiTestTemplateResponse> sceneTestTemplate(@Parameter(description = "场景编码", required = true) @RequestParam String sceneCode) {
|
||||
return Result.success(runtimeService.buildSceneTestTemplate(sceneCode));
|
||||
}
|
||||
|
||||
@Operation(summary = "创建 Scene", description = "创建一个新的 AI Scene 绑定配置。")
|
||||
@PostMapping("/scenes")
|
||||
public Result<AiSceneBinding> createScene(@RequestBody AiSceneBinding scene) {
|
||||
if (scene.getIsEnabled() == null) {
|
||||
scene.setIsEnabled(1);
|
||||
}
|
||||
if (scene.getRequiredStream() == null) {
|
||||
scene.setRequiredStream(1);
|
||||
}
|
||||
sceneBindingService.save(scene);
|
||||
return Result.success(scene);
|
||||
}
|
||||
|
||||
@Operation(summary = "更新 Scene", description = "更新已有的 AI Scene 绑定配置。")
|
||||
@PutMapping("/scenes")
|
||||
public Result<AiSceneBinding> updateScene(@RequestBody AiSceneBinding scene) {
|
||||
sceneBindingService.updateById(scene);
|
||||
return Result.success(sceneBindingService.getById(scene.getId()));
|
||||
}
|
||||
|
||||
@Operation(summary = "删除 Scene", description = "根据 ID 删除指定的 AI Scene 绑定配置。")
|
||||
@DeleteMapping("/scenes")
|
||||
public Result<Void> deleteScene(@Parameter(description = "Scene ID", required = true) @RequestParam String id) {
|
||||
sceneBindingService.removeById(id);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
@Operation(summary = "查询调用日志", description = "查询 AI 调用日志列表,支持限制返回数量。")
|
||||
@GetMapping("/call-logs")
|
||||
public Result<List<AiCallLog>> callLogs(@Parameter(description = "返回数量限制") @RequestParam(required = false) Integer limit) {
|
||||
return Result.success(callLogService.latest(limit));
|
||||
}
|
||||
|
||||
@Operation(summary = "分页查询调用日志", description = "分页查询 AI 调用日志,支持多条件筛选和关键词搜索。")
|
||||
@PostMapping("/call-logs")
|
||||
public Result<PageResult<AiCallLog>> queryCallLogs(@RequestBody @Valid AiCallLogQueryRequest request) {
|
||||
return Result.success(callLogService.query(request));
|
||||
}
|
||||
|
||||
@Operation(summary = "查询运行时调用结果", description = "用户端根据 requestId 查询刚刚触发的 AI 调用结果,用于流式连接异常后的结果恢复。")
|
||||
@GetMapping("/runtime/result")
|
||||
public Result<AiCallLog> runtimeResult(@RequestParam String requestId) {
|
||||
AiCallLog log = callLogService.findByRequestId(requestId, UserContextHolder.getCurrentUserId());
|
||||
if (log == null) {
|
||||
return Result.notFound("AI 调用结果未生成");
|
||||
}
|
||||
return Result.success(log);
|
||||
}
|
||||
|
||||
@Operation(summary = "运行时测试", description = "对指定的 AI 配置进行运行时连通性测试,支持同步和流式模式。")
|
||||
@PostMapping("/runtime/test")
|
||||
public Result<AiRuntimeTestResponse> runtimeTest(@RequestBody JSONObject payload) {
|
||||
AiRuntimeRequest request = withCurrentUser(AiRuntimeRequest.fromPayload(payload));
|
||||
return Result.success(runtimeService.test(request));
|
||||
}
|
||||
|
||||
@Operation(summary = "流式运行时测试", description = "对指定的 AI 配置进行流式运行时连通性测试,以 SSE 事件流返回结果。")
|
||||
@PostMapping("/runtime/stream")
|
||||
public SseEmitter runtimeStream(@RequestBody JSONObject payload) {
|
||||
AiRuntimeRequest request = withCurrentUser(AiRuntimeRequest.fromPayload(payload));
|
||||
SseEmitter emitter = new SseEmitter(0L);
|
||||
CompletableFuture.runAsync(() -> {
|
||||
runtimeService.invokeStream(request, event -> sendEvent(emitter, event));
|
||||
emitter.complete();
|
||||
}).exceptionally(error -> {
|
||||
sendEvent(emitter, AiStreamEvent.error("AI_STREAM_INTERRUPTED", error.getMessage()));
|
||||
emitter.completeWithError(error);
|
||||
return null;
|
||||
});
|
||||
return emitter;
|
||||
}
|
||||
|
||||
@Operation(summary = "Endpoint 运行时测试", description = "对指定的 Endpoint 进行运行时连通性测试。")
|
||||
@PostMapping("/endpoint/test")
|
||||
public Result<AiRuntimeTestResponse> endpointTest(@RequestBody JSONObject payload) {
|
||||
String endpointId = payload.getString("endpointId");
|
||||
JSONObject inputs = payload.getJSONObject("inputs");
|
||||
Map<String, Object> inputMap = inputs == null ? Map.of() : inputs;
|
||||
return Result.success(runtimeService.testEndpoint(endpointId, inputMap));
|
||||
}
|
||||
|
||||
@Operation(summary = "Endpoint 流式测试", description = "对指定的 Endpoint 进行流式运行时测试,以 SSE 事件流返回结果。")
|
||||
@PostMapping("/endpoint/stream")
|
||||
public SseEmitter endpointStream(@RequestBody JSONObject payload) {
|
||||
String endpointId = payload.getString("endpointId");
|
||||
JSONObject inputs = payload.getJSONObject("inputs");
|
||||
Map<String, Object> inputMap = inputs == null ? Map.of() : inputs;
|
||||
SseEmitter emitter = new SseEmitter(0L);
|
||||
CompletableFuture.runAsync(() -> {
|
||||
runtimeService.invokeEndpointStream(endpointId, inputMap, event -> sendEvent(emitter, event));
|
||||
emitter.complete();
|
||||
}).exceptionally(error -> {
|
||||
sendEvent(emitter, AiStreamEvent.error("AI_ENDPOINT_TEST_INTERRUPTED", error.getMessage()));
|
||||
emitter.completeWithError(error);
|
||||
return null;
|
||||
});
|
||||
return emitter;
|
||||
}
|
||||
|
||||
private AiRuntimeRequest withCurrentUser(AiRuntimeRequest request) {
|
||||
request.setUserId(UserContextHolder.getCurrentUserId());
|
||||
request.setUserName(UserContextHolder.getCurrentUsername());
|
||||
request.setUserType(UserContextHolder.getCurrentUserType());
|
||||
if (!org.springframework.util.StringUtils.hasText(request.getRequestId())) {
|
||||
request.setRequestId(UserContextHolder.getRequestId());
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
private void sendEvent(SseEmitter emitter, AiStreamEvent event) {
|
||||
try {
|
||||
emitter.send(SseEmitter.event()
|
||||
.name(event.getType())
|
||||
.data(event));
|
||||
} catch (IOException e) {
|
||||
log.warn("AI stream client disconnected: {}", e.getMessage());
|
||||
throw new IllegalStateException("AI_STREAM_CLIENT_DISCONNECTED", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.analytics.AnalyticsEventBatchRequest;
|
||||
import com.emotion.dto.response.analytics.AnalyticsBatchResponse;
|
||||
import com.emotion.service.AnalyticsService;
|
||||
import com.emotion.util.JwtUtil;
|
||||
import com.emotion.util.UserContextHolder;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.util.StringUtils;
|
||||
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 javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.Valid;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/analytics")
|
||||
@Tag(name = "事件分析", description = "前端行为事件上报和分析接口")
|
||||
public class AnalyticsController {
|
||||
|
||||
@Autowired
|
||||
private AnalyticsService analyticsService;
|
||||
|
||||
@Autowired
|
||||
private JwtUtil jwtUtil;
|
||||
|
||||
@PostMapping("/events/batch")
|
||||
public Result<AnalyticsBatchResponse> batch(@Valid @RequestBody AnalyticsEventBatchRequest request,
|
||||
HttpServletRequest servletRequest) {
|
||||
bindOptionalUser(servletRequest);
|
||||
try {
|
||||
return Result.success(analyticsService.ingestBatch(request));
|
||||
} finally {
|
||||
UserContextHolder.clear();
|
||||
}
|
||||
}
|
||||
|
||||
private void bindOptionalUser(HttpServletRequest request) {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
if (!StringUtils.hasText(authHeader) || !authHeader.startsWith("Bearer ")) {
|
||||
return;
|
||||
}
|
||||
|
||||
String token = authHeader.substring(7);
|
||||
if (!jwtUtil.validateToken(token)) {
|
||||
return;
|
||||
}
|
||||
|
||||
UserContextHolder.setCurrentUserId(jwtUtil.getUserIdFromToken(token));
|
||||
UserContextHolder.setCurrentUsername(jwtUtil.getUsernameFromToken(token));
|
||||
UserContextHolder.setCurrentToken(token);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.ApiEndpointListRequest;
|
||||
import com.emotion.dto.response.ApiEndpointDetailResponse;
|
||||
import com.emotion.dto.response.ApiEndpointItemResponse;
|
||||
import com.emotion.service.ApiEndpointService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 接口端点控制器
|
||||
*
|
||||
* @author Peanut
|
||||
* @date 2026-05-23
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/admin/endpoint")
|
||||
@Tag(name = "接口管理", description = "接口发现、同步和测试")
|
||||
public class ApiEndpointController {
|
||||
|
||||
@Autowired
|
||||
private ApiEndpointService apiEndpointService;
|
||||
|
||||
@Operation(summary = "分页查询接口列表", description = "支持关键词、方法、标签过滤")
|
||||
@PostMapping("/list")
|
||||
public Result<PageResult<ApiEndpointItemResponse>> list(@Valid @RequestBody ApiEndpointListRequest request) {
|
||||
IPage<ApiEndpointItemResponse> page = apiEndpointService.getPage(request);
|
||||
PageResult<ApiEndpointItemResponse> result = new PageResult<>(page);
|
||||
return Result.success(result);
|
||||
}
|
||||
|
||||
@Operation(summary = "查询接口详情", description = "返回接口详情和参数列表")
|
||||
@GetMapping("/detail")
|
||||
public Result<ApiEndpointDetailResponse> detail(@RequestParam String operationId) {
|
||||
ApiEndpointDetailResponse response = apiEndpointService.getDetail(operationId);
|
||||
if (response == null) {
|
||||
return Result.notFound("接口不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "手动触发同步", description = "异步执行,返回 taskId")
|
||||
@PostMapping("/sync")
|
||||
public Result<Map<String, String>> sync() {
|
||||
Map<String, String> result = new HashMap<>();
|
||||
result.put("taskId", "sync-" + System.currentTimeMillis());
|
||||
new Thread(() -> {
|
||||
try {
|
||||
apiEndpointService.syncFromOpenApi();
|
||||
} catch (Exception e) {
|
||||
// 异常已在 service 层记录
|
||||
}
|
||||
}).start();
|
||||
return Result.success("同步任务已提交", result);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.ApiTestProxyRequest;
|
||||
import com.emotion.dto.response.ApiTestProxyResponse;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.http.*;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
import org.springframework.web.client.HttpClientErrorException;
|
||||
import org.springframework.web.client.HttpServerErrorException;
|
||||
import org.springframework.web.client.ResourceAccessException;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 接口代理测试控制器
|
||||
* 仅允许转发到 /api/* 路径,避免 SSRF
|
||||
*
|
||||
* @author Peanut
|
||||
* @date 2026-05-23
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/admin/endpoint")
|
||||
@Tag(name = "接口管理", description = "接口测试代理")
|
||||
public class ApiTestProxyController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(ApiTestProxyController.class);
|
||||
private static final int DEFAULT_TIMEOUT = 30;
|
||||
private static final int MAX_RAW_BODY_LENGTH = 2000;
|
||||
|
||||
@Value("${server.port:19089}")
|
||||
private int serverPort;
|
||||
|
||||
@Autowired
|
||||
private RestTemplate restTemplate;
|
||||
|
||||
@Autowired
|
||||
private ObjectMapper objectMapper;
|
||||
|
||||
@Operation(summary = "代理测试请求", description = "转发请求到本地后端并返回响应")
|
||||
@PostMapping("/test")
|
||||
public Result<ApiTestProxyResponse> test(@Valid @RequestBody ApiTestProxyRequest request) {
|
||||
if (!request.getPath().startsWith("/api/")) {
|
||||
return Result.error("仅允许代理 /api/* 路径的请求");
|
||||
}
|
||||
|
||||
String url = "http://127.0.0.1:" + serverPort + request.getPath();
|
||||
int timeout = request.getTimeoutSeconds() != null ? request.getTimeoutSeconds() : DEFAULT_TIMEOUT;
|
||||
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||
if (request.getHeaders() != null) {
|
||||
for (Map.Entry<String, String> entry : request.getHeaders().entrySet()) {
|
||||
headers.set(entry.getKey(), entry.getValue());
|
||||
}
|
||||
}
|
||||
|
||||
// Append query params to URL if present
|
||||
if (request.getParams() != null && !request.getParams().isEmpty()) {
|
||||
StringBuilder sb = new StringBuilder(url);
|
||||
sb.append("?");
|
||||
for (Map.Entry<String, String> entry : request.getParams().entrySet()) {
|
||||
sb.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
|
||||
}
|
||||
url = sb.substring(0, sb.length() - 1);
|
||||
}
|
||||
|
||||
Object body = null;
|
||||
if (request.getBody() != null && !request.getBody().isBlank()) {
|
||||
try {
|
||||
body = objectMapper.readValue(request.getBody(), Object.class);
|
||||
} catch (Exception e) {
|
||||
return Result.error("请求体 JSON 格式错误: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
HttpMethod httpMethod = HttpMethod.valueOf(request.getMethod().toUpperCase());
|
||||
HttpEntity<Object> entity = new HttpEntity<>(body, headers);
|
||||
|
||||
long startTime = System.currentTimeMillis();
|
||||
ApiTestProxyResponse response = new ApiTestProxyResponse();
|
||||
|
||||
try {
|
||||
ResponseEntity<String> rawResponse = restTemplate.exchange(url, httpMethod, entity, String.class);
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
|
||||
response.setStatus(rawResponse.getStatusCodeValue());
|
||||
response.setDuration(duration);
|
||||
|
||||
try {
|
||||
response.setBody(objectMapper.readValue(rawResponse.getBody(), Object.class));
|
||||
} catch (Exception e) {
|
||||
String rawBody = rawResponse.getBody();
|
||||
if (rawBody != null && rawBody.length() > MAX_RAW_BODY_LENGTH) {
|
||||
rawBody = rawBody.substring(0, MAX_RAW_BODY_LENGTH) + "\n... (已截断)";
|
||||
}
|
||||
if (rawBody != null) {
|
||||
rawBody = rawBody.replace("<", "<").replace(">", ">");
|
||||
}
|
||||
response.setRawBody(rawBody);
|
||||
}
|
||||
|
||||
Map<String, String> respHeaders = new HashMap<>();
|
||||
for (String key : rawResponse.getHeaders().keySet()) {
|
||||
respHeaders.put(key, rawResponse.getHeaders().getFirst(key));
|
||||
}
|
||||
response.setHeaders(respHeaders);
|
||||
|
||||
return Result.success(response);
|
||||
|
||||
} catch (ResourceAccessException e) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
if (e.getCause() instanceof SocketTimeoutException) {
|
||||
return Result.error("代理请求超时(" + timeout + "s),目标接口可能响应过慢或不可达");
|
||||
}
|
||||
return Result.error("代理请求失败: " + e.getMessage());
|
||||
} catch (HttpClientErrorException | HttpServerErrorException e) {
|
||||
long duration = System.currentTimeMillis() - startTime;
|
||||
response.setStatus(e.getStatusCode().value());
|
||||
response.setDuration(duration);
|
||||
String rawBody = e.getResponseBodyAsString();
|
||||
if (rawBody.length() > MAX_RAW_BODY_LENGTH) {
|
||||
rawBody = rawBody.substring(0, MAX_RAW_BODY_LENGTH) + "\n... (已截断)";
|
||||
}
|
||||
response.setRawBody(rawBody.replace("<", "<").replace(">", ">"));
|
||||
return Result.success(response);
|
||||
} catch (Exception e) {
|
||||
return Result.error("代理请求失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.response.asr.AsrTranscribeResponse;
|
||||
import com.emotion.service.AsrService;
|
||||
import com.emotion.util.UserContextHolder;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/asr")
|
||||
@Tag(name = "语音识别(ASR)", description = "语音转文字识别接口")
|
||||
public class AsrController {
|
||||
|
||||
private final AsrService asrService;
|
||||
|
||||
public AsrController(AsrService asrService) {
|
||||
this.asrService = asrService;
|
||||
}
|
||||
|
||||
@Operation(summary = "语音转文字", description = "上传音频文件并将其转换为文字内容。")
|
||||
@PostMapping("/transcribe")
|
||||
public Result<AsrTranscribeResponse> transcribe(@Parameter(description = "音频文件") @RequestPart("file") MultipartFile file) {
|
||||
if (UserContextHolder.getCurrentUserId() == null) {
|
||||
return Result.unauthorized();
|
||||
}
|
||||
try {
|
||||
return Result.success(asrService.transcribe(file));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Result.badRequest(e.getMessage());
|
||||
} catch (IllegalStateException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,203 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.LoginRequest;
|
||||
import com.emotion.dto.request.RegisterRequest;
|
||||
import com.emotion.dto.request.RefreshTokenRequest;
|
||||
import com.emotion.dto.request.ResetPasswordRequest;
|
||||
import com.emotion.dto.request.WechatLoginRequest;
|
||||
import com.emotion.dto.response.ResetPasswordResponse;
|
||||
|
||||
import com.emotion.dto.response.AuthResponse;
|
||||
import com.emotion.dto.response.CaptchaResponse;
|
||||
import com.emotion.dto.response.LoginConfigResponse;
|
||||
import com.emotion.dto.response.SmsCodeResponse;
|
||||
import com.emotion.dto.response.UserInfoResponse;
|
||||
import com.emotion.service.AuthService;
|
||||
import com.emotion.service.SystemConfigService;
|
||||
import com.emotion.service.TokenService;
|
||||
import com.emotion.util.UserContextUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* 认证控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/auth")
|
||||
@Tag(name = "认证管理", description = "用户注册、登录、验证码等认证相关接口")
|
||||
public class AuthController {
|
||||
|
||||
@Autowired
|
||||
private AuthService authService;
|
||||
|
||||
@Autowired
|
||||
private TokenService tokenService;
|
||||
|
||||
@Autowired
|
||||
private SystemConfigService systemConfigService;
|
||||
|
||||
/**
|
||||
* 用户登录(简化版:手机号+验证码,不存在则自动注册)
|
||||
*/
|
||||
@PostMapping("/login")
|
||||
@Operation(summary = "用户登录", description = "使用手机号和短信验证码登录,若用户不存在则自动注册")
|
||||
public Result<AuthResponse> login(@Valid @RequestBody LoginRequest request) {
|
||||
AuthResponse response = authService.login(request);
|
||||
return Result.success("登录成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册(简化版:仅需手机号、密码和短信验证码)
|
||||
*/
|
||||
@PostMapping("/wechat/login")
|
||||
@Operation(summary = "微信小程序登录", description = "使用微信小程序登录 code 换取 openid 并登录")
|
||||
public Result<AuthResponse> wechatLogin(@Valid @RequestBody WechatLoginRequest request) {
|
||||
AuthResponse response = authService.wechatLogin(request);
|
||||
return Result.success("登录成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取登录方式配置
|
||||
*/
|
||||
@GetMapping("/loginConfig")
|
||||
@Operation(summary = "获取登录方式配置", description = "返回当前启用的登录方式,供小程序登录页动态渲染")
|
||||
public Result<LoginConfigResponse> getLoginConfig() {
|
||||
LoginConfigResponse response = systemConfigService.getLoginConfig();
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
@PostMapping(value = "/register")
|
||||
@Operation(summary = "用户注册", description = "使用手机号、密码和短信验证码进行注册")
|
||||
public Result<AuthResponse> register(@Valid @RequestBody RegisterRequest request) {
|
||||
AuthResponse response = authService.register(request);
|
||||
return Result.success("注册成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码(手机号 + 验证码)
|
||||
*/
|
||||
@PostMapping(value = "/resetPassword")
|
||||
@Operation(summary = "重置密码", description = "通过手机号和验证码重置密码,验证码本期固定为123456")
|
||||
public Result<ResetPasswordResponse> resetPassword(@Valid @RequestBody ResetPasswordRequest request) {
|
||||
ResetPasswordResponse response = authService.resetPassword(request);
|
||||
return Result.success("重置密码成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户信息
|
||||
*/
|
||||
@GetMapping("/userInfo")
|
||||
@Operation(summary = "获取当前用户信息", description = "根据当前登录用户的Token获取用户详细信息")
|
||||
public Result<UserInfoResponse> getCurrentUserInfo(HttpServletRequest request) {
|
||||
UserInfoResponse userInfo = tokenService.getUserInfoByToken(request);
|
||||
return Result.success(userInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成验证码(图形验证码,用于登录)
|
||||
*/
|
||||
@GetMapping("/captcha")
|
||||
@Operation(summary = "获取图形验证码", description = "用于登录时的图形验证码")
|
||||
public Result<CaptchaResponse> generateCaptcha() {
|
||||
CaptchaResponse response = authService.generateCaptcha();
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取短信验证码(用于注册)
|
||||
*/
|
||||
@GetMapping("/sms-code")
|
||||
@Operation(summary = "获取短信验证码", description = "用于注册时的短信验证码")
|
||||
public Result<SmsCodeResponse> getSmsCode(
|
||||
@Parameter(description = "手机号", required = true)
|
||||
@RequestParam String phone) {
|
||||
SmsCodeResponse response = authService.sendSmsCode(phone);
|
||||
return Result.success("验证码已发送", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户登出
|
||||
*/
|
||||
@PostMapping("/logout")
|
||||
@Operation(summary = "用户登出", description = "退出登录,清除服务端Token")
|
||||
public Result<Void> logout(HttpServletRequest request) {
|
||||
authService.logoutByToken(request);
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新访问令牌
|
||||
*/
|
||||
@PostMapping("/refreshToken")
|
||||
@Operation(summary = "刷新访问令牌", description = "使用刷新令牌获取新的访问令牌和刷新令牌")
|
||||
public Result<AuthResponse> refreshToken(@Valid @RequestBody RefreshTokenRequest request) {
|
||||
AuthResponse response = authService.refreshToken(request.getRefreshToken());
|
||||
return Result.success("令牌刷新成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证访问令牌
|
||||
*/
|
||||
@GetMapping("/validateToken")
|
||||
@Operation(summary = "验证访问令牌", description = "验证当前Token是否有效")
|
||||
public Result<Boolean> validateToken(HttpServletRequest request) {
|
||||
boolean isValid = authService.validateToken(request);
|
||||
return Result.success(isValid);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户名(通过令牌)
|
||||
*/
|
||||
@GetMapping("/username")
|
||||
@Operation(summary = "获取用户名", description = "根据Token获取当前登录用户的用户名")
|
||||
public Result<String> getUsernameFromToken(HttpServletRequest request) {
|
||||
String username = tokenService.getUsernameByToken(request);
|
||||
return Result.success(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查账号是否存在
|
||||
*/
|
||||
@GetMapping("/checkAccount")
|
||||
@Operation(summary = "检查账号是否存在", description = "检查指定账号是否已被注册")
|
||||
public Result<Boolean> checkAccount(
|
||||
@Parameter(description = "账号(手机号/邮箱/用户名)", required = true)
|
||||
@RequestParam String account) {
|
||||
boolean exists = authService.existsByAccount(account);
|
||||
return Result.success(exists);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查邮箱是否存在
|
||||
*/
|
||||
@GetMapping("/checkEmail")
|
||||
@Operation(summary = "检查邮箱是否存在", description = "检查指定邮箱是否已被注册")
|
||||
public Result<Boolean> checkEmail(
|
||||
@Parameter(description = "邮箱地址", required = true)
|
||||
@RequestParam String email) {
|
||||
boolean exists = authService.existsByEmail(email);
|
||||
return Result.success(exists);
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查手机号是否存在
|
||||
*/
|
||||
@GetMapping("/checkPhone")
|
||||
@Operation(summary = "检查手机号是否存在", description = "检查指定手机号是否已被注册")
|
||||
public Result<Boolean> checkPhone(
|
||||
@Parameter(description = "手机号码", required = true)
|
||||
@RequestParam String phone) {
|
||||
boolean exists = authService.existsByPhone(phone);
|
||||
return Result.success(exists);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.dto.request.WebSocketRequest;
|
||||
import com.emotion.dto.websocket.ConnectRequest;
|
||||
import com.emotion.service.WebSocketService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.messaging.handler.annotation.MessageMapping;
|
||||
import org.springframework.messaging.handler.annotation.Payload;
|
||||
import org.springframework.messaging.simp.SimpMessageHeaderAccessor;
|
||||
import org.springframework.stereotype.Controller;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.security.Principal;
|
||||
|
||||
/**
|
||||
* WebSocket聊天控制器
|
||||
* 使用规范的请求对象封装参数,符合项目开发规则
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@Slf4j
|
||||
@Controller
|
||||
@MessageMapping("/chat")
|
||||
@Tag(name = "WebSocket 聊天", description = "基于 WebSocket 的实时 AI 聊天通信接口")
|
||||
public class ChatWebSocketController {
|
||||
|
||||
@Autowired
|
||||
private WebSocketService webSocketService;
|
||||
|
||||
/**
|
||||
* 处理聊天消息
|
||||
*
|
||||
* @param webSocketRequest WebSocket请求对象
|
||||
* @param headerAccessor 消息头访问器
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
@Operation(summary = "发送聊天消息", description = "向 AI 发送聊天消息,触发流式响应。")
|
||||
@MessageMapping("/send")
|
||||
public void handleChatMessage(@Valid @Payload WebSocketRequest webSocketRequest,
|
||||
SimpMessageHeaderAccessor headerAccessor,
|
||||
Principal principal) {
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
webSocketService.handleChatMessage(webSocketRequest, sessionId, principal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户连接
|
||||
*
|
||||
* @param connectRequest 连接请求对象
|
||||
* @param headerAccessor 消息头访问器
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
@Operation(summary = "用户连接", description = "处理用户 WebSocket 连接建立。")
|
||||
@MessageMapping("/connect")
|
||||
public void connectUser(@Payload ConnectRequest connectRequest,
|
||||
SimpMessageHeaderAccessor headerAccessor,
|
||||
Principal principal) {
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
webSocketService.handleUserConnect(connectRequest, sessionId, principal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理用户断开连接
|
||||
*
|
||||
* @param headerAccessor 消息头访问器
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
@Operation(summary = "用户断开连接", description = "处理用户 WebSocket 连接断开。")
|
||||
@MessageMapping("/disconnect")
|
||||
public void disconnectUser(SimpMessageHeaderAccessor headerAccessor, Principal principal) {
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
webSocketService.handleUserDisconnect(sessionId, principal);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理心跳消息
|
||||
*
|
||||
* @param headerAccessor 消息头访问器
|
||||
* @param principal 用户主体
|
||||
*/
|
||||
@Operation(summary = "心跳检测", description = "处理客户端心跳消息,保持 WebSocket 连接活跃。")
|
||||
@MessageMapping("/heartbeat")
|
||||
public void heartbeat(SimpMessageHeaderAccessor headerAccessor, Principal principal) {
|
||||
String sessionId = headerAccessor.getSessionId();
|
||||
webSocketService.handleHeartbeat(sessionId, principal);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.comment.CommentCreateRequest;
|
||||
import com.emotion.dto.request.comment.CommentUpdateRequest;
|
||||
import com.emotion.dto.request.comment.CommentPageRequest;
|
||||
import com.emotion.dto.response.comment.CommentResponse;
|
||||
import com.emotion.service.CommentService;
|
||||
import com.emotion.util.UserContextUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 评论控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/comment")
|
||||
@Tag(name = "评论管理", description = "评论的查询、创建、更新和删除接口")
|
||||
public class CommentController {
|
||||
|
||||
@Autowired
|
||||
private CommentService commentService;
|
||||
|
||||
/**
|
||||
* 分页查询评论
|
||||
*/
|
||||
@Operation(summary = "分页查询评论", description = "根据条件分页查询评论列表。")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<CommentResponse>> getCommentPage(@Validated CommentPageRequest request) {
|
||||
PageResult<CommentResponse> pageResult = commentService.getPage(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建评论
|
||||
*/
|
||||
@Operation(summary = "创建评论", description = "对指定目标发表一条新评论。")
|
||||
@PostMapping(value = "/create")
|
||||
public Result<CommentResponse> createComment(@RequestBody @Validated CommentCreateRequest request) {
|
||||
// 从上下文中获取当前用户ID,而不是直接使用请求中的用户ID
|
||||
String currentUserId = UserContextUtils.getCurrentUserId();
|
||||
if (currentUserId != null) {
|
||||
request.setUserId(currentUserId);
|
||||
}
|
||||
CommentResponse response = commentService.create(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新评论
|
||||
*/
|
||||
@Operation(summary = "更新评论", description = "修改已有评论的内容。")
|
||||
@PutMapping(value = "/update")
|
||||
public Result<CommentResponse> updateComment(@RequestBody @Validated CommentUpdateRequest request) {
|
||||
CommentResponse response = commentService.update(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除评论
|
||||
*/
|
||||
@Operation(summary = "删除评论", description = "删除指定的评论。")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<Void> deleteComment(@Parameter(description = "评论 ID", required = true) @RequestParam String id) {
|
||||
boolean deleted = commentService.delete(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取评论
|
||||
*/
|
||||
@Operation(summary = "获取评论详情", description = "根据 ID 获取评论的详细信息。")
|
||||
@GetMapping(value = "/detail")
|
||||
public Result<CommentResponse> getCommentById(@Parameter(description = "评论 ID", required = true) @RequestParam String id) {
|
||||
CommentResponse response = commentService.getById(id);
|
||||
if (response == null) {
|
||||
return Result.notFound("评论不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.community.CommunityPostCreateRequest;
|
||||
import com.emotion.dto.request.community.CommunityPostUpdateRequest;
|
||||
import com.emotion.dto.request.community.CommunityPostPageRequest;
|
||||
import com.emotion.dto.response.community.CommunityPostResponse;
|
||||
import com.emotion.service.CommunityPostService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 社区帖子控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/communityPost")
|
||||
@Tag(name = "社区帖子管理", description = "社区帖子的查询、创建、更新和删除接口")
|
||||
public class CommunityPostController {
|
||||
|
||||
@Autowired
|
||||
private CommunityPostService communityPostService;
|
||||
|
||||
/**
|
||||
* 分页查询帖子
|
||||
*/
|
||||
@Operation(summary = "分页查询社区帖子", description = "根据条件分页查询社区帖子列表。")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<CommunityPostResponse>> getPage(@Validated CommunityPostPageRequest request) {
|
||||
PageResult<CommunityPostResponse> pageResult = communityPostService.getPage(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取帖子
|
||||
*/
|
||||
@Operation(summary = "获取帖子详情", description = "根据 ID 获取社区帖子的详细信息。")
|
||||
@GetMapping(value = "/detail")
|
||||
public Result<CommunityPostResponse> getById(@Parameter(description = "帖子 ID", required = true) @RequestParam String id) {
|
||||
CommunityPostResponse response = communityPostService.getById(id);
|
||||
if (response == null) {
|
||||
return Result.notFound("帖子不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建帖子
|
||||
*/
|
||||
@Operation(summary = "创建社区帖子", description = "发布一篇新的社区帖子。")
|
||||
@PostMapping(value = "/create")
|
||||
public Result<CommunityPostResponse> create(@RequestBody @Validated CommunityPostCreateRequest request) {
|
||||
CommunityPostResponse response = communityPostService.create(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新帖子
|
||||
*/
|
||||
@Operation(summary = "更新社区帖子", description = "修改已有的社区帖子内容。")
|
||||
@PutMapping(value = "/update")
|
||||
public Result<CommunityPostResponse> update(@RequestBody @Validated CommunityPostUpdateRequest request) {
|
||||
CommunityPostResponse response = communityPostService.update(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除帖子
|
||||
*/
|
||||
@Operation(summary = "删除社区帖子", description = "删除指定的社区帖子。")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<Void> delete(@Parameter(description = "帖子 ID", required = true) @RequestParam String id) {
|
||||
boolean deleted = communityPostService.delete(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.ConversationCreateRequest;
|
||||
import com.emotion.dto.request.ConversationPageRequest;
|
||||
import com.emotion.dto.response.ConversationResponse;
|
||||
import com.emotion.service.ConversationService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 对话控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/conversation")
|
||||
@Tag(name = "会话管理", description = "AI 对话会话的查询、创建、更新、删除和状态管理接口")
|
||||
public class ConversationController {
|
||||
|
||||
@Autowired
|
||||
private ConversationService conversationService;
|
||||
|
||||
/**
|
||||
* 分页查询对话
|
||||
*/
|
||||
@Operation(summary = "分页查询会话", description = "分页查询当前用户的对话会话列表。")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<ConversationResponse>> getPage(@Valid ConversationPageRequest request) {
|
||||
PageResult<ConversationResponse> pageResult = conversationService.getPageWithResponse(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取对话
|
||||
*/
|
||||
@Operation(summary = "获取会话详情", description = "根据 ID 获取会话的详细信息。")
|
||||
@GetMapping(value = "/detail")
|
||||
public Result<ConversationResponse> getById(@Parameter(description = "会话 ID") @RequestParam String id) {
|
||||
ConversationResponse response = conversationService.getConversationResponseById(id);
|
||||
if (response == null) {
|
||||
return Result.notFound("对话不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建对话
|
||||
*/
|
||||
@Operation(summary = "创建会话", description = "创建一个新的 AI 对话会话。")
|
||||
@PostMapping(value = "/create")
|
||||
public Result<ConversationResponse> create(@Valid @RequestBody ConversationCreateRequest request,
|
||||
HttpServletRequest httpRequest) {
|
||||
ConversationResponse response = conversationService.createConversationWithResponse(request, httpRequest);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新对话
|
||||
*/
|
||||
@Operation(summary = "更新会话", description = "修改已有会话的标题等属性。")
|
||||
@PutMapping(value = "/update")
|
||||
public Result<ConversationResponse> update(@RequestBody ConversationCreateRequest request) {
|
||||
ConversationResponse response = conversationService.updateConversationWithResponse(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除对话
|
||||
*/
|
||||
@Operation(summary = "删除会话", description = "删除指定的对话会话。")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<Void> delete(@Parameter(description = "会话 ID") @RequestParam String id) {
|
||||
boolean deleted = conversationService.removeById(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取活跃对话
|
||||
*/
|
||||
@Operation(summary = "获取活跃会话", description = "获取当前用户最近的活跃会话列表。")
|
||||
@GetMapping(value = "/active")
|
||||
public Result<List<ConversationResponse>> getActiveConversations() {
|
||||
List<ConversationResponse> responses = conversationService.getActiveConversationsWithResponse();
|
||||
return Result.success(responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取归档对话
|
||||
*/
|
||||
@Operation(summary = "获取归档会话", description = "获取当前用户已归档的会话列表。")
|
||||
@GetMapping(value = "/archived")
|
||||
public Result<List<ConversationResponse>> getArchivedConversations() {
|
||||
List<ConversationResponse> responses = conversationService.getArchivedConversationsWithResponse();
|
||||
return Result.success(responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新对话状态
|
||||
*/
|
||||
@Operation(summary = "获取会话状态", description = "获取指定会话的当前状态信息。")
|
||||
@PutMapping(value = "/status")
|
||||
public Result<Void> updateConversationStatus(
|
||||
@Parameter(description = "会话 ID") @RequestParam String id,
|
||||
@RequestParam String status) {
|
||||
boolean updated = conversationService.updateConversationStatus(id, status);
|
||||
if (!updated) {
|
||||
return Result.error("更新状态失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.coze.CozeApiCallPageRequest;
|
||||
import com.emotion.dto.request.coze.CozeApiCallCreateRequest;
|
||||
import com.emotion.dto.request.coze.CozeApiCallUpdateRequest;
|
||||
import com.emotion.dto.response.coze.CozeApiCallResponse;
|
||||
import com.emotion.service.CozeApiCallService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
|
||||
/**
|
||||
* Coze API调用记录控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/coze-api-call")
|
||||
@Tag(name = "Coze API 调用记录", description = "Coze API 调用记录的查询、创建、更新和删除接口")
|
||||
public class CozeApiCallController {
|
||||
|
||||
@Autowired
|
||||
private CozeApiCallService cozeApiCallService;
|
||||
|
||||
/**
|
||||
* 分页查询API调用记录
|
||||
*/
|
||||
@Operation(summary = "分页查询调用记录", description = "根据条件分页查询 Coze API 调用记录。")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<CozeApiCallResponse>> getCozeApiCallPage(@Validated CozeApiCallPageRequest request) {
|
||||
PageResult<CozeApiCallResponse> pageResult = cozeApiCallService.getPage(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取API调用记录
|
||||
*/
|
||||
@Operation(summary = "获取调用记录详情", description = "根据 ID 获取单条调用记录的详细信息。")
|
||||
@GetMapping(value = "/detail")
|
||||
public Result<CozeApiCallResponse> getCozeApiCallById(@Parameter(description = "调用记录 ID", required = true) @RequestParam String id) {
|
||||
CozeApiCallResponse response = cozeApiCallService.getById(id);
|
||||
if (response == null) {
|
||||
return Result.notFound("API调用记录不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建API调用记录
|
||||
*/
|
||||
@Operation(summary = "创建调用记录", description = "新增一条 Coze API 调用记录。")
|
||||
@PostMapping(value = "/create")
|
||||
public Result<CozeApiCallResponse> createCozeApiCall(@RequestBody @Validated CozeApiCallCreateRequest request) {
|
||||
CozeApiCallResponse response = cozeApiCallService.create(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新API调用记录
|
||||
*/
|
||||
@Operation(summary = "更新调用记录", description = "更新已有的 Coze API 调用记录。")
|
||||
@PutMapping(value = "/update")
|
||||
public Result<CozeApiCallResponse> updateCozeApiCall(@RequestBody @Validated CozeApiCallUpdateRequest request) {
|
||||
CozeApiCallResponse response = cozeApiCallService.update(request);
|
||||
if (response == null) {
|
||||
return Result.error("更新失败,记录不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除API调用记录
|
||||
*/
|
||||
@Operation(summary = "删除调用记录", description = "删除指定的 Coze API 调用记录。")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<Void> deleteCozeApiCall(@Parameter(description = "调用记录 ID", required = true) @RequestParam String id) {
|
||||
boolean deleted = cozeApiCallService.delete(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计用户的API调用次数
|
||||
*/
|
||||
@Operation(summary = "按用户统计调用量", description = "统计指定用户在时间范围内的 Coze API 调用次数。")
|
||||
@GetMapping(value = "/countByUser")
|
||||
public Result<Long> countByUserId(@Parameter(description = "用户 ID", required = true) @RequestParam String userId) {
|
||||
Long count = cozeApiCallService.countByUserId(userId);
|
||||
return Result.success(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计Bot的API调用次数
|
||||
*/
|
||||
@Operation(summary = "按 Bot 统计调用量", description = "统计指定 Bot 的 Coze API 调用次数。")
|
||||
@GetMapping(value = "/countByBot")
|
||||
public Result<Long> countByBotId(@Parameter(description = "Bot ID", required = true) @RequestParam String botId) {
|
||||
Long count = cozeApiCallService.countByBotId(botId);
|
||||
return Result.success(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计指定状态的API调用次数
|
||||
*/
|
||||
@Operation(summary = "按状态统计调用量", description = "按调用状态统计 Coze API 调用次数。")
|
||||
@GetMapping(value = "/countByStatus")
|
||||
public Result<Long> countByStatus(@RequestParam String status) {
|
||||
Long count = cozeApiCallService.countByStatus(status);
|
||||
return Result.success(count);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计用户的Token使用量
|
||||
*/
|
||||
@Operation(summary = "按用户统计 Token 消耗", description = "统计指定用户的 Token 总消耗量。")
|
||||
@GetMapping(value = "/tokensByUser")
|
||||
public Result<Long> sumTokensByUserId(@Parameter(description = "用户 ID", required = true) @RequestParam String userId) {
|
||||
Long totalTokens = cozeApiCallService.sumTokensByUserId(userId);
|
||||
return Result.success(totalTokens);
|
||||
}
|
||||
|
||||
/**
|
||||
* 统计用户的API调用费用
|
||||
*/
|
||||
@Operation(summary = "按用户统计费用", description = "统计指定用户的 API 调用总费用。")
|
||||
@GetMapping(value = "/costByUser")
|
||||
public Result<BigDecimal> sumCostByUserId(@Parameter(description = "用户 ID", required = true) @RequestParam String userId) {
|
||||
BigDecimal totalCost = cozeApiCallService.sumCostByUserId(userId);
|
||||
return Result.success(totalCost);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,199 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.DiaryCommentCreateRequest;
|
||||
import com.emotion.dto.request.DiaryCommentPageRequest;
|
||||
import com.emotion.dto.response.DiaryCommentResponse;
|
||||
import com.emotion.service.DiaryCommentService;
|
||||
import com.emotion.service.DiaryPostService;
|
||||
import com.emotion.util.UserContextUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 日记评论控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/diaryComment")
|
||||
@Tag(name = "日记评论管理", description = "日记评论的查询、创建、更新、删除、点赞、置顶等接口")
|
||||
public class DiaryCommentController {
|
||||
|
||||
@Autowired
|
||||
private DiaryCommentService diaryCommentService;
|
||||
|
||||
@Autowired
|
||||
private DiaryPostService diaryPostService;
|
||||
|
||||
/**
|
||||
* 分页查询评论
|
||||
*/
|
||||
@Operation(summary = "分页查询评论", description = "分页查询指定日记的评论列表。")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<DiaryCommentResponse>> getPage(@Validated DiaryCommentPageRequest request) {
|
||||
PageResult<DiaryCommentResponse> pageResult = diaryCommentService.getPageWithResponse(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取评论树结构
|
||||
*/
|
||||
@Operation(summary = "获取评论树", description = "以树形结构获取日记的评论及其回复列表。")
|
||||
@GetMapping(value = "/commentTree")
|
||||
public Result<List<DiaryCommentResponse>> getCommentTree(@Parameter(description = "日记 ID") @RequestParam String diaryId) {
|
||||
List<DiaryCommentResponse> responses = diaryCommentService.getCommentTreeWithResponse(diaryId);
|
||||
return Result.success(responses);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取评论详情
|
||||
*/
|
||||
@Operation(summary = "获取评论详情", description = "根据 ID 获取评论详情。")
|
||||
@GetMapping(value = "/detail")
|
||||
public Result<DiaryCommentResponse> getById(@Parameter(description = "评论 ID") @RequestParam String id) {
|
||||
DiaryCommentResponse response = diaryCommentService.getCommentResponseById(id);
|
||||
if (response == null) {
|
||||
return Result.notFound("评论不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建评论
|
||||
*/
|
||||
@Operation(summary = "创建评论", description = "对指定日记发表一条新评论或回复。")
|
||||
@PostMapping(value = "/create")
|
||||
public Result<DiaryCommentResponse> create(@Valid @RequestBody DiaryCommentCreateRequest request) {
|
||||
// 从上下文中获取当前用户ID,而不是直接使用请求中的用户ID
|
||||
String currentUserId = UserContextUtils.getCurrentUserId();
|
||||
if (currentUserId != null) {
|
||||
request.setUserId(currentUserId);
|
||||
}
|
||||
DiaryCommentResponse response = diaryCommentService.createCommentWithResponse(request);
|
||||
|
||||
// 更新日记的评论数
|
||||
diaryPostService.incrementCommentCount(request.getDiaryId());
|
||||
diaryPostService.updateLastCommentTime(request.getDiaryId());
|
||||
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新评论
|
||||
*/
|
||||
@Operation(summary = "更新评论", description = "修改已有评论的内容。")
|
||||
@PutMapping(value = "/update")
|
||||
public Result<DiaryCommentResponse> update(@Valid @RequestBody DiaryCommentCreateRequest request) {
|
||||
DiaryCommentResponse response = diaryCommentService.updateCommentWithResponse(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除评论
|
||||
*/
|
||||
@Operation(summary = "删除评论", description = "永久删除指定评论。")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<Void> delete(@Parameter(description = "评论 ID") @RequestParam String id) {
|
||||
boolean deleted = diaryCommentService.deleteComment(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
|
||||
// 更新日记的评论数
|
||||
DiaryCommentResponse response = diaryCommentService.getCommentResponseById(id);
|
||||
if (response != null) {
|
||||
diaryPostService.decrementCommentCount(response.getDiaryId());
|
||||
}
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除评论
|
||||
*/
|
||||
@Operation(summary = "软删除评论", description = "将评论标记为已删除状态。")
|
||||
@DeleteMapping(value = "/softDelete")
|
||||
public Result<Void> softDelete(@Parameter(description = "评论 ID") @RequestParam String id) {
|
||||
boolean deleted = diaryCommentService.softDeleteComment(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
|
||||
// 更新日记的评论数
|
||||
DiaryCommentResponse response = diaryCommentService.getCommentResponseById(id);
|
||||
if (response != null) {
|
||||
diaryPostService.decrementCommentCount(response.getDiaryId());
|
||||
}
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复评论
|
||||
*/
|
||||
@Operation(summary = "恢复评论", description = "恢复已软删除的评论。")
|
||||
@PutMapping(value = "/restore")
|
||||
public Result<Void> restore(@Parameter(description = "评论 ID") @RequestParam String id) {
|
||||
boolean restored = diaryCommentService.restoreComment(id);
|
||||
if (!restored) {
|
||||
return Result.error("恢复失败");
|
||||
}
|
||||
|
||||
// 更新日记的评论数
|
||||
DiaryCommentResponse response = diaryCommentService.getCommentResponseById(id);
|
||||
if (response != null) {
|
||||
diaryPostService.incrementCommentCount(response.getDiaryId());
|
||||
}
|
||||
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 点赞评论
|
||||
*/
|
||||
@Operation(summary = "点赞评论", description = "对指定评论进行点赞。")
|
||||
@PostMapping(value = "/like")
|
||||
public Result<Void> like(@Parameter(description = "评论 ID") @RequestParam String id) {
|
||||
boolean liked = diaryCommentService.incrementLikeCount(id);
|
||||
if (!liked) {
|
||||
return Result.error("点赞失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消点赞评论
|
||||
*/
|
||||
@Operation(summary = "取消点赞评论", description = "取消对指定评论的点赞。")
|
||||
@DeleteMapping(value = "/unlike")
|
||||
public Result<Void> unlike(@Parameter(description = "评论 ID") @RequestParam String id) {
|
||||
boolean unliked = diaryCommentService.decrementLikeCount(id);
|
||||
if (!unliked) {
|
||||
return Result.error("取消点赞失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置置顶状态
|
||||
*/
|
||||
@Operation(summary = "置顶评论", description = "将指定评论设为置顶显示。")
|
||||
@PutMapping(value = "/setTop")
|
||||
public Result<Void> setTop(@Parameter(description = "评论 ID") @RequestParam String id, @RequestParam Integer isTop) {
|
||||
boolean set = diaryCommentService.setTop(id, isTop);
|
||||
if (!set) {
|
||||
return Result.error("设置置顶状态失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,200 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.DiaryPostCreateRequest;
|
||||
import com.emotion.dto.request.DiaryPostPageRequest;
|
||||
import com.emotion.dto.request.DiaryPostUpdateRequest;
|
||||
import com.emotion.dto.response.DiaryPostResponse;
|
||||
import com.emotion.service.DiaryPostService;
|
||||
import com.emotion.util.UserContextUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* 用户日记控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/diaryPost")
|
||||
@Tag(name = "日记管理", description = "用户日记帖子的查询、创建、发布、更新、删除、点赞、分享等接口")
|
||||
public class DiaryPostController {
|
||||
|
||||
@Autowired
|
||||
private DiaryPostService diaryPostService;
|
||||
|
||||
/**
|
||||
* 分页查询日记
|
||||
*/
|
||||
@Operation(summary = "分页查询日记", description = "根据条件分页查询当前用户的日记帖子列表。")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<DiaryPostResponse>> getPage(@Validated DiaryPostPageRequest request) {
|
||||
return Result.success(diaryPostService.getPageWithResponse(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取日记详情
|
||||
*/
|
||||
@Operation(summary = "获取日记详情", description = "根据 ID 获取日记帖子的详细信息。")
|
||||
@GetMapping(value = "/detail")
|
||||
public Result<DiaryPostResponse> getById(@Parameter(description = "日记 ID") @RequestParam String id) {
|
||||
DiaryPostResponse response = diaryPostService.getDiaryPostResponseById(id);
|
||||
if (response == null) {
|
||||
return Result.notFound("日记不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建日记
|
||||
*/
|
||||
@Operation(summary = "创建日记", description = "创建一篇新的日记帖子。")
|
||||
@PostMapping(value = "/create")
|
||||
public Result<DiaryPostResponse> create(@Valid @RequestBody DiaryPostCreateRequest request) {
|
||||
// 从上下文中获取当前用户ID,而不是直接使用请求中的用户ID
|
||||
String currentUserId = UserContextUtils.getCurrentUserId();
|
||||
if (currentUserId != null) {
|
||||
request.setUserId(currentUserId);
|
||||
}
|
||||
return Result.success(diaryPostService.createDiaryPostWithResponse(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 发表日记并生成AI评论
|
||||
*/
|
||||
@Operation(summary = "发布日记", description = "将草稿状态的日记帖子发布为正式内容。")
|
||||
@PostMapping(value = "/publish")
|
||||
public Result<DiaryPostResponse> publish(@Valid @RequestBody DiaryPostCreateRequest request) {
|
||||
// 从上下文中获取当前用户ID,而不是直接使用请求中的用户ID
|
||||
String currentUserId = UserContextUtils.getCurrentUserId();
|
||||
if (currentUserId != null) {
|
||||
request.setUserId(currentUserId);
|
||||
}
|
||||
return Result.success(diaryPostService.publishDiaryWithAiComment(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新日记
|
||||
*/
|
||||
@Operation(summary = "更新日记", description = "修改已有日记帖子的内容。")
|
||||
@PutMapping(value = "/update")
|
||||
public Result<DiaryPostResponse> update(@Valid @RequestBody DiaryPostUpdateRequest request) {
|
||||
DiaryPostResponse response = diaryPostService.updateDiaryPostWithResponse(request);
|
||||
if (response == null) {
|
||||
return Result.error("更新失败");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除日记
|
||||
*/
|
||||
@Operation(summary = "删除日记", description = "永久删除指定的日记帖子。")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<Void> delete(@Parameter(description = "日记 ID") @RequestParam String id) {
|
||||
boolean deleted = diaryPostService.deleteDiaryPost(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 软删除日记
|
||||
*/
|
||||
@Operation(summary = "软删除日记", description = "将日记帖子标记为已删除状态(可恢复)。")
|
||||
@DeleteMapping(value = "/softDelete")
|
||||
public Result<Void> softDelete(@Parameter(description = "日记 ID") @RequestParam String id) {
|
||||
boolean deleted = diaryPostService.softDeleteDiaryPost(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复日记
|
||||
*/
|
||||
@Operation(summary = "恢复日记", description = "将已软删除的日记帖子恢复为正常状态。")
|
||||
@PutMapping(value = "/restore")
|
||||
public Result<Void> restore(@Parameter(description = "日记 ID") @RequestParam String id) {
|
||||
boolean restored = diaryPostService.restoreDiaryPost(id);
|
||||
if (!restored) {
|
||||
return Result.error("恢复失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 点赞日记
|
||||
*/
|
||||
@Operation(summary = "点赞日记", description = "对指定日记帖子进行点赞操作。")
|
||||
@PostMapping(value = "/like")
|
||||
public Result<Void> like(@Parameter(description = "日记 ID") @RequestParam String id) {
|
||||
boolean liked = diaryPostService.incrementLikeCount(id);
|
||||
if (!liked) {
|
||||
return Result.error("点赞失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 取消点赞日记
|
||||
*/
|
||||
@Operation(summary = "取消点赞日记", description = "取消对指定日记帖子的点赞。")
|
||||
@DeleteMapping(value = "/unlike")
|
||||
public Result<Void> unlike(@Parameter(description = "日记 ID") @RequestParam String id) {
|
||||
boolean unliked = diaryPostService.decrementLikeCount(id);
|
||||
if (!unliked) {
|
||||
return Result.error("取消点赞失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 分享日记
|
||||
*/
|
||||
@Operation(summary = "分享日记", description = "将指定日记帖子分享给其他用户。")
|
||||
@PostMapping(value = "/share")
|
||||
public Result<Void> share(@Parameter(description = "日记 ID") @RequestParam String id) {
|
||||
boolean shared = diaryPostService.incrementShareCount(id);
|
||||
if (!shared) {
|
||||
return Result.error("分享失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置精选状态
|
||||
*/
|
||||
@Operation(summary = "设置精选日记", description = "将指定日记帖子标记为精选内容。")
|
||||
@PutMapping(value = "/setFeatured")
|
||||
public Result<Void> setFeatured(@Parameter(description = "日记 ID") @RequestParam String id, @RequestParam Integer featured) {
|
||||
boolean set = diaryPostService.setFeatured(id, featured);
|
||||
if (!set) {
|
||||
return Result.error("设置精选状态失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置置顶优先级
|
||||
*/
|
||||
@Operation(summary = "设置日记优先级", description = "修改指定日记帖子的优先级排序。")
|
||||
@PutMapping(value = "/setPriority")
|
||||
public Result<Void> setPriority(@Parameter(description = "日记 ID") @RequestParam String id, @RequestParam Integer priority) {
|
||||
boolean set = diaryPostService.setPriority(id, priority);
|
||||
if (!set) {
|
||||
return Result.error("设置优先级失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.service.DictionaryService;
|
||||
import com.emotion.dto.request.dictionary.DictionaryCreateRequest;
|
||||
import com.emotion.dto.request.dictionary.DictionaryPageRequest;
|
||||
import com.emotion.dto.request.dictionary.DictionaryUpdateRequest;
|
||||
import com.emotion.dto.response.dictionary.DictionaryResponse;
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 字典Controller
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-12-22
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/dictionary")
|
||||
@Tag(name = "字典管理", description = "数据字典的查询、创建、更新和删除接口")
|
||||
public class DictionaryController {
|
||||
|
||||
@Autowired
|
||||
private DictionaryService dictionaryService;
|
||||
|
||||
/**
|
||||
* 创建字典
|
||||
*
|
||||
* @param request 创建请求
|
||||
* @return 创建结果
|
||||
*/
|
||||
@Operation(summary = "创建字典项", description = "创建一个新的数据字典项。")
|
||||
@PostMapping
|
||||
public Result<DictionaryResponse> createDictionary(@Validated @RequestBody DictionaryCreateRequest request) {
|
||||
return dictionaryService.createDictionary(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新字典
|
||||
*
|
||||
* @param request 更新请求
|
||||
* @return 更新结果
|
||||
*/
|
||||
@Operation(summary = "更新字典项", description = "修改已有数据字典项的内容。")
|
||||
@PutMapping
|
||||
public Result<DictionaryResponse> updateDictionary(@Validated @RequestBody DictionaryUpdateRequest request) {
|
||||
return dictionaryService.updateDictionary(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除字典
|
||||
*
|
||||
* @param id 字典ID
|
||||
* @return 删除结果
|
||||
*/
|
||||
@Operation(summary = "删除字典项", description = "删除指定的数据字典项。")
|
||||
@DeleteMapping("/delete")
|
||||
public Result<Void> deleteDictionary(@Parameter(description = "字典 ID") @RequestParam String id) {
|
||||
return dictionaryService.deleteDictionary(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取字典详情
|
||||
*
|
||||
* @param id 字典ID
|
||||
* @return 字典详情
|
||||
*/
|
||||
@Operation(summary = "获取字典详情", description = "根据 ID 获取字典项的详细信息。")
|
||||
@GetMapping("/detail")
|
||||
public Result<DictionaryResponse> getDictionary(@Parameter(description = "字典 ID") @RequestParam String id) {
|
||||
return dictionaryService.getDictionary(id);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询字典
|
||||
*
|
||||
* @param request 分页请求
|
||||
* @return 分页结果
|
||||
*/
|
||||
@Operation(summary = "分页查询字典", description = "分页查询数据字典列表。")
|
||||
@GetMapping("/list")
|
||||
public Result<PageResult<DictionaryResponse>> listDictionaries(DictionaryPageRequest request) {
|
||||
return dictionaryService.listDictionaries(request);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字典类型查询字典集合
|
||||
*
|
||||
* @param dictType 字典类型
|
||||
* @return 字典集合
|
||||
*/
|
||||
@Operation(summary = "根据类型查询字典", description = "根据字典类型查询字典集合。")
|
||||
@GetMapping("/byType")
|
||||
public Result<List<DictionaryResponse>> getDictionariesByType(@Parameter(description = "字典类型") @RequestParam String dictType) {
|
||||
return dictionaryService.getDictionariesByType(dictType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据字典类型查询启用的字典集合
|
||||
*
|
||||
* @param dictType 字典类型
|
||||
* @return 启用的字典集合
|
||||
*/
|
||||
@Operation(summary = "查询启用的字典", description = "根据字典类型查询启用的字典集合。")
|
||||
@GetMapping("/enabledByType")
|
||||
public Result<List<DictionaryResponse>> getEnabledDictionariesByType(@Parameter(description = "字典类型") @RequestParam String dictType) {
|
||||
return dictionaryService.getEnabledDictionariesByType(dictType);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.EmotionAnalysisCreateRequest;
|
||||
import com.emotion.dto.request.EmotionAnalysisPageRequest;
|
||||
import com.emotion.dto.request.EmotionAnalysisUpdateRequest;
|
||||
import com.emotion.dto.response.EmotionAnalysisResponse;
|
||||
import com.emotion.service.EmotionAnalysisService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 情绪分析控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/emotionAnalysis")
|
||||
@Tag(name = "情绪分析管理", description = "情绪分析记录的查询、创建、更新和删除接口")
|
||||
public class EmotionAnalysisController {
|
||||
|
||||
@Autowired
|
||||
private EmotionAnalysisService emotionAnalysisService;
|
||||
|
||||
/**
|
||||
* 分页查询情绪分析记录
|
||||
*/
|
||||
@Operation(summary = "分页查询情绪分析", description = "根据条件分页查询情绪分析记录列表。")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<EmotionAnalysisResponse>> getPage(@Validated EmotionAnalysisPageRequest request) {
|
||||
return Result.success(emotionAnalysisService.getPageWithResponse(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取情绪分析记录
|
||||
*/
|
||||
@Operation(summary = "获取分析详情", description = "根据 ID 获取情绪分析报告的详细信息。")
|
||||
@GetMapping(value = "/detail")
|
||||
public Result<EmotionAnalysisResponse> getById(@Parameter(description = "分析 ID") @RequestParam String id) {
|
||||
EmotionAnalysisResponse response = emotionAnalysisService.getEmotionAnalysisResponseById(id);
|
||||
if (response == null) {
|
||||
return Result.notFound("情绪分析记录不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建情绪分析记录
|
||||
*/
|
||||
@Operation(summary = "创建情绪分析", description = "基于情绪记录数据创建一条新的 AI 情绪分析报告。")
|
||||
@PostMapping(value = "/create")
|
||||
public Result<EmotionAnalysisResponse> create(@RequestBody @Valid EmotionAnalysisCreateRequest request) {
|
||||
return Result.success(emotionAnalysisService.createEmotionAnalysisWithResponse(request));
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新情绪分析记录
|
||||
*/
|
||||
@Operation(summary = "更新情绪分析", description = "修改已有情绪分析报告的内容。")
|
||||
@PutMapping(value = "/update")
|
||||
public Result<EmotionAnalysisResponse> update(@RequestBody @Valid EmotionAnalysisUpdateRequest request) {
|
||||
EmotionAnalysisResponse response = emotionAnalysisService.updateEmotionAnalysisWithResponse(request);
|
||||
if (response == null) {
|
||||
return Result.error("更新失败");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除情绪分析记录
|
||||
*/
|
||||
@Operation(summary = "删除情绪分析", description = "删除指定的情绪分析记录。")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<Void> delete(@Parameter(description = "分析 ID") @RequestParam String id) {
|
||||
boolean deleted = emotionAnalysisService.deleteEmotionAnalysis(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.EmotionRecordCreateRequest;
|
||||
import com.emotion.dto.request.EmotionRecordPageRequest;
|
||||
import com.emotion.dto.request.EmotionRecordUpdateRequest;
|
||||
import com.emotion.dto.response.EmotionRecordResponse;
|
||||
import com.emotion.service.EmotionRecordService;
|
||||
import com.emotion.util.UserContextUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 情绪记录控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-22
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/emotionRecord")
|
||||
@Tag(name = "情绪记录管理", description = "用户情绪记录的查询、创建、更新、删除和统计接口")
|
||||
public class EmotionRecordController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(EmotionRecordController.class);
|
||||
|
||||
@Autowired
|
||||
private EmotionRecordService emotionRecordService;
|
||||
|
||||
/**
|
||||
* 创建情绪记录
|
||||
*/
|
||||
@Operation(summary = "创建情绪记录", description = "记录用户当天的情绪数据,包括类型、强度、触发因素等。")
|
||||
@PostMapping(value = "/create")
|
||||
public Result<EmotionRecordResponse> createRecord(@RequestBody @Valid EmotionRecordCreateRequest request) {
|
||||
log.info("创建情绪记录: userId={}", request.getUserId());
|
||||
|
||||
// 从上下文中获取当前用户ID
|
||||
String userId = UserContextUtils.requireCurrentUserId();
|
||||
request.setUserId(userId);
|
||||
|
||||
EmotionRecordResponse response = emotionRecordService.createEmotionRecordWithResponse(request);
|
||||
return Result.success("创建成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询情绪记录
|
||||
*/
|
||||
@Operation(summary = "分页查询情绪记录", description = "根据日期范围、情绪类型等条件分页查询情绪记录。")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<EmotionRecordResponse>> getPage(@Validated EmotionRecordPageRequest request) {
|
||||
// 从上下文中获取当前用户ID
|
||||
String userId = UserContextUtils.requireCurrentUserId();
|
||||
|
||||
log.info("分页查询情绪记录: userId={}, current={}, size={}", userId, request.getCurrent(), request.getSize());
|
||||
|
||||
PageResult<EmotionRecordResponse> page = emotionRecordService.getPageByUserIdWithResponse(userId, request);
|
||||
|
||||
log.info("分页查询情绪记录成功: userId={}, total={}, records={}",
|
||||
userId, page.getTotal(), page.getRecords().size());
|
||||
|
||||
return Result.success(page);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取情绪记录详情
|
||||
*/
|
||||
@Operation(summary = "获取记录详情", description = "根据 ID 获取情绪记录的详细信息。")
|
||||
@GetMapping(value = "/detail")
|
||||
public Result<EmotionRecordResponse> getRecord(@Parameter(description = "记录 ID") @RequestParam String id) {
|
||||
log.info("获取情绪记录详情: {}", id);
|
||||
|
||||
EmotionRecordResponse response = emotionRecordService.getEmotionRecordResponseById(id);
|
||||
if (response == null) {
|
||||
return Result.notFound("情绪记录不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新情绪记录
|
||||
*/
|
||||
@Operation(summary = "更新情绪记录", description = "修改已有情绪记录的内容。")
|
||||
@PutMapping(value = "/update")
|
||||
public Result<EmotionRecordResponse> updateRecord(@RequestBody @Valid EmotionRecordUpdateRequest request) {
|
||||
log.info("更新情绪记录: {}", request.getId());
|
||||
|
||||
EmotionRecordResponse response = emotionRecordService.updateEmotionRecordWithResponse(request);
|
||||
if (response == null) {
|
||||
return Result.notFound("情绪记录不存在");
|
||||
}
|
||||
return Result.success("更新成功", response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除情绪记录
|
||||
*/
|
||||
@Operation(summary = "删除情绪记录", description = "删除指定的情绪记录。")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<String> deleteRecord(@Parameter(description = "记录 ID") @RequestParam String id) {
|
||||
log.info("删除情绪记录: {}", id);
|
||||
|
||||
boolean deleted = emotionRecordService.deleteEmotionRecord(id);
|
||||
if (!deleted) {
|
||||
return Result.notFound("情绪记录不存在");
|
||||
}
|
||||
return Result.success("删除成功");
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取情绪统计
|
||||
*/
|
||||
@Operation(summary = "获取情绪统计", description = "统计指定时间范围内的情绪数据分布。")
|
||||
@GetMapping(value = "/stats")
|
||||
public Result<Map<String, Object>> getEmotionStats(
|
||||
@RequestParam(required = false) String startDate,
|
||||
@RequestParam(required = false) String endDate) {
|
||||
|
||||
// 从上下文中获取当前用户ID
|
||||
String userId = UserContextUtils.requireCurrentUserId();
|
||||
|
||||
log.info("获取情绪统计: userId={}, startDate={}, endDate={}", userId, startDate, endDate);
|
||||
|
||||
Map<String, Object> stats = emotionRecordService.getEmotionStats(userId, startDate, endDate);
|
||||
|
||||
return Result.success(stats);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.EmotionSummaryGenerateRequest;
|
||||
import com.emotion.dto.request.EmotionSummaryStatusRequest;
|
||||
import com.emotion.dto.response.EmotionSummaryGenerateResponse;
|
||||
import com.emotion.dto.response.EmotionSummaryStatusResponse;
|
||||
import com.emotion.service.AiChatService;
|
||||
import com.emotion.util.UserContextUtils;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* 情绪总结控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-25
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/emotionSummary")
|
||||
@Tag(name = "情绪总结管理", description = "用户情绪记录总结和分析功能")
|
||||
public class EmotionSummaryController {
|
||||
|
||||
@Autowired
|
||||
private AiChatService aiChatService;
|
||||
|
||||
/**
|
||||
* 生成用户当天的情绪记录总结
|
||||
*/
|
||||
@Operation(summary = "生成情绪总结", description = "基于指定时间范围的情绪记录数据,生成 AI 情绪总结报告。")
|
||||
@PostMapping(value = "/generate")
|
||||
public Result<EmotionSummaryGenerateResponse> generateEmotionSummary(
|
||||
@RequestBody @Valid EmotionSummaryGenerateRequest request) {
|
||||
String userId = UserContextUtils.requireCurrentUserId();
|
||||
log.info("收到生成情绪记录总结请求: userId={}", userId);
|
||||
|
||||
// 调用AI服务生成情绪总结
|
||||
EmotionSummaryGenerateResponse response = aiChatService.generateEmotionSummaryWithResponse(userId);
|
||||
|
||||
if (response.getSuccess()) {
|
||||
log.info("情绪记录总结生成成功: userId={}", userId);
|
||||
return Result.success("情绪记录总结生成成功", response);
|
||||
} else {
|
||||
log.warn("情绪记录总结生成失败: userId={}, message={}", userId, response.getMessage());
|
||||
return Result.error(response.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户情绪记录总结状态
|
||||
*/
|
||||
@Operation(summary = "查询总结状态", description = "查询指定时间段内是否已生成情绪总结及其状态。")
|
||||
@GetMapping(value = "/status")
|
||||
public Result<EmotionSummaryStatusResponse> getEmotionSummaryStatus(
|
||||
@Validated EmotionSummaryStatusRequest request) {
|
||||
// 从上下文中获取当前用户ID
|
||||
String userId = UserContextUtils.requireCurrentUserId();
|
||||
log.info("查询用户情绪记录总结状态: userId={}", userId);
|
||||
|
||||
// 调用AI服务获取状态信息
|
||||
EmotionSummaryStatusResponse response = aiChatService.getEmotionSummaryStatusWithResponse(userId);
|
||||
|
||||
return Result.success(response);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.EpicScriptCreateRequest;
|
||||
import com.emotion.dto.request.EpicScriptInspirationRequest;
|
||||
import com.emotion.dto.request.EpicScriptPageRequest;
|
||||
import com.emotion.dto.request.EpicScriptUpdateRequest;
|
||||
import com.emotion.dto.response.EpicScriptInspirationResponse;
|
||||
import com.emotion.dto.response.EpicScriptResponse;
|
||||
import com.emotion.dto.response.InspirationSuggestionResponse;
|
||||
import com.emotion.service.EpicScriptService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 爽文剧本控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-12-22
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/epicScript")
|
||||
@Tag(name = "爽文剧本管理", description = "爽文剧本的查询、创建、更新、删除和灵感推荐接口")
|
||||
public class EpicScriptController {
|
||||
|
||||
@Autowired
|
||||
private EpicScriptService epicScriptService;
|
||||
|
||||
/**
|
||||
* 分页查询当前用户的爽文剧本
|
||||
*/
|
||||
@Operation(summary = "分页查询爽文剧本", description = "分页查询当前用户的爽文剧本列表。")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<EpicScriptResponse>> getPage(@Validated EpicScriptPageRequest request) {
|
||||
PageResult<EpicScriptResponse> pageResult = epicScriptService.getPageByCurrentUser(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的所有爽文剧本列表
|
||||
*/
|
||||
@Operation(summary = "获取爽文剧本列表", description = "获取当前用户的所有爽文剧本列表。")
|
||||
@GetMapping(value = "/listAll")
|
||||
public Result<List<EpicScriptResponse>> getList() {
|
||||
List<EpicScriptResponse> scripts = epicScriptService.getListByCurrentUser();
|
||||
return Result.success(scripts);
|
||||
}
|
||||
|
||||
@Operation(summary = "获取灵感推荐", description = "获取系统推荐的灵感建议列表。")
|
||||
@GetMapping(value = "/inspiration/recommendations")
|
||||
public Result<List<InspirationSuggestionResponse>> getInspirationRecommendations() {
|
||||
return Result.success(epicScriptService.getInspirationRecommendations());
|
||||
}
|
||||
|
||||
@Operation(summary = "获取随机灵感", description = "随机获取指定数量的灵感建议。")
|
||||
@GetMapping(value = "/inspiration/random")
|
||||
public Result<List<InspirationSuggestionResponse>> getRandomInspirations(
|
||||
@Parameter(description = "灵感数量") @RequestParam(required = false, defaultValue = "3") Integer size) {
|
||||
return Result.success(epicScriptService.getRandomInspirations(size));
|
||||
}
|
||||
|
||||
@Operation(summary = "从灵感生成剧本", description = "基于选定的灵感建议生成爽文剧本。")
|
||||
@PostMapping(value = "/inspiration/generate")
|
||||
public Result<EpicScriptInspirationResponse> generateFromInspiration(
|
||||
@Valid @RequestBody EpicScriptInspirationRequest request) {
|
||||
EpicScriptInspirationResponse response;
|
||||
try {
|
||||
response = epicScriptService.generateFromInspiration(request);
|
||||
} catch (IllegalStateException e) {
|
||||
return Result.error(e.getMessage());
|
||||
}
|
||||
if (response == null) {
|
||||
return Result.error("灵感剧本生成失败");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取爽文剧本详情
|
||||
*/
|
||||
@Operation(summary = "获取剧本详情", description = "根据 ID 获取爽文剧本的详细信息。")
|
||||
@GetMapping(value = "/detail")
|
||||
public Result<EpicScriptResponse> getById(@Parameter(description = "剧本 ID") @RequestParam String id) {
|
||||
EpicScriptResponse script = epicScriptService.getScriptById(id);
|
||||
if (script == null) {
|
||||
return Result.notFound("爽文剧本不存在");
|
||||
}
|
||||
return Result.success(script);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建爽文剧本
|
||||
*/
|
||||
@Operation(summary = "创建爽文剧本", description = "创建一个新的爽文剧本。")
|
||||
@PostMapping(value = "/create")
|
||||
public Result<EpicScriptResponse> create(@Valid @RequestBody EpicScriptCreateRequest request) {
|
||||
EpicScriptResponse script = epicScriptService.createScript(request);
|
||||
if (script == null) {
|
||||
return Result.error("创建失败");
|
||||
}
|
||||
return Result.success(script);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新爽文剧本
|
||||
*/
|
||||
@Operation(summary = "更新爽文剧本", description = "修改已有爽文剧本的内容。")
|
||||
@PutMapping(value = "/update")
|
||||
public Result<EpicScriptResponse> update(@Valid @RequestBody EpicScriptUpdateRequest request) {
|
||||
EpicScriptResponse script = epicScriptService.updateScript(request);
|
||||
if (script == null) {
|
||||
return Result.error("更新失败");
|
||||
}
|
||||
return Result.success(script);
|
||||
}
|
||||
|
||||
/**
|
||||
* 选中剧本(取消其他选中状态)
|
||||
*/
|
||||
@Operation(summary = "选中剧本", description = "选中指定剧本并取消其他剧本的选中状态。")
|
||||
@PutMapping(value = "/select")
|
||||
public Result<EpicScriptResponse> select(@Parameter(description = "剧本 ID") @RequestParam String id) {
|
||||
EpicScriptResponse script = epicScriptService.selectScript(id);
|
||||
if (script == null) {
|
||||
return Result.error("选中失败");
|
||||
}
|
||||
return Result.success(script);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除爽文剧本
|
||||
*/
|
||||
@Operation(summary = "删除爽文剧本", description = "删除指定的爽文剧本。")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<Void> delete(@Parameter(description = "剧本 ID") @RequestParam String id) {
|
||||
boolean deleted = epicScriptService.deleteScript(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.growth.GrowthTopicCreateRequest;
|
||||
import com.emotion.dto.request.growth.GrowthTopicPageRequest;
|
||||
import com.emotion.dto.request.growth.GrowthTopicUpdateRequest;
|
||||
import com.emotion.dto.response.growth.GrowthTopicResponse;
|
||||
import com.emotion.service.GrowthTopicService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* 成长话题控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/growthTopic")
|
||||
@Tag(name = "成长话题管理", description = "成长话题的查询、创建、更新和删除接口")
|
||||
public class GrowthTopicController {
|
||||
|
||||
@Autowired
|
||||
private GrowthTopicService growthTopicService;
|
||||
|
||||
/**
|
||||
* 分页查询成长话题
|
||||
*/
|
||||
@Operation(summary = "分页查询成长话题", description = "分页查询成长话题列表。")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<GrowthTopicResponse>> getPage(@Validated GrowthTopicPageRequest request) {
|
||||
PageResult<GrowthTopicResponse> pageResult = growthTopicService.getPageWithResponse(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取成长话题
|
||||
*/
|
||||
@Operation(summary = "获取话题详情", description = "根据 ID 获取成长话题的详细信息。")
|
||||
@GetMapping(value = "/detail")
|
||||
public Result<GrowthTopicResponse> getById(@Parameter(description = "话题 ID") @RequestParam String id) {
|
||||
GrowthTopicResponse response = growthTopicService.getGrowthTopicResponseById(id);
|
||||
if (response == null) {
|
||||
return Result.notFound("成长话题不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建成长话题
|
||||
*/
|
||||
@Operation(summary = "创建成长话题", description = "创建一个新的成长话题。")
|
||||
@PostMapping(value = "/create")
|
||||
public Result<GrowthTopicResponse> create(@Valid @RequestBody GrowthTopicCreateRequest request) {
|
||||
GrowthTopicResponse response = growthTopicService.createGrowthTopicWithResponse(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新成长话题
|
||||
*/
|
||||
@Operation(summary = "更新成长话题", description = "修改已有成长话题的内容。")
|
||||
@PutMapping(value = "/update")
|
||||
public Result<GrowthTopicResponse> update(@Valid @RequestBody GrowthTopicUpdateRequest request) {
|
||||
GrowthTopicResponse response = growthTopicService.updateGrowthTopicWithResponse(request);
|
||||
if (response == null) {
|
||||
return Result.notFound("成长话题不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除成长话题
|
||||
*/
|
||||
@Operation(summary = "删除成长话题", description = "删除指定的成长话题。")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<Void> delete(@Parameter(description = "话题 ID") @RequestParam String id) {
|
||||
boolean deleted = growthTopicService.deleteGrowthTopic(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.guest.GuestUserCreateRequest;
|
||||
import com.emotion.dto.request.guest.GuestUserPageRequest;
|
||||
import com.emotion.dto.request.guest.GuestUserUpdateRequest;
|
||||
import com.emotion.dto.response.guest.GuestUserResponse;
|
||||
import com.emotion.service.GuestUserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* 访客用户控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/guestUser")
|
||||
@Tag(name = "访客用户管理", description = "访客用户的查询、创建、更新和删除接口")
|
||||
public class GuestUserController {
|
||||
|
||||
@Autowired
|
||||
private GuestUserService guestUserService;
|
||||
|
||||
/**
|
||||
* 分页查询访客用户
|
||||
*/
|
||||
@Operation(summary = "分页查询访客用户", description = "分页查询访客用户列表。")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<GuestUserResponse>> getPage(@Validated GuestUserPageRequest request) {
|
||||
PageResult<GuestUserResponse> pageResult = guestUserService.getPageWithResponse(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取访客用户
|
||||
*/
|
||||
@Operation(summary = "获取访客详情", description = "根据 ID 获取访客用户的详细信息。")
|
||||
@GetMapping(value = "/detail")
|
||||
public Result<GuestUserResponse> getById(@Parameter(description = "访客 ID") @RequestParam String id) {
|
||||
GuestUserResponse response = guestUserService.getGuestUserResponseById(id);
|
||||
if (response == null) {
|
||||
return Result.notFound("访客用户不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建访客用户
|
||||
*/
|
||||
@Operation(summary = "创建访客用户", description = "创建一个新的访客用户。")
|
||||
@PostMapping(value = "/create")
|
||||
public Result<GuestUserResponse> create(@Valid @RequestBody GuestUserCreateRequest request) {
|
||||
GuestUserResponse response = guestUserService.createGuestUserWithResponse(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新访客用户
|
||||
*/
|
||||
@Operation(summary = "更新访客用户", description = "修改已有访客用户的信息。")
|
||||
@PutMapping(value = "/update")
|
||||
public Result<GuestUserResponse> update(@Valid @RequestBody GuestUserUpdateRequest request) {
|
||||
GuestUserResponse response = guestUserService.updateGuestUserWithResponse(request);
|
||||
if (response == null) {
|
||||
return Result.notFound("访客用户不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除访客用户
|
||||
*/
|
||||
@Operation(summary = "删除访客用户", description = "删除指定的访客用户。")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<Void> delete(@Parameter(description = "访客 ID") @RequestParam String id) {
|
||||
boolean deleted = guestUserService.deleteGuestUser(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 健康检查控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@RestController
|
||||
@Tag(name = "健康检查", description = "系统健康状态检查接口")
|
||||
public class HealthController {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(HealthController.class);
|
||||
|
||||
/**
|
||||
* 健康检查
|
||||
*/
|
||||
@Operation(summary = "系统健康检查", description = "返回系统各组件的健康状态,包括数据库、Redis 等。")
|
||||
@GetMapping("/health")
|
||||
public Map<String, Object> health() {
|
||||
log.info("健康检查请求");
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("service", "backend-single");
|
||||
response.put("message", "情感博物馆单体服务运行正常");
|
||||
response.put("version", "1.0.0");
|
||||
response.put("status", "UP");
|
||||
response.put("timestamp", LocalDateTime.now());
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 服务信息
|
||||
*/
|
||||
@Operation(summary = "获取服务信息", description = "返回服务的基本信息,包括版本号、构建时间等。")
|
||||
@GetMapping("/health/info")
|
||||
public Map<String, Object> info() {
|
||||
log.info("服务信息请求");
|
||||
|
||||
Map<String, Object> response = new HashMap<>();
|
||||
response.put("service", "backend-single");
|
||||
response.put("description", "情感博物馆单体服务");
|
||||
response.put("version", "1.0.0");
|
||||
response.put("author", "emotion-museum");
|
||||
response.put("buildTime", "2025-07-23");
|
||||
response.put("javaVersion", System.getProperty("java.version"));
|
||||
response.put("timestamp", LocalDateTime.now());
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.LifeEventCreateRequest;
|
||||
import com.emotion.dto.request.LifeEventPageRequest;
|
||||
import com.emotion.dto.request.LifeEventUpdateRequest;
|
||||
import com.emotion.dto.response.LifeEventResponse;
|
||||
import com.emotion.service.LifeEventService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 生命事件控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-12-22
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/lifeEvent")
|
||||
@Tag(name = "生命事件管理", description = "生命事件的查询、创建、更新和删除接口")
|
||||
public class LifeEventController {
|
||||
|
||||
@Autowired
|
||||
private LifeEventService lifeEventService;
|
||||
|
||||
/**
|
||||
* 分页查询当前用户的生命事件
|
||||
*/
|
||||
@Operation(summary = "分页查询生命事件", description = "分页查询生命事件列表。")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<LifeEventResponse>> getPage(@Validated LifeEventPageRequest request) {
|
||||
PageResult<LifeEventResponse> pageResult = lifeEventService.getPageByCurrentUser(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的所有生命事件列表
|
||||
*/
|
||||
@Operation(summary = "获取生命事件列表", description = "获取当前用户的所有生命事件列表。")
|
||||
@GetMapping(value = "/list")
|
||||
public Result<List<LifeEventResponse>> getList() {
|
||||
List<LifeEventResponse> events = lifeEventService.getListByCurrentUser();
|
||||
return Result.success(events);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取生命事件详情
|
||||
*/
|
||||
@Operation(summary = "获取生命事件详情", description = "根据 ID 获取生命事件的详细信息。")
|
||||
@GetMapping(value = "/detail")
|
||||
public Result<LifeEventResponse> getById(@Parameter(description = "事件 ID") @RequestParam String id) {
|
||||
LifeEventResponse event = lifeEventService.getEventById(id);
|
||||
if (event == null) {
|
||||
return Result.notFound("生命事件不存在");
|
||||
}
|
||||
return Result.success(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建生命事件
|
||||
*/
|
||||
@Operation(summary = "创建生命事件", description = "创建一个新的生命事件。")
|
||||
@PostMapping(value = "/create")
|
||||
public Result<LifeEventResponse> create(@Valid @RequestBody LifeEventCreateRequest request) {
|
||||
LifeEventResponse event = lifeEventService.createEvent(request);
|
||||
if (event == null) {
|
||||
return Result.error("创建失败");
|
||||
}
|
||||
return Result.success(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新生命事件
|
||||
*/
|
||||
@Operation(summary = "更新生命事件", description = "修改已有生命事件的内容。")
|
||||
@PutMapping(value = "/update")
|
||||
public Result<LifeEventResponse> update(@Valid @RequestBody LifeEventUpdateRequest request) {
|
||||
LifeEventResponse event = lifeEventService.updateEvent(request);
|
||||
if (event == null) {
|
||||
return Result.error("更新失败");
|
||||
}
|
||||
return Result.success(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除生命事件
|
||||
*/
|
||||
@Operation(summary = "删除生命事件", description = "删除指定的生命事件。")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<Void> delete(@Parameter(description = "事件 ID") @RequestParam String id) {
|
||||
boolean deleted = lifeEventService.deleteEvent(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
@Operation(summary = "AI 辅助生成内容", description = "根据标题和内容调用 AI 生成生命事件的解读文本、标签等信息。")
|
||||
@PostMapping(value = "/ai-assist")
|
||||
public Result<Map<String, Object>> aiAssist(@RequestBody Map<String, Object> request) {
|
||||
String title = stringValue(request.get("title"), "这段经历");
|
||||
String content = stringValue(request.get("content"), "");
|
||||
List<String> tags = readTags(request.get("tags"));
|
||||
if (tags.isEmpty()) {
|
||||
tags.add("成长");
|
||||
tags.add("记录");
|
||||
}
|
||||
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("content", content.isBlank()
|
||||
? "那一天,我清楚地感受到自己正在经历一次变化。事情本身也许并不宏大,但它让我重新看见了自己的选择、情绪和力量。"
|
||||
: content + "\n\n我愿意把这段经历记录下来,因为它提醒我:每一次真实面对,都会成为未来的底气。");
|
||||
data.put("aiReply", "AI占位解读:" + title + "体现了你的自我观察和复盘能力。后续接入真实AI后,这里会返回更完整的情绪整理、能力映射和行动建议。");
|
||||
data.put("tags", tags);
|
||||
data.put("placeholder", true);
|
||||
return Result.success(data);
|
||||
}
|
||||
|
||||
@Operation(summary = "聊天占位接口", description = "返回聊天功能的占位回复和建议问题。")
|
||||
@PostMapping(value = "/chat-placeholder")
|
||||
public Result<Map<String, Object>> chatPlaceholder(@RequestBody Map<String, Object> request) {
|
||||
String title = stringValue(request.get("title"), "这段经历");
|
||||
Map<String, Object> data = new LinkedHashMap<>();
|
||||
data.put("reply", "我在这里陪你回看「" + title + "」。真实聊天能力后续接入AI工作流;当前先保留这个入口和上下文。");
|
||||
data.put("suggestions", List.of("这件事让我学到了什么?", "如果重来一次我会怎么选?", "它会怎样影响我的人生剧本?"));
|
||||
data.put("placeholder", true);
|
||||
return Result.success(data);
|
||||
}
|
||||
|
||||
@Operation(summary = "分享占位接口", description = "返回分享功能的占位文本和分享信息。")
|
||||
@PostMapping(value = "/share-placeholder")
|
||||
public Result<Map<String, Object>> sharePlaceholder(@RequestBody Map<String, Object> request) {
|
||||
String title = stringValue(request.get("title"), "人生经历");
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("title", title);
|
||||
data.put("summary", "我刚刚记录了一段重要的人生轨迹。");
|
||||
data.put("shareText", "分享我的人生轨迹:" + title);
|
||||
data.put("placeholder", true);
|
||||
return Result.success(data);
|
||||
}
|
||||
|
||||
@Operation(summary = "收藏占位接口", description = "返回收藏功能的占位响应。")
|
||||
@PostMapping(value = "/favorite-placeholder")
|
||||
public Result<Map<String, Object>> favoritePlaceholder(@RequestBody Map<String, Object> request) {
|
||||
String id = stringValue(request.get("id"), "");
|
||||
Boolean favorite = Boolean.TRUE.equals(request.get("favorite"));
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("id", id);
|
||||
data.put("favorite", favorite);
|
||||
data.put("placeholder", true);
|
||||
return Result.success(data);
|
||||
}
|
||||
|
||||
private String stringValue(Object value, String fallback) {
|
||||
if (value == null) {
|
||||
return fallback;
|
||||
}
|
||||
String text = String.valueOf(value).trim();
|
||||
return text.isEmpty() ? fallback : text;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private List<String> readTags(Object value) {
|
||||
List<String> tags = new ArrayList<>();
|
||||
if (value instanceof List<?>) {
|
||||
for (Object item : (List<Object>) value) {
|
||||
if (item != null && !String.valueOf(item).trim().isEmpty()) {
|
||||
tags.add(String.valueOf(item).trim());
|
||||
}
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.LifePathCreateRequest;
|
||||
import com.emotion.dto.request.LifePathPageRequest;
|
||||
import com.emotion.dto.request.LifePathUpdateRequest;
|
||||
import com.emotion.dto.response.LifePathResponse;
|
||||
import com.emotion.service.LifePathService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 实现路径控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-12-22
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/lifePath")
|
||||
@Tag(name = "实现路径管理", description = "人生剧本实现路径的查询、创建、更新和删除接口")
|
||||
public class LifePathController {
|
||||
|
||||
@Autowired
|
||||
private LifePathService lifePathService;
|
||||
|
||||
/**
|
||||
* 分页查询当前用户的实现路径
|
||||
*/
|
||||
@Operation(summary = "分页查询实现路径", description = "分页查询当前用户的实现路径列表。")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<LifePathResponse>> getPage(@Validated LifePathPageRequest request) {
|
||||
PageResult<LifePathResponse> pageResult = lifePathService.getPageByCurrentUser(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户的所有实现路径列表
|
||||
*/
|
||||
@Operation(summary = "获取实现路径列表", description = "获取当前用户的所有实现路径列表。")
|
||||
@GetMapping(value = "/listAll")
|
||||
public Result<List<LifePathResponse>> getList() {
|
||||
List<LifePathResponse> paths = lifePathService.getListByCurrentUser();
|
||||
return Result.success(paths);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据剧本ID获取实现路径
|
||||
*/
|
||||
@Operation(summary = "根据剧本ID获取路径", description = "根据剧本 ID 获取对应的实现路径。")
|
||||
@GetMapping(value = "/byScript")
|
||||
public Result<LifePathResponse> getByScriptId(@Parameter(description = "剧本 ID") @RequestParam String scriptId) {
|
||||
LifePathResponse path = lifePathService.getByScriptId(scriptId);
|
||||
if (path == null) {
|
||||
return Result.notFound("实现路径不存在");
|
||||
}
|
||||
return Result.success(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取实现路径详情
|
||||
*/
|
||||
@Operation(summary = "获取路径详情", description = "根据 ID 获取实现路径的详细信息。")
|
||||
@GetMapping(value = "/detail")
|
||||
public Result<LifePathResponse> getById(@Parameter(description = "路径 ID") @RequestParam String id) {
|
||||
LifePathResponse path = lifePathService.getPathById(id);
|
||||
if (path == null) {
|
||||
return Result.notFound("实现路径不存在");
|
||||
}
|
||||
return Result.success(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建实现路径
|
||||
*/
|
||||
@Operation(summary = "创建实现路径", description = "创建一条新的实现路径。")
|
||||
@PostMapping(value = "/create")
|
||||
public Result<LifePathResponse> create(@Valid @RequestBody LifePathCreateRequest request) {
|
||||
LifePathResponse path = lifePathService.createPath(request);
|
||||
if (path == null) {
|
||||
return Result.error("创建失败");
|
||||
}
|
||||
return Result.success(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新实现路径
|
||||
*/
|
||||
@Operation(summary = "更新实现路径", description = "修改已有实现路径的内容。")
|
||||
@PutMapping(value = "/update")
|
||||
public Result<LifePathResponse> update(@Valid @RequestBody LifePathUpdateRequest request) {
|
||||
LifePathResponse path = lifePathService.updatePath(request);
|
||||
if (path == null) {
|
||||
return Result.error("更新失败");
|
||||
}
|
||||
return Result.success(path);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除实现路径
|
||||
*/
|
||||
@Operation(summary = "删除实现路径", description = "删除指定的实现路径。")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<Void> delete(@Parameter(description = "路径 ID") @RequestParam String id) {
|
||||
boolean deleted = lifePathService.deletePath(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.MessageCreateRequest;
|
||||
import com.emotion.dto.request.MessagePageRequest;
|
||||
import com.emotion.dto.request.MessageSearchRequest;
|
||||
import com.emotion.dto.request.MessageRecentRequest;
|
||||
import com.emotion.dto.response.MessageResponse;
|
||||
import com.emotion.service.MessageService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* 消息控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/message")
|
||||
@Slf4j
|
||||
@Tag(name = "消息管理", description = "会话消息的查询、创建、搜索、更新和删除接口")
|
||||
public class MessageController {
|
||||
|
||||
@Autowired
|
||||
private MessageService messageService;
|
||||
|
||||
/**
|
||||
* 创建消息
|
||||
*/
|
||||
@Operation(summary = "创建消息", description = "在指定会话中创建一条新消息。")
|
||||
@PostMapping(value = "/create")
|
||||
public Result<MessageResponse> create(@Valid @RequestBody MessageCreateRequest request) {
|
||||
MessageResponse response = messageService.createMessageFromRequest(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取消息
|
||||
*/
|
||||
@Operation(summary = "获取消息详情", description = "根据 ID 获取消息的详细信息。")
|
||||
@GetMapping(value = "/detail")
|
||||
public Result<MessageResponse> getById(@Parameter(description = "消息 ID") @RequestParam String id) {
|
||||
MessageResponse response = messageService.getMessageById(id);
|
||||
if (response == null) {
|
||||
return Result.notFound("消息不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询消息
|
||||
*/
|
||||
@Operation(summary = "分页查询消息", description = "分页查询指定会话的消息列表。")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<MessageResponse>> getPage(@Validated MessagePageRequest request) {
|
||||
PageResult<MessageResponse> pageResult = messageService.getPageWithResponse(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 搜索消息
|
||||
*/
|
||||
@Operation(summary = "搜索消息", description = "根据关键词在指定会话中搜索消息内容。")
|
||||
@PostMapping(value = "/search")
|
||||
public Result<PageResult<MessageResponse>> search(@Valid @RequestBody MessageSearchRequest request) {
|
||||
PageResult<MessageResponse> pageResult = messageService.searchWithResponse(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取最近的消息
|
||||
*/
|
||||
@Operation(summary = "获取最近消息", description = "获取指定会话最近的消息列表。")
|
||||
@PostMapping(value = "/recent")
|
||||
public Result<PageResult<MessageResponse>> getRecentMessages(@Valid @RequestBody MessageRecentRequest request) {
|
||||
PageResult<MessageResponse> pageResult = messageService.getRecentWithResponse(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新消息
|
||||
*/
|
||||
@Operation(summary = "更新消息", description = "修改指定消息的内容。")
|
||||
@PutMapping(value = "/update")
|
||||
public Result<MessageResponse> update(@RequestParam String id, @RequestParam String content) {
|
||||
MessageResponse response = messageService.updateMessage(id, content);
|
||||
if (response == null) {
|
||||
return Result.notFound("消息不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除消息
|
||||
*/
|
||||
@Operation(summary = "删除消息", description = "删除指定的消息记录。")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<Void> delete(@Parameter(description = "消息 ID") @RequestParam String id) {
|
||||
boolean deleted = messageService.deleteMessage(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.reward.RewardCreateRequest;
|
||||
import com.emotion.dto.request.reward.RewardPageRequest;
|
||||
import com.emotion.dto.request.reward.RewardUpdateRequest;
|
||||
import com.emotion.dto.response.reward.RewardResponse;
|
||||
import com.emotion.service.RewardService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* 奖励控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/reward")
|
||||
@Tag(name = "奖励管理", description = "奖励的查询、创建、更新和删除接口")
|
||||
public class RewardController {
|
||||
|
||||
@Autowired
|
||||
private RewardService rewardService;
|
||||
|
||||
/**
|
||||
* 分页查询奖励
|
||||
*/
|
||||
@Operation(summary = "分页查询奖励", description = "分页查询奖励列表。")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<RewardResponse>> getPage(@Validated RewardPageRequest request) {
|
||||
PageResult<RewardResponse> pageResult = rewardService.getPageWithResponse(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取奖励
|
||||
*/
|
||||
@Operation(summary = "获取奖励详情", description = "根据 ID 获取奖励的详细信息。")
|
||||
@GetMapping(value = "/detail")
|
||||
public Result<RewardResponse> getById(@Parameter(description = "奖励 ID") @RequestParam String id) {
|
||||
RewardResponse response = rewardService.getRewardResponseById(id);
|
||||
if (response == null) {
|
||||
return Result.notFound("奖励不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建奖励
|
||||
*/
|
||||
@Operation(summary = "创建奖励", description = "创建一个新的奖励。")
|
||||
@PostMapping(value = "/create")
|
||||
public Result<RewardResponse> create(@Valid @RequestBody RewardCreateRequest request) {
|
||||
RewardResponse response = rewardService.createRewardWithResponse(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新奖励
|
||||
*/
|
||||
@Operation(summary = "更新奖励", description = "修改已有奖励的信息。")
|
||||
@PutMapping(value = "/update")
|
||||
public Result<RewardResponse> update(@Valid @RequestBody RewardUpdateRequest request) {
|
||||
RewardResponse response = rewardService.updateRewardWithResponse(request);
|
||||
if (response == null) {
|
||||
return Result.notFound("奖励不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除奖励
|
||||
*/
|
||||
@Operation(summary = "删除奖励", description = "删除指定的奖励。")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<Void> delete(@Parameter(description = "奖励 ID") @RequestParam String id) {
|
||||
boolean deleted = rewardService.deleteReward(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.social.SocialContentApprovalRequest;
|
||||
import com.emotion.dto.request.social.SocialContentLinkImportRequest;
|
||||
import com.emotion.dto.request.social.SocialContentManualImportRequest;
|
||||
import com.emotion.dto.response.social.SocialContentItemResponse;
|
||||
import com.emotion.service.SocialContentService;
|
||||
import com.emotion.util.UserContextHolder;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/social/content")
|
||||
@Tag(name = "社交内容管理", description = "社交媒体内容的导入、截图、审核等接口")
|
||||
public class SocialContentController {
|
||||
|
||||
@Autowired
|
||||
private SocialContentService socialContentService;
|
||||
|
||||
@Operation(summary = "手动导入社交内容", description = "通过上传文本内容手动导入社交媒体数据。")
|
||||
@PostMapping("/manual")
|
||||
public Result<SocialContentItemResponse> manualImport(@Valid @RequestBody SocialContentManualImportRequest request) {
|
||||
String userId = currentUserId();
|
||||
if (userId == null) {
|
||||
return Result.unauthorized();
|
||||
}
|
||||
try {
|
||||
return Result.success(socialContentService.manualImport(userId, request));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Result.badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "链接导入社交内容", description = "通过社交媒体链接 URL 导入内容。")
|
||||
@PostMapping("/link")
|
||||
public Result<SocialContentItemResponse> linkImport(@Valid @RequestBody SocialContentLinkImportRequest request) {
|
||||
String userId = currentUserId();
|
||||
if (userId == null) {
|
||||
return Result.unauthorized();
|
||||
}
|
||||
try {
|
||||
return Result.success(socialContentService.linkImport(userId, request));
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Result.badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "截图导入社交内容", description = "通过上传截图图片来导入社交媒体内容。")
|
||||
@PostMapping("/screenshot")
|
||||
public Result<SocialContentItemResponse> screenshotImport(@Parameter(description = "平台名称", required = true) @RequestParam String platform,
|
||||
@RequestPart("file") MultipartFile file) {
|
||||
String userId = currentUserId();
|
||||
if (userId == null) {
|
||||
return Result.unauthorized();
|
||||
}
|
||||
try {
|
||||
return Result.success(socialContentService.screenshotImport(userId, platform, file));
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
return Result.badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取社交内容列表", description = "查询所有已导入的社交媒体内容列表。")
|
||||
@GetMapping("/list")
|
||||
public Result<List<SocialContentItemResponse>> list() {
|
||||
String userId = currentUserId();
|
||||
if (userId == null) {
|
||||
return Result.unauthorized();
|
||||
}
|
||||
return Result.success(socialContentService.listByUser(userId));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新内容审核状态", description = "修改指定社交内容的审核状态(通过/拒绝)。")
|
||||
@PutMapping("/{id}/approval")
|
||||
public Result<SocialContentItemResponse> updateApproval(@Parameter(description = "内容 ID", required = true) @PathVariable String id,
|
||||
@RequestBody SocialContentApprovalRequest request) {
|
||||
String userId = currentUserId();
|
||||
if (userId == null) {
|
||||
return Result.unauthorized();
|
||||
}
|
||||
SocialContentItemResponse response = socialContentService.updateApproval(userId, id, request.getApprovedForAi());
|
||||
if (response == null) {
|
||||
return Result.notFound("导入内容不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "删除社交内容", description = "删除指定的社交媒体内容记录。")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@Parameter(description = "内容 ID", required = true) @PathVariable String id,
|
||||
@Parameter(description = "是否保留已确认的洞察") @RequestParam(required = false, defaultValue = "true") Boolean keepConfirmedInsights) {
|
||||
String userId = currentUserId();
|
||||
if (userId == null) {
|
||||
return Result.unauthorized();
|
||||
}
|
||||
boolean deleted = socialContentService.deleteByUser(userId, id, keepConfirmedInsights);
|
||||
if (!deleted) {
|
||||
return Result.notFound("导入内容不存在");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
private String currentUserId() {
|
||||
return UserContextHolder.getCurrentUserId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.social.SocialInsightGenerateRequest;
|
||||
import com.emotion.dto.request.social.SocialInsightUpdateRequest;
|
||||
import com.emotion.dto.response.social.SocialProfileInsightResponse;
|
||||
import com.emotion.service.SocialInsightService;
|
||||
import com.emotion.util.UserContextHolder;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.DeleteMapping;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
@Validated
|
||||
@RestController
|
||||
@RequestMapping("/social/insight")
|
||||
@Tag(name = "社交洞察管理", description = "社交媒体洞察生成、查询和更新接口")
|
||||
public class SocialInsightController {
|
||||
|
||||
@Autowired
|
||||
private SocialInsightService socialInsightService;
|
||||
|
||||
@Operation(summary = "生成社交洞察", description = "基于用户社交数据生成 AI 分析洞察报告。")
|
||||
@PostMapping("/generate")
|
||||
public Result<List<SocialProfileInsightResponse>> generate(@RequestBody(required = false) SocialInsightGenerateRequest request) {
|
||||
String userId = currentUserId();
|
||||
if (userId == null) {
|
||||
return Result.unauthorized();
|
||||
}
|
||||
return Result.success(socialInsightService.generateInsights(userId, request));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取洞察列表", description = "查询当前用户的所有社交洞察记录。")
|
||||
@GetMapping("/list")
|
||||
public Result<List<SocialProfileInsightResponse>> list(@Parameter(description = "洞察状态") @RequestParam(required = false) String status) {
|
||||
String userId = currentUserId();
|
||||
if (userId == null) {
|
||||
return Result.unauthorized();
|
||||
}
|
||||
return Result.success(socialInsightService.listByUser(userId, status));
|
||||
}
|
||||
|
||||
@Operation(summary = "更新洞察记录", description = "更新已有洞察的状态或备注信息。")
|
||||
@PutMapping("/{id}")
|
||||
public Result<SocialProfileInsightResponse> update(@Parameter(description = "洞察 ID", required = true) @PathVariable String id,
|
||||
@Valid @RequestBody SocialInsightUpdateRequest request) {
|
||||
String userId = currentUserId();
|
||||
if (userId == null) {
|
||||
return Result.unauthorized();
|
||||
}
|
||||
try {
|
||||
SocialProfileInsightResponse response = socialInsightService.updateByUser(userId, id, request);
|
||||
if (response == null) {
|
||||
return Result.notFound("画像不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return Result.badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "删除洞察记录", description = "删除指定的社交洞察记录。")
|
||||
@DeleteMapping("/{id}")
|
||||
public Result<Void> delete(@Parameter(description = "洞察 ID", required = true) @PathVariable String id) {
|
||||
String userId = currentUserId();
|
||||
if (userId == null) {
|
||||
return Result.unauthorized();
|
||||
}
|
||||
boolean deleted = socialInsightService.deleteByUser(userId, id);
|
||||
if (!deleted) {
|
||||
return Result.notFound("画像不存在");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
private String currentUserId() {
|
||||
return UserContextHolder.getCurrentUserId();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.SystemConfigUpdateRequest;
|
||||
import com.emotion.dto.response.SystemConfigResponse;
|
||||
import com.emotion.service.SystemConfigService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 系统配置管理控制器(管理端)
|
||||
*
|
||||
* @author system
|
||||
* @date 2026-06-27
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/admin/systemConfig")
|
||||
@Tag(name = "系统配置管理", description = "系统配置的查询和更新功能")
|
||||
public class SystemConfigController {
|
||||
|
||||
@Autowired
|
||||
private SystemConfigService systemConfigService;
|
||||
|
||||
/**
|
||||
* 获取所有可见配置列表
|
||||
*/
|
||||
@GetMapping("/list")
|
||||
@Operation(summary = "获取配置列表", description = "获取所有可见的系统配置列表")
|
||||
public Result<List<SystemConfigResponse>> list() {
|
||||
List<SystemConfigResponse> configs = systemConfigService.getVisibleConfigList();
|
||||
return Result.success(configs);
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量更新配置
|
||||
*/
|
||||
@PutMapping("/update")
|
||||
@Operation(summary = "批量更新配置", description = "批量更新系统配置值")
|
||||
public Result<Void> update(@RequestBody @Validated List<SystemConfigUpdateRequest> requests) {
|
||||
systemConfigService.batchUpdateConfigs(requests);
|
||||
return Result.success("更新成功", null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.response.UserInfoResponse;
|
||||
import com.emotion.service.TokenService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
/**
|
||||
* Token控制器
|
||||
* 提供基于请求头Authorization的token验证和用户信息获取功能
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/token")
|
||||
@Tag(name = "Token管理", description = "Token验证和用户信息获取")
|
||||
public class TokenController {
|
||||
|
||||
@Autowired
|
||||
private TokenService tokenService;
|
||||
|
||||
/**
|
||||
* 通过请求头中的token获取用户信息
|
||||
* Token应该在请求头中以 "Authorization: Bearer {token}" 的形式传递
|
||||
*/
|
||||
@Operation(summary = "获取用户信息", description = "通过请求头中的token获取当前用户信息")
|
||||
@GetMapping("/user-info")
|
||||
public Result<UserInfoResponse> getUserInfoByToken(HttpServletRequest request) {
|
||||
UserInfoResponse userInfo = tokenService.getUserInfoByToken(request);
|
||||
return Result.success(userInfo);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过请求头中的token获取用户名
|
||||
* Token应该在请求头中以 "Authorization: Bearer {token}" 的形式传递
|
||||
*/
|
||||
@GetMapping("/username")
|
||||
@Operation(summary = "获取用户名", description = "通过请求头中的token获取当前用户名")
|
||||
public Result<String> getUsernameByToken(HttpServletRequest request) {
|
||||
String username = tokenService.getUsernameByToken(request);
|
||||
return Result.success(username);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证请求头中的token并返回用户ID
|
||||
* Token应该在请求头中以 "Authorization: Bearer {token}" 的形式传递
|
||||
*/
|
||||
@GetMapping("/validate")
|
||||
@Operation(summary = "验证Token", description = "验证请求头中的token并返回用户ID")
|
||||
public Result<String> validateTokenAndGetUserId(HttpServletRequest request) {
|
||||
String userId = tokenService.validateTokenAndGetUserId(request);
|
||||
return Result.success(userId);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.TopicInteractionCreateRequest;
|
||||
import com.emotion.dto.request.TopicInteractionPageRequest;
|
||||
import com.emotion.dto.request.TopicInteractionUpdateRequest;
|
||||
import com.emotion.dto.response.TopicInteractionResponse;
|
||||
import com.emotion.service.TopicInteractionService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* 话题互动控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/topicInteraction")
|
||||
@Tag(name = "话题互动管理", description = "话题互动记录的查询、创建、更新和删除接口")
|
||||
public class TopicInteractionController {
|
||||
|
||||
@Autowired
|
||||
private TopicInteractionService topicInteractionService;
|
||||
|
||||
/**
|
||||
* 分页查询话题互动
|
||||
*/
|
||||
@Operation(summary = "分页查询互动记录", description = "分页查询话题互动记录列表。")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<TopicInteractionResponse>> getPage(@Validated TopicInteractionPageRequest request) {
|
||||
PageResult<TopicInteractionResponse> pageResult = topicInteractionService.getPageWithResponse(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取话题互动
|
||||
*/
|
||||
@Operation(summary = "获取互动详情", description = "根据 ID 获取互动记录的详细信息。")
|
||||
@GetMapping(value = "/detail")
|
||||
public Result<TopicInteractionResponse> getById(@Parameter(description = "互动 ID") @RequestParam String id) {
|
||||
TopicInteractionResponse response = topicInteractionService.getTopicInteractionResponseById(id);
|
||||
if (response == null) {
|
||||
return Result.notFound("话题互动不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建话题互动
|
||||
*/
|
||||
@Operation(summary = "创建互动记录", description = "创建一条新的话题互动记录。")
|
||||
@PostMapping(value = "/create")
|
||||
public Result<TopicInteractionResponse> create(@Valid @RequestBody TopicInteractionCreateRequest request) {
|
||||
TopicInteractionResponse response = topicInteractionService.createTopicInteractionWithResponse(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新话题互动
|
||||
*/
|
||||
@Operation(summary = "更新互动记录", description = "修改已有话题互动的内容。")
|
||||
@PutMapping(value = "/update")
|
||||
public Result<TopicInteractionResponse> update(@Valid @RequestBody TopicInteractionUpdateRequest request) {
|
||||
TopicInteractionResponse response = topicInteractionService.updateTopicInteractionWithResponse(request);
|
||||
if (response == null) {
|
||||
return Result.notFound("话题互动不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除话题互动
|
||||
*/
|
||||
@Operation(summary = "删除互动记录", description = "删除指定的话题互动记录。")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<Void> delete(@Parameter(description = "互动 ID") @RequestParam String id) {
|
||||
boolean deleted = topicInteractionService.deleteTopicInteraction(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.tts.TtsTaskCreateRequest;
|
||||
import com.emotion.dto.response.tts.TtsTaskResponse;
|
||||
import com.emotion.service.TtsTaskService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.CacheControl;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RestController
|
||||
@RequestMapping("/tts")
|
||||
@Tag(name = "语音合成(TTS)", description = "文字转语音任务创建和查询接口")
|
||||
public class TtsController {
|
||||
|
||||
private final TtsTaskService ttsTaskService;
|
||||
|
||||
@Value("${emotion.tts.output-dir:/data/uploads/emotion-museum/tts}")
|
||||
private String outputDir;
|
||||
|
||||
public TtsController(TtsTaskService ttsTaskService) {
|
||||
this.ttsTaskService = ttsTaskService;
|
||||
}
|
||||
|
||||
@Operation(summary = "创建语音合成任务", description = "根据文本内容创建语音合成任务,返回任务信息。")
|
||||
@PostMapping("/tasks")
|
||||
public Result<TtsTaskResponse> create(@Valid @RequestBody TtsTaskCreateRequest request) {
|
||||
try {
|
||||
return Result.success(ttsTaskService.createOrReuse(request));
|
||||
} catch (IllegalArgumentException | IllegalStateException e) {
|
||||
return Result.badRequest(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Operation(summary = "获取语音合成任务详情", description = "根据任务 ID 获取语音合成任务的详细信息。")
|
||||
@GetMapping("/tasks/{id}")
|
||||
public Result<TtsTaskResponse> detail(@Parameter(description = "任务 ID") @PathVariable String id) {
|
||||
TtsTaskResponse response = ttsTaskService.getTask(id);
|
||||
return response == null ? Result.notFound("TTS task not found") : Result.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "根据来源查询语音合成任务", description = "根据来源类型和来源 ID 查询已存在的语音合成任务。")
|
||||
@GetMapping("/tasks/by-source")
|
||||
public Result<TtsTaskResponse> bySource(@Parameter(description = "来源类型") @RequestParam String sourceType,
|
||||
@Parameter(description = "来源 ID") @RequestParam String sourceId,
|
||||
@Parameter(description = "音色") @RequestParam(required = false) String voice,
|
||||
@Parameter(description = "语速") @RequestParam(required = false) Double speechRate,
|
||||
@Parameter(description = "音调") @RequestParam(required = false) Double pitch,
|
||||
@Parameter(description = "情绪") @RequestParam(required = false) String emotion) {
|
||||
return Result.success(ttsTaskService.getBySource(sourceType, sourceId, voice, speechRate, pitch, emotion));
|
||||
}
|
||||
|
||||
@Operation(summary = "获取音频文件", description = "返回已合成的音频音频文件(MP3 或 WAV 格式)。")
|
||||
@GetMapping("/audio/{filename:.+}")
|
||||
public ResponseEntity<Resource> audio(@Parameter(description = "音频文件名") @PathVariable String filename) {
|
||||
if (filename.contains("..") || filename.contains("/") || filename.contains("\\")) {
|
||||
return ResponseEntity.badRequest().build();
|
||||
}
|
||||
|
||||
Path path = Paths.get(outputDir).resolve(filename).normalize();
|
||||
FileSystemResource resource = new FileSystemResource(path);
|
||||
if (!resource.exists() || !resource.isReadable()) {
|
||||
return ResponseEntity.notFound().build();
|
||||
}
|
||||
|
||||
MediaType mediaType = filename.endsWith(".wav")
|
||||
? MediaType.valueOf("audio/wav")
|
||||
: MediaType.valueOf("audio/mpeg");
|
||||
return ResponseEntity.ok()
|
||||
.contentType(mediaType)
|
||||
.cacheControl(CacheControl.maxAge(30, TimeUnit.DAYS).cachePublic())
|
||||
.body(resource);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.UserCreateRequest;
|
||||
import com.emotion.dto.request.UserPageRequest;
|
||||
import com.emotion.dto.request.UserProfileUpdateRequest;
|
||||
import com.emotion.dto.request.UserUpdateRequest;
|
||||
import com.emotion.dto.response.UserResponse;
|
||||
import com.emotion.service.UserService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* 用户控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-22
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/user")
|
||||
@Tag(name = "用户管理", description = "管理员对用户的增删改查功能")
|
||||
public class UserController {
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
/**
|
||||
* 分页查询用户
|
||||
*/
|
||||
@Operation(summary = "分页查询用户", description = "分页查询用户列表")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<UserResponse>> getPage(@Validated UserPageRequest request) {
|
||||
PageResult<UserResponse> pageResult = userService.getPageWithResponse(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID获取用户信息
|
||||
*/
|
||||
@Operation(summary = "根据ID获取用户信息", description = "根据ID获取用户详情")
|
||||
@GetMapping(value = "/detail")
|
||||
public Result<UserResponse> getById(
|
||||
@Parameter(description = "用户ID", required = true)
|
||||
@RequestParam String id) {
|
||||
UserResponse user = userService.getUserResponseById(id);
|
||||
if (user == null) {
|
||||
return Result.notFound("用户不存在");
|
||||
}
|
||||
return Result.success(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建用户
|
||||
*/
|
||||
@Operation(summary = "创建用户", description = "创建新用户")
|
||||
@PostMapping(value = "/create")
|
||||
public Result<UserResponse> create(@Valid @RequestBody UserCreateRequest request) {
|
||||
UserResponse user = userService.createUserWithResponse(request);
|
||||
return Result.success(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户
|
||||
*/
|
||||
@Operation(summary = "更新用户", description = "更新指定用户信息")
|
||||
@PutMapping(value = "/update")
|
||||
public Result<UserResponse> update(@Valid @RequestBody UserUpdateRequest request) {
|
||||
UserResponse updatedUser = userService.updateUserWithResponse(request);
|
||||
if (updatedUser == null) {
|
||||
return Result.error("更新失败");
|
||||
}
|
||||
return Result.success(updatedUser);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除用户
|
||||
*/
|
||||
@Operation(summary = "删除用户", description = "删除指定用户")
|
||||
@DeleteMapping(value = "/delete")
|
||||
public Result<Void> delete(
|
||||
@Parameter(description = "用户ID", required = true)
|
||||
@RequestParam String id) {
|
||||
boolean deleted = userService.deleteUser(id);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前用户个人资料
|
||||
*/
|
||||
@Operation(summary = "获取当前用户个人资料", description = "获取当前登录用户的个人资料")
|
||||
@GetMapping(value = "/profile")
|
||||
public Result<UserResponse> getCurrentUserProfile() {
|
||||
UserResponse user = userService.getCurrentUserProfileWithResponse();
|
||||
if (user == null) {
|
||||
return Result.unauthorized("用户未登录");
|
||||
}
|
||||
return Result.success(user);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新当前用户个人资料
|
||||
*/
|
||||
@PutMapping(value = "/profile")
|
||||
public Result<UserResponse> updateCurrentUserProfile(@Valid @RequestBody UserProfileUpdateRequest request) {
|
||||
UserResponse updatedUser = userService.updateCurrentUserProfileWithResponse(request);
|
||||
if (updatedUser == null) {
|
||||
return Result.unauthorized("用户未登录");
|
||||
}
|
||||
return Result.success(updatedUser);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.userprofile.UserProfileCreateRequest;
|
||||
import com.emotion.dto.request.userprofile.UserProfilePageRequest;
|
||||
import com.emotion.dto.request.userprofile.UserProfileUpdateRequest;
|
||||
import com.emotion.dto.response.userprofile.UserProfileResponse;
|
||||
import com.emotion.service.UserProfileService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户档案控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-12-21
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/user-profile")
|
||||
@Tag(name = "用户档案管理", description = "用户档案的查询、创建、更新、删除和 AI 设置管理接口")
|
||||
public class UserProfileController {
|
||||
|
||||
@Autowired
|
||||
private UserProfileService userProfileService;
|
||||
|
||||
/**
|
||||
* 新增档案
|
||||
*/
|
||||
@Operation(summary = "创建用户档案", description = "为当前用户创建个人档案,包含昵称、MBTI 人格类型、童年经历等。")
|
||||
@PostMapping("/create")
|
||||
public Result<UserProfileResponse> create(@Valid @RequestBody UserProfileCreateRequest request) {
|
||||
UserProfileResponse response = userProfileService.createProfile(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除档案
|
||||
*/
|
||||
@Operation(summary = "删除用户档案", description = "删除指定的用户档案。")
|
||||
@DeleteMapping("/delete")
|
||||
public Result<Void> delete(@Parameter(description = "档案 ID") @RequestParam String id) {
|
||||
boolean success = userProfileService.deleteProfile(id);
|
||||
if (success) {
|
||||
return Result.success();
|
||||
} else {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改档案
|
||||
*/
|
||||
@Operation(summary = "更新用户档案", description = "更新当前用户的个人档案信息。")
|
||||
@PutMapping("/update")
|
||||
public Result<UserProfileResponse> update(@Valid @RequestBody UserProfileUpdateRequest request) {
|
||||
UserProfileResponse response = userProfileService.updateProfile(request);
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据ID查询详情
|
||||
*/
|
||||
@Operation(summary = "获取档案详情", description = "根据 ID 获取用户档案详情。")
|
||||
@GetMapping("/detail")
|
||||
public Result<UserProfileResponse> getById(@Parameter(description = "档案 ID") @RequestParam String id) {
|
||||
UserProfileResponse response = userProfileService.getProfileById(id);
|
||||
if (response == null) {
|
||||
return Result.notFound("档案不存在");
|
||||
}
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前登录用户的档案
|
||||
*/
|
||||
@Operation(summary = "获取当前用户档案", description = "获取当前登录用户的个人档案信息。")
|
||||
@GetMapping("/me")
|
||||
public Result<UserProfileResponse> getCurrentProfile() {
|
||||
UserProfileResponse response = userProfileService.getCurrentUserProfile();
|
||||
// 如果不存在,返回null data,不报错
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 分页查询
|
||||
*/
|
||||
@Operation(summary = "分页查询用户档案", description = "分页查询用户档案列表。")
|
||||
@GetMapping("/page")
|
||||
public Result<PageResult<UserProfileResponse>> getPage(@Validated UserProfilePageRequest request) {
|
||||
PageResult<UserProfileResponse> pageResult = userProfileService.getProfilePage(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 列表查询
|
||||
*/
|
||||
@Operation(summary = "列表查询用户档案", description = "列表查询用户档案。")
|
||||
@GetMapping("/list")
|
||||
public Result<List<UserProfileResponse>> getList(@Validated UserProfilePageRequest request) {
|
||||
List<UserProfileResponse> list = userProfileService.getProfileList(request);
|
||||
return Result.success(list);
|
||||
}
|
||||
|
||||
/**
|
||||
* 迁移用户档案中的生命事件数据到t_life_event表
|
||||
* 将childhood、peak、valley数据迁移到生命事件表
|
||||
* 注意:此接口为一次性数据迁移接口,迁移完成后可删除
|
||||
*/
|
||||
@Operation(summary = "迁移生活事件", description = "将生活事件模块的数据迁移到用户档案中。")
|
||||
@PostMapping("/migrateLifeEvents")
|
||||
public Result<Integer> migrateLifeEvents() {
|
||||
int count = userProfileService.migrateLifeEventsFromProfiles();
|
||||
return Result.success(count);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.emotion.controller;
|
||||
|
||||
import com.emotion.common.PageResult;
|
||||
import com.emotion.common.Result;
|
||||
import com.emotion.dto.request.UserStatsCreateRequest;
|
||||
import com.emotion.dto.request.UserStatsIncrementRequest;
|
||||
import com.emotion.dto.request.UserStatsPageRequest;
|
||||
import com.emotion.dto.request.UserStatsUpdateValueRequest;
|
||||
import com.emotion.dto.response.UserStatsResponse;
|
||||
import com.emotion.service.UserStatsService;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 用户统计控制器
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("/userStats")
|
||||
@Validated
|
||||
@Tag(name = "用户统计管理", description = "用户统计数据的查询、创建、更新、增量操作和重新计算接口")
|
||||
public class UserStatsController {
|
||||
|
||||
@Autowired
|
||||
private UserStatsService userStatsService;
|
||||
|
||||
/**
|
||||
* 分页查询用户统计
|
||||
*/
|
||||
@Operation(summary = "分页查询用户统计", description = "分页查询用户统计数据。")
|
||||
@GetMapping(value = "/page")
|
||||
public Result<PageResult<UserStatsResponse>> getPage(@Validated UserStatsPageRequest request) {
|
||||
PageResult<UserStatsResponse> pageResult = userStatsService.getPageWithResponse(request);
|
||||
return Result.success(pageResult);
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建或更新用户统计
|
||||
*/
|
||||
@Operation(summary = "创建或更新用户统计", description = "创建新用户统计数据或更新已有数据。")
|
||||
@PostMapping(value = "/createOrUpdate")
|
||||
public Result<UserStatsResponse> createOrUpdate(@Valid @RequestBody UserStatsCreateRequest request) {
|
||||
UserStatsResponse stats = userStatsService.createOrUpdateUserStatsWithResponse(request);
|
||||
return Result.success(stats);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新用户统计值
|
||||
*/
|
||||
@Operation(summary = "更新用户统计值", description = "直接更新指定统计项的值。")
|
||||
@PutMapping(value = "/updateStatsValue")
|
||||
public Result<Void> updateStatsValue(@Valid @RequestBody UserStatsUpdateValueRequest request) {
|
||||
boolean updated = userStatsService.updateStatsValue(request);
|
||||
if (!updated) {
|
||||
return Result.error("更新失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 增加用户统计值
|
||||
*/
|
||||
@Operation(summary = "增加用户统计值", description = "对指定统计项进行增量累加操作。")
|
||||
@PutMapping(value = "/incrementStatsValue")
|
||||
public Result<Void> incrementStatsValue(@Valid @RequestBody UserStatsIncrementRequest request) {
|
||||
boolean updated = userStatsService.incrementStatsValue(request);
|
||||
if (!updated) {
|
||||
return Result.error("增加失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新计算用户统计
|
||||
*/
|
||||
@Operation(summary = "重新计算用户统计", description = "基于原始数据重新计算用户的统计值。")
|
||||
@PutMapping(value = "/recalculateUserStats")
|
||||
public Result<Void> recalculateUserStats(@Parameter(description = "用户 ID") @RequestParam String userId) {
|
||||
boolean recalculated = userStatsService.recalculateUserStats(userId);
|
||||
if (!recalculated) {
|
||||
return Result.error("重新计算失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新计算所有用户统计
|
||||
*/
|
||||
@Operation(summary = "重新计算所有用户统计", description = "重新计算所有用户的统计值(管理员操作)。")
|
||||
@PutMapping(value = "/recalculateAll")
|
||||
public Result<Void> recalculateAllUserStats() {
|
||||
boolean recalculated = userStatsService.recalculateAllUserStats();
|
||||
if (!recalculated) {
|
||||
return Result.error("重新计算失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除过期的统计数据
|
||||
*/
|
||||
@Operation(summary = "删除过期统计数据", description = "清理超过指定天数的过期统计数据。")
|
||||
@DeleteMapping(value = "/deleteExpired")
|
||||
public Result<Void> deleteExpiredStats(@Parameter(description = "过期天数阈值") @RequestParam(defaultValue = "30") Integer days) {
|
||||
boolean deleted = userStatsService.deleteExpiredStats(days);
|
||||
if (!deleted) {
|
||||
return Result.error("删除失败");
|
||||
}
|
||||
return Result.success();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 管理员修改密码请求(修改自己的密码,需要原密码)
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2026-05-10
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "管理员修改密码请求")
|
||||
public class AdminChangePasswordRequest {
|
||||
|
||||
@NotBlank(message = "原密码不能为空")
|
||||
@Schema(description = "原密码")
|
||||
private String oldPassword;
|
||||
|
||||
@NotBlank(message = "新密码不能为空")
|
||||
@Schema(description = "新密码")
|
||||
private String newPassword;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.Email;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 管理员创建请求
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-10-27
|
||||
*/
|
||||
@Data
|
||||
public class AdminCreateRequest {
|
||||
|
||||
/**
|
||||
* 管理员账号
|
||||
*/
|
||||
@NotBlank(message = "账号不能为空")
|
||||
@Size(min = 3, max = 50, message = "账号长度必须在3-50个字符之间")
|
||||
@Schema(description = "管理员账号")
|
||||
private String account;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间")
|
||||
@Schema(description = "初始密码")
|
||||
private String password;
|
||||
|
||||
/**
|
||||
* 管理员姓名
|
||||
*/
|
||||
@NotBlank(message = "姓名不能为空")
|
||||
@Size(min = 2, max = 50, message = "姓名长度必须在2-50个字符之间")
|
||||
@Schema(description = "管理员姓名")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
@Email(message = "邮箱格式不正确")
|
||||
@Size(max = 100, message = "邮箱长度不能超过100个字符")
|
||||
@Schema(description = "邮箱")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
@Schema(description = "手机号")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 头像URL
|
||||
*/
|
||||
@Size(max = 500, message = "头像URL长度不能超过500个字符")
|
||||
@Schema(description = "头像URL")
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 角色
|
||||
*/
|
||||
@NotBlank(message = "角色不能为空")
|
||||
@Pattern(regexp = "^(super_admin|admin|operator)$", message = "角色必须是super_admin、admin或operator")
|
||||
@Schema(description = "角色(super_admin/admin/operator)")
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* 权限列表(JSON格式)
|
||||
*/
|
||||
@Schema(description = "权限列表(JSON格式)")
|
||||
private String permissions;
|
||||
|
||||
/**
|
||||
* 所属部门
|
||||
*/
|
||||
@Size(max = 50, message = "部门长度不能超过50个字符")
|
||||
@Schema(description = "所属部门")
|
||||
private String department;
|
||||
|
||||
/**
|
||||
* 职位
|
||||
*/
|
||||
@Size(max = 50, message = "职位长度不能超过50个字符")
|
||||
@Schema(description = "职位")
|
||||
private String position;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 管理员登录请求
|
||||
*
|
||||
* @author emotion-museum
|
||||
* @date 2025-10-27
|
||||
*/
|
||||
@Data
|
||||
public class AdminLoginRequest {
|
||||
|
||||
/**
|
||||
* 账号
|
||||
*/
|
||||
@NotBlank(message = "账号不能为空")
|
||||
@Size(min = 3, max = 50, message = "账号长度必须在3-50个字符之间")
|
||||
@Schema(description = "管理员账号")
|
||||
private String account;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Size(min = 6, max = 20, message = "密码长度必须在6-20个字符之间")
|
||||
@Schema(description = "管理员密码")
|
||||
private String password;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 管理员分页查询请求
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-10-27
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class AdminPageRequest extends BasePageRequest {
|
||||
|
||||
/**
|
||||
* 账号
|
||||
*/
|
||||
@Size(max = 50, message = "账号长度不能超过50个字符")
|
||||
@Schema(description = "账号(模糊搜索)")
|
||||
private String account;
|
||||
|
||||
/**
|
||||
* 姓名
|
||||
*/
|
||||
@Size(max = 50, message = "姓名长度不能超过50个字符")
|
||||
@Schema(description = "姓名(模糊搜索)")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
@Size(max = 100, message = "邮箱长度不能超过100个字符")
|
||||
@Schema(description = "邮箱(模糊搜索)")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@Size(max = 20, message = "手机号长度不能超过20个字符")
|
||||
@Schema(description = "手机号(模糊搜索)")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 角色
|
||||
*/
|
||||
@Size(max = 20, message = "角色长度不能超过20个字符")
|
||||
@Schema(description = "角色")
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* 状态: 0-禁用, 1-正常
|
||||
*/
|
||||
@Schema(description = "状态(0-禁用,1-正常)")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 部门
|
||||
*/
|
||||
@Size(max = 50, message = "部门长度不能超过50个字符")
|
||||
@Schema(description = "部门(模糊搜索)")
|
||||
private String department;
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 超级管理员重置其他管理员密码请求(不需要原密码)
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2026-05-10
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "管理员重置密码请求")
|
||||
public class AdminResetPasswordRequest {
|
||||
|
||||
@NotBlank(message = "管理员ID不能为空")
|
||||
@Schema(description = "管理员ID")
|
||||
private String id;
|
||||
|
||||
@NotBlank(message = "新密码不能为空")
|
||||
@Schema(description = "新密码")
|
||||
private String newPassword;
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.Email;
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Pattern;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 管理员更新请求
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-10-27
|
||||
*/
|
||||
@Data
|
||||
public class AdminUpdateRequest {
|
||||
|
||||
/**
|
||||
* ID
|
||||
*/
|
||||
@NotBlank(message = "ID不能为空")
|
||||
@Schema(description = "管理员ID")
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 管理员姓名
|
||||
*/
|
||||
@Size(min = 2, max = 50, message = "姓名长度必须在2-50个字符之间")
|
||||
@Schema(description = "管理员姓名")
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
@Email(message = "邮箱格式不正确")
|
||||
@Size(max = 100, message = "邮箱长度不能超过100个字符")
|
||||
@Schema(description = "邮箱")
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 手机号
|
||||
*/
|
||||
@Pattern(regexp = "^1[3-9]\\d{9}$", message = "手机号格式不正确")
|
||||
@Schema(description = "手机号")
|
||||
private String phone;
|
||||
|
||||
/**
|
||||
* 头像URL
|
||||
*/
|
||||
@Size(max = 500, message = "头像URL长度不能超过500个字符")
|
||||
@Schema(description = "头像URL")
|
||||
private String avatar;
|
||||
|
||||
/**
|
||||
* 角色
|
||||
*/
|
||||
@Pattern(regexp = "^(super_admin|admin|operator)$", message = "角色必须是super_admin、admin或operator")
|
||||
@Schema(description = "角色(super_admin/admin/operator)")
|
||||
private String role;
|
||||
|
||||
/**
|
||||
* 权限列表(JSON格式)
|
||||
*/
|
||||
@Schema(description = "权限列表(JSON格式)")
|
||||
private String permissions;
|
||||
|
||||
/**
|
||||
* 状态: 0-禁用, 1-正常
|
||||
*/
|
||||
@Schema(description = "状态(0-禁用,1-正常)")
|
||||
private Integer status;
|
||||
|
||||
/**
|
||||
* 所属部门
|
||||
*/
|
||||
@Size(max = 50, message = "部门长度不能超过50个字符")
|
||||
@Schema(description = "所属部门")
|
||||
private String department;
|
||||
|
||||
/**
|
||||
* 职位
|
||||
*/
|
||||
@Size(max = 50, message = "职位长度不能超过50个字符")
|
||||
@Schema(description = "职位")
|
||||
private String position;
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* AI聊天请求类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class AiChatRequest extends BaseRequest {
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String conversationId;
|
||||
|
||||
/**
|
||||
* 消息内容
|
||||
*/
|
||||
@NotBlank(message = "消息内容不能为空")
|
||||
private String message;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private String userId;
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.Max;
|
||||
import javax.validation.constraints.Min;
|
||||
|
||||
/**
|
||||
* AI配置调用次数统计请求
|
||||
* 用于管理后台仪表盘:按 t_ai_config 配置统计 t_coze_api_call 调用次数
|
||||
*
|
||||
* @author system
|
||||
* @date 2025-12-24
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "AI配置调用次数统计请求")
|
||||
public class AiConfigCallStatsRequest {
|
||||
|
||||
/**
|
||||
* 返回条数限制(默认 20,最大 200)
|
||||
*/
|
||||
@Min(1)
|
||||
@Max(200)
|
||||
@Schema(description = "返回条数限制(默认20,最大200)", example = "20")
|
||||
private Integer limit;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* AI总结请求类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class AiSummaryRequest extends BaseRequest {
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
@NotBlank(message = "会话ID不能为空")
|
||||
private String conversationId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private String userId;
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 接口端点分页查询请求
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ApiEndpointListRequest extends BasePageRequest {
|
||||
|
||||
/**
|
||||
* HTTP 方法过滤:GET/POST/PUT/DELETE/PATCH
|
||||
*/
|
||||
private String method;
|
||||
|
||||
/**
|
||||
* 标签过滤
|
||||
*/
|
||||
private String tags;
|
||||
|
||||
/**
|
||||
* 是否仅显示废弃接口
|
||||
*/
|
||||
private Integer deprecated;
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 代理测试请求参数
|
||||
*/
|
||||
@Data
|
||||
public class ApiTestProxyRequest {
|
||||
|
||||
@NotBlank(message = "请求方法不能为空")
|
||||
private String method;
|
||||
|
||||
@NotBlank(message = "接口路径不能为空")
|
||||
private String path;
|
||||
|
||||
private String body;
|
||||
|
||||
private Map<String, String> headers;
|
||||
|
||||
private Map<String, String> params;
|
||||
|
||||
private Integer timeoutSeconds;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 基础请求类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
public class BaseRequest {
|
||||
|
||||
/**
|
||||
* 请求ID,用于链路追踪
|
||||
*/
|
||||
private String requestId;
|
||||
|
||||
/**
|
||||
* 客户端时间戳
|
||||
*/
|
||||
private Long timestamp;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 聊天统计请求类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ChatStatsRequest extends BaseRequest {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 会话ID
|
||||
*/
|
||||
private String conversationId;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 对话创建请求类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-24
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ConversationCreateRequest extends BaseRequest {
|
||||
|
||||
/**
|
||||
* 对话ID(更新时使用)
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@NotBlank(message = "用户ID不能为空")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 对话标题
|
||||
*/
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 对话类型
|
||||
*/
|
||||
private String type;
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 对话分页请求类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class ConversationPageRequest extends PageRequest {
|
||||
|
||||
/**
|
||||
* 用户ID(可选)
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 对话状态(可选)
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 对话类型(可选)
|
||||
*/
|
||||
private String type;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 创建日记评论请求DTO
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
public class DiaryCommentCreateRequest {
|
||||
|
||||
/**
|
||||
* 评论ID (用于更新操作)
|
||||
*/
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 日记ID
|
||||
*/
|
||||
@NotBlank(message = "日记ID不能为空")
|
||||
private String diaryId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@NotBlank(message = "用户ID不能为空")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 评论内容
|
||||
*/
|
||||
@NotBlank(message = "评论内容不能为空")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 评论图片
|
||||
*/
|
||||
private List<String> images;
|
||||
|
||||
/**
|
||||
* 父评论ID (用于回复功能)
|
||||
*/
|
||||
private String parentCommentId;
|
||||
|
||||
/**
|
||||
* 是否匿名评论: 0-实名, 1-匿名
|
||||
*/
|
||||
@NotNull(message = "是否匿名不能为空")
|
||||
private Integer isAnonymous;
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.Pattern;
|
||||
|
||||
/**
|
||||
* 日记评论分页请求类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class DiaryCommentPageRequest extends BasePageRequest {
|
||||
|
||||
/**
|
||||
* 日记ID(可选)
|
||||
*/
|
||||
private String diaryId;
|
||||
|
||||
/**
|
||||
* 用户ID(可选)
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 评论类型(可选)
|
||||
*/
|
||||
private String commentType;
|
||||
|
||||
/**
|
||||
* 是否只查询顶级评论(可选)
|
||||
*/
|
||||
private Boolean topLevelOnly;
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 创建日记请求DTO
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
public class DiaryPostCreateRequest {
|
||||
|
||||
/**
|
||||
* 日记标题
|
||||
*/
|
||||
@Size(max = 200, message = "日记标题长度不能超过200个字符")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 日记内容
|
||||
*/
|
||||
@NotBlank(message = "日记内容不能为空")
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 图片列表
|
||||
*/
|
||||
private List<String> images;
|
||||
|
||||
/**
|
||||
* 视频列表
|
||||
*/
|
||||
private List<String> videos;
|
||||
|
||||
/**
|
||||
* 发布地点
|
||||
*/
|
||||
@Size(max = 200, message = "发布地点长度不能超过200个字符")
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 纬度
|
||||
*/
|
||||
private BigDecimal latitude;
|
||||
|
||||
/**
|
||||
* 经度
|
||||
*/
|
||||
private BigDecimal longitude;
|
||||
|
||||
/**
|
||||
* 天气信息
|
||||
*/
|
||||
@Size(max = 50, message = "天气信息长度不能超过50个字符")
|
||||
private String weather;
|
||||
|
||||
/**
|
||||
* 心情状态
|
||||
*/
|
||||
@Size(max = 50, message = "心情状态长度不能超过50个字符")
|
||||
private String mood;
|
||||
|
||||
/**
|
||||
* 心情评分 (0-10)
|
||||
*/
|
||||
private BigDecimal moodScore;
|
||||
|
||||
/**
|
||||
* 标签列表
|
||||
*/
|
||||
private List<String> tags;
|
||||
|
||||
/**
|
||||
* 是否公开: 0-仅自己可见, 1-公开
|
||||
*/
|
||||
@NotNull(message = "是否公开不能为空")
|
||||
private Integer isPublic;
|
||||
|
||||
/**
|
||||
* 是否匿名发布: 0-实名, 1-匿名
|
||||
*/
|
||||
@NotNull(message = "是否匿名不能为空")
|
||||
private Integer isAnonymous;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@NotBlank(message = "用户ID不能为空")
|
||||
private String userId;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
/**
|
||||
* 日记分页请求类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class DiaryPostPageRequest extends BasePageRequest {
|
||||
|
||||
/**
|
||||
* 用户ID(可选)
|
||||
*/
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 是否只查询公开日记(可选)
|
||||
*/
|
||||
private Boolean publicOnly;
|
||||
|
||||
/**
|
||||
* 是否只查询精选日记(可选)
|
||||
*/
|
||||
private Boolean featuredOnly;
|
||||
|
||||
/**
|
||||
* 心情状态(可选)
|
||||
*/
|
||||
private String mood;
|
||||
|
||||
/**
|
||||
* 标签(可选)
|
||||
*/
|
||||
private String tag;
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 更新日记请求DTO
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-07-23
|
||||
*/
|
||||
@Data
|
||||
public class DiaryPostUpdateRequest {
|
||||
|
||||
/**
|
||||
* 日记ID
|
||||
*/
|
||||
@NotBlank(message = "日记ID不能为空")
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 日记标题
|
||||
*/
|
||||
@Size(max = 200, message = "日记标题长度不能超过200个字符")
|
||||
private String title;
|
||||
|
||||
/**
|
||||
* 日记内容
|
||||
*/
|
||||
private String content;
|
||||
|
||||
/**
|
||||
* 图片列表
|
||||
*/
|
||||
private List<String> images;
|
||||
|
||||
/**
|
||||
* 视频列表
|
||||
*/
|
||||
private List<String> videos;
|
||||
|
||||
/**
|
||||
* 发布地点
|
||||
*/
|
||||
@Size(max = 200, message = "发布地点长度不能超过200个字符")
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 纬度
|
||||
*/
|
||||
private BigDecimal latitude;
|
||||
|
||||
/**
|
||||
* 经度
|
||||
*/
|
||||
private BigDecimal longitude;
|
||||
|
||||
/**
|
||||
* 天气信息
|
||||
*/
|
||||
@Size(max = 50, message = "天气信息长度不能超过50个字符")
|
||||
private String weather;
|
||||
|
||||
/**
|
||||
* 心情状态
|
||||
*/
|
||||
@Size(max = 50, message = "心情状态长度不能超过50个字符")
|
||||
private String mood;
|
||||
|
||||
/**
|
||||
* 心情评分 (0-10)
|
||||
*/
|
||||
private BigDecimal moodScore;
|
||||
|
||||
/**
|
||||
* 标签列表
|
||||
*/
|
||||
private List<String> tags;
|
||||
|
||||
/**
|
||||
* 是否公开: 0-仅自己可见, 1-公开
|
||||
*/
|
||||
private Integer isPublic;
|
||||
|
||||
/**
|
||||
* 是否匿名发布: 0-实名, 1-匿名
|
||||
*/
|
||||
private Integer isAnonymous;
|
||||
|
||||
/**
|
||||
* 状态: draft-草稿, published-已发布, hidden-隐藏, deleted-已删除
|
||||
*/
|
||||
private String status;
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 情绪分析创建请求类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@Data
|
||||
public class EmotionAnalysisCreateRequest {
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
@NotBlank(message = "消息ID不能为空")
|
||||
@Size(max = 64, message = "消息ID长度不能超过64个字符")
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@NotBlank(message = "用户ID不能为空")
|
||||
@Size(max = 32, message = "用户ID长度不能超过32个字符")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 主要情绪
|
||||
*/
|
||||
@NotBlank(message = "主要情绪不能为空")
|
||||
@Size(max = 32, message = "主要情绪长度不能超过32个字符")
|
||||
private String primaryEmotion;
|
||||
|
||||
/**
|
||||
* 情绪极性
|
||||
*/
|
||||
@Size(max = 9, message = "情绪极性长度不能超过9个字符")
|
||||
private String polarity;
|
||||
|
||||
/**
|
||||
* 情绪强度
|
||||
*/
|
||||
@NotNull(message = "情绪强度不能为空")
|
||||
private Double intensity;
|
||||
|
||||
/**
|
||||
* 置信度
|
||||
*/
|
||||
@NotNull(message = "置信度不能为空")
|
||||
private Double confidence;
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 情绪分析分页请求类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class EmotionAnalysisPageRequest extends BasePageRequest {
|
||||
|
||||
/**
|
||||
* 用户ID(可选)
|
||||
*/
|
||||
@Size(max = 32, message = "用户ID长度不能超过32个字符")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 消息ID(可选)
|
||||
*/
|
||||
@Size(max = 64, message = "消息ID长度不能超过64个字符")
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 主要情绪(可选)
|
||||
*/
|
||||
@Size(max = 32, message = "主要情绪长度不能超过32个字符")
|
||||
private String primaryEmotion;
|
||||
|
||||
/**
|
||||
* 情绪极性(可选)
|
||||
*/
|
||||
@Size(max = 9, message = "情绪极性长度不能超过9个字符")
|
||||
private String polarity;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 情绪分析更新请求类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@Data
|
||||
public class EmotionAnalysisUpdateRequest {
|
||||
|
||||
/**
|
||||
* 情绪分析ID
|
||||
*/
|
||||
@NotBlank(message = "情绪分析ID不能为空")
|
||||
@Size(max = 32, message = "情绪分析ID长度不能超过32个字符")
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 消息ID
|
||||
*/
|
||||
@Size(max = 64, message = "消息ID长度不能超过64个字符")
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@Size(max = 32, message = "用户ID长度不能超过32个字符")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 主要情绪
|
||||
*/
|
||||
@Size(max = 32, message = "主要情绪长度不能超过32个字符")
|
||||
private String primaryEmotion;
|
||||
|
||||
/**
|
||||
* 情绪极性
|
||||
*/
|
||||
@Size(max = 9, message = "情绪极性长度不能超过9个字符")
|
||||
private String polarity;
|
||||
|
||||
/**
|
||||
* 情绪强度
|
||||
*/
|
||||
private Double intensity;
|
||||
|
||||
/**
|
||||
* 置信度
|
||||
*/
|
||||
private Double confidence;
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.NotNull;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 情绪记录创建请求类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@Data
|
||||
public class EmotionRecordCreateRequest {
|
||||
|
||||
/**
|
||||
* 用户ID
|
||||
*/
|
||||
@NotBlank(message = "用户ID不能为空")
|
||||
@Size(max = 32, message = "用户ID长度不能超过32个字符")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 记录日期
|
||||
*/
|
||||
@NotNull(message = "记录日期不能为空")
|
||||
private LocalDate recordDate;
|
||||
|
||||
/**
|
||||
* 情绪类型
|
||||
*/
|
||||
@NotBlank(message = "情绪类型不能为空")
|
||||
@Size(max = 32, message = "情绪类型长度不能超过32个字符")
|
||||
private String emotionType;
|
||||
|
||||
/**
|
||||
* 情绪强度
|
||||
*/
|
||||
@NotNull(message = "情绪强度不能为空")
|
||||
private BigDecimal intensity;
|
||||
|
||||
/**
|
||||
* 触发因素
|
||||
*/
|
||||
@Size(max = 768, message = "触发因素长度不能超过768个字符")
|
||||
private String triggers;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@Size(max = 768, message = "描述长度不能超过768个字符")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
private List<String> tags;
|
||||
|
||||
/**
|
||||
* 天气
|
||||
*/
|
||||
@Size(max = 64, message = "天气长度不能超过64个字符")
|
||||
private String weather;
|
||||
|
||||
/**
|
||||
* 地点
|
||||
*/
|
||||
@Size(max = 128, message = "地点长度不能超过128个字符")
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 活动
|
||||
*/
|
||||
@Size(max = 128, message = "活动长度不能超过128个字符")
|
||||
private String activity;
|
||||
|
||||
/**
|
||||
* 相关人物
|
||||
*/
|
||||
@Size(max = 256, message = "相关人物长度不能超过256个字符")
|
||||
private String people;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
@Size(max = 1024, message = "备注长度不能超过1024个字符")
|
||||
private String notes;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import com.emotion.common.BasePageRequest;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
|
||||
import javax.validation.constraints.Size;
|
||||
|
||||
/**
|
||||
* 情绪记录分页请求类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@Data
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class EmotionRecordPageRequest extends BasePageRequest {
|
||||
|
||||
/**
|
||||
* 用户ID(可选)
|
||||
*/
|
||||
@Size(max = 32, message = "用户ID长度不能超过32个字符")
|
||||
private String userId;
|
||||
|
||||
/**
|
||||
* 情绪类型(可选)
|
||||
*/
|
||||
@Size(max = 32, message = "情绪类型长度不能超过32个字符")
|
||||
private String emotionType;
|
||||
|
||||
/**
|
||||
* 地点(可选)
|
||||
*/
|
||||
@Size(max = 128, message = "地点长度不能超过128个字符")
|
||||
private String location;
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
import javax.validation.constraints.Size;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 情绪记录更新请求类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@Data
|
||||
public class EmotionRecordUpdateRequest {
|
||||
|
||||
/**
|
||||
* 情绪记录ID
|
||||
*/
|
||||
@NotBlank(message = "情绪记录ID不能为空")
|
||||
@Size(max = 32, message = "情绪记录ID长度不能超过32个字符")
|
||||
private String id;
|
||||
|
||||
/**
|
||||
* 记录日期
|
||||
*/
|
||||
private LocalDate recordDate;
|
||||
|
||||
/**
|
||||
* 情绪类型
|
||||
*/
|
||||
@Size(max = 32, message = "情绪类型长度不能超过32个字符")
|
||||
private String emotionType;
|
||||
|
||||
/**
|
||||
* 情绪强度
|
||||
*/
|
||||
private BigDecimal intensity;
|
||||
|
||||
/**
|
||||
* 触发因素
|
||||
*/
|
||||
@Size(max = 768, message = "触发因素长度不能超过768个字符")
|
||||
private String triggers;
|
||||
|
||||
/**
|
||||
* 描述
|
||||
*/
|
||||
@Size(max = 768, message = "描述长度不能超过768个字符")
|
||||
private String description;
|
||||
|
||||
/**
|
||||
* 标签
|
||||
*/
|
||||
private List<String> tags;
|
||||
|
||||
/**
|
||||
* 天气
|
||||
*/
|
||||
@Size(max = 64, message = "天气长度不能超过64个字符")
|
||||
private String weather;
|
||||
|
||||
/**
|
||||
* 地点
|
||||
*/
|
||||
@Size(max = 128, message = "地点长度不能超过128个字符")
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 活动
|
||||
*/
|
||||
@Size(max = 128, message = "活动长度不能超过128个字符")
|
||||
private String activity;
|
||||
|
||||
/**
|
||||
* 相关人物
|
||||
*/
|
||||
@Size(max = 256, message = "相关人物长度不能超过256个字符")
|
||||
private String people;
|
||||
|
||||
/**
|
||||
* 备注
|
||||
*/
|
||||
@Size(max = 1024, message = "备注长度不能超过1024个字符")
|
||||
private String notes;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package com.emotion.dto.request;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 情绪总结生成请求类
|
||||
*
|
||||
* @author huazhongmin
|
||||
* @date 2025-09-08
|
||||
*/
|
||||
@Data
|
||||
public class EmotionSummaryGenerateRequest {
|
||||
// 目前情绪总结生成不需要额外参数
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user