feat: 增加ERP日计划推送接口

This commit is contained in:
zhouz
2026-08-14 14:35:27 +08:00
parent 9d7482af89
commit 0cab62b30d
7 changed files with 262 additions and 12 deletions

View File

@@ -0,0 +1,54 @@
package cn.code.nl.module.lms.controller.erp.dailyplan;
import cn.code.nl.framework.common.pojo.CommonResult;
import cn.code.nl.module.lms.controller.erp.dailyplan.vo.ErpDailyPlanPushReqVO;
import cn.code.nl.module.lms.service.dailyplan.DailyPlanService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.annotation.Resource;
import jakarta.validation.Valid;
import jakarta.validation.constraints.NotBlank;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_ERP_TOKEN_ERROR;
/** ERP 日计划对接接口。 */
@Tag(name = "ERP - 日计划")
@RestController
@RequestMapping("/lms/daily-plan")
@Validated
public class ErpDailyPlanController {
/** ERP 日计划接口令牌。 */
@Value("${erp.daily-plan.token}")
private String erpDailyPlanToken;
/** 日计划服务。 */
@Resource
private DailyPlanService dailyPlanService;
/** 接收 ERP 日计划;调用方还必须携带标准 tenant-id 请求头。 */
@PostMapping("/erpPush")
@Operation(summary = "ERP 推送日计划", description = "除 X-ERP-Token 外,还必须携带项目标准 tenant-id 请求头,数据按租户隔离")
public CommonResult<String> erpPush(
@Parameter(description = "ERP 接口令牌", required = true)
@RequestHeader("X-ERP-Token") @NotBlank(message = "ERP接口令牌不能为空") String token,
@Valid @RequestBody ErpDailyPlanPushReqVO reqVO) {
if (!MessageDigest.isEqual(token.getBytes(StandardCharsets.UTF_8),
erpDailyPlanToken.getBytes(StandardCharsets.UTF_8))) {
throw exception(DAILY_PLAN_ERP_TOKEN_ERROR);
}
return CommonResult.success(dailyPlanService.pushErpDailyPlan(reqVO));
}
}

View File

@@ -0,0 +1,60 @@
package cn.code.nl.module.lms.controller.erp.dailyplan.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import jakarta.validation.constraints.Size;
import lombok.Data;
import java.time.LocalDate;
/** ERP 日计划推送请求。 */
@Data
@Schema(description = "ERP 日计划推送请求")
public class ErpDailyPlanPushReqVO {
/** ERP 订单号。 */
@Schema(description = "ERP 订单号", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "ERP订单号不能为空")
@Size(max = 64, message = "ERP订单号长度不能超过64个字符")
private String erpOrderCode;
/** 计划日期。 */
@Schema(description = "计划日期", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "计划日期不能为空")
private LocalDate planDate;
/** 物料标识。 */
@Schema(description = "物料标识", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "物料标识不能为空")
@Positive(message = "物料标识必须为正整数")
private Long materialId;
/** 物料编码。 */
@Schema(description = "物料编码", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "物料编码不能为空")
@Size(max = 64, message = "物料编码长度不能超过64个字符")
private String materialCode;
/** 物料名称。 */
@Schema(description = "物料名称", requiredMode = Schema.RequiredMode.REQUIRED)
@NotBlank(message = "物料名称不能为空")
@Size(max = 128, message = "物料名称长度不能超过128个字符")
private String materialName;
/** 物料规格。 */
@Schema(description = "物料规格")
@Size(max = 128, message = "物料规格长度不能超过128个字符")
private String materialSpec;
/** ERP 最新计划总数量。 */
@Schema(description = "ERP 最新计划总数量", requiredMode = Schema.RequiredMode.REQUIRED)
@NotNull(message = "计划数量不能为空")
@Positive(message = "计划数量必须为正整数")
private Integer planQty;
/** 包装信息。 */
@Schema(description = "包装信息")
private String packingInfo;
}

View File

@@ -56,6 +56,11 @@ public interface DailyPlanMapper extends BaseMapperX<DailyPlanDO> {
*/
DailyPlanDO selectByErpOrderCode(@Param("erpOrderCode") String erpOrderCode);
/**
* 查询当前租户下一个日计划排序序号。
*/
Integer selectNextSortSeq();
/**
* 查询待备货日计划。
*/

View File

@@ -105,6 +105,8 @@ public class SecurityConfiguration {
.requestMatchers("/webjars/**").permitAll()
.requestMatchers("/swagger-ui").permitAll()
.requestMatchers("/swagger-ui/**").permitAll();
// ERP 日计划接口使用独立令牌鉴权
registry.requestMatchers("/lms/daily-plan/erpPush").permitAll();
// Spring Boot Actuator 的安全配置
registry.requestMatchers("/actuator").permitAll()
.requestMatchers("/actuator/**").permitAll();

View File

@@ -2,11 +2,14 @@ package cn.code.nl.module.lms.service.dailyplan;
import cn.code.nl.framework.common.pojo.PageResult;
import cn.code.nl.module.lms.controller.admin.dailyplan.vo.*;
import cn.code.nl.module.lms.controller.erp.dailyplan.vo.ErpDailyPlanPushReqVO;
import java.util.List;
/** LMS 日计划服务。 */
public interface DailyPlanService {
/** ERP 幂等推送日计划。 */
String pushErpDailyPlan(ErpDailyPlanPushReqVO reqVO);
/** 查询日计划管理分页。 */
PageResult<DailyPlanRespVO> getManagementPage(DailyPlanPageReqVO reqVO);
/** 查询日计划作业分页。 */

View File

@@ -19,6 +19,7 @@ import cn.code.nl.module.lms.controller.admin.dailyplan.vo.DailyPlanUpdateReqVO;
import cn.code.nl.module.lms.controller.admin.dailyplan.vo.DailyPlanVersionReqVO;
import cn.code.nl.module.lms.controller.admin.dailyplan.vo.DailyPlanWeightUpdateReqVO;
import cn.code.nl.module.lms.controller.admin.dailyplan.vo.DailyPlanWorkPageReqVO;
import cn.code.nl.module.lms.controller.erp.dailyplan.vo.ErpDailyPlanPushReqVO;
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;
@@ -33,8 +34,12 @@ 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.dao.DuplicateKeyException;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.support.TransactionTemplate;
import org.springframework.validation.annotation.Validated;
import java.time.LocalDateTime;
@@ -46,6 +51,8 @@ import java.util.Set;
import static cn.code.nl.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.code.nl.framework.security.core.util.SecurityFrameworkUtils.getLoginUserId;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_CODE_GENERATE_FAILED;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_ERP_FINAL_STATUS_NOT_UPDATE;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_ERP_PLAN_QTY_NOT_DECREASE;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_NOT_EXISTS;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_QTY_EXCEEDS_LIMIT;
import static cn.code.nl.module.lms.enums.ErrorCodeConstants.DAILY_PLAN_STATUS_OPERATION_NOT_ALLOWED;
@@ -71,6 +78,8 @@ public class DailyPlanServiceImpl implements DailyPlanService {
private static final String TYPE_FINISH = "FINISH";
private static final String TYPE_STOP = "STOP";
private static final String TYPE_UPDATE = "UPDATE";
private static final String TYPE_CREATE = "CREATE";
private static final String ERP_OPERATOR = "ERP";
@Resource
private DailyPlanMapper dailyPlanMapper;
@@ -80,6 +89,104 @@ public class DailyPlanServiceImpl implements DailyPlanService {
private DailyPlanOperationLogMapper dailyPlanOperationLogMapper;
@Resource
private CodeGenApi codeGenApi;
@Resource
private PlatformTransactionManager transactionManager;
/** ERP 幂等推送日计划,并在首次推送并发冲突时进行一次独立事务重试。 */
@Override
@Transactional(rollbackFor = Exception.class)
public String pushErpDailyPlan(ErpDailyPlanPushReqVO reqVO) {
TransactionTemplate transactionTemplate = new TransactionTemplate(transactionManager);
transactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW);
try {
return transactionTemplate.execute(status -> processErpDailyPlan(reqVO));
} catch (DuplicateKeyException ex) {
return transactionTemplate.execute(status -> processErpDailyPlan(reqVO));
}
}
/** 按 ERP 订单号和当前状态创建或更新日计划。 */
private String processErpDailyPlan(ErpDailyPlanPushReqVO reqVO) {
DailyPlanDO current = dailyPlanMapper.selectByErpOrderCode(reqVO.getErpOrderCode());
if (current == null) {
return createErpDailyPlan(reqVO);
}
if (DailyPlanOrderStatusEnum.NOT_STARTED.getCode().equals(current.getOrderStatus())) {
return updateNotStartedErpDailyPlan(current, reqVO);
}
if (DailyPlanOrderStatusEnum.IN_PROGRESS.getCode().equals(current.getOrderStatus())) {
return appendInProgressErpDailyPlan(current, reqVO);
}
throw exception(DAILY_PLAN_ERP_FINAL_STATUS_NOT_UPDATE);
}
/** 创建 ERP 日计划并记录创建日志。 */
private String createErpDailyPlan(ErpDailyPlanPushReqVO reqVO) {
DailyPlanDO plan = new DailyPlanDO();
plan.setDailyPlanId(IdWorker.getId());
plan.setPlanCode(generatePlanCode());
plan.setSourceType(DailyPlanSourceTypeEnum.ERP.getCode());
plan.setErpOrderCode(reqVO.getErpOrderCode());
plan.setManualOrderCode(null);
setErpFields(plan, reqVO);
plan.setSortSeq(dailyPlanMapper.selectNextSortSeq());
plan.setWeight(1);
plan.setCreator(ERP_OPERATOR);
plan.setUpdater(ERP_OPERATOR);
checkAffected(dailyPlanMapper.insertDailyPlan(plan));
insertLog(plan.getDailyPlanId(), TARGET_ORDER, TYPE_CREATE, null, String.valueOf(plan.getPlanQty()),
plan.getPlanQty(), null, ERP_OPERATOR);
return plan.getPlanCode();
}
/** 覆盖未开始 ERP 日计划的 ERP 可编辑字段并记录更新日志。 */
private String updateNotStartedErpDailyPlan(DailyPlanDO current, ErpDailyPlanPushReqVO reqVO) {
DailyPlanDO update = new DailyPlanDO();
update.setDailyPlanId(current.getDailyPlanId());
update.setVersion(current.getVersion());
update.setErpOrderCode(current.getErpOrderCode());
update.setManualOrderCode(current.getManualOrderCode());
setErpFields(update, reqVO);
update.setSortSeq(current.getSortSeq());
update.setWeight(current.getWeight());
update.setUpdater(ERP_OPERATOR);
checkAffected(dailyPlanMapper.updateNotStartedPlan(update));
insertLog(current.getDailyPlanId(), TARGET_QUANTITY, TYPE_UPDATE, String.valueOf(current.getPlanQty()),
String.valueOf(reqVO.getPlanQty()), null, null, ERP_OPERATOR);
return current.getPlanCode();
}
/** 按 ERP 最新需求总量幂等追加进行中日计划数量。 */
private String appendInProgressErpDailyPlan(DailyPlanDO current, ErpDailyPlanPushReqVO reqVO) {
int currentTotalQty = safeAddQuantity(current.getPlanQty(), current.getAppendQty());
if (reqVO.getPlanQty().equals(currentTotalQty)) {
return current.getPlanCode();
}
if (reqVO.getPlanQty() < currentTotalQty) {
throw exception(DAILY_PLAN_ERP_PLAN_QTY_NOT_DECREASE);
}
int changeQty = reqVO.getPlanQty() - currentTotalQty;
DailyPlanDO update = new DailyPlanDO();
update.setDailyPlanId(current.getDailyPlanId());
update.setVersion(current.getVersion());
update.setAppendQty(safeAddQuantity(current.getAppendQty(), changeQty));
update.setUpdater(ERP_OPERATOR);
checkAffected(dailyPlanMapper.updateStateWithVersion(update));
insertLog(current.getDailyPlanId(), TARGET_QUANTITY, TYPE_APPEND, String.valueOf(current.getAppendQty()),
String.valueOf(update.getAppendQty()), changeQty, null, ERP_OPERATOR);
return current.getPlanCode();
}
/** 设置 ERP 推送的日计划字段。 */
private void setErpFields(DailyPlanDO plan, ErpDailyPlanPushReqVO reqVO) {
plan.setPlanDate(reqVO.getPlanDate());
plan.setMaterialId(reqVO.getMaterialId());
plan.setMaterialCode(reqVO.getMaterialCode());
plan.setMaterialName(reqVO.getMaterialName());
plan.setMaterialSpec(reqVO.getMaterialSpec());
plan.setPlanQty(reqVO.getPlanQty());
plan.setPackingInfo(reqVO.getPackingInfo());
}
/** 查询日计划管理分页。 */
@Override
@@ -159,6 +266,22 @@ public class DailyPlanServiceImpl implements DailyPlanService {
@Override
@Transactional(rollbackFor = Exception.class)
public Long createManualPlan(DailyPlanCreateReqVO reqVO) {
String planCode = generatePlanCode();
String userId = String.valueOf(getLoginUserId());
DailyPlanDO plan = new DailyPlanDO();
plan.setDailyPlanId(IdWorker.getId());
plan.setPlanCode(planCode);
plan.setSourceType(DailyPlanSourceTypeEnum.MANUAL.getCode());
plan.setManualOrderCode(StrUtil.isEmpty(reqVO.getManualOrderCode()) ? planCode : reqVO.getManualOrderCode());
setEditableFields(plan, reqVO);
plan.setCreator(userId);
plan.setUpdater(userId);
checkAffected(dailyPlanMapper.insertDailyPlan(plan));
return plan.getDailyPlanId();
}
/** 通过编码服务安全生成日计划编码。 */
private String generatePlanCode() {
CodeGenerateReqDTO codeReq = new CodeGenerateReqDTO();
codeReq.setRuleCode("LMS_DAILY_PLAN_CODE");
CommonResult<String> codeResult = codeGenApi.generate(codeReq);
@@ -171,17 +294,7 @@ public class DailyPlanServiceImpl implements DailyPlanService {
if (StrUtil.isEmpty(codeResult.getData())) {
throw exception(DAILY_PLAN_CODE_GENERATE_FAILED);
}
String userId = String.valueOf(getLoginUserId());
DailyPlanDO plan = new DailyPlanDO();
plan.setDailyPlanId(IdWorker.getId());
plan.setPlanCode(codeResult.getData());
plan.setSourceType(DailyPlanSourceTypeEnum.MANUAL.getCode());
plan.setManualOrderCode(StrUtil.isEmpty(reqVO.getManualOrderCode()) ? codeResult.getData() : reqVO.getManualOrderCode());
setEditableFields(plan, reqVO);
plan.setCreator(userId);
plan.setUpdater(userId);
checkAffected(dailyPlanMapper.insertDailyPlan(plan));
return plan.getDailyPlanId();
return codeResult.getData();
}
/** 修改人工日计划。 */
@@ -532,6 +645,13 @@ public class DailyPlanServiceImpl implements DailyPlanService {
/** 新增日计划操作日志,操作时间由数据库生成。 */
private void insertLog(Long dailyPlanId, String target, String type, String beforeValue, String afterValue,
Integer changeQty, String reason) {
insertLog(dailyPlanId, target, type, beforeValue, afterValue, changeQty, reason,
String.valueOf(getLoginUserId()));
}
/** 新增指定操作人的日计划操作日志,供无管理端登录态的外部接口使用。 */
private void insertLog(Long dailyPlanId, String target, String type, String beforeValue, String afterValue,
Integer changeQty, String reason, String operator) {
DailyPlanOperationLogDO log = new DailyPlanOperationLogDO();
log.setOperationLogId(IdWorker.getId());
log.setDailyPlanId(dailyPlanId);
@@ -541,7 +661,7 @@ public class DailyPlanServiceImpl implements DailyPlanService {
log.setAfterValue(afterValue);
log.setChangeQty(changeQty);
log.setOperationReason(reason);
log.setOperator(String.valueOf(getLoginUserId()));
log.setOperator(operator);
checkAffected(dailyPlanOperationLogMapper.insertOperationLog(log));
}

View File

@@ -79,6 +79,12 @@
AND deleted = b'0'
</select>
<select id="selectNextSortSeq" resultType="java.lang.Integer">
SELECT COALESCE(MAX(sort_seq), 0) + 1
FROM lms_daily_plan
WHERE deleted = b'0'
</select>
<select id="selectStockingCandidates"
resultType="cn.code.nl.module.lms.dal.dataobject.dailyplan.DailyPlanDO">
SELECT <include refid="dailyPlanColumns"/>