Merge branch 'dev-ty1.2' into dev
This commit is contained in:
@@ -100,7 +100,7 @@ public interface WmsServiceFeign {
|
|||||||
|
|
||||||
@ApiOperation("oms取消下发单保存")
|
@ApiOperation("oms取消下发单保存")
|
||||||
@PostMapping(value = "/stockOutOrderApi/cancelExecutionOrder")
|
@PostMapping(value = "/stockOutOrderApi/cancelExecutionOrder")
|
||||||
public void omsCancelExecutionOrder(@RequestBody StockOutOrder stockOutOrder);
|
public AjaxResult omsCancelExecutionOrder(@RequestBody StockOutOrder stockOutOrder);
|
||||||
|
|
||||||
@ApiOperation("oms取消下发单自动分配")
|
@ApiOperation("oms取消下发单自动分配")
|
||||||
@PostMapping(value = "/stockOutOrderApi/omsAutoAssign")
|
@PostMapping(value = "/stockOutOrderApi/omsAutoAssign")
|
||||||
@@ -110,6 +110,14 @@ public interface WmsServiceFeign {
|
|||||||
@PostMapping(value = "/stockOutOrderApi/execution")
|
@PostMapping(value = "/stockOutOrderApi/execution")
|
||||||
public AjaxResult execution(@RequestBody com.mhd.system.api.domain.StockOutOrderDTO stockOutOrderDTO);
|
public AjaxResult execution(@RequestBody com.mhd.system.api.domain.StockOutOrderDTO stockOutOrderDTO);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* oms下发前库存校验:可用库存不足时返回错误,OMS据此在推送前拦截下发,
|
||||||
|
* 避免单据先落库、分配才失败而在WMS残留"已审核"状态的垃圾单
|
||||||
|
*/
|
||||||
|
@ApiOperation("oms下发前库存校验")
|
||||||
|
@PostMapping(value = "/stockOutOrderApi/checkOmsAssignInventory")
|
||||||
|
public AjaxResult checkOmsAssignInventory(@RequestBody List<OutMaterialDetail> outMaterialDetails);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* oms下发单明细保存
|
* oms下发单明细保存
|
||||||
*/
|
*/
|
||||||
|
|||||||
+9
-2
@@ -74,8 +74,9 @@ public class RemoteWmsFallbackFactory implements FallbackFactory<WmsServiceFeign
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void omsCancelExecutionOrder(StockOutOrder stockOutOrder) {
|
public AjaxResult omsCancelExecutionOrder(StockOutOrder stockOutOrder) {
|
||||||
|
//返回错误而非静默成功,让OMS侧补偿清理能感知失败并记录告警
|
||||||
|
return AjaxResult.error("WMS取消下发清理失败:" + cause.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -88,6 +89,12 @@ public class RemoteWmsFallbackFactory implements FallbackFactory<WmsServiceFeign
|
|||||||
return AjaxResult.error(cause.getMessage());
|
return AjaxResult.error(cause.getMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public AjaxResult checkOmsAssignInventory(List<OutMaterialDetail> outMaterialDetails) {
|
||||||
|
//校验接口本身不可用时返回错误,OMS会阻止下发(不能在WMS状态未知时盲目推送)
|
||||||
|
return AjaxResult.error("WMS库存校验服务不可用:" + cause.getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void omsInOrderAddDetail(List<InMaterialDetail> inMaterialDetails) {
|
public void omsInOrderAddDetail(List<InMaterialDetail> inMaterialDetails) {
|
||||||
}
|
}
|
||||||
|
|||||||
+112
@@ -0,0 +1,112 @@
|
|||||||
|
package com.mhd.common.core.jackson;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonFormat;
|
||||||
|
import com.fasterxml.jackson.core.JsonParser;
|
||||||
|
import com.fasterxml.jackson.databind.BeanProperty;
|
||||||
|
import com.fasterxml.jackson.databind.DeserializationContext;
|
||||||
|
import com.fasterxml.jackson.databind.JsonDeserializer;
|
||||||
|
import com.fasterxml.jackson.databind.deser.ContextualDeserializer;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.text.ParsePosition;
|
||||||
|
import java.text.SimpleDateFormat;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 宽松日期反序列化:兼容 yyyy-MM-dd(纯日期)等常见格式。
|
||||||
|
*
|
||||||
|
* 背景:库存查询等接口日期只保留年月日后,前端可能把 "2026-07-06" 这类纯日期回传,
|
||||||
|
* 而实体/DTO 上 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 是严格格式,反序列化直接报
|
||||||
|
* InvalidFormatException。本反序列化器解析顺序:
|
||||||
|
* 1) 字段 @JsonFormat 指定的模式(严格全匹配,保持原行为);
|
||||||
|
* 2) 兜底常见格式:yyyy-MM-dd HH:mm:ss → yyyy-MM-dd HH:mm → yyyy-MM-dd;
|
||||||
|
* 3) 纯数字按毫秒时间戳;
|
||||||
|
* 4) 交给 Jackson 默认解析(ISO-8601 等)。
|
||||||
|
* 仅放宽接受范围,不影响任何既有格式的解析结果;由各服务 DateCompatibilityConfig 显式注册生效。
|
||||||
|
*
|
||||||
|
* @author rcx
|
||||||
|
* @date 2026-09-05
|
||||||
|
*/
|
||||||
|
public class FlexibleDateDeserializer extends JsonDeserializer<Date> implements ContextualDeserializer {
|
||||||
|
|
||||||
|
/** 兜底格式按长度降序排列,避免长文本被短格式部分解析 */
|
||||||
|
private static final String[] FALLBACK_PATTERNS = {
|
||||||
|
"yyyy-MM-dd HH:mm:ss", "yyyy-MM-dd HH:mm", "yyyy-MM-dd"
|
||||||
|
};
|
||||||
|
|
||||||
|
/** 字段 @JsonFormat 指定的模式,可为空 */
|
||||||
|
private final String pattern;
|
||||||
|
|
||||||
|
public FlexibleDateDeserializer() {
|
||||||
|
this(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
public FlexibleDateDeserializer(String pattern) {
|
||||||
|
this.pattern = pattern;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Date deserialize(JsonParser parser, DeserializationContext ctxt) throws IOException {
|
||||||
|
String value = parser.getText();
|
||||||
|
if (value == null || value.trim().isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
value = value.trim();
|
||||||
|
|
||||||
|
// 纯数字按毫秒时间戳(10位秒级时间戳也兼容)
|
||||||
|
if (value.matches("\\d{10,}")) {
|
||||||
|
return new Date(Long.parseLong(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1) 字段 @JsonFormat 指定的模式优先
|
||||||
|
Date parsed = tryParse(value, pattern);
|
||||||
|
// 2) 兜底常见格式
|
||||||
|
if (parsed == null) {
|
||||||
|
for (String fallback : FALLBACK_PATTERNS) {
|
||||||
|
parsed = tryParse(value, fallback);
|
||||||
|
if (parsed != null) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 3) 交给 Jackson 默认解析(ISO-8601 等)
|
||||||
|
if (parsed == null) {
|
||||||
|
try {
|
||||||
|
parsed = ctxt.parseDate(value);
|
||||||
|
} catch (IllegalArgumentException ignore) {
|
||||||
|
// 走不到默认解析时统一在下方抛出友好异常
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (parsed == null) {
|
||||||
|
throw ctxt.weirdStringException(value, Date.class,
|
||||||
|
"支持格式:yyyy-MM-dd HH:mm:ss / yyyy-MM-dd HH:mm / yyyy-MM-dd");
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 严格全匹配解析:解析失败或文本未完整消费(如按 yyyy-MM-dd 解析 "2026-07-06 10:00:00")
|
||||||
|
* 都视为不匹配返回 null,交由下一个格式尝试
|
||||||
|
*/
|
||||||
|
private Date tryParse(String value, String pattern) {
|
||||||
|
if (pattern == null || pattern.isEmpty()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
SimpleDateFormat sdf = new SimpleDateFormat(pattern);
|
||||||
|
sdf.setLenient(false);
|
||||||
|
ParsePosition pos = new ParsePosition(0);
|
||||||
|
Date date = sdf.parse(value, pos);
|
||||||
|
return (date != null && pos.getIndex() == value.length()) ? date : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public JsonDeserializer<?> createContextual(DeserializationContext ctxt, BeanProperty property) {
|
||||||
|
if (property != null) {
|
||||||
|
JsonFormat jsonFormat = property.getAnnotation(JsonFormat.class);
|
||||||
|
if (jsonFormat != null && jsonFormat.pattern() != null && !jsonFormat.pattern().isEmpty()) {
|
||||||
|
return new FlexibleDateDeserializer(jsonFormat.pattern());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.mhd.oms.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.Module;
|
||||||
|
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||||
|
import com.mhd.common.core.jackson.FlexibleDateDeserializer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 日期反序列化兼容配置:Date 字段兼容 yyyy-MM-dd(纯日期)等格式。
|
||||||
|
*
|
||||||
|
* 背景:库存查询日期只保留年月日后,前端回传纯日期(如 createTime=2026-07-06)时,
|
||||||
|
* 实体/DTO 上 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 严格格式反序列化报
|
||||||
|
* InvalidFormatException。注册 FlexibleDateDeserializer 后优先按字段 @JsonFormat
|
||||||
|
* 严格解析,失败时兼容纯日期等常见格式(与 WMS 库存查询一致)。
|
||||||
|
*
|
||||||
|
* Spring Boot 会自动把容器中的 Module Bean 注册进 MVC 使用的 ObjectMapper。
|
||||||
|
*
|
||||||
|
* @author rcx
|
||||||
|
* @date 2026-09-05
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class DateCompatibilityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Module flexibleDateModule() {
|
||||||
|
SimpleModule module = new SimpleModule("oms-flexible-date-module");
|
||||||
|
module.addDeserializer(Date.class, new FlexibleDateDeserializer());
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
}
|
||||||
+73
-16
@@ -480,37 +480,59 @@ public class ExecutionStockOutOrderDomainService {
|
|||||||
* @param stockOutOrderDO
|
* @param stockOutOrderDO
|
||||||
* @return Boolean
|
* @return Boolean
|
||||||
*/
|
*/
|
||||||
@Transactional(rollbackFor = Exception.class)
|
//注意:不能用方法级事务。批量下发时若前一张已推送WMS成功、后一张失败,方法级事务回滚会把已成功单据的
|
||||||
|
//下发状态一并回滚(WMS已分配库存、已生成拣货单,OMS却显示未下发),造成两边状态撕裂。
|
||||||
|
//因此改为:每张单全链路推送成功后立即逐单更新并提交下发状态,失败单据补偿清理后抛错,不影响已成功单据。
|
||||||
public Boolean issueOrder(ExecutionStockOutOrderDO stockOutOrderDO) {
|
public Boolean issueOrder(ExecutionStockOutOrderDO stockOutOrderDO) {
|
||||||
LoginUser loginUser = SecurityUtils.getLoginUser();
|
LoginUser loginUser = SecurityUtils.getLoginUser();
|
||||||
// 获取用户姓名(优先从UserPo获取,如果为空则使用realname,最后使用username作为兜底)
|
// 获取用户姓名(优先从UserPo获取,如果为空则使用realname,最后使用username作为兜底)
|
||||||
String userRealName = loginUser.getUserPo() != null && loginUser.getUserPo().getUserName() != null
|
String userRealName = loginUser.getUserPo() != null && loginUser.getUserPo().getUserName() != null
|
||||||
? loginUser.getUserPo().getUserName()
|
? loginUser.getUserPo().getUserName()
|
||||||
: (loginUser.getRealname() != null ? loginUser.getRealname() : loginUser.getUsername());
|
: (loginUser.getRealname() != null ? loginUser.getRealname() : loginUser.getUsername());
|
||||||
boolean update = stockOutOrderService.update(null, new UpdateWrapper<ExecutionStockOutOrder>().lambda()
|
|
||||||
.set(ExecutionStockOutOrder::getIssueId, loginUser.getUserid())
|
|
||||||
.set(ExecutionStockOutOrder::getIssueName, userRealName)
|
|
||||||
.set(ExecutionStockOutOrder::getIssueTime, new Date())
|
|
||||||
//.set(ExecutionStockOutOrder::getIssueStatus, 1)
|
|
||||||
.set(ExecutionStockOutOrder::getUpdateBy, loginUser.getUserid())
|
|
||||||
.set(ExecutionStockOutOrder::getUpdateByName, userRealName)
|
|
||||||
.set(ExecutionStockOutOrder::getUpdateTime, new Date())
|
|
||||||
.in(ExecutionStockOutOrder::getOutOrderId, stockOutOrderDO.getOutOrderIds()));
|
|
||||||
|
|
||||||
//下发前统一校验所有单据:全部通过后再推送WMS,避免部分单据推送成功、部分失败造成两边数据不一致
|
//下发前统一校验所有单据:先完成全部校验并汇总所有问题一次性提示,全部通过后再推送WMS
|
||||||
List<Long> inOrderIds = stockOutOrderDO.getOutOrderIds();
|
List<Long> inOrderIds = stockOutOrderDO.getOutOrderIds();
|
||||||
List<ExecutionStockOutOrder> issueOrderList = new ArrayList<>();
|
List<ExecutionStockOutOrder> issueOrderList = new ArrayList<>();
|
||||||
|
List<String> validateErrors = new ArrayList<>();
|
||||||
for (Long inOrderId : inOrderIds) {
|
for (Long inOrderId : inOrderIds) {
|
||||||
ExecutionStockOutOrder byId = stockOutOrderService.getById(inOrderId);
|
ExecutionStockOutOrder byId = stockOutOrderService.getById(inOrderId);
|
||||||
if (byId == null) {
|
if (byId == null) {
|
||||||
throw new ServiceException("出库单不存在,outOrderId=" + inOrderId);
|
validateErrors.add("出库单不存在,outOrderId=" + inOrderId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
List<ExecutionOutMaterialDetail> detailList = validateIssueOrderDetail(byId);
|
||||||
|
//下发前调WMS校验可用库存(单内全部库存行校验完才返回结果),问题先收集,最后统一提示,不推送任何数据到WMS
|
||||||
|
List<OutMaterialDetail> checkList = new ArrayList<>();
|
||||||
|
for (ExecutionOutMaterialDetail detail : detailList) {
|
||||||
|
OutMaterialDetail checkDetail = new OutMaterialDetail();
|
||||||
|
checkDetail.setOutOrderNumber(byId.getOutOrderNumber());
|
||||||
|
checkDetail.setMaterialInventoryId(detail.getMaterialInventoryId());
|
||||||
|
checkDetail.setQuantity(detail.getQuantity());
|
||||||
|
checkDetail.setMaterialCode(detail.getMaterialCode());
|
||||||
|
checkDetail.setMaterialName(detail.getMaterialName());
|
||||||
|
checkList.add(checkDetail);
|
||||||
|
}
|
||||||
|
AjaxResult checkResult = wmsServiceFeign.checkOmsAssignInventory(checkList);
|
||||||
|
if (checkResult == null || !"200".equals(String.valueOf(checkResult.get("code")))) {
|
||||||
|
validateErrors.add("出库单[" + byId.getOutOrderNumber() + "]库存校验失败:"
|
||||||
|
+ (checkResult == null ? "WMS服务不可用,已阻止下发" : checkResult.get("msg")));
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
validateIssueOrderDetail(byId);
|
|
||||||
issueOrderList.add(byId);
|
issueOrderList.add(byId);
|
||||||
|
} catch (ServiceException e) {
|
||||||
|
//单张单据校验失败不中断,继续校验其余单据,最后统一提示
|
||||||
|
validateErrors.add(e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!validateErrors.isEmpty()) {
|
||||||
|
throw new ServiceException("下发校验未通过(共" + validateErrors.size() + "项):" + String.join(";", validateErrors));
|
||||||
}
|
}
|
||||||
|
|
||||||
//下发推送到wms
|
//下发推送到wms
|
||||||
for (ExecutionStockOutOrder byId : issueOrderList) {
|
for (ExecutionStockOutOrder byId : issueOrderList) {
|
||||||
|
String currentOrderNumber = byId.getOutOrderNumber();
|
||||||
|
try {
|
||||||
StockOutOrder stockOutOrder = new StockOutOrder();
|
StockOutOrder stockOutOrder = new StockOutOrder();
|
||||||
BeanUtils.copyProperties(byId, stockOutOrder,"outOrderId");
|
BeanUtils.copyProperties(byId, stockOutOrder,"outOrderId");
|
||||||
Long warehouseId = stockOutOrder.getWarehouseId();
|
Long warehouseId = stockOutOrder.getWarehouseId();
|
||||||
@@ -562,17 +584,50 @@ public class ExecutionStockOutOrderDomainService {
|
|||||||
throw new ServiceException("出库单[" + outOrderNumber + "]WMS生成拣货单失败:"
|
throw new ServiceException("出库单[" + outOrderNumber + "]WMS生成拣货单失败:"
|
||||||
+ (genPickingResult == null ? "WMS服务不可用或已降级" : genPickingResult.get("msg")));
|
+ (genPickingResult == null ? "WMS服务不可用或已降级" : genPickingResult.get("msg")));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//该单全链路成功,逐单更新并提交下发信息(后续单据失败不回滚本单,保持与WMS状态一致)
|
||||||
|
boolean updated = stockOutOrderService.update(null, new UpdateWrapper<ExecutionStockOutOrder>().lambda()
|
||||||
|
.set(ExecutionStockOutOrder::getIssueId, loginUser.getUserid())
|
||||||
|
.set(ExecutionStockOutOrder::getIssueName, userRealName)
|
||||||
|
.set(ExecutionStockOutOrder::getIssueTime, new Date())
|
||||||
|
//.set(ExecutionStockOutOrder::getIssueStatus, 1)
|
||||||
|
.set(ExecutionStockOutOrder::getUpdateBy, loginUser.getUserid())
|
||||||
|
.set(ExecutionStockOutOrder::getUpdateByName, userRealName)
|
||||||
|
.set(ExecutionStockOutOrder::getUpdateTime, new Date())
|
||||||
|
.eq(ExecutionStockOutOrder::getOutOrderId, byId.getOutOrderId()));
|
||||||
|
if (!updated) {
|
||||||
|
log.warn("出库单[{}]WMS下发成功,但OMS下发信息更新失败,请检查", currentOrderNumber);
|
||||||
}
|
}
|
||||||
return update;
|
} catch (RuntimeException e) {
|
||||||
|
//补偿清理:推送中途失败时WMS可能已落库单头/明细(甚至已分配冻结库存),调用取消下发清理,
|
||||||
|
//避免残留"已审核"状态的垃圾单;清理失败记error日志提示人工处理,不影响向用户报出原始错误
|
||||||
|
try {
|
||||||
|
StockOutOrder cancelParam = new StockOutOrder();
|
||||||
|
cancelParam.setOutOrderNumber(currentOrderNumber);
|
||||||
|
AjaxResult cancelResult = wmsServiceFeign.omsCancelExecutionOrder(cancelParam);
|
||||||
|
if (cancelResult == null || !"200".equals(String.valueOf(cancelResult.get("code")))) {
|
||||||
|
log.error("出库单[{}]下发失败,且WMS补偿清理失败({}),请手动到WMS删除残留单据",
|
||||||
|
currentOrderNumber, cancelResult == null ? "WMS服务不可用" : cancelResult.get("msg"), e);
|
||||||
|
} else {
|
||||||
|
log.warn("出库单[{}]下发失败,已补偿清理WMS侧单据", currentOrderNumber, e);
|
||||||
|
}
|
||||||
|
} catch (Exception cleanupEx) {
|
||||||
|
log.error("出库单[{}]下发失败,且WMS补偿清理异常,请手动到WMS删除残留单据", currentOrderNumber, cleanupEx);
|
||||||
|
}
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Boolean.TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 建单校验:出库明细数量必须大于0
|
* 建单校验:出库明细数量必须大于0
|
||||||
* 历史缺陷:0数量明细的单下发到WMS后自动分配必然失败,且异常被吞,出库单卡死在"已审核"状态无法生成拣货单
|
* 历史缺陷:0数量明细的单下发到WMS后自动分配必然失败,且异常被吞,出库单卡死在"已审核"状态无法生成拣货单
|
||||||
|
* 注意:明细为空时跳过校验(保持原有"先建单后补明细"的行为),下发时的校验会兜底拦截无明细单据
|
||||||
*/
|
*/
|
||||||
private void validateMaterialDetailQuantity(List<ExecutionOutMaterialDetailDO> materialDetailList) {
|
private void validateMaterialDetailQuantity(List<ExecutionOutMaterialDetailDO> materialDetailList) {
|
||||||
if (CollectionUtils.isEmpty(materialDetailList)) {
|
if (CollectionUtils.isEmpty(materialDetailList)) {
|
||||||
throw new ServiceException("出库单明细不能为空");
|
return;
|
||||||
}
|
}
|
||||||
for (ExecutionOutMaterialDetailDO detail : materialDetailList) {
|
for (ExecutionOutMaterialDetailDO detail : materialDetailList) {
|
||||||
if (detail.getQuantity() == null || detail.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
|
if (detail.getQuantity() == null || detail.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
@@ -587,8 +642,9 @@ public class ExecutionStockOutOrderDomainService {
|
|||||||
* 1.必须有物料明细,且数量大于0;
|
* 1.必须有物料明细,且数量大于0;
|
||||||
* 2.明细必须已关联库存(materialInventoryId),否则WMS按库存ID自动分配会空指针;
|
* 2.明细必须已关联库存(materialInventoryId),否则WMS按库存ID自动分配会空指针;
|
||||||
* 校验放在推送WMS之前,失败直接报错回滚,避免出库单卡死在WMS"已审核"状态
|
* 校验放在推送WMS之前,失败直接报错回滚,避免出库单卡死在WMS"已审核"状态
|
||||||
|
* @return 校验通过的明细列表,供后续WMS库存校验使用
|
||||||
*/
|
*/
|
||||||
private void validateIssueOrderDetail(ExecutionStockOutOrder order) {
|
private List<ExecutionOutMaterialDetail> validateIssueOrderDetail(ExecutionStockOutOrder order) {
|
||||||
String outOrderNumber = order.getOutOrderNumber();
|
String outOrderNumber = order.getOutOrderNumber();
|
||||||
List<ExecutionOutMaterialDetail> detailList = outMaterialDetailService.list(new QueryWrapper<ExecutionOutMaterialDetail>().lambda()
|
List<ExecutionOutMaterialDetail> detailList = outMaterialDetailService.list(new QueryWrapper<ExecutionOutMaterialDetail>().lambda()
|
||||||
.eq(ExecutionOutMaterialDetail::getOutOrderNumber, outOrderNumber));
|
.eq(ExecutionOutMaterialDetail::getOutOrderNumber, outOrderNumber));
|
||||||
@@ -605,6 +661,7 @@ public class ExecutionStockOutOrderDomainService {
|
|||||||
throw new ServiceException("出库单[" + outOrderNumber + "]物料[" + materialDesc + "]未关联库存,无法下发");
|
throw new ServiceException("出库单[" + outOrderNumber + "]物料[" + materialDesc + "]未关联库存,无法下发");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return detailList;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
+41
@@ -696,6 +696,47 @@ public class ReservationStockOutOrderDomainService {
|
|||||||
// 按照 warehouseId 分组
|
// 按照 warehouseId 分组
|
||||||
Map<Long, List<ReservationOutMaterialDetail>> materialDetailGroupByWarehouse = materialDetailList.stream()
|
Map<Long, List<ReservationOutMaterialDetail>> materialDetailGroupByWarehouse = materialDetailList.stream()
|
||||||
.collect(Collectors.groupingBy(ReservationOutMaterialDetail::getWarehouseId));
|
.collect(Collectors.groupingBy(ReservationOutMaterialDetail::getWarehouseId));
|
||||||
|
|
||||||
|
//下发前统一校验各仓明细(数量>0、已关联库存、WMS可用库存充足),全部通过后再逐仓生成执行单下发:
|
||||||
|
//本方法的事务回滚不了WMS已提交的数据,若后面的仓在生成执行单后才失败,前面的仓会在WMS残留单据
|
||||||
|
List<String> issueValidateErrors = new ArrayList<>();
|
||||||
|
materialDetailGroupByWarehouse.forEach((warehouseId, details) -> {
|
||||||
|
boolean groupBaseValid = true;
|
||||||
|
for (ReservationOutMaterialDetail detail : details) {
|
||||||
|
String materialDesc = detail.getMaterialCode() != null ? detail.getMaterialCode()
|
||||||
|
: (detail.getMaterialName() != null ? detail.getMaterialName() : String.valueOf(detail.getMaterialDetailId()));
|
||||||
|
if (detail.getQuantity() == null || detail.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
|
||||||
|
issueValidateErrors.add("预约单[" + inOrderNumber + "]物料[" + materialDesc + "]数量为0,无法下发");
|
||||||
|
groupBaseValid = false;
|
||||||
|
} else if (detail.getMaterialInventoryId() == null || detail.getMaterialInventoryId() <= 0) {
|
||||||
|
issueValidateErrors.add("预约单[" + inOrderNumber + "]物料[" + materialDesc + "]未关联库存,无法下发");
|
||||||
|
groupBaseValid = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!groupBaseValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//WMS库存校验(接口内部会聚合该仓全部明细的需求量后比对可用库存)
|
||||||
|
List<OutMaterialDetail> checkList = new ArrayList<>();
|
||||||
|
for (ReservationOutMaterialDetail detail : details) {
|
||||||
|
OutMaterialDetail checkDetail = new OutMaterialDetail();
|
||||||
|
checkDetail.setMaterialInventoryId(detail.getMaterialInventoryId());
|
||||||
|
checkDetail.setQuantity(detail.getQuantity());
|
||||||
|
checkDetail.setMaterialCode(detail.getMaterialCode());
|
||||||
|
checkDetail.setMaterialName(detail.getMaterialName());
|
||||||
|
checkList.add(checkDetail);
|
||||||
|
}
|
||||||
|
AjaxResult checkResult = wmsServiceFeign.checkOmsAssignInventory(checkList);
|
||||||
|
if (checkResult == null || !"200".equals(String.valueOf(checkResult.get("code")))) {
|
||||||
|
String groupWarehouseName = reservationStockInOrderMapper.getWarehouseName(warehouseId);
|
||||||
|
issueValidateErrors.add("仓库[" + (groupWarehouseName != null ? groupWarehouseName : warehouseId) + "]库存校验失败:"
|
||||||
|
+ (checkResult == null ? "WMS服务不可用,已阻止下发" : checkResult.get("msg")));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (!issueValidateErrors.isEmpty()) {
|
||||||
|
throw new ServiceException("下发校验未通过(共" + issueValidateErrors.size() + "项):" + String.join(";", issueValidateErrors));
|
||||||
|
}
|
||||||
|
|
||||||
List<String> executionInOrderNumbers = new ArrayList<>();
|
List<String> executionInOrderNumbers = new ArrayList<>();
|
||||||
materialDetailGroupByWarehouse.forEach((warehouseId, details) -> {
|
materialDetailGroupByWarehouse.forEach((warehouseId, details) -> {
|
||||||
if (warehouseId == null) warehouseId = warehouseId1;
|
if (warehouseId == null) warehouseId = warehouseId1;
|
||||||
|
|||||||
+92
@@ -1176,6 +1176,98 @@ public class StockOutOrderApplicationService {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OMS下发前库存校验:按库存ID聚合各明细计划数量,逐一比对WMS可用库存,
|
||||||
|
* 全部比对完成后汇总所有不足项一次性抛错。用于OMS在下发推送前拦截
|
||||||
|
* (此前单据先落库、分配才失败,会在WMS残留"已审核"状态的垃圾单)
|
||||||
|
*/
|
||||||
|
public void checkOmsAssignInventory(List<OutMaterialDetail> outMaterialDetails) {
|
||||||
|
if (CollectionUtils.isEmpty(outMaterialDetails)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
//重复下发保护:该单号在WMS已存在活动单据时直接拦截(继续下发会删改已分配明细、
|
||||||
|
//失败补偿还会误删正在拣货的单和拣货单)。历史残留单据需先在OMS取消下发后再重试
|
||||||
|
String outOrderNumber = outMaterialDetails.get(0).getOutOrderNumber();
|
||||||
|
if (outOrderNumber != null && !outOrderNumber.isEmpty()) {
|
||||||
|
StockOutOrder existOrder = stockOutOrderService.getOne(new QueryWrapper<StockOutOrder>().lambda()
|
||||||
|
.eq(StockOutOrder::getOutOrderNumber, outOrderNumber)
|
||||||
|
.eq(StockOutOrder::getDelFlag, 1), false);
|
||||||
|
if (existOrder != null) {
|
||||||
|
throw new ServiceException("WMS已存在该出库单(当前状态:" + stockOutOrderStatusName(existOrder.getStatus())
|
||||||
|
+ "),请勿重复下发;若为历史残留单据,请先取消下发后重试");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
//同一库存被多条明细引用时聚合需求量再比较(单条比会漏判)
|
||||||
|
Map<Long, BigDecimal> needByInventoryId = new LinkedHashMap<>();
|
||||||
|
Map<Long, String> materialDescByInventoryId = new HashMap<>();
|
||||||
|
for (OutMaterialDetail detail : outMaterialDetails) {
|
||||||
|
if (detail.getMaterialInventoryId() == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BigDecimal need = detail.getQuantity() == null ? BigDecimal.ZERO : detail.getQuantity();
|
||||||
|
needByInventoryId.merge(detail.getMaterialInventoryId(), need, BigDecimal::add);
|
||||||
|
materialDescByInventoryId.putIfAbsent(detail.getMaterialInventoryId(), buildMaterialDesc(detail));
|
||||||
|
}
|
||||||
|
List<String> errors = new ArrayList<>();
|
||||||
|
for (Map.Entry<Long, BigDecimal> entry : needByInventoryId.entrySet()) {
|
||||||
|
Long materialInventoryId = entry.getKey();
|
||||||
|
BigDecimal need = entry.getValue();
|
||||||
|
String materialDesc = materialDescByInventoryId.getOrDefault(materialInventoryId, "库存ID=" + materialInventoryId);
|
||||||
|
if (materialInventoryId <= 0) {
|
||||||
|
errors.add("物料[" + materialDesc + "]未关联库存,合计需求数量:" + need.stripTrailingZeros().toPlainString());
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
MaterialInventory materialInventory = materialInventoryMapper.selectById(materialInventoryId);
|
||||||
|
if (materialInventory == null) {
|
||||||
|
errors.add("物料[" + materialDesc + "]库存不存在,库存ID=" + materialInventoryId);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
BigDecimal available = materialInventory.getAllocationQuantity() == null
|
||||||
|
? BigDecimal.ZERO : materialInventory.getAllocationQuantity();
|
||||||
|
if (available.compareTo(need) < 0) {
|
||||||
|
errors.add(String.format("物料[%s]可用库存不足:需要%s,当前可用%s",
|
||||||
|
materialDesc, need.stripTrailingZeros().toPlainString(), available.stripTrailingZeros().toPlainString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!errors.isEmpty()) {
|
||||||
|
throw new ServiceException("下发前库存校验未通过(共" + errors.size() + "项):" + String.join(";", errors));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String buildMaterialDesc(OutMaterialDetail detail) {
|
||||||
|
if (detail.getMaterialCode() != null) {
|
||||||
|
return detail.getMaterialCode() + (detail.getMaterialName() != null ? "-" + detail.getMaterialName() : "");
|
||||||
|
}
|
||||||
|
if (detail.getMaterialName() != null) {
|
||||||
|
return detail.getMaterialName();
|
||||||
|
}
|
||||||
|
return "未知物料";
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 出库单状态码转名称(用于校验提示),未知状态码原样返回
|
||||||
|
*/
|
||||||
|
private String stockOutOrderStatusName(Integer status) {
|
||||||
|
if (status == null) {
|
||||||
|
return "未知";
|
||||||
|
}
|
||||||
|
switch (status) {
|
||||||
|
case 1: return "已创建";
|
||||||
|
case 2: return "已审核";
|
||||||
|
case 3: return "部分分配";
|
||||||
|
case 4: return "已分配";
|
||||||
|
case 5: return "波次中";
|
||||||
|
case 6: return "拣货中";
|
||||||
|
case 7: return "拣货完成";
|
||||||
|
case 8: return "复核中";
|
||||||
|
case 9: return "已出库";
|
||||||
|
case 10: return "取消";
|
||||||
|
case 11: return "关闭";
|
||||||
|
case 12: return "已复核";
|
||||||
|
default: return "状态码" + status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*public Boolean autoAssign(StockOutOrderDO stockOutOrderDO){
|
/*public Boolean autoAssign(StockOutOrderDO stockOutOrderDO){
|
||||||
StockOutOrderDO assembleStockOutOrderDO = assembleParamByOutOrder(stockOutOrderDO);
|
StockOutOrderDO assembleStockOutOrderDO = assembleParamByOutOrder(stockOutOrderDO);
|
||||||
return stockOutOrderDomainService.autoAssign(assembleStockOutOrderDO);
|
return stockOutOrderDomainService.autoAssign(assembleStockOutOrderDO);
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package com.mhd.wms.config;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.Module;
|
||||||
|
import com.fasterxml.jackson.databind.module.SimpleModule;
|
||||||
|
import com.mhd.common.core.jackson.FlexibleDateDeserializer;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JSON 日期反序列化兼容配置:Date 字段兼容 yyyy-MM-dd(纯日期)等格式。
|
||||||
|
*
|
||||||
|
* 背景:库存查询日期只保留年月日后,前端回传纯日期(如 createTime=2026-07-06)时,
|
||||||
|
* 实体/DTO 上 @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") 严格格式反序列化报
|
||||||
|
* InvalidFormatException。注册 FlexibleDateDeserializer 后优先按字段 @JsonFormat
|
||||||
|
* 严格解析,失败时兼容纯日期等常见格式(与 OMS 库存查询一致)。
|
||||||
|
*
|
||||||
|
* Spring Boot 会自动把容器中的 Module Bean 注册进 MVC 使用的 ObjectMapper。
|
||||||
|
*
|
||||||
|
* @author rcx
|
||||||
|
* @date 2026-09-05
|
||||||
|
*/
|
||||||
|
@Configuration
|
||||||
|
public class DateCompatibilityConfig {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Module flexibleDateModule() {
|
||||||
|
SimpleModule module = new SimpleModule("wms-flexible-date-module");
|
||||||
|
module.addDeserializer(Date.class, new FlexibleDateDeserializer());
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
}
|
||||||
+21
-9
@@ -2558,6 +2558,7 @@ public class StockOutOrderDomainService {
|
|||||||
stockOutOrderService.save(stockOutOrder);
|
stockOutOrderService.save(stockOutOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void cancelExecutionOrder(StockOutOrder stockOutOrder) {
|
public void cancelExecutionOrder(StockOutOrder stockOutOrder) {
|
||||||
String outOrderNumber = stockOutOrder.getOutOrderNumber();
|
String outOrderNumber = stockOutOrder.getOutOrderNumber();
|
||||||
StockOutOrder stockOutOrderDB = stockOutOrderService.getOne(new LambdaQueryWrapper<StockOutOrder>()
|
StockOutOrder stockOutOrderDB = stockOutOrderService.getOne(new LambdaQueryWrapper<StockOutOrder>()
|
||||||
@@ -2566,23 +2567,33 @@ public class StockOutOrderDomainService {
|
|||||||
if (stockOutOrderDB == null) {
|
if (stockOutOrderDB == null) {
|
||||||
throw new ServiceException("出库单不存在");
|
throw new ServiceException("出库单不存在");
|
||||||
}
|
}
|
||||||
|
//逻辑删除配送明细(放在最前:所有清理路径——含下方无明细提前返回——统一先清配送明细,避免残留)
|
||||||
|
deliveryDetailsLinkService.update(new UpdateWrapper<com.mhd.wms.domain.deliveryDetailsLink.entity.DeliveryDetailsLink>().lambda()
|
||||||
|
.set(com.mhd.wms.domain.deliveryDetailsLink.entity.DeliveryDetailsLink::getDelFlag, 2)
|
||||||
|
.eq(com.mhd.wms.domain.deliveryDetailsLink.entity.DeliveryDetailsLink::getOrderNumber, outOrderNumber)
|
||||||
|
.eq(com.mhd.wms.domain.deliveryDetailsLink.entity.DeliveryDetailsLink::getDelFlag, 1));
|
||||||
List<OutMaterialDetail> materialDetails = outMaterialDetailService.list(new LambdaQueryWrapper<OutMaterialDetail>()
|
List<OutMaterialDetail> materialDetails = outMaterialDetailService.list(new LambdaQueryWrapper<OutMaterialDetail>()
|
||||||
.eq(OutMaterialDetail::getOutOrderNumber, outOrderNumber)
|
.eq(OutMaterialDetail::getOutOrderNumber, outOrderNumber)
|
||||||
.eq(OutMaterialDetail::getDelFlag, 1));
|
.eq(OutMaterialDetail::getDelFlag, 1));
|
||||||
if (CollectionUtils.isEmpty(materialDetails)) {
|
if (CollectionUtils.isEmpty(materialDetails)) {
|
||||||
throw new ServiceException("出库单明细不存在");
|
//无明细(下发中途失败的补偿清理场景):没有可还原的库存,直接逻辑删除单头即可
|
||||||
|
stockOutOrderDB.setDelFlag(2);
|
||||||
|
stockOutOrderService.updateById(stockOutOrderDB);
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
for (OutMaterialDetail materialDetail : materialDetails) {
|
for (OutMaterialDetail materialDetail : materialDetails) {
|
||||||
//还原库存
|
//还原库存
|
||||||
BigDecimal allocationQuantity = materialDetail.getAllocationQuantity();
|
BigDecimal allocationQuantity = materialDetail.getAllocationQuantity();
|
||||||
Long materialInventoryId = materialDetail.getMaterialInventoryId();
|
Long materialInventoryId = materialDetail.getMaterialInventoryId();
|
||||||
MaterialInventory materialInventory = materialInventoryMapper.selectById(materialInventoryId);
|
MaterialInventory materialInventory = materialInventoryMapper.selectById(materialInventoryId);
|
||||||
if (materialInventory == null) {
|
//以下两种情况都无可还原的冻结量,跳过还原直接删单,保证此类单据也能被取消/补偿清理:
|
||||||
throw new ServiceException("库存信息不存在");
|
//1.库存不存在(如OMS漏传库存ID的历史脏单);
|
||||||
}
|
//2.未分配的明细(allocationQuantity为空或0,如下发失败后的补偿清理)
|
||||||
materialInventory.setAllocationQuantity(materialInventory.getAllocationQuantity().add(allocationQuantity));
|
if (materialInventory != null && allocationQuantity != null && allocationQuantity.compareTo(BigDecimal.ZERO) > 0) {
|
||||||
materialInventory.setFreezeQuantity(materialInventory.getFreezeQuantity().subtract(allocationQuantity));
|
materialInventory.setAllocationQuantity((materialInventory.getAllocationQuantity() == null ? BigDecimal.ZERO : materialInventory.getAllocationQuantity()).add(allocationQuantity));
|
||||||
|
materialInventory.setFreezeQuantity((materialInventory.getFreezeQuantity() == null ? BigDecimal.ZERO : materialInventory.getFreezeQuantity()).subtract(allocationQuantity));
|
||||||
materialInventoryMapper.updateById(materialInventory);
|
materialInventoryMapper.updateById(materialInventory);
|
||||||
|
}
|
||||||
//逻辑删除库存调整记录
|
//逻辑删除库存调整记录
|
||||||
Long uniqueId = materialDetail.getUniqueId();
|
Long uniqueId = materialDetail.getUniqueId();
|
||||||
String relateNo = outOrderNumber+"#"+uniqueId;
|
String relateNo = outOrderNumber+"#"+uniqueId;
|
||||||
@@ -2593,9 +2604,9 @@ public class StockOutOrderDomainService {
|
|||||||
materialDetail.setDelFlag(2);
|
materialDetail.setDelFlag(2);
|
||||||
outMaterialDetailService.updateById(materialDetail);
|
outMaterialDetailService.updateById(materialDetail);
|
||||||
}
|
}
|
||||||
//逻辑删除出库单
|
//逻辑删除出库单(用按单号查出的DB对象更新:OMS补偿清理只传单号,传入对象无主键,直接updateById不生效)
|
||||||
stockOutOrder.setDelFlag(2);
|
stockOutOrderDB.setDelFlag(2);
|
||||||
stockOutOrderService.updateById(stockOutOrder);
|
stockOutOrderService.updateById(stockOutOrderDB);
|
||||||
|
|
||||||
PickingOrder pickingOrder = pickingOrderService.getOne(new LambdaQueryWrapper<PickingOrder>()
|
PickingOrder pickingOrder = pickingOrderService.getOne(new LambdaQueryWrapper<PickingOrder>()
|
||||||
.eq(PickingOrder::getDelFlag, 1)
|
.eq(PickingOrder::getDelFlag, 1)
|
||||||
@@ -2609,6 +2620,7 @@ public class StockOutOrderDomainService {
|
|||||||
pickingMaterialDetail.setDelFlag(2);
|
pickingMaterialDetail.setDelFlag(2);
|
||||||
pickingMaterialDetailService.updateById(pickingMaterialDetail);
|
pickingMaterialDetailService.updateById(pickingMaterialDetail);
|
||||||
}
|
}
|
||||||
|
pickingOrder.setDelFlag(2);
|
||||||
pickingOrderService.updateById(pickingOrder);
|
pickingOrderService.updateById(pickingOrder);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+12
-1
@@ -544,10 +544,21 @@ public class StockOutOrderApi extends BaseController {
|
|||||||
stockOutOrderApplicationService.omsOrderAddDetail(outMaterialDetails);
|
stockOutOrderApplicationService.omsOrderAddDetail(outMaterialDetails);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* oms下发前库存校验:不足时返回错误,OMS据此拦截下发,避免单据落库后分配失败残留垃圾单
|
||||||
|
*/
|
||||||
|
@ApiOperation("oms下发前库存校验")
|
||||||
|
@PostMapping(value = "/checkOmsAssignInventory")
|
||||||
|
public AjaxResult checkOmsAssignInventory(@RequestBody List<OutMaterialDetail> outMaterialDetails) {
|
||||||
|
stockOutOrderApplicationService.checkOmsAssignInventory(outMaterialDetails);
|
||||||
|
return AjaxResult.success("库存校验通过");
|
||||||
|
}
|
||||||
|
|
||||||
@ApiOperation("oms下发单保存")
|
@ApiOperation("oms下发单保存")
|
||||||
@PostMapping(value = "/cancelExecutionOrder")
|
@PostMapping(value = "/cancelExecutionOrder")
|
||||||
public void omsCancelExecutionOrder(@RequestBody StockOutOrder stockOutOrder) {
|
public AjaxResult omsCancelExecutionOrder(@RequestBody StockOutOrder stockOutOrder) {
|
||||||
stockOutOrderApplicationService.cancelExecutionOrder(stockOutOrder);
|
stockOutOrderApplicationService.cancelExecutionOrder(stockOutOrder);
|
||||||
|
return AjaxResult.success();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user