diff --git a/backend-single/src/main/java/com/emotion/controller/AdminController.java b/backend-single/src/main/java/com/emotion/controller/AdminController.java index 1a267e7..0a89657 100644 --- a/backend-single/src/main/java/com/emotion/controller/AdminController.java +++ b/backend-single/src/main/java/com/emotion/controller/AdminController.java @@ -6,7 +6,9 @@ import com.emotion.dto.request.AdminCreateRequest; import com.emotion.dto.request.AdminPageRequest; import com.emotion.dto.request.AdminUpdateRequest; 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.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; @@ -27,6 +29,9 @@ public class AdminController { @Autowired private AdminService adminService; + @Autowired + private DashboardService dashboardService; + /** * 分页查询管理员 */ @@ -88,4 +93,54 @@ public class AdminController { } return Result.success("删除成功", null); } + + /** + * 获取仪表盘统计数据 + */ + @Operation(summary = "获取仪表盘统计数据", description = "获取管理后台仪表盘的统计数据,包括用户、内容、AI服务和系统统计") + @GetMapping("/dashboard/stats") + public Result getDashboardStats() { + DashboardStatsResponse stats = dashboardService.getDashboardStats(); + return Result.success("获取成功", stats); + } + + /** + * 获取用户统计数据 + */ + @Operation(summary = "获取用户统计数据", description = "获取用户相关的统计数据") + @GetMapping("/dashboard/user-stats") + public Result getUserStats() { + DashboardStatsResponse.UserStats userStats = dashboardService.getUserStats(); + return Result.success("获取成功", userStats); + } + + /** + * 获取内容统计数据 + */ + @Operation(summary = "获取内容统计数据", description = "获取内容相关的统计数据") + @GetMapping("/dashboard/content-stats") + public Result getContentStats() { + DashboardStatsResponse.ContentStats contentStats = dashboardService.getContentStats(); + return Result.success("获取成功", contentStats); + } + + /** + * 获取AI服务统计数据 + */ + @Operation(summary = "获取AI服务统计数据", description = "获取AI服务相关的统计数据") + @GetMapping("/dashboard/ai-stats") + public Result getAiServiceStats() { + DashboardStatsResponse.AiServiceStats aiStats = dashboardService.getAiServiceStats(); + return Result.success("获取成功", aiStats); + } + + /** + * 获取系统统计数据 + */ + @Operation(summary = "获取系统统计数据", description = "获取系统相关的统计数据") + @GetMapping("/dashboard/system-stats") + public Result getSystemStats() { + DashboardStatsResponse.SystemStats systemStats = dashboardService.getSystemStats(); + return Result.success("获取成功", systemStats); + } } diff --git a/backend-single/src/main/java/com/emotion/dto/response/DashboardStatsResponse.java b/backend-single/src/main/java/com/emotion/dto/response/DashboardStatsResponse.java new file mode 100644 index 0000000..bcd7c83 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/dto/response/DashboardStatsResponse.java @@ -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 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 extraData; + } +} \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/service/DashboardService.java b/backend-single/src/main/java/com/emotion/service/DashboardService.java new file mode 100644 index 0000000..3938236 --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/DashboardService.java @@ -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(); +} \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/service/impl/AiChatServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/AiChatServiceImpl.java index 686cef9..357a86e 100644 --- a/backend-single/src/main/java/com/emotion/service/impl/AiChatServiceImpl.java +++ b/backend-single/src/main/java/com/emotion/service/impl/AiChatServiceImpl.java @@ -842,6 +842,7 @@ public class AiChatServiceImpl implements AiChatService { private String executeStreamCozeApiCall(CozeApiCall apiCall, AiConfig config, Map requestBody, String conversationId, String userMessage, String userId) { try { + // 构建请求头 HttpHeaders headers = new HttpHeaders(); headers.set("Authorization", "Bearer " + config.getApiToken()); @@ -854,7 +855,9 @@ public class AiChatServiceImpl implements AiChatService { // 更新API调用记录的请求信息 updateApiCallRequest(apiCall, cozeApiUrl, requestBody, headers); - log.info("发送Coze流式请求到: {}, 请求体: {}", cozeApiUrl, requestBody); + log.info("发送Coze流式请求到: {}", cozeApiUrl); + log.info("请求头: {}", headers.toSingleValueMap()); + log.info("请求体: {}", JSON.toJSONString(requestBody)); // 使用RestTemplate处理流式响应 String streamResponse = handleStreamResponse(cozeApiUrl, headers, requestBody, apiCall); @@ -878,30 +881,37 @@ public class AiChatServiceImpl implements AiChatService { */ private String handleStreamResponse(String url, HttpHeaders headers, Map requestBody, CozeApiCall apiCall) { try { - // 创建HTTP客户端 + log.info("开始处理流式响应,URL: {}", url); + + // 创建HTTP客户端,增加更长的超时时间 java.net.http.HttpClient client = java.net.http.HttpClient.newBuilder() - .connectTimeout(java.time.Duration.ofSeconds(30)) + .connectTimeout(java.time.Duration.ofSeconds(60)) .build(); // 构建请求 java.net.http.HttpRequest.Builder requestBuilder = java.net.http.HttpRequest.newBuilder() .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))); // 添加请求头 + log.info("设置请求头:"); headers.forEach((key, values) -> { 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(); + log.info("发送流式请求,请求体: {}", JSON.toJSONString(requestBody)); // 发送请求并处理流式响应 StringBuilder responseBuilder = new StringBuilder(); StringBuilder fullStreamData = new StringBuilder(); - final String[] currentEvent = {null}; // 使用数组来解决lambda中的final问题 + String currentEvent = null; + boolean hasReceivedData = false; java.net.http.HttpResponse> response = client.send(request, java.net.http.HttpResponse.BodyHandlers.ofLines()); @@ -911,93 +921,198 @@ public class AiChatServiceImpl implements AiChatService { if (response.statusCode() != 200) { String errorBody = response.body().collect(java.util.stream.Collectors.joining("\n")); log.error("流式请求失败,状态码: {}, 响应: {}", response.statusCode(), errorBody); - return "流式请求失败,状态码: " + response.statusCode(); + updateApiCallError(apiCall, "HTTP_ERROR", "流式请求失败,状态码: " + response.statusCode()); + return handleNonStreamFallback(url, headers, requestBody, apiCall); } // 处理流式数据 - 正确处理SSE格式 - response.body().forEach(line -> { + java.util.Iterator lineIterator = response.body().iterator(); + while (lineIterator.hasNext()) { + String line = lineIterator.next(); fullStreamData.append(line).append("\n"); - log.debug("收到流式数据行: {}", line); + + if (line.trim().isEmpty()) { + // 空行表示事件结束,重置当前事件 + currentEvent = null; + continue; + } // 处理SSE事件格式 if (line.startsWith("event:")) { - currentEvent[0] = line.substring(6).trim(); - log.debug("当前事件类型: {}", currentEvent[0]); - } else if (line.startsWith("data: ")) { - String data = line.substring(6).trim(); - log.debug("解析流式数据: {}", data); + currentEvent = line.substring(6).trim(); + log.debug("当前事件类型: {}", currentEvent); + } else if (line.startsWith("data:")) { + String data = line.substring(5).trim(); // 注意这里是5,因为"data:"后面可能没有空格 // 检查是否为结束标记 - if ("[DONE]".equals(data)) { - log.info("流式响应完成"); - return; + if ("\"[DONE]\"".equals(data) || "[DONE]".equals(data)) { + log.info("收到流式响应结束标记"); + 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 { JSONObject jsonData = JSON.parseObject(data); - log.debug("解析后的JSON数据: {}", jsonData); - // 提取content字段 - if (jsonData.containsKey("content")) { + // 提取content字段并检查类型 + if (jsonData.containsKey("content") && jsonData.containsKey("type")) { + String messageType = jsonData.getString("type"); String content = jsonData.getString("content"); - if (content != null && !content.trim().isEmpty()) { + + // 只处理answer类型的消息内容 + if ("answer".equals(messageType) && content != null && !content.trim().isEmpty()) { log.debug("提取增量内容: {}", content); responseBuilder.append(content); } } } 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 { JSONObject jsonData = JSON.parseObject(data); // 检查是否为answer类型的消息 if (jsonData.containsKey("type") && "answer".equals(jsonData.getString("type"))) { String content = jsonData.getString("content"); - if (content != null && !content.trim().isEmpty() && responseBuilder.length() == 0) { - log.debug("使用完整消息内容作为备用: {}", content); - responseBuilder.append(content); + if (content != null && !content.trim().isEmpty()) { + // 如果增量方式没有获取到内容,使用完整消息作为备用 + if (responseBuilder.length() == 0) { + log.info("使用完整消息内容作为备用: {}", content); + responseBuilder.append(content); + } + // 记录完整消息用于验证 + log.debug("完整消息内容: {}", content); } } } catch (Exception e) { 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); + + // 如果是参数错误,直接返回错误信息,不再尝试降级处理 + if ("4000".equals(errorCode)) { + updateApiCallError(apiCall, "COZE_PARAM_ERROR", "参数错误: " + errorMsg); + return "抱歉,请求参数有误,请检查配置后重试。"; + } + } + } 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()); + } } - - // 重置当前事件 - currentEvent[0] = null; - } else if (line.trim().isEmpty()) { - // 空行表示事件结束 - currentEvent[0] = null; } - }); + } // 记录完整的流式数据用于调试 updateApiCallStreamData(apiCall, fullStreamData.toString()); - log.info("流式响应处理完成,提取内容长度: {}", responseBuilder.length()); + log.info("流式响应处理完成,提取内容长度: {}, 是否接收到数据: {}", responseBuilder.length(), hasReceivedData); String finalResponse = responseBuilder.toString(); 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("流式响应为空,尝试降级到非流式处理"); - // 降级到非流式处理 return handleNonStreamFallback(url, headers, requestBody, apiCall); } return finalResponse; } catch (Exception e) { - log.error("处理流式响应失败", e); + log.error("处理流式响应失败: {}", e.getMessage(), e); + updateApiCallError(apiCall, "STREAM_PROCESSING_ERROR", e.getMessage()); // 降级到非流式处理 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"); HttpEntity> request = new HttpEntity<>(nonStreamBody, nonStreamHeaders); - log.info("发送非流式降级请求到: {}", url); + log.info("发送非流式降级请求到: {}, 请求体: {}", url, JSON.toJSONString(nonStreamBody)); // 发送请求 ResponseEntity response = restTemplate.exchange( @@ -1028,7 +1143,10 @@ public class AiChatServiceImpl implements AiChatService { request, String.class); - log.info("收到非流式降级响应: {}", response.getBody()); + log.info("收到非流式降级响应状态: {}, 响应体: {}", response.getStatusCode(), response.getBody()); + + // 更新API调用记录的响应信息 + updateApiCallResponse(apiCall, response); // 解析响应获取chat_id和conversation_id JSONObject responseJson = JSON.parseObject(response.getBody()); @@ -1044,12 +1162,14 @@ public class AiChatServiceImpl implements AiChatService { log.info("非流式降级处理成功: reply={}", aiReply); return aiReply; } else { - log.error("非流式降级处理失败:无法从响应中获取chat_id或conversation_id"); - return "抱歉,AI服务暂时不可用,请稍后再试。"; + log.error("非流式降级处理失败:无法从响应中获取chat_id或conversation_id,响应: {}", response.getBody()); + updateApiCallError(apiCall, "INVALID_RESPONSE", "无法从响应中获取chat_id或conversation_id"); + return "抱歉,AI服务响应异常,请稍后再试。"; } } catch (Exception e) { - log.error("非流式降级处理失败", e); + log.error("非流式降级处理失败: {}", e.getMessage(), e); + updateApiCallError(apiCall, "FALLBACK_ERROR", e.getMessage()); return "抱歉,AI服务暂时不可用,请稍后再试。"; } } @@ -1162,42 +1282,42 @@ public class AiChatServiceImpl implements AiChatService { */ private Map buildCozeRequestWithConfig(String conversationId, String userMessage, String userId, AiConfig config) { Map cozeRequest = new HashMap<>(); + + // 严格按照成功示例的字段顺序和格式构建请求 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); // 根据配置决定是否使用流式输出 boolean useStream = config.getSupportStream() != null && config.getSupportStream() == 1; 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> messages = new java.util.ArrayList<>(); - // 添加当前用户消息 + // 添加当前用户消息,格式完全按照成功示例 Map currentMsg = new HashMap<>(); - currentMsg.put(ROLE_KEY, USER_ROLE); - currentMsg.put(CONTENT_KEY, userMessage); - currentMsg.put(CONTENT_TYPE_KEY, TEXT_TYPE); - // 移除type字段,可能不是必需的 + currentMsg.put("role", "user"); + currentMsg.put("content", userMessage); + currentMsg.put("content_type", "text"); + currentMsg.put("type", "question"); // 根据成功示例添加type字段 messages.add(currentMsg); cozeRequest.put("additional_messages", messages); - // 确保parameters不为空,添加一些默认参数 + // 添加空的parameters对象(根据成功示例) Map parameters = new HashMap<>(); 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; } @@ -1395,6 +1515,7 @@ public class AiChatServiceImpl implements AiChatService { currentMsg.put(ROLE_KEY, USER_ROLE); currentMsg.put(CONTENT_KEY, userMessage); currentMsg.put(CONTENT_TYPE_KEY, TEXT_TYPE); + currentMsg.put("type", "question"); // 添加type字段 messages.add(currentMsg); cozeRequest.put("additional_messages", messages); @@ -1866,4 +1987,47 @@ public class AiChatServiceImpl implements AiChatService { return "/v3/chat"; } + /** + * 验证Coze请求参数 + */ + private void validateCozeRequest(Map 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> messages = (java.util.List>) cozeRequest.get("additional_messages"); + if (messages.isEmpty()) { + throw new RuntimeException("additional_messages不能为空"); + } + + for (Map 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请求参数验证通过"); + } } \ No newline at end of file diff --git a/backend-single/src/main/java/com/emotion/service/impl/DashboardServiceImpl.java b/backend-single/src/main/java/com/emotion/service/impl/DashboardServiceImpl.java new file mode 100644 index 0000000..52e79ce --- /dev/null +++ b/backend-single/src/main/java/com/emotion/service/impl/DashboardServiceImpl.java @@ -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 getRecentActivities() { + List 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(); + } +} \ No newline at end of file diff --git a/web-admin/src/api/dashboard.ts b/web-admin/src/api/dashboard.ts new file mode 100644 index 0000000..30fa05c --- /dev/null +++ b/web-admin/src/api/dashboard.ts @@ -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 +} + +export interface DashboardStats { + userStats: UserStats + contentStats: ContentStats + aiServiceStats: AiServiceStats + systemStats: SystemStats + recentActivities: RecentActivity[] + updateTime: string +} + +/** + * 获取仪表盘统计数据 + */ +export function getDashboardStats() { + return request({ + url: '/admin/dashboard/stats', + method: 'get' + }) +} + +/** + * 获取用户统计数据 + */ +export function getUserStats() { + return request({ + url: '/admin/dashboard/user-stats', + method: 'get' + }) +} + +/** + * 获取内容统计数据 + */ +export function getContentStats() { + return request({ + url: '/admin/dashboard/content-stats', + method: 'get' + }) +} + +/** + * 获取AI服务统计数据 + */ +export function getAiServiceStats() { + return request({ + url: '/admin/dashboard/ai-stats', + method: 'get' + }) +} + +/** + * 获取系统统计数据 + */ +export function getSystemStats() { + return request({ + url: '/admin/dashboard/system-stats', + method: 'get' + }) +} \ No newline at end of file diff --git a/web-admin/src/views/Dashboard.vue b/web-admin/src/views/Dashboard.vue index 9cdbc45..f87cb36 100644 --- a/web-admin/src/views/Dashboard.vue +++ b/web-admin/src/views/Dashboard.vue @@ -1,8 +1,26 @@