Explorar o código

搞定搞定 参数告警

wukai hai 6 días
pai
achega
e0a6a164f2

+ 50 - 0
jjt-admin/src/test/java/com/jjt/dye/DyeProcAlarmTest.java

@@ -0,0 +1,50 @@
+package com.jjt.dye;
+
+import com.jjt.JjtApplication;
+import com.jjt.dye.service.IDyeProcAlarmService;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+import javax.annotation.Resource;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.LocalTime;
+
+/**
+ * EnergyTest$
+ *
+ * @author wukai
+ * @date 2025/10/19 03:38
+ */
+@SpringBootTest(classes = JjtApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
+//@ActiveProfiles("devp")
+public class DyeProcAlarmTest {
+    String st = "2025-12-14";
+    String ed = "2025-12-19";
+    LocalDate localDate = LocalDate.parse(st);
+    LocalDate endDate = LocalDate.parse(ed);
+    LocalDateTime start = LocalDateTime.of(localDate, LocalTime.MIN).plusHours(0);
+    @Resource
+    private IDyeProcAlarmService service;
+
+    @Test
+    public void calc() {
+        LocalDateTime end = LocalDateTime.of(endDate.plusDays(1), LocalTime.MIN).plusHours(23);
+        LocalDateTime curr = LocalDateTime.now();
+        if (end.isAfter(curr)) {
+            end = curr.minusHours(1);
+        }
+        do {
+            System.err.println(start);
+            service.calc(start, start.plusHours(1));
+            start = start.plusHours(1);
+        } while (!start.isAfter(end));
+    }
+
+    @Test
+    public void calc1() {
+        LocalDateTime start = LocalDateTime.parse("2025-12-19T22:00:00");
+        System.err.println(start);
+        service.calc(start, start.plusHours(1));
+    }
+}

+ 100 - 0
jjt-biz/src/main/java/com/jjt/dye/controller/DyeProcAlarmController.java

@@ -0,0 +1,100 @@
+package com.jjt.dye.controller;
+
+import com.jjt.common.annotation.Log;
+import com.jjt.common.core.controller.BaseController;
+import com.jjt.common.core.domain.AjaxResult;
+import com.jjt.common.core.page.TableDataInfo;
+import com.jjt.common.enums.BusinessType;
+import com.jjt.common.utils.poi.ExcelUtil;
+import com.jjt.dye.domain.DyeProcAlarm;
+import com.jjt.dye.service.IDyeProcAlarmService;
+import io.swagger.annotations.Api;
+import io.swagger.annotations.ApiOperation;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.Resource;
+import javax.servlet.http.HttpServletResponse;
+import java.util.List;
+
+/**
+ * 工艺参数告警信息Controller
+ *
+ * @author wukai
+ * @date 2025-12-19
+ */
+@Api(tags = "工艺参数告警信息")
+@RestController
+@RequestMapping("/dye/alarm")
+public class DyeProcAlarmController extends BaseController {
+    @Resource
+    private IDyeProcAlarmService dyeProcAlarmService;
+
+    /**
+     * 查询工艺参数告警信息列表
+     */
+    @ApiOperation("查询工艺参数告警信息列表")
+    @PreAuthorize("@ss.hasPermi('dye:alarm:list')")
+    @GetMapping("/list")
+    public TableDataInfo list(DyeProcAlarm dyeProcAlarm) {
+        startPage();
+        List<DyeProcAlarm> list = dyeProcAlarmService.selectDyeProcAlarmList(dyeProcAlarm);
+        return getDataTable(list);
+    }
+
+    /**
+     * 导出工艺参数告警信息列表
+     */
+    @ApiOperation("导出工艺参数告警信息列表")
+    @PreAuthorize("@ss.hasPermi('dye:alarm:export')")
+    @Log(title = "工艺参数告警信息", businessType = BusinessType.EXPORT)
+    @PostMapping("/export")
+    public void export(HttpServletResponse response, DyeProcAlarm dyeProcAlarm) {
+        List<DyeProcAlarm> list = dyeProcAlarmService.selectDyeProcAlarmList(dyeProcAlarm);
+        ExcelUtil<DyeProcAlarm> util = new ExcelUtil<DyeProcAlarm>(DyeProcAlarm.class);
+        util.exportExcel(response, list, "工艺参数告警信息数据");
+    }
+
+    /**
+     * 获取工艺参数告警信息详细信息
+     */
+    @ApiOperation("获取工艺参数告警信息详细信息")
+    @PreAuthorize("@ss.hasPermi('dye:alarm:query')")
+    @GetMapping(value = "/{alarmId}")
+    public AjaxResult getInfo(@PathVariable("alarmId") Long alarmId) {
+        return success(dyeProcAlarmService.selectDyeProcAlarmByAlarmId(alarmId));
+    }
+
+    /**
+     * 新增工艺参数告警信息
+     */
+    @ApiOperation("新增工艺参数告警信息")
+    @PreAuthorize("@ss.hasPermi('dye:alarm:add')")
+    @Log(title = "工艺参数告警信息", businessType = BusinessType.INSERT)
+    @PostMapping
+    public AjaxResult add(@RequestBody DyeProcAlarm dyeProcAlarm) {
+        return toAjax(dyeProcAlarmService.insertDyeProcAlarm(dyeProcAlarm));
+    }
+
+    /**
+     * 修改工艺参数告警信息
+     */
+    @ApiOperation("修改工艺参数告警信息")
+    @PreAuthorize("@ss.hasPermi('dye:alarm:edit')")
+    @Log(title = "工艺参数告警信息", businessType = BusinessType.UPDATE)
+    @PutMapping
+    public AjaxResult edit(@RequestBody DyeProcAlarm dyeProcAlarm) {
+        return toAjax(dyeProcAlarmService.updateDyeProcAlarm(dyeProcAlarm));
+    }
+
+    /**
+     * 删除工艺参数告警信息
+     */
+    @ApiOperation("删除工艺参数告警信息")
+    @PreAuthorize("@ss.hasPermi('dye:alarm:remove')")
+    @Log(title = "工艺参数告警信息", businessType = BusinessType.DELETE)
+    @DeleteMapping("/{alarmIds}")
+    public AjaxResult remove(@PathVariable Long[] alarmIds) {
+        return toAjax(dyeProcAlarmService.deleteDyeProcAlarmByAlarmIds(alarmIds));
+    }
+}

+ 193 - 3
jjt-biz/src/main/java/com/jjt/dye/controller/DyeTypeProcessController.java

@@ -7,8 +7,10 @@ import com.jjt.common.core.page.TableDataInfo;
 import com.jjt.common.enums.BusinessType;
 import com.jjt.common.utils.DateUtils;
 import com.jjt.dye.domain.DyeCalcHour;
+import com.jjt.dye.domain.DyeProcAlarm;
 import com.jjt.dye.domain.DyeTypeProcess;
 import com.jjt.dye.service.IDyeCalcHourService;
+import com.jjt.dye.service.IDyeProcAlarmService;
 import com.jjt.dye.service.IDyeTypeProcessService;
 import com.jjt.dye.vo.DyePowerVO;
 import com.jjt.dye.vo.DyeProcessVO;
@@ -18,6 +20,7 @@ import io.swagger.annotations.ApiOperation;
 import org.apache.poi.ss.usermodel.*;
 import org.apache.poi.ss.util.CellRangeAddress;
 import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import javax.annotation.Resource;
@@ -47,6 +50,8 @@ public class DyeTypeProcessController extends BaseController {
     private IDyeTypeProcessService dyeTypeProcessService;
     @Resource
     private IDyeCalcHourService dyeCalcHourService;
+    @Resource
+    private IDyeProcAlarmService alarmService;
 
     /**
      * 查询染整线设备类型工艺参数列表
@@ -243,11 +248,11 @@ public class DyeTypeProcessController extends BaseController {
 
                                 // 烫棍1电流(A)
                                 createDataRow(sheet, rowNum.getAndIncrement(), deviceName, "烫棍1电流(A)",
-                                        calenderingMachineParams.getRoller1CurrSet() * 0.1, calenderingMachineParams.getRoller1CurrActual());
+                                        calenderingMachineParams.getRoller1CurrSet(), calenderingMachineParams.getRoller1CurrActual());
 
                                 // 烫棍2电流(A)
                                 createDataRow(sheet, rowNum.getAndIncrement(), deviceName, "烫棍2电流(A)",
-                                        calenderingMachineParams.getRoller2CurrSet() * 0.1, calenderingMachineParams.getRoller2CurrActual());
+                                        calenderingMachineParams.getRoller2CurrSet(), calenderingMachineParams.getRoller2CurrActual());
 
                                 // 走布速度(m/min)
                                 createDataRow(sheet, rowNum.getAndIncrement(), deviceName, "走布速度(m/min)",
@@ -492,6 +497,191 @@ public class DyeTypeProcessController extends BaseController {
         }
     }
 
+    @ApiOperation("导出工艺")
+    //@PreAuthorize("@ss.hasPermi('dye:process:export')")
+    @PostMapping("/exportAlarm")
+    public void exportAlarm(HttpServletResponse response, ProcExpVO vo) {
+        String[] dateRange = vo.getDate().split(",");
+        LocalDate start = LocalDate.parse(dateRange[0]);
+        LocalDate end = LocalDate.parse(dateRange[1]);
+        List<DyeProcAlarm> list = alarmService.searchAlarm(start, end, vo.getTypes(), vo.getLines());
+        Map<String, List<DyeProcAlarm>> alarmVOsByLine = list.stream().collect(Collectors.groupingBy(DyeProcAlarm::getLine));
+        try (InputStream inputStream = getClass().getClassLoader().getResourceAsStream("tpl/qz-alarm.xlsx"); XSSFWorkbook wb = new XSSFWorkbook(inputStream); OutputStream outputStream = new BufferedOutputStream(response.getOutputStream())) {
+            alarmVOsByLine.forEach((line, vos) -> {
+                Sheet sheet = wb.cloneSheet(0);
+                String sheetName = line + "#产线";
+                wb.setSheetName(wb.getSheetIndex(sheet), sheetName);
+
+                Cell title = sheet.getRow(0).getCell(0);
+                title.setCellValue("前整车间" + sheetName + "工艺参数告警记录(" + start + "至" + end + ")");
+                //机台号:deviceName	说明:paraName 设定值:setValue	有效值范围	实际值	告警开始时间	告警结束时间
+
+                // 创建带边框的单元格样式
+                CellStyle borderStyle = wb.createCellStyle();
+                borderStyle.setBorderTop(BorderStyle.THIN);
+                borderStyle.setBorderBottom(BorderStyle.THIN);
+                borderStyle.setBorderLeft(BorderStyle.THIN);
+                borderStyle.setBorderRight(BorderStyle.THIN);
+                borderStyle.setAlignment(HorizontalAlignment.CENTER);
+                borderStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+                
+                // 创建时间格式的单元格样式
+                CellStyle dateStyle = wb.createCellStyle();
+                dateStyle.setBorderTop(BorderStyle.THIN);
+                dateStyle.setBorderBottom(BorderStyle.THIN);
+                dateStyle.setBorderLeft(BorderStyle.THIN);
+                dateStyle.setBorderRight(BorderStyle.THIN);
+                dateStyle.setAlignment(HorizontalAlignment.CENTER);
+                dateStyle.setVerticalAlignment(VerticalAlignment.CENTER);
+                CreationHelper createHelper = wb.getCreationHelper();
+                dateStyle.setDataFormat(createHelper.createDataFormat().getFormat("yyyy-mm-dd hh:mm:ss"));
+                
+                // 按照deviceName中的数字排序,然后按参数名称排序
+                List<DyeProcAlarm> sortedVos = vos.stream()
+                        .sorted(Comparator.comparing((DyeProcAlarm alarm) -> {
+                            String deviceName = alarm.getDeviceName();
+                            if (deviceName != null && deviceName.contains("#")) {
+                                try {
+                                    // 提取#前面的数字
+                                    String numStr = deviceName.substring(0, deviceName.indexOf("#"));
+                                    return Integer.parseInt(numStr);
+                                } catch (NumberFormatException e) {
+                                    // 如果解析失败,返回一个较大的数,使其排在后面
+                                    return Integer.MAX_VALUE;
+                                }
+                            }
+                            return Integer.MAX_VALUE;
+                        }).thenComparing(DyeProcAlarm::getParaName))
+                        .collect(Collectors.toList());
+
+                // 从第2行开始填充数据
+                AtomicInteger rowNum = new AtomicInteger(2);
+                // 存储所有行的信息,用于后续合并处理
+                List<Map<String, Object>> rowInfos = new ArrayList<>();
+                
+                for (int i = 0; i < sortedVos.size(); i++) {
+                    DyeProcAlarm alarm = sortedVos.get(i);
+                    Row row = sheet.createRow(rowNum.get());
+                    
+                    String deviceKey = alarm.getDeviceName() + "_" + alarm.getParaName();
+                    
+                    // 第一列:机台号(设备名称)
+                    Cell deviceCell = row.createCell(0);
+                    deviceCell.setCellValue(alarm.getDeviceName() != null ? alarm.getDeviceName() : "");
+                    deviceCell.setCellStyle(borderStyle);
+                    
+                    // 第二列:说明(参数名称)
+                    Cell paraCell = row.createCell(1);
+                    paraCell.setCellValue(alarm.getParaName() != null ? alarm.getParaName() : "");
+                    paraCell.setCellStyle(borderStyle);
+                    
+                    // 第三列:设定值
+                    Cell setCell = row.createCell(2);
+                    if (alarm.getSetValue() != null) {
+                        setCell.setCellValue(alarm.getSetValue().doubleValue());
+                    }
+                    setCell.setCellStyle(borderStyle);
+                    
+                    // 第四列:有效值范围
+                    Cell validRangeCell = row.createCell(3);
+                    validRangeCell.setCellValue(alarm.getValidRange() != null ? alarm.getValidRange() : "");
+                    validRangeCell.setCellStyle(borderStyle);
+                    
+                    // 第五列:实际值范围
+                    Cell actValueRangeCell = row.createCell(4);
+                    actValueRangeCell.setCellValue(alarm.getActValueRange() != null ? alarm.getActValueRange() : "");
+                    actValueRangeCell.setCellStyle(borderStyle);
+                    
+                    // 第六列:告警开始时间
+                    Cell startTimeCell = row.createCell(5);
+                    if (alarm.getStartTime() != null) {
+                        startTimeCell.setCellValue(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", alarm.getStartTime()));
+                        startTimeCell.setCellStyle(dateStyle);
+                    } else {
+                        startTimeCell.setCellStyle(borderStyle);
+                    }
+                    
+                    // 第七列:告警结束时间
+                    Cell endTimeCell = row.createCell(6);
+                    if (alarm.getEndTime() != null) {
+                        endTimeCell.setCellValue(DateUtils.parseDateToStr("yyyy-MM-dd HH:mm:ss", alarm.getEndTime()));
+                        endTimeCell.setCellStyle(dateStyle);
+                    } else {
+                        endTimeCell.setCellStyle(borderStyle);
+                    }
+                    
+                    // 记录行信息
+                    Map<String, Object> rowInfo = new HashMap<>();
+                    rowInfo.put("rowIndex", rowNum.get());
+                    rowInfo.put("deviceKey", deviceKey);
+                    rowInfos.add(rowInfo);
+                    
+                    // 增加行号
+                    rowNum.incrementAndGet();
+                }
+                
+                // 执行单元格合并 - 先按机台号合并,再在机台号内部按参数合并
+                if (!rowInfos.isEmpty()) {
+                    // 按机台号分组
+                    Map<String, List<Integer>> deviceGroups = new LinkedHashMap<>();
+                    for (int i = 0; i < sortedVos.size(); i++) {
+                        String deviceName = sortedVos.get(i).getDeviceName();
+                        int rowIdx = 2 + i; // 行号从2开始
+                        deviceGroups.computeIfAbsent(deviceName, k -> new ArrayList<>()).add(rowIdx);
+                    }
+                    
+                    // 合并机台号列
+                    for (List<Integer> rows : deviceGroups.values()) {
+                        if (rows.size() > 1) {
+                            int startRow = rows.get(0);
+                            int endRow = rows.get(rows.size() - 1);
+                            sheet.addMergedRegion(new CellRangeAddress(startRow, endRow, 0, 0));
+                        }
+                    }
+                    
+                    // 在每个机台号组内,按参数分组并合并参数列
+                    for (Map.Entry<String, List<Integer>> entry : deviceGroups.entrySet()) {
+                        List<Integer> deviceRows = entry.getValue();
+                        if (deviceRows.size() > 1) {
+                            // 获取这些行对应的参数
+                            Map<String, List<Integer>> paramGroups = new LinkedHashMap<>();
+                            for (int rowIdx : deviceRows) {
+                                int listIdx = rowIdx - 2; // 转换回列表索引
+                                if (listIdx < sortedVos.size()) {
+                                    String paramName = sortedVos.get(listIdx).getParaName();
+                                    paramGroups.computeIfAbsent(paramName, k -> new ArrayList<>()).add(rowIdx);
+                                }
+                            }
+                            
+                            // 合并参数列
+                            for (List<Integer> paramRows : paramGroups.values()) {
+                                if (paramRows.size() > 1) {
+                                    int startRow = paramRows.get(0);
+                                    int endRow = paramRows.get(paramRows.size() - 1);
+                                    sheet.addMergedRegion(new CellRangeAddress(startRow, endRow, 1, 1));
+                                }
+                            }
+                        }
+                    }
+                }
+            });
+            wb.removeSheetAt(0);
+            // 清空response
+            response.reset();
+            // 设置response的Header
+            response.setCharacterEncoding("UTF-8");
+            //Content-Disposition的作用:告知浏览器以何种方式显示响应返回的文件,用浏览器打开还是以附件的形式下载到本地保存
+            //attachment表示以附件方式下载 inline表示在线打开 "Content-Disposition: inline; filename=文件名.mp3"
+            // filename表示文件的默认名称,因为网络传输只支持URL编码的相关支付,因此需要将文件名URL编码后进行传输,前端收到后需要反编码才能获取到真正的名称
+            response.addHeader("Content-Disposition", "attachment;filename=" + URLEncoder.encode("前整工艺参数告警记录.xlsx", "UTF-8"));
+            response.setContentType("application/octet-stream");
+            wb.write(outputStream);
+            outputStream.flush();
+        } catch (IOException ex) {
+            ex.printStackTrace();
+        }
+    }
+
     /**
      * 创建数据行
      *
@@ -628,7 +818,7 @@ public class DyeTypeProcessController extends BaseController {
      * 删除染整线设备类型工艺参数
      */
     @ApiOperation("删除染整线设备类型工艺参数")
-    //@PreAuthorize("@ss.hasPermi('dye:process:remove')")
+    @PreAuthorize("@ss.hasPermi('dye:process:remove')")
     @Log(title = "染整线设备类型工艺参数", businessType = BusinessType.DELETE)
     @DeleteMapping("/{tpIds}")
     public AjaxResult remove(@PathVariable Long[] tpIds) {

+ 164 - 0
jjt-biz/src/main/java/com/jjt/dye/domain/DyeProcAlarm.java

@@ -0,0 +1,164 @@
+package com.jjt.dye.domain;
+
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import com.jjt.common.annotation.Excel;
+import com.jjt.common.core.domain.BaseEntity;
+import io.swagger.annotations.ApiModel;
+import io.swagger.annotations.ApiModelProperty;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.util.Date;
+
+/**
+ * 工艺参数告警信息对象 DYE_PROC_ALARM
+ *
+ * @author wukai
+ * @date 2025-12-19
+ */
+@ApiModel(value = "DyeProcAlarm", description = "工艺参数告警信息")
+@Data
+public class DyeProcAlarm extends BaseEntity implements Cloneable {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * 统计ID
+     */
+    @ApiModelProperty("统计ID")
+    @TableId
+    private Long alarmId;
+
+    /**
+     * 告警时间
+     */
+    @ApiModelProperty("告警时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "告警时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date startTime;
+
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private Date workDay;
+    /**
+     * 结束时间
+     */
+    @ApiModelProperty("结束时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "结束时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date endTime;
+
+    /**
+     * 设备类型
+     */
+    @ApiModelProperty("设备类型")
+    @Excel(name = "设备类型")
+    private Long typeId;
+
+    /**
+     * 类型名称
+     */
+    @ApiModelProperty("类型名称")
+    @Excel(name = "类型名称")
+    private String typeName;
+
+    @ApiModelProperty("产线")
+    private String line;
+
+    /**
+     * 设备ID
+     */
+    @ApiModelProperty("设备ID")
+    @Excel(name = "设备ID")
+    private Long deviceId;
+
+    /**
+     * 设备名称
+     */
+    @ApiModelProperty("设备名称")
+    @Excel(name = "设备名称")
+    private String deviceName;
+
+    /**
+     * 参数编码
+     */
+    @ApiModelProperty("参数编码")
+    @Excel(name = "参数编码")
+    private String paraCode;
+
+    /**
+     * 参数名称
+     */
+    @ApiModelProperty("参数名称")
+    @Excel(name = "参数名称")
+    private String paraName;
+
+    /**
+     * 有效值范围
+     */
+    @ApiModelProperty("有效值范围")
+    @Excel(name = "有效值范围")
+    private String validRange;
+
+    /**
+     * 设定值
+     */
+    @ApiModelProperty("设定值")
+    @Excel(name = "设定值")
+    private BigDecimal setValue;
+
+    /**
+     * 实际值
+     */
+    @ApiModelProperty("实际值")
+    @Excel(name = "实际值")
+    private BigDecimal actValue;
+
+    /**
+     * 实际值范围
+     */
+    @ApiModelProperty("实际值范围")
+    @Excel(name = "实际值范围")
+    private String actValueRange;
+
+    /**
+     * 创建人
+     */
+    @ApiModelProperty("创建人")
+    @Excel(name = "创建人")
+    private String createdBy;
+
+    /**
+     * 创建时间
+     */
+    @ApiModelProperty("创建时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date createdTime;
+
+    /**
+     * 更新人
+     */
+    @ApiModelProperty("更新人")
+    @Excel(name = "更新人")
+    private String updatedBy;
+
+    /**
+     * 更新时间
+     */
+    @ApiModelProperty("更新时间")
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @Excel(name = "更新时间", width = 30, dateFormat = "yyyy-MM-dd")
+    private Date updatedTime;
+
+    private Boolean inRange;
+
+    @Override
+    public DyeProcAlarm clone() {
+        try {
+            return (DyeProcAlarm) super.clone();
+        } catch (CloneNotSupportedException e) {
+            // This should never happen since we implement Cloneable
+            throw new RuntimeException("Failed to clone DyeProcAlarm", e);
+        }
+    }
+}

+ 57 - 30
jjt-biz/src/main/java/com/jjt/dye/domain/DyeTypeProcess.java

@@ -1,83 +1,110 @@
 package com.jjt.dye.domain;
 
-import java.math.BigDecimal;
-import java.util.Date;
-import com.fasterxml.jackson.annotation.JsonFormat;
 import com.baomidou.mybatisplus.annotation.TableId;
+import com.jjt.common.annotation.Excel;
+import com.jjt.common.core.domain.BaseEntity;
 import io.swagger.annotations.ApiModel;
 import io.swagger.annotations.ApiModelProperty;
 import lombok.Data;
-import org.apache.commons.lang3.builder.ToStringBuilder;
-import org.apache.commons.lang3.builder.ToStringStyle;
-import com.jjt.common.annotation.Excel;
-import com.jjt.common.core.domain.BaseEntity;
+
+import java.math.BigDecimal;
+import java.util.Date;
 
 /**
  * 染整线设备类型工艺参数对象 DYE_TYPE_PROCESS
  *
  * @author wukai
- * @date 2025-12-13
+ * @date 2025-12-17
  */
 @ApiModel(value = "DyeTypeProcess", description = "染整线设备类型工艺参数")
 @Data
-public class DyeTypeProcess extends BaseEntity{
+public class DyeTypeProcess extends BaseEntity {
     private static final long serialVersionUID = 1L;
 
-    /** 参数ID */
+    /**
+     * 参数ID
+     */
     @ApiModelProperty("参数ID")
     @TableId
     private Long tpId;
 
-    /** ID */
+    /**
+     * ID
+     */
     @ApiModelProperty("ID")
     @Excel(name = "ID")
     private Long typeId;
 
-    /** 参数编码 */
+    /**
+     * 参数编码
+     */
     @ApiModelProperty("参数编码")
     @Excel(name = "参数编码")
     private String paraCode;
 
-    /** 参数名称 */
+    /**
+     * 参数名称
+     */
     @ApiModelProperty("参数名称")
     @Excel(name = "参数名称")
     private String paraName;
 
-    /** 设定值表达式 */
+    /**
+     * 设定值表达式
+     */
     @ApiModelProperty("设定值表达式")
     @Excel(name = "设定值表达式")
     private String paraSet;
 
-    /** 实际值表达式 */
+    /**
+     * 设定值系数
+     */
+    @ApiModelProperty("设定值系数")
+    @Excel(name = "设定值系数")
+    private BigDecimal coeffSet;
+
+    /**
+     * 实际值表达式
+     */
     @ApiModelProperty("实际值表达式")
     @Excel(name = "实际值表达式")
     private String paraAct;
 
-    /** 实际值表达式 */
-    @ApiModelProperty("系数")
-    @Excel(name = "系数")
-    private BigDecimal coeff;
+    /**
+     * 实际值系数
+     */
+    @ApiModelProperty("实际值系数")
+    @Excel(name = "实际值系数")
+    private BigDecimal coeffAct;
+
+    /**
+     * 有效值范围
+     */
+    @ApiModelProperty("有效值范围")
+    @Excel(name = "有效值范围")
+    private String validRange;
 
-    /** 创建人 */
+    /**
+     * 创建人
+     */
     @ApiModelProperty("创建人")
-    @Excel(name = "创建人")
     private String createdBy;
 
-    /** 创建时间 */
+    /**
+     * 创建时间
+     */
     @ApiModelProperty("创建时间")
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
-    @Excel(name = "创建时间", width = 30, dateFormat = "yyyy-MM-dd")
     private Date createdTime;
 
-    /** 更新人 */
+    /**
+     * 更新人
+     */
     @ApiModelProperty("更新人")
-    @Excel(name = "更新人")
     private String updatedBy;
 
-    /** 更新时间 */
+    /**
+     * 更新时间
+     */
     @ApiModelProperty("更新时间")
-    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
-    @Excel(name = "更新时间", width = 30, dateFormat = "yyyy-MM-dd")
     private Date updatedTime;
-
 }

+ 62 - 0
jjt-biz/src/main/java/com/jjt/dye/mapper/DyeProcAlarmMapper.java

@@ -0,0 +1,62 @@
+package com.jjt.dye.mapper;
+
+import java.util.List;
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.jjt.dye.domain.DyeProcAlarm;
+
+/**
+ * 工艺参数告警信息Mapper接口
+ * 
+ * @author wukai
+ * @date 2025-12-19
+ */
+public interface DyeProcAlarmMapper extends BaseMapper<DyeProcAlarm>
+{
+    /**
+     * 查询工艺参数告警信息
+     * 
+     * @param alarmId 工艺参数告警信息主键
+     * @return 工艺参数告警信息
+     */
+    public DyeProcAlarm selectDyeProcAlarmByAlarmId(Long alarmId);
+
+    /**
+     * 查询工艺参数告警信息列表
+     * 
+     * @param dyeProcAlarm 工艺参数告警信息
+     * @return 工艺参数告警信息集合
+     */
+    public List<DyeProcAlarm> selectDyeProcAlarmList(DyeProcAlarm dyeProcAlarm);
+
+    /**
+     * 新增工艺参数告警信息
+     * 
+     * @param dyeProcAlarm 工艺参数告警信息
+     * @return 结果
+     */
+    public int insertDyeProcAlarm(DyeProcAlarm dyeProcAlarm);
+
+    /**
+     * 修改工艺参数告警信息
+     * 
+     * @param dyeProcAlarm 工艺参数告警信息
+     * @return 结果
+     */
+    public int updateDyeProcAlarm(DyeProcAlarm dyeProcAlarm);
+
+    /**
+     * 删除工艺参数告警信息
+     * 
+     * @param alarmId 工艺参数告警信息主键
+     * @return 结果
+     */
+    public int deleteDyeProcAlarmByAlarmId(Long alarmId);
+
+    /**
+     * 批量删除工艺参数告警信息
+     * 
+     * @param alarmIds 需要删除的数据主键集合
+     * @return 结果
+     */
+    public int deleteDyeProcAlarmByAlarmIds(Long[] alarmIds);
+}

+ 87 - 0
jjt-biz/src/main/java/com/jjt/dye/service/IDyeProcAlarmService.java

@@ -0,0 +1,87 @@
+package com.jjt.dye.service;
+
+import com.jjt.dye.domain.DyeProcAlarm;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.List;
+
+/**
+ * 工艺参数告警信息Service接口
+ *
+ * @author wukai
+ * @date 2025-12-19
+ */
+public interface IDyeProcAlarmService {
+    /**
+     * 查询工艺参数告警信息
+     *
+     * @param alarmId 工艺参数告警信息主键
+     * @return 工艺参数告警信息
+     */
+    public DyeProcAlarm selectDyeProcAlarmByAlarmId(Long alarmId);
+
+    /**
+     * 查询工艺参数告警信息列表
+     *
+     * @param dyeProcAlarm 工艺参数告警信息
+     * @return 工艺参数告警信息集合
+     */
+    public List<DyeProcAlarm> selectDyeProcAlarmList(DyeProcAlarm dyeProcAlarm);
+
+    /**
+     * 新增工艺参数告警信息
+     *
+     * @param dyeProcAlarm 工艺参数告警信息
+     * @return 结果
+     */
+    public int insertDyeProcAlarm(DyeProcAlarm dyeProcAlarm);
+
+    /**
+     * 修改工艺参数告警信息
+     *
+     * @param dyeProcAlarm 工艺参数告警信息
+     * @return 结果
+     */
+    public int updateDyeProcAlarm(DyeProcAlarm dyeProcAlarm);
+
+    /**
+     * 批量删除工艺参数告警信息
+     *
+     * @param alarmIds 需要删除的工艺参数告警信息主键集合
+     * @return 结果
+     */
+    public int deleteDyeProcAlarmByAlarmIds(Long[] alarmIds);
+
+    /**
+     * 删除工艺参数告警信息信息
+     *
+     * @param alarmId 工艺参数告警信息主键
+     * @return 结果
+     */
+    public int deleteDyeProcAlarmByAlarmId(Long alarmId);
+
+    /**
+     * 统计上一时段数据
+     */
+    void last();
+
+    /**
+     * 按指定时间统计
+     *
+     * @param start 开始时间
+     * @param end   结束时间
+     */
+    void calc(LocalDateTime start, LocalDateTime end);
+
+    /**
+     * 查询指定时间段内指定类型指定线别告警信息
+     *
+     * @param start 开始时间
+     * @param end   结束时间
+     * @param types 类型
+     * @param lines 线别
+     * @return 告警信息
+     */
+    List<DyeProcAlarm> searchAlarm(LocalDate start, LocalDate end, String[] types, String[] lines);
+}

+ 460 - 0
jjt-biz/src/main/java/com/jjt/dye/service/impl/DyeProcAlarmServiceImpl.java

@@ -0,0 +1,460 @@
+package com.jjt.dye.service.impl;
+
+import cn.hutool.json.JSONArray;
+import cn.hutool.json.JSONObject;
+import com.jjt.common.utils.DateUtils;
+import com.jjt.common.utils.StringUtils;
+import com.jjt.dye.domain.DyeProcAlarm;
+import com.jjt.dye.domain.DyeTypeProcess;
+import com.jjt.dye.mapper.DyeProcAlarmMapper;
+import com.jjt.dye.service.IDyeDeviceService;
+import com.jjt.dye.service.IDyeProcAlarmService;
+import com.jjt.dye.vo.DyeDeviceVO;
+import com.jjt.utils.IotService;
+import com.jjt.utils.Tools;
+import org.apache.ibatis.session.ExecutorType;
+import org.apache.ibatis.session.SqlSession;
+import org.apache.ibatis.session.SqlSessionFactory;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.Resource;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 工艺参数告警信息Service业务层处理
+ *
+ * @author wukai
+ * @date 2025-12-19
+ */
+@Service
+public class DyeProcAlarmServiceImpl implements IDyeProcAlarmService {
+    @Resource
+    private DyeProcAlarmMapper dyeProcAlarmMapper;
+    @Resource
+    private IotService iotService;
+    @Resource
+    private SqlSessionFactory factory;
+    @Resource
+    private IDyeDeviceService dyeDeviceService;
+
+    /**
+     * 查询工艺参数告警信息
+     *
+     * @param alarmId 工艺参数告警信息主键
+     * @return 工艺参数告警信息
+     */
+    @Override
+    public DyeProcAlarm selectDyeProcAlarmByAlarmId(Long alarmId) {
+        return dyeProcAlarmMapper.selectDyeProcAlarmByAlarmId(alarmId);
+    }
+
+    /**
+     * 查询工艺参数告警信息列表
+     *
+     * @param dyeProcAlarm 工艺参数告警信息
+     * @return 工艺参数告警信息
+     */
+    @Override
+    public List<DyeProcAlarm> selectDyeProcAlarmList(DyeProcAlarm dyeProcAlarm) {
+        return dyeProcAlarmMapper.selectDyeProcAlarmList(dyeProcAlarm);
+    }
+
+    /**
+     * 新增工艺参数告警信息
+     *
+     * @param dyeProcAlarm 工艺参数告警信息
+     * @return 结果
+     */
+    @Override
+    public int insertDyeProcAlarm(DyeProcAlarm dyeProcAlarm) {
+        return dyeProcAlarmMapper.insertDyeProcAlarm(dyeProcAlarm);
+    }
+
+    /**
+     * 修改工艺参数告警信息
+     *
+     * @param dyeProcAlarm 工艺参数告警信息
+     * @return 结果
+     */
+    @Override
+    public int updateDyeProcAlarm(DyeProcAlarm dyeProcAlarm) {
+        return dyeProcAlarmMapper.updateDyeProcAlarm(dyeProcAlarm);
+    }
+
+    /**
+     * 批量删除工艺参数告警信息
+     *
+     * @param alarmIds 需要删除的工艺参数告警信息主键
+     * @return 结果
+     */
+    @Override
+    public int deleteDyeProcAlarmByAlarmIds(Long[] alarmIds) {
+        return dyeProcAlarmMapper.deleteDyeProcAlarmByAlarmIds(alarmIds);
+    }
+
+    /**
+     * 删除工艺参数告警信息信息
+     *
+     * @param alarmId 工艺参数告警信息主键
+     * @return 结果
+     */
+    @Override
+    public int deleteDyeProcAlarmByAlarmId(Long alarmId) {
+        return dyeProcAlarmMapper.deleteDyeProcAlarmByAlarmId(alarmId);
+    }
+
+    /**
+     * 统计上一时段数据
+     */
+    @Override
+    public void last() {
+        LocalDateTime ldt = Tools.currWholeTime();
+        //上一个小时
+        ldt = ldt.minusHours(1);
+        LocalDateTime start = ldt;
+        LocalDateTime end = ldt.plusHours(1);
+        calc(start, end);
+    }
+
+    /**
+     * 按指定时间统计
+     *
+     * @param start 开始时间
+     * @param end   结束时间
+     */
+    @Override
+    public void calc(LocalDateTime start, LocalDateTime end) {
+        process(start, end);
+    }
+
+    /**
+     * 查询指定时间段内指定类型指定线别告警信息
+     *
+     * @param start 开始时间
+     * @param end   结束时间
+     * @param types 类型
+     * @param lines 线别
+     * @return 告警信息
+     */
+    @Override
+    public List<DyeProcAlarm> searchAlarm(LocalDate start, LocalDate end, String[] types, String[] lines) {
+        DyeProcAlarm dyeProcAlarm = new DyeProcAlarm();
+        Map<String, Object> params = new HashMap<>();
+        params.put("sTime", DateUtils.toDate(start));
+        params.put("eTime", DateUtils.toDate(end));
+        dyeProcAlarm.setParams(params);
+        List<DyeProcAlarm> result = selectDyeProcAlarmList(dyeProcAlarm);
+
+        // 先按paraCode+device_id分组
+        Map<String, List<DyeProcAlarm>> groupedAlarms = result.stream()
+                .collect(Collectors.groupingBy(
+                        alarm -> alarm.getParaCode() + "_" + alarm.getDeviceId()
+                ));
+
+        // 存储合并后的结果
+        List<DyeProcAlarm> mergedResults = new ArrayList<>();
+
+        // 对每组数据按startTime排序后进行合并处理
+        for (Map.Entry<String, List<DyeProcAlarm>> entry : groupedAlarms.entrySet()) {
+            List<DyeProcAlarm> group = entry.getValue();
+            
+            // 按startTime排序
+            group.sort(Comparator.comparing(DyeProcAlarm::getStartTime));
+            
+            // 合并相邻且时间间隔在5分钟内的记录
+            List<DyeProcAlarm> mergedGroup = mergeAlarmsInGroup(group);
+            mergedResults.addAll(mergedGroup);
+        }
+        
+        return mergedResults;
+    }
+
+    /**
+     * 合并同一组内的告警记录
+     * 
+     * @param alarms 按时间排序的告警记录列表
+     * @return 合并后的告警记录列表
+     */
+    private List<DyeProcAlarm> mergeAlarmsInGroup(List<DyeProcAlarm> alarms) {
+        if (alarms.isEmpty()) {
+            return Collections.emptyList();
+        }
+
+        List<DyeProcAlarm> mergedAlarms = new ArrayList<>();
+        DyeProcAlarm currentAlarm = alarms.get(0).clone();
+        
+        // 初始化actValue范围
+        if (currentAlarm.getActValue() != null) {
+            String formattedVal = formatBigDecimal(currentAlarm.getActValue());
+            currentAlarm.setActValueRange(formattedVal + "-" + formattedVal);
+        }
+        
+        // 设置初始endTime为startTime,因为在创建时endTime通常未设置
+        if (currentAlarm.getEndTime() == null && currentAlarm.getStartTime() != null) {
+            currentAlarm.setEndTime(currentAlarm.getStartTime());
+        }
+        
+        mergedAlarms.add(currentAlarm);
+
+        for (int i = 1; i < alarms.size(); i++) {
+            DyeProcAlarm nextAlarm = alarms.get(i);
+            
+            // 确保下一个告警的endTime也被初始化
+            if (nextAlarm.getEndTime() == null && nextAlarm.getStartTime() != null) {
+                nextAlarm.setEndTime(nextAlarm.getStartTime());
+            }
+            
+            // 判断是否需要合并:时间间隔在5分钟内(300000毫秒)
+            // 只有当时间字段都不为null时才进行合并判断
+            if (nextAlarm.getStartTime() != null && currentAlarm.getEndTime() != null) {
+                long timeDiff = nextAlarm.getStartTime().getTime() - currentAlarm.getEndTime().getTime();
+                if (timeDiff >= 0 && timeDiff <= 300000) {
+                    // 合并记录:更新endTime为当前记录的endTime
+                    if (nextAlarm.getEndTime() != null) {
+                        currentAlarm.setEndTime(nextAlarm.getEndTime());
+                    }
+                    
+                    // 更新actValue范围(最小值-最大值)
+                    if (nextAlarm.getActValue() != null) {
+                        String existingRange = currentAlarm.getActValueRange();
+                        if (existingRange != null && existingRange.contains("-")) {
+                            // 解析现有的范围值
+                            try {
+                                int lastDashIndex = existingRange.lastIndexOf('-');
+                                if (lastDashIndex > 0 && lastDashIndex < existingRange.length() - 1) {
+                                    String minStr = existingRange.substring(0, lastDashIndex);
+                                    String maxStr = existingRange.substring(lastDashIndex + 1);
+
+                                    BigDecimal minVal = new BigDecimal(minStr.trim());
+                                    BigDecimal maxVal = new BigDecimal(maxStr.trim());
+
+                                    // 更新最小值和最大值
+                                    if (nextAlarm.getActValue().compareTo(minVal) < 0) {
+                                        minVal = nextAlarm.getActValue();
+                                    }
+                                    if (nextAlarm.getActValue().compareTo(maxVal) > 0) {
+                                        maxVal = nextAlarm.getActValue();
+                                    }
+
+                                    // 格式化数值,最多保留2位小数
+                                    String formattedMin = formatBigDecimal(minVal);
+                                    String formattedMax = formatBigDecimal(maxVal);
+                                    currentAlarm.setActValueRange(formattedMin + "-" + formattedMax);
+                                } else {
+                                    // 格式不正确,重新初始化
+                                    String formattedVal = formatBigDecimal(nextAlarm.getActValue());
+                                    currentAlarm.setActValueRange(formattedVal + "-" + formattedVal);
+                                }
+                            } catch (NumberFormatException e) {
+                                // 解析失败时,直接使用当前值作为范围
+                                String formattedVal = formatBigDecimal(nextAlarm.getActValue());
+                                currentAlarm.setActValueRange(formattedVal + "-" + formattedVal);
+                            }
+                        } else {
+                            // actValueRange为空或者格式不对时,初始化范围
+                            String formattedVal = formatBigDecimal(nextAlarm.getActValue());
+                            currentAlarm.setActValueRange(formattedVal + "-" + formattedVal);
+                        }
+                    }
+                } else {
+                    // 不满足合并条件,新建记录
+                    currentAlarm = nextAlarm.clone();
+                    
+                    // 初始化actValue范围
+                    if (currentAlarm.getActValue() != null) {
+                        String formattedVal = formatBigDecimal(currentAlarm.getActValue());
+                        currentAlarm.setActValueRange(formattedVal + "-" + formattedVal);
+                    }
+                    
+                    // 设置初始endTime为startTime
+                    if (currentAlarm.getEndTime() == null && currentAlarm.getStartTime() != null) {
+                        currentAlarm.setEndTime(currentAlarm.getStartTime());
+                    }
+                    
+                    mergedAlarms.add(currentAlarm);
+                }
+            } else {
+                // 时间字段为null,不能判断是否合并,直接添加为新记录
+                currentAlarm = nextAlarm.clone();
+                
+                // 初始化actValue范围
+                if (currentAlarm.getActValue() != null) {
+                    String formattedVal = formatBigDecimal(currentAlarm.getActValue());
+                    currentAlarm.setActValueRange(formattedVal + "-" + formattedVal);
+                }
+                
+                // 设置初始endTime为startTime
+                if (currentAlarm.getEndTime() == null && currentAlarm.getStartTime() != null) {
+                    currentAlarm.setEndTime(currentAlarm.getStartTime());
+                }
+                
+                mergedAlarms.add(currentAlarm);
+            }
+        }
+        
+        return mergedAlarms;
+    }
+
+    /**
+     * 处理参数字段
+     *
+     * @param code          设备信息
+     * @param paraParameter 参数字段
+     * @param coefficient   系数
+     * @param fields        字段列表
+     */
+    private void handleParameterField(String code, String paraParameter, BigDecimal coefficient, String key,
+                                      List<String> fields) {
+        if (StringUtils.isNotEmpty(paraParameter)) {
+            String field = "MAX_VALUE(" + code + "." + paraParameter + ")";
+            if (Math.abs(coefficient.doubleValue() - 1.0) > 1e-10) {
+                field = field + "*" + coefficient;
+            }
+            field = field + " AS " + key;
+            fields.add(field);
+        }
+    }
+
+    /**
+     * 格式化BigDecimal数值,最多保留2位小数,去掉末尾的0
+     *
+     * @param value 待格式化的数值
+     * @return 格式化后的字符串
+     */
+    private String formatBigDecimal(BigDecimal value) {
+        if (value == null) {
+            return "0";
+        }
+        // 最多保留2位小数,去掉末尾的0
+        return value.stripTrailingZeros().setScale(Math.min(2, Math.max(0, value.stripTrailingZeros().scale())), RoundingMode.HALF_UP).toPlainString();
+    }
+
+    private void process(LocalDateTime start, LocalDateTime end) {
+        Map<String, List<DyeDeviceVO>> deviceByLine = dyeDeviceService.deviceByLine();
+        List<DyeProcAlarm> result = new ArrayList<>();
+        for (Map.Entry<String, List<DyeDeviceVO>> entry : deviceByLine.entrySet()) {
+            String line = entry.getKey();
+            List<String> fields = new ArrayList<>();
+            String key;
+            List<DyeDeviceVO> devices = entry.getValue();
+            //这个存储iot返回的字段映射  tp_id+device_id
+            Map<String, DyeProcAlarm> alarmMap = new HashMap<>(16);
+            for (DyeDeviceVO device : devices) {
+                List<DyeTypeProcess> typeProcesses = device.getProcParas();
+                for (DyeTypeProcess typeProcess : typeProcesses) {
+                    if (!"1".equals(typeProcess.getUpdatedBy())) {
+                        continue;
+                    }
+                    DyeProcAlarm alarm = new DyeProcAlarm();
+                    alarm.setTypeId(device.getTypeId());
+                    alarm.setTypeName(device.getTypeName());
+                    alarm.setDeviceId(device.getDeviceId());
+                    alarm.setDeviceName(device.getSortNum() + "#" + device.getTypeName().replaceAll("前整", ""));
+                    alarm.setParaCode(typeProcess.getParaCode());
+                    alarm.setParaName(typeProcess.getParaName());
+                    alarm.setValidRange(typeProcess.getValidRange());
+                    alarm.setLine(line);
+                    alarm.setInRange(true);
+                    key = typeProcess.getTpId() + "_" + device.getDeviceId();
+                    alarmMap.put(key, alarm);
+                    // 处理设定值和实际值
+                    handleParameterField(device.getDeviceCode(), typeProcess.getParaSet(), typeProcess.getCoeffSet(), "s_" + key, fields);
+                    handleParameterField(device.getDeviceCode(), typeProcess.getParaAct(), typeProcess.getCoeffAct(), "a_" + key, fields);
+                }
+            }
+            String sql = "select %s from root.tl.suxi GROUP BY ([%s, %s), 1m)";
+            sql = String.format(sql, String.join(",", fields), DateUtils.parseIso(start), DateUtils.parseIso(end));
+            JSONObject jsonObject = iotService.query(sql);
+            JSONObject data = jsonObject.getJSONObject("data");
+            JSONArray values = data.getJSONArray("values");
+            JSONArray timestamps = data.getJSONArray("timestamps");
+            JSONArray columnNames = data.getJSONArray("columnNames");
+            if (values.size() > 0) {
+                for (int j = 0; j < values.size(); j++) {
+                    JSONArray da = values.getJSONArray(j);
+                    long timestamp = timestamps.getLong(j);
+                    Map<String, DyeProcAlarm> timeAlarmMap = alarmMap.entrySet().stream()
+                            .collect(Collectors.toMap(
+                                    Map.Entry::getKey,
+                                    value -> value.getValue().clone()));
+                    for (int i = 0; i < da.size(); i++) {
+                        Double v = da.getDouble(i);
+                        if (v == null) {
+                            continue;
+                        }
+                        String filedName = columnNames.getStr(i);
+                        key = filedName.substring(filedName.indexOf("_") + 1);
+                        DyeProcAlarm alarm = timeAlarmMap.get(key);
+                        alarm.setStartTime(new java.util.Date(timestamp));
+                        if (filedName.startsWith("s_")) {
+                            alarm.setSetValue(new BigDecimal(v).setScale(2, RoundingMode.HALF_UP));
+                        }
+                        if (filedName.startsWith("a_")) {
+                            alarm.setActValue(new BigDecimal(v).setScale(2, RoundingMode.HALF_UP));
+                        }
+                        String validRange = alarm.getValidRange();
+                        boolean inRange = true;
+                        if (StringUtils.isNotEmpty(validRange) && v != null) {
+                            String[] rangeParts = validRange.split("-");
+                            if (rangeParts.length == 2) {
+                                try {
+                                    double lowerBound = Double.parseDouble(rangeParts[0]);
+                                    double upperBound = Double.parseDouble(rangeParts[1]);
+                                    if (v < lowerBound || v > upperBound) {
+                                        inRange = false;
+                                    }
+                                } catch (NumberFormatException e) {
+                                    // 无法解析范围值,保持inRange为true
+                                }
+                            }
+                        }
+                        // 如果需要进一步处理inRange的结果,可以在这里添加代码
+                        alarm.setInRange(inRange);
+                    }
+                    result.addAll(timeAlarmMap.values().stream().filter(alarm -> !alarm.getInRange()).collect(Collectors.toList()));
+                }
+            }
+        }
+
+        //需要合并记录 tp_id+device_id相同,记录actValue的范围(最小值-最大值)存入actValueRange字段中
+        // startTime记录第一次出现的时间,endTime记录最后一次出现的时间
+        // 只有当两条记录时间间隔在5分钟以内时才进行合并
+        
+        // 先按paraCode+device_id分组
+        Map<String, List<DyeProcAlarm>> groupedAlarms = result.stream()
+                .collect(Collectors.groupingBy(
+                        alarm -> alarm.getParaCode() + "_" + alarm.getDeviceId()
+                ));
+
+        // 存储合并后的结果
+        List<DyeProcAlarm> mergedResults = new ArrayList<>();
+
+        // 对每组数据按startTime排序后进行合并处理
+        for (Map.Entry<String, List<DyeProcAlarm>> entry : groupedAlarms.entrySet()) {
+            List<DyeProcAlarm> group = entry.getValue();
+            
+            // 按startTime排序
+            group.sort(Comparator.comparing(DyeProcAlarm::getStartTime));
+            
+            // 合并相邻且时间间隔在5分钟内的记录
+            List<DyeProcAlarm> mergedGroup = mergeAlarmsInGroup(group);
+            mergedResults.addAll(mergedGroup);
+        }
+
+        // 使用合并后的记录替换原结果
+        List<DyeProcAlarm> list = mergedResults;
+
+        try (SqlSession sqlSession = factory.openSession(ExecutorType.BATCH, false)) {
+            if (list.size() > 0) {
+                DyeProcAlarmMapper mapper = sqlSession.getMapper(DyeProcAlarmMapper.class);
+                list.forEach(mapper::insertDyeProcAlarm);
+                sqlSession.commit();
+            }
+        }
+    }
+}

+ 12 - 9
jjt-biz/src/main/java/com/jjt/dye/service/impl/DyeTypeProcessServiceImpl.java

@@ -5,7 +5,6 @@ import cn.hutool.json.JSONObject;
 import com.jjt.common.utils.DateUtils;
 import com.jjt.common.utils.StringUtils;
 import com.jjt.common.utils.bean.BeanUtils;
-import com.jjt.dye.domain.DyeDevicePara;
 import com.jjt.dye.domain.DyeTypeProcess;
 import com.jjt.dye.mapper.DyeTypeProcessMapper;
 import com.jjt.dye.service.IDyeDeviceService;
@@ -145,13 +144,13 @@ public class DyeTypeProcessServiceImpl implements IDyeTypeProcessService {
                     if (StringUtils.isNotEmpty(typeProcess.getParaSet())) {
                         field = device.getDeviceCode() + "." + typeProcess.getParaSet();
                         fields.add(field);
-                        value = "s-" + typeProcess.getParaCode() + "-" + device.getDeviceId() ;
+                        value = "s-" + typeProcess.getParaCode() + "-" + device.getDeviceId() + "-" + typeProcess.getCoeffSet();
                         fieldMap.put(keyPrefix + field, value);
                     }
                     if (StringUtils.isNotEmpty(typeProcess.getParaAct())) {
                         field = device.getDeviceCode() + "." + typeProcess.getParaAct();
                         fields.add(field);
-                        value = "a-" + typeProcess.getParaCode() + "-" + device.getDeviceId() ;
+                        value = "a-" + typeProcess.getParaCode() + "-" + device.getDeviceId() + "-" + typeProcess.getCoeffAct();
                         fieldMap.put(keyPrefix + field, value);
                     }
                 }
@@ -187,12 +186,16 @@ public class DyeTypeProcessServiceImpl implements IDyeTypeProcessService {
                         String paraType = split[0];
                         String paraCode = split[1];
                         String deviceId = split[2];
-//                        if (split.length > 3) {
-//                            String coeff = split[3];
-//                            if (StringUtils.isNotEmpty(coeff)) {
-//                                v = BigDecimal.valueOf(v).multiply(BigDecimal.valueOf(Double.parseDouble(coeff))).doubleValue();
-//                            }
-//                        }
+                        if (split.length > 3) {
+                            try {
+                                double coeffValue = Double.parseDouble(split[3]);
+                                if (Math.abs(coeffValue - 1.0) > 1e-10) {
+                                    v = BigDecimal.valueOf(v).multiply(BigDecimal.valueOf(coeffValue)).doubleValue();
+                                }
+                            } catch (NumberFormatException e) {
+                                // 忽略无效的系数值
+                            }
+                        }
                         if ("s".equals(paraType)) {
                             paraCode = paraCode + "_set";
                         }

+ 7 - 0
jjt-biz/src/main/java/com/jjt/task/YhjTask.java

@@ -3,6 +3,7 @@ package com.jjt.task;
 import com.jjt.calc.service.ITwinCalcDayYhjService;
 import com.jjt.calc.service.ITwinCalcHourYhjService;
 import com.jjt.dye.service.IDyeCalcHourService;
+import com.jjt.dye.service.IDyeProcAlarmService;
 import com.jjt.dyeing.service.IDyeingHourAvgService;
 import com.jjt.dyeing.service.IDyeingHourEnergyService;
 import com.jjt.dyeing.service.IDyeingHourLineService;
@@ -37,6 +38,8 @@ public class YhjTask {
     private IDyeCalcHourService dyeCalcHourService;
     @Resource
     private ILeanFanService leanService;
+    @Resource
+    private IDyeProcAlarmService alarmService;
 
     /**
      * 统计上一时段数据
@@ -59,6 +62,10 @@ public class YhjTask {
             dyeCalcHourService.calcLastHour();
         } catch (Exception ignored) {
         }
+        try {
+            alarmService.last();
+        } catch (Exception ignored) {
+        }
 //        avgService.calcLastHour();
 //        energyService.calcLastHour();
 //        lineService.calcLastHour();

+ 168 - 0
jjt-biz/src/main/resources/mapper/dye/DyeProcAlarmMapper.xml

@@ -0,0 +1,168 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<!DOCTYPE mapper
+        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
+        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
+<mapper namespace="com.jjt.dye.mapper.DyeProcAlarmMapper">
+
+    <resultMap type="DyeProcAlarm" id="DyeProcAlarmResult">
+        <result property="alarmId" column="ALARM_ID"/>
+        <result property="workDay" column="WORK_DAY"/>
+        <result property="startTime" column="START_TIME"/>
+        <result property="endTime" column="END_TIME"/>
+        <result property="typeId" column="TYPE_ID"/>
+        <result property="typeName" column="TYPE_NAME"/>
+        <result property="line" column="LINE"/>
+        <result property="deviceId" column="DEVICE_ID"/>
+        <result property="deviceName" column="DEVICE_NAME"/>
+        <result property="paraCode" column="PARA_CODE"/>
+        <result property="paraName" column="PARA_NAME"/>
+        <result property="validRange" column="VALID_RANGE"/>
+        <result property="setValue" column="SET_VALUE"/>
+        <result property="actValue" column="ACT_VALUE"/>
+        <result property="actValueRange" column="ACT_VALUE_RANGE"/>
+        <result property="createdBy" column="CREATED_BY"/>
+        <result property="createdTime" column="CREATED_TIME"/>
+        <result property="updatedBy" column="UPDATED_BY"/>
+        <result property="updatedTime" column="UPDATED_TIME"/>
+        <result property="remark" column="REMARK"/>
+    </resultMap>
+
+    <sql id="selectDyeProcAlarmVo">
+        select ALARM_ID,
+               WORK_DAY,
+               START_TIME,
+               END_TIME,
+               TYPE_ID,
+               TYPE_NAME,
+               LINE,
+               DEVICE_ID,
+               DEVICE_NAME,
+               PARA_CODE,
+               PARA_NAME,
+               VALID_RANGE,
+               SET_VALUE,
+               ACT_VALUE,
+               ACT_VALUE_RANGE,
+               CREATED_BY,
+               CREATED_TIME,
+               UPDATED_BY,
+               UPDATED_TIME,
+               REMARK
+        from DYE_PROC_ALARM
+    </sql>
+
+    <select id="selectDyeProcAlarmList" parameterType="DyeProcAlarm" resultMap="DyeProcAlarmResult">
+        <include refid="selectDyeProcAlarmVo"/>
+        <where>
+            <if test="startTime != null ">and START_TIME = #{startTime}</if>
+            <if test="endTime != null ">and END_TIME = #{endTime}</if>
+            <if test="typeId != null ">and TYPE_ID = #{typeId}</if>
+            <if test="typeName != null  and typeName != ''">and TYPE_NAME like concat('%', #{typeName}, '%')</if>
+            <if test="line != null  and line != ''">and LINE = #{line}</if>
+            <if test="deviceId != null ">and DEVICE_ID = #{deviceId}</if>
+            <if test="deviceName != null  and deviceName != ''">and DEVICE_NAME like concat('%', #{deviceName}, '%')
+            </if>
+            <if test="paraCode != null  and paraCode != ''">and PARA_CODE = #{paraCode}</if>
+            <if test="paraName != null  and paraName != ''">and PARA_NAME like concat('%', #{paraName}, '%')</if>
+            <if test="validRange != null  and validRange != ''">and VALID_RANGE = #{validRange}</if>
+            <if test="setValue != null ">and SET_VALUE = #{setValue}</if>
+            <if test="actValue != null ">and ACT_VALUE = #{actValue}</if>
+            <if test="actValueRange != null  and actValueRange != ''">and ACT_VALUE_RANGE = #{actValueRange}</if>
+            <if test="createdBy != null  and createdBy != ''">and CREATED_BY = #{createdBy}</if>
+            <if test="createdTime != null ">and CREATED_TIME = #{createdTime}</if>
+            <if test="updatedBy != null  and updatedBy != ''">and UPDATED_BY = #{updatedBy}</if>
+            <if test="updatedTime != null ">and UPDATED_TIME = #{updatedTime}</if>
+            <if test="remark != null  and remark != ''">and REMARK = #{remark}</if>
+            <if test="params.sTime != null and params.eTime != null">
+                and WORK_DAY between #{params.sTime} and #{params.eTime}
+            </if>
+        </where>
+    </select>
+
+    <select id="selectDyeProcAlarmByAlarmId" parameterType="Long" resultMap="DyeProcAlarmResult">
+        <include refid="selectDyeProcAlarmVo"/>
+        where ALARM_ID = #{alarmId}
+    </select>
+
+    <insert id="insertDyeProcAlarm" parameterType="DyeProcAlarm">
+        insert into DYE_PROC_ALARM
+        <trim prefix="(" suffix=")" suffixOverrides=",">
+            <if test="startTime != null">START_TIME,</if>
+            <if test="endTime != null">END_TIME,</if>
+            <if test="typeId != null">TYPE_ID,</if>
+            <if test="typeName != null">TYPE_NAME,</if>
+            <if test="line != null">LINE,</if>
+            <if test="deviceId != null">DEVICE_ID,</if>
+            <if test="deviceName != null">DEVICE_NAME,</if>
+            <if test="paraCode != null">PARA_CODE,</if>
+            <if test="paraName != null">PARA_NAME,</if>
+            <if test="validRange != null">VALID_RANGE,</if>
+            <if test="setValue != null">SET_VALUE,</if>
+            <if test="actValue != null">ACT_VALUE,</if>
+            <if test="actValueRange != null">ACT_VALUE_RANGE,</if>
+            <if test="createdBy != null">CREATED_BY,</if>
+            <if test="createdTime != null">CREATED_TIME,</if>
+            <if test="updatedBy != null">UPDATED_BY,</if>
+            <if test="updatedTime != null">UPDATED_TIME,</if>
+            <if test="remark != null">REMARK,</if>
+        </trim>
+        <trim prefix="values (" suffix=")" suffixOverrides=",">
+            <if test="startTime != null">#{startTime},</if>
+            <if test="endTime != null">#{endTime},</if>
+            <if test="typeId != null">#{typeId},</if>
+            <if test="typeName != null">#{typeName},</if>
+            <if test="line != null">#{line},</if>
+            <if test="deviceId != null">#{deviceId},</if>
+            <if test="deviceName != null">#{deviceName},</if>
+            <if test="paraCode != null">#{paraCode},</if>
+            <if test="paraName != null">#{paraName},</if>
+            <if test="validRange != null">#{validRange},</if>
+            <if test="setValue != null">#{setValue},</if>
+            <if test="actValue != null">#{actValue},</if>
+            <if test="actValueRange != null">#{actValueRange},</if>
+            <if test="createdBy != null">#{createdBy},</if>
+            <if test="createdTime != null">#{createdTime},</if>
+            <if test="updatedBy != null">#{updatedBy},</if>
+            <if test="updatedTime != null">#{updatedTime},</if>
+            <if test="remark != null">#{remark},</if>
+        </trim>
+    </insert>
+
+    <update id="updateDyeProcAlarm" parameterType="DyeProcAlarm">
+        update DYE_PROC_ALARM
+        <trim prefix="SET" suffixOverrides=",">
+            <if test="startTime != null">START_TIME = #{startTime},</if>
+            <if test="endTime != null">END_TIME = #{endTime},</if>
+            <if test="typeId != null">TYPE_ID = #{typeId},</if>
+            <if test="typeName != null">TYPE_NAME = #{typeName},</if>
+            <if test="line != null">LINE = #{line},</if>
+            <if test="deviceId != null">DEVICE_ID = #{deviceId},</if>
+            <if test="deviceName != null">DEVICE_NAME = #{deviceName},</if>
+            <if test="paraCode != null">PARA_CODE = #{paraCode},</if>
+            <if test="paraName != null">PARA_NAME = #{paraName},</if>
+            <if test="validRange != null">VALID_RANGE = #{validRange},</if>
+            <if test="setValue != null">SET_VALUE = #{setValue},</if>
+            <if test="actValue != null">ACT_VALUE = #{actValue},</if>
+            <if test="actValueRange != null">ACT_VALUE_RANGE = #{actValueRange},</if>
+            <if test="createdBy != null">CREATED_BY = #{createdBy},</if>
+            <if test="createdTime != null">CREATED_TIME = #{createdTime},</if>
+            <if test="updatedBy != null">UPDATED_BY = #{updatedBy},</if>
+            <if test="updatedTime != null">UPDATED_TIME = #{updatedTime},</if>
+            <if test="remark != null">REMARK = #{remark},</if>
+        </trim>
+        where ALARM_ID = #{alarmId}
+    </update>
+
+    <delete id="deleteDyeProcAlarmByAlarmId" parameterType="Long">
+        delete
+        from DYE_PROC_ALARM
+        where ALARM_ID = #{alarmId}
+    </delete>
+
+    <delete id="deleteDyeProcAlarmByAlarmIds" parameterType="String">
+        delete from DYE_PROC_ALARM where ALARM_ID in
+        <foreach item="alarmId" collection="array" open="(" separator="," close=")">
+            #{alarmId}
+        </foreach>
+    </delete>
+</mapper>

+ 17 - 12
jjt-biz/src/main/resources/mapper/dye/DyeTypeMapper.xml

@@ -36,17 +36,20 @@
         <result property="remark" column="REMARK"/>
     </resultMap>
     <resultMap type="DyeTypeProcess" id="DyeTypeProcessResult">
-        <result property="tpId" column="TP_ID"/>
-        <result property="typeId" column="TYPE_ID"/>
-        <result property="paraCode" column="PARA_CODE"/>
-        <result property="paraName" column="PARA_NAME"/>
-        <result property="paraSet" column="PARA_SET"/>
-        <result property="paraAct" column="PARA_ACT"/>
-        <result property="createdBy" column="CREATED_BY"/>
-        <result property="createdTime" column="CREATED_TIME"/>
-        <result property="updatedBy" column="UPDATED_BY"/>
-        <result property="updatedTime" column="UPDATED_TIME"/>
-        <result property="remark" column="REMARK"/>
+        <result property="tpId"    column="TP_ID"    />
+        <result property="typeId"    column="TYPE_ID"    />
+        <result property="paraCode"    column="PARA_CODE"    />
+        <result property="paraName"    column="PARA_NAME"    />
+        <result property="paraSet"    column="PARA_SET"    />
+        <result property="coeffSet"    column="COEFF_SET"    />
+        <result property="paraAct"    column="PARA_ACT"    />
+        <result property="coeffAct"    column="COEFF_ACT"    />
+        <result property="validRange"    column="VALID_RANGE"    />
+        <result property="createdBy"    column="CREATED_BY"    />
+        <result property="createdTime"    column="CREATED_TIME"    />
+        <result property="updatedBy"    column="UPDATED_BY"    />
+        <result property="updatedTime"    column="UPDATED_TIME"    />
+        <result property="remark"    column="REMARK"    />
     </resultMap>
 
     <sql id="selectDyeTypeVo">
@@ -108,8 +111,10 @@
                PARA_CODE,
                PARA_NAME,
                PARA_SET,
+               COEFF_SET,
                PARA_ACT,
-               COEFF,
+               COEFF_ACT,
+               VALID_RANGE,
                CREATED_BY,
                CREATED_TIME,
                UPDATED_BY,

+ 20 - 15
jjt-biz/src/main/resources/mapper/dye/DyeTypeProcessMapper.xml

@@ -3,14 +3,17 @@
 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
 "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.jjt.dye.mapper.DyeTypeProcessMapper">
-    
+
     <resultMap type="DyeTypeProcess" id="DyeTypeProcessResult">
         <result property="tpId"    column="TP_ID"    />
         <result property="typeId"    column="TYPE_ID"    />
         <result property="paraCode"    column="PARA_CODE"    />
         <result property="paraName"    column="PARA_NAME"    />
         <result property="paraSet"    column="PARA_SET"    />
+        <result property="coeffSet"    column="COEFF_SET"    />
         <result property="paraAct"    column="PARA_ACT"    />
+        <result property="coeffAct"    column="COEFF_ACT"    />
+        <result property="validRange"    column="VALID_RANGE"    />
         <result property="createdBy"    column="CREATED_BY"    />
         <result property="createdTime"    column="CREATED_TIME"    />
         <result property="updatedBy"    column="UPDATED_BY"    />
@@ -19,25 +22,18 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </resultMap>
 
     <sql id="selectDyeTypeProcessVo">
-        select TP_ID, TYPE_ID, PARA_CODE, PARA_NAME, PARA_SET, PARA_ACT, CREATED_BY, CREATED_TIME, UPDATED_BY, UPDATED_TIME, REMARK from DYE_TYPE_PROCESS
+        select TP_ID, TYPE_ID, PARA_CODE, PARA_NAME, PARA_SET, COEFF_SET, PARA_ACT, COEFF_ACT, VALID_RANGE, CREATED_BY, CREATED_TIME, UPDATED_BY, UPDATED_TIME, REMARK from DYE_TYPE_PROCESS
     </sql>
 
     <select id="selectDyeTypeProcessList" parameterType="DyeTypeProcess" resultMap="DyeTypeProcessResult">
         <include refid="selectDyeTypeProcessVo"/>
-        <where>  
+        <where>
             <if test="typeId != null "> and TYPE_ID = #{typeId}</if>
             <if test="paraCode != null  and paraCode != ''"> and PARA_CODE = #{paraCode}</if>
             <if test="paraName != null  and paraName != ''"> and PARA_NAME like concat('%', #{paraName}, '%')</if>
-            <if test="paraSet != null  and paraSet != ''"> and PARA_SET = #{paraSet}</if>
-            <if test="paraAct != null  and paraAct != ''"> and PARA_ACT = #{paraAct}</if>
-            <if test="createdBy != null  and createdBy != ''"> and CREATED_BY = #{createdBy}</if>
-            <if test="createdTime != null "> and CREATED_TIME = #{createdTime}</if>
-            <if test="updatedBy != null  and updatedBy != ''"> and UPDATED_BY = #{updatedBy}</if>
-            <if test="updatedTime != null "> and UPDATED_TIME = #{updatedTime}</if>
-            <if test="remark != null  and remark != ''"> and REMARK = #{remark}</if>
         </where>
     </select>
-    
+
     <select id="selectDyeTypeProcessByTpId" parameterType="Long" resultMap="DyeTypeProcessResult">
         <include refid="selectDyeTypeProcessVo"/>
         where TP_ID = #{tpId}
@@ -50,25 +46,31 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="paraCode != null">PARA_CODE,</if>
             <if test="paraName != null">PARA_NAME,</if>
             <if test="paraSet != null">PARA_SET,</if>
+            <if test="coeffSet != null">COEFF_SET,</if>
             <if test="paraAct != null">PARA_ACT,</if>
+            <if test="coeffAct != null">COEFF_ACT,</if>
+            <if test="validRange != null">VALID_RANGE,</if>
             <if test="createdBy != null">CREATED_BY,</if>
             <if test="createdTime != null">CREATED_TIME,</if>
             <if test="updatedBy != null">UPDATED_BY,</if>
             <if test="updatedTime != null">UPDATED_TIME,</if>
             <if test="remark != null">REMARK,</if>
-         </trim>
+        </trim>
         <trim prefix="values (" suffix=")" suffixOverrides=",">
             <if test="typeId != null">#{typeId},</if>
             <if test="paraCode != null">#{paraCode},</if>
             <if test="paraName != null">#{paraName},</if>
             <if test="paraSet != null">#{paraSet},</if>
+            <if test="coeffSet != null">#{coeffSet},</if>
             <if test="paraAct != null">#{paraAct},</if>
+            <if test="coeffAct != null">#{coeffAct},</if>
+            <if test="validRange != null">#{validRange},</if>
             <if test="createdBy != null">#{createdBy},</if>
             <if test="createdTime != null">#{createdTime},</if>
             <if test="updatedBy != null">#{updatedBy},</if>
             <if test="updatedTime != null">#{updatedTime},</if>
             <if test="remark != null">#{remark},</if>
-         </trim>
+        </trim>
     </insert>
 
     <update id="updateDyeTypeProcess" parameterType="DyeTypeProcess">
@@ -78,7 +80,10 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
             <if test="paraCode != null">PARA_CODE = #{paraCode},</if>
             <if test="paraName != null">PARA_NAME = #{paraName},</if>
             <if test="paraSet != null">PARA_SET = #{paraSet},</if>
+            <if test="coeffSet != null">COEFF_SET = #{coeffSet},</if>
             <if test="paraAct != null">PARA_ACT = #{paraAct},</if>
+            <if test="coeffAct != null">COEFF_ACT = #{coeffAct},</if>
+            <if test="validRange != null">VALID_RANGE = #{validRange},</if>
             <if test="createdBy != null">CREATED_BY = #{createdBy},</if>
             <if test="createdTime != null">CREATED_TIME = #{createdTime},</if>
             <if test="updatedBy != null">UPDATED_BY = #{updatedBy},</if>
@@ -93,9 +98,9 @@ PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
     </delete>
 
     <delete id="deleteDyeTypeProcessByTpIds" parameterType="String">
-        delete from DYE_TYPE_PROCESS where TP_ID in 
+        delete from DYE_TYPE_PROCESS where TP_ID in
         <foreach item="tpId" collection="array" open="(" separator="," close=")">
             #{tpId}
         </foreach>
     </delete>
-</mapper>
+</mapper>

BIN=BIN
jjt-biz/src/main/resources/tpl/qz-alarm.xlsx