fix: 使用领取令牌隔离日计划进度

This commit is contained in:
zhouz
2026-08-14 14:59:47 +08:00
parent 646bba8893
commit c57023facb
11 changed files with 129 additions and 80 deletions

View File

@@ -65,4 +65,5 @@ public interface ErrorCodeConstants {
ErrorCode DAILY_PLAN_CLAIM_CONFLICT = new ErrorCode(40, "日计划已被其他调度器领取,请重新获取候选");
ErrorCode DAILY_PLAN_CLAIM_INVALID = new ErrorCode(41, "日计划领取标识无效、已结束或已过期");
ErrorCode DAILY_PLAN_CLAIM_MISMATCH = new ErrorCode(42, "日计划领取信息与进度确认不一致");
ErrorCode DAILY_PLAN_CLAIM_PROGRESS_CONFLICT = new ErrorCode(43, "日计划作业进度已变化,该领取无法确认");
}

View File

@@ -19,6 +19,8 @@ public class DailyPlanClaimDO extends TenantBaseDO {
private Integer strategyType;
private Integer claimedQty;
private Integer planVersion;
private Integer progressQty;
private Integer version;
private Integer claimStatus;
private Integer activeFlag;
private LocalDateTime expireTime;

View File

@@ -15,11 +15,15 @@ public interface DailyPlanClaimMapper extends BaseMapperX<DailyPlanClaimDO> {
DailyPlanClaimDO selectByToken(@Param("claimToken") String claimToken);
int confirmClaim(@Param("claimId") Long claimId, @Param("updater") String updater);
DailyPlanClaimDO selectByTokenForUpdate(@Param("claimToken") String claimToken);
int confirmClaim(@Param("claimId") Long claimId, @Param("version") Integer version,
@Param("updater") String updater);
int releaseClaim(@Param("claimToken") String claimToken, @Param("updater") String updater);
int renewClaim(@Param("claimToken") String claimToken, @Param("expireTime") LocalDateTime expireTime,
int renewClaim(@Param("claimToken") String claimToken, @Param("version") Integer version,
@Param("expireTime") LocalDateTime expireTime,
@Param("updater") String updater);
int expireClaims(@Param("now") LocalDateTime now, @Param("updater") String updater);

View File

@@ -128,16 +128,16 @@ public interface DailyPlanMapper extends BaseMapperX<DailyPlanDO> {
/**
* 原子增加已备货数量,并在达到总量时完成备货。
*/
int increaseStockedQuantity(@Param("dailyPlanId") Long dailyPlanId,
@Param("incrementQty") Integer incrementQty,
@Param("version") Integer version,
@Param("updater") String updater);
int increaseStockedQuantityByClaim(@Param("dailyPlanId") Long dailyPlanId,
@Param("incrementQty") Integer incrementQty,
@Param("expectedQty") Integer expectedQty,
@Param("updater") String updater);
/**
* 原子增加已套管数量,并在达到总量时完成套管。
*/
int increaseSleevedQuantity(@Param("dailyPlanId") Long dailyPlanId,
@Param("incrementQty") Integer incrementQty,
@Param("version") Integer version,
@Param("updater") String updater);
int increaseSleevedQuantityByClaim(@Param("dailyPlanId") Long dailyPlanId,
@Param("incrementQty") Integer incrementQty,
@Param("expectedQty") Integer expectedQty,
@Param("updater") String updater);
}

View File

@@ -10,10 +10,10 @@ public interface DailyPlanScheduleService {
DailyPlanScheduleCandidate getNextSleevingCandidate(String operator);
/** 原子增加已备货数量。 */
void increaseStockedQuantity(String claimToken, Long dailyPlanId, Integer quantity, Integer version, String operator);
void increaseStockedQuantity(String claimToken, Long dailyPlanId, Integer quantity, String operator);
/** 原子增加已套管数量。 */
void increaseSleevedQuantity(String claimToken, Long dailyPlanId, Integer quantity, Integer version, String operator);
void increaseSleevedQuantity(String claimToken, Long dailyPlanId, Integer quantity, String operator);
/** 延长仍在执行中的领取租约。 */
void renewClaim(String claimToken, String operator);

View File

@@ -8,10 +8,8 @@ import cn.code.nl.module.lms.dal.mysql.dailyplan.DailyPlanClaimMapper;
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.IdUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
@@ -28,10 +26,10 @@ import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.ex
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_CLAIM_CONFLICT;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_CLAIM_INVALID;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_CLAIM_MISMATCH;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_CLAIM_PROGRESS_CONFLICT;
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;
@@ -44,6 +42,7 @@ public class DailyPlanScheduleServiceImpl implements DailyPlanScheduleService {
private static final String TARGET_SLEEVING = "SLEEVING";
private static final String TYPE_PROGRESS = "PROGRESS";
private static final int CLAIM_ACTIVE = 0;
private static final int CLAIM_CONFIRMED = 1;
@Resource
private DailyPlanMapper dailyPlanMapper;
@@ -128,6 +127,7 @@ public class DailyPlanScheduleServiceImpl implements DailyPlanScheduleService {
claim.setStrategyType(type.getCode());
claim.setClaimedQty(quantity);
claim.setPlanVersion(plan.getVersion());
claim.setProgressQty(type == DailyPlanStrategyTypeEnum.STOCKING ? plan.getStockedQty() : plan.getSleevedQty());
claim.setExpireTime(expireTime);
claim.setOperator(normalizedOperator(operator));
try {
@@ -154,56 +154,53 @@ public class DailyPlanScheduleServiceImpl implements DailyPlanScheduleService {
@Override
@Transactional(rollbackFor = Exception.class)
public void increaseStockedQuantity(String claimToken, Long dailyPlanId, Integer quantity, Integer version,
String operator) {
increaseQuantity(claimToken, dailyPlanId, quantity, version, operator, DailyPlanStrategyTypeEnum.STOCKING);
public void increaseStockedQuantity(String claimToken, Long dailyPlanId, Integer quantity, String operator) {
increaseQuantity(claimToken, dailyPlanId, quantity, operator, DailyPlanStrategyTypeEnum.STOCKING);
}
@Override
@Transactional(rollbackFor = Exception.class)
public void increaseSleevedQuantity(String claimToken, Long dailyPlanId, Integer quantity, Integer version,
String operator) {
increaseQuantity(claimToken, dailyPlanId, quantity, version, operator, DailyPlanStrategyTypeEnum.SLEEVING);
public void increaseSleevedQuantity(String claimToken, Long dailyPlanId, Integer quantity, String operator) {
increaseQuantity(claimToken, dailyPlanId, quantity, operator, DailyPlanStrategyTypeEnum.SLEEVING);
}
private void increaseQuantity(String claimToken, Long dailyPlanId, Integer quantity, Integer version, String operator,
private void increaseQuantity(String claimToken, Long dailyPlanId, Integer quantity, String operator,
DailyPlanStrategyTypeEnum type) {
if (quantity == null || quantity <= 0) {
throw exception(DAILY_PLAN_PROGRESS_QTY_NOT_POSITIVE);
}
DailyPlanClaimDO claim = validateActiveClaim(claimToken);
DailyPlanClaimDO claim = getClaimForUpdate(claimToken);
if (!claim.getDailyPlanId().equals(dailyPlanId) || !claim.getStrategyType().equals(type.getCode())
|| !claim.getClaimedQty().equals(quantity) || !claim.getPlanVersion().equals(version)) {
|| !claim.getClaimedQty().equals(quantity)) {
throw exception(DAILY_PLAN_CLAIM_MISMATCH);
}
if (Integer.valueOf(CLAIM_CONFIRMED).equals(claim.getClaimStatus())) {
return; // 同一 claim 的重复确认幂等成功,绝不重复累计。
}
validateActiveClaim(claim);
if (dailyPlanClaimMapper.confirmClaim(claim.getClaimId(), claim.getVersion(), normalizedOperator(operator)) != 1) {
DailyPlanClaimDO latest = getClaim(claimToken);
if (Integer.valueOf(CLAIM_CONFIRMED).equals(latest.getClaimStatus())) {
return;
}
throw exception(DAILY_PLAN_CLAIM_INVALID);
}
DailyPlanDO plan = dailyPlanMapper.selectDailyPlanById(dailyPlanId);
if (plan == null) {
throw exception(DAILY_PLAN_NOT_EXISTS);
}
if (!plan.getVersion().equals(claim.getPlanVersion())) {
// 领取后的任何计划变更都会使旧领取失效;不确认 claim调用方可处理冲突后主动释放或续租。
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) {
if ((long) claim.getProgressQty() + 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));
? dailyPlanMapper.increaseStockedQuantityByClaim(dailyPlanId, quantity, claim.getProgressQty(),
normalizedOperator(operator))
: dailyPlanMapper.increaseSleevedQuantityByClaim(dailyPlanId, quantity, claim.getProgressQty(),
normalizedOperator(operator));
if (affected != 1) {
throw exception(DAILY_PLAN_VERSION_CONFLICT);
}
if (dailyPlanClaimMapper.confirmClaim(claim.getClaimId(), normalizedOperator(operator)) != 1) {
throw exception(DAILY_PLAN_CLAIM_INVALID);
throw exception(DAILY_PLAN_CLAIM_PROGRESS_CONFLICT);
}
insertProgressLog(dailyPlanId, type, before, before + quantity, quantity, operator);
}
@@ -211,9 +208,11 @@ public class DailyPlanScheduleServiceImpl implements DailyPlanScheduleService {
@Override
@Transactional(rollbackFor = Exception.class)
public void renewClaim(String claimToken, String operator) {
validateActiveClaim(claimToken);
DailyPlanClaimDO claim = getClaimForUpdate(claimToken);
validateActiveClaim(claim);
LocalDateTime expireTime = nextClaimExpireTime();
if (dailyPlanClaimMapper.renewClaim(claimToken, expireTime, normalizedOperator(operator)) != 1) {
if (dailyPlanClaimMapper.renewClaim(claimToken, claim.getVersion(), expireTime,
normalizedOperator(operator)) != 1) {
throw exception(DAILY_PLAN_CLAIM_INVALID);
}
}
@@ -236,18 +235,35 @@ public class DailyPlanScheduleServiceImpl implements DailyPlanScheduleService {
return dailyPlanClaimMapper.expireClaims(LocalDateTime.now(), normalizedOperator(operator));
}
private DailyPlanClaimDO validateActiveClaim(String claimToken) {
private DailyPlanClaimDO getClaim(String claimToken) {
if (StrUtil.isBlank(claimToken)) {
throw exception(DAILY_PLAN_CLAIM_INVALID);
}
DailyPlanClaimDO claim = dailyPlanClaimMapper.selectByToken(claimToken);
if (claim == null || !Integer.valueOf(CLAIM_ACTIVE).equals(claim.getClaimStatus())
|| claim.getActiveFlag() == null || !claim.getExpireTime().isAfter(LocalDateTime.now())) {
if (claim == null) {
throw exception(DAILY_PLAN_CLAIM_INVALID);
}
return claim;
}
private DailyPlanClaimDO getClaimForUpdate(String claimToken) {
if (StrUtil.isBlank(claimToken)) {
throw exception(DAILY_PLAN_CLAIM_INVALID);
}
DailyPlanClaimDO claim = dailyPlanClaimMapper.selectByTokenForUpdate(claimToken);
if (claim == null) {
throw exception(DAILY_PLAN_CLAIM_INVALID);
}
return claim;
}
private void validateActiveClaim(DailyPlanClaimDO claim) {
if (!Integer.valueOf(CLAIM_ACTIVE).equals(claim.getClaimStatus()) || claim.getActiveFlag() == null
|| !claim.getExpireTime().isAfter(LocalDateTime.now())) {
throw exception(DAILY_PLAN_CLAIM_INVALID);
}
}
private LocalDateTime nextClaimExpireTime() {
return LocalDateTime.now().plusSeconds(Math.max(1L, claimTimeoutSeconds));
}

View File

@@ -3,18 +3,18 @@
<mapper namespace="cn.code.nl.module.lms.dal.mysql.dailyplan.DailyPlanClaimMapper">
<sql id="claimColumns">
claim_id, claim_token, daily_plan_id, strategy_type, claimed_qty, plan_version,
claim_id, claim_token, daily_plan_id, strategy_type, claimed_qty, plan_version, progress_qty, version,
claim_status, active_flag, expire_time, confirmed_time, operator,
creator, create_time, updater, update_time, deleted, tenant_id
</sql>
<insert id="insertClaim">
INSERT INTO lms_daily_plan_claim
(claim_id, claim_token, daily_plan_id, strategy_type, claimed_qty, plan_version,
(claim_id, claim_token, daily_plan_id, strategy_type, claimed_qty, plan_version, progress_qty, version,
claim_status, active_flag, expire_time, operator, creator, updater)
VALUES
(#{claim.claimId}, #{claim.claimToken}, #{claim.dailyPlanId}, #{claim.strategyType},
#{claim.claimedQty}, #{claim.planVersion}, 0, 1, #{claim.expireTime},
#{claim.claimedQty}, #{claim.planVersion}, #{claim.progressQty}, 0, 0, 1, #{claim.expireTime},
#{claim.operator}, #{claim.operator}, #{claim.operator})
</insert>
@@ -24,10 +24,17 @@
WHERE claim_token = #{claimToken} AND deleted = b'0'
</select>
<select id="selectByTokenForUpdate" resultType="cn.code.nl.module.lms.dal.dataobject.dailyplan.DailyPlanClaimDO">
SELECT <include refid="claimColumns"/>
FROM lms_daily_plan_claim
WHERE claim_token = #{claimToken} AND deleted = b'0'
FOR UPDATE
</select>
<update id="confirmClaim">
UPDATE lms_daily_plan_claim
SET claim_status = 1, active_flag = NULL, confirmed_time = NOW(), updater = #{updater}
WHERE claim_id = #{claimId} AND claim_status = 0 AND active_flag = 1
SET claim_status = 1, active_flag = NULL, confirmed_time = NOW(), updater = #{updater}, version = version + 1
WHERE claim_id = #{claimId} AND version = #{version} AND claim_status = 0 AND active_flag = 1
AND expire_time &gt; NOW() AND deleted = b'0'
</update>
@@ -39,8 +46,8 @@
<update id="renewClaim">
UPDATE lms_daily_plan_claim
SET expire_time = #{expireTime}, updater = #{updater}
WHERE claim_token = #{claimToken} AND claim_status = 0 AND active_flag = 1
SET expire_time = #{expireTime}, updater = #{updater}, version = version + 1
WHERE claim_token = #{claimToken} AND version = #{version} AND claim_status = 0 AND active_flag = 1
AND expire_time &gt; NOW() AND deleted = b'0'
</update>

View File

@@ -236,42 +236,42 @@
WHERE daily_plan_id = #{dailyPlanId} AND version = #{version} AND deleted = b'0'
</update>
<update id="increaseStockedQuantity">
<update id="increaseStockedQuantityByClaim">
UPDATE lms_daily_plan
SET stocking_status = CASE
WHEN stocked_qty + #{incrementQty} = plan_qty + append_qty THEN 2
WHEN stocking_status = 1 AND stocked_qty + #{incrementQty} = plan_qty + append_qty THEN 2
ELSE stocking_status END,
stocking_complete_time = CASE
WHEN stocked_qty + #{incrementQty} = plan_qty + append_qty THEN NOW()
WHEN stocking_status = 1 AND stocked_qty + #{incrementQty} = plan_qty + append_qty THEN NOW()
ELSE stocking_complete_time END,
stocked_qty = stocked_qty + #{incrementQty},
updater = #{updater},
version = version + 1
WHERE daily_plan_id = #{dailyPlanId}
AND version = #{version}
AND deleted = b'0'
AND order_status = 1
AND stocking_status = 1
AND order_status IN (1, 2, 3)
AND stocking_status IN (1, 2, 3)
AND stocked_qty = #{expectedQty}
AND #{incrementQty} &gt; 0
AND stocked_qty + #{incrementQty} &lt;= plan_qty + append_qty
</update>
<update id="increaseSleevedQuantity">
<update id="increaseSleevedQuantityByClaim">
UPDATE lms_daily_plan
SET sleeving_status = CASE
WHEN sleeved_qty + #{incrementQty} = plan_qty + append_qty THEN 2
WHEN sleeving_status = 1 AND sleeved_qty + #{incrementQty} = plan_qty + append_qty THEN 2
ELSE sleeving_status END,
sleeving_complete_time = CASE
WHEN sleeved_qty + #{incrementQty} = plan_qty + append_qty THEN NOW()
WHEN sleeving_status = 1 AND sleeved_qty + #{incrementQty} = plan_qty + append_qty THEN NOW()
ELSE sleeving_complete_time END,
sleeved_qty = sleeved_qty + #{incrementQty},
updater = #{updater},
version = version + 1
WHERE daily_plan_id = #{dailyPlanId}
AND version = #{version}
AND deleted = b'0'
AND order_status = 1
AND sleeving_status = 1
AND order_status IN (1, 2, 3)
AND sleeving_status IN (1, 2, 3)
AND sleeved_qty = #{expectedQty}
AND #{incrementQty} &gt; 0
AND sleeved_qty + #{incrementQty} &lt;= plan_qty + append_qty
</update>