From bbe79ecffb123d37468be939067e0f26b56acfaa Mon Sep 17 00:00:00 2001 From: huazhongmin Date: Fri, 31 Oct 2025 14:33:57 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=AA=E8=A1=A8=E7=9B=98=E5=8A=9F=E8=83=BD?= =?UTF-8?q?=E5=AE=8C=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../emotion/controller/AdminController.java | 23 +++ .../dto/response/DashboardStatsResponse.java | 53 ++++++ .../com/emotion/service/DashboardService.java | 17 ++ .../service/impl/DashboardServiceImpl.java | 105 ++++++++++++ .../emotion/service/DashboardServiceTest.java | 46 ++++++ sql/emotion_museum.sql | 1 + web-admin/src/api/dashboard.ts | 39 +++++ web-admin/src/views/Dashboard.vue | 152 +++++++++++++++--- 8 files changed, 416 insertions(+), 20 deletions(-) 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 0a89657..dc131f2 100644 --- a/backend-single/src/main/java/com/emotion/controller/AdminController.java +++ b/backend-single/src/main/java/com/emotion/controller/AdminController.java @@ -14,6 +14,7 @@ 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; /** * 管理员控制器 @@ -143,4 +144,26 @@ public class AdminController { DashboardStatsResponse.SystemStats systemStats = dashboardService.getSystemStats(); return Result.success("获取成功", systemStats); } + + /** + * 获取用户增长趋势数据 + */ + @Operation(summary = "获取用户增长趋势数据", description = "获取指定天数的用户增长趋势数据") + @GetMapping("/dashboard/user-growth-trends") + public Result> getUserGrowthTrends( + @RequestParam(defaultValue = "7") int days) { + List trends = dashboardService.getUserGrowthTrends(days); + return Result.success("获取成功", trends); + } + + /** + * 获取最近登录用户 + */ + @Operation(summary = "获取最近登录用户", description = "获取最近登录的用户列表") + @GetMapping("/dashboard/recent-logins") + public Result> getRecentLogins( + @RequestParam(defaultValue = "10") int limit) { + List recentLogins = dashboardService.getRecentLogins(limit); + return Result.success("获取成功", recentLogins); + } } 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 index bcd7c83..a91c95f 100644 --- a/backend-single/src/main/java/com/emotion/dto/response/DashboardStatsResponse.java +++ b/backend-single/src/main/java/com/emotion/dto/response/DashboardStatsResponse.java @@ -41,6 +41,12 @@ public class DashboardStatsResponse { @Schema(description = "数据更新时间") private LocalDateTime updateTime; + @Schema(description = "用户增长趋势数据") + private List userGrowthTrends; + + @Schema(description = "最近登录用户") + private List recentLogins; + /** * 用户统计 */ @@ -165,4 +171,51 @@ public class DashboardStatsResponse { @Schema(description = "额外数据") private Map extraData; } + + /** + * 用户增长趋势 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "用户增长趋势") + public static class UserGrowthTrend { + @Schema(description = "日期") + private String date; + + @Schema(description = "新增用户数") + private Long newUsers; + + @Schema(description = "累计用户数") + private Long totalUsers; + } + + /** + * 最近登录用户 + */ + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + @Schema(description = "最近登录用户") + public static class RecentLogin { + @Schema(description = "用户ID") + private String userId; + + @Schema(description = "用户名") + private String username; + + @Schema(description = "昵称") + private String nickname; + + @Schema(description = "头像") + private String avatar; + + @Schema(description = "登录时间") + private LocalDateTime loginTime; + + @Schema(description = "登录时间描述") + private String timeDescription; + } } \ 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 index 3938236..950b018 100644 --- a/backend-single/src/main/java/com/emotion/service/DashboardService.java +++ b/backend-single/src/main/java/com/emotion/service/DashboardService.java @@ -1,6 +1,7 @@ package com.emotion.service; import com.emotion.dto.response.DashboardStatsResponse; +import java.util.List; /** * 仪表盘服务接口 @@ -44,4 +45,20 @@ public interface DashboardService { * @return 系统统计数据 */ DashboardStatsResponse.SystemStats getSystemStats(); + + /** + * 获取用户增长趋势数据 + * + * @param days 天数,默认7天 + * @return 用户增长趋势数据 + */ + List getUserGrowthTrends(int days); + + /** + * 获取最近登录用户 + * + * @param limit 限制数量,默认10个 + * @return 最近登录用户列表 + */ + List getRecentLogins(int limit); } \ 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 index ee3f6ff..92194b8 100644 --- a/backend-single/src/main/java/com/emotion/service/impl/DashboardServiceImpl.java +++ b/backend-single/src/main/java/com/emotion/service/impl/DashboardServiceImpl.java @@ -73,6 +73,8 @@ public class DashboardServiceImpl implements DashboardService { .aiServiceStats(getAiServiceStats()) .systemStats(getSystemStats()) .recentActivities(getRecentActivities()) + .userGrowthTrends(getUserGrowthTrends(7)) + .recentLogins(getRecentLogins(10)) .updateTime(LocalDateTime.now()) .build(); } catch (Exception e) { @@ -385,7 +387,110 @@ public class DashboardServiceImpl implements DashboardService { .uptime("未知") .build()) .recentActivities(new ArrayList<>()) + .userGrowthTrends(new ArrayList<>()) + .recentLogins(new ArrayList<>()) .updateTime(LocalDateTime.now()) .build(); } + + @Override + public List getUserGrowthTrends(int days) { + List trends = new ArrayList<>(); + + try { + LocalDate endDate = LocalDate.now(); + LocalDate startDate = endDate.minusDays(days - 1); + + // 获取指定日期范围内每天的用户注册数据 + for (int i = 0; i < days; i++) { + LocalDate currentDate = startDate.plusDays(i); + LocalDateTime dayStart = currentDate.atStartOfDay(); + LocalDateTime dayEnd = dayStart.plusDays(1); + + // 统计当天新增用户数 + Long newUsers = userService.count(new LambdaQueryWrapper() + .eq(User::getIsDeleted, 0) + .ge(User::getCreateTime, dayStart) + .lt(User::getCreateTime, dayEnd)); + + // 统计截止到当天的总用户数 + Long totalUsers = userService.count(new LambdaQueryWrapper() + .eq(User::getIsDeleted, 0) + .lt(User::getCreateTime, dayEnd)); + + trends.add(DashboardStatsResponse.UserGrowthTrend.builder() + .date(currentDate.toString()) + .newUsers(newUsers) + .totalUsers(totalUsers) + .build()); + } + + } catch (Exception e) { + log.error("获取用户增长趋势数据失败", e); + } + + return trends; + } + + @Override + public List getRecentLogins(int limit) { + List recentLogins = new ArrayList<>(); + + try { + // 获取最近活跃的用户(按最后活跃时间排序) + List recentUsers = userService.list(new LambdaQueryWrapper() + .eq(User::getIsDeleted, 0) + .isNotNull(User::getLastActiveTime) + .orderByDesc(User::getLastActiveTime) + .last("LIMIT " + limit)); + + for (User user : recentUsers) { + String timeDescription = formatTimeDescription(user.getLastActiveTime()); + + recentLogins.add(DashboardStatsResponse.RecentLogin.builder() + .userId(user.getId()) + .username(user.getUsername()) + .nickname(user.getNickname()) + .avatar(user.getAvatar()) + .loginTime(user.getLastActiveTime()) + .timeDescription(timeDescription) + .build()); + } + + } catch (Exception e) { + log.error("获取最近登录用户失败", e); + } + + return recentLogins; + } + + /** + * 格式化时间描述 + */ + private String formatTimeDescription(LocalDateTime dateTime) { + if (dateTime == null) { + return "未知"; + } + + LocalDateTime now = LocalDateTime.now(); + long minutes = java.time.Duration.between(dateTime, now).toMinutes(); + + if (minutes < 1) { + return "刚刚"; + } else if (minutes < 60) { + return minutes + "分钟前"; + } else if (minutes < 1440) { // 24小时 + long hours = minutes / 60; + return hours + "小时前"; + } else { + long days = minutes / 1440; + if (days == 1) { + return "昨天"; + } else if (days < 7) { + return days + "天前"; + } else { + return dateTime.toLocalDate().toString(); + } + } + } } \ No newline at end of file diff --git a/backend-single/src/test/java/com/emotion/service/DashboardServiceTest.java b/backend-single/src/test/java/com/emotion/service/DashboardServiceTest.java index 09053d4..da69664 100644 --- a/backend-single/src/test/java/com/emotion/service/DashboardServiceTest.java +++ b/backend-single/src/test/java/com/emotion/service/DashboardServiceTest.java @@ -85,6 +85,8 @@ public class DashboardServiceTest { System.out.println("奖励数量: " + systemStats.getRewardCount()); System.out.println("系统运行时间: " + systemStats.getUptime()); System.out.println("最近活动数量: " + stats.getRecentActivities().size()); + System.out.println("用户增长趋势数量: " + (stats.getUserGrowthTrends() != null ? stats.getUserGrowthTrends().size() : 0)); + System.out.println("最近登录用户数量: " + (stats.getRecentLogins() != null ? stats.getRecentLogins().size() : 0)); } @Test @@ -137,4 +139,48 @@ public class DashboardServiceTest { assertTrue(systemStats.getRewardCount() >= 0, "奖励数量应大于等于0"); assertNotNull(systemStats.getUptime(), "系统运行时间不应为空"); } + + @Test + public void testGetUserGrowthTrends() { + // 测试获取用户增长趋势数据 + var trends = dashboardService.getUserGrowthTrends(7); + + assertNotNull(trends, "用户增长趋势数据不应为空"); + assertTrue(trends.size() <= 7, "趋势数据数量应不超过7天"); + + for (var trend : trends) { + assertNotNull(trend.getDate(), "日期不应为空"); + assertTrue(trend.getNewUsers() >= 0, "新增用户数应大于等于0"); + assertTrue(trend.getTotalUsers() >= 0, "总用户数应大于等于0"); + } + + System.out.println("=== 用户增长趋势测试结果 ==="); + for (var trend : trends) { + System.out.println(String.format("日期: %s, 新增: %d, 总计: %d", + trend.getDate(), trend.getNewUsers(), trend.getTotalUsers())); + } + } + + @Test + public void testGetRecentLogins() { + // 测试获取最近登录用户 + var recentLogins = dashboardService.getRecentLogins(10); + + assertNotNull(recentLogins, "最近登录用户数据不应为空"); + assertTrue(recentLogins.size() <= 10, "最近登录用户数量应不超过10个"); + + for (var login : recentLogins) { + assertNotNull(login.getUserId(), "用户ID不应为空"); + assertNotNull(login.getUsername(), "用户名不应为空"); + assertNotNull(login.getTimeDescription(), "时间描述不应为空"); + } + + System.out.println("=== 最近登录用户测试结果 ==="); + for (var login : recentLogins) { + System.out.println(String.format("用户: %s (%s), 时间: %s", + login.getNickname() != null ? login.getNickname() : login.getUsername(), + login.getUsername(), + login.getTimeDescription())); + } + } } \ No newline at end of file diff --git a/sql/emotion_museum.sql b/sql/emotion_museum.sql index 0298e6d..e2e50c3 100644 --- a/sql/emotion_museum.sql +++ b/sql/emotion_museum.sql @@ -167,6 +167,7 @@ CREATE TABLE t_message ( id VARCHAR(64) PRIMARY KEY COMMENT 'UUID主键', -- UUID主键 conversation_id VARCHAR(64) COMMENT '对话ID (关联t_conversation.id)', -- 对话ID (关联t_conversation.id) content TEXT COMMENT '消息内容', -- 消息内容 + message_order BIGINT COMMENT '消息顺序', type VARCHAR(50) DEFAULT 'text' COMMENT '消息类型', -- 消息类型 sender VARCHAR(20) COMMENT '发送者: user-用户, assistant-AI助手', -- 发送者: user-用户, assistant-AI助手 timestamp DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '消息时间戳', -- 消息时间戳 diff --git a/web-admin/src/api/dashboard.ts b/web-admin/src/api/dashboard.ts index 30fa05c..fe03c87 100644 --- a/web-admin/src/api/dashboard.ts +++ b/web-admin/src/api/dashboard.ts @@ -41,12 +41,29 @@ export interface RecentActivity { extraData: Record } +export interface UserGrowthTrend { + date: string + newUsers: number + totalUsers: number +} + +export interface RecentLogin { + userId: string + username: string + nickname: string + avatar: string + loginTime: string + timeDescription: string +} + export interface DashboardStats { userStats: UserStats contentStats: ContentStats aiServiceStats: AiServiceStats systemStats: SystemStats recentActivities: RecentActivity[] + userGrowthTrends: UserGrowthTrend[] + recentLogins: RecentLogin[] updateTime: string } @@ -98,4 +115,26 @@ export function getSystemStats() { url: '/admin/dashboard/system-stats', method: 'get' }) +} + +/** + * 获取用户增长趋势数据 + */ +export function getUserGrowthTrends(days: number = 7) { + return request({ + url: '/admin/dashboard/user-growth-trends', + method: 'get', + params: { days } + }) +} + +/** + * 获取最近登录用户 + */ +export function getRecentLogins(limit: number = 10) { + return request({ + url: '/admin/dashboard/recent-logins', + method: 'get', + params: { limit } + }) } \ No newline at end of file diff --git a/web-admin/src/views/Dashboard.vue b/web-admin/src/views/Dashboard.vue index f87cb36..2ab49b1 100644 --- a/web-admin/src/views/Dashboard.vue +++ b/web-admin/src/views/Dashboard.vue @@ -211,9 +211,25 @@ - - - + + + + + @@ -230,7 +246,7 @@ import { ref, reactive, onMounted } from 'vue' import { User, UserFilled, TrendCharts, ChatDotRound, Setting, CircleCheck, CircleClose, Star } from '@element-plus/icons-vue' import * as echarts from 'echarts' import { countEnabledConfigs, countDisabledConfigs, countDefaultConfigs } from '@/api/aiconfig' -import { getDashboardStats, type DashboardStats } from '@/api/dashboard' +import { getDashboardStats, getUserGrowthTrends, getRecentLogins, type DashboardStats, type UserGrowthTrend, type RecentLogin } from '@/api/dashboard' import AiConfigQuickActions from '@/components/AiConfigQuickActions.vue' import { ElMessage } from 'element-plus' @@ -276,12 +292,8 @@ const aiStats = reactive({ default: 0 }) -const recentLogins = ref([ - { username: '张三', time: '2分钟前' }, - { username: '李四', time: '5分钟前' }, - { username: '王五', time: '10分钟前' }, - { username: '赵六', time: '15分钟前' } -]) +const recentLogins = ref([]) +const userGrowthTrends = ref([]) const loading = ref(false) @@ -298,11 +310,36 @@ const fetchDashboardData = async () => { const statsRes = await getDashboardStats() if (statsRes.data) { Object.assign(dashboardStats, statsRes.data) + // 如果响应中包含增长趋势和最近登录数据,直接使用 + if (statsRes.data.userGrowthTrends) { + userGrowthTrends.value = statsRes.data.userGrowthTrends + } + if (statsRes.data.recentLogins) { + recentLogins.value = statsRes.data.recentLogins + } + } + + // 如果主接口没有返回这些数据,单独获取 + if (!userGrowthTrends.value.length) { + const trendsRes = await getUserGrowthTrends(7) + if (trendsRes.data) { + userGrowthTrends.value = trendsRes.data + } + } + + if (!recentLogins.value.length) { + const loginsRes = await getRecentLogins(10) + if (loginsRes.data) { + recentLogins.value = loginsRes.data + } } // 获取AI配置统计(保持原有逻辑) await fetchAiStats() + // 更新图表数据 + updateUserChart() + ElMessage.success('数据加载成功') } catch (error) { console.error('获取仪表盘数据失败:', error) @@ -329,40 +366,87 @@ const fetchAiStats = async () => { } } +let userChart: any = null + const initUserChart = () => { if (!userChartRef.value) return - const chart = echarts.init(userChartRef.value) + userChart = echarts.init(userChartRef.value) + + // 初始化空图表 + updateUserChart() + + window.addEventListener('resize', () => { + userChart?.resize() + }) +} + +const updateUserChart = () => { + if (!userChart || !userGrowthTrends.value.length) return + + // 处理日期标签,显示月-日格式 + const dates = userGrowthTrends.value.map(trend => { + const date = new Date(trend.date) + return `${date.getMonth() + 1}-${date.getDate()}` + }) + + const newUsersData = userGrowthTrends.value.map(trend => trend.newUsers) const option = { tooltip: { - trigger: 'axis' + trigger: 'axis', + formatter: (params: any) => { + const dataIndex = params[0].dataIndex + const trend = userGrowthTrends.value[dataIndex] + return ` +
+
日期: ${trend.date}
+
新增用户: ${trend.newUsers}
+
累计用户: ${trend.totalUsers}
+
+ ` + } }, xAxis: { type: 'category', - data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'] + data: dates, + axisLabel: { + fontSize: 12 + } }, yAxis: { - type: 'value' + type: 'value', + axisLabel: { + fontSize: 12 + } }, series: [ { name: '新增用户', type: 'line', - data: [12, 23, 18, 29, 35, 42, 38], + data: newUsersData, smooth: true, itemStyle: { color: '#409eff' + }, + areaStyle: { + color: { + type: 'linear', + x: 0, + y: 0, + x2: 0, + y2: 1, + colorStops: [ + { offset: 0, color: 'rgba(64, 158, 255, 0.3)' }, + { offset: 1, color: 'rgba(64, 158, 255, 0.1)' } + ] + } } } ] } - chart.setOption(option) - - window.addEventListener('resize', () => { - chart.resize() - }) + userChart.setOption(option) } @@ -429,5 +513,33 @@ const initUserChart = () => { .chart-container { height: 300px; } + + .user-info { + display: flex; + align-items: center; + gap: 10px; + + .user-name { + flex: 1; + min-width: 0; + + .nickname { + font-size: 14px; + font-weight: 500; + color: #333; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .username { + font-size: 12px; + color: #999; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + } + } }