广丰卷烟厂数采质量分析系统
zhuguifei
7 天以前 2b31fa203f3435a582be51f45899d99164c9917a
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/service/impl/StoreSilkInfoServiceImpl.java
@@ -1,5 +1,6 @@
package org.dromara.qa.analy.service.impl;
import com.baomidou.dynamic.datasource.annotation.DS;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.TableDataInfo;
@@ -9,17 +10,30 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.dromara.qa.analy.domain.FeedmatchTimeData;
import org.dromara.qa.analy.domain.PackerTimeData;
import org.dromara.qa.analy.domain.RollerTimeData;
import org.dromara.qa.analy.domain.vo.StoreSilkDetailVo;
import org.dromara.qa.analy.service.IRollerTimeDataService;
import org.dromara.qa.analy.service.IPackerTimeDataService;
import org.dromara.qa.md.domain.MdShift;
import org.dromara.qa.md.domain.bo.MdShiftBo;
import org.dromara.qa.md.service.OracleShiftReader;
import org.springframework.stereotype.Service;
import org.dromara.qa.analy.domain.bo.StoreSilkInfoBo;
import org.dromara.qa.analy.domain.vo.StoreSilkInfoVo;
import org.dromara.qa.analy.domain.StoreSilkInfo;
import org.dromara.qa.analy.mapper.StoreSilkInfoMapper;
import org.dromara.qa.analy.service.IStoreSilkInfoService;
import org.dromara.qa.analy.mapper.FeedmatchTimeDataMapper;
import java.util.List;
import java.util.Map;
import java.util.Collection;
import org.dromara.qa.analy.service.IFeedmatchTimeDataService;
import java.lang.reflect.Field;
import java.sql.Timestamp;
import java.util.*;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
/**
 * 储丝柜产量Service业务层处理
@@ -27,13 +41,17 @@
 * @author zhuguifei
 * @date 2026-03-02
 */
@DS("oracle_zs")
@Slf4j
@RequiredArgsConstructor
@Service
public class StoreSilkInfoServiceImpl implements IStoreSilkInfoService {
    private final StoreSilkInfoMapper baseMapper;
    private final FeedmatchTimeDataMapper feedmatchTimeDataMapper;
    private final IFeedmatchTimeDataService feedmatchTimeDataService;
    private final IRollerTimeDataService rollerTimeDataService;
    private final IPackerTimeDataService packerTimeDataService;
    private final OracleShiftReader oracleShiftReader;
    /**
     * 查询储丝柜产量
@@ -42,7 +60,7 @@
     * @return 储丝柜产量
     */
    @Override
    public StoreSilkInfoVo queryById(Long id){
    public StoreSilkInfoVo queryById(Long id) {
        return baseMapper.selectVoById(id);
    }
@@ -61,19 +79,557 @@
        queryFeedmatchData(result);
        return TableDataInfo.build(result);
    }
    private void queryFeedmatchData(Page<StoreSilkInfoVo> page) {
        if (page == null || page.getRecords() == null || page.getRecords().isEmpty()) {
            return;
        }
        // 查询数采系统班次时间(独立使用 oracle 数据源)
        List<MdShift> mdShifts = oracleShiftReader.listAll();
        List<StoreSilkInfoVo> storeSilkInfoList = page.getRecords();
        for (int i = 0; i < storeSilkInfoList.size(); i++) {
            //
            //储丝柜
            StoreSilkInfoVo storeSilkInfoVo = storeSilkInfoList.get(i);
            //出料开始时间
            Date distimebegin = storeSilkInfoVo.getDistimebegin();
            //出料结束时间
            Date distimeend = storeSilkInfoVo.getDistimeend();
            if (distimebegin == null) {
                continue;
            }
            /**
             * 统计“出料进行中”的关键点:
             * - 出料结束时间 distimeend 可能为空(仍在出料中)
             * - 也可能已经有结束时间(出料已结束)
             *
             * 因此这里统一计算一个“本次统计的有效结束时间” effectiveDistEnd:
             * 1) distimeend == null :说明还在出料中,本次统计截止到当前时间 now
             * 2) distimeend != null :说明已出料结束,截止到 distimeend
             * 3) 若 distimeend 在未来(脏数据/时钟误差),也按 now 截止,避免统计到未来
             *
             * 后续所有查询、班次切分、扣减都基于 effectiveDistEnd,
             * 这样同一套逻辑同时适配“出料已结束”和“出料未结束”两种场景。
             */
            Date now = new Date();
            Date effectiveDistEnd;
            if (distimeend == null) {
                effectiveDistEnd = now;
            } else {
                effectiveDistEnd = distimeend.after(now) ? now : distimeend;
            }
            if (!effectiveDistEnd.after(distimebegin)) {
                continue;
            }
            //储丝柜柜号
            String siloid = storeSilkInfoVo.getSiloid();
            if (StringUtils.isEmpty(siloid)) continue;
            int lastIndex = siloid.lastIndexOf("_");
            String containerNum = siloid.substring(lastIndex + 1);
            if (StringUtils.isEmpty(containerNum)) continue;
            /**
             * 根据出料开始时间查询“喂丝机 -> 储丝柜 -> 机台(管道)”对应关系(feedmatch_time_data)
             *
             * 说明:
             * - 这里不是直接用 distimebegin,而是取出料开始后 10 分钟的第一条记录,
             *   目的是避开出料刚开始时的波动/映射未稳定问题,保证映射准确性。
             * - 查询上界使用 effectiveDistEnd:如果出料还没结束,就以当前时间作为上界,仍然能取到对应关系。
             */
            Timestamp targetTime = new Timestamp(distimebegin.getTime() + 10 * 60 * 1000); // 出料开始后10分钟
            String containerStr = StringUtils.isEmpty(containerNum) ? "" : containerNum.trim();
            //小于10的柜号补0
            if (containerStr.length() == 1) {
                containerStr = "0" + containerStr;
            }
            LambdaQueryWrapper<FeedmatchTimeData> lqw = new LambdaQueryWrapper<>();
            String finalContainerStr = containerStr;
            lqw.ge(FeedmatchTimeData::getTime, targetTime)
                    .le(FeedmatchTimeData::getTime, effectiveDistEnd)
                    .and(wrapper -> wrapper
                            .like(FeedmatchTimeData::getFs11, finalContainerStr)
                            .or()
                            .like(FeedmatchTimeData::getFs12, finalContainerStr)
                            .or()
                            .like(FeedmatchTimeData::getFs21, finalContainerStr)
                            .or()
                            .like(FeedmatchTimeData::getFs22, finalContainerStr)
                            .or()
                            .like(FeedmatchTimeData::getFs31, finalContainerStr)
                            .or()
                            .like(FeedmatchTimeData::getFs32, finalContainerStr)
                            .or()
                            .like(FeedmatchTimeData::getFs41, finalContainerStr)
                            .or()
                            .like(FeedmatchTimeData::getFs42, finalContainerStr)
                    )
                    .orderByAsc(FeedmatchTimeData::getTime)
                    .last("LIMIT 1");
            FeedmatchTimeData feedMatch = feedmatchTimeDataService.selectOne(lqw);
            if (feedMatch == null) {
                // TODO  添加提示
                continue;
            }
            // feedMatch 转map:通过反向映射快速定位“该柜对应哪个喂丝机”、“该机组对应哪个管道”
            // fsRevMap:key=储丝柜号后两位(如 01/09),value=字段名(如 fs11/fs12...)
            Map<String, String> fsRevMap = new HashMap<>();
            // pipeRevMap:key=喂丝机号+管道组合(如 1x / 2x...),value=字段名(如 pipe01/pipe02...)
            Map<String, String> pipeRevMap = new HashMap<>();
            // pipeMap:key=字段名(pipe01/pipe02...),value=字段值(用于后续解析具体管道号)
            Map<String, String> pipeMap = new HashMap<>();
            Field[] fields = feedMatch.getClass().getDeclaredFields();
            for (Field field : fields) {
                field.setAccessible(true);
                Object value = null;
                try {
                    value = field.get(feedMatch);
                } catch (IllegalAccessException e) {
                    throw new RuntimeException(e);
                }
                if (field.getName().startsWith("fs") && value != null) {
                    String key = value.toString().trim();
                    if (key.length() == 1) {
                        key = "0" + key;
                    } else if (key.length() > 2) {
                        key = key.substring(key.length() - 2);
                    }
                    fsRevMap.put(key, field.getName());
                } else if (field.getName().startsWith("pipe") && value != null) {
                    pipeRevMap.put(value.toString(), field.getName());
                    pipeMap.put(field.getName(),value.toString());
                }
            }
            // 根据储丝柜号获取喂丝机号
            String containerKey = StringUtils.isEmpty(containerNum) ? "" : containerNum.trim();
            if (containerKey.length() == 1) {
                containerKey = "0" + containerKey;
            } else if (containerKey.length() > 2) {
                containerKey = containerKey.substring(containerKey.length() - 2);
            }
            String fsNum = fsRevMap.get(containerKey);
            if (StringUtils.isEmpty(fsNum)) {
                // TODO   喂丝机号空返回信息
                continue;
            }
            if (pipeRevMap.isEmpty()) {
                // TODO   管道号空返回信息
                continue;
            }
            /**
             * pipeList 存放“该喂丝机对应的机组字段名”
             * - 例如 pipe01、pipe02 代表 1#、2#卷接/包装机组
             * - 后续会从 pipe01/pipe02 提取机组号 equNo,并用于拼接 key 查询卷接/包装表
             */
            List<String> pipeList = new ArrayList<>();
            for (Map.Entry<String, String> entry : pipeRevMap.entrySet()) {
                //fsNum第三位是喂丝机序号
                if (entry.getKey().length() > 1 && entry.getKey().startsWith(fsNum.substring(2, 3))) {
                    pipeList.add(entry.getValue());
                }
            }
            if (pipeList.isEmpty()) {
                //TODO 添加提示
                continue;
            }
            /**
             * 根据 [distimebegin, effectiveDistEnd] 计算涉及到的班次列表
             * - 出料已结束:effectiveDistEnd=distimeend
             * - 出料未结束:effectiveDistEnd=now
             */
            List<MdShiftBo> distShiftList = calcShiftSpans(distimebegin, effectiveDistEnd, mdShifts);
            storeSilkInfoVo.setDistShiftList(distShiftList);
            if (distShiftList.isEmpty()) continue;
            //查询日期和班次内卷接机组的产量
            ZoneId zone = ZoneId.systemDefault();
            // 卷包产量统计
            Double rollerOutput = 0.0;
            Double packerOutput = 0.0;
            // 明细列表(仅存最终正数结果)
            List<StoreSilkDetailVo> rollerDetailList = new ArrayList<>();
            List<StoreSilkDetailVo> packerDetailList = new ArrayList<>();
            // 操作记录列表(存所有增减过程)
            List<StoreSilkDetailVo> rollerRecordList = new ArrayList<>();
            List<StoreSilkDetailVo> packerRecordList = new ArrayList<>();
            for (int s = 0; s < distShiftList.size(); s++) {
                MdShiftBo shiftBo = distShiftList.get(s);
                if (shiftBo.getDay() == null || StringUtils.isEmpty(shiftBo.getCode())) {
                    continue;
                }
                String shift = shiftBo.getCode();
                // 解析班次时间
                // 1) 获取班次配置的开始/结束时间 (可能是 "HH:mm" 或 "HH:mm:ss")
                String stimStr = shiftBo.getStim();
                String etimStr = shiftBo.getEtim();
                if (StringUtils.isEmpty(stimStr) || StringUtils.isEmpty(etimStr)) {
                    continue;
                }
                // 2) 补全秒数(如果配置仅为 HH:mm)
                if (stimStr.length() == 5) stimStr += ":00";
                if (etimStr.length() == 5) etimStr += ":00";
                // 3) 结合日期(shiftBo.getDay() 为班次归属日)解析为 LocalDateTime
                String dateStr = shiftBo.getDay().toInstant().atZone(zone).toLocalDate().toString();
                DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
                LocalDateTime shiftStart = LocalDateTime.parse(dateStr + " " + stimStr, formatter);
                LocalDateTime shiftEnd = LocalDateTime.parse(dateStr + " " + etimStr, formatter);
                // 4) 处理跨天班次:如果 结束时间 <= 开始时间,说明跨天,结束时间 +1 天
                if (!shiftEnd.isAfter(shiftStart)) {
                    shiftEnd = shiftEnd.plusDays(1);
                }
                // 转换 Date 对象用于比较(兼容现有逻辑)
                Date stimDate = Date.from(shiftStart.atZone(zone).toInstant());
                Date etimDate = Date.from(shiftEnd.atZone(zone).toInstant());
                /**
                 * 为了同时支持“出料未结束”,这里引入一个“本班次的统计窗口”:
                 *
                 * 统计结束时刻 calcEnd = min(班次结束, effectiveDistEnd)
                 * - 出料已结束:effectiveDistEnd=distimeend,可能早于班次结束,calcEnd=distimeend
                 * - 出料未结束:effectiveDistEnd=now,calcEnd=now 或 班次结束(取较小)
                 *
                 * 统计开始时刻 calcStart = max(班次开始, distimebegin)
                 * - 若出料开始晚于班次开始,则从出料开始统计
                 * - 若出料开始早于班次开始,则从班次开始统计
                 *
                 * 最终本班次有效产量口径为:
                 *   output = Qty(calcEnd) - Qty(calcStart)
                 *
                 * 说明:这个口径已经“隐含了扣尾”
                 * - 如果 calcEnd < 班次结束(例如出料已结束或统计到当前时刻),尾巴自然不会被统计进去
                 * - 因此不需要再额外写一个“扣尾 if(...)”分支,逻辑更统一、更不容易出错
                 */
                LocalDateTime effectiveEndLdt = LocalDateTime.ofInstant(effectiveDistEnd.toInstant(), zone);
                LocalDateTime calcEnd = shiftEnd.isBefore(effectiveEndLdt) ? shiftEnd : effectiveEndLdt;
                Date calcEndDate = Date.from(calcEnd.atZone(zone).toInstant());
                if (!calcEndDate.after(stimDate)) {
                    continue;
                }
                Date calcStartDate = distimebegin.after(stimDate) ? distimebegin : stimDate;
                if (!calcEndDate.after(calcStartDate)) {
                    continue;
                }
                String calcEndStr = calcEnd.format(formatter);
                String tenMinBeforeCalcEnd = calcEnd.minusMinutes(10).format(formatter);
                // 根据卷接机组和班次获取产量(pipeList = 多少个机组)
                for (int j = 0; j < pipeList.size(); j++) {
                    String pipe = pipeList.get(j);
                    // 提取机组号
                    String equNo = pipe.replaceAll("\\D+", "");
                    // 管道号(用于明细展示):从 feedMatch 的 pipe01/pipe02... 字段值里取末位
                    String channel = pipeMap.get("pipe" + equNo);
                    if (channel != null && channel.length() > 0) {
                        channel = channel.substring(channel.length() - 1);
                    }else {
                        channel = "";
                    }
                    /**
                     * 机组号映射规则(业务约定):
                     * - pipe04 对应的机组号在系统里是空缺的,需要跳到 5 号
                     * - 因此当管道号 >= 4 时,机组号需要 +1
                     * - 同时这里补齐为 2 位数字,便于拼 key(例如 01/02/05...)
                     */
                    try {
                        int equ = Integer.parseInt(equNo);
                        equNo = String.format("%02d", equ >= 4 ? equ + 1 : equ);
                    } catch (Exception e) {
                        e.printStackTrace();
                        // TODO 添加提示
                        continue;
                    }
                    // key 拼接规则:
                    // - 卷接机:班次 + "1" + 机组号(例如 101、105...)
                    String key = shift + "1" + equNo;
                    // - 包装机:班次 + "2" + 机组号(例如 201、205...)
                    String packerKey = shift + "2" + equNo;
                    // ================= 卷接机产量统计 =================
                    Double currentRollerOutput = 0.0;
                    /**
                     * 取数策略说明(卷接/包装一致):
                     * - 表内的 qty 视为累计值(同一 key 下单调递增)
                     * - 由于数据是采样上报,某个时刻不一定刚好有记录
                     * - 因此采用“目标时刻前 10 分钟内,离目标最近的一条记录”作为快照值
                     *
                     * 注意:10分钟是经验窗口,如果现场采样更稀疏,可考虑放宽窗口
                     */
                    // 1) 查询统计结束时刻 calcEnd 的快照值 Qty(calcEnd)
                    LambdaQueryWrapper<RollerTimeData> rlqw = new LambdaQueryWrapper<>();
                    rlqw.le(RollerTimeData::getTime, calcEndStr)
                            .eq(RollerTimeData::getKey, key)
                            .ge(RollerTimeData::getTime, tenMinBeforeCalcEnd)
                            .orderByDesc(RollerTimeData::getTime)
                            .isNotNull(RollerTimeData::getQty)
                            .gt(RollerTimeData::getQty, 0)
                            .last("LIMIT 1");
                    RollerTimeData rData = rollerTimeDataService.selectOne(rlqw);
                    if (rData != null) {
                        // 先把统计结束时刻累计值加进来:current = Qty(calcEnd)
                        currentRollerOutput += rData.getQty();
                        // 记录过程:班次截止累计
                        StoreSilkDetailVo endRecord = new StoreSilkDetailVo();
                        endRecord.setFsNum(fsNum.substring(2, 3));
                        endRecord.setSiloNum(containerNum);
                        endRecord.setPipeNum(channel);
                        endRecord.setEquNo(equNo);
                        endRecord.setShiftCode(shift);
                        endRecord.setShiftStartTime(calcStartDate);
                        endRecord.setShiftEndTime(calcEndDate);
                        endRecord.setOutput(rData.getQty());
                        endRecord.setCalcType("班次截止累计");
                        endRecord.setHitTime(rData.getTime());
                        rollerRecordList.add(endRecord);
                        // 2) 扣“头”:如果统计开始时刻晚于班次开始,则减去 Qty(calcStart)
                        if (calcStartDate.after(stimDate)) {
                            LocalDateTime calcStartLdt = LocalDateTime.ofInstant(calcStartDate.toInstant(), zone);
                            String calcStartStr = calcStartLdt.format(formatter);
                            String tenMinBeforeCalcStart = calcStartLdt.minusMinutes(10).format(formatter);
                            LambdaQueryWrapper<RollerTimeData> beginRlqw = new LambdaQueryWrapper<>();
                            beginRlqw.le(RollerTimeData::getTime, calcStartStr)
                                .eq(RollerTimeData::getKey, key)
                                .ge(RollerTimeData::getTime, tenMinBeforeCalcStart)
                                .orderByDesc(RollerTimeData::getTime)
                                .isNotNull(RollerTimeData::getQty)
                                .gt(RollerTimeData::getQty, 0)
                                .last("LIMIT 1");
                            RollerTimeData rBeginData = rollerTimeDataService.selectOne(beginRlqw);
                            if (rBeginData != null) {
                                currentRollerOutput -= rBeginData.getQty();
                                // 记录过程:扣除头部产量
                                StoreSilkDetailVo beginRecord = new StoreSilkDetailVo();
                                beginRecord.setFsNum(fsNum.substring(2, 3));
                                beginRecord.setSiloNum(containerNum);
                                beginRecord.setPipeNum(channel);
                                beginRecord.setEquNo(equNo);
                                beginRecord.setShiftCode(shift);
                                beginRecord.setShiftStartTime(calcStartDate);
                                beginRecord.setShiftEndTime(calcEndDate);
                                beginRecord.setOutput(-rBeginData.getQty()); // 负数表示扣除
                                beginRecord.setCalcType("扣除出料前累计");
                                beginRecord.setHitTime(rBeginData.getTime());
                                rollerRecordList.add(beginRecord);
                            }
                        }
                    }
                    // 累计总产量
                    rollerOutput += currentRollerOutput;
                    // 记录卷接机明细
                    if (currentRollerOutput > 0) {
                        StoreSilkDetailVo detail = new StoreSilkDetailVo();
                        detail.setFsNum(fsNum.substring(2, 3));
                        detail.setSiloNum(containerNum);
                        detail.setPipeNum(channel);
                        detail.setEquNo(equNo);
                        detail.setShiftCode(shift);
                        detail.setShiftStartTime(calcStartDate);
                        detail.setShiftEndTime(calcEndDate);
                        detail.setOutput(currentRollerOutput);
                        rollerDetailList.add(detail);
                    }
                    // ================= 包装机产量统计 =================
                    Double currentPackerOutput = 0.0;
                    // 1) 查询统计结束时刻 calcEnd 的快照值 Qty(calcEnd)
                    LambdaQueryWrapper<PackerTimeData> plqw = new LambdaQueryWrapper<>();
                    plqw.le(PackerTimeData::getTime, calcEndStr)
                            .eq(PackerTimeData::getKey, packerKey)
                            .ge(PackerTimeData::getTime, tenMinBeforeCalcEnd)
                            .orderByDesc(PackerTimeData::getTime)
                            .isNotNull(PackerTimeData::getQty)
                            .gt(PackerTimeData::getQty, 0)
                            .last("LIMIT 1");
                    PackerTimeData pData = packerTimeDataService.selectOne(plqw);
                    if (pData != null) {
                        // 先把统计结束时刻累计值加进来:current = Qty(calcEnd)
                        currentPackerOutput += pData.getQty();
                        // 记录过程:班次截止累计
                        StoreSilkDetailVo endRecord = new StoreSilkDetailVo();
                        endRecord.setFsNum(fsNum.substring(2, 3));
                        endRecord.setSiloNum(containerNum);
                        endRecord.setPipeNum(channel);
                        endRecord.setEquNo(equNo);
                        endRecord.setShiftCode(shift);
                        endRecord.setShiftStartTime(calcStartDate);
                        endRecord.setShiftEndTime(calcEndDate);
                        endRecord.setOutput(pData.getQty());
                        endRecord.setCalcType("班次截止累计");
                        endRecord.setHitTime(pData.getTime());
                        packerRecordList.add(endRecord);
                        // 2) 扣“头”:如果统计开始时刻晚于班次开始,则减去 Qty(calcStart)
                        if (calcStartDate.after(stimDate)) {
                            LocalDateTime calcStartLdt = LocalDateTime.ofInstant(calcStartDate.toInstant(), zone);
                            String calcStartStr = calcStartLdt.format(formatter);
                            String tenMinBeforeCalcStart = calcStartLdt.minusMinutes(10).format(formatter);
                            LambdaQueryWrapper<PackerTimeData> beginPlqw = new LambdaQueryWrapper<>();
                            beginPlqw.le(PackerTimeData::getTime, calcStartStr)
                                .eq(PackerTimeData::getKey, packerKey)
                                .ge(PackerTimeData::getTime, tenMinBeforeCalcStart)
                                .orderByDesc(PackerTimeData::getTime)
                                .isNotNull(PackerTimeData::getQty)
                                .gt(PackerTimeData::getQty, 0)
                                .last("LIMIT 1");
                            PackerTimeData pBeginData = packerTimeDataService.selectOne(beginPlqw);
                            if (pBeginData != null) {
                                currentPackerOutput -= pBeginData.getQty();
                                // 记录过程:扣除头部产量
                                StoreSilkDetailVo beginRecord = new StoreSilkDetailVo();
                                beginRecord.setFsNum(fsNum.substring(2, 3));
                                beginRecord.setSiloNum(containerNum);
                                beginRecord.setPipeNum(channel);
                                beginRecord.setEquNo(equNo);
                                beginRecord.setShiftCode(shift);
                                beginRecord.setShiftStartTime(calcStartDate);
                                beginRecord.setShiftEndTime(calcEndDate);
                                beginRecord.setOutput(-pBeginData.getQty()); // 负数表示扣除
                                beginRecord.setCalcType("扣除出料前累计");
                                beginRecord.setHitTime(pBeginData.getTime());
                                packerRecordList.add(beginRecord);
                            }
                        }
                    }
                    // 累计总产量
                    packerOutput += currentPackerOutput;
                    // 记录包装机明细
                    if (currentPackerOutput > 0) {
                        StoreSilkDetailVo detail = new StoreSilkDetailVo();
                        detail.setFsNum(fsNum.substring(2, 3));
                        detail.setSiloNum(containerNum);
                        detail.setPipeNum(channel);
                        detail.setEquNo(equNo);
                        detail.setShiftCode(shift);
                        detail.setShiftStartTime(calcStartDate);
                        detail.setShiftEndTime(calcEndDate);
                        detail.setOutput(currentPackerOutput);
                        packerDetailList.add(detail);
                    }
                }
            }
            // 将汇总结果与明细结果回写到 StoreSilkInfoVo,供前端列表/抽屉明细展示使用
            storeSilkInfoVo.setRollerOutput(rollerOutput);
            storeSilkInfoVo.setPackerOutput(packerOutput);
            storeSilkInfoVo.setRollerDetailList(rollerDetailList);
            storeSilkInfoVo.setPackerDetailList(packerDetailList);
            storeSilkInfoVo.setRollerRecordList(rollerRecordList);
            storeSilkInfoVo.setPackerRecordList(packerRecordList);
        }
    }
    /**
     * 计算出料区间 [begin, end) 涉及到的班次(仅 code=1、2 的早/中班)
     * 返回 Map<yyyy-MM-dd, code>,表示该日期上的该班次与出料区间存在时间重叠
     * 若同一日期早/中班均有重叠,value 使用逗号拼接,如 "1,2"
     * <p>
     * 简化思路:
     * 1. 将 begin/end 转为 LocalDateTime,取出覆盖的所有自然日
     * 2. 对每一天按班次的 stim/etim 生成班次时间窗 [shiftStart, shiftEnd)
     * - 若 etim <= stim,视为跨天班次:shiftEnd = 当天日期 + 1 天 + etim
     * 3. 用简单的区间重叠判断:shiftEnd > begin && shiftStart < end
     */
    private List<MdShiftBo> calcShiftSpans(Date begin, Date end, List<MdShift> mdShifts) {
        List<MdShiftBo> result = new ArrayList<>();
        // 基础校验
        if (begin == null || end == null || !end.after(begin) || mdShifts == null || mdShifts.isEmpty()) {
            return result;
        }
        // 仅保留早班(code=1)与中班(code=2)
        // 同时忽略 stim/etim 为空的班次
        ZoneId zone = ZoneId.systemDefault();
        LocalDateTime intervalStart = LocalDateTime.ofInstant(begin.toInstant(), zone);
        LocalDateTime intervalEnd = LocalDateTime.ofInstant(end.toInstant(), zone);
        LocalDate day = intervalStart.toLocalDate();
        LocalDate lastDay = intervalEnd.toLocalDate();
        // 解析时间格式:支持 "H:mm" 或 "H:mm:ss"
        DateTimeFormatter tf = DateTimeFormatter.ofPattern("H:mm[:ss]");
        Set<String> seen = new HashSet<>();
        // 按天遍历覆盖范围
        while (!day.isAfter(lastDay)) {
            for (MdShift s : mdShifts) {
                // 仅早/中班
                String code = s.getCode();
                if (!"1".equals(code) && !"2".equals(code)) {
                    continue;
                }
                String st = s.getStim();
                String et = s.getEtim();
                if (st == null || et == null) {
                    continue;
                }
                // 解析班次起止时间
                LocalTime stt;
                LocalTime ett;
                try {
                    stt = LocalTime.parse(st.trim(), tf);
                    ett = LocalTime.parse(et.trim(), tf);
                } catch (Exception ignore) {
                    // 时间格式异常则跳过该班次
                    continue;
                }
                // 生成当天该班次的时间窗
                LocalDateTime shiftStart = LocalDateTime.of(day, stt);
                LocalDateTime shiftEnd = ett.isAfter(stt) || ett.equals(stt)
                        ? LocalDateTime.of(day, ett)
                        : LocalDateTime.of(day.plusDays(1), ett); // 跨天处理
                // 判断区间是否重叠: [shiftStart, shiftEnd) 与 [intervalStart, intervalEnd)
                if (shiftEnd.isAfter(intervalStart) && shiftStart.isBefore(intervalEnd)) {
                    Date shiftDay = Date.from(day.atStartOfDay(zone).toInstant());
                    String dedupeKey = shiftDay.getTime() + "-" + code;
                    if (seen.add(dedupeKey)) {
                        MdShiftBo bo = new MdShiftBo();
                        bo.setId(s.getId());
                        bo.setWsId(s.getWsId());
                        bo.setCode(s.getCode());
                        bo.setName(s.getName());
                        bo.setStim(s.getStim());
                        bo.setEtim(s.getEtim());
                        bo.setSeq(s.getSeq());
                        bo.setEnable(s.getEnable());
                        bo.setDel(s.getDel());
                        bo.setCreateUserName(s.getCreateUserName());
                        bo.setCreateUserTime(s.getCreateUserTime());
                        bo.setUpdateUserName(s.getUpdateUserName());
                        bo.setUpdateUserTime(s.getUpdateUserTime());
                        bo.setDay(shiftDay);
                        result.add(bo);
                    }
                }
            }
            day = day.plusDays(1);
        }
        result.sort(Comparator
                .comparing(MdShiftBo::getDay, Comparator.nullsLast(Date::compareTo))
                .thenComparing(MdShiftBo::getCode, Comparator.nullsLast(String::compareTo)));
        return result;
    }
    /**
@@ -91,13 +647,50 @@
    private LambdaQueryWrapper<StoreSilkInfo> buildQueryWrapper(StoreSilkInfoBo bo) {
        Map<String, Object> params = bo.getParams();
        LambdaQueryWrapper<StoreSilkInfo> lqw = Wrappers.lambdaQuery();
        lqw.orderByAsc(StoreSilkInfo::getId);
        lqw.like(StringUtils.isNotBlank(bo.getMaterialname()), StoreSilkInfo::getMaterialname, bo.getMaterialname());
        lqw.eq(StringUtils.isNotBlank(bo.getBatchcode()), StoreSilkInfo::getBatchcode, bo.getBatchcode());
        lqw.eq(bo.getActualstarttime() != null, StoreSilkInfo::getActualstarttime, bo.getActualstarttime());
        lqw.like(StringUtils.isNotBlank(bo.getBatchcode()), StoreSilkInfo::getBatchcode, bo.getBatchcode());
        if (bo.getActualstarttime() != null) {
            ZoneId zone = ZoneId.systemDefault();
            LocalDate day = bo.getActualstarttime().toInstant().atZone(zone).toLocalDate();
            Date dayStart = Date.from(day.atStartOfDay(zone).toInstant());
            Date nextDayStart = Date.from(day.plusDays(1).atStartOfDay(zone).toInstant());
            lqw.ge(StoreSilkInfo::getActualstarttime, dayStart);
            lqw.lt(StoreSilkInfo::getActualstarttime, nextDayStart);
        }
        lqw.eq(bo.getDistimebegin() != null, StoreSilkInfo::getDistimebegin, bo.getDistimebegin());
        lqw.eq(bo.getDistimeend() != null, StoreSilkInfo::getDistimeend, bo.getDistimeend());
        lqw.eq(StringUtils.isNotBlank(bo.getSiloid()), StoreSilkInfo::getSiloid, bo.getSiloid());
        if (StringUtils.isNotBlank(bo.getSiloid())) {
            // 支持多个柜号查询,以逗号分隔
            String[] siloids = bo.getSiloid().split(",");
            lqw.and(wrapper -> {
                for (String val : siloids) {
                    val = val.trim();
                    if (StringUtils.isBlank(val)) {
                        continue;
                    }
                    String finalVal = val;
                    // 使用 OR 连接多个柜号条件
                    wrapper.or(w -> {
                        try {
                            int num = Integer.parseInt(finalVal);
                            String padded = String.format("%02d", num);
                            // 匹配 _1 或 _01 (兼容不补零和补零的情况)
                            w.likeLeft(StoreSilkInfo::getSiloid, "_" + num)
                                    .or()
                                    .likeLeft(StoreSilkInfo::getSiloid, "_" + padded);
                        } catch (NumberFormatException e) {
                            // 非数字则按原值匹配
                            w.eq(StoreSilkInfo::getSiloid, finalVal);
                        }
                    });
                }
            });
        }
        if (params.get("beginTime") != null && params.get("endTime") != null) {
            lqw.apply("distimeend BETWEEN TO_DATE({0}, 'YYYY-MM-DD HH24:MI:SS') AND TO_DATE({1}, 'YYYY-MM-DD HH24:MI:SS')",
                    params.get("beginTime"),
                    params.get("endTime"));
        }
        return lqw;
    }
@@ -113,7 +706,6 @@
        validEntityBeforeSave(add);
        boolean flag = baseMapper.insert(add) > 0;
        if (flag) {
            bo.setId(add.getId());
        }
        return flag;
    }
@@ -134,7 +726,7 @@
    /**
     * 保存前的数据校验
     */
    private void validEntityBeforeSave(StoreSilkInfo entity){
    private void validEntityBeforeSave(StoreSilkInfo entity) {
        //TODO 做一些数据校验,如唯一约束
    }
@@ -147,7 +739,7 @@
     */
    @Override
    public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) {
        if(isValid){
        if (isValid) {
            //TODO 做一些业务上的校验,判断是否需要校验
        }
        return baseMapper.deleteByIds(ids) > 0;