feat: 增强工作台趋势概览与聊天排行

- 支持用户活跃与智能体活跃趋势统计及自定义时间范围

- 增加用户活跃榜与智能体趋势数据结构及查询实现

- 同步补齐工作台页面展示与定向测试
This commit is contained in:
2026-05-06 19:22:09 +08:00
parent 5827ecde42
commit 31b0e21d3d
20 changed files with 2087 additions and 146 deletions

View File

@@ -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;
}
}
}