Commit 3d3915bf authored by DESKTOP-VKRD9QF\Administration's avatar DESKTOP-VKRD9QF\Administration

Merge remote-tracking branch 'origin/codex/jit-backward-changeover'

parents 23645062 19a80d12
......@@ -18,6 +18,8 @@ public class ScheduleResultDetail {
private double Quantity; // 时间段
private double efficiency=1;//效率
private List<TimeSegment> usedSegment;
// true 表示换型段,false 表示加工段。
private boolean setup;
// Key 的 getter/setter
......
package com.aps.service.Algorithm;
import com.aps.common.util.FileHelper;
import com.aps.common.util.SpringContextUtil;
import com.aps.entity.Algorithm.GAScheduleResult;
import com.aps.entity.Algorithm.ScheduleResultDetail;
import com.aps.entity.basic.Entry;
import com.aps.entity.basic.GlobalParam;
import com.aps.entity.basic.Machine;
import com.aps.entity.basic.SegmentType;
import com.aps.entity.basic.TimeSegment;
import com.aps.service.DiscreteParameterDurationService;
import com.aps.service.DiscreteParameterMatrixService;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.stream.Collectors;
/**
* 倒排换型辅助类:集中处理 JIT 倒排时绿色/蓝色/黄色之间的换型计算、窗口校验和黄色旧换型替换。
*/
class BackwardChangeoverHelper {
private static final String DEBUG_BACKWARD_SETUP_TIME_SECONDS_PROPERTY = "aps.debug.backwardSetupTimeSeconds";
private static final String DEBUG_BACKWARD_CHANGEOVER_VERBOSE_PROPERTY = "aps.debug.backwardChangeoverVerbose";
private final GlobalParam globalParam;
private final LocalDateTime baseTime;
private final MachineCalculator machineCalculator;
private final boolean verboseLogging;
BackwardChangeoverHelper(GlobalParam globalParam, LocalDateTime baseTime, MachineCalculator machineCalculator) {
this.globalParam = globalParam;
this.baseTime = baseTime;
this.machineCalculator = machineCalculator;
this.verboseLogging = Boolean.parseBoolean(System.getProperty(DEBUG_BACKWARD_CHANGEOVER_VERBOSE_PROPERTY, "false"));
}
boolean isVerboseLoggingEnabled() {
return verboseLogging;
}
void writeVerboseLog(String message) {
if (verboseLogging) {
FileHelper.writeLogFile(message);
}
}
private String convertTime(int second) {
return baseTime.plusSeconds(second).format(DateTimeFormatter.ofPattern("YYYY-MM-dd HH:mm"));
}
private boolean isBackwardInsufficientTimeException(RuntimeException ex) {
return ex instanceof IllegalStateException
&& ex.getMessage() != null
&& ex.getMessage().contains("\u8bbe\u5907\u53ef\u7528\u65f6\u95f4\u4e0d\u8db3");
}
// ==================== 倒排换型辅助方法开始 ====================
GAScheduleResult getNextMachineTaskForBackward(CopyOnWriteArrayList<GAScheduleResult> machineTasks, int processEndTime) {
if (machineTasks == null || machineTasks.isEmpty()) {
return null;
}
return machineTasks.stream()
.filter(t -> getProcessStartTime(t) >= processEndTime)
.min(Comparator.comparingInt(this::getProcessStartTime))
.orElse(null);
}
GAScheduleResult getPrevMachineTaskForBackward(CopyOnWriteArrayList<GAScheduleResult> machineTasks, int processStartTime) {
if (machineTasks == null || machineTasks.isEmpty()) {
return null;
}
return machineTasks.stream()
.filter(t -> getProcessEndTime(t) <= processStartTime)
.max(Comparator.comparingInt(this::getProcessEndTime))
.orElse(null);
}
int getProcessStartTime(GAScheduleResult result) {
if (result == null) {
return 0;
}
return getProcessStartTime(result.getGeneDetails(), result.getStartTime());
}
int getProcessEndTime(GAScheduleResult result) {
if (result == null) {
return 0;
}
return getProcessEndTime(result.getGeneDetails(), result.getEndTime());
}
int getProcessStartTime(List<ScheduleResultDetail> details, int fallback) {
if (details == null || details.isEmpty()) {
return fallback;
}
return details.stream()
.filter(detail -> !detail.isSetup())
.mapToInt(ScheduleResultDetail::getStartTime)
.min()
.orElse(fallback);
}
int getProcessEndTime(List<ScheduleResultDetail> details, int fallback) {
if (details == null || details.isEmpty()) {
return fallback;
}
return details.stream()
.filter(detail -> !detail.isSetup())
.mapToInt(ScheduleResultDetail::getEndTime)
.max()
.orElse(fallback);
}
JitBackwardScheduleResult scheduleJitBackwardWithChangeover(Machine machine,
Entry operation,
int latestEndTime,
int processingTimeTotal,
double processingTime,
CopyOnWriteArrayList<GAScheduleResult> machineTasks,
List<Entry> allOperations) {
// 倒排换型主流程:
// 1. 先不带换型试排当前工序,定位它后面的相邻任务;
// 2. 计算“当前 -> 后任务”的后置换型,把当前加工结束时间往前压;
// 3. 再计算“前任务 -> 当前”的前置换型,校验整块窗口能否插入;
// 4. 如果倒排已经压过基准时间或设备可用时间不足,交给外层整单正排兜底。
boolean backwardScheduled = false;
boolean forwardFallbackRequired = false;
int backwardLatestEndTime = latestEndTime;
int backwardAttemptLimit = Math.max(1, (machineTasks == null ? 0 : machineTasks.size()) + 1);
int setupTime = 0;
CopyOnWriteArrayList<ScheduleResultDetail> geneDetails = new CopyOnWriteArrayList<>();
int baselineTime = 0;
for (int backwardAttempt = 0; backwardAttempt < backwardAttemptLimit; backwardAttempt++) {
CopyOnWriteArrayList<TimeSegment> emptySetupSegments = new CopyOnWriteArrayList<>();
Map<Integer,Object> previewResult;
try {
// 试排只用于拿当前加工段位置;试排会临时占用日历,拿到位置后马上释放。
previewResult = machineCalculator.CreateScheduleResultBackward(machine, operation,
processingTimeTotal, backwardLatestEndTime, processingTime, operation.getQuantity(),
0, backwardLatestEndTime, backwardLatestEndTime, emptySetupSegments, false);
} catch (RuntimeException ex) {
if (isBackwardInsufficientTimeException(ex)) {
writeVerboseLog("[BackwardInsertWindow] switch to forward because backward preview has insufficient machine time"
+ ", operationId=" + operation.getId()
+ ", groupId=" + operation.getGroupId()
+ ", machineId=" + machine.getId()
+ ", attempt=" + backwardAttempt
+ ", backwardLatestEndTime=" + backwardLatestEndTime
+ ", backwardLatestEnd=" + convertTime(backwardLatestEndTime)
+ ", processingSeconds=" + processingTimeTotal
+ ", reason=" + ex.getMessage());
forwardFallbackRequired = true;
break;
}
throw ex;
}
CopyOnWriteArrayList<ScheduleResultDetail> previewGeneDetails =
(CopyOnWriteArrayList<ScheduleResultDetail>) previewResult.get(2);
int previewProcessStartTime = previewGeneDetails.stream()
.mapToInt(ScheduleResultDetail::getStartTime)
.min()
.orElse(backwardLatestEndTime);
int previewProcessEndTime = previewGeneDetails.stream()
.mapToInt(ScheduleResultDetail::getEndTime)
.max()
.orElse(backwardLatestEndTime);
machineCalculator.AddMachineAvailable(machine, previewGeneDetails);
GAScheduleResult nextGeneOnMachine = getNextMachineTaskForBackward(machineTasks, previewProcessEndTime);
// 黄色任务旧换型可能占着“绿色 -> 黄色”的时间,先释放出来再判断蓝色能不能插进去。
CopyOnWriteArrayList<ScheduleResultDetail> releasedNextSetupDetails =
releaseNextTaskSetupForBackward(nextGeneOnMachine, machine);
int nextSetupLatestEndTime = nextGeneOnMachine == null
? previewProcessEndTime
: Math.min(backwardLatestEndTime, getProcessStartTime(nextGeneOnMachine));
Map<Integer,Object> nextSetupResult = calculateSetupTimeBackward(nextGeneOnMachine, operation, machine,
nextSetupLatestEndTime, processingTimeTotal, globalParam.is_smoothChangeOverInWeek(),
allOperations, machineTasks);
// 后置换型属于后任务:它决定当前工序最晚只能加工到什么时候。
int nextSetupTime = (int) nextSetupResult.get(1);
int latestProcessEndTime = (int) nextSetupResult.get(4);
int attemptProcessingTimeTotal = (int) nextSetupResult.get(5);
boolean backwardChangeoverFeasible = true;
if (Boolean.TRUE.equals(nextSetupResult.get(8))) {
backwardChangeoverFeasible = false;
writeVerboseLog("[BackwardInsertWindow] 后置换型可用时间不足,当前插入位置不可行"
+ ", operationId=" + operation.getId()
+ ", groupId=" + operation.getGroupId()
+ ", machineId=" + machine.getId()
+ ", setupTime=" + nextSetupTime
+ ", availableSeconds=" + nextSetupResult.get(7));
}
CopyOnWriteArrayList<TimeSegment> noSetupSegments = new CopyOnWriteArrayList<>();
Map<Integer,Object> candidateResult;
try {
// 按“扣掉后置换型后的最晚结束时间”重新倒排当前加工段。
candidateResult = machineCalculator.CreateScheduleResultBackward(machine, operation,
attemptProcessingTimeTotal, latestProcessEndTime, processingTime, operation.getQuantity(),
0, latestProcessEndTime, latestProcessEndTime, noSetupSegments, false);
} catch (RuntimeException ex) {
if (isBackwardInsufficientTimeException(ex)) {
restoreNextTaskSetupForBackward(nextGeneOnMachine, machine, releasedNextSetupDetails);
writeVerboseLog("[BackwardInsertWindow] switch to forward because backward candidate has insufficient machine time"
+ ", operationId=" + operation.getId()
+ ", groupId=" + operation.getGroupId()
+ ", machineId=" + machine.getId()
+ ", attempt=" + backwardAttempt
+ ", latestProcessEndTime=" + latestProcessEndTime
+ ", latestProcessEnd=" + convertTime(latestProcessEndTime)
+ ", processingSeconds=" + attemptProcessingTimeTotal
+ ", reason=" + ex.getMessage());
forwardFallbackRequired = true;
break;
}
throw ex;
}
CopyOnWriteArrayList<ScheduleResultDetail> candidateGeneDetails =
(CopyOnWriteArrayList<ScheduleResultDetail>) candidateResult.get(2);
int finalProcessStartTime = getProcessStartTime(candidateGeneDetails, previewProcessStartTime);
int finalProcessEndTime = getProcessEndTime(candidateGeneDetails, previewProcessEndTime);
GAScheduleResult prevGeneOnMachine = getPrevMachineTaskForBackward(machineTasks, finalProcessStartTime);
if (finalProcessStartTime < baselineTime) {
machineCalculator.AddMachineAvailable(machine, candidateGeneDetails);
restoreNextTaskSetupForBackward(nextGeneOnMachine, machine, releasedNextSetupDetails);
writeVerboseLog("[BackwardInsertWindow] switch to forward because backward start is before baseline"
+ ", operationId=" + operation.getId()
+ ", groupId=" + operation.getGroupId()
+ ", machineId=" + machine.getId()
+ ", attempt=" + backwardAttempt
+ ", baselineTime=" + baselineTime
+ ", baseline=" + convertTime(baselineTime)
+ ", finalProcessStart=" + finalProcessStartTime
+ ", finalProcessStartTime=" + convertTime(finalProcessStartTime));
forwardFallbackRequired = true;
break;
}
writeVerboseLog("[BackwardChangeover] check previous adjacent task, operationId=" + operation.getId()
+ ", groupId=" + operation.getGroupId()
+ ", execId=" + operation.getExecId()
+ ", machineId=" + machine.getId()
+ ", attempt=" + backwardAttempt
+ ", finalProcessStart=" + finalProcessStartTime
+ ", finalProcessStartTime=" + convertTime(finalProcessStartTime)
+ ", prevTask=" + describeScheduleResult(prevGeneOnMachine));
Map<Integer,Object> previousSetupResult = calculateSetupTimeBeforeBackward(prevGeneOnMachine, operation, machine,
finalProcessStartTime, attemptProcessingTimeTotal, globalParam.is_smoothChangeOverInWeek(),
allOperations, machineTasks);
// 前置换型属于当前任务:要跟当前加工段一起作为一个 block 校验是否落在前后任务之间。
int previousSetupTime = (int) previousSetupResult.get(1);
int blockStartTime = finalProcessStartTime;
if (previousSetupTime > 0) {
blockStartTime = Math.min(blockStartTime, (int) previousSetupResult.get(2));
}
if (blockStartTime < baselineTime) {
machineCalculator.AddMachineAvailable(machine, candidateGeneDetails);
restoreNextTaskSetupForBackward(nextGeneOnMachine, machine, releasedNextSetupDetails);
writeVerboseLog("[BackwardInsertWindow] switch to forward because backward block is before baseline"
+ ", operationId=" + operation.getId()
+ ", groupId=" + operation.getGroupId()
+ ", machineId=" + machine.getId()
+ ", attempt=" + backwardAttempt
+ ", baselineTime=" + baselineTime
+ ", baseline=" + convertTime(baselineTime)
+ ", blockStart=" + blockStartTime
+ ", blockStartTime=" + convertTime(blockStartTime));
forwardFallbackRequired = true;
break;
}
if (Boolean.TRUE.equals(previousSetupResult.get(8))) {
backwardChangeoverFeasible = false;
writeVerboseLog("[BackwardInsertWindow] 前置换型可用时间不足,当前插入位置不可行"
+ ", operationId=" + operation.getId()
+ ", groupId=" + operation.getGroupId()
+ ", machineId=" + machine.getId()
+ ", setupTime=" + previousSetupTime
+ ", availableSeconds=" + previousSetupResult.get(7));
}
boolean canInsertBackwardWindow = backwardChangeoverFeasible
&& validateBackwardInsertWindow(prevGeneOnMachine, nextGeneOnMachine,
operation, machine, finalProcessStartTime, finalProcessEndTime,
previousSetupResult, nextSetupResult);
if (canInsertBackwardWindow) {
// 插入成功后,当前任务保存前置换型;后任务保存新的后置换型。
setupTime = previousSetupTime;
operation.setChangeLineTime(setupTime);
geneDetails = candidateGeneDetails;
if (previousSetupTime > 0) {
CopyOnWriteArrayList<TimeSegment> previousSetupSegments =
(CopyOnWriteArrayList<TimeSegment>) previousSetupResult.get(6);
CopyOnWriteArrayList<ScheduleResultDetail> previousSetupDetails =
machineCalculator.CreateSetupDetailsBackward(machine, previousSetupTime,
(int) previousSetupResult.get(2),
(int) previousSetupResult.get(3),
previousSetupSegments,
globalParam.is_smoothChangeOverInWeek());
if (previousSetupDetails != null && !previousSetupDetails.isEmpty()) {
geneDetails.addAll(previousSetupDetails);
geneDetails.sort(Comparator.comparingInt(ScheduleResultDetail::getStartTime));
}
}
replaceNextTaskSetupForBackward(nextGeneOnMachine, machine, nextSetupTime, nextSetupResult);
backwardScheduled = true;
break;
}
machineCalculator.AddMachineAvailable(machine, candidateGeneDetails);
restoreNextTaskSetupForBackward(nextGeneOnMachine, machine, releasedNextSetupDetails);
int nextLatestEndTime;
if (prevGeneOnMachine != null) {
// 当前窗口放不下,就继续往更早的相邻任务之前找插入点。
nextLatestEndTime = getProcessStartTime(prevGeneOnMachine);
} else {
nextLatestEndTime = Math.min(finalProcessStartTime, previewProcessStartTime) - 1;
}
if (nextLatestEndTime <= 0 || nextLatestEndTime >= backwardLatestEndTime) {
break;
}
backwardLatestEndTime = nextLatestEndTime;
}
if (!backwardScheduled && forwardFallbackRequired) {
writeVerboseLog("[BackwardInsertWindow] backward search passed schedule baseline, request whole order forward fallback"
+ ", operationId=" + operation.getId()
+ ", groupId=" + operation.getGroupId()
+ ", machineId=" + machine.getId()
+ ", baselineTime=" + baselineTime
+ ", baseline=" + convertTime(baselineTime));
return new JitBackwardScheduleResult(new CopyOnWriteArrayList<>(), 0, true);
} else if (!backwardScheduled) {
// 倒排换型插入不成功但没有触发基准线/设备时间兜底时,保留原倒排能力,不强行制造换型。
writeVerboseLog("[BackwardInsertWindow] 所有倒排插入位置都放不下,回退为不带换型倒排"
+ ", operationId=" + operation.getId()
+ ", groupId=" + operation.getGroupId()
+ ", machineId=" + machine.getId()
+ ", latestEndTime=" + latestEndTime
+ ", latestEnd=" + convertTime(latestEndTime));
geneDetails = machineCalculator.getNextAvailableTime(machine, operation, latestEndTime, -1,
processingTimeTotal, machineTasks, operation.getIsInterrupt() != 1, true,
processingTime, operation.getQuantity(), true, true);
setupTime = 0;
operation.setChangeLineTime(setupTime);
}
return new JitBackwardScheduleResult(geneDetails, setupTime, false);
}
/**
* helper 返回给 GeneticDecoder 的结果:
* geneDetails/setupTime 表示本工序排程成功;forwardFallbackRequired 表示需要外层整单正排。
*/
static class JitBackwardScheduleResult {
private final CopyOnWriteArrayList<ScheduleResultDetail> geneDetails;
private final int setupTime;
private final boolean forwardFallbackRequired;
JitBackwardScheduleResult(CopyOnWriteArrayList<ScheduleResultDetail> geneDetails, int setupTime,
boolean forwardFallbackRequired) {
this.geneDetails = geneDetails;
this.setupTime = setupTime;
this.forwardFallbackRequired = forwardFallbackRequired;
}
CopyOnWriteArrayList<ScheduleResultDetail> getGeneDetails() {
return geneDetails;
}
int getSetupTime() {
return setupTime;
}
boolean isForwardFallbackRequired() {
return forwardFallbackRequired;
}
}
boolean validateBackwardInsertWindow(GAScheduleResult prevGeneOnMachine,
GAScheduleResult nextGeneOnMachine,
Entry operation,
Machine machine,
int processStartTime,
int processEndTime,
Map<Integer,Object> previousSetupResult,
Map<Integer,Object> nextSetupResult) {
int prevBoundaryTime = prevGeneOnMachine == null
? Integer.MIN_VALUE
: getProcessEndTime(prevGeneOnMachine);
int nextBoundaryTime = nextGeneOnMachine == null
? Integer.MAX_VALUE
: getProcessStartTime(nextGeneOnMachine);
int blockStartTime = processStartTime;
int previousSetupTime = getSetupTime(previousSetupResult);
if (previousSetupTime > 0) {
blockStartTime = Math.min(blockStartTime, (int) previousSetupResult.get(2));
}
int blockEndTime = processEndTime;
int nextSetupTime = getSetupTime(nextSetupResult);
if (nextSetupTime > 0) {
blockEndTime = Math.max(blockEndTime, (int) nextSetupResult.get(3));
}
boolean previousSetupInsufficient = Boolean.TRUE.equals(previousSetupResult == null ? false : previousSetupResult.get(8));
boolean nextSetupInsufficient = Boolean.TRUE.equals(nextSetupResult == null ? false : nextSetupResult.get(8));
boolean canInsert = !previousSetupInsufficient
&& !nextSetupInsufficient
&& blockStartTime >= prevBoundaryTime
&& blockEndTime <= nextBoundaryTime;
writeVerboseLog("[BackwardInsertWindow] operationId=" + operation.getId()
+ ", groupId=" + operation.getGroupId()
+ ", machineId=" + machine.getId()
+ ", prevTask=" + describeScheduleResult(prevGeneOnMachine)
+ ", nextTask=" + describeScheduleResult(nextGeneOnMachine)
+ ", prevBoundary=" + (prevGeneOnMachine == null ? "null" : convertTime(prevBoundaryTime))
+ ", nextBoundary=" + (nextGeneOnMachine == null ? "null" : convertTime(nextBoundaryTime))
+ ", blockStart=" + convertTime(blockStartTime)
+ ", blockEnd=" + convertTime(blockEndTime)
+ ", processStart=" + convertTime(processStartTime)
+ ", processEnd=" + convertTime(processEndTime)
+ ", previousSetupTime=" + previousSetupTime
+ ", nextSetupTime=" + nextSetupTime
+ ", canInsert=" + canInsert);
return canInsert;
}
/**
* 试插蓝色前,先把黄色任务身上的旧后置换型释放出来。
* 这里不永久删除业务含义,只是让设备日历先空出来,方便判断真实的绿色/黄色间隔。
*/
CopyOnWriteArrayList<ScheduleResultDetail> releaseNextTaskSetupForBackward(GAScheduleResult nextGeneOnMachine,
Machine machine) {
CopyOnWriteArrayList<ScheduleResultDetail> releasedSetupDetails = new CopyOnWriteArrayList<>();
if (nextGeneOnMachine == null || nextGeneOnMachine.getGeneDetails() == null
|| nextGeneOnMachine.getGeneDetails().isEmpty()) {
return releasedSetupDetails;
}
CopyOnWriteArrayList<ScheduleResultDetail> nextDetails =
new CopyOnWriteArrayList<>(nextGeneOnMachine.getGeneDetails());
nextDetails.stream()
.filter(ScheduleResultDetail::isSetup)
.forEach(releasedSetupDetails::add);
if (releasedSetupDetails.isEmpty()) {
return releasedSetupDetails;
}
machineCalculator.AddMachineAvailable(machine, releasedSetupDetails);
nextDetails.removeIf(ScheduleResultDetail::isSetup);
nextGeneOnMachine.setGeneDetails(nextDetails);
nextGeneOnMachine.setChangeOverTime(0);
refreshScheduleResultWindow(nextGeneOnMachine);
writeVerboseLog("[BackwardInsertWindow] 临时释放黄色旧换型"
+ ", nextTask=" + describeScheduleResult(nextGeneOnMachine)
+ ", releasedSetupCount=" + releasedSetupDetails.size()
+ ", releasedSetupSeconds=" + sumSetupSeconds(releasedSetupDetails));
return releasedSetupDetails;
}
/**
* 当前绿色/黄色间隔放不下蓝色时,把刚才释放的黄色旧换型恢复回去。
*/
void restoreNextTaskSetupForBackward(GAScheduleResult nextGeneOnMachine,
Machine machine,
List<ScheduleResultDetail> releasedSetupDetails) {
if (releasedSetupDetails == null || releasedSetupDetails.isEmpty()) {
return;
}
setMachineAvailabilityUsed(machine, releasedSetupDetails, true);
if (nextGeneOnMachine != null) {
CopyOnWriteArrayList<ScheduleResultDetail> nextDetails = nextGeneOnMachine.getGeneDetails() == null
? new CopyOnWriteArrayList<>()
: new CopyOnWriteArrayList<>(nextGeneOnMachine.getGeneDetails());
nextDetails.removeIf(ScheduleResultDetail::isSetup);
nextDetails.addAll(releasedSetupDetails);
nextDetails.sort(Comparator.comparingInt(ScheduleResultDetail::getStartTime));
nextGeneOnMachine.setGeneDetails(nextDetails);
nextGeneOnMachine.setChangeOverTime(sumSetupSeconds(releasedSetupDetails));
refreshScheduleResultWindow(nextGeneOnMachine);
}
writeVerboseLog("[BackwardInsertWindow] 恢复黄色旧换型"
+ ", nextTask=" + describeScheduleResult(nextGeneOnMachine)
+ ", restoredSetupCount=" + releasedSetupDetails.size()
+ ", restoredSetupSeconds=" + sumSetupSeconds(releasedSetupDetails));
}
/**
* 按排程明细里的 key/usedSegment,把设备日历设置为占用或可用。
* 恢复旧换型时用 true,释放时仍沿用 machineCalculator.AddMachineAvailable。
*/
private void setMachineAvailabilityUsed(Machine machine,
List<ScheduleResultDetail> geneDetails,
boolean used) {
if (machine == null || machine.getAvailability() == null
|| geneDetails == null || geneDetails.isEmpty()) {
return;
}
Set<String> keys = new HashSet<>();
for (ScheduleResultDetail detail : geneDetails) {
if (detail == null) {
continue;
}
if (detail.getUsedSegment() != null && !detail.getUsedSegment().isEmpty()) {
detail.getUsedSegment().stream()
.map(TimeSegment::getKey)
.filter(Objects::nonNull)
.forEach(keys::add);
} else if (detail.getKey() != null) {
keys.add(detail.getKey());
}
}
if (keys.isEmpty()) {
return;
}
machine.getAvailability().stream()
.filter(t -> keys.contains(t.getKey()))
.forEach(t -> t.setUsed(used));
}
private int sumSetupSeconds(List<ScheduleResultDetail> setupDetails) {
if (setupDetails == null || setupDetails.isEmpty()) {
return 0;
}
return setupDetails.stream()
.mapToInt(ScheduleResultDetail::getProcessingTime)
.sum();
}
/**
* 后置换型归属黄色任务。
* 插入蓝色后,黄色原来的“绿色 -> 黄色”换型要释放掉,再替换成“蓝色 -> 黄色”的新换型。
*/
void replaceNextTaskSetupForBackward(GAScheduleResult nextGeneOnMachine,
Machine machine,
int setupTime,
Map<Integer,Object> setupResult) {
if (nextGeneOnMachine == null || setupResult == null) {
return;
}
CopyOnWriteArrayList<ScheduleResultDetail> nextDetails = nextGeneOnMachine.getGeneDetails() == null
? new CopyOnWriteArrayList<>()
: new CopyOnWriteArrayList<>(nextGeneOnMachine.getGeneDetails());
List<ScheduleResultDetail> oldSetupDetails = nextDetails.stream()
.filter(ScheduleResultDetail::isSetup)
.collect(Collectors.toList());
if (!oldSetupDetails.isEmpty()) {
machineCalculator.AddMachineAvailable(machine, oldSetupDetails);
nextDetails.removeIf(ScheduleResultDetail::isSetup);
}
nextGeneOnMachine.setChangeOverTime(0);
if (setupTime > 0) {
CopyOnWriteArrayList<TimeSegment> setupSegments =
(CopyOnWriteArrayList<TimeSegment>) setupResult.get(6);
CopyOnWriteArrayList<ScheduleResultDetail> newSetupDetails =
machineCalculator.CreateSetupDetailsBackward(machine, setupTime,
(int) setupResult.get(2),
(int) setupResult.get(3),
setupSegments,
globalParam.is_smoothChangeOverInWeek());
if (newSetupDetails != null && !newSetupDetails.isEmpty()) {
nextDetails.addAll(newSetupDetails);
nextDetails.sort(Comparator.comparingInt(ScheduleResultDetail::getStartTime));
}
nextGeneOnMachine.setChangeOverTime(setupTime);
}
nextGeneOnMachine.setGeneDetails(nextDetails);
refreshScheduleResultWindow(nextGeneOnMachine);
}
/**
* 根据明细刷新任务整体起止时间;加工开始/结束仍由 getProcessStartTime/getProcessEndTime 过滤 setup 得到。
*/
private void refreshScheduleResultWindow(GAScheduleResult result) {
if (result == null || result.getGeneDetails() == null || result.getGeneDetails().isEmpty()) {
return;
}
result.setStartTime(result.getGeneDetails().stream()
.mapToInt(ScheduleResultDetail::getStartTime)
.min()
.orElse(result.getStartTime()));
result.setEndTime(result.getGeneDetails().stream()
.mapToInt(ScheduleResultDetail::getEndTime)
.max()
.orElse(result.getEndTime()));
}
private int getSetupTime(Map<Integer,Object> setupResult) {
if (setupResult == null || setupResult.get(1) == null) {
return 0;
}
return (int) setupResult.get(1);
}
String describeScheduleResult(GAScheduleResult result) {
if (result == null) {
return "null";
}
return "{operationId=" + result.getOperationId()
+ ", groupId=" + result.getGroupId()
+ ", execId=" + result.getExecId()
+ ", start=" + result.getStartTime() + "(" + convertTime(result.getStartTime()) + ")"
+ ", end=" + result.getEndTime() + "(" + convertTime(result.getEndTime()) + ")"
+ ", processStart=" + getProcessStartTime(result) + "(" + convertTime(getProcessStartTime(result)) + ")"
+ ", processEnd=" + getProcessEndTime(result) + "(" + convertTime(getProcessEndTime(result)) + ")"
+ ", changeOverTime=" + result.getChangeOverTime()
+ "}";
}
String describeDiscreteParams(Entry operation) {
if (operation == null || operation.getDiscreteParameter() == null || operation.getDiscreteParameter().isEmpty()) {
return "[]";
}
return operation.getDiscreteParameter().stream()
.map(p -> "{groupId=" + p.getGroupId()
+ ", parameterId=" + p.getParameterId()
+ ", parameterName=" + p.getParameterName()
+ "}")
.collect(Collectors.joining(",", "[", "]"));
}
/**
* 临时测试开关:启动参数传 -Daps.debug.backwardSetupTimeSeconds=秒数 时,
* 只覆盖倒排换型时间;不传或传负数时走原来的矩阵/时长计算。
*/
private int overrideSetupTimeForDebug(int setupTime, String direction,
Entry fromOperation, Entry toOperation, Machine machine) {
String overrideValue = System.getProperty(DEBUG_BACKWARD_SETUP_TIME_SECONDS_PROPERTY);
if (overrideValue == null || overrideValue.trim().isEmpty()) {
return setupTime;
}
int debugSetupTime;
try {
debugSetupTime = Integer.parseInt(overrideValue.trim());
} catch (NumberFormatException ex) {
writeVerboseLog("[BackwardChangeoverDebug] 手动换型时间参数无效,继续使用原计算值"
+ ", property=" + DEBUG_BACKWARD_SETUP_TIME_SECONDS_PROPERTY
+ ", value=" + overrideValue
+ ", originalSetupTime=" + setupTime);
return setupTime;
}
if (debugSetupTime < 0) {
return setupTime;
}
writeVerboseLog("[BackwardChangeoverDebug] 使用手动换型时间"
+ ", direction=" + direction
+ ", machineId=" + (machine == null ? null : machine.getId())
+ ", fromOp=" + (fromOperation == null ? null : fromOperation.getId())
+ ", toOp=" + (toOperation == null ? null : toOperation.getId())
+ ", originalSetupTime=" + setupTime
+ ", debugSetupTime=" + debugSetupTime);
return debugSetupTime;
}
Map<Integer,Object> calculateSetupTimeBeforeBackward(GAScheduleResult prevGeneOnMachine, Entry operation,
Machine machine, int processStartTime,
int processingTimeTotal, boolean changeOverInWeek,
List<Entry> allOperations,
CopyOnWriteArrayList<GAScheduleResult> machineTasks) {
Map<Integer,Object> result = new HashMap<>();
CopyOnWriteArrayList<TimeSegment> setupSegments = new CopyOnWriteArrayList<>();
result.put(1, 0);
result.put(2, processStartTime);
result.put(3, processStartTime);
result.put(4, processStartTime);
result.put(5, processingTimeTotal);
result.put(6, setupSegments);
result.put(7, 0);
result.put(8, false);
if (prevGeneOnMachine == null || operation == null || machine == null) {
writeVerboseLog("[BackwardChangeover] skip previous changeover, prev/operation/machine is null"
+ ", operationId=" + (operation == null ? null : operation.getId())
+ ", machineId=" + (machine == null ? null : machine.getId())
+ ", processStartTime=" + processStartTime
+ ", processStart=" + convertTime(processStartTime)
+ ", machineTaskCount=" + (machineTasks == null ? 0 : machineTasks.size()));
return result;
}
Entry prevOperation = allOperations == null ? null : allOperations.stream()
.filter(t -> Objects.equals(t.getExecId(), prevGeneOnMachine.getExecId()))
.findFirst()
.orElse(null);
if (prevOperation == null) {
writeVerboseLog("[BackwardChangeover] skip previous changeover, previous Entry not found"
+ ", operationId=" + operation.getId()
+ ", prevTask=" + describeScheduleResult(prevGeneOnMachine)
+ ", allOperationCount=" + (allOperations == null ? 0 : allOperations.size()));
return result;
}
DiscreteParameterMatrixService discreteParameterMatrixService = SpringContextUtil.getBean(DiscreteParameterMatrixService.class);
DiscreteParameterDurationService discreteParameterDurationService = SpringContextUtil.getBean(DiscreteParameterDurationService.class);
double durationValue = discreteParameterDurationService.calculateChangeoverTime(
prevOperation, operation, machine, allOperations, processStartTime, machineTasks);
double matrixValue = discreteParameterMatrixService.getDiscreteParameterMatrixValue(operation, prevOperation);
int setupTime = (int) Math.max(durationValue, matrixValue);
setupTime = overrideSetupTimeForDebug(setupTime, "prev->current", prevOperation, operation, machine);
writeVerboseLog("[BackwardChangeover] previous changeover result prevOp=" + prevOperation.getId()
+ ", currentOp=" + operation.getId()
+ ", machineId=" + machine.getId()
+ ", prevParams=" + describeDiscreteParams(prevOperation)
+ ", currentParams=" + describeDiscreteParams(operation)
+ ", durationValue=" + durationValue
+ ", matrixValue=" + matrixValue
+ ", setupTime=" + setupTime
+ ", processStart=" + convertTime(processStartTime)
+ ", prevProcessEnd=" + convertTime(getProcessEndTime(prevGeneOnMachine)));
result.put(1, setupTime);
if (setupTime <= 0) {
return result;
}
int prevBoundaryTime = getProcessEndTime(prevGeneOnMachine);
LocalDateTime setupEnd = baseTime.plusSeconds(processStartTime);
if (changeOverInWeek) {
LocalDateTime setupStart = setupEnd.minusSeconds(setupTime);
int setupStartTime = secondsFromBase(setupStart);
setupSegments = collectAvailableSegmentsBetween(machine, setupStart, setupEnd);
result.put(2, setupStartTime);
result.put(3, processStartTime);
result.put(4, processStartTime);
result.put(6, setupSegments);
result.put(7, Math.max(0, processStartTime - prevBoundaryTime));
result.put(8, setupStartTime < prevBoundaryTime);
writeVerboseLog("[BackwardChangeover] previous changeover window currentOp=" + operation.getId()
+ ", setupStart=" + convertTime(setupStartTime)
+ ", setupEnd=" + convertTime(processStartTime)
+ ", prevBoundary=" + convertTime(prevBoundaryTime)
+ ", requiredSeconds=" + setupTime
+ ", availableCalendarSeconds=" + result.get(7)
+ ", insufficient=" + result.get(8)
+ ", segments=" + describeTimeSegments(setupSegments));
return result;
}
BackwardSetupWindow setupWindow = collectBackwardSetupWindow(machine, setupEnd, setupTime,
baseTime.plusSeconds(prevBoundaryTime));
result.put(2, setupWindow.startTime);
result.put(3, setupWindow.endTime);
result.put(4, processStartTime);
result.put(6, setupWindow.segments);
result.put(7, setupWindow.availableSeconds);
result.put(8, setupWindow.insufficient);
writeVerboseLog("[BackwardChangeover] previous changeover window currentOp=" + operation.getId()
+ ", setupStart=" + convertTime(setupWindow.startTime)
+ ", setupEnd=" + convertTime(setupWindow.endTime)
+ ", prevBoundary=" + convertTime(prevBoundaryTime)
+ ", requiredSeconds=" + setupTime
+ ", availableSeconds=" + setupWindow.availableSeconds
+ ", insufficient=" + setupWindow.insufficient
+ ", segments=" + describeTimeSegments(setupWindow.segments));
return result;
}
Map<Integer,Object> calculateSetupTimeBackward(GAScheduleResult nextGeneOnMachine, Entry operation,
Machine machine, int latestEndTime,
int processingTimeTotal, boolean changeOverInWeek,
List<Entry> allOperations,
CopyOnWriteArrayList<GAScheduleResult> machineTasks) {
Map<Integer,Object> result = new HashMap<>();
CopyOnWriteArrayList<TimeSegment> setupSegments = new CopyOnWriteArrayList<>();
result.put(1, 0);
result.put(2, latestEndTime);
result.put(3, latestEndTime);
result.put(4, latestEndTime);
result.put(5, processingTimeTotal);
result.put(6, setupSegments);
result.put(7, 0);
result.put(8, false);
if (nextGeneOnMachine == null || operation == null || machine == null) {
writeVerboseLog("[倒排换型定位] 未计算换型:nextGeneOnMachine/operation/machine为空"
+ ", operationId=" + (operation == null ? null : operation.getId())
+ ", machineId=" + (machine == null ? null : machine.getId())
+ ", latestEndTime=" + latestEndTime
+ ", latestEnd=" + convertTime(latestEndTime)
+ ", machineTaskCount=" + (machineTasks == null ? 0 : machineTasks.size()));
return result;
}
Entry nextOperation = allOperations == null ? null : allOperations.stream()
.filter(t -> Objects.equals(t.getExecId(), nextGeneOnMachine.getExecId()))
.findFirst()
.orElse(null);
if (nextOperation == null) {
writeVerboseLog("[倒排换型定位] 未计算换型:找不到后继工序Entry"
+ ", operationId=" + operation.getId()
+ ", nextTask=" + describeScheduleResult(nextGeneOnMachine)
+ ", allOperationCount=" + (allOperations == null ? 0 : allOperations.size()));
return result;
}
DiscreteParameterMatrixService discreteParameterMatrixService = SpringContextUtil.getBean(DiscreteParameterMatrixService.class);
DiscreteParameterDurationService discreteParameterDurationService = SpringContextUtil.getBean(DiscreteParameterDurationService.class);
double durationValue = discreteParameterDurationService.calculateChangeoverTime(
operation, nextOperation, machine, allOperations, latestEndTime, machineTasks);
double matrixValue = discreteParameterMatrixService.getDiscreteParameterMatrixValue(nextOperation, operation);
int setupTime = (int) Math.max(durationValue, matrixValue);
setupTime = overrideSetupTimeForDebug(setupTime, "current->next", operation, nextOperation, machine);
nextOperation.setChangeLineTime(setupTime);
int nextProcessStartTime = getProcessStartTime(nextGeneOnMachine);
writeVerboseLog("[倒排换型定位] 换型查询结果 currentOp=" + operation.getId()
+ ", nextOp=" + nextOperation.getId()
+ ", machineId=" + machine.getId()
+ ", currentParams=" + describeDiscreteParams(operation)
+ ", nextParams=" + describeDiscreteParams(nextOperation)
+ ", durationValue=" + durationValue
+ ", matrixValue=" + matrixValue
+ ", setupTime=" + setupTime
+ ", latestEnd=" + convertTime(latestEndTime)
+ ", nextStart=" + convertTime(nextProcessStartTime));
result.put(1, setupTime);
if (setupTime <= 0) {
return result;
}
int setupEndTime = Math.min(latestEndTime, nextProcessStartTime);
LocalDateTime setupEnd = baseTime.plusSeconds(setupEndTime);
if (changeOverInWeek) {
LocalDateTime setupStart = setupEnd.minusSeconds(setupTime);
setupSegments = collectAvailableSegmentsBetween(machine, setupStart, setupEnd);
result.put(2, secondsFromBase(setupStart));
result.put(3, setupEndTime);
result.put(4, secondsFromBase(setupStart));
result.put(6, setupSegments);
result.put(7, sumCalendarSeconds(setupSegments));
writeVerboseLog("[倒排换型定位] 换型窗口 currentOp=" + operation.getId()
+ ", setupStart=" + convertTime(secondsFromBase(setupStart))
+ ", setupEnd=" + convertTime(setupEndTime)
+ ", latestProcessEnd=" + convertTime(secondsFromBase(setupStart))
+ ", requiredSeconds=" + setupTime
+ ", availableSeconds=" + result.get(7)
+ ", segments=" + describeTimeSegments(setupSegments));
return result;
}
BackwardSetupWindow setupWindow = collectBackwardSetupWindow(machine, setupEnd, setupTime);
result.put(2, setupWindow.startTime);
result.put(3, setupWindow.endTime);
result.put(4, setupWindow.startTime);
result.put(6, setupWindow.segments);
result.put(7, setupWindow.availableSeconds);
result.put(8, setupWindow.insufficient);
writeVerboseLog("[倒排换型定位] 换型窗口 currentOp=" + operation.getId()
+ ", setupStart=" + convertTime(setupWindow.startTime)
+ ", setupEnd=" + convertTime(setupWindow.endTime)
+ ", latestProcessEnd=" + convertTime(setupWindow.startTime)
+ ", requiredSeconds=" + setupTime
+ ", availableSeconds=" + setupWindow.availableSeconds
+ ", insufficient=" + setupWindow.insufficient
+ ", segments=" + describeTimeSegments(setupWindow.segments));
return result;
}
private BackwardSetupWindow collectBackwardSetupWindow(Machine machine, LocalDateTime setupEnd, int setupTime) {
return collectBackwardSetupWindow(machine, setupEnd, setupTime, null);
}
private BackwardSetupWindow collectBackwardSetupWindow(Machine machine, LocalDateTime setupEnd, int setupTime,
LocalDateTime earliestStart) {
BackwardSetupWindow window = new BackwardSetupWindow();
window.endTime = secondsFromBase(setupEnd);
window.startTime = secondsFromBase(setupEnd);
if (machine == null || machine.getAvailability() == null || setupTime <= 0) {
window.insufficient = setupTime > 0;
return window;
}
int remaining = setupTime;
LocalDateTime cursor = setupEnd;
List<TimeSegment> slots = machine.getAvailability().stream()
.filter(slot -> !slot.isUsed())
.filter(slot -> slot.getType() != SegmentType.MAINTENANCE)
.filter(slot -> slot.getStart().isBefore(setupEnd))
.filter(slot -> earliestStart == null || slot.getEnd().isAfter(earliestStart))
.sorted(Comparator.comparing(TimeSegment::getEnd).reversed())
.collect(Collectors.toList());
for (TimeSegment slot : slots) {
LocalDateTime effectiveEnd = slot.getEnd().isAfter(cursor) ? cursor : slot.getEnd();
LocalDateTime effectiveSlotStart = earliestStart != null && slot.getStart().isBefore(earliestStart)
? earliestStart
: slot.getStart();
if (!effectiveSlotStart.isBefore(effectiveEnd)) {
continue;
}
int availableSeconds = (int) ChronoUnit.SECONDS.between(effectiveSlotStart, effectiveEnd);
int usedSeconds = Math.min(remaining, availableSeconds);
LocalDateTime effectiveStart = effectiveEnd.minusSeconds(usedSeconds);
window.segments.add(copySegment(slot, effectiveStart, effectiveEnd, true));
window.availableSeconds += usedSeconds;
remaining -= usedSeconds;
cursor = effectiveStart;
if (remaining <= 0) {
break;
}
}
window.segments.sort(Comparator.comparing(TimeSegment::getStart));
window.startTime = secondsFromBase(cursor);
window.insufficient = remaining > 0;
return window;
}
private CopyOnWriteArrayList<TimeSegment> collectAvailableSegmentsBetween(Machine machine,
LocalDateTime startTime,
LocalDateTime endTime) {
if (machine == null || machine.getAvailability() == null || !startTime.isBefore(endTime)) {
return new CopyOnWriteArrayList<>();
}
return machine.getAvailability().stream()
.filter(slot -> !slot.isUsed())
.filter(slot -> slot.getType() != SegmentType.MAINTENANCE)
.filter(slot -> slot.getStart().isBefore(endTime) && slot.getEnd().isAfter(startTime))
.map(slot -> {
LocalDateTime effectiveStart = slot.getStart().isBefore(startTime) ? startTime : slot.getStart();
LocalDateTime effectiveEnd = slot.getEnd().isAfter(endTime) ? endTime : slot.getEnd();
return copySegment(slot, effectiveStart, effectiveEnd, true);
})
.filter(slot -> slot.getStart().isBefore(slot.getEnd()))
.sorted(Comparator.comparing(TimeSegment::getStart))
.collect(Collectors.toCollection(CopyOnWriteArrayList::new));
}
private TimeSegment copySegment(TimeSegment source, LocalDateTime startTime,
LocalDateTime endTime, boolean used) {
TimeSegment segment = new TimeSegment();
segment.setStart(startTime);
segment.setEnd(endTime);
segment.setType(source.getType());
segment.setHoliday(source.isHoliday());
segment.setUsed(used);
segment.setEfficiency(source.getEfficiency());
return segment;
}
private int sumCalendarSeconds(List<TimeSegment> segments) {
if (segments == null || segments.isEmpty()) {
return 0;
}
return segments.stream()
.mapToInt(segment -> (int) ChronoUnit.SECONDS.between(segment.getStart(), segment.getEnd()))
.sum();
}
private String describeTimeSegments(List<TimeSegment> segments) {
if (segments == null || segments.isEmpty()) {
return "[]";
}
return segments.stream()
.map(segment -> "[" + secondsFromBase(segment.getStart())
+ "-" + secondsFromBase(segment.getEnd())
+ "](" + segment.getStart()
+ "-" + segment.getEnd()
+ "," + ChronoUnit.SECONDS.between(segment.getStart(), segment.getEnd())
+ ")")
.collect(Collectors.joining(",", "[", "]"));
}
private int secondsFromBase(LocalDateTime time) {
return (int) ChronoUnit.SECONDS.between(baseTime, time);
}
private static class BackwardSetupWindow {
private int startTime;
private int endTime;
private int availableSeconds;
private boolean insufficient;
private CopyOnWriteArrayList<TimeSegment> segments = new CopyOnWriteArrayList<>();
}
// ==================== 倒排换型辅助方法结束 ====================
}
......@@ -622,6 +622,8 @@ public class GeneticDecoder {
// List<Entry> allScheduledOps = new ArrayList<>();
// 缓存机器任务
Map<Long, CopyOnWriteArrayList<GAScheduleResult>> machineTasksCache = new HashMap<>();
// 记录已经整单转正排的订单,避免倒排序列后续再次进入同一个 groupId。
Set<Integer> forwardFallbackOrderIds = new HashSet<>();
int opCount = 0;
long slowOpThresholdNs = 500_000_000L; // 500ms
......@@ -670,6 +672,9 @@ public class GeneticDecoder {
if (scheduledCount >= orderOps.size()) {
if (forwardFallbackOrderIds.contains(groupId)) {
continue;
}
throw new IllegalStateException(String.format(
"订单%d的工序已全部调度(共%d道),无需重复处理!",
groupId, orderOps.size()));
......@@ -702,7 +707,21 @@ public class GeneticDecoder {
int dueDateForOp = opIsJit ? orderAnchor
: (schedInfo != null ? schedInfo.getValue() : 0);
int actualEndTime = processOperation(currentOp,machineId,processTime,machineOption,chromosome,machineIdMap,machineTasksCache,entryIndexById,scheduleIndexById,dueDateForOp,true,opIsJit);
OperationScheduleResult operationResult = processOperationSchedule(currentOp, machineId, processTime,
machineOption, chromosome, machineIdMap, machineTasksCache, entryIndexById,
scheduleIndexById, dueDateForOp, true, opIsJit);
if (operationResult.isForwardFallbackRequired()) {
// 倒排中任一道序因换型/日历约束放不下时,先撤回本订单已倒排结果,再整单正排。
clearOrderSchedulingForForwardFallback(chromosome, groupId, machineTasksCache, scheduleIndexById);
int forwardEndTime = scheduleOrderForwardFallback(groupId, chromosome, machineIdMap, machineTasksCache,
entryIndexById, scheduleIndexById, opMachineKeyMap);
orderProcessCounter.put(groupId, entrysBygroupId.get(groupId).size());
orderLastEndTime.put(groupId, forwardEndTime);
forwardFallbackOrderIds.add(groupId);
opCount += entrysBygroupId.get(groupId).size();
continue;
}
int actualEndTime = operationResult.getEndTime();
long opElapsed = System.nanoTime() - opStart;
opCount++;
......@@ -1167,52 +1186,130 @@ public class GeneticDecoder {
return null;
}
/**
* 倒排兜底:当前订单已排结果清掉后,按 sequence 从小到大重新正排整单。
*/
private int scheduleOrderForwardFallback(int groupId,
Chromosome chromosome,
Map<Long, Machine> machineIdMap,
Map<Long, CopyOnWriteArrayList<GAScheduleResult>> machineTasksCache,
Map<Integer, Entry> entryIndexById,
Map<Integer, GAScheduleResult> scheduleIndexById,
Map<String, OpMachine> opMachineKeyMap) {
List<Entry> orderOps = chromosome.getAllOperations().stream()
.filter(t -> t.getGroupId() == groupId)
.sorted(Comparator.comparing(Entry::getSequence))
.collect(Collectors.toList());
int finalEndTime = 0;
for (Entry currentOp : orderOps) {
String opMachineKey = groupId + "_" + currentOp.getSequence();
OpMachine machineOption = opMachineKeyMap.get(opMachineKey);
if (machineOption == null) {
throw new IllegalStateException("未找到正排兜底设备选择,groupId=" + groupId
+ ", operationId=" + currentOp.getId()
+ ", sequence=" + currentOp.getSequence());
}
private int processOperation(Entry currentOp,Long machineId,double processTime,OpMachine machineOption,Chromosome chromosome,Map<Long, Machine> machineIdMap,Map<Long, CopyOnWriteArrayList<GAScheduleResult>> machineTasksCache,Map<Integer, Entry> entryIndexById,Map<Integer, GAScheduleResult> scheduleIndexById,int orderDueDate,boolean islockMachineTime, boolean isJit)
{
OperationScheduleResult operationResult = processOperationSchedule(currentOp, machineOption.getMachineId(),
machineOption.getProcessingTime(), machineOption, chromosome, machineIdMap,
machineTasksCache, entryIndexById, scheduleIndexById, 0, true, false);
if (operationResult.isForwardFallbackRequired()) {
throw new IllegalStateException("正排兜底过程中不应再次触发正排兜底,groupId=" + groupId
+ ", operationId=" + currentOp.getId());
}
finalEndTime = operationResult.getEndTime();
}
return finalEndTime;
}
/**
* 整单正排前先撤回本订单已经写入的倒排结果,同时归还机台可用时间。
*/
private void clearOrderSchedulingForForwardFallback(Chromosome chromosome,
int groupId,
Map<Long, CopyOnWriteArrayList<GAScheduleResult>> machineTasksCache,
Map<Integer, GAScheduleResult> scheduleIndexById) {
List<GAScheduleResult> resultsToRemove = chromosome.getResult().stream()
.filter(t -> t.getGroupId() == groupId)
.collect(Collectors.toList());
for (GAScheduleResult result : resultsToRemove) {
Machine machine = getMachineById(chromosome, result.getMachineId());
if (machine != null) {
AddMachineAvailable(machine, result.getGeneDetails());
}
chromosome.getResult().remove(result);
scheduleIndexById.remove(result.getOperationId());
GAScheduleResult result= processOperationResult( currentOp, machineId, processTime, machineOption, chromosome, machineIdMap, machineTasksCache, entryIndexById, scheduleIndexById, orderDueDate, islockMachineTime, isJit);
if(result==null)
return 0;
return result.getEndTime();
CopyOnWriteArrayList<GAScheduleResult> machineTasks = machineTasksCache.get(result.getMachineId());
if (machineTasks != null) {
machineTasks.removeIf(t -> t.getOperationId() == result.getOperationId());
}
}
machineTasksCache.clear();
}
public GAScheduleResult processOperationResult(Entry currentOp,Long machineId,double processTime,OpMachine machineOption,Chromosome chromosome,Map<Long, Machine> machineIdMap,Map<Long, CopyOnWriteArrayList<GAScheduleResult>> machineTasksCache,Map<Integer, Entry> entryIndexById,Map<Integer, GAScheduleResult> scheduleIndexById,int orderDueDate,boolean islockMachineTime, boolean isJit)
{
private int processOperation(Entry currentOp, Long machineId, double processTime,
OpMachine machineOption, Chromosome chromosome,
Map<Long, Machine> machineIdMap,
Map<Long, CopyOnWriteArrayList<GAScheduleResult>> machineTasksCache,
Map<Integer, Entry> entryIndexById,
Map<Integer, GAScheduleResult> scheduleIndexById,
int orderDueDate, boolean islockMachineTime, boolean isJit) {
OperationScheduleResult result = processOperationSchedule(currentOp, machineId, processTime, machineOption,
chromosome, machineIdMap, machineTasksCache, entryIndexById, scheduleIndexById,
orderDueDate, islockMachineTime, isJit);
return result.getEndTime();
}
private OperationScheduleResult processOperationSchedule(Entry currentOp, Long machineId, double processTime,
OpMachine machineOption, Chromosome chromosome,
Map<Long, Machine> machineIdMap,
Map<Long, CopyOnWriteArrayList<GAScheduleResult>> machineTasksCache,
Map<Integer, Entry> entryIndexById,
Map<Integer, GAScheduleResult> scheduleIndexById,
int orderDueDate, boolean islockMachineTime, boolean isJit) {
Machine targetMachine = machineIdMap.get(machineId);
int prevtime = 0;
//后处理时间
// int teardownTime = currentOp.getTeardownTime();
if(isJit)
{
prevtime=orderDueDate;
if (isJit) {
// 倒排边界来自订单交期/锚点;如果存在后道序,则以后道序约束继续往前推。
prevtime = orderDueDate;
if (!currentOp.getNextEntryIds().isEmpty()) {
// 处理多个前工序
prevtime = CalNexttime(prevtime, currentOp, chromosome, processTime, targetMachine, entryIndexById, scheduleIndexById);
prevtime = CalNexttime(prevtime, currentOp, chromosome, processTime, targetMachine,
entryIndexById, scheduleIndexById);
}
}else {
} else {
if (!currentOp.getPrevEntryIds().isEmpty()) {
// 处理多个前工序
prevtime = CalPrevtime(prevtime, currentOp, chromosome, processTime, targetMachine, entryIndexById, scheduleIndexById);
// 正排边界来自前道序完成时间。
prevtime = CalPrevtime(prevtime, currentOp, chromosome, processTime, targetMachine,
entryIndexById, scheduleIndexById);
}
}
int prevendtime=prevtime;
int prevendtime = prevtime;
Machine machine = machineIdMap.get(machineId);
return processWithSingleMachine(currentOp, machine, processTime, prevtime, machineOption, chromosome,
false, prevendtime, machineTasksCache, entryIndexById, scheduleIndexById, islockMachineTime, isJit);
}
GAScheduleResult result= processWithSingleMachine(currentOp, machine, processTime, prevtime,machineOption, chromosome,false,prevendtime,machineTasksCache,entryIndexById, scheduleIndexById, islockMachineTime,isJit);
return result;
public GAScheduleResult processOperationResult(Entry currentOp, Long machineId, double processTime,
OpMachine machineOption, Chromosome chromosome,
Map<Long, Machine> machineIdMap,
Map<Long, CopyOnWriteArrayList<GAScheduleResult>> machineTasksCache,
Map<Integer, Entry> entryIndexById,
Map<Integer, GAScheduleResult> scheduleIndexById,
int orderDueDate, boolean islockMachineTime, boolean isJit) {
return processOperationSchedule(currentOp, machineId, processTime, machineOption, chromosome, machineIdMap,
machineTasksCache, entryIndexById, scheduleIndexById, orderDueDate, islockMachineTime, isJit)
.getScheduleResult();
}
private GAScheduleResult processWithSingleMachine(Entry operation, Machine machine, double processingTime,
private OperationScheduleResult processWithSingleMachine(Entry operation, Machine machine, double processingTime,
int prevOperationEndTime,OpMachine machineOption, Chromosome chromosome,boolean calbom,int prevendtime,Map<Long, CopyOnWriteArrayList<GAScheduleResult>> machineTasksCache,Map<Integer, Entry> entryIndexById,Map<Integer, GAScheduleResult> scheduleIndexById,boolean islockMachineTime,boolean isJit) {
long pwsStart = System.nanoTime();
......@@ -1230,7 +1327,7 @@ public class GeneticDecoder {
throw new RuntimeException("工序总加工时长" + days + "天超出限制");
}
if (machine == null || operation.getMachineOptions() == null) {
return null;
return OperationScheduleResult.success(null);
}
// MachineOption machineOption= operation.getMachineOptions().stream()
......@@ -1295,7 +1392,7 @@ public class GeneticDecoder {
// ", 前处理: " + preTime + ", 换型: " + 0 + ", 数量: " + operation.getQuantity() + ", 设备: " + machine.getId() + ", 是否可中断: " + operation.getIsInterrupt());
//
// }
return result;
return OperationScheduleResult.success(result);
}
......@@ -1322,34 +1419,53 @@ public class GeneticDecoder {
CopyOnWriteArrayList<ScheduleResultDetail> geneDetails = new CopyOnWriteArrayList<>();
// 下面开始生成正式的 geneDetails。
// 与 buildMachinePreview 的区别是:这里得到的结果会继续向下写入 GAScheduleResult,成为真正排程结果。
if (!isJit&&_globalParam.is_smoothChangeOver()) {
//是否考虑换型时间
Map<Integer, Object> reslte = calculateSetupTime(lastGeneOnMachine, operation, machine, earliestStartTime, processingTimeTotal, _globalParam.is_smoothChangeOverInWeek(), chromosome.getAllOperations());
setupTime = (int) reslte.get(1);//换型时间
int setupStartTime = (int) reslte.get(2);//换型开始时间
//earliestStartTime=(int)reslte.get(3);//上个任务的结束时间
earliestStartTime = (int) reslte.get(4);//最早开工时间
processingTimeTotal = (int) reslte.get(5);//processingTimeTotal
if (setupTime == 0) {
geneDetails = machineCalculator.getNextAvailableTime(machine, operation, earliestStartTime, -1,
processingTimeTotal, machineTasks, operation.getIsInterrupt() != 1, true, processingTime, operation.getQuantity(), islockMachineTime, isJit);
BackwardChangeoverHelper backwardChangeoverHelper =
new BackwardChangeoverHelper(_globalParam, baseTime, machineCalculator);
// 下面开始生成正式的 geneDetails。
if (_globalParam.is_smoothChangeOver()) {
if (isJit && islockMachineTime) {
// JIT 倒排 + 换型:同时校验前置换型、当前加工、后置换型能否插入相邻任务窗口。
BackwardChangeoverHelper.JitBackwardScheduleResult backwardResult =
backwardChangeoverHelper.scheduleJitBackwardWithChangeover(machine, operation, earliestStartTime,
processingTimeTotal, processingTime, machineTasks, chromosome.getAllOperations());
if (backwardResult.isForwardFallbackRequired()) {
return OperationScheduleResult.forwardFallback();
}
geneDetails = backwardResult.getGeneDetails();
setupTime = backwardResult.getSetupTime();
} else if (!isJit) {
// 非 JIT 正排 + 考虑换型:沿用原来的正排换型逻辑。
Map<Integer, Object> reslte = calculateSetupTime(lastGeneOnMachine, operation, machine, earliestStartTime,
processingTimeTotal, _globalParam.is_smoothChangeOverInWeek(), chromosome.getAllOperations());
setupTime = (int) reslte.get(1);//换型时间
int setupStartTime = (int) reslte.get(2);//换型开始时间
earliestStartTime = (int) reslte.get(4);//最早开工时间
processingTimeTotal = (int) reslte.get(5);//processingTimeTotal
if (setupTime == 0) {
geneDetails = machineCalculator.getNextAvailableTime(machine, operation, earliestStartTime, -1,
processingTimeTotal, machineTasks, operation.getIsInterrupt() != 1, true,
processingTime, operation.getQuantity(), islockMachineTime, isJit);
} else {
CopyOnWriteArrayList<TimeSegment> AvailableTimeSegment =
(CopyOnWriteArrayList<TimeSegment>) reslte.get(6);
Map<Integer, Object> result = machineCalculator.CreateScheduleResult(machine, operation,
processingTimeTotal, earliestStartTime, AvailableTimeSegment, processingTime,
operation.getQuantity(), operation.getIsInterrupt() != 1, setupTime,
_globalParam.is_smoothChangeOverInWeek(), setupStartTime, true, isJit);
setupTime = (int) result.get(1);
operation.setChangeLineTime(setupTime);
geneDetails = (CopyOnWriteArrayList<ScheduleResultDetail>) result.get(2);
}
} else {
CopyOnWriteArrayList<TimeSegment> AvailableTimeSegment = (CopyOnWriteArrayList<TimeSegment>) reslte.get(6);
Map<Integer, Object> result = machineCalculator.CreateScheduleResult(machine, operation, processingTimeTotal, earliestStartTime,
AvailableTimeSegment, processingTime, operation.getQuantity(), operation.getIsInterrupt() != 1, setupTime, _globalParam.is_smoothChangeOverInWeek(), setupStartTime, true, isJit);
setupTime = (int) result.get(1);
operation.setChangeLineTime(setupTime);
geneDetails = (CopyOnWriteArrayList<ScheduleResultDetail>) result.get(2);
geneDetails = machineCalculator.getNextAvailableTime(machine, operation, earliestStartTime, -1,
processingTimeTotal, machineTasks, operation.getIsInterrupt() != 1, true,
processingTime, operation.getQuantity(), islockMachineTime, isJit);
}
} else {
geneDetails = machineCalculator.getNextAvailableTime(machine, operation, earliestStartTime, -1,
processingTimeTotal, machineTasks, operation.getIsInterrupt() != 1, true, processingTime, operation.getQuantity(), islockMachineTime, isJit);
processingTimeTotal, machineTasks, operation.getIsInterrupt() != 1, true,
processingTime, operation.getQuantity(), islockMachineTime, isJit);
}
// FileHelper.writeLogFile(" 开始 "+operation.getGroupId()+" : "+operation.getId()+",处理时间: " + processingTime + ", 后处理: " + teardownTime +
......@@ -1374,7 +1490,7 @@ public class GeneticDecoder {
}
return null;
return OperationScheduleResult.success(null);
}
......@@ -1391,7 +1507,7 @@ public class GeneticDecoder {
// ", 前处理: " + preTime + ", 换型: " + setupTime + ", 数量: " + operation.getQuantity() + ", 设备: " + machine.getId() + ", 是否可中断: " + operation.getIsInterrupt());
}
return null;
return OperationScheduleResult.success(null);
}
if (startTime == 0 && islockMachineTime) {
......@@ -1446,7 +1562,7 @@ public class GeneticDecoder {
// + ", groupId=" + operation.getGroupId() + ", machineId=" + machineId
// + ", hasBOM=" + commitMaterialCheck + ", 耗时=" + fmtMs(pwsElapsed));
// }
return result;
return OperationScheduleResult.success(result);
}
private GAScheduleResult CreateResult(Entry operation,long machineId
......@@ -1629,6 +1745,40 @@ if(geneDetails!=null&&geneDetails.size()>0)
.orElse(null);
}
/**
* 单道序排程结果。
* scheduleResult 用于正常推进订单进度;forwardFallbackRequired 用于把“整单转正排”显式带回外层。
*/
private static class OperationScheduleResult {
private final GAScheduleResult scheduleResult;
private final boolean forwardFallbackRequired;
private OperationScheduleResult(GAScheduleResult scheduleResult, boolean forwardFallbackRequired) {
this.scheduleResult = scheduleResult;
this.forwardFallbackRequired = forwardFallbackRequired;
}
private static OperationScheduleResult success(GAScheduleResult scheduleResult) {
return new OperationScheduleResult(scheduleResult, false);
}
private static OperationScheduleResult forwardFallback() {
return new OperationScheduleResult(null, true);
}
private GAScheduleResult getScheduleResult() {
return scheduleResult;
}
private int getEndTime() {
return scheduleResult == null ? 0 : scheduleResult.getEndTime();
}
private boolean isForwardFallbackRequired() {
return forwardFallbackRequired;
}
}
private static class MachineSchedulePreview {
private final int startTime;
private final int endTime;
......@@ -1959,7 +2109,12 @@ if(geneDetails!=null&&geneDetails.size()>0)
.findFirst()
.orElse(null);
// 缓存机器任务
processWithSingleMachine(currentOp, machine, processTime, prevtime,opMachine, chromosome,true,prevendtime,machineTasksCache, entryIndexById, scheduleIndexById,true,true);
OperationScheduleResult operationResult = processWithSingleMachine(currentOp, machine, processTime,
prevtime, opMachine, chromosome, true, prevendtime, machineTasksCache, entryIndexById,
scheduleIndexById, true, true);
if (operationResult.isForwardFallbackRequired()) {
return;
}
}
......
......@@ -72,6 +72,202 @@ public class MachineCalculator {
}
// 基于已选可用时段组装排程结果,并把换型段/加工段对应的 availability 临时占用。
// ==================== 倒排换型排程辅助方法开始 ====================
/**
* 倒排生成排程明细:从 latestEndTime 往前找加工可用段,可选地在加工段前拼上换型段。
* 返回值沿用老结构:1=换型时间,2=ScheduleResultDetail 列表。
*/
public Map<Integer,Object> CreateScheduleResultBackward(
Machine machine, Entry operation, int processingTime, int latestEndTime,
double oneTime, double quantity, int changeOverTime, int setupStartTime,
int setupEndTime, CopyOnWriteArrayList<TimeSegment> setupSegments,
boolean setupInCalendarTime) {
Map<Integer,Object> result = new HashMap<>(2);
CopyOnWriteArrayList<ScheduleResultDetail> times = new CopyOnWriteArrayList<>();
CopyOnWriteArrayList<TimeSegment> usedSegments = new CopyOnWriteArrayList<>();
times.addAll(CreateSetupDetailsBackward(machine, changeOverTime, setupStartTime, setupEndTime,
setupSegments, setupInCalendarTime));
LocalDateTime cursor = baseTime.plusSeconds(latestEndTime);
int remainingTime = processingTime;
final LocalDateTime processSearchEnd = cursor;
List<TimeSegment> processSegments = machine.getAvailability().stream()
.filter(slot -> !slot.isUsed())
.filter(slot -> slot.getType() != SegmentType.MAINTENANCE)
.filter(slot -> slot.getStart().isBefore(processSearchEnd))
.sorted(Comparator.comparing(TimeSegment::getEnd).reversed())
.collect(Collectors.toList());
double totalAvailableSeconds = calculateTotalAvailableSecond(
new CopyOnWriteArrayList<>(processSegments), processSearchEnd, true);
if (totalAvailableSeconds + 0.0001 < processingTime) {
throw new IllegalStateException(buildInsufficientScheduleMessage(machine, operation,
processingTime, totalAvailableSeconds, processSegments.size()));
}
for (TimeSegment slot : processSegments) {
if (remainingTime <= 0) {
break;
}
LocalDateTime effectiveEnd = slot.getEnd().isAfter(cursor) ? cursor : slot.getEnd();
if (!slot.getStart().isBefore(effectiveEnd)) {
continue;
}
long availableSeconds = ChronoUnit.SECONDS.between(slot.getStart(), effectiveEnd);
long effectiveSeconds = Math.round(availableSeconds * slot.getEfficiency());
if (effectiveSeconds <= 0) {
continue;
}
int processable = Math.min(remainingTime, (int) effectiveSeconds);
int calendarSeconds = (int) Math.ceil(processable / slot.getEfficiency());
LocalDateTime effectiveStart = effectiveEnd.minusSeconds(calendarSeconds);
ScheduleResultDetail time = createScheduleDetail(slot, effectiveStart, effectiveEnd,
processable, oneTime, quantity, false);
times.add(time);
CopyOnWriteArrayList<TimeSegment> usedSegments1 = RemoveMachineAvailable(machine, time, slot);
if (usedSegments1 != null && usedSegments1.size() > 0) {
usedSegments.addAll(usedSegments1);
}
remainingTime -= processable;
cursor = effectiveStart;
}
if (remainingTime > 0) {
throw new IllegalStateException(buildInsufficientScheduleMessage(machine, operation,
processingTime, totalAvailableSeconds, processSegments.size()));
}
if (usedSegments != null && usedSegments.size() > 0) {
machine.getAvailability().addAll(usedSegments);
}
machine.getAvailability().sort(Comparator.comparing(TimeSegment::getStart));
times.sort(Comparator.comparingInt(ScheduleResultDetail::getStartTime));
result.put(1, changeOverTime);
result.put(2, times);
return result;
}
/**
* 生成倒排换型明细,并按配置决定换型是否占用设备日历工作段。
*/
public CopyOnWriteArrayList<ScheduleResultDetail> CreateSetupDetailsBackward(
Machine machine, int changeOverTime, int setupStartTime, int setupEndTime,
CopyOnWriteArrayList<TimeSegment> setupSegments, boolean setupInCalendarTime) {
CopyOnWriteArrayList<ScheduleResultDetail> times = new CopyOnWriteArrayList<>();
if (setupInCalendarTime && changeOverTime > 0 && setupEndTime > setupStartTime) {
ScheduleResultDetail setupDetail = new ScheduleResultDetail();
setupDetail.setKey(UUID.randomUUID().toString());
setupDetail.setStartTime(setupStartTime);
setupDetail.setEndTime(setupEndTime);
setupDetail.setSetup(true);
if (setupSegments != null && setupSegments.size() > 0) {
List<TimeSegment> occupiedSetupSegments = new ArrayList<>();
for (TimeSegment setupSegment : setupSegments) {
ScheduleResultDetail occupiedSetupDetail = occupySetupSegment(machine, setupSegment);
if (occupiedSetupDetail != null && occupiedSetupDetail.getKey() != null) {
TimeSegment keyRef = new TimeSegment();
keyRef.setKey(occupiedSetupDetail.getKey());
keyRef.setStart(baseTime.plusSeconds(occupiedSetupDetail.getStartTime()));
keyRef.setEnd(baseTime.plusSeconds(occupiedSetupDetail.getEndTime()));
occupiedSetupSegments.add(keyRef);
}
}
setupDetail.setUsedSegment(occupiedSetupSegments);
}
times.add(setupDetail);
} else if (setupSegments != null && setupSegments.size() > 0) {
for (TimeSegment setupSegment : setupSegments) {
ScheduleResultDetail setupDetail = occupySetupSegment(machine, setupSegment);
if (setupDetail != null) {
times.add(setupDetail);
}
}
} else if (changeOverTime > 0 && setupEndTime > setupStartTime) {
ScheduleResultDetail setupDetail = new ScheduleResultDetail();
setupDetail.setStartTime(setupStartTime);
setupDetail.setEndTime(setupEndTime);
setupDetail.setSetup(true);
times.add(setupDetail);
}
if (machine != null && machine.getAvailability() != null) {
machine.getAvailability().sort(Comparator.comparing(TimeSegment::getStart));
}
return times;
}
private ScheduleResultDetail occupySetupSegment(Machine machine, TimeSegment setupSegment) {
if (machine == null || setupSegment == null || !setupSegment.getStart().isBefore(setupSegment.getEnd())) {
return null;
}
TimeSegment targetSegment = machine.getAvailability().stream()
.filter(slot -> !slot.isUsed())
.filter(slot -> slot.getType() != SegmentType.MAINTENANCE)
.filter(slot -> slot.getStart().compareTo(setupSegment.getStart()) <= 0)
.filter(slot -> slot.getEnd().compareTo(setupSegment.getEnd()) >= 0)
.findFirst()
.orElse(null);
if (targetSegment == null) {
return null;
}
ScheduleResultDetail setupDetail = new ScheduleResultDetail();
setupDetail.setStartTime((int) ChronoUnit.SECONDS.between(baseTime, setupSegment.getStart()));
setupDetail.setEndTime((int) ChronoUnit.SECONDS.between(baseTime, setupSegment.getEnd()));
setupDetail.setSetup(true);
setupDetail.setKey(targetSegment.getKey());
CopyOnWriteArrayList<TimeSegment> usedSegments = RemoveMachineAvailable(machine, setupDetail, targetSegment);
if (usedSegments != null && usedSegments.size() > 0) {
machine.getAvailability().addAll(usedSegments);
}
return setupDetail;
}
private ScheduleResultDetail createScheduleDetail(TimeSegment slot, LocalDateTime startTime,
LocalDateTime endTime, int processable,
double oneTime, double quantity,
boolean useWholeQuantity) {
ScheduleResultDetail time = new ScheduleResultDetail();
time.setKey(slot.getKey());
time.setStartTime((int) ChronoUnit.SECONDS.between(baseTime, startTime));
time.setEndTime((int) ChronoUnit.SECONDS.between(baseTime, endTime));
time.setEfficiency(slot.getEfficiency());
time.setOneTime(oneTime);
if (useWholeQuantity) {
time.setQuantity(quantity);
} else if (oneTime > 0) {
BigDecimal bd = new BigDecimal(String.valueOf(processable / oneTime));
time.setQuantity(bd.setScale(4, RoundingMode.HALF_UP).doubleValue());
}
return time;
}
private String buildInsufficientScheduleMessage(Machine machine, Entry operation,
int requiredSeconds, double availableSeconds, int segmentCount) {
String machineId = machine == null ? "null" : String.valueOf(machine.getId());
String machineName = machine == null ? "" : String.valueOf(machine.getName());
String operationId = operation == null ? "null" : String.valueOf(operation.getId());
String groupId = operation == null ? "null" : String.valueOf(operation.getGroupId());
return String.format("设备可用时间不足,machineId=%s, machineName=%s, operationId=%s, groupId=%s, requiredSeconds=%d, availableSeconds=%.0f, segmentCount=%d",
machineId, machineName, operationId, groupId, requiredSeconds, availableSeconds, segmentCount);
}
// ==================== 倒排换型排程辅助方法结束 ====================
public Map<Integer,Object> CreateScheduleResult(
Machine machine,Entry operation, int processingTime, int proposedStartTime,CopyOnWriteArrayList<TimeSegment> timeSegments,
double oneTime,double quantity
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment