Commit 4db2bba4 by 丁伟峰

工程师容量初始化、定时计算,改用了java/schedule模式

1 parent cf6f6c13
......@@ -19,12 +19,13 @@ package com.dituhui.pea.order;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.scheduling.annotation.EnableScheduling;
/**
* @author TrevorLink
*/
@SpringBootApplication
@EnableScheduling
@EnableFeignClients(basePackages = {"com.dituhui.pea.user", "com.dituhui.pea.order"})
public class OrderServiceApplication {
......
......@@ -22,4 +22,6 @@ public interface CapacityEngineerCalendarDao extends JpaRepository<CapacityEngin
@Modifying
@Query("delete from CapacityEngineerCalendarEntity a where a.engineerCode in :engineerCodes and a.type = :type and a.workday between :startDate and :endDate")
void deleteByTypeAndEngineerCodesAndBetweenDates(List<String> engineerCodes, String type, String startDate, String endDate);
List<CapacityEngineerCalendarEntity> findCalendarByWorkdayAndEngineerCode(String date, String engineerCode);
}
package com.dituhui.pea.order.scheduler;
import com.dituhui.pea.order.common.DateUtils;
import com.dituhui.pea.order.dao.*;
import com.dituhui.pea.order.entity.*;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import java.util.stream.Collectors;
@Slf4j
@Component
public class CalcEngineerCapacityScheduler {
@Value("${scheduler.calc-engineer-capacity.day-offset-begin}")
private int dayOffsetBegin;
@Value("${scheduler.calc-engineer-capacity.day-offset-end}")
private int dayOffsetEnd;
@Autowired
private CapacityEngineerStatDao capacityEngineerStatDao;
@Autowired
private OrderInfoDao orderInfoDao;
@Autowired
private CapacityEngineerCalendarDao capacityEngineerCalendarDao;
@Autowired
private EngineerBusinessDao engineerBusinessDao;
@Autowired
private EngineerInfoDao engineerInfoDao;
@Scheduled(cron = "${scheduler.calc-engineer-capacity.cron-expr}")
public void run() {
log.info("开始初始化,所有工程师的容量将根据日历表的记录进行计算设置");
String bdate = DateUtils.formatDate(LocalDate.now().plusDays(dayOffsetBegin));
String edate = DateUtils.formatDate(LocalDate.now().plusDays(dayOffsetEnd));
calcAllEngineerByDays(bdate, edate);
}
private void calcAllEngineerByDays(String bdate, String edate) {
LocalDate currentDate = DateUtils.localDateFromStr(bdate);
LocalDate endDate = DateUtils.localDateFromStr(edate);
List<String> allEngineerCodes = engineerInfoDao.findAll().stream().map(EngineerInfoEntity::getEngineerCode).collect(Collectors.toList());
while (!currentDate.isAfter(endDate)) {
for (String engineerCode : allEngineerCodes) {
calcOneEngineer(DateUtils.formatDate(currentDate), engineerCode);
}
currentDate = currentDate.plusDays(1);
}
}
private void calcOneEngineer(String date, String engineerCode) {
Set<String> ss = Set.of("CANCELED", "RESCHEDULED");
List<OrderInfoEntity> orders = orderInfoDao.findByDtAndEngineerCode(DateUtils.localDateFromStr(date), engineerCode);
int used = orders.stream().map(e -> {
if (ss.contains(e.getOrderStatus())) {
return 0;
} else {
return e.getTakeTime();
}
}).mapToInt(Integer::intValue).sum();
long cnt = orders.stream()
.filter(e -> !ss.contains(e.getOrderStatus()))
.count();
long max = getMaxRemainBlock(date, engineerCode, orders);
log.info("正在处理: 日期[{}]技术员[{}]容量相关信息 ==> used:{}, orderCnt:{}, maxDuration:{}", date, engineerCode, used, cnt, max);
CapacityEngineerStatEntity statEntity = capacityEngineerStatDao.getByWorkdayAndEngineerCode(date, engineerCode);
if (statEntity == null) {
statEntity = new CapacityEngineerStatEntity();
statEntity.setWorkday(date);
statEntity.setEngineerCode(engineerCode);
statEntity.setCreateTime(LocalDateTime.now());
}
statEntity.setOrderCount((int) cnt);
statEntity.setCapUsed(used);
statEntity.setCapLeft(statEntity.getCapTotal() - used);
statEntity.setMaxDuration((int) max);
statEntity.setUpdateTime(LocalDateTime.now());
capacityEngineerStatDao.save(statEntity);
}
private long getMaxRemainBlock(String date, String engineerCode, List<OrderInfoEntity> orders) {
// 根据capacity_engineer_calendar和order_info,来确定当天剩下的最大连续时间区块;
EngineerBusinessEntity businessEntity = engineerBusinessDao.getByEngineerCode(engineerCode);
LocalDateTime startTime = DateUtils.localDateTimeFromStr(String.format("%s %s:00", date, businessEntity.getWorkOn()));
LocalDateTime endTime = DateUtils.localDateTimeFromStr(String.format("%s %s:00", date, businessEntity.getWorkOff()));
List<OccupyInfo> occupyInfos = new ArrayList<>();
List<CapacityEngineerCalendarEntity> configs = capacityEngineerCalendarDao.findCalendarByWorkdayAndEngineerCode(date, engineerCode);
if (!configs.isEmpty()) {
occupyInfos.addAll(
configs.stream().map(e -> new OccupyInfo().setBeginTime(e.getStartTime()).setEndTime(e.getEndTime())).collect(Collectors.toList())
);
}
if (!orders.isEmpty()) {
occupyInfos.addAll(
orders.stream().map(e -> new OccupyInfo().setBeginTime(e.getPlanStartTime()).setEndTime(e.getPlanEndTime())).collect(Collectors.toList())
);
}
if (occupyInfos.isEmpty()) {
return Duration.between(startTime, endTime).toMinutes();
} else {
occupyInfos.sort(Comparator.comparing(OccupyInfo::getBeginTime));
// 从 occupyInfos的配置间隙中,获取最大的闲时段,理论上,上面的配置段之间,是不会交叉的,如果交叉,那是存在问题的!
List<Long> idlePeriods = new ArrayList<>();
LocalDateTime preLast = startTime;
for (OccupyInfo o : occupyInfos) {
if (o.getBeginTime().isAfter(preLast)) {
idlePeriods.add(Duration.between(startTime, o.getBeginTime()).toMinutes());
}
preLast = o.getEndTime();
}
if (preLast.isBefore(endTime)) {
idlePeriods.add(Duration.between(preLast, endTime).toMinutes());
}
return Collections.max(idlePeriods);
}
}
@Data
@Accessors(chain = true)
private static class OccupyInfo {
private LocalDateTime beginTime;
private LocalDateTime endTime;
}
}
package com.dituhui.pea.order.scheduler;
import com.dituhui.pea.order.common.DateUtils;
import com.dituhui.pea.order.dao.CapacityEngineerCalendarDao;
import com.dituhui.pea.order.dao.CapacityEngineerStatDao;
import com.dituhui.pea.order.dao.EngineerBusinessDao;
import com.dituhui.pea.order.dao.EngineerInfoDao;
import com.dituhui.pea.order.entity.CapacityEngineerCalendarEntity;
import com.dituhui.pea.order.entity.CapacityEngineerStatEntity;
import com.dituhui.pea.order.entity.EngineerBusinessEntity;
import com.dituhui.pea.order.entity.EngineerInfoEntity;
import lombok.Data;
import lombok.experimental.Accessors;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Component
public class InitEngineerCapacityScheduler {
@Value("${scheduler.init-engineer-capacity.day-offset-begin}")
private int dayOffsetBegin;
@Value("${scheduler.init-engineer-capacity.day-offset-end}")
private int dayOffsetEnd;
@Value("${scheduler.init-engineer-capacity.rewrite-force}")
private boolean rewriteForce;
@Autowired
private EngineerBusinessDao engineerBusinessDao;
@Autowired
private CapacityEngineerCalendarDao capacityEngineerCalendarDao;
@Autowired
private CapacityEngineerStatDao capacityEngineerStatDao;
@Autowired
private EngineerInfoDao engineerInfoDao;
private boolean verifyCalendar(List<CapacityEngineerCalendarEntity> configs) {
// 检查多条请假配置是否有交叉行为; configs已经根据startTime排序
for (int i = 0; i < configs.size(); i++) {
for (int j = i + 1; j < configs.size(); j++) {
CapacityEngineerCalendarEntity config1 = configs.get(i);
CapacityEngineerCalendarEntity config2 = configs.get(j);
// 必须是 config1的start<end; config2的start<end, 且config1.end <= config2.start
if (config1.getStartTime().isAfter(config1.getEndTime())
|| config2.getStartTime().isAfter(config2.getStartTime())
|| config1.getEndTime().isAfter(config2.getStartTime())) {
return false;
}
}
}
return true;
}
private long sumLeaveTime(List<CapacityEngineerCalendarEntity> configs) {
return configs.stream().mapToLong(e -> Duration.between(e.getStartTime(), e.getEndTime()).toMinutes()).sum();
}
private CapacityStats calculateWorkTime(String date, String engineerCode, List<CapacityEngineerCalendarEntity> configs) {
// 计算一个工程师,一天的工作容量信息
// 省略实现细节
CapacityStats r = new CapacityStats();
EngineerBusinessEntity businessEntity = engineerBusinessDao.getByEngineerCode(engineerCode);
LocalDateTime startTime = DateUtils.localDateTimeFromStr(String.format("%s %s:00", date, businessEntity.getWorkOn()));
LocalDateTime endTime = DateUtils.localDateTimeFromStr(String.format("%s %s:00", date, businessEntity.getWorkOff()));
long totalWorkTime = Duration.between(startTime, endTime).toMinutes();
long totalLeaveTime = 0;
if (configs.isEmpty()) {
log.warn("日期[{}]技术员[{}]无记录,当全勤处理", date, engineerCode);
} else {
totalLeaveTime = sumLeaveTime(configs);
}
return new CapacityStats().setTotal(totalWorkTime).setUsed(totalLeaveTime).setRemain(totalWorkTime - totalLeaveTime);
}
private void initOneEngineer(String date, String engineerCode) {
log.info("正在处理日期[{}] 技术员[{}]", date, engineerCode);
// 初始化一个工程师、一天的容量
CapacityEngineerStatEntity statEntity = capacityEngineerStatDao.getByWorkdayAndEngineerCode(date, engineerCode);
if (statEntity != null && !rewriteForce) {
log.error("技术员容量信息记录已存在, 直接返回");
return;
}
List<CapacityEngineerCalendarEntity> configs = capacityEngineerCalendarDao.findCalendarByWorkdayAndEngineerCode(date, engineerCode)
.stream().sorted(Comparator.comparing(CapacityEngineerCalendarEntity::getStartTime)).collect(Collectors.toList());
if (!configs.isEmpty() && !verifyCalendar(configs)) {
log.error("配置检查失败,忽略退出");
return;
}
String memo = configs.stream().map(CapacityEngineerCalendarEntity::getType).collect(Collectors.joining("/"));
log.info("日期[{}] 技术员[{}] 有日历记录需要特别处理 === {}", date, engineerCode, memo);
CapacityStats stats = calculateWorkTime(date, engineerCode, configs);
log.info("日期[{}]技术员[{}],总容量[{}] 占用容量[{}] 剩余容量[{}]", date, engineerCode, stats.getTotal(), stats.getUsed(), stats.getRemain());
if (statEntity == null) {
statEntity = new CapacityEngineerStatEntity();
statEntity.setEngineerCode(engineerCode);
statEntity.setWorkday(date);
statEntity.setCapUsedTravel(0);
statEntity.setOrderCount(0);
statEntity.setCreateTime(LocalDateTime.now());
}
statEntity.setCapTotal((int) stats.getTotal());
statEntity.setCapUsed((int) stats.getUsed());
statEntity.setCapLeft((int) stats.getRemain());
statEntity.setMaxDuration((int) stats.getRemain());
statEntity.setMemo(memo);
statEntity.setUpdateTime(LocalDateTime.now());
capacityEngineerStatDao.save(statEntity);
log.info("====== 处理完毕 ======");
}
private void initAllEngineerByDays(String bdate, String edate) {
log.info("==== initAllEngineerByDays, bdate[{}] edate[{}]", bdate, edate);
LocalDate currentDate = DateUtils.localDateFromStr(bdate);
LocalDate endDate = DateUtils.localDateFromStr(edate);
List<String> allEngineerCodes = engineerInfoDao.findAll().stream().map(EngineerInfoEntity::getEngineerCode).collect(Collectors.toList());
while (!currentDate.isAfter(endDate)) {
for (String engineerCode : allEngineerCodes) {
initOneEngineer(DateUtils.formatDate(currentDate), engineerCode);
}
currentDate = currentDate.plusDays(1);
}
}
@Scheduled(cron = "${scheduler.init-engineer-capacity.cron-expr}")
public void run() {
log.info("开始初始化,所有工程师的容量将根据日历表的记录进行计算设置");
String bdate = DateUtils.formatDate(LocalDate.now().plusDays(dayOffsetBegin));
String edate = DateUtils.formatDate(LocalDate.now().plusDays(dayOffsetEnd));
initAllEngineerByDays(bdate, edate);
}
@Data
@Accessors(chain = true)
private static class CapacityStats {
private long total;
private long used;
private long remain;
}
}
......@@ -60,3 +60,14 @@ SaaS:
url: https://pea-test.bshg.com.cn
ak: 64e1cde3f9144bfb850b7d37c51af559
scheduler:
init-engineer-capacity:
cron-expr: 0 */10 8-18 * * ?
day-offset-begin: 0
day-offset-end: 14
rewrite-force: true
calc-engineer-capacity:
cron-expr: 0 */5 8-23 * * ?
day-offset-begin: 0
day-offset-end: 14
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!