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.mapper.WmsCoilPendingActionMapper; import com.klp.mapper.WmsMaterialCoilMapper; 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 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 StringRedisTemplate stringRedisTemplate; 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 queryPageList(WmsCoilPendingActionBo bo, PageQuery pageQuery) { QueryWrapper lqw = buildQueryWrapperPlus(bo); Page result = baseMapper.selectVoPagePlus(pageQuery.build(), lqw); List records = result.getRecords(); Set userNames = records.stream() .flatMap(v -> java.util.stream.Stream.of(v.getCreateBy(), v.getOperatorName())) .filter(StringUtils::isNotBlank) .collect(Collectors.toSet()); if (!userNames.isEmpty()) { Map 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 buildQueryWrapperPlus(WmsCoilPendingActionBo bo) { QueryWrapper qw = Wrappers.query(); qw.eq(bo.getCoilId() != null, "wcpa.coil_id", bo.getCoilId()); 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()); // 根据创建人筛选 qw.eq(StringUtils.isNotBlank(bo.getCreateBy()), "wcpa.create_by", bo.getCreateBy()); // 加工后的钢卷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 queryList(WmsCoilPendingActionBo bo) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); return baseMapper.selectVoList(lqw); } @Override public List queryActionIdCoilIdList(WmsCoilPendingActionBo bo) { // 复用与 /list 相同的查询条件(buildQueryWrapperPlus) QueryWrapper lqw = buildQueryWrapperPlus(bo); return baseMapper.selectActionIdCoilIdList(lqw); } private LambdaQueryWrapper buildQueryWrapper(WmsCoilPendingActionBo bo) { LambdaQueryWrapper 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()); } return flag; } /** * 修改钢卷待操作 */ @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 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) { // 先查询原记录,检查操作人是否为空 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 (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 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 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 allCoilIds = new HashSet<>(); for (WmsCoilPendingAction action : actions) { if (action.getCreateTime() == null || action.getCompleteTime() == null) { continue; } if (parseRemarkIds) { List ids = parseIdsFromRemark(action.getRemark()); allCoilIds.addAll(ids); } else { if (action.getCoilId() != null) { allCoilIds.add(action.getCoilId()); } } } Map weightTonMap = new HashMap<>(); if (!allCoilIds.isEmpty()) { List 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 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 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 samplePoints(List 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 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 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 buildLinePoints(List 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 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 parseIdsFromRemark(String remark) { if (StringUtils.isBlank(remark)) { return Collections.emptyList(); } Matcher matcher = Pattern.compile("\\d+").matcher(remark); List 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 linePoints = Collections.emptyList(); } }