feat(ops): 实现工单统计看板功能
1. 修复 MyBatis 类型安全问题 - 创建 9 个 DTO 类替换 List<Map<String, Object>> - 修复 @MapKey 错误,使用强类型返回值 2. 实现工单统计看板 5 大功能 - 漏斗统计:支持时间范围过滤 - 时段热力图:改为近 7 天,Y 轴显示日期(MM-dd) - 功能类型排行:替换区域排行,JOIN ops_bus_area 表 - 今日工单时段分布:X 轴优化为每 2 小时展示 - 近七天客流统计:独立接口,支持工作台实时趋势 3. 字典转换实现 - 新增 DictTypeConstants.OPS_AREA_FUNCTION_TYPE(保留供未来扩展) - 使用硬编码 Map 实现功能类型中文转换(性能最优) - 添加 TODO 说明未来可切换 DictFrameworkUtils 4. SQL 优化 - 功能类型统计:INNER JOIN ops_bus_area 表 - 热力图查询:按日期和小时分组统计 - 时段分布:仅统计当天数据 5. 缓存策略 - 看板统计:5 分钟缓存(@Cacheable) - 客流监测:5 分钟缓存 - 防止高并发查询压力 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -55,6 +55,11 @@ public class CleanWorkOrderServiceImpl implements CleanWorkOrderService {
|
|||||||
EVENT_TYPE_NAMES.put("COMPLETE", "工单完成");
|
EVENT_TYPE_NAMES.put("COMPLETE", "工单完成");
|
||||||
EVENT_TYPE_NAMES.put("CANCEL", "工单取消");
|
EVENT_TYPE_NAMES.put("CANCEL", "工单取消");
|
||||||
EVENT_TYPE_NAMES.put("UPGRADE_PRIORITY", "优先级升级");
|
EVENT_TYPE_NAMES.put("UPGRADE_PRIORITY", "优先级升级");
|
||||||
|
EVENT_TYPE_NAMES.put("ENQUEUE", "任务入队");
|
||||||
|
EVENT_TYPE_NAMES.put("QUEUED", "排队中");
|
||||||
|
EVENT_TYPE_NAMES.put("DISPATCH", "任务派发");
|
||||||
|
EVENT_TYPE_NAMES.put("DISPATCHED", "已派发");
|
||||||
|
EVENT_TYPE_NAMES.put("CONFIRMED", "已确认");
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -75,9 +80,29 @@ public class CleanWorkOrderServiceImpl implements CleanWorkOrderService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// 3. 转换为时间轴节点
|
// 3. 转换为时间轴节点
|
||||||
List<OrderTimelineRespDTO.TimelineItemDTO> timeline = events.stream()
|
List<OrderTimelineRespDTO.TimelineItemDTO> timeline = new java.util.ArrayList<>();
|
||||||
|
|
||||||
|
// 3.1 补充工单创建节点(PENDING)
|
||||||
|
boolean hasPendingNode = events.stream()
|
||||||
|
.anyMatch(e -> "PENDING".equals(e.getToStatus()) || "CREATE".equals(e.getEventType()));
|
||||||
|
|
||||||
|
if (!hasPendingNode) {
|
||||||
|
OrderTimelineRespDTO.TimelineItemDTO pendingNode = OrderTimelineRespDTO.TimelineItemDTO.builder()
|
||||||
|
.status("PENDING")
|
||||||
|
.statusName("待分配")
|
||||||
|
.time(formatDateTime(order.getCreateTime()))
|
||||||
|
.operator("系统")
|
||||||
|
.description("工单已创建")
|
||||||
|
.extra(buildPendingExtraInfo(order))
|
||||||
|
.build();
|
||||||
|
timeline.add(pendingNode);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.2 添加事件节点
|
||||||
|
List<OrderTimelineRespDTO.TimelineItemDTO> eventNodes = events.stream()
|
||||||
.map(this::convertToTimelineItem)
|
.map(this::convertToTimelineItem)
|
||||||
.toList();
|
.toList();
|
||||||
|
timeline.addAll(eventNodes);
|
||||||
|
|
||||||
// 4. 构建响应
|
// 4. 构建响应
|
||||||
return OrderTimelineRespDTO.builder()
|
return OrderTimelineRespDTO.builder()
|
||||||
@@ -195,8 +220,14 @@ public class CleanWorkOrderServiceImpl implements CleanWorkOrderService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 优先使用 toStatus,如果为空则使用 eventType
|
||||||
|
String status = event.getToStatus();
|
||||||
|
if (status == null || status.isEmpty()) {
|
||||||
|
status = event.getEventType();
|
||||||
|
}
|
||||||
|
|
||||||
return OrderTimelineRespDTO.TimelineItemDTO.builder()
|
return OrderTimelineRespDTO.TimelineItemDTO.builder()
|
||||||
.status(event.getToStatus())
|
.status(status)
|
||||||
.statusName(statusName)
|
.statusName(statusName)
|
||||||
.time(formatDateTime(event.getEventTime()))
|
.time(formatDateTime(event.getEventTime()))
|
||||||
.operator(operator)
|
.operator(operator)
|
||||||
@@ -227,4 +258,20 @@ public class CleanWorkOrderServiceImpl implements CleanWorkOrderService {
|
|||||||
}
|
}
|
||||||
return extra;
|
return extra;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建 PENDING 节点的额外信息
|
||||||
|
*/
|
||||||
|
private Map<String, Object> buildPendingExtraInfo(OpsOrderDO order) {
|
||||||
|
Map<String, Object> extra = new HashMap<>();
|
||||||
|
extra.put("event_type", "CREATE");
|
||||||
|
extra.put("operator_type", "SYSTEM");
|
||||||
|
extra.put("order_code", order.getOrderCode());
|
||||||
|
extra.put("priority", order.getPriority());
|
||||||
|
extra.put("source_type", order.getSourceType());
|
||||||
|
if (order.getTriggerSource() != null) {
|
||||||
|
extra.put("trigger_source", order.getTriggerSource());
|
||||||
|
}
|
||||||
|
return extra;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package com.viewsh.module.ops.enums;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ops 字典类型的枚举类
|
||||||
|
*
|
||||||
|
* @author lzh
|
||||||
|
*/
|
||||||
|
public interface DictTypeConstants {
|
||||||
|
|
||||||
|
// ========== OPS 模块 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 区域功能类型
|
||||||
|
* MALE_TOILET=男卫/FEMALE_TOILET=女卫/PUBLIC=公共区/ELEVATOR=电梯厅/STAIRWAY=楼梯间/OFFICE=办公区/LOBBY=大堂/PARKING=停车场
|
||||||
|
*/
|
||||||
|
String OPS_AREA_FUNCTION_TYPE = "ops_area_function_type";
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.viewsh.module.ops.controller.admin.workorder.vo.statistics;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按区域统计工单数 Response VO
|
||||||
|
*
|
||||||
|
* @author lzh
|
||||||
|
*/
|
||||||
|
@Schema(description = "管理后台 - 按区域统计工单数 Response VO")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class AreaCountRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "区域ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
|
||||||
|
private Long areaId;
|
||||||
|
|
||||||
|
@Schema(description = "总工单数", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
|
||||||
|
private Long totalCount;
|
||||||
|
|
||||||
|
@Schema(description = "已完成数", requiredMode = Schema.RequiredMode.REQUIRED, example = "15")
|
||||||
|
private Long completedCount;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package com.viewsh.module.ops.controller.admin.workorder.vo.statistics;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按日期统计平均时长 Response VO
|
||||||
|
*
|
||||||
|
* @author lzh
|
||||||
|
*/
|
||||||
|
@Schema(description = "管理后台 - 按日期统计平均时长 Response VO")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class AvgTimeRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "统计日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-02-01")
|
||||||
|
private LocalDate statDate;
|
||||||
|
|
||||||
|
@Schema(description = "平均响应时长(秒)", example = "120.5")
|
||||||
|
private Double avgResponse;
|
||||||
|
|
||||||
|
@Schema(description = "平均完成时长(秒)", example = "3600.0")
|
||||||
|
private Double avgCompletion;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.viewsh.module.ops.controller.admin.workorder.vo.statistics;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按保洁类型统计平均作业时长 Response VO
|
||||||
|
*
|
||||||
|
* @author lzh
|
||||||
|
*/
|
||||||
|
@Schema(description = "管理后台 - 按保洁类型统计平均作业时长 Response VO")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class CleaningTypeAvgDurationRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "保洁类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "ROUTINE")
|
||||||
|
private String cleaningType;
|
||||||
|
|
||||||
|
@Schema(description = "平均作业时长(分钟)", requiredMode = Schema.RequiredMode.REQUIRED, example = "35")
|
||||||
|
private Long avgDuration;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
package com.viewsh.module.ops.controller.admin.workorder.vo.statistics;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 工单看板统计 Response VO")
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class DashboardStatsRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "待处理工单数")
|
||||||
|
private Integer pendingCount;
|
||||||
|
|
||||||
|
@Schema(description = "进行中工单数")
|
||||||
|
private Integer inProgressCount;
|
||||||
|
|
||||||
|
@Schema(description = "今日完成工单数")
|
||||||
|
private Integer completedTodayCount;
|
||||||
|
|
||||||
|
@Schema(description = "历史完成工单总数")
|
||||||
|
private Integer completedTotalCount;
|
||||||
|
|
||||||
|
@Schema(description = "趋势数据")
|
||||||
|
private TrendData trendData;
|
||||||
|
|
||||||
|
@Schema(description = "今日工单时段分布(24小时)")
|
||||||
|
private HourlyDistribution hourlyDistribution;
|
||||||
|
|
||||||
|
@Schema(description = "时效趋势数据")
|
||||||
|
private TimeTrendData timeTrendData;
|
||||||
|
|
||||||
|
@Schema(description = "漏斗数据")
|
||||||
|
private List<FunnelItem> funnelData;
|
||||||
|
|
||||||
|
@Schema(description = "热力图数据(近7天)")
|
||||||
|
private HeatmapData heatmapData;
|
||||||
|
|
||||||
|
@Schema(description = "功能类型排行")
|
||||||
|
private List<FunctionTypeRankingItem> functionTypeRanking;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class TrendData {
|
||||||
|
@Schema(description = "日期列表")
|
||||||
|
private List<String> dates;
|
||||||
|
@Schema(description = "创建工单数")
|
||||||
|
private List<Integer> createdData;
|
||||||
|
@Schema(description = "完成工单数")
|
||||||
|
private List<Integer> completedData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class HourlyDistribution {
|
||||||
|
@Schema(description = "小时列表")
|
||||||
|
private List<String> hours;
|
||||||
|
@Schema(description = "工单数")
|
||||||
|
private List<Integer> data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class TimeTrendData {
|
||||||
|
@Schema(description = "日期列表")
|
||||||
|
private List<String> dates;
|
||||||
|
@Schema(description = "响应时长(分钟)")
|
||||||
|
private List<Double> responseTimeData;
|
||||||
|
@Schema(description = "完成时长(分钟)")
|
||||||
|
private List<Double> completionTimeData;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class FunnelItem {
|
||||||
|
@Schema(description = "阶段名称")
|
||||||
|
private String name;
|
||||||
|
@Schema(description = "数量")
|
||||||
|
private Integer value;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class HeatmapData {
|
||||||
|
@Schema(description = "日期列表(近7天)")
|
||||||
|
private List<String> days;
|
||||||
|
@Schema(description = "小时列表")
|
||||||
|
private List<String> hours;
|
||||||
|
@Schema(description = "热力图数据 [dayIndex][hourIndex]")
|
||||||
|
private List<List<Integer>> data;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class FunctionTypeRankingItem {
|
||||||
|
@Schema(description = "功能类型")
|
||||||
|
private String functionType;
|
||||||
|
@Schema(description = "工单总数")
|
||||||
|
private Integer count;
|
||||||
|
@Schema(description = "已完成数")
|
||||||
|
private Integer completed;
|
||||||
|
@Schema(description = "完成率(%)")
|
||||||
|
private Double rate;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
package com.viewsh.module.ops.controller.admin.workorder.vo.statistics;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按日期统计工单数 Response VO
|
||||||
|
*
|
||||||
|
* @author lzh
|
||||||
|
*/
|
||||||
|
@Schema(description = "管理后台 - 按日期统计工单数 Response VO")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class DateCountRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "统计日期", requiredMode = Schema.RequiredMode.REQUIRED, example = "2026-02-01")
|
||||||
|
private LocalDate statDate;
|
||||||
|
|
||||||
|
@Schema(description = "工单数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
|
||||||
|
private Long cnt;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.viewsh.module.ops.controller.admin.workorder.vo.statistics;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按功能类型统计工单数 Response VO
|
||||||
|
*
|
||||||
|
* @author lzh
|
||||||
|
*/
|
||||||
|
@Schema(description = "管理后台 - 按功能类型统计工单数 Response VO")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class FunctionTypeCountRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "功能类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "CLEANING")
|
||||||
|
private String functionType;
|
||||||
|
|
||||||
|
@Schema(description = "总工单数", requiredMode = Schema.RequiredMode.REQUIRED, example = "20")
|
||||||
|
private Long totalCount;
|
||||||
|
|
||||||
|
@Schema(description = "已完成数", requiredMode = Schema.RequiredMode.REQUIRED, example = "15")
|
||||||
|
private Long completedCount;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package com.viewsh.module.ops.controller.admin.workorder.vo.statistics;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按星期和小时统计工单数(热力图)Response VO
|
||||||
|
*
|
||||||
|
* @author lzh
|
||||||
|
*/
|
||||||
|
@Schema(description = "管理后台 - 按星期和小时统计工单数(热力图)Response VO")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class HeatmapRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "星期几(1=周日, 2=周一, ..., 7=周六)", requiredMode = Schema.RequiredMode.REQUIRED, example = "2")
|
||||||
|
private Integer dayOfWeek;
|
||||||
|
|
||||||
|
@Schema(description = "小时值(0-23)", requiredMode = Schema.RequiredMode.REQUIRED, example = "14")
|
||||||
|
private Integer hourVal;
|
||||||
|
|
||||||
|
@Schema(description = "工单数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "5")
|
||||||
|
private Long cnt;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
package com.viewsh.module.ops.controller.admin.workorder.vo.statistics;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按小时统计工单数 Response VO
|
||||||
|
*
|
||||||
|
* @author lzh
|
||||||
|
*/
|
||||||
|
@Schema(description = "管理后台 - 按小时统计工单数 Response VO")
|
||||||
|
@Data
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Builder
|
||||||
|
public class HourCountRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "小时值(0-23)", requiredMode = Schema.RequiredMode.REQUIRED, example = "8")
|
||||||
|
private Integer hourVal;
|
||||||
|
|
||||||
|
@Schema(description = "工单数量", requiredMode = Schema.RequiredMode.REQUIRED, example = "12")
|
||||||
|
private Long cnt;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
package com.viewsh.module.ops.controller.admin.workorder.vo.statistics;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 实时客流监测 Response VO")
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class TrafficRealtimeRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "今日总进入人数")
|
||||||
|
private Long totalIn;
|
||||||
|
|
||||||
|
@Schema(description = "今日总离开人数")
|
||||||
|
private Long totalOut;
|
||||||
|
|
||||||
|
@Schema(description = "当前在场人数")
|
||||||
|
private Long currentOccupancy;
|
||||||
|
|
||||||
|
@Schema(description = "区域客流明细")
|
||||||
|
private List<AreaTrafficItem> areas;
|
||||||
|
|
||||||
|
@Schema(description = "小时趋势")
|
||||||
|
private HourlyTrend hourlyTrend;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class AreaTrafficItem {
|
||||||
|
@Schema(description = "区域ID")
|
||||||
|
private Long areaId;
|
||||||
|
@Schema(description = "区域名称")
|
||||||
|
private String areaName;
|
||||||
|
@Schema(description = "今日进入人数")
|
||||||
|
private Long todayIn;
|
||||||
|
@Schema(description = "今日离开人数")
|
||||||
|
private Long todayOut;
|
||||||
|
@Schema(description = "当前在场人数")
|
||||||
|
private Long currentOccupancy;
|
||||||
|
@Schema(description = "设备数量")
|
||||||
|
private Integer deviceCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class HourlyTrend {
|
||||||
|
@Schema(description = "小时列表")
|
||||||
|
private List<String> hours;
|
||||||
|
@Schema(description = "进入人数")
|
||||||
|
private List<Long> inData;
|
||||||
|
@Schema(description = "离开人数")
|
||||||
|
private List<Long> outData;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package com.viewsh.module.ops.controller.admin.workorder.vo.statistics;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 近7天客流统计 Response VO
|
||||||
|
*
|
||||||
|
* @author lzh
|
||||||
|
*/
|
||||||
|
@Schema(description = "管理后台 - 近7天客流统计 Response VO")
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class TrafficTrendRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "日期列表(近7天)")
|
||||||
|
private List<String> dates;
|
||||||
|
|
||||||
|
@Schema(description = "每日进入人数")
|
||||||
|
private List<Long> inData;
|
||||||
|
|
||||||
|
@Schema(description = "每日离开人数")
|
||||||
|
private List<Long> outData;
|
||||||
|
|
||||||
|
@Schema(description = "每日净增人数")
|
||||||
|
private List<Long> netData;
|
||||||
|
|
||||||
|
@Schema(description = "总进入人数")
|
||||||
|
private Long totalIn;
|
||||||
|
|
||||||
|
@Schema(description = "总离开人数")
|
||||||
|
private Long totalOut;
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package com.viewsh.module.ops.controller.admin.workorder.vo.statistics;
|
||||||
|
|
||||||
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Builder;
|
||||||
|
import lombok.Data;
|
||||||
|
import lombok.NoArgsConstructor;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Schema(description = "管理后台 - 工作台统计 Response VO")
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public class WorkspaceStatsRespVO {
|
||||||
|
|
||||||
|
@Schema(description = "在线员工数")
|
||||||
|
private Integer onlineStaffCount;
|
||||||
|
|
||||||
|
@Schema(description = "员工总数")
|
||||||
|
private Integer totalStaffCount;
|
||||||
|
|
||||||
|
@Schema(description = "待处理工单数")
|
||||||
|
private Integer pendingCount;
|
||||||
|
|
||||||
|
@Schema(description = "平均响应时长(分钟)")
|
||||||
|
private Double avgResponseMinutes;
|
||||||
|
|
||||||
|
@Schema(description = "满意度(%)")
|
||||||
|
private Double satisfactionRate;
|
||||||
|
|
||||||
|
@Schema(description = "今日工单数")
|
||||||
|
private Integer todayOrderCount;
|
||||||
|
|
||||||
|
@Schema(description = "新增工单数(最近1小时)")
|
||||||
|
private Integer newOrderCount;
|
||||||
|
|
||||||
|
@Schema(description = "紧急任务列表")
|
||||||
|
private List<UrgentTaskItem> urgentTasks;
|
||||||
|
|
||||||
|
@Schema(description = "今日工单趋势")
|
||||||
|
private WorkOrderTrend workOrderTrend;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class UrgentTaskItem {
|
||||||
|
@Schema(description = "工单ID")
|
||||||
|
private Long id;
|
||||||
|
@Schema(description = "工单标题")
|
||||||
|
private String title;
|
||||||
|
@Schema(description = "位置")
|
||||||
|
private String location;
|
||||||
|
@Schema(description = "优先级")
|
||||||
|
private Integer priority;
|
||||||
|
@Schema(description = "状态")
|
||||||
|
private String status;
|
||||||
|
@Schema(description = "创建时间")
|
||||||
|
private LocalDateTime createTime;
|
||||||
|
@Schema(description = "执行人姓名")
|
||||||
|
private String assigneeName;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
@Builder
|
||||||
|
@NoArgsConstructor
|
||||||
|
@AllArgsConstructor
|
||||||
|
public static class WorkOrderTrend {
|
||||||
|
@Schema(description = "小时列表")
|
||||||
|
private List<String> hours;
|
||||||
|
@Schema(description = "工单数")
|
||||||
|
private List<Integer> data;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -2,9 +2,12 @@ package com.viewsh.module.ops.dal.mysql.workorder;
|
|||||||
|
|
||||||
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
|
import com.viewsh.framework.mybatis.core.mapper.BaseMapperX;
|
||||||
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
|
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.*;
|
||||||
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
|
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -71,6 +74,60 @@ public interface OpsOrderMapper extends BaseMapperX<OpsOrderDO> {
|
|||||||
.last("LIMIT 1"));
|
.last("LIMIT 1"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 统计聚合查询 ====================
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按日期分组统计工单创建数
|
||||||
|
*/
|
||||||
|
List<DateCountRespVO> selectCreatedCountGroupByDate(@Param("orderType") String orderType,
|
||||||
|
@Param("startTime") LocalDateTime startTime,
|
||||||
|
@Param("endTime") LocalDateTime endTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按日期分组统计工单完成数
|
||||||
|
*/
|
||||||
|
List<DateCountRespVO> selectCompletedCountGroupByDate(@Param("orderType") String orderType,
|
||||||
|
@Param("startTime") LocalDateTime startTime,
|
||||||
|
@Param("endTime") LocalDateTime endTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按日期分组统计平均响应时长和完成时长(秒)
|
||||||
|
*/
|
||||||
|
List<AvgTimeRespVO> selectAvgTimeGroupByDate(@Param("orderType") String orderType,
|
||||||
|
@Param("startTime") LocalDateTime startTime,
|
||||||
|
@Param("endTime") LocalDateTime endTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按区域分组统计工单数和完成数(非取消的工单)
|
||||||
|
*/
|
||||||
|
List<AreaCountRespVO> selectCountGroupByAreaId(@Param("orderType") String orderType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按小时分组统计工单创建数
|
||||||
|
*/
|
||||||
|
List<HourCountRespVO> selectCountGroupByHour(@Param("orderType") String orderType,
|
||||||
|
@Param("startTime") LocalDateTime startTime,
|
||||||
|
@Param("endTime") LocalDateTime endTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按星期和小时分组统计工单创建数(用于热力图)
|
||||||
|
* day_of_week: MySQL DAYOFWEEK 1=周日 2=周一 ... 7=周六
|
||||||
|
*/
|
||||||
|
List<HeatmapRespVO> selectCountGroupByDayOfWeekAndHour(@Param("orderType") String orderType,
|
||||||
|
@Param("startTime") LocalDateTime startTime,
|
||||||
|
@Param("endTime") LocalDateTime endTime);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按保洁类型分组统计平均作业时长(分钟)
|
||||||
|
* JOIN ops_order_clean_ext,使用 arrived_time 和 completed_time 计算
|
||||||
|
*/
|
||||||
|
List<CleaningTypeAvgDurationRespVO> selectAvgDurationGroupByCleaningType(@Param("orderType") String orderType);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按功能类型分组统计工单数和完成数(非取消的工单)
|
||||||
|
*/
|
||||||
|
List<FunctionTypeCountRespVO> selectCountGroupByFunctionType(@Param("orderType") String orderType);
|
||||||
|
|
||||||
// 注意:分页查询方法需要在Service层实现,这里只提供基础查询方法
|
// 注意:分页查询方法需要在Service层实现,这里只提供基础查询方法
|
||||||
// 具体分页查询请参考Service实现
|
// 具体分页查询请参考Service实现
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package com.viewsh.module.ops.service.statistics;
|
||||||
|
|
||||||
|
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.DashboardStatsRespVO;
|
||||||
|
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.TrafficRealtimeRespVO;
|
||||||
|
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.TrafficTrendRespVO;
|
||||||
|
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.WorkspaceStatsRespVO;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工单统计服务接口
|
||||||
|
*
|
||||||
|
* @author lzh
|
||||||
|
*/
|
||||||
|
public interface OpsStatisticsService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取看板统计数据
|
||||||
|
*
|
||||||
|
* @param orderType 工单类型,默认 CLEAN
|
||||||
|
* @param startDate 开始日期
|
||||||
|
* @param endDate 结束日期
|
||||||
|
* @return 看板统计数据
|
||||||
|
*/
|
||||||
|
DashboardStatsRespVO getDashboardStats(String orderType, LocalDate startDate, LocalDate endDate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取实时客流监测数据
|
||||||
|
*
|
||||||
|
* @return 实时客流数据
|
||||||
|
*/
|
||||||
|
TrafficRealtimeRespVO getTrafficRealtime();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取工作台统计数据
|
||||||
|
*
|
||||||
|
* @return 工作台统计数据
|
||||||
|
*/
|
||||||
|
WorkspaceStatsRespVO getWorkspaceStats();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取近7天客流趋势统计
|
||||||
|
*
|
||||||
|
* @return 近7天客流趋势数据
|
||||||
|
*/
|
||||||
|
TrafficTrendRespVO getTrafficTrend();
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,713 @@
|
|||||||
|
package com.viewsh.module.ops.service.statistics;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.viewsh.framework.mybatis.core.query.LambdaQueryWrapperX;
|
||||||
|
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.*;
|
||||||
|
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.DashboardStatsRespVO.*;
|
||||||
|
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.TrafficRealtimeRespVO.AreaTrafficItem;
|
||||||
|
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.TrafficRealtimeRespVO.HourlyTrend;
|
||||||
|
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.WorkspaceStatsRespVO.UrgentTaskItem;
|
||||||
|
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.WorkspaceStatsRespVO.WorkOrderTrend;
|
||||||
|
import com.viewsh.module.ops.dal.dataobject.area.OpsBusAreaDO;
|
||||||
|
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.mysql.workorder.OpsOrderMapper;
|
||||||
|
import jakarta.annotation.Resource;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.cache.annotation.Cacheable;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.DayOfWeek;
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 工单统计服务实现
|
||||||
|
*
|
||||||
|
* @author lzh
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
public class OpsStatisticsServiceImpl implements OpsStatisticsService {
|
||||||
|
|
||||||
|
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("MM-dd");
|
||||||
|
|
||||||
|
private static final List<String> IN_PROGRESS_STATUSES = List.of("DISPATCHED", "CONFIRMED", "ARRIVED", "QUEUED");
|
||||||
|
|
||||||
|
// TODO: 功能类型字典映射(用于API返回的中文转换)
|
||||||
|
// 当前使用硬编码 Map,性能最优且功能类型相对固定
|
||||||
|
// 如果未来需要动态管理字典,可考虑:
|
||||||
|
// 1. 使用 DictFrameworkUtils.parseDictDataLabel(DictTypeConstants.OPS_AREA_FUNCTION_TYPE, value)
|
||||||
|
// 2. 从系统字典表动态加载并缓存
|
||||||
|
private static final Map<String, String> FUNCTION_TYPE_DICT = Map.of(
|
||||||
|
"MALE_TOILET", "男卫",
|
||||||
|
"FEMALE_TOILET", "女卫",
|
||||||
|
"PUBLIC", "公共区",
|
||||||
|
"ELEVATOR", "电梯厅",
|
||||||
|
"STAIRWAY", "楼梯间",
|
||||||
|
"OFFICE", "办公区",
|
||||||
|
"LOBBY", "大堂",
|
||||||
|
"PARKING", "停车场"
|
||||||
|
);
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private OpsOrderMapper opsOrderMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private OpsBusAreaMapper opsBusAreaMapper;
|
||||||
|
|
||||||
|
@Resource
|
||||||
|
private OpsTrafficStatisticsMapper trafficStatisticsMapper;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 在线员工数提供者(由 ops-server 层注入,从 environment-biz 获取工牌状态)
|
||||||
|
* 返回 [onlineCount, totalCount]
|
||||||
|
*/
|
||||||
|
@Autowired(required = false)
|
||||||
|
private java.util.function.Supplier<int[]> staffCountProvider;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Cacheable(value = "ops:statistics:dashboard#5m", key = "#orderType + ':' + #startDate + ':' + #endDate",
|
||||||
|
unless = "#result == null")
|
||||||
|
public DashboardStatsRespVO getDashboardStats(String orderType, LocalDate startDate, LocalDate endDate) {
|
||||||
|
LocalDateTime startDateTime = startDate.atStartOfDay();
|
||||||
|
LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay();
|
||||||
|
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
|
||||||
|
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||||
|
|
||||||
|
// 1. 待处理工单数
|
||||||
|
Integer pendingCount = countByStatusAndType("PENDING", orderType);
|
||||||
|
|
||||||
|
// 2. 进行中工单数
|
||||||
|
Integer inProgressCount = Math.toIntExact(opsOrderMapper.selectCount(
|
||||||
|
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||||
|
.in(OpsOrderDO::getStatus, IN_PROGRESS_STATUSES)
|
||||||
|
.eq(OpsOrderDO::getOrderType, orderType)));
|
||||||
|
|
||||||
|
// 3. 今日完成数(使用 ge + lt 避免边界重叠)
|
||||||
|
Integer completedTodayCount = Math.toIntExact(opsOrderMapper.selectCount(
|
||||||
|
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||||
|
.eq(OpsOrderDO::getStatus, "COMPLETED")
|
||||||
|
.eq(OpsOrderDO::getOrderType, orderType)
|
||||||
|
.ge(OpsOrderDO::getEndTime, todayStart)
|
||||||
|
.lt(OpsOrderDO::getEndTime, todayEnd)));
|
||||||
|
|
||||||
|
// 4. 历史完成总数
|
||||||
|
Integer completedTotalCount = Math.toIntExact(opsOrderMapper.selectCount(
|
||||||
|
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||||
|
.eq(OpsOrderDO::getStatus, "COMPLETED")
|
||||||
|
.eq(OpsOrderDO::getOrderType, orderType)));
|
||||||
|
|
||||||
|
// 5. 趋势数据(单次 GROUP BY 查询)
|
||||||
|
TrendData trendData = buildTrendData(orderType, startDate, endDate);
|
||||||
|
|
||||||
|
// 6. 今日工单时段分布(24小时)
|
||||||
|
HourlyDistribution hourlyDistribution = buildHourlyDistribution(orderType, todayStart, todayEnd);
|
||||||
|
|
||||||
|
// 7. 时效趋势(单次 GROUP BY 查询)
|
||||||
|
TimeTrendData timeTrendData = buildTimeTrendData(orderType, startDate, endDate);
|
||||||
|
|
||||||
|
// 8. 漏斗数据(基于指定时间范围的工单状态推算)
|
||||||
|
List<FunnelItem> funnelData = buildFunnelData(orderType, startDateTime, endDateTime);
|
||||||
|
|
||||||
|
// 9. 热力图(近7天)
|
||||||
|
LocalDate sevenDaysAgo = LocalDate.now().minusDays(6); // 包含今天共7天
|
||||||
|
LocalDateTime sevenDaysAgoStart = sevenDaysAgo.atStartOfDay();
|
||||||
|
LocalDateTime nowEnd = LocalDate.now().plusDays(1).atStartOfDay();
|
||||||
|
HeatmapData heatmapData = buildHeatmapData(orderType, sevenDaysAgoStart, nowEnd);
|
||||||
|
|
||||||
|
// 10. 功能类型排行(单次 GROUP BY 查询)
|
||||||
|
List<FunctionTypeRankingItem> functionTypeRanking = buildFunctionTypeRanking(orderType);
|
||||||
|
|
||||||
|
return DashboardStatsRespVO.builder()
|
||||||
|
.pendingCount(pendingCount)
|
||||||
|
.inProgressCount(inProgressCount)
|
||||||
|
.completedTodayCount(completedTodayCount)
|
||||||
|
.completedTotalCount(completedTotalCount)
|
||||||
|
.trendData(trendData)
|
||||||
|
.hourlyDistribution(hourlyDistribution)
|
||||||
|
.timeTrendData(timeTrendData)
|
||||||
|
.funnelData(funnelData)
|
||||||
|
.heatmapData(heatmapData)
|
||||||
|
.functionTypeRanking(functionTypeRanking)
|
||||||
|
.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);
|
||||||
|
|
||||||
|
// 从 ops_traffic_statistics 表查询今日汇总(使用 ge + lt 避免边界重叠)
|
||||||
|
List<OpsTrafficStatisticsDO> todayStats = trafficStatisticsMapper.selectList(
|
||||||
|
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
|
||||||
|
.ge(OpsTrafficStatisticsDO::getStatHour, todayStart)
|
||||||
|
.lt(OpsTrafficStatisticsDO::getStatHour, todayEnd));
|
||||||
|
|
||||||
|
// 加载区域名称映射
|
||||||
|
Map<Long, String> areaNameMap = loadAreaNameMap();
|
||||||
|
|
||||||
|
// 按区域汇总
|
||||||
|
Map<Long, List<OpsTrafficStatisticsDO>> byArea = todayStats.stream()
|
||||||
|
.filter(s -> s.getAreaId() != null)
|
||||||
|
.collect(Collectors.groupingBy(OpsTrafficStatisticsDO::getAreaId));
|
||||||
|
|
||||||
|
long totalIn = 0;
|
||||||
|
long totalOut = 0;
|
||||||
|
List<AreaTrafficItem> areas = new ArrayList<>();
|
||||||
|
|
||||||
|
for (Map.Entry<Long, List<OpsTrafficStatisticsDO>> entry : byArea.entrySet()) {
|
||||||
|
Long areaId = entry.getKey();
|
||||||
|
List<OpsTrafficStatisticsDO> stats = entry.getValue();
|
||||||
|
long areaIn = stats.stream().mapToLong(s -> s.getPeopleIn() != null ? s.getPeopleIn() : 0).sum();
|
||||||
|
long areaOut = stats.stream().mapToLong(s -> s.getPeopleOut() != null ? s.getPeopleOut() : 0).sum();
|
||||||
|
int deviceCount = (int) stats.stream().map(OpsTrafficStatisticsDO::getDeviceId).distinct().count();
|
||||||
|
totalIn += areaIn;
|
||||||
|
totalOut += areaOut;
|
||||||
|
|
||||||
|
areas.add(AreaTrafficItem.builder()
|
||||||
|
.areaId(areaId)
|
||||||
|
.areaName(areaNameMap.getOrDefault(areaId, "未知区域"))
|
||||||
|
.todayIn(areaIn)
|
||||||
|
.todayOut(areaOut)
|
||||||
|
.currentOccupancy(Math.max(0, areaIn - areaOut))
|
||||||
|
.deviceCount(deviceCount)
|
||||||
|
.build());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 小时趋势
|
||||||
|
HourlyTrend hourlyTrend = buildTrafficHourlyTrend(todayStats);
|
||||||
|
|
||||||
|
return TrafficRealtimeRespVO.builder()
|
||||||
|
.totalIn(totalIn)
|
||||||
|
.totalOut(totalOut)
|
||||||
|
.currentOccupancy(Math.max(0, totalIn - totalOut))
|
||||||
|
.areas(areas)
|
||||||
|
.hourlyTrend(hourlyTrend)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Cacheable(value = "ops:statistics:workspace#5m", unless = "#result == null")
|
||||||
|
public WorkspaceStatsRespVO getWorkspaceStats() {
|
||||||
|
String orderType = "CLEAN";
|
||||||
|
LocalDateTime todayStart = LocalDate.now().atStartOfDay();
|
||||||
|
LocalDateTime todayEnd = todayStart.plusDays(1);
|
||||||
|
LocalDateTime oneHourAgo = LocalDateTime.now().minusHours(1);
|
||||||
|
|
||||||
|
// 1. 在线/总员工数
|
||||||
|
int onlineStaffCount = 0;
|
||||||
|
int totalStaffCount = 0;
|
||||||
|
if (staffCountProvider != null) {
|
||||||
|
try {
|
||||||
|
int[] counts = staffCountProvider.get();
|
||||||
|
onlineStaffCount = counts[0];
|
||||||
|
totalStaffCount = counts[1];
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("[getWorkspaceStats] 查询员工数量失败", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 待处理工单数
|
||||||
|
Integer pendingCount = countByStatusAndType("PENDING", orderType);
|
||||||
|
|
||||||
|
// 3. 平均响应时长(今日已完成的工单,使用 ge + lt 避免边界重叠)
|
||||||
|
List<OpsOrderDO> completedToday = opsOrderMapper.selectList(
|
||||||
|
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||||
|
.eq(OpsOrderDO::getStatus, "COMPLETED")
|
||||||
|
.eq(OpsOrderDO::getOrderType, orderType)
|
||||||
|
.ge(OpsOrderDO::getEndTime, todayStart)
|
||||||
|
.lt(OpsOrderDO::getEndTime, todayEnd)
|
||||||
|
.isNotNull(OpsOrderDO::getResponseSeconds));
|
||||||
|
Double avgResponseMinutes = completedToday.stream()
|
||||||
|
.filter(o -> o.getResponseSeconds() != null && o.getResponseSeconds() > 0)
|
||||||
|
.mapToInt(OpsOrderDO::getResponseSeconds)
|
||||||
|
.average()
|
||||||
|
.orElse(0.0) / 60.0;
|
||||||
|
avgResponseMinutes = Math.round(avgResponseMinutes * 10) / 10.0;
|
||||||
|
|
||||||
|
// 4. 满意度(基于评分,qualityScore >= 4 视为满意,使用 ge + lt 避免边界重叠)
|
||||||
|
List<OpsOrderDO> ratedOrders = opsOrderMapper.selectList(
|
||||||
|
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||||
|
.eq(OpsOrderDO::getOrderType, orderType)
|
||||||
|
.isNotNull(OpsOrderDO::getQualityScore)
|
||||||
|
.ge(OpsOrderDO::getEndTime, todayStart.minusDays(30))
|
||||||
|
.lt(OpsOrderDO::getEndTime, todayEnd));
|
||||||
|
double satisfactionRate = 0.0;
|
||||||
|
if (!ratedOrders.isEmpty()) {
|
||||||
|
long satisfiedCount = ratedOrders.stream()
|
||||||
|
.filter(o -> o.getQualityScore() != null && o.getQualityScore() >= 4)
|
||||||
|
.count();
|
||||||
|
satisfactionRate = Math.round(satisfiedCount * 1000.0 / ratedOrders.size()) / 10.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5. 今日工单数(使用 ge + lt 避免边界重叠)
|
||||||
|
Integer todayOrderCount = Math.toIntExact(opsOrderMapper.selectCount(
|
||||||
|
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||||
|
.eq(OpsOrderDO::getOrderType, orderType)
|
||||||
|
.ge(OpsOrderDO::getCreateTime, todayStart)
|
||||||
|
.lt(OpsOrderDO::getCreateTime, todayEnd)));
|
||||||
|
|
||||||
|
// 6. 新增工单数(最近1小时)
|
||||||
|
Integer newOrderCount = Math.toIntExact(opsOrderMapper.selectCount(
|
||||||
|
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||||
|
.eq(OpsOrderDO::getOrderType, orderType)
|
||||||
|
.ge(OpsOrderDO::getCreateTime, oneHourAgo)));
|
||||||
|
|
||||||
|
// 7. 紧急任务列表(P0 + 待处理/进行中)
|
||||||
|
List<OpsOrderDO> urgentOrders = opsOrderMapper.selectList(
|
||||||
|
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||||
|
.eq(OpsOrderDO::getOrderType, orderType)
|
||||||
|
.eq(OpsOrderDO::getPriority, 0)
|
||||||
|
.notIn(OpsOrderDO::getStatus, "COMPLETED", "CANCELLED")
|
||||||
|
.orderByDesc(OpsOrderDO::getCreateTime)
|
||||||
|
.last("LIMIT 10"));
|
||||||
|
List<UrgentTaskItem> urgentTasks = urgentOrders.stream()
|
||||||
|
.map(o -> UrgentTaskItem.builder()
|
||||||
|
.id(o.getId())
|
||||||
|
.title(o.getTitle())
|
||||||
|
.location(o.getLocation())
|
||||||
|
.priority(o.getPriority())
|
||||||
|
.status(o.getStatus())
|
||||||
|
.createTime(o.getCreateTime())
|
||||||
|
.assigneeName(o.getAssigneeName())
|
||||||
|
.build())
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
|
||||||
|
// 8. 今日工单趋势(按小时)
|
||||||
|
WorkOrderTrend workOrderTrend = buildWorkOrderTrend(orderType, todayStart, todayEnd);
|
||||||
|
|
||||||
|
return WorkspaceStatsRespVO.builder()
|
||||||
|
.onlineStaffCount(onlineStaffCount)
|
||||||
|
.totalStaffCount(totalStaffCount)
|
||||||
|
.pendingCount(pendingCount)
|
||||||
|
.avgResponseMinutes(avgResponseMinutes)
|
||||||
|
.satisfactionRate(satisfactionRate)
|
||||||
|
.todayOrderCount(todayOrderCount)
|
||||||
|
.newOrderCount(newOrderCount)
|
||||||
|
.urgentTasks(urgentTasks)
|
||||||
|
.workOrderTrend(workOrderTrend)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Cacheable(value = "ops:statistics:traffic-trend#5m", unless = "#result == null")
|
||||||
|
public TrafficTrendRespVO getTrafficTrend() {
|
||||||
|
LocalDate today = LocalDate.now();
|
||||||
|
LocalDate sevenDaysAgo = today.minusDays(6); // 包含今天共7天
|
||||||
|
|
||||||
|
// 从 ops_traffic_statistics 表查询近7天的客流数据
|
||||||
|
LocalDateTime startDateTime = sevenDaysAgo.atStartOfDay();
|
||||||
|
LocalDateTime endDateTime = today.plusDays(1).atStartOfDay();
|
||||||
|
|
||||||
|
List<OpsTrafficStatisticsDO> stats = trafficStatisticsMapper.selectList(
|
||||||
|
new LambdaQueryWrapperX<OpsTrafficStatisticsDO>()
|
||||||
|
.ge(OpsTrafficStatisticsDO::getStatHour, startDateTime)
|
||||||
|
.lt(OpsTrafficStatisticsDO::getStatHour, endDateTime));
|
||||||
|
|
||||||
|
// 按日期汇总
|
||||||
|
Map<LocalDate, Long> dailyInMap = new HashMap<>();
|
||||||
|
Map<LocalDate, Long> dailyOutMap = new HashMap<>();
|
||||||
|
|
||||||
|
for (OpsTrafficStatisticsDO stat : stats) {
|
||||||
|
if (stat.getStatHour() != null) {
|
||||||
|
LocalDate date = stat.getStatHour().toLocalDate();
|
||||||
|
long peopleIn = stat.getPeopleIn() != null ? stat.getPeopleIn() : 0;
|
||||||
|
long peopleOut = stat.getPeopleOut() != null ? stat.getPeopleOut() : 0;
|
||||||
|
|
||||||
|
dailyInMap.put(date, dailyInMap.getOrDefault(date, 0L) + peopleIn);
|
||||||
|
dailyOutMap.put(date, dailyOutMap.getOrDefault(date, 0L) + peopleOut);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建返回数据(按日期排序,补零)
|
||||||
|
List<String> dates = new ArrayList<>();
|
||||||
|
List<Long> inData = new ArrayList<>();
|
||||||
|
List<Long> outData = new ArrayList<>();
|
||||||
|
List<Long> netData = new ArrayList<>();
|
||||||
|
|
||||||
|
long totalIn = 0;
|
||||||
|
long totalOut = 0;
|
||||||
|
|
||||||
|
for (LocalDate d = sevenDaysAgo; !d.isAfter(today); d = d.plusDays(1)) {
|
||||||
|
dates.add(d.format(DATE_FORMATTER));
|
||||||
|
long dayIn = dailyInMap.getOrDefault(d, 0L);
|
||||||
|
long dayOut = dailyOutMap.getOrDefault(d, 0L);
|
||||||
|
long dayNet = dayIn - dayOut;
|
||||||
|
|
||||||
|
inData.add(dayIn);
|
||||||
|
outData.add(dayOut);
|
||||||
|
netData.add(dayNet);
|
||||||
|
|
||||||
|
totalIn += dayIn;
|
||||||
|
totalOut += dayOut;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TrafficTrendRespVO.builder()
|
||||||
|
.dates(dates)
|
||||||
|
.inData(inData)
|
||||||
|
.outData(outData)
|
||||||
|
.netData(netData)
|
||||||
|
.totalIn(totalIn)
|
||||||
|
.totalOut(totalOut)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ==================== 私有方法 ====================
|
||||||
|
|
||||||
|
private Integer countByStatusAndType(String status, String orderType) {
|
||||||
|
return Math.toIntExact(opsOrderMapper.selectCount(
|
||||||
|
new LambdaQueryWrapperX<OpsOrderDO>()
|
||||||
|
.eq(OpsOrderDO::getStatus, status)
|
||||||
|
.eq(OpsOrderDO::getOrderType, orderType)));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建趋势数据(创建数/完成数 按日)-- 单次 GROUP BY 查询 + Java 补零
|
||||||
|
*/
|
||||||
|
private TrendData buildTrendData(String orderType, LocalDate startDate, LocalDate endDate) {
|
||||||
|
LocalDateTime startDateTime = startDate.atStartOfDay();
|
||||||
|
LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay();
|
||||||
|
|
||||||
|
// 一次查询获取所有天的创建数
|
||||||
|
List<DateCountRespVO> createdRows = opsOrderMapper.selectCreatedCountGroupByDate(
|
||||||
|
orderType, startDateTime, endDateTime);
|
||||||
|
Map<String, Integer> createdMap = createdRows.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
row -> row.getStatDate().toString(),
|
||||||
|
row -> row.getCnt().intValue(),
|
||||||
|
(a, b) -> a
|
||||||
|
));
|
||||||
|
|
||||||
|
// 一次查询获取所有天的完成数
|
||||||
|
List<DateCountRespVO> completedRows = opsOrderMapper.selectCompletedCountGroupByDate(
|
||||||
|
orderType, startDateTime, endDateTime);
|
||||||
|
Map<String, Integer> completedMap = completedRows.stream()
|
||||||
|
.collect(Collectors.toMap(
|
||||||
|
row -> row.getStatDate().toString(),
|
||||||
|
row -> row.getCnt().intValue(),
|
||||||
|
(a, b) -> a
|
||||||
|
));
|
||||||
|
|
||||||
|
// 按日期遍历,补零
|
||||||
|
List<String> dates = new ArrayList<>();
|
||||||
|
List<Integer> createdData = new ArrayList<>();
|
||||||
|
List<Integer> completedData = new ArrayList<>();
|
||||||
|
|
||||||
|
for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
|
||||||
|
String dateKey = d.toString(); // yyyy-MM-dd
|
||||||
|
dates.add(d.format(DATE_FORMATTER));
|
||||||
|
createdData.add(createdMap.getOrDefault(dateKey, 0));
|
||||||
|
completedData.add(completedMap.getOrDefault(dateKey, 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return TrendData.builder()
|
||||||
|
.dates(dates)
|
||||||
|
.createdData(createdData)
|
||||||
|
.completedData(completedData)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建时段分布(24小时)-- 单次 GROUP BY HOUR 查询
|
||||||
|
*/
|
||||||
|
private HourlyDistribution buildHourlyDistribution(String orderType,
|
||||||
|
LocalDateTime startDateTime,
|
||||||
|
LocalDateTime endDateTime) {
|
||||||
|
List<HourCountRespVO> rows = opsOrderMapper.selectCountGroupByHour(
|
||||||
|
orderType, startDateTime, endDateTime);
|
||||||
|
|
||||||
|
// 构建小时 -> 计数映射
|
||||||
|
int[] hourBuckets = new int[24];
|
||||||
|
for (HourCountRespVO row : rows) {
|
||||||
|
int hour = row.getHourVal();
|
||||||
|
int cnt = row.getCnt().intValue();
|
||||||
|
if (hour >= 0 && hour < 24) {
|
||||||
|
hourBuckets[hour] = cnt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> hours = new ArrayList<>();
|
||||||
|
List<Integer> data = new ArrayList<>();
|
||||||
|
for (int h = 0; h < 24; h++) {
|
||||||
|
hours.add(String.format("%02d:00", h));
|
||||||
|
data.add(hourBuckets[h]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HourlyDistribution.builder().hours(hours).data(data).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建时效趋势(响应时长/完成时长 按日)-- 单次 GROUP BY 查询 + Java 补零
|
||||||
|
*/
|
||||||
|
private TimeTrendData buildTimeTrendData(String orderType, LocalDate startDate, LocalDate endDate) {
|
||||||
|
LocalDateTime startDateTime = startDate.atStartOfDay();
|
||||||
|
LocalDateTime endDateTime = endDate.plusDays(1).atStartOfDay();
|
||||||
|
|
||||||
|
List<AvgTimeRespVO> rows = opsOrderMapper.selectAvgTimeGroupByDate(
|
||||||
|
orderType, startDateTime, endDateTime);
|
||||||
|
|
||||||
|
// 构建日期 -> 响应/完成时长映射
|
||||||
|
Map<String, Double> responseMap = new HashMap<>();
|
||||||
|
Map<String, Double> completionMap = new HashMap<>();
|
||||||
|
for (AvgTimeRespVO row : rows) {
|
||||||
|
String dateKey = row.getStatDate().toString();
|
||||||
|
if (row.getAvgResponse() != null) {
|
||||||
|
double responseMinutes = row.getAvgResponse() / 60.0;
|
||||||
|
responseMap.put(dateKey, Math.round(responseMinutes * 10) / 10.0);
|
||||||
|
}
|
||||||
|
if (row.getAvgCompletion() != null) {
|
||||||
|
double completionMinutes = row.getAvgCompletion() / 60.0;
|
||||||
|
completionMap.put(dateKey, Math.round(completionMinutes * 10) / 10.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按日期遍历,补零
|
||||||
|
List<String> dates = new ArrayList<>();
|
||||||
|
List<Double> responseTimeData = new ArrayList<>();
|
||||||
|
List<Double> completionTimeData = new ArrayList<>();
|
||||||
|
|
||||||
|
for (LocalDate d = startDate; !d.isAfter(endDate); d = d.plusDays(1)) {
|
||||||
|
String dateKey = d.toString();
|
||||||
|
dates.add(d.format(DATE_FORMATTER));
|
||||||
|
responseTimeData.add(responseMap.getOrDefault(dateKey, 0.0));
|
||||||
|
completionTimeData.add(completionMap.getOrDefault(dateKey, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
return TimeTrendData.builder()
|
||||||
|
.dates(dates)
|
||||||
|
.responseTimeData(responseTimeData)
|
||||||
|
.completionTimeData(completionTimeData)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建漏斗数据(统计指定时间范围内的工单)
|
||||||
|
*/
|
||||||
|
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));
|
||||||
|
|
||||||
|
// 已分配 = 时间范围内创建且已进入工作流程的工单(排除 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));
|
||||||
|
|
||||||
|
// 已到岗 = 时间范围内创建且已到岗的工单(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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建热力图数据(日期 x 小时)-- 单次 GROUP BY 查询
|
||||||
|
* 显示近7天的具体日期,而不是周几
|
||||||
|
*/
|
||||||
|
private HeatmapData buildHeatmapData(String orderType,
|
||||||
|
LocalDateTime startDateTime,
|
||||||
|
LocalDateTime endDateTime) {
|
||||||
|
List<HeatmapRespVO> rows = opsOrderMapper.selectCountGroupByDayOfWeekAndHour(
|
||||||
|
orderType, startDateTime, endDateTime);
|
||||||
|
|
||||||
|
// 生成近7天的日期列表(格式:MM-dd)
|
||||||
|
LocalDate startDate = startDateTime.toLocalDate();
|
||||||
|
List<String> days = new ArrayList<>();
|
||||||
|
Map<LocalDate, Integer> dateIndexMap = new HashMap<>();
|
||||||
|
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("MM-dd");
|
||||||
|
|
||||||
|
for (int i = 0; i < 7; i++) {
|
||||||
|
LocalDate date = startDate.plusDays(i);
|
||||||
|
days.add(date.format(formatter));
|
||||||
|
dateIndexMap.put(date, i);
|
||||||
|
}
|
||||||
|
|
||||||
|
// [date 0-6][hour 0-23]
|
||||||
|
int[][] matrix = new int[7][24];
|
||||||
|
for (HeatmapRespVO row : rows) {
|
||||||
|
int hour = row.getHourVal();
|
||||||
|
int cnt = row.getCnt().intValue();
|
||||||
|
|
||||||
|
// 根据 dayOfWeek 和 startDate 计算实际日期
|
||||||
|
int mysqlDow = row.getDayOfWeek();
|
||||||
|
DayOfWeek dayOfWeek = mysqlDow == 1 ? DayOfWeek.SUNDAY : DayOfWeek.of(mysqlDow - 1);
|
||||||
|
|
||||||
|
// 在7天范围内查找匹配的日期
|
||||||
|
for (int i = 0; i < 7; i++) {
|
||||||
|
LocalDate date = startDate.plusDays(i);
|
||||||
|
if (date.getDayOfWeek() == dayOfWeek) {
|
||||||
|
Integer dayIndex = dateIndexMap.get(date);
|
||||||
|
if (dayIndex != null && hour >= 0 && hour < 24) {
|
||||||
|
matrix[dayIndex][hour] += cnt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> hours = new ArrayList<>();
|
||||||
|
for (int h = 0; h < 24; h++) {
|
||||||
|
hours.add(String.format("%d:00", h));
|
||||||
|
}
|
||||||
|
|
||||||
|
List<List<Integer>> data = new ArrayList<>();
|
||||||
|
for (int d = 0; d < 7; d++) {
|
||||||
|
List<Integer> dayData = new ArrayList<>();
|
||||||
|
for (int h = 0; h < 24; h++) {
|
||||||
|
dayData.add(matrix[d][h]);
|
||||||
|
}
|
||||||
|
data.add(dayData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HeatmapData.builder()
|
||||||
|
.days(days)
|
||||||
|
.hours(hours)
|
||||||
|
.data(data)
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建功能类型排行 -- 单次 GROUP BY function_type 查询
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 构建功能类型排行(将枚举值转换为中文)
|
||||||
|
*/
|
||||||
|
private List<FunctionTypeRankingItem> buildFunctionTypeRanking(String orderType) {
|
||||||
|
List<FunctionTypeCountRespVO> rows = opsOrderMapper.selectCountGroupByFunctionType(orderType);
|
||||||
|
|
||||||
|
return rows.stream()
|
||||||
|
.map(row -> {
|
||||||
|
int total = row.getTotalCount().intValue();
|
||||||
|
int completedCount = row.getCompletedCount().intValue();
|
||||||
|
double rate = total > 0 ? Math.round(completedCount * 1000.0 / total) / 10.0 : 0.0;
|
||||||
|
|
||||||
|
// 转换枚举值为中文(用于JSON API响应)
|
||||||
|
String functionTypeLabel = FUNCTION_TYPE_DICT.getOrDefault(
|
||||||
|
row.getFunctionType(),
|
||||||
|
row.getFunctionType() // 如果映射不存在,保留原值
|
||||||
|
);
|
||||||
|
|
||||||
|
return FunctionTypeRankingItem.builder()
|
||||||
|
.functionType(functionTypeLabel)
|
||||||
|
.count(total)
|
||||||
|
.completed(completedCount)
|
||||||
|
.rate(rate)
|
||||||
|
.build();
|
||||||
|
})
|
||||||
|
.collect(Collectors.toList());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建客流小时趋势
|
||||||
|
*/
|
||||||
|
private HourlyTrend buildTrafficHourlyTrend(List<OpsTrafficStatisticsDO> todayStats) {
|
||||||
|
long[] hourIn = new long[24];
|
||||||
|
long[] hourOut = new long[24];
|
||||||
|
|
||||||
|
for (OpsTrafficStatisticsDO stat : todayStats) {
|
||||||
|
if (stat.getStatHour() != null) {
|
||||||
|
int hour = stat.getStatHour().getHour();
|
||||||
|
hourIn[hour] += stat.getPeopleIn() != null ? stat.getPeopleIn() : 0;
|
||||||
|
hourOut[hour] += stat.getPeopleOut() != null ? stat.getPeopleOut() : 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
List<String> hours = new ArrayList<>();
|
||||||
|
List<Long> inData = new ArrayList<>();
|
||||||
|
List<Long> outData = new ArrayList<>();
|
||||||
|
// 每2小时显示一个点,避免横坐标太挤
|
||||||
|
for (int h = 0; h < 24; h += 2) {
|
||||||
|
hours.add(String.format("%02d:00", h));
|
||||||
|
inData.add(hourIn[h]);
|
||||||
|
outData.add(hourOut[h]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return HourlyTrend.builder().hours(hours).inData(inData).outData(outData).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 构建今日工单趋势(按小时)-- 使用 GROUP BY HOUR 查询
|
||||||
|
*/
|
||||||
|
/**
|
||||||
|
* 构建今日工单趋势(按小时,优化展示:每2小时)
|
||||||
|
*/
|
||||||
|
private WorkOrderTrend buildWorkOrderTrend(String orderType,
|
||||||
|
LocalDateTime todayStart,
|
||||||
|
LocalDateTime todayEnd) {
|
||||||
|
List<HourCountRespVO> rows = opsOrderMapper.selectCountGroupByHour(
|
||||||
|
orderType, todayStart, todayEnd);
|
||||||
|
|
||||||
|
int[] hourBuckets = new int[24];
|
||||||
|
for (HourCountRespVO row : rows) {
|
||||||
|
int hour = row.getHourVal();
|
||||||
|
int cnt = row.getCnt().intValue();
|
||||||
|
if (hour >= 0 && hour < 24) {
|
||||||
|
hourBuckets[hour] = cnt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int currentHour = LocalDateTime.now().getHour();
|
||||||
|
List<String> hours = new ArrayList<>();
|
||||||
|
List<Integer> data = new ArrayList<>();
|
||||||
|
|
||||||
|
// 每2小时显示一个点,避免横坐标太挤
|
||||||
|
for (int h = 0; h <= currentHour; h += 2) {
|
||||||
|
hours.add(String.format("%02d:00", h));
|
||||||
|
data.add(hourBuckets[h]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果当前小时是奇数,补充最后一个点
|
||||||
|
if (currentHour % 2 == 1 && currentHour > 0) {
|
||||||
|
hours.add(String.format("%02d:00", currentHour));
|
||||||
|
data.add(hourBuckets[currentHour]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WorkOrderTrend.builder().hours(hours).data(data).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 加载区域名称映射
|
||||||
|
*/
|
||||||
|
private Map<Long, String> loadAreaNameMap() {
|
||||||
|
List<OpsBusAreaDO> areas = opsBusAreaMapper.selectList(new LambdaQueryWrapper<>());
|
||||||
|
return areas.stream()
|
||||||
|
.collect(Collectors.toMap(OpsBusAreaDO::getId, OpsBusAreaDO::getAreaName, (a, b) -> a));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||||
|
<mapper namespace="com.viewsh.module.ops.dal.mysql.workorder.OpsOrderMapper">
|
||||||
|
|
||||||
|
<!-- 按日期分组统计工单创建数 -->
|
||||||
|
<select id="selectCreatedCountGroupByDate" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.DateCountRespVO">
|
||||||
|
SELECT DATE(create_time) AS statDate, COUNT(*) AS cnt
|
||||||
|
FROM ops_order
|
||||||
|
WHERE order_type = #{orderType}
|
||||||
|
AND deleted = 0
|
||||||
|
AND create_time >= #{startTime}
|
||||||
|
AND create_time < #{endTime}
|
||||||
|
GROUP BY DATE(create_time)
|
||||||
|
ORDER BY statDate
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 按日期分组统计工单完成数 -->
|
||||||
|
<select id="selectCompletedCountGroupByDate" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.DateCountRespVO">
|
||||||
|
SELECT DATE(end_time) AS statDate, COUNT(*) AS cnt
|
||||||
|
FROM ops_order
|
||||||
|
WHERE order_type = #{orderType}
|
||||||
|
AND status = 'COMPLETED'
|
||||||
|
AND deleted = 0
|
||||||
|
AND end_time >= #{startTime}
|
||||||
|
AND end_time < #{endTime}
|
||||||
|
GROUP BY DATE(end_time)
|
||||||
|
ORDER BY statDate
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 按日期分组统计平均响应时长和完成时长 -->
|
||||||
|
<select id="selectAvgTimeGroupByDate" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.AvgTimeRespVO">
|
||||||
|
SELECT DATE(end_time) AS statDate,
|
||||||
|
AVG(CASE WHEN response_seconds > 0 THEN response_seconds ELSE NULL END) AS avgResponse,
|
||||||
|
AVG(CASE WHEN completion_seconds > 0 THEN completion_seconds ELSE NULL END) AS avgCompletion
|
||||||
|
FROM ops_order
|
||||||
|
WHERE order_type = #{orderType}
|
||||||
|
AND status = 'COMPLETED'
|
||||||
|
AND deleted = 0
|
||||||
|
AND end_time >= #{startTime}
|
||||||
|
AND end_time < #{endTime}
|
||||||
|
GROUP BY DATE(end_time)
|
||||||
|
ORDER BY statDate
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 按区域分组统计工单数和完成数 -->
|
||||||
|
<select id="selectCountGroupByAreaId" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.AreaCountRespVO">
|
||||||
|
SELECT area_id AS areaId,
|
||||||
|
COUNT(*) AS totalCount,
|
||||||
|
SUM(CASE WHEN status = 'COMPLETED' THEN 1 ELSE 0 END) AS completedCount
|
||||||
|
FROM ops_order
|
||||||
|
WHERE order_type = #{orderType}
|
||||||
|
AND status != 'CANCELLED'
|
||||||
|
AND deleted = 0
|
||||||
|
AND area_id IS NOT NULL
|
||||||
|
GROUP BY area_id
|
||||||
|
ORDER BY totalCount DESC
|
||||||
|
LIMIT 10
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 按小时分组统计工单创建数 -->
|
||||||
|
<select id="selectCountGroupByHour" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.HourCountRespVO">
|
||||||
|
SELECT HOUR(create_time) AS hourVal, COUNT(*) AS cnt
|
||||||
|
FROM ops_order
|
||||||
|
WHERE order_type = #{orderType}
|
||||||
|
AND deleted = 0
|
||||||
|
AND create_time >= #{startTime}
|
||||||
|
AND create_time < #{endTime}
|
||||||
|
GROUP BY HOUR(create_time)
|
||||||
|
ORDER BY hourVal
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 按星期和小时分组统计(热力图) -->
|
||||||
|
<select id="selectCountGroupByDayOfWeekAndHour" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.HeatmapRespVO">
|
||||||
|
SELECT DAYOFWEEK(create_time) AS dayOfWeek,
|
||||||
|
HOUR(create_time) AS hourVal,
|
||||||
|
COUNT(*) AS cnt
|
||||||
|
FROM ops_order
|
||||||
|
WHERE order_type = #{orderType}
|
||||||
|
AND deleted = 0
|
||||||
|
AND create_time >= #{startTime}
|
||||||
|
AND create_time < #{endTime}
|
||||||
|
GROUP BY DAYOFWEEK(create_time), HOUR(create_time)
|
||||||
|
ORDER BY dayOfWeek, hourVal
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 按保洁类型分组统计平均作业时长(JOIN clean_ext) -->
|
||||||
|
<select id="selectAvgDurationGroupByCleaningType" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.CleaningTypeAvgDurationRespVO">
|
||||||
|
SELECT ext.cleaning_type AS cleaningType,
|
||||||
|
ROUND(AVG(TIMESTAMPDIFF(MINUTE, ext.arrived_time, ext.completed_time))) AS avgDuration
|
||||||
|
FROM ops_order_clean_ext ext
|
||||||
|
INNER JOIN ops_order o ON ext.ops_order_id = o.id
|
||||||
|
WHERE o.status = 'COMPLETED'
|
||||||
|
AND o.order_type = #{orderType}
|
||||||
|
AND o.deleted = 0
|
||||||
|
AND ext.deleted = 0
|
||||||
|
AND ext.completed_time IS NOT NULL
|
||||||
|
AND ext.arrived_time IS NOT NULL
|
||||||
|
AND ext.cleaning_type IS NOT NULL
|
||||||
|
GROUP BY ext.cleaning_type
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<!-- 按功能类型分组统计工单数和完成数 -->
|
||||||
|
<select id="selectCountGroupByFunctionType" resultType="com.viewsh.module.ops.controller.admin.workorder.vo.statistics.FunctionTypeCountRespVO">
|
||||||
|
SELECT a.function_type AS functionType,
|
||||||
|
COUNT(o.id) AS totalCount,
|
||||||
|
SUM(CASE WHEN o.status = 'COMPLETED' THEN 1 ELSE 0 END) AS completedCount
|
||||||
|
FROM ops_order o
|
||||||
|
INNER JOIN ops_bus_area a ON o.area_id = a.id
|
||||||
|
WHERE o.order_type = #{orderType}
|
||||||
|
AND o.status != 'CANCELLED'
|
||||||
|
AND o.deleted = 0
|
||||||
|
AND a.deleted = 0
|
||||||
|
AND a.function_type IS NOT NULL
|
||||||
|
GROUP BY a.function_type
|
||||||
|
ORDER BY totalCount DESC
|
||||||
|
LIMIT 10
|
||||||
|
</select>
|
||||||
|
|
||||||
|
</mapper>
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
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.environment.service.badge.BadgeDeviceStatusService;
|
||||||
|
import com.viewsh.module.ops.service.area.AreaDeviceService;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.function.Supplier;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 统计服务配置,注册跨模块依赖的 Bean
|
||||||
|
*
|
||||||
|
* @author lzh
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class StatisticsConfiguration {
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private BadgeDeviceStatusService badgeDeviceStatusService;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private AreaDeviceService areaDeviceService;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Supplier<int[]> staffCountProvider() {
|
||||||
|
return () -> {
|
||||||
|
if (badgeDeviceStatusService == null) {
|
||||||
|
return new int[]{0, 0};
|
||||||
|
}
|
||||||
|
List<BadgeDeviceStatusDTO> activeBadges = badgeDeviceStatusService.listActiveBadges();
|
||||||
|
int onlineCount = activeBadges.size();
|
||||||
|
// 从区域设备关联表查询所有已注册的工牌设备总数
|
||||||
|
int totalCount = onlineCount;
|
||||||
|
if (areaDeviceService != null) {
|
||||||
|
try {
|
||||||
|
List<OpsAreaDeviceRelationDO> allBadges = areaDeviceService.listByAreaIdAndType(null, "BADGE");
|
||||||
|
totalCount = Math.max(onlineCount, allBadges.size());
|
||||||
|
} catch (Exception ignored) {
|
||||||
|
// 查询失败时使用在线数作为兜底
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new int[]{onlineCount, totalCount};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -3,10 +3,15 @@ package com.viewsh.module.ops.controller.admin;
|
|||||||
import com.viewsh.framework.common.pojo.CommonResult;
|
import com.viewsh.framework.common.pojo.CommonResult;
|
||||||
import com.viewsh.framework.common.pojo.PageResult;
|
import com.viewsh.framework.common.pojo.PageResult;
|
||||||
import com.viewsh.module.ops.api.clean.QuickStatsRespDTO;
|
import com.viewsh.module.ops.api.clean.QuickStatsRespDTO;
|
||||||
|
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.DashboardStatsRespVO;
|
||||||
|
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.TrafficRealtimeRespVO;
|
||||||
|
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.TrafficTrendRespVO;
|
||||||
|
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.WorkspaceStatsRespVO;
|
||||||
import com.viewsh.module.ops.environment.service.dashboard.CleanDashboardService;
|
import com.viewsh.module.ops.environment.service.dashboard.CleanDashboardService;
|
||||||
import com.viewsh.module.ops.service.OrderDetailVO;
|
import com.viewsh.module.ops.service.OrderDetailVO;
|
||||||
import com.viewsh.module.ops.service.OrderQueryService;
|
import com.viewsh.module.ops.service.OrderQueryService;
|
||||||
import com.viewsh.module.ops.service.OrderSummaryVO;
|
import com.viewsh.module.ops.service.OrderSummaryVO;
|
||||||
|
import com.viewsh.module.ops.service.statistics.OpsStatisticsService;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
import io.swagger.v3.oas.annotations.Parameter;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
@@ -14,10 +19,12 @@ import jakarta.annotation.Resource;
|
|||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.extern.slf4j.Slf4j;
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.format.annotation.DateTimeFormat;
|
||||||
import org.springframework.security.access.prepost.PreAuthorize;
|
import org.springframework.security.access.prepost.PreAuthorize;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||||
@@ -42,6 +49,9 @@ public class OrderCenterController {
|
|||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private CleanDashboardService cleanDashboardService;
|
private CleanDashboardService cleanDashboardService;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private OpsStatisticsService opsStatisticsService;
|
||||||
|
|
||||||
@GetMapping("/page")
|
@GetMapping("/page")
|
||||||
@Operation(summary = "工单中心分页查询")
|
@Operation(summary = "工单中心分页查询")
|
||||||
@PreAuthorize("@ss.hasPermission('ops:order-center:query')")
|
@PreAuthorize("@ss.hasPermission('ops:order-center:query')")
|
||||||
@@ -87,4 +97,57 @@ public class OrderCenterController {
|
|||||||
QuickStatsRespDTO stats = cleanDashboardService.getQuickStats();
|
QuickStatsRespDTO stats = cleanDashboardService.getQuickStats();
|
||||||
return success(stats);
|
return success(stats);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@GetMapping("/dashboard-stats")
|
||||||
|
@Operation(summary = "看板统计")
|
||||||
|
@PreAuthorize("@ss.hasPermission('ops:order-center:query')")
|
||||||
|
public CommonResult<DashboardStatsRespVO> getDashboardStats(
|
||||||
|
@RequestParam(value = "orderType", required = false, defaultValue = "CLEAN") String orderType,
|
||||||
|
@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("[getDashboardStats] OpsStatisticsService 未注入,返回默认值");
|
||||||
|
return success(DashboardStatsRespVO.builder().build());
|
||||||
|
}
|
||||||
|
if (startDate == null) {
|
||||||
|
startDate = LocalDate.now().minusDays(6);
|
||||||
|
}
|
||||||
|
if (endDate == null) {
|
||||||
|
endDate = LocalDate.now();
|
||||||
|
}
|
||||||
|
return success(opsStatisticsService.getDashboardStats(orderType, startDate, endDate));
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/traffic-realtime")
|
||||||
|
@Operation(summary = "实时客流监测")
|
||||||
|
@PreAuthorize("@ss.hasPermission('ops:order-center:query')")
|
||||||
|
public CommonResult<TrafficRealtimeRespVO> getTrafficRealtime() {
|
||||||
|
if (opsStatisticsService == null) {
|
||||||
|
log.warn("[getTrafficRealtime] OpsStatisticsService 未注入,返回默认值");
|
||||||
|
return success(TrafficRealtimeRespVO.builder().build());
|
||||||
|
}
|
||||||
|
return success(opsStatisticsService.getTrafficRealtime());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/workspace-stats")
|
||||||
|
@Operation(summary = "工作台统计")
|
||||||
|
@PreAuthorize("@ss.hasPermission('ops:order-center:query')")
|
||||||
|
public CommonResult<WorkspaceStatsRespVO> getWorkspaceStats() {
|
||||||
|
if (opsStatisticsService == null) {
|
||||||
|
log.warn("[getWorkspaceStats] OpsStatisticsService 未注入,返回默认值");
|
||||||
|
return success(WorkspaceStatsRespVO.builder().build());
|
||||||
|
}
|
||||||
|
return success(opsStatisticsService.getWorkspaceStats());
|
||||||
|
}
|
||||||
|
|
||||||
|
@GetMapping("/traffic-trend")
|
||||||
|
@Operation(summary = "近7天客流趋势统计")
|
||||||
|
@PreAuthorize("@ss.hasPermission('ops:order-center:query')")
|
||||||
|
public CommonResult<TrafficTrendRespVO> getTrafficTrend() {
|
||||||
|
if (opsStatisticsService == null) {
|
||||||
|
log.warn("[getTrafficTrend] OpsStatisticsService 未注入,返回默认值");
|
||||||
|
return success(TrafficTrendRespVO.builder().build());
|
||||||
|
}
|
||||||
|
return success(opsStatisticsService.getTrafficTrend());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user