feat: 增强管理端工作台用户活跃榜能力
- 新增用户活跃榜接口、筛选与导出能力 - 支持按智能体过滤排行榜并补充前后端测试
This commit is contained in:
@@ -1,13 +1,18 @@
|
||||
package tech.easyflow.admin.service.dashboard.impl;
|
||||
|
||||
import com.mybatisflex.core.query.QueryWrapper;
|
||||
import org.apache.poi.ss.usermodel.WorkbookFactory;
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.Test;
|
||||
import tech.easyflow.ai.entity.Bot;
|
||||
import tech.easyflow.ai.service.BotService;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardAssistantTrendSeriesVo;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardDistributionItemVo;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardOverviewQuery;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardSummaryVo;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardTrendItemVo;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardUserRankItemVo;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardUserRankQuery;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatActiveUserRank;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatAssistantSessionTrend;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatAssistantUsageRank;
|
||||
@@ -15,8 +20,15 @@ import tech.easyflow.chatlog.domain.dto.ChatDashboardSummary;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatDashboardTrend;
|
||||
import tech.easyflow.chatlog.service.ChatDashboardQueryService;
|
||||
import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysAccount;
|
||||
import tech.easyflow.system.service.CategoryPermissionService;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
import tech.easyflow.system.service.SysAccountRoleService;
|
||||
import tech.easyflow.system.service.SysRoleService;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
@@ -25,13 +37,15 @@ import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.IntStream;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.ArgumentMatchers.eq;
|
||||
import static org.mockito.ArgumentMatchers.isNull;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
@@ -71,7 +85,7 @@ public class DashboardServiceImplTest {
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 today 返回 24 个小时点位,且排行名称与均值回退正确。
|
||||
* 验证 today 返回 24 个小时点位,且 overview 不再触发用户榜查询。
|
||||
*
|
||||
* @throws Exception 反射调用失败
|
||||
*/
|
||||
@@ -91,12 +105,7 @@ public class DashboardServiceImplTest {
|
||||
.thenReturn(List.of(new ChatAssistantUsageRank(BigInteger.ONE, "", 2L, 3L, 9L)));
|
||||
when(chatDashboardQueryService.queryAssistantHourlyTrends(any(), any(), any(), any()))
|
||||
.thenReturn(List.of(new ChatAssistantSessionTrend(BigInteger.ONE, "", currentHourKey, 3L)));
|
||||
when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), any(Integer.class)))
|
||||
.thenReturn(List.of(new ChatActiveUserRank(BigInteger.valueOf(2), "demo-user", 3L, 9L, 1L)));
|
||||
SysAccountService sysAccountService = mock(SysAccountService.class);
|
||||
when(sysAccountService.resolveDisplayNameMap(any())).thenReturn(Map.of(BigInteger.valueOf(2), "演示用户"));
|
||||
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||
setField(service, "sysAccountService", sysAccountService);
|
||||
|
||||
Object context = newContext("today", BigInteger.valueOf(9));
|
||||
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||
@@ -107,7 +116,6 @@ public class DashboardServiceImplTest {
|
||||
List<DashboardAssistantTrendSeriesVo> assistantTrends =
|
||||
(List<DashboardAssistantTrendSeriesVo>) readField(payload, "assistantTrends");
|
||||
List<DashboardDistributionItemVo> distribution = (List<DashboardDistributionItemVo>) readField(payload, "distribution");
|
||||
List<DashboardUserRankItemVo> userRanks = (List<DashboardUserRankItemVo>) readField(payload, "userRanks");
|
||||
|
||||
Assert.assertTrue(Boolean.TRUE.equals(invokeGetter(chatStatus, "getAvailable")));
|
||||
Assert.assertEquals(trends.size(), 24);
|
||||
@@ -132,40 +140,31 @@ public class DashboardServiceImplTest {
|
||||
Assert.assertEquals(distribution.get(0).getLabel(), "智能体-1");
|
||||
Assert.assertEquals(distribution.get(0).getAvgMessagePerSession(), Double.valueOf(3D));
|
||||
Assert.assertEquals(distribution.get(0).getAvgSessionPerUser(), Double.valueOf(1.5D));
|
||||
Assert.assertEquals(userRanks.get(0).getLabel(), "演示用户");
|
||||
Assert.assertEquals(userRanks.get(0).getAssistantTotal(), Long.valueOf(1L));
|
||||
verify(chatDashboardQueryService, never()).queryActiveUserRanks(any(), any(), any(), any(), any());
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证当系统账号名称仅回退为纯 ID 时,仍优先继续回退到聊天侧账号。
|
||||
*
|
||||
* @throws Exception 反射调用失败
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldFallbackToUserAccountWhenSystemDisplayNameIsOnlyId() throws Exception {
|
||||
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||
when(chatDashboardQueryService.querySummary(any(), any(), any()))
|
||||
.thenReturn(new ChatDashboardSummary(1L, 1L, 1L, 1L));
|
||||
when(chatDashboardQueryService.queryHourlyTrends(any(), any(), any()))
|
||||
.thenReturn(List.of());
|
||||
when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), any(Integer.class)))
|
||||
.thenReturn(List.of());
|
||||
when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), any(Integer.class)))
|
||||
when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), any(), any()))
|
||||
.thenReturn(List.of(new ChatActiveUserRank(BigInteger.valueOf(9), "chat-user", 1L, 1L, 1L)));
|
||||
SysAccountService sysAccountService = mock(SysAccountService.class);
|
||||
when(sysAccountService.resolveDisplayNameMap(any())).thenReturn(Map.of(BigInteger.valueOf(9), "9"));
|
||||
when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of(buildSysAccount(9L, "", "仅昵称")));
|
||||
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||
setField(service, "sysAccountService", sysAccountService);
|
||||
|
||||
Object context = newContext("today", BigInteger.ONE);
|
||||
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||
Object payload = invokeBuildChatPayload(service, context, summary);
|
||||
DashboardUserRankQuery query = new DashboardUserRankQuery();
|
||||
query.setRange("7d");
|
||||
|
||||
List<DashboardUserRankItemVo> userRanks = (List<DashboardUserRankItemVo>) readField(payload, "userRanks");
|
||||
List<DashboardUserRankItemVo> userRanks = service.getUserRanks(new LoginAccount(), query);
|
||||
Assert.assertEquals(userRanks.get(0).getLabel(), "chat-user");
|
||||
Assert.assertEquals(userRanks.get(0).getLoginName(), "chat-user");
|
||||
Assert.assertEquals(userRanks.get(0).getNickname(), "仅昵称");
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -194,12 +193,7 @@ public class DashboardServiceImplTest {
|
||||
new ChatAssistantSessionTrend(BigInteger.ONE, "助手-A", LocalDate.now().toString(), 4L),
|
||||
new ChatAssistantSessionTrend(null, "未知助手", LocalDate.now().minusDays(3).toString(), 2L)
|
||||
));
|
||||
when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), any(Integer.class)))
|
||||
.thenReturn(List.of());
|
||||
SysAccountService sysAccountService = mock(SysAccountService.class);
|
||||
when(sysAccountService.resolveDisplayNameMap(any())).thenReturn(Map.of());
|
||||
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||
setField(service, "sysAccountService", sysAccountService);
|
||||
|
||||
Object context = newContext("7d", BigInteger.ONE);
|
||||
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||
@@ -238,10 +232,7 @@ public class DashboardServiceImplTest {
|
||||
.thenReturn(List.of(new ChatDashboardTrend(currentHourKey, 2L, 6L, 1L)));
|
||||
when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), any(Integer.class)))
|
||||
.thenReturn(List.of());
|
||||
when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), any(Integer.class)))
|
||||
.thenReturn(List.of());
|
||||
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||
setField(service, "sysAccountService", mock(SysAccountService.class));
|
||||
|
||||
Object context = newContext(
|
||||
"custom",
|
||||
@@ -321,10 +312,7 @@ public class DashboardServiceImplTest {
|
||||
new ChatAssistantSessionTrend(BigInteger.ONE, "助手-1", startDate.toString(), 2L),
|
||||
new ChatAssistantSessionTrend(BigInteger.ONE, "助手-1", endDate.toString(), 4L)
|
||||
));
|
||||
when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), any(Integer.class)))
|
||||
.thenReturn(List.of());
|
||||
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||
setField(service, "sysAccountService", mock(SysAccountService.class));
|
||||
|
||||
Object context = newContext("30d", BigInteger.ONE);
|
||||
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||
@@ -344,6 +332,150 @@ public class DashboardServiceImplTest {
|
||||
verify(chatDashboardQueryService).queryAssistantUsageRanks(any(), any(), any(), eq(8));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证用户榜筛选会透传 assistantId。
|
||||
*/
|
||||
@Test
|
||||
public void shouldQueryUserRanksWithAssistantFilter() throws Exception {
|
||||
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||
BotService botService = mock(BotService.class);
|
||||
CategoryPermissionService categoryPermissionService = mock(CategoryPermissionService.class);
|
||||
SysAccountService sysAccountService = mock(SysAccountService.class);
|
||||
SysAccountRoleService sysAccountRoleService = mock(SysAccountRoleService.class);
|
||||
SysRoleService sysRoleService = mock(SysRoleService.class);
|
||||
|
||||
Bot bot = new Bot();
|
||||
bot.setId(BigInteger.TEN);
|
||||
bot.setStatus(1);
|
||||
bot.setCreatedBy(BigInteger.ONE);
|
||||
bot.setCategoryId(BigInteger.valueOf(8));
|
||||
|
||||
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||
when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), eq(BigInteger.TEN), eq(5)))
|
||||
.thenReturn(List.of(new ChatActiveUserRank(BigInteger.valueOf(2), "demo-user", 2L, 4L, 1L)));
|
||||
when(botService.getById(BigInteger.TEN)).thenReturn(bot);
|
||||
when(categoryPermissionService.canAccessCategory(any(LoginAccount.class), eq("BOT"), eq(BigInteger.ONE), eq(BigInteger.valueOf(8))))
|
||||
.thenReturn(true);
|
||||
when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of(buildSysAccount(2L, "demo-user", "演示用户")));
|
||||
when(sysAccountRoleService.list(any(QueryWrapper.class))).thenReturn(Collections.emptyList());
|
||||
|
||||
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||
setField(service, "botService", botService);
|
||||
setField(service, "categoryPermissionService", categoryPermissionService);
|
||||
setField(service, "sysAccountService", sysAccountService);
|
||||
setField(service, "sysAccountRoleService", sysAccountRoleService);
|
||||
setField(service, "sysRoleService", sysRoleService);
|
||||
|
||||
DashboardUserRankQuery query = new DashboardUserRankQuery();
|
||||
query.setRange("7d");
|
||||
query.setAssistantId(BigInteger.TEN);
|
||||
|
||||
LoginAccount loginAccount = new LoginAccount();
|
||||
loginAccount.setId(BigInteger.valueOf(12));
|
||||
loginAccount.setTenantId(BigInteger.valueOf(33));
|
||||
|
||||
List<DashboardUserRankItemVo> userRanks = service.getUserRanks(loginAccount, query);
|
||||
Assert.assertEquals(userRanks.size(), 1);
|
||||
Assert.assertEquals(userRanks.get(0).getLabel(), "demo-user(演示用户)");
|
||||
verify(chatDashboardQueryService).queryActiveUserRanks(any(), any(), any(), eq(BigInteger.TEN), eq(5));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证未启用智能体会被拒绝。
|
||||
*/
|
||||
@Test(expectedExceptions = BusinessException.class, expectedExceptionsMessageRegExp = "聊天助手不存在或未启用")
|
||||
public void shouldRejectDisabledAssistantFilter() {
|
||||
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||
BotService botService = mock(BotService.class);
|
||||
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||
|
||||
Bot bot = new Bot();
|
||||
bot.setId(BigInteger.TEN);
|
||||
bot.setStatus(0);
|
||||
|
||||
when(botService.getById(BigInteger.TEN)).thenReturn(bot);
|
||||
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||
|
||||
setFieldSilently(service, "botService", botService);
|
||||
setFieldSilently(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||
setFieldSilently(service, "categoryPermissionService", mock(CategoryPermissionService.class));
|
||||
setFieldSilently(service, "sysAccountService", mock(SysAccountService.class));
|
||||
|
||||
DashboardUserRankQuery query = new DashboardUserRankQuery();
|
||||
query.setRange("7d");
|
||||
query.setAssistantId(BigInteger.TEN);
|
||||
service.getUserRanks(new LoginAccount(), query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证当前作用域不可见的智能体会被拒绝。
|
||||
*/
|
||||
@Test(expectedExceptions = BusinessException.class, expectedExceptionsMessageRegExp = "聊天助手不存在或未启用")
|
||||
public void shouldRejectInvisibleAssistantFilter() {
|
||||
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||
BotService botService = mock(BotService.class);
|
||||
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||
CategoryPermissionService categoryPermissionService = mock(CategoryPermissionService.class);
|
||||
|
||||
Bot bot = new Bot();
|
||||
bot.setId(BigInteger.TEN);
|
||||
bot.setStatus(1);
|
||||
bot.setCreatedBy(BigInteger.ONE);
|
||||
bot.setCategoryId(BigInteger.valueOf(8));
|
||||
|
||||
when(botService.getById(BigInteger.TEN)).thenReturn(bot);
|
||||
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||
when(categoryPermissionService.canAccessCategory(any(LoginAccount.class), eq("BOT"), eq(BigInteger.ONE), eq(BigInteger.valueOf(8))))
|
||||
.thenReturn(false);
|
||||
|
||||
setFieldSilently(service, "botService", botService);
|
||||
setFieldSilently(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||
setFieldSilently(service, "categoryPermissionService", categoryPermissionService);
|
||||
setFieldSilently(service, "sysAccountService", mock(SysAccountService.class));
|
||||
|
||||
DashboardUserRankQuery query = new DashboardUserRankQuery();
|
||||
query.setRange("7d");
|
||||
query.setAssistantId(BigInteger.TEN);
|
||||
service.getUserRanks(new LoginAccount(), query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证导出结果包含表头与账号昵称列。
|
||||
*/
|
||||
@Test
|
||||
public void shouldExportDashboardUserRanksWithSeparatedIdentityColumns() throws Exception {
|
||||
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||
SysAccountService sysAccountService = mock(SysAccountService.class);
|
||||
|
||||
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||
when(chatDashboardQueryService.queryActiveUserRanks(any(), any(), any(), isNull(), isNull()))
|
||||
.thenReturn(List.of(new ChatActiveUserRank(BigInteger.valueOf(7), "export-user", 3L, 6L, 1L)));
|
||||
when(sysAccountService.list(any(QueryWrapper.class))).thenReturn(List.of(buildSysAccount(7L, "export-user", "导出演示")));
|
||||
|
||||
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||
setField(service, "sysAccountService", sysAccountService);
|
||||
|
||||
DashboardUserRankQuery query = new DashboardUserRankQuery();
|
||||
query.setRange("7d");
|
||||
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
service.exportUserRanks(new LoginAccount(), query, outputStream);
|
||||
|
||||
try (org.apache.poi.ss.usermodel.Workbook workbook =
|
||||
WorkbookFactory.create(new ByteArrayInputStream(outputStream.toByteArray()))) {
|
||||
org.apache.poi.ss.usermodel.Sheet sheet = workbook.getSheetAt(0);
|
||||
Assert.assertEquals(sheet.getRow(0).getCell(0).getStringCellValue(), "登录账号");
|
||||
Assert.assertEquals(sheet.getRow(0).getCell(1).getStringCellValue(), "昵称");
|
||||
Assert.assertEquals(sheet.getRow(0).getCell(2).getStringCellValue(), "会话数");
|
||||
Assert.assertEquals(sheet.getRow(0).getCell(3).getStringCellValue(), "消息数");
|
||||
Assert.assertEquals(sheet.getRow(1).getCell(0).getStringCellValue(), "export-user");
|
||||
Assert.assertEquals(sheet.getRow(1).getCell(1).getStringCellValue(), "导出演示");
|
||||
Assert.assertEquals(sheet.getRow(1).getCell(2).getStringCellValue(), "3");
|
||||
Assert.assertEquals(sheet.getRow(1).getCell(3).getStringCellValue(), "6");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造查询上下文。
|
||||
*
|
||||
@@ -475,4 +607,20 @@ public class DashboardServiceImplTest {
|
||||
}
|
||||
throw new IllegalArgumentException("未找到字段: " + fieldName);
|
||||
}
|
||||
|
||||
private void setFieldSilently(Object target, String fieldName, Object value) {
|
||||
try {
|
||||
setField(target, fieldName, value);
|
||||
} catch (Exception ex) {
|
||||
throw new IllegalStateException(ex);
|
||||
}
|
||||
}
|
||||
|
||||
private SysAccount buildSysAccount(long id, String loginName, String nickname) {
|
||||
SysAccount account = new SysAccount();
|
||||
account.setId(BigInteger.valueOf(id));
|
||||
account.setLoginName(loginName);
|
||||
account.setNickname(nickname);
|
||||
return account;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user