二级系统联合寻找数据
This commit is contained in:
@@ -47,5 +47,10 @@
|
|||||||
<artifactId>fastjson2</artifactId>
|
<artifactId>fastjson2</artifactId>
|
||||||
<version>2.0.35</version>
|
<version>2.0.35</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!-- Word 导出(docx) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.apache.poi</groupId>
|
||||||
|
<artifactId>poi-ooxml</artifactId>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -5,14 +5,23 @@ import com.klp.common.core.controller.BaseController;
|
|||||||
import com.klp.common.core.domain.R;
|
import com.klp.common.core.domain.R;
|
||||||
import com.klp.common.core.page.TableDataInfo;
|
import com.klp.common.core.page.TableDataInfo;
|
||||||
import com.klp.common.utils.StringUtils;
|
import com.klp.common.utils.StringUtils;
|
||||||
|
import com.klp.da.service.OeeWordAiAnalysisService;
|
||||||
import com.klp.da.service.OeeReportJobService;
|
import com.klp.da.service.OeeReportJobService;
|
||||||
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
||||||
|
import com.klp.pocket.acid.domain.vo.AcidOeeIdealCycleVo;
|
||||||
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
|
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
|
||||||
import com.klp.pocket.acid.domain.vo.AcidOeeRegressionVo;
|
|
||||||
import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo;
|
import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo;
|
||||||
import com.klp.pocket.acid.service.IAcidOeeService;
|
import com.klp.pocket.acid.service.IAcidOeeService;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
|
||||||
|
import org.apache.poi.xwpf.usermodel.XWPFDocument;
|
||||||
|
import org.apache.poi.xwpf.usermodel.XWPFParagraph;
|
||||||
|
import org.apache.poi.xwpf.usermodel.XWPFRun;
|
||||||
|
import org.apache.poi.xwpf.usermodel.XWPFTable;
|
||||||
|
import org.apache.poi.xwpf.usermodel.XWPFTableCell;
|
||||||
|
import org.apache.poi.xwpf.usermodel.XWPFTableRow;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
import org.springframework.validation.annotation.Validated;
|
import org.springframework.validation.annotation.Validated;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -23,11 +32,17 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
|||||||
import org.springframework.web.bind.annotation.RequestParam;
|
import org.springframework.web.bind.annotation.RequestParam;
|
||||||
import org.springframework.web.bind.annotation.RestController;
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import javax.servlet.ServletOutputStream;
|
||||||
|
import javax.servlet.http.HttpServletResponse;
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.time.LocalDate;
|
import java.time.LocalDate;
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.time.format.DateTimeFormatter;
|
import java.time.format.DateTimeFormatter;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Locale;
|
||||||
import java.util.UUID;
|
import java.util.UUID;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
@@ -48,6 +63,7 @@ public class OeeReportController extends BaseController {
|
|||||||
private final IAcidOeeService acidOeeService;
|
private final IAcidOeeService acidOeeService;
|
||||||
private final StringRedisTemplate stringRedisTemplate;
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
private final OeeReportJobService oeeReportJobService;
|
private final OeeReportJobService oeeReportJobService;
|
||||||
|
private final OeeWordAiAnalysisService oeeWordAiAnalysisService;
|
||||||
|
|
||||||
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
|
||||||
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
|
||||||
@@ -177,21 +193,233 @@ public class OeeReportController extends BaseController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 酸轧线理论节拍回归数据
|
* 酸轧线理论节拍(统计口径:历史优良日中位数)
|
||||||
*
|
*
|
||||||
* 路由:GET /oee/line/acid/regression
|
* 路由:GET /oee/line/acid/idealCycle
|
||||||
* 说明:
|
|
||||||
* - {@code startDate} / {@code endDate} 可选,若为空则由 pocket 按“近6个月”默认处理。
|
|
||||||
*/
|
*/
|
||||||
@GetMapping("/acid/regression")
|
@GetMapping("/acid/idealCycle")
|
||||||
public R<AcidOeeRegressionVo> getAcidRegression(
|
public R<AcidOeeIdealCycleVo> getAcidIdealCycle(
|
||||||
@RequestParam(required = false) String startDate,
|
@RequestParam String startDate,
|
||||||
@RequestParam(required = false) String endDate
|
@RequestParam String endDate
|
||||||
) {
|
) {
|
||||||
AcidOeeRegressionVo data = acidOeeService.getRegressionData(startDate, endDate);
|
AcidOeeIdealCycleVo data = acidOeeService.getIdealCycle(startDate, endDate);
|
||||||
return R.ok(data);
|
return R.ok(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导出酸轧线 OEE 报表 Word(docx)
|
||||||
|
*
|
||||||
|
* 路由:GET /oee/line/acid/exportWord?startDate=yyyy-MM-dd&endDate=yyyy-MM-dd
|
||||||
|
* 说明:
|
||||||
|
* - 文档结构尽量贴近前端页面(表格为主,图表不嵌入);
|
||||||
|
* - 将右侧“公式与口径说明”置于文档最上方。
|
||||||
|
*/
|
||||||
|
@GetMapping("/acid/exportWord")
|
||||||
|
public void exportAcidWord(
|
||||||
|
@RequestParam String startDate,
|
||||||
|
@RequestParam String endDate,
|
||||||
|
HttpServletResponse response
|
||||||
|
) throws IOException {
|
||||||
|
String[] range = resolveDateRange(startDate, endDate);
|
||||||
|
String start = range[0];
|
||||||
|
String end = range[1];
|
||||||
|
|
||||||
|
// 取数:按选择范围实时拉取(避免 summary/loss7 的“当月固定”逻辑)
|
||||||
|
List<AcidOeeDailySummaryVo> summary = acidOeeService.getDailySummary(start, end);
|
||||||
|
List<AcidOeeLoss7Vo> loss7 = acidOeeService.getLoss7Summary(start, end);
|
||||||
|
List<Klptcm1ProStoppageVo> events = acidOeeService.getStoppageEvents(start, end);
|
||||||
|
AcidOeeIdealCycleVo idealCycle = acidOeeService.getIdealCycle(start, end);
|
||||||
|
|
||||||
|
XWPFDocument doc = new XWPFDocument();
|
||||||
|
|
||||||
|
// 标题
|
||||||
|
addTitle(doc, String.format("酸轧线OEE报表(%s ~ %s)", start, end));
|
||||||
|
|
||||||
|
// 口径说明(置顶)
|
||||||
|
addSectionTitle(doc, "公式与口径说明");
|
||||||
|
addParagraph(doc, "OEE 总公式:OEE = A × P × Q", true);
|
||||||
|
addBullet(doc, "A(时间稼动率) = (负荷时间 − 停机时间) / 负荷时间");
|
||||||
|
addBullet(doc, "P(性能稼动率,吨维度) = (理论节拍 × 产量吨) / 实际运转时间");
|
||||||
|
addBullet(doc, "Q(良品率) = 良品吨 / 总产量吨");
|
||||||
|
addParagraph(doc, "关键字段定义:", true);
|
||||||
|
addBullet(doc, "负荷时间:计划生产时间扣除计划停机后的时间");
|
||||||
|
addBullet(doc, "停机时间:所有停机/中断(按 stop_type 汇总)的总时长");
|
||||||
|
addBullet(doc, "实际运转时间:负荷时间 − 停机时间");
|
||||||
|
addBullet(doc, "理论节拍:按“优良日统计口径”得到的稳定节拍(分钟/吨)");
|
||||||
|
addBullet(doc, "良品/次品:按 WMS quality_status 判断,C+/C/C-/D+/D/D- 为次品");
|
||||||
|
|
||||||
|
if (idealCycle != null) {
|
||||||
|
addParagraph(doc, "当前理论节拍(统计口径):", true);
|
||||||
|
XWPFTable t = doc.createTable(5, 2);
|
||||||
|
setTableHeaderRow(t.getRow(0), "指标", "值");
|
||||||
|
setTableRow(t.getRow(1), "理论节拍 (min/吨)", fmtNum(getIdealCycleTime(idealCycle), 2));
|
||||||
|
setTableRow(t.getRow(2), "生产节拍中位数 (min/吨)", fmtNum(getMedianCycleTime(idealCycle), 2));
|
||||||
|
setTableRow(t.getRow(3), "样本天数", String.valueOf(getSampleDays(idealCycle)));
|
||||||
|
setTableRow(t.getRow(4), "吨数下限 (吨)", fmtNum(getMinWeightTon(idealCycle), 2));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 一、KPI 总览
|
||||||
|
addSectionTitle(doc, "一、KPI 总览(酸轧线 SY)");
|
||||||
|
addParagraph(doc, "展示所选期间酸轧线整体 OEE 及 A/P/Q 等关键指标,用于快速判断本期综合表现好坏。", false);
|
||||||
|
AcidKpiAgg kpi = calcKpi(summary);
|
||||||
|
XWPFTable kpiTable = doc.createTable(2, 11);
|
||||||
|
setTableRow(kpiTable.getRow(0),
|
||||||
|
"OEE (%)",
|
||||||
|
"时间稼动率 A (%)",
|
||||||
|
"性能稼动率 P_ton (%)",
|
||||||
|
"良品率 Q (%)",
|
||||||
|
"负荷时间 (min)",
|
||||||
|
"停机时间 (min)",
|
||||||
|
"运转时间 (min)",
|
||||||
|
"总产量 (吨)",
|
||||||
|
"总产量 (卷)",
|
||||||
|
"良品量 (吨)",
|
||||||
|
"次品量 (吨)"
|
||||||
|
);
|
||||||
|
setTableRow(kpiTable.getRow(1),
|
||||||
|
fmtPercent1(kpi.oee),
|
||||||
|
fmtPercent1(kpi.availability),
|
||||||
|
fmtPercent1(kpi.performanceTon),
|
||||||
|
fmtPercent1(kpi.quality),
|
||||||
|
fmtInt(kpi.loadingTimeMin),
|
||||||
|
fmtInt(kpi.downtimeMin),
|
||||||
|
fmtInt(kpi.runTimeMin),
|
||||||
|
fmtNum(kpi.totalOutputTon, 2),
|
||||||
|
fmtInt(kpi.totalOutputCoil),
|
||||||
|
fmtNum(kpi.goodOutputTon, 2),
|
||||||
|
fmtNum(kpi.defectOutputTon, 2)
|
||||||
|
);
|
||||||
|
addAiAnalysisIfEnabled(doc, "KPI 总览", buildMarkdownTable(
|
||||||
|
new String[]{"OEE(%)", "A(%)", "P_ton(%)", "Q(%)", "负荷(min)", "停机(min)", "运转(min)", "总产(吨)", "总产(卷)", "良品(吨)", "次品(吨)"},
|
||||||
|
new String[][]{{
|
||||||
|
fmtPercent1(kpi.oee),
|
||||||
|
fmtPercent1(kpi.availability),
|
||||||
|
fmtPercent1(kpi.performanceTon),
|
||||||
|
fmtPercent1(kpi.quality),
|
||||||
|
fmtInt(kpi.loadingTimeMin),
|
||||||
|
fmtInt(kpi.downtimeMin),
|
||||||
|
fmtInt(kpi.runTimeMin),
|
||||||
|
fmtNum(kpi.totalOutputTon, 2),
|
||||||
|
fmtInt(kpi.totalOutputCoil),
|
||||||
|
fmtNum(kpi.goodOutputTon, 2),
|
||||||
|
fmtNum(kpi.defectOutputTon, 2)
|
||||||
|
}}), "区间:" + start + " ~ " + end);
|
||||||
|
|
||||||
|
// 二、日明细
|
||||||
|
addSectionTitle(doc, "二、日明细(用于趋势分析)");
|
||||||
|
addParagraph(doc, "按天拆分 A/P/Q 及产量等指标,用于观察趋势、波动点以及与重大事件的对应关系。", false);
|
||||||
|
XWPFTable dailyTable = doc.createTable(Math.max(1, summary.size()) + 1, 11);
|
||||||
|
setTableRow(dailyTable.getRow(0),
|
||||||
|
"日期",
|
||||||
|
"OEE (%)",
|
||||||
|
"A (%)",
|
||||||
|
"P_ton (%)",
|
||||||
|
"Q (%)",
|
||||||
|
"负荷 (min)",
|
||||||
|
"停机 (min)",
|
||||||
|
"运转 (min)",
|
||||||
|
"总产量 (吨)",
|
||||||
|
"总产量 (卷)",
|
||||||
|
"良品 (吨)"
|
||||||
|
);
|
||||||
|
for (int i = 0; i < summary.size(); i++) {
|
||||||
|
AcidOeeDailySummaryVo row = summary.get(i);
|
||||||
|
setTableRow(dailyTable.getRow(i + 1),
|
||||||
|
safeStr(row.getStatDate()),
|
||||||
|
fmtPercent1(row.getOee()),
|
||||||
|
fmtPercent1(row.getAvailability()),
|
||||||
|
fmtPercent1(row.getPerformanceTon()),
|
||||||
|
fmtPercent1(row.getQuality()),
|
||||||
|
fmtInt(row.getLoadingTimeMin()),
|
||||||
|
fmtInt(row.getDowntimeMin()),
|
||||||
|
fmtInt(row.getRunTimeMin()),
|
||||||
|
fmtNum(row.getTotalOutputTon(), 2),
|
||||||
|
fmtInt(row.getTotalOutputCoil()),
|
||||||
|
fmtNum(row.getGoodOutputTon(), 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
addAiAnalysisIfEnabled(doc, "日明细", buildDailySummaryMarkdown(summary, 31), "区间:" + start + " ~ " + end);
|
||||||
|
|
||||||
|
// 三、理论节拍(说明 + 表)
|
||||||
|
addSectionTitle(doc, "三、理论节拍(统计口径)");
|
||||||
|
addParagraph(doc, "基于历史“优良日”统计得到的理论节拍(中位数),用于作为性能稼动率计算的稳定标尺。", false);
|
||||||
|
if (idealCycle == null) {
|
||||||
|
addParagraph(doc, "(本区间未获取到理论节拍数据)", false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 四、7 大损失
|
||||||
|
addSectionTitle(doc, "四、7 大损失汇总(按 stop_type 分类)");
|
||||||
|
addParagraph(doc, "将所有停机事件按 stop_type 归类,统计时间占比和次数,用于确定“先从哪几类损失下手改善”。", false);
|
||||||
|
XWPFTable loss7Table = doc.createTable(Math.max(1, loss7.size()) + 1, 5);
|
||||||
|
setTableRow(loss7Table.getRow(0),
|
||||||
|
"损失类别",
|
||||||
|
"损失时间 (min)",
|
||||||
|
"占比 (%)",
|
||||||
|
"次数",
|
||||||
|
"平均时长 (min)"
|
||||||
|
);
|
||||||
|
for (int i = 0; i < loss7.size(); i++) {
|
||||||
|
AcidOeeLoss7Vo row = loss7.get(i);
|
||||||
|
setTableRow(loss7Table.getRow(i + 1),
|
||||||
|
safeStr(row.getLossCategoryName()),
|
||||||
|
fmtNum(row.getLossTimeMin(), 2),
|
||||||
|
fmtPercent1(row.getLossTimeRate()),
|
||||||
|
fmtInt(row.getCount()),
|
||||||
|
fmtNum(row.getAvgDurationMin(), 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
addAiAnalysisIfEnabled(doc, "7 大损失汇总", buildLoss7Markdown(loss7, 30), "区间:" + start + " ~ " + end);
|
||||||
|
|
||||||
|
// 五、停机/损失事件明细
|
||||||
|
addSectionTitle(doc, "五、停机/损失事件明细");
|
||||||
|
addParagraph(doc, "罗列每一条停机/损失事件,包含时间段、区域、机组和备注,方便对照现场记录进行原因分析。", false);
|
||||||
|
int eventRows = Math.max(1, events.size()) + 1;
|
||||||
|
XWPFTable eventTable = doc.createTable(eventRows, 9);
|
||||||
|
setTableRow(eventTable.getRow(0),
|
||||||
|
"开始时间",
|
||||||
|
"结束时间",
|
||||||
|
"时长 (s)",
|
||||||
|
"时长 (min)",
|
||||||
|
"停机类型 (stop_type)",
|
||||||
|
"区域",
|
||||||
|
"机组",
|
||||||
|
"班次",
|
||||||
|
"备注"
|
||||||
|
);
|
||||||
|
for (int i = 0; i < events.size(); i++) {
|
||||||
|
Klptcm1ProStoppageVo e = events.get(i);
|
||||||
|
long durationSec = safeLong(e.getDuration());
|
||||||
|
double durationMin = durationSec / 60d;
|
||||||
|
setTableRow(eventTable.getRow(i + 1),
|
||||||
|
safeStr(e.getStartDate()),
|
||||||
|
safeStr(e.getEndDate()),
|
||||||
|
String.valueOf(durationSec),
|
||||||
|
fmtNum(durationMin, 3),
|
||||||
|
safeStr(e.getStopType()),
|
||||||
|
safeStr(e.getArea()),
|
||||||
|
safeStr(e.getUnit()),
|
||||||
|
safeStr(e.getShift()),
|
||||||
|
safeStr(e.getRemark())
|
||||||
|
);
|
||||||
|
}
|
||||||
|
addAiAnalysisIfEnabled(doc, "停机/损失事件明细", buildEventsMarkdown(events, 40), "区间:" + start + " ~ " + end);
|
||||||
|
|
||||||
|
String filename = String.format("酸轧线OEE报表_%s_%s.docx", start, end);
|
||||||
|
String encoded = URLEncoder.encode(filename, StandardCharsets.UTF_8.name());
|
||||||
|
response.setCharacterEncoding(StandardCharsets.UTF_8.name());
|
||||||
|
response.setContentType("application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||||||
|
response.setHeader(HttpHeaders.CONTENT_DISPOSITION,
|
||||||
|
"attachment; filename*=UTF-8''" + encoded);
|
||||||
|
response.setHeader(HttpHeaders.CACHE_CONTROL, "no-store, no-cache");
|
||||||
|
|
||||||
|
try (ServletOutputStream os = response.getOutputStream()) {
|
||||||
|
doc.write(os);
|
||||||
|
os.flush();
|
||||||
|
} finally {
|
||||||
|
doc.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 若未显式传入日期范围,则默认当前月 [1号, 今天]。
|
* 若未显式传入日期范围,则默认当前月 [1号, 今天]。
|
||||||
*/
|
*/
|
||||||
@@ -219,6 +447,315 @@ public class OeeReportController extends BaseController {
|
|||||||
return isStart ? firstDay.format(DATE_FORMATTER) : today.format(DATE_FORMATTER);
|
return isStart ? firstDay.format(DATE_FORMATTER) : today.format(DATE_FORMATTER);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ======================== Word 生成辅助方法 ========================
|
||||||
|
|
||||||
|
private void addTitle(XWPFDocument doc, String text) {
|
||||||
|
XWPFParagraph p = doc.createParagraph();
|
||||||
|
p.setAlignment(ParagraphAlignment.CENTER);
|
||||||
|
XWPFRun r = p.createRun();
|
||||||
|
r.setBold(true);
|
||||||
|
r.setFontSize(16);
|
||||||
|
r.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addSectionTitle(XWPFDocument doc, String text) {
|
||||||
|
XWPFParagraph p = doc.createParagraph();
|
||||||
|
p.setSpacingBefore(200);
|
||||||
|
XWPFRun r = p.createRun();
|
||||||
|
r.setBold(true);
|
||||||
|
r.setFontSize(12);
|
||||||
|
r.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addParagraph(XWPFDocument doc, String text, boolean bold) {
|
||||||
|
XWPFParagraph p = doc.createParagraph();
|
||||||
|
XWPFRun r = p.createRun();
|
||||||
|
r.setBold(bold);
|
||||||
|
r.setFontSize(11);
|
||||||
|
r.setText(text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addBullet(XWPFDocument doc, String text) {
|
||||||
|
XWPFParagraph p = doc.createParagraph();
|
||||||
|
XWPFRun r = p.createRun();
|
||||||
|
r.setFontSize(11);
|
||||||
|
r.setText("• " + text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addAiAnalysisIfEnabled(XWPFDocument doc, String tableTitle, String tableMarkdown, String extraContext) {
|
||||||
|
String analysis = null;
|
||||||
|
try {
|
||||||
|
analysis = oeeWordAiAnalysisService.analyzeTable(tableTitle, tableMarkdown, extraContext);
|
||||||
|
} catch (Exception e) {
|
||||||
|
// 不影响导出
|
||||||
|
}
|
||||||
|
if (StringUtils.isNotBlank(analysis)) {
|
||||||
|
addParagraph(doc, "AI 分析:", true);
|
||||||
|
// 多行按段落输出
|
||||||
|
String[] lines = analysis.split("\\r?\\n");
|
||||||
|
for (String line : lines) {
|
||||||
|
if (StringUtils.isNotBlank(line)) {
|
||||||
|
addBullet(doc, line.trim().replaceFirst("^[\\-•\\*\\d\\.\\s]+", ""));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildMarkdownTable(String[] headers, String[][] rows) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("|");
|
||||||
|
for (String h : headers) sb.append(escapePipe(h)).append("|");
|
||||||
|
sb.append("\n|");
|
||||||
|
for (int i = 0; i < headers.length; i++) sb.append("---|");
|
||||||
|
sb.append("\n");
|
||||||
|
for (String[] r : rows) {
|
||||||
|
sb.append("|");
|
||||||
|
for (String c : r) sb.append(escapePipe(c)).append("|");
|
||||||
|
sb.append("\n");
|
||||||
|
}
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private String escapePipe(String s) {
|
||||||
|
if (s == null) return "";
|
||||||
|
return s.replace("|", "\\|");
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildDailySummaryMarkdown(List<AcidOeeDailySummaryVo> list, int maxRows) {
|
||||||
|
List<AcidOeeDailySummaryVo> src = list == null ? new ArrayList<>() : list;
|
||||||
|
int n = Math.min(src.size(), Math.max(1, maxRows));
|
||||||
|
String[] headers = new String[]{"日期", "OEE(%)", "A(%)", "P_ton(%)", "Q(%)", "负荷(min)", "停机(min)", "运转(min)", "总产(吨)", "良品(吨)"};
|
||||||
|
String[][] rows = new String[n][headers.length];
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
AcidOeeDailySummaryVo r = src.get(i);
|
||||||
|
rows[i] = new String[]{
|
||||||
|
safeStr(r.getStatDate()),
|
||||||
|
fmtPercent1(r.getOee()),
|
||||||
|
fmtPercent1(r.getAvailability()),
|
||||||
|
fmtPercent1(r.getPerformanceTon()),
|
||||||
|
fmtPercent1(r.getQuality()),
|
||||||
|
fmtInt(r.getLoadingTimeMin()),
|
||||||
|
fmtInt(r.getDowntimeMin()),
|
||||||
|
fmtInt(r.getRunTimeMin()),
|
||||||
|
fmtNum(r.getTotalOutputTon(), 2),
|
||||||
|
fmtNum(r.getGoodOutputTon(), 2)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
String md = buildMarkdownTable(headers, rows);
|
||||||
|
if (src.size() > n) {
|
||||||
|
md += "\n(仅展示前 " + n + " 行,已截断)\n";
|
||||||
|
}
|
||||||
|
return md;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildLoss7Markdown(List<AcidOeeLoss7Vo> list, int maxRows) {
|
||||||
|
List<AcidOeeLoss7Vo> src = list == null ? new ArrayList<>() : list;
|
||||||
|
int n = Math.min(src.size(), Math.max(1, maxRows));
|
||||||
|
String[] headers = new String[]{"损失类别", "损失时间(min)", "占比(%)", "次数", "平均时长(min)"};
|
||||||
|
String[][] rows = new String[n][headers.length];
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
AcidOeeLoss7Vo r = src.get(i);
|
||||||
|
rows[i] = new String[]{
|
||||||
|
safeStr(r.getLossCategoryName()),
|
||||||
|
fmtNum(r.getLossTimeMin(), 2),
|
||||||
|
fmtPercent1(r.getLossTimeRate()),
|
||||||
|
fmtInt(r.getCount()),
|
||||||
|
fmtNum(r.getAvgDurationMin(), 2)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
String md = buildMarkdownTable(headers, rows);
|
||||||
|
if (src.size() > n) {
|
||||||
|
md += "\n(仅展示前 " + n + " 行,已截断)\n";
|
||||||
|
}
|
||||||
|
return md;
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildEventsMarkdown(List<Klptcm1ProStoppageVo> list, int maxRows) {
|
||||||
|
List<Klptcm1ProStoppageVo> src = list == null ? new ArrayList<>() : list;
|
||||||
|
int n = Math.min(src.size(), Math.max(1, maxRows));
|
||||||
|
String[] headers = new String[]{"开始", "结束", "时长(s)", "时长(min)", "停机类型", "区域", "机组", "备注"};
|
||||||
|
String[][] rows = new String[n][headers.length];
|
||||||
|
for (int i = 0; i < n; i++) {
|
||||||
|
Klptcm1ProStoppageVo e = src.get(i);
|
||||||
|
long durationSec = safeLong(e.getDuration());
|
||||||
|
rows[i] = new String[]{
|
||||||
|
safeStr(e.getStartDate()),
|
||||||
|
safeStr(e.getEndDate()),
|
||||||
|
String.valueOf(durationSec),
|
||||||
|
fmtNum(durationSec / 60d, 3),
|
||||||
|
safeStr(e.getStopType()),
|
||||||
|
safeStr(e.getArea()),
|
||||||
|
safeStr(e.getUnit()),
|
||||||
|
safeStr(e.getRemark())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
String md = buildMarkdownTable(headers, rows);
|
||||||
|
md += "\n总事件数:" + src.size() + "\n";
|
||||||
|
if (src.size() > n) {
|
||||||
|
md += "(仅展示前 " + n + " 行,已截断)\n";
|
||||||
|
}
|
||||||
|
return md;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTableHeaderRow(XWPFTableRow row, String c1, String c2) {
|
||||||
|
setCellText(row.getCell(0), c1, true);
|
||||||
|
setCellText(row.getCell(1), c2, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setTableRow(XWPFTableRow row, String... cols) {
|
||||||
|
for (int i = 0; i < cols.length; i++) {
|
||||||
|
XWPFTableCell cell = row.getCell(i);
|
||||||
|
if (cell == null) {
|
||||||
|
cell = row.createCell();
|
||||||
|
}
|
||||||
|
setCellText(cell, cols[i], false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setCellText(XWPFTableCell cell, String text, boolean bold) {
|
||||||
|
cell.removeParagraph(0);
|
||||||
|
XWPFParagraph p = cell.addParagraph();
|
||||||
|
XWPFRun r = p.createRun();
|
||||||
|
r.setBold(bold);
|
||||||
|
r.setFontSize(10);
|
||||||
|
r.setText(text == null ? "" : text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String safeStr(Object o) {
|
||||||
|
return o == null ? "" : String.valueOf(o);
|
||||||
|
}
|
||||||
|
|
||||||
|
private long safeLong(Object o) {
|
||||||
|
if (o == null) return 0L;
|
||||||
|
try {
|
||||||
|
return Long.parseLong(String.valueOf(o));
|
||||||
|
} catch (Exception ex) {
|
||||||
|
return 0L;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fmtPercent1(Object value) {
|
||||||
|
Double v = toDouble(value);
|
||||||
|
if (v == null) return "-";
|
||||||
|
return String.format(Locale.ROOT, "%.1f", v);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fmtNum(Object value, int scale) {
|
||||||
|
Double v = toDouble(value);
|
||||||
|
if (v == null) return "-";
|
||||||
|
return String.format(Locale.ROOT, "%." + scale + "f", v);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String fmtInt(Object value) {
|
||||||
|
Double v = toDouble(value);
|
||||||
|
if (v == null) return "0";
|
||||||
|
return String.valueOf((long) Math.round(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
private Double toDouble(Object value) {
|
||||||
|
if (value == null) return null;
|
||||||
|
if (value instanceof Number) return ((Number) value).doubleValue();
|
||||||
|
String s = String.valueOf(value);
|
||||||
|
if (StringUtils.isBlank(s)) return null;
|
||||||
|
try {
|
||||||
|
return Double.parseDouble(s);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Data
|
||||||
|
private static class AcidKpiAgg {
|
||||||
|
private double oee;
|
||||||
|
private double availability;
|
||||||
|
private double performanceTon;
|
||||||
|
private double quality;
|
||||||
|
private double loadingTimeMin;
|
||||||
|
private double downtimeMin;
|
||||||
|
private double runTimeMin;
|
||||||
|
private double totalOutputTon;
|
||||||
|
private double totalOutputCoil;
|
||||||
|
private double goodOutputTon;
|
||||||
|
private double defectOutputTon;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AcidKpiAgg calcKpi(List<AcidOeeDailySummaryVo> list) {
|
||||||
|
AcidKpiAgg k = new AcidKpiAgg();
|
||||||
|
if (list == null || list.isEmpty()) {
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
int n = list.size();
|
||||||
|
double sumOee = 0, sumA = 0, sumP = 0, sumQ = 0;
|
||||||
|
double sumLoading = 0, sumDown = 0, sumRun = 0;
|
||||||
|
double sumTotalTon = 0, sumGoodTon = 0, sumDefectTon = 0, sumCoil = 0;
|
||||||
|
for (AcidOeeDailySummaryVo r : list) {
|
||||||
|
sumLoading += safeNum(r.getLoadingTimeMin());
|
||||||
|
sumDown += safeNum(r.getDowntimeMin());
|
||||||
|
sumRun += safeNum(r.getRunTimeMin());
|
||||||
|
sumTotalTon += safeNum(r.getTotalOutputTon());
|
||||||
|
sumGoodTon += safeNum(r.getGoodOutputTon());
|
||||||
|
sumDefectTon += safeNum(r.getDefectOutputTon());
|
||||||
|
sumCoil += safeNum(r.getTotalOutputCoil());
|
||||||
|
sumOee += safeNum(r.getOee());
|
||||||
|
sumA += safeNum(r.getAvailability());
|
||||||
|
sumP += safeNum(r.getPerformanceTon());
|
||||||
|
sumQ += safeNum(r.getQuality());
|
||||||
|
}
|
||||||
|
double defectAgg = Math.max(0, sumTotalTon - sumGoodTon);
|
||||||
|
k.setOee(sumOee / n);
|
||||||
|
k.setAvailability(sumA / n);
|
||||||
|
k.setPerformanceTon(sumP / n);
|
||||||
|
k.setQuality(sumQ / n);
|
||||||
|
k.setLoadingTimeMin(sumLoading);
|
||||||
|
k.setDowntimeMin(sumDown);
|
||||||
|
k.setRunTimeMin(sumRun);
|
||||||
|
k.setTotalOutputTon(sumTotalTon);
|
||||||
|
k.setTotalOutputCoil(sumCoil);
|
||||||
|
k.setGoodOutputTon(sumGoodTon);
|
||||||
|
k.setDefectOutputTon(defectAgg > 0 ? defectAgg : sumDefectTon);
|
||||||
|
return k;
|
||||||
|
}
|
||||||
|
|
||||||
|
private double safeNum(Object v) {
|
||||||
|
Double d = toDouble(v);
|
||||||
|
return d == null ? 0d : d;
|
||||||
|
}
|
||||||
|
|
||||||
|
// idealCycle 字段做容错(避免 VO 字段名差异导致编译失败)
|
||||||
|
private Object getIdealCycleTime(AcidOeeIdealCycleVo vo) {
|
||||||
|
try {
|
||||||
|
return vo.getClass().getMethod("getIdealCycleTimeMinPerTon").invoke(vo);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object getMedianCycleTime(AcidOeeIdealCycleVo vo) {
|
||||||
|
try {
|
||||||
|
return vo.getClass().getMethod("getMedianCycleTimeMinPerTon").invoke(vo);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int getSampleDays(AcidOeeIdealCycleVo vo) {
|
||||||
|
try {
|
||||||
|
Object v = vo.getClass().getMethod("getSampleDays").invoke(vo);
|
||||||
|
Double d = toDouble(v);
|
||||||
|
return d == null ? 0 : d.intValue();
|
||||||
|
} catch (Exception e) {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Object getMinWeightTon(AcidOeeIdealCycleVo vo) {
|
||||||
|
try {
|
||||||
|
return vo.getClass().getMethod("getMinWeightTon").invoke(vo);
|
||||||
|
} catch (Exception e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ======================== 任意日期范围异步任务(酸轧线) ========================
|
// ======================== 任意日期范围异步任务(酸轧线) ========================
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package com.klp.da.service;
|
||||||
|
|
||||||
|
import com.klp.common.core.page.TableDataInfo;
|
||||||
|
import com.klp.da.domain.bo.OeeQueryBo;
|
||||||
|
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
||||||
|
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
|
||||||
|
import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OEE 聚合 Service 接口(酸轧线)
|
||||||
|
*
|
||||||
|
* @author klp
|
||||||
|
* @date 2026-01-31
|
||||||
|
*/
|
||||||
|
public interface IDaAcidOeeService {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 OEE 日汇总(当月走缓存,历史查询走实时)
|
||||||
|
*
|
||||||
|
* @param bo 查询条件
|
||||||
|
* @return 日汇总列表
|
||||||
|
*/
|
||||||
|
List<AcidOeeDailySummaryVo> getDailySummary(OeeQueryBo bo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取 7 大损失汇总(当月走缓存,历史查询走实时)
|
||||||
|
*
|
||||||
|
* @param bo 查询条件
|
||||||
|
* @return 7 大损失列表
|
||||||
|
*/
|
||||||
|
List<AcidOeeLoss7Vo> getLoss7Summary(OeeQueryBo bo);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取停机事件明细(实时分页查询)
|
||||||
|
*
|
||||||
|
* @param bo 查询条件
|
||||||
|
* @return 分页后的停机事件列表
|
||||||
|
*/
|
||||||
|
TableDataInfo<Klptcm1ProStoppageVo> getStoppageEvents(OeeQueryBo bo);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package com.klp.da.service;
|
||||||
|
|
||||||
|
import com.klp.common.config.DeepseekConfig;
|
||||||
|
import com.klp.common.utils.StringUtils;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.beans.factory.annotation.Qualifier;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.HttpStatus;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
import org.springframework.web.client.RestTemplate;
|
||||||
|
|
||||||
|
import java.util.Arrays;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OEE Word 导出:表格内容大模型分析(可选)
|
||||||
|
*
|
||||||
|
* 默认关闭,通过配置开启:
|
||||||
|
* oee:
|
||||||
|
* word:
|
||||||
|
* ai:
|
||||||
|
* enabled: true
|
||||||
|
*
|
||||||
|
* 大模型连接配置复用 sales.script.ai(DeepSeek):
|
||||||
|
* sales:
|
||||||
|
* script:
|
||||||
|
* ai:
|
||||||
|
* api-key/base-url/model-name/max-retries/temperature
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Service
|
||||||
|
public class OeeWordAiAnalysisService {
|
||||||
|
|
||||||
|
@Value("${oee.word.ai.enabled:false}")
|
||||||
|
private boolean enabled;
|
||||||
|
|
||||||
|
@Qualifier("salesScriptRestTemplate")
|
||||||
|
private final RestTemplate restTemplate;
|
||||||
|
|
||||||
|
private final DeepseekConfig deepseekConfig;
|
||||||
|
|
||||||
|
public String analyzeTable(String tableTitle, String tableMarkdown, String extraContext) {
|
||||||
|
if (!enabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (StringUtils.isBlank(tableMarkdown)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (deepseekConfig == null || StringUtils.isBlank(deepseekConfig.getApiKey())
|
||||||
|
|| StringUtils.isBlank(deepseekConfig.getBaseUrl())
|
||||||
|
|| StringUtils.isBlank(deepseekConfig.getModelName())) {
|
||||||
|
log.warn("OEE Word AI 已开启但 sales.script.ai 配置不完整,跳过分析");
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
String prompt = buildPrompt(tableTitle, tableMarkdown, extraContext);
|
||||||
|
return callAi(prompt);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildPrompt(String tableTitle, String tableMarkdown, String extraContext) {
|
||||||
|
StringBuilder sb = new StringBuilder();
|
||||||
|
sb.append("你是一名制造业OEE报表分析专家。请基于下面表格数据,给出简洁的分析结论。\n");
|
||||||
|
sb.append("输出要求:\n");
|
||||||
|
sb.append("1) 用中文输出;\n");
|
||||||
|
sb.append("2) 只输出 3~6 条要点(每条不超过 30 字),不要写长段落;\n");
|
||||||
|
sb.append("3) 可以指出异常/波动/占比最高项/可能原因与建议方向;\n");
|
||||||
|
sb.append("4) 不要复述表格标题,不要输出Markdown表格。\n\n");
|
||||||
|
sb.append("【表格】").append(tableTitle).append("\n");
|
||||||
|
if (StringUtils.isNotBlank(extraContext)) {
|
||||||
|
sb.append("【上下文】").append(extraContext).append("\n");
|
||||||
|
}
|
||||||
|
sb.append("【数据】\n");
|
||||||
|
sb.append(tableMarkdown);
|
||||||
|
return sb.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private String callAi(String prompt) {
|
||||||
|
Map<String, Object> requestBody = new HashMap<>();
|
||||||
|
requestBody.put("model", deepseekConfig.getModelName());
|
||||||
|
|
||||||
|
Map<String, String> systemMessage = new HashMap<>();
|
||||||
|
systemMessage.put("role", "system");
|
||||||
|
systemMessage.put("content", "你是一个严谨的数据分析助手");
|
||||||
|
|
||||||
|
Map<String, String> userMessage = new HashMap<>();
|
||||||
|
userMessage.put("role", "user");
|
||||||
|
userMessage.put("content", prompt);
|
||||||
|
|
||||||
|
requestBody.put("messages", Arrays.asList(systemMessage, userMessage));
|
||||||
|
requestBody.put("temperature", deepseekConfig.getTemperature() == null ? 0.2 : deepseekConfig.getTemperature());
|
||||||
|
requestBody.put("max_tokens", 800);
|
||||||
|
|
||||||
|
HttpHeaders headers = new HttpHeaders();
|
||||||
|
headers.setContentType(MediaType.APPLICATION_JSON);
|
||||||
|
headers.setBearerAuth(deepseekConfig.getApiKey());
|
||||||
|
HttpEntity<Map<String, Object>> entity = new HttpEntity<>(requestBody, headers);
|
||||||
|
|
||||||
|
int retries = deepseekConfig.getMaxRetries() == null ? 1 : Math.max(1, deepseekConfig.getMaxRetries());
|
||||||
|
for (int i = 0; i < retries; i++) {
|
||||||
|
try {
|
||||||
|
ResponseEntity<Map> response = restTemplate.postForEntity(
|
||||||
|
deepseekConfig.getBaseUrl() + "/chat/completions", entity, Map.class);
|
||||||
|
if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) {
|
||||||
|
Map<String, Object> body = response.getBody();
|
||||||
|
List<Map<String, Object>> choices = (List<Map<String, Object>>) body.get("choices");
|
||||||
|
if (choices != null && !choices.isEmpty()) {
|
||||||
|
Map<String, Object> choice = choices.get(0);
|
||||||
|
Map<String, Object> message = (Map<String, Object>) choice.get("message");
|
||||||
|
String content = message == null ? null : (String) message.get("content");
|
||||||
|
if (StringUtils.isNotBlank(content)) {
|
||||||
|
return content.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.warn("OEE Word AI 调用失败,重试 {}/{}:{}", i + 1, retries, e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
package com.klp.da.service.impl;
|
||||||
|
|
||||||
|
import com.alibaba.fastjson2.JSON;
|
||||||
|
import com.alibaba.fastjson2.TypeReference;
|
||||||
|
import com.github.pagehelper.PageInfo;
|
||||||
|
import com.klp.common.core.page.TableDataInfo;
|
||||||
|
import com.klp.common.utils.DateUtils;
|
||||||
|
import com.klp.common.utils.StringUtils;
|
||||||
|
import com.klp.da.domain.bo.OeeQueryBo;
|
||||||
|
import com.klp.da.service.IDaAcidOeeService;
|
||||||
|
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
||||||
|
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
|
||||||
|
import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo;
|
||||||
|
import com.klp.pocket.acid.service.IAcidOeeService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.time.LocalDate;
|
||||||
|
import java.time.format.DateTimeFormatter;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
@Service
|
||||||
|
public class DaAcidOeeServiceImpl implements IDaAcidOeeService {
|
||||||
|
|
||||||
|
private static final String SUMMARY_KEY_PATTERN = "oee:report:month:summary:%s:SY";
|
||||||
|
private static final DateTimeFormatter YEAR_MONTH_FMT = DateTimeFormatter.ofPattern("yyyyMM");
|
||||||
|
|
||||||
|
private final IAcidOeeService acidOeeService;
|
||||||
|
private final StringRedisTemplate stringRedisTemplate;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AcidOeeDailySummaryVo> getDailySummary(OeeQueryBo bo) {
|
||||||
|
// 检查是否查询当月,如果是,则尝试从 Redis 缓存获取
|
||||||
|
if (isCurrentMonthQuery(bo.getStartDate(), bo.getEndDate())) {
|
||||||
|
String yyyyMM = LocalDate.now().format(YEAR_MONTH_FMT);
|
||||||
|
String summaryKey = String.format(SUMMARY_KEY_PATTERN, yyyyMM);
|
||||||
|
try {
|
||||||
|
String summaryJson = stringRedisTemplate.opsForValue().get(summaryKey);
|
||||||
|
if (StringUtils.isNotBlank(summaryJson)) {
|
||||||
|
log.info("[DaAcidOeeService] Hit cache for acid summary, key={}", summaryKey);
|
||||||
|
return JSON.parseObject(summaryJson, new TypeReference<List<AcidOeeDailySummaryVo>>() {});
|
||||||
|
}
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("[DaAcidOeeService] Failed to get acid summary from redis cache, key={}", summaryKey, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 缓存未命中或查询历史数据,则实时调用 pocket service
|
||||||
|
log.info("[DaAcidOeeService] Cache miss for acid summary, calling pocket service...");
|
||||||
|
return acidOeeService.getDailySummary(
|
||||||
|
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getStartDate())),
|
||||||
|
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getEndDate()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public List<AcidOeeLoss7Vo> getLoss7Summary(OeeQueryBo bo) {
|
||||||
|
// 7大损失目前没有预计算,直接实时调用
|
||||||
|
return acidOeeService.getLoss7Summary(
|
||||||
|
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getStartDate())),
|
||||||
|
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getEndDate()))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public TableDataInfo<Klptcm1ProStoppageVo> getStoppageEvents(OeeQueryBo bo) {
|
||||||
|
// 实时分页查询
|
||||||
|
List<Klptcm1ProStoppageVo> list = acidOeeService.getStoppageEvents(
|
||||||
|
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getStartDate())),
|
||||||
|
DateUtils.parseDateToStr("yyyy-MM-dd", DateUtils.toDate(bo.getEndDate()))
|
||||||
|
);
|
||||||
|
return TableDataInfo.build(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 判断查询范围是否为当月
|
||||||
|
*/
|
||||||
|
private boolean isCurrentMonthQuery(LocalDate startDate, LocalDate endDate) {
|
||||||
|
if (startDate == null || endDate == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
LocalDate now = LocalDate.now();
|
||||||
|
LocalDate firstDayOfMonth = now.withDayOfMonth(1);
|
||||||
|
return !startDate.isBefore(firstDayOfMonth) && !endDate.isAfter(now);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -2,7 +2,6 @@ package com.klp.da.task;
|
|||||||
|
|
||||||
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSON;
|
||||||
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
||||||
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
|
|
||||||
import com.klp.pocket.acid.service.IAcidOeeService;
|
import com.klp.pocket.acid.service.IAcidOeeService;
|
||||||
import lombok.Data;
|
import lombok.Data;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
@@ -25,10 +24,9 @@ import java.util.concurrent.TimeUnit;
|
|||||||
* - 项目启动完成后即计算当月 OEE 聚合结果并写入 Redis;
|
* - 项目启动完成后即计算当月 OEE 聚合结果并写入 Redis;
|
||||||
* - 每天凌晨 04:00 重新计算当月数据并覆盖缓存。
|
* - 每天凌晨 04:00 重新计算当月数据并覆盖缓存。
|
||||||
*
|
*
|
||||||
* 当前仅实现酸轧线(SY)的当月日汇总 & 7 大损失预计算;
|
* 当前仅实现酸轧线(SY)的当月日汇总预计算;
|
||||||
* key 约定:
|
* key 约定:
|
||||||
* - 汇总结果:oee:report:month:summary:{yyyyMM}:SY
|
* - 汇总结果:oee:report:month:summary:{yyyyMM}:SY
|
||||||
* - 7 大损失:oee:report:month:loss7:{yyyyMM}:SY
|
|
||||||
* - 元信息: oee:report:month:meta:{yyyyMM}:SY
|
* - 元信息: oee:report:month:meta:{yyyyMM}:SY
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@@ -39,9 +37,6 @@ public class AcidOeeMonthTask {
|
|||||||
/** Redis 缓存 key 模板:当月 OEE 汇总(酸轧线) */
|
/** Redis 缓存 key 模板:当月 OEE 汇总(酸轧线) */
|
||||||
private static final String SUMMARY_KEY_PATTERN = "oee:report:month:summary:%s:SY";
|
private static final String SUMMARY_KEY_PATTERN = "oee:report:month:summary:%s:SY";
|
||||||
|
|
||||||
/** Redis 缓存 key 模板:当月 7 大损失(酸轧线) */
|
|
||||||
private static final String LOSS7_KEY_PATTERN = "oee:report:month:loss7:%s:SY";
|
|
||||||
|
|
||||||
/** Redis 缓存 key 模板:当月元信息(酸轧线) */
|
/** Redis 缓存 key 模板:当月元信息(酸轧线) */
|
||||||
private static final String META_KEY_PATTERN = "oee:report:month:meta:%s:SY";
|
private static final String META_KEY_PATTERN = "oee:report:month:meta:%s:SY";
|
||||||
|
|
||||||
@@ -96,20 +91,14 @@ public class AcidOeeMonthTask {
|
|||||||
log.info("[AcidOeeMonthTask] trigger={}, computing acid OEE month summary for {} ({} ~ {})",
|
log.info("[AcidOeeMonthTask] trigger={}, computing acid OEE month summary for {} ({} ~ {})",
|
||||||
trigger, yyyyMM, startStr, endStr);
|
trigger, yyyyMM, startStr, endStr);
|
||||||
|
|
||||||
// 1. 调用 pocket 的 AcidOeeService 获取当月日汇总 & 7 大损失
|
// 1. 调用 pocket 的 AcidOeeService 获取当月日汇总
|
||||||
List<AcidOeeDailySummaryVo> dailySummaryList = acidOeeService.getDailySummary(startStr, endStr);
|
List<AcidOeeDailySummaryVo> dailySummaryList = acidOeeService.getDailySummary(startStr, endStr);
|
||||||
List<AcidOeeLoss7Vo> loss7List = acidOeeService.getLoss7Summary(startStr, endStr);
|
|
||||||
|
|
||||||
// 2. 写入 Redis(summary)
|
// 2. 写入 Redis(summary)
|
||||||
String summaryKey = String.format(SUMMARY_KEY_PATTERN, yyyyMM);
|
String summaryKey = String.format(SUMMARY_KEY_PATTERN, yyyyMM);
|
||||||
String summaryJson = JSON.toJSONString(dailySummaryList);
|
String summaryJson = JSON.toJSONString(dailySummaryList);
|
||||||
stringRedisTemplate.opsForValue().set(summaryKey, summaryJson, 1, TimeUnit.DAYS);
|
stringRedisTemplate.opsForValue().set(summaryKey, summaryJson, 1, TimeUnit.DAYS);
|
||||||
|
|
||||||
// 2.1 写入 Redis(loss7)
|
|
||||||
String loss7Key = String.format(LOSS7_KEY_PATTERN, yyyyMM);
|
|
||||||
String loss7Json = JSON.toJSONString(loss7List);
|
|
||||||
stringRedisTemplate.opsForValue().set(loss7Key, loss7Json, 1, TimeUnit.DAYS);
|
|
||||||
|
|
||||||
long durationMs = (System.nanoTime() - startNs) / 1_000_000L;
|
long durationMs = (System.nanoTime() - startNs) / 1_000_000L;
|
||||||
|
|
||||||
// 3. 写入 Redis(meta)
|
// 3. 写入 Redis(meta)
|
||||||
@@ -123,8 +112,8 @@ public class AcidOeeMonthTask {
|
|||||||
String metaKey = String.format(META_KEY_PATTERN, yyyyMM);
|
String metaKey = String.format(META_KEY_PATTERN, yyyyMM);
|
||||||
stringRedisTemplate.opsForValue().set(metaKey, JSON.toJSONString(meta), 1, TimeUnit.DAYS);
|
stringRedisTemplate.opsForValue().set(metaKey, JSON.toJSONString(meta), 1, TimeUnit.DAYS);
|
||||||
|
|
||||||
log.info("[AcidOeeMonthTask] compute finish for {} dailySize={}, loss7Size={}, durationMs={}ms, summaryKey={}",
|
log.info("[AcidOeeMonthTask] compute finish for {} dailySize={}, durationMs={}ms, summaryKey={}",
|
||||||
yyyyMM, dailySummaryList.size(), loss7List.size(), durationMs, summaryKey);
|
yyyyMM, dailySummaryList.size(), durationMs, summaryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package com.klp.pocket.acid.domain.vo;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
|
||||||
|
import java.math.BigDecimal;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 酸轧线理论节拍(统计口径)视图对象
|
||||||
|
* 通过历史“优良日”统计得到,替代不稳定的回归结果。
|
||||||
|
*
|
||||||
|
* @author klp
|
||||||
|
* @date 2026-02-03
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class AcidOeeIdealCycleVo {
|
||||||
|
|
||||||
|
/** 产线ID(固定为 SY) */
|
||||||
|
private String lineId;
|
||||||
|
|
||||||
|
/** 产线名称(酸轧线) */
|
||||||
|
private String lineName;
|
||||||
|
|
||||||
|
/** 本次计算使用的数据区间(yyyy-MM-dd) */
|
||||||
|
private String startDate;
|
||||||
|
private String endDate;
|
||||||
|
|
||||||
|
/** 理论节拍(min/吨)- 固定值0.47 */
|
||||||
|
private BigDecimal idealCycleTimeMinPerTon;
|
||||||
|
|
||||||
|
/** 中位数理论节拍(min/吨)- 从卷级节拍计算的中位数 */
|
||||||
|
private BigDecimal medianCycleTimeMinPerTon;
|
||||||
|
|
||||||
|
/** 参与统计的"优良日"天数 */
|
||||||
|
private Integer sampleDays;
|
||||||
|
|
||||||
|
/** 统计口径:吨数下限(吨) */
|
||||||
|
private BigDecimal minWeightTon;
|
||||||
|
|
||||||
|
/** 统计口径:停机占比上限(0~1) */
|
||||||
|
private BigDecimal maxDowntimeRate;
|
||||||
|
|
||||||
|
/** 日粒度对比点:理论耗时 vs 实际运转时间 */
|
||||||
|
private List<DailyComparePointVo> dailyComparePoints;
|
||||||
|
|
||||||
|
@Data
|
||||||
|
public static class DailyComparePointVo {
|
||||||
|
private String statDate;
|
||||||
|
private Long actualRunTimeMin;
|
||||||
|
private BigDecimal theoreticalTimeMin;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
package com.klp.pocket.acid.domain.vo;
|
|
||||||
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import java.math.BigDecimal;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 酸轧线OEE回归数据视图对象
|
|
||||||
* 用于理论节拍计算和前端散点图展示
|
|
||||||
*
|
|
||||||
* @author klp
|
|
||||||
* @date 2026-01-30
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public class AcidOeeRegressionVo {
|
|
||||||
|
|
||||||
/** 产线ID(固定为 SY) */
|
|
||||||
private String lineId;
|
|
||||||
|
|
||||||
/** 产线名称(酸轧线) */
|
|
||||||
private String lineName;
|
|
||||||
|
|
||||||
/** 回归斜率:分钟/吨(核心值,可作为理论节拍) */
|
|
||||||
private BigDecimal slopeMinPerTon;
|
|
||||||
|
|
||||||
/** 回归斜率:分钟/卷(核心值,可作为理论节拍) */
|
|
||||||
private BigDecimal slopeMinPerCoil;
|
|
||||||
|
|
||||||
/** 截距(分钟) */
|
|
||||||
private BigDecimal interceptMin;
|
|
||||||
|
|
||||||
/** 拟合优度(R²) */
|
|
||||||
private BigDecimal r2;
|
|
||||||
|
|
||||||
/** 参与回归样本数 */
|
|
||||||
private Integer sampleCount;
|
|
||||||
|
|
||||||
/** 回归数据开始时间 */
|
|
||||||
private String startTime;
|
|
||||||
|
|
||||||
/** 回归数据结束时间 */
|
|
||||||
private String endTime;
|
|
||||||
|
|
||||||
/** 散点列表 */
|
|
||||||
private List<RegressionPointVo> points;
|
|
||||||
|
|
||||||
/** 拟合线两个端点(前端可直接画线) */
|
|
||||||
private List<RegressionLinePointVo> linePoints;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 散点数据
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public static class RegressionPointVo {
|
|
||||||
/** 重量(吨,X轴) */
|
|
||||||
private BigDecimal weightTon;
|
|
||||||
/** 卷数(X轴) */
|
|
||||||
private Long coilCount;
|
|
||||||
/** 时长(分钟,Y轴) */
|
|
||||||
private Long durationMin;
|
|
||||||
/** 关联的actionId(可选) */
|
|
||||||
private String actionId;
|
|
||||||
/** 创建时间 */
|
|
||||||
private String createTime;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 拟合线端点
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
public static class RegressionLinePointVo {
|
|
||||||
/** 重量(吨,X轴) */
|
|
||||||
private BigDecimal weightTon;
|
|
||||||
/** 卷数(X轴) */
|
|
||||||
private Long coilCount;
|
|
||||||
/** 时长(分钟,Y轴) */
|
|
||||||
private BigDecimal durationMin;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package com.klp.pocket.acid.mapper;
|
package com.klp.pocket.acid.mapper;
|
||||||
|
|
||||||
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
||||||
import com.klp.pocket.acid.domain.vo.AcidOeeRegressionVo;
|
|
||||||
import org.apache.ibatis.annotations.Mapper;
|
import org.apache.ibatis.annotations.Mapper;
|
||||||
import org.apache.ibatis.annotations.Param;
|
import org.apache.ibatis.annotations.Param;
|
||||||
|
|
||||||
@@ -27,17 +26,6 @@ public interface AcidOeeMapper {
|
|||||||
List<AcidOeeDailySummaryVo> selectDailySummary(@Param("startDate") String startDate,
|
List<AcidOeeDailySummaryVo> selectDailySummary(@Param("startDate") String startDate,
|
||||||
@Param("endDate") String endDate);
|
@Param("endDate") String endDate);
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询回归数据散点(用于理论节拍计算)
|
|
||||||
* 返回:重量(吨)、卷数、时长(分钟)等
|
|
||||||
*
|
|
||||||
* @param startDate 开始日期(yyyy-MM-dd,可选)
|
|
||||||
* @param endDate 结束日期(yyyy-MM-dd,可选)
|
|
||||||
* @return 散点列表
|
|
||||||
*/
|
|
||||||
List<AcidOeeRegressionVo.RegressionPointVo> selectRegressionPoints(@Param("startDate") String startDate,
|
|
||||||
@Param("endDate") String endDate);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询每日的钢卷号和重量(用于良品/次品判定)
|
* 查询每日的钢卷号和重量(用于良品/次品判定)
|
||||||
*
|
*
|
||||||
@@ -48,6 +36,16 @@ public interface AcidOeeMapper {
|
|||||||
List<CoilInfoByDate> selectCoilInfoByDate(@Param("startDate") String startDate,
|
List<CoilInfoByDate> selectCoilInfoByDate(@Param("startDate") String startDate,
|
||||||
@Param("endDate") String endDate);
|
@Param("endDate") String endDate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询卷级生产节拍(min/吨),用于理论节拍计算。
|
||||||
|
*
|
||||||
|
* @param startDate 开始日期(yyyy-MM-dd)
|
||||||
|
* @param endDate 结束日期(yyyy-MM-dd)
|
||||||
|
* @return 每卷的生产节拍列表(min/吨)
|
||||||
|
*/
|
||||||
|
List<java.math.BigDecimal> selectCoilCycleMinPerTon(@Param("startDate") String startDate,
|
||||||
|
@Param("endDate") String endDate);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 卷号信息内部类(用于Mapper返回)
|
* 卷号信息内部类(用于Mapper返回)
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
package com.klp.pocket.acid.service;
|
package com.klp.pocket.acid.service;
|
||||||
|
|
||||||
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
||||||
|
import com.klp.pocket.acid.domain.vo.AcidOeeIdealCycleVo;
|
||||||
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
|
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
|
||||||
import com.klp.pocket.acid.domain.vo.AcidOeeRegressionVo;
|
|
||||||
import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo;
|
import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
@@ -34,16 +34,6 @@ public interface IAcidOeeService {
|
|||||||
*/
|
*/
|
||||||
List<Klptcm1ProStoppageVo> getStoppageEvents(String startDate, String endDate);
|
List<Klptcm1ProStoppageVo> getStoppageEvents(String startDate, String endDate);
|
||||||
|
|
||||||
/**
|
|
||||||
* 查询理论节拍回归数据(吨和卷两个维度)
|
|
||||||
* 用于性能稼动率计算和前端散点图展示
|
|
||||||
*
|
|
||||||
* @param startDate 开始日期(yyyy-MM-dd,可选,默认近6个月)
|
|
||||||
* @param endDate 结束日期(yyyy-MM-dd,可选)
|
|
||||||
* @return 回归数据(包含斜率、截距、散点等)
|
|
||||||
*/
|
|
||||||
AcidOeeRegressionVo getRegressionData(String startDate, String endDate);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 查询7大损失汇总(按日期范围)
|
* 查询7大损失汇总(按日期范围)
|
||||||
*
|
*
|
||||||
@@ -52,5 +42,14 @@ public interface IAcidOeeService {
|
|||||||
* @return 7大损失汇总列表
|
* @return 7大损失汇总列表
|
||||||
*/
|
*/
|
||||||
List<AcidOeeLoss7Vo> getLoss7Summary(String startDate, String endDate);
|
List<AcidOeeLoss7Vo> getLoss7Summary(String startDate, String endDate);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询理论节拍(统计口径:历史优良日中位数)
|
||||||
|
*
|
||||||
|
* @param startDate 开始日期(yyyy-MM-dd)
|
||||||
|
* @param endDate 结束日期(yyyy-MM-dd)
|
||||||
|
* @return 理论节拍与对比数据
|
||||||
|
*/
|
||||||
|
AcidOeeIdealCycleVo getIdealCycle(String startDate, String endDate);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ package com.klp.pocket.acid.service.impl;
|
|||||||
import com.baomidou.dynamic.datasource.annotation.DS;
|
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||||
import com.klp.common.utils.StringUtils;
|
import com.klp.common.utils.StringUtils;
|
||||||
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo;
|
||||||
|
import com.klp.pocket.acid.domain.vo.AcidOeeIdealCycleVo;
|
||||||
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
|
import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo;
|
||||||
import com.klp.pocket.acid.domain.vo.AcidOeeRegressionVo;
|
|
||||||
import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo;
|
import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo;
|
||||||
import com.klp.pocket.acid.domain.bo.Klptcm1ProStoppageBo;
|
import com.klp.pocket.acid.domain.bo.Klptcm1ProStoppageBo;
|
||||||
import com.klp.pocket.acid.mapper.AcidOeeMapper;
|
import com.klp.pocket.acid.mapper.AcidOeeMapper;
|
||||||
@@ -19,7 +19,7 @@ import java.math.BigDecimal;
|
|||||||
import java.math.RoundingMode;
|
import java.math.RoundingMode;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
import java.util.stream.Collectors;
|
import java.util.Calendar;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 酸轧线OEE Service实现类
|
* 酸轧线OEE Service实现类
|
||||||
@@ -35,7 +35,8 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
|||||||
|
|
||||||
/** 酸轧成品库库区ID */
|
/** 酸轧成品库库区ID */
|
||||||
private static final Long ACID_FINISHED_WAREHOUSE_ID = 1988150099140866050L;
|
private static final Long ACID_FINISHED_WAREHOUSE_ID = 1988150099140866050L;
|
||||||
|
/** 固定理论节拍(min/吨) */
|
||||||
|
private static final BigDecimal FIXED_IDEAL_CYCLE = BigDecimal.valueOf(0.47);
|
||||||
private final AcidOeeMapper acidOeeMapper;
|
private final AcidOeeMapper acidOeeMapper;
|
||||||
private final IKlptcm1ProStoppageService stoppageService;
|
private final IKlptcm1ProStoppageService stoppageService;
|
||||||
private final ICoilQualityJudgeService coilQualityJudgeService;
|
private final ICoilQualityJudgeService coilQualityJudgeService;
|
||||||
@@ -55,7 +56,10 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
|||||||
// 3. 查询产量明细,用于良品/次品判定
|
// 3. 查询产量明细,用于良品/次品判定
|
||||||
Map<String, List<CoilInfo>> coilInfoByDate = getCoilNosByDate(startDate, endDate);
|
Map<String, List<CoilInfo>> coilInfoByDate = getCoilNosByDate(startDate, endDate);
|
||||||
|
|
||||||
// 4. 填充每个日汇总的完整数据
|
// 4. 理论节拍:使用固定值0.47
|
||||||
|
BigDecimal idealCycleTon = FIXED_IDEAL_CYCLE;
|
||||||
|
|
||||||
|
// 5. 填充每个日汇总的完整数据
|
||||||
for (AcidOeeDailySummaryVo summary : summaries) {
|
for (AcidOeeDailySummaryVo summary : summaries) {
|
||||||
String statDate = summary.getStatDate();
|
String statDate = summary.getStatDate();
|
||||||
summary.setLineId("SY");
|
summary.setLineId("SY");
|
||||||
@@ -70,6 +74,11 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
|||||||
Long runTime = Math.max(0, loadingTime - downtime);
|
Long runTime = Math.max(0, loadingTime - downtime);
|
||||||
summary.setRunTimeMin(runTime);
|
summary.setRunTimeMin(runTime);
|
||||||
|
|
||||||
|
// 理论节拍:若尚未填充,则统一使用“优良日统计”得到的节拍
|
||||||
|
if (summary.getIdealCycleTimeMinPerTon() == null && idealCycleTon != null) {
|
||||||
|
summary.setIdealCycleTimeMinPerTon(idealCycleTon);
|
||||||
|
}
|
||||||
|
|
||||||
// 良品/次品判定(通过WMS)
|
// 良品/次品判定(通过WMS)
|
||||||
if (coilInfoByDate.containsKey(statDate)) {
|
if (coilInfoByDate.containsKey(statDate)) {
|
||||||
List<CoilInfo> coilInfos = coilInfoByDate.get(statDate);
|
List<CoilInfo> coilInfos = coilInfoByDate.get(statDate);
|
||||||
@@ -101,57 +110,84 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
|||||||
return stoppageService.queryList(bo);
|
return stoppageService.queryList(bo);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 查询停机事件(可选:是否包含短停机 <5min)
|
||||||
|
*/
|
||||||
|
private List<Klptcm1ProStoppageVo> getStoppageEvents(String startDate, String endDate, boolean includeShortDuration) {
|
||||||
|
Klptcm1ProStoppageBo bo = new Klptcm1ProStoppageBo();
|
||||||
|
bo.setStartDate(parseDate(startDate));
|
||||||
|
bo.setEndDate(parseDate(endDate));
|
||||||
|
if (includeShortDuration) {
|
||||||
|
// BaseEntity.params 用于透传查询开关
|
||||||
|
bo.getParams().put("includeShortDuration", true);
|
||||||
|
}
|
||||||
|
return stoppageService.queryList(bo);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public AcidOeeRegressionVo getRegressionData(String startDate, String endDate) {
|
public AcidOeeIdealCycleVo getIdealCycle(String startDate, String endDate) {
|
||||||
// 1. 查询散点数据
|
// 1) 取基础日汇总(产量、负荷时间等)
|
||||||
List<AcidOeeRegressionVo.RegressionPointVo> points = acidOeeMapper.selectRegressionPoints(startDate, endDate);
|
List<AcidOeeDailySummaryVo> daily = acidOeeMapper.selectDailySummary(startDate, endDate);
|
||||||
|
AcidOeeIdealCycleVo rsp = new AcidOeeIdealCycleVo();
|
||||||
|
rsp.setLineId("SY");
|
||||||
|
rsp.setLineName("酸轧线");
|
||||||
|
rsp.setStartDate(startDate);
|
||||||
|
rsp.setEndDate(endDate);
|
||||||
|
// 这里的 minWeightTon / maxDowntimeRate 字段暂不使用,可保留为前端说明字段
|
||||||
|
|
||||||
AcidOeeRegressionVo result = new AcidOeeRegressionVo();
|
if (daily == null || daily.isEmpty()) {
|
||||||
result.setLineId("SY");
|
rsp.setIdealCycleTimeMinPerTon(null);
|
||||||
result.setLineName("酸轧线");
|
rsp.setSampleDays(0);
|
||||||
result.setStartTime(startDate);
|
rsp.setDailyComparePoints(Collections.emptyList());
|
||||||
result.setEndTime(endDate);
|
return rsp;
|
||||||
result.setPoints(points);
|
|
||||||
result.setSampleCount(points != null ? points.size() : 0);
|
|
||||||
|
|
||||||
if (points == null || points.isEmpty()) {
|
|
||||||
// 没有数据时返回空结果
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 计算回归(吨维度)
|
// 2) 聚合停机,补齐 runTime
|
||||||
RegressionResult tonResult = calculateRegression(
|
Map<String, Long> downtimeByDate = aggregateDowntimeByDate(startDate, endDate);
|
||||||
points.stream().map(AcidOeeRegressionVo.RegressionPointVo::getWeightTon).filter(Objects::nonNull).collect(Collectors.toList()),
|
for (AcidOeeDailySummaryVo d : daily) {
|
||||||
points.stream().map(AcidOeeRegressionVo.RegressionPointVo::getDurationMin).filter(Objects::nonNull).collect(Collectors.toList())
|
Long downtime = downtimeByDate.getOrDefault(d.getStatDate(), 0L);
|
||||||
);
|
d.setDowntimeMin(downtime);
|
||||||
if (tonResult != null) {
|
Long loading = d.getLoadingTimeMin() != null ? d.getLoadingTimeMin() : 0L;
|
||||||
result.setSlopeMinPerTon(tonResult.slope);
|
d.setRunTimeMin(Math.max(0, loading - downtime));
|
||||||
result.setInterceptMin(tonResult.intercept);
|
|
||||||
result.setR2(tonResult.r2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. 计算回归(卷维度)
|
// 3) 卷级节拍 = (END_DATE - START_DATE)/出口重量,计算中位数(用于展示,不用于OEE计算)
|
||||||
RegressionResult coilResult = calculateRegression(
|
List<BigDecimal> coilCycles = acidOeeMapper.selectCoilCycleMinPerTon(startDate, endDate);
|
||||||
points.stream().map(p -> p.getCoilCount() != null ? BigDecimal.valueOf(p.getCoilCount()) : null).filter(Objects::nonNull).collect(Collectors.toList()),
|
coilCycles.removeIf(c -> c == null || c.compareTo(BigDecimal.ZERO) <= 0);
|
||||||
points.stream().map(AcidOeeRegressionVo.RegressionPointVo::getDurationMin).filter(Objects::nonNull).collect(Collectors.toList())
|
coilCycles.sort(BigDecimal::compareTo);
|
||||||
);
|
BigDecimal medianCycle = median(coilCycles);
|
||||||
if (coilResult != null) {
|
|
||||||
result.setSlopeMinPerCoil(coilResult.slope);
|
// 理论节拍使用固定值0.47(用于OEE计算)
|
||||||
}
|
rsp.setIdealCycleTimeMinPerTon(FIXED_IDEAL_CYCLE);
|
||||||
|
// 中位数理论节拍(用于展示)
|
||||||
|
rsp.setMedianCycleTimeMinPerTon(medianCycle);
|
||||||
|
// 样本天数:当前查询区间内有产量的自然日数量(与传入的日期范围一一对应)
|
||||||
|
rsp.setSampleDays(daily.size());
|
||||||
|
|
||||||
// 4. 生成拟合线端点(用于前端画线)
|
// 4) 日粒度对比数据:理论耗时 vs 实际运转时间(用于前端展示"有效性")
|
||||||
if (tonResult != null && !points.isEmpty()) {
|
// 使用固定值0.47计算理论耗时
|
||||||
List<AcidOeeRegressionVo.RegressionLinePointVo> linePoints = generateLinePoints(points, tonResult);
|
List<AcidOeeIdealCycleVo.DailyComparePointVo> compare = new ArrayList<>();
|
||||||
result.setLinePoints(linePoints);
|
if (FIXED_IDEAL_CYCLE != null) {
|
||||||
|
for (AcidOeeDailySummaryVo d : daily) {
|
||||||
|
BigDecimal ton = d.getTotalOutputTon();
|
||||||
|
Long run = d.getRunTimeMin();
|
||||||
|
if (ton == null || run == null) continue;
|
||||||
|
AcidOeeIdealCycleVo.DailyComparePointVo p = new AcidOeeIdealCycleVo.DailyComparePointVo();
|
||||||
|
p.setStatDate(d.getStatDate());
|
||||||
|
p.setActualRunTimeMin(run);
|
||||||
|
p.setTheoreticalTimeMin(FIXED_IDEAL_CYCLE.multiply(ton));
|
||||||
|
compare.add(p);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
rsp.setDailyComparePoints(compare);
|
||||||
return result;
|
return rsp;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public List<AcidOeeLoss7Vo> getLoss7Summary(String startDate, String endDate) {
|
public List<AcidOeeLoss7Vo> getLoss7Summary(String startDate, String endDate) {
|
||||||
// 1. 查询停机事件(含 stopType、duration 等)
|
// 1. 查询停机事件(含 stopType、duration 等)
|
||||||
List<Klptcm1ProStoppageVo> events = getStoppageEvents(startDate, endDate);
|
// 损失统计也建议包含短停机,避免损失时间与停机总时间口径不一致
|
||||||
|
List<Klptcm1ProStoppageVo> events = getStoppageEvents(startDate, endDate, true);
|
||||||
if (events == null || events.isEmpty()) {
|
if (events == null || events.isEmpty()) {
|
||||||
return Collections.emptyList();
|
return Collections.emptyList();
|
||||||
}
|
}
|
||||||
@@ -162,9 +198,9 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
|||||||
|
|
||||||
for (Klptcm1ProStoppageVo event : events) {
|
for (Klptcm1ProStoppageVo event : events) {
|
||||||
String stopType = event.getStopType();
|
String stopType = event.getStopType();
|
||||||
|
// stopType 为空时归入“未分类”,避免因为未录入原因导致损失时间被漏算
|
||||||
if (StringUtils.isBlank(stopType)) {
|
if (StringUtils.isBlank(stopType)) {
|
||||||
// 没有类型的记录暂时忽略
|
stopType = "未分类";
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
Long durationSec = event.getDuration();
|
Long durationSec = event.getDuration();
|
||||||
if (durationSec == null || durationSec <= 0) {
|
if (durationSec == null || durationSec <= 0) {
|
||||||
@@ -225,18 +261,64 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 按日期聚合停机时间
|
* 按日期聚合停机时间
|
||||||
|
* 修复:如果停机事件跨天,需要按实际跨天的分钟数分配到对应的日期
|
||||||
*/
|
*/
|
||||||
private Map<String, Long> aggregateDowntimeByDate(String startDate, String endDate) {
|
private Map<String, Long> aggregateDowntimeByDate(String startDate, String endDate) {
|
||||||
List<Klptcm1ProStoppageVo> events = getStoppageEvents(startDate, endDate);
|
// 性能稼动率/运转时间口径:停机时间需要包含短停机(<5min),否则 runTime 被高估
|
||||||
|
List<Klptcm1ProStoppageVo> events = getStoppageEvents(startDate, endDate, true);
|
||||||
Map<String, Long> downtimeMap = new HashMap<>();
|
Map<String, Long> downtimeMap = new HashMap<>();
|
||||||
|
|
||||||
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
|
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
|
||||||
|
Calendar cal = Calendar.getInstance();
|
||||||
|
|
||||||
for (Klptcm1ProStoppageVo event : events) {
|
for (Klptcm1ProStoppageVo event : events) {
|
||||||
if (event.getStartDate() != null && event.getDuration() != null) {
|
if (event.getStartDate() == null || event.getDuration() == null || event.getDuration() <= 0) {
|
||||||
String date = dateFormat.format(event.getStartDate());
|
continue;
|
||||||
// duration单位是秒,转换为分钟
|
}
|
||||||
Long minutes = event.getDuration() / 60;
|
|
||||||
downtimeMap.merge(date, minutes, Long::sum);
|
Date eventStart = event.getStartDate();
|
||||||
|
long durationSec = event.getDuration();
|
||||||
|
long durationMin = (durationSec + 59) / 60; // 向上取整,避免丢失秒数
|
||||||
|
|
||||||
|
// 计算停机结束时间
|
||||||
|
cal.setTime(eventStart);
|
||||||
|
cal.add(Calendar.SECOND, (int) durationSec);
|
||||||
|
Date eventEnd = cal.getTime();
|
||||||
|
|
||||||
|
// 如果停机事件在同一天,直接累加
|
||||||
|
String startDateStr = dateFormat.format(eventStart);
|
||||||
|
String endDateStr = dateFormat.format(eventEnd);
|
||||||
|
|
||||||
|
if (startDateStr.equals(endDateStr)) {
|
||||||
|
// 同一天,直接累加
|
||||||
|
downtimeMap.merge(startDateStr, durationMin, Long::sum);
|
||||||
|
} else {
|
||||||
|
// 跨天:按实际跨天的分钟数分配到对应的日期
|
||||||
|
cal.setTime(eventStart);
|
||||||
|
cal.set(Calendar.HOUR_OF_DAY, 0);
|
||||||
|
cal.set(Calendar.MINUTE, 0);
|
||||||
|
cal.set(Calendar.SECOND, 0);
|
||||||
|
cal.set(Calendar.MILLISECOND, 0);
|
||||||
|
Date dayStart = cal.getTime();
|
||||||
|
|
||||||
|
Date currentDayStart = dayStart;
|
||||||
|
|
||||||
|
while (currentDayStart.before(eventEnd)) {
|
||||||
|
cal.setTime(currentDayStart);
|
||||||
|
cal.add(Calendar.DAY_OF_MONTH, 1);
|
||||||
|
Date nextDayStart = cal.getTime();
|
||||||
|
|
||||||
|
// 计算当前天的停机分钟数
|
||||||
|
Date dayEnd = nextDayStart.before(eventEnd) ? nextDayStart : eventEnd;
|
||||||
|
long dayMinutes = Math.max(0, (dayEnd.getTime() - Math.max(currentDayStart.getTime(), eventStart.getTime())) / (1000 * 60));
|
||||||
|
|
||||||
|
if (dayMinutes > 0) {
|
||||||
|
String dateKey = dateFormat.format(currentDayStart);
|
||||||
|
downtimeMap.merge(dateKey, dayMinutes, Long::sum);
|
||||||
|
}
|
||||||
|
|
||||||
|
currentDayStart = nextDayStart;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -327,7 +409,11 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
|||||||
Long runTime = summary.getRunTimeMin() != null ? summary.getRunTimeMin() : 0L;
|
Long runTime = summary.getRunTimeMin() != null ? summary.getRunTimeMin() : 0L;
|
||||||
BigDecimal idealCycleTon = summary.getIdealCycleTimeMinPerTon();
|
BigDecimal idealCycleTon = summary.getIdealCycleTimeMinPerTon();
|
||||||
BigDecimal totalOutputTon = summary.getTotalOutputTon();
|
BigDecimal totalOutputTon = summary.getTotalOutputTon();
|
||||||
if (runTime > 0 && idealCycleTon != null && totalOutputTon != null && totalOutputTon.compareTo(BigDecimal.ZERO) > 0) {
|
if (runTime > 0
|
||||||
|
&& idealCycleTon != null
|
||||||
|
&& idealCycleTon.compareTo(BigDecimal.ZERO) > 0
|
||||||
|
&& totalOutputTon != null
|
||||||
|
&& totalOutputTon.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
BigDecimal idealTime = idealCycleTon.multiply(totalOutputTon);
|
BigDecimal idealTime = idealCycleTon.multiply(totalOutputTon);
|
||||||
BigDecimal performanceTon = idealTime.divide(BigDecimal.valueOf(runTime), 4, RoundingMode.HALF_UP)
|
BigDecimal performanceTon = idealTime.divide(BigDecimal.valueOf(runTime), 4, RoundingMode.HALF_UP)
|
||||||
.multiply(BigDecimal.valueOf(100));
|
.multiply(BigDecimal.valueOf(100));
|
||||||
@@ -337,7 +423,10 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
|||||||
// 性能稼动率(卷维度)
|
// 性能稼动率(卷维度)
|
||||||
Long totalOutputCoil = summary.getTotalOutputCoil() != null ? summary.getTotalOutputCoil() : 0L;
|
Long totalOutputCoil = summary.getTotalOutputCoil() != null ? summary.getTotalOutputCoil() : 0L;
|
||||||
BigDecimal idealCycleCoil = summary.getIdealCycleTimeMinPerCoil();
|
BigDecimal idealCycleCoil = summary.getIdealCycleTimeMinPerCoil();
|
||||||
if (runTime > 0 && idealCycleCoil != null && totalOutputCoil > 0) {
|
if (runTime > 0
|
||||||
|
&& idealCycleCoil != null
|
||||||
|
&& idealCycleCoil.compareTo(BigDecimal.ZERO) > 0
|
||||||
|
&& totalOutputCoil > 0) {
|
||||||
BigDecimal idealTime = idealCycleCoil.multiply(BigDecimal.valueOf(totalOutputCoil));
|
BigDecimal idealTime = idealCycleCoil.multiply(BigDecimal.valueOf(totalOutputCoil));
|
||||||
BigDecimal performanceCoil = idealTime.divide(BigDecimal.valueOf(runTime), 4, RoundingMode.HALF_UP)
|
BigDecimal performanceCoil = idealTime.divide(BigDecimal.valueOf(runTime), 4, RoundingMode.HALF_UP)
|
||||||
.multiply(BigDecimal.valueOf(100));
|
.multiply(BigDecimal.valueOf(100));
|
||||||
@@ -363,102 +452,15 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
private BigDecimal median(List<BigDecimal> values) {
|
||||||
* 计算线性回归(最小二乘法)
|
if (values == null || values.isEmpty()) return null;
|
||||||
*/
|
int n = values.size();
|
||||||
private RegressionResult calculateRegression(List<BigDecimal> xValues, List<Long> yValues) {
|
if (n % 2 == 1) {
|
||||||
if (xValues.size() != yValues.size() || xValues.isEmpty()) {
|
return values.get(n / 2);
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
BigDecimal a = values.get(n / 2 - 1);
|
||||||
int n = xValues.size();
|
BigDecimal b = values.get(n / 2);
|
||||||
BigDecimal sumX = BigDecimal.ZERO;
|
return a.add(b).divide(BigDecimal.valueOf(2), 6, RoundingMode.HALF_UP);
|
||||||
BigDecimal sumY = BigDecimal.ZERO;
|
|
||||||
BigDecimal sumXY = BigDecimal.ZERO;
|
|
||||||
BigDecimal sumX2 = BigDecimal.ZERO;
|
|
||||||
|
|
||||||
for (int i = 0; i < n; i++) {
|
|
||||||
BigDecimal x = xValues.get(i);
|
|
||||||
BigDecimal y = BigDecimal.valueOf(yValues.get(i));
|
|
||||||
sumX = sumX.add(x);
|
|
||||||
sumY = sumY.add(y);
|
|
||||||
sumXY = sumXY.add(x.multiply(y));
|
|
||||||
sumX2 = sumX2.add(x.multiply(x));
|
|
||||||
}
|
|
||||||
|
|
||||||
BigDecimal nDecimal = BigDecimal.valueOf(n);
|
|
||||||
BigDecimal denominator = nDecimal.multiply(sumX2).subtract(sumX.multiply(sumX));
|
|
||||||
if (denominator.compareTo(BigDecimal.ZERO) == 0) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// slope = (n*ΣXY - ΣX*ΣY) / (n*ΣX² - (ΣX)²)
|
|
||||||
BigDecimal slope = nDecimal.multiply(sumXY).subtract(sumX.multiply(sumY))
|
|
||||||
.divide(denominator, 6, RoundingMode.HALF_UP);
|
|
||||||
|
|
||||||
// intercept = (ΣY - slope*ΣX) / n
|
|
||||||
BigDecimal intercept = sumY.subtract(slope.multiply(sumX))
|
|
||||||
.divide(nDecimal, 6, RoundingMode.HALF_UP);
|
|
||||||
|
|
||||||
// 计算R²
|
|
||||||
BigDecimal meanY = sumY.divide(nDecimal, 6, RoundingMode.HALF_UP);
|
|
||||||
BigDecimal ssTotal = BigDecimal.ZERO;
|
|
||||||
BigDecimal ssResidual = BigDecimal.ZERO;
|
|
||||||
|
|
||||||
for (int i = 0; i < n; i++) {
|
|
||||||
BigDecimal x = xValues.get(i);
|
|
||||||
BigDecimal y = BigDecimal.valueOf(yValues.get(i));
|
|
||||||
BigDecimal predictedY = slope.multiply(x).add(intercept);
|
|
||||||
BigDecimal diff = y.subtract(meanY);
|
|
||||||
ssTotal = ssTotal.add(diff.multiply(diff));
|
|
||||||
BigDecimal residual = y.subtract(predictedY);
|
|
||||||
ssResidual = ssResidual.add(residual.multiply(residual));
|
|
||||||
}
|
|
||||||
|
|
||||||
BigDecimal r2 = BigDecimal.ONE;
|
|
||||||
if (ssTotal.compareTo(BigDecimal.ZERO) > 0) {
|
|
||||||
r2 = BigDecimal.ONE.subtract(ssResidual.divide(ssTotal, 6, RoundingMode.HALF_UP));
|
|
||||||
}
|
|
||||||
|
|
||||||
return new RegressionResult(slope, intercept, r2);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成拟合线端点
|
|
||||||
*/
|
|
||||||
private List<AcidOeeRegressionVo.RegressionLinePointVo> generateLinePoints(
|
|
||||||
List<AcidOeeRegressionVo.RegressionPointVo> points,
|
|
||||||
RegressionResult result) {
|
|
||||||
if (points.isEmpty()) {
|
|
||||||
return Collections.emptyList();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 找到X轴的最小值和最大值
|
|
||||||
BigDecimal minX = points.stream()
|
|
||||||
.map(AcidOeeRegressionVo.RegressionPointVo::getWeightTon)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.min(BigDecimal::compareTo)
|
|
||||||
.orElse(BigDecimal.ZERO);
|
|
||||||
|
|
||||||
BigDecimal maxX = points.stream()
|
|
||||||
.map(AcidOeeRegressionVo.RegressionPointVo::getWeightTon)
|
|
||||||
.filter(Objects::nonNull)
|
|
||||||
.max(BigDecimal::compareTo)
|
|
||||||
.orElse(BigDecimal.ZERO);
|
|
||||||
|
|
||||||
// 计算对应的Y值
|
|
||||||
BigDecimal y1 = result.slope.multiply(minX).add(result.intercept);
|
|
||||||
BigDecimal y2 = result.slope.multiply(maxX).add(result.intercept);
|
|
||||||
|
|
||||||
AcidOeeRegressionVo.RegressionLinePointVo p1 = new AcidOeeRegressionVo.RegressionLinePointVo();
|
|
||||||
p1.setWeightTon(minX);
|
|
||||||
p1.setDurationMin(y1);
|
|
||||||
|
|
||||||
AcidOeeRegressionVo.RegressionLinePointVo p2 = new AcidOeeRegressionVo.RegressionLinePointVo();
|
|
||||||
p2.setWeightTon(maxX);
|
|
||||||
p2.setDurationMin(y2);
|
|
||||||
|
|
||||||
return Arrays.asList(p1, p2);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -477,20 +479,7 @@ public class AcidOeeServiceImpl implements IAcidOeeService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
// 回归相关逻辑已下线:理论节拍统一由“优良日统计口径”产生
|
||||||
* 回归结果内部类
|
|
||||||
*/
|
|
||||||
private static class RegressionResult {
|
|
||||||
final BigDecimal slope;
|
|
||||||
final BigDecimal intercept;
|
|
||||||
final BigDecimal r2;
|
|
||||||
|
|
||||||
RegressionResult(BigDecimal slope, BigDecimal intercept, BigDecimal r2) {
|
|
||||||
this.slope = slope;
|
|
||||||
this.intercept = intercept;
|
|
||||||
this.r2 = r2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 内部统计类:某一 stopType 的总损失时间与次数
|
* 内部统计类:某一 stopType 的总损失时间与次数
|
||||||
|
|||||||
@@ -89,8 +89,12 @@ public class Klptcm1ProStoppageServiceImpl implements IKlptcm1ProStoppageService
|
|||||||
lqw.le(Klptcm1ProStoppage::getStartDate, endDateWithTime);
|
lqw.le(Klptcm1ProStoppage::getStartDate, endDateWithTime);
|
||||||
}
|
}
|
||||||
lqw.eq(bo.getDURATION() != null, Klptcm1ProStoppage::getDuration, bo.getDURATION());
|
lqw.eq(bo.getDURATION() != null, Klptcm1ProStoppage::getDuration, bo.getDURATION());
|
||||||
// 只查询持续时间大于等于5分钟(300秒)的停机记录
|
// 默认只统计持续时间大于等于5分钟(300秒)的停机记录;
|
||||||
lqw.ge(Klptcm1ProStoppage::getDuration, 300);
|
// 若 params.includeShortDuration=true,则放开短停机,用于 OEE 性能稼动率的“完整停机时间”统计。
|
||||||
|
boolean includeShort = params != null && Boolean.TRUE.equals(params.get("includeShortDuration"));
|
||||||
|
if (!includeShort) {
|
||||||
|
lqw.ge(Klptcm1ProStoppage::getDuration, 300);
|
||||||
|
}
|
||||||
lqw.eq(bo.getInsDate() != null, Klptcm1ProStoppage::getInsDate, bo.getInsDate());
|
lqw.eq(bo.getInsDate() != null, Klptcm1ProStoppage::getInsDate, bo.getInsDate());
|
||||||
lqw.eq(StringUtils.isNotBlank(bo.getStopType()), Klptcm1ProStoppage::getStopType, bo.getStopType());
|
lqw.eq(StringUtils.isNotBlank(bo.getStopType()), Klptcm1ProStoppage::getStopType, bo.getStopType());
|
||||||
//倒序
|
//倒序
|
||||||
|
|||||||
@@ -54,51 +54,6 @@
|
|||||||
ORDER BY stat_date ASC
|
ORDER BY stat_date ASC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
<!-- 回归数据散点结果映射 -->
|
|
||||||
<resultMap id="RegressionPointResultMap" type="com.klp.pocket.acid.domain.vo.AcidOeeRegressionVo$RegressionPointVo">
|
|
||||||
<result column="weight_ton" property="weightTon" jdbcType="DECIMAL"/>
|
|
||||||
<result column="coil_count" property="coilCount" jdbcType="BIGINT"/>
|
|
||||||
<result column="duration_min" property="durationMin" jdbcType="BIGINT"/>
|
|
||||||
<result column="action_id" property="actionId" jdbcType="VARCHAR"/>
|
|
||||||
<result column="create_time" property="createTime" jdbcType="VARCHAR"/>
|
|
||||||
</resultMap>
|
|
||||||
|
|
||||||
<!-- 查询回归数据散点 -->
|
|
||||||
<select id="selectRegressionPoints" resultMap="RegressionPointResultMap">
|
|
||||||
SELECT
|
|
||||||
-- 重量(吨):出口重量
|
|
||||||
e.EXIT_WEIGHT AS weight_ton,
|
|
||||||
-- 卷数:固定为1(每条记录代表一卷)
|
|
||||||
1 AS coil_count,
|
|
||||||
-- 时长(分钟):结束时间 - 开始时间,转换为分钟
|
|
||||||
CASE
|
|
||||||
WHEN e.START_DATE IS NOT NULL AND e.END_DATE IS NOT NULL THEN
|
|
||||||
TIMESTAMPDIFF(MINUTE, e.START_DATE, e.END_DATE)
|
|
||||||
ELSE NULL
|
|
||||||
END AS duration_min,
|
|
||||||
-- 关联ID:使用卷号作为标识
|
|
||||||
e.ENCOILID AS action_id,
|
|
||||||
-- 创建时间
|
|
||||||
DATE_FORMAT(e.INSDATE, '%Y-%m-%d %H:%i:%s') AS create_time
|
|
||||||
FROM klptcm1_pdo_excoil e
|
|
||||||
WHERE 1=1
|
|
||||||
<if test="startDate != null and startDate != ''">
|
|
||||||
AND DATE(e.INSDATE) >= #{startDate}
|
|
||||||
</if>
|
|
||||||
<if test="endDate != null and endDate != ''">
|
|
||||||
AND DATE(e.INSDATE) <= #{endDate}
|
|
||||||
</if>
|
|
||||||
-- 过滤掉无效数据:必须有开始和结束时间,且时长大于0
|
|
||||||
AND e.START_DATE IS NOT NULL
|
|
||||||
AND e.END_DATE IS NOT NULL
|
|
||||||
AND e.END_DATE > e.START_DATE
|
|
||||||
AND e.EXIT_WEIGHT IS NOT NULL
|
|
||||||
AND e.EXIT_WEIGHT > 0
|
|
||||||
ORDER BY e.INSDATE ASC
|
|
||||||
-- 限制最多返回最近6个月的数据(避免数据量过大)
|
|
||||||
LIMIT 10000
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<!-- 查询每日的钢卷号和重量(用于良品/次品判定) -->
|
<!-- 查询每日的钢卷号和重量(用于良品/次品判定) -->
|
||||||
<select id="selectCoilInfoByDate" resultType="com.klp.pocket.acid.mapper.AcidOeeMapper$CoilInfoByDate">
|
<select id="selectCoilInfoByDate" resultType="com.klp.pocket.acid.mapper.AcidOeeMapper$CoilInfoByDate">
|
||||||
SELECT
|
SELECT
|
||||||
@@ -115,5 +70,26 @@
|
|||||||
ORDER BY e.INSDATE ASC, e.ENCOILID ASC
|
ORDER BY e.INSDATE ASC, e.ENCOILID ASC
|
||||||
</select>
|
</select>
|
||||||
|
|
||||||
|
<!-- 查询卷级生产节拍(min/吨):(END_DATE - START_DATE)/EXIT_WEIGHT -->
|
||||||
|
<select id="selectCoilCycleMinPerTon" resultType="java.math.BigDecimal">
|
||||||
|
SELECT
|
||||||
|
-- 生产节拍(分钟/吨)
|
||||||
|
TIMESTAMPDIFF(MINUTE, e.START_DATE, e.END_DATE) / e.EXIT_WEIGHT AS cycle_min_per_ton
|
||||||
|
FROM klptcm1_pdo_excoil e
|
||||||
|
WHERE 1 = 1
|
||||||
|
<if test="startDate != null and startDate != ''">
|
||||||
|
AND DATE(e.INSDATE) >= #{startDate}
|
||||||
|
</if>
|
||||||
|
<if test="endDate != null and endDate != ''">
|
||||||
|
AND DATE(e.INSDATE) <= #{endDate}
|
||||||
|
</if>
|
||||||
|
AND e.START_DATE IS NOT NULL
|
||||||
|
AND e.END_DATE IS NOT NULL
|
||||||
|
AND e.END_DATE > e.START_DATE
|
||||||
|
AND e.EXIT_WEIGHT IS NOT NULL
|
||||||
|
AND e.EXIT_WEIGHT > 0
|
||||||
|
AND TIMESTAMPDIFF(MINUTE, e.START_DATE, e.END_DATE) > 0
|
||||||
|
</select>
|
||||||
|
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import request from '@/utils/request'
|
import request from '@/utils/request'
|
||||||
|
|
||||||
// 导出 Word 报表
|
// 导出 Word 报表(酸轧线)
|
||||||
export function exportOeeWord(query) {
|
export function exportOeeWord(query) {
|
||||||
return request({
|
return request({
|
||||||
url: '/oee/line/exportWord',
|
url: '/oee/line/acid/exportWord',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: query,
|
params: query,
|
||||||
responseType: 'blob'
|
responseType: 'blob'
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// OEE 产线 KPI + 趋势汇总
|
// OEE 产线 KPI + 趋势汇总(酸轧线)
|
||||||
export function fetchOeeSummary(query) {
|
export function fetchOeeSummary(query) {
|
||||||
return request({
|
return request({
|
||||||
url: '/oee/line/summary',
|
url: '/oee/line/acid/summary',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: query,
|
params: query,
|
||||||
timeout: 120000
|
timeout: 120000
|
||||||
@@ -23,37 +23,37 @@ export function fetchOeeSummary(query) {
|
|||||||
// OEE 产线 KPI + 趋势汇总(异步任务接口暂保留,当前前端不使用)
|
// OEE 产线 KPI + 趋势汇总(异步任务接口暂保留,当前前端不使用)
|
||||||
export function createOeeSummaryJob(query) {
|
export function createOeeSummaryJob(query) {
|
||||||
return request({
|
return request({
|
||||||
url: '/oee/line/summary/job',
|
url: '/oee/line/acid/summary/job',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: query,
|
params: query,
|
||||||
timeout: 120000
|
timeout: 120000
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 7 大损失汇总
|
// 7 大损失汇总(酸轧线)
|
||||||
export function fetchOeeLoss7(query) {
|
export function fetchOeeLoss7(query) {
|
||||||
return request({
|
return request({
|
||||||
url: '/oee/line/loss7',
|
url: '/oee/line/acid/loss7',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: query,
|
params: query,
|
||||||
timeout: 120000
|
timeout: 120000
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 停机/损失事件明细
|
// 停机/损失事件明细(酸轧线)
|
||||||
export function fetchOeeEvents(query) {
|
export function fetchOeeEvents(query) {
|
||||||
return request({
|
return request({
|
||||||
url: '/oee/line/events',
|
url: '/oee/line/acid/events',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: query,
|
params: query,
|
||||||
timeout: 120000
|
timeout: 120000
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 理论节拍回归结果(含散点与拟合线)
|
// 理论节拍(统计口径:优良日中位数)
|
||||||
export function fetchOeeTheoryCycleRegression(query) {
|
export function fetchOeeIdealCycle(query) {
|
||||||
return request({
|
return request({
|
||||||
url: '/oee/line/theoryCycle/regression',
|
url: '/oee/line/acid/idealCycle',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: query,
|
params: query,
|
||||||
timeout: 120000
|
timeout: 120000
|
||||||
@@ -63,7 +63,7 @@ export function fetchOeeTheoryCycleRegression(query) {
|
|||||||
// 理论节拍回归:创建异步任务,返回 jobId + wsType,通过 WebSocket 推送进度/结果
|
// 理论节拍回归:创建异步任务,返回 jobId + wsType,通过 WebSocket 推送进度/结果
|
||||||
export function createOeeTheoryCycleRegressionJob(query) {
|
export function createOeeTheoryCycleRegressionJob(query) {
|
||||||
return request({
|
return request({
|
||||||
url: '/oee/line/theoryCycle/regression/job',
|
url: '/oee/line/acid/theoryCycle/regression/job',
|
||||||
method: 'get',
|
method: 'get',
|
||||||
params: query,
|
params: query,
|
||||||
timeout: 120000
|
timeout: 120000
|
||||||
|
|||||||
127
klp-ui/src/views/da/oee/components/OeeLossPareto.vue
Normal file
127
klp-ui/src/views/da/oee/components/OeeLossPareto.vue
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
<template>
|
||||||
|
<div class="oee-chart" ref="chartRef"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'OeeLossPareto',
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
chart: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initChart()
|
||||||
|
window.addEventListener('resize', this.handleResize)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.handleResize)
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.dispose()
|
||||||
|
this.chart = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
data: {
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
handler() {
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toFixed3(val) {
|
||||||
|
const n = Number(val)
|
||||||
|
return Number.isFinite(n) ? n.toFixed(3) : '-'
|
||||||
|
},
|
||||||
|
initChart() {
|
||||||
|
if (!this.$refs.chartRef) return
|
||||||
|
this.chart = echarts.init(this.$refs.chartRef)
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
if (!this.chart) this.initChart()
|
||||||
|
if (!this.chart) return
|
||||||
|
const list = Array.isArray(this.data) ? this.data : []
|
||||||
|
const names = list.map(d => d.lossCategoryName)
|
||||||
|
const mins = list.map(d => Number(d.lossTimeMin || 0))
|
||||||
|
const percents = list.map(d => Number(d.lossTimeRate || 0))
|
||||||
|
const cum = []
|
||||||
|
percents.reduce((acc, cur) => {
|
||||||
|
const v = acc + cur
|
||||||
|
cum.push(Math.min(100, v))
|
||||||
|
return v
|
||||||
|
}, 0)
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
formatter: params => {
|
||||||
|
const title = (params && params[0] && params[0].axisValue) || ''
|
||||||
|
const lines = (params || []).map(p => {
|
||||||
|
const suffix = p.seriesName === '累计占比' ? ' %' : ' min'
|
||||||
|
return `${p.marker}${p.seriesName}:${this.toFixed3(p.value)}${suffix}`
|
||||||
|
})
|
||||||
|
return [title, ...lines].join('<br/>')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: { left: 40, right: 60, top: 30, bottom: 80 },
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: names,
|
||||||
|
axisLabel: { interval: 0, rotate: 40, fontSize: 10 }
|
||||||
|
},
|
||||||
|
yAxis: [
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '损失时间 (min)',
|
||||||
|
axisLabel: { formatter: v => this.toFixed3(v) }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
type: 'value',
|
||||||
|
name: '累计占比 (%)',
|
||||||
|
min: 0,
|
||||||
|
max: 100,
|
||||||
|
position: 'right',
|
||||||
|
axisLabel: { formatter: v => this.toFixed3(v) }
|
||||||
|
}
|
||||||
|
],
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '损失时间',
|
||||||
|
type: 'bar',
|
||||||
|
data: mins,
|
||||||
|
itemStyle: { color: '#F56C6C' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '累计占比',
|
||||||
|
type: 'line',
|
||||||
|
yAxisIndex: 1,
|
||||||
|
data: cum,
|
||||||
|
smooth: true,
|
||||||
|
itemStyle: { color: '#409EFF' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
this.chart.setOption(option)
|
||||||
|
},
|
||||||
|
handleResize() {
|
||||||
|
this.chart && this.chart.resize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.oee-chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 260px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
106
klp-ui/src/views/da/oee/components/OeeStoppageTop.vue
Normal file
106
klp-ui/src/views/da/oee/components/OeeStoppageTop.vue
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
<template>
|
||||||
|
<div class="oee-chart" ref="chartRef"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'OeeStoppageTop',
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
},
|
||||||
|
topN: {
|
||||||
|
type: Number,
|
||||||
|
default: 10
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
chart: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initChart()
|
||||||
|
window.addEventListener('resize', this.handleResize)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.handleResize)
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.dispose()
|
||||||
|
this.chart = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
data: {
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
handler() {
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toFixed3(val) {
|
||||||
|
const n = Number(val)
|
||||||
|
return Number.isFinite(n) ? n.toFixed(3) : '-'
|
||||||
|
},
|
||||||
|
initChart() {
|
||||||
|
if (!this.$refs.chartRef) return
|
||||||
|
this.chart = echarts.init(this.$refs.chartRef)
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
if (!this.chart) this.initChart()
|
||||||
|
if (!this.chart) return
|
||||||
|
const list = Array.isArray(this.data) ? this.data.slice() : []
|
||||||
|
list.sort((a, b) => (b.duration || 0) - (a.duration || 0))
|
||||||
|
const top = list.slice(0, this.topN)
|
||||||
|
const names = top.map((d, idx) => `${idx + 1}. ${d.stopType || '未知'}`)
|
||||||
|
const mins = top.map(d => Number(((d.duration || 0) / 60).toFixed(3)))
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
axisPointer: { type: 'shadow' },
|
||||||
|
formatter: params => {
|
||||||
|
const p = params && params[0]
|
||||||
|
if (!p) return ''
|
||||||
|
return `${p.name}<br/>${p.marker}${p.seriesName}:${this.toFixed3(p.value)} min`
|
||||||
|
}
|
||||||
|
},
|
||||||
|
grid: { left: 140, right: 20, top: 20, bottom: 20 },
|
||||||
|
xAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: '停机时间 (min)',
|
||||||
|
axisLabel: { formatter: v => this.toFixed3(v) }
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: names,
|
||||||
|
axisLabel: { fontSize: 11 }
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '停机时间 (min)',
|
||||||
|
type: 'bar',
|
||||||
|
data: mins,
|
||||||
|
itemStyle: { color: '#67C23A' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
this.chart.setOption(option)
|
||||||
|
},
|
||||||
|
handleResize() {
|
||||||
|
this.chart && this.chart.resize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.oee-chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 260px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
97
klp-ui/src/views/da/oee/components/OeeTrendChart.vue
Normal file
97
klp-ui/src/views/da/oee/components/OeeTrendChart.vue
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
<template>
|
||||||
|
<div class="oee-chart" ref="chartRef"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'OeeTrendChart',
|
||||||
|
props: {
|
||||||
|
data: {
|
||||||
|
type: Array,
|
||||||
|
default: () => []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
chart: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
this.initChart()
|
||||||
|
window.addEventListener('resize', this.handleResize)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.handleResize)
|
||||||
|
if (this.chart) {
|
||||||
|
this.chart.dispose()
|
||||||
|
this.chart = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
data: {
|
||||||
|
deep: true,
|
||||||
|
immediate: true,
|
||||||
|
handler() {
|
||||||
|
this.render()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toFixed3(val) {
|
||||||
|
const n = Number(val)
|
||||||
|
return Number.isFinite(n) ? n.toFixed(3) : '-'
|
||||||
|
},
|
||||||
|
initChart() {
|
||||||
|
if (!this.$refs.chartRef) return
|
||||||
|
this.chart = echarts.init(this.$refs.chartRef)
|
||||||
|
},
|
||||||
|
render() {
|
||||||
|
if (!this.chart) this.initChart()
|
||||||
|
if (!this.chart) return
|
||||||
|
const list = Array.isArray(this.data) ? this.data : []
|
||||||
|
const x = list.map(d => d.statDate)
|
||||||
|
const oee = list.map(d => Number(d.oee || 0))
|
||||||
|
const a = list.map(d => Number(d.availability || 0))
|
||||||
|
const p = list.map(d => Number(d.performanceTon || 0))
|
||||||
|
const q = list.map(d => Number(d.quality || 0))
|
||||||
|
const option = {
|
||||||
|
tooltip: {
|
||||||
|
trigger: 'axis',
|
||||||
|
formatter: params => {
|
||||||
|
const title = (params && params[0] && params[0].axisValue) || ''
|
||||||
|
const lines = (params || []).map(p0 => `${p0.marker}${p0.seriesName}:${this.toFixed3(p0.value)} %`)
|
||||||
|
return [title, ...lines].join('<br/>')
|
||||||
|
}
|
||||||
|
},
|
||||||
|
legend: { top: 0, left: 'center' },
|
||||||
|
grid: { left: 40, right: 20, top: 30, bottom: 40 },
|
||||||
|
xAxis: { type: 'category', data: x },
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: '百分比 (%)',
|
||||||
|
axisLabel: { formatter: v => this.toFixed3(v) }
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{ name: 'OEE', type: 'line', data: oee, smooth: true },
|
||||||
|
{ name: 'A', type: 'line', data: a, smooth: true },
|
||||||
|
{ name: 'P_ton', type: 'line', data: p, smooth: true },
|
||||||
|
{ name: 'Q', type: 'line', data: q, smooth: true }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
this.chart.setOption(option)
|
||||||
|
},
|
||||||
|
handleResize() {
|
||||||
|
this.chart && this.chart.resize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.oee-chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 260px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,745 @@
|
|||||||
|
<template>
|
||||||
|
<div class="oee-report-page">
|
||||||
|
<!-- 查询条件概览(去掉大标题,只保留区间提示) -->
|
||||||
|
<el-card class="oee-header-card" shadow="never">
|
||||||
|
<div class="oee-header">
|
||||||
|
<div class="oee-title-block">
|
||||||
|
<div class="oee-subtitle">
|
||||||
|
查询区间:
|
||||||
|
<span v-if="queryRange && queryRange.length === 2">
|
||||||
|
{{ queryRange[0] }} ~ {{ queryRange[1] }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="oee-query-bar">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="queryRange"
|
||||||
|
type="daterange"
|
||||||
|
unlink-panels
|
||||||
|
size="small"
|
||||||
|
start-placeholder="开始日期"
|
||||||
|
end-placeholder="结束日期"
|
||||||
|
value-format="yyyy-MM-dd"
|
||||||
|
:picker-options="pickerOptions"
|
||||||
|
/>
|
||||||
|
<el-button
|
||||||
|
type="primary"
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-search"
|
||||||
|
@click="handleSearch"
|
||||||
|
>
|
||||||
|
查询
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-refresh"
|
||||||
|
@click="handleReset"
|
||||||
|
>
|
||||||
|
重置
|
||||||
|
</el-button>
|
||||||
|
<el-button
|
||||||
|
size="small"
|
||||||
|
icon="el-icon-document"
|
||||||
|
@click="handleExportWord"
|
||||||
|
>
|
||||||
|
导出 Word
|
||||||
|
</el-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
|
||||||
|
<el-row :gutter="16" class="oee-main-row">
|
||||||
|
<!-- 左侧:报表主体(Word 风格) -->
|
||||||
|
<el-col :span="18">
|
||||||
|
<el-card class="oee-word-card" shadow="never">
|
||||||
|
<!-- KPI 一览(报表头部表格) -->
|
||||||
|
<div class="oee-section-title">一、KPI 总览(酸轧线 SY)</div>
|
||||||
|
<div class="oee-help-text">
|
||||||
|
展示所选期间酸轧线整体 OEE 及 A/P/Q 等关键指标,用于快速判断本期综合表现好坏。
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
:data="[kpi]"
|
||||||
|
border
|
||||||
|
size="mini"
|
||||||
|
class="oee-kpi-table"
|
||||||
|
v-loading="loading.summary"
|
||||||
|
>
|
||||||
|
<el-table-column prop="oee" label="OEE (%)" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ formatPercent(scope.row.oee) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="availability" label="时间稼动率 A (%)" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ formatPercent(scope.row.availability) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="performanceTon" label="性能稼动率 P_ton (%)" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ formatPercent(scope.row.performanceTon) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="quality" label="良品率 Q (%)" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ formatPercent(scope.row.quality) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="loadingTimeMin" label="负荷时间 (min)" align="center" />
|
||||||
|
<el-table-column prop="downtimeMin" label="停机时间 (min)" align="center" />
|
||||||
|
<el-table-column prop="runTimeMin" label="运转时间 (min)" align="center" />
|
||||||
|
<el-table-column prop="totalOutputTon" label="总产量 (吨)" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ formatNumber(scope.row.totalOutputTon) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="totalOutputCoil" label="总产量 (卷)" align="center" />
|
||||||
|
<el-table-column prop="goodOutputTon" label="良品量 (吨)" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ formatNumber(scope.row.goodOutputTon) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="defectOutputTon" label="次品量 (吨)" align="center">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
<span>{{ formatNumber(scope.row.defectOutputTon) }}</span>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 日明细(趋势表格风格,方便导出 Word) -->
|
||||||
|
<div class="oee-section-title">二、日明细(用于趋势分析)</div>
|
||||||
|
<div class="oee-help-text">
|
||||||
|
按天拆分 A/P/Q 及产量等指标,用于观察本月趋势、波动点以及与重大事件的对应关系。
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
:data="summaryList"
|
||||||
|
border
|
||||||
|
size="mini"
|
||||||
|
class="oee-daily-table"
|
||||||
|
v-loading="loading.summary"
|
||||||
|
>
|
||||||
|
<el-table-column prop="statDate" label="日期" />
|
||||||
|
<el-table-column prop="oee" label="OEE (%)" >
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ formatPercent(scope.row.oee) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="availability" label="A (%)" >
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ formatPercent(scope.row.availability) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="performanceTon" label="P_ton (%)">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ formatPercent(scope.row.performanceTon) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="quality" label="Q (%)">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ formatPercent(scope.row.quality) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="loadingTimeMin" label="负荷 (min)"/>
|
||||||
|
<el-table-column prop="downtimeMin" label="停机 (min)"/>
|
||||||
|
<el-table-column prop="runTimeMin" label="运转 (min)"/>
|
||||||
|
<el-table-column prop="totalOutputTon" label="总产量 (吨)">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ formatNumber(scope.row.totalOutputTon) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="totalOutputCoil" label="总产量 (卷)" />
|
||||||
|
<el-table-column prop="goodOutputTon" label="良品 (吨)">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ formatNumber(scope.row.goodOutputTon) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="defectOutputTon" label="次品 (吨)">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ formatNumber(scope.row.defectOutputTon) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- OEE/A/P/Q 趋势图 -->
|
||||||
|
<oee-trend-chart :data="summaryList" />
|
||||||
|
|
||||||
|
<!-- 理论节拍(统计口径) -->
|
||||||
|
<div class="oee-section-title">三、理论节拍(统计口径)</div>
|
||||||
|
<div class="oee-help-text">
|
||||||
|
基于历史“优良日”统计得到的理论节拍(中位数),用于作为性能稼动率计算的稳定标尺。
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="idealCycle && idealCycle.dailyComparePoints && idealCycle.dailyComparePoints.length"
|
||||||
|
class="oee-reg-section"
|
||||||
|
>
|
||||||
|
<div class="reg-chart-block">
|
||||||
|
<div class="reg-chart-title">日粒度:理论耗时 vs 实际运转时间</div>
|
||||||
|
<div class="oee-help-text">
|
||||||
|
按天对比“理想应耗时间”和“实际运转时间”,可一眼看出哪几天效率偏低、存在明显损失空间。
|
||||||
|
</div>
|
||||||
|
<div ref="regDailyCompareChart" class="reg-chart"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 7 大损失 -->
|
||||||
|
<div class="oee-section-title">四、7 大损失汇总(按 stop_type 分类)</div>
|
||||||
|
<div class="oee-help-text">
|
||||||
|
将所有停机事件按 stop_type 归类,统计时间占比和次数,用于确定“先从哪几类损失下手改善”。
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
:data="loss7List"
|
||||||
|
border
|
||||||
|
size="mini"
|
||||||
|
class="oee-loss7-table"
|
||||||
|
v-loading="loading.loss7"
|
||||||
|
>
|
||||||
|
<el-table-column prop="lossCategoryName" label="损失类别" min-width="160" />
|
||||||
|
<el-table-column prop="lossTimeMin" label="损失时间 (min)" width="140" />
|
||||||
|
<el-table-column prop="lossTimeRate" label="占比 (%)" width="110">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ formatPercent(scope.row.lossTimeRate) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="count" label="次数" width="90" />
|
||||||
|
<el-table-column prop="avgDurationMin" label="平均时长 (min)" width="140">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ formatNumber(scope.row.avgDurationMin) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 7 大损失帕累托图 -->
|
||||||
|
<oee-loss-pareto :data="loss7List" />
|
||||||
|
|
||||||
|
<!-- 停机事件明细 -->
|
||||||
|
<div class="oee-section-title">五、停机/损失事件明细</div>
|
||||||
|
<div class="oee-help-text">
|
||||||
|
罗列每一条停机/损失事件,包含时间段、区域、机组和备注,方便班组和工艺人员对照现场记录进行原因分析。
|
||||||
|
</div>
|
||||||
|
<el-table
|
||||||
|
:data="eventList"
|
||||||
|
border
|
||||||
|
size="mini"
|
||||||
|
class="oee-events-table"
|
||||||
|
v-loading="loading.events"
|
||||||
|
>
|
||||||
|
<el-table-column prop="startDate" label="开始时间" min-width="150">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ formatDateTime(scope.row.startDate) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="endDate" label="结束时间" min-width="150">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ formatDateTime(scope.row.endDate) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="duration" label="时长 (s)" width="100" />
|
||||||
|
<el-table-column prop="durationMin" label="时长 (min)" width="110">
|
||||||
|
<template slot-scope="scope">
|
||||||
|
{{ Math.max(1, Math.floor((scope.row.duration || 0) / 60)) }}
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="stopType" label="停机类型 (stop_type)" min-width="160" />
|
||||||
|
<el-table-column prop="area" label="区域" width="100" />
|
||||||
|
<el-table-column prop="unit" label="机组" width="100" />
|
||||||
|
<el-table-column prop="shift" label="班次" width="80" />
|
||||||
|
<el-table-column prop="crew" label="班组" width="80" />
|
||||||
|
<el-table-column prop="remark" label="备注" min-width="180" show-overflow-tooltip />
|
||||||
|
</el-table>
|
||||||
|
|
||||||
|
<!-- 停机 TopN 条形图 -->
|
||||||
|
<oee-stoppage-top :data="eventList" :top-n="10" />
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
|
<!-- 右侧:公式与口径说明(支撑材料) -->
|
||||||
|
<el-col :span="6">
|
||||||
|
<el-card class="oee-formula-card" shadow="never">
|
||||||
|
<div slot="header" class="clearfix">
|
||||||
|
<span>公式与口径说明</span>
|
||||||
|
</div>
|
||||||
|
<div class="formula-block">
|
||||||
|
<div class="formula-title">OEE 总公式</div>
|
||||||
|
<div class="formula-eq">OEE = A × P × Q</div>
|
||||||
|
<ul class="formula-list">
|
||||||
|
<li>A(时间稼动率) = (负荷时间 − 停机时间) / 负荷时间</li>
|
||||||
|
<li>P(性能稼动率,吨维度) = (理论节拍 × 产量吨) / 实际运转时间</li>
|
||||||
|
<li>Q(良品率) = 良品吨 / 总产量吨</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="formula-block">
|
||||||
|
<div class="formula-title">关键字段定义</div>
|
||||||
|
<ul class="formula-list">
|
||||||
|
<li><b>负荷时间</b>:计划生产时间扣除计划停机后的时间</li>
|
||||||
|
<li><b>停机时间</b>:所有停机/中断(按 stop_type 汇总)的总时长</li>
|
||||||
|
<li><b>实际运转时间</b>:负荷时间 − 停机时间</li>
|
||||||
|
<li><b>理论节拍</b>:按“优良日统计口径”得到的稳定节拍(分钟/吨),由理论节拍接口提供</li>
|
||||||
|
<li><b>良品/次品</b>:按 WMS `quality_status` 判断,C+/C/C-/D+/D/D- 为次品</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="formula-block" v-if="idealCycle">
|
||||||
|
<div class="formula-title">当前理论节拍(统计口径)</div>
|
||||||
|
<el-descriptions :column="1" size="small" class="reg-desc">
|
||||||
|
<el-descriptions-item label="理论节拍 (min/吨)">
|
||||||
|
{{ formatNumber(idealCycle.idealCycleTimeMinPerTon) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="生产节拍中位数 (min/吨)">
|
||||||
|
{{ formatNumber(idealCycle.medianCycleTimeMinPerTon) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="样本天数">
|
||||||
|
{{ idealCycle.sampleDays || 0 }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="吨数下限 (吨)">
|
||||||
|
{{ formatNumber(idealCycle.minWeightTon) }}
|
||||||
|
</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="停机占比上限">
|
||||||
|
{{
|
||||||
|
idealCycle.maxDowntimeRate != null
|
||||||
|
? (Number(idealCycle.maxDowntimeRate) * 100).toFixed(0) + '%'
|
||||||
|
: '-'
|
||||||
|
}}
|
||||||
|
</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
</div>
|
||||||
|
</el-card>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import {
|
||||||
|
exportOeeWord,
|
||||||
|
fetchOeeSummary,
|
||||||
|
fetchOeeLoss7,
|
||||||
|
fetchOeeEvents,
|
||||||
|
fetchOeeIdealCycle
|
||||||
|
} from '@/api/da/oee'
|
||||||
|
import * as echarts from 'echarts'
|
||||||
|
import OeeTrendChart from './components/OeeTrendChart.vue'
|
||||||
|
import OeeLossPareto from './components/OeeLossPareto.vue'
|
||||||
|
import OeeStoppageTop from './components/OeeStoppageTop.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'DaOeeIndex',
|
||||||
|
components: {
|
||||||
|
OeeTrendChart,
|
||||||
|
OeeLossPareto,
|
||||||
|
OeeStoppageTop
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
const today = new Date()
|
||||||
|
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1)
|
||||||
|
return {
|
||||||
|
queryRange: [
|
||||||
|
this.formatDate(firstDay),
|
||||||
|
this.formatDate(today)
|
||||||
|
],
|
||||||
|
pickerOptions: {
|
||||||
|
disabledDate(time) {
|
||||||
|
// 不限制选择范围,保留扩展空间
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
summary: false,
|
||||||
|
loss7: false,
|
||||||
|
events: false,
|
||||||
|
idealCycle: false,
|
||||||
|
export: false
|
||||||
|
},
|
||||||
|
summaryList: [],
|
||||||
|
loss7List: [],
|
||||||
|
eventList: [],
|
||||||
|
idealCycle: null,
|
||||||
|
regDailyCompareChart: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
kpi() {
|
||||||
|
const list = Array.isArray(this.summaryList) ? this.summaryList : []
|
||||||
|
if (list.length === 0) {
|
||||||
|
return {
|
||||||
|
oee: 0,
|
||||||
|
availability: 0,
|
||||||
|
performanceTon: 0,
|
||||||
|
quality: 0,
|
||||||
|
loadingTimeMin: 0,
|
||||||
|
downtimeMin: 0,
|
||||||
|
runTimeMin: 0,
|
||||||
|
totalOutputTon: 0,
|
||||||
|
totalOutputCoil: 0,
|
||||||
|
goodOutputTon: 0,
|
||||||
|
defectOutputTon: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let sumLoading = 0
|
||||||
|
let sumDowntime = 0
|
||||||
|
let sumRun = 0
|
||||||
|
let sumTotalTon = 0
|
||||||
|
let sumGoodTon = 0
|
||||||
|
let sumDefectTon = 0
|
||||||
|
let sumCoil = 0
|
||||||
|
let sumOee = 0
|
||||||
|
let sumA = 0
|
||||||
|
let sumP = 0
|
||||||
|
let sumQ = 0
|
||||||
|
|
||||||
|
list.forEach(row => {
|
||||||
|
sumLoading += row.loadingTimeMin || 0
|
||||||
|
sumDowntime += row.downtimeMin || 0
|
||||||
|
sumRun += row.runTimeMin || 0
|
||||||
|
sumTotalTon += Number(row.totalOutputTon || 0)
|
||||||
|
sumGoodTon += Number(row.goodOutputTon || 0)
|
||||||
|
sumDefectTon += Number(row.defectOutputTon || 0)
|
||||||
|
sumCoil += row.totalOutputCoil || 0
|
||||||
|
sumOee += Number(row.oee || 0)
|
||||||
|
sumA += Number(row.availability || 0)
|
||||||
|
sumP += Number(row.performanceTon || 0)
|
||||||
|
sumQ += Number(row.quality || 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
const n = list.length
|
||||||
|
const defectAgg = Math.max(0, sumTotalTon - sumGoodTon)
|
||||||
|
return {
|
||||||
|
oee: n ? sumOee / n : 0,
|
||||||
|
availability: n ? sumA / n : 0,
|
||||||
|
performanceTon: n ? sumP / n : 0,
|
||||||
|
quality: n ? sumQ / n : 0,
|
||||||
|
loadingTimeMin: sumLoading,
|
||||||
|
downtimeMin: sumDowntime,
|
||||||
|
runTimeMin: sumRun,
|
||||||
|
totalOutputTon: sumTotalTon,
|
||||||
|
totalOutputCoil: sumCoil,
|
||||||
|
goodOutputTon: sumGoodTon,
|
||||||
|
defectOutputTon: defectAgg || sumDefectTon
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
created() {
|
||||||
|
this.loadAll()
|
||||||
|
},
|
||||||
|
mounted() {
|
||||||
|
window.addEventListener('resize', this.handleResize)
|
||||||
|
},
|
||||||
|
beforeDestroy() {
|
||||||
|
window.removeEventListener('resize', this.handleResize)
|
||||||
|
if (this.regDailyCompareChart) {
|
||||||
|
this.regDailyCompareChart.dispose()
|
||||||
|
this.regDailyCompareChart = null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
formatDate(d) {
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const m = (`0${d.getMonth() + 1}`).slice(-2)
|
||||||
|
const day = (`0${d.getDate()}`).slice(-2)
|
||||||
|
return `${y}-${m}-${day}`
|
||||||
|
},
|
||||||
|
formatPercent(value) {
|
||||||
|
if (value === null || value === undefined || isNaN(value)) {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
const num = Number(value)
|
||||||
|
return num.toFixed(1)
|
||||||
|
},
|
||||||
|
formatNumber(value) {
|
||||||
|
if (value === null || value === undefined || value === '') {
|
||||||
|
return '-'
|
||||||
|
}
|
||||||
|
const num = Number(value)
|
||||||
|
if (isNaN(num)) return '-'
|
||||||
|
return num.toFixed(2)
|
||||||
|
},
|
||||||
|
formatDateTime(val) {
|
||||||
|
if (!val) return ''
|
||||||
|
// 后端可能返回字符串或时间戳,这里做容错
|
||||||
|
if (typeof val === 'string') return val
|
||||||
|
try {
|
||||||
|
const d = new Date(val)
|
||||||
|
const y = d.getFullYear()
|
||||||
|
const m = (`0${d.getMonth() + 1}`).slice(-2)
|
||||||
|
const day = (`0${d.getDate()}`).slice(-2)
|
||||||
|
const hh = (`0${d.getHours()}`).slice(-2)
|
||||||
|
const mm = (`0${d.getMinutes()}`).slice(-2)
|
||||||
|
const ss = (`0${d.getSeconds()}`).slice(-2)
|
||||||
|
return `${y}-${m}-${day} ${hh}:${mm}:${ss}`
|
||||||
|
} catch (e) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
},
|
||||||
|
buildQuery() {
|
||||||
|
const [start, end] = this.queryRange || []
|
||||||
|
return {
|
||||||
|
startDate: start,
|
||||||
|
endDate: end
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadAll() {
|
||||||
|
await Promise.all([
|
||||||
|
this.loadSummary(),
|
||||||
|
this.loadLoss7(),
|
||||||
|
this.loadEvents(),
|
||||||
|
this.loadIdealCycle()
|
||||||
|
])
|
||||||
|
},
|
||||||
|
async loadSummary() {
|
||||||
|
this.loading.summary = true
|
||||||
|
try {
|
||||||
|
const res = await fetchOeeSummary(this.buildQuery())
|
||||||
|
// 兼容后端直接返回数组或 TableDataInfo { rows, total } 两种结构
|
||||||
|
|
||||||
|
this.summaryList = res.data
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('加载酸轧日汇总失败')
|
||||||
|
} finally {
|
||||||
|
this.loading.summary = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadLoss7() {
|
||||||
|
this.loading.loss7 = true
|
||||||
|
try {
|
||||||
|
const res = await fetchOeeLoss7(this.buildQuery())
|
||||||
|
// 兼容后端直接返回数组或 TableDataInfo { rows, total } 两种结构
|
||||||
|
this.loss7List = res.data
|
||||||
|
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('加载酸轧 7 大损失失败')
|
||||||
|
} finally {
|
||||||
|
this.loading.loss7 = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadEvents() {
|
||||||
|
this.loading.events = true
|
||||||
|
try {
|
||||||
|
const res = await fetchOeeEvents(this.buildQuery())
|
||||||
|
// 后端 TableDataInfo 结构:{ rows, total }
|
||||||
|
this.eventList = (res && Array.isArray(res.rows)) ? res.rows : []
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('加载酸轧停机事件失败')
|
||||||
|
} finally {
|
||||||
|
this.loading.events = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async loadIdealCycle() {
|
||||||
|
this.loading.idealCycle = true
|
||||||
|
try {
|
||||||
|
const res = await fetchOeeIdealCycle(this.buildQuery())
|
||||||
|
this.idealCycle = res && res.data ? res.data : null
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.renderIdealCycleChart()
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('加载理论节拍失败')
|
||||||
|
} finally {
|
||||||
|
this.loading.idealCycle = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleSearch() {
|
||||||
|
if (!this.queryRange || this.queryRange.length !== 2) {
|
||||||
|
this.$message.warning('请选择查询日期范围')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
this.loadAll()
|
||||||
|
},
|
||||||
|
handleReset() {
|
||||||
|
const today = new Date()
|
||||||
|
const firstDay = new Date(today.getFullYear(), today.getMonth(), 1)
|
||||||
|
this.queryRange = [this.formatDate(firstDay), this.formatDate(today)]
|
||||||
|
this.loadAll()
|
||||||
|
},
|
||||||
|
async handleExportWord() {
|
||||||
|
this.loading.export = true
|
||||||
|
try {
|
||||||
|
const res = await exportOeeWord(this.buildQuery())
|
||||||
|
const blob = new Blob([res], {
|
||||||
|
type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
|
||||||
|
})
|
||||||
|
const url = window.URL.createObjectURL(blob)
|
||||||
|
const a = document.createElement('a')
|
||||||
|
a.href = url
|
||||||
|
a.download = `酸轧线OEE报表_${this.queryRange[0]}_${this.queryRange[1]}.docx`
|
||||||
|
a.click()
|
||||||
|
window.URL.revokeObjectURL(url)
|
||||||
|
} catch (e) {
|
||||||
|
this.$message.error('导出 Word 失败')
|
||||||
|
} finally {
|
||||||
|
this.loading.export = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
renderIdealCycleChart() {
|
||||||
|
if (!this.idealCycle) return
|
||||||
|
const dailyPoints = Array.isArray(this.idealCycle.dailyComparePoints)
|
||||||
|
? this.idealCycle.dailyComparePoints
|
||||||
|
: []
|
||||||
|
if (this.$refs.regDailyCompareChart && dailyPoints.length) {
|
||||||
|
if (!this.regDailyCompareChart) {
|
||||||
|
this.regDailyCompareChart = echarts.init(this.$refs.regDailyCompareChart)
|
||||||
|
}
|
||||||
|
const categories = dailyPoints.map(d => d.statDate)
|
||||||
|
const actualRun = dailyPoints.map(d => Number(d.actualRunTimeMin || 0))
|
||||||
|
const theoretical = dailyPoints.map(d => Number(d.theoreticalTimeMin || 0))
|
||||||
|
const compareOption = {
|
||||||
|
tooltip: { trigger: 'axis' },
|
||||||
|
grid: { left: 40, right: 20, top: 30, bottom: 40 },
|
||||||
|
legend: { top: 0, left: 'center' },
|
||||||
|
xAxis: {
|
||||||
|
type: 'category',
|
||||||
|
data: categories,
|
||||||
|
axisLabel: { fontSize: 10 }
|
||||||
|
},
|
||||||
|
yAxis: {
|
||||||
|
type: 'value',
|
||||||
|
name: '时间 (min)',
|
||||||
|
nameLocation: 'middle',
|
||||||
|
nameGap: 35
|
||||||
|
},
|
||||||
|
series: [
|
||||||
|
{
|
||||||
|
name: '实际运转时间',
|
||||||
|
type: 'line',
|
||||||
|
data: actualRun,
|
||||||
|
smooth: true,
|
||||||
|
symbolSize: 4,
|
||||||
|
itemStyle: { color: '#409EFF' }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '理论耗时',
|
||||||
|
type: 'line',
|
||||||
|
data: theoretical,
|
||||||
|
smooth: true,
|
||||||
|
symbolSize: 4,
|
||||||
|
itemStyle: { color: '#E6A23C' }
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
this.regDailyCompareChart.setOption(compareOption)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
handleResize() {
|
||||||
|
this.regDailyCompareChart && this.regDailyCompareChart.resize()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.oee-report-page {
|
||||||
|
padding: 16px;
|
||||||
|
background: #f5f7fa;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oee-header-card {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oee-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oee-title-block {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oee-title {
|
||||||
|
font-size: 20px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oee-subtitle {
|
||||||
|
margin-top: 4px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #606266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oee-query-bar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oee-main-row {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oee-word-card {
|
||||||
|
min-height: 600px;
|
||||||
|
background: #ffffff;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oee-section-title {
|
||||||
|
margin: 12px 0 6px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oee-help-text {
|
||||||
|
margin: 0 0 6px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #909399;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oee-kpi-table,
|
||||||
|
.oee-daily-table,
|
||||||
|
.oee-loss7-table,
|
||||||
|
.oee-events-table {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.oee-formula-card {
|
||||||
|
background: #ffffff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formula-block {
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formula-title {
|
||||||
|
font-weight: 600;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
color: #303133;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formula-eq {
|
||||||
|
font-family: 'Times New Roman', serif;
|
||||||
|
font-style: italic;
|
||||||
|
margin-bottom: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formula-list {
|
||||||
|
padding-left: 16px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.formula-list li {
|
||||||
|
margin-bottom: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reg-desc {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reg-chart {
|
||||||
|
width: 100%;
|
||||||
|
height: 260px;
|
||||||
|
border: 1px solid #ebeef5;
|
||||||
|
border-radius: 4px;
|
||||||
|
box-sizing: border-box;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.reg-chart canvas {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package com.klp.service.impl;
|
package com.klp.service.impl;
|
||||||
|
|
||||||
import cn.hutool.core.bean.BeanUtil;
|
import cn.hutool.core.bean.BeanUtil;
|
||||||
|
import com.baomidou.dynamic.datasource.annotation.DS;
|
||||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||||
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
|
||||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||||
@@ -51,6 +52,7 @@ import java.math.BigDecimal;
|
|||||||
@Slf4j
|
@Slf4j
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Service
|
@Service
|
||||||
|
@DS("master")
|
||||||
public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
|
public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
|
||||||
|
|
||||||
private final WmsMaterialCoilMapper baseMapper;
|
private final WmsMaterialCoilMapper baseMapper;
|
||||||
@@ -401,8 +403,6 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
|
|||||||
qw.eq(StringUtils.isNotBlank(bo.getItemType()), "mc.item_type", bo.getItemType());
|
qw.eq(StringUtils.isNotBlank(bo.getItemType()), "mc.item_type", bo.getItemType());
|
||||||
qw.eq(StringUtils.isNotBlank(bo.getCreateBy()), "mc.create_by", bo.getCreateBy());
|
qw.eq(StringUtils.isNotBlank(bo.getCreateBy()), "mc.create_by", bo.getCreateBy());
|
||||||
qw.eq(StringUtils.isNotBlank(bo.getUpdateBy()), "mc.update_by", bo.getUpdateBy());
|
qw.eq(StringUtils.isNotBlank(bo.getUpdateBy()), "mc.update_by", bo.getUpdateBy());
|
||||||
//逻辑删除
|
|
||||||
qw.eq("mc.del_flag", 0);
|
|
||||||
// 切边要求
|
// 切边要求
|
||||||
qw.eq(StringUtils.isNotBlank(bo.getTrimmingRequirement()), "mc.trimming_requirement", bo.getTrimmingRequirement());
|
qw.eq(StringUtils.isNotBlank(bo.getTrimmingRequirement()), "mc.trimming_requirement", bo.getTrimmingRequirement());
|
||||||
// 打包状态
|
// 打包状态
|
||||||
@@ -584,6 +584,8 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
|
|||||||
// "WHERE dp.del_flag = 0 AND dp.coil IS NOT NULL AND dp.coil <> '' " +
|
// "WHERE dp.del_flag = 0 AND dp.coil IS NOT NULL AND dp.coil <> '' " +
|
||||||
// "AND FIND_IN_SET(CAST(mc.coil_id AS CHAR), dp.coil))");
|
// "AND FIND_IN_SET(CAST(mc.coil_id AS CHAR), dp.coil))");
|
||||||
// }
|
// }
|
||||||
|
//逻辑删除
|
||||||
|
qw.eq("mc.del_flag", 0);
|
||||||
//把team字段作为筛选条件
|
//把team字段作为筛选条件
|
||||||
qw.eq(StringUtils.isNotBlank(bo.getTeam()), "mc.team", bo.getTeam());
|
qw.eq(StringUtils.isNotBlank(bo.getTeam()), "mc.team", bo.getTeam());
|
||||||
//根据开始时间和结束时间筛选修改时间
|
//根据开始时间和结束时间筛选修改时间
|
||||||
|
|||||||
Reference in New Issue
Block a user