feat:生成甘特图添加生产时间安排以及是否跳过节假日条件

main
HuangHuiKang 3 days ago
parent b19632ab32
commit f5be8bc03a

@ -185,6 +185,9 @@ public interface ErrorCodeConstants {
ErrorCode UNSUPPORTED_CAPACITY_TYPE = new ErrorCode(100_301_0009, "不支持的产能来源: {}");
ErrorCode SCHEDULE_DELIVERY_DATE_EMPTY = new ErrorCode(100_301_0010, "订单交期不能为空taskDetailId={}");
ErrorCode SCHEDULE_DEVICE_SELECT_FAILED = new ErrorCode(100_301_0011, "设备分配失败无可用设备productId={}");
ErrorCode SCHEDULE_TIME_FORMAT_INVALID = new ErrorCode(100_301_0012, "排产时间格式错误start={}, end={}格式需为HH:mm");
ErrorCode SCHEDULE_TIME_RANGE_INVALID = new ErrorCode(100_301_0013, "排产时间范围非法start={}, end={},结束时间必须晚于开始时间");
ErrorCode SCHEDULE_WORK_HOURS_INVALID = new ErrorCode(100_301_0014, "排产工时非法start={}, end={}可用工时必须大于0");

@ -2,14 +2,17 @@ package cn.iocoder.yudao.module.mes.controller.admin.task.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;
@Data
@AllArgsConstructor
@NoArgsConstructor
public class DeviceCandidate {
private Long deviceId;
private String deviceName;
private Integer capacityValue;
private LocalDateTime nextAvailableTime;
}

@ -7,6 +7,7 @@ import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import java.util.List;
@ -31,4 +32,17 @@ public class TaskOneClickScheduleReqVO {
@Schema(description = "产能来源1-额定产能 2-每日报工平均值 3-数据采集产能", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "产能来源不能为空")
private Integer capacityType;
@Schema(description = "每日开工时间(HH:mm)")
@NotBlank(message = "每日开工时间不能为空")
private String workStartTime;
@Schema(description = "每日收工时间(HH:mm)")
@NotBlank(message = "每日收工时间不能为空")
private String workEndTime;
@Schema(description = "是否跳过节假日true-跳过")
@NotNull(message = "是否跳过节假日不能为空")
private Boolean skipHoliday;
}

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mes.dal.mysql.calholiday;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.*;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
@ -9,6 +10,8 @@ import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.module.mes.dal.dataobject.calholiday.CalHolidayDO;
import org.apache.ibatis.annotations.Mapper;
import cn.iocoder.yudao.module.mes.controller.admin.calholiday.vo.*;
import org.apache.ibatis.annotations.Param;
import java.time.ZoneId;
import java.time.LocalTime;
/**
@ -44,4 +47,7 @@ public interface CalHolidayMapper extends BaseMapperX<CalHolidayDO> {
}
Integer countHolidayInRange(
@Param("start") LocalDateTime start,
@Param("end") LocalDateTime end);
}

@ -1,6 +1,7 @@
package cn.iocoder.yudao.module.mes.service.task;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.common.pojo.PageParam;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
@ -18,11 +19,13 @@ import cn.iocoder.yudao.module.mes.controller.admin.deviceledger.enums.CapacityT
import cn.iocoder.yudao.module.mes.controller.admin.plan.vo.PlanSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.plan.vo.PlanStatusEnum;
import cn.iocoder.yudao.module.mes.controller.admin.task.vo.*;
import cn.iocoder.yudao.module.mes.dal.dataobject.calholiday.CalHolidayDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.deviceledger.DeviceLedgerDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.plan.PlanDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.task.TaskDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.task.TaskDetailDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.task.ViewTaskProductSummary;
import cn.iocoder.yudao.module.mes.dal.mysql.calholiday.CalHolidayMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.deviceledger.DeviceLedgerMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.plan.PlanMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.task.TaskDetailMapper;
@ -41,8 +44,11 @@ import org.springframework.transaction.annotation.Transactional;
import org.springframework.validation.annotation.Validated;
import javax.annotation.Resource;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.*;
@ -82,6 +88,9 @@ public class TaskServiceImpl implements TaskService {
@Resource
private DeviceLedgerService deviceLedgerService;
@Resource
private CalHolidayMapper calHolidayMapper;
@Resource
private AutoCodeUtil autoCodeUtil;
@Resource
@ -428,9 +437,35 @@ public class TaskServiceImpl implements TaskService {
if (reqVO == null || CollUtil.isEmpty(reqVO.getCreateReqVO())) {
return Collections.emptyList();
}
Map<Long, TaskDO> taskCache = new HashMap<>();
Map<Long, ErpProductDO> productCache = new HashMap<>();
//是否跳过节假日
boolean skipHoliday = Boolean.TRUE.equals(reqVO.getSkipHoliday());
// 解析本次请求的工作时段参数
LocalTime workStart;
LocalTime workEnd;
try {
workStart = LocalTime.parse(reqVO.getWorkStartTime()); // HH:mm
workEnd = LocalTime.parse(reqVO.getWorkEndTime()); // HH:mm
} catch (Exception e) {
throw exception(SCHEDULE_TIME_FORMAT_INVALID, reqVO.getWorkStartTime(), reqVO.getWorkEndTime());
}
if (!workEnd.isAfter(workStart)) {
throw exception(SCHEDULE_TIME_RANGE_INVALID, reqVO.getWorkStartTime(), reqVO.getWorkEndTime());
}
long dailyWorkMinutes = ChronoUnit.MINUTES.between(workStart, workEnd);
if (dailyWorkMinutes <= 0) {
throw exception(SCHEDULE_WORK_HOURS_INVALID, reqVO.getWorkStartTime(), reqVO.getWorkEndTime());
}
BigDecimal dailyWorkHours = BigDecimal.valueOf(dailyWorkMinutes)
.divide(BigDecimal.valueOf(60), 6, RoundingMode.HALF_UP);
// 1) 先按规则排序
List<PlanSaveReqVO> sortedPlans = new ArrayList<>(reqVO.getCreateReqVO());
// 排序前:若明细交期为空,则回填为订单交期
@ -484,34 +519,30 @@ public class TaskServiceImpl implements TaskService {
// 构建设备候选(含选中口径的产能 + nextAvailable
List<DeviceCandidate> candidates = new ArrayList<>();
for (ProductRelationRespVO rel : deviceRels) {
if (rel == null || rel.getId() == null) {
DeviceLedgerDO device = deviceLedgerMapper.selectById(rel.getId());
Integer dailyCapacity = capacityType.getCapacity(device); // 每日产能
if (dailyCapacity == null || dailyCapacity <= 0) {
continue;
}
Long deviceId = rel.getId();
DeviceLedgerDO device = deviceLedgerMapper.selectById(deviceId);
if (device == null) {
continue;
}
Integer capacityValue = capacityType.getCapacity(device);
if (capacityValue == null || capacityValue <= 0) {
continue; // 当前口径产能无效则跳过
}
LocalDateTime nextTime = deviceNextAvailableTime.get(deviceId);
if (nextTime == null) {
nextTime = queryDeviceNextAvailableTime(deviceId);
deviceNextAvailableTime.put(deviceId, nextTime);
}
LocalDateTime nextAvailable = deviceNextAvailableTime.computeIfAbsent(
rel.getId(),
id -> queryDeviceNextAvailableTime(id, workStart, workEnd, skipHoliday)
);
nextAvailable = normalizeToWorkTime(nextAvailable, workStart, workEnd, skipHoliday);
candidates.add(new DeviceCandidate(deviceId, device.getDeviceName(), capacityValue, nextTime));
}
DeviceCandidate c = new DeviceCandidate();
c.setDeviceId(rel.getId());
c.setDeviceName(rel.getName());
c.setCapacityValue(dailyCapacity);
c.setNextAvailableTime(nextAvailable);
candidates.add(c); }
if (CollUtil.isEmpty(candidates)) {
throw exception(SCHEDULE_PRODUCT_DEVICE_UNAVAILABLE, item.getProductId());
}
// 5) 选设备:优先空闲/最早可开工nextAvailable最小并列按deviceId
// 5) 选设备:优先空闲/最早可开工nextAvailable最小并列按deviceId
DeviceCandidate chosen = candidates.stream()
.sorted(Comparator
.comparing(DeviceCandidate::getNextAvailableTime)
@ -519,39 +550,23 @@ public class TaskServiceImpl implements TaskService {
.findFirst()
.orElseThrow(() -> exception(SCHEDULE_PRODUCT_DEVICE_UNAVAILABLE, item.getProductId()));
BigDecimal planNumber = BigDecimal.valueOf(item.getPlanNumber());
BigDecimal dailyCapacity = BigDecimal.valueOf(chosen.getCapacityValue()); // 每日产能
// needHours = planNumber * dailyWorkHours / dailyCapacity
BigDecimal needHours = planNumber.multiply(dailyWorkHours)
.divide(dailyCapacity, 6, RoundingMode.HALF_UP);
// 6) 按 数量/额定产能 计算天数
int days = (int) Math.ceil((double) item.getPlanNumber() / chosen.getCapacityValue());
if (days <= 0) {
days = 1;
}
LocalDateTime planStartTime = normalizeToWorkTime(chosen.getNextAvailableTime(), workStart, workEnd, skipHoliday);
LocalDateTime planEndTime = addWorkingHours(planStartTime, needHours, workStart, workEnd, skipHoliday);
LocalDate dueDate = LocalDate.from(item.getOrderDetailDeliveryDate() != null
? item.getOrderDetailDeliveryDate()
: item.getDeliveryDate());
LocalDateTime dueEnd = LocalDateTime.of(dueDate, workEnd);
LocalDateTime latestStartTime = subtractWorkingHours(dueEnd, needHours, workStart, workEnd, skipHoliday);
// 7) 计算开始/结束时间(算法生成)
LocalDate startDate = chosen.getNextAvailableTime().toLocalDate();
LocalDateTime planStartTime = startDate.atStartOfDay();
LocalDateTime planEndTime = startDate.plusDays(days - 1).atTime(23, 59, 59);
// 7.1) 计算最晚开工时间(优先 orderDetailDeliveryDate缺失则降级 deliveryDate
LocalDate dueDate;
if (item.getOrderDetailDeliveryDate() != null) {
dueDate = item.getOrderDetailDeliveryDate().toLocalDate();
} else if (item.getDeliveryDate() != null) {
dueDate = item.getDeliveryDate().toLocalDate();; // 如果你的 deliveryDate 是 LocalDateTime这里改成 toLocalDate()
} else {
throw exception(SCHEDULE_DELIVERY_DATE_EMPTY, item.getTaskDetailId());
}
// 天数A = (截止日期 - 计划开始日期) - scheduleDays
long availableDays = ChronoUnit.DAYS.between(startDate, dueDate) + 1;
// 最晚可向后平移天数
long dayA = availableDays - days;
LocalDateTime latestStartTime = dayA <= 0
? planStartTime
: planStartTime.plusDays(dayA);
// 8) 更新该设备下次可开工时间下一天00:00:00
LocalDateTime nextAvailable = planEndTime.plusSeconds(1);
deviceNextAvailableTime.put(chosen.getDeviceId(), nextAvailable);
// 更新设备下次可开工
LocalDateTime newNextAvailable = normalizeToWorkTime(planEndTime, workStart, workEnd, skipHoliday);
deviceNextAvailableTime.put(chosen.getDeviceId(), newNextAvailable);
// 9) 组装返回(按设备分组)
TaskOneClickScheduleRespVO deviceResp = deviceResultMap.computeIfAbsent(chosen.getDeviceId(), k -> {
@ -571,10 +586,6 @@ public class TaskServiceImpl implements TaskService {
p.setTaskId(item.getTaskId());
p.setTaskDetailId(item.getTaskDetailId());
p.setPlanNumber(item.getPlanNumber());
p.setScheduleDays(days);
// p.setPlanStartTime(planStartTime);
// p.setPlanEndTime(planEndTime);
// p.setLatestStartTime(latestStartTime);
p.setSourceType("CURRENT");
p.setTaskCode(taskDO == null ? null : taskDO.getCode());
p.setProductCode(productDO == null ? null : productDO.getBarCode());
@ -663,24 +674,166 @@ public class TaskServiceImpl implements TaskService {
* - +1
* -
*/
private LocalDateTime queryDeviceNextAvailableTime(Long deviceId) {
// ===================== 6) 替换 queryDeviceNextAvailableTime =====================
// 文件TaskServiceImpl.java
private LocalDateTime queryDeviceNextAvailableTime(Long deviceId, LocalTime workStart, LocalTime workEnd, boolean skipHoliday) {
PlanDO lastPlan = planMapper.selectOne(new LambdaQueryWrapper<PlanDO>()
.eq(PlanDO::getDeviceId, deviceId)
.eq(PlanDO::getIsEnable, true)
.orderByDesc(PlanDO::getPlanEndTime)
.last("limit 1"));
LocalDate today = LocalDate.now();
LocalDate historyNextDate;
if (lastPlan == null || lastPlan.getPlanEndTime() == null) {
historyNextDate = today;
} else {
historyNextDate = lastPlan.getPlanEndTime().toLocalDate().plusDays(1);
LocalDateTime base = (lastPlan == null || lastPlan.getPlanEndTime() == null)
? LocalDateTime.now()
: lastPlan.getPlanEndTime();
return normalizeToWorkTime(base, workStart, workEnd, skipHoliday);
}
private LocalDateTime normalizeToWorkTime(LocalDateTime time, LocalTime workStart, LocalTime workEnd, boolean skipHoliday) {
LocalDate date = time.toLocalDate();
LocalTime t = time.toLocalTime();
if (skipHoliday && !isWorkingDay(date)) {
date = nextWorkingDate(date.plusDays(1));
return LocalDateTime.of(date, workStart);
}
if (t.isBefore(workStart)) {
return LocalDateTime.of(date, workStart);
}
if (!t.isBefore(workEnd)) {
LocalDate next = date.plusDays(1);
if (skipHoliday) {
next = nextWorkingDate(next);
}
return LocalDateTime.of(next, workStart);
}
return time;
}
private LocalDateTime addWorkingHours(LocalDateTime start, BigDecimal hours,
LocalTime workStart, LocalTime workEnd,
boolean skipHoliday) {
LocalDateTime cursor = normalizeToWorkTime(start, workStart, workEnd, skipHoliday);
long remainMinutes = hours.multiply(BigDecimal.valueOf(60))
.setScale(0, RoundingMode.HALF_UP)
.longValue();
while (remainMinutes > 0) {
if (skipHoliday && !isWorkingDay(cursor.toLocalDate())) {
cursor = LocalDateTime.of(nextWorkingDate(cursor.toLocalDate().plusDays(1)), workStart);
continue;
}
LocalDateTime dayEnd = LocalDateTime.of(cursor.toLocalDate(), workEnd);
long available = ChronoUnit.MINUTES.between(cursor, dayEnd);
if (available <= 0) {
LocalDate next = cursor.toLocalDate().plusDays(1);
if (skipHoliday) {
next = nextWorkingDate(next);
}
cursor = LocalDateTime.of(next, workStart);
continue;
}
if (remainMinutes <= available) {
return cursor.plusMinutes(remainMinutes);
}
remainMinutes -= available;
LocalDate next = cursor.toLocalDate().plusDays(1);
if (skipHoliday) {
next = nextWorkingDate(next);
}
cursor = LocalDateTime.of(next, workStart);
}
// 取 max(今天, 历史下一天)
LocalDate nextDate = historyNextDate.isBefore(today) ? today : historyNextDate;
return nextDate.atStartOfDay();
return cursor;
}
private LocalDateTime subtractWorkingHours(LocalDateTime end, BigDecimal hours,
LocalTime workStart, LocalTime workEnd,
boolean skipHoliday) {
LocalDateTime cursor = end;
LocalTime t = cursor.toLocalTime();
if (t.isAfter(workEnd)) {
cursor = LocalDateTime.of(cursor.toLocalDate(), workEnd);
} else if (!t.isAfter(workStart)) {
LocalDate prev = cursor.toLocalDate().minusDays(1);
if (skipHoliday) {
prev = prevWorkingDate(prev);
}
cursor = LocalDateTime.of(prev, workEnd);
}
if (skipHoliday && !isWorkingDay(cursor.toLocalDate())) {
LocalDate prev = prevWorkingDate(cursor.toLocalDate().minusDays(1));
cursor = LocalDateTime.of(prev, workEnd);
}
long remainMinutes = hours.multiply(BigDecimal.valueOf(60))
.setScale(0, RoundingMode.HALF_UP)
.longValue();
while (remainMinutes > 0) {
if (skipHoliday && !isWorkingDay(cursor.toLocalDate())) {
LocalDate prev = prevWorkingDate(cursor.toLocalDate().minusDays(1));
cursor = LocalDateTime.of(prev, workEnd);
continue;
}
LocalDateTime dayStart = LocalDateTime.of(cursor.toLocalDate(), workStart);
long available = ChronoUnit.MINUTES.between(dayStart, cursor);
if (available <= 0) {
LocalDate prev = cursor.toLocalDate().minusDays(1);
if (skipHoliday) {
prev = prevWorkingDate(prev);
}
cursor = LocalDateTime.of(prev, workEnd);
continue;
}
if (remainMinutes <= available) {
return cursor.minusMinutes(remainMinutes);
}
remainMinutes -= available;
LocalDate prev = cursor.toLocalDate().minusDays(1);
if (skipHoliday) {
prev = prevWorkingDate(prev);
}
cursor = LocalDateTime.of(prev, workEnd);
}
return cursor;
}
// 规则:周末默认上班;仅 HOLIDAY 休息
private boolean isWorkingDay(LocalDate date) {
return !isHolidayByDate(date);
}
private LocalDate nextWorkingDate(LocalDate date) {
LocalDate d = date;
while (!isWorkingDay(d)) {
d = d.plusDays(1);
}
return d;
}
private LocalDate prevWorkingDate(LocalDate date) {
LocalDate d = date;
while (!isWorkingDay(d)) {
d = d.minusDays(1);
}
return d;
}
private Set<Long> queryDeviceIdsFromCurrentMonth() {
@ -725,4 +878,10 @@ public class TaskServiceImpl implements TaskService {
}
private boolean isHolidayByDate(LocalDate day) {
LocalDateTime start = day.atStartOfDay();
LocalDateTime end = day.plusDays(1).atStartOfDay();
Integer cnt = calHolidayMapper.countHolidayInRange(start, end);
return cnt != null && cnt > 0;
}
}

@ -9,4 +9,12 @@
文档可见https://www.iocoder.cn/MyBatis/x-plugins/
-->
<select id="countHolidayInRange" resultType="java.lang.Integer">
SELECT COUNT(1)
FROM mes_cal_holiday
WHERE deleted = 0
AND holiday_type = 'HOLIDAY'
AND the_day &gt;= #{start}
AND the_day &lt; #{end}
</select>
</mapper>
Loading…
Cancel
Save