✨ 完善运单导入配载标识、导入状态与凭证文件夹统计

记录配载标识号并在同批次多运单时生成配载单,导入状态与运单状态对齐,凭证按车牌文件夹统计并支持压缩包重传匹配。

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-09-27 22:54:30 +08:00
parent f3a614ffe9
commit eef9d7ad9d
17 changed files with 545 additions and 62 deletions
@@ -56,7 +56,7 @@ public class LoadingManage extends TenantEntity {
@Schema(description = "货物名称")
private String cargoName;
@Schema(description = "车/船/航班/班列")
@Schema(description = "车牌号/船班号/船次号/班次号")
private String vehicleNo;
@Schema(description = "挂车车牌号")
@@ -162,7 +162,7 @@ public class Waybill extends TenantEntity {
@TableField(updateStrategy = FieldStrategy.ALWAYS)
private String driverRejectReason;
@Schema(description = "车/船/航班/班列号")
@Schema(description = "车牌号/船班号/船次号/班次号")
private String vehicleNo;
@Schema(description = "船长")
@@ -234,6 +234,9 @@ public class Waybill extends TenantEntity {
@Schema(description = "配载单号")
private String loadingNo;
@Schema(description = "配载标识号")
private String loadingIdentifier;
@Schema(description = "运单批次号")
private String batchNo;
@@ -25,6 +25,7 @@ public class VoucherFolderVO implements Serializable {
private String folderName;
private Long waybillId;
private String waybillNo;
private String loadingNo;
private Integer matched;
private String processStatus;
private String fileName;
@@ -66,6 +66,10 @@ public class WaybillVO extends Waybill {
@Schema(description = "业务状态名称")
private String businessStatusName;
@TableField(exist = false)
@Schema(description = "运输方式名称")
private String transportTypeName;
@TableField(exist = false)
@Schema(description = "是否需要司机确认接单(过程配置接单节点)")
private Boolean requireAccept;
@@ -51,7 +51,9 @@ 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.pojo.vo.VoucherFolderVO;
import org.springblade.transport.service.IProcessConfigService;
import org.springblade.transport.service.IVoucherManageService;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.GetMapping;
@@ -80,6 +82,7 @@ import java.util.concurrent.TimeUnit;
public class ProcessConfigController extends BladeController {
private final IProcessConfigService processConfigService;
private final IVoucherManageService voucherManageService;
private final WaybillMapper waybillMapper;
private final VoucherFileMapper voucherFileMapper;
private final VoucherImageMapper voucherImageMapper;
@@ -88,11 +91,13 @@ public class ProcessConfigController extends BladeController {
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}")
private String minioBucketName;
public ProcessConfigController(IProcessConfigService processConfigService, WaybillMapper waybillMapper,
public ProcessConfigController(IProcessConfigService processConfigService, IVoucherManageService voucherManageService,
WaybillMapper waybillMapper,
VoucherFileMapper voucherFileMapper, VoucherImageMapper voucherImageMapper,
VoucherManageMapper voucherManageMapper,
ObjectProvider<MinioClient> minioClientProvider) {
this.processConfigService = processConfigService;
this.voucherManageService = voucherManageService;
this.waybillMapper = waybillMapper;
this.voucherFileMapper = voucherFileMapper;
this.voucherImageMapper = voucherImageMapper;
@@ -128,6 +133,23 @@ public class ProcessConfigController extends BladeController {
return R.data(detail);
}
@GetMapping("/voucher-folders")
@ApiOperationSupport(order = 2)
@Operation(summary = "查询运单执行凭证文件夹")
public R<List<VoucherFolderVO>> voucherFolders(
@Parameter(description = "运单主键", required = true) @RequestParam Long waybillId) {
return R.data(voucherManageService.listWaybillFolders(waybillId));
}
@GetMapping("/voucher-folder-detail")
@ApiOperationSupport(order = 2)
@Operation(summary = "查询运单执行凭证文件夹详情")
public R<VoucherFolderVO> voucherFolderDetail(
@Parameter(description = "凭证批次主键", required = true) @RequestParam Long voucherId,
@Parameter(description = "车牌号", required = true) @RequestParam String plateNo) {
return R.data(voucherManageService.folderDetail(voucherId, plateNo));
}
@GetMapping("/voucher-images")
@ApiOperationSupport(order = 2)
@Operation(summary = "查询运单已关联凭证图片")
@@ -34,7 +34,7 @@ public class LoadingManageExcel implements Serializable {
private String loadingNo;
@ExcelProperty("配载子单号")
private String loadingSubNos;
@ExcelProperty("车牌号/航班号/船号/班列号")
@ExcelProperty("车牌号/船班号/船次号/班次号")
private String vehicleNo;
@ExcelProperty("司机")
private String driverName;
@@ -59,7 +59,7 @@ public class MasterOrderWaybillExcel {
private String carrierName;
@ExcelProperty("司机")
private String driverName;
@ExcelProperty("车牌号/航班号/船号/班列号")
@ExcelProperty("车牌号/船班号/船次号/班次号")
private String vehicleNo;
@ExcelProperty("开始日期")
private LocalDate startDate;
@@ -98,7 +98,7 @@ public class WaybillExcel implements Serializable {
private String driverName;
@ExcelProperty("司机手机号")
private String driverPhone;
@ExcelProperty("车/船/航班/班列号")
@ExcelProperty("车牌号/船班号/船次号/班次号")
private String vehicleNo;
@ExcelProperty("挂车车牌号")
private String trailerVehicleNo;
@@ -28,7 +28,7 @@ public class WaybillImportBatchExcel implements Serializable {
private String originalNo;
@ExcelProperty("配载标识号")
private String loadingIdentifier;
@ExcelProperty("*车牌号/航班号/船号/班列号")
@ExcelProperty("*车牌号/船班号/船次号/班次号")
private String vehicleNo;
@ExcelProperty("*运输方式")
private String transportType;
@@ -19,6 +19,7 @@ public interface IVoucherManageService extends BaseService<VoucherManage> {
VoucherManageVO detail(Long id);
IPage<VoucherFolderVO> folderPage(IPage<?> page, Long voucherId, String plateNo, Integer matched);
VoucherFolderVO folderDetail(Long voucherId, String plateNo);
List<VoucherFolderVO> listWaybillFolders(Long waybillId);
void replaceFolder(Long voucherId, String plateNo, MultipartFile file);
void replaceFolderByObject(Long voucherId, String plateNo, String objectKey, String fileName, Long size, String contentType);
void removeFolder(Long voucherId, String plateNo);
@@ -42,6 +42,7 @@ import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
@@ -176,7 +177,11 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
.eq(LoadingManage::getLoadingNo, normalizedLoadingNo)
.eq(LoadingManage::getIsDeleted, 0), false);
if (existing != null) {
throw new ServiceException("配载标识号已存在:" + normalizedLoadingNo);
if (!"批量导入".equals(existing.getDataSource())) {
throw new ServiceException("配载标识号已存在:" + normalizedLoadingNo);
}
appendImportedWaybills(existing, waybills, normalizedLoadingNo);
return;
}
Waybill first = waybills.get(0);
Waybill last = waybills.get(waybills.size() - 1);
@@ -221,7 +226,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
loadingManage.setTaskInfoJson(first.getTaskInfoJson());
loadingManage.setDeptId(first.getDeptId());
loadingManage.setDeptName(first.getDeptName());
loadingManage.setBusinessStatus(STATUS_COMPLETED);
loadingManage.setBusinessStatus(STATUS_RUNNING.equals(first.getBusinessStatus()) ? STATUS_RUNNING : STATUS_COMPLETED);
save(loadingManage);
waybillMapper.update(null, Wrappers.<Waybill>lambdaUpdate()
@@ -229,6 +234,27 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
.set(Waybill::getLoadingNo, normalizedLoadingNo));
}
/**
* 同一配载标识号再次导入时,把新运单补进已有的导入配载单。
*/
private void appendImportedWaybills(LoadingManage existing, List<Waybill> waybills, String loadingNo) {
Set<Long> waybillIds = new LinkedHashSet<>(waybillIds(existing.getWaybillIdsJson()));
waybills.stream().map(Waybill::getId).filter(Objects::nonNull).forEach(waybillIds::add);
List<Long> mergedWaybillIds = new ArrayList<>(waybillIds);
List<Waybill> linkedWaybills = waybillMapper.selectList(Wrappers.<Waybill>lambdaQuery()
.in(Waybill::getId, mergedWaybillIds)
.eq(Waybill::getIsDeleted, 0));
existing.setWaybillIdsJson(JsonUtil.toJson(mergedWaybillIds));
existing.setLoadingSubNos(linkedWaybills.stream()
.map(Waybill::getWaybillNo)
.filter(Func::isNotEmpty)
.collect(Collectors.joining(",")));
updateById(existing);
waybillMapper.update(null, Wrappers.<Waybill>lambdaUpdate()
.in(Waybill::getId, mergedWaybillIds)
.set(Waybill::getLoadingNo, loadingNo));
}
/**
* 根据导入运单构建配载单的途经地:
* 组内运单按导入顺序串联路线,上一票的到货地址与下一票的发货地址相同(中途卸货点)时只保留一个点,
@@ -644,7 +670,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
return;
}
TransportBusinessSupport.validateRequired(loadingManage.getCarrierType(), "承运类型不能为空");
TransportBusinessSupport.validateRequired(loadingManage.getVehicleNo(), "车/船/航班/班列不能为空");
TransportBusinessSupport.validateRequired(loadingManage.getVehicleNo(), "车牌号/船班号/船次号/班次号不能为空");
TransportBusinessSupport.validateRequired(loadingManage.getDepartureAddress(), "发货地不能为空");
TransportBusinessSupport.validateRequired(loadingManage.getArrivalAddress(), "到货地不能为空");
if (Objects.equals(loadingManage.getCarrierType(), "承运商")
@@ -663,7 +689,7 @@ public class LoadingManageServiceImpl extends BaseServiceImpl<LoadingManageMappe
TransportBusinessSupport.validateLength(loadingManage.getTransportType(), 100, "运输类型不能超过100字");
TransportBusinessSupport.validateLength(loadingManage.getCargoType(), 100, "货物类型不能超过100字");
TransportBusinessSupport.validateLength(loadingManage.getCargoName(), 100, "货物名称不能超过100字");
TransportBusinessSupport.validateLength(loadingManage.getVehicleNo(), 100, "车/船/航班/班列不能超过100字");
TransportBusinessSupport.validateLength(loadingManage.getVehicleNo(), 100, "车牌号/船班号/船次号/班次号不能超过100字");
TransportBusinessSupport.validateLength(loadingManage.getDriverName(), 100, "司机不能超过100字");
TransportBusinessSupport.validateLength(loadingManage.getDriverPhone(), 50, "联系电话不能超过50字");
TransportBusinessSupport.validateLength(loadingManage.getTrailerVehicleNo(), 100, "挂车车牌号不能超过100字");
@@ -9,6 +9,7 @@ import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.jackson.JsonUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.mapper.LoadingManageMapper;
import org.springblade.transport.mapper.VoucherManageMapper;
import org.springblade.transport.mapper.VoucherImageMapper;
import org.springblade.transport.mapper.VoucherFileMapper;
@@ -18,6 +19,7 @@ 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;
import org.springblade.transport.pojo.entity.LoadingManage;
import org.springblade.transport.pojo.entity.VoucherManage;
import org.springblade.transport.pojo.entity.VoucherWaybillBatch;
import org.springblade.transport.pojo.entity.VoucherImage;
@@ -66,6 +68,7 @@ import java.nio.file.Path;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;
import java.util.regex.Pattern;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.springframework.web.multipart.MultipartFile;
@@ -79,6 +82,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
private final IWaybillService waybillService;
private final VoucherImageMapper voucherImageMapper;
private final VoucherFileMapper voucherFileMapper;
private final LoadingManageMapper loadingManageMapper;
private final ApplicationEventPublisher eventPublisher;
private final ObjectProvider<MinioClient> minioClientProvider;
private final String minioBucketName;
@@ -87,6 +91,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
public VoucherManageServiceImpl(VoucherWaybillBatchMapper voucherWaybillBatchMapper, IProjectApplyService projectApplyService,
IWaybillService waybillService,
VoucherImageMapper voucherImageMapper, VoucherFileMapper voucherFileMapper,
LoadingManageMapper loadingManageMapper,
ApplicationEventPublisher eventPublisher, ObjectProvider<MinioClient> minioClientProvider,
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}") String minioBucketName,
@Value("${file.storage.minio.root-directory:${minio.root-directory:}}") String minioRootDirectory) {
@@ -95,6 +100,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
this.waybillService = waybillService;
this.voucherImageMapper = voucherImageMapper;
this.voucherFileMapper = voucherFileMapper;
this.loadingManageMapper = loadingManageMapper;
this.eventPublisher = eventPublisher;
this.minioClientProvider = minioClientProvider;
this.minioBucketName = minioBucketName;
@@ -104,7 +110,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
@Override
public IPage<VoucherManageVO> selectPage(IPage<VoucherManage> page, VoucherManageVO 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());
@@ -182,12 +188,22 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
if (Func.isEmpty(fileName)) throw new ServiceException("凭证文件名称不能为空");
boolean isZipFile = fileName.toLowerCase().endsWith(".zip");
if (!isZipFile && !isImageFile(fileName)) throw new ServiceException("仅支持 jpg、jpeg、png、bmp、webp 格式的凭证图片或 zip 压缩包");
List<VoucherFile> oldFiles = voucherFileMapper.selectList(Wrappers.<VoucherFile>lambdaQuery()
.eq(VoucherFile::getTenantId, voucher.getTenantId()).eq(VoucherFile::getVoucherId, voucherId)
.eq(VoucherFile::getPlateNo, normalizedPlateNo).eq(VoucherFile::getIsDeleted, 0));
List<VoucherImage> oldImages = voucherImageMapper.selectList(Wrappers.<VoucherImage>lambdaQuery()
.eq(VoucherImage::getTenantId, voucher.getTenantId()).eq(VoucherImage::getVoucherId, voucherId)
.eq(VoucherImage::getPlateNo, normalizedPlateNo).eq(VoucherImage::getIsDeleted, 0));
if (isZipFile) {
try {
// 压缩包里只要有一个文件夹名对上已有车牌或运单车牌,就替换对应凭证,不必整包都匹配。
if (replaceZipFoldersByName(voucher, normalizedPlateNo, uploadFile)) {
refreshVoucherCounts(voucher);
return;
}
} catch (ServiceException exception) {
throw exception;
} catch (Exception exception) {
log.error("按文件夹替换车牌凭证失败 voucherId={}, plateNo={}", voucherId, normalizedPlateNo, exception);
throw new ServiceException("凭证文件上传失败");
}
}
List<VoucherFile> oldFiles = listNormalizedPlateFiles(voucher, normalizedPlateNo);
List<VoucherImage> oldImages = listNormalizedPlateImages(voucher, normalizedPlateNo);
if (oldFiles.isEmpty() && oldImages.isEmpty()) throw new ServiceException("车牌凭证不存在");
if (oldFiles.stream().anyMatch(item -> Objects.equals(item.getMatched(), 1))
|| oldImages.stream().anyMatch(item -> Objects.equals(item.getMatched(), 1))) {
@@ -228,11 +244,10 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
// 分片上传完成后对象已存在于 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);
replaceFolder(voucherId, plateNo, uploadedFile, false);
log.info("[分片上传] 压缩包下载、解压并匹配完成 voucherId={}, requestedPlateNo={}, objectKey={}",
voucherId, plateNo, objectKey);
} catch (ServiceException exception) {
throw exception;
} catch (Exception exception) {
@@ -381,9 +396,108 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
folders.get(index).setVoucherNo(voucher.getVoucherBatchNo() + "-" + String.format("%04d", index + 1));
if (!includeFiles) folders.get(index).setFiles(null);
}
fillFolderLoadingNo(folders);
return folders;
}
@Override
public List<VoucherFolderVO> listWaybillFolders(Long waybillId) {
if (waybillId == null) return List.of();
Waybill waybill = waybillService.getById(waybillId);
if (waybill == null || Objects.equals(waybill.getIsDeleted(), 1)) return List.of();
Set<Long> voucherIds = new HashSet<>();
voucherImageMapper.selectList(Wrappers.<VoucherImage>lambdaQuery()
.eq(VoucherImage::getWaybillId, waybillId)
.eq(VoucherImage::getIsDeleted, 0))
.forEach(image -> {
if (image.getVoucherId() != null) voucherIds.add(image.getVoucherId());
});
voucherFileMapper.selectList(Wrappers.<VoucherFile>lambdaQuery()
.eq(VoucherFile::getWaybillId, waybillId)
.eq(VoucherFile::getIsDeleted, 0))
.forEach(file -> {
if (file.getVoucherId() != null) voucherIds.add(file.getVoucherId());
});
if (voucherIds.isEmpty() && Func.isNotEmpty(waybill.getBatchNo())) {
list(Wrappers.<VoucherManage>lambdaQuery()
.eq(VoucherManage::getTenantId, waybill.getTenantId())
.eq(VoucherManage::getIsDeleted, 0))
.stream()
.filter(voucher -> containsWaybillBatchNo(voucher.getWaybillBatchNo(), waybill.getBatchNo()))
.forEach(voucher -> voucherIds.add(voucher.getId()));
}
List<VoucherFolderVO> folders = new ArrayList<>();
for (Long voucherId : voucherIds) {
VoucherManage voucher = getById(voucherId);
if (!visibleWaybillVoucher(voucher)) continue;
folderRecords(voucher, false).stream()
.filter(folder -> folderBelongsToWaybill(folder, waybill))
.forEach(folders::add);
}
folders.sort(Comparator.comparing(VoucherFolderVO::getCreateTime, Comparator.nullsLast(Comparator.reverseOrder())));
return folders;
}
private boolean folderBelongsToWaybill(VoucherFolderVO folder, Waybill waybill) {
if (Objects.equals(folder.getWaybillId(), waybill.getId())) return true;
if (folder.getWaybillId() != null) return false;
String plateNo = normalizePlateNo(folder.getPlateNo());
return Func.isNotEmpty(plateNo) && waybillPlateNumbers(waybill).contains(plateNo);
}
/**
* 承运商上传的凭证只在审核通过后展示到运单执行详情;内部上传保持原展示规则。
*/
private boolean visibleWaybillVoucher(VoucherManage voucher) {
return voucher != null && !Objects.equals(voucher.getIsDeleted(), 1)
&& (!"承运商".equals(voucher.getUploadSource()) || "审核通过".equals(voucher.getAuditStatus()));
}
private boolean containsWaybillBatchNo(String batchNos, String batchNo) {
return Func.isNotEmpty(batchNos) && Func.isNotEmpty(batchNo)
&& Arrays.stream(batchNos.split(",")).map(String::trim).anyMatch(batchNo::equals);
}
private void fillFolderLoadingNo(List<VoucherFolderVO> folders) {
List<Long> waybillIds = folders.stream().map(VoucherFolderVO::getWaybillId).filter(Objects::nonNull).distinct().toList();
if (waybillIds.isEmpty()) return;
Map<Long, Waybill> waybillMap = waybillService.listByIds(waybillIds).stream()
.collect(Collectors.toMap(Waybill::getId, item -> item, (left, right) -> left));
Map<Long, String> loadingNoMap = new HashMap<>();
for (Long waybillId : waybillIds) {
Waybill waybill = waybillMap.get(waybillId);
String loadingNo = waybill == null ? null : waybill.getLoadingNo();
if (Func.isEmpty(loadingNo)) {
loadingNo = findLoadingNo(waybill == null ? null : waybill.getTenantId(), waybillId);
}
if (Func.isNotEmpty(loadingNo)) loadingNoMap.put(waybillId, loadingNo);
}
folders.forEach(folder -> {
if (folder.getWaybillId() != null) folder.setLoadingNo(loadingNoMap.get(folder.getWaybillId()));
});
}
private String findLoadingNo(String tenantId, Long waybillId) {
if (waybillId == null) return null;
String waybillIdText = String.valueOf(waybillId);
LambdaQueryWrapper<LoadingManage> query = Wrappers.<LoadingManage>lambdaQuery()
.like(LoadingManage::getWaybillIdsJson, waybillIdText)
.orderByDesc(LoadingManage::getCreateTime)
.last("limit 20");
if (Func.isNotEmpty(tenantId)) query.eq(LoadingManage::getTenantId, tenantId);
return loadingManageMapper.selectList(query).stream()
.filter(item -> containsJsonId(item.getWaybillIdsJson(), waybillIdText))
.map(LoadingManage::getLoadingNo)
.filter(Func::isNotEmpty)
.findFirst()
.orElse(null);
}
private boolean containsJsonId(String json, String idText) {
if (Func.isEmpty(json) || Func.isEmpty(idText)) return false;
return Arrays.stream(json.split("\\D+")).anyMatch(idText::equals);
}
private void addFolderFile(Map<String, VoucherFolderVO> grouped, VoucherManage voucher, VoucherFileVO file, java.util.Date createTime, java.util.Date updateTime) {
String plate = normalizePlateNo(file.getPlateNo());
if (Func.isEmpty(plate)) plate = "未识别车牌";
@@ -398,6 +512,174 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
if (folder.getFileName() == null) folder.setFileName(file.getFileName());
}
/**
* 按压缩包内文件夹名替换凭证。文件夹名与当前车牌、已有凭证车牌或关联运单车牌任一一致即替换。
*
* @return 是否至少替换了一个匹配文件夹
*/
private boolean replaceZipFoldersByName(VoucherManage voucher, String requestedPlateNo, MultipartFile zipFile) throws Exception {
Path archivePath = Files.createTempFile("voucher-replace-", ".zip");
try (InputStream source = zipFile.getInputStream()) {
Files.copy(source, archivePath, java.nio.file.StandardCopyOption.REPLACE_EXISTING);
}
try {
Map<String, Waybill> waybillByPlate = new HashMap<>();
for (Waybill waybill : listRelatedWaybills(voucher)) {
for (String waybillPlateNo : waybillPlateNumbers(waybill)) {
waybillByPlate.putIfAbsent(waybillPlateNo, waybill);
}
}
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()));
Charset archiveCharset = detectArchiveCharset(archivePath);
Set<String> matchedPlates = 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;
String plateNo = plateFromZipEntry(entry.getName(), requestedPlateNo, existingPlates, waybillByPlate);
if (Func.isNotEmpty(plateNo)) matchedPlates.add(plateNo);
}
}
if (matchedPlates.isEmpty()) return false;
log.info("[单个上传] 压缩包文件夹命中,准备替换 voucherId={}, requestedPlateNo={}, matchedPlates={}",
voucher.getId(), requestedPlateNo, matchedPlates);
matchedPlates.forEach(plateNo -> deletePlateVoucherFiles(voucher, plateNo));
int fileCount = 0;
int imageCount = 0;
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;
String entryName = entry.getName().replace('\\', '/');
String plateNo = plateFromZipEntry(entryName, requestedPlateNo, existingPlates, waybillByPlate);
if (!matchedPlates.contains(plateNo)) continue;
List<String> pathParts = Arrays.stream(entryName.split("/")).filter(Func::isNotEmpty).toList();
if (pathParts.isEmpty()) continue;
String rawFileName = pathParts.get(pathParts.size() - 1);
boolean imageFile = isImageFile(rawFileName);
String storedFileName = safeFileName(rawFileName);
if (Func.isEmpty(storedFileName)) continue;
Waybill matchedWaybill = waybillByPlate.get(plateNo);
if (saveZipVoucherFile(voucher, plateNo, matchedWaybill, entryName, storedFileName, imageFile, entry, zipInputStream)) {
fileCount++;
if (imageFile) imageCount++;
}
}
}
if (fileCount == 0) throw new ServiceException("压缩包内没有找到有效的文件");
log.info("[单个上传] 按文件夹替换完成 voucherId={}, matchedPlates={}, fileCount={}, imageCount={}",
voucher.getId(), matchedPlates, fileCount, imageCount);
return true;
} finally {
deleteTempArchive(archivePath);
}
}
/**
* 从压缩包路径的每一层目录识别车牌。车牌文件夹外面还套了其它目录时,也不能漏掉。
* 优先当前重新上传的车牌,其次关联运单车牌,再其次批次里已有车牌。
*/
private String plateFromZipEntry(String entryName, String requestedPlateNo, Set<String> existingPlates, Map<String, Waybill> waybillByPlate) {
if (Func.isEmpty(entryName)) return null;
String normalizedName = entryName.replace('\\', '/');
if (normalizedName.equals("__MACOSX") || normalizedName.startsWith("__MACOSX/")) return null;
List<String> pathParts = Arrays.stream(normalizedName.split("/")).filter(Func::isNotEmpty).toList();
if (pathParts.size() <= 1) return null;
List<String> directories = new ArrayList<>();
for (int index = 0; index < pathParts.size() - 1; index++) {
String plateNo = normalizePlateNo(pathParts.get(index));
if (Func.isEmpty(plateNo) || "凭证导入".equals(plateNo) || plateNo.startsWith("__MACOSX") || ".".equals(plateNo)) continue;
directories.add(plateNo);
}
if (directories.isEmpty()) return null;
if (Func.isNotEmpty(requestedPlateNo) && directories.contains(requestedPlateNo)) return requestedPlateNo;
for (String plateNo : directories) {
if (waybillByPlate.containsKey(plateNo)) return plateNo;
}
for (String plateNo : directories) {
if (existingPlates.contains(plateNo)) return plateNo;
}
return null;
}
private List<VoucherFile> listNormalizedPlateFiles(VoucherManage voucher, String plateNo) {
String normalizedPlateNo = normalizePlateNo(plateNo);
return voucherFileMapper.selectList(Wrappers.<VoucherFile>lambdaQuery()
.eq(VoucherFile::getTenantId, voucher.getTenantId())
.eq(VoucherFile::getVoucherId, voucher.getId())
.eq(VoucherFile::getIsDeleted, 0))
.stream()
.filter(file -> normalizedPlateNo.equals(normalizePlateNo(file.getPlateNo())))
.toList();
}
private List<VoucherImage> listNormalizedPlateImages(VoucherManage voucher, String plateNo) {
String normalizedPlateNo = normalizePlateNo(plateNo);
return voucherImageMapper.selectList(Wrappers.<VoucherImage>lambdaQuery()
.eq(VoucherImage::getTenantId, voucher.getTenantId())
.eq(VoucherImage::getVoucherId, voucher.getId())
.eq(VoucherImage::getIsDeleted, 0))
.stream()
.filter(image -> normalizedPlateNo.equals(normalizePlateNo(image.getPlateNo())))
.toList();
}
private void deletePlateVoucherFiles(VoucherManage voucher, String plateNo) {
List<VoucherFile> oldFiles = listNormalizedPlateFiles(voucher, plateNo);
List<VoucherImage> oldImages = listNormalizedPlateImages(voucher, plateNo);
oldFiles.forEach(item -> voucherFileMapper.deleteById(item.getId()));
oldImages.forEach(item -> voucherImageMapper.deleteById(item.getId()));
java.util.stream.Stream.concat(oldFiles.stream().map(VoucherFile::getObjectKey), oldImages.stream().map(VoucherImage::getObjectKey))
.filter(Func::isNotEmpty).distinct().forEach(this::deleteObjectQuietly);
}
private boolean saveZipVoucherFile(VoucherManage voucher, String plateNo, Waybill matchedWaybill, String entryName,
String fileName, boolean imageFile, ZipEntry entry, ZipInputStream zipInputStream) throws Exception {
String waybillNo = matchedWaybill == null ? "unmatched" : safePathPart(matchedWaybill.getWaybillNo());
String objectKey = buildObjectKey(voucher.getId(), waybillNo, plateNo, entryName);
String contentType = contentType(fileName);
minioClient().putObject(PutObjectArgs.builder().bucket(minioBucketName).object(objectKey)
.stream(zipInputStream, entry.getSize(), 10 * 1024 * 1024)
.contentType(contentType).build());
VoucherFile file = new VoucherFile();
file.setVoucherId(voucher.getId());
file.setVoucherBatchNo(voucher.getVoucherBatchNo());
file.setWaybillId(matchedWaybill == null ? null : matchedWaybill.getId());
file.setWaybillNo(matchedWaybill == null ? null : matchedWaybill.getWaybillNo());
file.setPlateNo(plateNo);
file.setFolderName(plateNo);
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(matchedWaybill == null ? 0 : 1);
file.setTenantId(voucher.getTenantId());
voucherFileMapper.insert(file);
if (!imageFile) return true;
VoucherImage image = new VoucherImage();
image.setVoucherId(voucher.getId());
image.setVoucherBatchNo(voucher.getVoucherBatchNo());
image.setWaybillId(matchedWaybill == null ? null : matchedWaybill.getId());
image.setWaybillNo(matchedWaybill == null ? null : matchedWaybill.getWaybillNo());
image.setPlateNo(plateNo);
image.setImageName(fileName);
image.setObjectKey(objectKey);
image.setMatched(matchedWaybill == null ? 0 : 1);
image.setTenantId(voucher.getTenantId());
voucherImageMapper.insert(image);
return true;
}
private void replaceFolderWithZip(VoucherManage voucher, String plateNo, Waybill matchedWaybill, MultipartFile zipFile) throws Exception {
log.info("[单个上传] 开始解压替换 voucherId={}, plateNo={}, fileName={}", voucher.getId(), plateNo, zipFile.getOriginalFilename());
Charset zipCharset = StandardCharsets.UTF_8;
@@ -583,23 +865,82 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
return countVoucherFolderRelations(voucher.getTenantId(), voucher.getId()).total();
}
/**
* 按压缩包中的车牌文件夹统计。凭证数量是车牌文件夹个数;已关联、未关联是这些文件夹是否匹配到运单。
* 同一车牌下的多张图片只计 1 个文件夹,图片张数不计入。
*/
private VoucherFolderCounts countVoucherFolderRelations(String tenantId, Long voucherId) {
VoucherManage voucher = getById(voucherId);
Set<String> waybillPlates = new HashSet<>();
if (voucher != null) {
for (Waybill waybill : listRelatedWaybills(voucher)) {
waybillPlates.addAll(waybillPlateNumbers(waybill));
}
}
List<VoucherFile> files = voucherFileMapper.selectList(Wrappers.<VoucherFile>lambdaQuery()
.eq(VoucherFile::getTenantId, tenantId).eq(VoucherFile::getVoucherId, voucherId).eq(VoucherFile::getIsDeleted, 0));
Map<String, Boolean> relatedByFolder = new LinkedHashMap<>();
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());
for (VoucherFile file : files) {
accumulatePlateFolder(relatedByFolder, waybillPlates, file.getPlateNo(), file.getFolderName(), file.getEntryName(), file.getMatched());
}
} else {
List<VoucherImage> images = voucherImageMapper.selectList(Wrappers.<VoucherImage>lambdaQuery()
.eq(VoucherImage::getTenantId, tenantId).eq(VoucherImage::getVoucherId, voucherId).eq(VoucherImage::getIsDeleted, 0));
for (VoucherImage image : images) {
accumulatePlateFolder(relatedByFolder, waybillPlates, image.getPlateNo(), image.getPlateNo(), null, image.getMatched());
}
}
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());
int related = (int) relatedByFolder.values().stream().filter(Boolean::booleanValue).count();
int total = relatedByFolder.size();
return new VoucherFolderCounts(total, related, total - related);
}
private void accumulatePlateFolder(Map<String, Boolean> relatedByFolder, Set<String> waybillPlates,
String plateNo, String folderName, String entryName, Integer matched) {
String plateFolder = countedPlateFolder(plateNo, folderName, entryName, waybillPlates);
if (Func.isEmpty(plateFolder)) return;
boolean related = Objects.equals(matched, 1) || waybillPlates.contains(plateFolder);
relatedByFolder.merge(plateFolder, related, (left, right) -> left || right);
}
/**
* 从车牌字段和压缩包路径中识别车牌文件夹。外层业务目录、内层图片目录都不计入凭证数量。
*/
private String countedPlateFolder(String plateNo, String folderName, String entryName, Set<String> waybillPlates) {
List<String> candidates = new ArrayList<>();
addPlateCandidate(candidates, plateNo);
addPlateCandidate(candidates, folderName);
if (Func.isNotEmpty(entryName)) {
List<String> pathParts = Arrays.stream(entryName.replace('\\', '/').split("/")).filter(Func::isNotEmpty).toList();
int directoryCount = Math.max(pathParts.size() - 1, 0);
for (int index = 0; index < directoryCount; index++) {
addPlateCandidate(candidates, pathParts.get(index));
}
}
for (String candidate : candidates) {
if (waybillPlates.contains(candidate)) return candidate;
}
for (String candidate : candidates) {
if (isPlateFolderName(candidate)) return candidate;
}
return null;
}
private void addPlateCandidate(List<String> candidates, String plateNo) {
String normalizedPlateNo = normalizePlateNo(plateNo);
if (Func.isNotEmpty(normalizedPlateNo) && !"未识别车牌".equals(normalizedPlateNo) && !candidates.contains(normalizedPlateNo)) {
candidates.add(normalizedPlateNo);
}
}
private boolean isPlateFolderName(String plateNo) {
return Func.isNotEmpty(plateNo) && PLATE_FOLDER_PATTERN.matcher(plateNo).matches();
}
private static final Pattern PLATE_FOLDER_PATTERN = Pattern.compile(
"^[京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-Z0-9]{4,6}(?:挂|学|警|港|澳)?$");
private record VoucherFolderCounts(int total, int related, int unrelated) {}
private String folderKey(String plateNo) {
@@ -1020,13 +1361,19 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
if (pathParts.size() <= 1) {
return null;
}
for (String pathPart : pathParts) {
String normalizedPart = normalizePlateNo(pathPart);
for (int index = 0; index < pathParts.size() - 1; index++) {
String normalizedPart = normalizePlateNo(pathParts.get(index));
if (Func.isNotEmpty(normalizedPart) && waybillByPlate.containsKey(normalizedPart)) {
return safeArchiveSegment(pathPart);
return safeArchiveSegment(pathParts.get(index));
}
}
// 未匹配车牌时仍保留最接近文件名的目录,便于前端展示和后续人工替换。
for (int index = 0; index < pathParts.size() - 1; index++) {
String normalizedPart = normalizePlateNo(pathParts.get(index));
if (isPlateFolderName(normalizedPart)) {
return safeArchiveSegment(pathParts.get(index));
}
}
// 未识别到车牌目录时仍保留最接近文件名的目录,便于前端展示和后续人工替换。
return safeArchiveSegment(pathParts.get(pathParts.size() - 2));
}
@@ -45,6 +45,7 @@ import org.springblade.transport.service.ITransportPlanService;
import org.springblade.transport.service.IWaybillImportBatchService;
import org.springblade.transport.service.IWaybillService;
import org.springblade.transport.support.TransportBusinessSupport;
import org.springblade.transport.support.WaybillProcessSupport;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -68,8 +69,9 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImportBatchMapper, WaybillImportBatch> implements IWaybillImportBatchService {
/** 批量导入状态仅保留草稿与导入完成两种。 */
/** 保存草稿仍使用 draft;确认导入的状态与运单业务状态一致。 */
private static final String STATUS_DRAFT = "draft";
private static final String STATUS_RUNNING = "running";
private static final String STATUS_COMPLETED = "completed";
private static final String IMPORT_TYPE_WAYBILL = "waybill";
private static final String IMPORT_TYPE_SETTLEMENT = "settlement";
@@ -101,7 +103,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
if (Func.isEmpty(request.getRows())) throw new ServiceException("请上传至少一条运单明细");
WaybillImportValidateResultVO result = new WaybillImportValidateResultVO();
String importStatus = STATUS_DRAFT.equals(request.getStatus()) ? STATUS_DRAFT : STATUS_COMPLETED;
String importStatus = resolveImportStatus(request.getStatus());
boolean draft = STATUS_DRAFT.equals(importStatus);
// 草稿状态不校验,直接返回成功
@@ -141,7 +143,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
public WaybillImportBatchVO confirm(WaybillImportBatchRequest request) {
if (Func.isEmpty(request.getRows())) throw new ServiceException("请上传至少一条运单明细");
String importStatus = STATUS_DRAFT.equals(request.getStatus()) ? STATUS_DRAFT : STATUS_COMPLETED;
String importStatus = resolveImportStatus(request.getStatus());
boolean draft = STATUS_DRAFT.equals(importStatus);
if (!draft) {
@@ -206,35 +208,62 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
mergeMultiCargoRows(rows, rowWaybills, waybillByRow, skipRows);
List<Waybill> waybills = new ArrayList<>();
Map<String, List<Waybill>> loadingWaybills = new TreeMap<>();
for (int index = 0; index < rows.size(); index++) {
if (skipRows.contains(index)) continue;
Waybill waybill = waybillByRow.getOrDefault(index, rowWaybills.get(index));
// 配载标识号必须取自行数据:运单落库(prepareForSave)会把新建运单的 loadingNo 置空
String loadingIdentifier = stringValue(rows.get(index), "loadingIdentifier", "配载标识号");
try {
if (draft) waybillService.saveDraft(waybill); else waybillService.submit(waybill);
} catch (Exception exception) {
throw new ServiceException("第" + (index + 1) + "行" + (draft ? "保存" : "导入") + "失败:" + exception.getMessage());
}
waybills.add(waybill);
if (!draft && IMPORT_TYPE_WAYBILL.equals(batch.getImportType()) && Func.isNotEmpty(loadingIdentifier)) {
loadingWaybills.computeIfAbsent(loadingIdentifier, key -> new ArrayList<>()).add(waybill);
// 进行中导入要保持运单为进行中,避免过程接单配置在查询时把状态改回待执行。
if (STATUS_RUNNING.equals(importStatus)) {
waybillService.update(Wrappers.<Waybill>lambdaUpdate()
.eq(Waybill::getId, waybill.getId())
.set(Waybill::getBusinessStatus, STATUS_RUNNING)
.set(Waybill::getDriverAcceptStatus, WaybillProcessSupport.ACCEPT_ACCEPTED));
waybill.setBusinessStatus(STATUS_RUNNING);
waybill.setDriverAcceptStatus(WaybillProcessSupport.ACCEPT_ACCEPTED);
}
waybills.add(waybill);
}
// 同一配载标识号有多条运单时,生成一张配载单并回写运单关联。
if (!draft && IMPORT_TYPE_WAYBILL.equals(batch.getImportType())) {
createLoadingsForSharedIdentifiers(batch);
}
loadingWaybills.forEach(loadingManageService::createFromImportedWaybills);
batch.setWaybillCount(existingWaybillCount + waybills.size());
if (append && !draft) {
batch.setImportStatus(STATUS_COMPLETED);
batch.setImportStatus(importStatus);
}
updateById(batch);
// 导入完成且导入类型为运单时,按合同费用生成模式(系统生成)同步生成应收应付明细。
if (!draft && IMPORT_TYPE_WAYBILL.equals(batch.getImportType())) {
// 导入状态为已完成时,按合同费用生成模式(系统生成)同步生成应收应付明细。
if (STATUS_COMPLETED.equals(importStatus) && IMPORT_TYPE_WAYBILL.equals(batch.getImportType())) {
receivablePayableDetailService.generateForImportedWaybills(waybills);
}
return batch;
}
/**
* 按批次内已落库运单的配载标识号分组。标识号相同且不少于两条时创建或补齐配载单。
*/
private void createLoadingsForSharedIdentifiers(WaybillImportBatch batch) {
List<Waybill> batchWaybills = waybillService.list(Wrappers.<Waybill>lambdaQuery()
.eq(Waybill::getImportBatchId, batch.getId())
.eq(Waybill::getIsDeleted, 0)
.orderByAsc(Waybill::getCreateTime)
.orderByAsc(Waybill::getId));
Map<String, List<Waybill>> loadingWaybills = new java.util.LinkedHashMap<>();
for (Waybill waybill : batchWaybills) {
if (Func.isEmpty(waybill.getLoadingIdentifier())) continue;
loadingWaybills.computeIfAbsent(waybill.getLoadingIdentifier(), key -> new ArrayList<>()).add(waybill);
}
loadingWaybills.forEach((loadingIdentifier, groupedWaybills) -> {
if (groupedWaybills.size() > 1) {
loadingManageService.createFromImportedWaybills(loadingIdentifier, groupedWaybills);
}
});
}
private void clearBatchWaybills(Long batchId) {
if (Func.isEmpty(batchId)) return;
List<Waybill> waybills = waybillService.list(Wrappers.<Waybill>lambdaQuery()
@@ -311,6 +340,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
public IPage<WaybillImportBatchVO> page(IPage<WaybillImportBatch> page, WaybillImportBatchRequest request) {
LambdaQueryWrapper<WaybillImportBatch> queryWrapper = Wrappers.<WaybillImportBatch>lambdaQuery()
.eq(WaybillImportBatch::getIsDeleted, 0)
.eq(request.getId() != null, WaybillImportBatch::getId, request.getId())
.like(Func.isNotEmpty(request.getBatchNo()), WaybillImportBatch::getBatchNo, request.getBatchNo())
.apply(Func.isNotEmpty(request.getCarrierId()), "FIND_IN_SET({0}, carrier_ids)", request.getCarrierId())
.eq(Func.isNotEmpty(request.getCarrierName()), WaybillImportBatch::getCarrierName, request.getCarrierName())
@@ -341,6 +371,23 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
return result;
}
/**
* 确认导入只接受进行中、已完成,并原样写入运单业务状态;保存草稿仍为 draft。
*/
private String resolveImportStatus(String status) {
if (STATUS_DRAFT.equals(status)) return STATUS_DRAFT;
if (STATUS_RUNNING.equals(status) || "进行中".equals(status)) return STATUS_RUNNING;
if (STATUS_COMPLETED.equals(status) || "已完成".equals(status)) return STATUS_COMPLETED;
throw new ServiceException("导入状态仅支持进行中或已完成");
}
private String importStatusName(String importStatus) {
if (STATUS_DRAFT.equals(importStatus)) return "草稿";
if (STATUS_RUNNING.equals(importStatus)) return "进行中";
if (STATUS_COMPLETED.equals(importStatus)) return "已完成";
return Func.toStr(importStatus);
}
private WaybillImportBatch buildBatch(WaybillImportBatchRequest request, String importStatus) {
if (Func.isEmpty(request.getProjectId())) throw new ServiceException("请选择项目");
if (Func.isEmpty(request.getContractId())) throw new ServiceException("请选择客户合同");
@@ -368,8 +415,8 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
if (row == null) throw new ServiceException("运单数据不正确");
Waybill waybill = new Waybill();
waybill.setOriginalNo(stringValue(row, "originalNo"));
waybill.setLoadingNo(stringValue(row, "loadingIdentifier", "配载标识号"));
waybill.setVehicleNo(stringValue(row, "vehicleNo", "车牌号/航班号/船号/班列号"));
waybill.setLoadingIdentifier(stringValue(row, "loadingIdentifier", "配载标识号"));
waybill.setVehicleNo(stringValue(row, "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号"));
waybill.setDriverId(longValue(row, "driverId", "司机ID"));
waybill.setDriverName(stringValue(row, "driverName", "司机/船长姓名", "司机/船长"));
waybill.setDriverPhone(stringValue(row, "driverPhone", "司机/船长手机号"));
@@ -507,7 +554,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
WaybillImportBatchVO vo = BeanUtil.copyProperties(batch, WaybillImportBatchVO.class);
if (vo == null) throw new ServiceException("运单批次数据转换失败");
vo.setImportTypeName(IMPORT_TYPE_SETTLEMENT.equals(batch.getImportType()) ? "结算单" : "运单");
vo.setStatusName(STATUS_DRAFT.equals(batch.getImportStatus()) ? "草稿" : "导入完成");
vo.setStatusName(importStatusName(batch.getImportStatus()));
vo.setCreateUserName(UserCache.getUserRealName(batch.getCreateUser()));
vo.setUpdateUserName(UserCache.getUserRealName(batch.getUpdateUser()));
return vo;
@@ -637,12 +684,12 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
}
// 检查同一配载标识号下车牌号是否一致
String currentVehicleNo = stringValue(row, "vehicleNo", "车牌号/航班号/船号/班列号");
String currentVehicleNo = stringValue(row, "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号");
for (Integer otherRowIndex : sameIdentifierRows) {
if (otherRowIndex.equals(rowIndex)) {
continue;
}
String otherVehicleNo = stringValue(allRows.get(otherRowIndex), "vehicleNo", "车牌号/航班号/船号/班列号");
String otherVehicleNo = stringValue(allRows.get(otherRowIndex), "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号");
if (Func.isNotEmpty(currentVehicleNo) && Func.isNotEmpty(otherVehicleNo)
&& !currentVehicleNo.equals(otherVehicleNo)) {
errors.add("同一配载标识号下,车牌号不一致");
@@ -653,14 +700,14 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
}
private void validateVehicleNo(Map<String, Object> row, List<String> errors) {
String vehicleNo = stringValue(row, "vehicleNo", "车牌号/航班号/船号/班列号");
String vehicleNo = stringValue(row, "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号");
String transportType = stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式");
if (Func.isEmpty(transportType)) {
return;
}
// 公路运输必须填写车牌号;其他运输方式的该字段可填写航班号、船号或班列号。
// 公路运输必须填写车牌号;其他运输方式的该字段可填写船班号、船次号或班次号。
if (!isRoadTransport(transportType)) {
return;
}
@@ -897,12 +944,12 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
}
// 检查同一运单标识号下车牌号是否一致
String currentVehicleNo = stringValue(row, "vehicleNo", "车牌号/航班号/船号/班列号");
String currentVehicleNo = stringValue(row, "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号");
for (Integer otherRowIndex : sameIdentifierRows) {
if (otherRowIndex.equals(rowIndex)) {
continue;
}
String otherVehicleNo = stringValue(allRows.get(otherRowIndex), "vehicleNo", "车牌号/航班号/船号/班列号");
String otherVehicleNo = stringValue(allRows.get(otherRowIndex), "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号");
if (Func.isNotEmpty(currentVehicleNo) && Func.isNotEmpty(otherVehicleNo)
&& !currentVehicleNo.equals(otherVehicleNo)) {
errors.add("同一运单标识号下,车牌号必须一致");
@@ -1042,7 +1089,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
WaybillImportBatchExcel excel = new WaybillImportBatchExcel();
excel.setOriginalNo(stringValue(row, "originalNo"));
excel.setLoadingIdentifier(stringValue(row, "loadingIdentifier", "配载标识号"));
excel.setVehicleNo(stringValue(row, "vehicleNo", "车牌号/航班号/船号/班列号"));
excel.setVehicleNo(stringValue(row, "vehicleNo", "车牌号/船班号/船次号/班次号", "车牌号/航班号/船号/班列号"));
excel.setTransportType(stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式"));
excel.setDriverName(stringValue(row, "driverName", "司机/船长姓名", "司机/船长"));
excel.setDriverPhone(stringValue(row, "driverPhone", "司机/船长手机号"));
@@ -715,6 +715,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
assertNotLoaded(oldRecord);
waybill.setWaybillNo(oldRecord.getWaybillNo());
waybill.setLoadingNo(oldRecord.getLoadingNo());
waybill.setLoadingIdentifier(oldRecord.getLoadingIdentifier());
waybill.setDeptId(oldRecord.getDeptId());
waybill.setDeptName(oldRecord.getDeptName());
waybill.setMileageRemark(oldRecord.getMileageRemark());
@@ -993,6 +994,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
target.setPlanName(source.getPlanName());
target.setMasterNo(null);
target.setLoadingNo(null);
target.setLoadingIdentifier(null);
target.setBatchNo(source.getBatchNo());
target.setRelationNo(source.getRelationNo());
target.setCurrentProcessNode(source.getCurrentProcessNode());
@@ -1418,6 +1420,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
waybill.setPlanName(TransportBusinessSupport.trimToNull(waybill.getPlanName()));
waybill.setMasterNo(TransportBusinessSupport.trimToNull(waybill.getMasterNo()));
waybill.setLoadingNo(TransportBusinessSupport.trimToNull(waybill.getLoadingNo()));
waybill.setLoadingIdentifier(TransportBusinessSupport.trimToNull(waybill.getLoadingIdentifier()));
waybill.setBatchNo(TransportBusinessSupport.trimToNull(waybill.getBatchNo()));
waybill.setRelationNo(TransportBusinessSupport.trimToNull(waybill.getRelationNo()));
waybill.setCurrentProcessNode(TransportBusinessSupport.trimToNull(waybill.getCurrentProcessNode()));
@@ -1490,7 +1493,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
TransportBusinessSupport.validateRequired(waybill.getQuantityUnit(), "数量单位不能为空");
TransportBusinessSupport.validateRequired(waybill.getCarrierType(), "承运类型不能为空");
TransportBusinessSupport.validateRequired(
waybill.getVehicleNo(), isRoadTransport(waybill.getTransportType()) ? "车牌号不能为空" : "船/航/班列号不能为空"
waybill.getVehicleNo(), isRoadTransport(waybill.getTransportType()) ? "车牌号不能为空" : "船班号/船次号/班次号不能为空"
);
if (isRoadTransport(waybill.getTransportType())) {
if ("承运商".equals(waybill.getCarrierType())) {
@@ -1527,7 +1530,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
TransportBusinessSupport.validateLength(waybill.getCarrierName(), 255, "承运商名称不能超过255字");
TransportBusinessSupport.validateLength(waybill.getDriverName(), 255, "司机姓名不能超过255字");
TransportBusinessSupport.validateLength(waybill.getDriverPhone(), 50, "司机手机号不能超过50字");
TransportBusinessSupport.validateLength(waybill.getVehicleNo(), 255, "车/船/航班/班列号不能超过255字");
TransportBusinessSupport.validateLength(waybill.getVehicleNo(), 255, "车牌号/船班号/船次号/班次号不能超过255字");
TransportBusinessSupport.validateLength(waybill.getCaptainName(), 20, "船长不能超过20字");
TransportBusinessSupport.validateLength(waybill.getCabinNo(), 30, "舱位不能超过30字");
TransportBusinessSupport.validateLength(waybill.getContainerNo(), 30, "箱号不能超过30字");
@@ -1637,7 +1640,7 @@ public class WaybillServiceImpl extends BaseServiceImpl<WaybillMapper, Waybill>
if (isRoadTransport(waybill.getTransportType())) {
return;
}
TransportBusinessSupport.validateLength(waybill.getVehicleNo(), 30, "船/航/班列号不能超过30字");
TransportBusinessSupport.validateLength(waybill.getVehicleNo(), 30, "船班号/船次号/班次号不能超过30字");
TransportBusinessSupport.validateLength(waybill.getCaptainName(), 20, "船长不能超过20字");
TransportBusinessSupport.validateLength(waybill.getCabinNo(), 30, "舱位不能超过30字");
TransportBusinessSupport.validateLength(waybill.getContainerNo(), 30, "箱号不能超过30字");
@@ -26,6 +26,8 @@ import org.springblade.core.mp.support.BaseEntityWrapper;
import org.springblade.core.secure.utils.AuthUtil;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.system.cache.DictBizCache;
import org.springblade.system.cache.DictCache;
import org.springblade.system.cache.UserCache;
import org.springblade.transport.pojo.entity.Waybill;
import org.springblade.transport.pojo.vo.WaybillVO;
@@ -58,6 +60,7 @@ public class WaybillWrapper extends BaseEntityWrapper<Waybill, WaybillVO> {
waybill.getBusinessStatus(), waybill.getProcessJson(), waybill.getDriverAcceptStatus());
waybillVO.setBusinessStatus(displayStatus);
waybillVO.setBusinessStatusName(businessStatusName(displayStatus));
waybillVO.setTransportTypeName(transportTypeName(waybill.getTransportType()));
waybillVO.setRequireAccept(WaybillProcessSupport.requiresDriverAcceptConfirmation(waybill.getProcessJson()));
// 未维护里程等占位值 -1 对外展示为空
if (isSentinelMinusOne(waybillVO.getMileage())) {
@@ -76,6 +79,29 @@ public class WaybillWrapper extends BaseEntityWrapper<Waybill, WaybillVO> {
return value != null && value.compareTo(BigDecimal.valueOf(-1)) == 0;
}
/**
* 运输方式按业务字典名称展示,系统内仍保存字典键。
*/
public static String transportTypeName(String value) {
if (Func.isEmpty(value)) {
return value;
}
String name = "";
try {
name = DictBizCache.getValue("transport_type", value);
} catch (Exception exception) {
name = "";
}
if (Func.isEmpty(name)) {
try {
name = DictCache.getValue("transport_type", value);
} catch (Exception exception) {
name = "";
}
}
return Func.isEmpty(name) ? value : name;
}
public static String businessStatusName(String status) {
if (status == null) {
return "未知";
+1
View File
@@ -283,6 +283,7 @@ CREATE TABLE `blade_waybill` (
`plan_name` varchar(100) DEFAULT NULL COMMENT '计划名称',
`master_no` varchar(100) DEFAULT NULL COMMENT '多联总单',
`loading_no` varchar(100) DEFAULT NULL COMMENT '配载单号',
`loading_identifier` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '配载标识号',
`batch_no` varchar(100) DEFAULT NULL COMMENT '运单批次号',
`relation_no` varchar(100) DEFAULT NULL COMMENT '关联单号',
`current_process_node` varchar(100) DEFAULT NULL COMMENT '当前过程节点',
@@ -0,0 +1,2 @@
ALTER TABLE `blade_waybill`
ADD COLUMN `loading_identifier` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL COMMENT '配载标识号' AFTER `loading_no`;