fix: 收紧日计划排程与租户策略边界

This commit is contained in:
zhouz
2026-08-14 16:17:48 +08:00
parent a693d528cc
commit f3c1f61dbd
8 changed files with 94 additions and 23 deletions

View File

@@ -76,7 +76,7 @@
- 备货策略。
- 套管策略。
每条配置保存业务类型、当前模式、修改人、修改时间和版本号。当前模式包括“顺序模式”和“权重轮转模式”。数据库唯一约束保证同一租户、同一业务类型只有一条有效配置。
每条配置保存业务类型、当前模式、修改人、修改时间和版本号。当前模式包括“顺序模式”和“权重轮转模式”。数据库唯一约束保证同一租户、同一业务类型只有一条有效配置;查询或调度时若当前租户尚无配置,则并发安全地懒初始化备货、套管两类默认顺序策略,已逻辑删除的历史配置不自动恢复或重复插入
### 操作记录表 `lms_daily_plan_operation_log`
@@ -135,7 +135,7 @@ ERP 推送不允许直接修改顺序、权重、当前完成数量或作业状
## 人工创建和维护
人工创建字段包括计划日期、人工订单号、管芯物料、计划数量、装箱信息、顺序和权重。人工订单号允许手填;未填写时使用 LMS 日计划编号作为订单号。
人工创建字段包括计划日期、人工订单号、管芯物料、计划数量、装箱信息、顺序和权重。人工订单号统一去除首尾空白;未填写、全空白或修改时清空,均使用 LMS 日计划编号作为订单号。
只有未开始的人工来源日计划允许编辑基本信息。ERP 来源计划通过 ERP 幂等推送维护,管理页面只读。开始订单时 ERP 和人工来源均允许再次确认计划数量,并可填写追加数量;计划数量、累计追加数量和开始状态在同一事务中原子更新,开始成功后进入日计划作业页面。
@@ -238,7 +238,7 @@ ERP 推送不允许直接修改顺序、权重、当前完成数量或作业状
- 未开始订单不能执行备货、套管、追加和结束作业操作。
- 已结束订单不能取消、恢复、追加或重新开始。
- 完成数量不能为负数,也不能超过需求总量。
- 顺序号和权重必须为正整数。
- 顺序号和权重必须为正整数,且只允许对订单状态为“进行中”的计划修改;批量排序任一计划状态不符时整批回滚
- 取消和恢复未填写原因时由参数校验直接拒绝。
- 非法状态流转返回业务异常,不做静默幂等成功。
- 请求失败沿用全局错误提示,不在前端吞掉异常。

View File

@@ -1,10 +1,7 @@
package cn.code.nl.module.lms.controller.admin.dailyplan.vo;
import cn.code.nl.module.system.api.user.AdminUserApi;
import com.baomidou.mybatisplus.annotation.TableId;
import lombok.Data;
import org.dromara.core.trans.anno.Trans;
import org.dromara.core.trans.constant.TransType;
import org.dromara.core.trans.vo.VO;
import java.time.LocalDate;
@@ -47,7 +44,6 @@ public class DailyPlanRespVO implements VO {
private Integer version;
private String creator;
private LocalDateTime createTime;
@Trans(type = TransType.AUTO_TRANS, key = AdminUserApi.PREFIX, fields = "nickname", ref = "updaterName")
private String updater;
private String updaterName;
private LocalDateTime updateTime;

View File

@@ -16,6 +16,9 @@ public interface DailyPlanStrategyMapper extends BaseMapperX<DailyPlanStrategyDO
*/
DailyPlanStrategyDO selectByStrategyType(@Param("strategyType") Integer strategyType);
/** 查询包含逻辑删除记录的策略,用于避免唯一键冲突时误补。 */
DailyPlanStrategyDO selectAnyByStrategyType(@Param("strategyType") Integer strategyType);
/**
* 乐观锁更新策略模式并清空游标。
*/

View File

@@ -53,6 +53,8 @@ public class DailyPlanScheduleServiceImpl implements DailyPlanScheduleService {
private DailyPlanStrategyMapper dailyPlanStrategyMapper;
@Resource
private DailyPlanOperationLogMapper dailyPlanOperationLogMapper;
@Resource
private DailyPlanStrategyInitializer strategyInitializer;
@Value("${lms.daily-plan.claim-timeout-seconds:300}")
private long claimTimeoutSeconds;
@@ -72,10 +74,7 @@ public class DailyPlanScheduleServiceImpl implements DailyPlanScheduleService {
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);
}
DailyPlanStrategyDO strategy = strategyInitializer.getOrCreate(type);
if (candidates.isEmpty()) {
return null;
}

View File

@@ -93,6 +93,8 @@ public class DailyPlanServiceImpl implements DailyPlanService {
@Resource
private DailyPlanOperationLogMapper dailyPlanOperationLogMapper;
@Resource
private DailyPlanStrategyInitializer strategyInitializer;
@Resource
private CodeGenApi codeGenApi;
@Resource
private AdminUserApi adminUserApi;
@@ -207,6 +209,7 @@ public class DailyPlanServiceImpl implements DailyPlanService {
for (DailyPlanDO plan : page.getList()) {
result.add(convertPlan(plan));
}
resolvePlanUpdaterNames(result);
return new PageResult<>(result, page.getTotal());
}
@@ -222,6 +225,7 @@ public class DailyPlanServiceImpl implements DailyPlanService {
for (DailyPlanDO plan : page.getList()) {
result.add(convertPlan(plan));
}
resolvePlanUpdaterNames(result);
return new PageResult<>(result, page.getTotal());
}
@@ -232,7 +236,9 @@ public class DailyPlanServiceImpl implements DailyPlanService {
if (plan == null) {
throw exception(DAILY_PLAN_NOT_EXISTS);
}
return convertPlan(plan);
DailyPlanRespVO result = convertPlan(plan);
resolvePlanUpdaterNames(List.of(result));
return result;
}
/** 查询日计划操作日志。 */
@@ -273,9 +279,12 @@ public class DailyPlanServiceImpl implements DailyPlanService {
if (ERP_OPERATOR.equalsIgnoreCase(operator)) {
return "ERP";
}
if ("SCHEDULER".equalsIgnoreCase(operator) || "系统调度".equals(operator)) {
return "系统调度";
}
Long operatorId = parseOperatorId(operator);
if (operatorId == null) {
return "系统";
return StrUtil.blankToDefault(operator, "系统");
}
AdminUserRespDTO user = userMap.get(operatorId);
return user == null ? "未知用户" : user.getNickname();
@@ -297,10 +306,7 @@ public class DailyPlanServiceImpl implements DailyPlanService {
public List<DailyPlanStrategyRespVO> getStrategies() {
List<DailyPlanStrategyRespVO> result = new ArrayList<>(2);
for (DailyPlanStrategyTypeEnum type : DailyPlanStrategyTypeEnum.values()) {
DailyPlanStrategyDO strategy = dailyPlanStrategyMapper.selectByStrategyType(type.getCode());
if (strategy == null) {
throw exception(DAILY_PLAN_STRATEGY_NOT_EXISTS);
}
DailyPlanStrategyDO strategy = strategyInitializer.getOrCreate(type);
result.add(convertStrategy(strategy));
}
return result;
@@ -316,7 +322,7 @@ public class DailyPlanServiceImpl implements DailyPlanService {
plan.setDailyPlanId(IdWorker.getId());
plan.setPlanCode(planCode);
plan.setSourceType(DailyPlanSourceTypeEnum.MANUAL.getCode());
plan.setManualOrderCode(StrUtil.isEmpty(reqVO.getManualOrderCode()) ? planCode : reqVO.getManualOrderCode());
plan.setManualOrderCode(StrUtil.blankToDefault(StrUtil.trim(reqVO.getManualOrderCode()), planCode));
setEditableFields(plan, reqVO);
plan.setCreator(userId);
plan.setUpdater(userId);
@@ -353,7 +359,7 @@ public class DailyPlanServiceImpl implements DailyPlanService {
DailyPlanDO update = new DailyPlanDO();
update.setDailyPlanId(reqVO.getDailyPlanId());
update.setVersion(reqVO.getVersion());
update.setManualOrderCode(reqVO.getManualOrderCode());
update.setManualOrderCode(StrUtil.blankToDefault(StrUtil.trim(reqVO.getManualOrderCode()), current.getPlanCode()));
setEditableFields(update, reqVO);
update.setUpdater(String.valueOf(getLoginUserId()));
checkAffected(dailyPlanMapper.updateNotStartedPlan(update));
@@ -528,7 +534,7 @@ public class DailyPlanServiceImpl implements DailyPlanService {
throw exception(DAILY_PLAN_STATUS_OPERATION_NOT_ALLOWED, item.getId(), "重复修改排序");
}
DailyPlanDO current = validateVersion(item.getId(), item.getVersion());
validateNotFinished(current, "修改排序");
validateOrderStatus(current, DailyPlanOrderStatusEnum.IN_PROGRESS, "修改排序");
checkAffected(dailyPlanMapper.updateSortWithVersion(current.getDailyPlanId(), item.getSortSeq(),
item.getVersion(), String.valueOf(getLoginUserId())));
insertLog(current.getDailyPlanId(), TARGET_SORT, TYPE_UPDATE, String.valueOf(current.getSortSeq()),
@@ -541,7 +547,7 @@ public class DailyPlanServiceImpl implements DailyPlanService {
@Transactional(rollbackFor = Exception.class)
public void updateWeight(DailyPlanWeightUpdateReqVO reqVO) {
DailyPlanDO current = validateVersion(reqVO.getId(), reqVO.getVersion());
validateNotFinished(current, "修改权重");
validateOrderStatus(current, DailyPlanOrderStatusEnum.IN_PROGRESS, "修改权重");
checkAffected(dailyPlanMapper.updateWeightWithVersion(current.getDailyPlanId(), reqVO.getWeight(),
reqVO.getVersion(), String.valueOf(getLoginUserId())));
insertLog(current.getDailyPlanId(), TARGET_WEIGHT, TYPE_UPDATE, String.valueOf(current.getWeight()),
@@ -722,6 +728,14 @@ public class DailyPlanServiceImpl implements DailyPlanService {
checkAffected(dailyPlanOperationLogMapper.insertOperationLog(log));
}
/** 批量解析列表修改人,数字用户走用户服务,系统标识保留可靠的显示名称。 */
private void resolvePlanUpdaterNames(List<DailyPlanRespVO> plans) {
Set<Long> userIds = plans.stream().map(DailyPlanRespVO::getUpdater)
.map(DailyPlanServiceImpl::parseOperatorId).filter(Objects::nonNull).collect(Collectors.toSet());
Map<Long, AdminUserRespDTO> userMap = userIds.isEmpty() ? Map.of() : adminUserApi.getUserMap(userIds);
plans.forEach(plan -> plan.setUpdaterName(resolveOperatorName(plan.getUpdater(), userMap)));
}
/** 将日计划数据对象逐字段转换为响应对象。 */
private DailyPlanRespVO convertPlan(DailyPlanDO plan) {
DailyPlanRespVO response = new DailyPlanRespVO();

View File

@@ -0,0 +1,51 @@
package cn.code.nl.module.lms.service.dailyplan;
import cn.code.nl.module.lms.dal.dataobject.dailyplan.DailyPlanStrategyDO;
import cn.code.nl.module.lms.dal.mysql.dailyplan.DailyPlanStrategyMapper;
import cn.code.nl.module.lms.enums.DailyPlanStrategyModeEnum;
import cn.code.nl.module.lms.enums.DailyPlanStrategyTypeEnum;
import com.baomidou.mybatisplus.core.toolkit.IdWorker;
import jakarta.annotation.Resource;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.stereotype.Component;
import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_STRATEGY_NOT_EXISTS;
/** 按当前租户幂等补齐默认日计划策略。 */
@Component
public class DailyPlanStrategyInitializer {
private static final String SYSTEM_OPERATOR = "SCHEDULER";
@Resource
private DailyPlanStrategyMapper strategyMapper;
public DailyPlanStrategyDO getOrCreate(DailyPlanStrategyTypeEnum type) {
DailyPlanStrategyDO current = strategyMapper.selectByStrategyType(type.getCode());
if (current != null) {
return current;
}
// 唯一键不包含 deleted存在已删除记录时不能另插一条也不能擅自恢复历史配置。
if (strategyMapper.selectAnyByStrategyType(type.getCode()) != null) {
throw exception(DAILY_PLAN_STRATEGY_NOT_EXISTS);
}
DailyPlanStrategyDO strategy = new DailyPlanStrategyDO();
strategy.setStrategyId(IdWorker.getId());
strategy.setStrategyType(type.getCode());
strategy.setStrategyMode(DailyPlanStrategyModeEnum.SEQUENCE.getCode());
strategy.setVersion(0);
strategy.setCreator(SYSTEM_OPERATOR);
strategy.setUpdater(SYSTEM_OPERATOR);
try {
strategyMapper.insert(strategy);
return strategy;
} catch (DuplicateKeyException ignored) {
DailyPlanStrategyDO concurrent = strategyMapper.selectByStrategyType(type.getCode());
if (concurrent != null) {
return concurrent;
}
throw exception(DAILY_PLAN_STRATEGY_NOT_EXISTS);
}
}
}

View File

@@ -263,13 +263,13 @@
<update id="updateSortWithVersion">
UPDATE lms_daily_plan
SET sort_seq = #{sortSeq}, updater = #{updater}, version = version + 1
WHERE daily_plan_id = #{dailyPlanId} AND version = #{version} AND deleted = b'0'
WHERE daily_plan_id = #{dailyPlanId} AND version = #{version} AND order_status = 1 AND deleted = b'0'
</update>
<update id="updateWeightWithVersion">
UPDATE lms_daily_plan
SET weight = #{weight}, updater = #{updater}, version = version + 1
WHERE daily_plan_id = #{dailyPlanId} AND version = #{version} AND deleted = b'0'
WHERE daily_plan_id = #{dailyPlanId} AND version = #{version} AND order_status = 1 AND deleted = b'0'
</update>
<update id="increaseStockedQuantityByClaim">

View File

@@ -15,6 +15,14 @@
AND deleted = b'0'
</select>
<select id="selectAnyByStrategyType"
resultType="cn.code.nl.module.lms.dal.dataobject.dailyplan.DailyPlanStrategyDO">
SELECT <include refid="strategyColumns"/>
FROM lms_daily_plan_strategy
WHERE strategy_type = #{strategyType}
LIMIT 1
</select>
<update id="updateModeWithVersion">
UPDATE lms_daily_plan_strategy
SET strategy_mode = #{strategyMode},