Commit 998c16f9 by 张晓

算法读取批次部分合并

1 parent f26dde47
Showing with 1572 additions and 21 deletions
...@@ -95,6 +95,13 @@ ...@@ -95,6 +95,13 @@
<artifactId>commons-lang3</artifactId> <artifactId>commons-lang3</artifactId>
<version>3.12.0</version> <version>3.12.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.gavaghan</groupId>
<artifactId>geodesy</artifactId>
<version>1.1.3</version>
</dependency>
</dependencies> </dependencies>
<build> <build>
<plugins> <plugins>
......
package com.dituhui.pea.dispatch.common;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
public class DateUtil {
public static Long localDateTimeToTimestamp(LocalDateTime localDateTime) {
try {
ZoneId zoneId = ZoneId.systemDefault();
Instant instant = localDateTime.atZone(zoneId).toInstant();
return instant.toEpochMilli();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* 时间戳转LocalDateTime
*
* @param timestamp 时间戳
* @return LocalDateTime
*/
public static LocalDateTime timestampToLocalDateTime(long timestamp) {
try {
Instant instant = Instant.ofEpochMilli(timestamp);
ZoneId zone = ZoneId.systemDefault();
return LocalDateTime.ofInstant(instant, zone);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* Date转LocalDateTime
*
* @param date Date
* @return LocalDateTime
*/
public static LocalDateTime dateToLocalDateTime(Date date) {
try {
Instant instant = date.toInstant();
ZoneId zoneId = ZoneId.systemDefault();
return instant.atZone(zoneId).toLocalDateTime();
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
/**
* LocalDateTime转Date
*
* @param localDateTime LocalDateTime
* @return Date
*/
public static Date localDateTimeToDate(LocalDateTime localDateTime) {
try {
ZoneId zoneId = ZoneId.systemDefault();
ZonedDateTime zdt = localDateTime.atZone(zoneId);
return Date.from(zdt.toInstant());
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}
package com.dituhui.pea.dispatch.common;
import com.dituhui.pea.dispatch.pojo.Location;
import org.gavaghan.geodesy.Ellipsoid;
import org.gavaghan.geodesy.GeodeticCalculator;
import org.gavaghan.geodesy.GeodeticCurve;
import org.gavaghan.geodesy.GlobalCoordinates;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
@Component
public class GeoDistanceCalculator {
private long calculateDistance(Location from, Location to) {
if (from.equals(to)) {
return 0L;
}
GlobalCoordinates source = new GlobalCoordinates(from.getLatitude(), from.getLongitude());
GlobalCoordinates target = new GlobalCoordinates(to.getLatitude(), to.getLongitude());
GeodeticCurve geoCurve = new GeodeticCalculator().calculateGeodeticCurve(Ellipsoid.WGS84, source, target);
long distance = Math.round(geoCurve.getEllipsoidalDistance());
// todo *1.4倍 约等于实际路线距离
distance = Math.round(distance * 1.4);
return distance;
}
private Map<Location, Map<Location, Long>> calculateBulkDistance(
Collection<Location> fromLocations,
Collection<Location> toLocations) {
return fromLocations.stream().collect(Collectors.toMap(
Function.identity(),
from -> toLocations.stream().collect(Collectors.toMap(
Function.identity(),
to -> calculateDistance(from, to)
))
));
}
public void initDistanceMaps(Collection<Location> locationList) {
Map<Location, Map<Location, Long>> distanceMatrix = calculateBulkDistance(locationList, locationList);
locationList.forEach(location -> location.setDistanceMap(distanceMatrix.get(location)));
}
}
...@@ -13,11 +13,8 @@ public class DispatchConstraintProvider implements ConstraintProvider { ...@@ -13,11 +13,8 @@ public class DispatchConstraintProvider implements ConstraintProvider {
@Override @Override
public Constraint[] defineConstraints(ConstraintFactory factory) { public Constraint[] defineConstraints(ConstraintFactory factory) {
return new Constraint[] { greaterThanZero(factory), customerTimeWindowsMatch1(factory), return new Constraint[] { greaterThanZero(factory), customerTimeWindowsMatch1(factory),
customerTimeWindowsMatch2(factory), customerTimeWindowsMatch2(factory), skillMatch(factory), technicianBalance(factory),
skillMatch(factory), technicianBalance2(factory), technicianBalanceSoft(factory), totalDistance(factory),
technicianBalance(factory),
technicianBalance2(factory),
totalDistance(factory),
preferredTotalDistance(factory) }; preferredTotalDistance(factory) };
} }
...@@ -30,7 +27,7 @@ public class DispatchConstraintProvider implements ConstraintProvider { ...@@ -30,7 +27,7 @@ public class DispatchConstraintProvider implements ConstraintProvider {
// 4,技术员技能跟订单相匹配skillMatch // 4,技术员技能跟订单相匹配skillMatch
// 5,订单均分technicianBalance // 5,订单均分technicianBalance
protected Constraint greaterThanZero(ConstraintFactory factory) { public Constraint greaterThanZero(ConstraintFactory factory) {
return factory.forEach(Technician.class).filter(technician -> technician.getCustomerList().size() == 0) return factory.forEach(Technician.class).filter(technician -> technician.getCustomerList().size() == 0)
.penalizeLong(HardSoftLongScore.ONE_HARD, technician -> 1).asConstraint("每个技术员至少分配一个单子"); .penalizeLong(HardSoftLongScore.ONE_HARD, technician -> 1).asConstraint("每个技术员至少分配一个单子");
} }
...@@ -42,6 +39,7 @@ public class DispatchConstraintProvider implements ConstraintProvider { ...@@ -42,6 +39,7 @@ public class DispatchConstraintProvider implements ConstraintProvider {
} }
protected Constraint customerTimeWindowsMatch2(ConstraintFactory factory) { protected Constraint customerTimeWindowsMatch2(ConstraintFactory factory) {
// 迟到2小时惩罚
return factory.forEach(Customer.class).filter( return factory.forEach(Customer.class).filter(
customer -> customer.getTechnician() != null && customer.getArrivalTime() > customer.getEndTime() + 120) customer -> customer.getTechnician() != null && customer.getArrivalTime() > customer.getEndTime() + 120)
.penalizeLong(HardSoftLongScore.ONE_HARD, customer -> 1).asConstraint("技术员到达时间跟时间窗吻合2"); .penalizeLong(HardSoftLongScore.ONE_HARD, customer -> 1).asConstraint("技术员到达时间跟时间窗吻合2");
...@@ -51,12 +49,17 @@ public class DispatchConstraintProvider implements ConstraintProvider { ...@@ -51,12 +49,17 @@ public class DispatchConstraintProvider implements ConstraintProvider {
return factory.forEach(Customer.class) return factory.forEach(Customer.class)
.filter(customer -> customer.getTechnician() != null .filter(customer -> customer.getTechnician() != null
&& !customer.getTechnician().getSkills().contains(customer.getRequiredSkill())) && !customer.getTechnician().getSkills().contains(customer.getRequiredSkill()))
.penalizeLong(HardSoftLongScore.ONE_HARD, customer -> 1).asConstraint("技术员技能跟订单相匹配skillMatch"); .penalizeLong(HardSoftLongScore.ONE_HARD,
// 技能匹配跟时间窗匹配存在很明显的跷跷板效应,权重小于3就会存在技能匹配问题
// 3-技能匹配问题1个,时间窗问题8个
// 4-技能匹配问题0个,时间窗问题14个
customer -> 4)
.asConstraint("技术员技能跟订单相匹配skillMatch");
} }
protected Constraint technicianBalance(ConstraintFactory factory) { protected Constraint technicianBalance(ConstraintFactory factory) {
// 会导致剩余单子集中? // 会导致剩余单子集中?
return factory.forEach(Technician.class).filter(technician -> technician.getOffWorkTime() > 1440) return factory.forEach(Technician.class).filter(technician -> technician.getOffWorkTime() > 1140)
.penalizeLong(HardSoftLongScore.ONE_HARD, .penalizeLong(HardSoftLongScore.ONE_HARD,
technician -> (long) Math.ceil(technician.getOffWorkTime() / 1440f)) technician -> (long) Math.ceil(technician.getOffWorkTime() / 1440f))
.asConstraint("订单均分"); .asConstraint("订单均分");
...@@ -64,10 +67,8 @@ public class DispatchConstraintProvider implements ConstraintProvider { ...@@ -64,10 +67,8 @@ public class DispatchConstraintProvider implements ConstraintProvider {
protected Constraint technicianBalance2(ConstraintFactory factory) { protected Constraint technicianBalance2(ConstraintFactory factory) {
// 单量不能过少 FIXME // 单量不能过少 FIXME
return factory.forEach(Technician.class).filter(technician -> technician.getOffWorkTime() <= 1140) return factory.forEach(Technician.class).filter(technician -> technician.getOffWorkTime() <= 960)
.penalizeLong(HardSoftLongScore.ONE_HARD, .penalizeLong(HardSoftLongScore.ONE_HARD, technician -> 1).asConstraint("订单均分2");
technician -> 1)
.asConstraint("订单均分2");
} }
// protected Constraint technicianBalance(ConstraintFactory factory) { // protected Constraint technicianBalance(ConstraintFactory factory) {
...@@ -92,6 +93,7 @@ public class DispatchConstraintProvider implements ConstraintProvider { ...@@ -92,6 +93,7 @@ public class DispatchConstraintProvider implements ConstraintProvider {
// ************************************************************************ // ************************************************************************
// 1, 总路程最小 totalDistance // 1, 总路程最小 totalDistance
// 2, 技术员中心点偏好 preferredTotalDistance // 2, 技术员中心点偏好 preferredTotalDistance
// 3, 订单数量均衡 technicianBalanceSoft
protected Constraint totalDistance(ConstraintFactory factory) { protected Constraint totalDistance(ConstraintFactory factory) {
return factory.forEach(Technician.class) return factory.forEach(Technician.class)
...@@ -104,4 +106,10 @@ public class DispatchConstraintProvider implements ConstraintProvider { ...@@ -104,4 +106,10 @@ public class DispatchConstraintProvider implements ConstraintProvider {
.asConstraint("技术员中心点偏好"); .asConstraint("技术员中心点偏好");
} }
protected Constraint technicianBalanceSoft(ConstraintFactory factory) {
return factory.forEachUniquePair(Technician.class).penalizeLong(HardSoftLongScore.ONE_SOFT,
// 权重需要调节,差距一个相当于多一公里 FIXME 这里应该是时长均衡,不是订单量均衡
(a, b) -> Math.abs(a.getCustomerSize() - b.getCustomerSize()) * 1000).asConstraint("订单均分soft");
}
} }
...@@ -19,6 +19,7 @@ package com.dituhui.pea.dispatch.controller; ...@@ -19,6 +19,7 @@ package com.dituhui.pea.dispatch.controller;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import com.dituhui.pea.common.Result; import com.dituhui.pea.common.Result;
...@@ -35,10 +36,19 @@ public class DispatchController { ...@@ -35,10 +36,19 @@ public class DispatchController {
private DispatchService dispatchService; private DispatchService dispatchService;
@GetMapping("/manual") @GetMapping("/manual")
public Result<?> manualDispatch() { public Result<?> manualDispatch(
@RequestParam(value = "unimprovedSecondsSpentLimit", required = false) Long unimprovedSecondsSpentLimit,
@RequestParam(value = "secondsSpentLimit", required = false) Long secondsSpentLimit) {
if (null == unimprovedSecondsSpentLimit) {
unimprovedSecondsSpentLimit = 0L;
}
if (null == secondsSpentLimit) {
secondsSpentLimit = 0L;
}
try { try {
return dispatchService.manualDispatch(); return dispatchService.manualDispatch(unimprovedSecondsSpentLimit, secondsSpentLimit);
} catch (Exception e) { } catch (Exception e) {
e.printStackTrace();
return Result.failed(e.getMessage()); return Result.failed(e.getMessage());
} }
} }
......
package com.dituhui.pea.dispatch.dao;
import com.dituhui.pea.dispatch.entity.DispatchBatch;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Optional;
@Repository
public interface DispatchBatchRepository extends CrudRepository<DispatchBatch, Long> {
List<DispatchBatch> findByGroupId(String groupId);
Optional<DispatchBatch> findByGroupIdAndBatchDate(String groupId, String batchDay);
@Query(value = "from DispatchBatch where groupId = ?1 and batchNo=?2 ")
List<DispatchBatch> findLatestGroup(String groupId, String batchNo);
}
\ No newline at end of file
package com.dituhui.pea.dispatch.dao;
import com.dituhui.pea.dispatch.entity.DispatchEngineer;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
public interface DispatchEngineerRepository extends CrudRepository<DispatchEngineer, Long> {
List<DispatchEngineer> findByGroupId(String groupId);
List<DispatchEngineer> findByGroupIdAndBatchNo(String groupId, String batchNo);
}
\ No newline at end of file
package com.dituhui.pea.dispatch.dao;
import com.dituhui.pea.dispatch.entity.DispatchOrder;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
import java.util.Optional;
public interface DispatchOrderRepository extends CrudRepository<DispatchOrder, Long> {
List<DispatchOrder> findByGroupIdAndBatchNo(String groupId, String batchNo);
List<DispatchOrder> findByGroupIdAndBatchNoAndEngineerCodeNot(String groupId, String batchNo, String code);
@Query("from DispatchOrder where groupId=?1 and batchNo=?2 and engineerCode is not null and engineerCode!='' ")
List<DispatchOrder> findAssigned(String groupId, String batchNo);
Optional<DispatchOrder> findByGroupIdAndBatchNoAndOrderId(String groupId, String batchNo, String orderId);
}
\ No newline at end of file
package com.dituhui.pea.dispatch.dao;
import com.dituhui.pea.dispatch.entity.EngineerInfo;
import org.springframework.data.repository.CrudRepository;
import java.util.List;
import java.util.Optional;
public interface EngineerInfoRepository extends CrudRepository<EngineerInfo, Long> {
List<EngineerInfo> findByGroupId(String groupId);
Optional<EngineerInfo> findByEngineerCode(String engineerCode);
}
\ No newline at end of file
package com.dituhui.pea.dispatch.dao;
import com.dituhui.pea.dispatch.entity.OrderAppointment;
import org.springframework.data.repository.CrudRepository;
import java.util.Optional;
public interface OrderAppointmentRepository extends CrudRepository<OrderAppointment, Long> {
Optional<OrderAppointment> findByOrderId(String orderId);
}
package com.dituhui.pea.dispatch.dao;
import com.dituhui.pea.dispatch.entity.OrderRequest;
import org.springframework.data.repository.CrudRepository;
import java.util.Optional;
public interface OrderRequestRepository extends CrudRepository<OrderRequest, Long> {
Optional<OrderRequest> findByOrderId(String orderId);
}
package com.dituhui.pea.dispatch.dao;
import java.util.List;
import java.util.Optional;
import com.dituhui.pea.dispatch.entity.DispatchBatch;
import com.dituhui.pea.dispatch.entity.OrgGroup;
import org.springframework.data.repository.CrudRepository;
import com.fasterxml.jackson.annotation.JsonFormat;
import org.springframework.stereotype.Repository;
@Repository
public interface OrgGroupRepository extends CrudRepository<OrgGroup, Long> {
Optional<OrgGroup> findByGroupId(String groupId);
}
package com.dituhui.pea.dispatch.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import javax.persistence.*;
/**
* 排班批次总表
*/
@Entity
@Data
@Table(name = "dispatch_batch")
public class DispatchBatch implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name = "id", nullable = false)
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "group_id")
private String groupId;
/**
* 批次号
*/
@Column(name = "batch_no")
private String batchNo;
/**
* 跑批日期
*/
@Column(name = "batch_date")
private String batchDate;
/**
* 技术员数量
*/
@Column(name = "engineer_num")
private Integer engineerNum;
/**
* 服务单数量
*/
@Column(name = "order_num")
private Integer orderNum;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Column(name = "start_time")
private LocalDateTime startTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Column(name = "end_time")
private LocalDateTime endTime;
/**
* RUNNING,DONE
*/
@Column(name = "status")
private String status;
@Column(name = "memo")
private String memo;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Column(name = "update_time")
private LocalDateTime updateTime;
@Column(name = "ext")
private String ext;
public DispatchBatch() {
}
public DispatchBatch(String groupId, String batchNo, String batchDate, Integer engineerNum, Integer orderNum) {
this.groupId = groupId;
this.batchNo = batchNo;
this.batchDate = batchDate;
this.engineerNum = engineerNum;
this.orderNum = orderNum;
}
}
package com.dituhui.pea.dispatch.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "dispatch_engineer")
public class DispatchEngineer implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "group_id")
private String groupId;
@Column(name = "batch_no")
private String batchNo;
@Column(name = "engineer_code")
private String engineerCode;
@Column(name = "engineer_name")
private String engineerName;
@Column(name = "x")
private String X;
@Column(name = "y")
private String Y;
@Column(name = "max_num")
private Integer maxNum;
@Column(name = "max_minute")
private Integer maxMinute;
@Column(name = "max_distance")
private Integer maxDistance;
private String ext = "";
private String memo;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Column(name = "update_time")
private LocalDateTime updateTime;
}
\ No newline at end of file
package com.dituhui.pea.dispatch.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import javax.persistence.*;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.util.Date;
@Data
@Entity
@Table(name = "dispatch_order")
public class DispatchOrder implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "group_id")
private String groupId;
@Column(name = "batch_no")
private String batchNo;
@Column(name = "order_id")
private String orderId;
@Column(name = "x")
private String X;
@Column(name = "y")
private String Y;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "expect_time_begin")
private Date expectTimeBegin;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "expect_time_end")
private Date expectTimeEnd;
private String tags;
private Integer priority;
private String skills;
@Column(name = "take_time")
private Integer takeTime;
@Column(name = "engineer_code")
private String engineerCode;
private Integer seq;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Column(name = "time_begin")
private LocalDateTime timeBegin;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Column(name = "time_end")
private LocalDateTime timeEnd;
private String status;
private String ext;
private String memo;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Column(name = "update_time")
private LocalDateTime updateTime;
}
\ No newline at end of file
package com.dituhui.pea.dispatch.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "engineer_info")
public class EngineerInfo implements Serializable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
@Column(name = "engineer_code")
private String engineerCode;
private String name;
@Column(name = "group_id")
private String groupId;
@Column(name = "cosmos_id")
private String cosmosId;
private String gender;
private String birth;
private String phone;
private String address;
private Integer kind;
private String grade;
private String credentials;
private Integer vehicle;
@Column(name = "vehicle_no")
private String vehicleNo;
@Column(name = "bean_status")
private Integer beanStatus;
private String tags;
private String memo;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Column(name = "create_time")
private LocalDateTime createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
@Column(name = "update_time")
private LocalDateTime updateTime;
}
package com.dituhui.pea.dispatch.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "order_appointment")
public class OrderAppointment implements Serializable {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
@Column(name = "order_id")
private String orderId;
@Column(name = "suborder_id")
private String suborderId;
@Column(name = "main_sub")
private Integer mainSub;
@Column(name = "engineer_code")
private String engineerCode;
@Column(name = "engineer_name")
private String engineerName;
@Column(name = "engineer_phone")
private String engineerPhone;
@Column(name = "engineer_age")
private Integer engineerAge;
@Column(name = "is_workshop")
private Integer isWorkshop;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "expect_start_time")
private LocalDateTime expectStartTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "expect_end_time")
private LocalDateTime expectEndTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "actual_time")
private LocalDateTime actualTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "actual_start_time")
private LocalDateTime actualStartTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "actual_end_time")
private LocalDateTime actualEndTime;
@Column(name = "pre_status")
private String preStatus;
private String status;
private String memo;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "create_time")
private LocalDateTime createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "update_time")
private LocalDateTime updateTime;
}
package com.dituhui.pea.dispatch.entity;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@Entity
@Table(name = "order_request")
public class OrderRequest implements Serializable {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private String id;
@Column(name = "order_id")
private String orderId;
private String name;
private String phone;
private String address;
@Column(name = "x")
private String X;
@Column(name = "y")
private String Y;
private String province;
private String city;
private String county;
@Column(name = "category_id")
private String categoryId;
private String brand;
private String type;
private String skill;
@Column(name = "apply_note")
private String applyNote;
@Column(name = "fault_describe")
private String faultDescribe;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "expect_time_begin")
private LocalDateTime expectTimeBegin;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "expect_time_end")
private LocalDateTime expectTimeEnd;
@Column(name = "expect_time_desc")
private String expectTimeDesc;
private String source;
@Column(name = "area_id")
private String areaId;
@Column(name = "order_priority")
private String orderPriority;
@Column(name = "order_tags")
private String orderTags;
private Integer priority;
private String tags;
private String status;
@Column(name = "appointment_status")
private String appointmentStatus;
@Column(name = "appointment_method")
private String appointmentMethod;
@Column(name = "org_cluster_id")
private String orgClusterId;
@Column(name = "org_cluster_name")
private String orgClusterName;
@Column(name = "org_branch_id")
private String orgBranchId;
@Column(name = "org_branch_name")
private String orgBranchName;
@Column(name = "org_group_id")
private String orgGroupId;
@Column(name = "org_group_name")
private String orgGroupName;
@Column(name = "org_team_id")
private String orgTeamId;
@Column(name = "org_team_name")
private String orgTeamName;
private String description;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "create_time")
private LocalDateTime createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "update_time")
private LocalDateTime updateTime;
}
package com.dituhui.pea.dispatch.entity;
import lombok.Data;
import java.util.Date;
import java.io.Serializable;
import com.fasterxml.jackson.annotation.JsonFormat;
import javax.persistence.*;
import javax.persistence.Table;
@Data
@Entity
@Table(name = "org_group")
public class OrgGroup implements Serializable {
@Id
@GeneratedValue(strategy=GenerationType.IDENTITY)
private Integer id;
@Column(name = "group_id")
private String groupId;
@Column(name = "group_name")
private String groupName;
@Column(name = "cluster_id")
private String clusterId;
@Column(name = "branch_id")
private String branchId;
private String address;
@Column(name = "x")
private String X;
@Column(name = "y")
private String Y;
@Column(name = "city_code")
private String cityCode;
private Integer kind;
private Integer category;
@Column(name = "warehouse_id")
private Integer warehouseId;
private String memo;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "create_time")
private Date createTime;
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
@Column(name = "update_time")
private Date updateTime;
}
...@@ -6,12 +6,14 @@ import lombok.Data; ...@@ -6,12 +6,14 @@ import lombok.Data;
* 出发地 * 出发地
* *
* @author gpzhang * @author gpzhang
*
*/ */
@Data @Data
public class Depot { public class Depot {
private final long id; private final long id;
private String code = "";
private final Location location; private final Location location;
// 时间窗 分钟 // 时间窗 分钟
...@@ -25,4 +27,11 @@ public class Depot { ...@@ -25,4 +27,11 @@ public class Depot {
this.endTime = endTime; this.endTime = endTime;
} }
public Depot(long id, String code, Location location, int startTime, int endTime) {
this.id = id;
this.code = code;
this.location = location;
this.startTime = startTime;
this.endTime = endTime;
}
} }
...@@ -16,6 +16,11 @@ import lombok.Data; ...@@ -16,6 +16,11 @@ import lombok.Data;
@PlanningSolution @PlanningSolution
public class DispatchSolution { public class DispatchSolution {
private String groupId;
private String batchNo;
private String name; private String name;
@ProblemFactCollectionProperty @ProblemFactCollectionProperty
...@@ -47,6 +52,15 @@ public class DispatchSolution { ...@@ -47,6 +52,15 @@ public class DispatchSolution {
this.customerList = customerList; this.customerList = customerList;
} }
public DispatchSolution(String groupId, String batchNo, List<Location> locationList, Depot depot, List<Technician> technicianList, List<Customer> customerList) {
this.groupId = groupId;
this.batchNo = batchNo;
this.locationList = locationList;
this.depot = depot;
this.technicianList = technicianList;
this.customerList = customerList;
}
// ************************************************************************ // ************************************************************************
// Complex methods // Complex methods
// ************************************************************************ // ************************************************************************
......
...@@ -13,15 +13,40 @@ import lombok.Setter; ...@@ -13,15 +13,40 @@ import lombok.Setter;
public class Location { public class Location {
private final long id; private final long id;
private String code;
// 类型 engineer order
private String type;
private double latitude;
private double longitude;
@JsonIgnore @JsonIgnore
private Map<Location, Long> distanceMap= new HashMap<Location, Long>();// 路网距离矩阵 private Map<Location, Long> distanceMap = new HashMap<Location, Long>();// 路网距离矩阵
@JsonIgnore @JsonIgnore
private Map<Location, Long> distanceTimeMap= new HashMap<Location, Long>();// 路网时间矩阵 private Map<Location, Long> distanceTimeMap = new HashMap<Location, Long>();// 路网时间矩阵
public Location(long id) { public Location(long id) {
this.id = id; this.id = id;
} }
public Location(long id, String code, String type, double longitude, double latitude) {
this.id = id;
this.code = code;
this.type = type;
this.longitude = longitude;
this.latitude = latitude;
}
@Override
public String toString() {
return "Location{" +
"code='" + code + '\'' +
", type='" + type + '\'' +
", latitude=" + latitude +
", longitude=" + longitude +
'}';
}
/** /**
* Set the distance map. Distances are in meters. * Set the distance map. Distances are in meters.
* *
...@@ -41,10 +66,10 @@ public class Location { ...@@ -41,10 +66,10 @@ public class Location {
return distanceMap.get(location); return distanceMap.get(location);
} }
/** /**
* time to the given location in minutes. * time to the given location in minutes.
* FIXME 这里简化处理没有用时间矩阵 时间=距离/100 * FIXME 这里简化处理没有用时间矩阵 时间=距离/100
*
* @param location other location * @param location other location
* @return time in minutes * @return time in minutes
*/ */
...@@ -56,10 +81,20 @@ public class Location { ...@@ -56,10 +81,20 @@ public class Location {
// Complex methods // Complex methods
// ************************************************************************ // ************************************************************************
@Override
public int hashCode() {
return Long.valueOf(this.id).hashCode();
}
@Override @Override
public String toString() { public boolean equals(Object obj) {
return "Location{" + "id=" + id + '}'; if (obj == null)
return false;
if (!(obj instanceof Location))
return false;
if (obj == this)
return true;
return this.id == ((Location) obj).getId();
} }
} }
...@@ -31,6 +31,16 @@ public class Technician { ...@@ -31,6 +31,16 @@ public class Technician {
// 技能 // 技能
private Set<String> skills; private Set<String> skills;
// 每日最大单量
private int maxCount;
// 每日最大工作时长
private int maxMinute;
// 单位是米,这里要注意
private int maxDistanceMeter;
// 偏好坐标 // 偏好坐标
// private Location preferredlocation; // private Location preferredlocation;
// teck code : customer code , distance // teck code : customer code , distance
...@@ -54,6 +64,21 @@ public class Technician { ...@@ -54,6 +64,21 @@ public class Technician {
this.preferredlocationDistanceMap = preferredlocationDistanceMap; this.preferredlocationDistanceMap = preferredlocationDistanceMap;
} }
public Technician(long id, String code, int maxCount, int maxMinute, int maxDistanceMeter,
Depot depot, int startTime, int endTime, Set<String> skills,
Map<String, Long> preferredlocationDistanceMap) {
this.id = id;
this.code = code;
this.depot = depot;
this.startTime = startTime;
this.endTime = endTime;
this.skills = skills;
this.maxCount = maxCount;
this.maxMinute = maxMinute;
this.maxDistanceMeter = maxDistanceMeter;
this.preferredlocationDistanceMap = preferredlocationDistanceMap;
}
// ************************************************************************ // ************************************************************************
// Complex methods // Complex methods
// ************************************************************************ // ************************************************************************
...@@ -99,6 +124,7 @@ public class Technician { ...@@ -99,6 +124,7 @@ public class Technician {
return totalDistance; return totalDistance;
} }
/** /**
* 获取偏好总距离 所有customer与该技术员的PreferredLocation距离之和 * 获取偏好总距离 所有customer与该技术员的PreferredLocation距离之和
* *
...@@ -135,6 +161,7 @@ public class Technician { ...@@ -135,6 +161,7 @@ public class Technician {
} }
} }
/** /**
* 获取下班时间,最后一个订单完成时间 * 获取下班时间,最后一个订单完成时间
* *
...@@ -150,4 +177,23 @@ public class Technician { ...@@ -150,4 +177,23 @@ public class Technician {
} }
} }
@Override
public int hashCode() {
return Long.valueOf(this.id).hashCode();
}
@Override
public boolean equals(Object obj) {
if (obj == null)
return false;
if (!(obj instanceof Technician))
return false;
if (obj == this)
return true;
return this.id == ((Technician) obj).getId();
}
} }
package com.dituhui.pea.dispatch.service;
import com.dituhui.pea.dispatch.entity.DispatchBatch;
import org.springframework.transaction.annotation.Transactional;
import java.sql.SQLException;
/**
* @author zhangx
* <p>
* 批次排班数据准备
*/
public interface BatchService {
// 检查指定日期的小组是否有在运行的批次任务,有则返回,没有则创建后返回批次码
@Transactional
String buildBatchNo(String groupId, String day) throws SQLException;
DispatchBatch queryBatch(String groupId, String day);
}
...@@ -26,6 +26,7 @@ import com.dituhui.pea.common.Result; ...@@ -26,6 +26,7 @@ import com.dituhui.pea.common.Result;
*/ */
public interface DispatchService { public interface DispatchService {
Result<?> manualDispatch() throws UncheckedIOException, IOException; Result<?> manualDispatch(long unimprovedSecondsSpentLimit, long secondsSpentLimit)
throws UncheckedIOException, IOException;
} }
package com.dituhui.pea.dispatch.service;
import com.dituhui.pea.dispatch.pojo.DispatchSolution;
import java.sql.SQLException;
/**
* @author zhangx
* <p>
* 排班算法数据准备
* 排班结果解析到dispatch_order(更新补充技术员工号、上门时间) ,order_appointment、order_request
*/
public interface ExtractService {
/*
* 将计算结果回写到dispatch2个表、以及order两个表
* 是下面两个方法的包装
* */
void saveAndExtractSolution(DispatchSolution solution) throws RuntimeException;
/*
* 将计算结果回写到dispatch_order表(更新补充技术员工号、上门时间)
* */
void saveSolutionToDispatch(String groupId, String batchNo, DispatchSolution solution) throws RuntimeException;
/*
* 将dispath_order 中的计算结果,回写到 order_request, order_appointment
* order_appointment(新增、更新)
* order_request(主要更新状态)
* */
void extractDispatchToOrder(String groupId, String batchNo) throws SQLException;
}
package com.dituhui.pea.dispatch.service;
import com.dituhui.pea.dispatch.pojo.DispatchSolution;
import java.util.UUID;
/**
* @author zhangx
* <p>
* 排班算法执行
*/
public interface SolveService {
/*
* 按小组、批次号组装问题对象
* 调用optaplaner计算输出结果
* */
DispatchSolution prepareAndSolveSolution(String groupId, String batchNo);
UUID generateProblemId(String groupId, String batchNo);
DispatchSolution prepareSolution(String groupId, String batchNo) ;
}
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 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.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionTemplate;
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;
/**
* @author zhangx
*/
@Slf4j
@Service
public class BatchServiceImpl implements BatchService {
@Autowired
DispatchBatchRepository batchRepository;
@Autowired
private JdbcTemplate jdbcTemplate;
// 生成最新批次号
private String calcBatchNo(String day) {
// 定义日期时间格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HHmm");
// 将当前时间转换为字符串
String result = LocalTime.now().format(formatter);
return day.replaceAll("-","") + "-" + result;
}
// 检查给定小组、日期是否有在运行的批次任务,没则返回,没有则创建
@Transactional
@Override
public String buildBatchNo(String groupId, String day) {
log.info("准备批次数据, groupId:{}, day:{}", groupId, day);
String batchNo = "";
String batchDay = "";
Optional<DispatchBatch> optional = batchRepository.findByGroupIdAndBatchDate(groupId, day);
if (!optional.isPresent()) {
batchNo = calcBatchNo(day);
batchDay = day;
// 执行数据库操作
String sqlInsert = "INSERT INTO `dispatch_batch` ( `group_id`, `batch_no`, `batch_date`, `engineer_num`, `order_num`, `start_time`, `end_time`, `status`) " +
" VALUES(?, ?, ?, ?, ?, ?, ?, ?)";
jdbcTemplate.update(sqlInsert, groupId, batchNo, batchDay, 0, 0, LocalDateTime.now(), null, "RUNNING");
// queryRunner.execute(sqlInsert, groupId, batchNo, batchDay, 0, 0, LocalDateTime.now(), null, "RUNNING");
log.info("生成新批次, groupId:{}, day:{}", groupId, batchDay);
} else {
batchNo = optional.get().getBatchNo();
batchDay = optional.get().getBatchDate();
}
// int engCount = queryEnginerCount(groupId);
// int orderCount = queryOrderCount(groupId, batchDay);
log.info("清理原批次数据, groupId:{}, day:{}, batchNo:{}", groupId, batchDay, batchNo);
jdbcTemplate.update("delete from dispatch_engineer where group_id=? and batch_no=?", groupId, batchNo);
jdbcTemplate.update("delete from dispatch_order where group_id=? and batch_no=?", groupId, batchNo);
log.info("写入新批次技术员、工单数据, groupId:{}, day:{}, batchNo:{}", groupId, batchDay, batchNo);
String sqlEngineer = "INSERT INTO dispatch_engineer (group_id, batch_no, engineer_code, engineer_name, x, y, max_num, max_minute, max_distance)\n" +
"select a.group_id, ? , a.engineer_code, a.name , b.x, b.y , max_num, max_minute, max_distance from \n" +
" engineer_info a left join engineer_business b \n" +
" on a.engineer_code=b.engineer_code \n" +
" where a.group_id=? and b.x is not null and b.x !=''\n" +
" order by a.engineer_code asc";
int engCount = jdbcTemplate.update(sqlEngineer, batchNo, groupId);
String sqlOrder = "INSERT INTO dispatch_order (group_id, batch_no, order_id , x, y, expect_time_begin, expect_time_end, tags, priority , skills , take_time )\n" +
" select a.org_group_id, ? , a.order_id, a.x, a.y , \n" +
" a.expect_time_begin, a.expect_time_end, a.tags, a.priority , concat(a.brand, a.type, a.skill) skills , b.take_time \n" +
" from order_request a left join product_category b on (a.brand=b.brand and a.type=b.type and a.skill=b.skill )\n" +
" where a.org_group_id=? and status='OPEN' and appointment_status='NOT_ASSIGNED' and appointment_method like 'AUTO%' \n" +
" and expect_time_begin between ? and ? \n" +
" order by a.expect_time_begin asc ";
int orderCount = jdbcTemplate.update(sqlOrder, batchNo, groupId, batchDay + " 00:00:00", batchDay + " 23:59:59");
jdbcTemplate.update("update dispatch_batch set engineer_num=? , order_num=? where group_id=? and batch_no=?", engCount, orderCount, groupId, batchNo);
log.info("准备批次数据完成, groupId:{}, day:{}, batchNo:{}", groupId, batchDay, batchNo);
return batchNo;
}
public DispatchBatch queryBatch(String groupId, String batchNo) {
List<DispatchBatch> batchList = batchRepository.findLatestGroup(groupId, batchNo);
if (batchList.size() > 0) {
return batchList.get(0);
} else {
return new DispatchBatch();
}
}
}
package com.dituhui.pea.dispatch.service.impl;
import cn.hutool.core.date.DateTime;
import cn.hutool.core.date.DateUtil;
import com.dituhui.pea.dispatch.dao.*;
import com.dituhui.pea.dispatch.entity.DispatchOrder;
import com.dituhui.pea.dispatch.entity.EngineerInfo;
import com.dituhui.pea.dispatch.entity.OrderAppointment;
import com.dituhui.pea.dispatch.entity.OrderRequest;
import com.dituhui.pea.dispatch.pojo.DispatchSolution;
import com.dituhui.pea.dispatch.service.ExtractService;
import com.mysql.cj.util.StringUtils;
import lombok.extern.slf4j.Slf4j;
import org.optaplanner.core.api.score.ScoreExplanation;
import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
import org.optaplanner.core.api.solver.SolutionManager;
import org.optaplanner.core.api.solver.SolutionUpdatePolicy;
import org.optaplanner.core.api.solver.Solver;
import org.optaplanner.core.api.solver.SolverFactory;
import org.optaplanner.core.config.solver.SolverConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j
@Service
public class ExtractServiceImpl implements ExtractService {
@Autowired
EngineerInfoRepository engineerInfoRepo;
@Autowired
DispatchOrderRepository dispatchOrderRepo;
@Autowired
OrderRequestRepository orderRequestRepo;
@Autowired
OrderAppointmentRepository orderAppointmentRepo;
@Autowired
private JdbcTemplate jdbcTemplate;
/*
* 将计算结果回写到dispatch2个表、以及order两个表
* */
@Override
public void saveAndExtractSolution(DispatchSolution solution) throws RuntimeException{
String groupId = solution.getGroupId();
String batchNo = solution.getBatchNo();
log.info("算法结果回写包装方法, groupId:{}, batchNo:{}", groupId, batchNo);
this.saveSolutionToDispatch(groupId, batchNo, solution);
try {
this.extractDispatchToOrder(solution.getGroupId(), solution.getBatchNo());
} catch (SQLException e) {
throw new RuntimeException(e);
}
}
/**
* 将计算结果回写到dispatch_order表(更新补充技术员工号、上门时间)
*/
@Override
public void saveSolutionToDispatch(String groupId, String batchNo, DispatchSolution solution) throws RuntimeException {
log.info("算法结果回写dispatch, groupId:{}, batchNo:{}", groupId, batchNo);
// 清理当前批次指派结果
log.info("算法结果回写dispatch, step1-清除历史, groupId:{}, batchNo:{}", groupId, batchNo);
Object[] paramClear = {groupId, batchNo};
jdbcTemplate.update(" update dispatch_order set engineer_code='' , seq=0, time_begin=null ,time_end=null where group_id=? and batch_no=? ", paramClear);
String sql = "update dispatch_order set engineer_code=? , seq=?, time_begin=? ,time_end=? where group_id=? and batch_no=? and order_id=? ";
log.info("算法结果回写dispatch, step2-开始回写, groupId:{}, batchNo:{}", groupId, batchNo);
// 保存当前批次指派结果
solution.getTechnicianList().forEach(vehicle -> {
log.info("算法结果回写dispatch, step2.1-按技术员逐个回写, groupId:{}, batchNo:{}, employ: {}, max-minute:{}, customlist.size:{}",
groupId, batchNo, vehicle.getId(), vehicle.getMaxMinute(), vehicle.getCustomerList().size());
AtomicInteger seq = new AtomicInteger();
final Date[] expectBegin = {null};
vehicle.getCustomerList().forEach(customer -> {
int idx = seq.getAndIncrement();
// 统计按8:00开始 +take_time + 20分钟路程向后累积
Optional<DispatchOrder> optional = dispatchOrderRepo.findByGroupIdAndBatchNoAndOrderId(groupId, batchNo, customer.getCode());
if (optional.isPresent()) {
DispatchOrder dOrder = optional.get();
if (expectBegin[0] == null) {
expectBegin[0] = dOrder.getExpectTimeBegin();
}
LocalDateTime localExpectBegin = LocalDateTime.ofInstant(expectBegin[0].toInstant(), ZoneId.systemDefault());
// 时间相加操作
LocalDateTime localEndTime = localExpectBegin.plusMinutes(dOrder.getTakeTime());
Date end = Date.from(localEndTime.atZone(ZoneId.systemDefault()).toInstant());
log.info("算法结果回写dispatch, step3-逐个客户处理, groupId:{}, batchNo:{}, employ: {}, customer:{}, service-duration:{} ",
groupId, batchNo, vehicle.getId(), customer.getId(), customer.getServiceDuration());
Object[] param = {vehicle.getId(), idx, expectBegin[0], end, groupId, batchNo, customer.getId()};
jdbcTemplate.update(sql, param);
// 再追加20分钟路程时间做为下一次时间的开始
expectBegin[0] = Date.from(localEndTime.plusMinutes(20).atZone(ZoneId.systemDefault()).toInstant());
}
});
});
log.info("算法结果回写dispatch完成, groupId:{}, batchNo:{}", groupId, batchNo);
}
/**
* 将dispath_order 中的计算结果,回写到 order_request, order_appointment
* order_appointment(新增、更新)
* order_request(主要更新状态)
*/
@Transactional
@Override
public void extractDispatchToOrder(String groupId, String batchNo) throws SQLException {
log.info("算法结果更新到工单, groupId:{}, batchNo:{}", groupId, batchNo);
Map<String, EngineerInfo> engineerInfoMap = engineerInfoRepo.findByGroupId(groupId).stream().collect(Collectors.toMap(EngineerInfo::getEngineerCode, y -> y));
List<DispatchOrder> dispatchOrderList = dispatchOrderRepo.findByGroupIdAndBatchNoAndEngineerCodeNot(groupId, batchNo, "");
log.info("算法结果更新到工单, step1-开始处理, groupId:{}, batchNo:{}, order-size:{}", groupId, batchNo, dispatchOrderList.size());
AtomicInteger atomicInteger = new AtomicInteger();
dispatchOrderList.forEach(dispatchOrder -> {
int idx = atomicInteger.getAndIncrement();
String orderId = dispatchOrder.getOrderId();
String engCode = dispatchOrder.getEngineerCode();
log.info("算法结果更新到工单, step1.1-loop, groupId:{}, batchNo:{}, {}/{}, orderId:{}, engCode:{}",
groupId, batchNo, idx, dispatchOrderList.size(), orderId, engCode);
Optional<OrderRequest> orderOpt = orderRequestRepo.findByOrderId(orderId);
if (!orderOpt.isPresent()) {
log.warn("算法结果更新到工单, step1.1-loop, 工单不存在, groupId:{}, batchNo:{}, orderId:{}",
groupId, batchNo, orderId);
return;
}
OrderRequest orderRequest = orderOpt.get();
if (!("OPEN".equals(orderRequest.getStatus()) && ("ASSIGNED".equals(orderRequest.getAppointmentStatus()) || "NOT_ASSIGNED".equals(orderRequest.getAppointmentStatus())))) {
log.warn("算法结果更新到工单, step1.1-loop, 工单状态异常, groupId:{}, batchNo:{}, orderId:{}, status:{}, appointment-status:{}",
groupId, batchNo, orderId, orderRequest.getStatus(), orderRequest.getAppointmentStatus());
return;
}
EngineerInfo engineerInfo = engineerInfoMap.get(engCode);
if (engineerInfo == null) {
log.warn("算法结果更新到工单, step1.1-loop, 未找到技术员, groupId:{}, batchNo:{}, engCode:{}",
groupId, batchNo, engCode);
return;
}
String engName = engineerInfo.getName();
String phone = engineerInfo.getPhone();
int age = 0;
if (!StringUtils.isNullOrEmpty(engineerInfo.getBirth())) {
DateTime birthDate = DateUtil.parse(engineerInfo.getBirth(), "yyyy-MM-dd");
age = DateUtil.age(birthDate.toJdkDate(), DateUtil.date());
}
if ("NOT_ASSIGNED".equals(orderRequest.getAppointmentStatus())) {
jdbcTemplate.update("update order_request set appointment_status ='ASSIGNED' where order_id =? and appointment_status='NOT_ASSIGNED'", orderId);
}
// 会有多次上门情况( pre_status='dispatch' and status in ('NOT_ASSIGNED', 'ASSIGNED') ,直接更新,其它情况新增)
Optional<OrderAppointment> appointmentOpt = orderAppointmentRepo.findByOrderId(orderId);
if (appointmentOpt.isPresent() && "dispatch".equals(appointmentOpt.get().getPreStatus()) &&
("NOT_ASSIGNED".equals(appointmentOpt.get().getStatus()) || "ASSIGNED".equals(appointmentOpt.get().getStatus()))) {
OrderAppointment appointment = appointmentOpt.get();
appointment.setStatus("ASSIGNED");
appointment.setEngineerCode(engCode);
appointment.setEngineerName(engName);
appointment.setEngineerPhone(phone);
appointment.setEngineerAge(age);
appointment.setExpectStartTime(dispatchOrder.getTimeBegin());
appointment.setExpectEndTime(dispatchOrder.getTimeEnd());
appointment.setUpdateTime(LocalDateTime.now());
orderAppointmentRepo.save(appointment);
} else {
OrderAppointment appointment = new OrderAppointment();
appointment.setOrderId(dispatchOrder.getOrderId());
String subId = String.format("%s_%s", dispatchOrder.getOrderId(), DateUtil.format(LocalDateTime.now(), "yyyyMMddHHmmss"));
appointment.setSuborderId(subId);
appointment.setMainSub(1);
appointment.setEngineerCode(engCode);
appointment.setEngineerName(engName);
appointment.setEngineerPhone(phone);
appointment.setEngineerAge(age);
appointment.setIsWorkshop(0);
appointment.setExpectStartTime(dispatchOrder.getTimeBegin());
appointment.setExpectEndTime(dispatchOrder.getTimeEnd());
appointment.setPreStatus("dispatch");
appointment.setStatus("ASSIGNED");
appointment.setMemo("");
appointment.setCreateTime(LocalDateTime.now());
appointment.setUpdateTime(LocalDateTime.now());
orderAppointmentRepo.save(appointment);
}
});
log.info("算法结果更新到工单完成, groupId:{}, batchNo:{}", groupId, batchNo);
}
}
package com.dituhui.pea.dispatch.service.impl;
import cn.hutool.crypto.SecureUtil;
import com.dituhui.pea.dispatch.common.GeoDistanceCalculator;
import com.dituhui.pea.dispatch.constraint.DispatchConstraintProvider;
import com.dituhui.pea.dispatch.dao.DispatchEngineerRepository;
import com.dituhui.pea.dispatch.dao.DispatchOrderRepository;
import com.dituhui.pea.dispatch.dao.OrgGroupRepository;
import com.dituhui.pea.dispatch.entity.OrgGroup;
import com.dituhui.pea.dispatch.pojo.*;
import com.dituhui.pea.dispatch.service.ExtractService;
import com.dituhui.pea.dispatch.service.SolveService;
import lombok.extern.slf4j.Slf4j;
import org.optaplanner.core.api.solver.*;
import org.optaplanner.core.config.solver.SolverConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.*;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import static com.dituhui.pea.dispatch.common.DateUtil.dateToLocalDateTime;
@Slf4j
@Service
public class SolveServiceImpl implements SolveService {
@Autowired
GeoDistanceCalculator distanceCalculator;
@Autowired
DispatchEngineerRepository dispatchEngineerRepo;
@Autowired
DispatchOrderRepository dispatchOrderRepo;
@Autowired
OrgGroupRepository groupRepository;
@Autowired
ExtractService extractService;
@Autowired
private JdbcTemplate jdbcTemplate;
// 查询技术员所有技能集
private List<String> queryEngineerSkills(String engineerCode) {
List<String> result = new ArrayList<>();
String sql = "select concat( b.brand, b.type, b.skill) as skill from engineer_skill a left join product_category b \n" + "\ton a.category_id= b.product_category_id where a.engineer_code=? and a.status=1 ";
Object[] param = {engineerCode};
result = jdbcTemplate.queryForList(sql, param, String.class);
return result;
}
// 按小组、批次号组装问题对象
@Override
public DispatchSolution prepareSolution(String groupId, String batchNo) {
log.info("组织问题对象, groupId:{}, batchNo:{}", groupId, batchNo);
// 统一出发地
Depot oneDepot;
Optional<OrgGroup> optional = groupRepository.findByGroupId(groupId);
if (!optional.isPresent()) {
log.error("组织问题对象, 未查询到组织信息 ,groupId:{}, batchNo:{}");
throw new RuntimeException(String.format("组织问题对象, 未查询到组织信息 ,groupId:%s, batchNo:%s", groupId, batchNo));
}
OrgGroup oneGroup = optional.get();
Location deptLocation = new Location(oneGroup.getId(), oneGroup.getGroupId(), "起点", Double.parseDouble(oneGroup.getX()), Double.parseDouble(oneGroup.getY()));
oneDepot = new Depot(oneGroup.getId(), oneGroup.getGroupId(), deptLocation, 60 * 8, 60 * 18);
// 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);
List<String> skillList = queryEngineerSkills(engineer.getEngineerCode());
// todo
// Technician vehicle = new Technician(engineer.getId(), engineer.getEngineerCode(),
// engineer.getMaxNum(), engineer.getMaxMinute(), engineer.getMaxDistance() * 1000, depot,
// 60 * 8, 60 * 18, Set.copyOf(skillList), ? );
// technicianList.add(vehicle);
});
// customerlist
ArrayList<Customer> customerList = new ArrayList<>();
dispatchOrderRepo.findByGroupIdAndBatchNo(groupId, batchNo).forEach(order -> {
Location location = new Location(order.getId(), order.getOrderId(), "工单", Double.parseDouble(order.getX()), Double.parseDouble(order.getY()));
LocalDateTime ldt1 = dateToLocalDateTime(order.getExpectTimeBegin());
LocalDateTime ldt2 = dateToLocalDateTime(order.getExpectTimeEnd());
int start = 60 * 8;
int end = 60 * 18;
if (ldt1 != null) {
start = ldt1.getMinute();
}
if (ldt2 != null) {
end = ldt2.getMinute();
}
Customer customer = new Customer(order.getId(), order.getOrderId(), location, start, end,
order.getSkills(), order.getTakeTime());
customerList.add(customer);
});
//locationlist
List<Location> locationList = Stream.concat(depotList.stream().map(Depot::getLocation), customerList.stream().map(Customer::getLocation)).collect(Collectors.toList());
DispatchSolution solution = new DispatchSolution(groupId, batchNo, locationList, oneDepot, technicianList, customerList);
distanceCalculator.initDistanceMaps(locationList);
log.info("组织问题对象, groupId:{}, batchNo:{}, technician-size:{}, customer-size:{}, location-size:{}",
groupId, batchNo, technicianList.size(), customerList.size(), locationList.size());
return solution;
}
/*
* 按小组、批次号组装问题对象
* 调用optaplaner计算输出结果
* */
@Override
public DispatchSolution prepareAndSolveSolution(String groupId, String batchNo) {
log.info("组织问题对象/调用引擎处理, groupId:{}, batchNo:{}", groupId, batchNo);
// Load the problem
DispatchSolution problem = prepareSolution(groupId, batchNo);
SolverConfig solverConfig = new SolverConfig().withSolutionClass(DispatchSolution.class);
solverConfig.withEntityClassList(Arrays.asList(Technician.class, Customer.class));// 这里不能漏掉,否则约束不生效
solverConfig.withConstraintProviderClass(DispatchConstraintProvider.class);
solverConfig.withTerminationSpentLimit(Duration.ofSeconds(10));
SolverFactory<DispatchSolution> solverFactory = SolverFactory.create(solverConfig);
// Solve the problem
log.info("调用引擎处理-开始, groupId:{}, batchNo:{}", groupId, batchNo);
Solver<DispatchSolution> solver = solverFactory.buildSolver();
DispatchSolution solution = solver.solve(problem);
log.info("调用引擎处理-结束, groupId:{}, batchNo:{}, score:{}", groupId, batchNo, solution.getScore());
return solution;
}
/**
* @param groupId
* @param batchNo
* @return
*/
@Override
public UUID generateProblemId(String groupId, String batchNo) {
String md5 = SecureUtil.md5(groupId + "_" + batchNo);
UUID uid = UUID.nameUUIDFromBytes(md5.getBytes());
log.info("------uuid: {}----", uid.toString());
return uid;
}
}
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!