1、调整导入运单
2、调整对账
This commit is contained in:
+29
@@ -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<Long> waybillImportBatchIds;
|
||||||
|
}
|
||||||
+2
@@ -32,4 +32,6 @@ public class VoucherManage extends TenantEntity {
|
|||||||
private Integer relatedWaybillCount;
|
private Integer relatedWaybillCount;
|
||||||
private Integer unRelatedWaybillCount;
|
private Integer unRelatedWaybillCount;
|
||||||
private String auditStatus;
|
private String auditStatus;
|
||||||
|
@Schema(description = "审核驳回原因")
|
||||||
|
private String rejectReason;
|
||||||
}
|
}
|
||||||
|
|||||||
+162
-4
@@ -40,11 +40,15 @@ import org.springblade.core.tool.api.R;
|
|||||||
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.transport.excel.ProcessConfigExportExcel;
|
import org.springblade.transport.excel.ProcessConfigExportExcel;
|
||||||
|
import org.springblade.transport.mapper.VoucherFileMapper;
|
||||||
import org.springblade.transport.mapper.VoucherImageMapper;
|
import org.springblade.transport.mapper.VoucherImageMapper;
|
||||||
import org.springblade.transport.mapper.VoucherManageMapper;
|
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.ProcessConfig;
|
||||||
|
import org.springblade.transport.pojo.entity.VoucherFile;
|
||||||
import org.springblade.transport.pojo.entity.VoucherImage;
|
import org.springblade.transport.pojo.entity.VoucherImage;
|
||||||
import org.springblade.transport.pojo.entity.VoucherManage;
|
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.BusinessRemoveResultVO;
|
||||||
import org.springblade.transport.pojo.vo.ProcessConfigVO;
|
import org.springblade.transport.pojo.vo.ProcessConfigVO;
|
||||||
import org.springblade.transport.service.IProcessConfigService;
|
import org.springblade.transport.service.IProcessConfigService;
|
||||||
@@ -60,6 +64,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.LinkedHashMap;
|
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.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -74,16 +79,21 @@ import java.util.concurrent.TimeUnit;
|
|||||||
public class ProcessConfigController extends BladeController {
|
public class ProcessConfigController extends BladeController {
|
||||||
|
|
||||||
private final IProcessConfigService processConfigService;
|
private final IProcessConfigService processConfigService;
|
||||||
|
private final WaybillMapper waybillMapper;
|
||||||
|
private final VoucherFileMapper voucherFileMapper;
|
||||||
private final VoucherImageMapper voucherImageMapper;
|
private final VoucherImageMapper voucherImageMapper;
|
||||||
private final VoucherManageMapper voucherManageMapper;
|
private final VoucherManageMapper voucherManageMapper;
|
||||||
private final MinioClient minioClient;
|
private final MinioClient minioClient;
|
||||||
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}")
|
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}")
|
||||||
private String minioBucketName;
|
private String minioBucketName;
|
||||||
|
|
||||||
public ProcessConfigController(IProcessConfigService processConfigService, VoucherImageMapper voucherImageMapper,
|
public ProcessConfigController(IProcessConfigService processConfigService, WaybillMapper waybillMapper,
|
||||||
|
VoucherFileMapper voucherFileMapper, VoucherImageMapper voucherImageMapper,
|
||||||
VoucherManageMapper voucherManageMapper,
|
VoucherManageMapper voucherManageMapper,
|
||||||
MinioClient minioClient) {
|
MinioClient minioClient) {
|
||||||
this.processConfigService = processConfigService;
|
this.processConfigService = processConfigService;
|
||||||
|
this.waybillMapper = waybillMapper;
|
||||||
|
this.voucherFileMapper = voucherFileMapper;
|
||||||
this.voucherImageMapper = voucherImageMapper;
|
this.voucherImageMapper = voucherImageMapper;
|
||||||
this.voucherManageMapper = voucherManageMapper;
|
this.voucherManageMapper = voucherManageMapper;
|
||||||
this.minioClient = minioClient;
|
this.minioClient = minioClient;
|
||||||
@@ -107,15 +117,20 @@ public class ProcessConfigController extends BladeController {
|
|||||||
@ApiOperationSupport(order = 2)
|
@ApiOperationSupport(order = 2)
|
||||||
@Operation(summary = "查询运单已关联凭证图片")
|
@Operation(summary = "查询运单已关联凭证图片")
|
||||||
public R<List<Map<String, Object>>> voucherImages(
|
public R<List<Map<String, Object>>> 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<VoucherImage> imageRecords = voucherImageMapper.selectList(
|
List<VoucherImage> imageRecords = voucherImageMapper.selectList(
|
||||||
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<VoucherImage>()
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<VoucherImage>()
|
||||||
.eq(VoucherImage::getWaybillId, waybillId)
|
.eq(VoucherImage::getWaybillId, waybillId)
|
||||||
.eq(VoucherImage::getMatched, 1)
|
.eq(VoucherImage::getMatched, 1)
|
||||||
.eq(VoucherImage::getIsDeleted, 0)
|
.eq(VoucherImage::getIsDeleted, 0)
|
||||||
.orderByDesc(VoucherImage::getCreateTime));
|
.orderByDesc(VoucherImage::getCreateTime));
|
||||||
|
if (voucherId != null && Func.isNotEmpty(folderName)) {
|
||||||
|
return R.data(folderImages(waybillId, folderName));
|
||||||
|
}
|
||||||
List<Long> voucherIds = imageRecords.stream()
|
List<Long> voucherIds = imageRecords.stream()
|
||||||
.map(VoucherImage::getVoucherId).filter(java.util.Objects::nonNull).distinct().toList();
|
.map(VoucherImage::getVoucherId).filter(Objects::nonNull).distinct().toList();
|
||||||
Map<Long, VoucherManage> voucherMap = voucherIds.isEmpty() ? Map.of() : voucherManageMapper.selectBatchIds(voucherIds).stream()
|
Map<Long, VoucherManage> voucherMap = voucherIds.isEmpty() ? Map.of() : voucherManageMapper.selectBatchIds(voucherIds).stream()
|
||||||
.collect(java.util.stream.Collectors.toMap(VoucherManage::getId, item -> item, (left, right) -> left));
|
.collect(java.util.stream.Collectors.toMap(VoucherManage::getId, item -> item, (left, right) -> left));
|
||||||
List<Map<String, Object>> images = imageRecords.stream()
|
List<Map<String, Object>> images = imageRecords.stream()
|
||||||
@@ -140,7 +155,150 @@ public class ProcessConfigController extends BladeController {
|
|||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}).toList();
|
}).toList();
|
||||||
return R.data(images);
|
return R.data(images.isEmpty() ? fallbackVoucherFolders(waybillId) : images);
|
||||||
|
}
|
||||||
|
|
||||||
|
private List<Map<String, Object>> fallbackVoucherFolders(Long waybillId) {
|
||||||
|
Waybill waybill = waybillMapper.selectById(waybillId);
|
||||||
|
if (waybill == null || Func.isEmpty(waybill.getBatchNo())) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<VoucherManage> vouchers = voucherManageMapper.selectList(
|
||||||
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<VoucherManage>()
|
||||||
|
.eq(VoucherManage::getTenantId, waybill.getTenantId())
|
||||||
|
.eq(VoucherManage::getIsDeleted, 0));
|
||||||
|
Map<String, List<VoucherImageRecord>> folderRecords = new LinkedHashMap<>();
|
||||||
|
Map<String, List<VoucherManage>> folderVouchers = new LinkedHashMap<>();
|
||||||
|
for (VoucherManage voucher : vouchers) {
|
||||||
|
if (!containsBatchNo(voucher.getWaybillBatchNo(), waybill.getBatchNo()) || !visibleVoucher(voucher)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Map<String, List<VoucherImageRecord>> grouped = new LinkedHashMap<>();
|
||||||
|
for (VoucherFile file : voucherFileMapper.selectList(
|
||||||
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<VoucherFile>()
|
||||||
|
.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<VoucherImage>()
|
||||||
|
.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<String, List<VoucherImageRecord>> entry : grouped.entrySet()) {
|
||||||
|
folderRecords.computeIfAbsent(entry.getKey(), ignored -> new ArrayList<>()).addAll(entry.getValue());
|
||||||
|
folderVouchers.computeIfAbsent(entry.getKey(), ignored -> new ArrayList<>()).add(voucher);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
List<Map<String, Object>> folders = new ArrayList<>();
|
||||||
|
for (Map.Entry<String, List<VoucherImageRecord>> entry : folderRecords.entrySet()) {
|
||||||
|
Map<String, Object> folder = new LinkedHashMap<>();
|
||||||
|
folder.put("type", "folder");
|
||||||
|
folder.put("isFolder", true);
|
||||||
|
folder.put("waybillId", waybillId);
|
||||||
|
folder.put("waybillBatchNo", waybill.getBatchNo());
|
||||||
|
List<VoucherManage> 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<Map<String, Object>> folderImages(Long waybillId, String folderName) {
|
||||||
|
Waybill waybill = waybillMapper.selectById(waybillId);
|
||||||
|
if (waybill == null || Func.isEmpty(waybill.getBatchNo()) || Func.isEmpty(folderName)) {
|
||||||
|
return List.of();
|
||||||
|
}
|
||||||
|
List<VoucherManage> vouchers = voucherManageMapper.selectList(
|
||||||
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<VoucherManage>()
|
||||||
|
.eq(VoucherManage::getTenantId, waybill.getTenantId())
|
||||||
|
.eq(VoucherManage::getIsDeleted, 0));
|
||||||
|
Map<String, VoucherImageRecord> recordMap = new LinkedHashMap<>();
|
||||||
|
for (VoucherManage voucher : vouchers) {
|
||||||
|
if (!containsBatchNo(voucher.getWaybillBatchNo(), waybill.getBatchNo()) || !visibleVoucher(voucher)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<VoucherImageRecord> voucherRecords = voucherFileMapper.selectList(
|
||||||
|
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<VoucherFile>()
|
||||||
|
.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<VoucherImage>()
|
||||||
|
.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<String, Object> imageMap(VoucherImageRecord image) {
|
||||||
|
Map<String, Object> 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")
|
@GetMapping("/list")
|
||||||
|
|||||||
+16
-7
@@ -10,6 +10,7 @@ import org.springblade.core.mp.support.Condition;
|
|||||||
import org.springblade.core.mp.support.Query;
|
import org.springblade.core.mp.support.Query;
|
||||||
import org.springblade.core.secure.annotation.PreAuth;
|
import org.springblade.core.secure.annotation.PreAuth;
|
||||||
import org.springblade.core.tool.api.R;
|
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.VoucherManageSubmitRequest;
|
||||||
import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest;
|
import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest;
|
||||||
import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest;
|
import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest;
|
||||||
@@ -70,12 +71,12 @@ public class VoucherManageController extends BladeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/folder-replace-object")
|
@PostMapping("/folder-replace-object")
|
||||||
@Operation(summary = "替换单个车牌凭证(系统文件上传后处理)")
|
@Operation(summary = "替换单个车牌凭证(分片上传文件处理)")
|
||||||
public R replaceFolderByObject(@RequestParam Long voucherId, @RequestParam String plateNo,
|
public R replaceFolderByObject(@RequestParam Long voucherId, @RequestParam String plateNo,
|
||||||
@RequestParam String objectKey, @RequestParam String fileName,
|
@RequestParam String objectKey, @RequestParam String fileName,
|
||||||
@RequestParam(required = false) Long size, @RequestParam(required = false) String contentType) {
|
@RequestParam(required = false) Long size, @RequestParam(required = false) String contentType) {
|
||||||
voucherManageService.replaceFolderByObject(voucherId, plateNo, objectKey, fileName, size, contentType);
|
voucherManageService.replaceFolderByObject(voucherId, plateNo, objectKey, fileName, size, contentType);
|
||||||
return R.success("上传成功");
|
return R.success("处理成功");
|
||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/folder-remove")
|
@PostMapping("/folder-remove")
|
||||||
@@ -90,8 +91,16 @@ public class VoucherManageController extends BladeController {
|
|||||||
@Operation(summary = "上传凭证或更换运输批次")
|
@Operation(summary = "上传凭证或更换运输批次")
|
||||||
public R submit(@RequestBody VoucherManageSubmitRequest request) { voucherManageService.submit(request); return R.success("提交成功"); }
|
public R submit(@RequestBody VoucherManageSubmitRequest request) { voucherManageService.submit(request); return R.success("提交成功"); }
|
||||||
|
|
||||||
@PostMapping("/upload-draft")
|
@PostMapping("/change-waybill-batch")
|
||||||
@ApiOperationSupport(order = 4)
|
@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 = "创建凭证上传草稿")
|
@Operation(summary = "创建凭证上传草稿")
|
||||||
public R<VoucherManage> createUploadDraft(@RequestBody VoucherUploadDraftRequest request) {
|
public R<VoucherManage> createUploadDraft(@RequestBody VoucherUploadDraftRequest request) {
|
||||||
return R.data(voucherManageService.createUploadDraft(request));
|
return R.data(voucherManageService.createUploadDraft(request));
|
||||||
@@ -112,12 +121,12 @@ public class VoucherManageController extends BladeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/remove")
|
@PostMapping("/remove")
|
||||||
@ApiOperationSupport(order = 5)
|
@ApiOperationSupport(order = 6)
|
||||||
@Operation(summary = "删除凭证")
|
@Operation(summary = "删除凭证")
|
||||||
public R remove(@RequestParam Long id) { voucherManageService.removeVoucher(id); return R.success("删除成功"); }
|
public R remove(@RequestParam Long id) { voucherManageService.removeVoucher(id); return R.success("删除成功"); }
|
||||||
|
|
||||||
@GetMapping("/waybill-batches")
|
@GetMapping("/waybill-batches")
|
||||||
@ApiOperationSupport(order = 6)
|
@ApiOperationSupport(order = 7)
|
||||||
@Operation(summary = "可关联运输批次")
|
@Operation(summary = "可关联运输批次")
|
||||||
public R<IPage<Map<String, Object>>> waybillBatches(Query query, @RequestParam(required = false) String batchNo,
|
public R<IPage<Map<String, Object>>> waybillBatches(Query query, @RequestParam(required = false) String batchNo,
|
||||||
@RequestParam(required = false) String createUser, @RequestParam(required = false) Integer waybillCount,
|
@RequestParam(required = false) String createUser, @RequestParam(required = false) Integer waybillCount,
|
||||||
@@ -126,7 +135,7 @@ public class VoucherManageController extends BladeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/audit-pass")
|
@PostMapping("/audit-pass")
|
||||||
@ApiOperationSupport(order = 7)
|
@ApiOperationSupport(order = 8)
|
||||||
@Operation(summary = "审核通过")
|
@Operation(summary = "审核通过")
|
||||||
public R auditPass(@RequestParam Long id) {
|
public R auditPass(@RequestParam Long id) {
|
||||||
voucherManageService.auditPass(id);
|
voucherManageService.auditPass(id);
|
||||||
@@ -134,7 +143,7 @@ public class VoucherManageController extends BladeController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PostMapping("/audit-reject")
|
@PostMapping("/audit-reject")
|
||||||
@ApiOperationSupport(order = 8)
|
@ApiOperationSupport(order = 9)
|
||||||
@Operation(summary = "审核驳回")
|
@Operation(summary = "审核驳回")
|
||||||
public R auditReject(@RequestParam Long id, @RequestParam(required = false) String rejectReason) {
|
public R auditReject(@RequestParam Long id, @RequestParam(required = false) String rejectReason) {
|
||||||
voucherManageService.auditReject(id, rejectReason);
|
voucherManageService.auditReject(id, rejectReason);
|
||||||
|
|||||||
+4
-2
@@ -14,6 +14,8 @@ public interface VoucherWaybillBatchMapper extends BaseMapper<VoucherWaybillBatc
|
|||||||
@Delete("DELETE FROM blade_voucher_waybill_batch WHERE voucher_id = #{voucherId}")
|
@Delete("DELETE FROM blade_voucher_waybill_batch WHERE voucher_id = #{voucherId}")
|
||||||
int deletePhysicalByVoucherId(@Param("voucherId") Long voucherId);
|
int deletePhysicalByVoucherId(@Param("voucherId") Long voucherId);
|
||||||
IPage<Map<String, Object>> selectVoucherWaybillBatchPage(IPage<?> page, @Param("tenantId") String tenantId, @Param("batchNo") String batchNo, @Param("createUser") String createUser,
|
IPage<Map<String, Object>> 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);
|
@Param("waybillCount") Integer waybillCount, @Param("createTimeStart") String createTimeStart, @Param("createTimeEnd") String createTimeEnd,
|
||||||
List<Map<String, Object>> selectWaybillBatchesByIds(@Param("tenantId") String tenantId, @Param("ids") List<Long> ids);
|
@Param("restrictCarrier") boolean restrictCarrier, @Param("carrierName") String carrierName);
|
||||||
|
List<Map<String, Object>> selectWaybillBatchesByIds(@Param("tenantId") String tenantId, @Param("ids") List<Long> ids,
|
||||||
|
@Param("restrictCarrier") boolean restrictCarrier, @Param("carrierName") String carrierName);
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -8,6 +8,7 @@
|
|||||||
FROM blade_waybill w
|
FROM blade_waybill w
|
||||||
LEFT JOIN blade_user u ON u.id = w.create_user
|
LEFT JOIN blade_user u ON u.id = w.create_user
|
||||||
WHERE w.is_deleted = 0 AND w.tenant_id = #{tenantId}
|
WHERE w.is_deleted = 0 AND w.tenant_id = #{tenantId}
|
||||||
|
<if test="restrictCarrier">AND w.carrier_name = #{carrierName}</if>
|
||||||
<if test="batchNo != null and batchNo != ''">AND w.batch_no LIKE CONCAT('%', #{batchNo}, '%')</if>
|
<if test="batchNo != null and batchNo != ''">AND w.batch_no LIKE CONCAT('%', #{batchNo}, '%')</if>
|
||||||
<if test="createUser != null and createUser != ''">AND w.create_user = #{createUser}</if>
|
<if test="createUser != null and createUser != ''">AND w.create_user = #{createUser}</if>
|
||||||
<if test="createTimeStart != null and createTimeStart != ''">AND w.create_time <![CDATA[>=]]> #{createTimeStart}</if>
|
<if test="createTimeStart != null and createTimeStart != ''">AND w.create_time <![CDATA[>=]]> #{createTimeStart}</if>
|
||||||
@@ -21,6 +22,7 @@
|
|||||||
FROM blade_waybill w
|
FROM blade_waybill w
|
||||||
WHERE w.is_deleted = 0 AND w.tenant_id = #{tenantId} AND w.id IN
|
WHERE w.is_deleted = 0 AND w.tenant_id = #{tenantId} AND w.id IN
|
||||||
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
|
<foreach collection="ids" item="id" open="(" separator="," close=")">#{id}</foreach>
|
||||||
|
<if test="restrictCarrier">AND w.carrier_name = #{carrierName}</if>
|
||||||
GROUP BY COALESCE(NULLIF(w.batch_no, ''), '未分批运单')
|
GROUP BY COALESCE(NULLIF(w.batch_no, ''), '未分批运单')
|
||||||
</select>
|
</select>
|
||||||
</mapper>
|
</mapper>
|
||||||
|
|||||||
+2
@@ -2,6 +2,7 @@ package org.springblade.transport.service;
|
|||||||
|
|
||||||
import com.baomidou.mybatisplus.core.metadata.IPage;
|
import com.baomidou.mybatisplus.core.metadata.IPage;
|
||||||
import org.springblade.core.mp.base.BaseService;
|
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.VoucherManageSubmitRequest;
|
||||||
import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest;
|
import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest;
|
||||||
import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest;
|
import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest;
|
||||||
@@ -22,6 +23,7 @@ public interface IVoucherManageService extends BaseService<VoucherManage> {
|
|||||||
void replaceFolderByObject(Long voucherId, String plateNo, String objectKey, String fileName, Long size, String contentType);
|
void replaceFolderByObject(Long voucherId, String plateNo, String objectKey, String fileName, Long size, String contentType);
|
||||||
void removeFolder(Long voucherId, String plateNo);
|
void removeFolder(Long voucherId, String plateNo);
|
||||||
void submit(VoucherManageSubmitRequest request);
|
void submit(VoucherManageSubmitRequest request);
|
||||||
|
void changeWaybillBatch(VoucherManageChangeBatchRequest request);
|
||||||
VoucherManage createUploadDraft(VoucherUploadDraftRequest request);
|
VoucherManage createUploadDraft(VoucherUploadDraftRequest request);
|
||||||
void completeUploadFile(VoucherFileCompleteRequest request);
|
void completeUploadFile(VoucherFileCompleteRequest request);
|
||||||
void processUploadedVoucher(Long voucherId);
|
void processUploadedVoucher(Long voucherId);
|
||||||
|
|||||||
+239
-32
@@ -14,6 +14,7 @@ import org.springblade.transport.mapper.VoucherImageMapper;
|
|||||||
import org.springblade.transport.mapper.VoucherFileMapper;
|
import org.springblade.transport.mapper.VoucherFileMapper;
|
||||||
import org.springblade.transport.mapper.VoucherWaybillBatchMapper;
|
import org.springblade.transport.mapper.VoucherWaybillBatchMapper;
|
||||||
import org.springblade.transport.event.VoucherUploadCompletedEvent;
|
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.VoucherManageSubmitRequest;
|
||||||
import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest;
|
import org.springblade.transport.pojo.dto.VoucherUploadDraftRequest;
|
||||||
import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest;
|
import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest;
|
||||||
@@ -101,7 +102,17 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IPage<VoucherManageVO> selectPage(IPage<VoucherManage> page, VoucherManageVO query) {
|
public IPage<VoucherManageVO> selectPage(IPage<VoucherManage> page, VoucherManageVO query) {
|
||||||
return VoucherManageWrapper.build().pageVO(page(page, buildQuery(query)));
|
IPage<VoucherManageVO> 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
|
@Override
|
||||||
@@ -154,9 +165,13 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
@Override
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void replaceFolder(Long voucherId, String plateNo, MultipartFile uploadFile) {
|
public void replaceFolder(Long voucherId, String plateNo, MultipartFile uploadFile) {
|
||||||
|
replaceFolder(voucherId, plateNo, uploadFile, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void replaceFolder(Long voucherId, String plateNo, MultipartFile uploadFile, boolean enforceSizeLimit) {
|
||||||
VoucherManage voucher = getVoucher(voucherId);
|
VoucherManage voucher = getVoucher(voucherId);
|
||||||
if (uploadFile == null || uploadFile.isEmpty()) throw new ServiceException("请选择凭证文件");
|
if (uploadFile == null || uploadFile.isEmpty()) throw new ServiceException("请选择凭证文件");
|
||||||
if (uploadFile.getSize() > 50L * 1024 * 1024) throw new ServiceException("凭证文件大小不能超过50M");
|
if (enforceSizeLimit && uploadFile.getSize() > 50L * 1024 * 1024) throw new ServiceException("凭证文件大小不能超过50M");
|
||||||
String normalizedPlateNo = normalizePlateNo(plateNo);
|
String normalizedPlateNo = normalizePlateNo(plateNo);
|
||||||
if (Func.isEmpty(normalizedPlateNo)) throw new ServiceException("车牌号不能为空");
|
if (Func.isEmpty(normalizedPlateNo)) throw new ServiceException("车牌号不能为空");
|
||||||
validateMinioConfig();
|
validateMinioConfig();
|
||||||
@@ -200,26 +215,135 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void replaceFolderByObject(Long voucherId, String plateNo, String objectKey, String fileName, Long size, String contentType) {
|
public void replaceFolderByObject(Long voucherId, String plateNo, String objectKey, String fileName, Long size, String contentType) {
|
||||||
if (Func.isEmpty(objectKey) || Func.isEmpty(fileName)) throw new ServiceException("文件信息不完整");
|
if (Func.isEmpty(objectKey) || Func.isEmpty(fileName)) throw new ServiceException("文件信息不完整");
|
||||||
|
VoucherManage voucher = getVoucher(voucherId);
|
||||||
validateMinioConfig();
|
validateMinioConfig();
|
||||||
MultipartFile uploadedFile = new MultipartFile() {
|
if (size != null && size == 0) throw new ServiceException("请选择凭证文件");
|
||||||
|
String originalFileName = safeFileName(fileName);
|
||||||
|
if (!originalFileName.toLowerCase(Locale.ROOT).endsWith(".zip")) {
|
||||||
|
throw new ServiceException("系统上传接口仅支持 zip 压缩包");
|
||||||
|
}
|
||||||
|
Path archivePath = null;
|
||||||
|
try {
|
||||||
|
// 分片上传完成后对象已存在于 MinIO。先下载到临时文件,避免直接使用远程流导致
|
||||||
|
// ZIP 解压与 MinIO 分片上传之间的流状态冲突。
|
||||||
|
archivePath = downloadMinioObject(objectKey);
|
||||||
|
String effectivePlateNo = resolveReplacementPlateNo(voucher, plateNo, originalFileName, archivePath);
|
||||||
|
MultipartFile uploadedFile = localArchiveMultipartFile(archivePath, originalFileName, contentType);
|
||||||
|
replaceFolder(voucherId, effectivePlateNo, uploadedFile, false);
|
||||||
|
log.info("[分片上传] 压缩包下载、解压并匹配完成 voucherId={}, requestedPlateNo={}, matchedPlateNo={}, objectKey={}",
|
||||||
|
voucherId, plateNo, effectivePlateNo, objectKey);
|
||||||
|
} catch (ServiceException exception) {
|
||||||
|
throw exception;
|
||||||
|
} catch (Exception exception) {
|
||||||
|
log.error("[分片上传] 压缩包处理失败 voucherId={}, objectKey={}", voucherId, objectKey, exception);
|
||||||
|
throw new ServiceException("凭证文件上传失败");
|
||||||
|
} finally {
|
||||||
|
deleteTempArchive(archivePath);
|
||||||
|
// 系统上传接口产生的临时附件仅用于本次替换,复制到凭证目录后清理源对象。
|
||||||
|
deleteSourceObjectQuietly(objectKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Path downloadMinioObject(String objectKey) throws Exception {
|
||||||
|
Path archivePath = Files.createTempFile("voucher-object-", ".zip");
|
||||||
|
String resolvedObjectKey = resolveMinioObjectKey(objectKey);
|
||||||
|
Exception lastException = null;
|
||||||
|
List<String> 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 getName() { return "file"; }
|
||||||
@Override public String getOriginalFilename() { return fileName; }
|
@Override public String getOriginalFilename() { return fileName; }
|
||||||
@Override public String getContentType() { return contentType; }
|
@Override public String getContentType() { return Func.isEmpty(contentType) ? "application/zip" : contentType; }
|
||||||
@Override public boolean isEmpty() { return size != null && size == 0; }
|
@Override public boolean isEmpty() { return archiveSize == 0; }
|
||||||
@Override public long getSize() { return size == null ? -1L : size; }
|
@Override public long getSize() { return archiveSize; }
|
||||||
@Override public byte[] getBytes() throws java.io.IOException { try (InputStream input = getInputStream()) { return input.readAllBytes(); } }
|
@Override public byte[] getBytes() throws java.io.IOException { return Files.readAllBytes(archivePath); }
|
||||||
@Override public InputStream getInputStream() throws java.io.IOException {
|
@Override public InputStream getInputStream() throws java.io.IOException { return Files.newInputStream(archivePath); }
|
||||||
try { return minioClient.getObject(GetObjectArgs.builder().bucket(minioBucketName).object(objectKey).build()); }
|
@Override public void transferTo(java.io.File destination) throws java.io.IOException {
|
||||||
catch (Exception exception) { throw new java.io.IOException("读取系统上传文件失败", exception); }
|
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<String> existingPlates = new HashSet<>();
|
||||||
|
voucherFileMapper.selectList(Wrappers.<VoucherFile>lambdaQuery()
|
||||||
|
.eq(VoucherFile::getTenantId, voucher.getTenantId()).eq(VoucherFile::getVoucherId, voucher.getId())
|
||||||
|
.eq(VoucherFile::getIsDeleted, 0)).forEach(file -> addPlateNumber(existingPlates, file.getPlateNo()));
|
||||||
|
voucherImageMapper.selectList(Wrappers.<VoucherImage>lambdaQuery()
|
||||||
|
.eq(VoucherImage::getTenantId, voucher.getTenantId()).eq(VoucherImage::getVoucherId, voucher.getId())
|
||||||
|
.eq(VoucherImage::getIsDeleted, 0)).forEach(image -> addPlateNumber(existingPlates, image.getPlateNo()));
|
||||||
|
Map<String, Waybill> 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<String> 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<String> 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<String> matchedArchivePlates = archivePlates.stream().filter(waybillByPlate::containsKey).collect(Collectors.toSet());
|
||||||
|
if (matchedArchivePlates.size() == 1) return matchedArchivePlates.iterator().next();
|
||||||
|
Set<String> 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
|
@Override
|
||||||
@@ -448,16 +572,40 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void refreshVoucherCounts(VoucherManage voucher) {
|
private void refreshVoucherCounts(VoucherManage voucher) {
|
||||||
List<VoucherImage> images = voucherImageMapper.selectList(Wrappers.<VoucherImage>lambdaQuery()
|
VoucherFolderCounts folderCounts = countVoucherFolderRelations(voucher.getTenantId(), voucher.getId());
|
||||||
.eq(VoucherImage::getTenantId, voucher.getTenantId()).eq(VoucherImage::getVoucherId, voucher.getId()).eq(VoucherImage::getIsDeleted, 0));
|
VoucherManage update = new VoucherManage(); update.setId(voucher.getId()); update.setVoucherCount(folderCounts.total());
|
||||||
long relatedWaybillCount = images.stream().filter(item -> Objects.equals(item.getMatched(), 1))
|
update.setRelatedWaybillCount(folderCounts.related()); update.setUnRelatedWaybillCount(folderCounts.unrelated());
|
||||||
.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));
|
|
||||||
update.setProcessStatus("处理完成"); updateById(update);
|
update.setProcessStatus("处理完成"); updateById(update);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private int countVoucherFolders(VoucherManage voucher) {
|
||||||
|
return countVoucherFolderRelations(voucher.getTenantId(), voucher.getId()).total();
|
||||||
|
}
|
||||||
|
|
||||||
|
private VoucherFolderCounts countVoucherFolderRelations(String tenantId, Long voucherId) {
|
||||||
|
List<VoucherFile> files = voucherFileMapper.selectList(Wrappers.<VoucherFile>lambdaQuery()
|
||||||
|
.eq(VoucherFile::getTenantId, tenantId).eq(VoucherFile::getVoucherId, voucherId).eq(VoucherFile::getIsDeleted, 0));
|
||||||
|
if (!files.isEmpty()) {
|
||||||
|
Set<String> folders = files.stream().map(item -> folderKey(item.getPlateNo())).collect(Collectors.toSet());
|
||||||
|
Set<String> 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<VoucherImage> images = voucherImageMapper.selectList(Wrappers.<VoucherImage>lambdaQuery()
|
||||||
|
.eq(VoucherImage::getTenantId, tenantId).eq(VoucherImage::getVoucherId, voucherId).eq(VoucherImage::getIsDeleted, 0));
|
||||||
|
Set<String> folders = images.stream().map(item -> folderKey(item.getPlateNo())).collect(Collectors.toSet());
|
||||||
|
Set<String> 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); } }
|
private void deleteObjectQuietly(String objectKey) { try { minioClient.removeObject(RemoveObjectArgs.builder().bucket(minioBucketName).object(objectKey).build()); } catch (Exception exception) { log.warn("删除凭证对象失败 objectKey={}", objectKey, exception); } }
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -478,6 +626,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
voucher.setUploadSource("内部");
|
voucher.setUploadSource("内部");
|
||||||
voucher.setProcessStatus("上传中");
|
voucher.setProcessStatus("上传中");
|
||||||
voucher.setAuditStatus("-");
|
voucher.setAuditStatus("-");
|
||||||
|
voucher.setRejectReason("");
|
||||||
voucher.setVoucherCount(0);
|
voucher.setVoucherCount(0);
|
||||||
voucher.setRelatedWaybillCount(0);
|
voucher.setRelatedWaybillCount(0);
|
||||||
voucher.setUnRelatedWaybillCount(0);
|
voucher.setUnRelatedWaybillCount(0);
|
||||||
@@ -515,6 +664,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
|
|
||||||
voucher.setProcessStatus("上传中");
|
voucher.setProcessStatus("上传中");
|
||||||
voucher.setAuditStatus("-");
|
voucher.setAuditStatus("-");
|
||||||
|
voucher.setRejectReason("");
|
||||||
voucher.setVoucherCount(0);
|
voucher.setVoucherCount(0);
|
||||||
voucher.setRelatedWaybillCount(0);
|
voucher.setRelatedWaybillCount(0);
|
||||||
voucher.setUnRelatedWaybillCount(0);
|
voucher.setUnRelatedWaybillCount(0);
|
||||||
@@ -547,6 +697,45 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
@Transactional(rollbackFor = Exception.class)
|
||||||
|
public void changeWaybillBatch(VoucherManageChangeBatchRequest request) {
|
||||||
|
if (request == null || Func.isEmpty(request.getVoucherId())) throw new ServiceException("凭证批次不能为空");
|
||||||
|
if (Func.isEmpty(request.getWaybillImportBatchIds())) throw new ServiceException("请至少关联一个运输批次");
|
||||||
|
VoucherManage voucher = getVoucher(request.getVoucherId());
|
||||||
|
List<Map<String, Object>> batches = selectableWaybillBatchesByIds(request.getWaybillImportBatchIds());
|
||||||
|
if (batches.size() != request.getWaybillImportBatchIds().size()) throw new ServiceException("存在无效的运输批次");
|
||||||
|
|
||||||
|
// 更换批次必须同步重建关联关系,否则后续匹配仍会读取旧批次。
|
||||||
|
voucherWaybillBatchMapper.deletePhysicalByVoucherId(voucher.getId());
|
||||||
|
List<VoucherWaybillBatch> relations = new ArrayList<>();
|
||||||
|
for (Map<String, Object> 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
|
@Override
|
||||||
@Transactional(rollbackFor = Exception.class)
|
@Transactional(rollbackFor = Exception.class)
|
||||||
public void completeUploadFile(VoucherFileCompleteRequest request) {
|
public void completeUploadFile(VoucherFileCompleteRequest request) {
|
||||||
@@ -697,17 +886,19 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
voucherId, fileCount, imageCount, matchedImageCount);
|
voucherId, fileCount, imageCount, matchedImageCount);
|
||||||
VoucherManage update = new VoucherManage();
|
VoucherManage update = new VoucherManage();
|
||||||
update.setId(voucher.getId());
|
update.setId(voucher.getId());
|
||||||
update.setVoucherCount(imageCount);
|
VoucherFolderCounts folderCounts = countVoucherFolderRelations(voucher.getTenantId(), voucher.getId());
|
||||||
update.setRelatedWaybillCount(relatedWaybillIds.size());
|
update.setVoucherCount(folderCounts.total());
|
||||||
update.setUnRelatedWaybillCount(Math.max(waybills.size() - relatedWaybillIds.size(), 0));
|
update.setRelatedWaybillCount(folderCounts.related());
|
||||||
|
update.setUnRelatedWaybillCount(folderCounts.unrelated());
|
||||||
update.setProcessStatus("处理完成");
|
update.setProcessStatus("处理完成");
|
||||||
// 内部上传的凭证无需人工审核,MQ处理完成后直接通过;承运商上传仍进入待审核流程。
|
// 内部上传的凭证无需人工审核,MQ处理完成后直接通过;承运商上传仍进入待审核流程。
|
||||||
boolean internalUpload = "内部".equals(voucher.getUploadSource());
|
boolean internalUpload = "内部".equals(voucher.getUploadSource());
|
||||||
update.setAuditStatus(internalUpload ? "审核通过" : "待审核");
|
update.setAuditStatus(internalUpload ? "审核通过" : "待审核");
|
||||||
|
update.setRejectReason("");
|
||||||
updateById(update);
|
updateById(update);
|
||||||
log.info("[凭证处理] 进度 100%:处理完成 voucherId={}, voucherBatchNo={}, uploadSource={}, auditStatus={}, fileCount={}, imageCount={}, relatedWaybillCount={}, unrelatedWaybillCount={}",
|
log.info("[凭证处理] 进度 100%:处理完成 voucherId={}, voucherBatchNo={}, uploadSource={}, auditStatus={}, fileCount={}, imageCount={}, folderCount={}, relatedVoucherCount={}, unrelatedVoucherCount={}",
|
||||||
voucherId, voucher.getVoucherBatchNo(), voucher.getUploadSource(), update.getAuditStatus(), fileCount, imageCount,
|
voucherId, voucher.getVoucherBatchNo(), voucher.getUploadSource(), update.getAuditStatus(), fileCount, imageCount, folderCounts.total(),
|
||||||
relatedWaybillIds.size(), Math.max(waybills.size() - relatedWaybillIds.size(), 0));
|
folderCounts.related(), folderCounts.unrelated());
|
||||||
} finally {
|
} finally {
|
||||||
deleteTempArchive(archivePath);
|
deleteTempArchive(archivePath);
|
||||||
}
|
}
|
||||||
@@ -734,12 +925,22 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public IPage<Map<String, Object>> selectableWaybillBatches(IPage<?> page, String batchNo, String createUser, Integer waybillCount, String createTimeStart, String createTimeEnd) {
|
public IPage<Map<String, Object>> 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<Map<String, Object>> selectableWaybillBatchesByIds(List<Long> ids) {
|
private List<Map<String, Object>> selectableWaybillBatchesByIds(List<Long> ids) {
|
||||||
if (Func.isEmpty(ids)) return List.of();
|
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<Waybill> listRelatedWaybills(VoucherManage voucher) {
|
private List<Waybill> listRelatedWaybills(VoucherManage voucher) {
|
||||||
@@ -1029,6 +1230,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
throw new ServiceException("仅处理完成的凭证可以审核");
|
throw new ServiceException("仅处理完成的凭证可以审核");
|
||||||
}
|
}
|
||||||
voucher.setAuditStatus("审核通过");
|
voucher.setAuditStatus("审核通过");
|
||||||
|
voucher.setRejectReason("");
|
||||||
updateById(voucher);
|
updateById(voucher);
|
||||||
log.info("凭证批次审核通过 id={}, voucherBatchNo={}, operator={}", id, voucher.getVoucherBatchNo(), AuthUtil.getUserId());
|
log.info("凭证批次审核通过 id={}, voucherBatchNo={}, operator={}", id, voucher.getVoucherBatchNo(), AuthUtil.getUserId());
|
||||||
}
|
}
|
||||||
@@ -1046,10 +1248,15 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
|
|||||||
if (!"处理完成".equals(voucher.getProcessStatus())) {
|
if (!"处理完成".equals(voucher.getProcessStatus())) {
|
||||||
throw new ServiceException("仅处理完成的凭证可以审核");
|
throw new ServiceException("仅处理完成的凭证可以审核");
|
||||||
}
|
}
|
||||||
|
String reason = rejectReason == null ? null : rejectReason.trim();
|
||||||
|
if (reason != null && reason.length() > 200) {
|
||||||
|
throw new ServiceException("驳回原因不能超过200字");
|
||||||
|
}
|
||||||
voucher.setAuditStatus("审核驳回");
|
voucher.setAuditStatus("审核驳回");
|
||||||
|
voucher.setRejectReason(Func.isEmpty(reason) ? "" : reason);
|
||||||
updateById(voucher);
|
updateById(voucher);
|
||||||
log.info("凭证批次审核驳回 id={}, voucherBatchNo={}, operator={}, reason={}",
|
log.info("凭证批次审核驳回 id={}, voucherBatchNo={}, operator={}, reason={}",
|
||||||
id, voucher.getVoucherBatchNo(), AuthUtil.getUserId(), rejectReason);
|
id, voucher.getVoucherBatchNo(), AuthUtil.getUserId(), reason);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ CREATE TABLE IF NOT EXISTS `blade_voucher_manage` (
|
|||||||
`related_waybill_count` int NOT NULL DEFAULT 0 COMMENT '已关联运单数',
|
`related_waybill_count` int NOT NULL DEFAULT 0 COMMENT '已关联运单数',
|
||||||
`un_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 '待审核、审核通过、审核驳回',
|
`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_user` bigint DEFAULT NULL, `create_dept` bigint DEFAULT NULL,
|
||||||
`create_time` datetime DEFAULT NULL, `update_user` bigint DEFAULT NULL, `update_time` datetime 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,
|
`status` tinyint NOT NULL DEFAULT 1, `is_deleted` tinyint NOT NULL DEFAULT 0,
|
||||||
|
|||||||
@@ -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 ;
|
||||||
@@ -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
|
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 '待审核、审核通过、审核驳回';
|
ALTER TABLE `blade_voucher_manage` ADD COLUMN `audit_status` varchar(20) NOT NULL DEFAULT '-' COMMENT '待审核、审核通过、审核驳回';
|
||||||
END IF;
|
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
|
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 '创建人';
|
ALTER TABLE `blade_voucher_manage` ADD COLUMN `create_user` bigint DEFAULT NULL COMMENT '创建人';
|
||||||
END IF;
|
END IF;
|
||||||
|
|||||||
Reference in New Issue
Block a user