广丰卷烟厂数采质量分析系统
zhuguifei
2026-03-09 25415898ce4e709b22425526b6f30076c663d832
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/service/impl/StoreSilkInfoServiceImpl.java
@@ -102,6 +102,32 @@
            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();
@@ -110,11 +136,18 @@
            String containerNum = siloid.substring(lastIndex + 1);
            if (StringUtils.isEmpty(containerNum)) continue;
            //根据出料开始时间查询喂丝机->储丝柜->机台对应关系(feedmatch_time_data)
            Timestamp targetTime = new Timestamp(distimebegin.getTime() + 10 * 60 * 1000); // 查询出料10分钟后的第一条记录,保证数据准确性
            /**
             * 根据出料开始时间查询“喂丝机 -> 储丝柜 -> 机台(管道)”对应关系(feedmatch_time_data)
             *
             * 说明:
             * - 这里不是直接用 distimebegin,而是取出料开始后 10 分钟的第一条记录,
             *   目的是避开出料刚开始时的波动/映射未稳定问题,保证映射准确性。
             * - 查询上界使用 effectiveDistEnd:如果出料还没结束,就以当前时间作为上界,仍然能取到对应关系。
             */
            Timestamp targetTime = new Timestamp(distimebegin.getTime() + 10 * 60 * 1000); // 出料开始后10分钟
            LambdaQueryWrapper<FeedmatchTimeData> lqw = new LambdaQueryWrapper<>();
            lqw.ge(FeedmatchTimeData::getTime, targetTime)
                    .le(FeedmatchTimeData::getTime, distimeend) // 不能大于出料结束时间
                    .le(FeedmatchTimeData::getTime, effectiveDistEnd) // 不能大于出料结束时间(出料中则使用当前时间)
                    .orderByAsc(FeedmatchTimeData::getTime)
                    .last("LIMIT 1");
            FeedmatchTimeData feedMatch = feedmatchTimeDataMapper.selectOne(lqw);
@@ -123,11 +156,12 @@
                continue;
            }
            // feedMatch 转map  TODO 逆转map需验证key是否会重复
            //fsRevMap是逆转map     key->喂丝机对应的储丝柜号   value-> fs + 序号
            // feedMatch 转map:通过反向映射快速定位“该柜对应哪个喂丝机”、“该机组对应哪个管道”
            // fsRevMap:key=储丝柜号末位(如 1/2/3),value=字段名(如 fs11/fs12...)
            Map<String, String> fsRevMap = new HashMap<>();
            //pipeRevMap是逆转map   key->机组对应的喂丝机和管道  value-> pipe + 序号
            // 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) {
@@ -155,7 +189,11 @@
                // TODO   管道号空返回信息
                continue;
            }
            // List存->  喂丝机对应的机组(如 pipe01 pipe02 代表1#、2#卷接机组)
            /**
             * pipeList 存放“该喂丝机对应的机组字段名”
             * - 例如 pipe01、pipe02 代表 1#、2#卷接/包装机组
             * - 后续会从 pipe01/pipe02 提取机组号 equNo,并用于拼接 key 查询卷接/包装表
             */
            List<String> pipeList = new ArrayList<>();
            for (Map.Entry<String, String> entry : pipeRevMap.entrySet()) {
                //fsNum第三位是喂丝机序号
@@ -168,8 +206,12 @@
                continue;
            }
            // 根据出料开始结束时间,查询该柜料在哪几个班次生产
            List<MdShiftBo> distShiftList = calcShiftSpans(distimebegin, distimeend, mdShifts);
            /**
             * 根据 [distimebegin, effectiveDistEnd] 计算涉及到的班次列表
             * - 出料已结束:effectiveDistEnd=distimeend
             * - 出料未结束:effectiveDistEnd=now
             */
            List<MdShiftBo> distShiftList = calcShiftSpans(distimebegin, effectiveDistEnd, mdShifts);
            storeSilkInfoVo.setDistShiftList(distShiftList);
            if (distShiftList.isEmpty()) continue;
            //查询日期和班次内卷接机组的产量
@@ -189,24 +231,23 @@
                String shift = shiftBo.getCode();
                // 解析班次时间
                // 1. 获取班次配置的开始和结束时间字符串 (格式如 "07:30:00")
                // 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)
                // 2) 补全秒数(如果配置仅为 HH:mm)
                if (stimStr.length() == 5) stimStr += ":00";
                if (etimStr.length() == 5) etimStr += ":00";
                // 3. 结合日期解析为 LocalDateTime
                // 注意:shiftBo.getDay() 是该班次的归属日期
                // 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 天
                // 4) 处理跨天班次:如果 结束时间 <= 开始时间,说明跨天,结束时间 +1 天
                if (!shiftEnd.isAfter(shiftStart)) {
                    shiftEnd = shiftEnd.plusDays(1);
                }
@@ -215,16 +256,43 @@
                Date stimDate = Date.from(shiftStart.atZone(zone).toInstant());
                Date etimDate = Date.from(shiftEnd.atZone(zone).toInstant());
                // 计算班次结束前10分钟的时间点
                String shiftEndStr = shiftEnd.format(formatter);
                String tenMinBeforeShiftEnd = shiftEnd.minusMinutes(10).format(formatter);
                /**
                 * 为了同时支持“出料未结束”,这里引入一个“本班次的统计窗口”:
                 *
                 * 统计结束时刻 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);
@@ -232,7 +300,12 @@
                        channel = "";
                    }
                    //注意4号管道以后对应的机组需要+1,机组4号空缺跳到5号
                    /**
                     * 机组号映射规则(业务约定):
                     * - pipe04 对应的机组号在系统里是空缺的,需要跳到 5 号
                     * - 因此当管道号 >= 4 时,机组号需要 +1
                     * - 同时这里补齐为 2 位数字,便于拼 key(例如 01/02/05...)
                     */
                    try {
                        int equ = Integer.parseInt(equNo);
                        equNo = String.format("%02d", equ >= 4 ? equ + 1 : equ);
@@ -243,18 +316,27 @@
                    }
                    // 拼接卷接机的key,中间 "1" 代表卷接机,格式:班次 + "1" + 机组号
                    // key 拼接规则:
                    // - 卷接机:班次 + "1" + 机组号(例如 101、105...)
                    String key = shift + "1" + equNo;
                    // 拼接包装机的key,中间 "2" 代表包装机,格式:班次 + "2" + 机组号
                    // - 包装机:班次 + "2" + 机组号(例如 201、205...)
                    String packerKey = shift + "2" + equNo;
                    // ================= 卷接机产量统计 =================
                    Double currentRollerOutput = 0.0;
                    // 1. 查询班次结束时的产量快照
                    /**
                     * 取数策略说明(卷接/包装一致):
                     * - 表内的 qty 视为累计值(同一 key 下单调递增)
                     * - 由于数据是采样上报,某个时刻不一定刚好有记录
                     * - 因此采用“目标时刻前 10 分钟内,离目标最近的一条记录”作为快照值
                     *
                     * 注意:10分钟是经验窗口,如果现场采样更稀疏,可考虑放宽窗口
                     */
                    // 1) 查询统计结束时刻 calcEnd 的快照值 Qty(calcEnd)
                    LambdaQueryWrapper<RollerTimeData> rlqw = new LambdaQueryWrapper<>();
                    rlqw.le(RollerTimeData::getTime, shiftEndStr)
                    rlqw.le(RollerTimeData::getTime, calcEndStr)
                            .eq(RollerTimeData::getKey, key)
                            .ge(RollerTimeData::getTime, tenMinBeforeShiftEnd)
                            .ge(RollerTimeData::getTime, tenMinBeforeCalcEnd)
                            .orderByDesc(RollerTimeData::getTime)
                            .isNotNull(RollerTimeData::getQty)
                            .gt(RollerTimeData::getQty, 0)
@@ -262,51 +344,27 @@
                    RollerTimeData rData = rollerTimeDataMapper.selectOne(rlqw);
                    if (rData != null) {
                        // 初始产量设为班次结束时的累计值
                        // 先把统计结束时刻累计值加进来:current = Qty(calcEnd)
                        currentRollerOutput += rData.getQty();
                        // 2. 处理“扣头”:出料开始时间 > 班次开始时间
                        if (distimebegin.after(stimDate)) {
                            LocalDateTime distBeginTime = LocalDateTime.ofInstant(distimebegin.toInstant(), zone);
                            String distBeginStr = distBeginTime.format(formatter);
                            String tenMinBeforeDistBegin = distBeginTime.minusMinutes(10).format(formatter);
                        // 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, distBeginStr)
                                    .eq(RollerTimeData::getKey, key)
                                    .ge(RollerTimeData::getTime, tenMinBeforeDistBegin)
                                    .orderByDesc(RollerTimeData::getTime)
                                    .isNotNull(RollerTimeData::getQty)
                                    .gt(RollerTimeData::getQty, 0)
                                    .last("LIMIT 1");
                            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 = rollerTimeDataMapper.selectOne(beginRlqw);
                            if (rBeginData != null) {
                                currentRollerOutput -= rBeginData.getQty();
                            }
                        }
                        // 3. 处理“扣尾”:出料结束时间 < 班次结束时间
                        if (etimDate.after(distimeend)) {
                            LocalDateTime distEndTime = LocalDateTime.ofInstant(distimeend.toInstant(), zone);
                            String distEndStr = distEndTime.format(formatter);
                            String tenMinBeforeDistEnd = distEndTime.minusMinutes(10).format(formatter);
                            LambdaQueryWrapper<RollerTimeData> endRlqw = new LambdaQueryWrapper<>();
                            endRlqw.le(RollerTimeData::getTime, distEndStr)
                                    .eq(RollerTimeData::getKey, key)
                                    .ge(RollerTimeData::getTime, tenMinBeforeDistEnd)
                                    .orderByDesc(RollerTimeData::getTime)
                                    .isNotNull(RollerTimeData::getQty)
                                    .gt(RollerTimeData::getQty, 0)
                                    .last("LIMIT 1");
                            RollerTimeData rEndData = rollerTimeDataMapper.selectOne(endRlqw);
                            if (rEndData != null) {
                                double qtyDelta = rData.getQty() - rEndData.getQty();
                                if (qtyDelta > 0) {
                                    currentRollerOutput -= qtyDelta;
                                }
                            }
                        }
                    }
@@ -320,71 +378,47 @@
                        detail.setPipeNum(channel);
                        detail.setEquNo(equNo);
                        detail.setShiftCode(shift);
                        detail.setShiftStartTime(stimDate);
                        detail.setShiftEndTime(etimDate);
                        detail.setShiftStartTime(calcStartDate);
                        detail.setShiftEndTime(calcEndDate);
                        detail.setOutput(currentRollerOutput);
                        rollerDetailList.add(detail);
                    }
                    // ================= 包装机产量统计 =================
                    Double currentPackerOutput = 0.0;
                    // 1. 查询班次结束时的产量快照
                    // 1) 查询统计结束时刻 calcEnd 的快照值 Qty(calcEnd)
                    LambdaQueryWrapper<PackerTimeData> plqw = new LambdaQueryWrapper<>();
                    plqw.le(PackerTimeData::getTime, shiftEndStr)
                    plqw.le(PackerTimeData::getTime, calcEndStr)
                            .eq(PackerTimeData::getKey, packerKey)
                            .ge(PackerTimeData::getTime, tenMinBeforeShiftEnd)
                            .ge(PackerTimeData::getTime, tenMinBeforeCalcEnd)
                            .orderByDesc(PackerTimeData::getTime)
                            .isNotNull(PackerTimeData::getQty)
                            .gt(PackerTimeData::getQty, 0)
                            .isNotNull(PackerTimeData::getTsQty)
                            .gt(PackerTimeData::getTsQty, 0)
                            .last("LIMIT 1");
                    PackerTimeData pData = packerTimeDataMapper.selectOne(plqw);
                    if (pData != null) {
                        // 初始产量设为班次结束时的累计值
                        currentPackerOutput += pData.getQty();
                        // 先把统计结束时刻累计值加进来:current = Qty(calcEnd)
                        currentPackerOutput += pData.getTsQty();
                        // 2. 处理“扣头”
                        if (distimebegin.after(stimDate)) {
                            LocalDateTime distBeginTime = LocalDateTime.ofInstant(distimebegin.toInstant(), zone);
                            String distBeginStr = distBeginTime.format(formatter);
                            String tenMinBeforeDistBegin = distBeginTime.minusMinutes(10).format(formatter);
                        // 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, distBeginStr)
                                    .eq(PackerTimeData::getKey, packerKey)
                                    .ge(PackerTimeData::getTime, tenMinBeforeDistBegin)
                                    .orderByDesc(PackerTimeData::getTime)
                                    .isNotNull(PackerTimeData::getQty)
                                    .gt(PackerTimeData::getQty, 0)
                                    .last("LIMIT 1");
                            beginPlqw.le(PackerTimeData::getTime, calcStartStr)
                                .eq(PackerTimeData::getKey, packerKey)
                                .ge(PackerTimeData::getTime, tenMinBeforeCalcStart)
                                .orderByDesc(PackerTimeData::getTime)
                                .isNotNull(PackerTimeData::getTsQty)
                                .gt(PackerTimeData::getTsQty, 0)
                                .last("LIMIT 1");
                            PackerTimeData pBeginData = packerTimeDataMapper.selectOne(beginPlqw);
                            if (pBeginData != null) {
                                currentPackerOutput -= pBeginData.getQty();
                            }
                        }
                        // 3. 处理“扣尾”
                        if (etimDate.after(distimeend)) {
                            LocalDateTime distEndTime = LocalDateTime.ofInstant(distimeend.toInstant(), zone);
                            String distEndStr = distEndTime.format(formatter);
                            String tenMinBeforeDistEnd = distEndTime.minusMinutes(10).format(formatter);
                            LambdaQueryWrapper<PackerTimeData> endPlqw = new LambdaQueryWrapper<>();
                            endPlqw.le(PackerTimeData::getTime, distEndStr)
                                    .eq(PackerTimeData::getKey, packerKey)
                                    .ge(PackerTimeData::getTime, tenMinBeforeDistEnd)
                                    .orderByDesc(PackerTimeData::getTime)
                                    .isNotNull(PackerTimeData::getQty)
                                    .gt(PackerTimeData::getQty, 0)
                                    .last("LIMIT 1");
                            PackerTimeData pEndData = packerTimeDataMapper.selectOne(endPlqw);
                            if (pEndData != null) {
                                double qtyDelta = pData.getQty() - pEndData.getQty();
                                if (qtyDelta > 0) {
                                    currentPackerOutput -= qtyDelta;
                                }
                                currentPackerOutput -= pBeginData.getTsQty();
                            }
                        }
                    }
@@ -398,13 +432,14 @@
                        detail.setPipeNum(channel);
                        detail.setEquNo(equNo);
                        detail.setShiftCode(shift);
                        detail.setShiftStartTime(stimDate);
                        detail.setShiftEndTime(etimDate);
                        detail.setShiftStartTime(calcStartDate);
                        detail.setShiftEndTime(calcEndDate);
                        detail.setOutput(currentPackerOutput);
                        packerDetailList.add(detail);
                    }
                }
            }
            // 将汇总结果与明细结果回写到 StoreSilkInfoVo,供前端列表/抽屉明细展示使用
            storeSilkInfoVo.setRollerOutput(rollerOutput);
            storeSilkInfoVo.setPackerOutput(packerOutput);
            storeSilkInfoVo.setRollerDetailList(rollerDetailList);