From 0cab62b30d53aa7fe3abc1918f345c2df8c94903 Mon Sep 17 00:00:00 2001 From: zhouz <> Date: Fri, 14 Aug 2026 14:35:27 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0ERP=E6=97=A5=E8=AE=A1?= =?UTF-8?q?=E5=88=92=E6=8E=A8=E9=80=81=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../erp/dailyplan/ErpDailyPlanController.java | 54 +++++++ .../dailyplan/vo/ErpDailyPlanPushReqVO.java | 60 ++++++++ .../dal/mysql/dailyplan/DailyPlanMapper.java | 5 + .../config/SecurityConfiguration.java | 2 + .../service/dailyplan/DailyPlanService.java | 3 + .../dailyplan/DailyPlanServiceImpl.java | 144 ++++++++++++++++-- .../mapper/dailyplan/DailyPlanMapper.xml | 6 + 7 files changed, 262 insertions(+), 12 deletions(-) create mode 100644 nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/erp/dailyplan/ErpDailyPlanController.java create mode 100644 nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/erp/dailyplan/vo/ErpDailyPlanPushReqVO.java diff --git a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/erp/dailyplan/ErpDailyPlanController.java b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/erp/dailyplan/ErpDailyPlanController.java new file mode 100644 index 00000000..bb1aa674 --- /dev/null +++ b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/erp/dailyplan/ErpDailyPlanController.java @@ -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 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)); + } +} diff --git a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/erp/dailyplan/vo/ErpDailyPlanPushReqVO.java b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/erp/dailyplan/vo/ErpDailyPlanPushReqVO.java new file mode 100644 index 00000000..abaec4d7 --- /dev/null +++ b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/controller/erp/dailyplan/vo/ErpDailyPlanPushReqVO.java @@ -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; +} diff --git a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/dailyplan/DailyPlanMapper.java b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/dailyplan/DailyPlanMapper.java index 960d5c54..0991df5f 100644 --- a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/dailyplan/DailyPlanMapper.java +++ b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/dal/mysql/dailyplan/DailyPlanMapper.java @@ -56,6 +56,11 @@ public interface DailyPlanMapper extends BaseMapperX { */ DailyPlanDO selectByErpOrderCode(@Param("erpOrderCode") String erpOrderCode); + /** + * 查询当前租户下一个日计划排序序号。 + */ + Integer selectNextSortSeq(); + /** * 查询待备货日计划。 */ diff --git a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/framwork/security/config/SecurityConfiguration.java b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/framwork/security/config/SecurityConfiguration.java index 7ef36b0f..6c6cab41 100644 --- a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/framwork/security/config/SecurityConfiguration.java +++ b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/framwork/security/config/SecurityConfiguration.java @@ -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(); diff --git a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanService.java b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanService.java index 8a08cfb7..4af7b35b 100644 --- a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanService.java +++ b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanService.java @@ -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 getManagementPage(DailyPlanPageReqVO reqVO); /** 查询日计划作业分页。 */ diff --git a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanServiceImpl.java b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanServiceImpl.java index 21c14aca..b9bf57ab 100644 --- a/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanServiceImpl.java +++ b/nl-module-lms/nl-module-lms-server/src/main/java/cn/code/nl/module/lms/service/dailyplan/DailyPlanServiceImpl.java @@ -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 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)); } diff --git a/nl-module-lms/nl-module-lms-server/src/main/resources/mapper/dailyplan/DailyPlanMapper.xml b/nl-module-lms/nl-module-lms-server/src/main/resources/mapper/dailyplan/DailyPlanMapper.xml index b758da5a..c95a48c4 100644 --- a/nl-module-lms/nl-module-lms-server/src/main/resources/mapper/dailyplan/DailyPlanMapper.xml +++ b/nl-module-lms/nl-module-lms-server/src/main/resources/mapper/dailyplan/DailyPlanMapper.xml @@ -79,6 +79,12 @@ AND deleted = b'0' + +