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("CANCEL", "工单取消");
|
||||
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
|
||||
@@ -75,9 +80,29 @@ public class CleanWorkOrderServiceImpl implements CleanWorkOrderService {
|
||||
);
|
||||
|
||||
// 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)
|
||||
.toList();
|
||||
timeline.addAll(eventNodes);
|
||||
|
||||
// 4. 构建响应
|
||||
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()
|
||||
.status(event.getToStatus())
|
||||
.status(status)
|
||||
.statusName(statusName)
|
||||
.time(formatDateTime(event.getEventTime()))
|
||||
.operator(operator)
|
||||
@@ -227,4 +258,20 @@ public class CleanWorkOrderServiceImpl implements CleanWorkOrderService {
|
||||
}
|
||||
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.query.LambdaQueryWrapperX;
|
||||
import com.viewsh.module.ops.controller.admin.workorder.vo.statistics.*;
|
||||
import com.viewsh.module.ops.dal.dataobject.workorder.OpsOrderDO;
|
||||
import org.apache.ibatis.annotations.Mapper;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
@@ -71,6 +74,60 @@ public interface OpsOrderMapper extends BaseMapperX<OpsOrderDO> {
|
||||
.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实现
|
||||
|
||||
|
||||
@@ -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.PageResult;
|
||||
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.service.OrderDetailVO;
|
||||
import com.viewsh.module.ops.service.OrderQueryService;
|
||||
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.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
@@ -14,10 +19,12 @@ import jakarta.annotation.Resource;
|
||||
import jakarta.validation.Valid;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.security.access.prepost.PreAuthorize;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import java.time.LocalDate;
|
||||
import java.util.Map;
|
||||
|
||||
import static com.viewsh.framework.common.pojo.CommonResult.success;
|
||||
@@ -42,6 +49,9 @@ public class OrderCenterController {
|
||||
@Autowired(required = false)
|
||||
private CleanDashboardService cleanDashboardService;
|
||||
|
||||
@Autowired(required = false)
|
||||
private OpsStatisticsService opsStatisticsService;
|
||||
|
||||
@GetMapping("/page")
|
||||
@Operation(summary = "工单中心分页查询")
|
||||
@PreAuthorize("@ss.hasPermission('ops:order-center:query')")
|
||||
@@ -87,4 +97,57 @@ public class OrderCenterController {
|
||||
QuickStatsRespDTO stats = cleanDashboardService.getQuickStats();
|
||||
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