package com.by4cloud.platform.processing.service.impl; import cn.hutool.json.JSONObject; import com.alibaba.excel.util.CollectionUtils; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.by4cloud.platform.common.core.util.R; import com.by4cloud.platform.processing.entity.*; import com.by4cloud.platform.processing.mapper.PlanMapper; import com.by4cloud.platform.processing.service.PlanService; import com.by4cloud.platform.yunxiao.api.feign.RemoteMaxSize; import com.by4cloud.platform.yunxiao.api.feign.RemotePonderation; import com.by4cloud.platform.yunxiao.constant.MaxSizeContant; import com.by4cloud.platform.yunxiao.entity.ContractOrder; import com.by4cloud.platform.yunxiao.entity.Ponderation; import com.by4cloud.platform.yunxiao.utils.NumUtils; import lombok.AllArgsConstructor; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.text.DateFormat; import java.text.ParseException; import java.time.*; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; import java.time.temporal.ChronoUnit; import java.time.temporal.TemporalAdjusters; import java.util.*; import java.util.stream.Collectors; import static com.by4cloud.platform.yunxiao.utils.AvgTimeUtils.isValidDateTimeWithDateTimeFormatter; /** * 计划表 * * @author zzl * @date 2025-10-17 09:47:25 */ @Service @AllArgsConstructor public class PlanServiceImpl extends ServiceImpl implements PlanService { RemotePonderation remotePonderation; RemoteMaxSize remoteMaxSize; @Override public void insertPonderation(Plan plan) { //可用车辆 Map usableCar = new HashMap<>(); CustomerUseCar customerUseCar = new CustomerUseCar(); //车辆平均重量 List carAvgTares = new ArrayList<>(); //历史服务过的包含车辆的才可用 carAvgTares.forEach(e -> { if (customerUseCar.getUseCars().contains(e.getVehicleNo())){ usableCar.put(e.getVehicleNo(),e); } }); } /** * 按每日目标湿煤量循环生成磅单 * @param plan 调整计划(含总干煤量) * @param contractShui 合同水分(%,如8d) * @param customerUseCars 客户历史车辆 * @param carAvgTares 车辆皮重数据 * @param loadTimes 装载时间数据 * @param transitTimes 运输时间数据 * @param * @return 生成的磅单列表 */ public List generateByDailyTarget(Plan plan, Double contractShui, List customerUseCars, List carAvgTares, List loadTimes, List transitTimes, List validDates) { // 基础参数初始化 LocalDate planMonth = convertToLocalDate(plan.getMonth()); // List validDates = getAllDatesOfMonth(planMonth); // if (holidays != null) validDates.removeAll(holidays); int validDays = validDates.size(); double totalDryTarget = plan.getQuantity(); // 总干煤目标 double dailyDryTarget = NumUtils.divideDouble(totalDryTarget,(double)validDays,3); // 每日干煤目标 // 筛选可用车辆和基础时间数据 Map usableCars = filterUsableCars(carAvgTares, customerUseCars); TransitAvgSch transitTime = getTransitTime(plan, transitTimes); LoadUnloadAvgTime loadTime = getLoadTime(plan, loadTimes); // 按日循环生成 List allPonderations = new ArrayList<>(); Random random = new Random(); double totalWetAccumulated = 0.0; // 总湿煤累计 Map> ponderations = new HashMap<>(); for (LocalDate date : validDates) { System.out.println("===== 处理日期:" + date + " ====="); ZonedDateTime utcZonedDateTime = date.atStartOfDay(ZoneId.of("UTC")); Date utcDate = Date.from(utcZonedDateTime.toInstant()); // 计算当日湿煤目标 R r = remotePonderation.getDayCheck(plan.getFyCompId(),plan.getCoalId(),plan.getCustomerId(),utcDate); JSONObject object = r.getData(); Double realShui = 0d; if(object == null) continue; realShui = (Double) object.get("mt"); Integer checkId = (Integer) object.get("checkId"); if (realShui == null) continue; double dailyWetTarget = calculateDailyWetTarget(dailyDryTarget, realShui, contractShui); // 当日湿煤目标 System.out.println("当日干煤目标:" + dailyDryTarget + "吨,实际水分:" + realShui + "%,湿煤目标:" + dailyWetTarget + "吨"); // 当日磅单生成 List dailyPds = new ArrayList<>(); double dailyWetAccumulated = 0.0; // 当日湿煤累计 Map vehicleLastOutTime = new HashMap<>(); // 车辆当日最后出场时间 while (dailyWetAccumulated < dailyWetTarget) { // 随机选择车辆 String vehicleNo = selectRandomVehicle(usableCars, random); List dayPonderations = ponderations.get(vehicleNo); if (dayPonderations == null){ R> p = remotePonderation.getPonderationByDay(utcDate,plan.getFyCompId(),plan.getFiledId(),vehicleNo); if (p.getCode() == 0){ ponderations.put(vehicleNo,p.getData()); }else { ponderations.put(vehicleNo,new ArrayList<>()); } } CarAvgTare car = usableCars.get(vehicleNo); String dateKey = vehicleNo + date; // 3.2.2 计算入场/出场时间 LocalDateTime[] times = getTimes(ponderations.get(vehicleNo),date,vehicleNo,loadTime.getAvgTime(), transitTime.getAvgTime(),random); if (times == null) continue; LocalDateTime inTime = times[0].withSecond(random.nextInt(60)); LocalDateTime outTime = times[1].withSecond(random.nextInt(60)); R times1 = remotePonderation.verifyTimes(inTime,outTime,plan.getFiledId(),plan.getFyCompId()); String firstMan = null; String secondMan = null; if (times1.getCode()==0){ JSONObject object1 = times1.getData(); if (object1 == null)continue; String time1 = object1.get("inTime").toString(); String time2 = object1.get("outTime").toString(); inTime = stringToLocalDateTime(time1); outTime = stringToLocalDateTime(time2); firstMan = object1.get("firstMan").toString(); secondMan = object1.get("secondMan").toString(); } // 生成单车载湿煤量 double tare = generateTare(car, random); // 皮重 double wetNet = generateWetNet(tare, random); // 湿煤净重 double gross = NumUtils.addDouble(tare,wetNet); // 毛重 // 3.2.4 构建磅单 Ponderation pd = buildPonderation(plan, vehicleNo, inTime, outTime, tare, gross, wetNet,firstMan,secondMan,checkId); ponderations.get(vehicleNo).add(pd); dailyPds.add(pd); // 3.2.5 更新累计 dailyWetAccumulated = NumUtils.addDouble(dailyWetAccumulated,wetNet,2); vehicleLastOutTime.put(dateKey, outTime); //检查是否接近目标 if (dailyWetAccumulated >= dailyWetTarget - 2) { break; } } // 调整当日最后一车 // adjustLastPonderation(dailyPds, dailyWetAccumulated, dailyWetTarget, realShui); // 汇总当日数据 allPonderations.addAll(dailyPds); totalWetAccumulated = NumUtils.addDouble(totalWetAccumulated,dailyWetTarget,2); // 按目标值累计 System.out.println("当日生成磅单:" + dailyPds.size() + "条,实际湿煤量:" + dailyWetTarget + "吨\n"); } // 校验总湿煤量 double totalDryActual = NumUtils.accurateDouble(totalWetAccumulated * (100 - contractShui) / 100,2); // 按合同水分反算总干煤 // System.out.println("总干煤目标:" + totalDryTarget + "吨,实际总干煤:" + totalDryActual + "吨"); return allPonderations; } // 核心工具方法:计算当日湿煤目标 private double calculateDailyWetTarget(double dailyDryTarget, double realShui, double contractShui) { // 湿煤目标 = 干煤目标 × (100 - 合同水分) / (100 - 实际水分) if (realShui <= contractShui) return dailyDryTarget; return NumUtils.accurateDouble(dailyDryTarget * (100 - contractShui) / (100 - realShui), 2); } // 工具方法:调整当日最后一车重量,确保累计达标 private void adjustLastPonderation(List dailyPds, double currentTotal, double target, double realShui) { if (dailyPds.isEmpty()) return; double diff = target - currentTotal; if (Math.abs(diff) <= 0.001) return; // 已达标 // 调整最后一车的湿净重和毛重 Ponderation last = dailyPds.get(dailyPds.size() - 1); double newWetNet = last.getExecutive() + diff; last.setExecutive(NumUtils.accurateDouble(newWetNet, 3)); // 湿净重 last.setSecWeight(NumUtils.accurateDouble(last.getFirWeight() + newWetNet, 3)); // 毛重 // 更新备注(记录调整信息) last.setRemark(last.getRemark() + ",调整量:" + NumUtils.accurateDouble(diff, 3) + "吨(确保当日达标)"); } // 以下为辅助方法(与之前逻辑一致,确保完整性) private LocalDate convertToLocalDate(Date date) { return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); } // private List getAllDatesOfMonth(LocalDate month) { // // return dates; // } private Map filterUsableCars(List carAvgTares, List customerUseCars) { List cars = carAvgTares.stream().map(e -> e.getVehicleNo()).collect(Collectors.toList()); return carAvgTares.stream() .filter(car -> customerUseCars.contains(car.getVehicleNo())) .collect(Collectors.toMap(CarAvgTare::getVehicleNo, car -> car)); } private TransitAvgSch getTransitTime(Plan plan, List transitTimes) { return transitTimes.stream() .filter(t -> t.getFyCompId().equals(plan.getFyCompId()) && t.getCustomerAddressId().equals(plan.getCustomerAddressId()) && t.getTransitType().equals(1)) .findFirst() .orElseThrow(() -> new RuntimeException("未找到匹配的运输时间数据")); } private LoadUnloadAvgTime getLoadTime(Plan plan, List loadTimes) { return loadTimes.stream() .filter(l -> l.getCoalId().equals(plan.getCoalId())) .findFirst() .orElseThrow(() -> new RuntimeException("未找到匹配的装载时间数据")); } private String selectRandomVehicle(Map usableCars, Random random) { List vehicleNos = new ArrayList<>(usableCars.keySet()); return vehicleNos.get(random.nextInt(vehicleNos.size())); } //入场时间计算 private LocalDateTime calculateInTime(String vehicleNo, LocalDate date, int avgTransitTime, LocalDateTime lastOutTime, Random random) { //如果今天这一车没有发运记录 if (lastOutTime == null) { int hour = 8; int minute = (hour == 17) ? random.nextInt(31) : random.nextInt(60); return LocalDateTime.of(date, LocalTime.of(hour, minute)); } else { //有的话这个时间 + 平均时间 int transitTime = avgTransitTime + (random.nextInt(10) - 5); LocalDateTime inTime = lastOutTime.plusMinutes(transitTime); return inTime; } } /** * * @param inTime 入场时间 * @param avgLoadTime 平均装卸时间 * @return */ private LocalDateTime calculateOutTime(LocalDateTime inTime, int avgLoadTime,Random random) { //装卸时间 + 随机生成 +- 3分钟 int loadTime = avgLoadTime + (random.nextInt(7) - 3); LocalDateTime outTime = inTime.plusMinutes(loadTime); //若大于 下班时间 // if (outTime.toLocalTime().isAfter(LocalTime.of(18, 0))) { // outTime = outTime.with(LocalTime.of(18, 0)); // } return outTime; } private LocalDateTime[] getTimes(List ponderations,LocalDate day,String vehicleNo, int avgLoadTime,int transitAvgTime,Random random) { //出场和下一次入场所需要平均多少分钟 ponderations.sort(Comparator.comparing(Ponderation::getCreateTime)); int zuiduanM = avgLoadTime + transitAvgTime; if (ponderations.size() == 0){ LocalDateTime inTime = this.calculateInTime(vehicleNo,day,transitAvgTime,null,random); LocalDateTime ouTime = this.calculateOutTime(inTime,avgLoadTime,random); return new LocalDateTime[]{inTime,ouTime}; } for (int i = 0; i < ponderations.size(); i++) { Ponderation ponderation = ponderations.get(i); if (i == ponderations.size()-1){ //和当天上班时间对比 long c = getMinutesDifference(day.atTime(8,0,0),ponderation.getCreateTime()); //可以插入数据 if (c>zuiduanM){ LocalDateTime inTime = this.calculateInTime(vehicleNo,day,transitAvgTime,day.atTime(8,0,0),random); LocalDateTime ouTime = this.calculateOutTime(inTime,avgLoadTime,random); return new LocalDateTime[]{inTime,ouTime}; }else{ //和下班时间对比 long o = getMinutesDifference(ponderation.getOutTime(),day.atTime(18,0,0)); //可以插入数据 if (o>zuiduanM){ // LocalDateTime inTime = this.calculateInTime(vehicleNo,day,transitAvgTime,LocalDateTime.ofInstant(ponderation.getOutTime().toInstant(), ZoneId.systemDefault()),random); LocalDateTime ouTime = this.calculateOutTime(inTime,avgLoadTime,random); return new LocalDateTime[]{inTime,ouTime}; } } return null; }else{ Ponderation ponderation1 = ponderations.get(i+1); long c = getMinutesDifference(ponderation.getOutTime(),ponderation1.getCreateTime()); LocalDateTime inTime = this.calculateInTime(vehicleNo,day,transitAvgTime,day.atTime(8,0,0),random); LocalDateTime ouTime = this.calculateOutTime(inTime,avgLoadTime,random); return new LocalDateTime[]{inTime,ouTime}; } } return null; } /** * 随机皮重 * @param car 车辆级皮重 * @param random * @return */ private double generateTare(CarAvgTare car, Random random) { // +- 0.02 double base = 0.02 + random.nextDouble() * 0.02; // 生成 0.02~0.04 之间的随机数 int sign = random.nextBoolean() ? 1 : -1; // 随机确定正负 double d = base * sign; //随机结果 double tare = NumUtils.addDouble(car.getAvgTare(),d,2); return tare; } /** * 随机毛重 * @param tare 皮重 * @param random * @return */ private double generateWetNet(double tare, Random random) { double base = 49 - tare - 0.5; return NumUtils.accurateDouble(base + (random.nextDouble() * 0.4 - 0.2), 2); } private boolean isInServiceTime(LocalDateTime time) { LocalTime t = time.toLocalTime(); return t.isAfter(LocalTime.of(7, 59)) && t.isBefore(LocalTime.of(18, 1)); } private Ponderation buildPonderation(Plan plan, String vehicleNo, LocalDateTime inTime, LocalDateTime outTime, double tare, double gross, double wetNet,String firstMan,String secondMan,Integer checkId) { Ponderation pd = new Ponderation(); pd.setCompId(plan.getFyCompId());//矿 pd.setVehicleNo(vehicleNo);//车牌号 pd.setFirWeight(tare);//皮重 pd.setSecWeight(gross);//毛重 pd.setExecutive(wetNet);//净重 pd.setCreateTime(inTime);//入场时间 R r = remoteMaxSize.nextNo(MaxSizeContant.WX_BD_NUM,plan.getFyCompId()); pd.setNumber(r.getData()); pd.setRefId(checkId);//检查id // pd.setInTime(localDateTimeToDate());//入场时间 pd.setFirstMan(firstMan);//一次称重司磅员 pd.setSecondMan(secondMan);//二次称重司磅员 pd.setOutTime(Date.from(outTime.atZone(ZoneId.systemDefault()).toInstant()));//出厂时间 pd.setFiledId(plan.getFiledId());//煤场 pd.setCustomerId(plan.getCustomerId());//客户 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm"); pd.setPrintTime(outTime.format(formatter)); pd.setCoalId(plan.getCoalId());//产品 pd.setFromCompId(plan.getFyCompId()); pd.setPonderationType("外销"); return pd; } private double getDailyRealShui(Integer coalId, LocalDate date) { return 0d; } /** * 根据折干量计算湿煤量 * @param dryQuantity 折干量(已知) * @param realShui 实际水分(%,如10d) * @param contractShui 合同水分(%,如8d) * @return 湿煤量(保留3位小数) */ public static Double getWetQuantityByDry(Double dryQuantity, Double realShui, Double contractShui) { // 校验异常值 if (realShui >= 100) { throw new IllegalArgumentException("实际水分不能大于等于100%"); } // 校验折干量不为空 if (dryQuantity == null || dryQuantity <= 0) { throw new IllegalArgumentException("折干量必须为正数"); } // 计算分母:100 - 实际水分 double denominator = 100 - realShui; // 计算分子:100 - 合同水分 double numerator = 100 - contractShui; // 湿煤量 = 折干量 × (分子 / 分母),保留3位小数 return NumUtils.accurateDouble(dryQuantity * (numerator / denominator), 3); } /** * 两个时间相差多少分钟 * @param * @param * @return * @throws ParseException */ public static long getMinutesDifference(String datetime1, String datetime2){ if (!isValidDateTimeWithDateTimeFormatter(datetime1) || !isValidDateTimeWithDateTimeFormatter(datetime2)) { return 0; } // 定义匹配"yyyy-MM-dd HH:mm:ss"格式的解析器 DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); // 解析字符串为LocalDateTime对象 LocalDateTime time1 = LocalDateTime.parse(datetime1, formatter); LocalDateTime time2 = LocalDateTime.parse(datetime2, formatter); // 计算分钟差并返回绝对值 return Math.abs(ChronoUnit.MINUTES.between(time1, time2)); } /** * 计算 Date 和 LocalDateTime 的分钟差 * @param date 日期对象(含时区信息) * @param localDateTime 本地日期时间(需结合默认时区转换) * @return 分钟差的绝对值 */ public static long getMinutesDifference(Date date, LocalDateTime localDateTime) { if (date == null || localDateTime == null) { return 0; // 空值直接返回0 } // Date 转 LocalDateTime(使用系统默认时区,可根据需求修改) LocalDateTime dateTime1 = LocalDateTime.ofInstant(date.toInstant(), ZoneId.systemDefault()); // 计算分钟差 return Math.abs(ChronoUnit.MINUTES.between(dateTime1, localDateTime)); } /** * 计算两个 Date 的分钟差 * @param date1 第一个日期 * @param date2 第二个日期 * @return 分钟差的绝对值 */ public static long getMinutesDifference(Date date1, Date date2) { if (date1 == null || date2 == null) { return 0; // 空值直接返回0 } // 转换为毫秒差后计算分钟(1分钟=60000毫秒) long diffMs = Math.abs(date1.getTime() - date2.getTime()); return diffMs / 60000; } /** * 计算 Date 和 LocalDateTime 的分钟差 * @param date 日期对象(含时区信息) * @param localDateTime 本地日期时间(需结合默认时区转换) * @return 分钟差的绝对值 */ public static long getMinutesDifference(LocalDateTime date, LocalDateTime localDateTime) { // 计算分钟差 return Math.abs(ChronoUnit.MINUTES.between(date, localDateTime)); } /** * localDate转换成Date * @param localDateTime * @return */ public static Date localDateTimeToDate(LocalDateTime localDateTime) { if (localDateTime == null) { return null; } // 使用系统默认时区(ZoneId.systemDefault()) return Date.from(localDateTime.atZone(ZoneId.systemDefault()).toInstant()); } /** * 将字符串转换为LocalDateTime * @param dateTimeStr 待转换的日期时间字符串 * @param * @return 转换后的LocalDateTime */ public static LocalDateTime stringToLocalDateTime(String dateTimeStr) { String pattern = "yyyy-MM-dd HH:mm:ss"; if (dateTimeStr == null) { return null; } try { // 创建指定格式的DateTimeFormatter DateTimeFormatter formatter = DateTimeFormatter.ofPattern(pattern); // 解析字符串为LocalDateTime return LocalDateTime.parse(dateTimeStr, formatter); } catch (DateTimeParseException e) { // 格式不匹配时抛出异常,这里捕获并返回null System.err.println("字符串格式与指定模式不匹配:" + e.getMessage()); return null; } } // public static void genOrderByPonList(Integer orderId,Plan plan){ // ContractOrder order = new ContractOrder(); // order.setCreateCompId(); // order.setCompId(plan.getFyCompId()); // order.setCustomerId(plan.getCustomerId()); // order.setCustomerAddressId(plan.getCustomerAddressId()); // order.setNumber(remoteMaxSize.nextNo(MaxSizeContant.WX_ORDER_NUM,plan.getFyCompId()).getData()); // }; }