refactor(ops): 重构统计模块,支持日期参数化查询及代码质量优化
Some checks failed
Java CI with Maven / build (11) (push) Has been cancelled
Java CI with Maven / build (17) (push) Has been cancelled
Java CI with Maven / build (8) (push) Has been cancelled

- 客流接口支持指定日期查询(getTrafficRealtime、getTrafficTrend、getAreaTrafficRealtime)
- 移除昨日对比趋势字段(yesterdayHourlyTrend),简化为单日期模式
- 漏斗图改为工单状态分布(FunnelItem→StatusDistributionItem),使用 SQL COUNT 替代内存分组
- 新增工牌队列统计(BadgeQueueStats),按 orderType 过滤避免跨类型数据混入
- 在线工牌计数仅统计 IDLE/BUSY 状态(排除 PAUSED/OFFLINE)
- 修复通配符导入和全限定类名引用,规范化 import 语句

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
lzh
2026-03-01 00:17:26 +08:00
parent 7d1012bba7
commit 1c8eee9db4
8 changed files with 218 additions and 156 deletions

View File

@@ -36,8 +36,8 @@ public class DashboardStatsRespVO {
@Schema(description = "时效趋势数据")
private TimeTrendData timeTrendData;
@Schema(description = "漏斗数据")
private List<FunnelItem> funnelData;
@Schema(description = "工单状态分布数据")
private List<StatusDistributionItem> statusDistribution;
@Schema(description = "热力图数据近7天")
private HeatmapData heatmapData;
@@ -45,6 +45,9 @@ public class DashboardStatsRespVO {
@Schema(description = "功能类型排行")
private List<FunctionTypeRankingItem> functionTypeRanking;
@Schema(description = "工牌队列统计近7天每天的排队数量")
private BadgeQueueStats badgeQueueStats;
@Data
@Builder
@NoArgsConstructor
@@ -86,8 +89,8 @@ public class DashboardStatsRespVO {
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class FunnelItem {
@Schema(description = "阶段名称")
public static class StatusDistributionItem {
@Schema(description = "状态名称")
private String name;
@Schema(description = "数量")
private Integer value;
@@ -121,4 +124,15 @@ public class DashboardStatsRespVO {
private Double rate;
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public static class BadgeQueueStats {
@Schema(description = "日期列表")
private List<String> dates;
@Schema(description = "每日排队工牌数量")
private List<Integer> queueData;
}
}

View File

@@ -39,10 +39,10 @@ public class DeviceTrafficRealtimeRespVO {
@Schema(description = "当前在场人数")
private Long currentOccupancy;
@Schema(description = "今日小时趋势")
@Schema(description = "统计日期")
private String statDate;
@Schema(description = "小时趋势")
private TrafficRealtimeRespVO.HourlyTrend hourlyTrend;
@Schema(description = "昨日小时趋势")
private TrafficRealtimeRespVO.HourlyTrend yesterdayHourlyTrend;
}

View File

@@ -27,11 +27,11 @@ public class TrafficRealtimeRespVO {
@Schema(description = "区域客流明细")
private List<AreaTrafficItem> areas;
@Schema(description = "今日小时趋势")
private HourlyTrend hourlyTrend;
@Schema(description = "统计日期")
private String statDate;
@Schema(description = "昨日小时趋势")
private HourlyTrend yesterdayHourlyTrend;
@Schema(description = "小时趋势")
private HourlyTrend hourlyTrend;
@Schema(description = "提示信息,如区域暂未配置客流设备")
private String message;

View File

@@ -13,14 +13,14 @@ import java.util.List;
*
* @author lzh
*/
@Schema(description = "管理后台 - 近7天客流统计 Response VO")
@Schema(description = "管理后台 - 客流趋势统计 Response VO")
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TrafficTrendRespVO {
@Schema(description = "日期列表近7天")
@Schema(description = "日期列表")
private List<String> dates;
@Schema(description = "每日进入人数")

View File

@@ -30,9 +30,10 @@ public interface OpsStatisticsService {
/**
* 获取实时客流监测数据
*
* @param date 指定日期(可选,默认今天)
* @return 实时客流数据
*/
TrafficRealtimeRespVO getTrafficRealtime();
TrafficRealtimeRespVO getTrafficRealtime(LocalDate date);
/**
* 获取工作台统计数据
@@ -42,11 +43,13 @@ public interface OpsStatisticsService {
WorkspaceStatsRespVO getWorkspaceStats();
/**
* 获取近7天客流趋势统计
* 获取客流趋势统计
*
* @return 近7天客流趋势数据
* @param startDate 开始日期可选默认近7天
* @param endDate 结束日期(可选,默认今天)
* @return 客流趋势数据
*/
TrafficTrendRespVO getTrafficTrend();
TrafficTrendRespVO getTrafficTrend(LocalDate startDate, LocalDate endDate);
/**
* 按设备查实时客流
@@ -88,9 +91,10 @@ public interface OpsStatisticsService {
* 按区域ID列表查实时客流汇总
*
* @param areaIds 区域ID列表
* @param date 指定日期(可选,默认今天)
* @return 汇总的实时客流数据
*/
TrafficRealtimeRespVO getAreaTrafficRealtime(List<Long> areaIds);
TrafficRealtimeRespVO getAreaTrafficRealtime(List<Long> areaIds, LocalDate date);
/**
* 按区域ID列表查趋势汇总

View File

@@ -15,6 +15,8 @@ import com.viewsh.module.ops.dal.dataobject.statistics.OpsTrafficStatisticsDO;
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
import com.viewsh.module.ops.dal.mysql.area.OpsBusAreaMapper;
import com.viewsh.module.ops.dal.mysql.statistics.OpsTrafficStatisticsMapper;
import com.viewsh.module.ops.dal.dataobject.queue.OpsOrderQueueDO;
import com.viewsh.module.ops.dal.mysql.queue.OpsOrderQueueMapper;
import com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper;
import jakarta.annotation.Resource;
import lombok.extern.slf4j.Slf4j;
@@ -28,8 +30,10 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
@@ -70,6 +74,9 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
@Resource
private OpsTrafficStatisticsMapper trafficStatisticsMapper;
@Resource
private OpsOrderQueueMapper opsOrderQueueMapper;
/**
* 在线员工数提供者(由 ops-server 层注入,从 environment-biz 获取工牌状态)
* 返回 [onlineCount, totalCount]
@@ -118,18 +125,23 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
// 7. 时效趋势(单次 GROUP BY 查询)
TimeTrendData timeTrendData = buildTimeTrendData(orderType, startDate, endDate);
// 8. 漏斗数据(基于指定时间范围的工单状态推算
List<FunnelItem> funnelData = buildFunnelData(orderType, startDateTime, endDateTime);
// 9. 热力图近7天
// 近7天时间范围多个图表复用
LocalDate sevenDaysAgo = LocalDate.now().minusDays(6); // 包含今天共7天
LocalDateTime sevenDaysAgoStart = sevenDaysAgo.atStartOfDay();
LocalDateTime nowEnd = LocalDate.now().plusDays(1).atStartOfDay();
// 8. 工单状态分布近7天所有工单的状态占比
List<StatusDistributionItem> statusDistribution = buildStatusDistribution(orderType, sevenDaysAgoStart, nowEnd);
// 9. 热力图近7天
HeatmapData heatmapData = buildHeatmapData(orderType, sevenDaysAgoStart, nowEnd);
// 10. 功能类型排行(单次 GROUP BY 查询)
List<FunctionTypeRankingItem> functionTypeRanking = buildFunctionTypeRanking(orderType);
// 11. 工牌队列统计近7天每天排队的工单数 = QUEUED 状态)
BadgeQueueStats badgeQueueStats = buildBadgeQueueStats(orderType, sevenDaysAgo, LocalDate.now());
return DashboardStatsRespVO.builder()
.pendingCount(pendingCount)
.inProgressCount(inProgressCount)
@@ -138,38 +150,33 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
.trendData(trendData)
.hourlyDistribution(hourlyDistribution)
.timeTrendData(timeTrendData)
.funnelData(funnelData)
.statusDistribution(statusDistribution)
.heatmapData(heatmapData)
.functionTypeRanking(functionTypeRanking)
.badgeQueueStats(badgeQueueStats)
.build();
}
@Override
@Cacheable(value = "ops:statistics:traffic#5m", unless = "#result == null")
public TrafficRealtimeRespVO getTrafficRealtime() {
LocalDate today = LocalDate.now();
LocalDateTime todayStart = today.atStartOfDay();
LocalDateTime todayEnd = todayStart.plusDays(1);
@Cacheable(value = "ops:statistics:traffic#5m", key = "#date != null ? #date.toString() : T(java.time.LocalDate).now().toString()", unless = "#result == null")
public TrafficRealtimeRespVO getTrafficRealtime(LocalDate date) {
if (date == null) {
date = LocalDate.now();
}
LocalDateTime dayStart = date.atStartOfDay();
LocalDateTime dayEnd = dayStart.plusDays(1);
// 从 ops_traffic_statistics 表查询今日汇总(使用 ge + lt 避免边界重叠)
List<OpsTrafficStatisticsDO> todayStats = trafficStatisticsMapper.selectList(
// 从 ops_traffic_statistics 表查询指定日期汇总(使用 ge + lt 避免边界重叠)
List<OpsTrafficStatisticsDO> dayStats = trafficStatisticsMapper.selectList(
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
.ge(OpsTrafficStatisticsDO::getStatHour, todayStart)
.lt(OpsTrafficStatisticsDO::getStatHour, todayEnd));
// 查询昨日数据(用于昨日小时趋势对比)
LocalDate yesterday = today.minusDays(1);
LocalDateTime yesterdayStart = yesterday.atStartOfDay();
List<OpsTrafficStatisticsDO> yesterdayStats = trafficStatisticsMapper.selectList(
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
.ge(OpsTrafficStatisticsDO::getStatHour, yesterdayStart)
.lt(OpsTrafficStatisticsDO::getStatHour, todayStart));
.ge(OpsTrafficStatisticsDO::getStatHour, dayStart)
.lt(OpsTrafficStatisticsDO::getStatHour, dayEnd));
// 加载区域名称映射
Map<Long, String> areaNameMap = loadAreaNameMap();
// 按区域汇总
Map<Long, List<OpsTrafficStatisticsDO>> byArea = todayStats.stream()
Map<Long, List<OpsTrafficStatisticsDO>> byArea = dayStats.stream()
.filter(s -> s.getAreaId() != null)
.collect(Collectors.groupingBy(OpsTrafficStatisticsDO::getAreaId));
@@ -196,17 +203,16 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
.build());
}
// 小时趋势(今日 + 昨日)
HourlyTrend hourlyTrend = buildTrafficHourlyTrend(todayStats);
HourlyTrend yesterdayHourlyTrend = buildTrafficHourlyTrend(yesterdayStats);
// 小时趋势
HourlyTrend hourlyTrend = buildTrafficHourlyTrend(dayStats);
return TrafficRealtimeRespVO.builder()
.totalIn(totalIn)
.totalOut(totalOut)
.currentOccupancy(Math.max(0, totalIn - totalOut))
.areas(areas)
.statDate(date.toString())
.hourlyTrend(hourlyTrend)
.yesterdayHourlyTrend(yesterdayHourlyTrend)
.build();
}
@@ -231,8 +237,11 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
}
}
// 2. 待处理工单数
Integer pendingCount = countByStatusAndType("PENDING", orderType);
// 2. 待处理工单数(所有未取消、未完成的工单)
Integer pendingCount = Math.toIntExact(opsOrderMapper.selectCount(
new LambdaQueryWrapperX<OpsOrderDO>()
.eq(OpsOrderDO::getOrderType, orderType)
.notIn(OpsOrderDO::getStatus, "COMPLETED", "CANCELLED")));
// 3. 平均响应时长(今日已完成的工单,使用 ge + lt 避免边界重叠)
List<OpsOrderDO> completedToday = opsOrderMapper.selectList(
@@ -314,14 +323,24 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
}
@Override
@Cacheable(value = "ops:statistics:traffic-trend#5m", unless = "#result == null")
public TrafficTrendRespVO getTrafficTrend() {
@Cacheable(value = "ops:statistics:traffic-trend#5m",
key = "(#startDate != null ? #startDate.toString() : 'default') + ':' + (#endDate != null ? #endDate.toString() : 'default')", unless = "#result == null")
public TrafficTrendRespVO getTrafficTrend(LocalDate startDate, LocalDate endDate) {
LocalDate today = LocalDate.now();
LocalDate sevenDaysAgo = today.minusDays(6); // 包含今天共7天
if (startDate == null) {
startDate = today.minusDays(6);
}
if (endDate == null) {
endDate = today;
}
// 限制最大范围为31天
if (java.time.temporal.ChronoUnit.DAYS.between(startDate, endDate) > 30) {
startDate = endDate.minusDays(30);
}
// 从 ops_traffic_statistics 表查询近7天的客流数据
LocalDateTime startDateTime = sevenDaysAgo.atStartOfDay();
LocalDateTime endDateTime = today.plusDays(1).atStartOfDay();
// 从 ops_traffic_statistics 表查询客流数据
LocalDateTime startDateTime = startDate.atStartOfDay();
LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay();
List<OpsTrafficStatisticsDO> stats = trafficStatisticsMapper.selectList(
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
@@ -352,7 +371,7 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
long totalIn = 0;
long totalOut = 0;
for (LocalDate d = sevenDaysAgo; !d.isAfter(today); d = d.plusDays(1)) {
for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
dates.add(d.format(DATE_FORMATTER));
long dayIn = dailyInMap.getOrDefault(d, 0L);
long dayOut = dailyOutMap.getOrDefault(d, 0L);
@@ -389,15 +408,6 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
.ge(OpsTrafficStatisticsDO::getStatHour, todayStart)
.lt(OpsTrafficStatisticsDO::getStatHour, todayEnd));
// 查询昨日数据
LocalDate yesterday = today.minusDays(1);
LocalDateTime yesterdayStart = yesterday.atStartOfDay();
List<OpsTrafficStatisticsDO> yesterdayStats = trafficStatisticsMapper.selectList(
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
.eq(OpsTrafficStatisticsDO::getDeviceId, deviceId)
.ge(OpsTrafficStatisticsDO::getStatHour, yesterdayStart)
.lt(OpsTrafficStatisticsDO::getStatHour, todayStart));
long totalIn = stats.stream().mapToLong(s -> s.getPeopleIn() != null ? s.getPeopleIn() : 0).sum();
long totalOut = stats.stream().mapToLong(s -> s.getPeopleOut() != null ? s.getPeopleOut() : 0).sum();
@@ -406,7 +416,6 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
Map<Long, String> areaNameMap = loadAreaNameMap();
HourlyTrend hourlyTrend = buildTrafficHourlyTrend(stats);
HourlyTrend yesterdayHourlyTrend = buildTrafficHourlyTrend(yesterdayStats);
return DeviceTrafficRealtimeRespVO.builder()
.deviceId(deviceId)
@@ -415,8 +424,8 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
.todayIn(totalIn)
.todayOut(totalOut)
.currentOccupancy(Math.max(0, totalIn - totalOut))
.statDate(today.toString())
.hourlyTrend(hourlyTrend)
.yesterdayHourlyTrend(yesterdayHourlyTrend)
.build();
}
@@ -433,28 +442,14 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
.ge(OpsTrafficStatisticsDO::getStatHour, todayStart)
.lt(OpsTrafficStatisticsDO::getStatHour, todayEnd));
// 查询昨日数据
LocalDate yesterday = today.minusDays(1);
LocalDateTime yesterdayStart = yesterday.atStartOfDay();
List<OpsTrafficStatisticsDO> yesterdayStats = trafficStatisticsMapper.selectList(
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
.eq(OpsTrafficStatisticsDO::getAreaId, areaId)
.ge(OpsTrafficStatisticsDO::getStatHour, yesterdayStart)
.lt(OpsTrafficStatisticsDO::getStatHour, todayStart));
Map<Long, String> areaNameMap = loadAreaNameMap();
String areaName = areaNameMap.getOrDefault(areaId, "未知区域");
// 按设备分组(今日)
// 按设备分组
Map<Long, List<OpsTrafficStatisticsDO>> byDevice = stats.stream()
.filter(s -> s.getDeviceId() != null)
.collect(Collectors.groupingBy(OpsTrafficStatisticsDO::getDeviceId));
// 按设备分组(昨日)
Map<Long, List<OpsTrafficStatisticsDO>> yesterdayByDevice = yesterdayStats.stream()
.filter(s -> s.getDeviceId() != null)
.collect(Collectors.groupingBy(OpsTrafficStatisticsDO::getDeviceId));
List<DeviceTrafficRealtimeRespVO> result = new ArrayList<>();
for (Map.Entry<Long, List<OpsTrafficStatisticsDO>> entry : byDevice.entrySet()) {
Long deviceId = entry.getKey();
@@ -464,8 +459,6 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
long deviceOut = deviceStats.stream().mapToLong(s -> s.getPeopleOut() != null ? s.getPeopleOut() : 0).sum();
HourlyTrend hourlyTrend = buildTrafficHourlyTrend(deviceStats);
HourlyTrend yesterdayHourlyTrend = buildTrafficHourlyTrend(
yesterdayByDevice.getOrDefault(deviceId, List.of()));
result.add(DeviceTrafficRealtimeRespVO.builder()
.deviceId(deviceId)
@@ -474,8 +467,8 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
.todayIn(deviceIn)
.todayOut(deviceOut)
.currentOccupancy(Math.max(0, deviceIn - deviceOut))
.statDate(today.toString())
.hourlyTrend(hourlyTrend)
.yesterdayHourlyTrend(yesterdayHourlyTrend)
.build());
}
return result;
@@ -509,8 +502,8 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
@Override
@Cacheable(value = "ops:statistics:area-traffic-realtime#5m",
key = "T(String).join(',', #areaIds.![toString()])", unless = "#result == null")
public TrafficRealtimeRespVO getAreaTrafficRealtime(List<Long> areaIds) {
key = "T(String).join(',', #areaIds.![toString()]) + ':' + (#date != null ? #date.toString() : T(java.time.LocalDate).now().toString())", unless = "#result == null")
public TrafficRealtimeRespVO getAreaTrafficRealtime(List<Long> areaIds, LocalDate date) {
if (areaIds == null || areaIds.isEmpty()) {
return TrafficRealtimeRespVO.builder()
.totalIn(0L).totalOut(0L).currentOccupancy(0L)
@@ -521,42 +514,36 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
if (areaIds.size() > 200) {
areaIds = areaIds.subList(0, 200);
}
if (date == null) {
date = LocalDate.now();
}
LocalDate today = LocalDate.now();
LocalDateTime todayStart = today.atStartOfDay();
LocalDateTime todayEnd = todayStart.plusDays(1);
LocalDateTime dayStart = date.atStartOfDay();
LocalDateTime dayEnd = dayStart.plusDays(1);
// 查询今日数据,条件 areaId IN (areaIds)
List<OpsTrafficStatisticsDO> todayStats = trafficStatisticsMapper.selectList(
// 查询指定日期数据,条件 areaId IN (areaIds)
List<OpsTrafficStatisticsDO> dayStats = trafficStatisticsMapper.selectList(
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
.in(OpsTrafficStatisticsDO::getAreaId, areaIds)
.ge(OpsTrafficStatisticsDO::getStatHour, todayStart)
.lt(OpsTrafficStatisticsDO::getStatHour, todayEnd));
// 查询昨日数据
LocalDate yesterday = today.minusDays(1);
LocalDateTime yesterdayStart = yesterday.atStartOfDay();
List<OpsTrafficStatisticsDO> yesterdayStats = trafficStatisticsMapper.selectList(
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
.in(OpsTrafficStatisticsDO::getAreaId, areaIds)
.ge(OpsTrafficStatisticsDO::getStatHour, yesterdayStart)
.lt(OpsTrafficStatisticsDO::getStatHour, todayStart));
.ge(OpsTrafficStatisticsDO::getStatHour, dayStart)
.lt(OpsTrafficStatisticsDO::getStatHour, dayEnd));
// 无数据时设置提示信息
if (todayStats.isEmpty() && yesterdayStats.isEmpty()) {
if (dayStats.isEmpty()) {
return TrafficRealtimeRespVO.builder()
.totalIn(0L).totalOut(0L).currentOccupancy(0L)
.statDate(date.toString())
.message("该区域暂未配置客流设备")
.build();
}
// 汇总 totalIn、totalOut
long totalIn = todayStats.stream().mapToLong(s -> s.getPeopleIn() != null ? s.getPeopleIn() : 0).sum();
long totalOut = todayStats.stream().mapToLong(s -> s.getPeopleOut() != null ? s.getPeopleOut() : 0).sum();
long totalIn = dayStats.stream().mapToLong(s -> s.getPeopleIn() != null ? s.getPeopleIn() : 0).sum();
long totalOut = dayStats.stream().mapToLong(s -> s.getPeopleOut() != null ? s.getPeopleOut() : 0).sum();
// 按区域汇总明细
Map<Long, String> areaNameMap = loadAreaNameMap();
Map<Long, List<OpsTrafficStatisticsDO>> byArea = todayStats.stream()
Map<Long, List<OpsTrafficStatisticsDO>> byArea = dayStats.stream()
.filter(s -> s.getAreaId() != null)
.collect(Collectors.groupingBy(OpsTrafficStatisticsDO::getAreaId));
@@ -579,16 +566,15 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
}
// 小时趋势
HourlyTrend hourlyTrend = buildTrafficHourlyTrend(todayStats);
HourlyTrend yesterdayHourlyTrend = buildTrafficHourlyTrend(yesterdayStats);
HourlyTrend hourlyTrend = buildTrafficHourlyTrend(dayStats);
return TrafficRealtimeRespVO.builder()
.totalIn(totalIn)
.totalOut(totalOut)
.currentOccupancy(Math.max(0, totalIn - totalOut))
.areas(areas)
.statDate(date.toString())
.hourlyTrend(hourlyTrend)
.yesterdayHourlyTrend(yesterdayHourlyTrend)
.build();
}
@@ -743,48 +729,42 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
}
/**
* 构建漏斗数据(统计指定时间范围内的工单)
* 构建工单状态分布数据 -- SQL GROUP BY 聚合,避免大量数据加载到内存
*/
private List<FunnelItem> buildFunnelData(String orderType, LocalDateTime startDateTime, LocalDateTime endDateTime) {
// 创建工单 = 时间范围内创建的所有非取消工单
long totalCreated = opsOrderMapper.selectCount(
new LambdaQueryWrapperX<OpsOrderDO>()
.eq(OpsOrderDO::getOrderType, orderType)
.ne(OpsOrderDO::getStatus, "CANCELLED")
.ge(OpsOrderDO::getCreateTime, startDateTime)
.lt(OpsOrderDO::getCreateTime, endDateTime));
private List<StatusDistributionItem> buildStatusDistribution(String orderType,
LocalDateTime startDateTime,
LocalDateTime endDateTime) {
Map<String, String> statusLabelMap = new LinkedHashMap<>();
statusLabelMap.put("PENDING", "待处理");
statusLabelMap.put("QUEUED", "排队中");
statusLabelMap.put("DISPATCHED", "已派单");
statusLabelMap.put("ARRIVED", "已到岗");
statusLabelMap.put("COMPLETED", "已完成");
statusLabelMap.put("CANCELLED", "已取消");
statusLabelMap.put("PAUSED", "已暂停");
// 已分配 = 时间范围内创建且已进入工作流程的工单(排除 PENDING 和 CANCELLED
long assigned = opsOrderMapper.selectCount(
new LambdaQueryWrapperX<OpsOrderDO>()
.eq(OpsOrderDO::getOrderType, orderType)
.notIn(OpsOrderDO::getStatus, "PENDING", "CANCELLED")
.ge(OpsOrderDO::getCreateTime, startDateTime)
.lt(OpsOrderDO::getCreateTime, endDateTime));
// 使用 selectCount 按状态逐个查询状态数量有限最多7次查询避免大量数据加载到内存
Map<String, Long> statusCountMap = new HashMap<>();
for (String status : statusLabelMap.keySet()) {
Long count = opsOrderMapper.selectCount(
new LambdaQueryWrapperX<OpsOrderDO>()
.eq(OpsOrderDO::getOrderType, orderType)
.eq(OpsOrderDO::getStatus, status)
.ge(OpsOrderDO::getCreateTime, startDateTime)
.lt(OpsOrderDO::getCreateTime, endDateTime));
if (count > 0) {
statusCountMap.put(status, count);
}
}
// 已到岗 = 时间范围内创建且已到岗的工单ARRIVED + COMPLETED + PAUSED
long arrived = opsOrderMapper.selectCount(
new LambdaQueryWrapperX<OpsOrderDO>()
.eq(OpsOrderDO::getOrderType, orderType)
.in(OpsOrderDO::getStatus, "ARRIVED", "COMPLETED", "PAUSED")
.ge(OpsOrderDO::getCreateTime, startDateTime)
.lt(OpsOrderDO::getCreateTime, endDateTime));
// 已完成 = 时间范围内创建且已完成的工单
long completed = opsOrderMapper.selectCount(
new LambdaQueryWrapperX<OpsOrderDO>()
.eq(OpsOrderDO::getStatus, "COMPLETED")
.eq(OpsOrderDO::getOrderType, orderType)
.ge(OpsOrderDO::getCreateTime, startDateTime)
.lt(OpsOrderDO::getCreateTime, endDateTime));
List<FunnelItem> funnelData = new ArrayList<>();
funnelData.add(FunnelItem.builder().name("创建工单").value((int) totalCreated).build());
funnelData.add(FunnelItem.builder().name("已分配").value((int) assigned).build());
funnelData.add(FunnelItem.builder().name("已到岗").value((int) arrived).build());
funnelData.add(FunnelItem.builder().name("已完成").value((int) completed).build());
return funnelData;
List<StatusDistributionItem> result = new ArrayList<>();
for (Map.Entry<String, String> entry : statusLabelMap.entrySet()) {
Long count = statusCountMap.get(entry.getKey());
if (count != null && count > 0) {
result.add(StatusDistributionItem.builder().name(entry.getValue()).value(count.intValue()).build());
}
}
return result;
}
/**
@@ -883,6 +863,61 @@ public class OpsStatisticsServiceImpl implements OpsStatisticsService {
.collect(Collectors.toList());
}
/**
* 构建工牌队列统计近7天每天入队的工单数查 ops_order_queue 表)
* 通过关联 ops_order 表按 orderType 过滤
*/
private BadgeQueueStats buildBadgeQueueStats(String orderType, LocalDate startDate, LocalDate endDate) {
LocalDateTime startDateTime = startDate.atStartOfDay();
LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay();
// 先查询该类型在时间范围内的工单ID集合
List<OpsOrderDO> orders = opsOrderMapper.selectList(
new LambdaQueryWrapperX<OpsOrderDO>()
.select(OpsOrderDO::getId)
.eq(OpsOrderDO::getOrderType, orderType)
.ge(OpsOrderDO::getCreateTime, startDateTime)
.lt(OpsOrderDO::getCreateTime, endDateTime));
Set<Long> orderIds = orders.stream()
.map(OpsOrderDO::getId)
.collect(Collectors.toSet());
if (orderIds.isEmpty()) {
// 无工单,直接返回空数据
List<String> dates = new ArrayList<>();
List<Integer> queueData = new ArrayList<>();
for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
dates.add(d.format(DATE_FORMATTER));
queueData.add(0);
}
return BadgeQueueStats.builder().dates(dates).queueData(queueData).build();
}
// 查询队列记录按工单ID过滤
List<OpsOrderQueueDO> queueRecords =
opsOrderQueueMapper.selectList(
new LambdaQueryWrapperX<OpsOrderQueueDO>()
.in(OpsOrderQueueDO::getOpsOrderId, orderIds)
.ge(OpsOrderQueueDO::getEnqueueTime, startDateTime)
.lt(OpsOrderQueueDO::getEnqueueTime, endDateTime));
Map<String, Integer> dailyQueueMap = queueRecords.stream()
.filter(q -> q.getEnqueueTime() != null)
.collect(Collectors.groupingBy(
q -> q.getEnqueueTime().toLocalDate().toString(),
Collectors.summingInt(q -> 1)
));
List<String> dates = new ArrayList<>();
List<Integer> queueData = new ArrayList<>();
for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
dates.add(d.format(DATE_FORMATTER));
queueData.add(dailyQueueMap.getOrDefault(d.toString(), 0));
}
return BadgeQueueStats.builder().dates(dates).queueData(queueData).build();
}
/**
* 构建客流小时趋势
*/

View File

@@ -2,6 +2,7 @@ package com.viewsh.module.ops.config;
import com.viewsh.module.ops.api.badge.BadgeDeviceStatusDTO;
import com.viewsh.module.ops.dal.dataobject.area.OpsAreaDeviceRelationDO;
import com.viewsh.module.ops.enums.BadgeDeviceStatusEnum;
import com.viewsh.module.ops.environment.service.badge.BadgeDeviceStatusService;
import com.viewsh.module.ops.service.area.AreaDeviceService;
import org.springframework.beans.factory.annotation.Autowired;
@@ -32,7 +33,11 @@ public class StatisticsConfiguration {
return new int[]{0, 0};
}
List<BadgeDeviceStatusDTO> activeBadges = badgeDeviceStatusService.listActiveBadges();
int onlineCount = activeBadges.size();
// 只计算 IDLE 和 BUSY 状态的工牌作为在线数(不含 PAUSED 和 OFFLINE
int onlineCount = (int) activeBadges.stream()
.filter(b -> b.getStatus() == BadgeDeviceStatusEnum.IDLE
|| b.getStatus() == BadgeDeviceStatusEnum.BUSY)
.count();
// 从区域设备关联表查询所有已注册的工牌设备总数
int totalCount = onlineCount;
if (areaDeviceService != null) {

View File

@@ -42,23 +42,26 @@ public class OpsTrafficController {
@GetMapping("/realtime")
@Operation(summary = "全局实时客流监测")
@PreAuthorize("@ss.hasPermission('ops:traffic:query')")
public CommonResult<TrafficRealtimeRespVO> getTrafficRealtime() {
public CommonResult<TrafficRealtimeRespVO> getTrafficRealtime(
@RequestParam(value = "date", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
if (opsStatisticsService == null) {
log.warn("[getTrafficRealtime] OpsStatisticsService 未注入,返回默认值");
return success(TrafficRealtimeRespVO.builder().build());
}
return success(opsStatisticsService.getTrafficRealtime());
return success(opsStatisticsService.getTrafficRealtime(date));
}
@GetMapping("/trend")
@Operation(summary = "全局近7天客流趋势统计")
@Operation(summary = "客流趋势统计")
@PreAuthorize("@ss.hasPermission('ops:traffic:query')")
public CommonResult<TrafficTrendRespVO> getTrafficTrend() {
public CommonResult<TrafficTrendRespVO> getTrafficTrend(
@RequestParam(value = "startDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate startDate,
@RequestParam(value = "endDate", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate endDate) {
if (opsStatisticsService == null) {
log.warn("[getTrafficTrend] OpsStatisticsService 未注入,返回默认值");
return success(TrafficTrendRespVO.builder().build());
}
return success(opsStatisticsService.getTrafficTrend());
return success(opsStatisticsService.getTrafficTrend(startDate, endDate));
}
@GetMapping("/device/realtime")
@@ -79,7 +82,8 @@ public class OpsTrafficController {
@Parameter(name = "areaIds", description = "区域ID列表", required = true)
@PreAuthorize("@ss.hasPermission('ops:traffic:query')")
public CommonResult<TrafficRealtimeRespVO> getAreaTrafficRealtime(
@RequestParam("areaIds") List<Long> areaIds) {
@RequestParam("areaIds") List<Long> areaIds,
@RequestParam(value = "date", required = false) @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate date) {
if (opsStatisticsService == null) {
log.warn("[getAreaTrafficRealtime] OpsStatisticsService 未注入,返回默认值");
return success(TrafficRealtimeRespVO.builder().build());
@@ -87,7 +91,7 @@ public class OpsTrafficController {
if (areaIds.size() > 200) {
areaIds = areaIds.subList(0, 200);
}
return success(opsStatisticsService.getAreaTrafficRealtime(areaIds));
return success(opsStatisticsService.getAreaTrafficRealtime(areaIds, date));
}
@GetMapping("/device/trend")