Commit 26a4e727 by 丁伟峰

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

2 parents fe68b974 7f062990
Showing with 20 additions and 2477 deletions
......@@ -4,7 +4,6 @@ package com.dituhui.pea.dispatch.controller;
import cn.hutool.core.map.MapUtil;
import com.dituhui.pea.common.Result;
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;
......@@ -66,9 +65,11 @@ public class PrepareController {
* 运行批次任务,直接返回结果(不重新进行数据准备)
*/
@GetMapping("/solveTest/{groupId}/{batchNo}")
public Result<?> prepareAndSolve(@PathVariable String groupId, @PathVariable String batchNo) {
log.info("prepareSolve, groupId:{}, day:{}", groupId, batchNo);
DispatchSolution solution = solveService.prepareAndSolveSolution(groupId, batchNo);
public Result<?> prepareAndSolve(@PathVariable String groupId, @PathVariable String day) {
log.info("prepareSolve, groupId:{}, day:{}", groupId, day);
String batchNo = batchService.buildBatchData(groupId, day);
log.info("调用引擎处理, groupId:{}, day:{}, batchNo:{}", groupId, day, batchNo);
DispatchSolution solution = solveService.prepareAndSolveTest(groupId, batchNo);
List<Technician> technicianList = solution.getTechnicianList();
List<Customer> customerList = solution.getCustomerList();
HardSoftLongScore score = solution.getScore();
......@@ -83,8 +84,7 @@ public class PrepareController {
@GetMapping("/solveStart/{groupId}/{day}")
public Result<?> solveAsync(@PathVariable String groupId, @PathVariable String day) {
log.info("调用引擎处理-异步处理, groupId:{}, day:{}", groupId, day);
String batchNo = null;
batchNo = batchService.buildBatchData(groupId, day);
String batchNo = batchService.buildBatchData(groupId, day);
// DispatchBatch newBatch= batchService.queryBatchInfoByDay(groupId, day);
// log.info("newBatch:{}", newBatch);
......@@ -93,7 +93,10 @@ public class PrepareController {
log.info("调用引擎处理, groupId:{}, day:{}, batchNo:{}", groupId, day, batchNo);
DispatchSolution problem = solveService.prepareSolution(groupId, batchNo);
if (problem.getCustomerList().size() <= 0) {
log.info("dispatchRun no order , group:{}, day:{}, batch:{}, order-size:{}", groupId, day, batchNo, problem.getCustomerList().size());
return Result.failed("当前批次没有工单数据");
}
DispatchSolution solution = solver.solve(problem);
solveService.saveSolutionWrp(solution);
......
......@@ -17,7 +17,7 @@ public interface SolveService {
* 按小组、批次号组装问题对象
* 调用optaplaner计算输出结果
* */
DispatchSolution prepareAndSolveSolution(String groupId, String batchNo);
DispatchSolution prepareAndSolveTest(String groupId, String batchNo);
UUID generateProblemId(String groupId, String batchNo);
......
......@@ -110,7 +110,6 @@ public class SolveServiceImpl implements SolveService {
if (dispatchOrderList.isEmpty()) {
log.error("组织问题对象, 未查询到工单信息 ,groupId:{}, batchNo:{}", groupId, batchNo);
throw new RuntimeException(String.format("组织问题对象, 未查询到工单信息 ,groupId:%s, batchNo:%s", groupId, batchNo));
}
dispatchOrderList.forEach(order -> {
......@@ -195,11 +194,15 @@ public class SolveServiceImpl implements SolveService {
* 调用optaplaner计算输出结果
* */
@Override
public DispatchSolution prepareAndSolveSolution(String groupId, String batchNo) {
public DispatchSolution prepareAndSolveTest(String groupId, String batchNo) {
log.info("组织问题对象/调用引擎处理, groupId:{}, batchNo:{}", groupId, batchNo);
// Load the problem
DispatchSolution problem = prepareSolution(groupId, batchNo);
if (problem.getCustomerList().size() <= 0) {
log.info("dispatchRun no order , group:{}, batch:{}, order-size:{}", groupId, batchNo, problem.getCustomerList().size());
throw new RuntimeException("当前批次没有工单信息");
}
SolverConfig solverConfig = new SolverConfig().withSolutionClass(DispatchSolution.class);
......
......@@ -57,7 +57,7 @@ class SolveServiceTest {
log.info("init");
DispatchSolution solution = solveService.prepareAndSolveSolution(groupId, batchNo);
DispatchSolution solution = solveService.prepareAndSolveTest(groupId, batchNo);
log.info("result:{}", solution);
......
package com.dituhui.pea.order.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.dituhui.pea.common.BusinessException;
import com.dituhui.pea.common.Result;
......@@ -25,7 +24,6 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import javax.annotation.Resource;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.ArrayList;
......@@ -74,7 +72,7 @@ public class OrderAssignImpl implements OrderAssign {
OrderRecommend or = new OrderRecommend();
for (EngineerInfo engineer : engineers) {
List<OrderAppointment> orderAppointments = this.queryOrderAppointments(engineer.getEngineerCode(), date);
List<OrderAppointment> orderAppointments = orderAppointmentMPDao.selectByEngineerCodeAndDt(engineer.getEngineerCode(), order.getDt());
RecommendResult rr = or.recommend(order, orderAppointments);
if (rr.getIndex() == -1) {
continue;
......@@ -214,10 +212,8 @@ public class OrderAssignImpl implements OrderAssign {
return Result.success(null);
}
private List<OrderAppointment> queryOrderAppointments(String engineerCode, String date) {
LambdaQueryWrapper<OrderAppointment> lqw = new LambdaQueryWrapper<>();
lqw.eq(OrderAppointment::getEngineerCode, engineerCode);
return orderAppointmentMPDao.selectList(lqw);
private List<String> searchEngineerCodes(Integer distance, String key, String recommend){
return null;
}
private List<TimeLineDTO> packTimelines(List<OrderAppointment> orders, HashMap<String, List<LabelValueDTO>> orderTips) {
......
version: "3"
services:
nacos:
image: nacos/nacos-server:v2.2.3-slim
container_name: nacos-standalone-local
environment:
- PREFER_HOST_MODE=hostname
- MODE=standalone
- NACOS_AUTH_IDENTITY_KEY=serverIdentity
- NACOS_AUTH_IDENTITY_VALUE=security
- NACOS_AUTH_TOKEN=SecretKey012345678901234567890123456789012345678901234567890123456789
volumes:
- ./nacos-logs/:/home/nacos/logs
ports:
- "8848:8848"
- "9848:9848"
seata-server:
image: seataio/seata-server:1.6.1
hostname: seata-server
restart: always
container_name: seata-standalone-local
ports:
- "8091:8091"
environment:
- SEATA_PORT=8091
- STORE_MODE=file
# redis:
# image: redis:6.2-alpine
# hostname: redis
# restart: always
# container_name: redis-standalone-local
# ports:
# - "6379:6379"
# command: redis-server --save 20 1 --loglevel warning --requirepass 123456
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-parent</artifactId>
<version>${revision}</version>
<relativePath>../pom.xml</relativePath>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>project-pre-dispatch</artifactId>
<name>Pea PreDispatch</name>
<properties>
<druid.version>1.1.10</druid.version>
<version.org.optaplanner>9.38.0.Final</version.org.optaplanner>
<mysql.version>8.0.28</mysql.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>${druid.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>${mysql.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>project-interface</artifactId>
<version>${revision}</version>
</dependency>
<dependency>
<groupId>org.optaplanner</groupId>
<artifactId>optaplanner-core</artifactId>
<version>${version.org.optaplanner}</version>
</dependency>
<dependency>
<groupId>org.optaplanner</groupId>
<artifactId>optaplanner-spring-boot-starter</artifactId>
<version>${version.org.optaplanner}</version>
</dependency>
<dependency>
<groupId>org.gavaghan</groupId>
<artifactId>geodesy</artifactId>
<version>1.1.3</version>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.7</version>
</dependency>
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>5.7.5</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.13.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.12.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<scope>runtime</scope>
<optional>true</optional>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>10</source>
<target>10</target>
</configuration>
</plugin>
</plugins>
</build>
</project>
\ No newline at end of file
package com.dituhui.pea.pre;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.transaction.annotation.EnableTransactionManagement;
/**
* @author zhangx
*/
@SpringBootApplication
@EnableScheduling
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
package com.dituhui.pea.pre;
import com.dituhui.pea.pre.interceptor.RequestInterceptor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
@EnableJpaRepositories
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private RequestInterceptor requestInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(requestInterceptor).addPathPatterns("/**");
}
}
package com.dituhui.pea.pre.common;
import java.time.Instant;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.util.Date;
public class DateUtil {
/**
* LocalDateTime转毫秒时间戳
*
* @param localDateTime LocalDateTime
* @return 时间戳
*/
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.pre.controller;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.bean.copier.CopyOptions;
import com.dituhui.pea.common.Result;
import com.dituhui.pea.pre.entity.DispatchBatch;
import com.dituhui.pea.pre.service.BatchService;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.sql.SQLException;
import java.time.LocalDateTime;
/**
* @author zhangx
*/
@Slf4j
@RequestMapping("/pea-pre")
@RestController
public class BatchController {
@Autowired
BatchService batchService;
/*
* 检查指定日期的小组是否有在运行的批次任务,有则返回,没有则创建后返回批次码
*/
@GetMapping("/batch/build/{groupId}/{day}")
public Result<?> buildBatch(@PathVariable String groupId, @PathVariable String day) {
log.info("buildBatch, groupId:{}, day:{}", groupId, day);
try {
String batchNo = batchService.buildBatchNo(groupId, day);
DispatchBatch batch = batchService.queryBatch(groupId, batchNo);
DispatchBatchDTO batchDTO = new DispatchBatchDTO();
BeanUtil.copyProperties(batch, batchDTO, CopyOptions.create().setIgnoreNullValue(true));
return Result.success(batchDTO);
} catch (SQLException e) {
log.error("buildBatch error", e);
return Result.failed(e.getMessage());
}
}
@GetMapping("/batch/query/{groupId}/{batchNo}")
public Result<?> queryBatch(@PathVariable String groupId, @PathVariable String batchNo) {
log.info("buildBatch, groupId:{}, batchNo:{}", groupId, batchNo);
DispatchBatch batch = batchService.queryBatch(groupId, batchNo);
DispatchBatchDTO batchDTO = new DispatchBatchDTO();
BeanUtil.copyProperties(batch, batchDTO, CopyOptions.create().setIgnoreNullValue(true));
return Result.success(batchDTO);
}
@Data
class DispatchBatchDTO {
String groupId;
String batchNo;
int engineerNum;
int orderNum;
LocalDateTime startTime;
LocalDateTime endTime;
String status;
}
}
package com.dituhui.pea.pre.controller;
import com.dituhui.pea.common.Result;
import org.springframework.validation.BindException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(BindException.class)
@ResponseBody
public Result<?> handleBindException(BindException e) {
// 处理 BindException 异常并返回自定义错误信息
return Result.failed("Invalid request parameters,"+e.getMessage());
}
@ExceptionHandler(Exception.class)
@ResponseBody
public Result<?> handleException(Exception e) {
return Result.failed(e.getMessage());
}
}
package com.dituhui.pea.pre.controller;
import cn.hutool.core.map.MapUtil;
import com.dituhui.pea.common.Result;
import com.dituhui.pea.pre.opta.domain.Customer;
import com.dituhui.pea.pre.opta.domain.Vehicle;
import com.dituhui.pea.pre.opta.domain.VehicleRoutingSolution;
import com.dituhui.pea.pre.opta.solver.VehicleRoutingConstraintProvider;
import com.dituhui.pea.pre.service.PrepareService;
import com.dituhui.pea.pre.service.SolveService;
import lombok.extern.slf4j.Slf4j;
import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore;
import org.optaplanner.core.api.solver.SolverManager;
import org.optaplanner.core.api.solver.SolverStatus;
import org.optaplanner.core.config.solver.SolverConfig;
import org.optaplanner.core.config.solver.SolverManagerConfig;
import org.optaplanner.core.impl.solver.DefaultSolverManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.*;
/**
* @author zhangx
*/
@Slf4j
@RequestMapping("/pea-pre")
@RestController
public class PrepareController {
@Autowired
PrepareService prepareService;
@Autowired
SolveService solveService;
private SolverManager<VehicleRoutingSolution, UUID> solverManager;
public PrepareController() {
SolverConfig solverConfig = new SolverConfig();
solverConfig.withSolutionClass(VehicleRoutingSolution.class);
solverConfig.withEntityClasses(Vehicle.class);
solverConfig.withConstraintProviderClass(VehicleRoutingConstraintProvider.class);
// solverConfig.with
solverManager = SolverManager.create(solverConfig, new SolverManagerConfig());
}
/*
* 检查指定日期的小组是否有在运行的批次任务,有则返回,没有则创建后返回批次码
*/
@GetMapping("/prepare/solveTest/{groupId}/{batchNo}")
public Result<?> prepareAndSolve(@PathVariable String groupId, @PathVariable String batchNo) {
log.info("prepareSolve, groupId:{}, day:{}", groupId, batchNo);
VehicleRoutingSolution solution = solveService.prepareAndSolveSolution(groupId, batchNo);
List<Vehicle> engineerList = solution.getVehicleList();
List<Customer> customerList = solution.getCustomerList();
HardSoftLongScore score = solution.getScore();
log.info("prepareSolve done, groupId:{}, day:{}, score:{}", groupId, batchNo, score.toString());
Map<String, Object> resultMap = MapUtil.builder(new HashMap<String, Object>()).put("score", score).put("engineers", engineerList).put("customer-size", customerList.size()).build();
return Result.success(resultMap);
}
// 异步任务运行 todo
@GetMapping("/prepare/solveAsync/{groupId}/{batchNo}")
public Result<?> solveAsync(@PathVariable String groupId, @PathVariable String batchNo) {
log.info("调用引擎处理-异步处理, groupId:{}, batchNo:{}", groupId, batchNo);
UUID problemId = solveService.generateProblemId(groupId, batchNo);
// 提交问题开始求解
VehicleRoutingSolution problem = solveService.prepareSolution(groupId, batchNo);
solverManager.solveAndListen(problemId, id -> problem,
this.prepareService::saveAndExtractSolution);
log.error("调用引擎处理-异步处理, 已触发异步, groupId:{}, batchNo:{}", groupId, batchNo);
return Result.success("已触发异步执行");
}
@GetMapping("/prepare/solveStatus/{groupId}/{batchNo}")
public Result<?> solveStatus(@PathVariable String groupId, @PathVariable String batchNo) {
log.info("查询引擎处理状态, groupId:{}, batchNo:{}", groupId, batchNo);
UUID problemId = solveService.generateProblemId(groupId, batchNo);
SolverStatus status = solverManager.getSolverStatus(problemId);
log.info("查询引擎处理状态, groupId:{}, batchNo:{}, status:{}", groupId, batchNo, status.toString());
return Result.success(status);
}
@GetMapping("/prepare/solveStop/{groupId}/{batchNo}")
public Result<?> solveStop(@PathVariable String groupId, @PathVariable String batchNo) {
log.info("停止引擎处理批次, groupId:{}, batchNo:{}", groupId, batchNo);
UUID problemId = solveService.generateProblemId(groupId, batchNo);
solverManager.terminateEarly(problemId);
SolverStatus status = solverManager.getSolverStatus(problemId);
log.info("停止引擎处理批次, groupId:{}, batchNo:{}, status:{}", groupId, batchNo, status.toString());
return Result.success(status);
}
}
package com.dituhui.pea.pre.dao;
import com.dituhui.pea.pre.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.pre.dao;
import com.dituhui.pea.pre.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.pre.dao;
import com.dituhui.pea.pre.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.pre.dao;
import com.dituhui.pea.pre.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.pre.dao;
import com.dituhui.pea.pre.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.pre.dao;
import com.dituhui.pea.pre.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.pre.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.pre.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;
}
package com.dituhui.pea.pre.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;
}
package com.dituhui.pea.pre.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.pre.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.pre.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.pre.interceptor;
import com.fasterxml.jackson.databind.ObjectMapper;
import javax.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import org.springframework.web.servlet.HandlerInterceptor;
@Slf4j
@Aspect
@Component
public class RequestInterceptor implements HandlerInterceptor {
/**
* 以 controller 包下定义的所有请求为切入点
*/
@Pointcut(value = "execution(public * com.dituhui.pea.pre.controller..*.*(..))")
public void reqOpenAPILog() {
}
/**
* 在切点之前织入
*
* @param joinPoint
* @throws Throwable
*/
@Before("reqOpenAPILog()")
public void doBefore(JoinPoint joinPoint) throws Throwable {
// 开始打印请求日志
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest request = attributes.getRequest();
// 打印请求 url
log.info("Request URL : {}", request.getRequestURL().toString());
// 打印 Http method
log.info("HTTP Method : {}", request.getMethod());
// 打印调用 controller 的全路径以及执行方法
log.info("Class Method : {}.{}", joinPoint.getSignature().getDeclaringTypeName(), joinPoint.getSignature().getName());
// 打印请求的 IP
log.info("Request IP : {}", request.getRemoteAddr());
// 打印请求入参
log.info("Request Args : {}", new ObjectMapper().writeValueAsString(joinPoint.getArgs()));
}
/**
* 在切点之后织入
*
* @throws Throwable
*/
@After("reqOpenAPILog()")
public void doAfter() throws Throwable {
// TODO
}
/**
* 环绕
*
* @param proceedingJoinPoint
* @return
* @throws Throwable
*/
@Around("reqOpenAPILog()")
public Object doAround(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
log.info("========================================== Start ==========================================");
long startTime = System.currentTimeMillis();
Object result = proceedingJoinPoint.proceed();
// 打印出参
// log.info("Response Args : {}", result);
log.info("Response Args : {}", new ObjectMapper().writeValueAsString(result));
// 执行耗时
log.info("Time-Consuming : {} ms", System.currentTimeMillis() - startTime);
log.info("=========================================== End ===========================================");
return result;
}
}
package com.dituhui.pea.pre.opta.domain;
public class Customer {
private String id;
private Location location;
// 耗时
private int duration;
private String skill;
public Customer() {
}
public Customer(String id, Location location, int duration, String skill) {
this.id = id;
this.location = location;
this.duration = duration;
this.skill = skill;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public Location getLocation() {
return location;
}
public void setLocation(Location location) {
this.location = location;
}
public int getDuration() {
return duration;
}
public void setDuration(int duration) {
this.duration = duration;
}
public String getSkill() {
return skill;
}
public void setSkill(String skill) {
this.skill = skill;
}
// ************************************************************************
// Complex methods
// ************************************************************************
@Override
public String toString() {
return "Customer{" +
"id='" + id + '\'' +
", location=" + location +
", duration=" + duration +
", skill='" + skill + '\'' +
'}';
}
}
package com.dituhui.pea.pre.opta.domain;
// 起点
public class Depot {
private final String id;
// 类型 分站
private final String type;
private final Location location;
public Depot(String id, String type, Location location) {
this.id = id;
this.type = type;
this.location = location;
}
public String getId() {
return id;
}
public String getType() {
return type;
}
public Location getLocation() {
return location;
}
}
package com.dituhui.pea.pre.opta.domain;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import java.util.Map;
@JsonFormat(shape = JsonFormat.Shape.ARRAY)
@JsonIgnoreProperties({ "id" })
public class Location {
private final String id;
// 类型 engineer order
private final String type;
private final double latitude;
private final double longitude;
private Map<Location, Long> distanceMap;
public Location(String id, String type, double longitude, double latitude ) {
this.id = id;
this.type = type;
this.longitude = longitude;
this.latitude = latitude;
}
@Override
public String toString() {
return "Location{" +
"id='" + id + '\'' +
", type='" + type + '\'' +
", latitude=" + latitude +
", longitude=" + longitude +
'}';
}
public String getId() {
return id;
}
public String getType() {
return type;
}
public double getLatitude() {
return latitude;
}
public double getLongitude() {
return longitude;
}
/**
* Set the distance map. Distances are in meters.
*
* @param distanceMap a map containing distances from here to other locations
*/
public void setDistanceMap(Map<Location, Long> distanceMap) {
this.distanceMap = distanceMap;
}
/**
* Distance to the given location in meters.
*
* @param location other location
* @return distance in meters
*/
public long getDistanceTo(Location location) {
return distanceMap.get(location);
}
// ************************************************************************
// Complex methods
// ************************************************************************
/**
* The angle relative to the direction EAST.
*
* @param location never null
* @return in Cartesian coordinates
*/
public double getAngle(Location location) {
// Euclidean distance (Pythagorean theorem) - not correct when the surface is a sphere
double latitudeDifference = location.latitude - latitude;
double longitudeDifference = location.longitude - longitude;
return Math.atan2(latitudeDifference, longitudeDifference);
}
}
package com.dituhui.pea.pre.opta.domain;
import org.optaplanner.core.api.solver.SolverStatus;
class Status {
public final VehicleRoutingSolution solution;
public final String scoreExplanation;
public final boolean isSolving;
Status(VehicleRoutingSolution solution, String scoreExplanation, SolverStatus solverStatus) {
this.solution = solution;
this.scoreExplanation = scoreExplanation;
this.isSolving = solverStatus != SolverStatus.NOT_SOLVING;
}
}
package com.dituhui.pea.pre.opta.domain;
import org.optaplanner.core.api.domain.entity.PlanningEntity;
import org.optaplanner.core.api.domain.lookup.PlanningId;
import org.optaplanner.core.api.domain.variable.PlanningListVariable;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Set;
@PlanningEntity
public class Vehicle {
@PlanningId
private String id;
private int maxMinute;
// 单位是米,这里要注意
private int maxDistanceMeter;
private Depot depot;
private Set<String> skillSet;
@PlanningListVariable
private List<Customer> customerList;
public Vehicle() {
}
public Vehicle(String id, int maxMinute, int maxDistanceMeter, Depot depot, Set<String> skillSet) {
this.id = id;
this.maxMinute = maxMinute;
this.maxDistanceMeter = maxDistanceMeter;
this.depot = depot;
this.skillSet = skillSet;
this.customerList = new ArrayList<>();
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public int getMaxMinute() {
return maxMinute;
}
public void setMaxMinute(int maxMinute) {
this.maxMinute = maxMinute;
}
public int getMaxDistanceMeter() {
return maxDistanceMeter;
}
public void setMaxDistanceMeter(int maxDistanceMeter) {
this.maxDistanceMeter = maxDistanceMeter;
}
public Depot getDepot() {
return depot;
}
public void setDepot(Depot depot) {
this.depot = depot;
}
public List<Customer> getCustomerList() {
return customerList;
}
public void setCustomerList(List<Customer> customerList) {
this.customerList = customerList;
}
public Set<String> getSkillSet() {
return skillSet;
}
public void setSkillSet(Set<String> skillSet) {
this.skillSet = skillSet;
}
// ************************************************************************
// Complex methods
// ************************************************************************
/**
* @return route of the vehicle
*/
public List<Location> getRoute() {
if (customerList.isEmpty()) {
return Collections.emptyList();
}
List<Location> route = new ArrayList<Location>();
route.add(depot.getLocation());
for (Customer customer : customerList) {
route.add(customer.getLocation());
}
// route.add(depot.getLocation());
return route;
}
private int getTotalDuration() {
int total = 0;
for (Customer customer : customerList) {
total += customer.getDuration();
}
return total;
}
public long getTotalDurationMinutes() {
// 服务耗时
int durationMinute = getTotalDuration();
// 路程耗时
// 驾车每秒种平均距离 (高德导航历史数据计算得出) avg_rate = 7.65
int distance = getTotalDistanceMeters();
double avg_rate = 7.65;
long routeMinute = Math.round(distance / avg_rate / 60);
return durationMinute + routeMinute;
}
public int getTotalDistanceMeters() {
if (customerList.isEmpty()) {
return 0;
}
int total = 0;
Location previousLocation = depot.getLocation();
for (Customer customer : customerList) {
total += previousLocation.getDistanceTo(customer.getLocation());
previousLocation = customer.getLocation();
}
total += previousLocation.getDistanceTo(depot.getLocation());
return total;
}
@Override
public String toString() {
return "Vehicle{" +
"id='" + id + '\'' +
", maxMinute=" + maxMinute +
", maxDistanceMeter=" + maxDistanceMeter +
", depot=" + depot +
", skills=" + skillSet +
", customerList=" + customerList +
'}';
}
}
package com.dituhui.pea.pre.opta.domain;
import org.optaplanner.core.api.domain.solution.PlanningEntityCollectionProperty;
import org.optaplanner.core.api.domain.solution.PlanningScore;
import org.optaplanner.core.api.domain.solution.PlanningSolution;
import org.optaplanner.core.api.domain.solution.ProblemFactCollectionProperty;
import org.optaplanner.core.api.domain.valuerange.ValueRangeProvider;
import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore;
import java.util.List;
@PlanningSolution
public class VehicleRoutingSolution {
private String groupId;
private String batchNo;
@ProblemFactCollectionProperty
private List<Location> locationList;
@ProblemFactCollectionProperty
private List<Depot> depotList;
@PlanningEntityCollectionProperty
private List<Vehicle> vehicleList;
@ProblemFactCollectionProperty
@ValueRangeProvider
private List<Customer> customerList;
@PlanningScore
private HardSoftLongScore score;
public VehicleRoutingSolution() {
}
public VehicleRoutingSolution(String groupId, String batchNo, List<Depot> depotList, List<Vehicle> vehicleList,
List<Customer> customerList, List<Location> locationList) {
this.groupId = groupId;
this.batchNo = batchNo;
this.depotList = depotList;
this.vehicleList = vehicleList;
this.customerList = customerList;
this.locationList = locationList;
}
public String getGroupId() {
return groupId;
}
public void setGroupId(String groupId) {
this.groupId = groupId;
}
public String getBatchNo() {
return batchNo;
}
public void setBatchNo(String batchNo) {
this.batchNo = batchNo;
}
public List<Location> getLocationList() {
return locationList;
}
public void setLocationList(List<Location> locationList) {
this.locationList = locationList;
}
public List<Depot> getDepotList() {
return depotList;
}
public void setDepotList(List<Depot> depotList) {
this.depotList = depotList;
}
public List<Vehicle> getVehicleList() {
return vehicleList;
}
public void setVehicleList(List<Vehicle> vehicleList) {
this.vehicleList = vehicleList;
}
public List<Customer> getCustomerList() {
return customerList;
}
public void setCustomerList(List<Customer> customerList) {
this.customerList = customerList;
}
public HardSoftLongScore getScore() {
return score;
}
public void setScore(HardSoftLongScore score) {
this.score = score;
}
// ************************************************************************
// Complex methods
// ************************************************************************
}
package com.dituhui.pea.pre.opta.domain.geo;
import com.dituhui.pea.pre.opta.domain.Location;
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 interface DistanceCalculator {
/**
* Calculate the distance between {@code from} and {@code to} in meters.
*
* @param from starting location
* @param to target location
* @return distance in meters
*/
long calculateDistance(Location from, Location to);
/**
* Bulk calculation of distance.
* Typically much more scalable than {@link #calculateDistance(Location, Location)} iteratively.
*
* @param fromLocations never null
* @param toLocations never null
* @return never null
*/
default 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)
))
));
}
/**
* Calculate distance matrix for the given list of locations and assign distance maps accordingly.
*
* @param locationList
*/
default void initDistanceMaps(Collection<Location> locationList) {
Map<Location, Map<Location, Long>> distanceMatrix = calculateBulkDistance(locationList, locationList);
locationList.forEach(location -> location.setDistanceMap(distanceMatrix.get(location)));
}
}
package com.dituhui.pea.pre.opta.domain.geo;
import com.dituhui.pea.pre.opta.domain.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;
@Component
public class GeoDistanceCalculator implements DistanceCalculator {
@Override
public 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;
}
}
package com.dituhui.pea.pre.opta.solver;
import com.dituhui.pea.pre.opta.domain.Vehicle;
import org.optaplanner.core.api.score.buildin.hardsoftlong.HardSoftLongScore;
import org.optaplanner.core.api.score.stream.Constraint;
import org.optaplanner.core.api.score.stream.ConstraintFactory;
import org.optaplanner.core.api.score.stream.ConstraintProvider;
import org.springframework.stereotype.Service;
import java.util.Set;
@Service
public class VehicleRoutingConstraintProvider implements ConstraintProvider {
@Override
public Constraint[] defineConstraints(ConstraintFactory factory) {
return new Constraint[]{
// skillMatchHard(factory),
mixCountHard(factory),
maxTimeCapacityHard(factory),
maxDistanceHard(factory),
totalDistance(factory),
avgCustomers(factory),
};
}
//
protected Constraint skillMatchHard(ConstraintFactory factory) {
return factory.forEach(Vehicle.class).filter(vehicle -> vehicle.getCustomerList().size() > 0)
.penalizeLong(HardSoftLongScore.ofUninitialized(0, 100, 0),
vehicle -> {
Set<String> skillSet = vehicle.getSkillSet();
final int[] missCount = {0};
vehicle.getCustomerList().forEach(customer -> {
if (!skillSet.contains(customer.getSkill())) {
missCount[0] = missCount[0] + 1;
}
});
return missCount[0];
})
.asConstraint("技能匹配");
}
// 最少n单
public Constraint mixCountHard(ConstraintFactory factory) {
return factory.forEach(Vehicle.class).filter(vehicle -> vehicle.getCustomerList().size() <= 2)
.penalizeLong(HardSoftLongScore.ofUninitialized(0, 100, 0),
vehicle -> {
// int score = 15 - vehicle.getCustomerList().size();
return 1;
})
.asConstraint("最小单数约束(3单)");
}
// 总时长限制
protected Constraint maxTimeCapacityHard(ConstraintFactory factory) {
return factory.forEach(Vehicle.class)
.filter(vehicle -> {
long span = vehicle.getTotalDurationMinutes() - vehicle.getMaxMinute();
return span > 0;
})
.penalizeLong(HardSoftLongScore.ofUninitialized(0, 20, 0),
vehicle -> {
long span = vehicle.getTotalDurationMinutes() - vehicle.getMaxMinute();
return span / 60;
})
.asConstraint("工作时长限制");
}
// 总里程限制
protected Constraint maxDistanceHard(ConstraintFactory factory) {
return factory.forEach(Vehicle.class).filter(vehicle -> {
int total = vehicle.getTotalDistanceMeters();
int max = vehicle.getMaxDistanceMeter();
int diff = total - max;
// Log.infof("DistanceHard filter vehicle:%s, customlist.size:%d , total-distance:%d, max-meter:%d diff:%s",
// vehicle.getId(), vehicle.getCustomerList().size(), total, max, diff);
return diff > 0;
})
.penalizeLong(HardSoftLongScore.ofUninitialized(0, 20, 0),
vehicle -> {
int total = vehicle.getTotalDistanceMeters();
int max = vehicle.getMaxDistanceMeter();
int diff = total - max;
// Log.infof("DistanceHard vehicle:%s, customlist.size:%d , total-distance:%d, max-meter:%d diff:%s",
// vehicle.getId(), vehicle.getCustomerList().size(), total, max, diff);
return diff / 1000;
})
.asConstraint("最大里程约束");
}
/*
protected Constraint vehicleDistanceCapacity(ConstraintFactory factory) {
return factory.forEach(Vehicle.class)
.filter(vehicle -> {
int span = Math.abs(vehicle.getTotalDistanceMeters() - vehicle.getMaxDistance());
double rate = Math.round(span / vehicle.getMaxDistance());
return rate > 0.2;
})
.penalizeLong(HardSoftLongScore.ONE_HARD,
vehicle -> {
int span = Math.abs(vehicle.getTotalDistanceMeters() - vehicle.getMaxDistance());
double rate = Math.round(span / vehicle.getMaxDistance());
return span;
})
.asConstraint("vehicleDistanceCapacity");
}*/
// ************************************************************************
// Soft constraints
// ************************************************************************
// 路径最短为最优
protected Constraint totalDistance(ConstraintFactory factory) {
return factory.forEach(Vehicle.class)
.penalizeLong(HardSoftLongScore.ofUninitialized(0, 1000, 10),
vehicle-> vehicle.getTotalDistanceMeters()/1000)
.asConstraint("里程最短最优");
}
// 车辆间货物均衡为最优级
protected Constraint avgCustomers(ConstraintFactory factory) {
return factory.forEach(Vehicle.class)
.penalizeLong(HardSoftLongScore.ofUninitialized(0, 100, 0),
vehicle -> Math.abs(9 - vehicle.getCustomerList().size()))
.asConstraint("车辆间货物均衡最优");
}
}
package com.dituhui.pea.pre.scheduler;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
* @author zhangx
*/
@Slf4j
@Component
public class Scheduler {
// @Scheduled(fixedRate = 1000*10)
public void RunLog(){
log.info("RunLog");
}
// @Scheduled(cron = "${pre-dispatch.cron.expr}")
public void dispatchRun(){
log.info("dispatchRun");
}
}
package com.dituhui.pea.pre.service;
import com.dituhui.pea.pre.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);
}
package com.dituhui.pea.pre.service;
import cn.hutool.crypto.SecureUtil;
import com.dituhui.pea.pre.opta.domain.VehicleRoutingSolution;
import java.sql.SQLException;
import java.util.UUID;
/**
* @author zhangx
* <p>
* 排班算法数据准备
* 排班结果解析到dispatch_order(更新补充技术员工号、上门时间) ,order_appointment、order_request
*/
public interface PrepareService {
/*
* 将计算结果回写到dispatch2个表、以及order两个表
* 是下面两个方法的包装
* */
void saveAndExtractSolution(VehicleRoutingSolution solution) throws RuntimeException;
/*
* 将计算结果回写到dispatch_order表(更新补充技术员工号、上门时间)
* */
void saveSolutionToDispatch(String groupId, String batchNo, VehicleRoutingSolution 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.pre.service;
import com.dituhui.pea.pre.opta.domain.VehicleRoutingSolution;
import org.optaplanner.core.api.solver.SolverStatus;
import java.sql.SQLException;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
/**
* @author zhangx
* <p>
* 排班算法执行
*/
public interface SolveService {
/*
* 按小组、批次号组装问题对象
* 调用optaplaner计算输出结果
* */
VehicleRoutingSolution prepareAndSolveSolution(String groupId, String batchNo);
UUID generateProblemId(String groupId, String batchNo);
VehicleRoutingSolution prepareSolution(String groupId, String batchNo) ;
}
package com.dituhui.pea.pre.service.impl;
import com.dituhui.pea.pre.dao.DispatchBatchRepository;
import com.dituhui.pea.pre.entity.DispatchBatch;
import com.dituhui.pea.pre.service.BatchService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.ScalarHandler;
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;
private QueryRunner queryRunner;
@Autowired
private JdbcTemplate jdbcTemplate;
public BatchServiceImpl(DataSource dataSource) {
this.queryRunner = new QueryRunner(dataSource);
}
// 生成最新批次号
private String calcBatchNo(String day) {
// 定义日期时间格式
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HHmm");
// 将当前时间转换为字符串
String result = LocalTime.now().format(formatter);
return day.replaceAll("-","") + "-" + result;
}
// 查询当前小组内已配置有中心点的技术员数量
private int queryEnginerCount(String groupId) {
int result = 0;
String sql = "select count(1) from engineer_info a left join engineer_business b on a.engineer_code=b.engineer_code " +
" where group_id=? and b.x is not null and b.x !='' ";
ScalarHandler<Long> scal = new ScalarHandler<>();
try {
result = Math.toIntExact(queryRunner.query(sql, scal, groupId));
} catch (SQLException e) {
throw new RuntimeException(e);
}
return result;
}
// 查询待指派工单数量
private int queryOrderCount(String groupId, String batchDay) {
int result = 0;
String sql = "select count(1) from order_request where org_group_id=? " +
" and status='OPEN' and appointment_status='NOT_ASSIGNED' and appointment_method like 'AUTO%' " +
" and expect_time_begin between ? and ? " +
" order by create_time asc ";
ScalarHandler<Long> scal = new ScalarHandler<>();
try {
long size = queryRunner.query(sql, scal, groupId, batchDay + " 00:00:00", batchDay + " 23:59:59");
result = Math.toIntExact(size);
} catch (SQLException e) {
throw new RuntimeException(e);
}
return 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.pre.service.impl;
import cn.hutool.crypto.SecureUtil;
import com.dituhui.pea.pre.dao.DispatchEngineerRepository;
import com.dituhui.pea.pre.dao.DispatchOrderRepository;
import com.dituhui.pea.pre.opta.domain.*;
import com.dituhui.pea.pre.opta.domain.geo.DistanceCalculator;
import com.dituhui.pea.pre.opta.solver.VehicleRoutingConstraintProvider;
import com.dituhui.pea.pre.service.PrepareService;
import com.dituhui.pea.pre.service.SolveService;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.dbutils.QueryRunner;
import org.apache.commons.dbutils.handlers.ColumnListHandler;
import org.optaplanner.core.api.score.buildin.hardsoft.HardSoftScore;
import org.optaplanner.core.api.solver.*;
import org.optaplanner.core.config.solver.SolverConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import javax.sql.DataSource;
import java.sql.SQLException;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;
import java.util.stream.Stream;
@Slf4j
@Service
public class SolveServiceImpl implements SolveService {
@Autowired
DistanceCalculator distanceCalculator;
@Autowired
DispatchEngineerRepository dispatchEngineerRepo;
@Autowired
DispatchOrderRepository dispatchOrderRepo;
@Autowired
PrepareService prepareService;
private QueryRunner queryRunner;
public SolveServiceImpl(DataSource dataSource) {
this.queryRunner = new QueryRunner(dataSource);
}
// 查询技术员所有技能集
private ArrayList<String> queryEngineerSkills(String engineerCode) {
List<String> result = List.of();
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};
ColumnListHandler<String> colu = new ColumnListHandler<>("skill");
try {
result = queryRunner.query(sql, param, colu);
} catch (SQLException e) {
throw new RuntimeException(e);
}
return (ArrayList<String>) result;
}
// 按小组、批次号组装问题对象
@Override
public VehicleRoutingSolution prepareSolution(String groupId, String batchNo) {
log.info("组织问题对象, groupId:{}, batchNo:{}", groupId, batchNo);
// depotlist
ArrayList<Depot> depotList = new ArrayList<Depot>();
/* 统一出发地暂时不加
Optional<OrgGroup> optional = groupRepository.findByGroupId(groupId);
if (optional.isPresent()) {
OrgGroup oneGroup = optional.get();
Location location = new Location(oneGroup.getGroupId(), "起点", Double.parseDouble(oneGroup.getX()), Double.parseDouble(oneGroup.getY()));
Depot depot = new Depot(oneGroup.getGroupId(), "", location);
depots.add(depot);
}*/
// vehiclelist
ArrayList<Vehicle> vehicleList = new ArrayList<>();
dispatchEngineerRepo.findByGroupIdAndBatchNo(groupId, batchNo).forEach(engineer -> {
Location location = new Location(engineer.getEngineerCode(), "中心点", Double.parseDouble(engineer.getX()), Double.parseDouble(engineer.getY()));
Depot depot = new Depot(engineer.getEngineerCode(), "中心点", location);
depotList.add(depot);
ArrayList<String> skillList = queryEngineerSkills(engineer.getEngineerCode());
Vehicle vehicle = new Vehicle(engineer.getEngineerCode(), engineer.getMaxMinute(), engineer.getMaxDistance() * 1000, depot, Set.copyOf(skillList));
vehicleList.add(vehicle);
});
// customerlist
ArrayList<Customer> customerList = new ArrayList<>();
dispatchOrderRepo.findByGroupIdAndBatchNo(groupId, batchNo).forEach(order -> {
Location location = new Location(order.getOrderId(), "工单", Double.parseDouble(order.getX()), Double.parseDouble(order.getY()));
Customer customer = new Customer(order.getOrderId(), location, order.getTakeTime(), order.getSkills());
customerList.add(customer);
});
//locationlist
List<Location> locationList = Stream.concat(depotList.stream().map(Depot::getLocation), customerList.stream().map(Customer::getLocation)).collect(Collectors.toList());
VehicleRoutingSolution solution = new VehicleRoutingSolution(groupId, batchNo, depotList, vehicleList, customerList, locationList);
distanceCalculator.initDistanceMaps(locationList);
log.info("组织问题对象, groupId:{}, batchNo:{}, employ-centerpoi-size:{}, employ-size:{}, customer-size:{}, center-location-size:{}", groupId, batchNo, depotList.size(), vehicleList.size(), customerList.size(), locationList.size());
return solution;
}
/*
* 按小组、批次号组装问题对象
* 调用optaplaner计算输出结果
* */
@Override
public VehicleRoutingSolution prepareAndSolveSolution(String groupId, String batchNo) {
log.info("组织问题对象/调用引擎处理, groupId:{}, batchNo:{}", groupId, batchNo);
// Load the problem
VehicleRoutingSolution problem = prepareSolution(groupId, batchNo);
SolverConfig solverConfig = new SolverConfig().withSolutionClass(VehicleRoutingSolution.class).withEntityClasses(Vehicle.class).withConstraintProviderClass(VehicleRoutingConstraintProvider.class).withTerminationSpentLimit(Duration.ofSeconds(10));
SolverFactory<VehicleRoutingSolution> solverFactory = SolverFactory.create(solverConfig);
// Solve the problem
log.info("调用引擎处理-开始, groupId:{}, batchNo:{}", groupId, batchNo);
Solver<VehicleRoutingSolution> solver = solverFactory.buildSolver();
VehicleRoutingSolution 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;
}
}
server:
port: 8012
pre-dispatch:
cron:
expr: 0 */5 8-18 * * ?
spring:
application:
name: project-pre-dispatch
jackson:
default-property-inclusion: NON_NULL
# time-zone: GMT+8
date-format: yyyy-MM-dd HH:mm:ss
cloud:
nacos:
discovery:
server-addr: 127.0.0.1:8848
group: project
config:
server-addr: 127.0.0.1:8848
group: project
file-extension: yaml
import-check:
# no config file
enabled: false
config:
import:
- optional:nacos:project-order.yaml
# - optional:nacos:datasource-config.yaml
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3388/saas_aftersale_test?serverTimezone=UTC
username: root
password: 123456
type: com.alibaba.druid.pool.DruidDataSource
redis:
database: 0
host: 127.0.0.1
port: 6379
password: 123456
jedis:
pool:
max-active: 32
min-idle: 0
max-idle: 8
max-wait: -1
jpa:
show-sql: true
hibernate:
ddl-auto: none
seata:
application-id: ${spring.application.name}
tx-service-group: ${spring.application.name}-group
service:
vgroup-mapping:
project-order-group: default
grouplist:
default: 127.0.0.1:8091
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{yyyy-MM-dd'T'HH:mm:ss.SSSZ} %level [%thread] %logger{15} : %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<!-- <root level="DEBUG">-->
<appender-ref ref="CONSOLE" />
</root>
</configuration>
package com.dituhui.pea.pre;
import cn.hutool.core.util.IdUtil;
import com.dituhui.pea.pre.service.BatchService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
//import JpaTransactionManager;
import java.sql.SQLException;
@Slf4j
@SpringBootTest
public class BatchServiceTest {
@Autowired
BatchService batchService;
String groupId = "gsuzhou";
String day = "2023-07-05";
@Test
public void test1() {
log.info("init");
try {
batchService.buildBatchNo(groupId, "2023-07-04");
batchService.buildBatchNo(groupId, "2023-07-05");
batchService.buildBatchNo(groupId, "2023-07-06");
batchService.buildBatchNo(groupId, "2023-07-07");
} catch (SQLException e) {
log.info("error %s", e);
throw new RuntimeException(e);
}
log.info("done");
}
@Test
public void test2() {
log.info("init");
for (int i = 0; i < 100; i++) {
String orderNO= IdUtil.getSnowflake().nextIdStr();
log.info("oid:{}", orderNO);
}
}
}
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!