广丰卷烟厂数采质量分析系统
zhuguifei
2026-03-06 b12c9e77a6b6a7b410ac421c5a3d68da88823460
RuoYi-Vue-Plus/ruoyi-modules/ruoyi-qa/src/main/java/org/dromara/qa/analy/service/impl/StoreSilkInfoServiceImpl.java
@@ -1,5 +1,8 @@
package org.dromara.qa.analy.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.BeanUtils;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.dromara.common.core.utils.MapstructUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.mybatis.core.page.TableDataInfo;
@@ -9,6 +12,15 @@
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.mapper.RollerTimeDataMapper;
import org.dromara.qa.md.domain.MdShift;
import org.dromara.qa.md.domain.bo.MdShiftBo;
import org.dromara.qa.md.mapper.MdShiftMapper;
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;
@@ -17,9 +29,16 @@
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 java.lang.reflect.Field;
import java.sql.Timestamp;
import java.text.ParseException;
import java.text.SimpleDateFormat;
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业务层处理
@@ -34,6 +53,10 @@
    private final StoreSilkInfoMapper baseMapper;
    private final FeedmatchTimeDataMapper feedmatchTimeDataMapper;
    private final RollerTimeDataMapper rollerTimeDataMapper;
    private final org.dromara.qa.analy.mapper.PackerTimeDataMapper packerTimeDataMapper;
    private final MdShiftMapper mdShiftMapper;
    private final OracleShiftReader oracleShiftReader;
    /**
     * 查询储丝柜产量
@@ -66,14 +89,421 @@
        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();
            //储丝柜柜号
            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)
            Timestamp targetTime = new Timestamp(distimebegin.getTime() + 10 * 60 * 1000); // 查询出料10分钟后的第一条记录,保证数据准确性
            LambdaQueryWrapper<FeedmatchTimeData> lqw = new LambdaQueryWrapper<>();
            lqw.ge(FeedmatchTimeData::getTime, targetTime)
                    .le(FeedmatchTimeData::getTime, distimeend) // 不能大于出料结束时间
                    .orderByAsc(FeedmatchTimeData::getTime)
                    .last("LIMIT 1");
            FeedmatchTimeData feedMatch = feedmatchTimeDataMapper.selectOne(lqw);
            if (feedMatch == null) {
                // TODO  添加提示
                continue;
            }
            // feedMatch 转map  TODO 逆转map需验证key是否会重复
            //fsRevMap是逆转map     key->喂丝机对应的储丝柜号   value-> fs + 序号
            Map<String, String> fsRevMap = new HashMap<>();
            //pipeRevMap是逆转map   key->机组对应的喂丝机和管道  value-> pipe + 序号
            Map<String, String> pipeRevMap = new HashMap<>();
            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) {
                    fsRevMap.put(value.toString(), field.getName());
                } else if (field.getName().startsWith("pipe") && value != null) {
                    pipeRevMap.put(value.toString(), field.getName());
                    pipeMap.put(field.getName(),value.toString());
                }
            }
            // 根据储丝柜号获取喂丝机号
            String fsNum = fsRevMap.get(containerNum);
            if (StringUtils.isEmpty(fsNum)) {
                // TODO   喂丝机号空返回信息
                continue;
            }
            if (pipeRevMap.isEmpty()) {
                // TODO   管道号空返回信息
                continue;
            }
            // List存->  喂丝机对应的机组(如 pipe01 pipe02 代表1#、2#卷接机组)
            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;
            }
            // 根据出料开始结束时间,查询该柜料在哪几个班次生产
            List<MdShiftBo> distShiftList = calcShiftSpans(distimebegin, distimeend, 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<>();
            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. 获取班次配置的开始和结束时间字符串 (格式如 "07:30:00")
                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. 结合日期解析为 LocalDateTime
                // 注意:shiftBo.getDay() 是该班次的归属日期
                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());
                // 计算班次结束前10分钟的时间点
                String shiftEndStr = shiftEnd.format(formatter);
                String tenMinBeforeShiftEnd = shiftEnd.minusMinutes(10).format(formatter);
                // 根据卷接机组和班次获取产量(pipeList = 多少个机组)
                for (int j = 0; j < pipeList.size(); j++) {
                    String pipe = pipeList.get(j);
                    // 提取机组号
                    String equNo = pipe.replaceAll("\\D+", "");
                    //管道号
                    String channel = pipeMap.get("pipe" + equNo);
                    if (channel != null && channel.length() > 0) {
                        channel = channel.substring(channel.length() - 1);
                    }else {
                        channel = "";
                    }
                    //注意4号管道以后对应的机组需要+1,机组4号空缺跳到5号
                    try {
                        int equ = Integer.parseInt(equNo);
                        equNo = String.format("%02d", equ >= 4 ? equ + 1 : equ);
                    } catch (Exception e) {
                        e.printStackTrace();
                        // TODO 添加提示
                        continue;
                    }
                    // 拼接卷接机的key,中间 "1" 代表卷接机,格式:班次 + "1" + 机组号
                    String key = shift + "1" + equNo;
                    // 拼接包装机的key,中间 "2" 代表包装机,格式:班次 + "2" + 机组号
                    String packerKey = shift + "2" + equNo;
                    // ================= 卷接机产量统计 =================
                    Double currentRollerOutput = 0.0;
                    // 1. 查询班次结束时的产量快照
                    LambdaQueryWrapper<RollerTimeData> rlqw = new LambdaQueryWrapper<>();
                    rlqw.le(RollerTimeData::getTime, shiftEndStr)
                            .eq(RollerTimeData::getKey, key)
                            .ge(RollerTimeData::getTime, tenMinBeforeShiftEnd)
                            .orderByDesc(RollerTimeData::getTime)
                            .isNotNull(RollerTimeData::getQty)
                            .gt(RollerTimeData::getQty, 0)
                            .last("LIMIT 1");
                    RollerTimeData rData = rollerTimeDataMapper.selectOne(rlqw);
                    if (rData != null) {
                        // 初始产量设为班次结束时的累计值
                        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);
                            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");
                            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;
                                }
                            }
                        }
                    }
                    // 累计总产量
                    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(stimDate);
                        detail.setShiftEndTime(etimDate);
                        detail.setOutput(currentRollerOutput);
                        rollerDetailList.add(detail);
                    }
                    // ================= 包装机产量统计 =================
                    Double currentPackerOutput = 0.0;
                    // 1. 查询班次结束时的产量快照
                    LambdaQueryWrapper<PackerTimeData> plqw = new LambdaQueryWrapper<>();
                    plqw.le(PackerTimeData::getTime, shiftEndStr)
                            .eq(PackerTimeData::getKey, packerKey)
                            .ge(PackerTimeData::getTime, tenMinBeforeShiftEnd)
                            .orderByDesc(PackerTimeData::getTime)
                            .isNotNull(PackerTimeData::getQty)
                            .gt(PackerTimeData::getQty, 0)
                            .last("LIMIT 1");
                    PackerTimeData pData = packerTimeDataMapper.selectOne(plqw);
                    if (pData != null) {
                        // 初始产量设为班次结束时的累计值
                        currentPackerOutput += pData.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);
                            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");
                            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;
                                }
                            }
                        }
                    }
                    // 累计总产量
                    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(stimDate);
                        detail.setShiftEndTime(etimDate);
                        detail.setOutput(currentPackerOutput);
                        packerDetailList.add(detail);
                    }
                }
            }
            storeSilkInfoVo.setRollerOutput(rollerOutput);
            storeSilkInfoVo.setPackerOutput(packerOutput);
            storeSilkInfoVo.setRollerDetailList(rollerDetailList);
            storeSilkInfoVo.setPackerDetailList(packerDetailList);
        }
    }
    /**
     * 计算出料区间 [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;
    }
    /**
@@ -98,6 +528,8 @@
        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());
        lqw.between(params.get("beginTime") != null && params.get("endTime") != null,
                StoreSilkInfo::getDistimeend, params.get("beginTime"), params.get("endTime"));
        return lqw;
    }