Merge remote-tracking branch 'origin/0.8.X' into 0.8.X

This commit is contained in:
2026-01-29 09:35:41 +08:00
22 changed files with 2811 additions and 545 deletions

View File

@@ -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));
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,18 @@
package com.klp.domain.vo;
import lombok.Data;
import java.util.List;
/**
* 理论节拍回归结果集合
*/
@Data
public class TheoryCycleRegressionResultVo {
/**
* 产线回归结果列表
*/
private List<TheoryCycleRegressionVo> lines;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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();
}
}

View File

@@ -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
@@ -628,50 +623,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。