refactor: 重命名 backend-single 目录为 server

This commit is contained in:
2026-06-27 17:23:53 +08:00
parent 8daf51301a
commit 0d77d76858
464 changed files with 0 additions and 0 deletions
+125
View File
@@ -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. 最优配置查询会优先返回默认配置,然后按优先级排序
+234
View File
@@ -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
+17
View File
@@ -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.
+114
View File
@@ -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
+8
View File
@@ -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
+34
View File
@@ -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;
+201
View File
@@ -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
+248
View File
@@ -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()
+180
View File
@@ -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
+223
View File
@@ -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白名单限制
- 登录失败次数限制
+262
View File
@@ -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服务正常运行
+239
View File
@@ -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. **文档完善**: 提供详细的设计文档和测试指南
系统已经可以投入使用,后续可根据实际需求进行功能扩展和优化。
+139
View File
@@ -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
View File
@@ -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>
+34
View File
@@ -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("<", "&lt;").replace(">", "&gt;");
}
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("<", "&lt;").replace(">", "&gt;"));
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