✨ 运单批量导入新增与已有运单重复比对接口
This commit is contained in:
+28
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* BladeX Commercial License Agreement
|
||||
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
|
||||
*/
|
||||
package org.springblade.transport.pojo.vo;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/** 运单批量导入行与已有运单的重复情况。 */
|
||||
@Data
|
||||
@Schema(description = "运单批量导入行与已有运单的重复情况")
|
||||
public class WaybillImportDuplicateVO implements Serializable {
|
||||
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@Schema(description = "行号(从0开始,相对当前批次)")
|
||||
private Integer rowIndex;
|
||||
|
||||
@Schema(description = "命中的已有运单号")
|
||||
private List<String> waybillNos = new ArrayList<>();
|
||||
}
|
||||
+8
@@ -57,6 +57,7 @@ import org.springblade.transport.pojo.dto.WaybillImportBatchRequest;
|
||||
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
||||
import org.springblade.transport.pojo.vo.LoadingManageVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportDuplicateVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillLocateVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillTrackVO;
|
||||
@@ -230,6 +231,13 @@ public class WaybillController extends BladeController {
|
||||
waybillImportBatchService.exportFailures(request, response);
|
||||
}
|
||||
|
||||
@PostMapping("/import-batch/duplicates")
|
||||
@ApiOperationSupport(order = 6)
|
||||
@Operation(summary = "查询与已有运单重复的导入行")
|
||||
public R<List<WaybillImportDuplicateVO>> checkImportDuplicates(@RequestBody WaybillImportBatchRequest request) {
|
||||
return R.data(waybillImportBatchService.checkDuplicates(request));
|
||||
}
|
||||
|
||||
@PostMapping("/import-batch/confirm")
|
||||
@ApiOperationSupport(order = 7)
|
||||
@Operation(summary = "确认运单批量导入")
|
||||
|
||||
+6
@@ -7,8 +7,11 @@ import org.springblade.transport.pojo.dto.WaybillImportBatchRequest;
|
||||
import org.springblade.transport.pojo.entity.WaybillImportBatch;
|
||||
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportDuplicateVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/** 运单批次服务。 */
|
||||
public interface IWaybillImportBatchService extends BaseService<WaybillImportBatch> {
|
||||
WaybillImportBatch saveDraft(WaybillImportBatchRequest request);
|
||||
@@ -18,4 +21,7 @@ public interface IWaybillImportBatchService extends BaseService<WaybillImportBat
|
||||
IPage<WaybillImportBatchVO> page(IPage<WaybillImportBatch> page, WaybillImportBatchRequest request);
|
||||
BusinessRemoveResultVO removeBatches(String ids);
|
||||
String nextBatchNo();
|
||||
|
||||
/** 逐行比对导入行与项目下已有运单,返回命中的运单号。 */
|
||||
List<WaybillImportDuplicateVO> checkDuplicates(WaybillImportBatchRequest request);
|
||||
}
|
||||
|
||||
+91
@@ -12,6 +12,7 @@ import jakarta.servlet.http.HttpServletResponse;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.common.excel.ImportFailureExcelUtil;
|
||||
import org.springblade.common.excel.LenientDateParser;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.mp.base.BaseServiceImpl;
|
||||
import org.springblade.core.secure.utils.AuthUtil;
|
||||
@@ -39,6 +40,7 @@ import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.entity.WaybillImportBatch;
|
||||
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportDuplicateVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportRowErrorVO;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportValidateResultVO;
|
||||
import org.springblade.transport.service.ICustomerArchiveService;
|
||||
@@ -50,6 +52,7 @@ import org.springblade.transport.service.ITransportPlanService;
|
||||
import org.springblade.transport.service.IWaybillImportBatchService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
import org.springblade.transport.support.TransportBusinessSupport;
|
||||
import org.springblade.transport.support.VehiclePlateSupport;
|
||||
import org.springblade.transport.support.WaybillProcessSupport;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
@@ -173,6 +176,94 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
|
||||
return toVO(batch);
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<WaybillImportDuplicateVO> checkDuplicates(WaybillImportBatchRequest request) {
|
||||
List<WaybillImportDuplicateVO> duplicates = new ArrayList<>();
|
||||
if (request == null || Func.isEmpty(request.getRows()) || Func.isEmpty(request.getProjectId())) {
|
||||
return duplicates;
|
||||
}
|
||||
List<Long> carrierIds = resolveCarrierIds(request);
|
||||
String carrierName = resolveCarrierNames(carrierIds, request.getCarrierName());
|
||||
LambdaQueryWrapper<Waybill> wrapper = Wrappers.<Waybill>lambdaQuery()
|
||||
.eq(Waybill::getIsDeleted, 0)
|
||||
.eq(Waybill::getProjectId, request.getProjectId());
|
||||
if (Func.isNotEmpty(request.getId())) {
|
||||
// 追加导入时,本批次已落库的运单不算历史运单。
|
||||
wrapper.ne(Waybill::getImportBatchId, request.getId());
|
||||
}
|
||||
List<Waybill> candidates = waybillService.list(wrapper);
|
||||
if (Func.isEmpty(candidates)) {
|
||||
return duplicates;
|
||||
}
|
||||
List<Map<String, Object>> rows = request.getRows();
|
||||
for (int index = 0; index < rows.size(); index++) {
|
||||
List<String> waybillNos = new ArrayList<>();
|
||||
for (Waybill waybill : candidates) {
|
||||
if (!isDuplicateRow(rows.get(index), waybill, carrierIds, carrierName)) continue;
|
||||
String waybillNo = waybill.getWaybillNo();
|
||||
if (Func.isNotEmpty(waybillNo) && !waybillNos.contains(waybillNo)) {
|
||||
waybillNos.add(waybillNo);
|
||||
}
|
||||
}
|
||||
if (waybillNos.isEmpty()) continue;
|
||||
WaybillImportDuplicateVO duplicate = new WaybillImportDuplicateVO();
|
||||
duplicate.setRowIndex(index);
|
||||
duplicate.setWaybillNos(waybillNos);
|
||||
duplicates.add(duplicate);
|
||||
}
|
||||
return duplicates;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判重口径为导入必填项逐一相同:承运商、车牌、运输方式、起止地址、货物名称与类型、
|
||||
* 数量及单位、实际发货与实际完成时间。历史运单这些字段常为空,双方都空按相同处理。
|
||||
*/
|
||||
private boolean isDuplicateRow(Map<String, Object> row, Waybill waybill, List<Long> carrierIds, String carrierName) {
|
||||
if (row == null || waybill == null) return false;
|
||||
if (!carrierMatches(waybill, carrierIds, carrierName)) return false;
|
||||
if (!samePlate(vehicleNoFromRow(row), waybill.getVehicleNo())) return false;
|
||||
String transportType = resolveTransportTypeKey(
|
||||
stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式"));
|
||||
if (!sameText(transportType, waybill.getTransportType())) return false;
|
||||
if (!sameText(stringValue(row, "departureAddress"), waybill.getDepartureAddress())) return false;
|
||||
if (!sameText(stringValue(row, "arrivalAddress"), waybill.getArrivalAddress())) return false;
|
||||
if (!sameText(stringValue(row, "cargoName"), waybill.getCargoName())) return false;
|
||||
if (!sameText(stringValue(row, "cargoType"), waybill.getCargoType())) return false;
|
||||
BigDecimal quantity = defaultQuantity(decimalValue(row, "quantity", "数量", "重量"));
|
||||
if (quantity.compareTo(defaultQuantity(waybill.getQuantity())) != 0) return false;
|
||||
String quantityUnit = stringValue(row, "quantityUnit", "数量单位");
|
||||
if (!sameText(Func.isEmpty(quantityUnit) ? "吨" : quantityUnit, waybill.getQuantityUnit())) return false;
|
||||
if (!Objects.equals(duplicateDate(row.get("actualStartDate")), waybill.getStartDate())) {
|
||||
return false;
|
||||
}
|
||||
return Objects.equals(duplicateDate(row.get("actualEndDate")), waybill.getEndDate());
|
||||
}
|
||||
|
||||
/** 承运商优先按档案 ID 比对;历史手工运单只有名称,退化为名称比对。 */
|
||||
private boolean carrierMatches(Waybill waybill, List<Long> carrierIds, String carrierName) {
|
||||
if (Func.isNotEmpty(carrierIds) && waybill.getCarrierId() != null) {
|
||||
return carrierIds.contains(waybill.getCarrierId());
|
||||
}
|
||||
return sameText(waybill.getCarrierName(), carrierName);
|
||||
}
|
||||
|
||||
private boolean sameText(String left, String right) {
|
||||
String a = left == null ? "" : left.trim();
|
||||
String b = right == null ? "" : right.trim();
|
||||
return a.equalsIgnoreCase(b);
|
||||
}
|
||||
|
||||
private boolean samePlate(String left, String right) {
|
||||
return VehiclePlateSupport.normalize(left).equals(VehiclePlateSupport.normalize(right));
|
||||
}
|
||||
|
||||
/** 判重时的日期解析不阻断查询:无法识别的日期返回 null,按「与空值相同」处理。 */
|
||||
private LocalDate duplicateDate(Object value) {
|
||||
if (value == null) return null;
|
||||
String text = String.valueOf(value).trim();
|
||||
return text.isEmpty() ? null : LenientDateParser.parseDateLenientlyOrNull(text);
|
||||
}
|
||||
|
||||
/** 草稿与确认导入共用落库流程,差异仅在于运单是否走校验以及是否生成应收应付明细。 */
|
||||
private WaybillImportBatch persist(WaybillImportBatchRequest request, String importStatus) {
|
||||
boolean draft = STATUS_DRAFT.equals(importStatus);
|
||||
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
package org.springblade.transport.service.impl;
|
||||
|
||||
import com.baomidou.mybatisplus.core.conditions.Wrapper;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springblade.system.feign.ISysClient;
|
||||
import org.springblade.transport.pojo.dto.WaybillImportBatchRequest;
|
||||
import org.springblade.transport.pojo.entity.Waybill;
|
||||
import org.springblade.transport.pojo.vo.WaybillImportDuplicateVO;
|
||||
import org.springblade.transport.service.ICustomerArchiveService;
|
||||
import org.springblade.transport.service.IDriverService;
|
||||
import org.springblade.transport.service.ILoadingManageService;
|
||||
import org.springblade.transport.service.IProjectApplyService;
|
||||
import org.springblade.transport.service.IReceivablePayableDetailService;
|
||||
import org.springblade.transport.service.ITransportPlanService;
|
||||
import org.springblade.transport.service.IWaybillService;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
import static org.mockito.ArgumentMatchers.any;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* 批量导入行与历史运单的判重测试。
|
||||
* <p>
|
||||
* 口径为导入必填项逐一相同;历史手工运单的承运商档案 ID 与时间列常为空,
|
||||
* 这类字段双方都空时按相同处理,否则手工运单永远判不出重复。
|
||||
*/
|
||||
class WaybillImportDuplicateTest {
|
||||
|
||||
private static final Long PROJECT_ID = 1001L;
|
||||
private static final Long CARRIER_ID = 2002L;
|
||||
private static final String CARRIER_NAME = "广西驰远干线运输有限公司";
|
||||
private static final String VEHICLE_NO = "桂A5D61N";
|
||||
private static final String CARGO_NAME = "混煤";
|
||||
private static final String DEPARTURE = "鄂尔多斯市达拉特旗树林召镇";
|
||||
private static final String ARRIVAL = "南宁市江南区沙井大道";
|
||||
|
||||
private IWaybillService waybillService;
|
||||
private WaybillImportBatchServiceImpl service;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
waybillService = mock(IWaybillService.class);
|
||||
service = new WaybillImportBatchServiceImpl(
|
||||
waybillService,
|
||||
mock(ICustomerArchiveService.class),
|
||||
mock(IProjectApplyService.class),
|
||||
mock(ITransportPlanService.class),
|
||||
mock(IReceivablePayableDetailService.class),
|
||||
mock(ILoadingManageService.class),
|
||||
mock(ISysClient.class),
|
||||
mock(IDriverService.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("必填项全一致:返回命中的已有运单号")
|
||||
void reportsDuplicateWaybillNo() {
|
||||
stubCandidates(waybill("YD-20261023-0012"));
|
||||
|
||||
WaybillImportBatchRequest req = request(importRow());
|
||||
List<WaybillImportDuplicateVO> duplicates = service.checkDuplicates(req);
|
||||
|
||||
assertEquals(1, duplicates.size(), "rows=" + req.getRows() + " projectId=" + req.getProjectId());
|
||||
assertEquals(0, duplicates.get(0).getRowIndex());
|
||||
assertEquals(List.of("YD-20261023-0012"), duplicates.get(0).getWaybillNos());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("数量不同:不判为重复")
|
||||
void ignoresDifferentQuantity() {
|
||||
stubCandidates(waybill("YD-20261023-0012"));
|
||||
Map<String, Object> row = importRow();
|
||||
row.put("quantity", "21");
|
||||
|
||||
assertTrue(service.checkDuplicates(request(row)).isEmpty());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("车牌大小写与空格不同:仍判为重复")
|
||||
void ignoresPlateFormatting() {
|
||||
stubCandidates(waybill("YD-20261023-0012"));
|
||||
Map<String, Object> row = importRow();
|
||||
row.put("vehicleNo", " 桂a5d61n ");
|
||||
|
||||
assertEquals(1, service.checkDuplicates(request(row)).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("历史运单没有承运商档案 ID:退化为承运商名称比对")
|
||||
void matchesByCarrierNameWhenHistoryHasNoId() {
|
||||
Waybill waybill = waybill("YD-20261023-0012");
|
||||
waybill.setCarrierId(null);
|
||||
stubCandidates(waybill);
|
||||
|
||||
assertEquals(1, service.checkDuplicates(request(importRow())).size());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("项目为空:直接返回空,不查库")
|
||||
void returnsEmptyWithoutProject() {
|
||||
WaybillImportBatchRequest request = request(importRow());
|
||||
request.setProjectId(null);
|
||||
|
||||
assertTrue(service.checkDuplicates(request).isEmpty());
|
||||
}
|
||||
|
||||
private void stubCandidates(Waybill... waybills) {
|
||||
when(waybillService.list(any(Wrapper.class))).thenReturn(List.of(waybills));
|
||||
}
|
||||
|
||||
private WaybillImportBatchRequest request(Map<String, Object> row) {
|
||||
WaybillImportBatchRequest request = new WaybillImportBatchRequest();
|
||||
request.setProjectId(PROJECT_ID);
|
||||
request.setCarrierId(CARRIER_ID);
|
||||
request.setCarrierName(CARRIER_NAME);
|
||||
request.setRows(List.of(row));
|
||||
return request;
|
||||
}
|
||||
|
||||
private Map<String, Object> importRow() {
|
||||
Map<String, Object> row = new HashMap<>();
|
||||
row.put("vehicleNo", VEHICLE_NO);
|
||||
// 无 Spring 上下文时字典读不到,运输方式按“名称即键”兜底,故此处直接给内部键。
|
||||
row.put("transportType", "road");
|
||||
row.put("departureAddress", DEPARTURE);
|
||||
row.put("arrivalAddress", ARRIVAL);
|
||||
row.put("cargoName", CARGO_NAME);
|
||||
row.put("cargoType", "煤制品");
|
||||
row.put("quantity", "20");
|
||||
row.put("quantityUnit", "吨");
|
||||
row.put("actualStartDate", "2026-10-13");
|
||||
row.put("actualEndDate", "2026-10-14");
|
||||
return row;
|
||||
}
|
||||
|
||||
private Waybill waybill(String waybillNo) {
|
||||
Waybill waybill = new Waybill();
|
||||
waybill.setWaybillNo(waybillNo);
|
||||
waybill.setProjectId(PROJECT_ID);
|
||||
waybill.setCarrierId(CARRIER_ID);
|
||||
waybill.setCarrierName(CARRIER_NAME);
|
||||
waybill.setVehicleNo(VEHICLE_NO);
|
||||
waybill.setTransportType("road");
|
||||
waybill.setDepartureAddress(DEPARTURE);
|
||||
waybill.setArrivalAddress(ARRIVAL);
|
||||
waybill.setCargoName(CARGO_NAME);
|
||||
waybill.setCargoType("煤制品");
|
||||
waybill.setQuantity(new BigDecimal("20.00"));
|
||||
waybill.setQuantityUnit("吨");
|
||||
waybill.setStartDate(LocalDate.of(2026, 10, 13));
|
||||
waybill.setEndDate(LocalDate.of(2026, 10, 14));
|
||||
return waybill;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user