✨ 运单导入支持多货物合并与配载路线串联
- 同一运单标识号的多行货物合并为一条运单,各行转为货物明细写入 goodsJson,数量与其他费用合计按行累加 - 新增组内一致性校验:发货地址、到货地址、数量单位、配载标识号不一致时报错,避免无法合并的数据静默落库 - 配载单路线按导入顺序串联,上一票到货地与下一票发货地重合时去重写入途经地,到货地取末票
This commit is contained in:
+36
-1
@@ -174,6 +174,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
|||||||
throw new ServiceException("配载标识号已存在:" + normalizedLoadingNo);
|
throw new ServiceException("配载标识号已存在:" + normalizedLoadingNo);
|
||||||
}
|
}
|
||||||
Waybill first = waybills.get(0);
|
Waybill first = waybills.get(0);
|
||||||
|
Waybill last = waybills.get(waybills.size() - 1);
|
||||||
LoadingManage loadingManage = new LoadingManage();
|
LoadingManage loadingManage = new LoadingManage();
|
||||||
loadingManage.setLoadingNo(normalizedLoadingNo);
|
loadingManage.setLoadingNo(normalizedLoadingNo);
|
||||||
loadingManage.setLoadingSubNos(waybills.stream()
|
loadingManage.setLoadingSubNos(waybills.stream()
|
||||||
@@ -197,7 +198,8 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
|||||||
loadingManage.setCarrierName(first.getCarrierName());
|
loadingManage.setCarrierName(first.getCarrierName());
|
||||||
loadingManage.setCarrierContractId(first.getCarrierContractId());
|
loadingManage.setCarrierContractId(first.getCarrierContractId());
|
||||||
loadingManage.setDepartureAddress(first.getDepartureAddress());
|
loadingManage.setDepartureAddress(first.getDepartureAddress());
|
||||||
loadingManage.setArrivalAddress(first.getArrivalAddress());
|
loadingManage.setTransitAddress(buildImportedRouteTransitAddress(waybills));
|
||||||
|
loadingManage.setArrivalAddress(last.getArrivalAddress());
|
||||||
loadingManage.setOriginalNo(first.getOriginalNo());
|
loadingManage.setOriginalNo(first.getOriginalNo());
|
||||||
loadingManage.setDataSource("批量导入");
|
loadingManage.setDataSource("批量导入");
|
||||||
loadingManage.setStartDate(first.getStartDate());
|
loadingManage.setStartDate(first.getStartDate());
|
||||||
@@ -222,6 +224,39 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
|
|||||||
.set(Waybill::getLoadingNo, normalizedLoadingNo));
|
.set(Waybill::getLoadingNo, normalizedLoadingNo));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 根据导入运单构建配载单的途经地:
|
||||||
|
* 组内运单按导入顺序串联路线,上一票的到货地址与下一票的发货地址相同(中途卸货点)时只保留一个点,
|
||||||
|
* 最终形成“首票发货地 → 途经点 → 末票到货地”。
|
||||||
|
*/
|
||||||
|
private String buildImportedRouteTransitAddress(List<Waybill> waybills) {
|
||||||
|
// 按顺序收集全部节点:首票发货地、每票到货地;节点与相邻前一点相同则重合跳过
|
||||||
|
List<String> nodes = new ArrayList<>();
|
||||||
|
for (Waybill waybill : waybills) {
|
||||||
|
String departure = TransportBusinessSupport.trimToNull(waybill.getDepartureAddress());
|
||||||
|
String arrival = TransportBusinessSupport.trimToNull(waybill.getArrivalAddress());
|
||||||
|
appendRouteNode(nodes, departure);
|
||||||
|
appendRouteNode(nodes, arrival);
|
||||||
|
}
|
||||||
|
// 途经点 = 去掉首尾(首票发货地、末票到货地)后的中间节点
|
||||||
|
List<String> transitNodes = nodes.size() > 2 ? nodes.subList(1, nodes.size() - 1) : List.of();
|
||||||
|
if (Func.isEmpty(transitNodes)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return String.join(" - ", transitNodes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void appendRouteNode(List<String> nodes, String address) {
|
||||||
|
if (Func.isEmpty(address)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!nodes.isEmpty() && nodes.get(nodes.size() - 1).equals(address)) {
|
||||||
|
// 与上一节点相同视为同一地点,重合不重复
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
nodes.add(address);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public BusinessRemoveResultVO removeLoadingManage(String ids) {
|
public BusinessRemoveResultVO removeLoadingManage(String ids) {
|
||||||
|
|||||||
+133
-6
@@ -19,6 +19,7 @@ import org.springblade.core.tool.utils.BeanUtil;
|
|||||||
import org.springblade.core.tool.utils.DateUtil;
|
import org.springblade.core.tool.utils.DateUtil;
|
||||||
import org.springblade.core.tool.utils.Func;
|
import org.springblade.core.tool.utils.Func;
|
||||||
import org.springblade.core.tool.utils.WebUtil;
|
import org.springblade.core.tool.utils.WebUtil;
|
||||||
|
import org.springblade.core.tool.jackson.JsonUtil;
|
||||||
import org.springblade.system.cache.DictCache;
|
import org.springblade.system.cache.DictCache;
|
||||||
import org.springblade.system.cache.DictBizCache;
|
import org.springblade.system.cache.DictBizCache;
|
||||||
import org.springblade.system.cache.UserCache;
|
import org.springblade.system.cache.UserCache;
|
||||||
@@ -55,6 +56,7 @@ import java.time.format.DateTimeParseException;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
@@ -184,20 +186,36 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
|||||||
|
|
||||||
List<Map<String, Object>> rows = Func.isEmpty(request.getRows()) ? List.of() : request.getRows();
|
List<Map<String, Object>> rows = Func.isEmpty(request.getRows()) ? List.of() : request.getRows();
|
||||||
|
|
||||||
|
// 逐行构建运单(未落库),构建失败时报出对应行号
|
||||||
|
List<Waybill> rowWaybills = new ArrayList<>();
|
||||||
|
for (int index = 0; index < rows.size(); index++) {
|
||||||
|
try {
|
||||||
|
rowWaybills.add(buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft));
|
||||||
|
} catch (Exception exception) {
|
||||||
|
throw new ServiceException("第" + (index + 1) + "行" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 同一运单标识号的多货物行合并为一条运单,各行转为货物明细
|
||||||
|
Map<Integer, Waybill> waybillByRow = new HashMap<>();
|
||||||
|
Set<Integer> skipRows = new HashSet<>();
|
||||||
|
mergeMultiCargoRows(rows, rowWaybills, waybillByRow, skipRows);
|
||||||
|
|
||||||
List<Waybill> waybills = new ArrayList<>();
|
List<Waybill> waybills = new ArrayList<>();
|
||||||
Map<String, List<Waybill>> loadingWaybills = new TreeMap<>();
|
Map<String, List<Waybill>> loadingWaybills = new TreeMap<>();
|
||||||
for (int index = 0; index < rows.size(); index++) {
|
for (int index = 0; index < rows.size(); index++) {
|
||||||
|
if (skipRows.contains(index)) continue;
|
||||||
|
Waybill waybill = waybillByRow.getOrDefault(index, rowWaybills.get(index));
|
||||||
|
// 配载标识号必须取自行数据:运单落库(prepareForSave)会把新建运单的 loadingNo 置空
|
||||||
|
String loadingIdentifier = stringValue(rows.get(index), "loadingIdentifier", "配载标识号");
|
||||||
try {
|
try {
|
||||||
String loadingIdentifier = stringValue(rows.get(index), "loadingIdentifier", "配载标识号");
|
|
||||||
Waybill waybill = buildWaybill(rows.get(index), batch, request.getCarrierContractId(), draft);
|
|
||||||
if (draft) waybillService.saveDraft(waybill); else waybillService.submit(waybill);
|
if (draft) waybillService.saveDraft(waybill); else waybillService.submit(waybill);
|
||||||
waybills.add(waybill);
|
|
||||||
if (!draft && IMPORT_TYPE_WAYBILL.equals(batch.getImportType()) && Func.isNotEmpty(loadingIdentifier)) {
|
|
||||||
loadingWaybills.computeIfAbsent(loadingIdentifier, key -> new ArrayList<>()).add(waybill);
|
|
||||||
}
|
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
throw new ServiceException("第" + (index + 1) + "行" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage());
|
throw new ServiceException("第" + (index + 1) + "行" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage());
|
||||||
}
|
}
|
||||||
|
waybills.add(waybill);
|
||||||
|
if (!draft && IMPORT_TYPE_WAYBILL.equals(batch.getImportType()) && Func.isNotEmpty(loadingIdentifier)) {
|
||||||
|
loadingWaybills.computeIfAbsent(loadingIdentifier, key -> new ArrayList<>()).add(waybill);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
loadingWaybills.forEach(loadingManageService::createFromImportedWaybills);
|
loadingWaybills.forEach(loadingManageService::createFromImportedWaybills);
|
||||||
batch.setWaybillCount(waybills.size());
|
batch.setWaybillCount(waybills.size());
|
||||||
@@ -218,6 +236,69 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将同一运单标识号的多货物行合并为一条运单:
|
||||||
|
* 首行作为主行(数值字段累加各行),后续行转为货物明细追加到主行货物信息中;
|
||||||
|
* 被 merge 的行记入 skipRows,落库时跳过。
|
||||||
|
*/
|
||||||
|
private void mergeMultiCargoRows(List<Map<String, Object>> rows, List<Waybill> rowWaybills,
|
||||||
|
Map<Integer, Waybill> waybillByRow, Set<Integer> skipRows) {
|
||||||
|
Map<String, List<Integer>> groups = new LinkedHashMap<>();
|
||||||
|
for (int index = 0; index < rowWaybills.size(); index++) {
|
||||||
|
String relationNo = rowWaybills.get(index).getRelationNo();
|
||||||
|
if (Func.isEmpty(relationNo)) continue;
|
||||||
|
groups.computeIfAbsent(relationNo, key -> new ArrayList<>()).add(index);
|
||||||
|
}
|
||||||
|
for (List<Integer> groupRows : groups.values()) {
|
||||||
|
if (groupRows.size() <= 1) continue;
|
||||||
|
Waybill primary = rowWaybills.get(groupRows.get(0));
|
||||||
|
List<Map<String, Object>> goodsList = new ArrayList<>();
|
||||||
|
for (Integer rowIndex : groupRows) {
|
||||||
|
Waybill current = rowWaybills.get(rowIndex);
|
||||||
|
if (!rowIndex.equals(groupRows.get(0))) {
|
||||||
|
// 主行字段保留首行值,数量等数值字段累加同组其余各行
|
||||||
|
mergeNumericFields(primary, current);
|
||||||
|
skipRows.add(rowIndex);
|
||||||
|
waybillByRow.remove(rowIndex);
|
||||||
|
}
|
||||||
|
goodsList.add(buildGoodsItem(rows.get(rowIndex), current));
|
||||||
|
}
|
||||||
|
primary.setGoodsJson(JsonUtil.toJson(goodsList));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 合并多货物行时累加数量、其他费用等可加数值字段;为空的字段跳过。 */
|
||||||
|
private void mergeNumericFields(Waybill primary, Waybill current) {
|
||||||
|
primary.setQuantity(sumNullable(primary.getQuantity(), current.getQuantity()));
|
||||||
|
primary.setOtherFeeTotal(sumNullable(primary.getOtherFeeTotal(), current.getOtherFeeTotal()));
|
||||||
|
}
|
||||||
|
|
||||||
|
private BigDecimal sumNullable(BigDecimal first, BigDecimal second) {
|
||||||
|
if (first == null) return second;
|
||||||
|
if (second == null) return first;
|
||||||
|
return first.add(second);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 由导入行构建一条货物明细,字段与运单详情货物编辑结构保持一致。 */
|
||||||
|
private Map<String, Object> buildGoodsItem(Map<String, Object> row, Waybill waybill) {
|
||||||
|
Map<String, Object> goods = new LinkedHashMap<>();
|
||||||
|
putIfNotEmpty(goods, "cargoName", waybill.getCargoName());
|
||||||
|
putIfNotEmpty(goods, "cargoType", waybill.getCargoType());
|
||||||
|
putIfNotEmpty(goods, "specification", waybill.getSpecification());
|
||||||
|
putIfNotEmpty(goods, "model", waybill.getModel());
|
||||||
|
putIfNotEmpty(goods, "packageType", stringValue(row, "packageType", "包装"));
|
||||||
|
if (waybill.getQuantity() != null) {
|
||||||
|
goods.put("quantity", waybill.getQuantity().toPlainString());
|
||||||
|
}
|
||||||
|
putIfNotEmpty(goods, "quantityUnit", waybill.getQuantityUnit());
|
||||||
|
putIfNotEmpty(goods, "remark", waybill.getRemark());
|
||||||
|
return goods;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void putIfNotEmpty(Map<String, Object> goods, String key, String value) {
|
||||||
|
if (Func.isNotEmpty(value)) goods.put(key, value);
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IPage<WaybillImportBatchVO> page(IPage<WaybillImportBatch> page, WaybillImportBatchRequest request) {
|
public IPage<WaybillImportBatchVO> page(IPage<WaybillImportBatch> page, WaybillImportBatchRequest request) {
|
||||||
LambdaQueryWrapper<WaybillImportBatch> queryWrapper = Wrappers.<WaybillImportBatch>lambdaQuery()
|
LambdaQueryWrapper<WaybillImportBatch> queryWrapper = Wrappers.<WaybillImportBatch>lambdaQuery()
|
||||||
@@ -523,6 +604,9 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
|||||||
// 16. 同一运单标识号校验
|
// 16. 同一运单标识号校验
|
||||||
validateWaybillIdentifier(row, i, waybillIdentifierMap, rows, errors);
|
validateWaybillIdentifier(row, i, waybillIdentifierMap, rows, errors);
|
||||||
|
|
||||||
|
// 17. 同一运单标识号组内一致性校验(地址/配载标识号/数量单位,多货物合并的前提)
|
||||||
|
validateWaybillIdentifierConsistency(row, i, waybillIdentifierMap, rows, errors);
|
||||||
|
|
||||||
if (!errors.isEmpty()) {
|
if (!errors.isEmpty()) {
|
||||||
errorMap.put(i, String.join("; ", errors));
|
errorMap.put(i, String.join("; ", errors));
|
||||||
}
|
}
|
||||||
@@ -826,6 +910,49 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同一运单标识号组内的发货地址、到货地址、数量单位、配载标识号必须一致:
|
||||||
|
* 多货物行会合并为一条运单,上述字段不一致时无法合并(数量累加也要求单位相同)。
|
||||||
|
*/
|
||||||
|
private void validateWaybillIdentifierConsistency(Map<String, Object> row, int rowIndex,
|
||||||
|
Map<String, List<Integer>> waybillIdentifierMap, List<Map<String, Object>> allRows, List<String> errors) {
|
||||||
|
String waybillIdentifier = stringValue(row, "waybillIdentifier", "同一运单标识号");
|
||||||
|
if (Func.isEmpty(waybillIdentifier)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
List<Integer> sameIdentifierRows = waybillIdentifierMap.get(waybillIdentifier);
|
||||||
|
if (sameIdentifierRows == null || sameIdentifierRows.size() <= 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
compareGroupValue(rowIndex, sameIdentifierRows, allRows, "departureAddress", "发货地址", errors);
|
||||||
|
compareGroupValue(rowIndex, sameIdentifierRows, allRows, "arrivalAddress", "到货地址", errors);
|
||||||
|
compareGroupValue(rowIndex, sameIdentifierRows, allRows, "quantityUnit", "数量单位", errors);
|
||||||
|
compareGroupValue(rowIndex, sameIdentifierRows, allRows, "loadingIdentifier", "配载标识号", errors);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 比较同一运单标识号组内其余行的字段值,不一致时记录错误;空值不参与比较。 */
|
||||||
|
private void compareGroupValue(int rowIndex, List<Integer> sameIdentifierRows, List<Map<String, Object>> allRows,
|
||||||
|
String field, String fieldName, List<String> errors) {
|
||||||
|
String currentValue = normalizeCompareValue(stringValue(allRows.get(rowIndex), field, fieldName));
|
||||||
|
if (Func.isEmpty(currentValue)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (Integer otherRowIndex : sameIdentifierRows) {
|
||||||
|
if (otherRowIndex <= rowIndex) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
String otherValue = normalizeCompareValue(stringValue(allRows.get(otherRowIndex), field, fieldName));
|
||||||
|
if (Func.isNotEmpty(otherValue) && !currentValue.equals(otherValue)) {
|
||||||
|
errors.add("同一运单标识号下," + fieldName + "必须一致");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private String normalizeCompareValue(String value) {
|
||||||
|
return Func.isEmpty(value) ? null : value.replaceAll("\\s+", "");
|
||||||
|
}
|
||||||
|
|
||||||
private LocalDate parseDateForValidation(Object value, String fieldName, List<String> errors) {
|
private LocalDate parseDateForValidation(Object value, String fieldName, List<String> errors) {
|
||||||
if (value == null || String.valueOf(value).isBlank()) {
|
if (value == null || String.valueOf(value).isBlank()) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
Reference in New Issue
Block a user