feat(report): 新增设备参数分析功能

- 创建设备参数分析Controller,提供参数选项、异常报表、SPC报表和切换追溯接口
- 实现设备参数分析Mapper,定义参数选项、异常列表、SPC点位和切换追溯查询方法
- 开发设备参数分析Service实现类,包含SPC统计计算和参数归一化处理逻辑
- 添加设备参数相关的VO类,包括异常报表、参数选项、SPC报告和切换追溯数据结构
- 配置MyBatis映射文件,实现复杂的参数异常检测和切换追溯SQL查询逻辑
- 实现权限控制注解,确保只有授权用户可访问设备参数分析功能
master
zangch@mesnac.com 5 days ago
parent e607a76630
commit 013d1fa34f

@ -0,0 +1,236 @@
package com.aucma.base.utils;
import com.aucma.common.exception.ServiceException;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.DataFormatter;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
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.xssf.usermodel.XSSFWorkbook;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* BOM
*/
public final class MaterialImportExcelHelper
{
private static final String HEADER_INDEX = "序号";
private static final String HEADER_CODE = "编码";
private static final String HEADER_NAME = "对象描述";
private static final List<String> TEMPLATE_SHEET_NAMES = Arrays.asList("原件", "印刷件", "喷漆件");
private MaterialImportExcelHelper()
{
}
public static ParseResult parse(InputStream inputStream) throws IOException
{
try (Workbook workbook = WorkbookFactory.create(inputStream))
{
DataFormatter formatter = new DataFormatter();
Map<String, MaterialImportRow> rowMap = new LinkedHashMap<>();
List<DuplicateMaterialRow> duplicateRows = new ArrayList<>();
for (int sheetIndex = 0; sheetIndex < workbook.getNumberOfSheets(); sheetIndex++)
{
Sheet sheet = workbook.getSheetAt(sheetIndex);
if (sheet == null || sheet.getPhysicalNumberOfRows() == 0)
{
continue;
}
validateHeader(sheet, formatter);
for (int rowIndex = 1; rowIndex <= sheet.getLastRowNum(); rowIndex++)
{
Row row = sheet.getRow(rowIndex);
if (row == null)
{
continue;
}
String materialCode = normalizeCell(formatter, row.getCell(1));
String materialName = normalizeCell(formatter, row.getCell(2));
if (isBlank(materialCode) && isBlank(materialName))
{
continue;
}
if (isBlank(materialCode) || isBlank(materialName))
{
throw new ServiceException("模板第" + (sheetIndex + 1) + "个sheet[" + sheet.getSheetName() + "]第" + (rowIndex + 1) + "行存在空编码或空对象描述");
}
MaterialImportRow existed = rowMap.get(materialCode);
if (existed != null)
{
// 为什么直接跳过重复编码BOM滚算模板按业务来源会跨sheet重复出现同一物料导入目标表又以物料编码为唯一识别口径保留首次出现即可避免整批失败。
duplicateRows.add(new DuplicateMaterialRow(existed, new MaterialImportRow(sheet.getSheetName(), rowIndex + 1, materialCode, materialName)));
continue;
}
rowMap.put(materialCode, new MaterialImportRow(sheet.getSheetName(), rowIndex + 1, materialCode, materialName));
}
}
if (rowMap.isEmpty())
{
throw new ServiceException("导入数据不能为空请检查Excel是否包含有效物料编码");
}
return new ParseResult(new ArrayList<>(rowMap.values()), duplicateRows);
}
}
public static void writeTemplate(OutputStream outputStream) throws IOException
{
try (Workbook workbook = new XSSFWorkbook())
{
CellStyle headerStyle = workbook.createCellStyle();
headerStyle.setAlignment(HorizontalAlignment.CENTER);
headerStyle.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
for (String sheetName : TEMPLATE_SHEET_NAMES)
{
Sheet sheet = workbook.createSheet(sheetName);
Row headerRow = sheet.createRow(0);
createHeaderCell(headerRow, 0, HEADER_INDEX, headerStyle);
createHeaderCell(headerRow, 1, HEADER_CODE, headerStyle);
createHeaderCell(headerRow, 2, HEADER_NAME, headerStyle);
sheet.setColumnWidth(0, 12 * 256);
sheet.setColumnWidth(1, 18 * 256);
sheet.setColumnWidth(2, 60 * 256);
}
workbook.write(outputStream);
outputStream.flush();
}
}
private static void validateHeader(Sheet sheet, DataFormatter formatter)
{
Row headerRow = sheet.getRow(0);
if (headerRow == null)
{
throw new ServiceException("sheet[" + sheet.getSheetName() + "]缺少表头");
}
String headerIndex = normalizeCell(formatter, headerRow.getCell(0));
String headerCode = normalizeCell(formatter, headerRow.getCell(1));
String headerName = normalizeCell(formatter, headerRow.getCell(2));
if (!HEADER_INDEX.equals(headerIndex) || !HEADER_CODE.equals(headerCode) || !HEADER_NAME.equals(headerName))
{
throw new ServiceException("sheet[" + sheet.getSheetName() + "]表头不符合要求,必须为:序号/编码/对象描述");
}
}
private static void createHeaderCell(Row headerRow, int columnIndex, String value, CellStyle headerStyle)
{
Cell cell = headerRow.createCell(columnIndex);
cell.setCellValue(value);
cell.setCellStyle(headerStyle);
}
private static String normalizeCell(DataFormatter formatter, Cell cell)
{
return normalizeText(cell == null ? null : formatter.formatCellValue(cell));
}
private static String normalizeText(String text)
{
if (text == null)
{
return null;
}
// 为什么要在这里抹平换行模板里部分单元格只是为了展示自动换行实际物料描述仍是一条业务数据直接入库会污染检索和SQL脚本。
return text.replace("\r", "").replace("\n", "").trim();
}
private static boolean isBlank(String text)
{
return text == null || text.trim().isEmpty();
}
public static class MaterialImportRow
{
private final String sheetName;
private final int rowNum;
private final String materialCode;
private final String materialName;
public MaterialImportRow(String sheetName, int rowNum, String materialCode, String materialName)
{
this.sheetName = sheetName;
this.rowNum = rowNum;
this.materialCode = materialCode;
this.materialName = materialName;
}
public String getSheetName()
{
return sheetName;
}
public int getRowNum()
{
return rowNum;
}
public String getMaterialCode()
{
return materialCode;
}
public String getMaterialName()
{
return materialName;
}
}
public static class DuplicateMaterialRow
{
private final MaterialImportRow firstRow;
private final MaterialImportRow duplicateRow;
public DuplicateMaterialRow(MaterialImportRow firstRow, MaterialImportRow duplicateRow)
{
this.firstRow = firstRow;
this.duplicateRow = duplicateRow;
}
public MaterialImportRow getFirstRow()
{
return firstRow;
}
public MaterialImportRow getDuplicateRow()
{
return duplicateRow;
}
}
public static class ParseResult
{
private final List<MaterialImportRow> materialRows;
private final List<DuplicateMaterialRow> duplicateRows;
public ParseResult(List<MaterialImportRow> materialRows, List<DuplicateMaterialRow> duplicateRows)
{
this.materialRows = materialRows;
this.duplicateRows = duplicateRows;
}
public List<MaterialImportRow> getMaterialRows()
{
return materialRows;
}
public List<DuplicateMaterialRow> getDuplicateRows()
{
return duplicateRows;
}
}
}

@ -0,0 +1,63 @@
package com.aucma.report.controller;
import com.aucma.common.core.controller.BaseController;
import com.aucma.common.core.domain.AjaxResult;
import com.aucma.common.core.page.TableDataInfo;
import com.aucma.report.domain.DeviceParamReportQuery;
import com.aucma.report.service.IDeviceParamAnalysisService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* Controller
*
* @author Codex
*/
@RestController
@RequestMapping("/report/deviceParamAnalysis")
public class DeviceParamAnalysisController extends BaseController {
@Autowired
private IDeviceParamAnalysisService deviceParamAnalysisService;
/**
*
*/
@PreAuthorize("@ss.hasPermi('baseDeviceParamVal:trace:list')")
@GetMapping("/paramOptions")
public AjaxResult paramOptions(DeviceParamReportQuery query) {
return AjaxResult.success(deviceParamAnalysisService.selectParamOptions(query));
}
/**
*
*/
@PreAuthorize("@ss.hasPermi('baseDeviceParamVal:trace:list')")
@GetMapping("/anomaly/list")
public TableDataInfo anomalyList(DeviceParamReportQuery query) {
startPage();
return getDataTable(deviceParamAnalysisService.selectParamAnomalyList(query));
}
/**
* /SPC
*/
@PreAuthorize("@ss.hasPermi('baseDeviceParamVal:trace:list')")
@GetMapping("/spc")
public AjaxResult spc(DeviceParamReportQuery query) {
return AjaxResult.success(deviceParamAnalysisService.getSpcReport(query));
}
/**
* //
*/
@PreAuthorize("@ss.hasPermi('baseDeviceParamVal:trace:list')")
@GetMapping("/switch/list")
public TableDataInfo switchList(DeviceParamReportQuery query) {
startPage();
return getDataTable(deviceParamAnalysisService.selectSwitchTraceList(query));
}
}

@ -0,0 +1,89 @@
package com.aucma.report.domain;
import java.io.Serializable;
/**
*
*
* @author Codex
*/
public class DeviceParamReportQuery implements Serializable {
private static final long serialVersionUID = 1L;
/** 设备编号 */
private String deviceCode;
/** 参数编号 */
private String paramCode;
/** 查询场景anomaly/spc/switch */
private String scene;
/** 统计开始时间,格式 yyyy-MM-dd HH:mm:ss */
private String startTime;
/** 统计结束时间,格式 yyyy-MM-dd HH:mm:ss */
private String endTime;
/** 切换类型ALL/MOLD/MATERIAL/PRODUCT */
private String switchType;
/** 切换后观察窗口(分钟) */
private Integer observeMinutes;
public String getDeviceCode() {
return deviceCode;
}
public void setDeviceCode(String deviceCode) {
this.deviceCode = deviceCode;
}
public String getParamCode() {
return paramCode;
}
public void setParamCode(String paramCode) {
this.paramCode = paramCode;
}
public String getScene() {
return scene;
}
public void setScene(String scene) {
this.scene = scene;
}
public String getStartTime() {
return startTime;
}
public void setStartTime(String startTime) {
this.startTime = startTime;
}
public String getEndTime() {
return endTime;
}
public void setEndTime(String endTime) {
this.endTime = endTime;
}
public String getSwitchType() {
return switchType;
}
public void setSwitchType(String switchType) {
this.switchType = switchType;
}
public Integer getObserveMinutes() {
return observeMinutes;
}
public void setObserveMinutes(Integer observeMinutes) {
this.observeMinutes = observeMinutes;
}
}

@ -0,0 +1,183 @@
package com.aucma.report.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serializable;
import java.util.Date;
/**
* VO
*
* @author Codex
*/
public class DeviceParamAnomalyVo implements Serializable {
private static final long serialVersionUID = 1L;
private String deviceCode;
private String deviceName;
private String paramCode;
private String paramName;
private String alertLevel;
private String alertLevelName;
private Double upperLimit;
private Double lowerLimit;
/** 超限次数,按连续异常段统计 */
private Long overLimitCount;
/** 超上限次数 */
private Long upperAnomalyCount;
/** 超下限次数 */
private Long lowerAnomalyCount;
/** 异常持续时长(分钟) */
private Double abnormalDurationMinutes;
/** 异常样本最高值 */
private Double maxValue;
/** 异常样本最低值 */
private Double minValue;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date firstAbnormalTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date lastAbnormalTime;
public String getDeviceCode() {
return deviceCode;
}
public void setDeviceCode(String deviceCode) {
this.deviceCode = deviceCode;
}
public String getDeviceName() {
return deviceName;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
public String getParamCode() {
return paramCode;
}
public void setParamCode(String paramCode) {
this.paramCode = paramCode;
}
public String getParamName() {
return paramName;
}
public void setParamName(String paramName) {
this.paramName = paramName;
}
public String getAlertLevel() {
return alertLevel;
}
public void setAlertLevel(String alertLevel) {
this.alertLevel = alertLevel;
}
public String getAlertLevelName() {
return alertLevelName;
}
public void setAlertLevelName(String alertLevelName) {
this.alertLevelName = alertLevelName;
}
public Double getUpperLimit() {
return upperLimit;
}
public void setUpperLimit(Double upperLimit) {
this.upperLimit = upperLimit;
}
public Double getLowerLimit() {
return lowerLimit;
}
public void setLowerLimit(Double lowerLimit) {
this.lowerLimit = lowerLimit;
}
public Long getOverLimitCount() {
return overLimitCount;
}
public void setOverLimitCount(Long overLimitCount) {
this.overLimitCount = overLimitCount;
}
public Long getUpperAnomalyCount() {
return upperAnomalyCount;
}
public void setUpperAnomalyCount(Long upperAnomalyCount) {
this.upperAnomalyCount = upperAnomalyCount;
}
public Long getLowerAnomalyCount() {
return lowerAnomalyCount;
}
public void setLowerAnomalyCount(Long lowerAnomalyCount) {
this.lowerAnomalyCount = lowerAnomalyCount;
}
public Double getAbnormalDurationMinutes() {
return abnormalDurationMinutes;
}
public void setAbnormalDurationMinutes(Double abnormalDurationMinutes) {
this.abnormalDurationMinutes = abnormalDurationMinutes;
}
public Double getMaxValue() {
return maxValue;
}
public void setMaxValue(Double maxValue) {
this.maxValue = maxValue;
}
public Double getMinValue() {
return minValue;
}
public void setMinValue(Double minValue) {
this.minValue = minValue;
}
public Date getFirstAbnormalTime() {
return firstAbnormalTime;
}
public void setFirstAbnormalTime(Date firstAbnormalTime) {
this.firstAbnormalTime = firstAbnormalTime;
}
public Date getLastAbnormalTime() {
return lastAbnormalTime;
}
public void setLastAbnormalTime(Date lastAbnormalTime) {
this.lastAbnormalTime = lastAbnormalTime;
}
}

@ -0,0 +1,34 @@
package com.aucma.report.domain.vo;
import java.io.Serializable;
/**
* VO
*
* @author Codex
*/
public class DeviceParamOptionVo implements Serializable {
private static final long serialVersionUID = 1L;
/** 参数编号 */
private String paramCode;
/** 参数名称 */
private String paramName;
public String getParamCode() {
return paramCode;
}
public void setParamCode(String paramCode) {
this.paramCode = paramCode;
}
public String getParamName() {
return paramName;
}
public void setParamName(String paramName) {
this.paramName = paramName;
}
}

@ -0,0 +1,96 @@
package com.aucma.report.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serializable;
import java.util.Date;
/**
* VO
*
* @author Codex
*/
public class DeviceParamSpcPointVo implements Serializable {
private static final long serialVersionUID = 1L;
private String deviceCode;
private String deviceName;
private String paramCode;
private String paramName;
private Double upperLimit;
private Double lowerLimit;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date collectTime;
private Double paramValue;
public String getDeviceCode() {
return deviceCode;
}
public void setDeviceCode(String deviceCode) {
this.deviceCode = deviceCode;
}
public String getDeviceName() {
return deviceName;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
public String getParamCode() {
return paramCode;
}
public void setParamCode(String paramCode) {
this.paramCode = paramCode;
}
public String getParamName() {
return paramName;
}
public void setParamName(String paramName) {
this.paramName = paramName;
}
public Double getUpperLimit() {
return upperLimit;
}
public void setUpperLimit(Double upperLimit) {
this.upperLimit = upperLimit;
}
public Double getLowerLimit() {
return lowerLimit;
}
public void setLowerLimit(Double lowerLimit) {
this.lowerLimit = lowerLimit;
}
public Date getCollectTime() {
return collectTime;
}
public void setCollectTime(Date collectTime) {
this.collectTime = collectTime;
}
public Double getParamValue() {
return paramValue;
}
public void setParamValue(Double paramValue) {
this.paramValue = paramValue;
}
}

@ -0,0 +1,203 @@
package com.aucma.report.domain.vo;
import java.io.Serializable;
import java.util.List;
/**
* /SPCVO
*
* @author Codex
*/
public class DeviceParamSpcReportVo implements Serializable {
private static final long serialVersionUID = 1L;
private String deviceCode;
private String deviceName;
private String paramCode;
private String paramName;
private Double upperLimit;
private Double lowerLimit;
private Integer sampleSize;
private Double mean;
private Double maxValue;
private Double minValue;
private Double rangeValue;
private Double stdDev;
private Double coefficientVariation;
private Double ucl;
private Double cl;
private Double lcl;
private Double cpk;
private Integer outOfControlCount;
private List<DeviceParamSpcPointVo> points;
public String getDeviceCode() {
return deviceCode;
}
public void setDeviceCode(String deviceCode) {
this.deviceCode = deviceCode;
}
public String getDeviceName() {
return deviceName;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
public String getParamCode() {
return paramCode;
}
public void setParamCode(String paramCode) {
this.paramCode = paramCode;
}
public String getParamName() {
return paramName;
}
public void setParamName(String paramName) {
this.paramName = paramName;
}
public Double getUpperLimit() {
return upperLimit;
}
public void setUpperLimit(Double upperLimit) {
this.upperLimit = upperLimit;
}
public Double getLowerLimit() {
return lowerLimit;
}
public void setLowerLimit(Double lowerLimit) {
this.lowerLimit = lowerLimit;
}
public Integer getSampleSize() {
return sampleSize;
}
public void setSampleSize(Integer sampleSize) {
this.sampleSize = sampleSize;
}
public Double getMean() {
return mean;
}
public void setMean(Double mean) {
this.mean = mean;
}
public Double getMaxValue() {
return maxValue;
}
public void setMaxValue(Double maxValue) {
this.maxValue = maxValue;
}
public Double getMinValue() {
return minValue;
}
public void setMinValue(Double minValue) {
this.minValue = minValue;
}
public Double getRangeValue() {
return rangeValue;
}
public void setRangeValue(Double rangeValue) {
this.rangeValue = rangeValue;
}
public Double getStdDev() {
return stdDev;
}
public void setStdDev(Double stdDev) {
this.stdDev = stdDev;
}
public Double getCoefficientVariation() {
return coefficientVariation;
}
public void setCoefficientVariation(Double coefficientVariation) {
this.coefficientVariation = coefficientVariation;
}
public Double getUcl() {
return ucl;
}
public void setUcl(Double ucl) {
this.ucl = ucl;
}
public Double getCl() {
return cl;
}
public void setCl(Double cl) {
this.cl = cl;
}
public Double getLcl() {
return lcl;
}
public void setLcl(Double lcl) {
this.lcl = lcl;
}
public Double getCpk() {
return cpk;
}
public void setCpk(Double cpk) {
this.cpk = cpk;
}
public Integer getOutOfControlCount() {
return outOfControlCount;
}
public void setOutOfControlCount(Integer outOfControlCount) {
this.outOfControlCount = outOfControlCount;
}
public List<DeviceParamSpcPointVo> getPoints() {
return points;
}
public void setPoints(List<DeviceParamSpcPointVo> points) {
this.points = points;
}
}

@ -0,0 +1,139 @@
package com.aucma.report.domain.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import java.io.Serializable;
import java.util.Date;
/**
* //VO
*
* @author Codex
*/
public class DeviceParamSwitchTraceVo implements Serializable {
private static final long serialVersionUID = 1L;
private String deviceCode;
private String deviceName;
private String switchType;
private String paramCode;
private String paramName;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
private Date changeTime;
private String beforeValue;
private String afterValue;
private Integer observeMinutes;
/** 切换后观察窗口内产量增量 */
private Double outputIncrement;
/** 切换后观察窗口内异常次数 */
private Long abnormalCount;
/** 切换后最近设备状态 */
private String latestStatus;
public String getDeviceCode() {
return deviceCode;
}
public void setDeviceCode(String deviceCode) {
this.deviceCode = deviceCode;
}
public String getDeviceName() {
return deviceName;
}
public void setDeviceName(String deviceName) {
this.deviceName = deviceName;
}
public String getSwitchType() {
return switchType;
}
public void setSwitchType(String switchType) {
this.switchType = switchType;
}
public String getParamCode() {
return paramCode;
}
public void setParamCode(String paramCode) {
this.paramCode = paramCode;
}
public String getParamName() {
return paramName;
}
public void setParamName(String paramName) {
this.paramName = paramName;
}
public Date getChangeTime() {
return changeTime;
}
public void setChangeTime(Date changeTime) {
this.changeTime = changeTime;
}
public String getBeforeValue() {
return beforeValue;
}
public void setBeforeValue(String beforeValue) {
this.beforeValue = beforeValue;
}
public String getAfterValue() {
return afterValue;
}
public void setAfterValue(String afterValue) {
this.afterValue = afterValue;
}
public Integer getObserveMinutes() {
return observeMinutes;
}
public void setObserveMinutes(Integer observeMinutes) {
this.observeMinutes = observeMinutes;
}
public Double getOutputIncrement() {
return outputIncrement;
}
public void setOutputIncrement(Double outputIncrement) {
this.outputIncrement = outputIncrement;
}
public Long getAbnormalCount() {
return abnormalCount;
}
public void setAbnormalCount(Long abnormalCount) {
this.abnormalCount = abnormalCount;
}
public String getLatestStatus() {
return latestStatus;
}
public void setLatestStatus(String latestStatus) {
this.latestStatus = latestStatus;
}
}

@ -0,0 +1,39 @@
package com.aucma.report.mapper;
import com.aucma.report.domain.DeviceParamReportQuery;
import com.aucma.report.domain.vo.DeviceParamAnomalyVo;
import com.aucma.report.domain.vo.DeviceParamOptionVo;
import com.aucma.report.domain.vo.DeviceParamSpcPointVo;
import com.aucma.report.domain.vo.DeviceParamSwitchTraceVo;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
* Mapper
*
* @author Codex
*/
@Mapper
public interface DeviceParamAnalysisMapper {
/**
*
*/
List<DeviceParamOptionVo> selectParamOptions(DeviceParamReportQuery query);
/**
*
*/
List<DeviceParamAnomalyVo> selectParamAnomalyList(DeviceParamReportQuery query);
/**
*
*/
List<DeviceParamSpcPointVo> selectSpcPoints(DeviceParamReportQuery query);
/**
*
*/
List<DeviceParamSwitchTraceVo> selectSwitchTraceList(DeviceParamReportQuery query);
}

@ -0,0 +1,37 @@
package com.aucma.report.service;
import com.aucma.report.domain.DeviceParamReportQuery;
import com.aucma.report.domain.vo.DeviceParamAnomalyVo;
import com.aucma.report.domain.vo.DeviceParamOptionVo;
import com.aucma.report.domain.vo.DeviceParamSpcReportVo;
import com.aucma.report.domain.vo.DeviceParamSwitchTraceVo;
import java.util.List;
/**
* Service
*
* @author Codex
*/
public interface IDeviceParamAnalysisService {
/**
*
*/
List<DeviceParamOptionVo> selectParamOptions(DeviceParamReportQuery query);
/**
*
*/
List<DeviceParamAnomalyVo> selectParamAnomalyList(DeviceParamReportQuery query);
/**
* /SPC
*/
DeviceParamSpcReportVo getSpcReport(DeviceParamReportQuery query);
/**
*
*/
List<DeviceParamSwitchTraceVo> selectSwitchTraceList(DeviceParamReportQuery query);
}

@ -0,0 +1,209 @@
package com.aucma.report.service.impl;
import com.aucma.report.domain.DeviceParamReportQuery;
import com.aucma.report.domain.vo.DeviceParamAnomalyVo;
import com.aucma.report.domain.vo.DeviceParamOptionVo;
import com.aucma.report.domain.vo.DeviceParamSpcPointVo;
import com.aucma.report.domain.vo.DeviceParamSpcReportVo;
import com.aucma.report.domain.vo.DeviceParamSwitchTraceVo;
import com.aucma.report.mapper.DeviceParamAnalysisMapper;
import com.aucma.report.service.IDeviceParamAnalysisService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.List;
/**
* Service
*
* @author Codex
*/
@Service
public class DeviceParamAnalysisServiceImpl implements IDeviceParamAnalysisService {
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@Autowired
private DeviceParamAnalysisMapper deviceParamAnalysisMapper;
@Override
public List<DeviceParamOptionVo> selectParamOptions(DeviceParamReportQuery query) {
normalizeQuery(query);
return deviceParamAnalysisMapper.selectParamOptions(query);
}
@Override
public List<DeviceParamAnomalyVo> selectParamAnomalyList(DeviceParamReportQuery query) {
normalizeQuery(query);
return deviceParamAnalysisMapper.selectParamAnomalyList(query);
}
@Override
public DeviceParamSpcReportVo getSpcReport(DeviceParamReportQuery query) {
normalizeQuery(query);
List<DeviceParamSpcPointVo> points = deviceParamAnalysisMapper.selectSpcPoints(query);
DeviceParamSpcReportVo reportVo = new DeviceParamSpcReportVo();
reportVo.setPoints(points != null ? points : new ArrayList<DeviceParamSpcPointVo>());
if (points == null || points.isEmpty()) {
reportVo.setSampleSize(0);
reportVo.setOutOfControlCount(0);
return reportVo;
}
DeviceParamSpcPointVo firstPoint = points.get(0);
reportVo.setDeviceCode(firstPoint.getDeviceCode());
reportVo.setDeviceName(firstPoint.getDeviceName());
reportVo.setParamCode(firstPoint.getParamCode());
reportVo.setParamName(firstPoint.getParamName());
reportVo.setUpperLimit(firstPoint.getUpperLimit());
reportVo.setLowerLimit(firstPoint.getLowerLimit());
reportVo.setSampleSize(points.size());
List<Double> values = new ArrayList<Double>(points.size());
double sum = 0D;
double max = Double.NEGATIVE_INFINITY;
double min = Double.POSITIVE_INFINITY;
for (DeviceParamSpcPointVo point : points) {
double value = point.getParamValue();
values.add(value);
sum += value;
max = Math.max(max, value);
min = Math.min(min, value);
}
// 这里把SPC口径固化在服务层是为了保证控制限和过程能力前后端只算一次。
double mean = sum / values.size();
double variance = 0D;
for (Double value : values) {
variance += Math.pow(value - mean, 2);
}
double stdDev = values.size() > 1 ? Math.sqrt(variance / values.size()) : 0D;
double cl = mean;
double ucl = mean + 3 * stdDev;
double lcl = mean - 3 * stdDev;
int outOfControlCount = 0;
for (Double value : values) {
if (value > ucl || value < lcl) {
outOfControlCount++;
}
}
reportVo.setMean(scale(mean));
reportVo.setMaxValue(scale(max));
reportVo.setMinValue(scale(min));
reportVo.setRangeValue(scale(max - min));
reportVo.setStdDev(scale(stdDev));
reportVo.setCoefficientVariation(mean == 0D ? 0D : scale(stdDev / mean * 100));
reportVo.setCl(scale(cl));
reportVo.setUcl(scale(ucl));
reportVo.setLcl(scale(lcl));
reportVo.setCpk(calcCpk(mean, stdDev, firstPoint.getUpperLimit(), firstPoint.getLowerLimit()));
reportVo.setOutOfControlCount(outOfControlCount);
return reportVo;
}
@Override
public List<DeviceParamSwitchTraceVo> selectSwitchTraceList(DeviceParamReportQuery query) {
normalizeQuery(query);
return deviceParamAnalysisMapper.selectSwitchTraceList(query);
}
/**
* SQL
*/
private void normalizeQuery(DeviceParamReportQuery query) {
if (query == null) {
return;
}
query.setScene(normalizeScene(query.getScene()));
query.setDeviceCode(trimToNull(query.getDeviceCode()));
query.setParamCode(trimToNull(query.getParamCode()));
query.setStartTime(normalizeDateTime(query.getStartTime()));
query.setEndTime(normalizeDateTime(query.getEndTime()));
query.setSwitchType(normalizeSwitchType(query.getSwitchType()));
Integer observeMinutes = query.getObserveMinutes();
if (observeMinutes == null) {
query.setObserveMinutes(30);
} else if (observeMinutes < 1) {
query.setObserveMinutes(1);
} else if (observeMinutes > 1440) {
// 观察窗口限制在1天内避免单次查询把整库相关数据扫出来。
query.setObserveMinutes(1440);
}
}
private String normalizeScene(String scene) {
if ("anomaly".equalsIgnoreCase(scene)) {
return "anomaly";
}
if ("switch".equalsIgnoreCase(scene)) {
return "switch";
}
return "spc";
}
private String normalizeSwitchType(String switchType) {
if (StringUtils.isBlank(switchType)) {
return "ALL";
}
String normalized = switchType.trim().toUpperCase();
if ("MOLD".equals(normalized) || "MATERIAL".equals(normalized) || "PRODUCT".equals(normalized)) {
return normalized;
}
return "ALL";
}
private String normalizeDateTime(String dateTime) {
if (StringUtils.isBlank(dateTime)) {
return null;
}
try {
LocalDateTime parsed = LocalDateTime.parse(dateTime.trim(), DATE_TIME_FORMATTER);
return DATE_TIME_FORMATTER.format(parsed);
} catch (DateTimeParseException ex) {
throw new IllegalArgumentException("时间格式必须为yyyy-MM-dd HH:mm:ss");
}
}
private String trimToNull(String text) {
return StringUtils.isBlank(text) ? null : text.trim();
}
private Double calcCpk(double mean, double stdDev, Double upperLimit, Double lowerLimit) {
if (stdDev <= 0D) {
return 0D;
}
Double cpu = null;
Double cpl = null;
if (upperLimit != null) {
cpu = (upperLimit - mean) / (3 * stdDev);
}
if (lowerLimit != null) {
cpl = (mean - lowerLimit) / (3 * stdDev);
}
if (cpu == null && cpl == null) {
return null;
}
if (cpu == null) {
return scale(cpl);
}
if (cpl == null) {
return scale(cpu);
}
return scale(Math.min(cpu, cpl));
}
private Double scale(Double value) {
if (value == null) {
return null;
}
return BigDecimal.valueOf(value).setScale(4, RoundingMode.HALF_UP).doubleValue();
}
}

@ -0,0 +1,317 @@
<?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.aucma.report.mapper.DeviceParamAnalysisMapper">
<select id="selectParamOptions" parameterType="com.aucma.report.domain.DeviceParamReportQuery"
resultType="com.aucma.report.domain.vo.DeviceParamOptionVo">
SELECT DISTINCT
p.param_code AS paramCode,
p.param_name AS paramName
FROM base_deviceparam p
WHERE NVL(p.is_flag, 0) = 1
<if test="deviceCode != null and deviceCode != ''">
AND p.device_code = #{deviceCode}
</if>
<choose>
<when test="scene == 'anomaly'">
AND p.alert_enabled = '1'
AND (p.upper_limit IS NOT NULL OR p.lower_limit IS NOT NULL)
AND p.param_type IN ('1', '2', '3', '4', '5')
</when>
<when test="scene == 'switch'">
AND (
p.param_name = '机台状态-模具数据名称'
OR p.param_name = '机台状态-物料描述'
OR p.param_name = '机台状态-产品描述'
OR p.param_name = '机台状态-产品描述1'
)
</when>
<otherwise>
AND p.param_type IN ('1', '2', '3', '4', '5')
</otherwise>
</choose>
ORDER BY p.param_name, p.param_code
</select>
<select id="selectParamAnomalyList" parameterType="com.aucma.report.domain.DeviceParamReportQuery"
resultType="com.aucma.report.domain.vo.DeviceParamAnomalyVo">
WITH source_data AS (
SELECT
v.device_code,
d.device_name,
v.param_code,
p.param_name,
p.alert_level,
p.upper_limit,
p.lower_limit,
p.read_frequency,
NVL(v.record_time, v.collect_time) AS collect_time,
TO_NUMBER(v.param_value) AS param_value_num
FROM base_device_param_val v
INNER JOIN base_deviceparam p
ON p.device_code = v.device_code
AND p.param_code = v.param_code
LEFT JOIN base_deviceledger d
ON d.device_code = v.device_code
WHERE NVL(p.is_flag, 0) = 1
AND p.alert_enabled = '1'
AND (p.upper_limit IS NOT NULL OR p.lower_limit IS NOT NULL)
AND REGEXP_LIKE(v.param_value, '^-?[0-9]+(\.[0-9]+)?$')
<if test="deviceCode != null and deviceCode != ''">
AND v.device_code = #{deviceCode}
</if>
<if test="paramCode != null and paramCode != ''">
AND v.param_code = #{paramCode}
</if>
<if test="startTime != null and startTime != ''">
AND NVL(v.record_time, v.collect_time) &gt;= TO_DATE(#{startTime}, 'YYYY-MM-DD HH24:MI:SS')
</if>
<if test="endTime != null and endTime != ''">
AND NVL(v.record_time, v.collect_time) &lt;= TO_DATE(#{endTime}, 'YYYY-MM-DD HH24:MI:SS')
</if>
),
tag_data AS (
SELECT
source_data.*,
CASE
WHEN upper_limit IS NOT NULL AND param_value_num &gt; upper_limit THEN 1
WHEN lower_limit IS NOT NULL AND param_value_num &lt; lower_limit THEN 1
ELSE 0
END AS abnormal_flag
FROM source_data
),
seq_data AS (
SELECT
tag_data.*,
CASE
WHEN abnormal_flag = 1
AND NVL(LAG(abnormal_flag) OVER (PARTITION BY device_code, param_code ORDER BY collect_time), 0) = 0
THEN 1
ELSE 0
END AS group_start_flag
FROM tag_data
),
abnormal_data AS (
SELECT
seq_data.*,
SUM(group_start_flag) OVER (
PARTITION BY device_code, param_code
ORDER BY collect_time
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS group_no
FROM seq_data
WHERE abnormal_flag = 1
),
event_data AS (
/* 这里先按连续异常段分组,后面再汇总成设备+参数报表,避免一次超限被重复算多次。 */
SELECT
device_code,
device_name,
param_code,
param_name,
alert_level,
upper_limit,
lower_limit,
MAX(read_frequency) AS read_frequency,
group_no,
MIN(collect_time) AS event_start_time,
MAX(collect_time) AS event_end_time,
MAX(param_value_num) AS event_max_value,
MIN(param_value_num) AS event_min_value,
MAX(CASE WHEN upper_limit IS NOT NULL AND param_value_num &gt; upper_limit THEN 1 ELSE 0 END) AS has_upper_abnormal,
MAX(CASE WHEN lower_limit IS NOT NULL AND param_value_num &lt; lower_limit THEN 1 ELSE 0 END) AS has_lower_abnormal
FROM abnormal_data
GROUP BY device_code, device_name, param_code, param_name, alert_level, upper_limit, lower_limit, group_no
)
SELECT
device_code AS deviceCode,
device_name AS deviceName,
param_code AS paramCode,
param_name AS paramName,
alert_level AS alertLevel,
CASE alert_level
WHEN '1' THEN '一般'
WHEN '2' THEN '重要'
WHEN '3' THEN '紧急'
ELSE '未配置'
END AS alertLevelName,
upper_limit AS upperLimit,
lower_limit AS lowerLimit,
COUNT(1) AS overLimitCount,
SUM(has_upper_abnormal) AS upperAnomalyCount,
SUM(has_lower_abnormal) AS lowerAnomalyCount,
ROUND(SUM(
CASE
WHEN event_end_time &gt; event_start_time THEN (event_end_time - event_start_time) * 24 * 60
ELSE NVL(read_frequency, 60000) / 60000
END
), 2) AS abnormalDurationMinutes,
MAX(event_max_value) AS maxValue,
MIN(event_min_value) AS minValue,
MIN(event_start_time) AS firstAbnormalTime,
MAX(event_end_time) AS lastAbnormalTime
FROM event_data
GROUP BY
device_code, device_name, param_code, param_name, alert_level, upper_limit, lower_limit
ORDER BY overLimitCount DESC, abnormalDurationMinutes DESC, lastAbnormalTime DESC
</select>
<select id="selectSpcPoints" parameterType="com.aucma.report.domain.DeviceParamReportQuery"
resultType="com.aucma.report.domain.vo.DeviceParamSpcPointVo">
SELECT *
FROM (
SELECT
v.device_code AS deviceCode,
d.device_name AS deviceName,
v.param_code AS paramCode,
p.param_name AS paramName,
p.upper_limit AS upperLimit,
p.lower_limit AS lowerLimit,
NVL(v.record_time, v.collect_time) AS collectTime,
TO_NUMBER(v.param_value) AS paramValue
FROM base_device_param_val v
LEFT JOIN base_deviceparam p
ON p.device_code = v.device_code
AND p.param_code = v.param_code
LEFT JOIN base_deviceledger d
ON d.device_code = v.device_code
WHERE REGEXP_LIKE(v.param_value, '^-?[0-9]+(\.[0-9]+)?$')
AND v.device_code = #{deviceCode}
AND v.param_code = #{paramCode}
<if test="startTime != null and startTime != ''">
AND NVL(v.record_time, v.collect_time) &gt;= TO_DATE(#{startTime}, 'YYYY-MM-DD HH24:MI:SS')
</if>
<if test="endTime != null and endTime != ''">
AND NVL(v.record_time, v.collect_time) &lt;= TO_DATE(#{endTime}, 'YYYY-MM-DD HH24:MI:SS')
</if>
ORDER BY NVL(v.record_time, v.collect_time) ASC
)
WHERE ROWNUM &lt;= 1500
</select>
<select id="selectSwitchTraceList" parameterType="com.aucma.report.domain.DeviceParamReportQuery"
resultType="com.aucma.report.domain.vo.DeviceParamSwitchTraceVo">
WITH source_data AS (
SELECT
v.device_code,
d.device_name,
v.param_code,
v.param_name,
v.param_value,
NVL(v.record_time, v.collect_time) AS collect_time,
LAG(v.param_value) OVER (
PARTITION BY v.device_code, v.param_code
ORDER BY NVL(v.record_time, v.collect_time)
) AS prev_value
FROM base_device_param_val v
LEFT JOIN base_deviceledger d
ON d.device_code = v.device_code
WHERE 1 = 1
<if test="deviceCode != null and deviceCode != ''">
AND v.device_code = #{deviceCode}
</if>
<if test="startTime != null and startTime != ''">
AND NVL(v.record_time, v.collect_time) &gt;= TO_DATE(#{startTime}, 'YYYY-MM-DD HH24:MI:SS')
</if>
<if test="endTime != null and endTime != ''">
AND NVL(v.record_time, v.collect_time) &lt;= TO_DATE(#{endTime}, 'YYYY-MM-DD HH24:MI:SS')
</if>
<choose>
<when test="switchType == 'MOLD'">
AND v.param_name = '机台状态-模具数据名称'
</when>
<when test="switchType == 'MATERIAL'">
AND v.param_name = '机台状态-物料描述'
</when>
<when test="switchType == 'PRODUCT'">
AND (v.param_name = '机台状态-产品描述' OR v.param_name = '机台状态-产品描述1')
</when>
<otherwise>
AND (
v.param_name = '机台状态-模具数据名称'
OR v.param_name = '机台状态-物料描述'
OR v.param_name = '机台状态-产品描述'
OR v.param_name = '机台状态-产品描述1'
)
</otherwise>
</choose>
),
switch_events AS (
SELECT
device_code,
device_name,
param_code,
param_name,
collect_time,
prev_value,
param_value,
CASE
WHEN param_name = '机台状态-模具数据名称' THEN '模具'
WHEN param_name = '机台状态-物料描述' THEN '物料'
ELSE '产品'
END AS switch_type
FROM source_data
WHERE prev_value IS NOT NULL
AND NVL(prev_value, '#') &lt;&gt; NVL(param_value, '#')
)
SELECT
e.device_code AS deviceCode,
e.device_name AS deviceName,
e.switch_type AS switchType,
e.param_code AS paramCode,
e.param_name AS paramName,
e.collect_time AS changeTime,
e.prev_value AS beforeValue,
e.param_value AS afterValue,
#{observeMinutes} AS observeMinutes,
NVL((
SELECT ROUND(MAX(TO_NUMBER(p.param_value)) - MIN(TO_NUMBER(p.param_value)), 2)
FROM base_device_param_val p
WHERE p.device_code = e.device_code
AND p.param_name = '机台状态-实际产出数量'
AND REGEXP_LIKE(p.param_value, '^-?[0-9]+(\.[0-9]+)?$')
AND NVL(p.record_time, p.collect_time) BETWEEN e.collect_time AND e.collect_time + (#{observeMinutes} / 1440)
), 0) AS outputIncrement,
NVL((
SELECT COUNT(1)
FROM base_device_param_val av
INNER JOIN base_deviceparam ap
ON ap.device_code = av.device_code
AND ap.param_code = av.param_code
WHERE av.device_code = e.device_code
AND NVL(ap.is_flag, 0) = 1
AND ap.alert_enabled = '1'
AND REGEXP_LIKE(av.param_value, '^-?[0-9]+(\.[0-9]+)?$')
AND NVL(av.record_time, av.collect_time) BETWEEN e.collect_time AND e.collect_time + (#{observeMinutes} / 1440)
AND (
(ap.upper_limit IS NOT NULL AND TO_NUMBER(av.param_value) &gt; ap.upper_limit)
OR (ap.lower_limit IS NOT NULL AND TO_NUMBER(av.param_value) &lt; ap.lower_limit)
)
), 0) AS abnormalCount,
NVL((
SELECT device_status
FROM (
SELECT
CASE s.param_name
WHEN '机台状态-三色灯机器运行' THEN '运行'
WHEN '机台状态-三色灯机器暂停' THEN '停机'
WHEN '机台状态-三色灯机器待机' THEN '待机'
WHEN '机台状态-三色灯机器报警' THEN '报警'
ELSE '未知'
END AS device_status
FROM base_device_param_val s
WHERE s.device_code = e.device_code
AND s.param_name IN ('机台状态-三色灯机器运行', '机台状态-三色灯机器暂停', '机台状态-三色灯机器待机', '机台状态-三色灯机器报警')
AND UPPER(s.param_value) = 'TRUE'
AND NVL(s.record_time, s.collect_time) BETWEEN e.collect_time AND e.collect_time + (#{observeMinutes} / 1440)
ORDER BY NVL(s.record_time, s.collect_time) DESC
)
WHERE ROWNUM = 1
), '未知') AS latestStatus
FROM switch_events e
ORDER BY e.collect_time DESC
</select>
</mapper>

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save