Compare commits

...

3 Commits

@ -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");

@ -201,11 +201,9 @@ public class PlanController {
return Collections.emptyList();
}
// 原有:投料管道名称
Map<Long, OrganizationDO> organizationMap = organizationService.getOrganizationVOMap(
convertSet(list, PlanRespVO::getFeedingPipeline));
// 新增:设备名称映射(按你项目里的设备 service 替换)
Map<Long, DeviceLedgerDO> deviceMap = deviceLedgerService.getDeviceMap(
convertSet(list, PlanRespVO::getDeviceId));
@ -213,19 +211,32 @@ public class PlanController {
MapUtils.findAndThen(organizationMap, item.getFeedingPipeline(),
organization -> item.setFeedingPipelineName(organization.getName()));
DeviceLedgerDO device = deviceMap.get(item.getDeviceId());
if (device != null) {
item.setDeviceName(device.getDeviceName());
} else {
String baseName = StringUtils.hasText(item.getDeviceName())
? item.getDeviceName()
: "设备[" + item.getDeviceId() + "]";
item.setDeviceName(baseName + "(已删除)");
if (item.getDeviceId() != null) {
DeviceLedgerDO device = deviceMap.get(item.getDeviceId());
if (device != null) {
if (Boolean.TRUE.equals(device.getDeleted())) {
// 已删除:优先原值,其次 code-设备名,最后兜底“设备(已删除)”
String code = StringUtils.hasText(device.getDeviceCode()) ? device.getDeviceCode() : "";
String name = StringUtils.hasText(device.getDeviceName()) ? device.getDeviceName() : "";
String codeName = StringUtils.hasText(code) && StringUtils.hasText(name)
? code + "-" + name
: code + name;
String baseName = StringUtils.hasText(item.getDeviceName())
? item.getDeviceName()
: (StringUtils.hasText(codeName) ? codeName : "设备");
item.setDeviceName(baseName + "(已删除)");
} else {
item.setDeviceName(device.getDeviceCode()+"-"+device.getDeviceName());
}
}
}
});
}
@GetMapping("/export-excel")
@Operation(summary = "导出生产计划 Excel")
@PreAuthorize("@ss.hasPermission('mes:plan:export')")
@ -297,6 +308,9 @@ public class PlanController {
planDO.setStatus(PlanStatusEnum..getValue());
planService.createPlanStockIn(statusUpdateVO, planDO);
}
else if ("commence".equals(statusUpdateVO.getCode())) {
planDO.setStatus(PlanStatusEnum..getValue());
}
planMapper.updateById(planDO);
return success(true);
}
@ -376,7 +390,14 @@ public class PlanController {
List<PlanDO> proList = planService.getPlanByStatus(statusList);
List<PlanRespVO> planRespVOList = planService.buildVOList(proList);
return success(planRespVOList);
} else {
}else if(status ==8){
List<Integer> statusList = new ArrayList<>();
statusList.add(PlanStatusEnum..getValue());
List<PlanDO> proList = planService.getPlanByStatus(statusList);
List<PlanRespVO> planRespVOList = planService.buildVOList(proList);
return success(planRespVOList);
}
else {
return success(Collections.emptyList());
}
}

@ -14,7 +14,8 @@ public enum PlanStatusEnum {
(4),
(5),
(6),
(7);
(7),
(8);
private final int value;

@ -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;
}

@ -108,4 +108,7 @@ public class TaskDetailRespVO {
@Schema(description = "任务单编码", example = "")
@ExcelProperty("任务单编码")
private String taskCode;
@Schema(description = "关联设备", example = "EQ-01-搅拌机,EQ-02-封口机")
private String deviceDisplayName;
}

@ -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;
}

@ -99,4 +99,9 @@ public class ZjTaskRespVO {
@ExcelProperty("取消原因")
private String cancelReason;
@Schema(description = "工单类型1-生产过程 2-产品入库")
private Integer ticketType;
}

@ -68,4 +68,7 @@ public class ZjTaskSaveReqVO {
@Schema(description = "取消原因")
private String cancelReason;
@Schema(description = "工单类型 1-生产过程 2-产品入库")
private Integer ticketType;
}

@ -96,5 +96,10 @@ public class ZjTaskDO extends BaseDO {
*
*/
private String cancelReason;
/**
*
*/
private Integer ticketType;
}

@ -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;
@ -9,24 +10,29 @@ import cn.iocoder.yudao.module.erp.controller.admin.autocode.util.AutoCodeUtil;
import cn.iocoder.yudao.module.erp.controller.admin.product.vo.product.ProductRelationRespVO;
import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductUnitDO;
import cn.iocoder.yudao.module.erp.dal.dataobject.productdevicerel.ProductDeviceRelDO;
import cn.iocoder.yudao.module.erp.dal.mysql.product.ErpProductMapper;
import cn.iocoder.yudao.module.erp.dal.mysql.productdevicerel.ProductDeviceRelMapper;
import cn.iocoder.yudao.module.erp.service.product.ErpProductService;
import cn.iocoder.yudao.module.erp.service.product.ErpProductUnitService;
import cn.iocoder.yudao.module.mes.controller.admin.deviceledger.enums.CapacityTypeEnum;
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;
import cn.iocoder.yudao.module.mes.dal.mysql.task.TaskMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.task.ViewTaskProductSummaryMapper;
import cn.iocoder.yudao.module.mes.dal.redis.no.MesNoRedisDAO;
import cn.iocoder.yudao.module.mes.service.deviceledger.DeviceLedgerService;
import cn.iocoder.yudao.module.mes.strategy.task.ScheduleSortStrategyFactory;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@ -38,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.*;
@ -66,12 +75,22 @@ public class TaskServiceImpl implements TaskService {
@Resource
private MesNoRedisDAO noRedisDAO;
@Resource
private ErpProductMapper erpProductMapper;
@Resource
@Lazy
private DeviceLedgerMapper deviceLedgerMapper;
@Resource
private ProductDeviceRelMapper productDeviceRelMapper;
@Resource
private DeviceLedgerService deviceLedgerService;
@Resource
private CalHolidayMapper calHolidayMapper;
@Resource
private AutoCodeUtil autoCodeUtil;
@Resource
@ -251,28 +270,77 @@ public class TaskServiceImpl implements TaskService {
if (CollUtil.isEmpty(list)) {
return Collections.emptyList();
}
Map<Long, ErpProductDO> map = productService.getProductMap(
convertSet(list, TaskDetailDO::getProductId));
Map<Long, ErpProductUnitDO> unitMap = productUnitService.getProductUnitMap(
convertSet(list, TaskDetailDO::getUnitId));
List<TaskDetailRespVO> resList = BeanUtils.toBean(list, TaskDetailRespVO.class, item -> {
Set<Long> productIds = convertSet(list, TaskDetailDO::getProductId);
Set<Long> unitIds = convertSet(list, TaskDetailDO::getUnitId);
Map<Long, ErpProductDO> map = productService.getProductMap(productIds);
Map<Long, ErpProductUnitDO> unitMap = productUnitService.getProductUnitMap(unitIds);
// 1) 产品-设备关系(按 sort、id 排序,保证拼接顺序稳定)
List<ProductDeviceRelDO> relList = productDeviceRelMapper.selectList(
Wrappers.<ProductDeviceRelDO>lambdaQuery()
.in(ProductDeviceRelDO::getProductId, productIds)
.orderByAsc(ProductDeviceRelDO::getSort, ProductDeviceRelDO::getId));
// productId -> deviceId 列表(去重且保持顺序)
Map<Long, LinkedHashSet<Long>> productDeviceIdsMap = new HashMap<>();
Set<Long> allDeviceIds = new HashSet<>();
for (ProductDeviceRelDO rel : relList) {
productDeviceIdsMap
.computeIfAbsent(rel.getProductId(), k -> new LinkedHashSet<>())
.add(rel.getDeviceId());
allDeviceIds.add(rel.getDeviceId());
}
// 2) 批量查设备
Map<Long, DeviceLedgerDO> deviceMap = CollUtil.isEmpty(allDeviceIds)
? Collections.emptyMap()
: deviceLedgerService.getDeviceMap(allDeviceIds);
// 3) 先构建 productId -> "code-name,code-name"
Map<Long, String> productDeviceDisplayMap = new HashMap<>();
for (Map.Entry<Long, LinkedHashSet<Long>> entry : productDeviceIdsMap.entrySet()) {
Long productId = entry.getKey();
LinkedHashSet<Long> deviceIds = entry.getValue();
String deviceDisplay = deviceIds.stream()
.map(deviceMap::get)
.filter(Objects::nonNull)
.map(device -> {
String code = StringUtils.defaultString(device.getDeviceCode());
String name = StringUtils.defaultString(device.getDeviceName());
if (StringUtils.isNotBlank(code) && StringUtils.isNotBlank(name)) {
return code + "-" + name;
}
return code + name;
})
.filter(StringUtils::isNotBlank)
.collect(Collectors.joining(","));
productDeviceDisplayMap.put(productId, deviceDisplay);
}
List<TaskDetailRespVO> resList = BeanUtils.toBean(list, TaskDetailRespVO.class, item -> {
MapUtils.findAndThen(map, item.getProductId(),
product -> item.setProductName(product.getName()).setBarCode(product.getBarCode()));
MapUtils.findAndThen(unitMap, item.getUnitId(),
unit -> item.setUnitName(unit.getName()));
item.setDeviceDisplayName(productDeviceDisplayMap.getOrDefault(item.getProductId(), ""));
});
for (TaskDetailRespVO respVO : resList) {
Long number = planMapper.selectSumTaskDetail(respVO.getId(),null);
Long number = planMapper.selectSumTaskDetail(respVO.getId(), null);
respVO.setPlanNumber(number);
TaskDO taskDO = taskMapper.selectOne(Wrappers.<TaskDO>lambdaQuery().eq(TaskDO::getId, respVO.getTaskId()));
respVO.setTaskCode(taskDO !=null && StringUtils.isNotBlank(taskDO.getCode()) ? taskDO.getCode() : "");
respVO.setTaskCode(taskDO != null && StringUtils.isNotBlank(taskDO.getCode()) ? taskDO.getCode() : "");
}
return resList;
}
@Override
public Long createTaskDetail(TaskDetailDO taskDetail) {
taskDetailMapper.insert(taskDetail);
@ -369,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());
// 排序前:若明细交期为空,则回填为订单交期
@ -425,34 +519,30 @@ public class TaskServiceImpl implements TaskService {
// 构建设备候选(含选中口径的产能 + nextAvailable
List<DeviceCandidate> candidates = new ArrayList<>();
for (ProductRelationRespVO rel : deviceRels) {
if (rel == null || rel.getId() == null) {
continue;
}
Long deviceId = rel.getId();
DeviceLedgerDO device = deviceLedgerMapper.selectById(deviceId);
if (device == null) {
DeviceLedgerDO device = deviceLedgerMapper.selectById(rel.getId());
Integer dailyCapacity = capacityType.getCapacity(device); // 每日产能
if (dailyCapacity == null || dailyCapacity <= 0) {
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)
@ -460,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;
}
// 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);
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);
// 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 -> {
@ -512,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());
@ -604,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);
}
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);
}
// 取 max(今天, 历史下一天)
LocalDate nextDate = historyNextDate.isBefore(today) ? today : historyNextDate;
return nextDate.atStartOfDay();
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() {
@ -666,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