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 org.dromara.common.mybatis.core.page.PageQuery; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; 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.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 IFeedmatchTimeDataService feedmatchTimeDataService; private final IRollerTimeDataService rollerTimeDataService; private final IPackerTimeDataService packerTimeDataService; private final OracleShiftReader oracleShiftReader; /** * 查询储丝柜产量 * * @param id 主键 * @return 储丝柜产量 */ @Override public StoreSilkInfoVo queryById(Long id) { return baseMapper.selectVoById(id); } /** * 分页查询储丝柜产量列表 * * @param bo 查询条件 * @param pageQuery 分页参数 * @return 储丝柜产量分页列表 */ @Override public TableDataInfo queryPageList(StoreSilkInfoBo bo, PageQuery pageQuery) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); Page result = baseMapper.selectVoPage(pageQuery.build(), lqw); queryFeedmatchData(result); return TableDataInfo.build(result); } private void queryFeedmatchData(Page page) { if (page == null || page.getRecords() == null || page.getRecords().isEmpty()) { return; } // 查询数采系统班次时间(独立使用 oracle 数据源) List mdShifts = oracleShiftReader.listAll(); List 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 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 fsRevMap = new HashMap<>(); // pipeRevMap:key=喂丝机号+管道组合(如 1x / 2x...),value=字段名(如 pipe01/pipe02...) Map pipeRevMap = new HashMap<>(); // pipeMap:key=字段名(pipe01/pipe02...),value=字段值(用于后续解析具体管道号) Map 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 pipeList = new ArrayList<>(); for (Map.Entry 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 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 rollerDetailList = new ArrayList<>(); List packerDetailList = new ArrayList<>(); // 操作记录列表(存所有增减过程) List rollerRecordList = new ArrayList<>(); List 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 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 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 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 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,表示该日期上的该班次与出料区间存在时间重叠 * 若同一日期早/中班均有重叠,value 使用逗号拼接,如 "1,2" *

* 简化思路: * 1. 将 begin/end 转为 LocalDateTime,取出覆盖的所有自然日 * 2. 对每一天按班次的 stim/etim 生成班次时间窗 [shiftStart, shiftEnd) * - 若 etim <= stim,视为跨天班次:shiftEnd = 当天日期 + 1 天 + etim * 3. 用简单的区间重叠判断:shiftEnd > begin && shiftStart < end */ private List calcShiftSpans(Date begin, Date end, List mdShifts) { List 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 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; } /** * 查询符合条件的储丝柜产量列表 * * @param bo 查询条件 * @return 储丝柜产量列表 */ @Override public List queryList(StoreSilkInfoBo bo) { LambdaQueryWrapper lqw = buildQueryWrapper(bo); return baseMapper.selectVoList(lqw); } private LambdaQueryWrapper buildQueryWrapper(StoreSilkInfoBo bo) { Map params = bo.getParams(); LambdaQueryWrapper lqw = Wrappers.lambdaQuery(); lqw.like(StringUtils.isNotBlank(bo.getMaterialname()), StoreSilkInfo::getMaterialname, bo.getMaterialname()); 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()); 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; } /** * 新增储丝柜产量 * * @param bo 储丝柜产量 * @return 是否新增成功 */ @Override public Boolean insertByBo(StoreSilkInfoBo bo) { StoreSilkInfo add = MapstructUtils.convert(bo, StoreSilkInfo.class); validEntityBeforeSave(add); boolean flag = baseMapper.insert(add) > 0; if (flag) { } return flag; } /** * 修改储丝柜产量 * * @param bo 储丝柜产量 * @return 是否修改成功 */ @Override public Boolean updateByBo(StoreSilkInfoBo bo) { StoreSilkInfo update = MapstructUtils.convert(bo, StoreSilkInfo.class); validEntityBeforeSave(update); return baseMapper.updateById(update) > 0; } /** * 保存前的数据校验 */ private void validEntityBeforeSave(StoreSilkInfo entity) { //TODO 做一些数据校验,如唯一约束 } /** * 校验并批量删除储丝柜产量信息 * * @param ids 待删除的主键集合 * @param isValid 是否进行有效性校验 * @return 是否删除成功 */ @Override public Boolean deleteWithValidByIds(Collection ids, Boolean isValid) { if (isValid) { //TODO 做一些业务上的校验,判断是否需要校验 } return baseMapper.deleteByIds(ids) > 0; } }