From 5804c4613ad42936ce95f39ff91e6c06696bdb4d Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Fri, 14 Aug 2026 14:42:07 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E6=97=A5=E8=AE=A1?= =?UTF-8?q?=E5=88=92=E8=B0=83=E5=BA=A6=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../module/lms/enums/ErrorCodeConstants.java | 2 + .../dailyplan/DailyPlanScheduleCandidate.java | 27 +++ .../dailyplan/DailyPlanScheduleService.java | 17 ++ .../DailyPlanScheduleServiceImpl.java | 182 ++++++++++++++++++ 4 files changed, 228 insertions(+) create mode 100644 nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanScheduleCandidate.java create mode 100644 nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanScheduleService.java create mode 100644 nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanScheduleServiceImpl.java diff --git a/nl-module-lms/nl-module-lms-api/src/main/java/cn/code/nl/module/lms/enums/ErrorCodeConstants.java b/nl-module-lms/nl-module-lms-api/src/main/java/cn/code/nl/module/lms/enums/ErrorCodeConstants.java index 63297b73..78bfaaa0 100644 --- a/nl-module-lms/nl-module-lms-api/src/main/java/cn/code/nl/module/lms/enums/ErrorCodeConstants.java +++ b/nl-module-lms/nl-module-lms-api/src/main/java/cn/code/nl/module/lms/enums/ErrorCodeConstants.java @@ -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, "日计划作业进度数量必须为正整数"); } diff --git a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanScheduleCandidate.java b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanScheduleCandidate.java new file mode 100644 index 00000000..bcfe72d2 --- /dev/null +++ b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanScheduleCandidate.java @@ -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; +} diff --git a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanScheduleService.java b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanScheduleService.java new file mode 100644 index 00000000..61184869 --- /dev/null +++ b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanScheduleService.java @@ -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); +} diff --git a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanScheduleServiceImpl.java b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanScheduleServiceImpl.java new file mode 100644 index 00000000..1ba30512 --- /dev/null +++ b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanScheduleServiceImpl.java @@ -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 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 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"); + } +}