Commit e63273c7 by huangjinxin

Merge remote-tracking branch 'origin/develop' into develop

2 parents caa47486 5eb60d24
package com.dituhui.pea.dispatch.common;
import com.dituhui.pea.dispatch.pojo.Location;
import com.dituhui.pea.dispatch.utils.RoadDistanceUtils;
import com.dituhui.pea.dispatch.utils.RoadDistanceUtils.Distance;
import org.gavaghan.geodesy.Ellipsoid;
import org.gavaghan.geodesy.GeodeticCalculator;
import org.gavaghan.geodesy.GeodeticCurve;
......@@ -62,9 +65,10 @@ public class GeoDistanceCalculator {
from -> toLocations.stream().collect(Collectors.toMap(
Function.identity(),
to -> {
long distance = calculateDistance(from, to);
long duration = Math.round(distance / avgRate);
return new Pair(distance, duration);
Distance distance = RoadDistanceUtils.getDistance(from, to);
long path = (long) distance.getDis();
long time = distance.getTime();
return new Pair(path, time);
}
))
));
......
......@@ -14,10 +14,18 @@ public enum ConstraintNameEnum {
*/
skillMatch,
/**
* 技术员到达时间跟时间窗吻合
* 订单到达时间跟时间窗吻合
*/
customerTimeWindowsMatch,
/**
* 技术员不加班
*/
technicianTimeWindowsMatch,
/**
* 技术员订单数量不超过最大值
*/
technicianCapacityMatch,
/**
* 已分配匹配
*/
dispatchedMatch,
......
......@@ -19,6 +19,8 @@ public class DispatchConstraintProvider implements ConstraintProvider {
// 5s/60s: 技能权重4-技能匹配问题0个,时间窗问题14个
// 10s/300s: 技能权重4-技能匹配问题0个,时间窗问题9个
customerTimeWindowsMatch(factory),
technicianTimeWindowsMatch(factory),
technicianCapacityMatch(factory),
skillMatch(factory),
dispatchedMatch(factory),
......@@ -50,6 +52,22 @@ public class DispatchConstraintProvider implements ConstraintProvider {
.asConstraint(ConstraintNameEnum.customerTimeWindowsMatch.name());
}
protected Constraint technicianTimeWindowsMatch(ConstraintFactory factory) {
return factory.forEach(Technician.class).filter(
// EndTime ==0 表示不起作用
technician -> technician.getEndTime() > 0 && technician.getOffWorkTime() > technician.getEndTime())
.penalizeLong(HardSoftLongScore.ONE_HARD, technician -> 1)
.asConstraint(ConstraintNameEnum.technicianTimeWindowsMatch.name());
}
protected Constraint technicianCapacityMatch(ConstraintFactory factory) {
return factory.forEach(Technician.class).filter(
// MaxCount ==0 表示不起作用
technician -> technician.getMaxCount() > 0 && technician.getCustomerSize() > technician.getMaxCount())
.penalizeLong(HardSoftLongScore.ONE_HARD, technician -> 1)
.asConstraint(ConstraintNameEnum.technicianCapacityMatch.name());
}
protected Constraint dispatchedMatch(ConstraintFactory factory) {
return factory.forEach(Customer.class)
.filter(customer -> customer.getDispatchedTechnicianCode() != null
......@@ -120,7 +138,7 @@ public class DispatchConstraintProvider implements ConstraintProvider {
protected Constraint technicianBalanceSoft(ConstraintFactory factory) {
return factory.forEachUniquePair(Technician.class).penalizeLong(HardSoftLongScore.ONE_SOFT,
// 权重需要调节,差距一个相当于多一公里 FIXME 这里应该是时长均衡,不是订单量均衡
(a, b) -> Math.abs(a.getCustomerSize() - b.getCustomerSize()) * 1000)
(a, b) -> Math.abs(a.getCustomerSize() - b.getCustomerSize()) * 4000)
.asConstraint(ConstraintNameEnum.technicianBalanceSoft.name());
}
......
......@@ -14,6 +14,9 @@ public interface DispatchOrderRepository extends CrudRepository<DispatchOrder, L
@Query("from DispatchOrder where groupId=?1 and batchNo=?2 and status !='CONFIRM' and (engineerCode is null or engineerCode='' ) ")
List<DispatchOrder> findNotAssigned(String groupId, String batchNo);
// 查看未指派非confirm的,供算法计算
@Query("from DispatchOrder where groupId=?1 and batchNo=?2")
List<DispatchOrder> findAll(String groupId, String batchNo);
// 查看算法指派成功(也有抹掉技术员、时间情况),非confirm状态的
@Query("from DispatchOrder where groupId=?1 and batchNo=?2 and status !='CONFIRM' ")
......
......@@ -5,6 +5,7 @@ import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
......@@ -14,4 +15,8 @@ public interface OrderInfoRepository extends CrudRepository<OrderInfo, Long> {
Optional<OrderInfo> findOrderInfoByOrderIdAndDt(String orderId, LocalDate dt);
List<OrderInfo> findByOrderId(String orderId);
List<OrderInfo> findByOrgTeamIdAndDt(String teamId, LocalDate dt);
}
package com.dituhui.pea.dispatch.scheduler;
import com.dituhui.pea.dispatch.constraint.DispatchConstraintProvider;
import com.dituhui.pea.dispatch.entity.DispatchBatch;
import com.dituhui.pea.dispatch.pojo.Customer;
import com.dituhui.pea.dispatch.pojo.DispatchSolution;
import com.dituhui.pea.dispatch.pojo.Technician;
import com.dituhui.pea.dispatch.service.BatchService;
import com.dituhui.pea.dispatch.service.ExtractService;
import com.dituhui.pea.dispatch.service.SolveService;
import com.dituhui.pea.dispatch.utils.DispatchSolutionUtils;
import java.io.File;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.UUID;
import lombok.extern.slf4j.Slf4j;
import org.optaplanner.core.api.solver.Solver;
import org.optaplanner.core.api.solver.SolverFactory;
import org.optaplanner.core.config.solver.SolverConfig;
import org.optaplanner.core.impl.solver.DefaultSolverFactory;
import org.optaplanner.persistence.jackson.impl.domain.solution.JacksonSolutionFileIO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.io.File;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Arrays;
import java.util.UUID;
import com.dituhui.pea.dispatch.pojo.DispatchSolution;
import com.dituhui.pea.dispatch.service.BatchService;
import com.dituhui.pea.dispatch.service.ExtractService;
import com.dituhui.pea.dispatch.service.SolveService;
import com.dituhui.pea.dispatch.utils.DispatchSolutionUtils;
import lombok.extern.slf4j.Slf4j;
@Slf4j
@Component
......@@ -49,17 +41,11 @@ public class BatchScheduler {
@Autowired
ExtractService extractService;
private SolverFactory<DispatchSolution> solverFactory;
private DefaultSolverFactory<DispatchSolution> solverFactory;
private Solver<DispatchSolution> solver;
public BatchScheduler() {
SolverConfig solverConfig = new SolverConfig().withSolutionClass(DispatchSolution.class);
solverConfig.withEntityClassList(Arrays.asList(Technician.class, Customer.class));// 这里不能漏掉,否则约束不生效
solverConfig.withConstraintProviderClass(DispatchConstraintProvider.class);
solverConfig.withTerminationSpentLimit(Duration.ofSeconds(60 * 5));
solverFactory = SolverFactory.create(solverConfig);
solverFactory = DispatchSolutionUtils.getSolverFactory(30, 60 * 5);
solver = solverFactory.buildSolver();
}
......@@ -74,8 +60,8 @@ public class BatchScheduler {
String currDay = LocalDate.now().plusDays(i).format(DateTimeFormatter.ISO_LOCAL_DATE);
log.info("dispatchRun begin----- group:{}, day:{}", groupId, currDay);
LocalTime currentTime = LocalTime.now();
LocalTime cutoffTime = LocalTime.parse("18:00:00", DateTimeFormatter.ISO_LOCAL_TIME);
// LocalTime currentTime = LocalTime.now();
// LocalTime cutoffTime = LocalTime.parse("18:00:00", DateTimeFormatter.ISO_LOCAL_TIME);
// 明天单才有cutoff
......
package com.dituhui.pea.dispatch.service.impl;
import com.dituhui.pea.dispatch.dao.DispatchBatchRepository;
import com.dituhui.pea.dispatch.entity.DispatchBatch;
import com.dituhui.pea.dispatch.service.BatchService;
import lombok.extern.slf4j.Slf4j;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.Optional;
import javax.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
import java.sql.Connection;
import java.sql.SQLException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Optional;
import com.dituhui.pea.dispatch.dao.DispatchBatchRepository;
import com.dituhui.pea.dispatch.entity.DispatchBatch;
import com.dituhui.pea.dispatch.service.BatchService;
import lombok.extern.slf4j.Slf4j;
/**
......@@ -112,7 +105,12 @@ public class BatchServiceImpl implements BatchService {
" and appointment_method like 'AUTO%' and a.appointment_status in ('INIT', 'PRE')\n" +
" and order_status ='NORMAL' and service_status='INIT'\n" +
" order by a.expect_time_begin asc ";
int orderCount = jdbcTemplate.update(sqlOrder, batchNo, groupId, batchDay);
String tempDay = "2023-08-13";
int orderCount = jdbcTemplate.update(sqlOrder, batchNo, groupId,
/**
* FIXME 因为系统暂时没有订单,每次固定拉取2023-08-13号数据tempDay,上线后改成batchDay
*/
tempDay);
// confirm的要做预占用,所以也加入进来
String sqlOrderConfirm = "INSERT INTO dispatch_order (group_id, batch_no, team_id, order_id , dt, x, y, \n" +
......@@ -128,7 +126,7 @@ public class BatchServiceImpl implements BatchService {
" order by a.expect_time_begin asc ";
int orderConfirmCount = jdbcTemplate.update(sqlOrderConfirm, batchNo, groupId, batchDay);
log.info("准备批次数据 orderCount:{}, orderConfirmCount:{}", orderCount, orderConfirmCount);
log.info("准备批次数据 engCount:{}, orderCount:{}, orderConfirmCount:{}", engCount, orderCount, orderConfirmCount);
if (orderCount + orderConfirmCount > 0) {
......
......@@ -12,6 +12,8 @@ import com.dituhui.pea.dispatch.pojo.*;
import com.dituhui.pea.dispatch.service.ExtractService;
import com.dituhui.pea.dispatch.service.SolveService;
import com.dituhui.pea.dispatch.utils.DispatchSolutionUtils;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.optaplanner.core.api.solver.Solver;
import org.optaplanner.core.api.solver.SolverFactory;
......@@ -98,7 +100,9 @@ public class SolveServiceImpl implements SolveService {
// customerlist
ArrayList<Customer> customerList = new ArrayList<>();
List<DispatchOrder> dispatchOrderList = dispatchOrderRepo.findNotAssigned(groupId, batchNo);
// 已分配和未分配一起排班,因为已分配会影响整体排班结果
List<DispatchOrder> dispatchOrderList = dispatchOrderRepo.findAll(groupId, batchNo);
// List<DispatchOrder> dispatchOrderList = dispatchOrderRepo.findNotAssigned(groupId, batchNo);
log.info("组织问题对象, dispatchorder-list, groupId:{}, batchNo:{}, dispatchorder-size:{}", groupId, batchNo, dispatchOrderList.size());
......@@ -121,7 +125,7 @@ public class SolveServiceImpl implements SolveService {
}
// 40分钟兜低(技能未能正确匹配原因)
// 40分钟兜低(技能未能正确匹配原因) FIXME 需要跟客户沟通
if (null == order.getTakeTime()) {
order.setTakeTime(40);
}
......@@ -139,14 +143,12 @@ public class SolveServiceImpl implements SolveService {
log.info("组织问题对象, customer-list, groupId:{}, batchNo:{}, customer-list:{}", groupId, batchNo, customerList.size());
// depotlist 技术员中收点列表
ArrayList<Depot> depotList = new ArrayList<Depot>();
// technicianList
ArrayList<Technician> technicianList = new ArrayList<>();
dispatchEngineerRepo.findByGroupIdAndBatchNo(groupId, batchNo).forEach(engineer -> {
Location location = new Location(engineer.getId(), engineer.getEngineerCode(), "中心点", Double.parseDouble(engineer.getX()), Double.parseDouble(engineer.getY()));
Depot depot = new Depot(engineer.getId(), engineer.getEngineerCode(), location, 60 * 8, 60 * 18);
depotList.add(depot);
// Depot depot = new Depot(engineer.getId(), engineer.getEngineerCode(), location, 60 * 8, 60 * 18);
// depotList.add(depot);
// log.debug("组织问题对象, technicianList groupId:{}, batchNo:{}, engineer-code:{}", groupId, batchNo, engineer.getEngineerCode());
List<String> skillList = queryEngineerSkills(engineer.getEngineerCode());
......@@ -160,20 +162,24 @@ public class SolveServiceImpl implements SolveService {
preferedLoctionDistanceMap.put(customer.getCode(), distance);
});
Technician vehicle = new Technician(engineer.getId(), engineer.getEngineerCode(), engineer.getMaxNum(), engineer.getMaxMinute(), engineer.getMaxDistance() * 1000, depot, 60 * 8, 60 * 18, Set.copyOf(skillList), preferedLoctionDistanceMap);
Technician vehicle = new Technician(engineer.getId(), engineer.getEngineerCode(), engineer.getMaxNum(), engineer.getMaxMinute(), engineer.getMaxDistance() * 1000, oneDepot, 60 * 8, 60 * 18, Set.copyOf(skillList), preferedLoctionDistanceMap);
technicianList.add(vehicle);
});
log.info("组织问题对象, depotList-list, groupId:{}, batchNo:{}, depotList-list:{}", groupId, batchNo, depotList.size());
log.info("组织问题对象, depotList-list, groupId:{}, batchNo:{}", groupId, batchNo);
log.info("组织问题对象, technician-list, groupId:{}, batchNo:{}, technician-list:{}", groupId, batchNo, technicianList.size());
//locationlist
List<Location> locationList = Stream.concat(depotList.stream().map(Depot::getLocation), customerList.stream().map(Customer::getLocation)).collect(Collectors.toList());
// locationlist 起点+订单地点
List<Location> locationList = Stream.concat(Lists.newArrayList(oneDepot).stream().map(Depot::getLocation), customerList.stream().map(Customer::getLocation)).collect(Collectors.toList());
DispatchSolution solution = new DispatchSolution(groupId, batchNo, locationList, oneDepot, technicianList, customerList);
// path 路网数据初始化,FIXME 需要专门路网数据缓存库
long time1 = System.currentTimeMillis();
distanceCalculator.initDistanceMaps(locationList);
long time2 = System.currentTimeMillis();
log.info("组织问题对象 done, groupId:{}, batchNo:{}, technician-size:{}, customer-size:{}, location-size:{}", groupId, batchNo, technicianList.size(), customerList.size(), locationList.size());
log.info("组织问题对象 done, groupId:{}, batchNo:{}, technician-size:{}, customer-size:{}, location-size:{}, 路网耗时path:{}ms", groupId, batchNo, technicianList.size(), customerList.size(), locationList.size(), time2-time1);
return solution;
......
......@@ -44,7 +44,31 @@ public class DataUtils {
Map<String, Integer> customerCodeServiceTimeMap = loadCustomerCodeServiceTimeMap();
DispatchSolution problem = createVehicleRoutingSolution(customerIndexMap, customerIndexXyMap,
technicianIndexMap, technicianCodeSkillsMap, customerCodeSkillMap, technicianCodePreferredLocationMap,
preferredlocationDistanceMap, customerCodeServiceTimeMap);
preferredlocationDistanceMap, customerCodeServiceTimeMap, false);
return problem;
}
/**
* 获取初始化测试数据
* fullDay 是否全天派工
* @return
* @throws UncheckedIOException
* @throws FileNotFoundException
*/
public static DispatchSolution getInitialProblem(boolean fullDay) throws UncheckedIOException, FileNotFoundException {
Map<Integer, String> customerIndexMap = loadCustomerIndex();
Map<Integer, String> customerIndexXyMap = loadCustomerIndexXY();
Map<Integer, String> technicianIndexMap = loadTechnicianIndex();
Map<String, Set<String>> technicianCodeSkillsMap = loadTechnicianCodeSkillsMap();
Map<String, String> customerCodeSkillMap = loadCustomerCodeSkillMap();
// 偏好中心点位置
Map<String, String> technicianCodePreferredLocationMap = loadPreferredlocationMap();
Map<String, Map<String, Long>> preferredlocationDistanceMap = loadPreferredlocationDistanceMap(
technicianCodePreferredLocationMap);
Map<String, Integer> customerCodeServiceTimeMap = loadCustomerCodeServiceTimeMap();
DispatchSolution problem = createVehicleRoutingSolution(customerIndexMap, customerIndexXyMap,
technicianIndexMap, technicianCodeSkillsMap, customerCodeSkillMap, technicianCodePreferredLocationMap,
preferredlocationDistanceMap, customerCodeServiceTimeMap,fullDay);
return problem;
}
......@@ -189,7 +213,7 @@ public class DataUtils {
Map<String, Set<String>> technicianCodeSkillsMap, Map<String, String> customerCodeSkillMap,
Map<String, String> technicianCodePreferredLocationMap,
Map<String, Map<String, Long>> preferredlocationDistanceMap,
Map<String, Integer> customerCodeServiceTimeMap) throws UncheckedIOException, FileNotFoundException {
Map<String, Integer> customerCodeServiceTimeMap, boolean fullDay) throws UncheckedIOException, FileNotFoundException {
DispatchSolution vehicleRoutingSolution = new DispatchSolution();
// 翻转map
......@@ -290,7 +314,7 @@ public class DataUtils {
String[] temps = xyString.split(",");
Location preferredlocation = new Location(i + 1, Float.parseFloat(RegExUtils.removeAll(temps[0], "\"")),
Float.parseFloat(RegExUtils.removeAll(temps[1], "\"")));
technicianList.add(new Technician(i + 1, technicianIndexMap.get(i + 1), depot, 480, 1080, skills,
technicianList.add(new Technician(i + 1, technicianIndexMap.get(i + 1), depot, 480, fullDay ? 1440 : 1080 , skills,
preferredlocationDistanceMap.get(technicianIndexMap.get(i + 1)), preferredlocation));
}
......
......@@ -140,9 +140,11 @@ public class DispatchSolutionUtils {
// 技术员路线
String lines_ = "[";
for (Technician technician : solution.getTechnicianList()) {
if (technician.getCustomerList().size() > 0) {
lines_ += "\"" + technician.getCustomerList().stream()
.map(c -> c.getLocation().getX() + "," + c.getLocation().getY()).reduce((a, b) -> a + ";" + b)
.get() + "\",";
.map(c -> c.getLocation().getX() + "," + c.getLocation().getY())
.reduce((a, b) -> a + ";" + b).get() + "\",";
}
}
lines_ += "]";
final String lines = lines_;
......
package com.dituhui.pea.dispatch.utils;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLEncoder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import com.dituhui.pea.dispatch.pojo.Location;
import com.google.common.collect.Maps;
import com.google.gson.Gson;
import lombok.Data;
/**
* 路网组件<br>
* TODO 需要做成分布式缓存模式,这里会造成内存问题<br>
* TODO 调用方式需要改成批量调用方式
*
* @author gpzhang
*
*/
public class RoadDistanceUtils {
public static String URL = "https://api.map.baidu.com/routematrix/v2/riding?";
public static String AK = "doR30pE7R0I7ivGLwMpkpsTT4bos9Akg";
/**
* 格式 x1,y1;x2,y2
*/
private static Map<String, Distance> distanceCache = Maps.newHashMap();
private static Gson gson = new Gson();
/**
* 获取路网距离和时间<br>
* TODO 需要做成分布式缓存模式,这里会造成内存问题<br>
* TODO 调用方式需要改成批量调用方式
*
* @param from
* @param to
* @return
*/
public static Distance getDistance(Location from, Location to) {
try {
String key = from.getLongitude() + "," + from.getLatitude() + ";" + to.getLongitude() + ","
+ to.getLatitude();
Distance distance = distanceCache.get(key);
if (null == distance) {
distance = getDistance(from.getLatitude() + "," + from.getLongitude(),
to.getLatitude() + "," + to.getLongitude());
if(null == distance) {
Distance dis = new Distance();
return dis;
}else {
distanceCache.put(key, distance);
}
return distance;
} else {
return distance;
}
} catch (Exception e) {
Distance dis = new Distance();
return dis;
}
}
private static Distance getDistance(String yx1, String yx2) throws Exception {
Map<String, String> params = new HashMap<String, String>();
params.put("origins", yx1);
params.put("destinations", yx2);
params.put("ak", AK);
params.put("riding_type", "1");// 电动自行车
params.put("coord_type", "gcj02");
String text = requestGetAK(URL, params);
BDResult webResult = gson.fromJson(text, BDResult.class);
float dis = webResult.getResult().get(0).getDistance().getValue() / 1000F;
int time = webResult.getResult().get(0).getDuration().getValue();
Distance d = new Distance();
d.setDis(dis);
d.setTime(time);
return d;
}
/**
* 默认ak 选择了ak,使用IP白名单校验: 根据您选择的AK已为您生成调用代码 检测到您当前的ak设置了IP白名单校验
* 您的IP白名单中的IP非公网IP,请设置为公网IP,否则将请求失败 请在IP地址为xxxxxxx的计算发起请求,否则将请求失败
*/
public static String requestGetAK(String strUrl, Map<String, String> param) throws Exception {
if (strUrl == null || strUrl.length() <= 0 || param == null || param.size() <= 0) {
return "";
}
StringBuffer queryString = new StringBuffer();
queryString.append(strUrl);
for (Map.Entry<?, ?> pair : param.entrySet()) {
queryString.append(pair.getKey() + "=");
// 第一种方式使用的 jdk 自带的转码方式 第二种方式使用的 spring 的转码方法 两种均可
queryString.append(URLEncoder.encode((String) pair.getValue(), "UTF-8").replace("+", "%20") + "&");
}
if (queryString.length() > 0) {
queryString.deleteCharAt(queryString.length() - 1);
}
java.net.URL url = new URL(queryString.toString());
URLConnection httpConnection = (HttpURLConnection) url.openConnection();
httpConnection.connect();
InputStreamReader isr = new InputStreamReader(httpConnection.getInputStream());
BufferedReader reader = new BufferedReader(isr);
StringBuffer buffer = new StringBuffer();
String line;
while ((line = reader.readLine()) != null) {
buffer.append(line);
}
reader.close();
isr.close();
return (buffer.toString());
}
@Data
public static class Distance {
float dis;
int time;
}
@Data
static class BDResult {
List<BDDistance> result;
}
@Data
static class BDDistance {
Dis distance;
Dur duration;
}
@Data
static class Dis {
Float value;
}
@Data
static class Dur {
Integer value;
}
}
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!