feat: 增强管理员工作台聊天统计看板
- 工作台接入聊天消息总数、会话总数、活跃智能体数、趋势与 Top5 排行 - dashboard 接口新增 chatStatus 与聊天统计字段,分析库不可用时明确降级 - today 维度按小时聚合聊天趋势,并补充后端查询与测试覆盖
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
package tech.easyflow.chatlog.domain.dto;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 聊天助手使用排行项。
|
||||
*
|
||||
* @param assistantId 智能体 ID
|
||||
* @param assistantName 智能体名称
|
||||
* @param sessionTotal 会话总数
|
||||
* @param messageTotal 消息总数
|
||||
*/
|
||||
public record ChatAssistantUsageRank(BigInteger assistantId,
|
||||
String assistantName,
|
||||
long sessionTotal,
|
||||
long messageTotal) {
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package tech.easyflow.chatlog.domain.dto;
|
||||
|
||||
/**
|
||||
* 聊天工作台汇总统计。
|
||||
*
|
||||
* @param sessionTotal 会话总数
|
||||
* @param messageTotal 消息总数
|
||||
* @param activeAssistantTotal 活跃智能体数
|
||||
*/
|
||||
public record ChatDashboardSummary(long sessionTotal, long messageTotal, long activeAssistantTotal) {
|
||||
|
||||
/**
|
||||
* 创建空汇总结果。
|
||||
*
|
||||
* @return 空汇总结果
|
||||
*/
|
||||
public static ChatDashboardSummary empty() {
|
||||
return new ChatDashboardSummary(0L, 0L, 0L);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package tech.easyflow.chatlog.domain.dto;
|
||||
|
||||
/**
|
||||
* 聊天工作台趋势项。
|
||||
*
|
||||
* @param bucketKey 趋势桶标识,日趋势为 yyyy-MM-dd,小时趋势为 yyyy-MM-dd HH:00:00
|
||||
* @param sessionTotal 会话总数
|
||||
* @param messageTotal 消息总数
|
||||
*/
|
||||
public record ChatDashboardTrend(String bucketKey, long sessionTotal, long messageTotal) {
|
||||
}
|
||||
@@ -3,6 +3,10 @@ package tech.easyflow.chatlog.repository.analyticaldb;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.jdbc.core.ParameterizedPreparedStatementSetter;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.util.StringUtils;
|
||||
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.domain.dto.ChatHistoryPage;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatMessageRecord;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatSessionPage;
|
||||
@@ -19,6 +23,7 @@ import java.math.BigInteger;
|
||||
import java.sql.Timestamp;
|
||||
import java.time.Instant;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.Collections;
|
||||
@@ -204,6 +209,160 @@ public class ChatAnalyticalDBRepository {
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天工作台汇总统计。
|
||||
*
|
||||
* @param startDate 开始日期,包含
|
||||
* @param endDate 结束日期,不包含
|
||||
* @param tenantId 租户 ID,空表示全局
|
||||
* @return 聊天汇总统计
|
||||
*/
|
||||
public ChatDashboardSummary queryDashboardSummary(LocalDate startDate, LocalDate endDate, BigInteger tenantId) {
|
||||
assertAvailable();
|
||||
List<Object> args = new java.util.ArrayList<>();
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT ")
|
||||
.append("ifNull(sum(message_count), 0) AS message_total, ")
|
||||
.append("ifNull(sum(session_count), 0) AS session_total, ")
|
||||
.append("uniqExact(dimension_id) AS active_assistant_total ")
|
||||
.append("FROM dws_chat_assistant_day ")
|
||||
.append("WHERE stat_date >= toDate(?) AND stat_date < toDate(?)");
|
||||
args.add(startDate.toString());
|
||||
args.add(endDate.toString());
|
||||
appendOptionalTenantFilter(sql, args, tenantId, "tenant_id");
|
||||
|
||||
ChatDashboardSummary summary = analyticalDBOperations.queryOne(
|
||||
sql.toString(),
|
||||
(rs, rowNum) -> new ChatDashboardSummary(
|
||||
rs.getLong("session_total"),
|
||||
rs.getLong("message_total"),
|
||||
rs.getLong("active_assistant_total")
|
||||
),
|
||||
args.toArray()
|
||||
);
|
||||
return summary == null ? ChatDashboardSummary.empty() : summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天工作台日趋势。
|
||||
*
|
||||
* @param startDate 开始日期,包含
|
||||
* @param endDate 结束日期,不包含
|
||||
* @param tenantId 租户 ID,空表示全局
|
||||
* @return 日趋势列表
|
||||
*/
|
||||
public List<ChatDashboardTrend> queryDashboardTrends(LocalDate startDate, LocalDate endDate, BigInteger tenantId) {
|
||||
assertAvailable();
|
||||
List<Object> args = new java.util.ArrayList<>();
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT toString(stat_date) AS bucket_key, ")
|
||||
.append("ifNull(sum(message_count), 0) AS message_total, ")
|
||||
.append("ifNull(sum(session_count), 0) AS session_total ")
|
||||
.append("FROM dws_chat_assistant_day ")
|
||||
.append("WHERE stat_date >= toDate(?) AND stat_date < toDate(?)");
|
||||
args.add(startDate.toString());
|
||||
args.add(endDate.toString());
|
||||
appendOptionalTenantFilter(sql, args, tenantId, "tenant_id");
|
||||
sql.append(" GROUP BY stat_date ORDER BY stat_date ASC");
|
||||
|
||||
return analyticalDBOperations.query(
|
||||
sql.toString(),
|
||||
(rs, rowNum) -> new ChatDashboardTrend(
|
||||
rs.getString("bucket_key"),
|
||||
rs.getLong("session_total"),
|
||||
rs.getLong("message_total")
|
||||
),
|
||||
args.toArray()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天工作台小时趋势。
|
||||
*
|
||||
* @param startTime 开始时间,包含
|
||||
* @param endTime 结束时间,不包含
|
||||
* @param tenantId 租户 ID,空表示全局
|
||||
* @return 小时趋势列表
|
||||
*/
|
||||
public List<ChatDashboardTrend> queryDashboardHourlyTrends(LocalDateTime startTime,
|
||||
LocalDateTime endTime,
|
||||
BigInteger tenantId) {
|
||||
assertAvailable();
|
||||
List<Object> args = new java.util.ArrayList<>();
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT formatDateTime(toStartOfHour(l.created), '%Y-%m-%d %H:00:00') AS bucket_key, ")
|
||||
.append("count() AS message_total, ")
|
||||
.append("uniqExact(l.session_id) AS session_total ")
|
||||
.append("FROM ods_chat_log l ")
|
||||
.append("LEFT JOIN (SELECT id, tenant_id FROM ods_chat_session FINAL) s ON s.id = l.session_id ")
|
||||
.append("WHERE l.created >= toDateTime(?) AND l.created < toDateTime(?)");
|
||||
args.add(CH_DATE_TIME_FORMATTER.format(startTime));
|
||||
args.add(CH_DATE_TIME_FORMATTER.format(endTime));
|
||||
appendOptionalTenantFilter(sql, args, tenantId, "s.tenant_id");
|
||||
sql.append(" GROUP BY bucket_key ORDER BY bucket_key ASC");
|
||||
|
||||
return analyticalDBOperations.query(
|
||||
sql.toString(),
|
||||
(rs, rowNum) -> new ChatDashboardTrend(
|
||||
rs.getString("bucket_key"),
|
||||
rs.getLong("session_total"),
|
||||
rs.getLong("message_total")
|
||||
),
|
||||
args.toArray()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天助手使用排行。
|
||||
*
|
||||
* @param startDate 开始日期,包含
|
||||
* @param endDate 结束日期,不包含
|
||||
* @param tenantId 租户 ID,空表示全局
|
||||
* @param limit 返回条数
|
||||
* @return 排行列表
|
||||
*/
|
||||
public List<ChatAssistantUsageRank> queryAssistantUsageRanks(LocalDate startDate,
|
||||
LocalDate endDate,
|
||||
BigInteger tenantId,
|
||||
int limit) {
|
||||
assertAvailable();
|
||||
int safeLimit = Math.max(limit, 1);
|
||||
List<Object> args = new java.util.ArrayList<>();
|
||||
StringBuilder sql = new StringBuilder();
|
||||
sql.append("SELECT agg.assistant_id, snapshot.assistant_name, agg.session_total, agg.message_total ")
|
||||
.append("FROM (")
|
||||
.append("SELECT dimension_id AS assistant_id, ")
|
||||
.append("ifNull(sum(session_count), 0) AS session_total, ")
|
||||
.append("ifNull(sum(message_count), 0) AS message_total ")
|
||||
.append("FROM dws_chat_assistant_day ")
|
||||
.append("WHERE stat_date >= toDate(?) AND stat_date < toDate(?)");
|
||||
args.add(startDate.toString());
|
||||
args.add(endDate.toString());
|
||||
appendOptionalTenantFilter(sql, args, tenantId, "tenant_id");
|
||||
sql.append(" GROUP BY dimension_id")
|
||||
.append(") agg ")
|
||||
.append("LEFT JOIN (")
|
||||
.append("SELECT assistant_id, argMax(assistant_name, modified) AS assistant_name ")
|
||||
.append("FROM ods_chat_session FINAL WHERE is_deleted = 0 ");
|
||||
appendOptionalTenantFilter(sql, args, tenantId, "tenant_id");
|
||||
sql.append(" GROUP BY assistant_id")
|
||||
.append(") snapshot ON snapshot.assistant_id = agg.assistant_id ")
|
||||
.append("ORDER BY agg.message_total DESC, agg.session_total DESC, agg.assistant_id ASC ")
|
||||
.append("LIMIT ?");
|
||||
args.add(safeLimit);
|
||||
|
||||
return analyticalDBOperations.query(
|
||||
sql.toString(),
|
||||
(rs, rowNum) -> new ChatAssistantUsageRank(
|
||||
bigInteger(rs.getObject("assistant_id")),
|
||||
rs.getString("assistant_name"),
|
||||
rs.getLong("session_total"),
|
||||
rs.getLong("message_total")
|
||||
),
|
||||
args.toArray()
|
||||
);
|
||||
}
|
||||
|
||||
public void refreshDws(Set<LocalDate> dates) {
|
||||
if (!enabled() || dates.isEmpty()) {
|
||||
return;
|
||||
@@ -212,8 +371,15 @@ public class ChatAnalyticalDBRepository {
|
||||
String dateLiteral = date.toString();
|
||||
analyticalDBOperations.update("ALTER TABLE dws_chat_assistant_day DELETE WHERE stat_date = toDate(?)", dateLiteral);
|
||||
analyticalDBOperations.update("INSERT INTO dws_chat_assistant_day " +
|
||||
"SELECT toDate(created) AS stat_date, assistant_id AS dimension_id, uniqExact(session_id) AS session_count, count() AS message_count " +
|
||||
"FROM ods_chat_log WHERE toDate(created) = toDate(?) GROUP BY stat_date, dimension_id", dateLiteral);
|
||||
"SELECT toDate(l.created) AS stat_date, " +
|
||||
"l.assistant_id AS dimension_id, " +
|
||||
"ifNull(s.tenant_id, toUInt64(0)) AS tenant_id, " +
|
||||
"uniqExact(l.session_id) AS session_count, " +
|
||||
"count() AS message_count " +
|
||||
"FROM ods_chat_log l " +
|
||||
"LEFT JOIN (SELECT id, tenant_id FROM ods_chat_session FINAL) s ON s.id = l.session_id " +
|
||||
"WHERE toDate(l.created) = toDate(?) " +
|
||||
"GROUP BY stat_date, dimension_id, tenant_id", dateLiteral);
|
||||
|
||||
analyticalDBOperations.update("ALTER TABLE dws_chat_user_day DELETE WHERE stat_date = toDate(?)", dateLiteral);
|
||||
analyticalDBOperations.update("INSERT INTO dws_chat_user_day " +
|
||||
@@ -313,6 +479,22 @@ public class ChatAnalyticalDBRepository {
|
||||
return new BigInteger(String.valueOf(value));
|
||||
}
|
||||
|
||||
/**
|
||||
* 追加可选租户过滤,超管场景可不传租户。
|
||||
*
|
||||
* @param sql SQL 构造器
|
||||
* @param args 参数列表
|
||||
* @param tenantId 租户 ID
|
||||
* @param columnName 列名
|
||||
*/
|
||||
private void appendOptionalTenantFilter(StringBuilder sql, List<Object> args, BigInteger tenantId, String columnName) {
|
||||
if (tenantId == null || !StringUtils.hasText(columnName)) {
|
||||
return;
|
||||
}
|
||||
sql.append(" AND ").append(columnName).append(" = ?");
|
||||
args.add(tenantId);
|
||||
}
|
||||
|
||||
private void appendSessionFilters(ChatSessionFilterQuery query, StringBuilder sql, List<Object> args) {
|
||||
if (query == null) {
|
||||
return;
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
package tech.easyflow.chatlog.service;
|
||||
|
||||
import tech.easyflow.chatlog.domain.dto.ChatAssistantUsageRank;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatDashboardSummary;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatDashboardTrend;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 聊天工作台统计查询服务。
|
||||
*/
|
||||
public interface ChatDashboardQueryService {
|
||||
|
||||
/**
|
||||
* 查询聊天汇总指标。
|
||||
*
|
||||
* @param startDate 开始日期,包含当天
|
||||
* @param endDate 结束日期,不包含当天
|
||||
* @param tenantId 租户 ID,空表示全局
|
||||
* @return 聊天汇总指标
|
||||
*/
|
||||
ChatDashboardSummary querySummary(LocalDate startDate, LocalDate endDate, BigInteger tenantId);
|
||||
|
||||
/**
|
||||
* 查询聊天日趋势。
|
||||
*
|
||||
* @param startDate 开始日期,包含当天
|
||||
* @param endDate 结束日期,不包含当天
|
||||
* @param tenantId 租户 ID,空表示全局
|
||||
* @return 聊天日趋势
|
||||
*/
|
||||
List<ChatDashboardTrend> queryTrends(LocalDate startDate, LocalDate endDate, BigInteger tenantId);
|
||||
|
||||
/**
|
||||
* 查询聊天小时趋势。
|
||||
*
|
||||
* @param startTime 开始时间,包含
|
||||
* @param endTime 结束时间,不包含
|
||||
* @param tenantId 租户 ID,空表示全局
|
||||
* @return 聊天小时趋势
|
||||
*/
|
||||
List<ChatDashboardTrend> queryHourlyTrends(LocalDateTime startTime, LocalDateTime endTime, BigInteger tenantId);
|
||||
|
||||
/**
|
||||
* 查询智能体使用排行。
|
||||
*
|
||||
* @param startDate 开始日期,包含当天
|
||||
* @param endDate 结束日期,不包含当天
|
||||
* @param tenantId 租户 ID,空表示全局
|
||||
* @param limit 返回条数
|
||||
* @return 智能体使用排行
|
||||
*/
|
||||
List<ChatAssistantUsageRank> queryAssistantUsageRanks(LocalDate startDate,
|
||||
LocalDate endDate,
|
||||
BigInteger tenantId,
|
||||
int limit);
|
||||
|
||||
/**
|
||||
* 当前分析库是否可用。
|
||||
*
|
||||
* @return true 表示可用
|
||||
*/
|
||||
boolean available();
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package tech.easyflow.chatlog.service.impl;
|
||||
|
||||
import org.springframework.stereotype.Service;
|
||||
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.repository.analyticaldb.ChatAnalyticalDBRepository;
|
||||
import tech.easyflow.chatlog.service.ChatDashboardQueryService;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 基于分析库的聊天工作台统计查询实现。
|
||||
*/
|
||||
@Service
|
||||
public class ChatDashboardQueryServiceImpl implements ChatDashboardQueryService {
|
||||
|
||||
private final ChatAnalyticalDBRepository analyticalDBRepository;
|
||||
|
||||
public ChatDashboardQueryServiceImpl(ChatAnalyticalDBRepository analyticalDBRepository) {
|
||||
this.analyticalDBRepository = analyticalDBRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天汇总指标。
|
||||
*
|
||||
* @param startDate 开始日期,包含当天
|
||||
* @param endDate 结束日期,不包含当天
|
||||
* @param tenantId 租户 ID,空表示全局
|
||||
* @return 聊天汇总指标
|
||||
*/
|
||||
@Override
|
||||
public ChatDashboardSummary querySummary(LocalDate startDate, LocalDate endDate, BigInteger tenantId) {
|
||||
if (!available()) {
|
||||
return ChatDashboardSummary.empty();
|
||||
}
|
||||
return analyticalDBRepository.queryDashboardSummary(startDate, endDate, tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天日趋势。
|
||||
*
|
||||
* @param startDate 开始日期,包含当天
|
||||
* @param endDate 结束日期,不包含当天
|
||||
* @param tenantId 租户 ID,空表示全局
|
||||
* @return 聊天日趋势
|
||||
*/
|
||||
@Override
|
||||
public List<ChatDashboardTrend> queryTrends(LocalDate startDate, LocalDate endDate, BigInteger tenantId) {
|
||||
if (!available()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return analyticalDBRepository.queryDashboardTrends(startDate, endDate, tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询聊天小时趋势。
|
||||
*
|
||||
* @param startTime 开始时间,包含
|
||||
* @param endTime 结束时间,不包含
|
||||
* @param tenantId 租户 ID,空表示全局
|
||||
* @return 聊天小时趋势
|
||||
*/
|
||||
@Override
|
||||
public List<ChatDashboardTrend> queryHourlyTrends(LocalDateTime startTime, LocalDateTime endTime, BigInteger tenantId) {
|
||||
if (!available()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return analyticalDBRepository.queryDashboardHourlyTrends(startTime, endTime, tenantId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 查询智能体使用排行。
|
||||
*
|
||||
* @param startDate 开始日期,包含当天
|
||||
* @param endDate 结束日期,不包含当天
|
||||
* @param tenantId 租户 ID,空表示全局
|
||||
* @param limit 返回条数
|
||||
* @return 智能体使用排行
|
||||
*/
|
||||
@Override
|
||||
public List<ChatAssistantUsageRank> queryAssistantUsageRanks(LocalDate startDate,
|
||||
LocalDate endDate,
|
||||
BigInteger tenantId,
|
||||
int limit) {
|
||||
if (!available()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
return analyticalDBRepository.queryAssistantUsageRanks(startDate, endDate, tenantId, limit);
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前分析库是否可用。
|
||||
*
|
||||
* @return true 表示可用
|
||||
*/
|
||||
@Override
|
||||
public boolean available() {
|
||||
return analyticalDBRepository.enabled();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user