1、调整凭证

2、调整运单
This commit is contained in:
2026-09-08 22:41:40 +08:00
parent f370f56a92
commit bdafb83583
16 changed files with 1816 additions and 51 deletions
@@ -41,8 +41,10 @@ import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.transport.excel.ProcessConfigExportExcel;
import org.springblade.transport.mapper.VoucherImageMapper;
import org.springblade.transport.mapper.VoucherManageMapper;
import org.springblade.transport.pojo.entity.ProcessConfig;
import org.springblade.transport.pojo.entity.VoucherImage;
import org.springblade.transport.pojo.entity.VoucherManage;
import org.springblade.transport.pojo.vo.BusinessRemoveResultVO;
import org.springblade.transport.pojo.vo.ProcessConfigVO;
import org.springblade.transport.service.IProcessConfigService;
@@ -73,14 +75,17 @@ public class ProcessConfigController extends BladeController {
private final IProcessConfigService processConfigService;
private final VoucherImageMapper voucherImageMapper;
private final VoucherManageMapper voucherManageMapper;
private final MinioClient minioClient;
@Value("${file.storage.minio.bucket-name:${minio.bucket-name:}}")
private String minioBucketName;
public ProcessConfigController(IProcessConfigService processConfigService, VoucherImageMapper voucherImageMapper,
VoucherManageMapper voucherManageMapper,
MinioClient minioClient) {
this.processConfigService = processConfigService;
this.voucherImageMapper = voucherImageMapper;
this.voucherManageMapper = voucherManageMapper;
this.minioClient = minioClient;
}
@@ -92,8 +97,9 @@ public class ProcessConfigController extends BladeController {
ProcessConfigVO detail = processConfigService.detail(id);
detail.setHasRelatedVoucher(waybillId != null && voucherImageMapper.selectCount(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<VoucherImage>()
.eq(VoucherImage::getWaybillId, waybillId)
.eq(VoucherImage::getMatched, 1)) > 0);
.eq(VoucherImage::getWaybillId, waybillId)
.eq(VoucherImage::getMatched, 1)
.eq(VoucherImage::getIsDeleted, 0)) > 0);
return R.data(detail);
}
@@ -102,12 +108,22 @@ public class ProcessConfigController extends BladeController {
@Operation(summary = "查询运单已关联凭证图片")
public R<List<Map<String, Object>>> voucherImages(
@Parameter(description = "运单主键", required = true) @RequestParam Long waybillId) {
List<Map<String, Object>> images = voucherImageMapper.selectList(
List<VoucherImage> imageRecords = voucherImageMapper.selectList(
new com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper<VoucherImage>()
.eq(VoucherImage::getWaybillId, waybillId)
.eq(VoucherImage::getMatched, 1)
.orderByDesc(VoucherImage::getCreateTime))
.stream().map(image -> {
.eq(VoucherImage::getIsDeleted, 0)
.orderByDesc(VoucherImage::getCreateTime));
Map<Long, VoucherManage> voucherMap = voucherManageMapper.selectBatchIds(imageRecords.stream()
.map(VoucherImage::getVoucherId).filter(java.util.Objects::nonNull).distinct().toList()).stream()
.collect(java.util.stream.Collectors.toMap(VoucherManage::getId, item -> item, (left, right) -> left));
List<Map<String, Object>> images = imageRecords.stream()
// 承运商上传的凭证只允许审核通过后在运单详情展示,内部上传保持原有展示规则。
.filter(image -> {
VoucherManage voucher = voucherMap.get(image.getVoucherId());
return voucher == null || !"承运商".equals(voucher.getUploadSource())
|| "审核通过".equals(voucher.getAuditStatus());
}).map(image -> {
Map<String, Object> result = new LinkedHashMap<>();
result.put("id", image.getId());
result.put("imageName", image.getImageName());
@@ -126,6 +126,21 @@ public class TransportPlanController extends BladeController {
ExcelUtil.export(response, "运输计划模板", "运输计划导入模板", List.of(template), TransportPlanImportExcel.class);
}
@PostMapping("/validate-transport-plan")
@ApiOperationSupport(order = 6)
@Operation(summary = "校验运输计划导入数据", description = "传入 Excel、项目和客户合同")
public R validateTransportPlan(MultipartFile file, @RequestParam Long projectId, @RequestParam String projectName,
@RequestParam Long contractId, @RequestParam String contractName, @RequestParam String customerName,
HttpServletResponse response) {
List<TransportPlanImportExcel> failureList = transportPlanService.validateTransportPlan(
ExcelUtil.read(file, TransportPlanImportExcel.class), projectId, projectName, contractId, contractName, customerName);
if (Func.isNotEmpty(failureList)) {
ImportFailureExcelUtil.export(response, "运输计划导入失败明细" + DateUtil.time(), "导入失败明细", failureList, TransportPlanImportExcel.class);
return null;
}
return R.success("校验通过");
}
@PostMapping("/import-transport-plan")
@ApiOperationSupport(order = 7)
@Operation(summary = "导入运输计划", description = "传入 Excel、项目和客户合同")
@@ -69,6 +69,15 @@ public class VoucherManageController extends BladeController {
return R.success("上传成功");
}
@PostMapping("/folder-replace-object")
@Operation(summary = "替换单个车牌凭证(系统文件上传后处理)")
public R replaceFolderByObject(@RequestParam Long voucherId, @RequestParam String plateNo,
@RequestParam String objectKey, @RequestParam String fileName,
@RequestParam(required = false) Long size, @RequestParam(required = false) String contentType) {
voucherManageService.replaceFolderByObject(voucherId, plateNo, objectKey, fileName, size, contentType);
return R.success("上传成功");
}
@PostMapping("/folder-remove")
@Operation(summary = "删除车牌凭证")
public R removeFolder(@RequestParam Long voucherId, @RequestParam String plateNo) {
@@ -153,11 +153,18 @@ public class WaybillController extends BladeController {
return R.data(waybillImportBatchService.saveDraft(request));
}
@PostMapping("/import-batch/validate")
@ApiOperationSupport(order = 6)
@Operation(summary = "校验运单批量导入数据")
public void validateImportBatch(@RequestBody WaybillImportBatchRequest request, HttpServletResponse response) {
waybillImportBatchService.validate(request, response);
}
@PostMapping("/import-batch/confirm")
@ApiOperationSupport(order = 7)
@Operation(summary = "确认运单批量导入")
public R confirmImportBatch(@RequestBody WaybillImportBatchRequest request) {
return R.data(waybillImportBatchService.confirm(request));
public void confirmImportBatch(@RequestBody WaybillImportBatchRequest request, HttpServletResponse response) {
waybillImportBatchService.confirm(request, response);
}
@PostMapping("/import-batch/remove")
@@ -85,4 +85,7 @@ public class WaybillImportBatchExcel implements Serializable {
@ExcelProperty("同一运单标识号")
private String waybillIdentifier;
/** 导入失败原因(不导出到模板,仅用于失败明细) */
private String errorMessage;
}
@@ -8,8 +8,11 @@ import java.util.List;
import java.util.Map;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Delete;
public interface VoucherWaybillBatchMapper extends BaseMapper<VoucherWaybillBatch> {
@Delete("DELETE FROM blade_voucher_waybill_batch WHERE voucher_id = #{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,
@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);
@@ -45,6 +45,7 @@ public interface ITransportPlanService extends BaseService<TransportPlan> {
boolean submit(TransportPlan transportPlan);
BusinessRemoveResultVO removeTransportPlan(String ids);
List<TransportPlanExcel> exportTransportPlan(TransportPlanVO transportPlan, String ids);
List<TransportPlanImportExcel> validateTransportPlan(List<TransportPlanImportExcel> data, Long projectId, String projectName, Long contractId, String contractName, String customerName);
List<TransportPlanImportExcel> importTransportPlan(List<TransportPlanImportExcel> data, Long projectId, String projectName, Long contractId, String contractName, String customerName);
TransportPlanVO copy(Long id);
int dispatch(TransportPlanDispatchRequest request);
@@ -19,6 +19,7 @@ public interface IVoucherManageService extends BaseService<VoucherManage> {
IPage<VoucherFolderVO> folderPage(IPage<?> page, Long voucherId, String plateNo, Integer matched);
VoucherFolderVO folderDetail(Long voucherId, String plateNo);
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);
void submit(VoucherManageSubmitRequest request);
VoucherManage createUploadDraft(VoucherUploadDraftRequest request);
@@ -1,10 +1,7 @@
/**
* BladeX Commercial License Agreement
* Copyright (c) 2018-2099, https://bladex.cn. All rights reserved.
*/
package org.springblade.transport.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import jakarta.servlet.http.HttpServletResponse;
import org.springblade.core.mp.base.BaseService;
import org.springblade.transport.pojo.dto.WaybillImportBatchRequest;
import org.springblade.transport.pojo.entity.WaybillImportBatch;
@@ -14,7 +11,8 @@ import org.springblade.transport.pojo.vo.WaybillImportBatchVO;
/** 运单批次服务。 */
public interface IWaybillImportBatchService extends BaseService<WaybillImportBatch> {
WaybillImportBatch saveDraft(WaybillImportBatchRequest request);
WaybillImportBatch confirm(WaybillImportBatchRequest request);
void validate(WaybillImportBatchRequest request, HttpServletResponse response);
void confirm(WaybillImportBatchRequest request, HttpServletResponse response);
IPage<WaybillImportBatchVO> page(IPage<WaybillImportBatch> page, WaybillImportBatchRequest request);
BusinessRemoveResultVO removeBatches(String ids);
}
@@ -28,7 +28,6 @@ import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import lombok.AllArgsConstructor;
import org.springblade.core.log.exception.ServiceException;
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.BeanUtil;
@@ -37,9 +36,6 @@ import org.springblade.system.cache.UserCache;
import org.springblade.system.pojo.entity.Dept;
import org.springblade.transport.excel.TransportPlanExcel;
import org.springblade.transport.excel.TransportPlanImportExcel;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import org.springblade.transport.mapper.TransportPlanMapper;
import org.springblade.transport.pojo.dto.TransportPlanDispatchRequest;
import org.springblade.transport.pojo.entity.ContractManage;
@@ -57,6 +53,10 @@ import org.springblade.transport.wrapper.WaybillWrapper;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
@@ -66,7 +66,6 @@ import java.util.Map;
import java.util.Objects;
import java.util.TreeMap;
import java.util.stream.Collectors;
import java.math.BigDecimal;
/**
* 运输计划 服务实现类
@@ -196,6 +195,57 @@ public class TransportPlanServiceImpl extends BaseServiceImpl<TransportPlanMappe
}).toList();
}
@Override
public List<TransportPlanImportExcel> validateTransportPlan(List<TransportPlanImportExcel> data, Long projectId, String projectName, Long contractId, String contractName, String customerName) {
if (Func.isEmpty(data)) {
throw new ServiceException("导入数据不能为空");
}
if (Func.isEmpty(projectId)) {
throw new ServiceException("项目不能为空");
}
TransportBusinessSupport.validateRequired(projectName, "项目不能为空");
if (Func.isEmpty(contractId)) {
throw new ServiceException("客户合同不能为空");
}
TransportBusinessSupport.validateRequired(contractName, "客户合同不能为空");
// 只做校验,不入库
Map<Integer, TransportPlanImportExcel> errorMap = new TreeMap<>();
Map<String, Integer> planNameCountMap = buildImportPlanNameCountMap(data);
Map<String, List<Integer>> planGroupMap = buildImportPlanGroupMap(data);
Long currentDeptId = TransportBusinessSupport.currentDept("运输计划").getId();
for (int index = 0; index < data.size(); index++) {
TransportPlanImportExcel excel = data.get(index);
try {
LocalDate planStartDate = Func.isNotEmpty(excel.getPlanStartDate())
? parseImportDate(excel.getPlanStartDate(), "计划开始时间") : null;
LocalDate planEndDate = Func.isNotEmpty(excel.getPlanEndDate())
? parseImportDate(excel.getPlanEndDate(), "计划结束时间") : null;
List<String> validationErrors = validateImportExcel(excel, planStartDate, planEndDate, planNameCountMap, planGroupMap, currentDeptId);
if (Func.isNotEmpty(validationErrors)) {
excel.setErrorMessage(formatImportErrorMessage(validationErrors));
errorMap.put(index, excel);
} else {
// 校验通过,清空错误信息
excel.setErrorMessage("");
}
} catch (Exception exception) {
String message = exception instanceof ServiceException ? exception.getMessage() : "数据解析失败";
excel.setErrorMessage(formatImportErrorMessage(List.of(message)));
errorMap.put(index, excel);
}
}
// 返回所有数据(包含错误信息)
if (Func.isNotEmpty(errorMap)) {
return data;
}
return new ArrayList<>();
}
@Override
@Transactional(rollbackFor = Exception.class)
public List<TransportPlanImportExcel> importTransportPlan(List<TransportPlanImportExcel> data, Long projectId, String projectName, Long contractId, String contractName, String customerName) {
@@ -36,6 +36,7 @@ import org.springframework.transaction.annotation.Transactional;
import io.minio.MinioClient;
import io.minio.GetPresignedObjectUrlArgs;
import io.minio.GetObjectArgs;
import io.minio.PutObjectArgs;
import io.minio.RemoveObjectArgs;
import io.minio.http.Method;
@@ -195,6 +196,32 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
refreshVoucherCounts(voucher);
}
@Override
@Transactional(rollbackFor = Exception.class)
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("文件信息不完整");
validateMinioConfig();
MultipartFile uploadedFile = new MultipartFile() {
@Override public String getName() { return "file"; }
@Override public String getOriginalFilename() { return fileName; }
@Override public String getContentType() { return contentType; }
@Override public boolean isEmpty() { return size != null && size == 0; }
@Override public long getSize() { return size == null ? -1L : size; }
@Override public byte[] getBytes() throws java.io.IOException { try (InputStream input = getInputStream()) { return input.readAllBytes(); } }
@Override public InputStream getInputStream() throws java.io.IOException {
try { return minioClient.getObject(GetObjectArgs.builder().bucket(minioBucketName).object(objectKey).build()); }
catch (Exception exception) { throw new java.io.IOException("读取系统上传文件失败", exception); }
}
@Override public 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);
}
}
@Override
@Transactional(rollbackFor = Exception.class)
public void removeFolder(Long voucherId, String plateNo) {
@@ -493,7 +520,9 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
voucher.setUnRelatedWaybillCount(0);
if (Func.isEmpty(voucher.getVoucherBatchNo())) voucher.setVoucherBatchNo(nextCode());
saveOrUpdate(voucher);
voucherWaybillBatchMapper.delete(Wrappers.<VoucherWaybillBatch>lambdaQuery().eq(VoucherWaybillBatch::getVoucherId, voucher.getId()));
// 关联表存在 voucher_id + waybill_import_batch_id 唯一索引,逻辑删除会保留索引值;
// 重新上传/重新提交同一批次时必须物理清理旧关联,避免重复键冲突。
voucherWaybillBatchMapper.deletePhysicalByVoucherId(voucher.getId());
List<Map<String, Object>> batches = selectableWaybillBatchesByIds(request.getWaybillImportBatchIds());
if (batches.size() != request.getWaybillImportBatchIds().size()) throw new ServiceException("存在无效的运输批次");
List<VoucherWaybillBatch> relations = new ArrayList<>();
@@ -605,7 +634,7 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
if (pathParts.isEmpty()) {
continue;
}
String folderName = pathParts.size() > 1 ? safeArchiveSegment(pathParts.get(0)) : null;
String folderName = resolvePlateFolderName(pathParts, waybillByPlate);
String plateNo = normalizePlateNo(folderName);
String rawFileName = pathParts.get(pathParts.size() - 1);
boolean imageFile = isImageFile(rawFileName);
@@ -672,11 +701,13 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
update.setRelatedWaybillCount(relatedWaybillIds.size());
update.setUnRelatedWaybillCount(Math.max(waybills.size() - relatedWaybillIds.size(), 0));
update.setProcessStatus("处理完成");
// 处理完成后自动设置为待审核状态
update.setAuditStatus("待审核");
// 内部上传的凭证无需人工审核,MQ处理完成后直接通过;承运商上传仍进入待审核流程。
boolean internalUpload = "内部".equals(voucher.getUploadSource());
update.setAuditStatus(internalUpload ? "审核通过" : "待审核");
updateById(update);
log.info("[凭证处理] 进度 100%:处理完成 voucherId={}, voucherBatchNo={}, fileCount={}, imageCount={}, relatedWaybillCount={}, unrelatedWaybillCount={}",
voucherId, voucher.getVoucherBatchNo(), fileCount, imageCount, relatedWaybillIds.size(), Math.max(waybills.size() - relatedWaybillIds.size(), 0));
log.info("[凭证处理] 进度 100%:处理完成 voucherId={}, voucherBatchNo={}, uploadSource={}, auditStatus={}, fileCount={}, imageCount={}, relatedWaybillCount={}, unrelatedWaybillCount={}",
voucherId, voucher.getVoucherBatchNo(), voucher.getUploadSource(), update.getAuditStatus(), fileCount, imageCount,
relatedWaybillIds.size(), Math.max(waybills.size() - relatedWaybillIds.size(), 0));
} finally {
deleteTempArchive(archivePath);
}
@@ -762,7 +793,14 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
private Charset detectArchiveCharset(Path archivePath) throws Exception {
try (InputStream source = Files.newInputStream(archivePath);
ZipInputStream zipInputStream = new ZipInputStream(new BufferedInputStream(source), StandardCharsets.UTF_8)) {
while (zipInputStream.getNextEntry() != null) {
ZipEntry entry;
while ((entry = zipInputStream.getNextEntry()) != null) {
// ZipInputStream 默认会将无法按 UTF-8 解码的字节替换为 U+FFFD
// 此时不会抛出异常,必须显式检查替换字符才能回退到 GB18030。
if (entry.getName() != null && entry.getName().indexOf('\uFFFD') >= 0) {
log.warn("[凭证处理] 压缩包文件名包含 UTF-8 替换字符,回退使用 GB18030archivePath={}", archivePath);
return Charset.forName("GB18030");
}
zipInputStream.closeEntry();
}
return StandardCharsets.UTF_8;
@@ -772,6 +810,24 @@ public class VoucherManageServiceImpl extends BaseServiceImpl<VoucherManageMappe
}
}
/**
* 从压缩包路径中解析车牌目录。上传方可能在车牌目录外再包一层业务目录,
* 例如“凭证导入/桂A11111/图片.png”,不能固定取第一层目录。
*/
private String resolvePlateFolderName(List<String> pathParts, Map<String, Waybill> waybillByPlate) {
if (pathParts.size() <= 1) {
return null;
}
for (String pathPart : pathParts) {
String normalizedPart = normalizePlateNo(pathPart);
if (Func.isNotEmpty(normalizedPart) && waybillByPlate.containsKey(normalizedPart)) {
return safeArchiveSegment(pathPart);
}
}
// 未匹配车牌时仍保留最接近文件名的目录,便于前端展示和后续人工替换。
return safeArchiveSegment(pathParts.get(pathParts.size() - 2));
}
private void deleteTempArchive(Path archivePath) {
try {
Files.deleteIfExists(archivePath);
@@ -8,12 +8,21 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springblade.common.excel.ImportFailureExcelUtil;
import org.springblade.core.log.exception.ServiceException;
import org.springblade.core.mp.base.BaseServiceImpl;
import org.springblade.core.tool.api.R;
import org.springblade.core.tool.utils.BeanUtil;
import org.springblade.core.tool.utils.DateUtil;
import org.springblade.core.tool.utils.Func;
import org.springblade.core.tool.utils.WebUtil;
import org.springblade.system.cache.DictCache;
import org.springblade.system.cache.DictBizCache;
import org.springblade.system.cache.UserCache;
import org.springblade.system.pojo.entity.DictBiz;
import org.springblade.transport.excel.WaybillImportBatchExcel;
import org.springblade.transport.mapper.WaybillImportBatchMapper;
import org.springblade.transport.pojo.dto.WaybillImportBatchRequest;
import org.springblade.transport.pojo.entity.CustomerArchive;
@@ -34,6 +43,7 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
import java.io.IOException;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
@@ -76,10 +86,72 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
}
@Override
@Transactional(rollbackFor = Exception.class)
public WaybillImportBatch confirm(WaybillImportBatchRequest request) {
public void validate(WaybillImportBatchRequest request, HttpServletResponse response) {
if (Func.isEmpty(request.getRows())) throw new ServiceException("请上传至少一条运单明细");
return persist(request, STATUS_DRAFT.equals(request.getStatus()) ? STATUS_DRAFT : STATUS_COMPLETED);
// 确定导入状态
String importStatus = STATUS_DRAFT.equals(request.getStatus()) ? STATUS_DRAFT : STATUS_COMPLETED;
boolean draft = STATUS_DRAFT.equals(importStatus);
// 草稿状态不校验,直接返回成功
if (draft) {
WebUtil.renderJson(response, R.success("校验通过"));
return;
}
// 执行校验
List<Map<String, Object>> rows = request.getRows();
Map<Integer, String> validationErrors = validateImportRows(rows, importStatus);
// 如果有校验错误,导出错误明细Excel
if (!validationErrors.isEmpty()) {
List<WaybillImportBatchExcel> failureList = new ArrayList<>();
for (int i = 0; i < rows.size(); i++) {
WaybillImportBatchExcel excel = mapToExcel(rows.get(i));
String errorMessage = validationErrors.get(i);
excel.setErrorMessage(Func.isNotEmpty(errorMessage) ? errorMessage : "");
failureList.add(excel);
}
ImportFailureExcelUtil.export(response, "运单导入失败明细" + DateUtil.time(), "导入失败明细", failureList, WaybillImportBatchExcel.class);
return;
}
// 校验通过,返回成功响应
WebUtil.renderJson(response, R.success("校验通过"));
}
@Override
@Transactional(rollbackFor = Exception.class)
public void confirm(WaybillImportBatchRequest request, HttpServletResponse response) {
if (Func.isEmpty(request.getRows())) throw new ServiceException("请上传至少一条运单明细");
// 执行校验
String importStatus = STATUS_DRAFT.equals(request.getStatus()) ? STATUS_DRAFT : STATUS_COMPLETED;
boolean draft = STATUS_DRAFT.equals(importStatus);
if (!draft) {
List<Map<String, Object>> rows = request.getRows();
Map<Integer, String> validationErrors = validateImportRows(rows, importStatus);
// 如果有校验错误,导出错误明细Excel
if (!validationErrors.isEmpty()) {
List<WaybillImportBatchExcel> failureList = new ArrayList<>();
for (int i = 0; i < rows.size(); i++) {
WaybillImportBatchExcel excel = mapToExcel(rows.get(i));
String errorMessage = validationErrors.get(i);
excel.setErrorMessage(Func.isNotEmpty(errorMessage) ? errorMessage : "");
failureList.add(excel);
}
ImportFailureExcelUtil.export(response, "运单导入失败明细" + DateUtil.time(), "导入失败明细", failureList, WaybillImportBatchExcel.class);
return;
}
}
// 校验通过,执行导入
WaybillImportBatch batch = persist(request, importStatus);
// 返回成功响应
WebUtil.renderJson(response, R.success("操作成功"));
}
/** 草稿与确认导入共用落库流程,差异仅在于运单是否走校验以及是否生成应收应付明细。 */
@@ -100,20 +172,6 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
List<Map<String, Object>> rows = Func.isEmpty(request.getRows()) ? List.of() : request.getRows();
// 执行批量数据校验
Map<Integer, String> validationErrors = new TreeMap<>();
if (!draft) {
validationErrors = validateImportRows(rows, importStatus);
if (!validationErrors.isEmpty()) {
// 将所有错误信息合并抛出
StringBuilder errorMessage = new StringBuilder("数据校验失败:\n");
validationErrors.forEach((index, error) ->
errorMessage.append("").append(index + 1).append("行:").append(error).append("\n")
);
throw new ServiceException(errorMessage.toString());
}
}
List<Waybill> waybills = new ArrayList<>();
for (int index = 0; index < rows.size(); index++) {
try {
@@ -208,7 +266,8 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
waybill.setDriverId(longValue(row, "driverId", "司机ID"));
waybill.setDriverName(stringValue(row, "driverName", "司机/船长姓名", "司机/船长"));
waybill.setDriverPhone(stringValue(row, "driverPhone", "司机/船长手机号"));
waybill.setTransportType(stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式"));
waybill.setTransportType(resolveTransportTypeKey(
stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式")));
waybill.setCargoName(stringValue(row, "cargoName"));
waybill.setCargoType(stringValue(row, "cargoType"));
waybill.setSpecification(stringValue(row, "specification", "规格"));
@@ -363,7 +422,7 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
boolean isDraft = STATUS_DRAFT.equals(importStatus);
// 加载系统枚举值
List<String> transportTypeOptions = loadTransportTypeOptions();
Map<String, String> transportTypeOptions = loadTransportTypeOptions();
List<String> quantityUnitOptions = loadQuantityUnitOptions();
// 构建配载标识号和同一运单标识号的映射
@@ -490,13 +549,14 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
}
}
private void validateTransportType(Map<String, Object> row, List<String> transportTypeOptions, List<String> errors) {
private void validateTransportType(Map<String, Object> row, Map<String, String> transportTypeOptions, List<String> errors) {
String transportType = stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式");
if (Func.isEmpty(transportType)) {
return;
}
if (!transportTypeOptions.contains(transportType)) {
if (!transportTypeOptions.containsKey(transportType)
&& transportTypeOptions.keySet().stream().noneMatch(option -> option.equalsIgnoreCase(transportType))) {
errors.add("运输方式必须为系统枚举值之一");
}
}
@@ -689,16 +749,93 @@ public class WaybillImportBatchServiceImpl extends BaseServiceImpl<WaybillImport
}
}
private List<String> loadTransportTypeOptions() {
// 常见运输方式枚举值
return List.of(
"公路整车", "公路配载/零担", "铁路整车", "铁路零担",
"水路", "航空", "多式联运", "管道运输", "其他"
);
private Map<String, String> loadTransportTypeOptions() {
// 导入模板展示的是字典名称(如“公路运输”),系统内部保存的是字典键(如“road”)。
// 运输类型在不同版本中可能配置为业务字典或系统字典,因此两者均兼容。
Map<String, String> options = new HashMap<>();
try {
List<DictBiz> dictBizList = DictBizCache.getList("transport_type");
if (Func.isNotEmpty(dictBizList)) {
dictBizList.forEach(dict -> addTransportTypeOption(options, dict.getDictKey(), dict.getDictValue()));
}
} catch (Exception ignored) {
// 业务字典不可用时继续读取系统字典。
}
try {
List<org.springblade.system.pojo.entity.Dict> dictList = DictCache.getList("transport_type");
if (Func.isNotEmpty(dictList)) {
dictList.forEach(dict -> addTransportTypeOption(options, dict.getDictKey(), dict.getDictValue()));
}
} catch (Exception ignored) {
// 字典读取失败时使用默认值。
}
if (options.isEmpty()) {
List<String> defaults = List.of("公路运输", "铁路运输", "水路运输", "航空运输",
"公路整车", "公路配载/零担", "铁路整车", "铁路零担", "水路", "航空", "多式联运", "管道运输", "其他");
defaults.forEach(value -> options.put(value, value));
}
return options;
}
private void addTransportTypeOption(Map<String, String> options, String dictKey, String dictValue) {
if (Func.isNotEmpty(dictKey)) options.put(dictKey.trim(), dictKey.trim());
if (Func.isNotEmpty(dictValue)) options.put(dictValue.trim(), Func.isNotEmpty(dictKey) ? dictKey.trim() : dictValue.trim());
}
/** 将导入模板中的字典名称转换为系统保存的字典键。 */
private String resolveTransportTypeKey(String transportType) {
if (Func.isEmpty(transportType)) return transportType;
Map<String, String> options = loadTransportTypeOptions();
String normalized = transportType.trim();
String key = options.get(normalized);
if (Func.isNotEmpty(key)) return key;
return options.entrySet().stream()
.filter(entry -> entry.getKey().equalsIgnoreCase(normalized))
.map(Map.Entry::getValue)
.findFirst()
.orElse(normalized);
}
private List<String> loadQuantityUnitOptions() {
// 常见数量单位
return List.of("", "千克", "立方米", "", "", "", "", "", "", "平方米");
}
/**
* 将 Map 数据转换为 Excel 对象
*/
private WaybillImportBatchExcel mapToExcel(Map<String, Object> row) {
WaybillImportBatchExcel excel = new WaybillImportBatchExcel();
excel.setOriginalNo(stringValue(row, "originalNo"));
excel.setLoadingIdentifier(stringValue(row, "loadingIdentifier", "配载标识号"));
excel.setVehicleNo(stringValue(row, "vehicleNo"));
excel.setTransportType(stringValue(row, "transportType", "运输类型", "*运输类型", "运输方式", "*运输方式"));
excel.setDriverName(stringValue(row, "driverName", "司机/船长姓名", "司机/船长"));
excel.setDriverPhone(stringValue(row, "driverPhone", "司机/船长手机号"));
excel.setDepartureAddress(stringValue(row, "departureAddress"));
excel.setDepartureContact(stringValue(row, "departureContact"));
excel.setDeparturePhone(stringValue(row, "departurePhone"));
excel.setArrivalAddress(stringValue(row, "arrivalAddress"));
excel.setArrivalContact(stringValue(row, "arrivalContact", "收货联系人"));
excel.setArrivalPhone(stringValue(row, "arrivalPhone", "收货联系人电话"));
excel.setCargoName(stringValue(row, "cargoName"));
excel.setCargoType(stringValue(row, "cargoType"));
excel.setPackageType(stringValue(row, "packageType"));
excel.setQuantity(decimalValue(row, "quantity", "数量", "重量"));
excel.setQuantityUnit(stringValue(row, "quantityUnit", "数量单位"));
excel.setSpecification(stringValue(row, "specification", "规格"));
excel.setModel(stringValue(row, "model", "型号"));
excel.setMileage(decimalValue(row, "mileage", "里程", "里程(km)", "里程(公里)"));
excel.setUnitPrice(decimalValue(row, "unitPrice", "单价"));
excel.setFreight(decimalValue(row, "freight", "运费"));
excel.setOtherFeeTotal(decimalValue(row, "otherFeeTotal", "其他费用合计"));
excel.setFreightTotal(decimalValue(row, "freightTotal", "运费合计"));
excel.setActualStartDate(stringValue(row, "actualStartDate", "实际发货时间"));
excel.setActualEndDate(stringValue(row, "actualEndDate", "实际完成时间"));
excel.setPlanStartDate(stringValue(row, "planStartDate", "预计发货时间"));
excel.setPlanEndDate(stringValue(row, "planEndDate", "预计完成时间"));
excel.setRemark(stringValue(row, "remark"));
excel.setWaybillIdentifier(stringValue(row, "waybillIdentifier", "同一运单标识号"));
return excel;
}
}