| | |
| | | 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; |
| | |
| | | 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业务层处理 |
| | |
| | | * @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; |
| | | |
| | | /** |
| | | * 查询储丝柜产量 |
| | |
| | | * @return 储丝柜产量 |
| | | */ |
| | | @Override |
| | | public StoreSilkInfoVo queryById(Long id){ |
| | | public StoreSilkInfoVo queryById(Long id) { |
| | | return baseMapper.selectVoById(id); |
| | | } |
| | | |
| | |
| | | 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; |
| | | } |
| | | |
| | | /** |
| | |
| | | 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; |
| | | } |
| | | |
| | |
| | | validEntityBeforeSave(add); |
| | | boolean flag = baseMapper.insert(add) > 0; |
| | | if (flag) { |
| | | bo.setId(add.getId()); |
| | | } |
| | | return flag; |
| | | } |
| | |
| | | /** |
| | | * 保存前的数据校验 |
| | | */ |
| | | private void validEntityBeforeSave(StoreSilkInfo entity){ |
| | | private void validEntityBeforeSave(StoreSilkInfo entity) { |
| | | //TODO 做一些数据校验,如唯一约束 |
| | | } |
| | | |
| | |
| | | */ |
| | | @Override |
| | | public Boolean deleteWithValidByIds(Collection<Long> ids, Boolean isValid) { |
| | | if(isValid){ |
| | | if (isValid) { |
| | | //TODO 做一些业务上的校验,判断是否需要校验 |
| | | } |
| | | return baseMapper.deleteByIds(ids) > 0; |