feat: 支持日计划逐根权重调度(任务4)

This commit is contained in:
zhouz
2026-08-17 10:38:46 +08:00
parent b55f8dfe13
commit 1bb22e9930
5 changed files with 156 additions and 31 deletions

View File

@@ -21,6 +21,8 @@ public class DailyPlanStrategyDO extends TenantBaseDO {
private Integer strategyMode;
/** 当前游标日计划 ID */
private Long cursorDailyPlanId;
/** 当前权重轮转已执行数量 */
private Integer cursorExecutedQty;
/** 乐观锁版本号 */
private Integer version;
}

View File

@@ -34,4 +34,13 @@ public interface DailyPlanStrategyMapper extends BaseMapperX<DailyPlanStrategyDO
@Param("cursorDailyPlanId") Long cursorDailyPlanId,
@Param("version") Integer version,
@Param("updater") String updater);
/**
* 乐观锁更新逐根权重轮转进度。
*/
int updateSingleProgressWithVersion(@Param("strategyId") Long strategyId,
@Param("cursorDailyPlanId") Long cursorDailyPlanId,
@Param("cursorExecutedQty") Integer cursorExecutedQty,
@Param("version") Integer version,
@Param("updater") String updater);
}

View File

@@ -1,5 +1,7 @@
package cn.code.nl.module.lms.service.dailyplan;
import java.util.List;
/** 供后续定时器调用的日计划调度服务。 */
public interface DailyPlanScheduleService {
@@ -9,6 +11,21 @@ public interface DailyPlanScheduleService {
/** 获取下一张待套管计划;无候选时返回 {@code null}。 */
DailyPlanScheduleCandidate getNextSleevingCandidate(String operator);
/** 预览本轮备货候选顺序,不领取计划。 */
List<DailyPlanScheduleCandidate> previewStockingCandidates();
/** 预览本轮套管候选顺序,不领取计划。 */
List<DailyPlanScheduleCandidate> previewSleevingCandidates();
/** 指定领取一张备货计划,本次固定一根。 */
DailyPlanScheduleCandidate claimStockingCandidate(Long dailyPlanId, String operator);
/** 指定领取一张套管计划,本次固定一根。 */
DailyPlanScheduleCandidate claimSleevingCandidate(Long dailyPlanId, String operator);
/** 确认单根现场任务已经创建,成功后才推进权重轮转。 */
void confirmTaskCreated(String claimToken, String operator);
/** 原子增加已备货数量。 */
void increaseStockedQuantity(String claimToken, Long dailyPlanId, Integer quantity, String operator);

View File

@@ -20,6 +20,8 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception;
@@ -62,45 +64,76 @@ public class DailyPlanScheduleServiceImpl implements DailyPlanScheduleService {
@Transactional(rollbackFor = Exception.class)
public DailyPlanScheduleCandidate getNextStockingCandidate(String operator) {
expireClaims(operator);
return selectCandidate(DailyPlanStrategyTypeEnum.STOCKING, dailyPlanMapper.selectStockingCandidates(), operator);
List<DailyPlanScheduleCandidate> candidates = previewStockingCandidates();
return candidates.isEmpty() ? null : claimStockingCandidate(candidates.get(0).getDailyPlanId(), operator);
}
@Override
@Transactional(rollbackFor = Exception.class)
public DailyPlanScheduleCandidate getNextSleevingCandidate(String operator) {
expireClaims(operator);
return selectCandidate(DailyPlanStrategyTypeEnum.SLEEVING, dailyPlanMapper.selectSleevingCandidates(), operator);
List<DailyPlanScheduleCandidate> candidates = previewSleevingCandidates();
return candidates.isEmpty() ? null : claimSleevingCandidate(candidates.get(0).getDailyPlanId(), operator);
}
private DailyPlanScheduleCandidate selectCandidate(DailyPlanStrategyTypeEnum type,
List<DailyPlanDO> candidates, String operator) {
/** 预览本轮备货候选顺序。 */
@Override
public List<DailyPlanScheduleCandidate> previewStockingCandidates() {
return previewCandidates(DailyPlanStrategyTypeEnum.STOCKING, dailyPlanMapper.selectStockingCandidates());
}
/** 预览本轮套管候选顺序。 */
@Override
public List<DailyPlanScheduleCandidate> previewSleevingCandidates() {
return previewCandidates(DailyPlanStrategyTypeEnum.SLEEVING, dailyPlanMapper.selectSleevingCandidates());
}
/** 按当前策略排列候选,但不产生领取。 */
private List<DailyPlanScheduleCandidate> previewCandidates(DailyPlanStrategyTypeEnum type,
List<DailyPlanDO> candidates) {
DailyPlanStrategyDO strategy = strategyInitializer.getOrCreate(type);
if (candidates.isEmpty()) {
return null;
return Collections.emptyList();
}
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);
DailyPlanDO locked = lockSchedulablePlan(selected.getDailyPlanId(), type);
return claimCandidate(locked, remaining(locked, type), mode.getCode(), type, operator);
List<DailyPlanDO> ordered = new ArrayList<>(candidates.size());
int start = mode == DailyPlanStrategyModeEnum.WEIGHT_ROUND_ROBIN
? findCursorIndex(candidates, strategy.getCursorDailyPlanId()) : 0;
for (int index = 0; index < candidates.size(); index++) {
ordered.add(candidates.get((start + index) % candidates.size()));
}
List<DailyPlanScheduleCandidate> result = new ArrayList<>(ordered.size());
for (DailyPlanDO plan : ordered) {
result.add(convertCandidate(plan, mode.getCode()));
}
return result;
}
int selectedIndex = findCursorIndex(candidates, strategy.getCursorDailyPlanId());
DailyPlanDO selected = candidates.get(selectedIndex);
DailyPlanDO next = candidates.get((selectedIndex + 1) % candidates.size());
DailyPlanDO locked = lockSchedulablePlan(selected.getDailyPlanId(), type);
int quantity = Math.min(locked.getWeight(), remaining(locked, type));
DailyPlanScheduleCandidate candidate = claimCandidate(locked, quantity, mode.getCode(), type, operator);
int affected = dailyPlanStrategyMapper.updateCursorWithVersion(strategy.getStrategyId(), next.getDailyPlanId(),
strategy.getVersion(), normalizedOperator(operator));
if (affected != 1) {
// 本次候选不应在游标推进失败时被执行,调用方可重新获取最新候选。
throw exception(DAILY_PLAN_STRATEGY_VERSION_CONFLICT);
}
return candidate;
/** 指定领取一张备货计划。 */
@Override
@Transactional(rollbackFor = Exception.class)
public DailyPlanScheduleCandidate claimStockingCandidate(Long dailyPlanId, String operator) {
expireClaims(operator);
return claimSingleCandidate(dailyPlanId, DailyPlanStrategyTypeEnum.STOCKING, operator);
}
/** 指定领取一张套管计划。 */
@Override
@Transactional(rollbackFor = Exception.class)
public DailyPlanScheduleCandidate claimSleevingCandidate(Long dailyPlanId, String operator) {
expireClaims(operator);
return claimSingleCandidate(dailyPlanId, DailyPlanStrategyTypeEnum.SLEEVING, operator);
}
/** 锁定指定计划并领取一根任务额度。 */
private DailyPlanScheduleCandidate claimSingleCandidate(Long dailyPlanId, DailyPlanStrategyTypeEnum type,
String operator) {
DailyPlanStrategyDO strategy = strategyInitializer.getOrCreate(type);
DailyPlanDO locked = lockSchedulablePlan(dailyPlanId, type);
return claimCandidate(locked, 1, strategy.getStrategyMode(), type, operator);
}
private DailyPlanDO lockSchedulablePlan(Long dailyPlanId, DailyPlanStrategyTypeEnum type) {
@@ -122,11 +155,7 @@ public class DailyPlanScheduleServiceImpl implements DailyPlanScheduleService {
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 claimCandidate(DailyPlanDO plan, int quantity, Integer strategyMode,
DailyPlanStrategyTypeEnum type, String operator) {
LocalDateTime expireTime = nextClaimExpireTime();
@@ -147,6 +176,15 @@ public class DailyPlanScheduleServiceImpl implements DailyPlanScheduleService {
} catch (DuplicateKeyException ex) {
throw exception(DAILY_PLAN_CLAIM_CONFLICT);
}
DailyPlanScheduleCandidate candidate = convertCandidate(plan, strategyMode);
candidate.setSuggestedQuantity(quantity);
candidate.setClaimToken(claim.getClaimToken());
candidate.setClaimExpireTime(expireTime);
return candidate;
}
/** 将日计划转换为调度候选。 */
private DailyPlanScheduleCandidate convertCandidate(DailyPlanDO plan, Integer strategyMode) {
DailyPlanScheduleCandidate candidate = new DailyPlanScheduleCandidate();
candidate.setDailyPlanId(plan.getDailyPlanId());
candidate.setPlanCode(plan.getPlanCode());
@@ -154,14 +192,60 @@ public class DailyPlanScheduleServiceImpl implements DailyPlanScheduleService {
candidate.setMaterialCode(plan.getMaterialCode());
candidate.setMaterialName(plan.getMaterialName());
candidate.setMaterialSpec(plan.getMaterialSpec());
candidate.setSuggestedQuantity(quantity);
candidate.setSuggestedQuantity(1);
candidate.setVersion(plan.getVersion());
candidate.setStrategyMode(strategyMode);
candidate.setClaimToken(claim.getClaimToken());
candidate.setClaimExpireTime(expireTime);
return candidate;
}
/** 现场单根任务创建成功后推进权重轮转。 */
@Override
@Transactional(rollbackFor = Exception.class)
public void confirmTaskCreated(String claimToken, String operator) {
DailyPlanClaimDO claim = getClaimForUpdate(claimToken);
validateActiveClaim(claim);
DailyPlanStrategyTypeEnum type = DailyPlanStrategyTypeEnum.getByCode(claim.getStrategyType());
if (type == null) {
throw exception(DAILY_PLAN_STRATEGY_NOT_EXISTS);
}
DailyPlanStrategyDO strategy = strategyInitializer.getOrCreate(type);
if (!DailyPlanStrategyModeEnum.WEIGHT_ROUND_ROBIN.getCode().equals(strategy.getStrategyMode())) {
return;
}
if (strategy.getCursorDailyPlanId() != null
&& !strategy.getCursorDailyPlanId().equals(claim.getDailyPlanId())) {
return;
}
DailyPlanDO current = dailyPlanMapper.selectDailyPlanById(claim.getDailyPlanId());
if (current == null) {
throw exception(DAILY_PLAN_NOT_EXISTS);
}
int executedQty = strategy.getCursorExecutedQty() + 1;
Long nextDailyPlanId = current.getDailyPlanId();
if (executedQty >= current.getWeight()) {
executedQty = 0;
List<DailyPlanDO> candidates = type == DailyPlanStrategyTypeEnum.STOCKING
? dailyPlanMapper.selectStockingCandidates() : dailyPlanMapper.selectSleevingCandidates();
nextDailyPlanId = findNextDailyPlanId(current, candidates);
}
if (dailyPlanStrategyMapper.updateSingleProgressWithVersion(strategy.getStrategyId(), nextDailyPlanId,
executedQty, strategy.getVersion(), normalizedOperator(operator)) != 1) {
throw exception(DAILY_PLAN_STRATEGY_VERSION_CONFLICT);
}
}
/** 查找当前计划之后的下一张可执行计划。 */
private Long findNextDailyPlanId(DailyPlanDO current, List<DailyPlanDO> candidates) {
for (DailyPlanDO candidate : candidates) {
if (candidate.getSortSeq() > current.getSortSeq()
|| candidate.getSortSeq().equals(current.getSortSeq())
&& candidate.getDailyPlanId() > current.getDailyPlanId()) {
return candidate.getDailyPlanId();
}
}
return candidates.isEmpty() ? current.getDailyPlanId() : candidates.get(0).getDailyPlanId();
}
@Override
@Transactional(rollbackFor = Exception.class)
public void increaseStockedQuantity(String claimToken, Long dailyPlanId, Integer quantity, String operator) {

View File

@@ -3,7 +3,7 @@
<mapper namespace="cn.code.nl.module.lms.dal.mysql.dailyplan.DailyPlanStrategyMapper">
<sql id="strategyColumns">
strategy_id, strategy_type, strategy_mode, cursor_daily_plan_id, version,
strategy_id, strategy_type, strategy_mode, cursor_daily_plan_id, cursor_executed_qty, version,
creator, create_time, updater, update_time, deleted, tenant_id
</sql>
@@ -27,6 +27,7 @@
UPDATE lms_daily_plan_strategy
SET strategy_mode = #{strategyMode},
cursor_daily_plan_id = NULL,
cursor_executed_qty = 0,
updater = #{updater},
version = version + 1
WHERE strategy_id = #{strategyId}
@@ -37,6 +38,18 @@
<update id="updateCursorWithVersion">
UPDATE lms_daily_plan_strategy
SET cursor_daily_plan_id = #{cursorDailyPlanId},
cursor_executed_qty = 0,
updater = #{updater},
version = version + 1
WHERE strategy_id = #{strategyId}
AND version = #{version}
AND deleted = b'0'
</update>
<update id="updateSingleProgressWithVersion">
UPDATE lms_daily_plan_strategy
SET cursor_daily_plan_id = #{cursorDailyPlanId},
cursor_executed_qty = #{cursorExecutedQty},
updater = #{updater},
version = version + 1
WHERE strategy_id = #{strategyId}