diff --git a/klp-da/pom.xml b/klp-da/pom.xml index ced04771..c2a84dd5 100644 --- a/klp-da/pom.xml +++ b/klp-da/pom.xml @@ -47,5 +47,10 @@ fastjson2 2.0.35 + + + org.apache.poi + poi-ooxml + diff --git a/klp-da/src/main/java/com/klp/da/controller/OeeReportController.java b/klp-da/src/main/java/com/klp/da/controller/OeeReportController.java index cfe61e2c..7af9aab9 100644 --- a/klp-da/src/main/java/com/klp/da/controller/OeeReportController.java +++ b/klp-da/src/main/java/com/klp/da/controller/OeeReportController.java @@ -5,14 +5,23 @@ import com.klp.common.core.controller.BaseController; import com.klp.common.core.domain.R; import com.klp.common.core.page.TableDataInfo; import com.klp.common.utils.StringUtils; +import com.klp.da.service.OeeWordAiAnalysisService; import com.klp.da.service.OeeReportJobService; 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.AcidOeeRegressionVo; import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo; import com.klp.pocket.acid.service.IAcidOeeService; import lombok.Data; 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.validation.annotation.Validated; 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.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.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.ArrayList; import java.util.List; +import java.util.Locale; import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; @@ -48,6 +63,7 @@ public class OeeReportController extends BaseController { private final IAcidOeeService acidOeeService; private final StringRedisTemplate stringRedisTemplate; private final OeeReportJobService oeeReportJobService; + private final OeeWordAiAnalysisService oeeWordAiAnalysisService; private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd"); 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 - * 说明: - * - {@code startDate} / {@code endDate} 可选,若为空则由 pocket 按“近6个月”默认处理。 + * 路由:GET /oee/line/acid/idealCycle */ - @GetMapping("/acid/regression") - public R getAcidRegression( - @RequestParam(required = false) String startDate, - @RequestParam(required = false) String endDate + @GetMapping("/acid/idealCycle") + public R getAcidIdealCycle( + @RequestParam String startDate, + @RequestParam String endDate ) { - AcidOeeRegressionVo data = acidOeeService.getRegressionData(startDate, endDate); + AcidOeeIdealCycleVo data = acidOeeService.getIdealCycle(startDate, endDate); 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 summary = acidOeeService.getDailySummary(start, end); + List loss7 = acidOeeService.getLoss7Summary(start, end); + List 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号, 今天]。 */ @@ -219,6 +447,315 @@ public class OeeReportController extends BaseController { 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 list, int maxRows) { + List 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 list, int maxRows) { + List 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 list, int maxRows) { + List 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 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; + } + } + // ======================== 任意日期范围异步任务(酸轧线) ======================== /** diff --git a/klp-da/src/main/java/com/klp/da/service/IDaAcidOeeService.java b/klp-da/src/main/java/com/klp/da/service/IDaAcidOeeService.java new file mode 100644 index 00000000..cd9d2dbf --- /dev/null +++ b/klp-da/src/main/java/com/klp/da/service/IDaAcidOeeService.java @@ -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 getDailySummary(OeeQueryBo bo); + + /** + * 获取 7 大损失汇总(当月走缓存,历史查询走实时) + * + * @param bo 查询条件 + * @return 7 大损失列表 + */ + List getLoss7Summary(OeeQueryBo bo); + + /** + * 获取停机事件明细(实时分页查询) + * + * @param bo 查询条件 + * @return 分页后的停机事件列表 + */ + TableDataInfo getStoppageEvents(OeeQueryBo bo); + +} + diff --git a/klp-da/src/main/java/com/klp/da/service/OeeWordAiAnalysisService.java b/klp-da/src/main/java/com/klp/da/service/OeeWordAiAnalysisService.java new file mode 100644 index 00000000..c76c275c --- /dev/null +++ b/klp-da/src/main/java/com/klp/da/service/OeeWordAiAnalysisService.java @@ -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 requestBody = new HashMap<>(); + requestBody.put("model", deepseekConfig.getModelName()); + + Map systemMessage = new HashMap<>(); + systemMessage.put("role", "system"); + systemMessage.put("content", "你是一个严谨的数据分析助手"); + + Map 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> 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 response = restTemplate.postForEntity( + deepseekConfig.getBaseUrl() + "/chat/completions", entity, Map.class); + if (response.getStatusCode() == HttpStatus.OK && response.getBody() != null) { + Map body = response.getBody(); + List> choices = (List>) body.get("choices"); + if (choices != null && !choices.isEmpty()) { + Map choice = choices.get(0); + Map message = (Map) 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; + } +} + + diff --git a/klp-da/src/main/java/com/klp/da/service/impl/DaAcidOeeServiceImpl.java b/klp-da/src/main/java/com/klp/da/service/impl/DaAcidOeeServiceImpl.java new file mode 100644 index 00000000..4cf472e2 --- /dev/null +++ b/klp-da/src/main/java/com/klp/da/service/impl/DaAcidOeeServiceImpl.java @@ -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 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>() {}); + } + } 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 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 getStoppageEvents(OeeQueryBo bo) { + // 实时分页查询 + List 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); + } +} + diff --git a/klp-da/src/main/java/com/klp/da/task/AcidOeeMonthTask.java b/klp-da/src/main/java/com/klp/da/task/AcidOeeMonthTask.java index c31cd34f..16af3a5c 100644 --- a/klp-da/src/main/java/com/klp/da/task/AcidOeeMonthTask.java +++ b/klp-da/src/main/java/com/klp/da/task/AcidOeeMonthTask.java @@ -2,7 +2,6 @@ package com.klp.da.task; import com.alibaba.fastjson2.JSON; import com.klp.pocket.acid.domain.vo.AcidOeeDailySummaryVo; -import com.klp.pocket.acid.domain.vo.AcidOeeLoss7Vo; import com.klp.pocket.acid.service.IAcidOeeService; import lombok.Data; import lombok.RequiredArgsConstructor; @@ -25,10 +24,9 @@ import java.util.concurrent.TimeUnit; * - 项目启动完成后即计算当月 OEE 聚合结果并写入 Redis; * - 每天凌晨 04:00 重新计算当月数据并覆盖缓存。 * - * 当前仅实现酸轧线(SY)的当月日汇总 & 7 大损失预计算; + * 当前仅实现酸轧线(SY)的当月日汇总预计算; * key 约定: * - 汇总结果:oee:report:month:summary:{yyyyMM}:SY - * - 7 大损失:oee:report:month:loss7:{yyyyMM}:SY * - 元信息: oee:report:month:meta:{yyyyMM}:SY */ @Slf4j @@ -39,9 +37,6 @@ public class AcidOeeMonthTask { /** Redis 缓存 key 模板:当月 OEE 汇总(酸轧线) */ 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 模板:当月元信息(酸轧线) */ 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 {} ({} ~ {})", trigger, yyyyMM, startStr, endStr); - // 1. 调用 pocket 的 AcidOeeService 获取当月日汇总 & 7 大损失 + // 1. 调用 pocket 的 AcidOeeService 获取当月日汇总 List dailySummaryList = acidOeeService.getDailySummary(startStr, endStr); - List loss7List = acidOeeService.getLoss7Summary(startStr, endStr); // 2. 写入 Redis(summary) String summaryKey = String.format(SUMMARY_KEY_PATTERN, yyyyMM); String summaryJson = JSON.toJSONString(dailySummaryList); 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; // 3. 写入 Redis(meta) @@ -123,8 +112,8 @@ public class AcidOeeMonthTask { String metaKey = String.format(META_KEY_PATTERN, yyyyMM); stringRedisTemplate.opsForValue().set(metaKey, JSON.toJSONString(meta), 1, TimeUnit.DAYS); - log.info("[AcidOeeMonthTask] compute finish for {} dailySize={}, loss7Size={}, durationMs={}ms, summaryKey={}", - yyyyMM, dailySummaryList.size(), loss7List.size(), durationMs, summaryKey); + log.info("[AcidOeeMonthTask] compute finish for {} dailySize={}, durationMs={}ms, summaryKey={}", + yyyyMM, dailySummaryList.size(), durationMs, summaryKey); } /** diff --git a/klp-pocket/src/main/java/com/klp/pocket/acid/domain/vo/AcidOeeIdealCycleVo.java b/klp-pocket/src/main/java/com/klp/pocket/acid/domain/vo/AcidOeeIdealCycleVo.java new file mode 100644 index 00000000..3cd7c17a --- /dev/null +++ b/klp-pocket/src/main/java/com/klp/pocket/acid/domain/vo/AcidOeeIdealCycleVo.java @@ -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 dailyComparePoints; + + @Data + public static class DailyComparePointVo { + private String statDate; + private Long actualRunTimeMin; + private BigDecimal theoreticalTimeMin; + } +} + + diff --git a/klp-pocket/src/main/java/com/klp/pocket/acid/domain/vo/AcidOeeRegressionVo.java b/klp-pocket/src/main/java/com/klp/pocket/acid/domain/vo/AcidOeeRegressionVo.java deleted file mode 100644 index b7312f43..00000000 --- a/klp-pocket/src/main/java/com/klp/pocket/acid/domain/vo/AcidOeeRegressionVo.java +++ /dev/null @@ -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 points; - - /** 拟合线两个端点(前端可直接画线) */ - private List 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; - } -} - diff --git a/klp-pocket/src/main/java/com/klp/pocket/acid/mapper/AcidOeeMapper.java b/klp-pocket/src/main/java/com/klp/pocket/acid/mapper/AcidOeeMapper.java index 1a36ef42..a0111cfa 100644 --- a/klp-pocket/src/main/java/com/klp/pocket/acid/mapper/AcidOeeMapper.java +++ b/klp-pocket/src/main/java/com/klp/pocket/acid/mapper/AcidOeeMapper.java @@ -1,7 +1,6 @@ package com.klp.pocket.acid.mapper; 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.Param; @@ -27,17 +26,6 @@ public interface AcidOeeMapper { List selectDailySummary(@Param("startDate") String startDate, @Param("endDate") String endDate); - /** - * 查询回归数据散点(用于理论节拍计算) - * 返回:重量(吨)、卷数、时长(分钟)等 - * - * @param startDate 开始日期(yyyy-MM-dd,可选) - * @param endDate 结束日期(yyyy-MM-dd,可选) - * @return 散点列表 - */ - List selectRegressionPoints(@Param("startDate") String startDate, - @Param("endDate") String endDate); - /** * 查询每日的钢卷号和重量(用于良品/次品判定) * @@ -48,6 +36,16 @@ public interface AcidOeeMapper { List selectCoilInfoByDate(@Param("startDate") String startDate, @Param("endDate") String endDate); + /** + * 查询卷级生产节拍(min/吨),用于理论节拍计算。 + * + * @param startDate 开始日期(yyyy-MM-dd) + * @param endDate 结束日期(yyyy-MM-dd) + * @return 每卷的生产节拍列表(min/吨) + */ + List selectCoilCycleMinPerTon(@Param("startDate") String startDate, + @Param("endDate") String endDate); + /** * 卷号信息内部类(用于Mapper返回) */ diff --git a/klp-pocket/src/main/java/com/klp/pocket/acid/service/IAcidOeeService.java b/klp-pocket/src/main/java/com/klp/pocket/acid/service/IAcidOeeService.java index 545d0a91..25ed427c 100644 --- a/klp-pocket/src/main/java/com/klp/pocket/acid/service/IAcidOeeService.java +++ b/klp-pocket/src/main/java/com/klp/pocket/acid/service/IAcidOeeService.java @@ -1,8 +1,8 @@ package com.klp.pocket.acid.service; 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.AcidOeeRegressionVo; import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo; import java.util.List; @@ -34,16 +34,6 @@ public interface IAcidOeeService { */ List getStoppageEvents(String startDate, String endDate); - /** - * 查询理论节拍回归数据(吨和卷两个维度) - * 用于性能稼动率计算和前端散点图展示 - * - * @param startDate 开始日期(yyyy-MM-dd,可选,默认近6个月) - * @param endDate 结束日期(yyyy-MM-dd,可选) - * @return 回归数据(包含斜率、截距、散点等) - */ - AcidOeeRegressionVo getRegressionData(String startDate, String endDate); - /** * 查询7大损失汇总(按日期范围) * @@ -52,5 +42,14 @@ public interface IAcidOeeService { * @return 7大损失汇总列表 */ List getLoss7Summary(String startDate, String endDate); + + /** + * 查询理论节拍(统计口径:历史优良日中位数) + * + * @param startDate 开始日期(yyyy-MM-dd) + * @param endDate 结束日期(yyyy-MM-dd) + * @return 理论节拍与对比数据 + */ + AcidOeeIdealCycleVo getIdealCycle(String startDate, String endDate); } diff --git a/klp-pocket/src/main/java/com/klp/pocket/acid/service/impl/AcidOeeServiceImpl.java b/klp-pocket/src/main/java/com/klp/pocket/acid/service/impl/AcidOeeServiceImpl.java index ab8fe2a0..a5ec1a6d 100644 --- a/klp-pocket/src/main/java/com/klp/pocket/acid/service/impl/AcidOeeServiceImpl.java +++ b/klp-pocket/src/main/java/com/klp/pocket/acid/service/impl/AcidOeeServiceImpl.java @@ -3,8 +3,8 @@ package com.klp.pocket.acid.service.impl; import com.baomidou.dynamic.datasource.annotation.DS; import com.klp.common.utils.StringUtils; 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.AcidOeeRegressionVo; import com.klp.pocket.acid.domain.vo.Klptcm1ProStoppageVo; import com.klp.pocket.acid.domain.bo.Klptcm1ProStoppageBo; import com.klp.pocket.acid.mapper.AcidOeeMapper; @@ -19,7 +19,7 @@ import java.math.BigDecimal; import java.math.RoundingMode; import java.text.SimpleDateFormat; import java.util.*; -import java.util.stream.Collectors; +import java.util.Calendar; /** * 酸轧线OEE Service实现类 @@ -35,7 +35,8 @@ public class AcidOeeServiceImpl implements IAcidOeeService { /** 酸轧成品库库区ID */ 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 IKlptcm1ProStoppageService stoppageService; private final ICoilQualityJudgeService coilQualityJudgeService; @@ -55,7 +56,10 @@ public class AcidOeeServiceImpl implements IAcidOeeService { // 3. 查询产量明细,用于良品/次品判定 Map> coilInfoByDate = getCoilNosByDate(startDate, endDate); - // 4. 填充每个日汇总的完整数据 + // 4. 理论节拍:使用固定值0.47 + BigDecimal idealCycleTon = FIXED_IDEAL_CYCLE; + + // 5. 填充每个日汇总的完整数据 for (AcidOeeDailySummaryVo summary : summaries) { String statDate = summary.getStatDate(); summary.setLineId("SY"); @@ -70,6 +74,11 @@ public class AcidOeeServiceImpl implements IAcidOeeService { Long runTime = Math.max(0, loadingTime - downtime); summary.setRunTimeMin(runTime); + // 理论节拍:若尚未填充,则统一使用“优良日统计”得到的节拍 + if (summary.getIdealCycleTimeMinPerTon() == null && idealCycleTon != null) { + summary.setIdealCycleTimeMinPerTon(idealCycleTon); + } + // 良品/次品判定(通过WMS) if (coilInfoByDate.containsKey(statDate)) { List coilInfos = coilInfoByDate.get(statDate); @@ -101,57 +110,84 @@ public class AcidOeeServiceImpl implements IAcidOeeService { return stoppageService.queryList(bo); } + /** + * 查询停机事件(可选:是否包含短停机 <5min) + */ + private List 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 - public AcidOeeRegressionVo getRegressionData(String startDate, String endDate) { - // 1. 查询散点数据 - List points = acidOeeMapper.selectRegressionPoints(startDate, endDate); + public AcidOeeIdealCycleVo getIdealCycle(String startDate, String endDate) { + // 1) 取基础日汇总(产量、负荷时间等) + List 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(); - result.setLineId("SY"); - result.setLineName("酸轧线"); - result.setStartTime(startDate); - result.setEndTime(endDate); - result.setPoints(points); - result.setSampleCount(points != null ? points.size() : 0); - - if (points == null || points.isEmpty()) { - // 没有数据时返回空结果 - return result; + if (daily == null || daily.isEmpty()) { + rsp.setIdealCycleTimeMinPerTon(null); + rsp.setSampleDays(0); + rsp.setDailyComparePoints(Collections.emptyList()); + return rsp; } - // 2. 计算回归(吨维度) - RegressionResult tonResult = calculateRegression( - points.stream().map(AcidOeeRegressionVo.RegressionPointVo::getWeightTon).filter(Objects::nonNull).collect(Collectors.toList()), - points.stream().map(AcidOeeRegressionVo.RegressionPointVo::getDurationMin).filter(Objects::nonNull).collect(Collectors.toList()) - ); - if (tonResult != null) { - result.setSlopeMinPerTon(tonResult.slope); - result.setInterceptMin(tonResult.intercept); - result.setR2(tonResult.r2); + // 2) 聚合停机,补齐 runTime + Map downtimeByDate = aggregateDowntimeByDate(startDate, endDate); + for (AcidOeeDailySummaryVo d : daily) { + Long downtime = downtimeByDate.getOrDefault(d.getStatDate(), 0L); + d.setDowntimeMin(downtime); + Long loading = d.getLoadingTimeMin() != null ? d.getLoadingTimeMin() : 0L; + d.setRunTimeMin(Math.max(0, loading - downtime)); } - // 3. 计算回归(卷维度) - RegressionResult coilResult = calculateRegression( - points.stream().map(p -> p.getCoilCount() != null ? BigDecimal.valueOf(p.getCoilCount()) : null).filter(Objects::nonNull).collect(Collectors.toList()), - points.stream().map(AcidOeeRegressionVo.RegressionPointVo::getDurationMin).filter(Objects::nonNull).collect(Collectors.toList()) - ); - if (coilResult != null) { - result.setSlopeMinPerCoil(coilResult.slope); - } + // 3) 卷级节拍 = (END_DATE - START_DATE)/出口重量,计算中位数(用于展示,不用于OEE计算) + List coilCycles = acidOeeMapper.selectCoilCycleMinPerTon(startDate, endDate); + coilCycles.removeIf(c -> c == null || c.compareTo(BigDecimal.ZERO) <= 0); + coilCycles.sort(BigDecimal::compareTo); + BigDecimal medianCycle = median(coilCycles); + + // 理论节拍使用固定值0.47(用于OEE计算) + rsp.setIdealCycleTimeMinPerTon(FIXED_IDEAL_CYCLE); + // 中位数理论节拍(用于展示) + rsp.setMedianCycleTimeMinPerTon(medianCycle); + // 样本天数:当前查询区间内有产量的自然日数量(与传入的日期范围一一对应) + rsp.setSampleDays(daily.size()); - // 4. 生成拟合线端点(用于前端画线) - if (tonResult != null && !points.isEmpty()) { - List linePoints = generateLinePoints(points, tonResult); - result.setLinePoints(linePoints); + // 4) 日粒度对比数据:理论耗时 vs 实际运转时间(用于前端展示"有效性") + // 使用固定值0.47计算理论耗时 + List compare = new ArrayList<>(); + 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); + } } - - return result; + rsp.setDailyComparePoints(compare); + return rsp; } @Override public List getLoss7Summary(String startDate, String endDate) { // 1. 查询停机事件(含 stopType、duration 等) - List events = getStoppageEvents(startDate, endDate); + // 损失统计也建议包含短停机,避免损失时间与停机总时间口径不一致 + List events = getStoppageEvents(startDate, endDate, true); if (events == null || events.isEmpty()) { return Collections.emptyList(); } @@ -162,9 +198,9 @@ public class AcidOeeServiceImpl implements IAcidOeeService { for (Klptcm1ProStoppageVo event : events) { String stopType = event.getStopType(); + // stopType 为空时归入“未分类”,避免因为未录入原因导致损失时间被漏算 if (StringUtils.isBlank(stopType)) { - // 没有类型的记录暂时忽略 - continue; + stopType = "未分类"; } Long durationSec = event.getDuration(); if (durationSec == null || durationSec <= 0) { @@ -225,18 +261,64 @@ public class AcidOeeServiceImpl implements IAcidOeeService { /** * 按日期聚合停机时间 + * 修复:如果停机事件跨天,需要按实际跨天的分钟数分配到对应的日期 */ private Map aggregateDowntimeByDate(String startDate, String endDate) { - List events = getStoppageEvents(startDate, endDate); + // 性能稼动率/运转时间口径:停机时间需要包含短停机(<5min),否则 runTime 被高估 + List events = getStoppageEvents(startDate, endDate, true); Map downtimeMap = new HashMap<>(); SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd"); + Calendar cal = Calendar.getInstance(); + for (Klptcm1ProStoppageVo event : events) { - if (event.getStartDate() != null && event.getDuration() != null) { - String date = dateFormat.format(event.getStartDate()); - // duration单位是秒,转换为分钟 - Long minutes = event.getDuration() / 60; - downtimeMap.merge(date, minutes, Long::sum); + if (event.getStartDate() == null || event.getDuration() == null || event.getDuration() <= 0) { + continue; + } + + 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; BigDecimal idealCycleTon = summary.getIdealCycleTimeMinPerTon(); 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 performanceTon = idealTime.divide(BigDecimal.valueOf(runTime), 4, RoundingMode.HALF_UP) .multiply(BigDecimal.valueOf(100)); @@ -337,7 +423,10 @@ public class AcidOeeServiceImpl implements IAcidOeeService { // 性能稼动率(卷维度) Long totalOutputCoil = summary.getTotalOutputCoil() != null ? summary.getTotalOutputCoil() : 0L; 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 performanceCoil = idealTime.divide(BigDecimal.valueOf(runTime), 4, RoundingMode.HALF_UP) .multiply(BigDecimal.valueOf(100)); @@ -363,102 +452,15 @@ public class AcidOeeServiceImpl implements IAcidOeeService { } } - /** - * 计算线性回归(最小二乘法) - */ - private RegressionResult calculateRegression(List xValues, List yValues) { - if (xValues.size() != yValues.size() || xValues.isEmpty()) { - return null; + private BigDecimal median(List values) { + if (values == null || values.isEmpty()) return null; + int n = values.size(); + if (n % 2 == 1) { + return values.get(n / 2); } - - int n = xValues.size(); - BigDecimal sumX = BigDecimal.ZERO; - 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 generateLinePoints( - List 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); + BigDecimal a = values.get(n / 2 - 1); + BigDecimal b = values.get(n / 2); + return a.add(b).divide(BigDecimal.valueOf(2), 6, RoundingMode.HALF_UP); } /** @@ -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 的总损失时间与次数 diff --git a/klp-pocket/src/main/java/com/klp/pocket/acid/service/impl/Klptcm1ProStoppageServiceImpl.java b/klp-pocket/src/main/java/com/klp/pocket/acid/service/impl/Klptcm1ProStoppageServiceImpl.java index 39114f62..f40219f8 100644 --- a/klp-pocket/src/main/java/com/klp/pocket/acid/service/impl/Klptcm1ProStoppageServiceImpl.java +++ b/klp-pocket/src/main/java/com/klp/pocket/acid/service/impl/Klptcm1ProStoppageServiceImpl.java @@ -89,8 +89,12 @@ public class Klptcm1ProStoppageServiceImpl implements IKlptcm1ProStoppageService lqw.le(Klptcm1ProStoppage::getStartDate, endDateWithTime); } lqw.eq(bo.getDURATION() != null, Klptcm1ProStoppage::getDuration, bo.getDURATION()); - // 只查询持续时间大于等于5分钟(300秒)的停机记录 - lqw.ge(Klptcm1ProStoppage::getDuration, 300); + // 默认只统计持续时间大于等于5分钟(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(StringUtils.isNotBlank(bo.getStopType()), Klptcm1ProStoppage::getStopType, bo.getStopType()); //倒序 diff --git a/klp-pocket/src/main/resources/mapper/pocket/AcidOeeMapper.xml b/klp-pocket/src/main/resources/mapper/pocket/AcidOeeMapper.xml index 35d15297..d6856fc6 100644 --- a/klp-pocket/src/main/resources/mapper/pocket/AcidOeeMapper.xml +++ b/klp-pocket/src/main/resources/mapper/pocket/AcidOeeMapper.xml @@ -54,51 +54,6 @@ ORDER BY stat_date ASC - - - - - - - - - - - - + + + diff --git a/klp-ui/src/api/da/oee.js b/klp-ui/src/api/da/oee.js index 83ce3002..2641e365 100644 --- a/klp-ui/src/api/da/oee.js +++ b/klp-ui/src/api/da/oee.js @@ -1,19 +1,19 @@ import request from '@/utils/request' -// 导出 Word 报表 +// 导出 Word 报表(酸轧线) export function exportOeeWord(query) { return request({ - url: '/oee/line/exportWord', + url: '/oee/line/acid/exportWord', method: 'get', params: query, responseType: 'blob' }) } -// OEE 产线 KPI + 趋势汇总 +// OEE 产线 KPI + 趋势汇总(酸轧线) export function fetchOeeSummary(query) { return request({ - url: '/oee/line/summary', + url: '/oee/line/acid/summary', method: 'get', params: query, timeout: 120000 @@ -23,37 +23,37 @@ export function fetchOeeSummary(query) { // OEE 产线 KPI + 趋势汇总(异步任务接口暂保留,当前前端不使用) export function createOeeSummaryJob(query) { return request({ - url: '/oee/line/summary/job', + url: '/oee/line/acid/summary/job', method: 'get', params: query, timeout: 120000 }) } -// 7 大损失汇总 +// 7 大损失汇总(酸轧线) export function fetchOeeLoss7(query) { return request({ - url: '/oee/line/loss7', + url: '/oee/line/acid/loss7', method: 'get', params: query, timeout: 120000 }) } -// 停机/损失事件明细 +// 停机/损失事件明细(酸轧线) export function fetchOeeEvents(query) { return request({ - url: '/oee/line/events', + url: '/oee/line/acid/events', method: 'get', params: query, timeout: 120000 }) } -// 理论节拍回归结果(含散点与拟合线) -export function fetchOeeTheoryCycleRegression(query) { +// 理论节拍(统计口径:优良日中位数) +export function fetchOeeIdealCycle(query) { return request({ - url: '/oee/line/theoryCycle/regression', + url: '/oee/line/acid/idealCycle', method: 'get', params: query, timeout: 120000 @@ -63,7 +63,7 @@ export function fetchOeeTheoryCycleRegression(query) { // 理论节拍回归:创建异步任务,返回 jobId + wsType,通过 WebSocket 推送进度/结果 export function createOeeTheoryCycleRegressionJob(query) { return request({ - url: '/oee/line/theoryCycle/regression/job', + url: '/oee/line/acid/theoryCycle/regression/job', method: 'get', params: query, timeout: 120000 diff --git a/klp-ui/src/views/da/oee/components/OeeLossPareto.vue b/klp-ui/src/views/da/oee/components/OeeLossPareto.vue new file mode 100644 index 00000000..087be4b2 --- /dev/null +++ b/klp-ui/src/views/da/oee/components/OeeLossPareto.vue @@ -0,0 +1,127 @@ + + + + + \ No newline at end of file diff --git a/klp-ui/src/views/da/oee/components/OeeStoppageTop.vue b/klp-ui/src/views/da/oee/components/OeeStoppageTop.vue new file mode 100644 index 00000000..fc11296f --- /dev/null +++ b/klp-ui/src/views/da/oee/components/OeeStoppageTop.vue @@ -0,0 +1,106 @@ + + + + + \ No newline at end of file diff --git a/klp-ui/src/views/da/oee/components/OeeTrendChart.vue b/klp-ui/src/views/da/oee/components/OeeTrendChart.vue new file mode 100644 index 00000000..87528581 --- /dev/null +++ b/klp-ui/src/views/da/oee/components/OeeTrendChart.vue @@ -0,0 +1,97 @@ + + + + + \ No newline at end of file diff --git a/klp-ui/src/views/da/oee/index.vue b/klp-ui/src/views/da/oee/index.vue index e69de29b..e6b4b579 100644 --- a/klp-ui/src/views/da/oee/index.vue +++ b/klp-ui/src/views/da/oee/index.vue @@ -0,0 +1,745 @@ + + + + + + diff --git a/klp-wms/src/main/java/com/klp/service/impl/WmsMaterialCoilServiceImpl.java b/klp-wms/src/main/java/com/klp/service/impl/WmsMaterialCoilServiceImpl.java index a6d487de..56661288 100644 --- a/klp-wms/src/main/java/com/klp/service/impl/WmsMaterialCoilServiceImpl.java +++ b/klp-wms/src/main/java/com/klp/service/impl/WmsMaterialCoilServiceImpl.java @@ -1,6 +1,7 @@ package com.klp.service.impl; 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.update.UpdateWrapper; import com.fasterxml.jackson.core.JsonProcessingException; @@ -51,6 +52,7 @@ import java.math.BigDecimal; @Slf4j @RequiredArgsConstructor @Service +@DS("master") public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService { 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.getCreateBy()), "mc.create_by", bo.getCreateBy()); 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()); // 打包状态 @@ -584,6 +584,8 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService { // "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))"); // } + //逻辑删除 + qw.eq("mc.del_flag", 0); //把team字段作为筛选条件 qw.eq(StringUtils.isNotBlank(bo.getTeam()), "mc.team", bo.getTeam()); //根据开始时间和结束时间筛选修改时间