Commit 8312e0fa by Ren Ping

feat:移除不满足工程师时间窗强约束的订单

1 parent f83b0d31
...@@ -76,6 +76,9 @@ public class DispatchConstraintProvider implements ConstraintProvider { ...@@ -76,6 +76,9 @@ public class DispatchConstraintProvider implements ConstraintProvider {
if (!in) { if (!in) {
// 到达时间在日历窗口外,惩罚得分 // 到达时间在日历窗口外,惩罚得分
ret = true; ret = true;
customer.setInTechnicianTimeWindows(false);
} else {
customer.setInTechnicianTimeWindows(true);
} }
} }
} }
...@@ -93,18 +96,18 @@ public class DispatchConstraintProvider implements ConstraintProvider { ...@@ -93,18 +96,18 @@ public class DispatchConstraintProvider implements ConstraintProvider {
.asConstraint(ConstraintNameEnum.technicianCapacityMatch.name()); .asConstraint(ConstraintNameEnum.technicianCapacityMatch.name());
} }
protected Constraint dispatchedMatch(ConstraintFactory factory) { protected Constraint dispatchedMatch(ConstraintFactory factory) {
return factory.forEach(Customer.class).filter(customer -> return factory.forEach(Customer.class).filter(customer ->
// 已分配但是分给了别人 // 已分配但是分给了别人
(customer.getDispatchedTechnicianCode() != null && customer.getTechnician() != null (customer.getDispatchedTechnicianCode() != null && customer.getTechnician() != null
&& !StringUtils.equals(customer.getDispatchedTechnicianCode(), customer.getTechnician().getCode())) || && !StringUtils.equals(customer.getDispatchedTechnicianCode(), customer.getTechnician().getCode())) ||
// 已排除但是分给了这个人 // 已排除但是分给了这个人
(customer.getExclusiveTechnicianCode() != null && customer.getTechnician() != null (customer.getExclusiveTechnicianCode() != null && customer.getTechnician() != null
&& StringUtils.equals(customer.getExclusiveTechnicianCode(), && StringUtils.equals(customer.getExclusiveTechnicianCode(),
customer.getTechnician().getCode()))) customer.getTechnician().getCode())))
.penalizeLong(HardSoftLongScore.ONE_HARD, customer -> 50) .penalizeLong(HardSoftLongScore.ONE_HARD, customer -> 50)
.asConstraint(ConstraintNameEnum.dispatchedMatch.name()); .asConstraint(ConstraintNameEnum.dispatchedMatch.name());
} }
// protected Constraint customerTimeWindowsMatch1(ConstraintFactory factory) { // protected Constraint customerTimeWindowsMatch1(ConstraintFactory factory) {
// return factory.forEach(Customer.class).filter( // return factory.forEach(Customer.class).filter(
......
...@@ -21,12 +21,12 @@ public class Customer { ...@@ -21,12 +21,12 @@ public class Customer {
private long id; private long id;
private String code; private String code;
// 已分配 // 已分配
private String dispatchedTechnicianCode; private String dispatchedTechnicianCode;
// 已排除 // 已排除
private String exclusiveTechnicianCode; private String exclusiveTechnicianCode;
// orderid(code)+dt 确定唯一一条工单 // orderid(code)+dt 确定唯一一条工单
private String dt; private String dt;
@JsonIgnore @JsonIgnore
...@@ -50,6 +50,8 @@ public class Customer { ...@@ -50,6 +50,8 @@ public class Customer {
// 离开时间 // 离开时间
// private Integer departureTime; // private Integer departureTime;
private boolean isInTechnicianTimeWindows = true;
public Customer() { public Customer() {
} }
...@@ -135,9 +137,9 @@ public class Customer { ...@@ -135,9 +137,9 @@ public class Customer {
// throw new IllegalStateException("This method must not be called when the shadow variables are not initialized yet."); // throw new IllegalStateException("This method must not be called when the shadow variables are not initialized yet.");
} }
if (previousCustomer == null) { if (previousCustomer == null) {
return technician.getDepot().getLocation().getDistanceTo(technician.getVehicleType(),location); return technician.getDepot().getLocation().getDistanceTo(technician.getVehicleType(), location);
} }
return previousCustomer.getLocation().getDistanceTo(technician.getVehicleType(),location); return previousCustomer.getLocation().getDistanceTo(technician.getVehicleType(), location);
} }
/** /**
...@@ -151,9 +153,9 @@ public class Customer { ...@@ -151,9 +153,9 @@ public class Customer {
// throw new IllegalStateException("This method must not be called when the shadow variables are not initialized yet."); // throw new IllegalStateException("This method must not be called when the shadow variables are not initialized yet.");
} }
if (previousCustomer == null) { if (previousCustomer == null) {
return technician.getDepot().getLocation().getPathTimeTo(technician.getVehicleType(),location); return technician.getDepot().getLocation().getPathTimeTo(technician.getVehicleType(), location);
} }
return previousCustomer.getLocation().getPathTimeTo(technician.getVehicleType(),location); return previousCustomer.getLocation().getPathTimeTo(technician.getVehicleType(), location);
} }
@Override @Override
......
package com.dituhui.pea.dispatch.quartz.dispatch; package com.dituhui.pea.dispatch.quartz.dispatch;
import com.dituhui.pea.dispatch.common.RedissonUtil;
import com.dituhui.pea.dispatch.service.SchedulerService; import com.dituhui.pea.dispatch.service.SchedulerService;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import org.quartz.DisallowConcurrentExecution; import org.quartz.DisallowConcurrentExecution;
...@@ -23,7 +24,7 @@ import javax.annotation.Resource; ...@@ -23,7 +24,7 @@ import javax.annotation.Resource;
@DisallowConcurrentExecution @DisallowConcurrentExecution
public class AutoDispatchJob extends QuartzJobBean { public class AutoDispatchJob extends QuartzJobBean {
public static final String TEAM_JOB_PREFIX="BOXI_TEAM_"; public static final String TEAM_JOB_PREFIX = "BOXI_TEAM_";
@Resource @Resource
private SchedulerService schedulerService; private SchedulerService schedulerService;
...@@ -36,6 +37,9 @@ public class AutoDispatchJob extends QuartzJobBean { ...@@ -36,6 +37,9 @@ public class AutoDispatchJob extends QuartzJobBean {
String teamId = name.substring(TEAM_JOB_PREFIX.length()); String teamId = name.substring(TEAM_JOB_PREFIX.length());
long start = System.currentTimeMillis(); long start = System.currentTimeMillis();
log.info(">>> 自动派工(teamId:{}) 自动任务开始", teamId); log.info(">>> 自动派工(teamId:{}) 自动任务开始", teamId);
/*RedissonUtil.lockOperation(AutoDispatchJob.TEAM_JOB_PREFIX + teamId, 60, () -> {
schedulerService.dispatchRun2(teamId);
});*/
schedulerService.dispatchRun2(teamId); schedulerService.dispatchRun2(teamId);
long end = System.currentTimeMillis(); long end = System.currentTimeMillis();
log.info(">>> 自动派工(teamId:{}) 自动任务结束,耗时:{}", teamId, end - start); log.info(">>> 自动派工(teamId:{}) 自动任务结束,耗时:{}", teamId, end - start);
......
...@@ -113,52 +113,45 @@ public class SchedulerServiceImpl implements SchedulerService { ...@@ -113,52 +113,45 @@ public class SchedulerServiceImpl implements SchedulerService {
for (int i = 1; i <= nextDaysLimit; i++) { for (int i = 1; i <= nextDaysLimit; i++) {
String currDay = LocalDate.now().plusDays(i).format(DateTimeFormatter.ISO_LOCAL_DATE); String currDay = LocalDate.now().plusDays(i).format(DateTimeFormatter.ISO_LOCAL_DATE);
boolean finalCutOff = cutOff; Optional<DispatchBatch> optional = dispatchBatchRepository.findByTeamIdAndBatchDate(teamId, currDay);
RedissonUtil.lockOperation(AutoDispatchJob.TEAM_JOB_PREFIX + teamId, 60, () -> { if (optional.isPresent()
dispatchRun2OneDay(teamId, currDay, today, finalCutOff); && Objects.nonNull(optional.get().getCutoffedTime())
}); && DateUtil.format(optional.get().getCutoffedTime(), "yyyy-MM-dd").equals(today)) {
} //自动任务截止
} log.error(">>> teamId:{}, day:{} 自动任务已截止", teamId, currDay);
private void dispatchRun2OneDay(String teamId, String currDay, String today, boolean cutOff) {
Optional<DispatchBatch> optional = dispatchBatchRepository.findByTeamIdAndBatchDate(teamId, currDay);
if (optional.isPresent()
&& Objects.nonNull(optional.get().getCutoffedTime())
&& DateUtil.format(optional.get().getCutoffedTime(), "yyyy-MM-dd").equals(today)) {
//自动任务截止
log.error(">>> teamId:{}, day:{} 自动任务已截止", teamId, currDay);
return;
}
try {
log.info("dispatchRun begin----- teamId:{}, day:{}", teamId, currDay);
String batchNo = batchService.buildBatchData2(teamId, currDay, cutOff);
UUID problemId = solveService.generateProblemId(teamId, batchNo);
log.info("dispatchRun teamId:{}, day:{}, batch:{}, problemId:{}", teamId, currDay, batchNo, problemId);
DispatchSolution problem = solveService.prepareSolution2(teamId, batchNo, currDay);
if (problem.getCustomerList().size() <= 0) {
log.info("dispatchRun 当前批次没有待指派工单 , teamId:{}, day:{}, batch:{}, problemId:{}, order-size:{}", teamId, currDay, batchNo, problemId, problem.getCustomerList().size());
return; return;
} }
log.info("dispatchRun prepare done, teamId:{}, day:{}, batch:{}, problemId:{}", teamId, currDay, batchNo, problemId);
Solver<DispatchSolution> solver = solverFactory.buildSolver(); try {
DispatchSolution solution = solver.solve(problem); log.info("dispatchRun begin----- teamId:{}, day:{}", teamId, currDay);
DispatchSolutionUtils.removeHardConstraintCustomer(solution, solverFactory);
log.info("dispatchRun solve done, teamId:{}, day:{}, batch:{}, problemId:{}, score:{}", teamId, currDay, batchNo, problemId, solution.getScore().toShortString()); String batchNo = batchService.buildBatchData2(teamId, currDay, cutOff);
this.solveService.saveSolutionWrp2(solution); UUID problemId = solveService.generateProblemId(teamId, batchNo);
this.extractService.extractDispatchToOrder2(teamId, batchNo, cutOff); log.info("dispatchRun teamId:{}, day:{}, batch:{}, problemId:{}", teamId, currDay, batchNo, problemId);
log.info("dispatchRun done ------ teamId:{}, day:{}", teamId, currDay);
DispatchSolution problem = solveService.prepareSolution2(teamId, batchNo, currDay);
JacksonSolutionFileIO<DispatchSolution> exporter = new JacksonSolutionFileIO<DispatchSolution>(DispatchSolution.class);
exporter.write(solution, new File(String.format("dispatchSolution_%s_%s.json", teamId, currDay))); if (problem.getCustomerList().size() <= 0) {
log.info("dispatchRun 当前批次没有待指派工单 , teamId:{}, day:{}, batch:{}, problemId:{}, order-size:{}", teamId, currDay, batchNo, problemId, problem.getCustomerList().size());
//log.info("dispatchRun group:{}, team:{} done", groupId, teamId); return;
} catch (Exception e) { }
log.error(">>> (teamId:{}, day:{})自动排班失败:{}", teamId, currDay, e.getMessage(), e); log.info("dispatchRun prepare done, teamId:{}, day:{}, batch:{}, problemId:{}", teamId, currDay, batchNo, problemId);
//throw e; Solver<DispatchSolution> solver = solverFactory.buildSolver();
DispatchSolution solution = solver.solve(problem);
DispatchSolutionUtils.removeHardConstraintCustomer(solution, solverFactory);
log.info("dispatchRun solve done, teamId:{}, day:{}, batch:{}, problemId:{}, score:{}", teamId, currDay, batchNo, problemId, solution.getScore().toShortString());
this.solveService.saveSolutionWrp2(solution);
this.extractService.extractDispatchToOrder2(teamId, batchNo, cutOff);
log.info("dispatchRun done ------ teamId:{}, day:{}", teamId, currDay);
JacksonSolutionFileIO<DispatchSolution> exporter = new JacksonSolutionFileIO<DispatchSolution>(DispatchSolution.class);
exporter.write(solution, new File(String.format("dispatchSolution_%s_%s.json", teamId, currDay)));
//log.info("dispatchRun group:{}, team:{} done", groupId, teamId);
} catch (Exception e) {
log.error(">>> (teamId:{}, day:{})自动排班失败:{}", teamId, currDay, e.getMessage(), e);
//throw e;
}
} }
} }
} }
\ No newline at end of file
...@@ -553,7 +553,6 @@ public class SolveServiceImpl implements SolveService { ...@@ -553,7 +553,6 @@ public class SolveServiceImpl implements SolveService {
// Date end = Date.from(localEndTime.atZone(ZoneId.systemDefault()).toInstant()); // Date end = Date.from(localEndTime.atZone(ZoneId.systemDefault()).toInstant());
log.info("算法结果回写dispatch, step3-逐个客户处理, teamId:{}, batchNo:{}, employ: {}, customer:{}, service-duration:{} ", teamId, batchNo, technician.getCode(), customer.getCode(), customer.getServiceDuration()); log.info("算法结果回写dispatch, step3-逐个客户处理, teamId:{}, batchNo:{}, employ: {}, customer:{}, service-duration:{} ", teamId, batchNo, technician.getCode(), customer.getCode(), customer.getServiceDuration());
log.info(customer.toString());
LocalDateTime customDateTime = LocalDateTime.parse(customer.getDt() + " 00:00:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); LocalDateTime customDateTime = LocalDateTime.parse(customer.getDt() + " 00:00:00", DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
LocalDateTime arriveTime = customDateTime.plusMinutes(customer.getArrivalTime()); LocalDateTime arriveTime = customDateTime.plusMinutes(customer.getArrivalTime());
LocalDateTime leaveTime = customDateTime.plusMinutes(customer.getDepartureTime()); LocalDateTime leaveTime = customDateTime.plusMinutes(customer.getDepartureTime());
......
...@@ -5,13 +5,12 @@ import static java.util.Comparator.comparing; ...@@ -5,13 +5,12 @@ import static java.util.Comparator.comparing;
import java.io.File; import java.io.File;
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.util.Arrays; import java.util.*;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ObjectUtil;
import org.apache.commons.io.IOUtils; import org.apache.commons.io.IOUtils;
import org.apache.commons.lang3.StringUtils; import org.apache.commons.lang3.StringUtils;
import org.optaplanner.constraint.streams.drools.DroolsConstraintStreamScoreDirector; import org.optaplanner.constraint.streams.drools.DroolsConstraintStreamScoreDirector;
...@@ -38,423 +37,442 @@ import com.dituhui.pea.dispatch.pojo.Technician; ...@@ -38,423 +37,442 @@ import com.dituhui.pea.dispatch.pojo.Technician;
public class DispatchSolutionUtils { public class DispatchSolutionUtils {
/** /**
* 打印方案 * 打印方案
* *
* @param solution * @param solution
*/ */
public static void printSolution(DispatchSolution solution) { public static void printSolution(DispatchSolution solution) {
System.out.println("Score: " + solution.getScore().toShortString()); System.out.println("Score: " + solution.getScore().toShortString());
System.out.println("技能约束:"); System.out.println("技能约束:");
solution.getTechnicianList().forEach(technician -> { solution.getTechnicianList().forEach(technician -> {
System.out.printf("技术员%s(%s) %s%n", technician.getId(), technician.getCode(), technician.getSkills()); System.out.printf("技术员%s(%s) %s%n", technician.getId(), technician.getCode(), technician.getSkills());
for (Customer customer : technician.getCustomerList()) { for (Customer customer : technician.getCustomerList()) {
if (!technician.getSkills().contains(customer.getRequiredSkill())) { if (!technician.getSkills().contains(customer.getRequiredSkill())) {
// no match // no match
System.err.printf(" 预约单%s(%s) %s%n", customer.getId(), customer.getCode(), System.err.printf(" 预约单%s(%s) %s%n", customer.getId(), customer.getCode(),
customer.getRequiredSkill()); customer.getRequiredSkill());
} else { } else {
System.out.printf(" 预约单%s(%s) %s%n", customer.getId(), customer.getCode(), System.out.printf(" 预约单%s(%s) %s%n", customer.getId(), customer.getCode(),
customer.getRequiredSkill()); customer.getRequiredSkill());
} }
} }
}); });
AtomicInteger totalNum = new AtomicInteger(0); AtomicInteger totalNum = new AtomicInteger(0);
solution.getTechnicianList().forEach(technician -> { solution.getTechnicianList().forEach(technician -> {
System.out.printf("技术员%s(%s) [%s,%s]%n", technician.getId(), technician.getCode(), System.out.printf("技术员%s(%s) [%s,%s]%n", technician.getId(), technician.getCode(),
printTime(technician.getTimeWindows()[0][0]), printTime(technician.getTimeWindows()[0][1])); printTime(technician.getTimeWindows()[0][0]), printTime(technician.getTimeWindows()[0][1]));
totalNum.addAndGet(technician.getCustomerList().size()); totalNum.addAndGet(technician.getCustomerList().size());
for (Customer customer : technician.getCustomerList()) { for (Customer customer : technician.getCustomerList()) {
Customer previousCustomer = customer.getPreviousCustomer(); Customer previousCustomer = customer.getPreviousCustomer();
int startPath, endPath;// 路上时间 int startPath, endPath;// 路上时间
if (null == previousCustomer) { if (null == previousCustomer) {
startPath = technician.getDepot().getStartTime(); startPath = technician.getDepot().getStartTime();
// endPath = startPath + // endPath = startPath +
// customer.getLocation().getPathTimeTo(technician.getDepot().getLocation()); // customer.getLocation().getPathTimeTo(technician.getDepot().getLocation());
endPath = startPath + technician.getDepot().getLocation().getPathTimeTo(technician.getVehicleType(),customer.getLocation()); endPath = startPath + technician.getDepot().getLocation().getPathTimeTo(technician.getVehicleType(), customer.getLocation());
} else { } else {
startPath = previousCustomer.getDepartureTime(); startPath = previousCustomer.getDepartureTime();
// endPath = startPath + // endPath = startPath +
// customer.getLocation().getPathTimeTo(previousCustomer.getLocation()); // customer.getLocation().getPathTimeTo(previousCustomer.getLocation());
endPath = startPath + previousCustomer.getLocation().getPathTimeTo(technician.getVehicleType(),customer.getLocation()); endPath = startPath + previousCustomer.getLocation().getPathTimeTo(technician.getVehicleType(), customer.getLocation());
} }
if (customer.getArrivalTime() > customer.getEndTime()) { if (customer.getArrivalTime() > customer.getEndTime()) {
// 迟到 // 迟到
System.err.printf( System.err.printf(
" 预约单%s(%s) 预约时间窗[%s=>%s] 路上时间[%s=>%s] 早到等待时间[%s=>%s] 派工时间[%s=>%s] 迟到时间[%s=>%s]%n", " 预约单%s(%s) 预约时间窗[%s=>%s] 路上时间[%s=>%s] 早到等待时间[%s=>%s] 派工时间[%s=>%s] 迟到时间[%s=>%s]%n",
customer.getId(), customer.getCode(), customer.getId(), customer.getCode(),
// 预约时间窗 // 预约时间窗
printTime(customer.getStartTime()), printTime(customer.getEndTime()), printTime(customer.getStartTime()), printTime(customer.getEndTime()),
// 路上时间 // 路上时间
printTime(startPath), printTime(endPath), printTime(startPath), printTime(endPath),
// 早到等待时间 // 早到等待时间
customer.getArrivalTime() < customer.getStartTime() ? printTime(endPath) : "", customer.getArrivalTime() < customer.getStartTime() ? printTime(endPath) : "",
customer.getArrivalTime() < customer.getStartTime() ? printTime(customer.getStartTime()) customer.getArrivalTime() < customer.getStartTime() ? printTime(customer.getStartTime())
: "", : "",
// 派工时间 // 派工时间
printTime(customer.getArrivalTime()), printTime(customer.getDepartureTime()), printTime(customer.getArrivalTime()), printTime(customer.getDepartureTime()),
// 迟到时间 // 迟到时间
printTime(customer.getEndTime()), printTime(customer.getArrivalTime())); printTime(customer.getEndTime()), printTime(customer.getArrivalTime()));
} else { } else {
System.out.printf( System.out.printf(
" 预约单%s(%s) 预约时间窗[%s=>%s] 路上时间[%s=>%s] 早到等待时间[%s=>%s] 派工时间[%s=>%s] 迟到时间[%s=>%s]%n", " 预约单%s(%s) 预约时间窗[%s=>%s] 路上时间[%s=>%s] 早到等待时间[%s=>%s] 派工时间[%s=>%s] 迟到时间[%s=>%s]%n",
customer.getId(), customer.getCode(), customer.getId(), customer.getCode(),
// 预约时间窗 // 预约时间窗
printTime(customer.getStartTime()), printTime(customer.getEndTime()), printTime(customer.getStartTime()), printTime(customer.getEndTime()),
// 路上时间 // 路上时间
printTime(startPath), printTime(endPath), printTime(startPath), printTime(endPath),
// 早到等待时间 // 早到等待时间
customer.getArrivalTime() < customer.getStartTime() ? printTime(endPath) : "", customer.getArrivalTime() < customer.getStartTime() ? printTime(endPath) : "",
customer.getArrivalTime() < customer.getStartTime() ? printTime(customer.getStartTime()) customer.getArrivalTime() < customer.getStartTime() ? printTime(customer.getStartTime())
: "", : "",
// 派工时间 // 派工时间
printTime(customer.getArrivalTime()), printTime(customer.getDepartureTime()), printTime(customer.getArrivalTime()), printTime(customer.getDepartureTime()),
// 迟到时间 // 迟到时间
"", ""); "", "");
} }
} }
}); });
} }
private static String printTime(int startTime) { private static String printTime(int startTime) {
int hour = startTime / 60; int hour = startTime / 60;
int minite = startTime % 60; int minite = startTime % 60;
return StringUtils.leftPad("" + hour, 2, '0') + ":" + StringUtils.leftPad("" + minite, 2, '0'); return StringUtils.leftPad("" + hour, 2, '0') + ":" + StringUtils.leftPad("" + minite, 2, '0');
} }
/** /**
* 生成方案地图页 * 生成方案地图页
* *
* @param solution * @param solution
* @param filename * @param filename
*/ */
public static void exportMapHtml(DispatchSolution solution, String filename) { public static void exportMapHtml(DispatchSolution solution, String filename) {
try { try {
// 仓库起点 // 仓库起点
String depot = "\"" + solution.getDepot().getLocation().getX() + "," String depot = "\"" + solution.getDepot().getLocation().getX() + ","
+ solution.getDepot().getLocation().getY() + "\""; + solution.getDepot().getLocation().getY() + "\"";
// 技术员路线 // 技术员路线
String lines_ = "["; String lines_ = "[";
for (Technician technician : solution.getTechnicianList()) { for (Technician technician : solution.getTechnicianList()) {
if (technician.getCustomerList().size() > 0) { if (technician.getCustomerList().size() > 0) {
lines_ += "\"" + technician.getCustomerList().stream() lines_ += "\"" + technician.getCustomerList().stream()
.map(c -> c.getLocation().getX() + "," + c.getLocation().getY()) .map(c -> c.getLocation().getX() + "," + c.getLocation().getY())
.reduce((a, b) -> a + ";" + b).get() + "\","; .reduce((a, b) -> a + ";" + b).get() + "\",";
} }
} }
lines_ += "]"; lines_ += "]";
final String lines = lines_; final String lines = lines_;
// 技术员偏好中心点 // 技术员偏好中心点
String preferredlocation = "\"" + solution.getTechnicianList().stream() String preferredlocation = "\"" + solution.getTechnicianList().stream()
.map(c -> (c.getPreferredlocation() == null ? "" : c.getPreferredlocation().getX()) + "," .map(c -> (c.getPreferredlocation() == null ? "" : c.getPreferredlocation().getX()) + ","
+ (c.getPreferredlocation() == null ? "" : c.getPreferredlocation().getY())) + (c.getPreferredlocation() == null ? "" : c.getPreferredlocation().getY()))
.reduce((a, b) -> a + ";" + b).get() + "\""; .reduce((a, b) -> a + ";" + b).get() + "\"";
// 技术员名称 // 技术员名称
String names = "\"" + solution.getTechnicianList().stream() String names = "\"" + solution.getTechnicianList().stream()
.map(c -> (c.getPreferredlocation() == null ? "" : c.getPreferredlocation().getX()) + "," .map(c -> (c.getPreferredlocation() == null ? "" : c.getPreferredlocation().getX()) + ","
+ (c.getPreferredlocation() == null ? "" : c.getPreferredlocation().getY()) + "," + (c.getPreferredlocation() == null ? "" : c.getPreferredlocation().getY()) + ","
+ c.getCode()) + c.getCode())
.reduce((a, b) -> a + ";" + b).get() + "\""; .reduce((a, b) -> a + ";" + b).get() + "\"";
List<String> dispatchMapLines = IOUtils.readLines(new FileInputStream("data/dispatchMap.html"), "GBK"); List<String> dispatchMapLines = IOUtils.readLines(new FileInputStream("data/dispatchMap.html"), "GBK");
dispatchMapLines = dispatchMapLines.stream().map(line -> { dispatchMapLines = dispatchMapLines.stream().map(line -> {
if (StringUtils.startsWith(line, " var depot = ")) { if (StringUtils.startsWith(line, " var depot = ")) {
return " var depot = " + depot; return " var depot = " + depot;
} else if (StringUtils.startsWith(line, " var preferredlocation = ")) { } else if (StringUtils.startsWith(line, " var preferredlocation = ")) {
return " var preferredlocation = " + preferredlocation; return " var preferredlocation = " + preferredlocation;
} else if (StringUtils.startsWith(line, " var lines = ")) { } else if (StringUtils.startsWith(line, " var lines = ")) {
return " var lines = " + lines; return " var lines = " + lines;
} else if (StringUtils.startsWith(line, " var names = ")) { } else if (StringUtils.startsWith(line, " var names = ")) {
return " var names = " + names; return " var names = " + names;
} else{ } else {
return line; return line;
} }
}).collect(Collectors.toList()); }).collect(Collectors.toList());
IOUtils.writeLines(dispatchMapLines, "\r\n", new FileOutputStream(filename + ".html"), "GBK"); IOUtils.writeLines(dispatchMapLines, "\r\n", new FileOutputStream(filename + ".html"), "GBK");
System.out.println("output map : " + filename); System.out.println("output map : " + filename);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace(); e.printStackTrace();
} }
} }
/** /**
* 计算方案分数 * 计算方案分数
* *
* @param solution * @param solution
* @return * @return
*/ */
public static HardSoftLongScore getScore(DispatchSolution solution) { public static HardSoftLongScore getScore(DispatchSolution solution) {
DefaultSolverFactory<DispatchSolution> solverFactory = getSolverFactory(); DefaultSolverFactory<DispatchSolution> solverFactory = getSolverFactory();
ScoreDirectorFactory<DispatchSolution> scoreDirectorFactory = solverFactory.getScoreDirectorFactory(); ScoreDirectorFactory<DispatchSolution> scoreDirectorFactory = solverFactory.getScoreDirectorFactory();
DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore> scoreDirector = (DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore>) scoreDirectorFactory DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore> scoreDirector = (DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore>) scoreDirectorFactory
.buildScoreDirector(); .buildScoreDirector();
// Set the working solution of the ScoreDirector object. // Set the working solution of the ScoreDirector object.
scoreDirector.setWorkingSolution(solution); scoreDirector.setWorkingSolution(solution);
// Iterate over the constraints in your problem and call the // Iterate over the constraints in your problem and call the
// `addConstraintMatch()` method // `addConstraintMatch()` method
// for each constraint that is violated by the solution. // for each constraint that is violated by the solution.
// for (Constraint constraint : myConstraints) { // for (Constraint constraint : myConstraints) {
// if (constraint.isViolated(mySolution)) { // if (constraint.isViolated(mySolution)) {
// scoreDirector.addConstraintMatch(constraint, constraint.getScore()); // scoreDirector.addConstraintMatch(constraint, constraint.getScore());
// } // }
// } // }
// Calculate the score of the solution. // Calculate the score of the solution.
HardSoftLongScore score = (HardSoftLongScore) scoreDirector.calculateScore(); HardSoftLongScore score = (HardSoftLongScore) scoreDirector.calculateScore();
return score; return score;
} }
/** /**
* 移动某个技术员的订单,返回新方案<br> * 移动某个技术员的订单,返回新方案<br>
* FIXME 没有成功 * FIXME 没有成功
* *
* @param solution * @param solution
* @param source 技术员 * @param source 技术员
* @param sourceIndex 0开始 * @param sourceIndex 0开始
* @param destinationIndex 0开始 * @param destinationIndex 0开始
* @return * @return
*/ */
public static DispatchSolution moveCustomer(DefaultSolverFactory<DispatchSolution> solverFactory, public static DispatchSolution moveCustomer(DefaultSolverFactory<DispatchSolution> solverFactory,
DispatchSolution solution, Technician source, int sourceIndex, int destinationIndex) { DispatchSolution solution, Technician source, int sourceIndex, int destinationIndex) {
if (null == solverFactory) { if (null == solverFactory) {
solverFactory = getSolverFactory(); solverFactory = getSolverFactory();
} }
ScoreDirectorFactory<DispatchSolution> scoreDirectorFactory = solverFactory.getScoreDirectorFactory(); ScoreDirectorFactory<DispatchSolution> scoreDirectorFactory = solverFactory.getScoreDirectorFactory();
DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore> scoreDirector = (DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore>) scoreDirectorFactory DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore> scoreDirector = (DroolsConstraintStreamScoreDirector<DispatchSolution, HardSoftLongScore>) scoreDirectorFactory
.buildScoreDirector(); .buildScoreDirector();
// Make a change to the solution. // Make a change to the solution.
// doMove(), doSwap(), or doChangeVariable() // doMove(), doSwap(), or doChangeVariable()
// Create a variable descriptor for the "customerList" list property // Create a variable descriptor for the "customerList" list property
ListVariableDescriptor<DispatchSolution> customerDescriptor = (ListVariableDescriptor<DispatchSolution>) scoreDirector ListVariableDescriptor<DispatchSolution> customerDescriptor = (ListVariableDescriptor<DispatchSolution>) scoreDirector
.getSolutionDescriptor().getEntityDescriptorStrict(Technician.class) .getSolutionDescriptor().getEntityDescriptorStrict(Technician.class)
.getVariableDescriptor("customerList"); .getVariableDescriptor("customerList");
// Create the ListChangeMove // Create the ListChangeMove
ListChangeMove<DispatchSolution> move = new ListChangeMove<DispatchSolution>(customerDescriptor, source, ListChangeMove<DispatchSolution> move = new ListChangeMove<DispatchSolution>(customerDescriptor, source,
sourceIndex, source, destinationIndex); sourceIndex, source, destinationIndex);
// apply the move and re-evaluate the score // apply the move and re-evaluate the score
scoreDirector.doAndProcessMove(move, true); scoreDirector.doAndProcessMove(move, true);
return scoreDirector.getWorkingSolution(); return scoreDirector.getWorkingSolution();
} }
public static DefaultSolverFactory<DispatchSolution> getSolverFactory() { public static DefaultSolverFactory<DispatchSolution> getSolverFactory() {
return getSolverFactory(0, 0); return getSolverFactory(0, 0);
} }
public static DefaultSolverFactory<DispatchSolution> getSolverFactory(long unimprovedSecondsSpentLimit, public static DefaultSolverFactory<DispatchSolution> getSolverFactory(long unimprovedSecondsSpentLimit,
long secondsSpentLimit) { long secondsSpentLimit) {
// 创建求解器配置 // 创建求解器配置
// 创建 SolverConfig 对象,并设置求解器配置 // 创建 SolverConfig 对象,并设置求解器配置
SolverConfig solverConfig = new SolverConfig(); SolverConfig solverConfig = new SolverConfig();
solverConfig.setSolutionClass(DispatchSolution.class); solverConfig.setSolutionClass(DispatchSolution.class);
solverConfig.withEntityClassList(Arrays.asList(Technician.class, Customer.class));// 这里不能漏掉,否则约束不生效 solverConfig.withEntityClassList(Arrays.asList(Technician.class, Customer.class));// 这里不能漏掉,否则约束不生效
TerminationConfig terminationConfig = new TerminationConfig(); TerminationConfig terminationConfig = new TerminationConfig();
terminationConfig terminationConfig
.setUnimprovedSecondsSpentLimit(unimprovedSecondsSpentLimit <= 0 ? 5 : unimprovedSecondsSpentLimit);// XX秒没有找到更好方案 .setUnimprovedSecondsSpentLimit(unimprovedSecondsSpentLimit <= 0 ? 5 : unimprovedSecondsSpentLimit);// XX秒没有找到更好方案
terminationConfig.setSecondsSpentLimit(secondsSpentLimit <= 0 ? 60 : secondsSpentLimit);// 总时间不能超过XXs terminationConfig.setSecondsSpentLimit(secondsSpentLimit <= 0 ? 60 : secondsSpentLimit);// 总时间不能超过XXs
solverConfig.withTerminationConfig(terminationConfig); solverConfig.withTerminationConfig(terminationConfig);
// 约束条件 // 约束条件
solverConfig.withConstraintProviderClass(DispatchConstraintProvider.class); solverConfig.withConstraintProviderClass(DispatchConstraintProvider.class);
solverConfig.setScoreDirectorFactoryConfig( solverConfig.setScoreDirectorFactoryConfig(
new ScoreDirectorFactoryConfig().withConstraintProviderClass(DispatchConstraintProvider.class)); new ScoreDirectorFactoryConfig().withConstraintProviderClass(DispatchConstraintProvider.class));
DefaultSolverFactory<DispatchSolution> solverFactory = (DefaultSolverFactory<DispatchSolution>) SolverFactory DefaultSolverFactory<DispatchSolution> solverFactory = (DefaultSolverFactory<DispatchSolution>) SolverFactory
.<DispatchSolution>create(solverConfig); .<DispatchSolution>create(solverConfig);
return solverFactory; return solverFactory;
} }
/** /**
* 打印详细约束得分 * 打印详细约束得分
* *
* @param solution * @param solution
* @param solverFactory * @param solverFactory
*/ */
public static void explainSolutionConstraintDetail(DispatchSolution solution, DefaultSolverFactory<DispatchSolution> solverFactory) { public static void explainSolutionConstraintDetail(DispatchSolution solution, DefaultSolverFactory<DispatchSolution> solverFactory) {
// Obtain a ScoreExplanation object for the best solution // Obtain a ScoreExplanation object for the best solution
// Using score calculation outside the Solver // Using score calculation outside the Solver
// https://www.optaplanner.org/docs/optaplanner/latest/score-calculation/score-calculation.html // https://www.optaplanner.org/docs/optaplanner/latest/score-calculation/score-calculation.html
SolutionManager<DispatchSolution, HardSoftLongScore> scoreManager = SolutionManager.create(solverFactory); SolutionManager<DispatchSolution, HardSoftLongScore> scoreManager = SolutionManager.create(solverFactory);
ScoreExplanation<DispatchSolution, HardSoftLongScore> scoreExplanation = scoreManager.explain(solution); ScoreExplanation<DispatchSolution, HardSoftLongScore> scoreExplanation = scoreManager.explain(solution);
// System.out.println(scoreExplanation.getSummary()); // System.out.println(scoreExplanation.getSummary());
Map<String, ConstraintMatchTotal<HardSoftLongScore>> constraintMatchTotalMap = scoreExplanation Map<String, ConstraintMatchTotal<HardSoftLongScore>> constraintMatchTotalMap = scoreExplanation
.getConstraintMatchTotalMap(); .getConstraintMatchTotalMap();
constraintMatchTotalMap.forEach((key, value) -> { constraintMatchTotalMap.forEach((key, value) -> {
if (!value.getScore().isFeasible()) { if (!value.getScore().isFeasible()) {
// 违反硬约束 // 违反硬约束
System.out.printf("%s 匹配%s次 hard得分:%s%n", value.getConstraintName(), value.getConstraintMatchCount(), System.out.printf("%s 匹配%s次 hard得分:%s%n", value.getConstraintName(), value.getConstraintMatchCount(),
value.getScore().hardScore()); value.getScore().hardScore());
} else { } else {
// 软约束 // 软约束
System.out.printf("%s 匹配%s次 soft得分:%s%n", value.getConstraintName(), value.getConstraintMatchCount(), System.out.printf("%s 匹配%s次 soft得分:%s%n", value.getConstraintName(), value.getConstraintMatchCount(),
value.getScore().softScore()); value.getScore().softScore());
} }
value.getConstraintMatchSet().stream().sorted(comparing(ConstraintMatch::getScore)) value.getConstraintMatchSet().stream().sorted(comparing(ConstraintMatch::getScore))
.forEach(constraintMatch -> { .forEach(constraintMatch -> {
String text = ""; String text = "";
switch (ConstraintNameEnum.valueOf(value.getConstraintName())) { switch (ConstraintNameEnum.valueOf(value.getConstraintName())) {
case skillMatch: case skillMatch:
case customerTimeWindowsMatch: case customerTimeWindowsMatch:
case dispatchedMatch: case dispatchedMatch:
for (Object indictedObject : constraintMatch.getIndictedObjectList()) { for (Object indictedObject : constraintMatch.getIndictedObjectList()) {
// 违反硬约束对象,根据具体约束返回不同类型对象 // 违反硬约束对象,根据具体约束返回不同类型对象
if (indictedObject instanceof Customer) { if (indictedObject instanceof Customer) {
Customer customer = (Customer) indictedObject; Customer customer = (Customer) indictedObject;
text += customer.getCode() + ","; text += customer.getCode() + ",";
} }
} }
System.out.printf(" 预约单(%s)违反约束,扣分%s%n", text, constraintMatch.getScore().toShortString()); System.out.printf(" 预约单(%s)违反约束,扣分%s%n", text, constraintMatch.getScore().toShortString());
break; break;
case totalDistance: case totalDistance:
case preferredTotalDistance: case preferredTotalDistance:
for (Object indictedObject : constraintMatch.getIndictedObjectList()) { for (Object indictedObject : constraintMatch.getIndictedObjectList()) {
// 违反软约束对象,根据具体约束返回不同类型对象 // 违反软约束对象,根据具体约束返回不同类型对象
if (indictedObject instanceof Technician) { if (indictedObject instanceof Technician) {
Technician technician = (Technician) indictedObject; Technician technician = (Technician) indictedObject;
text += technician.getCode() + ","; text += technician.getCode() + ",";
} }
} }
System.out.printf(" 技术员(%s)总路程得分%s%n", text, constraintMatch.getScore().toShortString()); System.out.printf(" 技术员(%s)总路程得分%s%n", text, constraintMatch.getScore().toShortString());
break; break;
case technicianBalanceSoft: case technicianBalanceSoft:
if (constraintMatch.getScore().softScore() < 0) { if (constraintMatch.getScore().softScore() < 0) {
for (Object indictedObject : constraintMatch.getIndictedObjectList()) { for (Object indictedObject : constraintMatch.getIndictedObjectList()) {
// 违反软约束对象,根据具体约束返回不同类型对象 // 违反软约束对象,根据具体约束返回不同类型对象
if (indictedObject instanceof Technician) { if (indictedObject instanceof Technician) {
Technician technician = (Technician) indictedObject; Technician technician = (Technician) indictedObject;
text += technician.getCode() + ","; text += technician.getCode() + ",";
} }
} }
System.out.printf(" 技术员(%s)订单量差距得分%s%n", text, System.out.printf(" 技术员(%s)订单量差距得分%s%n", text,
constraintMatch.getScore().toShortString()); constraintMatch.getScore().toShortString());
} }
break; break;
default: default:
break; break;
} }
}); });
}); });
} }
/** /**
* 导出json结构 * 导出json结构
* *
* @param solution * @param solution
* @param fileName * @param fileName
*/ */
public static void exportSolutionJson(DispatchSolution solution, String fileName) { public static void exportSolutionJson(DispatchSolution solution, String fileName) {
// Create a JacksonSolutionFileIO instance. // Create a JacksonSolutionFileIO instance.
JacksonSolutionFileIO<DispatchSolution> exporter = new JacksonSolutionFileIO<DispatchSolution>( JacksonSolutionFileIO<DispatchSolution> exporter = new JacksonSolutionFileIO<DispatchSolution>(
DispatchSolution.class); DispatchSolution.class);
// Set the output file. // Set the output file.
exporter.write(solution, new File(fileName)); exporter.write(solution, new File(fileName));
} }
/** /**
* 移除hard约束元素订单 * 移除hard约束元素订单
* *
* @param solution * @param solution
* @param solverFactory * @param solverFactory
*/ */
public static void removeHardConstraintCustomer(DispatchSolution solution, public static void removeHardConstraintCustomer(DispatchSolution solution,
SolverFactory<DispatchSolution> solverFactory) { SolverFactory<DispatchSolution> solverFactory) {
SolutionManager<DispatchSolution, HardSoftLongScore> scoreManager = SolutionManager.create(solverFactory); if ("1719308075152764928".equals(solution.getTeamId())) {
ScoreExplanation<DispatchSolution, HardSoftLongScore> scoreExplanation = scoreManager.explain(solution); System.out.println(solution.getTeamId());
Map<String, ConstraintMatchTotal<HardSoftLongScore>> constraintMatchTotalMap = scoreExplanation }
.getConstraintMatchTotalMap(); SolutionManager<DispatchSolution, HardSoftLongScore> scoreManager = SolutionManager.create(solverFactory);
constraintMatchTotalMap.forEach((key, value) -> { ScoreExplanation<DispatchSolution, HardSoftLongScore> scoreExplanation = scoreManager.explain(solution);
if (!value.getScore().isFeasible()) { Map<String, ConstraintMatchTotal<HardSoftLongScore>> constraintMatchTotalMap = scoreExplanation
// 违反硬约束 .getConstraintMatchTotalMap();
value.getConstraintMatchSet().stream().sorted(comparing(ConstraintMatch::getScore)) constraintMatchTotalMap.forEach((key, value) -> {
.forEach(constraintMatch -> { if (!value.getScore().isFeasible()) {
switch (ConstraintNameEnum.valueOf(value.getConstraintName())) { // 违反硬约束
case skillMatch: value.getConstraintMatchSet().stream().sorted(comparing(ConstraintMatch::getScore))
case customerTimeWindowsMatch: .forEach(constraintMatch -> {
for (Object indictedObject : constraintMatch.getIndictedObjectList()) { switch (ConstraintNameEnum.valueOf(value.getConstraintName())) {
// 违反硬约束对象,根据具体约束返回不同类型对象 case skillMatch:
if (indictedObject instanceof Customer) { case customerTimeWindowsMatch:
Customer customer = (Customer) indictedObject; for (Object indictedObject : constraintMatch.getIndictedObjectList()) {
// 违反硬约束对象,根据具体约束返回不同类型对象
// 更新shadow变量 if (indictedObject instanceof Customer) {
updateShadowVariable(customer); Customer customer = (Customer) indictedObject;
// 更新shadow变量
// 移除技术员 updateShadowVariable(customer);
customer.getTechnician().getCustomerList().remove(customer); // 移除技术员
customer.getTechnician().getCustomerList().remove(customer);
solution.getUnDispatchedCustomers().add(customer); solution.getUnDispatchedCustomers().add(customer);
} }
} }
break; break;
default: case technicianTimeWindowsMatch:
break; for (Object indictedObject : constraintMatch.getIndictedObjectList()) {
} // 违反硬约束对象,根据具体约束返回不同类型对象
}); if (indictedObject instanceof Technician
} && ObjectUtil.isNotEmpty(((Technician) indictedObject).getCustomerList())) {
}); List<Customer> customerList = new ArrayList<>();
} customerList.addAll(((Technician) indictedObject).getCustomerList());
for (Customer customer : customerList) {
if (!customer.isInTechnicianTimeWindows()) {
private static void updateShadowVariable(Customer sourceCustomer) { // 更新shadow变量
if (sourceCustomer.getTechnician() == null) { updateShadowVariable(customer);
if (sourceCustomer.getArrivalTime() != null) { // 移除技术员
sourceCustomer.setArrivalTime(null); customer.getTechnician().getCustomerList().remove(customer);
} solution.getUnDispatchedCustomers().add(customer);
return; }
} }
}
// 移除当前订单引用 }
Customer previousCustomer = sourceCustomer.getPreviousCustomer(); break;
Customer nextCustomer = sourceCustomer.getNextCustomer(); default:
if (previousCustomer == null) { break;
// 当前订单是第一个订单 }
if(null != nextCustomer) { });
nextCustomer.setPreviousCustomer(null); }
} });
} }
if (nextCustomer == null) {
// 当前订单是最后一个订单
if(null != previousCustomer) { private static void updateShadowVariable(Customer sourceCustomer) {
previousCustomer.setNextCustomer(null); if (sourceCustomer.getTechnician() == null) {
} if (sourceCustomer.getArrivalTime() != null) {
} sourceCustomer.setArrivalTime(null);
if (previousCustomer != null && null != nextCustomer) { }
previousCustomer.setNextCustomer(nextCustomer); return;
nextCustomer.setPreviousCustomer(previousCustomer); }
}
// 移除当前订单引用
// 前一个的离开时间 Customer previousCustomer = sourceCustomer.getPreviousCustomer();
Integer departureTime; Customer nextCustomer = sourceCustomer.getNextCustomer();
if (previousCustomer == null) { if (previousCustomer == null) {
departureTime = (sourceCustomer.getTechnician().getDepot()).getStartTime(); // 当前订单是第一个订单
} else { if (null != nextCustomer) {
departureTime = previousCustomer.getDepartureTime(); nextCustomer.setPreviousCustomer(null);
} }
if (nextCustomer != null) { }
// 更新后续订单 if (nextCustomer == null) {
Customer shadowCustomer = nextCustomer; // 当前订单是最后一个订单
Integer arrivalTime = calculateArrivalTime(shadowCustomer, departureTime); if (null != previousCustomer) {
while (shadowCustomer != null && !Objects.equals(shadowCustomer.getArrivalTime(), arrivalTime)) { previousCustomer.setNextCustomer(null);
shadowCustomer.setArrivalTime(arrivalTime); }
departureTime = shadowCustomer.getDepartureTime(); }
shadowCustomer = shadowCustomer.getNextCustomer(); if (previousCustomer != null && null != nextCustomer) {
arrivalTime = calculateArrivalTime(shadowCustomer, departureTime); previousCustomer.setNextCustomer(nextCustomer);
} nextCustomer.setPreviousCustomer(previousCustomer);
} }
}
// 前一个的离开时间
private static Integer calculateArrivalTime(Customer customer, Integer previousDepartureTime) { Integer departureTime;
if (customer == null || previousDepartureTime == null) { if (previousCustomer == null) {
return null; departureTime = (sourceCustomer.getTechnician().getDepot()).getStartTime();
} } else {
return previousDepartureTime + customer.getPathTimeFromPreviousStandstill(); departureTime = previousCustomer.getDepartureTime();
} }
if (nextCustomer != null) {
// 更新后续订单
Customer shadowCustomer = nextCustomer;
Integer arrivalTime = calculateArrivalTime(shadowCustomer, departureTime);
while (shadowCustomer != null && !Objects.equals(shadowCustomer.getArrivalTime(), arrivalTime)) {
shadowCustomer.setArrivalTime(arrivalTime);
departureTime = shadowCustomer.getDepartureTime();
shadowCustomer = shadowCustomer.getNextCustomer();
arrivalTime = calculateArrivalTime(shadowCustomer, departureTime);
}
}
}
private static Integer calculateArrivalTime(Customer customer, Integer previousDepartureTime) {
if (customer == null || previousDepartureTime == null) {
return null;
}
return previousDepartureTime + customer.getPathTimeFromPreviousStandstill();
}
} }
...@@ -3,8 +3,8 @@ server: ...@@ -3,8 +3,8 @@ server:
dispatch: dispatch:
cron: cron:
expr: 0 */3 8-23 * * ? expr: 0 10 8-23 * * ?
next-day-limit: 20 next-day-limit: 2
scheduler: scheduler:
init-engineer-capacity: init-engineer-capacity:
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!