feat: 增强工作台趋势概览与聊天排行
- 支持用户活跃与智能体活跃趋势统计及自定义时间范围 - 增加用户活跃榜与智能体趋势数据结构及查询实现 - 同步补齐工作台页面展示与定向测试
This commit is contained in:
@@ -0,0 +1,37 @@
|
||||
package tech.easyflow.admin.model.dashboard;
|
||||
|
||||
/**
|
||||
* 工作台智能体趋势点位。
|
||||
*/
|
||||
public class DashboardAssistantTrendPointVo {
|
||||
|
||||
private String key;
|
||||
|
||||
private String label;
|
||||
|
||||
private Long sessionTotal;
|
||||
|
||||
public String getKey() {
|
||||
return key;
|
||||
}
|
||||
|
||||
public void setKey(String key) {
|
||||
this.key = key;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public Long getSessionTotal() {
|
||||
return sessionTotal;
|
||||
}
|
||||
|
||||
public void setSessionTotal(Long sessionTotal) {
|
||||
this.sessionTotal = sessionTotal;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package tech.easyflow.admin.model.dashboard;
|
||||
|
||||
import java.math.BigInteger;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 工作台智能体趋势序列。
|
||||
*/
|
||||
public class DashboardAssistantTrendSeriesVo {
|
||||
|
||||
private BigInteger assistantId;
|
||||
|
||||
private String label;
|
||||
|
||||
private Long totalSessionCount;
|
||||
|
||||
private List<DashboardAssistantTrendPointVo> points;
|
||||
|
||||
public BigInteger getAssistantId() {
|
||||
return assistantId;
|
||||
}
|
||||
|
||||
public void setAssistantId(BigInteger assistantId) {
|
||||
this.assistantId = assistantId;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public Long getTotalSessionCount() {
|
||||
return totalSessionCount;
|
||||
}
|
||||
|
||||
public void setTotalSessionCount(Long totalSessionCount) {
|
||||
this.totalSessionCount = totalSessionCount;
|
||||
}
|
||||
|
||||
public List<DashboardAssistantTrendPointVo> getPoints() {
|
||||
return points;
|
||||
}
|
||||
|
||||
public void setPoints(List<DashboardAssistantTrendPointVo> points) {
|
||||
this.points = points;
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ public class DashboardDistributionItemVo {
|
||||
|
||||
private Long sessionTotal;
|
||||
|
||||
private Double avgSessionPerUser;
|
||||
|
||||
private Double avgMessagePerSession;
|
||||
|
||||
public String getKey() {
|
||||
@@ -119,6 +121,14 @@ public class DashboardDistributionItemVo {
|
||||
this.sessionTotal = sessionTotal;
|
||||
}
|
||||
|
||||
public Double getAvgSessionPerUser() {
|
||||
return avgSessionPerUser;
|
||||
}
|
||||
|
||||
public void setAvgSessionPerUser(Double avgSessionPerUser) {
|
||||
this.avgSessionPerUser = avgSessionPerUser;
|
||||
}
|
||||
|
||||
public Double getAvgMessagePerSession() {
|
||||
return avgMessagePerSession;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ package tech.easyflow.admin.model.dashboard;
|
||||
public class DashboardOverviewQuery {
|
||||
|
||||
private String range;
|
||||
private String startDate;
|
||||
private String endDate;
|
||||
|
||||
public String getRange() {
|
||||
return range;
|
||||
@@ -14,4 +16,20 @@ public class DashboardOverviewQuery {
|
||||
public void setRange(String range) {
|
||||
this.range = range;
|
||||
}
|
||||
|
||||
public String getStartDate() {
|
||||
return startDate;
|
||||
}
|
||||
|
||||
public void setStartDate(String startDate) {
|
||||
this.startDate = startDate;
|
||||
}
|
||||
|
||||
public String getEndDate() {
|
||||
return endDate;
|
||||
}
|
||||
|
||||
public void setEndDate(String endDate) {
|
||||
this.endDate = endDate;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,8 +14,12 @@ public class DashboardOverviewVo {
|
||||
|
||||
private List<DashboardTrendItemVo> trends;
|
||||
|
||||
private List<DashboardAssistantTrendSeriesVo> assistantTrends;
|
||||
|
||||
private List<DashboardDistributionItemVo> distribution;
|
||||
|
||||
private List<DashboardUserRankItemVo> userRanks;
|
||||
|
||||
private DashboardOverviewQuery query;
|
||||
|
||||
private Date updatedAt;
|
||||
@@ -52,6 +56,22 @@ public class DashboardOverviewVo {
|
||||
this.distribution = distribution;
|
||||
}
|
||||
|
||||
public List<DashboardAssistantTrendSeriesVo> getAssistantTrends() {
|
||||
return assistantTrends;
|
||||
}
|
||||
|
||||
public void setAssistantTrends(List<DashboardAssistantTrendSeriesVo> assistantTrends) {
|
||||
this.assistantTrends = assistantTrends;
|
||||
}
|
||||
|
||||
public List<DashboardUserRankItemVo> getUserRanks() {
|
||||
return userRanks;
|
||||
}
|
||||
|
||||
public void setUserRanks(List<DashboardUserRankItemVo> userRanks) {
|
||||
this.userRanks = userRanks;
|
||||
}
|
||||
|
||||
public DashboardOverviewQuery getQuery() {
|
||||
return query;
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ public class DashboardSummaryVo {
|
||||
|
||||
private Long activeAssistantTotal;
|
||||
|
||||
private Long chatActiveUserTotal;
|
||||
|
||||
public Long getUserTotal() {
|
||||
return userTotal;
|
||||
}
|
||||
@@ -84,4 +86,12 @@ public class DashboardSummaryVo {
|
||||
public void setActiveAssistantTotal(Long activeAssistantTotal) {
|
||||
this.activeAssistantTotal = activeAssistantTotal;
|
||||
}
|
||||
|
||||
public Long getChatActiveUserTotal() {
|
||||
return chatActiveUserTotal;
|
||||
}
|
||||
|
||||
public void setChatActiveUserTotal(Long chatActiveUserTotal) {
|
||||
this.chatActiveUserTotal = chatActiveUserTotal;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package tech.easyflow.admin.model.dashboard;
|
||||
|
||||
import java.math.BigInteger;
|
||||
|
||||
/**
|
||||
* 工作台用户活跃排行项。
|
||||
*/
|
||||
public class DashboardUserRankItemVo {
|
||||
|
||||
private BigInteger userId;
|
||||
|
||||
private String label;
|
||||
|
||||
private Long sessionTotal;
|
||||
|
||||
private Long messageTotal;
|
||||
|
||||
private Long assistantTotal;
|
||||
|
||||
public BigInteger getUserId() {
|
||||
return userId;
|
||||
}
|
||||
|
||||
public void setUserId(BigInteger userId) {
|
||||
this.userId = userId;
|
||||
}
|
||||
|
||||
public String getLabel() {
|
||||
return label;
|
||||
}
|
||||
|
||||
public void setLabel(String label) {
|
||||
this.label = label;
|
||||
}
|
||||
|
||||
public Long getSessionTotal() {
|
||||
return sessionTotal;
|
||||
}
|
||||
|
||||
public void setSessionTotal(Long sessionTotal) {
|
||||
this.sessionTotal = sessionTotal;
|
||||
}
|
||||
|
||||
public Long getMessageTotal() {
|
||||
return messageTotal;
|
||||
}
|
||||
|
||||
public void setMessageTotal(Long messageTotal) {
|
||||
this.messageTotal = messageTotal;
|
||||
}
|
||||
|
||||
public Long getAssistantTotal() {
|
||||
return assistantTotal;
|
||||
}
|
||||
|
||||
public void setAssistantTotal(Long assistantTotal) {
|
||||
this.assistantTotal = assistantTotal;
|
||||
}
|
||||
}
|
||||
@@ -7,12 +7,17 @@ import org.springframework.util.StringUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardChatStatusVo;
|
||||
import tech.easyflow.admin.model.dashboard.DashboardAssistantTrendPointVo;
|
||||
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.DashboardOverviewVo;
|
||||
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.service.dashboard.DashboardService;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatActiveUserRank;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatAssistantSessionTrend;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatAssistantUsageRank;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatDashboardSummary;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatDashboardTrend;
|
||||
@@ -22,6 +27,7 @@ import tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.common.web.exceptions.BusinessException;
|
||||
import tech.easyflow.system.entity.SysAccountRole;
|
||||
import tech.easyflow.system.entity.SysRole;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
import tech.easyflow.system.service.SysAccountRoleService;
|
||||
import tech.easyflow.system.service.SysRoleService;
|
||||
|
||||
@@ -35,6 +41,7 @@ import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
@@ -49,6 +56,8 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
private static final ZoneId DEFAULT_ZONE_ID = ZoneId.systemDefault();
|
||||
private static final String CHAT_UNAVAILABLE_MESSAGE = "聊天数据不可用";
|
||||
private static final int DEFAULT_ASSISTANT_RANK_LIMIT = 5;
|
||||
private static final int DEFAULT_ASSISTANT_TREND_LIMIT = 8;
|
||||
private static final int DEFAULT_USER_RANK_LIMIT = 5;
|
||||
|
||||
@Resource
|
||||
private SysAccountRoleService sysAccountRoleService;
|
||||
@@ -56,6 +65,9 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
@Resource
|
||||
private SysRoleService sysRoleService;
|
||||
|
||||
@Resource
|
||||
private SysAccountService sysAccountService;
|
||||
|
||||
@Resource
|
||||
private ChatDashboardQueryService chatDashboardQueryService;
|
||||
|
||||
@@ -76,10 +88,14 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
result.setSummary(summary);
|
||||
result.setChatStatus(chatPayload.chatStatus);
|
||||
result.setTrends(chatPayload.trends);
|
||||
result.setAssistantTrends(chatPayload.assistantTrends);
|
||||
result.setDistribution(chatPayload.distribution);
|
||||
result.setUserRanks(chatPayload.userRanks);
|
||||
|
||||
DashboardOverviewQuery normalizedQuery = new DashboardOverviewQuery();
|
||||
normalizedQuery.setRange(context.range);
|
||||
normalizedQuery.setStartDate(context.queryStartDate);
|
||||
normalizedQuery.setEndDate(context.queryEndDate);
|
||||
result.setQuery(normalizedQuery);
|
||||
result.setUpdatedAt(new Date());
|
||||
return result;
|
||||
@@ -101,6 +117,7 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
summary.setChatMessageTotal(0L);
|
||||
summary.setChatSessionTotal(0L);
|
||||
summary.setActiveAssistantTotal(0L);
|
||||
summary.setChatActiveUserTotal(0L);
|
||||
return summary;
|
||||
}
|
||||
|
||||
@@ -122,7 +139,14 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
summary.setChatMessageTotal(0L);
|
||||
summary.setChatSessionTotal(0L);
|
||||
summary.setActiveAssistantTotal(0L);
|
||||
return new ChatDashboardPayload(chatStatus, new ArrayList<>(), new ArrayList<>());
|
||||
summary.setChatActiveUserTotal(0L);
|
||||
return new ChatDashboardPayload(
|
||||
chatStatus,
|
||||
new ArrayList<>(),
|
||||
new ArrayList<>(),
|
||||
new ArrayList<>(),
|
||||
new ArrayList<>()
|
||||
);
|
||||
}
|
||||
|
||||
LocalDate startDate = context.startTime.toLocalDate();
|
||||
@@ -132,20 +156,34 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
summary.setChatMessageTotal(chatSummary.messageTotal());
|
||||
summary.setChatSessionTotal(chatSummary.sessionTotal());
|
||||
summary.setActiveAssistantTotal(chatSummary.activeAssistantTotal());
|
||||
summary.setChatActiveUserTotal(chatSummary.chatActiveUserTotal());
|
||||
|
||||
List<ChatDashboardTrend> rawTrends = "today".equals(context.range)
|
||||
List<ChatDashboardTrend> rawTrends = useHourlyBuckets(context)
|
||||
? chatDashboardQueryService.queryHourlyTrends(context.startTime, context.endTime, context.tenantFilterId)
|
||||
: chatDashboardQueryService.queryTrends(startDate, endDate, context.tenantFilterId);
|
||||
List<DashboardTrendItemVo> trends = buildTrendItems(context.range, rawTrends);
|
||||
List<DashboardTrendItemVo> trends = buildTrendItems(context, rawTrends);
|
||||
|
||||
List<ChatAssistantUsageRank> rawRanks = chatDashboardQueryService.queryAssistantUsageRanks(
|
||||
startDate,
|
||||
endDate,
|
||||
context.tenantFilterId,
|
||||
DEFAULT_ASSISTANT_RANK_LIMIT
|
||||
DEFAULT_ASSISTANT_TREND_LIMIT
|
||||
);
|
||||
List<DashboardDistributionItemVo> distribution = buildAssistantDistribution(rawRanks);
|
||||
return new ChatDashboardPayload(chatStatus, trends, distribution);
|
||||
List<DashboardAssistantTrendSeriesVo> assistantTrends = buildAssistantTrendSeries(
|
||||
context,
|
||||
rawRanks
|
||||
);
|
||||
List<DashboardDistributionItemVo> distribution = buildAssistantDistribution(
|
||||
rawRanks.subList(0, Math.min(DEFAULT_ASSISTANT_RANK_LIMIT, rawRanks.size()))
|
||||
);
|
||||
List<ChatActiveUserRank> rawUserRanks = chatDashboardQueryService.queryActiveUserRanks(
|
||||
startDate,
|
||||
endDate,
|
||||
context.tenantFilterId,
|
||||
DEFAULT_USER_RANK_LIMIT
|
||||
);
|
||||
List<DashboardUserRankItemVo> userRanks = buildUserRanks(rawUserRanks);
|
||||
return new ChatDashboardPayload(chatStatus, trends, assistantTrends, distribution, userRanks);
|
||||
} catch (Exception ex) {
|
||||
log.warn("加载工作台聊天统计失败,已降级为不可用状态,range={}, tenantId={}",
|
||||
context.range,
|
||||
@@ -156,7 +194,14 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
summary.setChatMessageTotal(0L);
|
||||
summary.setChatSessionTotal(0L);
|
||||
summary.setActiveAssistantTotal(0L);
|
||||
return new ChatDashboardPayload(chatStatus, new ArrayList<>(), new ArrayList<>());
|
||||
summary.setChatActiveUserTotal(0L);
|
||||
return new ChatDashboardPayload(
|
||||
chatStatus,
|
||||
new ArrayList<>(),
|
||||
new ArrayList<>(),
|
||||
new ArrayList<>(),
|
||||
new ArrayList<>()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -167,8 +212,12 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
* @param rawTrends 原始趋势
|
||||
* @return 趋势项
|
||||
*/
|
||||
private List<DashboardTrendItemVo> buildTrendItems(String range, List<ChatDashboardTrend> rawTrends) {
|
||||
List<TimeBucket> buckets = buildBuckets(range);
|
||||
private List<DashboardTrendItemVo> buildTrendItems(DashboardQueryContext context, List<ChatDashboardTrend> rawTrends) {
|
||||
List<TimeBucket> buckets = buildBuckets(
|
||||
context.range,
|
||||
context.startTime.toLocalDate(),
|
||||
context.endTime.toLocalDate().minusDays(1)
|
||||
);
|
||||
Map<String, ChatDashboardTrend> trendMap = new HashMap<>();
|
||||
for (ChatDashboardTrend rawTrend : rawTrends) {
|
||||
trendMap.put(rawTrend.bucketKey(), rawTrend);
|
||||
@@ -180,7 +229,7 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
DashboardTrendItemVo item = new DashboardTrendItemVo();
|
||||
item.setKey(bucket.key);
|
||||
item.setLabel(bucket.label);
|
||||
item.setActiveUserTotal(0L);
|
||||
item.setActiveUserTotal(trend == null ? 0L : trend.activeUserTotal());
|
||||
item.setChatMessageTotal(trend == null ? 0L : trend.messageTotal());
|
||||
item.setChatSessionTotal(trend == null ? 0L : trend.sessionTotal());
|
||||
items.add(item);
|
||||
@@ -201,10 +250,100 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
item.setKey(rank.assistantId() == null ? "" : rank.assistantId().toString());
|
||||
item.setAssistantId(rank.assistantId());
|
||||
item.setLabel(resolveAssistantLabel(rank.assistantId(), rank.assistantName()));
|
||||
item.setUserTotal(rank.userTotal());
|
||||
item.setMessageTotal(rank.messageTotal());
|
||||
item.setSessionTotal(rank.sessionTotal());
|
||||
item.setAvgMessagePerSession(calculateAvg(rank.messageTotal(), rank.sessionTotal()));
|
||||
item.setValue(rank.messageTotal());
|
||||
item.setAvgSessionPerUser(calculateAvg(rank.sessionTotal(), rank.userTotal()));
|
||||
item.setValue(rank.sessionTotal());
|
||||
items.add(item);
|
||||
}
|
||||
return items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建智能体活跃趋势序列。
|
||||
*
|
||||
* @param context 查询上下文
|
||||
* @param ranks 智能体排行
|
||||
* @return 趋势序列
|
||||
*/
|
||||
private List<DashboardAssistantTrendSeriesVo> buildAssistantTrendSeries(DashboardQueryContext context,
|
||||
List<ChatAssistantUsageRank> ranks) {
|
||||
if (ranks == null || ranks.isEmpty()) {
|
||||
return new ArrayList<>();
|
||||
}
|
||||
List<TimeBucket> buckets = buildBuckets(
|
||||
context.range,
|
||||
context.startTime.toLocalDate(),
|
||||
context.endTime.toLocalDate().minusDays(1)
|
||||
);
|
||||
Map<BigInteger, ChatAssistantUsageRank> rankMap = new LinkedHashMap<>();
|
||||
for (ChatAssistantUsageRank rank : ranks) {
|
||||
rankMap.putIfAbsent(rank.assistantId(), rank);
|
||||
}
|
||||
List<BigInteger> assistantIds = new ArrayList<>(rankMap.keySet());
|
||||
List<ChatAssistantSessionTrend> rawAssistantTrends = useHourlyBuckets(context)
|
||||
? chatDashboardQueryService.queryAssistantHourlyTrends(
|
||||
context.startTime,
|
||||
context.endTime,
|
||||
context.tenantFilterId,
|
||||
assistantIds
|
||||
)
|
||||
: chatDashboardQueryService.queryAssistantTrends(
|
||||
context.startTime.toLocalDate(),
|
||||
context.endTime.toLocalDate(),
|
||||
context.tenantFilterId,
|
||||
assistantIds
|
||||
);
|
||||
|
||||
Map<BigInteger, Map<String, ChatAssistantSessionTrend>> trendMap = new HashMap<>();
|
||||
for (ChatAssistantSessionTrend rawTrend : rawAssistantTrends) {
|
||||
trendMap.computeIfAbsent(rawTrend.assistantId(), key -> new HashMap<>())
|
||||
.put(rawTrend.bucketKey(), rawTrend);
|
||||
}
|
||||
|
||||
List<DashboardAssistantTrendSeriesVo> seriesList = new ArrayList<>(rankMap.size());
|
||||
for (ChatAssistantUsageRank rank : rankMap.values()) {
|
||||
BigInteger assistantId = rank.assistantId();
|
||||
DashboardAssistantTrendSeriesVo series = new DashboardAssistantTrendSeriesVo();
|
||||
series.setAssistantId(assistantId);
|
||||
series.setLabel(resolveAssistantLabel(assistantId, rank.assistantName()));
|
||||
series.setTotalSessionCount(rank.sessionTotal());
|
||||
|
||||
List<DashboardAssistantTrendPointVo> points = new ArrayList<>(buckets.size());
|
||||
Map<String, ChatAssistantSessionTrend> assistantTrendMap =
|
||||
trendMap.getOrDefault(assistantId, new HashMap<>());
|
||||
for (TimeBucket bucket : buckets) {
|
||||
ChatAssistantSessionTrend trend = assistantTrendMap.get(bucket.key);
|
||||
DashboardAssistantTrendPointVo point = new DashboardAssistantTrendPointVo();
|
||||
point.setKey(bucket.key);
|
||||
point.setLabel(bucket.label);
|
||||
point.setSessionTotal(trend == null ? 0L : trend.sessionTotal());
|
||||
points.add(point);
|
||||
}
|
||||
series.setPoints(points);
|
||||
seriesList.add(series);
|
||||
}
|
||||
return seriesList;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建用户活跃排行。
|
||||
*
|
||||
* @param ranks 原始排行数据
|
||||
* @return 页面排行项
|
||||
*/
|
||||
private List<DashboardUserRankItemVo> buildUserRanks(List<ChatActiveUserRank> ranks) {
|
||||
List<DashboardUserRankItemVo> items = new ArrayList<>(ranks.size());
|
||||
Map<BigInteger, String> displayNameMap = resolveUserDisplayNameMap(ranks);
|
||||
for (ChatActiveUserRank rank : ranks) {
|
||||
DashboardUserRankItemVo item = new DashboardUserRankItemVo();
|
||||
item.setUserId(rank.userId());
|
||||
item.setLabel(resolveUserLabel(rank.userId(), rank.userAccount(), displayNameMap));
|
||||
item.setSessionTotal(rank.sessionTotal());
|
||||
item.setMessageTotal(rank.messageTotal());
|
||||
item.setAssistantTotal(rank.assistantTotal());
|
||||
items.add(item);
|
||||
}
|
||||
return items;
|
||||
@@ -311,12 +450,28 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
if ("today".equals(context.range)) {
|
||||
context.startTime = LocalDateTime.of(today, LocalTime.MIN);
|
||||
context.endTime = context.startTime.plusDays(1);
|
||||
context.queryStartDate = today.toString();
|
||||
context.queryEndDate = today.toString();
|
||||
} else if ("7d".equals(context.range)) {
|
||||
context.startTime = LocalDateTime.of(today.minusDays(6), LocalTime.MIN);
|
||||
context.endTime = LocalDateTime.of(today.plusDays(1), LocalTime.MIN);
|
||||
} else {
|
||||
context.queryStartDate = today.minusDays(6).toString();
|
||||
context.queryEndDate = today.toString();
|
||||
} else if ("30d".equals(context.range)) {
|
||||
context.startTime = LocalDateTime.of(today.minusDays(29), LocalTime.MIN);
|
||||
context.endTime = LocalDateTime.of(today.plusDays(1), LocalTime.MIN);
|
||||
context.queryStartDate = today.minusDays(29).toString();
|
||||
context.queryEndDate = today.toString();
|
||||
} else {
|
||||
LocalDate customStartDate = parseRequiredDate(query == null ? null : query.getStartDate(), "开始日期不能为空");
|
||||
LocalDate customEndDate = parseRequiredDate(query == null ? null : query.getEndDate(), "结束日期不能为空");
|
||||
if (customStartDate.isAfter(customEndDate)) {
|
||||
throw new BusinessException("开始日期不能晚于结束日期");
|
||||
}
|
||||
context.startTime = LocalDateTime.of(customStartDate, LocalTime.MIN);
|
||||
context.endTime = LocalDateTime.of(customEndDate.plusDays(1), LocalTime.MIN);
|
||||
context.queryStartDate = customStartDate.toString();
|
||||
context.queryEndDate = customEndDate.toString();
|
||||
}
|
||||
|
||||
context.tenantFilterId = context.superAdmin || loginAccount == null ? null : loginAccount.getTenantId();
|
||||
@@ -359,7 +514,7 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
if (!StringUtils.hasText(range)) {
|
||||
return "7d";
|
||||
}
|
||||
if ("today".equals(range) || "7d".equals(range) || "30d".equals(range)) {
|
||||
if ("today".equals(range) || "7d".equals(range) || "30d".equals(range) || "custom".equals(range)) {
|
||||
return range;
|
||||
}
|
||||
throw new BusinessException("不支持的时间范围: " + range);
|
||||
@@ -371,13 +526,19 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
* @param range 时间范围
|
||||
* @return 时间桶列表
|
||||
*/
|
||||
private List<TimeBucket> buildBuckets(String range) {
|
||||
private List<TimeBucket> buildBuckets(String range, LocalDate customStartDate, LocalDate customEndDate) {
|
||||
List<TimeBucket> buckets = new ArrayList<>();
|
||||
LocalDate today = LocalDate.now(DEFAULT_ZONE_ID);
|
||||
if ("today".equals(range)) {
|
||||
boolean hourlyBucket = "today".equals(range)
|
||||
|| ("custom".equals(range)
|
||||
&& customStartDate != null
|
||||
&& customEndDate != null
|
||||
&& customStartDate.equals(customEndDate));
|
||||
if (hourlyBucket) {
|
||||
DateTimeFormatter keyFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00:00");
|
||||
DateTimeFormatter labelFormatter = DateTimeFormatter.ofPattern("HH:00");
|
||||
LocalDateTime start = LocalDateTime.of(today, LocalTime.MIN);
|
||||
LocalDate bucketDate = "today".equals(range) ? today : customStartDate;
|
||||
LocalDateTime start = LocalDateTime.of(bucketDate, LocalTime.MIN);
|
||||
for (int hour = 0; hour < 24; hour++) {
|
||||
LocalDateTime current = start.plusHours(hour);
|
||||
buckets.add(new TimeBucket(current.format(keyFormatter), current.format(labelFormatter)));
|
||||
@@ -385,10 +546,20 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
return buckets;
|
||||
}
|
||||
|
||||
int days = "7d".equals(range) ? 7 : 30;
|
||||
DateTimeFormatter keyFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||
DateTimeFormatter labelFormatter = DateTimeFormatter.ofPattern("MM-dd");
|
||||
LocalDate start = today.minusDays(days - 1L);
|
||||
int days;
|
||||
LocalDate start;
|
||||
if ("7d".equals(range)) {
|
||||
days = 7;
|
||||
start = today.minusDays(6);
|
||||
} else if ("30d".equals(range)) {
|
||||
days = 30;
|
||||
start = today.minusDays(29);
|
||||
} else {
|
||||
start = customStartDate;
|
||||
days = (int) java.time.temporal.ChronoUnit.DAYS.between(customStartDate, customEndDate) + 1;
|
||||
}
|
||||
for (int i = 0; i < days; i++) {
|
||||
LocalDate current = start.plusDays(i);
|
||||
buckets.add(new TimeBucket(current.format(keyFormatter), current.format(labelFormatter)));
|
||||
@@ -396,6 +567,38 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
return buckets;
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前上下文是否按小时构建趋势。
|
||||
*
|
||||
* @param context 查询上下文
|
||||
* @return true 表示按小时
|
||||
*/
|
||||
private boolean useHourlyBuckets(DashboardQueryContext context) {
|
||||
return "today".equals(context.range)
|
||||
|| ("custom".equals(context.range)
|
||||
&& context.startTime != null
|
||||
&& context.endTime != null
|
||||
&& context.startTime.toLocalDate().equals(context.endTime.toLocalDate().minusDays(1)));
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析必填日期参数。
|
||||
*
|
||||
* @param dateText 日期文本
|
||||
* @param errorMessage 错误信息
|
||||
* @return 日期
|
||||
*/
|
||||
private LocalDate parseRequiredDate(String dateText, String errorMessage) {
|
||||
if (!StringUtils.hasText(dateText)) {
|
||||
throw new BusinessException(errorMessage);
|
||||
}
|
||||
try {
|
||||
return LocalDate.parse(dateText.trim());
|
||||
} catch (Exception ex) {
|
||||
throw new BusinessException("日期格式不正确: " + dateText);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 把 LocalDateTime 转换为 Date。
|
||||
*
|
||||
@@ -436,6 +639,24 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
return (double) messageTotal / (double) sessionTotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量解析用户展示名称映射。
|
||||
*
|
||||
* @param ranks 活跃排行
|
||||
* @return 名称映射
|
||||
*/
|
||||
private Map<BigInteger, String> resolveUserDisplayNameMap(List<ChatActiveUserRank> ranks) {
|
||||
List<BigInteger> userIds = ranks.stream()
|
||||
.map(ChatActiveUserRank::userId)
|
||||
.filter(java.util.Objects::nonNull)
|
||||
.distinct()
|
||||
.collect(Collectors.toList());
|
||||
if (userIds.isEmpty()) {
|
||||
return new HashMap<>();
|
||||
}
|
||||
return sysAccountService.resolveDisplayNameMap(userIds);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析智能体展示名称。
|
||||
*
|
||||
@@ -450,6 +671,27 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
return assistantId == null ? "智能体-未知" : "智能体-" + assistantId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析用户展示名称。
|
||||
*
|
||||
* @param userId 用户 ID
|
||||
* @param userAccount 聊天侧账号快照
|
||||
* @param displayNameMap 系统账号名称映射
|
||||
* @return 展示名称
|
||||
*/
|
||||
private String resolveUserLabel(BigInteger userId, String userAccount, Map<BigInteger, String> displayNameMap) {
|
||||
if (userId != null) {
|
||||
String displayName = displayNameMap.get(userId);
|
||||
if (StringUtils.hasText(displayName) && !displayName.equals(userId.toString())) {
|
||||
return displayName;
|
||||
}
|
||||
}
|
||||
if (StringUtils.hasText(userAccount)) {
|
||||
return userAccount.trim();
|
||||
}
|
||||
return userId == null ? "用户-未知" : "用户-" + userId;
|
||||
}
|
||||
|
||||
/**
|
||||
* 工作台查询上下文。
|
||||
*/
|
||||
@@ -460,6 +702,8 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
private boolean superAdmin;
|
||||
private LocalDateTime startTime;
|
||||
private LocalDateTime endTime;
|
||||
private String queryStartDate;
|
||||
private String queryEndDate;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -481,7 +725,9 @@ public class DashboardServiceImpl implements DashboardService {
|
||||
private record ChatDashboardPayload(
|
||||
DashboardChatStatusVo chatStatus,
|
||||
List<DashboardTrendItemVo> trends,
|
||||
List<DashboardDistributionItemVo> distribution
|
||||
List<DashboardAssistantTrendSeriesVo> assistantTrends,
|
||||
List<DashboardDistributionItemVo> distribution,
|
||||
List<DashboardUserRankItemVo> userRanks
|
||||
) {
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,13 +2,20 @@ package tech.easyflow.admin.service.dashboard.impl;
|
||||
|
||||
import org.testng.Assert;
|
||||
import org.testng.annotations.Test;
|
||||
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.chatlog.domain.dto.ChatActiveUserRank;
|
||||
import tech.easyflow.chatlog.domain.dto.ChatAssistantSessionTrend;
|
||||
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 tech.easyflow.common.entity.LoginAccount;
|
||||
import tech.easyflow.system.service.SysAccountService;
|
||||
|
||||
import java.lang.reflect.Constructor;
|
||||
import java.lang.reflect.Field;
|
||||
@@ -19,9 +26,13 @@ import java.time.LocalDateTime;
|
||||
import java.time.LocalTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
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.Mockito.mock;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
@@ -56,6 +67,7 @@ public class DashboardServiceImplTest {
|
||||
Assert.assertEquals(summary.getChatMessageTotal(), Long.valueOf(0L));
|
||||
Assert.assertEquals(summary.getChatSessionTotal(), Long.valueOf(0L));
|
||||
Assert.assertEquals(summary.getActiveAssistantTotal(), Long.valueOf(0L));
|
||||
Assert.assertEquals(summary.getChatActiveUserTotal(), Long.valueOf(0L));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,12 +84,19 @@ public class DashboardServiceImplTest {
|
||||
.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));
|
||||
.thenReturn(new ChatDashboardSummary(3L, 9L, 1L, 2L));
|
||||
when(chatDashboardQueryService.queryHourlyTrends(any(), any(), any()))
|
||||
.thenReturn(List.of(new ChatDashboardTrend(currentHourKey, 3L, 9L)));
|
||||
.thenReturn(List.of(new ChatDashboardTrend(currentHourKey, 3L, 9L, 2L)));
|
||||
when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), any(Integer.class)))
|
||||
.thenReturn(List.of(new ChatAssistantUsageRank(BigInteger.ONE, "", 3L, 9L)));
|
||||
.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();
|
||||
@@ -85,7 +104,10 @@ public class DashboardServiceImplTest {
|
||||
|
||||
Object chatStatus = readField(payload, "chatStatus");
|
||||
List<DashboardTrendItemVo> trends = (List<DashboardTrendItemVo>) readField(payload, "trends");
|
||||
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);
|
||||
@@ -94,13 +116,232 @@ public class DashboardServiceImplTest {
|
||||
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(10).getActiveUserTotal(), Long.valueOf(2L));
|
||||
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(summary.getChatActiveUserTotal(), Long.valueOf(2L));
|
||||
Assert.assertEquals(assistantTrends.size(), 1);
|
||||
Assert.assertEquals(assistantTrends.get(0).getLabel(), "智能体-1");
|
||||
Assert.assertEquals(assistantTrends.get(0).getTotalSessionCount(), Long.valueOf(3L));
|
||||
Assert.assertEquals(assistantTrends.get(0).getPoints().size(), 24);
|
||||
Assert.assertEquals(assistantTrends.get(0).getPoints().get(10).getSessionTotal(), Long.valueOf(3L));
|
||||
Assert.assertEquals(assistantTrends.get(0).getPoints().get(11).getSessionTotal(), Long.valueOf(0L));
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证当系统账号名称仅回退为纯 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)))
|
||||
.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"));
|
||||
setField(service, "chatDashboardQueryService", chatDashboardQueryService);
|
||||
setField(service, "sysAccountService", sysAccountService);
|
||||
|
||||
Object context = newContext("today", BigInteger.ONE);
|
||||
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||
Object payload = invokeBuildChatPayload(service, context, summary);
|
||||
|
||||
List<DashboardUserRankItemVo> userRanks = (List<DashboardUserRankItemVo>) readField(payload, "userRanks");
|
||||
Assert.assertEquals(userRanks.get(0).getLabel(), "chat-user");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证日趋势会保留 assistantId 为空的排行项,并补齐 7 天点位。
|
||||
*
|
||||
* @throws Exception 反射调用失败
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldBuildDailyAssistantTrendSeriesForRankedAssistants() 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(4L, 12L, 2L, 3L));
|
||||
when(chatDashboardQueryService.queryTrends(any(), any(), any()))
|
||||
.thenReturn(List.of());
|
||||
when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), any(Integer.class)))
|
||||
.thenReturn(List.of(
|
||||
new ChatAssistantUsageRank(BigInteger.ONE, "助手-A", 3L, 4L, 12L),
|
||||
new ChatAssistantUsageRank(null, "未知助手", 1L, 2L, 4L)
|
||||
));
|
||||
when(chatDashboardQueryService.queryAssistantTrends(any(), any(), any(), any()))
|
||||
.thenReturn(List.of(
|
||||
new ChatAssistantSessionTrend(BigInteger.ONE, "助手-A", LocalDate.now().minusDays(6).toString(), 2L),
|
||||
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();
|
||||
Object payload = invokeBuildChatPayload(service, context, summary);
|
||||
|
||||
List<DashboardAssistantTrendSeriesVo> assistantTrends =
|
||||
(List<DashboardAssistantTrendSeriesVo>) readField(payload, "assistantTrends");
|
||||
Assert.assertEquals(assistantTrends.size(), 2);
|
||||
Assert.assertEquals(assistantTrends.get(0).getLabel(), "助手-A");
|
||||
Assert.assertEquals(assistantTrends.get(0).getPoints().size(), 7);
|
||||
Assert.assertEquals(assistantTrends.get(0).getPoints().get(0).getSessionTotal(), Long.valueOf(2L));
|
||||
Assert.assertEquals(assistantTrends.get(0).getPoints().get(6).getSessionTotal(), Long.valueOf(4L));
|
||||
Assert.assertEquals(assistantTrends.get(0).getPoints().get(1).getSessionTotal(), Long.valueOf(0L));
|
||||
Assert.assertNull(assistantTrends.get(1).getAssistantId());
|
||||
Assert.assertEquals(assistantTrends.get(1).getLabel(), "未知助手");
|
||||
Assert.assertEquals(assistantTrends.get(1).getPoints().get(3).getSessionTotal(), Long.valueOf(2L));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证自定义单天范围按小时桶构建。
|
||||
*
|
||||
* @throws Exception 反射调用失败
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldBuildHourlyTrendForCustomSingleDayRange() throws Exception {
|
||||
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||
LocalDate customDate = LocalDate.now().minusDays(2);
|
||||
String currentHourKey = LocalDateTime.of(customDate, LocalTime.of(8, 0))
|
||||
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:00:00"));
|
||||
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||
when(chatDashboardQueryService.querySummary(any(), any(), any()))
|
||||
.thenReturn(new ChatDashboardSummary(2L, 6L, 1L, 1L));
|
||||
when(chatDashboardQueryService.queryHourlyTrends(any(), any(), any()))
|
||||
.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",
|
||||
BigInteger.ONE,
|
||||
LocalDateTime.of(customDate, LocalTime.MIN),
|
||||
LocalDateTime.of(customDate.plusDays(1), LocalTime.MIN)
|
||||
);
|
||||
DashboardSummaryVo summary = new DashboardSummaryVo();
|
||||
Object payload = invokeBuildChatPayload(service, context, summary);
|
||||
|
||||
List<DashboardTrendItemVo> trends = (List<DashboardTrendItemVo>) readField(payload, "trends");
|
||||
Assert.assertEquals(trends.size(), 24);
|
||||
Assert.assertEquals(trends.get(8).getKey(), currentHourKey);
|
||||
Assert.assertEquals(trends.get(8).getActiveUserTotal(), Long.valueOf(1L));
|
||||
Assert.assertEquals(trends.get(9).getChatSessionTotal(), Long.valueOf(0L));
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证自定义多天范围按天桶构建,并保留查询日期。
|
||||
*
|
||||
* @throws Exception 反射调用失败
|
||||
*/
|
||||
@Test
|
||||
public void shouldBuildDailyBucketsForCustomMultiDayRangeContext() throws Exception {
|
||||
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||
DashboardOverviewQuery query = new DashboardOverviewQuery();
|
||||
query.setRange("custom");
|
||||
query.setStartDate("2026-05-01");
|
||||
query.setEndDate("2026-05-03");
|
||||
|
||||
Object context = invokeBuildContext(service, query);
|
||||
Assert.assertEquals(readField(context, "range"), "custom");
|
||||
Assert.assertEquals(
|
||||
readField(context, "startTime"),
|
||||
LocalDateTime.of(LocalDate.of(2026, 5, 1), LocalTime.MIN)
|
||||
);
|
||||
Assert.assertEquals(
|
||||
readField(context, "endTime"),
|
||||
LocalDateTime.of(LocalDate.of(2026, 5, 4), LocalTime.MIN)
|
||||
);
|
||||
Assert.assertEquals(readField(context, "queryStartDate"), "2026-05-01");
|
||||
Assert.assertEquals(readField(context, "queryEndDate"), "2026-05-03");
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证近 30 天趋势补齐完整 30 个桶,并按 Top 8 请求智能体活跃排行。
|
||||
*
|
||||
* @throws Exception 反射调用失败
|
||||
*/
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void shouldBuildThirtyDayBucketsAndRequestTopEightAssistantRanks() throws Exception {
|
||||
DashboardServiceImpl service = new DashboardServiceImpl();
|
||||
ChatDashboardQueryService chatDashboardQueryService = mock(ChatDashboardQueryService.class);
|
||||
LocalDate startDate = LocalDate.now().minusDays(29);
|
||||
LocalDate endDate = LocalDate.now();
|
||||
when(chatDashboardQueryService.available()).thenReturn(true);
|
||||
when(chatDashboardQueryService.querySummary(any(), any(), any()))
|
||||
.thenReturn(new ChatDashboardSummary(10L, 20L, 8L, 4L));
|
||||
when(chatDashboardQueryService.queryTrends(any(), any(), any()))
|
||||
.thenReturn(List.of(
|
||||
new ChatDashboardTrend(startDate.toString(), 3L, 6L, 2L),
|
||||
new ChatDashboardTrend(endDate.toString(), 7L, 14L, 4L)
|
||||
));
|
||||
when(chatDashboardQueryService.queryAssistantUsageRanks(any(), any(), any(), eq(8)))
|
||||
.thenReturn(IntStream.rangeClosed(1, 8)
|
||||
.mapToObj(index -> new ChatAssistantUsageRank(
|
||||
BigInteger.valueOf(index),
|
||||
"助手-" + index,
|
||||
index,
|
||||
index * 2L,
|
||||
index * 4L
|
||||
))
|
||||
.toList());
|
||||
when(chatDashboardQueryService.queryAssistantTrends(any(), any(), any(), any()))
|
||||
.thenReturn(List.of(
|
||||
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();
|
||||
Object payload = invokeBuildChatPayload(service, context, summary);
|
||||
|
||||
List<DashboardTrendItemVo> trends = (List<DashboardTrendItemVo>) readField(payload, "trends");
|
||||
List<DashboardAssistantTrendSeriesVo> assistantTrends =
|
||||
(List<DashboardAssistantTrendSeriesVo>) readField(payload, "assistantTrends");
|
||||
Assert.assertEquals(trends.size(), 30);
|
||||
Assert.assertEquals(trends.get(0).getKey(), startDate.toString());
|
||||
Assert.assertEquals(trends.get(0).getChatSessionTotal(), Long.valueOf(3L));
|
||||
Assert.assertEquals(trends.get(29).getKey(), endDate.toString());
|
||||
Assert.assertEquals(trends.get(29).getChatMessageTotal(), Long.valueOf(14L));
|
||||
Assert.assertEquals(trends.get(1).getChatSessionTotal(), Long.valueOf(0L));
|
||||
Assert.assertEquals(assistantTrends.size(), 8);
|
||||
Assert.assertEquals(assistantTrends.get(0).getPoints().size(), 30);
|
||||
verify(chatDashboardQueryService).queryAssistantUsageRanks(any(), any(), any(), eq(8));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,6 +353,28 @@ public class DashboardServiceImplTest {
|
||||
* @throws Exception 反射失败
|
||||
*/
|
||||
private Object newContext(String range, BigInteger tenantId) throws Exception {
|
||||
return newContext(
|
||||
range,
|
||||
tenantId,
|
||||
LocalDateTime.of(LocalDate.now(), java.time.LocalTime.MIN),
|
||||
LocalDateTime.of(LocalDate.now().plusDays(1), java.time.LocalTime.MIN)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造查询上下文。
|
||||
*
|
||||
* @param range 时间范围
|
||||
* @param tenantId 租户 ID
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
* @return 查询上下文实例
|
||||
* @throws Exception 反射失败
|
||||
*/
|
||||
private Object newContext(String range,
|
||||
BigInteger tenantId,
|
||||
LocalDateTime startTime,
|
||||
LocalDateTime endTime) throws Exception {
|
||||
Class<?> contextClass = Class.forName(
|
||||
"tech.easyflow.admin.service.dashboard.impl.DashboardServiceImpl$DashboardQueryContext"
|
||||
);
|
||||
@@ -120,11 +383,29 @@ public class DashboardServiceImplTest {
|
||||
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));
|
||||
setField(context, "startTime", startTime);
|
||||
setField(context, "endTime", endTime);
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用私有上下文构建方法。
|
||||
*
|
||||
* @param service service
|
||||
* @param query 查询参数
|
||||
* @return 上下文
|
||||
* @throws Exception 反射失败
|
||||
*/
|
||||
private Object invokeBuildContext(DashboardServiceImpl service, DashboardOverviewQuery query) throws Exception {
|
||||
Method method = DashboardServiceImpl.class.getDeclaredMethod(
|
||||
"buildContext",
|
||||
LoginAccount.class,
|
||||
DashboardOverviewQuery.class
|
||||
);
|
||||
method.setAccessible(true);
|
||||
return method.invoke(service, null, query);
|
||||
}
|
||||
|
||||
/**
|
||||
* 调用私有聊天载荷组装方法。
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user