feat: 增强工作台趋势概览与聊天排行
- 支持用户活跃与智能体活跃趋势统计及自定义时间范围 - 增加用户活跃榜与智能体趋势数据结构及查询实现 - 同步补齐工作台页面展示与定向测试
This commit is contained in:
@@ -0,0 +1,188 @@
|
||||
package tech.easyflow.chatlog.repository.analyticaldb;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Test;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.support.StaticListableBeanFactory;
|
||||
import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter;
|
||||
import org.springframework.jdbc.core.RowMapper;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatDashboardSummary;
|
||||
import tech.easyflow.chatlog.support.ChatJsonSupport;
|
||||
import tech.easyflow.common.analyticaldb.config.AnalyticalDBFlywayProperties;
|
||||
import tech.easyflow.common.analyticaldb.core.AnalyticalDBOperations;
|
||||
import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageRequest;
|
||||
import tech.easyflow.common.analyticaldb.page.AnalyticalDBPageResult;
|
||||
import tech.easyflow.common.analyticaldb.support.AnalyticalDBHealthSupport;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.time.LocalDate;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* {@link ChatAnalyticalDBRepository} 测试。
|
||||
*/
|
||||
public class ChatAnalyticalDBRepositoryTest {
|
||||
|
||||
/**
|
||||
* 验证工作台汇总使用跨天去重的 session 口径。
|
||||
*/
|
||||
@Test
|
||||
public void shouldUseDistinctSessionSqlForDashboardSummary() {
|
||||
RecordingAnalyticalDBOperations operations = new RecordingAnalyticalDBOperations();
|
||||
operations.queryOneResult = new ChatDashboardSummary(2L, 5L, 1L, 1L);
|
||||
|
||||
ChatAnalyticalDBRepository repository = newRepository(operations);
|
||||
repository.queryDashboardSummary(LocalDate.of(2026, 4, 1), LocalDate.of(2026, 4, 8), BigInteger.ONE);
|
||||
|
||||
Assert.assertNotNull(operations.lastQueryOneSql);
|
||||
Assert.assertTrue(operations.lastQueryOneSql.contains("FROM dws_chat_session_day agg"));
|
||||
Assert.assertTrue(operations.lastQueryOneSql.contains("uniqExact(agg.dimension_id) AS session_total"));
|
||||
Assert.assertTrue(operations.lastQueryOneSql.contains("uniqExact(agg.user_id) AS active_user_total"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证智能体使用榜按去重会话数排序,并同时统计用户数。
|
||||
*/
|
||||
@Test
|
||||
public void shouldUseDistinctSessionSqlForAssistantUsageRanks() {
|
||||
RecordingAnalyticalDBOperations operations = new RecordingAnalyticalDBOperations();
|
||||
|
||||
ChatAnalyticalDBRepository repository = newRepository(operations);
|
||||
repository.queryAssistantUsageRanks(LocalDate.of(2026, 4, 1), LocalDate.of(2026, 4, 8), BigInteger.ONE, 5);
|
||||
|
||||
Assert.assertNotNull(operations.lastQuerySql);
|
||||
Assert.assertTrue(operations.lastQuerySql.contains("FROM dws_chat_session_day agg"));
|
||||
Assert.assertTrue(operations.lastQuerySql.contains("uniqExact(agg.user_id) AS user_total"));
|
||||
Assert.assertTrue(operations.lastQuerySql.contains("uniqExact(agg.dimension_id) AS session_total"));
|
||||
Assert.assertTrue(operations.lastQuerySql.contains(
|
||||
"ORDER BY agg.session_total DESC, agg.user_total DESC, agg.message_total DESC, agg.assistant_id ASC"
|
||||
));
|
||||
Assert.assertTrue(operations.lastQuerySql.contains("agg.assistant_id AS assistant_id"));
|
||||
Assert.assertTrue(operations.lastQuerySql.contains("snapshot.assistant_name AS assistant_name"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证智能体趋势查询显式返回 assistant_id 别名,避免 ClickHouse JDBC 无法按列名映射。
|
||||
*/
|
||||
@Test
|
||||
public void shouldAliasAssistantIdForAssistantTrendQueries() {
|
||||
RecordingAnalyticalDBOperations operations = new RecordingAnalyticalDBOperations();
|
||||
|
||||
ChatAnalyticalDBRepository repository = newRepository(operations);
|
||||
repository.queryAssistantSessionTrends(
|
||||
LocalDate.of(2026, 4, 1),
|
||||
LocalDate.of(2026, 4, 8),
|
||||
BigInteger.ONE,
|
||||
List.of(BigInteger.ONE, BigInteger.TWO)
|
||||
);
|
||||
|
||||
Assert.assertNotNull(operations.lastQuerySql);
|
||||
Assert.assertTrue(operations.lastQuerySql.contains("agg.assistant_id AS assistant_id"));
|
||||
Assert.assertTrue(operations.lastQuerySql.contains("snapshot.assistant_name AS assistant_name"));
|
||||
Assert.assertTrue(operations.lastQuerySql.contains("agg.assistant_id IN (?, ?)"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证智能体趋势查询在 Top 列表包含空 assistant_id 时会补上 IS NULL 条件。
|
||||
*/
|
||||
@Test
|
||||
public void shouldSupportNullAssistantIdInAssistantTrendQueries() {
|
||||
RecordingAnalyticalDBOperations operations = new RecordingAnalyticalDBOperations();
|
||||
|
||||
ChatAnalyticalDBRepository repository = newRepository(operations);
|
||||
repository.queryAssistantSessionTrends(
|
||||
LocalDate.of(2026, 4, 1),
|
||||
LocalDate.of(2026, 4, 8),
|
||||
BigInteger.ONE,
|
||||
Arrays.asList(BigInteger.ONE, null)
|
||||
);
|
||||
|
||||
Assert.assertNotNull(operations.lastQuerySql);
|
||||
Assert.assertTrue(operations.lastQuerySql.contains("(agg.assistant_id IN (?) OR agg.assistant_id IS NULL)"));
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造仓储实例。
|
||||
*
|
||||
* @param operations 分析库操作桩
|
||||
* @return 仓储实例
|
||||
*/
|
||||
private ChatAnalyticalDBRepository newRepository(RecordingAnalyticalDBOperations operations) {
|
||||
StaticListableBeanFactory beanFactory = new StaticListableBeanFactory();
|
||||
beanFactory.addBean("analyticalDBOperations", operations);
|
||||
ObjectProvider<AnalyticalDBOperations> provider = beanFactory.getBeanProvider(AnalyticalDBOperations.class);
|
||||
AnalyticalDBHealthSupport healthSupport =
|
||||
new AnalyticalDBHealthSupport(provider, new AnalyticalDBFlywayProperties());
|
||||
ChatJsonSupport jsonSupport = new ChatJsonSupport(new ObjectMapper());
|
||||
return new ChatAnalyticalDBRepository(provider, healthSupport, jsonSupport);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录 SQL 的分析库桩实现。
|
||||
*/
|
||||
private static class RecordingAnalyticalDBOperations implements AnalyticalDBOperations {
|
||||
|
||||
private String lastQueryOneSql;
|
||||
private String lastQuerySql;
|
||||
private ChatDashboardSummary queryOneResult;
|
||||
|
||||
@Override
|
||||
public boolean available() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void assertAvailable() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> query(String sql, RowMapper<T> rowMapper, Object... args) {
|
||||
this.lastQuerySql = sql;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T queryOne(String sql, Class<T> requiredType, Object... args) {
|
||||
this.lastQueryOneSql = sql;
|
||||
return null;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T> T queryOne(String sql, RowMapper<T> rowMapper, Object... args) {
|
||||
this.lastQueryOneSql = sql;
|
||||
return (T) queryOneResult;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> List<T> queryForList(String sql, Class<T> elementType, Object... args) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int update(String sql, Object... args) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> int[][] batchUpdate(String sql,
|
||||
List<T> items,
|
||||
int batchSize,
|
||||
ParameterizedPreparedStatementSetter<T> setter) {
|
||||
return new int[0][];
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> AnalyticalDBPageResult<T> page(String countSql,
|
||||
Object[] countArgs,
|
||||
String dataSql,
|
||||
Object[] dataArgs,
|
||||
AnalyticalDBPageRequest pageRequest,
|
||||
RowMapper<T> rowMapper) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user