Browse Source

feat: 新增统计页面相关数据接口

master
yinhuaiwei 2 weeks ago
parent
commit
03f5fec5e6
22 changed files with 1838 additions and 0 deletions
  1. +109
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/constant/PatrolScreenConst.java
  2. +79
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/controller/PatrolStatisticsController.java
  3. +23
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/DeviceOnlineEvent.java
  4. +18
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/PatrolScreenCount.java
  5. +18
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/PatrolScreenDict.java
  6. +19
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/PatrolScreenRate.java
  7. +19
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/AlarmStatsVo.java
  8. +34
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/CameraStatsVo.java
  9. +19
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/DefectStatsVo.java
  10. +21
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/DeviceOnlineStatVo.java
  11. +17
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/DeviceStatsVo.java
  12. +18
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/LevelStatVo.java
  13. +46
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/ManualConfirmDefectVo.java
  14. +28
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/RobotDroneStatusVo.java
  15. +27
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/ScreenResultVo.java
  16. +65
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/TaskStatsVo.java
  17. +201
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/mapper/PatrolScreenMapper.java
  18. +26
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/service/PatrolStatisticsService.java
  19. +764
    -0
      inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/service/impl/PatrolStatisticsServiceImpl.java
  20. +280
    -0
      inspect-main/inspect-main-statistics/src/main/resources/mapper/screen/PatrolScreenMapper.xml
  21. +1
    -0
      inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/utils/ResultAnalysisUtils.java
  22. +6
    -0
      inspect-main/pom.xml

+ 109
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/constant/PatrolScreenConst.java View File

@ -0,0 +1,109 @@
package com.inspect.statistics.constant;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
/**
* 巡视大屏统计接口常量:Redis 缓存 key、缓存时长、period 取值与固定名目。
*/
public class PatrolScreenConst {
/** 大屏统计缓存 key 统一前缀 */
public static final String CACHE_PREFIX = "patrol_screen:";
/** 巡视设备统计 */
public static final String CACHE_DEVICE_STATS = CACHE_PREFIX + "device_stats";
/** 机器人/无人工况 */
public static final String CACHE_ROBOT_DRONE_STATUS = CACHE_PREFIX + "robot_drone_status";
/** 摄像机统计 */
public static final String CACHE_CAMERA_STATS = CACHE_PREFIX + "camera_stats";
/** 巡视任务统计(需拼接 period) */
public static final String CACHE_TASK_STATS = CACHE_PREFIX + "task_stats:";
/** 人工确认缺陷统计(需拼接 period) */
public static final String CACHE_MANUAL_CONFIRM_DEFECT = CACHE_PREFIX + "manual_confirm_defect:";
/** 巡视告警统计(需拼接 period) */
public static final String CACHE_ALARM_STATS = CACHE_PREFIX + "alarm_stats:";
/** 巡视缺陷统计(需拼接 period) */
public static final String CACHE_DEFECT_STATS = CACHE_PREFIX + "defect_stats:";
/** 低频接口缓存时长(秒):设备统计、机器人/无人工况、摄像机统计 */
public static final long TTL_LOW_FREQUENCY = 60L;
/** 中频接口缓存时长(秒):巡视任务统计、人工确认缺陷统计 */
public static final long TTL_MIDDLE_FREQUENCY = 30L;
/** 高频接口缓存时长(秒):巡视告警统计、巡视缺陷统计 */
public static final long TTL_HIGH_FREQUENCY = 15L;
public static final String PERIOD_DAY = "day";
public static final String PERIOD_WEEK = "week";
public static final String PERIOD_MONTH = "month";
public static final String PERIOD_YEAR = "year";
/** period 缺省值 */
public static final String PERIOD_DEFAULT = PERIOD_DAY;
/** period 合法取值 */
public static final List<String> PERIODS = Collections.unmodifiableList(
Arrays.asList(PERIOD_DAY, PERIOD_WEEK, PERIOD_MONTH, PERIOD_YEAR));
/** basedata_eqpbook.type:摄像头 */
public static final String DEVICE_TYPE_CAMERA = "10";
/** basedata_eqpbook.type:无人机 */
public static final String DEVICE_TYPE_DRONE = "13";
/** basedata_eqpbook.type:机器人 */
public static final List<String> DEVICE_TYPE_ROBOTS = Collections.unmodifiableList(
Arrays.asList("1", "2", "3"));
/** 未删除标志:del_flag / delete_flag 为 0 表示未删除,2 表示已删除 */
public static final String FLAG_NORMAL = "0";
/** sys_dict_data.dict_type:摄像机使用类型(basedata_eqpbook_channel.use_mode 的取值字典) */
public static final String DICT_USE_MODE = "use_mode";
/** sys_dict_data.dict_type:通道类型(basedata_eqpbook_channel.channel_type 的取值字典) */
public static final String DICT_CHANNEL_TYPE = "channel_type";
/** 摄像机类型分布与光谱分布的兜底名目:取值未配置或不在对应字典内的通道 */
public static final String CAMERA_OTHER = "其他";
/** basedata_mont_patdev_online.status:在线 */
public static final String ONLINE_STATUS = "1";
/** patrol_task.dev_type:机器人 */
public static final String PATROL_DEV_TYPE_ROBOT = "1";
/** patrol_task.dev_type:无人机 */
public static final String PATROL_DEV_TYPE_DRONE = "0";
/** 连续运行天数回溯上限(天),用于兜底防止脏数据导致统计失控 */
public static final int MAX_RUN_DAYS = 3650;
/** 巡视类型名目(固定顺序,含 0 值) */
public static final List<String> PATROL_TYPES = Collections.unmodifiableList(
Arrays.asList("全面巡视", "例行巡视", "特殊巡视", "专项巡视", "自定义巡视", "熄灯巡视"));
/** 巡视类型名目对应的 patrol_task.type 取值,顺序与 PATROL_TYPES 一致;空串表示库中无对应取值、恒为 0 */
public static final List<String> PATROL_TYPE_CODES = Collections.unmodifiableList(
Arrays.asList("", "1", "2", "3", "4", ""));
/**
* 不参与大屏统计的巡视任务类型:5 联动任务、6 静默任务。
*
* <p>注意:MyBatis OGNL 会对该集合调用 {@code size()},JDK 17 下无法反射访问
* {@code Collections$UnmodifiableCollection} 等包私有实现类(InaccessibleObjectException),
* 故这里使用可变 ArrayList,不要改成 unmodifiableList。</p>
*/
public static final List<String> PATROL_TYPE_EXCLUDED = new ArrayList<>(Arrays.asList("5", "6"));
/** 巡视状态名目(固定顺序,含 0 值) */
public static final List<String> PATROL_STATUSES = Collections.unmodifiableList(
Arrays.asList("正在执行", "已经执行", "暂停", "终止", "未执行", "超期"));
/** 巡视状态名目对应的 patrol_task_status.task_state 取值,顺序与 PATROL_STATUSES 一致 */
public static final List<String> PATROL_STATUS_CODES = Collections.unmodifiableList(
Arrays.asList("2", "1", "3", "4", "5", "6"));
/** 巡视率指标名目:与数量项分开返回,避免前端按名称硬编码判断 */
public static final String RATE_POINT_MISS = "巡视点位漏检率";
public static final String RATE_TASK_CLOSED_LOOP = "任务执行闭环率";
public static final List<String> PATROL_RATES = Collections.unmodifiableList(
Arrays.asList(RATE_POINT_MISS, RATE_TASK_CLOSED_LOOP));
/** 告警级别名目(固定顺序,含 0 值) */
public static final List<String> ALARM_LEVELS = Collections.unmodifiableList(
Arrays.asList("预警", "一般", "严重", "危急"));
/** 缺陷级别名目(固定顺序,含 0 值) */
public static final List<String> DEFECT_LEVELS = Collections.unmodifiableList(
Arrays.asList("一般缺陷", "严重缺陷", "危急缺陷"));
}

+ 79
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/controller/PatrolStatisticsController.java View File

@ -15,6 +15,7 @@ import com.inspect.common.log.annotation.Log;
import com.inspect.common.log.enums.BizType; import com.inspect.common.log.enums.BizType;
import com.inspect.message.MessageUtils; import com.inspect.message.MessageUtils;
import com.inspect.statistics.domain.StatisticsPatrolResult; import com.inspect.statistics.domain.StatisticsPatrolResult;
import com.inspect.statistics.domain.vo.ScreenResultVo;
import com.inspect.statistics.service.PatrolStatisticsService; import com.inspect.statistics.service.PatrolStatisticsService;
import java.io.IOException; import java.io.IOException;
@ -32,6 +33,7 @@ import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@RestController @RestController
@ -283,4 +285,81 @@ public class PatrolStatisticsController extends BaseController {
HashMap<String, String> colMap = this.statisticsService.colMap(); HashMap<String, String> colMap = this.statisticsService.colMap();
return AjaxResult.success(colMap); return AjaxResult.success(colMap);
} }
/*
* ==================== 巡视大屏统计接口(/patrol 为网关前缀,本服务内路径为 /statistics/xxx) ====================
*/
/**
* 大屏-巡视设备统计:摄像机/机器人/无人机的总数、在线数、离线数。
*/
@GetMapping({"/device-stats"})
public AjaxResult deviceStats() {
return this.screenResult(this.statisticsService.deviceStats());
}
/**
* 大屏-机器人/无人工况:连续运行天数、巡检天数、巡检出勤率。
*/
@GetMapping({"/robot-drone-status"})
public AjaxResult robotDroneStatus() {
return this.screenResult(this.statisticsService.robotDroneStatus());
}
/**
* 大屏-摄像机统计:总数、在线/离线占比、类型分布、光谱分布。
*/
@GetMapping({"/camera-stats"})
public AjaxResult cameraStats() {
return this.screenResult(this.statisticsService.cameraStats());
}
/**
* 大屏-巡视任务统计:巡视类型、巡视率指标、巡视状态。
*
* @param period 时间维度 day/week/month/year,默认 day
*/
@GetMapping({"/task-stats"})
public AjaxResult taskStats(@RequestParam(value = "period", required = false) String period) {
return this.screenResult(this.statisticsService.taskStats(period));
}
/**
* 大屏-人工确认缺陷统计:严重度分布、审核相关完成率。
*
* @param period 时间维度 day/week/month/year,默认 day
*/
@GetMapping({"/manual-confirm-defect"})
public AjaxResult manualConfirmDefect(@RequestParam(value = "period", required = false) String period) {
return this.screenResult(this.statisticsService.manualConfirmDefect(period));
}
/**
* 大屏-巡视告警统计:按报警级别统计。
*
* @param period 时间维度 day/week/month/year,默认 day
*/
@GetMapping({"/alarm-stats"})
public AjaxResult alarmStats(@RequestParam(value = "period", required = false) String period) {
return this.screenResult(this.statisticsService.alarmStats(period));
}
/**
* 大屏-巡视缺陷统计:按缺陷级别统计。
*
* @param period 时间维度 day/week/month/year,默认 day
*/
@GetMapping({"/defect-stats"})
public AjaxResult defectStats(@RequestParam(value = "period", required = false) String period) {
return this.screenResult(this.statisticsService.defectStats(period));
}
/**
* 大屏统计统一返回:AjaxResult 外层 + 顶层 updateTime(数据实际生成时间)。
*/
private AjaxResult screenResult(ScreenResultVo<?> screenResult) {
AjaxResult ajaxResult = AjaxResult.success(screenResult.getData());
ajaxResult.put("updateTime", screenResult.getUpdateTime());
return ajaxResult;
}
} }

+ 23
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/DeviceOnlineEvent.java View File

@ -0,0 +1,23 @@
package com.inspect.statistics.domain;
import lombok.Getter;
import lombok.Setter;
import java.util.Date;
/**
* 巡视设备在线/离线事件,对应 basedata_mont_patdev_online。
*
* <p>该表记录的是状态变化事件(在线事件、离线事件各一条),不是按时间采样的状态快照,
* 因此连续运行天数需要由事件还原在线区间后再统计。</p>
*/
@Setter
@Getter
public class DeviceOnlineEvent {
/** 设备台账ID,对应 basedata_mont_patdev_online.eqp_book_id */
private Long eqpBookId;
/** 状态:1 在线、0 离线 */
private String status;
/** 事件时间,对应 basedata_mont_patdev_online.action_time */
private Date actionTime;
}

+ 18
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/PatrolScreenCount.java View File

@ -0,0 +1,18 @@
package com.inspect.statistics.domain;
import lombok.Getter;
import lombok.Setter;
/**
* 巡视大屏分组计数结果:按某个编码字段 group by 得到的「编码 - 数量」。
*
* <p>用于巡视类型、巡视状态、告警级别、缺陷级别等固定名目统计,业务侧再把编码翻译成名目。</p>
*/
@Setter
@Getter
public class PatrolScreenCount {
/** 分组编码,对应 SQL 的 code 列 */
private String code;
/** 数量,对应 SQL 的 num 列 */
private Long num;
}

+ 18
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/PatrolScreenDict.java View File

@ -0,0 +1,18 @@
package com.inspect.statistics.domain;
import lombok.Getter;
import lombok.Setter;
/**
* 巡视大屏字典项:对应 sys_dict_data 的字典值(dict_value)与字典名称(dict_label)。
*
* <p>大屏的类型分布等名目需要跟随字典配置变化,故运行时从字典表读取,不写死在代码里。</p>
*/
@Setter
@Getter
public class PatrolScreenDict {
/** 字典值,对应 SQL 的 code 列(dict_value) */
private String code;
/** 字典名称,对应 SQL 的 name 列(dict_label) */
private String name;
}

+ 19
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/PatrolScreenRate.java View File

@ -0,0 +1,19 @@
package com.inspect.statistics.domain;
import lombok.Getter;
import lombok.Setter;
/**
* 巡视大屏比率取数结果:总数与有效数。
*
* <p>口径与 inspect-main-client 的 {@code ResDomain} 保持一致(totalNum / validNum),
* 便于直接复制既有客户端统计 SQL。</p>
*/
@Setter
@Getter
public class PatrolScreenRate {
/** 总数,对应 SQL 的 totalNum 列 */
private Integer totalNum;
/** 有效数,对应 SQL 的 validNum 列 */
private Integer validNum;
}

+ 19
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/AlarmStatsVo.java View File

@ -0,0 +1,19 @@
package com.inspect.statistics.domain.vo;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
/**
* 接口6 巡视告警统计:按报警级别(预警、一般、严重、危急)统计。
*/
@Setter
@Getter
public class AlarmStatsVo {
/** 当前查询时间维度 */
private String period;
/** 各级别统计,固定返回全部级别(含 0 值) */
private List<LevelStatVo> items = new ArrayList<>();
}

+ 34
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/CameraStatsVo.java View File

@ -0,0 +1,34 @@
package com.inspect.statistics.domain.vo;
import lombok.Getter;
import lombok.Setter;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* 接口3 摄像机统计:摄像机详情。
*
* <p>total/online/offline 复用 {@link DeviceStatsVo} 中 camera 的同一口径,
* 避免设备总览与摄像机详情两处统计口径漂移。</p>
*/
@Setter
@Getter
public class CameraStatsVo extends DeviceOnlineStatVo {
/** 在线率(%),保留 2 位小数 */
private Double onlineRate = 0D;
/** 离线率(%),保留 2 位小数 */
private Double offlineRate = 0D;
/**
* 类型分布:key 为字典名称(sys_dict_data.dict_type = use_mode 的 dict_label),value 为摄像机通道数。
*
* <p>类型名目跟随字典配置,不固定为三种,故用有序 Map 承载,并按字典顺序返回(含 0 值)。</p>
*/
private Map<String, Long> typeDistribution = new LinkedHashMap<>();
/**
* 光谱分布:key 为通道类型字典名称(sys_dict_data.dict_type = channel_type 的 dict_label),value 为摄像机通道数。
*
* <p>名目同样跟随字典配置,按字典顺序返回(含 0 值),末项为兜底名目“其他”。</p>
*/
private Map<String, Long> spectrumDistribution = new LinkedHashMap<>();
}

+ 19
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/DefectStatsVo.java View File

@ -0,0 +1,19 @@
package com.inspect.statistics.domain.vo;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
/**
* 接口7 巡视缺陷统计:按缺陷级别(一般缺陷、严重缺陷、危急缺陷)统计。
*/
@Setter
@Getter
public class DefectStatsVo {
/** 当前查询时间维度 */
private String period;
/** 各级别统计,固定返回全部级别(含 0 值) */
private List<LevelStatVo> items = new ArrayList<>();
}

+ 21
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/DeviceOnlineStatVo.java View File

@ -0,0 +1,21 @@
package com.inspect.statistics.domain.vo;
import lombok.Getter;
import lombok.Setter;
/**
* 设备在线统计:总数、在线数、离线数。
*
* <p>设备总览({@link DeviceStatsVo})与摄像机统计({@link CameraStatsVo})共用该结构,
* 保证同一口径只统计一次。</p>
*/
@Setter
@Getter
public class DeviceOnlineStatVo {
/** 总数 */
private Long total = 0L;
/** 在线数 */
private Long online = 0L;
/** 离线数 */
private Long offline = 0L;
}

+ 17
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/DeviceStatsVo.java View File

@ -0,0 +1,17 @@
package com.inspect.statistics.domain.vo;
import lombok.Getter;
import lombok.Setter;
/**
* 接口1 巡视设备统计:设备总览。
*
* <p>只承载各类型设备的在线总览;摄像机明细(类型分布、光谱分布)见 {@link CameraStatsVo}。</p>
*/
@Setter
@Getter
public class DeviceStatsVo {
private DeviceOnlineStatVo camera = new DeviceOnlineStatVo();
private DeviceOnlineStatVo robot = new DeviceOnlineStatVo();
private DeviceOnlineStatVo drone = new DeviceOnlineStatVo();
}

+ 18
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/LevelStatVo.java View File

@ -0,0 +1,18 @@
package com.inspect.statistics.domain.vo;
import lombok.Getter;
import lombok.Setter;
/**
* 级别统计项:告警级别、缺陷级别共用,结构完全一致故不重复定义。
*/
@Setter
@Getter
public class LevelStatVo {
/** 级别名称 */
private String level;
/** 数量 */
private Long count = 0L;
/** 占比(%),保留 2 位小数 */
private Double percent = 0D;
}

+ 46
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/ManualConfirmDefectVo.java View File

@ -0,0 +1,46 @@
package com.inspect.statistics.domain.vo;
import lombok.Getter;
import lombok.Setter;
/**
* 接口5 人工确认缺陷统计:严重度分布 + 审核相关完成率。
*/
@Setter
@Getter
public class ManualConfirmDefectVo {
/** 当前查询时间维度 */
private String period;
/** 人工确认缺陷严重度分布 */
private Level level = new Level();
/** 审核相关完成率 */
private Rates rates = new Rates();
/**
* 严重度分布。
*/
@Setter
@Getter
public static class Level {
/** 高严重度 */
private Long high = 0L;
/** 中严重度 */
private Long medium = 0L;
/** 低严重度 */
private Long low = 0L;
}
/**
* 审核相关完成率。
*/
@Setter
@Getter
public static class Rates {
/** 巡视告警人工审核完成率(%) */
private Double alarmManualReviewRate = 0D;
/** 巡视告警准确率(%) */
private Double alarmAccuracyRate = 0D;
/** 巡视结果人工审核完成率(%) */
private Double resultManualReviewRate = 0D;
}
}

+ 28
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/RobotDroneStatusVo.java View File

@ -0,0 +1,28 @@
package com.inspect.statistics.domain.vo;
import lombok.Getter;
import lombok.Setter;
/**
* 接口2 机器人/无人工况:连续运行天数、巡检天数、巡检出勤率。
*/
@Setter
@Getter
public class RobotDroneStatusVo {
private Item robot = new Item();
private Item drone = new Item();
/**
* 单个设备的运行工况。
*/
@Setter
@Getter
public static class Item {
/** 连续运行天数 */
private Integer runDays = 0;
/** 巡检天数 */
private Integer patrolDays = 0;
/** 巡检出勤率(%),保留 2 位小数 */
private Double attendanceRate = 0D;
}
}

+ 27
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/ScreenResultVo.java View File

@ -0,0 +1,27 @@
package com.inspect.statistics.domain.vo;
import lombok.Getter;
import lombok.Setter;
/**
* 大屏统计接口统一返回载体:业务数据 + 数据统计时间。
*
* <p>updateTime 为该份数据的实际生成时间;命中 Redis 缓存时保持为缓存生成时间,
* 前端可据此判断数据新鲜度。</p>
*
* @param <T> 业务数据类型
*/
@Setter
@Getter
public class ScreenResultVo<T> {
private T data;
private String updateTime;
public ScreenResultVo() {
}
public ScreenResultVo(T data, String updateTime) {
this.data = data;
this.updateTime = updateTime;
}
}

+ 65
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/domain/vo/TaskStatsVo.java View File

@ -0,0 +1,65 @@
package com.inspect.statistics.domain.vo;
import lombok.Getter;
import lombok.Setter;
import java.util.ArrayList;
import java.util.List;
/**
* 接口4 巡视任务统计:巡视类型统计 + 巡视率指标 + 巡视状态统计。
*
* <p>率指标(漏检率、闭环率)与数量类指标拆开返回,前端无需按名称硬编码区分。</p>
*/
@Setter
@Getter
public class TaskStatsVo {
/** 当前查询时间维度 */
private String period;
/** 巡视类型统计(数量项) */
private List<TypeStat> typeStats = new ArrayList<>();
/** 巡视率指标 */
private List<RateStat> rateStats = new ArrayList<>();
/** 巡视状态统计 */
private List<StatusStat> statusStats = new ArrayList<>();
/**
* 巡视类型统计项。
*/
@Setter
@Getter
public static class TypeStat {
/** 巡视类型名称 */
private String type;
/** 数量 */
private Long count = 0L;
/** 占比(%),保留 2 位小数 */
private Double percent = 0D;
}
/**
* 巡视率指标项。
*/
@Setter
@Getter
public static class RateStat {
/** 率指标名称 */
private String name;
/** 比率(%),保留 2 位小数 */
private Double rate = 0D;
}
/**
* 巡视状态统计项。
*/
@Setter
@Getter
public static class StatusStat {
/** 状态名称 */
private String status;
/** 数量 */
private Long count = 0L;
/** 占比(%),保留 2 位小数 */
private Double percent = 0D;
}
}

+ 201
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/mapper/PatrolScreenMapper.java View File

@ -0,0 +1,201 @@
package com.inspect.statistics.mapper;
import com.inspect.statistics.domain.DeviceOnlineEvent;
import com.inspect.statistics.domain.PatrolScreenCount;
import com.inspect.statistics.domain.PatrolScreenDict;
import com.inspect.statistics.domain.PatrolScreenRate;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
/**
* 巡视大屏设备类统计的数据访问。
*
* <p>设备台账(basedata_eqpbook)可复用 {@code IPatrolResultService#selectBasedataEqpBookList},
* 但摄像机通道表在其它模块中的查询无法从本模块直接引用,故大屏自持该查询。</p>
*/
@Mapper
public interface PatrolScreenMapper {
/**
* 查询指定设备类型下全部未删除通道的通道编码(摄像机在线状态按通道口径统计)。
*
* @param eqpBookType basedata_eqpbook.type,摄像机为 10
* @return 通道编码列表
*/
List<String> selectCameraChannelCodes(@Param("eqpBookType") String eqpBookType);
/**
* 查询字典项(字典值 + 字典名称),按字典顺序返回。
*
* @param dictType sys_dict_data.dict_type
* @return 字典项列表
*/
List<PatrolScreenDict> selectDictData(@Param("dictType") String dictType);
/**
* 摄像机类型分布:type = 10 摄像机名下未删除通道按通道自身 use_mode 分组计数(通道口径)。
*
* @param eqpBookType basedata_eqpbook.type,摄像机为 10
* @return 「use_mode - 通道数」列表
*/
List<PatrolScreenCount> selectCameraUseModeCounts(@Param("eqpBookType") String eqpBookType);
/**
* 摄像机类型分布兜底计数:use_mode 未配置(为空)或取值不在 use_mode 字典内的通道,
* 统一计入大屏名目“其他”。
*
* @param eqpBookType basedata_eqpbook.type,摄像机为 10
* @param dictType sys_dict_data.dict_type,摄像机使用类型为 use_mode
* @return 未归类通道数
*/
Long selectCameraOtherTypeCount(@Param("eqpBookType") String eqpBookType,
@Param("dictType") String dictType);
/**
* 摄像机光谱分布:type = 10 摄像机名下未删除通道按通道自身 channel_type 分组计数(通道口径)。
*
* <p>名目取值跟随 sys_dict_data 的 dict_type = channel_type 配置,不写死取值。</p>
*
* @param eqpBookType basedata_eqpbook.type,摄像机为 10
* @return 「channel_type - 通道数」列表
*/
List<PatrolScreenCount> selectCameraChannelTypeCounts(@Param("eqpBookType") String eqpBookType);
/**
* 摄像机光谱分布兜底计数:channel_type 未配置(为空)或取值不在 channel_type 字典内的通道,
* 统一计入大屏名目“其他”。
*
* @param eqpBookType basedata_eqpbook.type,摄像机为 10
* @param dictType sys_dict_data.dict_type,通道类型为 channel_type
* @return 未归类通道数
*/
Long selectCameraOtherChannelTypeCount(@Param("eqpBookType") String eqpBookType,
@Param("dictType") String dictType);
/**
* 查询指定设备类型下未删除设备的在线/离线事件,按设备、事件时间升序返回。
*
* @param eqpBookTypes basedata_eqpbook.type 取值集合
* @param onlineSince 事件时间下限(yyyy-MM-dd),限制回溯窗口
* @return 在线/离线事件列表
*/
List<DeviceOnlineEvent> selectDeviceOnlineEvents(@Param("eqpBookTypes") List<String> eqpBookTypes,
@Param("onlineSince") String onlineSince);
/**
* 查询属于指定 dev_type 的巡检执行日期(去重),用于巡检天数统计。
*
* @param devType patrol_task.dev_type 取值,逗号分隔多值用 find_in_set 匹配
* @return 日期字符串(yyyy-MM-dd)列表
*/
List<String> selectPatrolDays(@Param("devType") String devType);
/**
* 巡视类型数量:按“已执行任务”统计——patrol_task_status 内联 patrol_task,
* 按 task_patrolled_id 去重后按 patrol_task.type 分组,与巡视状态统计同源。
*
* @param beginTime 统计开始时间(任务开始时间 start_time)
* @param endTime 统计结束时间
* @param excludeTypes 需排除的 patrol_task.type 取值(联动 5、静默 6)
* @return 「巡视类型编码 - 数量」列表
*/
List<PatrolScreenCount> selectTaskTypeCounts(@Param("beginTime") String beginTime,
@Param("endTime") String endTime,
@Param("excludeTypes") List<String> excludeTypes);
/**
* 巡视状态数量:patrol_task_status 按 task_state 分组,
* 按巡视任务执行ID task_patrolled_id 去重计数(一条执行实例计一次)。
*
* @param beginTime 统计开始时间
* @param endTime 统计结束时间
* @param excludeTypes 需排除的 patrol_task.type 取值(联动 5、静默 6)
* @return 「任务状态编码 - 数量」列表
*/
List<PatrolScreenCount> selectTaskStatusCounts(@Param("beginTime") String beginTime,
@Param("endTime") String endTime,
@Param("excludeTypes") List<String> excludeTypes);
/**
* 任务终止数取数:口径复制自 inspect-main-client 的 ClientStatisticsMapper#queryTaskDone,
* totalNum 为 patrol_task_status 任务总数,validNum 为终止任务数(task_state = 4)。
*
* <p>大屏“任务执行闭环率”按名称字面口径取反:闭环率 = (totalNum - validNum) / totalNum。</p>
*
* @param beginTime 统计开始时间
* @param endTime 统计结束时间
* @return 总数与终止数
*/
PatrolScreenRate selectTaskDoneRate(@Param("beginTime") String beginTime,
@Param("endTime") String endTime);
/**
* 巡检点位覆盖数取数:口径复制自 inspect-main-client 的 ClientStatisticsMapper#queryPointNum,
* totalNum 为巡检点位总数,validNum 为已巡检点位数量(patrol_result 出现过的点位)。
*
* <p>大屏“巡视点位漏检率”按名称字面口径取反:漏检率 = (totalNum - validNum) / totalNum。</p>
*
* @param beginTime 统计开始时间
* @param endTime 统计结束时间
* @return 总数与已巡检数
*/
PatrolScreenRate selectPointNumRate(@Param("beginTime") String beginTime,
@Param("endTime") String endTime);
/**
* 巡视告警人工审核完成率取数:口径复制自 inspect-main-client 的 ClientStatisticsMapper#queryPersonExamine。
*
* @param beginTime 统计开始时间
* @param endTime 统计结束时间
* @return 总数与有效数
*/
PatrolScreenRate selectAlarmReviewRate(@Param("beginTime") String beginTime,
@Param("endTime") String endTime);
/**
* 巡视告警准确率取数:口径复制自 inspect-main-client 的 ClientStatisticsMapper#queryWarning。
*
* @param beginTime 统计开始时间
* @param endTime 统计结束时间
* @return 总数与有效数
*/
PatrolScreenRate selectAlarmAccuracyRate(@Param("beginTime") String beginTime,
@Param("endTime") String endTime);
/**
* 巡视结果人工审核完成率取数:口径复制自 inspect-main-client 的 ClientStatisticsMapper#queryResultPersonExamine。
*
* @param beginTime 统计开始时间
* @param endTime 统计结束时间
* @return 总数与有效数
*/
PatrolScreenRate selectResultReviewRate(@Param("beginTime") String beginTime,
@Param("endTime") String endTime);
/**
* 告警记录数:result_analysis 中 result_type = 2 的记录。
*
* <p>本地 result_analysis 无告警级别字段,该批记录统一按大屏最低级别「预警」计入。</p>
*
* @param beginTime 统计开始时间
* @param endTime 统计结束时间
* @return 告警记录数
*/
Long selectAlarmCount(@Param("beginTime") String beginTime,
@Param("endTime") String endTime);
/**
* 缺陷数量:缺陷记录取 result_analysis 中 result_type = 0 的记录,时间取 create_time。
*
* <p>本地结果表没有缺陷等级字段(等级由站端判定,只落在同步镜像表),故只统计总量、不按等级分组。</p>
*
* @param beginTime 统计开始时间
* @param endTime 统计结束时间
* @return 缺陷数量
*/
Long selectDefectCount(@Param("beginTime") String beginTime,
@Param("endTime") String endTime);
}

+ 26
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/service/PatrolStatisticsService.java View File

@ -3,6 +3,14 @@ package com.inspect.statistics.service;
import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONArray;
import com.inspect.base.core.web.domain.AjaxResult; import com.inspect.base.core.web.domain.AjaxResult;
import com.inspect.statistics.domain.StatisticsPatrolResult; import com.inspect.statistics.domain.StatisticsPatrolResult;
import com.inspect.statistics.domain.vo.AlarmStatsVo;
import com.inspect.statistics.domain.vo.CameraStatsVo;
import com.inspect.statistics.domain.vo.DefectStatsVo;
import com.inspect.statistics.domain.vo.DeviceStatsVo;
import com.inspect.statistics.domain.vo.ManualConfirmDefectVo;
import com.inspect.statistics.domain.vo.RobotDroneStatusVo;
import com.inspect.statistics.domain.vo.ScreenResultVo;
import com.inspect.statistics.domain.vo.TaskStatsVo;
import java.io.IOException; import java.io.IOException;
import java.util.HashMap; import java.util.HashMap;
@ -30,4 +38,22 @@ public interface PatrolStatisticsService {
void configExport(List<StatisticsPatrolResult> resultList, StatisticsPatrolResult result, HttpServletResponse response) throws IOException; void configExport(List<StatisticsPatrolResult> resultList, StatisticsPatrolResult result, HttpServletResponse response) throws IOException;
HashMap<String, String> colMap(); HashMap<String, String> colMap();
/*
* 以下为「巡视大屏」统计接口,统计口径见 docs/巡视大屏独立接口文档.md。
* period 取值 day/week/month/year,仅 deviceStats/robotDroneStatus/cameraStats 无该参数。
*/
ScreenResultVo<DeviceStatsVo> deviceStats();
ScreenResultVo<RobotDroneStatusVo> robotDroneStatus();
ScreenResultVo<CameraStatsVo> cameraStats();
ScreenResultVo<TaskStatsVo> taskStats(String period);
ScreenResultVo<ManualConfirmDefectVo> manualConfirmDefect(String period);
ScreenResultVo<AlarmStatsVo> alarmStats(String period);
ScreenResultVo<DefectStatsVo> defectStats(String period);
} }

+ 764
- 0
inspect-main/inspect-main-statistics/src/main/java/com/inspect/statistics/service/impl/PatrolStatisticsServiceImpl.java View File

@ -4,20 +4,53 @@ import cn.hutool.poi.excel.ExcelWriter;
import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject; import com.alibaba.fastjson.JSONObject;
import com.alibaba.nacos.common.utils.CollectionUtils; import com.alibaba.nacos.common.utils.CollectionUtils;
import com.inspect.base.core.constant.MontCacheConstants;
import com.inspect.base.core.exception.ServiceException; import com.inspect.base.core.exception.ServiceException;
import com.inspect.base.core.utils.DateUtils;
import com.inspect.base.core.utils.StringUtils; import com.inspect.base.core.utils.StringUtils;
import com.inspect.base.core.web.domain.AjaxResult; import com.inspect.base.core.web.domain.AjaxResult;
import com.inspect.base.redis.service.RedisService;
import com.inspect.partrolresult.service.IPatrolResultService;
import com.inspect.statistics.constant.PatrolScreenConst;
import com.inspect.statistics.domain.DeviceOnlineEvent;
import com.inspect.statistics.domain.PatrolScreenCount;
import com.inspect.statistics.domain.PatrolScreenDict;
import com.inspect.statistics.domain.PatrolScreenRate;
import com.inspect.statistics.domain.StatisticsPatrolResult; import com.inspect.statistics.domain.StatisticsPatrolResult;
import com.inspect.statistics.domain.vo.AlarmStatsVo;
import com.inspect.statistics.domain.vo.CameraStatsVo;
import com.inspect.statistics.domain.vo.DefectStatsVo;
import com.inspect.statistics.domain.vo.DeviceOnlineStatVo;
import com.inspect.statistics.domain.vo.DeviceStatsVo;
import com.inspect.statistics.domain.vo.LevelStatVo;
import com.inspect.statistics.domain.vo.ManualConfirmDefectVo;
import com.inspect.statistics.domain.vo.RobotDroneStatusVo;
import com.inspect.statistics.domain.vo.ScreenResultVo;
import com.inspect.statistics.domain.vo.TaskStatsVo;
import com.inspect.statistics.mapper.PatrolScreenMapper;
import com.inspect.statistics.mapper.StatisticsPatrolResultMapper; import com.inspect.statistics.mapper.StatisticsPatrolResultMapper;
import com.inspect.statistics.service.PatrolStatisticsService; import com.inspect.statistics.service.PatrolStatisticsService;
import com.inspect.system.base.openDomain.BasedataEqpBookMoMain;
import java.io.IOException; import java.io.IOException;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.time.DayOfWeek;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Comparator; import java.util.Comparator;
import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import javax.annotation.Resource; import javax.annotation.Resource;
import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponse;
@ -38,6 +71,15 @@ public class PatrolStatisticsServiceImpl implements PatrolStatisticsService {
@Autowired @Autowired
private MessageUtils messageUtils; private MessageUtils messageUtils;
@Resource
RedisService redisService;
@Resource
IPatrolResultService patrolResultService;
@Resource
PatrolScreenMapper screenMapper;
public List<StatisticsPatrolResult> queryPatrolResult(StatisticsPatrolResult result) { public List<StatisticsPatrolResult> queryPatrolResult(StatisticsPatrolResult result) {
return this.resultMapper.queryPatrolResult(result); return this.resultMapper.queryPatrolResult(result);
} }
@ -213,4 +255,726 @@ public class PatrolStatisticsServiceImpl implements PatrolStatisticsService {
colMap.put("dataType", messageUtils.get("巡视结果数据来源")); colMap.put("dataType", messageUtils.get("巡视结果数据来源"));
return colMap; return colMap;
} }
/*
* ==================== 巡视大屏统计接口(口径见 docs/巡视大屏独立接口文档.md) ====================
*/
@Override
public ScreenResultVo<DeviceStatsVo> deviceStats() {
return this.loadWithCache(PatrolScreenConst.CACHE_DEVICE_STATS, PatrolScreenConst.TTL_LOW_FREQUENCY,
this::countDeviceStats);
}
/**
* 巡视设备统计口径:basedata_eqpbook.type 取 1/2/3(机器人)、13(无人机)、10(摄像机);
* 机器人与无人机按设备编码判定在线,摄像机按通道口径统计(见 countCamera),已删除设备不参与统计。
*/
private DeviceStatsVo countDeviceStats() {
DeviceStatsVo deviceStats = new DeviceStatsVo();
List<BasedataEqpBookMoMain> eqpBooks = this.patrolResultService
.selectBasedataEqpBookList(new BasedataEqpBookMoMain());
if (eqpBooks != null) {
for (BasedataEqpBookMoMain eqpBook : eqpBooks) {
if (!PatrolScreenConst.FLAG_NORMAL.equals(eqpBook.getDelFlag())) {
continue;
}
DeviceOnlineStatVo deviceCategory = robotOrDroneCategory(deviceStats, eqpBook.getType());
if (deviceCategory == null) {
continue;
}
appendDevice(deviceCategory, this.isDeviceOnline(eqpBook.getPatrolDeviceCode()));
}
}
this.countCamera(deviceStats.getCamera());
return deviceStats;
}
/**
* 摄像机统计:type=10 的摄像机按 basedata_eqpbook_channel 通道统计,
* 通道在线判定为 Redis 存在 mont_patrol_device_channel_online_status:{channelCode}。
*/
private void countCamera(DeviceOnlineStatVo camera) {
List<String> channelCodes = this.screenMapper.selectCameraChannelCodes(PatrolScreenConst.DEVICE_TYPE_CAMERA);
if (channelCodes == null || channelCodes.isEmpty()) {
return;
}
for (String channelCode : channelCodes) {
appendDevice(camera, this.isChannelOnline(channelCode));
}
}
/**
* 按 basedata_eqpbook.type 定位机器人与无人机分组,其余类型(含摄像机)返回 null(不计入本方法统计)。
*/
private static DeviceOnlineStatVo robotOrDroneCategory(DeviceStatsVo deviceStats, String type) {
if (PatrolScreenConst.DEVICE_TYPE_DRONE.equals(type)) {
return deviceStats.getDrone();
}
if (PatrolScreenConst.DEVICE_TYPE_ROBOTS.contains(type)) {
return deviceStats.getRobot();
}
return null;
}
/**
* 累加一台设备(或一个通道)的总数以及在线的在线数/离线数。
*/
private static void appendDevice(DeviceOnlineStatVo deviceCategory, boolean online) {
deviceCategory.setTotal(deviceCategory.getTotal() + 1L);
if (online) {
deviceCategory.setOnline(deviceCategory.getOnline() + 1L);
} else {
deviceCategory.setOffline(deviceCategory.getOffline() + 1L);
}
}
/**
* 设备在线判定:Redis 存在 mont_patrol_device_online_status:{patrolDeviceCode} 即在线。
*/
private boolean isDeviceOnline(String patrolDeviceCode) {
return this.hasOnlineKey(MontCacheConstants.MONT_PATROL_DEVICE_ONLINE_STATUS, patrolDeviceCode);
}
/**
* 通道在线判定:Redis 存在 mont_patrol_device_channel_online_status:{channelCode} 即在线。
*/
private boolean isChannelOnline(String channelCode) {
return this.hasOnlineKey(MontCacheConstants.MONT_PATROL_DEVICE_CHANNEL_ONLINE_STATUS, channelCode);
}
/**
* 在线判定通用实现:编码为空按离线;Redis 查询异常按离线处理,避免大屏接口整体失败。
*/
private boolean hasOnlineKey(String keyPrefix, String code) {
if (StringUtils.isEmpty(code)) {
return false;
}
try {
return Boolean.TRUE.equals(this.redisService.hasKey(keyPrefix + code));
} catch (Exception e) {
this.logger.warn("[SCREEN] check online status fail, key: {}", keyPrefix + code, e);
return false;
}
}
@Override
public ScreenResultVo<RobotDroneStatusVo> robotDroneStatus() {
return this.loadWithCache(PatrolScreenConst.CACHE_ROBOT_DRONE_STATUS, PatrolScreenConst.TTL_LOW_FREQUENCY,
this::countRobotDroneStatus);
}
/**
* 机器人/无人工况统计口径:
* <ol>
* <li>设备来源:basedata_eqpbook 中 del_flag = '0' 的机器人(type 1/2/3)与无人机(type 13)。</li>
* <li>巡检天数:patrol_task_status 按 task_code 关联 patrol_task,用 patrol_task.dev_type 归类
* (1 机器人、0 无人机);dev_type 为逗号分隔多值,用 find_in_set 匹配;排除尚未执行(5)与
* 超过期限(6)的任务,按自然日去重计数。该口径按设备类型统计,与具体是哪一台设备无关。</li>
* <li>连续运行天数:basedata_mont_patdev_online 记录的是状态变化事件(不是状态快照),
* 先按设备还原在线区间,再折算为“存在在线时刻”的自然日集合,取截至今日的连续天数,同类设备取最大值。</li>
* <li>巡检出勤率 = 巡检天数 / 连续运行天数 * 100,保留 2 位小数;连续运行天数为 0 时返回 0。</li>
* </ol>
*/
private RobotDroneStatusVo countRobotDroneStatus() {
RobotDroneStatusVo robotDroneStatus = new RobotDroneStatusVo();
Map<Long, String> robotDevices = new LinkedHashMap<>();
Map<Long, String> droneDevices = new LinkedHashMap<>();
this.collectRobotDroneDevices(robotDevices, droneDevices);
robotDroneStatus.getRobot().setPatrolDays(this.countPatrolDays(PatrolScreenConst.PATROL_DEV_TYPE_ROBOT));
robotDroneStatus.getDrone().setPatrolDays(this.countPatrolDays(PatrolScreenConst.PATROL_DEV_TYPE_DRONE));
if (!robotDevices.isEmpty() || !droneDevices.isEmpty()) {
Map<Long, List<DeviceOnlineEvent>> eventsByDevice = this.loadDeviceOnlineEvents();
robotDroneStatus.getRobot().setRunDays(this.maxRunDays(robotDevices, eventsByDevice));
robotDroneStatus.getDrone().setRunDays(this.maxRunDays(droneDevices, eventsByDevice));
}
robotDroneStatus.getRobot().setAttendanceRate(percentage(robotDroneStatus.getRobot().getPatrolDays().longValue(),
robotDroneStatus.getRobot().getRunDays().longValue()));
robotDroneStatus.getDrone().setAttendanceRate(percentage(robotDroneStatus.getDrone().getPatrolDays().longValue(),
robotDroneStatus.getDrone().getRunDays().longValue()));
return robotDroneStatus;
}
/**
* 收集机器人与无人机台账,key 为设备台账 ID(basedata_mont_patdev_online.eqp_book_id),value 为设备编码。
*/
private void collectRobotDroneDevices(Map<Long, String> robotDevices, Map<Long, String> droneDevices) {
List<BasedataEqpBookMoMain> eqpBooks = this.patrolResultService
.selectBasedataEqpBookList(new BasedataEqpBookMoMain());
if (eqpBooks == null) {
return;
}
for (BasedataEqpBookMoMain eqpBook : eqpBooks) {
if (!PatrolScreenConst.FLAG_NORMAL.equals(eqpBook.getDelFlag()) || eqpBook.getEqpBookId() == null) {
continue;
}
if (PatrolScreenConst.DEVICE_TYPE_DRONE.equals(eqpBook.getType())) {
droneDevices.put(eqpBook.getEqpBookId(), eqpBook.getPatrolDeviceCode());
} else if (PatrolScreenConst.DEVICE_TYPE_ROBOTS.contains(eqpBook.getType())) {
robotDevices.put(eqpBook.getEqpBookId(), eqpBook.getPatrolDeviceCode());
}
}
}
/**
* 巡检天数:patrol_task_status 关联 patrol_task 后按 dev_type 归类,统计执行过的不同自然日数量。
*
* <p>按确认口径不做任务状态排除,6 种任务状态全部统计。</p>
*/
private int countPatrolDays(String devType) {
List<String> patrolDays = this.screenMapper.selectPatrolDays(devType);
return patrolDays == null ? 0 : patrolDays.size();
}
/**
* 加载机器人与无人机的在线/离线事件,按设备台账 ID 分组(SQL 已按设备、事件时间升序返回)。
*/
private Map<Long, List<DeviceOnlineEvent>> loadDeviceOnlineEvents() {
List<String> deviceTypes = new ArrayList<>(PatrolScreenConst.DEVICE_TYPE_ROBOTS);
deviceTypes.add(PatrolScreenConst.DEVICE_TYPE_DRONE);
String onlineSince = LocalDate.now().minusDays(PatrolScreenConst.MAX_RUN_DAYS).toString();
List<DeviceOnlineEvent> events = this.screenMapper.selectDeviceOnlineEvents(deviceTypes, onlineSince);
Map<Long, List<DeviceOnlineEvent>> eventsByDevice = new LinkedHashMap<>();
if (events == null) {
return eventsByDevice;
}
for (DeviceOnlineEvent event : events) {
if (event.getEqpBookId() == null || event.getActionTime() == null) {
continue;
}
eventsByDevice.computeIfAbsent(event.getEqpBookId(), key -> new ArrayList<>()).add(event);
}
return eventsByDevice;
}
/**
* 同类设备中连续运行天数的最大值。
*/
private int maxRunDays(Map<Long, String> devices, Map<Long, List<DeviceOnlineEvent>> eventsByDevice) {
int maxRunDays = 0;
for (Map.Entry<Long, String> device : devices.entrySet()) {
int runDays = this.countRunDays(eventsByDevice.get(device.getKey()), device.getValue());
if (runDays > maxRunDays) {
maxRunDays = runDays;
}
}
return maxRunDays;
}
/**
* 单台设备的连续运行天数:把状态变化事件还原成在线区间,折算为存在在线时刻的自然日集合,
* 再取截至今日的连续天数。
*
* <p>一天只要发生过在线(含跨天的在线区间),该自然日即视为运行日,与项目内
* basedata_statis_robotuav_online_day 的“时段内出现过在线即算在线”口径保持一致。</p>
*/
private int countRunDays(List<DeviceOnlineEvent> events, String patrolDeviceCode) {
if (events == null || events.isEmpty()) {
return 0;
}
Set<LocalDate> onlineDays = new HashSet<>();
LocalDate intervalStart = null;
for (DeviceOnlineEvent event : events) {
LocalDate eventDay = toLocalDate(event.getActionTime());
if (PatrolScreenConst.ONLINE_STATUS.equals(event.getStatus())) {
if (intervalStart == null) {
intervalStart = eventDay;
}
continue;
}
if (intervalStart != null) {
this.markOnlineDays(onlineDays, intervalStart, eventDay);
intervalStart = null;
}
}
if (intervalStart != null) {
// 末次事件为上线且此后没有下线事件:先用 Redis 现行在线状态确认今天,
// 确认在线则区间延续到今天,否则收敛到该上线事件当天,避免虚增连续天数。
this.markOnlineDays(onlineDays, intervalStart,
this.isDeviceOnline(patrolDeviceCode) ? LocalDate.now() : intervalStart);
}
return this.countContinuousDays(onlineDays);
}
/**
* 把在线区间覆盖的自然日全部计入在线日集合。
*/
private void markOnlineDays(Set<LocalDate> onlineDays, LocalDate start, LocalDate end) {
if (start == null || end == null || end.isBefore(start)) {
return;
}
LocalDate day = start;
while (!day.isAfter(end)) {
onlineDays.add(day);
day = day.plusDays(1);
}
}
/**
* 统计截至今日的连续运行天数:今天不在在线日集合内说明当前未连续运行,返回 0。
*/
private int countContinuousDays(Set<LocalDate> onlineDays) {
if (onlineDays.isEmpty()) {
return 0;
}
int runDays = 0;
LocalDate day = LocalDate.now();
while (runDays < PatrolScreenConst.MAX_RUN_DAYS && onlineDays.contains(day)) {
runDays++;
day = day.minusDays(1);
}
return runDays;
}
/**
* 事件时间归属的自然日(服务器时区)。
*/
private static LocalDate toLocalDate(Date date) {
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}
@Override
public ScreenResultVo<CameraStatsVo> cameraStats() {
return this.loadWithCache(PatrolScreenConst.CACHE_CAMERA_STATS, PatrolScreenConst.TTL_LOW_FREQUENCY,
this::countCameraStats);
}
/**
* 摄像机统计口径(三项统一按摄像机通道统计,单位都是通道数):
* <ol>
* <li>总数/在线数/离线数与设备总览同源,统一取 deviceStats 的 camera 数据(通道口径),避免两处口径漂移。</li>
* <li>类型分布:type = 10(摄像机)且 del_flag = '0' 的摄像机名下、delete_flag = '0' 的通道,
* 按通道自身 use_mode 分组计数;类型名目取 sys_dict_data 中 dict_type = use_mode 的 dict_label
* (dict_value 对应 use_mode 值),按字典顺序返回;use_mode 未配置或不在字典内的通道统一计入“其他”。</li>
* <li>光谱分布:同一范围内的通道按自身 channel_type 分组计数;名目取 sys_dict_data 中
* dict_type = channel_type 的 dict_label,按字典顺序返回;channel_type 未配置或不在字典内的通道统一计入“其他”。</li>
* </ol>
*/
private CameraStatsVo countCameraStats() {
CameraStatsVo cameraStats = new CameraStatsVo();
// 摄像机总数/在线数/离线数与设备总览同源,统一取 deviceStats 的 camera 数据,避免两处口径漂移
DeviceOnlineStatVo camera = this.deviceStats().getData().getCamera();
cameraStats.setTotal(camera.getTotal());
cameraStats.setOnline(camera.getOnline());
cameraStats.setOffline(camera.getOffline());
cameraStats.setOnlineRate(percentage(camera.getOnline(), camera.getTotal()));
cameraStats.setOfflineRate(percentage(camera.getOffline(), camera.getTotal()));
cameraStats.setTypeDistribution(this.countCameraTypeDistribution());
cameraStats.setSpectrumDistribution(this.countCameraSpectrumDistribution());
return cameraStats;
}
/**
* 摄像机类型分布:名目取自 use_mode 字典(含 0 值),数量按摄像机通道 basedata_eqpbook_channel.use_mode 分组统计;
* use_mode 未配置或不在字典内的通道统一计入兜底名目“其他”。
*/
private Map<String, Long> countCameraTypeDistribution() {
return this.dictDistribution(PatrolScreenConst.DICT_USE_MODE,
this.toCountMap(this.screenMapper.selectCameraUseModeCounts(PatrolScreenConst.DEVICE_TYPE_CAMERA)),
this.screenMapper.selectCameraOtherTypeCount(
PatrolScreenConst.DEVICE_TYPE_CAMERA, PatrolScreenConst.DICT_USE_MODE));
}
/**
* 摄像机光谱分布:名目取自 channel_type 字典(含 0 值),数量按摄像机通道 basedata_eqpbook_channel.channel_type 分组统计;
* channel_type 未配置或不在字典内的通道统一计入兜底名目“其他”。
*/
private Map<String, Long> countCameraSpectrumDistribution() {
return this.dictDistribution(PatrolScreenConst.DICT_CHANNEL_TYPE,
this.toCountMap(this.screenMapper.selectCameraChannelTypeCounts(PatrolScreenConst.DEVICE_TYPE_CAMERA)),
this.screenMapper.selectCameraOtherChannelTypeCount(
PatrolScreenConst.DEVICE_TYPE_CAMERA, PatrolScreenConst.DICT_CHANNEL_TYPE));
}
/**
* 按字典名目组装摄像机通道分布:名目取字典(含 0 值,按字典顺序),数量按字典值回填,末项固定为兜底名目“其他”。
*
* @param dictType sys_dict_data.dict_type
* @param valueCounts 「字典值 - 数量」映射
* @param otherCount 取值不在字典内的数量
*/
private Map<String, Long> dictDistribution(String dictType, Map<String, Long> valueCounts, Long otherCount) {
Map<String, Long> distribution = new LinkedHashMap<>();
List<PatrolScreenDict> dictItems = this.screenMapper.selectDictData(dictType);
if (dictItems != null) {
for (PatrolScreenDict dictItem : dictItems) {
if (StringUtils.isEmpty(dictItem.getName())) {
continue;
}
// 字典维护时可能带入首尾空白(如回车),统一去除,避免 JSON key 带控制字符
distribution.put(dictItem.getName().trim(), this.countOfCode(valueCounts, dictItem.getCode()));
}
}
distribution.put(PatrolScreenConst.CAMERA_OTHER, otherCount == null ? 0L : otherCount);
return distribution;
}
@Override
public ScreenResultVo<TaskStatsVo> taskStats(String period) {
String queryPeriod = this.normalizePeriod(period);
return this.loadWithCache(PatrolScreenConst.CACHE_TASK_STATS + queryPeriod, PatrolScreenConst.TTL_MIDDLE_FREQUENCY,
() -> this.countTaskStats(queryPeriod));
}
/**
* 巡视任务统计口径:
* <ol>
* <li>巡视类型:按“已执行任务”统计——patrol_task_status 内联 patrol_task,按 task_patrolled_id 去重后
* 按 patrol_task.type 分组(1 例行、2 特殊、3 专项、4 自定义),按任务开始时间 start_time 落在时间窗内计数,
* 排除联动(type=5)/静默(type=6)任务。大屏名目中的“全面巡视”“熄灯巡视”库中无对应取值,
* 固定返回 0。</li>
* <li>巡视状态:patrol_task_status 按 task_state 分组,按巡视任务执行ID task_patrolled_id 去重计数,
* 排除联动(type=5)/静默(type=6)任务,按任务开始时间 start_time 落在时间窗内计数。</li>
* <li>巡视率:沿用客户端既有口径,比率 = validNum / totalNum * 100。</li>
* </ol>
*/
private TaskStatsVo countTaskStats(String queryPeriod) {
TaskStatsVo taskStats = new TaskStatsVo();
taskStats.setPeriod(queryPeriod);
String[] range = this.periodRange(queryPeriod);
Map<String, Long> typeCounts = this.toCountMap(
this.screenMapper.selectTaskTypeCounts(range[0], range[1], PatrolScreenConst.PATROL_TYPE_EXCLUDED));
List<TaskStatsVo.TypeStat> typeStats = this.emptyTypeStats();
long typeTotal = 0L;
for (int i = 0; i < typeStats.size(); i++) {
TaskStatsVo.TypeStat typeStat = typeStats.get(i);
typeStat.setCount(this.countOfCode(typeCounts, PatrolScreenConst.PATROL_TYPE_CODES.get(i)));
typeTotal += typeStat.getCount();
}
for (TaskStatsVo.TypeStat typeStat : typeStats) {
typeStat.setPercent(percentage(typeStat.getCount(), typeTotal));
}
taskStats.setTypeStats(typeStats);
Map<String, Long> statusCounts = this.toCountMap(
this.screenMapper.selectTaskStatusCounts(range[0], range[1], PatrolScreenConst.PATROL_TYPE_EXCLUDED));
List<TaskStatsVo.StatusStat> statusStats = this.emptyStatusStats();
long statusTotal = 0L;
for (int i = 0; i < statusStats.size(); i++) {
TaskStatsVo.StatusStat statusStat = statusStats.get(i);
statusStat.setCount(this.countOfCode(statusCounts, PatrolScreenConst.PATROL_STATUS_CODES.get(i)));
statusTotal += statusStat.getCount();
}
for (TaskStatsVo.StatusStat statusStat : statusStats) {
statusStat.setPercent(percentage(statusStat.getCount(), statusTotal));
}
taskStats.setStatusStats(statusStats);
List<TaskStatsVo.RateStat> rateStats = this.emptyRateStats();
// 大屏按名称字面口径出数:漏检率 = 未巡检点位占比,闭环率 = 非终止任务占比
this.fillRate(rateStats, PatrolScreenConst.RATE_POINT_MISS,
this.inversePercentage(this.screenMapper.selectPointNumRate(range[0], range[1])));
this.fillRate(rateStats, PatrolScreenConst.RATE_TASK_CLOSED_LOOP,
this.inversePercentage(this.screenMapper.selectTaskDoneRate(range[0], range[1])));
taskStats.setRateStats(rateStats);
return taskStats;
}
@Override
public ScreenResultVo<ManualConfirmDefectVo> manualConfirmDefect(String period) {
String queryPeriod = this.normalizePeriod(period);
return this.loadWithCache(PatrolScreenConst.CACHE_MANUAL_CONFIRM_DEFECT + queryPeriod, PatrolScreenConst.TTL_MIDDLE_FREQUENCY,
() -> this.countManualConfirmDefect(queryPeriod));
}
/**
* 人工确认缺陷统计口径:三项完成率沿用客户端既有口径(比率 = validNum / totalNum * 100)——
* 告警人工审核完成率取 result_analysis 中告警(result_type = 0)的 res_status = 1 占比;
* 告警准确率取 patrol_result 未关联 patrol_result_aux 的占比;
* 结果人工审核完成率取 patrol_result 中 patrol_status = 1 的占比。
*
* <p>高/中/低严重度:本地结果表没有缺陷等级字段(等级由站端判定,只落在同步镜像表),
* 缺陷总量统一计入最低严重度 low,high / medium 固定为 0(口径与“巡视缺陷统计”同源,不按 res_status 过滤)。</p>
*/
private ManualConfirmDefectVo countManualConfirmDefect(String queryPeriod) {
ManualConfirmDefectVo manualConfirmDefect = new ManualConfirmDefectVo();
manualConfirmDefect.setPeriod(queryPeriod);
String[] range = this.periodRange(queryPeriod);
ManualConfirmDefectVo.Rates rates = manualConfirmDefect.getRates();
rates.setAlarmManualReviewRate(this.validPercentage(
this.screenMapper.selectAlarmReviewRate(range[0], range[1])));
rates.setAlarmAccuracyRate(this.validPercentage(
this.screenMapper.selectAlarmAccuracyRate(range[0], range[1])));
rates.setResultManualReviewRate(this.validPercentage(
this.screenMapper.selectResultReviewRate(range[0], range[1])));
// 高/中/低严重度:本地结果表没有缺陷等级字段(等级由站端判定,只落在同步镜像表),
// 参照“巡视告警统计”的既有口径,缺陷总量统一计入最低严重度 low,high / medium 固定为 0。
Long defectCount = this.screenMapper.selectDefectCount(range[0], range[1]);
ManualConfirmDefectVo.Level level = manualConfirmDefect.getLevel();
level.setHigh(0L);
level.setMedium(0L);
level.setLow(defectCount == null ? 0L : defectCount);
return manualConfirmDefect;
}
@Override
public ScreenResultVo<AlarmStatsVo> alarmStats(String period) {
String queryPeriod = this.normalizePeriod(period);
return this.loadWithCache(PatrolScreenConst.CACHE_ALARM_STATS + queryPeriod, PatrolScreenConst.TTL_HIGH_FREQUENCY,
() -> this.countAlarmStats(queryPeriod));
}
/**
* 巡视告警统计口径:告警记录取本地结果表 result_analysis.result_type = 2,时间取 create_time。
*
* <p>本地 result_analysis 没有告警级别字段,该批记录统一计入大屏最低级别「预警」,其余级别为 0;
* 若后续本地提供可用的等级字段,只需改这里与对应 SQL。</p>
*/
private AlarmStatsVo countAlarmStats(String queryPeriod) {
AlarmStatsVo alarmStats = new AlarmStatsVo();
alarmStats.setPeriod(queryPeriod);
String[] range = this.periodRange(queryPeriod);
Map<String, Long> levelCounts = new HashMap<>();
this.appendCount(levelCounts, PatrolScreenConst.ALARM_LEVELS.get(0),
this.screenMapper.selectAlarmCount(range[0], range[1]));
alarmStats.setItems(this.levelStats(PatrolScreenConst.ALARM_LEVELS, levelCounts));
return alarmStats;
}
@Override
public ScreenResultVo<DefectStatsVo> defectStats(String period) {
String queryPeriod = this.normalizePeriod(period);
return this.loadWithCache(PatrolScreenConst.CACHE_DEFECT_STATS + queryPeriod, PatrolScreenConst.TTL_HIGH_FREQUENCY,
() -> this.countDefectStats(queryPeriod));
}
/**
* 巡视缺陷统计口径:缺陷记录取本地结果表 result_analysis.result_type = 0,时间取 create_time。
*
* <p>本地结果表没有缺陷等级字段(等级由站端判定,只落在同步镜像表),故三个级别名目固定返回,
* 缺陷总量统一计入最低级别“一般缺陷”,其余级别为 0。</p>
*/
private DefectStatsVo countDefectStats(String queryPeriod) {
DefectStatsVo defectStats = new DefectStatsVo();
defectStats.setPeriod(queryPeriod);
String[] range = this.periodRange(queryPeriod);
Map<String, Long> levelCounts = new HashMap<>();
this.appendCount(levelCounts, PatrolScreenConst.DEFECT_LEVELS.get(0),
this.screenMapper.selectDefectCount(range[0], range[1]));
defectStats.setItems(this.levelStats(PatrolScreenConst.DEFECT_LEVELS, levelCounts));
return defectStats;
}
/**
* period 折算为统计时间窗,返回 [开始时间, 结束时间](yyyy-MM-dd HH:mm:ss):
* day 当天、week 本周(周一起)、month 本月、year 本年。
*/
private String[] periodRange(String queryPeriod) {
LocalDate today = LocalDate.now();
LocalDate begin;
switch (queryPeriod) {
case PatrolScreenConst.PERIOD_WEEK:
begin = today.with(DayOfWeek.MONDAY);
break;
case PatrolScreenConst.PERIOD_MONTH:
begin = today.withDayOfMonth(1);
break;
case PatrolScreenConst.PERIOD_YEAR:
begin = today.withDayOfYear(1);
break;
case PatrolScreenConst.PERIOD_DAY:
default:
begin = today;
break;
}
return new String[]{this.dayStart(begin), this.dayEnd(today)};
}
/** 自然日起点:某天 00:00:00。 */
private String dayStart(LocalDate day) {
return DateUtils.format(DateUtils.YYYY_MM_DD, DateUtils.toDate(day)) + " 00:00:00";
}
/** 自然日终点:某天 23:59:59。 */
private String dayEnd(LocalDate day) {
return DateUtils.format(DateUtils.YYYY_MM_DD, DateUtils.toDate(day)) + " 23:59:59";
}
/** 分组计数结果转「编码 - 数量」映射;编码为空的分组不进入名目统计。 */
private Map<String, Long> toCountMap(List<PatrolScreenCount> counts) {
Map<String, Long> countMap = new HashMap<>();
if (counts == null) {
return countMap;
}
for (PatrolScreenCount count : counts) {
if (StringUtils.isEmpty(count.getCode())) {
continue;
}
this.appendCount(countMap, count.getCode(), count.getNum());
}
return countMap;
}
/** 累加名目数量。 */
private void appendCount(Map<String, Long> counts, String name, Long num) {
long value = num == null ? 0L : num;
Long exist = counts.get(name);
counts.put(name, exist == null ? value : exist + value);
}
/** 按编码取分组数量;编码为空(大屏专有名目,库中无对应取值)时返回 0。 */
private long countOfCode(Map<String, Long> counts, String code) {
if (StringUtils.isEmpty(code)) {
return 0L;
}
Long count = counts.get(code);
return count == null ? 0L : count;
}
/** 按名目回填比率,未匹配到的名目保持 0。 */
private void fillRate(List<TaskStatsVo.RateStat> rateStats, String name, Double rate) {
for (TaskStatsVo.RateStat rateStat : rateStats) {
if (name.equals(rateStat.getName())) {
rateStat.setRate(rate == null ? 0D : rate);
return;
}
}
}
/** 客户端既有口径:比率 = validNum / totalNum * 100,保留 2 位小数。 */
private Double validPercentage(PatrolScreenRate rate) {
if (rate == null || rate.getTotalNum() == null || rate.getValidNum() == null) {
return 0D;
}
return percentage(rate.getValidNum().longValue(), rate.getTotalNum().longValue());
}
/**
* 按名称字面口径取反:比率 = (总数 - 有效数) / 总数 * 100,保留 2 位小数。
*
* <p>用于“漏检率 = 未巡检点位占比”“闭环率 = 非终止任务占比”这类与客户端上报口径方向相反的指标。</p>
*/
private Double inversePercentage(PatrolScreenRate rate) {
if (rate == null || rate.getTotalNum() == null || rate.getValidNum() == null) {
return 0D;
}
long total = rate.getTotalNum().longValue();
return percentage(total - rate.getValidNum().longValue(), total);
}
/** 按固定级别名目回填数量与占比,保证大屏始终拿到全部级别(含 0 值)。 */
private List<LevelStatVo> levelStats(List<String> levelNames, Map<String, Long> counts) {
List<LevelStatVo> items = this.emptyLevelStats(levelNames);
long total = 0L;
for (LevelStatVo item : items) {
Long count = counts.get(item.getLevel());
item.setCount(count == null ? 0L : count);
total += item.getCount();
}
for (LevelStatVo item : items) {
item.setPercent(percentage(item.getCount(), total));
}
return items;
}
/**
* 大屏统计通用取数:先读缓存,未命中再回源计算并写入缓存。
*
* <p>缓存读写异常时降级为直接回源,避免大屏接口因缓存故障整体不可用;
* updateTime 与数据一起缓存,命中缓存时仍为数据实际生成时间。</p>
*
* @param cacheKey 缓存 key
* @param ttlSeconds 缓存时长(秒)
* @param loader 统计口径计算逻辑
*/
@SuppressWarnings("unchecked")
private <T> ScreenResultVo<T> loadWithCache(String cacheKey, long ttlSeconds, Supplier<T> loader) {
try {
ScreenResultVo<T> cached = this.redisService.getCacheObject(cacheKey);
if (cached != null) {
return cached;
}
} catch (Exception e) {
this.logger.warn("[SCREEN] read cache fail, key: {}", cacheKey, e);
}
ScreenResultVo<T> screenResult = new ScreenResultVo<>(loader.get(), DateUtils.getTime());
try {
this.redisService.setCacheObject(cacheKey, screenResult, ttlSeconds, TimeUnit.SECONDS);
} catch (Exception e) {
this.logger.warn("[SCREEN] write cache fail, key: {}", cacheKey, e);
}
return screenResult;
}
/**
* period 归一化:空值取 day,非法值按 day 处理并记录告警。
*/
private String normalizePeriod(String period) {
if (StringUtils.isEmpty(period)) {
return PatrolScreenConst.PERIOD_DEFAULT;
}
String value = period.trim().toLowerCase();
if (!PatrolScreenConst.PERIODS.contains(value)) {
this.logger.warn("[SCREEN] invalid period: {}, fallback to {}", period, PatrolScreenConst.PERIOD_DEFAULT);
return PatrolScreenConst.PERIOD_DEFAULT;
}
return value;
}
/**
* 计算占比,保留 2 位小数;分母小于等于 0 时返回 0。
*/
private static Double percentage(Long numerator, Long denominator) {
if (numerator == null || denominator == null || denominator <= 0L) {
return 0D;
}
return BigDecimal.valueOf(numerator * 100D / denominator).setScale(2, RoundingMode.HALF_UP).doubleValue();
}
/**
* 巡视类型统计骨架:固定名目,数量默认 0,由调用方回填真实数量。
*/
private List<TaskStatsVo.TypeStat> emptyTypeStats() {
List<TaskStatsVo.TypeStat> typeStats = new ArrayList<>(PatrolScreenConst.PATROL_TYPES.size());
for (String type : PatrolScreenConst.PATROL_TYPES) {
TaskStatsVo.TypeStat typeStat = new TaskStatsVo.TypeStat();
typeStat.setType(type);
typeStats.add(typeStat);
}
return typeStats;
}
/**
* 巡视率指标骨架:固定名目,比率默认 0,由调用方回填真实比率。
*/
private List<TaskStatsVo.RateStat> emptyRateStats() {
List<TaskStatsVo.RateStat> rateStats = new ArrayList<>(PatrolScreenConst.PATROL_RATES.size());
for (String rateName : PatrolScreenConst.PATROL_RATES) {
TaskStatsVo.RateStat rateStat = new TaskStatsVo.RateStat();
rateStat.setName(rateName);
rateStats.add(rateStat);
}
return rateStats;
}
/**
* 巡视状态统计骨架:固定名目,数量默认 0,由调用方回填真实数量。
*/
private List<TaskStatsVo.StatusStat> emptyStatusStats() {
List<TaskStatsVo.StatusStat> statusStats = new ArrayList<>(PatrolScreenConst.PATROL_STATUSES.size());
for (String status : PatrolScreenConst.PATROL_STATUSES) {
TaskStatsVo.StatusStat statusStat = new TaskStatsVo.StatusStat();
statusStat.setStatus(status);
statusStats.add(statusStat);
}
return statusStats;
}
/**
* 级别统计骨架:固定名目,数量默认 0,由调用方回填真实数量;告警与缺陷共用。
*/
private List<LevelStatVo> emptyLevelStats(List<String> levels) {
List<LevelStatVo> items = new ArrayList<>(levels.size());
for (String level : levels) {
LevelStatVo item = new LevelStatVo();
item.setLevel(level);
items.add(item);
}
return items;
}
} }

+ 280
- 0
inspect-main/inspect-main-statistics/src/main/resources/mapper/screen/PatrolScreenMapper.xml View File

@ -0,0 +1,280 @@
<?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.inspect.statistics.mapper.PatrolScreenMapper">
<!--
摄像机通道编码:摄像机在线状态按 basedata_eqpbook_channel 通道口径统计。
设备与通道均排除已删除记录(del_flag / delete_flag 为 0 表示未删除,2 表示已删除)。
-->
<select id="selectCameraChannelCodes" resultType="java.lang.String">
select c.channel_code
from basedata_eqpbook_channel c
where c.delete_flag = '0'
and c.channel_code is not null
and c.eqp_book_id in (
select e.eqp_book_id
from basedata_eqpbook e
where e.type = #{eqpBookType}
and e.del_flag = '0'
)
</select>
<!--
字典项:大屏的类型分布名目跟随字典配置,运行时读取,不写死名目。
-->
<select id="selectDictData" resultType="com.inspect.statistics.domain.PatrolScreenDict">
select d.dict_value code, d.dict_label name
from sys_dict_data d
where d.dict_type = #{dictType}
order by d.dict_sort, d.dict_value
</select>
<!--
摄像机类型分布(通道口径):type = 10 摄像机名下、未删除通道,按通道自身 use_mode 分组。
use_mode 为空(未配置类型)的通道不计入任何类型名目。
-->
<select id="selectCameraUseModeCounts" resultType="com.inspect.statistics.domain.PatrolScreenCount">
select c.use_mode code, count(1) num
from basedata_eqpbook_channel c
where c.delete_flag = '0'
and c.use_mode is not null
and c.eqp_book_id in (
select e.eqp_book_id
from basedata_eqpbook e
where e.type = #{eqpBookType}
and e.del_flag = '0'
)
group by c.use_mode
</select>
<!--
摄像机类型分布兜底(通道口径):use_mode 未配置(为空)或取值不在 use_mode 字典内的通道,统一计入“其他”。
用 not exists 同时覆盖空值与非字典取值,避免 not in 遇空值的语义陷阱。
-->
<select id="selectCameraOtherTypeCount" resultType="java.lang.Long">
select count(1)
from basedata_eqpbook_channel c
where c.delete_flag = '0'
and c.eqp_book_id in (
select e.eqp_book_id
from basedata_eqpbook e
where e.type = #{eqpBookType}
and e.del_flag = '0'
)
and not exists (
select 1
from sys_dict_data d
where d.dict_type = #{dictType}
and d.dict_value = c.use_mode
)
</select>
<!--
摄像机光谱分布(通道口径):type = 10 摄像机名下、未删除通道,按通道自身 channel_type 分组。
名目跟随 sys_dict_data 的 dict_type = channel_type 配置,不写死取值。
channel_type 为空(未配置类型)的通道不计入任何名目。
-->
<select id="selectCameraChannelTypeCounts" resultType="com.inspect.statistics.domain.PatrolScreenCount">
select c.channel_type code, count(1) num
from basedata_eqpbook_channel c
where c.delete_flag = '0'
and c.channel_type is not null
and c.eqp_book_id in (
select e.eqp_book_id
from basedata_eqpbook e
where e.type = #{eqpBookType}
and e.del_flag = '0'
)
group by c.channel_type
</select>
<!--
摄像机光谱分布兜底(通道口径):channel_type 未配置(为空)或取值不在 channel_type 字典内的通道,统一计入“其他”。
用 not exists 同时覆盖空值与非字典取值,避免 not in 遇空值的语义陷阱。
-->
<select id="selectCameraOtherChannelTypeCount" resultType="java.lang.Long">
select count(1)
from basedata_eqpbook_channel c
where c.delete_flag = '0'
and c.eqp_book_id in (
select e.eqp_book_id
from basedata_eqpbook e
where e.type = #{eqpBookType}
and e.del_flag = '0'
)
and not exists (
select 1
from sys_dict_data d
where d.dict_type = #{dictType}
and d.dict_value = c.channel_type
)
</select>
<!--
设备在线/离线事件:basedata_mont_patdev_online 只记录状态变化,按事件时间升序返回,
由业务侧还原在线区间后再统计连续运行天数。
-->
<select id="selectDeviceOnlineEvents" resultType="com.inspect.statistics.domain.DeviceOnlineEvent">
select o.eqp_book_id eqpBookId,
o.status status,
o.action_time actionTime
from basedata_mont_patdev_online o
where o.del_flag = '0'
and o.action_time is not null
and o.action_time &gt;= #{onlineSince}
and o.eqp_book_id in (
select e.eqp_book_id
from basedata_eqpbook e
where e.del_flag = '0'
and e.type in
<foreach collection="eqpBookTypes" item="eqpBookType" open="(" separator="," close=")">
#{eqpBookType}
</foreach>
)
order by o.eqp_book_id, o.action_time
</select>
<!--
巡检天数:patrol_task_status 记录的是已执行的巡检任务,按 task_code 关联 patrol_task.dev_type;
dev_type 为逗号分隔多值,使用 find_in_set 匹配;按自然日去重,不做任务状态过滤。
-->
<select id="selectPatrolDays" resultType="java.lang.String">
select distinct date_format(s.start_time, '%Y-%m-%d')
from patrol_task_status s
inner join patrol_task t on t.task_code = s.task_code
where s.start_time is not null
and find_in_set(#{devType}, t.dev_type) &gt; 0
order by 1
</select>
<!--
巡视类型数量:按“已执行任务”统计——patrol_task_status 内联 patrol_task,
按巡视任务执行ID task_patrolled_id 去重计数,类型取 patrol_task.type(与巡视状态统计同源),
排除联动(type=5)/静默(type=6)任务;统计时间取任务开始时间 start_time。
-->
<select id="selectTaskTypeCounts" resultType="com.inspect.statistics.domain.PatrolScreenCount">
select t.type code, count(distinct s.task_patrolled_id) num
from patrol_task_status s
inner join patrol_task t on t.task_code = s.task_code
where t.type is not null
<if test="excludeTypes != null and excludeTypes.size() > 0">
and t.type not in
<foreach collection="excludeTypes" item="excludeType" open="(" separator="," close=")">
#{excludeType}
</foreach>
</if>
<if test="beginTime != null and endTime != null">
and s.start_time between #{beginTime} and #{endTime}
</if>
group by t.type
</select>
<!--
巡视状态数量:patrol_task_status.task_state 取值 1 已执行、2 正在执行、3 暂停、4 终止、
5 未执行、6 超期;按巡视任务执行ID task_patrolled_id 去重计数,
排除联动(type=5)/静默(type=6)任务;统计时间取任务开始时间 start_time。
-->
<select id="selectTaskStatusCounts" resultType="com.inspect.statistics.domain.PatrolScreenCount">
select s.task_state code, count(distinct s.task_patrolled_id) num
from patrol_task_status s
where s.task_state is not null
<if test="excludeTypes != null and excludeTypes.size() > 0">
and not exists (
select 1
from patrol_task t
where t.task_code = s.task_code
and t.type in
<foreach collection="excludeTypes" item="excludeType" open="(" separator="," close=")">
#{excludeType}
</foreach>
)
</if>
<if test="beginTime != null and endTime != null">
and s.start_time between #{beginTime} and #{endTime}
</if>
group by s.task_state
</select>
<!--
以下 5 个比率取数口径复制自 inspect-main-client 的 mapper/ClientStatisticsMapper.xml,
仅补上大屏需要的统计时间窗,保持与客户端上报口径完全一致。
-->
<!-- 任务完成率:复制自 queryTaskDone -->
<select id="selectTaskDoneRate" resultType="com.inspect.statistics.domain.PatrolScreenRate">
select count(1) totalNum, sum(case when task_state = 4 then 1 else 0 end) validNum
from patrol_task_status
<if test="beginTime != null and endTime != null">
where start_time between #{beginTime} and #{endTime}
</if>
</select>
<!-- 巡检点位漏检率:复制自 queryPointNum -->
<select id="selectPointNumRate" resultType="com.inspect.statistics.domain.PatrolScreenRate">
select count(distinct point.patrol_point_id) totalNum, count(distinct result.device_id) validNum
from basedata_patrolpoint point
left join patrol_result result on point.patrol_point_id = result.device_id
<if test="beginTime != null and endTime != null">
and result.time between #{beginTime} and #{endTime}
</if>
</select>
<!-- 巡视告警人工审核完成率:复制自 queryPersonExamine -->
<select id="selectAlarmReviewRate" resultType="com.inspect.statistics.domain.PatrolScreenRate">
select count(1) totalNum, sum(case when res_status = 1 then 1 else 0 end) validNum
from result_analysis
where result_type = 0
<if test="beginTime != null and endTime != null">
and create_time between #{beginTime} and #{endTime}
</if>
</select>
<!-- 巡视告警准确率:复制自 queryWarning -->
<select id="selectAlarmAccuracyRate" resultType="com.inspect.statistics.domain.PatrolScreenRate">
select count(result.line_id) totalNum, sum(case when aux.line_id is null then 1 else 0 end) validNum
from patrol_result result
left join patrol_result_aux aux on aux.result_id = result.line_id
<if test="beginTime != null and endTime != null">
where result.time between #{beginTime} and #{endTime}
</if>
</select>
<!-- 巡视结果人工审核完成率:复制自 queryResultPersonExamine -->
<select id="selectResultReviewRate" resultType="com.inspect.statistics.domain.PatrolScreenRate">
select count(1) totalNum, sum(case when patrol_status = 1 then 1 else 0 end) validNum
from patrol_result
<if test="beginTime != null and endTime != null">
where time between #{beginTime} and #{endTime}
</if>
</select>
<!--
告警记录数:result_analysis.result_type = 2。本地结果表没有告警级别字段,
该批记录统一按大屏最低级别「预警」计入(未再按级别分组)。
-->
<select id="selectAlarmCount" resultType="java.lang.Long">
select count(1)
from result_analysis r
where r.result_type = 2
<if test="beginTime != null and endTime != null">
and r.create_time between #{beginTime} and #{endTime}
</if>
</select>
<!--
缺陷数量:缺陷记录取本地分析结果表 result_analysis.result_type = 0,时间取 create_time。
本地结果表没有缺陷等级字段(等级由站端判定,只落在同步镜像表,本模块不读镜像/测试表),
故不按等级分组,由业务侧统一计入大屏最低级别。
-->
<select id="selectDefectCount" resultType="java.lang.Long">
select count(1)
from result_analysis r
where r.result_type = 0
<if test="beginTime != null and endTime != null">
and r.create_time between #{beginTime} and #{endTime}
</if>
</select>
</mapper>

+ 1
- 0
inspect-main/inspect-main-task/src/main/java/com/inspect/analysis/utils/ResultAnalysisUtils.java View File

@ -98,6 +98,7 @@ public class ResultAnalysisUtils {
// 初筛时, infrared算法要做阈值判断 // 初筛时, infrared算法要做阈值判断
if ("1".equals(resultAnalysis.getFilter()) && !AlgConstants.INFRARED.equals(algType)) { if ("1".equals(resultAnalysis.getFilter()) && !AlgConstants.INFRARED.equals(algType)) {
algType = ""; algType = "";
resultAnalysis.setResValue("");
} }
if (StringUtils.isNull(algType)) { if (StringUtils.isNull(algType)) {


+ 6
- 0
inspect-main/pom.xml View File

@ -13,6 +13,12 @@
<artifactId>inspect-main</artifactId> <artifactId>inspect-main</artifactId>
<version>3.4.0</version> <version>3.4.0</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>
<modules> <modules>
<module>inspect-main-start</module> <module>inspect-main-start</module>
<module>inspect-main-task</module> <module>inspect-main-task</module>


Loading…
Cancel
Save