diff --git a/src/main/java/com/gaotao/modules/warehouse/entity/CrossSiteHuTransferResult.java b/src/main/java/com/gaotao/modules/warehouse/entity/CrossSiteHuTransferResult.java index 68298471..cc156203 100644 --- a/src/main/java/com/gaotao/modules/warehouse/entity/CrossSiteHuTransferResult.java +++ b/src/main/java/com/gaotao/modules/warehouse/entity/CrossSiteHuTransferResult.java @@ -24,7 +24,7 @@ public class CrossSiteHuTransferResult { private int successCount; /** - * 失败数量,来源冲突校验或更新影响行数为0的标签数 - rqrq + * 失败或未执行标签数量,来源逐组IFS失败、本地写入失败及后续中断;不落库,供页面汇总。- rqrq */ private int failCount; @@ -38,19 +38,22 @@ public class CrossSiteHuTransferResult { */ private List failedReasons = new ArrayList<>(); + /** IFS已成功但本地写入失败时为true;来源为分组事务结果,不落库,页面须提示人工对账。- rqrq */ + private boolean reconciliationRequired; + /** * 处理结果摘要,前端结果弹窗标题补充说明 - rqrq */ private String message; /** - * 出库事务号,来源登录大site的 transNoControl(STO),写入 trans_header.trans_no;前端结果弹窗回显 - rqrq + * 成功组出库事务号,多个组用逗号连接;来源登录大site的transNoControl(STO),前端回显。- rqrq *

是否落库:是,对应 STO 出库单(site=来源小site);赋值时机:校验通过后生成;消费方:前端结果展示、后续调拨接口

*/ private String stoTransNo; /** - * 入库事务号,来源登录大site的 transNoControl(STI),写入 trans_header.trans_no;前端结果弹窗回显 - rqrq + * 成功组入库事务号,多个组用逗号连接;来源登录大site的transNoControl(STI),前端回显。- rqrq *

是否落库:是,对应 STI 入库单(site=目标小site);赋值时机:校验通过后生成;消费方:前端结果展示、后续调拨接口

*/ private String stiTransNo; diff --git a/src/main/java/com/gaotao/modules/warehouse/service/impl/CrossSiteHuTransferServiceImpl.java b/src/main/java/com/gaotao/modules/warehouse/service/impl/CrossSiteHuTransferServiceImpl.java index f29dd830..5a6d4670 100644 --- a/src/main/java/com/gaotao/modules/warehouse/service/impl/CrossSiteHuTransferServiceImpl.java +++ b/src/main/java/com/gaotao/modules/warehouse/service/impl/CrossSiteHuTransferServiceImpl.java @@ -32,14 +32,18 @@ import org.apache.shiro.SecurityUtils; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.transaction.PlatformTransactionManager; +import org.springframework.transaction.TransactionDefinition; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import java.math.BigDecimal; +import java.text.SimpleDateFormat; import java.time.LocalDate; import java.time.ZoneId; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.LinkedHashMap; @@ -47,11 +51,12 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.stream.Collectors; /** * @author rqrq - * @Description 跨小site调拨实现:提交时写STO/STI、改标签site和海安sub_site,再按目标合同版本回写标签工程版本 - rqrq + * @Description 跨小site调拨:按库存主键逐组先查版本并执行IFS移库,成功后写本地单据、标签和托盘明细 - rqrq * @date 2026/09/04 */ @Slf4j @@ -76,6 +81,9 @@ public class CrossSiteHuTransferServiceImpl implements CrossSiteHuTransferServic @Autowired private SysUserService sysUserService; + @Autowired + private PlatformTransactionManager transactionManager; + @Value("${custom.ifs-url}") private String ifsUrl; @@ -149,134 +157,207 @@ public class CrossSiteHuTransferServiceImpl implements CrossSiteHuTransferServic } /** - * @author rqrq - * @Description 跨小site调拨主流程:二次校验 -> 写STO/STI -> 改标签site -> 查询并回写目标版本;调拨外部接口仍留空 - rqrq - * - *

事务边界:单据写入与标签site改写同一事务;任一校验或写入失败整批回滚,避免只出库不改site。

- *

出入库记录site使用小site:STO.site=fromSite,STI.site=toSite。

- * - * @param request 登录大site、来源/目标小site、操作人、选中标签 - * @return 成功数量与STO/STI事务号 + * 按库存主键分组调拨:逐组查目标版本,事务内先写WMS再调用IFS,成功才提交。- rqrq + * 本地或IFS明确失败回滚当前组并继续;IFS结果不明或成功后提交失败则停止并人工对账。- rqrq + * @param request 大site、来源/目标小site、操作人与选中标签;空值或越权直接失败 - rqrq + * @return 每组成功单号、成功/失败标签和失败原因 - rqrq */ @Override - @Transactional(rollbackFor = Exception.class) public CrossSiteHuTransferResult transferHuSite(CrossSiteHuTransferRequest request) { - // 先校验登录大site、来源/目标归属和整批同一来源;失败直接抛错,不写任何单据 - rqrq validateTransferRequest(request); - - // 从选中行提取去重条码号,空条码会失败,避免后续IN条件无值 - rqrq List unitIds = collectUnitIds(request.getSelectedItems()); - log.info("开始跨小site调拨 - rqrq,loginSite={}, fromSite={}, toSite={}, count={}", - request.getSite(), request.getFromSite(), request.getToSite(), unitIds.size()); - - // 加锁读取来源site下选中标签,入参fromSite+unitIds;失败或漏行则中断,避免按过期列表写单 - rqrq + // 先校验整批仍在来源site且在库未预定;每组IFS成功后还会加锁复核,防止期间并发改动。- rqrq List handlingUnits = crossSiteHuTransferMapper.listTransferHandlingUnits(request.getFromSite(), unitIds); validateHandlingUnitStatus(unitIds, handlingUnits); - - // 目标site已存在同号标签时禁止改联合主键,避免唯一约束冲突导致半成品单据 - rqrq - List conflictUnitIds = crossSiteHuTransferMapper.listConflictUnitIds(request.getToSite(), unitIds); - if (!CollectionUtils.isEmpty(conflictUnitIds)) { - throw new RuntimeException("目标site已存在相同条码,无法调拨:" + String.join(",", conflictUnitIds)); - } - - // 按操作人查用户档案,写入出入库单 userId/userName;查不到则仅用请求中的createdBy,不阻断调拨 - rqrq - SysUserEntity operatorUser = null; - if (StringUtils.hasText(request.getCreatedBy())) { - operatorUser = sysUserService.queryByUserName(request.getCreatedBy()); - } - String userId = operatorUser != null && operatorUser.getUserId() != null - ? operatorUser.getUserId().toString() : ""; - String userName = operatorUser != null && StringUtils.hasText(operatorUser.getUsername()) - ? operatorUser.getUsername() : request.getCreatedBy(); - - // 流水号按登录大site取号(trans_no_control通常按大site维护);单据site仍写小site。取号失败则无法写单,整批回滚 - rqrq - TransNoControl stoTransData = transNoControlService.getTransNo(request.getSite(), "STO", 10); - if (stoTransData == null || !StringUtils.hasText(stoTransData.getNewTransNo())) { - throw new RuntimeException("生成出库事务号失败,请检查当前工厂的STO流水配置:" + request.getSite()); - } - String stoTransNo = stoTransData.getNewTransNo(); - - // STI流水同样按登录大site取号;失败时STO号虽已占用(独立事务)但本事务回滚业务单据 - rqrq - TransNoControl stiTransData = transNoControlService.getTransNo(request.getSite(), "STI", 10); - if (stiTransData == null || !StringUtils.hasText(stiTransData.getNewTransNo())) { - throw new RuntimeException("生成入库事务号失败,请检查当前工厂的STI流水配置:" + request.getSite()); - } - String stiTransNo = stiTransData.getNewTransNo(); - - String warehouseId = handlingUnits.get(0).getWarehouseId(); - - // 先写出库头:site=来源小site,orderRef1互指入库单号,便于后续对账 - rqrq - TransHeader stoHeader = buildTransHeader(request.getFromSite(), stoTransNo, "STO", - warehouseId, stiTransNo, "跨小site调拨出库 " + request.getFromSite() + " -> " + request.getToSite(), - userId, userName); - transHeaderService.save(stoHeader); - - // 再写入库头:site=目标小site,orderRef1互指出库单号 - rqrq - TransHeader stiHeader = buildTransHeader(request.getToSite(), stiTransNo, "STI", - warehouseId, stoTransNo, "跨小site调拨入库 " + request.getFromSite() + " -> " + request.getToSite(), - userId, userName); - transHeaderService.save(stiHeader); - - // 按物料+批次+库位+WDR+工程变更级别汇总明细,减少单据行数;库位不改,仅site变化 - rqrq - Map> groupedHUs = handlingUnits.stream() - .collect(Collectors.groupingBy(hu -> - nvl(hu.getPartNo()) + "|" + nvl(hu.getBatchNo()) + "|" + - nvl(hu.getLocationId()) + "|" + nvl(hu.getWdr()) + "|" + - (StringUtils.hasText(hu.getEngChgLevel()) ? hu.getEngChgLevel() : "1"))); - - int itemNo = 1; - for (Map.Entry> entry : groupedHUs.entrySet()) { - List groupHUs = entry.getValue(); - HandlingUnit firstHU = groupHUs.get(0); - BigDecimal totalQty = groupHUs.stream() - .map(hu -> hu.getQty() == null ? BigDecimal.ZERO : hu.getQty()) - .reduce(BigDecimal.ZERO, BigDecimal::add); - - // 写出库明细:site=来源小site,direction=-,库位仍为标签当前库位 - rqrq - transDetailService.save(buildTransDetail(request.getFromSite(), stoTransNo, itemNo, firstHU, - totalQty, "-", firstHU.getLocationId())); - // 写入库明细:site=目标小site,direction=+,库位不变(本次只改site) - rqrq - transDetailService.save(buildTransDetail(request.getToSite(), stiTransNo, itemNo, firstHU, - totalQty, "+", firstHU.getLocationId())); - - int seqNo = 1; - for (HandlingUnit hu : groupHUs) { - // 出库子明细按标签落 subNo=unitId,site用来源小site - rqrq - transDetailSubService.save(buildTransDetailSub(request.getFromSite(), stoTransNo, (double) itemNo, - seqNo, hu, "-")); - // 入库子明细同样按标签落行,site用目标小site - rqrq - transDetailSubService.save(buildTransDetailSub(request.getToSite(), stiTransNo, (double) itemNo, - seqNo, hu, "+")); - seqNo++; + List conflicts = crossSiteHuTransferMapper.listConflictUnitIds(request.getToSite(), unitIds); + if (!CollectionUtils.isEmpty(conflicts)) { + throw new XJException("目标site已存在相同条码,无法调拨:" + String.join(",", conflicts)); + } + // 单据操作人取用户档案;未查到时沿用请求用户名,不影响IFS入参。- rqrq + SysUserEntity operator = sysUserService.queryByUserName(request.getCreatedBy()); + String userId = operator != null && operator.getUserId() != null ? operator.getUserId().toString() : ""; + String userName = operator != null && StringUtils.hasText(operator.getUsername()) + ? operator.getUsername() : request.getCreatedBy(); + Map, List> groups = new LinkedHashMap<>(); + for (HandlingUnit hu : handlingUnits) { + groups.computeIfAbsent(buildIfsMoveGroupKey(hu), key -> new ArrayList<>()).add(hu); + } + CrossSiteHuTransferResult result = new CrossSiteHuTransferResult(); + List stoNos = new ArrayList<>(); + List stiNos = new ArrayList<>(); + List completedIds = new ArrayList<>(); + Map revisionCache = new HashMap<>(); + TransactionTemplate groupTransaction = new TransactionTemplate(transactionManager); + groupTransaction.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + for (List group : groups.values()) { + HandlingUnit first = group.get(0); + List groupIds = group.stream().map(HandlingUnit::getUnitId).collect(Collectors.toList()); + BigDecimal quantity = group.stream().map(HandlingUnit::getQty) + .filter(qty -> qty != null).reduce(BigDecimal.ZERO, BigDecimal::add); + if (group.stream().anyMatch(hu -> hu.getQty() == null || hu.getQty().signum() <= 0)) { + recordGroupFailure(result, groupIds, "分组数量不是正数,未调用IFS"); + continue; + } + String sourceRevision = normalizedRevision(first.getEngChgLevel()); + String revisionKey = request.getToSite() + "|" + first.getPartNo() + "|" + sourceRevision; + String destRevision; + // 目标版本查询只读且在写事务外;查询失败时本组不写WMS也不调用移库。- rqrq + try { + destRevision = revisionCache.get(revisionKey); + if (destRevision == null) { + destRevision = queryDestinationRevision(request.getToSite(), first.getPartNo(), sourceRevision); + revisionCache.put(revisionKey, destRevision); + } + } catch (RuntimeException e) { + log.warn("跨site调拨查询目标版本失败,跳过分组 - rqrq,unitIds={}, reason={}", groupIds, e.getMessage(), e); + recordGroupFailure(result, groupIds, "目标版本查询失败:" + e.getMessage()); + continue; + } + AtomicBoolean ifsAttempted = new AtomicBoolean(false); + AtomicBoolean ifsConfirmed = new AtomicBoolean(false); + try { + // 本组写入和IFS调用同处一个WMS事务;IFS明确失败会抛错并回滚本组。- rqrq + final String revisionToWrite = destRevision; + String[] transNos = groupTransaction.execute(status -> { + String[] numbers = persistSuccessfulGroup(request, group, groupIds, revisionToWrite, userId, userName); + callCrossSiteTransferApi(request, first, quantity, revisionToWrite, ifsAttempted); + ifsConfirmed.set(true); + return numbers; + }); + if (transNos == null) { + throw new XJException("分组本地事务没有返回单号"); + } + stoNos.add(transNos[0]); + stiNos.add(transNos[1]); + completedIds.addAll(groupIds); + result.setSuccessCount(result.getSuccessCount() + groupIds.size()); + } catch (RuntimeException e) { + if (e instanceof IfsTransferUncertainException || ifsConfirmed.get() + || (ifsAttempted.get() && !(e instanceof IfsTransferRejectedException))) { + // 超时/网络异常可能已完成IFS;IFS成功后提交失败也无法自动撤销外部移库。- rqrq + log.error("跨site调拨结果不明,停止后续分组并人工对账 - rqrq,unitIds={}", groupIds, e); + result.setReconciliationRequired(true); + recordGroupFailure(result, groupIds, "IFS或本地提交结果不明,需人工对账:" + e.getMessage()); + break; + } + // 本地写入失败发生于IFS调用前,或IFS返回明确失败;当前组已回滚,继续下一组。- rqrq + log.warn("跨site调拨分组已回滚,继续下一组 - rqrq,unitIds={}, reason={}", groupIds, e.getMessage(), e); + recordGroupFailure(result, groupIds, "本组已回滚:" + e.getMessage()); } - itemNo++; } - - // 单据写完后再改标签site;WHERE再次卡在库未预定,影响行数必须等于选中数,否则回滚 - rqrq - int updatedRows = crossSiteHuTransferMapper.updateHandlingUnitSite(request, unitIds); - if (updatedRows != unitIds.size()) { - throw new RuntimeException("更新标签site失败,期望更新" + unitIds.size() + "条,实际" + updatedRows - + "条,可能标签已被预定或出库,请刷新后重试"); + // 结果不明时中断后续分组;未执行的标签逐条列出,避免页面将其误认为成功。- rqrq + for (String unitId : unitIds) { + if (!completedIds.contains(unitId) && !result.getFailedUnitIds().contains(unitId)) { + recordGroupFailure(result, Arrays.asList(unitId), "前一组结果不明,本组未执行"); + } } + result.setStoTransNo(String.join(",", stoNos)); + result.setStiTransNo(String.join(",", stiNos)); + result.setMessage(result.isReconciliationRequired() + ? "部分分组结果不明,请停止重试并人工对账;成功" + result.getSuccessCount() + "条,失败或未执行" + result.getFailCount() + "条" + : "跨site调拨完成:成功" + result.getSuccessCount() + "条,失败" + result.getFailCount() + "条"); + return result; + } - // 标签site改完后再查海安托盘明细:有明细才改sub_site,无明细视为普通仓标签直接跳过 - rqrq - syncHaiAnPalletDetailSubSite(request, unitIds); + /** + * IFS分组键与托盘自动移库一致:来源site、物料、库位、批次、WDR、来源版本、失效日期。- rqrq + * @param hu 来源标签;失效日期为空单独成组 - rqrq + * @return 不可变用途的七字段组合,避免不同库存主键混调用IFS - rqrq + */ + private List buildIfsMoveGroupKey(HandlingUnit hu) { + String expiry = hu.getExpiredDate() == null ? "" : new SimpleDateFormat("yyyy-MM-dd").format(hu.getExpiredDate()); + return Arrays.asList(hu.getSite(), hu.getPartNo(), hu.getLocationId(), hu.getBatchNo(), + hu.getWdr() == null ? "*" : hu.getWdr(), normalizedRevision(hu.getEngChgLevel()), expiry); + } - // 按来源标签版本查询目标合同版本并回写标签;失败时回滚本次site、单据和托盘明细改写 - rqrq - syncDestinationRevisions(request, handlingUnits); + /** 来源版本为空时按IFS惯例使用1,查询和分组必须一致。- rqrq */ + private String normalizedRevision(String revision) { + return StringUtils.hasText(revision) ? revision.trim() : "1"; + } - // 调拨外部接口当前留空:后续接入失败策略需与本事务一起评估,避免先提交本地再调外部导致不一致 - rqrq - callCrossSiteTransferApi(request, handlingUnits, stoTransNo, stiTransNo); + /** + * 记录整个失败分组,保证失败标签与原因一一对应,供页面定位与重试。- rqrq + * @param result 本次结果;只记录当前组,不改其他组数据 - rqrq + * @param unitIds 当前组标签号;IFS失败时均保持原site - rqrq + * @param reason 查询/移库失败原因;失败会出现在页面 - rqrq + */ + private void recordGroupFailure(CrossSiteHuTransferResult result, List unitIds, String reason) { + result.setFailCount(result.getFailCount() + unitIds.size()); + for (String unitId : unitIds) { + result.getFailedUnitIds().add(unitId); + result.getFailedReasons().add(reason); + } + } - CrossSiteHuTransferResult result = new CrossSiteHuTransferResult(); - result.setSuccessCount(updatedRows); - result.setFailCount(0); - result.setStoTransNo(stoTransNo); - result.setStiTransNo(stiTransNo); - result.setMessage("跨site调拨完成,出库事务号:" + stoTransNo + ",入库事务号:" + stiTransNo); - log.info("跨小site调拨完成 - rqrq,fromSite={}, toSite={}, sto={}, sti={}, count={}", - request.getFromSite(), request.getToSite(), stoTransNo, stiTransNo, updatedRows); - return result; + /** + * 仅写当前分组;调用方在同一个TransactionTemplate事务中随后调用IFS,失败时本组回滚。- rqrq + * @param request 含来源、目标与操作人;写失败时不调用IFS - rqrq + * @param group 来源标签快照;写前加锁验证 - rqrq + * @param unitIds 当前组主键;写入行数必须完全匹配 - rqrq + * @param destRevision IFS查询的目标合同版本;仅入库单和目标标签使用 - rqrq + * @return 当前组STO、STI单号 - rqrq + */ + private String[] persistSuccessfulGroup(CrossSiteHuTransferRequest request, List group, + List unitIds, String destRevision, String userId, String userName) { + List current = crossSiteHuTransferMapper.listTransferHandlingUnits(request.getFromSite(), unitIds); + validateHandlingUnitStatus(unitIds, current); + Map currentById = current.stream().collect(Collectors.toMap(HandlingUnit::getUnitId, hu -> hu)); + for (HandlingUnit original : group) { + HandlingUnit fresh = currentById.get(original.getUnitId()); + if (!buildIfsMoveGroupKey(original).equals(buildIfsMoveGroupKey(fresh)) + || original.getQty() == null || fresh.getQty() == null + || original.getQty().compareTo(fresh.getQty()) != 0) { + throw new XJException("IFS执行后标签库存主键或数量变化:" + original.getUnitId()); + } + } + List conflicts = crossSiteHuTransferMapper.listConflictUnitIds(request.getToSite(), unitIds); + if (!CollectionUtils.isEmpty(conflicts)) { + throw new XJException("IFS执行后目标site已有同号标签:" + String.join(",", conflicts)); + } + TransNoControl sto = transNoControlService.getTransNo(request.getSite(), "STO", 10); + TransNoControl sti = transNoControlService.getTransNo(request.getSite(), "STI", 10); + if (sto == null || !StringUtils.hasText(sto.getNewTransNo()) + || sti == null || !StringUtils.hasText(sti.getNewTransNo())) { + throw new XJException("生成STO/STI事务号失败"); + } + String stoNo = sto.getNewTransNo(); + String stiNo = sti.getNewTransNo(); + String warehouseId = group.get(0).getWarehouseId(); + if (!transHeaderService.save(buildTransHeader(request.getFromSite(), stoNo, "STO", warehouseId, + stiNo, "跨小site调拨出库 " + request.getFromSite() + " -> " + request.getToSite(), userId, userName)) + || !transHeaderService.save(buildTransHeader(request.getToSite(), stiNo, "STI", warehouseId, + stoNo, "跨小site调拨入库 " + request.getFromSite() + " -> " + request.getToSite(), userId, userName))) { + throw new XJException("保存STO/STI单头失败"); + } + HandlingUnit first = group.get(0); + BigDecimal total = group.stream().map(HandlingUnit::getQty).reduce(BigDecimal.ZERO, BigDecimal::add); + TransDetail outDetail = buildTransDetail(request.getFromSite(), stoNo, 1, first, total, "-", first.getLocationId()); + TransDetail inDetail = buildTransDetail(request.getToSite(), stiNo, 1, first, total, "+", first.getLocationId()); + inDetail.setEngChgLevel(destRevision); + if (!transDetailService.save(outDetail) || !transDetailService.save(inDetail)) { + throw new XJException("保存STO/STI明细失败"); + } + int seqNo = 1; + for (HandlingUnit hu : group) { + TransDetailSub outSub = buildTransDetailSub(request.getFromSite(), stoNo, 1D, seqNo, hu, "-"); + TransDetailSub inSub = buildTransDetailSub(request.getToSite(), stiNo, 1D, seqNo, hu, "+"); + inSub.setEngChgLevel(destRevision); + if (!transDetailSubService.save(outSub) || !transDetailSubService.save(inSub)) { + throw new XJException("保存STO/STI标签子明细失败:" + hu.getUnitId()); + } + seqNo++; + } + int siteRows = crossSiteHuTransferMapper.updateHandlingUnitSite(request, unitIds); + if (siteRows != unitIds.size()) { + throw new XJException("更新标签site数量不符,期望" + unitIds.size() + ",实际" + siteRows); + } + syncHaiAnPalletDetailSubSite(request, unitIds); + int revisionRows = crossSiteHuTransferMapper.updateHandlingUnitEngChgLevel( + request.getToSite(), destRevision, request.getCreatedBy(), unitIds); + if (revisionRows != unitIds.size()) { + throw new XJException("更新标签工程版本数量不符,期望" + unitIds.size() + ",实际" + revisionRows); + } + return new String[]{stoNo, stiNo}; } /** @@ -492,31 +573,6 @@ public class CrossSiteHuTransferServiceImpl implements CrossSiteHuTransferServic } } - /** - * 以加锁读取的来源标签版本查询目标合同版本,按返回版本批量回写目的site标签。- rqrq - * 查询或更新行数不符时抛错,使本次本地调拨整体回滚;来源单据仍保留原版本。- rqrq - */ - private void syncDestinationRevisions(CrossSiteHuTransferRequest request, List handlingUnits) { - Map> revisionCache = new HashMap<>(); - Map> unitIdsByRevision = new LinkedHashMap<>(); - for (HandlingUnit hu : handlingUnits) { - String sourceRevision = StringUtils.hasText(hu.getEngChgLevel()) ? hu.getEngChgLevel().trim() : "1"; - // 同一目标site、物料和来源版本只查询一次IFS;返回空版本必须阻断本次调拨 - rqrq - String destRevision = revisionCache.computeIfAbsent(hu.getPartNo(), key -> new HashMap<>()) - .computeIfAbsent(sourceRevision, - key -> queryDestinationRevision(request.getToSite(), hu.getPartNo(), sourceRevision)); - unitIdsByRevision.computeIfAbsent(destRevision, key -> new ArrayList<>()).add(hu.getUnitId()); - } - for (Map.Entry> entry : unitIdsByRevision.entrySet()) { - // site、在库和未预定条件再次限定标签,少更新一条即回滚,避免标签版本不一致 - rqrq - int rows = crossSiteHuTransferMapper.updateHandlingUnitEngChgLevel( - request.getToSite(), entry.getKey(), request.getCreatedBy(), entry.getValue()); - if (rows != entry.getValue().size()) { - throw new XJException("更新标签工程版本失败,目的site=" + request.getToSite() - + ",版本=" + entry.getKey() + ",期望" + entry.getValue().size() + "条,实际" + rows + "条"); - } - } - } /** * 按目标site、物料和来源版本查询IFS目标合同工程版本;空值或歧义版本视为失败。- rqrq @@ -535,7 +591,7 @@ public class CrossSiteHuTransferServiceImpl implements CrossSiteHuTransferServic params.put("ifsPartNo", partNo); params.put("ifsPartRev", sourceRevision); jsonBody = new ObjectMapper().writeValueAsString(params); - // GetPartRevision只接受携带JSON Body的GET;请求失败时阻断本地事务 - rqrq + // GetPartRevision只接受携带JSON Body的GET;失败时跳过当前分组且不写本地。- rqrq String response = HttpUtils.doGetWithBody(ifsUrl + "GetPartRevision", jsonBody, null); String revision = parseDestinationRevision(response); if (!StringUtils.hasText(revision)) { @@ -606,21 +662,85 @@ public class CrossSiteHuTransferServiceImpl implements CrossSiteHuTransferServic } /** - * @author rqrq - * @Description 调用跨site调拨外部接口的预留点;当前不发请求,只记日志,后续补接口时在此接入 - rqrq - * - *

后续接入时需明确:成功条件、失败是否回滚本地STO/STI/标签site、幂等键用哪张单据号。

- * - * @param request 调拨请求 - * @param handlingUnits 已校验通过的标签 - * @param stoTransNo 出库事务号 - * @param stiTransNo 入库事务号 + * 按托盘自动移库的接口字段调用IFS;明确返回IFSUpdated才允许提交当前组WMS事务。- rqrq + * 有内容的业务错误响应使本组回滚后继续;超时、网络异常或空响应结果不明,调用方停止后续组。- rqrq + * @param request 来源与目标小site;两侧库位均保持标签原库位 - rqrq + * @param first 当前库存主键分组的首张标签;批次、WDR、失效日期取自本组 - rqrq + * @param quantity 当前组数量合计;必须大于0 - rqrq + * @param destRevision 目标合同工程版本,来自GetPartRevision - rqrq + * @param ifsAttempted 发起HTTP调用前置true;回滚异常掩盖原始异常时用于保守判定 - rqrq */ - private void callCrossSiteTransferApi(CrossSiteHuTransferRequest request, List handlingUnits, - String stoTransNo, String stiTransNo) { - log.info("跨site调拨外部接口暂未接入,本次仅完成本地STO/STI单据和标签site改写 - rqrq,fromSite={}, toSite={}, sto={}, sti={}, count={}", - request.getFromSite(), request.getToSite(), stoTransNo, stiTransNo, - handlingUnits == null ? 0 : handlingUnits.size()); + private void callCrossSiteTransferApi(CrossSiteHuTransferRequest request, HandlingUnit first, + BigDecimal quantity, String destRevision, AtomicBoolean ifsAttempted) { + String wdr = first.getWdr() == null ? "*" : first.getWdr(); + Map params = new LinkedHashMap<>(); + params.put("ifsDBName", ifsDBName); + params.put("domainUserID", getRevisionDomainUserID()); + params.put("ifsSiteID", request.getFromSite()); + params.put("partNo", first.getPartNo()); + params.put("qtyToIssue", quantity); + params.put("locationNo", first.getLocationId()); + params.put("destSiteID", request.getToSite()); + params.put("destLocationNo", first.getLocationId()); + params.put("lotBatchNo", first.getBatchNo()); + params.put("expiryDate", first.getExpiredDate() == null ? null + : new SimpleDateFormat("dd/MM/yyyy").format(first.getExpiredDate())); + params.put("WdrNo", wdr); + params.put("SerialNo", "*"); + params.put("ConfigurationID", "*"); + params.put("ActivitySequenceNo", 0); + params.put("EngChgLevel", destRevision); + params.put("HandlingUntiID", 0); + params.put("ToWdrNo", wdr); + params.put("AvailabilityControlID", ""); + params.put("ConsumeConsignmentStock", "N"); + params.put("ToDestination", "Move to inventory"); + params.put("UnatachFromHandlingUnit", "FALSE"); + String jsonBody; + try { + jsonBody = new ObjectMapper().writeValueAsString(params); + } catch (Exception e) { + throw new XJException("构造IFS移库请求失败:" + e.getMessage()); + } + String response; + try { + // HTTP异常不能证明IFS未执行;本组WMS回滚后必须先对账再决定是否重试。- rqrq + ifsAttempted.set(true); + response = HttpUtils.doPost(ifsUrl + "MoveInventoryPartToContract", jsonBody, null); + } catch (Exception e) { + try { + ErrorLogUtils.logInterface(request.getFromSite(), "IFS接口", "跨site标签移库", "IFS", + "MoveInventoryPartToContract", first.getPartNo() + ";" + first.getBatchNo() + ";" + quantity, + jsonBody, e.getMessage(), e.getMessage()); + } catch (Exception logError) { + log.error("记录IFS移库异常日志失败 - rqrq,partNo={}", first.getPartNo(), logError); + } + throw new IfsTransferUncertainException("IFS移库调用异常,结果可能不明确,请先核对IFS库存再重试:" + + IfsErrorMessageUtils.extractOracleErrorMessage(e.getMessage())); + } + if (!StringUtils.hasText(response)) { + throw new IfsTransferUncertainException("IFS移库响应为空,结果不明,请先核对IFS库存"); + } + String normalizedResponse = response.trim(); + if (!"IFSUpdated".equals(normalizedResponse) && !"\"IFSUpdated\"".equals(normalizedResponse)) { + throw new IfsTransferRejectedException("IFS明确返回失败:" + IfsErrorMessageUtils.extractOracleErrorMessage(response)); + } + log.info("跨site分组IFS移库成功 - rqrq,fromSite={}, toSite={}, partNo={}, batchNo={}, qty={}", + request.getFromSite(), request.getToSite(), first.getPartNo(), first.getBatchNo(), quantity); + } + + /** IFS调用后响应不明确时阻止继续处理,避免自动重试造成重复移库。- rqrq */ + private static class IfsTransferUncertainException extends RuntimeException { + private IfsTransferUncertainException(String message) { + super(message); + } + } + + /** IFS已明确返回业务失败时回滚当前组,再继续下一组。- rqrq */ + private static class IfsTransferRejectedException extends RuntimeException { + private IfsTransferRejectedException(String message) { + super(message); + } } /** diff --git a/src/main/resources/mapper/warehouse/CrossSiteHuTransferMapper.xml b/src/main/resources/mapper/warehouse/CrossSiteHuTransferMapper.xml index be1cfb33..35724db4 100644 --- a/src/main/resources/mapper/warehouse/CrossSiteHuTransferMapper.xml +++ b/src/main/resources/mapper/warehouse/CrossSiteHuTransferMapper.xml @@ -124,6 +124,7 @@ h.warehouse_id, h.wdr, h.eng_chg_level, + h.expired_date AS expiredDate, h.in_stock_flag, h.reserve_flag FROM handling_unit h WITH (UPDLOCK, ROWLOCK)