Files
klp-oa/klp-wms/src/main/java/com/klp/service/impl/WmsCoilPendingActionServiceImpl.java
wangyu 53a180787b 1完成酸轧轧辊调整
2完成双机架工艺规格串联
3完成双机架计划串联
4完成双机架wip快捷录入检索
5完成双机架实绩串联
2026-05-19 17:13:37 +08:00

741 lines
31 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package com.klp.service.impl;
import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
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.exception.ServiceException;
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.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.domain.vo.WmsCoilPendingActionIdCoilVo;
import com.klp.domain.DrMillProductionPlan;
import com.klp.domain.WmsRawMaterial;
import com.klp.mapper.DrMillProductionPlanMapper;
import com.klp.mapper.WmsCoilPendingActionMapper;
import com.klp.mapper.WmsMaterialCoilMapper;
import com.klp.mapper.WmsRawMaterialMapper;
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 org.springframework.transaction.annotation.Transactional;
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;
/**
* 钢卷待操作Service业务层处理
*
* @author Joshi
* @date 2025-11-03
*/
@RequiredArgsConstructor
@Service
public class WmsCoilPendingActionServiceImpl implements IWmsCoilPendingActionService {
private final WmsCoilPendingActionMapper baseMapper;
private final ISysUserService userService;
private final WmsMaterialCoilMapper materialCoilMapper;
private final WmsRawMaterialMapper rawMaterialMapper;
private final DrMillProductionPlanMapper drPlanMapper;
private final StringRedisTemplate stringRedisTemplate;
/** 双机架工序 / 修复的 actionType */
private static final int ACTION_TYPE_DR_NORMAL = 504;
private static final int ACTION_TYPE_DR_REPAIR = 524;
private static final String REDIS_KEY_IDEAL_CYCLE = "oee:ideal-cycle-time";
/**
* 查询钢卷待操作
*/
@Override
public WmsCoilPendingActionVo queryById(Long actionId){
return baseMapper.selectVoById(actionId);
}
/**
* 查询钢卷待操作列表
*/
@Override
public TableDataInfo<WmsCoilPendingActionVo> queryPageList(WmsCoilPendingActionBo bo, PageQuery pageQuery) {
QueryWrapper<WmsCoilPendingAction> lqw = buildQueryWrapperPlus(bo);
Page<WmsCoilPendingActionVo> result = baseMapper.selectVoPagePlus(pageQuery.build(), lqw);
List<WmsCoilPendingActionVo> records = result.getRecords();
Set<String> userNames = records.stream()
.flatMap(v -> java.util.stream.Stream.of(v.getCreateBy(), v.getOperatorName()))
.filter(StringUtils::isNotBlank)
.collect(Collectors.toSet());
if (!userNames.isEmpty()) {
Map<String, String> nickMap = userService.selectNickNameMapByUserNames(records.stream()
.flatMap(v -> java.util.stream.Stream.of(v.getCreateBy(), v.getOperatorName()))
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList()));
records.forEach(item -> {
if (StringUtils.isNotBlank(item.getCreateBy())) {
item.setCreateByName(nickMap.getOrDefault(item.getCreateBy(), item.getCreateBy()));
}
if (StringUtils.isNotBlank(item.getOperatorName())) {
item.setOperatorByName(nickMap.getOrDefault(item.getOperatorName(), item.getOperatorName()));
}
});
}
return TableDataInfo.build(result);
}
private QueryWrapper<WmsCoilPendingAction> buildQueryWrapperPlus(WmsCoilPendingActionBo bo) {
QueryWrapper<WmsCoilPendingAction> qw = Wrappers.query();
qw.eq(bo.getCoilId() != null, "wcpa.coil_id", bo.getCoilId());
if (StringUtils.isNotBlank(bo.getCoilIds())) {
List<Long> coilIdList = Arrays.stream(bo.getCoilIds().split(","))
.filter(StringUtils::isNotBlank)
.map(Long::parseLong)
.collect(Collectors.toList());
if (!coilIdList.isEmpty()) {
qw.in("wcpa.coil_id", coilIdList);
}
}
qw.like(StringUtils.isNotBlank(bo.getCurrentCoilNo()), "wcpa.current_coil_no", bo.getCurrentCoilNo());
if (bo.getActionTypes() != null && !bo.getActionTypes().isEmpty()) {
qw.in("wcpa.action_type", bo.getActionTypes());
} else {
qw.eq(bo.getActionType() != null, "wcpa.action_type", bo.getActionType());
}
if (bo.getActionStatus() != null) {
if (bo.getActionStatus() == -1) {
qw.ne("wcpa.action_status", 2);
} else {
qw.eq("wcpa.action_status", bo.getActionStatus());
}
}
qw.eq(bo.getWarehouseId() != null, "wcpa.warehouse_id", bo.getWarehouseId());
qw.eq(bo.getPriority() != null, "wcpa.priority", bo.getPriority());
qw.like(StringUtils.isNotBlank(bo.getSourceType()), "wcpa.source_type", bo.getSourceType());
qw.orderByDesc("wcpa.create_time");
qw.orderByDesc("wcpa.scan_time");
//根据开始时间和结束时间筛选修改时间
qw.ge(bo.getStartTime() != null, "wcpa.complete_time", bo.getStartTime());
qw.le(bo.getEndTime() != null, "wcpa.complete_time", bo.getEndTime());
// 根据更新人查询
qw.eq(StringUtils.isNotBlank(bo.getUpdateBy()), "wcpa.update_by", bo.getUpdateBy());
// 根据创建人筛选(支持逗号分隔的多个创建人)
if (StringUtils.isNotBlank(bo.getCreateBys())) {
List<String> createByList = Arrays.stream(bo.getCreateBys().split(","))
.filter(StringUtils::isNotBlank)
.map(String::trim)
.collect(Collectors.toList());
if (!createByList.isEmpty()) {
qw.in("wcpa.create_by", createByList);
}
}
// 加工后的钢卷ids
qw.like(StringUtils.isNotBlank(bo.getProcessedCoilIds()), "wcpa.processed_coil_ids", bo.getProcessedCoilIds());
//逻辑删除 - 支持查询已删除记录
if (bo.getIncludeDeleted() != null) {
if (bo.getIncludeDeleted() == 1) {
// 包含已删除记录不添加del_flag过滤查询所有记录
} else if (bo.getIncludeDeleted() == 2) {
// 仅查询已删除记录
qw.eq("wcpa.del_flag", 2);
} else {
// 默认:仅查询正常记录
qw.eq("wcpa.del_flag", 0);
}
} else {
// 未传参数时默认仅查询正常记录
qw.eq("wcpa.del_flag", 0);
}
return qw;
}
/**
* 查询钢卷待操作列表
*/
@Override
public List<WmsCoilPendingActionVo> queryList(WmsCoilPendingActionBo bo) {
LambdaQueryWrapper<WmsCoilPendingAction> lqw = buildQueryWrapper(bo);
return baseMapper.selectVoList(lqw);
}
/**
* 查询待操作记录中关联钢卷已是历史钢卷dataType=0且操作未完成actionStatus != 2的记录
*/
@Override
@Transactional(readOnly = true)
public TableDataInfo<WmsCoilPendingActionVo> queryStaleActionPageList(PageQuery pageQuery) {
QueryWrapper<WmsCoilPendingAction> lqw = Wrappers.query();
lqw.ne("wcpa.action_status", 2);
lqw.eq("wcpa.del_flag", 0);
lqw.orderByDesc("wcpa.create_time");
lqw.orderByDesc("wcpa.scan_time");
Page<WmsCoilPendingActionVo> result = baseMapper.selectStaleActionVoPagePlus(pageQuery.build(), lqw);
List<WmsCoilPendingActionVo> records = result.getRecords();
if (records != null && !records.isEmpty()) {
Set<String> userNames = records.stream()
.flatMap(v -> java.util.stream.Stream.of(v.getCreateBy(), v.getOperatorName()))
.filter(StringUtils::isNotBlank)
.collect(Collectors.toSet());
if (!userNames.isEmpty()) {
Map<String, String> nickMap = userService.selectNickNameMapByUserNames(records.stream()
.flatMap(v -> java.util.stream.Stream.of(v.getCreateBy(), v.getOperatorName()))
.filter(StringUtils::isNotBlank)
.distinct()
.collect(Collectors.toList()));
records.forEach(item -> {
if (StringUtils.isNotBlank(item.getCreateBy())) {
item.setCreateByName(nickMap.getOrDefault(item.getCreateBy(), item.getCreateBy()));
}
if (StringUtils.isNotBlank(item.getOperatorName())) {
item.setOperatorByName(nickMap.getOrDefault(item.getOperatorName(), item.getOperatorName()));
}
});
}
}
return TableDataInfo.build(result);
}
@Override
public List<WmsCoilPendingActionIdCoilVo> queryActionIdCoilIdList(WmsCoilPendingActionBo bo) {
// 复用与 /list 相同的查询条件buildQueryWrapperPlus
QueryWrapper<WmsCoilPendingAction> lqw = buildQueryWrapperPlus(bo);
return baseMapper.selectActionIdCoilIdList(lqw);
}
private LambdaQueryWrapper<WmsCoilPendingAction> buildQueryWrapper(WmsCoilPendingActionBo bo) {
LambdaQueryWrapper<WmsCoilPendingAction> lqw = Wrappers.lambdaQuery();
lqw.eq(bo.getCoilId() != null, WmsCoilPendingAction::getCoilId, bo.getCoilId());
lqw.like(StringUtils.isNotBlank(bo.getCurrentCoilNo()), WmsCoilPendingAction::getCurrentCoilNo, bo.getCurrentCoilNo());
if (bo.getActionTypes() != null && !bo.getActionTypes().isEmpty()) {
lqw.in(WmsCoilPendingAction::getActionType, bo.getActionTypes());
} else {
lqw.eq(bo.getActionType() != null, WmsCoilPendingAction::getActionType, bo.getActionType());
}
lqw.eq(bo.getActionStatus() != null, WmsCoilPendingAction::getActionStatus, bo.getActionStatus());
lqw.eq(bo.getWarehouseId() != null, WmsCoilPendingAction::getWarehouseId, bo.getWarehouseId());
lqw.eq(bo.getPriority() != null, WmsCoilPendingAction::getPriority, bo.getPriority());
lqw.like(StringUtils.isNotBlank(bo.getSourceType()), WmsCoilPendingAction::getSourceType, bo.getSourceType());
lqw.orderByDesc(WmsCoilPendingAction::getCreateTime);
lqw.orderByDesc(WmsCoilPendingAction::getScanTime);
return lqw;
}
/**
* 新增钢卷待操作
*/
@Override
public Boolean insertByBo(WmsCoilPendingActionBo bo) {
WmsCoilPendingAction add = BeanUtil.toBean(bo, WmsCoilPendingAction.class);
validEntityBeforeSave(add);
if (add.getCoilId() != null){
WmsMaterialCoil materialCoil = materialCoilMapper.selectById(add.getCoilId());
if (materialCoil.getDataType() == 0) {
throw new RuntimeException("该钢卷为历史钢卷不能被操作");
}
}
// 设置默认值
if (add.getActionStatus() == null) {
add.setActionStatus(0); // 默认待处理
}
if (StringUtils.isBlank(add.getSourceType())) {
add.setSourceType("manual"); // 默认手动创建
}
if (add.getScanTime() == null && "scan".equals(add.getSourceType())) {
add.setScanTime(new Date());
}
boolean flag = baseMapper.insert(add) > 0;
if (flag) {
bo.setActionId(add.getActionId());
// 双机架工序/修复:同步在 double-rack 数据库创建生产计划
if (ACTION_TYPE_DR_NORMAL == add.getActionType() || ACTION_TYPE_DR_REPAIR == add.getActionType()) {
autoCreateDrPlan(add);
}
}
return flag;
}
/**
* 自动创建双机架生产计划(写入 double-rack 数据源)。
* 同时查询钢卷库wms_material_coil和原材料库wms_raw_material
* 将完整的钢卷/原料规格信息写入计划。
* planNo = "DR" + actionId保证唯一且可追溯。
*/
@com.baomidou.dynamic.datasource.annotation.DS("double-rack")
private void autoCreateDrPlan(WmsCoilPendingAction action) {
try {
DrMillProductionPlan plan = new DrMillProductionPlan();
plan.setPlanNo("DR" + action.getActionId());
if (action.getCoilId() != null) {
// ① 查钢卷库
WmsMaterialCoil coil = materialCoilMapper.selectById(action.getCoilId());
if (coil != null) {
plan.setInMatNo(coil.getEnterCoilNo() != null ? coil.getEnterCoilNo() : coil.getCurrentCoilNo());
plan.setEnterCoilNo(coil.getEnterCoilNo());
plan.setCurrentCoilNo(coil.getCurrentCoilNo());
// 实测厚度(优先)
if (coil.getActualThickness() != null) {
try { plan.setInMatThick(new java.math.BigDecimal(coil.getActualThickness())); } catch (Exception ignored) {}
}
// 实测宽度(优先)
plan.setInMatWidth(coil.getActualWidth());
// 净重 > 毛重
plan.setInMatWeight(coil.getNetWeight() != null ? coil.getNetWeight() : coil.getGrossWeight());
// 长度
plan.setInMatLength(coil.getLength());
// ② 查原材料库itemType='raw_material' 时通过 itemId 关联)
if ("raw_material".equals(coil.getItemType()) && coil.getItemId() != null) {
WmsRawMaterial rm = rawMaterialMapper.selectById(coil.getItemId());
if (rm != null) {
// 钢种/合金号
plan.setAlloyNo(rm.getSteelGrade());
// 标称厚度(实测没有时用标称补齐)
if (plan.getInMatThick() == null && rm.getThickness() != null) {
plan.setInMatThick(rm.getThickness());
}
// 标称宽度(实测没有时用标称补齐)
if (plan.getInMatWidth() == null && rm.getWidth() != null) {
plan.setInMatWidth(rm.getWidth());
}
// 卷重WMS 净重没有时用原材料卷重补齐)
if (plan.getInMatWeight() == null && rm.getCoilWeight() != null) {
plan.setInMatWeight(rm.getCoilWeight());
}
// 目标冷轧厚度 / 宽度写入出口目标厚度
if (rm.getTargetColdThickness() != null) {
plan.setOutThick(rm.getTargetColdThickness());
}
}
}
}
} else if (action.getCurrentCoilNo() != null) {
plan.setInMatNo(action.getCurrentCoilNo());
plan.setCurrentCoilNo(action.getCurrentCoilNo());
}
// 修复工序在备注中标记
if (ACTION_TYPE_DR_REPAIR == action.getActionType()) {
plan.setRemark("[修复] " + (action.getRemark() != null ? action.getRemark() : ""));
} else {
plan.setRemark(action.getRemark());
}
String user = LoginHelper.getUsername();
plan.setCreateBy(user);
plan.setUpdateBy(user);
plan.setPlanStatus("0");
plan.setProdStatus("Idle");
// 排到队列末尾
List<DrMillProductionPlan> all = drPlanMapper.selectList(new DrMillProductionPlan());
plan.setSortNo(all.size() + 1);
drPlanMapper.insert(plan);
} catch (Exception e) {
org.slf4j.LoggerFactory.getLogger(getClass())
.error("[双机架] 自动创建生产计划失败, actionId={}", action.getActionId(), e);
}
}
/**
* 修改钢卷待操作
*/
@Override
public Boolean updateByBo(WmsCoilPendingActionBo bo) {
WmsCoilPendingAction update = BeanUtil.toBean(bo, WmsCoilPendingAction.class);
validEntityBeforeSave(update);
return baseMapper.updateById(update) > 0;
}
/**
* 保存前的数据校验
*/
private void validEntityBeforeSave(WmsCoilPendingAction entity){
// TODO 做一些数据校验,如唯一约束
}
/**
* 批量删除钢卷待操作
*/
@Override
public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
if(isValid){
// TODO 做一些业务上的校验,判断是否需要校验
}
return baseMapper.deleteBatchIds(ids) > 0;
}
/**
* 更新操作状态
*/
@Override
public Boolean updateStatus(Long actionId, Integer status) {
WmsCoilPendingAction action = new WmsCoilPendingAction();
action.setActionId(actionId);
action.setActionStatus(status);
return baseMapper.updateById(action) > 0;
}
/**
* 开始处理操作
*/
@Override
public Boolean startProcess(Long actionId) {
WmsCoilPendingAction action = new WmsCoilPendingAction();
action.setActionId(actionId);
action.setActionStatus(1); // 处理中
action.setProcessTime(new Date());
try {
action.setOperatorId(LoginHelper.getUserId());
action.setOperatorName(LoginHelper.getUsername());
} catch (Exception e) {
// 如果获取登录用户失败,不影响主流程
}
return baseMapper.updateById(action) > 0;
}
/**
* 完成操作
*/
@Override
public Boolean completeAction(Long actionId, String newCoilIds) {
// 先查询原记录,检查操作人是否为空
WmsCoilPendingAction oldAction = baseMapper.selectById(actionId);
if (oldAction == null) {
throw new RuntimeException("待操作记录不存在");
}
WmsCoilPendingAction action = new WmsCoilPendingAction();
action.setActionId(actionId);
action.setActionStatus(2); // 已完成
action.setCompleteTime(new Date());
if(StringUtils.isNotBlank(newCoilIds) && !newCoilIds.equals("-")) {
action.setProcessedCoilIds(newCoilIds);
}
// 如果操作人为空,设置当前登录用户为操作人
if (oldAction.getOperatorId() == null || oldAction.getOperatorName() == null) {
try {
action.setOperatorId(LoginHelper.getUserId());
action.setOperatorName(LoginHelper.getUsername());
action.setProcessTime(new Date());
} catch (Exception e) {
// 如果获取登录用户失败,不影响主流程
}
}
return baseMapper.updateById(action) > 0;
}
/**
* 取消操作
*/
@Override
public Boolean cancelAction(Long actionId) {
WmsCoilPendingAction action = new WmsCoilPendingAction();
action.setActionId(actionId);
action.setActionStatus(3); // 已取消
return baseMapper.updateById(action) > 0;
}
/**
* 还原操作(将已删除的记录恢复为正常状态)
*/
@Override
public Boolean restoreAction(Long actionId) {
// 参数校验
if (actionId == null) {
throw new ServiceException("操作ID不能为空");
}
// 使用自定义查询方法检查记录是否存在且del_flag为1(已删除)
WmsCoilPendingAction oldAction = baseMapper.selectByActionIdAndDelFlag(actionId, 2);
if (oldAction == null) {
throw new ServiceException("待操作记录不存在或未被删除");
}
// 使用自定义更新方法更新del_flag绕过@TableLogic注解限制
int rows = baseMapper.updateDelFlag(actionId, 0);
return rows > 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();
}
}