|
|
|
@ -4,20 +4,53 @@ import cn.hutool.poi.excel.ExcelWriter; |
|
|
|
import com.alibaba.fastjson.JSONArray; |
|
|
|
import com.alibaba.fastjson.JSONObject; |
|
|
|
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.utils.DateUtils; |
|
|
|
import com.inspect.base.core.utils.StringUtils; |
|
|
|
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.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.service.PatrolStatisticsService; |
|
|
|
import com.inspect.system.base.openDomain.BasedataEqpBookMoMain; |
|
|
|
|
|
|
|
import java.io.IOException; |
|
|
|
import java.math.BigDecimal; |
|
|
|
import java.math.RoundingMode; |
|
|
|
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.Date; |
|
|
|
import java.util.HashMap; |
|
|
|
import java.util.HashSet; |
|
|
|
import java.util.Iterator; |
|
|
|
import java.util.LinkedHashMap; |
|
|
|
import java.util.List; |
|
|
|
import java.util.Map; |
|
|
|
import java.util.Set; |
|
|
|
import java.util.concurrent.TimeUnit; |
|
|
|
import java.util.function.Supplier; |
|
|
|
import java.util.stream.Collectors; |
|
|
|
import javax.annotation.Resource; |
|
|
|
import javax.servlet.http.HttpServletResponse; |
|
|
|
@ -38,6 +71,15 @@ public class PatrolStatisticsServiceImpl implements PatrolStatisticsService { |
|
|
|
@Autowired |
|
|
|
private MessageUtils messageUtils; |
|
|
|
|
|
|
|
@Resource |
|
|
|
RedisService redisService; |
|
|
|
|
|
|
|
@Resource |
|
|
|
IPatrolResultService patrolResultService; |
|
|
|
|
|
|
|
@Resource |
|
|
|
PatrolScreenMapper screenMapper; |
|
|
|
|
|
|
|
public List<StatisticsPatrolResult> queryPatrolResult(StatisticsPatrolResult result) { |
|
|
|
return this.resultMapper.queryPatrolResult(result); |
|
|
|
} |
|
|
|
@ -213,4 +255,726 @@ public class PatrolStatisticsServiceImpl implements PatrolStatisticsService { |
|
|
|
colMap.put("dataType", messageUtils.get("巡视结果数据来源")); |
|
|
|
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; |
|
|
|
} |
|
|
|
} |