Browse Source

立库更新

master
常熟吴彦祖 4 weeks ago
parent
commit
c9b088d262
  1. 9
      src/main/java/com/gaotao/modules/warehouse/entity/CrossSiteHuTransferResult.java
  2. 432
      src/main/java/com/gaotao/modules/warehouse/service/impl/CrossSiteHuTransferServiceImpl.java
  3. 1
      src/main/resources/mapper/warehouse/CrossSiteHuTransferMapper.xml

9
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<String> 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
* <p>是否落库:是,对应 STO 出库单(site=来源小site);赋值时机:校验通过后生成;消费方:前端结果展示、后续调拨接口</p>
*/
private String stoTransNo;
/**
* 入库事务号,来源登录大site的 transNoControl(STI),写入 trans_header.trans_no;前端结果弹窗回显 - rqrq
* 成功组入库事务号,多个组用逗号连接;来源登录大site的transNoControl(STI),前端回显。- rqrq
* <p>是否落库:是,对应 STI 入库单(site=目标小site);赋值时机:校验通过后生成;消费方:前端结果展示、后续调拨接口</p>
*/
private String stiTransNo;

432
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
*
* <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);
}
}
/**

1
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)

Loading…
Cancel
Save