调整结算模块

This commit is contained in:
2026-09-08 10:58:21 +08:00
parent 4e9bb19b23
commit a3eeb24f4f
7 changed files with 249 additions and 41 deletions
@@ -0,0 +1,15 @@
package org.springblade.transport.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.springblade.transport.pojo.entity.VoucherFile;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Param;
/**
* 凭证解压文件明细 Mapper。
*/
public interface VoucherFileMapper extends BaseMapper<VoucherFile> {
@Delete("DELETE FROM blade_voucher_file WHERE voucher_id = #{voucherId}")
void deleteByVoucherId(@Param("voucherId") Long voucherId);
}
@@ -10,6 +10,7 @@ import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.mapper.VoucherManageMapper;
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.VoucherManageSubmitRequest;
@@ -18,9 +19,11 @@ import org.springblade.transport.pojo.dto.VoucherFileCompleteRequest;
import org.springblade.transport.pojo.entity.VoucherManage;
import org.springblade.transport.pojo.entity.VoucherWaybillBatch;
import org.springblade.transport.pojo.entity.VoucherImage;
import org.springblade.transport.pojo.entity.VoucherFile;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.entity.ProjectApply;
import org.springblade.transport.pojo.vo.VoucherManageVO;
import org.springblade.transport.pojo.vo.VoucherFileVO;
import org.springblade.transport.service.IVoucherManageService;
import org.springblade.transport.service.IProjectApplyService;
import org.springblade.transport.service.IWaybillService;
@@ -30,13 +33,17 @@ import org.springframework.context.ApplicationEventPublisher;
import org.springframework.transaction.annotation.Transactional;
import io.minio.MinioClient;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.PutObjectArgs;
import io.minio.http.Method;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
@@ -58,6 +65,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
private final IProjectApplyService projectApplyService;
private final IWaybillService waybillService;
private final VoucherImageMapper voucherImageMapper;
private final VoucherFileMapper voucherFileMapper;
private final ApplicationEventPublisher eventPublisher;
private final MinioClient minioClient;
private final String minioBucketName;
@@ -65,13 +73,15 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
public VoucherManageServiceImpl(VoucherWaybillBatchMapper voucherWaybillBatchMapper, IProjectApplyService projectApplyService,
IWaybillService waybillService,
VoucherImageMapper voucherImageMapper, ApplicationEventPublisher eventPublisher, MinioClient minioClient,
VoucherImageMapper voucherImageMapper, VoucherFileMapper voucherFileMapper,
ApplicationEventPublisher eventPublisher, MinioClient minioClient,
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") String minioBucketName,
@Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory) {
this.voucherWaybillBatchMapper = voucherWaybillBatchMapper;
this.projectApplyService = projectApplyService;
this.waybillService = waybillService;
this.voucherImageMapper = voucherImageMapper;
this.voucherFileMapper = voucherFileMapper;
this.eventPublisher = eventPublisher;
this.minioClient = minioClient;
this.minioBucketName = minioBucketName;
@@ -87,7 +97,14 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
public VoucherManageVO detail(Long id) {
VoucherManage record = getById(id);
if (record == null || Objects.equals(record.getIsDeleted(), 1)) throw new ServiceException("凭证批次不存在");
return VoucherManageWrapper.build().entityVO(record);
VoucherManageVO result = VoucherManageWrapper.build().entityVO(record);
List<VoucherFile> files = voucherFileMapper.selectList(Wrappers.<VoucherFile>lambdaQuery()
.eq(VoucherFile::getTenantId, record.getTenantId())
.eq(VoucherFile::getVoucherId, record.getId())
.eq(VoucherFile::getIsDeleted, 0)
.orderByAsc(VoucherFile::getEntryName));
result.setVoucherFiles(files.stream().map(this::toVoucherFileVO).toList());
return result;
}
@Override
@@ -225,6 +242,8 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
}
log.info("[凭证处理] 进度 20%:已建立车牌匹配索引 voucherId={}, plateCount={}", voucherId, waybillByPlate.size());
voucherImageMapper.deleteByVoucherId(voucher.getId());
voucherFileMapper.deleteByVoucherId(voucher.getId());
int fileCount = 0;
int imageCount = 0;
int matchedImageCount = 0;
Set<Long> relatedWaybillIds = new HashSet<>();
@@ -239,51 +258,72 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
if (entryName.equals("__MACOSX") || entryName.startsWith("__MACOSX/")) {
continue;
}
String[] pathParts = entryName.split("/");
if (pathParts.length < 2 || !isImageFile(pathParts[pathParts.length - 1])) {
List<String> pathParts = Arrays.stream(entryName.split("/"))
.filter(Func::isNotEmpty).toList();
if (pathParts.isEmpty()) {
continue;
}
String plateNo = normalizePlateNo(pathParts[0]);
String imageName = safeFileName(pathParts[pathParts.length - 1]);
if (Func.isEmpty(plateNo) || Func.isEmpty(imageName)) {
String folderName = pathParts.size() > 1 ? safeArchiveSegment(pathParts.get(0)) : null;
String plateNo = normalizePlateNo(folderName);
String rawFileName = pathParts.get(pathParts.size() - 1);
boolean imageFile = isImageFile(rawFileName);
String fileName = safeFileName(rawFileName);
if (Func.isEmpty(fileName)) {
continue;
}
Waybill waybill = waybillByPlate.get(plateNo);
if (waybill == null) {
log.warn("[凭证处理] 图片未匹配运单 voucherId={}, plateNo={}, indexedPlates={}",
voucherId, plateNo, waybillByPlate.keySet());
} else {
log.info("[凭证处理] 图片匹配运单 voucherId={}, plateNo={}, waybillId={}, waybillNo={}",
voucherId, plateNo, waybill.getId(), waybill.getWaybillNo());
}
if (imageFile && waybill == null) log.warn("[凭证处理] 图片未匹配运单 voucherId={}, plateNo={}, indexedPlates={}",
voucherId, plateNo, waybillByPlate.keySet());
String waybillNo = waybill == null ? "unmatched" : safePathPart(waybill.getWaybillNo());
String objectKey = buildObjectKey(voucher.getId(), waybillNo, plateNo, imageName);
String objectKey = buildObjectKey(voucher.getId(), waybillNo,
Func.isEmpty(plateNo) ? "root" : plateNo, entryName);
String contentType = contentType(fileName);
minioClient.putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
.stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024).build());
VoucherImage image = new VoucherImage();
image.setVoucherId(voucher.getId());
image.setVoucherBatchNo(voucher.getVoucherBatchNo());
image.setWaybillId(waybill == null ? null : waybill.getId());
image.setWaybillNo(waybill == null ? null : waybill.getWaybillNo());
image.setPlateNo(plateNo);
image.setImageName(imageName);
image.setObjectKey(objectKey);
image.setMatched(waybill == null ? 0 : 1);
image.setTenantId(voucher.getTenantId());
voucherImageMapper.insert(image);
imageCount++;
if (waybill != null) {
.stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024)
.contentType(contentType).build());
VoucherFile file = new VoucherFile();
file.setVoucherId(voucher.getId());
file.setVoucherBatchNo(voucher.getVoucherBatchNo());
file.setWaybillId(waybill == null ? null : waybill.getId());
file.setWaybillNo(waybill == null ? null : waybill.getWaybillNo());
file.setPlateNo(Func.isEmpty(plateNo) ? null : plateNo);
file.setFolderName(folderName);
file.setEntryName(entryName);
file.setFileName(fileName);
file.setObjectKey(objectKey);
file.setFileSize(entry.getSize() < 0 ? null : entry.getSize());
file.setContentType(contentType);
file.setFileType(imageFile ? "image" : "file");
file.setMatched(waybill == null ? 0 : 1);
file.setTenantId(voucher.getTenantId());
voucherFileMapper.insert(file);
fileCount++;
if (imageFile) {
VoucherImage image = new VoucherImage();
image.setVoucherId(voucher.getId());
image.setVoucherBatchNo(voucher.getVoucherBatchNo());
image.setWaybillId(waybill == null ? null : waybill.getId());
image.setWaybillNo(waybill == null ? null : waybill.getWaybillNo());
image.setPlateNo(Func.isEmpty(plateNo) ? "" : plateNo);
image.setImageName(fileName);
image.setObjectKey(objectKey);
image.setMatched(waybill == null ? 0 : 1);
image.setTenantId(voucher.getTenantId());
voucherImageMapper.insert(image);
imageCount++;
}
if (waybill != null && imageFile) {
matchedImageCount++;
relatedWaybillIds.add(waybill.getId());
}
if (imageCount % 10 == 0) {
log.info("[凭证处理] 图片处理中 voucherId={}, processedImages={}, matchedImages={}, relatedWaybills={}",
voucherId, imageCount, matchedImageCount, relatedWaybillIds.size());
if (fileCount % 10 == 0) {
log.info("[凭证处理] 文件处理中 voucherId={}, processedFiles={}, imageCount={}, matchedImages={}, relatedWaybills={}",
voucherId, fileCount, imageCount, matchedImageCount, relatedWaybillIds.size());
}
}
}
log.info("[凭证处理] 进度 90%图片解压上传完成 voucherId={}, imageCount={}, matchedImageCount={}",
voucherId, imageCount, matchedImageCount);
log.info("[凭证处理] 进度 90%文件解压上传完成 voucherId={}, fileCount={}, imageCount={}, matchedImageCount={}",
voucherId, fileCount, imageCount, matchedImageCount);
VoucherManage update = new VoucherManage();
update.setId(voucher.getId());
update.setVoucherCount(imageCount);
@@ -291,8 +331,8 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
update.setUnRelatedWaybillCount(Math.max(waybills.size() - relatedWaybillIds.size(), 0));
update.setProcessStatus("处理完成");
updateById(update);
log.info("[凭证处理] 进度 100%:处理完成 voucherId={}, voucherBatchNo={}, imageCount={}, relatedWaybillCount={}, unrelatedWaybillCount={}",
voucherId, voucher.getVoucherBatchNo(), imageCount, relatedWaybillIds.size(), Math.max(waybills.size() - relatedWaybillIds.size(), 0));
log.info("[凭证处理] 进度 100%:处理完成 voucherId={}, voucherBatchNo={}, fileCount={}, imageCount={}, relatedWaybillCount={}, unrelatedWaybillCount={}",
voucherId, voucher.getVoucherBatchNo(), fileCount, imageCount, relatedWaybillIds.size(), Math.max(waybills.size() - relatedWaybillIds.size(), 0));
} catch (Exception exception) {
VoucherManage update = new VoucherManage();
update.setId(voucher.getId());
@@ -310,6 +350,8 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
if (voucher == null) throw new ServiceException("凭证批次不存在");
deleteLogic(List.of(id));
voucherWaybillBatchMapper.delete(Wrappers.<VoucherWaybillBatch>lambdaQuery().eq(VoucherWaybillBatch::getVoucherId, id));
voucherFileMapper.deleteByVoucherId(id);
voucherImageMapper.deleteByVoucherId(id);
}
@Override
@@ -347,26 +389,75 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
}
private InputStream openSourceFile(String fileUrl) throws Exception {
URLConnection connection = new java.net.URL(fileUrl).openConnection();
String normalizedUrl = stripZipSuffixParameters(fileUrl);
URLConnection connection = new java.net.URL(normalizedUrl).openConnection();
connection.setConnectTimeout(30_000);
connection.setReadTimeout(300_000);
return connection.getInputStream();
}
private String stripZipSuffixParameters(String fileUrl) {
if (Func.isEmpty(fileUrl)) {
return fileUrl;
}
String lowerUrl = fileUrl.toLowerCase(Locale.ROOT);
int zipEnd = lowerUrl.indexOf(".zip");
return zipEnd < 0 ? fileUrl : fileUrl.substring(0, zipEnd + 4);
}
private void validateMinioConfig() {
if (Func.isEmpty(minioBucketName)) {
throw new ServiceException("Nacos 未配置 file.storage.minio.bucket-name");
}
}
private String buildObjectKey(Long voucherId, String waybillNo, String plateNo, String imageName) {
String objectKey = voucherId + "/" + waybillNo + "/" + safePathPart(plateNo) + "/" + imageName;
private String buildObjectKey(Long voucherId, String waybillNo, String plateNo, String entryName) {
String objectKey = voucherId + "/" + waybillNo + "/" + safePathPart(plateNo) + "/" + safeArchivePath(entryName);
if (Func.isEmpty(minioRootDirectory)) {
return objectKey;
}
return minioRootDirectory.endsWith("/") ? minioRootDirectory + objectKey : minioRootDirectory + "/" + objectKey;
}
private String safeArchivePath(String entryName) {
return Arrays.stream(entryName == null ? new String[0] : entryName.replace('\\', '/').split("/"))
.filter(Func::isNotEmpty).map(this::safeArchiveSegment).filter(Func::isNotEmpty).collect(Collectors.joining("/"));
}
private String safeArchiveSegment(String value) {
return value == null ? "" : value.replaceAll("[^0-9A-Za-z\\u4e00-\\u9fa5._-]", "_");
}
private String contentType(String fileName) {
String contentType = URLConnection.guessContentTypeFromName(fileName);
return Func.isEmpty(contentType) ? "application/octet-stream" : contentType;
}
private VoucherFileVO toVoucherFileVO(VoucherFile file) {
VoucherFileVO result = new VoucherFileVO();
result.setId(file.getId());
result.setVoucherId(file.getVoucherId());
result.setVoucherBatchNo(file.getVoucherBatchNo());
result.setWaybillId(file.getWaybillId());
result.setWaybillNo(file.getWaybillNo());
result.setPlateNo(file.getPlateNo());
result.setFolderName(file.getFolderName());
result.setEntryName(file.getEntryName());
result.setFileName(file.getFileName());
result.setObjectKey(file.getObjectKey());
result.setFileSize(file.getFileSize());
result.setContentType(file.getContentType());
result.setFileType(file.getFileType());
result.setMatched(file.getMatched());
try {
result.setUrl(minioClient.getPresignedObjectUrl(GetPresignedObjectUrlArgs.builder()
.method(Method.GET).bucket(minioBucketName).object(file.getObjectKey()).expiry(1, java.util.concurrent.TimeUnit.HOURS).build()));
} catch (Exception exception) {
log.warn("生成凭证文件预览地址失败 voucherFileId={}, objectKey={}", file.getId(), file.getObjectKey(), exception);
}
return result;
}
private String normalizePlateNo(String plateNo) {
return plateNo == null ? null : plateNo.replaceAll("[\\s-]", "").toUpperCase();
}
@@ -412,7 +503,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
}
private String safeFileName(String value) {
return value == null ? "" : safePathPart(value.replaceFirst("(?s)^.*[/\\\\]", ""));
return value == null ? "" : safeArchiveSegment(value.replaceFirst("(?s)^.*[/\\\\]", ""));
}
private boolean isImageFile(String fileName) {