Merge branch 'dev-ty1.2' into dev

This commit is contained in:
rcx
2026-09-05 13:53:01 +08:00
10 changed files with 436 additions and 30 deletions
@@ -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;
}
}
@@ -480,37 +480,59 @@ public class ExecutionStockOutOrderDomainService {
* @param stockOutOrderDO
* @return Boolean
*/
@Transactional(rollbackFor = Exception.class)
//注意:不能用方法级事务。批量下发时若前一张已推送WMS成功、后一张失败,方法级事务回滚会把已成功单据的
//下发状态一并回滚(WMS已分配库存、已生成拣货单,OMS却显示未下发),造成两边状态撕裂。
//因此改为:每张单全链路推送成功后立即逐单更新并提交下发状态,失败单据补偿清理后抛错,不影响已成功单据。
public Boolean issueOrder(ExecutionStockOutOrderDO stockOutOrderDO) {
LoginUser loginUser = SecurityUtils.getLoginUser();
// 获取用户姓名(优先从UserPo获取,如果为空则使用realname,最后使用username作为兜底)
String userRealName = loginUser.getUserPo() != null && loginUser.getUserPo().getUserName() != null
? loginUser.getUserPo().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<ExecutionStockOutOrder> issueOrderList = new ArrayList<>();
List<String> validateErrors = new ArrayList<>();
for (Long inOrderId : inOrderIds) {
ExecutionStockOutOrder byId = stockOutOrderService.getById(inOrderId);
if (byId == null) {
throw new ServiceException("出库单不存在,outOrderId=" + inOrderId);
validateErrors.add("出库单不存在,outOrderId=" + inOrderId);
continue;
}
validateIssueOrderDetail(byId);
issueOrderList.add(byId);
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;
}
issueOrderList.add(byId);
} catch (ServiceException e) {
//单张单据校验失败不中断,继续校验其余单据,最后统一提示
validateErrors.add(e.getMessage());
}
}
if (!validateErrors.isEmpty()) {
throw new ServiceException("下发校验未通过(共" + validateErrors.size() + "项):" + String.join("", validateErrors));
}
//下发推送到wms
for (ExecutionStockOutOrder byId : issueOrderList) {
String currentOrderNumber = byId.getOutOrderNumber();
try {
StockOutOrder stockOutOrder = new StockOutOrder();
BeanUtils.copyProperties(byId, stockOutOrder,"outOrderId");
Long warehouseId = stockOutOrder.getWarehouseId();
@@ -562,17 +584,50 @@ public class ExecutionStockOutOrderDomainService {
throw new ServiceException("出库单[" + outOrderNumber + "]WMS生成拣货单失败:"
+ (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);
}
} 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 update;
return Boolean.TRUE;
}
/**
* 建单校验:出库明细数量必须大于0
* 历史缺陷:0数量明细的单下发到WMS后自动分配必然失败,且异常被吞,出库单卡死在"已审核"状态无法生成拣货单
* 注意:明细为空时跳过校验(保持原有"先建单后补明细"的行为),下发时的校验会兜底拦截无明细单据
*/
private void validateMaterialDetailQuantity(List<ExecutionOutMaterialDetailDO> materialDetailList) {
if (CollectionUtils.isEmpty(materialDetailList)) {
throw new ServiceException("出库单明细不能为空");
return;
}
for (ExecutionOutMaterialDetailDO detail : materialDetailList) {
if (detail.getQuantity() == null || detail.getQuantity().compareTo(BigDecimal.ZERO) <= 0) {
@@ -587,8 +642,9 @@ public class ExecutionStockOutOrderDomainService {
* 1.必须有物料明细,且数量大于0;
* 2.明细必须已关联库存(materialInventoryId),否则WMS按库存ID自动分配会空指针;
* 校验放在推送WMS之前,失败直接报错回滚,避免出库单卡死在WMS"已审核"状态
* @return 校验通过的明细列表,供后续WMS库存校验使用
*/
private void validateIssueOrderDetail(ExecutionStockOutOrder order) {
private List<ExecutionOutMaterialDetail> validateIssueOrderDetail(ExecutionStockOutOrder order) {
String outOrderNumber = order.getOutOrderNumber();
List<ExecutionOutMaterialDetail> detailList = outMaterialDetailService.list(new QueryWrapper<ExecutionOutMaterialDetail>().lambda()
.eq(ExecutionOutMaterialDetail::getOutOrderNumber, outOrderNumber));
@@ -605,6 +661,7 @@ public class ExecutionStockOutOrderDomainService {
throw new ServiceException("出库单[" + outOrderNumber + "]物料[" + materialDesc + "]未关联库存,无法下发");
}
}
return detailList;
}
/**
@@ -696,6 +696,47 @@ public class ReservationStockOutOrderDomainService {
// 按照 warehouseId 分组
Map<Long, List<ReservationOutMaterialDetail>> materialDetailGroupByWarehouse = materialDetailList.stream()
.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<>();
materialDetailGroupByWarehouse.forEach((warehouseId, details) -> {
if (warehouseId == null) warehouseId = warehouseId1;