feat: 增加日计划调度能力

This commit is contained in:
zhouz
2026-08-14 14:42:07 +08:00
parent 5048c0fcc9
commit 5804c4613a
4 changed files with 228 additions and 0 deletions

View File

@@ -60,4 +60,6 @@ public interface ErrorCodeConstants {
ErrorCode DAILY_PLAN_ERP_TOKEN_ERROR = new ErrorCode(35, "ERP日计划接口令牌错误");
ErrorCode DAILY_PLAN_QTY_EXCEEDS_LIMIT = new ErrorCode(36, "日计划数量超过系统支持范围");
ErrorCode DAILY_PLAN_CODE_GENERATE_FAILED = new ErrorCode(37, "日计划编码生成失败");
ErrorCode DAILY_PLAN_STRATEGY_VERSION_CONFLICT = new ErrorCode(38, "日计划策略已变化,请重新获取调度候选");
ErrorCode DAILY_PLAN_PROGRESS_QTY_NOT_POSITIVE = new ErrorCode(39, "日计划作业进度数量必须为正整数");
}

View File

@@ -0,0 +1,27 @@
package cn.code.nl.module.lms.service.dailyplan;
import lombok.Data;
/** 供日计划定时调度器使用的候选任务。 */
@Data
public class DailyPlanScheduleCandidate {
/** 日计划 ID。 */
private Long dailyPlanId;
/** 日计划编号。 */
private String planCode;
/** 物料 ID。 */
private Long materialId;
/** 物料编码。 */
private String materialCode;
/** 物料名称。 */
private String materialName;
/** 物料规格。 */
private String materialSpec;
/** 本轮建议处理数量。 */
private Integer suggestedQuantity;
/** 日计划乐观锁版本号。 */
private Integer version;
/** 本轮使用的策略模式。 */
private Integer strategyMode;
}

View File

@@ -0,0 +1,17 @@
package cn.code.nl.module.lms.service.dailyplan;
/** 供后续定时器调用的日计划调度服务。 */
public interface DailyPlanScheduleService {
/** 获取下一张待备货计划;无候选时返回 {@code null}。 */
DailyPlanScheduleCandidate getNextStockingCandidate(String operator);
/** 获取下一张待套管计划;无候选时返回 {@code null}。 */
DailyPlanScheduleCandidate getNextSleevingCandidate(String operator);
/** 原子增加已备货数量。 */
void increaseStockedQuantity(Long dailyPlanId, Integer quantity, Integer version, String operator);
/** 原子增加已套管数量。 */
void increaseSleevedQuantity(Long dailyPlanId, Integer quantity, Integer version, String operator);
}

View File

@@ -0,0 +1,182 @@
package cn.code.nl.module.lms.service.dailyplan;
import cn.code.nl.module.lms.dal.dataobject.dailyplan.DailyPlanDO;
import cn.code.nl.module.lms.dal.dataobject.dailyplan.DailyPlanOperationLogDO;
import cn.code.nl.module.lms.dal.dataobject.dailyplan.DailyPlanStrategyDO;
import cn.code.nl.module.lms.dal.mysql.dailyplan.DailyPlanMapper;
import cn.code.nl.module.lms.dal.mysql.dailyplan.DailyPlanOperationLogMapper;
import cn.code.nl.module.lms.dal.mysql.dailyplan.DailyPlanStrategyMapper;
import cn.code.nl.module.lms.enums.DailyPlanOrderStatusEnum;
import cn.code.nl.module.lms.enums.DailyPlanStrategyModeEnum;
import cn.code.nl.module.lms.enums.DailyPlanStrategyTypeEnum;
import cn.code.nl.module.lms.enums.DailyPlanWorkStatusEnum;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_COMPLETED_QTY_EXCEEDS_TOTAL;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_NOT_EXISTS;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_PROGRESS_QTY_NOT_POSITIVE;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_STATUS_OPERATION_NOT_ALLOWED;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_STRATEGY_NOT_EXISTS;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_STRATEGY_VERSION_CONFLICT;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_VERSION_CONFLICT;
/** 日计划调度服务实现。 */
@Service
public class DailyPlanScheduleServiceImpl implements DailyPlanScheduleService {
private static final String TARGET_STOCKING = "STOCKING";
private static final String TARGET_SLEEVING = "SLEEVING";
private static final String TYPE_PROGRESS = "PROGRESS";
@Resource
private DailyPlanMapper dailyPlanMapper;
@Resource
private DailyPlanStrategyMapper dailyPlanStrategyMapper;
@Resource
private DailyPlanOperationLogMapper dailyPlanOperationLogMapper;
@Override
@Transactional(rollbackFor = Exception.class)
public DailyPlanScheduleCandidate getNextStockingCandidate(String operator) {
return selectCandidate(DailyPlanStrategyTypeEnum.STOCKING, dailyPlanMapper.selectStockingCandidates(), operator);
}
@Override
@Transactional(rollbackFor = Exception.class)
public DailyPlanScheduleCandidate getNextSleevingCandidate(String operator) {
return selectCandidate(DailyPlanStrategyTypeEnum.SLEEVING, dailyPlanMapper.selectSleevingCandidates(), operator);
}
private DailyPlanScheduleCandidate selectCandidate(DailyPlanStrategyTypeEnum type,
List<DailyPlanDO> candidates, String operator) {
DailyPlanStrategyDO strategy = dailyPlanStrategyMapper.selectByStrategyType(type.getCode());
if (strategy == null) {
throw exception(DAILY_PLAN_STRATEGY_NOT_EXISTS);
}
if (candidates.isEmpty()) {
return null;
}
DailyPlanStrategyModeEnum mode = DailyPlanStrategyModeEnum.getByCode(strategy.getStrategyMode());
if (mode == null) {
throw exception(DAILY_PLAN_STRATEGY_NOT_EXISTS);
}
if (mode == DailyPlanStrategyModeEnum.SEQUENCE) {
DailyPlanDO selected = candidates.get(0);
return convertCandidate(selected, remaining(selected, type), mode.getCode());
}
int selectedIndex = findCursorIndex(candidates, strategy.getCursorDailyPlanId());
DailyPlanDO selected = candidates.get(selectedIndex);
DailyPlanDO next = candidates.get((selectedIndex + 1) % candidates.size());
int affected = dailyPlanStrategyMapper.updateCursorWithVersion(strategy.getStrategyId(),
next.getDailyPlanId(), strategy.getVersion(), normalizedOperator(operator));
if (affected != 1) {
// 本次候选不应在游标推进失败时被执行,调用方可重新获取最新候选。
throw exception(DAILY_PLAN_STRATEGY_VERSION_CONFLICT);
}
int quantity = Math.min(selected.getWeight(), remaining(selected, type));
return convertCandidate(selected, quantity, mode.getCode());
}
private int findCursorIndex(List<DailyPlanDO> candidates, Long cursorDailyPlanId) {
if (cursorDailyPlanId != null) {
for (int i = 0; i < candidates.size(); i++) {
if (cursorDailyPlanId.equals(candidates.get(i).getDailyPlanId())) {
return i;
}
}
}
return 0;
}
private int remaining(DailyPlanDO plan, DailyPlanStrategyTypeEnum type) {
int completed = type == DailyPlanStrategyTypeEnum.STOCKING ? plan.getStockedQty() : plan.getSleevedQty();
return plan.getPlanQty() + plan.getAppendQty() - completed;
}
private DailyPlanScheduleCandidate convertCandidate(DailyPlanDO plan, int quantity, Integer strategyMode) {
DailyPlanScheduleCandidate candidate = new DailyPlanScheduleCandidate();
candidate.setDailyPlanId(plan.getDailyPlanId());
candidate.setPlanCode(plan.getPlanCode());
candidate.setMaterialId(plan.getMaterialId());
candidate.setMaterialCode(plan.getMaterialCode());
candidate.setMaterialName(plan.getMaterialName());
candidate.setMaterialSpec(plan.getMaterialSpec());
candidate.setSuggestedQuantity(quantity);
candidate.setVersion(plan.getVersion());
candidate.setStrategyMode(strategyMode);
return candidate;
}
@Override
@Transactional(rollbackFor = Exception.class)
public void increaseStockedQuantity(Long dailyPlanId, Integer quantity, Integer version, String operator) {
increaseQuantity(dailyPlanId, quantity, version, operator, DailyPlanStrategyTypeEnum.STOCKING);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void increaseSleevedQuantity(Long dailyPlanId, Integer quantity, Integer version, String operator) {
increaseQuantity(dailyPlanId, quantity, version, operator, DailyPlanStrategyTypeEnum.SLEEVING);
}
private void increaseQuantity(Long dailyPlanId, Integer quantity, Integer version, String operator,
DailyPlanStrategyTypeEnum type) {
if (quantity == null || quantity <= 0) {
throw exception(DAILY_PLAN_PROGRESS_QTY_NOT_POSITIVE);
}
DailyPlanDO plan = dailyPlanMapper.selectDailyPlanById(dailyPlanId);
if (plan == null) {
throw exception(DAILY_PLAN_NOT_EXISTS);
}
if (!plan.getVersion().equals(version)) {
throw exception(DAILY_PLAN_VERSION_CONFLICT);
}
Integer workStatus = type == DailyPlanStrategyTypeEnum.STOCKING
? plan.getStockingStatus() : plan.getSleevingStatus();
if (!DailyPlanOrderStatusEnum.IN_PROGRESS.getCode().equals(plan.getOrderStatus())
|| !DailyPlanWorkStatusEnum.IN_PROGRESS.getCode().equals(workStatus)) {
throw exception(DAILY_PLAN_STATUS_OPERATION_NOT_ALLOWED,
plan.getOrderStatus() + "/" + workStatus, "更新作业进度");
}
int before = type == DailyPlanStrategyTypeEnum.STOCKING ? plan.getStockedQty() : plan.getSleevedQty();
long total = (long) plan.getPlanQty() + plan.getAppendQty();
if ((long) before + quantity > total) {
throw exception(DAILY_PLAN_COMPLETED_QTY_EXCEEDS_TOTAL);
}
int affected = type == DailyPlanStrategyTypeEnum.STOCKING
? dailyPlanMapper.increaseStockedQuantity(dailyPlanId, quantity, version, normalizedOperator(operator))
: dailyPlanMapper.increaseSleevedQuantity(dailyPlanId, quantity, version, normalizedOperator(operator));
if (affected != 1) {
throw exception(DAILY_PLAN_VERSION_CONFLICT);
}
insertProgressLog(dailyPlanId, type, before, before + quantity, quantity, operator);
}
private void insertProgressLog(Long dailyPlanId, DailyPlanStrategyTypeEnum type, int before, int after,
int quantity, String operator) {
DailyPlanOperationLogDO log = new DailyPlanOperationLogDO();
log.setOperationLogId(IdWorker.getId());
log.setDailyPlanId(dailyPlanId);
log.setOperationTarget(type == DailyPlanStrategyTypeEnum.STOCKING ? TARGET_STOCKING : TARGET_SLEEVING);
log.setOperationType(TYPE_PROGRESS);
log.setBeforeValue(String.valueOf(before));
log.setAfterValue(String.valueOf(after));
log.setChangeQty(quantity);
log.setOperator(normalizedOperator(operator));
if (dailyPlanOperationLogMapper.insertOperationLog(log) != 1) {
throw exception(DAILY_PLAN_VERSION_CONFLICT);
}
}
private String normalizedOperator(String operator) {
return StrUtil.blankToDefault(StrUtil.trim(operator), "SCHEDULER");
}
}