From f6aad2752e6a49128196268e27293487d35aa610 Mon Sep 17 00:00:00 2001 From: HuangHuiKang Date: Mon, 27 Jul 2026 17:22:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=E6=96=B0=E5=A2=9E=E4=BA=A7=E8=83=BD?= =?UTF-8?q?=E6=8A=A5=E8=A1=A8=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mqtt/consumer/IotMqttRuntimeCache.java | 2 +- .../mqtt/consumer/MqttDataHandler.java | 1 - ...eviceProductionDailyReportServiceImpl.java | 374 ++++++++++++++++-- .../iot/service/device/TDengineService.java | 34 ++ ...ding-production-daily-report-template.xlsx | Bin 8600 -> 5798 bytes 5 files changed, 379 insertions(+), 32 deletions(-) diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/mqtt/consumer/IotMqttRuntimeCache.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/mqtt/consumer/IotMqttRuntimeCache.java index 1fd3f241a..8818c17bb 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/mqtt/consumer/IotMqttRuntimeCache.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/mqtt/consumer/IotMqttRuntimeCache.java @@ -122,7 +122,7 @@ public class IotMqttRuntimeCache { } List parsedRules = new ArrayList<>(); for (DevicePointRulesDO rule : rules) { - if (StringUtils.isBlank(rule.getFieldRule())) { + if (StringUtils.isBlank(rule.getAlarmLevel()) || StringUtils.isBlank(rule.getFieldRule())) { continue; } try { diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/mqtt/consumer/MqttDataHandler.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/mqtt/consumer/MqttDataHandler.java index 048f301f3..f3ba88628 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/mqtt/consumer/MqttDataHandler.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/framework/mqtt/consumer/MqttDataHandler.java @@ -398,7 +398,6 @@ public class MqttDataHandler extends SuperConsumer { attributeCode, processedValue, JSON.toJSONString(pointRulesRespVO)); handleMatchedSuccessRule(cachedRule.getRule(), pointRulesRespVO, processedValue, device, attributeCode, point); - break; } else { log.debug("规则不匹配: modelId={}, value={}, rule={}", attributeCode, processedValue, diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceProductionDailyReportServiceImpl.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceProductionDailyReportServiceImpl.java index 6d82107bd..7d5795e95 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceProductionDailyReportServiceImpl.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/DeviceProductionDailyReportServiceImpl.java @@ -12,24 +12,31 @@ import cn.iocoder.yudao.module.iot.controller.admin.device.vo.DeviceProductionDa import cn.iocoder.yudao.module.iot.controller.admin.device.vo.DeviceProductionDailyReportRespVO; import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO; import cn.iocoder.yudao.module.iot.dal.dataobject.devicecontactmodel.DeviceContactModelDO; +import cn.iocoder.yudao.module.iot.dal.dataobject.devicewarinningrecord.DeviceWarinningRecordDO; import cn.iocoder.yudao.module.iot.dal.mysql.device.DeviceMapper; import cn.iocoder.yudao.module.iot.dal.mysql.devicecontactmodel.DeviceContactModelMapper; import cn.iocoder.yudao.module.mes.api.workteam.WorkTeamApi; import cn.iocoder.yudao.module.mes.api.workteam.dto.WorkTeamRespDTO; +import cn.iocoder.yudao.module.system.api.dict.DictDataApi; +import cn.iocoder.yudao.module.system.api.dict.dto.DictDataRespDTO; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; import org.apache.poi.ss.usermodel.Cell; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.CellType; import org.apache.poi.ss.usermodel.Row; import org.apache.poi.ss.usermodel.Sheet; import org.apache.poi.ss.usermodel.Workbook; import org.apache.poi.ss.usermodel.WorkbookFactory; +import org.apache.poi.ss.util.CellRangeAddress; import org.springframework.core.io.ClassPathResource; import org.springframework.stereotype.Service; import org.springframework.validation.annotation.Validated; import javax.annotation.Resource; +import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.InputStream; import java.math.BigDecimal; @@ -57,8 +64,10 @@ public class DeviceProductionDailyReportServiceImpl implements DeviceProductionD private static final String TEMPLATE_PATH = "excel/templates/fully-automatic-molding-production-daily-report-template.xlsx"; private static final String REPORT_PATH_PREFIX = "iot/device/production-daily-report"; + private static final String ALARM_LEVEL_DICT_TYPE = "alarm_registration"; private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("HH:mm:ss"); @Resource private DeviceMapper deviceMapper; @@ -69,6 +78,8 @@ public class DeviceProductionDailyReportServiceImpl implements DeviceProductionD @Resource private WorkTeamApi workTeamApi; @Resource + private DictDataApi dictDataApi; + @Resource private FileService fileService; @Resource private FileMapper fileMapper; @@ -96,15 +107,19 @@ public class DeviceProductionDailyReportServiceImpl implements DeviceProductionD if (!endTime.isAfter(startTime)) { endTime = endTime.plusDays(1); } + log.info("[production-daily-report] generate request: reportDate={}, workTeamId={}, workTeamName={}, startTime={}, endTime={}, deviceIds={}", + reqVO.getReportDate(), reqVO.getWorkTeamId(), workTeam.getTeamName(), startTime, endTime, deviceIds); List items = buildCapacityItems(deviceIds, deviceMap, startTime, endTime); - byte[] excelContent = buildExcel(reqVO, workTeam, startTime, endTime, items); + byte[] excelContent = buildExcel(reqVO, workTeam, startTime, endTime, items, deviceIds); String fileName = String.format("全自动成型机生产日报表_%s_%s_%s.xlsx", reqVO.getReportDate().format(DateTimeFormatter.BASIC_ISO_DATE), reqVO.getWorkTeamId(), System.currentTimeMillis()); String filePath = String.format("%s/date=%s/workTeamId=%s/deviceIds=%s/deviceTags=%s/%s", REPORT_PATH_PREFIX, reqVO.getReportDate(), reqVO.getWorkTeamId(), joinDeviceIds(deviceIds), buildDeviceTags(deviceIds), fileName); Map file = fileService.createFile(fileName, filePath, excelContent); + log.info("[production-daily-report] file created: fileName={}, filePath={}, fileUrl={}, contentLength={}", + file.get("fileName"), filePath, file.get("fileUrl"), excelContent.length); DeviceProductionDailyReportRespVO respVO = buildBaseResp(reqVO.getReportDate(), workTeam, startTime, endTime, deviceIds); respVO.setFileName(file.get("fileName")); @@ -141,7 +156,7 @@ public class DeviceProductionDailyReportServiceImpl implements DeviceProductionD Map deviceMap, LocalDateTime startTime, LocalDateTime endTime) { - Map> capacityPointMap = queryCapacityPoints(deviceIds); + Map capacityPointMap = queryCapacityPoints(deviceIds); Map> firstRows = tDengineService.queryDevicesEarliestRow(deviceIds, startTime.format(DATE_TIME_FORMATTER), endTime.format(DATE_TIME_FORMATTER)); Map> lastRows = tDengineService.queryDevicesLatestRow(deviceIds, @@ -150,31 +165,28 @@ public class DeviceProductionDailyReportServiceImpl implements DeviceProductionD List items = new ArrayList<>(); for (Long deviceId : deviceIds) { DeviceDO device = deviceMap.get(deviceId); - List points = capacityPointMap.getOrDefault(deviceId, Collections.emptyList()); - if (CollUtil.isEmpty(points)) { + DeviceContactModelDO point = capacityPointMap.get(deviceId); + if (point == null) { items.add(buildEmptyCapacityItem(device)); continue; } Map firstRow = firstRows.get(deviceId); Map lastRow = lastRows.get(deviceId); - for (DeviceContactModelDO point : points) { - items.add(buildCapacityItem(device, point, firstRow, lastRow)); - } + items.add(buildCapacityItem(device, point, firstRow, lastRow)); } return items; } - private Map> queryCapacityPoints(List deviceIds) { + private Map queryCapacityPoints(List deviceIds) { List points = deviceContactModelMapper.selectList( Wrappers.lambdaQuery() .in(DeviceContactModelDO::getDeviceId, deviceIds) - .and(wrapper -> wrapper.like(DeviceContactModelDO::getTypeName, "产能") - .or().like(DeviceContactModelDO::getAttributeType, "产能") - .or().like(DeviceContactModelDO::getAttributeName, "产能")) + .eq(DeviceContactModelDO::getTypeName, "产能") .orderByAsc(DeviceContactModelDO::getDeviceId) - .orderByAsc(DeviceContactModelDO::getSort) - .orderByAsc(DeviceContactModelDO::getId)); - return points.stream().collect(Collectors.groupingBy(DeviceContactModelDO::getDeviceId, LinkedHashMap::new, Collectors.toList())); + .orderByDesc(DeviceContactModelDO::getCreateTime) + .orderByDesc(DeviceContactModelDO::getId)); + return points.stream().collect(Collectors.toMap(DeviceContactModelDO::getDeviceId, item -> item, (first, second) -> first, + LinkedHashMap::new)); } private DeviceProductionDailyReportRespVO.DeviceCapacityItem buildEmptyCapacityItem(DeviceDO device) { @@ -212,36 +224,190 @@ public class DeviceProductionDailyReportServiceImpl implements DeviceProductionD @SneakyThrows private byte[] buildExcel(DeviceProductionDailyReportGenerateReqVO reqVO, WorkTeamRespDTO workTeam, LocalDateTime startTime, LocalDateTime endTime, - List items) { + List items, List deviceIds) { try (InputStream inputStream = new ClassPathResource(TEMPLATE_PATH).getInputStream(); - Workbook workbook = WorkbookFactory.create(inputStream); + Workbook workbook = WorkbookFactory.create(inputStream); ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { Sheet sheet = workbook.getSheetAt(0); - setCellValue(sheet, 2, 0, "日期:" + LocalDate.now().format(DATE_FORMATTER)); - setCellValue(sheet, 2, 4, "班次:" + StrUtil.blankToDefault(workTeam.getTeamName(), "")); - setCellValue(sheet, 19, 9, "审核:"); - - int rowIndex = 5; + log.info("[production-daily-report] template loaded: path={}, sheetName={}, mergedRegions={}", + TEMPLATE_PATH, sheet.getSheetName(), sheet.getMergedRegions()); + String dateText = "日期:" + reqVO.getReportDate().format(DATE_FORMATTER); + String shiftText = "班次:" + StrUtil.blankToDefault(workTeam.getTeamName(), ""); + setHeaderValue(sheet, "日期:", dateText, 2, 0); + setHeaderValue(sheet, "班次:", shiftText, 2, 4); + log.info("[production-daily-report] header cells after write: A3={}, C3={}, E3={}", + getCellValue(sheet, 2, 0), getCellValue(sheet, 2, 2), getCellValue(sheet, 2, 4)); + + int rowIndex = 4; + int exceptionStartRowIndex = 5; + Row templateRow = sheet.getRow(rowIndex); + if (items.size() > 1) { + sheet.shiftRows(exceptionStartRowIndex, sheet.getLastRowNum(), items.size() - 1, true, false); + } for (int index = 0; index < items.size(); index++) { - if (rowIndex + index > 9) { - sheet.shiftRows(10, sheet.getLastRowNum(), 1, true, false); - } Row row = sheet.getRow(rowIndex + index); if (row == null) { row = sheet.createRow(rowIndex + index); + copyRowStyle(templateRow, row, 5); } DeviceProductionDailyReportRespVO.DeviceCapacityItem item = items.get(index); setCellValue(row, 0, index + 1); setCellValue(row, 1, StrUtil.blankToDefault(item.getDeviceCode(), item.getDeviceName())); - setCellValue(row, 4, item.getCapacityName()); - setCellValue(row, 20, item.getCapacityDiff()); - setCellValue(row, 21, toHour(startTime, endTime)); - setCellValue(row, 22, startTime.toLocalTime() + "-" + endTime.toLocalTime()); - setCellValue(row, 24, workTeam.getTeamName()); + setCellValue(row, 2, buildCapacityPointText(item)); + setCellValue(row, 3, item.getCapacityDiff()); + setCellValue(row, 4, startTime.toLocalTime() + "-" + endTime.toLocalTime()); } + fillWarningDescription(sheet, rowIndex + items.size() + 2, deviceIds, startTime, endTime); workbook.write(outputStream); - return outputStream.toByteArray(); + byte[] bytes = outputStream.toByteArray(); + logGeneratedHeader(bytes); + return bytes; + } + } + + private void fillWarningDescription(Sheet sheet, int warningRowIndex, List deviceIds, + LocalDateTime startTime, LocalDateTime endTime) { + List warningRecords = tDengineService.selectDeviceWarningList(deviceIds, startTime, endTime); + Map> warningMap = warningRecords.stream() + .collect(Collectors.groupingBy(DeviceWarinningRecordDO::getDeviceId, LinkedHashMap::new, Collectors.toList())); + Row templateRow = sheet.getRow(warningRowIndex); + for (int index = 0; index < deviceIds.size(); index++) { + Row row = sheet.getRow(warningRowIndex + index); + if (row == null) { + row = sheet.createRow(warningRowIndex + index); + copyRowStyle(templateRow, row, 5); + mergeCellsIfAbsent(sheet, warningRowIndex + index, 2, 4); + } + Long deviceId = deviceIds.get(index); + setCellValue(row, 0, index + 1); + setCellValue(row, 1, getDeviceText(deviceId)); + String warningDescription = buildWarningDescription(warningMap.getOrDefault(deviceId, Collections.emptyList())); + setCellValue(row, 2, warningDescription); + setWrapText(row, 2); + row.setHeightInPoints(Math.max(row.getHeightInPoints(), 18F * Math.max(1, warningDescription.split("\n", -1).length))); + } + } + + private String buildCapacityPointText(DeviceProductionDailyReportRespVO.DeviceCapacityItem item) { + String capacityName = item.getCapacityName(); + String capacityCode = item.getCapacityCode(); + if (StringUtils.isBlank(capacityName)) { + return StrUtil.blankToDefault(capacityCode, ""); + } + if (StringUtils.isBlank(capacityCode) || StringUtils.equals(capacityName, capacityCode)) { + return capacityName; + } + return capacityName + "(" + capacityCode + ")"; + } + + private String getDeviceText(Long deviceId) { + DeviceDO device = deviceMapper.selectById(deviceId); + if (device == null) { + return ""; + } + return StrUtil.blankToDefault(device.getDeviceCode(), device.getDeviceName()); + } + + private String buildWarningDescription(List warningRecords) { + if (CollUtil.isEmpty(warningRecords)) { + return ""; } + return warningRecords.stream() + .collect(Collectors.groupingBy(this::getWarningPointName, LinkedHashMap::new, Collectors.toList())) + .entrySet().stream() + .map(entry -> entry.getKey() + ":" + buildWarningPointSummary(entry.getValue())) + .collect(Collectors.joining("\n")); + } + + private String buildWarningPointSummary(List warningRecords) { + return warningRecords.stream() + .collect(Collectors.groupingBy(this::getWarningRuleKey, LinkedHashMap::new, Collectors.toList())) + .values().stream() + .map(this::buildWarningRuleSummary) + .collect(Collectors.joining(";")); + } + + private String getWarningRuleKey(DeviceWarinningRecordDO record) { + return getWarningName(record) + "|" + StrUtil.blankToDefault(record.getAlarmLevel(), ""); + } + + private String buildWarningRuleSummary(List warningRecords) { + DeviceWarinningRecordDO firstRecord = warningRecords.get(0); + String levelText = buildAlarmLevelText(firstRecord.getAlarmLevel()); + return getWarningName(firstRecord) + warningRecords.size() + "次" + + buildWarningValueRangeText(warningRecords) + levelText + buildWarningTimeRangeText(warningRecords); + } + + private String buildAlarmLevelText(String alarmLevel) { + if (StringUtils.isBlank(alarmLevel)) { + return ""; + } + DictDataRespDTO dictData = dictDataApi.getDictData(ALARM_LEVEL_DICT_TYPE, alarmLevel); + return ",等级=" + (dictData != null && StringUtils.isNotBlank(dictData.getLabel()) ? dictData.getLabel() : alarmLevel); + } + + private String getWarningPointName(DeviceWarinningRecordDO record) { + return StrUtil.blankToDefault(record.getModelName(), + StrUtil.blankToDefault(record.getRuleName(), StrUtil.blankToDefault(record.getRule(), "未知点位"))); + } + + private String getWarningName(DeviceWarinningRecordDO record) { + return StrUtil.blankToDefault(record.getRuleName(), StrUtil.blankToDefault(record.getRule(), "告警")); + } + + private String buildWarningValueRangeText(List warningRecords) { + List values = warningRecords.stream() + .map(this::parseWarningValue) + .filter(Objects::nonNull) + .collect(Collectors.toList()); + if (CollUtil.isEmpty(values)) { + return ""; + } + double minValue = values.stream().min(Double::compareTo).orElse(0D); + double maxValue = values.stream().max(Double::compareTo).orElse(0D); + if (BigDecimal.valueOf(minValue).compareTo(BigDecimal.valueOf(maxValue)) == 0) { + return ",值=" + formatNumber(minValue); + } + return ",值=" + formatNumber(minValue) + "~" + formatNumber(maxValue); + } + + private Double parseWarningValue(DeviceWarinningRecordDO record) { + if (StringUtils.isBlank(record.getAddressValue())) { + return null; + } + try { + return Double.valueOf(record.getAddressValue()); + } catch (NumberFormatException e) { + return null; + } + } + + private String buildWarningTimeRangeText(List warningRecords) { + List times = warningRecords.stream() + .map(DeviceWarinningRecordDO::getCreateTime) + .filter(Objects::nonNull) + .sorted() + .collect(Collectors.toList()); + if (CollUtil.isEmpty(times)) { + return ""; + } + LocalDateTime startTime = times.get(0); + LocalDateTime endTime = times.get(times.size() - 1); + if (Objects.equals(startTime, endTime)) { + return ",时间=" + formatWarningTime(startTime, startTime); + } + return ",时间=" + formatWarningTime(startTime, endTime) + "~" + formatWarningTime(endTime, startTime); + } + + private String formatWarningTime(LocalDateTime time, LocalDateTime compareTime) { + if (Objects.equals(time.toLocalDate(), compareTime.toLocalDate())) { + return time.format(TIME_FORMATTER); + } + return time.format(DATE_TIME_FORMATTER); + } + + private String formatNumber(Double value) { + return BigDecimal.valueOf(value).stripTrailingZeros().toPlainString(); } private DeviceProductionDailyReportRespVO convertFileToReport(FileDO file) { @@ -343,6 +509,11 @@ public class DeviceProductionDailyReportServiceImpl implements DeviceProductionD } private void setCellValue(Sheet sheet, int rowIndex, int columnIndex, Object value) { + CellRangeAddress mergedRegion = getMergedRegion(sheet, rowIndex, columnIndex); + if (mergedRegion != null) { + rowIndex = mergedRegion.getFirstRow(); + columnIndex = mergedRegion.getFirstColumn(); + } Row row = sheet.getRow(rowIndex); if (row == null) { row = sheet.createRow(rowIndex); @@ -361,5 +532,148 @@ public class DeviceProductionDailyReportServiceImpl implements DeviceProductionD cell.setCellValue(value == null ? "" : value.toString()); } } + + private void setWrapText(Row row, int columnIndex) { + Cell cell = row.getCell(columnIndex); + if (cell == null) { + return; + } + CellStyle cellStyle = row.getSheet().getWorkbook().createCellStyle(); + cellStyle.cloneStyleFrom(cell.getCellStyle()); + cellStyle.setWrapText(true); + cell.setCellStyle(cellStyle); + } + + private void copyRowStyle(Row sourceRow, Row targetRow, int columnCount) { + if (sourceRow == null || targetRow == null) { + return; + } + targetRow.setHeight(sourceRow.getHeight()); + for (int columnIndex = 0; columnIndex < columnCount; columnIndex++) { + Cell sourceCell = sourceRow.getCell(columnIndex); + if (sourceCell == null) { + continue; + } + Cell targetCell = targetRow.getCell(columnIndex); + if (targetCell == null) { + targetCell = targetRow.createCell(columnIndex); + } + targetCell.setCellStyle(sourceCell.getCellStyle()); + } + } + + private void mergeCellsIfAbsent(Sheet sheet, int rowIndex, int firstColumn, int lastColumn) { + CellRangeAddress newRegion = new CellRangeAddress(rowIndex, rowIndex, firstColumn, lastColumn); + for (CellRangeAddress mergedRegion : sheet.getMergedRegions()) { + if (mergedRegion.intersects(newRegion)) { + return; + } + } + sheet.addMergedRegion(newRegion); + } + + private void setHeaderValue(Sheet sheet, String label, String value, int fallbackRowIndex, int fallbackColumnIndex) { + Cell labelCell = findCellByPrefix(sheet, label); + if (labelCell != null) { + CellRangeAddress mergedRegion = getMergedRegion(sheet, labelCell.getRowIndex(), labelCell.getColumnIndex()); + log.info("[production-daily-report] set header by label: label={}, foundCell={}, rowIndex={}, columnIndex={}, mergedRegion={}, value={}", + label, labelCell.getAddress().formatAsString(), labelCell.getRowIndex(), labelCell.getColumnIndex(), mergedRegion, value); + setStringCellValueForce(sheet, labelCell.getRowIndex(), labelCell.getColumnIndex(), value); + return; + } + CellRangeAddress mergedRegion = getMergedRegion(sheet, fallbackRowIndex, fallbackColumnIndex); + log.info("[production-daily-report] set header by fallback: label={}, fallbackRowIndex={}, fallbackColumnIndex={}, mergedRegion={}, value={}", + label, fallbackRowIndex, fallbackColumnIndex, mergedRegion, value); + setStringCellValueForce(sheet, fallbackRowIndex, fallbackColumnIndex, value); + } + + private void setStringCellValueForce(Sheet sheet, int rowIndex, int columnIndex, String value) { + CellRangeAddress mergedRegion = getMergedRegion(sheet, rowIndex, columnIndex); + int firstRowIndex = mergedRegion != null ? mergedRegion.getFirstRow() : rowIndex; + int firstColumnIndex = mergedRegion != null ? mergedRegion.getFirstColumn() : columnIndex; + Row row = sheet.getRow(firstRowIndex); + if (row == null) { + row = sheet.createRow(firstRowIndex); + } + + Cell oldCell = row.getCell(firstColumnIndex); + org.apache.poi.ss.usermodel.CellStyle cellStyle = oldCell != null ? oldCell.getCellStyle() : null; + if (oldCell != null) { + row.removeCell(oldCell); + } + Cell newCell = row.createCell(firstColumnIndex, CellType.STRING); + if (cellStyle != null) { + newCell.setCellStyle(cellStyle); + } + newCell.setCellValue(value); + + if (mergedRegion != null) { + for (int column = mergedRegion.getFirstColumn() + 1; column <= mergedRegion.getLastColumn(); column++) { + Cell mergedCell = row.getCell(column); + if (mergedCell != null) { + mergedCell.setCellValue(""); + } + } + } + log.info("[production-daily-report] force set header cell: targetRowIndex={}, targetColumnIndex={}, mergedRegion={}, value={}, actualValue={}", + firstRowIndex, firstColumnIndex, mergedRegion, value, getCellValue(sheet, firstRowIndex, firstColumnIndex)); + } + + @SneakyThrows + private void logGeneratedHeader(byte[] bytes) { + try (Workbook workbook = WorkbookFactory.create(new ByteArrayInputStream(bytes))) { + Sheet sheet = workbook.getSheetAt(0); + log.info("[production-daily-report] generated bytes header check: A3={}, C3={}, E3={}, mergedRegions={}", + getCellValue(sheet, 2, 0), getCellValue(sheet, 2, 2), getCellValue(sheet, 2, 4), sheet.getMergedRegions()); + } + } + + private String getCellValue(Sheet sheet, int rowIndex, int columnIndex) { + Row row = sheet.getRow(rowIndex); + Cell cell = row != null ? row.getCell(columnIndex) : null; + if (cell == null) { + CellRangeAddress mergedRegion = getMergedRegion(sheet, rowIndex, columnIndex); + if (mergedRegion != null) { + row = sheet.getRow(mergedRegion.getFirstRow()); + cell = row != null ? row.getCell(mergedRegion.getFirstColumn()) : null; + } + } + if (row == null) { + return null; + } + if (cell == null) { + return null; + } + if (cell.getCellType() == CellType.STRING) { + return cell.getStringCellValue(); + } + if (cell.getCellType() == CellType.NUMERIC) { + return String.valueOf(cell.getNumericCellValue()); + } + if (cell.getCellType() == CellType.BOOLEAN) { + return String.valueOf(cell.getBooleanCellValue()); + } + return cell.toString(); + } + + private Cell findCellByPrefix(Sheet sheet, String label) { + for (Row row : sheet) { + for (Cell cell : row) { + if (cell.getCellType() == CellType.STRING && StringUtils.startsWith(cell.getStringCellValue(), label)) { + return cell; + } + } + } + return null; + } + + private CellRangeAddress getMergedRegion(Sheet sheet, int rowIndex, int columnIndex) { + for (CellRangeAddress mergedRegion : sheet.getMergedRegions()) { + if (mergedRegion.isInRange(rowIndex, columnIndex)) { + return mergedRegion; + } + } + return null; + } } diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/TDengineService.java b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/TDengineService.java index 8c5409b1f..5c579f585 100644 --- a/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/TDengineService.java +++ b/yudao-module-iot/yudao-module-iot-biz/src/main/java/cn/iocoder/yudao/module/iot/service/device/TDengineService.java @@ -2662,6 +2662,40 @@ public class TDengineService { } } + /** + * 查询设备在指定时间段内的告警记录 + */ + @DS("tdengine") + public List selectDeviceWarningList(List deviceIds, LocalDateTime startTime, LocalDateTime endTime) { + if (CollectionUtils.isEmpty(deviceIds)) { + return Collections.emptyList(); + } + + String placeholders = deviceIds.stream().map(item -> "?").collect(Collectors.joining(",")); + StringBuilder sql = new StringBuilder(); + List params = new ArrayList<>(); + sql.append("SELECT * FROM ").append(tdengineTable("iot_device_warning_record")).append(" ") + .append("WHERE deleted = 0 ") + .append("AND device_id IN (").append(placeholders).append(") "); + params.addAll(deviceIds); + if (startTime != null) { + sql.append("AND create_time >= ? "); + params.add(Timestamp.valueOf(startTime)); + } + if (endTime != null) { + sql.append("AND create_time <= ? "); + params.add(Timestamp.valueOf(endTime)); + } + sql.append("ORDER BY device_id ASC, create_time ASC"); + + try { + return jdbcTemplate.query(sql.toString(), params.toArray(), (rs, rowNum) -> mapRowToWarningRecordDO(rs)); + } catch (Exception e) { + log.error("查询设备时间段告警记录失败: deviceIds={}, startTime={}, endTime={}", deviceIds, startTime, endTime, e); + return Collections.emptyList(); + } + } + /** * 映射查询结果 */ diff --git a/yudao-module-iot/yudao-module-iot-biz/src/main/resources/excel/templates/fully-automatic-molding-production-daily-report-template.xlsx b/yudao-module-iot/yudao-module-iot-biz/src/main/resources/excel/templates/fully-automatic-molding-production-daily-report-template.xlsx index ab81f204d592b48d5a8ee08582f9b6ac7de9f380..551e70a49383fdba9a0248f1b398e557980eb03a 100644 GIT binary patch delta 3463 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 delta 6229 zcma)AWmuH!)*ebqq>(NGkuIr0N*O@98wRA5k`yFf8i7HOp%IV+2^>wIN$T<{qe4~p66b1Uu&&A?xd##>Pi@xWFQa-2UKWq`hlPo_x59KJtSzn z`(c6uh!4-~k9?tq7l_eW6Ejy0g@^vknXeHkF$!D5COU|10Q^qC3PnNRd^=XBuwj<` z^RUuG#kF#4`7B0ItYnr};Q($%>V>4Z+!-|?QQWmUc5uoexhRVj*`p+XIsJFa)ew9- z4Go52&+Pj?r?C0Eg4E?t!_rB#t+13;N!wpKZ5h_0vr(G6tvC2vGYySPeR0K}5))%< z$O$!P53?RZ(Q$Zszx7F;Fp$ajxARE8oRP5iFk-VaEyMHE8>;~?yyM8Y zIO#nKL+VT&b?t0R2{+^PQTScqpJDxmzhWOv^3J_KJizpwFXB5s2&AG0e}Y2~RDCpX zovo(mMjk#A+ELCw$t0KYx1$~I$YRyJXx>w)O4iq!bz#ljY09_`WPAiByAE@)K8WdQ zC0vDyonp-KnpHQn9G@t<6kJ`L?p5BnIRl~+7f0vU$C6&gyEjvk57lCQrg!Kkv6JU~ zOLjBf^q#9yJ&_z!X)oW5-W(hSI>&SSXm(0@YLNE`Q}Zori?&;yNS402X;P)H=FQ-l zlKa5F;L{|PLv>Wtzj`!j8B1$D{Pi+%(7c|m5B709T5?x7x_8vC!uRub?5(_{pp>u< zv5rNvqUo{Ib?NqQ4R6W(N-5_oQ-hW9*!R-wy7}w)r8Svn`S5DX(5zF5ejM!n=;mztn-}fcn?Guio?~9@T`kdtQ*Rd4-jX zA-9js%dX3sEUii+XSQpu%rBB=`|e0Mrhi2qwVdfUKH3dDur^)bia(L|N>5z1Nx1Lj zrNJ?L7Yaq)nW($t;Rpp1@6xI-^!9^Mc$9pvT`^9KF?dDxhG?@Z?F_!bT)E$yONtB? z;|PI8z8B*>28&D-;|O!g(-@MCSfgII`;jeLqdeQiBm65|Ix^*moptYtRkF(t_3?*| zPH+V}CIr1#i6}@#6}*6FH}{)&v&qd3y3Bo1B=$U3$PD18li!PO zxo)d_O?PHjpYVFLl9MYT&!JuGBmb=G=s>0RjDUXnuHnmOD}hdj9ns__yPTKp(d-WUezz_2PCZRF6EuB^1C_ZzlY{vDAs(g%G(5}(j$n6SJK+;<0*_gnQ6id z7yPabrE}#zOt@KM(mW2eMDHbwrD8ek`vgvht139+Eq-)6J|{15%Z|JEd_9#}WkCDG zu=f37?M!NiMF{@rYZq3d`hb=piK!0bVfXIfP0e)b`1INyGDrTpw=X3yld)HYL`CA8 zQH_1)FZoLV&6IC%ioVw@ekOkhb_xG}c8~K(+$2}KW%BzZ!-|}Afu`&)QP9oV@si<9 z?FYY@OfF5pG#o*8oSTivT$`y^H;Z$m;)(Om6VGH3RQIQ8_jBZ<@=cF?s_JHVe^M0u ze3kR!^Z6imRVEIchQWK-F6!jkyQ%+-?M)G3Sy9kSL7-jR=ux7}wA}_}V*~cwYfs&< z7nC;R{61!M4BA`-C$+ijKJSO3GRGow^TIeE_BpqXVvTBZD)`03vQ>E_C{>SfSw`Pr z^JKKUyjD5hZ$I2_rhhdf0JhN}oz8@BU1G;0wne&m*FQ7K9i$d7N~Pef?3PKt9`*E= z{xxrZx(65)6j^KN5Y(Wwb&cxxR9BO_mPxuSbejBLyHE8>4EM|ZOs*GS8h~Gz&>fFb#%mG{{pJM2uP0 z&&9lVdfC=yrFKTnj~3rVc>yKc5TWACU_&{`4;Vr#J8VNVK0YAr?;rZv8Scsoc5RDR zy{@xJJOUOr2K>ND$j!UitzF{nx+?bLZMsYlVvngI4b-&B+xmhD?QV}T64Rf8N}}C9 z^B<<_9+Ry`PoqtxQrl;$GVTXZ?RXJpOF}SUp_Ea4)Gve(wh<$Dv&A6zumsPD;&L5( zK=1Q#p>BrjXUU2jN$euR>{i6azhC6xSARZKve-Qy=DpBf5Iq9QW`aC`by2GDrC12H z+rA$`&t^?$=CmgV$8We6jjyJEj!qm~wKy^-tJU|N`W#+jIK|Py-ga+xM@haeFIz*%Am*aAFyrdeC9pUXKkreHUM-L0XeEP5H1Co@tCnLS5;|Z8#2(HF zcJr?r`021F@|Ea$axNdD`>jLGyq=-F2o?7XqoJ{_g_c}L;@7j}Gn=k5i&_oR&<5Cb8tCZhE)MIY`uW!XPBIe?uWX19eB|9x7 z>r3Q{6fk@Qy+zK6HIRa7N6f{d|EydXA&$98QY;UYOCs)I(va9cE9XbBVWyJUL(0Vv z6qrUN_HyO5&^WRVDovt%O?ERWu0Gpbe*<;C5;8tJl47~?TlXPmFgJ;cmCET6=a@7^ z_6p_1h%U@jBKzm%REQN!BO-gna&p8{poV7oP)wjZN60~^gf+2dD;Knblm_!3Mu7jP z6${-f39O3Yn820r2>9l!jtAtJ_>}wVScw0I=>Gzgq$4%g8j(*^z-fSdvhb*y)b2Ps8NH$MJYt z{vU$|{4>M;j9SuDh1Md!(HN_f$B%bCOkRw&vbV0?$!3Fa!(?I1uo+5sz7319|E&%o zoG=US^Xn+XWtiS80#wLbsqrD?@}Q{)VmE)K)a{Tmn*wsn9+2$~dS~-OaDvw`G#WUs zP56_jD5LmOphBItp(B^DPc|Z!b#~$~>MWg|h`Mij$0SFak>PWdwW13@xqWy;TL`;@ zkOb|QR~0f%tm()~>yF4`wxCE62muTo=E(QTVyun*w&)9@G_bRIAWvZDlmvX#7D8>d zuo2AcM-V~S3MDt6p+#jI`vj0>pEZf}Sy|+t_SG?)pPqEAL8e<8%r<($(|osVhOYE| zo3rA0hZV0xFN)=pr~IIH2l;IohAEd=f|tKE;s%|<7e znmkxk3uXftF9WKZs$0g+&bZ`%8iJWcN|9rzuyh8ml*{Md*(&`v=VQUF9J$id>2oS9$237j}m){uAfxmQQ-jv z6*2O9*P3?3^?o@fQOO^n$~Da;#!|WzN0Vuc7tF>^O&9fEz|16lEU$|m-?ak6(ySG> ztie*buj-9*+2Li~lb+=qX7`NZ-B^>9SnqF%+sEoGrY(<8w(F|r9b)s_#jj3h^CQEK zM(3|Ru09%%rK4^ddRKt(x5cKu(K|;VE+vK*tGMcmQ>><<8;sw^0b^7a8S^?C2!wu# zaXSW30RlXp4)(rU{f@JIM3>|;XfAinm|3U4`gb^t(Bo0!&Y(Aq9M6!*hI7bGt|Jrs zrvf#mo@@s-2!fTqiW{DY3xg!RHkGA#zIR=iLwagoei~g4qWeVn5WFeA{dDcvl9qPO z!G7cJVjRc!^=h?EjuqX~(k(=4by&Po*VDCiInSCuQSi8^l_nJbMJr^GqSUPNT-Z86=j9Ze1LYIkUun%bx zpm)(j`2EuOM#D*Hc&`M{^9!EX8f+3LOkCR5G#li|rP!P26Lv|>F58H%e;D;=sGM|1 zZ2|nw1r?>jYm9FS?7ZiOBi}F~?=YJokVtcj1aVj_YGFg^Sx-6oi9qT_+TsWIp$ z@K&V{n99v@CgAFS07k`T#hXish725X0i}<4r#s_h)#;aE*8B7Y0~QQ=yBxMMBOJX- z?~fu51!e+Q4xh0h1|vL$Gz9Xp2GMP31LB?eWy3Ew5_&sl5x~AqgYRr~cz{Wa5`#-J z+s;)<6ZXQreY291lj=xE(&mSvTsW&>yDPEQd`aKabKc+60U$O)oAkl}fyQV-AcEVW z+0&la)y>P^%Jq-DKp8kW&hU}7JdeZRlAeJn`&TmQq*SWJ&k$a`dWVtlQPlM#&o8`- zTJNnp4TB!hXVU@Fo0GCw+-@@L)fvd$tAz}fXg>Dwz|kVH+`b~g^u(t?srEh(PT%@R z>l~j4cqw*(B8Md;F1^n{ogy|i2QwFj-K6>uPoT{O5b7z~V%EwPA=fDl_3I{t1Zn@= z?yGaU|4TPkV>fg!Q;O}Q3X7n(V%KRK&k59=qc$e}yY||*-L~gmcAX|*j06JJE*BZA98PTAFWZCF*R0I}SuZcGH9O?eK&o6E_!Bq?li*Lvkd+g$0NOGF_}7@;-IK)UQ)GN|>hY z@7b{8;Y6uFw|pDJCqOF3&yqmBnUY?(mRYuv$1H+Ql-&L;pd>1a`|*J3QyV3$Cf8nX zC4z=C07cht+xB@&r@q>Ih@PNJxAaNG0f5@p$m?F7tFYYBc4SrSneDIj$T6559{j0r zSl!?{C@q4i)pthT>Z3ASD*bf4&?dDn8uu5RUg%MOrXy=gX2=&(zRLI3Unb3$A1=Pd zY9QWtnrqTCcxl8+))XgXRh2xs9svqnXdhA30QNe)hpfFIC5J{dv;;kx!>Rbaj#ZD> zcJkf^JV!QUYJOQi?|Y*dVP{yBH+gr1Y?U^hadoG{C3JJUl9EBV&*ouI-J6b&U%WM) zcu!dFUR{1#8<81#qcD%LreETwR<651FT^}TEdO(h-0BPPt%yy6{cYoYH?CCP8%x3z zVOO9eaemEGuZX^}facunM-+#Xm?u2Kx%0qU*hW$I(3_wnRL`<)ovnEx<6E4zu}N9* z(-t~FIL)hZ#vRxBrAO7W+8#}7^JgKt#_G<4;0Ylr5v}>}(UacZ-u`VSy&+J{G+5cQ&PSst3~?KmpkEL$4|uFJN7R; zq%r!@b-_@i1NN)F64KBZ9T<%V)^e<97mIr24s{eWi#V~*+!>-YmJjx#2Gi2<&K`$T ztP#5210Dn=2wU0WrUq|EB03P|%3*ucO?&k_b;BzOSAahjIUoGc+FNfaTD zZ=780ES#L|{wUh2*kQXdX|nE{cu}rj6P7^~#=`iWSrpIi8@*l29k4b{kzbYf6r3$- ziqw>OOSF;6P9G8Z$#h2PhF)YRW08eFWl4rNoeg;sy6dwb;v_2Q#>c8d91ebdYHc8ASUSYfdX?m$_6hZ9DZg>d_Y7GNZd~C9iG> zU2yW%e3|9zcN7oVN1iC@UvURu;XUS%xbva>9hR_2MOf(Glx;Uk_c^35_$amOUQGM1 z=mTA>y1|FlrJ|BveNPnHCYYU)m<;rg1;ACEjRYo8r=exDlkFx=Gl#$s7)3wwvs%u_ z0lyd%tF$3q^1o()GWwNbW+A}AFp12St4o{n54@( zy|3|J)_(`o>7L-=k1jCANbg;dx{-gg@L}wgyK=$&LOoKNLP6_l|2XqqlhYM`3>2NS_Ul)DBEb>amRb=K)8vIGLrD1m2tw1rPyQcgv-wa2WA-h9&SM>TN-J=}}aDq|Af zd{}U_pm{vEFP!oN|3hn54}Et>x(*Iq_TyZD?R`;DS+m9gsf-IMCMYVr_6KHWDam=y zu)o&)CYq)IxusFsnjCmYGfhW99-lp*we%O|#!Q@gh|6y6GYjQUoc5p|1F;C6yxtDd z(0y-_RH+_iG%?py*=HVHi0#@6R$q5|100g%^#`W!xSm}2d}}a~IZ_giK6#Miv4=6v zTRxDnuNTdGE^_?kyjybNsqM}A=aVA0nNvYCg5BN~QgtOXbTZIiJ8if*8TemIZFnx3 zFvcw}z!%79&==8S(aGUN