diff --git a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanWorkOrderServiceImpl.java b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanWorkOrderServiceImpl.java index 593eb23..d7d5e3e 100644 --- a/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanWorkOrderServiceImpl.java +++ b/viewsh-module-ops/viewsh-module-environment-biz/src/main/java/com/viewsh/module/ops/environment/service/cleanorder/CleanWorkOrderServiceImpl.java @@ -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 timeline = events.stream() + List 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 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 buildPendingExtraInfo(OpsOrderDO order) { + Map 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; + } } diff --git a/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/DictTypeConstants.java b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/DictTypeConstants.java new file mode 100644 index 0000000..9223298 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-api/src/main/java/com/viewsh/module/ops/enums/DictTypeConstants.java @@ -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"; + +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/AreaCountRespVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/AreaCountRespVO.java new file mode 100644 index 0000000..eea3a51 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/AreaCountRespVO.java @@ -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; + +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/AvgTimeRespVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/AvgTimeRespVO.java new file mode 100644 index 0000000..d31a4b0 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/AvgTimeRespVO.java @@ -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; + +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/CleaningTypeAvgDurationRespVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/CleaningTypeAvgDurationRespVO.java new file mode 100644 index 0000000..b3ada5e --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/CleaningTypeAvgDurationRespVO.java @@ -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; + +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/DashboardStatsRespVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/DashboardStatsRespVO.java new file mode 100644 index 0000000..a3abd95 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/DashboardStatsRespVO.java @@ -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 funnelData; + + @Schema(description = "热力图数据(近7天)") + private HeatmapData heatmapData; + + @Schema(description = "功能类型排行") + private List functionTypeRanking; + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class TrendData { + @Schema(description = "日期列表") + private List dates; + @Schema(description = "创建工单数") + private List createdData; + @Schema(description = "完成工单数") + private List completedData; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class HourlyDistribution { + @Schema(description = "小时列表") + private List hours; + @Schema(description = "工单数") + private List data; + } + + @Data + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class TimeTrendData { + @Schema(description = "日期列表") + private List dates; + @Schema(description = "响应时长(分钟)") + private List responseTimeData; + @Schema(description = "完成时长(分钟)") + private List 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 days; + @Schema(description = "小时列表") + private List hours; + @Schema(description = "热力图数据 [dayIndex][hourIndex]") + private List> 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; + } + +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/DateCountRespVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/DateCountRespVO.java new file mode 100644 index 0000000..ef74fda --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/DateCountRespVO.java @@ -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; + +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/FunctionTypeCountRespVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/FunctionTypeCountRespVO.java new file mode 100644 index 0000000..2d9b38f --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/FunctionTypeCountRespVO.java @@ -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; + +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/HeatmapRespVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/HeatmapRespVO.java new file mode 100644 index 0000000..ef5b8d1 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/HeatmapRespVO.java @@ -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; + +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/HourCountRespVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/HourCountRespVO.java new file mode 100644 index 0000000..7a9c37b --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/HourCountRespVO.java @@ -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; + +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/TrafficRealtimeRespVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/TrafficRealtimeRespVO.java new file mode 100644 index 0000000..ccaec2a --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/TrafficRealtimeRespVO.java @@ -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 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 hours; + @Schema(description = "进入人数") + private List inData; + @Schema(description = "离开人数") + private List outData; + } + +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/TrafficTrendRespVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/TrafficTrendRespVO.java new file mode 100644 index 0000000..2cf07e4 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/TrafficTrendRespVO.java @@ -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 dates; + + @Schema(description = "每日进入人数") + private List inData; + + @Schema(description = "每日离开人数") + private List outData; + + @Schema(description = "每日净增人数") + private List netData; + + @Schema(description = "总进入人数") + private Long totalIn; + + @Schema(description = "总离开人数") + private Long totalOut; + +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/WorkspaceStatsRespVO.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/WorkspaceStatsRespVO.java new file mode 100644 index 0000000..bdc6ad6 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/controller/admin/workorder/vo/statistics/WorkspaceStatsRespVO.java @@ -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 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 hours; + @Schema(description = "工单数") + private List data; + } + +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/mysql/workorder/OpsOrderMapper.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/mysql/workorder/OpsOrderMapper.java index f927eda..c64d919 100644 --- a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/mysql/workorder/OpsOrderMapper.java +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/dal/mysql/workorder/OpsOrderMapper.java @@ -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 { .last("LIMIT 1")); } + // ==================== 统计聚合查询 ==================== + + /** + * 按日期分组统计工单创建数 + */ + List selectCreatedCountGroupByDate(@Param("orderType") String orderType, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 按日期分组统计工单完成数 + */ + List selectCompletedCountGroupByDate(@Param("orderType") String orderType, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 按日期分组统计平均响应时长和完成时长(秒) + */ + List selectAvgTimeGroupByDate(@Param("orderType") String orderType, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 按区域分组统计工单数和完成数(非取消的工单) + */ + List selectCountGroupByAreaId(@Param("orderType") String orderType); + + /** + * 按小时分组统计工单创建数 + */ + List selectCountGroupByHour(@Param("orderType") String orderType, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 按星期和小时分组统计工单创建数(用于热力图) + * day_of_week: MySQL DAYOFWEEK 1=周日 2=周一 ... 7=周六 + */ + List selectCountGroupByDayOfWeekAndHour(@Param("orderType") String orderType, + @Param("startTime") LocalDateTime startTime, + @Param("endTime") LocalDateTime endTime); + + /** + * 按保洁类型分组统计平均作业时长(分钟) + * JOIN ops_order_clean_ext,使用 arrived_time 和 completed_time 计算 + */ + List selectAvgDurationGroupByCleaningType(@Param("orderType") String orderType); + + /** + * 按功能类型分组统计工单数和完成数(非取消的工单) + */ + List selectCountGroupByFunctionType(@Param("orderType") String orderType); + // 注意:分页查询方法需要在Service层实现,这里只提供基础查询方法 // 具体分页查询请参考Service实现 diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/statistics/OpsStatisticsService.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/statistics/OpsStatisticsService.java new file mode 100644 index 0000000..6e2d9cd --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/statistics/OpsStatisticsService.java @@ -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(); + +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/statistics/OpsStatisticsServiceImpl.java b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/statistics/OpsStatisticsServiceImpl.java new file mode 100644 index 0000000..8cc30a9 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/java/com/viewsh/module/ops/service/statistics/OpsStatisticsServiceImpl.java @@ -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 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 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 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() + .in(OpsOrderDO::getStatus, IN_PROGRESS_STATUSES) + .eq(OpsOrderDO::getOrderType, orderType))); + + // 3. 今日完成数(使用 ge + lt 避免边界重叠) + Integer completedTodayCount = Math.toIntExact(opsOrderMapper.selectCount( + new LambdaQueryWrapperX() + .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() + .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 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 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 todayStats = trafficStatisticsMapper.selectList( + new LambdaQueryWrapperX() + .ge(OpsTrafficStatisticsDO::getStatHour, todayStart) + .lt(OpsTrafficStatisticsDO::getStatHour, todayEnd)); + + // 加载区域名称映射 + Map areaNameMap = loadAreaNameMap(); + + // 按区域汇总 + Map> byArea = todayStats.stream() + .filter(s -> s.getAreaId() != null) + .collect(Collectors.groupingBy(OpsTrafficStatisticsDO::getAreaId)); + + long totalIn = 0; + long totalOut = 0; + List areas = new ArrayList<>(); + + for (Map.Entry> entry : byArea.entrySet()) { + Long areaId = entry.getKey(); + List 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 completedToday = opsOrderMapper.selectList( + new LambdaQueryWrapperX() + .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 ratedOrders = opsOrderMapper.selectList( + new LambdaQueryWrapperX() + .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() + .eq(OpsOrderDO::getOrderType, orderType) + .ge(OpsOrderDO::getCreateTime, todayStart) + .lt(OpsOrderDO::getCreateTime, todayEnd))); + + // 6. 新增工单数(最近1小时) + Integer newOrderCount = Math.toIntExact(opsOrderMapper.selectCount( + new LambdaQueryWrapperX() + .eq(OpsOrderDO::getOrderType, orderType) + .ge(OpsOrderDO::getCreateTime, oneHourAgo))); + + // 7. 紧急任务列表(P0 + 待处理/进行中) + List urgentOrders = opsOrderMapper.selectList( + new LambdaQueryWrapperX() + .eq(OpsOrderDO::getOrderType, orderType) + .eq(OpsOrderDO::getPriority, 0) + .notIn(OpsOrderDO::getStatus, "COMPLETED", "CANCELLED") + .orderByDesc(OpsOrderDO::getCreateTime) + .last("LIMIT 10")); + List 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 stats = trafficStatisticsMapper.selectList( + new LambdaQueryWrapperX() + .ge(OpsTrafficStatisticsDO::getStatHour, startDateTime) + .lt(OpsTrafficStatisticsDO::getStatHour, endDateTime)); + + // 按日期汇总 + Map dailyInMap = new HashMap<>(); + Map 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 dates = new ArrayList<>(); + List inData = new ArrayList<>(); + List outData = new ArrayList<>(); + List 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() + .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 createdRows = opsOrderMapper.selectCreatedCountGroupByDate( + orderType, startDateTime, endDateTime); + Map createdMap = createdRows.stream() + .collect(Collectors.toMap( + row -> row.getStatDate().toString(), + row -> row.getCnt().intValue(), + (a, b) -> a + )); + + // 一次查询获取所有天的完成数 + List completedRows = opsOrderMapper.selectCompletedCountGroupByDate( + orderType, startDateTime, endDateTime); + Map completedMap = completedRows.stream() + .collect(Collectors.toMap( + row -> row.getStatDate().toString(), + row -> row.getCnt().intValue(), + (a, b) -> a + )); + + // 按日期遍历,补零 + List dates = new ArrayList<>(); + List createdData = new ArrayList<>(); + List 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 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 hours = new ArrayList<>(); + List 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 rows = opsOrderMapper.selectAvgTimeGroupByDate( + orderType, startDateTime, endDateTime); + + // 构建日期 -> 响应/完成时长映射 + Map responseMap = new HashMap<>(); + Map 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 dates = new ArrayList<>(); + List responseTimeData = new ArrayList<>(); + List 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 buildFunnelData(String orderType, LocalDateTime startDateTime, LocalDateTime endDateTime) { + // 创建工单 = 时间范围内创建的所有非取消工单 + long totalCreated = opsOrderMapper.selectCount( + new LambdaQueryWrapperX() + .eq(OpsOrderDO::getOrderType, orderType) + .ne(OpsOrderDO::getStatus, "CANCELLED") + .ge(OpsOrderDO::getCreateTime, startDateTime) + .lt(OpsOrderDO::getCreateTime, endDateTime)); + + // 已分配 = 时间范围内创建且已进入工作流程的工单(排除 PENDING 和 CANCELLED) + long assigned = opsOrderMapper.selectCount( + new LambdaQueryWrapperX() + .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() + .eq(OpsOrderDO::getOrderType, orderType) + .in(OpsOrderDO::getStatus, "ARRIVED", "COMPLETED", "PAUSED") + .ge(OpsOrderDO::getCreateTime, startDateTime) + .lt(OpsOrderDO::getCreateTime, endDateTime)); + + // 已完成 = 时间范围内创建且已完成的工单 + long completed = opsOrderMapper.selectCount( + new LambdaQueryWrapperX() + .eq(OpsOrderDO::getStatus, "COMPLETED") + .eq(OpsOrderDO::getOrderType, orderType) + .ge(OpsOrderDO::getCreateTime, startDateTime) + .lt(OpsOrderDO::getCreateTime, endDateTime)); + + List 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 rows = opsOrderMapper.selectCountGroupByDayOfWeekAndHour( + orderType, startDateTime, endDateTime); + + // 生成近7天的日期列表(格式:MM-dd) + LocalDate startDate = startDateTime.toLocalDate(); + List days = new ArrayList<>(); + Map 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 hours = new ArrayList<>(); + for (int h = 0; h < 24; h++) { + hours.add(String.format("%d:00", h)); + } + + List> data = new ArrayList<>(); + for (int d = 0; d < 7; d++) { + List 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 buildFunctionTypeRanking(String orderType) { + List 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 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 hours = new ArrayList<>(); + List inData = new ArrayList<>(); + List 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 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 hours = new ArrayList<>(); + List 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 loadAreaNameMap() { + List areas = opsBusAreaMapper.selectList(new LambdaQueryWrapper<>()); + return areas.stream() + .collect(Collectors.toMap(OpsBusAreaDO::getId, OpsBusAreaDO::getAreaName, (a, b) -> a)); + } + +} diff --git a/viewsh-module-ops/viewsh-module-ops-biz/src/main/resources/mapper/workorder/OpsOrderMapper.xml b/viewsh-module-ops/viewsh-module-ops-biz/src/main/resources/mapper/workorder/OpsOrderMapper.xml new file mode 100644 index 0000000..1fa3758 --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-biz/src/main/resources/mapper/workorder/OpsOrderMapper.xml @@ -0,0 +1,119 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/config/StatisticsConfiguration.java b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/config/StatisticsConfiguration.java new file mode 100644 index 0000000..dba48de --- /dev/null +++ b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/config/StatisticsConfiguration.java @@ -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 staffCountProvider() { + return () -> { + if (badgeDeviceStatusService == null) { + return new int[]{0, 0}; + } + List activeBadges = badgeDeviceStatusService.listActiveBadges(); + int onlineCount = activeBadges.size(); + // 从区域设备关联表查询所有已注册的工牌设备总数 + int totalCount = onlineCount; + if (areaDeviceService != null) { + try { + List allBadges = areaDeviceService.listByAreaIdAndType(null, "BADGE"); + totalCount = Math.max(onlineCount, allBadges.size()); + } catch (Exception ignored) { + // 查询失败时使用在线数作为兜底 + } + } + return new int[]{onlineCount, totalCount}; + }; + } + +} diff --git a/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/OrderCenterController.java b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/OrderCenterController.java index 88b7434..086895b 100644 --- a/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/OrderCenterController.java +++ b/viewsh-module-ops/viewsh-module-ops-server/src/main/java/com/viewsh/module/ops/controller/admin/OrderCenterController.java @@ -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 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 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 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 getTrafficTrend() { + if (opsStatisticsService == null) { + log.warn("[getTrafficTrend] OpsStatisticsService 未注入,返回默认值"); + return success(TrafficTrendRespVO.builder().build()); + } + return success(opsStatisticsService.getTrafficTrend()); + } }