|
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<PlanMapper, Plan> implements PlanService {
|
|
RemotePonderation remotePonderation;
|
|
RemoteMaxSize remoteMaxSize;
|
|
@Override
|
public void insertPonderation(Plan plan) {
|
|
//可用车辆
|
Map<String,CarAvgTare> usableCar = new HashMap<>();
|
CustomerUseCar customerUseCar = new CustomerUseCar();
|
|
//车辆平均重量
|
List<CarAvgTare> 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<Ponderation> generateByDailyTarget(Plan plan,
|
Double contractShui,
|
List<String> customerUseCars,
|
List<CarAvgTare> carAvgTares,
|
List<LoadUnloadAvgTime> loadTimes,
|
List<TransitAvgSch> transitTimes,
|
List<LocalDate> validDates) {
|
|
// 基础参数初始化
|
LocalDate planMonth = convertToLocalDate(plan.getMonth());
|
// List<LocalDate> 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<String, CarAvgTare> usableCars = filterUsableCars(carAvgTares, customerUseCars);
|
TransitAvgSch transitTime = getTransitTime(plan, transitTimes);
|
LoadUnloadAvgTime loadTime = getLoadTime(plan, loadTimes);
|
|
// 按日循环生成
|
List<Ponderation> allPonderations = new ArrayList<>();
|
Random random = new Random();
|
double totalWetAccumulated = 0.0; // 总湿煤累计
|
Map<String,List<Ponderation>> 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<JSONObject> 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<Ponderation> dailyPds = new ArrayList<>();
|
double dailyWetAccumulated = 0.0; // 当日湿煤累计
|
Map<String, LocalDateTime> vehicleLastOutTime = new HashMap<>(); // 车辆当日最后出场时间
|
|
while (dailyWetAccumulated < dailyWetTarget) {
|
// 随机选择车辆
|
String vehicleNo = selectRandomVehicle(usableCars, random);
|
List<Ponderation> dayPonderations = ponderations.get(vehicleNo);
|
if (dayPonderations == null){
|
R<List<Ponderation>> 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<JSONObject> 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<Ponderation> 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<LocalDate> getAllDatesOfMonth(LocalDate month) {
|
//
|
// return dates;
|
// }
|
|
private Map<String, CarAvgTare> filterUsableCars(List<CarAvgTare> carAvgTares, List<String> customerUseCars) {
|
List<String> 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<TransitAvgSch> 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<LoadUnloadAvgTime> loadTimes) {
|
return loadTimes.stream()
|
.filter(l -> l.getCoalId().equals(plan.getCoalId()))
|
.findFirst()
|
.orElseThrow(() -> new RuntimeException("未找到匹配的装载时间数据"));
|
}
|
|
private String selectRandomVehicle(Map<String, CarAvgTare> usableCars, Random random) {
|
List<String> 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<Ponderation> 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<String> 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());
|
// };
|
}
|