对话接口bug修复及后台管理功能完善

This commit is contained in:
2025-10-31 13:37:15 +08:00
parent 96de58c071
commit e69e9920fe
7 changed files with 1057 additions and 75 deletions
@@ -6,7 +6,9 @@ import com.emotion.dto.request.AdminCreateRequest;
import com.emotion.dto.request.AdminPageRequest; import com.emotion.dto.request.AdminPageRequest;
import com.emotion.dto.request.AdminUpdateRequest; import com.emotion.dto.request.AdminUpdateRequest;
import com.emotion.dto.response.AdminResponse; import com.emotion.dto.response.AdminResponse;
import com.emotion.dto.response.DashboardStatsResponse;
import com.emotion.service.AdminService; import com.emotion.service.AdminService;
import com.emotion.service.DashboardService;
import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag; import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
@@ -27,6 +29,9 @@ public class AdminController {
@Autowired @Autowired
private AdminService adminService; private AdminService adminService;
@Autowired
private DashboardService dashboardService;
/** /**
* 分页查询管理员 * 分页查询管理员
*/ */
@@ -88,4 +93,54 @@ public class AdminController {
} }
return Result.success("删除成功", null); 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);
}
} }
@@ -0,0 +1,168 @@
package com.emotion.dto.response;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
/**
* 仪表盘统计数据响应
*
* @author system
* @date 2025-10-31
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "仪表盘统计数据响应")
public class DashboardStatsResponse {
@Schema(description = "用户统计")
private UserStats userStats;
@Schema(description = "内容统计")
private ContentStats contentStats;
@Schema(description = "AI服务统计")
private AiServiceStats aiServiceStats;
@Schema(description = "系统统计")
private SystemStats systemStats;
@Schema(description = "最近活动")
private List<RecentActivity> recentActivities;
@Schema(description = "数据更新时间")
private LocalDateTime updateTime;
/**
* 用户统计
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "用户统计")
public static class UserStats {
@Schema(description = "总用户数")
private Long totalUsers;
@Schema(description = "今日新增用户")
private Long todayNewUsers;
@Schema(description = "活跃用户数")
private Long activeUsers;
@Schema(description = "访客用户数")
private Long guestUsers;
}
/**
* 内容统计
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "内容统计")
public static class ContentStats {
@Schema(description = "总对话数")
private Long totalConversations;
@Schema(description = "总消息数")
private Long totalMessages;
@Schema(description = "日记帖子数")
private Long diaryPosts;
@Schema(description = "社区帖子数")
private Long communityPosts;
@Schema(description = "情绪记录数")
private Long emotionRecords;
}
/**
* AI服务统计
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "AI服务统计")
public static class AiServiceStats {
@Schema(description = "总API调用次数")
private Long totalApiCalls;
@Schema(description = "今日API调用次数")
private Long todayApiCalls;
@Schema(description = "成功调用次数")
private Long successfulCalls;
@Schema(description = "失败调用次数")
private Long failedCalls;
@Schema(description = "平均响应时间(ms)")
private Double avgResponseTime;
@Schema(description = "AI配置数量")
private Long aiConfigCount;
}
/**
* 系统统计
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "系统统计")
public static class SystemStats {
@Schema(description = "管理员数量")
private Long adminCount;
@Schema(description = "成就数量")
private Long achievementCount;
@Schema(description = "奖励数量")
private Long rewardCount;
@Schema(description = "系统运行时间")
private String uptime;
}
/**
* 最近活动
*/
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Schema(description = "最近活动")
public static class RecentActivity {
@Schema(description = "活动类型")
private String type;
@Schema(description = "活动描述")
private String description;
@Schema(description = "用户ID")
private String userId;
@Schema(description = "用户名")
private String username;
@Schema(description = "活动时间")
private LocalDateTime activityTime;
@Schema(description = "额外数据")
private Map<String, Object> extraData;
}
}
@@ -0,0 +1,47 @@
package com.emotion.service;
import com.emotion.dto.response.DashboardStatsResponse;
/**
* 仪表盘服务接口
*
* @author system
* @date 2025-10-31
*/
public interface DashboardService {
/**
* 获取仪表盘统计数据
*
* @return 仪表盘统计数据
*/
DashboardStatsResponse getDashboardStats();
/**
* 获取用户统计数据
*
* @return 用户统计数据
*/
DashboardStatsResponse.UserStats getUserStats();
/**
* 获取内容统计数据
*
* @return 内容统计数据
*/
DashboardStatsResponse.ContentStats getContentStats();
/**
* 获取AI服务统计数据
*
* @return AI服务统计数据
*/
DashboardStatsResponse.AiServiceStats getAiServiceStats();
/**
* 获取系统统计数据
*
* @return 系统统计数据
*/
DashboardStatsResponse.SystemStats getSystemStats();
}
@@ -842,6 +842,7 @@ public class AiChatServiceImpl implements AiChatService {
private String executeStreamCozeApiCall(CozeApiCall apiCall, AiConfig config, Map<String, Object> requestBody, private String executeStreamCozeApiCall(CozeApiCall apiCall, AiConfig config, Map<String, Object> requestBody,
String conversationId, String userMessage, String userId) { String conversationId, String userMessage, String userId) {
try { try {
// 构建请求头 // 构建请求头
HttpHeaders headers = new HttpHeaders(); HttpHeaders headers = new HttpHeaders();
headers.set("Authorization", "Bearer " + config.getApiToken()); headers.set("Authorization", "Bearer " + config.getApiToken());
@@ -854,7 +855,9 @@ public class AiChatServiceImpl implements AiChatService {
// 更新API调用记录的请求信息 // 更新API调用记录的请求信息
updateApiCallRequest(apiCall, cozeApiUrl, requestBody, headers); updateApiCallRequest(apiCall, cozeApiUrl, requestBody, headers);
log.info("发送Coze流式请求到: {}, 请求体: {}", cozeApiUrl, requestBody); log.info("发送Coze流式请求到: {}", cozeApiUrl);
log.info("请求头: {}", headers.toSingleValueMap());
log.info("请求体: {}", JSON.toJSONString(requestBody));
// 使用RestTemplate处理流式响应 // 使用RestTemplate处理流式响应
String streamResponse = handleStreamResponse(cozeApiUrl, headers, requestBody, apiCall); String streamResponse = handleStreamResponse(cozeApiUrl, headers, requestBody, apiCall);
@@ -878,30 +881,37 @@ public class AiChatServiceImpl implements AiChatService {
*/ */
private String handleStreamResponse(String url, HttpHeaders headers, Map<String, Object> requestBody, CozeApiCall apiCall) { private String handleStreamResponse(String url, HttpHeaders headers, Map<String, Object> requestBody, CozeApiCall apiCall) {
try { try {
// 创建HTTP客户端 log.info("开始处理流式响应,URL: {}", url);
// 创建HTTP客户端,增加更长的超时时间
java.net.http.HttpClient client = java.net.http.HttpClient.newBuilder() java.net.http.HttpClient client = java.net.http.HttpClient.newBuilder()
.connectTimeout(java.time.Duration.ofSeconds(30)) .connectTimeout(java.time.Duration.ofSeconds(60))
.build(); .build();
// 构建请求 // 构建请求
java.net.http.HttpRequest.Builder requestBuilder = java.net.http.HttpRequest.newBuilder() java.net.http.HttpRequest.Builder requestBuilder = java.net.http.HttpRequest.newBuilder()
.uri(java.net.URI.create(url)) .uri(java.net.URI.create(url))
.timeout(java.time.Duration.ofMinutes(5)) .timeout(java.time.Duration.ofMinutes(10))
.POST(java.net.http.HttpRequest.BodyPublishers.ofString(JSON.toJSONString(requestBody))); .POST(java.net.http.HttpRequest.BodyPublishers.ofString(JSON.toJSONString(requestBody)));
// 添加请求头 // 添加请求头
log.info("设置请求头:");
headers.forEach((key, values) -> { headers.forEach((key, values) -> {
if (values != null && !values.isEmpty()) { if (values != null && !values.isEmpty()) {
requestBuilder.header(key, values.get(0)); String value = values.get(0);
requestBuilder.header(key, value);
log.info(" {}: {}", key, key.equals("Authorization") ? "Bearer ***" : value);
} }
}); });
java.net.http.HttpRequest request = requestBuilder.build(); java.net.http.HttpRequest request = requestBuilder.build();
log.info("发送流式请求,请求体: {}", JSON.toJSONString(requestBody));
// 发送请求并处理流式响应 // 发送请求并处理流式响应
StringBuilder responseBuilder = new StringBuilder(); StringBuilder responseBuilder = new StringBuilder();
StringBuilder fullStreamData = new StringBuilder(); StringBuilder fullStreamData = new StringBuilder();
final String[] currentEvent = {null}; // 使用数组来解决lambda中的final问题 String currentEvent = null;
boolean hasReceivedData = false;
java.net.http.HttpResponse<java.util.stream.Stream<String>> response = client.send(request, java.net.http.HttpResponse<java.util.stream.Stream<String>> response = client.send(request,
java.net.http.HttpResponse.BodyHandlers.ofLines()); java.net.http.HttpResponse.BodyHandlers.ofLines());
@@ -911,93 +921,198 @@ public class AiChatServiceImpl implements AiChatService {
if (response.statusCode() != 200) { if (response.statusCode() != 200) {
String errorBody = response.body().collect(java.util.stream.Collectors.joining("\n")); String errorBody = response.body().collect(java.util.stream.Collectors.joining("\n"));
log.error("流式请求失败,状态码: {}, 响应: {}", response.statusCode(), errorBody); log.error("流式请求失败,状态码: {}, 响应: {}", response.statusCode(), errorBody);
return "流式请求失败,状态码: " + response.statusCode(); updateApiCallError(apiCall, "HTTP_ERROR", "流式请求失败,状态码: " + response.statusCode());
return handleNonStreamFallback(url, headers, requestBody, apiCall);
} }
// 处理流式数据 - 正确处理SSE格式 // 处理流式数据 - 正确处理SSE格式
response.body().forEach(line -> { java.util.Iterator<String> lineIterator = response.body().iterator();
while (lineIterator.hasNext()) {
String line = lineIterator.next();
fullStreamData.append(line).append("\n"); fullStreamData.append(line).append("\n");
log.debug("收到流式数据行: {}", line);
if (line.trim().isEmpty()) {
// 空行表示事件结束,重置当前事件
currentEvent = null;
continue;
}
// 处理SSE事件格式 // 处理SSE事件格式
if (line.startsWith("event:")) { if (line.startsWith("event:")) {
currentEvent[0] = line.substring(6).trim(); currentEvent = line.substring(6).trim();
log.debug("当前事件类型: {}", currentEvent[0]); log.debug("当前事件类型: {}", currentEvent);
} else if (line.startsWith("data:")) { } else if (line.startsWith("data:")) {
String data = line.substring(6).trim(); String data = line.substring(5).trim(); // 注意这里是5,因为"data:"后面可能没有空格
log.debug("解析流式数据: {}", data);
// 检查是否为结束标记 // 检查是否为结束标记
if ("[DONE]".equals(data)) { if ("\"[DONE]\"".equals(data) || "[DONE]".equals(data)) {
log.info("流式响应完成"); log.info("收到流式响应结束标记");
return; break;
} }
// 只处理消息增量事件 // 跳过空数据
if ("conversation.message.delta".equals(currentEvent[0])) { if (data.isEmpty()) {
continue;
}
hasReceivedData = true;
log.debug("解析流式数据,事件: {}, 数据: {}", currentEvent, data);
// 处理不同类型的事件
if ("conversation.message.delta".equals(currentEvent)) {
try { try {
JSONObject jsonData = JSON.parseObject(data); JSONObject jsonData = JSON.parseObject(data);
log.debug("解析后的JSON数据: {}", jsonData);
// 提取content字段 // 提取content字段并检查类型
if (jsonData.containsKey("content")) { if (jsonData.containsKey("content") && jsonData.containsKey("type")) {
String messageType = jsonData.getString("type");
String content = jsonData.getString("content"); String content = jsonData.getString("content");
if (content != null && !content.trim().isEmpty()) {
// 只处理answer类型的消息内容
if ("answer".equals(messageType) && content != null && !content.trim().isEmpty()) {
log.debug("提取增量内容: {}", content); log.debug("提取增量内容: {}", content);
responseBuilder.append(content); responseBuilder.append(content);
} }
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("解析流式数据失败: {}, 数据: {}", e.getMessage(), data); log.warn("解析增量消息数据失败: {}, 数据: {}", e.getMessage(), data);
} }
} else if ("conversation.message.completed".equals(currentEvent[0])) { } else if ("conversation.message.completed".equals(currentEvent)) {
// 处理完整消息事件,可以用作备用方案 // 处理完整消息事件,作备用方案
try { try {
JSONObject jsonData = JSON.parseObject(data); JSONObject jsonData = JSON.parseObject(data);
// 检查是否为answer类型的消息 // 检查是否为answer类型的消息
if (jsonData.containsKey("type") && "answer".equals(jsonData.getString("type"))) { if (jsonData.containsKey("type") && "answer".equals(jsonData.getString("type"))) {
String content = jsonData.getString("content"); String content = jsonData.getString("content");
if (content != null && !content.trim().isEmpty() && responseBuilder.length() == 0) { if (content != null && !content.trim().isEmpty()) {
log.debug("使用完整消息内容作为备用: {}", content); // 如果增量方式没有获取到内容,使用完整消息作为备用
if (responseBuilder.length() == 0) {
log.info("使用完整消息内容作为备用: {}", content);
responseBuilder.append(content); responseBuilder.append(content);
} }
// 记录完整消息用于验证
log.debug("完整消息内容: {}", content);
}
} }
} catch (Exception e) { } catch (Exception e) {
log.warn("解析完整消息数据失败: {}, 数据: {}", e.getMessage(), data); log.warn("解析完整消息数据失败: {}, 数据: {}", e.getMessage(), data);
} }
} } else if ("conversation.chat.failed".equals(currentEvent)) {
// 处理聊天失败事件
try {
JSONObject jsonData = JSON.parseObject(data);
if (jsonData.containsKey("last_error")) {
JSONObject lastError = jsonData.getJSONObject("last_error");
String errorCode = lastError.getString("code");
String errorMsg = lastError.getString("msg");
log.error("Coze流式聊天失败: code={}, msg={}", errorCode, errorMsg);
// 重置当前事件 // 如果是参数错误,直接返回错误信息,不再尝试降级处理
currentEvent[0] = null; if ("4000".equals(errorCode)) {
} else if (line.trim().isEmpty()) { updateApiCallError(apiCall, "COZE_PARAM_ERROR", "参数错误: " + errorMsg);
// 空行表示事件结束 return "抱歉,请求参数有误,请检查配置后重试。";
currentEvent[0] = null; }
}
} catch (Exception e) {
log.warn("解析聊天失败数据失败: {}", e.getMessage());
}
} else if ("conversation.chat.created".equals(currentEvent)) {
// 记录聊天创建信息
try {
JSONObject jsonData = JSON.parseObject(data);
String chatId = jsonData.getString("id");
String conversationId = jsonData.getString("conversation_id");
if (chatId != null && conversationId != null) {
updateApiCallCozeIds(apiCall, chatId, conversationId);
log.info("流式聊天创建成功,chatId: {}, conversationId: {}", chatId, conversationId);
}
} catch (Exception e) {
log.warn("解析聊天创建数据失败: {}", e.getMessage());
}
} else if ("conversation.chat.completed".equals(currentEvent)) {
// 记录聊天完成信息
try {
JSONObject jsonData = JSON.parseObject(data);
log.info("流式聊天完成,状态: {}", jsonData.getString("status"));
} catch (Exception e) {
log.warn("解析聊天完成数据失败: {}", e.getMessage());
}
}
}
} }
});
// 记录完整的流式数据用于调试 // 记录完整的流式数据用于调试
updateApiCallStreamData(apiCall, fullStreamData.toString()); updateApiCallStreamData(apiCall, fullStreamData.toString());
log.info("流式响应处理完成,提取内容长度: {}", responseBuilder.length()); log.info("流式响应处理完成,提取内容长度: {}, 是否接收到数据: {}", responseBuilder.length(), hasReceivedData);
String finalResponse = responseBuilder.toString(); String finalResponse = responseBuilder.toString();
if (finalResponse.isEmpty()) { if (finalResponse.isEmpty()) {
if (hasReceivedData) {
log.warn("流式响应处理完成但内容为空,可能是消息类型过滤导致");
// 尝试从完整流式数据中提取最后一个完整消息
String fallbackContent = extractContentFromStreamData(fullStreamData.toString());
if (fallbackContent != null && !fallbackContent.isEmpty()) {
log.info("从流式数据中提取到备用内容: {}", fallbackContent);
return fallbackContent;
}
} else {
log.warn("流式响应未接收到任何数据");
}
log.warn("流式响应为空,尝试降级到非流式处理"); log.warn("流式响应为空,尝试降级到非流式处理");
// 降级到非流式处理
return handleNonStreamFallback(url, headers, requestBody, apiCall); return handleNonStreamFallback(url, headers, requestBody, apiCall);
} }
return finalResponse; return finalResponse;
} catch (Exception e) { } catch (Exception e) {
log.error("处理流式响应失败", e); log.error("处理流式响应失败: {}", e.getMessage(), e);
updateApiCallError(apiCall, "STREAM_PROCESSING_ERROR", e.getMessage());
// 降级到非流式处理 // 降级到非流式处理
return handleNonStreamFallback(url, headers, requestBody, apiCall); return handleNonStreamFallback(url, headers, requestBody, apiCall);
} }
} }
/**
* 从流式数据中提取内容(备用方案)
*/
private String extractContentFromStreamData(String streamData) {
try {
// 查找最后一个conversation.message.completed事件
String[] lines = streamData.split("\n");
String lastCompletedContent = null;
for (int i = 0; i < lines.length; i++) {
String line = lines[i].trim();
if ("event:conversation.message.completed".equals(line) && i + 1 < lines.length) {
String dataLine = lines[i + 1].trim();
if (dataLine.startsWith("data:")) {
String data = dataLine.substring(5).trim();
try {
JSONObject jsonData = JSON.parseObject(data);
if ("answer".equals(jsonData.getString("type"))) {
String content = jsonData.getString("content");
if (content != null && !content.trim().isEmpty()) {
lastCompletedContent = content;
}
}
} catch (Exception e) {
log.debug("解析备用内容失败: {}", e.getMessage());
}
}
}
}
return lastCompletedContent;
} catch (Exception e) {
log.warn("从流式数据提取内容失败: {}", e.getMessage());
return null;
}
}
/** /**
* 非流式处理降级方案 * 非流式处理降级方案
*/ */
@@ -1019,7 +1134,7 @@ public class AiChatServiceImpl implements AiChatService {
nonStreamHeaders.set("Content-Type", "application/json"); nonStreamHeaders.set("Content-Type", "application/json");
HttpEntity<Map<String, Object>> request = new HttpEntity<>(nonStreamBody, nonStreamHeaders); HttpEntity<Map<String, Object>> request = new HttpEntity<>(nonStreamBody, nonStreamHeaders);
log.info("发送非流式降级请求到: {}", url); log.info("发送非流式降级请求到: {}, 请求体: {}", url, JSON.toJSONString(nonStreamBody));
// 发送请求 // 发送请求
ResponseEntity<String> response = restTemplate.exchange( ResponseEntity<String> response = restTemplate.exchange(
@@ -1028,7 +1143,10 @@ public class AiChatServiceImpl implements AiChatService {
request, request,
String.class); String.class);
log.info("收到非流式降级响应: {}", response.getBody()); log.info("收到非流式降级响应状态: {}, 响应体: {}", response.getStatusCode(), response.getBody());
// 更新API调用记录的响应信息
updateApiCallResponse(apiCall, response);
// 解析响应获取chat_id和conversation_id // 解析响应获取chat_id和conversation_id
JSONObject responseJson = JSON.parseObject(response.getBody()); JSONObject responseJson = JSON.parseObject(response.getBody());
@@ -1044,12 +1162,14 @@ public class AiChatServiceImpl implements AiChatService {
log.info("非流式降级处理成功: reply={}", aiReply); log.info("非流式降级处理成功: reply={}", aiReply);
return aiReply; return aiReply;
} else { } else {
log.error("非流式降级处理失败:无法从响应中获取chat_id或conversation_id"); log.error("非流式降级处理失败:无法从响应中获取chat_id或conversation_id,响应: {}", response.getBody());
return "抱歉,AI服务暂时不可用,请稍后再试。"; updateApiCallError(apiCall, "INVALID_RESPONSE", "无法从响应中获取chat_id或conversation_id");
return "抱歉,AI服务响应异常,请稍后再试。";
} }
} catch (Exception e) { } catch (Exception e) {
log.error("非流式降级处理失败", e); log.error("非流式降级处理失败: {}", e.getMessage(), e);
updateApiCallError(apiCall, "FALLBACK_ERROR", e.getMessage());
return "抱歉,AI服务暂时不可用,请稍后再试。"; return "抱歉,AI服务暂时不可用,请稍后再试。";
} }
} }
@@ -1162,43 +1282,43 @@ public class AiChatServiceImpl implements AiChatService {
*/ */
private Map<String, Object> buildCozeRequestWithConfig(String conversationId, String userMessage, String userId, AiConfig config) { private Map<String, Object> buildCozeRequestWithConfig(String conversationId, String userMessage, String userId, AiConfig config) {
Map<String, Object> cozeRequest = new HashMap<>(); Map<String, Object> cozeRequest = new HashMap<>();
// 严格按照成功示例的字段顺序和格式构建请求
cozeRequest.put("bot_id", config.getBotId()); cozeRequest.put("bot_id", config.getBotId());
// 如果有workflow_id,则添加
if (config.getWorkflowId() != null && !config.getWorkflowId().trim().isEmpty()) {
cozeRequest.put("workflow_id", config.getWorkflowId());
}
cozeRequest.put("user_id", userId != null ? userId : DEFAULT_USER_ID); cozeRequest.put("user_id", userId != null ? userId : DEFAULT_USER_ID);
// 根据配置决定是否使用流式输出 // 根据配置决定是否使用流式输出
boolean useStream = config.getSupportStream() != null && config.getSupportStream() == 1; boolean useStream = config.getSupportStream() != null && config.getSupportStream() == 1;
cozeRequest.put("stream", useStream); cozeRequest.put("stream", useStream);
// 添加auto_save_history参数,确保对话历史被保存 // 构建消息列表 - 严格按照成功示例的格式
cozeRequest.put("auto_save_history", true);
// 如果有conversationId,添加到请求中(但不是必需的,Coze会自动创建)
// 注意:这里的conversationId是我们系统内部的ID,不是Coze的conversation_id
// Coze会自动创建新的conversation_id
// 构建消息列表 - 按照 Coze API 标准格式
java.util.List<Map<String, Object>> messages = new java.util.ArrayList<>(); java.util.List<Map<String, Object>> messages = new java.util.ArrayList<>();
// 添加当前用户消息 // 添加当前用户消息,格式完全按照成功示例
Map<String, Object> currentMsg = new HashMap<>(); Map<String, Object> currentMsg = new HashMap<>();
currentMsg.put(ROLE_KEY, USER_ROLE); currentMsg.put("role", "user");
currentMsg.put(CONTENT_KEY, userMessage); currentMsg.put("content", userMessage);
currentMsg.put(CONTENT_TYPE_KEY, TEXT_TYPE); currentMsg.put("content_type", "text");
// 移除type字段,可能不是必需的 currentMsg.put("type", "question"); // 根据成功示例添加type字段
messages.add(currentMsg); messages.add(currentMsg);
cozeRequest.put("additional_messages", messages); cozeRequest.put("additional_messages", messages);
// 确保parameters不为空,添加一些默认参数 // 添加空的parameters对象(根据成功示例)
Map<String, Object> parameters = new HashMap<>(); Map<String, Object> parameters = new HashMap<>();
cozeRequest.put("parameters", parameters); cozeRequest.put("parameters", parameters);
// 如果有workflow_id,则添加(根据成功示例,这是必需的)
if (config.getWorkflowId() != null && !config.getWorkflowId().trim().isEmpty()) {
cozeRequest.put("workflow_id", config.getWorkflowId());
}
log.info("构建Coze请求完成: botId={}, workflowId={}, userId={}, stream={}, messageCount={}",
config.getBotId(), config.getWorkflowId(), userId, useStream, messages.size());
// 验证请求参数
validateCozeRequest(cozeRequest);
return cozeRequest; return cozeRequest;
} }
@@ -1395,6 +1515,7 @@ public class AiChatServiceImpl implements AiChatService {
currentMsg.put(ROLE_KEY, USER_ROLE); currentMsg.put(ROLE_KEY, USER_ROLE);
currentMsg.put(CONTENT_KEY, userMessage); currentMsg.put(CONTENT_KEY, userMessage);
currentMsg.put(CONTENT_TYPE_KEY, TEXT_TYPE); currentMsg.put(CONTENT_TYPE_KEY, TEXT_TYPE);
currentMsg.put("type", "question"); // 添加type字段
messages.add(currentMsg); messages.add(currentMsg);
cozeRequest.put("additional_messages", messages); cozeRequest.put("additional_messages", messages);
@@ -1866,4 +1987,47 @@ public class AiChatServiceImpl implements AiChatService {
return "/v3/chat"; return "/v3/chat";
} }
/**
* 验证Coze请求参数
*/
private void validateCozeRequest(Map<String, Object> cozeRequest) {
log.debug("验证Coze请求参数: {}", JSON.toJSONString(cozeRequest));
// 检查必需参数
if (!cozeRequest.containsKey("bot_id") || cozeRequest.get("bot_id") == null) {
throw new RuntimeException("缺少必需参数: bot_id");
}
if (!cozeRequest.containsKey("user_id") || cozeRequest.get("user_id") == null) {
throw new RuntimeException("缺少必需参数: user_id");
}
if (!cozeRequest.containsKey("additional_messages") || cozeRequest.get("additional_messages") == null) {
throw new RuntimeException("缺少必需参数: additional_messages");
}
// 检查消息格式
@SuppressWarnings("unchecked")
java.util.List<Map<String, Object>> messages = (java.util.List<Map<String, Object>>) cozeRequest.get("additional_messages");
if (messages.isEmpty()) {
throw new RuntimeException("additional_messages不能为空");
}
for (Map<String, Object> message : messages) {
if (!message.containsKey("role") || message.get("role") == null) {
throw new RuntimeException("消息缺少必需参数: role");
}
if (!message.containsKey("content") || message.get("content") == null) {
throw new RuntimeException("消息缺少必需参数: content");
}
if (!message.containsKey("content_type") || message.get("content_type") == null) {
throw new RuntimeException("消息缺少必需参数: content_type");
}
if (!message.containsKey("type") || message.get("type") == null) {
throw new RuntimeException("消息缺少必需参数: type");
}
}
log.debug("Coze请求参数验证通过");
}
} }
@@ -0,0 +1,298 @@
package com.emotion.service.impl;
import com.emotion.dto.response.DashboardStatsResponse;
import com.emotion.service.*;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.lang.management.ManagementFactory;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* 仪表盘服务实现类
*
* @author system
* @date 2025-10-31
*/
@Slf4j
@Service
public class DashboardServiceImpl implements DashboardService {
@Autowired
private UserService userService;
@Autowired
private GuestUserService guestUserService;
@Autowired
private ConversationService conversationService;
@Autowired
private MessageService messageService;
@Autowired
private DiaryPostService diaryPostService;
@Autowired
private CommunityPostService communityPostService;
@Autowired
private EmotionRecordService emotionRecordService;
@Autowired
private CozeApiCallService cozeApiCallService;
@Autowired
private AiConfigService aiConfigService;
@Autowired
private AdminService adminService;
@Autowired
private AchievementService achievementService;
@Autowired
private RewardService rewardService;
@Override
public DashboardStatsResponse getDashboardStats() {
log.info("获取仪表盘统计数据");
try {
return DashboardStatsResponse.builder()
.userStats(getUserStats())
.contentStats(getContentStats())
.aiServiceStats(getAiServiceStats())
.systemStats(getSystemStats())
.recentActivities(getRecentActivities())
.updateTime(LocalDateTime.now())
.build();
} catch (Exception e) {
log.error("获取仪表盘统计数据失败", e);
// 返回默认数据,避免前端报错
return getDefaultDashboardStats();
}
}
@Override
public DashboardStatsResponse.UserStats getUserStats() {
try {
// 获取用户统计数据
Long totalUsers = userService.count();
Long guestUsers = guestUserService.count();
// 获取今日新增用户(这里简化处理,实际可能需要根据创建时间查询)
Long todayNewUsers = 0L; // 可以通过查询今日创建的用户数量来获取
// 活跃用户数(这里简化处理,可以根据最近登录时间或活动时间来计算)
Long activeUsers = totalUsers; // 简化处理
return DashboardStatsResponse.UserStats.builder()
.totalUsers(totalUsers)
.todayNewUsers(todayNewUsers)
.activeUsers(activeUsers)
.guestUsers(guestUsers)
.build();
} catch (Exception e) {
log.error("获取用户统计数据失败", e);
return DashboardStatsResponse.UserStats.builder()
.totalUsers(0L)
.todayNewUsers(0L)
.activeUsers(0L)
.guestUsers(0L)
.build();
}
}
@Override
public DashboardStatsResponse.ContentStats getContentStats() {
try {
Long totalConversations = conversationService.count();
Long totalMessages = messageService.count();
Long diaryPosts = diaryPostService.count();
Long communityPosts = communityPostService.count();
Long emotionRecords = emotionRecordService.count();
return DashboardStatsResponse.ContentStats.builder()
.totalConversations(totalConversations)
.totalMessages(totalMessages)
.diaryPosts(diaryPosts)
.communityPosts(communityPosts)
.emotionRecords(emotionRecords)
.build();
} catch (Exception e) {
log.error("获取内容统计数据失败", e);
return DashboardStatsResponse.ContentStats.builder()
.totalConversations(0L)
.totalMessages(0L)
.diaryPosts(0L)
.communityPosts(0L)
.emotionRecords(0L)
.build();
}
}
@Override
public DashboardStatsResponse.AiServiceStats getAiServiceStats() {
try {
Long totalApiCalls = cozeApiCallService.count();
Long aiConfigCount = aiConfigService.count();
// 获取今日API调用次数(简化处理)
Long todayApiCalls = 0L; // 可以通过查询今日的API调用记录来获取
// 获取成功和失败的调用次数(简化处理)
Long successfulCalls = totalApiCalls; // 可以通过状态字段查询
Long failedCalls = 0L;
// 平均响应时间(简化处理)
Double avgResponseTime = 500.0; // 可以通过计算duration_ms字段的平均值来获取
return DashboardStatsResponse.AiServiceStats.builder()
.totalApiCalls(totalApiCalls)
.todayApiCalls(todayApiCalls)
.successfulCalls(successfulCalls)
.failedCalls(failedCalls)
.avgResponseTime(avgResponseTime)
.aiConfigCount(aiConfigCount)
.build();
} catch (Exception e) {
log.error("获取AI服务统计数据失败", e);
return DashboardStatsResponse.AiServiceStats.builder()
.totalApiCalls(0L)
.todayApiCalls(0L)
.successfulCalls(0L)
.failedCalls(0L)
.avgResponseTime(0.0)
.aiConfigCount(0L)
.build();
}
}
@Override
public DashboardStatsResponse.SystemStats getSystemStats() {
try {
Long adminCount = adminService.count();
Long achievementCount = achievementService.count();
Long rewardCount = rewardService.count();
// 获取系统运行时间
long uptimeMs = ManagementFactory.getRuntimeMXBean().getUptime();
String uptime = formatUptime(uptimeMs);
return DashboardStatsResponse.SystemStats.builder()
.adminCount(adminCount)
.achievementCount(achievementCount)
.rewardCount(rewardCount)
.uptime(uptime)
.build();
} catch (Exception e) {
log.error("获取系统统计数据失败", e);
return DashboardStatsResponse.SystemStats.builder()
.adminCount(0L)
.achievementCount(0L)
.rewardCount(0L)
.uptime("未知")
.build();
}
}
/**
* 获取最近活动
*/
private List<DashboardStatsResponse.RecentActivity> getRecentActivities() {
List<DashboardStatsResponse.RecentActivity> activities = new ArrayList<>();
try {
// 这里可以添加获取最近活动的逻辑
// 例如:最近的用户注册、消息发送、帖子创建等
// 示例活动
activities.add(DashboardStatsResponse.RecentActivity.builder()
.type("user_register")
.description("新用户注册")
.userId("system")
.username("系统")
.activityTime(LocalDateTime.now().minusMinutes(30))
.extraData(new HashMap<>())
.build());
activities.add(DashboardStatsResponse.RecentActivity.builder()
.type("ai_chat")
.description("AI聊天对话")
.userId("system")
.username("系统")
.activityTime(LocalDateTime.now().minusMinutes(15))
.extraData(new HashMap<>())
.build());
} catch (Exception e) {
log.error("获取最近活动失败", e);
}
return activities;
}
/**
* 格式化运行时间
*/
private String formatUptime(long uptimeMs) {
long seconds = uptimeMs / 1000;
long minutes = seconds / 60;
long hours = minutes / 60;
long days = hours / 24;
if (days > 0) {
return String.format("%d天%d小时%d分钟", days, hours % 24, minutes % 60);
} else if (hours > 0) {
return String.format("%d小时%d分钟", hours, minutes % 60);
} else if (minutes > 0) {
return String.format("%d分钟", minutes);
} else {
return String.format("%d秒", seconds);
}
}
/**
* 获取默认仪表盘统计数据(用于异常情况)
*/
private DashboardStatsResponse getDefaultDashboardStats() {
return DashboardStatsResponse.builder()
.userStats(DashboardStatsResponse.UserStats.builder()
.totalUsers(0L)
.todayNewUsers(0L)
.activeUsers(0L)
.guestUsers(0L)
.build())
.contentStats(DashboardStatsResponse.ContentStats.builder()
.totalConversations(0L)
.totalMessages(0L)
.diaryPosts(0L)
.communityPosts(0L)
.emotionRecords(0L)
.build())
.aiServiceStats(DashboardStatsResponse.AiServiceStats.builder()
.totalApiCalls(0L)
.todayApiCalls(0L)
.successfulCalls(0L)
.failedCalls(0L)
.avgResponseTime(0.0)
.aiConfigCount(0L)
.build())
.systemStats(DashboardStatsResponse.SystemStats.builder()
.adminCount(0L)
.achievementCount(0L)
.rewardCount(0L)
.uptime("未知")
.build())
.recentActivities(new ArrayList<>())
.updateTime(LocalDateTime.now())
.build();
}
}
+101
View File
@@ -0,0 +1,101 @@
import request from '@/utils/request'
// 仪表盘统计数据类型定义
export interface UserStats {
totalUsers: number
todayNewUsers: number
activeUsers: number
guestUsers: number
}
export interface ContentStats {
totalConversations: number
totalMessages: number
diaryPosts: number
communityPosts: number
emotionRecords: number
}
export interface AiServiceStats {
totalApiCalls: number
todayApiCalls: number
successfulCalls: number
failedCalls: number
avgResponseTime: number
aiConfigCount: number
}
export interface SystemStats {
adminCount: number
achievementCount: number
rewardCount: number
uptime: string
}
export interface RecentActivity {
type: string
description: string
userId: string
username: string
activityTime: string
extraData: Record<string, any>
}
export interface DashboardStats {
userStats: UserStats
contentStats: ContentStats
aiServiceStats: AiServiceStats
systemStats: SystemStats
recentActivities: RecentActivity[]
updateTime: string
}
/**
* 获取仪表盘统计数据
*/
export function getDashboardStats() {
return request<DashboardStats>({
url: '/admin/dashboard/stats',
method: 'get'
})
}
/**
* 获取用户统计数据
*/
export function getUserStats() {
return request<UserStats>({
url: '/admin/dashboard/user-stats',
method: 'get'
})
}
/**
* 获取内容统计数据
*/
export function getContentStats() {
return request<ContentStats>({
url: '/admin/dashboard/content-stats',
method: 'get'
})
}
/**
* 获取AI服务统计数据
*/
export function getAiServiceStats() {
return request<AiServiceStats>({
url: '/admin/dashboard/ai-stats',
method: 'get'
})
}
/**
* 获取系统统计数据
*/
export function getSystemStats() {
return request<SystemStats>({
url: '/admin/dashboard/system-stats',
method: 'get'
})
}
+157 -8
View File
@@ -1,8 +1,26 @@
<template> <template>
<div class="dashboard"> <div class="dashboard">
<div class="dashboard-header">
<h2 class="page-title">仪表盘</h2> <h2 class="page-title">仪表盘</h2>
<div class="dashboard-actions">
<el-text type="info" size="small" v-if="dashboardStats.updateTime">
最后更新: {{ new Date(dashboardStats.updateTime).toLocaleString() }}
</el-text>
<el-text type="success" size="small" v-if="dashboardStats.systemStats.uptime">
系统运行: {{ dashboardStats.systemStats.uptime }}
</el-text>
<el-button
type="primary"
size="small"
:loading="loading"
@click="fetchDashboardData"
>
刷新数据
</el-button>
</div>
</div>
<el-row :gutter="20" class="stats-row"> <el-row :gutter="20" class="stats-row" v-loading="loading">
<el-col :span="6"> <el-col :span="6">
<el-card class="stat-card"> <el-card class="stat-card">
<div class="stat-content"> <div class="stat-content">
@@ -10,7 +28,7 @@
<el-icon><User /></el-icon> <el-icon><User /></el-icon>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<div class="stat-value">1,234</div> <div class="stat-value">{{ dashboardStats.userStats.totalUsers.toLocaleString() }}</div>
<div class="stat-label">用户总数</div> <div class="stat-label">用户总数</div>
</div> </div>
</div> </div>
@@ -24,7 +42,7 @@
<el-icon><UserFilled /></el-icon> <el-icon><UserFilled /></el-icon>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<div class="stat-value">5</div> <div class="stat-value">{{ dashboardStats.systemStats.adminCount }}</div>
<div class="stat-label">管理员总数</div> <div class="stat-label">管理员总数</div>
</div> </div>
</div> </div>
@@ -38,8 +56,8 @@
<el-icon><TrendCharts /></el-icon> <el-icon><TrendCharts /></el-icon>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<div class="stat-value">89</div> <div class="stat-value">{{ dashboardStats.userStats.activeUsers.toLocaleString() }}</div>
<div class="stat-label">今日活跃</div> <div class="stat-label">活跃用户</div>
</div> </div>
</div> </div>
</el-card> </el-card>
@@ -52,7 +70,7 @@
<el-icon><ChatDotRound /></el-icon> <el-icon><ChatDotRound /></el-icon>
</div> </div>
<div class="stat-info"> <div class="stat-info">
<div class="stat-value">456</div> <div class="stat-value">{{ dashboardStats.contentStats.totalConversations.toLocaleString() }}</div>
<div class="stat-label">对话总数</div> <div class="stat-label">对话总数</div>
</div> </div>
</div> </div>
@@ -60,6 +78,65 @@
</el-col> </el-col>
</el-row> </el-row>
<!-- 内容统计行 -->
<el-row :gutter="20" class="stats-row" v-loading="loading">
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon" style="background-color: #909399;">
<el-icon><ChatDotRound /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ dashboardStats.contentStats.totalMessages.toLocaleString() }}</div>
<div class="stat-label">消息总数</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon" style="background-color: #67c23a;">
<el-icon><User /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ dashboardStats.userStats.guestUsers.toLocaleString() }}</div>
<div class="stat-label">访客用户</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon" style="background-color: #e6a23c;">
<el-icon><TrendCharts /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ dashboardStats.contentStats.emotionRecords.toLocaleString() }}</div>
<div class="stat-label">情绪记录</div>
</div>
</div>
</el-card>
</el-col>
<el-col :span="6">
<el-card class="stat-card">
<div class="stat-content">
<div class="stat-icon" style="background-color: #f56c6c;">
<el-icon><Setting /></el-icon>
</div>
<div class="stat-info">
<div class="stat-value">{{ dashboardStats.aiServiceStats.totalApiCalls.toLocaleString() }}</div>
<div class="stat-label">API调用总数</div>
</div>
</div>
</el-card>
</el-col>
</el-row>
<!-- AI配置统计 --> <!-- AI配置统计 -->
<el-row :gutter="20" class="stats-row"> <el-row :gutter="20" class="stats-row">
<el-col :span="6"> <el-col :span="6">
@@ -153,10 +230,45 @@ import { ref, reactive, onMounted } from 'vue'
import { User, UserFilled, TrendCharts, ChatDotRound, Setting, CircleCheck, CircleClose, Star } from '@element-plus/icons-vue' import { User, UserFilled, TrendCharts, ChatDotRound, Setting, CircleCheck, CircleClose, Star } from '@element-plus/icons-vue'
import * as echarts from 'echarts' import * as echarts from 'echarts'
import { countEnabledConfigs, countDisabledConfigs, countDefaultConfigs } from '@/api/aiconfig' import { countEnabledConfigs, countDisabledConfigs, countDefaultConfigs } from '@/api/aiconfig'
import { getDashboardStats, type DashboardStats } from '@/api/dashboard'
import AiConfigQuickActions from '@/components/AiConfigQuickActions.vue' import AiConfigQuickActions from '@/components/AiConfigQuickActions.vue'
import { ElMessage } from 'element-plus'
const userChartRef = ref<HTMLElement>() const userChartRef = ref<HTMLElement>()
// 仪表盘统计数据
const dashboardStats = reactive<DashboardStats>({
userStats: {
totalUsers: 0,
todayNewUsers: 0,
activeUsers: 0,
guestUsers: 0
},
contentStats: {
totalConversations: 0,
totalMessages: 0,
diaryPosts: 0,
communityPosts: 0,
emotionRecords: 0
},
aiServiceStats: {
totalApiCalls: 0,
todayApiCalls: 0,
successfulCalls: 0,
failedCalls: 0,
avgResponseTime: 0,
aiConfigCount: 0
},
systemStats: {
adminCount: 0,
achievementCount: 0,
rewardCount: 0,
uptime: ''
},
recentActivities: [],
updateTime: ''
})
const aiStats = reactive({ const aiStats = reactive({
total: 0, total: 0,
enabled: 0, enabled: 0,
@@ -171,11 +283,35 @@ const recentLogins = ref([
{ username: '赵六', time: '15分钟前' } { username: '赵六', time: '15分钟前' }
]) ])
const loading = ref(false)
onMounted(() => { onMounted(() => {
initUserChart() initUserChart()
fetchAiStats() fetchDashboardData()
}) })
// 获取仪表盘数据
const fetchDashboardData = async () => {
loading.value = true
try {
// 获取仪表盘统计数据
const statsRes = await getDashboardStats()
if (statsRes.data) {
Object.assign(dashboardStats, statsRes.data)
}
// 获取AI配置统计(保持原有逻辑)
await fetchAiStats()
ElMessage.success('数据加载成功')
} catch (error) {
console.error('获取仪表盘数据失败:', error)
ElMessage.error('数据加载失败')
} finally {
loading.value = false
}
}
// 获取AI配置统计 // 获取AI配置统计
const fetchAiStats = async () => { const fetchAiStats = async () => {
try { try {
@@ -232,12 +368,25 @@ const initUserChart = () => {
<style scoped lang="scss"> <style scoped lang="scss">
.dashboard { .dashboard {
.page-title { .dashboard-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px; margin-bottom: 20px;
.page-title {
margin: 0;
font-size: 24px; font-size: 24px;
color: #333; color: #333;
} }
.dashboard-actions {
display: flex;
align-items: center;
gap: 15px;
}
}
.stats-row { .stats-row {
margin-bottom: 20px; margin-bottom: 20px;
} }