From e7d7e618468cb0c0256567b19114f3e5998703d4 Mon Sep 17 00:00:00 2001 From: b2894lxlx <517289602@qq.com> Date: Thu, 10 Sep 2026 02:01:51 +0800 Subject: [PATCH] =?UTF-8?q?1=E3=80=81=E8=B0=83=E6=95=B4=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E8=BF=90=E5=8D=95=202=E3=80=81=E8=B0=83=E6=95=B4=E5=AF=B9?= =?UTF-8?q?=E8=B4=A6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/VoucherManageChangeBatchRequest.java | 29 ++ .../transport/pojo/entity/VoucherManage.java | 2 + .../controller/ProcessConfigController.java | 168 ++++++++++- .../controller/VoucherManageController.java | 23 +- .../mapper/VoucherWaybillBatchMapper.java | 6 +- .../mapper/VoucherWaybillBatchMapper.xml | 2 + .../service/IVoucherManageService.java | 2 + .../impl/VoucherManageServiceImpl.java | 271 +++++++++++++++--- .../blade_voucher_manage_20260812.sql | 1 + ..._voucher_manage_reject_reason_20260909.sql | 25 ++ .../blade_voucher_manage_upgrade_20260812.sql | 3 + 11 files changed, 486 insertions(+), 46 deletions(-) create mode 100644 blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/VoucherManageChangeBatchRequest.java create mode 100644 doc/sql/transport/blade_voucher_manage_reject_reason_20260909.sql diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/VoucherManageChangeBatchRequest.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/VoucherManageChangeBatchRequest.java new file mode 100644 index 0000000..83e47a2 --- /dev/null +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/dto/VoucherManageChangeBatchRequest.java @@ -0,0 +1,29 @@ +/** + * BladeX Commercial License Agreement + * Copyright (c) 2018-2099, https://bladex.cn. All rights reserved. + */ +package org.springblade.transport.pojo.dto; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + +import java.io.Serial; +import java.io.Serializable; +import java.util.List; + +/** + * 凭证更换运单批次请求。 + */ +@Data +@Schema(description = "凭证更换运单批次请求") +public class VoucherManageChangeBatchRequest implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + @Schema(description = "凭证批次ID") + private Long voucherId; + + @Schema(description = "运单导入批次对应的运单ID") + private List waybillImportBatchIds; +} diff --git a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/VoucherManage.java b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/VoucherManage.java index caf41a8..eed2006 100644 --- a/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/VoucherManage.java +++ b/blade-service-api/blade-transport-api/src/main/java/org/springblade/transport/pojo/entity/VoucherManage.java @@ -32,4 +32,6 @@ public class VoucherManage extends TenantEntity { private Integer relatedWaybillCount; private Integer unRelatedWaybillCount; private String auditStatus; + @Schema(description = "审核驳回原因") + private String rejectReason; } diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProcessConfigController.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProcessConfigController.java index 8ff1c0c..8df54de 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProcessConfigController.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/controller/ProcessConfigController.java @@ -40,11 +40,15 @@ import org.springblade.core.tool.api.R; import org.springblade.core.tool.utils.DateUtil; import org.springblade.core.tool.utils.Func; import org.springblade.transport.excel.ProcessConfigExportExcel; +import org.springblade.transport.mapper.VoucherFileMapper; import org.springblade.transport.mapper.VoucherImageMapper; import org.springblade.transport.mapper.VoucherManageMapper; +import org.springblade.transport.mapper.WaybillMapper; import org.springblade.transport.pojo.entity.ProcessConfig; +import org.springblade.transport.pojo.entity.VoucherFile; import org.springblade.transport.pojo.entity.VoucherImage; import org.springblade.transport.pojo.entity.VoucherManage; +import org.springblade.transport.pojo.entity.Waybill; import org.springblade.transport.pojo.vo.BusinessRemoveResultVO; import org.springblade.transport.pojo.vo.ProcessConfigVO; import org.springblade.transport.service.IProcessConfigService; @@ -60,6 +64,7 @@ import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Objects; import java.util.concurrent.TimeUnit; /** @@ -74,16 +79,21 @@ import java.util.concurrent.TimeUnit; public class ProcessConfigController extends BladeController { private final IProcessConfigService processConfigService; + private final WaybillMapper waybillMapper; + private final VoucherFileMapper voucherFileMapper; private final VoucherImageMapper voucherImageMapper; private final VoucherManageMapper voucherManageMapper; private final MinioClient minioClient; @Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") private String minioBucketName; - public ProcessConfigController(IProcessConfigService processConfigService, VoucherImageMapper voucherImageMapper, + public ProcessConfigController(IProcessConfigService processConfigService, WaybillMapper waybillMapper, + VoucherFileMapper voucherFileMapper, VoucherImageMapper voucherImageMapper, VoucherManageMapper voucherManageMapper, MinioClient minioClient) { this.processConfigService = processConfigService; + this.waybillMapper = waybillMapper; + this.voucherFileMapper = voucherFileMapper; this.voucherImageMapper = voucherImageMapper; this.voucherManageMapper = voucherManageMapper; this.minioClient = minioClient; @@ -107,15 +117,20 @@ public class ProcessConfigController extends BladeController { @ApiOperationSupport(order = 2) @Operation(summary = "查询运单已关联凭证图片") public R>> voucherImages( - @Parameter(description = "运单主键", required = true) @RequestParam Long waybillId) { + @Parameter(description = "运单主键", required = true) @RequestParam Long waybillId, + @Parameter(description = "凭证批次主键,点击文件夹时传入") @RequestParam(required = false) Long voucherId, + @Parameter(description = "文件夹名称,点击文件夹时传入") @RequestParam(required = false) String folderName) { List imageRecords = voucherImageMapper.selectList( new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() .eq(VoucherImage::getWaybillId, waybillId) .eq(VoucherImage::getMatched, 1) .eq(VoucherImage::getIsDeleted, 0) .orderByDesc(VoucherImage::getCreateTime)); + if (voucherId != null && Func.isNotEmpty(folderName)) { + return R.data(folderImages(waybillId, folderName)); + } List voucherIds = imageRecords.stream() - .map(VoucherImage::getVoucherId).filter(java.util.Objects::nonNull).distinct().toList(); + .map(VoucherImage::getVoucherId).filter(Objects::nonNull).distinct().toList(); Map voucherMap = voucherIds.isEmpty() ? Map.of() : voucherManageMapper.selectBatchIds(voucherIds).stream() .collect(java.util.stream.Collectors.toMap(VoucherManage::getId, item -> item, (left, right) -> left)); List> images = imageRecords.stream() @@ -124,7 +139,7 @@ public class ProcessConfigController extends BladeController { VoucherManage voucher = voucherMap.get(image.getVoucherId()); return voucher == null || !"承运商".equals(voucher.getUploadSource()) || "审核通过".equals(voucher.getAuditStatus()); - }).map(image -> { + }).map(image -> { Map result = new LinkedHashMap<>(); result.put("id", image.getId()); result.put("imageName", image.getImageName()); @@ -140,7 +155,150 @@ public class ProcessConfigController extends BladeController { } return result; }).toList(); - return R.data(images); + return R.data(images.isEmpty() ? fallbackVoucherFolders(waybillId) : images); + } + + private List> fallbackVoucherFolders(Long waybillId) { + Waybill waybill = waybillMapper.selectById(waybillId); + if (waybill == null || Func.isEmpty(waybill.getBatchNo())) { + return List.of(); + } + List vouchers = voucherManageMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(VoucherManage::getTenantId, waybill.getTenantId()) + .eq(VoucherManage::getIsDeleted, 0)); + Map> folderRecords = new LinkedHashMap<>(); + Map> folderVouchers = new LinkedHashMap<>(); + for (VoucherManage voucher : vouchers) { + if (!containsBatchNo(voucher.getWaybillBatchNo(), waybill.getBatchNo()) || !visibleVoucher(voucher)) { + continue; + } + Map> grouped = new LinkedHashMap<>(); + for (VoucherFile file : voucherFileMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(VoucherFile::getTenantId, voucher.getTenantId()) + .eq(VoucherFile::getVoucherId, voucher.getId()) + .eq(VoucherFile::getIsDeleted, 0))) { + if ("image".equals(file.getFileType())) { + String key = Func.isEmpty(file.getFolderName()) ? "未命名文件夹" : file.getFolderName(); + grouped.computeIfAbsent(key, ignored -> new ArrayList<>()).add(VoucherImageRecord.from(file)); + } + } + if (grouped.isEmpty()) { + for (VoucherImage image : voucherImageMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(VoucherImage::getTenantId, voucher.getTenantId()) + .eq(VoucherImage::getVoucherId, voucher.getId()) + .eq(VoucherImage::getIsDeleted, 0))) { + String key = Func.isEmpty(image.getPlateNo()) ? "未命名文件夹" : image.getPlateNo(); + grouped.computeIfAbsent(key, ignored -> new ArrayList<>()).add(VoucherImageRecord.from(image)); + } + } + for (Map.Entry> entry : grouped.entrySet()) { + folderRecords.computeIfAbsent(entry.getKey(), ignored -> new ArrayList<>()).addAll(entry.getValue()); + folderVouchers.computeIfAbsent(entry.getKey(), ignored -> new ArrayList<>()).add(voucher); + } + } + List> folders = new ArrayList<>(); + for (Map.Entry> entry : folderRecords.entrySet()) { + Map folder = new LinkedHashMap<>(); + folder.put("type", "folder"); + folder.put("isFolder", true); + folder.put("waybillId", waybillId); + folder.put("waybillBatchNo", waybill.getBatchNo()); + List relatedVouchers = folderVouchers.get(entry.getKey()); + folder.put("voucherId", relatedVouchers.get(0).getId()); + folder.put("voucherIds", relatedVouchers.stream().map(VoucherManage::getId).toList()); + folder.put("voucherBatchNo", relatedVouchers.get(0).getVoucherBatchNo()); + folder.put("voucherBatchNos", relatedVouchers.stream().map(VoucherManage::getVoucherBatchNo).toList()); + folder.put("folderName", entry.getKey()); + folder.put("plateNo", entry.getKey()); + folder.put("name", entry.getKey()); + folder.put("imageCount", entry.getValue().size()); + folder.put("icon", "/img/文件夹.png"); + folders.add(folder); + } + return folders; + } + + private List> folderImages(Long waybillId, String folderName) { + Waybill waybill = waybillMapper.selectById(waybillId); + if (waybill == null || Func.isEmpty(waybill.getBatchNo()) || Func.isEmpty(folderName)) { + return List.of(); + } + List vouchers = voucherManageMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(VoucherManage::getTenantId, waybill.getTenantId()) + .eq(VoucherManage::getIsDeleted, 0)); + Map recordMap = new LinkedHashMap<>(); + for (VoucherManage voucher : vouchers) { + if (!containsBatchNo(voucher.getWaybillBatchNo(), waybill.getBatchNo()) || !visibleVoucher(voucher)) { + continue; + } + List voucherRecords = voucherFileMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(VoucherFile::getTenantId, voucher.getTenantId()) + .eq(VoucherFile::getVoucherId, voucher.getId()) + .eq(VoucherFile::getFolderName, folderName) + .eq(VoucherFile::getFileType, "image") + .eq(VoucherFile::getIsDeleted, 0)).stream().map(VoucherImageRecord::from).toList(); + if (voucherRecords.isEmpty()) { + voucherRecords = voucherImageMapper.selectList( + new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper() + .eq(VoucherImage::getTenantId, voucher.getTenantId()) + .eq(VoucherImage::getVoucherId, voucher.getId()) + .eq(VoucherImage::getPlateNo, folderName) + .eq(VoucherImage::getIsDeleted, 0)).stream().map(VoucherImageRecord::from).toList(); + } + for (VoucherImageRecord record : voucherRecords) { + String key = record.id() != null ? String.valueOf(record.id()) : record.objectKey(); + recordMap.putIfAbsent(key, record); + } + } + return recordMap.values().stream().map(this::imageMap).toList(); + } + + private Map imageMap(VoucherImageRecord image) { + Map result = new LinkedHashMap<>(); + result.put("type", "image"); + result.put("isFolder", false); + result.put("id", image.id()); + result.put("imageName", image.fileName()); + result.put("voucherBatchNo", image.voucherBatchNo()); + result.put("waybillNo", image.waybillNo()); + result.put("folderName", image.folderName()); + result.put("objectKey", image.objectKey()); + result.put("matched", image.matched()); + try { + result.put("url", minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder() + .method(Method.GET).bucket(minioBucketName).object(image.objectKey()) + .expiry(1, TimeUnit.HOURS).build())); + } catch (Exception exception) { + throw new IllegalStateException("生成凭证图片预览地址失败", exception); + } + return result; + } + + private boolean visibleVoucher(VoucherManage voucher) { + return voucher == null || !"承运商".equals(voucher.getUploadSource()) || "审核通过".equals(voucher.getAuditStatus()); + } + + private boolean containsBatchNo(String batchNos, String batchNo) { + return Func.isNotEmpty(batchNos) && Func.isNotEmpty(batchNo) + && java.util.Arrays.stream(batchNos.split(",")).map(String::trim).anyMatch(batchNo::equals); + } + + private record VoucherImageRecord(Long id, String voucherBatchNo, String waybillNo, String fileName, + String folderName, String objectKey, Integer matched) { + private static VoucherImageRecord from(VoucherFile file) { + return new VoucherImageRecord(file.getId(), file.getVoucherBatchNo(), file.getWaybillNo(), file.getFileName(), + file.getFolderName(), file.getObjectKey(), file.getMatched()); + } + + private static VoucherImageRecord from(VoucherImage image) { + return new VoucherImageRecord(image.getId(), image.getVoucherBatchNo(), image.getWaybillNo(), image.getImageName(), + image.getPlateNo(), image.getObjectKey(), image.getMatched()); + } } @GetMapping("/list") 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 930b171..b7e1ef2 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 @@ -10,6 +10,7 @@ import org.springblade.core.mp.support.Condition; import org.springblade.core.mp.support.Query; import org.springblade.core.secure.annotation.PreAuth; import org.springblade.core.tool.api.R; +import org.springblade.transport.pojo.dto.VoucherManageChangeBatchRequest; import org.springblade.transport.pojo.dto.VoucherManageSubmitRequest; import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest; import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest; @@ -70,12 +71,12 @@ public class VoucherManageController extends BladeController { } @PostMapping("/folder-replace-object") - @Operation(summary = "替换单个车牌凭证(系统文件上传后处理)") + @Operation(summary = "替换单个车牌凭证(分片上传文件处理)") public R replaceFolderByObject(@RequestParam Long voucherId, @RequestParam String plateNo, @RequestParam String objectKey, @RequestParam String fileName, @RequestParam(required = false) Long size, @RequestParam(required = false) String contentType) { voucherManageService.replaceFolderByObject(voucherId, plateNo, objectKey, fileName, size, contentType); - return R.success("上传成功"); + return R.success("处理成功"); } @PostMapping("/folder-remove") @@ -90,8 +91,16 @@ public class VoucherManageController extends BladeController { @Operation(summary = "上传凭证或更换运输批次") public R submit(@RequestBody VoucherManageSubmitRequest request) { voucherManageService.submit(request); return R.success("提交成功"); } - @PostMapping("/upload-draft") + @PostMapping("/change-waybill-batch") @ApiOperationSupport(order = 4) + @Operation(summary = "更换运单批次并重新匹配凭证") + public R changeWaybillBatch(@RequestBody VoucherManageChangeBatchRequest request) { + voucherManageService.changeWaybillBatch(request); + return R.success("运单批次更换成功"); + } + + @PostMapping("/upload-draft") + @ApiOperationSupport(order = 5) @Operation(summary = "创建凭证上传草稿") public R createUploadDraft(@RequestBody VoucherUploadDraftRequest request) { return R.data(voucherManageService.createUploadDraft(request)); @@ -112,12 +121,12 @@ public class VoucherManageController extends BladeController { } @PostMapping("/remove") - @ApiOperationSupport(order = 5) + @ApiOperationSupport(order = 6) @Operation(summary = "删除凭证") public R remove(@RequestParam Long id) { voucherManageService.removeVoucher(id); return R.success("删除成功"); } @GetMapping("/waybill-batches") - @ApiOperationSupport(order = 6) + @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, @@ -126,7 +135,7 @@ public class VoucherManageController extends BladeController { } @PostMapping("/audit-pass") - @ApiOperationSupport(order = 7) + @ApiOperationSupport(order = 8) @Operation(summary = "审核通过") public R auditPass(@RequestParam Long id) { voucherManageService.auditPass(id); @@ -134,7 +143,7 @@ public class VoucherManageController extends BladeController { } @PostMapping("/audit-reject") - @ApiOperationSupport(order = 8) + @ApiOperationSupport(order = 9) @Operation(summary = "审核驳回") public R auditReject(@RequestParam Long id, @RequestParam(required = false) String rejectReason) { voucherManageService.auditReject(id, rejectReason); 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 058a504..86e7a57 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 @@ -14,6 +14,8 @@ public interface VoucherWaybillBatchMapper extends BaseMapper> 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); - List> selectWaybillBatchesByIds(@Param("tenantId") String tenantId, @Param("ids") List ids); + @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 624250b..2eadd45 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 @@ -8,6 +8,7 @@ FROM blade_waybill w LEFT JOIN blade_user u ON u.id = w.create_user WHERE w.is_deleted = 0 AND w.tenant_id = #{tenantId} + AND w.carrier_name = #{carrierName} AND w.batch_no LIKE CONCAT('%', #{batchNo}, '%') AND w.create_user = #{createUser} AND w.create_time =]]> #{createTimeStart} @@ -21,6 +22,7 @@ FROM blade_waybill w WHERE w.is_deleted = 0 AND w.tenant_id = #{tenantId} AND w.id IN #{id} + AND w.carrier_name = #{carrierName} GROUP BY COALESCE(NULLIF(w.batch_no, ''), '未分批运单') diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVoucherManageService.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVoucherManageService.java index 9529647..92c253a 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVoucherManageService.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/IVoucherManageService.java @@ -2,6 +2,7 @@ package org.springblade.transport.service; import com.baomidou.mybatisplus.core.metadata.IPage; import org.springblade.core.mp.base.BaseService; +import org.springblade.transport.pojo.dto.VoucherManageChangeBatchRequest; import org.springblade.transport.pojo.dto.VoucherManageSubmitRequest; import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest; import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest; @@ -22,6 +23,7 @@ public interface IVoucherManageService extends BaseService { void replaceFolderByObject(Long voucherId, String plateNo, String objectKey, String fileName, Long size, String contentType); void removeFolder(Long voucherId, String plateNo); void submit(VoucherManageSubmitRequest request); + void changeWaybillBatch(VoucherManageChangeBatchRequest request); VoucherManage createUploadDraft(VoucherUploadDraftRequest request); void completeUploadFile(VoucherFileCompleteRequest request); void processUploadedVoucher(Long voucherId); diff --git a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java index ef88a08..3b4d519 100644 --- a/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java +++ b/blade-service/blade-transport/src/main/java/org/springblade/transport/service/impl/VoucherManageServiceImpl.java @@ -14,6 +14,7 @@ import org.springblade.transport.mapper.VoucherImageMapper; import org.springblade.transport.mapper.VoucherFileMapper; import org.springblade.transport.mapper.VoucherWaybillBatchMapper; import org.springblade.transport.event.VoucherUploadCompletedEvent; +import org.springblade.transport.pojo.dto.VoucherManageChangeBatchRequest; import org.springblade.transport.pojo.dto.VoucherManageSubmitRequest; import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest; import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest; @@ -101,7 +102,17 @@ public class VoucherManageServiceImpl extends BaseServiceImpl selectPage(IPage page, VoucherManageVO query) { - return VoucherManageWrapper.build().pageVO(page(page, buildQuery(query))); + IPage result = VoucherManageWrapper.build().pageVO(page(page, buildQuery(query))); + // 凭证数量按文件夹(车牌目录)统计,兼容历史记录中仍保存图片数量的旧数据。 + if (result.getRecords() != null) { + result.getRecords().forEach(item -> { + VoucherFolderCounts folderCounts = countVoucherFolderRelations(item.getTenantId(), item.getId()); + item.setVoucherCount(folderCounts.total()); + item.setRelatedWaybillCount(folderCounts.related()); + item.setUnRelatedWaybillCount(folderCounts.unrelated()); + }); + } + return result; } @Override @@ -154,9 +165,13 @@ public class VoucherManageServiceImpl extends BaseServiceImpl 50L * 1024 * 1024) throw new ServiceException("凭证文件大小不能超过50M"); + if (enforceSizeLimit && uploadFile.getSize() > 50L * 1024 * 1024) throw new ServiceException("凭证文件大小不能超过50M"); String normalizedPlateNo = normalizePlateNo(plateNo); if (Func.isEmpty(normalizedPlateNo)) throw new ServiceException("车牌号不能为空"); validateMinioConfig(); @@ -200,26 +215,135 @@ public class VoucherManageServiceImpl extends BaseServiceImpl candidateObjectKeys = resolvedObjectKey.equals(objectKey) + ? List.of(objectKey) : List.of(resolvedObjectKey, objectKey); + for (String candidateObjectKey : candidateObjectKeys) { + try (InputStream source = minioClient.getObject(GetObjectArgs.builder().bucket(minioBucketName).object(candidateObjectKey).build()); + OutputStream target = Files.newOutputStream(archivePath)) { + source.transferTo(target); + return archivePath; + } catch (Exception exception) { + lastException = exception; + log.warn("读取系统上传对象失败,尝试下一个对象路径 bucket={}, objectKey={}", minioBucketName, candidateObjectKey, exception); + } + } + deleteTempArchive(archivePath); + log.error("读取系统上传文件失败 bucket={}, objectKey={}, candidateObjectKeys={}", + minioBucketName, objectKey, candidateObjectKeys, lastException); + throw new ServiceException("读取系统上传文件失败"); + } + + private String resolveMinioObjectKey(String objectKey) { + if (Func.isEmpty(minioRootDirectory) || Func.isEmpty(objectKey)) { + return objectKey; + } + String normalizedRoot = minioRootDirectory.endsWith("/") + ? minioRootDirectory.substring(0, minioRootDirectory.length() - 1) : minioRootDirectory; + return objectKey.equals(normalizedRoot) || objectKey.startsWith(normalizedRoot + "/") + ? objectKey : normalizedRoot + "/" + objectKey; + } + + private void deleteSourceObjectQuietly(String objectKey) { + deleteObjectQuietly(resolveMinioObjectKey(objectKey)); + } + + private MultipartFile localArchiveMultipartFile(Path archivePath, String fileName, String contentType) throws java.io.IOException { + long archiveSize = Files.size(archivePath); + return new MultipartFile() { @Override public String getName() { return "file"; } @Override public String getOriginalFilename() { return fileName; } - @Override public String getContentType() { return contentType; } - @Override public boolean isEmpty() { return size != null && size == 0; } - @Override public long getSize() { return size == null ? -1L : size; } - @Override public byte[] getBytes() throws java.io.IOException { try (InputStream input = getInputStream()) { return input.readAllBytes(); } } - @Override public InputStream getInputStream() throws java.io.IOException { - try { return minioClient.getObject(GetObjectArgs.builder().bucket(minioBucketName).object(objectKey).build()); } - catch (Exception exception) { throw new java.io.IOException("读取系统上传文件失败", exception); } + @Override public String getContentType() { return Func.isEmpty(contentType) ? "application/zip" : contentType; } + @Override public boolean isEmpty() { return archiveSize == 0; } + @Override public long getSize() { return archiveSize; } + @Override public byte[] getBytes() throws java.io.IOException { return Files.readAllBytes(archivePath); } + @Override public InputStream getInputStream() throws java.io.IOException { return Files.newInputStream(archivePath); } + @Override public void transferTo(java.io.File destination) throws java.io.IOException { + try (InputStream input = getInputStream(); OutputStream output = Files.newOutputStream(destination.toPath())) { + input.transferTo(output); + } } - @Override public void transferTo(java.io.File dest) throws java.io.IOException { try (InputStream input = getInputStream(); OutputStream output = Files.newOutputStream(dest.toPath())) { input.transferTo(output); } } }; - try { - replaceFolder(voucherId, plateNo, uploadedFile); - } finally { - // 系统上传接口产生的临时附件仅用于本次替换,复制到凭证目录后清理源对象。 - deleteObjectQuietly(objectKey); + } + + private String resolveReplacementPlateNo(VoucherManage voucher, String requestedPlateNo, String archiveFileName, Path archivePath) throws Exception { + String normalizedRequested = normalizePlateNo(requestedPlateNo); + Set existingPlates = new HashSet<>(); + voucherFileMapper.selectList(Wrappers.lambdaQuery() + .eq(VoucherFile::getTenantId, voucher.getTenantId()).eq(VoucherFile::getVoucherId, voucher.getId()) + .eq(VoucherFile::getIsDeleted, 0)).forEach(file -> addPlateNumber(existingPlates, file.getPlateNo())); + voucherImageMapper.selectList(Wrappers.lambdaQuery() + .eq(VoucherImage::getTenantId, voucher.getTenantId()).eq(VoucherImage::getVoucherId, voucher.getId()) + .eq(VoucherImage::getIsDeleted, 0)).forEach(image -> addPlateNumber(existingPlates, image.getPlateNo())); + Map waybillByPlate = new HashMap<>(); + for (Waybill waybill : listRelatedWaybills(voucher)) { + for (String waybillPlateNo : waybillPlateNumbers(waybill)) { + waybillByPlate.putIfAbsent(waybillPlateNo, waybill); + } } + if (Func.isNotEmpty(normalizedRequested) && waybillByPlate.containsKey(normalizedRequested)) { + return normalizedRequested; + } + String archiveNamePlate = normalizePlateNo(archiveFileName.replaceFirst("(?i)\\.zip$", "")); + if (Func.isNotEmpty(archiveNamePlate) && (waybillByPlate.containsKey(archiveNamePlate) || existingPlates.contains(archiveNamePlate))) { + return archiveNamePlate; + } + Charset archiveCharset = detectArchiveCharset(archivePath); + Set archivePlates = new HashSet<>(); + try (InputStream source = Files.newInputStream(archivePath); + ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(source), archiveCharset)) { + ZipEntry entry; + while ((entry = zipInputStream.getNextEntry()) != null) { + if (entry.isDirectory()) continue; + List pathParts = Arrays.stream(entry.getName().replace('\\', '/').split("/")) + .filter(Func::isNotEmpty).toList(); + if (pathParts.isEmpty()) continue; + String folderName = resolvePlateFolderName(pathParts, waybillByPlate); + String archivePlate = normalizePlateNo(folderName); + if (Func.isNotEmpty(archivePlate)) archivePlates.add(archivePlate); + } + } + Set matchedArchivePlates = archivePlates.stream().filter(waybillByPlate::containsKey).collect(Collectors.toSet()); + if (matchedArchivePlates.size() == 1) return matchedArchivePlates.iterator().next(); + Set existingArchivePlates = archivePlates.stream().filter(existingPlates::contains).collect(Collectors.toSet()); + if (existingArchivePlates.size() == 1) return existingArchivePlates.iterator().next(); + if (archivePlates.size() == 1 && !"凭证导入".equals(archivePlates.iterator().next())) { + return archivePlates.iterator().next(); + } + if (Func.isNotEmpty(normalizedRequested)) return normalizedRequested; + throw new ServiceException("无法从压缩包目录识别车牌号"); } @Override @@ -448,16 +572,40 @@ public class VoucherManageServiceImpl extends BaseServiceImpl images = voucherImageMapper.selectList(Wrappers.lambdaQuery() - .eq(VoucherImage::getTenantId, voucher.getTenantId()).eq(VoucherImage::getVoucherId, voucher.getId()).eq(VoucherImage::getIsDeleted, 0)); - long relatedWaybillCount = images.stream().filter(item -> Objects.equals(item.getMatched(), 1)) - .map(VoucherImage::getWaybillId).filter(Objects::nonNull).distinct().count(); - int waybillCount = listRelatedWaybills(voucher).size(); - VoucherManage update = new VoucherManage(); update.setId(voucher.getId()); update.setVoucherCount(images.size()); - update.setRelatedWaybillCount((int) relatedWaybillCount); update.setUnRelatedWaybillCount(Math.max(waybillCount - (int) relatedWaybillCount, 0)); + VoucherFolderCounts folderCounts = countVoucherFolderRelations(voucher.getTenantId(), voucher.getId()); + VoucherManage update = new VoucherManage(); update.setId(voucher.getId()); update.setVoucherCount(folderCounts.total()); + update.setRelatedWaybillCount(folderCounts.related()); update.setUnRelatedWaybillCount(folderCounts.unrelated()); update.setProcessStatus("处理完成"); updateById(update); } + private int countVoucherFolders(VoucherManage voucher) { + return countVoucherFolderRelations(voucher.getTenantId(), voucher.getId()).total(); + } + + private VoucherFolderCounts countVoucherFolderRelations(String tenantId, Long voucherId) { + List files = voucherFileMapper.selectList(Wrappers.lambdaQuery() + .eq(VoucherFile::getTenantId, tenantId).eq(VoucherFile::getVoucherId, voucherId).eq(VoucherFile::getIsDeleted, 0)); + if (!files.isEmpty()) { + Set folders = files.stream().map(item -> folderKey(item.getPlateNo())).collect(Collectors.toSet()); + Set relatedFolders = files.stream().filter(item -> Objects.equals(item.getMatched(), 1)) + .map(item -> folderKey(item.getPlateNo())).collect(Collectors.toSet()); + return new VoucherFolderCounts(folders.size(), relatedFolders.size(), folders.size() - relatedFolders.size()); + } + List images = voucherImageMapper.selectList(Wrappers.lambdaQuery() + .eq(VoucherImage::getTenantId, tenantId).eq(VoucherImage::getVoucherId, voucherId).eq(VoucherImage::getIsDeleted, 0)); + Set folders = images.stream().map(item -> folderKey(item.getPlateNo())).collect(Collectors.toSet()); + Set relatedFolders = images.stream().filter(item -> Objects.equals(item.getMatched(), 1)) + .map(item -> folderKey(item.getPlateNo())).collect(Collectors.toSet()); + return new VoucherFolderCounts(folders.size(), relatedFolders.size(), folders.size() - relatedFolders.size()); + } + + private record VoucherFolderCounts(int total, int related, int unrelated) {} + + private String folderKey(String plateNo) { + String normalizedPlateNo = normalizePlateNo(plateNo); + return Func.isEmpty(normalizedPlateNo) ? "未识别车牌" : normalizedPlateNo; + } + private void deleteObjectQuietly(String objectKey) { try { minioClient.removeObject(RemoveObjectArgs.builder().bucket(minioBucketName).object(objectKey).build()); } catch (Exception exception) { log.warn("删除凭证对象失败 objectKey={}", objectKey, exception); } } @Override @@ -478,6 +626,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl> batches = selectableWaybillBatchesByIds(request.getWaybillImportBatchIds()); + if (batches.size() != request.getWaybillImportBatchIds().size()) throw new ServiceException("存在无效的运输批次"); + + // 更换批次必须同步重建关联关系,否则后续匹配仍会读取旧批次。 + voucherWaybillBatchMapper.deletePhysicalByVoucherId(voucher.getId()); + List relations = new ArrayList<>(); + for (Map batch : batches) { + VoucherWaybillBatch relation = new VoucherWaybillBatch(); + relation.setVoucherId(voucher.getId()); + relation.setWaybillImportBatchId(((Number) batch.get("id")).longValue()); + relation.setWaybillBatchNo((String) batch.get("batchNo")); + relation.setWaybillCount(((Number) batch.get("waybillCount")).intValue()); + relations.add(relation); + } + relations.forEach(voucherWaybillBatchMapper::insert); + voucher.setWaybillBatchNo(relations.stream().map(VoucherWaybillBatch::getWaybillBatchNo).distinct().collect(Collectors.joining(","))); + voucher.setRelatedWaybillCount(0); + voucher.setUnRelatedWaybillCount(0); + voucher.setAuditStatus("-"); + voucher.setRejectReason(""); + if (Func.isEmpty(voucher.getFileUrl())) { + // 文件尚未上传完成时只保存批次关系,待上传完成后再执行匹配。 + voucher.setProcessStatus("上传中"); + updateById(voucher); + return; + } + voucher.setProcessStatus("处理中"); + updateById(voucher); + + // 事务提交后重新读取凭证文件并按新批次执行车牌匹配。 + eventPublisher.publishEvent(new VoucherUploadCompletedEvent(voucher.getId())); + } + @Override @Transactional(rollbackFor = Exception.class) public void completeUploadFile(VoucherFileCompleteRequest request) { @@ -697,17 +886,19 @@ public class VoucherManageServiceImpl extends BaseServiceImpl> selectableWaybillBatches(IPage page, String batchNo, String createUser, Integer waybillCount, String createTimeStart, String createTimeEnd) { - return voucherWaybillBatchMapper.selectVoucherWaybillBatchPage(page, AuthUtil.getTenantId(), batchNo, createUser, waybillCount, createTimeStart, createTimeEnd); + return voucherWaybillBatchMapper.selectVoucherWaybillBatchPage(page, AuthUtil.getTenantId(), batchNo, createUser, waybillCount, createTimeStart, createTimeEnd, + !AuthUtil.isAdministrator(), currentCarrierName()); } private List> selectableWaybillBatchesByIds(List ids) { if (Func.isEmpty(ids)) return List.of(); - return voucherWaybillBatchMapper.selectWaybillBatchesByIds(AuthUtil.getTenantId(), ids); + return voucherWaybillBatchMapper.selectWaybillBatchesByIds(AuthUtil.getTenantId(), ids, !AuthUtil.isAdministrator(), currentCarrierName()); + } + + /** + * 普通用户只能关联承运商名称与其当前组织名称一致的运单批次;超级管理员不受此限制。 + */ + private String currentCarrierName() { + if (AuthUtil.isAdministrator()) return null; + Long currentDeptId = Func.firstLong(AuthUtil.getDeptId()); + return getOrganizationName(currentDeptId); } private List listRelatedWaybills(VoucherManage voucher) { @@ -1029,6 +1230,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl 200) { + throw new ServiceException("驳回原因不能超过200字"); + } voucher.setAuditStatus("审核驳回"); + voucher.setRejectReason(Func.isEmpty(reason) ? "" : reason); updateById(voucher); log.info("凭证批次审核驳回 id={}, voucherBatchNo={}, operator={}, reason={}", - id, voucher.getVoucherBatchNo(), AuthUtil.getUserId(), rejectReason); + id, voucher.getVoucherBatchNo(), AuthUtil.getUserId(), reason); } /** diff --git a/doc/sql/transport/blade_voucher_manage_20260812.sql b/doc/sql/transport/blade_voucher_manage_20260812.sql index 071fb70..2ec6872 100644 --- a/doc/sql/transport/blade_voucher_manage_20260812.sql +++ b/doc/sql/transport/blade_voucher_manage_20260812.sql @@ -15,6 +15,7 @@ CREATE TABLE IF NOT EXISTS `blade_voucher_manage` ( `related_waybill_count` int NOT NULL DEFAULT 0 COMMENT '已关联运单数', `un_related_waybill_count` int NOT NULL DEFAULT 0 COMMENT '未关联运单数', `audit_status` varchar(20) NOT NULL DEFAULT '-' COMMENT '待审核、审核通过、审核驳回', + `reject_reason` varchar(200) DEFAULT NULL COMMENT '审核驳回原因', `create_user` bigint DEFAULT NULL, `create_dept` bigint DEFAULT NULL, `create_time` datetime DEFAULT NULL, `update_user` bigint DEFAULT NULL, `update_time` datetime DEFAULT NULL, `status` tinyint NOT NULL DEFAULT 1, `is_deleted` tinyint NOT NULL DEFAULT 0, diff --git a/doc/sql/transport/blade_voucher_manage_reject_reason_20260909.sql b/doc/sql/transport/blade_voucher_manage_reject_reason_20260909.sql new file mode 100644 index 0000000..89d7014 --- /dev/null +++ b/doc/sql/transport/blade_voucher_manage_reject_reason_20260909.sql @@ -0,0 +1,25 @@ +-- 凭证管理补充审核驳回原因 +-- 日期:2026-09-09 + +DELIMITER $$ + +DROP PROCEDURE IF EXISTS `upgrade_blade_voucher_manage_reject_reason_20260909`$$ +CREATE PROCEDURE `upgrade_blade_voucher_manage_reject_reason_20260909`() +BEGIN + DECLARE db_name varchar(128); + SET db_name = DATABASE(); + + IF NOT EXISTS (SELECT 1 FROM information_schema.columns + WHERE table_schema = db_name + AND table_name = 'blade_voucher_manage' + AND column_name = 'reject_reason') THEN + ALTER TABLE `blade_voucher_manage` + ADD COLUMN `reject_reason` varchar(200) DEFAULT NULL COMMENT '审核驳回原因' + AFTER `audit_status`; + END IF; +END$$ + +CALL `upgrade_blade_voucher_manage_reject_reason_20260909`()$$ +DROP PROCEDURE `upgrade_blade_voucher_manage_reject_reason_20260909`$$ + +DELIMITER ; diff --git a/doc/sql/transport/blade_voucher_manage_upgrade_20260812.sql b/doc/sql/transport/blade_voucher_manage_upgrade_20260812.sql index 44cc8d0..76a4404 100644 --- a/doc/sql/transport/blade_voucher_manage_upgrade_20260812.sql +++ b/doc/sql/transport/blade_voucher_manage_upgrade_20260812.sql @@ -54,6 +54,9 @@ BEGIN IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'audit_status') THEN ALTER TABLE `blade_voucher_manage` ADD COLUMN `audit_status` varchar(20) NOT NULL DEFAULT '-' COMMENT '待审核、审核通过、审核驳回'; END IF; + IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'reject_reason') THEN + ALTER TABLE `blade_voucher_manage` ADD COLUMN `reject_reason` varchar(200) DEFAULT NULL COMMENT '审核驳回原因'; + END IF; IF NOT EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema = db_name AND table_name = 'blade_voucher_manage' AND column_name = 'create_user') THEN ALTER TABLE `blade_voucher_manage` ADD COLUMN `create_user` bigint DEFAULT NULL COMMENT '创建人'; END IF;