fix:优化app统计报表查询速度

main
HuangHuiKang 2 weeks ago
parent 728121dd77
commit c2ec3f0dbf

@ -102,4 +102,8 @@ public interface MoldService {
List<MoldDO> getInTransitMoldAllList();
List<MoldDO> getMoldListByStatus(Long status);
List<MoldDO> getMoldListByIds(Collection<Long> ids);
}

@ -136,4 +136,11 @@ public class MoldServiceImpl implements MoldService {
.eqIfPresent(MoldDO::getStatus, status);
return moldMapper.selectList(queryWrapper);
}
@Override
public List<MoldDO> getMoldListByIds(Collection<Long> ids) {
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
}
return moldMapper.selectBatchIds(ids); }
}

@ -137,9 +137,9 @@ public class DeviceController {
}
@GetMapping("/deviceList")
// @PreAuthorize("@ss.hasPermission('iot:device:query')")
public CommonResult<List<DeviceRespVO>> deviceList(@Valid DevicePageReqVO pageReqVO) {
pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
List<DeviceRespVO> list = deviceService.getDevicePage(pageReqVO).getList();
public CommonResult<List<DeviceDO>> deviceList(@Valid DevicePageReqVO pageReqVO) {
// List<DeviceRespVO> list = deviceService.getDevicePage(pageReqVO).getList();
List<DeviceDO> list = deviceService.deviceList(pageReqVO);
return success(list);
}

@ -9,6 +9,10 @@ import java.time.temporal.TemporalAdjusters;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.iot.enums.ErrorCodeConstants.DEVICE_RATE_TREND_PERIOD_INVALID;
/**
*/
@Getter
@AllArgsConstructor
public enum DeviceRateTrendPeriodEnum {

@ -111,10 +111,11 @@ public class DeviceOperationRecordController {
}
@GetMapping("/deviceRateTrendByDeviceId")
@Operation(summary = "根据设备ID查询某个设备近7日开机率和稼动率")
@Operation(summary = "根据设备ID查询某个设备开机率和稼动率")
@PreAuthorize("@ss.hasPermission('iot:device-operation-record:query')")
public CommonResult<List<DeviceOperationRateTrendRespVO>> getDeviceRateTrendByDeviceId(@RequestParam("deviceId") Long deviceId) {
return success(deviceOperationRecordService.getDeviceRateTrendByDeviceId(deviceId));
public CommonResult<List<DeviceOperationRateTrendRespVO>> getDeviceRateTrendByDeviceId(@RequestParam("deviceId") Long deviceId,
@RequestParam(value = "period", required = false) String period) {
return success(deviceOperationRecordService.getDeviceRateTrendByDeviceId(deviceId, period));
}

@ -14,6 +14,9 @@ import lombok.ToString;
@ToString(callSuper = true)
public class DeviceTotalTimeRecordReqVO extends PageParam{
@Schema(description = "时间区间LAST_WEEK/THIS_WEEK/LAST_7_DAYS/LAST_MONTH/THIS_MONTH")
private String period;
@Schema(description = "设备编码")
private String deviceCode;

@ -140,6 +140,9 @@ public interface DeviceMapper extends BaseMapperX<DeviceDO> {
List<Date> selectHolidayDays(@Param("startDay") Date startDay, @Param("endDay") Date endDay);
List<DeviceSelectRespVO> getAvailableDevicePage(@Param("page") Page<DeviceSelectRespVO> page,@Param("param") DevicePageReqVO pageReqVO);
List<Date> selectHolidayDaysInRange(@Param("startDay") Date startDay, @Param("endDayExclusive") Date endDayExclusive);
}

@ -157,4 +157,6 @@ public interface DeviceService {
PageResult<DeviceSelectRespVO> getAvailableDevicePage(DevicePageReqVO pageReqVO);
List<DeviceDO> deviceList(@Valid DevicePageReqVO pageReqVO);
}

@ -15,6 +15,7 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.scheduled.scheduler.T
import cn.iocoder.yudao.module.iot.controller.admin.device.utils.DataTypeParseUtil;
import cn.iocoder.yudao.module.iot.controller.admin.device.vo.*;
import cn.iocoder.yudao.module.iot.controller.admin.devicecontactmodel.vo.DeviceContactModelPageReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord.utils.TimeConverterUtil;
import cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord.vo.DeviceTotalTimeRecordReqVO;
import cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord.vo.DeviceTotalTimeRecordRespVO;
import cn.iocoder.yudao.module.iot.controller.admin.mqttdatarecord.vo.MqttDataRecordPageReqVO;
@ -64,10 +65,7 @@ import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.sql.Timestamp;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.util.*;
import java.util.function.Function;
@ -1746,40 +1744,97 @@ public class DeviceServiceImpl implements DeviceService {
return new PageResult<>(availableDeviceList, page.getTotal());
}
@Override
public List<DeviceDO> deviceList(DevicePageReqVO pageReqVO) {
return deviceMapper.selectList(pageReqVO);
}
/**
*
* deviceOperationPage
*
*
*/
@Override
public List<DeviceRateTrendPointRespVO> getDeviceRateTrend(DeviceRateTrendReqVO reqVO) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND);
// 根据 period 解析查询日期范围,不包含当天
DeviceRateTrendPeriodEnum.DateRange dateRange = DeviceRateTrendPeriodEnum.valueOfCode(reqVO.getPeriod()).resolve(LocalDate.now());
LocalDateTime startDateTime = dateRange.getStart().atStartOfDay();
LocalDateTime endDateTime = dateRange.getEnd().atTime(LocalTime.MAX).withNano(0);
// 默认只统计排产设备,这里先得到最终参与统计的设备集合
DeviceRateTrendPeriodEnum.DateRange dateRange =
DeviceRateTrendPeriodEnum.valueOfCode(reqVO.getPeriod()).resolve(LocalDate.now());
LocalDate startDate = dateRange.getStart();
LocalDate endDate = dateRange.getEnd();
LocalDateTime startDateTime = startDate.atStartOfDay();
LocalDateTime endDateTime = endDate.atTime(LocalTime.MAX).withNano(0);
String filteredIds = buildFilteredDeviceIds(reqVO);
if (StringUtils.isBlank(filteredIds)) {
return Collections.emptyList();
}
List<Long> deviceIds = Arrays.stream(filteredIds.split(","))
.filter(StringUtils::isNotBlank)
.map(Long::valueOf)
.distinct()
.collect(Collectors.toList());
List<Map<String, Object>> statsList = tdengineService.selectDeviceStatsTrendFromTD(
deviceIds,
startDateTime.format(formatter),
endDateTime.format(formatter)
);
// day -> (deviceId -> record)
Map<LocalDate, Map<Long, DeviceTotalTimeRecordRespVO>> dayDeviceRecordMap = convertTrendStatsToMap(statsList);
// skipHoliday 时,区间一次查出所有节假日,避免按天查库
Set<LocalDate> holidaySet = Collections.emptySet();
if (Boolean.TRUE.equals(reqVO.getSkipHoliday())) {
List<Date> holidays = deviceMapper.selectHolidayDaysInRange(
java.sql.Timestamp.valueOf(startDate.atStartOfDay()),
java.sql.Timestamp.valueOf(endDate.plusDays(1).atStartOfDay())
);
holidaySet = holidays == null ? Collections.emptySet() : holidays.stream()
.filter(Objects::nonNull)
.map(this::toLocalDateSafe)
.collect(Collectors.toSet());
}
int deviceCount = deviceIds.size();
List<DeviceRateTrendPointRespVO> result = new ArrayList<>();
for (LocalDate day = startDateTime.toLocalDate(); !day.isAfter(endDateTime.toLocalDate()); day = day.plusDays(1)) {
// 跳过节假日时,这一天不查询,也不返回
if (Boolean.TRUE.equals(reqVO.getSkipHoliday()) && isHoliday(day)) {
for (LocalDate day = startDate; !day.isAfter(endDate); day = day.plusDays(1)) {
if (Boolean.TRUE.equals(reqVO.getSkipHoliday()) && holidaySet.contains(day)) {
continue;
}
LocalDateTime dayStart = day.atStartOfDay();
LocalDateTime dayEnd = day.atTime(LocalTime.MAX).withNano(0);
// 复用现有 deviceOperationPage查出当天每台设备的开机率、稼动率基础数据
List<DeviceTotalTimeRecordRespVO> deviceDayList = queryDeviceDayList(reqVO, filteredIds, dayStart.format(formatter), dayEnd.format(formatter));
Map<Long, DeviceTotalTimeRecordRespVO> dayMap =
dayDeviceRecordMap.getOrDefault(day, Collections.emptyMap());
double powerOnRateSum = 0D;
double utilizationRateSum = 0D;
int deviceCount = deviceDayList.size();
for (DeviceTotalTimeRecordRespVO record : deviceDayList) {
powerOnRateSum += parsePercentValue(record.getPowerOnRate());
utilizationRateSum += parsePercentValue(record.getUtilizationRate());
}
for (Long deviceId : deviceIds) {
DeviceTotalTimeRecordRespVO record = dayMap.get(deviceId);
if (record == null) {
// 无数据按 0
continue;
}
// 直接按秒计算,不走字符串百分比解析
double offlineSec = record.getTotalOfflineTime();
double runningSec = record.getTotalRunningTime();
double standbySec = record.getTotalStandbyTime();
double faultSec = record.getTotalFaultTime();
double onlineSec = runningSec + standbySec + faultSec;
double totalSec = offlineSec + onlineSec;
double powerOnRate = totalSec > 0 ? (onlineSec / totalSec) : 0D;
double utilizationRate = onlineSec > 0 ? (runningSec / onlineSec) : 0D;
powerOnRateSum += powerOnRate * 100D;
utilizationRateSum += utilizationRate * 100D;
}
DeviceRateTrendPointRespVO point = new DeviceRateTrendPointRespVO();
point.setDay(day.toString());
@ -1787,11 +1842,101 @@ public class DeviceServiceImpl implements DeviceService {
point.setPowerOnRate(formatPercentValue(deviceCount > 0 ? powerOnRateSum / deviceCount : 0D));
point.setUtilizationRate(formatPercentValue(deviceCount > 0 ? utilizationRateSum / deviceCount : 0D));
result.add(point);
}
return result;
}
private Map<LocalDate, Map<Long, DeviceTotalTimeRecordRespVO>> convertTrendStatsToMap(List<Map<String, Object>> statsList) {
Map<LocalDate, Map<Long, DeviceTotalTimeRecordRespVO>> result = new LinkedHashMap<>();
if (statsList == null || statsList.isEmpty()) {
return result;
}
for (Map<String, Object> row : statsList) {
Object statDayObj = row.get("statDay");
if (statDayObj == null) {
statDayObj = row.get("statday");
}
if (statDayObj == null || row.get("deviceId") == null) {
continue;
}
LocalDate statDay;
if (statDayObj instanceof Timestamp) {
statDay = ((Timestamp) statDayObj).toLocalDateTime().toLocalDate();
} else if (statDayObj instanceof LocalDateTime) {
statDay = ((LocalDateTime) statDayObj).toLocalDate();
} else {
statDay = LocalDateTime.parse(statDayObj.toString().replace(" ", "T")).toLocalDate();
}
Long deviceId = ((Number) row.get("deviceId")).longValue();
DeviceTotalTimeRecordRespVO vo = new DeviceTotalTimeRecordRespVO();
vo.setId(deviceId);
vo.setTotalOfflineTime(getDoubleValue(row, "totalOfflineTime"));
vo.setTotalRunningTime(getDoubleValue(row, "totalRunningTime"));
vo.setTotalStandbyTime(getDoubleValue(row, "totalStandbyTime"));
vo.setTotalFaultTime(getDoubleValue(row, "totalFaultTime"));
result.computeIfAbsent(statDay, key -> new HashMap<>()).put(deviceId, vo);
}
return result;
}
/**
* day -> device stats
*/
private Map<LocalDate, List<DeviceTotalTimeRecordRespVO>> convertTrendStats(List<Map<String, Object>> statsList) {
Map<LocalDate, List<DeviceTotalTimeRecordRespVO>> result = new LinkedHashMap<>();
if (statsList == null || statsList.isEmpty()) {
return result;
}
for (Map<String, Object> row : statsList) {
Object statDayObj = row.get("statDay");
if (statDayObj == null) {
statDayObj = row.get("statday");
}
if (statDayObj == null) {
continue;
}
LocalDate statDay;
if (statDayObj instanceof Timestamp) {
statDay = ((Timestamp) statDayObj).toLocalDateTime().toLocalDate();
} else if (statDayObj instanceof LocalDateTime) {
statDay = ((LocalDateTime) statDayObj).toLocalDate();
} else {
statDay = LocalDateTime.parse(statDayObj.toString().replace(" ", "T")).toLocalDate();
}
DeviceTotalTimeRecordRespVO vo = new DeviceTotalTimeRecordRespVO();
vo.setId(((Number) row.get("deviceId")).longValue());
vo.setTotalOfflineTime(getDoubleValue(row, "totalOfflineTime"));
vo.setTotalRunningTime(getDoubleValue(row, "totalRunningTime"));
vo.setTotalStandbyTime(getDoubleValue(row, "totalStandbyTime"));
vo.setTotalFaultTime(getDoubleValue(row, "totalFaultTime"));
result.computeIfAbsent(statDay, key -> new ArrayList<>()).add(vo);
}
return result;
}
private DeviceTotalTimeRecordReqVO buildTrendReqVO(LocalDate day) {
DeviceTotalTimeRecordReqVO reqVO = new DeviceTotalTimeRecordReqVO();
reqVO.setStartTime(day.atStartOfDay().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
reqVO.setEndTime(day.atTime(LocalTime.MAX).withNano(0).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
return reqVO;
}
private double parsePercentValue(String percent) {
if (StringUtils.isBlank(percent)) {
return 0D;
@ -1810,37 +1955,57 @@ public class DeviceServiceImpl implements DeviceService {
return String.format("%.2f%%", value);
}
private LocalDate toLocalDateSafe(Date date) {
if (date instanceof java.sql.Date) {
return ((java.sql.Date) date).toLocalDate();
}
if (date instanceof java.sql.Timestamp) {
return ((java.sql.Timestamp) date).toLocalDateTime().toLocalDate();
}
return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
}
private List<DeviceTotalTimeRecordRespVO> queryDeviceDayList(DeviceRateTrendReqVO reqVO, String ids, String startTime, String endTime) {
DeviceTotalTimeRecordReqVO pageReqVO =
new DeviceTotalTimeRecordReqVO();
pageReqVO.setDeviceCode(reqVO.getDeviceCode());
pageReqVO.setDeviceName(reqVO.getDeviceName());
pageReqVO.setIds(ids);
pageReqVO.setStartTime(startTime);
pageReqVO.setEndTime(endTime);
return deviceOperationRecordService.deviceOperationPageList(pageReqVO);
}
private String buildFilteredDeviceIds(DeviceRateTrendReqVO reqVO) {
boolean onlyScheduled = reqVO.getOnlyScheduled() == null || reqVO.getOnlyScheduled();
// 不只看排产设备时:
// 1. 如果前端传了 ids就按传入 ids 查询
// 2. 如果前端没传 ids就查询全部设备 id
if (!onlyScheduled) {
return reqVO.getIds();
if (StringUtils.isNotBlank(reqVO.getIds())) {
return reqVO.getIds();
}
List<Long> allDeviceIds = deviceMapper.getAllDeviceIds();
if (CollectionUtils.isEmpty(allDeviceIds)) {
return "-1";
}
return allDeviceIds.stream()
.map(String::valueOf)
.collect(Collectors.joining(","));
}
// 只统计排产设备
Collection<Long> requestIds = parseIds(reqVO.getIds());
List<Long> filteredDvIds = deviceMapper.selectScheduledDvIds(requestIds);
// 如果前端传了 ids则在排产设备中再次过滤
if (StringUtils.isNotBlank(reqVO.getIds())) {
Set<Long> requestIdSet = new HashSet<>(requestIds);
filteredDvIds = filteredDvIds.stream().filter(requestIdSet::contains).collect(Collectors.toList());
filteredDvIds = filteredDvIds.stream()
.filter(requestIdSet::contains)
.collect(Collectors.toList());
}
if (filteredDvIds.isEmpty()) {
return "-1";
}
return filteredDvIds.stream().map(String::valueOf).collect(Collectors.joining(","));
return filteredDvIds.stream()
.map(String::valueOf)
.collect(Collectors.joining(","));
}
private boolean isHoliday(LocalDate day) {
@ -1848,10 +2013,6 @@ public class DeviceServiceImpl implements DeviceService {
return !CollectionUtils.isEmpty(holidays);
}
private String toPercent(double value) {
return String.format("%.2f%%", value * 100);
}
private Collection<Long> parseIds(String ids) {
if (StringUtils.isBlank(ids)) {
return Collections.emptyList();
@ -1865,5 +2026,71 @@ public class DeviceServiceImpl implements DeviceService {
/**
*
* calculateAndSetConvertedValues
* = 线 /
* = / 线
*/
private void calculateRate(DeviceTotalTimeRecordRespVO record) {
if (record == null) {
return;
}
double offlineSec = record.getTotalOfflineTime();
double runningSec = record.getTotalRunningTime();
double standbySec = record.getTotalStandbyTime();
double faultSec = record.getTotalFaultTime();
// 在线时间 = 运行 + 待机 + 故障
double onlineSec = runningSec + standbySec + faultSec;
double totalSec = offlineSec + onlineSec;
// 防止异常值
if (totalSec < 0) {
totalSec = 0;
}
// 开机率 = 在线时间 / 总时间
double powerOnRate = 0D;
if (totalSec > 0) {
powerOnRate = onlineSec / totalSec;
}
// 稼动率 = 运行时间 / 在线时间
double utilizationRate = 0D;
if (onlineSec > 0) {
utilizationRate = runningSec / onlineSec;
}
record.setPowerOnRate(TimeConverterUtil.getPercentString(powerOnRate));
record.setUtilizationRate(TimeConverterUtil.getPercentString(utilizationRate));
}
private Double getDoubleValue(Map<String, Object> row, String key) {
if (row == null || key == null) {
return 0D;
}
Object value = row.get(key);
if (value == null) {
value = row.get(key.toLowerCase());
}
if (value == null) {
return 0D;
}
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
try {
return Double.parseDouble(value.toString());
} catch (Exception e) {
return 0D;
}
}
}

@ -2535,4 +2535,109 @@ public class TDengineService {
}
}
// ========================= app报表相关接口 ===================
@DS("tdengine")
public List<Map<String, Object>> selectDeviceStatsTrendFromTD(List<Long> deviceIds, String startTime, String endTime) {
StringBuilder sql = new StringBuilder();
List<Object> params = new ArrayList<>();
sql.append("SELECT ")
.append("_wstart AS statDay, ")
.append("device_id AS deviceId, ")
.append("SUM(CASE WHEN rule = '0' THEN 1 ELSE 0 END) * 60 AS totalOfflineTime, ")
.append("SUM(CASE WHEN rule = '1' THEN 1 ELSE 0 END) * 60 AS totalRunningTime, ")
.append("SUM(CASE WHEN rule = '2' THEN 1 ELSE 0 END) * 60 AS totalStandbyTime, ")
.append("SUM(CASE WHEN rule = '3' THEN 1 ELSE 0 END) * 60 AS totalFaultTime ")
.append("FROM besure_server.iot_device_operation_record ")
.append("WHERE deleted = 0 ");
if (startTime != null && !startTime.trim().isEmpty()) {
sql.append("AND create_time >= ? ");
params.add(startTime);
}
if (endTime != null && !endTime.trim().isEmpty()) {
sql.append("AND create_time <= ? ");
params.add(endTime);
}
// deviceIds 不为空时才加过滤
if (deviceIds != null && !deviceIds.isEmpty()) {
sql.append("AND device_id IN (");
for (int i = 0; i < deviceIds.size(); i++) {
if (i > 0) {
sql.append(", ");
}
sql.append("?");
params.add(deviceIds.get(i));
}
sql.append(") ");
}
sql.append("PARTITION BY device_id ")
.append("INTERVAL(1d) ")
.append("ORDER BY statDay ASC");
try {
return jdbcTemplate.queryForList(sql.toString(), params.toArray());
} catch (Exception e) {
log.error("TDengine 查询设备日趋势统计失败sql={}, params={}", sql, params, e);
return Collections.emptyList();
}
}
@DS("tdengine")
public List<Map<String, Object>> queryDeviceDayTrend(Long deviceId, String startTime, String endTime) {
StringBuilder sql = new StringBuilder();
List<Object> params = new ArrayList<>();
sql.append("SELECT ")
.append("_wstart AS statDay, ")
.append("device_id AS deviceId, ")
.append("SUM(CASE WHEN rule = '0' THEN 1 ELSE 0 END) * 60 AS totalOfflineTime, ")
.append("SUM(CASE WHEN rule = '1' THEN 1 ELSE 0 END) * 60 AS totalRunningTime, ")
.append("SUM(CASE WHEN rule = '2' THEN 1 ELSE 0 END) * 60 AS totalStandbyTime, ")
.append("SUM(CASE WHEN rule = '3' THEN 1 ELSE 0 END) * 60 AS totalFaultTime ")
.append("FROM besure_server.iot_device_operation_record ")
.append("WHERE deleted = 0 ")
.append("AND device_id = ? ");
params.add(deviceId);
if (startTime != null && !startTime.trim().isEmpty()) {
sql.append("AND create_time >= ? ");
params.add(startTime);
}
if (endTime != null && !endTime.trim().isEmpty()) {
sql.append("AND create_time <= ? ");
params.add(endTime);
}
sql.append("PARTITION BY device_id ")
.append("INTERVAL(1d) ")
.append("ORDER BY statDay ASC");
try {
List<Map<String, Object>> list = jdbcTemplate.queryForList(sql.toString(), params.toArray());
for (Map<String, Object> row : list) {
Object statDay = row.get("statDay");
if (statDay instanceof java.sql.Timestamp) {
row.put("day", ((java.sql.Timestamp) statDay).toLocalDateTime().toLocalDate().toString());
} else if (statDay != null) {
String text = String.valueOf(statDay);
row.put("day", text.length() >= 10 ? text.substring(0, 10) : text);
} else {
row.put("day", null);
}
}
return list;
} catch (Exception e) {
log.error("TDengine 查询单设备日趋势失败sql={}, params={}", sql, params, e);
return Collections.emptyList();
}
}
}

@ -56,7 +56,7 @@ public interface DeviceOperationRecordService {
List<DeviceTotalTimeRecordRespVO> deviceOperationPageList(@Valid DeviceTotalTimeRecordReqVO pageReqVO);
List<DeviceOperationRateTrendRespVO> getDeviceRateTrendByDeviceId(Long deviceId);
List<DeviceOperationRateTrendRespVO> getDeviceRateTrendByDeviceId(Long deviceId, String period);
List<DeviceTotalTimeRecordRespVO> deviceOperationList(@Valid DeviceTotalTimeRecordReqVO pageReqVO);
}

@ -1,5 +1,7 @@
package cn.iocoder.yudao.module.iot.service.deviceoperationrecord;
import cn.iocoder.yudao.framework.common.util.date.DateUtils;
import cn.iocoder.yudao.module.iot.controller.admin.device.enums.DeviceRateTrendPeriodEnum;
import cn.iocoder.yudao.module.iot.controller.admin.deviceoperationrecord.utils.TimeConverterUtil;
import cn.iocoder.yudao.module.iot.service.device.TDengineService;
import com.baomidou.mybatisplus.core.metadata.IPage;
@ -12,11 +14,9 @@ import javax.annotation.Resource;
import org.springframework.validation.annotation.Validated;
import org.springframework.transaction.annotation.Transactional;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.*;
import java.time.format.DateTimeFormatter;
import java.time.temporal.TemporalAdjusters;
import java.util.*;
import java.util.stream.Collectors;
@ -92,6 +92,8 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe
@Override
public PageResult<DeviceTotalTimeRecordRespVO> deviceOperationPage(DeviceTotalTimeRecordReqVO pageReqVO) {
applyPeriodIfNecessary(pageReqVO);
// 1. 先从 MySQL 查询设备分页信息
Page<DeviceTotalTimeRecordRespVO> page = new Page<>(pageReqVO.getPageNo(), pageReqVO.getPageSize());
IPage<DeviceTotalTimeRecordRespVO> mysqlDevicePage =
@ -106,6 +108,9 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe
.map(DeviceTotalTimeRecordRespVO::getId)
.collect(Collectors.toList());
log.info("deviceOperationPage mysql设备ID列表 pageReqVO.startTime={}, pageReqVO.endTime={}, deviceIds={}",
pageReqVO.getStartTime(), pageReqVO.getEndTime(), deviceIds);
// 3. 从 TDengine 批量查询统计数据
List<Map<String, Object>> statsList =
deviceOperationRecordMapper.selectDeviceStatsFromTD(
@ -114,6 +119,9 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe
pageReqVO.getEndTime()
);
log.info("deviceOperationPage TD原始统计 startTime={}, endTime={}, statsList={}",
pageReqVO.getStartTime(), pageReqVO.getEndTime(), statsList);
// 4. 转换为 Map<设备ID, 统计数据>
Map<Long, DeviceTotalTimeRecordRespVO> tdStatsMap = convertStatsListToMap(statsList);
@ -122,6 +130,10 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe
List<DeviceTotalTimeRecordRespVO> result = new ArrayList<>();
for (DeviceTotalTimeRecordRespVO device : mysqlDevicePage.getRecords()) {
DeviceTotalTimeRecordRespVO stats = tdStatsMap.get(device.getId());
log.info("deviceOperationPage 合并前 deviceId={}, deviceCode={}, deviceName={}, stats={}",
device.getId(), device.getDeviceCode(), device.getDeviceName(), stats);
if (stats != null) {
// 设置统计数据
device.setTotalOfflineTime(stats.getTotalOfflineTime());
@ -137,6 +149,7 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe
}
result.add(device);
}
log.info("deviceOperationPage mysql设备ID列表: {}", deviceIds);
// 5. 计算和转换
calculateAndSetConvertedValues(result, pageReqVO);
@ -148,6 +161,8 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe
@Override
public List<DeviceTotalTimeRecordRespVO> deviceOperationPageList(DeviceTotalTimeRecordReqVO pageReqVO) {
applyPeriodIfNecessary(pageReqVO);
List<DeviceTotalTimeRecordRespVO> deviceList =
deviceOperationRecordMapper.selectDeviceListFromMySQL(pageReqVO);
@ -191,33 +206,153 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe
}
@Override
public List<DeviceOperationRateTrendRespVO> getDeviceRateTrendByDeviceId(Long deviceId) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
List<DeviceOperationRateTrendRespVO> result = new ArrayList<>();
public List<DeviceOperationRateTrendRespVO> getDeviceRateTrendByDeviceId(Long deviceId, String period) {
DeviceRateTrendPeriodEnum.DateRange dateRange = resolvePeriodRange(period);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND);
for (int offset = 6; offset >= 0; offset--) {
LocalDate day = LocalDate.now().minusDays(offset);
LocalDateTime dayStart = day.atStartOfDay();
LocalDateTime dayEnd = day.atTime(LocalTime.MAX).withNano(0);
String startTime = dateRange.getStart().atStartOfDay().format(formatter);
String endTime = dateRange.getEnd().atTime(LocalTime.MAX).withNano(0).format(formatter);
DeviceTotalTimeRecordReqVO reqVO = new DeviceTotalTimeRecordReqVO();
reqVO.setIds(String.valueOf(deviceId));
reqVO.setStartTime(dayStart.format(formatter));
reqVO.setEndTime(dayEnd.format(formatter));
log.info("getDeviceRateTrendByDeviceId 开始 deviceId={}, period={}, startTime={}, endTime={}",
deviceId, period, startTime, endTime);
// queryDeviceDayTrend 改成“按 create_time 的日期分组”,
// 则这里每一天的结果就等价于 deviceOperationPage 单独查当天
List<Map<String, Object>> dayRows = tDengineService.queryDeviceDayTrend(deviceId, startTime, endTime);
log.info("getDeviceRateTrendByDeviceId TD原始结果 deviceId={}, dayRows={}", deviceId, dayRows);
Map<LocalDate, Map<String, Object>> dayMap = new HashMap<>();
if (CollectionUtils.isNotEmpty(dayRows)) {
for (Map<String, Object> row : dayRows) {
Object dayObj = row.get("day");
if (dayObj == null) {
continue;
}
try {
dayMap.put(LocalDate.parse(String.valueOf(dayObj)), row);
} catch (Exception e) {
log.warn("解析设备日趋势日期失败deviceId={}, day={}", deviceId, dayObj);
}
}
}
List<DeviceTotalTimeRecordRespVO> dayList = deviceOperationPageList(reqVO);
DeviceTotalTimeRecordRespVO record = CollectionUtils.isNotEmpty(dayList) ? dayList.get(0) : null;
List<DeviceOperationRateTrendRespVO> result = new ArrayList<>();
for (LocalDate day = dateRange.getStart(); !day.isAfter(dateRange.getEnd()); day = day.plusDays(1)) {
Map<String, Object> row = dayMap.get(day);
DeviceOperationRateTrendRespVO trend = new DeviceOperationRateTrendRespVO();
trend.setDay(day.toString());
trend.setPowerOnRate(record != null ? record.getPowerOnRate() : "0%");
trend.setUtilizationRate(record != null ? record.getUtilizationRate() : "0%");
if (row == null) {
trend.setPowerOnRate("0%");
trend.setUtilizationRate("0%");
result.add(trend);
continue;
}
try {
// 与 calculateAndSetConvertedValues 完全一致
double offlineSec = toDouble(row, "totalOfflineTime");
double runningSec = toDouble(row, "totalRunningTime");
double standbySec = toDouble(row, "totalStandbyTime");
double faultSec = toDouble(row, "totalFaultTime");
double onlineSec = runningSec + standbySec + faultSec;
double totalSec = offlineSec + onlineSec;
if (totalSec < 0) {
totalSec = 0;
}
double powerOnRate = 0D;
if (totalSec > 0D) {
powerOnRate = onlineSec / totalSec;
}
double utilizationRate = 0D;
if (onlineSec > 0D) {
utilizationRate = runningSec / onlineSec;
}
trend.setPowerOnRate(TimeConverterUtil.getPercentString(powerOnRate));
trend.setUtilizationRate(TimeConverterUtil.getPercentString(utilizationRate));
log.info("getDeviceRateTrendByDeviceId 每日计算 deviceId={}, day={}, row={}, " +
"offlineSec={}, runningSec={}, standbySec={}, faultSec={}, onlineSec={}, totalSec={}, " +
"powerOnRateRaw={}, utilizationRateRaw={}, powerOnRateStr={}, utilizationRateStr={}",
deviceId,
day,
row,
offlineSec,
runningSec,
standbySec,
faultSec,
onlineSec,
totalSec,
powerOnRate,
utilizationRate,
TimeConverterUtil.getPercentString(powerOnRate),
TimeConverterUtil.getPercentString(utilizationRate)
);
} catch (Exception e) {
log.error("计算设备日趋势失败deviceId={}, day={}", deviceId, day, e);
trend.setPowerOnRate("0%");
trend.setUtilizationRate("0%");
}
result.add(trend);
}
return result;
}
private static double toDouble(Map<String, Object> row, String key) {
if (row == null || row.get(key) == null) {
return 0D;
}
Object value = row.get(key);
if (value instanceof Number) {
return ((Number) value).doubleValue();
}
try {
return Double.parseDouble(String.valueOf(value));
} catch (Exception e) {
return 0D;
}
}
private void applyPeriodIfNecessary(DeviceTotalTimeRecordReqVO reqVO) {
if (StringUtils.isNotBlank(reqVO.getStartTime()) && StringUtils.isNotBlank(reqVO.getEndTime())) {
return;
}
String period = StringUtils.defaultIfBlank(
reqVO.getPeriod(),
DeviceRateTrendPeriodEnum.LAST_7_DAYS.getCode()
);
DeviceRateTrendPeriodEnum.DateRange dateRange = DeviceRateTrendPeriodEnum
.valueOfCode(period)
.resolve(LocalDate.now());
LocalDateTime startDateTime = dateRange.getStart().atStartOfDay();
LocalDateTime endDateTime = dateRange.getEnd().atTime(LocalTime.MAX).withNano(0);
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DateUtils.FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND);
reqVO.setStartTime(startDateTime.format(formatter));
reqVO.setEndTime(endDateTime.format(formatter));
}
private DeviceRateTrendPeriodEnum.DateRange resolvePeriodRange(String period) {
String actualPeriod = StringUtils.defaultIfBlank(period, DeviceRateTrendPeriodEnum.LAST_7_DAYS.getCode());
return DeviceRateTrendPeriodEnum.valueOfCode(actualPeriod).resolve(LocalDate.now());
}
@Override
public List<DeviceTotalTimeRecordRespVO> deviceOperationList(DeviceTotalTimeRecordReqVO deviceTotalTimeRecordReqVO) {
@ -391,6 +526,25 @@ public class DeviceOperationRecordServiceImpl implements DeviceOperationRecordSe
record.setPowerOnRate(TimeConverterUtil.getPercentString(powerOnRate));
record.setUtilizationRate(TimeConverterUtil.getPercentString(utilizationRate));
log.info("calculateAndSetConvertedValues deviceId={}, deviceCode={}, startTime={}, endTime={}, " +
"offlineSec={}, runningSec={}, standbySec={}, faultSec={}, onlineSec={}, totalSec={}, " +
"powerOnRateRaw={}, utilizationRateRaw={}, powerOnRateStr={}, utilizationRateStr={}",
record.getId(),
record.getDeviceCode(),
startTimeStr,
endTimeStr,
offlineSec,
runningSec,
standbySec,
faultSec,
onlineSec,
totalSec,
powerOnRate,
utilizationRate,
TimeConverterUtil.getPercentString(powerOnRate),
TimeConverterUtil.getPercentString(utilizationRate)
);
} catch (Exception e) {
log.error("计算设备{}时间统计出错: {}", record.getDeviceCode(), e.getMessage());
setDefaultValues(record);

@ -299,5 +299,13 @@
ORDER BY d.id DESC
</select>
<select id="selectHolidayDaysInRange" resultType="java.util.Date">
SELECT DISTINCT the_day
FROM mes_cal_holiday
WHERE deleted = b'0'
AND the_day &gt;= #{startDay}
AND the_day &lt; #{endDayExclusive}
AND holiday_type = 'HOLIDAY'
</select>
</mapper>

@ -139,39 +139,6 @@ public class DashboardController {
item.setValue(taskMapper.selectCount(new LambdaQueryWrapperX<TaskDO>()
.betweenIfPresent(TaskDO::getCreateTime, taskReqVO.getStartTime()))); // 创建时间
taskItems.add(item);
// // 未开工任务数
// item = new TaskRespVO.Item();
// item.setKey("2");
// item.setLabel("未开工任务数");
// Collection<Long> count1 = new ArrayList<>();
// count1.add(1L);
// count1.add(2L);
// count1.add(3L);
// item.setValue(taskMapper.selectCount(new LambdaQueryWrapperX<TaskDO>()
// .in(TaskDO::getStatus, count1)
// .betweenIfPresent(TaskDO::getOrderDate, taskReqVO.getStartTime()))); // 下达时间
// taskItems.add(item);
// // 生产中任务数
// item = new TaskRespVO.Item();
// item.setKey("3");
// item.setLabel("生产中任务数");
// Collection<Long> count2 = new ArrayList<>();
// count2.add(4L);
// item.setValue(taskMapper.selectCount(new LambdaQueryWrapperX<TaskDO>()
// .in(TaskDO::getStatus, count2)
// .betweenIfPresent(TaskDO::getOrderDate, taskReqVO.getStartTime())));
// taskItems.add(item);
// // 完工任务数
// item = new TaskRespVO.Item();
// item.setKey("4");
// item.setLabel("完工任务数");
// Collection<Long> count3 = new ArrayList<>();
// count3.add(5L);
// count3.add(6L);
// item.setValue(taskMapper.selectCount(new LambdaQueryWrapperX<TaskDO>()
// .in(TaskDO::getStatus, count3)
// .betweenIfPresent(TaskDO::getOrderDate, taskReqVO.getStartTime())));
// taskItems.add(item);
// 生产计划总数
@ -347,66 +314,108 @@ public class DashboardController {
@GetMapping("/getTodoList")
@Operation(summary = "获得待办任务")
@Parameter(name = "id", description = "编号", required = true, example = "1024")
// @PreAuthorize("@ss.hasPermission('mes:bom:query')")
// @PreAuthorize("@ss.hasPermission('mes:bom:query')")
public CommonResult<List<TodoRespVO>> getTodoList() {
// 待办结果集
List<TodoRespVO> todoRespVOList = new ArrayList<>();
// 设备维修
// 1. 查询各类待办数据
List<DvRepairDO> dvRepairDOList = dvRepairService.getDvRepairDOListByStatus();
List<TicketManagementDO> ticketManagementDOList = ticketManagementService.getListByJobStatus();
List<MoldRepairDO> moldRepairDOList = moldRepairService.getMoldRepairDOListByStatus();
List<MoldTicketManagementDO> moldTicketManagementDOList = moldTicketManagementService.getListByJobStatus();
// 2. 批量查询设备名称,避免循环中逐条查库
Map<Long, String> deviceNameMap = new HashMap<>();
if (dvRepairDOList != null && !dvRepairDOList.isEmpty()) {
List<Long> deviceIds = dvRepairDOList.stream()
.map(DvRepairDO::getDeviceId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (!deviceIds.isEmpty()) {
List<DeviceLedgerDO> deviceLedgerList = deviceLedgerService.getDeviceLedgerListByIds(deviceIds);
deviceNameMap = deviceLedgerList.stream()
.collect(Collectors.toMap(
DeviceLedgerDO::getId,
DeviceLedgerDO::getDeviceName,
(a, b) -> a
));
}
}
// 3. 批量查询模具名称,避免循环中逐条查库
Map<Long, String> moldNameMap = new HashMap<>();
if (moldRepairDOList != null && !moldRepairDOList.isEmpty()) {
List<Long> moldIds = moldRepairDOList.stream()
.map(MoldRepairDO::getMoldId)
.filter(Objects::nonNull)
.distinct()
.collect(Collectors.toList());
if (!moldIds.isEmpty()) {
List<MoldDO> moldList = moldService.getMoldListByIds(moldIds);
moldNameMap = moldList.stream()
.collect(Collectors.toMap(
MoldDO::getId,
MoldDO::getName,
(a, b) -> a
));
}
}
// 4. 组装设备维修待办
for (DvRepairDO dvRepairDO : dvRepairDOList) {
TodoRespVO todoRespVO = new TodoRespVO();
TodoRespVO todoRespVO = new TodoRespVO();
todoRespVO.setCode(dvRepairDO.getRepairCode());
todoRespVO.setName(dvRepairDO.getRepairName());
todoRespVO.setType("设备维修");
todoRespVO.setDeviceName(deviceLedgerService.getDeviceLedger(dvRepairDO.getDeviceId()).getDeviceName());
todoRespVO.setDeviceName(deviceNameMap.getOrDefault(dvRepairDO.getDeviceId(), "未知设备"));
todoRespVO.setCreateTime(dvRepairDO.getCreateTime());
todoRespVOList.add(todoRespVO);
}
// 设备保养 点检
List<TicketManagementDO> ticketManagementDOList = ticketManagementService.getListByJobStatus();
for (TicketManagementDO ticketManagementDO : ticketManagementDOList) {
TodoRespVO todoRespVO = new TodoRespVO();
// 5. 组装设备保养 / 点检待办
for (TicketManagementDO ticketManagementDO : ticketManagementDOList) {
TodoRespVO todoRespVO = new TodoRespVO();
todoRespVO.setCode(ticketManagementDO.getPlanNo());
todoRespVO.setName(ticketManagementDO.getConfigName());
if (ticketManagementDO.getPlanType() == 2) {
todoRespVO.setType("设备保养");
} else {
todoRespVO.setType("设备点检");
}
todoRespVO.setType(ticketManagementDO.getPlanType() == 2 ? "设备保养" : "设备点检");
todoRespVO.setDeviceName(ticketManagementDO.getDeviceName());
todoRespVO.setCreateTime(ticketManagementDO.getCreateTime());
todoRespVOList.add(todoRespVO);
}
// 模具维修
List<MoldRepairDO> moldRepairDOList = moldRepairService.getMoldRepairDOListByStatus();
for (MoldRepairDO moldRepairDO : moldRepairDOList) {
TodoRespVO todoRespVO = new TodoRespVO();
// 6. 组装模具维修待办
for (MoldRepairDO moldRepairDO : moldRepairDOList) {
TodoRespVO todoRespVO = new TodoRespVO();
todoRespVO.setCode(moldRepairDO.getRepairCode());
todoRespVO.setName(moldRepairDO.getRepairName());
todoRespVO.setType("模具维修");
todoRespVO.setDeviceName( moldRepairDO.getMoldId() != null && moldService.getMold(moldRepairDO.getMoldId()) != null
? moldService.getMold(moldRepairDO.getMoldId()).getName()
: "未知设备");
todoRespVO.setDeviceName(moldNameMap.getOrDefault(moldRepairDO.getMoldId(), "未知设备"));
todoRespVO.setCreateTime(moldRepairDO.getCreateTime());
todoRespVOList.add(todoRespVO);
}
// 模具保养 点检
List<MoldTicketManagementDO> moldTicketManagementDOList = moldTicketManagementService.getListByJobStatus();
for (MoldTicketManagementDO moldTicketManagementDO : moldTicketManagementDOList) {
TodoRespVO todoRespVO = new TodoRespVO();
// 7. 组装模具保养 / 点检待办
for (MoldTicketManagementDO moldTicketManagementDO : moldTicketManagementDOList) {
TodoRespVO todoRespVO = new TodoRespVO();
todoRespVO.setCode(moldTicketManagementDO.getPlanNo());
todoRespVO.setName(moldTicketManagementDO.getConfigName());
if (moldTicketManagementDO.getPlanType() == 2) {
todoRespVO.setType("模具保养");
} else {
todoRespVO.setType("模具点检");
}
todoRespVO.setType(moldTicketManagementDO.getPlanType() == 2 ? "模具保养" : "模具点检");
todoRespVO.setDeviceName(moldTicketManagementDO.getMoldName());
todoRespVO.setCreateTime(moldTicketManagementDO.getCreateTime());
todoRespVOList.add(todoRespVO);
}
return success(todoRespVOList);
// 8. 按创建时间倒序排序,最近的待办排前面
todoRespVOList.sort(Comparator.comparing(
TodoRespVO::getCreateTime,
Comparator.nullsLast(Comparator.reverseOrder())
));
return success(todoRespVOList);
}
@GetMapping("/getDeviceRepairLineOptions")

@ -83,5 +83,7 @@ public interface DeviceLedgerService {
Map<Long, DeviceLedgerDO> getDeviceMap(Set<Long> longs);
List<DeviceLedgerDO> getDeviceLedgerListByIds(Collection<Long> ids);
}

@ -431,4 +431,11 @@ public class DeviceLedgerServiceImpl implements DeviceLedgerService {
return deviceLedgerMapper.selectListByIds(ids).stream()
.collect(Collectors.toMap(DeviceLedgerDO::getId, Function.identity(), (a, b) -> a));
}
@Override
public List<DeviceLedgerDO> getDeviceLedgerListByIds(Collection<Long> ids) {
if (ids == null || ids.isEmpty()) {
return Collections.emptyList();
}
return deviceLedgerMapper.selectBatchIds(ids); }
}
Loading…
Cancel
Save