feat: 增强管理员工作台聊天统计看板
- 工作台接入聊天消息总数、会话总数、活跃智能体数、趋势与 Top5 排行 - dashboard 接口新增 chatStatus 与聊天统计字段,分析库不可用时明确降级 - today 维度按小时聚合聊天趋势,并补充后端查询与测试覆盖
This commit is contained in:
@@ -0,0 +1,197 @@
|
||||
package tech.easyflow.admin.service.dashboard.impl;
|
||||
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.Test;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardDistributionItemVo;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardSummaryVo;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardTrendItemVo;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatAssistantUsageRank;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatDashboardSummary;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatDashboardTrend;
|
||||
import tech.easyflow.chatlog.service.ChatDashboardQueryService;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
import java.lang.reflect.Method;
|
||||
import java.math.BigInteger;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.List;
|
||||
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* {@link DashboardServiceImpl} 测试。
|
||||
*/
|
||||
public class DashboardServiceImplTest {
|
||||
|
||||
/**
|
||||
* 验证分析库不可用时返回明确不可用状态,且趋势与排行为空。
|
||||
*
|
||||
* @throws Exception 反射调用失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldReturnUnavailableChatPayloadWhenAnalyticalDbIsDisabled() throws Exception {
|
||||
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||
when(chatDashboardQueryService.available()).thenReturn(false);
|
||||
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||
|
||||
Object context = newContext("7d", null);
|
||||
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||
Object payload = invokeBuildChatPayload(service, context, summary);
|
||||
|
||||
Object chatStatus = readField(payload, "chatStatus");
|
||||
List<?> trends = (List<?>) readField(payload, "trends");
|
||||
List<?> distribution = (List<?>) readField(payload, "distribution");
|
||||
|
||||
Assert.assertFalse(Boolean.TRUE.equals(invokeGetter(chatStatus, "getAvailable")));
|
||||
Assert.assertEquals(invokeGetter(chatStatus, "getMessage"), "聊天数据不可用");
|
||||
Assert.assertTrue(trends.isEmpty());
|
||||
Assert.assertTrue(distribution.isEmpty());
|
||||
Assert.assertEquals(summary.getChatMessageTotal(), Long.valueOf(0L));
|
||||
Assert.assertEquals(summary.getChatSessionTotal(), Long.valueOf(0L));
|
||||
Assert.assertEquals(summary.getActiveAssistantTotal(), Long.valueOf(0L));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证 today 返回 24 个小时点位,且排行名称与均值回退正确。
|
||||
*
|
||||
* @throws Exception 反射调用失败
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldBuildHourlyTrendForToday() throws Exception {
|
||||
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||
String currentHourKey = LocalDateTime.of(LocalDate.now(), LocalTime.of(10, 0))
|
||||
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00:00"));
|
||||
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||
when(chatDashboardQueryService.querySummary(any(), any(), any()))
|
||||
.thenReturn(new ChatDashboardSummary(3L, 9L, 1L));
|
||||
when(chatDashboardQueryService.queryHourlyTrends(any(), any(), any()))
|
||||
.thenReturn(List.of(new ChatDashboardTrend(currentHourKey, 3L, 9L)));
|
||||
when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), any(Integer.class)))
|
||||
.thenReturn(List.of(new ChatAssistantUsageRank(BigInteger.ONE, "", 3L, 9L)));
|
||||
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||
|
||||
Object context = newContext("today", BigInteger.valueOf(9));
|
||||
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||
Object payload = invokeBuildChatPayload(service, context, summary);
|
||||
|
||||
Object chatStatus = readField(payload, "chatStatus");
|
||||
List<DashboardTrendItemVo> trends = (List<DashboardTrendItemVo>) readField(payload, "trends");
|
||||
List<DashboardDistributionItemVo> distribution = (List<DashboardDistributionItemVo>) readField(payload, "distribution");
|
||||
|
||||
Assert.assertTrue(Boolean.TRUE.equals(invokeGetter(chatStatus, "getAvailable")));
|
||||
Assert.assertEquals(trends.size(), 24);
|
||||
Assert.assertEquals(trends.get(0).getLabel(), "00:00");
|
||||
Assert.assertEquals(trends.get(10).getKey(), currentHourKey);
|
||||
Assert.assertEquals(trends.get(10).getLabel(), "10:00");
|
||||
Assert.assertEquals(trends.get(10).getChatMessageTotal(), Long.valueOf(9L));
|
||||
Assert.assertEquals(trends.get(10).getChatSessionTotal(), Long.valueOf(3L));
|
||||
Assert.assertEquals(trends.get(11).getChatMessageTotal(), Long.valueOf(0L));
|
||||
Assert.assertEquals(trends.get(11).getChatSessionTotal(), Long.valueOf(0L));
|
||||
Assert.assertEquals(summary.getChatMessageTotal(), Long.valueOf(9L));
|
||||
Assert.assertEquals(summary.getChatSessionTotal(), Long.valueOf(3L));
|
||||
Assert.assertEquals(summary.getActiveAssistantTotal(), Long.valueOf(1L));
|
||||
Assert.assertEquals(distribution.get(0).getLabel(), "智能体-1");
|
||||
Assert.assertEquals(distribution.get(0).getAvgMessagePerSession(), Double.valueOf(3D));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造查询上下文。
|
||||
*
|
||||
* @param range 时间范围
|
||||
* @param tenantId 租户 ID
|
||||
* @return 查询上下文实例
|
||||
* @throws Exception 反射失败
|
||||
*/
|
||||
private Object newContext(String range, BigInteger tenantId) throws Exception {
|
||||
Class<?> contextClass = Class.forName(
|
||||
"tech.easyflow.admin.service.dashboard.impl.DashboardServiceImpl$DashboardQueryContext"
|
||||
);
|
||||
Constructor<?> constructor = contextClass.getDeclaredConstructor();
|
||||
constructor.setAccessible(true);
|
||||
Object context = constructor.newInstance();
|
||||
setField(context, "range", range);
|
||||
setField(context, "tenantFilterId", tenantId);
|
||||
setField(context, "startTime", LocalDateTime.of(LocalDate.now(), java.time.LocalTime.MIN));
|
||||
setField(context, "endTime", LocalDateTime.of(LocalDate.now().plusDays(1), java.time.LocalTime.MIN));
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用私有聊天载荷组装方法。
|
||||
*
|
||||
* @param service service
|
||||
* @param context 上下文
|
||||
* @param summary 汇总对象
|
||||
* @return 载荷
|
||||
* @throws Exception 反射失败
|
||||
*/
|
||||
private Object invokeBuildChatPayload(DashboardServiceImpl service, Object context, DashboardSummaryVo summary)
|
||||
throws Exception {
|
||||
Method method = DashboardServiceImpl.class.getDeclaredMethod(
|
||||
"buildChatPayload",
|
||||
context.getClass(),
|
||||
DashboardSummaryVo.class
|
||||
);
|
||||
method.setAccessible(true);
|
||||
return method.invoke(service, context, summary);
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取对象字段。
|
||||
*
|
||||
* @param target 目标对象
|
||||
* @param fieldName 字段名
|
||||
* @return 字段值
|
||||
* @throws Exception 反射失败
|
||||
*/
|
||||
private Object readField(Object target, String fieldName) throws Exception {
|
||||
Field field = target.getClass().getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
return field.get(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用 getter。
|
||||
*
|
||||
* @param target 目标对象
|
||||
* @param methodName 方法名
|
||||
* @return 返回值
|
||||
* @throws Exception 反射失败
|
||||
*/
|
||||
private Object invokeGetter(Object target, String methodName) throws Exception {
|
||||
Method method = target.getClass().getMethod(methodName);
|
||||
return method.invoke(target);
|
||||
}
|
||||
|
||||
/**
|
||||
* 通过反射设置字段值。
|
||||
*
|
||||
* @param target 目标对象
|
||||
* @param fieldName 字段名
|
||||
* @param value 字段值
|
||||
* @throws Exception 反射失败
|
||||
*/
|
||||
private void setField(Object target, String fieldName, Object value) throws Exception {
|
||||
Class<?> current = target.getClass();
|
||||
while (current != null) {
|
||||
try {
|
||||
Field field = current.getDeclaredField(fieldName);
|
||||
field.setAccessible(true);
|
||||
field.set(target, value);
|
||||
return;
|
||||
} catch (NoSuchFieldException ignored) {
|
||||
current = current.getSuperclass();
|
||||
}
|
||||
}
|
||||
throw new IllegalArgumentException("未找到字段: " + fieldName);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user