OEE初版,错误问题和交互问题1.29再说
This commit is contained in:
@@ -15,6 +15,10 @@
|
||||
<groupId>com.klp</groupId>
|
||||
<artifactId>klp-common</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.klp</groupId>
|
||||
<artifactId>klp-framework</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
|
||||
@@ -8,10 +8,10 @@ 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 com.klp.da.service.OeeSummaryJobService;
|
||||
import com.klp.da.service.OeeTheoryCycleJobService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -40,6 +40,8 @@ import java.util.Map;
|
||||
public class OeeReportController extends BaseController {
|
||||
|
||||
private final IOeeReportService oeeReportService;
|
||||
private final OeeTheoryCycleJobService oeeTheoryCycleJobService;
|
||||
private final OeeSummaryJobService oeeSummaryJobService;
|
||||
|
||||
/**
|
||||
* KPI + 趋势汇总
|
||||
@@ -50,6 +52,24 @@ public class OeeReportController extends BaseController {
|
||||
return R.ok(oeeReportService.summary(queryBo));
|
||||
}
|
||||
|
||||
/**
|
||||
* KPI + 趋势汇总(异步任务):立即返回 jobId,通过 WebSocket 推送进度与结果。
|
||||
*
|
||||
* 前端订阅:
|
||||
* ws://{host}/websocket?type={wsType}
|
||||
*/
|
||||
@Log(title = "OEE 报表-汇总(异步)", businessType = BusinessType.OTHER)
|
||||
@GetMapping("/summary/job")
|
||||
public R<Map<String, Object>> summaryJob(OeeQueryBo queryBo) {
|
||||
System.out.println("[OEE][summary][job][controller] request received, thread=" + Thread.currentThread().getName()
|
||||
+ ", startDate=" + (queryBo == null ? null : queryBo.getStartDate())
|
||||
+ ", endDate=" + (queryBo == null ? null : queryBo.getEndDate())
|
||||
+ ", lineIds=" + (queryBo == null ? null : queryBo.getLineIds()));
|
||||
Map<String, Object> resp = oeeSummaryJobService.createAndStart(queryBo);
|
||||
System.out.println("[OEE][summary][job][controller] job created, resp=" + resp);
|
||||
return R.ok(resp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 7 大损失汇总
|
||||
*/
|
||||
@@ -77,5 +97,34 @@ public class OeeReportController extends BaseController {
|
||||
public void exportWord(OeeQueryBo queryBo, HttpServletResponse response) {
|
||||
oeeReportService.exportWord(queryBo, response);
|
||||
}
|
||||
|
||||
/**
|
||||
* 理论节拍回归(散点+拟合线)
|
||||
*
|
||||
* 说明:该接口用于前端绘制“散点+回归折线”的图形,节拍数据来源于 WMS 回归结果。
|
||||
*/
|
||||
@Log(title = "OEE 报表-理论节拍回归", businessType = BusinessType.OTHER)
|
||||
@GetMapping("/theoryCycle/regression")
|
||||
public R<Map<String, Object>> theoryCycleRegression(OeeQueryBo queryBo) {
|
||||
return R.ok(oeeReportService.theoryCycleRegression(queryBo));
|
||||
}
|
||||
|
||||
/**
|
||||
* 理论节拍回归(异步任务):立即返回 jobId,通过 WebSocket 推送进度与结果。
|
||||
*
|
||||
* 前端订阅:
|
||||
* ws://{host}/websocket?type={wsType}
|
||||
*/
|
||||
@Log(title = "OEE 报表-理论节拍回归(异步)", businessType = BusinessType.OTHER)
|
||||
@GetMapping("/theoryCycle/regression/job")
|
||||
public R<Map<String, Object>> theoryCycleRegressionJob(OeeQueryBo queryBo) {
|
||||
System.out.println("[OEE][theoryCycle][job][controller] request received, thread=" + Thread.currentThread().getName()
|
||||
+ ", startTime=" + (queryBo == null ? null : queryBo.getStartTime())
|
||||
+ ", endTime=" + (queryBo == null ? null : queryBo.getEndTime())
|
||||
+ ", lineIds=" + (queryBo == null ? null : queryBo.getLineIds()));
|
||||
Map<String, Object> resp = oeeTheoryCycleJobService.createAndStart(queryBo);
|
||||
System.out.println("[OEE][theoryCycle][job][controller] job created, resp=" + resp);
|
||||
return R.ok(resp);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,5 +43,10 @@ public interface IOeeReportService {
|
||||
* 导出 Word 报表
|
||||
*/
|
||||
void exportWord(OeeQueryBo queryBo, HttpServletResponse response);
|
||||
|
||||
/**
|
||||
* 理论节拍线性回归(散点+拟合线),用于前端绘图。
|
||||
*/
|
||||
Map<String, Object> theoryCycleRegression(OeeQueryBo queryBo);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.klp.da.service;
|
||||
|
||||
import com.klp.da.domain.bo.OeeQueryBo;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* OEE summary 异步任务(WebSocket 推送)
|
||||
*/
|
||||
public interface OeeSummaryJobService {
|
||||
|
||||
/**
|
||||
* 创建任务并异步执行,立即返回 job 信息。
|
||||
*
|
||||
* @return Map: { jobId, wsType }
|
||||
*/
|
||||
Map<String, Object> createAndStart(OeeQueryBo queryBo);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.klp.da.service;
|
||||
|
||||
import com.klp.da.domain.bo.OeeQueryBo;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 理论节拍回归异步任务(WebSocket 推送)
|
||||
*/
|
||||
public interface OeeTheoryCycleJobService {
|
||||
|
||||
/**
|
||||
* 创建任务并异步执行,立即返回 job 信息。
|
||||
*
|
||||
* @return Map: { jobId, wsType }
|
||||
*/
|
||||
Map<String, Object> createAndStart(OeeQueryBo queryBo);
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,155 @@
|
||||
package com.klp.da.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.klp.da.domain.bo.OeeQueryBo;
|
||||
import com.klp.da.domain.vo.OeeLineSummaryVo;
|
||||
import com.klp.da.service.IOeeReportService;
|
||||
import com.klp.da.service.OeeSummaryJobService;
|
||||
import com.klp.framework.websocket.TypeWebSocketUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OeeSummaryJobServiceImpl implements OeeSummaryJobService {
|
||||
|
||||
public static final String WS_TYPE_PREFIX = "oee_summary:";
|
||||
|
||||
private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(2, r -> {
|
||||
Thread t = new Thread(r);
|
||||
t.setName("oee-summary-job");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
private final IOeeReportService oeeReportService;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public Map<String, Object> createAndStart(OeeQueryBo queryBo) {
|
||||
String jobId = UUID.randomUUID().toString().replace("-", "");
|
||||
String wsType = WS_TYPE_PREFIX + jobId;
|
||||
|
||||
String auth = captureAuthorization();
|
||||
log.info("[OEE][summary][job] created jobId={}, wsType={}, window={}~{}, lines={}, authPresent={}",
|
||||
jobId,
|
||||
wsType,
|
||||
queryBo == null ? null : queryBo.getStartDate(),
|
||||
queryBo == null ? null : queryBo.getEndDate(),
|
||||
queryBo == null ? null : queryBo.getLineIds(),
|
||||
auth != null && !auth.trim().isEmpty());
|
||||
|
||||
push(jobId, wsType, "running", 5, "任务已创建,准备生成汇总…", null, null);
|
||||
|
||||
OeeQueryBo qb = copyQueryBo(queryBo);
|
||||
EXECUTOR.submit(() -> run(jobId, wsType, qb, auth));
|
||||
|
||||
Map<String, Object> resp = new HashMap<>();
|
||||
resp.put("jobId", jobId);
|
||||
resp.put("wsType", wsType);
|
||||
return resp;
|
||||
}
|
||||
|
||||
private void run(String jobId, String wsType, OeeQueryBo queryBo, String authorization) {
|
||||
long t0 = System.currentTimeMillis();
|
||||
try {
|
||||
System.out.println("[OEE][summary][job] start jobId=" + jobId + ", wsType=" + wsType
|
||||
+ ", thread=" + Thread.currentThread().getName()
|
||||
+ ", authPresent=" + (authorization != null && !authorization.trim().isEmpty()));
|
||||
log.info("[OEE][summary][job] start jobId={}, wsType={}, thread={}, authPresent={}",
|
||||
jobId, wsType, Thread.currentThread().getName(), authorization != null && !authorization.trim().isEmpty());
|
||||
push(jobId, wsType, "running", 20, "正在聚合 KPI 与趋势…", null, null);
|
||||
OeeReportServiceImpl.setAuthOverride(authorization);
|
||||
List<OeeLineSummaryVo> lines = oeeReportService.summary(queryBo);
|
||||
push(jobId, wsType, "running", 90, "正在整理汇总结果…", null, null);
|
||||
|
||||
Map<String, Object> data = new HashMap<>();
|
||||
data.put("lines", lines);
|
||||
|
||||
push(jobId, wsType, "success", 100, "汇总生成完成", data, null);
|
||||
System.out.println("[OEE][summary][job] success jobId=" + jobId + ", costMs=" + (System.currentTimeMillis() - t0)
|
||||
+ ", lineCount=" + (lines == null ? 0 : lines.size()));
|
||||
log.info("[OEE][summary][job] success jobId={}, costMs={}, lineCount={}",
|
||||
jobId, (System.currentTimeMillis() - t0), lines == null ? 0 : lines.size());
|
||||
} catch (Exception e) {
|
||||
log.warn("summary job failed, jobId={}", jobId, e);
|
||||
push(jobId, wsType, "failed", 100, "汇总生成失败", null, e.getMessage());
|
||||
System.out.println("[OEE][summary][job] failed jobId=" + jobId + ", costMs=" + (System.currentTimeMillis() - t0)
|
||||
+ ", msg=" + e.getMessage());
|
||||
log.warn("[OEE][summary][job] failed jobId={}, costMs={}, msg={}",
|
||||
jobId, (System.currentTimeMillis() - t0), e.getMessage());
|
||||
} finally {
|
||||
OeeReportServiceImpl.clearAuthOverride();
|
||||
System.out.println("[OEE][summary][job] end jobId=" + jobId + ", costMs=" + (System.currentTimeMillis() - t0));
|
||||
log.info("[OEE][summary][job] end jobId={}, costMs={}", jobId, (System.currentTimeMillis() - t0));
|
||||
}
|
||||
}
|
||||
|
||||
private void push(String jobId, String wsType, String status, int progress, String text,
|
||||
Map<String, Object> data, String errorMsg) {
|
||||
try {
|
||||
Map<String, Object> msg = new HashMap<>();
|
||||
msg.put("jobId", jobId);
|
||||
msg.put("status", status);
|
||||
msg.put("progress", progress);
|
||||
msg.put("text", text);
|
||||
if (data != null) {
|
||||
msg.put("data", data);
|
||||
}
|
||||
if (errorMsg != null && !errorMsg.trim().isEmpty()) {
|
||||
msg.put("errorMsg", errorMsg);
|
||||
}
|
||||
TypeWebSocketUtil.sendToType(wsType, objectMapper.writeValueAsString(msg));
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
private OeeQueryBo copyQueryBo(OeeQueryBo queryBo) {
|
||||
OeeQueryBo qb = new OeeQueryBo();
|
||||
if (queryBo == null) {
|
||||
return qb;
|
||||
}
|
||||
qb.setStartDate(queryBo.getStartDate());
|
||||
qb.setEndDate(queryBo.getEndDate());
|
||||
qb.setLineIds(queryBo.getLineIds());
|
||||
qb.setStartTime(queryBo.getStartTime());
|
||||
qb.setEndTime(queryBo.getEndTime());
|
||||
qb.setTopN(queryBo.getTopN());
|
||||
qb.setLossCategoryCode(queryBo.getLossCategoryCode());
|
||||
qb.setKeyword(queryBo.getKeyword());
|
||||
return qb;
|
||||
}
|
||||
|
||||
private String captureAuthorization() {
|
||||
try {
|
||||
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attrs == null || attrs.getRequest() == null) {
|
||||
return null;
|
||||
}
|
||||
HttpServletRequest req = attrs.getRequest();
|
||||
String auth = req.getHeader("Authorization");
|
||||
if (auth != null && !auth.trim().isEmpty()) {
|
||||
return auth.trim();
|
||||
}
|
||||
String token = req.getHeader("token");
|
||||
if (token != null && !token.trim().isEmpty()) {
|
||||
return "Bearer " + token.trim();
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
package com.klp.da.service.impl;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.klp.da.domain.bo.OeeQueryBo;
|
||||
import com.klp.da.service.IOeeReportService;
|
||||
import com.klp.da.service.OeeTheoryCycleJobService;
|
||||
import com.klp.framework.websocket.TypeWebSocketUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class OeeTheoryCycleJobServiceImpl implements OeeTheoryCycleJobService {
|
||||
|
||||
/**
|
||||
* 推送 type 前缀:前端用 ws://host/websocket?type=oee_theory_cycle_regression:{jobId} 订阅。
|
||||
*/
|
||||
public static final String WS_TYPE_PREFIX = "oee_theory_cycle_regression:";
|
||||
|
||||
private static final long TTL_MS = 10 * 60_000L;
|
||||
private static final ConcurrentHashMap<String, JobState> JOBS = new ConcurrentHashMap<>();
|
||||
|
||||
private static final ExecutorService EXECUTOR = Executors.newFixedThreadPool(2, r -> {
|
||||
Thread t = new Thread(r);
|
||||
t.setName("oee-theory-cycle-job");
|
||||
t.setDaemon(true);
|
||||
return t;
|
||||
});
|
||||
|
||||
private final IOeeReportService oeeReportService;
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
@Override
|
||||
public Map<String, Object> createAndStart(OeeQueryBo queryBo) {
|
||||
String jobId = UUID.randomUUID().toString().replace("-", "");
|
||||
String wsType = WS_TYPE_PREFIX + jobId;
|
||||
|
||||
JobState st = new JobState();
|
||||
st.updatedAt = System.currentTimeMillis();
|
||||
JOBS.put(jobId, st);
|
||||
|
||||
String auth = captureAuthorization();
|
||||
log.info("[OEE][theoryCycle][job] created jobId={}, wsType={}, window={}~{}, lines={}, authPresent={}",
|
||||
jobId,
|
||||
wsType,
|
||||
queryBo == null ? null : queryBo.getStartTime(),
|
||||
queryBo == null ? null : queryBo.getEndTime(),
|
||||
queryBo == null ? null : queryBo.getLineIds(),
|
||||
auth != null && !auth.trim().isEmpty());
|
||||
|
||||
// 立即推送“已创建”
|
||||
push(jobId, wsType, "running", 5, "任务已创建,准备计算…", null, null);
|
||||
|
||||
// 异步执行
|
||||
OeeQueryBo qb = copyQueryBo(queryBo);
|
||||
EXECUTOR.submit(() -> run(jobId, wsType, qb, auth));
|
||||
|
||||
Map<String, Object> resp = new HashMap<>();
|
||||
resp.put("jobId", jobId);
|
||||
resp.put("wsType", wsType);
|
||||
return resp;
|
||||
}
|
||||
|
||||
private void run(String jobId, String wsType, OeeQueryBo queryBo, String authorization) {
|
||||
long t0 = System.currentTimeMillis();
|
||||
try {
|
||||
cleanupExpired();
|
||||
System.out.println("[OEE][theoryCycle][job] start jobId=" + jobId + ", wsType=" + wsType
|
||||
+ ", thread=" + Thread.currentThread().getName()
|
||||
+ ", authPresent=" + (authorization != null && !authorization.trim().isEmpty()));
|
||||
log.info("[OEE][theoryCycle][job] start jobId={}, wsType={}, thread={}, authPresent={}",
|
||||
jobId, wsType, Thread.currentThread().getName(), authorization != null && !authorization.trim().isEmpty());
|
||||
push(jobId, wsType, "running", 20, "开始计算理论节拍回归…", null, null);
|
||||
|
||||
// 这里复用现有 WMS 同步回归聚合逻辑(内部会走 WMS 接口)
|
||||
push(jobId, wsType, "running", 50, "正在拉取样本并拟合回归…", null, null);
|
||||
OeeReportServiceImpl.setAuthOverride(authorization);
|
||||
Map<String, Object> data = oeeReportService.theoryCycleRegression(queryBo);
|
||||
|
||||
push(jobId, wsType, "running", 90, "正在整理回归结果…", null, null);
|
||||
JobState st = JOBS.get(jobId);
|
||||
if (st != null) {
|
||||
st.updatedAt = System.currentTimeMillis();
|
||||
}
|
||||
push(jobId, wsType, "success", 100, "回归计算完成", data, null);
|
||||
System.out.println("[OEE][theoryCycle][job] success jobId=" + jobId + ", costMs=" + (System.currentTimeMillis() - t0));
|
||||
log.info("[OEE][theoryCycle][job] success jobId={}, costMs={}", jobId, (System.currentTimeMillis() - t0));
|
||||
} catch (Exception e) {
|
||||
log.warn("theoryCycle job failed, jobId={}", jobId, e);
|
||||
JobState st = JOBS.get(jobId);
|
||||
if (st != null) {
|
||||
st.updatedAt = System.currentTimeMillis();
|
||||
}
|
||||
push(jobId, wsType, "failed", 100, "回归计算失败", null, e.getMessage());
|
||||
System.out.println("[OEE][theoryCycle][job] failed jobId=" + jobId + ", costMs=" + (System.currentTimeMillis() - t0)
|
||||
+ ", msg=" + e.getMessage());
|
||||
log.warn("[OEE][theoryCycle][job] failed jobId={}, costMs={}, msg={}",
|
||||
jobId, (System.currentTimeMillis() - t0), e.getMessage());
|
||||
} finally {
|
||||
OeeReportServiceImpl.clearAuthOverride();
|
||||
System.out.println("[OEE][theoryCycle][job] end jobId=" + jobId + ", costMs=" + (System.currentTimeMillis() - t0));
|
||||
log.info("[OEE][theoryCycle][job] end jobId={}, costMs={}", jobId, (System.currentTimeMillis() - t0));
|
||||
}
|
||||
}
|
||||
|
||||
private void push(String jobId, String wsType, String status, int progress, String text,
|
||||
Map<String, Object> data, String errorMsg) {
|
||||
try {
|
||||
Map<String, Object> msg = new HashMap<>();
|
||||
msg.put("jobId", jobId);
|
||||
msg.put("status", status);
|
||||
msg.put("progress", progress);
|
||||
msg.put("text", text);
|
||||
if (data != null) {
|
||||
msg.put("data", data);
|
||||
}
|
||||
if (errorMsg != null && !errorMsg.trim().isEmpty()) {
|
||||
msg.put("errorMsg", errorMsg);
|
||||
}
|
||||
TypeWebSocketUtil.sendToType(wsType, objectMapper.writeValueAsString(msg));
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
private void cleanupExpired() {
|
||||
long now = System.currentTimeMillis();
|
||||
JOBS.entrySet().removeIf(e -> e.getValue() == null || (now - e.getValue().updatedAt) > TTL_MS);
|
||||
}
|
||||
|
||||
private OeeQueryBo copyQueryBo(OeeQueryBo queryBo) {
|
||||
OeeQueryBo qb = new OeeQueryBo();
|
||||
if (queryBo == null) {
|
||||
return qb;
|
||||
}
|
||||
qb.setStartDate(queryBo.getStartDate());
|
||||
qb.setEndDate(queryBo.getEndDate());
|
||||
qb.setLineIds(queryBo.getLineIds());
|
||||
qb.setStartTime(queryBo.getStartTime());
|
||||
qb.setEndTime(queryBo.getEndTime());
|
||||
qb.setTopN(queryBo.getTopN());
|
||||
qb.setLossCategoryCode(queryBo.getLossCategoryCode());
|
||||
qb.setKeyword(queryBo.getKeyword());
|
||||
return qb;
|
||||
}
|
||||
|
||||
private String captureAuthorization() {
|
||||
try {
|
||||
ServletRequestAttributes attrs = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
|
||||
if (attrs == null || attrs.getRequest() == null) {
|
||||
return null;
|
||||
}
|
||||
HttpServletRequest req = attrs.getRequest();
|
||||
String auth = req.getHeader("Authorization");
|
||||
if (auth != null && !auth.trim().isEmpty()) {
|
||||
return auth.trim();
|
||||
}
|
||||
String token = req.getHeader("token");
|
||||
if (token != null && !token.trim().isEmpty()) {
|
||||
return "Bearer " + token.trim();
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static class JobState {
|
||||
long updatedAt;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
package com.klp.framework.websocket;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.web.socket.config.annotation.EnableWebSocket;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketConfigurer;
|
||||
import org.springframework.web.socket.config.annotation.WebSocketHandlerRegistry;
|
||||
|
||||
/**
|
||||
* 基于 Spring WebSocket 的 type 分流端点(兼容前端 ws://host/websocket?type=xxx 用法)
|
||||
*
|
||||
* 注意:项目中同时存在 javax.websocket 的 {@link WebSocketServer}(/websocket/message),互不冲突。
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSocket
|
||||
@RequiredArgsConstructor
|
||||
public class TypeWebSocketConfig implements WebSocketConfigurer {
|
||||
|
||||
private final TypeWebSocketHandler typeWebSocketHandler;
|
||||
private final TypeWebSocketInterceptor typeWebSocketInterceptor;
|
||||
|
||||
@Override
|
||||
public void registerWebSocketHandlers(@NonNull WebSocketHandlerRegistry registry) {
|
||||
registry.addHandler(typeWebSocketHandler, "/websocket")
|
||||
.addInterceptors(typeWebSocketInterceptor)
|
||||
.setAllowedOrigins("*");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
package com.klp.framework.websocket;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.CloseStatus;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
import org.springframework.web.socket.handler.TextWebSocketHandler;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* /websocket?type=xxx 连接处理器:按 type 维护会话集合。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TypeWebSocketHandler extends TextWebSocketHandler {
|
||||
|
||||
/**
|
||||
* type -> (sessionId -> session)
|
||||
*/
|
||||
private final Map<String, Map<String, WebSocketSession>> clients = new ConcurrentHashMap<>();
|
||||
|
||||
public Map<String, Map<String, WebSocketSession>> getClients() {
|
||||
return clients;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionEstablished(@NonNull WebSocketSession session) {
|
||||
String sid = session.getId();
|
||||
String type = (String) session.getAttributes().get(TypeWebSocketInterceptor.ATTR_TYPE);
|
||||
if (type == null || type.trim().isEmpty()) {
|
||||
type = "DEFAULT";
|
||||
}
|
||||
clients.computeIfAbsent(type, k -> new ConcurrentHashMap<>()).put(sid, session);
|
||||
log.info("[websocket]建立连接: {}-{}", type, sid);
|
||||
try {
|
||||
session.sendMessage(new TextMessage("{\"event\":\"connected\",\"type\":\"" + type + "\"}"));
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void handleTextMessage(@NonNull WebSocketSession session, @NonNull TextMessage message) throws Exception {
|
||||
String payload = message.getPayload();
|
||||
// 兼容前端心跳
|
||||
if (Objects.equals(payload, "ping")) {
|
||||
session.sendMessage(new TextMessage("pong"));
|
||||
return;
|
||||
}
|
||||
// 默认不做业务处理(目前主要用于服务端推送)
|
||||
log.debug("[websocket]收到客户端消息: {} -> {}", session.getId(), payload);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void handleTransportError(@NonNull WebSocketSession session, @NonNull Throwable exception) throws Exception {
|
||||
log.warn("[websocket]连接异常: {}", session.getId(), exception);
|
||||
onClose(session);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterConnectionClosed(@NonNull WebSocketSession session, @NonNull CloseStatus status) {
|
||||
onClose(session);
|
||||
}
|
||||
|
||||
private void onClose(WebSocketSession session) {
|
||||
try {
|
||||
String sid = session.getId();
|
||||
String type = (String) session.getAttributes().get(TypeWebSocketInterceptor.ATTR_TYPE);
|
||||
if (type == null || type.trim().isEmpty()) {
|
||||
type = "DEFAULT";
|
||||
}
|
||||
Map<String, WebSocketSession> map = clients.get(type);
|
||||
if (map != null) {
|
||||
map.remove(sid);
|
||||
}
|
||||
try {
|
||||
if (session.isOpen()) {
|
||||
session.close();
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
log.info("[websocket]连接关闭: {}-{}", type, sid);
|
||||
} catch (Exception e) {
|
||||
log.warn("[websocket]连接关闭处理异常", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package com.klp.framework.websocket;
|
||||
|
||||
import org.springframework.http.server.ServerHttpRequest;
|
||||
import org.springframework.http.server.ServerHttpResponse;
|
||||
import org.springframework.lang.NonNull;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.WebSocketHandler;
|
||||
import org.springframework.web.socket.server.HandshakeInterceptor;
|
||||
import org.springframework.web.util.UriComponentsBuilder;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 从 ws://host/websocket?type=xxx 提取 type,写入 session attributes。
|
||||
*/
|
||||
@Component
|
||||
public class TypeWebSocketInterceptor implements HandshakeInterceptor {
|
||||
|
||||
public static final String ATTR_TYPE = "type";
|
||||
|
||||
@Override
|
||||
public boolean beforeHandshake(@NonNull ServerHttpRequest request,
|
||||
@NonNull ServerHttpResponse response,
|
||||
@NonNull WebSocketHandler wsHandler,
|
||||
@NonNull Map<String, Object> attributes) {
|
||||
try {
|
||||
URI uri = request.getURI();
|
||||
String type = UriComponentsBuilder.fromUri(uri).build().getQueryParams().getFirst("type");
|
||||
if (type != null && !type.trim().isEmpty()) {
|
||||
attributes.put(ATTR_TYPE, type.trim());
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterHandshake(@NonNull ServerHttpRequest request,
|
||||
@NonNull ServerHttpResponse response,
|
||||
@NonNull WebSocketHandler wsHandler,
|
||||
@Nullable Exception exception) {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
package com.klp.framework.websocket;
|
||||
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.socket.TextMessage;
|
||||
import org.springframework.web.socket.WebSocketSession;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* type 分流 websocket 推送工具:向订阅了指定 type 的客户端广播消息。
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class TypeWebSocketUtil {
|
||||
|
||||
private static TypeWebSocketHandler handler;
|
||||
|
||||
public TypeWebSocketUtil(TypeWebSocketHandler handler) {
|
||||
TypeWebSocketUtil.handler = handler;
|
||||
}
|
||||
|
||||
public static void sendToType(String type, String text) {
|
||||
if (handler == null || type == null) {
|
||||
return;
|
||||
}
|
||||
Map<String, WebSocketSession> sessions = handler.getClients().get(type);
|
||||
if (sessions == null || sessions.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
TextMessage msg = new TextMessage(text == null ? "" : text);
|
||||
sessions.values().forEach(s -> {
|
||||
try {
|
||||
if (s != null && s.isOpen()) {
|
||||
s.sendMessage(msg);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.warn("[websocket]推送消息失败 type={}, sid={}", type, s == null ? null : s.getId(), e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,32 +1,5 @@
|
||||
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({
|
||||
@@ -37,3 +10,62 @@ export function exportOeeWord(query) {
|
||||
})
|
||||
}
|
||||
|
||||
// OEE 产线 KPI + 趋势汇总
|
||||
export function fetchOeeSummary(query) {
|
||||
return request({
|
||||
url: '/oee/line/summary',
|
||||
method: 'get',
|
||||
params: query,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
// OEE 产线 KPI + 趋势汇总(异步任务接口暂保留,当前前端不使用)
|
||||
export function createOeeSummaryJob(query) {
|
||||
return request({
|
||||
url: '/oee/line/summary/job',
|
||||
method: 'get',
|
||||
params: query,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
// 7 大损失汇总
|
||||
export function fetchOeeLoss7(query) {
|
||||
return request({
|
||||
url: '/oee/line/loss7',
|
||||
method: 'get',
|
||||
params: query,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
// 停机/损失事件明细
|
||||
export function fetchOeeEvents(query) {
|
||||
return request({
|
||||
url: '/oee/line/events',
|
||||
method: 'get',
|
||||
params: query,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
// 理论节拍回归结果(含散点与拟合线)
|
||||
export function fetchOeeTheoryCycleRegression(query) {
|
||||
return request({
|
||||
url: '/oee/line/theoryCycle/regression',
|
||||
method: 'get',
|
||||
params: query,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
// 理论节拍回归:创建异步任务,返回 jobId + wsType,通过 WebSocket 推送进度/结果
|
||||
export function createOeeTheoryCycleRegressionJob(query) {
|
||||
return request({
|
||||
url: '/oee/line/theoryCycle/regression/job',
|
||||
method: 'get',
|
||||
params: query,
|
||||
timeout: 120000
|
||||
})
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,9 +18,13 @@ import com.klp.common.core.validate.EditGroup;
|
||||
import com.klp.common.enums.BusinessType;
|
||||
import com.klp.common.utils.poi.ExcelUtil;
|
||||
import com.klp.domain.vo.WmsCoilPendingActionVo;
|
||||
import com.klp.domain.vo.TheoryCycleRegressionResultVo;
|
||||
import com.klp.domain.bo.WmsCoilPendingActionBo;
|
||||
import com.klp.service.IWmsCoilPendingActionService;
|
||||
import com.klp.common.core.page.TableDataInfo;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 钢卷待操作
|
||||
@@ -133,5 +137,17 @@ public class WmsCoilPendingActionController extends BaseController {
|
||||
public R<Void> cancelAction(@PathVariable("actionId") Long actionId) {
|
||||
return toAjax(iWmsCoilPendingActionService.cancelAction(actionId));
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算理论节拍回归(默认近6个月),并返回散点+拟合线
|
||||
*/
|
||||
@GetMapping("/theoryCycle/regression")
|
||||
public R<TheoryCycleRegressionResultVo> theoryCycleRegression(
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date startTime,
|
||||
@DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss") Date endTime,
|
||||
@RequestParam(value = "includePoints", required = false, defaultValue = "false") Boolean includePoints,
|
||||
@RequestParam(value = "maxPoints", required = false, defaultValue = "2000") Integer maxPoints) {
|
||||
return R.ok(iWmsCoilPendingActionService.calcTheoryCycleRegression(startTime, endTime, includePoints, maxPoints));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
package com.klp.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* 理论节拍散点
|
||||
*/
|
||||
@Data
|
||||
public class TheoryCyclePointVo {
|
||||
|
||||
/**
|
||||
* X:产量(吨)
|
||||
*/
|
||||
private Double weightTon;
|
||||
|
||||
/**
|
||||
* Y:耗时(分钟)
|
||||
*/
|
||||
private Double durationMin;
|
||||
|
||||
/**
|
||||
* 对应的动作ID
|
||||
*/
|
||||
private Long actionId;
|
||||
|
||||
/**
|
||||
* 创建时间(便于前端提示)
|
||||
*/
|
||||
private Date createTime;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package com.klp.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 理论节拍回归结果集合
|
||||
*/
|
||||
@Data
|
||||
public class TheoryCycleRegressionResultVo {
|
||||
|
||||
/**
|
||||
* 产线回归结果列表
|
||||
*/
|
||||
private List<TheoryCycleRegressionVo> lines;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
package com.klp.domain.vo;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 单条产线的理论节拍线性回归结果
|
||||
*/
|
||||
@Data
|
||||
public class TheoryCycleRegressionVo {
|
||||
|
||||
private String lineId;
|
||||
|
||||
private String lineName;
|
||||
|
||||
/**
|
||||
* 斜率:分钟/吨
|
||||
*/
|
||||
private Double slopeMinPerTon;
|
||||
|
||||
/**
|
||||
* 截距:分钟
|
||||
*/
|
||||
private Double interceptMin;
|
||||
|
||||
/**
|
||||
* 拟合优度
|
||||
*/
|
||||
private Double r2;
|
||||
|
||||
private Integer sampleCount;
|
||||
|
||||
private Date startTime;
|
||||
|
||||
private Date endTime;
|
||||
|
||||
/**
|
||||
* 散点数据
|
||||
*/
|
||||
private List<TheoryCyclePointVo> points;
|
||||
|
||||
/**
|
||||
* 拟合线两个端点(前端可直接画线)
|
||||
*/
|
||||
private List<TheoryCyclePointVo> linePoints;
|
||||
}
|
||||
|
||||
@@ -7,6 +7,8 @@ import com.klp.common.core.page.TableDataInfo;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Date;
|
||||
import com.klp.domain.vo.TheoryCycleRegressionResultVo;
|
||||
|
||||
/**
|
||||
* 钢卷待操作Service接口
|
||||
@@ -65,5 +67,18 @@ public interface IWmsCoilPendingActionService {
|
||||
* 取消操作
|
||||
*/
|
||||
Boolean cancelAction(Long actionId);
|
||||
|
||||
/**
|
||||
* 计算理论节拍线性回归(默认近6个月),同时返回散点用于前端绘图并将结果缓存。
|
||||
*/
|
||||
TheoryCycleRegressionResultVo calcTheoryCycleRegression(Date startTime, Date endTime);
|
||||
|
||||
/**
|
||||
* 计算理论节拍线性回归(可选择是否返回散点;并对散点数量进行上限控制)。
|
||||
*
|
||||
* @param includePoints 是否返回散点 points(默认 false,避免结果过大)
|
||||
* @param maxPoints 最大散点数(includePoints=true 时生效)
|
||||
*/
|
||||
TheoryCycleRegressionResultVo calcTheoryCycleRegression(Date startTime, Date endTime, Boolean includePoints, Integer maxPoints);
|
||||
}
|
||||
|
||||
|
||||
@@ -2,28 +2,36 @@ package com.klp.service.impl;
|
||||
|
||||
import cn.hutool.core.bean.BeanUtil;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
|
||||
import com.klp.common.core.domain.PageQuery;
|
||||
import com.klp.common.core.domain.entity.SysUser;
|
||||
import com.klp.common.core.page.TableDataInfo;
|
||||
import com.klp.common.utils.StringUtils;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.klp.common.core.domain.PageQuery;
|
||||
import com.klp.common.core.page.TableDataInfo;
|
||||
import com.klp.common.helper.LoginHelper;
|
||||
import com.klp.common.utils.StringUtils;
|
||||
import com.klp.domain.WmsCoilPendingAction;
|
||||
import com.klp.domain.WmsMaterialCoil;
|
||||
import com.klp.domain.vo.WmsMaterialCoilVo;
|
||||
import com.klp.domain.bo.WmsCoilPendingActionBo;
|
||||
import com.klp.domain.vo.TheoryCyclePointVo;
|
||||
import com.klp.domain.vo.TheoryCycleRegressionResultVo;
|
||||
import com.klp.domain.vo.TheoryCycleRegressionVo;
|
||||
import com.klp.domain.vo.WmsCoilPendingActionVo;
|
||||
import com.klp.mapper.WmsCoilPendingActionMapper;
|
||||
import com.klp.mapper.WmsMaterialCoilMapper;
|
||||
import com.klp.service.IWmsMaterialCoilService;
|
||||
import com.klp.service.IWmsCoilPendingActionService;
|
||||
import com.klp.system.service.ISysUserService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
import com.klp.domain.bo.WmsCoilPendingActionBo;
|
||||
import com.klp.domain.vo.WmsCoilPendingActionVo;
|
||||
import com.klp.domain.WmsCoilPendingAction;
|
||||
import com.klp.mapper.WmsCoilPendingActionMapper;
|
||||
import com.klp.service.IWmsCoilPendingActionService;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.math.RoundingMode;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.ZoneId;
|
||||
import java.util.*;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
@@ -40,6 +48,9 @@ public class WmsCoilPendingActionServiceImpl implements IWmsCoilPendingActionSer
|
||||
|
||||
private final ISysUserService userService;
|
||||
private final WmsMaterialCoilMapper materialCoilMapper;
|
||||
private final StringRedisTemplate stringRedisTemplate;
|
||||
|
||||
private static final String REDIS_KEY_IDEAL_CYCLE = "oee:ideal-cycle-time";
|
||||
|
||||
/**
|
||||
* 查询钢卷待操作
|
||||
@@ -248,5 +259,262 @@ public class WmsCoilPendingActionServiceImpl implements IWmsCoilPendingActionSer
|
||||
action.setActionStatus(3); // 已取消
|
||||
return baseMapper.updateById(action) > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TheoryCycleRegressionResultVo calcTheoryCycleRegression(Date startTime, Date endTime) {
|
||||
return calcTheoryCycleRegression(startTime, endTime, true, 2000);
|
||||
}
|
||||
|
||||
@Override
|
||||
public TheoryCycleRegressionResultVo calcTheoryCycleRegression(Date startTime, Date endTime, Boolean includePoints, Integer maxPoints) {
|
||||
LocalDateTime end = endTime == null ? LocalDateTime.now() : toLocalDateTime(endTime);
|
||||
LocalDateTime start = startTime == null ? end.minusMonths(6) : toLocalDateTime(startTime);
|
||||
|
||||
boolean inc = includePoints != null && includePoints;
|
||||
int limit = (maxPoints == null || maxPoints <= 0) ? 2000 : maxPoints;
|
||||
TheoryCycleRegressionVo sy = buildRegression("SY", "酸轧线", 11, start, end, false, inc, limit);
|
||||
TheoryCycleRegressionVo dx1 = buildRegression("DX1", "镀锌一线", 501, start, end, true, inc, limit);
|
||||
|
||||
cacheIdealCycle(sy);
|
||||
cacheIdealCycle(dx1);
|
||||
|
||||
TheoryCycleRegressionResultVo result = new TheoryCycleRegressionResultVo();
|
||||
result.setLines(Arrays.asList(sy, dx1));
|
||||
return result;
|
||||
}
|
||||
|
||||
private TheoryCycleRegressionVo buildRegression(String lineId, String lineName, int actionType,
|
||||
LocalDateTime start, LocalDateTime end,
|
||||
boolean parseRemarkIds,
|
||||
boolean includePoints, int maxPoints) {
|
||||
LambdaQueryWrapper<WmsCoilPendingAction> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(WmsCoilPendingAction::getActionType, actionType)
|
||||
.eq(WmsCoilPendingAction::getDelFlag, 0)
|
||||
.eq(WmsCoilPendingAction::getActionStatus, 2)
|
||||
.ge(WmsCoilPendingAction::getCreateTime, Date.from(start.atZone(ZoneId.systemDefault()).toInstant()))
|
||||
.le(WmsCoilPendingAction::getCreateTime, Date.from(end.atZone(ZoneId.systemDefault()).toInstant()));
|
||||
|
||||
List<WmsCoilPendingAction> actions = baseMapper.selectList(lqw);
|
||||
if (actions == null || actions.isEmpty()) {
|
||||
TheoryCycleRegressionVo vo = new TheoryCycleRegressionVo();
|
||||
vo.setLineId(lineId);
|
||||
vo.setLineName(lineName);
|
||||
vo.setStartTime(Date.from(start.atZone(ZoneId.systemDefault()).toInstant()));
|
||||
vo.setEndTime(Date.from(end.atZone(ZoneId.systemDefault()).toInstant()));
|
||||
vo.setSampleCount(0);
|
||||
vo.setPoints(Collections.emptyList());
|
||||
vo.setLinePoints(Collections.emptyList());
|
||||
return vo;
|
||||
}
|
||||
|
||||
// 预先收集所有需要的钢卷 ID,一次性批量查询,避免在循环中逐条访问数据库
|
||||
Set<Long> allCoilIds = new HashSet<>();
|
||||
for (WmsCoilPendingAction action : actions) {
|
||||
if (action.getCreateTime() == null || action.getCompleteTime() == null) {
|
||||
continue;
|
||||
}
|
||||
if (parseRemarkIds) {
|
||||
List<Long> ids = parseIdsFromRemark(action.getRemark());
|
||||
allCoilIds.addAll(ids);
|
||||
} else {
|
||||
if (action.getCoilId() != null) {
|
||||
allCoilIds.add(action.getCoilId());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Map<Long, Double> weightTonMap = new HashMap<>();
|
||||
if (!allCoilIds.isEmpty()) {
|
||||
List<WmsMaterialCoil> coils = materialCoilMapper.selectBatchIds(allCoilIds);
|
||||
if (coils != null) {
|
||||
for (WmsMaterialCoil coil : coils) {
|
||||
if (coil == null || coil.getCoilId() == null) continue;
|
||||
BigDecimal net = coil.getNetWeight();
|
||||
BigDecimal gross = coil.getGrossWeight();
|
||||
BigDecimal weightKg = net != null && net.compareTo(BigDecimal.ZERO) > 0 ? net : gross;
|
||||
if (weightKg == null || weightKg.compareTo(BigDecimal.ZERO) <= 0) {
|
||||
continue;
|
||||
}
|
||||
double ton = weightKg.divide(BigDecimal.valueOf(1000), 6, RoundingMode.HALF_UP).doubleValue();
|
||||
weightTonMap.put(coil.getCoilId(), ton);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
List<TheoryCyclePointVo> points = new ArrayList<>();
|
||||
for (WmsCoilPendingAction action : actions) {
|
||||
if (action.getCreateTime() == null || action.getCompleteTime() == null) {
|
||||
continue;
|
||||
}
|
||||
long minutes = Duration.between(
|
||||
toLocalDateTime(action.getCreateTime()),
|
||||
toLocalDateTime(action.getCompleteTime())
|
||||
).toMinutes();
|
||||
if (minutes <= 0) {
|
||||
continue;
|
||||
}
|
||||
double weightTon = 0D;
|
||||
if (parseRemarkIds) {
|
||||
List<Long> ids = parseIdsFromRemark(action.getRemark());
|
||||
for (Long id : ids) {
|
||||
Double wt = weightTonMap.get(id);
|
||||
if (wt != null && wt > 0D) {
|
||||
weightTon += wt;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (action.getCoilId() != null) {
|
||||
Double wt = weightTonMap.get(action.getCoilId());
|
||||
if (wt != null && wt > 0D) {
|
||||
weightTon = wt;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (weightTon <= 0D) {
|
||||
continue;
|
||||
}
|
||||
TheoryCyclePointVo p = new TheoryCyclePointVo();
|
||||
p.setActionId(action.getActionId());
|
||||
p.setCreateTime(action.getCreateTime());
|
||||
p.setDurationMin((double) minutes);
|
||||
p.setWeightTon(weightTon);
|
||||
points.add(p);
|
||||
}
|
||||
|
||||
TheoryCycleRegressionVo vo = new TheoryCycleRegressionVo();
|
||||
vo.setLineId(lineId);
|
||||
vo.setLineName(lineName);
|
||||
vo.setStartTime(Date.from(start.atZone(ZoneId.systemDefault()).toInstant()));
|
||||
vo.setEndTime(Date.from(end.atZone(ZoneId.systemDefault()).toInstant()));
|
||||
vo.setSampleCount(points.size());
|
||||
if (includePoints) {
|
||||
vo.setPoints(samplePoints(points, maxPoints));
|
||||
} else {
|
||||
vo.setPoints(Collections.emptyList());
|
||||
}
|
||||
|
||||
RegressionStat stat = linearRegression(points);
|
||||
vo.setSlopeMinPerTon(stat.slope);
|
||||
vo.setInterceptMin(stat.intercept);
|
||||
vo.setR2(stat.r2);
|
||||
vo.setLinePoints(stat.linePoints);
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 散点抽样:避免返回体过大导致网络/序列化问题。
|
||||
*/
|
||||
private List<TheoryCyclePointVo> samplePoints(List<TheoryCyclePointVo> points, int maxPoints) {
|
||||
if (points == null || points.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
if (maxPoints <= 0 || points.size() <= maxPoints) {
|
||||
return points;
|
||||
}
|
||||
int n = points.size();
|
||||
double step = (double) n / (double) maxPoints;
|
||||
List<TheoryCyclePointVo> sampled = new ArrayList<>(maxPoints);
|
||||
for (int i = 0; i < maxPoints; i++) {
|
||||
int idx = (int) Math.floor(i * step);
|
||||
if (idx < 0) idx = 0;
|
||||
if (idx >= n) idx = n - 1;
|
||||
sampled.add(points.get(idx));
|
||||
}
|
||||
return sampled;
|
||||
}
|
||||
|
||||
private void cacheIdealCycle(TheoryCycleRegressionVo vo) {
|
||||
if (vo == null || vo.getSlopeMinPerTon() == null) {
|
||||
return;
|
||||
}
|
||||
String field = vo.getLineId() == null ? "UNKNOWN" : vo.getLineId().toUpperCase();
|
||||
stringRedisTemplate.opsForHash().put(REDIS_KEY_IDEAL_CYCLE, field, vo.getSlopeMinPerTon().toString());
|
||||
if (vo.getInterceptMin() != null) {
|
||||
stringRedisTemplate.opsForHash().put(REDIS_KEY_IDEAL_CYCLE + ":intercept", field, vo.getInterceptMin().toString());
|
||||
}
|
||||
}
|
||||
|
||||
private RegressionStat linearRegression(List<TheoryCyclePointVo> points) {
|
||||
RegressionStat stat = new RegressionStat();
|
||||
if (points == null || points.size() < 2) {
|
||||
return stat;
|
||||
}
|
||||
double sumX = 0, sumY = 0, sumXX = 0, sumXY = 0;
|
||||
for (TheoryCyclePointVo p : points) {
|
||||
double x = p.getWeightTon();
|
||||
double y = p.getDurationMin();
|
||||
sumX += x;
|
||||
sumY += y;
|
||||
sumXX += x * x;
|
||||
sumXY += x * y;
|
||||
}
|
||||
int n = points.size();
|
||||
double denominator = n * sumXX - sumX * sumX;
|
||||
if (denominator == 0) {
|
||||
return stat;
|
||||
}
|
||||
double slope = (n * sumXY - sumX * sumY) / denominator;
|
||||
double intercept = (sumY - slope * sumX) / n;
|
||||
|
||||
double ssTot = 0, ssRes = 0;
|
||||
double meanY = sumY / n;
|
||||
for (TheoryCyclePointVo p : points) {
|
||||
double y = p.getDurationMin();
|
||||
double yHat = slope * p.getWeightTon() + intercept;
|
||||
ssTot += Math.pow(y - meanY, 2);
|
||||
ssRes += Math.pow(y - yHat, 2);
|
||||
}
|
||||
double r2 = ssTot == 0 ? 0 : 1 - ssRes / ssTot;
|
||||
|
||||
stat.slope = slope;
|
||||
stat.intercept = intercept;
|
||||
stat.r2 = r2;
|
||||
stat.linePoints = buildLinePoints(points, slope, intercept);
|
||||
return stat;
|
||||
}
|
||||
|
||||
private List<TheoryCyclePointVo> buildLinePoints(List<TheoryCyclePointVo> points, double slope, double intercept) {
|
||||
if (points == null || points.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
double minX = points.stream().mapToDouble(TheoryCyclePointVo::getWeightTon).min().orElse(0D);
|
||||
double maxX = points.stream().mapToDouble(TheoryCyclePointVo::getWeightTon).max().orElse(0D);
|
||||
List<TheoryCyclePointVo> line = new ArrayList<>();
|
||||
TheoryCyclePointVo p1 = new TheoryCyclePointVo();
|
||||
p1.setWeightTon(minX);
|
||||
p1.setDurationMin(slope * minX + intercept);
|
||||
TheoryCyclePointVo p2 = new TheoryCyclePointVo();
|
||||
p2.setWeightTon(maxX);
|
||||
p2.setDurationMin(slope * maxX + intercept);
|
||||
line.add(p1);
|
||||
line.add(p2);
|
||||
return line;
|
||||
}
|
||||
|
||||
private List<Long> parseIdsFromRemark(String remark) {
|
||||
if (StringUtils.isBlank(remark)) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
Matcher matcher = Pattern.compile("\\d+").matcher(remark);
|
||||
List<Long> ids = new ArrayList<>();
|
||||
while (matcher.find()) {
|
||||
try {
|
||||
ids.add(Long.parseLong(matcher.group()));
|
||||
} catch (NumberFormatException ignore) {
|
||||
}
|
||||
}
|
||||
return ids;
|
||||
}
|
||||
|
||||
private LocalDateTime toLocalDateTime(Date date) {
|
||||
return LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault());
|
||||
}
|
||||
|
||||
private static class RegressionStat {
|
||||
Double slope;
|
||||
Double intercept;
|
||||
Double r2;
|
||||
List<TheoryCyclePointVo> linePoints = Collections.emptyList();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -394,15 +394,6 @@ 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());
|
||||
//逻辑删除
|
||||
qw.eq("mc.del_flag", 0);
|
||||
// 按创建时间范围筛选
|
||||
if (bo.getByCreateTimeStart() != null) {
|
||||
qw.ge("mc.create_time", bo.getByCreateTimeStart());
|
||||
}
|
||||
if (bo.getByCreateTimeEnd() != null) {
|
||||
qw.le("mc.create_time", bo.getByCreateTimeEnd());
|
||||
}
|
||||
// 统一处理 warehouseId 与 warehouseIds:
|
||||
List<Long> warehouseIdList = new ArrayList<>();
|
||||
if (bo.getWarehouseId() != null) {
|
||||
@@ -571,13 +562,17 @@ 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());
|
||||
//根据开始时间和结束时间筛选修改时间
|
||||
qw.ge(bo.getStartTime() != null, "mc.update_time", bo.getStartTime());
|
||||
qw.le(bo.getEndTime() != null, "mc.update_time", bo.getEndTime());
|
||||
|
||||
qw.ge(bo.getByCreateTimeStart() != null, "mc.create_time", bo.getByCreateTimeStart());
|
||||
qw.le(bo.getByCreateTimeEnd() != null, "mc.create_time", bo.getByCreateTimeEnd());
|
||||
|
||||
// 处理发货时间筛选逻辑(核心修改部分)
|
||||
if (bo.getByExportTimeStart() != null || bo.getByExportTimeEnd() != null) {
|
||||
// 开启OR条件分组:满足情况1 或 情况2
|
||||
@@ -618,50 +613,69 @@ public class WmsMaterialCoilServiceImpl implements IWmsMaterialCoilService {
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getDuplicateCoilGroups() {
|
||||
// 使用优化的数据库查询方法,直接获取重复入场卷号的钢卷信息
|
||||
List<WmsMaterialCoilVo> enterDuplicates = baseMapper.selectDuplicateEnterCoilNoList();
|
||||
LambdaQueryWrapper<WmsMaterialCoil> lqw = Wrappers.lambdaQuery();
|
||||
lqw.eq(WmsMaterialCoil::getDataType, 1);
|
||||
lqw.eq(WmsMaterialCoil::getDelFlag, 0);
|
||||
List<WmsMaterialCoil> all = baseMapper.selectList(lqw);
|
||||
|
||||
// 使用优化的数据库查询方法,直接获取重复当前卷号的钢卷信息
|
||||
List<WmsMaterialCoilVo> currentDuplicates = baseMapper.selectDuplicateCurrentCoilNoList();
|
||||
|
||||
// 按入场卷号分组重复项
|
||||
Map<String, List<WmsMaterialCoilVo>> enterGrouped = enterDuplicates.stream()
|
||||
Map<String, List<WmsMaterialCoil>> enterGrouped = all.stream()
|
||||
.filter(e -> StringUtils.isNotBlank(e.getEnterCoilNo()))
|
||||
.collect(Collectors.groupingBy(WmsMaterialCoilVo::getEnterCoilNo));
|
||||
|
||||
// 按当前卷号分组重复项
|
||||
Map<String, List<WmsMaterialCoilVo>> currentGrouped = currentDuplicates.stream()
|
||||
.collect(Collectors.groupingBy(WmsMaterialCoil::getEnterCoilNo));
|
||||
Map<String, List<WmsMaterialCoil>> currentGrouped = all.stream()
|
||||
.filter(e -> StringUtils.isNotBlank(e.getCurrentCoilNo()))
|
||||
.collect(Collectors.groupingBy(WmsMaterialCoilVo::getCurrentCoilNo));
|
||||
.collect(Collectors.groupingBy(WmsMaterialCoil::getCurrentCoilNo));
|
||||
|
||||
// 构建入场卷号重复组
|
||||
List<Map<String, Object>> enterGroups = enterGrouped.entrySet().stream()
|
||||
.filter(entry -> entry.getValue() != null && entry.getValue().size() > 1)
|
||||
.map(entry -> {
|
||||
Map<String, Object> group = new HashMap<>();
|
||||
group.put("enterCoilNo", entry.getKey());
|
||||
group.put("coils", entry.getValue());
|
||||
return group;
|
||||
.filter(en -> en.getValue() != null && en.getValue().size() > 1)
|
||||
.map(en -> {
|
||||
List<WmsMaterialCoilVo> vos = en.getValue().stream().map(this::toVoBasic).collect(Collectors.toList());
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("enterCoilNo", en.getKey());
|
||||
m.put("coils", vos);
|
||||
return m;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 构建当前卷号重复组
|
||||
List<Map<String, Object>> currentGroups = currentGrouped.entrySet().stream()
|
||||
.filter(entry -> entry.getValue() != null && entry.getValue().size() > 1)
|
||||
.map(entry -> {
|
||||
Map<String, Object> group = new HashMap<>();
|
||||
group.put("currentCoilNo", entry.getKey());
|
||||
group.put("coils", entry.getValue());
|
||||
return group;
|
||||
.filter(en -> en.getValue() != null && en.getValue().size() > 1)
|
||||
.map(en -> {
|
||||
List<WmsMaterialCoilVo> vos = en.getValue().stream().map(this::toVoBasic).collect(Collectors.toList());
|
||||
Map<String, Object> m = new HashMap<>();
|
||||
m.put("currentCoilNo", en.getKey());
|
||||
m.put("coils", vos);
|
||||
return m;
|
||||
})
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 可选:批量填充关联对象信息
|
||||
List<WmsMaterialCoilVo> allVos = new ArrayList<>();
|
||||
for (Map<String, Object> g : enterGroups) {
|
||||
Object list = g.get("coils");
|
||||
if (list instanceof List) {
|
||||
allVos.addAll((List<WmsMaterialCoilVo>) list);
|
||||
}
|
||||
}
|
||||
for (Map<String, Object> g : currentGroups) {
|
||||
Object list = g.get("coils");
|
||||
if (list instanceof List) {
|
||||
allVos.addAll((List<WmsMaterialCoilVo>) list);
|
||||
}
|
||||
}
|
||||
if (!allVos.isEmpty()) {
|
||||
fillRelatedObjectsBatch(allVos);
|
||||
}
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
result.put("enterGroups", enterGroups);
|
||||
result.put("currentGroups", currentGroups);
|
||||
return result;
|
||||
}
|
||||
|
||||
private WmsMaterialCoilVo toVoBasic(WmsMaterialCoil e) {
|
||||
WmsMaterialCoilVo vo = new WmsMaterialCoilVo();
|
||||
BeanUtils.copyProperties(e, vo);
|
||||
return vo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建 OR 连接的 LIKE 子句,使用 MyBatis-Plus apply 的 {index} 占位符并将参数加入 args。
|
||||
|
||||
Reference in New Issue
Block a user