From 655023b91f97eecd5659f3acac63d07e441e0047 Mon Sep 17 00:00:00 2001 From: 86156 <823267011@qq.com> Date: Tue, 27 Jan 2026 19:07:25 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0OEE=E5=86=85=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/main/resources/application-dev.yml | 8 + .../src/main/resources/application-prod.yml | 8 + .../da/controller/OeeReportController.java | 81 +++ .../java/com/klp/da/domain/bo/OeeQueryBo.java | 68 ++ .../java/com/klp/da/domain/vo/OeeEventVo.java | 39 + .../klp/da/domain/vo/OeeLineSummaryVo.java | 71 ++ .../domain/vo/OeeLossCategorySummaryVo.java | 38 + .../com/klp/da/domain/vo/OeeLossReasonVo.java | 40 + .../com/klp/da/service/IOeeReportService.java | 47 ++ .../da/service/impl/OeeReportServiceImpl.java | 684 ++++++++++++++++++ klp-ui/src/api/da/oee.js | 39 + klp-ui/src/views/da/oee/index.vue | 384 ++++++++++ .../impl/WmsMaterialCoilServiceImpl.java | 14 + script/sql/mysql/eqp_auxiliary_material.sql | 2 +- 14 files changed, 1522 insertions(+), 1 deletion(-) create mode 100644 klp-da/src/main/java/com/klp/da/controller/OeeReportController.java create mode 100644 klp-da/src/main/java/com/klp/da/domain/bo/OeeQueryBo.java create mode 100644 klp-da/src/main/java/com/klp/da/domain/vo/OeeEventVo.java create mode 100644 klp-da/src/main/java/com/klp/da/domain/vo/OeeLineSummaryVo.java create mode 100644 klp-da/src/main/java/com/klp/da/domain/vo/OeeLossCategorySummaryVo.java create mode 100644 klp-da/src/main/java/com/klp/da/domain/vo/OeeLossReasonVo.java create mode 100644 klp-da/src/main/java/com/klp/da/service/IOeeReportService.java create mode 100644 klp-da/src/main/java/com/klp/da/service/impl/OeeReportServiceImpl.java create mode 100644 klp-ui/src/api/da/oee.js create mode 100644 klp-ui/src/views/da/oee/index.vue diff --git a/klp-admin/src/main/resources/application-dev.yml b/klp-admin/src/main/resources/application-dev.yml index b3201244..cc7cd0ad 100644 --- a/klp-admin/src/main/resources/application-dev.yml +++ b/klp-admin/src/main/resources/application-dev.yml @@ -7,6 +7,14 @@ klp: # 开发环境文件存储目录 directory-path: testDirectory +--- # OEE 聚合(klp-da)多服务地址配置 +da: + oee: + # 酸轧线(klp-pocket)在同一套服务内时可指向本服务端口 + acid-line-base-url: http://localhost:${server.port} + # 镀锌一线(Fizz) + galvanize-line-base-url: http://140.143.206.120:18081 + --- # 监控中心配置 spring.boot.admin.client: # 增加客户端开关 diff --git a/klp-admin/src/main/resources/application-prod.yml b/klp-admin/src/main/resources/application-prod.yml index 2ec08cf3..a2fb885c 100644 --- a/klp-admin/src/main/resources/application-prod.yml +++ b/klp-admin/src/main/resources/application-prod.yml @@ -7,6 +7,14 @@ klp: # 生产环境文件存储目录 directory-path: /home/ubuntu/oa/folder +--- # OEE 聚合(klp-da)多服务地址配置 +da: + oee: + # 酸轧线(klp-pocket)在同一套服务内时可指向本服务端口(也可改成内网域名) + acid-line-base-url: http://127.0.0.1:${server.port} + # 镀锌一线(Fizz) + galvanize-line-base-url: http://140.143.206.120:18081 + --- # 监控中心配置 spring.boot.admin.client: # 增加客户端开关 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 new file mode 100644 index 00000000..964bd48d --- /dev/null +++ b/klp-da/src/main/java/com/klp/da/controller/OeeReportController.java @@ -0,0 +1,81 @@ +package com.klp.da.controller; + +import com.klp.common.annotation.Log; +import com.klp.common.core.controller.BaseController; +import com.klp.common.core.domain.PageQuery; +import com.klp.common.core.domain.R; +import com.klp.common.core.page.TableDataInfo; +import com.klp.common.enums.BusinessType; +import com.klp.da.domain.bo.OeeQueryBo; +import com.klp.da.domain.vo.OeeLineSummaryVo; +import com.klp.da.domain.vo.OeeLossCategorySummaryVo; +import com.klp.da.domain.vo.OeeLossReasonVo; +import com.klp.da.domain.vo.OeeEventVo; +import com.klp.da.service.IOeeReportService; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Map; + +/** + * OEE 报表聚合 Controller(方式 A:后端统一聚合多服务) + * + * 当前仅提供接口“架子”,具体聚合逻辑在 {@link IOeeReportService} 中实现。 + * + * 路由前缀与 docs/oee-report-design.md 设计文档保持一致: + * - /api/ems/oee/line/summary + * - /api/ems/oee/line/loss7 + * - /api/ems/oee/line/events + * - /api/ems/oee/line/exportWord + */ +@Validated +@RequiredArgsConstructor +@RestController +@RequestMapping("/oee/line") +public class OeeReportController extends BaseController { + + private final IOeeReportService oeeReportService; + + /** + * KPI + 趋势汇总 + */ + @Log(title = "OEE 报表-汇总", businessType = BusinessType.OTHER) + @GetMapping("/summary") + public R> summary(OeeQueryBo queryBo) { + return R.ok(oeeReportService.summary(queryBo)); + } + + /** + * 7 大损失汇总 + */ + @Log(title = "OEE 报表-7大损失", businessType = BusinessType.OTHER) + @GetMapping("/loss7") + public R> lossSummary(OeeQueryBo queryBo) { + Map result = oeeReportService.lossSummary(queryBo); + return R.ok(result); + } + + /** + * 停机/损失事件明细 + */ + @Log(title = "OEE 报表-事件明细", businessType = BusinessType.OTHER) + @GetMapping("/events") + public TableDataInfo events(OeeQueryBo queryBo, PageQuery pageQuery) { + return oeeReportService.events(queryBo, pageQuery); + } + + /** + * 导出 Word 报表(整体版式由后端模板控制) + */ + @Log(title = "OEE 报表-导出Word", businessType = BusinessType.EXPORT) + @GetMapping("/exportWord") + public void exportWord(OeeQueryBo queryBo, HttpServletResponse response) { + oeeReportService.exportWord(queryBo, response); + } +} + diff --git a/klp-da/src/main/java/com/klp/da/domain/bo/OeeQueryBo.java b/klp-da/src/main/java/com/klp/da/domain/bo/OeeQueryBo.java new file mode 100644 index 00000000..74891f46 --- /dev/null +++ b/klp-da/src/main/java/com/klp/da/domain/bo/OeeQueryBo.java @@ -0,0 +1,68 @@ +package com.klp.da.domain.bo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; +import org.springframework.format.annotation.DateTimeFormat; + +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; + +/** + * OEE 查询条件 Bo + * + * 主要用于两条产线(酸轧线、镀锌一线)的聚合查询。 + * 具体字段与 docs/oee-report-design.md 中“接口设计”保持一致。 + */ +@Data +public class OeeQueryBo { + + /** + * 开始日期(yyyy-MM-dd) + */ + @DateTimeFormat(pattern = "yyyy-MM-dd") + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate startDate; + + /** + * 结束日期(yyyy-MM-dd) + */ + @DateTimeFormat(pattern = "yyyy-MM-dd") + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate endDate; + + /** + * 产线 ID 列表,例如 ["SY", "DX1"] + */ + private List lineIds; + + /** + * 事件查询:开始时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime startTime; + + /** + * 事件查询:结束时间 + */ + @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime endTime; + + /** + * 7 大损失类别编码(可选) + */ + private String lossCategoryCode; + + /** + * 关键字(事件明细筛选,匹配原因/备注等) + */ + private String keyword; + + /** + * TopN 设置(7 大损失 TOP 原因等,可选) + */ + private Integer topN; +} + diff --git a/klp-da/src/main/java/com/klp/da/domain/vo/OeeEventVo.java b/klp-da/src/main/java/com/klp/da/domain/vo/OeeEventVo.java new file mode 100644 index 00000000..f358bb79 --- /dev/null +++ b/klp-da/src/main/java/com/klp/da/domain/vo/OeeEventVo.java @@ -0,0 +1,39 @@ +package com.klp.da.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.time.LocalDateTime; + +/** + * OEE 事件(停机/损失)明细 VO + */ +@Data +public class OeeEventVo { + + private String lineId; + + private String lineName; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime eventStartTime; + + @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") + private LocalDateTime eventEndTime; + + /** + * 时长(分钟) + */ + private Integer durationMin; + + private String rawReasonCode; + + private String rawReasonName; + + private String lossCategoryCode; + + private String lossCategoryName; + + private String remark; +} + diff --git a/klp-da/src/main/java/com/klp/da/domain/vo/OeeLineSummaryVo.java b/klp-da/src/main/java/com/klp/da/domain/vo/OeeLineSummaryVo.java new file mode 100644 index 00000000..1d4cf5c4 --- /dev/null +++ b/klp-da/src/main/java/com/klp/da/domain/vo/OeeLineSummaryVo.java @@ -0,0 +1,71 @@ +package com.klp.da.domain.vo; + +import com.fasterxml.jackson.annotation.JsonFormat; +import lombok.Data; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.List; + +/** + * 产线 OEE 汇总 + 日趋势 VO + * + * 对应设计文档 7.1 返回结构中的一条 line 记录。 + */ +@Data +public class OeeLineSummaryVo { + + /** + * 产线 ID + */ + private String lineId; + + /** + * 产线名称 + */ + private String lineName; + + /** + * 区间汇总 + */ + private Summary total; + + /** + * 日粒度数据(用于趋势图) + */ + private List daily; + + @Data + public static class Summary { + private Integer loadingTimeMin; + private Integer downtimeMin; + private Integer runTimeMin; + private BigDecimal totalOutput; + private BigDecimal goodOutput; + private BigDecimal defectOutput; + + private BigDecimal availability; + private BigDecimal performance; + private BigDecimal quality; + private BigDecimal oee; + } + + @Data + public static class Daily { + @JsonFormat(pattern = "yyyy-MM-dd") + private LocalDate statDate; + + private Integer loadingTimeMin; + private Integer downtimeMin; + private Integer runTimeMin; + private BigDecimal totalOutput; + private BigDecimal goodOutput; + private BigDecimal defectOutput; + + private BigDecimal availability; + private BigDecimal performance; + private BigDecimal quality; + private BigDecimal oee; + } +} + diff --git a/klp-da/src/main/java/com/klp/da/domain/vo/OeeLossCategorySummaryVo.java b/klp-da/src/main/java/com/klp/da/domain/vo/OeeLossCategorySummaryVo.java new file mode 100644 index 00000000..4700c9f7 --- /dev/null +++ b/klp-da/src/main/java/com/klp/da/domain/vo/OeeLossCategorySummaryVo.java @@ -0,0 +1,38 @@ +package com.klp.da.domain.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 7 大损失分类汇总 VO + */ +@Data +public class OeeLossCategorySummaryVo { + + /** + * 损失类别编码(1~7 或枚举) + */ + private String lossCategoryCode; + + /** + * 损失类别名称 + */ + private String lossCategoryName; + + /** + * 损失时间(分钟) + */ + private Integer lossTimeMin; + + /** + * 损失占比(0~1 或 0~100,随整体口径配置) + */ + private BigDecimal lossTimeRate; + + /** + * 事件次数(可选) + */ + private Integer count; +} + diff --git a/klp-da/src/main/java/com/klp/da/domain/vo/OeeLossReasonVo.java b/klp-da/src/main/java/com/klp/da/domain/vo/OeeLossReasonVo.java new file mode 100644 index 00000000..8e4498d4 --- /dev/null +++ b/klp-da/src/main/java/com/klp/da/domain/vo/OeeLossReasonVo.java @@ -0,0 +1,40 @@ +package com.klp.da.domain.vo; + +import lombok.Data; + +import java.math.BigDecimal; + +/** + * 损失原因 TopN VO + */ +@Data +public class OeeLossReasonVo { + + private String lineId; + + /** + * 原因编码 + */ + private String reasonCode; + + /** + * 原因名称 + */ + private String reasonName; + + /** + * 所属损失类别编码 + */ + private String lossCategoryCode; + + /** + * 损失时间(分钟) + */ + private Integer lossTimeMin; + + /** + * 时间占比 + */ + private BigDecimal lossTimeRate; +} + diff --git a/klp-da/src/main/java/com/klp/da/service/IOeeReportService.java b/klp-da/src/main/java/com/klp/da/service/IOeeReportService.java new file mode 100644 index 00000000..b198d21c --- /dev/null +++ b/klp-da/src/main/java/com/klp/da/service/IOeeReportService.java @@ -0,0 +1,47 @@ +package com.klp.da.service; + +import com.klp.common.core.domain.PageQuery; +import com.klp.common.core.page.TableDataInfo; +import com.klp.da.domain.bo.OeeQueryBo; +import com.klp.da.domain.vo.OeeEventVo; +import com.klp.da.domain.vo.OeeLineSummaryVo; + +import javax.servlet.http.HttpServletResponse; +import java.util.List; +import java.util.Map; + +/** + * OEE 报表聚合 Service 接口(方式 A:后端统一聚合多服务) + * + * 实现类负责: + * - 调用酸轧线、镀锌一线等外部服务 + * - 做数据汇总、口径统一和格式转换 + */ +public interface IOeeReportService { + + /** + * KPI + 趋势汇总 + */ + List summary(OeeQueryBo queryBo); + + /** + * 7 大损失汇总 + * + * 返回 Map 以便后续扩展: + * - byLine: List + * - topReasons: List + * 等 + */ + Map lossSummary(OeeQueryBo queryBo); + + /** + * 事件明细 + */ + TableDataInfo events(OeeQueryBo queryBo, PageQuery pageQuery); + + /** + * 导出 Word 报表 + */ + void exportWord(OeeQueryBo queryBo, HttpServletResponse response); +} + diff --git a/klp-da/src/main/java/com/klp/da/service/impl/OeeReportServiceImpl.java b/klp-da/src/main/java/com/klp/da/service/impl/OeeReportServiceImpl.java new file mode 100644 index 00000000..4d0fdee0 --- /dev/null +++ b/klp-da/src/main/java/com/klp/da/service/impl/OeeReportServiceImpl.java @@ -0,0 +1,684 @@ +package com.klp.da.service.impl; + +import com.klp.common.core.domain.PageQuery; +import com.klp.common.core.page.TableDataInfo; +import com.klp.da.domain.bo.OeeQueryBo; +import com.klp.da.domain.vo.OeeEventVo; +import com.klp.da.domain.vo.OeeLineSummaryVo; +import com.klp.da.service.IOeeReportService; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import javax.servlet.http.HttpServletResponse; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * OEE 报表聚合 Service 实现(方式 A) + * + * 仅搭建“架子”,并预先约定两条线的数据来源: + * - {@link #acidLineBaseUrl}:酸轧线(TCM1)服务基础地址,本地 klp-pocket 模块,例如 http://localhost:8080 + * - {@link #galvanizeLineBaseUrl}:镀锌一线服务基础地址,Fizz 平台,例如 http://140.143.206.120:18081 + * + * 配置示例(application-*.yml): + * da: + * oee: + * acid-line-base-url: http://localhost:8080 + * galvanize-line-base-url: http://140.143.206.120:18081 + */ +@Slf4j +@RequiredArgsConstructor +@Service +public class OeeReportServiceImpl implements IOeeReportService { + + private final RestTemplate restTemplate; + + private final ObjectMapper objectMapper = new ObjectMapper(); + + @Value("${da.oee.acid-line-base-url:}") + private String acidLineBaseUrl; + + @Value("${da.oee.galvanize-line-base-url:}") + private String galvanizeLineBaseUrl; + + @Override + public List summary(OeeQueryBo queryBo) { + log.info("OEE summary query: {}", queryBo); + LocalDate start = queryBo.getStartDate(); + LocalDate end = queryBo.getEndDate(); + if (start == null || end == null) { + return Collections.emptyList(); + } + + List lineIds = (queryBo.getLineIds() == null || queryBo.getLineIds().isEmpty()) + ? java.util.Arrays.asList("SY", "DX1") + : queryBo.getLineIds(); + + List result = new ArrayList<>(); + for (String lineId : lineIds) { + if (isAcidLine(lineId)) { + result.add(buildAcidLineSummary(lineId, "酸轧线", start, end)); + } else if (isGalvanizeLine(lineId)) { + result.add(buildGalvanizeLineSummary(lineId, "镀锌一线", start, end)); + } else { + // 未识别的产线:跳过(后续可扩展配置化) + log.warn("Unknown lineId: {}", lineId); + } + } + return result; + } + + @Override + public Map lossSummary(OeeQueryBo queryBo) { + log.info("OEE loss summary query: {}", queryBo); + LocalDate start = queryBo.getStartDate(); + LocalDate end = queryBo.getEndDate(); + if (start == null || end == null) { + return new HashMap<>(); + } + List lineIds = (queryBo.getLineIds() == null || queryBo.getLineIds().isEmpty()) + ? java.util.Arrays.asList("SY", "DX1") + : queryBo.getLineIds(); + + List> byLine = new ArrayList<>(); + for (String lineId : lineIds) { + List events = fetchEvents(lineId, start, end); + Map byStopType = events.stream() + .collect(Collectors.groupingBy( + e -> e.getLossCategoryCode() == null ? "UNKNOWN" : e.getLossCategoryCode(), + Collectors.summingInt(e -> e.getDurationMin() == null ? 0 : e.getDurationMin()) + )); + List> losses = byStopType.entrySet().stream() + .map(e -> { + Map m = new HashMap<>(); + m.put("lossCategoryCode", e.getKey()); + m.put("lossCategoryName", e.getKey()); + m.put("lossTimeMin", e.getValue()); + return m; + }) + .collect(Collectors.toList()); + + Map lineBlock = new HashMap<>(); + lineBlock.put("lineId", lineId); + lineBlock.put("lineName", isAcidLine(lineId) ? "酸轧线" : (isGalvanizeLine(lineId) ? "镀锌一线" : lineId)); + lineBlock.put("losses", losses); + byLine.add(lineBlock); + } + + Map resp = new HashMap<>(); + resp.put("byLine", byLine); + // topReasons:当前两套数据源只有 remark/stopType,先不做更细的 Pareto(后续可按 remark 做 TopN) + return resp; + } + + @Override + public TableDataInfo events(OeeQueryBo queryBo, PageQuery pageQuery) { + log.info("OEE events query: {}, pageQuery: {}", queryBo, pageQuery); + LocalDate start = queryBo.getStartDate(); + LocalDate end = queryBo.getEndDate(); + if (start == null || end == null) { + TableDataInfo empty = new TableDataInfo<>(); + empty.setRows(Collections.emptyList()); + empty.setTotal(0); + return empty; + } + + List lineIds = (queryBo.getLineIds() == null || queryBo.getLineIds().isEmpty()) + ? java.util.Arrays.asList("SY", "DX1") + : queryBo.getLineIds(); + + List all = new ArrayList<>(); + for (String lineId : lineIds) { + all.addAll(fetchEvents(lineId, start, end)); + } + + // keyword / lossCategoryCode 简单过滤 + if (queryBo.getLossCategoryCode() != null && !queryBo.getLossCategoryCode().trim().isEmpty()) { + all = all.stream() + .filter(e -> Objects.equals(queryBo.getLossCategoryCode(), e.getLossCategoryCode())) + .collect(Collectors.toList()); + } + if (queryBo.getKeyword() != null && !queryBo.getKeyword().trim().isEmpty()) { + String kw = queryBo.getKeyword().trim(); + all = all.stream() + .filter(e -> (e.getRawReasonName() != null && e.getRawReasonName().contains(kw)) + || (e.getRemark() != null && e.getRemark().contains(kw))) + .collect(Collectors.toList()); + } + + // 默认按开始时间倒序 + all.sort((a, b) -> { + LocalDateTime at = a.getEventStartTime(); + LocalDateTime bt = b.getEventStartTime(); + if (at == null && bt == null) return 0; + if (at == null) return 1; + if (bt == null) return -1; + return bt.compareTo(at); + }); + + int pageNum = pageQuery.getPageNum() == null ? 1 : pageQuery.getPageNum(); + int pageSize = pageQuery.getPageSize() == null ? 10 : pageQuery.getPageSize(); + int from = Math.max(0, (pageNum - 1) * pageSize); + int to = Math.min(all.size(), from + pageSize); + List pageRows = from >= all.size() ? Collections.emptyList() : all.subList(from, to); + + TableDataInfo table = new TableDataInfo<>(); + table.setRows(pageRows); + table.setTotal(all.size()); + return table; + } + + @Override + public void exportWord(OeeQueryBo queryBo, HttpServletResponse response) { + // TODO: 1) 聚合数据;2) 调用 Word 导出模板生成 .docx;3) 写入 response 输出流 + log.info("OEE exportWord query: {}", queryBo); + } + + /** + * 构建镀锌一线 Fizz 报表汇总接口 URL:/api/report/summary + * 对应 ReportController.getReportSummary(groupNo, shiftNo, startTime, endTime) + */ + private String buildGalvanizeSummaryUrl(OeeQueryBo queryBo) { + StringBuilder sb = new StringBuilder(); + sb.append(galvanizeLineBaseUrl).append("/api/report/summary"); + boolean hasParam = false; + DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + if (queryBo.getStartTime() != null) { + sb.append(hasParam ? "&" : "?") + .append("startTime=").append(queryBo.getStartTime().format(formatter)); + hasParam = true; + } + if (queryBo.getEndTime() != null) { + sb.append(hasParam ? "&" : "?") + .append("endTime=").append(queryBo.getEndTime().format(formatter)); + } + // groupNo / shiftNo 暂不使用,后续需要可从 queryBo 中扩展字段再拼接 + return sb.toString(); + } + + private boolean isAcidLine(String lineId) { + if (lineId == null) return false; + String v = lineId.trim().toUpperCase(); + return v.equals("SY") || v.equals("TCM1") || v.contains("酸") || v.contains("ACID"); + } + + private boolean isGalvanizeLine(String lineId) { + if (lineId == null) return false; + String v = lineId.trim().toUpperCase(); + return v.equals("DX1") || v.equals("G1") || v.contains("镀") || v.contains("GALV"); + } + + private OeeLineSummaryVo buildAcidLineSummary(String lineId, String lineName, LocalDate start, LocalDate end) { + OeeLineSummaryVo vo = new OeeLineSummaryVo(); + vo.setLineId(lineId); + vo.setLineName(lineName); + + List daily = new ArrayList<>(); + BigDecimal totalOutput = BigDecimal.ZERO; + BigDecimal goodOutput = BigDecimal.ZERO; + BigDecimal defectOutput = BigDecimal.ZERO; + int downtimeSum = 0; + int loadingSum = 0; + int runSum = 0; + + for (LocalDate d = start; !d.isAfter(end); d = d.plusDays(1)) { + PocketProductionStats stats = fetchPocketProductionStats(d, d); + BigDecimal qualityRate = fetchCoilQualityRateByDay(d, lineId); + List events = fetchPocketStoppageEvents(d, d); + int downtime = events.stream().mapToInt(e -> e.getDurationMin() == null ? 0 : e.getDurationMin()).sum(); + int loading = (int) ChronoUnit.MINUTES.between(d.atStartOfDay(), d.plusDays(1).atStartOfDay()); + int run = Math.max(0, loading - downtime); + + OeeLineSummaryVo.Daily day = new OeeLineSummaryVo.Daily(); + day.setStatDate(d); + day.setLoadingTimeMin(loading); + day.setDowntimeMin(downtime); + day.setRunTimeMin(run); + day.setTotalOutput(stats.totalExitWeight); + BigDecimal good = nvl(stats.totalEntryWeight).multiply(qualityRate); + BigDecimal defect = nvl(stats.totalEntryWeight).subtract(good).max(BigDecimal.ZERO); + day.setGoodOutput(good); + day.setDefectOutput(defect); + + day.setAvailability(calcRate(run, loading)); + day.setQuality(qualityRate); + day.setPerformance(BigDecimal.ONE); + day.setOee(day.getAvailability().multiply(day.getQuality())); + + daily.add(day); + + totalOutput = totalOutput.add(nvl(stats.totalEntryWeight)); + goodOutput = goodOutput.add(good); + defectOutput = defectOutput.add(defect); + downtimeSum += downtime; + loadingSum += loading; + runSum += run; + } + + OeeLineSummaryVo.Summary total = new OeeLineSummaryVo.Summary(); + total.setLoadingTimeMin(loadingSum); + total.setDowntimeMin(downtimeSum); + total.setRunTimeMin(runSum); + total.setTotalOutput(totalOutput); + total.setGoodOutput(goodOutput); + total.setDefectOutput(defectOutput); + total.setAvailability(calcRate(runSum, loadingSum)); + BigDecimal qualityRate = calcRate(goodOutput, totalOutput); + total.setQuality(qualityRate); + total.setPerformance(BigDecimal.ONE); + total.setOee(total.getAvailability().multiply(total.getQuality())); + + vo.setDaily(daily); + vo.setTotal(total); + return vo; + } + + private OeeLineSummaryVo buildGalvanizeLineSummary(String lineId, String lineName, LocalDate start, LocalDate end) { + OeeLineSummaryVo vo = new OeeLineSummaryVo(); + vo.setLineId(lineId); + vo.setLineName(lineName); + + List daily = new ArrayList<>(); + BigDecimal totalOutput = BigDecimal.ZERO; + BigDecimal goodOutput = BigDecimal.ZERO; + BigDecimal defectOutput = BigDecimal.ZERO; + int downtimeSum = 0; + int loadingSum = 0; + int runSum = 0; + + for (LocalDate d = start; !d.isAfter(end); d = d.plusDays(1)) { + FizzReportSummary stats = fetchFizzReportSummary(d.atStartOfDay(), d.plusDays(1).atStartOfDay().minusSeconds(1)); + BigDecimal qualityRate = fetchCoilQualityRateByDay(d, lineId); + List events = fetchFizzStoppageEvents(d, d); + int downtime = events.stream().mapToInt(e -> e.getDurationMin() == null ? 0 : e.getDurationMin()).sum(); + int loading = (int) ChronoUnit.MINUTES.between(d.atStartOfDay(), d.plusDays(1).atStartOfDay()); + int run = Math.max(0, loading - downtime); + + OeeLineSummaryVo.Daily day = new OeeLineSummaryVo.Daily(); + day.setStatDate(d); + day.setLoadingTimeMin(loading); + day.setDowntimeMin(downtime); + day.setRunTimeMin(run); + day.setTotalOutput(stats.totalActualWeight); + BigDecimal good = nvl(stats.totalEntryWeight).multiply(qualityRate); + BigDecimal defect = nvl(stats.totalEntryWeight).subtract(good).max(BigDecimal.ZERO); + day.setGoodOutput(good); + day.setDefectOutput(defect); + + day.setAvailability(calcRate(run, loading)); + day.setQuality(qualityRate); + day.setPerformance(BigDecimal.ONE); + day.setOee(day.getAvailability().multiply(day.getQuality())); + + daily.add(day); + + totalOutput = totalOutput.add(nvl(stats.totalEntryWeight)); + goodOutput = goodOutput.add(good); + defectOutput = defectOutput.add(defect); + downtimeSum += downtime; + loadingSum += loading; + runSum += run; + } + + OeeLineSummaryVo.Summary total = new OeeLineSummaryVo.Summary(); + total.setLoadingTimeMin(loadingSum); + total.setDowntimeMin(downtimeSum); + total.setRunTimeMin(runSum); + total.setTotalOutput(totalOutput); + total.setGoodOutput(goodOutput); + total.setDefectOutput(defectOutput); + total.setAvailability(calcRate(runSum, loadingSum)); + BigDecimal qualityRate = calcRate(goodOutput, totalOutput); + total.setQuality(qualityRate); + total.setPerformance(BigDecimal.ONE); + total.setOee(total.getAvailability().multiply(total.getQuality())); + + vo.setDaily(daily); + vo.setTotal(total); + return vo; + } + + private List fetchEvents(String lineId, LocalDate start, LocalDate end) { + if (isAcidLine(lineId)) { + return fetchPocketStoppageEvents(start, end); + } + if (isGalvanizeLine(lineId)) { + return fetchFizzStoppageEvents(start, end); + } + return Collections.emptyList(); + } + + /** + * 酸轧线(klp-pocket)生产统计汇总:GET /pocket/productionStatistics/summary + * 响应为 com.klp.common.core.domain.R 包裹。 + */ + private PocketProductionStats fetchPocketProductionStats(LocalDate startDate, LocalDate endDate) { + try { + String base = normalizeBaseUrl(acidLineBaseUrl); + String url = base + "/pocket/productionStatistics/summary" + + "?startDate=" + startDate + + "&endDate=" + endDate; + String json = restTemplate.getForObject(url, String.class); + JsonNode root = objectMapper.readTree(json); + JsonNode data = root.get("data"); + PocketProductionStats s = new PocketProductionStats(); + s.totalExitWeight = toBigDecimal(data, "totalExitWeight"); + s.totalEntryWeight = toBigDecimal(data, "totalEntryWeight"); + s.yieldRate = toBigDecimal(data, "yieldRate"); + return s; + } catch (Exception e) { + log.warn("fetchPocketProductionStats failed: {}~{}", startDate, endDate, e); + return new PocketProductionStats(); + } + } + + /** + * 酸轧线(klp-pocket)停机事件:GET /pocket/proStoppage/list + * + * 该接口是 TableDataInfo 包裹,对应的 Bo 为 Klptcm1ProStoppageBo, + * 其中查询字段名就是 startDate/endDate(已确认)。 + */ + private List fetchPocketStoppageEvents(LocalDate startDate, LocalDate endDate) { + try { + String base = normalizeBaseUrl(acidLineBaseUrl); + String url = base + "/pocket/proStoppage/list" + + "?pageNum=1&pageSize=10000" + + "&startDate=" + startDate + + "&endDate=" + endDate; + String json = restTemplate.getForObject(url, String.class); + JsonNode root = objectMapper.readTree(json); + JsonNode rows = root.get("rows"); + if (rows == null || !rows.isArray()) return Collections.emptyList(); + + List list = new ArrayList<>(); + for (JsonNode n : rows) { + OeeEventVo e = new OeeEventVo(); + e.setLineId("SY"); + e.setLineName("酸轧线"); + e.setEventStartTime(toLocalDateTime(n, "startDate")); + e.setEventEndTime(toLocalDateTime(n, "endDate")); + e.setDurationMin(toInt(n, "duration")); + e.setRawReasonCode(toText(n, "stopType")); + e.setRawReasonName(toText(n, "remark")); + e.setLossCategoryCode(toText(n, "stopType")); + e.setLossCategoryName(toText(n, "stopType")); + e.setRemark(toText(n, "remark")); + list.add(e); + } + return list; + } catch (Exception e) { + log.warn("fetchPocketStoppageEvents failed: {}~{}", startDate, endDate, e); + return Collections.emptyList(); + } + } + + /** + * 镀锌一线(Fizz)生产实绩汇总:GET /api/report/summary + * 响应为 ReportSummaryVO 结构(无 R 包裹)。 + */ + private FizzReportSummary fetchFizzReportSummary(LocalDateTime startTime, LocalDateTime endTime) { + try { + OeeQueryBo qb = new OeeQueryBo(); + qb.setStartTime(startTime); + qb.setEndTime(endTime); + String url = buildGalvanizeSummaryUrl(qb); + String json = restTemplate.getForObject(url, String.class); + JsonNode root = objectMapper.readTree(json); + FizzReportSummary s = new FizzReportSummary(); + s.totalActualWeight = toBigDecimal(root, "totalActualWeight"); + s.totalEntryWeight = toBigDecimal(root, "totalEntryWeight"); + s.yieldRate = toBigDecimal(root, "yieldRate"); + return s; + } catch (Exception e) { + log.warn("fetchFizzReportSummary failed: {}~{}", startTime, endTime, e); + return new FizzReportSummary(); + } + } + + /** + * 使用本地 WMS 钢卷物料接口,根据 quality_status + actual_warehouse_id 统计某一天某条线的良品率: + * - 分母:当天该逻辑库区的钢卷数量(按创建时间 byCreateTimeStart/End + actualWarehouseId) + * - 分子:quality_status=0(正常)的钢卷数量 + * + * lineId -> 库区映射: + * - 酸轧线:actualWarehouseId = 1988150099140866050 + * - 镀锌一线:actualWarehouseId = 1988150323162836993 + */ + private BigDecimal fetchCoilQualityRateByDay(LocalDate day, String lineId) { + try { + Long actualWarehouseId = null; + if (isAcidLine(lineId)) { + actualWarehouseId = 1988150099140866050L; + } else if (isGalvanizeLine(lineId)) { + actualWarehouseId = 1988150323162836993L; + } + if (actualWarehouseId == null) { + // 未知产线:退化为全厂口径 + return fetchCoilQualityRateByDay(day); + } + + String base = normalizeBaseUrl(acidLineBaseUrl); + String start = day.atStartOfDay().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + String end = day.plusDays(1).atStartOfDay().minusSeconds(1) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + + // 全部钢卷数量 + String allUrl = base + "/wms/materialCoil/list" + + "?pageNum=1&pageSize=1" + + "&byCreateTimeStart=" + start + + "&byCreateTimeEnd=" + end + + "&actualWarehouseId=" + actualWarehouseId; + String allJson = restTemplate.getForObject(allUrl, String.class); + JsonNode allRoot = objectMapper.readTree(allJson); + long totalAll = allRoot.path("total").asLong(0); + if (totalAll <= 0) { + return BigDecimal.ONE; + } + + // 良品钢卷数量:quality_status = '0' + String goodUrl = allUrl + "&qualityStatus=0"; + String goodJson = restTemplate.getForObject(goodUrl, String.class); + JsonNode goodRoot = objectMapper.readTree(goodJson); + long totalGood = goodRoot.path("total").asLong(0); + + return calcRate(BigDecimal.valueOf(totalGood), BigDecimal.valueOf(totalAll)); + } catch (Exception e) { + log.warn("fetchCoilQualityRateByDay failed for day {}: {}", day, e.getMessage()); + // 失败时默认按 100% 良品率处理,避免影响主逻辑 + return BigDecimal.ONE; + } + } + + /** + * 全厂整体良品率(仅在无法识别产线时兜底使用)。 + */ + private BigDecimal fetchCoilQualityRateByDay(LocalDate day) { + try { + String base = normalizeBaseUrl(acidLineBaseUrl); + String start = day.atStartOfDay().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + String end = day.plusDays(1).atStartOfDay().minusSeconds(1) + .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); + + String allUrl = base + "/wms/materialCoil/list" + + "?pageNum=1&pageSize=1" + + "&byCreateTimeStart=" + start + + "&byCreateTimeEnd=" + end; + String allJson = restTemplate.getForObject(allUrl, String.class); + JsonNode allRoot = objectMapper.readTree(allJson); + long totalAll = allRoot.path("total").asLong(0); + if (totalAll <= 0) { + return BigDecimal.ONE; + } + + String goodUrl = allUrl + "&qualityStatus=0"; + String goodJson = restTemplate.getForObject(goodUrl, String.class); + JsonNode goodRoot = objectMapper.readTree(goodJson); + long totalGood = goodRoot.path("total").asLong(0); + + return calcRate(BigDecimal.valueOf(totalGood), BigDecimal.valueOf(totalAll)); + } catch (Exception e) { + log.warn("fetchCoilQualityRateByDay(all) failed for day {}: {}", day, e.getMessage()); + return BigDecimal.ONE; + } + } + + /** + * 镀锌一线(Fizz)停机事件:POST /api/stoppage/list + * 响应为 com.ruoyi.common.core.domain.R 包裹。 + */ + private List fetchFizzStoppageEvents(LocalDate startDate, LocalDate endDate) { + try { + String base = normalizeBaseUrl(galvanizeLineBaseUrl); + String url = base + "/api/stoppage/list"; + Map body = new HashMap<>(); + body.put("startDate", startDate.toString()); + body.put("endDate", endDate.toString()); + + String json = restTemplate.postForObject(url, body, String.class); + JsonNode root = objectMapper.readTree(json); + JsonNode data = root.get("data"); + if (data == null || !data.isArray()) return Collections.emptyList(); + + List list = new ArrayList<>(); + for (JsonNode n : data) { + OeeEventVo e = new OeeEventVo(); + e.setLineId("DX1"); + e.setLineName("镀锌一线"); + e.setEventStartTime(toLocalDateTime(n, "startDate")); + e.setEventEndTime(toLocalDateTime(n, "endDate")); + // duration 在 Fizz 为 BigDecimal,字段名 duration + e.setDurationMin(toBigDecimal(n, "duration").intValue()); + e.setRawReasonCode(toText(n, "stopType")); + e.setRawReasonName(toText(n, "remark")); + e.setLossCategoryCode(toText(n, "stopType")); + e.setLossCategoryName(toText(n, "stopType")); + e.setRemark(toText(n, "remark")); + list.add(e); + } + return list; + } catch (Exception e) { + log.warn("fetchFizzStoppageEvents failed: {}~{}", startDate, endDate, e); + return Collections.emptyList(); + } + } + + private String normalizeBaseUrl(String baseUrl) { + if (baseUrl == null || baseUrl.trim().isEmpty()) { + // 默认指向当前应用(同域调用) + return "http://localhost:8080"; + } + String s = baseUrl.trim(); + if (s.endsWith("/")) s = s.substring(0, s.length() - 1); + return s; + } + + private BigDecimal calcRate(int numerator, int denominator) { + if (denominator <= 0) return BigDecimal.ZERO; + return BigDecimal.valueOf(numerator) + .divide(BigDecimal.valueOf(denominator), 6, BigDecimal.ROUND_HALF_UP); + } + + private BigDecimal calcRate(BigDecimal numerator, BigDecimal denominator) { + if (denominator == null || denominator.compareTo(BigDecimal.ZERO) <= 0) { + return BigDecimal.ZERO; + } + return nvl(numerator).divide(denominator, 6, BigDecimal.ROUND_HALF_UP); + } + + private BigDecimal nvl(BigDecimal v) { + return v == null ? BigDecimal.ZERO : v; + } + + /** + * 将可能是 0~1 或 0~100 的比率统一规范到 0~1 区间。 + */ + private BigDecimal normalizeRate(BigDecimal raw) { + BigDecimal r = nvl(raw); + if (r.compareTo(BigDecimal.ONE) > 0 && r.compareTo(BigDecimal.valueOf(100)) <= 0) { + r = r.divide(BigDecimal.valueOf(100), 6, BigDecimal.ROUND_HALF_UP); + } + if (r.compareTo(BigDecimal.ZERO) < 0) { + r = BigDecimal.ZERO; + } else if (r.compareTo(BigDecimal.ONE) > 0) { + r = BigDecimal.ONE; + } + return r; + } + + private BigDecimal toBigDecimal(JsonNode node, String field) { + if (node == null) return BigDecimal.ZERO; + JsonNode v = node.get(field); + if (v == null || v.isNull()) return BigDecimal.ZERO; + if (v.isNumber()) return v.decimalValue(); + String s = v.asText(); + if (s == null || s.trim().isEmpty()) return BigDecimal.ZERO; + try { + return new BigDecimal(s); + } catch (Exception e) { + return BigDecimal.ZERO; + } + } + + private String toText(JsonNode node, String field) { + if (node == null) return null; + JsonNode v = node.get(field); + if (v == null || v.isNull()) return null; + String s = v.asText(); + return (s == null || s.trim().isEmpty()) ? null : s; + } + + private Integer toInt(JsonNode node, String field) { + if (node == null) return 0; + JsonNode v = node.get(field); + if (v == null || v.isNull()) return 0; + if (v.isNumber()) return v.asInt(); + try { + return Integer.parseInt(v.asText()); + } catch (Exception e) { + return 0; + } + } + + private LocalDateTime toLocalDateTime(JsonNode node, String field) { + String s = toText(node, field); + if (s == null) return null; + try { + // 兼容 "yyyy-MM-dd HH:mm:ss" / ISO + if (s.contains("T")) { + return LocalDateTime.parse(s); + } + DateTimeFormatter f = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); + return LocalDateTime.parse(s, f); + } catch (Exception e) { + return null; + } + } + + private static class PocketProductionStats { + BigDecimal totalEntryWeight = BigDecimal.ZERO; + BigDecimal totalExitWeight = BigDecimal.ZERO; + BigDecimal yieldRate = BigDecimal.ZERO; + } + + private static class FizzReportSummary { + BigDecimal totalEntryWeight = BigDecimal.ZERO; + BigDecimal totalActualWeight = BigDecimal.ZERO; + BigDecimal yieldRate = BigDecimal.ZERO; + } +} + diff --git a/klp-ui/src/api/da/oee.js b/klp-ui/src/api/da/oee.js new file mode 100644 index 00000000..c232d00e --- /dev/null +++ b/klp-ui/src/api/da/oee.js @@ -0,0 +1,39 @@ +import request from '@/utils/request' + +// OEE 汇总(两条产线 KPI + 日趋势) +export function fetchOeeSummary(query) { + return request({ + url: '/oee/line/summary', + method: 'get', + params: query + }) +} + +// 7 大损失汇总 +export function fetchOeeLoss7(query) { + return request({ + url: '/oee/line/loss7', + method: 'get', + params: query + }) +} + +// 停机/损失事件明细 +export function fetchOeeEvents(query) { + return request({ + url: '/oee/line/events', + method: 'get', + params: query + }) +} + +// 导出 Word 报表 +export function exportOeeWord(query) { + return request({ + url: '/oee/line/exportWord', + method: 'get', + params: query, + responseType: 'blob' + }) +} + diff --git a/klp-ui/src/views/da/oee/index.vue b/klp-ui/src/views/da/oee/index.vue new file mode 100644 index 00000000..6bedc913 --- /dev/null +++ b/klp-ui/src/views/da/oee/index.vue @@ -0,0 +1,384 @@ + + + + + + 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 dfb7781b..7c5445fd 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 @@ -394,6 +394,20 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService { qw.eq(StringUtils.isNotBlank(bo.getTemperGrade()), "mc.temper_grade", bo.getTemperGrade()); // 独占状态 qw.eq(bo.getExclusiveStatus() != null, "mc.exclusive_status", bo.getExclusiveStatus()); + // 按创建时间范围筛选 + if (bo.getByCreateTimeStart() != null) { + qw.ge("mc.create_time", bo.getByCreateTimeStart()); + } + if (bo.getByCreateTimeEnd() != null) { + qw.le("mc.create_time", bo.getByCreateTimeEnd()); + } + // 按发货时间范围筛选 + if (bo.getByExportTimeStart() != null) { + qw.ge("mc.export_time", bo.getByExportTimeStart()); + } + if (bo.getByExportTimeEnd() != null) { + qw.le("mc.export_time", bo.getByExportTimeEnd()); + } // 统一处理 warehouseId 与 warehouseIds: List warehouseIdList = new ArrayList<>(); if (bo.getWarehouseId() != null) { diff --git a/script/sql/mysql/eqp_auxiliary_material.sql b/script/sql/mysql/eqp_auxiliary_material.sql index 56718cd9..dbe8406b 100644 --- a/script/sql/mysql/eqp_auxiliary_material.sql +++ b/script/sql/mysql/eqp_auxiliary_material.sql @@ -110,7 +110,7 @@ create table wms_material_coil item_id bigint not null comment '物品ID(指向原材料或产品主键)', item_type varchar(20) not null comment '物品类型(raw_material/product)', material_type varchar(20) null comment '材料类型(废品,成品,原料)', - quality_status varchar(20) null comment '质量状态(0=正常,1=待检,2=不合格)', + quality_status varchar(20) null comment '质量状态(C+,C,C-,D+,D,D-为次品,ABC*为良品)', status tinyint(1) default 0 null comment '状态(0=在库,1=已出库)', remark varchar(255) null comment '备注', export_time datetime null comment '发货时间',