fix:添加报工数、数据采集的产量排产的算法

main
HuangHuiKang 1 day ago
parent 999dd4f12e
commit 5ca9c9112a

@ -2599,6 +2599,47 @@ public class TDengineService {
}
}
@DS("tdengine")
public Map<Long, Map<LocalDate, Double>> queryDailyLatestCapacityValues(Collection<Long> deviceIds,
LocalDateTime beginTime,
LocalDateTime endTime) {
Map<Long, Map<LocalDate, Double>> result = new HashMap<>();
if (deviceIds == null || deviceIds.isEmpty() || beginTime == null || endTime == null || !endTime.isAfter(beginTime)) {
return result;
}
try {
StringBuilder sql = new StringBuilder();
sql.append("SELECT _wstart AS day_key, device_id, LAST(capacity_value) AS capacityvalue ")
.append("FROM besure_server.iot_device_capacity_record ")
.append("WHERE ts >= ? AND ts < ? AND device_id IN (")
.append(deviceIds.stream().map(id -> "?").collect(Collectors.joining(",")))
.append(") PARTITION BY device_id INTERVAL(1d)");
List<Object> params = new ArrayList<>();
params.add(Timestamp.valueOf(beginTime));
params.add(Timestamp.valueOf(endTime));
params.addAll(deviceIds);
jdbcTemplate.query(sql.toString(), params.toArray(), rs -> {
Long deviceId = rs.getLong("device_id");
Timestamp dayTs = rs.getTimestamp("day_key");
Double capacityValue = rs.getDouble("capacityvalue");
if (rs.wasNull() || dayTs == null) {
return;
}
LocalDate day = dayTs.toLocalDateTime().toLocalDate();
result.computeIfAbsent(deviceId, key -> new HashMap<>())
.put(day, capacityValue);
});
} catch (Exception e) {
log.error("TDengine查询每日最新容量值失败, deviceIds: {}, beginTime: {}, endTime: {}",
deviceIds, beginTime, endTime, e);
}
return result;
}
// ========================= app报表相关接口 ===================
@DS("tdengine")
@ -2704,4 +2745,4 @@ public class TDengineService {
}
}

@ -194,6 +194,8 @@ public interface ErrorCodeConstants {
ErrorCode SCHEDULE_WORK_HOURS_INVALID = new ErrorCode(100_301_0014, "排产工时非法start={}, end={}可用工时必须大于0");
ErrorCode WAREHOUSE_NOT_EXISTS= new ErrorCode(100_301_0014, "仓库Id不能为空");
ErrorCode PLAN_RECORD_NOT_EXISTS = new ErrorCode(100_301_0015, "生产计划操作记录不存在");
ErrorCode SCHEDULE_PRODUCT_DAILY_AVERAGE_ZERO = new ErrorCode(100_301_0016, "产品最近半年平均报工值为0productId={}productCode={}productName={}");
ErrorCode SCHEDULE_DEVICE_COLLECTION_CAPACITY_ZERO = new ErrorCode(100_301_0017, "设备最近半年数据采集产能为0deviceId={}deviceName={}");

@ -8,19 +8,19 @@ import lombok.Getter;
@AllArgsConstructor
public enum CapacityTypeEnum {
RATED(1, "额定产能") {
RATED(1, "计划产能") {
@Override
public Integer getCapacity(DeviceLedgerDO device) {
return device.getRatedCapacity();
}
},
DAILY_AVERAGE(2, "每日报工平均值") {
DAILY_AVERAGE(2, "报工产能") {
@Override
public Integer getCapacity(DeviceLedgerDO device) {
return device.getDailyAverageValue();
}
},
DATA_COLLECTION(3, "数据采集产能") {
DATA_COLLECTION(3, "实际产能") {
@Override
public Integer getCapacity(DeviceLedgerDO device) {
return device.getDataCollectionCapacity();

@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.mes.controller.admin.task.vo;
import cn.iocoder.yudao.module.erp.dal.dataobject.product.ErpProductDO;
import cn.iocoder.yudao.module.iot.dal.dataobject.device.DeviceDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.deviceledger.DeviceLedgerDO;
import lombok.Data;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Data
public class CapacityContext {
private List<LocalDate> statDays = new ArrayList<>();
private Integer statDayCount = 0;
private Map<Long, Integer> productDailyAverageMap = new HashMap<>();
private Map<Long, Integer> deviceCollectionAverageMap = new HashMap<>();
private Map<Long, ErpProductDO> productCache = new HashMap<>();
private Map<Long, DeviceDO> deviceCache = new HashMap<>();
public Integer getProductDailyAverageCapacity(Long productId) {
return productDailyAverageMap.get(productId);
}
public Integer getDeviceCollectionAverageCapacity(Long deviceId) {
return deviceCollectionAverageMap.get(deviceId);
}
}

@ -29,7 +29,7 @@ public class TaskOneClickScheduleReqVO {
@NotNull(message = "排序规则不能为空")
private Integer sortRule;
@Schema(description = "产能来源1-额定产能 2-每日报工平均值 3-数据采集产能", requiredMode = Schema.RequiredMode.REQUIRED)
@Schema(description = "产能来源1-计划产能 2-报工产能 3-实际产能", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "产能来源不能为空")
private Integer capacityType;

@ -3,6 +3,7 @@ package cn.iocoder.yudao.module.mes.controller.admin.task.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import javax.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.List;
@ -16,9 +17,12 @@ public class TaskOneClickScheduleRespVO {
@Schema(description = "设备名称")
private String deviceName;
@Schema(description = "设备额定产能")
@Schema(description = "产能")
private Integer ratedCapacity;
@Schema(description = "产能来源1-计划产能 2-报工产能 3-实际产能")
private Integer capacityType;
@Schema(description = "该设备排产计划")
private List<PlanRespItem> plans;

@ -22,11 +22,11 @@ public class TaskPageReqVO extends PageParam {
private String code;
@Schema(description = "下达日期")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] orderDate;
@Schema(description = "交货日期")
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
@DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY_HOUR_MINUTE_SECOND)
private LocalDateTime[] deliveryDate;
@Schema(description = "状态", example = "2")

@ -11,6 +11,7 @@ import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.apache.ibatis.annotations.Mapper;
import cn.iocoder.yudao.module.mes.controller.admin.baogongrecord.vo.*;
import org.apache.ibatis.annotations.Param;
import java.time.LocalDateTime;
/**
* Mapper
@ -43,4 +44,8 @@ public interface BaogongRecordMapper extends BaseMapperX<BaogongRecordDO> {
List<BaogongRecordTrendDayRespVO> selectTrendByHour(@Param("reqVO") BaogongRecordTrendReqVO reqVO);
List<Map<String, Object>> selectProductDailyReportTotals(@Param("productIds") Collection<Long> productIds,
@Param("beginTime") LocalDateTime beginTime,
@Param("endTime") LocalDateTime endTime);
}

@ -1,7 +1,6 @@
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;
@ -15,16 +14,18 @@ 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.iot.dal.dataobject.device.DeviceDO;
import cn.iocoder.yudao.module.iot.dal.mysql.device.DeviceMapper;
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.baogongrecord.BaogongRecordMapper;
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;
@ -33,6 +34,7 @@ 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.enums.BaogongTrendTypeEnum;
import cn.iocoder.yudao.module.iot.service.device.TDengineService;
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;
@ -84,6 +86,10 @@ public class TaskServiceImpl implements TaskService {
@Resource
private DeviceLedgerMapper deviceLedgerMapper;
@Resource
@Lazy
private DeviceMapper deviceMapper;
@Resource
private ProductDeviceRelMapper productDeviceRelMapper;
@ -93,6 +99,13 @@ public class TaskServiceImpl implements TaskService {
@Resource
private CalHolidayMapper calHolidayMapper;
@Resource
private BaogongRecordMapper baogongRecordMapper;
@Resource
private TDengineService tdengineService;
@Resource
private AutoCodeUtil autoCodeUtil;
@Resource
@ -450,7 +463,6 @@ public class TaskServiceImpl implements TaskService {
}
@Override
@Transactional(rollbackFor = Exception.class)
public List<TaskOneClickScheduleRespVO> oneClickSchedule(TaskOneClickScheduleReqVO reqVO) {
if (reqVO == null || CollUtil.isEmpty(reqVO.getCreateReqVO())) {
return Collections.emptyList();
@ -515,6 +527,8 @@ public class TaskServiceImpl implements TaskService {
throw exception(UNSUPPORTED_CAPACITY_TYPE, reqVO.getCapacityType());
}
CapacityContext capacityContext = buildCapacityContext(sortedPlans, skipHoliday, capacityType);
DateTimeFormatter DATETIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
@ -538,7 +552,7 @@ public class TaskServiceImpl implements TaskService {
List<DeviceCandidate> candidates = new ArrayList<>();
for (ProductRelationRespVO rel : deviceRels) {
DeviceLedgerDO device = deviceLedgerMapper.selectById(rel.getId());
Integer dailyCapacity = capacityType.getCapacity(device); // 每日产能
Integer dailyCapacity = resolveCapacityValue(capacityType, device, item.getProductId(), rel.getId(), capacityContext);
if (dailyCapacity == null || dailyCapacity <= 0) {
continue;
}
@ -554,7 +568,8 @@ public class TaskServiceImpl implements TaskService {
c.setDeviceName(rel.getName());
c.setCapacityValue(dailyCapacity);
c.setNextAvailableTime(nextAvailable);
candidates.add(c); }
candidates.add(c);
}
if (CollUtil.isEmpty(candidates)) {
throw exception(SCHEDULE_PRODUCT_DEVICE_UNAVAILABLE, item.getProductId());
@ -592,6 +607,7 @@ public class TaskServiceImpl implements TaskService {
vo.setDeviceId(chosen.getDeviceId());
vo.setDeviceName(chosen.getDeviceName());
vo.setRatedCapacity(chosen.getCapacityValue());
vo.setCapacityType(reqVO.getCapacityType());
vo.setPlans(new ArrayList<>());
return vo;
});
@ -633,7 +649,8 @@ public class TaskServiceImpl implements TaskService {
TaskOneClickScheduleRespVO vo = new TaskOneClickScheduleRespVO();
vo.setDeviceId(deviceId);
vo.setDeviceName(device == null ? null : device.getDeviceName());
vo.setRatedCapacity(capacityType.getCapacity(device)); // device为空时注意判空
// vo.setRatedCapacity(resolveCapacityValue(capacityType, device, null, deviceId, capacityContext));
// vo.setCapacityType(reqVO.getCapacityType());
vo.setPlans(new ArrayList<>());
return vo;
});
@ -854,6 +871,157 @@ public class TaskServiceImpl implements TaskService {
return d;
}
private Integer resolveCapacityValue(CapacityTypeEnum capacityType, DeviceLedgerDO device,
Long productId, Long deviceId, CapacityContext capacityContext) {
if (capacityType == CapacityTypeEnum.RATED) {
return device == null ? null : capacityType.getCapacity(device);
}
if (capacityContext == null) {
return null;
}
if (capacityType == CapacityTypeEnum.DAILY_AVERAGE) {
if (productId == null) {
return null;
}
Integer value = capacityContext.getProductDailyAverageCapacity(productId);
if (value == null || value <= 0) {
ErpProductDO product = capacityContext.getProductCache().get(productId);
throw exception(SCHEDULE_PRODUCT_DAILY_AVERAGE_ZERO, productId,
product == null ? null : product.getBarCode(),
product == null ? null : product.getName());
}
return value;
}
if (capacityType == CapacityTypeEnum.DATA_COLLECTION) {
if (deviceId == null) {
return null;
}
Integer value = capacityContext.getDeviceCollectionAverageCapacity(deviceId);
if (value == null || value <= 0) {
DeviceDO deviceInfo = capacityContext.getDeviceCache().get(deviceId);
throw exception(SCHEDULE_DEVICE_COLLECTION_CAPACITY_ZERO, deviceId,
deviceInfo == null ? null : deviceInfo.getDeviceName());
}
return value;
}
return device == null ? null : capacityType.getCapacity(device);
}
private CapacityContext buildCapacityContext(List<PlanSaveReqVO> sortedPlans, boolean skipHoliday,
CapacityTypeEnum capacityType) {
CapacityContext context = new CapacityContext();
if (CollUtil.isEmpty(sortedPlans)) {
return context;
}
LocalDate endDate = LocalDate.now();
LocalDate startDate = endDate.minusMonths(6).plusDays(1);
List<LocalDate> statDays = listStatDays(startDate, endDate, skipHoliday);
context.setStatDays(statDays);
context.setStatDayCount(statDays.size());
if (CollUtil.isEmpty(statDays)) {
return context;
}
LocalDateTime beginTime = statDays.get(0).atStartOfDay();
LocalDateTime endTime = statDays.get(statDays.size() - 1).plusDays(1).atStartOfDay();
if (capacityType == CapacityTypeEnum.DAILY_AVERAGE) {
Set<Long> productIds = sortedPlans.stream().map(PlanSaveReqVO::getProductId)
.filter(Objects::nonNull).collect(Collectors.toSet());
if (CollUtil.isNotEmpty(productIds)) {
context.setProductCache(productIds.stream().collect(Collectors.toMap(
id -> id,
erpProductMapper::selectById,
(a, b) -> a
)));
}
context.setProductDailyAverageMap(queryProductDailyAverageMap(productIds, statDays, beginTime, endTime));
} else if (capacityType == CapacityTypeEnum.DATA_COLLECTION) {
Set<Long> deviceIds = deviceMapper.selectList(new LambdaQueryWrapper<DeviceDO>()
.select(DeviceDO::getId))
.stream()
.map(DeviceDO::getId)
.filter(Objects::nonNull)
.collect(Collectors.toSet());
if (CollUtil.isNotEmpty(deviceIds)) {
context.setDeviceCache(deviceMapper.selectBatchIds(deviceIds).stream()
.filter(Objects::nonNull)
.collect(Collectors.toMap(
DeviceDO::getId,
device -> device,
(a, b) -> a
)));
}
context.setDeviceCollectionAverageMap(queryDeviceCollectionAverageMap(deviceIds, statDays, beginTime, endTime));
}
return context;
}
private Map<Long, Integer> queryProductDailyAverageMap(Set<Long> productIds, List<LocalDate> statDays,
LocalDateTime beginTime, LocalDateTime endTime) {
Map<Long, Integer> result = new HashMap<>();
if (CollUtil.isEmpty(productIds) || CollUtil.isEmpty(statDays)) {
return result;
}
Set<LocalDate> validDays = new HashSet<>(statDays);
List<Map<String, Object>> rows = baogongRecordMapper.selectProductDailyReportTotals(productIds, beginTime, endTime);
Map<Long, BigDecimal> productTotalMap = new HashMap<>();
for (Map<String, Object> row : rows) {
Long productId = row.get("productId") == null ? null : Long.valueOf(String.valueOf(row.get("productId")));
LocalDate day = row.get("day") == null ? null : LocalDate.parse(String.valueOf(row.get("day")));
if (productId == null || day == null || !validDays.contains(day)) {
continue;
}
BigDecimal totalNum = new BigDecimal(String.valueOf(row.get("totalNum")));
productTotalMap.merge(productId, totalNum, BigDecimal::add);
}
BigDecimal divisor = BigDecimal.valueOf(statDays.size());
for (Long productId : productIds) {
BigDecimal total = productTotalMap.getOrDefault(productId, BigDecimal.ZERO);
result.put(productId, total.divide(divisor, 0, RoundingMode.HALF_UP).intValue());
}
return result;
}
private Map<Long, Integer> queryDeviceCollectionAverageMap(Set<Long> deviceIds, List<LocalDate> statDays,
LocalDateTime beginTime, LocalDateTime endTime) {
Map<Long, Integer> result = new HashMap<>();
if (CollUtil.isEmpty(deviceIds) || CollUtil.isEmpty(statDays)) {
return result;
}
Set<LocalDate> validDays = new HashSet<>(statDays);
Map<Long, Map<LocalDate, Double>> rawMap = tdengineService.queryDailyLatestCapacityValues(deviceIds, beginTime, endTime);
BigDecimal divisor = BigDecimal.valueOf(statDays.size());
for (Long deviceId : deviceIds) {
Map<LocalDate, Double> dayMap = rawMap.getOrDefault(deviceId, Collections.emptyMap());
BigDecimal total = BigDecimal.ZERO;
for (Map.Entry<LocalDate, Double> entry : dayMap.entrySet()) {
if (entry.getKey() == null || entry.getValue() == null || !validDays.contains(entry.getKey())) {
continue;
}
total = total.add(BigDecimal.valueOf(entry.getValue()));
}
result.put(deviceId, total.divide(divisor, 0, RoundingMode.HALF_UP).intValue());
}
return result;
}
private List<LocalDate> listStatDays(LocalDate startDate, LocalDate endDate, boolean skipHoliday) {
List<LocalDate> days = new ArrayList<>();
LocalDate cursor = startDate;
while (!cursor.isAfter(endDate)) {
if (!skipHoliday || isWorkingDay(cursor)) {
days.add(cursor);
}
cursor = cursor.plusDays(1);
}
return days;
}
private Set<Long> queryDeviceIdsFromCurrentMonth() {
LocalDateTime monthStart = LocalDate.now().withDayOfMonth(1).atStartOfDay();
List<PlanDO> plans = planMapper.selectList(new LambdaQueryWrapper<PlanDO>()

@ -195,4 +195,27 @@
ORDER BY h.day ASC
</select>
<select id="selectProductDailyReportTotals" resultType="java.util.HashMap">
SELECT
p.product_id AS productId,
DATE_FORMAT(r.baogong_time, '%Y-%m-%d') AS day,
IFNULL(SUM(IFNULL(r.num, 0) + IFNULL(r.no_pass_num, 0)), 0) AS totalNum
FROM mes_baogong_record r
INNER JOIN mes_plan p ON p.id = r.plan_id AND p.deleted = b'0'
WHERE r.deleted = b'0'
<if test="productIds != null and productIds.size() > 0">
AND p.product_id IN
<foreach collection="productIds" item="productId" open="(" separator="," close=")">
#{productId}
</foreach>
</if>
<if test="beginTime != null">
AND r.baogong_time &gt;= #{beginTime}
</if>
<if test="endTime != null">
AND r.baogong_time &lt; #{endTime}
</if>
GROUP BY p.product_id, DATE_FORMAT(r.baogong_time, '%Y-%m-%d')
</select>
</mapper>

Loading…
Cancel
Save