1、调整凭证
2、调整运单
This commit is contained in:
+21
-5
@@ -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());
|
||||
|
||||
+15
@@ -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、项目和客户合同")
|
||||
|
||||
+9
@@ -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) {
|
||||
|
||||
+9
-2
@@ -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")
|
||||
|
||||
+3
@@ -85,4 +85,7 @@ public class WaybillImportBatchExcel implements Serializable {
|
||||
@ExcelProperty("同一运单标识号")
|
||||
private String waybillIdentifier;
|
||||
|
||||
/** 导入失败原因(不导出到模板,仅用于失败明细) */
|
||||
private String errorMessage;
|
||||
|
||||
}
|
||||
|
||||
+3
@@ -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);
|
||||
|
||||
+1
@@ -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);
|
||||
|
||||
+1
@@ -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);
|
||||
|
||||
+3
-5
@@ -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);
|
||||
}
|
||||
|
||||
+55
-5
@@ -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) {
|
||||
|
||||
+63
-7
@@ -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 替换字符,回退使用 GB18030,archivePath={}", 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);
|
||||
|
||||
+164
-27
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user