|
|
|
@ -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 |
|
|
|
* |
|
|
|
* <p>事务边界:单据写入与标签site改写同一事务;任一校验或写入失败整批回滚,避免只出库不改site。</p> |
|
|
|
* <p>出入库记录site使用小site:STO.site=fromSite,STI.site=toSite。</p> |
|
|
|
* |
|
|
|
* @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<String> 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<HandlingUnit> handlingUnits = crossSiteHuTransferMapper.listTransferHandlingUnits(request.getFromSite(), unitIds); |
|
|
|
validateHandlingUnitStatus(unitIds, handlingUnits); |
|
|
|
|
|
|
|
// 目标site已存在同号标签时禁止改联合主键,避免唯一约束冲突导致半成品单据 - rqrq |
|
|
|
List<String> 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<String, List<HandlingUnit>> 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<String, List<HandlingUnit>> entry : groupedHUs.entrySet()) { |
|
|
|
List<HandlingUnit> 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<String> 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<String>, List<HandlingUnit>> groups = new LinkedHashMap<>(); |
|
|
|
for (HandlingUnit hu : handlingUnits) { |
|
|
|
groups.computeIfAbsent(buildIfsMoveGroupKey(hu), key -> new ArrayList<>()).add(hu); |
|
|
|
} |
|
|
|
CrossSiteHuTransferResult result = new CrossSiteHuTransferResult(); |
|
|
|
List<String> stoNos = new ArrayList<>(); |
|
|
|
List<String> stiNos = new ArrayList<>(); |
|
|
|
List<String> completedIds = new ArrayList<>(); |
|
|
|
Map<String, String> revisionCache = new HashMap<>(); |
|
|
|
TransactionTemplate groupTransaction = new TransactionTemplate(transactionManager); |
|
|
|
groupTransaction.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); |
|
|
|
for (List<HandlingUnit> group : groups.values()) { |
|
|
|
HandlingUnit first = group.get(0); |
|
|
|
List<String> 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<String> 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<String> 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<HandlingUnit> group, |
|
|
|
List<String> unitIds, String destRevision, String userId, String userName) { |
|
|
|
List<HandlingUnit> current = crossSiteHuTransferMapper.listTransferHandlingUnits(request.getFromSite(), unitIds); |
|
|
|
validateHandlingUnitStatus(unitIds, current); |
|
|
|
Map<String, HandlingUnit> 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<String> 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<HandlingUnit> handlingUnits) { |
|
|
|
Map<String, Map<String, String>> revisionCache = new HashMap<>(); |
|
|
|
Map<String, List<String>> 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<String, List<String>> 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 |
|
|
|
* |
|
|
|
* <p>后续接入时需明确:成功条件、失败是否回滚本地STO/STI/标签site、幂等键用哪张单据号。</p> |
|
|
|
* |
|
|
|
* @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<HandlingUnit> 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<String, Object> 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); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
|