From ec4e8fe83b93249cdb8ab959061da995cd23bb57 Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Fri, 6 Nov 2026 07:29:59 +0800 Subject: [PATCH 1/2] =?UTF-8?q?:sparkles:=20=E8=BF=90=E5=8D=95=E6=89=B9?= =?UTF-8?q?=E9=87=8F=E5=AF=BC=E5=85=A5=E6=96=B0=E5=A2=9E=E4=B8=8E=E5=B7=B2?= =?UTF-8?q?=E6=9C=89=E8=BF=90=E5=8D=95=E9=87=8D=E5=A4=8D=E6=AF=94=E5=AF=B9?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../pojo/vo/WaybillImportDuplicateVO.java | 28 +++ .../controller/WaybillController.java | 8 + .../service/IWaybillImportBatchService.java | 6 + .../impl/WaybillImportBatchServiceImpl.java | 91 ++++++++++ .../impl/WaybillImportDuplicateTest.java | 163 ++++++++++++++++++ 5 files changed, 296 insertions(+) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportDuplicateVO.java create mode 100644 blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillImportDuplicateTest.java diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportDuplicateVO.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportDuplicateVO.java new file mode 100644 index 00000000..66dc148a --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/vo/WaybillImportDuplicateVO.java @@ -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 waybillNos = new ArrayList<>(); +} diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java index 30af8573..c2c66d05 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/WaybillController.java @@ -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> checkImportDuplicates(@RequestBody WaybillImportBatchRequest request) { + return R.data(waybillImportBatchService.checkDuplicates(request)); + } + @PostMapping("/import-batch/confirm") @ApiOperationSupport(order = 7) @Operation(summary = "确认运单批量导入") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillImportBatchService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillImportBatchService.java index 58c60733..0d3e7536 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillImportBatchService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IWaybillImportBatchService.java @@ -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 saveDraft(WaybillImportBatchRequest request); @@ -18,4 +21,7 @@ public interface IWaybillImportBatchService extends BaseService page(IPage page, WaybillImportBatchRequest request); BusinessRemoveResultVO removeBatches(String ids); String nextBatchNo(); + + /** 逐行比对导入行与项目下已有运单,返回命中的运单号。 */ + List checkDuplicates(WaybillImportBatchRequest request); } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java index 5eabbc36..9e4c9e1b 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/WaybillImportBatchServiceImpl.java @@ -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 checkDuplicates(WaybillImportBatchRequest request) { + List duplicates = new ArrayList<>(); + if (request == null || Func.isEmpty(request.getRows()) || Func.isEmpty(request.getProjectId())) { + return duplicates; + } + List carrierIds = resolveCarrierIds(request); + String carrierName = resolveCarrierNames(carrierIds, request.getCarrierName()); + LambdaQueryWrapper wrapper = Wrappers.lambdaQuery() + .eq(Waybill::getIsDeleted, 0) + .eq(Waybill::getProjectId, request.getProjectId()); + if (Func.isNotEmpty(request.getId())) { + // 追加导入时,本批次已落库的运单不算历史运单。 + wrapper.ne(Waybill::getImportBatchId, request.getId()); + } + List candidates = waybillService.list(wrapper); + if (Func.isEmpty(candidates)) { + return duplicates; + } + List> rows = request.getRows(); + for (int index = 0; index < rows.size(); index++) { + List 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 row, Waybill waybill, List 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 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); diff --git a/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillImportDuplicateTest.java b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillImportDuplicateTest.java new file mode 100644 index 00000000..3a05e92e --- /dev/null +++ b/blade-service/blade-transport/src/test/java/org/springblade/transport/service/impl/WaybillImportDuplicateTest.java @@ -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; + +/** + * 批量导入行与历史运单的判重测试。 + *

+ * 口径为导入必填项逐一相同;历史手工运单的承运商档案 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 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 row = importRow(); + row.put("quantity", "21"); + + assertTrue(service.checkDuplicates(request(row)).isEmpty()); + } + + @Test + @DisplayName("车牌大小写与空格不同:仍判为重复") + void ignoresPlateFormatting() { + stubCandidates(waybill("YD-20261023-0012")); + Map 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 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 importRow() { + Map 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; + } +} From f7df68bdd651a4d40265e2df07212a2116eff563 Mon Sep 17 00:00:00 2001 From: weicw <594098497@qq.com> Date: Fri, 6 Nov 2026 08:10:37 +0800 Subject: [PATCH 2/2] =?UTF-8?q?:sparkles:=20=E5=87=AD=E8=AF=81=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E5=85=B3=E8=81=94=E8=BF=90=E5=8D=95=E6=89=B9=E6=AC=A1?= =?UTF-8?q?=E6=9F=A5=E8=AF=A2=E6=94=AF=E6=8C=81=E9=A1=B9=E7=9B=AE=E7=AD=9B?= =?UTF-8?q?=E9=80=89=E5=B9=B6=E8=BF=94=E5=9B=9E=E9=A1=B9=E7=9B=AE=E5=90=8D?= =?UTF-8?q?=E7=A7=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - waybill-batches 接口新增 projectId 筛选项 - 分页结果增加 projectName,同一批次跨多个项目时返回「多个项目」 --- .../transport/controller/VoucherManageController.java | 7 ++++--- .../transport/mapper/VoucherWaybillBatchMapper.java | 5 +++-- .../transport/mapper/VoucherWaybillBatchMapper.xml | 2 ++ .../transport/service/IVoucherManageService.java | 2 +- .../transport/service/impl/VoucherManageServiceImpl.java | 4 ++-- 5 files changed, 12 insertions(+), 8 deletions(-) diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/VoucherManageController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/VoucherManageController.java index 69f53eb0..abcee864 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/VoucherManageController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/VoucherManageController.java @@ -135,9 +135,10 @@ public class VoucherManageController extends BladeController { @ApiOperationSupport(order = 7) @Operation(summary = "可关联运输批次") public R>> waybillBatches(Query query, @RequestParam(required = false) String batchNo, - @RequestParam(required = false) String createUser, @RequestParam(required = false) Integer waybillCount, - @RequestParam(required = false) String createTimeStart, @RequestParam(required = false) String createTimeEnd) { - return R.data(voucherManageService.selectableWaybillBatches(Condition.getPage(query), batchNo, createUser, waybillCount, createTimeStart, createTimeEnd)); + @RequestParam(required = false) Long projectId, @RequestParam(required = false) String createUser, + @RequestParam(required = false) Integer waybillCount, @RequestParam(required = false) String createTimeStart, + @RequestParam(required = false) String createTimeEnd) { + return R.data(voucherManageService.selectableWaybillBatches(Condition.getPage(query), batchNo, projectId, createUser, waybillCount, createTimeStart, createTimeEnd)); } @PostMapping("/audit-pass") diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherWaybillBatchMapper.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherWaybillBatchMapper.java index 86e7a57e..7c30968e 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherWaybillBatchMapper.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherWaybillBatchMapper.java @@ -13,8 +13,9 @@ import org.apache.ibatis.annotations.Delete; public interface VoucherWaybillBatchMapper extends BaseMapper { @Delete("DELETE FROM blade_voucher_waybill_batch WHERE voucher_id = #{voucherId}") int deletePhysicalByVoucherId(@Param("voucherId") Long voucherId); - IPage> selectVoucherWaybillBatchPage(IPage page, @Param("tenantId") String tenantId, @Param("batchNo") String batchNo, @Param("createUser") String createUser, - @Param("waybillCount") Integer waybillCount, @Param("createTimeStart") String createTimeStart, @Param("createTimeEnd") String createTimeEnd, + IPage> selectVoucherWaybillBatchPage(IPage page, @Param("tenantId") String tenantId, @Param("batchNo") String batchNo, + @Param("projectId") Long projectId, @Param("createUser") String createUser, @Param("waybillCount") Integer waybillCount, + @Param("createTimeStart") String createTimeStart, @Param("createTimeEnd") String createTimeEnd, @Param("restrictCarrier") boolean restrictCarrier, @Param("carrierName") String carrierName); List> selectWaybillBatchesByIds(@Param("tenantId") String tenantId, @Param("ids") List ids, @Param("restrictCarrier") boolean restrictCarrier, @Param("carrierName") String carrierName); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherWaybillBatchMapper.xml b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherWaybillBatchMapper.xml index 2eadd452..4b599bb5 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherWaybillBatchMapper.xml +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/mapper/VoucherWaybillBatchMapper.xml @@ -4,12 +4,14 @@